blockyard 0.0.9 → 0.1.0
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.
- package/CHANGELOG.md +251 -1
- package/README.md +42 -23
- package/bin/blockyard.js +2 -1
- package/docs/API.md +16 -14
- package/docs/ARCHITECTURE.md +92 -5
- package/docs/CONFIGURATION.md +33 -26
- package/docs/GETTING-STARTED.md +5 -2
- package/docs/INSTALL.md +90 -33
- package/docs/MEASUREMENTS.md +147 -0
- package/docs/SECURITY.md +32 -15
- package/docs/TROUBLESHOOTING.md +35 -1
- package/docs/USER-GUIDE.md +266 -26
- package/package.json +1 -1
- package/public/404.html +1 -1
- package/public/css/app.css +306 -82
- package/public/donate-qr.png +0 -0
- package/public/index.html +295 -103
- package/public/js/agents.js +228 -51
- package/public/js/app.js +82 -8
- package/public/js/blockscene3d.js +179 -27
- package/public/js/charts.js +21 -21
- package/public/js/depthchart.js +31 -27
- package/public/js/details3d.js +1456 -71
- package/public/js/doom.js +31 -0
- package/public/js/dosaudio.js +48 -0
- package/public/js/dosgame.js +389 -0
- package/public/js/dosio.js +186 -0
- package/public/js/dospc.js +1353 -0
- package/public/js/dosworker.js +196 -0
- package/public/js/login.js +5 -0
- package/public/js/markets.js +46 -8
- package/public/js/mining.js +310 -32
- package/public/js/panels.js +14 -10
- package/public/js/pricechart.js +14 -13
- package/public/js/quake.js +20 -0
- package/public/js/settings.js +103 -21
- package/public/js/soundcard.js +459 -0
- package/public/js/theme.js +235 -0
- package/public/js/wolf3d.js +22 -0
- package/public/js/x86.js +1978 -0
- package/scripts/donate-qr.py +12 -9
- package/scripts/dos-bench.js +56 -0
- package/scripts/setup.js +34 -12
- package/scripts/shots.mjs +6 -0
- package/scripts/smoke.sh +1 -1
- package/scripts/tls.js +31 -0
- package/server/chain/index/build.js +21 -4
- package/server/collect/monitor.js +30 -1
- package/server/collect/network.js +295 -0
- package/server/config.js +46 -22
- package/server/http/api.js +49 -5
- package/server/http/games.js +77 -0
- package/server/http/server.js +8 -0
- package/server/main.js +53 -8
- package/server/tls/selfsigned.js +160 -0
- package/systemd/blockyard.service +7 -5
- package/docs/PRIVATE-LEADERBOARD.md +0 -230
- package/docs/STATE-2026-09-09.md +0 -200
|
@@ -912,8 +912,20 @@ export const FX_NONE = Object.freeze({ glow: 0, outline: 0, lift: 0, color: null
|
|
|
912
912
|
// just outside one side of the board and leaving past the other
|
|
913
913
|
export function fxFront(fx) {
|
|
914
914
|
const qs = [0, fx.gridW * fx.dx, fx.gridH * fx.dy, fx.gridW * fx.dx + fx.gridH * fx.dy];
|
|
915
|
-
|
|
916
|
-
|
|
915
|
+
// THE MARGIN IS THE PANEL'S, NOT A CONSTANT (operator, 2026-09-15: "It needs to cleanly travel on
|
|
916
|
+
// and off screen"). Four units clears the BOARD, and the panel reaches much further than that --
|
|
917
|
+
// measured 21.8 units beyond a 96-wide board -- so a front that started four units out began its
|
|
918
|
+
// sweep in plain view and ended it there. render3d measures the real overhang from the viewRect
|
|
919
|
+
// and puts it on `fx.margin`; four is the fallback for a caller with no panel (tests).
|
|
920
|
+
const m = fx.margin ?? 4;
|
|
921
|
+
const lo = Math.min(...qs) - m, hi = Math.max(...qs) + m;
|
|
922
|
+
// A SEARCHLIGHT PANS (operator, 2026-09-15: "the scanner needs to be more like a search light,
|
|
923
|
+
// slowly panning back and forth"). A single linear pass is a scanner; sweeping out and back is a
|
|
924
|
+
// light being AIMED, which is what reads as something looking for things. `pan` is set for the
|
|
925
|
+
// top-down board only -- on the price chart the camera is low and one pass along the hours is
|
|
926
|
+
// the right motion.
|
|
927
|
+
const t = fx.pan ? 1 - Math.abs(((fx.u * 2) % 2) - 1) : fx.u;
|
|
928
|
+
return lo + t * (hi - lo);
|
|
917
929
|
}
|
|
918
930
|
|
|
919
931
|
// a deterministic 0..1 from an integer: where a firework bursts, which cube flares. Hashed, not
|
|
@@ -957,8 +969,26 @@ export function fxAt(t, fx) {
|
|
|
957
969
|
return { glow: A * w, outline: 0.8 * A * w, lift: 0, color: [255, 255, 255] };
|
|
958
970
|
}
|
|
959
971
|
case 'scan': {
|
|
960
|
-
|
|
961
|
-
|
|
972
|
+
// the beam's own width -- ten units, so a whole swath of blocks is lit as it passes
|
|
973
|
+
// (2026-09-15: "Reach more blocks with the scanner effect")
|
|
974
|
+
// THE LIGHT IS THE POINT (operator, 2026-09-15: "We really need to make it much more obvious
|
|
975
|
+
// somehow that the blocks are being illuminated"). The swath tracks the beam's own half-width
|
|
976
|
+
// (fx.beamR, set with the cone) so light and geometry cannot disagree, and it hits harder:
|
|
977
|
+
// a near-white glow, a bright outline and real lift, so a struck cube stands up out of the
|
|
978
|
+
// board rather than merely brightening.
|
|
979
|
+
const w = g((along() - fxFront(fx)) / (fx.beamR ?? 5.5));
|
|
980
|
+
return { glow: Math.min(1, 1.6 * A * w), outline: Math.min(1, 1.2 * A * w), lift: 1.4 * A * w, color: [225, 248, 255] };
|
|
981
|
+
}
|
|
982
|
+
case 'xray': {
|
|
983
|
+
// X-RAY (2026-09-15, operator: "Add xray as additional choosable effect"): a front sweeps the
|
|
984
|
+
// board and behind it every tile goes x-ray -- its body dims to glass and its edges and a
|
|
985
|
+
// raster across its faces light up (buildScene's xray treatment) -- then develops back to
|
|
986
|
+
// solid over a few units, the front itself a bright edge
|
|
987
|
+
const d = fxFront(fx) - along(); // > 0 once the front has passed
|
|
988
|
+
const edge = g(d / 1.2);
|
|
989
|
+
const back = d > 0 && d < 9 ? (d < 5 ? 1 : 1 - (d - 5) / 4) : 0;
|
|
990
|
+
const xray = Math.max(edge, back) * A;
|
|
991
|
+
return xray > 0.02 ? { glow: 0.5 * A * edge, outline: A * Math.max(edge, 0.8 * back), lift: 0, color: [190, 240, 255], xray } : FX_NONE;
|
|
962
992
|
}
|
|
963
993
|
case 'ball': {
|
|
964
994
|
// "illuminating everything it comes near": brightest right under the ball, gone ~5 units off
|
|
@@ -997,30 +1027,41 @@ export function fxAt(t, fx) {
|
|
|
997
1027
|
return { glow: hot * A * w, outline: 0.8 * hot * A * w, lift: (inward ? 0.4 : 3.2) * A * w,
|
|
998
1028
|
color: inward ? [150, 190, 255] : [255, 255, 245] };
|
|
999
1029
|
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
let glow = 0, outline = 0, lift = 0, col = [255, 200, 120];
|
|
1003
|
-
for (let i = 0; i < 3; i++) {
|
|
1004
|
-
const t0 = 0.05 + 0.26 * i, life = 0.45;
|
|
1005
|
-
const v = (fx.u - t0) / life;
|
|
1006
|
-
if (!(v > 0 && v < 1)) continue;
|
|
1007
|
-
const bx = fxHash(fx.seed + i * 31 + 1) * fx.gridW, by = fxHash(fx.seed + i * 31 + 2) * fx.gridH;
|
|
1008
|
-
const r = 9 * Math.pow(v, 0.55);
|
|
1009
|
-
const w = g((Math.hypot(cx - bx, cy - by) - r) / 1.4) * (1 - v);
|
|
1010
|
-
if (w > glow) {
|
|
1011
|
-
glow = w; outline = 0.9 * w; lift = 1.8 * w;
|
|
1012
|
-
col = [[255, 170, 110], [140, 220, 255], [220, 160, 255]][i];
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
return glow > 0.02 ? { glow: A * glow, outline: A * outline, lift: A * lift, color: col } : FX_NONE;
|
|
1016
|
-
}
|
|
1030
|
+
// 'firework' lights through fxNow's heads now (details3d fireworkShells): one head per live
|
|
1031
|
+
// shell, in the shell's colour -- see the heads branch below
|
|
1017
1032
|
case 'flare': {
|
|
1018
1033
|
// one cube goes supernova and lights its neighbourhood -- the same hashed cube every replay
|
|
1019
1034
|
const fxp = fxHash(fx.seed + 7) * fx.gridW, fyp = fxHash(fx.seed + 8) * fx.gridH;
|
|
1020
1035
|
const bell = Math.sin(Math.PI * fx.u);
|
|
1021
1036
|
const d = Math.hypot(cx - fxp, cy - fyp);
|
|
1022
|
-
|
|
1023
|
-
|
|
1037
|
+
// A SUPERNOVA'S LIGHT (2026-09-15, drawn in details3d drawSupernova): on a thin board -- the
|
|
1038
|
+
// price board, eight deep -- the light reaches three times as far, so the chart is lit, and
|
|
1039
|
+
// it cools with the remnant, gold to red to violet, after the white of the blast
|
|
1040
|
+
const thin = fx.gridH <= 12;
|
|
1041
|
+
const w = g(d / (1.2 + (thin ? 22 : 7) * bell)) * bell;
|
|
1042
|
+
const cool = Math.max(0, Math.min(1, (fx.u - 0.2) / 0.8));
|
|
1043
|
+
// white at the blast, then blue, then violet, as the cloud goes (details3d drawSupernova)
|
|
1044
|
+
const col = cool < 0.3 ? [Math.round(255 - 105 * cool / 0.3), Math.round(255 - 60 * cool / 0.3), 255] : [Math.round(150 + 25 * (cool - 0.3) / 0.7), Math.round(195 - 85 * (cool - 0.3) / 0.7), 255];
|
|
1045
|
+
// THE SHOCKWAVE THROWS WHAT IT CROSSES (operator, 2026-09-15: "Consider perturbing all the
|
|
1046
|
+
// candles that are affected by the shockwave"): the ring drawn in drawSupernova (4.5 shell
|
|
1047
|
+
// radii, out over u 0.14-0.6) runs through the tiles here too -- a tile it reaches is thrown
|
|
1048
|
+
// up hard and lit white, and shakes itself out for a while after the front has passed
|
|
1049
|
+
let lift = 1.4 * A * w, glow = A * w, outline = 0.8 * A * w, colour = col;
|
|
1050
|
+
if (fx.u >= 0.14) {
|
|
1051
|
+
const fr = Math.min(1, (fx.u - 0.14) / 0.46), ring = (thin ? 27 : 31) * (1 - Math.pow(1 - fr, 2.2));
|
|
1052
|
+
const front = g((d - ring) / 1.8) * (1 - fr * 0.6);
|
|
1053
|
+
const passed = ring - d; // > 0 once the front has gone by
|
|
1054
|
+
// the shaking starts as the front APPROACHES, five units out, not once it has passed
|
|
1055
|
+
// (operator: "trigger their shaking sooner in the blast radius")
|
|
1056
|
+
const shake = passed > -5 && fx.u < 0.75 ? Math.max(0, Math.sin(fx.u * 70 + jitterOf(t.txid, 'sn' + fx.seed) * 6.28)) * Math.exp(-Math.max(0, passed) / 12) * (1 - (fx.u - 0.14) / 0.61) : 0;
|
|
1057
|
+
if (front > 0.02 || shake > 0.02) {
|
|
1058
|
+
lift = Math.max(lift, 0.5 * A * front + 0.12 * A * shake); // a tenth of the first cut (operator: "affects the candles too much", "half as much still", "we really need to turn down the block shaking")
|
|
1059
|
+
glow = Math.max(glow, A * front + 0.4 * A * shake);
|
|
1060
|
+
outline = Math.max(outline, A * front);
|
|
1061
|
+
if (front > w) colour = [255, 255, 255];
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
return glow > 0.02 || lift > 0.05 ? { glow: Math.min(1, glow), outline: Math.min(1, outline), lift, color: colour } : FX_NONE;
|
|
1024
1065
|
}
|
|
1025
1066
|
case 'wave': {
|
|
1026
1067
|
// a swell rolling across the board: the cubes rise and fall with it, several crests at once
|
|
@@ -1125,6 +1166,9 @@ export function fxAt(t, fx) {
|
|
|
1125
1166
|
const col = [Math.round(140 + 115 * ph), Math.round(90 + 140 * (1 - ph)), Math.round(190 + 60 * ph)];
|
|
1126
1167
|
return { glow: 0.85 * A * w, outline: 0.2 * A * w, lift: 0, color: col };
|
|
1127
1168
|
}
|
|
1169
|
+
case 'pulse': // the price line's surge lights the candles under its head (details3d fxNow)
|
|
1170
|
+
case 'firework': // each shell lights its surroundings in its colour (details3d fxNow)
|
|
1171
|
+
case 'blackhole': // the hole's head shrinks the candles it reaches toward nothing (scale) and lights the rest orange
|
|
1128
1172
|
case 'lightcycle':
|
|
1129
1173
|
case 'packets':
|
|
1130
1174
|
case 'centipede':
|
|
@@ -1151,10 +1195,29 @@ export function fxAt(t, fx) {
|
|
|
1151
1195
|
// the very fade that meant it was working. Measured: 0 cubes hidden over a whole run.
|
|
1152
1196
|
// So the physical effect uses PROXIMITY alone and the visual effect keeps using w.
|
|
1153
1197
|
const reach = g(Math.hypot(ddx, ddy) / Math.max(0.2, hd.r ?? 0.8));
|
|
1198
|
+
// `release` (1 unless the head says otherwise): the head letting go of everything it
|
|
1199
|
+
// holds, evenly -- the pull and the scale fade by it, so a tile under the head glides
|
|
1200
|
+
// home over the head's whole release rather than when its reach finally passes it
|
|
1201
|
+
const rel = hd.release ?? 1;
|
|
1154
1202
|
best = {
|
|
1155
1203
|
glow: 0.85 * w, outline: 0.8 * w, lift: (hd.lift ?? 0) * w, color: hd.color,
|
|
1156
1204
|
hide: (hd.hide ?? 0) > 0 && reach > 0.45 ? 1 : 0,
|
|
1157
|
-
scale: hd.scale == null ? 1 : 1 - (1 - hd.scale) * reach,
|
|
1205
|
+
scale: hd.scale == null ? 1 : 1 - (1 - hd.scale) * reach * rel,
|
|
1206
|
+
// `pull`: how hard the head drags this tile toward itself (the black hole; buildScene
|
|
1207
|
+
// shears the faces toward the head's screen point by it)
|
|
1208
|
+
pull: (hd.pull ?? 0) * reach * rel, pullAt: hd.pull ? { x: hd.x, y: hd.y, z: hd.z ?? 0 } : null,
|
|
1209
|
+
// the pull at the head's full size (rPeak): the orbit is laid out against this, so it
|
|
1210
|
+
// does not unwind as the head shrinks; the current pull only says how far along the
|
|
1211
|
+
// line from home to that orbit the tile sits
|
|
1212
|
+
pullPeak: (hd.pull ?? 0) * (hd.rPeak ? g(Math.hypot(ddx, ddy) / Math.max(0.2, hd.rPeak)) : reach),
|
|
1213
|
+
// `lean`: how much the corners lean and stretch toward the head on top of the glide
|
|
1214
|
+
// (1 on the price board; 0 on the block board, where the cubes are lifted out whole)
|
|
1215
|
+
lean: hd.lean ?? 1,
|
|
1216
|
+
// `shrink`: scale the footprint with the height, so the cube stays a cube as it goes
|
|
1217
|
+
shrink: hd.shrink ? 1 : 0,
|
|
1218
|
+
// `orbitR`: the head's reach in grid units -- buildScene lays the block board's orbits
|
|
1219
|
+
// out at a fixed distance round the hole from it, rather than part-way home
|
|
1220
|
+
orbitR: hd.r ?? 0,
|
|
1158
1221
|
};
|
|
1159
1222
|
}
|
|
1160
1223
|
}
|
|
@@ -1220,6 +1283,12 @@ export function buildScene(tiles, o = {}) {
|
|
|
1220
1283
|
const ground = [];
|
|
1221
1284
|
const air = [];
|
|
1222
1285
|
const shadows = [];
|
|
1286
|
+
// IN THE AIR, PAINTED LAST (operator, 2026-09-15, of the black hole on the block board: "There
|
|
1287
|
+
// are too many collision errors happening"): a cube pulled off the board toward the hole was
|
|
1288
|
+
// still painted in the board's own order, so it cut through the neighbours it was passing over.
|
|
1289
|
+
// Its faces are moved to the end of the frame instead, the least-pulled first, so the ones
|
|
1290
|
+
// nearest the hole are on top of everything
|
|
1291
|
+
const pulled = new Map();
|
|
1223
1292
|
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
1224
1293
|
const note = (p) => {
|
|
1225
1294
|
if (p.x < minX) minX = p.x;
|
|
@@ -1266,10 +1335,70 @@ export function buildScene(tiles, o = {}) {
|
|
|
1266
1335
|
// A SHORTER CUBE, the same way: `tall` on a copy. A collapse or a dig is a height override,
|
|
1267
1336
|
// never an edit to the tile the board was built from.
|
|
1268
1337
|
const sc = fxv.scale == null ? 1 : fxv.scale;
|
|
1269
|
-
|
|
1338
|
+
let shaped = sc < 0.999 ? { ...t, tall: Math.max(0.04, cubeHeight(t) * sc) } : t;
|
|
1339
|
+
// a WHOLE smaller cube, not a plate, where the head asks for it (the black hole over the block
|
|
1340
|
+
// board): the footprint shrinks about its centre with the height
|
|
1341
|
+
if (sc < 0.999 && fxv.shrink) { const s2 = Math.max(0.08, t.s * Math.max(0.08, sc)); shaped = { ...shaped, x: t.x + (t.s - s2) / 2, y: t.y + (t.s - s2) / 2, s: s2 }; }
|
|
1270
1342
|
// NEON is a flat cube: no facets, no crown -- solid faces and the tubes on their edges
|
|
1271
1343
|
const f = tileFaces(fxv.lift > 0.001 ? { ...shaped, fxz: fxv.lift } : shaped, o, t.s >= facetMin && o.neon !== true);
|
|
1272
|
-
|
|
1344
|
+
// SPAGHETTIFIED (operator, 2026-09-15: "the candles need to stretch and bend toward the hole
|
|
1345
|
+
// before they shrink"): a tile within a pulling head's reach has its faces sheared toward the
|
|
1346
|
+
// head's point on screen -- the top corners dragged hard, the base corners a little -- so it
|
|
1347
|
+
// leans and lengthens toward the hole before the scale takes it. Each corner object is moved
|
|
1348
|
+
// once, however many faces share it.
|
|
1349
|
+
if (fxv.pull > 0.001 && fxv.pullAt) {
|
|
1350
|
+
pulled.set(String(t.txid), fxv.pull);
|
|
1351
|
+
// ...AND ORBITS (operator, 2026-09-15: "The chart bars need to cleanly orbit the disc"): the
|
|
1352
|
+
// tile is swept round the hole along the disk's flow as it is drawn in -- its centre swings
|
|
1353
|
+
// about the hole by an angle that grows with the pull and the clock, on a radius that
|
|
1354
|
+
// shrinks with the pull, flattened toward the disk's plane -- and its corners lean and
|
|
1355
|
+
// stretch toward the hole on top of that
|
|
1356
|
+
const hp = project(fxv.pullAt.x, fxv.pullAt.y, fxv.pullAt.z, o);
|
|
1357
|
+
// EVERY CORNER THE CUBE HAS, the bevel's inner quad included -- it was left at home while
|
|
1358
|
+
// the top and sides moved, so a bevelled cube on the block board drew as a slab stretched
|
|
1359
|
+
// from its old place to its new one (operator, 2026-09-15: "too much bad shearing"); the
|
|
1360
|
+
// crown's points are projected later through f.P, which is shifted the same way below
|
|
1361
|
+
const pts = [...f.top, ...f.sides.flatMap((sd) => sd.points), ...(f.inset ?? [])];
|
|
1362
|
+
const uniq = [...new Set(pts)];
|
|
1363
|
+
const cx0 = uniq.reduce((a, p) => a + p.x, 0) / uniq.length, cy0 = uniq.reduce((a, p) => a + p.y, 0) / uniq.length;
|
|
1364
|
+
// THE ORBIT IS LAID OUT AGAINST THE HOLE'S FULL SIZE, and the tile sits somewhere on the
|
|
1365
|
+
// straight line between its home and that orbit by the CURRENT pull (operator, 2026-09-15:
|
|
1366
|
+
// "Instead of the bars reversing into place, can they just reorient themselves into their
|
|
1367
|
+
// original positions when the blackhole starts to disappear?"). While the hole grows the
|
|
1368
|
+
// tile glides out to its orbit and turns with it; while it shrinks the pull falls and the
|
|
1369
|
+
// tile glides straight home -- the orbit never unwinds.
|
|
1370
|
+
// STEADILY (operator, 2026-09-15: "The bars don't cleanly leave or return ... They sorta get
|
|
1371
|
+
// ripped off, and snapped back into place"): the capture and the release are eased with a
|
|
1372
|
+
// smoothstep, and the orbit is KEPLERIAN -- its angular speed goes with the square of the
|
|
1373
|
+
// pull, so a bar far out barely turns and one close in whips round -- where before every
|
|
1374
|
+
// bar's target swept a wide circle at one speed and a bar just being caught was yanked after
|
|
1375
|
+
// it. The angle's starting offset is small for the same reason.
|
|
1376
|
+
const peak = Math.max(fxv.pullPeak ?? fxv.pull, 0.001), kl = Math.min(1, fxv.pull / peak), k0 = kl * kl * (3 - 2 * kl);
|
|
1377
|
+
const dx = cx0 - hp.x, dy = cy0 - hp.y, r0 = Math.hypot(dx, dy) || 1, a0 = Math.atan2(dy, dx);
|
|
1378
|
+
// HALF THE SPEED (operator, 2026-09-15: "The bars sping around much much too quickly. Try to
|
|
1379
|
+
// calm it down by at least half to start" -- then "slow the spin ... by another half. Want it
|
|
1380
|
+
// smoother and calmer"): 0.00035 a millisecond at full pull, a quarter of the 0.0014 it was
|
|
1381
|
+
const swing = peak * peak * (o.now ?? 0) * 0.00035 + 0.5 * peak; // clockwise on screen, Keplerian
|
|
1382
|
+
// ROUND THE HOLE, on the block board (operator, 2026-09-15: "the blocks don't look right"):
|
|
1383
|
+
// part-way home from a hole twenty units up left the cubes hanging in the middle distance;
|
|
1384
|
+
// each is carried instead to its own ring round the hole, one and a half to three reaches
|
|
1385
|
+
// out in screen pixels, and glides straight back from it as the pull falls
|
|
1386
|
+
const ring = fxv.lean === 0 && fxv.orbitR > 0 && o.unit ? fxv.orbitR * o.unit * (1.5 + 1.5 * fxHash(Math.round(t.x * 31 + t.y * 7 + 3))) : null;
|
|
1387
|
+
const r1 = ring != null ? r0 + (ring - r0) * peak : r0 * (1 - 0.55 * peak), a1 = a0 + swing;
|
|
1388
|
+
const cx1 = hp.x + Math.cos(a1) * r1, cy1 = hp.y + Math.sin(a1) * r1 * (1 - 0.6 * peak); // flattened toward the disk
|
|
1389
|
+
const sx = (cx1 - cx0) * k0, sy = (cy1 - cy0) * k0;
|
|
1390
|
+
const tops = new Set([...f.top, ...f.sides.flatMap((sd) => [sd.points[0], sd.points[1]])]);
|
|
1391
|
+
for (const p of uniq) {
|
|
1392
|
+
p.x += sx; p.y += sy; // carried toward, and round, the orbit
|
|
1393
|
+
const k = (tops.has(p) ? 0.55 : 0.2) * peak * k0 * (fxv.lean ?? 1); // and leaning in (not on the block board: lean 0)
|
|
1394
|
+
p.x += (hp.x - p.x) * k; p.y += (hp.y - p.y) * k;
|
|
1395
|
+
}
|
|
1396
|
+
const P0 = f.P, kc = 0.55 * peak * k0 * (fxv.lean ?? 1);
|
|
1397
|
+
f.P = (x, y, z) => { const p = P0(x, y, z); const q = { x: p.x + sx, y: p.y + sy }; q.x += (hp.x - q.x) * kc; q.y += (hp.y - q.y) * kc; return q; };
|
|
1398
|
+
}
|
|
1399
|
+
// an x-rayed tile's body is glass: most of its alpha goes, and its edges come back below
|
|
1400
|
+
const xray = fxv.xray ?? 0;
|
|
1401
|
+
const a = (t.alpha ?? 1) * (1 - 0.72 * xray);
|
|
1273
1402
|
const airborne = (t.z ?? 0) > 0.02;
|
|
1274
1403
|
const out = airborne && !o.oblique ? air : ground;
|
|
1275
1404
|
// A tile that just locked flashes brighter for a moment -- the Tetris
|
|
@@ -1582,6 +1711,23 @@ export function buildScene(tiles, o = {}) {
|
|
|
1582
1711
|
// a tint at the top's weight (0.5) was there in the pixels and nowhere to the eye (measured:
|
|
1583
1712
|
// the scan's peak moved a candle's face by a 0.26 cyan wash). At 0.85 it reads as struck.
|
|
1584
1713
|
if (pulse > 0.03 && o.axes) for (const side of f.sides) out.push({ txid: t.txid, face: 'glow', points: side.points, fill: `rgba(${fxv.color.join(',')},${round3(0.85 * pulse * a)})` });
|
|
1714
|
+
// THE X-RAY TREATMENT: bright edges round every face, and a raster of thin lines across the top
|
|
1715
|
+
// and the sides, so the tile reads as a wireframe scan of itself
|
|
1716
|
+
if (xray > 0.02) {
|
|
1717
|
+
const xc = `rgba(200,245,255,${round3(0.95 * xray)})`;
|
|
1718
|
+
out.push({ txid: t.txid, face: 'outline', points: f.top, fill: 'rgba(0,0,0,0)', stroke: xc, lw: 1.2, always: true });
|
|
1719
|
+
for (const side of f.sides) out.push({ txid: t.txid, face: 'outline', points: side.points, fill: 'rgba(0,0,0,0)', stroke: xc, lw: 1.2, always: true });
|
|
1720
|
+
const raster = (poly) => {
|
|
1721
|
+
const [p0, p1, p2, p3] = poly; // p0->p1 and p3->p2 are the edges the lines run between
|
|
1722
|
+
for (let i = 1; i <= 3; i++) {
|
|
1723
|
+
const u = i / 4;
|
|
1724
|
+
const a0 = { x: p0.x + (p3.x - p0.x) * u, y: p0.y + (p3.y - p0.y) * u }, b0 = { x: p1.x + (p2.x - p1.x) * u, y: p1.y + (p2.y - p1.y) * u };
|
|
1725
|
+
out.push({ txid: t.txid, face: 'outline', points: [a0, b0], fill: 'rgba(0,0,0,0)', stroke: `rgba(160,230,255,${round3(0.45 * xray)})`, lw: 0.8, always: true });
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1728
|
+
raster(f.top);
|
|
1729
|
+
for (const side of f.sides) raster(side.points);
|
|
1730
|
+
}
|
|
1585
1731
|
if (fxv.outline > 0.03) {
|
|
1586
1732
|
const col = fxv.color.join(',');
|
|
1587
1733
|
out.push({ txid: t.txid, face: 'outline', points: f.top, fill: 'rgba(0,0,0,0)', stroke: `rgba(${col},${round3(0.95 * fxv.outline * a)})`, lw: 1 + 4 * fxv.outline });
|
|
@@ -1630,7 +1776,13 @@ export function buildScene(tiles, o = {}) {
|
|
|
1630
1776
|
if (o.oblique && !viewerLit && zt < 1.5) shadows.push(...restingShadowOps(t, o, 1 - zt / 1.5));
|
|
1631
1777
|
}
|
|
1632
1778
|
}
|
|
1633
|
-
|
|
1779
|
+
let ops = o.oblique ? [...shadows, ...ground] : [...ground, ...shadows, ...air];
|
|
1780
|
+
if (pulled.size) {
|
|
1781
|
+
const rest = [], lifted = [];
|
|
1782
|
+
ops.forEach((op, i) => (pulled.has(String(op.txid)) ? lifted : rest).push({ op, i }));
|
|
1783
|
+
lifted.sort((p, q) => (pulled.get(String(p.op.txid)) - pulled.get(String(q.op.txid))) || (p.i - q.i));
|
|
1784
|
+
ops = [...rest.map((e) => e.op), ...lifted.map((e) => e.op)];
|
|
1785
|
+
}
|
|
1634
1786
|
return { ops, bounds: ops.length ? { minX, maxX, minY, maxY } : null, count: ordered.length };
|
|
1635
1787
|
}
|
|
1636
1788
|
|
package/public/js/charts.js
CHANGED
|
@@ -12,12 +12,11 @@
|
|
|
12
12
|
// old chart and calling it current is the same sin as inventing a number, so the
|
|
13
13
|
// pill says how old it is.
|
|
14
14
|
const M = { top: 8, right: 10, bottom: 18, left: 46 };
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const PALETTE = [COL.accent, COL.info, COL.ok, COL.purple, COL.cyan, COL.pink, COL.warn, COL.bad];
|
|
15
|
+
// THE COLOURS COME FROM THE THEME (theme.js, 2026-09-16): COL is the theme module's live object,
|
|
16
|
+
// refilled when the operator picks a theme, and the chrome colours below read INK the same way --
|
|
17
|
+
// a chart painted from literals of the dark look was unreadable on a light card.
|
|
18
|
+
import { COL, INK } from './theme.js';
|
|
19
|
+
const palette = () => [COL.accent, COL.info, COL.ok, COL.purple, COL.cyan, COL.pink, COL.warn, COL.bad];
|
|
21
20
|
|
|
22
21
|
function prep(canvas) {
|
|
23
22
|
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
@@ -64,7 +63,7 @@ export function resetCanvas(canvas) {
|
|
|
64
63
|
export function empty(canvas, msg = 'no data from the node yet') {
|
|
65
64
|
if (canvas.__hasData) { markStale(canvas, msg); return; }
|
|
66
65
|
const { ctx, w, h } = prep(canvas);
|
|
67
|
-
ctx.fillStyle =
|
|
66
|
+
ctx.fillStyle = INK.textDim;
|
|
68
67
|
ctx.textAlign = 'center';
|
|
69
68
|
ctx.fillText(msg, w / 2, h / 2);
|
|
70
69
|
ctx.textAlign = 'left';
|
|
@@ -83,8 +82,8 @@ export function markStale(canvas, msg = 'no fresh samples') {
|
|
|
83
82
|
const tw = ctx.measureText(label).width;
|
|
84
83
|
const pw = tw + 20;
|
|
85
84
|
const x = Math.max(4, w - pw - 6);
|
|
86
|
-
ctx.fillStyle =
|
|
87
|
-
ctx.strokeStyle =
|
|
85
|
+
ctx.fillStyle = INK.panel;
|
|
86
|
+
ctx.strokeStyle = INK.panelLine;
|
|
88
87
|
ctx.lineWidth = 1;
|
|
89
88
|
ctx.beginPath();
|
|
90
89
|
ctx.roundRect(x, 4, pw, 16, 5);
|
|
@@ -94,7 +93,7 @@ export function markStale(canvas, msg = 'no fresh samples') {
|
|
|
94
93
|
ctx.beginPath();
|
|
95
94
|
ctx.arc(x + 9, 12, 3, 0, 7);
|
|
96
95
|
ctx.fill();
|
|
97
|
-
ctx.fillStyle =
|
|
96
|
+
ctx.fillStyle = INK.label;
|
|
98
97
|
ctx.textAlign = 'left';
|
|
99
98
|
ctx.fillText(label, x + 15, 12);
|
|
100
99
|
}
|
|
@@ -205,7 +204,8 @@ export function lineChart(canvas, series, opts = {}) {
|
|
|
205
204
|
|
|
206
205
|
for (let si = 0; si < pts.length; si++) {
|
|
207
206
|
const s = pts[si];
|
|
208
|
-
const
|
|
207
|
+
const pal = palette();
|
|
208
|
+
const color = s.color ?? pal[si % pal.length];
|
|
209
209
|
const yOf = s.axis === 'right' ? Y2 : (opts.logY ? (v) => logY(v, lo, hi, L, plotH) : Y);
|
|
210
210
|
const sorted = s.points.slice().sort((a, b) => a.t - b.t);
|
|
211
211
|
|
|
@@ -318,15 +318,15 @@ function attachLineTip(canvas, opts) {
|
|
|
318
318
|
if (!rows.length) return hideTip(canvas);
|
|
319
319
|
redraw(canvas);
|
|
320
320
|
const { ctx } = ctxOf(canvas);
|
|
321
|
-
ctx.strokeStyle =
|
|
321
|
+
ctx.strokeStyle = INK.faintLine;
|
|
322
322
|
ctx.beginPath(); ctx.moveTo(nearX, c.L.top); ctx.lineTo(nearX, c.L.top + c.plotH); ctx.stroke();
|
|
323
323
|
const fmt = opts.fmtTip ?? ((v, s) => short(v));
|
|
324
324
|
const boxW = 118;
|
|
325
325
|
const boxH = 14 + rows.length * 12;
|
|
326
326
|
const bx = Math.min(c.w - boxW - 4, nearX + 8);
|
|
327
327
|
const by = c.L.top + 4;
|
|
328
|
-
ctx.fillStyle =
|
|
329
|
-
ctx.strokeStyle =
|
|
328
|
+
ctx.fillStyle = INK.tip;
|
|
329
|
+
ctx.strokeStyle = INK.tipLine;
|
|
330
330
|
ctx.beginPath(); ctx.roundRect(bx, by, boxW, boxH, 5); ctx.fill(); ctx.stroke();
|
|
331
331
|
ctx.textAlign = 'left';
|
|
332
332
|
ctx.fillStyle = COL.text;
|
|
@@ -335,11 +335,11 @@ function attachLineTip(canvas, opts) {
|
|
|
335
335
|
const y = by + 20 + i * 12;
|
|
336
336
|
ctx.fillStyle = row.color;
|
|
337
337
|
ctx.fillRect(bx + 6, y - 3, 6, 2);
|
|
338
|
-
ctx.fillStyle =
|
|
338
|
+
ctx.fillStyle = INK.tipText;
|
|
339
339
|
const label = (row.label || '').slice(0, 12);
|
|
340
340
|
ctx.fillText(label, bx + 16, y - 2);
|
|
341
341
|
ctx.textAlign = 'right';
|
|
342
|
-
ctx.fillStyle =
|
|
342
|
+
ctx.fillStyle = INK.bright;
|
|
343
343
|
ctx.fillText(fmt(row.v, row), bx + boxW - 6, y - 2);
|
|
344
344
|
ctx.textAlign = 'left';
|
|
345
345
|
});
|
|
@@ -512,7 +512,7 @@ export function meter(canvas, { value, max, label, danger = 0.9, fmt }) {
|
|
|
512
512
|
const a1 = Math.PI * 2;
|
|
513
513
|
ctx.lineWidth = Math.max(7, r * 0.34);
|
|
514
514
|
ctx.lineCap = 'butt';
|
|
515
|
-
ctx.strokeStyle =
|
|
515
|
+
ctx.strokeStyle = INK.panel;
|
|
516
516
|
ctx.beginPath(); ctx.arc(cx, cy, r, a0, a1); ctx.stroke();
|
|
517
517
|
// the region past which the node starts feerate-evicting
|
|
518
518
|
ctx.strokeStyle = hexA(COL.bad, 0.5);
|
|
@@ -520,7 +520,7 @@ export function meter(canvas, { value, max, label, danger = 0.9, fmt }) {
|
|
|
520
520
|
const col = pct >= danger ? COL.bad : pct > 0.75 ? COL.warn : COL.ok;
|
|
521
521
|
ctx.strokeStyle = col;
|
|
522
522
|
ctx.beginPath(); ctx.arc(cx, cy, r, a0, a0 + Math.PI * Math.min(1, pct)); ctx.stroke();
|
|
523
|
-
ctx.fillStyle =
|
|
523
|
+
ctx.fillStyle = INK.bright;
|
|
524
524
|
ctx.textAlign = 'center';
|
|
525
525
|
ctx.font = '600 17px ui-monospace, monospace';
|
|
526
526
|
ctx.fillText(`${((value / max) * 100).toFixed(1)}%`, cx, cy - 4);
|
|
@@ -556,12 +556,12 @@ export function stackedBars(canvas, rows, opts = {}) {
|
|
|
556
556
|
ctx.fillStyle = COL.text;
|
|
557
557
|
ctx.textAlign = 'right';
|
|
558
558
|
ctx.fillText(String(r.label ?? '').slice(0, 20), L.left - 6, y + rowH / 2);
|
|
559
|
-
ctx.fillStyle =
|
|
559
|
+
ctx.fillStyle = INK.panel;
|
|
560
560
|
ctx.fillRect(L.left, y + 2, plotW, rowH - 5);
|
|
561
561
|
const bw = (r.value / max) * plotW;
|
|
562
562
|
ctx.fillStyle = r.color ?? COL.accent;
|
|
563
563
|
ctx.fillRect(L.left, y + 2, Math.max(1, bw), rowH - 5);
|
|
564
|
-
ctx.fillStyle =
|
|
564
|
+
ctx.fillStyle = INK.bright;
|
|
565
565
|
ctx.textAlign = 'left';
|
|
566
566
|
ctx.fillText(String(r.display ?? short(r.value)), L.left + Math.min(bw + 5, plotW - 40), y + rowH / 2);
|
|
567
567
|
});
|
|
@@ -632,4 +632,4 @@ function hexA(hex, a) {
|
|
|
632
632
|
}
|
|
633
633
|
function clampN(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
|
|
634
634
|
|
|
635
|
-
export { COL,
|
|
635
|
+
export { COL, palette };
|
package/public/js/depthchart.js
CHANGED
|
@@ -10,15 +10,15 @@
|
|
|
10
10
|
// gives 500 levels, OKX 5000 -- a percent or two either side) is not extended: its line ends,
|
|
11
11
|
// the end is marked, and the total past it counts only the books that reach.
|
|
12
12
|
import { niceTicks } from './charts.js';
|
|
13
|
+
import { INK } from './theme.js'; // the axis, grid and tooltip colours of the chosen theme
|
|
13
14
|
import { prep } from './pricechart.js';
|
|
14
15
|
|
|
15
16
|
export const AGOS = [[60, '1m'], [300, '5m'], [600, '10m'], [1800, '30m'], [3600, '1h']];
|
|
16
17
|
export const ZOOMS = [[0.01, '±1%'], [0.025, '±2.5%'], [0.05, '±5%'], [0.1, '±10%']];
|
|
17
18
|
export const DEPTH_MS = 30_000;
|
|
18
19
|
const D = { data: null, at: 0, busy: false, key: null, ago: 600, zoom: 0.05, hover: null, bound: false, error: null };
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const BID_THEN = 'rgba(61,255,122,0.55)', ASK_THEN = 'rgba(255,69,69,0.55)';
|
|
20
|
+
// the two sides' colours come from the theme's ink (theme.js, 2026-09-16): neon green and red on a
|
|
21
|
+
// dark ground, the theme's own good and bad on a light one, where neon has no contrast
|
|
22
22
|
const PAD_FULL = { top: 28, right: 62, bottom: 22, left: 56 };
|
|
23
23
|
// COMPACT, for the kiosk. The full chart spends 62 px on the right purely to label the change
|
|
24
24
|
// bars' axis and 56 on the left for the cumulative-BTC labels plus a rotated caption -- on a wall
|
|
@@ -90,13 +90,13 @@ export function depthSums(ser, pct) {
|
|
|
90
90
|
return { bids: ser.bid[ib] ?? null, asks: ser.ask[ia] ?? null, atLeast: Boolean(ser.bidPart?.[ib] || ser.askPart?.[ia]) };
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
export function drawDepth(canvas, ser, { zoom = 0.05, hover = null, compact = false } = {}) {
|
|
93
|
+
export function drawDepth(canvas, ser, { zoom = 0.05, hover = null, compact = false, empty = null } = {}) {
|
|
94
94
|
if (!canvas?.getContext) return null;
|
|
95
95
|
const PAD = compact ? PAD_COMPACT : PAD_FULL;
|
|
96
96
|
const { ctx, w, h } = prep(canvas);
|
|
97
97
|
if (!ser) {
|
|
98
|
-
ctx.fillStyle =
|
|
99
|
-
ctx.fillText('asking the exchanges for their order books…', w / 2, h / 2);
|
|
98
|
+
ctx.fillStyle = INK.text; ctx.textAlign = 'center';
|
|
99
|
+
ctx.fillText(empty ?? 'asking the exchanges for their order books…', w / 2, h / 2);
|
|
100
100
|
return null;
|
|
101
101
|
}
|
|
102
102
|
const plotW = Math.max(10, w - PAD.left - PAD.right), plotH = Math.max(40, h - PAD.top - PAD.bottom);
|
|
@@ -120,13 +120,13 @@ export function drawDepth(canvas, ser, { zoom = 0.05, hover = null, compact = fa
|
|
|
120
120
|
for (const v of niceTicks(0, vmax, Math.max(3, Math.floor(plotH / 44)))) {
|
|
121
121
|
const y = Math.round(Y(v)) + 0.5;
|
|
122
122
|
if (y < PAD.top) continue;
|
|
123
|
-
ctx.strokeStyle =
|
|
124
|
-
ctx.fillStyle =
|
|
123
|
+
ctx.strokeStyle = INK.grid; line(PAD.left, y, w - PAD.right, y);
|
|
124
|
+
ctx.fillStyle = INK.axisText; ctx.fillText(btc(v), PAD.left - 6, y);
|
|
125
125
|
}
|
|
126
126
|
if (!compact) {
|
|
127
127
|
ctx.save?.();
|
|
128
128
|
ctx.translate(12, PAD.top + plotH / 2); ctx.rotate(-Math.PI / 2);
|
|
129
|
-
ctx.textAlign = 'center'; ctx.fillStyle =
|
|
129
|
+
ctx.textAlign = 'center'; ctx.fillStyle = INK.textDim; ctx.fillText('BTC, cumulative', 0, 0);
|
|
130
130
|
ctx.restore?.();
|
|
131
131
|
}
|
|
132
132
|
ctx.setTransform(Math.min(globalThis.devicePixelRatio || 1, 2), 0, 0, Math.min(globalThis.devicePixelRatio || 1, 2), 0, 0);
|
|
@@ -137,10 +137,10 @@ export function drawDepth(canvas, ser, { zoom = 0.05, hover = null, compact = fa
|
|
|
137
137
|
const v = e === 0 ? 0 : Math.sign(e) * 10 ** Math.abs(e);
|
|
138
138
|
const y = Math.round(YC(v)) + 0.5;
|
|
139
139
|
if (!compact) {
|
|
140
|
-
ctx.fillStyle =
|
|
140
|
+
ctx.fillStyle = INK.axisText;
|
|
141
141
|
ctx.fillText(v === 0 ? '0' : `${v > 0 ? '' : '-'}${shortN(Math.abs(v))}`, w - PAD.right + 6, y);
|
|
142
142
|
}
|
|
143
|
-
if (v === 0) { ctx.strokeStyle =
|
|
143
|
+
if (v === 0) { ctx.strokeStyle = INK.axis; line(PAD.left, y, w - PAD.right, y); }
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
// price axis
|
|
@@ -148,10 +148,10 @@ export function drawDepth(canvas, ser, { zoom = 0.05, hover = null, compact = fa
|
|
|
148
148
|
for (const p of niceTicks(pmin, pmax, Math.max(3, Math.floor(plotW / 90)))) {
|
|
149
149
|
const x = Math.round(X(p)) + 0.5;
|
|
150
150
|
if (x < PAD.left || x > w - PAD.right) continue;
|
|
151
|
-
ctx.strokeStyle =
|
|
152
|
-
ctx.fillStyle =
|
|
151
|
+
ctx.strokeStyle = INK.grid; line(x, PAD.top, x, PAD.top + plotH);
|
|
152
|
+
ctx.fillStyle = INK.axisText; ctx.fillText(zoom <= 0.01 ? money(p) : kUsd(p), x, h - PAD.bottom / 2);
|
|
153
153
|
}
|
|
154
|
-
ctx.strokeStyle =
|
|
154
|
+
ctx.strokeStyle = INK.axis;
|
|
155
155
|
line(PAD.left + 0.5, PAD.top, PAD.left + 0.5, PAD.top + plotH);
|
|
156
156
|
line(w - PAD.right + 0.5, PAD.top, w - PAD.right + 0.5, PAD.top + plotH);
|
|
157
157
|
line(PAD.left, PAD.top + plotH + 0.5, w - PAD.right, PAD.top + plotH + 0.5);
|
|
@@ -180,15 +180,15 @@ export function drawDepth(canvas, ser, { zoom = 0.05, hover = null, compact = fa
|
|
|
180
180
|
ctx.stroke();
|
|
181
181
|
ctx.setLineDash([]); ctx.lineWidth = 1;
|
|
182
182
|
};
|
|
183
|
-
for (const e of ser.exchanges) { curve(e.bids,
|
|
184
|
-
if (ser.change) { curve(ser.thenBid,
|
|
183
|
+
for (const e of ser.exchanges) { curve(e.bids, INK.bidT, 1); curve(e.asks, INK.askT, 1); }
|
|
184
|
+
if (ser.change) { curve(ser.thenBid, INK.bidThen, 1.4, [5, 4]); curve(ser.thenAsk, INK.askThen, 1.4, [5, 4]); }
|
|
185
185
|
const only = (arr, part, want) => arr.map((v, i) => (v != null && (part[i] === want || (i > 0 && part[i - 1] === want) || (i < arr.length - 1 && part[i + 1] === want)) ? v : null));
|
|
186
|
-
curve(only(ser.bid, ser.bidPart, true),
|
|
187
|
-
curve(only(ser.ask, ser.askPart, true),
|
|
188
|
-
curve(only(ser.bid, ser.bidPart, false),
|
|
189
|
-
curve(only(ser.ask, ser.askPart, false),
|
|
186
|
+
curve(only(ser.bid, ser.bidPart, true), INK.bidHalf, 2.2, [2, 3]);
|
|
187
|
+
curve(only(ser.ask, ser.askPart, true), INK.askHalf, 2.2, [2, 3]);
|
|
188
|
+
curve(only(ser.bid, ser.bidPart, false), INK.bid, 2.2);
|
|
189
|
+
curve(only(ser.ask, ser.askPart, false), INK.ask, 2.2);
|
|
190
190
|
// the mid, and where each shallow book ends
|
|
191
|
-
ctx.strokeStyle =
|
|
191
|
+
ctx.strokeStyle = INK.dash;
|
|
192
192
|
const xm = Math.round(X(ser.mid)) + 0.5;
|
|
193
193
|
line(xm, PAD.top, xm, PAD.top + plotH);
|
|
194
194
|
ctx.font = '10px ui-monospace, SFMono-Regular, Menlo, monospace';
|
|
@@ -197,9 +197,9 @@ export function drawDepth(canvas, ser, { zoom = 0.05, hover = null, compact = fa
|
|
|
197
197
|
for (const p of [r.low, r.high]) {
|
|
198
198
|
if (p == null || p < pmin || p > pmax) continue;
|
|
199
199
|
const x = Math.round(X(p)) + 0.5;
|
|
200
|
-
ctx.strokeStyle =
|
|
200
|
+
ctx.strokeStyle = INK.dash; line(x, PAD.top + plotH - 8, x, PAD.top + plotH);
|
|
201
201
|
if (!compact) {
|
|
202
|
-
ctx.fillStyle =
|
|
202
|
+
ctx.fillStyle = INK.label; ctx.textAlign = 'center';
|
|
203
203
|
ctx.fillText(`${r.name} ends`, x, PAD.top + plotH - 14 - 11 * (lane++ % 3));
|
|
204
204
|
}
|
|
205
205
|
}
|
|
@@ -207,14 +207,14 @@ export function drawDepth(canvas, ser, { zoom = 0.05, hover = null, compact = fa
|
|
|
207
207
|
ctx.font = '11px ui-monospace, SFMono-Regular, Menlo, monospace';
|
|
208
208
|
|
|
209
209
|
// readout: the pointer's price, or the sums near the mid
|
|
210
|
-
ctx.textAlign = 'left'; ctx.fillStyle =
|
|
210
|
+
ctx.textAlign = 'left'; ctx.fillStyle = INK.bright;
|
|
211
211
|
if (hover && hover.x >= PAD.left && hover.x <= w - PAD.right) {
|
|
212
212
|
const p = pmin + ((hover.x - PAD.left) / plotW) * (pmax - pmin);
|
|
213
213
|
const i = Math.max(0, Math.min(ser.prices.length - 1, Math.round((p - ser.prices[0]) / ser.step)));
|
|
214
214
|
const bidSide = ser.prices[i] <= ser.mid;
|
|
215
215
|
const cum = bidSide ? ser.bid[i] : ser.ask[i];
|
|
216
216
|
const per = ser.exchanges.map((e) => [e.name, bidSide ? e.bids[i] : e.asks?.[i]]).filter(([, v]) => v != null).map(([nm, v]) => `${nm} ${btc(v)}`).join(' · ');
|
|
217
|
-
ctx.strokeStyle =
|
|
217
|
+
ctx.strokeStyle = INK.dash; ctx.setLineDash([3, 3]);
|
|
218
218
|
const x = Math.round(X(ser.prices[i])) + 0.5;
|
|
219
219
|
line(x, PAD.top, x, PAD.top + plotH);
|
|
220
220
|
ctx.setLineDash([]);
|
|
@@ -237,8 +237,12 @@ export function depthBarHtml(ago = D.ago, zoom = D.zoom) {
|
|
|
237
237
|
<div class="mkgrp"><span class="mklab">price window</span>${ZOOMS.map(([z, l]) => b('data-zoom', z, l, z === zoom)).join('')}</div>`;
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
// what the empty chart says while the feed is off: the switch, or the server's hard off
|
|
241
|
+
const offText = (d) => (d?.enabled === false ? (d.polling === false ? 'market polling is off — Display settings → Markets & Price → Enable market polling' : 'market data is off on this server') : null);
|
|
242
|
+
|
|
240
243
|
export function depthNote(d, fmt) {
|
|
241
244
|
if (!d) return 'asking the exchanges for their order books…';
|
|
245
|
+
if (d.enabled === false) return d.note ?? 'market data is off on this monitor';
|
|
242
246
|
if (d.warming) return 'reading the order books for the first time…';
|
|
243
247
|
const agoL = AGOS.find(([s]) => s === d.ago)?.[1] ?? `${d.ago} s`;
|
|
244
248
|
const parts = [`books read at ${clock(d.at)} UTC, every ${Math.round((d.bookMs ?? DEPTH_MS) / 1000)} s while this tab is open`];
|
|
@@ -252,7 +256,7 @@ export function depthNote(d, fmt) {
|
|
|
252
256
|
function draw() {
|
|
253
257
|
const canvas = document.getElementById('mkDepth');
|
|
254
258
|
if (!canvas) return;
|
|
255
|
-
drawDepth(canvas, depthSeries(D.data), { zoom: D.zoom, hover: D.hover });
|
|
259
|
+
drawDepth(canvas, depthSeries(D.data), { zoom: D.zoom, hover: D.hover, empty: offText(D.data) });
|
|
256
260
|
}
|
|
257
261
|
|
|
258
262
|
/**
|
|
@@ -282,7 +286,7 @@ export function renderDepthInto(canvasId, h, { zoom = 0.025, compact = true } =
|
|
|
282
286
|
ensureDepth(h);
|
|
283
287
|
const canvas = document.getElementById(canvasId);
|
|
284
288
|
if (!canvas) return null;
|
|
285
|
-
return drawDepth(canvas, depthSeries(D.data), { zoom, compact });
|
|
289
|
+
return drawDepth(canvas, depthSeries(D.data), { zoom, compact, empty: offText(D.data) });
|
|
286
290
|
}
|
|
287
291
|
|
|
288
292
|
export function renderDepth(h) {
|