agentleFS
soilmasssoilmass/motion-design-agent/skills/attention-choreography/SKILL.md

attention-choreography

Skill7 starsChanged 10 months agoMITView on GitHub

Reading order and gaze path choreography for animation. Use when sequencing element entrances by visual importance - F-pattern for text content, Z-pattern for landing pages, center-out for hero sections, or custom focus anchor hierarchies.

The file

Files
SKILL.md

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

StandardDocumentation
W3C DTCGdesigntokens.org
Nielsen Norman GroupF-pattern, Z-pattern research
Material Design 3m3.material.io/styles/motion
Gutenberg DiagramVisual gravity principles

Quick Reference

Load from: tokens/attention-path.tokens.json

Reading Patterns

PatternPrimary AxisBest ForStagger
F-patternHorizontalText-heavy, articles60ms
Z-patternDiagonalLanding pages, sparse80ms
center-outRadialHero sections, modals50ms
gutenbergDiagonalForms, cards70ms
layer-depthZ-axisParallax, 3D scenes100ms

Focus Anchors

LevelAttention WeightAnimation Priority
primary1.01 (first)
secondary0.62
tertiary0.33
ambient0.14 (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 ChoreographyReduced
Staggered by patternSimultaneous fade
Attention shiftsInstant focus change
Bridge connectorsSkip
Hierarchy animationUniform 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 details
  • references/z-pattern.md — Z-pattern implementation details

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.