website_next: part 12

This commit is contained in:
nym21
2026-07-10 12:45:55 +02:00
parent e279116bff
commit 8d43f340a0
11 changed files with 320 additions and 161 deletions
+37 -17
View File
@@ -1,11 +1,17 @@
import { brk } from "../../../utils/client.js";
import { FILTERS, getFilterBit, getTypeBit } from "./filters/model.js";
import {
FILTERS,
TYPE_BITS,
TYPE_FILTER_MASK,
getFilterBit,
} from "./filters/model.js";
const VERSION_BITS = new Map([
[1, getFilterBit("version:1")],
[2, getFilterBit("version:2")],
[3, getFilterBit("version:3")],
]);
const VERSION_BITS = [
0,
getFilterBit("version:1"),
getFilterBit("version:2"),
getFilterBit("version:3"),
];
const RBF_YES_BIT = getFilterBit("rbf:yes");
const RBF_NO_BIT = getFilterBit("rbf:no");
@@ -13,6 +19,11 @@ const INPUT_ONE_BIT = getFilterBit("input:one");
const INPUT_MULTI_BIT = getFilterBit("input:multi");
const OUTPUT_ONE_BIT = getFilterBit("output:one");
const OUTPUT_MULTI_BIT = getFilterBit("output:multi");
const FILTER_INDEX_BY_BIT = FILTERS.reduce((indexes, { bit, index }) => {
indexes[bit] = index;
return indexes;
}, /** @type {Record<number, number>} */ ({}));
/**
* @template T
@@ -73,11 +84,8 @@ export async function loadBlockPreview(block, signal) {
return {
range: { start, end },
transactions: weights.data.map((weight, index) => ({
txIndex: start + index,
weight: /** @type {number} */ (weight),
feeRate: /** @type {number} */ (feeRates.data[index]),
})),
feeRates: /** @type {number[]} */ (feeRates.data),
weights: /** @type {number[]} */ (weights.data),
};
}
@@ -112,8 +120,11 @@ function rangeEnd(starts, counts) {
* @param {number} mask
*/
function countMask(counts, mask) {
for (const { bit, index } of FILTERS) {
if (mask & bit) counts[index] += 1;
while (mask) {
const bit = mask & -mask;
counts[FILTER_INDEX_BY_BIT[bit]] += 1;
mask ^= bit;
}
}
@@ -126,7 +137,8 @@ function getTypesMask(types, start, end) {
let mask = 0;
for (let index = start; index < end; index += 1) {
mask |= getTypeBit(types[index]);
mask |= TYPE_BITS[/** @type {keyof typeof TYPE_BITS} */ (types[index])] ?? 0;
if (mask === TYPE_FILTER_MASK) return mask;
}
return mask;
@@ -186,7 +198,8 @@ export async function loadBlockPreviewFilters(range, signal) {
const masks = new Uint32Array(end - start);
const counts = new Uint32Array(FILTERS.length);
versions.data.forEach((version, index) => {
for (let index = 0; index < versions.data.length; index += 1) {
const version = /** @type {number} */ (versions.data[index]);
const inputCount = /** @type {number} */ (inputCounts.data[index]);
const outputCount = /** @type {number} */ (outputCounts.data[index]);
const inputTypeStart = /** @type {number} */ (firstInputs.data[index]);
@@ -194,7 +207,7 @@ export async function loadBlockPreviewFilters(range, signal) {
const outputTypeStart = /** @type {number} */ (firstOutputs.data[index]);
const outputTypeEnd = outputTypeStart + outputCount;
const mask =
(VERSION_BITS.get(/** @type {number} */ (version)) ?? 0) |
(VERSION_BITS[version] ?? 0) |
(rbfs.data[index] ? RBF_YES_BIT : RBF_NO_BIT) |
(inputCount === 1 ? INPUT_ONE_BIT : INPUT_MULTI_BIT) |
(outputCount === 1 ? OUTPUT_ONE_BIT : OUTPUT_MULTI_BIT) |
@@ -211,7 +224,7 @@ export async function loadBlockPreviewFilters(range, signal) {
masks[index] = mask;
countMask(counts, mask);
});
}
return { counts, masks, start };
}
@@ -222,6 +235,13 @@ export async function loadBlockPreviewFilters(range, signal) {
* @property {number} end
*/
/**
* @typedef {Object} BlockPreviewData
* @property {BlockPreviewRange} range
* @property {number[]} weights
* @property {number[]} feeRates
*/
/**
* @typedef {Object} BlockPreviewTransaction
* @property {number} txIndex
@@ -50,18 +50,22 @@ const FILTER_BITS = /** @type {Map<string, number>} */ (
new Map(FILTERS.map(({ bit, key }) => [key, bit]))
);
const TYPE_KEYS = /** @type {const} */ ({
empty: "type:empty",
multisig: "type:multisig",
op_return: "type:op_return",
p2a: "type:p2a",
p2pk: "type:p2pk",
p2pkh: "type:p2pkh",
p2sh: "type:p2sh",
unknown: "type:unknown",
v0_p2wpkh: "type:p2wpkh",
v0_p2wsh: "type:p2wsh",
v1_p2tr: "type:taproot",
export const TYPE_FILTER_MASK = FILTERS
.filter(({ group }) => group === "type")
.reduce((mask, { bit }) => mask | bit, 0);
export const TYPE_BITS = /** @type {const} */ ({
empty: getFilterBit("type:empty"),
multisig: getFilterBit("type:multisig"),
op_return: getFilterBit("type:op_return"),
p2a: getFilterBit("type:p2a"),
p2pk: getFilterBit("type:p2pk"),
p2pkh: getFilterBit("type:p2pkh"),
p2sh: getFilterBit("type:p2sh"),
unknown: getFilterBit("type:unknown"),
v0_p2wpkh: getFilterBit("type:p2wpkh"),
v0_p2wsh: getFilterBit("type:p2wsh"),
v1_p2tr: getFilterBit("type:taproot"),
});
/**
@@ -70,12 +74,3 @@ const TYPE_KEYS = /** @type {const} */ ({
export function getFilterBit(key) {
return FILTER_BITS.get(key) ?? 0;
}
/**
* @param {string} type
*/
export function getTypeBit(type) {
const key = TYPE_KEYS[/** @type {keyof typeof TYPE_KEYS} */ (type)];
return key === undefined ? 0 : getFilterBit(key);
}
@@ -16,12 +16,16 @@ function weightToSpan(weight, capacity, columns) {
* @param {readonly Cell[]} cells
* @param {number} capacity
* @param {number} columns
* @returns {ResolvedCapacityCell<Cell>[]}
*/
function resolveCapacityCells(cells, capacity, columns) {
return cells.map((cell) => ({
...cell,
span: weightToSpan(cell.weight, capacity, columns),
}));
const resolved = /** @type {ResolvedCapacityCell<Cell>[]} */ (cells);
for (const cell of resolved) {
cell.span = weightToSpan(cell.weight, capacity, columns);
}
return resolved;
}
/**
@@ -31,10 +35,7 @@ function resolveCapacityCells(cells, capacity, columns) {
* @param {number} columns
*/
function fitCapacityCells(cells, capacity, columns) {
let resolvedCells = resolveCapacityCells(cells, capacity, columns).slice(
0,
columns * columns,
);
const resolvedCells = resolveCapacityCells(cells, capacity, columns);
let layouts = packCells(resolvedCells, columns, columns);
while (layouts === null) {
@@ -71,3 +72,8 @@ export function createSquareLayout(cells, capacity, columns) {
* @typedef {Object} CapacityCell
* @property {number} weight
*/
/**
* @template {CapacityCell} Cell
* @typedef {Cell & { span: number }} ResolvedCapacityCell
*/
@@ -1,23 +1,27 @@
import { createFeeRateRange } from "../../fee-rates.js";
import { FEE_RATE_PERCENTILES } from "../../fee-rates.js";
/**
* @param {BlockPreviewTransaction[]} transactions
* @param {number[]} feeRates
* @param {Uint32Array} order
*/
export function createPreviewFeeRange(transactions) {
const feeRates = new Array(transactions.length);
export function createPreviewFeeRange(feeRates, order) {
return FEE_RATE_PERCENTILES.map((percentile) => {
const index = Math.round(((100 - percentile) / 100) * (order.length - 1));
for (let index = 0; index < transactions.length; index += 1) {
feeRates[index] = transactions[index].feeRate;
return feeRates[order[index]];
});
}
/**
* @param {number[]} weights
* @param {number[]} feeRates
*/
export function orderTransactions(weights, feeRates) {
const order = new Uint32Array(feeRates.length);
for (let index = 0; index < order.length; index += 1) {
order[index] = index;
}
return createFeeRateRange(feeRates);
return order.sort((a, b) => feeRates[b] - feeRates[a] || weights[b] - weights[a]);
}
/**
* @param {BlockPreviewTransaction[]} transactions
*/
export function orderTransactions(transactions) {
return transactions.sort((a, b) => b.feeRate - a.feeRate || b.weight - a.weight);
}
/** @typedef {import("../data.js").BlockPreviewTransaction} BlockPreviewTransaction */
@@ -21,20 +21,42 @@ function getGap(canvas, width) {
return Math.max(1, maxGap * Math.min(1, width / GAP_REFERENCE_WIDTH));
}
/**
* @param {Uint16Array} lookup
* @param {number} columns
* @param {SquareCellLayout} layout
* @param {number} value
*/
function fillLookup(lookup, columns, layout, value) {
const endX = layout.x + layout.span;
const endY = layout.y + layout.span;
for (let row = layout.y; row < endY; row += 1) {
const offset = row * columns;
for (let column = layout.x; column < endX; column += 1) {
lookup[offset + column] = value;
}
}
}
/**
* @param {SquareLayout} square
* @param {HTMLCanvasElement} canvas
* @param {number} width
*/
export function createPreviewRects(square, canvas, width) {
export function createPreviewGeometry(square, canvas, width) {
const gap = getGap(canvas, width);
const cell = Math.max(1, (width - gap * (square.columns - 1)) / square.columns);
const unit = cell + gap;
const lookup = new Uint16Array(square.columns * square.columns);
return square.layouts.map((layout, index) => {
const rects = square.layouts.map((layout, index) => {
const transaction = square.resolvedCells[index].transaction;
const rectSize = unitsToPixels(layout.span, cell, gap);
fillLookup(lookup, square.columns, layout, index + 1);
return {
color: square.resolvedCells[index].color,
size: rectSize,
@@ -43,25 +65,39 @@ export function createPreviewRects(square, canvas, width) {
y: width - layout.y * unit - rectSize,
};
});
return { columns: square.columns, lookup, rects, unit, width };
}
/**
* @param {PreviewRect[]} rects
* @param {PreviewGeometry} geometry
* @param {number} x
* @param {number} y
*/
export function hitTest(rects, x, y) {
for (let index = rects.length - 1; index >= 0; index -= 1) {
const rect = rects[index];
export function hitTest(geometry, x, y) {
if (x < 0 || y < 0 || x > geometry.width || y > geometry.width) {
return null;
}
if (
x >= rect.x &&
x <= rect.x + rect.size &&
y >= rect.y &&
y <= rect.y + rect.size
) {
return rect.transaction;
}
const column = Math.min(geometry.columns - 1, Math.floor(x / geometry.unit));
const rowFromTop = Math.min(
geometry.columns - 1,
Math.floor(y / geometry.unit),
);
const row = geometry.columns - rowFromTop - 1;
const index = geometry.lookup[row * geometry.columns + column] - 1;
if (index < 0) return null;
const rect = geometry.rects[index];
if (
x >= rect.x &&
x <= rect.x + rect.size &&
y >= rect.y &&
y <= rect.y + rect.size
) {
return rect.transaction;
}
return null;
@@ -76,6 +112,15 @@ export function hitTest(rects, x, y) {
* @property {number} y
*/
/**
* @typedef {Object} PreviewGeometry
* @property {number} columns
* @property {Uint16Array} lookup
* @property {PreviewRect[]} rects
* @property {number} unit
* @property {number} width
*/
/**
* @typedef {Object} SquareLayout
* @property {number} columns
@@ -2,44 +2,83 @@ import { MAX_BLOCK_WEIGHT } from "../../format.js";
import { createPreviewFeeRange, orderTransactions } from "./fees.js";
import { createSquareLayout } from "./capacity.js";
import { getCanvasFeeRateColor } from "./color.js";
import { createPreviewRects, hitTest } from "./geometry.js";
import { createPreviewGeometry, hitTest } from "./geometry.js";
import { drawPreview } from "./draw.js";
const COLUMNS = 84;
const VISIBLE_CELLS = COLUMNS * COLUMNS;
/**
* @param {BlockPreviewTransaction[]} transactions
* @param {BlockPreviewData} data
* @param {Uint32Array} order
* @param {number[]} ranges
*/
function createCapacityCells(data, order, ranges) {
const count = Math.min(order.length, VISIBLE_CELLS);
const colors = /** @type {Map<number, string>} */ (new Map());
const cells = [];
for (let index = 0; index < count; index += 1) {
const offset = order[index];
const feeRate = data.feeRates[offset];
const weight = data.weights[offset];
let color = colors.get(feeRate);
if (color === undefined) {
color = getCanvasFeeRateColor(feeRate, ranges);
colors.set(feeRate, color);
}
cells.push({
color,
transaction: {
feeRate,
txIndex: data.range.start + offset,
weight,
},
weight,
});
}
return cells;
}
/**
* @param {BlockPreviewData} data
* @param {Object} [options]
* @param {(transaction: BlockPreviewTransaction | null, point: BlockPreviewPointer | null, eager: boolean) => void} [options.onInspect]
*/
export function createBlockPreviewHeatmap(transactions, options = {}) {
export function createBlockPreviewHeatmap(data, options = {}) {
const canvas = document.createElement("canvas");
const context = /** @type {CanvasRenderingContext2D} */ (
canvas.getContext("2d")
);
const ordered = orderTransactions(transactions);
const ranges = createPreviewFeeRange(ordered);
const visible = ordered.slice(0, VISIBLE_CELLS);
const cells = visible.map((transaction) => ({
color: getCanvasFeeRateColor(transaction.feeRate, ranges),
transaction,
weight: transaction.weight,
}));
const order = orderTransactions(data.weights, data.feeRates);
const ranges = createPreviewFeeRange(data.feeRates, order);
const cells = createCapacityCells(data, order, ranges);
const square = createSquareLayout(cells, MAX_BLOCK_WEIGHT, COLUMNS);
let disabledMask = 0;
let filterState = /** @type {BlockPreviewFilterState | null} */ (null);
let frame = 0;
let inspected = /** @type {BlockPreviewTransaction | null} */ (null);
let inspectFrame = 0;
let inspectPoint = /** @type {BlockPreviewPointer | null} */ (null);
let bounds = /** @type {DOMRectReadOnly | null} */ (null);
let previewMask = /** @type {number | null} */ (null);
let rects = /** @type {PreviewRect[]} */ ([]);
let geometry = /** @type {PreviewGeometry | null} */ (null);
let rectWidth = 0;
let capturedPointer = /** @type {number | null} */ (null);
canvas.dataset.blockPreviewHeatmap = "";
function measure() {
bounds = canvas.getBoundingClientRect();
return bounds;
}
function draw() {
const width = canvas.getBoundingClientRect().width;
const width = measure().width;
if (width <= 0) return;
@@ -53,7 +92,7 @@ export function createBlockPreviewHeatmap(transactions, options = {}) {
if (rectWidth !== width) {
rectWidth = width;
rects = createPreviewRects(square, canvas, width);
geometry = createPreviewGeometry(square, canvas, width);
}
context.setTransform(dpr, 0, 0, dpr, 0, 0);
@@ -64,7 +103,7 @@ export function createBlockPreviewHeatmap(transactions, options = {}) {
filterState,
inspected,
previewMask,
rects,
rects: geometry?.rects ?? [],
});
}
@@ -73,6 +112,12 @@ export function createBlockPreviewHeatmap(transactions, options = {}) {
frame = requestAnimationFrame(draw);
}
function cancelInspectFrame() {
cancelAnimationFrame(inspectFrame);
inspectFrame = 0;
inspectPoint = null;
}
/** @param {BlockPreviewTransaction | null} transaction */
function setInspected(transaction) {
if (transaction === inspected) return;
@@ -82,46 +127,64 @@ export function createBlockPreviewHeatmap(transactions, options = {}) {
}
/**
* @param {PointerEvent} event
* @param {BlockPreviewPointer} point
* @param {boolean} eager
*/
function inspectAt(event, eager) {
const bounds = canvas.getBoundingClientRect();
function inspectAt(point, eager) {
const nextBounds = bounds ?? measure();
if (geometry === null) draw();
if (geometry === null) return;
const transaction = hitTest(
rects,
event.clientX - bounds.left,
event.clientY - bounds.top,
geometry,
point.clientX - nextBounds.left,
point.clientY - nextBounds.top,
);
if (transaction === null && inspected !== null) {
options.onInspect?.(
inspected,
{ clientX: event.clientX, clientY: event.clientY },
eager,
);
options.onInspect?.(inspected, point, eager);
return;
}
setInspected(transaction);
options.onInspect?.(
transaction,
{ clientX: event.clientX, clientY: event.clientY },
eager,
);
options.onInspect?.(transaction, point, eager);
}
/** @param {PointerEvent} event */
function pointFromEvent(event) {
return { clientX: event.clientX, clientY: event.clientY };
}
function inspectLatest() {
const point = inspectPoint;
inspectFrame = 0;
inspectPoint = null;
if (point !== null) inspectAt(point, false);
}
/** @param {PointerEvent} event */
function scheduleInspect(event) {
inspectPoint = pointFromEvent(event);
if (inspectFrame) return;
inspectFrame = requestAnimationFrame(inspectLatest);
}
/** @param {PointerEvent} event */
function startInspect(event) {
capturedPointer = event.pointerId;
canvas.setPointerCapture(event.pointerId);
inspectAt(event, true);
cancelInspectFrame();
measure();
inspectAt(pointFromEvent(event), true);
if (event.pointerType !== "mouse") event.preventDefault();
}
/** @param {PointerEvent} event */
function moveInspect(event) {
if (capturedPointer !== null || event.pointerType === "mouse") {
inspectAt(event, false);
scheduleInspect(event);
if (capturedPointer !== null && event.pointerType !== "mouse") {
event.preventDefault();
}
@@ -138,6 +201,7 @@ export function createBlockPreviewHeatmap(transactions, options = {}) {
}
function clearInspect() {
cancelInspectFrame();
setInspected(null);
options.onInspect?.(null, null, false);
}
@@ -151,6 +215,7 @@ export function createBlockPreviewHeatmap(transactions, options = {}) {
const observer = new ResizeObserver(scheduleDraw);
canvas.addEventListener("pointerenter", measure);
canvas.addEventListener("pointermove", moveInspect);
canvas.addEventListener("pointerdown", startInspect);
canvas.addEventListener("pointerup", stopInspect);
@@ -170,9 +235,9 @@ export function createBlockPreviewHeatmap(transactions, options = {}) {
return /** @type {const} */ ({
element: canvas,
ordered,
destroy() {
cancelAnimationFrame(frame);
cancelInspectFrame();
document.removeEventListener("pointerdown", clearOnOutsidePointer);
window.removeEventListener("blur", clearInspect);
observer.disconnect();
@@ -199,6 +264,7 @@ export function createBlockPreviewHeatmap(transactions, options = {}) {
});
}
/** @typedef {import("../data.js").BlockPreviewData} BlockPreviewData */
/** @typedef {import("../data.js").BlockPreviewTransaction} BlockPreviewTransaction */
/** @typedef {import("../data.js").BlockPreviewFilterState} BlockPreviewFilterState */
@@ -208,4 +274,4 @@ export function createBlockPreviewHeatmap(transactions, options = {}) {
* @property {number} clientY
*/
/** @typedef {import("./geometry.js").PreviewRect} PreviewRect */
/** @typedef {import("./geometry.js").PreviewGeometry} PreviewGeometry */
@@ -1,41 +1,39 @@
/**
* @param {readonly PackCell[]} cells
* @param {number} columns
* @param {number} [rows]
* @param {number} rows
* @returns {PackLayout[] | null}
*/
export function packCells(cells, columns, rows) {
const occupied = /** @type {boolean[][]} */ ([]);
const occupied = new Uint8Array(columns * rows);
const layouts = [];
let usedRows = 0;
for (const cell of cells) {
const span = Math.min(cell.span, columns);
const position = findPosition(occupied, columns, span, rows);
const position = findPosition(occupied, columns, rows, span);
if (position === null) return null;
fillCells(occupied, position.x, position.y, span);
usedRows = Math.max(usedRows, position.y + span);
layouts.push({ ...position, span, rows: rows ?? usedRows });
fillCells(occupied, columns, position.x, position.y, span);
layouts.push({ x: position.x, y: position.y, span });
}
return layouts;
}
/**
* @param {boolean[][]} occupied
* @param {Uint8Array} occupied
* @param {number} columns
* @param {number} rows
* @param {number} span
* @param {number} [rows]
* @returns {{ x: number, y: number } | null}
*/
function findPosition(occupied, columns, span, rows) {
const lastRow = rows === undefined ? Infinity : rows - span;
function findPosition(occupied, columns, rows, span) {
const lastRow = rows - span;
for (let y = 0; y <= lastRow; y += 1) {
for (let x = 0; x <= columns - span; x += 1) {
if (canPlace(occupied, x, y, span)) return { x, y };
if (canPlace(occupied, columns, x, y, span)) return { x, y };
}
}
@@ -43,15 +41,18 @@ function findPosition(occupied, columns, span, rows) {
}
/**
* @param {boolean[][]} occupied
* @param {Uint8Array} occupied
* @param {number} columns
* @param {number} x
* @param {number} y
* @param {number} span
*/
function canPlace(occupied, x, y, span) {
function canPlace(occupied, columns, x, y, span) {
for (let row = y; row < y + span; row += 1) {
const offset = row * columns;
for (let column = x; column < x + span; column += 1) {
if (occupied[row]?.[column]) return false;
if (occupied[offset + column]) return false;
}
}
@@ -59,17 +60,18 @@ function canPlace(occupied, x, y, span) {
}
/**
* @param {boolean[][]} occupied
* @param {Uint8Array} occupied
* @param {number} columns
* @param {number} x
* @param {number} y
* @param {number} span
*/
function fillCells(occupied, x, y, span) {
function fillCells(occupied, columns, x, y, span) {
for (let row = y; row < y + span; row += 1) {
occupied[row] ??= [];
const offset = row * columns;
for (let column = x; column < x + span; column += 1) {
occupied[row][column] = true;
occupied[offset + column] = 1;
}
}
}
@@ -84,5 +86,4 @@ function fillCells(occupied, x, y, span) {
* @property {number} x
* @property {number} y
* @property {number} span
* @property {number} rows
*/
+7 -7
View File
@@ -47,14 +47,14 @@ function memoize(load) {
}
/**
* @param {BlockPreviewTransaction[]} transactions
* @param {BlockPreviewData} data
* @param {() => Promise<BlockPreviewFilterState>} loadFilters
* @param {AbortSignal} signal
*/
function createPreview(transactions, loadFilters, signal) {
function createPreview(data, loadFilters, signal) {
const loadFilterState = memoize(loadFilters);
const inspector = createBlockPreviewInspector(signal, loadFilterState);
const heatmap = createBlockPreviewHeatmap(transactions, {
const heatmap = createBlockPreviewHeatmap(data, {
onInspect: inspector.inspect,
});
const filters = createPreviewFilters(loadFilterState, heatmap);
@@ -94,11 +94,11 @@ export function createBlockPreviewPane(block) {
renderStatus(content, "Loading");
void loadBlockPreview(block, controller.signal)
.then(({ range, transactions }) => {
.then((data) => {
if (!live) return;
const preview = createPreview(
transactions,
() => loadBlockPreviewFilters(range, controller.signal),
data,
() => loadBlockPreviewFilters(data.range, controller.signal),
controller.signal,
);
@@ -122,7 +122,7 @@ export function createBlockPreviewPane(block) {
};
}
/** @typedef {import("./data.js").BlockPreviewTransaction} BlockPreviewTransaction */
/** @typedef {import("./data.js").BlockPreviewData} BlockPreviewData */
/** @typedef {import("./data.js").BlockPreviewFilterState} BlockPreviewFilterState */
/**
@@ -50,6 +50,7 @@ export function createBlockPreviewInspector(parentSignal, loadFilters) {
const txidLoader = createTxidLoader(parentSignal);
let inspected = /** @type {BlockPreviewTransaction | null} */ (null);
let point = /** @type {BlockPreviewPointer | null} */ (null);
let readoutSize = /** @type {BlockPreviewReadoutSize | null} */ (null);
let filterState = /** @type {BlockPreviewFilterState | null} */ (null);
let filterPromise = /** @type {Promise<void> | null} */ (null);
let traitsTimer = 0;
@@ -62,7 +63,24 @@ export function createBlockPreviewInspector(parentSignal, loadFilters) {
txidLoader.abort();
}
function invalidateReadoutSize() {
readoutSize = null;
}
/**
* @param {BlockPreviewPointer} nextPoint
*/
function place(nextPoint) {
readoutSize ??= {
height: element.offsetHeight,
width: element.offsetWidth,
};
placeReadout(element, nextPoint, readoutSize);
}
function setTraitsLoading() {
invalidateReadoutSize();
for (const { value } of traitFields) {
value.removeAttribute("title");
setField(value, "loading", true);
@@ -71,6 +89,8 @@ export function createBlockPreviewInspector(parentSignal, loadFilters) {
/** @param {number} mask */
function setTraits(mask) {
invalidateReadoutSize();
for (const { filters, value } of traitFields) {
const labels = filters
.filter(({ bit }) => mask & bit)
@@ -88,7 +108,7 @@ export function createBlockPreviewInspector(parentSignal, loadFilters) {
const mask = state.masks[transaction.txIndex - state.start];
setTraits(mask);
if (point !== null) placeReadout(element, point);
if (point !== null) place(point);
}
function loadFilterState() {
@@ -141,12 +161,15 @@ export function createBlockPreviewInspector(parentSignal, loadFilters) {
element.hidden = false;
point = nextPoint;
placeReadout(element, nextPoint);
if (transaction === inspected) return;
if (transaction === inspected) {
place(nextPoint);
return;
}
abortPending();
inspected = transaction;
invalidateReadoutSize();
setField(tx, `#${formatNumber(transaction.txIndex)}`);
setField(fee, `${formatFeeRate(transaction.feeRate)} sat/vB`);
@@ -159,8 +182,10 @@ export function createBlockPreviewInspector(parentSignal, loadFilters) {
(loadedTxid) => {
if (inspected !== transaction) return;
invalidateReadoutSize();
setField(txid, loadedTxid);
txid.title = loadedTxid;
if (point !== null) place(point);
},
(error, signal) => {
if (signal.aborted) return;
@@ -173,6 +198,8 @@ export function createBlockPreviewInspector(parentSignal, loadFilters) {
txid.removeAttribute("title");
setField(txid, "loading", true);
}
place(nextPoint);
}
return /** @type {const} */ ({
@@ -190,3 +217,9 @@ export function createBlockPreviewInspector(parentSignal, loadFilters) {
* @property {number} clientX
* @property {number} clientY
*/
/**
* @typedef {Object} BlockPreviewReadoutSize
* @property {number} width
* @property {number} height
*/
@@ -3,12 +3,12 @@ const OVERLAY_MARGIN = 8;
/**
* @param {HTMLElement} element
* @param {BlockPreviewPointer} point
* @param {BlockPreviewReadoutSize} size
*/
export function placeReadout(element, point) {
export function placeReadout(element, point, size) {
const figure = /** @type {HTMLElement} */ (element.parentElement);
const bounds = figure.getBoundingClientRect();
const width = element.offsetWidth;
const height = element.offsetHeight;
const { width, height } = size;
const minX = Math.min(bounds.width / 2, width / 2 + OVERLAY_MARGIN);
const maxX = Math.max(minX, bounds.width - minX);
const rawX = point.clientX - bounds.left;
@@ -31,3 +31,9 @@ export function placeReadout(element, point) {
* @property {number} clientX
* @property {number} clientY
*/
/**
* @typedef {Object} BlockPreviewReadoutSize
* @property {number} width
* @property {number} height
*/