Skip to content

Commit

Permalink
Merge pull request #159 from online-go/svg
Browse files Browse the repository at this point in the history
Goban SVG renderer
  • Loading branch information
anoek authored May 21, 2024
2 parents 7966731 + 04a398a commit 483539d
Show file tree
Hide file tree
Showing 15 changed files with 5,203 additions and 175 deletions.
1 change: 1 addition & 0 deletions .vscode/cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"HDPI",
"Heatmaps",
"hilight",
"hoshi",
"hostinfo",
"hoverable",
"icontains",
Expand Down
225 changes: 225 additions & 0 deletions src/GoTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,31 @@ export interface GoThemeBackgroundReactStyles {
backgroundSize?: string;
}

export interface SVGStop {
offset: number;
color: string;
}

export interface SVGStoneParameters {
id: string;
fill?: string;
stroke?: string;
gradient?: {
stops: SVGStop[];
type?: "radial" | "linear"; // default radial
x1?: number;
x2?: number;
y1?: number;
y2?: number;
cx?: number;
cy?: number;
r?: number;
fx?: number;
fy?: number;
};
url?: string;
}

export class GoTheme {
public name: string;
public styles: { [style_name: string]: string } = {};
Expand Down Expand Up @@ -67,6 +92,61 @@ export class GoTheme {
return { white: "stone" };
}

/* Returns an array of black stone objects. The structure
* of the array elements is up to the implementor, as they are passed
* verbatim to the placeBlackStone method */
public preRenderBlackSVG(
defs: SVGDefsElement,
radius: number,
_seed: number,
_deferredRenderCallback: () => void,
): string[] {
const ret = [];
const key = `black-${radius}`;
ret.push(key);

defs.appendChild(
this.renderSVG(
{
id: key,
//fill: "hsl(8, 7%, 10%)",
//stroke: "hsl(8, 7%, 10%)",
fill: this.getBlackStoneColor(),
stroke: this.getBlackStoneColor(),
},
radius,
),
);
return ret;
}

/* Returns an array of white stone objects. The structure
* of the array elements is up to the implementor, as they are passed
* verbatim to the placeWhiteStone method */
public preRenderWhiteSVG(
defs: SVGDefsElement,
radius: number,
_seed: number,
_deferredRenderCallback: () => void,
): string[] {
const ret = [];
const key = `white-${radius}`;
ret.push(key);
defs.appendChild(
this.renderSVG(
{
id: key,
//fill: "hsl(8, 7%, 90%)",
//stroke: "hsl(8, 7%, 30%)",
fill: this.getWhiteStoneColor(),
stroke: this.getBlackStoneColor(),
},
radius,
),
);
return ret;
}

/* Places a pre rendered stone onto the canvas, centered at cx, cy */
public placeWhiteStone(
ctx: CanvasRenderingContext2D,
Expand Down Expand Up @@ -98,6 +178,74 @@ export class GoTheme {
ctx.fill();
}

public placeStoneShadowSVG(
shadow_cell: SVGGraphicsElement | undefined,
cx: number,
cy: number,
radius: number,
): SVGElement | undefined {
if (!shadow_cell) {
return;
}

const invisible_circle_to_cast_shadow = document.createElementNS(
"http://www.w3.org/2000/svg",
"circle",
);
invisible_circle_to_cast_shadow.setAttribute("class", "shadow");
invisible_circle_to_cast_shadow.setAttribute("cx", cx.toString());
invisible_circle_to_cast_shadow.setAttribute("cy", cy.toString());
invisible_circle_to_cast_shadow.setAttribute("r", Math.max(0.1, radius).toString());
//invisible_circle_to_cast_shadow.setAttribute("fill", "rgba(0,0,0,0.4)");
const sx = radius * 0.1;
const sy = radius * 0.1;
const softness = radius * 0.05;
invisible_circle_to_cast_shadow.setAttribute(
"style",
`filter: drop-shadow(${sx}px ${sy}px ${softness}px rgba(0,0,0,0.4)`,
);
shadow_cell.appendChild(invisible_circle_to_cast_shadow);
return invisible_circle_to_cast_shadow;
}

public placeWhiteStoneSVG(
cell: SVGGraphicsElement,
shadow_cell: SVGGraphicsElement | undefined,
stone: string,
cx: number,
cy: number,
radius: number,
): [SVGElement, SVGElement | undefined] {
const shadow = this.placeStoneShadowSVG(shadow_cell, cx, cy, radius);

const ref = document.createElementNS("http://www.w3.org/2000/svg", "use");
ref.setAttribute("href", `#${stone}`);
ref.setAttribute("x", `${cx - radius}`);
ref.setAttribute("y", `${cy - radius}`);
cell.appendChild(ref);

return [ref, shadow];
}

public placeBlackStoneSVG(
cell: SVGGraphicsElement,
shadow_cell: SVGGraphicsElement | undefined,
stone: string,
cx: number,
cy: number,
radius: number,
): [SVGElement, SVGElement | undefined] {
const shadow = this.placeStoneShadowSVG(shadow_cell, cx, cy, radius);

const ref = document.createElementNS("http://www.w3.org/2000/svg", "use");
ref.setAttribute("href", `#${stone}`);
ref.setAttribute("x", `${cx - radius}`);
ref.setAttribute("y", `${cy - radius}`);
cell.appendChild(ref);

return [ref, shadow];
}

/* Resolve which stone graphic we should use. By default we just pick a
* random one, if there are multiple images, otherwise whatever was
* returned by the pre-render method */
Expand Down Expand Up @@ -199,4 +347,81 @@ export class GoTheme {
public getLabelTextColor(): string {
return "#000000";
}

public renderSVG(params: SVGStoneParameters, radius: number): SVGGraphicsElement {
const cx = radius;
const cy = radius;

const stone = document.createElementNS("http://www.w3.org/2000/svg", "g");
stone.setAttribute("id", params.id);
stone.setAttribute("class", "stone");

if (params.fill || params.stroke || params.gradient) {
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
stone.appendChild(circle);
if (params.fill) {
circle.setAttribute("fill", params.fill);
}
if (params.stroke) {
circle.setAttribute("stroke", params.stroke);
circle.setAttribute("stroke-width", `${radius / 20}`);
}
circle.setAttribute("cx", cx.toString());
circle.setAttribute("cy", cy.toString());
circle.setAttribute("r", radius.toString());

// gradient
if (params.gradient) {
const grad = params.gradient;
const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");

let gradient;

if (grad.type === "linear") {
gradient = document.createElementNS(
"http://www.w3.org/2000/svg",
"linearGradient",
);
gradient.setAttribute("x1", (grad.x1 ?? 0.0).toFixed(2));
gradient.setAttribute("y1", (grad.y1 ?? 0.0).toFixed(2));
gradient.setAttribute("x2", (grad.x2 ?? 1.0).toFixed(2));
gradient.setAttribute("y2", (grad.y2 ?? 1.0).toFixed(2));
} else {
gradient = document.createElementNS(
"http://www.w3.org/2000/svg",
params.gradient.type === "linear" ? "linearGradient" : "radialGradient",
);
gradient.setAttribute("cx", (grad.cx ?? 0.5).toFixed(2));
gradient.setAttribute("cy", (grad.cy ?? 0.5).toFixed(2));
gradient.setAttribute("r", (grad.r ?? 0.5).toFixed(2));
gradient.setAttribute("fx", (grad.fx ?? 0.3).toFixed(2));
gradient.setAttribute("fy", (grad.fy ?? 0.2).toFixed(2));
}
gradient.setAttribute("id", params.id + "-gradient");

for (const stop of params.gradient.stops) {
const s = document.createElementNS("http://www.w3.org/2000/svg", "stop");
s.setAttribute("offset", `${stop.offset}%`);
s.setAttribute("stop-color", stop.color);
gradient.appendChild(s);
}
defs.appendChild(gradient);
stone.appendChild(defs);
circle.setAttribute("fill", `url(#${params.id}-gradient)`);
}
}

if (params.url) {
const stone_image = document.createElementNS("http://www.w3.org/2000/svg", "image");
stone_image.setAttribute("class", "stone");
stone_image.setAttribute("x", `${cx - radius}`);
stone_image.setAttribute("y", `${cy - radius}`);
stone_image.setAttribute("width", `${radius * 2}`);
stone_image.setAttribute("height", `${radius * 2}`);
stone_image.setAttributeNS("http://www.w3.org/1999/xlink", "href", params.url);
stone.appendChild(stone_image);
}

return stone;
}
}
10 changes: 10 additions & 0 deletions src/Goban.styl
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,16 @@
box-shadow: 0px 2px 10px 0px rgba(0, 0, 0, 0.16);
}
}

