feat: add random number generator (#116)

Closes: #14
This commit is contained in:
Jacob
2023-01-06 17:48:40 +01:00
committed by GitHub
parent 369ded5c92
commit d5053123ee
20 changed files with 262 additions and 28 deletions

View File

@@ -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', {

View File

@@ -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 => {

View 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;
}
}

View File

@@ -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>;
/**

View File

@@ -1,5 +1,6 @@
export * from './GeneratorScene';
export * from './Inspectable';
export * from './Random';
export * from './Scene';
export * from './SceneState';
export * from './Threadable';

View File

@@ -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';

View 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;
}

View File

@@ -1,5 +1,5 @@
---
sidebar_position: 8
sidebar_position: 9
---
# Configuration

View File

@@ -1,5 +1,5 @@
---
sidebar_position: 7
sidebar_position: 8
---
# Rendering

View File

@@ -0,0 +1,3 @@
label: Utilities
position: 7
collapsed: false

View 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>

View File

@@ -19,6 +19,7 @@
align-items: center;
justify-content: center;
min-height: min(56vw, 320px);
margin-bottom: var(--ifm-leading);
}
.player {

View File

@@ -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} />
</>
)}
</>
);
}

View File

@@ -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>
);

View File

@@ -0,0 +1,3 @@
{
"version": 0
}

View 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),
});

View File

@@ -0,0 +1,4 @@
{
"version": 1,
"seed": 1456280284
}

View 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),
),
),
),
);
});

View File

@@ -10,6 +10,7 @@ export default defineConfig({
'./src/tweening-cubic.ts',
'./src/tweening-color.ts',
'./src/tweening-vector.ts',
'./src/random.ts',
],
}),
],

View File

@@ -33,6 +33,7 @@
"@typeParam": true,
"@ignore": true,
"@inheritDoc": true,
"@default": true
"@default": true,
"@see": true
}
}