playback-control

کنترل سرعت، جهت، مکث، اسکراب و تکرار انیمیشن‌ها با الگوهای آماده

این skill برای ساخت کنترل‌های تعاملی پخش انیمیشن طراحی شده است؛ از play و pause و تغییر سرعت تا reverse، alternate، looping، seeking و scrubbing دقیق. توکن‌های آماده برای سرعت، جهت، تعداد تکرار و اندازهٔ گام، پیاده‌سازی را منسجم‌تر می‌کنند.

با نمونه‌های کاربردی برای Web Animations API، GSAP، Framer Motion و Remotion، می‌توانید کنترل timeline، اسلایدر پیشرفت، slow motion و مدیریت state را سریع‌تر بسازید. پشتیبانی از prefers-reduced-motion نیز به تجربه‌ای دسترس‌پذیرتر کمک می‌کند.

چه چیزی می‌گیرید

  • توکن‌های آماده برای سرعت‌های paused، slow-motion، normal، double و rapid
  • الگوهای کنترل جهت، reverse، alternate و تعداد تکرار انیمیشن
  • نمونهٔ پیاده‌سازی scrubbing با mouse، touch، scroll و slider
  • روش‌های seeking، حرکت فریم‌به‌فریم و پرش به زمان یا label
  • نمونه‌های مدیریت state و تغییر نرم سرعت در Web Animations API، GSAP، Framer Motion و Remotion

برای چه کاری خوب است

  • توسعه‌دهندگان رابط‌های تعاملی با کنترل play، pause، سرعت و progress
  • ساخت timelineهای قابل اسکراب برای انیمیشن‌های GSAP یا ویدیوهای Remotion
  • پروژه‌هایی که به reverse، ping-pong، slow motion یا loopهای قابل تنظیم نیاز دارند
  • طراحی کنترل‌های دسترسی‌پذیر با پشتیبانی از prefers-reduced-motion

برای چه کاری نیست

  • وقتی فقط به تعریف keyframe یا easing ساده و بدون کنترل تعاملی نیاز دارید
  • پروژه‌هایی که مدیریت کامل timeline یا پخش ویدیو خارج از scope انیمیشن رابط کاربری است
  • وقتی انیمیشن ثابت است و نیازی به تغییر سرعت، جهت، زمان یا تکرار ندارد

دربارهٔ این اسکیل

Playback Control

Interactive control over animation playback.

Standards Reference

StandardDocumentation
W3C DTCGdesigntokens.org
Web Animations APIdeveloper.mozilla.org/Web/API/Web_Animations_API
CSS animation-*developer.mozilla.org/CSS/animation
GSAP Timelinegsap.com/docs/v3/GSAP/Timeline

Quick Reference

Load from: tokens/playback.tokens.json

Speed Multipliers

TokenValueUse
speed.paused0Frozen
speed.slow-motion0.25Dramatic slow-mo
speed.half0.5Gentle slow
speed.normal1Standard
speed.double2Fast forward
speed.rapid4Very fast

Direction Values

TokenValueUse
direction.forward1Normal play
direction.reverse-1Backwards
direction.alternate"alternate"Ping-pong

Iteration Counts

TokenValueUse
iteration.once1Play once
iteration.twice2Play twice
iteration.infinite-1Loop forever

Selection Guide

What control is needed?
│
├── SPEED CONTROL
│   ├── Slow motion → speed.slow-motion (0.25x)
│   ├── Normal → speed.normal (1x)
│   ├── Fast forward → speed.double (2x)
│   └── Pause → speed.paused (0)
│
├── DIRECTION CONTROL
│   ├── Forward → direction.forward
│   ├── Reverse → direction.reverse
│   └── Ping-pong → direction.alternate
│
├── LOOPING
│   ├── Once → iteration.once
│   ├── Fixed count → iteration.twice/thrice
│   └── Infinite → iteration.infinite
│
├── SCRUBBING
│   ├── Precise → scrub.sensitivity.low
│   ├── Normal → scrub.sensitivity.normal
│   └── Fast → scrub.sensitivity.high
│
└── SEEKING
    ├── Frame step → seek.step-frame
    ├── Small step → seek.step-small (0.1s)
    └── Large step → seek.step-large (1s)

Patterns

Speed Control

Dynamically adjust animation playback speed.

Web Animations API:

// Create animation
const animation = element.animate(
  [
    { transform: 'translateX(0)' },
    { transform: 'translateX(300px)' }
  ],
  { duration: 1000, fill: 'forwards' }
);

// Speed control functions
function setSpeed(rate) {
  animation.playbackRate = rate; // {motion.playback.speed.*}
}

function slowMotion() {
  setSpeed(0.25); // {motion.playback.speed.slow-motion}
}

function normalSpeed() {
  setSpeed(1); // {motion.playback.speed.normal}
}

function fastForward() {
  setSpeed(2); // {motion.playback.speed.double}
}

function pause() {
  animation.pause();
}

function play() {
  animation.play();
}

GSAP:

const tl = gsap.timeline();

tl.to(element, {
  x: 300,
  duration: 1,
  ease: 'power2.out'
});

