attention-choreography
با زمانبندی هوشمندانه، مسیر نگاه کاربر را در انیمیشن هدایت کنید.
این Skill ترتیب ورود و جابهجایی توجه بین عناصر رابط کاربری را بر اساس الگوهای F-pattern، Z-pattern، center-out، Gutenberg و layer-depth طراحی میکند. با تعریف Focus Anchorها، اهمیت بصری هر عنصر به زمانبندی و شدت انیمیشن تبدیل میشود.
برای landing page، hero section، فرمها، modalها و محتوای متنی مناسب است و نمونههای اجرایی برای GSAP و Framer Motion ارائه میدهد. پشتیبانی از RTL و prefers-reduced-motion کمک میکند تجربهای قابلفهمتر و دسترسپذیرتر بسازید.
چه چیزی میگیرید
- انتخاب الگوی مناسب حرکت بر اساس نوع محتوا و چیدمان
- تعریف اولویتهای primary، secondary، tertiary و ambient برای عناصر
- زمانبندی stagger و مسیرهای توجه با GSAP یا Framer Motion
- الگوهای انتقال تمرکز مانند glide shift و bridge shift
- راهکارهای RTL و حالت `prefers-reduced-motion`
برای چه کاری خوب است
- طراحان و توسعهدهندگانی که برای landing page یا hero section موشن طراحی میکنند
- پروژههایی با محتوای متنی، کارتها، فرمها یا modalهای چندعنصری
- تیمهایی که میخواهند hierarchy بصری و ترتیب نمایش عناصر را بهصورت سیستماتیک تنظیم کنند
برای چه کاری نیست
- ساخت انیمیشنهای مستقل بدون نیاز به ترتیبدهی توجه یا سلسلهمراتب بصری
- پروژههایی که به طراحی کامل visual identity یا تولید assetهای گرافیکی نیاز دارند
- صفحههایی که تمام عناصر باید همزمان و بدون حرکت ترتیبی نمایش داده شوند
دربارهٔ این اسکیل
Attention Choreography
Guide visual attention through animation sequencing. Align animation order with natural reading patterns and visual hierarchy to create intuitive, scannable motion.
Standards Reference
| Standard | Documentation |
|---|---|
| W3C DTCG | designtokens.org |
| Nielsen Norman Group | F-pattern, Z-pattern research |
| Material Design 3 | m3.material.io/styles/motion |
| Gutenberg Diagram | Visual gravity principles |
Quick Reference
Load from: tokens/attention-path.tokens.json
Reading Patterns
| Pattern | Primary Axis | Best For | Stagger |
|---|---|---|---|
| F-pattern | Horizontal | Text-heavy, articles | 60ms |
| Z-pattern | Diagonal | Landing pages, sparse | 80ms |
| center-out | Radial | Hero sections, modals | 50ms |
| gutenberg | Diagonal | Forms, cards | 70ms |
| layer-depth | Z-axis | Parallax, 3D scenes | 100ms |
Focus Anchors
| Level | Attention Weight | Animation Priority |
|---|---|---|
| primary | 1.0 | 1 (first) |
| secondary | 0.6 | 2 |
| tertiary | 0.3 | 3 |
| ambient | 0.1 | 4 (last) |
Selection Guide
What's the content type?
├── Text-heavy content (articles, docs)
│ └── F-PATTERN
│ └── Animate top-left → top-right → down-left
│
├── Sparse, marketing-focused (landing)
│ └── Z-PATTERN
│ └── Animate top-left → top-right → bottom-left → CTA
│
├── Focal point content (hero, modal)
│ └── CENTER-OUT
│ └── Animate center → inner ring → outer ring
│
├── Balanced layout (forms, cards)
│ └── GUTENBERG
│ └── Primary optical → strong fallow → terminal
│
└── Layered/3D content
└── LAYER-DEPTH
└── Foreground → midground → background
Pattern Implementations
F-Pattern
For text-heavy content with strong horizontal bias.
// F-pattern: top-left emphasis, decreasing attention downward
const fPatternConfig = {
zones: {
'top-left': { priority: 1, stagger: 0 },
'top-right': { priority: 2, stagger: 60 },
'mid-left': { priority: 3, stagger: 120 },
'mid-right': { priority: 4, stagger: 180 },
'bottom-left': { priority: 5, stagger: 240 }
},
attentionDecay: 0.15
};
function applyFPattern(elements) {
const zones = categorizeByPosition(elements);
Object.entries(fPatternConfig.zones).forEach(([zone, config]) => {
const zoneElements = zones[zone] || [];
gsap.from(zoneElements, {
opacity: 0,
y: 20,
duration: 0.3,
delay: config.stagger / 1000,
stagger: 0.03,
ease: 'power2.out'
});
});
}
function categorizeByPosition(elements) {
const zones = {};
const container = elements[0].parentElement.getBoundingClientRect();
elements.forEach(el => {
const rect = el.getBoundingClientRect();
const relX = (rect.left - container.left) / container.width;
const relY = (rect.top - container.top) / container.height;
let zone;
if (relY < 0.33) {
zone = relX < 0.5 ? 'top-left' : 'top-right';
} else if (relY < 0.66) {
zone = relX < 0.5 ? 'mid-left' : 'mid-right';
} else {
zone = 'bottom-left';
}
if (!zones[zone]) zones[zone] = [];
zones[zone].push(el);
});
return zones;
}
Framer Motion:
const fPatternVariants = {
hidden: {},
visible: {
transition: {
staggerChildren: 0.06,
staggerDirection: 1
}
}
};
// Assign data-priority based on position
<motion.div variants={fPatternVariants}>
<motion.h1 data-priority="1" variants={itemVariants}>Headline</motion.h1>
<motion.nav data-priority="2" variants={itemVariants}>Navigation</motion.nav>
<motion.p data-priority="3" variants={itemVariants}>Content</motion.p>
</motion.div>
Z-Pattern
For landing pages and sparse layouts.
const zPatternConfig = {
path: ['top-left', 'top-right', 'center', 'bottom-left', 'bottom-right'],
emphasisZones: ['top-left', 'bottom-right'], // Logo and CTA
staggerBase: 80
};
function applyZPattern(elements) {
const zones = categorizeForZ(elements);
let cumulativeDelay = 0;
zPatternConfig.path.forEach((zone, index) => {
const zoneElements = zones[zone] || [];
const isEmphasis = zPatternConfig.emphasisZones.includes(zone);
gsap.from(zoneElements, {
opacity: 0,
scale: isEmphasis ? 0.9 : 0.95,
y: isEmphasis ? 30 : 15,
duration: isEmphasis ? 0.4 : 0.3,
delay: cumulativeDelay / 1000,
ease: isEmphasis ? 'back.out(1.5)' : 'power2.out'
});
cumulativeDelay += zPatternConfig.staggerBase;
});
}
Center-Out
For hero sections and modals with central focus.
const centerOutConfig = {
rings: ['center', 'inner-ring', 'outer-ring', 'edges'],
staggerPerRing: 50,
intensityDecay: 0.2
};
function applyCenterOut(elements, containerCenter) {
const rings = categorizeByDistance(elements, containerCenter);
rings.forEach((ring, ringIndex) => {
const intensity = 1 - (ringIndex * centerOutConfig.intensityDecay);
gsap.from(ring, {
opacity: 0,
scale: 0.8 + (0.2 * (1 - intensity)),
duration: 0.3 + (0.1 * intensity),
delay: ringIndex * centerOutConfig.staggerPerRing / 1000,
ease: 'power2.out'
});
});
}
function categorizeByDistance(elements, center) {
const distances = elements.map(el => {
const rect = el.getBoundingClientRect();
const elCenter = {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2
};
return {
element: el,
distance: Math.hypot(elCenter.x - center.x, elCenter.y - center.y)
};
});
// Sort by distance and group into rings
distances.sort((a, b) => a.distance - b.distance);
const ringSize = Math.ceil(elements.length / 4);
const rings = [];
for (let i = 0; i < 4; i++) {
rings.push(
distances
.slice(i * ringSize, (i + 1) * ringSize)
.map(d => d.element)
);
}
return rings;
}
Framer Motion:
function CenterOutContainer({ children }) {
const containerRef = useRef(null);
return (
<motion.div
ref={containerRef}
variants={{
hidden: {},
visible: {
transition: {
staggerChildren: 0.05,
staggerDirection: 1
}
}
}}
>
{React.Children.map(children, (child, index) => {
// Calculate ring based on distance from center
const ring = Math.floor(index / 3); // Simplified
return (
<motion.div
variants={{
hidden: { opacity: 0, scale: 0.8 },
visible: {
opacity: 1,
scale: 1,
transition: { delay: ring * 0.05 }
}
}}
>
{child}
</motion.div>
);
})}
</motion.div>
);
}
Gutenberg Diagram
For balanced layouts following natural visual flow.
const gutenbergConfig = {
zones: {
'primary-optical': { position: 'top-left', weight: 1.0 },
'strong-fallow': { position: 'top-right', weight: 0.6 },
'weak-fallow': { position: 'bottom-left', weight: 0.3 },
'terminal-area': { position: 'bottom-right', weight: 0.9 }
},
readingGravity: 'diagonal'
};
function applyGutenberg(elements) {
const zones = categorizeGutenberg(elements);
// Primary optical area first
gsap.from(zones['primary-optical'], {
opacity: 0,
y: 20,
duration: 0.4,
ease: 'power2.out'
});
// Strong fallow (often navigation)
gsap.from(zones['strong-fallow'], {
opacity: 0,
y: 15,
duration: 0.3,
delay: 0.08,
ease: 'power2.out'
});
// Terminal area (CTA)
gsap.from(zones['terminal-area'], {
opacity: 0,
scale: 0.95,
duration: 0.35,
delay: 0.15,
ease: 'back.out(1.5)'
});
// Weak fallow last
gsap.from(zones['weak-fallow'], {
opacity: 0,
duration: 0.25,
delay: 0.2,
ease: 'power2.out'
});
}
Focus Anchors
Establish visual hierarchy with explicit priority.
interface FocusAnchor {
element: HTMLElement;
level: 'primary' | 'secondary' | 'tertiary' | 'ambient';
attentionWeight: number;
entrancePriority: number;
}
const focusLevels = {
primary: { weight: 1.0, priority: 1, intensity: 0.8 },
secondary: { weight: 0.6, priority: 2, intensity: 0.6 },
tertiary: { weight: 0.3, priority: 3, intensity: 0.4 },
ambient: { weight: 0.1, priority: 4, intensity: 0.2 }
};
function animateByHierarchy(anchors: FocusAnchor[]) {
// Sort by priority
anchors.sort((a, b) => a.entrancePriority - b.entrancePriority);
let cumulativeDelay = 0;
anchors.forEach(anchor => {
const config = focusLevels[anchor.level];
gsap.from(anchor.element, {
opacity: 0,
y: 20 * config.intensity,
scale: 1 - (0.1 * config.intensity),
duration: 0.3,
delay: cumulativeDelay,
ease: 'power2.out'
});
cumulativeDelay += 0.08;
});
}
Attention Shifts
Transitioning focus between elements.
Glide Shift
Smooth attention transfer.
function glideShift(from, to) {
const tl = gsap.timeline();
// Defocus current
tl.to(from, {
opacity: 0.6,
scale: 0.98,
duration: 0.15
});
// Focus new
tl.to(to, {
opacity: 1,
scale: 1,
boxShadow: '0 0 0 3px rgba(33, 150, 243, 0.3)',
duration: 0.2
}, '-=0.1');
return tl;
}
Bridge Shift
Connected path between focus points.
function bridgeShift(from, to) {
const fromRect = from.getBoundingClientRect();
const toRect = to.getBoundingClientRect();
// Create visual connector
const connector = createConnector(fromRect, toRect);
const tl = gsap.timeline();
// Draw connector
tl.from(connector, {
strokeDashoffset: connector.getTotalLength(),
duration: 0.3,
ease: 'power2.inOut'
});
// Shift focus
tl.to(from, { opacity: 0.5, duration: 0.15 }, '-=0.15');
tl.to(to, { opacity: 1, scale: 1.02, duration: 0.2 });
// Fade connector
tl.to(connector, { opacity: 0, duration: 0.2 });
return tl;
}
Distraction Management
Reduce competing attention during focus moments.
function suppressSecondary(primarySelector) {
// Dim non-primary elements
gsap.to('*:not(' + primarySelector + ')', {
opacity: 0.6,
filter: 'blur(1px)',
duration: 0.2,
ease: 'power2.out'
});
// Enhance primary
gsap.to(primarySelector, {
opacity: 1,
filter: 'blur(0)',
boxShadow: '0 4px 20px rgba(0,0,0,0.15)',
duration: 0.3
});
}
function restoreAll() {
gsap.to('*', {
opacity: 1,
filter: 'blur(0)',
boxShadow: 'none',
duration: 0.3
});
}
Cultural Adaptations
RTL (Right-to-Left) Languages
function getPatternForDirection(pattern, direction) {
if (direction === 'rtl') {
const mirrorMap = {
'f-pattern': {
'top-left': 'top-right',
'top-right': 'top-left',
'mid-left': 'mid-right',
'mid-right': 'mid-left'
},
'z-pattern': {
'top-left': 'top-right',
'top-right': 'top-left',
'bottom-left': 'bottom-right',
'bottom-right': 'bottom-left'
}
};
return mirrorMap[pattern] || pattern;
}
return pattern;
}
Reduced Motion
For prefers-reduced-motion: reduce:
| Full Choreography | Reduced |
|---|---|
| Staggered by pattern | Simultaneous fade |
| Attention shifts | Instant focus change |
| Bridge connectors | Skip |
| Hierarchy animation | Uniform timing |
const prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches;
if (prefersReducedMotion) {
// Skip choreography, simple fade
gsap.from(elements, {
opacity: 0,
duration: 0.2,
stagger: 0 // No stagger
});
}
Cross-References
- See
sequencing/for stagger patterns - See
narrative-arc/for story-aligned timing - See
dramaturgy/for scene composition - See
entrance-patterns/for element entrance animations
References
references/f-pattern.md— F-pattern implementation detailsreferences/z-pattern.md— Z-pattern implementation details