feat: general improvements

This commit is contained in:
aarthificial
2022-06-03 01:52:27 +02:00
parent b21a44a0b2
commit 320ccede3d
21 changed files with 404 additions and 138 deletions

33
package-lock.json generated
View File

@@ -12,6 +12,7 @@
"@types/dom-webcodecs": "^0.1.4",
"@types/wicg-file-system-access": "^2020.9.5",
"canvas": "^2.9.1",
"csv-loader": "^3.0.3",
"image-size": "^1.0.1",
"konva": "^8.3.2",
"meow": "^10.1.2",
@@ -1422,6 +1423,19 @@
"node": ">=4"
}
},
"node_modules/csv-loader": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/csv-loader/-/csv-loader-3.0.3.tgz",
"integrity": "sha512-JMr83kH2sOFKbRO95fAQV1fLEc1Chx1osJpU7Gd5ZQhmXrsQN479P08sDuyZoO5LMiJ8IsR72Xtl/nSA7rh4Lw==",
"dependencies": {
"loader-utils": "^2.0.0",
"papaparse": "^5.2.0"
},
"funding": {
"type": "individual",
"url": "butmeacoffee.com/allenkoren"
}
},
"node_modules/dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
@@ -4047,6 +4061,11 @@
"node": ">=6"
}
},
"node_modules/papaparse": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.3.2.tgz",
"integrity": "sha512-6dNZu0Ki+gyV0eBsFKJhYr+MdQYAzFUGlBMNj3GNrmHxmz1lfRa24CjFObPXtjcetlOv5Ad299MhIK0znp3afw=="
},
"node_modules/parse-json": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
@@ -7516,6 +7535,15 @@
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true
},
"csv-loader": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/csv-loader/-/csv-loader-3.0.3.tgz",
"integrity": "sha512-JMr83kH2sOFKbRO95fAQV1fLEc1Chx1osJpU7Gd5ZQhmXrsQN479P08sDuyZoO5LMiJ8IsR72Xtl/nSA7rh4Lw==",
"requires": {
"loader-utils": "^2.0.0",
"papaparse": "^5.2.0"
}
},
"dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
@@ -9463,6 +9491,11 @@
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true
},
"papaparse": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.3.2.tgz",
"integrity": "sha512-6dNZu0Ki+gyV0eBsFKJhYr+MdQYAzFUGlBMNj3GNrmHxmz1lfRa24CjFObPXtjcetlOv5Ad299MhIK0znp3afw=="
},
"parse-json": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",

View File

@@ -14,6 +14,7 @@
"@types/dom-webcodecs": "^0.1.4",
"@types/wicg-file-system-access": "^2020.9.5",
"canvas": "^2.9.1",
"csv-loader": "^3.0.3",
"image-size": "^1.0.1",
"konva": "^8.3.2",
"meow": "^10.1.2",

View File

@@ -222,14 +222,6 @@ export class Project extends Stage {
return lastScene;
}
public secondsToFrames(seconds: number) {
return Math.ceil(seconds * this.framesPerSeconds);
}
public framesToSeconds(frames: number) {
return frames / this.framesPerSeconds;
}
private getNextScene(scene?: Scene): Scene {
const scenes = Object.values(this.sceneLookup);
if (!scene) {
@@ -242,4 +234,18 @@ export class Project extends Stage {
}
return scenes[index + 1] ?? null;
}
public secondsToFrames(seconds: number) {
return Math.ceil(seconds * this.framesPerSeconds);
}
public framesToSeconds(frames: number) {
return frames / this.framesPerSeconds;
}
public async getBlob(): Promise<Blob> {
return new Promise<Blob>(resolve =>
this.master.getNativeCanvasElement().toBlob(resolve, 'image/png'),
);
}
}

View File

