GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

css 影 おしゃれ 9パターン — 光の位置から影を計算する場面

css 影 おしゃれ に見せたくてbox-shadow一行で済ませると、光が回っても影は動かず、壁の裏まで光が漏れ、二つの光でも影は一つのまま — 01番・05番・08番が計算で直す。九つの場面はキャンバスで影を計算して描くので、ファイルごと貼ればそのまま動きます。

影の九つの場面をこのページで見る

下の格子は、ページを開いた時点から自動で動いています。光を動かすのは仮想の経路で、キャンバスを押すとそこからポインタと矢印キーが光を動かします。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

css 影 おしゃれ 9パターン — 光から計算しないと影は動かない

九つの場面はどれも480×300(W・H)のキャンバス一枚に描き、入力は光の目標点ひとつだけです。場面は目標点を LIGHT_LERP の割合で追いかけ、毎フレーム影を最初から計算し直します。時間は16ミリ秒(FIXED_DT)ごとに区切って使い、キャンバスを押す前は仮想の経路が光を動かし、押したあとはポインタと矢印キーが動かします。

box-shadow の一行は、光がどこにあるかを知らない値です。css 影のつけ方の定番は box-shadow: 8px 12px 20px のようにオフセットを手で決める方法ですが、その数字は最初から固定なので、光が回っても影はついて回りません。九つの場面は、この数字を手で書かずに、光の位置と高さから取り出す形に変えました。

計算の骨組みは一行です。光(lx, ly)と高さ lh、物体の点(x, y)と高さ h があれば、床の影は 光 + (点 − 光) × lh ÷ (lh − h) の場所に落ちます。点が高いほど分母が小さくなって影は光の反対側へ遠く押され、分母は最小8に固定してゼロ割りを防いでいます。01・02・08・09 はこの式を箱の上面の四点に当てて凸包にまとめ、03・04・06・07 は同じ考えで高さからぼかし・長さ・濃さを取り出し、05 は投影のかわりに光線で光をさえぎります。

文字に値を重ねて立体にする側はcss text-shadow 9パターン — 影の値で立つ文字が扱い、この編は影の形そのものを光の位置から計算することに絞ります。同じ入力なら同じ絵が出ます。種を固定した乱数(02の柱の高さ)は使いますが、Math.random・new Date・fetch・WebGL・ライブラリは九つのファイルのどこにも使っていません。

技法別のコードで見る影

下の断片は各場面の index.html から連続した行をそのまま切り出したもので、最初から最後までつながったファイルは暗号化zipの中で開きます。場面ごとに makeScene が違い、入力・時間・ボタンを受け持つ main は九つとも同じ骨組みです。

01光が回ると床へ投影される影

castPoint が光の高さ220と箱の高さ70から倍率220÷(220−70)=1.47を求めて上面の四つの角を光の反対側の床へ押し出し、boxShadow が床の四点と押し出した四点を凸包で一つの塊にして塗ります。光が楕円の軌道(ORBIT_RADIUS 110)を一周すると、影はいつも反対側に立って一緒に回ります。測定は累積面積63.4%・平均強度40.4・23/23フレームです。

光が回ると床に投影される影
新しいタブで開く

01 · 光が回ると床に投影される影 — 計測 累積面積 63.387% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

LIGHT_HEIGHTORBIT_RADIUSLIGHT_LERPBOX_HEIGHT
    function castPoint(lx, ly, lh, x, y, h) {
        const k = lh / Math.max(lh - h, 8);
        return [lx + (x - lx) * k, ly + (y - ly) * k];
    }
    function boxShadow(lx, ly, lh, x0, y0, x1, y1, h) {
        const base = [[x0, y0], [x1, y0], [x1, y1], [x0, y1]];
        const pts = base.slice();
        for (let i = 0; i < base.length; i += 1) {
            pts.push(castPoint(lx, ly, lh, base[i][0], base[i][1], h));
        }
        return hull(pts);
    }
            path: function (t) {
                const a = PI2 * t / PERIOD_MS;
                return [CENTER_X + ORBIT_RADIUS * Math.cos(a), CENTER_Y + ORBIT_RADIUS * 0.72 * Math.sin(a)];
            },
            pos: function () { return [lx, ly]; },
            update: function (tx, ty) {
                lx = lerp(lx, tx, LIGHT_LERP);
                ly = lerp(ly, ty, LIGHT_LERP);
            },

