07완료 값 반짝 표시 — 다운로드가 끝나는 순간 완료 값이 1로 바뀌었다가 일정 시간 뒤 다시 0으로 사라지는 방식
다운로드가 끝나는 순간 완료 값이 1로 바뀌며 잠깐 나타났다가, 정해진 시간이 지나면 값이 0으로 돌아가 사라집니다.
쓰임다운로드 완료를 알리는 상태줄, 백그라운드 저장 완료 안내
.dcf__sweepDECAYSWEEP_COUNT

<main class="stage">
<div class="dcf is-demo" id="dcf">
<div class="dcf__badge" id="dcfBadge">
<span class="dcf__sweep" id="dcfSweep"></span>
<span class="dcf__text">완료</span>
</div>
<button class="dcf__btn" id="dcfBtn" type="button">다시 보기</button>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 겹: 광택 sweep 세기 = 초기값 × 0.55^회차(지수 감쇠) — index.html sweepOpacity() 참조
@include stage($stage-ink);
.dcf {
width: 100%;
max-width: 220px;
display: grid;
justify-items: center;
gap: $sp-4;
}
.dcf__badge {
position: relative;
overflow: hidden;
width: 140px;
height: 56px;
border-radius: $r-card;
background: $subject-mint;
display: grid;
place-items: center;
opacity: 0;
transform: scale(.7);
transition: opacity $dur-quick $ease-out, transform $dur-quick $ease-pop;
}
.dcf--on .dcf__badge {
opacity: 1;
transform: scale(1);
}
.dcf__sweep {
position: absolute;
top: -20%;
left: 0;
width: 40%;
height: 140%;
background: linear-gradient(90deg, rgba($subject-cream, 0), rgba($subject-cream, .8), rgba($subject-cream, 0));
opacity: 0;
pointer-events: none;
}
.dcf__text {
font: 700 15px/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
color: $stage-ink;
}
.dcf__btn {
width: 100%;
border: 0;
border-radius: $r-control;
padding: $sp-2 $sp-4;
background: $subject-cream;
color: $stage-ink;
font: 700 12px/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
cursor: pointer;
transition: transform $dur-instant $ease-out;
&:active { transform: scale(.96); }
}
@media (max-width: 340px) {
.dcf { max-width: 180px; }
.dcf__badge { width: 120px; height: 48px; }
}
@media (prefers-reduced-motion: reduce) {
.dcf__badge, .dcf__btn { transition: none; }
}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);
}
.dcf {
width: 100%;
max-width: 220px;
display: grid;
justify-items: center;
gap: 16px;
}
.dcf__badge {
position: relative;
overflow: hidden;
width: 140px;
height: 56px;
border-radius: 16px;
background: #4cd4a6;
display: grid;
place-items: center;
opacity: 0;
transform: scale(0.7);
transition: opacity 220ms ease-out, transform 220ms cubic-bezier(0.2, 1.4, 0.4, 1);
}
.dcf--on .dcf__badge {
opacity: 1;
transform: scale(1);
}
.dcf__sweep {
position: absolute;
top: -20%;
left: 0;
width: 40%;
height: 140%;
background: linear-gradient(90deg, rgba(255, 247, 230, 0), rgba(255, 247, 230, 0.8), rgba(255, 247, 230, 0));
opacity: 0;
pointer-events: none;
}
.dcf__text {
font: 700 15px/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
color: #17141a;
}
.dcf__btn {
width: 100%;
border: 0;
border-radius: 12px;
padding: 8px 16px;
background: #fff7e6;
color: #17141a;
font: 700 12px/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
cursor: pointer;
transition: transform 140ms ease-out;
}
.dcf__btn:active {
transform: scale(0.96);
}
@media (max-width: 340px) {
.dcf {
max-width: 180px;
}
.dcf__badge {
width: 120px;
height: 48px;
}
}
@media (prefers-reduced-motion: reduce) {
.dcf__badge, .dcf__btn {
transition: none;
}
}(function () {
var root = document.getElementById('dcf');
var badge = document.getElementById('dcfBadge');
var sweep = document.getElementById('dcfSweep');
var btn = document.getElementById('dcfBtn');
// 상수: 완료 표시 유지 시간(ms) · 광택 sweep 1회 길이(ms) · 반복 횟수 · 감쇠율
var VISIBLE_MS = 1500;
var SWEEP_MS = 420;
var SWEEP_COUNT = 3;
var DECAY = 0.55; // 회차마다 강도(불투명도)가 이 비율로 줄어든다
var CYCLE_MS = 2400;
var start = performance.now();
var raf = null;
// 계산 겹: 광택 sweep 의 세기 = 초기값 × DECAY^회차(지수 감쇠) — 매 회차 옅어지다 사라진다
function sweepOpacity(passIndex) {
return Math.pow(DECAY, passIndex);
}
function frame(now) {
var e = (now - start) % CYCLE_MS;
var on = e < VISIBLE_MS;
root.classList.toggle('dcf--on', on);
if (on) {
var passLen = SWEEP_MS;
var passIdx = Math.floor(e / passLen);
var passT = (e % passLen) / passLen;
if (passIdx < SWEEP_COUNT) {
var x = -30 + passT * 160; // %
sweep.style.transform = 'translateX(' + x.toFixed(1) + '%) skewX(-18deg)';
sweep.style.opacity = sweepOpacity(passIdx).toFixed(3);
} else {
sweep.style.opacity = '0';
}
}
raf = requestAnimationFrame(frame);
}
raf = requestAnimationFrame(frame);
btn.addEventListener('click', function () {
root.classList.remove('is-demo');
start = performance.now();
root.classList.add('dcf--on');
});
})();import { useEffect, useRef } from "react";
import styles from "./DownloadManagerCompleteFlash.module.scss";
type Box<T> = { current: T };
export default function DownloadManagerCompleteFlash() {
const rootRef = useRef(null) as unknown as Box<HTMLDivElement | null>;
useEffect(() => {
const root = rootRef.current;
if (!root) return;
const sweep = root.querySelector(`.${styles.sweep}`) as HTMLElement | null;
const btn = root.querySelector(`.${styles.btn}`) as HTMLElement | null;
if (!sweep || !btn) return;
// 상수: 완료 표시 유지 시간(ms) · 광택 sweep 1회 길이(ms) · 반복 횟수 · 감쇠율
const VISIBLE_MS = 1500;
const SWEEP_MS = 420;
const SWEEP_COUNT = 3;
const DECAY = 0.55;
const CYCLE_MS = 2400;
let start = performance.now();
let rafId = 0;
let stopped = false;
// 계산 겹: 광택 sweep 의 세기 = 초기값 × DECAY^회차(지수 감쇠) — 매 회차 옅어지다 사라진다
function sweepOpacity(passIndex: number) {
return Math.pow(DECAY, passIndex);
}
function frame(now: number) {
if (stopped) return;
const e = (now - start) % CYCLE_MS;
const on = e < VISIBLE_MS;
root!.classList.toggle(styles["dcf--on"], on);
if (on) {
const passLen = SWEEP_MS;
const passIdx = Math.floor(e / passLen);
const passT = (e % passLen) / passLen;
if (passIdx < SWEEP_COUNT) {
const x = -30 + passT * 160;
sweep!.style.transform = "translateX(" + x.toFixed(1) + "%) skewX(-18deg)";
sweep!.style.opacity = sweepOpacity(passIdx).toFixed(3);
} else {
sweep!.style.opacity = "0";
}
}
rafId = requestAnimationFrame(frame);
}
rafId = requestAnimationFrame(frame);
function onClick() {
root!.classList.remove(styles["is-demo"]);
start = performance.now();
root!.classList.add(styles["dcf--on"]);
}
btn.addEventListener("click", onClick);
return () => {
stopped = true;
cancelAnimationFrame(rafId);
btn.removeEventListener("click", onClick);
};
}, []);
return (
<div ref={rootRef} className={`${styles.stage} ${styles.dcf} ${styles["is-demo"]}`}>
<div className={styles.badge}>
<span className={styles.sweep} />
<span className={styles.text}>완료</span>
</div>
<button className={styles.btn} type="button">다시 보기</button>
</div>
);
}07 완료 값 반짝 표시 — 다운로드가 끝나는 순간 완료 값이 1로 바뀌었다가 일정 시간 뒤 다시 0으로 사라지는 방식 — 측정 누적면적 5.561% · 움직인 프레임 17/23GODRICH 원작 · zip 에 SCSS·React 포함 · 다운로드 진행 상황 표시 9종 — 값이 퍼센트 단위로 갱신되는 아홉 가지 방식
이 데모가 속한 글
다운로드 진행 상황 표시 9종 — 값이 퍼센트 단위로 갱신되는 아홉 가지 방식다운로드 진행 상황 표시는 지금 받아지는 파일이 어느 상태에 있는지를 값 하나로 계산해 화면에 드러내는 인터랙션을 말하는데, 아홉 개 중 하나는 여러 파일이 동시에 받아질 때 각 파일의 진행 값이 목록에서 나란히 오 이 글의 데모 전부 보기

