01Washer drum spin
Press the button and the water and a sock tumble inside the porthole — a start-stop button pauses and resumes the drum spin while a cat blinks behind the glass.
Best forAppliance landing pages, wash-start notices
rotateanimation-play-statescaleY

<main class="stage">
<div class="ws is-demo" id="wsRoot">
<div class="ws__machine">
<span class="ws__side"></span>
<div class="ws__top">
<span class="ws__mark"></span>
<span class="ws__mark ws__mark--short"></span>
<span class="ws__dial"></span>
<span class="ws__led ws__led--run"></span>
<span class="ws__led ws__led--pause"></span>
</div>
<div class="ws__door">
<span class="ws__ring"></span>
<div class="ws__glass">
<div class="ws__drum">
<span class="ws__water ws__water--a"></span>
<span class="ws__water ws__water--b"></span>
<span class="ws__sock"></span>
<span class="ws__shirt"></span>
</div>
<span class="ws__cat">
<span class="ws__ear ws__ear--l"></span>
<span class="ws__ear ws__ear--r"></span>
<span class="ws__tail"></span>
<span class="ws__head"></span>
<span class="ws__eye ws__eye--l"></span>
<span class="ws__eye ws__eye--r"></span>
</span>
<span class="ws__shine"></span>
<span class="ws__shine ws__shine--b"></span>
</div>
<span class="ws__notch"></span>
</div>
<span class="ws__label"></span>
</div>
<button class="ws__btn" id="wsBtn" type="button" aria-pressed="false">Start wash</button>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 세탁기 드럼 회전 — 정면 세탁기를 도형으로만 그린다(이미지 파일 0).
// 드럼 안 물과 양말·셔츠는 하나의 그룹으로 묶어 2초에 한 바퀴 돌고,
// 고양이는 유리 앞쪽 바닥에 붙어 도는 물건을 지켜본다(눈만 깜빡).
// 시작/정지 버튼은 animation-play-state 하나로 드럼을 멈췄다 놓는다.
$duration: $dur-loop;
$easing: $ease-out;
$color: $subject-cream;
$side: #ded1ba;
$glass: #211d29;
$steel: #4a4453;
@include stage($stage-ink);
.ws {
display: flex; flex-direction: column; align-items: center; gap: $sp-2;
width: min(100%, 320px);
}
// ── 세탁기 몸체
.ws__machine {
position: relative; box-sizing: border-box;
width: 128px; height: 148px;
border-radius: $r-card;
background: $color;
padding: $sp-2 $sp-2 0;
}
.ws__side {
position: absolute; right: -6px; top: 6px; bottom: 6px; width: 12px;
border-radius: 0 $r-card $r-card 0;
background: $side;
}
// ── 상단 컨트롤 패널
.ws__top {
position: relative; height: 22px;
display: flex; align-items: center; gap: $sp-1;
}
.ws__mark {
width: 18px; height: 4px; border-radius: $r-pill; background: $side;
}
.ws__mark--short { width: 10px; }
.ws__dial {
width: 14px; height: 14px; border-radius: 50%;
border: 3px solid $steel; box-sizing: border-box;
margin-left: auto;
}
.ws__led {
width: 6px; height: 6px; border-radius: 50%;
transition: opacity $dur-quick $easing;
}
.ws__led--run { background: $tick-up; opacity: 1; }
.ws__led--pause { background: $tick-down; opacity: .25; }
.ws.is-pause .ws__led--run { opacity: .25; }
.ws.is-pause .ws__led--pause { opacity: 1; }
// ── 원형 도어. 유리 안은 드럼(회전 그룹)과 고양이(고정) 두 겹
.ws__door {
position: relative; width: 84px; height: 84px;
margin: $sp-1 auto 0;
}
.ws__ring {
position: absolute; inset: -8px; border-radius: 50%;
background: $side;
}
.ws__glass {
position: absolute; inset: 0; border-radius: 50%;
background: $glass; overflow: hidden;
}
.ws__notch {
position: absolute; right: -12px; top: 50%; width: 8px; height: 8px;
margin-top: -4px; border-radius: 0 $r-xs $r-xs 0;
background: $steel;
}
// 드럼: 물 두 겹 + 양말 + 셔츠가 한 덩어리로 돈다
.ws__drum {
position: absolute; inset: 0;
animation-name: wsSpin; animation-duration: $duration;
animation-timing-function: linear; animation-iteration-count: infinite;
}
.ws.is-pause .ws__drum { animation-play-state: paused; }
.ws__water {
position: absolute; left: -10%; bottom: -38%; width: 120%; height: 78%;
border-radius: 50%;
background: rgba($subject-blue, .82);
}
.ws__water--b {
bottom: -46%; height: 72%; left: -16%;
background: rgba($subject-blue, .45);
}
.ws__sock {
position: absolute; left: 58%; top: 16%; width: 12px; height: 20px;
border-radius: $r-pill;
background: $stage-yellow;
}
.ws__shirt {
position: absolute; left: 16%; top: 30%; width: 18px; height: 14px;
border-radius: $r-xs;
background: $subject-mint;
}
// 고양이: 유리 하단에 앉아 도는 세탁물을 본다. 몸은 고정, 눈만 깜빡
.ws__cat {
position: absolute; left: 50%; bottom: -2px; width: 40px; height: 26px;
margin-left: -20px;
}
.ws__head {
position: absolute; inset: 0; bottom: -6px;
border-radius: 50% 50% $r-xs $r-xs;
background: $subject-ink;
}
.ws__ear {
position: absolute; top: -1px; width: 0; height: 0;
border-left: 5px solid transparent; border-right: 5px solid transparent;
border-bottom: 9px solid $subject-ink;
}
.ws__ear--l { left: 5px; }
.ws__ear--r { right: 5px; }
.ws__tail {
position: absolute; left: -8px; bottom: 2px; width: 14px; height: 9px;
border-radius: 50%; background: $subject-ink;
transform: rotate(24deg);
}
.ws__eye {
position: absolute; top: 9px; width: 7px; height: 7px;
border-radius: 50%; background: $subject-cream;
animation-name: wsBlink; animation-duration: $duration;
animation-iteration-count: infinite;
}
.ws__eye--l { left: 10px; }
.ws__eye--r { right: 10px; }
// 유리 반사: 두 줄 사선
.ws__shine {
position: absolute; left: 18%; top: 12%; width: 6px; height: 30px;
border-radius: $r-pill;
background: rgba(255, 247, 230, .16);
transform: rotate(38deg);
}
.ws__shine--b { left: 30%; top: 8%; height: 16px; opacity: .7; }
.ws__label {
position: absolute; right: $sp-2; bottom: $sp-2;
width: 26px; height: 8px; border-radius: $r-xs; background: $side;
}
.ws__btn {
box-sizing: border-box; padding: $sp-2 $sp-4; border: 0;
border-radius: $r-pill; background: $subject-blue-deep; color: $subject-cream;
font-family: inherit; font-weight: 700; font-size: 12px; line-height: 1.2;
cursor: pointer;
}
@keyframes wsSpin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes wsBlink {
0%, 42% { transform: scaleY(1); }
46%, 50% { transform: scaleY(.1); }
54%, 100% { transform: scaleY(1); }
}
@media (max-width: 340px) {
.ws__machine { width: 104px; height: 118px; }
.ws__top { height: 18px; }
.ws__mark { width: 12px; }
.ws__dial { width: 10px; height: 10px; }
.ws__door { width: 64px; height: 64px; }
.ws__cat { width: 32px; height: 20px; bottom: -1px; }
.ws__sock { width: 10px; height: 16px; }
.ws__shirt { width: 14px; height: 11px; }
.ws__btn { padding: $sp-1 $sp-3; font-size: 11px; }
}
@media (prefers-reduced-motion: reduce) {
.ws__drum, .ws__eye { animation-name: none !important; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.ws {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
width: min(100%, 320px);
}
.ws__machine {
position: relative;
box-sizing: border-box;
width: 128px;
height: 148px;
border-radius: 16px;
background: #fff7e6;
padding: 8px 8px 0;
}
.ws__side {
position: absolute;
right: -6px;
top: 6px;
bottom: 6px;
width: 12px;
border-radius: 0 16px 16px 0;
background: #ded1ba;
}
.ws__top {
position: relative;
height: 22px;
display: flex;
align-items: center;
gap: 4px;
}
.ws__mark {
width: 18px;
height: 4px;
border-radius: 999px;
background: #ded1ba;
}
.ws__mark--short {
width: 10px;
}
.ws__dial {
width: 14px;
height: 14px;
border-radius: 50%;
border: 3px solid #4a4453;
box-sizing: border-box;
margin-left: auto;
}
.ws__led {
width: 6px;
height: 6px;
border-radius: 50%;
transition: opacity 220ms ease-out;
}
.ws__led--run {
background: #3ddc97;
opacity: 1;
}
.ws__led--pause {
background: #ff7a5e;
opacity: 0.25;
}
.ws.is-pause .ws__led--run {
opacity: 0.25;
}
.ws.is-pause .ws__led--pause {
opacity: 1;
}
.ws__door {
position: relative;
width: 84px;
height: 84px;
margin: 4px auto 0;
}
.ws__ring {
position: absolute;
inset: -8px;
border-radius: 50%;
background: #ded1ba;
}
.ws__glass {
position: absolute;
inset: 0;
border-radius: 50%;
background: #211d29;
overflow: hidden;
}
.ws__notch {
position: absolute;
right: -12px;
top: 50%;
width: 8px;
height: 8px;
margin-top: -4px;
border-radius: 0 4px 4px 0;
background: #4a4453;
}
.ws__drum {
position: absolute;
inset: 0;
animation-name: wsSpin;
animation-duration: 2s;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
.ws.is-pause .ws__drum {
animation-play-state: paused;
}
.ws__water {
position: absolute;
left: -10%;
bottom: -38%;
width: 120%;
height: 78%;
border-radius: 50%;
background: rgba(47, 109, 246, 0.82);
}
.ws__water--b {
bottom: -46%;
height: 72%;
left: -16%;
background: rgba(47, 109, 246, 0.45);
}
.ws__sock {
position: absolute;
left: 58%;
top: 16%;
width: 12px;
height: 20px;
border-radius: 999px;
background: #ffd23f;
}
.ws__shirt {
position: absolute;
left: 16%;
top: 30%;
width: 18px;
height: 14px;
border-radius: 4px;
background: #4cd4a6;
}
.ws__cat {
position: absolute;
left: 50%;
bottom: -2px;
width: 40px;
height: 26px;
margin-left: -20px;
}
.ws__head {
position: absolute;
inset: 0;
bottom: -6px;
border-radius: 50% 50% 4px 4px;
background: #17141a;
}
.ws__ear {
position: absolute;
top: -1px;
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-bottom: 9px solid #17141a;
}
.ws__ear--l {
left: 5px;
}
.ws__ear--r {
right: 5px;
}
.ws__tail {
position: absolute;
left: -8px;
bottom: 2px;
width: 14px;
height: 9px;
border-radius: 50%;
background: #17141a;
transform: rotate(24deg);
}
.ws__eye {
position: absolute;
top: 9px;
width: 7px;
height: 7px;
border-radius: 50%;
background: #fff7e6;
animation-name: wsBlink;
animation-duration: 2s;
animation-iteration-count: infinite;
}
.ws__eye--l {
left: 10px;
}
.ws__eye--r {
right: 10px;
}
.ws__shine {
position: absolute;
left: 18%;
top: 12%;
width: 6px;
height: 30px;
border-radius: 999px;
background: rgba(255, 247, 230, 0.16);
transform: rotate(38deg);
}
.ws__shine--b {
left: 30%;
top: 8%;
height: 16px;
opacity: 0.7;
}
.ws__label {
position: absolute;
right: 8px;
bottom: 8px;
width: 26px;
height: 8px;
border-radius: 4px;
background: #ded1ba;
}
.ws__btn {
box-sizing: border-box;
padding: 8px 16px;
border: 0;
border-radius: 999px;
background: #0a4fe8;
color: #fff7e6;
font-family: inherit;
font-weight: 700;
font-size: 12px;
line-height: 1.2;
cursor: pointer;
}
@keyframes wsSpin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes wsBlink {
0%, 42% {
transform: scaleY(1);
}
46%, 50% {
transform: scaleY(0.1);
}
54%, 100% {
transform: scaleY(1);
}
}
@media (max-width: 340px) {
.ws__machine {
width: 104px;
height: 118px;
}
.ws__top {
height: 18px;
}
.ws__mark {
width: 12px;
}
.ws__dial {
width: 10px;
height: 10px;
}
.ws__door {
width: 64px;
height: 64px;
}
.ws__cat {
width: 32px;
height: 20px;
bottom: -1px;
}
.ws__sock {
width: 10px;
height: 16px;
}
.ws__shirt {
width: 14px;
height: 11px;
}
.ws__btn {
padding: 4px 12px;
font-size: 11px;
}
}
@media (prefers-reduced-motion: reduce) {
.ws__drum, .ws__eye {
animation-name: none !important;
}
}// 시작/정지는 .is-pause 클래스 하나로 끝난다. 손이 닿는 순간 미리보기 루프(.is-demo)를 떼고
// 사람이 누른 상태를 그대로 유지한다. 드럼 회전 자체는 사용자 조작에서도 계속 돈다.
(function () {
var root = document.getElementById('wsRoot');
var btn = document.getElementById('wsBtn');
var runLabel = 'Start wash';
var pauseLabel = 'Pause';
btn.addEventListener('click', function () {
root.classList.remove('is-demo');
var paused = root.classList.toggle('is-pause');
btn.textContent = paused ? pauseLabel : runLabel;
btn.setAttribute('aria-pressed', paused ? 'true' : 'false');
});
})();// WasherDrum — vanilla 판(run/078/demo/01-object-washer-drum/index.html) 의 React 포트.
// 시작/정지는 paused 하나로 갈리고, 첫 클릭에 미리보기 루프(is-demo)를 뗀다.
import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./WasherDrum.module.scss";
export type WasherDrumProps = {
duration?: number;
easing?: string;
color?: string;
initialPaused?: boolean;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function WasherDrum(props: WasherDrumProps) {
const duration = props.duration ?? 2000;
const easing = props.easing ?? "ease-out";
const color = props.color ?? "#fff7e6";
const [paused, setPaused] = useState<boolean>(props.initialPaused ?? false);
const [preview, setPreview] = useState<boolean>(true);
const vars: Vars = { "--duration": duration + "ms", "--easing": easing, "--color": color };
const rootClass = ["ws", preview ? "is-demo" : "", paused ? "is-pause" : ""]
.filter(Boolean).map((c) => (styles as { [k: string]: string })[c]).join(" ");
return (
<main className={styles.stage}>
<div className={rootClass} style={vars}>
<div className={styles["ws__machine"]}>
<span className={styles["ws__side"]}></span>
<div className={styles["ws__top"]}>
<span className={styles["ws__mark"]}></span>
<span className={styles["ws__mark"] + " " + styles["ws__mark--short"]}></span>
<span className={styles["ws__dial"]}></span>
<span className={styles["ws__led"] + " " + styles["ws__led--run"]}></span>
<span className={styles["ws__led"] + " " + styles["ws__led--pause"]}></span>
</div>
<div className={styles["ws__door"]}>
<span className={styles["ws__ring"]}></span>
<div className={styles["ws__glass"]}>
<div className={styles["ws__drum"]}>
<span className={styles["ws__water"] + " " + styles["ws__water--a"]}></span>
<span className={styles["ws__water"] + " " + styles["ws__water--b"]}></span>
<span className={styles["ws__sock"]}></span>
<span className={styles["ws__shirt"]}></span>
</div>
<span className={styles["ws__cat"]}>
<span className={styles["ws__ear"] + " " + styles["ws__ear--l"]}></span>
<span className={styles["ws__ear"] + " " + styles["ws__ear--r"]}></span>
<span className={styles["ws__tail"]}></span>
<span className={styles["ws__head"]}></span>
<span className={styles["ws__eye"] + " " + styles["ws__eye--l"]}></span>
<span className={styles["ws__eye"] + " " + styles["ws__eye--r"]}></span>
</span>
<span className={styles["ws__shine"]}></span>
<span className={styles["ws__shine"] + " " + styles["ws__shine--b"]}></span>
</div>
<span className={styles["ws__notch"]}></span>
</div>
<span className={styles["ws__label"]}></span>
</div>
<button
className={styles["ws__btn"]}
type="button"
aria-pressed={paused ? "true" : "false"}
onClick={() => { setPreview(false); setPaused((p) => !p); }}
>
{paused ? "일시정지" : "세탁 시작"}
</button>
</div>
</main>
);
}01 Washer drum spin — Measured area 3.349% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · CSS Art Animation — 9 Objects From Pure Shapes
The post this demo lives in
CSS Art Animation — 9 Objects From Pure ShapesCSS art animation draws everyday objects — a washer, a house, a car — with a few colored boxes and makes them move, not See all demos in this post


