- 74 brands: airbnb, stripe, apple, figma, linear, vercel, etc. - 147 files: DESIGN.md + README.md per brand (~2.1 MB) - Each DESIGN.md contains structured brand analysis (colors, typography, components) - Updated 00-overview.md with new design-systems directory
230 lines
9.2 KiB
Markdown
230 lines
9.2 KiB
Markdown
---
|
||
name: motion/procedural-patterns
|
||
title: Procedural Pattern Generator
|
||
description: Canvas-based procedural generation of animated patterns around title elements. Uses occupancy grid, BFS proximity map, and seeded RNG for deterministic layout.
|
||
tags: [motion, procedural, pattern, canvas, generation, animation, title-card]
|
||
source: github.com/halfof8/patterngen-oss
|
||
---
|
||
|
||
# Procedural Pattern Generator
|
||
|
||
## Что это
|
||
|
||
Инструментарий для автоматического создания анимированных заставок с процедурно размещёнными паттернами вокруг заголовков. Состоит из трёх компонентов:
|
||
|
||
1. **Web App** (React + Canvas 2D) — основной генератор
|
||
2. **Figma Plugin** — импорт дизайна из Figma
|
||
3. **Blender Add-on** — импорт PNG-последовательностей как emissive planes
|
||
|
||
Пайплайн: Figma → Web App (генерация) → PNG-последовательности → Blender (композиция).
|
||
|
||
## Алгоритм размещения
|
||
|
||
### 1. Occupancy Grid
|
||
Сетка 20px на 1920×1080 (96×54 ячеек). Title-элементы занимают ячейки, помечая их как занятые.
|
||
|
||
```typescript
|
||
class GridOccupancy {
|
||
private cells: boolean[][]; // GRID_ROWS × GRID_COLS
|
||
|
||
markRect(x, y, w, h) { /* occupy cells */ }
|
||
isBlockFree(col, row, spanCols, spanRows) { /* collision check */ }
|
||
findFreeBlocks(spanCols, spanRows, align) { /* find valid positions */ }
|
||
}
|
||
```
|
||
|
||
### 2. BFS Proximity Map
|
||
BFS от всех title-элементов к каждой ячейке сетки. Определяет «зону влияния» — где паттерны могут появляться относительно заголовка.
|
||
|
||
```typescript
|
||
function buildProximityMap(titles): number[][] {
|
||
const dist: number[][] = fill(Infinity);
|
||
// BFS from all title edges
|
||
while (queue.length > 0) {
|
||
const [cx, cy] = queue.shift();
|
||
for (const [dx, dy] of neighbors) {
|
||
const nd = dist[cy+dy][cx+dx];
|
||
if (nd > dist[cy][cx] + 1) {
|
||
dist[cy+dy][cx+dx] = dist[cy][cx] + 1;
|
||
queue.push([cx+dx, cy+dy]);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3. Seeded Placement
|
||
Deterministic RNG (mulberry32). Same seed = identical layout.
|
||
|
||
```typescript
|
||
function mulberry32(seed) {
|
||
let state = seed >>> 0;
|
||
return () => {
|
||
state = (state + 0x6D2B79F5) >>> 0;
|
||
let t = state;
|
||
t = Math.imul(t ^ (t >>> 15), t | 1);
|
||
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||
};
|
||
}
|
||
```
|
||
|
||
## 7 типов анимации паттернов
|
||
|
||
| Тип | Эффект | CSS/Canvas |
|
||
|-----|--------|------------|
|
||
| `dots` | Поэлементный fade-in со stagger | `globalAlpha` per shape |
|
||
| `capsule` | Рост от центра по длинной оси | Clip rect grow |
|
||
| `circle` | Pie-fill по часовой | Arc + clip |
|
||
| `arrow` | Slide up снизу | `translateY` offset |
|
||
| `square` | Scale 1.6× → 1× | `scale()` transform |
|
||
| `stripes` | Полосы slide-in staggered | Y-offset per stripe |
|
||
| `cross` | Scale 1.6× → 1× | `scale()` transform |
|
||
|
||
## Timeline: 3 фазы
|
||
|
||
```
|
||
0──────────→ durationMs ──────────→ 2×durationMs ──────────→ 4×durationMs (loop)
|
||
│ ФАЗА 1 │ ФАЗА 2 │ ФАЗА 3 │
|
||
│ Title reveal │ Pattern reveal │ Dots pulse │
|
||
│ half-reveal из центра │ clip-side reveal │ sin wave │
|
||
```
|
||
|
||
```typescript
|
||
// Фазовая логика timeline
|
||
function evaluate(elapsedMs, durationMs, titles, patterns, dots, easing) {
|
||
// Phase 1: Title reveal (0→1×)
|
||
const titleT = Math.min(1, elapsedMs / durationMs);
|
||
const titleEased = applyEasing(titleT, easing); // half-reveal
|
||
|
||
// Phase 2: Pattern reveal (1×→2×)
|
||
const cycleMs = afterTitle % (4 * durationMs);
|
||
|
||
// Phase 3: Dots pulse (2×→4×)
|
||
const holdMs = cycleMs - durationMs;
|
||
const pulse = 0.15 + 0.85 * (0.5 + 0.5 * Math.sin(2π * speed * t + phase));
|
||
}
|
||
```
|
||
|
||
## Color System
|
||
|
||
```typescript
|
||
function getPairsForColor(primary: string): ColorPair[] {
|
||
const lum = luminance(r, g, b);
|
||
const dark = mixRGB(primary, #000000, 0.45);
|
||
const light = mixRGB(primary, #FFFFFF, 0.35);
|
||
|
||
if (lum > 0.5) {
|
||
return [
|
||
{ bg: primary, fg: '#000000' },
|
||
{ bg: dark, fg: primary },
|
||
{ bg: primary, fg: dark },
|
||
];
|
||
}
|
||
// inverted for dark colors
|
||
}
|
||
```
|
||
|
||
Конвенция SVG-паттернов (40×40): `fill="white"` = фон (перекрашивается runtime), `fill="black"` / `stroke="black"` = foreground.
|
||
|
||
## Figma Plugin
|
||
|
||
Экспортирует фреймы 1920×1080 в JSON. Каждый слой экспортируется как 2× PNG, цвета автодетектятся.
|
||
|
||
```javascript
|
||
// code.js — экспорт слоя
|
||
bytes = await child.exportAsync({
|
||
format: 'PNG',
|
||
constraint: { type: 'SCALE', value: 2 },
|
||
});
|
||
|
||
// Автодетект цветов
|
||
collectUsedColors(node, usedColors); // BFS по иерархии
|
||
```
|
||
|
||
## Blender Add-on
|
||
|
||
Импорт ZIP с 3 PNG-последовательностями (titles, patterns, dots) как emissive planes:
|
||
|
||
```python
|
||
# Создаёт plane с aspect-ratio из разрешения PNG
|
||
plane.scale = (6.0, 6.0, 6.0)
|
||
plane.rotation_euler = (90°, 0, 0)
|
||
|
||
# Emissive shader node graph
|
||
tex.image.source = 'SEQUENCE'
|
||
tex.image_user.use_cyclic = cyclic # dots = True
|
||
nt.links.new(tex.outputs['Color'], bsdf.inputs['Emission'])
|
||
nt.links.new(tex.outputs['Alpha'], bsdf.inputs['Alpha'])
|
||
```
|
||
|
||
## Ключевые параметры
|
||
|
||
| Параметр | Диапазон | Описание |
|
||
|----------|----------|----------|
|
||
| density | 1–20 | Количество паттернов (формула: `3 + 40 × (d/10)²`) |
|
||
| proximity | 1–20 | Зона влияния от title (maxRadius = `2 + proximity × 2.5`) |
|
||
| stagger | 0–5 | Stagger window: `(stagger/5) × 0.8` |
|
||
| durationMs | 100–20000 | Длительность фазы |
|
||
| EXPORT_SCALE | 4 | Рендер в 4× разрешении |
|
||
| FPS | 24 | Частота кадров |
|
||
|
||
## Anti-patterns
|
||
|
||
| ❌ Плохо | ✅ Хорошо |
|
||
|----------|-----------|
|
||
| Паттерны накладываются на title | BFS proximity map ограничивает placement |
|
||
| Без collision detection | Occupancy grid предотвращает наложения |
|
||
| Рандомная расстановка | Seeded RNG — детерминизм для итераций |
|
||
| Паттерны без анимации | 7 типов анимации с clip-side |
|
||
| Фиксированные цвета | Алгоритмическая генерация color pairs |
|
||
| Рендер без bg/fg разделения | white→bg, black→fg runtime colorize |
|
||
| 1 кадр = 1 экспорт | Layer export: titles/patterns/dots separately |
|
||
|
||
## Где используется
|
||
|
||
- **patterngen-oss** (github.com/halfof8/patterngen-oss) — open-source референс
|
||
- Title cards в motion design
|
||
- Анимированные фоны лендингов
|
||
- Procedural patterns для социальных сетей
|
||
- Генерация фоновых текстур для 3D-композиций в Blender
|
||
|
||
## CV Service Validation
|
||
|
||
Процедурные паттерны рендерятся на Canvas 2D — это клиентская генерация, не серверная. Для валидации дизайна страницы с pattern-генератором:
|
||
|
||
```bash
|
||
curl -X POST http://localhost:8001/design/full-analysis \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"html": "<HTML>", "full_analysis": true}'
|
||
```
|
||
|
||
**Особенности:** Canvas-элементы не оцениваются по semantic HTML — паттерны — это визуальный слой. Оценивается обёртка (header, sidebar, controls).
|
||
|
||
## Технические детали
|
||
|
||
```
|
||
Структура проекта:
|
||
├── src/store.ts # Zustand store — 373 строки, вся бизнес-логика
|
||
├── src/core/
|
||
│ ├── patterns.ts # Load SVG, colorize (white→bg, black→fg)
|
||
│ ├── colors.ts # getPairsForColor, luminance-based
|
||
│ ├── grid.ts # GridOccupancy + snapToGrid(20px)
|
||
│ ├── placement.ts # generatePlacement — BFS + occupancy
|
||
│ └── rng.ts # mulberry32 seeded RNG
|
||
├── src/engine/
|
||
│ ├── animations.ts # 7 анимаций (drawAnimatedPattern)
|
||
│ ├── easing.ts # 7 easing функций
|
||
│ └── timeline.ts # evaluate() — фазовая логика
|
||
├── src/export/
|
||
│ ├── renderer.ts # renderFrame() — canvas layer compositing
|
||
│ └── exporter.ts # exportAll() → ZIP with 3 layers × 24fps
|
||
├── figma-plugin/
|
||
│ ├── code.js # Figma sandbox export
|
||
│ └── ui.html # Plugin UI
|
||
└── blender-addon/
|
||
└── patterngen_importer.py # Blender 3.4+ operator
|
||
```
|
||
|
||
**Зависимости:** React 18, Zustand 5, JSZip, FileSaver, Vite
|