284 lines
7.8 KiB
Markdown
284 lines
7.8 KiB
Markdown
# Deterministic Generation — Seed-Based Layout Methodology
|
||
|
||
> Детерминированная генерация паттернов с использованием seeded PRNG.
|
||
> Основано на [patterngen-oss](https://github.com/halfof8/patterngen-oss).
|
||
|
||
## Обзор
|
||
|
||
Каждая генерация паттернов **детерминирована** — одинаковый seed всегда даёт
|
||
идентичный layout. Это позволяет:
|
||
- Повторно воспроизводить удачные комбинации
|
||
- Синхронизировать дизайн между Figma и PatternGen
|
||
- Кэшировать результаты генерации
|
||
- Делиться сценами через JSON с seed
|
||
|
||
---
|
||
|
||
## Seeded PRNG
|
||
|
||
### Алгоритм
|
||
|
||
```typescript
|
||
class SeededRNG {
|
||
private seed: number;
|
||
|
||
constructor(seed: number) {
|
||
this.seed = seed & 0x7FFFFFFF; // 31-bit positive
|
||
}
|
||
|
||
next(): number {
|
||
// LCG: Linear Congruential Generator
|
||
this.seed = (this.seed * 1103515245 + 12345) & 0x7FFFFFFF;
|
||
return this.seed / 0x7FFFFFFF; // range [0, 1)
|
||
}
|
||
|
||
nextInt(min: number, max: number): number {
|
||
return Math.floor(this.next() * (max - min + 1)) + min;
|
||
}
|
||
|
||
pick<T>(array: T[]): T {
|
||
return array[this.nextInt(0, array.length - 1)];
|
||
}
|
||
}
|
||
```
|
||
|
||
### Seed из Figma
|
||
|
||
```json
|
||
{
|
||
"seed": 123456789
|
||
}
|
||
```
|
||
|
||
31-bit integer генерируется плагином Figma. Значение зашито в JSON
|
||
и передается в PatternGen.
|
||
|
||
---
|
||
|
||
## Placement Engine
|
||
|
||
### Input parameters
|
||
|
||
| Параметр | Тип | Диапазон | Описание |
|
||
|----------|-----|----------|----------|
|
||
| `seed` | int | 0–2³¹-1 | Seed для PRNG |
|
||
| `density` | int | 1–20 | Количество элементов |
|
||
| `proximity` | int | 1–20 | Мин. расстояние до title (20 px grid units) |
|
||
|
||
### Placement algorithm
|
||
|
||
```
|
||
1. Создать RNG из seed
|
||
2. Для каждого элемента:
|
||
a. Сгенерировать случайную позицию (x, y)
|
||
b. Проверить proximity к существующим элементам и title
|
||
c. Если too close → пересадить с тем же RNG state
|
||
d. Иначе → записать позицию
|
||
3. Для каждого элемента:
|
||
a. Выбрать случайный pattern из доступных
|
||
b. Выбрать случайную clipSide (top/bottom/left/right)
|
||
c. Выбрать случайную анимацию
|
||
d. Сгенерировать stagger delay
|
||
```
|
||
|
||
### Stagger calculation
|
||
|
||
```typescript
|
||
const staggerWindow = Math.max(0, Math.min(0.85, (stagger / 5) * 0.8));
|
||
const staggeredStart = element.animDelay * staggerWindow * durationMs;
|
||
const elemDur = durationMs * (1 - staggerWindow);
|
||
const localMs = cycleMs - staggeredStart;
|
||
const p = Math.max(0, Math.min(1, localMs / elemDur));
|
||
```
|
||
|
||
---
|
||
|
||
## Animation Timeline
|
||
|
||
### Фаза 1: Title Reveal (0 — durationMs)
|
||
|
||
Title проявляется из центра по горизонтали (если wider) или вертикали (if taller).
|
||
|
||
```typescript
|
||
const titleT = min(1, max(0, elapsedMs / durationMs));
|
||
const titleEased = applyEasing(titleT, easing);
|
||
|
||
if (title.w >= title.h) {
|
||
// wide title: half-reveal horizontally
|
||
halfReveal = (title.w / 2) * titleEased;
|
||
revealBox = {
|
||
x: cx - halfReveal,
|
||
y: title.y,
|
||
w: halfReveal * 2,
|
||
h: title.h
|
||
};
|
||
} else {
|
||
// tall title: half-reveal vertically
|
||
halfReveal = (title.h / 2) * titleEased;
|
||
revealBox = {
|
||
x: title.x,
|
||
y: cy - halfReveal,
|
||
w: title.w,
|
||
h: halfReveal * 2
|
||
};
|
||
}
|
||
```
|
||
|
||
### Фаза 2: Pattern Reveal (durationMs — 2× durationMs)
|
||
|
||
Паттерны проявляются с stagger-задержкой.
|
||
|
||
### Фаза 3: Dots Loop (2× durationMs — 4× durationMs)
|
||
|
||
Точки пульсируют синусоидой 4000ms цикл.
|
||
|
||
```typescript
|
||
const fadeIn = min(1, loopMs / 800);
|
||
const wave = 0.5 + 0.5 * sin(2 * PI * dot.blinkSpeed * (loopMs / 1000) + dot.blinkPhase * 2 * PI);
|
||
const pulse = 0.15 + 0.85 * wave;
|
||
opacity = pulse * fadeIn;
|
||
```
|
||
|
||
---
|
||
|
||
## Color System
|
||
|
||
### Fixed Colors
|
||
|
||
```typescript
|
||
const FIXED_COLORS = ['#000000', '#FFFFFF'];
|
||
```
|
||
|
||
Черный и белый всегда доступны.
|
||
|
||
### Custom Colors
|
||
|
||
До 3 кастомных цветов добавляются через color picker.
|
||
Color pairs для паттернов генерируются алгоритмически:
|
||
|
||
```typescript
|
||
// Для каждого pair custom_color:
|
||
bg = customColor;
|
||
fg = generateComplementary(customColor);
|
||
```
|
||
|
||
### SVG Colorization
|
||
|
||
```typescript
|
||
function colorizeSvg(svgText: string, colors: ColorPair): string {
|
||
return svgText
|
||
.replace(/fill="white"/g, `fill="${colors.bg}"`)
|
||
.replace(/fill="black"/g, `fill="${colors.fg}"`)
|
||
.replace(/stroke="black"/g, `stroke="${colors.fg}"`);
|
||
}
|
||
```
|
||
|
||
SVG-паттерны хранят только `white` (фон) и `black` (foreground).
|
||
Окрашивание происходит в runtime.
|
||
|
||
---
|
||
|
||
## Image Caching
|
||
|
||
```typescript
|
||
const imageCache = new Map<string, HTMLImageElement>();
|
||
|
||
export function svgToImage(svgText: string): Promise<HTMLImageElement> {
|
||
if (imageCache.has(svgText)) return Promise.resolve(imageCache.get(svgText)!);
|
||
|
||
return new Promise((resolve, reject) => {
|
||
const img = new Image();
|
||
const blob = new Blob([svgText], { type: 'image/svg+xml;charset=utf-8' });
|
||
const url = URL.createObjectURL(blob);
|
||
img.onload = () => {
|
||
URL.revokeObjectURL(url);
|
||
imageCache.set(svgText, img);
|
||
resolve(img);
|
||
};
|
||
img.onerror = () => {
|
||
URL.revokeObjectURL(url);
|
||
reject(new Error('Failed to load SVG image'));
|
||
};
|
||
img.src = url;
|
||
});
|
||
}
|
||
|
||
export function clearImageCache() {
|
||
imageCache.clear();
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Export Pipeline
|
||
|
||
### Layer structure
|
||
|
||
```
|
||
titles: durationMs (1×) — plays once
|
||
patterns: durationMs (1×) — plays once
|
||
dots: 4000ms loop — infinite loop
|
||
```
|
||
|
||
### Export scale
|
||
|
||
- Canvas: 1920×1080
|
||
- Export Scale: 4× → 7680×4320
|
||
- FPS: 24
|
||
- Format: PNG (RGBA, transparent background)
|
||
|
||
### Frame calculation
|
||
|
||
```typescript
|
||
const totalFrames = Math.ceil((layerDuration / 1000) * FPS);
|
||
const frameRatio = totalFrames > 1 ? frame / (totalFrames - 1) : 1;
|
||
```
|
||
|
||
### Naming convention
|
||
|
||
```
|
||
<sceneName>_<layer>_<frame>.png
|
||
|
||
Пример:
|
||
MyTitle_titles_00000.png
|
||
MyTitle_titles_00001.png
|
||
MyTitle_patterns_00000.png
|
||
MyTitle_dots_00000.png
|
||
```
|
||
|
||
---
|
||
|
||
## Использование determinism в дизайне
|
||
|
||
### Scenario 1: Figma → PatternGen sync
|
||
|
||
1. Экспортировать сцену из Figma (содержит seed)
|
||
2. Загрузить в PatternGen
|
||
3. Паттерны генерируются точно так же каждый раз
|
||
4. Можно итерировать параметры и снова генерировать тот же layout
|
||
|
||
### Scenario 2: A/B testing
|
||
|
||
1. Создать базовую сцену с seed
|
||
2. Экспортировать 10 раз с разными параметрами density/stagger
|
||
3. Каждый раз layout идентичен, но timing разный
|
||
4. Сравнить конверсию/визуальное впечатление
|
||
|
||
### Scenario 3: Team collaboration
|
||
|
||
1. Дизайнер делится scene JSON
|
||
2. Коллега загружает и видит ту же генерацию
|
||
3. Может менять цвета, паттерны, параметры
|
||
4. Seed остаётся — layout predictable
|
||
|
||
---
|
||
|
||
## Референсы кода
|
||
|
||
- `src/core/rng.ts` — SeededRNG класс, LCG algorithm
|
||
- `src/core/placement.ts` — Placement engine, proximity check
|
||
- `src/engine/timeline.ts` — Phase 1/2/3, dots loop
|
||
- `src/export/exporter.ts` — PNG export, frame calculation
|
||
- `src/export/renderer.ts` — Canvas rendering per frame
|
||
- `src/types.ts` — AnimationState, PatternAnimType
|