feat: remove ui elements

This commit is contained in:
aarthificial
2022-04-18 23:23:18 +02:00
parent e69a56635f
commit 8e5c288750
13 changed files with 3483 additions and 550 deletions

3287
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -36,9 +36,14 @@
"tsconfig.project.json"
],
"devDependencies": {
"@types/node-sass": "^4.11.2",
"@types/three": "^0.138.0",
"css-loader": "^6.7.1",
"node-loader": "^2.0.0",
"node-sass": "^7.0.1",
"prettier": "^2.5.1",
"sass-loader": "^12.6.0",
"style-loader": "^3.3.1",
"webpack-cli": "^4.9.2"
}
}

View File

@@ -8,81 +8,13 @@
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<link rel="icon" href="data:;base64,iVBORw0KGgo=" />
<link rel="stylesheet" href="styles.css" />
<title>Motion Canvas</title>
</head>
<body>
<div id="container"></div>
<details id="threads">
<summary>THREADS</summary>
</details>
<div id="ui">
<div id="timeline">
<div class="timeline-text js-current-time">0</div>
<div class="track">
<div class="fill fill-seek"></div>
<div class="fill fill-time"></div>
<div class="fill fill-start"></div>
<div class="marker"></div>
</div>
<div class="timeline-text js-duration">10,000</div>
</div>
<form id="controls">
<div class="icon-checkbox">
<input type="checkbox" id="controls-audio" name="audio" checked />
<label for="controls-audio"></label>
</div>
<input
class="icon-button"
type="button"
id="controls-reset"
name="refresh"
/>
<div class="icon-checkbox">
<input type="checkbox" id="controls-play" name="play" checked />
<label for="controls-play"></label>
</div>
<input
class="icon-button"
type="button"
id="controls-next"
name="next"
/>
<div class="icon-checkbox">
<input type="checkbox" id="controls-loop" name="loop" checked />
<label for="controls-loop"></label>
</div>
<div hidden>
<div>
<label for="controls-speed">Speed:</label>
<select name="speed" id="controls-speed">
<option value="2">2x</option>
<option value="1.5">1.5x</option>
<option value="1" selected>1x</option>
<option value="0.5">0.5x</option>
<option value="0.25">0.25x</option>
</select>
</div>
<div>
<label for="controls-fps">FPS:</label>
<input
type="number"
readonly
id="controls-fps"
name="fps"
value="0"
/>
</div>
<input
type="button"
id="controls-render"
name="render"
value="Render"
/>
<div id="loading">Loading...</div>
</div>
</form>
</div>
<div hidden id="konva"></div>
<main id="app"></main>
<script src="runtime.js"></script>
<script src="index.js"></script>
<script src="ui.js"></script>
</body>
</html>

View File

@@ -5,8 +5,10 @@ import {Rect} from 'konva/lib/shapes/Rect';
import {Layer} from 'konva/lib/Layer';
import {Vector2d} from 'konva/lib/types';
import {Konva} from 'konva/lib/Global';
import {ThreadsCallback} from './threading';
import {Thread, ThreadsCallback} from './threading';
import {Scene, SceneRunner} from './Scene';
import {SimpleEventDispatcher} from 'strongly-typed-events';
import {PlayerState} from './player/Player';
Konva.autoDrawEnabled = false;
@@ -19,6 +21,10 @@ const Sizes: Record<ProjectSize, [number, number]> = {
};
export class Project extends Stage {
public get ScenesChanged() {
return this.scenesChanged.asEvent();
}
public readonly background: Rect;
public readonly foreground: Layer;
public readonly center: Vector2d;
@@ -30,9 +36,19 @@ export class Project extends Stage {
return this.framesToSeconds(this.frame);
}
public get scenes(): Scene[] {
return Object.values(this.sceneLookup);
}
public get thread(): Thread {
return this.currentThread;
}
private readonly scenesChanged = new SimpleEventDispatcher<Scene[]>();
private readonly sceneLookup: Record<string, Scene> = {};
private previousScene: Scene = null;
private currentScene: Scene = null;
private currentThread: Thread = null;
public constructor(
scenes: SceneRunner[],
@@ -76,9 +92,10 @@ export class Project extends Stage {
}
const handle = new Scene(this, scene);
this.sceneLookup[scene.name] = handle;
handle.threadsCallback = (...args) => {
handle.threadsCallback = thread => {
if (this.currentScene === handle) {
this.threadsCallback?.(...args);
this.currentThread = thread;
this.threadsCallback?.(thread);
}
};
}
@@ -95,6 +112,7 @@ export class Project extends Stage {
for (const runner of runners) {
this.sceneLookup[runner.name]?.reload(runner);
}
this.scenesChanged.dispatch(this.scenes);
}
public async next(speed: number = 1): Promise<boolean> {
@@ -103,10 +121,13 @@ export class Project extends Stage {
if (!this.currentScene || this.currentScene.isAfterTransitionIn()) {
this.previousScene.remove();
this.previousScene.lastFrame = this.frame;
this.scenesChanged.dispatch(this.scenes);
this.previousScene = null;
}
}
this.frame += speed;
if (this.currentScene) {
await this.currentScene.next();
if (this.currentScene.canTransitionOut()) {
@@ -115,17 +136,35 @@ export class Project extends Stage {
if (this.currentScene) {
await this.currentScene.reset(this.previousScene);
this.currentScene.firstFrame = this.frame;
this.scenesChanged.dispatch(this.scenes);
this.add(this.currentScene);
this.foreground.moveToTop();
} else {
this.previousScene.lastFrame = this.frame;
}
}
}
this.frame += speed;
return !this.currentScene || this.currentScene.isFinished();
}
public async recalculate() {
this.previousScene?.remove();
this.previousScene = null;
this.currentScene?.remove();
this.currentScene = this.findBestScene(Infinity);
this.frame = this.currentScene.firstFrame ?? 0;
this.add(this.currentScene);
this.foreground.moveToTop();
await this.currentScene.reset();
let finished = false;
while (!finished) {
finished = await this.next(1);
}
}
public async seek(frame: number, speed: number = 1): Promise<boolean> {
if (
frame <= this.frame ||

View File

@@ -124,7 +124,7 @@ export class Scene extends Layer {
}
public canTransitionOut(): boolean {
return this.state === SceneState.CanTransitionOut;
return this.state === SceneState.CanTransitionOut || this.state === SceneState.Finished;
}
public add(...children: (Shape | Group)[]): this {

View File

@@ -1,6 +1,6 @@
import {Player} from './player/Player';
export function hot(player: Player, root: typeof module) {
export function hot(player: Player, root: any) {
const update = async (modules: string[]) => {
const runners = [];
for (const module of modules) {
@@ -21,6 +21,7 @@ export function hot(player: Player, root: typeof module) {
player.reload();
};
//@ts-ignore
const scenePaths = require.cache[root.id].children.filter(name =>
//@ts-ignore
name.match(/\.scene\.[jt]sx?/),

View File

@@ -1,120 +0,0 @@
import {Player, PlayerRenderEvent, PlayerState} from './Player';
export class Controls {
private play: HTMLInputElement;
private loop: HTMLInputElement;
private audio: HTMLInputElement;
private speed: HTMLInputElement;
private fps: HTMLInputElement;
private loadingIndicator: HTMLElement;
private lastUpdate: number = 0;
private updateTimes: number[] = [];
private overallTime: number = 0;
private directory: FileSystemDirectoryHandle;
public constructor(
private readonly player: Player,
private readonly form: HTMLFormElement,
) {
this.play = form.play;
this.loop = form.loop;
this.audio = form.audio;
this.speed = form.speed;
this.fps = form.fps;
this.loadingIndicator = document.getElementById('loading');
this.play.addEventListener('change', () =>
this.player.togglePlayback(this.play.checked),
);
this.loop.addEventListener('change', () =>
this.player.updateState({loop: this.loop.checked}),
);
this.audio.addEventListener('change', () =>
this.player.toggleAudio(this.audio.checked),
);
this.speed.addEventListener('change', () =>
this.player.updateState({speed: parseFloat(this.speed.value)}),
);
form.refresh.addEventListener('click', () => this.player.requestReset());
form.next.addEventListener('click', () => this.player.requestNextFrame());
form.render.addEventListener('click', () => this.player.toggleRendering());
document.addEventListener('keydown', event => {
switch (event.key) {
case ' ':
event.preventDefault();
this.player.togglePlayback();
break;
case 'ArrowRight':
event.preventDefault();
this.player.requestNextFrame();
break;
case 'ArrowLeft':
event.preventDefault();
this.player.requestReset();
break;
case 'm':
event.preventDefault();
this.player.toggleAudio();
break;
}
});
this.player.StateChanged.sub(this.handleStateUpdate);
this.player.RenderChanged.sub(this.handleRenderChange);
}
public handleStateUpdate = (state: PlayerState) => {
if (state.frame === state.startFrame || state.frame === 0) {
this.overallTime = 0;
this.updateTimes = [];
}
this.updateFramerate(state.frame);
this.play.checked = !state.paused;
this.loop.checked = state.loop;
this.audio.checked = !state.muted;
this.speed.value = state.speed.toString();
this.loadingIndicator.hidden = !state.loading;
};
private previousFrame: number = 0;
public updateFramerate(frame: number) {
const passed = performance.now() - this.lastUpdate;
if (this.previousFrame === frame) return;
this.previousFrame = frame;
this.overallTime += passed;
this.updateTimes.push(passed);
if (this.updateTimes.length > 10) {
this.overallTime -= this.updateTimes.shift();
}
const average = this.overallTime / this.updateTimes.length;
this.fps.value = Math.floor(1000 / average).toString();
this.lastUpdate = performance.now();
}
public handleRenderChange = async ({frame, blob}: PlayerRenderEvent) => {
const name = frame.toString().padStart(6, '0');
const size = blob.size / 1024;
try {
this.directory ??= await window.showDirectoryPicker();
const file = await this.directory.getFileHandle(`frame-${name}.png`, {
create: true,
});
const stream = await file.createWritable();
await stream.write(blob);
await stream.close();
console.log(`Frame: ${name}, Size: ${Math.round(size)} kB`);
this.updateFramerate(frame);
} catch (e) {
console.error(e);
this.player.toggleRendering(false);
}
};
}

View File

@@ -1,141 +0,0 @@
import {Grid} from '../components/Grid';
import {Player} from './Player';
import {Vector2d} from 'konva/lib/types';
const ZOOM_SPEED = 0.05;
const STORAGE_KEY = 'navigator-state';
interface NavigatorState {
position: Vector2d;
scale: number;
gridVisible: boolean;
}
export class Navigator {
private readonly state: NavigatorState = {
position: {x: 0, y: 0},
scale: 1,
gridVisible: false,
};
private isPanning = false;
private startPosition = {x: 0, y: 0};
private panStartPosition = {x: 0, y: 0};
private readonly grid: Grid;
private gridZoom: number = null;
public constructor(
private readonly player: Player,
private readonly root: HTMLElement,
) {
const savedState = localStorage.getItem(STORAGE_KEY);
if (savedState) {
this.state = JSON.parse(savedState);
}
this.grid = new Grid({
...player.project.center,
...player.project.size(),
strokeWidth: 2,
stroke: 'rgba(255, 255, 255, 0.32',
subdivision: true,
});
player.project.foreground.add(this.grid);
player.project.foreground.drawScene();
document.addEventListener('wheel', this.handleWheel);
document.addEventListener('mousedown', this.handleMouseDown);
document.addEventListener('mouseup', this.handleMouseUp);
document.addEventListener('mousemove', this.handleMouseMove);
document.addEventListener('keydown', event => {
switch (event.key) {
case '0':
this.state.scale = 1;
this.state.position = {x: 0, y: 0};
this.update();
break;
case '=':
this.state.scale *= 1 + ZOOM_SPEED;
this.update();
break;
case '-':
this.state.scale *= 1 - ZOOM_SPEED;
this.update();
break;
case "'":
this.state.gridVisible = !this.state.gridVisible;
this.update();
break;
}
});
this.update();
}
private handleWheel = (event: WheelEvent) => {
if (this.isPanning) return;
const pointer = {
x: event.x - document.body.offsetWidth / 2,
y: event.y - document.body.offsetHeight / 2,
};
const ratio = 1 - Math.sign(event.deltaY) * ZOOM_SPEED;
this.state.scale *= ratio;
this.state.position = {
x: pointer.x + (this.state.position.x - pointer.x) * ratio,
y: pointer.y + (this.state.position.y - pointer.y) * ratio,
};
this.update();
};
private handleMouseDown = (event: MouseEvent) => {
if (event.button === 1) {
event.preventDefault();
this.isPanning = true;
this.startPosition = {...this.state.position};
this.panStartPosition = {x: event.x, y: event.y};
}
};
private handleMouseUp = (event: MouseEvent) => {
if (event.button === 1) {
event.preventDefault();
this.isPanning = false;
}
};
private handleMouseMove = (event: MouseEvent) => {
if (this.isPanning) {
this.state.position = {
x: this.startPosition.x - this.panStartPosition.x + event.x,
y: this.startPosition.y - this.panStartPosition.y + event.y,
};
this.update();
}
};
private update() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.state));
this.root.style.transform = `translate(${this.state.position.x}px, ${this.state.position.y}px) scale(${this.state.scale})`;
if (this.state.gridVisible) {
const newZoom = Math.min(
Math.pow(2, Math.round(Math.log2(this.state.scale))),
2,
);
if (newZoom !== this.gridZoom) {
this.gridZoom = newZoom;
this.grid.gridSize(80 / newZoom);
this.player.project.foreground.drawScene();
}
}
if (this.grid.visible() !== this.state.gridVisible) {
this.grid.visible(this.state.gridVisible);
this.player.project.foreground.drawScene();
}
}
}

View File

@@ -8,8 +8,8 @@ const MAX_AUDIO_DESYNC = 1 / 50;
export interface PlayerState {
duration: number;
frame: number;
startFrame: number;
endFrame: number;
paused: boolean;
loading: boolean;
finished: boolean;
@@ -19,9 +19,18 @@ export interface PlayerState {
muted: boolean;
}
export interface PlayerTime {
duration: number;
durationTime: number;
frame: number;
completion: number;
time: number;
}
interface PlayerCommands {
reset: boolean;
seek: number;
recalculate: boolean;
}
export interface PlayerRenderEvent {
@@ -36,11 +45,16 @@ export class Player {
return this.stateChanged.asEvent();
}
public get TimeChanged() {
return this.timeChanged.asEvent();
}
public get RenderChanged() {
return this.renderChanged.asEvent();
}
private readonly stateChanged = new SimpleEventDispatcher<PlayerState>();
private readonly timeChanged = new SimpleEventDispatcher<PlayerTime>();
private readonly renderChanged =
new PromiseSimpleEventDispatcher<PlayerRenderEvent>();
@@ -50,10 +64,24 @@ export class Player {
private requestId: number = null;
private audioError = false;
public getState(): PlayerState {
return {...this.state};
}
public getTime(): PlayerTime {
return {
frame: this.frame,
time: this.project.framesToSeconds(this.frame),
duration: this.state.duration,
durationTime: this.project.framesToSeconds(this.state.duration),
completion: this.frame / this.state.duration,
};
}
private state: PlayerState = {
duration: 100,
frame: 0,
duration: Infinity,
startFrame: 0,
endFrame: Infinity,
paused: true,
loading: false,
finished: false,
@@ -63,29 +91,56 @@ export class Player {
muted: true,
};
private frame: number = 0;
private commands: PlayerCommands = {
reset: true,
seek: -1,
recalculate: true,
};
public updateState(newState: Partial<PlayerState>) {
this.state = {
...this.state,
...newState,
};
this.stateChanged.dispatch(this.state);
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.state));
let changed = false;
for (const prop in newState) {
if (prop === 'frame') {
continue;
}
// @ts-ignore
if (newState[prop] !== this.state[prop]) {
changed = true;
break;
}
}
if (changed) {
this.state = {
...this.state,
...newState,
};
this.stateChanged.dispatch(this.state);
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.state));
}
}
private updateFrame(value: number) {
this.frame = value;
this.timeChanged.dispatch(this.getTime());
}
private consumeCommands(): PlayerCommands {
const commands = {...this.commands};
this.commands.reset = false;
this.commands.seek = -1;
this.commands.recalculate = false;
return commands;
}
public constructor(public readonly project: Project, audioSrc?: string) {
public constructor(
public readonly project: Project,
audioSrc?: string,
public readonly labels?: Record<string, number>,
) {
this.startTime = performance.now();
const savedState = localStorage.getItem(STORAGE_KEY);
@@ -93,6 +148,7 @@ export class Player {
const state = JSON.parse(savedState) as PlayerState;
this.state.paused = state.paused;
this.state.startFrame = state.startFrame;
this.state.endFrame = state.endFrame;
this.state.loop = state.loop;
this.state.speed = state.speed;
this.state.muted = state.muted;
@@ -100,26 +156,20 @@ export class Player {
if (audioSrc) {
this.audio = new Audio(audioSrc);
this.audio.addEventListener('durationchange', () => {
this.updateState({
duration: this.project.secondsToFrames(this.audio.duration),
});
this.requestSeek(this.state.duration);
});
}
this.request();
}
public reload() {
this.requestSeek(this.project.frame);
this.commands.recalculate = true;
if (this.requestId === null) {
this.request();
}
}
public requestNextFrame(): void {
this.commands.seek = this.state.frame + 1;
this.commands.seek = this.frame + 1;
}
public requestReset(): void {
@@ -127,10 +177,10 @@ export class Player {
}
public requestSeek(value: number): void {
this.commands.seek = value;
if (value < this.state.startFrame) {
this.updateState({startFrame: value});
}
this.commands.seek = this.inRange(value, this.state);
// if (value < this.state.startFrame) {
// this.updateState({startFrame: value});
// }
}
public togglePlayback(value?: boolean): void {
@@ -155,7 +205,24 @@ export class Player {
let commands = this.consumeCommands();
let state = {...this.state};
if (state.finished && state.loop && commands.seek < 0) {
commands.seek = 0;
commands.seek = state.startFrame;
}
// Recalculate
if (commands.recalculate) {
await this.project.recalculate();
const duration = this.project.frame;
const finished = await this.project.seek(this.frame);
this.project.draw();
this.updateState({
duration,
finished,
});
if (this.frame + 1 !== this.project.frame) {
this.updateFrame(this.project.frame);
}
this.request();
return;
}
// Pause / play audio.
@@ -185,23 +252,24 @@ export class Player {
frame: this.project.frame,
blob: await this.getContent(),
});
if (state.finished) {
if (state.finished || this.project.frame >= state.endFrame) {
state.render = false;
}
this.updateState({
finished: state.finished,
render: state.render,
frame: this.project.frame,
});
this.updateFrame(this.project.frame);
this.request();
return;
}
// Seek to the given frame
if (commands.seek >= 0 || this.project.frame < state.startFrame) {
const seekFrame = Math.max(commands.seek, state.startFrame);
state.finished = await this.project.seek(seekFrame, state.speed);
if (commands.seek >= 0 || !this.isInRange(this.project.frame, state)) {
const seekFrame = commands.seek < 0 ? this.project.frame : commands.seek;
const clampedFrame = this.inRange(seekFrame, state);
state.finished = await this.project.seek(clampedFrame, state.speed);
this.syncAudio(-3);
}
// Do nothing if paused or is ahead of the audio.
@@ -224,7 +292,7 @@ export class Player {
state.finished = await this.project.seek(seekFrame, state.speed);
}
// Simply move forward one frame
else {
else if (this.project.frame < state.endFrame) {
state.finished = await this.project.next(state.speed);
// Synchronize audio.
@@ -238,21 +306,31 @@ export class Player {
// handle finishing
if (state.finished) {
state.duration = this.project.frame;
if (commands.seek >= 0) {
this.requestSeek(state.startFrame);
}
}
this.updateState({
finished: state.finished,
duration: state.duration,
frame: this.project.frame,
finished: state.finished || this.project.frame >= state.endFrame,
});
this.updateFrame(this.project.frame);
this.request();
}
private inRange(frame: number, state: PlayerState): number {
return frame > state.endFrame
? state.endFrame
: frame < state.startFrame
? state.startFrame
: frame;
}
private isInRange(frame: number, state: PlayerState): boolean {
return frame >= state.startFrame && frame <= state.endFrame;
}
private hasAudio(): boolean {
return this.audio && !this.audioError;
}

View File

@@ -1,52 +0,0 @@
import {GeneratorHelper} from '../helpers';
import {Player} from './Player';
import {Thread} from '../threading';
const STORAGE_KEY = 'threads-monitor-state';
export class ThreadsMonitor {
private list = document.createElement('ul');
public constructor(
private readonly player: Player,
private readonly root: HTMLDetailsElement,
) {
const savedState = localStorage.getItem(STORAGE_KEY);
if (savedState) {
this.root.open = JSON.parse(savedState);
}
this.player.project.threadsCallback = this.render;
this.root.addEventListener('toggle', this.handleToggle);
this.root.appendChild(this.list);
}
private handleToggle = () => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.root.open));
};
private render = (rootThread: Thread) => {
if (!this.root.open) return;
this.list.firstElementChild?.remove();
const queue: [Thread, HTMLElement][] = [[rootThread, this.list]];
while (queue.length > 0) {
const [thread, parent] = queue.shift();
const element = this.createRunnerElement(thread);
parent.appendChild(element);
for (const child of thread.children) {
queue.push([child, element]);
}
}
};
private createRunnerElement(thread: Thread): HTMLElement {
const element = document.createElement('ul');
const title = document.createElement('li');
title.innerText = GeneratorHelper.getName(thread.runner);
element.appendChild(title);
element.classList.toggle('cancelled', thread.canceled);
return element;
}
}

View File

@@ -1,110 +0,0 @@
import {clampRemap} from '../tweening';
import {Player, PlayerState} from './Player';
import {clamp} from 'three/src/math/MathUtils';
export class Timeline {
private readonly fillTime: HTMLElement;
private readonly fillSeek: HTMLElement;
private readonly fillStart: HTMLElement;
private readonly track: HTMLElement;
private readonly marker: HTMLElement;
private readonly timeText: HTMLElement;
private readonly durationText: HTMLElement;
private duration: number;
private labelElements: Record<string, HTMLElement> = {};
private lastState: PlayerState;
public constructor(
private readonly player: Player,
private readonly root: HTMLElement,
private readonly labels: Record<string, number>,
) {
this.fillTime = root.querySelector('.fill-time')!;
this.fillSeek = root.querySelector('.fill-seek')!;
this.fillStart = root.querySelector('.fill-start')!;
this.timeText = root.querySelector('.js-current-time')!;
this.durationText = root.querySelector('.js-duration')!;
this.track = root.querySelector('.track')!;
this.marker = root.querySelector('.marker')!;
this.player.StateChanged.sub(this.update);
this.root.addEventListener('click', e => {
const target = <HTMLElement>e.target;
if (target === this.timeText) {
this.player.updateState({startFrame: this.lastState.frame});
return;
}
this.player.requestSeek(
target.classList.contains('label')
? parseFloat(target.dataset.time!)
: this.mousePositionToFrame(e.clientX),
);
});
this.root.addEventListener('contextmenu', e => {
e.preventDefault();
this.player.updateState({
startFrame: this.mousePositionToFrame(e.clientX),
});
});
this.root.addEventListener('mousemove', e => {
const target = <HTMLElement>e.target;
const rect = this.track.getBoundingClientRect();
let x = clamp(e.clientX - rect.left, 8, rect.width);
if (target.classList.contains('label')) {
const frame = parseInt(target.dataset.time!);
x = (frame / this.duration) * rect.width;
}
this.fillSeek.style.width = `${x}px`;
});
for (const label in labels) {
const element = document.createElement('div');
element.classList.add('label');
element.dataset.title = label;
element.dataset.time = this.player.project
.secondsToFrames(labels[label])
.toString();
this.track.appendChild(element);
this.labelElements[label] = element;
}
}
private mousePositionToFrame(position: number) {
const rect = this.track.getBoundingClientRect();
const x = position - rect.left;
return Math.floor(clampRemap(0, rect.width, 0, this.duration, x));
}
private update = (state: PlayerState) => {
this.lastState = {...state};
this.timeText.innerText = state.frame.toString();
this.marker.dataset.title = `Frame:${state.startFrame}`;
const width = this.track.clientWidth;
const fillWidth = clampRemap(1, state.duration, 8, width, state.frame);
const startWidth = clampRemap(
1,
state.duration,
8,
width,
state.startFrame,
);
this.marker.style.left = `${startWidth - 8}px`;
this.fillStart.style.width = `${startWidth}px`;
this.fillTime.style.width = `${fillWidth}px`;
this.durationText.innerText = state.duration.toString();
this.duration = state.duration;
for (const label in this.labels) {
const element = this.labelElements[label];
const time = parseInt(element.dataset.time);
const elementLeft = clampRemap(1, state.duration, 8, width, time) - 4;
element.style.left = `${elementLeft - 20}px`;
element.classList.toggle('hidden', time > state.duration);
element.classList.toggle('inverted', time > state.startFrame);
}
};
}

View File

@@ -9,14 +9,38 @@ const projectFile = path.resolve(process.cwd(), process.argv[2]);
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const compiler = webpack({
entry: projectFile,
entry: {
index: projectFile,
ui: path.resolve(__dirname, '../../ui/src/index.ts'),
},
mode: 'development',
devtool: 'inline-source-map',
module: {
rules: [
{
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'),
instance: 'ui',
},
},
{
test: /\.tsx?$/,
exclude: path.resolve(__dirname, '../../ui/'),
loader: 'ts-loader',
options: {
instance: 'project',
},
},
{
test: /\.glsl$/i,
@@ -59,13 +83,17 @@ const compiler = webpack({
modules: ['node_modules', path.resolve(__dirname, '../node_modules')],
extensions: ['.js', '.ts', '.tsx'],
alias: {
MC: path.resolve(__dirname, '../dist'),
'@motion-canvas/core': path.resolve(__dirname, '../dist'),
'@motion-canvas/ui': path.resolve(__dirname, '../../ui/dist'),
MC: path.resolve(__dirname, '../src'),
'@motion-canvas/core': path.resolve(__dirname, '../src'),
},
},
optimization: {
runtimeChunk: {
name: 'runtime',
},
},
output: {
filename: `index.js`,
filename: `[name].js`,
path: __dirname,
},
experiments: {

View File

@@ -11,10 +11,8 @@
"jsx": "react-jsx",
"jsxImportSource": "@motion-canvas/core",
"paths": {
"MC/*": ["../../core/dist/*"],
"@motion-canvas/core/*": ["../../core/dist/*"],
"@motion-canvas/ui": ["../../ui/dist"],
"@motion-canvas/ui/*": ["../../ui/dist/*"],
"MC/*": ["../../core/src/*"],
"@motion-canvas/core/*": ["../../core/src/*"],
"*": ["../../core/node_modules/*", "../../core/node_modules/@types/*"]
}
}