04낙서 선 캐릭터 걷기
얇은 선으로 그린 캐릭터가 화면 밖에서 선을 그으며 걸어 들어옵니다. 다리는 두 개의 선이 교대로 흔들리고, 누르면 반대 방향으로 돌아 걷습니다.
쓰임소개 페이지 스크롤 안내 캐릭터
stroke-dashoffsetstroke-linecaptranslateX

<main class="stage">
<div class="dw is-demo" id="dw" data-dir="fwd">
<button class="dw__hit" type="button" aria-label="걷는 방향 바꾸기">
<svg class="dw__scene" viewBox="0 0 480 300" aria-hidden="true">
<line class="dw__ground" x1="60" y1="228" x2="420" y2="228" pathLength="100"/>
<g class="dw__walker" id="dwWalker">
<g class="dw__flipper">
<path class="dw__draw dw__body" d="M150 96 C150 66 176 52 200 52 C224 52 250 66 250 96 C250 128 226 150 200 150 C174 150 150 128 150 96 Z" pathLength="100"/>
<path class="dw__draw dw__leg dw__leg--l" d="M184 148 L180 224" pathLength="100"/>
<path class="dw__draw dw__leg dw__leg--r" d="M216 148 L220 224" pathLength="100"/>
<path class="dw__draw dw__arm dw__arm--l" d="M156 104 L128 122" pathLength="100"/>
<path class="dw__draw dw__arm dw__arm--r" d="M244 104 L272 96" pathLength="100"/>
<path class="dw__draw dw__eye" d="M186 92 L190 92" pathLength="100"/>
<path class="dw__draw dw__eye" d="M210 92 L214 92" pathLength="100"/>
<path class="dw__draw dw__mouth" d="M192 110 Q200 118 208 110" pathLength="100"/>
<path class="dw__draw dw__crest" d="M184 54 Q200 40 216 54" pathLength="100"/>
</g>
</g>
</svg>
</button>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 낙서 선 캐릭터 걷기 — 얇은 선(stroke)으로만 그린 캐릭터가 선을 그려가며 걷는다.
// 등장은 stroke-dashoffset(모든 path 에 pathLength="100" 을 걸어 길이를 100 으로 통일),
// 걷기는 다리 두 선의 교대 rotate(원점 = 엉덩이 붙는 지점) + 몸의 상하 바운스.
// 방향 전환은 안쪽 .dw__flipper 에 scaleX(-1) — 바깥 걷기 애니메이션(transform)과 겹치지 않게 층을 나눴다.
$duration: $dur-loop;
$easing: $ease-pop;
$color: $subject-blue;
@include stage($stage-paper);
.dw { display: grid; place-items: center; }
.dw__hit { border: 0; padding: 0; background: none; cursor: pointer; -webkit-tap-highlight-color: transparent; }
.dw__hit:focus-visible { outline: 2px solid $color; outline-offset: 4px; border-radius: $r-card; }
.dw__scene { display: block; width: min(100%, 440px); height: auto; overflow: visible; }
// 선 공통 — 낙서 느낌의 둥근 끝
.dw__draw {
fill: none; stroke: $subject-ink; stroke-width: 4;
stroke-linecap: round; stroke-linejoin: round;
stroke-dasharray: 100; stroke-dashoffset: 0;
}
.dw__ground { stroke: rgba(23, 20, 26, .3); stroke-width: 3; }
.dw__eye { stroke-width: 6; }
.dw__crest { stroke: $color; }
// 다리·팔 — 엉덩이·어깨(붙는 지점)를 원점으로 교대로 흔든다.
// SVG 자식의 transform-origin 은 transform-box: view-box 를 명시해야 viewBox 좌표(px) 로 잡힌다
.dw__leg { transform-box: view-box; transform-origin: 200px 148px; }
.dw__arm { transform-box: view-box; transform-origin: 200px 104px; }
.dw__leg--l, .dw__arm--r { animation-name: dwSwingA; }
.dw__leg--r, .dw__arm--l { animation-name: dwSwingB; }
.dw__leg, .dw__arm {
animation-duration: .5s;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
}
@keyframes dwSwingA {
0%, 100% { transform: rotate(16deg); }
50% { transform: rotate(-16deg); }
}
@keyframes dwSwingB {
0%, 100% { transform: rotate(-16deg); }
50% { transform: rotate(16deg); }
}
// 걷기 — 몸통 그룹이 좌우로 건너가며 상하로 까딱인다. 걸음(.5s)과 주기(2s)가 맞아떨어진다
.dw__walker {
animation-name: dwWalk;
animation-duration: $duration;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
@keyframes dwWalk {
0% { transform: translateX(-110px) translateY(0); }
12% { transform: translateX(-80px) translateY(-4px); }
25% { transform: translateX(-50px) translateY(0); }
37% { transform: translateX(-20px) translateY(-4px); }
50% { transform: translateX(10px) translateY(0); }
62% { transform: translateX(40px) translateY(-4px); }
75% { transform: translateX(70px) translateY(0); }
87% { transform: translateX(100px) translateY(-4px); }
100% { transform: translateX(110px) translateY(0); }
}
// 방향 — 누르면 안쪽 층이 좌우로 뒤집힌다
.dw[data-dir="back"] .dw__flipper { transform: scaleX(-1); }
.dw__flipper { transform-box: view-box; transform-origin: 200px 150px; }
// ── 격자 미리보기 루프(2초): 첫 38% 에 선이 그려지며 등장, 뒤는 계속 걷는다
.dw.is-demo .dw__draw {
animation-name: dwDraw;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
@keyframes dwDraw {
0% { stroke-dashoffset: 100; }
38%, 100% { stroke-dashoffset: 0; }
}
// ── 좁은 화면: svg 는 비율 유지라 폭이 곧 높이다 — 무대 세로(320×200, 패딩 제외 174px) 안에 들게 폭을 묶는다
@media (max-width: 340px) {
.dw__scene { width: min(100%, 272px); } // 272 × 300/480 = 170px
}
@media (prefers-reduced-motion: reduce) {
// 걷기를 끊면 다 그려진 캐릭터가 제자리에 선다 — 다리 흔들림도 멈춘다
.dw__walker,
.dw__leg,
.dw__arm,
.dw.is-demo .dw__draw {
animation: none !important;
}
.dw.is-demo .dw__draw { stroke-dashoffset: 0; }
}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);
}
.dw {
display: grid;
place-items: center;
}
.dw__hit {
border: 0;
padding: 0;
background: none;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.dw__hit:focus-visible {
outline: 2px solid #2f6df6;
outline-offset: 4px;
border-radius: 16px;
}
.dw__scene {
display: block;
width: min(100%, 440px);
height: auto;
overflow: visible;
}
.dw__draw {
fill: none;
stroke: #17141a;
stroke-width: 4;
stroke-linecap: round;
stroke-linejoin: round;
stroke-dasharray: 100;
stroke-dashoffset: 0;
}
.dw__ground {
stroke: rgba(23, 20, 26, 0.3);
stroke-width: 3;
}
.dw__eye {
stroke-width: 6;
}
.dw__crest {
stroke: #2f6df6;
}
.dw__leg {
transform-box: view-box;
transform-origin: 200px 148px;
}
.dw__arm {
transform-box: view-box;
transform-origin: 200px 104px;
}
.dw__leg--l, .dw__arm--r {
animation-name: dwSwingA;
}
.dw__leg--r, .dw__arm--l {
animation-name: dwSwingB;
}
.dw__leg, .dw__arm {
animation-duration: 0.5s;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
}
@keyframes dwSwingA {
0%, 100% {
transform: rotate(16deg);
}
50% {
transform: rotate(-16deg);
}
}
@keyframes dwSwingB {
0%, 100% {
transform: rotate(-16deg);
}
50% {
transform: rotate(16deg);
}
}
.dw__walker {
animation-name: dwWalk;
animation-duration: 2s;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
@keyframes dwWalk {
0% {
transform: translateX(-110px) translateY(0);
}
12% {
transform: translateX(-80px) translateY(-4px);
}
25% {
transform: translateX(-50px) translateY(0);
}
37% {
transform: translateX(-20px) translateY(-4px);
}
50% {
transform: translateX(10px) translateY(0);
}
62% {
transform: translateX(40px) translateY(-4px);
}
75% {
transform: translateX(70px) translateY(0);
}
87% {
transform: translateX(100px) translateY(-4px);
}
100% {
transform: translateX(110px) translateY(0);
}
}
.dw[data-dir=back] .dw__flipper {
transform: scaleX(-1);
}
.dw__flipper {
transform-box: view-box;
transform-origin: 200px 150px;
}
.dw.is-demo .dw__draw {
animation-name: dwDraw;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 1.4, 0.4, 1);
animation-iteration-count: infinite;
}
@keyframes dwDraw {
0% {
stroke-dashoffset: 100;
}
38%, 100% {
stroke-dashoffset: 0;
}
}
@media (max-width: 340px) {
.dw__scene {
width: min(100%, 272px);
}
}
@media (prefers-reduced-motion: reduce) {
.dw__walker,
.dw__leg,
.dw__arm,
.dw.is-demo .dw__draw {
animation: none !important;
}
.dw.is-demo .dw__draw {
stroke-dashoffset: 0;
}
}var dw = document.getElementById('dw');
// 누르면 걷는 방향이 뒤집힌다 — data-dir 하나만 바꾸고 거울은 CSS 가 씌운다
document.querySelector('.dw__hit').addEventListener('click', function () {
dw.classList.remove('is-demo');
dw.dataset.dir = dw.dataset.dir === 'fwd' ? 'back' : 'fwd';
});
dw.addEventListener('pointerdown', function () { dw.classList.remove('is-demo'); });// CuteDoodleWalkCycle — vanilla 판(run/075/demo/04-cute-doodle-walk-cycle) 의 React 포트.
// 누르면 걷는 방향이 뒤집힌다 — data-dir 하나만 바꾸고 거울은 CSS 가 씌운다.
import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./CuteDoodleWalkCycle.module.scss";
export type CuteDoodleWalkCycleProps = {
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function CuteDoodleWalkCycle(props: CuteDoodleWalkCycleProps) {
const duration = props.duration ?? 2000;
const easing = props.easing ?? "cubic-bezier(.2, 1.4, .4, 1)";
const color = props.color ?? "#2f6df6";
const [dir, setDir] = useState<"fwd" | "back">("fwd");
const [preview, setPreview] = useState<boolean>(true);
const vars: Vars = { "--duration": duration + "ms", "--easing": easing, "--color": color };
return (
<main className={styles.stage}>
<div className={styles.dw + (preview ? " " + styles.isDemo : "")} style={vars} data-dir={dir}>
<button
className={styles.hit}
type="button"
aria-label="걷는 방향 바꾸기"
onClick={() => setDir(dir === "fwd" ? "back" : "fwd")}
onPointerDown={() => setPreview(false)}
>
<svg className={styles.scene} viewBox="0 0 480 300" aria-hidden="true">
<line className={styles.ground} x1="60" y1="228" x2="420" y2="228" pathLength="100" />
<g className={styles.walker}>
<g className={styles.flipper}>
<path className={styles.draw + " " + styles.body} d="M150 96 C150 66 176 52 200 52 C224 52 250 66 250 96 C250 128 226 150 200 150 C174 150 150 128 150 96 Z" pathLength="100" />
<path className={styles.draw + " " + styles.leg + " " + styles.legL} d="M184 148 L180 224" pathLength="100" />
<path className={styles.draw + " " + styles.leg + " " + styles.legR} d="M216 148 L220 224" pathLength="100" />
<path className={styles.draw + " " + styles.arm + " " + styles.armL} d="M156 104 L128 122" pathLength="100" />
<path className={styles.draw + " " + styles.arm + " " + styles.armR} d="M244 104 L272 96" pathLength="100" />
<path className={styles.draw + " " + styles.eye} d="M186 92 L190 92" pathLength="100" />
<path className={styles.draw + " " + styles.eye} d="M210 92 L214 92" pathLength="100" />
<path className={styles.draw + " " + styles.mouth} d="M192 110 Q200 118 208 110" pathLength="100" />
<path className={styles.draw + " " + styles.crest} d="M184 54 Q200 40 216 54" pathLength="100" />
</g>
</g>
</svg>
</button>
</div>
</main>
);
}04 낙서 선 캐릭터 걷기 — 측정 누적면적 12.113% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 귀여운 캐릭터 애니메이션 9종 — 일러스트 캐릭터 CSS 모음
이 데모가 속한 글
귀여운 캐릭터 애니메이션 9종 — 일러스트 캐릭터 CSS 모음귀여운 캐릭터 애니메이션은 그린 캐릭터를 이미지 파일 없이 코드만으로 깜빡이고 튀게 만드는 일입니다. 하트 눈 얼굴에서 쿠키 나눠 주는 친구까지 아홉 마리를 CSS 와 SVG 도형으로 지었고, 클릭하면 캐릭터가 바로 이 글의 데모 전부 보기