// Speed control
tl.timeScale(0.25); // {motion.playback.speed.slow-motion}
tl.timeScale(1);    // {motion.playback.speed.normal}
tl.timeScale(2);    // {motion.playback.speed.double}

// Pause/resume
tl.pause();
tl.resume();
tl.play();

Framer Motion:

import { motion, useAnimationControls } from 'framer-motion';
import { useState } from 'react';

const PlaybackControl = () => {
  const controls = useAnimationControls();
  const [speed, setSpeed] = useState(1);

  const startAnimation = async () => {
    await controls.start({
      x: 300,
      transition: {
        duration: 1 / speed, // Adjust duration for speed
        ease: [0.2, 0, 0, 1]
      }
    });
  };

  return (
    <>
      <motion.div animate={controls} />
      <button onClick={() => setSpeed(0.25)}>Slow Motion</button>
      <button onClick={() => setSpeed(1)}>Normal</button>
      <button onClick={() => setSpeed(2)}>Fast</button>
      <button onClick={startAnimation}>Play</button>
    </>
  );
};

Direction Control

Reverse and alternate animation direction.

Web Animations API:

const animation = element.animate(keyframes, {
  duration: 1000,
  iterations: Infinity,
  direction: 'alternate' // {motion.playback.direction.alternate}
});

// Reverse current animation
function reverse() {
  animation.reverse();
}

// Set direction explicitly
function setDirection(dir) {
  // Recreate with new direction
  animation.effect.updateTiming({ direction: dir });
}

CSS:

/* Alternate direction */
.ping-pong {
  animation: slide 1s ease-in-out infinite alternate;
  /* {motion.playback.direction.alternate} */
}

/* Reverse animation */
.reverse {
  animation-direction: reverse;
  /* {motion.playback.direction.reverse} */
}

@keyframes slide {
  from { transform: translateX(0); }
  to { transform: translateX(300px); }
}

GSAP:

const tl = gsap.timeline({ yoyo: true, repeat: -1 });
// yoyo = alternate direction

// Reverse the timeline
tl.reverse();

// Play forward
tl.play();

// Toggle direction
function toggleDirection() {
  tl.reversed(!tl.reversed());
}

Interactive Scrubbing

Scrub animation based on user input.

GSAP + Scroll:

const tl = gsap.timeline({
  scrollTrigger: {
    trigger: '.container',
    start: 'top top',
    end: 'bottom bottom',
    scrub: true, // Smooth scrubbing
    // scrub: 0.5 for slight delay
  }
});

tl.to('.element', {
  x: 300,
  rotation: 360,
  duration: 1
});

GSAP + Mouse/Touch:

const tl = gsap.timeline({ paused: true });

tl.to('.element', {
  x: 300,
  duration: 1
});

// Scrub with horizontal mouse position
document.addEventListener('mousemove', (e) => {
  const progress = e.clientX / window.innerWidth;
  tl.progress(progress);
});

// Scrub with slider
const slider = document.querySelector('#scrubber');
slider.addEventListener('input', (e) => {
  const progress = e.target.value / 100;
  tl.progress(progress);
});

Web Animations API:

const animation = element.animate(keyframes, {
  duration: 1000,
  fill: 'both'
});

animation.pause();

// Scrub with slider
slider.addEventListener('input', (e) => {
  const progress = e.target.value / 100;
  animation.currentTime = progress * animation.effect.getTiming().duration;
});

Framer Motion + Scroll:

import { motion, useScroll, useTransform } from 'framer-motion';

const ScrollScrub = () => {
  const { scrollYProgress } = useScroll();

  const x = useTransform(scrollYProgress, [0, 1], [0, 300]);
  const rotate = useTransform(scrollYProgress, [0, 1], [0, 360]);

  return (
    <motion.div style={{ x, rotate }} />
  );
};

Frame Stepping

Step through animation frame by frame.

Web Animations API:

const animation = element.animate(keyframes, {
  duration: 1000,
  fill: 'both'
});

animation.pause();

const frameStep = 33; // {motion.playback.seek.step-frame} ~30fps

function stepForward() {
  animation.currentTime = Math.min(
    animation.currentTime + frameStep,
    animation.effect.getTiming().duration
  );
}

function stepBackward() {
  animation.currentTime = Math.max(
    animation.currentTime - frameStep,
    0
  );
}

// Seek to specific percentage
function seekTo(percent) {
  animation.currentTime = (percent / 100) * animation.effect.getTiming().duration;
}

GSAP:

const tl = gsap.timeline({ paused: true });

function stepForward() {
  const stepSize = 0.033; // {motion.playback.seek.step-frame}
  tl.progress(Math.min(tl.progress() + stepSize / tl.duration(), 1));
}

function stepBackward() {
  const stepSize = 0.033;
  tl.progress(Math.max(tl.progress() - stepSize / tl.duration(), 0));
}

// Seek to time
function seekToTime(seconds) {
  tl.seek(seconds);
}

// Seek to label
function seekToLabel(label) {
  tl.seek(label);
}

Playback State Management

