mirror of
https://github.com/motion-canvas/motion-canvas.git
synced 2026-01-14 00:08:39 -05:00
feat: minor improvements
- allow to pass `SignalValue`s as properties - support composite operation - add a dispose callback for nodes - add arcLerp for Rect and Vector
This commit is contained in:
@@ -1,14 +1,26 @@
|
||||
import {Signal} from '@motion-canvas/core/lib/utils';
|
||||
import {Signal, SignalValue} from '@motion-canvas/core/lib/utils';
|
||||
import {computed, property} from '../decorators';
|
||||
import {Layout, LayoutProps} from './Layout';
|
||||
import {Rect as RectType, Vector2} from '@motion-canvas/core/lib/types';
|
||||
import Color from 'colorjs.io';
|
||||
import {drawImage} from '../utils';
|
||||
import {Rect, RectProps} from './Rect';
|
||||
|
||||
export interface ImageProps extends LayoutProps {
|
||||
src?: string;
|
||||
export interface ImageProps extends RectProps {
|
||||
src?: SignalValue<string>;
|
||||
alpha?: SignalValue<number>;
|
||||
smoothing?: SignalValue<boolean>;
|
||||
}
|
||||
|
||||
export class Image extends Layout {
|
||||
export class Image extends Rect {
|
||||
@property()
|
||||
public declare readonly src: Signal<string, this>;
|
||||
|
||||
@property(1)
|
||||
public declare readonly alpha: Signal<number, this>;
|
||||
|
||||
@property(true)
|
||||
public declare readonly smoothing: Signal<boolean, this>;
|
||||
|
||||
protected readonly image: HTMLImageElement;
|
||||
|
||||
public constructor(props: ImageProps) {
|
||||
@@ -20,10 +32,22 @@ export class Image extends Layout {
|
||||
}
|
||||
|
||||
protected override draw(context: CanvasRenderingContext2D) {
|
||||
const {width, height} = this.computedSize();
|
||||
|
||||
context.drawImage(this.image, width / -2, height / -2, width, height);
|
||||
super.draw(context);
|
||||
this.drawShape(context);
|
||||
if (this.clip()) {
|
||||
context.clip(this.getPath());
|
||||
}
|
||||
const alpha = this.alpha();
|
||||
if (alpha > 0) {
|
||||
const rect = RectType.fromSizeCentered(this.computedSize());
|
||||
context.save();
|
||||
if (alpha < 1) {
|
||||
context.globalAlpha *= alpha;
|
||||
}
|
||||
context.imageSmoothingEnabled = this.smoothing();
|
||||
drawImage(context, this.image, rect);
|
||||
context.restore();
|
||||
}
|
||||
this.drawChildren(context);
|
||||
}
|
||||
|
||||
protected override updateLayout() {
|
||||
@@ -36,6 +60,49 @@ export class Image extends Layout {
|
||||
this.image.src = this.src();
|
||||
}
|
||||
|
||||
@computed()
|
||||
protected imageCanvas(): CanvasRenderingContext2D {
|
||||
const canvas = document
|
||||
.createElement('canvas')
|
||||
.getContext('2d', {willReadFrequently: true});
|
||||
if (!canvas) {
|
||||
throw new Error('Could not create an image canvas');
|
||||
}
|
||||
|
||||
return canvas;
|
||||
}
|
||||
|
||||
@computed()
|
||||
protected imageDrawnCanvas() {
|
||||
this.applySrc();
|
||||
const context = this.imageCanvas();
|
||||
context.canvas.width = this.image.naturalWidth;
|
||||
context.canvas.height = this.image.naturalHeight;
|
||||
context.imageSmoothingEnabled = this.smoothing();
|
||||
context.drawImage(this.image, 0, 0);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
public getColorAtPoint(position: Vector2): any {
|
||||
const context = this.imageDrawnCanvas();
|
||||
const relativePosition = position.add(
|
||||
this.computedSize().vector.scale(0.5),
|
||||
);
|
||||
const data = context.getImageData(
|
||||
relativePosition.x,
|
||||
relativePosition.y,
|
||||
1,
|
||||
1,
|
||||
).data;
|
||||
|
||||
return new Color(
|
||||
'sRGB',
|
||||
[data[0] / 255, data[1] / 255, data[2] / 255],
|
||||
data[3] / 255,
|
||||
);
|
||||
}
|
||||
|
||||
protected override collectAsyncResources(deps: Promise<any>[]) {
|
||||
super.collectAsyncResources(deps);
|
||||
this.applySrc();
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import {compound, computed, Property, property} from '../decorators';
|
||||
import {
|
||||
Vector2,
|
||||
transformAngle,
|
||||
Origin,
|
||||
PossibleSpacing,
|
||||
Rect,
|
||||
Size,
|
||||
PossibleSpacing,
|
||||
Spacing,
|
||||
transformAngle,
|
||||
Vector2,
|
||||
originToOffset,
|
||||
} from '@motion-canvas/core/lib/types';
|
||||
import {isReactive, Signal, SignalValue} from '@motion-canvas/core/lib/utils';
|
||||
import {
|
||||
@@ -32,58 +34,58 @@ export interface LayoutProps extends NodeProps {
|
||||
layout?: LayoutMode;
|
||||
tagName?: keyof HTMLElementTagNameMap;
|
||||
|
||||
width?: Length;
|
||||
height?: Length;
|
||||
maxWidth?: Length;
|
||||
maxHeight?: Length;
|
||||
minWidth?: Length;
|
||||
minHeight?: Length;
|
||||
ratio?: number;
|
||||
width?: SignalValue<Length>;
|
||||
height?: SignalValue<Length>;
|
||||
maxWidth?: SignalValue<Length>;
|
||||
maxHeight?: SignalValue<Length>;
|
||||
minWidth?: SignalValue<Length>;
|
||||
minHeight?: SignalValue<Length>;
|
||||
ratio?: SignalValue<number>;
|
||||
|
||||
marginTop?: number;
|
||||
marginBottom?: number;
|
||||
marginLeft?: number;
|
||||
marginRight?: number;
|
||||
margin?: PossibleSpacing;
|
||||
marginTop?: SignalValue<number>;
|
||||
marginBottom?: SignalValue<number>;
|
||||
marginLeft?: SignalValue<number>;
|
||||
marginRight?: SignalValue<number>;
|
||||
margin?: SignalValue<PossibleSpacing>;
|
||||
|
||||
paddingTop?: number;
|
||||
paddingBottom?: number;
|
||||
paddingLeft?: number;
|
||||
paddingRight?: number;
|
||||
padding?: PossibleSpacing;
|
||||
paddingTop?: SignalValue<number>;
|
||||
paddingBottom?: SignalValue<number>;
|
||||
paddingLeft?: SignalValue<number>;
|
||||
paddingRight?: SignalValue<number>;
|
||||
padding?: SignalValue<PossibleSpacing>;
|
||||
|
||||
direction?: FlexDirection;
|
||||
basis?: FlexBasis;
|
||||
grow?: number;
|
||||
shrink?: number;
|
||||
wrap?: FlexWrap;
|
||||
direction?: SignalValue<FlexDirection>;
|
||||
basis?: SignalValue<FlexBasis>;
|
||||
grow?: SignalValue<number>;
|
||||
shrink?: SignalValue<number>;
|
||||
wrap?: SignalValue<FlexWrap>;
|
||||
|
||||
justifyContent?: FlexJustify;
|
||||
alignItems?: FlexAlign;
|
||||
rowGap?: Length;
|
||||
columnGap?: Length;
|
||||
gap?: Length;
|
||||
justifyContent?: SignalValue<FlexJustify>;
|
||||
alignItems?: SignalValue<FlexAlign>;
|
||||
rowGap?: SignalValue<Length>;
|
||||
columnGap?: SignalValue<Length>;
|
||||
gap?: SignalValue<Length>;
|
||||
|
||||
fontFamily?: string;
|
||||
fontSize?: number;
|
||||
fontStyle?: string;
|
||||
fontWeight?: number;
|
||||
lineHeight?: number;
|
||||
letterSpacing?: number;
|
||||
textWrap?: boolean;
|
||||
fontFamily?: SignalValue<string>;
|
||||
fontSize?: SignalValue<number>;
|
||||
fontStyle?: SignalValue<string>;
|
||||
fontWeight?: SignalValue<number>;
|
||||
lineHeight?: SignalValue<number>;
|
||||
letterSpacing?: SignalValue<number>;
|
||||
textWrap?: SignalValue<boolean>;
|
||||
|
||||
x?: number;
|
||||
y?: number;
|
||||
position?: Vector2;
|
||||
size?: Size;
|
||||
rotation?: number;
|
||||
offsetX?: number;
|
||||
offsetY?: number;
|
||||
offset?: Vector2;
|
||||
scaleX?: number;
|
||||
scaleY?: number;
|
||||
scale?: Vector2;
|
||||
overflow?: boolean;
|
||||
x?: SignalValue<number>;
|
||||
y?: SignalValue<number>;
|
||||
position?: SignalValue<Vector2>;
|
||||
size?: SignalValue<Size>;
|
||||
rotation?: SignalValue<number>;
|
||||
offsetX?: SignalValue<number>;
|
||||
offsetY?: SignalValue<number>;
|
||||
offset?: SignalValue<Vector2>;
|
||||
scaleX?: SignalValue<number>;
|
||||
scaleY?: SignalValue<number>;
|
||||
scale?: SignalValue<Vector2>;
|
||||
clip?: SignalValue<boolean>;
|
||||
}
|
||||
|
||||
export class Layout extends Node {
|
||||
@@ -389,9 +391,6 @@ export class Layout extends Node {
|
||||
// TODO Implement setter
|
||||
}
|
||||
|
||||
@property(false)
|
||||
public declare readonly overflow: Signal<boolean, this>;
|
||||
|
||||
@compound(['x', 'y'], Vector2)
|
||||
@property(undefined, Vector2.lerp, Vector2)
|
||||
public declare readonly position: Signal<Vector2, this>;
|
||||
@@ -428,6 +427,9 @@ export class Layout extends Node {
|
||||
}
|
||||
}
|
||||
|
||||
@property(false)
|
||||
public declare readonly clip: Signal<boolean, this>;
|
||||
|
||||
public readonly element: HTMLElement;
|
||||
public readonly styles: CSSStyleDeclaration;
|
||||
|
||||
@@ -495,6 +497,10 @@ export class Layout extends Node {
|
||||
}
|
||||
}
|
||||
|
||||
if (mode === 'root' && parentMode !== 'disabled') {
|
||||
mode = 'enabled';
|
||||
}
|
||||
|
||||
return mode;
|
||||
}
|
||||
|
||||
@@ -565,7 +571,9 @@ export class Layout extends Node {
|
||||
protected updateLayout() {
|
||||
this.applyFont();
|
||||
this.applyFlex();
|
||||
this.syncDOM();
|
||||
if (this.resolvedMode() !== 'disabled') {
|
||||
this.syncDOM();
|
||||
}
|
||||
}
|
||||
|
||||
@computed()
|
||||
@@ -597,6 +605,47 @@ export class Layout extends Node {
|
||||
return new Rect(size.vector.scale(-0.5), size);
|
||||
}
|
||||
|
||||
protected override draw(context: CanvasRenderingContext2D) {
|
||||
if (this.clip()) {
|
||||
const size = this.computedSize();
|
||||
if (size.width === 0 || size.height === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.beginPath();
|
||||
context.rect(size.width / -2, size.height / -2, size.width, size.height);
|
||||
context.closePath();
|
||||
context.clip();
|
||||
}
|
||||
|
||||
this.drawChildren(context);
|
||||
}
|
||||
|
||||
public getOriginDelta(origin: Origin) {
|
||||
const size = this.computedSize().scale(0.5);
|
||||
const offset = this.offset().mul(size.vector);
|
||||
if (origin === Origin.Middle) {
|
||||
return offset.flipped;
|
||||
}
|
||||
|
||||
const newOffset = originToOffset(origin).mul(size.vector);
|
||||
return newOffset.sub(offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the offset of this node and adjust the position to keep it in the
|
||||
* same place.
|
||||
*
|
||||
* @param offset - The new offset.
|
||||
*/
|
||||
public moveOffset(offset: Vector2) {
|
||||
const size = this.computedSize().scale(0.5);
|
||||
const oldOffset = this.offset().mul(size.vector);
|
||||
const newOffset = offset.mul(size.vector);
|
||||
this.offset(offset);
|
||||
this.position(this.position().add(newOffset).sub(oldOffset));
|
||||
}
|
||||
|
||||
protected parseValue(value: number | string | null): string {
|
||||
return value === null ? '' : value.toString();
|
||||
}
|
||||
@@ -617,7 +666,7 @@ export class Layout extends Node {
|
||||
|
||||
@computed()
|
||||
protected applyFlex() {
|
||||
const mode = this.layout();
|
||||
const mode = this.resolvedMode();
|
||||
this.element.style.position =
|
||||
mode === 'disabled' || mode === 'root' ? 'absolute' : 'relative';
|
||||
|
||||
@@ -653,8 +702,8 @@ export class Layout extends Node {
|
||||
this.element.style.flexGrow = '0';
|
||||
this.element.style.flexShrink = '0';
|
||||
} else {
|
||||
this.element.style.flexGrow = this.parsePixels(this.grow());
|
||||
this.element.style.flexShrink = this.parsePixels(this.shrink());
|
||||
this.element.style.flexGrow = this.parseValue(this.grow());
|
||||
this.element.style.flexShrink = this.parseValue(this.shrink());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,29 +1,38 @@
|
||||
import {compound, computed, initialize, property} from '../decorators';
|
||||
import {Vector2, Rect, transformScalar} from '@motion-canvas/core/lib/types';
|
||||
import {createSignal, Reference, Signal} from '@motion-canvas/core/lib/utils';
|
||||
import {
|
||||
createSignal,
|
||||
isReactive,
|
||||
Reference,
|
||||
Signal,
|
||||
SignalValue,
|
||||
} from '@motion-canvas/core/lib/utils';
|
||||
import {ComponentChild, ComponentChildren} from './types';
|
||||
import {Promisable} from '@motion-canvas/core/lib/threading';
|
||||
import {TwoDView} from '../scenes';
|
||||
import {TwoDView, use2DView} from '../scenes';
|
||||
import {TimingFunction} from '@motion-canvas/core/lib/tweening';
|
||||
import {threadable} from '@motion-canvas/core/lib/decorators';
|
||||
|
||||
export interface NodeProps {
|
||||
ref?: Reference<Node>;
|
||||
ref?: Reference<any>;
|
||||
children?: ComponentChildren;
|
||||
opacity?: number;
|
||||
blur?: number;
|
||||
brightness?: number;
|
||||
contrast?: number;
|
||||
grayscale?: number;
|
||||
hue?: number;
|
||||
invert?: number;
|
||||
saturate?: number;
|
||||
sepia?: number;
|
||||
shadowColor?: string;
|
||||
shadowBlur?: number;
|
||||
shadowOffsetX?: number;
|
||||
shadowOffsetY?: number;
|
||||
shadowOffset?: Vector2;
|
||||
cache?: boolean;
|
||||
composite?: boolean;
|
||||
opacity?: SignalValue<number>;
|
||||
blur?: SignalValue<number>;
|
||||
brightness?: SignalValue<number>;
|
||||
contrast?: SignalValue<number>;
|
||||
grayscale?: SignalValue<number>;
|
||||
hue?: SignalValue<number>;
|
||||
invert?: SignalValue<number>;
|
||||
saturate?: SignalValue<number>;
|
||||
sepia?: SignalValue<number>;
|
||||
shadowColor?: SignalValue<string>;
|
||||
shadowBlur?: SignalValue<number>;
|
||||
shadowOffsetX?: SignalValue<number>;
|
||||
shadowOffsetY?: SignalValue<number>;
|
||||
shadowOffset?: SignalValue<Vector2>;
|
||||
cache?: SignalValue<boolean>;
|
||||
composite?: SignalValue<boolean>;
|
||||
compositeOperation?: SignalValue<GlobalCompositeOperation>;
|
||||
}
|
||||
|
||||
export class Node implements Promisable<Node> {
|
||||
@@ -35,6 +44,32 @@ export class Node implements Promisable<Node> {
|
||||
@property(false)
|
||||
public declare readonly composite: Signal<boolean, this>;
|
||||
|
||||
@property('source-over')
|
||||
public declare readonly compositeOperation: Signal<
|
||||
GlobalCompositeOperation,
|
||||
this
|
||||
>;
|
||||
|
||||
private readonly compositeOverride = createSignal(0);
|
||||
|
||||
@threadable()
|
||||
protected *tweenCompositeOperation(
|
||||
value: SignalValue<GlobalCompositeOperation>,
|
||||
time: number,
|
||||
timingFunction: TimingFunction,
|
||||
) {
|
||||
const nextValue = isReactive(value) ? value() : value;
|
||||
if (nextValue === 'source-over') {
|
||||
yield* this.compositeOverride(1, time, timingFunction);
|
||||
this.compositeOverride(0);
|
||||
this.compositeOperation(nextValue);
|
||||
} else {
|
||||
this.compositeOperation(nextValue);
|
||||
this.compositeOverride(1);
|
||||
yield* this.compositeOverride(0, time, timingFunction);
|
||||
}
|
||||
}
|
||||
|
||||
@property(1)
|
||||
public declare readonly opacity: Signal<number, this>;
|
||||
|
||||
@@ -153,6 +188,7 @@ export class Node implements Promisable<Node> {
|
||||
public constructor({children, ...rest}: NodeProps) {
|
||||
initialize(this, {defaults: rest});
|
||||
this.append(children);
|
||||
use2DView()?.registerNode(this);
|
||||
}
|
||||
|
||||
@computed()
|
||||
@@ -229,9 +265,12 @@ export class Node implements Promisable<Node> {
|
||||
@computed()
|
||||
public compositeToLocal() {
|
||||
const root = this.compositeRoot();
|
||||
return root
|
||||
? root.localToWorld().multiply(this.worldToLocal())
|
||||
: new DOMMatrix();
|
||||
if (root) {
|
||||
const worldToLocal = this.worldToLocal();
|
||||
worldToLocal.m44 = 1;
|
||||
return root.localToWorld().multiply();
|
||||
}
|
||||
return new DOMMatrix();
|
||||
}
|
||||
|
||||
@computed()
|
||||
@@ -250,14 +289,14 @@ export class Node implements Promisable<Node> {
|
||||
return this;
|
||||
}
|
||||
|
||||
public remove() {
|
||||
this.moveTo(null);
|
||||
public remove(): this {
|
||||
return this.moveTo(null);
|
||||
}
|
||||
|
||||
protected moveTo(parent: Node | null) {
|
||||
protected moveTo(parent: Node | null): this {
|
||||
const current = this.parent();
|
||||
if (current === parent) {
|
||||
return;
|
||||
return this;
|
||||
}
|
||||
|
||||
if (current) {
|
||||
@@ -269,6 +308,7 @@ export class Node implements Promisable<Node> {
|
||||
}
|
||||
|
||||
this.parent(parent);
|
||||
return this;
|
||||
}
|
||||
|
||||
public removeChildren() {
|
||||
@@ -277,6 +317,10 @@ export class Node implements Promisable<Node> {
|
||||
}
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this node should be cached or not.
|
||||
*/
|
||||
@@ -284,6 +328,7 @@ export class Node implements Promisable<Node> {
|
||||
return (
|
||||
this.cache() ||
|
||||
this.opacity() < 1 ||
|
||||
this.compositeOperation() !== 'source-over' ||
|
||||
this.hasFilters() ||
|
||||
this.hasShadow()
|
||||
);
|
||||
@@ -395,6 +440,7 @@ export class Node implements Promisable<Node> {
|
||||
* @param context - The context using which the cache will be drawn.
|
||||
*/
|
||||
protected setupDrawFromCache(context: CanvasRenderingContext2D) {
|
||||
context.globalCompositeOperation = this.compositeOperation();
|
||||
context.globalAlpha = this.opacity();
|
||||
if (this.hasFilters()) {
|
||||
context.filter = this.filterString();
|
||||
@@ -417,6 +463,10 @@ export class Node implements Promisable<Node> {
|
||||
* @param context - The context to draw with.
|
||||
*/
|
||||
public render(context: CanvasRenderingContext2D) {
|
||||
if (this.absoluteOpacity() <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.save();
|
||||
this.transformContext(context);
|
||||
|
||||
@@ -424,7 +474,15 @@ export class Node implements Promisable<Node> {
|
||||
this.setupDrawFromCache(context);
|
||||
const cacheContext = this.cachedCanvas();
|
||||
const cacheRect = this.cacheRect();
|
||||
const compositeOverride = this.compositeOverride();
|
||||
context.drawImage(cacheContext.canvas, cacheRect.x, cacheRect.y);
|
||||
if (compositeOverride > 0) {
|
||||
context.save();
|
||||
context.globalAlpha *= compositeOverride;
|
||||
context.globalCompositeOperation = 'source-over';
|
||||
context.drawImage(cacheContext.canvas, cacheRect.x, cacheRect.y);
|
||||
context.restore();
|
||||
}
|
||||
} else {
|
||||
this.draw(context);
|
||||
}
|
||||
@@ -443,6 +501,10 @@ export class Node implements Promisable<Node> {
|
||||
* @param context - The context to draw with.
|
||||
*/
|
||||
protected draw(context: CanvasRenderingContext2D) {
|
||||
this.drawChildren(context);
|
||||
}
|
||||
|
||||
protected drawChildren(context: CanvasRenderingContext2D) {
|
||||
for (const child of this.children()) {
|
||||
child.render(context);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import {Signal} from '@motion-canvas/core/lib/utils';
|
||||
import {Signal, SignalValue} from '@motion-canvas/core/lib/utils';
|
||||
import {Rect as RectType} from '@motion-canvas/core/lib/types';
|
||||
import {Shape, ShapeProps} from './Shape';
|
||||
import {property} from '../decorators';
|
||||
import {drawRoundRect} from '../utils';
|
||||
|
||||
export interface RectProps extends ShapeProps {
|
||||
radius?: number;
|
||||
radius?: SignalValue<number>;
|
||||
}
|
||||
|
||||
export class Rect extends Shape {
|
||||
@@ -17,20 +19,22 @@ export class Rect extends Shape {
|
||||
protected override getPath(): Path2D {
|
||||
const path = new Path2D();
|
||||
const radius = this.radius();
|
||||
const {width, height} = this.size();
|
||||
const x = width / -2;
|
||||
const y = height / -2;
|
||||
const rect = RectType.fromSizeCentered(this.size());
|
||||
drawRoundRect(path, rect, radius);
|
||||
|
||||
if (radius > 0) {
|
||||
const maxRadius = Math.min(height / 2, width / 2, radius);
|
||||
path.moveTo(x + maxRadius, y);
|
||||
path.arcTo(x + width, y, x + width, y + height, maxRadius);
|
||||
path.arcTo(x + width, y + height, x, y + height, maxRadius);
|
||||
path.arcTo(x, y + height, x, y, maxRadius);
|
||||
path.arcTo(x, y, x + width, y, maxRadius);
|
||||
} else {
|
||||
path.rect(x, y, width, height);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
protected override getCacheRect(): RectType {
|
||||
return super.getCacheRect().expand(this.rippleSize());
|
||||
}
|
||||
|
||||
protected override getRipplePath(): Path2D {
|
||||
const path = new Path2D();
|
||||
const rippleSize = this.rippleSize();
|
||||
const radius = this.radius() + rippleSize;
|
||||
const rect = RectType.fromSizeCentered(this.size()).expand(rippleSize);
|
||||
drawRoundRect(path, rect, radius);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import {Gradient, Pattern} from '../partials';
|
||||
import {property} from '../decorators';
|
||||
import {Signal} from '@motion-canvas/core/lib/utils';
|
||||
import {CanvasStyle} from '../partials';
|
||||
import {computed, property} from '../decorators';
|
||||
import {createSignal, Signal, SignalValue} from '@motion-canvas/core/lib/utils';
|
||||
import {Rect} from '@motion-canvas/core/lib/types';
|
||||
import {Layout, LayoutProps} from './Layout';
|
||||
|
||||
export type CanvasStyle = null | string | Gradient | Pattern;
|
||||
import {threadable} from '@motion-canvas/core/lib/decorators';
|
||||
import {easeOutExpo, linear, map} from '@motion-canvas/core/lib/tweening';
|
||||
import {parseCanvasStyle} from '../utils';
|
||||
|
||||
export interface ShapeProps extends LayoutProps {
|
||||
fill?: CanvasStyle;
|
||||
stroke?: CanvasStyle;
|
||||
strokeFirst?: boolean;
|
||||
lineWidth?: number;
|
||||
lineJoin?: CanvasLineJoin;
|
||||
lineCap?: CanvasLineCap;
|
||||
lineDash?: number[];
|
||||
lineDashOffset?: number;
|
||||
fill?: SignalValue<CanvasStyle>;
|
||||
stroke?: SignalValue<CanvasStyle>;
|
||||
strokeFirst?: SignalValue<boolean>;
|
||||
lineWidth?: SignalValue<number>;
|
||||
lineJoin?: SignalValue<CanvasLineJoin>;
|
||||
lineCap?: SignalValue<CanvasLineCap>;
|
||||
lineDash?: SignalValue<number>[];
|
||||
lineDashOffset?: SignalValue<number>;
|
||||
}
|
||||
|
||||
export abstract class Shape extends Layout {
|
||||
@@ -35,29 +36,20 @@ export abstract class Shape extends Layout {
|
||||
@property(0)
|
||||
public declare readonly lineDashOffset: Signal<number, this>;
|
||||
|
||||
protected readonly rippleStrength = createSignal<number, this>(0);
|
||||
|
||||
@computed()
|
||||
protected rippleSize() {
|
||||
return easeOutExpo(this.rippleStrength(), 0, 50);
|
||||
}
|
||||
|
||||
public constructor(props: ShapeProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
protected parseCanvasStyle(
|
||||
style: CanvasStyle,
|
||||
context: CanvasRenderingContext2D,
|
||||
): string | CanvasGradient | CanvasPattern {
|
||||
if (style === null) {
|
||||
return '';
|
||||
}
|
||||
if (typeof style === 'string') {
|
||||
return style;
|
||||
}
|
||||
if (style instanceof Gradient) {
|
||||
return style.canvasGradient(context);
|
||||
}
|
||||
return style.canvasPattern(context) ?? '';
|
||||
}
|
||||
|
||||
protected applyStyle(context: CanvasRenderingContext2D) {
|
||||
context.fillStyle = this.parseCanvasStyle(this.fill(), context);
|
||||
context.strokeStyle = this.parseCanvasStyle(this.stroke(), context);
|
||||
context.fillStyle = parseCanvasStyle(this.fill(), context);
|
||||
context.strokeStyle = parseCanvasStyle(this.stroke(), context);
|
||||
context.lineWidth = this.lineWidth();
|
||||
context.lineJoin = this.lineJoin();
|
||||
context.lineCap = this.lineCap();
|
||||
@@ -66,22 +58,28 @@ export abstract class Shape extends Layout {
|
||||
}
|
||||
|
||||
protected override draw(context: CanvasRenderingContext2D) {
|
||||
this.drawShape(context);
|
||||
if (this.clip()) {
|
||||
context.clip(this.getPath());
|
||||
}
|
||||
this.drawChildren(context);
|
||||
}
|
||||
|
||||
protected drawShape(context: CanvasRenderingContext2D) {
|
||||
const path = this.getPath();
|
||||
const hasStroke = this.lineWidth() > 0 && this.stroke() !== null;
|
||||
const hasFill = this.fill() !== null;
|
||||
context.save();
|
||||
this.applyStyle(context);
|
||||
if (this.lineWidth() <= 0) {
|
||||
context.fill(path);
|
||||
} else if (this.strokeFirst()) {
|
||||
context.stroke(path);
|
||||
context.fill(path);
|
||||
this.drawRipple(context);
|
||||
if (this.strokeFirst()) {
|
||||
hasStroke && context.stroke(path);
|
||||
hasFill && context.fill(path);
|
||||
} else {
|
||||
context.fill(path);
|
||||
context.stroke(path);
|
||||
hasFill && context.fill(path);
|
||||
hasStroke && context.stroke(path);
|
||||
}
|
||||
|
||||
context.restore();
|
||||
|
||||
super.draw(context);
|
||||
}
|
||||
|
||||
protected override getCacheRect(): Rect {
|
||||
@@ -91,4 +89,26 @@ export abstract class Shape extends Layout {
|
||||
protected getPath(): Path2D {
|
||||
return new Path2D();
|
||||
}
|
||||
|
||||
protected getRipplePath(): Path2D {
|
||||
return new Path2D();
|
||||
}
|
||||
|
||||
protected drawRipple(context: CanvasRenderingContext2D) {
|
||||
const rippleStrength = this.rippleStrength();
|
||||
if (rippleStrength > 0) {
|
||||
const ripplePath = this.getRipplePath();
|
||||
context.save();
|
||||
context.globalAlpha *= map(0.54, 0, rippleStrength);
|
||||
context.fill(ripplePath);
|
||||
context.restore();
|
||||
}
|
||||
}
|
||||
|
||||
@threadable()
|
||||
public *ripple(duration = 1) {
|
||||
this.rippleStrength(0);
|
||||
yield* this.rippleStrength(1, duration, linear);
|
||||
this.rippleStrength(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import {property} from '../decorators';
|
||||
import {Signal} from '@motion-canvas/core/lib/utils';
|
||||
import {Signal, SignalValue} from '@motion-canvas/core/lib/utils';
|
||||
import {textLerp} from '@motion-canvas/core/lib/tweening';
|
||||
import {Shape, ShapeProps} from './Shape';
|
||||
import {Rect} from '@motion-canvas/core/lib/types';
|
||||
|
||||
export interface TextProps extends ShapeProps {
|
||||
children?: string;
|
||||
text?: string;
|
||||
text?: SignalValue<string>;
|
||||
}
|
||||
|
||||
export class Text extends Shape {
|
||||
@@ -80,7 +80,7 @@ export class Text extends Shape {
|
||||
context.measureText(text).fontBoundingBoxDescent;
|
||||
|
||||
if (this.lineWidth() <= 0) {
|
||||
context.fillText(text, rect.x, rect.y + rect.height / 2);
|
||||
context.fillText(text, rect.x, y);
|
||||
} else if (this.strokeFirst()) {
|
||||
context.strokeText(text, rect.x, y);
|
||||
context.fillText(text, rect.x, y);
|
||||
@@ -103,7 +103,7 @@ export class Text extends Shape {
|
||||
this.element.appendChild(document.createTextNode(word.segment));
|
||||
}
|
||||
} else {
|
||||
this.element.innerText = this.text();
|
||||
this.element.innerHTML = this.text();
|
||||
}
|
||||
|
||||
if (wrap && !Text.segmenter) {
|
||||
|
||||
@@ -122,11 +122,19 @@ export function createProperty<
|
||||
}
|
||||
|
||||
const from = getter();
|
||||
return tween(duration, value => {
|
||||
setter(
|
||||
interpolationFunction(from, unwrap(newValue), timingFunction(value)),
|
||||
);
|
||||
});
|
||||
return tween(
|
||||
duration,
|
||||
value => {
|
||||
setter(
|
||||
interpolationFunction(
|
||||
from,
|
||||
unwrap(newValue),
|
||||
timingFunction(value),
|
||||
),
|
||||
);
|
||||
},
|
||||
() => setter(wrap(newValue)),
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import type {Gradient} from './Gradient';
|
||||
import type {Pattern} from './Pattern';
|
||||
|
||||
export type FlexDirection = 'row' | 'row-reverse' | 'column' | 'column-reverse';
|
||||
|
||||
export type FlexWrap = 'nowrap' | 'wrap' | 'wrap-reverse';
|
||||
@@ -32,3 +35,5 @@ export type ResolvedLayoutMode = 'disabled' | 'enabled' | 'root' | 'pop';
|
||||
export type LayoutMode = ResolvedLayoutMode | null;
|
||||
|
||||
export type Length = number | `${number}%` | null;
|
||||
|
||||
export type CanvasStyle = null | string | Gradient | Pattern;
|
||||
|
||||
@@ -4,6 +4,15 @@ import {
|
||||
SceneRenderEvent,
|
||||
} from '@motion-canvas/core/lib/scenes';
|
||||
import {TwoDView} from './TwoDView';
|
||||
import {useScene} from '@motion-canvas/core/lib/utils';
|
||||
|
||||
export function use2DView(): TwoDView | null {
|
||||
const scene = useScene();
|
||||
if (scene instanceof TwoDScene) {
|
||||
return scene.getView();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export class TwoDScene extends GeneratorScene<TwoDView> {
|
||||
private readonly view = new TwoDView();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Layout} from '../components';
|
||||
import {Layout, Node} from '../components';
|
||||
|
||||
export class TwoDView extends Layout {
|
||||
public static frameID = 'motion-canvas-2d-frame';
|
||||
@@ -23,6 +23,8 @@ export class TwoDView extends Layout {
|
||||
this.document = frame.contentDocument ?? document;
|
||||
}
|
||||
|
||||
private registeredNodes: Node[] = [];
|
||||
|
||||
public constructor() {
|
||||
super({
|
||||
// TODO Sync with the project size
|
||||
@@ -42,6 +44,10 @@ export class TwoDView extends Layout {
|
||||
|
||||
public reset() {
|
||||
this.removeChildren();
|
||||
for (const node of this.registeredNodes) {
|
||||
node.dispose();
|
||||
}
|
||||
this.registeredNodes = [];
|
||||
this.element.innerText = '';
|
||||
}
|
||||
|
||||
@@ -89,4 +95,8 @@ export class TwoDView extends Layout {
|
||||
public override view(): TwoDView | null {
|
||||
return this;
|
||||
}
|
||||
|
||||
public registerNode(node: Node) {
|
||||
this.registeredNodes.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
84
packages/2d/src/utils/CanvasUtils.ts
Normal file
84
packages/2d/src/utils/CanvasUtils.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import {CanvasStyle, Gradient, Pattern} from '../partials';
|
||||
import {Rect} from '@motion-canvas/core/lib/types';
|
||||
|
||||
export function parseCanvasStyle(
|
||||
style: CanvasStyle,
|
||||
context: CanvasRenderingContext2D,
|
||||
): string | CanvasGradient | CanvasPattern {
|
||||
if (style === null) {
|
||||
return '';
|
||||
}
|
||||
if (typeof style === 'string') {
|
||||
return style;
|
||||
}
|
||||
if (style instanceof Gradient) {
|
||||
return style.canvasGradient(context);
|
||||
}
|
||||
if (style instanceof Pattern) {
|
||||
return style.canvasPattern(context) ?? '';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
export function drawRoundRect(
|
||||
context: CanvasRenderingContext2D | Path2D,
|
||||
rect: Rect,
|
||||
radius: number,
|
||||
) {
|
||||
if (radius > 0) {
|
||||
const maxRadius = Math.min(rect.width / 2, rect.height / 2, radius);
|
||||
context.moveTo(rect.left + maxRadius, rect.top);
|
||||
context.arcTo(rect.right, rect.top, rect.right, rect.bottom, maxRadius);
|
||||
context.arcTo(rect.right, rect.bottom, rect.left, rect.bottom, maxRadius);
|
||||
context.arcTo(rect.left, rect.bottom, rect.left, rect.top, maxRadius);
|
||||
context.arcTo(rect.left, rect.top, rect.right, rect.top, maxRadius);
|
||||
} else {
|
||||
drawRect(context, rect);
|
||||
}
|
||||
}
|
||||
|
||||
export function drawRect(
|
||||
context: CanvasRenderingContext2D | Path2D,
|
||||
rect: Rect,
|
||||
) {
|
||||
context.rect(rect.x, rect.y, rect.width, rect.height);
|
||||
}
|
||||
|
||||
export function fillRect(context: CanvasRenderingContext2D, rect: Rect) {
|
||||
context.fillRect(rect.x, rect.y, rect.width, rect.height);
|
||||
}
|
||||
|
||||
export function drawImage(
|
||||
context: CanvasRenderingContext2D,
|
||||
image: CanvasImageSource,
|
||||
destination: Rect,
|
||||
): void;
|
||||
export function drawImage(
|
||||
context: CanvasRenderingContext2D,
|
||||
image: CanvasImageSource,
|
||||
source: Rect,
|
||||
destination: Rect,
|
||||
): void;
|
||||
export function drawImage(
|
||||
context: CanvasRenderingContext2D,
|
||||
image: CanvasImageSource,
|
||||
first: Rect,
|
||||
second?: Rect,
|
||||
): void {
|
||||
if (second) {
|
||||
context.drawImage(
|
||||
image,
|
||||
first.x,
|
||||
first.y,
|
||||
first.width,
|
||||
first.height,
|
||||
second.x,
|
||||
second.y,
|
||||
second.width,
|
||||
second.height,
|
||||
);
|
||||
} else {
|
||||
context.drawImage(image, first.x, first.y, first.width, first.height);
|
||||
}
|
||||
}
|
||||
1
packages/2d/src/utils/index.ts
Normal file
1
packages/2d/src/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './CanvasUtils';
|
||||
@@ -1,5 +1,5 @@
|
||||
import Color from 'colorjs.io';
|
||||
import type {Rect, Vector2, PossibleSpacing, Spacing, Size} from '../types';
|
||||
import {Vector2} from '../types';
|
||||
|
||||
export interface InterpolationFunction<T, Rest extends unknown[] = unknown[]> {
|
||||
(from: T, to: T, value: number, ...args: Rest): T;
|
||||
@@ -189,3 +189,27 @@ export function clampRemap(
|
||||
|
||||
return clamp(fromOut, toOut, remappedValue);
|
||||
}
|
||||
|
||||
export function arcLerp(
|
||||
value: number,
|
||||
reverse: boolean,
|
||||
ratio: number,
|
||||
): Vector2 {
|
||||
let flip = reverse;
|
||||
if (ratio > 1) {
|
||||
ratio = 1 / ratio;
|
||||
} else {
|
||||
flip = !flip;
|
||||
}
|
||||
|
||||
const normalized = flip ? Math.acos(1 - value) : Math.asin(value);
|
||||
const radians = map(normalized, map(0, Math.PI / 2, value), ratio);
|
||||
|
||||
let xValue = Math.sin(radians);
|
||||
let yValue = 1 - Math.cos(radians);
|
||||
if (reverse) {
|
||||
[xValue, yValue] = [yValue, xValue];
|
||||
}
|
||||
|
||||
return new Vector2(xValue, yValue);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type {Vector2} from './Vector';
|
||||
import type {Size} from './Size';
|
||||
import {Vector2} from './Vector';
|
||||
|
||||
export enum Center {
|
||||
Vertical = 1,
|
||||
@@ -49,3 +48,36 @@ export function flipOrigin(
|
||||
|
||||
return origin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the given origin to a vector representing its offset.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const bottomRight = originToOffset(Origin.TopRight);
|
||||
* // bottomRight = {x: 1, y: -1}
|
||||
* ```
|
||||
*
|
||||
* @param origin - The origin to convert.
|
||||
*/
|
||||
export function originToOffset(origin: Origin | Direction): Vector2 {
|
||||
if (origin === Origin.Middle) {
|
||||
return Vector2.zero;
|
||||
}
|
||||
|
||||
let x = 0;
|
||||
if (origin & Direction.Left) {
|
||||
x = -1;
|
||||
} else if (origin & Direction.Right) {
|
||||
x = 1;
|
||||
}
|
||||
|
||||
let y = 0;
|
||||
if (origin & Direction.Top) {
|
||||
y = -1;
|
||||
} else if (origin & Direction.Bottom) {
|
||||
y = 1;
|
||||
}
|
||||
|
||||
return new Vector2(x, y);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Vector2} from './Vector';
|
||||
import {Size} from './Size';
|
||||
import {map} from '../tweening';
|
||||
import {arcLerp, map} from '../tweening';
|
||||
|
||||
export type SerializedRect = {
|
||||
x: number;
|
||||
@@ -21,15 +21,54 @@ export class Rect {
|
||||
public width = 0;
|
||||
public height = 0;
|
||||
|
||||
public static lerp(from: Rect, to: Rect, value: number): Rect {
|
||||
public static lerp(
|
||||
from: Rect,
|
||||
to: Rect,
|
||||
value: number | Vector2 | Rect,
|
||||
): Rect {
|
||||
let valueX;
|
||||
let valueY;
|
||||
let valueWidth;
|
||||
let valueHeight;
|
||||
if (typeof value === 'number') {
|
||||
valueX = valueY = valueWidth = valueHeight = value;
|
||||
} else if (value instanceof Vector2) {
|
||||
valueX = valueWidth = value.x;
|
||||
valueY = valueHeight = value.y;
|
||||
} else {
|
||||
valueX = value.x;
|
||||
valueY = value.y;
|
||||
valueWidth = value.width;
|
||||
valueHeight = value.height;
|
||||
}
|
||||
|
||||
return new Rect(
|
||||
map(from.x, to.x, value),
|
||||
map(from.y, to.y, value),
|
||||
map(from.width, to.width, value),
|
||||
map(from.height, to.height, value),
|
||||
map(from.x, to.x, valueX),
|
||||
map(from.y, to.y, valueY),
|
||||
map(from.width, to.width, valueWidth),
|
||||
map(from.height, to.height, valueHeight),
|
||||
);
|
||||
}
|
||||
|
||||
public static arcLerp(
|
||||
from: Rect,
|
||||
to: Rect,
|
||||
value: number,
|
||||
reverse = false,
|
||||
ratio?: number,
|
||||
) {
|
||||
ratio ??=
|
||||
(from.position.sub(to.position).ctg +
|
||||
from.size.vector.sub(to.size.vector).ctg) /
|
||||
2;
|
||||
|
||||
return Rect.lerp(from, to, arcLerp(value, reverse, ratio));
|
||||
}
|
||||
|
||||
public static fromSizeCentered(size: Size): Rect {
|
||||
return new Rect(-size.width / 2, -size.height / 2, size.width, size.height);
|
||||
}
|
||||
|
||||
public static fromPoints(...points: Vector2[]): Rect {
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
@@ -88,6 +127,40 @@ export class Rect {
|
||||
return new Size(this.width, this.height);
|
||||
}
|
||||
|
||||
public get left() {
|
||||
return this.x;
|
||||
}
|
||||
|
||||
public set left(value: number) {
|
||||
this.width = this.right - value;
|
||||
this.x = value;
|
||||
}
|
||||
|
||||
public get right() {
|
||||
return this.x + this.width;
|
||||
}
|
||||
|
||||
public set right(value: number) {
|
||||
this.width = value - this.x;
|
||||
}
|
||||
|
||||
public get top() {
|
||||
return this.y;
|
||||
}
|
||||
|
||||
public set top(value: number) {
|
||||
this.width = this.bottom - value;
|
||||
this.y = value;
|
||||
}
|
||||
|
||||
public get bottom() {
|
||||
return this.y + this.height;
|
||||
}
|
||||
|
||||
public set bottom(value: number) {
|
||||
this.height = value - this.y;
|
||||
}
|
||||
|
||||
public get topLeft() {
|
||||
return this.position;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Size} from './Size';
|
||||
import {Rect} from './Rect';
|
||||
import {map} from '../tweening';
|
||||
import {arcLerp, map} from '../tweening';
|
||||
import {Direction, Origin} from './Origin';
|
||||
|
||||
export type SerializedVector2 = {
|
||||
@@ -20,9 +20,35 @@ export class Vector2 {
|
||||
public y = 0;
|
||||
|
||||
public static readonly zero = new Vector2();
|
||||
public static readonly one = new Vector2(1, 1);
|
||||
public static readonly right = new Vector2(1, 0);
|
||||
public static readonly left = new Vector2(-1, 0);
|
||||
public static readonly up = new Vector2(0, 1);
|
||||
public static readonly down = new Vector2(0, -1);
|
||||
|
||||
public static lerp(from: Vector2, to: Vector2, value: number) {
|
||||
return new Vector2(map(from.x, to.x, value), map(from.y, to.y, value));
|
||||
public static lerp(from: Vector2, to: Vector2, value: number | Vector2) {
|
||||
let valueX;
|
||||
let valueY;
|
||||
|
||||
if (typeof value === 'number') {
|
||||
valueX = valueY = value;
|
||||
} else {
|
||||
valueX = value.x;
|
||||
valueY = value.y;
|
||||
}
|
||||
|
||||
return new Vector2(map(from.x, to.x, valueX), map(from.y, to.y, valueY));
|
||||
}
|
||||
|
||||
public static arcLerp(
|
||||
from: Vector2,
|
||||
to: Vector2,
|
||||
value: number,
|
||||
reverse?: boolean,
|
||||
ratio?: number,
|
||||
) {
|
||||
ratio ??= from.sub(to).ctg;
|
||||
return Vector2.lerp(from, to, arcLerp(value, reverse, ratio));
|
||||
}
|
||||
|
||||
public static fromOrigin(origin: Origin | Direction) {
|
||||
@@ -47,6 +73,10 @@ export class Vector2 {
|
||||
return position;
|
||||
}
|
||||
|
||||
public static fromScalar(value: number): Vector2 {
|
||||
return new Vector2(value, value);
|
||||
}
|
||||
|
||||
public static fromRadians(radians: number) {
|
||||
return new Vector2(Math.cos(radians), Math.sin(radians));
|
||||
}
|
||||
@@ -59,10 +89,30 @@ export class Vector2 {
|
||||
return Vector2.magnitude(this.x, this.y);
|
||||
}
|
||||
|
||||
public get normalized(): Vector2 {
|
||||
return this.scale(1 / Vector2.magnitude(this.x, this.y));
|
||||
}
|
||||
|
||||
public get safe(): Vector2 {
|
||||
return new Vector2(isNaN(this.x) ? 0 : this.x, isNaN(this.y) ? 0 : this.y);
|
||||
}
|
||||
|
||||
public get flipped(): Vector2 {
|
||||
return new Vector2(-this.x, -this.y);
|
||||
}
|
||||
|
||||
public get perpendicular(): Vector2 {
|
||||
return new Vector2(this.y, -this.x);
|
||||
}
|
||||
|
||||
public get radians() {
|
||||
return Math.atan2(this.y, this.x);
|
||||
}
|
||||
|
||||
public get ctg(): number {
|
||||
return this.x / this.y;
|
||||
}
|
||||
|
||||
public constructor();
|
||||
public constructor(from: PossibleVector2);
|
||||
public constructor(x: number, y: number);
|
||||
@@ -119,6 +169,14 @@ export class Vector2 {
|
||||
return new Vector2(this.x + vector.x, this.y + vector.y);
|
||||
}
|
||||
|
||||
public sub(vector: Vector2) {
|
||||
return new Vector2(this.x - vector.x, this.y - vector.y);
|
||||
}
|
||||
|
||||
public dot(vector: Vector2): number {
|
||||
return this.x * vector.x + this.y * vector.y;
|
||||
}
|
||||
|
||||
public addX(value: number) {
|
||||
return new Vector2(this.x + value, this.y);
|
||||
}
|
||||
|
||||
@@ -154,15 +154,19 @@ export function createSignal<TValue, TReturn = void>(
|
||||
}
|
||||
|
||||
const from = get();
|
||||
return tween(duration, v => {
|
||||
set(
|
||||
interpolationFunction(
|
||||
from,
|
||||
isReactive(value) ? value() : value,
|
||||
timingFunction(v),
|
||||
),
|
||||
);
|
||||
});
|
||||
return tween(
|
||||
duration,
|
||||
v => {
|
||||
set(
|
||||
interpolationFunction(
|
||||
from,
|
||||
isReactive(value) ? value() : value,
|
||||
timingFunction(v),
|
||||
),
|
||||
);
|
||||
},
|
||||
() => set(value),
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export type Reference<TValue> = TValue extends (config: {
|
||||
ref: infer TReference;
|
||||
ref?: infer TReference;
|
||||
}) => void
|
||||
? TReference
|
||||
: {value: TValue};
|
||||
|
||||
Reference in New Issue
Block a user