diff --git a/skills/gsap-effects/SKILL.md b/skills/gsap-effects/SKILL.md new file mode 100644 index 00000000..2f71fe8c --- /dev/null +++ b/skills/gsap-effects/SKILL.md @@ -0,0 +1,16 @@ +--- +name: gsap-effects +description: Ready-made GSAP animation effects for HyperFrames compositions. Use when adding typewriter text, text reveals, or character-by-character animation to a composition. Reference files contain copy-paste patterns. +--- + +# GSAP Effects + +Drop-in animation patterns for HyperFrames compositions. Each effect is a self-contained reference with the HTML, CSS, and GSAP code needed to add it to a composition. + +These effects follow all HyperFrames composition rules — deterministic, no randomness, timelines registered via `window.__timelines`. + +## Available Effects + +| Effect | File | Use when | +| ---------- | -------------------------------- | ---------------------------------------------------------------------------- | +| Typewriter | [typewriter.md](./typewriter.md) | Text should appear character by character, with or without a blinking cursor | diff --git a/skills/gsap-effects/typewriter.md b/skills/gsap-effects/typewriter.md new file mode 100644 index 00000000..226b7c6b --- /dev/null +++ b/skills/gsap-effects/typewriter.md @@ -0,0 +1,314 @@ +# Typewriter Effect + +Reveal text character by character with an optional blinking cursor. Uses GSAP's `TextPlugin` to animate the `text` property of an element. + +## Required Plugin + +```html + + + +``` + +## Basic Typewriter + +Type a sentence into an empty element at a steady pace. + +```html +
+``` + +```js +// Characters per second controls the feel: +// 3-5 cps = deliberate, dramatic +// 8-12 cps = conversational +// 15-20 cps = fast, energetic +const text = "Hello, world!"; +const cps = 10; +const duration = text.length / cps; + +tl.to( + "#typed-text", + { + text: { value: text }, + duration: duration, + ease: "none", // "none" gives even spacing — use "power2.in" for acceleration + }, + startTime, +); +``` + +`ease: "none"` produces evenly-spaced characters. Any other ease changes the typing rhythm — `"power2.in"` starts slow and speeds up, `"power4.out"` types fast then slows to a stop. + +## With Blinking Cursor + +Add a cursor element that blinks while idle and holds steady while typing. Three rules: + +1. **Only one cursor visible at a time.** Multiple visible cursors on screen looks broken. Every line gets its own cursor element, but only the active line's cursor is visible — all others must be `cursor-hide`. When a line finishes and the next line starts, hide the previous cursor before showing the next one. +2. **The cursor must always blink when idle** — after typing finishes, after clearing, during hold pauses. A cursor that just sits there solid looks broken. +3. **No gap between text and cursor** — the cursor element must be immediately adjacent to the text element in the HTML (no whitespace, no flex gap). Any space between the last character and `|` looks wrong. + +```html + +| +``` + +```css +@keyframes blink { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0; + } +} +.cursor-blink { + animation: blink 0.8s step-end infinite; +} +.cursor-solid { + animation: none; + opacity: 1; +} +.cursor-hide { + animation: none; + opacity: 0; +} +``` + +Three states: `cursor-blink` (idle), `cursor-solid` (actively typing), `cursor-hide` (cursor belongs to a different line). The pattern is always: blink → solid → type → solid → blink. + +```js +const text = "Hello, world!"; +const cps = 10; +const duration = text.length / cps; +const cursor = document.querySelector("#cursor"); + +// Cursor blinks before typing starts +cursor.classList.add("cursor-blink"); + +// Solid while typing +tl.call( + () => { + cursor.classList.replace("cursor-blink", "cursor-solid"); + }, + [], + startTime, +); + +// Type the text +tl.to( + "#typed-text", + { + text: { value: text }, + duration: duration, + ease: "none", + }, + startTime, +); + +// Back to blinking when done — never leave it solid +tl.call( + () => { + cursor.classList.replace("cursor-solid", "cursor-blink"); + }, + [], + startTime + duration, +); +``` + +When handing off between multiple typewriter lines, the new cursor must blink before it starts typing. Going straight from hidden to solid skips the idle state and looks like the cursor just appeared mid-keystroke. Always: hide previous → blink new → pause → then solid when typing begins. + +```js +// Step 1: hand off — new cursor appears blinking +tl.call( + () => { + prevCursor.classList.replace("cursor-blink", "cursor-hide"); + nextCursor.classList.replace("cursor-hide", "cursor-blink"); + }, + [], + handoffTime, +); + +// Step 2: after a brief blink pause (0.4-0.6s), go solid and start typing +const typeStart = handoffTime + 0.5; +tl.call( + () => { + nextCursor.classList.replace("cursor-blink", "cursor-solid"); + }, + [], + typeStart, +); +tl.to("#next-text", { text: { value: text }, duration: dur, ease: "none" }, typeStart); +tl.call( + () => { + nextCursor.classList.replace("cursor-solid", "cursor-blink"); + }, + [], + typeStart + dur, +); +``` + +## Spacing with Static Text + +When a typewriter word sits next to static text (e.g. "Ship something **bold.**"), use `margin-left` on a wrapper span around the dynamic text + cursor. Do not use flex gap (it spaces the cursor away from the text) or a trailing space in the static text (it collapses when the dynamic text is empty). + +```html +