looping

الگوهای آماده برای ساخت انیمیشن‌های حلقه‌ای، پینگ‌پنگی و حرکت‌های ظریف

این Skill برای طراحی انیمیشن‌های تکرارشونده و seamless مناسب است؛ از spinner و loading گرفته تا breathing، heartbeat، idle motion و cinemagraph. مدت‌زمان، حالت loop، دامنه حرکت و offset بین عناصر را بر اساس کاربرد انتخاب می‌کند.

نمونه‌های آماده برای CSS، Framer Motion، GSAP و JavaScript ارائه می‌دهد و اصول دسترس‌پذیری مانند prefers-reduced-motion را هم در نظر می‌گیرد. برای ساخت حرکت‌های روان، قابل‌کنترل و بدون پرش در UI و ویدیو کاربردی است.

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

  • الگوهای loop معمولی، ping-pong، oscillation و phase-offset
  • پیشنهاد duration از ۱ ثانیه تا ۳۰ ثانیه برای کاربردهای مختلف
  • نمونه‌کدهای آماده برای CSS، Framer Motion، GSAP و JavaScript
  • الگوهای heartbeat، breathing، idle animation و cinemagraph
  • راهنمای ساخت seamless loop و پشتیبانی از reduced motion

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

  • توسعه‌دهندگان رابط کاربری که به spinner، pulse یا حرکت‌های ambient نیاز دارند
  • طراحان و سازندگان motion system برای تعریف زمان‌بندی و رفتار loopها
  • پروژه‌هایی با انیمیشن‌های idle، پس‌زمینه‌های زنده یا cinemagraphهای ظریف

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

  • انیمیشن‌های روایی یا چندمرحله‌ای که به timeline پیچیده و کنترل دستی نیاز دارند
  • ساخت ویدیوهای کامل و تدوین حرفه‌ای با ابزارهایی مانند After Effects یا FFmpeg
  • حرکت‌های تعاملی وابسته به gesture یا physics که خارج از الگوهای loop هستند

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

Looping

Continuous and repeating animation patterns.

Standards Reference

StandardDocumentation
W3C DTCGdesigntokens.org
CSS Animationsdeveloper.mozilla.org/CSS/animation-iteration-count
GSAPgsap.com/docs/v3/GSAP/Tween/repeat

Quick Reference

Load from: tokens/looping.tokens.json

Loop Durations

TokenValueUse
fast1sActive spinners
medium2sStandard loading
normal3sBreathing pace
slow5sRelaxed ambient
ambient8sBackground motion
ultra-slow30sCinemagraph subtlety

Loop Modes

TokenCSS ValueEffect
normalnormalRestart from beginning
alternatealternatePing-pong (forward/back)
reversereversePlay backwards

Selection Guide

What type of loop?
│
├── LOADING/SPINNER → Continuous rotation or pulse
│   ├── Duration → fast (1s) to medium (2s)
│   └── Mode → normal (continuous)
│
├── BREATHING/PULSE → Oscillating property
│   ├── Duration → normal (3s) to slow (5s)
│   ├── Mode → alternate (ping-pong)
│   └── Amplitude → gentle (0.05) to noticeable (0.1)
│
├── AMBIENT/BACKGROUND → Subtle continuous motion
│   ├── Duration → slow (5s) to ambient (8s)
│   └── Mode → alternate
│
├── IDLE ANIMATION → Character/object at rest
│   ├── Duration → normal (3s)
│   ├── Delay → 3s before starting
│   └── Intensity → subtle (0.3)
│
└── CINEMAGRAPH → Near-static with subtle loop
    ├── Duration → ultra-slow (30s)
    └── Use seamless crossfade for video

Patterns

Basic Infinite Loop

Simple continuous animation.

CSS:

@keyframes spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

.spinner {
  animation: spin 1s linear infinite; /* {motion.looping.duration.fast} */
}

/* With count limit */
.spin-few {
  animation: spin 1s linear 3; /* {motion.looping.count.few} */
}

Framer Motion:

<motion.div
  animate={{ rotate: 360 }}
  transition={{
    duration: 1, // {motion.looping.duration.fast}
    repeat: Infinity,
    ease: "linear"
  }}
