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

css 電卓 デザイン9種 — 入力フォーム デザインが値を描く

css 電卓 デザインの9つのうち、1つは入力したテキストの値を格子値に変えてキャンバスにQR模様を直接描き、1つは生年月日の値から残り日数を計算してリングゲージを満たし、1つは入力した1つの時刻の値で複数のタイムゾーンカードを描き直します。

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

9つに共通しているのは、入力欄に打ち込んだ値がそのまま画面の形を変えるという一手間です。摂氏の値はグラデーション目盛りの上でハンドルを動かし、バイト値は自分で単位を選んでからバッジの棒を満たし、生年月日の値は基準日との差だけ円形リングを塗り、1つの時刻の値は4枚のタイムゾーンカードを一度に描き直し、hex値は白背景・黒背景それぞれとのコントラスト比を計算してバッジの合否を切り替え、度分秒と十進数の2つの欄は互いを追いかけて同期し、打ち込んだ文字列はハッシュ化されてキャンバス上にQR風の格子となり、金額と人数の値はチップ分担リングの一片を満たし、長さの値は2本の目盛りを同時に押します。html css 電卓で検索して見つかる多くは静的な見本かライブラリ任せのウィジェットですが、以下の9パターンはどれも入力リスナー1つ・名前付き関数1つ・requestAnimationFrameで補間されるtarget/rendered値の組だけで動きます。

パターン01は温度の値をどう読んで動くのか

温度の値で満ちるグラデーション温度計インプット
新しいタブで開く

01 · 温度の値で満ちるグラデーション温度計インプット — 計測 累積面積 2.117% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

LERP_RATEcelsiusToFahrenheitrenderedT

上のパターン01を動かしているのはcelsiusToFahrenheitという関数と、renderedTという値の2つだけです。摂氏の入力をapplyValueが受け取って目標値targetTを計算し、requestAnimationFrameのループがrenderedTをその目標へ毎フレーム少しずつ近づけて、ハンドルの位置を書き換えます。残り8つのパターンもこの先に、それぞれのコードと並べて置いてあります。

9つの電卓インプットを実測して比べると

# パターン 累積面積 平均強度 動いたフレーム
01 グラデーション温度計インプット 2.117% 119.9 23/23
02 ファイルサイズ単位バッジ 13.481% 113.3 23/23
03 生年月日リング 2.981% 122.3 23/23
04 タイムゾーンカードスタック 9.295% 93.0 9/23
05 コントラストバッジ 4.078% 109.2 23/23
06 座標同期インプット 3.172% 151.9 23/23
07 テキストQRキャンバス 4.272% 202.8 23/23
08 チップ分担リング 3.385% 116.8 23/23
09 長さ単位スケール 5.763% 123.3 23/23

9つの中で画面を最も広く塗り替えるのはパターン02のバッジで13.481%、逆に最も狭いのはパターン01のハンドルで2.117%です。動いたフレームの列だけはパターン04が浮いていて、23フレーム中9フレームしか「動いた」と判定されておらず、他の8パターンが揃って到達する23/23には届いていません。

1つの値が1つの形だけを動かす — 01・02・05・07

温度がハンドルの位置へ、バイト数が塗りつぶし比率へ、hex値が合否バッジへ、文字列がハッシュ化された格子へ——この4つは打った値1つがそのまま1つの見た目に対応します。

2つの入力欄が1つの数字にまとまる — 03・08

03では生年月日と基準日という2つの値が年齢と残り日数という1つの結果になり、08では総額と人数という2つの値が1人あたりの分担額という1つの結果になります。どちらも片方の欄だけでは出力の意味が成立しません。

1つの値が複数の出力を動かす、または2つの欄が同期する — 04・06・09

パターン04は1つの時刻の値を4枚のカードへ同時に広げます。一方パターン06と09は逆方向で、対になった2つの欄のどちらを書き換えても、もう片方が自動で計算し直されるので、どちらが「本当の」入力かという区別自体がありません。

