打ち上げカウントダウン・ローダー
HUD風の3・2・1カウントダウンからロケットが発射し、星が流線になるワープで読み込み完了を告げるローダー演出です。待ち時間をミッション開始の高揚感に変える、体験型サイトの導入に。
ライブデモ
使用例(お題: アイドルグループ Sakura)
この技法を「アイドルグループ Sakura」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- Sakura: ライブ開演前のカウントダウン。発射→ワープで開演を告げる -->
<div class="stage" data-loader aria-label="ライブ開演カウントダウン">
<canvas class="stars" aria-hidden="true"></canvas>
<p class="caption">LIVE OPENING<span class="dots">...</span></p>
<svg class="ring" viewBox="0 0 160 160" aria-hidden="true">
<circle class="ring-bg" cx="80" cy="80" r="70" fill="none" stroke="rgba(255,255,255,.12)" stroke-width="6"/>
<circle class="ring-fg" cx="80" cy="80" r="70" fill="none" stroke="#FF7DA8" stroke-width="6" stroke-linecap="round" stroke-dasharray="440"/>
</svg>
<div class="count" aria-hidden="true">3</div>
<svg class="rocket" viewBox="0 0 40 72" aria-hidden="true">
<path d="M12 18 L20 2 L28 18 Z" fill="#FF7DA8"/>
<rect x="10" y="16" width="20" height="40" rx="10" fill="#F2F2F2"/>
<circle cx="20" cy="30" r="5" fill="#9B6BFF"/>
<path d="M10 46 L4 60 L10 56 Z" fill="#FF7DA8"/>
<path d="M30 46 L36 60 L30 56 Z" fill="#FF7DA8"/>
<path class="flame" d="M14 54 L20 72 L26 54 Z" fill="#FFE3B0"/>
</svg>
<div class="loaded" aria-hidden="true">WELCOME</div>
</div>
CSS
* { box-sizing: border-box; }
html, body { margin: 0; width: 100%; min-height: 100%; background: #14081e; }
.stage {
position: relative; width: 100%; height: 100vh;
min-height: 260px; max-height: 100%; overflow: hidden;
background: #14081e;
font-family: system-ui, -apple-system, "Segoe UI", sans-serif; color: #F2F2F2;
}
.stars { position: absolute; inset: 0; z-index: 0; display: block; }
.caption {
position: absolute; left: 50%; top: 15%; transform: translateX(-50%);
z-index: 1; margin: 0; font-size: 12px; letter-spacing: .26em; color: rgba(255,209,224,.7);
}
.ring {
position: absolute; left: 50%; top: 50%;
width: 180px; height: 180px;
transform: translate(-50%, -50%) rotate(-90deg); z-index: 1;
}
.count {
position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 2;
font-size: 96px; font-weight: 900; line-height: 1; font-variant-numeric: tabular-nums; color: #fff;
}
.rocket { position: absolute; left: 50%; top: 60%; width: 56px; height: auto; transform: translate(-50%, -50%); z-index: 2; }
.flame { transform-box: fill-box; transform-origin: center top; animation: flame .12s steps(3) infinite alternate; }
@keyframes flame { from { transform: scaleY(.7); } to { transform: scaleY(1.2); } }
.loaded {
position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 2;
font-size: 28px; font-weight: 700; letter-spacing: .4em; text-indent: .4em; color: #FFD9E0; opacity: 0;
}
JavaScript
// 星空canvas+数字/リング/ロケット。フェーズは elapsed 1本で8s分割(デモと同じロジック)。
(() => {
const stage = document.querySelector('[data-loader]');
if (!stage) return;
const cv = stage.querySelector('.stars');
const ctx = cv.getContext('2d');
const numEl = stage.querySelector('.count');
const ringFg = stage.querySelector('.ring-fg');
const ring = stage.querySelector('.ring');
const rocket = stage.querySelector('.rocket');
const loaded = stage.querySelector('.loaded');
const easeOutExpo = t => (t >= 1 ? 1 : 1 - Math.pow(2, -10 * t));
const clamp01 = t => Math.max(0, Math.min(1, t));
let W = 0, H = 0, dpr = 1;
const stars = [];
const initStars = () => {
stars.length = 0;
for (let i = 0; i < 120; i++) stars.push({ xr: ((i * 137.5) % 100) / 100, y: ((i * 53) % 100) / 100 * H, size: 1 + (i % 2) });
};
const resize = () => {
dpr = Math.min(window.devicePixelRatio || 1, 2);
W = cv.clientWidth; H = cv.clientHeight;
cv.width = W * dpr; cv.height = H * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
initStars();
};
window.addEventListener('resize', resize);
resize();
const T = 8, RING = 440;
let start = 0;
const loop = (now) => {
if (!start) start = now;
const e = ((now - start) / 1000) % T;
let warp = 0;
if (e >= 4.2 && e < 6) warp = 1 + 17 * ((e - 4.2) / 1.8);
const step = warp > 0 ? warp : 0.4;
ctx.fillStyle = warp > 0 ? 'rgba(20,8,30,0.3)' : '#14081e';
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = '#fff'; ctx.strokeStyle = '#fff';
for (const s of stars) {
const x = s.xr * W;
const prevY = s.y;
let y = prevY + step;
let wrapped = false;
if (y > H) { y -= H; wrapped = true; }
s.y = y;
if (warp > 0 && !wrapped) { ctx.lineWidth = s.size; ctx.beginPath(); ctx.moveTo(x, prevY); ctx.lineTo(x, y); ctx.stroke(); }
else { ctx.fillRect(x - s.size / 2, y - s.size / 2, s.size, s.size); }
}
stage.style.opacity = e > 7.7 ? clamp01(1 - (e - 7.7) / 0.3) : 1;
if (e < 3) {
const n = 3 - Math.floor(e);
const p = e - Math.floor(e);
const a = clamp01(p / 0.3);
numEl.textContent = n;
numEl.style.opacity = easeOutExpo(a);
numEl.style.transform = `translate(-50%, -50%) scale(${1.4 - 0.4 * easeOutExpo(a)})`;
ring.style.opacity = 1;
ringFg.style.strokeDashoffset = RING * (1 - p);
rocket.style.opacity = 1;
rocket.style.transform = 'translate(-50%, -50%)';
loaded.style.opacity = 0;
} else if (e < 4.2) {
numEl.style.opacity = 0; ring.style.opacity = 0;
const lp = clamp01((e - 3) / 1.2);
const ease = lp * lp;
rocket.style.opacity = 1;
rocket.style.transform = `translate(-50%, -50%) translateY(${-130 * ease}vh)`;
loaded.style.opacity = 0;
} else if (e < 6) {
rocket.style.opacity = 0; numEl.style.opacity = 0; ring.style.opacity = 0; loaded.style.opacity = 0;
} else if (e < 7) {
const lp = clamp01((e - 6) / 0.6);
loaded.style.opacity = easeOutExpo(lp);
loaded.style.letterSpacing = (0.4 - 0.3 * easeOutExpo(lp)) + 'em';
} else {
loaded.style.opacity = 1; loaded.style.letterSpacing = '0.1em';
}
requestAnimationFrame(loop);
};
requestAnimationFrame(loop);
})();
実装ガイド
使いどころ
体験型サイトやイベントの導入ローダーに。待ち時間を「開始」の高揚感に変えます。
実装時の注意点
星空はcanvas、数字/リング/ロケットはDOM+SVGのハイブリッド構成です(全canvasより役割分担が楽)。フェーズはelapsed時間1本で8sを分割します(複数タイマー併用は事故る)。ワープ中は星を「前フレーム→現位置」の線分で描き、線幅=星サイズにしないと細くて消えます。残像は半透明塗りで表現します。
対応ブラウザ
Canvas 2D・SVG・requestAnimationFrameは全モダンブラウザで安定動作します。tabular-numsで数字幅を固定し、実際のローダーでは読み込み完了イベントと演出の終端を同期させます。対応は実機で確認してください。
よくある失敗
フェーズを複数タイマーで分けるとズレるので経過時間ひとつで分岐します。ワープの線幅を細いままにすると流線が消えます。文字とUIを全部canvasに描くと管理が煩雑になるためDOMと分担します。
応用例
配色やキャプションをテーマに、カウント秒数を実ロードに連動、ロケットをブランド要素に、完了後に本編へフェードなどに発展できます。
コード
HTML
<!-- 打ち上げカウントダウン・ローダー:3・2・1→発射→ワープで読み込み完了を告げる -->
<div class="stage" data-loader aria-label="打ち上げカウントダウンのローダー">
<canvas class="stars" aria-hidden="true"></canvas>
<p class="caption">INITIALIZING<span class="dots">...</span></p>
<svg class="ring" viewBox="0 0 160 160" aria-hidden="true">
<circle class="ring-bg" cx="80" cy="80" r="70" fill="none" stroke="rgba(255,255,255,.12)" stroke-width="6"/>
<circle class="ring-fg" cx="80" cy="80" r="70" fill="none" stroke="#4ED1A1" stroke-width="6" stroke-linecap="round" stroke-dasharray="440"/>
</svg>
<div class="count" aria-hidden="true">3</div>
<svg class="rocket" viewBox="0 0 40 72" aria-hidden="true">
<path d="M12 18 L20 2 L28 18 Z" fill="#FF5C5C"/>
<rect x="10" y="16" width="20" height="40" rx="10" fill="#F2F2F2"/>
<circle cx="20" cy="30" r="5" fill="#4D7CFE"/>
<path d="M10 46 L4 60 L10 56 Z" fill="#FF5C5C"/>
<path d="M30 46 L36 60 L30 56 Z" fill="#FF5C5C"/>
<path class="flame" d="M14 54 L20 72 L26 54 Z" fill="#FFC83D"/>
</svg>
<div class="loaded" aria-hidden="true">LOADED</div>
</div>
CSS
:root{
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
--ease-inout: cubic-bezier(0.65, 0, 0.35, 1);
}
* { box-sizing: border-box; }
html, body { margin: 0; width: 100%; min-height: 100%; background: #0F1117; }
.stage {
position: relative;
width: 100%;
height: 100vh;
min-height: 260px;
max-height: 100%;
overflow: hidden;
background: #0F1117;
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
color: #F2F2F2;
}
.stars { position: absolute; inset: 0; z-index: 0; display: block; }
.caption {
position: absolute; left: 50%; top: 15%; transform: translateX(-50%);
z-index: 1; margin: 0;
font-size: 12px; letter-spacing: .26em; color: rgba(242,242,242,.6);
}
.ring {
position: absolute; left: 50%; top: 50%;
width: 180px; height: 180px;
transform: translate(-50%, -50%) rotate(-90deg); /* 12時始点 */
z-index: 1;
}
.count {
position: absolute; left: 50%; top: 50%;
transform: translate(-50%, -50%);
z-index: 2;
font-size: 96px; font-weight: 900; line-height: 1;
font-variant-numeric: tabular-nums;
color: #F2F2F2;
}
.rocket {
position: absolute; left: 50%; top: 60%;
width: 56px; height: auto;
transform: translate(-50%, -50%);
z-index: 2;
}
.flame {
transform-box: fill-box;
transform-origin: center top;
animation: flame .12s steps(3) infinite alternate;
}
@keyframes flame { from { transform: scaleY(.7); } to { transform: scaleY(1.2); } }
.loaded {
position: absolute; left: 50%; top: 50%;
transform: translate(-50%, -50%);
z-index: 2;
font-size: 28px; font-weight: 700; letter-spacing: .4em;
text-indent: .4em; /* letter-spacing分の右寄りを補正 */
color: #F2F2F2; opacity: 0;
}
JavaScript
// 星空canvas+数字/リング/ロケットのDOM。フェーズは elapsed 時間1本で分割(8sサイクル)。
(() => {
const stage = document.querySelector('[data-loader]');
if (!stage) return; // null安全
const cv = stage.querySelector('.stars');
const ctx = cv.getContext('2d');
const numEl = stage.querySelector('.count');
const ringFg = stage.querySelector('.ring-fg');
const ring = stage.querySelector('.ring');
const rocket = stage.querySelector('.rocket');
const loaded = stage.querySelector('.loaded');
const easeOutExpo = t => (t >= 1 ? 1 : 1 - Math.pow(2, -10 * t));
const clamp01 = t => Math.max(0, Math.min(1, t));
let W = 0, H = 0, dpr = 1;
const stars = [];
const initStars = () => {
stars.length = 0;
for (let i = 0; i < 120; i++) {
stars.push({ xr: ((i * 137.5) % 100) / 100, y: ((i * 53) % 100) / 100 * H, size: 1 + (i % 2) });
}
};
const resize = () => {
dpr = Math.min(window.devicePixelRatio || 1, 2);
W = cv.clientWidth; H = cv.clientHeight;
cv.width = W * dpr; cv.height = H * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
initStars();
};
window.addEventListener('resize', resize);
resize();
const T = 8, RING = 440;
let start = 0;
const loop = (now) => {
if (!start) start = now;
const e = ((now - start) / 1000) % T;
// ---- 星空 / ワープ ----
let warp = 0;
if (e >= 4.2 && e < 6) warp = 1 + 17 * ((e - 4.2) / 1.8); // 1→18px/f
const step = warp > 0 ? warp : 0.4;
if (warp > 0) { ctx.fillStyle = 'rgba(15,17,23,0.3)'; }
else { ctx.fillStyle = '#0F1117'; }
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = '#fff'; ctx.strokeStyle = '#fff';
for (const s of stars) {
const x = s.xr * W;
const prevY = s.y;
let y = prevY + step;
let wrapped = false;
if (y > H) { y -= H; wrapped = true; }
s.y = y;
if (warp > 0 && !wrapped) {
ctx.lineWidth = s.size; // 線幅=星サイズにしないと細くて消える
ctx.beginPath(); ctx.moveTo(x, prevY); ctx.lineTo(x, y); ctx.stroke();
} else {
ctx.fillRect(x - s.size / 2, y - s.size / 2, s.size, s.size);
}
}
// ---- 全体フェード(サイクル末) ----
stage.style.opacity = e > 7.7 ? clamp01(1 - (e - 7.7) / 0.3) : 1;
// ---- フェーズ別のDOM ----
if (e < 3) { // 1. カウントダウン
const n = 3 - Math.floor(e);
const p = e - Math.floor(e);
const a = clamp01(p / 0.3);
numEl.textContent = n;
numEl.style.opacity = easeOutExpo(a);
numEl.style.transform = `translate(-50%, -50%) scale(${1.4 - 0.4 * easeOutExpo(a)})`;
ring.style.opacity = 1;
ringFg.style.strokeDashoffset = RING * (1 - p); // 毎秒 440→0
rocket.style.opacity = 1;
rocket.style.transform = 'translate(-50%, -50%)';
loaded.style.opacity = 0;
} else if (e < 4.2) { // 2. 発射
numEl.style.opacity = 0;
ring.style.opacity = 0;
const lp = clamp01((e - 3) / 1.2);
const ease = lp * lp; // cubic-bezier(0.5,0,1,1) ≒ ease-in
rocket.style.opacity = 1;
rocket.style.transform = `translate(-50%, -50%) translateY(${-130 * ease}vh)`;
loaded.style.opacity = 0;
} else if (e < 6) { // 3. ワープ
rocket.style.opacity = 0;
numEl.style.opacity = 0;
ring.style.opacity = 0;
loaded.style.opacity = 0;
} else if (e < 7) { // 4. 完了
const lp = clamp01((e - 6) / 0.6);
loaded.style.opacity = easeOutExpo(lp);
loaded.style.letterSpacing = (0.4 - 0.3 * easeOutExpo(lp)) + 'em';
} else { // 5. 停止
loaded.style.opacity = 1;
loaded.style.letterSpacing = '0.1em';
}
requestAnimationFrame(loop);
};
requestAnimationFrame(loop);
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「打ち上げカウントダウン・ローダー」の効果を追加してください。
# 追加してほしい効果
打ち上げカウントダウン・ローダー(ローダー & スケルトン)
HUD風の3・2・1カウントダウンからロケットが発射し、星が流線になるワープで読み込み完了を告げるローダー演出です。待ち時間をミッション開始の高揚感に変える、体験型サイトの導入に。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 打ち上げカウントダウン・ローダー:3・2・1→発射→ワープで読み込み完了を告げる -->
<div class="stage" data-loader aria-label="打ち上げカウントダウンのローダー">
<canvas class="stars" aria-hidden="true"></canvas>
<p class="caption">INITIALIZING<span class="dots">...</span></p>
<svg class="ring" viewBox="0 0 160 160" aria-hidden="true">
<circle class="ring-bg" cx="80" cy="80" r="70" fill="none" stroke="rgba(255,255,255,.12)" stroke-width="6"/>
<circle class="ring-fg" cx="80" cy="80" r="70" fill="none" stroke="#4ED1A1" stroke-width="6" stroke-linecap="round" stroke-dasharray="440"/>
</svg>
<div class="count" aria-hidden="true">3</div>
<svg class="rocket" viewBox="0 0 40 72" aria-hidden="true">
<path d="M12 18 L20 2 L28 18 Z" fill="#FF5C5C"/>
<rect x="10" y="16" width="20" height="40" rx="10" fill="#F2F2F2"/>
<circle cx="20" cy="30" r="5" fill="#4D7CFE"/>
<path d="M10 46 L4 60 L10 56 Z" fill="#FF5C5C"/>
<path d="M30 46 L36 60 L30 56 Z" fill="#FF5C5C"/>
<path class="flame" d="M14 54 L20 72 L26 54 Z" fill="#FFC83D"/>
</svg>
<div class="loaded" aria-hidden="true">LOADED</div>
</div>
【CSS】
:root{
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
--ease-inout: cubic-bezier(0.65, 0, 0.35, 1);
}
* { box-sizing: border-box; }
html, body { margin: 0; width: 100%; min-height: 100%; background: #0F1117; }
.stage {
position: relative;
width: 100%;
height: 100vh;
min-height: 260px;
max-height: 100%;
overflow: hidden;
background: #0F1117;
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
color: #F2F2F2;
}
.stars { position: absolute; inset: 0; z-index: 0; display: block; }
.caption {
position: absolute; left: 50%; top: 15%; transform: translateX(-50%);
z-index: 1; margin: 0;
font-size: 12px; letter-spacing: .26em; color: rgba(242,242,242,.6);
}
.ring {
position: absolute; left: 50%; top: 50%;
width: 180px; height: 180px;
transform: translate(-50%, -50%) rotate(-90deg); /* 12時始点 */
z-index: 1;
}
.count {
position: absolute; left: 50%; top: 50%;
transform: translate(-50%, -50%);
z-index: 2;
font-size: 96px; font-weight: 900; line-height: 1;
font-variant-numeric: tabular-nums;
color: #F2F2F2;
}
.rocket {
position: absolute; left: 50%; top: 60%;
width: 56px; height: auto;
transform: translate(-50%, -50%);
z-index: 2;
}
.flame {
transform-box: fill-box;
transform-origin: center top;
animation: flame .12s steps(3) infinite alternate;
}
@keyframes flame { from { transform: scaleY(.7); } to { transform: scaleY(1.2); } }
.loaded {
position: absolute; left: 50%; top: 50%;
transform: translate(-50%, -50%);
z-index: 2;
font-size: 28px; font-weight: 700; letter-spacing: .4em;
text-indent: .4em; /* letter-spacing分の右寄りを補正 */
color: #F2F2F2; opacity: 0;
}
【JavaScript】
// 星空canvas+数字/リング/ロケットのDOM。フェーズは elapsed 時間1本で分割(8sサイクル)。
(() => {
const stage = document.querySelector('[data-loader]');
if (!stage) return; // null安全
const cv = stage.querySelector('.stars');
const ctx = cv.getContext('2d');
const numEl = stage.querySelector('.count');
const ringFg = stage.querySelector('.ring-fg');
const ring = stage.querySelector('.ring');
const rocket = stage.querySelector('.rocket');
const loaded = stage.querySelector('.loaded');
const easeOutExpo = t => (t >= 1 ? 1 : 1 - Math.pow(2, -10 * t));
const clamp01 = t => Math.max(0, Math.min(1, t));
let W = 0, H = 0, dpr = 1;
const stars = [];
const initStars = () => {
stars.length = 0;
for (let i = 0; i < 120; i++) {
stars.push({ xr: ((i * 137.5) % 100) / 100, y: ((i * 53) % 100) / 100 * H, size: 1 + (i % 2) });
}
};
const resize = () => {
dpr = Math.min(window.devicePixelRatio || 1, 2);
W = cv.clientWidth; H = cv.clientHeight;
cv.width = W * dpr; cv.height = H * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
initStars();
};
window.addEventListener('resize', resize);
resize();
const T = 8, RING = 440;
let start = 0;
const loop = (now) => {
if (!start) start = now;
const e = ((now - start) / 1000) % T;
// ---- 星空 / ワープ ----
let warp = 0;
if (e >= 4.2 && e < 6) warp = 1 + 17 * ((e - 4.2) / 1.8); // 1→18px/f
const step = warp > 0 ? warp : 0.4;
if (warp > 0) { ctx.fillStyle = 'rgba(15,17,23,0.3)'; }
else { ctx.fillStyle = '#0F1117'; }
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = '#fff'; ctx.strokeStyle = '#fff';
for (const s of stars) {
const x = s.xr * W;
const prevY = s.y;
let y = prevY + step;
let wrapped = false;
if (y > H) { y -= H; wrapped = true; }
s.y = y;
if (warp > 0 && !wrapped) {
ctx.lineWidth = s.size; // 線幅=星サイズにしないと細くて消える
ctx.beginPath(); ctx.moveTo(x, prevY); ctx.lineTo(x, y); ctx.stroke();
} else {
ctx.fillRect(x - s.size / 2, y - s.size / 2, s.size, s.size);
}
}
// ---- 全体フェード(サイクル末) ----
stage.style.opacity = e > 7.7 ? clamp01(1 - (e - 7.7) / 0.3) : 1;
// ---- フェーズ別のDOM ----
if (e < 3) { // 1. カウントダウン
const n = 3 - Math.floor(e);
const p = e - Math.floor(e);
const a = clamp01(p / 0.3);
numEl.textContent = n;
numEl.style.opacity = easeOutExpo(a);
numEl.style.transform = `translate(-50%, -50%) scale(${1.4 - 0.4 * easeOutExpo(a)})`;
ring.style.opacity = 1;
ringFg.style.strokeDashoffset = RING * (1 - p); // 毎秒 440→0
rocket.style.opacity = 1;
rocket.style.transform = 'translate(-50%, -50%)';
loaded.style.opacity = 0;
} else if (e < 4.2) { // 2. 発射
numEl.style.opacity = 0;
ring.style.opacity = 0;
const lp = clamp01((e - 3) / 1.2);
const ease = lp * lp; // cubic-bezier(0.5,0,1,1) ≒ ease-in
rocket.style.opacity = 1;
rocket.style.transform = `translate(-50%, -50%) translateY(${-130 * ease}vh)`;
loaded.style.opacity = 0;
} else if (e < 6) { // 3. ワープ
rocket.style.opacity = 0;
numEl.style.opacity = 0;
ring.style.opacity = 0;
loaded.style.opacity = 0;
} else if (e < 7) { // 4. 完了
const lp = clamp01((e - 6) / 0.6);
loaded.style.opacity = easeOutExpo(lp);
loaded.style.letterSpacing = (0.4 - 0.3 * easeOutExpo(lp)) + 'em';
} else { // 5. 停止
loaded.style.opacity = 1;
loaded.style.letterSpacing = '0.1em';
}
requestAnimationFrame(loop);
};
requestAnimationFrame(loop);
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。