Add a Lerp node that blends from a to b with an ease (#246)
Every animation in a behavior had to build its interpolation from Subtract, Multiply and Add, once per axis. The gold pickup in "Garden of Sacrifices" needed about 30 nodes for that alone. A single Lerp node with an ease makes those chains one node each. - Lerp has the inputs a (default 0), b (1) and t (0) and the output result. It sits in the data category next to Clamp. - The Ease setting offers Linear, In, Out and In-out, all quadratic. The node row shows it as "Ease: Out". - t is held in 0..1. A timer that overshoots in the last frame still lands exactly on b. Tests: - ./check.sh: 178 files, 2583 tests, CHECK EXIT 0 - 7 new cases in the node catalog test. Without the clamp on t, 2 of them fail. - e2e 6z-iii (2 checks): typing "lerp" in the palette adds a linear Lerp, and picking "Out" in the inspector reaches the node and its row. GM_STEPS=1-6: ALL PASSED, 267 checks. - README check sum is 1459, computed, not counted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3a3401f8ac
commit
320d3e27e1
5 files changed
+122
-2
No files matched your search
@@ -43,7 +43,7 @@ npm run build # writes dist/ and regenerates index.html; the folder is wh
|
||||
The browser suite and the three example games need a machine with a GPU:
|
||||
|
||||
```bash
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1457 checks (GM_STEPS=20 for one step)
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1459 checks (GM_STEPS=20 for one step)
|
||||
# every tool signs in first – see the Accounts page
|
||||
node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs
|
||||
node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { asNumber } from '../../values';
|
||||
import { num, withDefault } from '../shared';
|
||||
|
||||
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)),
|
||||
};
|
||||
|
||||
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.',
|
||||
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' },
|
||||
],
|
||||
},
|
||||
],
|
||||
run: (_ctx, inputs, params) => {
|
||||
const a = asNumber(inputs.a);
|
||||
const t = Math.min(Math.max(asNumber(inputs.t), 0), 1);
|
||||
const ease = EASES[textOf(params.ease)] ?? EASES.linear!;
|
||||
return { outputs: { result: a + (asNumber(inputs.b) - a) * ease(t) } };
|
||||
},
|
||||
};
|
||||
@@ -47,6 +47,7 @@ import { IsTouching } from './data/IsTouching';
|
||||
import { Join } from './data/Join';
|
||||
import { KeyIsDown } from './data/KeyIsDown';
|
||||
import { Length } from './data/Length';
|
||||
import { Lerp } from './data/Lerp';
|
||||
import {
|
||||
AnimationLiteral,
|
||||
AssetLiteral,
|
||||
@@ -186,6 +187,7 @@ export const BUILT_IN_NODES: readonly NodeDef[] = [
|
||||
Abs,
|
||||
Sqrt,
|
||||
Clamp,
|
||||
Lerp,
|
||||
Floor,
|
||||
Ceil,
|
||||
Round,
|
||||
|
||||
@@ -556,6 +556,30 @@ const CASES: Record<string, Case[]> = {
|
||||
{ name: 'above the range', inputs: { value: 5, min: 0, max: 1 }, outputs: { result: 1 } },
|
||||
{ name: 'below the range', inputs: { value: -5, min: 0, max: 1 }, outputs: { result: 0 } },
|
||||
],
|
||||
Lerp: [
|
||||
{ name: 'linear by default', inputs: { a: 10, b: 20, t: 0.25 }, outputs: { result: 12.5 } },
|
||||
{ name: 'ease in', inputs: { a: 0, b: 8, t: 0.5 }, params: { ease: 'in' }, outputs: { result: 2 } },
|
||||
{ name: 'ease out', inputs: { a: 0, b: 8, t: 0.5 }, params: { ease: 'out' }, outputs: { result: 6 } },
|
||||
{
|
||||
name: 'ease in-out, slow at both ends',
|
||||
inputs: { a: 0, b: 8, t: 0.25 },
|
||||
params: { ease: 'inOut' },
|
||||
outputs: { result: 1 },
|
||||
},
|
||||
{
|
||||
name: 'ease in-out, half way at the middle',
|
||||
inputs: { a: 0, b: 8, t: 0.5 },
|
||||
params: { ease: 'inOut' },
|
||||
outputs: { result: 4 },
|
||||
},
|
||||
{ name: 't past 1 stops at b', inputs: { a: 3, b: -5, t: 1.2 }, outputs: { result: -5 } },
|
||||
{
|
||||
name: 't below 0 stops at a',
|
||||
inputs: { a: 3, b: -5, t: -0.5 },
|
||||
params: { ease: 'out' },
|
||||
outputs: { result: 3 },
|
||||
},
|
||||
],
|
||||
Floor: [
|
||||
{ name: 'rounds down', inputs: { value: 2.9 }, outputs: { result: 2 } },
|
||||
{ name: 'rounds down for negatives too', inputs: { value: -2.1 }, outputs: { result: -3 } },
|
||||
|
||||
+53
-1
@@ -1098,8 +1098,60 @@ export async function graph(t) {
|
||||
await mutate(`
|
||||
const p = __gm.state.project;
|
||||
p.assets = p.assets.filter((a) => a.id !== 'spawny');
|
||||
__gm.state.select({ kind: 'asset', assetId: 'mover' });
|
||||
`);
|
||||
|
||||
await lerpNode();
|
||||
|
||||
await mutate(`__gm.state.select({ kind: 'asset', assetId: 'mover' });`);
|
||||
|
||||
await showTab('scene');
|
||||
|
||||
// 6z-iii. Lerp (#246): typed into the palette it arrives linear with a 0…1 range, and the ease
|
||||
// picked in the inspector lands on the node and on its setting row.
|
||||
async function lerpNode() {
|
||||
await mutate(`
|
||||
const p = __gm.state.project;
|
||||
p.assets.push({
|
||||
id: 'lerpy',
|
||||
type: 'behavior',
|
||||
name: 'Lerpy',
|
||||
props: { enabled: true, runInEditor: false },
|
||||
variables: [],
|
||||
graph: { nodes: [], edges: [] },
|
||||
});
|
||||
__gm.state.select({ kind: 'asset', assetId: 'lerpy' });
|
||||
`);
|
||||
await showTab('graph');
|
||||
const spot = await json('__gm.graph.emptySpot()');
|
||||
await click(spot.x, spot.y, 'right');
|
||||
await typeText('lerp');
|
||||
await press('Enter');
|
||||
const lerp = (await asset('lerpy')).graph.nodes[0];
|
||||
const inputs = lerp ? await json(`__gm.graph.nodeInputs(${JSON.stringify(lerp.id)})`) : null;
|
||||
const settings = lerp ? await json(`__gm.graph.nodeSettings(${JSON.stringify(lerp.id)})`) : null;
|
||||
check(
|
||||
'typing "lerp" into the palette adds a linear Lerp from 0 to 1',
|
||||
lerp?.type === 'Lerp' &&
|
||||
JSON.stringify(inputs) === JSON.stringify(['a 0', 'b 1', 't 0']) &&
|
||||
JSON.stringify(settings) === JSON.stringify(['Ease: Linear']),
|
||||
JSON.stringify({ lerp, inputs, settings }),
|
||||
);
|
||||
const easeField = await json(`__gm.inspectorField('Ease')`);
|
||||
if (easeField) {
|
||||
await click(easeField.x, easeField.y);
|
||||
await clickText('Out');
|
||||
}
|
||||
const picked = lerp ? (await asset('lerpy')).graph.nodes.find((n) => n.id === lerp.id) : null;
|
||||
const pickedRows = lerp ? await json(`__gm.graph.nodeSettings(${JSON.stringify(lerp.id)})`) : null;
|
||||
check(
|
||||
'picking "Out" in the inspector sets the node and its setting row',
|
||||
picked?.params.ease === 'out' && JSON.stringify(pickedRows) === JSON.stringify(['Ease: Out']),
|
||||
JSON.stringify({ easeField, params: picked?.params, pickedRows }),
|
||||
);
|
||||
await shot('22-lerp');
|
||||
await mutate(`
|
||||
const p = __gm.state.project;
|
||||
p.assets = p.assets.filter((a) => a.id !== 'lerpy');
|
||||
`);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user