Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions submissions/docs/engine-missing-keyframes-ks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Engine: Missing Keyframes Bug Report

## What does this do?

Identifies 6 motion engine animations (`spin`, `wobble`, `flip-x`, `flip-y`, `heartbeat`, `rubber-band`) that silently fail at runtime because their `@keyframes` are defined in `easemotion/engine/compiler.js`'s `KEYFRAME_MAP` but missing from `core/animations.css`.

The demo includes the missing keyframes inline so they work immediately, and the suggested `ease-*` utility classes that should accompany them when promoted to core.

## How is it used?

```html
<!-- These currently produce NO visible animation -->
<div em="spin 2s linear repeat-infinite">⚙️</div>
<div em="wobble 1s ease repeat-infinite">🔔</div>
<div em="flip-x 800ms ease">Flip</div>
<div em="flip-y 800ms ease">Flip</div>
<div em="heartbeat 1.5s ease repeat-infinite">❤️</div>
<div em="rubber-band 1s ease repeat-infinite">🏀</div>
```

After the fix, these would also work as plain CSS classes:

```html
<div class="ease-spin">⚙️</div>
<div class="ease-wobble">🔔</div>
<div class="ease-flip-x">Flip</div>
<div class="ease-flip-y">Flip</div>
<div class="ease-heartbeat">❤️</div>
<div class="ease-rubber-band">🏀</div>
```

## Why is it useful?

The engine's `ANIMATION_NAMES` set in `parser.js` and `KEYFRAME_MAP` in `compiler.js` accept 25 animation names. Only 19 actually work. The remaining 6 (24% of the API) produce no visible output — the user sees no animation, no error in the browser, only a `console.warn` in development builds.

This bug report documents the exact gap and the exact fix needed: add the 6 missing `@keyframes` and `ease-*` utility classes to `core/animations.css`.

**Files to update in core:**
- `core/animations.css` — add `@keyframes ease-kf-spin`, `-wobble`, `-flip-x`, `-flip-y`, `-heartbeat`, `-rubber-band` and matching `.ease-spin` through `.ease-rubber-band` utility classes
- `easemotion.min.css` — rebuild
125 changes: 125 additions & 0 deletions submissions/docs/engine-missing-keyframes-ks/demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Engine Missing Keyframes — Bug Report &amp; Fix</title>
<link rel="stylesheet" href="../../../easemotion.css">
<link rel="stylesheet" href="style.css">
</head>
<body>

<main class="ease-container ease-padding-8">
<h1 class="ease-text-3xl ease-font-bold ease-mb-4 ease-text-center">
Motion Engine: Missing Keyframes Demo
</h1>
<p class="ease-text-muted ease-text-center ease-mb-8">
6 animations silently fail via <code>em=""</code> DSL because their
<code>@keyframes</code> are missing from <code>core/animations.css</code>.
This demo proves they work correctly after the fix.
</p>

<div class="ease-grid ease-grid-cols-2 ease-grid-cols-3 ease-gap-6">
<!-- Before: using em="" relying on style.css keyframe fallback -->
<div class="demo-card ease-card ease-card-hover ease-fade-in ease-delay-100">
<h3 class="ease-card-title ease-text-lg">Spin</h3>
<div class="demo-visual">
<div class="anim-spin" role="img" aria-label="Spinning gear icon">⚙️</div>
</div>
<p class="ease-text-sm ease-text-muted ease-mb-2"><code>em="spin"</code> → <span class="status-text" data-status="spin">failed</span></p>
<pre class="ease-text-xs ease-bg-neutral ease-padding-2 ease-rounded">em="spin 2s linear repeat-infinite"</pre>
</div>

<div class="demo-card ease-card ease-card-hover ease-fade-in ease-delay-200">
<h3 class="ease-card-title ease-text-lg">Wobble</h3>
<div class="demo-visual">
<div class="anim-wobble demo-emoji" role="img" aria-label="Wobbling icon">🔔</div>
</div>
<p class="ease-text-sm ease-text-muted ease-mb-2"><code>em="wobble"</code> → <span class="status-text" data-status="wobble">failed</span></p>
<pre class="ease-text-xs ease-bg-neutral ease-padding-2 ease-rounded">em="wobble 1s ease repeat-infinite"</pre>
</div>

<div class="demo-card ease-card ease-card-hover ease-fade-in ease-delay-300">
<h3 class="ease-card-title ease-text-lg">Flip X</h3>
<div class="demo-visual flip-demo">
<div class="anim-flip-x demo-square" role="img" aria-label="Flipping on X axis">
<span>X</span>
</div>
</div>
<p class="ease-text-sm ease-text-muted ease-mb-2"><code>em="flip-x"</code> → <span class="status-text" data-status="flip-x">failed</span></p>
<pre class="ease-text-xs ease-bg-neutral ease-padding-2 ease-rounded">em="flip-x 800ms ease repeat-infinite"</pre>
</div>