電卓 デザイン おしゃれに見せる、たった一つの計算の一手間

9つのパターンはどれも似た骨格を持っています。inputかchangeのイベントリスナーが1つ、そのイベントを受けて目標値を計算する名前付き関数が1つ、そしてrequestAnimationFrameのループがrendered系の値をtarget系の値へ毎フレーム少しずつ近づける処理が1つです。見た目の効きは装飾の多さではなく、値が変わるたびに再計算されるこの1つの関数のほうにあります。

01温度の値で満ちるグラデーション温度計インプット

摂氏の値を打ち込むとcelsiusToFahrenheitが華氏の値を同時に計算し、targetTという目標比率をグラデーション目盛りの範囲に合わせて求めます。requestAnimationFrameのループはrenderedTをその目標へ毎フレームLERP_RATEの割合で近づけ、ハンドルの位置を書き換えます。inputイベントのたびにこの一連の計算が最初からやり直されます。

温度の値で満ちるグラデーション温度計インプット
新しいタブで開く

01 · 温度の値で満ちるグラデーション温度計インプット — 計測 累積面積 2.117% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

LERP_RATEcelsiusToFahrenheitrenderedT
  function celsiusToFahrenheit(c) { return c * 9 / 5 + 32; }
  function clamp01(v) { return v < 0 ? 0 : v > 1 ? 1 : v; }

  var SENT_PREFIX = '摂氏', SENT_MID = '度は華氏', SENT_SUFFIX = '度です。';

  var targetT = 0, renderedT = 0;
  var LERP_RATE = 0.14; // 変形01: 下げるとハンドルが値の変化により遅れて追従する

  function readValue() {
    var c = parseFloat(input.value);
    return isNaN(c) ? 0 : c;
  }

  function applyValue(c) {
    var f = celsiusToFahrenheit(c);
    targetT = clamp01((c - MIN_C) / (MAX_C - MIN_C));
    readout.textContent = SENT_PREFIX + c.toFixed(1) + SENT_MID + f.toFixed(1) + SENT_SUFFIX;
    if (reduce) { renderedT = targetT; paint(); }
  }

  function paint() {
    var trackH = scale.clientHeight - handle.offsetHeight;
    var y = (1 - renderedT) * trackH;
    handle.style.transform = 'translateY(' + y.toFixed(1) + 'px)';
  }

02バイト値が自分で単位を選ぶファイルサイズバッジ

打ち込んだバイト値の桁数をdigitsOfが数え、pickUnitがB・KB・MB・GBのどれに当たるかを選び、fillRatioがその単位の中での塗りつぶし比率を計算します。値そのものは変わらなくても、桁数が単位の境界をまたいだ瞬間に塗りつぶし量の基準が変わるので、バーの動きが直感に反して見える場面があります——詳しくは後半で扱います。

バイト値が自分で単位を選ぶファイルサイズバッジ
新しいタブで開く

02 · バイト値が自分で単位を選ぶファイルサイズバッジ — 計測 累積面積 13.481% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

pickUnitdigitsOffillRatio
  function fillRatio(bytes, unit) {
    var v = bytes / Math.pow(1024, unit.pow);
    return clamp01(v / 1024);
  }

  var SENT_PREFIX = '入力した', SENT_MID1 = 'バイトは', SENT_MID2 = '単位で', SENT_SUFFIX = 'です。';

  var targetR = 0, renderedR = 0;
  var LERP_RATE = 0.16; // 変形02: 下げるとバーが値の変化により遅れて追従する

  function readValue() {
    var n = parseFloat(input.value);
    return isNaN(n) || n < 0 ? 0 : n;
  }

  function applyValue(bytes) {
    var unit = pickUnit(bytes);
    var valueInUnit = bytes / Math.pow(1024, unit.pow);
    targetR = fillRatio(bytes, unit);
    unitEl.textContent = unit.name;
    readout.textContent = SENT_PREFIX + bytes + SENT_MID1 + unit.name + SENT_MID2 + valueInUnit.toFixed(2) + SENT_SUFFIX;
    if (reduce) { renderedR = targetR; paint(); }
  }

