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

オーロラ 背景 9種 — カーテン・光の筋・湖の映り込み・稜線

オーロラ 背景は、画像ファイルを使わずにキャンバスが毎フレーム光の帯をすべて計算して描く、ページの最初の画面に敷く動く夜空です。オーロラをグラデーション一枚でまねると、壁紙のように止まって見えます。

暗い舞台で動かしてみる夜空の九場面

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

何も触らなくても九つの場面はそれぞれ動き続け、各マスの「動きを止める」ボタンでいつでも止められます。05と09では、シードを固定した仮のポインターが動き続けます。05は周期の違う二つのサイン波を組み合わせた道筋を回り、09は一つのサイン波で左右に往復します。人がポインターを乗せるとその位置に切り替わり、舞台から外れると仮の動きに戻ります。08は2,600msで一周する実演の中で、200msにダイヤルを9へ、1,300msに2へ動かし、そのときつまみも一緒に動きます。

並べ方は、描き方が一段ずつ変わるように決めました。01はバッファを持たずに縦の線をベクターで引くカーテン、02は同じ帯を小さなピクセルバッファでマスごとに塗る方式、03はそのバッファに、読む座標からゆがめたノイズを塗る方式です。04は描き終えた空を逆さにしてもう一度使う映り込み、05は塗る代わりに光の粉900個を流れ場で運ぶ粒子、06はオーロラの強さを星の位置で読み直す標本です。07は同じ強さを見出しの箱の範囲で平均して下敷きの濃さに変え、08は人が選んだ値をばねで受け止め、09は01のカーテンと04の映り込みに山の稜線を足して一つの場面にまとめます。

オーロラ 背景 9種 — ノイズのカーテン・縦の光の筋・ゆがめた空・湖の映り込み・ポインターの風・星の隠れ・見出しの下敷き・強さのダイヤル・山の稜線のヒーロー

九つの場面はキャンバス2DとJavaScriptだけで描いています。CSS側の@keyframesとfilter: blurは0個で、ライブラリ・WebGL・CDN・画像ファイルもありません。時間はrequestAnimationFrameが渡す間隔を16msの固定ティックに区切り、1フレームで追いつくのは最大6ティック・100msまでで、ノイズが読む時間の座標はこうして積んだsimMsを1,000で割った秒です。キャンバスの画素倍率はMath.min(window.devicePixelRatio || 1, 2)で2を上限にしています。

2秒の映像を23回見比べた動きの量

各場面を2秒間撮って24枚に分け、隣どうしの違いを23回見比べました。累積面積はその間に変化した場所を全部重ねた広さが舞台に占める割合、動いたフレームは23回のうち画面が変わった回数です。

場面 累積面積 動いたフレーム 強度平均
01 ノイズのカーテン 36.2% 23/23 22.4
02 縦の光の筋 26.7% 23/23 17.2
03 ゆがめた空 67.9% 23/23 18.4
04 湖の映り込み 43.7% 23/23 16.2
05 ポインターの風 71.0% 23/23 53.1
06 星の隠れ 34.8% 23/23 22.4
07 見出しの下敷き 69.5% 23/23 27.8
08 強さのダイヤル 36.0% 23/23 26.2
09 稜線のヒーロー 41.3% 23/23 18.3

九場面とも23/23で、見比べるたびに画面が変わっていました。05の71.0%と強度53.1がいちばん大きいのは、光の粉900個が舞台全体に散って流れ、残像が7.3ティックで半分まで薄れるので、画面のほぼどこでも明るさが変わり続けるためと考えられます。02が26.7%でいちばん小さいのは、光の筋がバッファ100行のうち帯の高さ55行の中でしか点かず、その上の夜空はそのまま残るためと考えられます。

旅行のトップ画面から天気の小窓まで — 置き場所の目安

最初の画面をまるごと任せたいなら01・03・09です。01のカーテンは一層目の中心線が舞台の下寄り、およそ213〜232pxに敷かれるため、その上に広がる暗い空にサービス名の見出しを置きやすくなります。03は会社紹介のランディングのように、境目のない色のにじみがゆっくり混ざってほしい場所に、09は旅行プランを紹介するページの冒頭を山・空・湖で埋めたいときに向きます。水辺の景色が似合う湖畔の宿の案内には04、星が主役になる星空観察イベントの告知には06、光の筋に拍子を刻ませたい音楽アルバムの紹介には02が合います。

