Test rendering multiple blocks on one canvas
This commit is contained in:
parent
05e88a25be
commit
a33de8bc8c
@ -18,6 +18,8 @@ export default class BlockScene {
|
|||||||
animationOffset: number;
|
animationOffset: number;
|
||||||
highlightingEnabled: boolean;
|
highlightingEnabled: boolean;
|
||||||
filterFlags: bigint | null = 0b00000100_00000000_00000000_00000000n;
|
filterFlags: bigint | null = 0b00000100_00000000_00000000_00000000n;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
gridWidth: number;
|
gridWidth: number;
|
||||||
@ -31,14 +33,16 @@ export default class BlockScene {
|
|||||||
animateUntil = 0;
|
animateUntil = 0;
|
||||||
dirty: boolean;
|
dirty: boolean;
|
||||||
|
|
||||||
constructor({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction }:
|
constructor({ x = 0, y = 0, width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction }:
|
||||||
{ width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
|
{ x?: number, y?: number, width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
|
||||||
orientation: string, flip: boolean, vertexArray: FastVertexArray, theme: ThemeService, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null }
|
orientation: string, flip: boolean, vertexArray: FastVertexArray, theme: ThemeService, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null }
|
||||||
) {
|
) {
|
||||||
this.init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction });
|
this.init({ x, y,width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction });
|
||||||
}
|
}
|
||||||
|
|
||||||
resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void {
|
resize({ x = 0, y = 0, width = this.width, height = this.height, animate = true }: { x?: number, y?: number, width?: number, height?: number, animate: boolean }): void {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
this.width = width;
|
this.width = width;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
this.gridSize = this.width / this.gridWidth;
|
this.gridSize = this.width / this.gridWidth;
|
||||||
@ -238,8 +242,8 @@ export default class BlockScene {
|
|||||||
this.animateUntil = Math.max(this.animateUntil, tx.setHighlight(value));
|
this.animateUntil = Math.max(this.animateUntil, tx.setHighlight(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
private init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction }:
|
private init({ x, y, width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction }:
|
||||||
{ width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
|
{ x: number, y: number, width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
|
||||||
orientation: string, flip: boolean, vertexArray: FastVertexArray, theme: ThemeService, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null }
|
orientation: string, flip: boolean, vertexArray: FastVertexArray, theme: ThemeService, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null }
|
||||||
): void {
|
): void {
|
||||||
this.animationDuration = animationDuration || this.animationDuration || 1000;
|
this.animationDuration = animationDuration || this.animationDuration || 1000;
|
||||||
@ -264,7 +268,7 @@ export default class BlockScene {
|
|||||||
this.vbytesPerUnit = blockLimit / Math.pow(resolution / 1.02, 2);
|
this.vbytesPerUnit = blockLimit / Math.pow(resolution / 1.02, 2);
|
||||||
this.gridWidth = resolution;
|
this.gridWidth = resolution;
|
||||||
this.gridHeight = resolution;
|
this.gridHeight = resolution;
|
||||||
this.resize({ width, height, animate: true });
|
this.resize({ x, y, width, height, animate: true });
|
||||||
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
|
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
|
||||||
|
|
||||||
this.txs = {};
|
this.txs = {};
|
||||||
@ -449,18 +453,18 @@ export default class BlockScene {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
x: x + this.unitPadding - (slotSize / 2),
|
x: this.x + x + this.unitPadding - (slotSize / 2),
|
||||||
y: y + this.unitPadding - (slotSize / 2),
|
y: this.y + y + this.unitPadding - (slotSize / 2),
|
||||||
s: squareSize
|
s: squareSize
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return { x: 0, y: 0, s: 0 };
|
return { x: this.x, y: this.y, s: 0 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private screenToGrid(position: Position): Position {
|
private screenToGrid(position: Position): Position {
|
||||||
let x = position.x;
|
let x = position.x - this.x;
|
||||||
let y = this.height - position.y;
|
let y = this.height - (position.y - this.y);
|
||||||
let t;
|
let t;
|
||||||
|
|
||||||
switch (this.orientation) {
|
switch (this.orientation) {
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
<div class="block-overview-graph">
|
||||||
|
<canvas *browserOnly class="block-overview-canvas" [class.clickable]="!!hoverTx" #blockCanvas></canvas>
|
||||||
|
@if (!disableSpinner) {
|
||||||
|
<div class="loader-wrapper" [class.hidden]="!isLoading && !unavailable">
|
||||||
|
<div *ngIf="!unavailable" class="spinner-border ml-3 loading" role="status"></div>
|
||||||
|
<div *ngIf="!isLoading && unavailable" class="ml-3" i18n="block.not-available">not available</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<app-block-overview-tooltip
|
||||||
|
[tx]="selectedTx || hoverTx"
|
||||||
|
[cursorPosition]="tooltipPosition"
|
||||||
|
[clickable]="!!selectedTx"
|
||||||
|
[auditEnabled]="auditHighlighting"
|
||||||
|
[blockConversion]="blockConversion"
|
||||||
|
[filterFlags]="activeFilterFlags"
|
||||||
|
[filterMode]="filterMode"
|
||||||
|
[relativeTime]="relativeTime"
|
||||||
|
></app-block-overview-tooltip>
|
||||||
|
<app-block-filters *ngIf="webGlEnabled && showFilters && filtersAvailable" [excludeFilters]="excludeFilters" [cssWidth]="cssWidth" (onFilterChanged)="setFilterFlags($event)"></app-block-filters>
|
||||||
|
<div *ngIf="!webGlEnabled" class="placeholder">
|
||||||
|
<span i18n="webgl-disabled">Your browser does not support this feature.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,67 @@
|
|||||||
|
.block-overview-graph {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--stat-box-bg);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
grid-column: 1/-1;
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-alignment {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-align {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, 75px);
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-overview-canvas {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
background: #181b2d7f;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
transition: opacity 500ms 500ms;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,647 @@
|
|||||||
|
import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, NgZone, AfterViewInit, OnDestroy, OnChanges } from '@angular/core';
|
||||||
|
import { TransactionStripped } from '../../interfaces/node-api.interface';
|
||||||
|
import { FastVertexArray } from '../block-overview-graph/fast-vertex-array';
|
||||||
|
import BlockScene from '../block-overview-graph/block-scene';
|
||||||
|
import TxSprite from '../block-overview-graph/tx-sprite';
|
||||||
|
import TxView from '../block-overview-graph/tx-view';
|
||||||
|
import { Color, Position } from '../block-overview-graph/sprite-types';
|
||||||
|
import { Price } from '../../services/price.service';
|
||||||
|
import { StateService } from '../../services/state.service';
|
||||||
|
import { ThemeService } from '../../services/theme.service';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
import { defaultColorFunction, setOpacity, defaultAuditColors, defaultColors, ageColorFunction, contrastColorFunction, contrastAuditColors, contrastColors } from '../block-overview-graph/utils';
|
||||||
|
import { ActiveFilter, FilterMode, toFlags } from '../../shared/filters.utils';
|
||||||
|
import { detectWebGL } from '../../shared/graphs.utils';
|
||||||
|
|
||||||
|
const unmatchedOpacity = 0.2;
|
||||||
|
const unmatchedAuditColors = {
|
||||||
|
censored: setOpacity(defaultAuditColors.censored, unmatchedOpacity),
|
||||||
|
missing: setOpacity(defaultAuditColors.missing, unmatchedOpacity),
|
||||||
|
added: setOpacity(defaultAuditColors.added, unmatchedOpacity),
|
||||||
|
added_prioritized: setOpacity(defaultAuditColors.added_prioritized, unmatchedOpacity),
|
||||||
|
prioritized: setOpacity(defaultAuditColors.prioritized, unmatchedOpacity),
|
||||||
|
accelerated: setOpacity(defaultAuditColors.accelerated, unmatchedOpacity),
|
||||||
|
};
|
||||||
|
const unmatchedContrastAuditColors = {
|
||||||
|
censored: setOpacity(contrastAuditColors.censored, unmatchedOpacity),
|
||||||
|
missing: setOpacity(contrastAuditColors.missing, unmatchedOpacity),
|
||||||
|
added: setOpacity(contrastAuditColors.added, unmatchedOpacity),
|
||||||
|
added_prioritized: setOpacity(contrastAuditColors.added_prioritized, unmatchedOpacity),
|
||||||
|
prioritized: setOpacity(contrastAuditColors.prioritized, unmatchedOpacity),
|
||||||
|
accelerated: setOpacity(contrastAuditColors.accelerated, unmatchedOpacity),
|
||||||
|
};
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-block-overview-multi',
|
||||||
|
templateUrl: './block-overview-multi.component.html',
|
||||||
|
styleUrls: ['./block-overview-multi.component.scss'],
|
||||||
|
})
|
||||||
|
export class BlockOverviewMultiComponent implements AfterViewInit, OnDestroy, OnChanges {
|
||||||
|
@Input() isLoading: boolean;
|
||||||
|
@Input() resolution: number;
|
||||||
|
@Input() numBlocks: number;
|
||||||
|
@Input() blockWidth: number = 360;
|
||||||
|
@Input() autofit: boolean = false;
|
||||||
|
@Input() blockLimit: number;
|
||||||
|
@Input() orientation = 'left';
|
||||||
|
@Input() flip = true;
|
||||||
|
@Input() animationDuration: number = 1000;
|
||||||
|
@Input() animationOffset: number | null = null;
|
||||||
|
@Input() disableSpinner = false;
|
||||||
|
@Input() mirrorTxid: string | void;
|
||||||
|
@Input() unavailable: boolean = false;
|
||||||
|
@Input() auditHighlighting: boolean = false;
|
||||||
|
@Input() showFilters: boolean = false;
|
||||||
|
@Input() excludeFilters: string[] = [];
|
||||||
|
@Input() filterFlags: bigint | null = null;
|
||||||
|
@Input() filterMode: FilterMode = 'and';
|
||||||
|
@Input() gradientMode: 'fee' | 'age' = 'fee';
|
||||||
|
@Input() relativeTime: number | null;
|
||||||
|
@Input() blockConversion: Price;
|
||||||
|
@Input() overrideColors: ((tx: TxView) => Color) | null = null;
|
||||||
|
@Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>();
|
||||||
|
@Output() txHoverEvent = new EventEmitter<string>();
|
||||||
|
@Output() readyEvent = new EventEmitter();
|
||||||
|
|
||||||
|
@ViewChild('blockCanvas')
|
||||||
|
canvas: ElementRef<HTMLCanvasElement>;
|
||||||
|
themeChangedSubscription: Subscription;
|
||||||
|
|
||||||
|
gl: WebGLRenderingContext;
|
||||||
|
animationFrameRequest: number;
|
||||||
|
animationHeartBeat: number;
|
||||||
|
displayWidth: number;
|
||||||
|
displayHeight: number;
|
||||||
|
cssWidth: number;
|
||||||
|
cssHeight: number;
|
||||||
|
shaderProgram: WebGLProgram;
|
||||||
|
vertexArray: FastVertexArray;
|
||||||
|
running: boolean;
|
||||||
|
scenes: BlockScene[] = [];
|
||||||
|
hoverTx: TxView | void;
|
||||||
|
selectedTx: TxView | void;
|
||||||
|
highlightTx: TxView | void;
|
||||||
|
mirrorTx: TxView | void;
|
||||||
|
tooltipPosition: Position;
|
||||||
|
|
||||||
|
readyNextFrame = false;
|
||||||
|
lastUpdate: number = 0;
|
||||||
|
pendingUpdates: {
|
||||||
|
count: number,
|
||||||
|
add: { [txid: string]: TransactionStripped },
|
||||||
|
remove: { [txid: string]: string },
|
||||||
|
change: { [txid: string]: { txid: string, rate: number | undefined, acc: boolean | undefined } },
|
||||||
|
direction?: string,
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
searchText: string;
|
||||||
|
searchSubscription: Subscription;
|
||||||
|
filtersAvailable: boolean = true;
|
||||||
|
activeFilterFlags: bigint | null = null;
|
||||||
|
|
||||||
|
webGlEnabled = true;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly ngZone: NgZone,
|
||||||
|
readonly elRef: ElementRef,
|
||||||
|
public stateService: StateService,
|
||||||
|
private themeService: ThemeService,
|
||||||
|
) {
|
||||||
|
this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
|
||||||
|
this.vertexArray = new FastVertexArray(512, TxSprite.dataSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
if (this.canvas) {
|
||||||
|
this.canvas.nativeElement.addEventListener('webglcontextlost', this.handleContextLost, false);
|
||||||
|
this.canvas.nativeElement.addEventListener('webglcontextrestored', this.handleContextRestored, false);
|
||||||
|
this.gl = this.canvas.nativeElement.getContext('webgl');
|
||||||
|
this.initScenes();
|
||||||
|
|
||||||
|
if (this.gl) {
|
||||||
|
this.initCanvas();
|
||||||
|
this.resizeCanvas();
|
||||||
|
this.themeChangedSubscription = this.themeService.themeChanged$.subscribe(() => {
|
||||||
|
for (const scene of this.scenes) {
|
||||||
|
scene.setColorFunction(this.getColorFunction());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initScenes(): void {
|
||||||
|
for (const scene of this.scenes) {
|
||||||
|
if (scene) {
|
||||||
|
scene.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.scenes = [];
|
||||||
|
this.pendingUpdates = [];
|
||||||
|
for (let i = 0; i < this.numBlocks; i++) {
|
||||||
|
this.scenes.push(null);
|
||||||
|
this.pendingUpdates.push({
|
||||||
|
count: 0,
|
||||||
|
add: {},
|
||||||
|
remove: {},
|
||||||
|
change: {},
|
||||||
|
direction: 'left',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.resizeCanvas();
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges(changes): void {
|
||||||
|
if (changes.numBlocks) {
|
||||||
|
this.initScenes();
|
||||||
|
}
|
||||||
|
if (changes.orientation || changes.flip) {
|
||||||
|
for (const scene of this.scenes) {
|
||||||
|
scene?.setOrientation(this.orientation, this.flip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changes.auditHighlighting) {
|
||||||
|
this.setHighlightingEnabled(this.auditHighlighting);
|
||||||
|
}
|
||||||
|
if (changes.overrideColor) {
|
||||||
|
for (const scene of this.scenes) {
|
||||||
|
scene?.setColorFunction(this.getFilterColorFunction(0n, this.gradientMode));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((changes.filterFlags || changes.showFilters || changes.filterMode || changes.gradientMode)) {
|
||||||
|
this.setFilterFlags();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilterFlags(goggle?: ActiveFilter): void {
|
||||||
|
this.filterMode = goggle?.mode || this.filterMode;
|
||||||
|
this.gradientMode = goggle?.gradient || this.gradientMode;
|
||||||
|
this.activeFilterFlags = goggle?.filters ? toFlags(goggle.filters) : this.filterFlags;
|
||||||
|
for (const scene of this.scenes) {
|
||||||
|
if (this.activeFilterFlags != null && this.filtersAvailable) {
|
||||||
|
scene.setColorFunction(this.getFilterColorFunction(this.activeFilterFlags, this.gradientMode));
|
||||||
|
} else {
|
||||||
|
scene.setColorFunction(this.getFilterColorFunction(0n, this.gradientMode));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (this.animationFrameRequest) {
|
||||||
|
cancelAnimationFrame(this.animationFrameRequest);
|
||||||
|
clearTimeout(this.animationHeartBeat);
|
||||||
|
}
|
||||||
|
if (this.canvas) {
|
||||||
|
this.canvas.nativeElement.removeEventListener('webglcontextlost', this.handleContextLost);
|
||||||
|
this.canvas.nativeElement.removeEventListener('webglcontextrestored', this.handleContextRestored);
|
||||||
|
this.themeChangedSubscription?.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(block: number, direction): void {
|
||||||
|
this.exit(block, direction);
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(block: number): void {
|
||||||
|
if (this.scenes[block]) {
|
||||||
|
this.scenes[block].destroy();
|
||||||
|
this.clearUpdateQueue(block);
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize the scene without any entry transition
|
||||||
|
setup(block: number, transactions: TransactionStripped[], sort: boolean = false): void {
|
||||||
|
const filtersAvailable = transactions.reduce((flagSet, tx) => flagSet || tx.flags > 0, false);
|
||||||
|
if (filtersAvailable !== this.filtersAvailable) {
|
||||||
|
this.setFilterFlags();
|
||||||
|
}
|
||||||
|
this.filtersAvailable = filtersAvailable;
|
||||||
|
if (this.scenes[block]) {
|
||||||
|
this.clearUpdateQueue(block);
|
||||||
|
this.scenes[block].setup(transactions, sort);
|
||||||
|
this.readyNextFrame = true;
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enter(block: number, transactions: TransactionStripped[], direction: string): void {
|
||||||
|
if (this.scenes[block]) {
|
||||||
|
this.clearUpdateQueue(block);
|
||||||
|
this.scenes[block].enter(transactions, direction);
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exit(block: number, direction: string): void {
|
||||||
|
if (this.scenes[block]) {
|
||||||
|
this.clearUpdateQueue(block);
|
||||||
|
this.scenes[block].exit(direction);
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
replace(block: number, transactions: TransactionStripped[], direction: string, sort: boolean = true, startTime?: number): void {
|
||||||
|
if (this.scenes[block]) {
|
||||||
|
this.clearUpdateQueue(block);
|
||||||
|
this.scenes[block].replace(transactions || [], direction, sort, startTime);
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// collates deferred updates into a set of consistent pending changes
|
||||||
|
queueUpdate(block: number, add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left'): void {
|
||||||
|
for (const tx of add) {
|
||||||
|
this.pendingUpdates[block].add[tx.txid] = tx;
|
||||||
|
delete this.pendingUpdates[block].remove[tx.txid];
|
||||||
|
delete this.pendingUpdates[block].change[tx.txid];
|
||||||
|
}
|
||||||
|
for (const txid of remove) {
|
||||||
|
delete this.pendingUpdates[block].add[txid];
|
||||||
|
this.pendingUpdates[block].remove[txid] = txid;
|
||||||
|
delete this.pendingUpdates[block].change[txid];
|
||||||
|
}
|
||||||
|
for (const tx of change) {
|
||||||
|
if (this.pendingUpdates[block].add[tx.txid]) {
|
||||||
|
this.pendingUpdates[block].add[tx.txid].rate = tx.rate;
|
||||||
|
this.pendingUpdates[block].add[tx.txid].acc = tx.acc;
|
||||||
|
} else {
|
||||||
|
this.pendingUpdates[block].change[tx.txid] = tx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.pendingUpdates[block].direction = direction;
|
||||||
|
this.pendingUpdates[block].count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
deferredUpdate(block: number, add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left'): void {
|
||||||
|
this.queueUpdate(block, add, remove, change, direction);
|
||||||
|
this.applyQueuedUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
applyQueuedUpdates(): void {
|
||||||
|
for (const [index, pendingUpdate] of this.pendingUpdates.entries()) {
|
||||||
|
if (pendingUpdate.count && performance.now() > (this.lastUpdate + this.animationDuration)) {
|
||||||
|
this.applyUpdate(index, Object.values(pendingUpdate.add), Object.values(pendingUpdate.remove), Object.values(pendingUpdate.change), pendingUpdate.direction);
|
||||||
|
}
|
||||||
|
this.clearUpdateQueue(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearUpdateQueue(block: number): void {
|
||||||
|
this.pendingUpdates[block] = {
|
||||||
|
count: 0,
|
||||||
|
add: {},
|
||||||
|
remove: {},
|
||||||
|
change: {},
|
||||||
|
};
|
||||||
|
this.lastUpdate = performance.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
update(block: number, add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
|
||||||
|
// merge any pending changes into this update
|
||||||
|
this.queueUpdate(block, add, remove, change, direction);
|
||||||
|
this.applyUpdate(block,Object.values(this.pendingUpdates[block].add), Object.values(this.pendingUpdates[block].remove), Object.values(this.pendingUpdates[block].change), direction, resetLayout);
|
||||||
|
this.clearUpdateQueue(block);
|
||||||
|
}
|
||||||
|
|
||||||
|
applyUpdate(block: number, add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
|
||||||
|
if (this.scenes[block]) {
|
||||||
|
add = add.filter(tx => !this.scenes[block].txs[tx.txid]);
|
||||||
|
remove = remove.filter(txid => this.scenes[block].txs[txid]);
|
||||||
|
change = change.filter(tx => this.scenes[block].txs[tx.txid]);
|
||||||
|
|
||||||
|
if (this.gradientMode === 'age') {
|
||||||
|
this.scenes[block].updateAllColors();
|
||||||
|
}
|
||||||
|
this.scenes[block].update(add, remove, change, direction, resetLayout);
|
||||||
|
this.start();
|
||||||
|
this.lastUpdate = performance.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initCanvas(): void {
|
||||||
|
if (!this.canvas || !this.gl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.gl.clearColor(0.0, 0.0, 0.0, 0.0);
|
||||||
|
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
|
||||||
|
|
||||||
|
const shaderSet = [
|
||||||
|
{
|
||||||
|
type: this.gl.VERTEX_SHADER,
|
||||||
|
src: vertShaderSrc
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: this.gl.FRAGMENT_SHADER,
|
||||||
|
src: fragShaderSrc
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
this.shaderProgram = this.buildShaderProgram(shaderSet);
|
||||||
|
|
||||||
|
this.gl.useProgram(this.shaderProgram);
|
||||||
|
|
||||||
|
// Set up alpha blending
|
||||||
|
this.gl.enable(this.gl.BLEND);
|
||||||
|
this.gl.blendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA);
|
||||||
|
|
||||||
|
const glBuffer = this.gl.createBuffer();
|
||||||
|
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, glBuffer);
|
||||||
|
|
||||||
|
/* SET UP SHADER ATTRIBUTES */
|
||||||
|
Object.keys(attribs).forEach((key, i) => {
|
||||||
|
attribs[key].pointer = this.gl.getAttribLocation(this.shaderProgram, key);
|
||||||
|
this.gl.enableVertexAttribArray(attribs[key].pointer);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleContextLost(event): void {
|
||||||
|
event.preventDefault();
|
||||||
|
cancelAnimationFrame(this.animationFrameRequest);
|
||||||
|
this.animationFrameRequest = null;
|
||||||
|
this.running = false;
|
||||||
|
this.gl = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleContextRestored(event): void {
|
||||||
|
if (this.canvas?.nativeElement) {
|
||||||
|
this.gl = this.canvas.nativeElement.getContext('webgl');
|
||||||
|
if (this.gl) {
|
||||||
|
this.initCanvas();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('window:resize', ['$event'])
|
||||||
|
resizeCanvas(): void {
|
||||||
|
if (this.canvas) {
|
||||||
|
this.cssWidth = this.canvas.nativeElement.offsetParent.clientWidth;
|
||||||
|
this.cssHeight = this.canvas.nativeElement.offsetParent.clientHeight;
|
||||||
|
this.displayWidth = window.devicePixelRatio * this.cssWidth;
|
||||||
|
this.displayHeight = window.devicePixelRatio * this.cssHeight;
|
||||||
|
this.canvas.nativeElement.width = this.displayWidth;
|
||||||
|
this.canvas.nativeElement.height = this.displayHeight;
|
||||||
|
if (this.gl) {
|
||||||
|
this.gl.viewport(0, 0, this.displayWidth, this.displayHeight);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < this.scenes.length; i++) {
|
||||||
|
const blocksPerRow = Math.floor(this.displayWidth / this.blockWidth);
|
||||||
|
const x = (i % blocksPerRow) * this.blockWidth;
|
||||||
|
const row = Math.floor(i / blocksPerRow);
|
||||||
|
const y = this.displayHeight - ((row + 1) * this.blockWidth);
|
||||||
|
if (this.scenes[i]) {
|
||||||
|
this.scenes[i].resize({ x, y, width: this.blockWidth, height: this.blockWidth, animate: false });
|
||||||
|
this.start();
|
||||||
|
} else {
|
||||||
|
this.scenes[i] = new BlockScene({ x, y, width: this.blockWidth, height: this.blockWidth, resolution: this.resolution,
|
||||||
|
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray, theme: this.themeService,
|
||||||
|
highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: 0,
|
||||||
|
colorFunction: this.getColorFunction() });
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileShader(src, type): WebGLShader {
|
||||||
|
if (!this.gl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const shader = this.gl.createShader(type);
|
||||||
|
|
||||||
|
this.gl.shaderSource(shader, src);
|
||||||
|
this.gl.compileShader(shader);
|
||||||
|
|
||||||
|
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
|
||||||
|
console.log(`Error compiling ${type === this.gl.VERTEX_SHADER ? 'vertex' : 'fragment'} shader:`);
|
||||||
|
console.log(this.gl.getShaderInfoLog(shader));
|
||||||
|
}
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildShaderProgram(shaderInfo): WebGLProgram {
|
||||||
|
if (!this.gl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const program = this.gl.createProgram();
|
||||||
|
|
||||||
|
shaderInfo.forEach((desc) => {
|
||||||
|
const shader = this.compileShader(desc.src, desc.type);
|
||||||
|
if (shader) {
|
||||||
|
this.gl.attachShader(program, shader);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.gl.linkProgram(program);
|
||||||
|
|
||||||
|
if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
|
||||||
|
console.log('Error linking shader program:');
|
||||||
|
console.log(this.gl.getProgramInfoLog(program));
|
||||||
|
}
|
||||||
|
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
start(): void {
|
||||||
|
this.running = true;
|
||||||
|
this.ngZone.runOutsideAngular(() => this.doRun());
|
||||||
|
}
|
||||||
|
|
||||||
|
doRun(): void {
|
||||||
|
if (this.animationFrameRequest) {
|
||||||
|
cancelAnimationFrame(this.animationFrameRequest);
|
||||||
|
}
|
||||||
|
this.animationFrameRequest = requestAnimationFrame(() => this.run());
|
||||||
|
}
|
||||||
|
|
||||||
|
run(now?: DOMHighResTimeStamp): void {
|
||||||
|
if (!now) {
|
||||||
|
now = performance.now();
|
||||||
|
}
|
||||||
|
this.applyQueuedUpdates();
|
||||||
|
// skip re-render if there's no change to the scene
|
||||||
|
if (this.scenes.length && this.gl) {
|
||||||
|
/* SET UP SHADER UNIFORMS */
|
||||||
|
// screen dimensions
|
||||||
|
this.gl.uniform2f(this.gl.getUniformLocation(this.shaderProgram, 'screenSize'), this.displayWidth, this.displayHeight);
|
||||||
|
// frame timestamp
|
||||||
|
this.gl.uniform1f(this.gl.getUniformLocation(this.shaderProgram, 'now'), now);
|
||||||
|
|
||||||
|
if (this.vertexArray.dirty) {
|
||||||
|
/* SET UP SHADER ATTRIBUTES */
|
||||||
|
Object.keys(attribs).forEach((key, i) => {
|
||||||
|
this.gl.vertexAttribPointer(attribs[key].pointer,
|
||||||
|
attribs[key].count, // number of primitives in this attribute
|
||||||
|
this.gl[attribs[key].type], // type of primitive in this attribute (e.g. gl.FLOAT)
|
||||||
|
false, // never normalised
|
||||||
|
stride, // distance between values of the same attribute
|
||||||
|
attribs[key].offset); // offset of the first value
|
||||||
|
});
|
||||||
|
|
||||||
|
const pointArray = this.vertexArray.getVertexData();
|
||||||
|
|
||||||
|
if (pointArray.length) {
|
||||||
|
this.gl.bufferData(this.gl.ARRAY_BUFFER, pointArray, this.gl.DYNAMIC_DRAW);
|
||||||
|
this.gl.drawArrays(this.gl.TRIANGLES, 0, pointArray.length / TxSprite.vertexSize);
|
||||||
|
}
|
||||||
|
this.vertexArray.dirty = false;
|
||||||
|
} else {
|
||||||
|
const pointArray = this.vertexArray.getVertexData();
|
||||||
|
if (pointArray.length) {
|
||||||
|
this.gl.drawArrays(this.gl.TRIANGLES, 0, pointArray.length / TxSprite.vertexSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.readyNextFrame) {
|
||||||
|
this.readyNextFrame = false;
|
||||||
|
this.readyEvent.emit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* LOOP */
|
||||||
|
if (this.running && this.scenes.length && now <= (this.scenes.reduce((max, scene) => scene.animateUntil > max ? scene.animateUntil : max, 0) + 500)) {
|
||||||
|
this.doRun();
|
||||||
|
} else {
|
||||||
|
if (this.animationHeartBeat) {
|
||||||
|
clearTimeout(this.animationHeartBeat);
|
||||||
|
}
|
||||||
|
this.animationHeartBeat = window.setTimeout(() => {
|
||||||
|
this.start();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setHighlightingEnabled(enabled: boolean): void {
|
||||||
|
for (const scene of this.scenes) {
|
||||||
|
scene.setHighlighting(enabled);
|
||||||
|
}
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
getColorFunction(): ((tx: TxView) => Color) {
|
||||||
|
if (this.overrideColors) {
|
||||||
|
return this.overrideColors;
|
||||||
|
} else if (this.filterFlags) {
|
||||||
|
return this.getFilterColorFunction(this.filterFlags, this.gradientMode);
|
||||||
|
} else if (this.activeFilterFlags) {
|
||||||
|
return this.getFilterColorFunction(this.activeFilterFlags, this.gradientMode);
|
||||||
|
} else {
|
||||||
|
return this.getFilterColorFunction(0n, this.gradientMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getFilterColorFunction(flags: bigint, gradient: 'fee' | 'age'): ((tx: TxView) => Color) {
|
||||||
|
return (tx: TxView) => {
|
||||||
|
if ((this.filterMode === 'and' && (tx.bigintFlags & flags) === flags) || (this.filterMode === 'or' && (flags === 0n || (tx.bigintFlags & flags) > 0n))) {
|
||||||
|
if (this.themeService.theme !== 'contrast' && this.themeService.theme !== 'bukele') {
|
||||||
|
return (gradient === 'age') ? ageColorFunction(tx, defaultColors.fee, defaultAuditColors, this.relativeTime || (Date.now() / 1000)) : defaultColorFunction(tx, defaultColors.fee, defaultAuditColors, this.relativeTime || (Date.now() / 1000));
|
||||||
|
} else {
|
||||||
|
return (gradient === 'age') ? ageColorFunction(tx, contrastColors.fee, contrastAuditColors, this.relativeTime || (Date.now() / 1000)) : contrastColorFunction(tx, contrastColors.fee, contrastAuditColors, this.relativeTime || (Date.now() / 1000));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.themeService.theme !== 'contrast' && this.themeService.theme !== 'bukele') {
|
||||||
|
return (gradient === 'age') ? { r: 1, g: 1, b: 1, a: 0.05 } : defaultColorFunction(
|
||||||
|
tx,
|
||||||
|
defaultColors.unmatchedfee,
|
||||||
|
unmatchedAuditColors,
|
||||||
|
this.relativeTime || (Date.now() / 1000)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (gradient === 'age') ? { r: 1, g: 1, b: 1, a: 0.05 } : contrastColorFunction(
|
||||||
|
tx,
|
||||||
|
contrastColors.unmatchedfee,
|
||||||
|
unmatchedContrastAuditColors,
|
||||||
|
this.relativeTime || (Date.now() / 1000)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebGL shader attributes
|
||||||
|
const attribs = {
|
||||||
|
offset: { type: 'FLOAT', count: 2, pointer: null, offset: 0 },
|
||||||
|
posX: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
|
||||||
|
posY: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
|
||||||
|
posR: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
|
||||||
|
colR: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
|
||||||
|
colG: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
|
||||||
|
colB: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
|
||||||
|
colA: { type: 'FLOAT', count: 4, pointer: null, offset: 0 }
|
||||||
|
};
|
||||||
|
// Calculate the number of bytes per vertex based on specified attributes
|
||||||
|
const stride = Object.values(attribs).reduce((total, attrib) => {
|
||||||
|
return total + (attrib.count * 4);
|
||||||
|
}, 0);
|
||||||
|
// Calculate vertex attribute offsets
|
||||||
|
for (let i = 0, offset = 0; i < Object.keys(attribs).length; i++) {
|
||||||
|
const attrib = Object.values(attribs)[i];
|
||||||
|
attrib.offset = offset;
|
||||||
|
offset += (attrib.count * 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
const vertShaderSrc = `
|
||||||
|
varying lowp vec4 vColor;
|
||||||
|
|
||||||
|
// each attribute contains [x: startValue, y: endValue, z: startTime, w: rate]
|
||||||
|
// shader interpolates between start and end values at the given rate, from the given time
|
||||||
|
|
||||||
|
attribute vec2 offset;
|
||||||
|
attribute vec4 posX;
|
||||||
|
attribute vec4 posY;
|
||||||
|
attribute vec4 posR;
|
||||||
|
attribute vec4 colR;
|
||||||
|
attribute vec4 colG;
|
||||||
|
attribute vec4 colB;
|
||||||
|
attribute vec4 colA;
|
||||||
|
|
||||||
|
uniform vec2 screenSize;
|
||||||
|
uniform float now;
|
||||||
|
|
||||||
|
float smootherstep(float x) {
|
||||||
|
x = clamp(x, 0.0, 1.0);
|
||||||
|
float ix = 1.0 - x;
|
||||||
|
x = x * x;
|
||||||
|
return x / (x + ix * ix);
|
||||||
|
}
|
||||||
|
|
||||||
|
float interpolateAttribute(vec4 attr) {
|
||||||
|
float d = (now - attr.z) * attr.w;
|
||||||
|
float delta = smootherstep(d);
|
||||||
|
return mix(attr.x, attr.y, delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 screenTransform = vec4(2.0 / screenSize.x, 2.0 / screenSize.y, -1.0, -1.0);
|
||||||
|
// vec4 screenTransform = vec4(1.0 / screenSize.x, 1.0 / screenSize.y, -0.5, -0.5);
|
||||||
|
|
||||||
|
float radius = interpolateAttribute(posR);
|
||||||
|
vec2 position = vec2(interpolateAttribute(posX), interpolateAttribute(posY)) + (radius * offset);
|
||||||
|
|
||||||
|
gl_Position = vec4(position * screenTransform.xy + screenTransform.zw, 1.0, 1.0);
|
||||||
|
|
||||||
|
float red = interpolateAttribute(colR);
|
||||||
|
float green = interpolateAttribute(colG);
|
||||||
|
float blue = interpolateAttribute(colB);
|
||||||
|
float alpha = interpolateAttribute(colA);
|
||||||
|
|
||||||
|
vColor = vec4(red, green, blue, alpha);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const fragShaderSrc = `
|
||||||
|
varying lowp vec4 vColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
gl_FragColor = vColor;
|
||||||
|
// premultiply alpha
|
||||||
|
gl_FragColor.rgb *= gl_FragColor.a;
|
||||||
|
}
|
||||||
|
`;
|
@ -1,10 +1,12 @@
|
|||||||
<div class="blocks" [class.wrap]="wrapBlocks">
|
<!-- <div class="blocks" [class.wrap]="wrapBlocks">
|
||||||
<ng-container *ngFor="let i of blockIndices">
|
<ng-container *ngFor="let i of blockIndices">
|
||||||
<div class="block-wrapper" [style]="wrapperStyle">
|
<div class="block-wrapper" [style]="wrapperStyle">
|
||||||
<div class="block-container" [style]="containerStyle">
|
<div class="block-container" [style]="containerStyle"> -->
|
||||||
<app-block-overview-graph
|
<app-block-overview-multi
|
||||||
#blockGraph
|
#blockGraph
|
||||||
[isLoading]="false"
|
[isLoading]="false"
|
||||||
|
[numBlocks]="8"
|
||||||
|
[blockWidth]="blockWidth"
|
||||||
[resolution]="resolution"
|
[resolution]="resolution"
|
||||||
[blockLimit]="stateService.blockVSize"
|
[blockLimit]="stateService.blockVSize"
|
||||||
[orientation]="'top'"
|
[orientation]="'top'"
|
||||||
@ -12,14 +14,12 @@
|
|||||||
[animationDuration]="animationDuration"
|
[animationDuration]="animationDuration"
|
||||||
[animationOffset]="animationOffset"
|
[animationOffset]="animationOffset"
|
||||||
[disableSpinner]="true"
|
[disableSpinner]="true"
|
||||||
[relativeTime]="blockInfo[i]?.timestamp"
|
></app-block-overview-multi>
|
||||||
(txClickEvent)="onTxClick($event)"
|
<!-- <div *ngIf="showInfo && blockInfo[i]" class="info" @infoChange>
|
||||||
></app-block-overview-graph>
|
|
||||||
<div *ngIf="showInfo && blockInfo[i]" class="info" @infoChange>
|
|
||||||
<h1 class="height">{{ blockInfo[i].height }}</h1>
|
<h1 class="height">{{ blockInfo[i].height }}</h1>
|
||||||
<h2 class="mined-by">by {{ blockInfo[i].extras.pool.name || 'Unknown' }}</h2>
|
<h2 class="mined-by">by {{ blockInfo[i].extras.pool.name || 'Unknown' }}</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div> -->
|
@ -1,16 +1,15 @@
|
|||||||
import { Component, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/core';
|
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { catchError, startWith } from 'rxjs/operators';
|
import { catchError } from 'rxjs/operators';
|
||||||
import { Subject, Subscription, of } from 'rxjs';
|
import { Subject, Subscription, of } from 'rxjs';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { WebsocketService } from '../../services/websocket.service';
|
import { WebsocketService } from '../../services/websocket.service';
|
||||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
|
||||||
import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
|
import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '../../services/api.service';
|
||||||
import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overview-graph.component';
|
|
||||||
import { detectWebGL } from '../../shared/graphs.utils';
|
import { detectWebGL } from '../../shared/graphs.utils';
|
||||||
import { animate, style, transition, trigger } from '@angular/animations';
|
import { animate, style, transition, trigger } from '@angular/animations';
|
||||||
import { BytesPipe } from '../../shared/pipes/bytes-pipe/bytes.pipe';
|
import { BytesPipe } from '../../shared/pipes/bytes-pipe/bytes.pipe';
|
||||||
|
import { BlockOverviewMultiComponent } from '../block-overview-multi/block-overview-multi.component';
|
||||||
|
|
||||||
function bestFitResolution(min, max, n): number {
|
function bestFitResolution(min, max, n): number {
|
||||||
const target = (min + max) / 2;
|
const target = (min + max) / 2;
|
||||||
@ -65,7 +64,7 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
|
|||||||
autofit: boolean = false;
|
autofit: boolean = false;
|
||||||
padding: number = 0;
|
padding: number = 0;
|
||||||
wrapBlocks: boolean = false;
|
wrapBlocks: boolean = false;
|
||||||
blockWidth: number = 1080;
|
blockWidth: number = 360;
|
||||||
animationDuration: number = 2000;
|
animationDuration: number = 2000;
|
||||||
animationOffset: number = 0;
|
animationOffset: number = 0;
|
||||||
stagger: number = 0;
|
stagger: number = 0;
|
||||||
@ -85,7 +84,7 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
|
|||||||
containerStyle = {};
|
containerStyle = {};
|
||||||
resolution: number = 86;
|
resolution: number = 86;
|
||||||
|
|
||||||
@ViewChildren('blockGraph') blockGraphs: QueryList<BlockOverviewGraphComponent>;
|
@ViewChild('blockGraph') blockGraph: BlockOverviewMultiComponent;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
@ -149,9 +148,7 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
ngAfterViewInit(): void {
|
||||||
this.graphChangeSubscription = this.blockGraphs.changes.pipe(startWith(null)).subscribe(() => {
|
this.setupBlockGraphs();
|
||||||
this.setupBlockGraphs();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
@ -208,10 +205,10 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
updateBlockGraphs(blocks): void {
|
updateBlockGraphs(blocks): void {
|
||||||
const startTime = performance.now() + 1000 - (this.stagger < 0 ? this.stagger * 8 : 0);
|
const startTime = performance.now() + 1000 - (this.stagger < 0 ? this.stagger * 8 : 0);
|
||||||
if (this.blockGraphs) {
|
if (this.blockGraph) {
|
||||||
this.blockGraphs.forEach((graph, index) => {
|
for (let i = 0; i < this.numBlocks; i++) {
|
||||||
graph.replace(this.strippedTransactions[blocks?.[index]?.height] || [], 'right', false, startTime + (this.stagger * index));
|
this.blockGraph.replace(i, this.strippedTransactions[blocks?.[i]?.height] || [], 'right', false, startTime + (this.stagger * i));
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
this.showInfo = false;
|
this.showInfo = false;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -226,28 +223,11 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setupBlockGraphs(): void {
|
setupBlockGraphs(): void {
|
||||||
if (this.blockGraphs) {
|
if (this.blockGraph) {
|
||||||
this.blockGraphs.forEach((graph, index) => {
|
for (let i = 0; i < this.numBlocks; i++) {
|
||||||
graph.destroy();
|
this.blockGraph.destroy(i);
|
||||||
graph.setup(this.strippedTransactions[this.latestBlocks?.[index]?.height] || []);
|
this.blockGraph.setup(i, this.strippedTransactions[this.latestBlocks?.[i]?.height] || []);
|
||||||
});
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onTxClick(event: { tx: TransactionStripped, keyModifier: boolean }): void {
|
|
||||||
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.tx.txid}`);
|
|
||||||
if (!event.keyModifier) {
|
|
||||||
this.router.navigate([url]);
|
|
||||||
} else {
|
|
||||||
window.open(url, '_blank');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onTxHover(txid: string): void {
|
|
||||||
if (txid && txid.length) {
|
|
||||||
this.hoverTx = txid;
|
|
||||||
} else {
|
|
||||||
this.hoverTx = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,6 +48,7 @@ import { CapAddressPipe } from './pipes/cap-address-pipe/cap-address-pipe';
|
|||||||
import { StartComponent } from '../components/start/start.component';
|
import { StartComponent } from '../components/start/start.component';
|
||||||
import { TransactionsListComponent } from '../components/transactions-list/transactions-list.component';
|
import { TransactionsListComponent } from '../components/transactions-list/transactions-list.component';
|
||||||
import { BlockOverviewGraphComponent } from '../components/block-overview-graph/block-overview-graph.component';
|
import { BlockOverviewGraphComponent } from '../components/block-overview-graph/block-overview-graph.component';
|
||||||
|
import { BlockOverviewMultiComponent } from '../components/block-overview-multi/block-overview-multi.component';
|
||||||
import { BlockOverviewTooltipComponent } from '../components/block-overview-tooltip/block-overview-tooltip.component';
|
import { BlockOverviewTooltipComponent } from '../components/block-overview-tooltip/block-overview-tooltip.component';
|
||||||
import { BlockFiltersComponent } from '../components/block-filters/block-filters.component';
|
import { BlockFiltersComponent } from '../components/block-filters/block-filters.component';
|
||||||
import { AddressGroupComponent } from '../components/address-group/address-group.component';
|
import { AddressGroupComponent } from '../components/address-group/address-group.component';
|
||||||
@ -163,6 +164,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
|||||||
PreviewTitleComponent,
|
PreviewTitleComponent,
|
||||||
StartComponent,
|
StartComponent,
|
||||||
BlockOverviewGraphComponent,
|
BlockOverviewGraphComponent,
|
||||||
|
BlockOverviewMultiComponent,
|
||||||
BlockOverviewTooltipComponent,
|
BlockOverviewTooltipComponent,
|
||||||
BlockFiltersComponent,
|
BlockFiltersComponent,
|
||||||
TransactionsListComponent,
|
TransactionsListComponent,
|
||||||
@ -306,6 +308,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
|||||||
AmountComponent,
|
AmountComponent,
|
||||||
StartComponent,
|
StartComponent,
|
||||||
BlockOverviewGraphComponent,
|
BlockOverviewGraphComponent,
|
||||||
|
BlockOverviewMultiComponent,
|
||||||
BlockOverviewTooltipComponent,
|
BlockOverviewTooltipComponent,
|
||||||
BlockFiltersComponent,
|
BlockFiltersComponent,
|
||||||
TransactionsListComponent,
|
TransactionsListComponent,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user