03生年月日の値が残り日数を満たすD-dayリング

生年月日の値と、別に打ち込む基準日の値をdaysBetweenが日数に変換し、ageAtが年齢を、nextBirthdayGapが次の誕生日までの残り日数を求めます。この残り日数の比率だけ円形リングが塗られ、prefers-reduced-motionがオフのときはrequestAnimationFrameが塗りの進み方を毎フレーム補間します。

生年月日の値が残り日数を満たすD-dayリング
新しいタブで開く

03 · 生年月日の値が残り日数を満たすD-dayリング — 計測 累積面積 2.981% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ageAtnextBirthdayGapdaysBetween
  function daysBetween(a, b) {
    var msPerDay = 86400000;
    var da = Date.UTC(a.y, a.m - 1, a.d);
    var db = Date.UTC(b.y, b.m - 1, b.d);
    return Math.round((db - da) / msPerDay);
  }
  // 計算の一層 — 生年月日と基準日、2つの値の日数差から年齢と次の誕生日までの残り日数、その比率(リングの塗り)を求める
  function ageAt(birth, ref) {
    var age = ref.y - birth.y;
    var hadBirthdayThisYear = (ref.m > birth.m) || (ref.m === birth.m && ref.d >= birth.d);
    if (!hadBirthdayThisYear) age -= 1;
    return age;
  }
  function nextBirthdayGap(birth, ref) {
    var nextY = ref.y;
    var thisYearBday = { y: ref.y, m: birth.m, d: birth.d };
    var gapThisYear = daysBetween(ref, thisYearBday);
    if (gapThisYear < 0) { nextY = ref.y + 1; }
    var target = { y: nextY, m: birth.m, d: birth.d };
    return daysBetween(ref, target);
  }

  var SENT_PREFIX = '満', SENT_MID = '歳で、次の誕生日まであと', SENT_SUFFIX = '日です。';

  var targetT = 0, renderedT = 0;

04時刻の値が複数枚に分かれるタイムゾーンカード

1つの時刻の値に対してaddOffsetMinutesが各地域のオフセット(分)を足し引きし、カードごとの時刻を書き換えます。isDaytimeとbrightnessOfが計算した昼夜の判定がカード背景の明るさにも反映されるので、同じ時刻でも地域によってカードの見た目が変わります。

時刻の値が複数枚に分かれるタイムゾーンカード
新しいタブで開く

04 · 時刻の値が複数枚に分かれるタイムゾーンカード — 計測 累積面積 9.295% · 動いたフレーム 9/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

addOffsetMinutesbrightnessOfisDaytime
  function pad2(n) { return (n < 10 ? '0' : '') + n; }
  function formatOffset(off) {
    var sign = off >= 0 ? '+' : '-';
    var abs = Math.abs(off);
    var h = Math.floor(abs);
    var m = Math.round((abs - h) * 60);
    return 'UTC' + sign + h + (m ? ':' + pad2(m) : '');
  }
  // 計算の一層 — 基準時刻(分)にオフセット(分)を足して各カードの時刻を求め、昼・夜を判定して明るさを決める
  function addOffsetMinutes(baseMinutes, offsetHours) {
    var total = (baseMinutes + offsetHours * 60) % 1440;
    if (total < 0) total += 1440;
    return total;
  }
  function isDaytime(minutes) { return minutes >= 360 && minutes < 1080; }
  function brightnessOf(minutes) {
    var distFromNoon = Math.abs(minutes - 720) / 720;
    return clamp01(1 - distFromNoon * 0.7);
  }
  function clamp01(v) { return v < 0 ? 0 : v > 1 ? 1 : v; }

  var SENT_PREFIX = '基準', SENT_MID = '時刻のとき、最初のカードは', SENT_SUFFIX = 'です。';

  var targets = cards.map(function () { return 0; });
  var rendered = cards.map(function () { return 0; });