キャンバスで描くオーロラ アニメーションをページに敷くとき、この編では文字をキャンバスに描きません。07の見出し「新しいワイヤレスイヤホン、今夜公開」は<p data-role="headline">の中の本物の文字で、place()が見出しの箱を舞台に対する百分率で配置し、キャンバスはaria-hidden="true"でその後ろに敷かれます。08を天気アプリの夜空の小窓に使うなら、範囲入力の代わりに受け取った値をlevelTargetに入れても、同じばねが受け止めると考えられます。

オーロラ グラデーション一枚だと、なぜ止まった壁紙のように見えるのか?

グラデーションが動かせるのは置き場所と色だけ

CSSで作るグラデーションは色を置く場所を最初に決めておき、アニメーションはその塊全体を動かすか、回すか、色を変えることしかしません。塊の中のきめは変わらないので、しばらく眺めていると同じ模様が滑っているだけに見え、それが壁紙のように止まって感じる正体と考えられます。夜空の光の帯らしく見せるには、裾がくっきりしていて、上へ行くほど縦の筋になって薄れ、ひだの形が時間とともに新しく生まれる必要があります。三つとも横の位置と時刻ごとに違う値が要るので、この編のオーロラ 作り方は、その値をノイズの計算で生み出すところから始まります。

これまでの三つのオーロラとの分かれ目

CSS 背景アニメーションの03はradial-gradient3つにfilter: blur(3px)と回転のキーフレームをかけた色の塊で、オーロラのメッシュグラデーションのランディングの01はradial-gradient5つにblur(28px)と漂うキーフレームを重ねたセクションの飾りです。キャンバスのノイズ地形の05はオクターブの和のノイズで帯を塗ったキャンバスのオーロラでしたが、項目は一つだけでした。この編はキーフレームとblurを一つも使わず、同じノイズの材料からカーテン・光の筋・映り込み・流れ・星の隠れ・コントラストまで九つの道筋に分けます。

ノイズを重ねてゆがめる計算 — オクターブの和・ドメインワープ・渦の流れ場で光の帯を作る

整数の格子のハッシュからオクターブの和まで

材料は一つです。hash2が整数の格子点ごとに0〜1の値を出し、noise2は点が入ったマスの四隅の値を3t²−2t³の曲線で混ぜて、なめらかな値ノイズを返します。octavesは周波数を2倍に上げて強さを半分にしたノイズをn層足し、強さの合計で割って、大きなうねりに細かいさざ波が乗った値を0〜1に収めます。01の中心線はこの和を3層使って高さを決め、ひだの明るさは、きめを2.6倍細かく読んだノイズをしきい値0.25の上で引き伸ばして1.8乗し、明るいひだと暗いすき間を分けます。

読む座標をずらすワープと、傾きを回した流れ

03のドメインワープは、ノイズの値は変えずにノイズを読む場所を動かします。qを二つ読んでpを1.6qだけずらした場所でrを二つ読み、さらにpを1.6rだけずらした場所で最終の値を読むので、オクターブの和5回×3オクターブでピクセル一つにノイズ15回、120×75のバッファ9,000マスなら1フレームで135,000回になります。開発時の手元のヘッドレス計測では、paintWarp一回が2.07msでした。

05はポテンシャルψの縦横の偏微分を90度回して速度に使います。傾きをそのまま使うと粉は山の頂か谷底へ寄っていきますが、回した流れは等高線に沿って回るだけなので、ある場所へ入る量と出る量がつり合います。数値で見ると、回転の流れ場の発散の大きさは平均2.27e-06、同じポテンシャルの傾きの場は0.013で約5,700倍の開きがあり、粉が流れても一か所に固まったり空っぽの場所ができたりしない根拠はこの開きにあると考えられます。

小さなバッファに塗って引き伸ばして足す

マスごとに値を書く02・03・06・07・08は、画面の大きさに関係なく小さなバッファを使います。makeBufferが画面外のキャンバスとcreateImageDataを用意し、blitBufferがputImageDataでバッファを載せてから、imageSmoothingQuality = 'high'と'lighter'の合成で480×300の舞台に引き伸ばして足します(createImageData の解説)。バッファの大きさは02が160×100、03・07・08が120×75、06が96×60です。画面を読み戻すgetImageDataは九場面のどこにもないので、07の明るさも自分で塗った値から計算します。

01ノイズで揺らめくオーロラのカーテン

