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

css 画像 フィルター 9パターン — 値ひとつで色が変わる

css 画像 フィルターは、値ひとつで印象が大きく変わります。フィルターは写真を台無しにするものだと思っていた — 値ひとつを触るまでは。1番は色の混合値で二色調を立て、5番はノイズの値でフィルムグレインを散らし、9番はカーソルが乗った瞬間その彩度の値を戻す。

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

9つとも同じ手続き的な場面から始まります。buildPhoto()が空とグラデーション太陽、いくつかの図形をseedRand()(シード固定の疑似乱数)で描くだけなので、実在する写真は一枚も使っていません。並び順はそのフレームをどれだけ塗り替えたかの多い順で、1番の二色調(73.1%)が先頭、8番の代表色スウォッチ(6.7%)が最後です。

01二色マッピング値で立てる二色調

1番は各ピクセルの明るさを暗い色と明るい色の間の一点にマッピングし、写真全体をブランドの二色に寄せます。マーケティングLPのヒーロー画像で、写真の色味をブランドカラーに合わせたいときに向きます。この値はDUOTONE_MIX_PERCENTで、キャンバスを1周する毎フレームのループの中、ピクセルごとにmapDuotone()が読み取ります。

二色マッピング値で立てる二色調
新しいタブで開く

01 · 二色マッピング値で立てる二色調 — 計測 累積面積 73.067% · 動いたフレーム 16/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

DUOTONE_MIX_PERCENTmapDuotoneDUOTONE_DARK
  function mapDuotone(lum, mix) {
    var t = mix / 100;
    var dr = DUOTONE_DARK[0] + (DUOTONE_LIGHT[0] - DUOTONE_DARK[0]) * lum;
    var dg = DUOTONE_DARK[1] + (DUOTONE_LIGHT[1] - DUOTONE_DARK[1]) * lum;
    var db = DUOTONE_DARK[2] + (DUOTONE_LIGHT[2] - DUOTONE_DARK[2]) * lum;
    return [dr, dg, db, t];
  }

  var userMix = DUOTONE_MIX_PERCENT;
  if (range) {
    range.value = String(userMix);
    range.addEventListener('input', function () { userMix = Number(range.value); });
  }

面積73.1%・強度21.4・動いたフレームは16/23です。

02チャンネルずれ距離値で裂けるグリッチ

2番は赤・緑・青のチャンネルをそれぞれ違う方向へ少しずつずらし、ずれ距離値が大きくなるほど色が大きく離れて壊れたように見えます。プロモーションバナーのホバーで、一瞬だけ視線を引く壊れ方として使えます。CHANNEL_OFFSET_PXがshiftChannels()の中で赤チャンネルと青チャンネルをどれだけ逆方向へ動かすかを決め、元のバッファから毎回サンプリングします。

チャンネルずれ距離値で裂けるグリッチ
新しいタブで開く

02 · チャンネルずれ距離値で裂けるグリッチ — 計測 累積面積 14.143% · 動いたフレーム 22/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

CHANNEL_OFFSET_PXshiftChannelssampleBase
  function shiftChannels(offset) {
    var idx = 0;
    for (var y = 0; y < CALC_H; y += 1) {
      for (var x = 0; x < CALC_W; x += 1) {
        outBuf[idx] = sampleBase(x - offset, y, 0);
        outBuf[idx + 1] = sampleBase(x, y, 1);
        outBuf[idx + 2] = sampleBase(x + offset, y, 2);
        outBuf[idx + 3] = 255;
        idx += 4;
      }
    }
  }

  function renderFrame(t) {
    var wave = Math.abs(Math.sin(t * Math.PI * 4 / LOOP_MS));
    var offset = Math.round(wave * CHANNEL_OFFSET_PX);
    shiftChannels(offset);
  }

面積14.1%・強度37.5(9つの中で面積20%未満では最も高い値)・動いたフレームは22/23です。

03点サイズ値が印刷質感を作る網点フィルター

3番は写真を明るさに応じた大きさの点の格子に置き換え、HALFTONE_DOT_SIZE_PXが大きいほど昔の新聞印刷のような質感になります。レトロなレイアウトが欲しい印刷物風のポスターカードに向いています。sampleBrightness()が格子の各マスの明るさを読み取り、drawHalftoneDot()がそれを円の半径に変換していて、1番と同じくこの値もドラッグできるレンジ入力として画面に出ています。

