| Stage | :root { width: 800px; height: 600px; background: #0f0f23; } |
| Shapes | type: rect(x,y,width,height,rx,ry) · circle(cx,cy,r) · ellipse(cx,cy,rx,ry) · path(d) · star/polygon(sides,outer-radius,inner-radius) · text · image · group |
| Paint | fill/stroke (hex, rgb(), linear-gradient(), named color, none); stroke-width, stroke-linecap, stroke-linejoin, stroke-dasharray, fill-rule, opacity |
border-radius | 1 value → uniform rx/ry; 2–4 values → CSS corner shorthand, expands to animatable border-top-left-radius etc. (rect only; no elliptical / form) |
box-shadow | [inset] dx dy [blur] [spread] [color], comma-separated multi-shadow, animatable; spread only inflates rect/circle/ellipse (paths ignore it) |
mix-blend-mode | all 16 CSS keywords, per-shape (no group isolation), static |
| Transform | transform: translate(x,y) rotate(45deg) scale(1.2) · transform-origin: center (no skew) |
| Individual transforms | translate: 40px 10px · rotate: 45deg · scale: 1.2 (same channels as transform:, last-wins) |
| Animate | animation: <name> <dur> <easing> <count> <dir> <delay> e.g. pulse 1.5s ease-in-out infinite |
| Keyframes | @keyframes n { 0% {…} 50% {…} 100% {…} } (transform: decomposes & merges) |
| Per-kf easing / hold | animation-timing-function: ease-out (or step-end, steps(3, jump-end)) inside a keyframe block — eases the segment from that keyframe to the next |
| Composite | animation-composition: add (longhand only, not in shorthand) — adds numeric channels onto the base pose; color/path fall back to replace |
| Easings | linear ease ease-in ease-out ease-in-out step-start step-end cubic-bezier(…) steps(<n>, <pos>) linear(<stops>) |
| Spring/bounce | linear(0, 1 33%, 0.55 46%, 1 62%, 0.78 74%, 1) — overshoot control points fake physics with 2 keyframes |
| Symbols | @define name {…} then #x { use: name; cx: …; fill: … } (use-site overrides) |
| Repeat / instancing | repeat: <int> on any node stamps N sibling copies with derived ids #field → field-1…field-N (descendants re-suffix too); composes with use:, nests multiplicatively. Static count only (no input()); repeat: 1 ≡ absent. Differentiate copies with sibling-index() / sibling-count() (1-based position / total among all siblings, so give a repeated family its own group) or random(per-element) — e.g. cx: calc(sibling-index() * 40px), animation-delay: calc(sibling-index() * -0.1s). See reference.md §Repeat & sibling math |
| Nesting | > #child { … } inside a rule body |
| Interactivity | :root { --cx: input(cursor.x) } + cx: var(--cx); &:hover {…} &:active {…}; cursor: pointer (pointer cursor on hover); clicks emit a popkorn:click DOM event (detail.{id,path,x,y}, no opt-in) |
Typed var() | --brand: #e94560 / --label: "Score" / --n: 30px then fill: var(--brand) / content: var(--label) / r: var(--n) — numeric interpolates, color/string snap (discrete); input() stays numeric-only |
| Random constant | random([per-element || --ident]?, <min>, <max>[, by <step>]) — a fixed roll frozen at build (not live noise), any number/calc() operand; carries min/max unit. Default = one shared roll; per-element rolls per instance (particles); --ident correlates calls; deterministic from source. See reference.md §3 |
| Text | text-align: center (maps to text-anchor) · letter-spacing: 2px (animatable; no-op on RN/Skia) · line-height: 1.4 (animatable) · content: "a\nb" for multi-line (\n \r \t \" \\ unescape) |
| Transitions | transition: fill 0.3s ease, transform 0.2s — state flips tween (enter+exit) instead of snapping; runtime-only, timeline stays pure |
| State machines | @machine m { initial: off; state off { to: on on click(#btn) } state on { to: off on click(#btn) } } + #btn:state(on) { animation: … } — named states that outlive the pointer (toggles, sequences, timeouts); :state() can start animation: (the jump over :hover). See reference.md §14, examples 11/12 |
| Scrubbing | animation-timeline: var(--progress) or input(scroll.progress) — drive an animation by a 0..1 value instead of the clock |
| Filters | filter: blur(12px) (radius animatable in @keyframes) · filter: blur(2px) drop-shadow(4px 6px 8px rgba(0,0,0,.4)) (drop-shadow static); applies to node + subtree |
| Motion path | offset-path: path('…'); offset-distance: 50%; offset-rotate: auto (animate offset-distance) |
| Mask | clip-path: circle(80 at 200 200) · mask: #layer alpha |
| Gradient/path animation | animate fill: linear-gradient(…) (same type + stop count) or d: 'M…' (same command sequence) in @keyframes; incompatible endpoints step |
| Retime subtree | time-offset: 2s; time-scale: 0.5 on a group — shifts + scales that node and all descendants (precomp-style; static) |
| Group opacity | cascades: opacity on a group dims its whole subtree |
| Paint order | siblings paint in document order; override with z-index: <int> (negatives allowed; also sets hit-test priority) |
| Visibility window | visible-from: 1s; visible-until: 3s — show node + subtree only in that scene-local window |
| Embed options | <popkorn-player loop controls autoplay fit="contain"> (fit: contain/cover/fill/none) |