三層のカーテンそれぞれに3px間隔で縦の線を引き、線の下端はオクターブの和が決めた中心線、上へ行くほどexp(-RAY_FALLOFF × d)で薄れて38.5pxごとに明るさが半分になります。線一本が縦のグラデーション一つで塗られるベクターの筆跡なのでピクセルバッファは要らず、長さ165pxの先端ではアルファの倍率が0.051まで下がって、カーテンの上の方が夜空に溶けていきます。1秒間に一層目の中心線がいちばん大きく動いた距離は20.1pxで、サービスのトップ画面のように長く表示しておく画面でも揺れはゆったりしています。

ノイズで揺らめくオーロラのカーテン
新しいタブで開く

01 · ノイズで揺らめくオーロラのカーテン — 計測 累積面積 36.213% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

CURTAIN_LAYERSNOISE_SCALERAY_FALLOFF
        for (let layer = 0; layer < CURTAIN_LAYERS; layer += 1) {
            const k = CURTAIN_LAYERS > 1 ? layer / (CURTAIN_LAYERS - 1) : 0;
            const layerAlpha = o.alpha * (1 - 0.35 * k);
            for (let x = -STROKE_STEP; x <= w + STROKE_STEP; x += STROKE_STEP) {
                const fold = curtainFold(o, layer, x, t);
                if (fold < 0.02) {
                    continue;
                }
                const yc = curtainCenter(o, layer, x, t);
                const len = o.len * (0.55 + 0.75 * fold) * (1 - 0.25 * k);
                const top = yc - len;
                const g = c.createLinearGradient(0, yc + 6, 0, top);
                g.addColorStop(0, rgba(o.low, 0));
                // 下端 6px — 短く明るくしてカーテンの裾をはっきりさせる
                g.addColorStop(6 / (len + 6), rgba(o.low, layerAlpha * fold));
                for (let s = 1; s <= 4; s += 1) {
                    const d = len * s / 4;
                    g.addColorStop((6 + d) / (len + 6), rgba(mix(o.low, o.high, s / 4), layerAlpha * fold * Math.exp(-RAY_FALLOFF * d)));
                }
                c.fillStyle = g;
                c.fillRect(x, top, STROKE_STEP + 0.6, len + 6);
            }
        }

02ピクセルごとに計算してちらつく縦の光の筋

160×100のバッファ16,000マスごとに、その列の光の強さと高さのグラデーションを掛けてアルファにし、バッファを3倍に引き伸ばして舞台に足すので、針のような光の筋が列ごとに別々に明るくなっては消えます。列の強さは速いノイズと遅いノイズを混ぜてなめらかな段差の関数に通してから2乗し、ちらつきは列ごとに0.6〜1.4倍と速さの違うサインが明るさを0.85倍まで押し下げます。5.2秒の場面では160列のうち151列が点いていて、音楽アルバムの紹介ページのように帯全体を光の筋で埋めたいときに向きます。

ピクセルごとに計算してちらつく縦の光の筋
新しいタブで開く

02 · ピクセルごとに計算してちらつく縦の光の筋 — 計測 累積面積 26.675% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SHADER_WIDTHRAY_FLICKERBAND_HEIGHT
    function rayColumn(col, t) {
        const fast = noise2(col * RAY_GRAIN, t * 0.7, 211);
        const slow = noise2(col * RAY_GRAIN * 0.12 + t * 0.12, 3.5, 223);
        const lit = Math.pow(smooth((fast * 0.6 + slow * 0.7 - 0.42) / 0.5), 2);
        const flick = 1 - RAY_FLICKER * (0.5 + 0.5 * Math.sin(t * FLICKER_SPEED * colRate[col] + colPhase[col]));
        return lit * flick;
    }
    // 帯の下端(バッファの行) — 列に沿ってゆっくり波打つ
    function rayBottom(col, t) {
        return SHADER_HEIGHT * (0.8 + (noise2(col * 0.035 + t * 0.15, 7.5, 229) - 0.5) * 0.22);
    }
    // 高さのグラデーション — 下端より下は2マス以内で消え、上は帯の高さにわたって (1 - u)^1.6 で薄れる
    function heightProfile(row, bottom) {
        const band = BAND_HEIGHT * SHADER_HEIGHT;
        const d = bottom - row;
        if (d < 0) {
            return Math.max(0, 1 + d / 2);
        }
        const u = d / band;
        return u >= 1 ? 0 : Math.pow(1 - u, 1.6) * (0.55 + 0.45 * Math.exp(-d / 3));
    }

