@takagaki/cortex-decisions-viewer 0.12.114 → 0.12.115

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/render.js +149 -27
  2. package/package.json +1 -1
package/dist/render.js CHANGED
@@ -20603,6 +20603,12 @@ ${calendarAddUrl.toString()}
20603
20603
  (inAdj[e.b.id] = inAdj[e.b.id] || []).push(e.a.id);
20604
20604
  });
20605
20605
 
20606
+ // 描き直しの間引き(engine #551 のカクつき対策)。操作・検索・期間の切り替え・ホバーの波の間は毎フレーム描き、
20607
+ // それが済んで止まっている間は、星の瞬き(ゆっくり)のためだけに IDLE_FRAME_MS ごとに描く
20608
+ var IDLE_FRAME_MS = 100;
20609
+ var activeUntil = 0;
20610
+ function poke(ms) { activeUntil = Math.max(activeUntil, performance.now() + ms); }
20611
+
20606
20612
  // 検索: 一致ノード(matched)が光り、その隣接(lit)は残し、他は減光する
20607
20613
  var matched = {}, lit = {}, searching = false;
20608
20614
  // 期間(engine #551)を選んでいるときは、その期間に生まれた・変わった記録だけが一致になる(キーワードと両方なら両方を満たす記録)
@@ -20611,6 +20617,7 @@ ${calendarAddUrl.toString()}
20611
20617
  var range = graphPeriodRange(graphState.period.unit === "all" ? null : graphState.period);
20612
20618
  searching = q.length > 0 || !!range;
20613
20619
  matched = {}; lit = {};
20620
+ poke(250);
20614
20621
  if (!searching) {
20615
20622
  countEl.textContent = "点 " + nodes.length + " / つながり " + edges.length;
20616
20623
  return;
@@ -20659,6 +20666,8 @@ ${calendarAddUrl.toString()}
20659
20666
  var UPSTREAM_HOPS = 1; // 参照元(入辺)を辿る深さ
20660
20667
  var SWEEP_STEP_MS = 120; // 波が1段(1ホップ)進むごとの間隔
20661
20668
  var SWEEP_GROW_MS = 200; // 辺・点が灯り切るまでの時間
20669
+ // ホバーの波が流れ切るまで毎フレーム描く時間(参照元 1 段+参照先 2 段=順番 3 まで+灯り切る時間に余裕を見る)
20670
+ var HOVER_SWEEP_MS = (DOWNSTREAM_HOPS + UPSTREAM_HOPS + 2) * SWEEP_STEP_MS + SWEEP_GROW_MS + 300;
20662
20671
  function computeFitTarget() {
20663
20672
  if (!nodes.length) return null;
20664
20673
  var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
@@ -20686,7 +20695,10 @@ ${calendarAddUrl.toString()}
20686
20695
  view.x += (t.x - view.x) * 0.18;
20687
20696
  view.y += (t.y - view.y) * 0.18;
20688
20697
  }
20698
+ // まだ目に見えて動いているか(止まっていれば描き直しを間引ける)
20699
+ return Math.abs(t.scale - view.scale) > view.scale * 0.0005 || Math.abs(t.x - view.x) > 0.1 || Math.abs(t.y - view.y) > 0.1;
20689
20700
  }
20701
+ var fitMoving = true;
20690
20702
 
20691
20703
  // ロード時に1回だけ: Goldの中心から外へ「ふわっ」と光が広がるイントロ。
20692
20704
  // 金色(#fcd34d)はホバー連鎖・検索の専用色なので、イントロは寒色で別物として見せる。
@@ -20700,6 +20712,7 @@ ${calendarAddUrl.toString()}
20700
20712
  resetBtn.addEventListener("click", function () {
20701
20713
  autoFit = true; // 全ノードが収まる表示へ滑らかに戻す
20702
20714
  resize();
20715
+ poke(250);
20703
20716
  if (!reduceMotion) introStart = performance.now(); // 中心から光が広がるイントロを再生
20704
20717
  });
20705
20718
  canvas.parentNode.appendChild(resetBtn);
@@ -20834,6 +20847,96 @@ ${calendarAddUrl.toString()}
20834
20847
  return n.recordId && n.glow !== null ? 0.4 + 0.6 * n.glow : 1;
20835
20848
  }
20836
20849
 