<div class="demo-card ease-card ease-card-hover ease-fade-in ease-delay-300">
<h3 class="ease-card-title ease-text-lg">Flip Y</h3>
<div class="demo-visual flip-demo">
<div class="anim-flip-y demo-square" role="img" aria-label="Flipping on Y axis">
<span>Y</span>
</div>
</div>
<p class="ease-text-sm ease-text-muted ease-mb-2"><code>em="flip-y"</code> → <span class="status-text" data-status="flip-y">failed</span></p>
<pre class="ease-text-xs ease-bg-neutral ease-padding-2 ease-rounded">em="flip-y 800ms ease repeat-infinite"</pre>
</div>

<div class="demo-card ease-card ease-card-hover ease-fade-in ease-delay-400">
<h3 class="ease-card-title ease-text-lg">Heartbeat</h3>
<div class="demo-visual">
<div class="anim-heartbeat demo-emoji" role="img" aria-label="Beating heart">❤️</div>
</div>
<p class="ease-text-sm ease-text-muted ease-mb-2"><code>em="heartbeat"</code> → <span class="status-text" data-status="heartbeat">failed</span></p>
<pre class="ease-text-xs ease-bg-neutral ease-padding-2 ease-rounded">em="heartbeat 1.5s ease repeat-infinite"</pre>
</div>

<div class="demo-card ease-card ease-card-hover ease-fade-in ease-delay-500">
<h3 class="ease-card-title ease-text-lg">Rubber Band</h3>
<div class="demo-visual">
<div class="anim-rubber-band demo-emoji" role="img" aria-label="Rubber banding icon">🏀</div>
</div>
<p class="ease-text-sm ease-text-muted ease-mb-2"><code>em="rubber-band"</code> → <span class="status-text" data-status="rubber-band">failed</span></p>
<pre class="ease-text-xs ease-bg-neutral ease-padding-2 ease-rounded">em="rubber-band 1s ease repeat-infinite"</pre>
</div>
</div>

<div class="ease-card ease-card-info ease-mt-8 ease-padding-6">
<h2 class="ease-text-xl ease-font-semibold ease-mb-2">What's wrong?</h2>
<p class="ease-text-sm ease-text-muted ease-mb-2">
The engine's <code>KEYFRAME_MAP</code> in <code>easemotion/engine/compiler.js</code>
references these 6 <code>@keyframes</code>, but they were never defined in
<code>core/animations.css</code>. The parser accepts the names but compilation
produces CSS referencing non-existent keyframes — resulting in zero visible motion.
</p>
<p class="ease-text-sm ease-text-muted">
<strong>Fix:</strong> The <code>@keyframes</code> in <code>style.css</code> need to
be promoted into <code>core/animations.css</code> as <code>ease-kf-spin</code>,
<code>ease-kf-wobble</code>, <code>ease-kf-flip-x</code>, <code>ease-kf-flip-y</code>,
<code>ease-kf-heartbeat</code>, and <code>ease-kf-rubber-band</code>.
Utility classes <code>.ease-spin</code>, <code>.ease-wobble</code>, etc. should also
be added for plain CSS usage.
</p>
</div>
</main>

<script type="module">
import { parse, compile, className } from '../../../easemotion/engine/compiler.js';
const anims = ['spin','wobble','flip-x','flip-y','heartbeat','rubber-band'];
for (const name of anims) {
const ast = parse(name);
const statusEl = document.querySelector(`[data-status="${name}"]`);
if (ast && ast.animation === name) {
const cls = className(ast);
const css = compile(ast, cls);
if (css.includes(`ease-kf-${name}`)) {
statusEl.textContent = '✓ working';
statusEl.className = 'status-ok';
} else {
statusEl.textContent = '✗ fails';
statusEl.className = 'status-fail';
}
} else {
statusEl.textContent = '✗ fails';
statusEl.className = 'status-fail';
}
}
</script>
</body>
</html>
191 changes: 191 additions & 0 deletions submissions/docs/engine-missing-keyframes-ks/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/* ============================================================
Engine Missing Keyframes — style.css
Bug report demonstrating 6 motion engine animations that
silently fail without these @keyframes in core/animations.css.

These keyframes and utility classes need to be promoted into
core/animations.css as:
ease-kf-spin, ease-kf-wobble, ease-kf-flip-x,
ease-kf-flip-y, ease-kf-heartbeat, ease-kf-rubber-band

And utility classes:
ease-spin, ease-wobble, ease-flip-x,
ease-flip-y, ease-heartbeat, ease-rubber-band
============================================================ */

