Build your own easing curve and use it on keys and Lerp (#247)
The built-in eases cover the usual shapes, but a game often wants its own: a coin that bounces twice, a door that overshoots and settles. A new asset type holds such a curve, and keys and the Lerp node can pick it instead of a built-in ease. - New asset type "curve": points like in Vector art, x is t and y is the value. A new curve is the straight line from (0, 0) to (1, 1). - One pure evaluation in src/core/model/curve.ts serves the timeline preview, the running game and the Lerp node. - The curve always has one value per t. The end points stay at t 0 and 1, points cannot pass each other, and a handle that reaches past its neighbour is shortened along its direction. Loading a hand-edited file applies the same rules. - Curve tab: click on the line to insert a point that keeps the shape, click on free space to put one there, drag points and handles, double-click or Delete removes, Alt-click toggles round and sharp. Every step is one undo step. - Presets: all 30 built-in eases plus Linear, fitted with adaptive subdivision. Each stays within 0.4 % of the built-in ease. - Preview: a dot runs along the curve once a second, a second dot shows only the value. The Preview button stops it. - Animation keys get "Curve" under Easing with a curve picker. Lerp gets "Curve" under Ease, and its result may overshoot b. - A missing or unpicked curve runs linear. The inspector says so, the node shows it as a problem, and deleting a used curve warns with the number of keys and Lerp nodes. Tests: - ./check.sh: 180 files, 2612 tests, CHECK EXIT 0 - GM_STEPS=1-6,25,53: ALL PASSED, 309 checks - New e2e module 53 with 13 checks, all with the real mouse. Expected values come from its own Bezier evaluation. - Counter-checks bite: without Bezier evaluation 4 unit tests fail, without handle clamping 2. In the browser, a key that cannot find its curve, a click that sets no point and a handle drag that moves the point all turn checks red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
320d3e27e1
commit
393b8ec7f9
41 files changed
+1981
-67
No files matched your search
+23
-15
@@ -68,6 +68,7 @@ import { canvasSide, PALETTE_MAX } from '../model/pixelart';
|
||||
import { isCelFile } from '../model/pixelCel';
|
||||
import { newId } from '../ids';
|
||||
import { SHAPE_FILL_DEFAULT } from '../model/vectorart';
|
||||
import { sanitizedCurve } from '../model/curve';
|
||||
import { hasParentCycle } from '../model/walk';
|
||||
import {
|
||||
DECLARED_PORT_KINDS,
|
||||
@@ -230,6 +231,8 @@ export function deserializeProject(raw: unknown, fallbackId = ''): Project {
|
||||
assets.push({ id, type, name, props, ...normalizePixelArt(a, takenIds) });
|
||||
} else if (type === 'palette') {
|
||||
assets.push({ id, type, name, props, colors: normalizePalette(a.colors) });
|
||||
} else if (type === 'curve') {
|
||||
assets.push({ id, type, name, props, points: sanitizedCurve(normalizePoints(a.points)) });
|
||||
} else if (type === 'animation') {
|
||||
// A root or row aiming at something gone is kept: it reads as missing and keeps its keys.
|
||||
const root = typeof a.root === 'string' ? a.root : '';
|
||||
@@ -702,22 +705,8 @@ function normalizeShapes(raw: unknown): VectorShape[] {
|
||||
const rings: VectorRing[] = [];
|
||||
for (const r of Array.isArray(s.rings) ? s.rings : []) {
|
||||
const ring = asRecord(r);
|
||||
const points: VectorPoint[] = [];
|
||||
for (const p of Array.isArray(ring.points) ? ring.points : []) {
|
||||
const point = asRecord(p);
|
||||
const x = asFinite(point.x);
|
||||
const y = asFinite(point.y);
|
||||
if (x === null || y === null) continue;
|
||||
const kept: VectorPoint = { x, y };
|
||||
for (const side of ['in', 'out'] as const) {
|
||||
const handle = normalizeHandle(point[side]);
|
||||
if (handle) kept[side] = handle;
|
||||
}
|
||||
if (point.mode === 'smooth' || point.mode === 'symmetric') kept.mode = point.mode;
|
||||
points.push(kept);
|
||||
}
|
||||
// Closed is what a shape usually is, and what every ring written by the editor says.
|
||||
rings.push({ points, closed: ring.closed !== false });
|
||||
rings.push({ points: normalizePoints(ring.points), closed: ring.closed !== false });
|
||||
}
|
||||
const shape: VectorShape = {
|
||||
id: s.id,
|
||||
@@ -740,6 +729,25 @@ function normalizeShapes(raw: unknown): VectorShape[] {
|
||||
return shapes;
|
||||
}
|
||||
|
||||
/** The points of a ring or of a curve (#247); the rules are those of `normalizeShapes`. */
|
||||
function normalizePoints(raw: unknown): VectorPoint[] {
|
||||
const points: VectorPoint[] = [];
|
||||
for (const p of Array.isArray(raw) ? raw : []) {
|
||||
const point = asRecord(p);
|
||||
const x = asFinite(point.x);
|
||||
const y = asFinite(point.y);
|
||||
if (x === null || y === null) continue;
|
||||
const kept: VectorPoint = { x, y };
|
||||
for (const side of ['in', 'out'] as const) {
|
||||
const handle = normalizeHandle(point[side]);
|
||||
if (handle) kept[side] = handle;
|
||||
}
|
||||
if (point.mode === 'smooth' || point.mode === 'symmetric') kept.mode = point.mode;
|
||||
points.push(kept);
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
/**
|
||||
* A number as written, or null – unlike `toNumber`, which reads anything unusable as 0. A coordinate
|
||||
* of 0 is a place on the canvas, so "broken" and "the left edge" must not come out the same.
|
||||
|
||||
Reference in new issue
Block a user