mirror of
https://github.com/motion-canvas/motion-canvas.git
synced 2026-01-12 07:18:01 -05:00
feat: improve async signals (#156)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import {computed, initial, signal} from '../decorators';
|
||||
import {
|
||||
Color,
|
||||
PossibleVector2,
|
||||
Rect as RectType,
|
||||
SerializedVector2,
|
||||
Vector2,
|
||||
@@ -124,20 +125,37 @@ export class Image extends Rect {
|
||||
);
|
||||
}
|
||||
|
||||
public getColorAtPoint(position: Vector2): Color {
|
||||
/**
|
||||
* Get color of the image at the given position.
|
||||
*
|
||||
* @param position - The position in local space at which to sample the color.
|
||||
*/
|
||||
public getColorAtPoint(position: PossibleVector2): Color {
|
||||
const image = this.image();
|
||||
const size = this.computedSize();
|
||||
const naturalSize = new Vector2(image.naturalWidth, image.naturalHeight);
|
||||
|
||||
const pixelPosition = new Vector2(position)
|
||||
.add(this.computedSize().scale(0.5))
|
||||
.mul(naturalSize.div(size).safe);
|
||||
|
||||
return this.getPixelColor(pixelPosition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color of the image at the given pixel.
|
||||
*
|
||||
* @param position - The pixel's position.
|
||||
*/
|
||||
public getPixelColor(position: PossibleVector2): Color {
|
||||
const context = this.filledImageCanvas();
|
||||
const relativePosition = position.add(this.computedSize().scale(0.5));
|
||||
const data = context.getImageData(
|
||||
relativePosition.x,
|
||||
relativePosition.y,
|
||||
1,
|
||||
1,
|
||||
).data;
|
||||
const vector = new Vector2(position);
|
||||
const data = context.getImageData(vector.x, vector.y, 1, 1).data;
|
||||
|
||||
return new Color({
|
||||
r: data[0] / 255,
|
||||
g: data[1] / 255,
|
||||
b: data[2] / 255,
|
||||
r: data[0],
|
||||
g: data[1],
|
||||
b: data[2],
|
||||
a: data[3] / 255,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -948,20 +948,6 @@ export class Node implements Promisable<Node> {
|
||||
return hit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for any asynchronous resources that this node or its children have.
|
||||
*
|
||||
* @remarks
|
||||
* Certain resources like images are always loaded asynchronously.
|
||||
* Awaiting this method makes sure that all such resources are done loading
|
||||
* before continuing the animation.
|
||||
*/
|
||||
public waitForAsyncResources() {
|
||||
this.collectAsyncResources();
|
||||
const promises = DependencyContext.consumePromises();
|
||||
return Promise.all(promises.map(handle => handle.promise));
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all asynchronous resources used by this node.
|
||||
*/
|
||||
@@ -971,8 +957,21 @@ export class Node implements Promisable<Node> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for any asynchronous resources that this node or its children have.
|
||||
*
|
||||
* @remarks
|
||||
* Certain resources like images are always loaded asynchronously.
|
||||
* Awaiting this method makes sure that all such resources are done loading
|
||||
* before continuing the animation.
|
||||
*/
|
||||
public async toPromise(): Promise<this> {
|
||||
await this.waitForAsyncResources();
|
||||
let promises = DependencyContext.consumePromises();
|
||||
do {
|
||||
await Promise.all(promises.map(handle => handle.promise));
|
||||
this.collectAsyncResources();
|
||||
promises = DependencyContext.consumePromises();
|
||||
} while (promises.length > 0);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -177,6 +177,8 @@ export class Video extends Rect {
|
||||
|
||||
protected setCurrentTime(value: number) {
|
||||
const video = this.video();
|
||||
if (video.readyState < 2) return;
|
||||
|
||||
video.currentTime = value;
|
||||
this.lastTime = value;
|
||||
if (video.seeking) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {FlagDispatcher, Subscribable} from '../events';
|
||||
import {DetailedError} from '../utils';
|
||||
import {Promisable} from '../threading';
|
||||
|
||||
export interface PromiseHandle<T> {
|
||||
promise: Promise<T>;
|
||||
@@ -8,7 +9,9 @@ export interface PromiseHandle<T> {
|
||||
owner?: any;
|
||||
}
|
||||
|
||||
export class DependencyContext<TOwner = void> {
|
||||
export class DependencyContext<TOwner = void>
|
||||
implements Promisable<DependencyContext<TOwner>>
|
||||
{
|
||||
protected static collectionSet = new Set<DependencyContext<any>>();
|
||||
protected static collectionStack: DependencyContext<any>[] = [];
|
||||
protected static promises: PromiseHandle<any>[] = [];
|
||||
@@ -28,7 +31,7 @@ export class DependencyContext<TOwner = void> {
|
||||
stack: new Error().stack,
|
||||
};
|
||||
|
||||
const context = this.collectionStack.at(-2);
|
||||
const context = this.collectionStack.at(-1);
|
||||
if (context) {
|
||||
handle.owner = context.owner;
|
||||
}
|
||||
@@ -59,6 +62,10 @@ export class DependencyContext<TOwner = void> {
|
||||
Object.defineProperty(this.invokable, 'context', {
|
||||
value: this,
|
||||
});
|
||||
|
||||
Object.defineProperty(this.invokable, 'toPromise', {
|
||||
value: this.toPromise.bind(this),
|
||||
});
|
||||
}
|
||||
|
||||
protected invoke() {
|
||||
@@ -92,4 +99,14 @@ export class DependencyContext<TOwner = void> {
|
||||
this.event.subscribe(signal.markDirty);
|
||||
}
|
||||
}
|
||||
|
||||
public async toPromise(): Promise<this> {
|
||||
let promises = DependencyContext.consumePromises();
|
||||
do {
|
||||
await Promise.all(promises.map(handle => handle.promise));
|
||||
this.invokable();
|
||||
promises = DependencyContext.consumePromises();
|
||||
} while (promises.length > 0);
|
||||
return this.invokable;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {describe, test, expect} from 'vitest';
|
||||
import {createSignal} from './createSignal';
|
||||
|
||||
import {createComputedAsync} from './createComputedAsync';
|
||||
import {createSignal} from './createSignal';
|
||||
|
||||
function sleep(duration = 0) {
|
||||
return new Promise(resolve => setTimeout(resolve, duration));
|
||||
@@ -8,7 +9,10 @@ function sleep(duration = 0) {
|
||||
|
||||
describe('createComputedAsync()', () => {
|
||||
test('Value is updated when the promise resolves', async () => {
|
||||
const computed = createComputedAsync(() => sleep().then(() => true), false);
|
||||
const computed = createComputedAsync(async () => {
|
||||
await sleep();
|
||||
return true;
|
||||
}, false);
|
||||
const signal = createSignal(() => computed());
|
||||
|
||||
expect(signal()).toBe(false);
|
||||
@@ -17,4 +21,24 @@ describe('createComputedAsync()', () => {
|
||||
|
||||
expect(signal()).toBe(true);
|
||||
});
|
||||
test('Value is updated when its dependencies change', async () => {
|
||||
const dependency = createSignal(2);
|
||||
const computed = createComputedAsync(async () => {
|
||||
const value = dependency();
|
||||
await sleep();
|
||||
return value;
|
||||
}, 1);
|
||||
const signal = createSignal(() => computed());
|
||||
|
||||
expect(signal()).toBe(1);
|
||||
|
||||
await sleep(1);
|
||||
expect(signal()).toBe(2);
|
||||
|
||||
dependency(3);
|
||||
expect(signal()).toBe(2);
|
||||
|
||||
await sleep(1);
|
||||
expect(signal()).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import {createComputed} from './createComputed';
|
||||
import {Computed, ComputedContext} from '../signals';
|
||||
import {
|
||||
Computed,
|
||||
ComputedContext,
|
||||
createSignal,
|
||||
PromiseHandle,
|
||||
} from '../signals';
|
||||
|
||||
export function createComputedAsync<T>(
|
||||
factory: () => Promise<T>,
|
||||
@@ -12,8 +17,17 @@ export function createComputedAsync<T>(
|
||||
factory: () => Promise<T>,
|
||||
initial: T | null = null,
|
||||
): Computed<T | null> {
|
||||
const handle = createComputed(() =>
|
||||
ComputedContext.collectPromise(factory(), initial),
|
||||
);
|
||||
return createComputed(() => handle().value);
|
||||
let handle: PromiseHandle<T | null>;
|
||||
const signal = createSignal(factory);
|
||||
return createComputed(() => {
|
||||
const promise = signal();
|
||||
if (!handle || handle.promise !== promise) {
|
||||
handle = ComputedContext.collectPromise(
|
||||
promise,
|
||||
handle?.value ?? initial,
|
||||
);
|
||||
}
|
||||
|
||||
return handle.value;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,7 +5,11 @@ export interface Promisable<T> {
|
||||
}
|
||||
|
||||
export function isPromisable(value: any): value is Promisable<any> {
|
||||
return value && typeof value === 'object' && 'toPromise' in value;
|
||||
return (
|
||||
value &&
|
||||
(typeof value === 'object' || typeof value === 'function') &&
|
||||
'toPromise' in value
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user