feat: NCWF-5/6/7/8/9 chess analysis page and engine integration (#11)
Co-authored-by: Janis Eccarius <eccariusjanis@gmail.com> Reviewed-on: #11
This commit was merged in pull request #11.
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.timeline-wrap {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.timeline-svg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.midline {
|
||||
stroke: rgba(255, 255, 255, 0.12);
|
||||
stroke-width: 1;
|
||||
stroke-dasharray: 4 4;
|
||||
}
|
||||
|
||||
.area-white {
|
||||
fill: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.area-black {
|
||||
fill: rgba(20, 20, 30, 0.55);
|
||||
}
|
||||
|
||||
.eval-line {
|
||||
fill: none;
|
||||
stroke: var(--nc-neon, #ff45c8);
|
||||
stroke-width: 1.5;
|
||||
stroke-linejoin: round;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.active-marker {
|
||||
stroke: var(--nc-warning, #ffb13a);
|
||||
stroke-width: 1.5;
|
||||
stroke-dasharray: 3 3;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.empty {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
padding: 12px 0;
|
||||
font-family: var(--nc-mono, monospace);
|
||||
letter-spacing: 0.06em;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
@if (points.length === 0) {
|
||||
<div class="empty">No evaluation data yet.</div>
|
||||
} @else {
|
||||
<div class="timeline-wrap" role="img" aria-label="Evaluation timeline">
|
||||
<svg
|
||||
class="timeline-svg"
|
||||
[attr.viewBox]="'0 0 ' + svgWidth + ' ' + svgHeight"
|
||||
preserveAspectRatio="none"
|
||||
>
|
||||
<!-- Midline (balanced) -->
|
||||
<line
|
||||
x1="0"
|
||||
[attr.y1]="svgHeight / 2"
|
||||
[attr.x2]="svgWidth"
|
||||
[attr.y2]="svgHeight / 2"
|
||||
class="midline"
|
||||
/>
|
||||
|
||||
<!-- White-advantage fill (clip above midline) -->
|
||||
<defs>
|
||||
<clipPath id="clip-white">
|
||||
<rect x="0" y="0" [attr.width]="svgWidth" [attr.height]="svgHeight / 2" />
|
||||
</clipPath>
|
||||
<clipPath id="clip-black">
|
||||
<rect
|
||||
x="0"
|
||||
[attr.y]="svgHeight / 2"
|
||||
[attr.width]="svgWidth"
|
||||
[attr.height]="svgHeight / 2"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<polygon [attr.points]="polylineWhite" class="area-white" clip-path="url(#clip-white)" />
|
||||
|
||||
<polygon [attr.points]="polylineBlack" class="area-black" clip-path="url(#clip-black)" />
|
||||
|
||||
<!-- Eval line -->
|
||||
<polyline [attr.points]="evalPolyline" class="eval-line" />
|
||||
|
||||
<!-- Active ply marker -->
|
||||
@if (activeX() !== null) {
|
||||
<line
|
||||
[attr.x1]="activeX()"
|
||||
y1="0"
|
||||
[attr.x2]="activeX()"
|
||||
[attr.y2]="svgHeight"
|
||||
class="active-marker"
|
||||
/>
|
||||
}
|
||||
</svg>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Component, Input, OnChanges } from '@angular/core';
|
||||
import { AnnotatedMove } from '../../models/analysis.models';
|
||||
|
||||
interface TimelinePoint {
|
||||
x: number;
|
||||
y: number;
|
||||
eval: number;
|
||||
san: string;
|
||||
plyIndex: number;
|
||||
}
|
||||
|
||||
const CLAMP = 5; // clamp eval to ±5 pawns for display
|
||||
const HEIGHT = 80;
|
||||
const WIDTH = 600;
|
||||
|
||||
@Component({
|
||||
selector: 'app-eval-timeline',
|
||||
standalone: true,
|
||||
imports: [],
|
||||
templateUrl: './eval-timeline.component.html',
|
||||
styleUrl: './eval-timeline.component.css',
|
||||
})
|
||||
export class EvalTimelineComponent implements OnChanges {
|
||||
@Input({ required: true }) moves: AnnotatedMove[] = [];
|
||||
@Input() activePly: number | null = null;
|
||||
|
||||
points: TimelinePoint[] = [];
|
||||
evalPolyline = '';
|
||||
polylineWhite = '';
|
||||
polylineBlack = '';
|
||||
svgWidth = WIDTH;
|
||||
svgHeight = HEIGHT;
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.buildChart();
|
||||
}
|
||||
|
||||
activeX(): number | null {
|
||||
if (this.activePly === null) return null;
|
||||
const pt = this.points[this.activePly];
|
||||
return pt ? pt.x : null;
|
||||
}
|
||||
|
||||
private buildChart(): void {
|
||||
if (this.moves.length === 0) {
|
||||
this.points = [];
|
||||
this.evalPolyline = '';
|
||||
this.polylineWhite = '';
|
||||
this.polylineBlack = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const total = this.moves.length;
|
||||
this.points = this.moves.map((m, i) => {
|
||||
const evalValue = m.evalAfter ?? 0;
|
||||
const clamped = Math.max(-CLAMP, Math.min(CLAMP, evalValue));
|
||||
const x = (i / Math.max(total - 1, 1)) * WIDTH;
|
||||
// y=0 => white winning (top), y=HEIGHT => black winning (bottom)
|
||||
const y = ((CLAMP - clamped) / (CLAMP * 2)) * HEIGHT;
|
||||
return { x, y, eval: evalValue, san: m.san, plyIndex: i };
|
||||
});
|
||||
|
||||
const coordStr = this.points.map((p) => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' ');
|
||||
this.evalPolyline = coordStr;
|
||||
|
||||
const mid = HEIGHT / 2;
|
||||
const first = this.points[0];
|
||||
const last = this.points[this.points.length - 1];
|
||||
|
||||
this.polylineWhite = `${first.x.toFixed(1)},${mid} ${coordStr} ${last.x.toFixed(1)},${mid}`;
|
||||
this.polylineBlack = this.polylineWhite;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user