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 000000000..372105fbd
Binary files /dev/null and b/frontend/src/resources/clock/gradient.png differ