mirror of
https://github.com/motion-canvas/motion-canvas.git
synced 2026-01-11 06:48:12 -05:00
feat(2d): unify desired sizes (#118)
All nodes whose sizes are governed by custom rules (`Image`, `Video`, etc) now use a unified `desiredSize()` method to specify said rules.
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
import {computed, initial, property} from '../decorators';
|
||||
import {
|
||||
createComputedAsync,
|
||||
createSignal,
|
||||
Signal,
|
||||
SignalValue,
|
||||
useLogger,
|
||||
} from '@motion-canvas/core/lib/utils';
|
||||
import {Shape, ShapeProps} from './Shape';
|
||||
import {CodeTree, parse, diff, ready, MorphToken} from 'code-fns';
|
||||
import {CodeTree, parse, diff, ready, MorphToken, Token} from 'code-fns';
|
||||
import {
|
||||
clampRemap,
|
||||
easeInOutSine,
|
||||
@@ -14,6 +15,8 @@ import {
|
||||
tween,
|
||||
} from '@motion-canvas/core/lib/tweening';
|
||||
import {threadable} from '@motion-canvas/core/lib/decorators';
|
||||
import {Length} from '../partials';
|
||||
import {SerializedVector2, Vector2} from '@motion-canvas/core/lib/types';
|
||||
|
||||
export interface CodeProps extends ShapeProps {
|
||||
children?: CodeTree;
|
||||
@@ -30,11 +33,15 @@ export class CodeBlock extends Shape {
|
||||
@property()
|
||||
public declare readonly code: Signal<CodeTree, this>;
|
||||
|
||||
private progress: number | null = null;
|
||||
private progress = createSignal<number | null>(null);
|
||||
private diffed: MorphToken[] | null = null;
|
||||
|
||||
@computed()
|
||||
protected parsed() {
|
||||
if (!CodeBlock.initialized()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return parse(this.code());
|
||||
}
|
||||
|
||||
@@ -45,6 +52,55 @@ export class CodeBlock extends Shape {
|
||||
}
|
||||
}
|
||||
|
||||
@computed()
|
||||
protected characterSize() {
|
||||
this.requestFontUpdate();
|
||||
const context = this.cacheCanvas();
|
||||
context.save();
|
||||
this.applyStyle(context);
|
||||
context.font = this.styles.font;
|
||||
const width = context.measureText('X').width;
|
||||
context.restore();
|
||||
|
||||
return new Vector2(width, parseFloat(this.styles.lineHeight));
|
||||
}
|
||||
|
||||
protected override desiredSize(): SerializedVector2<Length> {
|
||||
const custom = super.desiredSize();
|
||||
const tokensSize = this.getTokensSize(this.parsed());
|
||||
return {
|
||||
x: custom.x ?? tokensSize.x,
|
||||
y: custom.y ?? tokensSize.y,
|
||||
};
|
||||
}
|
||||
|
||||
protected getTokensSize(tokens: Token[]) {
|
||||
const size = this.characterSize();
|
||||
let maxWidth = 0;
|
||||
let height = size.height;
|
||||
let width = 0;
|
||||
|
||||
for (const token of tokens) {
|
||||
for (let i = 0; i < token.code.length; i++) {
|
||||
if (token.code[i] === '\n') {
|
||||
if (width > maxWidth) {
|
||||
maxWidth = width;
|
||||
}
|
||||
width = 0;
|
||||
height += size.height;
|
||||
} else {
|
||||
width += size.width;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (width > maxWidth) {
|
||||
maxWidth = width;
|
||||
}
|
||||
|
||||
return {x: maxWidth, y: height};
|
||||
}
|
||||
|
||||
protected override collectAsyncResources(): void {
|
||||
super.collectAsyncResources();
|
||||
CodeBlock.initialized();
|
||||
@@ -57,14 +113,40 @@ export class CodeBlock extends Shape {
|
||||
timingFunction: TimingFunction,
|
||||
) {
|
||||
if (typeof code === 'function') throw new Error();
|
||||
this.progress = 0;
|
||||
if (!CodeBlock.initialized()) return;
|
||||
|
||||
const autoWidth = this.customWidth() === null;
|
||||
const autoHeight = this.customHeight() === null;
|
||||
const fromSize = this.size();
|
||||
const toSize = this.getTokensSize(parse(code));
|
||||
|
||||
const beginning = 0.2;
|
||||
const ending = 0.8;
|
||||
|
||||
this.progress(0);
|
||||
this.diffed = diff(this.code(), code);
|
||||
yield* tween(
|
||||
time,
|
||||
value => (this.progress = timingFunction(value)),
|
||||
value => {
|
||||
const progress = timingFunction(value);
|
||||
const remapped = clampRemap(beginning, ending, 0, 1, progress);
|
||||
this.progress(progress);
|
||||
if (autoWidth) {
|
||||
this.customWidth(easeInOutSine(remapped, fromSize.x, toSize.x));
|
||||
}
|
||||
if (autoHeight) {
|
||||
this.customHeight(easeInOutSine(remapped, fromSize.y, toSize.y));
|
||||
}
|
||||
},
|
||||
() => {
|
||||
this.progress = null;
|
||||
this.progress(null);
|
||||
this.diffed = null;
|
||||
if (autoWidth) {
|
||||
this.customWidth(null);
|
||||
}
|
||||
if (autoHeight) {
|
||||
this.customHeight(null);
|
||||
}
|
||||
this.code(code);
|
||||
},
|
||||
);
|
||||
@@ -77,10 +159,13 @@ export class CodeBlock extends Shape {
|
||||
this.applyStyle(context);
|
||||
context.font = this.styles.font;
|
||||
context.textBaseline = 'top';
|
||||
const lh = parseInt(this.styles.fontSize) * 1.5;
|
||||
|
||||
const lh = parseFloat(this.styles.lineHeight);
|
||||
const w = context.measureText('X').width;
|
||||
if (this.progress == null) {
|
||||
const size = this.computedSize();
|
||||
const progress = this.progress();
|
||||
|
||||
context.translate(size.x / -2, size.y / -2);
|
||||
if (progress == null) {
|
||||
const parsed = this.parsed();
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
@@ -106,31 +191,25 @@ export class CodeBlock extends Shape {
|
||||
context.fillStyle = token.color as string;
|
||||
if (token.morph === 'delete') {
|
||||
context.save();
|
||||
const opacity = clampRemap(
|
||||
0,
|
||||
beginning + overlap,
|
||||
1,
|
||||
0,
|
||||
this.progress,
|
||||
);
|
||||
const opacity = clampRemap(0, beginning + overlap, 1, 0, progress);
|
||||
context.globalAlpha = opacity;
|
||||
context.fillText(token.code, token.from![0] * w, token.from![1] * lh);
|
||||
context.restore();
|
||||
} else if (token.morph === 'create') {
|
||||
context.save();
|
||||
const opacity = clampRemap(ending - overlap, 1, 0, 1, this.progress);
|
||||
const opacity = clampRemap(ending - overlap, 1, 0, 1, progress);
|
||||
context.globalAlpha = opacity;
|
||||
context.fillText(token.code, token.to![0] * w, token.to![1] * lh);
|
||||
context.restore();
|
||||
} else if (token.morph === 'retain') {
|
||||
const progress = clampRemap(beginning, ending, 0, 1, this.progress);
|
||||
const remapped = clampRemap(beginning, ending, 0, 1, progress);
|
||||
const x = easeInOutSine(
|
||||
progress,
|
||||
remapped,
|
||||
token.from![0] * w,
|
||||
token.to![0] * w,
|
||||
);
|
||||
const y = easeInOutSine(
|
||||
progress,
|
||||
remapped,
|
||||
token.from![1] * lh,
|
||||
token.to![1] * lh,
|
||||
);
|
||||
|
||||
@@ -4,9 +4,15 @@ import {
|
||||
SignalValue,
|
||||
} from '@motion-canvas/core/lib/utils';
|
||||
import {computed, initial, property} from '../decorators';
|
||||
import {Color, Rect as RectType, Vector2} from '@motion-canvas/core/lib/types';
|
||||
import {
|
||||
Color,
|
||||
Rect as RectType,
|
||||
SerializedVector2,
|
||||
Vector2,
|
||||
} from '@motion-canvas/core/lib/types';
|
||||
import {drawImage} from '../utils';
|
||||
import {Rect, RectProps} from './Rect';
|
||||
import {Length} from '../partials';
|
||||
|
||||
export interface ImageProps extends RectProps {
|
||||
src?: SignalValue<string>;
|
||||
@@ -32,6 +38,19 @@ export class Image extends Rect {
|
||||
super(props);
|
||||
}
|
||||
|
||||
protected override desiredSize(): SerializedVector2<Length> {
|
||||
const custom = super.desiredSize();
|
||||
if (custom.x === null && custom.y === null) {
|
||||
const image = this.image();
|
||||
return {
|
||||
x: image.naturalWidth,
|
||||
y: image.naturalHeight,
|
||||
};
|
||||
}
|
||||
|
||||
return custom;
|
||||
}
|
||||
|
||||
@computed()
|
||||
protected image(): HTMLImageElement {
|
||||
const src = this.src();
|
||||
|
||||
@@ -217,7 +217,7 @@ export class Layout extends Node {
|
||||
timingFunction: TimingFunction,
|
||||
interpolationFunction: InterpolationFunction<Length>,
|
||||
): ThreadGenerator {
|
||||
const width = this.customWidth();
|
||||
const width = this.desiredSize().x;
|
||||
const lock = typeof width !== 'number' || typeof value !== 'number';
|
||||
let from: number;
|
||||
if (lock) {
|
||||
@@ -257,7 +257,7 @@ export class Layout extends Node {
|
||||
timingFunction: TimingFunction,
|
||||
interpolationFunction: InterpolationFunction<Length>,
|
||||
): ThreadGenerator {
|
||||
const height = this.customHeight();
|
||||
const height = this.desiredSize().y;
|
||||
const lock = typeof height !== 'number' || typeof value !== 'number';
|
||||
|
||||
let from: number;
|
||||
@@ -297,7 +297,7 @@ export class Layout extends Node {
|
||||
@property()
|
||||
protected declare readonly customHeight: Signal<Length, this>;
|
||||
@computed()
|
||||
protected customSize(): SerializedVector2<Length> {
|
||||
protected desiredSize(): SerializedVector2<Length> {
|
||||
return {
|
||||
x: this.customWidth(),
|
||||
y: this.customHeight(),
|
||||
@@ -311,7 +311,7 @@ export class Layout extends Node {
|
||||
timingFunction: TimingFunction,
|
||||
interpolationFunction: InterpolationFunction<Vector2>,
|
||||
): ThreadGenerator {
|
||||
const size = this.customSize();
|
||||
const size = this.desiredSize();
|
||||
let from: Vector2;
|
||||
if (typeof size.x !== 'number' || typeof size.y !== 'number') {
|
||||
from = this.size();
|
||||
@@ -633,8 +633,9 @@ export class Layout extends Node {
|
||||
protected applyFlex() {
|
||||
this.element.style.position = this.isLayoutRoot() ? 'absolute' : 'relative';
|
||||
|
||||
this.element.style.width = this.parseLength(this.customWidth());
|
||||
this.element.style.height = this.parseLength(this.customHeight());
|
||||
const size = this.desiredSize();
|
||||
this.element.style.width = this.parseLength(size.x);
|
||||
this.element.style.height = this.parseLength(size.y);
|
||||
this.element.style.maxWidth = this.parseLength(this.maxWidth());
|
||||
this.element.style.minWidth = this.parseLength(this.minWidth());
|
||||
this.element.style.maxHeight = this.parseLength(this.maxHeight());
|
||||
|
||||
@@ -3,7 +3,7 @@ import {Node} from './Node';
|
||||
import {computed, initial, property} from '../decorators';
|
||||
import {arc, lineTo, moveTo, resolveCanvasStyle} from '../utils';
|
||||
import {Signal, SignalValue} from '@motion-canvas/core/lib/utils';
|
||||
import {Rect, Vector2} from '@motion-canvas/core/lib/types';
|
||||
import {Rect, SerializedVector2, Vector2} from '@motion-canvas/core/lib/types';
|
||||
import {clamp} from '@motion-canvas/core/lib/tweening';
|
||||
import {Length} from '../partials';
|
||||
import {Layout} from './Layout';
|
||||
@@ -65,18 +65,8 @@ export class Line extends Shape {
|
||||
@property()
|
||||
public declare readonly arrowSize: Signal<number, this>;
|
||||
|
||||
public getCustomWidth(): Length {
|
||||
return this.childrenRect().width;
|
||||
}
|
||||
public setCustomWidth() {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
public getCustomHeight(): Length {
|
||||
return this.childrenRect().height;
|
||||
}
|
||||
public setCustomHeight() {
|
||||
// do nothing
|
||||
protected override desiredSize(): SerializedVector2<Length> {
|
||||
return this.childrenRect().size;
|
||||
}
|
||||
|
||||
public constructor(props: LineProps) {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import {Rect as RectType} from '@motion-canvas/core/lib/types';
|
||||
import {
|
||||
Rect as RectType,
|
||||
SerializedVector2,
|
||||
} from '@motion-canvas/core/lib/types';
|
||||
import {drawImage} from '../utils';
|
||||
import {computed, initial, property} from '../decorators';
|
||||
import {
|
||||
@@ -11,6 +14,7 @@ import {
|
||||
import {PlaybackState} from '@motion-canvas/core';
|
||||
import {clamp} from '@motion-canvas/core/lib/tweening';
|
||||
import {Rect, RectProps} from './Rect';
|
||||
import {Length} from '../partials';
|
||||
|
||||
export interface VideoProps extends RectProps {
|
||||
src?: SignalValue<string>;
|
||||
@@ -48,6 +52,19 @@ export class Video extends Rect {
|
||||
super(props);
|
||||
}
|
||||
|
||||
protected override desiredSize(): SerializedVector2<Length> {
|
||||
const custom = super.desiredSize();
|
||||
if (custom.x === null && custom.y === null) {
|
||||
const image = this.video();
|
||||
return {
|
||||
x: image.videoWidth,
|
||||
y: image.videoHeight,
|
||||
};
|
||||
}
|
||||
|
||||
return custom;
|
||||
}
|
||||
|
||||
@computed()
|
||||
public completion(): number {
|
||||
return this.clampTime(this.time()) / this.video().duration;
|
||||
|
||||
Reference in New Issue
Block a user