Track and manage animation state.

JavaScript:

class AnimationController {
  constructor(animation) {
    this.animation = animation;
    this.state = 'idle'; // {motion.playback.state.idle}
    this.speed = 1;
    this.direction = 1;

    this.animation.onfinish = () => {
      this.state = 'finished'; // {motion.playback.state.finished}
      this.onStateChange?.(this.state);
    };
  }

  play() {
    this.animation.play();
    this.state = 'running'; // {motion.playback.state.running}
    this.onStateChange?.(this.state);
  }

  pause() {
    this.animation.pause();
    this.state = 'paused'; // {motion.playback.state.paused}
    this.onStateChange?.(this.state);
  }

  toggle() {
    if (this.state === 'running') {
      this.pause();
    } else {
      this.play();
    }
  }

  setSpeed(rate) {
    this.speed = rate;
    this.animation.playbackRate = rate * this.direction;
  }

  reverse() {
    this.direction *= -1;
    this.animation.playbackRate = this.speed * this.direction;
  }

  reset() {
    this.animation.currentTime = this.direction > 0 ? 0 : this.animation.effect.getTiming().duration;
    this.state = 'idle';
    this.onStateChange?.(this.state);
  }
}

React Hook:

import { useCallback, useRef, useState } from 'react';

type PlaybackState = 'idle' | 'running' | 'paused' | 'finished';

function usePlaybackControl() {
  const animationRef = useRef<Animation | null>(null);
  const [state, setState] = useState<PlaybackState>('idle');
  const [speed, setSpeedState] = useState(1);

  const play = useCallback(() => {
    animationRef.current?.play();
    setState('running');
  }, []);

  const pause = useCallback(() => {
    animationRef.current?.pause();
    setState('paused');
  }, []);

  const setSpeed = useCallback((rate: number) => {
    if (animationRef.current) {
      animationRef.current.playbackRate = rate;
    }
    setSpeedState(rate);
  }, []);

  const seek = useCallback((progress: number) => {
    if (animationRef.current) {
      const duration = animationRef.current.effect?.getTiming().duration as number;
      animationRef.current.currentTime = progress * duration;
    }
  }, []);

  return {
    animationRef,
    state,
    speed,
    play,
    pause,
    setSpeed,
    seek
  };
}

Smooth Speed Transitions

Gradually change speed instead of instant jumps.

JavaScript:

function smoothSpeedChange(animation, targetSpeed, duration = 200) {
  // {motion.playback.transition-speed.smooth}
  const startSpeed = animation.playbackRate;
  const startTime = performance.now();

  function update(currentTime) {
    const elapsed = currentTime - startTime;
    const progress = Math.min(elapsed / duration, 1);

    // Ease-out interpolation
    const eased = 1 - Math.pow(1 - progress, 3);
    animation.playbackRate = startSpeed + (targetSpeed - startSpeed) * eased;

    if (progress < 1) {
      requestAnimationFrame(update);
    }
  }

  requestAnimationFrame(update);
}

// Usage
smoothSpeedChange(animation, 0.25); // Smooth transition to slow-mo

Remotion Implementation

import { useCurrentFrame, interpolate, Sequence } from 'remotion';

// Playback speed is controlled via frame mapping
export const VariableSpeed: React.FC<{ speedCurve: number[] }> = ({ speedCurve }) => {
  const frame = useCurrentFrame();

  // Map frame to adjusted time based on speed curve
  // speedCurve: [1, 1, 0.5, 0.5, 2, 2] means:
  // normal, normal, half-speed, half-speed, double, double

  let adjustedTime = 0;
  for (let i = 0; i < Math.min(frame, speedCurve.length); i++) {
    adjustedTime += speedCurve[i] || 1;
  }

  // Use adjustedTime for animation calculations
  const x = interpolate(adjustedTime, [0, 100], [0, 300]);

  return (
    <div style={{ transform: `translateX(${x}px)` }} />
  );
};

// Scrubbing in Remotion Preview
// Use the timeline scrubber in the Remotion Studio

Reduced Motion

For prefers-reduced-motion: reduce:

Full MotionReduced Alternative
Speed variationsSingle normal speed only
Slow motionInstant or fast transition
LoopingPlay once
ScrubbingJump to positions

Implementation:

const prefersReducedMotion = window.matchMedia(
  '(prefers-reduced-motion: reduce)'
).matches;

function getPlaybackConfig() {
  if (prefersReducedMotion) {
    return {
      speed: 10, // {motion.playback.speed.instant} - near instant
      iterations: 1, // {motion.playback.iteration.once}
      direction: 'normal'
    };
  }
  return {
    speed: 1,
    iterations: Infinity,
    direction: 'alternate'
  };
}

Cross-References

  • See looping/ for loop configuration details
  • See sequencing/ for timeline orchestration
  • See duration/ for timing values
  • See scroll-animations/ for scroll-based scrubbing
  • See gesture-motion/ for touch-based control
  • See lottie/ for Lottie player control
  • See remotion/ for video timeline control

References

  • No additional references needed for this skill

دانلود ZIP