点サイズ値が印刷質感を作る網点フィルター
新しいタブで開く

03 · 点サイズ値が印刷質感を作る網点フィルター — 計測 累積面積 62.988% · 動いたフレーム 18/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

HALFTONE_DOT_SIZE_PXdrawHalftoneDotsampleBrightness
  function sampleBrightness(xCalc, yCalc) {
    var xi = Math.max(0, Math.min(CALC_W - 1, Math.round(xCalc)));
    var yi = Math.max(0, Math.min(CALC_H - 1, Math.round(yCalc)));
    var idx = (yi * CALC_W + xi) * 4;
    return (0.299 * baseBuf[idx] + 0.587 * baseBuf[idx + 1] + 0.114 * baseBuf[idx + 2]) / 255;
  }

  function drawHalftoneDot(x, y, size, brightness) {
    var radius = (1 - brightness) * size * 0.62;
    if (radius < 0.4) return;
    ctx.beginPath();
    ctx.arc(x, y, radius, 0, Math.PI * 2);
    ctx.fill();
  }

面積63.0%・強度82.9(9つの中で最も高い値)・動いたフレームは18/23です。

04ぼかし値とノイズ値が重なる磨りガラスフィルター

4番は写真を大きくぼかした上に微細なノイズ値を重ね、奥が透ける磨りガラスの表面のように見せます。モーダル背後の背景画像として、パネルの向こうに何かがあると感じさせたいときに向いています。FROST_BLUR_PXが毎フレームのサインカーブで揺れる基準ぼかし半径で、drawNoiseOverlay()がそのぼかしを平坦に見せないためのノイズを加えます。

ぼかし値とノイズ値が重なる磨りガラスフィルター
新しいタブで開く

04 · ぼかし値とノイズ値が重なる磨りガラスフィルター — 計測 累積面積 43.31% · 動いたフレーム 22/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

FROST_BLUR_PXdrawNoiseOverlaybuildPhoto
  function draw(elapsed) {
    lastElapsed = elapsed;
    var wave = Math.sin(elapsed * Math.PI * 4 / LOOP_MS);
    var blur = Math.max(2, FROST_BLUR_PX + wave * 6);
    ctx.clearRect(0, 0, cw, ch);
    ctx.filter = 'blur(' + blur.toFixed(1) + 'px)';
    ctx.drawImage(photo, 0, 0, cw, ch);
    ctx.filter = 'none';
    drawNoiseOverlay(Math.floor(elapsed / 150));
  }

面積43.3%・強度19.0・動いたフレームは22/23です。

05ノイズ強度値が散らすヴィンテージフィルムグレイン

5番は写真の上に微細な白黒ノイズを重ね、GRAIN_STRENGTHの値が大きいほど古いフィルム写真の質感が濃くなります。エモいフォトギャラリーカードで、粒子感を傷ではなく温かみとして見せたいときに向きます。applyFilmGrain()はシード付き乱数から1ピクセルごとに1つのずれ値を取り出し、三つの色チャンネルへ均等に足すので、グレインが色づかず灰色のまま保たれます。

ノイズ強度値が散らすヴィンテージフィルムグレイン
新しいタブで開く

05 · ノイズ強度値が散らすヴィンテージフィルムグレイン — 計測 累積面積 68.114% · 動いたフレーム 20/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

GRAIN_STRENGTHapplyFilmGraingrainRand
  function applyFilmGrain(strength, bucket) {
    var grainRand = seedRand(414051 + bucket);
    var idx = 0;
    for (var i = 0; i < CALC_W * CALC_H; i += 1) {
      var n = (grainRand() - 0.5) * 2 * strength;
      outBuf[idx] = baseBuf[idx] + n;
      outBuf[idx + 1] = baseBuf[idx + 1] + n;
      outBuf[idx + 2] = baseBuf[idx + 2] + n;
      outBuf[idx + 3] = 255;
      idx += 4;
    }
  }

  function renderFrame(elapsed) {
    var wave = Math.sin(elapsed * Math.PI * 4 / LOOP_MS);
    var strength = Math.max(4, GRAIN_STRENGTH + wave * 14);
    var bucket = Math.floor(elapsed / 90);
    applyFilmGrain(strength, bucket);
  }

面積68.1%・強度17.7(9つの中で面積60%を超える項目では最も低い値)・動いたフレームは20/23です。

