「木漏れ日」を表現する

葉の間から落ちる光、揺れる影、舞う塵。木漏れ日の柔らかい時間を、CSS/SVGの軽量表現で再現するテクニック。

このページについて

  • 「木漏れ日の表現」とは:葉群に遮られた太陽光が作る光斑・透過光・影のゆらぎ。「komorebi」は海外でもそのまま通じる日本語で、穏やかさ・自然・午後の時間感覚を一語で伝えるモチーフ。
  • こんな時に使う:カフェ・オーガニック系ブランド・園芸/植物EC・リラクゼーション・アウトドア・写真館・保育/教育系サイト。
  • 関連タグ🌿 癒し🌊 海☁️ 空(準備中)
  • AIへの伝え方のコツ:「木漏れ日っぽく」では再現性が低い。「ぼけた光の斑がゆっくり揺れる」「逆光で透ける葉」「壁に枝影」など、このページの表現名と数値で指示する。

表現の引き出し

🎨 揺れる光だまり

📝 表現の語彙

日本語: 木漏れ日、光だまり、光の斑、揺れる光
英語: komorebi, dappled light, sunlight patches, light speckles
抽象キーワード: warmth, serenity, dapple

💬 AIへの指示テンプレ

「森の地面に木漏れ日の光斑が揺れる背景を作ってください。光斑はradial-gradientの輪郭のない楕円(丸み必須、葉の形にしない)で8〜10個、mix-blend-mode: screenで重なりを明るくします。各光斑に位置の揺れ・明滅・形の変化の3アニメを独立した素数的周期(3.7〜19秒)で重ね、全部が同時に暗くなる瞬間を作らないでください。輪郭は必ずボケさせます。」

🎯 似合うシーン

カフェ・オーガニックブランド・公園/植物園・ヨガ・写真館・絵本/教育系

🔧 実装手法

radial-gradient楕円 × screen合成 + 位置/明滅/形の3軸独立周期 + 擬似乱数ジッター

📄 コード例を見る
tsx
// 光斑9個。木漏れ日の光斑は「葉の隙間の形」ではなく
// 葉の隙間がピンホールになって投影した「太陽の像(丸い楕円)」。輪郭は必ずボケさせる
const patches = Array.from({ length: 9 }, (_, i) => {
  const j1 = ((i * 7919) % 100) / 100;
  const j2 = ((i * 104729) % 100) / 100;
  const j3 = ((i * 1299709) % 100) / 100;
  return {
    left: 3 + ((i * 9973) % 85),       // %
    top: 6 + ((i * 6271) % 68),        // %
    w: 20 + j1 * 50,                   // 20〜70px
    ratio: 0.7 + j2 * 0.6,             // 縦横比 = 地面への投影角度の違い
    peak: 0.38 + j3 * 0.3,             // 明るさの基準値
    dx: (j2 - 0.5) * 16,               // 位置の揺れ ±8px
    dy: (j3 - 0.5) * 16,
    driftDur: 7 + j1 * 10,             // 位置の揺れ 7〜17s(頭上の枝の大きな揺れ)
    flickDur: 3.7 + j2 * 4.8,          // 明滅 3.7〜8.5s(葉が光を遮ったり開けたり)
    morphDur: 11 + j3 * 8,             // 形の変化 11〜19s(隙間の形が変わる)
    driftDelay: -(j2 * 15),
    flickDelay: -(j3 * 8),
    morphDelay: -(j1 * 12),
  };
});

<div className="komorebi-stage">
  {/* 午後の空気感(静止した薄い暖色オーバーレイ) */}
  <div className="komorebi-air" />
  {patches.map((p, i) => (
    // 位置の揺れ/形の変化/明滅 の3軸を親子で分離し、独立周期で合成
    <div key={i} className="patch-drift"
      style={{
        left: `${p.left}%`, top: `${p.top}%`,
        width: p.w, height: p.w * p.ratio,
        "--dx": `${p.dx}px`, "--dy": `${p.dy}px`,
        animationDuration: `${p.driftDur}s`, animationDelay: `${p.driftDelay}s`,
      }}>
      <div className="patch-morph"
        style={{ animationDuration: `${p.morphDur}s`, animationDelay: `${p.morphDelay}s` }}>
        <div className="patch-glow"
          style={{ "--peak": p.peak,
            animationDuration: `${p.flickDur}s`, animationDelay: `${p.flickDelay}s` }} />
      </div>
    </div>
  ))}
</div>
css
.komorebi-stage {
  position: relative;
  height: 180px;
  border-radius: 8px;
  overflow: hidden;
  /* 午後の森の地面。苔と土の緑褐色 */
  background: linear-gradient(160deg, #4a5238 0%, #5c6344 40%, #6b7050 100%);
}
.komorebi-air {
  position: absolute; inset: 0;
  background: #d9c27a;
  opacity: 0.08; /* 午後の空気(静止) */
}
/* 軸1: 位置の揺れ。±8pxを光斑ごとに違う方向・周期で */
.patch-drift {
  position: absolute;
  animation: patchDrift 10s ease-in-out infinite alternate;
}
@keyframes patchDrift {
  from { transform: translate(calc(var(--dx) * -1), calc(var(--dy) * -1)); }
  to   { transform: translate(var(--dx), var(--dy)); }
}
/* 軸2: 形の変化。隙間の形が変わる気配 */
.patch-morph {
  width: 100%; height: 100%;
  animation: patchMorph 14s ease-in-out infinite alternate;
}
@keyframes patchMorph {
  from { transform: scale(0.85, 1.12); }
  to   { transform: scale(1.15, 0.88); }
}
/* 軸3: 明滅(基準値±40%)。輪郭のないradial-gradient + screen合成で
   光斑の重なりが加算的に明るくなる */
.patch-glow {
  width: 100%; height: 100%;
  border-radius: 50%;
  background: radial-gradient(closest-side,
    rgba(255,242,200,0.55), rgba(255,242,200,0.18) 55%, transparent 100%);
  mix-blend-mode: screen;
  animation: patchFlick 5s ease-in-out infinite;
}
@keyframes patchFlick {
  0%, 100% { opacity: calc(var(--peak) * 0.6); }
  50%      { opacity: var(--peak); }
}
/* アクセント: 小さな隙間からの鋭い光。時々ふっと現れて消える */
.glint-dot {
  position: absolute;
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(255,250,225,0.95), transparent);
  mix-blend-mode: screen;
  opacity: 0;
  animation: glintPop 11s ease-in-out infinite;
}
@keyframes glintPop {
  0%, 40%, 60%, 100% { opacity: 0; transform: scale(0.6); }
  47%, 53%           { opacity: 0.8; transform: scale(1); }
}
📖 関連: グラデーション

🎨 逆光の葉(透過光)

📝 表現の語彙

日本語: 透ける葉、逆光の葉、葉脈、透過光
英語: backlit leaves, translucent foliage, leaf veins
抽象キーワード: translucency, vitality, delicacy

💬 AIへの指示テンプレ

「逆光に透ける葉を5〜6枚描いてください。各葉は4層構造のSVG(ベース形状→根元側の陰影→中央の透過光ハイライト→葉脈のディテール線)で、透け方の強い葉(明るい黄緑)と弱い葉(深緑)を混ぜ、葉が重なる部分は暗くしてください(透過光の物理)。各葉は付け根支点で±2度ゆっくり揺らし、葉全体と先端側に位相差をつけて『しなり』を出してください。」

🎯 似合うシーン

植物EC・ハーブティーブランド・ボタニカル系サロン・環境系企業・初夏キャンペーン

🔧 実装手法

4層SVG構造の葉 + 透過光の色設計(透け強弱・multiplyによる重なりの暗化)+ 2段rotateのしなり

📄 コード例を見る
tsx
// 逆光に透ける葉。輪郭と葉脈が意味を持つので4層SVG構造で描き込む
function LeafShape({ len, tone }) {
  const W = len * 0.3;
  // 先の尖った楕円の葉形(base=原点、tip=上方向)
  const d = `M0,0 C${-W},${-len * 0.3} ${-W * 0.92},${-len * 0.74} 0,${-len}
             C${W * 0.92},${-len * 0.74} ${W},${-len * 0.3} 0,0 Z`;
  return (
    <>
      {/* ① ベース形状。multiply合成で葉の重なりが暗くなる(透過光の物理) */}
      <path d={d} fill={`url(#leaf-${tone})`} className="leaf-base" />
      {/* ② 根元側の陰影(光は先端側から透ける想定) */}
      <path d={rootHalfPath} fill="#3a5c2a" opacity={0.26} />
      {/* ③ 中央〜先端の透過光ハイライト。逆光なので「明るい=薄い部分」 */}
      <ellipse className="leaf-hi" cx={0} cy={-len * 0.62}
        rx={W * 0.55} ry={len * 0.25} fill="url(#leaf-hi)" />
      {/* ④ ディテール線: 主脈1本 + 側脈4対(濃い同系緑) */}
      <path d={`M0,-3 L0,${-len + 4}`}
        stroke="#3a5c2a" strokeWidth={0.9} opacity={0.6} fill="none" />
      {[0.22, 0.4, 0.56, 0.72].map((t) => sideVeinPair(t, len, W))}
      {/* 光源側の縁のリムライト */}
      <path d={rightEdgePath} stroke="#fff8d0" strokeWidth={1} opacity={0.7} fill="none" />
    </>
  );
}