02ポインタを光にする柱の列

光ひとつが LIGHT_LERP 0.12 の割合で目標点を追い、柱六本は種444で決めた高さ67・101・88・59・64・74に合わせて、それぞれ影を投影し直します。デモの間は仮想の経路で動き、キャンバスを押すとそこからポインタを追います。測定は累積面積35.0%・平均強度44.0・23/23フレームです。

ポインターを光源にした柱の列
新しいタブで開く

02 · ポインターを光源にした柱の列 — 計測 累積面積 34.99% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

LIGHT_LERPPILLAR_COUNTPILLAR_WIDTHLIGHT_HEIGHT
    function makeScene(ctx) {
        let lx = 240;
        let ly = 200;
        const rnd = mulberry32(SEED);
        const heights = [];
        for (let i = 0; i < PILLAR_COUNT; i += 1) {
            heights.push(58 + Math.floor(rnd() * 44));
        }
        function pillarX(i) { return 58 + i * (364 / (PILLAR_COUNT - 1)); }
                for (let i = 0; i < PILLAR_COUNT; i += 1) {
                    const x = pillarX(i);
                    const poly = boxShadow(lx, fy, LIGHT_HEIGHT, x - PILLAR_WIDTH / 2, BASE_Y - PILLAR_DEPTH / 2, x + PILLAR_WIDTH / 2, BASE_Y + PILLAR_DEPTH / 2, heights[i]);
                    fillShadow(ctx, poly, SHADOW_RGB, 0.44);
                }

03高さに応じてぼやける影

板を64本の帯に割って帯ごとに床から浮いた高さを求め、ぼかしを 2 + 高さ×BLUR_PER_HEIGHT、濃さを CONTACT_ALPHA÷(1 + 高さ×0.03)で決めて、帯ごとに縦のグラデーションを塗ります。床に触れた端の帯(高さ0.86)はぼかし2.17px・濃さ0.604、浮いた端の帯(高さ109.14)はぼかし23.83px・濃さ0.145で、一枚の板の中でくっきり具合が分かれます。測定は累積面積32.8%・平均強度44.9・23/23フレームです。

高さに応じてぼやける影
新しいタブで開く

03 · 高さに応じてぼやける影 — 計測 累積面積 32.814% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

BLUR_PER_HEIGHTCONTACT_ALPHALIFT_MAX
                for (let i = 0; i < STRIPS; i += 1) {
                    const h0 = heightAt(i / STRIPS, total);
                    const h1 = heightAt((i + 1) / STRIPS, total);
                    const hm = (h0 + h1) / 2;
                    const xa = Math.round(PLANK_LEFT + (PLANK_LEN * i) / STRIPS + h0 * slide * 0.9);
                    const xb = Math.round(PLANK_LEFT + (PLANK_LEN * (i + 1)) / STRIPS + h1 * slide * 0.9);
                    const blur = 2 + hm * BLUR_PER_HEIGHT;
                    const alpha = CONTACT_ALPHA / (1 + hm * 0.03);
                    const yTop = BASE_Y - 2;
                    const yBot = BASE_Y + 16 + hm * 0.5;
                    const g = ctx.createLinearGradient(0, yTop - blur, 0, yBot + blur);
                    const span = yBot - yTop + 2 * blur;
                    g.addColorStop(0, rgba(SHADOW_RGB, 0));
                    g.addColorStop(clamp(blur * 1.2 / span, 0, 0.45), rgba(SHADOW_RGB, alpha));
                    g.addColorStop(clamp(1 - blur * 1.2 / span, 0.55, 1), rgba(SHADOW_RGB, alpha * 0.85));
                    g.addColorStop(1, rgba(SHADOW_RGB, 0));
                    ctx.fillStyle = g;
                    ctx.fillRect(xa, yTop - blur, xb - xa, span);
                }

04仮想の太陽が弧を描く日時計

太陽の進み具合ひとつから高度を 14° + 56°×sin(π×進み)で決め、影の長さは GNOMON_HEIGHT÷tan(高度)を SHADOW_LEN_MAX の140で打ち切って求めます。向きは太陽の角度の反対側なので、太陽が160°の弧を渡る間、影は長くなって短くなりながら逆向きに回ります。測定は累積面積28.5%・平均強度37.1・23/23フレームです。

仮想の太陽が弧を描く日時計
新しいタブで開く

04 · 仮想の太陽が弧を描く日時計 — 計測 累積面積 28.476% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SUN_ARC_DEGSHADOW_LEN_MAXGNOMON_HEIGHT
        // 太陽の進み u(0 は日の出の直後、1 は日没の直前)を画面の角度に変える
        function sunAngle(u) {
            const mid = 270;
            return (mid - SUN_ARC_DEG / 2 + u * SUN_ARC_DEG) * Math.PI / 180;
        }
        function elevation(u) { return (ELEV_LOW_DEG + ELEV_SPAN_DEG * Math.sin(Math.PI * u)) * Math.PI / 180; }
                const phi = sunAngle(ux);
                const e = elevation(ux);
                const len = Math.min(SHADOW_LEN_MAX, GNOMON_HEIGHT / Math.tan(e));

05壁にさえぎられる光の領域

visibilityPolygon が等間隔の光線 RAY_COUNT 180本と、壁の線分ごとに端点へ向けて −ANGLE_EPS・0・+ANGLE_EPS とずらして放つ三本ずつを集めて角度順に並べ、castRay が最も近い交点を探して、その点をつないで光の多角形にします。この多角形の内側だけを clip して明るく塗るので、壁の裏は暗いまま残ります。測定は累積面積65.6%・平均強度74.3・23/23フレームです。

壁に遮られる光の領域
新しいタブで開く

05 · 壁に遮られる光の領域 — 計測 累積面積 65.603% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

RAY_COUNTWALL_SEGMENTSANGLE_EPSLIGHT_RADIUS
    function castRay(ox, oy, dx, dy) {
        let best = 4000;
        for (let i = 0; i < WALL_SEGMENTS.length; i += 1) {
            const s = WALL_SEGMENTS[i];
            const ex = s[2] - s[0];
            const ey = s[3] - s[1];
            const den = dx * ey - dy * ex;
            if (Math.abs(den) < 1e-9) {
                continue;
            }
            const t = ((s[0] - ox) * ey - (s[1] - oy) * ex) / den;
            const u = ((s[0] - ox) * dy - (s[1] - oy) * dx) / den;
            if (t > 0 && u >= 0 && u <= 1 && t < best) {
                best = t;
            }
        }
        return best;
    }
    function visibilityPolygon(ox, oy) {
        const angles = [];
        for (let i = 0; i < RAY_COUNT; i += 1) {
            angles.push(PI2 * i / RAY_COUNT);
        }
        for (let i = 0; i < WALL_SEGMENTS.length; i += 1) {
            const s = WALL_SEGMENTS[i];
            const a = Math.atan2(s[1] - oy, s[0] - ox);
            angles.push(a - ANGLE_EPS, a, a + ANGLE_EPS);
        }
        angles.sort(function (p, q) { return p - q; });
        const out = [];
        for (let i = 0; i < angles.length; i += 1) {
            const dx = Math.cos(angles[i]);
            const dy = Math.sin(angles[i]);
            const d = castRay(ox, oy, dx, dy);
            out.push([ox + dx * d, oy + dy * d]);
        }
        return out;
    }

06段の高さから計算するカードの影

shadowOf が段の番号ひとつから、オフセット level × ELEV_STEP × (LIGHT_HEIGHT÷180)、ぼかし 2 + level × ELEV_STEP × 0.9、濃さ min(0.12 + 0.045×level, 0.6) を取り出し、キャンバスの shadowBlur で二重に塗ります。段1はオフセット3.33・ぼかし5.6・濃さ0.165、段5は16.67・20.0・0.345で、この三つの値はそのままCSSの box-shadow のオフセット・ぼかし・アルファに移せます。測定は累積面積41.6%・平均強度28.5・23/23フレームです。

層の高さから計算されるカードの影の値
新しいタブで開く

06 · 層の高さから計算されるカードの影の値 — 計測 累積面積 41.603% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ELEV_STEPSHADOW_ALPHA_BASEHOVER_BONUS
    function shadowOf(level, dx, dy) {
        const dist = Math.max(Math.sqrt(dx * dx + dy * dy), 1);
        const reach = level * ELEV_STEP * (LIGHT_HEIGHT / 180);
        return {
            ox: (dx / dist) * reach,
            oy: (dy / dist) * reach + level * 0.5,
            blur: 2 + level * ELEV_STEP * 0.9,
            alpha: Math.min(SHADOW_ALPHA_BASE + level * 0.045, 0.6)
        };
    }
                    const s = shadowOf(level, x + CARD_W / 2 - lx, y + CARD_H / 2 - ly);
                    // 大きくぼかした環境光の層と、短い主の影の層を重ねる
                    ctx.shadowColor = rgba(INK_RGB, s.alpha);
                    ctx.shadowBlur = s.blur;
                    ctx.shadowOffsetX = s.ox;
                    ctx.shadowOffsetY = s.oy;
                    ctx.fillStyle = rgba(CARD_RGBS[i], 1);
                    ctx.fillRect(x, y, CARD_W, CARD_H);

07物が降りるほど濃くなる接触の影

contactAlpha は高さ h を HEIGHT_MAX 96 で割った値から濃さを AO_STRENGTH×(1 − h÷96)² + 0.05 と決め、contactRadius は半径を(AO_RADIUS + 半幅×0.4)×(1 + 1.2×h÷96)で広げます。半幅26の球は床に触れると濃さ0.71・半径36.4で、いちばん高い96では濃さ0.05・半径80.1までほどけ、この影はDOMの box-shadow ではなくキャンバスに塗った楕円です。測定は累積面積16.8%・平均強度41.9・23/23フレームです。

物体が降りるほど濃くなる接触影
新しいタブで開く

07 · 物体が降りるほど濃くなる接触影 — 計測 累積面積 16.829% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

AO_RADIUSAO_STRENGTHHEIGHT_MAX
    function contactAlpha(h) {
        const near = 1 - clamp(h / HEIGHT_MAX, 0, 1);
        return AO_STRENGTH * near * near + 0.05;
    }
    function contactRadius(h, half) {
        return (AO_RADIUS + half * 0.4) * (1 + 1.2 * clamp(h / HEIGHT_MAX, 0, 1));
    }

08二色の光が重なってできる二重の影

光ごとにオフスクリーンの層へ色の溜まりを描き、destination-out で自分の影の多角形だけを消してから、lighter で本体のキャンバスに足します。だから片方の光だけが遮られた所にはもう一方の光の色が残り、二つとも遮られた所だけが床の色のまま最も暗くなります。測定は累積面積66.3%・平均強度34.1・23/23フレームです。

二色の光源が重なって作る二重の影
新しいタブで開く

08 · 二色の光源が重なって作る二重の影 — 計測 累積面積 66.294% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

LIGHT_A_HUELIGHT_B_HUELIGHT_GAPLIGHT_HEIGHT
        function lights(t) {
            const a = t * AXIS_SPEED;
            const dx = Math.cos(a) * LIGHT_GAP / 2;
            const dy = Math.sin(a) * LIGHT_GAP / 2;
            return [
                [clamp(cx + dx, 10, W - 10), clamp(cy + dy, 10, H - 10)],
                [clamp(cx - dx, 10, W - 10), clamp(cy - dy, 10, H - 10)]
            ];
        }
                    lctx.globalCompositeOperation = 'source-over';
                    lctx.clearRect(0, 0, W, H);
                    const g = lctx.createRadialGradient(ls[i][0], ls[i][1], 0, ls[i][0], ls[i][1], POOL_RADIUS);
                    g.addColorStop(0, lightColor(hues[i], 0.62));
                    g.addColorStop(1, lightColor(hues[i], 0.05));
                    lctx.fillStyle = g;
                    lctx.fillRect(0, 0, W, H);
                    lctx.globalCompositeOperation = 'destination-out';
                    lctx.fillStyle = 'rgba(0,0,0,1)';
                    for (let b = 0; b < BLOCKS.length; b += 1) {
                        const r = BLOCKS[b];
                        const poly = boxShadow(ls[i][0], ls[i][1], LIGHT_HEIGHT, r[0], r[1], r[2], r[3], r[4]);
                        polyPath(lctx, poly);
                        lctx.fill();
                    }
                    ctx.globalCompositeOperation = 'lighter';
                    ctx.drawImage(layer, 0, 0);
                }

