ShapeUtil.getInterpolatedProps
(#4162)
This PR adds a `getInterpolatedProps` method to the `ShapeUtil` class. It is used in `Editor.animateShapes` to allow shapes to lerp values that the editor doesn't specifically know about. ![Kapture 2024-07-13 at 09 12 48](https://github.com/user-attachments/assets/f9711aa0-278b-4a26-84d3-4b6bbe610b81) ### Change type - [x] `feature` - [x] `api` ### Test plan 1. Animate a shape's props. ```ts const shape = editor.getOnlySelectedShape() setInterval(() => { editor.animateShape( { ...shape, x: Math.random() * 500, y: Math.random() * 200, props: { w: 200 + Math.random() * 200, h: 200 + Math.random() * 200 }, }, { animation: { duration: 500 } } ) }, 1000) ``` - [ ] Unit tests (Could be done!) ### Release notes - SDK: adds `ShapeUtil.getInterpolatedProps` so that shapes can better participate in animations. --------- Co-authored-by: alex <alex@dytry.ch>
This commit is contained in:
parent
7ba4040e84
commit
f4ceb581dd
4 changed files with 44 additions and 24 deletions
|
@ -175,6 +175,8 @@ export abstract class BaseBoxShapeUtil<Shape extends TLBaseBoxShape> extends Sha
|
|||
// (undocumented)
|
||||
getHandleSnapGeometry(shape: Shape): HandleSnapGeometry;
|
||||
// (undocumented)
|
||||
getInterpolatedProps(startShape: Shape, endShape: Shape, t: number): Shape['props'];
|
||||
// (undocumented)
|
||||
onResize: TLOnResizeHandler<any>;
|
||||
}
|
||||
|
||||
|
@ -2044,6 +2046,7 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
|||
abstract getGeometry(shape: Shape): Geometry2d;
|
||||
getHandles?(shape: Shape): TLHandle[];
|
||||
getHandleSnapGeometry(shape: Shape): HandleSnapGeometry;
|
||||
getInterpolatedProps?(startShape: Shape, endShape: Shape, progress: number): Shape['props'];
|
||||
hideResizeHandles: TLShapeUtilFlag<Shape>;
|
||||
hideRotateHandle: TLShapeUtilFlag<Shape>;
|
||||
hideSelectionBoundsBg: TLShapeUtilFlag<Shape>;
|
||||
|
|
|
@ -6985,8 +6985,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
let t: number
|
||||
|
||||
interface ShapeAnimation {
|
||||
partial: TLShapePartial
|
||||
values: { prop: string; from: number; to: number }[]
|
||||
start: TLShape
|
||||
end: TLShape
|
||||
}
|
||||
|
||||
const animations: ShapeAnimation[] = []
|
||||
|
@ -6996,27 +6996,18 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
partial = partials[i]
|
||||
if (!partial) continue
|
||||
|
||||
result = {
|
||||
partial,
|
||||
values: [],
|
||||
}
|
||||
|
||||
const shape = this.getShape(partial.id)!
|
||||
if (!shape) continue
|
||||
|
||||
// We only support animations for certain props
|
||||
for (const key of ['x', 'y', 'rotation'] as const) {
|
||||
if (partial[key] !== undefined && shape[key] !== partial[key]) {
|
||||
result.values.push({ prop: key, from: shape[key], to: partial[key] as number })
|
||||
}
|
||||
result = {
|
||||
start: structuredClone(shape),
|
||||
end: applyPartialToRecordWithProps(structuredClone(shape), partial),
|
||||
}
|
||||
|
||||
animations.push(result)
|
||||
this.animatingShapes.set(shape.id, animationId)
|
||||
}
|
||||
|
||||
let value: ShapeAnimation
|
||||
|
||||
const handleTick = (elapsed: number) => {
|
||||
remaining -= elapsed
|
||||
|
||||
|
@ -7026,8 +7017,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
(p) => p && animatingShapes.get(p.id) === animationId
|
||||
)
|
||||
if (partialsToUpdate.length) {
|
||||
// the regular update shapes also removes the shape from
|
||||
// the animating shapes set
|
||||
this.updateShapes(partialsToUpdate)
|
||||
// update shapes also removes the shape from animating shapes
|
||||
}
|
||||
|
||||
this.off('tick', handleTick)
|
||||
|
@ -7042,22 +7034,22 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
|
||||
let animationIdForShape: string | undefined
|
||||
for (let i = 0, n = animations.length; i < n; i++) {
|
||||
value = animations[i]
|
||||
const { start, end } = animations[i]
|
||||
// Is the animation for this shape still active?
|
||||
animationIdForShape = animatingShapes.get(value.partial.id)
|
||||
animationIdForShape = animatingShapes.get(start.id)
|
||||
if (animationIdForShape !== animationId) continue
|
||||
|
||||
// Create the update
|
||||
updates.push({
|
||||
id: value.partial.id,
|
||||
type: value.partial.type,
|
||||
...value.values.reduce((acc, { prop, from, to }) => {
|
||||
acc[prop] = from + (to - from) * t
|
||||
return acc
|
||||
}, {} as any),
|
||||
...end,
|
||||
x: start.x + (end.x - start.x) * t,
|
||||
y: start.y + (end.y - start.y) * t,
|
||||
rotation: start.rotation + (end.rotation - start.rotation) * t,
|
||||
props: this.getShapeUtil(end).getInterpolatedProps?.(start, end, t) ?? end.props,
|
||||
})
|
||||
}
|
||||
|
||||
// The _updateShapes method does NOT remove the
|
||||
// shapes from the animated shapes set
|
||||
this._updateShapes(updates)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { TLBaseShape } from '@tldraw/tlschema'
|
||||
import { lerp } from '@tldraw/utils'
|
||||
import { Geometry2d } from '../../primitives/geometry/Geometry2d'
|
||||
import { Rectangle2d } from '../../primitives/geometry/Rectangle2d'
|
||||
import { HandleSnapGeometry } from '../managers/SnapManager/HandleSnaps'
|
||||
|
@ -27,4 +28,12 @@ export abstract class BaseBoxShapeUtil<Shape extends TLBaseBoxShape> extends Sha
|
|||
points: this.getGeometry(shape).bounds.cornersAndCenter,
|
||||
}
|
||||
}
|
||||
|
||||
override getInterpolatedProps(startShape: Shape, endShape: Shape, t: number): Shape['props'] {
|
||||
return {
|
||||
...endShape.props,
|
||||
w: lerp(startShape.props.w, endShape.props.w, t),
|
||||
h: lerp(startShape.props.h, endShape.props.h, t),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -211,6 +211,22 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
|||
*/
|
||||
backgroundComponent?(shape: Shape): any
|
||||
|
||||
/**
|
||||
* Get the interpolated props for an animating shape. This is an optional method.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* util.getInterpolatedProps?.(startShape, endShape, t)
|
||||
* ```
|
||||
*
|
||||
* @param startShape - The initial shape.
|
||||
* @param endShape - The initial shape.
|
||||
* @param progress - The normalized progress between zero (start) and 1 (end).
|
||||
* @public
|
||||
*/
|
||||
getInterpolatedProps?(startShape: Shape, endShape: Shape, progress: number): Shape['props']
|
||||
|
||||
/**
|
||||
* Get an array of handle models for the shape. This is an optional method.
|
||||
*
|
||||
|
|
Loading…
Reference in a new issue