03ノイズをノイズでゆがめて流れるやわらかな空

warpedはノイズを読む座標をqの分、さらにrの分だけ1.6倍でずらして読むので、きめが一方向に流れずに渦を巻きます。その値とrの横成分、時間を足した位置を6色の表から選んで塗り、時間の分が毎秒0.05なので色は20秒で一周します。境目のない光のにじみが舞台の67.9%を覆いながらゆっくり混ざり、会社紹介のランディングのヒーローのように色が静かに移り変わってほしい背景に合います。

ノイズをノイズでゆがめて流れるやわらかな空
新しいタブで開く

03 · ノイズをノイズでゆがめて流れるやわらかな空 — 計測 累積面積 67.861% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

WARP_STRENGTHWARP_OCTAVESHUE_DRIFT
    function warped(x, y, t, out) {
        const f = t * WARP_FLOW;
        const qx = fbm(x + f, y, 301);
        const qy = fbm(x + 5.2, y + 1.3 - f, 302);
        const rx = fbm(x + WARP_STRENGTH * qx + 1.7 - f * 0.7, y + WARP_STRENGTH * qy + 9.2, 303);
        const ry = fbm(x + WARP_STRENGTH * qx + 8.3, y + WARP_STRENGTH * qy + 2.8 + f * 0.8, 304);
        out[0] = qx;
        out[1] = rx;
        return fbm(x + WARP_STRENGTH * rx, y + WARP_STRENGTH * ry, 305);
    }
    // 色表の参照 — 0..1 の位置を表の隣り合う二色のあいだで補間する。1 を超えると最初に戻る
    function paletteAt(u) {
        const n = PALETTE_03.length;
        const p = (((u % 1) + 1) % 1) * n;
        const i = Math.floor(p);
        return mix(PALETTE_03[i % n], PALETTE_03[(i + 1) % n], smooth(p - i));
    }

04行ごとに違う波で空を映す湖

地平線176pxより下の水124pxを2pxの行62本に分け、行ごとに鏡の位置にある空の行を、二つのサインの和の分だけ横にずらして描きます。深くなるほど振幅が0.25倍から1倍に大きくなるので、ずれは岸で最大1.09px、画面の下で4.30pxになり、左右に広げた半透明の写し2枚を重ねて、深いところほど映り込みをぼかします。空は画面外のキャンバスに一度だけ描いて上に使い、水にもう一度使うので、湖畔の宿の紹介ページのように水辺を描くときも空を二回計算せずに済みます。

行ごとに違う波で空を映す湖
新しいタブで開く

04 · 行ごとに違う波で空を映す湖 — 計測 累積面積 43.742% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

RIPPLE_AMPRIPPLE_FREQREFLECT_DIM
        for (let y = 0; y < waterH; y += REFLECT_ROW) {
            const depth = y / waterH;
            // 遠近 — 手前(下)の水ほど波が大きく粗い
            const amp = RIPPLE_AMP * (0.25 + 0.75 * depth);
            const ph = y * RIPPLE_FREQ * (1 - 0.45 * depth);
            const dx = amp * Math.sin(ph + t * RIPPLE_SPEED) + amp * 0.45 * Math.sin(ph * 2.3 - t * RIPPLE_SPEED * 1.4);
            // 鏡 — 岸から y だけ下がった行は、空で y だけ上の行を映す
            const sy = clamp(horizon - REFLECT_ROW - y, 0, horizon - REFLECT_ROW);
            c.globalAlpha = 0.88 - 0.3 * depth;
            c.drawImage(src, 0, sy * sd, src.width, REFLECT_ROW * sd, dx, horizon + y, W, REFLECT_ROW);
            // ぼかし — 深いほど左右にずらした写し二枚を半透明で重ねる
            const spread = 1 + 4 * depth;
            c.globalAlpha = 0.22 * depth;
            c.drawImage(src, 0, sy * sd, src.width, REFLECT_ROW * sd, dx - spread, horizon + y, W, REFLECT_ROW);
            c.drawImage(src, 0, sy * sd, src.width, REFLECT_ROW * sd, dx + spread, horizon + y, W, REFLECT_ROW);
        }

05ポインターが風を起こす光の粉の流れ

