GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

계산기 디자인 9종 — 인풋 박스 디자인이 값을 그리는 방식

css 계산기 디자인 아홉 가지 가운데 하나는 입력한 텍스트 값을 격자 값으로 바꿔 캔버스에 QR 모양을 직접 그리고, 하나는 생년월일 값으로 남은 날수를 계산해 링 게이지를 채우며, 하나는 입력한 시각 값 하나로 여러 시간대 카드를 다시 그립니다.

아홉 개는 전부 숫자·텍스트 칸에 값을 넣으면 그 값을 계산해 옆이나 아래 결과 디스플레이로 보여주는 인풋이라서, html 계산기 디자인 카테고리에서 흔히 찾는 "입력→계산→표시" 뼈대를 각각 다른 계산 한 겹으로 채웠습니다. 화면이 움직이는 부분은 모두 requestAnimationFrame 으로 매 프레임 목표 값을 향해 조금씩 따라붙는 관성 값(렌더 값)이고, 듣는 이벤트는 대부분 input·change 두 가지뿐입니다(01·05·06·09 는 패널 크기 변경에 맞춰 다시 그리도록 resize 도 함께 듣습니다). fetch·localStorage·clipboard·Math.random 은 한 번도 부르지 않고, 07 항목의 QR풍 격자도 실제 QR 인코딩이 아니라 텍스트를 결정적 해시로 바꿔 칸을 켜고 끄는 방식이라 스캐너로는 읽히지 않습니다.

데모

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

아홉 가지 계산기 입력 UI — 계산 한 겹이 어디서 도는가

아홉 개를 나열한 순서는 계산이 다루는 대상을 기준으로 잡았습니다. 01·02·09 는 단위를 서로 바꾸는 변환 계산이고, 03·04 는 날짜·시각처럼 시간을 다루는 계산이며, 05·06 은 좌표·색처럼 값 하나를 두 표기법으로 오가는 계산이고, 07·08 은 텍스트·인원처럼 입력을 나누거나 흩는 계산입니다. 09 항목의 단위 스케일은 실무 계산기 UI에서 자주 요구하는 인풋 박스 디자인 감각을 눈금 두 개가 나란히 움직이는 형태로 바꾼 사례입니다.

다음 값들은 각 항목의 렌더 캡처 창을 24프레임 찍어 실측한 결과입니다. 누적면적은 그 프레임들에서 픽셀이 이전과 달라진 비율을 전부 더한 값이고, 움직인프레임은 23개 프레임 간 차이 가운데 실제로 값이 바뀐 개수입니다.

항목 누적면적 강도평균 움직인프레임
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

04 시간대 카드만 움직인프레임이 9/23 으로 낮은 것은 시연 값이 2초 주기로 몇 번만 바뀌고 그 사이엔 카드 밝기가 이미 목표 값을 따라잡아 멈춰 있기 때문이고, 나머지 여덟은 렌더 창 내내 값이 계속 갱신되는 쪽입니다.

패턴별 코드

아래 아홉 개는 실제 데모 소스에서 계산을 맡은 함수를 그대로 옮긴 조각이고, 항목마다 손볼 수 있는 상수는 그 줄 옆 주석에 남겨 뒀습니다.

01온도 값이 채워가는 그라디언트 온도계 인풋

입력한 섭씨 값을 화씨로 함께 계산해 보여주면서, 그 값의 위치를 그라디언트 눈금 위 손잡이로 옮겨 온도계처럼 보이게 하는 인풋입니다. 숫자 칸 아래 결과 문장이 함께 뜨고, 손잡이는 매 프레임 목표 위치를 향해 조금씩 따라붙는 관성 값으로 움직입니다.

  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)';
  }
온도 값이 채워가는 그라디언트 온도계 인풋
새 창에서 열기

01 · 온도 값이 채워가는 그라디언트 온도계 인풋 — 측정 누적면적 2.117% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

LERP_RATEcelsiusToFahrenheitrenderedT

02바이트 값이 스스로 단위를 고르는 파일 크기 배지

입력한 바이트 값의 자릿수를 계산해 KB·MB·GB 가운데 알맞은 단위를 스스로 고르고, 그 값에 비례한 길이만큼 배지 안 막대를 채우는 인풋입니다. 숫자 칸에 값을 넣으면 배지 위 단위 글자와 막대 길이가 함께 바뀌며, 막대는 목표 비율을 향해 관성 값으로 따라붙습니다.

  function clamp01(v) { return v < 0 ? 0 : v > 1 ? 1 : v; }

  // 계산 한 겹 — 자릿수로 단위를 고르고(digitsOf/pickUnit), 그 단위 안에서 채움 비율(fillRatio)을 구한다
  function digitsOf(n) {
    n = Math.floor(Math.abs(n));
    return n === 0 ? 1 : String(n).length;
  }
  function pickUnit(bytes) {
    var d = digitsOf(bytes);
    if (d <= 3) return { name: 'B', pow: 0 };
    if (d <= 6) return { name: 'KB', pow: 1 };
    if (d <= 9) return { name: 'MB', pow: 2 };
    return { name: 'GB', pow: 3 };
  }
  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: 낮추면 막대가 값 변화를 더 천천히 뒤따라간다