20850
+ // 点の光(にじみ・shadowBlur)は、点ごとに毎フレーム描くと 700 点で 1 フレーム 30ms を超える(engine #551 のカクつき)。
20851
+ // にじみだけを小さな絵に前もって描いて使い回し、本体の円はその上に正確な半径で描く。
20852
+ // 絵は画面の画素(device px)で作る(shadowBlur は座標変換に影響されない=元の描き方と同じ幅)。
20853
+ // 半径は 0.25px・にじみの幅は小さいうちは 0.5 刻み、大きいと約 4% 刻みに丸めて、絵の数を抑える
20854
+ // (小さい点のにじみの明るさは円の面積に比例するので、1px 刻みでは引いた表示で明るさが ±数十% ずれる)
20855
+ var TAU = Math.PI * 2;
20856
+ // 使い回しの置き場は「最近使った順」の上限つき(上限を超えたら一番古いものから捨てる=ズームし続けても一度に作り直さない)
20857
+ function spriteStore(max) {
20858
+ var m = new Map();
20859
+ return {
20860
+ get: function (k) { var v = m.get(k); if (v) { m.delete(k); m.set(k, v); } return v; },
20861
+ set: function (k, v) { m.set(k, v); if (m.size > max) m.delete(m.keys().next().value); },
20862
+ };
20863
+ }
20864
+ var haloCache = spriteStore(800);
20865
+ // これより大きいにじみ(大きく寄ったとき・画面にわずかな点しか無い)は絵にせず直接描く(大きな絵を溜めない)
20866
+ var HALO_SPRITE_MAX = 512;
20867
+ function haloBlurStep(blur) {
20868
+ if (blur < 0.25) return 0;
20869
+ if (blur <= 12) return Math.round(blur * 2) / 2;
20870
+ return Math.round(12 * Math.pow(1.04, Math.round(Math.log(blur / 12) / Math.log(1.04))) * 2) / 2;
20871
+ }
20872
+ function haloSprite(color, R, blur) {
20873
+ var bq = haloBlurStep(blur);
20874
+ if (!bq) return null;
20875
+ var rq = Math.max(0.25, Math.round(R * 4) / 4);
20876
+ var half = Math.ceil(rq + bq * 1.5 + 2); // にじみ(σ=blur/2)が見える範囲 ≒ 3σ
20877
+ if (half * 2 > HALO_SPRITE_MAX) return false;
20878
+ var key = color + "|" + rq + "|" + bq;
20879
+ var sp = haloCache.get(key);
20880
+ if (sp) return sp;
20881
+ var size = half * 2;
20882
+ var c = document.createElement("canvas");
20883
+ c.width = size; c.height = size;
20884
+ var g = c.getContext("2d");
20885
+ if (!g) return null;
20886
+ // 円は絵の外(左へ size ずらす)に描き、影だけを絵の中央へ落とす=にじみだけが残る
20887
+ g.shadowColor = color;
20888
+ g.shadowBlur = bq;
20889
+ g.shadowOffsetX = size;
20890
+ g.fillStyle = color;
20891
+ g.beginPath();
20892
+ g.arc(half - size, half, rq, 0, TAU);
20893
+ g.fill();
20894
+ sp = { c: c, h: half };
20895
+ haloCache.set(key, sp);
20896
+ return sp;
20897
+ }
20898
+ // X・Y は画素の境目に揃えた値を渡す(半端な位置だと絵の引き伸ばしが入り、1 枚ごとに何倍も重くなる)
20899
+ function drawHalo(color, R, blur, X, Y) {
20900
+ var sp = haloSprite(color, R, blur);
20901
+ if (sp) { ctx.drawImage(sp.c, X - sp.h, Y - sp.h); return; }
20902
+ if (sp === false) {
20903
+ ctx.save();
20904
+ ctx.shadowColor = color;
20905
+ ctx.shadowBlur = blur;
20906
+ ctx.fillStyle = color;
20907
+ ctx.beginPath();
20908
+ ctx.arc(X, Y, R, 0, TAU);
20909
+ ctx.fill();
20910
+ ctx.restore();
20911
+ }
20912
+ }
20913
+ // ラベルも文字を絵にして使い回す(検索で数百のラベルを毎フレーム文字から描くと 1 フレーム 100ms を超える)。
20914
+ // 文字の大きさは画面の画素で一定(11px)なので、ズームしても同じ絵が使える
20915
+ var labelCache = spriteStore(3000);
20916
+ function labelFont(bold) { return (bold ? "600 " : "") + (11 * dpr) + "px 'Noto Sans JP', sans-serif"; }
20917
+ function drawLabel(text, color, bold, X, baseY) {
20918
+ var key = color + "|" + (bold ? 1 : 0) + "|" + text;
20919
+ var sp = labelCache.get(key);
20920
+ if (!sp) {
20921
+ var font = labelFont(bold);
20922
+ var c = document.createElement("canvas");
20923
+ var g = c.getContext("2d");
20924
+ if (!g) return;
20925
+ g.font = font;
20926
+ var asc = Math.ceil(13 * dpr), desc = Math.ceil(4 * dpr); // 11px の文字の上下+少しの余白
20927
+ c.width = Math.max(1, Math.ceil(g.measureText(text).width) + 2);
20928
+ c.height = asc + desc;
20929
+ g.font = font; // 大きさを変えると描き方の設定が戻るので、もう一度
20930
+ g.fillStyle = color;
20931
+ g.fillText(text, 0, asc);
20932
+ sp = { c: c, asc: asc };
20933
+ labelCache.set(key, sp);
20934
+ }
20935
+ ctx.drawImage(sp.c, Math.round(X), Math.round(baseY - sp.asc));
20936
+ }
20937
+ // ホバーの連鎖(参照先・参照元)は触った点が変わったときだけ辿り直す
20938
+ var linFor = null, linCache = null;
20939
+
20837
20940
  function draw() {
20838
20941
  var rect = canvas.getBoundingClientRect();
20839
20942
  var now = performance.now();
@@ -20862,7 +20965,7 @@ ${calendarAddUrl.toString()}
20862
20965
  ctx.fill();
20863
20966
  });