// 透けの強い葉(明るい黄緑)と弱い葉(深緑)を混ぜる
const leaves = [
  { x: 70,  y: 186, angle: 14,  len: 96,  tone: "deep",   swayDur: 8.5 },
  { x: 104, y: 192, angle: -8,  len: 112, tone: "bright", swayDur: 6.1 },
  { x: 142, y: 194, angle: 5,   len: 84,  tone: "mid",    swayDur: 7.3 },
  // ...右側のクラスター3枚
];

<svg viewBox="0 0 400 180" preserveAspectRatio="none">
  <defs>
    {/* 逆光の透過色: 根元が濃く、先端へ透ける */}
    <linearGradient id="leaf-bright" x1="0" y1="1" x2="0" y2="0">
      <stop offset="0%" stopColor="#6f9c3c" />
      <stop offset="55%" stopColor="#9bc44a" />
      <stop offset="100%" stopColor="#c8e06a" />
    </linearGradient>
    {/* leaf-mid / leaf-deep / leaf-hi(radial) も同様に定義 */}
  </defs>
  {leaves.map((lf, i) => (
    <g key={i} transform={`translate(${lf.x},${lf.y}) rotate(${lf.angle})`}>
      {/* 葉全体のrotate + 先端側の位相差rotateの2段で「しなり」 */}
      <g className="leaf-sway" style={{ "--dur": `${lf.swayDur}s` }}>
        <g className="leaf-bend">
          <LeafShape len={lf.len} tone={lf.tone} />
        </g>
      </g>
    </g>
  ))}
