Offer the easings of an animation key on Lerp and sound FX ramps
A timed Set sound FX parameters ran only linearly, and a Lerp had four quadratic eases of its own. An animation key already had the full set. Now all three offer the same choice: Linear, Step, the ten curves from Sine to Bounce with In, Out or In-out, and a drawn Curve asset. - easingParams in src/core/graph/nodes/easing.ts gives a node the fields Easing, Direction and Curve from the key's own lists, and easingOfParams reads them back as a key interpolation. - There is no migration. A Lerp saved with the old in, out or inOut runs linear until its easing is picked again. - Set sound FX parameters hands the easing to the player. Linear and Step stay ramps of the audio clock, every other easing goes to setValueCurveAtTime as 200 samples a second, in semitones and dB. - Step now reaches its end at t 1. A key never asked for that, it reads the next key itself, so animations play as before. - The curve-in-use count and the warning for a gone curve cover the sound node too. Tests: - node-catalog: Lerp with cubic in, quad out and in-out, a curve without a direction, step, an unknown easing as linear. The sound node hands on a curve, step and a gone curve as linear. - sound-library: an eased change becomes a curve in semitones and decibels, a step a jump at the end. - node-catalog and curve after dropping the old values: 399, green. - e2e 6, 34, 53 on melektaus-atx: ALL PASSED, 198 checks. In Chrome a 2 s Quad in from depth 7 to -5 read speed 0.864 early and 0.5 at the end, a 1 s Step held 1 and then jumped to 0.5. - Counter-check: with curves sent as linear ramps, the curve test of the sound library fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
501a1524ed
commit
e7a360db73
17 files changed
+347
-104
No files matched your search
@@ -1,8 +1,11 @@
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { asNumber } from '../../values';
|
||||
import { curvesOf, easingOfParams, easingParams, hiddenEasing } from '../easing';
|
||||
import { exec, num } from '../shared';
|
||||
import { fxParamsOf } from './PlaySound';
|
||||
|
||||
import { easeShareOf } from '../../../model/animation';
|
||||
|
||||
/**
|
||||
* Changes the sound FX parameters of a sound that is already playing (#315). It reaches one voice,
|
||||
* by the number its `Play sound` handed out: every play is a sound of its own, and two objects with
|
||||
@@ -17,8 +20,8 @@ export const SetSoundFxParameters: NodeDef = {
|
||||
'glides to the values from a Sound FX parameters node. Keep the voice in a variable to reach it ' +
|
||||
'from another event. A parameter not given keeps its value, a voice that has ended is not ' +
|
||||
'reached. A pitch changes by speed here, so the sound gets shorter or longer while it is moved. ' +
|
||||
'time: seconds the values take to get there, evenly and on the clock of the sound itself, so ' +
|
||||
'a sweep sounds the same every time; 0 is a quick glide.',
|
||||
'time: seconds the values take to get there, along the chosen easing and on the clock of the ' +
|
||||
'sound itself, so a sweep sounds the same every time; 0 is a quick glide.',
|
||||
inputs: [
|
||||
exec(),
|
||||
{ ...num('voice'), inline: false },
|
||||
@@ -26,12 +29,18 @@ export const SetSoundFxParameters: NodeDef = {
|
||||
num('time'),
|
||||
],
|
||||
outputs: [exec()],
|
||||
params: [],
|
||||
run: (ctx, inputs) => {
|
||||
params: easingParams(),
|
||||
hiddenParams: hiddenEasing,
|
||||
run: (ctx, inputs, params, _def, _entry, lookup) => {
|
||||
const voice = asNumber(inputs.voice);
|
||||
const time = asNumber(inputs.time);
|
||||
if (voice > 0)
|
||||
ctx.tuneSound(voice, fxParamsOf(inputs.fxParams), Number.isFinite(time) && time > 0 ? time : 0);
|
||||
ctx.tuneSound(
|
||||
voice,
|
||||
fxParamsOf(inputs.fxParams),
|
||||
Number.isFinite(time) && time > 0 ? time : 0,
|
||||
easeShareOf(easingOfParams(params), curvesOf(lookup)),
|
||||
);
|
||||
return { next: 'exec' };
|
||||
},
|
||||
};
|
||||
@@ -1,60 +1,27 @@
|
||||
import type { GraphNode } from '../../Graph';
|
||||
import type { GraphLookup } from '../../lookup';
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { asNumber } from '../../values';
|
||||
import { curvesOf, easingOfParams, easingParams, hiddenEasing } from '../easing';
|
||||
import { num, withDefault } from '../shared';
|
||||
|
||||
import { curveValue, LERP, LERP_CURVE } from '../../../model/curve';
|
||||
import { textOf } from '../../../model/Properties';
|
||||
|
||||
/** Quadratic curves over t in 0…1, each running from 0 to 1. */
|
||||
const EASES: Record<string, (t: number) => number> = {
|
||||
linear: (t) => t,
|
||||
in: (t) => t * t,
|
||||
out: (t) => 1 - (1 - t) * (1 - t),
|
||||
inOut: (t) => (t < 0.5 ? 2 * t * t : 1 - 2 * (1 - t) * (1 - t)),
|
||||
};
|
||||
import { easedShare } from '../../../model/animation';
|
||||
import { LERP } from '../../../model/curve';
|
||||
|
||||
export const Lerp: NodeDef = {
|
||||
type: LERP,
|
||||
label: 'Lerp',
|
||||
category: 'data',
|
||||
description:
|
||||
'Blends from a to b as t runs from 0 to 1, along the chosen ease. t is held in 0…1. With Ease ' +
|
||||
'set to Curve it follows the Curve asset picked below, and may overshoot b.',
|
||||
'Blends from a to b as t runs from 0 to 1, along the chosen easing – the same ones an animation ' +
|
||||
'key offers. t is held in 0…1. Step stays at a until t reaches 1. Back, Elastic and a Curve ' +
|
||||
'asset may overshoot b.',
|
||||
inputs: [withDefault(num('a'), 0), withDefault(num('b'), 1), withDefault(num('t'), 0)],
|
||||
outputs: [num('result')],
|
||||
params: [
|
||||
{
|
||||
key: 'ease',
|
||||
label: 'Ease',
|
||||
kind: 'enum',
|
||||
default: 'linear',
|
||||
options: [
|
||||
{ value: 'linear', label: 'Linear' },
|
||||
{ value: 'in', label: 'In' },
|
||||
{ value: 'out', label: 'Out' },
|
||||
{ value: 'inOut', label: 'In-out' },
|
||||
{ value: 'curve', label: 'Curve' },
|
||||
],
|
||||
},
|
||||
{ key: LERP_CURVE, label: 'Curve', kind: 'asset', default: '', assetTypes: ['curve'] },
|
||||
],
|
||||
hiddenParams: (params) => (textOf(params.ease) === 'curve' ? [] : [LERP_CURVE]),
|
||||
params: easingParams(),
|
||||
hiddenParams: hiddenEasing,
|
||||
run: (_ctx, inputs, params, _def, _entry, lookup) => {
|
||||
const a = asNumber(inputs.a);
|
||||
const t = Math.min(Math.max(asNumber(inputs.t), 0), 1);
|
||||
const ease = textOf(params.ease);
|
||||
const points = ease === 'curve' ? lookup.curve(textOf(params[LERP_CURVE] ?? '')) : null;
|
||||
const share = points ? curveValue(points, t) : (EASES[ease] ?? EASES.linear!)(t);
|
||||
const share = easedShare(easingOfParams(params), t, curvesOf(lookup));
|
||||
return { outputs: { result: a + (asNumber(inputs.b) - a) * share } };
|
||||
},
|
||||
};
|
||||
|
||||
/** Why a Lerp set to Curve runs linear instead, or null when it eases as it says. */
|
||||
export function lerpProblem(node: GraphNode, lookup: GraphLookup): string | null {
|
||||
if (node.type !== LERP || textOf(node.params.ease) !== 'curve') return null;
|
||||
const id = textOf(node.params[LERP_CURVE] ?? '');
|
||||
if (!id) return 'No curve picked – runs linear';
|
||||
return lookup.curve(id) ? null : 'That curve is gone – runs linear';
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { GraphNode } from '../Graph';
|
||||
import type { GraphLookup } from '../lookup';
|
||||
|
||||
import type { KeyInterpolation } from '../../model/Asset';
|
||||
import { EASING_DIRECTIONS, EASING_SHAPES, easingOf, easingValue } from '../../model/animation';
|
||||
import { type CurveLookup, EASED_NODES, LERP_CURVE } from '../../model/curve';
|
||||
import { type PropertyBag, textOf } from '../../model/Properties';
|
||||
import type { PropertyDef } from '../../properties/PropertySchema';
|
||||
|
||||
const EASE = 'ease';
|
||||
const DIRECTION = 'direction';
|
||||
|
||||
/** The easings an animation key offers, as node settings: the same choice wherever a value runs somewhere. */
|
||||
export function easingParams(): PropertyDef[] {
|
||||
return [
|
||||
{
|
||||
key: EASE,
|
||||
label: 'Easing',
|
||||
kind: 'enum',
|
||||
default: 'linear',
|
||||
options: EASING_SHAPES.map((o) => ({ ...o })),
|
||||
},
|
||||
{
|
||||
key: DIRECTION,
|
||||
label: 'Direction',
|
||||
kind: 'enum',
|
||||
default: 'in-out',
|
||||
options: EASING_DIRECTIONS.map((o) => ({ ...o })),
|
||||
},
|
||||
{ key: LERP_CURVE, label: 'Curve', kind: 'asset', default: '', assetTypes: ['curve'] },
|
||||
];
|
||||
}
|
||||
|
||||
/** The settings read as an animation key's interpolation; unknown text is linear. */
|
||||
export function easingOfParams(params: PropertyBag): KeyInterpolation {
|
||||
const ease = textOf(params[EASE] ?? 'linear');
|
||||
const shape = EASING_SHAPES.find((o) => o.value === ease)?.value ?? 'linear';
|
||||
const direction = EASING_DIRECTIONS.find((o) => o.value === textOf(params[DIRECTION] ?? ''))?.value;
|
||||
return easingValue(shape, direction, textOf(params[LERP_CURVE] ?? ''));
|
||||
}
|
||||
|
||||
/** The settings that say nothing for this easing, for `hiddenParams`. */
|
||||
export function hiddenEasing(params: PropertyBag): string[] {
|
||||
const { shape, direction } = easingOf(easingOfParams(params));
|
||||
return [...(direction ? [] : [DIRECTION]), ...(shape === 'curve' ? [] : [LERP_CURVE])];
|
||||
}
|
||||
|
||||
export function curvesOf(lookup: GraphLookup): CurveLookup {
|
||||
return (id) => lookup.curve(id) ?? undefined;
|
||||
}
|
||||
|
||||
/** Why a node set to Curve runs linear instead, or null when it eases as it says. */
|
||||
export function easingProblem(node: GraphNode, lookup: GraphLookup): string | null {
|
||||
if (!EASED_NODES.includes(node.type)) return null;
|
||||
const { shape, curve } = easingOf(easingOfParams(node.params));
|
||||
if (shape !== 'curve') return null;
|
||||
if (!curve) return 'No curve picked – runs linear';
|
||||
return lookup.curve(curve) ? null : 'That curve is gone – runs linear';
|
||||
}
|
||||
Reference in new issue
Block a user