CRT・VHSレトロ画面

画像にスキャンライン・色収差(RGBずれ)・微振動・ノイズを重ね、ブラウン管/ビデオ風のレトロ画面に変換します。走査グローやVHS風OSD(REC表示)も入り、エモい・サイバーパンク系のヒーローや作品アーカイブの演出に最適。canvasノイズは取得失敗時もCSSグラデにフォールバックします。

#image#retro#crt#vhs#glitch

ライブデモ

使用例(お題: SaaS FlowDesk)

この技法を「SaaS FlowDesk」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- FlowDesk:ブランドヒストリー(VHS風アーカイブ画面) -->
<section class="fd-crt-wrap">
  <!-- 説明 -->
  <div class="fd-crt-wrap__head">
    <span class="fd-crt-wrap__tag">SINCE 2018</span>
    <h2 class="fd-crt-wrap__title">あの頃の<br>FlowDesk。</h2>
    <p class="fd-crt-wrap__lead">創業時のプロトタイプ映像を、当時のテープから。チームの原点をアーカイブで振り返ります。</p>
    <a class="fd-crt-wrap__btn" href="#">沿革を見る</a>
  </div>

  <!-- ブラウン管/VHS風画面 -->
  <div class="fd-crt" aria-label="CRT・VHSレトロ画面">
    <div class="fd-crt__screen">
      <img class="fd-crt__img" src="https://picsum.photos/seed/flowdesk-archive/640/420" alt="" crossorigin="anonymous">
    </div>
    <div class="fd-crt__lines" aria-hidden="true"></div>
    <div class="fd-crt__vignette" aria-hidden="true"></div>
    <canvas class="fd-crt__noise" width="160" height="105" aria-hidden="true"></canvas>
    <div class="fd-crt__osd" aria-hidden="true">
      <span class="fd-crt__rec"><i></i>REC</span>
      <span class="fd-crt__time">SP&nbsp;2018:04</span>
    </div>
  </div>
