Add last-move arrow + destination pulse so opponent moves are obvious
Draws a lichess-style arrow from the last move's origin to its destination (persists until you reply) and pulses the destination square when a new move lands, so the opponent's move can't be missed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c6337f52c3
commit
5abe960ffd
2 changed files with 50 additions and 1 deletions
|
|
@ -171,7 +171,10 @@ class BoardUI {
|
|||
setOrientation(o) { this.orient = o; this.render(); }
|
||||
load(fen, lastMove) {
|
||||
try { this.chess.load(fen); } catch {}
|
||||
const prev = this.lastMove;
|
||||
this.lastMove = lastMove || null;
|
||||
// flag a genuinely new move so render() can pulse the destination square
|
||||
this._pulse = !!(this.lastMove && (!prev || prev.from !== this.lastMove.from || prev.to !== this.lastMove.to));
|
||||
this.selected = null;
|
||||
this.render();
|
||||
}
|
||||
|
|
@ -195,6 +198,7 @@ class BoardUI {
|
|||
const dark = (FILES.indexOf(file) + rank) % 2 === 0;
|
||||
const cell = el(`<div class="sq ${dark ? 'dark' : 'light'}" data-sq="${sq}"></div>`);
|
||||
if (this.lastMove && (sq === this.lastMove.from || sq === this.lastMove.to)) cell.classList.add('last');
|
||||
if (this.lastMove && sq === this.lastMove.to) { cell.classList.add('lastto'); if (this._pulse) cell.classList.add('pulse'); }
|
||||
if (sq === this.selected) cell.classList.add('sel');
|
||||
if (sq === kingSq) cell.classList.add('check');
|
||||
|
||||
|
|
@ -211,8 +215,41 @@ class BoardUI {
|
|||
if (legalTo.has(sq)) cell.appendChild(el(`<div class="hint ${p ? 'cap' : ''}"></div>`));
|
||||
board.appendChild(cell);
|
||||
});
|
||||
this._drawLastArrow();
|
||||
this._pulse = false;
|
||||
if (this.pendingPromo) this._renderPromo();
|
||||
}
|
||||
// column/row (0..7) of a square in the CURRENT board orientation
|
||||
_cellPos(sq) {
|
||||
const f = FILES.indexOf(sq[0]);
|
||||
const r = +sq[1];
|
||||
return this.orient === 'w'
|
||||
? { col: f, row: 8 - r }
|
||||
: { col: 7 - f, row: r - 1 };
|
||||
}
|
||||
// draw a lichess-style arrow from the last move's origin to its destination
|
||||
_drawLastArrow() {
|
||||
const m = this.lastMove;
|
||||
if (!m || !m.from || !m.to) return;
|
||||
const a = this._cellPos(m.from), b = this._cellPos(m.to);
|
||||
const x1 = a.col + 0.5, y1 = a.row + 0.5;
|
||||
const x2 = b.col + 0.5, y2 = b.row + 0.5;
|
||||
const dx = x2 - x1, dy = y2 - y1;
|
||||
const len = Math.hypot(dx, dy) || 1;
|
||||
const ux = dx / len, uy = dy / len;
|
||||
const head = 0.42; // arrowhead length (in square units)
|
||||
const start = 0.34; // pull the tail out from the piece center
|
||||
const sx = x1 + ux * start, sy = y1 + uy * start;
|
||||
const tipX = x2 - ux * 0.06, tipY = y2 - uy * 0.06;
|
||||
const baseX = tipX - ux * head, baseY = tipY - uy * head;
|
||||
const px = -uy, py = ux; // perpendicular
|
||||
const hw = 0.24; // half-width of arrowhead base
|
||||
const svg = el(`<svg class="arrows" viewBox="0 0 8 8" preserveAspectRatio="none"></svg>`);
|
||||
svg.innerHTML =
|
||||
`<line x1="${sx}" y1="${sy}" x2="${baseX}" y2="${baseY}" class="arrow-line"/>` +
|
||||
`<polygon points="${tipX},${tipY} ${baseX + px * hw},${baseY + py * hw} ${baseX - px * hw},${baseY - py * hw}" class="arrow-head"/>`;
|
||||
this.board.appendChild(svg);
|
||||
}
|
||||
_kingSquare(color) {
|
||||
for (const sq of this.squares()) { const p = this.chess.get(sq); if (p && p.type === 'k' && p.color === color) return sq; }
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -116,11 +116,23 @@ main { flex: 1; width: 100%; }
|
|||
|
||||
/* board */
|
||||
.board {
|
||||
width: min(72vh, 100%); aspect-ratio: 1; margin: 0 auto;
|
||||
width: min(72vh, 100%); aspect-ratio: 1; margin: 0 auto; position: relative;
|
||||
display: grid; grid-template-columns: repeat(8, 1fr); grid-template-rows: repeat(8, 1fr);
|
||||
border-radius: 10px; overflow: hidden; border: 1px solid var(--line);
|
||||
box-shadow: 0 18px 60px rgba(0,0,0,.5); touch-action: none; user-select: none;
|
||||
}
|
||||
/* last-move arrow overlay */
|
||||
.arrows { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 4; }
|
||||
.arrow-line { stroke: var(--red); stroke-width: .17; stroke-linecap: round; opacity: .82; }
|
||||
.arrow-head { fill: var(--red); opacity: .82; }
|
||||
/* destination-square emphasis + one-shot pulse when a new move lands */
|
||||
.sq.lastto::before { content: ''; position: absolute; inset: 0; box-shadow: inset 0 0 0 3px var(--red); opacity: .55; z-index: 1; }
|
||||
.sq.pulse::before { animation: sqpulse 1.1s ease-out 2; }
|
||||
@keyframes sqpulse {
|
||||
0% { box-shadow: inset 0 0 0 3px var(--red); opacity: 1; }
|
||||
60% { box-shadow: inset 0 0 0 7px var(--red); opacity: .2; }
|
||||
100% { box-shadow: inset 0 0 0 3px var(--red); opacity: .55; }
|
||||
}
|
||||
.sq { position: relative; display: flex; align-items: center; justify-content: center; }
|
||||
.sq.light { background: var(--light-sq); }
|
||||
.sq.dark { background: var(--dark-sq); }
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue