feat: project variables (#255)

Adds variables class to store and set project variables signals
This commit is contained in:
Tim Hawkins
2023-02-09 14:48:44 +00:00
committed by GitHub
parent 2ee0eadb09
commit 4883295259
7 changed files with 123 additions and 1 deletions

View File

@@ -27,6 +27,7 @@ export interface ProjectConfig {
canvas?: HTMLCanvasElement;
size?: Vector2;
background?: string | false;
variables?: Record<string, unknown>;
}
export enum PlaybackState {
@@ -148,6 +149,15 @@ export class Project {
return this.size;
}
public setVariables(variables: Record<string, unknown>) {
this.variables = variables;
this.currentScene.current?.variables.updateSignals();
}
public getVariables(): {[key: string]: any} {
return this.variables;
}
public readonly name: string;
public readonly audio = new AudioManager();
public readonly logger = new Logger();
@@ -170,6 +180,7 @@ export class Project {
private previousBufferContext: CanvasRenderingContext2D | null = null;
private exportCounter = 0;
private size = Vector2.zero;
private variables: Record<string, unknown> = {};
/**
* @deprecated Use {@link makeProject} instead.
@@ -194,6 +205,7 @@ export class Project {
canvas,
size = ProjectSize.FullHD,
background = false,
variables = {},
} = typeof name === 'string' ? config! : name;
this.name = typeof name === 'string' ? name : '';
@@ -202,6 +214,7 @@ export class Project {
this.creationStack = new Error().stack ?? '';
this.setCanvas(canvas);
this.setSize(size);
this.setVariables(variables);
this.background = background;
if (audio) {

View File

@@ -7,6 +7,7 @@ import {
} from '../threading';
import {Meta} from '../Meta';
import {TimeEvents} from './TimeEvents';
import {Variables} from './Variables';
import {EventDispatcher, ValueDispatcher} from '../events';
import {Project} from '../Project';
import {decorate, threadable} from '../decorators';
@@ -41,6 +42,7 @@ export abstract class GeneratorScene<T>
public readonly project: Project;
public readonly meta: Meta<SceneMetadata>;
public readonly timeEvents: TimeEvents;
public readonly variables: Variables;
public random: Random;
public creationStack?: string;
@@ -113,6 +115,7 @@ export abstract class GeneratorScene<T>
decorate(this.runnerFactory, threadable(this.name));
this.timeEvents = new TimeEvents(this);
this.variables = new Variables(this);
let seed = this.meta.getData().seed;
if (typeof seed !== 'number') {

View File

@@ -1,6 +1,7 @@
import type {Project} from '../Project';
import {Meta, Metadata} from '../Meta';
import {SavedTimeEvent, TimeEvents} from './TimeEvents';
import {Variables} from './Variables';
import {
SubscribableEvent,
SubscribableValueEvent,
@@ -122,6 +123,7 @@ export interface Scene<T = unknown> {
*/
readonly project: Project;
readonly timeEvents: TimeEvents;
readonly variables: Variables;
readonly random: Random;
readonly meta: Meta<SceneMetadata>;
creationStack?: string;

View File

@@ -0,0 +1,49 @@
import type {Scene} from './Scene';
import {createSignal, SimpleSignal} from '../signals';
export class Variables {
private signals: {[key: string]: SimpleSignal<any>} = {};
public constructor(private readonly scene: Scene) {
scene.onReset.subscribe(this.handleReset);
}
/**
* Get variable signal if exists or create signal if not
*
* @param name - The name of the variable.
*/
public get<T>(name: string, initial: T): SimpleSignal<T> {
const variables = this.scene.project.getVariables();
if (!(name in variables)) {
this.scene.project.logger.warn(
`Variable ${name} has not been set in project ${this.scene.project.name}`,
);
this.signals[name] = createSignal(initial);
}
if (!(name in this.signals)) {
this.signals[name] = createSignal(variables[name]);
}
return this.signals[name];
}
/**
* Update all signals with new project variable values.
*/
public updateSignals = () => {
const variables = this.scene.project.getVariables();
Object.keys(variables).map(variableName => {
if (variableName in this.signals) {
this.signals[variableName](variables[variableName]);
}
});
};
/**
* Reset all stored signals.
*/
public handleReset = () => {
this.signals = {};
};
}

View File

@@ -11,4 +11,5 @@ export * from './Scene';
export * from './SceneState';
export * from './Threadable';
export * from './TimeEvents';
export * from './Variables';
export * from './LifecycleEvents';

View File

@@ -0,0 +1,38 @@
---
slug: /project-variables
---
# Project variables
Making animations dynamic can be achieved using project variables. These could
be used for light/dark themes, changing Node styling or editing text content.
Project variables make use of signals - allowing variables to be updated during
animations.
Adding variables via the player component can be done by passing a stringified
json object to the variables attribute:
```html
<motion-canvas-player
src="/@id/__x00__virtual:template"
style="aspect-ratio: 16 / 9"
variables='{"circleFill":"red"}'
></motion-canvas-player>
```
They can also be added using makeProject():
```ts
export default makeProject({
scenes: [example],
background: '#141414',
variables: {circleFill: 'red'},
});
```
Accessing the variables inside of a scene is through
[`useScene()`](/api/core/utils#useScene):
```ts
const circleFill = useScene().variables.get('circleFill');
```

View File

@@ -15,7 +15,7 @@ enum State {
class MotionCanvasPlayer extends HTMLElement {
public static get observedAttributes() {
return ['src', 'quality', 'width', 'height', 'auto'];
return ['src', 'quality', 'width', 'height', 'auto', 'variables'];
}
private get auto() {
@@ -42,6 +42,16 @@ class MotionCanvasPlayer extends HTMLElement {
return attr ? parseFloat(attr) : this.defaultHeight;
}
private get variables() {
try {
const attr = this.getAttribute('variables');
return attr ? JSON.parse(attr) : {};
} catch {
this.project.logger.warn(`Project variables could not be parsed.`);
return {};
}
}
private readonly root: ShadowRoot;
private readonly canvas: HTMLCanvasElement;
private readonly overlay: HTMLCanvasElement;
@@ -71,6 +81,7 @@ class MotionCanvasPlayer extends HTMLElement {
this.overlay.addEventListener('mousemove', this.handleMouseMove);
this.overlay.addEventListener('mouseleave', this.handleMouseLeave);
this.button.addEventListener('mousedown', this.handleMouseDown);
this.setState(State.Initial);
}
@@ -170,6 +181,7 @@ class MotionCanvasPlayer extends HTMLElement {
await Promise.any([delay, promise]);
this.setState(State.Loading);
project = (await promise).default();
project.setVariables(this.variables);
} catch (e) {
this.setState(State.Error);
return;
@@ -245,6 +257,10 @@ class MotionCanvasPlayer extends HTMLElement {
this.project.setSize([this.width, this.height]);
}
break;
case 'variables':
if (this.project) {
this.project.setVariables(this.variables);
}
}
}