mirror of
https://github.com/motion-canvas/motion-canvas.git
synced 2026-01-12 07:18:01 -05:00
@@ -121,16 +121,6 @@ export function createProperty<
|
||||
return setter(wrap(newValue));
|
||||
}
|
||||
|
||||
if (tweener) {
|
||||
return tweener.call(
|
||||
node,
|
||||
wrap(newValue),
|
||||
duration,
|
||||
timingFunction,
|
||||
interpolationFunction,
|
||||
);
|
||||
}
|
||||
|
||||
return makeAnimate(timingFunction, interpolationFunction)(
|
||||
<TGetterValue>newValue,
|
||||
duration,
|
||||
@@ -171,14 +161,24 @@ export function createProperty<
|
||||
yield* before;
|
||||
}
|
||||
|
||||
const from = getter();
|
||||
yield* tween(
|
||||
duration,
|
||||
v => {
|
||||
setter(interpolationFunction(from, unwrap(value), timingFunction(v)));
|
||||
},
|
||||
() => setter(wrap(value)),
|
||||
);
|
||||
if (tweener) {
|
||||
yield* tweener.call(
|
||||
node,
|
||||
wrap(value),
|
||||
duration,
|
||||
timingFunction,
|
||||
interpolationFunction,
|
||||
);
|
||||
} else {
|
||||
const from = getter();
|
||||
yield* tween(
|
||||
duration,
|
||||
v => {
|
||||
setter(interpolationFunction(from, unwrap(value), timingFunction(v)));
|
||||
},
|
||||
() => setter(wrap(value)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(handler, 'reset', {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {LifecycleEvents} from './LifecycleEvents';
|
||||
import {Threadable} from './Threadable';
|
||||
import {Rect, Vector2} from '../types';
|
||||
import {SceneState} from './SceneState';
|
||||
import {Random} from './Random';
|
||||
|
||||
export interface ThreadGeneratorFactory<T> {
|
||||
(view: T): ThreadGenerator;
|
||||
@@ -30,6 +31,7 @@ export abstract class GeneratorScene<T>
|
||||
implements Scene<ThreadGeneratorFactory<T>>, Threadable
|
||||
{
|
||||
public readonly timeEvents: TimeEvents;
|
||||
public random: Random;
|
||||
|
||||
public get project(): Project {
|
||||
if (!this.currentProject) {
|
||||
@@ -106,6 +108,14 @@ export abstract class GeneratorScene<T>
|
||||
) {
|
||||
decorate(this.runnerFactory, threadable(name));
|
||||
this.timeEvents = new TimeEvents(this);
|
||||
|
||||
let seed = this.meta.getData().seed;
|
||||
if (typeof seed !== 'number') {
|
||||
seed = Random.createSeed();
|
||||
this.meta.setDataSync({seed});
|
||||
}
|
||||
|
||||
this.random = new Random(seed);
|
||||
}
|
||||
|
||||
public abstract getView(): T;
|
||||
@@ -234,6 +244,7 @@ export abstract class GeneratorScene<T>
|
||||
public async reset(previousScene: Scene | null = null) {
|
||||
this.counters = {};
|
||||
this.previousScene = previousScene;
|
||||
this.random = new Random(this.meta.getData().seed!);
|
||||
this.runner = threads(
|
||||
() => this.runnerFactory(this.getView()),
|
||||
thread => {
|
||||
|
||||
79
packages/core/src/scenes/Random.ts
Normal file
79
packages/core/src/scenes/Random.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import {map} from '../tweening';
|
||||
import {range} from '../utils';
|
||||
|
||||
/**
|
||||
* A random number generator based on
|
||||
* {@link https://gist.github.com/tommyettinger/46a874533244883189143505d203312c | Mulberry32}.
|
||||
*/
|
||||
export class Random {
|
||||
public constructor(private state: number) {}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public static createSeed() {
|
||||
return Math.floor(Math.random() * 4294967296);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next random float in the given range.
|
||||
*
|
||||
* @param from - The start of the range.
|
||||
* @param to - The end of the range.
|
||||
*/
|
||||
public nextFloat(from = 0, to = 1) {
|
||||
return map(from, to, this.next());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next random integer in the given range.
|
||||
*
|
||||
* @param from - The start of the range.
|
||||
* @param to - The end of the range. Exclusive.
|
||||
*/
|
||||
public nextInt(from = 0, to = 4294967296) {
|
||||
let value = Math.floor(map(from, to, this.next()));
|
||||
if (value === to) {
|
||||
value = from;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array filled with random floats in the given range.
|
||||
*
|
||||
* @param size - The size of the array.
|
||||
* @param from - The start of the range.
|
||||
* @param to - The end of the range.
|
||||
*/
|
||||
public floatArray(size: number, from = 0, to = 1): number[] {
|
||||
return range(size).map(() => this.nextFloat(from, to));
|
||||
}
|
||||
|
||||
/**
|
||||
Get an array filled with random integers in the given range.
|
||||
*
|
||||
* @param size - The size of the array.
|
||||
* @param from - The start of the range.
|
||||
* @param to - The end of the range. Exclusive.
|
||||
*/
|
||||
public intArray(size: number, from = 0, to = 4294967296): number[] {
|
||||
return range(size).map(() => this.nextInt(from, to));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new independent generator.
|
||||
*/
|
||||
public spawn() {
|
||||
return new Random(this.nextInt());
|
||||
}
|
||||
|
||||
private next() {
|
||||
this.state |= 0;
|
||||
this.state = (this.state + 0x6d2b79f5) | 0;
|
||||
let t = Math.imul(this.state ^ (this.state >>> 15), 1 | this.state);
|
||||
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,11 @@ import {SavedTimeEvent, TimeEvents} from './TimeEvents';
|
||||
import {SubscribableEvent, SubscribableValueEvent} from '../events';
|
||||
import {Vector2} from '../types';
|
||||
import {LifecycleEvents} from './LifecycleEvents';
|
||||
import {Random} from './Random';
|
||||
|
||||
export interface SceneMetadata extends Metadata {
|
||||
timeEvents: SavedTimeEvent[];
|
||||
seed: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,6 +102,7 @@ export interface Scene<T = unknown> {
|
||||
*/
|
||||
project: Project;
|
||||
readonly timeEvents: TimeEvents;
|
||||
readonly random: Random;
|
||||
readonly meta: Meta<SceneMetadata>;
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './GeneratorScene';
|
||||
export * from './Inspectable';
|
||||
export * from './Random';
|
||||
export * from './Scene';
|
||||
export * from './SceneState';
|
||||
export * from './Threadable';
|
||||
|
||||
@@ -5,6 +5,7 @@ export * from './createSignal';
|
||||
export * from './getContext';
|
||||
export * from './range';
|
||||
export * from './useProject';
|
||||
export * from './useRandom';
|
||||
export * from './useRef';
|
||||
export * from './useScene';
|
||||
export * from './useThread';
|
||||
|
||||
20
packages/core/src/utils/useRandom.ts
Normal file
20
packages/core/src/utils/useRandom.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import {useScene} from './useScene';
|
||||
import {Random} from '../scenes';
|
||||
|
||||
/**
|
||||
* Get the random number generator for the current scene.
|
||||
**/
|
||||
export function useRandom(): Random;
|
||||
/**
|
||||
* Get the random number generator for the given seed.
|
||||
*
|
||||
* @param seed - The seed for the generator.
|
||||
* @param fixed - Whether the seed should be fixed. Fixed seeds remain
|
||||
* the same even when the main scene seed changes.
|
||||
*/
|
||||
export function useRandom(seed: number, fixed?: boolean): Random;
|
||||
export function useRandom(seed?: number, fixed = true): Random {
|
||||
return typeof seed === 'number'
|
||||
? new Random(fixed ? seed : seed + useScene().meta.getData().seed)
|
||||
: useScene().random;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
sidebar_position: 8
|
||||
sidebar_position: 9
|
||||
---
|
||||
|
||||
# Configuration
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
sidebar_position: 7
|
||||
sidebar_position: 8
|
||||
---
|
||||
|
||||
# Rendering
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
label: Utilities
|
||||
position: 7
|
||||
collapsed: false
|
||||
39
packages/docs/docs/guides/getting-started/utils/random.mdx
Normal file
39
packages/docs/docs/guides/getting-started/utils/random.mdx
Normal file
@@ -0,0 +1,39 @@
|
||||
import AnimationPlayer from '@site/src/components/AnimationPlayer';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import source from '!!raw-loader!@motion-canvas/examples/src/scenes/random';
|
||||
|
||||
# Random values
|
||||
|
||||
<AnimationPlayer name="random" banner />
|
||||
|
||||
Randomly generated values can be used to create a sense of variety and
|
||||
unpredictability in your animation. In Motion Canvas, it's achieved using the
|
||||
[`useRandom()`](/api/core/utils#useRandom) function.
|
||||
It returns a random number generator (RNG) for the current scene:
|
||||
|
||||
```ts
|
||||
import {useRandom} from '@motion-canvas/core/lib/utils';
|
||||
|
||||
const random = useRandom();
|
||||
const integer = random.nextInt(0, 10);
|
||||
```
|
||||
|
||||
In this case, calling `nextInt()` will return an integer in the range from
|
||||
0 to 10 (exclusive). Check the [`Random` api](/api/core/scenes/Random) to see
|
||||
all available methods.
|
||||
|
||||
Unlike `Math.random()`, `useRandom()` is completely reproducible - each time the
|
||||
animation is played the generated values will be exactly the same. The seed used
|
||||
to generate these numbers is stored in the meta file of each scene.
|
||||
|
||||
You can also provide your own seed to find a sequence of numbers that best suits
|
||||
your needs:
|
||||
|
||||
```ts
|
||||
const random = useRandom(123);
|
||||
```
|
||||
|
||||
The animation at the top of this page uses a random number generator to vary
|
||||
the height of rectangles and make them look like a sound-wave:
|
||||
|
||||
<CodeBlock language="tsx">{source}</CodeBlock>
|
||||
@@ -19,6 +19,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: min(56vw, 320px);
|
||||
margin-bottom: var(--ifm-leading);
|
||||
}
|
||||
|
||||
.player {
|
||||
|
||||
@@ -4,23 +4,37 @@ import Summary from '@site/src/components/Api/Comment/Summary';
|
||||
|
||||
export default function Comment({
|
||||
comment,
|
||||
withExamples = true,
|
||||
full = true,
|
||||
}: {
|
||||
comment: JSONOutput.Comment;
|
||||
withExamples?: boolean;
|
||||
full?: boolean;
|
||||
}) {
|
||||
const remarks = useMemo(() => {
|
||||
return comment?.blockTags?.find(({tag}) => tag === '@remarks');
|
||||
}, [comment]);
|
||||
const examples = useMemo(() => {
|
||||
return comment?.blockTags?.filter(({tag}) => tag === '@example') ?? [];
|
||||
}, [comment]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Summary id={comment?.summaryId} />
|
||||
<Summary id={remarks?.contentId} />
|
||||
{withExamples && examples.length > 0 && (
|
||||
{full && <ExamplesAndSeeAlso comment={comment} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ExamplesAndSeeAlso({comment}: {comment: JSONOutput.Comment}) {
|
||||
const examples = useMemo(
|
||||
() => comment?.blockTags?.filter(({tag}) => tag === '@example') ?? [],
|
||||
[comment],
|
||||
);
|
||||
const seeAlso = useMemo(
|
||||
() => comment?.blockTags?.find(({tag}) => tag === '@see'),
|
||||
[comment],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{examples.length > 0 && (
|
||||
<>
|
||||
<h4>Examples</h4>
|
||||
{examples.map(example => (
|
||||
@@ -28,6 +42,12 @@ export default function Comment({
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{seeAlso && (
|
||||
<>
|
||||
<h4>See also</h4>
|
||||
<Summary id={seeAlso.contentId} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ export default function Tooltip({children}: {children: ReactNode}) {
|
||||
show && styles.active,
|
||||
)}
|
||||
>
|
||||
{comment && <Comment comment={comment} withExamples={false} />}
|
||||
{comment && <Comment comment={comment} full={false} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
3
packages/examples/src/random.meta
Normal file
3
packages/examples/src/random.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"version": 0
|
||||
}
|
||||
10
packages/examples/src/random.ts
Normal file
10
packages/examples/src/random.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import {Project} from '@motion-canvas/core/lib';
|
||||
|
||||
import scene from './scenes/random?scene';
|
||||
import {Vector2} from '@motion-canvas/core/lib/types';
|
||||
|
||||
export default new Project({
|
||||
scenes: [scene],
|
||||
background: '#141414',
|
||||
size: new Vector2(960, 540),
|
||||
});
|
||||
4
packages/examples/src/scenes/random.meta
Normal file
4
packages/examples/src/scenes/random.meta
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"version": 1,
|
||||
"seed": 1456280284
|
||||
}
|
||||
37
packages/examples/src/scenes/random.tsx
Normal file
37
packages/examples/src/scenes/random.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import {makeScene2D} from '@motion-canvas/2d/lib/scenes';
|
||||
import {Layout, Rect} from '@motion-canvas/2d/lib/components';
|
||||
import {makeRef, range, useRandom} from '@motion-canvas/core/lib/utils';
|
||||
import {all, loop, sequence} from '@motion-canvas/core/lib/flow';
|
||||
|
||||
export default makeScene2D(function* (view) {
|
||||
// highlight-next-line
|
||||
const random = useRandom();
|
||||
const rects: Rect[] = [];
|
||||
|
||||
view.add(
|
||||
<Layout layout gap={10} alignItems="center">
|
||||
{range(40).map(i => (
|
||||
<Rect
|
||||
ref={makeRef(rects, i)}
|
||||
radius={20}
|
||||
width={10}
|
||||
height={10}
|
||||
fill={'#e13238'}
|
||||
/>
|
||||
))}
|
||||
</Layout>,
|
||||
);
|
||||
|
||||
yield* loop(3, () =>
|
||||
sequence(
|
||||
0.04,
|
||||
...rects.map(rect =>
|
||||
all(
|
||||
// highlight-next-line
|
||||
rect.size.y(random.nextInt(100, 200), 0.5).to(10, 0.5),
|
||||
rect.fill('#e6a700', 0.5).to('#e13238', 0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -10,6 +10,7 @@ export default defineConfig({
|
||||
'./src/tweening-cubic.ts',
|
||||
'./src/tweening-color.ts',
|
||||
'./src/tweening-vector.ts',
|
||||
'./src/random.ts',
|
||||
],
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"@typeParam": true,
|
||||
"@ignore": true,
|
||||
"@inheritDoc": true,
|
||||
"@default": true
|
||||
"@default": true,
|
||||
"@see": true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user