</section>
CSS
/* FlowDesk:ブランドヒストリー VHS風アーカイブ画面 */
:root {
  --navy: #0f1b34;
  --blue: #4f7cff;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  height: 400px;
  display: flex;
  align-items: center;
  gap: 32px;
  padding: 0 32px;
  font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
  background: radial-gradient(120% 120% at 50% -10%, #16223f 0%, var(--navy) 70%);
  color: #fff;
  overflow: hidden;
}

.fd-crt-wrap__head { flex: 1; }
.fd-crt-wrap__tag { font-size: 10px; letter-spacing: 0.3em; color: #8aa6ff; }
.fd-crt-wrap__title {
  margin: 10px 0 12px;
  font-size: 27px;
  line-height: 1.35;
  font-weight: 800;
}
.fd-crt-wrap__lead {
  margin: 0 0 22px;
  font-size: 13px;
  line-height: 1.85;
  max-width: 280px;
  color: rgba(255,255,255,0.75);
}
.fd-crt-wrap__btn {
  display: inline-block;
  padding: 11px 22px;
  border-radius: 10px;
  background: var(--blue);
  color: #fff;
  font-size: 13px;
  font-weight: 700;
  text-decoration: none;
  box-shadow: 0 10px 24px rgba(79,124,255,0.45);
  transition: transform 0.2s ease;
}
.fd-crt-wrap__btn:hover { transform: translateY(-2px); }

/* ブラウン管の外枠(ベゼル) */
.fd-crt {
  position: relative;
  flex: 0 0 360px;
  width: 360px;
  height: 300px;
  border-radius: 16px;
  overflow: hidden;
  background: #000;
  animation: fdCrtJitter 5.5s steps(1) infinite;
  box-shadow:
    0 22px 60px rgba(0,0,0,0.6),
    inset 0 0 0 2px rgba(255,255,255,0.06),
    inset 0 0 40px rgba(0,0,0,0.55);
}

/* 画面(球面感) */
.fd-crt__screen {
  position: absolute;
  inset: 0;
  transform: scale(1.04);
  filter: saturate(1.1) contrast(1.08) brightness(1.02);
}
.fd-crt__img,
.fd-crt__screen::before,
.fd-crt__screen::after {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  content: "";
  background-image: var(--crt-src);
  background-size: cover;
  background-position: center;
}
.fd-crt__img { z-index: 1; }

/* R/B チャンネルをずらした色付き複製 */
.fd-crt__screen::before {
  z-index: 2;
  background-color: #f00;
  background-blend-mode: screen;
  mix-blend-mode: screen;
  opacity: 0.5;
  animation: fdCrtShiftR 3.2s ease-in-out infinite;
}
.fd-crt__screen::after {
  z-index: 2;
  background-color: #00f;
  background-blend-mode: screen;
  mix-blend-mode: screen;
  opacity: 0.5;
  animation: fdCrtShiftB 3.2s ease-in-out infinite;
}

/* スキャンライン+走査グロー */
.fd-crt__lines {
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  background:
    repeating-linear-gradient(to bottom,
      rgba(0,0,0,0) 0, rgba(0,0,0,0) 2px,
      rgba(0,0,0,0.28) 3px, rgba(0,0,0,0.28) 4px),
    linear-gradient(to bottom, rgba(255,255,255,0.06), rgba(255,255,255,0) 8%);
  background-size: 100% 100%, 100% 50px;
  animation: fdCrtScan 6s linear infinite;
}

/* ビネット */
.fd-crt__vignette {
  position: absolute;
  inset: 0;
  z-index: 5;
  pointer-events: none;
  background: radial-gradient(120% 120% at 50% 50%, transparent 55%, rgba(0,0,0,0.55) 100%);
}

/* ノイズ */
.fd-crt__noise {
  position: absolute;
  inset: 0;
  z-index: 3;
  width: 100%;
  height: 100%;
  opacity: 0.12;
  mix-blend-mode: overlay;
  pointer-events: none;
}

/* VHS風OSD */
.fd-crt__osd {
  position: absolute;
  inset: 12px 14px auto 14px;
  z-index: 6;
  display: flex;
  justify-content: space-between;
  font-family: "Courier New", ui-monospace, monospace;
  font-weight: 700;
  font-size: 13px;
  letter-spacing: 0.18em;
  color: #eafff0;
  text-shadow: 0 0 6px rgba(120,255,170,0.8), 0 1px 2px #000;
  pointer-events: none;
}
.fd-crt__rec { display: inline-flex; align-items: center; gap: 6px; }
.fd-crt__rec i {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: #ff3b3b;
  box-shadow: 0 0 8px #ff3b3b;
  animation: fdCrtBlink 1.2s steps(1) infinite;
}

/* アニメーション */
@keyframes fdCrtScan { to { background-position: 0 0, 0 50px; } }
@keyframes fdCrtShiftR { 0%,100% { transform: translateX(-1.5px); } 50% { transform: translateX(1.5px); } }
@keyframes fdCrtShiftB { 0%,100% { transform: translateX(1.5px); } 50% { transform: translateX(-1.5px); } }
@keyframes fdCrtJitter {
  0%, 92%, 100% { transform: translateY(0); }
  93% { transform: translateY(-1px) skewX(0.3deg); }
  95% { transform: translateY(2px); }
  97% { transform: translateY(-1px); }
}
@keyframes fdCrtBlink { 0%,49% { opacity: 1; } 50%,100% { opacity: 0.15; } }

@media (prefers-reduced-motion: reduce) {
  .fd-crt,
  .fd-crt__lines,
  .fd-crt__screen::before,
  .fd-crt__screen::after,
  .fd-crt__rec i { animation: none; }
  .fd-crt__screen::before { transform: translateX(-1.5px); }
  .fd-crt__screen::after { transform: translateX(1.5px); }
  .fd-crt-wrap__btn { transition: none; }
}
JavaScript
// CRT・VHSレトロ画面:色収差レイヤーへの画像供給+canvasノイズ+安全なフォールバック
(() => {
  const crt = document.querySelector(".fd-crt");
  const img = document.querySelector(".fd-crt__img");
  const noise = document.querySelector(".fd-crt__noise");
  if (!crt) return;

  // フォールバック背景(取得失敗・CORS事故でも破綻させない)
  const fallback =
    "linear-gradient(135deg, #0f1b34 0%, #2a3a6a 50%, #4f7cff 100%)," +
    "repeating-linear-gradient(45deg, rgba(255,255,255,.06) 0 10px, transparent 10px 22px)";

  // 画像URLを CSS変数へ流し込む(::before/::after の色収差レイヤーが参照)
  const applySrc = (cssBg) => { crt.style.setProperty("--crt-src", cssBg); };

  applySrc(fallback); // 画像が来る前から見える
  if (img) img.style.background = fallback;

  // picsum 画像を CORS 安全に読み込み、成功したら本画像を採用
  if (img && img.getAttribute("src")) {
    const probe = new Image();
    probe.crossOrigin = "anonymous";
    probe.onload = () => {
      applySrc(`url("${img.src}")`);
      img.style.background = "none";
    };
    probe.onerror = () => {
      img.removeAttribute("src");
      img.style.background = fallback;
      img.style.backgroundSize = "cover";
    };
    probe.src = img.src;
  }

  // canvasノイズ(毎フレーム軽量生成)
  const ctx = noise && noise.getContext ? noise.getContext("2d") : null;
  let raf = 0;
  let running = false;

  const drawNoise = () => {
    if (!ctx) return;
    const w = noise.width, h = noise.height;
    const imageData = ctx.createImageData(w, h);
    const buf = imageData.data;
    for (let i = 0; i < buf.length; i += 4) {
      const v = (Math.random() * 255) | 0;
      buf[i] = buf[i + 1] = buf[i + 2] = v;
      buf[i + 3] = 255;
    }
    ctx.putImageData(imageData, 0, 0);
  };

  // 30fps 程度に間引いてループ
  let last = 0;
  const loop = (now) => {
    if (!running) return;
    if (now - last > 33) { drawNoise(); last = now; }
    raf = requestAnimationFrame(loop);
  };
  const start = () => { if (running || !ctx) return; running = true; raf = requestAnimationFrame(loop); };
  const stop = () => { running = false; if (raf) cancelAnimationFrame(raf); raf = 0; };

  document.addEventListener("visibilitychange", () => {
    document.hidden ? stop() : start();
  });

  drawNoise();
  start();
})();

コード

HTML
<!-- ブラウン管/VHS風レトロ画面:スキャンライン+色収差+微振動+ノイズ -->
<div class="crt-stage">
  <div class="crt" aria-label="CRT・VHSレトロ画面">
    <!-- 画像本体(RGBずれは ::before/::after の複製レイヤーで表現) -->
    <div class="crt__screen">
      <img class="crt__img" src="https://picsum.photos/seed/crtvhs/640/420" alt="" crossorigin="anonymous">
    </div>
    <!-- スキャンライン+ビネット+走査グロー -->
    <div class="crt__lines" aria-hidden="true"></div>
    <div class="crt__vignette" aria-hidden="true"></div>
    <!-- ノイズ(canvasで毎フレーム生成) -->
    <canvas class="crt__noise" width="160" height="105" aria-hidden="true"></canvas>
    <!-- VHS風のオンスクリーン表示 -->
    <div class="crt__osd" aria-hidden="true">
      <span class="crt__rec"><i></i>REC</span>
      <span class="crt__time">SP&nbsp;0:00:14</span>
    </div>
  </div>
</div>
CSS
/* CRT・VHSレトロ画面 */
:root {
  --crt-w: min(80vw, 460px);
  --crt-radius: 16px;
}
body {
  background:
    radial-gradient(120% 120% at 50% -10%, #1a1f2b 0%, #07090e 70%);
}
.crt-stage { padding: 22px; }

/* 画面の外枠(ベゼル)。樽型のふくらみと角丸でブラウン管らしく */
.crt {
  position: relative;
  width: var(--crt-w);
  aspect-ratio: 4 / 3;
  border-radius: var(--crt-radius);
  overflow: hidden;
  background: #000;
  /* 微振動(フレーム全体)。reduce時は止まる */
  animation: crt-jitter 5.5s steps(1) infinite;
  box-shadow:
    0 22px 60px -20px rgba(0, 0, 0, .9),
    inset 0 0 0 2px rgba(255, 255, 255, .06),
    inset 0 0 40px rgba(0, 0, 0, .55);
}

/* 画面(わずかに膨らませて球面感を出す) */
.crt__screen {
  position: absolute;
  inset: 0;
  transform: scale(1.04);
  filter: saturate(1.15) contrast(1.08) brightness(1.02);
}

/* 元画像。複製を作れないので drop-shadow で軽い色収差を補助しつつ、
   実際のRGBずれは ::before/::after の同一画像レイヤーで表現する */
.crt__img,
.crt__screen::before,
.crt__screen::after {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  content: "";
  background-image: var(--crt-src);
  background-size: cover;
  background-position: center;
}
.crt__img { z-index: 1; }

/* R/Bチャンネルをずらした色付き複製(mix-blend:screenで加算合成風) */
.crt__screen::before {
  z-index: 2;
  background-color: #f00;
  background-blend-mode: screen;
  mix-blend-mode: screen;
  opacity: .5;
  animation: crt-shift-r 3.2s ease-in-out infinite;
}
.crt__screen::after {
  z-index: 2;
  background-color: #00f;
  background-blend-mode: screen;
  mix-blend-mode: screen;
  opacity: .5;
  animation: crt-shift-b 3.2s ease-in-out infinite;
}

/* スキャンライン+ゆっくり流れる走査グロー */
.crt__lines {
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  background:
    repeating-linear-gradient(
      to bottom,
      rgba(0, 0, 0, 0) 0,
      rgba(0, 0, 0, 0) 2px,
      rgba(0, 0, 0, .28) 3px,
      rgba(0, 0, 0, .28) 4px
    ),
    linear-gradient(
      to bottom,
      rgba(255, 255, 255, .06),
      rgba(255, 255, 255, 0) 8%
    );
  background-size: 100% 100%, 100% 50px;
  animation: crt-scan 6s linear infinite;
}

/* ビネット+上下の暗がり */
.crt__vignette {
  position: absolute;
  inset: 0;
  z-index: 5;
  pointer-events: none;
  background: radial-gradient(120% 120% at 50% 50%, transparent 55%, rgba(0, 0, 0, .55) 100%);
}

/* ノイズcanvas(薄く重ねる) */
.crt__noise {
  position: absolute;
  inset: 0;
  z-index: 3;
  width: 100%;
  height: 100%;
  opacity: .12;
  mix-blend-mode: overlay;
  pointer-events: none;
}

/* VHS風OSD */
.crt__osd {
  position: absolute;
  inset: 12px 14px auto auto;
  left: 14px;
  z-index: 6;
  display: flex;
  justify-content: space-between;
  font-family: "Courier New", ui-monospace, monospace;
  font-weight: 700;
  font-size: clamp(11px, 2.6vw, 15px);
  letter-spacing: .18em;
  color: #eafff0;
  text-shadow: 0 0 6px rgba(120, 255, 170, .8), 0 1px 2px #000;
  pointer-events: none;
}
.crt__rec { display: inline-flex; align-items: center; gap: 6px; }
.crt__rec i {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: #ff3b3b;
  box-shadow: 0 0 8px #ff3b3b;
  animation: crt-blink 1.2s steps(1) infinite;
}

/* --- アニメーション --- */
@keyframes crt-scan { to { background-position: 0 0, 0 50px; } }

/* RGBずれ(左右に小さく揺れる) */
@keyframes crt-shift-r {
  0%, 100% { transform: translateX(-1.5px); }
  50%      { transform: translateX(1.5px); }
}
@keyframes crt-shift-b {
  0%, 100% { transform: translateX(1.5px); }
  50%      { transform: translateX(-1.5px); }
}

/* 微振動:たまにガクッと画面が縦ずれするVHSトラッキング風 */
@keyframes crt-jitter {
  0%, 92%, 100% { transform: translateY(0); }
  93%  { transform: translateY(-1px) skewX(.3deg); }
  95%  { transform: translateY(2px); }
  97%  { transform: translateY(-1px); }
}

@keyframes crt-blink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: .15; } }

/* 動きが苦手な人向けに静止(merge側で除去されるが単体表示の保険) */
@media (prefers-reduced-motion: reduce) {
  .crt,
  .crt__lines,
  .crt__screen::before,
  .crt__screen::after,
  .crt__rec i { animation: none; }
  .crt__screen::before { transform: translateX(-1.5px); }
  .crt__screen::after  { transform: translateX(1.5px); }
}
JavaScript
// CRT・VHSレトロ画面:色収差レイヤーの画像供給+canvasノイズ+安全なフォールバック
(() => {
  const crt = document.querySelector(".crt");
  const img = document.querySelector(".crt__img");
  const noise = document.querySelector(".crt__noise");
  if (!crt) return;

  // フォールバック用のグラデ/パターン背景(取得失敗・CORS事故でも破綻させない)
  const fallback =
    "linear-gradient(135deg, #2a3a6a 0%, #6a2f7a 45%, #c2466b 100%)," +
    "repeating-linear-gradient(45deg, rgba(255,255,255,.06) 0 10px, transparent 10px 22px)";

  // 画像URLを CSS変数へ流し込む(::before/::after の色収差レイヤーが参照)
  const applySrc = (cssBg) => {
    crt.style.setProperty("--crt-src", cssBg);
  };

  // まずフォールバックを敷く(画像が来る前から見える)
  applySrc(fallback);
  if (img) img.style.background = fallback;

  // picsum 画像を CORS 安全に読み込み、成功したら本画像を採用
  if (img && img.getAttribute("src")) {
    const probe = new Image();
    probe.crossOrigin = "anonymous";
    probe.onload = () => {
      const url = `url("${img.src}")`;
      applySrc(url);                 // 色収差レイヤーも本画像に
      img.style.background = "none"; // <img>本体はそのまま表示
    };
    probe.onerror = () => {
      // 失敗時はフォールバックを画像にも適用(imgは透過させる)
      img.removeAttribute("src");
      img.style.background = fallback;
      img.style.backgroundSize = "cover";
    };
    probe.src = img.src;
  }

  // --- canvasノイズ(毎フレーム軽量生成) ---
  const ctx = noise && noise.getContext ? noise.getContext("2d") : null;
  let raf = 0;
  let running = false;

  const drawNoise = () => {
    if (!ctx) return;
    const w = noise.width, h = noise.height;
    const imageData = ctx.createImageData(w, h);
    const buf = imageData.data;
    // モノクロのランダムノイズを敷き詰める
    for (let i = 0; i < buf.length; i += 4) {
      const v = (Math.random() * 255) | 0;
      buf[i] = buf[i + 1] = buf[i + 2] = v;
      buf[i + 3] = 255;
    }
    ctx.putImageData(imageData, 0, 0);
  };

  // 30fps程度に間引いてループ(負荷軽減)
  let last = 0;
  const loop = (now) => {
    if (!running) return;
    if (now - last > 33) { drawNoise(); last = now; }
    raf = requestAnimationFrame(loop);
  };

  const start = () => {
    if (running || !ctx) return;
    running = true;
    raf = requestAnimationFrame(loop);
  };
  const stop = () => {
    running = false;
    if (raf) cancelAnimationFrame(raf);
    raf = 0;
  };

  // タブ非表示で停止/復帰で再開
  document.addEventListener("visibilitychange", () => {
    document.hidden ? stop() : start();
  });

  // 初期ノイズを1枚描いてからループ開始
  drawNoise();
  start();
})();

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「CRT・VHSレトロ画面」の効果を追加してください。

# 追加してほしい効果
CRT・VHSレトロ画面(画像エフェクト)
画像にスキャンライン・色収差(RGBずれ)・微振動・ノイズを重ね、ブラウン管/ビデオ風のレトロ画面に変換します。走査グローやVHS風OSD(REC表示)も入り、エモい・サイバーパンク系のヒーローや作品アーカイブの演出に最適。canvasノイズは取得失敗時もCSSグラデにフォールバックします。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- ブラウン管/VHS風レトロ画面:スキャンライン+色収差+微振動+ノイズ -->
<div class="crt-stage">
  <div class="crt" aria-label="CRT・VHSレトロ画面">
    <!-- 画像本体(RGBずれは ::before/::after の複製レイヤーで表現) -->
    <div class="crt__screen">
      <img class="crt__img" src="https://picsum.photos/seed/crtvhs/640/420" alt="" crossorigin="anonymous">
    </div>
    <!-- スキャンライン+ビネット+走査グロー -->
    <div class="crt__lines" aria-hidden="true"></div>
    <div class="crt__vignette" aria-hidden="true"></div>
    <!-- ノイズ(canvasで毎フレーム生成) -->
    <canvas class="crt__noise" width="160" height="105" aria-hidden="true"></canvas>
    <!-- VHS風のオンスクリーン表示 -->
    <div class="crt__osd" aria-hidden="true">
      <span class="crt__rec"><i></i>REC</span>
      <span class="crt__time">SP&nbsp;0:00:14</span>
    </div>
  </div>
</div>

【CSS】
/* CRT・VHSレトロ画面 */
:root {
  --crt-w: min(80vw, 460px);
  --crt-radius: 16px;
}
body {
  background:
    radial-gradient(120% 120% at 50% -10%, #1a1f2b 0%, #07090e 70%);
}
.crt-stage { padding: 22px; }

/* 画面の外枠(ベゼル)。樽型のふくらみと角丸でブラウン管らしく */
.crt {
  position: relative;
  width: var(--crt-w);
  aspect-ratio: 4 / 3;
  border-radius: var(--crt-radius);
  overflow: hidden;
  background: #000;
  /* 微振動(フレーム全体)。reduce時は止まる */
  animation: crt-jitter 5.5s steps(1) infinite;
  box-shadow:
    0 22px 60px -20px rgba(0, 0, 0, .9),
    inset 0 0 0 2px rgba(255, 255, 255, .06),
    inset 0 0 40px rgba(0, 0, 0, .55);
}

/* 画面(わずかに膨らませて球面感を出す) */
.crt__screen {
  position: absolute;
  inset: 0;
  transform: scale(1.04);
  filter: saturate(1.15) contrast(1.08) brightness(1.02);
}

/* 元画像。複製を作れないので drop-shadow で軽い色収差を補助しつつ、
   実際のRGBずれは ::before/::after の同一画像レイヤーで表現する */
.crt__img,
.crt__screen::before,
.crt__screen::after {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  content: "";
  background-image: var(--crt-src);
  background-size: cover;
  background-position: center;
}
.crt__img { z-index: 1; }

/* R/Bチャンネルをずらした色付き複製(mix-blend:screenで加算合成風) */
.crt__screen::before {
  z-index: 2;
  background-color: #f00;
  background-blend-mode: screen;
  mix-blend-mode: screen;
  opacity: .5;
  animation: crt-shift-r 3.2s ease-in-out infinite;
}
.crt__screen::after {
  z-index: 2;
  background-color: #00f;
  background-blend-mode: screen;
  mix-blend-mode: screen;
  opacity: .5;
  animation: crt-shift-b 3.2s ease-in-out infinite;
}

/* スキャンライン+ゆっくり流れる走査グロー */
.crt__lines {
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  background:
    repeating-linear-gradient(
      to bottom,
      rgba(0, 0, 0, 0) 0,
      rgba(0, 0, 0, 0) 2px,
      rgba(0, 0, 0, .28) 3px,
      rgba(0, 0, 0, .28) 4px
    ),
    linear-gradient(
      to bottom,
      rgba(255, 255, 255, .06),
      rgba(255, 255, 255, 0) 8%
    );
  background-size: 100% 100%, 100% 50px;
  animation: crt-scan 6s linear infinite;
}

/* ビネット+上下の暗がり */
.crt__vignette {
  position: absolute;
  inset: 0;
  z-index: 5;
  pointer-events: none;
  background: radial-gradient(120% 120% at 50% 50%, transparent 55%, rgba(0, 0, 0, .55) 100%);
}

/* ノイズcanvas(薄く重ねる) */
.crt__noise {
  position: absolute;
  inset: 0;
  z-index: 3;
  width: 100%;
  height: 100%;
  opacity: .12;
  mix-blend-mode: overlay;
  pointer-events: none;
}

/* VHS風OSD */
.crt__osd {
  position: absolute;
  inset: 12px 14px auto auto;
  left: 14px;
  z-index: 6;
  display: flex;
  justify-content: space-between;
  font-family: "Courier New", ui-monospace, monospace;
  font-weight: 700;
  font-size: clamp(11px, 2.6vw, 15px);
  letter-spacing: .18em;
  color: #eafff0;
  text-shadow: 0 0 6px rgba(120, 255, 170, .8), 0 1px 2px #000;
  pointer-events: none;
}
.crt__rec { display: inline-flex; align-items: center; gap: 6px; }
.crt__rec i {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: #ff3b3b;
  box-shadow: 0 0 8px #ff3b3b;
  animation: crt-blink 1.2s steps(1) infinite;
}

/* --- アニメーション --- */
@keyframes crt-scan { to { background-position: 0 0, 0 50px; } }

/* RGBずれ(左右に小さく揺れる) */
@keyframes crt-shift-r {
  0%, 100% { transform: translateX(-1.5px); }
  50%      { transform: translateX(1.5px); }
}
@keyframes crt-shift-b {
  0%, 100% { transform: translateX(1.5px); }
  50%      { transform: translateX(-1.5px); }
}

/* 微振動:たまにガクッと画面が縦ずれするVHSトラッキング風 */
@keyframes crt-jitter {
  0%, 92%, 100% { transform: translateY(0); }
  93%  { transform: translateY(-1px) skewX(.3deg); }
  95%  { transform: translateY(2px); }
  97%  { transform: translateY(-1px); }
}

@keyframes crt-blink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: .15; } }

