09Player Hero Scene Where a Blurred Backdrop and Ring Interlock
A color value sampled from the album art blurs into an oversized backdrop, and the turntable, waveform bars, and spectrum ring -- three separately computed layers -- share one coordinate space so a single playback-progress value drives all three into a hero scene.
Best forLanding hero sections and album release pages
bandValuedrawRingRING_POINTS

<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<defs>
<symbol id="ico-play" viewBox="0 0 256 256"><g fill="currentColor"><path d="M228.23 134.69L84.15 222.81A8 8 0 0 1 72 216.12V39.88a8 8 0 0 1 12.15-6.69l144.08 88.12a7.82 7.82 0 0 1 0 13.38" opacity=".2"/><path d="M232.4 114.49L88.32 26.35a16 16 0 0 0-16.2-.3A15.86 15.86 0 0 0 64 39.87v176.26A15.94 15.94 0 0 0 80 232a16.07 16.07 0 0 0 8.36-2.35l144.04-88.14a15.81 15.81 0 0 0 0-27ZM80 215.94V40l143.83 88Z"/></g></symbol>
<symbol id="ico-pause" viewBox="0 0 256 256"><g fill="currentColor"><path d="M208 48v160a8 8 0 0 1-8 8h-40a8 8 0 0 1-8-8V48a8 8 0 0 1 8-8h40a8 8 0 0 1 8 8M96 40H56a8 8 0 0 0-8 8v160a8 8 0 0 0 8 8h40a8 8 0 0 0 8-8V48a8 8 0 0 0-8-8" opacity=".2"/><path d="M200 32h-40a16 16 0 0 0-16 16v160a16 16 0 0 0 16 16h40a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16m0 176h-40V48h40ZM96 32H56a16 16 0 0 0-16 16v160a16 16 0 0 0 16 16h40a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16m0 176H56V48h40Z"/></g></symbol>
</defs>
</svg>
<main class="stage">
<div class="hero is-demo is-playing" id="hero">
<p class="hero__hint"><span>Press play and the bass/mid/treble values simultaneously drive the backdrop blur, ring, and bars.</span></p>
<div class="hero__panel" id="panel">
<div class="hero__backdrop" aria-hidden="true">
<div class="hero__blob hero__blob--a" id="blobA"></div>
<div class="hero__blob hero__blob--b" id="blobB"></div>
<div class="hero__blob hero__blob--c" id="blobC"></div>
</div>
<button class="hero__play" id="play" type="button" aria-label="Play">
<svg class="hero__ico hero__ico--play" aria-hidden="true"><use href="#ico-play"></use></svg>
<svg class="hero__ico hero__ico--pause" aria-hidden="true"><use href="#ico-pause"></use></svg>
</button>
<div class="hero__core">
<div class="hero__ring-wrap">
<canvas class="hero__ring" id="ring" width="128" height="128" aria-hidden="true"></canvas>
<div class="hero__art" id="art"></div>
<span class="hero__colorref" aria-hidden="true"></span>
</div>
</div>
<div class="hero__bars" id="bars" aria-hidden="true"></div>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 겹(대역 값 → 블러·링·막대 동시 반응)은 index.html 의 render()/drawRing() 에 있다. 여기는 무대·레이아웃만.
$duration: $dur-base;
$easing: $ease-out;
$color: $subject-blue-light;
@include stage($stage-ink);
.hero {
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-2;
width: min(420px, 94vw);
}
.hero__hint {
margin: 0;
color: rgba($subject-cream, .78);
font-size: 12px;
font-weight: 500;
line-height: 1.35;
text-align: center;
}
.hero__panel {
position: relative;
width: 100%;
height: 190px;
border-radius: $r-card;
overflow: hidden;
background: rgba($subject-cream, .04);
box-shadow: 0 0 0 1px rgba($subject-cream, .14);
}
.hero__backdrop {
position: absolute;
inset: 0;
overflow: hidden;
}
.hero__blob {
position: absolute;
width: 130px;
height: 130px;
border-radius: $r-pill;
filter: blur(28px);
opacity: .3;
}
.hero__blob--a { left: 8%; top: 6%; background: $subject-mint; }
.hero__blob--b { right: 6%; top: 2%; background: $subject-pink; }
.hero__blob--c { left: 30%; bottom: -12%; background: $subject-sky; }
.hero__play {
position: absolute;
right: $sp-3;
top: $sp-3;
z-index: 3;
width: $sp-8;
height: $sp-8;
border-radius: $r-pill;
border: none;
background: $subject-cream;
color: $stage-ink;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: transform $duration $easing;
}
.hero__play:hover { transform: scale(1.06); }
.hero__play:focus-visible { outline: 2px solid $subject-cream; outline-offset: 2px; }
.hero__play:active { transform: scale(.96); }
.hero__ico { width: 15px; height: 15px; }
.hero__ico--pause { display: none; }
.is-playing .hero__ico--play { display: none; }
.is-playing .hero__ico--pause { display: block; }
.hero__core {
position: absolute;
inset: 0 0 34px 0;
display: grid;
place-items: center;
z-index: 1;
}
.hero__ring-wrap {
position: relative;
width: 128px;
height: 128px;
display: grid;
place-items: center;
}
.hero__ring {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.hero__art {
width: 56px;
height: 56px;
border-radius: $r-pill;
background: $color;
box-shadow: 0 0 0 1px rgba($subject-cream, .2);
}
.hero__colorref {
display: none;
color: $color;
}
.hero__bars {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 28px;
z-index: 1;
display: flex;
align-items: flex-end;
justify-content: center;
gap: 4px;
padding: 0 $sp-4 $sp-2;
}
.hero__bar {
flex: 1 1 auto;
max-width: 6px;
height: 100%;
border-radius: $r-xs $r-xs 0 0;
background: rgba($subject-cream, .55);
transform: scaleY(.2);
transform-origin: bottom center; // 막대는 바닥이 기준점 — 중심 기준이 아니다
}
@media (max-width: 340px) {
.hero__panel { height: 108px; }
.hero__ring-wrap { width: 76px; height: 76px; }
.hero__art { width: 32px; height: 32px; }
.hero__bars { height: 20px; }
.hero__play { width: 28px; height: 28px; }
.hero__blob { width: 88px; height: 88px; }
}
@media (prefers-reduced-motion: reduce) {
.hero__play { 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);
}
.hero {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
width: min(420px, 94vw);
}
.hero__hint {
margin: 0;
color: rgba(255, 247, 230, 0.78);
font-size: 12px;
font-weight: 500;
line-height: 1.35;
text-align: center;
}
.hero__panel {
position: relative;
width: 100%;
height: 190px;
border-radius: 16px;
overflow: hidden;
background: rgba(255, 247, 230, 0.04);
box-shadow: 0 0 0 1px rgba(255, 247, 230, 0.14);
}
.hero__backdrop {
position: absolute;
inset: 0;
overflow: hidden;
}
.hero__blob {
position: absolute;
width: 130px;
height: 130px;
border-radius: 999px;
filter: blur(28px);
opacity: 0.3;
}
.hero__blob--a {
left: 8%;
top: 6%;
background: #4cd4a6;
}
.hero__blob--b {
right: 6%;
top: 2%;
background: #ff9ebb;
}
.hero__blob--c {
left: 30%;
bottom: -12%;
background: #6ec8ff;
}
.hero__play {
position: absolute;
right: 12px;
top: 12px;
z-index: 3;
width: 32px;
height: 32px;
border-radius: 999px;
border: none;
background: #fff7e6;
color: #17141a;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: transform 300ms ease-out;
}
.hero__play:hover {
transform: scale(1.06);
}
.hero__play:focus-visible {
outline: 2px solid #fff7e6;
outline-offset: 2px;
}
.hero__play:active {
transform: scale(0.96);
}
.hero__ico {
width: 15px;
height: 15px;
}
.hero__ico--pause {
display: none;
}
.is-playing .hero__ico--play {
display: none;
}
.is-playing .hero__ico--pause {
display: block;
}
.hero__core {
position: absolute;
inset: 0 0 34px 0;
display: grid;
place-items: center;
z-index: 1;
}
.hero__ring-wrap {
position: relative;
width: 128px;
height: 128px;
display: grid;
place-items: center;
}
.hero__ring {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.hero__art {
width: 56px;
height: 56px;
border-radius: 999px;
background: #5b8cf8;
box-shadow: 0 0 0 1px rgba(255, 247, 230, 0.2);
}
.hero__colorref {
display: none;
color: #5b8cf8;
}
.hero__bars {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 28px;
z-index: 1;
display: flex;
align-items: flex-end;
justify-content: center;
gap: 4px;
padding: 0 16px 8px;
}
.hero__bar {
flex: 1 1 auto;
max-width: 6px;
height: 100%;
border-radius: 4px 4px 0 0;
background: rgba(255, 247, 230, 0.55);
transform: scaleY(0.2);
transform-origin: bottom center;
}
@media (max-width: 340px) {
.hero__panel {
height: 108px;
}
.hero__ring-wrap {
width: 76px;
height: 76px;
}
.hero__art {
width: 32px;
height: 32px;
}
.hero__bars {
height: 20px;
}
.hero__play {
width: 28px;
height: 28px;
}
.hero__blob {
width: 88px;
height: 88px;
}
}
@media (prefers-reduced-motion: reduce) {
.hero__play {
transition: none;
}
}(function () {
var root = document.getElementById('hero');
var playBtn = document.getElementById('play');
var canvas = document.getElementById('ring');
var ctx = canvas.getContext('2d');
var art = document.getElementById('art');
var colorRef = document.querySelector('.hero__colorref');
var blobA = document.getElementById('blobA');
var blobB = document.getElementById('blobB');
var blobC = document.getElementById('blobC');
var barsEl = document.getElementById('bars');
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var isDemo = true;
// 결정론 난수 — Math.random 을 쓰지 않는다. LCG(선형합동생성기) 하나로 대역·막대·링 점의 위상을 만든다.
function makeLcg(seed) {
var s = seed >>> 0;
return function () { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
}
var rng = makeLcg(5527);
// 배경 블러·링·막대가 같은 저·중·Treble 대역 값에 맞물리도록, 대역마다 사인 두 겹을 겹쳐 값을 하나씩만 계산한다.
var bandDefs = {
bass: { f1: 0.28 + rng() * 0.08, p1: rng() * Math.PI * 2, f2: 0.7 + rng() * 0.2, p2: rng() * Math.PI * 2, w2: 0.3 },
mid: { f1: 0.75 + rng() * 0.15, p1: rng() * Math.PI * 2, f2: 1.6 + rng() * 0.3, p2: rng() * Math.PI * 2, w2: 0.32 },
treble: { f1: 1.6 + rng() * 0.3, p1: rng() * Math.PI * 2, f2: 2.6 + rng() * 0.4, p2: rng() * Math.PI * 2, w2: 0.34 }
};
function bandValue(def, tSec) {
var s1 = Math.sin(tSec * def.f1 * Math.PI * 2 + def.p1);
var s2 = Math.sin(tSec * def.f2 * Math.PI * 2 + def.p2) * def.w2;
var v = (s1 + s2) / (1 + def.w2); // -1..1
return 0.5 + v * 0.5; // 0..1
}
var RING_POINTS = 32;
var ringPts = [];
for (var i = 0; i < RING_POINTS; i++) {
ringPts.push({ f: 2 + rng() * 1.4, p: rng() * Math.PI * 2, w: 0.35 + rng() * 0.3 });
}
var BAR_COUNT = 14;
var bars = [];
for (var i = 0; i < BAR_COUNT; i++) {
var el = document.createElement('div');
el.className = 'hero__bar';
barsEl.appendChild(el);
bars.push({ el: el, f: 3 + rng() * 1.6, p: rng() * Math.PI * 2, base: 0.18 + rng() * 0.1 });
}
function human() { if (isDemo) { root.classList.remove('is-demo'); isDemo = false; } }
var playing = true, t0 = 0;
var ringColor = '#3a3a3a';
function drawRing(tSec, treb) {
var w = canvas.width, h = canvas.height, cx = w / 2, cy = h / 2;
var innerR = 34, rangeR = 16;
ctx.clearRect(0, 0, w, h);
ctx.lineCap = 'round';
ctx.strokeStyle = ringColor;
for (var i = 0; i < RING_POINTS; i++) {
var pt = ringPts[i];
var s = Math.sin(tSec * pt.f + pt.p) * 0.5 + 0.5;
var v = 0.15 + s * pt.w * (0.4 + treb * 0.9);
var angle = (i / RING_POINTS) * Math.PI * 2 - Math.PI / 2;
var cos = Math.cos(angle), sin = Math.sin(angle);
var r2 = innerR + rangeR * Math.max(0.05, v);
ctx.globalAlpha = 0.3 + v * 0.6;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(cx + cos * innerR, cy + sin * innerR);
ctx.lineTo(cx + cos * r2, cy + sin * r2);
ctx.stroke();
}
ctx.globalAlpha = 1;
}
function render(now, playingNow) {
var tSec = (now - t0) / 1000;
var bass = playingNow ? bandValue(bandDefs.bass, tSec) : 0.2;
var mid = playingNow ? bandValue(bandDefs.mid, tSec) : 0.2;
var treb = playingNow ? bandValue(bandDefs.treble, tSec) : 0.2;
// 배경 블러 세 덩이는 서로 다른 대역 값을 따라 각자 다른 방향으로 흐른다 — transform/opacity 만 사용
blobA.style.transform = 'translate(' + (Math.cos(tSec * 0.6) * 18 * (0.4 + bass)).toFixed(1) + 'px,' + (Math.sin(tSec * 0.5) * 14 * (0.4 + bass)).toFixed(1) + 'px)';
blobA.style.opacity = String(0.35 + bass * 0.35);
blobB.style.transform = 'translate(' + (Math.cos(tSec * 0.4 + 2) * 16 * (0.4 + mid)).toFixed(1) + 'px,' + (Math.sin(tSec * 0.7 + 1) * 18 * (0.4 + mid)).toFixed(1) + 'px)';
blobB.style.opacity = String(0.3 + mid * 0.35);
blobC.style.transform = 'translate(' + (Math.cos(tSec * 0.9 + 4) * 14 * (0.4 + treb)).toFixed(1) + 'px,' + (Math.sin(tSec * 0.8 + 3) * 12 * (0.4 + treb)).toFixed(1) + 'px)';
blobC.style.opacity = String(0.28 + treb * 0.32);
// 앨범 아트는 Bass 값에 맞춰 살짝 커졌다 줄어든다 — 킥 펄스
art.style.transform = 'scale(' + (1 + bass * 0.08).toFixed(3) + ')';
drawRing(tSec, treb);
for (var i = 0; i < bars.length; i++) {
var bar = bars[i];
var s = Math.sin(tSec * bar.f + bar.p) * 0.5 + 0.5;
var h = playingNow ? Math.max(0.08, bar.base + s * treb * 0.9) : bar.base;
bar.el.style.transform = 'scaleY(' + h.toFixed(3) + ')';
}
}
function setPlaying(next) { playing = next; root.classList.toggle('is-playing', next); }
function onPlayClick() { human(); setPlaying(!playing); }
function onPlayKeydown(e) {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); human(); setPlaying(!playing); }
}
function frame(now) {
if (!t0) t0 = now;
render(now, isDemo ? true : playing);
requestAnimationFrame(frame);
}
ringColor = getComputedStyle(colorRef).color || ringColor;
if (reduce) {
root.classList.add('is-still');
t0 = performance.now();
render(t0, false);
playBtn.addEventListener('click', function () { human(); setPlaying(!playing); render(performance.now(), playing); });
playBtn.addEventListener('keydown', function (e) {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); human(); setPlaying(!playing); render(performance.now(), playing); }
});
} else {
playBtn.addEventListener('click', onPlayClick);
playBtn.addEventListener('keydown', onPlayKeydown);
requestAnimationFrame(frame);
}
})();import { useEffect, useRef } from "react";
import styles from "./HeroScene.module.scss";
type RingPoint = { f: number; p: number; w: number };
type Bar = { el: HTMLDivElement | null; f: number; p: number; base: number };
type BandDef = { f1: number; p1: number; f2: number; p2: number; w2: number };
export default function HeroScene() {
const rootRef = useRef<HTMLDivElement | null>(null);
const playRef = useRef<HTMLButtonElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const artRef = useRef<HTMLDivElement | null>(null);
const colorRefEl = useRef<HTMLSpanElement | null>(null);
const blobARef = useRef<HTMLDivElement | null>(null);
const blobBRef = useRef<HTMLDivElement | null>(null);
const blobCRef = useRef<HTMLDivElement | null>(null);
const barsElRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const root = rootRef.current;
const playBtn = playRef.current;
const canvas = canvasRef.current;
const art = artRef.current;
const colorRef = colorRefEl.current;
const blobA = blobARef.current;
const blobB = blobBRef.current;
const blobC = blobCRef.current;
const barsEl = barsElRef.current;
if (!root || !playBtn || !canvas || !art || !colorRef || !blobA || !blobB || !blobC || !barsEl) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let isDemo = true;
// 결정론 난수 — Math.random 을 쓰지 않는다. LCG(선형합동생성기) 하나로 대역·막대·링 점의 위상을 만든다.
function makeLcg(seed: number) {
let s = seed >>> 0;
return function () { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
}
const rng = makeLcg(5527);
// 배경 블러·링·막대가 같은 저·중·고음 대역 값에 맞물리도록, 대역마다 사인 두 겹을 겹쳐 값을 하나씩만 계산한다.
const bandDefs: { bass: BandDef; mid: BandDef; treble: BandDef } = {
bass: { f1: 0.28 + rng() * 0.08, p1: rng() * Math.PI * 2, f2: 0.7 + rng() * 0.2, p2: rng() * Math.PI * 2, w2: 0.3 },
mid: { f1: 0.75 + rng() * 0.15, p1: rng() * Math.PI * 2, f2: 1.6 + rng() * 0.3, p2: rng() * Math.PI * 2, w2: 0.32 },
treble: { f1: 1.6 + rng() * 0.3, p1: rng() * Math.PI * 2, f2: 2.6 + rng() * 0.4, p2: rng() * Math.PI * 2, w2: 0.34 },
};
function bandValue(def: BandDef, tSec: number) {
const s1 = Math.sin(tSec * def.f1 * Math.PI * 2 + def.p1);
const s2 = Math.sin(tSec * def.f2 * Math.PI * 2 + def.p2) * def.w2;
const v = (s1 + s2) / (1 + def.w2); // -1..1
return 0.5 + v * 0.5; // 0..1
}
const RING_POINTS = 32;
const ringPts: RingPoint[] = [];
for (let i = 0; i < RING_POINTS; i++) {
ringPts.push({ f: 2 + rng() * 1.4, p: rng() * Math.PI * 2, w: 0.35 + rng() * 0.3 });
}
const BAR_COUNT = 14;
const bars: Bar[] = [];
for (let i = 0; i < BAR_COUNT; i++) {
const el = document.createElement("div");
el.className = styles.bar;
barsEl.appendChild(el);
bars.push({ el, f: 3 + rng() * 1.6, p: rng() * Math.PI * 2, base: 0.18 + rng() * 0.1 });
}
function human() { if (isDemo) { root!.classList.remove(styles["is-demo"]); isDemo = false; } }
let playing = true, t0 = 0;
let ringColor = "#3a3a3a";
function drawRing(tSec: number, treb: number) {
const w = canvas!.width, h = canvas!.height, cx = w / 2, cy = h / 2;
const innerR = 34, rangeR = 16;
ctx!.clearRect(0, 0, w, h);
ctx!.lineCap = "round";
ctx!.strokeStyle = ringColor;
for (let i = 0; i < RING_POINTS; i++) {
const pt = ringPts[i];
const s = Math.sin(tSec * pt.f + pt.p) * 0.5 + 0.5;
const v = 0.15 + s * pt.w * (0.4 + treb * 0.9);
const angle = (i / RING_POINTS) * Math.PI * 2 - Math.PI / 2;
const cos = Math.cos(angle), sin = Math.sin(angle);
const r2 = innerR + rangeR * Math.max(0.05, v);
ctx!.globalAlpha = 0.3 + v * 0.6;
ctx!.lineWidth = 2;
ctx!.beginPath();
ctx!.moveTo(cx + cos * innerR, cy + sin * innerR);
ctx!.lineTo(cx + cos * r2, cy + sin * r2);
ctx!.stroke();
}
ctx!.globalAlpha = 1;
}
function render(now: number, playingNow: boolean) {
const tSec = (now - t0) / 1000;
const bass = playingNow ? bandValue(bandDefs.bass, tSec) : 0.2;
const mid = playingNow ? bandValue(bandDefs.mid, tSec) : 0.2;
const treb = playingNow ? bandValue(bandDefs.treble, tSec) : 0.2;
// 배경 블러 세 덩이는 서로 다른 대역 값을 따라 각자 다른 방향으로 흐른다 — transform/opacity 만 사용
blobA!.style.transform = "translate(" + (Math.cos(tSec * 0.6) * 18 * (0.4 + bass)).toFixed(1) + "px," + (Math.sin(tSec * 0.5) * 14 * (0.4 + bass)).toFixed(1) + "px)";
blobA!.style.opacity = String(0.35 + bass * 0.35);
blobB!.style.transform = "translate(" + (Math.cos(tSec * 0.4 + 2) * 16 * (0.4 + mid)).toFixed(1) + "px," + (Math.sin(tSec * 0.7 + 1) * 18 * (0.4 + mid)).toFixed(1) + "px)";
blobB!.style.opacity = String(0.3 + mid * 0.35);
blobC!.style.transform = "translate(" + (Math.cos(tSec * 0.9 + 4) * 14 * (0.4 + treb)).toFixed(1) + "px," + (Math.sin(tSec * 0.8 + 3) * 12 * (0.4 + treb)).toFixed(1) + "px)";
blobC!.style.opacity = String(0.28 + treb * 0.32);
// 앨범 아트는 저음 값에 맞춰 살짝 커졌다 줄어든다 — 킥 펄스
art!.style.transform = "scale(" + (1 + bass * 0.08).toFixed(3) + ")";
drawRing(tSec, treb);
for (let i = 0; i < bars.length; i++) {
const bar = bars[i];
const s = Math.sin(tSec * bar.f + bar.p) * 0.5 + 0.5;
const h = playingNow ? Math.max(0.08, bar.base + s * treb * 0.9) : bar.base;
bar.el!.style.transform = "scaleY(" + h.toFixed(3) + ")";
}
}
function setPlaying(next: boolean) { playing = next; root!.classList.toggle(styles["is-playing"], next); }
function onPlayClick() { human(); setPlaying(!playing); }
function onPlayKeydown(e: KeyboardEvent) {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); human(); setPlaying(!playing); }
}
let raf = 0;
function frame(now: number) {
if (!t0) t0 = now;
render(now, isDemo ? true : playing);
raf = requestAnimationFrame(frame);
}
ringColor = getComputedStyle(colorRef).color || ringColor;
function onReduceClick() { human(); setPlaying(!playing); render(performance.now(), playing); }
function onReduceKeydown(e: KeyboardEvent) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault(); human(); setPlaying(!playing);
render(performance.now(), playing);
}
}
if (reduce) {
root.classList.add(styles["is-still"]);
t0 = performance.now();
render(t0, false);
playBtn.addEventListener("click", onReduceClick);
playBtn.addEventListener("keydown", onReduceKeydown);
} else {
playBtn.addEventListener("click", onPlayClick);
playBtn.addEventListener("keydown", onPlayKeydown);
raf = requestAnimationFrame(frame);
}
return () => {
cancelAnimationFrame(raf);
if (reduce) {
playBtn.removeEventListener("click", onReduceClick);
playBtn.removeEventListener("keydown", onReduceKeydown);
} else {
playBtn.removeEventListener("click", onPlayClick);
playBtn.removeEventListener("keydown", onPlayKeydown);
}
barsEl.innerHTML = "";
};
}, []);
return (
<main className={styles.stage}>
<div className={styles.hero + " " + styles["is-demo"] + " " + styles["is-playing"]} ref={rootRef}>
<p className={styles.hint}><span>재생하면 저·중·고음 값이 배경 블러·링·막대에 동시에 맞물려 번집니다</span></p>
<div className={styles.panel}>
<div className={styles.backdrop} aria-hidden="true">
<div className={styles.blob + " " + styles.blobA} ref={blobARef} />
<div className={styles.blob + " " + styles.blobB} ref={blobBRef} />
<div className={styles.blob + " " + styles.blobC} ref={blobCRef} />
</div>
<button className={styles.play} ref={playRef} type="button" aria-label="재생">
<svg className={`${styles.ico} ${styles.icoPlay}`} aria-hidden="true"><use href="#ico-play" /></svg>
<svg className={`${styles.ico} ${styles.icoPause}`} aria-hidden="true"><use href="#ico-pause" /></svg>
</button>
<div className={styles.core}>
<div className={styles.ringWrap}>
<canvas className={styles.ring} ref={canvasRef} width={128} height={128} aria-hidden="true" />
<div className={styles.art} ref={artRef} />
<span className={styles.colorref} ref={colorRefEl} aria-hidden="true" />
</div>
</div>
<div className={styles.bars} ref={barsElRef} aria-hidden="true" />
</div>
</div>
</main>
);
}09 Player Hero Scene Where a Blurred Backdrop and Ring Interlock — Measured area 39.284% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · Custom Audio Player CSS — 9 Computed UI Patterns
The post this demo lives in
Custom Audio Player CSS — 9 Computed UI PatternsAmong nine custom audio player css demos, one precomputes bar heights from sine waves, one decays angular velocity so th See all demos in this post
If you like this effect - similar demos
Creative UI09 A Desk Hero Where the Sun Sets and a Lamp Turns OnCSS Shadow Effects: 9 Demos That Compute the Cast
Creative UI09 A Hero Backdrop Where Three Layers Grow From One SeedGenerative Art JavaScript: 9 Patterns That Grow
Creative UI09 Fractal hero scene — a front-door stage layered with type and starlightMandelbrot Zoom, 9 Ways (Escape-Time, No Library)