squash-stretch

برای ساخت حرکت‌های زنده، کشسان و طبیعی با اصل Squash & Stretch

این مهارت اصل انیمیشن Squash & Stretch را برای ایجاد حس وزن، انعطاف و زندگی در عناصر UI و ویدیو به‌کار می‌گیرد. با حفظ حجم، عناصر هنگام برخورد فشرده و در مسیر حرکت کشیده می‌شوند.

برای ساخت دکمه‌های جهنده، افکت‌های ضربه، تعامل‌های بازی‌گونه و انیمیشن‌های شخصیت‌محور، presetهای آماده و راهکارهای اجرایی برای CSS، GSAP، Framer Motion و Remotion ارائه می‌کند.

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

  • توکن‌های شدت subtle، medium، expressive و impact برای کنترل تغییر شکل
  • الگوهای آماده برای bounce، press و landing با حفظ حجم
  • نمونه‌کدهای CSS Keyframes، GSAP، Framer Motion و Remotion
  • راهنمای انتخاب محور درست کشش و فشردگی بر اساس جهت حرکت
  • نکات دسترس‌پذیری برای پشتیبانی از prefers-reduced-motion

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

  • طراحان و توسعه‌دهندگانی که برای دکمه‌ها، کارت‌ها و اعلان‌ها بازخورد زنده می‌سازند
  • پروژه‌های بازی، اپلیکیشن‌های کودکانه و رابط‌های کاربری دوستانه و تعاملی
  • ساخت افکت‌های برخورد، فرود، پرش و حرکت‌های ارگانیک در ویدیو یا UI

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

  • عناصر سخت و هندسی مانند ماشین‌آلات، پنل‌های رسمی یا اجزای صنعتی
  • انیمیشن مستقیم متن و عناصر متن‌محور که تغییر شکل خوانایی آن‌ها را کاهش می‌دهد
  • زمینه‌ها و کانتینرهایی که نباید حس بازی‌گوش یا کشسان داشته باشند

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

Squash & Stretch

Disney's most fundamental animation principle. Creates the illusion of weight, flexibility, and life through volume-preserving deformation.

Core Principle

Objects compress on impact (squash) and elongate during motion (stretch), while maintaining constant volume.

Volume Preservation Formula:
scaleX * scaleY = 1 (constant)

If scaleY = 0.8 (squash), then scaleX = 1.25 (compensating width)

Token Reference

Load from: tokens/squash-stretch.tokens.json

TokenSquashStretchUse Case
subtle0.951.05Buttons, icons, professional UI
medium0.851.15Cards, modals, consumer apps
expressive0.701.30Playful, gaming, emphasis
impact0.601.00Landing, collision (squash only)

When to Apply

Use Squash & Stretch For:

  • Bounces and impacts - Ball drops, button presses, landing
  • Organic motion - Characters, creatures, living UI elements
  • Emphasis moments - Attention-grabbing notifications
  • Playful interactions - Games, kids apps, creative tools

Skip For:

  • Rigid objects (machinery, geometry)
  • Serious/professional contexts
  • Text animations (readability issues)
  • Backgrounds and containers

Axis Alignment

Critical: Squash perpendicular to motion, stretch along motion axis.

Falling Ball:

Before impact:  Stretched vertically (tall, narrow)
At impact:      Squashed vertically (flat, wide)
After rebound:  Stretched vertically again

Implementation Presets

CSS Keyframes

@keyframes bounce-squash {
  0% { transform: scaleX(1) scaleY(1); }
  /* Falling - stretch vertically */
  40% { transform: scaleX(0.9) scaleY(1.1); }
  /* Impact - squash */
  50% { transform: scaleX(1.15) scaleY(0.85); }
  /* Rebound - stretch */
  60% { transform: scaleX(0.95) scaleY(1.05); }
  /* Settle */
  80% { transform: scaleX(1.02) scaleY(0.98); }
  100% { transform: scaleX(1) scaleY(1); }
}

.bouncy-button:active {
  animation: bounce-squash 300ms var(--ease-emphasized);
}

GSAP

import tokens from '../tokens/squash-stretch.tokens.json';

const { squash, stretch } = tokens['squash-stretch'].medium.$value;

// Button press with squash
gsap.to('.button', {
  scaleX: 1 / squash,  // Volume preservation
  scaleY: squash,
  duration: 0.1,
  ease: 'power2.out',
  yoyo: true,
  repeat: 1
});