光の粉900個は、ポテンシャルを上下左右に1.5pxずつ離して測った偏微分を90度回した速度で流れ、ポインターの周りではポインターの速さに2.4倍と距離による減衰を掛けた風が足されます。風はポインターから40px離れると0.779倍、80pxで0.368倍、160pxで0.018倍まで弱まり、ティックごとに黒をアルファ0.09で重ねた上に粉を足して描くので、残像が帯のように残ります。舞台にフォーカスを置いて矢印キーを押すと24pxずつ風を送れるので、ポートフォリオの導入画面のように初めての訪問者に触ってみてほしい場所に使えます。

ポインターが風を起こす光の粉の流れ
新しいタブで開く

05 · ポインターが風を起こす光の粉の流れ — 計測 累積面積 71.011% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SWIRL_SCALEPOINTER_WINDDUST_COUNT
    function potential(x, y, t) {
        return octaves(x * SWIRL_SCALE, y * SWIRL_SCALE + t * 0.05, 2, 501);
    }
    // 回転の流れ場 — v = (∂ψ/∂y, -∂ψ/∂x)。ポテンシャルの等高線に沿って流れるので発散が 0 — 粉が一か所に固まったり空いたりしない
    function flow(x, y, t, out) {
        const e = CURL_EPS;
        const dPdy = (potential(x, y + e, t) - potential(x, y - e, t)) / (2 * e);
        const dPdx = (potential(x + e, y, t) - potential(x - e, y, t)) / (2 * e);
        out[0] = dPdy / SWIRL_SCALE;
        out[1] = -dPdx / SWIRL_SCALE;
    }

06オーロラが通ると隠れるまたたく星

星140個はそれぞれ違う位相と0.6〜1.6倍の速さのサインで1.78〜4.76秒ごとにまたたき、その位置のオーロラの強さaを計算し直して1 - AURORA_OCCLUSION × aを掛けるので、帯に完全に覆われると0.15倍まで暗くなります。半径1.5pxを超える星31個は十字の光条を持ち、光条の長さも明るさに合わせて伸び縮みします。5秒の場面では帯の中にある星11個の明るさの倍率が平均0.441で、星空観察イベントの案内ページのように星が主役の空に光の幕を重ねたいときに向きます。

オーロラが通ると隠れるまたたく星
新しいタブで開く

06 · オーロラが通ると隠れるまたたく星 — 計測 累積面積 34.751% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

STAR_COUNTTWINKLE_SPEEDAURORA_OCCLUSION
    function drawStars06(t) {
        for (let i = 0; i < STAR_COUNT; i += 1) {
            const tw = 0.5 + 0.5 * Math.sin(t * TWINKLE_SPEED * starRate[i] + starPhase[i]);
            const a = auroraAt(starX[i], starY[i], t);
            const vis = (0.3 + 0.7 * tw) * (1 - AURORA_OCCLUSION * a);
            if (vis < 0.02) {
                continue;
            }
            ctx.fillStyle = rgba(STAR_RGB, vis);
            ctx.beginPath();
            ctx.arc(starX[i], starY[i], starR[i], 0, TAU);
            ctx.fill();
            // 大きな星は十字の光条 — 長さも明るさに合わせて伸び縮みする
            if (starR[i] > 1.5) {
                const L = starR[i] * 4 * vis;
                ctx.fillStyle = rgba(STAR_RGB, vis * 0.5);
                ctx.fillRect(starX[i] - L, starY[i] - 0.4, L * 2, 0.8);
                ctx.fillRect(starX[i] - 0.4, starY[i] - L, 0.8, L * 2);
            }
        }
    }

07見出しの裏の明るさを測って自ら濃くなる下敷き

バッファを塗りながら見出しの箱340×76pxの下にあるマスの平均色を出し、3ティック(48ms)ごとにneededAlphaが、その色と下敷きの色を混ぜた輝度が目標0.1142以下になるアルファを二分探索14回で探します。下敷きの実際のアルファは毎ティック目標との差の0.12だけ追いかけて8ティック(128ms)で63%まで届き、明るさは画面を読み戻さずに自分で塗ったバッファの値と空の式から計算します。8秒間、下敷きがなければ3.06:1まで落ちていたコントラストを下敷きが最低5.04:1、平均7.78:1で支えるので、新製品の発売ランディングの見出しのように動く背景の上に文字を載せるときに使います。

見出しの裏の明るさを測って自ら濃くなる下敷き
新しいタブで開く