20864
20967
  ctx.globalAlpha = 1;
20865
- if (autoFit) applyFit(false); // 全ノードが収まる位置・倍率へ滑らかに追従
20968
+ fitMoving = autoFit ? !!applyFit(false) : false; // 全ノードが収まる位置・倍率へ滑らかに追従
20866
20969
  ctx.translate(view.x, view.y);
20867
20970
  ctx.scale(view.scale, view.scale);
20868
20971
 
@@ -20882,7 +20985,8 @@ ${calendarAddUrl.toString()}
20882
20985
  }
20883
20986
  return hop;
20884
20987
  }
20885
- if (hovered) {
20988
+ if (hovered && linFor === hovered) lin = linCache;
20989
+ else if (hovered) {
20886
20990
  var downHop = bfsDir(outAdj, DOWNSTREAM_HOPS); // 参照先
20887
20991
  var upHop = bfsDir(inAdj, UPSTREAM_HOPS); // 参照元
20888
20992
  var maxUp = 0;
@@ -20897,6 +21001,7 @@ ${calendarAddUrl.toString()}
20897
21001
  if (id === hovered.id || lin[id] !== undefined) return;
20898
21002
  lin[id] = { order: maxUp + downHop[id], hop: downHop[id], dir: "down" };
20899
21003
  });
21004
+ linFor = hovered; linCache = lin;
20900
21005
  }
20901
21006
  function hopAlpha(d) {
20902
21007
  return d <= 1 ? 1 : Math.max(0.6, 1 - d * 0.1);
@@ -20915,7 +21020,13 @@ ${calendarAddUrl.toString()}
20915
21020
  reference: "rgba(180,196,230,0.32)", source: "rgba(180,196,230,0.32)", link: "rgba(180,196,230,0.22)",
20916
21021
  about: "rgba(196,181,253,0.22)",
20917
21022
  };