09日が暮れてランプが点く机のヒーロー

進み具合ひとつから太陽の高さを420から70へ lerp し、ランプの点灯を smooth((p−0.35)÷0.4)、太陽の濃さを 1 − 0.9×smooth((p−0.55)÷0.45) で決めて、同じ箱三つに太陽の影とランプの影を別々に重ねて塗ります。進み0.75では太陽の高さ124.7・ランプ点灯1・太陽の濃さ0.625で二つの影が同時に見え、進み1では太陽の濃さ0.1だけが残ります。測定は累積面積70.1%・平均強度21.4・18/23フレームです。

日が沈みランプが灯る机のヒーロー
新しいタブで開く

09 · 日が沈みランプが灯る机のヒーロー — 計測 累積面積 70.11% · 動いたフレーム 18/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

DUSK_FRAMESLAMP_HEIGHTSUN_HEIGHT_DUSK
                const night = smooth(p);
                const lampOn = smooth((p - 0.35) / 0.4);
                const sunAlpha = 1 - 0.9 * smooth((p - 0.55) / 0.45);
                const desk = [lerp(DESK_DAY[0], DESK_NIGHT[0], night), lerp(DESK_DAY[1], DESK_NIGHT[1], night), lerp(DESK_DAY[2], DESK_NIGHT[2], night)];
                ctx.fillStyle = rgba(desk, 1);
                ctx.fillRect(0, 0, W, H);
                glow(ctx, SUN_SPOT[0] + 20, SUN_SPOT[1], 380, SUN_RGB, 0.7 * sunAlpha);
                glow(ctx, LAMP_SPOT[0], LAMP_SPOT[1], 280, LAMP_RGB, 0.9 * lampOn);
                const sunH = lerp(SUN_HEIGHT_NOON, SUN_HEIGHT_DUSK, smooth(p));
                for (let i = 0; i < ITEMS.length; i += 1) {
                    const r = ITEMS[i];
                    fillShadow(ctx, boxShadow(SUN_SPOT[0], SUN_SPOT[1], sunH, r[0], r[1], r[2], r[3], r[4]), INK_RGB, 0.4 * sunAlpha);
                    fillShadow(ctx, boxShadow(LAMP_SPOT[0], LAMP_SPOT[1], LAMP_HEIGHT, r[0], r[1], r[2], r[3], r[4]), INK_RGB, 0.5 * lampOn);
                }

