@hegemonart/get-design-done 1.15.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +9 -5
- package/.claude-plugin/plugin.json +19 -5
- package/CHANGELOG.md +122 -0
- package/README.md +41 -0
- package/SKILL.md +4 -1
- package/agents/component-benchmark-harvester.md +112 -0
- package/agents/component-benchmark-synthesizer.md +88 -0
- package/agents/design-auditor.md +60 -1
- package/agents/design-doc-writer.md +21 -0
- package/agents/design-executor.md +22 -4
- package/agents/design-pattern-mapper.md +61 -0
- package/agents/motion-mapper.md +74 -9
- package/agents/token-mapper.md +8 -0
- package/connections/design-corpora.md +158 -0
- package/package.json +13 -3
- package/reference/components/README.md +94 -0
- package/reference/components/TEMPLATE.md +184 -0
- package/reference/components/accordion.md +217 -0
- package/reference/components/alert.md +198 -0
- package/reference/components/badge.md +202 -0
- package/reference/components/breadcrumbs.md +198 -0
- package/reference/components/button.md +195 -0
- package/reference/components/card.md +200 -0
- package/reference/components/checkbox.md +207 -0
- package/reference/components/chip.md +209 -0
- package/reference/components/command-palette.md +228 -0
- package/reference/components/date-picker.md +227 -0
- package/reference/components/drawer.md +201 -0
- package/reference/components/file-upload.md +219 -0
- package/reference/components/input.md +208 -0
- package/reference/components/label.md +200 -0
- package/reference/components/link.md +193 -0
- package/reference/components/list.md +217 -0
- package/reference/components/menu.md +212 -0
- package/reference/components/modal-dialog.md +210 -0
- package/reference/components/navbar.md +211 -0
- package/reference/components/pagination.md +205 -0
- package/reference/components/popover.md +197 -0
- package/reference/components/progress.md +210 -0
- package/reference/components/radio.md +203 -0
- package/reference/components/rich-text-editor.md +226 -0
- package/reference/components/select-combobox.md +219 -0
- package/reference/components/sidebar.md +211 -0
- package/reference/components/skeleton.md +197 -0
- package/reference/components/slider.md +208 -0
- package/reference/components/stepper.md +220 -0
- package/reference/components/switch.md +194 -0
- package/reference/components/table.md +229 -0
- package/reference/components/tabs.md +213 -0
- package/reference/components/toast.md +200 -0
- package/reference/components/tooltip.md +201 -0
- package/reference/components/tree.md +225 -0
- package/reference/css-grid-layout.md +835 -0
- package/reference/external/NOTICE.hyperframes +28 -0
- package/reference/image-optimization.md +582 -0
- package/reference/motion-advanced.md +754 -0
- package/reference/motion-easings.md +381 -0
- package/reference/motion-interpolate.md +282 -0
- package/reference/motion-spring.md +234 -0
- package/reference/motion-transition-taxonomy.md +155 -0
- package/reference/motion.md +20 -0
- package/reference/output-contracts/motion-map.schema.json +135 -0
- package/reference/registry.json +285 -0
- package/reference/registry.schema.json +6 -1
- package/reference/variable-fonts-loading.md +532 -0
- package/scripts/lib/easings.cjs +280 -0
- package/scripts/lib/parse-contract.cjs +220 -0
- package/scripts/lib/spring.cjs +160 -0
- package/scripts/tests/test-motion-provenance.sh +64 -0
- package/skills/benchmark/SKILL.md +105 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Easing functions for web animation.
|
|
3
|
+
*
|
|
4
|
+
* Source: React Native — Libraries/Animated/Easing.js (MIT License)
|
|
5
|
+
* Attribution: Facebook, Inc. and its affiliates
|
|
6
|
+
* https://github.com/facebook/react-native/blob/main/Libraries/Animated/Easing.js
|
|
7
|
+
*
|
|
8
|
+
* Each function accepts t in [0, 1] and returns a value in [0, 1]
|
|
9
|
+
* (elastic/back/bounce may transiently exceed [0,1] for overshoot).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A linear function, `f(t) = t`. Position correlates to elapsed time one to one.
|
|
16
|
+
*/
|
|
17
|
+
function linear(t) {
|
|
18
|
+
return t;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A quadratic function, `f(t) = t * t`. Position equals the square of elapsed time.
|
|
23
|
+
*/
|
|
24
|
+
function quad(t) {
|
|
25
|
+
return t * t;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A cubic function, `f(t) = t * t * t`. Position equals the cube of elapsed time.
|
|
30
|
+
*/
|
|
31
|
+
function cubic(t) {
|
|
32
|
+
return t * t * t;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A power function. Position is equal to the Nth power of elapsed time.
|
|
37
|
+
* @param {number} n - The exponent.
|
|
38
|
+
* @returns {function(number): number}
|
|
39
|
+
*/
|
|
40
|
+
function poly(n) {
|
|
41
|
+
return function (t) {
|
|
42
|
+
return Math.pow(t, n);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A sinusoidal function.
|
|
48
|
+
*/
|
|
49
|
+
function sin(t) {
|
|
50
|
+
return 1 - Math.cos((t * Math.PI) / 2);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A circular function.
|
|
55
|
+
*/
|
|
56
|
+
function circle(t) {
|
|
57
|
+
return 1 - Math.sqrt(1 - t * t);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* An exponential function.
|
|
62
|
+
*/
|
|
63
|
+
function exp(t) {
|
|
64
|
+
return Math.pow(2, 10 * (t - 1));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A spring-like elastic function that overshoots its target value one or more times.
|
|
69
|
+
*
|
|
70
|
+
* @param {number} bounciness - Amplitude of overshoot. Default 1.
|
|
71
|
+
* @param {number} speed - Controls oscillation frequency. Default 1.
|
|
72
|
+
* @returns {function(number): number}
|
|
73
|
+
*/
|
|
74
|
+
function elastic(bounciness, speed) {
|
|
75
|
+
if (bounciness === undefined) bounciness = 1;
|
|
76
|
+
if (speed === undefined) speed = 1;
|
|
77
|
+
|
|
78
|
+
const p = (bounciness === 0)
|
|
79
|
+
? Math.PI / 2
|
|
80
|
+
: Math.asin(1 / (bounciness = Math.max(1, bounciness)));
|
|
81
|
+
|
|
82
|
+
const s = (bounciness / 10) * 0.3 * (speed / 10) || 0.3;
|
|
83
|
+
|
|
84
|
+
return function (t) {
|
|
85
|
+
if (t === 0) return 0;
|
|
86
|
+
if (t === 1) return 1;
|
|
87
|
+
return (
|
|
88
|
+
bounciness *
|
|
89
|
+
Math.pow(2, -10 * t) *
|
|
90
|
+
Math.sin(((t - s / 4) * (2 * Math.PI)) / s) +
|
|
91
|
+
1
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Use with `Easing.out` for an overshoot effect. Default s = 1.70158.
|
|
98
|
+
* @param {number} s - Overshoot magnitude.
|
|
99
|
+
* @returns {function(number): number}
|
|
100
|
+
*/
|
|
101
|
+
function back(s) {
|
|
102
|
+
if (s === undefined) s = 1.70158;
|
|
103
|
+
return function (t) {
|
|
104
|
+
return t * t * ((s + 1) * t - s);
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Provides a bouncing effect.
|
|
110
|
+
* @param {number} t
|
|
111
|
+
* @returns {number}
|
|
112
|
+
*/
|
|
113
|
+
function bounce(t) {
|
|
114
|
+
if (t < 1 / 2.75) {
|
|
115
|
+
return 7.5625 * t * t;
|
|
116
|
+
} else if (t < 2 / 2.75) {
|
|
117
|
+
const t2 = t - 1.5 / 2.75;
|
|
118
|
+
return 7.5625 * t2 * t2 + 0.75;
|
|
119
|
+
} else if (t < 2.5 / 2.75) {
|
|
120
|
+
const t2 = t - 2.25 / 2.75;
|
|
121
|
+
return 7.5625 * t2 * t2 + 0.9375;
|
|
122
|
+
} else {
|
|
123
|
+
const t2 = t - 2.625 / 2.75;
|
|
124
|
+
return 7.5625 * t2 * t2 + 0.984375;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Provides a raw cubic Bézier easing matching the CSS `cubic-bezier()` primitive.
|
|
130
|
+
*
|
|
131
|
+
* Implements the same algorithm as CSS Transitions spec (De Casteljau + Newton-Raphson solve).
|
|
132
|
+
*
|
|
133
|
+
* @param {number} x1
|
|
134
|
+
* @param {number} y1
|
|
135
|
+
* @param {number} x2
|
|
136
|
+
* @param {number} y2
|
|
137
|
+
* @returns {function(number): number}
|
|
138
|
+
*/
|
|
139
|
+
function bezier(x1, y1, x2, y2) {
|
|
140
|
+
const NEWTON_ITERATIONS = 4;
|
|
141
|
+
const NEWTON_MIN_SLOPE = 0.001;
|
|
142
|
+
const SUBDIVISION_PRECISION = 0.0000001;
|
|
143
|
+
const SUBDIVISION_MAX_ITERATIONS = 10;
|
|
144
|
+
const kSplineTableSize = 11;
|
|
145
|
+
const kSampleStepSize = 1.0 / (kSplineTableSize - 1.0);
|
|
146
|
+
|
|
147
|
+
function A(a1, a2) { return 1.0 - 3.0 * a2 + 3.0 * a1; }
|
|
148
|
+
function B(a1, a2) { return 3.0 * a2 - 6.0 * a1; }
|
|
149
|
+
function C(a1) { return 3.0 * a1; }
|
|
150
|
+
|
|
151
|
+
function calcBezier(t, a1, a2) {
|
|
152
|
+
return ((A(a1, a2) * t + B(a1, a2)) * t + C(a1)) * t;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function getSlope(t, a1, a2) {
|
|
156
|
+
return 3.0 * A(a1, a2) * t * t + 2.0 * B(a1, a2) * t + C(a1);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function binarySubdivide(x, a, b) {
|
|
160
|
+
let currentX, currentT, i = 0;
|
|
161
|
+
do {
|
|
162
|
+
currentT = a + (b - a) / 2.0;
|
|
163
|
+
currentX = calcBezier(currentT, x1, x2) - x;
|
|
164
|
+
if (currentX > 0.0) b = currentT;
|
|
165
|
+
else a = currentT;
|
|
166
|
+
} while (Math.abs(currentX) > SUBDIVISION_PRECISION && ++i < SUBDIVISION_MAX_ITERATIONS);
|
|
167
|
+
return currentT;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function newtonRaphsonIterate(x, guessT) {
|
|
171
|
+
for (let i = 0; i < NEWTON_ITERATIONS; ++i) {
|
|
172
|
+
const currentSlope = getSlope(guessT, x1, x2);
|
|
173
|
+
if (currentSlope === 0.0) return guessT;
|
|
174
|
+
const currentX = calcBezier(guessT, x1, x2) - x;
|
|
175
|
+
guessT -= currentX / currentSlope;
|
|
176
|
+
}
|
|
177
|
+
return guessT;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Precompute sample table
|
|
181
|
+
const sampleValues = new Float32Array(kSplineTableSize);
|
|
182
|
+
if (x1 !== y1 || x2 !== y2) {
|
|
183
|
+
for (let i = 0; i < kSplineTableSize; ++i) {
|
|
184
|
+
sampleValues[i] = calcBezier(i * kSampleStepSize, x1, x2);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function getTForX(x) {
|
|
189
|
+
let intervalStart = 0.0;
|
|
190
|
+
let currentSample = 1;
|
|
191
|
+
const lastSample = kSplineTableSize - 1;
|
|
192
|
+
|
|
193
|
+
for (; currentSample !== lastSample && sampleValues[currentSample] <= x; ++currentSample) {
|
|
194
|
+
intervalStart += kSampleStepSize;
|
|
195
|
+
}
|
|
196
|
+
--currentSample;
|
|
197
|
+
|
|
198
|
+
const dist = (x - sampleValues[currentSample]) / (sampleValues[currentSample + 1] - sampleValues[currentSample]);
|
|
199
|
+
const guessForT = intervalStart + dist * kSampleStepSize;
|
|
200
|
+
const initialSlope = getSlope(guessForT, x1, x2);
|
|
201
|
+
|
|
202
|
+
if (initialSlope >= NEWTON_MIN_SLOPE) {
|
|
203
|
+
return newtonRaphsonIterate(x, guessForT);
|
|
204
|
+
} else if (initialSlope === 0.0) {
|
|
205
|
+
return guessForT;
|
|
206
|
+
} else {
|
|
207
|
+
return binarySubdivide(x, intervalStart, intervalStart + kSampleStepSize);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return function (t) {
|
|
212
|
+
if (x1 === y1 && x2 === y2) return t; // linear shortcut
|
|
213
|
+
if (t === 0) return 0;
|
|
214
|
+
if (t === 1) return 1;
|
|
215
|
+
return calcBezier(getTForX(t), y1, y2);
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
// Higher-order composition wrappers
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Runs an easing function forwards (ease-in direction).
|
|
225
|
+
* `in(f)(t) = f(t)`
|
|
226
|
+
*
|
|
227
|
+
* @param {function(number): number} easing
|
|
228
|
+
* @returns {function(number): number}
|
|
229
|
+
*/
|
|
230
|
+
function _in(easing) {
|
|
231
|
+
return easing;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Runs an easing function backwards. Useful to apply ease-out variants.
|
|
236
|
+
* `out(f)(t) = 1 - f(1 - t)`
|
|
237
|
+
*
|
|
238
|
+
* @param {function(number): number} easing
|
|
239
|
+
* @returns {function(number): number}
|
|
240
|
+
*/
|
|
241
|
+
function out(easing) {
|
|
242
|
+
return function (t) {
|
|
243
|
+
return 1 - easing(1 - t);
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Makes any easing function symmetrical. The easing function will run
|
|
249
|
+
* forwards for half of the duration, then backwards for the rest of
|
|
250
|
+
* the duration.
|
|
251
|
+
* `inOut(f)(t) = t < 0.5 ? f(2t)/2 : 1 - f(2(1-t))/2`
|
|
252
|
+
*
|
|
253
|
+
* @param {function(number): number} easing
|
|
254
|
+
* @returns {function(number): number}
|
|
255
|
+
*/
|
|
256
|
+
function inOut(easing) {
|
|
257
|
+
return function (t) {
|
|
258
|
+
if (t < 0.5) {
|
|
259
|
+
return easing(t * 2) / 2;
|
|
260
|
+
}
|
|
261
|
+
return 1 - easing((1 - t) * 2) / 2;
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
module.exports = {
|
|
266
|
+
linear,
|
|
267
|
+
quad,
|
|
268
|
+
cubic,
|
|
269
|
+
poly,
|
|
270
|
+
sin,
|
|
271
|
+
circle,
|
|
272
|
+
exp,
|
|
273
|
+
elastic,
|
|
274
|
+
back,
|
|
275
|
+
bounce,
|
|
276
|
+
bezier,
|
|
277
|
+
in: _in,
|
|
278
|
+
out,
|
|
279
|
+
inOut,
|
|
280
|
+
};
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* parse-contract.cjs — shared helper for validating agent output contracts.
|
|
4
|
+
*
|
|
5
|
+
* Agents that emit structured JSON blocks (e.g. motion-mapper) use this helper
|
|
6
|
+
* to validate and extract the structured data from their output. Works without
|
|
7
|
+
* optional dependencies: pure-JS JSON schema validation for the motion-map contract.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* const { parseMotionMap, validate } = require('./parse-contract.cjs');
|
|
11
|
+
* const result = parseMotionMap(markdownString);
|
|
12
|
+
* if (result.ok) console.log(result.data);
|
|
13
|
+
* else console.error(result.error);
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// JSON block extraction
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Extract the first ```json ... ``` fenced block from a markdown string.
|
|
25
|
+
* Returns { ok: true, raw: string } or { ok: false, error: string }.
|
|
26
|
+
*/
|
|
27
|
+
function extractJsonBlock(markdown) {
|
|
28
|
+
const match = markdown.match(/```json\s*\n([\s\S]*?)\n```/);
|
|
29
|
+
if (!match) {
|
|
30
|
+
return { ok: false, error: 'No ```json ... ``` block found in output' };
|
|
31
|
+
}
|
|
32
|
+
return { ok: true, raw: match[1] };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parse a JSON string. Returns { ok: true, data } or { ok: false, error }.
|
|
37
|
+
*/
|
|
38
|
+
function parseJson(raw) {
|
|
39
|
+
try {
|
|
40
|
+
return { ok: true, data: JSON.parse(raw) };
|
|
41
|
+
} catch (e) {
|
|
42
|
+
return { ok: false, error: `JSON parse error: ${e.message}` };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Motion map contract validation (hand-written — no ajv dependency required)
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
const VALID_EASINGS = new Set([
|
|
51
|
+
'linear',
|
|
52
|
+
'quad', 'quad-in', 'quad-out', 'quad-in-out',
|
|
53
|
+
'cubic', 'cubic-in', 'cubic-out', 'cubic-in-out',
|
|
54
|
+
'poly', 'poly-in', 'poly-out', 'poly-in-out',
|
|
55
|
+
'sin', 'sin-in', 'sin-out', 'sin-in-out',
|
|
56
|
+
'circle', 'circle-in', 'circle-out', 'circle-in-out',
|
|
57
|
+
'exp', 'exp-in', 'exp-out', 'exp-in-out',
|
|
58
|
+
'elastic', 'elastic-in', 'elastic-out', 'elastic-in-out',
|
|
59
|
+
'back', 'back-in', 'back-out', 'back-in-out',
|
|
60
|
+
'bounce', 'bounce-in', 'bounce-out', 'bounce-in-out',
|
|
61
|
+
'bezier',
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
const VALID_TRANSITION_FAMILIES = new Set([
|
|
65
|
+
'3d', 'blur', 'cover', 'destruction', 'dissolve', 'distortion', 'grid', 'light',
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
const VALID_DURATION_CLASSES = new Set([
|
|
69
|
+
'instant', 'quick', 'standard', 'slow', 'narrative',
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
const VALID_TRIGGERS = new Set([
|
|
73
|
+
'user-gesture', 'state-change', 'scroll-progress', 'time', 'loop',
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Validate a single AnimationBinding object.
|
|
78
|
+
* Returns an array of error strings (empty = valid).
|
|
79
|
+
*/
|
|
80
|
+
function validateBinding(binding, index) {
|
|
81
|
+
const errors = [];
|
|
82
|
+
const ctx = `animations[${index}]`;
|
|
83
|
+
|
|
84
|
+
if (typeof binding.id !== 'string' || !binding.id) {
|
|
85
|
+
errors.push(`${ctx}.id: required string`);
|
|
86
|
+
}
|
|
87
|
+
if (!binding.location || typeof binding.location.file !== 'string' || typeof binding.location.line !== 'number') {
|
|
88
|
+
errors.push(`${ctx}.location: required {file: string, line: number}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Easing: either a canonical string or a custom object
|
|
92
|
+
const easing = binding.easing;
|
|
93
|
+
if (easing === undefined || easing === null) {
|
|
94
|
+
errors.push(`${ctx}.easing: required`);
|
|
95
|
+
} else if (typeof easing === 'string') {
|
|
96
|
+
if (!VALID_EASINGS.has(easing)) {
|
|
97
|
+
errors.push(`${ctx}.easing: "${easing}" is not a canonical easing. Use one of: ${[...VALID_EASINGS].join(', ')} — or use { type: "custom", justification: "..." }`);
|
|
98
|
+
}
|
|
99
|
+
} else if (typeof easing === 'object') {
|
|
100
|
+
if (easing.type !== 'custom') {
|
|
101
|
+
errors.push(`${ctx}.easing.type: must be "custom" for object form`);
|
|
102
|
+
}
|
|
103
|
+
if (typeof easing.justification !== 'string' || !easing.justification.trim()) {
|
|
104
|
+
errors.push(`${ctx}.easing.justification: required non-empty string when using custom easing`);
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
errors.push(`${ctx}.easing: must be string (canonical name) or object { type: "custom", justification, value? }`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// transition_family: optional, but if present must be valid
|
|
111
|
+
if (binding.transition_family !== undefined) {
|
|
112
|
+
if (!VALID_TRANSITION_FAMILIES.has(binding.transition_family)) {
|
|
113
|
+
errors.push(`${ctx}.transition_family: "${binding.transition_family}" is not a valid family. Use one of: ${[...VALID_TRANSITION_FAMILIES].join(', ')}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// duration_class: required
|
|
118
|
+
if (!VALID_DURATION_CLASSES.has(binding.duration_class)) {
|
|
119
|
+
errors.push(`${ctx}.duration_class: required, must be one of: ${[...VALID_DURATION_CLASSES].join(', ')}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// trigger: required
|
|
123
|
+
if (!VALID_TRIGGERS.has(binding.trigger)) {
|
|
124
|
+
errors.push(`${ctx}.trigger: required, must be one of: ${[...VALID_TRIGGERS].join(', ')}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return errors;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Validate a parsed motion-map object against the contract.
|
|
132
|
+
* Returns { ok: true, data } or { ok: false, errors: string[] }.
|
|
133
|
+
*/
|
|
134
|
+
function validateMotionMap(data) {
|
|
135
|
+
const errors = [];
|
|
136
|
+
|
|
137
|
+
if (data.schema_version !== '1.0.0') {
|
|
138
|
+
errors.push(`schema_version: must be "1.0.0", got "${data.schema_version}"`);
|
|
139
|
+
}
|
|
140
|
+
if (typeof data.generated_at !== 'string') {
|
|
141
|
+
errors.push('generated_at: required ISO-8601 string');
|
|
142
|
+
}
|
|
143
|
+
if (!Array.isArray(data.animations)) {
|
|
144
|
+
errors.push('animations: required array');
|
|
145
|
+
} else {
|
|
146
|
+
data.animations.forEach((b, i) => {
|
|
147
|
+
errors.push(...validateBinding(b, i));
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (errors.length > 0) {
|
|
152
|
+
return { ok: false, errors };
|
|
153
|
+
}
|
|
154
|
+
return { ok: true, data };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// Public API
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Extract, parse, and validate a motion-map JSON block from agent output markdown.
|
|
163
|
+
*
|
|
164
|
+
* @param {string} markdown - Full markdown output from motion-mapper
|
|
165
|
+
* @returns {{ ok: true, data: object } | { ok: false, error: string }}
|
|
166
|
+
*/
|
|
167
|
+
function parseMotionMap(markdown) {
|
|
168
|
+
const extracted = extractJsonBlock(markdown);
|
|
169
|
+
if (!extracted.ok) return { ok: false, error: extracted.error };
|
|
170
|
+
|
|
171
|
+
const parsed = parseJson(extracted.raw);
|
|
172
|
+
if (!parsed.ok) return { ok: false, error: parsed.error };
|
|
173
|
+
|
|
174
|
+
const validated = validateMotionMap(parsed.data);
|
|
175
|
+
if (!validated.ok) {
|
|
176
|
+
return {
|
|
177
|
+
ok: false,
|
|
178
|
+
error: `Motion map contract violations:\n${validated.errors.map(e => ` - ${e}`).join('\n')}`,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { ok: true, data: validated.data };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Generic JSON block extractor and parser (no schema validation).
|
|
187
|
+
* Use for contracts without a built-in validator.
|
|
188
|
+
*/
|
|
189
|
+
function parseGenericContract(markdown) {
|
|
190
|
+
const extracted = extractJsonBlock(markdown);
|
|
191
|
+
if (!extracted.ok) return { ok: false, error: extracted.error };
|
|
192
|
+
return parseJson(extracted.raw);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Load the motion-map JSON schema from the reference directory.
|
|
197
|
+
* Used by external validators (e.g., ajv if available).
|
|
198
|
+
*/
|
|
199
|
+
function loadMotionMapSchema(projectRoot) {
|
|
200
|
+
const schemaPath = path.join(
|
|
201
|
+
projectRoot || process.cwd(),
|
|
202
|
+
'reference/output-contracts/motion-map.schema.json',
|
|
203
|
+
);
|
|
204
|
+
return JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
module.exports = {
|
|
208
|
+
parseMotionMap,
|
|
209
|
+
parseGenericContract,
|
|
210
|
+
loadMotionMapSchema,
|
|
211
|
+
validateMotionMap,
|
|
212
|
+
extractJsonBlock,
|
|
213
|
+
parseJson,
|
|
214
|
+
// Exported for testing
|
|
215
|
+
_validateBinding: validateBinding,
|
|
216
|
+
VALID_EASINGS,
|
|
217
|
+
VALID_TRANSITION_FAMILIES,
|
|
218
|
+
VALID_DURATION_CLASSES,
|
|
219
|
+
VALID_TRIGGERS,
|
|
220
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spring animation utilities.
|
|
3
|
+
*
|
|
4
|
+
* Source: React Native — Libraries/Animated/SpringConfig.js (MIT License)
|
|
5
|
+
* Attribution: Facebook, Inc. and its affiliates
|
|
6
|
+
* https://github.com/facebook/react-native/blob/main/Libraries/Animated/SpringConfig.js
|
|
7
|
+
*
|
|
8
|
+
* Implements a mass-spring-damper physical model.
|
|
9
|
+
* Parameters:
|
|
10
|
+
* stiffness (k) — spring constant; higher = faster, snappier
|
|
11
|
+
* damping (c) — friction coefficient; higher = less oscillation
|
|
12
|
+
* mass (m) — simulated mass; higher = slower, heavier feel
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Canonical presets
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Canonical spring presets.
|
|
23
|
+
*
|
|
24
|
+
* Each preset includes stiffness, damping, and mass.
|
|
25
|
+
* Settle times are approximate at 60fps with threshold=0.001.
|
|
26
|
+
*
|
|
27
|
+
* | name | stiffness | damping | mass | settle | character |
|
|
28
|
+
* |---------|-----------|---------|------|---------|---------------------|
|
|
29
|
+
* | gentle | 120 | 14 | 1 | ~400ms | soft, mild bounce |
|
|
30
|
+
* | wobbly | 180 | 12 | 1 | ~600ms | bouncy, 2–3 cycles |
|
|
31
|
+
* | stiff | 400 | 30 | 1 | ~200ms | snappy, minimal |
|
|
32
|
+
* | slow | 280 | 60 | 1 | ~800ms | heavy, no bounce |
|
|
33
|
+
*/
|
|
34
|
+
const PRESETS = {
|
|
35
|
+
gentle: { stiffness: 120, damping: 14, mass: 1 },
|
|
36
|
+
wobbly: { stiffness: 180, damping: 12, mass: 1 },
|
|
37
|
+
stiff: { stiffness: 400, damping: 30, mass: 1 },
|
|
38
|
+
slow: { stiffness: 280, damping: 60, mass: 1 },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Critical damping
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Computes the critical damping coefficient for a given stiffness and mass.
|
|
47
|
+
*
|
|
48
|
+
* At critical damping (`c = 2 * sqrt(k * m)`), the spring reaches its target
|
|
49
|
+
* in the shortest time possible without oscillating.
|
|
50
|
+
*
|
|
51
|
+
* Values below this threshold produce an underdamped (bouncy) spring.
|
|
52
|
+
* Values above this threshold produce an overdamped (sluggish) spring.
|
|
53
|
+
*
|
|
54
|
+
* @param {number} stiffness - Spring constant k.
|
|
55
|
+
* @param {number} mass - Simulated mass m.
|
|
56
|
+
* @returns {number} Critical damping coefficient c.
|
|
57
|
+
*/
|
|
58
|
+
function criticalDamping(stiffness, mass) {
|
|
59
|
+
return 2 * Math.sqrt(stiffness * mass);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Settle time estimation
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Estimates the settle time of a spring in milliseconds by stepping the
|
|
68
|
+
* simulation forward until position and velocity are both within `threshold`
|
|
69
|
+
* of the target.
|
|
70
|
+
*
|
|
71
|
+
* Uses a fixed timestep of 1/60 s (60fps) for numerical integration.
|
|
72
|
+
*
|
|
73
|
+
* @param {number} stiffness - Spring constant k.
|
|
74
|
+
* @param {number} damping - Damping coefficient c.
|
|
75
|
+
* @param {number} mass - Simulated mass m.
|
|
76
|
+
* @param {number} [threshold=0.001] - Convergence threshold for position and velocity.
|
|
77
|
+
* @param {number} [maxMs=5000] - Safety ceiling to prevent infinite loops.
|
|
78
|
+
* @returns {number} Estimated settle time in milliseconds.
|
|
79
|
+
*/
|
|
80
|
+
function settleTime(stiffness, damping, mass, threshold, maxMs) {
|
|
81
|
+
if (threshold === undefined) threshold = 0.001;
|
|
82
|
+
if (maxMs === undefined) maxMs = 5000;
|
|
83
|
+
|
|
84
|
+
const dt = 1 / 60; // seconds per frame
|
|
85
|
+
const maxFrames = Math.ceil(maxMs / (dt * 1000));
|
|
86
|
+
|
|
87
|
+
let pos = 1; // start displaced from target (normalized)
|
|
88
|
+
let vel = 0;
|
|
89
|
+
let elapsed = 0;
|
|
90
|
+
|
|
91
|
+
for (let i = 0; i < maxFrames; i++) {
|
|
92
|
+
const result = _stepInternal(stiffness, damping, mass, pos, vel, dt);
|
|
93
|
+
pos = result.position;
|
|
94
|
+
vel = result.velocity;
|
|
95
|
+
elapsed += dt * 1000;
|
|
96
|
+
|
|
97
|
+
if (Math.abs(pos) < threshold && Math.abs(vel) < threshold) {
|
|
98
|
+
return elapsed;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return maxMs; // did not settle within safety ceiling
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Step function
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Internal integration step — semi-implicit Euler.
|
|
111
|
+
* @private
|
|
112
|
+
*/
|
|
113
|
+
function _stepInternal(stiffness, damping, mass, position, velocity, dt) {
|
|
114
|
+
const acceleration = (-stiffness * position - damping * velocity) / mass;
|
|
115
|
+
const newVelocity = velocity + acceleration * dt;
|
|
116
|
+
const newPosition = position + newVelocity * dt;
|
|
117
|
+
return { position: newPosition, velocity: newVelocity };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Advances a spring simulation by one timestep.
|
|
122
|
+
*
|
|
123
|
+
* The spring is always simulated toward the target value 0 (normalized).
|
|
124
|
+
* To animate from `from` to `to`, offset your inputs:
|
|
125
|
+
* position = currentValue - toValue
|
|
126
|
+
* velocity = currentVelocity (positive = moving toward target)
|
|
127
|
+
*
|
|
128
|
+
* @param {number} stiffness - Spring constant k.
|
|
129
|
+
* @param {number} damping - Damping coefficient c.
|
|
130
|
+
* @param {number} mass - Simulated mass m.
|
|
131
|
+
* @param {number} initialVelocity - Current velocity in units/second.
|
|
132
|
+
* @param {number} dt - Timestep in seconds. Use 1/60 for 60fps.
|
|
133
|
+
* @returns {{ position: number, velocity: number }}
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* // Animate a value from 0 to 100:
|
|
137
|
+
* let pos = 0 - 100; // offset: (current - target)
|
|
138
|
+
* let vel = 0;
|
|
139
|
+
* const dt = 1/60;
|
|
140
|
+
*
|
|
141
|
+
* function tick() {
|
|
142
|
+
* const result = step(400, 30, 1, vel, dt);
|
|
143
|
+
* // Note: pass current velocity, not initialVelocity parameter which is legacy
|
|
144
|
+
* pos = result.position;
|
|
145
|
+
* vel = result.velocity;
|
|
146
|
+
* const displayValue = pos + 100; // un-offset
|
|
147
|
+
* }
|
|
148
|
+
*/
|
|
149
|
+
function step(stiffness, damping, mass, initialVelocity, dt) {
|
|
150
|
+
// Position starts at 1 (displaced) when called externally without a position arg.
|
|
151
|
+
// For incremental use, callers manage position themselves — see _stepInternal.
|
|
152
|
+
return _stepInternal(stiffness, damping, mass, 1 - initialVelocity * dt, initialVelocity, dt);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = {
|
|
156
|
+
PRESETS,
|
|
157
|
+
criticalDamping,
|
|
158
|
+
settleTime,
|
|
159
|
+
step,
|
|
160
|
+
};
|