06明るさ順の値を色に変える熱画像トーン

6番は暗い部分から明るい部分まで順に青から赤へ色をマッピングし、サーモカメラで撮ったように見せます。データ可視化風フォトカードに使えるアイデア段階の項目です。THERMAL_STOPSがthermalColor()の補間元になる5色のグラデーション表で、THERMAL_SHIFTが毎フレーム全ピクセルの参照位置を一緒にずらします。

明るさ順の値を色に変える熱画像トーン
新しいタブで開く

06 · 明るさ順の値を色に変える熱画像トーン — 計測 累積面積 75.314% · 動いたフレーム 20/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

THERMAL_STOPSthermalColorTHERMAL_SHIFT
  function thermalColor(v) {
    var t = Math.max(0, Math.min(1, v)) * (THERMAL_STOPS.length - 1);
    var i0 = Math.floor(t), i1 = Math.min(THERMAL_STOPS.length - 1, i0 + 1);
    var f = t - i0;
    var a = THERMAL_STOPS[i0], b = THERMAL_STOPS[i1];
    return [a[0] + (b[0] - a[0]) * f, a[1] + (b[1] - a[1]) * f, a[2] + (b[2] - a[2]) * f];
  }

  function renderFrame(elapsed) {
    var wave = Math.sin(elapsed * Math.PI * 4 / LOOP_MS);
    var shift = THERMAL_SHIFT + wave * 0.28;
    var idx = 0;
    for (var i = 0; i < CALC_W * CALC_H; i += 1) {
      var lum = (0.299 * baseBuf[idx] + 0.587 * baseBuf[idx + 1] + 0.114 * baseBuf[idx + 2]) / 255;
      var c = thermalColor(lum + shift);
      outBuf[idx] = c[0]; outBuf[idx + 1] = c[1]; outBuf[idx + 2] = c[2]; outBuf[idx + 3] = 255;
      idx += 4;
    }
  }

面積75.3%(9つの中で最も高い値)・強度72.9・動いたフレームは20/23です。

07縁の値が色収差を作るレンズフィルター

7番は画像の縁に近づくほどチャンネルを少しずつずらし、EDGE_STRENGTH_PXの値が大きいほど中心から離れた場所の色縁が濃くなり、中央はきれいなまま残ります。レンズ風のプロフィール画像に使えるアイデア段階の項目です。distFromCenter()が各ピクセルの位置を0〜1の距離に変換し、renderFrame()がその距離に縁の値を掛けて実際のチャンネルずれ幅を求めます。

縁の値が色収差を作るレンズフィルター
新しいタブで開く

07 · 縁の値が色収差を作るレンズフィルター — 計測 累積面積 13.646% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

EDGE_STRENGTH_PXdistFromCentersampleBase
  function renderFrame(elapsed) {
    var wave = Math.abs(Math.sin(elapsed * Math.PI * 4 / LOOP_MS));
    var strength = EDGE_STRENGTH_PX * wave;
    var cx = CALC_W / 2, cy = CALC_H / 2;
    var idx = 0;
    for (var y = 0; y < CALC_H; y += 1) {
      for (var x = 0; x < CALC_W; x += 1) {
        var ux = (x - cx) / cx, uy = (y - cy) / cy;
        var len = Math.sqrt(ux * ux + uy * uy) || 1;
        var d = distFromCenter(x, y);
        var off2 = strength * d;
        var offX = Math.round((ux / len) * off2), offY = Math.round((uy / len) * off2);
        outBuf[idx] = sampleBase(x - offX, y - offY, 0);
        outBuf[idx + 1] = sampleBase(x, y, 1);
        outBuf[idx + 2] = sampleBase(x + offX, y + offY, 2);
        outBuf[idx + 3] = 255;
        idx += 4;
      }
    }
  }

面積13.6%(9つの中で最も小さい値)・強度37.3・動いたフレームは23/23で、9つの中で唯一すべてのフレームが動いた項目です。

08ピクセル値を読み取る代表色スウォッチ

8番は全ピクセルの値を走査して最も多い色を計算し、その色をそのままカード背景に使うので、写真の色と衝突しません。商品カード背景の同期という発想の元になった項目です。computeDominantColor()は各チャンネルの上位4ビットでピクセルをバケットに分け(近い色をまとめる簡易な方法)、最もピクセル数が多いバケットを読み戻します。

