feat: general improvements

This commit is contained in:
aarthificial
2022-05-03 23:16:00 +02:00
parent 94543d1079
commit dbff3cce37
22 changed files with 263 additions and 45 deletions

View File

@@ -6,7 +6,7 @@
"author": "aarthificial",
"license": "ISC",
"scripts": {
"build": "tsc",
"build": "webpack",
"watch": "tsc -w"
},
"types": "dist/global.d.ts",

View File

@@ -30,6 +30,7 @@ export interface TimeEvent {
name: string;
initialFrame: number;
offset: number;
fps: number;
}
export class Scene extends Layer {
@@ -80,7 +81,16 @@ export class Scene extends Layer {
this.storageKey = `scene-${this.name()}`;
const storedEvents = localStorage.getItem(this.storageKey);
if (storedEvents) {
this.storedEventLookup = JSON.parse(storedEvents);
const fps = project.framesPerSeconds;
for (const event of Object.values<TimeEvent>(JSON.parse(storedEvents))) {
const oldFps = event.fps ?? 30;
if (oldFps !== fps) {
event.initialFrame = (event.offset * fps) / oldFps;
event.offset = (event.offset * fps) / oldFps;
}
event.fps = fps;
this.storedEventLookup[event.name] = event;
}
}
}
@@ -95,6 +105,7 @@ export class Scene extends Layer {
...this.timeEventLookup,
};
this.timeEventLookup = {};
this.timeEventsChanged.dispatch([]);
}
public async reset(previousScene: Scene = null) {
@@ -191,6 +202,7 @@ export class Scene extends Layer {
name,
initialFrame,
offset: this.storedEventLookup[name]?.offset ?? 0,
fps: this.project.framesPerSeconds,
};
this.timeEventsChanged.dispatch(this.timeEvents);
} else if (this.timeEventLookup[name].initialFrame !== initialFrame) {

View File

@@ -9,6 +9,7 @@ import {
easeInOutCubic,
easeOutExpo,
linear,
map,
spacingTween,
tween,
} from '../tweening';
@@ -68,6 +69,7 @@ export function showSurface(surface: Surface): ThreadGenerator {
decorate(showCircle, threadable());
export function showCircle(
surface: Surface,
duration: number = 0.6,
origin?: Origin | Vector2d,
): ThreadGenerator {
const position =
@@ -85,9 +87,24 @@ export function showCircle(
mask.radius = 0;
return chain(
tween(target / 2000, value => {
tween(duration, value => {
mask.radius = easeInOutCubic(value, 0, target);
}),
() => surface.setCircleMask(null),
);
}
export function unravelSurface(surface: Surface): ThreadGenerator {
const mask = surface.getMask();
surface.setMask({...mask, height: 0});
return tween(
0.5,
value => {
surface.setMask({
...mask,
height: map(0, mask.height, easeInOutCubic(value)),
});
},
() => surface.setMask(null),
);
}

View File

@@ -28,13 +28,17 @@ export interface SurfaceTransitionConfig {
}
decorate(surfaceTransition, threadable());
export function surfaceTransition(fromSurfaceOriginal: Surface) {
const fromSurface = fromSurfaceOriginal
.clone()
.moveTo(fromSurfaceOriginal.parent)
.zIndex(fromSurfaceOriginal.zIndex());
export function surfaceTransition(fromSurfaceOriginal: Surface, clone = true) {
const fromSurface = clone
? fromSurfaceOriginal
.clone()
.moveTo(fromSurfaceOriginal.parent)
.zIndex(fromSurfaceOriginal.zIndex())
: fromSurfaceOriginal;
fromSurfaceOriginal.hide();
if (clone) {
fromSurfaceOriginal.hide();
}
const from = fromSurfaceOriginal.getMask();
decorate(surfaceTransitionExecutor, threadable());

View File

@@ -39,6 +39,8 @@ export class ColorPicker extends Surface {
public readonly b: Range;
public readonly a: Range;
public parsedColor: ReturnType<typeof parseColor>;
public constructor(config?: ColorPickerConfig) {
super(config);
@@ -73,6 +75,8 @@ export class ColorPicker extends Surface {
}
private updateColor() {
if (!this.a) return;
const style = this.style();
const preview = this.previewColor();
@@ -80,13 +84,13 @@ export class ColorPicker extends Surface {
...style,
foreground: preview,
});
const color = parseColor(preview);
color.a = Math.round(color.a * 255);
this.parsedColor = parseColor(preview);
this.parsedColor.a = Math.round(this.parsedColor.a * 255);
this.r.value(color.r);
this.g.value(color.g);
this.b.value(color.b);
this.a.value(color.a);
this.r.value(this.parsedColor.r);
this.g.value(this.parsedColor.g);
this.b.value(this.parsedColor.b);
this.a.value(this.parsedColor.a);
this.preview.fill(preview);
}

View File

@@ -10,6 +10,8 @@ import {map} from '../tweening';
export interface ConnectionConfig extends ContainerConfig {
start?: Pin;
end?: Pin;
startTarget?: Node;
endTarget?: Node;
crossing?: Node;
arrow?: Arrow;
}
@@ -59,6 +61,13 @@ export class Connection extends Group {
if (!this.arrow.getParent()) {
this.add(this.arrow);
}
if (config?.startTarget) {
this.start.target(config?.startTarget);
}
if (config?.endTarget) {
this.end.target(config?.endTarget);
}
}
private static measurePosition(

View File

@@ -2,21 +2,26 @@ import {Context} from 'konva/lib/Context';
import {GetSet} from 'konva/lib/types';
import {LayoutShape, LayoutShapeConfig} from './LayoutShape';
import {KonvaNode, getset} from '../decorators';
import {CanvasHelper} from '../helpers';
export interface GridConfig extends LayoutShapeConfig {
gridSize?: number;
subdivision?: boolean;
checker?: boolean;
}
@KonvaNode()
export class Grid extends LayoutShape {
@getset(16, Grid.prototype.recalculate)
public gridSize: GetSet<number, this>;
@getset(false)
public checker: GetSet<GridConfig['checker'], this>;
@getset(false)
public subdivision: GetSet<GridConfig['subdivision'], this>;
private path: Path2D;
private checkerPath: Path2D;
public constructor(config?: GridConfig) {
super(config);
@@ -42,10 +47,26 @@ export class Grid extends LayoutShape {
context.stroke(this.path);
}
};
this._fillFunc = context => {
if (!this.checkerPath) this.recalculate();
const size = this.size();
context._context.clip(
CanvasHelper.roundRectPath(
new Path2D(),
size.width / -2,
size.height / -2,
size.width,
size.height,
8,
),
);
context.fill(this.checkerPath);
};
}
private recalculate() {
this.path = new Path2D();
this.checkerPath = new Path2D();
let gridSize = this.gridSize();
if (gridSize < 1) {
@@ -56,9 +77,20 @@ export class Grid extends LayoutShape {
size.width = size.width / 2 + gridSize;
size.height = size.height / 2 + gridSize;
let i = 0;
for (let x = -size.width; x <= size.width; x += gridSize) {
this.path.moveTo(x, -size.height);
this.path.lineTo(x, size.height);
for (
let y = -size.height + (i % 2 ? 0 : gridSize);
y <= size.height;
y += gridSize * 2
) {
this.checkerPath.rect(x, y, gridSize, gridSize);
this.checkerPath.rect(x, y, gridSize, gridSize);
}
i++;
}
for (let y = -size.height; y <= size.height; y += gridSize) {
@@ -68,6 +100,10 @@ export class Grid extends LayoutShape {
}
public _sceneFunc(context: Context) {
context.strokeShape(this);
if (this.checker()) {
context.fillShape(this);
} else {
context.strokeShape(this);
}
}
}

View File

@@ -7,9 +7,11 @@ import {getset, KonvaNode, threadable} from '../decorators';
import {GeneratorHelper} from '../helpers';
import {InterpolationFunction, map, tween} from '../tweening';
import {cancel, ThreadGenerator} from '../threading';
import {parseColor} from 'mix-color';
export interface SpriteData {
fileName: string;
src: string;
data: number[];
width: number;
height: number;
@@ -49,6 +51,7 @@ export class Sprite extends LayoutShape {
private spriteData: SpriteData = {
height: 0,
width: 0,
src: '',
data: [],
fileName: '',
};
@@ -120,21 +123,26 @@ export class Sprite extends LayoutShape {
(skin.data[skinId + 3] / 255) *
255,
);
if (mask || this.baseMask) {
const maskValue = map(
mask?.data[id] ?? 255,
this.baseMask?.data[id] ?? 255,
this.baseMaskBlend,
);
this.imageData.data[id + 3] *= map(1, maskValue / 255, blend);
}
}
}
} else {
this.imageData.data.set(this.spriteData.data);
}
if (mask || this.baseMask) {
for (let y = 0; y < this.spriteData.height; y++) {
for (let x = 0; x < this.spriteData.width; x++) {
const id = this.positionToId({x, y});
const maskValue = map(
mask?.data[id] ?? 255,
this.baseMask?.data[id] ?? 255,
this.baseMaskBlend,
);
this.imageData.data[id + 3] *= map(1, maskValue / 255, blend);
}
}
}
this.context.putImageData(this.imageData, 0, 0);
this.fire(SPRITE_CHANGE_EVENT);
this.fireLayoutChange();
@@ -156,6 +164,20 @@ export class Sprite extends LayoutShape {
return this.task;
}
@threadable()
public *playOnce(
animation: SpriteData[],
next: SpriteData[] = null,
): ThreadGenerator {
next ??= this.animation();
this.animation(animation);
for (let i = 0; i < animation.length; i++) {
this.frame(i);
yield* waitFor(1 / this.fps());
}
this.animation(next);
}
@threadable()
public *stop() {
if (this.task) {
@@ -164,12 +186,17 @@ export class Sprite extends LayoutShape {
}
}
private synced = false;
@threadable()
private *playRunner(): ThreadGenerator {
this.frame(0);
while (this.task !== null) {
if (this.playing()) {
this.synced = true;
this.frame(this.frame() + 1);
} else {
this.synced = false;
}
yield* waitFor(1 / this.fps());
}
@@ -178,7 +205,11 @@ export class Sprite extends LayoutShape {
@threadable()
public *waitForFrame(frame: number): ThreadGenerator {
let limit = 1000;
while (this.frame() !== frame && limit > 0) {
while (
this.frame() % this.animation().length !== frame &&
limit > 0 &&
!this.synced
) {
limit--;
yield;
}
@@ -218,6 +249,16 @@ export class Sprite extends LayoutShape {
.padStart(3, ' ')}, ${this.imageData.data[id + 3] / 255})`;
}
public getParsedColorAt(position: Vector2d): ReturnType<typeof parseColor> {
const id = this.positionToId(position);
return {
r: this.imageData.data[id],
g: this.imageData.data[id + 1],
b: this.imageData.data[id + 2],
a: this.imageData.data[id + 3],
}
}
public positionToId(position: Vector2d): number {
return (position.y * this.imageData.width + position.x) * 4;
}

View File

@@ -98,7 +98,7 @@ export class ThreeView extends LayoutShape {
private handleCanvasSizeChange() {
if (!this.renderer) return;
const size = this.canvasSize();
const size = {...this.canvasSize()};
const camera = this.camera();
const ratio = size.width / size.height;
@@ -117,6 +117,7 @@ export class ThreeView extends LayoutShape {
size.width *= this.quality();
size.height *= this.quality();
this.renderer.setSize(size.width, size.height);
this.fireLayoutChange();
}
getLayoutSize(): Size {
@@ -125,7 +126,7 @@ export class ThreeView extends LayoutShape {
_sceneFunc(context: Context) {
const scale = this.quality();
const size = this.canvasSize();
const size = {...this.canvasSize()};
if (this.renderedFrames < 1) {
this.renderedFrames = this.skipFrames();

View File

@@ -1,9 +1,16 @@
import {waitFor} from '../animations';
import {decorate, threadable} from '../decorators';
import {ThreadGenerator} from '../threading';
import {isThreadGenerator, ThreadGenerator} from '../threading';
decorate(delay, threadable());
export function* delay(time: number, task: ThreadGenerator): ThreadGenerator {
export function* delay(
time: number,
task: ThreadGenerator | Function,
): ThreadGenerator {
yield* waitFor(time);
yield* task;
if (isThreadGenerator(task)) {
yield* task;
} else {
task();
}
}

View File

@@ -3,3 +3,4 @@ export * from './any';
export * from './chain';
export * from './delay';
export * from './loop';
export * from './every';

View File

@@ -4,10 +4,10 @@ import {ThreadGenerator} from '../threading';
decorate(loop, threadable());
export function* loop(
iterations: number,
factory: () => ThreadGenerator | void,
factory: (i: number) => ThreadGenerator | void,
) {
for (let i = 0; i < iterations; i++) {
const generator = factory();
const generator = factory(i);
if (generator) {
yield* generator;
} else {

View File

@@ -242,6 +242,9 @@ export class Player {
this.syncAudio(-3);
this.audioError = false;
} catch (e) {
if (!this.audioError) {
console.error(e);
}
this.audioError = true;
}
}

View File

@@ -62,7 +62,7 @@ void main() {
gl_FragColor = mix(
gl_FragColor,
mix(vec4(0.0, 0.0, 0.0, 1.0), highlightDiffuse, vHighlight),
mix(vec4(0.0, 0.0, 0.0, gl_FragColor.a), vec4(highlightDiffuse.rgb, gl_FragColor.a), vHighlight),
edge
);
#else

View File

@@ -46,5 +46,5 @@ export function getFontColor(background: string) {
const brightness = Math.round(
(color.r * 299 + color.g * 587 + color.b * 114) / 1000,
);
return brightness > 125 ? 'rgba(0, 0, 0, 0.87)' : 'rgba(0, 0, 0, 0.6)';
return brightness > 125 ? 'rgba(0, 0, 0, 0.87)' : 'rgba(255, 255, 255, 0.87)';
}

View File

@@ -87,6 +87,14 @@ export class Animator<Type, This extends Node> {
return this;
}
public do(callback: Function): this {
this.keys.push(function* (): ThreadGenerator {
callback();
});
return this;
}
public diff<Rest extends any[]>(
value: Type,
time: number,
@@ -124,7 +132,7 @@ export class Animator<Type, This extends Node> {
return this;
}
public waitUntil(time: number): this {
public waitUntil(time: number | string): this {
this.keys.push(() => waitUntil(time));
return this;
}

View File

@@ -42,6 +42,16 @@ export function easeInOutCubic(value: number, from = 0, to = 1) {
return map(from, to, value);
}
export function easeOutCubic(value: number, from = 0, to = 1): number {
value = 1 - Math.pow(1 - value, 3);
return map(from, to, value);
}
export function easeInCubic(value: number, from = 0, to = 1): number {
value = value * value * value;
return map(from, to, value);
}
export function easeInOutQuint(value: number, from = 0, to = 1) {
value =
value < 0.5

View File

@@ -115,7 +115,7 @@ export function calculateRatio(
ratio /= numberOfValues;
}
return ratio;
return isNaN(ratio) ? 1 : ratio;
}
export function map(from: number, to: number, value: number) {

View File

@@ -1,14 +1,20 @@
const path = require('path');
const {readdirSync} = require('fs');
const loadImage = require('../utils/load-image');
const nameRegex = /([^\d]*)\d+\.png$/;
const nameRegex = /[^\d]*(\d+)\.png$/;
function loader () {
function loader() {
const callback = this.async();
const directoryPath = path.dirname(this.resourcePath);
const files = readdirSync(directoryPath)
.filter(file => nameRegex.test(file))
.map(file => path.resolve(directoryPath, file));
.map(file => nameRegex.exec(file))
.filter(match => !!match)
.map(match => [match.input, parseInt(match[1])])
.sort(([, indexA], [, indexB]) =>
indexA < indexB ? -1 : indexA > indexB ? 1 : 0,
)
.map(([file]) => path.resolve(directoryPath, file));
files.forEach(file => this.addDependency(file));

View File

@@ -7,12 +7,17 @@ import WebpackDevServer from 'webpack-dev-server';
const projectFile = path.resolve(process.cwd(), process.argv[2]);
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const withUI = process.argv[3] === '--ui';
const compiler = webpack({
entry: {
index: projectFile,
ui: path.resolve(__dirname, '../../ui/src/index.ts'),
},
entry: withUI
? {
index: projectFile,
ui: path.resolve(__dirname, '../../ui/src/index.ts'),
}
: {
index: projectFile,
},
mode: 'development',
devtool: 'inline-source-map',
module: {
@@ -95,6 +100,7 @@ const compiler = webpack({
output: {
filename: `[name].js`,
path: __dirname,
uniqueName: 'motion-canvas',
},
experiments: {
topLevelAwait: true,

View File

@@ -22,6 +22,7 @@ module.exports = async function (fileName) {
return {
fileName,
src: image.src,
data: Array.from(imageData.data),
width: dimensions.width,
height: dimensions.height,

52
webpack.config.js Normal file
View File

@@ -0,0 +1,52 @@
const path = require('path');
module.exports = {
entry: {
ui: path.resolve(__dirname, '../ui/src/index.ts'),
},
mode: 'production',
devtool: false,
module: {
rules: [
{
test: /\.svg$/,
type: 'asset',
},
{
test: /\.scss$/,
use: [
{loader: 'style-loader'},
{loader: 'css-loader', options: {modules: true}},
{loader: 'sass-loader'},
],
},
{
test: /\.tsx?$/,
// include: path.resolve(__dirname, '../ui/'),
loader: 'ts-loader',
options: {
configFile: path.resolve(__dirname, '../ui/tsconfig.json'),
},
},
],
},
resolve: {
extensions: ['.js', '.ts', '.tsx'],
alias: {
'@motion-canvas/core': path.resolve(__dirname, 'src'),
},
},
optimization: {
runtimeChunk: {
name: 'runtime',
},
},
output: {
filename: `[name].js`,
path: path.resolve(__dirname, 'public'),
uniqueName: 'motion-canvas',
},
experiments: {
topLevelAwait: true,
},
};