바이트 값이 스스로 단위를 고르는 파일 크기 배지
새 창에서 열기

02 · 바이트 값이 스스로 단위를 고르는 파일 크기 배지 — 측정 누적면적 13.481% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

pickUnitdigitsOffillRatio

03생년월일 값이 남은 날을 채우는 디데이 링

생년월일 칸과 나란히 둔 기준 날짜 칸의 차이를 일수로 계산해 나이와 다음 생일까지 남은 날을 함께 구하고, 그 날수 비율만큼 원형 링을 채우는 인풋입니다. 오늘 날짜를 자동으로 읽지 않고 기준 날짜 칸에 값을 직접 넣게 해 두어서, 그 칸의 값을 바꾸면 계산 기준 자체가 달라지는 것을 확인할 수 있습니다.

  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);
  }
  // 계산 한 겹 — 생년월일·기준일 두 값의 날짜 차이로 나이와 다음 생일까지 남은 날, 그 비율(링 채움)을 구한다
  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);
  }
생년월일 값이 남은 날을 채우는 디데이 링
새 창에서 열기

03 · 생년월일 값이 남은 날을 채우는 디데이 링 — 측정 누적면적 2.981% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ageAtnextBirthdayGapdaysBetween

04시각 값이 여러 장으로 갈라지는 시간대 카드

입력한 시각 값 하나에 여러 지역의 시차를 더하거나 빼 각 카드의 시각을 다시 계산하고, 낮과 밤 여부에 따라 카드 배경 밝기도 함께 바뀌는 인풋입니다. 카드에는 도시나 국가 이름 대신 UTC 오프셋 라벨만 붙어 있고, 기준 시각도 지금 시각이 아니라 입력 칸의 값을 그대로 씁니다.

  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; });
  var LERP_RATE = 0.15; // 변형 04: 낮추면 카드 밝기가 값 변화를 더 천천히 뒤따라간다
시각 값이 여러 장으로 갈라지는 시간대 카드
새 창에서 열기

04 · 시각 값이 여러 장으로 갈라지는 시간대 카드 — 측정 누적면적 9.295% · 움직인 프레임 9/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

addOffsetMinutesbrightnessOfisDaytime

05hex 값이 통과선을 넘나드는 대비 배지

입력한 hex 값의 밝기를 계산해 흰 배경·검은 배경 각각과의 대비 비율을 구하고, 통과 기준을 넘는지에 따라 배지 색과 문구가 바뀌는 인풋입니다. 대비 비율은 상대 밝기 공식으로 직접 계산한 값이라, 색상 칸에 새 hex 값을 넣을 때마다 두 배지가 같이 다시 계산됩니다.


  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;
  var LERP_RATE = 0.17; // 변형 05: 낮추면 표시가 값 변화를 더 천천히 뒤따라간다
hex 값이 통과선을 넘나드는 대비 배지
새 창에서 열기

05 · hex 값이 통과선을 넘나드는 대비 배지 — 측정 누적면적 4.078% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

hexToRgbrelativeLuminancecontrastRatio

06좌표 값이 두 표기법을 오가는 동기화 인풋

도·분·초로 입력한 값을 십진수 좌표로 계산해 옆 칸에 함께 표시하고, 어느 쪽을 고쳐도 나머지 칸이 그 값에 맞춰 다시 계산되는 인풋입니다. 도분초 세 칸과 십진수 한 칸 중 어느 쪽을 채워도 되고, 값을 넣은 쪽을 기준으로 반대쪽이 그 값에서 역산됩니다.

  function applyFromDms() {
    var deg = parseFloat(degInput.value); if (isNaN(deg)) deg = 0;
    var min = parseFloat(minInput.value); if (isNaN(min)) min = 0;
    var sec = parseFloat(secInput.value); if (isNaN(sec)) sec = 0;
    var dec = dmsToDecimal(deg, min, sec);
    decInput.value = dec.toFixed(4);
    readSentence(deg, min, sec, dec);
    paintCommon(dec);
  }

  function applyFromDecimal() {
    var dec = parseFloat(decInput.value); if (isNaN(dec)) dec = 0;
    var dms = decimalToDms(dec);
    degInput.value = dms.deg;
    minInput.value = dms.min;
    secInput.value = dms.sec.toFixed(1);
    readSentence(dms.deg, dms.min, dms.sec, dec);
    paintCommon(dec);
  }