05hex値が合格ラインを行き来するコントラストバッジ

打ち込んだhexコードをhexToRgbがRGB値に直し、relativeLuminanceがsRGBのガンマ補正込みの相対輝度を、contrastRatioが白背景・黒背景それぞれとのコントラスト比を計算します。どちらかがPASS_THRESHOLDの4.5を超えるとバッジの色と文言が合格側に切り替わります。

hex値が合格ラインを行き来するコントラストバッジ
新しいタブで開く

05 · hex値が合格ラインを行き来するコントラストバッジ — 計測 累積面積 4.078% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

hexToRgbrelativeLuminancecontrastRatio
  var PASS_THRESHOLD = 4.5; // WCAG AA 本文文字の基準

  function hexToRgb(hex) {
    var m = /^#?([0-9a-f]{6})$/i.exec(hex || '');
    if (!m) return { r: 255, g: 91, b: 60 };
    var n = parseInt(m[1], 16);
    return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
  }
  // 計算の一層 — sRGBのガンマ補正相対輝度とWCAGコントラスト式
  function relativeLuminance(rgb) {
    function chan(c) {
      var v = c / 255;
      return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
    }
    return 0.2126 * chan(rgb.r) + 0.7152 * chan(rgb.g) + 0.0722 * chan(rgb.b);
  }
  function contrastRatio(l1, l2) {
    var hi = Math.max(l1, l2), lo = Math.min(l1, l2);
    return (hi + 0.05) / (lo + 0.05);
  }
  function clamp01(v) { return v < 0 ? 0 : v > 1 ? 1 : v; }

  var SENT_PREFIX = '白背景コントラスト', SENT_MID1 = ':1・黒背景コントラスト', SENT_MID2 = ':1のうち', SENT_SUFFIX = 'の方が良いです。';

  var targetX = 0, renderedX = 0;

06座標の値が2つの表記を行き来する同期インプット

度分秒の欄をdmsToDecimalが10進数に、10進数の欄をdecimalToDmsが度分秒に変換し、applyFromDecimalが変換結果をもう片方の欄へ書き戻します。どちらの欄を編集しても、もう一方が自動で追随するので、2つの入力欄のどちらが「元の値」かという区別がありません。

座標の値が2つの表記を行き来する同期インプット
新しいタブで開く

06 · 座標の値が2つの表記を行き来する同期インプット — 計測 累積面積 3.172% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

dmsToDecimaldecimalToDmsapplyFromDecimal
  // 計算の一層 — 度分秒と十進数の相互変換、目盛り上の位置(0〜1)
  function dmsToDecimal(deg, min, sec) {
    var sign = deg < 0 ? -1 : 1;
    return sign * (Math.abs(deg) + min / 60 + sec / 3600);
  }
  function decimalToDms(dec) {
    var sign = dec < 0 ? -1 : 1;
    var abs = Math.abs(dec);
    var deg = Math.floor(abs);
    var minFull = (abs - deg) * 60;
    var min = Math.floor(minFull);
    var sec = (minFull - min) * 60;
    return { deg: sign * deg, min: min, sec: sec };
  }
  function clamp01(v) { return v < 0 ? 0 : v > 1 ? 1 : v; }

  var SENT_PREFIX = '度分秒', SENT_MID = 'は十進数で', SENT_SUFFIX = '度です。';

  var targetX = 0, renderedX = 0;
  var LERP_RATE = 0.15;

  function paintCommon(dec) {
    targetX = clamp01((dec + RANGE) / (RANGE * 2));
    if (reduce) { renderedX = targetX; paint(); }
  }

07テキストの値が格子に散らばるQRキャンバス

