design-systems: add 74 brand DESIGN.md files (VoltAgent/awesome-design-md)
- 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
This commit is contained in:
1 parent
e3738f2b3e
commit
76c4c5b4fe
233 files changed
+54886
-1
No files matched your search
@@ -0,0 +1,320 @@
|
||||
---
|
||||
name: motion/animated-gradient
|
||||
title: Animated Gradient Background
|
||||
description: Smoothly shifting color gradients for backgrounds, buttons, and text. Creates premium, living feel.
|
||||
tags: [motion, gradient, animation, background, glassmorphism]
|
||||
source: stripe.com, raycast.com, resend.com, linear.app
|
||||
---
|
||||
|
||||
# Animated Gradient Background
|
||||
|
||||
## Что это
|
||||
Анимированные градиентные фоны, которые плавно перетекают друг в друга. Один из ключевых паттернов Stripe.com и современных SaaS-лендингов. Создаёт ощущение "живого" дизайна.
|
||||
|
||||
## Типы анимированных градиентов
|
||||
|
||||
### 1. Stripe-Style (shifting color flow)
|
||||
```css
|
||||
.gradient-bg {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
#6366f1,
|
||||
#8b5cf6,
|
||||
#ec4899,
|
||||
#f43f5e,
|
||||
#6366f1
|
||||
);
|
||||
background-size: 400% 400%;
|
||||
animation: gradient-shift 15s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes gradient-shift {
|
||||
0% { background-position: 0% 50%; }
|
||||
25% { background-position: 100% 50%; }
|
||||
50% { background-position: 100% 100%; }
|
||||
75% { background-position: 0% 100%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Radial Pulse (расширяющийся круг)
|
||||
```css
|
||||
.gradient-pulse {
|
||||
background: radial-gradient(
|
||||
circle at 30% 30%,
|
||||
rgba(99, 102, 241, 0.15),
|
||||
transparent 50%
|
||||
);
|
||||
animation: pulse 8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
opacity: 0.5;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Mesh Gradient (несколько источников)
|
||||
```css
|
||||
.mesh-gradient {
|
||||
background:
|
||||
radial-gradient(ellipse at 20% 80%, rgba(99, 102, 241, 0.15), transparent 50%),
|
||||
radial-gradient(ellipse at 80% 20%, rgba(236, 72, 153, 0.15), transparent 50%),
|
||||
radial-gradient(ellipse at 50% 50%, rgba(139, 92, 246, 0.1), transparent 50%);
|
||||
background-size: 100% 100%;
|
||||
animation: mesh-move 20s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes mesh-move {
|
||||
0% {
|
||||
background-position: 0% 0%, 100% 100%, 50% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 100% 100%, 0% 0%, 60% 40%;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Gradient Border (подвижная рамка)
|
||||
```css
|
||||
.gradient-border {
|
||||
position: relative;
|
||||
border-radius: 20px;
|
||||
padding: 2px; /* border width */
|
||||
background: linear-gradient(90deg, #6366f1, #ec4899, #f43f5e, #6366f1);
|
||||
background-size: 300% 100%;
|
||||
animation: border-gradient 4s linear infinite;
|
||||
}
|
||||
|
||||
.gradient-border::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 18px;
|
||||
background: #0a0a0a;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
@keyframes border-gradient {
|
||||
0% { background-position: 0% 50%; }
|
||||
100% { background-position: 100% 50%; }
|
||||
}
|
||||
```
|
||||
|
||||
## Glassmorphism Card (Stripe style)
|
||||
|
||||
```css
|
||||
.glass-card {
|
||||
position: relative;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.glass-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 255, 255, 0.05) 0%,
|
||||
transparent 50%
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
```
|
||||
|
||||
## Hero Gradient Background (Stripe.com style)
|
||||
|
||||
```html
|
||||
<section class="hero-gradient">
|
||||
<div class="gradient-orb orb-1"></div>
|
||||
<div class="gradient-orb orb-2"></div>
|
||||
<div class="gradient-orb orb-3"></div>
|
||||
<div class="hero-content">
|
||||
<h1>Your text here</h1>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
```css
|
||||
.hero-gradient {
|
||||
position: relative;
|
||||
min-height: 60vh;
|
||||
overflow: hidden;
|
||||
background: #0a0a0a;
|
||||
}
|
||||
|
||||
.gradient-orb {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(80px);
|
||||
opacity: 0.4;
|
||||
animation: orb-float 20s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.orb-1 {
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
background: radial-gradient(circle, #6366f1, transparent 70%);
|
||||
top: -20%;
|
||||
left: -10%;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.orb-2 {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background: radial-gradient(circle, #ec4899, transparent 70%);
|
||||
bottom: -20%;
|
||||
right: -5%;
|
||||
animation-delay: -5s;
|
||||
}
|
||||
|
||||
.orb-3 {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
background: radial-gradient(circle, #10b981, transparent 70%);
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
animation-delay: -10s;
|
||||
}
|
||||
|
||||
@keyframes orb-float {
|
||||
0%, 100% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
}
|
||||
25% {
|
||||
transform: translate(50px, -30px) scale(1.1);
|
||||
}
|
||||
50% {
|
||||
transform: translate(-30px, 50px) scale(0.95);
|
||||
}
|
||||
75% {
|
||||
transform: translate(30px, 30px) scale(1.05);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Animated Button Gradient
|
||||
|
||||
```css
|
||||
.btn-gradient-animated {
|
||||
position: relative;
|
||||
padding: 14px 28px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-gradient-animated::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
120deg,
|
||||
#6366f1,
|
||||
#8b5cf6,
|
||||
#ec4899,
|
||||
#f43f5e,
|
||||
#6366f1
|
||||
);
|
||||
background-size: 300% 100%;
|
||||
animation: btn-gradient 6s ease infinite;
|
||||
}
|
||||
|
||||
.btn-gradient-animated:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 30px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
@keyframes btn-gradient {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
```
|
||||
|
||||
## Color Palettes
|
||||
|
||||
### Indie/SaaS (Stripe/Linear style)
|
||||
```css
|
||||
--grad-indigo: #6366f1;
|
||||
--grad-purple: #8b5cf6;
|
||||
--grad-pink: #ec4899;
|
||||
--grad-rose: #f43f5e;
|
||||
--grad-cyan: #06b6d4;
|
||||
```
|
||||
|
||||
### Warm (Resend style)
|
||||
```css
|
||||
--grad-orange: #f97316;
|
||||
--grad-yellow: #eab308;
|
||||
--grad-red: #ef4444;
|
||||
--grad-pink: #ec4899;
|
||||
```
|
||||
|
||||
### Cool (Raycast style)
|
||||
```css
|
||||
--grad-blue: #3b82f6;
|
||||
--grad-cyan: #06b6d4;
|
||||
--grad-teal: #14b8a6;
|
||||
--grad-green: #10b981;
|
||||
```
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
| ❌ Плохо | ✅ Хорошо |
|
||||
|----------|-----------|
|
||||
| `animation: 3s` — слишком быстро (мозг не успевает) | `animation: 15–30s` — медленное плавное движение |
|
||||
| Яркие цвета `#ff0000` | Приглушённые `rgba(99,102,241,0.15)` |
|
||||
| Без `filter: blur()` на орбах | `blur(80–120px)` для мягких переходов |
|
||||
| Без `prefers-reduced-motion` | Уважать настройки пользователя |
|
||||
| Градиент на тексте без fallback | Всегда `-webkit-background-clip` |
|
||||
|
||||
## Performance
|
||||
|
||||
```css
|
||||
/* ✅ Good — CSS animation (GPU accelerated) */
|
||||
.gradient-bg {
|
||||
animation: gradient-shift 15s ease infinite;
|
||||
will-change: background-position;
|
||||
}
|
||||
|
||||
/* ❌ Bad — JS animation (main thread) */
|
||||
element.animate([
|
||||
{ backgroundPosition: '0% 50%' },
|
||||
{ backgroundPosition: '100% 50%' }
|
||||
], { duration: 15000, iterations: Infinity });
|
||||
```
|
||||
|
||||
## CV Service Validation
|
||||
|
||||
```bash
|
||||
# Проверить визуальную гармонию градиентов
|
||||
curl -s -X POST http://localhost:8001/design/analyze \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"image_base64": "...", "full_analysis": false}' | python3 -c "
|
||||
import sys, json
|
||||
d = json.load(sys.stdin)
|
||||
color = d['dimensions'].get('color_harmony', {})
|
||||
print(f\"Color Harmony: {color.get('score', 'N/A')}/10\")
|
||||
print(f\" Palette: {color.get('palette', 'N/A')}\")
|
||||
"
|
||||
```
|
||||
|
||||
Ожидается: `color_harmony` > 6.
|
||||
@@ -0,0 +1,145 @@
|
||||
---
|
||||
name: motion/hover-micro-interaction
|
||||
title: Hover Micro-interactions
|
||||
description: Subtle animations on hover: scale, shadow, border glow, color shift.
|
||||
tags: [motion, hover, micro-interaction, subtle, premium]
|
||||
source: linear.app, raycast.com, stripe.com, resend.com
|
||||
---
|
||||
|
||||
# Hover Micro-interactions
|
||||
|
||||
## Что это
|
||||
Микровзаимодействия при наведении: плавные изменения масштаба, тени, свечения бордера, цветового сдвига. Создают ощущение живого интерфейса.
|
||||
|
||||
## Типы hover-эффектов
|
||||
|
||||
### 1. Card Lift + Shadow
|
||||
```css
|
||||
.card {
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
.card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Border Glow
|
||||
```css
|
||||
.card {
|
||||
border: 1px solid var(--color-border);
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
.card:hover {
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: 0 0 20px rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Image Zoom
|
||||
```css
|
||||
.image-wrapper {
|
||||
overflow: hidden;
|
||||
border-radius: 16px;
|
||||
}
|
||||
.image-wrapper img {
|
||||
transition: transform 0.5s ease;
|
||||
}
|
||||
.image-wrapper:hover img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Text Color Shift
|
||||
```css
|
||||
.link {
|
||||
color: var(--color-text-secondary);
|
||||
position: relative;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
.link:hover {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
.link::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: var(--color-accent);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
.link:hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Icon Rotation/Swing
|
||||
```css
|
||||
.icon {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
.icon:hover {
|
||||
transform: rotate(15deg) scale(1.1);
|
||||
}
|
||||
```
|
||||
|
||||
## Magnetic Hover (Enhanced)
|
||||
|
||||
```css
|
||||
.hover-magnetic {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.hover-magnetic:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
document.querySelectorAll('.hover-magnetic').forEach(el => {
|
||||
el.addEventListener('mousemove', (e) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = (e.clientX - rect.left) / rect.width - 0.5;
|
||||
const y = (e.clientY - rect.top) / rect.height - 0.5;
|
||||
el.style.transform = `perspective(500px) rotateY(${x * 10}deg) rotateX(${-y * 10}deg) scale(1.02)`;
|
||||
});
|
||||
el.addEventListener('mouseleave', () => {
|
||||
el.style.transform = 'perspective(500px) rotateY(0) rotateX(0) scale(1)';
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
| ❌ Плохо | ✅ Хорошо |
|
||||
|----------|-----------|
|
||||
| `transform: scale(1.2)` — слишком резко | `transform: scale(1.02–1.05)` |
|
||||
| Без `transition` | `transition: all 0.3s ease` |
|
||||
| Без `will-change` на тяжёлых | `will-change: transform` |
|
||||
| Hover на touch-устройствах | `@media (hover: hover)` |
|
||||
|
||||
## Mobile Detection
|
||||
|
||||
```css
|
||||
@media (hover: none) {
|
||||
.hover-effect:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## CV Service Validation
|
||||
|
||||
```bash
|
||||
curl -s -X POST http://localhost:8001/design/analyze \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"image_base64": "...", "full_analysis": false}' | python3 -c "
|
||||
import sys, json
|
||||
d = json.load(sys.stdin)
|
||||
motion = d['dimensions'].get('motion_quality', {})
|
||||
print(f\"Motion Quality: {motion.get('score', 'N/A')}/10\")
|
||||
"
|
||||
```
|
||||
|
||||
Ожидается: `motion_quality` > 6.
|
||||
@@ -0,0 +1,151 @@
|
||||
---
|
||||
name: motion/magnetic-button
|
||||
title: Magnetic Button Effect
|
||||
description: Buttons that follow cursor within radius. Creates premium tactile feel.
|
||||
tags: [motion, magnetic, button, micro-interaction, premium]
|
||||
source: raycast.com, linear.app, brad.co, amppz.com
|
||||
---
|
||||
|
||||
# Magnetic Button Effect
|
||||
|
||||
## Что это
|
||||
Кнопки и элементы, которые притягиваются к курсору мыши в пределах радиуса. Создаёт ощущение тактильности и премиальности.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| **Radius** | 60–100px |
|
||||
| **Strength** | 0.1–0.3 |
|
||||
| **Easing** | `cubic-bezier(0.25, 0.1, 0.25, 1)` |
|
||||
| **Duration** | 0.3s |
|
||||
|
||||
## Реализация
|
||||
|
||||
```html
|
||||
<button class="magnetic-btn">Hover me</button>
|
||||
```
|
||||
|
||||
```css
|
||||
.magnetic-btn {
|
||||
padding: 16px 32px;
|
||||
background: var(--color-accent);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: transform 0.3s cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
class MagneticButton {
|
||||
constructor(element, strength = 0.2, radius = 80) {
|
||||
this.el = element;
|
||||
this.strength = strength;
|
||||
this.radius = radius;
|
||||
this.rect = element.getBoundingClientRect();
|
||||
this.isHovering = false;
|
||||
this.animationId = null;
|
||||
this.pos = { x: 0, y: 0 };
|
||||
this.target = { x: 0, y: 0 };
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.el.addEventListener('mouseenter', (e) => {
|
||||
this.isHovering = true;
|
||||
this.rect = this.el.getBoundingClientRect();
|
||||
});
|
||||
|
||||
this.el.addEventListener('mouseleave', () => {
|
||||
this.isHovering = false;
|
||||
this.target = { x: 0, y: 0 };
|
||||
this.animate();
|
||||
});
|
||||
|
||||
this.el.addEventListener('mousemove', (e) => {
|
||||
const x = e.clientX - this.rect.left - this.rect.width / 2;
|
||||
const y = e.clientY - this.rect.top - this.rect.height / 2;
|
||||
this.target = { x: x * this.strength, y: y * this.strength };
|
||||
});
|
||||
|
||||
this.animate();
|
||||
}
|
||||
|
||||
animate() {
|
||||
if (this.isHovering) {
|
||||
this.pos.x += (this.target.x - this.pos.x) * 0.1;
|
||||
this.pos.y += (this.target.y - this.pos.y) * 0.1;
|
||||
} else {
|
||||
this.pos.x *= 0.9;
|
||||
this.pos.y *= 0.9;
|
||||
}
|
||||
|
||||
this.el.style.transform = `translate(${this.pos.x}px, ${this.pos.y}px)`;
|
||||
this.animationId = requestAnimationFrame(() => this.animate());
|
||||
}
|
||||
|
||||
destroy() {
|
||||
cancelAnimationFrame(this.animationId);
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
document.querySelectorAll('.magnetic-btn').forEach(el => {
|
||||
new MagneticButton(el);
|
||||
});
|
||||
```
|
||||
|
||||
## Варианты силы притяжения
|
||||
|
||||
```javascript
|
||||
// Слабое (для текста)
|
||||
new MagneticButton(el, 0.1, 60);
|
||||
|
||||
// Среднее (для кнопок)
|
||||
new MagneticButton(el, 0.2, 80);
|
||||
|
||||
// Сильное (для крупных элементов)
|
||||
new MagneticButton(el, 0.3, 100);
|
||||
```
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
| ❌ Плохо | ✅ Хорошо |
|
||||
|----------|-----------|
|
||||
| `strength: 0.5` — слишком резко | `strength: 0.1–0.3` |
|
||||
| Без `mouseleave` сброса | Всегда сбрасывать в 0,0 |
|
||||
| Без `requestAnimationFrame` | Использовать RAF |
|
||||
| На мобильных без mouse | Проверять `matchMedia('(pointer: fine)')` |
|
||||
|
||||
## Mobile Fallback
|
||||
|
||||
```javascript
|
||||
if (window.matchMedia('(pointer: fine)').matches) {
|
||||
document.querySelectorAll('.magnetic-btn').forEach(el => {
|
||||
new MagneticButton(el);
|
||||
});
|
||||
}
|
||||
// На мобильных — просто hover-эффект
|
||||
```
|
||||
|
||||
## CV Service Validation
|
||||
|
||||
```bash
|
||||
# Проверить micro-interactions
|
||||
curl -s -X POST http://localhost:8001/design/analyze \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"image_base64": "...", "full_analysis": false}' | python3 -c "
|
||||
import sys, json
|
||||
d = json.load(sys.stdin)
|
||||
motion = d['dimensions'].get('motion_quality', {})
|
||||
print(f\"Motion Quality: {motion.get('score', 'N/A')}/10\")
|
||||
"
|
||||
```
|
||||
|
||||
Ожидается: `motion_quality` > 6.
|
||||
@@ -0,0 +1,119 @@
|
||||
---
|
||||
name: motion/page-transitions
|
||||
title: Page Transitions & Scroll Animations
|
||||
description: Smooth page transitions, section reveals, and scroll-triggered animations.
|
||||
tags: [motion, page-transition, scroll, reveal, animation]
|
||||
source: linear.app, raycast.com, brad.co, amppz.com
|
||||
---
|
||||
|
||||
# Page Transitions & Scroll Animations
|
||||
|
||||
## Что это
|
||||
Анимации при переходе между страницами и появление секций при скролле.
|
||||
|
||||
## 1. Section Reveal on Scroll
|
||||
|
||||
```javascript
|
||||
const sections = document.querySelectorAll('.reveal-section');
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('revealed');
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.1, rootMargin: '0px 0px -10% 0px' });
|
||||
|
||||
sections.forEach(section => observer.observe(section));
|
||||
```
|
||||
|
||||
```css
|
||||
.reveal-section {
|
||||
opacity: 0;
|
||||
transform: translateY(40px);
|
||||
transition: opacity 0.8s ease, transform 0.8s ease;
|
||||
}
|
||||
.reveal-section.revealed {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
```
|
||||
|
||||
## 2. Staggered Children Reveal
|
||||
|
||||
```javascript
|
||||
const sections = document.querySelectorAll('.stagger-section');
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.querySelectorAll('.stagger-child').forEach((child, i) => {
|
||||
setTimeout(() => {
|
||||
child.classList.add('revealed');
|
||||
}, i * 100);
|
||||
});
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.1 });
|
||||
|
||||
sections.forEach(section => observer.observe(section));
|
||||
```
|
||||
|
||||
```css
|
||||
.stagger-child {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: all 0.6s ease;
|
||||
}
|
||||
.stagger-child.revealed {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Page Transition (SPA style)
|
||||
|
||||
```css
|
||||
.page {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
transform: scale(1.05);
|
||||
transition: all 0.4s ease;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
.page.active {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
pointer-events: all;
|
||||
z-index: 1;
|
||||
}
|
||||
.page.exit {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Smooth Scroll Snap
|
||||
|
||||
```css
|
||||
.snap-scroll {
|
||||
scroll-snap-type: y mandatory;
|
||||
overflow-y: auto;
|
||||
height: 100vh;
|
||||
}
|
||||
.snap-section {
|
||||
scroll-snap-align: start;
|
||||
min-height: 100vh;
|
||||
}
|
||||
```
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
| ❌ Плохо | ✅ Хорошо |
|
||||
|----------|-----------|
|
||||
| `transition: 0.1s` — слишком быстро | `transition: 0.6–1s` |
|
||||
| Без `IntersectionObserver` | Использовать observer |
|
||||
| Без `prefers-reduced-motion` | Уважать настройки |
|
||||
| Без `will-change` | `will-change: transform, opacity` |
|
||||
@@ -0,0 +1,142 @@
|
||||
---
|
||||
name: motion/parallax-scroll
|
||||
title: Parallax Depth Effect
|
||||
description: Multi-layer scrolling with different speeds. Creates 3D depth perception.
|
||||
tags: [motion, parallax, depth, scroll, 3d, premium]
|
||||
source: apple.com, linear.app, amppz.com, brad.co
|
||||
---
|
||||
|
||||
# Parallax Depth Effect
|
||||
|
||||
## Что это
|
||||
Многослойный скролл с разной скоростью — создаёт ощущение глубины и 3D-пространства.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| **Foreground speed** | 1x (обычный скролл) |
|
||||
| **Midground speed** | 0.5x–0.7x |
|
||||
| **Background speed** | 0.1x–0.3x |
|
||||
|
||||
## CSS-реализация (простая)
|
||||
|
||||
```css
|
||||
.parallax-section {
|
||||
position: relative;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.parallax-layer {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 120%;
|
||||
top: -10%;
|
||||
}
|
||||
|
||||
.parallax-layer.background {
|
||||
z-index: 1;
|
||||
}
|
||||
.parallax-layer.midground {
|
||||
z-index: 2;
|
||||
transform: translateY(10%);
|
||||
}
|
||||
.parallax-layer.foreground {
|
||||
z-index: 3;
|
||||
transform: translateY(20%);
|
||||
}
|
||||
```
|
||||
|
||||
## GSAP ScrollTrigger (рекомендуется)
|
||||
|
||||
```javascript
|
||||
import gsap from 'gsap';
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
// Background slow
|
||||
gsap.to('.parallax-bg', {
|
||||
yPercent: 30,
|
||||
ease: 'none',
|
||||
scrollTrigger: {
|
||||
trigger: '.parallax-section',
|
||||
start: 'top bottom',
|
||||
end: 'bottom top',
|
||||
scrub: true
|
||||
}
|
||||
});
|
||||
|
||||
// Midground medium
|
||||
gsap.to('.parallax-mid', {
|
||||
yPercent: 50,
|
||||
ease: 'none',
|
||||
scrollTrigger: {
|
||||
trigger: '.parallax-section',
|
||||
start: 'top bottom',
|
||||
end: 'bottom top',
|
||||
scrub: true
|
||||
}
|
||||
});
|
||||
|
||||
// Foreground fast
|
||||
gsap.to('.parallax-fg', {
|
||||
yPercent: 80,
|
||||
ease: 'none',
|
||||
scrollTrigger: {
|
||||
trigger: '.parallax-section',
|
||||
start: 'top bottom',
|
||||
end: 'bottom top',
|
||||
scrub: true
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Intersection Observer (лёгкий вариант)
|
||||
|
||||
```javascript
|
||||
const layers = document.querySelectorAll('.parallax-layer');
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
const speed = parseFloat(entry.target.dataset.speed || 0.1);
|
||||
const rect = entry.target.getBoundingClientRect();
|
||||
const scrollPercent = window.scrollY / (window.innerHeight + rect.height);
|
||||
entry.target.style.transform = `translateY(${scrollPercent * speed * 100}%)`;
|
||||
});
|
||||
}, { threshold: 0.1 });
|
||||
|
||||
layers.forEach(layer => observer.observe(layer));
|
||||
```
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
| ❌ Плохо | ✅ Хорошо |
|
||||
|----------|-----------|
|
||||
| Без `prefers-reduced-motion` | Уважать настройки пользователя |
|
||||
| `yPercent: 100` — слишком резко | `yPercent: 30–50` |
|
||||
| Без `scrub: true` | `scrub: 1` для плавности |
|
||||
|太重ых элементов | SVG/градиенты вместо фото |
|
||||
|
||||
## Mobile
|
||||
|
||||
```css
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.parallax-layer {
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## CV Service Validation
|
||||
|
||||
```bash
|
||||
curl -s -X POST http://localhost:8001/design/analyze \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"image_base64": "...", "full_analysis": false}' | python3 -c "
|
||||
import sys, json
|
||||
d = json.load(sys.stdin)
|
||||
motion = d['dimensions'].get('motion_quality', {})
|
||||
print(f\"Motion Quality: {motion.get('score', 'N/A')}/10\")
|
||||
"
|
||||
```
|
||||
@@ -0,0 +1,213 @@
|
||||
# Pattern Animations — 7 типов процедурных анимаций
|
||||
|
||||
> Основано на реализациях из [patterngen-oss](https://github.com/halfof8/patterngen-oss).
|
||||
> 7 анимационных типов для SVG-паттернов, рендерящихся через Canvas 2D с таймлайном.
|
||||
|
||||
## Обзор
|
||||
|
||||
Каждый паттерн — это SVG-файл 40×40 px, который анимируется одним из 7 типов.
|
||||
Рендер происходит на Canvas 2D с 4× масштабированием (EXPORT_SCALE = 4).
|
||||
Базовый кадр 1920×1080, сетка 20 px, FPS = 24.
|
||||
|
||||
## Таймлайн
|
||||
|
||||
```
|
||||
Фаза 1 (0 — durationMs): Title reveal (half-reveal с easing)
|
||||
Фаза 2 (durationMs — 2×): Patterns reveal + squares reveal (staggered)
|
||||
Фаза 3 (2× — 4× durationMs): Patterns hold + dots pulsation (loop 4s)
|
||||
Цикл: repeat cycle = 4 × durationMs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. Dots — fade-in с stagger
|
||||
|
||||
Каждый SVG-элемент проявляется индивидуально с рандомной задержкой.
|
||||
|
||||
**Механика:**
|
||||
```
|
||||
dotP = max(0, min(1, (progress - stagger * 0.6) / 0.4))
|
||||
globalAlpha = dotP
|
||||
```
|
||||
|
||||
**Использование:**
|
||||
- Мягкое проявление множества мелких элементов
|
||||
- Звёздные/точечные паттерны, частицы, текстуры
|
||||
- Элементы с `stagger` из SVG (случайный при парсинге)
|
||||
|
||||
**Параметры:**
|
||||
- `stagger` определяет задержку: 0 = мгновенно, 5 = максимальный разброс
|
||||
- Визуально: элементы появляются поочерёдно, создавая «волну»
|
||||
|
||||
---
|
||||
|
||||
## 2. Capsule — рост из центра по длинной оси
|
||||
|
||||
Каждый элемент масштабируется от центра вдоль своей более длинной оси.
|
||||
|
||||
**Механика:**
|
||||
```
|
||||
elP = max(0, min(1, (progress - stagger * 0.3) / 0.7))
|
||||
|
||||
if (w >= h) {
|
||||
// горизонтальный: ширина растёт, высота стоит
|
||||
animW = w * elP
|
||||
animX = cx - animW / 2
|
||||
} else {
|
||||
// вертикальный: высота растёт, ширина стоит
|
||||
animH = h * elP
|
||||
animY = cy - animH / 2
|
||||
}
|
||||
```
|
||||
|
||||
**Использование:**
|
||||
- Анимация линий, полос, разделителей
|
||||
- Эффект «заполнения» вдоль направления
|
||||
- Элементы с rotation поддерживаются (через rotate transform)
|
||||
|
||||
---
|
||||
|
||||
## 3. Circle — pie-fill clockwise reveal
|
||||
|
||||
Полное SVG-изображение проявляется секторально, как таймер.
|
||||
|
||||
**Механика:**
|
||||
```
|
||||
cx = size / 2
|
||||
cy = size / 2
|
||||
startAngle = -PI / 2
|
||||
endAngle = startAngle + PI * 2 * progress
|
||||
|
||||
ctx.arc(cx, cy, size, startAngle, endAngle)
|
||||
ctx.drawImage(img, 0, 0, size, size)
|
||||
```
|
||||
|
||||
**Использование:**
|
||||
- Круговые паттерны, диаграммы, радары
|
||||
- Прогресс-индикаторы, loading-анимации
|
||||
- Полное изображение, не по-элементно
|
||||
|
||||
---
|
||||
|
||||
## 4. Arrow — slide up снизу
|
||||
|
||||
Полное изображение физически сдвигается вверх из-за нижнего края.
|
||||
|
||||
**Механика:**
|
||||
```
|
||||
offset = size * (1 - progress)
|
||||
ctx.drawImage(img, 0, offset, size, size)
|
||||
```
|
||||
|
||||
**Использование:**
|
||||
- Элементы «влетают» снизу
|
||||
- Эффект подъёма, выдвижения
|
||||
- Подходит для контрастных паттернов на тёмном фоне
|
||||
|
||||
---
|
||||
|
||||
## 5. Square — scale down 1.6× → 1×
|
||||
|
||||
Изображение масштабируется от 1.6× до 1× (на 60% больше до нормального).
|
||||
|
||||
**Механика:**
|
||||
```
|
||||
s = 1 + (1 - progress) * 0.6
|
||||
cx = size / 2
|
||||
cy = size / 2
|
||||
ctx.translate(cx, cy)
|
||||
ctx.scale(s, s)
|
||||
ctx.translate(-cx, -cy)
|
||||
ctx.drawImage(img, 0, 0, size, size)
|
||||
```
|
||||
|
||||
**Использование:**
|
||||
- Резкий акцент, «взрывное» проявление
|
||||
- Привлечение внимания к паттерну
|
||||
- Квадратные/крестообразные элементы
|
||||
|
||||
---
|
||||
|
||||
## 6. Stripes — slide-in слева направо
|
||||
|
||||
Каждая полоса (элемент) сдвигается сверху вниз, сортировка по X.
|
||||
|
||||
**Механика:**
|
||||
```
|
||||
sorted = shapes.sort((a, b) => a.x - b.x)
|
||||
stripeStart = (i / n) * 0.5
|
||||
stripeP = max(0, min(1, (progress - stripeStart) / 0.5))
|
||||
slideOffset = (1 - stripeP) * -size
|
||||
ctx.translate(0, slideOffset)
|
||||
```
|
||||
|
||||
**Использование:**
|
||||
- Полосатые паттерны, штрих-код текстуры
|
||||
- Горизонтальные линии, решётки
|
||||
- Эффект «сканирования» слева направо
|
||||
|
||||
---
|
||||
|
||||
## 7. Cross — scale down 1.6× → 1×
|
||||
|
||||
Тот же алгоритм, что и `square`, но для крестообразных элементов.
|
||||
Разница исключительно в семантике и визуальном контексте.
|
||||
|
||||
**Механика:**
|
||||
```
|
||||
// identical to square
|
||||
s = 1 + (1 - progress) * 0.6
|
||||
ctx.scale(s, s)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Easing функции
|
||||
|
||||
| Название | Формула |
|
||||
|---------------|---------|
|
||||
| `linear` | `t` |
|
||||
| `easeIn` | `t²` |
|
||||
| `easeOut` | `t(2-t)` |
|
||||
| `easeInOut` | `2t²` при t<0.5, `-1+(4-2t)t` при t≥0.5 |
|
||||
| `easeInCubic` | `t³` |
|
||||
| `easeOutCubic`| `1-(1-t)³` |
|
||||
| `easeInOutCubic` | `4t³` при t<0.5, `1-(-2t+2)³/2` при t≥0.5 |
|
||||
|
||||
---
|
||||
|
||||
## SVG Spec
|
||||
|
||||
Каждый SVG-паттерн должен соответствовать формату:
|
||||
|
||||
```xml
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none">
|
||||
<rect width="40" height="40" fill="white"/>
|
||||
<!-- foreground shapes -->
|
||||
<circle cx="20" cy="20" r="14" fill="black"/>
|
||||
<rect x="10" y="10" width="20" height="20" rx="4" fill="black" stroke="black"/>
|
||||
</svg>
|
||||
```
|
||||
|
||||
**Правила:**
|
||||
1. Размер: 40×40 px (viewBox 0 0 40 40)
|
||||
2. Фон: `<rect width="40" height="40" fill="white"/>` (переокрашивается в runtime)
|
||||
3.Foreground: `fill="black"` и/или `stroke="black"` (переокрашивается)
|
||||
4. Тег `<defs>` игнорируется при парсинге
|
||||
5. Вложенные `<g>` обрабатываются рекурсивно
|
||||
6. Анимация определяется типом анимации, не структурой SVG
|
||||
|
||||
**Поддерживаемые теги:** `<rect>`, `<circle>`, `<path>`
|
||||
|
||||
**Transform:** `rotate(angle, ox, oy)` — угол поворота и точка вращения.
|
||||
|
||||
---
|
||||
|
||||
## Референсы кода
|
||||
|
||||
- `src/engine/animations.ts` — 7 switch-касов для каждого animType
|
||||
- `src/engine/timeline.ts` — фаза titles → patterns → dots (цикл 4× duration)
|
||||
- `src/engine/easing.ts` — 7 easing функций
|
||||
- `src/core/patterns.ts` — парсинг SVG, colorizeSvg, svgToImage
|
||||
- `src/export/exporter.ts` — экспорт PNG-секвенций (titles, patterns, dots)
|
||||
- `src/types.ts` — PatternAnimType, SvgShapeInfo, PatternDef
|
||||
@@ -0,0 +1,229 @@
|
||||
---
|
||||
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
|
||||
@@ -0,0 +1,187 @@
|
||||
---
|
||||
name: motion/staggered-reveal
|
||||
title: Staggered Element Reveal
|
||||
description: Cascading entrance animations where elements appear sequentially. Used on Linear, Raycast, Linear.app hero sections.
|
||||
tags: [motion, gsap, scrolltrigger, entrance, stagger]
|
||||
source: linear.app, raycast.com, godly.website top 50
|
||||
---
|
||||
|
||||
# Staggered Element Reveal
|
||||
|
||||
## Что это
|
||||
Последовательное появление элементов с нарастающей задержкой. Создаёт ощущение "потока" и профессионализма. Элементы появляются не все сразу, а каскадом с интервалом.
|
||||
|
||||
## Где используется
|
||||
- Hero-секции Linear, Raycast, Resend
|
||||
- Feature-блоки с иконками
|
||||
- Списки преимуществ
|
||||
- Карточки с услугами
|
||||
|
||||
## Ключевые параметры
|
||||
|
||||
| Параметр | Значение | Почему |
|
||||
|----------|----------|--------|
|
||||
| `stagger` | `0.08–0.12` | Оптимальный ритм — не слишком быстро, не медленно |
|
||||
| `duration` | `0.6–0.8s` | Достаточное время для заметности |
|
||||
| `ease` | `power3.out` | Плавное замедление, ощущение "приземления" |
|
||||
| `delay` | `0.2–0.3` | Задержка перед началом каскада |
|
||||
| `from opacity` | `0.8` | Не полная прозрачность, чтобы не было "прыжка" |
|
||||
| `from translateY` | `20–30px` | Элементы "влетают" снизу |
|
||||
|
||||
## Референсы
|
||||
|
||||
### Linear Hero
|
||||
```
|
||||
1. Logo появляется → 0.3s задержка
|
||||
2. H1 появляется (line by line) → stagger 0.1s
|
||||
3. Подзаголовок → 0.4s задержка
|
||||
4. CTA кнопка → 0.6s задержка
|
||||
5. Декоративные элементы → 0.8s задержка
|
||||
```
|
||||
|
||||
### Resend Feature Grid
|
||||
```
|
||||
- Карточки появляются с задержкой 0.08s друг за другом
|
||||
- Каждая карточка: fade + slide up 15px
|
||||
- Hover на карточку: scale 1.02 + shadow
|
||||
```
|
||||
|
||||
## Реализация
|
||||
|
||||
```javascript
|
||||
// GSAP + ScrollTrigger
|
||||
import gsap from 'gsap';
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
// 1. Базовый stagger-ревеал
|
||||
gsap.from('.stagger-item', {
|
||||
opacity: 0,
|
||||
y: 30,
|
||||
duration: 0.7,
|
||||
stagger: 0.08,
|
||||
ease: 'power3.out',
|
||||
delay: 0.3,
|
||||
scrollTrigger: {
|
||||
trigger: '.stagger-section',
|
||||
start: 'top 80%',
|
||||
toggleActions: 'play none none none'
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Stagger для карточек
|
||||
gsap.from('.feature-card', {
|
||||
opacity: 0,
|
||||
y: 25,
|
||||
scale: 0.97,
|
||||
duration: 0.6,
|
||||
stagger: 0.06,
|
||||
ease: 'power2.out',
|
||||
scrollTrigger: {
|
||||
trigger: '.features-grid',
|
||||
start: 'top 85%'
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Stagger для текстовых строк
|
||||
gsap.from('.hero-line', {
|
||||
opacity: 0,
|
||||
y: 40,
|
||||
duration: 0.8,
|
||||
stagger: 0.12,
|
||||
ease: 'power3.out',
|
||||
delay: 0.2
|
||||
});
|
||||
|
||||
// 4. Line-by-line reveal для заголовка
|
||||
const lines = document.querySelectorAll('.hero h1 .line');
|
||||
gsap.from(lines, {
|
||||
opacity: 0,
|
||||
y: 50,
|
||||
rotateX: -15,
|
||||
duration: 0.8,
|
||||
stagger: 0.15,
|
||||
ease: 'power3.out',
|
||||
delay: 0.3
|
||||
});
|
||||
```
|
||||
|
||||
## CSS подготовка
|
||||
|
||||
```css
|
||||
.stagger-item {
|
||||
/* Начальное состояние до анимации */
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
/* Альтернатива: CSS-only для простых случаев */
|
||||
@keyframes fadeUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(25px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.stagger-item {
|
||||
opacity: 0;
|
||||
animation: fadeUp 0.6s ease forwards;
|
||||
}
|
||||
|
||||
.stagger-item:nth-child(1) { animation-delay: 0.1s; }
|
||||
.stagger-item:nth-child(2) { animation-delay: 0.18s; }
|
||||
.stagger-item:nth-child(3) { animation-delay: 0.26s; }
|
||||
.stagger-item:nth-child(4) { animation-delay: 0.34s; }
|
||||
.stagger-item:nth-child(5) { animation-delay: 0.42s; }
|
||||
.stagger-item:nth-child(6) { animation-delay: 0.5s; }
|
||||
```
|
||||
|
||||
## Anti-patterns (Чего НЕ делать)
|
||||
|
||||
| ❌ Плохо | ✅ Хорошо |
|
||||
|----------|-----------|
|
||||
| `stagger: 0.02` — слишком быстро, "мигание" | `stagger: 0.08` — чёткий ритм |
|
||||
| `stagger: 0.3` — слишком медленно, скучно | `stagger: 0.06–0.12` — оптимально |
|
||||
| Все элементы `opacity: 0 → 1` | `opacity: 0.8 → 1` + `y: 20 → 0` |
|
||||
| Без `will-change` | `will-change: opacity, transform` |
|
||||
| Без `stagger` (все сразу) | Каскадное появление |
|
||||
|
||||
## Performance
|
||||
|
||||
```javascript
|
||||
// ✅ GPU accelerated
|
||||
gsap.from('.item', { opacity: 0, y: 20, transform: 'translateY(20px)' });
|
||||
|
||||
// ❌ Bad — заставляет layout
|
||||
gsap.from('.item', { marginLeft: '20px' });
|
||||
gsap.from('.item', { top: '20px', left: '20px' });
|
||||
```
|
||||
|
||||
## Validation (CV Service)
|
||||
```bash
|
||||
# После рендеринга страницы
|
||||
curl -s -X POST http://localhost:8001/design/analyze-dom \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"html": "...", "full_analysis": false}' | python3 -m json.tool
|
||||
```
|
||||
|
||||
Проверить:
|
||||
- `performance` score > 8 — анимации только на `transform` и `opacity`
|
||||
- `ux_heuristics` score > 6 — анимации не мешают навигации
|
||||
- `wcag_compliance` — анимация имеет `prefers-reduced-motion` фоллбэк
|
||||
|
||||
```css
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.stagger-item {
|
||||
opacity: 1 !important;
|
||||
transform: none !important;
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in new issue
Block a user