diff --git a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts index c596691ad..953ed5715 100644 --- a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts +++ b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, NgZone, AfterViewInit } from '@angular/core'; +import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, NgZone, AfterViewInit, OnDestroy } from '@angular/core'; import { MempoolBlockDelta, TransactionStripped } from 'src/app/interfaces/websocket.interface'; import { WebsocketService } from 'src/app/services/websocket.service'; import { FastVertexArray } from './fast-vertex-array'; @@ -11,7 +11,7 @@ import TxView from './tx-view'; templateUrl: './block-overview-graph.component.html', styleUrls: ['./block-overview-graph.component.scss'], }) -export class BlockOverviewGraphComponent implements AfterViewInit { +export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy { @Input() isLoading: boolean; @Input() resolution: number; @Input() blockLimit: number; @@ -24,6 +24,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit { gl: WebGLRenderingContext; animationFrameRequest: number; + animationHeartBeat: number; displayWidth: number; displayHeight: number; cssWidth: number; @@ -50,34 +51,46 @@ export class BlockOverviewGraphComponent implements AfterViewInit { this.resizeCanvas(); } + ngOnDestroy(): void { + if (this.animationFrameRequest) { + cancelAnimationFrame(this.animationFrameRequest); + clearTimeout(this.animationHeartBeat); + } + } + clear(direction): void { this.exit(direction); this.hoverTx = null; this.selectedTx = null; this.txPreviewEvent.emit(null); + this.start(); } enter(transactions: TransactionStripped[], direction: string): void { if (this.scene) { this.scene.enter(transactions, direction); + this.start(); } } exit(direction: string): void { if (this.scene) { this.scene.exit(direction); + this.start(); } } replace(transactions: TransactionStripped[], direction: string, sort: boolean = true): void { if (this.scene) { this.scene.replace(transactions || [], direction, sort); + this.start(); } } update(add: TransactionStripped[], remove: string[], direction: string = 'left', resetLayout: boolean = false): void { if (this.scene) { this.scene.update(add, remove, direction, resetLayout); + this.start(); } } @@ -140,6 +153,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit { } if (this.scene) { this.scene.resize({ width: this.displayWidth, height: this.displayHeight }); + this.start(); } else { this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution, blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray }); @@ -182,44 +196,64 @@ export class BlockOverviewGraphComponent implements AfterViewInit { start(): void { this.running = true; - this.ngZone.runOutsideAngular(() => this.run()); + 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(); } + // skip re-render if there's no change to the scene + if (this.scene) { + /* 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); - /* 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 + }); - /* 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(); - 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); + 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); + } + } } /* LOOP */ - if (this.running) { - if (this.animationFrameRequest) { - cancelAnimationFrame(this.animationFrameRequest); - this.animationFrameRequest = null; + if (this.running && this.scene && now <= (this.scene.animateUntil + 500)) { + this.doRun(); + } else { + if (this.animationHeartBeat) { + clearTimeout(this.animationHeartBeat); } - this.animationFrameRequest = requestAnimationFrame(() => this.run()); + this.animationHeartBeat = window.setTimeout(() => { + this.start(); + }, 1000); } } @@ -246,11 +280,15 @@ export class BlockOverviewGraphComponent implements AfterViewInit { const currentPreview = this.selectedTx || this.hoverTx; if (selected !== currentPreview) { - if (currentPreview) { - currentPreview.setHover(false); + if (currentPreview && this.scene) { + this.scene.setHover(currentPreview, false); + this.start(); } if (selected) { - selected.setHover(true); + if (selected && this.scene) { + this.scene.setHover(selected, true); + this.start(); + } this.txPreviewEvent.emit({ txid: selected.txid, fee: selected.fee, diff --git a/frontend/src/app/components/block-overview-graph/block-scene.ts b/frontend/src/app/components/block-overview-graph/block-scene.ts index ffae2ed6a..fc5bfff8e 100644 --- a/frontend/src/app/components/block-overview-graph/block-scene.ts +++ b/frontend/src/app/components/block-overview-graph/block-scene.ts @@ -1,7 +1,7 @@ import { FastVertexArray } from './fast-vertex-array'; import TxView from './tx-view'; import { TransactionStripped } from 'src/app/interfaces/websocket.interface'; -import { Position, Square } from './sprite-types'; +import { Position, Square, ViewUpdateParams } from './sprite-types'; export default class BlockScene { scene: { count: number, offset: { x: number, y: number}}; @@ -19,6 +19,7 @@ export default class BlockScene { unitWidth: number; initialised: boolean; layout: BlockLayout; + animateUntil = 0; dirty: boolean; constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray }: @@ -41,7 +42,7 @@ export default class BlockScene { this.dirty = true; if (this.initialised && this.scene) { - this.updateAll(performance.now()); + this.updateAll(performance.now(), 50); } } @@ -103,7 +104,7 @@ export default class BlockScene { }); } - this.updateAll(startTime, direction); + this.updateAll(startTime, 200, direction); } update(add: TransactionStripped[], remove: string[], direction: string = 'left', resetLayout: boolean = false): void { @@ -139,7 +140,7 @@ export default class BlockScene { this.layout.applyGravity(); } - this.updateAll(startTime, direction); + this.updateAll(startTime, 100, direction); } // return the tx at this screen position, if any @@ -152,6 +153,10 @@ export default class BlockScene { } } + setHover(tx: TxView, value: boolean): void { + this.animateUntil = Math.max(this.animateUntil, tx.setHover(value)); + } + private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray }: { width: number, height: number, resolution: number, blockLimit: number, orientation: string, flip: boolean, vertexArray: FastVertexArray } @@ -169,7 +174,7 @@ export default class BlockScene { }; // Set the scale of the visualization (with a 5% margin) - this.vbytesPerUnit = blockLimit / Math.pow(resolution / 1.05, 2); + this.vbytesPerUnit = blockLimit / Math.pow(resolution / 1.02, 2); this.gridWidth = resolution; this.gridHeight = resolution; this.resize({ width, height }); @@ -181,23 +186,21 @@ export default class BlockScene { this.dirty = true; } - private insert(tx: TxView, startTime: number, direction: string = 'left'): void { - this.txs[tx.txid] = tx; - this.place(tx); - this.updateTx(tx, startTime, direction); + private applyTxUpdate(tx: TxView, update: ViewUpdateParams): void { + this.animateUntil = Math.max(this.animateUntil, tx.update(update)); } - private updateTx(tx: TxView, startTime: number, direction: string = 'left'): void { + private updateTx(tx: TxView, startTime: number, delay: number, direction: string = 'left'): void { if (tx.dirty || this.dirty) { this.saveGridToScreenPosition(tx); - this.setTxOnScreen(tx, startTime, direction); + this.setTxOnScreen(tx, startTime, delay, direction); } } - private setTxOnScreen(tx: TxView, startTime: number, direction: string = 'left'): void { + private setTxOnScreen(tx: TxView, startTime: number, delay: number = 50, direction: string = 'left'): void { if (!tx.initialised) { const txColor = tx.getColor(); - tx.update({ + this.applyTxUpdate(tx, { display: { position: { x: tx.screenPosition.x + (direction === 'right' ? -this.width : (direction === 'left' ? this.width : 0)) * 1.4, @@ -209,35 +212,35 @@ export default class BlockScene { start: startTime, delay: 0, }); - tx.update({ + this.applyTxUpdate(tx, { display: { position: tx.screenPosition, color: txColor }, duration: 1000, start: startTime, - delay: 50, + delay, }); } else { - tx.update({ + this.applyTxUpdate(tx, { display: { position: tx.screenPosition }, duration: 1000, minDuration: 500, start: startTime, - delay: 50, + delay, adjust: true }); } } - private updateAll(startTime: number, direction: string = 'left'): void { + private updateAll(startTime: number, delay: number = 50, direction: string = 'left'): void { this.scene.count = 0; const ids = this.getTxList(); startTime = startTime || performance.now(); for (const id of ids) { - this.updateTx(this.txs[id], startTime, direction); + this.updateTx(this.txs[id], startTime, delay, direction); } this.dirty = false; } @@ -246,7 +249,7 @@ export default class BlockScene { const tx = this.txs[id]; if (tx) { this.layout.remove(tx); - tx.update({ + this.applyTxUpdate(tx, { display: { position: { x: tx.screenPosition.x + (direction === 'right' ? this.width : (direction === 'left' ? -this.width : 0)) * 1.4, @@ -319,7 +322,7 @@ export default class BlockScene { } } - screenToGrid(position: Position): Position { + private screenToGrid(position: Position): Position { let x = position.x; let y = this.height - position.y; let t; diff --git a/frontend/src/app/components/block-overview-graph/fast-vertex-array.ts b/frontend/src/app/components/block-overview-graph/fast-vertex-array.ts index 0581fb391..bc0900238 100644 --- a/frontend/src/app/components/block-overview-graph/fast-vertex-array.ts +++ b/frontend/src/app/components/block-overview-graph/fast-vertex-array.ts @@ -18,6 +18,7 @@ export class FastVertexArray { data: Float32Array; freeSlots: number[]; lastSlot: number; + dirty = false; constructor(length, stride) { this.length = length; @@ -27,6 +28,7 @@ export class FastVertexArray { this.data = new Float32Array(this.length * this.stride); this.freeSlots = []; this.lastSlot = 0; + this.dirty = true; } insert(sprite: TxSprite): number { @@ -44,6 +46,7 @@ export class FastVertexArray { } this.sprites[position] = sprite; return position; + this.dirty = true; } remove(index: number): void { @@ -54,14 +57,17 @@ export class FastVertexArray { if (this.length > 2048 && this.count < (this.length * 0.4)) { this.compact(); } + this.dirty = true; } setData(index: number, dataChunk: number[]): void { this.data.set(dataChunk, (index * this.stride)); + this.dirty = true; } clearData(index: number): void { this.data.fill(0, (index * this.stride), ((index + 1) * this.stride)); + this.dirty = true; } getData(index: number): Float32Array { @@ -73,6 +79,7 @@ export class FastVertexArray { const newData = new Float32Array(this.length * this.stride); newData.set(this.data); this.data = newData; + this.dirty = true; } compact(): void { @@ -97,6 +104,7 @@ export class FastVertexArray { this.freeSlots = []; this.lastSlot = i; } + this.dirty = true; } getVertexData(): Float32Array { diff --git a/frontend/src/app/components/block-overview-graph/tx-view.ts b/frontend/src/app/components/block-overview-graph/tx-view.ts index cb98bef72..5093f480b 100644 --- a/frontend/src/app/components/block-overview-graph/tx-view.ts +++ b/frontend/src/app/components/block-overview-graph/tx-view.ts @@ -82,8 +82,10 @@ export default class TxView implements TransactionStripped { delay: additional milliseconds to wait before starting jitter: if set, adds a random amount to the delay, adjust: if true, modify an in-progress transition instead of replacing it + + returns minimum transition end time */ - update(params: ViewUpdateParams): void { + update(params: ViewUpdateParams): number { if (params.jitter) { params.delay += (Math.random() * params.jitter); } @@ -96,6 +98,7 @@ export default class TxView implements TransactionStripped { ); // apply any pending hover event if (this.hover) { + params.duration = Math.max(params.duration, hoverTransitionTime); this.sprite.update({ ...this.hoverColor, duration: hoverTransitionTime, @@ -109,10 +112,12 @@ export default class TxView implements TransactionStripped { ); } this.dirty = false; + return (params.start || performance.now()) + (params.delay || 0) + (params.duration || 0); } // Temporarily override the tx color - setHover(hoverOn: boolean, color: Color | void = defaultHoverColor): void { + // returns minimum transition end time + setHover(hoverOn: boolean, color: Color | void = defaultHoverColor): number { if (hoverOn) { this.hover = true; this.hoverColor = color; @@ -131,6 +136,7 @@ export default class TxView implements TransactionStripped { } } this.dirty = false; + return performance.now() + hoverTransitionTime; } getColor(): Color { diff --git a/frontend/src/app/components/block/block.component.ts b/frontend/src/app/components/block/block.component.ts index 4ffacabaa..6394a624e 100644 --- a/frontend/src/app/components/block/block.component.ts +++ b/frontend/src/app/components/block/block.component.ts @@ -216,7 +216,6 @@ export class BlockComponent implements OnInit, OnDestroy { return of([]); }), switchMap((transactions) => { - console.log('overview loaded: ', prevBlock && prevBlock.height, block.height); if (prevBlock) { return of({ transactions, direction: (prevBlock.height < block.height) ? 'right' : 'left' }); } else {