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