From 3b459b6857e7a3610bd13a6acc7d644df7e6a301 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sat, 15 Apr 2023 05:26:18 +0900 Subject: [PATCH 01/12] hourly blocks clock faces --- frontend/src/app/app-routing.module.ts | 10 ++ .../block-overview-graph/block-scene.ts | 2 +- .../clock-face/clock-face.component.html | 93 ++++++++++++++ .../clock-face/clock-face.component.scss | 23 ++++ .../clock-face/clock-face.component.ts | 15 +++ .../components/clock/clock-a.component.html | 19 +++ .../app/components/clock/clock-a.component.ts | 57 +++++++++ .../components/clock/clock-b.component.html | 15 +++ .../app/components/clock/clock-b.component.ts | 58 +++++++++ .../app/components/clock/clock.component.scss | 119 ++++++++++++++++++ frontend/src/app/graphs/graphs.module.ts | 4 +- frontend/src/app/shared/shared.module.ts | 15 +++ frontend/src/index.mempool.html | 2 +- 13 files changed, 428 insertions(+), 4 deletions(-) create mode 100644 frontend/src/app/components/clock-face/clock-face.component.html create mode 100644 frontend/src/app/components/clock-face/clock-face.component.scss create mode 100644 frontend/src/app/components/clock-face/clock-face.component.ts create mode 100644 frontend/src/app/components/clock/clock-a.component.html create mode 100644 frontend/src/app/components/clock/clock-a.component.ts create mode 100644 frontend/src/app/components/clock/clock-b.component.html create mode 100644 frontend/src/app/components/clock/clock-b.component.ts create mode 100644 frontend/src/app/components/clock/clock.component.scss diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 06334c5b5..0146fb535 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -4,6 +4,8 @@ import { AppPreloadingStrategy } from './app.preloading-strategy' import { StartComponent } from './components/start/start.component'; import { TransactionComponent } from './components/transaction/transaction.component'; import { BlockComponent } from './components/block/block.component'; +import { ClockAComponent } from './components/clock/clock-a.component'; +import { ClockBComponent } from './components/clock/clock-b.component'; import { AddressComponent } from './components/address/address.component'; import { MasterPageComponent } from './components/master-page/master-page.component'; import { AboutComponent } from './components/about/about.component'; @@ -355,6 +357,14 @@ let routes: Routes = [ }, ], }, + { + path: 'clock-face-a', + component: ClockAComponent, + }, + { + path: 'clock-face-b', + component: ClockBComponent, + }, { path: 'status', data: { networks: ['bitcoin', 'liquid'] }, 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 7fb0a1e99..5ab0eefea 100644 --- a/frontend/src/app/components/block-overview-graph/block-scene.ts +++ b/frontend/src/app/components/block-overview-graph/block-scene.ts @@ -34,7 +34,7 @@ export default class BlockScene { this.width = width; this.height = height; this.gridSize = this.width / this.gridWidth; - this.unitPadding = width / 500; + this.unitPadding = this.gridSize / 5; this.unitWidth = this.gridSize - (this.unitPadding * 2); this.dirty = true; diff --git a/frontend/src/app/components/clock-face/clock-face.component.html b/frontend/src/app/components/clock-face/clock-face.component.html new file mode 100644 index 000000000..a5db1538c --- /dev/null +++ b/frontend/src/app/components/clock-face/clock-face.component.html @@ -0,0 +1,93 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/frontend/src/app/components/clock-face/clock-face.component.scss b/frontend/src/app/components/clock-face/clock-face.component.scss new file mode 100644 index 000000000..fa73ad90a --- /dev/null +++ b/frontend/src/app/components/clock-face/clock-face.component.scss @@ -0,0 +1,23 @@ +.clock-face { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + width: 100%; + height: 100%; + + .cut-out, .demo-dial { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + width: 100%; + height: 100%; + + .face { + fill: #11131f; + } + } +} \ No newline at end of file diff --git a/frontend/src/app/components/clock-face/clock-face.component.ts b/frontend/src/app/components/clock-face/clock-face.component.ts new file mode 100644 index 000000000..fcd4b75cc --- /dev/null +++ b/frontend/src/app/components/clock-face/clock-face.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-clock-face', + templateUrl: './clock-face.component.html', + styleUrls: ['./clock-face.component.scss'], +}) +export class ClockFaceComponent implements OnInit { + + constructor() {} + + ngOnInit(): void { + // initialize stuff + } +} diff --git a/frontend/src/app/components/clock/clock-a.component.html b/frontend/src/app/components/clock/clock-a.component.html new file mode 100644 index 000000000..c7eea67c9 --- /dev/null +++ b/frontend/src/app/components/clock/clock-a.component.html @@ -0,0 +1,19 @@ +
+ +
+ +
+
+
+
+
+
+
+
+
+

{{ block.height }}

+
+
+
+
+
\ No newline at end of file diff --git a/frontend/src/app/components/clock/clock-a.component.ts b/frontend/src/app/components/clock/clock-a.component.ts new file mode 100644 index 000000000..0242e0ab8 --- /dev/null +++ b/frontend/src/app/components/clock/clock-a.component.ts @@ -0,0 +1,57 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { StateService } from '../../services/state.service'; +import { BlockExtended } from '../../interfaces/node-api.interface'; +import { WebsocketService } from '../../services/websocket.service'; + +@Component({ + selector: 'app-clock-a', + templateUrl: './clock-a.component.html', + styleUrls: ['./clock.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ClockAComponent implements OnInit { + blocksSubscription: Subscription; + block: BlockExtended; + blockStyle; + + gradientColors = { + '': ['#9339f4', '#105fb0'], + bisq: ['#9339f4', '#105fb0'], + liquid: ['#116761', '#183550'], + 'liquidtestnet': ['#494a4a', '#272e46'], + testnet: ['#1d486f', '#183550'], + signet: ['#6f1d5d', '#471850'], + }; + + constructor( + public stateService: StateService, + private websocketService: WebsocketService, + private cd: ChangeDetectorRef, + ) {} + + ngOnInit(): void { + this.websocketService.want(['blocks']); + this.blocksSubscription = this.stateService.blocks$ + .subscribe(([block]) => { + if (block) { + this.block = block; + this.blockStyle = this.getStyleForBlock(this.block); + this.cd.markForCheck(); + } + }); + } + + getStyleForBlock(block: BlockExtended) { + const greenBackgroundHeight = 100 - (block.weight / this.stateService.env.BLOCK_WEIGHT_UNITS) * 100; + + return { + background: `repeating-linear-gradient( + #2d3348, + #2d3348 ${greenBackgroundHeight}%, + ${this.gradientColors[''][0]} ${Math.max(greenBackgroundHeight, 0)}%, + ${this.gradientColors[''][1]} 100% + )`, + }; + } +} diff --git a/frontend/src/app/components/clock/clock-b.component.html b/frontend/src/app/components/clock/clock-b.component.html new file mode 100644 index 000000000..f3d65562d --- /dev/null +++ b/frontend/src/app/components/clock/clock-b.component.html @@ -0,0 +1,15 @@ +
+ +
+ +
+ +
+
+
+

{{ block.height }}

+
+
+
+
+
\ No newline at end of file diff --git a/frontend/src/app/components/clock/clock-b.component.ts b/frontend/src/app/components/clock/clock-b.component.ts new file mode 100644 index 000000000..95ae803b8 --- /dev/null +++ b/frontend/src/app/components/clock/clock-b.component.ts @@ -0,0 +1,58 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, OnInit } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { StateService } from '../../services/state.service'; +import { BlockExtended } from '../../interfaces/node-api.interface'; +import { WebsocketService } from '../../services/websocket.service'; + +@Component({ + selector: 'app-clock-b', + templateUrl: './clock-b.component.html', + styleUrls: ['./clock.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ClockBComponent implements OnInit { + blocksSubscription: Subscription; + block: BlockExtended; + blockSizerStyle; + + gradientColors = { + '': ['#9339f4', '#105fb0'], + bisq: ['#9339f4', '#105fb0'], + liquid: ['#116761', '#183550'], + 'liquidtestnet': ['#494a4a', '#272e46'], + testnet: ['#1d486f', '#183550'], + signet: ['#6f1d5d', '#471850'], + }; + + constructor( + public stateService: StateService, + private websocketService: WebsocketService, + private cd: ChangeDetectorRef, + ) {} + + ngOnInit(): void { + this.resizeCanvas(); + this.websocketService.want(['blocks']); + this.blocksSubscription = this.stateService.blocks$ + .subscribe(([block]) => { + if (block) { + this.block = block; + this.cd.markForCheck(); + } + }); + } + + @HostListener('window:resize', ['$event']) + resizeCanvas(): void { + const screenSize = Math.min(window.innerWidth, window.innerHeight); + const baseSize = 0.92 * screenSize; + const size = Math.ceil(baseSize / 75) * 75; + const margin = screenSize - size; + this.blockSizerStyle = { + transform: `translate(${margin}px, ${margin}px)`, + width: `${size}px`, + height: `${size}px`, + }; + this.cd.markForCheck(); + } +} diff --git a/frontend/src/app/components/clock/clock.component.scss b/frontend/src/app/components/clock/clock.component.scss new file mode 100644 index 000000000..385df2ea0 --- /dev/null +++ b/frontend/src/app/components/clock/clock.component.scss @@ -0,0 +1,119 @@ +.clock-wrapper { + --clock-width: min(100vw, 100vh); + position: relative; + width: 100vw; + max-width: var(--clock-width); + height: 100vh; + max-height: var(--clock-width); + margin: auto; +} + +.title-wrapper { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + width: 100%; + height: 100%; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + + .block-height { + font-size: calc(0.2 * var(--clock-width)); + padding: 0; + margin: 0; + background: radial-gradient(rgba(0,0,0,0.5), transparent 67%); + padding: calc(0.05 * var(--clock-width)) calc(0.15 * var(--clock-width)); + } +} + +.block-wrapper { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + width: 100%; + height: 100%; + + .block-sizer { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + } + + .fader { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + background: radial-gradient(transparent 0%, transparent 55%, #11131f 65%, #11131f 100%); + } + + .block-cube { + --side-width: calc(0.4 * var(--clock-width)); + --half-side: calc(0.2 * var(--clock-width)); + --neg-half-side: calc(-0.2 * var(--clock-width)); + transform-style: preserve-3d; + animation: block-spin 60s infinite linear; + position: absolute; + z-index: -1; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: var(--side-width); + height: var(--side-width); + + .side { + width: var(--side-width); + height: var(--side-width); + line-height: 100px; + text-align: center; + background: #232838; + display: block; + position: absolute; + } + + .side.top { + transform: rotateX(90deg); + margin-top: var(--neg-half-side); + } + + .side.bottom { + background: #105fb0; + transform: rotateX(-90deg); + margin-top: var(--half-side); + } + + .side.right { + transform: rotateY(90deg); + margin-left: var(--half-side); + } + + .side.left { + transform: rotateY(-90deg); + margin-left: var(--neg-half-side); + } + + .side.front { + transform: translateZ(var(--half-side)); + } + + .side.back { + transform: translateZ(var(--neg-half-side)); + } + } +} + + + +@keyframes block-spin { + 0% {transform: translate(-50%, -50%) rotateX(-20deg) rotateY(0deg);} + 100% {transform: translate(-50%, -50%) rotateX(-20deg) rotateY(-360deg);} +} \ No newline at end of file diff --git a/frontend/src/app/graphs/graphs.module.ts b/frontend/src/app/graphs/graphs.module.ts index 4cb803888..a7e627736 100644 --- a/frontend/src/app/graphs/graphs.module.ts +++ b/frontend/src/app/graphs/graphs.module.ts @@ -14,7 +14,7 @@ import { LbtcPegsGraphComponent } from '../components/lbtc-pegs-graph/lbtc-pegs- import { GraphsComponent } from '../components/graphs/graphs.component'; import { StatisticsComponent } from '../components/statistics/statistics.component'; import { MempoolBlockComponent } from '../components/mempool-block/mempool-block.component'; -import { MempoolBlockOverviewComponent } from '../components/mempool-block-overview/mempool-block-overview.component'; +// import { MempoolBlockOverviewComponent } from '../components/mempool-block-overview/mempool-block-overview.component'; import { PoolRankingComponent } from '../components/pool-ranking/pool-ranking.component'; import { PoolComponent } from '../components/pool/pool.component'; import { TelevisionComponent } from '../components/television/television.component'; @@ -42,7 +42,7 @@ import { CommonModule } from '@angular/common'; BlockFeeRatesGraphComponent, BlockSizesWeightsGraphComponent, FeeDistributionGraphComponent, - MempoolBlockOverviewComponent, + // MempoolBlockOverviewComponent, IncomingTransactionsGraphComponent, MempoolGraphComponent, LbtcPegsGraphComponent, diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 7d3e51d20..4dd915889 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -90,6 +90,11 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati import { TestnetAlertComponent } from './components/testnet-alert/testnet-alert.component'; import { GlobalFooterComponent } from './components/global-footer/global-footer.component'; +import { MempoolBlockOverviewComponent } from '../components/mempool-block-overview/mempool-block-overview.component'; +import { ClockFaceComponent } from '../components/clock-face/clock-face.component'; +import { ClockAComponent } from '../components/clock/clock-a.component'; +import { ClockBComponent } from '../components/clock/clock-b.component'; + @NgModule({ declarations: [ ClipboardComponent, @@ -172,6 +177,11 @@ import { GlobalFooterComponent } from './components/global-footer/global-footer. GeolocationComponent, TestnetAlertComponent, GlobalFooterComponent, + + MempoolBlockOverviewComponent, + ClockAComponent, + ClockBComponent, + ClockFaceComponent, ], imports: [ CommonModule, @@ -279,6 +289,11 @@ import { GlobalFooterComponent } from './components/global-footer/global-footer. GeolocationComponent, PreviewTitleComponent, GlobalFooterComponent, + + MempoolBlockOverviewComponent, + ClockAComponent, + ClockBComponent, + ClockFaceComponent, ] }) export class SharedModule { diff --git a/frontend/src/index.mempool.html b/frontend/src/index.mempool.html index 60f1b4421..02765c0ba 100644 --- a/frontend/src/index.mempool.html +++ b/frontend/src/index.mempool.html @@ -32,7 +32,7 @@ - + From 61531171c91b6a7b20f3a26c4bb7237f8e01d10f Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 19 Apr 2023 00:21:02 +0900 Subject: [PATCH 02/12] clocktower top blocks & layout adjustment --- .../block-overview-graph/block-scene.ts | 7 +- .../blockchain-blocks.component.html | 71 +++---- .../blockchain-blocks.component.scss | 6 +- .../blockchain-blocks.component.ts | 1 + .../clock-face/clock-face.component.html | 185 +++++++++--------- .../clock-face/clock-face.component.scss | 44 +++-- .../clock-face/clock-face.component.ts | 26 ++- .../components/clock/clock-a.component.html | 36 ++-- .../components/clock/clock-b.component.html | 28 ++- .../app/components/clock/clock-b.component.ts | 7 +- .../app/components/clock/clock.component.scss | 12 +- .../clockchain/clockchain.component.html | 11 ++ .../clockchain/clockchain.component.scss | 120 ++++++++++++ .../clockchain/clockchain.component.ts | 50 +++++ .../mempool-blocks.component.html | 88 ++++++--- .../mempool-blocks.component.scss | 6 +- .../mempool-blocks.component.ts | 12 +- frontend/src/app/shared/shared.module.ts | 3 + 18 files changed, 489 insertions(+), 224 deletions(-) create mode 100644 frontend/src/app/components/clockchain/clockchain.component.html create mode 100644 frontend/src/app/components/clockchain/clockchain.component.scss create mode 100644 frontend/src/app/components/clockchain/clockchain.component.ts 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 5ab0eefea..f1c1b4d77 100644 --- a/frontend/src/app/components/block-overview-graph/block-scene.ts +++ b/frontend/src/app/components/block-overview-graph/block-scene.ts @@ -34,8 +34,9 @@ export default class BlockScene { this.width = width; this.height = height; this.gridSize = this.width / this.gridWidth; - this.unitPadding = this.gridSize / 5; - this.unitWidth = this.gridSize - (this.unitPadding * 2); + this.unitPadding = Math.max(1, Math.floor(this.gridSize / 2.5)); + this.unitWidth = this.gridSize - (this.unitPadding); + console.log(this.gridSize, this.unitPadding, this.unitWidth); this.dirty = true; if (this.initialised && this.scene) { @@ -342,7 +343,7 @@ export default class BlockScene { private gridToScreen(position: Square | void): Square { if (position) { const slotSize = (position.s * this.gridSize); - const squareSize = slotSize - (this.unitPadding * 2); + const squareSize = slotSize - (this.unitPadding); // The grid is laid out notionally left-to-right, bottom-to-top, // so we rotate and/or flip the y axis to match the target configuration. diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html index 373605667..695e7c97d 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html @@ -1,53 +1,56 @@ -
  -
+
-
- ~{{ block?.extras?.medianFee | number:feeRounding }} sat/vB -
- -
-   + +
+ ~{{ block?.extras?.medianFee | number:feeRounding }} sat/vB
- -
- {{ block?.extras?.feeRange?.[0] | number:feeRounding }} - {{ - block?.extras?.feeRange[block?.extras?.feeRange?.length - 1] | number:feeRounding }} sat/vB -
- -
-   + +
+   +
+
+
+ {{ block?.extras?.feeRange?.[0] | number:feeRounding }} - {{ + block?.extras?.feeRange[block?.extras?.feeRange?.length - 1] | number:feeRounding }} sat/vB
- -
- -
-
-
- - {{ i }} transaction - {{ i }} transactions -
-
-
+ +
+   +
+
+
+ +
+
+
+ + {{ i }} transaction + {{ i }} transactions +
+
+
+
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/frontend/src/app/components/clock-face/clock-face.component.scss b/frontend/src/app/components/clock-face/clock-face.component.scss index fa73ad90a..66d53784f 100644 --- a/frontend/src/app/components/clock-face/clock-face.component.scss +++ b/frontend/src/app/components/clock-face/clock-face.component.scss @@ -1,4 +1,4 @@ -.clock-face { +.clock-wrapper { position: absolute; left: 0; right: 0; @@ -6,18 +6,40 @@ bottom: 0; width: 100%; height: 100%; + display: flex; + flex-direction: column; + justify-content: flex-start; - .cut-out, .demo-dial { - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - width: 100%; - height: 100%; + .clockchain-bar, .clock-face { + flex-shrink: 0; + flex-grow: 0; + } - .face { - fill: #11131f; + .clockchain-bar { + position: relative; + height: 15.625%; + // background: #1d1f31; + // box-shadow: 0 0 15px #000; + } + + .clock-face { + position: relative; + height: 84.375%; + margin: auto; + overflow: hidden; + + .cut-out, .demo-dial { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + width: 100%; + height: 100%; + + .face { + fill: #11131f; + } } } } \ No newline at end of file diff --git a/frontend/src/app/components/clock-face/clock-face.component.ts b/frontend/src/app/components/clock-face/clock-face.component.ts index fcd4b75cc..747b6221c 100644 --- a/frontend/src/app/components/clock-face/clock-face.component.ts +++ b/frontend/src/app/components/clock-face/clock-face.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, HostListener, OnInit } from '@angular/core'; @Component({ selector: 'app-clock-face', @@ -6,10 +6,34 @@ import { Component, OnInit } from '@angular/core'; styleUrls: ['./clock-face.component.scss'], }) export class ClockFaceComponent implements OnInit { + size: number; + wrapperStyle; + chainStyle; + faceStyle; + showDial: boolean = false; constructor() {} ngOnInit(): void { // initialize stuff + this.resizeCanvas(); + } + + @HostListener('window:resize', ['$event']) + resizeCanvas(): void { + this.size = Math.min(window.innerWidth, 0.78125 * window.innerHeight); + this.wrapperStyle = { + '--clock-width': `${this.size}px` + }; + const scaleFactor = window.innerWidth / 1390; + this.chainStyle = { + transform: `translate(2vw, 0.5vw) scale(${scaleFactor})`, + transformOrigin: 'top left', + }; + + this.faceStyle = { + width: `${this.size}px`, + height: `${this.size}px`, + }; } } diff --git a/frontend/src/app/components/clock/clock-a.component.html b/frontend/src/app/components/clock/clock-a.component.html index c7eea67c9..2029b9f24 100644 --- a/frontend/src/app/components/clock/clock-a.component.html +++ b/frontend/src/app/components/clock/clock-a.component.html @@ -1,19 +1,17 @@ -
- -
- -
-
-
-
-
-
-
-
-
-

{{ block.height }}

-
-
-
-
-
\ No newline at end of file + +
+ +
+
+
+
+
+
+
+
+
+

{{ block.height }}

+
+
+
+
\ No newline at end of file diff --git a/frontend/src/app/components/clock/clock-b.component.html b/frontend/src/app/components/clock/clock-b.component.html index f3d65562d..887c0a1cf 100644 --- a/frontend/src/app/components/clock/clock-b.component.html +++ b/frontend/src/app/components/clock/clock-b.component.html @@ -1,15 +1,13 @@ -
- -
- -
- -
-
-
-

{{ block.height }}

-
-
-
-
-
\ No newline at end of file + +
+ +
+ +
+
+
+

{{ block.height }}

+
+
+
+
\ No newline at end of file diff --git a/frontend/src/app/components/clock/clock-b.component.ts b/frontend/src/app/components/clock/clock-b.component.ts index 95ae803b8..626f388b6 100644 --- a/frontend/src/app/components/clock/clock-b.component.ts +++ b/frontend/src/app/components/clock/clock-b.component.ts @@ -44,10 +44,9 @@ export class ClockBComponent implements OnInit { @HostListener('window:resize', ['$event']) resizeCanvas(): void { - const screenSize = Math.min(window.innerWidth, window.innerHeight); - const baseSize = 0.92 * screenSize; - const size = Math.ceil(baseSize / 75) * 75; - const margin = screenSize - size; + const clockSize = Math.min(window.innerWidth, 0.78125 * window.innerHeight); + const size = Math.ceil(clockSize / 75) * 75; + const margin = (clockSize - size) / 2; this.blockSizerStyle = { transform: `translate(${margin}px, ${margin}px)`, width: `${size}px`, diff --git a/frontend/src/app/components/clock/clock.component.scss b/frontend/src/app/components/clock/clock.component.scss index 385df2ea0..c41b33e8a 100644 --- a/frontend/src/app/components/clock/clock.component.scss +++ b/frontend/src/app/components/clock/clock.component.scss @@ -1,13 +1,3 @@ -.clock-wrapper { - --clock-width: min(100vw, 100vh); - position: relative; - width: 100vw; - max-width: var(--clock-width); - height: 100vh; - max-height: var(--clock-width); - margin: auto; -} - .title-wrapper { position: absolute; left: 0; @@ -53,7 +43,7 @@ right: 0; top: 0; bottom: 0; - background: radial-gradient(transparent 0%, transparent 55%, #11131f 65%, #11131f 100%); + background: radial-gradient(transparent 0%, transparent 48%, #11131f 62%, #11131f 100%); } .block-cube { diff --git a/frontend/src/app/components/clockchain/clockchain.component.html b/frontend/src/app/components/clockchain/clockchain.component.html new file mode 100644 index 000000000..0eabc2862 --- /dev/null +++ b/frontend/src/app/components/clockchain/clockchain.component.html @@ -0,0 +1,11 @@ +
+
+ +
+ + +
+
+
+
+
diff --git a/frontend/src/app/components/clockchain/clockchain.component.scss b/frontend/src/app/components/clockchain/clockchain.component.scss new file mode 100644 index 000000000..d6fc00090 --- /dev/null +++ b/frontend/src/app/components/clockchain/clockchain.component.scss @@ -0,0 +1,120 @@ +.divider { + width: 4px; + height: 180px; + left: 0; + top: -40px; + position: absolute; + margin-bottom: 120px; + background-image: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3cline x1='0' y1='0' x2='0' y2='100%' stroke='white' stroke-width='8' stroke-dasharray='18%2c32' stroke-dashoffset='-5' stroke-linecap='square'/%3e%3c/svg%3e"); +} + +.blockchain-wrapper { + height: 250px; + + -webkit-user-select: none; /* Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+/Edge */ + user-select: none; /* Standard */ +} + +.position-container { + position: absolute; + left: 0; + top: 75px; + transform: translateX(50vw); +} + +.position-container.liquid, .position-container.liquidtestnet { + transform: translateX(420px); +} + +.blockchain-wrapper { + .position-container { + transform: translateX(95vw); + } + .position-container.liquid, .position-container.liquidtestnet { + transform: translateX(50vw); + } + .position-container.loading { + transform: translateX(50vw); + } +} +.blockchain-wrapper.time-ltr { + .position-container { + transform: translateX(5vw); + } + .position-container.liquid, .position-container.liquidtestnet { + transform: translateX(50vw); + } + .position-container.loading { + transform: translateX(50vw); + } +} + +.black-background { + background-color: #11131f; + z-index: 100; + position: relative; +} + +.scroll-spacer { + position: absolute; + top: 0; + left: 0; + width: 1px; + height: 1px; + pointer-events: none; +} + +.loading-block { + position: absolute; + text-align: center; + margin: auto; + width: 300px; + left: -150px; + top: 0px; +} + +.time-toggle { + color: white; + font-size: 0.8rem; + position: absolute; + bottom: -1.8em; + left: 1px; + transform: translateX(-50%); + background: none; + border: none; + outline: none; + margin: 0; + padding: 0; +} + +.blockchain-wrapper.ltr-transition .blocks-wrapper, +.blockchain-wrapper.ltr-transition .position-container, +.blockchain-wrapper.ltr-transition .time-toggle { + transition: transform 1s; +} + +.blockchain-wrapper.time-ltr { + .blocks-wrapper { + transform: scaleX(-1); + } + + .time-toggle { + transform: translateX(-50%) scaleX(-1); + } +} + +:host-context(.ltr-layout) { + .blockchain-wrapper.time-ltr .blocks-wrapper, + .blockchain-wrapper .blocks-wrapper { + direction: ltr; + } +} + +:host-context(.rtl-layout) { + .blockchain-wrapper.time-ltr .blocks-wrapper, + .blockchain-wrapper .blocks-wrapper { + direction: rtl; + } +} \ No newline at end of file diff --git a/frontend/src/app/components/clockchain/clockchain.component.ts b/frontend/src/app/components/clockchain/clockchain.component.ts new file mode 100644 index 000000000..e5c63dbe4 --- /dev/null +++ b/frontend/src/app/components/clockchain/clockchain.component.ts @@ -0,0 +1,50 @@ +import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input } from '@angular/core'; +import { firstValueFrom, Subscription } from 'rxjs'; +import { StateService } from '../../services/state.service'; + +@Component({ + selector: 'app-clockchain', + templateUrl: './clockchain.component.html', + styleUrls: ['./clockchain.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ClockchainComponent implements OnInit, OnDestroy { + network: string; + timeLtrSubscription: Subscription; + timeLtr: boolean = this.stateService.timeLtr.value; + ltrTransitionEnabled = false; + connectionStateSubscription: Subscription; + loadingTip: boolean = true; + connected: boolean = true; + + constructor( + public stateService: StateService, + ) {} + + ngOnInit() { + this.network = this.stateService.network; + this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => { + this.timeLtr = !!ltr; + }); + this.connectionStateSubscription = this.stateService.connectionState$.subscribe(state => { + this.connected = (state === 2); + }) + firstValueFrom(this.stateService.chainTip$).then(tip => { + this.loadingTip = false; + }); + } + + ngOnDestroy() { + this.timeLtrSubscription.unsubscribe(); + this.connectionStateSubscription.unsubscribe(); + } + + trackByPageFn(index: number, item: { index: number }) { + return item.index; + } + + toggleTimeDirection() { + this.ltrTransitionEnabled = true; + this.stateService.timeLtr.next(!this.timeLtr); + } +} diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.html b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.html index 6e397dee7..b11fbb888 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.html +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.html @@ -1,40 +1,68 @@ - +
-
+
 
-
- ~{{ projectedBlock.medianFee | number:feeRounding }} sat/vB -
-
- {{ projectedBlock.feeRange[0] | number:feeRounding }} - {{ projectedBlock.feeRange[projectedBlock.feeRange.length - 1] | number:feeRounding }} sat/vB -
-
- -
-
-
- - {{ i }} transaction - {{ i }} transactions -
-
- - - - - - -
- -
- () - {{ i }} blocks + +
+ ~{{ projectedBlock.medianFee | number:feeRounding }} sat/vB
- +
+ {{ projectedBlock.feeRange[0] | number:feeRounding }} - {{ projectedBlock.feeRange[projectedBlock.feeRange.length - 1] | number:feeRounding }} sat/vB +
+
+ +
+
+
+ + {{ i }} transaction + {{ i }} transactions +
+
+ + + + + + +
+ +
+ () + {{ i }} blocks +
+
+ {{ projectedBlock.feeRange[0] | number:feeRounding }} - {{ projectedBlock.feeRange[projectedBlock.feeRange.length - 1] | number:feeRounding }} sat/vB +
+
+ +
+
+
+ + {{ i }} transaction + {{ i }} transactions +
+
+ + + + + + +
+ +
+ () + {{ i }} blocks +
+
+
+
diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.scss b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.scss index 565d4b302..a0140f55c 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.scss +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.scss @@ -1,7 +1,7 @@ .bitcoin-block { width: 125px; height: 125px; - transition: background 2s, right 2s, transform 1s; + transition: background 2s, right 2s, transform 1s, opacity 1s; } .block-size { @@ -100,6 +100,10 @@ background-color: #2d2825; } +.mempool-block.last-block { + opacity: 0; +} + .black-background { background-color: #11131f; z-index: 100; diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts index 91273169c..b711ce805 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener } from '@angular/core'; +import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input } from '@angular/core'; import { Subscription, Observable, fromEvent, merge, of, combineLatest } from 'rxjs'; import { MempoolBlock } from '../../interfaces/websocket.interface'; import { StateService } from '../../services/state.service'; @@ -24,6 +24,9 @@ import { animate, style, transition, trigger } from '@angular/animations'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class MempoolBlocksComponent implements OnInit, OnDestroy { + @Input() tiny: boolean = false; + @Input() count: number = null; + specialBlocks = specialBlocks; mempoolBlocks: MempoolBlock[] = []; mempoolEmptyBlocks: MempoolBlock[] = this.mountEmptyBlocks(); @@ -238,7 +241,12 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy { reduceMempoolBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] { const innerWidth = this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2; - const blocksAmount = Math.min(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT, Math.floor(innerWidth / (this.blockWidth + this.blockPadding))); + let blocksAmount; + if (this.count) { + blocksAmount = this.count; + } else { + blocksAmount = Math.min(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT, Math.floor(innerWidth / (this.blockWidth + this.blockPadding))); + } while (blocks.length > blocksAmount) { const block = blocks.pop(); const lastBlock = blocks[blocks.length - 1]; diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 4dd915889..0765298bd 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -91,6 +91,7 @@ import { TestnetAlertComponent } from './components/testnet-alert/testnet-alert. import { GlobalFooterComponent } from './components/global-footer/global-footer.component'; import { MempoolBlockOverviewComponent } from '../components/mempool-block-overview/mempool-block-overview.component'; +import { ClockchainComponent } from '../components/clockchain/clockchain.component'; import { ClockFaceComponent } from '../components/clock-face/clock-face.component'; import { ClockAComponent } from '../components/clock/clock-a.component'; import { ClockBComponent } from '../components/clock/clock-b.component'; @@ -179,6 +180,7 @@ import { ClockBComponent } from '../components/clock/clock-b.component'; GlobalFooterComponent, MempoolBlockOverviewComponent, + ClockchainComponent, ClockAComponent, ClockBComponent, ClockFaceComponent, @@ -291,6 +293,7 @@ import { ClockBComponent } from '../components/clock/clock-b.component'; GlobalFooterComponent, MempoolBlockOverviewComponent, + ClockchainComponent, ClockAComponent, ClockBComponent, ClockFaceComponent, From f879a34021271b46d857021953e6a9ae4f8c1796 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 19 Apr 2023 03:34:13 +0900 Subject: [PATCH 03/12] responsive clock, fix blockchain --- .../block-overview-graph/block-scene.ts | 1 - .../blockchain-blocks.component.html | 14 +- .../blockchain-blocks.component.scss | 24 +-- .../blockchain-blocks.component.ts | 32 +++- .../clock-face/clock-face.component.html | 179 +++++++++--------- .../clock-face/clock-face.component.scss | 55 ++---- .../clock-face/clock-face.component.ts | 27 +-- .../components/clock/clock-a.component.html | 18 +- .../app/components/clock/clock-a.component.ts | 54 +----- .../components/clock/clock-b.component.html | 14 +- .../app/components/clock/clock-b.component.ts | 54 +----- .../app/components/clock/clock.component.html | 34 ++++ .../app/components/clock/clock.component.scss | 43 ++++- .../app/components/clock/clock.component.ts | 82 ++++++++ .../clockchain/clockchain.component.html | 22 ++- .../clockchain/clockchain.component.scss | 45 +---- .../clockchain/clockchain.component.ts | 26 ++- .../mempool-blocks.component.html | 12 +- .../mempool-blocks.component.scss | 25 +-- .../mempool-blocks.component.ts | 44 +++-- frontend/src/app/shared/shared.module.ts | 3 + 21 files changed, 412 insertions(+), 396 deletions(-) create mode 100644 frontend/src/app/components/clock/clock.component.html create mode 100644 frontend/src/app/components/clock/clock.component.ts 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 f1c1b4d77..1c0072e31 100644 --- a/frontend/src/app/components/block-overview-graph/block-scene.ts +++ b/frontend/src/app/components/block-overview-graph/block-scene.ts @@ -36,7 +36,6 @@ export default class BlockScene { this.gridSize = this.width / this.gridWidth; this.unitPadding = Math.max(1, Math.floor(this.gridSize / 2.5)); this.unitWidth = this.gridSize - (this.unitPadding); - console.log(this.gridSize, this.unitPadding, this.unitWidth); this.dirty = true; if (this.initialised && this.scene) { diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html index 695e7c97d..0a2f0decb 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html @@ -1,21 +1,21 @@ -
  -
+
- +
~{{ block?.extras?.medianFee | number:feeRounding }} sat/vB @@ -82,11 +82,11 @@
-
+
+ [ngStyle]="emptyBlockStyles[i]" [class.offscreen]="!static && count && i >= count">
diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss index f0afaafd8..7e04a3af6 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss @@ -1,6 +1,6 @@ .bitcoin-block { - width: 125px; - height: 125px; + width: var(--block-size); + height: var(--block-size); } .blockLink { @@ -39,9 +39,11 @@ } .blocks-container { + --block-size: 125px; + --block-offset: calc(0.32 * var(--block-size)); position: absolute; top: 0px; - left: 40px; + left: var(--block-offset); } .block-body { @@ -81,11 +83,11 @@ .bitcoin-block::after { content: ''; - width: 125px; - height: 24px; + width: var(--block-size); + height: calc(0.192 * var(--block-size)); position:absolute; - top: -24px; - left: -20px; + top: calc(-0.192 * var(--block-size)); + left: calc(-0.16 * var(--block-size)); background-color: #232838; transform:skew(40deg); transform-origin:top; @@ -93,11 +95,11 @@ .bitcoin-block::before { content: ''; - width: 20px; - height: 125px; + width: calc(0.16 * var(--block-size)); + height: var(--block-size); position: absolute; - top: -12px; - left: -20px; + top: calc(-0.096 * var(--block-size)); + left: calc(-0.16 * var(--block-size)); background-color: #191c27; transform: skewY(50deg); diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts index e894caff7..65124e0d3 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts @@ -24,7 +24,8 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy { @Input() count: number = 8; // number of blocks in this chunk (dynamic blocks only) @Input() loadingTip: boolean = false; @Input() connected: boolean = true; - @Input() tiny: boolean = false; + @Input() minimal: boolean = false; + @Input() blockWidth: number = 125; specialBlocks = specialBlocks; network = ''; @@ -52,6 +53,10 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy { timeLtrSubscription: Subscription; timeLtr: boolean; + blockOffset: number = 155; + dividerBlockOffset: number = 205; + blockPadding: number = 30; + gradientColors = { '': ['#9339f4', '#105fb0'], bisq: ['#9339f4', '#105fb0'], @@ -119,7 +124,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy { this.blockStyles = []; if (this.blocksFilled && block.height > this.chainTip) { - this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, i ? -155 : -205))); + this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, i ? -this.blockOffset : -this.dividerBlockOffset))); setTimeout(() => { this.blockStyles = []; this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i))); @@ -160,6 +165,13 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy { } ngOnChanges(changes: SimpleChanges): void { + if (changes.blockWidth && this.blockWidth) { + this.blockPadding = 0.24 * this.blockWidth; + this.blockOffset = this.blockWidth + this.blockPadding; + this.dividerBlockOffset = this.blockOffset + (0.4 * this.blockWidth); + this.blockStyles = []; + this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i))); + } if (this.static) { const animateSlide = changes.height && (changes.height.currentValue === changes.height.previousValue + 1); this.updateStaticBlocks(animateSlide); @@ -192,14 +204,14 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy { } this.arrowVisible = true; if (newBlockFromLeft) { - this.arrowLeftPx = blockindex * 155 + 30 - 205; + this.arrowLeftPx = blockindex * this.blockOffset + this.blockPadding - this.dividerBlockOffset; setTimeout(() => { this.arrowTransition = '2s'; - this.arrowLeftPx = blockindex * 155 + 30; + this.arrowLeftPx = blockindex * this.blockOffset + this.blockPadding; this.cd.markForCheck(); }, 50); } else { - this.arrowLeftPx = blockindex * 155 + 30; + this.arrowLeftPx = blockindex * this.blockOffset + this.blockPadding; if (!animate) { setTimeout(() => { this.arrowTransition = '2s'; @@ -246,7 +258,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy { } this.blocks = this.blocks.slice(0, this.count); this.blockStyles = []; - this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, animateSlide ? -155 : 0))); + this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, animateSlide ? -this.blockOffset : 0))); this.cd.markForCheck(); if (animateSlide) { // animate blocks slide right @@ -288,7 +300,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy { } return { - left: addLeft + 155 * index + 'px', + left: addLeft + this.blockOffset * index + 'px', background: `repeating-linear-gradient( #2d3348, #2d3348 ${greenBackgroundHeight}%, @@ -310,7 +322,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy { const addLeft = animateEnterFrom || 0; return { - left: addLeft + (155 * index) + 'px', + left: addLeft + (this.blockOffset * index) + 'px', background: "#2d3348", }; } @@ -318,7 +330,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy { getStyleForPlaceholderBlock(index: number, animateEnterFrom: number = 0) { const addLeft = animateEnterFrom || 0; return { - left: addLeft + (155 * index) + 'px', + left: addLeft + (this.blockOffset * index) + 'px', }; } @@ -326,7 +338,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy { const addLeft = animateEnterFrom || 0; return { - left: addLeft + 155 * this.emptyBlocks.indexOf(block) + 'px', + left: addLeft + this.blockOffset * this.emptyBlocks.indexOf(block) + 'px', background: "#2d3348", }; } diff --git a/frontend/src/app/components/clock-face/clock-face.component.html b/frontend/src/app/components/clock-face/clock-face.component.html index f1893cd85..6e17dab05 100644 --- a/frontend/src/app/components/clock-face/clock-face.component.html +++ b/frontend/src/app/components/clock-face/clock-face.component.html @@ -1,94 +1,87 @@ -
-
-
- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/frontend/src/app/components/clock-face/clock-face.component.scss b/frontend/src/app/components/clock-face/clock-face.component.scss index 66d53784f..60b2c4eba 100644 --- a/frontend/src/app/components/clock-face/clock-face.component.scss +++ b/frontend/src/app/components/clock-face/clock-face.component.scss @@ -1,45 +1,20 @@ -.clock-wrapper { - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - justify-content: flex-start; +.clock-face { + position: relative; + height: 84.375%; + margin: auto; + overflow: hidden; - .clockchain-bar, .clock-face { - flex-shrink: 0; - flex-grow: 0; - } + .cut-out, .demo-dial { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + width: 100%; + height: 100%; - .clockchain-bar { - position: relative; - height: 15.625%; - // background: #1d1f31; - // box-shadow: 0 0 15px #000; - } - - .clock-face { - position: relative; - height: 84.375%; - margin: auto; - overflow: hidden; - - .cut-out, .demo-dial { - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - width: 100%; - height: 100%; - - .face { - fill: #11131f; - } + .face { + fill: #11131f; } } } \ No newline at end of file diff --git a/frontend/src/app/components/clock-face/clock-face.component.ts b/frontend/src/app/components/clock-face/clock-face.component.ts index 747b6221c..c63ea56ea 100644 --- a/frontend/src/app/components/clock-face/clock-face.component.ts +++ b/frontend/src/app/components/clock-face/clock-face.component.ts @@ -1,36 +1,17 @@ -import { Component, HostListener, OnInit } from '@angular/core'; +import { Component, Input, OnChanges } from '@angular/core'; @Component({ selector: 'app-clock-face', templateUrl: './clock-face.component.html', styleUrls: ['./clock-face.component.scss'], }) -export class ClockFaceComponent implements OnInit { - size: number; - wrapperStyle; - chainStyle; +export class ClockFaceComponent implements OnChanges { + @Input() size: number = 300; faceStyle; - showDial: boolean = false; constructor() {} - ngOnInit(): void { - // initialize stuff - this.resizeCanvas(); - } - - @HostListener('window:resize', ['$event']) - resizeCanvas(): void { - this.size = Math.min(window.innerWidth, 0.78125 * window.innerHeight); - this.wrapperStyle = { - '--clock-width': `${this.size}px` - }; - const scaleFactor = window.innerWidth / 1390; - this.chainStyle = { - transform: `translate(2vw, 0.5vw) scale(${scaleFactor})`, - transformOrigin: 'top left', - }; - + ngOnChanges(): void { this.faceStyle = { width: `${this.size}px`, height: `${this.size}px`, diff --git a/frontend/src/app/components/clock/clock-a.component.html b/frontend/src/app/components/clock/clock-a.component.html index 2029b9f24..a3bebd4bd 100644 --- a/frontend/src/app/components/clock/clock-a.component.html +++ b/frontend/src/app/components/clock/clock-a.component.html @@ -1,17 +1 @@ - -
- -
-
-
-
-
-
-
-
-
-

{{ block.height }}

-
-
-
-
\ No newline at end of file + \ No newline at end of file diff --git a/frontend/src/app/components/clock/clock-a.component.ts b/frontend/src/app/components/clock/clock-a.component.ts index 0242e0ab8..50f834bad 100644 --- a/frontend/src/app/components/clock/clock-a.component.ts +++ b/frontend/src/app/components/clock/clock-a.component.ts @@ -1,57 +1,7 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core'; -import { Subscription } from 'rxjs'; -import { StateService } from '../../services/state.service'; -import { BlockExtended } from '../../interfaces/node-api.interface'; -import { WebsocketService } from '../../services/websocket.service'; +import { Component } from '@angular/core'; @Component({ selector: 'app-clock-a', templateUrl: './clock-a.component.html', - styleUrls: ['./clock.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ClockAComponent implements OnInit { - blocksSubscription: Subscription; - block: BlockExtended; - blockStyle; - - gradientColors = { - '': ['#9339f4', '#105fb0'], - bisq: ['#9339f4', '#105fb0'], - liquid: ['#116761', '#183550'], - 'liquidtestnet': ['#494a4a', '#272e46'], - testnet: ['#1d486f', '#183550'], - signet: ['#6f1d5d', '#471850'], - }; - - constructor( - public stateService: StateService, - private websocketService: WebsocketService, - private cd: ChangeDetectorRef, - ) {} - - ngOnInit(): void { - this.websocketService.want(['blocks']); - this.blocksSubscription = this.stateService.blocks$ - .subscribe(([block]) => { - if (block) { - this.block = block; - this.blockStyle = this.getStyleForBlock(this.block); - this.cd.markForCheck(); - } - }); - } - - getStyleForBlock(block: BlockExtended) { - const greenBackgroundHeight = 100 - (block.weight / this.stateService.env.BLOCK_WEIGHT_UNITS) * 100; - - return { - background: `repeating-linear-gradient( - #2d3348, - #2d3348 ${greenBackgroundHeight}%, - ${this.gradientColors[''][0]} ${Math.max(greenBackgroundHeight, 0)}%, - ${this.gradientColors[''][1]} 100% - )`, - }; - } -} +export class ClockAComponent {} diff --git a/frontend/src/app/components/clock/clock-b.component.html b/frontend/src/app/components/clock/clock-b.component.html index 887c0a1cf..a8620a212 100644 --- a/frontend/src/app/components/clock/clock-b.component.html +++ b/frontend/src/app/components/clock/clock-b.component.html @@ -1,13 +1 @@ - -
- -
- -
-
-
-

{{ block.height }}

-
-
-
-
\ No newline at end of file + \ No newline at end of file diff --git a/frontend/src/app/components/clock/clock-b.component.ts b/frontend/src/app/components/clock/clock-b.component.ts index 626f388b6..b47c9dba3 100644 --- a/frontend/src/app/components/clock/clock-b.component.ts +++ b/frontend/src/app/components/clock/clock-b.component.ts @@ -1,57 +1,7 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, OnInit } from '@angular/core'; -import { Subscription } from 'rxjs'; -import { StateService } from '../../services/state.service'; -import { BlockExtended } from '../../interfaces/node-api.interface'; -import { WebsocketService } from '../../services/websocket.service'; +import { Component } from '@angular/core'; @Component({ selector: 'app-clock-b', templateUrl: './clock-b.component.html', - styleUrls: ['./clock.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ClockBComponent implements OnInit { - blocksSubscription: Subscription; - block: BlockExtended; - blockSizerStyle; - - gradientColors = { - '': ['#9339f4', '#105fb0'], - bisq: ['#9339f4', '#105fb0'], - liquid: ['#116761', '#183550'], - 'liquidtestnet': ['#494a4a', '#272e46'], - testnet: ['#1d486f', '#183550'], - signet: ['#6f1d5d', '#471850'], - }; - - constructor( - public stateService: StateService, - private websocketService: WebsocketService, - private cd: ChangeDetectorRef, - ) {} - - ngOnInit(): void { - this.resizeCanvas(); - this.websocketService.want(['blocks']); - this.blocksSubscription = this.stateService.blocks$ - .subscribe(([block]) => { - if (block) { - this.block = block; - this.cd.markForCheck(); - } - }); - } - - @HostListener('window:resize', ['$event']) - resizeCanvas(): void { - const clockSize = Math.min(window.innerWidth, 0.78125 * window.innerHeight); - const size = Math.ceil(clockSize / 75) * 75; - const margin = (clockSize - size) / 2; - this.blockSizerStyle = { - transform: `translate(${margin}px, ${margin}px)`, - width: `${size}px`, - height: `${size}px`, - }; - this.cd.markForCheck(); - } -} +export class ClockBComponent {} diff --git a/frontend/src/app/components/clock/clock.component.html b/frontend/src/app/components/clock/clock.component.html new file mode 100644 index 000000000..74e06418d --- /dev/null +++ b/frontend/src/app/components/clock/clock.component.html @@ -0,0 +1,34 @@ +
+
+
+ +
+
+
+ +
+ + +
+
+
+
+
+
+
+
+
+ +
+ +
+
+
+
+

{{ block.height }}

+
+
+
+
+
+
\ No newline at end of file diff --git a/frontend/src/app/components/clock/clock.component.scss b/frontend/src/app/components/clock/clock.component.scss index c41b33e8a..e5904b4f1 100644 --- a/frontend/src/app/components/clock/clock.component.scss +++ b/frontend/src/app/components/clock/clock.component.scss @@ -1,3 +1,44 @@ +.clock-wrapper { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: flex-start; + + --clock-width: 300px; + + .clockchain-bar, .clock-face { + flex-shrink: 0; + flex-grow: 0; + } + + .clockchain-bar { + position: relative; + width: 100%; + height: 15.625%; + z-index: 2; + overflow: hidden; + // background: #1d1f31; + // box-shadow: 0 0 15px #000; + } + + .clock-face { + position: relative; + height: 84.375%; + margin: auto; + overflow: hidden; + display: flex; + justify-content: center; + align-items: center; + z-index: 1; + } +} + .title-wrapper { position: absolute; left: 0; @@ -101,8 +142,6 @@ } } - - @keyframes block-spin { 0% {transform: translate(-50%, -50%) rotateX(-20deg) rotateY(0deg);} 100% {transform: translate(-50%, -50%) rotateX(-20deg) rotateY(-360deg);} diff --git a/frontend/src/app/components/clock/clock.component.ts b/frontend/src/app/components/clock/clock.component.ts new file mode 100644 index 000000000..7aa875695 --- /dev/null +++ b/frontend/src/app/components/clock/clock.component.ts @@ -0,0 +1,82 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Input, OnInit } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { StateService } from '../../services/state.service'; +import { BlockExtended } from '../../interfaces/node-api.interface'; +import { WebsocketService } from '../../services/websocket.service'; + +@Component({ + selector: 'app-clock', + templateUrl: './clock.component.html', + styleUrls: ['./clock.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ClockComponent implements OnInit { + @Input() mode: string = 'block'; + blocksSubscription: Subscription; + block: BlockExtended; + clockSize: number = 300; + chainWidth: number = 384; + chainHeight: number = 60; + blockStyle; + blockSizerStyle; + wrapperStyle; + + gradientColors = { + '': ['#9339f4', '#105fb0'], + bisq: ['#9339f4', '#105fb0'], + liquid: ['#116761', '#183550'], + 'liquidtestnet': ['#494a4a', '#272e46'], + testnet: ['#1d486f', '#183550'], + signet: ['#6f1d5d', '#471850'], + }; + + constructor( + public stateService: StateService, + private websocketService: WebsocketService, + private cd: ChangeDetectorRef, + ) {} + + ngOnInit(): void { + this.resizeCanvas(); + this.websocketService.want(['blocks']); + this.blocksSubscription = this.stateService.blocks$ + .subscribe(([block]) => { + if (block) { + this.block = block; + this.blockStyle = this.getStyleForBlock(this.block); + this.cd.markForCheck(); + } + }); + } + + getStyleForBlock(block: BlockExtended) { + const greenBackgroundHeight = 100 - (block.weight / this.stateService.env.BLOCK_WEIGHT_UNITS) * 100; + + return { + background: `repeating-linear-gradient( + #2d3348, + #2d3348 ${greenBackgroundHeight}%, + ${this.gradientColors[''][0]} ${Math.max(greenBackgroundHeight, 0)}%, + ${this.gradientColors[''][1]} 100% + )`, + }; + } + + @HostListener('window:resize', ['$event']) + resizeCanvas(): void { + this.chainWidth = window.innerWidth; + this.chainHeight = Math.max(60, window.innerHeight / 8); + this.clockSize = Math.min(500, window.innerWidth, window.innerHeight - (1.4 * this.chainHeight)); + const size = Math.ceil(this.clockSize / 75) * 75; + const margin = (this.clockSize - size) / 2; + this.blockSizerStyle = { + transform: `translate(${margin}px, ${margin}px)`, + width: `${size}px`, + height: `${size}px`, + }; + this.wrapperStyle = { + '--clock-width': `${this.clockSize}px` + }; + this.cd.markForCheck(); + } +} diff --git a/frontend/src/app/components/clockchain/clockchain.component.html b/frontend/src/app/components/clockchain/clockchain.component.html index 0eabc2862..3a28296ca 100644 --- a/frontend/src/app/components/clockchain/clockchain.component.html +++ b/frontend/src/app/components/clockchain/clockchain.component.html @@ -1,11 +1,25 @@
-
+
- - + + +
+
+ + +
-
diff --git a/frontend/src/app/components/clockchain/clockchain.component.scss b/frontend/src/app/components/clockchain/clockchain.component.scss index d6fc00090..0b01adc26 100644 --- a/frontend/src/app/components/clockchain/clockchain.component.scss +++ b/frontend/src/app/components/clockchain/clockchain.component.scss @@ -1,15 +1,17 @@ .divider { - width: 4px; - height: 180px; - left: 0; - top: -40px; position: absolute; - margin-bottom: 120px; - background-image: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3cline x1='0' y1='0' x2='0' y2='100%' stroke='white' stroke-width='8' stroke-dasharray='18%2c32' stroke-dashoffset='-5' stroke-linecap='square'/%3e%3c/svg%3e"); + left: -1px; + top: 0; + .divider-line { + stroke: white; + stroke-width: 4px; + stroke-linecap: butt; + stroke-dasharray: 25px 25px; + } } .blockchain-wrapper { - height: 250px; + height: 100%; -webkit-user-select: none; /* Safari */ -moz-user-select: none; /* Firefox */ @@ -20,37 +22,10 @@ .position-container { position: absolute; left: 0; - top: 75px; + top: 0; transform: translateX(50vw); } -.position-container.liquid, .position-container.liquidtestnet { - transform: translateX(420px); -} - -.blockchain-wrapper { - .position-container { - transform: translateX(95vw); - } - .position-container.liquid, .position-container.liquidtestnet { - transform: translateX(50vw); - } - .position-container.loading { - transform: translateX(50vw); - } -} -.blockchain-wrapper.time-ltr { - .position-container { - transform: translateX(5vw); - } - .position-container.liquid, .position-container.liquidtestnet { - transform: translateX(50vw); - } - .position-container.loading { - transform: translateX(50vw); - } -} - .black-background { background-color: #11131f; z-index: 100; diff --git a/frontend/src/app/components/clockchain/clockchain.component.ts b/frontend/src/app/components/clockchain/clockchain.component.ts index e5c63dbe4..addc22948 100644 --- a/frontend/src/app/components/clockchain/clockchain.component.ts +++ b/frontend/src/app/components/clockchain/clockchain.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input } from '@angular/core'; +import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, OnChanges, ChangeDetectorRef } from '@angular/core'; import { firstValueFrom, Subscription } from 'rxjs'; import { StateService } from '../../services/state.service'; @@ -8,7 +8,15 @@ import { StateService } from '../../services/state.service'; styleUrls: ['./clockchain.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ClockchainComponent implements OnInit, OnDestroy { +export class ClockchainComponent implements OnInit, OnChanges, OnDestroy { + @Input() width: number = 300; + @Input() height: number = 60; + + mempoolBlocks: number = 3; + blockchainBlocks: number = 6; + blockWidth: number = 50; + dividerStyle; + network: string; timeLtrSubscription: Subscription; timeLtr: boolean = this.stateService.timeLtr.value; @@ -19,9 +27,12 @@ export class ClockchainComponent implements OnInit, OnDestroy { constructor( public stateService: StateService, + private cd: ChangeDetectorRef, ) {} ngOnInit() { + this.ngOnChanges(); + this.network = this.stateService.network; this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => { this.timeLtr = !!ltr; @@ -34,6 +45,17 @@ export class ClockchainComponent implements OnInit, OnDestroy { }); } + ngOnChanges() { + this.blockWidth = Math.floor(7 * this.height / 12); + this.mempoolBlocks = Math.floor(((this.width / 2) - (this.blockWidth * 0.32)) / (1.24 * this.blockWidth)); + this.blockchainBlocks = this.mempoolBlocks; + this.dividerStyle = { + width: '2px', + height: `${this.height}px`, + }; + this.cd.markForCheck(); + } + ngOnDestroy() { this.timeLtrSubscription.unsubscribe(); this.connectionStateSubscription.unsubscribe(); diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.html b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.html index b11fbb888..cfb091e9b 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.html +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.html @@ -1,12 +1,12 @@ - -
+ +
-
+
 
- +
~{{ projectedBlock.medianFee | number:feeRounding }} sat/vB
@@ -73,10 +73,10 @@
-
+
-
+
diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.scss b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.scss index a0140f55c..6d1ec326e 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.scss +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.scss @@ -1,6 +1,6 @@ .bitcoin-block { - width: 125px; - height: 125px; + width: var(--block-size); + height: var(--block-size); transition: background 2s, right 2s, transform 1s, opacity 1s; } @@ -14,6 +14,7 @@ top: 0px; right: 0px; left: 0px; + --block-size: 125px; } .flashing { @@ -66,11 +67,11 @@ .bitcoin-block::after { content: ''; - width: 125px; - height: 24px; + width: var(--block-size); + height: calc(0.192 * var(--block-size)); position:absolute; - top: -24px; - left: -20px; + top: calc(-0.192 * var(--block-size)); + left: calc(-0.16 * var(--block-size)); background-color: #232838; transform:skew(40deg); transform-origin:top; @@ -79,11 +80,11 @@ .bitcoin-block::before { content: ''; - width: 20px; - height: 125px; + width: calc(0.16 * var(--block-size)); + height: var(--block-size); position: absolute; - top: -12px; - left: -20px; + top: calc(-0.096 * var(--block-size)); + left: calc(-0.16 * var(--block-size)); background-color: #191c27; z-index: -1; @@ -100,7 +101,7 @@ background-color: #2d2825; } -.mempool-block.last-block { +.mempool-block.hide-block { opacity: 0; } @@ -145,7 +146,7 @@ .bitcoin-block::before { transform: skewY(-50deg); - left: 125px; + left: var(--block-size); } .block-body { transform: scaleX(-1); diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts index b711ce805..6877823f5 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input } from '@angular/core'; +import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Subscription, Observable, fromEvent, merge, of, combineLatest } from 'rxjs'; import { MempoolBlock } from '../../interfaces/websocket.interface'; import { StateService } from '../../services/state.service'; @@ -23,8 +23,9 @@ import { animate, style, transition, trigger } from '@angular/animations'; ])], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class MempoolBlocksComponent implements OnInit, OnDestroy { - @Input() tiny: boolean = false; +export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { + @Input() minimal: boolean = false; + @Input() blockWidth: number = 125; @Input() count: number = null; specialBlocks = specialBlocks; @@ -51,8 +52,9 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy { timeLtr: boolean; animateEntry: boolean = false; - blockWidth = 125; - blockPadding = 30; + blockOffset: number = 155; + blockPadding: number = 30; + containerOffset: number = 40; arrowVisible = false; tabHidden = false; feeRounding = '1.0-0'; @@ -221,6 +223,14 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy { }); } + ngOnChanges(changes: SimpleChanges): void { + if (changes.blockWidth && this.blockWidth) { + this.blockPadding = 0.24 * this.blockWidth; + this.containerOffset = 0.32 * this.blockWidth; + this.blockOffset = this.blockWidth + this.blockPadding; + } + } + ngOnDestroy() { this.markBlocksSubscription.unsubscribe(); this.blockSubscription.unsubscribe(); @@ -243,20 +253,22 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy { const innerWidth = this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2; let blocksAmount; if (this.count) { - blocksAmount = this.count; + blocksAmount = 8; } else { blocksAmount = Math.min(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT, Math.floor(innerWidth / (this.blockWidth + this.blockPadding))); } while (blocks.length > blocksAmount) { const block = blocks.pop(); - const lastBlock = blocks[blocks.length - 1]; - lastBlock.blockSize += block.blockSize; - lastBlock.blockVSize += block.blockVSize; - lastBlock.nTx += block.nTx; - lastBlock.feeRange = lastBlock.feeRange.concat(block.feeRange); - lastBlock.feeRange.sort((a, b) => a - b); - lastBlock.medianFee = this.median(lastBlock.feeRange); - lastBlock.totalFees += block.totalFees; + if (!this.count) { + const lastBlock = blocks[blocks.length - 1]; + lastBlock.blockSize += block.blockSize; + lastBlock.blockVSize += block.blockVSize; + lastBlock.nTx += block.nTx; + lastBlock.feeRange = lastBlock.feeRange.concat(block.feeRange); + lastBlock.feeRange.sort((a, b) => a - b); + lastBlock.medianFee = this.median(lastBlock.feeRange); + lastBlock.totalFees += block.totalFees; + } } if (blocks.length) { blocks[blocks.length - 1].isStack = blocks[blocks.length - 1].blockVSize > this.stateService.blockVSize; @@ -302,14 +314,14 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy { }); return { - 'right': 40 + index * 155 + 'px', + 'right': this.containerOffset + index * this.blockOffset + 'px', 'background': backgroundGradients.join(',') + ')' }; } getStyleForMempoolEmptyBlock(index: number) { return { - 'right': 40 + index * 155 + 'px', + 'right': this.containerOffset + index * this.blockOffset + 'px', 'background': '#554b45', }; } diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 0765298bd..21cbb17a8 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -93,6 +93,7 @@ import { GlobalFooterComponent } from './components/global-footer/global-footer. import { MempoolBlockOverviewComponent } from '../components/mempool-block-overview/mempool-block-overview.component'; import { ClockchainComponent } from '../components/clockchain/clockchain.component'; import { ClockFaceComponent } from '../components/clock-face/clock-face.component'; +import { ClockComponent } from '../components/clock/clock.component'; import { ClockAComponent } from '../components/clock/clock-a.component'; import { ClockBComponent } from '../components/clock/clock-b.component'; @@ -181,6 +182,7 @@ import { ClockBComponent } from '../components/clock/clock-b.component'; MempoolBlockOverviewComponent, ClockchainComponent, + ClockComponent, ClockAComponent, ClockBComponent, ClockFaceComponent, @@ -294,6 +296,7 @@ import { ClockBComponent } from '../components/clock/clock-b.component'; MempoolBlockOverviewComponent, ClockchainComponent, + ClockComponent, ClockAComponent, ClockBComponent, ClockFaceComponent, From 3ddd51d4cbbdec014d4f18e5c9dcfe6f4cfcea38 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 19 Apr 2023 09:39:56 +0900 Subject: [PATCH 04/12] implement clock face & dial --- .../clock-face/clock-face.component.html | 81 +++-------- .../clock-face/clock-face.component.scss | 48 ++++++ .../clock-face/clock-face.component.ts | 137 +++++++++++++++++- .../app/components/clock/clock.component.scss | 2 +- .../app/components/clock/clock.component.ts | 2 +- .../clockchain/clockchain.component.scss | 2 +- .../clockchain/clockchain.component.ts | 4 +- .../mempool-blocks.component.ts | 2 +- frontend/src/resources/clock/gradient.png | Bin 0 -> 38328 bytes 9 files changed, 205 insertions(+), 73 deletions(-) create mode 100644 frontend/src/resources/clock/gradient.png diff --git a/frontend/src/app/components/clock-face/clock-face.component.html b/frontend/src/app/components/clock-face/clock-face.component.html index 6e17dab05..b3d478ebb 100644 --- a/frontend/src/app/components/clock-face/clock-face.component.html +++ b/frontend/src/app/components/clock-face/clock-face.component.html @@ -20,68 +20,23 @@ height="384" viewBox="0 0 384 384" > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/frontend/src/app/components/clock-face/clock-face.component.scss b/frontend/src/app/components/clock-face/clock-face.component.scss index 60b2c4eba..d671341a6 100644 --- a/frontend/src/app/components/clock-face/clock-face.component.scss +++ b/frontend/src/app/components/clock-face/clock-face.component.scss @@ -17,4 +17,52 @@ fill: #11131f; } } + + .gnomon { + transform-origin: center; + stroke-linejoin: round; + + &.minute { + fill:#80C2E1; + stroke:#80C2E1; + stroke-width: 2px; + } + + &.hour { + fill: #105fb0; + stroke: #105fb0; + stroke-width: 6px; + } + } + + .tick { + transform-origin: center; + fill: none; + stroke: white; + stroke-width: 2px; + + &.minor { + stroke-opacity: 0.5; + } + + &.very.major { + stroke-width: 4px; + } + } + + .block-segment { + fill: none; + stroke: url(#dial-gradient); + stroke-width: 18px; + } + + .dial-segment { + fill: none; + stroke: white; + stroke-width: 2px; + } + + .dial-gradient-img { + transform-origin: center; + } } \ No newline at end of file diff --git a/frontend/src/app/components/clock-face/clock-face.component.ts b/frontend/src/app/components/clock-face/clock-face.component.ts index c63ea56ea..9c373a50d 100644 --- a/frontend/src/app/components/clock-face/clock-face.component.ts +++ b/frontend/src/app/components/clock-face/clock-face.component.ts @@ -1,15 +1,55 @@ -import { Component, Input, OnChanges } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core'; +import { Subscription, tap, timer } from 'rxjs'; +import { WebsocketService } from '../../services/websocket.service'; +import { StateService } from '../../services/state.service'; @Component({ selector: 'app-clock-face', templateUrl: './clock-face.component.html', styleUrls: ['./clock-face.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ClockFaceComponent implements OnChanges { +export class ClockFaceComponent implements OnInit, OnChanges, OnDestroy { @Input() size: number = 300; - faceStyle; - constructor() {} + blocksSubscription: Subscription; + timeSubscription: Subscription; + + faceStyle; + dialPath; + blockTimes = []; + segments = []; + hours: number = 0; + minutes: number = 0; + minorTicks: number[] = []; + majorTicks: number[] = []; + + constructor( + public stateService: StateService, + private websocketService: WebsocketService, + private cd: ChangeDetectorRef + ) { + this.updateTime(); + this.makeTicks(); + } + + ngOnInit(): void { + this.timeSubscription = timer(0, 250).pipe( + tap(() => { + this.updateTime(); + }) + ).subscribe(); + this.websocketService.want(['blocks']); + this.blocksSubscription = this.stateService.blocks$ + .subscribe(([block]) => { + if (block) { + this.blockTimes.push([block.height, new Date(block.timestamp * 1000)]); + // using block-reported times, so ensure they are sorted chronologically + this.blockTimes = this.blockTimes.sort((a, b) => a[1].getTime() - b[1].getTime()); + this.updateSegments(); + } + }); + } ngOnChanges(): void { this.faceStyle = { @@ -17,4 +57,93 @@ export class ClockFaceComponent implements OnChanges { height: `${this.size}px`, }; } + + ngOnDestroy(): void { + this.timeSubscription.unsubscribe(); + } + + updateTime(): void { + const now = new Date(); + const seconds = now.getSeconds() + (now.getMilliseconds() / 1000); + this.minutes = (now.getMinutes() + (seconds / 60)) % 60; + this.hours = now.getHours() + (this.minutes / 60); + this.updateSegments(); + } + + updateSegments(): void { + const now = new Date(); + this.blockTimes = this.blockTimes.filter(time => (now.getTime() - time[1].getTime()) <= 3600000); + const tail = new Date(now.getTime() - 3600000); + const hourStart = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours()); + + const times = [ + ['start', tail], + ...this.blockTimes, + ['end', now], + ]; + const minuteTimes = times.map(time => { + return [time[0], (time[1].getTime() - hourStart.getTime()) / 60000]; + }); + this.segments = []; + const r = 174; + const cx = 192; + const cy = cx; + for (let i = 1; i < minuteTimes.length; i++) { + const arc = this.getArc(minuteTimes[i-1][1], minuteTimes[i][1], r, cx, cy); + if (arc) { + arc.id = minuteTimes[i][0]; + this.segments.push(arc); + } + } + const arc = this.getArc(minuteTimes[0][1], minuteTimes[1][1], r, cx, cy); + if (arc) { + this.dialPath = arc.path; + } + + this.cd.markForCheck(); + } + + getArc(startTime, endTime, r, cx, cy): any { + const startDegrees = (startTime + 0.2) * 6; + const endDegrees = (endTime - 0.2) * 6; + const start = this.getPointOnCircle(startDegrees, r, cx, cy); + const end = this.getPointOnCircle(endDegrees, r, cx, cy); + const arcLength = endDegrees - startDegrees; + // merge gaps and omit lines shorter than 1 degree + if (arcLength >= 1) { + const path = `M ${start.x} ${start.y} A ${r} ${r} 0 ${arcLength > 180 ? 1 : 0} 1 ${end.x} ${end.y}`; + return { + path, + start, + end + }; + } else { + return null; + } + } + + getPointOnCircle(deg, r, cx, cy) { + const modDeg = ((deg % 360) + 360) % 360; + const rad = (modDeg * Math.PI) / 180; + return { + x: cx + (r * Math.sin(rad)), + y: cy - (r * Math.cos(rad)), + }; + } + + makeTicks() { + this.minorTicks = []; + this.majorTicks = []; + for (let i = 1; i < 60; i++) { + if (i % 5 === 0) { + this.majorTicks.push(i * 6); + } else { + this.minorTicks.push(i * 6); + } + } + } + + trackBySegment(index: number, segment) { + return segment.id; + } } diff --git a/frontend/src/app/components/clock/clock.component.scss b/frontend/src/app/components/clock/clock.component.scss index e5904b4f1..a27c62499 100644 --- a/frontend/src/app/components/clock/clock.component.scss +++ b/frontend/src/app/components/clock/clock.component.scss @@ -84,7 +84,7 @@ right: 0; top: 0; bottom: 0; - background: radial-gradient(transparent 0%, transparent 48%, #11131f 62%, #11131f 100%); + background: radial-gradient(transparent 0%, transparent 44%, #11131f 58%, #11131f 100%); } .block-cube { diff --git a/frontend/src/app/components/clock/clock.component.ts b/frontend/src/app/components/clock/clock.component.ts index 7aa875695..c804860af 100644 --- a/frontend/src/app/components/clock/clock.component.ts +++ b/frontend/src/app/components/clock/clock.component.ts @@ -66,7 +66,7 @@ export class ClockComponent implements OnInit { resizeCanvas(): void { this.chainWidth = window.innerWidth; this.chainHeight = Math.max(60, window.innerHeight / 8); - this.clockSize = Math.min(500, window.innerWidth, window.innerHeight - (1.4 * this.chainHeight)); + this.clockSize = Math.min(800, window.innerWidth, window.innerHeight - (1.4 * this.chainHeight)); const size = Math.ceil(this.clockSize / 75) * 75; const margin = (this.clockSize - size) / 2; this.blockSizerStyle = { diff --git a/frontend/src/app/components/clockchain/clockchain.component.scss b/frontend/src/app/components/clockchain/clockchain.component.scss index 0b01adc26..acff1e725 100644 --- a/frontend/src/app/components/clockchain/clockchain.component.scss +++ b/frontend/src/app/components/clockchain/clockchain.component.scss @@ -1,6 +1,6 @@ .divider { position: absolute; - left: -1px; + left: -0.5px; top: 0; .divider-line { stroke: white; diff --git a/frontend/src/app/components/clockchain/clockchain.component.ts b/frontend/src/app/components/clockchain/clockchain.component.ts index addc22948..ab9220c54 100644 --- a/frontend/src/app/components/clockchain/clockchain.component.ts +++ b/frontend/src/app/components/clockchain/clockchain.component.ts @@ -39,8 +39,8 @@ export class ClockchainComponent implements OnInit, OnChanges, OnDestroy { }); this.connectionStateSubscription = this.stateService.connectionState$.subscribe(state => { this.connected = (state === 2); - }) - firstValueFrom(this.stateService.chainTip$).then(tip => { + }); + firstValueFrom(this.stateService.chainTip$).then(() => { this.loadingTip = false; }); } diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts index 6877823f5..6267eed21 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts @@ -27,7 +27,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { @Input() minimal: boolean = false; @Input() blockWidth: number = 125; @Input() count: number = null; - + specialBlocks = specialBlocks; mempoolBlocks: MempoolBlock[] = []; mempoolEmptyBlocks: MempoolBlock[] = this.mountEmptyBlocks(); diff --git a/frontend/src/resources/clock/gradient.png b/frontend/src/resources/clock/gradient.png new file mode 100644 index 0000000000000000000000000000000000000000..372105fbd128b7428d4e31f393bc70587d0b44d3 GIT binary patch literal 38328 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

141E;Qoq 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 literal 0 HcmV?d00001 From d3a7950e785801577edf5b22dc3f8b8cb4378590 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 19 Apr 2023 09:41:53 +0900 Subject: [PATCH 05/12] Add clock statistics --- .../clock-face/clock-face.component.scss | 1 + .../app/components/clock/clock.component.html | 28 ++++++++++++++++ .../app/components/clock/clock.component.scss | 33 +++++++++++++++++++ .../app/components/clock/clock.component.ts | 12 +++++-- frontend/src/styles.scss | 4 +++ 5 files changed, 75 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/components/clock-face/clock-face.component.scss b/frontend/src/app/components/clock-face/clock-face.component.scss index d671341a6..1ca2ce914 100644 --- a/frontend/src/app/components/clock-face/clock-face.component.scss +++ b/frontend/src/app/components/clock-face/clock-face.component.scss @@ -40,6 +40,7 @@ fill: none; stroke: white; stroke-width: 2px; + stroke-linecap: butt; &.minor { stroke-opacity: 0.5; diff --git a/frontend/src/app/components/clock/clock.component.html b/frontend/src/app/components/clock/clock.component.html index 74e06418d..ee62dd521 100644 --- a/frontend/src/app/components/clock/clock.component.html +++ b/frontend/src/app/components/clock/clock.component.html @@ -31,4 +31,32 @@

+
+

fiat price

+

+ +

+
+
+

priority rate

+

{{ recommendedFees.fastestFee }} sat/vB

+
+
+

+

block size

+
+
+

{{ block.tx_count | number }}

+

transactions

+
+ +
+

+

memory usage

+
+
+

{{ mempoolInfo.size | number }}

+

unconfirmed

+
+
\ No newline at end of file diff --git a/frontend/src/app/components/clock/clock.component.scss b/frontend/src/app/components/clock/clock.component.scss index a27c62499..3ccf6c0df 100644 --- a/frontend/src/app/components/clock/clock.component.scss +++ b/frontend/src/app/components/clock/clock.component.scss @@ -10,6 +10,7 @@ flex-direction: column; justify-content: flex-start; + --chain-height: 60px; --clock-width: 300px; .clockchain-bar, .clock-face { @@ -37,6 +38,38 @@ align-items: center; z-index: 1; } + + .stats { + position: absolute; + z-index: 3; + + p { + margin: 0; + font-size: calc(0.05 * var(--clock-width)); + line-height: calc(0.07 * var(--clock-width)); + opacity: 0.8; + + ::ng-deep .symbol { + font-size: inherit; + color: white; + } + } + + &.top { + top: calc(var(--chain-height) + 2%); + } + &.bottom { + bottom: 2%; + } + &.left { + left: 5%; + } + &.right { + right: 5%; + text-align: end; + text-align: right; + } + } } .title-wrapper { diff --git a/frontend/src/app/components/clock/clock.component.ts b/frontend/src/app/components/clock/clock.component.ts index c804860af..bc4e5625e 100644 --- a/frontend/src/app/components/clock/clock.component.ts +++ b/frontend/src/app/components/clock/clock.component.ts @@ -1,8 +1,9 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Input, OnInit } from '@angular/core'; -import { Subscription } from 'rxjs'; +import { Observable, Subscription } from 'rxjs'; import { StateService } from '../../services/state.service'; import { BlockExtended } from '../../interfaces/node-api.interface'; import { WebsocketService } from '../../services/websocket.service'; +import { MempoolInfo, Recommendedfees } from '../../interfaces/websocket.interface'; @Component({ selector: 'app-clock', @@ -11,8 +12,10 @@ import { WebsocketService } from '../../services/websocket.service'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ClockComponent implements OnInit { - @Input() mode: string = 'block'; + @Input() mode: 'block' | 'mempool' = 'block'; blocksSubscription: Subscription; + recommendedFees$: Observable; + mempoolInfo$: Observable; block: BlockExtended; clockSize: number = 300; chainWidth: number = 384; @@ -47,6 +50,8 @@ export class ClockComponent implements OnInit { this.cd.markForCheck(); } }); + this.recommendedFees$ = this.stateService.recommendedFees$; + this.mempoolInfo$ = this.stateService.mempoolInfo$; } getStyleForBlock(block: BlockExtended) { @@ -75,7 +80,8 @@ export class ClockComponent implements OnInit { height: `${size}px`, }; this.wrapperStyle = { - '--clock-width': `${this.clockSize}px` + '--clock-width': `${this.clockSize}px`, + '--chain-height': `${this.chainHeight}px` }; this.cd.markForCheck(); } diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index fbaaa5ed2..e58bcdc6a 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -285,6 +285,10 @@ body { color: #fff; } +.white-color { + color: white; +} + .green-color { color: #3bcc49; } From 056d61a28d47a9008793b22409fe03ebc46c1b69 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 19 Apr 2023 10:09:44 +0900 Subject: [PATCH 06/12] clock i18n --- .../app/components/clock/clock.component.html | 19 +++++++++++-------- .../app/components/clock/clock.component.scss | 4 ++++ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/components/clock/clock.component.html b/frontend/src/app/components/clock/clock.component.html index ee62dd521..c47133495 100644 --- a/frontend/src/app/components/clock/clock.component.html +++ b/frontend/src/app/components/clock/clock.component.html @@ -32,31 +32,34 @@
-

fiat price

+

fiat price

-

priority rate

-

{{ recommendedFees.fastestFee }} sat/vB

+

priority rate

+

{{ recommendedFees.fastestFee }} sat/vB

-

block size

+

block size

-

{{ block.tx_count | number }}

-

transactions

+

+ + {{ i }} transaction + {{ i }} transactions +

-

memory usage

+

memory usage

{{ mempoolInfo.size | number }}

-

unconfirmed

+

unconfirmed

\ No newline at end of file diff --git a/frontend/src/app/components/clock/clock.component.scss b/frontend/src/app/components/clock/clock.component.scss index 3ccf6c0df..cffe3ee69 100644 --- a/frontend/src/app/components/clock/clock.component.scss +++ b/frontend/src/app/components/clock/clock.component.scss @@ -49,6 +49,10 @@ line-height: calc(0.07 * var(--clock-width)); opacity: 0.8; + &.force-wrap { + word-spacing: 1000px; + } + ::ng-deep .symbol { font-size: inherit; color: white; From fdb0cf509d99b851d8739d421935db051ce6534f Mon Sep 17 00:00:00 2001 From: Mononaut Date: Thu, 20 Apr 2023 00:30:55 +0900 Subject: [PATCH 07/12] query param toggle for clock stats --- .../app/components/clock/clock.component.html | 58 ++++++++++--------- .../app/components/clock/clock.component.ts | 9 +++ 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/frontend/src/app/components/clock/clock.component.html b/frontend/src/app/components/clock/clock.component.html index c47133495..e444664c9 100644 --- a/frontend/src/app/components/clock/clock.component.html +++ b/frontend/src/app/components/clock/clock.component.html @@ -31,35 +31,37 @@
-
-

fiat price

-

- -

-
-
-

priority rate

-

{{ recommendedFees.fastestFee }} sat/vB

-
-
-

-

block size

-
-
-

- - {{ i }} transaction - {{ i }} transactions -

-
- -
-

-

memory usage

+ +
+

fiat price

+

+ +

-
-

{{ mempoolInfo.size | number }}

-

unconfirmed

+
+

priority rate

+

{{ recommendedFees.fastestFee }} sat/vB

+
+

+

block size

+
+
+

+ + {{ i }} transaction + {{ i }} transactions +

+
+ +
+

+

memory usage

+
+
+

{{ mempoolInfo.size | number }}

+

unconfirmed

+
+
\ No newline at end of file diff --git a/frontend/src/app/components/clock/clock.component.ts b/frontend/src/app/components/clock/clock.component.ts index bc4e5625e..f66ba5c15 100644 --- a/frontend/src/app/components/clock/clock.component.ts +++ b/frontend/src/app/components/clock/clock.component.ts @@ -4,6 +4,7 @@ import { StateService } from '../../services/state.service'; import { BlockExtended } from '../../interfaces/node-api.interface'; import { WebsocketService } from '../../services/websocket.service'; import { MempoolInfo, Recommendedfees } from '../../interfaces/websocket.interface'; +import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'app-clock', @@ -13,6 +14,7 @@ import { MempoolInfo, Recommendedfees } from '../../interfaces/websocket.interfa }) export class ClockComponent implements OnInit { @Input() mode: 'block' | 'mempool' = 'block'; + hideStats: boolean = false; blocksSubscription: Subscription; recommendedFees$: Observable; mempoolInfo$: Observable; @@ -36,12 +38,18 @@ export class ClockComponent implements OnInit { constructor( public stateService: StateService, private websocketService: WebsocketService, + private route: ActivatedRoute, private cd: ChangeDetectorRef, ) {} ngOnInit(): void { this.resizeCanvas(); this.websocketService.want(['blocks']); + + this.route.queryParams.subscribe((params) => { + this.hideStats = params && params.stats === 'false'; + }); + this.blocksSubscription = this.stateService.blocks$ .subscribe(([block]) => { if (block) { @@ -50,6 +58,7 @@ export class ClockComponent implements OnInit { this.cd.markForCheck(); } }); + this.recommendedFees$ = this.stateService.recommendedFees$; this.mempoolInfo$ = this.stateService.mempoolInfo$; } From 1fccd70379efb0919147ed23b63f4a8f36e61443 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Thu, 20 Apr 2023 05:30:24 +0900 Subject: [PATCH 08/12] clock size query params --- .../app/components/clock/clock.component.ts | 26 ++++++++++++------- .../clockchain/clockchain.component.html | 5 +++- .../clockchain/clockchain.component.scss | 3 +-- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/frontend/src/app/components/clock/clock.component.ts b/frontend/src/app/components/clock/clock.component.ts index f66ba5c15..dea2de4c8 100644 --- a/frontend/src/app/components/clock/clock.component.ts +++ b/frontend/src/app/components/clock/clock.component.ts @@ -25,6 +25,8 @@ export class ClockComponent implements OnInit { blockStyle; blockSizerStyle; wrapperStyle; + limitWidth: number; + limitHeight: number; gradientColors = { '': ['#9339f4', '#105fb0'], @@ -40,16 +42,18 @@ export class ClockComponent implements OnInit { private websocketService: WebsocketService, private route: ActivatedRoute, private cd: ChangeDetectorRef, - ) {} + ) { + this.route.queryParams.subscribe((params) => { + this.hideStats = params && params.stats === 'false'; + this.limitWidth = Number.parseInt(params.width) || null; + this.limitHeight = Number.parseInt(params.height) || null; + }); + } ngOnInit(): void { this.resizeCanvas(); this.websocketService.want(['blocks']); - this.route.queryParams.subscribe((params) => { - this.hideStats = params && params.stats === 'false'; - }); - this.blocksSubscription = this.stateService.blocks$ .subscribe(([block]) => { if (block) { @@ -78,9 +82,11 @@ export class ClockComponent implements OnInit { @HostListener('window:resize', ['$event']) resizeCanvas(): void { - this.chainWidth = window.innerWidth; - this.chainHeight = Math.max(60, window.innerHeight / 8); - this.clockSize = Math.min(800, window.innerWidth, window.innerHeight - (1.4 * this.chainHeight)); + const windowWidth = this.limitWidth || window.innerWidth; + const windowHeight = this.limitHeight || window.innerHeight; + this.chainWidth = windowWidth; + this.chainHeight = Math.max(60, windowHeight / 8); + this.clockSize = Math.min(800, windowWidth, windowHeight - (1.4 * this.chainHeight)); const size = Math.ceil(this.clockSize / 75) * 75; const margin = (this.clockSize - size) / 2; this.blockSizerStyle = { @@ -90,7 +96,9 @@ export class ClockComponent implements OnInit { }; this.wrapperStyle = { '--clock-width': `${this.clockSize}px`, - '--chain-height': `${this.chainHeight}px` + '--chain-height': `${this.chainHeight}px`, + 'width': this.limitWidth ? `${this.limitWidth}px` : undefined, + 'height': this.limitHeight ? `${this.limitHeight}px` : undefined, }; this.cd.markForCheck(); } diff --git a/frontend/src/app/components/clockchain/clockchain.component.html b/frontend/src/app/components/clockchain/clockchain.component.html index 3a28296ca..7ef320333 100644 --- a/frontend/src/app/components/clockchain/clockchain.component.html +++ b/frontend/src/app/components/clockchain/clockchain.component.html @@ -1,4 +1,7 @@ -
+
diff --git a/frontend/src/app/components/clockchain/clockchain.component.scss b/frontend/src/app/components/clockchain/clockchain.component.scss index acff1e725..6ffc144e9 100644 --- a/frontend/src/app/components/clockchain/clockchain.component.scss +++ b/frontend/src/app/components/clockchain/clockchain.component.scss @@ -21,9 +21,8 @@ .position-container { position: absolute; - left: 0; + left: 50%; top: 0; - transform: translateX(50vw); } .black-background { From 19353fc1d05b087f333a73bc63f56e749e80321c Mon Sep 17 00:00:00 2001 From: Mononaut Date: Fri, 21 Apr 2023 23:13:19 +0900 Subject: [PATCH 09/12] rename clock components --- frontend/src/app/app-routing.module.ts | 12 ++++++------ .../components/clock-face/clock-face.component.ts | 1 - .../src/app/components/clock/clock-a.component.ts | 7 ------- .../src/app/components/clock/clock-b.component.ts | 7 ------- ...b.component.html => clock-mempool.component.html} | 0 .../app/components/clock/clock-mempool.component.ts | 7 +++++++ ...k-a.component.html => clock-mined.component.html} | 0 .../app/components/clock/clock-mined.component.ts | 7 +++++++ .../src/app/components/clock/clock.component.html | 4 ++-- .../src/app/components/clock/clock.component.scss | 2 +- frontend/src/app/shared/shared.module.ts | 12 ++++++------ 11 files changed, 29 insertions(+), 30 deletions(-) delete mode 100644 frontend/src/app/components/clock/clock-a.component.ts delete mode 100644 frontend/src/app/components/clock/clock-b.component.ts rename frontend/src/app/components/clock/{clock-b.component.html => clock-mempool.component.html} (100%) create mode 100644 frontend/src/app/components/clock/clock-mempool.component.ts rename frontend/src/app/components/clock/{clock-a.component.html => clock-mined.component.html} (100%) create mode 100644 frontend/src/app/components/clock/clock-mined.component.ts diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 0146fb535..0fe496d3e 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -4,8 +4,8 @@ import { AppPreloadingStrategy } from './app.preloading-strategy' import { StartComponent } from './components/start/start.component'; import { TransactionComponent } from './components/transaction/transaction.component'; import { BlockComponent } from './components/block/block.component'; -import { ClockAComponent } from './components/clock/clock-a.component'; -import { ClockBComponent } from './components/clock/clock-b.component'; +import { ClockMinedComponent as ClockMinedComponent } from './components/clock/clock-mined.component'; +import { ClockMempoolComponent as ClockMempoolComponent } from './components/clock/clock-mempool.component'; import { AddressComponent } from './components/address/address.component'; import { MasterPageComponent } from './components/master-page/master-page.component'; import { AboutComponent } from './components/about/about.component'; @@ -358,12 +358,12 @@ let routes: Routes = [ ], }, { - path: 'clock-face-a', - component: ClockAComponent, + path: 'clock-mined', + component: ClockMinedComponent, }, { - path: 'clock-face-b', - component: ClockBComponent, + path: 'clock-mempool', + component: ClockMempoolComponent, }, { path: 'status', diff --git a/frontend/src/app/components/clock-face/clock-face.component.ts b/frontend/src/app/components/clock-face/clock-face.component.ts index 9c373a50d..01e439e8e 100644 --- a/frontend/src/app/components/clock-face/clock-face.component.ts +++ b/frontend/src/app/components/clock-face/clock-face.component.ts @@ -39,7 +39,6 @@ export class ClockFaceComponent implements OnInit, OnChanges, OnDestroy { this.updateTime(); }) ).subscribe(); - this.websocketService.want(['blocks']); this.blocksSubscription = this.stateService.blocks$ .subscribe(([block]) => { if (block) { diff --git a/frontend/src/app/components/clock/clock-a.component.ts b/frontend/src/app/components/clock/clock-a.component.ts deleted file mode 100644 index 50f834bad..000000000 --- a/frontend/src/app/components/clock/clock-a.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-clock-a', - templateUrl: './clock-a.component.html', -}) -export class ClockAComponent {} diff --git a/frontend/src/app/components/clock/clock-b.component.ts b/frontend/src/app/components/clock/clock-b.component.ts deleted file mode 100644 index b47c9dba3..000000000 --- a/frontend/src/app/components/clock/clock-b.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-clock-b', - templateUrl: './clock-b.component.html', -}) -export class ClockBComponent {} diff --git a/frontend/src/app/components/clock/clock-b.component.html b/frontend/src/app/components/clock/clock-mempool.component.html similarity index 100% rename from frontend/src/app/components/clock/clock-b.component.html rename to frontend/src/app/components/clock/clock-mempool.component.html diff --git a/frontend/src/app/components/clock/clock-mempool.component.ts b/frontend/src/app/components/clock/clock-mempool.component.ts new file mode 100644 index 000000000..7e99cc08b --- /dev/null +++ b/frontend/src/app/components/clock/clock-mempool.component.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-clock-mempool', + templateUrl: './clock-mempool.component.html', +}) +export class ClockMempoolComponent {} diff --git a/frontend/src/app/components/clock/clock-a.component.html b/frontend/src/app/components/clock/clock-mined.component.html similarity index 100% rename from frontend/src/app/components/clock/clock-a.component.html rename to frontend/src/app/components/clock/clock-mined.component.html diff --git a/frontend/src/app/components/clock/clock-mined.component.ts b/frontend/src/app/components/clock/clock-mined.component.ts new file mode 100644 index 000000000..b26815ac6 --- /dev/null +++ b/frontend/src/app/components/clock/clock-mined.component.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-clock-mined', + templateUrl: './clock-mined.component.html', +}) +export class ClockMinedComponent {} diff --git a/frontend/src/app/components/clock/clock.component.html b/frontend/src/app/components/clock/clock.component.html index e444664c9..8da274a5c 100644 --- a/frontend/src/app/components/clock/clock.component.html +++ b/frontend/src/app/components/clock/clock.component.html @@ -42,11 +42,11 @@

priority rate

{{ recommendedFees.fastestFee }} sat/vB

-
+

block size

-
+

{{ i }} transaction diff --git a/frontend/src/app/components/clock/clock.component.scss b/frontend/src/app/components/clock/clock.component.scss index cffe3ee69..2af5317a3 100644 --- a/frontend/src/app/components/clock/clock.component.scss +++ b/frontend/src/app/components/clock/clock.component.scss @@ -50,7 +50,7 @@ opacity: 0.8; &.force-wrap { - word-spacing: 1000px; + word-spacing: 10000px; } ::ng-deep .symbol { diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 21cbb17a8..6e8d8d0f2 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -94,8 +94,8 @@ import { MempoolBlockOverviewComponent } from '../components/mempool-block-overv import { ClockchainComponent } from '../components/clockchain/clockchain.component'; import { ClockFaceComponent } from '../components/clock-face/clock-face.component'; import { ClockComponent } from '../components/clock/clock.component'; -import { ClockAComponent } from '../components/clock/clock-a.component'; -import { ClockBComponent } from '../components/clock/clock-b.component'; +import { ClockMinedComponent } from '../components/clock/clock-mined.component'; +import { ClockMempoolComponent } from '../components/clock/clock-mempool.component'; @NgModule({ declarations: [ @@ -183,8 +183,8 @@ import { ClockBComponent } from '../components/clock/clock-b.component'; MempoolBlockOverviewComponent, ClockchainComponent, ClockComponent, - ClockAComponent, - ClockBComponent, + ClockMinedComponent, + ClockMempoolComponent, ClockFaceComponent, ], imports: [ @@ -297,8 +297,8 @@ import { ClockBComponent } from '../components/clock/clock-b.component'; MempoolBlockOverviewComponent, ClockchainComponent, ClockComponent, - ClockAComponent, - ClockBComponent, + ClockMinedComponent, + ClockMempoolComponent, ClockFaceComponent, ] }) From 07dddd857bc3840fedfe8f24754ba7a177a6e098 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Thu, 4 May 2023 17:49:46 -0400 Subject: [PATCH 10/12] resize clock labels --- .../app/components/clock/clock.component.html | 16 ++++++++-------- .../app/components/clock/clock.component.scss | 9 +++++++-- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/components/clock/clock.component.html b/frontend/src/app/components/clock/clock.component.html index 8da274a5c..b3ca53c60 100644 --- a/frontend/src/app/components/clock/clock.component.html +++ b/frontend/src/app/components/clock/clock.component.html @@ -33,34 +33,34 @@

-

fiat price

+

fiat price

-

priority rate

-

{{ recommendedFees.fastestFee }} sat/vB

+

priority rate

+

{{ recommendedFees.fastestFee + 300 }} sat/vB

-

block size

+

block size

- {{ i }} transaction - {{ i }} transactions + {{ i }} transaction + {{ i }} transactions

-

memory usage

+

memory usage

{{ mempoolInfo.size | number }}

-

unconfirmed

+

unconfirmed

diff --git a/frontend/src/app/components/clock/clock.component.scss b/frontend/src/app/components/clock/clock.component.scss index 2af5317a3..f1d70dcd8 100644 --- a/frontend/src/app/components/clock/clock.component.scss +++ b/frontend/src/app/components/clock/clock.component.scss @@ -45,8 +45,8 @@ p { margin: 0; - font-size: calc(0.05 * var(--clock-width)); - line-height: calc(0.07 * var(--clock-width)); + font-size: calc(0.055 * var(--clock-width)); + line-height: calc(0.05 * var(--clock-width)); opacity: 0.8; &.force-wrap { @@ -59,6 +59,11 @@ } } + .label { + font-size: calc(0.04 * var(--clock-width)); + line-height: calc(0.05 * var(--clock-width)); + } + &.top { top: calc(var(--chain-height) + 2%); } From 9671259f5cee8718a18ac961f695dab005fddabb Mon Sep 17 00:00:00 2001 From: Mononaut Date: Thu, 4 May 2023 17:50:27 -0400 Subject: [PATCH 11/12] clock selected block arrow --- .../blockchain-blocks.component.html | 5 +++++ .../blockchain-blocks.component.scss | 12 ++++++++++++ .../blockchain-blocks/blockchain-blocks.component.ts | 1 + .../src/app/components/clock/clock.component.html | 2 +- .../src/app/components/clock/clock.component.scss | 2 +- .../components/clockchain/clockchain.component.html | 4 ++-- .../components/clockchain/clockchain.component.ts | 1 + .../mempool-blocks/mempool-blocks.component.html | 5 +++++ .../mempool-blocks/mempool-blocks.component.scss | 12 ++++++++++++ .../mempool-blocks/mempool-blocks.component.ts | 1 + 10 files changed, 41 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html index 0a2f0decb..8ea5acef6 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html @@ -3,6 +3,11 @@ *ngIf="static || (loadingBlocks$ | async) === false; else loadingBlocksTemplate">
+
- +
diff --git a/frontend/src/app/components/clock/clock.component.scss b/frontend/src/app/components/clock/clock.component.scss index f1d70dcd8..20baf02ee 100644 --- a/frontend/src/app/components/clock/clock.component.scss +++ b/frontend/src/app/components/clock/clock.component.scss @@ -23,7 +23,7 @@ width: 100%; height: 15.625%; z-index: 2; - overflow: hidden; + // overflow: hidden; // background: #1d1f31; // box-shadow: 0 0 15px #000; } diff --git a/frontend/src/app/components/clockchain/clockchain.component.html b/frontend/src/app/components/clockchain/clockchain.component.html index 7ef320333..169de58d4 100644 --- a/frontend/src/app/components/clockchain/clockchain.component.html +++ b/frontend/src/app/components/clockchain/clockchain.component.html @@ -5,8 +5,8 @@
- - + +
+
  diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.scss b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.scss index 6d1ec326e..40f43a015 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.scss +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.scss @@ -157,4 +157,16 @@ #arrow-up { transform: translateX(70px); } +} + +.spotlight-bottom { + position: absolute; + width: calc(0.6 * var(--block-size)); + height: calc(0.25 * var(--block-size)); + border-left: solid calc(0.3 * var(--block-size)) transparent; + border-bottom: solid calc(0.3 * var(--block-size)) white; + border-right: solid calc(0.3 * var(--block-size)) transparent; + transform: translate(calc(-0.2 * var(--block-size)), calc(1.1 * var(--block-size))); + border-radius: 2px; + z-index: -1; } \ No newline at end of file diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts index 6267eed21..93498d535 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts @@ -27,6 +27,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { @Input() minimal: boolean = false; @Input() blockWidth: number = 125; @Input() count: number = null; + @Input() spotlight: number = 0; specialBlocks = specialBlocks; mempoolBlocks: MempoolBlock[] = []; From f20bfb025be25eba8f37ef095667b8ccc9013dac Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 8 May 2023 08:57:24 -0600 Subject: [PATCH 12/12] fix clock merge conflicts --- .../block-overview-graph.component.ts | 4 ++- .../block-overview-graph/block-scene.ts | 28 +++++++++++++------ .../app/components/clock/clock.component.html | 2 +- .../mempool-block-overview.component.html | 1 + .../mempool-block-overview.component.ts | 1 + .../mempool-blocks.component.html | 26 ----------------- frontend/src/app/graphs/graphs.module.ts | 2 -- frontend/src/index.mempool.html | 2 +- 8 files changed, 27 insertions(+), 39 deletions(-) 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 940939470..15e41f1a7 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 @@ -23,6 +23,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On @Input() unavailable: boolean = false; @Input() auditHighlighting: boolean = false; @Input() blockConversion: Price; + @Input() pixelAlign: boolean = false; @Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>(); @Output() txHoverEvent = new EventEmitter(); @Output() readyEvent = new EventEmitter(); @@ -201,7 +202,8 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On 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, highlighting: this.auditHighlighting }); + blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray, + highlighting: this.auditHighlighting, pixelAlign: this.pixelAlign }); this.start(); } } 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 1c0072e31..0cd5c9391 100644 --- a/frontend/src/app/components/block-overview-graph/block-scene.ts +++ b/frontend/src/app/components/block-overview-graph/block-scene.ts @@ -15,6 +15,7 @@ export default class BlockScene { gridWidth: number; gridHeight: number; gridSize: number; + pixelAlign: boolean; vbytesPerUnit: number; unitPadding: number; unitWidth: number; @@ -23,19 +24,24 @@ export default class BlockScene { animateUntil = 0; dirty: boolean; - constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }: + constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting, pixelAlign }: { width: number, height: number, resolution: number, blockLimit: number, - orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean } + orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean, pixelAlign: boolean } ) { - this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }); + this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting, pixelAlign }); } resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void { this.width = width; this.height = height; this.gridSize = this.width / this.gridWidth; - this.unitPadding = Math.max(1, Math.floor(this.gridSize / 2.5)); - this.unitWidth = this.gridSize - (this.unitPadding); + if (this.pixelAlign) { + this.unitPadding = Math.max(1, Math.floor(this.gridSize / 2.5)); + this.unitWidth = this.gridSize - (this.unitPadding); + } else { + this.unitPadding = width / 500; + this.unitWidth = this.gridSize - (this.unitPadding * 2); + } this.dirty = true; if (this.initialised && this.scene) { @@ -209,14 +215,15 @@ export default class BlockScene { this.animateUntil = Math.max(this.animateUntil, tx.setHover(value)); } - private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }: + private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting, pixelAlign }: { width: number, height: number, resolution: number, blockLimit: number, - orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean } + orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean, pixelAlign: boolean } ): void { this.orientation = orientation; this.flip = flip; this.vertexArray = vertexArray; this.highlightingEnabled = highlighting; + this.pixelAlign = pixelAlign; this.scene = { count: 0, @@ -342,7 +349,12 @@ export default class BlockScene { private gridToScreen(position: Square | void): Square { if (position) { const slotSize = (position.s * this.gridSize); - const squareSize = slotSize - (this.unitPadding); + let squareSize; + if (this.pixelAlign) { + squareSize = slotSize - (this.unitPadding); + } else { + squareSize = slotSize - (this.unitPadding * 2); + } // The grid is laid out notionally left-to-right, bottom-to-top, // so we rotate and/or flip the y axis to match the target configuration. diff --git a/frontend/src/app/components/clock/clock.component.html b/frontend/src/app/components/clock/clock.component.html index e54626aa4..914450a79 100644 --- a/frontend/src/app/components/clock/clock.component.html +++ b/frontend/src/app/components/clock/clock.component.html @@ -20,7 +20,7 @@
- +
diff --git a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html index 3cb4ff3e8..37c82afad 100644 --- a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html +++ b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html @@ -5,5 +5,6 @@ [blockLimit]="stateService.blockVSize" [orientation]="timeLtr ? 'right' : 'left'" [flip]="true" + [pixelAlign]="pixelAlign" (txClickEvent)="onTxClick($event)" > diff --git a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts index 30632a862..540046e13 100644 --- a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts +++ b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts @@ -16,6 +16,7 @@ import { Router } from '@angular/router'; }) export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit { @Input() index: number; + @Input() pixelAlign: boolean = false; @Output() txPreviewEvent = new EventEmitter(); @ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent; diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.html b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.html index e405c3cfd..11dc28ad9 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.html +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.html @@ -40,32 +40,6 @@ () {{ i }} blocks
-
- {{ projectedBlock.feeRange[0] | number:feeRounding }} - {{ projectedBlock.feeRange[projectedBlock.feeRange.length - 1] | number:feeRounding }} sat/vB -
-
- -
-
-
- - {{ i }} transaction - {{ i }} transactions -
-
- - - - - - -
- -
- () - {{ i }} blocks -
-
diff --git a/frontend/src/app/graphs/graphs.module.ts b/frontend/src/app/graphs/graphs.module.ts index a7e627736..a4e4f5bfc 100644 --- a/frontend/src/app/graphs/graphs.module.ts +++ b/frontend/src/app/graphs/graphs.module.ts @@ -14,7 +14,6 @@ import { LbtcPegsGraphComponent } from '../components/lbtc-pegs-graph/lbtc-pegs- import { GraphsComponent } from '../components/graphs/graphs.component'; import { StatisticsComponent } from '../components/statistics/statistics.component'; import { MempoolBlockComponent } from '../components/mempool-block/mempool-block.component'; -// import { MempoolBlockOverviewComponent } from '../components/mempool-block-overview/mempool-block-overview.component'; import { PoolRankingComponent } from '../components/pool-ranking/pool-ranking.component'; import { PoolComponent } from '../components/pool/pool.component'; import { TelevisionComponent } from '../components/television/television.component'; @@ -42,7 +41,6 @@ import { CommonModule } from '@angular/common'; BlockFeeRatesGraphComponent, BlockSizesWeightsGraphComponent, FeeDistributionGraphComponent, - // MempoolBlockOverviewComponent, IncomingTransactionsGraphComponent, MempoolGraphComponent, LbtcPegsGraphComponent, diff --git a/frontend/src/index.mempool.html b/frontend/src/index.mempool.html index 02765c0ba..60f1b4421 100644 --- a/frontend/src/index.mempool.html +++ b/frontend/src/index.mempool.html @@ -32,7 +32,7 @@ - +