picasso-skill 2.7.0 → 3.0.0
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.
- package/README.md +118 -115
- package/agents/picasso.md +44 -17
- package/bin/install.mjs +5 -3
- package/commands/godmode.md +3 -4
- package/package.json +1 -1
- package/skills/picasso/SKILL.md +82 -21
- package/skills/picasso/references/figma-mcp.md +190 -0
- package/skills/picasso/references/ux-evaluation.md +211 -0
- package/skills/picasso/references/visual-preview.md +367 -0
- package/skills/picasso/references/animation-performance.md +0 -244
- package/skills/picasso/references/interaction-design.md +0 -162
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
# Visual Preview Reference
|
|
2
|
+
|
|
3
|
+
Generate self-contained HTML previews to show users what design options look like before they commit. This replaces text-only descriptions with actual visual examples.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. The Preview Protocol
|
|
8
|
+
|
|
9
|
+
Every time Picasso presents 2+ aesthetic options for the user to choose from, generate a visual preview. Text-only option lists are banned for aesthetic decisions.
|
|
10
|
+
|
|
11
|
+
### Standard Flow
|
|
12
|
+
|
|
13
|
+
1. **Generate** a self-contained HTML file with inline styles (NO external font imports -- use `system-ui` with font name labels)
|
|
14
|
+
2. **Write** to `/tmp/picasso-preview-{name}.html`
|
|
15
|
+
3. **Screenshot** via Bash: `npx playwright screenshot /tmp/picasso-preview-{name}.html /tmp/picasso-preview-{name}.png --viewport-size=1200,800`
|
|
16
|
+
- `npx playwright screenshot` accepts file paths directly (no `file://` prefix needed)
|
|
17
|
+
- Do NOT use `mcp__playwright__browser_navigate` + `mcp__playwright__browser_take_screenshot` -- these timeout on external font loading and block `file://` protocol
|
|
18
|
+
4. **View** the screenshot with `Read /tmp/picasso-preview-{name}.png` (mandatory -- never skip this)
|
|
19
|
+
5. **Present** to the user with both paths (HTML for full-res browser viewing, PNG for quick preview)
|
|
20
|
+
|
|
21
|
+
### If npx playwright Is Unavailable
|
|
22
|
+
|
|
23
|
+
1. Write the HTML file to `/tmp/`
|
|
24
|
+
2. Tell the user: "I've generated a visual preview at `/tmp/picasso-preview-{name}.html` -- open it in your browser to see the options."
|
|
25
|
+
3. Do NOT make visual claims about what the preview looks like without viewing it
|
|
26
|
+
|
|
27
|
+
### Font Rule for Previews
|
|
28
|
+
|
|
29
|
+
Do NOT import Google Fonts or Fontshare fonts via `<link>` or `@import` in preview HTML. External font loading causes screenshot timeouts. Instead:
|
|
30
|
+
- Use `system-ui, -apple-system, sans-serif` for body text
|
|
31
|
+
- Use `Georgia, serif` for serif directions
|
|
32
|
+
- Use `ui-monospace, monospace` for mono directions
|
|
33
|
+
- **Label the intended font** in each preview card: "Font: Satoshi + DM Sans" so the user knows what will be used in the real implementation
|
|
34
|
+
|
|
35
|
+
### File Naming
|
|
36
|
+
|
|
37
|
+
- Interview aesthetics: `/tmp/picasso-interview-vibes.html`
|
|
38
|
+
- Design brief preview: `/tmp/picasso-brief-preview.html`
|
|
39
|
+
- Variants comparison: `/tmp/picasso-variants.html`
|
|
40
|
+
- Mood preview: `/tmp/picasso-mood-{word}.html`
|
|
41
|
+
- Preset browser: `/tmp/picasso-preset-browser.html`
|
|
42
|
+
- Standalone preview: `/tmp/picasso-preview.html`
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 2. HTML Template: Side-by-Side Direction Comparison
|
|
47
|
+
|
|
48
|
+
Use this when showing 2-4 aesthetic directions for the user to choose from (interview, /variants, /preview compare).
|
|
49
|
+
|
|
50
|
+
Generate the full HTML dynamically. For each direction, substitute the actual font, colors, radius, and spacing values. The template below is a structural guide -- adapt the content to match each specific direction.
|
|
51
|
+
|
|
52
|
+
```html
|
|
53
|
+
<!DOCTYPE html>
|
|
54
|
+
<html lang="en">
|
|
55
|
+
<head>
|
|
56
|
+
<meta charset="UTF-8">
|
|
57
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
58
|
+
<title>Picasso: Choose a Direction</title>
|
|
59
|
+
<!-- Import fonts for all directions shown -->
|
|
60
|
+
<!-- No external font imports -- use system fonts to avoid screenshot timeouts -->
|
|
61
|
+
<!-- Label intended fonts in the .font-info footer of each card -->
|
|
62
|
+
<style>
|
|
63
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
64
|
+
body {
|
|
65
|
+
font-family: system-ui, sans-serif;
|
|
66
|
+
background: #f5f5f5;
|
|
67
|
+
padding: 32px;
|
|
68
|
+
color: #1a1a1a;
|
|
69
|
+
}
|
|
70
|
+
h1 {
|
|
71
|
+
text-align: center;
|
|
72
|
+
font-size: 20px;
|
|
73
|
+
font-weight: 600;
|
|
74
|
+
margin-bottom: 8px;
|
|
75
|
+
letter-spacing: -0.02em;
|
|
76
|
+
}
|
|
77
|
+
.subtitle {
|
|
78
|
+
text-align: center;
|
|
79
|
+
font-size: 13px;
|
|
80
|
+
color: #666;
|
|
81
|
+
margin-bottom: 32px;
|
|
82
|
+
}
|
|
83
|
+
.grid {
|
|
84
|
+
display: grid;
|
|
85
|
+
grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
|
|
86
|
+
gap: 24px;
|
|
87
|
+
max-width: 1200px;
|
|
88
|
+
margin: 0 auto;
|
|
89
|
+
}
|
|
90
|
+
.direction {
|
|
91
|
+
border-radius: 12px;
|
|
92
|
+
overflow: hidden;
|
|
93
|
+
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
|
|
94
|
+
}
|
|
95
|
+
.direction-label {
|
|
96
|
+
padding: 12px 16px;
|
|
97
|
+
font-size: 13px;
|
|
98
|
+
font-weight: 600;
|
|
99
|
+
letter-spacing: 0.05em;
|
|
100
|
+
text-transform: uppercase;
|
|
101
|
+
background: #fff;
|
|
102
|
+
border-bottom: 1px solid #eee;
|
|
103
|
+
display: flex;
|
|
104
|
+
align-items: center;
|
|
105
|
+
justify-content: space-between;
|
|
106
|
+
}
|
|
107
|
+
.direction-label span {
|
|
108
|
+
font-size: 11px;
|
|
109
|
+
font-weight: 400;
|
|
110
|
+
text-transform: none;
|
|
111
|
+
letter-spacing: 0;
|
|
112
|
+
color: #999;
|
|
113
|
+
}
|
|
114
|
+
/* Each .preview is a mini page rendered in the direction's style */
|
|
115
|
+
.preview {
|
|
116
|
+
padding: 24px;
|
|
117
|
+
min-height: 400px;
|
|
118
|
+
display: flex;
|
|
119
|
+
flex-direction: column;
|
|
120
|
+
gap: 16px;
|
|
121
|
+
}
|
|
122
|
+
/* Palette strip */
|
|
123
|
+
.palette {
|
|
124
|
+
display: flex;
|
|
125
|
+
gap: 4px;
|
|
126
|
+
height: 24px;
|
|
127
|
+
border-radius: 4px;
|
|
128
|
+
overflow: hidden;
|
|
129
|
+
}
|
|
130
|
+
.palette .swatch {
|
|
131
|
+
flex: 1;
|
|
132
|
+
position: relative;
|
|
133
|
+
}
|
|
134
|
+
/* Sample nav */
|
|
135
|
+
.sample-nav {
|
|
136
|
+
display: flex;
|
|
137
|
+
align-items: center;
|
|
138
|
+
justify-content: space-between;
|
|
139
|
+
padding: 8px 0;
|
|
140
|
+
}
|
|
141
|
+
.sample-nav .logo {
|
|
142
|
+
font-weight: 700;
|
|
143
|
+
font-size: 15px;
|
|
144
|
+
}
|
|
145
|
+
.sample-nav .links {
|
|
146
|
+
display: flex;
|
|
147
|
+
gap: 16px;
|
|
148
|
+
font-size: 12px;
|
|
149
|
+
opacity: 0.6;
|
|
150
|
+
}
|
|
151
|
+
/* Sample card */
|
|
152
|
+
.sample-card {
|
|
153
|
+
padding: 16px;
|
|
154
|
+
display: flex;
|
|
155
|
+
flex-direction: column;
|
|
156
|
+
gap: 8px;
|
|
157
|
+
}
|
|
158
|
+
.sample-card h3 {
|
|
159
|
+
font-size: 16px;
|
|
160
|
+
font-weight: 600;
|
|
161
|
+
}
|
|
162
|
+
.sample-card p {
|
|
163
|
+
font-size: 12px;
|
|
164
|
+
line-height: 1.6;
|
|
165
|
+
opacity: 0.7;
|
|
166
|
+
}
|
|
167
|
+
/* Sample button */
|
|
168
|
+
.sample-btn {
|
|
169
|
+
display: inline-flex;
|
|
170
|
+
align-items: center;
|
|
171
|
+
justify-content: center;
|
|
172
|
+
padding: 8px 20px;
|
|
173
|
+
font-size: 12px;
|
|
174
|
+
font-weight: 600;
|
|
175
|
+
border: none;
|
|
176
|
+
cursor: pointer;
|
|
177
|
+
width: fit-content;
|
|
178
|
+
}
|
|
179
|
+
/* Sample input */
|
|
180
|
+
.sample-input {
|
|
181
|
+
padding: 8px 12px;
|
|
182
|
+
font-size: 12px;
|
|
183
|
+
border: 1px solid;
|
|
184
|
+
background: transparent;
|
|
185
|
+
width: 100%;
|
|
186
|
+
}
|
|
187
|
+
/* Font info footer */
|
|
188
|
+
.font-info {
|
|
189
|
+
font-size: 10px;
|
|
190
|
+
opacity: 0.4;
|
|
191
|
+
padding-top: 8px;
|
|
192
|
+
border-top: 1px solid;
|
|
193
|
+
border-color: inherit;
|
|
194
|
+
}
|
|
195
|
+
</style>
|
|
196
|
+
</head>
|
|
197
|
+
<body>
|
|
198
|
+
|
|
199
|
+
<h1>Choose a Direction</h1>
|
|
200
|
+
<p class="subtitle">Pick one, combine elements, or describe something different.</p>
|
|
201
|
+
|
|
202
|
+
<div class="grid">
|
|
203
|
+
|
|
204
|
+
<!-- Direction A -->
|
|
205
|
+
<div class="direction">
|
|
206
|
+
<div class="direction-label">
|
|
207
|
+
A: {DIRECTION_A_NAME}
|
|
208
|
+
<span>{DIRECTION_A_VIBE}</span>
|
|
209
|
+
</div>
|
|
210
|
+
<div class="preview" style="background: {A_BG}; color: {A_TEXT}; font-family: '{A_BODY_FONT}', sans-serif;">
|
|
211
|
+
<div class="palette">
|
|
212
|
+
<div class="swatch" style="background: {A_COLOR_1};"></div>
|
|
213
|
+
<div class="swatch" style="background: {A_COLOR_2};"></div>
|
|
214
|
+
<div class="swatch" style="background: {A_COLOR_3};"></div>
|
|
215
|
+
<div class="swatch" style="background: {A_COLOR_4};"></div>
|
|
216
|
+
<div class="swatch" style="background: {A_COLOR_5};"></div>
|
|
217
|
+
</div>
|
|
218
|
+
<div class="sample-nav">
|
|
219
|
+
<div class="logo" style="font-family: '{A_HEADING_FONT}', sans-serif;">AppName</div>
|
|
220
|
+
<div class="links">Features Pricing Docs</div>
|
|
221
|
+
</div>
|
|
222
|
+
<div style="font-family: '{A_HEADING_FONT}', sans-serif; font-size: 22px; font-weight: 700; line-height: 1.2; letter-spacing: -0.02em;">
|
|
223
|
+
Build something people actually want
|
|
224
|
+
</div>
|
|
225
|
+
<div style="font-size: 13px; line-height: 1.6; opacity: 0.7;">
|
|
226
|
+
A short description that shows what body text looks like in this direction. Notice the font, weight, and spacing.
|
|
227
|
+
</div>
|
|
228
|
+
<div class="sample-card" style="background: {A_SURFACE}; border-radius: {A_RADIUS};">
|
|
229
|
+
<h3 style="font-family: '{A_HEADING_FONT}', sans-serif;">Sample Card</h3>
|
|
230
|
+
<p>This is how cards look in this direction. Pay attention to padding, radius, and depth.</p>
|
|
231
|
+
</div>
|
|
232
|
+
<div style="display: flex; gap: 8px;">
|
|
233
|
+
<div class="sample-btn" style="background: {A_PRIMARY}; color: {A_PRIMARY_TEXT}; border-radius: {A_RADIUS};">
|
|
234
|
+
Primary Action
|
|
235
|
+
</div>
|
|
236
|
+
<div class="sample-btn" style="background: transparent; color: {A_TEXT}; border: 1px solid {A_BORDER}; border-radius: {A_RADIUS};">
|
|
237
|
+
Secondary
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
<div class="sample-input" style="border-color: {A_BORDER}; border-radius: {A_RADIUS}; color: {A_TEXT};" placeholder="Input field...">
|
|
241
|
+
Input field...
|
|
242
|
+
</div>
|
|
243
|
+
<div class="font-info">
|
|
244
|
+
{A_HEADING_FONT} + {A_BODY_FONT} · {A_RADIUS} radius
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
<!-- Repeat for Direction B, C, D... -->
|
|
250
|
+
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
</body>
|
|
254
|
+
</html>
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### How to Use This Template
|
|
258
|
+
|
|
259
|
+
1. Do NOT copy-paste this template literally. Generate the HTML dynamically with actual values substituted for each direction.
|
|
260
|
+
2. Each `{PLACEHOLDER}` must be replaced with real values from the direction you're previewing.
|
|
261
|
+
3. The font `<link>` tag must include ALL fonts used across ALL directions being compared.
|
|
262
|
+
4. The number of directions (2, 3, or 4) determines the grid columns.
|
|
263
|
+
5. Keep the preview compact -- users should see all options without scrolling.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## 3. HTML Template: Full Page Mood Preview
|
|
268
|
+
|
|
269
|
+
Use this for /mood and Design Brief previews. Shows a complete page layout in the specified style.
|
|
270
|
+
|
|
271
|
+
Generate a full-page HTML that includes:
|
|
272
|
+
- A navigation bar with logo text, 3-4 links, and a CTA button
|
|
273
|
+
- A hero section with a large heading, subtitle, and primary button
|
|
274
|
+
- A 3-column feature/card section
|
|
275
|
+
- A form section with an input and button
|
|
276
|
+
- A footer with muted text
|
|
277
|
+
|
|
278
|
+
All styled with the mood's tokens (colors, fonts, radius, spacing). The content should be generic but realistic (not lorem ipsum). Size the page to 1440px viewport width.
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## 4. HTML Template: Preset Browser Grid
|
|
283
|
+
|
|
284
|
+
Use this for /preset without arguments. Shows all presets as a browsable grid.
|
|
285
|
+
|
|
286
|
+
Generate an HTML page with:
|
|
287
|
+
- A title: "Picasso Style Presets"
|
|
288
|
+
- A grid of cards (4 columns, wrapping), one per preset
|
|
289
|
+
- Each card shows:
|
|
290
|
+
- Preset name (in the preset's heading font)
|
|
291
|
+
- A 5-swatch color palette strip
|
|
292
|
+
- A one-line mood description
|
|
293
|
+
- A tiny sample button in the preset's primary color and radius
|
|
294
|
+
- Cards should be ~280px wide, ~180px tall
|
|
295
|
+
- The card background should use the preset's surface color
|
|
296
|
+
- Card text should use the preset's text color
|
|
297
|
+
|
|
298
|
+
This gives users a visual catalog to browse before choosing.
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## 5. Font Loading
|
|
303
|
+
|
|
304
|
+
### Google Fonts
|
|
305
|
+
|
|
306
|
+
Most fonts can be loaded via Google Fonts. Construct the URL:
|
|
307
|
+
```
|
|
308
|
+
https://fonts.googleapis.com/css2?family={FontName}:wght@400;600;700&display=swap
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Replace spaces with `+` in font names.
|
|
312
|
+
|
|
313
|
+
### Fontshare (for fonts not on Google)
|
|
314
|
+
|
|
315
|
+
Some popular Picasso fonts are on Fontshare:
|
|
316
|
+
```
|
|
317
|
+
https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700&display=swap
|
|
318
|
+
https://api.fontshare.com/v2/css?f[]=cabinet-grotesk@400,700,800&display=swap
|
|
319
|
+
https://api.fontshare.com/v2/css?f[]=general-sans@400,500,600&display=swap
|
|
320
|
+
https://api.fontshare.com/v2/css?f[]=clash-display@400,600,700&display=swap
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Common Font Mappings
|
|
324
|
+
|
|
325
|
+
| Font Name | Source | Import URL |
|
|
326
|
+
|-----------|--------|-----------|
|
|
327
|
+
| Satoshi | Fontshare | `https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700&display=swap` |
|
|
328
|
+
| Cabinet Grotesk | Fontshare | `https://api.fontshare.com/v2/css?f[]=cabinet-grotesk@400,700,800&display=swap` |
|
|
329
|
+
| General Sans | Fontshare | `https://api.fontshare.com/v2/css?f[]=general-sans@400,500,600&display=swap` |
|
|
330
|
+
| Clash Display | Fontshare | `https://api.fontshare.com/v2/css?f[]=clash-display@400,600,700&display=swap` |
|
|
331
|
+
| Archivo Black | Google | `family=Archivo+Black&display=swap` |
|
|
332
|
+
| Manrope | Google | `family=Manrope:wght@300;400;500;600;700;800&display=swap` |
|
|
333
|
+
| Syne | Google | `family=Syne:wght@400;600;700;800&display=swap` |
|
|
334
|
+
| Space Mono | Google | `family=Space+Mono:wght@400;700&display=swap` |
|
|
335
|
+
| Cormorant | Google | `family=Cormorant:ital,wght@0,400;0,600;1,400&display=swap` |
|
|
336
|
+
| IBM Plex Sans | Google | `family=IBM+Plex+Sans:wght@300;400;500;600&display=swap` |
|
|
337
|
+
| DM Sans | Google | `family=DM+Sans:wght@400;500;600;700&display=swap` |
|
|
338
|
+
| Plus Jakarta Sans | Google | `family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap` |
|
|
339
|
+
| Outfit | Google | `family=Outfit:wght@300;400;500;600;700&display=swap` |
|
|
340
|
+
| Fraunces | Google | `family=Fraunces:ital,wght@0,400;0,600;1,400&display=swap` |
|
|
341
|
+
| Work Sans | Google | `family=Work+Sans:wght@400;500;600;700&display=swap` |
|
|
342
|
+
| JetBrains Mono | Google | `family=JetBrains+Mono:wght@400;500;700&display=swap` |
|
|
343
|
+
| Bodoni Moda | Google | `family=Bodoni+Moda:ital,wght@0,400;0,700;1,400&display=swap` |
|
|
344
|
+
| Inter | Google | `family=Inter:wght@400;500;600;700&display=swap` |
|
|
345
|
+
| Geist | Local/Vercel | Use `system-ui` as fallback in previews |
|
|
346
|
+
|
|
347
|
+
### Fallback Rule
|
|
348
|
+
|
|
349
|
+
If a font fails to load (offline, CORS, not available), the preview should still render correctly using `system-ui, -apple-system, sans-serif` as fallback. The font name should be displayed in the `.font-info` footer so the user knows what was intended.
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## 6. The Show-Don't-Tell Rule
|
|
354
|
+
|
|
355
|
+
| Decision Type | Show Visual? | Why |
|
|
356
|
+
|---------------|-------------|-----|
|
|
357
|
+
| Aesthetic direction / vibe | YES | Users can't imagine "Bold Signal" from text |
|
|
358
|
+
| Color palette | YES | Hex values mean nothing without seeing them |
|
|
359
|
+
| Font pairing | YES | Font names mean nothing without rendering them |
|
|
360
|
+
| Layout structure | YES | "Asymmetric grid" means different things to everyone |
|
|
361
|
+
| Animation intensity | NO | Must be experienced in running code, not a static preview |
|
|
362
|
+
| Mobile priority level | NO | A number (1-5) is sufficient |
|
|
363
|
+
| Accessibility level | NO | A standard (AA/AAA) is sufficient |
|
|
364
|
+
| Performance budget | NO | A number is sufficient |
|
|
365
|
+
| Sound/haptics | NO | Must be experienced in running code |
|
|
366
|
+
|
|
367
|
+
The rule: if the decision involves how something LOOKS, show it. If it involves behavior, policy, or priority, text is fine.
|
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
# Animation Performance Reference
|
|
2
|
-
|
|
3
|
-
## Table of Contents
|
|
4
|
-
1. Compositor-Only Properties
|
|
5
|
-
2. Will-Change Best Practices
|
|
6
|
-
3. Layout Thrashing
|
|
7
|
-
4. IntersectionObserver vs Scroll Events
|
|
8
|
-
5. Web Animations API
|
|
9
|
-
6. Performance Measurement
|
|
10
|
-
7. Testing on Low-End Devices
|
|
11
|
-
8. Contain Property
|
|
12
|
-
9. Common Mistakes
|
|
13
|
-
|
|
14
|
-
---
|
|
15
|
-
|
|
16
|
-
## 1. Compositor-Only Properties
|
|
17
|
-
|
|
18
|
-
Only two CSS properties can be animated without triggering layout or paint: **transform** and **opacity**. Everything else causes reflow.
|
|
19
|
-
|
|
20
|
-
| Property | Layout | Paint | Composite | Animate? |
|
|
21
|
-
|---|---|---|---|---|
|
|
22
|
-
| `transform` | No | No | Yes | **Yes** |
|
|
23
|
-
| `opacity` | No | No | Yes | **Yes** |
|
|
24
|
-
| `filter` | No | Yes | Yes | Carefully |
|
|
25
|
-
| `background-color` | No | Yes | No | Avoid |
|
|
26
|
-
| `width`, `height` | Yes | Yes | No | **Never** |
|
|
27
|
-
| `top`, `left` | Yes | Yes | No | **Never** |
|
|
28
|
-
| `margin`, `padding` | Yes | Yes | No | **Never** |
|
|
29
|
-
| `border-radius` | No | Yes | No | Avoid |
|
|
30
|
-
|
|
31
|
-
```css
|
|
32
|
-
/* Good: compositor-only */
|
|
33
|
-
.slide-in {
|
|
34
|
-
transform: translateX(-100%);
|
|
35
|
-
opacity: 0;
|
|
36
|
-
transition: transform 300ms var(--ease-out), opacity 300ms var(--ease-out);
|
|
37
|
-
}
|
|
38
|
-
.slide-in.active {
|
|
39
|
-
transform: translateX(0);
|
|
40
|
-
opacity: 1;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/* Bad: triggers layout on every frame */
|
|
44
|
-
.slide-in-bad {
|
|
45
|
-
left: -100%;
|
|
46
|
-
transition: left 300ms ease;
|
|
47
|
-
}
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
---
|
|
51
|
-
|
|
52
|
-
## 2. Will-Change Best Practices
|
|
53
|
-
|
|
54
|
-
`will-change` promotes an element to its own compositor layer. This speeds up animation but consumes GPU memory.
|
|
55
|
-
|
|
56
|
-
Rules:
|
|
57
|
-
- Add `will-change` BEFORE the animation starts (e.g., on hover, not in the animation itself).
|
|
58
|
-
- Remove it AFTER the animation completes.
|
|
59
|
-
- Never use `will-change: all` — it promotes everything.
|
|
60
|
-
- Never apply it to more than 10 elements simultaneously.
|
|
61
|
-
- Don't put it in your stylesheet permanently.
|
|
62
|
-
|
|
63
|
-
```js
|
|
64
|
-
// Good: apply before, remove after
|
|
65
|
-
element.addEventListener('mouseenter', () => {
|
|
66
|
-
element.style.willChange = 'transform';
|
|
67
|
-
});
|
|
68
|
-
element.addEventListener('transitionend', () => {
|
|
69
|
-
element.style.willChange = 'auto';
|
|
70
|
-
});
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
```css
|
|
74
|
-
/* Acceptable: for elements that are ALWAYS animated (e.g., loading spinners) */
|
|
75
|
-
.spinner { will-change: transform; }
|
|
76
|
-
|
|
77
|
-
/* Bad: permanent will-change on static elements */
|
|
78
|
-
.card { will-change: transform, opacity; } /* don't do this */
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
---
|
|
82
|
-
|
|
83
|
-
## 3. Layout Thrashing
|
|
84
|
-
|
|
85
|
-
Reading layout properties then writing them in a loop forces the browser to recalculate layout on every iteration.
|
|
86
|
-
|
|
87
|
-
```js
|
|
88
|
-
// BAD: layout thrashing (read-write-read-write)
|
|
89
|
-
elements.forEach(el => {
|
|
90
|
-
const height = el.offsetHeight; // READ — forces layout
|
|
91
|
-
el.style.height = height + 10 + 'px'; // WRITE — invalidates layout
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
// GOOD: batch reads, then batch writes
|
|
95
|
-
const heights = elements.map(el => el.offsetHeight); // all reads first
|
|
96
|
-
elements.forEach((el, i) => {
|
|
97
|
-
el.style.height = heights[i] + 10 + 'px'; // all writes after
|
|
98
|
-
});
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
Properties that trigger forced layout when read: `offsetHeight`, `offsetWidth`, `getBoundingClientRect()`, `scrollTop`, `clientHeight`, `getComputedStyle()`.
|
|
102
|
-
|
|
103
|
-
---
|
|
104
|
-
|
|
105
|
-
## 4. IntersectionObserver vs Scroll Events
|
|
106
|
-
|
|
107
|
-
| | `scroll` event | IntersectionObserver |
|
|
108
|
-
|---|---|---|
|
|
109
|
-
| Performance | Fires every pixel, blocks main thread | Async, fires on threshold crossing |
|
|
110
|
-
| Throttling | Manual (requestAnimationFrame) | Built-in |
|
|
111
|
-
| Use case | Scroll-linked animation (position) | Visibility detection (enter/exit) |
|
|
112
|
-
| Recommendation | Almost never | Almost always |
|
|
113
|
-
|
|
114
|
-
```js
|
|
115
|
-
// Good: IntersectionObserver for reveal animations
|
|
116
|
-
const observer = new IntersectionObserver(
|
|
117
|
-
(entries) => {
|
|
118
|
-
entries.forEach(entry => {
|
|
119
|
-
entry.target.classList.toggle('visible', entry.isIntersecting);
|
|
120
|
-
});
|
|
121
|
-
},
|
|
122
|
-
{ threshold: 0.1 }
|
|
123
|
-
);
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
For scroll-linked animations where you need exact scroll position, use the Scroll-Driven Animations API:
|
|
127
|
-
|
|
128
|
-
```css
|
|
129
|
-
@keyframes fade-in {
|
|
130
|
-
from { opacity: 0; transform: translateY(20px); }
|
|
131
|
-
to { opacity: 1; transform: translateY(0); }
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
.scroll-reveal {
|
|
135
|
-
animation: fade-in linear;
|
|
136
|
-
animation-timeline: view();
|
|
137
|
-
animation-range: entry 0% entry 100%;
|
|
138
|
-
}
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
---
|
|
142
|
-
|
|
143
|
-
## 5. Web Animations API
|
|
144
|
-
|
|
145
|
-
For complex JS-driven animations, use WAAPI instead of manual `requestAnimationFrame`:
|
|
146
|
-
|
|
147
|
-
```js
|
|
148
|
-
element.animate(
|
|
149
|
-
[
|
|
150
|
-
{ transform: 'translateY(20px)', opacity: 0 },
|
|
151
|
-
{ transform: 'translateY(0)', opacity: 1 }
|
|
152
|
-
],
|
|
153
|
-
{
|
|
154
|
-
duration: 300,
|
|
155
|
-
easing: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
|
156
|
-
fill: 'forwards'
|
|
157
|
-
}
|
|
158
|
-
);
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
Benefits over CSS: programmable, cancellable, can read progress, can reverse.
|
|
162
|
-
|
|
163
|
-
---
|
|
164
|
-
|
|
165
|
-
## 6. Performance Measurement
|
|
166
|
-
|
|
167
|
-
```js
|
|
168
|
-
// Measure animation frame rate
|
|
169
|
-
let frames = 0;
|
|
170
|
-
let lastTime = performance.now();
|
|
171
|
-
|
|
172
|
-
function countFrames() {
|
|
173
|
-
frames++;
|
|
174
|
-
const now = performance.now();
|
|
175
|
-
if (now - lastTime >= 1000) {
|
|
176
|
-
console.log(`FPS: ${frames}`);
|
|
177
|
-
frames = 0;
|
|
178
|
-
lastTime = now;
|
|
179
|
-
}
|
|
180
|
-
requestAnimationFrame(countFrames);
|
|
181
|
-
}
|
|
182
|
-
requestAnimationFrame(countFrames);
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
Chrome DevTools:
|
|
186
|
-
1. Performance tab → Record → interact with animations → Stop
|
|
187
|
-
2. Look for long frames (> 16ms) in the flame chart
|
|
188
|
-
3. Rendering tab → Paint flashing (green = repaint, should be minimal)
|
|
189
|
-
4. Rendering tab → Layer borders (orange = composited layers)
|
|
190
|
-
|
|
191
|
-
Target: 60fps = 16.67ms per frame. If ANY frame takes > 33ms, users perceive jank.
|
|
192
|
-
|
|
193
|
-
---
|
|
194
|
-
|
|
195
|
-
## 7. Testing on Low-End Devices
|
|
196
|
-
|
|
197
|
-
Your M-series Mac is not representative. Test with:
|
|
198
|
-
- **Chrome DevTools CPU throttling:** Performance tab → CPU → 6x slowdown
|
|
199
|
-
- **Network throttling:** Slow 3G preset to test loading animations
|
|
200
|
-
- **Real device:** Test on a 3-year-old Android phone if possible
|
|
201
|
-
|
|
202
|
-
If an animation stutters at 6x CPU throttle, reduce:
|
|
203
|
-
1. Number of concurrent animations
|
|
204
|
-
2. Element count being animated
|
|
205
|
-
3. Complexity of each animation
|
|
206
|
-
|
|
207
|
-
---
|
|
208
|
-
|
|
209
|
-
## 8. Contain Property
|
|
210
|
-
|
|
211
|
-
`contain` tells the browser what NOT to recalculate when an element changes.
|
|
212
|
-
|
|
213
|
-
```css
|
|
214
|
-
/* Isolate layout/paint to this element */
|
|
215
|
-
.card {
|
|
216
|
-
contain: layout paint;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/* Full isolation — best for off-screen or independent components */
|
|
220
|
-
.widget {
|
|
221
|
-
contain: strict; /* = size + layout + paint + style */
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/* Content containment — layout + paint + style (most common) */
|
|
225
|
-
.list-item {
|
|
226
|
-
contain: content;
|
|
227
|
-
}
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
Use `contain: content` on repeated elements (list items, cards) to prevent layout changes from propagating to siblings.
|
|
231
|
-
|
|
232
|
-
---
|
|
233
|
-
|
|
234
|
-
## 9. Common Mistakes
|
|
235
|
-
|
|
236
|
-
- **Animating `width`/`height`/`top`/`left`.** Use `transform: translate/scale` instead.
|
|
237
|
-
- **`will-change` on everything.** Max 10 elements. Remove after animation completes.
|
|
238
|
-
- **`addEventListener('scroll')` for visibility.** Use IntersectionObserver.
|
|
239
|
-
- **Reading layout properties in animation loops.** Batch reads before writes.
|
|
240
|
-
- **Testing only on fast hardware.** Use Chrome CPU throttling at 6x.
|
|
241
|
-
- **No frame budget awareness.** 16ms per frame. If your JS takes 20ms, you drop frames.
|
|
242
|
-
- **CSS `transition: all`.** Transitions every property including layout triggers.
|
|
243
|
-
- **Forgetting `contain` on repeated elements.** One card's layout change recalculates the entire list.
|
|
244
|
-
- **`requestAnimationFrame` without cancellation.** Always store the ID and `cancelAnimationFrame` on cleanup.
|