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:
Steve Ruiz 2024-07-15 15:04:22 +01:00 committed by GitHub
parent 7ba4040e84
commit f4ceb581dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 44 additions and 24 deletions

View file

@ -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>;

View file

@ -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)
}

View file

@ -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),
}
}
}

View file

@ -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.
*