/* 動きが苦手な人向けに静止(merge側で除去されるが単体表示の保険) */
@media (prefers-reduced-motion: reduce) {
  .crt,
  .crt__lines,
  .crt__screen::before,
  .crt__screen::after,
  .crt__rec i { animation: none; }
  .crt__screen::before { transform: translateX(-1.5px); }
  .crt__screen::after  { transform: translateX(1.5px); }
}

【JavaScript】
// CRT・VHSレトロ画面:色収差レイヤーの画像供給+canvasノイズ+安全なフォールバック
(() => {
  const crt = document.querySelector(".crt");
  const img = document.querySelector(".crt__img");
  const noise = document.querySelector(".crt__noise");
  if (!crt) return;

  // フォールバック用のグラデ/パターン背景(取得失敗・CORS事故でも破綻させない)
  const fallback =
    "linear-gradient(135deg, #2a3a6a 0%, #6a2f7a 45%, #c2466b 100%)," +
    "repeating-linear-gradient(45deg, rgba(255,255,255,.06) 0 10px, transparent 10px 22px)";

  // 画像URLを CSS変数へ流し込む(::before/::after の色収差レイヤーが参照)
  const applySrc = (cssBg) => {
    crt.style.setProperty("--crt-src", cssBg);
  };

  // まずフォールバックを敷く(画像が来る前から見える)
  applySrc(fallback);
  if (img) img.style.background = fallback;

  // picsum 画像を CORS 安全に読み込み、成功したら本画像を採用
  if (img && img.getAttribute("src")) {
    const probe = new Image();
    probe.crossOrigin = "anonymous";
    probe.onload = () => {
      const url = `url("${img.src}")`;
      applySrc(url);                 // 色収差レイヤーも本画像に
      img.style.background = "none"; // <img>本体はそのまま表示
    };
    probe.onerror = () => {
      // 失敗時はフォールバックを画像にも適用(imgは透過させる)
      img.removeAttribute("src");
      img.style.background = fallback;
      img.style.backgroundSize = "cover";
    };
    probe.src = img.src;
  }

  // --- canvasノイズ(毎フレーム軽量生成) ---
  const ctx = noise && noise.getContext ? noise.getContext("2d") : null;
  let raf = 0;
  let running = false;

  const drawNoise = () => {
    if (!ctx) return;
    const w = noise.width, h = noise.height;
    const imageData = ctx.createImageData(w, h);
    const buf = imageData.data;
    // モノクロのランダムノイズを敷き詰める
    for (let i = 0; i < buf.length; i += 4) {
      const v = (Math.random() * 255) | 0;
      buf[i] = buf[i + 1] = buf[i + 2] = v;
      buf[i + 3] = 255;
    }
    ctx.putImageData(imageData, 0, 0);
  };

  // 30fps程度に間引いてループ(負荷軽減)
  let last = 0;
  const loop = (now) => {
    if (!running) return;
    if (now - last > 33) { drawNoise(); last = now; }
    raf = requestAnimationFrame(loop);
  };

  const start = () => {
    if (running || !ctx) return;
    running = true;
    raf = requestAnimationFrame(loop);
  };
  const stop = () => {
    running = false;
    if (raf) cancelAnimationFrame(raf);
    raf = 0;
  };

  // タブ非表示で停止/復帰で再開
  document.addEventListener("visibilitychange", () => {
    document.hidden ? stop() : start();
  });

  // 初期ノイズを1枚描いてからループ開始
  drawNoise();
  start();
})();

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。