좌표 값이 두 표기법을 오가는 동기화 인풋
새 창에서 열기

06 · 좌표 값이 두 표기법을 오가는 동기화 인풋 — 측정 누적면적 3.172% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

dmsToDecimaldecimalToDmsapplyFromDecimal

07텍스트 값이 격자로 흩뿌려지는 QR 캔버스

입력한 텍스트 값을 흑백 격자 값으로 바꿔 캔버스에 점을 하나씩 찍어 QR 모양을 만들고, 글자 수가 늘어나면 격자 칸 수도 다시 계산해 늘어나는 인풋입니다. 실제 QR 인코딩 규격을 따르는 것이 아니라 텍스트 값을 hashStr 로 결정적 해시로 바꿔 칸을 켜고 끄는 QR풍 격자라서, 스캐너 앱으로는 읽히지 않는 모양만 만듭니다.

  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: 키우면 글자 수가 많을 때 격자 칸 수(=캔버스를 채우는 칸 크기)가 더 커진다
  function computeGrid(text) {
    var hash = hashStr(text);
    var gridSize = Math.min(GRID_MAX, Math.max(4, 4 + Math.floor(text.length / 3)));
    var total = gridSize * gridSize;
    var finderSize = Math.min(3, gridSize - 1);
    var cells = new Array(total);
    for (var r = 0; r < gridSize; r++) {
      for (var c = 0; c < gridSize; c++) {
        var i = r * gridSize + c;
        var inFinder = (r < finderSize && c < finderSize) ||
          (r < finderSize && c >= gridSize - finderSize) ||
          (r >= gridSize - finderSize && c < finderSize);
        cells[i] = inFinder ? true : cellOn(hash, i);
      }
    }
    return { gridSize: gridSize, cells: cells, total: total };
  }
텍스트 값이 격자로 흩뿌려지는 QR 캔버스
새 창에서 열기

07 · 텍스트 값이 격자로 흩뿌려지는 QR 캔버스 — 측정 누적면적 4.272% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

hashStrcellOncomputeGrid

08인원 값이 몫을 나누는 팁 분담 링

입력한 총액과 인원 값을 나눠 1인당 분담액을 계산하고, 그 금액이 전체에서 차지하는 비율만큼 원형 링 조각이 채워지는 인풋입니다. 총액 칸과 인원 칸 둘 다 값이 바뀔 때마다 분담액 문장과 링 조각이 같은 계산으로 다시 그려집니다.

  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(); }
  }
인원 값이 몫을 나누는 팁 분담 링
새 창에서 열기

08 · 인원 값이 몫을 나누는 팁 분담 링 — 측정 누적면적 3.385% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

perPersonShareshareFractionformatNumber

09길이 값이 두 눈금을 함께 미는 단위 스케일

한쪽 눈금에 입력한 길이 값을 계산해 다른 단위 눈금 위 위치로 옮기고, 두 눈금이 같은 축 위에서 나란히 움직이며 비교되는 인풋입니다. 센티미터 칸과 인치 칸 어느 쪽에 값을 넣어도 반대쪽 숫자와 두 눈금 위치가 함께 다시 계산됩니다.

  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(); }
  }
길이 값이 두 눈금을 함께 미는 단위 스케일
새 창에서 열기

09 · 길이 값이 두 눈금을 함께 미는 단위 스케일 — 측정 누적면적 5.763% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

cmToInchinchToCmapplyFromCm

값이 화면을 바꾸는 곳 — 단위·비율·좌표

계산이 다루는 값은 다르지만 상수 하나로 화면 인상이 바뀌는 자리는 아홉 개가 같습니다. 01·02·03·04·05·06·08·09 의 LERP_RATE 는 표시 값이 목표 값을 얼마나 빨리 뒤따라가는지 정하고, 07 의 GRID_MAX 는 텍스트가 길어질 때 격자가 최대 몇 칸까지 잘게 나뉠지 정합니다. 실무에서 입력 폼 디자인을 손볼 때도 이런 상수 위치부터 눈여겨보면, 어떤 결과 디스플레이가 어떤 계산에서 나오는지 빠르게 알 수 있습니다.

이웃 가지

어디서 깨지는가 — 접근성·reduced-motion·저사양