ピクセル値を読み取る代表色スウォッチ
新しいタブで開く

08 · ピクセル値を読み取る代表色スウォッチ — 計測 累積面積 6.676% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

PHOTO_SEEDScomputeDominantColorcyclePhoto
  function computeDominantColor() {
    var bins = {};
    var idx = 0;
    for (var i = 0; i < CALC_W * CALC_H; i += 1) {
      var r = baseBuf[idx] >> 4, g = baseBuf[idx + 1] >> 4, b = baseBuf[idx + 2] >> 4;
      var key = r + '-' + g + '-' + b;
      bins[key] = (bins[key] || 0) + 1;
      idx += 4;
    }
    var bestKey = null, bestCount = -1;
    for (var k in bins) { if (bins[k] > bestCount) { bestCount = bins[k]; bestKey = k; } }
    var parts = bestKey.split('-');
    dominant = 'rgb(' + (parseInt(parts[0], 10) * 16 + 8) + ',' + (parseInt(parts[1], 10) * 16 + 8) + ',' + (parseInt(parts[2], 10) * 16 + 8) + ')';
  }

面積6.7%(9つの中で最も小さい値)・強度19.6・動いたフレームは23/23です。

09彩度の値が戻るモノクロホバーリビール

9番は普段は彩度の値を0に下げて白黒にしておき、カーソルが乗るとその値を元に戻して色を取り戻します。商品グリッドのホバーで、色が注目への見返りのように感じられる場面向けです。値が変わるのはsetHoverState()だけで、CSSのfilter文字列をgrayscale(1) saturate(0)とgrayscale(0) saturate(1)の間で、GRAYSCALE_HOVER_MSが決める時間をかけて切り替えます。

彩度の値が戻るモノクロホバーリビール
新しいタブで開く

09 · 彩度の値が戻るモノクロホバーリビール — 計測 累積面積 58.465% · 動いたフレーム 10/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

GRAYSCALE_HOVER_MSsetHoverStatebuildPhoto
  function setHoverState(on) {
    canvas.style.filter = on ? 'grayscale(0) saturate(1)' : 'grayscale(1) saturate(0)';
  }

  canvas.addEventListener('pointerenter', function () { setHoverState(true); });
  canvas.addEventListener('pointerleave', function () { setHoverState(false); });
  canvas.addEventListener('focus', function () { setHoverState(true); });
  canvas.addEventListener('blur', function () { setHoverState(false); });

  var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  var playing = !reduce;
  if (reduce) { root.classList.remove('is-demo'); setHoverState(true); }

面積58.5%を彩度の一往復で塗り替え・強度33.5・動いたフレームは10/23で、9つの中で最も少ない値です。毎フレームではなくホバーのたびに一度だけ変わるからです。

9枚のcss 画像 フィルターは、実際どれだけ塗り替えたか

項目 面積% 強度 動いたフレーム
01 duotone-colormap 73.1 21.4 16/23
02 chromatic-split-glitch 14.1 37.5 22/23
03 halftone-dot-print 63.0 82.9 18/23
04 frosted-glass-noise 43.3 19.0 22/23
05 vintage-film-grain 68.1 17.7 20/23
06 thermal-heatmap-tone 75.3 72.9 20/23
07 chromatic-edge-aberration 13.6 37.3 23/23
08 dominant-color-swatch 6.7 19.6 23/23
09 grayscale-hover-reveal 58.5 33.5 10/23

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

バリエーション 値の変更 感じ方
二色調の混合比率値を調整する 01 DUOTONE_MIX_PERCENT 30 → 70 値を小さくすると元の色がより多く残り控えめな二色調になり、値を大きくすると二色に完全に寄って、ずっと強い二色調に見えるはずです。
網点サイズ値を調整する 03 HALFTONE_DOT_SIZE_PX 4 → 12 値を小さくすると点が密になり写真に近く見え、値を大きくすると点が太くなり印刷物のようにずっと粗く見えるはずです。
ホバー時の彩度復元時間値を調整する 09 GRAYSCALE_HOVER_MS 200 → 600 値を小さくするとカーソルが乗った瞬間に色が一気に戻り、値を大きくすると色がゆっくりと染み込むように戻るはずです。

