@sabaki/shudan
Version:
A highly customizable, low-level Preact Goban component.
307 lines (268 loc) • 8.48 kB
JavaScript
import { createElement as h, Component } from "preact";
import classnames from "classnames";
import {
random,
readjustShifts,
neighborhood,
vertexEquals,
vertexEvents,
diffSignMap,
range,
getHoshis,
} from "./helper.js";
import { CoordX, CoordY } from "./Coord.js";
import Grid from "./Grid.js";
import Vertex from "./Vertex.js";
import Line from "./Line.js";
export default class Goban extends Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidUpdate() {
if (
this.props.animateStonePlacement &&
!this.state.clearAnimatedVerticesHandler &&
this.state.animatedVertices.length > 0
) {
// Handle stone animation
for (let [x, y] of this.state.animatedVertices) {
this.state.shiftMap[y][x] = random(7) + 1;
readjustShifts(this.state.shiftMap, [x, y]);
}
this.setState({ shiftMap: this.state.shiftMap });
// Clear animation classes
this.setState({
clearAnimatedVerticesHandler: setTimeout(() => {
this.setState({
animatedVertices: [],
clearAnimatedVerticesHandler: null,
});
}, this.props.animationDuration ?? 200),
});
}
}
render() {
let { width, height, rangeX, rangeY, xs, ys, hoshis, shiftMap, randomMap } =
this.state;
let {
innerProps = {},
vertexSize = 24,
coordX,
coordY,
busy,
signMap,
paintMap,
heatMap,
markerMap,
ghostStoneMap,
fuzzyStonePlacement = false,
showCoordinates = false,
lines = [],
selectedVertices = [],
dimmedVertices = [],
} = this.props;
let animatedVertices = [].concat(
...this.state.animatedVertices.map(neighborhood)
);
return h(
"div",
{
...innerProps,
id: this.props.id,
className:
classnames("shudan-goban", "shudan-goban-image", {
"shudan-busy": busy,
"shudan-coordinates": showCoordinates,
}) +
" " +
(this.props.class ?? this.props.className ?? ""),
style: {
display: "inline-grid",
gridTemplateRows: showCoordinates ? "1em 1fr 1em" : "1fr",
gridTemplateColumns: showCoordinates ? "1em 1fr 1em" : "1fr",
fontSize: vertexSize,
lineHeight: "1em",
...(this.props.style ?? {}),
},
},
showCoordinates &&
h(CoordX, { xs, style: { gridRow: "1", gridColumn: "2" }, coordX }),
showCoordinates &&
h(CoordY, {
height,
ys,
style: { gridRow: "2", gridColumn: "1" },
coordY,
}),
h(
"div",
{
className: "shudan-content",
style: {
position: "relative",
width: `${xs.length}em`,
height: `${ys.length}em`,
gridRow: showCoordinates ? "2" : "1",
gridColumn: showCoordinates ? "2" : "1",
},
},
h(Grid, {
vertexSize,
width,
height,
xs,
ys,
hoshis,
}),
h(
"div",
{
className: "shudan-vertices",
style: {
display: "grid",
gridTemplateColumns: `repeat(${xs.length}, 1em)`,
gridTemplateRows: `repeat(${ys.length}, 1em)`,
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: 1,
},
},
ys.map((y) =>
xs.map((x) => {
let equalsVertex = (v) => vertexEquals(v, [x, y]);
let selected = selectedVertices.some(equalsVertex);
return h(
Vertex,
Object.assign(
{
key: [x, y].join("-"),
position: [x, y],
shift: fuzzyStonePlacement ? shiftMap?.[y]?.[x] : 0,
random: randomMap?.[y]?.[x],
sign: signMap?.[y]?.[x],
heat: heatMap?.[y]?.[x],
marker: markerMap?.[y]?.[x],
ghostStone: ghostStoneMap?.[y]?.[x],
dimmed: dimmedVertices.some(equalsVertex),
animate: animatedVertices.some(equalsVertex),
paint: paintMap?.[y]?.[x],
paintLeft: paintMap?.[y]?.[x - 1],
paintRight: paintMap?.[y]?.[x + 1],
paintTop: paintMap?.[y - 1]?.[x],
paintBottom: paintMap?.[y + 1]?.[x],
paintTopLeft: paintMap?.[y - 1]?.[x - 1],
paintTopRight: paintMap?.[y - 1]?.[x + 1],
paintBottomLeft: paintMap?.[y + 1]?.[x - 1],
paintBottomRight: paintMap?.[y + 1]?.[x + 1],
selected,
selectedLeft:
selected &&
selectedVertices.some((v) => vertexEquals(v, [x - 1, y])),
selectedRight:
selected &&
selectedVertices.some((v) => vertexEquals(v, [x + 1, y])),
selectedTop:
selected &&
selectedVertices.some((v) => vertexEquals(v, [x, y - 1])),
selectedBottom:
selected &&
selectedVertices.some((v) => vertexEquals(v, [x, y + 1])),
},
...vertexEvents.map((e) => ({
[`on${e}`]: this.props[`onVertex${e}`],
}))
)
);
})
)
),
h(
"svg",
{
className: "shudan-lines",
style: {
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
pointerEvents: "none",
zIndex: 2,
},
},
h(
"g",
{
transform: `translate(-${rangeX[0] * vertexSize} -${
rangeY[0] * vertexSize
})`,
},
lines.map(({ v1, v2, type }, i) =>
h(Line, { key: i, v1, v2, type, vertexSize })
)
)
)
),
showCoordinates &&
h(CoordY, {
height,
ys,
style: { gridRow: "2", gridColumn: "3" },
coordY,
}),
showCoordinates &&
h(CoordX, { xs, style: { gridRow: "3", gridColumn: "2" }, coordX })
);
}
}
Goban.getDerivedStateFromProps = function (props, state) {
let { signMap = [], rangeX = [0, Infinity], rangeY = [0, Infinity] } = props;
let width = signMap.length === 0 ? 0 : signMap[0].length;
let height = signMap.length;
if (state.width === width && state.height === height) {
let animatedVertices = state.animatedVertices;
if (
props.animateStonePlacement &&
props.fuzzyStonePlacement &&
state.clearAnimatedVerticesHandler == null
) {
animatedVertices = diffSignMap(state.signMap, signMap);
}
let result = {
signMap,
animatedVertices,
};
if (
!vertexEquals(state.rangeX, rangeX) ||
!vertexEquals(state.rangeY, rangeY)
) {
// Range changed
Object.assign(result, {
rangeX,
rangeY,
xs: range(width).slice(rangeX[0], rangeX[1] + 1),
ys: range(height).slice(rangeY[0], rangeY[1] + 1),
});
}
return result;
}
// Board size changed
return {
signMap,
width,
height,
rangeX,
rangeY,
animatedVertices: [],
clearAnimatedVerticesHandler: null,
xs: range(width).slice(rangeX[0], rangeX[1] + 1),
ys: range(height).slice(rangeY[0], rangeY[1] + 1),
hoshis: getHoshis(width, height),
shiftMap: readjustShifts(signMap.map((row) => row.map((_) => random(8)))),
randomMap: signMap.map((row) => row.map((_) => random(4))),
};
};