/* SVG */
svg {
text {
font-family: Verdana, Arial, sans-serif;
text-anchor: middle;
font-weight: bold;
user-select: none;
}
}
}


57 changes: 14 additions & 43 deletions src/GobanCanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ interface ViewPortInterface {

const HOT_PINK = "#ff69b4";

interface GobanCanvasInterface {
export interface GobanCanvasInterface {
engine: GoEngine;
move_tree_container?: HTMLElement;

Expand Down Expand Up @@ -149,6 +149,7 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface {
constructor(config: GobanCanvasConfig, preloaded_data?: AdHocFormat | JGOF) {
/* TODO: Need to reconcile the clock fields before we can get rid of this `any` cast */
super(config, preloaded_data as any);
console.info("GobanCanvas created");

// console.log("Goban canvas v 0.5.74.debug 5"); // GaJ: I use this to be sure I have linked & loaded the updates
if (config.board_div) {
Expand Down Expand Up @@ -1705,48 +1706,6 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface {
}
}

/* Draw delete X's */
{
let draw_x = false;
let transparent_x = false;
if (
this.engine &&
(this.scoring_mode || this.engine.phase === "stone removal") &&
this.stone_placement_enabled &&
this.last_hover_square &&
this.last_hover_square.x === i &&
this.last_hover_square.y === j &&
(this.mode !== "analyze" || this.analyze_tool === "stone")
) {
draw_x = true;
transparent_x = true;
}

if (pos.mark_x) {
draw_x = true;
transparent_x = false;
}

draw_x = false;

if (draw_x) {
ctx.beginPath();
ctx.save();
ctx.strokeStyle = "#ff0000";
ctx.lineWidth = this.square_size * 0.175;
if (transparent_x) {
ctx.globalAlpha = 0.6;
}
const r = Math.max(1, this.metrics.mid * 0.7);
ctx.moveTo(cx - r, cy - r);
ctx.lineTo(cx + r, cy + r);
ctx.moveTo(cx + r, cy - r);
ctx.lineTo(cx - r, cy + r);
ctx.stroke();
ctx.restore();
}
}

/* Draw Scores */
{
if (
Expand Down Expand Up @@ -3307,6 +3266,18 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface {
});
}

protected computeThemeStoneRadius(): number {
// Scale proportionally in general
let r = this.square_size * 0.488;

// Prevent pixel sharing in low-res
if (this.square_size % 2 === 0) {
r = Math.min(r, (this.square_size - 1) / 2);
}

return Math.max(1, r);
}

move_tree_drawStone(
ctx: CanvasRenderingContext2D,
node: MoveTree,
Expand Down
12 changes: 0 additions & 12 deletions src/GobanCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1993,18 +1993,6 @@ export abstract class GobanCore extends EventEmitter<Events> {
}
}

protected computeThemeStoneRadius(): number {
// Scale proportionally in general
let r = this.square_size * 0.488;

// Prevent pixel sharing in low-res
if (this.square_size % 2 === 0) {
r = Math.min(r, (this.square_size - 1) / 2);
}

return Math.max(1, r);
}

protected updateMoveTree(): void {
this.move_tree_redraw();
}
Expand Down
Loading

0 comments on commit 483539d

Please sign in to comment.