Spring Config
Spring physics animation helper using the Web Animations API — no Framer Motion, no React Spring, ~50 lines. Generates a keyframe sequence from spring physics and runs it via element.animate().…
$ prime install @impeccable/template-spring-config Projection
Always in _index.xml · the agent never has to ask for this.
SpringConfig [template] v1.0.0
Spring physics animation helper using the Web Animations API — no Framer Motion, no React Spring, ~50 lines. Generates a keyframe sequence from spring physics and runs it via element.animate(). Honors prefers-reduced-motion.
Loaded when retrieval picks the atom as adjacent / supporting.
SpringConfig [template] v1.0.0
Spring physics animation helper using the Web Animations API — no Framer Motion, no React Spring, ~50 lines. Generates a keyframe sequence from spring physics and runs it via element.animate(). Honors prefers-reduced-motion.
Language
javascript
Body
<script>
/**
* Run a spring animation on an element.
* spring(el, { transform: ['scale(1)', 'scale(1.05)'] }, { stiffness: 180, damping: 18 })
*
* Returns the Animation object so you can .cancel() / .pause() / await .finished.
*/
function spring(el, properties, options) {
options = options || {};
var stiffness = options.stiffness != null ? options.stiffness : {STIFFNESS};
var damping = options.damping != null ? options.damping : {DAMPING};
var mass = options.mass != null ? options.mass : {MASS};
var precision = 0.001;
// Reduced motion: skip the spring, snap to the end value.
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
var endFrame = {};
for (var k in properties) endFrame[k] = properties[k][properties[k].length - 1];
return el.animate([endFrame, endFrame], { duration: 0, fill: 'forwards' });
}
// Solve spring ODE numerically — sample at 60fps until it settles.
var w0 = Math.sqrt(stiffness / mass);
var zeta = damping / (2 * Math.sqrt(stiffness * mass));
var wd = zeta < 1 ? w0 * Math.sqrt(1 - zeta * zeta) : 0;
function valueAt(t) {
if (zeta < 1) {
return 1 - Math.exp(-zeta * w0 * t) *
((zeta * w0) / wd * Math.sin(wd * t) + Math.cos(wd * t));
}
return 1 - Math.exp(-w0 * t) * (1 + w0 * t);
}
var frames = [];
var dt = 1 / 60;
var t = 0;
var lastVal = 0;
var settledFrames = 0;
while (settledFrames < 6 && t < 4) {
var v = valueAt(t);
frames.push(v);
if (Math.abs(v - lastVal) < precision && Math.abs(1 - v) < precision) settledFrames++;
else settledFrames = 0;
lastVal = v;
t += dt;
}
// Build keyframes by interpolating each property.
var keyframes = frames.map(function(progress) {
var frame = {};
for (var key in properties) {
var arr = properties[key];
// For two-stop arrays we mix start/end; for n-stops we'd need a tween table.
frame[key] = arr.length === 2
? interpolate(arr[0], arr[1], progress)
: arr[Math.min(arr.length - 1, Math.floor(progress * (arr.length - 1)))];
}
return frame;
});
return el.animate(keyframes, {
duration: frames.length * (1000 / 60),
easing: 'linear', // we already baked the curve into the frames
fill: 'forwards',
});
}
function interpolate(a, b, t) {
// String interpolation for transform/color values: lerp every numeric token.
var ai = String(a).match(/-?[\\d.]+/g) || [];
var bi = String(b).match(/-?[\\d.]+/g) || [];
if (ai.length !== bi.length) return t < 1 ? a : b;
var i = 0;
return String(a).replace(/-?[\\d.]+/g, function() {
var av = parseFloat(ai[i]); var bv = parseFloat(bi[i]); i++;
return (av + (bv - av) * t).toFixed(4);
});
}
window.spring = spring;
// Usage:
// spring(button, { transform: ['scale(1)', 'scale(1.08)'] });
// spring(toast, { transform: ['translateY(20px)', 'translateY(0)'], opacity: [0, 1] });
</script>
Usage Notes
- Stiffness 180 / damping 18 = the 'gentle' default Framer Motion ships with. Tune from there.
- For snappy buttons: stiffness 300, damping 24. For slow drawers: stiffness 80, damping 12.
- Always pass two-stop arrays (start/end) — multi-stop interpolation is intentionally not handled here.
- Returns a real Animation object: await spring(el, ...).finished works; .cancel() is reversible.
- Reduced-motion path returns instant Animation — caller code is identical either way.
Tested In
- Chrome 84+ (Web Animations API)
- Firefox 75+
- Safari 13.1+
Accessibility
- prefers-reduced-motion bypasses the spring entirely; element snaps to final state.
- No requestAnimationFrame loop — uses native Animation, pause-able + cancel-able + interruptible.
- Honors user preference even when called from userland helpers.
Loaded when retrieval picks the atom as a focal / direct hit.
SpringConfig [template] v1.0.0
Spring physics animation helper using the Web Animations API — no Framer Motion, no React Spring, ~50 lines. Generates a keyframe sequence from spring physics and runs it via element.animate(). Honors prefers-reduced-motion.
Language
javascript
Body
<script>
/**
* Run a spring animation on an element.
* spring(el, { transform: ['scale(1)', 'scale(1.05)'] }, { stiffness: 180, damping: 18 })
*
* Returns the Animation object so you can .cancel() / .pause() / await .finished.
*/
function spring(el, properties, options) {
options = options || {};
var stiffness = options.stiffness != null ? options.stiffness : {STIFFNESS};
var damping = options.damping != null ? options.damping : {DAMPING};
var mass = options.mass != null ? options.mass : {MASS};
var precision = 0.001;
// Reduced motion: skip the spring, snap to the end value.
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
var endFrame = {};
for (var k in properties) endFrame[k] = properties[k][properties[k].length - 1];
return el.animate([endFrame, endFrame], { duration: 0, fill: 'forwards' });
}
// Solve spring ODE numerically — sample at 60fps until it settles.
var w0 = Math.sqrt(stiffness / mass);
var zeta = damping / (2 * Math.sqrt(stiffness * mass));
var wd = zeta < 1 ? w0 * Math.sqrt(1 - zeta * zeta) : 0;
function valueAt(t) {
if (zeta < 1) {
return 1 - Math.exp(-zeta * w0 * t) *
((zeta * w0) / wd * Math.sin(wd * t) + Math.cos(wd * t));
}
return 1 - Math.exp(-w0 * t) * (1 + w0 * t);
}
var frames = [];
var dt = 1 / 60;
var t = 0;
var lastVal = 0;
var settledFrames = 0;
while (settledFrames < 6 && t < 4) {
var v = valueAt(t);
frames.push(v);
if (Math.abs(v - lastVal) < precision && Math.abs(1 - v) < precision) settledFrames++;
else settledFrames = 0;
lastVal = v;
t += dt;
}
// Build keyframes by interpolating each property.
var keyframes = frames.map(function(progress) {
var frame = {};
for (var key in properties) {
var arr = properties[key];
// For two-stop arrays we mix start/end; for n-stops we'd need a tween table.
frame[key] = arr.length === 2
? interpolate(arr[0], arr[1], progress)
: arr[Math.min(arr.length - 1, Math.floor(progress * (arr.length - 1)))];
}
return frame;
});
return el.animate(keyframes, {
duration: frames.length * (1000 / 60),
easing: 'linear', // we already baked the curve into the frames
fill: 'forwards',
});
}
function interpolate(a, b, t) {
// String interpolation for transform/color values: lerp every numeric token.
var ai = String(a).match(/-?[\\d.]+/g) || [];
var bi = String(b).match(/-?[\\d.]+/g) || [];
if (ai.length !== bi.length) return t < 1 ? a : b;
var i = 0;
return String(a).replace(/-?[\\d.]+/g, function() {
var av = parseFloat(ai[i]); var bv = parseFloat(bi[i]); i++;
return (av + (bv - av) * t).toFixed(4);
});
}
window.spring = spring;
// Usage:
// spring(button, { transform: ['scale(1)', 'scale(1.08)'] });
// spring(toast, { transform: ['translateY(20px)', 'translateY(0)'], opacity: [0, 1] });
</script>
Usage Notes
- Stiffness 180 / damping 18 = the 'gentle' default Framer Motion ships with. Tune from there.
- For snappy buttons: stiffness 300, damping 24. For slow drawers: stiffness 80, damping 12.
- Always pass two-stop arrays (start/end) — multi-stop interpolation is intentionally not handled here.
- Returns a real Animation object: await spring(el, ...).finished works; .cancel() is reversible.
- Reduced-motion path returns instant Animation — caller code is identical either way.
Tested In
- Chrome 84+ (Web Animations API)
- Firefox 75+
- Safari 13.1+
Accessibility
- prefers-reduced-motion bypasses the spring entirely; element snaps to final state.
- No requestAnimationFrame loop — uses native Animation, pause-able + cancel-able + interruptible.
- Honors user preference even when called from userland helpers.
Source
prime-system/examples/frontend-design/primes/compiled/@impeccable/template-spring-config/atom.yaml