</svg>
css
.backlit-stage {
  position: relative;
  height: 180px;
  border-radius: 8px;
  overflow: hidden;
  /* 光源=右上の白っぽい光から外側へ黄緑 */
  background: radial-gradient(circle at 60% 20%, #f5edc8 0%, #d9e0a8 40%, #a8c47a 100%);
}
/* 光源の大きく柔らかいにじみ。ごくゆっくり明滅(17s) */
.backlit-bloom {
  position: absolute;
  top: -40px; right: -30px;
  width: 200px; height: 170px;
  background: radial-gradient(closest-side, rgba(255,255,255,0.55), transparent 70%);
  animation: bloomPulse 17s ease-in-out infinite alternate;
}
@keyframes bloomPulse { from { opacity: 0.65; } to { opacity: 1; } }
/* 葉の重なりはmultiplyで暗くなる = 透過光ならではの重なり表現 */
.leaf-base { mix-blend-mode: multiply; }
/* 付け根支点の揺れ(±2deg)。葉ごとに周期を変える */
.leaf-sway {
  transform-box: fill-box;
  transform-origin: bottom center;
  animation: leafSway var(--dur) ease-in-out infinite;
  animation-delay: var(--delay);
}
@keyframes leafSway {
  0%, 100% { transform: rotate(-2deg); }
  50%      { transform: rotate(2deg); }
}
/* 先端側の追加rotate。位相を-1.3sズラして「しなり」を出す */
.leaf-bend {
  transform-box: fill-box;
  transform-origin: center bottom;
  animation: leafBend var(--dur) ease-in-out infinite;
  animation-delay: calc(var(--delay) - 1.3s);
}
@keyframes leafBend {
  0%, 100% { transform: rotate(-1deg); }
  50%      { transform: rotate(1deg); }
}
/* 透過光の明滅(±20%)。雲や上の枝が光を変化させる気配 */
.leaf-hi { animation: leafGlow var(--hdur) ease-in-out infinite alternate; }
@keyframes leafGlow { from { opacity: 0.72; } to { opacity: 1; } }
📖 関連: SVG

🎨 林間の光と舞う塵

📝 表現の語彙

日本語: 光芒、林の光、舞う塵、きらめく埃、チンダル現象
英語: forest light shafts, dust motes, sunbeams, tyndall effect
抽象キーワード: stillness, sacredness, atmosphere

💬 AIへの指示テンプレ

「朝の林に斜めの光が差し込む背景を作ってください。木の幹のシルエット2〜3本、斜め20度の光の帯2〜3本(mix-blend-mode: screen + blur)、浮遊する塵10個以上を重ねます。重要: 塵は普段ほぼ見えず(opacity 0.05)、光の帯を横切るタイミングで明るくきらめく(opacity 0.7 + わずかにscaleアップ)ようにしてください。漂いと明滅は塵ごとに独立した素数的周期で。」

🎯 似合うシーン

神社仏閣・ウェディング・写真スタジオ・高級旅館・自然保護系・朝活/モーニング系サービス

🔧 実装手法

斜めの光帯(screen+blur)+ 塵の「光帯内だけきらめく」明滅設計 + 幹シルエット + 朝靄

📄 コード例を見る
tsx
// 浮遊する塵12個。「光の帯の中にいる時だけ見える」のがリアルさの核
const dust = Array.from({ length: 12 }, (_, i) => {
  const j1 = ((i * 7919) % 100) / 100;
  const j2 = ((i * 104729) % 100) / 100;
  const j3 = ((i * 1299709) % 100) / 100;
  return {
    left: 8 + ((i * 9973) % 80),          // %
    top: 10 + ((i * 6271) % 72),          // %
    size: 1.5 + j1 * 1.5,                 // 1.5〜3px
    xDur: 7 + j2 * 16,                    // 漂い(横)7〜23s
    yDur: 5 + j3 * 14,                    // 漂い(縦)5〜19s
    xAmp: 6 + j1 * 12,
    yAmp: 8 + j2 * 16,
    twDur: 6 + j1 * 7,                    // きらめき 6〜13s
    twDelay: -(j3 * 11),
  };
});

// 斜めの光の帯(3本)。揺れ(rotate)と明滅(opacity)は別周期
const shafts = [
  { left: "30%", width: 84, tilt: 20,   blur: 6, opacity: 0.34, swayDur: 17, flickDur: 19 },
  { left: "48%", width: 46, tilt: 21.5, blur: 4, opacity: 0.5,  swayDur: 13, flickDur: 11 },
  { left: "64%", width: 60, tilt: 18.5, blur: 5, opacity: 0.42, swayDur: 23, flickDur: 13 },
];

<div className="forest-stage">
  {/* 幹のシルエット(リムライト付き) */}
  <svg className="forest-trunks" viewBox="0 0 400 180" preserveAspectRatio="none">
    <path d="M22,0 Q30,40 24,90 Q20,140 28,180 L64,180 Q56,130 60,80 Q64,30 58,0 Z"
      fill="#1f2a1c" />
    <path d="M58,0 Q64,30 60,80 Q56,130 64,180"
      stroke="rgba(233,219,160,0.16)" strokeWidth="2.5" fill="none" />
    {/* ...右の幹・中央の細い幹 */}
  </svg>
  {shafts.map((s, i) => (
    <div key={i} className="shaft-sway"
      style={{ left: s.left, width: s.width, "--tilt": `${s.tilt}deg`,
        animationDuration: `${s.swayDur}s` }}>
      <div className="shaft"
        style={{ "--so": s.opacity, filter: `blur(${s.blur}px)`,
          animationDuration: `${s.flickDur}s` }} />
    </div>
  ))}
  {dust.map((d, i) => (
    // 横の漂い × 縦の漂い × きらめき の3アニメを親子分離で合成
    <div key={i} className="dust-x" style={{ left: `${d.left}%`, top: `${d.top}%`,
      width: d.size, height: d.size, "--xamp": `${d.xAmp}px`,
      animationDuration: `${d.xDur}s` }}>
      <div className="dust-y" style={{ "--yamp": `${d.yAmp}px`,
        animationDuration: `${d.yDur}s` }}>
        <div className="dust-dot" style={{ animationDuration: `${d.twDur}s`,
          animationDelay: `${d.twDelay}s` }} />
      </div>
    </div>
  ))}
  <div className="morning-mist" />
</div>
css
.forest-stage {
  position: relative;
  height: 180px;
  border-radius: 8px;
  overflow: hidden;
  /* 朝の林の深い緑。下端はわずかに明るく=地面の照り返し */
  background: linear-gradient(170deg, #2e3a2a 0%, #3d4a34 50%, #5a6647 100%);
}
.forest-trunks { position: absolute; inset: 0; width: 100%; height: 100%; }
/* 光の帯。上端支点でゆっくり揺れる(±1.5deg) */
.shaft-sway {
  position: absolute;
  top: -50px;
  height: 300px;
  transform-origin: top center;
  animation: shaftSway 17s ease-in-out infinite alternate;
}
@keyframes shaftSway {
  from { transform: rotate(calc(var(--tilt) - 1.5deg)); }
  to   { transform: rotate(calc(var(--tilt) + 1.5deg)); }
}
/* 帯の中心が明るく縁へ溶ける + screen合成。明滅は±30% */
.shaft {
  width: 100%; height: 100%;
  background: linear-gradient(180deg,
    rgba(255,248,224,0.55) 0%, rgba(255,248,224,0.18) 55%, transparent 90%);
  mix-blend-mode: screen;
  animation: shaftGlow 13s ease-in-out infinite alternate;
}
@keyframes shaftGlow {
  from { opacity: calc(var(--so) * 0.7); }
  to   { opacity: var(--so); }
}
/* 塵の漂い: 横と縦を親子で分離(不規則な浮遊になる) */
.dust-x { position: absolute; animation: dustX 12s ease-in-out infinite alternate; }
@keyframes dustX {
  from { transform: translateX(calc(var(--xamp) * -1)); }
  to   { transform: translateX(var(--xamp)); }
}
.dust-y { width: 100%; height: 100%; animation: dustY 9s ease-in-out infinite alternate; }
@keyframes dustY {
  from { transform: translateY(calc(var(--yamp) * -1)); }
  to   { transform: translateY(var(--yamp)); }
}
/* リアルさの核: 塵は普段ほぼ見えず(opacity 0.05)、
   光の帯を横切るタイミングだけ明るくきらめく(0.7 + scaleアップ) */
.dust-dot {
  width: 100%; height: 100%;
  border-radius: 50%;
  background: #fff8e8;
  animation: dustTwinkle 8s ease-in-out infinite;
}
@keyframes dustTwinkle {
  0%, 34%, 66%, 100% { opacity: 0.05; transform: scale(1); }
  47%, 53%           { opacity: 0.7; transform: scale(1.4); }
}
/* 地面付近の朝靄。ゆっくり横流れ(29s) */
.morning-mist {
  position: absolute;
  bottom: 0; left: -30%;
  width: 160%; height: 46px;
  background: radial-gradient(ellipse 50% 100% at 50% 100%,
    rgba(238,238,222,0.16), transparent 75%);
  animation: mistDrift 29s ease-in-out infinite alternate;
}
@keyframes mistDrift {
  from { transform: translateX(-12px); }
  to   { transform: translateX(24px); }
}
📖 関連: フィルター

🎨 壁に揺れる枝影

📝 表現の語彙

日本語: 枝影、影絵、揺れる影、影のゆらぎ
英語: branch shadows, swaying shadows, shadow play
抽象キーワード: afternoon, nostalgia, quietude

💬 AIへの指示テンプレ

「午後の白壁に枝葉の影が揺れる背景を作ってください。枝と葉のシルエットをSVGで描き(影色は黒ではなく壁色の暗色 #8a7a62)、重要: 同じ影を『シャープ層(blur 1px)』と『ボケ層(blur 6px・少しずらす)』の2層で重ねて半影を表現してください。枝は±1.2度・葉群は位相差で±0.8度のゆっくりした揺れ、ボケ層はシャープ層から0.3秒遅らせます。影の中に光斑を1つだけ置いて焦点にしてください。」

🎯 似合うシーン

インテリア/家具EC・住宅/不動産・写真館・カフェ・ライフスタイルメディア・午後の時間帯訴求

🔧 実装手法

2層影(シャープ+ボケ)による半影 + 枝/葉の位相差揺れ + 環境色の影 + 光斑の焦点

📄 コード例を見る
tsx
// 枝影。本物の影は「1枚のくっきりした影」ではなく、
// シャープ層(blur 1px)+ ボケ層(blur 6px・少しずらす)の2層で半影(ペナンブラ)を作る
// 枝は制御点を数値で持ち、葉の付け根を曲線上に正確に置けるようにする
const SHADOW_BRANCH_PTS = [
  [-12, 16, 48, 30, 108, 24, 176, 48],   // 太い主枝(左上から)
  [176, 48, 214, 62, 248, 60, 284, 78],  // 先へ行くほど細く
  [92, 22, 128, 8, 166, 8, 206, 0],
  [222, 60, 248, 44, 278, 40, 306, 30],
];
const SHADOW_BRANCHES = SHADOW_BRANCH_PTS.map((p, i) => ({
  d: `M${p[0]},${p[1]} C${p[2]},${p[3]} ${p[4]},${p[5]} ${p[6]},${p[7]}`,
  w: [7, 4, 3, 2.2][i],
}));

// 3次ベジェ上の点と接線角度
function cubicAt(p, t) {
  const mt = 1 - t;
  const x = mt ** 3 * p[0] + 3 * mt ** 2 * t * p[2] + 3 * mt * t ** 2 * p[4] + t ** 3 * p[6];
  const y = mt ** 3 * p[1] + 3 * mt ** 2 * t * p[3] + 3 * mt * t ** 2 * p[5] + t ** 3 * p[7];
  const dx = 3 * mt ** 2 * (p[2] - p[0]) + 6 * mt * t * (p[4] - p[2]) + 3 * t ** 2 * (p[6] - p[4]);
  const dy = 3 * mt ** 2 * (p[3] - p[1]) + 6 * mt * t * (p[5] - p[3]) + 3 * t ** 2 * (p[7] - p[5]);
  return { x, y, deg: (Math.atan2(dy, dx) * 180) / Math.PI };
}

// 葉シルエット14枚。付け根(local原点)を必ず枝の曲線上に置き、
// 角度・サイズだけを擬似乱数でジッターする(葉が枝から浮かない)
const SHADOW_LEAF_SPOTS = [
  { b: 0, t: 0.78 }, { b: 0, t: 0.92 },
  { b: 1, t: 0.3 }, { b: 1, t: 0.55 }, { b: 1, t: 0.78 }, { b: 1, t: 1 },
  { b: 2, t: 0.45 }, { b: 2, t: 0.68 }, { b: 2, t: 0.88 }, { b: 2, t: 1 },
  { b: 3, t: 0.4 }, { b: 3, t: 0.66 }, { b: 3, t: 0.86 }, { b: 3, t: 1 },
];
const shadowLeaves = SHADOW_LEAF_SPOTS.map((s, i) => {
  const j1 = ((i * 7919) % 100) / 100;
  const j2 = ((i * 104729) % 100) / 100;
  const { x, y, deg } = cubicAt(SHADOW_BRANCH_PTS[s.b], s.t);
  const side = i % 2 ? 1 : -1;
  // 枝先(t=1)の葉は枝の延長方向へ、途中の葉は左右交互に側方へ開く
  const tipDeg = s.t === 1 ? deg + (j1 - 0.5) * 30 : deg + side * (50 + j2 * 45);
  return {
    x, y,
    angle: tipDeg + 90,        // 葉localは先端が上向き(-90°)なので+90
    scale: 0.5 + j1 * 0.55,
  };
});

// 影は黒ではなく「壁色の暗色」#8a7a62(影は環境色を含む)
function BranchShadow({ lag }) {
  return (
    <g className="shadow-branch" style={{ animationDelay: `${-lag}s` }}>
      {SHADOW_BRANCHES.map((b, i) => (
        <path key={i} d={b.d} stroke="#8a7a62" strokeWidth={b.w}
          fill="none" strokeLinecap="round" />
      ))}
      {/* 葉群は枝とは別グループ・位相差で揺らして「しなり」 */}
      <g className="shadow-leaves" style={{ animationDelay: `${-1.3 - lag}s` }}>
        {shadowLeaves.map((l, i) => (
          <path key={i} d="M0,0 C-5,-8 -4,-17 0,-25 C4,-17 5,-8 0,0 Z" fill="#8a7a62"
            transform={`translate(${l.x},${l.y}) rotate(${l.angle}) scale(${l.scale})`} />
        ))}
      </g>
    </g>
  );
}

<div className="wall-stage">
  <div className="wall-light" />
  <div className="shadow-group">
    {/* ボケ層: 同じ形を少しずらし、アニメは0.3s遅らせる → 空気の層を感じる */}
    <svg className="shadow-layer shadow-soft" viewBox="0 0 400 180" preserveAspectRatio="none">
      <BranchShadow lag={0.3} />
    </svg>
    <svg className="shadow-layer shadow-sharp" viewBox="0 0 400 180" preserveAspectRatio="none">
      <BranchShadow lag={0} />
    </svg>
  </div>
  {/* 影の中に1箇所だけ光斑(葉の隙間から漏れた光)= 画の焦点 */}
  <div className="shadow-patch" />
</div>
css
.wall-stage {
  position: relative;
  height: 180px;
  border-radius: 8px;
  overflow: hidden;
  /* 午後の生成りの壁。下にいくほど暖色=西日 */
  background: linear-gradient(165deg, #f7f0e3 0%, #efe4d0 60%, #e8dabf 100%);
}
/* 右上の窓からの光の気配 */
.wall-light {
  position: absolute;
  top: -50px; right: -40px;
  width: 240px; height: 200px;
  background: radial-gradient(closest-side, rgba(255,255,255,0.42), transparent 72%);
}
/* 影全体の明滅(±15%、23s)= 雲の通過 */
.shadow-group {
  position: absolute; inset: 0;
  animation: cloudPass 23s ease-in-out infinite alternate;
}
@keyframes cloudPass { from { opacity: 1; } to { opacity: 0.82; } }
.shadow-layer { position: absolute; inset: 0; width: 100%; height: 100%; }
/* 半影の2層: 光源に近い枝はシャープ、遠い葉はボケる */
.shadow-sharp { filter: blur(1px); opacity: 0.25; }
.shadow-soft  { filter: blur(6px); opacity: 0.15; transform: translate(6px, 5px); }
/* 枝全体: 根元支点の±1.2deg(7s) */
.shadow-branch {
  transform-box: fill-box;
  transform-origin: left center;
  animation: branchSway 7s ease-in-out infinite;
}
@keyframes branchSway {
  0%, 100% { transform: rotate(-1.2deg); }
  50%      { transform: rotate(1.2deg); }
}
/* 葉群: 位相差の±0.8deg(5s、delay -1.3s)でしなり */
.shadow-leaves {
  transform-box: fill-box;
  transform-origin: center;
  animation: leavesSway 5s ease-in-out infinite;
}
@keyframes leavesSway {
  0%, 100% { transform: rotate(-0.8deg); }
  50%      { transform: rotate(0.8deg); }
}
/* 影の中の光斑(焦点)。ゆっくり明滅 */
.shadow-patch {
  position: absolute;
  left: 58%; top: 26%;
  width: 46px; height: 32px;
  border-radius: 50%;
  background: radial-gradient(closest-side,
    rgba(255,244,210,0.85), rgba(255,244,210,0.3) 60%, transparent);
  animation: patchGlow 9s ease-in-out infinite alternate;
}
@keyframes patchGlow { from { opacity: 0.55; } to { opacity: 0.95; } }
📖 関連: キーフレームアニメーション

よくある質問

木漏れ日の光斑はなぜ丸いの?

葉の隙間がピンホールカメラの穴の役割をして、太陽の像(円形)を地面に投影しているため。隙間の形がどんなにギザギザでも、投影される光斑は丸みを帯びる。デモでも光斑は楕円で作るのが「本物らしさ」の鍵で、葉の形の光斑を作ると不自然になる。

木漏れ日表現と「癒し」表現の使い分けは?

木漏れ日は「自然・屋外・午後の時間」という具体的な情景を伴うモチーフ表現、癒しは情景を特定しない抽象的な印象表現。ブランドに自然や植物の文脈があるなら木漏れ日、文脈なしで穏やかさだけ欲しいなら癒しの表現(呼吸するグラデーション等)が向く。両方を組み合わせることも多い。

影の表現で黒を使わないのはなぜ?

現実の影は真っ黒ではなく、空や周囲からの環境光で照らされた「環境色を含む暗色」のため。白壁の影なら壁色の暗いトーン(ベージュ系の暗色)、緑の地面なら緑の暗色を使うと自然になる。黒い影は人工的・グラフィック的な印象になる。

AIに木漏れ日の演出を頼むコツは?

「木漏れ日風に」だけでは光の形や動きがバラつく。本ページの要素を分解して指示するのが確実: 「輪郭のボケた楕円の光斑をscreen合成で」「影はシャープ層とボケ層の2層で」「塵は光の帯の中だけきらめく」など。光学的な理屈(光斑は丸い・影は環境色)も添えるとAIの出力が安定する。