08ロゴバー — 数に比例して速度を求め直しループ時間を揃えるバー
ロゴの数を数えて一周にかかる時間が常に同じになるよう流れる速度をその数に比例させて求め直し、ロゴが増えても減っても一周の再生時間は変わらないようにします。
使いどころクライアントロゴのマーキー、協業企業の信頼バー
getBoundingClientRecttranslateX--marquee-duration

<main class="stage">
<section class="panel">
<p class="hint" id="hint">ロゴの数が変わっても流れる速度(1秒あたりのピクセル)が同じになるよう、実際の描画幅を測って一周にかかる時間を毎回計算します</p>
<div class="marqueeWrap">
<div class="marqueeTrack" id="track">
<span class="logo" data-set="a">NOVA</span>
<span class="logo" data-set="a">ATLAS</span>
<span class="logo" data-set="a">COBALT</span>
<span class="logo" data-set="a">SILVERLINE</span>
<span class="logo" data-set="a">TERRA</span>
<span class="logo" data-set="a">PRISM</span>
<span class="logo" data-set="b" aria-hidden="true">NOVA</span>
<span class="logo" data-set="b" aria-hidden="true">ATLAS</span>
<span class="logo" data-set="b" aria-hidden="true">COBALT</span>
<span class="logo" data-set="b" aria-hidden="true">SILVERLINE</span>
<span class="logo" data-set="b" aria-hidden="true">TERRA</span>
<span class="logo" data-set="b" aria-hidden="true">PRISM</span>
</div>
</div>
<p class="speedNote" id="speedNote">計算された一周0.0秒</p>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop;
$easing: linear;
$color: $subject-ink;
@include stage($stage-paper);
.panel {
width: 100%;
max-width: 400px;
display: flex;
flex-direction: column;
gap: $sp-2;
}
.hint {
margin: 0;
text-align: center;
color: rgba($subject-ink, .72);
font-weight: 400;
font-size: 10px;
line-height: 1.35;
}
.marqueeWrap {
overflow: hidden;
border-radius: $r-control;
background: rgba($subject-cream, .55);
padding: $sp-3 0;
-webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}
.marqueeTrack {
display: flex;
width: max-content;
--marquee-duration: #{$dur-loop};
animation: marquee var(--marquee-duration) $easing infinite;
}
@keyframes marquee {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
.logo {
flex: 0 0 auto;
margin-right: $sp-6;
color: $color;
font-weight: 700;
font-size: 15px;
letter-spacing: .04em;
white-space: nowrap;
}
.speedNote {
margin: 0;
text-align: center;
color: rgba($subject-ink, .72);
font-weight: 500;
font-size: 9px;
font-variant-numeric: tabular-nums;
}
@media (max-width: 340px) {
.panel { max-width: 280px; gap: $sp-1; }
.logo { font-size: 13px; margin-right: $sp-4; }
.hint { font-size: 9px; }
}
@media (prefers-reduced-motion: reduce) {
.marqueeTrack {
animation-play-state: paused;
}
}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);
}
.panel {
width: 100%;
max-width: 400px;
display: flex;
flex-direction: column;
gap: 8px;
}
.hint {
margin: 0;
text-align: center;
color: rgba(23, 20, 26, 0.72);
font-weight: 400;
font-size: 10px;
line-height: 1.35;
}
.marqueeWrap {
overflow: hidden;
border-radius: 12px;
background: rgba(255, 247, 230, 0.55);
padding: 12px 0;
-webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}
.marqueeTrack {
display: flex;
width: max-content;
--marquee-duration: 2s;
animation: marquee var(--marquee-duration) linear infinite;
}
@keyframes marquee {
from {
transform: translateX(0);
}
to {
transform: translateX(-50%);
}
}
.logo {
flex: 0 0 auto;
margin-right: 24px;
color: #17141a;
font-weight: 700;
font-size: 15px;
letter-spacing: 0.04em;
white-space: nowrap;
}
.speedNote {
margin: 0;
text-align: center;
color: rgba(23, 20, 26, 0.72);
font-weight: 500;
font-size: 9px;
font-variant-numeric: tabular-nums;
}
@media (max-width: 340px) {
.panel {
max-width: 280px;
gap: 4px;
}
.logo {
font-size: 13px;
margin-right: 16px;
}
.hint {
font-size: 9px;
}
}
@media (prefers-reduced-motion: reduce) {
.marqueeTrack {
animation-play-state: paused;
}
}// ロゴ 바(08) — ロゴ 개수가 바뀌어도 체감 속도(초당 픽셀)가 같도록, 실제 렌더 폭을 재서
// 한 바퀴 시간을 매번 계산한다(계산 한 겹). 트랙을 통째로 2벌 두고 -50% 만큼 옮기는
// 무한 루프라 flex gap 대신 margin-right 로 간격을 준다(BD15).
(function () {
var track = document.getElementById('track');
var speedNote = document.getElementById('speedNote');
if (!track || !speedNote) return;
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var LOOP_SEC = 15; // 변형: 15 → 30 (기준 폭에서의 한 바퀴 시간)
var BASE_WIDTH = 560; // 기준 렌더 폭(px) — 이 폭일 때 LOOP_SEC 이 그대로 적용된다
var MIN_SEC = 8;
var MAX_SEC = 40;
function recompute() {
var setA = track.querySelectorAll('.logo[data-set="a"]');
var halfWidth = 0;
setA.forEach(function (el) {
var rect = el.getBoundingClientRect();
var style = getComputedStyle(el);
halfWidth += rect.width + parseFloat(style.marginRight || '0');
});
var duration = LOOP_SEC * (halfWidth / BASE_WIDTH);
duration = Math.max(MIN_SEC, Math.min(MAX_SEC, duration));
track.style.setProperty('--marquee-duration', duration.toFixed(2) + 's');
var pxPerSec = halfWidth / duration;
speedNote.textContent = 'ロゴ ' + setA.length + '個・計算された一周 ' + duration.toFixed(1) + '秒・毎秒 ' + Math.round(pxPerSec) + 'px';
}
recompute();
window.addEventListener('resize', recompute);
if (reduce) {
track.style.animationPlayState = 'paused';
}
})();// CaseLogoBar — vanilla 판(run/340/demo/08-case-logo-bar/index.html) 의 React 포트.
// 로고 개수가 바뀌어도 체감 속도(초당 픽셀)가 같도록, 실제 렌더 폭을 재서 한 바퀴 시간을
// 매번 계산한다. 트랙을 통째로 2벌 두고 -50% 만큼 옮기는 무한 루프라 flex gap 대신
// margin-right 로 간격을 준다(BD15).
import { useEffect, useRef, useState } from "react";
import styles from "./CaseLogoBar.module.scss";
export type CaseLogoBarProps = {
duration?: string;
easing?: string;
color?: string;
};
const HINT = "로고 개수가 바뀌어도 흐르는 속도(초당 픽셀)는 같도록, 실제 렌더 폭을 재서 한 바퀴 시간을 매번 계산합니다";
const LOGOS = ["NOVA", "ATLAS", "COBALT", "SILVERLINE", "TERRA", "PRISM"];
const LOOP_SEC = 15; // 변형: 15 → 30 (기준 폭에서의 한 바퀴 시간)
const BASE_WIDTH = 560; // 기준 렌더 폭(px) — 이 폭일 때 LOOP_SEC 이 그대로 적용된다
const MIN_SEC = 8;
const MAX_SEC = 40;
export default function CaseLogoBar({
duration = "2s",
easing = "linear",
color = "#17141a"
}: CaseLogoBarProps) {
const trackRef = useRef<HTMLDivElement | null>(null);
const [speedNote, setSpeedNote] = useState("계산된 한 바퀴 0.0초");
useEffect(() => {
const track = trackRef.current;
if (!track) return;
function recompute(): void {
if (!track) return;
const setA = track.querySelectorAll<HTMLElement>('[data-set="a"]');
let halfWidth = 0;
setA.forEach((el) => {
const rect = el.getBoundingClientRect();
const style = getComputedStyle(el);
halfWidth += rect.width + parseFloat(style.marginRight || "0");
});
let dur = LOOP_SEC * (halfWidth / BASE_WIDTH);
dur = Math.max(MIN_SEC, Math.min(MAX_SEC, dur));
track.style.setProperty("--marquee-duration", dur.toFixed(2) + "s");
const pxPerSec = halfWidth / dur;
setSpeedNote(
"로고 " + setA.length + "개 · 계산된 한 바퀴 " + dur.toFixed(1) + "초 · 초당 " + Math.round(pxPerSec) + "px"
);
}
recompute();
window.addEventListener("resize", recompute);
return () => window.removeEventListener("resize", recompute);
}, []);
return (
<main className={styles.stage}>
<section className={styles.panel}>
<p className={styles.hint}>{HINT}</p>
<div className={styles.marqueeWrap}>
<div className={styles.marqueeTrack} ref={trackRef}>
{LOGOS.map((name) => (
<span key={"a-" + name} className={styles.logo} data-set="a">
{name}
</span>
))}
{LOGOS.map((name) => (
<span key={"b-" + name} className={styles.logo} data-set="b" aria-hidden="true">
{name}
</span>
))}
</div>
</div>
<p className={styles.speedNote}>{speedNote}</p>
</section>
</main>
);
}08 ロゴバー — 数に比例して速度を求め直しループ時間を揃えるバー — 計測 累積面積 3.005% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · 導入事例 web デザイン9選 — 成果数値まで計算して見せる
このデモが属する記事
導入事例 web デザイン9選 — 成果数値まで計算して見せる導入事例 web デザインは色や余白より先に、数字がどう動くかで差がつきます。画面に入った瞬間から成果数値がフレームごとに求め直されカウントアップし、事例ごとに計算したインパクトスコアでグリッドタイルの大きさが決まり、引用文の長さに合わせて この記事のデモをすべて見る


