import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, OnInit, OnDestroy, OnChanges, ChangeDetectionStrategy, NgZone } from '@angular/core'; import { StateService } from 'src/app/services/state.service'; import { MempoolBlockWithTransactions, MempoolBlockDelta, TransactionStripped } from 'src/app/interfaces/websocket.interface'; import { Observable, Subscription, BehaviorSubject } from 'rxjs'; import { WebsocketService } from 'src/app/services/websocket.service'; import { FastVertexArray } from './fast-vertex-array'; import BlockScene from './block-scene'; import TxSprite from './tx-sprite'; import TxView from './tx-view'; @Component({ selector: 'app-mempool-block-overview', templateUrl: './mempool-block-overview.component.html', styleUrls: ['./mempool-block-overview.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChanges { @Input() index: number; @Output() txPreviewEvent = new EventEmitter() @ViewChild('blockCanvas') canvas: ElementRef; gl: WebGLRenderingContext; animationFrameRequest: number; displayWidth: number; displayHeight: number; shaderProgram: WebGLProgram; vertexArray: FastVertexArray; running: boolean; scene: BlockScene; hoverTx: TxView | void; selectedTx: TxView | void; lastBlockHeight: number; blockIndex: number; isLoading$ = new BehaviorSubject(true); blockSub: Subscription; deltaSub: Subscription; constructor( public stateService: StateService, private websocketService: WebsocketService, readonly _ngZone: NgZone, ) { this.vertexArray = new FastVertexArray(512, TxSprite.dataSize) } ngOnInit(): void { this.blockSub = this.stateService.mempoolBlock$.subscribe((block) => { this.replaceBlock(block) }) this.deltaSub = this.stateService.mempoolBlockDelta$.subscribe((delta) => { this.updateBlock(delta) }) } ngAfterViewInit(): void { this.canvas.nativeElement.addEventListener("webglcontextlost", this.handleContextLost, false) this.canvas.nativeElement.addEventListener("webglcontextrestored", this.handleContextRestored, false) this.gl = this.canvas.nativeElement.getContext('webgl') this.initCanvas() this.resizeCanvas() } ngOnChanges(changes): void { if (changes.index) { this.clearBlock(changes.index.currentValue > changes.index.previousValue ? 'right' : 'left') this.isLoading$.next(true) this.websocketService.startTrackMempoolBlock(changes.index.currentValue); } } ngOnDestroy(): void { this.blockSub.unsubscribe(); this.deltaSub.unsubscribe(); this.websocketService.stopTrackMempoolBlock(); } clearBlock(direction): void { if (this.scene) { this.scene.exit(direction) } this.hoverTx = null this.selectedTx = null this.txPreviewEvent.emit(null) } replaceBlock(block: MempoolBlockWithTransactions): void { if (!this.scene) { this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: 75, blockLimit: this.stateService.blockVSize, vertexArray: this.vertexArray }) } const blockMined = (this.stateService.latestBlockHeight > this.lastBlockHeight) if (this.blockIndex != this.index) { const direction = (this.blockIndex == null || this.index < this.blockIndex) ? 'left' : 'right' this.scene.enter(block.transactions, direction) } else { this.scene.replace(block.transactions, blockMined ? 'right' : 'left') } this.lastBlockHeight = this.stateService.latestBlockHeight this.blockIndex = this.index this.isLoading$.next(false) } updateBlock(delta: MempoolBlockDelta): void { if (!this.scene) { this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: 75, blockLimit: this.stateService.blockVSize, vertexArray: this.vertexArray }) } const blockMined = (this.stateService.latestBlockHeight > this.lastBlockHeight) if (this.blockIndex != this.index) { const direction = (this.blockIndex == null || this.index < this.blockIndex) ? 'left' : 'right' this.scene.exit(direction) this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: 75, blockLimit: this.stateService.blockVSize, vertexArray: this.vertexArray }) this.scene.enter(delta.added, direction) } else { this.scene.update(delta.added, delta.removed, blockMined ? 'right' : 'left', blockMined) } this.lastBlockHeight = this.stateService.latestBlockHeight this.blockIndex = this.index this.isLoading$.next(false) } initCanvas (): void { 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 } handleContextRestored(event): void { this.initCanvas() } @HostListener('window:resize', ['$event']) resizeCanvas(): void { this.displayWidth = this.canvas.nativeElement.parentElement.clientWidth this.displayHeight = this.canvas.nativeElement.parentElement.clientHeight this.canvas.nativeElement.width = this.displayWidth this.canvas.nativeElement.height = this.displayHeight if (this.gl) this.gl.viewport(0, 0, this.canvas.nativeElement.width, this.canvas.nativeElement.height) if (this.scene) this.scene.resize({ width: this.displayWidth, height: this.displayHeight }) } compileShader(src, type): WebGLShader { let 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 { let program = this.gl.createProgram() shaderInfo.forEach((desc) => { let 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.run()) } run (now?: DOMHighResTimeStamp): void { if (!now) { now = performance.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) /* 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) } /* LOOP */ if (this.running) { if (this.animationFrameRequest) { cancelAnimationFrame(this.animationFrameRequest) this.animationFrameRequest = null } this.animationFrameRequest = requestAnimationFrame(() => this.run()); } } @HostListener('click', ['$event']) onClick(event) { this.setPreviewTx(event.offsetX, event.offsetY, true) } @HostListener('pointermove', ['$event']) onPointerMove(event) { this.setPreviewTx(event.offsetX, event.offsetY, false) } @HostListener('pointerleave', ['$event']) onPointerLeave(event) { this.setPreviewTx(-1, -1, false) } setPreviewTx(x: number, y: number, clicked: boolean = false) { if (this.scene && (!this.selectedTx || clicked)) { const selected = this.scene.getTxAt({ x, y }) const currentPreview = this.selectedTx || this.hoverTx if (selected !== currentPreview) { if (currentPreview) currentPreview.setHover(false) if (selected) { selected.setHover(true) this.txPreviewEvent.emit({ txid: selected.txid, fee: selected.fee, vsize: selected.vsize, value: selected.value }) if (clicked) this.selectedTx = selected else this.hoverTx = selected } else { if (clicked) { this.selectedTx = null } this.hoverTx = null this.txPreviewEvent.emit(null) } } else if (clicked) { if (selected === this.selectedTx) { this.hoverTx = this.selectedTx this.selectedTx = null } else { this.selectedTx = selected } } } } } // 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++) { let 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; } `