blockyard 0.0.9 → 0.1.1
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 +359 -1
- package/README.md +48 -27
- package/SECURITY.md +2 -2
- package/bin/blockyard.js +2 -1
- package/docs/API.md +17 -15
- package/docs/ARCHITECTURE.md +128 -10
- package/docs/CONFIGURATION.md +39 -30
- package/docs/DEFECTS.md +4 -1
- package/docs/GETTING-STARTED.md +18 -8
- package/docs/INSTALL.md +97 -37
- package/docs/MEASUREMENTS.md +147 -0
- package/docs/PLAN-SCORCHED-YARD.md +456 -0
- package/docs/PLAN-SKIES.md +142 -0
- package/docs/SECURITY-AUDIT-2026-09-16.md +647 -0
- package/docs/SECURITY.md +58 -22
- package/docs/TROUBLESHOOTING.md +44 -5
- package/docs/USER-GUIDE.md +505 -35
- package/package.json +4 -2
- package/public/404.html +1 -1
- package/public/css/app.css +393 -82
- package/public/donate-qr.png +0 -0
- package/public/index.html +353 -109
- package/public/js/agents.js +228 -51
- package/public/js/app.js +131 -16
- package/public/js/blockanoid.js +15 -7
- package/public/js/blockout.js +15 -7
- package/public/js/blockscene3d.js +230 -38
- package/public/js/charts.js +21 -21
- package/public/js/depthchart.js +31 -27
- package/public/js/details3d.js +1481 -73
- 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/explorer.js +7 -1
- package/public/js/livingsky.js +494 -0
- package/public/js/login.js +8 -2
- package/public/js/markets.js +46 -8
- package/public/js/mining.js +314 -36
- package/public/js/panels.js +41 -28
- package/public/js/pricechart.js +14 -13
- package/public/js/quake.js +20 -0
- package/public/js/safenext.js +14 -0
- package/public/js/scorched.js +1051 -0
- package/public/js/scorchedai.js +227 -0
- package/public/js/scorchedair.js +286 -0
- package/public/js/scorchedfx.js +376 -0
- package/public/js/scorchedshop.js +105 -0
- package/public/js/scorchedwind.js +69 -0
- package/public/js/scorchedyard.js +1338 -0
- package/public/js/settings.js +368 -100
- package/public/js/soundcard.js +459 -0
- package/public/js/tetrust.js +15 -6
- package/public/js/tetsound.js +35 -5
- package/public/js/theme.js +235 -0
- package/public/js/wolf3d.js +22 -0
- package/public/js/x86.js +1978 -0
- package/scripts/check.js +46 -0
- package/scripts/donate-qr.py +12 -9
- package/scripts/dos-bench.js +56 -0
- package/scripts/index-build.js +9 -2
- package/scripts/pool-map.js +152 -36
- package/scripts/setup.js +142 -22
- package/scripts/shots.mjs +27 -0
- package/scripts/smoke.sh +7 -6
- package/scripts/tls.js +31 -0
- package/scripts/ui.js +4 -2
- package/server/auth/sessions.js +33 -13
- package/server/chain/blockfile.js +64 -5
- package/server/chain/index/build.js +451 -58
- package/server/chain/index/heights.js +29 -3
- package/server/chain/index/live.js +13 -7
- package/server/chain/index/rows.js +6 -1
- package/server/chain/index/store.js +28 -5
- package/server/chain/index/worker.js +23 -11
- package/server/collect/logparse.js +65 -18
- package/server/collect/markets.js +76 -7
- package/server/collect/mining.js +32 -0
- package/server/collect/monitor.js +54 -12
- package/server/collect/network.js +305 -0
- package/server/config.js +53 -22
- package/server/http/api.js +119 -18
- package/server/http/games.js +77 -0
- package/server/http/server.js +30 -5
- package/server/http/sse.js +53 -7
- package/server/main.js +66 -11
- package/server/rpc/allowlist.js +26 -0
- package/server/rpc/client.js +30 -2
- package/server/store/audit.js +6 -1
- package/server/store/history.js +19 -3
- package/server/store/ledger.js +15 -4
- package/server/tls/selfsigned.js +160 -0
- package/systemd/blockyard.service +41 -8
- package/docs/PRIVATE-LEADERBOARD.md +0 -230
- package/docs/STATE-2026-09-09.md +0 -200
package/public/js/blockanoid.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
newGame, advance, step, movePaddle, nudge, launch, fire, tiles, remaining, powers, setOptions,
|
|
11
11
|
COLS, ROWS, LIVES, CAPSULE_LETTER, layoutFor,
|
|
12
12
|
} from './arkanoid.js';
|
|
13
|
-
import { loadSettings, setSetting, blockanoidOptions } from './settings.js';
|
|
13
|
+
import { loadSettings, setSetting, blockanoidOptions, nextSky, SKY_LABELS } from './settings.js';
|
|
14
14
|
import * as sound from './tetsound.js';
|
|
15
15
|
|
|
16
16
|
const SCORES_KEY = 'blockyard.blockanoid.scores';
|
|
@@ -130,9 +130,20 @@ function drawScores(highlightAt = null) {
|
|
|
130
130
|
if (t.innerHTML !== html) t.innerHTML = html;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
const SWITCHES = [['
|
|
133
|
+
const SWITCHES = [['baNeon', 'neon'], ['baSfx', 'sfx']];
|
|
134
|
+
/** The sky button says which sky it is on, and lights up while there is one. */
|
|
135
|
+
function skyButton(id, sky) {
|
|
136
|
+
const b = el(id);
|
|
137
|
+
if (!b) return;
|
|
138
|
+
b.textContent = `\u2600 ${SKY_LABELS[sky] ?? sky}`;
|
|
139
|
+
b.classList.toggle('on', sky !== 'none');
|
|
140
|
+
b.setAttribute('aria-pressed', sky !== 'none' ? 'true' : 'false');
|
|
141
|
+
}
|
|
134
142
|
function drawSwitches() {
|
|
135
143
|
const b = blockanoidOptions(loadSettings());
|
|
144
|
+
// ONE SKY BUTTON (docs/PLAN-SKIES.md): Galaxy, Earth or none, round the ring, in place of the old
|
|
145
|
+
// star field and galaxy pair -- the same setting the Sky tab's table shows for this board
|
|
146
|
+
skyButton('baSkySw', b.sky.sky);
|
|
136
147
|
for (const [id, key] of SWITCHES) {
|
|
137
148
|
const btn = el(id);
|
|
138
149
|
if (!btn) continue;
|
|
@@ -146,7 +157,6 @@ function flip(key) {
|
|
|
146
157
|
setSetting(loadSettings(), `blockanoid.${key}`, !cur);
|
|
147
158
|
sound.unlock();
|
|
148
159
|
drawSwitches();
|
|
149
|
-
if (key === 'stars' || key === 'galaxy') drawSky();
|
|
150
160
|
// capsules and minions change the RULES, so a flip must reach the game IN PLAY, not just the
|
|
151
161
|
// next one -- otherwise the switch does nothing until you lose, which reads as a broken control
|
|
152
162
|
if (G.game) { const b = blockanoidOptions(loadSettings()); setOptions(G.game, { capsules: b.capsules, enemies: b.enemies }); }
|
|
@@ -159,10 +169,7 @@ function drawSky() {
|
|
|
159
169
|
const b = blockanoidOptions(loadSettings());
|
|
160
170
|
board3d(sky, [], {
|
|
161
171
|
...SKY,
|
|
162
|
-
|
|
163
|
-
starDensity: b.starDensity, starBrightness: b.starBrightness,
|
|
164
|
-
nebulae: b.nebulae, galaxies: b.galaxies, dust: b.dust, clusters: b.clusters,
|
|
165
|
-
starColours: b.starColours, starGlints: b.starGlints,
|
|
172
|
+
...b.sky, // which sky this board chose, and what it is made of (settings.js skyFor)
|
|
166
173
|
});
|
|
167
174
|
}
|
|
168
175
|
|
|
@@ -327,6 +334,7 @@ function bind() {
|
|
|
327
334
|
});
|
|
328
335
|
court?.addEventListener('contextmenu', (e) => { if (G.running) e.preventDefault(); });
|
|
329
336
|
for (const [id, key] of SWITCHES) el(id)?.addEventListener('click', () => flip(key));
|
|
337
|
+
el('baSkySw')?.addEventListener('click', () => { const s = loadSettings(); setSetting(s, 'blockanoid.sky', nextSky(blockanoidOptions(s).sky.sky)); sound.unlock(); drawSwitches(); drawSky(); draw(); });
|
|
330
338
|
}
|
|
331
339
|
|
|
332
340
|
export function renderBlockanoid(s, state, h) {
|
package/public/js/blockout.js
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
newGame, advance, step, movePaddle, nudge, launch, tiles, remaining,
|
|
13
13
|
COLS, ROWS, LIVES,
|
|
14
14
|
} from './breakout.js';
|
|
15
|
-
import { loadSettings, setSetting, blockoutOptions } from './settings.js';
|
|
15
|
+
import { loadSettings, setSetting, blockoutOptions, nextSky, SKY_LABELS } from './settings.js';
|
|
16
16
|
import * as sound from './tetsound.js';
|
|
17
17
|
|
|
18
18
|
const SCORES_KEY = 'blockyard.blockout.scores';
|
|
@@ -145,9 +145,20 @@ function drawScores(highlightAt = null) {
|
|
|
145
145
|
if (t.innerHTML !== html) t.innerHTML = html;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
const SWITCHES = [['
|
|
148
|
+
const SWITCHES = [['boNeon', 'neon'], ['boSfx', 'sfx']];
|
|
149
|
+
/** The sky button says which sky it is on, and lights up while there is one. */
|
|
150
|
+
function skyButton(id, sky) {
|
|
151
|
+
const b = el(id);
|
|
152
|
+
if (!b) return;
|
|
153
|
+
b.textContent = `\u2600 ${SKY_LABELS[sky] ?? sky}`;
|
|
154
|
+
b.classList.toggle('on', sky !== 'none');
|
|
155
|
+
b.setAttribute('aria-pressed', sky !== 'none' ? 'true' : 'false');
|
|
156
|
+
}
|
|
149
157
|
function drawSwitches() {
|
|
150
158
|
const b = blockoutOptions(loadSettings());
|
|
159
|
+
// ONE SKY BUTTON (docs/PLAN-SKIES.md): Galaxy, Earth or none, round the ring, in place of the old
|
|
160
|
+
// star field and galaxy pair -- the same setting the Sky tab's table shows for this board
|
|
161
|
+
skyButton('boSkySw', b.sky.sky);
|
|
151
162
|
for (const [id, key] of SWITCHES) {
|
|
152
163
|
const btn = el(id);
|
|
153
164
|
if (!btn) continue;
|
|
@@ -161,7 +172,6 @@ function flip(key) {
|
|
|
161
172
|
setSetting(loadSettings(), `blockout.${key}`, !cur);
|
|
162
173
|
sound.unlock();
|
|
163
174
|
drawSwitches();
|
|
164
|
-
if (key === 'stars' || key === 'galaxy') drawSky();
|
|
165
175
|
G.dirty = true;
|
|
166
176
|
draw();
|
|
167
177
|
}
|
|
@@ -172,10 +182,7 @@ function drawSky() {
|
|
|
172
182
|
const b = blockoutOptions(loadSettings());
|
|
173
183
|
board3d(sky, [], {
|
|
174
184
|
...SKY,
|
|
175
|
-
|
|
176
|
-
starDensity: b.starDensity, starBrightness: b.starBrightness,
|
|
177
|
-
nebulae: b.nebulae, galaxies: b.galaxies, dust: b.dust, clusters: b.clusters,
|
|
178
|
-
starColours: b.starColours, starGlints: b.starGlints,
|
|
185
|
+
...b.sky, // which sky this board chose, and what it is made of (settings.js skyFor)
|
|
179
186
|
});
|
|
180
187
|
}
|
|
181
188
|
|
|
@@ -329,6 +336,7 @@ function bind() {
|
|
|
329
336
|
court?.addEventListener('pointermove', onPointer);
|
|
330
337
|
court?.addEventListener('pointerdown', (e) => { onPointer(e); serve(); });
|
|
331
338
|
for (const [id, key] of SWITCHES) el(id)?.addEventListener('click', () => flip(key));
|
|
339
|
+
el('boSkySw')?.addEventListener('click', () => { const s = loadSettings(); setSetting(s, 'blockout.sky', nextSky(blockoutOptions(s).sky.sky)); sound.unlock(); drawSwitches(); drawSky(); draw(); });
|
|
332
340
|
}
|
|
333
341
|
|
|
334
342
|
export function renderBlockout(s, state, h) {
|
|
@@ -852,21 +852,50 @@ const flipOf = (o) => (o.flipY === false ? -1 : 1);
|
|
|
852
852
|
// settings"). Each lamp is a direction over the board for the dome's slope shading (grid x,
|
|
853
853
|
// screen-up rows, up) and a direction across the screen for the side faces (x right, y down);
|
|
854
854
|
// `overhead` is straight above, so no slope is in shade and every side takes the same light.
|
|
855
|
+
// SIX PLACEMENTS AT THREE HEIGHTS (operator, 2026-09-16: "add more camera light angles ... Low
|
|
856
|
+
// Middle and High light placement locations, top left, top right, bottom left, bottom right,
|
|
857
|
+
// viewer, direct overhead"). A placement is a direction across the board (x right, y screen-up);
|
|
858
|
+
// the height is the lamp's elevation over it: low 25°, middle 45°, high 65°. `overhead` ignores
|
|
859
|
+
// the height. The old names (upper-left, upper-right) still answer, as top-left and top-right at
|
|
860
|
+
// the middle height; `front` is the lamp at the viewer. (Not 'viewer': `light: 'viewer'` is the
|
|
861
|
+
// Markets board's own flat, viewer-lit finish -- viewerLit below -- and a placement of that name
|
|
862
|
+
// turned the whole block board flat-shaded, which the operator saw at once on 2026-09-16.)
|
|
863
|
+
export const LIGHT_PLACES = Object.freeze({
|
|
864
|
+
'overhead': [0, 0], 'top-left': [-1, 1], 'top-right': [1, 1], 'bottom-left': [-1, -1], 'bottom-right': [1, -1], 'front': [0, -1],
|
|
865
|
+
});
|
|
866
|
+
export const LIGHT_HEIGHTS = Object.freeze({ low: 25, middle: 45, high: 65 });
|
|
867
|
+
const LIGHT_ALIASES = Object.freeze({ 'upper-left': 'top-left', 'upper-right': 'top-right' });
|
|
868
|
+
export function lampFor(place, height = 'middle') {
|
|
869
|
+
const p = LIGHT_PLACES[place] ?? LIGHT_PLACES['top-left'];
|
|
870
|
+
if (!p[0] && !p[1]) return { L: [0, 0, 1], side: [0, 0] };
|
|
871
|
+
const el = ((LIGHT_HEIGHTS[height] ?? 45) * Math.PI) / 180;
|
|
872
|
+
const n = Math.hypot(p[0], p[1]) || 1;
|
|
873
|
+
const ax = p[0] / n, ay = p[1] / n;
|
|
874
|
+
// the side faces take the lamp's direction across the screen (y down), weaker the higher it hangs
|
|
875
|
+
const k = 0.9 * Math.cos(el) / Math.cos((45 * Math.PI) / 180);
|
|
876
|
+
return { L: [ax * Math.cos(el), ay * Math.cos(el), Math.sin(el)], side: [ax * k, -ay * k] };
|
|
877
|
+
}
|
|
855
878
|
export const LIGHTS = Object.freeze({
|
|
856
|
-
'overhead':
|
|
857
|
-
'
|
|
858
|
-
'upper-
|
|
859
|
-
'front': { L: [0, -0.55, 0.63], side: [0, 0.9] },
|
|
879
|
+
'overhead': lampFor('overhead'),
|
|
880
|
+
'top-left': lampFor('top-left'), 'top-right': lampFor('top-right'), 'bottom-left': lampFor('bottom-left'), 'bottom-right': lampFor('bottom-right'), 'front': lampFor('front'),
|
|
881
|
+
'upper-left': lampFor('top-left'), 'upper-right': lampFor('top-right'),
|
|
860
882
|
});
|
|
861
|
-
export const LIGHT_DEFAULT = '
|
|
883
|
+
export const LIGHT_DEFAULT = 'top-left';
|
|
884
|
+
// the middle of the shipped brightness range: `lightGain` stretches the faces about this point
|
|
885
|
+
const LIT_MID = 0.58;
|
|
862
886
|
const NEON_HEX = /^#[0-9a-f]{6}$/i;
|
|
863
887
|
export function lightOf(o = {}) {
|
|
864
|
-
|
|
888
|
+
const name = LIGHT_ALIASES[o.light] ?? o.light;
|
|
889
|
+
return LIGHT_PLACES[name] ? name : o.overheadLight === true ? 'overhead' : LIGHT_DEFAULT;
|
|
890
|
+
}
|
|
891
|
+
/** The lamp a board asks for: its placement at its height. */
|
|
892
|
+
export function lampOf(o = {}) {
|
|
893
|
+
return lampFor(lightOf(o), LIGHT_HEIGHTS[o.lightHeight] ? o.lightHeight : 'middle');
|
|
865
894
|
}
|
|
866
895
|
export function domeLight(t, o = {}) {
|
|
867
896
|
const { dome = 0, gridW = 0, gridH = 0 } = o;
|
|
868
897
|
if (!dome || !gridW || !gridH) return 1;
|
|
869
|
-
const lamp =
|
|
898
|
+
const lamp = lampOf(o);
|
|
870
899
|
// straight above: the slope shades nothing -- the bottom rows, which lean away from a corner
|
|
871
900
|
// lamp and sat at the 0.6 floor, read as bright as the middle
|
|
872
901
|
if (lamp.L[0] === 0 && lamp.L[1] === 0) return 1;
|
|
@@ -912,8 +941,20 @@ export const FX_NONE = Object.freeze({ glow: 0, outline: 0, lift: 0, color: null
|
|
|
912
941
|
// just outside one side of the board and leaving past the other
|
|
913
942
|
export function fxFront(fx) {
|
|
914
943
|
const qs = [0, fx.gridW * fx.dx, fx.gridH * fx.dy, fx.gridW * fx.dx + fx.gridH * fx.dy];
|
|
915
|
-
|
|
916
|
-
|
|
944
|
+
// THE MARGIN IS THE PANEL'S, NOT A CONSTANT (operator, 2026-09-15: "It needs to cleanly travel on
|
|
945
|
+
// and off screen"). Four units clears the BOARD, and the panel reaches much further than that --
|
|
946
|
+
// measured 21.8 units beyond a 96-wide board -- so a front that started four units out began its
|
|
947
|
+
// sweep in plain view and ended it there. render3d measures the real overhang from the viewRect
|
|
948
|
+
// and puts it on `fx.margin`; four is the fallback for a caller with no panel (tests).
|
|
949
|
+
const m = fx.margin ?? 4;
|
|
950
|
+
const lo = Math.min(...qs) - m, hi = Math.max(...qs) + m;
|
|
951
|
+
// A SEARCHLIGHT PANS (operator, 2026-09-15: "the scanner needs to be more like a search light,
|
|
952
|
+
// slowly panning back and forth"). A single linear pass is a scanner; sweeping out and back is a
|
|
953
|
+
// light being AIMED, which is what reads as something looking for things. `pan` is set for the
|
|
954
|
+
// top-down board only -- on the price chart the camera is low and one pass along the hours is
|
|
955
|
+
// the right motion.
|
|
956
|
+
const t = fx.pan ? 1 - Math.abs(((fx.u * 2) % 2) - 1) : fx.u;
|
|
957
|
+
return lo + t * (hi - lo);
|
|
917
958
|
}
|
|
918
959
|
|
|
919
960
|
// a deterministic 0..1 from an integer: where a firework bursts, which cube flares. Hashed, not
|
|
@@ -957,8 +998,26 @@ export function fxAt(t, fx) {
|
|
|
957
998
|
return { glow: A * w, outline: 0.8 * A * w, lift: 0, color: [255, 255, 255] };
|
|
958
999
|
}
|
|
959
1000
|
case 'scan': {
|
|
960
|
-
|
|
961
|
-
|
|
1001
|
+
// the beam's own width -- ten units, so a whole swath of blocks is lit as it passes
|
|
1002
|
+
// (2026-09-15: "Reach more blocks with the scanner effect")
|
|
1003
|
+
// THE LIGHT IS THE POINT (operator, 2026-09-15: "We really need to make it much more obvious
|
|
1004
|
+
// somehow that the blocks are being illuminated"). The swath tracks the beam's own half-width
|
|
1005
|
+
// (fx.beamR, set with the cone) so light and geometry cannot disagree, and it hits harder:
|
|
1006
|
+
// a near-white glow, a bright outline and real lift, so a struck cube stands up out of the
|
|
1007
|
+
// board rather than merely brightening.
|
|
1008
|
+
const w = g((along() - fxFront(fx)) / (fx.beamR ?? 5.5));
|
|
1009
|
+
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] };
|
|
1010
|
+
}
|
|
1011
|
+
case 'xray': {
|
|
1012
|
+
// X-RAY (2026-09-15, operator: "Add xray as additional choosable effect"): a front sweeps the
|
|
1013
|
+
// board and behind it every tile goes x-ray -- its body dims to glass and its edges and a
|
|
1014
|
+
// raster across its faces light up (buildScene's xray treatment) -- then develops back to
|
|
1015
|
+
// solid over a few units, the front itself a bright edge
|
|
1016
|
+
const d = fxFront(fx) - along(); // > 0 once the front has passed
|
|
1017
|
+
const edge = g(d / 1.2);
|
|
1018
|
+
const back = d > 0 && d < 9 ? (d < 5 ? 1 : 1 - (d - 5) / 4) : 0;
|
|
1019
|
+
const xray = Math.max(edge, back) * A;
|
|
1020
|
+
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
1021
|
}
|
|
963
1022
|
case 'ball': {
|
|
964
1023
|
// "illuminating everything it comes near": brightest right under the ball, gone ~5 units off
|
|
@@ -997,30 +1056,41 @@ export function fxAt(t, fx) {
|
|
|
997
1056
|
return { glow: hot * A * w, outline: 0.8 * hot * A * w, lift: (inward ? 0.4 : 3.2) * A * w,
|
|
998
1057
|
color: inward ? [150, 190, 255] : [255, 255, 245] };
|
|
999
1058
|
}
|
|
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
|
-
}
|
|
1059
|
+
// 'firework' lights through fxNow's heads now (details3d fireworkShells): one head per live
|
|
1060
|
+
// shell, in the shell's colour -- see the heads branch below
|
|
1017
1061
|
case 'flare': {
|
|
1018
1062
|
// one cube goes supernova and lights its neighbourhood -- the same hashed cube every replay
|
|
1019
1063
|
const fxp = fxHash(fx.seed + 7) * fx.gridW, fyp = fxHash(fx.seed + 8) * fx.gridH;
|
|
1020
1064
|
const bell = Math.sin(Math.PI * fx.u);
|
|
1021
1065
|
const d = Math.hypot(cx - fxp, cy - fyp);
|
|
1022
|
-
|
|
1023
|
-
|
|
1066
|
+
// A SUPERNOVA'S LIGHT (2026-09-15, drawn in details3d drawSupernova): on a thin board -- the
|
|
1067
|
+
// price board, eight deep -- the light reaches three times as far, so the chart is lit, and
|
|
1068
|
+
// it cools with the remnant, gold to red to violet, after the white of the blast
|
|
1069
|
+
const thin = fx.gridH <= 12;
|
|
1070
|
+
const w = g(d / (1.2 + (thin ? 22 : 7) * bell)) * bell;
|
|
1071
|
+
const cool = Math.max(0, Math.min(1, (fx.u - 0.2) / 0.8));
|
|
1072
|
+
// white at the blast, then blue, then violet, as the cloud goes (details3d drawSupernova)
|
|
1073
|
+
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];
|
|
1074
|
+
// THE SHOCKWAVE THROWS WHAT IT CROSSES (operator, 2026-09-15: "Consider perturbing all the
|
|
1075
|
+
// candles that are affected by the shockwave"): the ring drawn in drawSupernova (4.5 shell
|
|
1076
|
+
// radii, out over u 0.14-0.6) runs through the tiles here too -- a tile it reaches is thrown
|
|
1077
|
+
// up hard and lit white, and shakes itself out for a while after the front has passed
|
|
1078
|
+
let lift = 1.4 * A * w, glow = A * w, outline = 0.8 * A * w, colour = col;
|
|
1079
|
+
if (fx.u >= 0.14) {
|
|
1080
|
+
const fr = Math.min(1, (fx.u - 0.14) / 0.46), ring = (thin ? 27 : 31) * (1 - Math.pow(1 - fr, 2.2));
|
|
1081
|
+
const front = g((d - ring) / 1.8) * (1 - fr * 0.6);
|
|
1082
|
+
const passed = ring - d; // > 0 once the front has gone by
|
|
1083
|
+
// the shaking starts as the front APPROACHES, five units out, not once it has passed
|
|
1084
|
+
// (operator: "trigger their shaking sooner in the blast radius")
|
|
1085
|
+
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;
|
|
1086
|
+
if (front > 0.02 || shake > 0.02) {
|
|
1087
|
+
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")
|
|
1088
|
+
glow = Math.max(glow, A * front + 0.4 * A * shake);
|
|
1089
|
+
outline = Math.max(outline, A * front);
|
|
1090
|
+
if (front > w) colour = [255, 255, 255];
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
return glow > 0.02 || lift > 0.05 ? { glow: Math.min(1, glow), outline: Math.min(1, outline), lift, color: colour } : FX_NONE;
|
|
1024
1094
|
}
|
|
1025
1095
|
case 'wave': {
|
|
1026
1096
|
// a swell rolling across the board: the cubes rise and fall with it, several crests at once
|
|
@@ -1125,6 +1195,9 @@ export function fxAt(t, fx) {
|
|
|
1125
1195
|
const col = [Math.round(140 + 115 * ph), Math.round(90 + 140 * (1 - ph)), Math.round(190 + 60 * ph)];
|
|
1126
1196
|
return { glow: 0.85 * A * w, outline: 0.2 * A * w, lift: 0, color: col };
|
|
1127
1197
|
}
|
|
1198
|
+
case 'pulse': // the price line's surge lights the candles under its head (details3d fxNow)
|
|
1199
|
+
case 'firework': // each shell lights its surroundings in its colour (details3d fxNow)
|
|
1200
|
+
case 'blackhole': // the hole's head shrinks the candles it reaches toward nothing (scale) and lights the rest orange
|
|
1128
1201
|
case 'lightcycle':
|
|
1129
1202
|
case 'packets':
|
|
1130
1203
|
case 'centipede':
|
|
@@ -1151,10 +1224,29 @@ export function fxAt(t, fx) {
|
|
|
1151
1224
|
// the very fade that meant it was working. Measured: 0 cubes hidden over a whole run.
|
|
1152
1225
|
// So the physical effect uses PROXIMITY alone and the visual effect keeps using w.
|
|
1153
1226
|
const reach = g(Math.hypot(ddx, ddy) / Math.max(0.2, hd.r ?? 0.8));
|
|
1227
|
+
// `release` (1 unless the head says otherwise): the head letting go of everything it
|
|
1228
|
+
// holds, evenly -- the pull and the scale fade by it, so a tile under the head glides
|
|
1229
|
+
// home over the head's whole release rather than when its reach finally passes it
|
|
1230
|
+
const rel = hd.release ?? 1;
|
|
1154
1231
|
best = {
|
|
1155
1232
|
glow: 0.85 * w, outline: 0.8 * w, lift: (hd.lift ?? 0) * w, color: hd.color,
|
|
1156
1233
|
hide: (hd.hide ?? 0) > 0 && reach > 0.45 ? 1 : 0,
|
|
1157
|
-
scale: hd.scale == null ? 1 : 1 - (1 - hd.scale) * reach,
|
|
1234
|
+
scale: hd.scale == null ? 1 : 1 - (1 - hd.scale) * reach * rel,
|
|
1235
|
+
// `pull`: how hard the head drags this tile toward itself (the black hole; buildScene
|
|
1236
|
+
// shears the faces toward the head's screen point by it)
|
|
1237
|
+
pull: (hd.pull ?? 0) * reach * rel, pullAt: hd.pull ? { x: hd.x, y: hd.y, z: hd.z ?? 0 } : null,
|
|
1238
|
+
// the pull at the head's full size (rPeak): the orbit is laid out against this, so it
|
|
1239
|
+
// does not unwind as the head shrinks; the current pull only says how far along the
|
|
1240
|
+
// line from home to that orbit the tile sits
|
|
1241
|
+
pullPeak: (hd.pull ?? 0) * (hd.rPeak ? g(Math.hypot(ddx, ddy) / Math.max(0.2, hd.rPeak)) : reach),
|
|
1242
|
+
// `lean`: how much the corners lean and stretch toward the head on top of the glide
|
|
1243
|
+
// (1 on the price board; 0 on the block board, where the cubes are lifted out whole)
|
|
1244
|
+
lean: hd.lean ?? 1,
|
|
1245
|
+
// `shrink`: scale the footprint with the height, so the cube stays a cube as it goes
|
|
1246
|
+
shrink: hd.shrink ? 1 : 0,
|
|
1247
|
+
// `orbitR`: the head's reach in grid units -- buildScene lays the block board's orbits
|
|
1248
|
+
// out at a fixed distance round the hole from it, rather than part-way home
|
|
1249
|
+
orbitR: hd.r ?? 0,
|
|
1158
1250
|
};
|
|
1159
1251
|
}
|
|
1160
1252
|
}
|
|
@@ -1208,7 +1300,7 @@ export function buildScene(tiles, o = {}) {
|
|
|
1208
1300
|
// among equals the one further from the vanishing point first (its sides
|
|
1209
1301
|
// lean outward, away from its nearer neighbours).
|
|
1210
1302
|
const vx0 = o.vanishX ?? 0, vy0 = o.vanishY ?? 0;
|
|
1211
|
-
const ordered = o.oblique && o.order === 'diagonal' ? diagonalOrder(tiles) : o.oblique ? obliqueOrder(tiles, o) : tiles.map((t) => (growth.has(t) ? { ...t, boost: growth.get(t) } : t))
|
|
1303
|
+
const ordered = o.order === 'given' ? tiles.slice() : o.oblique && o.order === 'diagonal' ? diagonalOrder(tiles) : o.oblique ? obliqueOrder(tiles, o) : tiles.map((t) => (growth.has(t) ? { ...t, boost: growth.get(t) } : t))
|
|
1212
1304
|
.map((t) => { const c = project(t.x + t.s / 2, t.y + t.s / 2, 0, o); return { t, top: (t.z ?? 0) + (t.floor ?? 0) + cubeHeight(t), d: Math.hypot(c.x - vx0, c.y - vy0) }; })
|
|
1213
1305
|
// Under the oblique camera a cube reaches STRAIGHT UP from its footprint (measured: height
|
|
1214
1306
|
// moves screen y only, grid x moves screen x only), so it can only cover blocks at smaller
|
|
@@ -1220,6 +1312,12 @@ export function buildScene(tiles, o = {}) {
|
|
|
1220
1312
|
const ground = [];
|
|
1221
1313
|
const air = [];
|
|
1222
1314
|
const shadows = [];
|
|
1315
|
+
// IN THE AIR, PAINTED LAST (operator, 2026-09-15, of the black hole on the block board: "There
|
|
1316
|
+
// are too many collision errors happening"): a cube pulled off the board toward the hole was
|
|
1317
|
+
// still painted in the board's own order, so it cut through the neighbours it was passing over.
|
|
1318
|
+
// Its faces are moved to the end of the frame instead, the least-pulled first, so the ones
|
|
1319
|
+
// nearest the hole are on top of everything
|
|
1320
|
+
const pulled = new Map();
|
|
1223
1321
|
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
1224
1322
|
const note = (p) => {
|
|
1225
1323
|
if (p.x < minX) minX = p.x;
|
|
@@ -1257,7 +1355,10 @@ export function buildScene(tiles, o = {}) {
|
|
|
1257
1355
|
})).filter((e) => e.k > 0.01) : [];
|
|
1258
1356
|
// the lamp's direction across the screen, for the side faces (LIGHTS); under the overhead
|
|
1259
1357
|
// lamp every side takes the same light, a shade under the top
|
|
1260
|
-
const lampSide =
|
|
1358
|
+
const lampSide = lampOf(o).side;
|
|
1359
|
+
// the lamp's hardness and how bright the tops sit under it (see the side loop below)
|
|
1360
|
+
const gain = Number.isFinite(o.lightGain) ? Math.max(0.2, Math.min(3, o.lightGain)) : 1;
|
|
1361
|
+
const topLit = Number.isFinite(o.topLight) ? Math.max(0.1, Math.min(1.4, o.topLight)) : 0.8;
|
|
1261
1362
|
for (const t of ordered) {
|
|
1262
1363
|
const fxv = (t.z ?? 0) > 0.02 ? FX_NONE : fxAt(t, o.fx);
|
|
1263
1364
|
// GONE FOR THE DURATION, not deleted: a hidden cube is skipped this frame and drawn again the
|
|
@@ -1266,10 +1367,70 @@ export function buildScene(tiles, o = {}) {
|
|
|
1266
1367
|
// A SHORTER CUBE, the same way: `tall` on a copy. A collapse or a dig is a height override,
|
|
1267
1368
|
// never an edit to the tile the board was built from.
|
|
1268
1369
|
const sc = fxv.scale == null ? 1 : fxv.scale;
|
|
1269
|
-
|
|
1370
|
+
let shaped = sc < 0.999 ? { ...t, tall: Math.max(0.04, cubeHeight(t) * sc) } : t;
|
|
1371
|
+
// a WHOLE smaller cube, not a plate, where the head asks for it (the black hole over the block
|
|
1372
|
+
// board): the footprint shrinks about its centre with the height
|
|
1373
|
+
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
1374
|
// NEON is a flat cube: no facets, no crown -- solid faces and the tubes on their edges
|
|
1271
1375
|
const f = tileFaces(fxv.lift > 0.001 ? { ...shaped, fxz: fxv.lift } : shaped, o, t.s >= facetMin && o.neon !== true);
|
|
1272
|
-
|
|
1376
|
+
// SPAGHETTIFIED (operator, 2026-09-15: "the candles need to stretch and bend toward the hole
|
|
1377
|
+
// before they shrink"): a tile within a pulling head's reach has its faces sheared toward the
|
|
1378
|
+
// head's point on screen -- the top corners dragged hard, the base corners a little -- so it
|
|
1379
|
+
// leans and lengthens toward the hole before the scale takes it. Each corner object is moved
|
|
1380
|
+
// once, however many faces share it.
|
|
1381
|
+
if (fxv.pull > 0.001 && fxv.pullAt) {
|
|
1382
|
+
pulled.set(String(t.txid), fxv.pull);
|
|
1383
|
+
// ...AND ORBITS (operator, 2026-09-15: "The chart bars need to cleanly orbit the disc"): the
|
|
1384
|
+
// tile is swept round the hole along the disk's flow as it is drawn in -- its centre swings
|
|
1385
|
+
// about the hole by an angle that grows with the pull and the clock, on a radius that
|
|
1386
|
+
// shrinks with the pull, flattened toward the disk's plane -- and its corners lean and
|
|
1387
|
+
// stretch toward the hole on top of that
|
|
1388
|
+
const hp = project(fxv.pullAt.x, fxv.pullAt.y, fxv.pullAt.z, o);
|
|
1389
|
+
// EVERY CORNER THE CUBE HAS, the bevel's inner quad included -- it was left at home while
|
|
1390
|
+
// the top and sides moved, so a bevelled cube on the block board drew as a slab stretched
|
|
1391
|
+
// from its old place to its new one (operator, 2026-09-15: "too much bad shearing"); the
|
|
1392
|
+
// crown's points are projected later through f.P, which is shifted the same way below
|
|
1393
|
+
const pts = [...f.top, ...f.sides.flatMap((sd) => sd.points), ...(f.inset ?? [])];
|
|
1394
|
+
const uniq = [...new Set(pts)];
|
|
1395
|
+
const cx0 = uniq.reduce((a, p) => a + p.x, 0) / uniq.length, cy0 = uniq.reduce((a, p) => a + p.y, 0) / uniq.length;
|
|
1396
|
+
// THE ORBIT IS LAID OUT AGAINST THE HOLE'S FULL SIZE, and the tile sits somewhere on the
|
|
1397
|
+
// straight line between its home and that orbit by the CURRENT pull (operator, 2026-09-15:
|
|
1398
|
+
// "Instead of the bars reversing into place, can they just reorient themselves into their
|
|
1399
|
+
// original positions when the blackhole starts to disappear?"). While the hole grows the
|
|
1400
|
+
// tile glides out to its orbit and turns with it; while it shrinks the pull falls and the
|
|
1401
|
+
// tile glides straight home -- the orbit never unwinds.
|
|
1402
|
+
// STEADILY (operator, 2026-09-15: "The bars don't cleanly leave or return ... They sorta get
|
|
1403
|
+
// ripped off, and snapped back into place"): the capture and the release are eased with a
|
|
1404
|
+
// smoothstep, and the orbit is KEPLERIAN -- its angular speed goes with the square of the
|
|
1405
|
+
// pull, so a bar far out barely turns and one close in whips round -- where before every
|
|
1406
|
+
// bar's target swept a wide circle at one speed and a bar just being caught was yanked after
|
|
1407
|
+
// it. The angle's starting offset is small for the same reason.
|
|
1408
|
+
const peak = Math.max(fxv.pullPeak ?? fxv.pull, 0.001), kl = Math.min(1, fxv.pull / peak), k0 = kl * kl * (3 - 2 * kl);
|
|
1409
|
+
const dx = cx0 - hp.x, dy = cy0 - hp.y, r0 = Math.hypot(dx, dy) || 1, a0 = Math.atan2(dy, dx);
|
|
1410
|
+
// HALF THE SPEED (operator, 2026-09-15: "The bars sping around much much too quickly. Try to
|
|
1411
|
+
// calm it down by at least half to start" -- then "slow the spin ... by another half. Want it
|
|
1412
|
+
// smoother and calmer"): 0.00035 a millisecond at full pull, a quarter of the 0.0014 it was
|
|
1413
|
+
const swing = peak * peak * (o.now ?? 0) * 0.00035 + 0.5 * peak; // clockwise on screen, Keplerian
|
|
1414
|
+
// ROUND THE HOLE, on the block board (operator, 2026-09-15: "the blocks don't look right"):
|
|
1415
|
+
// part-way home from a hole twenty units up left the cubes hanging in the middle distance;
|
|
1416
|
+
// each is carried instead to its own ring round the hole, one and a half to three reaches
|
|
1417
|
+
// out in screen pixels, and glides straight back from it as the pull falls
|
|
1418
|
+
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;
|
|
1419
|
+
const r1 = ring != null ? r0 + (ring - r0) * peak : r0 * (1 - 0.55 * peak), a1 = a0 + swing;
|
|
1420
|
+
const cx1 = hp.x + Math.cos(a1) * r1, cy1 = hp.y + Math.sin(a1) * r1 * (1 - 0.6 * peak); // flattened toward the disk
|
|
1421
|
+
const sx = (cx1 - cx0) * k0, sy = (cy1 - cy0) * k0;
|
|
1422
|
+
const tops = new Set([...f.top, ...f.sides.flatMap((sd) => [sd.points[0], sd.points[1]])]);
|
|
1423
|
+
for (const p of uniq) {
|
|
1424
|
+
p.x += sx; p.y += sy; // carried toward, and round, the orbit
|
|
1425
|
+
const k = (tops.has(p) ? 0.55 : 0.2) * peak * k0 * (fxv.lean ?? 1); // and leaning in (not on the block board: lean 0)
|
|
1426
|
+
p.x += (hp.x - p.x) * k; p.y += (hp.y - p.y) * k;
|
|
1427
|
+
}
|
|
1428
|
+
const P0 = f.P, kc = 0.55 * peak * k0 * (fxv.lean ?? 1);
|
|
1429
|
+
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; };
|
|
1430
|
+
}
|
|
1431
|
+
// an x-rayed tile's body is glass: most of its alpha goes, and its edges come back below
|
|
1432
|
+
const xray = fxv.xray ?? 0;
|
|
1433
|
+
const a = (t.alpha ?? 1) * (1 - 0.72 * xray);
|
|
1273
1434
|
const airborne = (t.z ?? 0) > 0.02;
|
|
1274
1435
|
const out = airborne && !o.oblique ? air : ground;
|
|
1275
1436
|
// A tile that just locked flashes brighter for a moment -- the Tetris
|
|
@@ -1388,11 +1549,19 @@ export function buildScene(tiles, o = {}) {
|
|
|
1388
1549
|
// brighter, one turned away falls into shadow
|
|
1389
1550
|
const d = side.nx * lampSide[0] + side.ny * lampSide[1];
|
|
1390
1551
|
// from the viewer: a face pointing down the screen (toward the camera) takes the most light
|
|
1391
|
-
const
|
|
1552
|
+
const k0 = viewerLit ? 0.6 + 0.42 * Math.max(0, side.ny) + 0.3 * Math.max(0, side.nx) - 0.16 * Math.max(0, -side.nx) : 0.34 + 0.26 * (d + 1);
|
|
1553
|
+
// HOW HARD THE LAMP IS (operator, 2026-09-16: "we need better lighting on the front of the
|
|
1554
|
+
// blocks. Still looks too washed out and not illuminated well enough"). The shipped range put
|
|
1555
|
+
// every face between 0.34 and 0.86 of its colour with the top at 0.8: a narrow band, and a
|
|
1556
|
+
// board of it reads as tinted rather than lit. `lightGain` stretches that range about its
|
|
1557
|
+
// middle -- above 1 the face turned to the lamp goes brighter than its own colour and the one
|
|
1558
|
+
// turned away falls properly dark. 1 is exactly what every board drew before this.
|
|
1559
|
+
const k = Math.max(0.06, Math.min(1.4, LIT_MID + (k0 - LIT_MID) * gain));
|
|
1392
1560
|
out.push({ txid: t.txid, face: 'side', key: side.key, points: side.points, fill: shade(c, k * lit, a), ...(viewerLit ? { stroke: lift(c, 0.45, round3(0.5 * a)) } : {}) });
|
|
1393
1561
|
side.points.forEach(note);
|
|
1394
1562
|
}
|
|
1395
|
-
|
|
1563
|
+
const topK = viewerLit ? 1.02 : Math.max(0.06, Math.min(1.4, LIT_MID + (topLit - LIT_MID) * gain));
|
|
1564
|
+
out.push({ txid: t.txid, face: 'top', points: f.top, fill: shade(c, topK * lit, a), stroke: viewerLit ? lift(c, 0.55, round3(0.6 * a)) : `rgba(0,0,0,${round3(seam * a)})` });
|
|
1396
1565
|
f.top.forEach(note);
|
|
1397
1566
|
|
|
1398
1567
|
if (f.innerG && !viewerLit) {
|
|
@@ -1582,6 +1751,23 @@ export function buildScene(tiles, o = {}) {
|
|
|
1582
1751
|
// a tint at the top's weight (0.5) was there in the pixels and nowhere to the eye (measured:
|
|
1583
1752
|
// the scan's peak moved a candle's face by a 0.26 cyan wash). At 0.85 it reads as struck.
|
|
1584
1753
|
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)})` });
|
|
1754
|
+
// THE X-RAY TREATMENT: bright edges round every face, and a raster of thin lines across the top
|
|
1755
|
+
// and the sides, so the tile reads as a wireframe scan of itself
|
|
1756
|
+
if (xray > 0.02) {
|
|
1757
|
+
const xc = `rgba(200,245,255,${round3(0.95 * xray)})`;
|
|
1758
|
+
out.push({ txid: t.txid, face: 'outline', points: f.top, fill: 'rgba(0,0,0,0)', stroke: xc, lw: 1.2, always: true });
|
|
1759
|
+
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 });
|
|
1760
|
+
const raster = (poly) => {
|
|
1761
|
+
const [p0, p1, p2, p3] = poly; // p0->p1 and p3->p2 are the edges the lines run between
|
|
1762
|
+
for (let i = 1; i <= 3; i++) {
|
|
1763
|
+
const u = i / 4;
|
|
1764
|
+
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 };
|
|
1765
|
+
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 });
|
|
1766
|
+
}
|
|
1767
|
+
};
|
|
1768
|
+
raster(f.top);
|
|
1769
|
+
for (const side of f.sides) raster(side.points);
|
|
1770
|
+
}
|
|
1585
1771
|
if (fxv.outline > 0.03) {
|
|
1586
1772
|
const col = fxv.color.join(',');
|
|
1587
1773
|
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 +1816,13 @@ export function buildScene(tiles, o = {}) {
|
|
|
1630
1816
|
if (o.oblique && !viewerLit && zt < 1.5) shadows.push(...restingShadowOps(t, o, 1 - zt / 1.5));
|
|
1631
1817
|
}
|
|
1632
1818
|
}
|
|
1633
|
-
|
|
1819
|
+
let ops = o.oblique ? [...shadows, ...ground] : [...ground, ...shadows, ...air];
|
|
1820
|
+
if (pulled.size) {
|
|
1821
|
+
const rest = [], lifted = [];
|
|
1822
|
+
ops.forEach((op, i) => (pulled.has(String(op.txid)) ? lifted : rest).push({ op, i }));
|
|
1823
|
+
lifted.sort((p, q) => (pulled.get(String(p.op.txid)) - pulled.get(String(q.op.txid))) || (p.i - q.i));
|
|
1824
|
+
ops = [...rest.map((e) => e.op), ...lifted.map((e) => e.op)];
|
|
1825
|
+
}
|
|
1634
1826
|
return { ops, bounds: ops.length ? { minX, maxX, minY, maxY } : null, count: ordered.length };
|
|
1635
1827
|
}
|
|
1636
1828
|
|
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 };
|