Skip to content

Commit b04ea36

Browse files
committed
Add lighten function and nudge prop to halo. Ready for live testing.
1 parent 89f55c1 commit b04ea36

File tree

6 files changed

+114
-50
lines changed

6 files changed

+114
-50
lines changed

src/common/colours.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { rgb as convert_rgb, hsl as convert_hsl, hex as convert_hex } from "color-convert";
2+
13
export const getRandomColor = ():string => {
24
const letters = '0123456789ABCDEF';
35
let color = '#';
@@ -14,17 +16,24 @@ export const afterOpacity = (fg: RGB,o: number,bg: RGB = [255,255,255]): RGB =>
1416
}
1517

1618
export const rgb2hex = (rgb: RGB): string => {
17-
return "#" + rgb.map(v => Math.round(v).toString(16)).join("");
19+
return "#" + convert_rgb.hex(rgb);
1820
}
1921

2022
export const hex2rgb = (hex: string): RGB => {
21-
let r: string; let g: string; let b: string;
22-
if (hex.length === 4) {
23-
[r, g, b] = [hex[1], hex[2], hex[3]];
24-
} else if (hex.length === 7) {
25-
[r, g, b] = [hex.substring(1,3), hex.substring(3,5), hex.substring(5)];
26-
} else {
27-
throw new Error(`Malformed hexadecimal colour provided: ${hex}`);
28-
}
29-
return [r,g,b].map(n => parseInt(n, 16)) as RGB;
23+
return convert_hex.rgb(hex);
24+
}
25+
26+
const unlogit = (x: number) => {
27+
return Math.log(x / (1 - x));
28+
}
29+
30+
const logit = (x: number) => {
31+
return 1 / (1 + Math.exp(-x));
32+
}
33+
34+
export const lighten = (rgb: [number, number, number], ds: number, dl: number): RGB => {
35+
const hsl = convert_rgb.hsl(rgb);
36+
const l = 100 * logit(unlogit(hsl[2] / 100) + dl);
37+
const s = 100 * logit(unlogit(hsl[1] / 100) + ds);
38+
return convert_hsl.rgb([hsl[0], s, l]);
3039
}

src/renderers/_base.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import tinycolor from "tinycolor2";
1515
import { Graph, SquareOrthGraph, SquareGraph, SquareFanoronaGraph } from "../graphs";
1616
import { IWheelArgs, wheel, wheelLabels, wheelPolys } from "../grids/wheel";
1717
import { convexHullPolys, unionPolys } from "../common/polys";
18-
import { hex2rgb, rgb2hex, afterOpacity } from "../common/colours";
18+
import { hex2rgb, rgb2hex, afterOpacity, lighten } from "../common/colours";
1919
// import { customAlphabet } from 'nanoid'
2020
// const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 10);
2121

