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
| Token | Squash | Stretch | Use Case |
|---|---|---|---|
subtle | 0.95 | 1.05 | Buttons, icons, professional UI |
medium | 0.85 | 1.15 | Cards, modals, consumer apps |
expressive | 0.70 | 1.30 | Playful, gaming, emphasis |
impact | 0.60 | 1.00 | Landing, 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 Style | Intensity | Example |
|---|---|---|
| Elegant/Luxury | subtle or none | Minimal, refined micro-interactions |
| Professional | subtle | Controlled feedback |
| Consumer/Friendly | medium | Approachable, lively |
| Playful/Gaming | expressive | Bouncy, characterful |
| Kids/Cartoon | expressive + extra | Maximum 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
- Breaking volume - Object grows/shrinks overall instead of redistributing
- Wrong axis - Stretching perpendicular to motion instead of along it
- Overuse - Applying to everything makes UI feel cheap
- Too fast - Squash/stretch needs time to read (minimum 100ms)
- 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