06질량 값에 따른 눌림 속도 차이 — 정해둔 질량 값이 클수록 눌리고 돌아오는 속도 값이 느려지는 방식
정해둔 질량 값이 클수록 버튼이 눌리고 원래 값으로 돌아오는 속도 값이 더 느리게 나타납니다.
쓰임무겁게 느껴지도록 설계한 삭제 버튼, 신중한 조작을 유도하는 결제 버튼
MASSKPRESS_DEPTH

<main class="stage">
<div class="bwm is-demo" id="bwm">
<div class="bwm__row">
<button class="bwm__btn" id="bwmLight" data-mass="light" type="button">가벼움</button>
<button class="bwm__btn" id="bwmMedium" data-mass="medium" type="button">보통</button>
<button class="bwm__btn" id="bwmHeavy" data-mass="heavy" type="button">무거움</button>
</div>
<p class="bwm__hint">눌러보면 정해둔 질량 값이 클수록 더 느리게 움직입니다</p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 겹(질량별 스프링 적분)은 JS 에 있다 — 여기는 정적 배치만.
@include stage($stage-paper);
.stage { position: relative; }
.bwm {
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-4;
}
.bwm__row {
display: flex;
gap: $sp-4;
}
.bwm__btn {
border: 0;
border-radius: $r-control;
padding: $sp-4 $sp-5;
background: $subject-blue-deep;
color: $subject-cream;
font-size: 13px;
font-weight: 700;
cursor: pointer;
box-shadow: $shadow-raised;
}
.bwm__hint {
color: rgba($subject-ink, .72);
font-size: 12px;
text-align: center;
max-width: 260px;
}
@media (max-width: 340px) {
.bwm__row { gap: $sp-2; }
.bwm__btn { padding: $sp-3 $sp-3; font-size: 12px; }
}
@media (prefers-reduced-motion: reduce) {
.bwm__btn { transition: none !important; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.stage {
position: relative;
}
.bwm {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.bwm__row {
display: flex;
gap: 16px;
}
.bwm__btn {
border: 0;
border-radius: 12px;
padding: 16px 20px;
background: #0a4fe8;
color: #fff7e6;
font-size: 13px;
font-weight: 700;
cursor: pointer;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
.bwm__hint {
color: rgba(23, 20, 26, 0.72);
font-size: 12px;
text-align: center;
max-width: 260px;
}
@media (max-width: 340px) {
.bwm__row {
gap: 8px;
}
.bwm__btn {
padding: 12px 12px;
font-size: 12px;
}
}
@media (prefers-reduced-motion: reduce) {
.bwm__btn {
transition: none !important;
}
}(function () {
var root = document.getElementById('bwm');
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// 계산 겹: 세 버튼은 같은 스프링 상수 K·감쇠 C 를 쓰지만 질량 값(MASS)이 다르다.
// accel = -(K/MASS)*(depth-target) - (C/MASS)*vel — MASS 가 클수록 같은 힘에서 가속도가
// 작아져(뉴턴 F=ma) 눌리고 돌아오는 속도가 함께 느려진다.
var K = 320, C = 8, PRESS_DEPTH = 12;
var MASS = { light: 0.5, medium: 1.4, heavy: 3.6 };
function makeBtn(el, key) {
return { el: el, mass: MASS[key], depth: 0, vel: 0, target: 0 };
}
var items = [
makeBtn(document.getElementById('bwmLight'), 'light'),
makeBtn(document.getElementById('bwmMedium'), 'medium'),
makeBtn(document.getElementById('bwmHeavy'), 'heavy')
];
var last = 0, raf = null;
function paint(it) { it.el.style.transform = 'translateY(' + it.depth.toFixed(2) + 'px)'; }
function tick(now) {
var dt = Math.min((now - last) / 1000, 0.032);
last = now;
for (var i = 0; i < items.length; i++) {
var it = items[i];
var accel = (-K * (it.depth - it.target) - C * it.vel) / it.mass;
it.vel += accel * dt;
it.depth += it.vel * dt;
paint(it);
}
raf = requestAnimationFrame(tick);
}
function start() { if (raf === null) { last = performance.now(); raf = requestAnimationFrame(tick); } }
function press(it, p) {
if (reduce) { it.depth = p; it.vel = 0; it.target = p; paint(it); return; }
it.target = p;
start();
}
function wireReal() {
items.forEach(function (it) {
it.el.addEventListener('pointerdown', function () { root.classList.remove('is-demo'); stopAutoplay(); press(it, PRESS_DEPTH); });
it.el.addEventListener('pointerup', function () { press(it, 0); });
it.el.addEventListener('pointercancel', function () { press(it, 0); });
});
}
wireReal();
items.forEach(paint);
// 자동 루프: 세 버튼을 동시에 눌렀다 동시에 떼어, 같은 힘에서 질량별 속도 차이를 나란히 보여준다.
var autoTimer = null, autoDown = null;
function autoTick() {
items.forEach(function (it) { press(it, PRESS_DEPTH); });
autoDown = setTimeout(function () { items.forEach(function (it) { press(it, 0); }); }, 300);
}
function stopAutoplay() {
if (autoTimer) { clearInterval(autoTimer); autoTimer = null; }
if (autoDown) { clearTimeout(autoDown); autoDown = null; }
}
if (!reduce) { autoTick(); autoTimer = setInterval(autoTick, 1700); }
})();import { useEffect, useRef } from "react";
import styles from "./ButtonPhysicsWeightMassPressDepth.module.scss";
export type ButtonPhysicsWeightMassPressDepthProps = {
color?: string;
};
type MassKey = "light" | "medium" | "heavy";
type MassItem = { el: HTMLButtonElement; mass: number; depth: number; vel: number; target: number };
export default function ButtonPhysicsWeightMassPressDepth({
color = "#0a4fe8",
}: ButtonPhysicsWeightMassPressDepthProps) {
const rootRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const root = rootRef.current;
if (!root) return;
const lightEl = root.querySelector<HTMLButtonElement>(`.${styles.light}`);
const mediumEl = root.querySelector<HTMLButtonElement>(`.${styles.medium}`);
const heavyEl = root.querySelector<HTMLButtonElement>(`.${styles.heavy}`);
if (!lightEl || !mediumEl || !heavyEl) return;
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
// 계산 겹: 세 버튼은 같은 스프링 상수 K·감쇠 C 를 쓰지만 질량 값(MASS)이 다르다.
// accel = -(K/MASS)*(depth-target) - (C/MASS)*vel — MASS 가 클수록 같은 힘에서 가속도가
// 작아져(뉴턴 F=ma) 눌리고 돌아오는 속도가 함께 느려진다.
const K = 320, C = 8, PRESS_DEPTH = 12;
const MASS: Record<MassKey, number> = { light: 0.5, medium: 1.4, heavy: 3.6 };
function makeBtn(el: HTMLButtonElement, key: MassKey): MassItem {
return { el, mass: MASS[key], depth: 0, vel: 0, target: 0 };
}
const items: MassItem[] = [
makeBtn(lightEl, "light"),
makeBtn(mediumEl, "medium"),
makeBtn(heavyEl, "heavy"),
];
let last = 0, raf: number | null = null;
function paint(it: MassItem) { it.el.style.transform = "translateY(" + it.depth.toFixed(2) + "px)"; }
function tick(now: number) {
const dt = Math.min((now - last) / 1000, 0.032);
last = now;
for (let i = 0; i < items.length; i++) {
const it = items[i];
const accel = (-K * (it.depth - it.target) - C * it.vel) / it.mass;
it.vel += accel * dt;
it.depth += it.vel * dt;
paint(it);
}
raf = requestAnimationFrame(tick);
}
function start() { if (raf === null) { last = performance.now(); raf = requestAnimationFrame(tick); } }
function press(it: MassItem, p: number) {
if (reduce) { it.depth = p; it.vel = 0; it.target = p; paint(it); return; }
it.target = p;
start();
}
const cleanups: Array<() => void> = [];
items.forEach((it) => {
const onDown = () => { stopAutoplay(); press(it, PRESS_DEPTH); };
const onUp = () => { press(it, 0); };
it.el.addEventListener("pointerdown", onDown);
it.el.addEventListener("pointerup", onUp);
it.el.addEventListener("pointercancel", onUp);
cleanups.push(() => {
it.el.removeEventListener("pointerdown", onDown);
it.el.removeEventListener("pointerup", onUp);
it.el.removeEventListener("pointercancel", onUp);
});
});
items.forEach(paint);
let autoTimer: number | null = null, autoDown: number | null = null;
function autoTick() {
items.forEach((it) => { press(it, PRESS_DEPTH); });
autoDown = window.setTimeout(() => { items.forEach((it) => { press(it, 0); }); }, 300);
}
function stopAutoplay() {
if (autoTimer !== null) { window.clearInterval(autoTimer); autoTimer = null; }
if (autoDown !== null) { window.clearTimeout(autoDown); autoDown = null; }
}
if (!reduce) { autoTick(); autoTimer = window.setInterval(autoTick, 1700); }
return () => {
stopAutoplay();
if (raf !== null) cancelAnimationFrame(raf);
cleanups.forEach((fn) => fn());
};
}, []);
return (
<div ref={rootRef} className={styles.stage}>
<div className={styles.bwm}>
<div className={styles.row}>
<button className={`${styles.btn} ${styles.light}`} type="button" style={{ background: color }}>가벼움</button>
<button className={`${styles.btn} ${styles.medium}`} type="button" style={{ background: color }}>보통</button>
<button className={`${styles.btn} ${styles.heavy}`} type="button" style={{ background: color }}>무거움</button>
</div>
<p className={styles.hint}>눌러보면 정해둔 질량 값이 클수록 더 느리게 움직입니다</p>
</div>
</div>
);
}06 질량 값에 따른 눌림 속도 차이 — 정해둔 질량 값이 클수록 눌리고 돌아오는 속도 값이 느려지는 방식 — 측정 누적면적 14.093% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 버튼 눌림 효과 UI 9종 — 눌림 값이 되돌아오는 아홉 가지 방식
이 데모가 속한 글
버튼 눌림 효과 UI 9종 — 눌림 값이 되돌아오는 아홉 가지 방식버튼 눌림 효과는 버튼을 누르거나 놓는 순간의 깊이·위치·각도 같은 값이 물리 계산을 거쳐 원래 상태로 돌아가는 것을 말하는데, 아홉 개 중 하나는 눌린 값이 목표 값을 넘었다가 되돌아오길 반복하며 잦아들고, 하나는 이 글의 데모 전부 보기