打ち込んだ文字列をhashStrが決定的なハッシュ値に変換し、cellOnがそのハッシュ値から各マスを黒にするか白にするかを1つずつ判定して、キャンバスにQR風の格子を描きます。文字数が増えるほど格子のマス数もcomputeGridによって再計算され、GRID_MAXという上限まで増えていきます。

テキストの値が格子に散らばるQRキャンバス
新しいタブで開く

07 · テキストの値が格子に散らばるQRキャンバス — 計測 累積面積 4.272% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

hashStrcellOncomputeGrid
  function hashStr(s) {
    var h = 2166136261;
    for (var i = 0; i < s.length; i++) {
      h ^= s.charCodeAt(i);
      h = (h * 16777619) >>> 0;
    }
    return h >>> 0;
  }
  function cellOn(hash, i) {
    var x = (hash ^ Math.imul(i + 1, 2654435761)) >>> 0;
    x = (x ^ (x >>> 15)) >>> 0;
    return (x & 1) === 1;
  }
  var GRID_MAX = 12; // 変形07: 上げると文字数が多いとき格子のマス数(=キャンバスを埋めるマスの大きさ)が増える

08人数の値が取り分を割るチップ分担リング

総額と人数、2つの値をperPersonShareが1人あたりの分担額に、shareFractionが全体に占める割合に変換します。formatNumberが3桁ごとの区切りを入れた金額を組み立て、その割合の分だけ円形リングの一片が塗られます。

人数の値が取り分を割るチップ分担リング
新しいタブで開く

08 · 人数の値が取り分を割るチップ分担リング — 計測 累積面積 3.385% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

perPersonShareshareFractionformatNumber
  function shareFraction(n) { return n > 0 ? 1 / n : 0; }
  function formatNumber(v) {
    var s = Math.round(v).toString();
    var out = '';
    for (var i = 0; i < s.length; i++) {
      var pos = s.length - i;
      out = s[i] + out;
      if (pos > 1 && (pos - 1) % 3 === 0) out += ',';
    }
    return out;
  }

  var SENT_PREFIX = '総額', SENT_MID1 = '円を', SENT_MID2 = '人で分けると1人あたり', SENT_MID3 = '円で、全体の', SENT_SUFFIX = '%です。';

  var targetF = 0, renderedF = 0;
  var LERP_RATE = 0.15;

  function applyValue(total, n) {
    var share = perPersonShare(total, n);
    var frac = shareFraction(n);
    targetF = frac;
    shareEl.textContent = '1/' + n;
    readout.textContent = SENT_PREFIX + formatNumber(total) + SENT_MID1 + n + SENT_MID2 + formatNumber(share) + SENT_MID3 + (frac * 100).toFixed(1) + SENT_SUFFIX;
    if (reduce) { renderedF = targetF; paint(); }
  }

09長さの値が2つの目盛りを同時に押す単位スケール

センチメートルの欄に打ち込んだ値をcmToInchがインチに換算し、applyFromCmがインチ欄の値と2本の目盛り上の位置を同時に書き換えます。CM_MAXとIN_MAXという2つの上限に対する比率がそれぞれの目盛りの位置を決めるので、2つの目盛りは同じ軸の上で並んで動きます。

長さの値が2つの目盛りを同時に押す単位スケール
新しいタブで開く

09 · 長さの値が2つの目盛りを同時に押す単位スケール — 計測 累積面積 5.763% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