値が場面を変えるところ — 光源の高さ・光線の数・二つの光の間隔

触ると場面が変わる値は三つです。01の光源の高さ LIGHT_HEIGHT、05の光線の数 RAY_COUNT、08の二つの光の間隔 LIGHT_GAP で、ぼかし具合は03の BLUR_PER_HEIGHT が、cssでいう影の透明度にあたる濃さは06の SHADOW_ALPHA_BASE と ELEV_STEP が決めます。

光源を低くすると影が長く伸びる

LIGHT_HEIGHT 220では箱の上面(高さ70)の倍率は220÷150=1.47です。120に下げると120÷50=2.4になって影がずっと長く伸び、400に上げると400÷330=1.21になって箱のすぐ脇に収まります。css 影 下だけ のように足元へ寄せた影にしたいなら、いちばん効くのは光の高さです。

光線を倍にしても光の形は変わらない

05の RAY_COUNT を180から360に増やすと、多角形の頂点は264個(180+3×28)から444個に増えますが、光の届く形は同じままです。形を決めるのは壁の端点へ向けて放った光線で、等間隔の光線は直線の辺を割るだけなので、増えるのは光線×線分の交差計算の費用だけです。

二つの光の間隔を広げると重なりが減る

LIGHT_GAP を160から260へ広げると、二つの光(色相330と170)の影が箱ごとに分かれる幅が大きくなり、両方の光が遮られて最も暗い重なりの領域は減ります。二つの光は中点を基準に AXIS_SPEED 0.0008ラジアン/msで回る軸の上にあり、間隔はその軸に沿って開きます。

