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

pdf 表示 js 実装 9パターン — スクロールでページを計算するビューア

「pdf 表示 js」の作り方は9通りあります。スクロール値がページ番号を作るのか、ページ番号がスクロール値を作るのか — 02番の拡大中心はポインター座標で、04番の次ページは指を離す速度で、08番のダウンロードリングは受信バイト数で答える。

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

pdf 表示 js 実装 9パターン — スクロールとズームを計算でつなぐビューア

以下の9つはどれも同じ問いに別の答え方をしています。インターフェースの残りを動かす値をどう計算するか、という問いです。01番はスクロール位置をそのまま重なり比率に変換し、04番になるとポインターや指の動きを速度のしきい値に変換し、06番はボタンの押下を連続的に補間される角度に変換し、09番はそれ以前の計算のうち三つを一つのツールバーにまとめています。順番は、スクロールイベントのたびに一度だけ読む値から、来訪者が他のことをしている間も毎フレーム計算し続ける値へと移っていきます。

01本文スクロール比率から現在ページを計算するサムネイルレール同期

本文スクロール比率から現在ページを計算するサムネイルレール同期
新しいタブで開く

01 · 本文スクロール比率から現在ページを計算するサムネイルレール同期 — 計測 累積面積 59.608% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

RAIL_LERPcomputeCurrentvisibleRatio
  var current = 0;
  function visibleRatio(el, viewTop, viewH) {
    var top = el.offsetTop, h = el.offsetHeight;
    var lo = Math.max(top, viewTop), hi = Math.min(top + h, viewTop + viewH);
    return Math.max(0, hi - lo) / h;
  }
  function computeCurrent() {
    var viewTop = doc.scrollTop, viewH = doc.clientHeight, best = 0, bestR = -1;
    for (var i = 0; i < docEls.length; i += 1) {
      var r = visibleRatio(docEls[i], viewTop, viewH);
      if (r > bestR) { bestR = r; best = i; }
    }
    if (best !== current) {
      current = best;
      for (var j = 0; j < thumbEls.length; j += 1) thumbEls[j].classList.toggle('pvt__thumb--active', j === current);
      badge.textContent = (current + 1) + ' / ' + pages.length;
    }
  }

computeCurrent() はスクロールイベントのたびに実行され、6ページ分の偽データすべてに visibleRatio() を呼び出してビューポートに最も重なっているページを探し、IntersectionObserver のしきい値コールバックを待たずにその場で計算しています。最も重なるページが変わるとサムネイルに active クラスが付き、別のループが rail の位置をそのサムネイルの中央へ lerp で少しずつ近づけていて、スクロール位置へ直接飛ばすことはしません。01番は23/23フレームで動き、平均強度39.5、累積面積59.6%を記録しています。

02ポインター座標を基準に拡大中心を計算するフォーカルズーム

ポインター座標を基準に拡大中心を計算するフォーカルズーム
新しいタブで開く