@@ -78,7 +78,7 @@ export class Scene extends Group {
});
decorate(runnerFactory, threadable());
this.storageKey = `scene-${this.name()}`;
this.storageKey = `scene-${this.project.name()}-${this.name()}`;
const storedEvents = localStorage.getItem(this.storageKey);
if (storedEvents) {
for (const event of Object.values<TimeEvent>(JSON.parse(storedEvents))) {
@@ -114,7 +114,7 @@ export class Scene extends Group {
}
public async reset(previousScene: Scene = null) {
this.x(0).y(0);
this.x(0).y(0).opacity(1).show();
this.counters = {};
this.destroyChildren();
this.previousScene = previousScene;
@@ -170,6 +170,8 @@ export class Scene extends Group {
public *transition(transitionRunner?: SceneTransition) {
if (transitionRunner) {
yield* transitionRunner(this, this.previousScene);
} else {
this.previousScene?.hide();
}
if (this.state === SceneState.Initial) {
this.state = SceneState.AfterTransitionIn;

View File

@@ -7,9 +7,11 @@ import {
clampRemap,
easeInExpo,
easeInOutCubic,
easeOutCubic,
easeOutExpo,
linear,
map,
rectArcTween,
spacingTween,
tween,
} from '../tweening';
@@ -54,8 +56,7 @@ export function showSurface(surface: Surface): ThreadGenerator {
value => {
surface.setMask({
...toMask,
width: easeInOutCubic(value, fromMask.width, toMask.width),
height: easeInOutCubic(value, fromMask.height, toMask.height),
...rectArcTween(fromMask, toMask, easeInOutCubic(value)),
});
surface.setMargin(
spacingTween(marginFrom, margin, easeInOutCubic(value)),
@@ -96,13 +97,14 @@ export function showCircle(
export function unravelSurface(surface: Surface): ThreadGenerator {
const mask = surface.getMask();
surface.show();
surface.setMask({...mask, height: 0});
return tween(
0.5,
value => {
surface.setMask({
...mask,
height: map(0, mask.height, easeInOutCubic(value)),
height: map(0, mask.height, easeOutCubic(value)),
});
},
() => surface.setMask(null),

View File

@@ -12,6 +12,7 @@ interface BootstrapConfig {
audio?: {
meta: Waveform;
src: string;
offset?: number;
};
}
@@ -22,7 +23,13 @@ export function bootstrap(config: BootstrapConfig) {
background: config.background ?? '#141414',
...(config.size ?? ProjectSize.FullHD),
});
const player = new Player(project, config.audio);
const player = new Player(
project,
config.audio && {
offset: 0,
...config.audio,
},
);
(<any>window).player = player;
//@ts-ignore

View File

@@ -20,10 +20,17 @@ const UNITY = [
),
];
const OBJECT = [
new Path2D(
'M49.71,20.94c-.36-1.31-1.05-2.51-2-3.48-.5-.51-1.07-.95-1.71-1.32l-12-6.92c-1.24-.72-2.62-1.08-4-1.08s-2.76,.36-4,1.08l-12,6.92c-.64,.37-1.21,.81-1.71,1.32-.95,.97-1.64,2.17-2,3.48-.19,.68-.29,1.4-.29,2.13v13.86c0,2.86,1.52,5.5,4,6.93l12,6.92c.64,.37,1.31,.64,2,.82,.65,.17,1.33,.26,2,.26s1.35-.09,2-.26c.69-.18,1.36-.45,2-.82l12-6.92c2.48-1.43,4-4.07,4-6.93v-13.86c0-.73-.1-1.45-.29-2.13ZM28,12.68c.61-.35,1.3-.54,2-.54s1.39,.19,2,.54l12,6.93-14,8.08-14-8.08,12-6.93Zm-12,27.71c-1.23-.71-2-2.04-2-3.46v-13.85l14,8.07v16.17l-12-6.93Zm30-3.46c0,1.42-.77,2.75-2,3.46l-12,6.93V31.15l14-8.08v13.86Z',
),
];
export enum IconType {
Fill,
Brush,
Unity,
Object,
}
interface IconConfig extends ShapeConfig {
@@ -47,6 +54,9 @@ export class Icon extends Shape {
case IconType.Unity:
this.paths = UNITY;
break;
case IconType.Object:
this.paths = OBJECT;
break;
}
this._fillFunc = context => {

View File

@@ -71,7 +71,10 @@ export class LinearLayout extends Group {
const margin = child.getMargin();
const scale = child.getAbsoluteScale(this);
const offset = child.getOriginDelta(Origin.TopLeft);
const parentOffset = getOriginOffset(this.contentSize, child.origin());
const parentOffset = getOriginOffset(
margin.shrink(this.contentSize),
child.origin(),
);
if (direction === Center.Vertical) {
child.position({
x: parentOffset.x,

View File

@@ -29,6 +29,8 @@ export interface SurfaceConfig extends ContainerConfig {
circleMask?: CircleMask;
background?: string;
child?: Node;
rescaleChild?: boolean;
shadow?: boolean;
}
@KonvaNode()
@@ -39,6 +41,10 @@ export class Surface extends Group {
public background: GetSet<SurfaceConfig['background'], this>;
@getset(null)
public child: GetSet<SurfaceConfig['child'], this>;
@getset(true)
public rescaleChild: GetSet<SurfaceConfig['rescaleChild'], this>;
@getset(false)
public shadow: GetSet<SurfaceConfig['shadow'], this>;
private surfaceMask: SurfaceMask | null = null;
private circleMask: CircleMask | null = null;
@@ -130,8 +136,10 @@ export class Surface extends Group {
);
this.surfaceMask = data;
child.scaleX(scale);
child.scaleY(scale);
if (this.rescaleChild()) {
child.scaleX(scale);
child.scaleY(scale);
}
child.position(getOriginDelta(data, Origin.Middle, child.getOrigin()));
this.markDirty();
}
@@ -204,19 +212,6 @@ export class Surface extends Group {
context.restore();
}
if (this.surfaceMask) {
context._context.clip(
CanvasHelper.roundRectPath(
new Path2D(),
-size.width / 2,
-size.height / 2,
size.width,
size.height,
this.surfaceMask.radius,
),
);
}
if (this.circleMask) {
context._context.beginPath();
context._context.arc(
@@ -233,6 +228,11 @@ export class Surface extends Group {
context.save();
context._context.fillStyle = this.surfaceMask?.color ?? this.background();
context._context.globalAlpha = opacity;
if (this.shadow()) {
context._context.shadowColor = 'rgba(0, 0, 0, 0.32)';
context._context.shadowOffsetY = 10;
context._context.shadowBlur = 40;
}
CanvasHelper.roundRect(
context._context,
-size.width / 2,
@@ -244,6 +244,19 @@ export class Surface extends Group {
context._context.fill();
context.restore();
if (this.surfaceMask) {
context._context.clip(
CanvasHelper.roundRectPath(
new Path2D(),
-size.width / 2,
-size.height / 2,
size.width,
size.height,
this.surfaceMask.radius,
),
);
}
m = transform.copy().invert().getMatrix();
context.transform(m[0], m[1], m[2], m[3], m[4], m[5]);

View File

@@ -78,7 +78,6 @@ export class Video extends Shape {
@threadable('videoRunner')
private *playRunner(): ThreadGenerator {
this.frame(0);
while (this.task !== null) {
if (this.playing()) {
this.frame(this.frame() + 1);

View File

@@ -1,12 +1,15 @@
import {getset, KonvaNode, threadable} from '../../decorators';
import {cached, getset, KonvaNode, threadable} from '../../decorators';
import {GetSet} from 'konva/lib/types';
import PrismJS from 'prismjs';
import {Context} from 'konva/lib/Context';
import {Text, TextConfig} from 'konva/lib/shapes/Text';
import {Util} from 'konva/lib/Util';
import {easeOutExpo, tween} from '../../tweening';
import {CodeTheme} from './CodeTheme';
import {easeInExpo, easeOutExpo, tween} from '../../tweening';
import {CodeTheme, CodeTokens} from './CodeTheme';
import {JS_CODE_THEME} from '../../themes';
import {ThreadGenerator} from '../../threading';
import {useScene} from '../../utils';
import {Node} from 'konva/lib/Node';
type CodePoint = [number, number];
type CodeRange = [CodePoint, CodePoint];
@@ -14,14 +17,22 @@ type CodeRange = [CodePoint, CodePoint];
interface CodeConfig extends TextConfig {
selection?: CodeRange[];
theme?: CodeTheme;
numbers?: boolean;
language?: string;
}
const FALLBACK_COLOR = '#FF00FF';
@KonvaNode({centroid: false})
export class Code extends Text {
@getset([])
public selection: GetSet<CodeConfig['selection'], this>;
@getset(JS_CODE_THEME)
public theme: GetSet<CodeConfig['theme'], this>;
@getset(false)
public numbers: GetSet<CodeConfig['numbers'], this>;
@getset('js')
public language: GetSet<CodeConfig['language'], this>;
private readonly textCanvas: HTMLCanvasElement;
private readonly textCtx: CanvasRenderingContext2D;
@@ -50,27 +61,72 @@ export class Code extends Text {
context.translate(padding.left, padding.right);
this.drawSelection(context._context);
this.drawText(context._context);
if (this.numbers()) {
this.drawLineNumbers(context._context);
}
}
private drawSelection(context: CanvasRenderingContext2D) {
const lines = this.text().split('\n');
const letterWidth = this.measureSize(' ').width;
const lineHeight = this.fontSize() * this.lineHeight();
const selection = [...this.selection()];
const outline = this.outline;
public setText(text: any): this {
super.setText(text);
this.markDirty();
context.beginPath();
this._clearCache(this.getLines);
this._clearCache(this.getTokens);
this._clearCache(this.getNormalizedSelection);
return this;
}
public setLanguage(langauge: string): this {
this.attrs.language = langauge;
this._clearCache(this.getTokens);
return this;
}
public setSelection(value: CodeRange[]): this {
this.attrs.selection = value;
this._clearCache(this.getNormalizedSelection);
return this;
}
@cached('Code.lines')
private getLines(): string[] {
return this.text().split('\n');
}
@cached('Code.tokens')
private getTokens(): (PrismJS.Token | string)[] {
const language = this.language();
if (language in PrismJS.languages) {
return PrismJS.tokenize(this.text(), PrismJS.languages[language]);
} else {
console.warn(
`Missing language: ${language}.`,
`Make sure that 'prismjs/components/prism-${language}' has been imported.`,
);
return PrismJS.tokenize(this.text(), PrismJS.languages.plain);
}
}
@cached('Code.selection')
private getNormalizedSelection(): CodeRange[] {
const lines = this.getLines();
const normalized: CodeRange[] = [];
const selection = [...this.selection()];
for (const range of selection) {
let [[startLine, startColumn], [endLine, endColumn]] = range;
if (startLine > endLine) {
[startLine, endLine] = [endLine, startLine];
}
if (startLine === endLine && startColumn > endColumn) {
[startColumn, endColumn] = [endColumn, startColumn];
}
if (startLine >= lines.length) {
startLine = lines.length - 1;
}
if (endLine >= lines.length) {
endLine = lines.length - 1;
}
if (endColumn >= lines[endLine].length) {
endColumn = Math.max(lines[endLine].length, 1);
}
@@ -82,7 +138,7 @@ export class Code extends Text {
: Math.max(1, lines[startLine + 1].length);
if (nextLineOffset <= startColumn) {
selection.push([
normalized.push([
[startLine + 1, 0],
[endLine, endColumn],
]);
@@ -91,6 +147,25 @@ export class Code extends Text {
}
}
normalized.push([
[startLine, startColumn],
[endLine, endColumn],
]);
}
return normalized;
}
private drawSelection(context: CanvasRenderingContext2D) {
const letterWidth = this.measureSize(' ').width;
const lineHeight = this.fontSize() * this.lineHeight();
const selection = this.getNormalizedSelection();
const outline = this.outline;
const lines = this.getLines();
context.beginPath();
for (const range of selection) {
let [[startLine, startColumn], [endLine, endColumn]] = range;
let offset =
startLine === endLine
? endColumn * letterWidth
@@ -176,13 +251,13 @@ export class Code extends Text {
context.closePath();
context.fillStyle = '#242424';
context.globalAlpha = this.getAbsoluteOpacity();
context.fill();
}
private drawText(context: CanvasRenderingContext2D) {
const letterWidth = this.measureSize(' ').width;
const lineHeight = this.fontSize() * this.lineHeight();
const tokens = PrismJS.tokenize(this.text(), PrismJS.languages.javascript);
const theme = this.theme();
context.font = this._getContextFont();
@@ -190,45 +265,70 @@ export class Code extends Text {
let x = 0;
let y = 0;
for (const token of tokens) {
const draw = (token: string | PrismJS.Token, colors: CodeTokens) => {
if (typeof token === 'string') {
context.fillStyle = theme.punctuation ?? theme.fallback;
if (token.includes('\n')) {
const words = token.split('\n');
for (let i = 0; i < words.length; i++) {
if (i > 0) {
x = 0;
y++;
}
context.globalAlpha = this.getOpacity(x, y);
context.fillText(words[i], x * letterWidth, (y + 0.5) * lineHeight);
x += words[i].length;
context.fillStyle = colors.punctuation ?? FALLBACK_COLOR;
const lines = token.split('\n');
let isFirst = true;
for (const line of lines) {
if (!isFirst) {
x = 0;
y++;
}
} else {
context.globalAlpha = this.getOpacity(x, y);
context.fillText(
token as string,
x * letterWidth,
(y + 0.5) * lineHeight,
);
x += token.length;
isFirst = false;
const trim = line.length - line.trimStart().length;
context.globalAlpha = this.getOpacityAtPoint(x + trim, y);
context.fillText(line, x * letterWidth, (y + 0.5) * lineHeight);
x += line.length;
}
} else {
context.fillStyle = theme[token.type] ?? theme.fallback;
context.globalAlpha = this.getOpacity(x, y);
} else if (typeof token.content === 'string') {
if (!(token.type in colors)) {
console.warn(`Unstyled token type:`, token.type);
}
context.fillStyle = colors[token.type] ?? FALLBACK_COLOR;
context.globalAlpha = this.getOpacityAtPoint(x, y);
context.fillText(
token.content as string,
token.content,
x * letterWidth,
(y + 0.5) * lineHeight,
);
x += token.length;
} else if (Array.isArray(token.content)) {
const subTheme = theme[token.type] ?? colors;
for (const subToken of token.content) {
draw(subToken, subTheme);
}
} else {
const subTheme = theme[token.type] ?? colors;
draw(token.content, subTheme);
}
};
for (const token of this.getTokens()) {
draw(token, theme.default);
}
}
private getOpacity(x: number, y: number): number {
return this.isSelected(x, y) ? 1 : this.unselectedOpacity;
private drawLineNumbers(context: CanvasRenderingContext2D) {
const theme = this.theme();
const lines = this.getLines();
const lineHeight = this.fontSize() * this.lineHeight();
context.save();
context.fillStyle = theme.default.comment ?? FALLBACK_COLOR;
context.globalAlpha = this.getAbsoluteOpacity();
context.textAlign = 'right';
for (let i = 0; i < lines.length; i++) {
context.fillText(i.toString(), -20, (i + 0.5) * lineHeight);
}
context.restore();
}
private getOpacityAtPoint(x: number, y: number): number {
return this.isSelected(x, y)
? this.getAbsoluteOpacity()
: this.getAbsoluteOpacity() * this.unselectedOpacity;
}
private isSelected(x: number, y: number): boolean {
@@ -256,11 +356,11 @@ export class Code extends Text {
return this;
}
public selectWord(line: number, from: number, to?: number): this {
public selectWord(line: number, from: number, length?: number): this {
this.selection([
[
[line, from],
[line, to ?? Infinity],
[line, from + (length ?? Infinity)],
],
]);
return this;
@@ -296,21 +396,29 @@ export class Code extends Text {
}
@threadable('animateCode')
public *animate() {
public *animate(): ThreadGenerator {
const hasSelection = this.hasSelection();
const currentOpacity = this.unselectedOpacity;
yield* tween(
0.5,
value => {
this.outline = easeOutExpo(value, -8, 0);
this.unselectedOpacity = easeOutExpo(
value,
currentOpacity,
hasSelection ? 0.32 : 1,
);
},
() => this.apply(),
);
yield* tween(0.5, value => {
this.outline = easeOutExpo(value, -8, 0);
this.unselectedOpacity = easeOutExpo(
value,
currentOpacity,
hasSelection ? 0.32 : 1,
);
});
this.apply();
}
@threadable()
public *animateClearSelection() {
const currentOpacity = this.unselectedOpacity;
yield* tween(0.5, value => {
this.outline = easeInExpo(value, 0, -8);
this.unselectedOpacity = easeInExpo(value, currentOpacity, 1);
});
this.clearSelection();
this.apply();
}
}

View File

@@ -1,6 +1,11 @@
export type CodeTheme = Record<string, string> & {fallback: string};
export type CodeTokens = Record<string, string>;
export interface JSCodeTheme {
export type CodeTheme<T extends CodeTokens = CodeTokens> = {
[Key: string]: T;
default: T;
};
export type JSCodeTokens = CodeTokens & {
boolean?: string;
'class-name'?: string;
comment?: string;
@@ -18,5 +23,4 @@ export interface JSCodeTheme {
string?: string;
'string-property'?: string;
'template-string'?: string;
fallback: string;
}
};

12
src/decorators/cached.ts Normal file
View File

@@ -0,0 +1,12 @@
import type {Node} from 'konva/lib/Node';
export function cached(key: string): MethodDecorator {
return function (target, propertyKey, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (this: Node) {
return this._getCache(key, original);
};
descriptor.value.prototype.cachedKey = key;
return descriptor;
};
}

View File

@@ -1,4 +1,5 @@
export * from './decorate';
export * from './threadable';
export * from './getset';
export * from './cached';
export * from './KonvaNode';

5
src/global.d.ts vendored
View File

@@ -23,6 +23,11 @@ declare module '*.wav' {
export = value;
}
declare module '*.csv' {
const value: any;
export = value;
}
declare module '*.wav?meta' {
const value: import('./types/Waveform').Waveform;
export = value;

View File

@@ -1,7 +1,12 @@
import type {Node} from 'konva/lib/Node';
import {Factory} from 'konva/lib/Factory';
import {ANIMATE} from '../symbols';
import {Animator, TweenProvider} from '../tweening';
import {
Animator,
InterpolationFunction,
TweenFunction,
TweenProvider,
} from '../tweening';
import {ThreadGenerator} from '../threading';
import {Vector2d} from 'konva/lib/types';
@@ -10,7 +15,13 @@ declare module 'konva/lib/types' {
(): Type;
(value: Type): This;
(value: typeof ANIMATE): Animator<Type, This>;
(value: Type, time: number): ThreadGenerator;
<Rest extends any[]>(
value: Type,
time: number,
interpolation?: InterpolationFunction,
mapper?: TweenFunction<Type, Rest>,
...rest: Rest
): ThreadGenerator;
}
}
@@ -23,15 +34,20 @@ Factory.addOverloadedGetterSetter = function addOverloadedGetterSetter(
const setter = 'set' + capitalizedAttr;
const getter = 'get' + capitalizedAttr;
constructor.prototype[attr] = function (
constructor.prototype[attr] = function <Rest extends any[]>(
value?: Vector2d | typeof ANIMATE,
time?: number,
interpolation?: InterpolationFunction,
mapper?: TweenFunction<any, Rest>,
...rest: Rest
) {
if (value === ANIMATE) {
return new Animator<any, Node>(this, attr, tween);
}
if (time !== undefined) {
return new Animator<any, Node>(this, attr, tween).key(value, time).run();
return new Animator<any, Node>(this, attr, tween)
.key(value, time, interpolation, mapper, ...rest)
.run();
}
return value === undefined ? this[getter]() : this[setter](value);
};

View File

@@ -69,6 +69,8 @@ declare module 'konva/lib/Node' {
wasDirty(): boolean;
subscribe(event: string, handler: () => void): () => void;
_clearCache(attr?: string | Function): void;
}
export interface NodeConfig {
@@ -249,6 +251,17 @@ Node.prototype.setAttrs = function (this: Node, config: any) {
return super_setAttrs.call(this, config);
};
const super__clearCache = Node.prototype._clearCache;
Node.prototype._clearCache = function (this: Node, attr?: string | Function) {
if (typeof attr === 'function') {
if (attr.prototype?.cachedKey) {
this._cache.delete(attr.prototype.cachedKey);
}
} else {
super__clearCache.call(this, attr);
}
};
Factory.addGetterSetter(Node, 'padd', new Spacing());
Factory.addGetterSetter(Node, 'margin', new Spacing());
Factory.addGetterSetter(Node, 'origin', Origin.Middle);

View File

@@ -36,7 +36,7 @@ interface PlayerCommands {
export interface PlayerRenderEvent {
frame: number;
data: string;
data: Blob;
}
const STORAGE_KEY = 'player-state';
@@ -147,6 +147,7 @@ export class Player {
public readonly audio?: {
src: string;
meta: Waveform;
offset: number;
},
) {
this.startTime = performance.now();
@@ -271,7 +272,7 @@ export class Player {
this.project.draw();
await this.renderChanged.dispatchAsync({
frame: this.project.frame,
data: this.project.master.getNativeCanvasElement().toDataURL(),
data: await this.project.getBlob(),
});
if (state.finished || this.project.frame >= state.endFrame) {
this.toggleRendering(false);
@@ -299,7 +300,7 @@ export class Player {
state.paused ||
(state.speed === 1 &&
this.hasAudio() &&
this.audioElement.currentTime < this.project.time)
this.audioTime() < this.project.time)
) {
this.request();
return;
@@ -308,11 +309,9 @@ export class Player {
else if (
this.hasAudio() &&
state.speed === 1 &&
this.project.time < this.audioElement.currentTime - MAX_AUDIO_DESYNC
this.project.time < this.audioTime() - MAX_AUDIO_DESYNC
) {
const seekFrame = this.project.secondsToFrames(
this.audioElement.currentTime,
);
const seekFrame = this.project.secondsToFrames(this.audioTime());
state.finished = await this.project.seek(seekFrame, state.speed);
}
// Simply move forward one frame
@@ -364,10 +363,15 @@ export class Player {
this.audioElement.currentTime = Math.max(
0,
this.project.framesToSeconds(this.project.frame + frameOffset),
this.project.framesToSeconds(this.project.frame + frameOffset) +
this.audio.offset,
);
}
private audioTime(): number {
return (this.audioElement?.currentTime ?? 0) - this.audio.offset;
}
private request() {
this.requestId = requestAnimationFrame(async time => {
if (time - this.renderTime >= 990 / this.project.framesPerSeconds) {

View File

@@ -1,4 +1,4 @@
import {JSCodeTheme} from '../components/code';
import {CodeTheme, JSCodeTokens} from '../components/code';
const KEYWORD = '#ff6470';
const TEXT = '#ACB3BF';
@@ -6,25 +6,30 @@ const FUNCTION = '#ffc66d';
const STRING = '#99C47A';
const NUMBER = '#68ABDF';
const PROPERTY = '#AC7BB5';
const COMMENT = '#a9b7c6';
const COMMENT = '#5c5e60';
export const JS_CODE_THEME: JSCodeTheme = {
boolean: KEYWORD,
constant: KEYWORD,
keyword: KEYWORD,
'class-name': TEXT,
operator: TEXT,
punctuation: TEXT,
function: FUNCTION,
string: STRING,
number: NUMBER,
'literal-property': PROPERTY,
comment: COMMENT,
'function-variable': '#ff00ff',
hashbang: '#ff00ff',
parameter: '#ff00ff',
regex: '#ff00ff',
'string-property': '#ff00ff',
'template-string': '#ff00ff',
fallback: '#ff00ff',
export const JS_CODE_THEME: CodeTheme<JSCodeTokens> = {
default: {
boolean: KEYWORD,
constant: KEYWORD,
keyword: KEYWORD,
'class-name': NUMBER,
operator: TEXT,
punctuation: TEXT,
'script-punctuation': TEXT,
function: FUNCTION,
string: STRING,
number: NUMBER,
'literal-property': PROPERTY,
comment: COMMENT,
'function-variable': FUNCTION,
},
parameter: {
'literal-property': TEXT,
operator: TEXT,
punctuation: NUMBER,
},
'attr-name': {
punctuation: FUNCTION,
},
};

View File

@@ -132,8 +132,8 @@ export class Animator<Type, This extends Node> {
return this;
}
public waitUntil(event: string): this
public waitUntil(time: number): this
public waitUntil(event: string): this;
public waitUntil(time: number): this;
public waitUntil(time: number | string): this {
// @ts-ignore
this.keys.push(() => waitUntil(time));
@@ -172,22 +172,30 @@ export class Animator<Type, This extends Node> {
private inferProperties() {
this.valueFrom ??= this.getValueFrom();
this.mapper = Animator.inferMapper(this.valueFrom);
}
if (typeof this.valueFrom === 'string') {
if (this.valueFrom.startsWith('#') || this.valueFrom.startsWith('rgb')) {
this.mapper = colorTween;
public static inferMapper<T>(value: T): TweenFunction<T> {
let tween: TweenFunction<any> = map;
if (typeof value === 'string') {
if (value.startsWith('#') || value.startsWith('rgb')) {
tween = colorTween;
} else {
this.mapper = textTween;
tween = textTween;
}
} else if (this.valueFrom && typeof this.valueFrom === 'object') {
if ('x' in this.valueFrom) {
if ('width' in this.valueFrom) {
this.mapper = rectArcTween;
} else if (value && typeof value === 'object') {
if ('x' in value) {
if ('width' in value) {
tween = rectArcTween;
} else {
tween = vector2dTween;
}
this.mapper = vector2dTween;
} else if ('left' in this.valueFrom) {
this.mapper = spacingTween;
} else if ('left' in value) {
tween = spacingTween;
}
}
return tween as TweenFunction<T>;
}
}

View File

@@ -72,6 +72,15 @@ const compiler = webpack({
test: /\.mp4/i,
type: 'asset',
},
{
test: /\.csv$/,
loader: 'csv-loader',
options: {
dynamicTyping: true,
header: true,
skipEmptyLines: true,
},
},
{
test: /\.label$/i,
use: [
@@ -143,6 +152,12 @@ const compiler = webpack({
experiments: {
topLevelAwait: true,
},
plugins: [
new webpack.ProvidePlugin({
// Required to load additional languages for Prism
Prism: 'prismjs',
}),
],
});
const server = new WebpackDevServer(
@@ -160,7 +175,6 @@ const server = new WebpackDevServer(
path.join(renderOutput, req.params.name),
{encoding: 'base64'},
);
req.setEncoding('utf8');
req.pipe(stream);
req.on('end', () => res.end());
},