九つを描画して測った値は次の表にまとめました。04と07を除く七つの場面は累積面積が30%を超え、01・05・08・09は60%を超えます。接触の影ひとつだけの07が16.8%でいちばん小さくても、10%を下回った場面はありません。動いたフレームは09だけが18/23で、残りは23/23なので、デモの間じゅう画面はほとんど休まず変わり続けます。

項目 累積面積 平均強度 動いたフレーム
01 光の軌道の投影 63.4% 40.4 23/23
02 ポインタの柱の列 35.0% 44.0 23/23
03 高さ別のぼかし 32.8% 44.9 23/23
04 日時計 28.5% 37.1 23/23
05 壁の遮蔽多角形 65.6% 74.3 23/23
06 段の高さのカード 41.6% 28.5 23/23
07 接触の影 16.8% 41.9 23/23
08 二色の光 66.3% 34.1 23/23
09 ランプのヒーロー 70.1% 21.4 18/23

どこで壊れるか — 狭い画面・光源が物体の中に入るとき・読めないコントラスト

光源が物体の高さまで下がると倍率が跳ねる

01で光源の高さが箱の高さ70以下になると、分母 lh − h が8に固定されて倍率が70÷8=8.75へ跳ねます。光が物体より低いと式そのものが成り立たないので、この固定は安全装置にすぎず、高さは物体より上に置きます。02でも最も高い柱(101)の近くまで LIGHT_HEIGHT を下げると、同じ固定が先にかかります。