21023
+ // 画面に入っている範囲(世界座標)。両端が同じ側の外にある線は描かない
21024
+ var em = 4 / view.scale; // 線の太さの分の余白(画面で 4px)
21025
+ var wx0 = -view.x / view.scale - em, wy0 = -view.y / view.scale - em;
21026
+ var wx1 = (rect.width - view.x) / view.scale + em, wy1 = (rect.height - view.y) / view.scale + em;
20918
21027
  edges.forEach(function (e) {
21028
+ if ((e.a.x < wx0 && e.b.x < wx0) || (e.a.x > wx1 && e.b.x > wx1) ||
21029
+ (e.a.y < wy0 && e.b.y < wy0) || (e.a.y > wy1 && e.b.y > wy1)) return;
20919
21030
  // ホバー中: リネージに入った辺だけ、波の順に「order の低い側→高い側」へ伸びながら金色に。
20920
21031
  if (hovered) {
20921
21032
  var la = lin[e.a.id], lb = lin[e.b.id];
@@ -20955,6 +21066,10 @@ ${calendarAddUrl.toString()}
20955
21066
  ctx.stroke();
20956
21067
  });
20957
21068
 
21069
+ // 点は画面の画素の座標で描く(にじみの絵をそのまま置くため)
21070
+ var k = view.scale * dpr, ox = view.x * dpr, oy = view.y * dpr;
21071
+ var cw = canvas.width, ch = canvas.height;
21072
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
20958
21073
  nodes.forEach(function (n) {
20959
21074
  var style = GROUP_STYLE[n.group] || GROUP_STYLE.phantom;
20960
21075
  var alphaN = 1;
@@ -20977,46 +21092,45 @@ ${calendarAddUrl.toString()}
20977
21092
  }
20978
21093
  var dim = alphaN < 0.3;
20979
21094
  var isGold = !!n.recordId;
21095
+ var r0 = nodeRadius(n);
21096
+ // 画面の画素での位置と半径(ラベル・輪・にじみはすべて画素で描く)
21097
+ // 中心は画素の境目に揃える(にじみの絵と本体の円の中心を合わせる。dpr 2 なら 0.25px 以内の差)
21098
+ var X = Math.round(n.x * k + ox), Y = Math.round(n.y * k + oy), R = r0 * k;
21099
+ var inChain = hovered && n !== hovered && lin[n.id] !== undefined;
21100
+ var blur =
21101
+ (isGold ? (n.glow === null || matched[n.id] ? Math.max(16, n.glow === null ? 16 : 6 + 22 * n.glow) : 6 + 22 * n.glow) : 7) * (dim ? 0.3 : 1) * (inChain ? 1.7 : 1) * (1 + introFlash * 1.8);
21102
+ // ラベル: Gold層レコードはズームイン時のみ表示(引いて全体表示中は出さない)。
21103
+ // それ以外はホバーの連鎖に入ったときか検索ヒット時のみ(ズームに関係なく表示)。
21104
+ var showLabel = (n.recordId && view.scale >= LABEL_MIN_SCALE) || (hovered && lin[n.id] !== undefined && sweepRamp(lin[n.id]) > 0.15) || (searching && lit[n.id]);
21105
+ // 画面の外の点は描かない(にじみの届く幅・輪・右に出るラベルの幅を見込む)
21106
+ var reach = R + Math.max(blur, 18) * 1.5 + 6 * dpr;
21107
+ if (X + reach + (showLabel ? 300 * dpr : 0) < 0 || X - reach > cw || Y + reach < 0 || Y - reach > ch) return;
20980
21108
  // Gold以外の星はゆっくり瞬く
20981
21109
  var tw = isGold ? 1 : 0.72 + 0.28 * (0.5 + 0.5 * Math.sin(now * 0.0017 + n.phase));
20982
21110
  ctx.globalAlpha = alphaN * tw * glowAlpha(n);
20983
- var r0 = nodeRadius(n);
20984
21111
  var fill = isGold ? style.color : "#c9d6f2";
20985
21112
  // 検索一致ノードとホバー中のノードは金色のリングで発光させる
20986
21113
  if ((matched[n.id] || n === hovered) && !dim) {
20987
- ctx.save();
20988
- ctx.shadowColor = "#fcd34d";
20989
- ctx.shadowBlur = 18;
21114
+ drawHalo("#fcd34d", R + 2 * dpr, 18, X, Y);
20990
21115
  ctx.beginPath();
20991
- ctx.arc(n.x, n.y, r0 + 2 / view.scale, 0, Math.PI * 2);
21116
+ ctx.arc(X, Y, R + 2 * dpr, 0, TAU);
20992
21117
  ctx.fillStyle = fill;
20993
21118
  ctx.fill();
20994
- ctx.restore();
20995
21119
  ctx.strokeStyle = "#fcd34d";
20996
- ctx.lineWidth = 2.5 / view.scale;
21120
+ ctx.lineWidth = 2.5 * dpr;
20997
21121
  ctx.beginPath();
20998
- ctx.arc(n.x, n.y, r0 + 3.5 / view.scale, 0, Math.PI * 2);
21122
+ ctx.arc(X, Y, R + 3.5 * dpr, 0, TAU);
20999
21123
  ctx.stroke();
21000
21124
  }
21001
21125
  // 星本体(色のにじみ=グロー付き)
21002
- var inChain = hovered && n !== hovered && lin[n.id] !== undefined;
21003
- ctx.save();
21004
- ctx.shadowColor =
21005
- introFlash > 0.01 ? "#bcd0ff" : isGold ? style.color : "#9fb4e8";
21006
- ctx.shadowBlur =
21007
- (isGold ? (n.glow === null || matched[n.id] ? Math.max(16, n.glow === null ? 16 : 6 + 22 * n.glow) : 6 + 22 * n.glow) : 7) * (dim ? 0.3 : 1) * (inChain ? 1.7 : 1) * (1 + introFlash * 1.8);
21126
+ drawHalo(introFlash > 0.01 ? "#bcd0ff" : isGold ? style.color : "#9fb4e8", R, blur, X, Y);
21008
21127
  ctx.beginPath();
21009
- ctx.arc(n.x, n.y, r0, 0, Math.PI * 2);
21128
+ ctx.arc(X, Y, R, 0, TAU);
21010
21129
  ctx.fillStyle = fill;
21011
21130
  ctx.fill();
21012
- ctx.restore();
21013
- // ラベル: Gold層レコードはズームイン時のみ表示(引いて全体表示中は出さない)。
21014
- // それ以外はホバーの連鎖に入ったときか検索ヒット時のみ(ズームに関係なく表示)。
21015
- if ((n.recordId && view.scale >= LABEL_MIN_SCALE) || (hovered && lin[n.id] !== undefined && sweepRamp(lin[n.id]) > 0.15) || (searching && lit[n.id])) {
21016
- ctx.fillStyle = dim ? "#3d4763" : matched[n.id] ? "#fde68a" : isGold ? "#e7edfb" : "#9fb0d6";
21017
- ctx.font = (matched[n.id] ? "600 " : "") + (11 / view.scale) + "px 'Noto Sans JP', sans-serif";
21131
+ if (showLabel) {
21018
21132
  var label = n.label.length > 24 ? n.label.slice(0, 23) + "…" : n.label;
21019
- ctx.fillText(label, n.x + nodeRadius(n) + 4 / view.scale, n.y + 4 / view.scale);
21133
+ drawLabel(label, dim ? "#3d4763" : matched[n.id] ? "#fde68a" : isGold ? "#e7edfb" : "#9fb0d6", !!matched[n.id], X + R + 4 * dpr, Y + 4 * dpr);
21020
21134
  }
21021
21135
  });
21022
21136
  ctx.globalAlpha = 1;
@@ -21054,7 +21168,7 @@ ${calendarAddUrl.toString()}
21054
21168
  return;
21055
21169
  }
