mirror of
https://github.com/motion-canvas/motion-canvas.git
synced 2026-04-22 03:00:03 -04:00
feat: surfaces
This commit is contained in:
11
package-lock.json
generated
11
package-lock.json
generated
@@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"canvas": "^2.9.0",
|
||||
"konva": "^8.3.2",
|
||||
"mix-color": "^1.1.2",
|
||||
"ts-loader": "^9.2.6",
|
||||
"typescript": "^4.5.5",
|
||||
"url-loader": "^4.1.1",
|
||||
@@ -2423,6 +2424,11 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/mix-color": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/mix-color/-/mix-color-1.1.2.tgz",
|
||||
"integrity": "sha512-dl0x92sQ+cMM37GVo5MpLJfPbLxFhyqTrf6paYp/k+1rRPK1aPPPI7xajEI+GtU+eWnCsAW2Bg1q0rH1gLbI7w=="
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
@@ -6007,6 +6013,11 @@
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"mix-color": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/mix-color/-/mix-color-1.1.2.tgz",
|
||||
"integrity": "sha512-dl0x92sQ+cMM37GVo5MpLJfPbLxFhyqTrf6paYp/k+1rRPK1aPPPI7xajEI+GtU+eWnCsAW2Bg1q0rH1gLbI7w=="
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
|
||||
@@ -9,11 +9,12 @@
|
||||
"prepare": "npm run build",
|
||||
"build": "tsc",
|
||||
"test:serve": "node ./tools/serve.mjs ./test/player.ts",
|
||||
"test:render": "node ./tools/serve.mjs ./test/render.ts"
|
||||
"test:render": "node ./tools/render.mjs ./test/render.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"canvas": "^2.9.0",
|
||||
"konva": "^8.3.2",
|
||||
"mix-color": "^1.1.2",
|
||||
"ts-loader": "^9.2.6",
|
||||
"typescript": "^4.5.5",
|
||||
"url-loader": "^4.1.1",
|
||||
|
||||
@@ -3,11 +3,15 @@ import {Project} from './Project';
|
||||
export function Player(factory: () => Project) {
|
||||
const project = factory();
|
||||
project.start();
|
||||
const interval = setInterval(() => {
|
||||
if (project.next()) {
|
||||
project.start();
|
||||
project.next();
|
||||
// clearInterval(interval);
|
||||
const run = () => {
|
||||
try {
|
||||
if (project.next()) {
|
||||
project.start();
|
||||
project.next();
|
||||
}
|
||||
requestAnimationFrame(run);
|
||||
} catch (e) {
|
||||
}
|
||||
}, 1000 / project.framesPerSeconds);
|
||||
}
|
||||
run();
|
||||
}
|
||||
|
||||
@@ -5,6 +5,13 @@ export const Renderer =
|
||||
(factory: () => Project) => (createCanvas: any, Image: any) => {
|
||||
Util.createCanvasElement = () => {
|
||||
const node = createCanvas(300, 300);
|
||||
const monkey = node.getContext;
|
||||
node.getContext = (type: string, options: Record<any, any>) => {
|
||||
return monkey.call(node, type, {
|
||||
...options,
|
||||
pixelFormat: 'RGB30',
|
||||
});
|
||||
};
|
||||
if (!node['style']) {
|
||||
node['style'] = {};
|
||||
}
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
export * from './move';
|
||||
export * from './show';
|
||||
export * from './show';
|
||||
export * from './pop';
|
||||
export * from './surfaceTransition';
|
||||
@@ -2,20 +2,37 @@ import {Node} from 'konva/lib/Node';
|
||||
import {Vector2d} from 'konva/lib/types';
|
||||
import {tween} from '../tweening';
|
||||
|
||||
export function move(node: Node, position: Vector2d): Generator;
|
||||
export function move(
|
||||
node: Node,
|
||||
position: Vector2d,
|
||||
absolute?: boolean,
|
||||
): Generator;
|
||||
export function move(
|
||||
node: Node,
|
||||
positionX: number,
|
||||
positionY: number,
|
||||
absolute?: boolean,
|
||||
): Generator;
|
||||
export function move(
|
||||
node: Node,
|
||||
positionX: number | Vector2d,
|
||||
positionY?: number,
|
||||
arg0: number | Vector2d,
|
||||
arg1?: number | boolean,
|
||||
arg2?: boolean,
|
||||
): Generator {
|
||||
let delta: Vector2d;
|
||||
let absolute: boolean;
|
||||
if (typeof arg0 === 'number') {
|
||||
delta = {x: arg0, y: <number>arg1};
|
||||
absolute = arg2;
|
||||
} else {
|
||||
delta = arg0;
|
||||
absolute = <boolean>arg1;
|
||||
}
|
||||
|
||||
const positionFrom = node.position();
|
||||
const positionTo =
|
||||
typeof positionX === 'number' ? {x: positionX, y: positionY} : positionX;
|
||||
let positionTo = absolute
|
||||
? delta
|
||||
: {x: delta.x + positionFrom.x, y: delta.y + positionFrom.y};
|
||||
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(positionFrom.x - positionTo.x, 2) +
|
||||
|
||||
16
src/animations/pop.ts
Normal file
16
src/animations/pop.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import {Node} from 'konva/lib/Node';
|
||||
|
||||
export function pop<T extends Node>(node: T): [T, () => void] {
|
||||
const clone: T = node.clone();
|
||||
clone.moveTo(node.getLayer());
|
||||
clone.position(node.absolutePosition());
|
||||
node.hide();
|
||||
|
||||
return [
|
||||
clone,
|
||||
() => {
|
||||
clone.destroy();
|
||||
node.show();
|
||||
},
|
||||
];
|
||||
}
|
||||
83
src/animations/surfaceTransition.ts
Normal file
83
src/animations/surfaceTransition.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import {Surface} from '../components/Surface';
|
||||
import {TimeTween, tween} from '../tweening';
|
||||
|
||||
export function surfaceTransition(
|
||||
fromSurface: Surface,
|
||||
toSurface: Surface,
|
||||
inverse?: boolean,
|
||||
) {
|
||||
const from = fromSurface.getSurfaceData();
|
||||
const fromPos = fromSurface.getPosition();
|
||||
const to = toSurface.getSurfaceData();
|
||||
const toPos = toSurface.getPosition();
|
||||
|
||||
const fromDelta = fromSurface.calculateOriginDelta(toSurface.origin());
|
||||
const fromNewPos = {
|
||||
x: fromPos.x + fromDelta.x,
|
||||
y: fromPos.y + fromDelta.y,
|
||||
};
|
||||
|
||||
const toDelta = toSurface.calculateOriginDelta(fromSurface.origin());
|
||||
const toNewPos = {
|
||||
x: toPos.x + toDelta.x,
|
||||
y: toPos.y + toDelta.y,
|
||||
};
|
||||
|
||||
fromSurface.show();
|
||||
toSurface.hide();
|
||||
|
||||
let check = true;
|
||||
return tween(0.6, value => {
|
||||
const distance = value.easeInOutQuint(0, Math.PI / 2);
|
||||
let xValue = Math.sin(distance);
|
||||
let yValue = 1 - Math.cos(distance);
|
||||
if (inverse) {
|
||||
[xValue, yValue] = [yValue, xValue];
|
||||
}
|
||||
|
||||
if (value.value > 1 / 3) {
|
||||
if (check) {
|
||||
toSurface.setOverride(true);
|
||||
toSurface.show();
|
||||
fromSurface.hide();
|
||||
fromSurface.setSurfaceData(from);
|
||||
fromSurface.setPosition(fromPos);
|
||||
fromSurface.getChild().opacity(1);
|
||||
fromSurface.setOverride(false);
|
||||
}
|
||||
|
||||
toSurface.setSurfaceData({
|
||||
...from,
|
||||
x: value.linear(from.x, to.x, xValue),
|
||||
y: value.linear(from.y, to.y, yValue),
|
||||
width: value.linear(from.width, to.width, xValue),
|
||||
height: value.linear(from.height, to.height, yValue),
|
||||
radius: value.easeInOutCubic(from.radius, to.radius),
|
||||
color: value.color(from.color, to.color, value.easeInOutQuint()),
|
||||
});
|
||||
toSurface.setPosition({
|
||||
x: value.linear(fromNewPos.x, toPos.x, xValue),
|
||||
y: value.linear(fromNewPos.y, toPos.y, yValue),
|
||||
});
|
||||
toSurface
|
||||
.getChild()
|
||||
.opacity(Math.max(TimeTween.map(0, 1, value.linear(-1 / 2, 1)), 0));
|
||||
} else {
|
||||
fromSurface.setOverride(true);
|
||||
fromSurface.setSurfaceData({
|
||||
...from,
|
||||
x: value.linear(from.x, to.x, xValue),
|
||||
y: value.linear(from.y, to.y, yValue),
|
||||
width: value.linear(from.width, to.width, xValue),
|
||||
height: value.linear(from.height, to.height, yValue),
|
||||
radius: value.easeInOutCubic(from.radius, to.radius),
|
||||
color: value.color(from.color, to.color, value.easeInOutQuint()),
|
||||
});
|
||||
fromSurface.setPosition({
|
||||
x: value.linear(fromPos.x, toNewPos.x, xValue),
|
||||
y: value.linear(fromPos.y, toNewPos.y, yValue),
|
||||
});
|
||||
fromSurface.getChild().opacity(TimeTween.map(1, 0, value.linear(0, 3)));
|
||||
}
|
||||
});
|
||||
}
|
||||
117
src/components/Layout.ts
Normal file
117
src/components/Layout.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import {Group} from 'konva/lib/Group';
|
||||
import {Container, ContainerConfig} from 'konva/lib/Container';
|
||||
import {Center} from 'MC/types/Origin';
|
||||
import {GetSet, IRect} from 'konva/lib/types';
|
||||
import {_registerNode} from 'konva/lib/Global';
|
||||
import {Factory} from 'konva/lib/Factory';
|
||||
import {ISurfaceChild, Surface, SURFACE_CHANGE_EVENT, SurfaceData} from 'MC/components/Surface';
|
||||
import {Shape} from 'konva/lib/Shape';
|
||||
import {getNumberValidator} from 'konva/lib/Validators';
|
||||
|
||||
export interface LayoutConfig extends ContainerConfig {
|
||||
direction?: Center;
|
||||
padding?: number;
|
||||
}
|
||||
|
||||
export class Layout extends Group implements ISurfaceChild {
|
||||
public direction: GetSet<Center, this>;
|
||||
public padding: GetSet<number, this>;
|
||||
|
||||
constructor(config?: LayoutConfig) {
|
||||
super(config);
|
||||
this.recalculate();
|
||||
}
|
||||
|
||||
private handleChildChange = () => this.recalculate();
|
||||
|
||||
getSurfaceData(): SurfaceData {
|
||||
return {
|
||||
...this.getClientRect(),
|
||||
color: 'rgba(36, 36, 36, 1)',
|
||||
radius: 20,
|
||||
};
|
||||
}
|
||||
|
||||
add(...children: (Group | Shape)[]): this {
|
||||
super.add(...children);
|
||||
for (const child of children) {
|
||||
child.on(SURFACE_CHANGE_EVENT, this.handleChildChange);
|
||||
}
|
||||
this.recalculate();
|
||||
return this;
|
||||
}
|
||||
|
||||
removeChildren(): this {
|
||||
for (const child of this.children) {
|
||||
child.off(SURFACE_CHANGE_EVENT, this.handleChildChange);
|
||||
}
|
||||
return super.removeChildren();
|
||||
}
|
||||
|
||||
private recalculate() {
|
||||
if (!this.children) return;
|
||||
|
||||
const padding = this.attrs.padding ?? 20;
|
||||
let height = 0;
|
||||
let width = 0;
|
||||
if (this.attrs.direction === Center.Horizontal) {
|
||||
for (const child of this.children) {
|
||||
const rect = child.getClientRect();
|
||||
const offset = child instanceof Surface ? child.calculateOffset() : {x: 0, y: 0};
|
||||
height = Math.max(height, rect.height);
|
||||
width += rect.width / 2;
|
||||
child.position({x: width, y: offset.y});
|
||||
width += rect.width / 2 + padding;
|
||||
}
|
||||
this.offsetX((width - padding) / 2);
|
||||
} else {
|
||||
for (const child of this.children) {
|
||||
const rect = child.getClientRect();
|
||||
const offset = child instanceof Surface ? child.calculateOffset() : {x: 0, y: 0};
|
||||
width = Math.max(width, rect.width);
|
||||
height += rect.height / 2;
|
||||
child.position({x: offset.x, y: height});
|
||||
height += rect.height / 2 + padding;
|
||||
}
|
||||
this.offsetY((height - padding) / 2);
|
||||
}
|
||||
|
||||
this.fire(SURFACE_CHANGE_EVENT, undefined, true);
|
||||
}
|
||||
|
||||
getClientRect(config?: {
|
||||
skipTransform?: boolean;
|
||||
skipShadow?: boolean;
|
||||
skipStroke?: boolean;
|
||||
relativeTo?: Container;
|
||||
}): IRect {
|
||||
const padding = this.attrs.padding ?? 20;
|
||||
const rect = super.getClientRect(config);
|
||||
rect.x -= padding;
|
||||
rect.y -= padding;
|
||||
rect.width += padding * 2;
|
||||
rect.height += padding * 2;
|
||||
|
||||
return rect;
|
||||
}
|
||||
}
|
||||
|
||||
Layout.prototype.className = 'Layout';
|
||||
_registerNode(Layout);
|
||||
|
||||
Factory.addGetterSetter(
|
||||
Layout,
|
||||
'direction',
|
||||
Center.Vertical,
|
||||
undefined,
|
||||
// @ts-ignore
|
||||
Layout.prototype.recalculate,
|
||||
);
|
||||
Factory.addGetterSetter(
|
||||
Layout,
|
||||
'padding',
|
||||
20,
|
||||
getNumberValidator(),
|
||||
// @ts-ignore
|
||||
Layout.prototype.recalculate,
|
||||
);
|
||||
@@ -1,118 +0,0 @@
|
||||
import {Group} from 'konva/lib/Group';
|
||||
import {Rect} from 'konva/lib/shapes/Rect';
|
||||
import {Text} from 'konva/lib/shapes/Text';
|
||||
import {Vector2d} from 'konva/lib/types';
|
||||
import {ContainerConfig} from 'konva/lib/Container';
|
||||
import {Origin, Direction} from '../types/Origin';
|
||||
import {tween} from '../tweening';
|
||||
|
||||
export class ObjectNode extends Group {
|
||||
public readonly box: Rect;
|
||||
public readonly text: Text;
|
||||
private _origin: Origin = Origin.Middle;
|
||||
|
||||
public constructor(config?: ContainerConfig) {
|
||||
super(config);
|
||||
|
||||
this.box = new Rect({
|
||||
x: 0,
|
||||
y: 0,
|
||||
height: 80,
|
||||
cornerRadius: 8,
|
||||
fill: '#242424',
|
||||
});
|
||||
|
||||
this.text = new Text({
|
||||
x: 0,
|
||||
y: 0,
|
||||
fontSize: 28,
|
||||
align: 'center',
|
||||
fontFamily: 'JetBrains Mono',
|
||||
fill: 'white',
|
||||
});
|
||||
|
||||
this.add(this.box);
|
||||
this.add(this.text);
|
||||
}
|
||||
|
||||
public setOrigin(origin: Origin): this {
|
||||
if (this._origin === origin) return;
|
||||
this._origin = origin;
|
||||
this.recalculate();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public setText(text: string): this {
|
||||
this.text.text(text);
|
||||
this.recalculate();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public *animateText(text: string) {
|
||||
const from = this.recalculateValues();
|
||||
const previousText = this.text.text();
|
||||
this.text.text(text);
|
||||
const to = this.recalculateValues();
|
||||
this.text.text(previousText);
|
||||
|
||||
yield* tween(0.3, value => {
|
||||
this.box.width(value.easeInOutCubic(from.width, to.width));
|
||||
this.text.width(value.easeInOutCubic(from.width, to.width));
|
||||
this.box.offset(value.vector2d(from.box, to.box, value.easeInOutCubic()));
|
||||
this.text.offset(
|
||||
value.vector2d(from.text, to.text, value.easeInOutCubic()),
|
||||
);
|
||||
this.text.text(value.text(previousText, text, value.easeInOutCubic()));
|
||||
});
|
||||
|
||||
this.recalculate();
|
||||
}
|
||||
|
||||
private recalculate() {
|
||||
const values = this.recalculateValues();
|
||||
|
||||
this.box.offset(values.box);
|
||||
this.text.offset(values.text);
|
||||
this.box.width(values.width);
|
||||
this.text.width(values.width);
|
||||
}
|
||||
|
||||
private recalculateValues() {
|
||||
const previousWidth = this.text.width();
|
||||
this.text.width(null);
|
||||
const width = this.text.getTextWidth();
|
||||
const height = this.text.height();
|
||||
const boxWidth = Math.ceil((width + 80) / 20) * 20;
|
||||
this.text.width(previousWidth);
|
||||
|
||||
const box: Vector2d = {x: 0, y: 0};
|
||||
const text: Vector2d = {x: 0, y: 0};
|
||||
|
||||
if (this._origin & Direction.Left) {
|
||||
box.x = text.x = boxWidth;
|
||||
} else if (this._origin & Direction.Right) {
|
||||
box.x = text.x = 0;
|
||||
} else {
|
||||
box.x = text.x = boxWidth / 2;
|
||||
}
|
||||
|
||||
if (this._origin & Direction.Top) {
|
||||
box.y = 80;
|
||||
text.y = height / 2 + 40;
|
||||
} else if (this._origin & Direction.Bottom) {
|
||||
box.y = 0;
|
||||
text.y = height / 2 - 40;
|
||||
} else {
|
||||
box.y = 40;
|
||||
text.y = height / 2;
|
||||
}
|
||||
|
||||
return {
|
||||
width: boxWidth,
|
||||
box,
|
||||
text,
|
||||
};
|
||||
}
|
||||
}
|
||||
275
src/components/Surface.ts
Normal file
275
src/components/Surface.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
import {Group} from 'konva/lib/Group';
|
||||
import {ContainerConfig} from 'konva/lib/Container';
|
||||
import {Rect} from 'konva/lib/shapes/Rect';
|
||||
import {Shape} from 'konva/lib/Shape';
|
||||
import {GetSet, IRect, Vector2d} from 'konva/lib/types';
|
||||
import {SceneContext} from 'konva/lib/Context';
|
||||
import {Direction, Origin} from 'MC/types/Origin';
|
||||
import {Factory} from 'konva/lib/Factory';
|
||||
import {_registerNode} from 'konva/lib/Global';
|
||||
import {tween} from 'MC/tweening';
|
||||
import {parseColor} from 'mix-color';
|
||||
|
||||
export const SURFACE_CHANGE_EVENT = 'surfaceChange';
|
||||
|
||||
export type SurfaceData = IRect & {
|
||||
radius: number;
|
||||
color: string;
|
||||
};
|
||||
|
||||
function roundRect(
|
||||
ctx: CanvasRenderingContext2D | SceneContext,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
radius: number,
|
||||
) {
|
||||
if (width < 2 * radius) radius = width / 2;
|
||||
if (height < 2 * radius) radius = height / 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + radius, y);
|
||||
ctx.arcTo(x + width, y, x + width, y + height, radius);
|
||||
ctx.arcTo(x + width, y + height, x, y + height, radius);
|
||||
ctx.arcTo(x, y + height, x, y, radius);
|
||||
ctx.arcTo(x, y, x + width, y, radius);
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
export interface ISurfaceChild {
|
||||
getSurfaceData(): SurfaceData;
|
||||
}
|
||||
|
||||
function isSurfaceChild(
|
||||
node: Shape | Group | ISurfaceChild,
|
||||
): node is (Shape | Group) & ISurfaceChild {
|
||||
return 'getSurfaceData' in node;
|
||||
}
|
||||
|
||||
export interface SurfaceConfig extends ContainerConfig {
|
||||
origin?: Origin;
|
||||
}
|
||||
|
||||
export class Surface extends Group {
|
||||
private box: Rect;
|
||||
private ripple: Rect;
|
||||
private child: (Shape | Group) & ISurfaceChild;
|
||||
private override: boolean;
|
||||
private surfaceData: SurfaceData;
|
||||
private maskData: SurfaceData;
|
||||
|
||||
public constructor(config?: SurfaceConfig) {
|
||||
super(config);
|
||||
this.add(
|
||||
new Rect({
|
||||
x: 0,
|
||||
y: 0,
|
||||
fill: '#242424',
|
||||
name: 'ripple',
|
||||
visible: false,
|
||||
}),
|
||||
);
|
||||
this.add(
|
||||
new Rect({
|
||||
x: 0,
|
||||
y: 0,
|
||||
fill: '#242424',
|
||||
name: 'box',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
add(...children: (Shape | Group)[]): this {
|
||||
super.add(...children);
|
||||
const child = children.find(isSurfaceChild);
|
||||
const ripple = children.find<Rect>((child): child is Rect =>
|
||||
child.hasName('ripple'),
|
||||
);
|
||||
const box = children.find<Rect>((child): child is Rect =>
|
||||
child.hasName('box'),
|
||||
);
|
||||
|
||||
if (child) {
|
||||
if (this.child) {
|
||||
this.child.off(SURFACE_CHANGE_EVENT, this.handleSurfaceChange);
|
||||
}
|
||||
this.child = child;
|
||||
this.child.on(SURFACE_CHANGE_EVENT, this.handleSurfaceChange);
|
||||
this.handleSurfaceChange();
|
||||
}
|
||||
|
||||
if (box) {
|
||||
this.box?.destroy();
|
||||
this.box = box;
|
||||
}
|
||||
if (ripple) {
|
||||
this.ripple?.destroy();
|
||||
this.ripple = ripple;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public *doRipple() {
|
||||
if (this.override) return;
|
||||
const opaque = parseColor(this.surfaceData.color);
|
||||
this.ripple.show();
|
||||
this.ripple
|
||||
.offsetX(this.surfaceData.width / 2)
|
||||
.offsetY(this.surfaceData.height / 2)
|
||||
.width(this.surfaceData.width)
|
||||
.height(this.surfaceData.height)
|
||||
.cornerRadius(this.surfaceData.radius)
|
||||
.fill(`rgba(${opaque.r}, ${opaque.g}, ${opaque.b}, ${0.5})`);
|
||||
|
||||
yield* tween(1, value => {
|
||||
const width = this.surfaceData.width + value.easeOutExpo(0, 100);
|
||||
const height = this.surfaceData.height + value.easeOutExpo(0, 100);
|
||||
const radius = this.surfaceData.radius + value.easeOutExpo(0, 50);
|
||||
|
||||
this.ripple
|
||||
.offsetX(width / 2)
|
||||
.offsetY(height / 2)
|
||||
.width(width)
|
||||
.height(height)
|
||||
.cornerRadius(radius)
|
||||
.fill(
|
||||
`rgba(${opaque.r}, ${opaque.g}, ${opaque.b}, ${value.linear(
|
||||
0.5,
|
||||
0,
|
||||
)})`,
|
||||
);
|
||||
});
|
||||
|
||||
this.ripple.hide();
|
||||
}
|
||||
|
||||
public getChild(): (Shape | Group) & ISurfaceChild {
|
||||
return this.child;
|
||||
}
|
||||
|
||||
public setOverride(value: boolean) {
|
||||
this.override = value;
|
||||
this.clipFunc(value ? this.drawMask : null);
|
||||
if (!value) this.handleSurfaceChange();
|
||||
}
|
||||
|
||||
public getSurfaceData(): SurfaceData {
|
||||
return this.surfaceData;
|
||||
}
|
||||
|
||||
public setSurfaceData(data: SurfaceData) {
|
||||
if (!this.override) return;
|
||||
const offset = this.offsetY();
|
||||
const newOffset = this.calculateOffset(data);
|
||||
const scale = Math.min(1, data.width / this.surfaceData.width);
|
||||
|
||||
this.maskData = data;
|
||||
this.maskData.x = -data.width / 2;
|
||||
this.maskData.y = -data.height / 2 + offset - newOffset.y;
|
||||
|
||||
this.offsetX(newOffset.x);
|
||||
this.child.scaleX(scale);
|
||||
this.child.scaleY(scale);
|
||||
|
||||
this.child.position({
|
||||
x: 0,
|
||||
y: (-this.surfaceData.height * (1 - scale)) / 2,
|
||||
});
|
||||
|
||||
this.box
|
||||
.offsetX(data.width / 2)
|
||||
.offsetY(data.height / 2 - offset + newOffset.y)
|
||||
// .absolutePosition(this.surfaceData)
|
||||
.width(data.width)
|
||||
.height(data.height)
|
||||
.cornerRadius(data.radius)
|
||||
.fill(data.color);
|
||||
}
|
||||
|
||||
private handleSurfaceChange = () => {
|
||||
if (this.override) return;
|
||||
this.maskData = this.surfaceData = this.child.getSurfaceData();
|
||||
this.updateSurface();
|
||||
};
|
||||
|
||||
private updateSurface() {
|
||||
this.box
|
||||
.offsetX(this.surfaceData.width / 2)
|
||||
.offsetY(this.surfaceData.height / 2)
|
||||
// .absolutePosition(this.surfaceData)
|
||||
.width(this.surfaceData.width)
|
||||
.height(this.surfaceData.height)
|
||||
.cornerRadius(this.surfaceData.radius)
|
||||
.fill(this.surfaceData.color);
|
||||
|
||||
this.offset(this.calculateOffset(this.surfaceData));
|
||||
}
|
||||
|
||||
public handleOriginChange() {
|
||||
if (!this.surfaceData || this.override) return;
|
||||
const previousOffset = this.offset();
|
||||
const nextOffset = this.calculateOffset(this.surfaceData);
|
||||
this.offset(nextOffset);
|
||||
this.move({
|
||||
x: -previousOffset.x + nextOffset.x,
|
||||
y: -previousOffset.y + nextOffset.y,
|
||||
});
|
||||
}
|
||||
|
||||
public calculateOriginDelta(newOrigin: Origin): Vector2d {
|
||||
const offset = this.calculateOffset(this.surfaceData);
|
||||
const nextOffset = this.calculateOffset(this.surfaceData, newOrigin);
|
||||
|
||||
return {
|
||||
x: -offset.x + nextOffset.x,
|
||||
y: -offset.y + nextOffset.y,
|
||||
};
|
||||
}
|
||||
|
||||
public calculateOffset(surfaceData?: SurfaceData, origin?: Origin): Vector2d {
|
||||
surfaceData ??= this.surfaceData;
|
||||
origin ??= this.attrs.origin ?? Origin.Middle;
|
||||
const width = surfaceData.width / 2;
|
||||
const height = surfaceData.height / 2;
|
||||
const offset: Vector2d = {x: 0, y: 0};
|
||||
|
||||
if (origin & Direction.Left) {
|
||||
offset.x = -width;
|
||||
} else if (origin & Direction.Right) {
|
||||
offset.x = width;
|
||||
}
|
||||
|
||||
if (origin & Direction.Top) {
|
||||
offset.y = -height;
|
||||
} else if (origin & Direction.Bottom) {
|
||||
offset.y = height;
|
||||
}
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
private drawMask(ctx: CanvasRenderingContext2D) {
|
||||
roundRect(
|
||||
ctx,
|
||||
this.maskData.x,
|
||||
this.maskData.y,
|
||||
this.maskData.width,
|
||||
this.maskData.height,
|
||||
this.maskData.radius,
|
||||
);
|
||||
}
|
||||
|
||||
origin: GetSet<Origin, this>;
|
||||
}
|
||||
|
||||
Surface.prototype.className = 'Surface';
|
||||
_registerNode(Surface);
|
||||
|
||||
Factory.addGetterSetter(
|
||||
Surface,
|
||||
'origin',
|
||||
Origin.Middle,
|
||||
undefined,
|
||||
Surface.prototype.handleOriginChange,
|
||||
);
|
||||
111
src/components/TextContent.ts
Normal file
111
src/components/TextContent.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import {Text, TextConfig} from 'konva/lib/shapes/Text';
|
||||
import {GetSet, Vector2d} from 'konva/lib/types';
|
||||
import {tween} from '../tweening';
|
||||
import {ISurfaceChild, SURFACE_CHANGE_EVENT, SurfaceData} from './Surface';
|
||||
import {ShapeGetClientRectConfig} from 'konva/lib/Shape';
|
||||
import {_registerNode} from 'konva/lib/Global';
|
||||
import {Factory} from 'konva/lib/Factory';
|
||||
import {getNumberValidator} from 'konva/lib/Validators';
|
||||
|
||||
export interface TextContentConfig extends TextConfig {
|
||||
minWidth?: number;
|
||||
}
|
||||
|
||||
export class TextContent extends Text implements ISurfaceChild {
|
||||
private contentOffset = 0;
|
||||
|
||||
public constructor(config?: TextContentConfig) {
|
||||
super({
|
||||
...config,
|
||||
x: 0,
|
||||
y: 0,
|
||||
height: 80,
|
||||
fontSize: 28,
|
||||
verticalAlign: 'middle',
|
||||
fontFamily: 'JetBrains Mono',
|
||||
fill: 'rgba(30, 30, 28, 0.87)',
|
||||
});
|
||||
|
||||
this.recalculate();
|
||||
}
|
||||
|
||||
getSurfaceData(): SurfaceData {
|
||||
return {
|
||||
...this.getClientRect(),
|
||||
radius: 40,
|
||||
color: '#c0b3a3',
|
||||
};
|
||||
}
|
||||
|
||||
public setText(text: string): this {
|
||||
super.setText(text);
|
||||
this.recalculate();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public *animateText(text: string) {
|
||||
const fromText = this.text();
|
||||
const from = this.recalculateValues(fromText);
|
||||
const to = this.recalculateValues(text);
|
||||
|
||||
yield* tween(0.3, value => {
|
||||
this.text(value.text(fromText, text, value.easeInOutCubic()));
|
||||
this.width(value.easeInOutCubic(from.width, to.width));
|
||||
this.offset(
|
||||
value.vector2d(from.offset, to.offset, value.easeInOutCubic()),
|
||||
);
|
||||
this.contentOffset = value.easeInOutCubic(from.contentOffset, to.contentOffset);
|
||||
this.fire(SURFACE_CHANGE_EVENT, undefined, true);
|
||||
});
|
||||
|
||||
this.recalculate();
|
||||
}
|
||||
|
||||
private recalculate() {
|
||||
const values = this.recalculateValues(this.text());
|
||||
|
||||
this.offset(values.offset);
|
||||
this.width(values.width);
|
||||
this.contentOffset = values.contentOffset;
|
||||
this.fire(SURFACE_CHANGE_EVENT, undefined, true);
|
||||
}
|
||||
|
||||
private recalculateValues(text: string) {
|
||||
const minWidth = this.attrs.minWidth ?? 0;
|
||||
const size = this.measureSize(text);
|
||||
const textWidth = Math.max(minWidth, size.width);
|
||||
const boxWidth = Math.ceil((textWidth + 80) / 20) * 20;
|
||||
|
||||
return {
|
||||
width: boxWidth,
|
||||
offset: <Vector2d>{x: textWidth / 2, y: 38},
|
||||
contentOffset: (boxWidth - textWidth) / 2,
|
||||
};
|
||||
}
|
||||
|
||||
getClientRect(config?: ShapeGetClientRectConfig): {
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
} {
|
||||
const rect = super.getClientRect(config);
|
||||
rect.x -= this.contentOffset;
|
||||
return rect;
|
||||
}
|
||||
|
||||
public minWidth: GetSet<number, this>;
|
||||
}
|
||||
|
||||
TextContent.prototype.className = 'TextContent';
|
||||
_registerNode(TextContent);
|
||||
|
||||
Factory.addGetterSetter(
|
||||
TextContent,
|
||||
'minWidth',
|
||||
0,
|
||||
getNumberValidator(),
|
||||
// @ts-ignore
|
||||
TextContent.prototype.recalculate,
|
||||
);
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from './Arrow';
|
||||
export * from './Connection';
|
||||
export * from './ObjectNode';
|
||||
export * from './TextContent';
|
||||
@@ -1,4 +1,5 @@
|
||||
import {Vector2d} from 'konva/lib/types';
|
||||
import mixColor from "mix-color";
|
||||
|
||||
export class TimeTween {
|
||||
public constructor(public value: number) {}
|
||||
@@ -11,18 +12,20 @@ export class TimeTween {
|
||||
return TimeTween.map(from, to, value);
|
||||
}
|
||||
|
||||
public easeOutExpo(from = 0, to = 1) {
|
||||
const value = this.value === 1 ? 1 : 1 - Math.pow(2, -10 * this.value);
|
||||
public easeOutExpo(from = 0, to = 1, value?: number) {
|
||||
value ??= this.value;
|
||||
value = value === 1 ? 1 : 1 - Math.pow(2, -10 * value);
|
||||
return TimeTween.map(from, to, value);
|
||||
}
|
||||
|
||||
public easeInExpo(from = 0, to = 1) {
|
||||
const value = this.value === 0 ? 0 : Math.pow(2, 10 * this.value - 10);
|
||||
public easeInExpo(from = 0, to = 1, value?: number) {
|
||||
value ??= this.value;
|
||||
value = value === 0 ? 0 : Math.pow(2, 10 * value - 10);
|
||||
return TimeTween.map(from, to, value);
|
||||
}
|
||||
|
||||
public linear(from = 0, to = 1) {
|
||||
return TimeTween.map(from, to, this.value);
|
||||
public linear(from = 0, to = 1, value?: number) {
|
||||
return TimeTween.map(from, to, value ?? this.value);
|
||||
}
|
||||
|
||||
public easeInCirc(from = 0, to = 1) {
|
||||
@@ -84,6 +87,10 @@ export class TimeTween {
|
||||
}
|
||||
}
|
||||
|
||||
public color(from: string, to: string, value?: number) {
|
||||
return mixColor(from, to, value ?? this.value);
|
||||
}
|
||||
|
||||
public vector2d(from: Vector2d, to: Vector2d, value?: number) {
|
||||
return {
|
||||
x: TimeTween.map(from.x, to.x, value ?? this.value),
|
||||
|
||||
@@ -45,7 +45,7 @@ function build(entry) {
|
||||
},
|
||||
output: {
|
||||
filename: `result.js`,
|
||||
path: tmpDir,
|
||||
path: output,
|
||||
library: {
|
||||
type: 'commonjs-module',
|
||||
},
|
||||
@@ -65,10 +65,11 @@ function build(entry) {
|
||||
await build(projectFile);
|
||||
const setup = await import(
|
||||
/* webpackIgnore: true */
|
||||
`file://${path.join(tmpDir, 'result.js')}`
|
||||
`file://${path.join(output, 'result.js')}`
|
||||
);
|
||||
|
||||
let totalSize = 0;
|
||||
const startTime = Date.now();
|
||||
const project = setup.default.default(createCanvas, Image);
|
||||
project.start();
|
||||
while (!project.next()) {
|
||||
@@ -85,7 +86,7 @@ function build(entry) {
|
||||
process.stdout.write(
|
||||
`Frame: ${name}, Size: ${Math.round(size)} kB, Total: ${Math.round(
|
||||
totalSize,
|
||||
)} kB`,
|
||||
)} kB, Elapsed: ${Math.round((Date.now() - startTime) / 1000)}`,
|
||||
);
|
||||
}
|
||||
})().catch(console.error);
|
||||
|
||||
Reference in New Issue
Block a user