/>

GSAP:

gsap.to(element, {
  rotation: 360,
  duration: 1,
  repeat: -1, // {motion.looping.count.infinite}
  ease: "none"
});

Ping-Pong (Alternate) Loop

Animation plays forward then backward.

CSS:

@keyframes breathe {
  from { transform: scale(1); }
  to { transform: scale(1.05); }
}

.breathing {
  animation: breathe 3s ease-in-out infinite alternate;
  /* {motion.looping.duration.normal} + {motion.looping.mode.alternate} */
}

Framer Motion:

<motion.div
  animate={{ scale: [1, 1.05, 1] }}
  transition={{
    duration: 3, // {motion.looping.duration.normal}
    repeat: Infinity,
    ease: "easeInOut"
  }}
/>

GSAP:

gsap.to(element, {
  scale: 1.05,
  duration: 1.5, // Half of total cycle
  repeat: -1,
  yoyo: true, // Ping-pong mode
  ease: "sine.inOut"
});

Oscillation (Sine Wave)

Smooth wave-based animation.

CSS:

@keyframes oscillate {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-10px); }
}

.floating {
  animation: oscillate 3s ease-in-out infinite;
}

JavaScript (manual sine wave):

function oscillate(element, amplitude, frequency) {
  let startTime = null;

  function animate(timestamp) {
    if (!startTime) startTime = timestamp;
    const elapsed = (timestamp - startTime) / 1000;

    const value = Math.sin(elapsed * frequency * Math.PI * 2) * amplitude;
    element.style.transform = `translateY(${value}px)`;

    requestAnimationFrame(animate);
  }

  requestAnimationFrame(animate);
}

// Usage: oscillate(element, 10, 0.5) // ±10px, 0.5Hz (2s period)

GSAP:

gsap.to(element, {
  y: 10,
  duration: 1.5,
  repeat: -1,
  yoyo: true,
  ease: "sine.inOut"
});

Heartbeat Effect

Double-pulse rhythm.

CSS:

@keyframes heartbeat {
  0%, 100% { transform: scale(1); }
  10% { transform: scale(1.1); }
  20% { transform: scale(1); }
  30% { transform: scale(1.15); }
  40% { transform: scale(1); }
}

.heartbeat {
  animation: heartbeat 1s ease-in-out infinite;
  /* {motion.looping.pulse.heartbeat-normal} */
}

Framer Motion:

const heartbeatVariants = {
  beat: {
    scale: [1, 1.1, 1, 1.15, 1],
    transition: {
      duration: 1,
      repeat: Infinity,
      times: [0, 0.1, 0.2, 0.3, 0.4]
    }
  }
};

Breathing Effect

Gentle, natural breathing rhythm.

CSS:

@keyframes breathe {
  0%, 100% {
    transform: scale(1);
    opacity: 0.8;
  }
  50% {
    transform: scale(1.02);
    opacity: 1;
  }
}

.breathing {
  animation: breathe 4s ease-in-out infinite;
  /* {motion.looping.pulse.breath-normal} */
}

Framer Motion:

<motion.div
  animate={{
    scale: [1, 1.02, 1],
    opacity: [0.8, 1, 0.8]
  }}
  transition={{
    duration: 4, // {motion.looping.pulse.breath-normal}
    repeat: Infinity,
    ease: "easeInOut"
  }}
/>

Idle Animation

Motion that starts after inactivity.

JavaScript:

class IdleAnimation {
  constructor(element, options = {}) {
    this.element = element;
    this.idleDelay = options.delay || 3000; // {motion.looping.idle.delay-before-idle}
    this.animation = null;
    this.timeout = null;

    this.setupListeners();
  }

  setupListeners() {
    ['mousemove', 'keydown', 'scroll', 'touchstart'].forEach(event => {
      document.addEventListener(event, () => this.resetIdle());
    });

    this.startIdleTimer();
  }

  startIdleTimer() {
    this.timeout = setTimeout(() => this.startIdle(), this.idleDelay);
  }

  resetIdle() {
    clearTimeout(this.timeout);
    this.stopIdle();
    this.startIdleTimer();
  }

