daemora 2026.1.2-beta.1 → 2026.1.2-beta.2

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.
@@ -0,0 +1,108 @@
1
+ ---
2
+ name: cursor-and-clicks
3
+ description: Animated cursor pointer, travel paths, and click feedback for product demo videos
4
+ metadata:
5
+ tags: cursor, pointer, click, ui-demo, interaction
6
+ ---
7
+
8
+ For product-demo videos that show a user "interacting" with a UI (clicking a button, toggling a setting, hovering a card), render an SVG cursor, animate its position with `interpolate`, and add press feedback when the click lands.
9
+
10
+ ## Cursor component
11
+
12
+ Use an SVG path for the arrow — CSS border-triangle hacks look amateurish. The path below is a clean macOS-style pointer with a white fill and black outline.
13
+
14
+ ```tsx
15
+ const Cursor = ({
16
+ x, y, pressed = 0, opacity = 1,
17
+ }: { x: number; y: number; pressed?: number; opacity?: number }) => (
18
+ <div style={{
19
+ position: 'absolute', left: x, top: y,
20
+ width: 80, height: 104,
21
+ transform: `translate(-6px,-4px) scale(${1 - pressed * 0.14})`,
22
+ transformOrigin: '6px 4px',
23
+ filter: 'drop-shadow(0 16px 32px rgba(0,0,0,0.75))',
24
+ opacity, pointerEvents: 'none',
25
+ }}>
26
+ <svg width="80" height="104" viewBox="0 0 28 36" xmlns="http://www.w3.org/2000/svg">
27
+ <path
28
+ d="M3 3 L3 28 L10 22 L14.5 32 L18.5 30.2 L14 20.2 L22.5 20.2 Z"
29
+ fill="#ffffff" stroke="#000000" strokeWidth="2.2"
30
+ strokeLinejoin="round" strokeLinecap="round"
31
+ />
32
+ </svg>
33
+ </div>
34
+ );
35
+ ```
36
+
37
+ The tip of the arrow is at `(3, 3)` in viewBox units. The `translate(-6px,-4px)` offset ensures `(x, y)` props refer to the **tip**, not the bounding box top-left — so you can target a button's center coordinates directly.
38
+
39
+ `transformOrigin: '6px 4px'` keeps the press scale anchored at the tip, not the middle of the arrow.
40
+
41
+ ## Travel path
42
+
43
+ Drive `x` and `y` with a single `travel` progress value from `0` → `1`, eased. Use a bezier that accelerates out of the start and decelerates into the target — matches how a real hand moves a mouse.
44
+
45
+ ```tsx
46
+ const travel = interpolate(frame, [START_F, END_F], [0, 1], {
47
+ easing: Easing.bezier(0.5, 0, 0.25, 1),
48
+ extrapolateLeft: 'clamp',
49
+ extrapolateRight: 'clamp',
50
+ });
51
+
52
+ const x = interpolate(travel, [0, 1], [START_X, TARGET_X]);
53
+ const y = interpolate(travel, [0, 1], [START_Y, TARGET_Y]);
54
+ ```
55
+
56
+ For non-linear paths (L-shapes, arcs), split `travel` into two ranges, or add a sine offset:
57
+
58
+ ```tsx
59
+ const arcY = Math.sin(travel * Math.PI) * -40; // lifts the cursor mid-travel
60
+ const y = interpolate(travel, [0, 1], [START_Y, TARGET_Y]) + arcY;
61
+ ```
62
+
63
+ ## Click feedback
64
+
65
+ A click is three things happening in ~8 frames: cursor scales down, the clicked element scales down, and a ring pulses outward. Drive all of them from a single `press` value with a 3-stop `interpolate`:
66
+
67
+ ```tsx
68
+ const press = interpolate(frame, [CLICK_F - 2, CLICK_F, CLICK_F + 6], [0, 1, 0], {
69
+ extrapolateLeft: 'clamp',
70
+ extrapolateRight: 'clamp',
71
+ });
72
+ ```
73
+
74
+ - **Cursor dip:** `scale(${1 - press * 0.14})` on the Cursor component (already wired above).
75
+ - **Button dip:** `scale(${1 + press * 0.05})` on the button wrapper — note the `+`, the button grows slightly before snapping back. Inverted looks wrong.
76
+ - **Ring pulse:** render a larger circle behind the click target, with opacity and border alpha tied to `press`:
77
+
78
+ ```tsx
79
+ {press > 0.01 && (
80
+ <div style={{
81
+ position: 'absolute',
82
+ left: thumbX - 10, top: thumbY - 10,
83
+ width: SIZE + 20, height: SIZE + 20,
84
+ borderRadius: '50%',
85
+ border: `3px solid rgba(124,58,237,${press * 0.7})`,
86
+ opacity: press,
87
+ }} />
88
+ )}
89
+ ```
90
+
91
+ ## Placement relative to zoom
92
+
93
+ If you're also zooming into the click target (see [focus-zoom.md](./focus-zoom.md)), render the cursor **inside** the zoomed container. The cursor scales with the content and stays visually locked on the button during the zoom — no extra inverse-transform math.
94
+
95
+ If you need the cursor to stay at a constant size (like macOS screen zoom), render it **outside** the zoom container and compute its screen-space position as `(target_x * s + tx, target_y * s + ty)` using the same `s/tx/ty` as the zoom transform.
96
+
97
+ ## Timing discipline
98
+
99
+ A realistic click sequence is:
100
+
101
+ 1. Cursor fades in at its start position (8–10 frames).
102
+ 2. Cursor travels to the target (30–40 frames for a full-screen diagonal).
103
+ 3. Cursor hovers for 2–4 frames (feels intentional, not machine-gun).
104
+ 4. Press fires (8 frames: down → hold 2 → up).
105
+ 5. Effect of the click plays (e.g. theme flip, page change) over 15–25 frames.
106
+ 6. Cursor holds or fades out.
107
+
108
+ Skipping the hover-pause in step 3 makes the click feel like a script, not a human.
@@ -0,0 +1,108 @@
1
+ ---
2
+ name: focus-zoom
3
+ description: Zoom into a specific UI element and back for product demo emphasis
4
+ metadata:
5
+ tags: zoom, transform, product-demo, focus, emphasis
6
+ ---
7
+
8
+ The "focus zoom" — zoom in on a button or region, hold while something happens, zoom back out — is the workhorse move of product demos. It draws attention to one element without cutting away from the scene.
9
+
10
+ ## The math
11
+
12
+ Given:
13
+
14
+ - **Target point** `(tx, ty)` in the zoom container's local coordinates (e.g. the center of a button)
15
+ - **Zoom scale** `s` (typical: 1.6–2.0; bigger feels cartoonish)
16
+ - **Viewport** `(W, H)` — the container you're zooming inside
17
+ - **Transform origin** `top left` (do not use `center` — the math gets messier)
18
+
19
+ You want `(tx, ty)` to land at `(W/2, H/2)` after the transform. With `translate(Tx, Ty) scale(s)` applied left-to-right, a point `(px, py)` becomes `(s*px + Tx, s*py + Ty)`. So:
20
+
21
+ ```
22
+ Tx = W/2 - tx * s
23
+ Ty = H/2 - ty * s
24
+ ```
25
+
26
+ Constants at the top of the file:
27
+
28
+ ```tsx
29
+ const VIEW_W = 1968;
30
+ const VIEW_H = 1200;
31
+ const TARGET_X = 1820;
32
+ const TARGET_Y = 112;
33
+ const ZOOM_SCALE = 1.85;
34
+ ```
35
+
36
+ ## The animation
37
+
38
+ Drive zoom from `0` (no zoom) → `1` (peak zoom) with a 4-stop `interpolate` that covers in → hold → out:
39
+
40
+ ```tsx
41
+ const zoom = interpolate(
42
+ frame,
43
+ [ZOOM_IN_START, ZOOM_IN_END, ZOOM_OUT_START, ZOOM_OUT_END],
44
+ [0, 1, 1, 0],
45
+ {
46
+ easing: Easing.inOut(Easing.cubic),
47
+ extrapolateLeft: 'clamp',
48
+ extrapolateRight: 'clamp',
49
+ },
50
+ );
51
+
52
+ const tx = interpolate(zoom, [0, 1], [0, VIEW_W / 2 - TARGET_X * ZOOM_SCALE]);
53
+ const ty = interpolate(zoom, [0, 1], [0, VIEW_H / 2 - TARGET_Y * ZOOM_SCALE]);
54
+ const scale = interpolate(zoom, [0, 1], [1, ZOOM_SCALE]);
55
+ ```
56
+
57
+ Apply the transform to a wrapping div:
58
+
59
+ ```tsx
60
+ <div style={{
61
+ position: 'absolute',
62
+ inset: 0,
63
+ transform: `translate(${tx}px, ${ty}px) scale(${scale})`,
64
+ transformOrigin: 'top left',
65
+ }}>
66
+ {/* content being zoomed */}
67
+ </div>
68
+ ```
69
+
70
+ `Easing.inOut(Easing.cubic)` gives a natural accelerate-then-decelerate. Avoid linear zoom — it feels robotic.
71
+
72
+ ## What to zoom
73
+
74
+ **Don't zoom the whole stage.** If your scene has 3D perspective, parallax, or a surrounding frame (laptop body, phone case, browser chrome), wrapping the entire composition in a zoom transform distorts those effects.
75
+
76
+ **Do zoom the inner content only.** Wrap just the screen's interior content (dashboard, UI, etc.) in the zoom div. The outer chrome stays rock-steady, so the zoom reads as "camera getting closer to the screen" rather than "whole scene inflating."
77
+
78
+ ## Sequencing with a click
79
+
80
+ Typical focus-zoom around a click lasts ~2 seconds and has five phases:
81
+
82
+ | Phase | Frames | What happens |
83
+ |---|---|---|
84
+ | Travel | 30–40 | Cursor moves toward target while zoom is still 0 |
85
+ | Zoom in | 18–20 | Cursor arrives; zoom goes 0 → 1 |
86
+ | Hold | 6–10 | Click fires, ring pulses, effect starts |
87
+ | Hold + effect | 15–20 | Whatever the click does plays out at peak zoom |
88
+ | Zoom out | 18–20 | Zoom goes 1 → 0 while the post-click state settles |
89
+
90
+ Overlap zoom-in with the tail of the cursor travel so both finish together — otherwise the cursor visibly "arrives and then waits" for the zoom.
91
+
92
+ ## Overflow and clipping
93
+
94
+ When the content is zoomed and translated, parts of it fall outside the viewport. Make sure the zoom's parent has `overflow: hidden` — otherwise clipped content spills over other scene elements.
95
+
96
+ ```tsx
97
+ <div style={{ position: 'absolute', inset: 0, overflow: 'hidden' }}>
98
+ <div style={{ transform: `translate(${tx}px, ${ty}px) scale(${scale})`, transformOrigin: 'top left' }}>
99
+ {/* ... */}
100
+ </div>
101
+ </div>
102
+ ```
103
+
104
+ ## Picking `ZOOM_SCALE`
105
+
106
+ - `1.3–1.5` — gentle emphasis, good for highlighting a region of a dashboard
107
+ - `1.6–2.0` — clear focus on a single button or control (the sweet spot)
108
+ - `2.5+` — dramatic, starts to feel like a jump-cut; content pixelation becomes visible on low-res renders
@@ -0,0 +1,103 @@
1
+ ---
2
+ name: theme-switching
3
+ description: Dark/light theme toggle animations with in-place cross-fade
4
+ metadata:
5
+ tags: theme, dark-mode, light-mode, toggle, cross-fade, ui-demo
6
+ ---
7
+
8
+ A theme toggle animation should look like a real app flipping its palette: same content, same positions, only colors change. The common mistake is to render two entirely different layouts and cross-fade between them — the content appears to morph, which never happens in a real app.
9
+
10
+ ## The rule
11
+
12
+ **One component, parameterized by theme. Render it twice.** Do not write separate `<DarkDashboard />` and `<LightDashboard />`.
13
+
14
+ ## Palette helper
15
+
16
+ Collect every theme-dependent color into a single object keyed by theme:
17
+
18
+ ```tsx
19
+ type Theme = 'dark' | 'light';
20
+
21
+ const getPalette = (theme: Theme) => {
22
+ const dark = theme === 'dark';
23
+ return {
24
+ bg: dark ? '#05070f' : 'linear-gradient(180deg,#f6f7fb,#eef1f8)',
25
+ text: dark ? '#ffffff' : '#0a0e1f',
26
+ textMid: dark ? 'rgba(255,255,255,0.62)' : 'rgba(10,14,31,0.58)',
27
+ cardBg: dark ? 'rgba(255,255,255,0.04)' : '#ffffff',
28
+ cardBorder: dark ? '1px solid rgba(255,255,255,0.1)' : '1px solid rgba(10,14,31,0.06)',
29
+ cardShadow: dark ? 'inset 0 1px 0 rgba(255,255,255,0.04)' : '0 20px 50px rgba(10,14,31,0.08)',
30
+ innerBg: dark ? 'rgba(255,255,255,0.05)' : '#f5f7fc',
31
+ innerBorder: dark ? '1px solid rgba(255,255,255,0.08)' : '1px solid rgba(10,14,31,0.05)',
32
+ };
33
+ };
34
+ ```
35
+
36
+ Put every color that differs between themes here. Anything you forget will flash wrong during the transition.
37
+
38
+ ## Parameterized component
39
+
40
+ ```tsx
41
+ const Dashboard = ({ theme, frame, progress }: { theme: Theme; frame: number; progress: number }) => {
42
+ const p = getPalette(theme);
43
+ return (
44
+ <div style={{ background: p.bg, color: p.text /* ... */ }}>
45
+ <h1 style={{ color: p.text }}>Analytics</h1>
46
+ <div style={{ background: p.cardBg, border: p.cardBorder, boxShadow: p.cardShadow }}>
47
+ {/* ... */}
48
+ </div>
49
+ </div>
50
+ );
51
+ };
52
+ ```
53
+
54
+ Data values (`$128.4k`, `42%`) can also vary by theme if you want the numbers to update along with the palette — the dashboard feels "alive" instead of a static snapshot being recolored. Keep the *layout* identical regardless.
55
+
56
+ ## Cross-fade
57
+
58
+ Render the component twice, with opposing opacities:
59
+
60
+ ```tsx
61
+ const themeFlip = interpolate(frame, [FLIP_START, FLIP_END], [0, 1], {
62
+ easing: Easing.inOut(Easing.cubic),
63
+ extrapolateLeft: 'clamp',
64
+ extrapolateRight: 'clamp',
65
+ });
66
+
67
+ return (
68
+ <>
69
+ <div style={{ position: 'absolute', inset: 0, opacity: 1 - themeFlip }}>
70
+ <Dashboard theme="dark" frame={frame} progress={dashProgress} />
71
+ </div>
72
+ <div style={{ position: 'absolute', inset: 0, opacity: themeFlip }}>
73
+ <Dashboard theme="light" frame={frame} progress={dashProgress} />
74
+ </div>
75
+ </>
76
+ );
77
+ ```
78
+
79
+ `Easing.inOut(Easing.cubic)` gives a natural flip. Duration of 15–25 frames feels right — faster looks glitchy, slower looks like a slow dissolve. Do **not** slide, wipe, or circular-reveal between themes; those transitions draw attention away from the content.
80
+
81
+ ## Accent colors that work on both backgrounds
82
+
83
+ Bar charts, progress tracks, and highlights often use gradients. If you use white in a gradient (`linear-gradient(180deg,#00d4ff,#ffffff)`), it disappears on a white background during the light theme. Pick accents that read on both:
84
+
85
+ - **Purple** `#7c3aed`
86
+ - **Cyan** `#00d4ff`
87
+ - **Pink** `#ec4899`
88
+
89
+ All three have enough saturation to stand out on both dark navy and off-white backgrounds. Avoid pure white and near-black as accent endpoints.
90
+
91
+ ## Toggle button placement
92
+
93
+ If you're also animating a toggle UI (pill + thumb), render the toggle **outside** both cross-faded layers, so it doesn't fade with the theme — it stays put while the background flips underneath it. The toggle's own appearance (track color, thumb position) is driven by the same `themeFlip` value:
94
+
95
+ ```tsx
96
+ <ToggleButton flip={themeFlip} /* ... */ />
97
+ ```
98
+
99
+ Inside `ToggleButton`, stack two track backgrounds with opposing opacities, and interpolate the thumb's `left` position from `0` → `1`.
100
+
101
+ ## Verification
102
+
103
+ After building, render a still at the midpoint of the flip (`frame = (FLIP_START + FLIP_END) / 2`). Both dashboards should be visible at 50% opacity and every piece of text, every card, every chart should be in the **exact same position**. If anything shifts, your layouts diverged — fix the component, don't tweak the cross-fade.
@@ -0,0 +1,139 @@
1
+ ---
2
+ name: ui-chrome
3
+ description: Rendering realistic device frames (macbook, phone, browser) with 3D perspective and inner screens
4
+ metadata:
5
+ tags: device-frame, macbook, phone, browser, perspective, 3d, mockup
6
+ ---
7
+
8
+ Product-demo videos often need a device "mockup" — a macbook, phone, or browser window showing an app. You can do this with pure CSS using layered divs, perspective transforms, and a clipped inner screen. No assets, no 3D libraries.
9
+
10
+ ## Structure
11
+
12
+ Every device frame has the same four-layer structure:
13
+
14
+ 1. **Outer wrapper** — sets position, applies perspective, handles motion blur/float
15
+ 2. **Body** — the physical shell (rounded rectangle with gradient fill + drop shadow)
16
+ 3. **Bezel** — inset rectangle, slightly darker, gives the "screen edge" look
17
+ 4. **Screen** — innermost div with `overflow: hidden`, where the actual UI content lives
18
+
19
+ ```tsx
20
+ <div style={{
21
+ position: 'absolute',
22
+ left: '50%', top: '50%',
23
+ transform: 'translate(-50%,-50%)',
24
+ }}>
25
+ {/* Wrapper: perspective + motion */}
26
+ <div style={{
27
+ position: 'relative',
28
+ width: 2320, height: 1380,
29
+ transform: `perspective(2600px) rotateX(20deg) rotateZ(-25deg) translateY(${bodyY}px) scale(${bodyScale})`,
30
+ filter: `blur(${motionBlur}px)`,
31
+ }}>
32
+ {/* Body */}
33
+ <div style={{
34
+ position: 'absolute', left: 50, top: 100,
35
+ width: 2220, height: 1160, borderRadius: 56,
36
+ background: 'linear-gradient(180deg,#15161b,#090b10)',
37
+ boxShadow: '0 80px 220px rgba(0,0,0,0.78), 0 0 130px rgba(124,58,237,0.24), inset 0 1px 0 rgba(255,255,255,0.12)',
38
+ }} />
39
+ {/* Bezel */}
40
+ <div style={{
41
+ position: 'absolute', left: 120, top: 0,
42
+ width: 2080, height: 1280, borderRadius: 40,
43
+ background: 'linear-gradient(180deg,#21242b,#0b0d11)',
44
+ boxShadow: '0 24px 80px rgba(0,0,0,0.5)',
45
+ }}>
46
+ {/* Screen */}
47
+ <div style={{
48
+ position: 'absolute', left: 56, top: 30,
49
+ width: 1968, height: 1200,
50
+ borderRadius: 30, overflow: 'hidden',
51
+ background: '#05070f',
52
+ boxShadow: 'inset 0 0 0 1px rgba(255,255,255,0.06)',
53
+ }}>
54
+ {/* app UI goes here */}
55
+ </div>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ ```
60
+
61
+ ## Perspective transform
62
+
63
+ `perspective(2600px) rotateX(20deg) rotateZ(-25deg)` gives the "hero angle" — a 3/4 view that shows depth without hiding the screen content. The perspective value controls foreshortening: lower numbers (`1200–1800px`) are more dramatic, higher (`3000–4000px`) are more subtle. Keep `rotateX` between `15°` and `25°` to avoid hiding too much of the screen.
64
+
65
+ The tilt also means device content needs to be **higher contrast** than flat content to remain readable — subtle text will disappear into the viewing angle.
66
+
67
+ ## Motion blur on entry
68
+
69
+ When the device animates into the scene (sliding up, scaling in), add a blur filter that fades to zero as the motion settles. This sells the "camera catching up" feeling:
70
+
71
+ ```tsx
72
+ const open = interpolate(frame, [10, 50], [0, 1], { easing: Easing.bezier(0.16, 1, 0.3, 1) });
73
+ const bodyY = interpolate(open, [0, 1], [150, 0]);
74
+ const bodyScale = interpolate(open, [0, 1], [0.84, 1]);
75
+ const motionBlur = interpolate(open, [0, 0.25], [30, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
76
+ ```
77
+
78
+ Blur should be gone by the first quarter of the open animation (`open = 0.25`), not linear across the whole thing — otherwise the device looks hazy even when it's stopped moving.
79
+
80
+ ## Idle float
81
+
82
+ Once the device is on screen, a subtle sine bob keeps it feeling alive:
83
+
84
+ ```tsx
85
+ const float = Math.sin((frame / DURATION) * Math.PI * 2);
86
+ const bodyY = interpolate(open, [0, 1], [150, 0]) + float * 8;
87
+ ```
88
+
89
+ `8px` amplitude is the sweet spot — any more feels like it's hovering in a breeze, any less is invisible.
90
+
91
+ ## Screen reflections and gradients
92
+
93
+ The inner screen should have subtle radial gradients layered over the app content to sell glass:
94
+
95
+ ```tsx
96
+ <div style={{
97
+ position: 'absolute', inset: 0,
98
+ background: 'radial-gradient(circle at 68% 24%, rgba(124,58,237,0.38), transparent 18%), radial-gradient(circle at 50% 50%, rgba(255,255,255,0.08), transparent 42%)',
99
+ pointerEvents: 'none',
100
+ }} />
101
+ ```
102
+
103
+ A bottom darkening gradient adds depth and visually separates the screen from the bezel:
104
+
105
+ ```tsx
106
+ <div style={{
107
+ position: 'absolute',
108
+ left: 0, right: 0, bottom: 0, height: 160,
109
+ background: 'linear-gradient(180deg, transparent, rgba(0,0,0,0.24))',
110
+ pointerEvents: 'none',
111
+ }} />
112
+ ```
113
+
114
+ ## Device proportions
115
+
116
+ | Device | Body W×H | Screen W×H | Radius |
117
+ |---|---|---|---|
118
+ | MacBook Pro 16" | `2320×1380` | `1968×1200` | body `56`, screen `30` |
119
+ | iPad Pro 13" | `1680×1200` | `1520×1080` | body `48`, screen `24` |
120
+ | iPhone 15 Pro | `540×1080` | `480×1020` | body `74`, screen `56` |
121
+ | Browser window | `2320×1380` | `2280×1300` (under chrome) | `24` |
122
+
123
+ ## Phones in a corner
124
+
125
+ For a secondary phone frame (showing a "mobile view" next to the main device), skip the 3D perspective and just drop a flat rounded-rectangle at a corner:
126
+
127
+ ```tsx
128
+ <div style={{ position: 'absolute', right: 190, bottom: 140 }}>
129
+ <div style={{
130
+ width: 540, height: 1080, borderRadius: 74,
131
+ background: 'linear-gradient(180deg,#17181d,#090a0f)',
132
+ boxShadow: '0 40px 140px rgba(0,0,0,0.8), 0 0 80px rgba(124,58,237,0.18)',
133
+ }}>
134
+ {/* inner screen */}
135
+ </div>
136
+ </div>
137
+ ```
138
+
139
+ Mixing a perspective-tilted macbook with a flat phone reads fine and is less visually busy than two tilted frames.