21056
21170
  var nh = nodeAt(ev.clientX, ev.clientY);
21057
- if (nh !== hovered) { hovered = nh; hoverStart = nh ? performance.now() : 0; } // 別ノードでスイープ再生
21171
+ if (nh !== hovered) { hovered = nh; hoverStart = nh ? performance.now() : 0; poke(HOVER_SWEEP_MS); } // 別ノードでスイープ再生
21058
21172
  canvas.style.cursor = hovered ? "pointer" : "grab";
21059
21173
  });
21060
21174
  canvas.addEventListener("mousedown", function (ev) {
@@ -21086,6 +21200,7 @@ ${calendarAddUrl.toString()}
21086
21200
  view.x = mx - (mx - view.x) * factor;
21087
21201
  view.y = my - (my - view.y) * factor;
21088
21202
  view.scale *= factor;
21203
+ poke(250);
21089
21204
  }, { passive: false });
21090
21205
 
21091
21206
  // ビルド時に座標計算済みなら、ウォームアップ(重い同期計算)を完全に省略する=開いた瞬間ゼロ待ち。
@@ -21104,10 +21219,17 @@ ${calendarAddUrl.toString()}
21104
21219
  if (!running || !document.body.contains(canvas)) { running = false; return; }
21105
21220
  // 収束後(alpha減衰しきり・無操作)は O(n²) の物理計算 tick を止める。これが「固まり続ける」主因。
21106
21221
  // ドラッグ/パン中は動かす必要があるので回す。描画(draw)はホバー演出・イントロのため毎フレーム続ける。
21107
- if (alpha > 0.004 || draggingNode || panning) tick();
21108
- draw();
21222
+ var moving = alpha > 0.004 || draggingNode || panning;
21223
+ if (moving) tick();
21224
+ // 動いている・操作の直後・イントロ・自動フレーミングの途中は毎フレーム描く。止まっている間は瞬きのためだけに間引いて描く(engine #551)
21225
+ var now = performance.now();
21226
+ if (moving || introStart || fitMoving || now < activeUntil || now - lastDrawAt >= IDLE_FRAME_MS) {
21227
+ lastDrawAt = now;
21228
+ draw();
21229
+ }
21109
21230
  requestAnimationFrame(loop);
21110
21231
  }
21232
+ var lastDrawAt = 0;
21111
21233
  loop();
21112
21234
  }
21113
21235
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.114",
3
+ "version": "0.12.115",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,