prefers-reduced-motion: reduceは1番から8番のis-demoクラスを外して毎フレームのループを止めますが、開始フレームではなくそれぞれ別の代表的な瞬間で止まります — 1番の二色調はDUOTONE_MIX_PERCENTのまま止まり、6番の熱画像トーンは揺れがかからない状態で止まり、5番のグレインはピークではなく基準のGRAIN_STRENGTHで止まります。9番だけは意図的に違う挙動で、彩度の値が毎フレームではなくpointerenter/pointerleaveやfocus/blurでしか変わらないため、モーション低減設定はsetHoverState(true)を一度呼ぶだけでキャンバスを常に色付きのまま残し、同じfocus/blurの組み合わせによってキーボードでタブ移動する読者にもポインターを使わずに色が戻る様子が見えます。

大きな写真や非力な端末でどこが壊れるか

どの項目もgetImageData()・putImageData()でCALC_W × CALC_Hピクセルを毎フレーム最低1回は読み書きします。このデモの480×300キャンバスなら15万読み取り未満で済みますが、同じピクセルごとのループを4000px級のアップロード写真にそのままかけると読み取り回数は50倍以上に跳ね上がり、テストでは一瞬で終わっていたcss フィルター ぼかしの処理が実機でもたつく地点はここです。1・2・5・6・7番は毎フレーム全バッファをループするので実写真の解像度で最初に計測すべき項目で、3・4・8・9番は粗い格子でサンプリングするか、キャンバス自身のctx.filterでぼかすか、ホバー時にしか再計算しないため、大きなサイズでも軽いままです。9枚すべてのindex.html・style.scss一式を開くwd482b7sはこの段落に置いてあります — ここまで読んだ方なら、どのループから計測すべきかもう見えているはずです。

css 画像 フィルターは実際どんな画面で使われるか

css 背景 画像 フィルターのようにページ全体へ一括でかける使い方と違い、この9つは1つのカードや1つのヒーロー画像だけに向けた計算です。1番と3番は<input type="range">として値をそのまま画面に出しているので、ジェネレーターを使わずに読者自身がその場で値を動かして確かめられます。同じ「毎フレーム値をひとつ読む」という考え方でまとめた編としては、CSS 画像 ホバー エフェクト 9選【コピペOK】が9番のようなホバー駆動フィルターに近く、CSS グラスモーフィズム効果9選【コピペOK】は4番のぼかしと重ね合わせの発想を共有していて、CSS フォト ギャラリー 9選 — メイソンリーからライトボックスまではこの9つの多くが実際に収まる先のグリッドです。

css 画像 フィルターは css フィルター モノクロ と何が違うのか?

css フィルター モノクロは彩度をゼロに落とす一つの計算で、9番がまさにその計算だけを行っている項目です。「css 画像 フィルター」という括りはもっと広く、ピクセルの明るさや位置を読んで新しい色を書き戻す計算全体を指し、2番から8番までがオフセット値・ノイズ値・参照テーブルといった別の値でそれを行っています。MDNのfilterリファレンスには4番と9番が直接使うblur()・grayscale()・saturate()が載っていますが、1・2・3・6・7・8番はgetImageData()に対する独自のピクセル計算で、これはネイティブのCSSフィルター関数よりもSVGのfeColorMatrixの考え方に近いです。

css 画像 フィルターを使う前に、よくある質問に答えます

この9つはアップロードした写真や外部ライブラリを読み込みますか?

いいえ。drawPhotoBase()がキャンバスのグラデーションと図形だけで場面を描き、seedRand()のシードが同じなら同じ場面が再現されます。1番の二色調からgrayscale・saturateだけで色を戻す9番まで、画像ファイルもライブラリも一つも読み込みません。css フィルター と は何かを一から試すのに、外部の素材は必要ありません。

9つの中で大きな写真に載せたとき一番重いのはどれですか?

3番の網点は強度が82.9と最も高く、drawHalftoneDot()が明るさの読み取りに加えて格子のマスごとに円を1つ描くためです。6番の熱画像トーンは面積が75.3%と最も広く、全ピクセルが5色のグラデーション参照を受けるためで、どちらも1・2・5・7番より先に実写真の解像度で計測する価値があります。

9番のホバーリビールはホバーではなくクリックにも答えられますか?

はい。setHoverState(on)はすでにpointerenter/pointerleaveとfocus/blurを同じ関数で受けているので、そのリスナーをclickやpointerdownに差し替えるだけで済み、それが駆動しているCSSのfilterトランジション自体には手を加える必要がありません。

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

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