아홉 개가 공통으로 겪는 함정은 입력값 검증입니다. 숫자 칸이 비었거나 문자가 섞여 parseFloat·parseInt 가 NaN 을 돌려주면 아홉 개 모두 에러를 보여주지 않고 조용히 0(08 인원 칸만 1)으로 되돌린 뒤 계산을 계속합니다. 사용자 눈에는 그저 결과가 0으로 보일 뿐이라, 값을 잘못 넣었다는 사실 자체를 놓치기 쉽습니다. 다른 폼에 이 계산을 옮겨 쓸 때는 이 되돌림 지점에 안내 문구를 더하는 편이 안전합니다. 이 아홉 개 인풋을 묶은 파일의 압축을 풀려면 eaaz4gef 을 넣어야 하고, 그 값은 바로 아래 카드에서 확인할 수 있습니다.

동작 줄이기(prefers-reduced-motion) 환경에서는 아홉 개 모두 같은 방식으로 반응합니다. 값이 바뀔 때마다 목표 값을 향해 매 프레임 조금씩 좇아가는 시연용 루프(driveTick·frame)를 아예 시작하지 않고, applyValue(또는 그에 대응하는 함수)가 렌더 값을 목표 값에 곧바로 맞춰 한 번만 그립니다. 계산 결과 자체는 그대로 나오고 사라지는 것은 뒤따라가는 시각 효과뿐입니다.

키보드·스크린리더

각 인풋 아래 결과 문장에는 role="status" 와 aria-live="polite" 가 붙어 있어 값이 바뀔 때마다 스크린리더가 새 문장을 자동으로 읽어 주고, 숫자·텍스트 칸에도 지금 무엇을 계산하는지 설명하는 aria-label 이 달려 있습니다. 페이지를 열면 잠시 정해진 값들이 자동으로 도는 시연 상태(is-demo)로 시작하지만, 탭으로 칸에 초점을 옮기고 값을 직접 입력하는 순간 그 시연은 멈추고 사용자가 넣은 값만 계산에 쓰입니다.

변형 세 가지와 접근성

이름 값 느낌
온도계 눈금 채움 보간 값 조정 01 LERP_RATE 0.14 → 0.08 온도 값이 바뀔 때 눈금 손잡이가 더 천천히 뒤따라가는 쪽에 가까워집니다
디데이 링 보간 값 조정 03 LERP_RATE 0.13 → 0.07 생년월일을 바꾼 뒤 링이 새 값을 더 천천히 뒤따라 채워지는 쪽에 가까워집니다
QR 풍 격자 칸 수 상한 값 조정 07 GRID_MAX 12 → 18 글자 수가 많을 때 격자가 더 많은 칸으로 잘게 나뉘는 쪽에 가까워집니다

세 변형 모두 입력값 검증·aria-live·reduced-motion 분기는 건드리지 않습니다. 상수 하나만 옮긴 사본을 만들어도 잘못된 입력을 0으로 되돌리는 자리나 스크린리더 안내는 그대로 남아, 값만 다르게 움직이는 인풋을 안전하게 시험해 볼 수 있습니다.

입력 폼 디자인 예제는 어디서 보나?

캔버스 픽셀을 직접 그리는 두 항목(07)은 MDN 의 CanvasRenderingContext2D 문서에, 화면을 매 프레임 다시 그리는 흐름은 requestAnimationFrame 문서에, 동작을 줄이는 조건은 prefers-reduced-motion 문서에 정리되어 있습니다. 위 아홉 개는 이 API들과 순수 자바스크립트 계산만으로 만든 결과라서, html 계산기 디자인 형태를 직접 검색해 표준 동작을 확인하면서 압축을 풀어 코드를 그대로 옮겨 써도 됩니다.

FAQ

인풋 박스 디자인 예제를 코드 그대로 옮겨 써도 되나요?

네, 라이브러리나 CDN 없이 순수 자바스크립트 계산과 CSS 만으로 짜여 있어서 파일만 그대로 옮기면 됩니다. 위쪽 카드에 적힌 비밀번호를 입력하면 아홉 개 항목의 원본이 통째로 풀립니다.

07 QR 캔버스는 실제 QR 코드를 만드나요?

아닙니다. 텍스트 값을 결정적 해시로 바꿔 칸을 켜고 끄는 QR풍 격자를 그릴 뿐 QR 인코딩 규격을 따르지 않아서, 화면에 보이는 모양은 스캐너 앱으로 읽히지 않습니다. 같은 텍스트를 다시 넣으면 항상 같은 격자가 나오는 것만 보장됩니다.

느린 기기나 동작 줄이기 설정에서는 어떻게 보이나요?

prefers-reduced-motion 이 켜져 있으면 시연용 자동 루프와 값이 뒤따라가는 관성 움직임이 꺼지고, 계산 결과가 목표 값으로 한 번에 맞춰져 그려집니다. 값을 입력하고 계산 결과를 확인하는 기능 자체는 그대로 남고, 부드럽게 뒤따라가는 시각 효과만 빠집니다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.