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:
Jacob
2023-01-07 00:44:53 +01:00
committed by GitHub
parent 1e52b945ca
commit 401a79946b
5 changed files with 146 additions and 40 deletions

View File

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

View File

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

View File

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

View File

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

View File

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