@@ -6793,14 +6793,20 @@ export abstract class RendererBase {
67936793
const minx = Math.min(...union.map(pt => pt.x));
67946794
const maxx = Math.max(...union.map(pt => pt.x));
67956795
const hullWidth = maxx - minx;
6796-
const cx = minx + (hullWidth / 2);
6796+
let cx = minx + (hullWidth / 2);
67976797
const miny = Math.min(...union.map(pt => pt.y));
67986798
const maxy = Math.max(...union.map(pt => pt.y));
67996799
const hullHeight = maxy - miny;
6800-
const cy = miny + (hullHeight / 2);
6800+
let cy = miny + (hullHeight / 2);
68016801
const diameter = Math.max(hullHeight, hullWidth) + (this.cellsize / 2);
68026802
const r = diameter / 2;
68036803

6804+
// check for centre nudging
6805+
if ("nudge" in marker && marker.nudge !== undefined) {
6806+
cx += marker.nudge.dx;
6807+
cy += marker.nudge.dy;
6808+
}
6809+
68046810
let degStart = 0;
68056811
if ( ("circular-start" in this.json.board) && (this.json.board["circular-start"] !== undefined) ) {
68066812
degStart = this.json.board["circular-start"];
@@ -6835,11 +6841,6 @@ export abstract class RendererBase {
68356841
haloPoly = this.rootSvg.path(`M${xleft},${yleft} A ${r} ${r} 0 0 1 ${xright},${yright} L${cx},${cy} L${xleft},${yleft}`).addClass(`aprender-marker-${x2uid(cloned)}-segment${i+1}`).fill(fill).stroke("none");
68366842
}
68376843
haloPoly.back();
6838-
// const board = this.rootSvg.findOne("#board") as SVGG|null;
6839-
// if (board === null) {
6840-
// throw new Error(`Can't do a board fill if there's no board.`);
6841-
// }
6842-
// board.add(haloPoly, 0);
68436844
}
68446845
}
68456846
}
@@ -8751,13 +8752,19 @@ export abstract class RendererBase {
87518752
});
87528753
colour.from(x1,y1).to(x2,y2);
87538754
}
8754-
// flatten
8755+
// Now check for functions
87558756
else if ("func" in val) {
8757+
// flatten
87568758
if (val.func === "flatten") {
87578759
const fg = hex2rgb(this.resolveColour(val.fg) as string);
87588760
const bg = hex2rgb(this.resolveColour(val.bg) as string);
87598761
colour = rgb2hex(afterOpacity(fg, val.opacity , bg));
87608762
}
8763+
// lighten
8764+
else if (val.func === "lighten") {
8765+
const base = hex2rgb(this.resolveColour(val.colour) as string);
8766+
colour = rgb2hex(lighten(base, val.ds, val.dl));
8767+
}
87618768
}
87628769
} else if (typeof val === "number") {
87638770
colour = this.options.colours[val - 1];

src/schemas/schema.d.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export type Colourstrings = string;
1212
/**
1313
* Colours can also be derived using various functions.
1414
*/
15-
export type Colourfuncs = FunctionFlatten;
15+
export type Colourfuncs = FunctionFlatten | FunctionLighten;
1616
export type PositiveInteger = number;
1717
/**
1818
* Schema for the `matrix` part of a polyomino-related feature
@@ -260,12 +260,24 @@ export interface Glyph {
260260
dy?: number;
261261
};
262262
}
263+
/**
264+
* This function returns the fully opaque colour that is equivalent to overlying the foreground colour with the given opacity over the background colour.
265+
*/
263266
export interface FunctionFlatten {
264267
func: "flatten";
265-
fg: Colourstrings;
266-
bg: Colourstrings;
268+
fg: PositiveInteger | Colourstrings;
269+
bg: PositiveInteger | Colourstrings;
267270
opacity: number;
268271
}
272+
/**
273+
* Lightens or darkens a colour by the specified amount of saturation and luminance. Positive deltas lighten, negative darken.
274+
*/
275+
export interface FunctionLighten {
276+
func: "lighten";
277+
colour: PositiveInteger | Colourstrings;
278+
ds: number;
279+
dl: number;
280+
}
269281
/**
270282
* A gradient one can use for flood fills and the like.
271283
*/
@@ -636,6 +648,13 @@ export interface MarkerHalo {
636648
* By default, the halo aligns with the board sections (see `circular-start`). Sometimes you want to offset them. Provide the offset in absolute degrees, with positive numbers rotating clockwise.
637649
*/
638650
offset?: number;
651+
/**
652+
* By default, the halo is centred on the centre of the combined polygons, but with 'limping' boards, the centre needs to be nudged. This is how you do it.
653+
*/
654+
nudge?: {
655+
dx: number;
656+
dy: number;
657+
};
639658
/**
640659
* Fill is drawn before grid lines, segments are drawn after grid lines.
641660
*/

src/schemas/schema.json

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,24 @@
3030
}
3131
]
3232
},
33-
"FunctionFlatten": {
33+
"functionFlatten": {
34+
"description": "This function returns the fully opaque colour that is equivalent to overlying the foreground colour with the given opacity over the background colour.",
3435
"type": "object",
3536
"properties": {
3637
"func": {
3738
"enum": ["flatten"]
3839
},
3940
"fg": {
40-
"$ref": "#/$defs/colourstrings"
41+
"anyOf": [
42+
{"$ref": "#/$defs/positiveInteger"},
43+
{"$ref": "#/$defs/colourstrings"}
44+
]
4145
},
4246
"bg": {
43-
"$ref": "#/$defs/colourstrings"
47+
"anyOf": [
48+
{"$ref": "#/$defs/positiveInteger"},
49+
{"$ref": "#/$defs/colourstrings"}
50+
]
4451
},
4552
"opacity": {
4653
"type": "number",
@@ -52,10 +59,34 @@
5259
"required": ["func", "fg", "bg", "opacity"],
5360
"additionalProperties": false
5461
},
62+
"functionLighten": {
63+
"description": "Lightens or darkens a colour by the specified amount of saturation and luminance. Positive deltas lighten, negative darken.",
64+
"type": "object",
65+
"properties": {
66+
"func": {
67+
"enum": ["lighten"]
68+
},
69+
"colour": {
70+
"anyOf": [
71+
{"$ref": "#/$defs/positiveInteger"},
72+
{"$ref": "#/$defs/colourstrings"}
73+
]
74+
},
75+
"ds": {
76+
"type": "number"
77+
},
78+
"dl": {
79+
"type": "number"
80+
}
81+
},
82+
"required": ["func", "colour", "ds", "dl"],
83+
"additionalProperties": false
84+
},
5585
"colourfuncs": {
5686
"description": "Colours can also be derived using various functions.",
5787
"anyOf": [
58-
{"$ref": "#/$defs/FunctionFlatten"}
88+
{"$ref": "#/$defs/functionFlatten"},
89+
{"$ref": "#/$defs/functionLighten"}
5990
]
6091
},
6192
"rowCol": {
@@ -1262,6 +1293,20 @@
12621293
"type": "number",
12631294
"default": 0
12641295
},
1296+
"nudge": {
1297+
"description": "By default, the halo is centred on the centre of the combined polygons, but with 'limping' boards, the centre needs to be nudged. This is how you do it.",
1298+
"type": "object",
1299+
"properties": {
1300+
"dx": {
1301+
"type": "number"
1302+
},
1303+
"dy": {
1304+
"type": "number"
1305+
}
1306+
},
1307+
"required": ["dx", "dy"],
1308+
"additionalProperties": false
1309+
},
12651310
"fill": {
12661311
"description": "Fill is drawn before grid lines, segments are drawn after grid lines.",
12671312
"anyOf": [

src/sheets/core.ts

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { Container as SVGContainer, Symbol as SVGSymbol } from "@svgdotjs/svg.js";
22
import type { ISheet } from "./ISheet";
33
import { Orientation, defineHex } from "honeycomb-grid";
4-
import { rgb as convert_rgb, hsl as convert_hsl, hex as convert_hex } from "color-convert";
4+
import { rgb as convert_rgb, hex as convert_hex } from "color-convert";
5+
import { lighten } from "../common/colours";
56
import fnv from "fnv-plus";
67

78
const sheet: ISheet = {
@@ -455,28 +456,6 @@ sheet.glyphs.set("meeple", (canvas: SVGContainer) => {
455456
return group;
456457
});
457458

458-
const unlogit = (x: number) => {
459-
return Math.log(x / (1 - x));
460-
}
461-
462-
const logit = (x: number) => {
463-
return 1 / (1 + Math.exp(-x));
464-
}
465-
466-
const lighten = (rgb: [number, number, number], ds: number, dl: number) => {
467-
const hsl = convert_rgb.hsl(rgb);
468-
const l = 100 * logit(unlogit(hsl[2] / 100) + dl);
469-
const s = 100 * logit(unlogit(hsl[1] / 100) + ds);
470-
return convert_hsl.rgb([hsl[0], s, l]);
471-
}
472-
473-
// const darken = (rgb: [number, number, number], ds: number, dl: number) => {
474-
// const hsl = convert_rgb.hsl(rgb);
475-
// const l = 100 * logit(unlogit(hsl[2] / 100) - dl);
476-
// const s = 100 * logit(unlogit(hsl[1] / 100) - ds);
477-
// return convert_hsl.rgb([hsl[0], s, l]);
478-
// }
479-
480459
// orb with no highlights
481460
sheet.glyphs.set("orb", (canvas: SVGContainer, color?:string) => {
482461
if (color === undefined) { color = "#fff"; }

test/playground.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@
108108
"board-akron4": {
109109
"name": "Boards: Akron4",
110110
"description": "This board is for simulating sphere stacking in layers. The `pieces` property is still two dimensional. This render has three full layers of pieces.",
111-
"render": "{\"board\":{\"style\":\"squares-stacked\",\"width\":7,\"height\":7,\"markers\":[{\"type\":\"edge\",\"edge\":\"N\",\"colour\":1},{\"type\":\"edge\",\"edge\":\"S\",\"colour\":1},{\"type\":\"edge\",\"edge\":\"E\",\"colour\":2},{\"type\":\"edge\",\"edge\":\"W\",\"colour\":2}]},\"legend\":{\"A\":{\"name\":\"orb0\",\"colour\":1,\"scale\":1.17},\"B\":{\"name\":\"orb0\",\"colour\":2,\"scale\":1.17},\"C\":{\"name\":\"orb0\",\"colour\":3,\"scale\":1.17},\"D\":{\"name\":\"orb0\",\"colour\":4,\"scale\":1.17},\"E\":{\"name\":\"orb0\",\"colour\":5,\"scale\":1.17},\"F\":{\"name\":\"orb0\",\"colour\":6,\"scale\":1.17},\"G\":{\"name\":\"orb0\",\"colour\":7,\"scale\":1.17},\"H\":{\"name\":\"orb0\",\"colour\":8,\"scale\":1.17},\"Y\":{\"name\":\"circle\",\"colour\":1,\"scale\":1.15,\"opacity\":0.5},\"Z\":{\"name\":\"circle\",\"colour\":2,\"scale\":1.15,\"opacity\":0.5}},\"pieces\":\"_\\n-ABBBAA\\n-ABBABA\\n-BAAAB-\\n-ABBAB-\\n-AA----\\nCDEFGHB\\n------\\n---BA-\\n----B-\\n------\\n------\\n------\",\"annotations\":[{\"type\":\"enter\",\"targets\":[{\"row\":6,\"col\":0}]}]}"
111+
"render": "{\"board\":{\"style\":\"squares-stacked\",\"width\":7,\"height\":7,\"markers\":[{\"type\":\"edge\",\"edge\":\"N\",\"colour\":1},{\"type\":\"edge\",\"edge\":\"S\",\"colour\":1},{\"type\":\"edge\",\"edge\":\"E\",\"colour\":2},{\"type\":\"edge\",\"edge\":\"W\",\"colour\":2}]},\"legend\":{\"A\":{\"name\":\"orb\",\"colour\":1,\"scale\":1.17},\"B\":{\"name\":\"orb\",\"colour\":2,\"scale\":1.17},\"C\":{\"name\":\"orb\",\"colour\":3,\"scale\":1.17},\"D\":{\"name\":\"orb\",\"colour\":4,\"scale\":1.17},\"E\":{\"name\":\"orb\",\"colour\":5,\"scale\":1.17},\"F\":{\"name\":\"orb\",\"colour\":6,\"scale\":1.17},\"G\":{\"name\":\"orb\",\"colour\":7,\"scale\":1.17},\"H\":{\"name\":\"orb\",\"colour\":8,\"scale\":1.17},\"Y\":{\"name\":\"circle\",\"colour\":1,\"scale\":1.15,\"opacity\":0.5},\"Z\":{\"name\":\"circle\",\"colour\":2,\"scale\":1.15,\"opacity\":0.5}},\"pieces\":\"_\\n-ABBBAA\\n-ABBABA\\n-BAAAB-\\n-ABBAB-\\n-AA----\\nCDEFGHB\\n------\\n---BA-\\n----B-\\n------\\n------\\n------\",\"annotations\":[{\"type\":\"enter\",\"targets\":[{\"row\":6,\"col\":0}]}]}"
112112
},
113113
"board-squares-stacked": {
114114
"name": "Boards: Square Stacked",
@@ -449,6 +449,11 @@
449449
"name": "Niche: Isometric",
450450
"description": "This is still experimental! Not all features will work as expected. The only glyphs currently supported are cubes and cylinders. Glyphs are generated on the fly, with a special legend structure. 90-degree rotation works correctly.",
451451
"render": `{"renderer":"isometric","board":{"style":"squares","width":6,"height":6,"heightmap":[[100,75,50,50,75,100],[75,50,25,25,50,75],[50,25,0,0,25,50],[50,25,0,0,25,50],[75,50,25,25,50,75],[100,75,50,50,75,100]]},"legend":{"A":{"piece":"cylinder","height":20,"colour":1},"B":{"piece":"cylinder","height":20,"colour":2}},"pieces":"_\\n_\\n_\\n-,-,-,B,-,-\\n-,-,B,A,B,-\\n-,A,-,A,BA,B\\n-,-,-,-,ABA,-"}`
452+
},
453+
"niche-functions": {
454+
"name": "Niche: Functions",
455+
"description": "Colours can also be specified by function. This example shows the 'flatten' function in the colour of the cells (which will automatically change between light and dark modes) and the 'lighten' function for the pieces.",
456+
"render": `{"options":["hide-labels"],"board":{"style":"hex-of-hex","minWidth":5,"maxWidth":10,"alternatingSymmetry":true,"strokeColour":"_context_background","backFill":{"type":"board","colour":{"func":"flatten","fg":"_context_fill","bg":"_context_background","opacity":0.25}},"markers":[{"type":"halo","offset":-27,"nudge":{"dx":0,"dy":7},"width":3,"segments":[{"colour":"_context_fill","opacity":0.9},{"colour":"_context_fill","opacity":0.5},{"colour":"_context_fill","opacity":0.1}]}]},"legend":{"P1":{"name":"piece","colour":1},"P1X":[{"name":"piece","colour":1},{"name":"meeple","colour":"#fff","scale":0.75,"opacity":0.25}],"P2":{"name":"piece","colour":2},"P2X":[{"name":"piece","colour":2},{"name":"meeple","colour":"#fff","scale":0.75,"opacity":0.25}],"G":{"name":"cube","colour":3,"scale":0.75},"Y":{"name":"cube","colour":{"func":"lighten","colour":3,"ds":3,"dl":1},"scale":0.75},"P":{"name":"cube","colour":{"func":"lighten","colour":3,"ds":6,"dl":2},"scale":0.75},"O":{"name":"cube","colour":{"func":"lighten","colour":3,"ds":-1.5,"dl":-0.5},"scale":0.75},"B":{"name":"cube","colour":{"func":"lighten","colour":3,"ds":-2.5,"dl":-1.5},"scale":0.75}},"pieces":"B,O,G,Y,P\\n-,-,-,-,-,-\\n-,-,-,-,-,-,-\\n-,-,-,-,-,-,-,-\\n-,-,-,-,-,-,-,-,-\\n-,-,-,-,-,-,-,-,-,-\\n-,-,-,-,-,-,-,-,-\\n-,-,-,-,-,-,-,-\\n-,-,-,-,-,-,-\\n-,-,-,-,-,-"}`
452457
}
453458
}
454459

0 commit comments

Comments
 (0)