太陽が地平線に近いと影が切れる

04で太陽の進みが0.04だと高度は21.0°で、本来の長さは156.2ですが SHADOW_LEN_MAX の140で切れます。同じ場面で進み0.25は高度53.6°・長さ44.2、進み0.5は高度70°・長さ21.8なので、真昼がいちばん影が短くなります。

壁の隙間が狭いと光源が入れない

05は壁の間の隙間が WALL_MARGIN 4の二倍(8)より狭いと、光源が隙間に入れません。pushOut は壁の矩形をマージンぶん広げた範囲に光源が入ると、最も近い外側の辺へ押し出す動作です。

縁では二つの光が近づく

08の二つの光は、それぞれキャンバスの縁から10px内側に収められます。中点が縁の近くへ行くと実際の間隔が LIGHT_GAP より縮み、同じ値でも重なりの領域が大きくなります。

暗い床では影の色をそのまま使えない

01は明るい床 [243, 238, 250] に影の色 [74, 56, 120] を濃さ0.46で重ねます。同じ値を暗い紫の床へ移すと影が床に溶けて読めません。08の床が暗い紫 [58, 50, 86] なのは色光の重なりを見せるための場面上の例外で、08は影を塗らずに光を足す方式なので、暗い床でも重なりが読めます。

狭い画面と動きの判定

キャンバスは aspect-ratio 480 / 300 で幅だけが縮み、九つの場面の横あふれ・スマホ幅あふれの判定はすべていいえです。09の動いたフレームが18/23なのは、デモの仮想の経路が左右の幅全体を往復して両端で減速し、フレームの間で変化のない区間ができるからです。

