shogiground
Version:
lishogi.org shogi ui
201 lines (172 loc) • 6 kB
text/typescript
import type * as sg from './types.js';
import { allKeys, colors } from './constants.js';
export const pos2key = (pos: sg.Pos): sg.Key => allKeys[pos[0] + 16 * pos[1]];
export const key2pos = (k: sg.Key): sg.Pos => {
if (k.length > 2) return [k.charCodeAt(1) - 39, k.charCodeAt(2) - 97];
else return [k.charCodeAt(0) - 49, k.charCodeAt(1) - 97];
};
export function memo<A>(f: () => A): sg.Memo<A> {
let v: A | undefined;
const ret = (): A => {
if (v === undefined) v = f();
return v;
};
ret.clear = () => {
v = undefined;
};
return ret;
}
export function callUserFunction<T extends (...args: any[]) => void>(
f: T | undefined,
...args: Parameters<T>
): void {
if (f) setTimeout(() => f(...args), 1);
}
export const opposite = (c: sg.Color): sg.Color => (c === 'sente' ? 'gote' : 'sente');
export const sentePov = (o: sg.Color): boolean => o === 'sente';
export const distanceSq = (pos1: sg.Pos, pos2: sg.Pos): number => {
const dx = pos1[0] - pos2[0],
dy = pos1[1] - pos2[1];
return dx * dx + dy * dy;
};
export const samePiece = (p1: sg.Piece, p2: sg.Piece): boolean =>
p1.role === p2.role && p1.color === p2.color;
const posToTranslateBase = (
pos: sg.Pos,
dims: sg.Dimensions,
asSente: boolean,
xFactor: number,
yFactor: number,
): sg.NumberPair => [
(asSente ? dims.files - 1 - pos[0] : pos[0]) * xFactor,
(asSente ? pos[1] : dims.ranks - 1 - pos[1]) * yFactor,
];
export const posToTranslateAbs = (
dims: sg.Dimensions,
bounds: DOMRect,
): ((pos: sg.Pos, asSente: boolean) => sg.NumberPair) => {
const xFactor = bounds.width / dims.files,
yFactor = bounds.height / dims.ranks;
return (pos, asSente) => posToTranslateBase(pos, dims, asSente, xFactor, yFactor);
};
export const posToTranslateRel =
(dims: sg.Dimensions): ((pos: sg.Pos, asSente: boolean) => sg.NumberPair) =>
(pos, asSente) =>
posToTranslateBase(pos, dims, asSente, 100, 100);
export const translateAbs = (el: HTMLElement, pos: sg.NumberPair, scale: number): void => {
el.style.transform = `translate(${pos[0]}px,${pos[1]}px) scale(${scale}`;
};
export const translateRel = (
el: HTMLElement,
percents: sg.NumberPair,
scaleFactor: number,
scale?: number,
): void => {
el.style.transform = `translate(${percents[0] * scaleFactor}%,${percents[1] * scaleFactor}%) scale(${
scale || scaleFactor
})`;
};
export const setDisplay = (el: HTMLElement, v: boolean): void => {
el.style.display = v ? '' : 'none';
};
export const eventPosition = (e: sg.MouchEvent): sg.NumberPair | undefined => {
if (e.clientX || e.clientX === 0) return [e.clientX, e.clientY!];
if (e.targetTouches?.[0]) return [e.targetTouches[0].clientX, e.targetTouches[0].clientY];
return; // touchend has no position!
};
export const isRightButton = (e: sg.MouchEvent): boolean => e.buttons === 2 || e.button === 2;
export const isMiddleButton = (e: sg.MouchEvent): boolean => e.buttons === 4 || e.button === 1;
export const createEl = (tagName: string, className?: string): HTMLElement => {
const el = document.createElement(tagName);
if (className) el.className = className;
return el;
};
export function pieceNameOf(piece: sg.Piece): sg.PieceName {
return `${piece.color} ${piece.role}`;
}
export function parsePieceName(pieceName: sg.PieceName): sg.Piece {
const splitted = pieceName.split(' ', 2);
return { color: splitted[0] as sg.Color, role: splitted[1] };
}
export function isPieceNode(el: HTMLElement): el is sg.PieceNode {
return el.tagName === 'PIECE';
}
export function isSquareNode(el: HTMLElement): el is sg.SquareNode {
return el.tagName === 'SQ';
}
export function computeSquareCenter(
key: sg.Key,
asSente: boolean,
dims: sg.Dimensions,
bounds: DOMRect,
): sg.NumberPair {
const pos = key2pos(key);
if (asSente) {
pos[0] = dims.files - 1 - pos[0];
pos[1] = dims.ranks - 1 - pos[1];
}
return [
bounds.left + (bounds.width * pos[0]) / dims.files + bounds.width / (dims.files * 2),
bounds.top +
(bounds.height * (dims.ranks - 1 - pos[1])) / dims.ranks +
bounds.height / (dims.ranks * 2),
];
}
export function domSquareIndexOfKey(key: sg.Key, asSente: boolean, dims: sg.Dimensions): number {
const pos = key2pos(key);
let index = dims.files - 1 - pos[0] + pos[1] * dims.files;
if (!asSente) index = dims.files * dims.ranks - 1 - index;
return index;
}
export function isInsideRect(rect: DOMRect, pos: sg.NumberPair): boolean {
return (
rect.left <= pos[0] &&
rect.top <= pos[1] &&
rect.left + rect.width > pos[0] &&
rect.top + rect.height > pos[1]
);
}
export function getKeyAtDomPos(
pos: sg.NumberPair,
asSente: boolean,
dims: sg.Dimensions,
bounds: DOMRect,
): sg.Key | undefined {
let file = Math.floor((dims.files * (pos[0] - bounds.left)) / bounds.width);
if (asSente) file = dims.files - 1 - file;
let rank = Math.floor((dims.ranks * (pos[1] - bounds.top)) / bounds.height);
if (!asSente) rank = dims.ranks - 1 - rank;
return file >= 0 && file < dims.files && rank >= 0 && rank < dims.ranks
? pos2key([file, rank])
: undefined;
}
export function getHandPieceAtDomPos(
pos: sg.NumberPair,
roles: sg.RoleString[],
bounds: Map<sg.PieceName, DOMRect>,
): sg.Piece | undefined {
for (const color of colors) {
for (const role of roles) {
const piece = { color, role },
pieceRect = bounds.get(pieceNameOf(piece));
if (pieceRect && isInsideRect(pieceRect, pos)) return piece;
}
}
return;
}
export function posOfOutsideEl(
left: number,
top: number,
asSente: boolean,
dims: sg.Dimensions,
boardBounds: DOMRect,
): sg.Pos | undefined {
const sqW = boardBounds.width / dims.files,
sqH = boardBounds.height / dims.ranks;
if (!sqW || !sqH) return;
let xOff = (left - boardBounds.left) / sqW;
if (asSente) xOff = dims.files - 1 - xOff;
let yOff = (top - boardBounds.top) / sqH;
if (!asSente) yOff = dims.ranks - 1 - yOff;
return [xOff, yOff];
}