cmToInchinchToCmapplyFromCm
  var CM_MAX = 60, IN_MAX = 24;
  // 計算の一層 — cmとinchの相互変換、各目盛り上の位置(0〜1)
  function cmToInch(cm) { return cm / 2.54; }
  function inchToCm(inch) { return inch * 2.54; }
  function clamp01(v) { return v < 0 ? 0 : v > 1 ? 1 : v; }

  var SENT_PREFIX = 'センチメートル', SENT_MID = 'はインチ', SENT_SUFFIX = 'です。';

  var targetCm = 0, renderedCm = 0, targetIn = 0, renderedIn = 0;
  var LERP_RATE = 0.15;

  function readSentence(cm, inch) {
    readout.textContent = SENT_PREFIX + cm.toFixed(1) + SENT_MID + inch.toFixed(2) + SENT_SUFFIX;
  }

  function applyFromCm() {
    var cm = parseFloat(cmInput.value); if (isNaN(cm) || cm < 0) cm = 0;
    var inch = cmToInch(cm);
    inInput.value = inch.toFixed(2);
    targetCm = clamp01(cm / CM_MAX);
    targetIn = clamp01(inch / IN_MAX);
    readSentence(cm, inch);
    if (reduce) { renderedCm = targetCm; renderedIn = targetIn; paint(); }
  }

この9種のような入力フォーム デザイン サンプルはどこで見られますか

ネットで見つかる入力フォーム デザイン サンプルの多くは、静止画のモックアップか、計算のないカラーピッカーのどちらかです。上の9パターンは違う場所に立っていて、input・changeイベントのたびに名前付き関数が打った値を読み、requestAnimationFrameのループが表示値をその結果へ補間します——グラフ描画ライブラリも日付計算パッケージもCDNのスクリプトも使わず、下のzipとブラウザだけで完結します。

実際にどんな場面でこの電卓型インプットが役立つのか

ファイルアップロードのフォームは生のバイト数をそのまま見せることは滅多にありませんが、パターン02の単位を自動で選ぶバッジは、アップロードが完了した後の表示にほぼそのまま流用できます。デモの入力欄を実際のアップロード処理が持っている数値に差し替えるだけで足ります。

分散したチームの打ち合わせ調整では、パターン04がより直接的に役立ちます。1つの候補時刻を打ち込むだけで4枚のオフセット付きカードが返ってくるので、4つの時計を別々に開くより速く確認できます——ただし本番では固定の4地域ではなく、実際の参加者ごとのオフセットに差し替える必要があります。パターン08の割り勘リングも似た場所に収まり、総額と人数を打てばリングの一片が即座に分担額を伝えるので、誰かが暗算で割り算をする手間がなくなります。

パターン02の桁数境界で壊れる場面

パターン02に999と打つと、バッジはおよそ97.6%まで満ちます。digitsOfがまだ3桁だと数え、pickUnitがB単位のままにとどめるからです。そこから1000と打つと——バイト数はたった1増えただけなのに——digitsOfは4桁だと数え直し、pickUnitは単位をKBへ切り替え、fillRatioは同じ値を1024で割った新しい単位の中で比率を計算し直すので、バーは1%にも満たない高さまで一気に落ち込みます。コードのどこにも誤りはなく、このバーはもともと「今の単位をどれだけ満たしているか」だけを描くよう作られています。単位が切り替わる瞬間に「満ちる」の基準そのものが変わってしまい、その変化を和らげる遷移をこのバーが持っていないだけです。

9つすべてが読み込み時に一度だけprefers-reduced-motionを確認します。オンになっていれば、すべてのapplyValue・paintCommonはrendered値を目標値へ直接代入してフレームループを飛ばすので、reduced-motionを設定した人は補間の途中経過を見ることなく、完成したリングやバー、格子をすぐに見ることになります。9つの小さな関数と9つのリスナーはファイルの中で最初から最後まで読み切れる長さなので、この挙動を確かめる一番早い方法はコードそのものを読むことです——下のzipはパスワードeaaz4gefで開きます。

LERP_RATEとGRID_MAXを動かすと何が変わるか

9パターンのうち8つは同じ骨格を共有しています。欄がinputかchangeを発火した瞬間にtarget系の値が書き換わり、フレームループがrendered系の値をLERP_RATEの割合で毎ティック目標へ近づけます。この割合は今回の実測ではパターン03のリングの0.13からパターン05のコントラストバッジの0.17までの範囲に収まっていて、リングが8つの中で最も追いつくのが遅く、コントラストバッジが最も速く追いつきます。パターン07だけはこの骨格に当てはまらず、computeGridが打ち込まれた文字列全体を一度に読んで格子のマスをまとめて描き切るので、文字列が長くなっても遅れて追いつくのではなく、単にマス数が増えるだけです。