02 · ポインター座標を基準に拡大中心を計算するフォーカルズーム — 計測 累積面積 57.451% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ZOOM_FOCAL_STEP_RATIOapplyWheeleaseInOut
  function render(scale, ox, oy) {
    page.style.transformOrigin = (ox * 100) + '% ' + (oy * 100) + '%';
    page.style.transform = 'scale(' + scale.toFixed(3) + ')';
    var vr = view.getBoundingClientRect();
    dot.style.left = (ox * vr.width) + 'px';
    dot.style.top = (oy * vr.height) + 'px';
    dot.style.transform = 'translate(-50%,-50%) scale(' + (1 / scale).toFixed(3) + ')';
    label.textContent = Math.round(scale * 100) + '%';
  }

  function scheduled(elapsed) {
    var phase = elapsed % LOOP_MS;
    var i = 0;
    while (i < KF.length - 1 && phase >= KF[i + 1].t) i += 1;
    var a = KF[i], b = KF[Math.min(i + 1, KF.length - 1)];
    var span = Math.max(1, b.t - a.t);
    var t = easeInOut(Math.max(0, Math.min(1, (phase - a.t) / span)));
    var s = a.s + (b.s - a.s) * t;
    render(s, b.x, b.y);
  }

  var manual = { s: 1, x: .5, y: .5 };
  function applyWheel(dir, ox, oy) {
    manual.s = Math.max(1, Math.min(L2 * 1.4, manual.s * Math.pow(ZOOM_FOCAL_STEP_RATIO, dir * 0.6)));

applyWheel() はホイールやピンチが起きた地点をページ自身の幅と高さに対する (ox, oy) の比率に変換し、render() がその比率をそのまま transform-origin に設定してから拡大するため、拡大後の表示は固定された中心ではなくその地点から外側へ広がります。拡大の刻み幅は ZOOM_FOCAL_STEP_RATIO をホイール方向でべき乗した値から決まり、比率を小さくすればほとんど気づかない刻みに、大きくすれば一気に跳ねる刻みになります。02番は23/23フレームで動き、平均強度93.4、累積面積57.5%を記録しています。

03一致した検索語の位置を計算してジャンプする検索ハイライト

一致した検索語の位置を計算してジャンプする検索ハイライト
新しいタブで開く

03 · 一致した検索語の位置を計算してジャンプする検索ハイライト — 計測 累積面積 40.346% · 動いたフレーム 22/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

buildPagespageOfjumpTo
  var words = Array.prototype.slice.call(doc.querySelectorAll('.pvs__w'));
  function pageOf(el) {
    var i = 1, node = el;
    while (node && !node.classList.contains('doc__page')) node = node.parentElement;
    var all = Array.prototype.slice.call(doc.children);
    return all.indexOf(node) + 1;
  }

  function find(q) {
    q = q.toLowerCase();
    if (!q) return [];
    return words.filter(function (w) { return w.textContent.toLowerCase().indexOf(q) === 0; });
  }

  var scrollAnim = null;
  function startScroll(to, dur, nowMs) {
    scrollAnim = { from: doc.scrollTop, to: to, start: nowMs, dur: dur };
  }
  function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); }

  var lastHit = null;
  function jumpTo(el, nowMs) {
    var target = Math.max(0, el.offsetTop - doc.clientHeight * 0.35);

find() は入力した検索語で始まる単語だけを拾います。indexOf(q) === 0 という前方一致で、単語の途中に含まれているだけでは一致とみなさず、実物のファイルバイトを全文検索しているわけではなく植え込まれた単語リストの中だけを探しています。候補を選ぶと jumpTo() がビューポート高さの35%だけ上にずらした位置へスクロールし、その移動は easeOutCubic() で複数フレームにわたってイージングされ、一度で飛ぶことはありません。03番は22/23フレームで動き、平均強度67.8、累積面積40.3%を記録しています。

04指を離す速度で次ページへの遷移を計算するスワイプフリック

指を離す速度で次ページへの遷移を計算するスワイプフリップ
新しいタブで開く

04 · 指を離す速度で次ページへの遷移を計算するスワイプフリップ — 計測 累積面積 42.158% · 動いたフレーム 10/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SWIPE_FLIP_VELOCITY_PX_SvelocityNowspringEase
  function velocityNow(nowT) {
    if (samples.length < 2) return 0;
    var windowStart = nowT - 120, first = samples[0];
    for (var i = 0; i < samples.length; i += 1) { if (samples[i].t >= windowStart) { first = samples[i]; break; } }
    var last = samples[samples.length - 1];
    var dt = (last.t - first.t) / 1000;
    if (dt <= 0) return 0;
    return (last.x - first.x) / dt;
  }

  function startDrag(x, t) { dragging = true; flying = false; springAnim = null; samples = []; dragStartX = x; cardStartX = curX; pushSample(x, t); }
  function moveDrag(x, t) {
    if (!dragging) return;
    var dx = x - dragStartX;
    curX = cardStartX + dx;
    curRot = dx / 22;
    pushSample(x, t);
    render();
  }
  function release(t) {
    if (!dragging) return;
    dragging = false;
    var v = velocityNow(t);

velocityNow() は指を離す直前120ミリ秒以内のサンプルだけを見て、それより前の記録は切り捨てるため、ゆっくり動かしてから最後だけ弾くような操作でも離す瞬間の速さとして読み取られます。その速度値を release() が SWIPE_FLIP_VELOCITY_PX_S と比較し、次ページへめくり切るか中央へばね戻りするかを決めています。04番は自動デモがスワイプとスワイプの間で静止するため10/23 フレームでしか動かず、平均強度27.2、累積面積42.2%を記録しています。

05受信バイト数から進行率を計算するプログレッシブロードバー

受信バイト数から進行率を計算するプログレッシブロードバー
新しいタブで開く

05 · 受信バイト数から進行率を計算するプログレッシブロードバー — 計測 累積面積 2.847% · 動いたフレーム 22/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

speedCurveTOTAL_BYTESHOLD_MS
  var TOTAL_BYTES = 480000;
  var TOTAL_PAGES = 12;
  var HOLD_MS = 260;

  function speedCurve(tSec) {
    // 決定的な疑似ジッター: 三つのサイン波を足し合わせて「速くなったり遅くなったり」するネットワーク感を出す
    var base = 210000;
    var v = base
      + 90000 * Math.sin(tSec * 2.3)
      + 40000 * Math.sin(tSec * 5.1 + 1.4)
      + 20000 * Math.sin(tSec * 9.7 + 0.6);
    return Math.max(30000, v);
  }

  var received = 0, speedMul = 1, paused = false, holdT = 0;

  function render() {
    var ratio = Math.max(0, Math.min(1, received / TOTAL_BYTES));
    fill.style.width = (ratio * 100).toFixed(1) + '%';
    pctEl.textContent = Math.round(ratio * 100) + '%';
    bytesEl.textContent = Math.round(received / 1000) + ' / ' + Math.round(TOTAL_BYTES / 1000) + ' KB';

speedCurve() は実際の通信を一切読んでおらず、210000 を基準にして周波数と位相が異なる三つのサイン波を足しているだけで、それが「速くなったり遅くなったり」する感覚を実物の通信なしに作り出しています。render() は毎フレーム、擬似的に受信した合計値を TOTAL_BYTES で割ってバーの幅と KB 表示の両方を決めているため、残りページ数もパーセント表示も常に同じ一つの比率から導かれています。05番は22/23フレームで動き、平均強度114.3を記録し、累積面積2.8%は細い進行表示という要素の性質上、カード全体を塗る要素より小さいのが正常です。

0690度単位で角度を合わせる回転スナップコントロール

90度単位で角度を合わせる回転スナップコントロール
新しいタブで開く

06 · 90度単位で角度を合わせる回転スナップコントロール — 計測 累積面積 11.449% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ROTATE_SNAP_DEGboxSizerotateBy
  function boxSize(angleDeg, w, h) {
    var r = angleDeg * Math.PI / 180;
    var c = Math.abs(Math.cos(r)), s = Math.abs(Math.sin(r));
    return { w: w * c + h * s, h: w * s + h * c };
  }

  var target = 0, current = 0;
  function render() {
    var box = boxSize(current, PAGE_W, PAGE_H);
    frame.style.width = box.w + 'px';
    frame.style.height = box.h + 'px';
    page.style.width = PAGE_W + 'px';
    page.style.height = PAGE_H + 'px';
    page.style.transform = 'translate(-50%,-50%) rotate(' + current.toFixed(1) + 'deg)';
    degEl.textContent = Math.round(((current % 360) + 360) % 360) + '°';
  }

  function rotateBy(dir) { target += dir * ROTATE_SNAP_DEG; }

回転ボタンを押すと target が ROTATE_SNAP_DEG ぶん増えるだけで current には直接触れず、描画ループ内の別の一行(current += (target - current) * 0.18)がその差を毎フレーム少しずつ縮めるため、ページは次の90度へ一気に切り替わるのではなく連続的に回転していきます。boxSize() はその途中の角度でも current から枠のサイズを再計算し続けていて、回転の間ずっと枠がなめらかにリサイズされ、静止時だけ形が変わるわけではありません。06番は23/23フレームで動き、平均強度96.2、累積面積11.4%を記録しています。

07残りスクロール距離を計算して滑らかに移動するキーボードページ移動

残りスクロール距離を計算して滑らかに移動するキーボードページ移動
新しいタブで開く

07 · 残りスクロール距離を計算して滑らかに移動するキーボードページ移動 — 計測 累積面積 67.574% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

easeScrollTonavigatepageTops
  Array.prototype.forEach.call(doc.children, function (el) { pageTops.push(el.offsetTop); });

  var currentIndex = 0, scrollAnim = null;
  function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); }
  function easeScrollTo(target, dur, nowT) { scrollAnim = { from: doc.scrollTop, to: target, start: nowT, dur: dur }; }
  function navigate(dir, nowT) {
    var idx = Math.max(0, Math.min(N - 1, currentIndex + dir));
    if (idx === currentIndex) return;
    currentIndex = idx;
    easeScrollTo(pageTops[idx], 520, nowT);
    badge.textContent = (idx + 1) + ' / ' + N;
  }

  var userActive = false;
  function markActive() { if (!userActive) { userActive = true; root.classList.remove('is-demo'); } }
  root.addEventListener('keydown', function (e) {
    if (e.key === 'ArrowDown' || e.key === 'PageDown') { e.preventDefault(); markActive(); navigate(1, lastTs || 0); }
    else if (e.key === 'ArrowUp' || e.key === 'PageUp') { e.preventDefault(); markActive(); navigate(-1, lastTs || 0); }
  });
  doc.addEventListener('wheel', function () { markActive(); }, { passive: true });

pageTops は各ページ自身の offsetTop からあらかじめ一度だけ作られていて、navigate() は動く前から目標位置を正確に把握しており、その場でスクロール距離を測り直すことはしません。easeScrollTo() はその目標を固定の520ミリ秒という長さとともに保存し、別のアニメーションループが easeOutCubic() で doc.scrollTop をその目標へ何フレームにもわたって近づけていくため、一度で切り替わることはありません。07番は23/23フレームで動き、平均強度45.8を記録し、累積面積67.6%は9つの中で最も高く、これはスクロール可能な文書全体が毎フレーム位置を描き直しているためです。

08受信バイト比率ぶん満ちるダウンロード進行リング

受信バイト比率ぶん満ちるダウンロード進行リング
新しいタブで開く

08 · 受信バイト比率ぶん満ちるダウンロード進行リング — 計測 累積面積 2.737% · 動いたフレーム 22/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

lineLenCIRCfinish
  var TOTAL_BYTES = 260000;
  var SPEED = 230000;
  var LOOP_MS = 2000;

  var phase = 'idle', received = 0, doneAt = 0, checkAnim = null;

  function render(ratio) {
    fill.style.strokeDashoffset = (CIRC * (1 - ratio)).toFixed(2);
    pctEl.textContent = Math.round(ratio * 100) + '%';
  }
  function startDownload(nowT) {
    if (phase === 'loading') return;
    phase = 'loading'; received = 0;
    root.classList.remove('pvg--done');
    checkA.style.strokeDashoffset = lenA.toFixed(2);
    checkB.style.strokeDashoffset = lenB.toFixed(2);
    btn.textContent = 'ダウンロード中';
    render(0);
  }
  function finish(nowT) {
    phase = 'done'; doneAt = nowT;
    root.classList.add('pvg--done');
    btn.textContent = '完了';
    checkAnim = { start: nowT };
  }

render() は毎フレーム received を TOTAL_BYTES で割った比率をリングの stroke-dashoffset にそのまま変換しているため、塗りの角度は常にそのときの受信バイト数を円周に対する割合として表したものであり、別に管理されたパーセント値ではありません。finish() は比率が1に達したときだけ発火して checkAnim に処理を渡し、二本のチェックマークの線をそれぞれ時間差のタイマーでフル長からゼロへ dashoffset を縮めていくため、チェックは一度に現れるのではなく一筆ずつ描かれます。08番は22/23フレームで動き、平均強度132.2を記録し、累積面積2.7%はこのサイズのリングとしては正常で、ダウンロード演出が壊れているわけではありません。

09複数の計算を一つのツールバーに集めたビューアヒーロー場面

複数の計算を一つのツールバーに集めたビューアヒーロー場面
新しいタブで開く

09 · 複数の計算を一つのツールバーに集めたビューアヒーロー場面 — 計測 累積面積 40.053% · 動いたフレーム 21/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

HERO_ZOOM_STEPHERO_RAIL_LERPHERO_TOTAL_BYTES
  function computeCurrent() {
    var top = doc.scrollTop, h = doc.clientHeight, best = 0, bestRatio = -1;
    pages.forEach(function (pg, i) {
      var pTop = pg.offsetTop, pBot = pTop + pg.offsetHeight;
      var visTop = Math.max(top, pTop), visBot = Math.min(top + h, pBot);
      var ratio = Math.max(0, visBot - visTop) / pg.offsetHeight;
      if (ratio > bestRatio) { bestRatio = ratio; best = i; }
    });
    return best;
  }
  var activeIdx = -1;
  function markRail(idx) {
    if (idx === activeIdx) return;
    activeIdx = idx;
    thumbs.forEach(function (t, i) { t.classList.toggle('pht__thumb--active', i === idx); });
  }
  var railX = 0;
  function railTarget(idx) {
    var t = thumbs[idx]; if (!t) return 0;
    var want = t.offsetLeft + t.offsetWidth / 2 - rail.clientWidth / 2;
    return -Math.max(0, Math.min(railTrack.scrollWidth - rail.clientWidth, want));
  }

ここの computeCurrent() は01番と同じ重なり比率の計算そのもので、railTarget() も01番のセンタリング計算をそのまま再利用しており、このヒーロー場面はその計算を02番のポインター基準ズーム・08番のバイト比率リングと同じツールバー上で並行して走らせているだけで、どれか一つに置き換えているわけではありません。三つはそれぞれ自分の入力と出力の組を保ったままなので、どれかが1フレームの計算を終えるのを他が待つ必要がありません。09番は21/23フレームで動き、平均強度46.2、累積面積40.1%を記録しています。

それぞれの計算が実際に使われる場面

pdf ビューアの9つの計算は、一つではなくいくつかの実際の場面に分かれます。01番のスクロール比率レールと09番のヒーローツールバーは常時ページ地図を表示し続けたい文書共同編集ツールに向いていて、02番のポインター基準ズームはカーソルの真下の一点を固定したまま拡大したい図面レビューに合っています — これは小さな画面向けの pdf viewer js html 実装の多くに共通する、地点を固定したままのズームという考え方でもあります。03番の検索ハイライトは条項番号を素早く見つけたい契約書ビューアに、04番のスワイプフリックと07番のキーボードイージングはタッチ操作とキーボード操作それぞれに向けた同じページめくりへの別の道であり、05番のロードバーと08番のダウンロードリングは大容量レポートの初回読み込みと最終的なダウンロードボタンという、どちらも受信バイト数に答える場面に向いています。02番のポインター基準ズームには 画像 拡大 表示 UI 9選 — 写真ファイルなしで作るビューア という近い親戚があり、08番のリングと同じ比率駆動の塗りつぶし計算は video player css で組む カスタム動画プレーヤー 9選 — 動画ファイルなしで動く のシークバーにも見られます。

9つのpdf ビューアパターンを面積と強度で測る

この9つのうち複数はスクロールコンテナや transform-origin の背景全体を状態変化のたびに塗り直しており、それが01・02・03・07・09番のすべてが下の表で累積面積40%を超えている理由です — 累積面積は固定カメラの各フレームのうち実際に塗り直された割合を全測定フレームで平均したもので、強度はそこで生じた平均ピクセル差です。05番のロードバーと08番のダウンロードリングは累積面積2.8%・2.7%にとどまりますが、これは細い進行表示という要素の性質上フレーム全体に占める割合が小さいのが自然だからで、この差こそが単に0%と100%でアイコンを切り替えるだけのものと、考え抜かれた pdf プレビューウィンドウとを分ける大きな部分です。04番のスワイプカードは自動デモがスワイプの合間で静止するため10/23 フレームしか動いていません。

| パターン | 累積面積 | 平均強度 | 動いたフレーム |

|---|---|---|---|

| 01 サムネイルレールのスクロール同期 | 59.6% | 39.5 | 23/23 |

| 02 ポインターフォーカルズーム | 57.5% | 93.4 | 23/23 |

| 03 検索一致ハイライト | 40.3% | 67.8 | 22/23 |

| 04 スワイプ速度ページフリック | 42.2% | 27.2 | 10/23 |

| 05 バイト進行ロードバー | 2.8% | 114.3 | 22/23 |

| 06 回転スナップコントロール | 11.4% | 96.2 | 23/23 |

| 07 キーボードページイーズスクロール | 67.6% | 45.8 | 23/23 |

| 08 ダウンロード進行リング | 2.7% | 132.2 | 22/23 |

| 09 ヒーローツールバー場面 | 40.1% | 46.2 | 21/23 |

値が反応を変える場所 — ズーム焦点比率・スワイプフリック速度・回転スナップ角度

下の三つの数値が動きの大部分を決めています。ZOOM_FOCAL_STEP_RATIO は02番がポインター自身の座標を中心に一回のホイール操作でどれだけ拡大するかを決め、SWIPE_FLIP_VELOCITY_PX_S は04番が中央へばね戻りせず次ページへめくり切るために必要な速さを決め、ROTATE_SNAP_DEG は06番の目標角度がボタン一回でどれだけ増え、連続的な lerp がそこへ追いつくまでの幅を決めます。三つとも何を計算するか自体は変えておらず、一回のホイール操作・一回のリリース・一回の押下がその値をどれだけ動かしてからアニメーションが落ち着くか、という幅だけを変えています。

どこで壊れるか — タッチスクロールとの衝突・大容量ファイルの読み込み遅延・キーボード代替経路

01番のレール同期と04番のスワイプフリックはどちらも自分の舞台の上で動きを監視していますが、01番の computeCurrent() は文書コンテナの本物の scroll イベントに乗って動き、04番は生のポインター移動量を直接追いかけているため、縦にスクロールしながら横にスワイプできるページの上でそれぞれのリスナーを自分の要素だけに絞らずに両方を組み合わせると、一つの斜めのドラッグが scroll 起点のページ切り替えと swipe 起点のフリックを同時に発火させることがあり、これはスクロールとスワイプを同じ舞台で混ぜるあらゆる場面に共通する継ぎ目です。05番の speedCurve() と08番の固定 SPEED 定数はどちらも毎フレーム、1秒あたりの割合から受信バイト数を新しく計算しているため、その割合を上げずに TOTAL_BYTES だけを増やしても比率そのものの計算方法は変わらず、比率が1に達するまでの実時間が延びるだけです。9つすべてが自分の tabindex ルートに対してちょうど一つの keydown リスナーを付けていますが、キーの組み合わせは統一されておらず、01番の PageUp・PageDown という代替経路は07番の矢印キーのみの経路には自動では引き継がれず、両者が共有しているのは次ページへのイージングの考え方だけで、キーの集合ではないため、意図して移植する必要があります。これらの制作物の背後にある9つの index.html ファイル、hat2hhry、を見れば、どの項目が scroll イベントから直接再計算していて、どの項目がドラッグやクリックが始まってから初めて再計算するのかが一目でわかります。

変形3つとアクセシビリティ

| 変形 | 値の変化 | 感触 |

|---|---|---|

| ズーム焦点比率を調整する | 02 ZOOM_FOCAL_STEP_RATIO 1.2 → 2.4 | 値を下げるとホイール一回で少しずつしか拡大されず、値を上げると同じホイール操作でもずっと大きく拡大される。 |

| スワイプフリック速度の基準を調整する | 04 SWIPE_FLIP_VELOCITY_PX_S 300 → 600 | 値を下げると軽く押すだけで次のページへ進み、値を上げるとかなり速くスワイプしないと進まない。 |

| 回転スナップ角度を調整する | 06 ROTATE_SNAP_DEG 90 → 45 | 値を上げるとボタン一回で大きな角度回転し、値を下げると同じボタンでもより細かく分割して回転する。 |

prefers-reduced-motion は9つすべてのスクリプトの冒頭でチェックされており、01番のレールはページの自動巡回を止め、08番のリングはアニメーションせずに静止した50%へ直接ジャンプします。同じ一行の matchMedia() チェックが、共通のライブラリを介さずファイルごとに繰り返されています。キーボード対応は9つで揃っておらず、01番はArrowUp・ArrowDown・PageUp・PageDownでページ移動に答え、07番は同じ種類の移動に矢印キーのみで答え、08番はEnterとSpaceでダウンロードを開始しEscapeで途中キャンセルに答えるため、二つを同じページに組み込むときはそれぞれの keydown ハンドラを一つにまとめるのではなく、各ルート自身の tabindex を保ったままにする必要があります。9つのどれも IntersectionObserver でページの重なりを計算しておらず、01番と09番の computeCurrent() は requestAnimationFrame() が呼ばれるたびに getBoundingClientRect() を直接読んでいて、コード量は増えますが、しきい値コールバックが速いスクロールに追いつくのを待つことがありません。04番のすべてのドラッグは個別のマウス・タッチリスナーではなく pointer events で追跡されており、prefers-reduced-motion は他の三つと並んでMDNに文書化されています。

pdf js ビューアはライブラリなしでスクロールとズームを一緒に計算できるか?

pdf js ビューアはライブラリなしでスクロールとズームを一緒に計算できるか、という疑問への答えは、09番が示している特定の組み合わせについては「できる」です。01番の getBoundingClientRect() に基づくスクロール比率と、02番のポインター基準 transform-origin 拡大は同じツールバー上で並んで動いていて、それぞれ自分の入力(片方はスクロール位置、もう片方はホイールやピンチの座標)を読み、自分の出力を書き込むだけで、どちらかがもう一方の計算の完了を待つことはありません。二つが同じフレームでぶつからずに済むのは、どちらの手法も共有のグローバル状態を持っていないからです。computeCurrent() は scrollTop を読んで CSS クラスを書くだけ、applyWheel() はポインター座標を読んで transform を書くだけで、08番のバイト比率リングのような三つ目の計算を同じページに重ねることも、もう一組の独立した読み書きを足すだけで済み、最初の二つを解きほぐす必要はありません。

FAQ — 導入前によくある質問

06番の回転は90度ごとに瞬間的にスナップしますか?

いいえ — ボタンを押すと目標角度が ROTATE_SNAP_DEG ぶん増えるだけで、別の lerp(current += (target - current) * 0.18)が毎フレームその差を少しずつ縮めていくため、ページは一つの90度位置から次へ一気に切り替わるのではなく途中の角度を連続的に通過し、boxSize() もその途中の角度ごとに枠のアスペクト比を再計算しています。

9つのうちどれが最もフレームを塗り直していますか、またその理由は?

07番が累積面積67.6%で最も高く、これはイージングするスクロールが文書コンテナのスクロール位置を何フレームにもわたって描き直しているためで、01番も同じ理由で自分のサムネイル同期スクロールコンテナについて59.6%と僅差で続いています。

03番の検索ボックスは本物のファイルの文字を読んでいますか、それとも簡略化した pdf viewer js download 相当の見せかけですか?

見せかけです、設計上そうなっています — find() は植え込まれた小さな words 配列に対して indexOf(q) === 0 という前方一致だけで比較しており、本物のファイルバイトを全文検索しているわけではありません。この9つは実物を解析した文書ではなく、シミュレートされたページの上で動いています。

出典として参照したMDNドキュメント

IntersectionObserver、requestAnimationFrame()、prefers-reduced-motion、pointer events はいずれもMDNに文書化されていて、上記9つの計算を支えるブラウザAPIです。

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

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