  startIdle() {
    this.animation = gsap.to(this.element, {
      y: 5,
      duration: 2,
      repeat: -1,
      yoyo: true,
      ease: "sine.inOut"
    });
  }

  stopIdle() {
    if (this.animation) {
      this.animation.kill();
      gsap.to(this.element, { y: 0, duration: 0.3 });
    }
  }
}

Seamless Loop (Video/Complex)

Creating visually seamless loops.

Crossfade technique:

function createSeamlessLoop(videoElement, crossfadeDuration = 0.5) {
  const duration = videoElement.duration;

  videoElement.addEventListener('timeupdate', () => {
    const remaining = duration - videoElement.currentTime;

    if (remaining <= crossfadeDuration) {
      // Start crossfade to beginning
      const progress = 1 - (remaining / crossfadeDuration);
      // Apply crossfade effect or transition
    }

    if (remaining <= 0.1) {
      videoElement.currentTime = 0;
    }
  });
}

Canvas-based seamless loop:

function seamlessLoopCanvas(frames, canvas, fps = 30) {
  const ctx = canvas.getContext('2d');
  const crossfadeFrames = 15; // Half second at 30fps
  let currentFrame = 0;

  function render() {
    const frame = frames[currentFrame];
    ctx.globalAlpha = 1;
    ctx.drawImage(frame, 0, 0);

    // Crossfade near loop point
    if (currentFrame >= frames.length - crossfadeFrames) {
      const blendFrame = currentFrame - (frames.length - crossfadeFrames);
      const alpha = blendFrame / crossfadeFrames;
      ctx.globalAlpha = alpha;
      ctx.drawImage(frames[blendFrame], 0, 0);
    }

    currentFrame = (currentFrame + 1) % frames.length;
    setTimeout(render, 1000 / fps);
  }

  render();
}

Phase-Offset Loops

Multiple elements looping with offset timing.

CSS:

.wave-item {
  animation: wave 2s ease-in-out infinite;
}

.wave-item:nth-child(1) { animation-delay: 0s; }
.wave-item:nth-child(2) { animation-delay: 0.2s; }
.wave-item:nth-child(3) { animation-delay: 0.4s; }
.wave-item:nth-child(4) { animation-delay: 0.6s; }
/* Creates wave effect */

@keyframes wave {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-10px); }
}

GSAP:

gsap.to('.wave-item', {
  y: -10,
  duration: 1,
  repeat: -1,
  yoyo: true,
  ease: "sine.inOut",
  stagger: {
    each: 0.2,
    repeat: -1
  }
});

Cinemagraph Effect

Near-static with subtle looping element.

CSS (isolated animation):

.cinemagraph {
  position: relative;
}

.cinemagraph .static {
  /* No animation */
}

.cinemagraph .animated {
  animation: subtleMove 30s ease-in-out infinite;
  /* {motion.looping.duration.ultra-slow} */
}

@keyframes subtleMove {
  0%, 100% { transform: translateX(0); }
  50% { transform: translateX(3px); }
}

Loop Count Guidelines

Use CaseCountToken
Single emphasis1once
Draw attention2-3twice, few
Temporary state5-10several, many
Continuous∞infinite

Reduced Motion

For prefers-reduced-motion: reduce:

Full MotionReduced Alternative
Spinning loaderStatic or very slow (20s+)
Breathing/pulseStatic or opacity-only
Idle animationNo animation
CinemagraphFreeze on single frame

Implementation:

@media (prefers-reduced-motion: reduce) {
  .spinner {
    animation-duration: 20s; /* Very slow, less disorienting */
  }

  .breathing {
    animation: none;
    /* Or opacity-only subtle pulse */
    animation: subtleFade 4s ease-in-out infinite;
  }

  @keyframes subtleFade {
    0%, 100% { opacity: 0.9; }
    50% { opacity: 1; }
  }

  .idle-animation {
    animation: none;
  }
}

Cross-References

  • See rotation/ for spin loop patterns
  • See scale/ for pulse/breathing scale animations
  • See opacity/ for fade loops
  • See procedural/ for noise-based variations
  • See loading-states/ for loader implementations

References

  • references/seamless-loop.md — Seamless loop techniques

دانلود ZIP