01 — LERP_RATE

0.14から0.08へ下げると、摂氏の値を素早く書き換えたときにグラデーションのハンドルがより長く遅れて追いかけるようになり、キーを打った直後もまだ値を追いかけている様子がはっきり見えるようになります。

03 — LERP_RATE

0.13から0.07へ下げると、リングの追いつきも同じように遅くなります。もともと8つの補間系パターンの中で最も遅かった基準値をさらに半分近くまで下げるので、落ち着くまでの時間がいっそう伸びます。

07 — GRID_MAX

12から18へ上げると、computeGridは文字列が長くなったときに12マスではなく最大18マスまで描けるようになるので、長い文字列ほど格子が目に見えて密になり、短い文字列と同じ12マス幅の上限で頭打ちになることがなくなります。

変形 変わる値 体感
01 LERP_RATE 0.14 → 0.08 温度計の補間速度がおよそ半分に 打ち込んだ温度にハンドルがすぐ追いつかず、目に見えて遅れて到着する
03 LERP_RATE 0.13 → 0.07 リングの補間速度がおよそ半分に 新しく打った生年月日や基準日にリングの塗りが長く遅れてから落ち着く
07 GRID_MAX 12 → 18 格子の上限マス数が12から18に 長い文字列は18マス幅の密な格子になり、短い文字列は見た目がほぼ変わらない

3つのバリエーションとreduced-motionが取り除くもの

上の表の3つが今回確認済みの変形のすべてで、01と03のLERP_RATE、07のGRID_MAXのそれぞれ1つの定数だけを書き換えたものです。リスナーの構成もprefers-reduced-motionの分岐もベースパターンとまったく同じままなので、上で説明したアクセシビリティの挙動は変形ごとに確かめ直す必要がありません。変形のindex.htmlは、ベースのファイルからその1つの数値だけを変えたものです。

FAQ

パターン07の格子は実際にスキャンできるQRコードですか

いいえ——hashStrとcellOnは打ち込んだテキストの決定的なハッシュ値から白黒の格子模様を作っているだけで、実際のQR符号化ではないので、どんなスキャナーで読み取っても意味のある結果は得られません。

パターン04のカードが都市名ではなくUTCオフセットで表示されるのはなぜですか

デモが計算しているのはaddOffsetMinutesによる分単位のオフセットだけで、そのオフセットにどの都市が対応するかを調べる処理を持っていないからです。実在する都市名をカードに付けると、コードが実際に知っている以上のことを主張してしまいます。

この9つを動かすのに数学ライブラリや日付ライブラリは必要ですか

いいえ——入力フォーム デザイン cssのcodepenのような場でよく見かける例では日付計算パッケージや多倍長数値ライブラリに頼ることがありますが、ここでの計算はどれもdaysBetweenやcontrastRatioのような小さな名前付き関数で、その関数を呼び出す入力リスナーと同じファイルの中に収まっています。

Sources: MDN — CanvasRenderingContext2D · MDN — Intl.DateTimeFormat · MDN — Date · MDN — requestAnimationFrame · MDN — prefers-reduced-motion

Related reading: 数値ステッパー UI 9選 — 長押しから目盛りスナップまで · カラーピッカー UI 9選 — ライブラリなしで作る色選択 · css グラフ をSVGで描く9種 — ライブラリなし — 1つ目は電卓ではなくステッパー操作で同じ「入力1つに関数1つ」という骨格を共有し、2つ目もライブラリなしでライブの値から表示を更新する点が同じで、3つ目は打ち込んだデータや生成したデータから9つのグラフを描く点でこの記事と同じライブラリなし・CDNなしの原則に従っています。

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

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