/* ── Waiting for promotion to core/animations.css ──────────── */
/* These @keyframes fill the gap between the engine's KEYFRAME_MAP
and what core/animations.css actually ships. Without them,
em="spin", em="wobble", em="flip-x", em="flip-y",
em="heartbeat", and em="rubber-band" produce no visible motion. */

@keyframes ease-kf-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}

@keyframes ease-kf-wobble {
0% { transform: translateX(0); }
15% { transform: translateX(-6px) rotate(-3deg); }
30% { transform: translateX(5px) rotate(2deg); }
45% { transform: translateX(-4px) rotate(-1deg); }
60% { transform: translateX(3px) rotate(1deg); }
75% { transform: translateX(-1px); }
100% { transform: translateX(0); }
}

@keyframes ease-kf-flip-x {
from { transform: perspective(400px) rotateX(90deg); opacity: 0; }
to { transform: perspective(400px) rotateX(0deg); opacity: 1; }
}

@keyframes ease-kf-flip-y {
from { transform: perspective(400px) rotateY(90deg); opacity: 0; }
to { transform: perspective(400px) rotateY(0deg); opacity: 1; }
}

@keyframes ease-kf-heartbeat {
0% { transform: scale(1); }
14% { transform: scale(1.3); }
28% { transform: scale(1); }
42% { transform: scale(1.3); }
70% { transform: scale(1); }
}

@keyframes ease-kf-rubber-band {
0% { transform: scaleX(1); }
30% { transform: scaleX(1.25) scaleY(0.75); }
40% { transform: scaleX(0.75) scaleY(1.25); }
50% { transform: scaleX(1.15) scaleY(0.85); }
65% { transform: scaleX(0.95) scaleY(1.05); }
75% { transform: scaleX(1.05) scaleY(0.95); }
100% { transform: scaleX(1); }
}

/* ── Utility classes (for plain CSS usage, not just em="") ── */

.ease-spin {
animation: ease-kf-spin 1.2s linear var(--ease-animation-iterations, infinite);
}

.ease-wobble {
animation: ease-kf-wobble 1s var(--ease-ease) both;
}

.ease-flip-x {
animation: ease-kf-flip-x var(--ease-speed-medium) var(--ease-ease) both;
backface-visibility: hidden;
}

.ease-flip-y {
animation: ease-kf-flip-y var(--ease-speed-medium) var(--ease-ease) both;
backface-visibility: hidden;
}

.ease-heartbeat {
animation: ease-kf-heartbeat 1.5s var(--ease-ease) var(--ease-animation-iterations, infinite);
}

.ease-rubber-band {
animation: ease-kf-rubber-band 1s var(--ease-ease) both;
}

/* ── Animated demo elements ────────────────────────────────── */

.anim-spin {
animation: ease-kf-spin 2s linear infinite;
font-size: 3rem;
display: inline-block;
line-height: 1;
}

.anim-wobble {
animation: ease-kf-wobble 1s var(--ease-ease) infinite;
font-size: 3rem;
display: inline-block;
line-height: 1;
}

.anim-flip-x, .anim-flip-y {
animation-duration: 1.2s;
animation-timing-function: var(--ease-ease);
animation-iteration-count: infinite;
backface-visibility: hidden;
}

.anim-flip-x { animation-name: ease-kf-flip-x; }
.anim-flip-y { animation-name: ease-kf-flip-y; }

.anim-heartbeat {
animation: ease-kf-heartbeat 1.5s var(--ease-ease) infinite;
font-size: 3rem;
display: inline-block;
line-height: 1;
}

.anim-rubber-band {
animation: ease-kf-rubber-band 1s var(--ease-ease) infinite;
font-size: 3rem;
display: inline-block;
line-height: 1;
}

/* ── Demo layout helpers ───────────────────────────────────── */

.demo-card {
min-height: 200px;
}

.demo-visual {
display: flex;
align-items: center;
justify-content: center;
padding: var(--ease-space-6) 0 var(--ease-space-4);
min-height: 90px;
}

.demo-emoji {
font-size: 3rem;
display: inline-block;
line-height: 1;
}

.demo-square {
width: 64px;
height: 64px;
background: var(--ease-color-primary);
border-radius: var(--ease-radius-md);
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 1.5rem;
font-weight: 700;
}

.flip-demo {
perspective: 600px;
}

.status-ok {
color: var(--ease-color-success);
font-weight: 600;
}

.status-fail {
color: var(--ease-color-danger);
font-weight: 600;
}

pre {
white-space: pre-wrap;
word-break: break-all;
}

/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
.anim-spin, .anim-wobble, .anim-flip-x, .anim-flip-y,
.anim-heartbeat, .anim-rubber-band {
animation: none;
}
}
Loading