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)
|
// (undocumented)
|
||||||
getHandleSnapGeometry(shape: Shape): HandleSnapGeometry;
|
getHandleSnapGeometry(shape: Shape): HandleSnapGeometry;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
getInterpolatedProps(startShape: Shape, endShape: Shape, t: number): Shape['props'];
|
||||||
|
// (undocumented)
|
||||||
onResize: TLOnResizeHandler<any>;
|
onResize: TLOnResizeHandler<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2044,6 +2046,7 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
||||||
abstract getGeometry(shape: Shape): Geometry2d;
|
abstract getGeometry(shape: Shape): Geometry2d;
|
||||||
getHandles?(shape: Shape): TLHandle[];
|
getHandles?(shape: Shape): TLHandle[];
|
||||||
getHandleSnapGeometry(shape: Shape): HandleSnapGeometry;
|
getHandleSnapGeometry(shape: Shape): HandleSnapGeometry;
|
||||||
|
getInterpolatedProps?(startShape: Shape, endShape: Shape, progress: number): Shape['props'];
|
||||||
hideResizeHandles: TLShapeUtilFlag<Shape>;
|
hideResizeHandles: TLShapeUtilFlag<Shape>;
|
||||||
hideRotateHandle: TLShapeUtilFlag<Shape>;
|
hideRotateHandle: TLShapeUtilFlag<Shape>;
|
||||||
hideSelectionBoundsBg: TLShapeUtilFlag<Shape>;
|
hideSelectionBoundsBg: TLShapeUtilFlag<Shape>;
|
||||||
|
|
|
@ -6985,8 +6985,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
let t: number
|
let t: number
|
||||||
|
|
||||||
interface ShapeAnimation {
|
interface ShapeAnimation {
|
||||||
partial: TLShapePartial
|
start: TLShape
|
||||||
values: { prop: string; from: number; to: number }[]
|
end: TLShape
|
||||||
}
|
}
|
||||||
|
|
||||||
const animations: ShapeAnimation[] = []
|
const animations: ShapeAnimation[] = []
|
||||||
|
@ -6996,27 +6996,18 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
partial = partials[i]
|
partial = partials[i]
|
||||||
if (!partial) continue
|
if (!partial) continue
|
||||||
|
|
||||||
result = {
|
|
||||||
partial,
|
|
||||||
values: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
const shape = this.getShape(partial.id)!
|
const shape = this.getShape(partial.id)!
|
||||||
if (!shape) continue
|
if (!shape) continue
|
||||||
|
|
||||||
// We only support animations for certain props
|
result = {
|
||||||
for (const key of ['x', 'y', 'rotation'] as const) {
|
start: structuredClone(shape),
|
||||||
if (partial[key] !== undefined && shape[key] !== partial[key]) {
|
end: applyPartialToRecordWithProps(structuredClone(shape), partial),
|
||||||
result.values.push({ prop: key, from: shape[key], to: partial[key] as number })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
animations.push(result)
|
animations.push(result)
|
||||||
this.animatingShapes.set(shape.id, animationId)
|
this.animatingShapes.set(shape.id, animationId)
|
||||||
}
|
}
|
||||||
|
|
||||||
let value: ShapeAnimation
|
|
||||||
|
|
||||||
const handleTick = (elapsed: number) => {
|
const handleTick = (elapsed: number) => {
|
||||||
remaining -= elapsed
|
remaining -= elapsed
|
||||||
|
|
||||||
|
@ -7026,8 +7017,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
(p) => p && animatingShapes.get(p.id) === animationId
|
(p) => p && animatingShapes.get(p.id) === animationId
|
||||||
)
|
)
|
||||||
if (partialsToUpdate.length) {
|
if (partialsToUpdate.length) {
|
||||||
|
// the regular update shapes also removes the shape from
|
||||||
|
// the animating shapes set
|
||||||
this.updateShapes(partialsToUpdate)
|
this.updateShapes(partialsToUpdate)
|
||||||
// update shapes also removes the shape from animating shapes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.off('tick', handleTick)
|
this.off('tick', handleTick)
|
||||||
|
@ -7042,22 +7034,22 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
|
|
||||||
let animationIdForShape: string | undefined
|
let animationIdForShape: string | undefined
|
||||||
for (let i = 0, n = animations.length; i < n; i++) {
|
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?
|
// Is the animation for this shape still active?
|
||||||
animationIdForShape = animatingShapes.get(value.partial.id)
|
animationIdForShape = animatingShapes.get(start.id)
|
||||||
if (animationIdForShape !== animationId) continue
|
if (animationIdForShape !== animationId) continue
|
||||||
|
|
||||||
// Create the update
|
|
||||||
updates.push({
|
updates.push({
|
||||||
id: value.partial.id,
|
...end,
|
||||||
type: value.partial.type,
|
x: start.x + (end.x - start.x) * t,
|
||||||
...value.values.reduce((acc, { prop, from, to }) => {
|
y: start.y + (end.y - start.y) * t,
|
||||||
acc[prop] = from + (to - from) * t
|
rotation: start.rotation + (end.rotation - start.rotation) * t,
|
||||||
return acc
|
props: this.getShapeUtil(end).getInterpolatedProps?.(start, end, t) ?? end.props,
|
||||||
}, {} as any),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The _updateShapes method does NOT remove the
|
||||||
|
// shapes from the animated shapes set
|
||||||
this._updateShapes(updates)
|
this._updateShapes(updates)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { TLBaseShape } from '@tldraw/tlschema'
|
import { TLBaseShape } from '@tldraw/tlschema'
|
||||||
|
import { lerp } from '@tldraw/utils'
|
||||||
import { Geometry2d } from '../../primitives/geometry/Geometry2d'
|
import { Geometry2d } from '../../primitives/geometry/Geometry2d'
|
||||||
import { Rectangle2d } from '../../primitives/geometry/Rectangle2d'
|
import { Rectangle2d } from '../../primitives/geometry/Rectangle2d'
|
||||||
import { HandleSnapGeometry } from '../managers/SnapManager/HandleSnaps'
|
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,
|
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
|
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.
|
* Get an array of handle models for the shape. This is an optional method.
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in a new issue