// Bounce landing
function bounceIn(element, intensity = 'medium') {
  const { squash, stretch } = tokens['squash-stretch'][intensity].$value;

  return gsap.timeline()
    .from(element, {
      y: -200,
      scaleX: 1/stretch,
      scaleY: stretch,
      duration: 0.4,
      ease: 'power2.in'
    })
    .to(element, {
      scaleX: 1/squash,
      scaleY: squash,
      duration: 0.1,
      ease: 'power4.out'
    })
    .to(element, {
      scaleX: 1,
      scaleY: 1,
      duration: 0.3,
      ease: 'elastic.out(1, 0.5)'
    });
}

Framer Motion

import { motion } from 'framer-motion';
import tokens from '../tokens/squash-stretch.tokens.json';

const squashStretchVariants = {
  rest: { scaleX: 1, scaleY: 1 },

  pressed: {
    scaleX: 1 / tokens['squash-stretch'].subtle.$value.squash,
    scaleY: tokens['squash-stretch'].subtle.$value.squash,
    transition: { duration: 0.1 }
  },

  bounce: {
    scaleY: [1, 1.15, 0.85, 1.05, 1],
    scaleX: [1, 0.87, 1.18, 0.95, 1],
    transition: { duration: 0.5, times: [0, 0.2, 0.4, 0.6, 1] }
  }
};

export const BouncyButton = ({ children }) => (
  <motion.button
    variants={squashStretchVariants}
    initial="rest"
    whileTap="pressed"
  >
    {children}
  </motion.button>
);

Remotion

import { useCurrentFrame, interpolate, spring } from 'remotion';
import tokens from '../tokens/squash-stretch.tokens.json';

interface SquashStretchProps {
  impactFrame: number;
  intensity: 'subtle' | 'medium' | 'expressive';
  children: React.ReactNode;
}

export const SquashStretch: React.FC<SquashStretchProps> = ({
  impactFrame,
  intensity = 'medium',
  children
}) => {
  const frame = useCurrentFrame();
  const { squash, stretch } = tokens['squash-stretch'][intensity].$value;

  // Phase 1: Falling/approaching - stretch in direction of motion
  // Phase 2: Impact - squash perpendicular to motion
  // Phase 3: Recovery - return to normal with slight overshoot

  const scaleY = interpolate(
    frame,
    [impactFrame - 10, impactFrame - 2, impactFrame, impactFrame + 5, impactFrame + 15],
    [1, stretch, squash, 1.05, 1],
    { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }
  );

  // Volume preservation: scaleX inverse of scaleY
  const scaleX = 1 / scaleY;

  return (
    <div style={{ transform: `scaleX(${scaleX}) scaleY(${scaleY})` }}>
      {children}
    </div>
  );
};

// Spring-based squash for button interactions
export const useSquashSpring = (isPressed: boolean) => {
  const frame = useCurrentFrame();

  const springValue = spring({
    frame,
    fps: 30,
    config: { damping: 10, stiffness: 200, mass: 0.5 }
  });

  const targetSquash = isPressed ? 0.85 : 1;
  const scaleY = interpolate(springValue, [0, 1], [1, targetSquash]);
  const scaleX = 1 / scaleY;

  return { scaleX, scaleY };
};

Brand Attitude Mapping

Brand StyleIntensityExample
Elegant/Luxurysubtle or noneMinimal, refined micro-interactions
ProfessionalsubtleControlled feedback
Consumer/FriendlymediumApproachable, lively
Playful/GamingexpressiveBouncy, characterful
Kids/Cartoonexpressive + extraMaximum squish

Accessibility

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

// Reduced motion: skip squash/stretch, use opacity instead
const animation = prefersReducedMotion
  ? { opacity: [0.7, 1] }
  : { scaleY: [0.85, 1], scaleX: [1.18, 1] };

Common Mistakes

  1. Breaking volume - Object grows/shrinks overall instead of redistributing
  2. Wrong axis - Stretching perpendicular to motion instead of along it
  3. Overuse - Applying to everything makes UI feel cheap
  4. Too fast - Squash/stretch needs time to read (minimum 100ms)
  5. Text distortion - Never apply to text-heavy elements

Related Skills

  • anticipation - The wind-up phase often includes a preparatory squash
  • follow-through - Settling after impact involves diminishing squash/stretch oscillations
  • exaggeration - Controls intensity of squash/stretch deformation
  • arc-motion - Objects stretch along their arc trajectory
  • secondary-action - Secondary elements may have delayed squash reactions

دانلود ZIP