diff --git a/package-lock.json b/package-lock.json index 5d3d7192..8e333bc9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4570,6 +4570,10 @@ "resolved": "packages/docs", "link": true }, + "node_modules/@motion-canvas/examples": { + "resolved": "packages/examples", + "link": true + }, "node_modules/@motion-canvas/template": { "resolved": "packages/template", "link": true @@ -13200,7 +13204,8 @@ }, "node_modules/load-script": { "version": "1.0.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz", + "integrity": "sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==" }, "node_modules/loader-runner": { "version": "4.3.0", @@ -13530,7 +13535,8 @@ }, "node_modules/memoize-one": { "version": "5.2.1", - "license": "MIT" + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" }, "node_modules/meow": { "version": "8.1.2", @@ -13979,10 +13985,6 @@ "color-name": "^1.1.4" } }, - "node_modules/mp4box": { - "version": "0.5.2", - "license": "BSD-3-Clause" - }, "node_modules/mrmime": { "version": "1.0.1", "license": "MIT", @@ -16378,8 +16380,9 @@ } }, "node_modules/react-player": { - "version": "2.10.1", - "license": "MIT", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/react-player/-/react-player-2.11.0.tgz", + "integrity": "sha512-fIrwpuXOBXdEg1FiyV9isKevZOaaIsAAtZy5fcjkQK9Nhmk1I2NXzY/hkPos8V0zb/ZX416LFy8gv7l/1k3a5w==", "dependencies": { "deepmerge": "^4.0.0", "load-script": "^1.0.0", @@ -20257,8 +20260,7 @@ "dependencies": { "@types/chroma-js": "^2.1.4", "chroma-js": "^2.4.2", - "mix-color": "^1.1.2", - "mp4box": "^0.5.2" + "mix-color": "^1.1.2" }, "devDependencies": { "@types/dom-webcodecs": "^0.1.4", @@ -20316,6 +20318,33 @@ "node": ">=16.14" } }, + "packages/examples": { + "name": "@motion-canvas/examples", + "version": "0.0.0", + "dependencies": { + "@motion-canvas/2d": "*", + "@motion-canvas/core": "*" + }, + "devDependencies": { + "@motion-canvas/ui": "*", + "@motion-canvas/vite-plugin": "*", + "vite": "^3.0.5" + } + }, + "packages/player": { + "name": "@motion-canvas/player", + "version": "0.0.0", + "extraneous": true, + "license": "MIT", + "devDependencies": { + "@motion-canvas/core": "^12.0.0-alpha.2", + "typescript": "^4.6.4", + "vite": "^3.0.4" + }, + "peerDependencies": { + "@motion-canvas/core": "*" + } + }, "packages/template": { "name": "@motion-canvas/template", "version": "0.0.0", @@ -23443,7 +23472,6 @@ "jest-canvas-mock": "^2.4.0", "jest-environment-jsdom": "^28.1.2", "mix-color": "^1.1.2", - "mp4box": "^0.5.2", "ts-jest": "^28.0.5", "typescript": "^4.7.4", "vite": "^3.0.5" @@ -23477,6 +23505,16 @@ "typescript": "^4.7.4" } }, + "@motion-canvas/examples": { + "version": "file:packages/examples", + "requires": { + "@motion-canvas/2d": "*", + "@motion-canvas/core": "*", + "@motion-canvas/ui": "*", + "@motion-canvas/vite-plugin": "*", + "vite": "^3.0.5" + } + }, "@motion-canvas/template": { "version": "file:packages/template", "requires": { @@ -28950,7 +28988,9 @@ } }, "load-script": { - "version": "1.0.0" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz", + "integrity": "sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==" }, "loader-runner": { "version": "4.3.0" @@ -29155,7 +29195,9 @@ } }, "memoize-one": { - "version": "5.2.1" + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" }, "meow": { "version": "8.1.2", @@ -29432,9 +29474,6 @@ "color-name": "^1.1.4" } }, - "mp4box": { - "version": "0.5.2" - }, "mrmime": { "version": "1.0.1" }, @@ -30861,7 +30900,9 @@ } }, "react-player": { - "version": "2.10.1", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/react-player/-/react-player-2.11.0.tgz", + "integrity": "sha512-fIrwpuXOBXdEg1FiyV9isKevZOaaIsAAtZy5fcjkQK9Nhmk1I2NXzY/hkPos8V0zb/ZX416LFy8gv7l/1k3a5w==", "requires": { "deepmerge": "^4.0.0", "load-script": "^1.0.0", diff --git a/packages/2d/src/decorators/property.ts b/packages/2d/src/decorators/property.ts index c5703f23..7c4f3762 100644 --- a/packages/2d/src/decorators/property.ts +++ b/packages/2d/src/decorators/property.ts @@ -15,7 +15,10 @@ import { SignalTween, SignalUtils, useLogger, + SignalGenerator, } from '@motion-canvas/core/lib/utils'; +import {decorate, threadable} from '@motion-canvas/core/lib/decorators'; +import {ThreadGenerator} from '@motion-canvas/core/lib/threading'; export function capitalize(value: T): Capitalize { return >(value[0].toUpperCase() + value.slice(1)); @@ -131,23 +134,56 @@ export function createProperty< ); } - const from = getter(); - return tween( + return makeAnimate(timingFunction, interpolationFunction)( + newValue, duration, - value => { - setter( - interpolationFunction( - from, - unwrap(newValue), - timingFunction(value), - ), - ); - }, - () => setter(wrap(newValue)), ); } ); + function makeAnimate( + defaultTimingFunction: TimingFunction, + defaultInterpolationFunction: InterpolationFunction, + before?: ThreadGenerator, + ) { + function animate( + value: SignalValue, + duration: number, + timingFunction = defaultTimingFunction, + interpolationFunction = defaultInterpolationFunction, + ) { + const task = >( + makeTask(value, duration, timingFunction, interpolationFunction, before) + ); + task.to = makeAnimate(timingFunction, interpolationFunction, task); + return task; + } + + return >animate; + } + + decorate(makeTask, threadable()); + function* makeTask( + value: SignalValue, + duration: number, + timingFunction: TimingFunction, + interpolationFunction: InterpolationFunction, + before?: ThreadGenerator, + ) { + if (before) { + yield* before; + } + + const from = getter(); + yield* tween( + duration, + v => { + setter(interpolationFunction(from, unwrap(value), timingFunction(v))); + }, + () => setter(wrap(value)), + ); + } + Object.defineProperty(handler, 'reset', { configurable: true, value: signal diff --git a/packages/2d/src/scenes/View2D.ts b/packages/2d/src/scenes/View2D.ts index fcc032fa..d1a1e86a 100644 --- a/packages/2d/src/scenes/View2D.ts +++ b/packages/2d/src/scenes/View2D.ts @@ -14,6 +14,7 @@ export class View2D extends Layout { frame.style.top = '0'; frame.style.left = '0'; frame.style.opacity = '0'; + frame.style.overflow = 'hidden'; document.body.prepend(frame); } View2D.shadowRoot = frame.shadowRoot ?? frame.attachShadow({mode: 'open'}); diff --git a/packages/2d/tsconfig.json b/packages/2d/tsconfig.json index 5e639515..818037ad 100644 --- a/packages/2d/tsconfig.json +++ b/packages/2d/tsconfig.json @@ -14,6 +14,7 @@ "experimentalDecorators": true, "jsx": "react-jsx", "jsxImportSource": "@motion-canvas/2d/lib", + "importHelpers": true, "paths": { "@motion-canvas/2d/lib/jsx-runtime": ["jsx-runtime.ts"] }, diff --git a/packages/core/package.json b/packages/core/package.json index 5ad6505f..4fdf3db1 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -28,8 +28,7 @@ "dependencies": { "@types/chroma-js": "^2.1.4", "chroma-js": "^2.4.2", - "mix-color": "^1.1.2", - "mp4box": "^0.5.2" + "mix-color": "^1.1.2" }, "devDependencies": { "@types/dom-webcodecs": "^0.1.4", diff --git a/packages/core/src/media/MP4Demuxer.js b/packages/core/src/media/MP4Demuxer.js deleted file mode 100644 index 467aef34..00000000 --- a/packages/core/src/media/MP4Demuxer.js +++ /dev/null @@ -1,176 +0,0 @@ -import {createFile} from 'mp4box/dist/mp4box.all'; - -class MP4Source { - constructor(uri) { - this.file = createFile(); - this.file.onError = console.error.bind(console); - this.file.onReady = this.onReady.bind(this); - this.file.onSamples = this.onSamples.bind(this); - - fetch(uri).then(response => { - const reader = response.body.getReader(); - let offset = 0; - let mp4File = this.file; - - function appendBuffers({done, value}) { - if (done) { - mp4File.flush(); - return; - } - - let buf = value.buffer; - buf.fileStart = offset; - - offset += buf.byteLength; - - mp4File.appendBuffer(buf); - - return reader.read().then(appendBuffers); - } - - return reader.read().then(appendBuffers); - }); - - this.info = null; - this._info_resolver = null; - } - - onReady(info) { - // TODO: Generate configuration changes. - this.info = info; - - if (this._info_resolver) { - this._info_resolver(info); - this._info_resolver = null; - } - } - - getInfo() { - if (this.info) return Promise.resolve(this.info); - - return new Promise(resolver => { - this._info_resolver = resolver; - }); - } - - getAvccBox() { - // TODO: make sure this is coming from the right track. - return this.file.moov.traks[0].mdia.minf.stbl.stsd.entries[0].avcC; - } - - start(track, onChunk) { - this._onChunk = onChunk; - this.file.setExtractionOptions(track.id); - this.file.start(); - } - - onSamples(track_id, ref, samples) { - for (const sample of samples) { - const type = sample.is_sync ? 'key' : 'delta'; - - const chunk = new EncodedVideoChunk({ - type: type, - timestamp: sample.cts, - duration: sample.duration, - data: sample.data, - }); - - this._onChunk(chunk); - } - } -} - -class Writer { - constructor(size) { - this.data = new Uint8Array(size); - this.idx = 0; - this.size = size; - } - - getData() { - if (this.idx != this.size) - throw 'Mismatch between size reserved and sized used'; - - return this.data.slice(0, this.idx); - } - - writeUint8(value) { - this.data.set([value], this.idx); - this.idx++; - } - - writeUint16(value) { - // TODO: find a more elegant solution to endianess. - var arr = new Uint16Array(1); - arr[0] = value; - var buffer = new Uint8Array(arr.buffer); - this.data.set([buffer[1], buffer[0]], this.idx); - this.idx += 2; - } - - writeUint8Array(value) { - this.data.set(value, this.idx); - this.idx += value.length; - } -} - -export class MP4Demuxer { - constructor(uri) { - this.source = new MP4Source(uri); - } - - getExtradata(avccBox) { - var i; - var size = 7; - for (i = 0; i < avccBox.SPS.length; i++) { - // nalu length is encoded as a uint16. - size += 2 + avccBox.SPS[i].length; - } - for (i = 0; i < avccBox.PPS.length; i++) { - // nalu length is encoded as a uint16. - size += 2 + avccBox.PPS[i].length; - } - - var writer = new Writer(size); - - writer.writeUint8(avccBox.configurationVersion); - writer.writeUint8(avccBox.AVCProfileIndication); - writer.writeUint8(avccBox.profile_compatibility); - writer.writeUint8(avccBox.AVCLevelIndication); - writer.writeUint8(avccBox.lengthSizeMinusOne + (63 << 2)); - - writer.writeUint8(avccBox.nb_SPS_nalus + (7 << 5)); - for (i = 0; i < avccBox.SPS.length; i++) { - writer.writeUint16(avccBox.SPS[i].length); - writer.writeUint8Array(avccBox.SPS[i].nalu); - } - - writer.writeUint8(avccBox.nb_PPS_nalus); - for (i = 0; i < avccBox.PPS.length; i++) { - writer.writeUint16(avccBox.PPS[i].length); - writer.writeUint8Array(avccBox.PPS[i].nalu); - } - - return writer.getData(); - } - - async getConfig() { - let info = await this.source.getInfo(); - this.track = info.videoTracks[0]; - - var extradata = this.getExtradata(this.source.getAvccBox()); - - let config = { - codec: this.track.codec, - codedHeight: this.track.video.height, - codedWidth: this.track.video.width, - description: extradata, - }; - - return Promise.resolve(config); - } - - start(onChunk) { - this.source.start(this.track, onChunk); - } -} diff --git a/packages/core/src/media/index.ts b/packages/core/src/media/index.ts index cd1ada0b..69713b72 100644 --- a/packages/core/src/media/index.ts +++ b/packages/core/src/media/index.ts @@ -1,4 +1,3 @@ export * from './AudioData'; export * from './AudioManager'; export * from './loadImage'; -export * from './loadVideo'; diff --git a/packages/core/src/media/loadVideo.ts b/packages/core/src/media/loadVideo.ts deleted file mode 100644 index 0413b3bd..00000000 --- a/packages/core/src/media/loadVideo.ts +++ /dev/null @@ -1,58 +0,0 @@ -import {MP4Demuxer} from './MP4Demuxer'; -import {useLogger} from '../utils'; - -const videos = new WeakMap(); -const keys: Record = {}; - -export async function loadVideo(src: string) { - const key = getKey(src); - const previous = videos.get(key); - if (previous) { - return previous; - } - - // TODO Replace with a more complex object to notify about changes etc. - const frames: ImageBitmap[] = []; - videos.set(key, frames); - - // TODO Actually check if the project is being rendered. - const isRendering = false; - if (isRendering) { - await decode(src, frames); - } else { - decode(src, frames).catch(e => useLogger().error(e)); - } - - return frames; -} - -function getKey(src: string) { - keys[src] ??= {src}; - return keys[src]; -} - -async function decode(src: string, frames: ImageBitmap[]) { - let error: DOMException = null; - const demuxer = new MP4Demuxer(src); - const decoder = new VideoDecoder({ - output: frame => { - createImageBitmap(frame).then(value => { - frames.push(value); - frame.close(); - }); - }, - error: e => { - error = e; - }, - }); - - const config = await demuxer.getConfig(); - decoder.configure(config); - demuxer.start((chunk: EncodedVideoChunk) => { - decoder.decode(chunk); - }); - await decoder.flush(); - if (error) { - throw error; - } -} diff --git a/packages/core/src/tweening/Animator.ts b/packages/core/src/tweening/Animator.ts deleted file mode 100644 index 4cc0a074..00000000 --- a/packages/core/src/tweening/Animator.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { - easeInOutCubic, - TimingFunction, - map, - textLerp, - tween, - InterpolationFunction, - deepLerp, -} from './index'; -import {threadable} from '../decorators'; -import {waitFor, waitUntil} from '../flow'; -import {ThreadGenerator} from '../threading'; -import {GeneratorHelper} from '../helpers'; - -export interface TweenProvider { - ( - from: T, - to: T, - time: number, - timingFunction: TimingFunction, - onEnd: Callback, - ): ThreadGenerator; -} - -export class Animator { - private valueFrom: Type = null; - private lastValue: Type; - private keys: (() => ThreadGenerator)[] = []; - private interpolationFunction: InterpolationFunction = map; - private loops = 1; - private readonly setter: string; - private readonly getter: string; - - public constructor( - private readonly object: This, - private readonly prop: string, - private readonly tweenProvider?: TweenProvider, - ) { - const name = this.prop.charAt(0).toUpperCase() + this.prop.slice(1); - this.getter = `get${name}`; - this.setter = `set${name}`; - } - - public from(value: Type): this { - this.valueFrom = value; - return this; - } - - public key( - value: Type, - time: number, - timingFunction: TimingFunction = easeInOutCubic, - interpolationFunction?: InterpolationFunction, - ...args: Rest - ): this { - this.lastValue = value; - this.keys.push(() => - GeneratorHelper.isThreadable(this.tweenProvider) - ? this.tweenProvider.call( - this.object, - this.valueFrom, - value, - time, - timingFunction, - () => (this.valueFrom = value), - ) - : tween( - time, - v => { - // @ts-ignore - this.object[this.setter]( - interpolationFunction === undefined - ? this.interpolationFunction( - this.valueFrom, - value, - timingFunction(v), - ) - : interpolationFunction( - this.valueFrom, - value, - timingFunction(v), - ...args, - ), - ); - }, - () => { - this.valueFrom = value; - }, - ), - ); - - return this; - } - - public do(callback: Callback): this { - this.keys.push(function* (): ThreadGenerator { - callback(); - }); - - return this; - } - - public diff( - value: Type, - time: number, - timingFunction: TimingFunction = easeInOutCubic, - interpolationFunction?: InterpolationFunction, - ...args: Rest - ): this { - let next: any = value; - const last: any = - this.lastValue === undefined ? this.getValueFrom() : this.lastValue; - if (Array.isArray(last)) { - next = last.map((value1, index) => value1 + next[index]); - } else if (typeof last === 'object') { - for (const key in last) { - next[key] += last[key]; - } - } else { - next += last; - } - - return this.key(next, time, timingFunction, interpolationFunction, ...args); - } - - public back( - time: number, - timingFunction: TimingFunction = easeInOutCubic, - interpolationFunction?: InterpolationFunction, - ...args: Rest - ): this { - return this.key( - this.getValueFrom(), - time, - timingFunction, - interpolationFunction, - ...args, - ); - } - - public waitFor(time: number): this { - this.keys.push(() => waitFor(time)); - return this; - } - - public waitUntil(event: string): this { - this.keys.push(() => waitUntil(event)); - return this; - } - - public run(loops = 1): ThreadGenerator { - this.loops = loops; - if (this.valueFrom !== null) { - //@ts-ignore - this.object[this.setter](this.valueFrom); - } - this.inferProperties(); - return this.runner(); - } - - @threadable('animatorRunner') - private *runner(): ThreadGenerator { - const valueFrom = this.valueFrom; - for (let loop = 0; loop < this.loops; loop++) { - for (let i = 0; i < this.keys.length; i++) { - yield* this.keys[i](); - } - this.valueFrom = valueFrom; - } - } - - private getValueFrom(): Type { - if (this.valueFrom !== null) { - return this.valueFrom; - } - - if (this.getter in this.object) { - //@ts-ignore - return this.object[this.getter](); - } - } - - private inferProperties() { - this.valueFrom ??= this.getValueFrom(); - this.interpolationFunction = Animator.inferInterpolationFunction( - this.valueFrom, - ); - } - - public static inferInterpolationFunction( - value: T, - ): InterpolationFunction { - let interpolationFunction: InterpolationFunction = map; - - if (typeof value === 'string') { - interpolationFunction = textLerp; - } else if (value && typeof value === 'object') { - interpolationFunction = deepLerp; - } - - return interpolationFunction as InterpolationFunction; - } -} diff --git a/packages/core/src/tweening/index.ts b/packages/core/src/tweening/index.ts index 81ecf3bf..b3b7da2b 100644 --- a/packages/core/src/tweening/index.ts +++ b/packages/core/src/tweening/index.ts @@ -1,4 +1,3 @@ -export * from './Animator'; export * from './interpolationFunctions'; export * from './timingFunctions'; export * from './tween'; diff --git a/packages/core/src/utils/createSignal.ts b/packages/core/src/utils/createSignal.ts index 482f1282..fcc0918c 100644 --- a/packages/core/src/utils/createSignal.ts +++ b/packages/core/src/utils/createSignal.ts @@ -8,6 +8,7 @@ import { } from '../tweening'; import {ThreadGenerator} from '../threading'; import {useLogger} from './useProject'; +import {decorate, threadable} from '../decorators'; export interface DependencyContext { dependencies: Set>; @@ -18,6 +19,10 @@ export interface DependencyContext { export type SignalValue = TValue | (() => TValue); +export type SignalGenerator = ThreadGenerator & { + to: SignalTween; +}; + export interface SignalSetter { (value: SignalValue): TReturn; } @@ -32,7 +37,7 @@ export interface SignalTween { time: number, timingFunction?: TimingFunction, interpolationFunction?: InterpolationFunction, - ): ThreadGenerator; + ): SignalGenerator; } export interface SignalUtils { @@ -234,23 +239,62 @@ export function createSignal( return set(value); } - const from = get(); - return tween( + return makeAnimate(timingFunction, interpolationFunction)( + value, duration, - v => { - set( - interpolationFunction( - from, - isReactive(value) ? value() : value, - timingFunction(v), - ), - ); - }, - () => set(value), ); } ); + function makeAnimate( + defaultTimingFunction: TimingFunction, + defaultInterpolationFunction: InterpolationFunction, + before?: ThreadGenerator, + ) { + function animate( + value: SignalValue, + duration: number, + timingFunction = defaultTimingFunction, + interpolationFunction = defaultInterpolationFunction, + ) { + const task = >( + makeTask(value, duration, timingFunction, interpolationFunction, before) + ); + task.to = makeAnimate(timingFunction, interpolationFunction, task); + return task; + } + + return >animate; + } + + decorate(makeTask, threadable()); + function* makeTask( + value: SignalValue, + duration: number, + timingFunction: TimingFunction, + interpolationFunction: InterpolationFunction, + before?: ThreadGenerator, + ) { + if (before) { + yield* before; + } + + const from = get(); + yield* tween( + duration, + v => { + set( + interpolationFunction( + from, + isReactive(value) ? value() : value, + timingFunction(v), + ), + ); + }, + () => set(value), + ); + } + Object.defineProperty(handler, 'reset', { value: initial !== undefined ? () => set(initial) : () => setterReturn, }); diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index 2b42321a..aba3f349 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -2,7 +2,6 @@ export * from './createComputed'; export * from './createComputedAsync'; export * from './createSignal'; export * from './range'; -export * from './useAnimator'; export * from './useProject'; export * from './useRef'; export * from './useScene'; diff --git a/packages/core/src/utils/useAnimator.ts b/packages/core/src/utils/useAnimator.ts deleted file mode 100644 index ccb5708b..00000000 --- a/packages/core/src/utils/useAnimator.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {Animator} from '../tweening'; - -export function useAnimator(initial: T, onUpdate: (value: T) => void) { - const object = { - value: initial, - setValue(value: T) { - this.value = value; - onUpdate(value); - }, - getValue(): T { - return this.value; - }, - }; - - return () => new Animator(object, 'value'); -} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 71d730c3..0d4a3088 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -13,6 +13,7 @@ "declarationMap": true, "allowSyntheticDefaultImports": true, "experimentalDecorators": true, + "importHelpers": true, "jsx": "react-jsx", "jsxImportSource": "@motion-canvas/core/lib", "paths": { diff --git a/packages/template/src/scenes/example.tsx b/packages/template/src/scenes/example.tsx index d16cba1f..ed2009b7 100644 --- a/packages/template/src/scenes/example.tsx +++ b/packages/template/src/scenes/example.tsx @@ -11,7 +11,7 @@ export default makeScene2D(function* (view) { ); yield* waitUntil('circle'); - yield* circle.value.scale(2, 2); + yield* circle.value.scale(2, 2).to(1, 2); yield* waitFor(5); });