自分で値を変えて境界を確かめる

02の光源の高さと01の箱の高さ、04の上限と05のマージンを一つずつ変えれば、上の境界が現れる値が見つかります。どの値でどう崩れるかを手で確かめたいなら、下のファイルをパスワード 6bvjpvkj で開いて、これらの定数を順に書き換えてみてください。

バリエーション三つとアクセシビリティ

名前 値 感じ
光源の高さを変える 01 LIGHT_HEIGHT 220 → 120 箱の上面が床に落ちる距離の倍率が1.47(220÷150)から2.4(120÷50)に増えて影がずっと長く伸び、400に上げると1.21倍に減って短くなります。
光線の数を増やす 05 RAY_COUNT 180 → 360 多角形の頂点は264個から444個に増えますが、光の届く形は同じです。増えるのは光線×線分の交差計算の費用です。
二つの光の間隔を変える 08 LIGHT_GAP 160 → 260 二つの光(色相330と170)の影が箱ごとに分かれる幅が広がり、両方の光が遮られて暗い重なりの領域は減ります。

prefers-reduced-motion が有効だと、九つの場面すべてで requestAnimationFrame のループを始めず、1000ミリ秒の時点の絵を一枚だけ描きます。この設定ではボタンの文字が「次の場面」に変わり、押すたびに経路が900ミリ秒だけ進んで補間なしにすぐ描かれ、ポインタや矢印キーの入力も同じようにその場で反映されます。通常の動きではキャンバスを押すまでポインタは光を動かしませんが、この設定ではポインタを動かすだけで光が追います。キャンバスは tabindex 0 なのでフォーカスを受け取り、矢印キーは光を12pxずつ動かし、Enterとスペースはボタンと同じ働きをします。状態の文字は aria-live="polite" の一行で、デモ中・一時停止・直接操作中の切り替えが読み上げられ、ボタンは <button> ひとつで停止と再生を行き来します。

css 影をつけるのはbox-shadow一行で足りるか?

光が固定で形が四角いカードなら足ります。box-shadow はオフセット・ぼかし・色の三つで済み、動かない一覧のカードに光の計算を足しても費用が増えるだけです。値が光の位置に結びつく必要が出たところから、計算の出番になります。

場面ごとに合う置き場所は違います。01は商品の箱を見せる場面、02と09はランディングのヒーロー背景、03は浮いたカードやフローティングボタン、04は時間帯を紹介するセクション、05はダンジョンの地図や部屋の構造の紹介、06はダッシュボードのパネルやカードの重なり、07は机の上の小物や商品写真の合成、08はステージ照明のようなイベントバナーに向きます。06の三つの値はそのままCSSへ移せますが、07は楕円のグラデーションなので、box-shadow の値ではなく濃さと半径の式を借ります。

回転を計算して物を描く場面はサイコロ アニメーション 9パターン — 回転行列で上の目を読む3D場面とコマ 回転 9パターン — 角速度と歳差で描く回転シーンへ続き、影を計算する側は、この編が光ひとつと二つまでを扱います。

css 影 おしゃれ を作る前によくある質問

css 影 内側に落とす影も光から計算できますか?

九つの場面に内側へ落とす影はなく、box-shadow の inset を置き換えるものではありません。ここで計算しているのは物が床に落とす影なので、内側の影が欲しいときは inset のまま、濃さとぼかしの目安だけを06の shadowOf から借りるのが近道です。

自分のページに貼るには何から変えますか?

場面を一つ選び、その index.html の上のほうにある LIGHT_HEIGHT と色の配列から書き換えてください。キャンバスは480×300の比率なので、置く場所に合わせてCSSの幅だけ調整すれば収まります。

ライブラリや画像は必要ですか?

いずれも不要です。影も床も箱もキャンバスの2Dで描いていて、外へ出ていく通信がないので、一つのファイルを置くだけで動きます。

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。