07 · 見出しの裏の明るさを測って自ら濃くなる下敷き — 計測 累積面積 69.529% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SCRIM_TARGET_RATIOSCRIM_ALPHA_MAXSCRIM_SMOOTH
    function neededAlpha(avg) {
        const goal = (luminance(TEXT_RGB) + 0.05) / SCRIM_TARGET_RATIO - 0.05;
        if (luminance(avg) <= goal) {
            return 0;
        }
        let lo = 0;
        let hi = 1;
        for (let k = 0; k < 14; k += 1) {
            const mid = (lo + hi) / 2;
            if (luminance(mix(avg, SCRIM_INK, mid)) > goal) {
                lo = mid;
            }
            else {
                hi = mid;
            }
        }
        return Math.min(hi, SCRIM_ALPHA_MAX);
    }
    function itemStep(dt) {
        tickNo += 1;
        if (tickNo % SCRIM_EVERY === 0) {
            scrimTarget = neededAlpha(boxAvg);
        }
        scrimAlpha += (scrimTarget - scrimAlpha) * SCRIM_SMOOTH;
    }

08強さのダイヤルを上げるとばねで広がる光の嵐

<input type="range">の0〜10の値を10で割った目標を剛性60・減衰7のばねが追いかけ、その値で帯の高さ(0で30px、1で165px)と流れの速さ、色相のずれ(最大0.35)を一緒に変えます。減衰比が0.452なので0.2から0.9へ上げると、288msで±2%の帯に初めて入り、432msに1.035まで19.2%はみ出してから、1,040ms以降は最後まで帯の中にとどまります。速さはflowPhaseに積んでおくのでダイヤルを一気に回しても模様が跳ばず、天気アプリの夜空の小窓のように一つの値で空の勢いを見せたいときに合います。

強さのダイヤルを上げるとばねで広がる光の嵐
新しいタブで開く

08 · 強さのダイヤルを上げるとばねで広がる光の嵐 — 計測 累積面積 36.038% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

STORM_LEVEL_DEFAULTLEVEL_STIFFNESSHUE_SHIFT_MAX
    // ばね — x は今の強さ(0..1、はみ出すことがある)、v は速度
    const lvl = { x: STORM_LEVEL_DEFAULT / LEVEL_MAX, v: 0 };
    let levelTarget = lvl.x;
    // 流れの位相 — 速さ x 時間を積み上げておく(速さが変わっても模様が跳ばない)
    let flowPhase = 0;
    function springStep(dt) {
        const s = dt / 1000;
        const a = -LEVEL_STIFFNESS * (lvl.x - levelTarget) - LEVEL_DAMPING * lvl.v;
        lvl.v += a * s;
        lvl.x += lvl.v * s;
    }
    function flowSpeed(x) { return lerp(0.25, 2.2, clamp(x, 0, 1.2)); }
    function bandHeight08(x) { return lerp(30, 165, clamp(x, 0, 1.25)); }
    function hueShift08(x) { return HUE_SHIFT_MAX * clamp(x, 0, 1.2); }

09山の稜線・オーロラ・湖の映り込みをまとめた視差ヒーロー

稜線三層は両端の高さを決めたあと、真ん中の点を両隣の平均から揺らし、揺れ幅を0.55倍ずつ縮めながら6回分けて65点にします。ポインターの位置が決めた目標へずれを毎ティック8%ずつ近づけるので95%に届くまで576ms(36ティック)かかり、層ごとに6・14・26pxと違う量だけずれて手前の山ほど大きく動きます。01のカーテンと星70個、稜線を空のキャンバスに描いてから04と同じdrawReflectionで水に映すので稜線まで湖に映り、旅行プランの紹介ページの最初の画面をまるごと任せられます。

山の稜線・オーロラ・湖の映り込みをまとめた視差ヒーロー
新しいタブで開く

09 · 山の稜線・オーロラ・湖の映り込みをまとめた視差ヒーロー — 計測 累積面積 41.267% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

