shogiground
Version:
lishogi.org shogi ui
202 lines (182 loc) • 6.14 kB
text/typescript
import type { State } from './state.js';
import type * as sg from './types.js';
import { allKeys, colors } from './constants.js';
import * as util from './util.js';
export type Mutation<A> = (state: State) => A;
// 0,1 animation goal
// 2,3 animation current status
export type AnimVector = sg.NumberQuad;
export type AnimVectors = Map<sg.Key, AnimVector>;
export type AnimFadings = Map<sg.Key, sg.Piece>;
export type AnimPromotions = Map<sg.Key, sg.Piece>;
export interface AnimPlan {
anims: AnimVectors;
fadings: AnimFadings;
promotions: AnimPromotions;
}
export interface AnimCurrent {
start: DOMHighResTimeStamp;
frequency: sg.KHz;
plan: AnimPlan;
}
export function anim<A>(mutation: Mutation<A>, state: State): A {
return state.animation.enabled ? animate(mutation, state) : render(mutation, state);
}
export function render<A>(mutation: Mutation<A>, state: State): A {
const result = mutation(state);
state.dom.redraw();
return result;
}
interface AnimPiece {
key?: sg.Key;
pos: sg.Pos;
piece: sg.Piece;
}
function makePiece(key: sg.Key, piece: sg.Piece): AnimPiece {
return {
key: key,
pos: util.key2pos(key),
piece: piece,
};
}
function closer(piece: AnimPiece, pieces: AnimPiece[]): AnimPiece | undefined {
return pieces.sort((p1, p2) => {
return util.distanceSq(piece.pos, p1.pos) - util.distanceSq(piece.pos, p2.pos);
})[0];
}
function computePlan(prevPieces: sg.Pieces, prevHands: sg.Hands, current: State): AnimPlan {
const anims: AnimVectors = new Map(),
animedOrigs: sg.Key[] = [],
fadings: AnimFadings = new Map(),
promotions: AnimPromotions = new Map(),
missings: AnimPiece[] = [],
news: AnimPiece[] = [],
prePieces = new Map<sg.Key, AnimPiece>();
for (const [k, p] of prevPieces) {
prePieces.set(k, makePiece(k, p));
}
for (const key of allKeys) {
const curP = current.pieces.get(key),
preP = prePieces.get(key);
if (curP) {
if (preP) {
if (!util.samePiece(curP, preP.piece)) {
missings.push(preP);
news.push(makePiece(key, curP));
}
} else news.push(makePiece(key, curP));
} else if (preP) missings.push(preP);
}
if (current.animation.hands) {
for (const color of colors) {
const curH = current.hands.handMap.get(color),
preH = prevHands.get(color);
if (preH && curH) {
for (const [role, n] of preH) {
const piece: sg.Piece = { role, color },
curN = curH.get(role) || 0;
if (curN < n) {
const handPieceOffset = current.dom.bounds.hands
.pieceBounds()
.get(util.pieceNameOf(piece)),
bounds = current.dom.bounds.board.bounds(),
outPos =
handPieceOffset && bounds
? util.posOfOutsideEl(
handPieceOffset.left,
handPieceOffset.top,
util.sentePov(current.orientation),
current.dimensions,
bounds,
)
: undefined;
if (outPos)
missings.push({
pos: outPos,
piece: piece,
});
}
}
}
}
}
for (const newP of news) {
const preP = closer(
newP,
missings.filter((p) => {
if (util.samePiece(newP.piece, p.piece)) return true;
// checking whether promoted pieces are the same
const pRole = current.promotion.promotesTo(p.piece.role),
pPiece = pRole && { color: p.piece.color, role: pRole };
const nRole = current.promotion.promotesTo(newP.piece.role),
nPiece = nRole && { color: newP.piece.color, role: nRole };
return (
(!!pPiece && util.samePiece(newP.piece, pPiece)) ||
(!!nPiece && util.samePiece(nPiece, p.piece))
);
}),
);
if (preP) {
const vector = [preP.pos[0] - newP.pos[0], preP.pos[1] - newP.pos[1]];
anims.set(newP.key!, vector.concat(vector) as AnimVector);
if (preP.key) animedOrigs.push(preP.key);
if (!util.samePiece(newP.piece, preP.piece) && newP.key) promotions.set(newP.key, preP.piece);
}
}
for (const p of missings) {
if (p.key && !animedOrigs.includes(p.key)) fadings.set(p.key, p.piece);
}
return {
anims: anims,
fadings: fadings,
promotions: promotions,
};
}
function step(state: State, now: DOMHighResTimeStamp): void {
const cur = state.animation.current;
if (cur === undefined) {
// animation was canceled :(
if (!state.dom.destroyed) state.dom.redrawNow();
return;
}
const rest = 1 - (now - cur.start) * cur.frequency;
if (rest <= 0) {
state.animation.current = undefined;
state.dom.redrawNow();
} else {
const ease = easing(rest);
for (const cfg of cur.plan.anims.values()) {
cfg[2] = cfg[0] * ease;
cfg[3] = cfg[1] * ease;
}
state.dom.redrawNow(true); // optimisation: don't render SVG changes during animations
requestAnimationFrame((now = performance.now()) => step(state, now));
}
}
function animate<A>(mutation: Mutation<A>, state: State): A {
// clone state before mutating it
const prevPieces: sg.Pieces = new Map(state.pieces),
prevHands: sg.Hands = new Map([
['sente', new Map(state.hands.handMap.get('sente'))],
['gote', new Map(state.hands.handMap.get('gote'))],
]);
const result = mutation(state),
plan = computePlan(prevPieces, prevHands, state);
if (plan.anims.size || plan.fadings.size) {
const alreadyRunning = state.animation.current?.start !== undefined;
state.animation.current = {
start: performance.now(),
frequency: 1 / Math.max(state.animation.duration, 1),
plan: plan,
};
if (!alreadyRunning) step(state, performance.now());
} else {
// don't animate, just render right away
state.dom.redraw();
}
return result;
}
// https://gist.github.com/gre/1650294
function easing(t: number): number {
return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
}