RIDGE_LAYERSRIDGE_ROUGHNESSPARALLAX_LERP
    function midpointRidge(rng, amp) {
        const n = RIDGE_POINTS;
        const a = new Float32Array(n);
        a[0] = (rng() - 0.5) * amp;
        a[n - 1] = (rng() - 0.5) * amp;
        let step = n - 1;
        let spread = amp;
        while (step > 1) {
            const half = step / 2;
            for (let i = half; i < n; i += step) {
                a[i] = (a[i - half] + a[i + half]) / 2 + (rng() - 0.5) * spread;
            }
            step = half;
            spread *= RIDGE_ROUGHNESS;
        }
        return a;
    }
    const ridges = [];
    for (let k = 0; k < RIDGE_LAYERS; k += 1) {
        ridges.push(midpointRidge(rng09, RIDGE_AMP[k]));
    }

大きな画面・明るい見出し・遅い端末 — ピクセル計算のオーロラが壊れるところ

目標6に対して最低5.04が出た理由

07の目標コントラストはSCRIM_TARGET_RATIOの6ですが、8秒(500ティック)の計測で実際の最低は5.04:1でした。下敷きの目標アルファは3ティックごとに決め直され、実際のアルファはその差の12%ずつしか追わないため、明るい帯が見出しの箱へ素早く入ってくる瞬間には下敷きが一拍遅れて濃くなります。この8秒間の最大アルファは0.348で、アルファが0.3を超えたティックは15.6%でした。追いかける割合のSCRIM_SMOOTHを大きくすれば遅れは縮みますが、下敷きが帯に合わせて震えるように変わると考えられるため、この編はなめらかさを選んでいます。

平均色そのものにも穴があります。箱全体の平均でアルファを決めるので、箱の一部だけを細い明るい帯が通ると平均は低めに出て、その行の上の文字は目標より低いコントラストに置かれることがあると考えられます。上限のSCRIM_ALPHA_MAXの0.82もあります。今の色では白い背景が来ても必要なアルファは0.657、混じりけのないミントの背景で0.602なので届きませんが、目標の比を大きく上げたり文字を暗くしたりすると、上限に止められて目標を出せなくなると考えられます。

バッファを舞台の画素どおりにすると64倍

03のバッファ9,000マスを舞台の実際の解像度480×300×倍率2の576,000マスまで広げるとマス数は64倍になり、開発時の2.07msをそのまま掛けると1フレームに約130msかかる計算になります。16msのティックに収まらない数字なので、02・03・06・07・08はバッファを小さく固定し、その代わり大きな画面では引き伸ばした分だけぼやけます。03のような境目のないにじみではぼやけが目立ちにくい一方、02の針のような光の筋は3倍に広がった列の幅が見えることがあります。

カーテンの線引き・ピクセルバッファ・行単位の映り込み・コントラストの下敷きを収めた九場面のindex.htmlと、同じソースから作ったReact版はダウンロードできる圧縮ファイルに入っていて、そのファイルは85c5k85fを入れると開きます。

数百本のグラデーションと、映り込みの行ごとに3回の描画

01は線の候補489本のうち、ひだの明るさが0.02以上の線ごとにcreateLinearGradientを新しく作るので、5.2秒の場面では1フレームに446本ぶんを作ります。0.016〜60秒を16msティックで3,750回たどると最少241・最多489・平均361.7本で、遅い端末ではこれらのオブジェクトがメモリ回収の負担になると考えられ、STROKE_STEPの3を大きくすれば本数は減る代わりに線がまばらになります。04の映り込みは2pxの行ごとにdrawImageを3回呼ぶので、水124px・62行で1フレーム186回になります。09も同じ関数で湖を描きますが、地平線が196pxと低いため水104px・52行で156回です。どちらも表示を大きく広げると、行単位の段差が見えてくると考えられます。

残像のしみと矢印キーが効かない瞬間

05はティックごとに黒をアルファ0.09で重ねて残像を消しますが、8ビットのキャンバスでは暗い値が丸めのせいで0まで下がらず、薄いしみが残ることがあると考えられます。ポインターが舞台の外から戻ってくる瞬間の大きな移動はWIND_CAPの12で切り、風が跳ねないようにしています。05・09の矢印キーは舞台にフォーカスがあるときだけ効き、「動きを止める」ボタンにフォーカスがあると舞台は反応しません。

ノイズの目を細かく・光の筋のちらつきを強く・波を大きくする三つの変形

名前 値 感じ
ノイズの目を細かく 01 NOISE_SCALE 0.004 → 0.012 ノイズの1マスが250pxから83pxに縮み、一層目の中心線が舞台の幅で5回ではなく14回曲がって、平均の傾きが0.045から0.246に増えます。ゆったりしていたひだが、細かく震える炎のように変わります
光の筋のちらつきを強く 02 RAY_FLICKER 0.15 → 0.5 ちらつきが光の筋を押し下げる最低倍率が0.85から0.50に下がり、点いている列の2秒間の相対標準偏差は0.450から0.538に上がります。針のような光の筋が列ごとに速くまたたきます
湖の波を大きく 04 RIPPLE_AMP 3 → 10 映り込みの行の最大のずれが岸1.09px・画面の下4.30pxから3.62px・14.32pxに広がり、くっきりしていたカーテンの映り込みが太い波で崩れます

ちらつきの強さを3倍以上にしても相対標準偏差が約2割しか増えないのは、点いている列の明るさの変化の大半を遅いノイズがすでに作っているためと考えられます。ちらつきはその上に掛かる一層なので、0.85〜1だった掛け算の幅が0.50〜1に広がっても、揺れ全体に占める分は大きくありません。04は振幅に正比例して岸も画面の下も約3.3倍になるので、三つのつまみのうち結果を数字で見積もりやすいのはこれです。

動きを減らす設定では止まった一場面に — 見出しを読みやすく保つアクセシビリティ

5.2秒で止めた一つの時計で、そろって止まる場面

prefers-reduced-motion: reduceが有効だと、九場面とも描画のループを始めず、時計をSTILL_MSの5,200msに止めた一場面だけを描きます(prefers-reduced-motion の解説)。02のちらつき、04・09の波、06のまたたきも同じ時計を読むので同じ瞬間に止まり、ページを開いたまま設定を変えるとchangeのリスナーがその瞬間の場面で止めます。このとき「動きを止める」ボタンは隠れます。ヘッドレスの点検では、01・05・08・09で300ms間隔に撮った2枚が同じでした。

止まった画面でも受け付ける入力

08はダイヤルを動かすとばねを飛ばして目標の強さをすぐに描き、09は視差を目標の位置にすぐ置き、05は24ティックだけ計算した一枚を描き直します。01・02・03・04・06・07には操作するコントロールがなく、止まった場面だけが残ります。

範囲入力・停止ボタン・舞台のフォーカス

08の強さの調整は、キャンバスの上の偽のつまみではなく「光の強さ」の<label>がついた<input type="range">の0〜10なので、矢印・Home・Endキーが標準の動きで値を変えます(range 入力の解説)。停止はaria-pressedを持つ<button>で、押すと名前が「動きを再開する」に変わり、状態の行には「止めました — ボタンで再開できます」と出ます。05・09は舞台がtabindex="0"とrole="group"でフォーカスを受け、05は上下左右に24px、09は左右に30pxずつ矢印キーで動きます。フォーカスの輪は:focus-visibleにクリーム色で描き、状態の行はaria-live="off"で待ってから最初の入力でpoliteに変わり、絵を受け持つキャンバスは九場面ともaria-hidden="true"です。

オーロラ css をサイトの背景に敷くときのよくある質問

オーロラ アニメーションはCSSだけでは作れませんか?

色の塊がゆっくり回ったり流れたりする程度ならCSSでできます。オーロラのメッシュグラデーションのランディングでは、radial-gradientとblur、キーフレームだけでそうしたセクションの飾りを作っています。裾のくっきりしたカーテン、列ごとに別々にちらつく光の筋、行ごとに揺れ方の違う映り込みのように、横の位置と時刻ごとに値が変わるきめには計算が要るので、この編はキャンバスで描きました。計算で描くキャンバスの場面はキャンバスの場面特殊効果にもあります。

大きなモニターでぼやけるとき、バッファを広げてもいいですか?

広げることはできますが、マス数に比例して計算が増えます。03の120×75を縦横2倍にするとマスは4倍になり、開発時の数値からすると一回塗るのに8msを超えると考えられます。01のカーテンのようにバッファを使わずベクターで引く場面は画素倍率2までそのまま描かれるので、大きな画面のトップには、そちらを選ぶ手もあります。

07の下敷きだけを切り出して、別の背景に使えますか?

neededAlphaは箱の下の平均色を一つ受け取るだけなので、関数はそのまま移せます。ただ07は平均色を画面から読まず、自分で塗ったバッファと空の式から計算しているので、別の背景につけるときは、その背景が箱の下に塗った色の平均を別に求めて渡す必要があります。背景を自分で塗らない場合は、画面を読み取って平均を出す段階がもう一つ要ると考えられます。

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

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