Vizualise chords
This commit is contained in:
parent
9b089af081
commit
39f0ec8f53
7
keyboards/kanata/laptop/README.md
Normal file
7
keyboards/kanata/laptop/README.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# My laptop layout!
|
||||
|
||||

|
||||
|
||||
## Layout philosophy
|
||||
|
||||
This is pretty much a port of my [ferris sweep layout](../../qmk/ferris-sweep).
|
|
@ -1,5 +1,11 @@
|
|||
{
|
||||
"layout": "alpha_staggered_double_switch",
|
||||
"measurements": {
|
||||
"keySize": 60,
|
||||
"keyPadding": 2,
|
||||
"keyCornerRadius": 5,
|
||||
"keyStrokeWidth": 1.5
|
||||
},
|
||||
"keys": [
|
||||
["Q", "!", "1", "f1"],
|
||||
["A", "<", "6", "f6"],
|
||||
|
@ -34,5 +40,88 @@
|
|||
[":", "`", "🗑️", "🔆"],
|
||||
["O", ";", "", "🔅"],
|
||||
["'", "\"", ""]
|
||||
],
|
||||
"chords": [
|
||||
[
|
||||
{
|
||||
"input": ["Q", "W"],
|
||||
"output": "⎋",
|
||||
"fill": "#9ccaff"
|
||||
},
|
||||
{
|
||||
"input": ["A", "R"],
|
||||
"output": "⌥",
|
||||
"fill": "#39f785"
|
||||
},
|
||||
{
|
||||
"input": ["S", "T"],
|
||||
"output": "⭾",
|
||||
"fill": "#fdff80",
|
||||
"fontSizeModifier": 0.8
|
||||
},
|
||||
{
|
||||
"input": ["G", "M"],
|
||||
"output": "⌫",
|
||||
"fill": "#f9adff"
|
||||
},
|
||||
{
|
||||
"input": ["N", "E"],
|
||||
"output": "⊥",
|
||||
"fill": "#f58e8e"
|
||||
},
|
||||
{
|
||||
"input": ["I", "O"],
|
||||
"output": "⌥",
|
||||
"fill": "#39f785"
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"input": ["R", "S"],
|
||||
"output": "⇧",
|
||||
"fill": "#f9adff"
|
||||
},
|
||||
{
|
||||
"input": ["E", "I"],
|
||||
"output": "⇧",
|
||||
"fill": "#f9adff"
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"input": ["R", "T"],
|
||||
"output": "ctrl",
|
||||
"fill": "#fdff80"
|
||||
},
|
||||
{
|
||||
"input": ["N", "I"],
|
||||
"output": "ctrl",
|
||||
"fill": "#fdff80"
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"input": ["C", "P"],
|
||||
"output": "📋",
|
||||
"fill": "#fdff80"
|
||||
},
|
||||
{
|
||||
"input": ["K", "I"],
|
||||
"output": "❖",
|
||||
"fill": "#f58e8e"
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"input": ["F", "T"],
|
||||
"output": "↵",
|
||||
"fill": "#9ccaff"
|
||||
},
|
||||
{
|
||||
"input": ["N", "U"],
|
||||
"output": "💾",
|
||||
"fill": "#9ccaff"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 71 KiB |
|
@ -1,5 +1,11 @@
|
|||
{
|
||||
"layout": "split_3x5_2",
|
||||
"measurements": {
|
||||
"keySize": 60,
|
||||
"keyPadding": 2,
|
||||
"keyCornerRadius": 5,
|
||||
"keyStrokeWidth": 1.5
|
||||
},
|
||||
"keys": [
|
||||
["Q", "!", "1", "f1"],
|
||||
["A", "<", "6", "f6"],
|
||||
|
@ -35,5 +41,88 @@
|
|||
[":", "`", "🗑️", "🔆"],
|
||||
["O", ";", "", "🔅"],
|
||||
["'", "\"", ""]
|
||||
],
|
||||
"chords": [
|
||||
[
|
||||
{
|
||||
"input": ["Q", "W"],
|
||||
"output": "⎋",
|
||||
"fill": "#9ccaff"
|
||||
},
|
||||
{
|
||||
"input": ["A", "R"],
|
||||
"output": "⌥",
|
||||
"fill": "#39f785"
|
||||
},
|
||||
{
|
||||
"input": ["S", "T"],
|
||||
"output": "⭾",
|
||||
"fill": "#fdff80",
|
||||
"fontSizeModifier": 0.8
|
||||
},
|
||||
{
|
||||
"input": ["G", "M"],
|
||||
"output": "⌫",
|
||||
"fill": "#f9adff"
|
||||
},
|
||||
{
|
||||
"input": ["N", "E"],
|
||||
"output": "⊥",
|
||||
"fill": "#f58e8e"
|
||||
},
|
||||
{
|
||||
"input": ["I", "O"],
|
||||
"output": "⌥",
|
||||
"fill": "#39f785"
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"input": ["R", "S"],
|
||||
"output": "⇧",
|
||||
"fill": "#f9adff"
|
||||
},
|
||||
{
|
||||
"input": ["E", "I"],
|
||||
"output": "⇧",
|
||||
"fill": "#f9adff"
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"input": ["R", "T"],
|
||||
"output": "ctrl",
|
||||
"fill": "#fdff80"
|
||||
},
|
||||
{
|
||||
"input": ["N", "I"],
|
||||
"output": "ctrl",
|
||||
"fill": "#fdff80"
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"input": ["C", "P"],
|
||||
"output": "📋",
|
||||
"fill": "#fdff80"
|
||||
},
|
||||
{
|
||||
"input": ["K", "I"],
|
||||
"output": "❖",
|
||||
"fill": "#f58e8e"
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"input": ["F", "T"],
|
||||
"output": "↵",
|
||||
"fill": "#9ccaff"
|
||||
},
|
||||
{
|
||||
"input": ["N", "U"],
|
||||
"output": "💾",
|
||||
"fill": "#9ccaff"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 74 KiB |
|
@ -1,13 +1,15 @@
|
|||
# Layout-lens
|
||||
|
||||
> NOTE: this project cannot yet render combos, which will change soon
|
||||
|
||||
This is a quickly-thrown-together set of scripts for generating SVG previews of keyboard layouts. For example configurations check out any config in the `keyboards` directory of this repository. To run this on your config simply do
|
||||
|
||||
```sh
|
||||
nix run github:mateiadrielrafael/keyswirl#layout-lens my-config.json out.svg
|
||||
```
|
||||
|
||||
## Future improvements
|
||||
|
||||
This project does not render chords involving rotated keys properly. Moreover, chord definitions in general can be a little verboose.
|
||||
|
||||
## Technical details
|
||||
|
||||
The code isn't very well written (i.e.: no error handling, only contains the features I needed myself, etc). I'd rewrite this in a better language given the motivation, but the current version does the job just fine. If you want to contribute a layout preset, add it to [./src/layouts](./src/layouts) and then modify the enum in [./src/types.ts](./src/types.ts) to know about it's existence.
|
||||
|
|
|
@ -9,12 +9,13 @@ import {
|
|||
SpecialSymbols,
|
||||
LayoutMeasurements,
|
||||
LayoutColorscheme,
|
||||
ChordConfig,
|
||||
ElementLayout,
|
||||
} from "./types";
|
||||
import split_3x5_2 from "./layouts/split_3x5_2";
|
||||
import alpha_staggered_double_switch from "./layouts/alpha_staggered_double_switch";
|
||||
|
||||
const defaultMeasurements: LayoutMeasurements = {
|
||||
imagePadding: 20,
|
||||
keySize: 60,
|
||||
keyPadding: 2,
|
||||
keyCornerRadius: 5,
|
||||
|
@ -30,6 +31,20 @@ const defaultColorscheme: LayoutColorscheme = {
|
|||
blLayerColor: "purple",
|
||||
};
|
||||
|
||||
const defaultElementLayout: ElementLayout = {
|
||||
mainToChordsGap: 10,
|
||||
imagePadding: 20,
|
||||
groupsPerRow: 2,
|
||||
groupPadding: 20,
|
||||
};
|
||||
|
||||
function parseSymbol(s: string) {
|
||||
const special = SpecialSymbols[s];
|
||||
const isNumber = String(parseInt(s)) == s;
|
||||
if (isNumber || special === undefined) return s;
|
||||
return special as SpecialSymbols;
|
||||
}
|
||||
|
||||
export function parseConfig(input: string): Config {
|
||||
const parsed = JSON.parse(input);
|
||||
|
||||
|
@ -42,16 +57,20 @@ export function parseConfig(input: string): Config {
|
|||
}
|
||||
|
||||
return {
|
||||
keys: (parsed.keys as string[][]).map((k) =>
|
||||
k.map((s) => {
|
||||
const special = SpecialSymbols[s];
|
||||
const isNumber = String(parseInt(s)) == s;
|
||||
if (isNumber || special === undefined) return s;
|
||||
return special as SpecialSymbols;
|
||||
}),
|
||||
keys: (parsed.keys as string[][]).map((k) => k.map(parseSymbol)),
|
||||
chords: ((parsed.chords as ChordConfig[][]) || []).map((group) =>
|
||||
group.map((chord) => ({
|
||||
...chord,
|
||||
input: chord.input.map((k) => parseSymbol(k as string)),
|
||||
output: parseSymbol(chord.output as string),
|
||||
})),
|
||||
),
|
||||
colorscheme: { ...defaultColorscheme, ...parsed.colorscheme },
|
||||
measurements: { ...defaultMeasurements, ...parsed.measurements },
|
||||
elementLayout: {
|
||||
...defaultElementLayout,
|
||||
...parsed.elementLayout,
|
||||
},
|
||||
layout,
|
||||
};
|
||||
}
|
||||
|
@ -77,6 +96,8 @@ export function makeLayout(config: Config): Layout {
|
|||
keys: config.keys.map((k) => key(...(k as Arguments<typeof key>))),
|
||||
colorscheme: config.colorscheme,
|
||||
measurements: config.measurements,
|
||||
elementLayout: config.elementLayout,
|
||||
chords: config.chords,
|
||||
visual: predefined.visual,
|
||||
size: predefined.size,
|
||||
};
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
import { VisualKey, VisualLayout } from "./types";
|
||||
import type {
|
||||
KeySymbol,
|
||||
Layout,
|
||||
LayoutMeasurements,
|
||||
VisualKey,
|
||||
VisualLayout,
|
||||
} from "./types";
|
||||
import * as V from "./vec2";
|
||||
|
||||
export function visualKey(
|
||||
|
@ -70,3 +76,28 @@ export function scaleVisual(visual: VisualKey, amount: number): VisualKey {
|
|||
size: V.scale(visual.size, amount),
|
||||
};
|
||||
}
|
||||
|
||||
export function scaleMeasurements(
|
||||
measurements: LayoutMeasurements,
|
||||
amount: number,
|
||||
): LayoutMeasurements {
|
||||
return {
|
||||
imagePadding: measurements.imagePadding * amount,
|
||||
keySize: measurements.keySize * amount,
|
||||
keyPadding: measurements.keyPadding * amount,
|
||||
keyCornerRadius: measurements.keyCornerRadius * amount,
|
||||
keyStrokeWidth: measurements.keyStrokeWidth * amount,
|
||||
mainToChordsGap: measurements.mainToChordsGap * amount,
|
||||
};
|
||||
}
|
||||
|
||||
export function findKeyByLabel(
|
||||
layout: Layout,
|
||||
label: KeySymbol,
|
||||
): VisualKey | null {
|
||||
for (let i = 0; i < layout.keys.length; i++) {
|
||||
if (layout.keys[i].main === label) return layout.visual[i];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ const layout: PredefinedLayout = {
|
|||
L.visualKey([7.75, 3.25]),
|
||||
L.cols([5, 0], block, offsets),
|
||||
].flat(),
|
||||
size: [10.8, 4],
|
||||
size: [10.8, 4.25],
|
||||
};
|
||||
|
||||
export default layout;
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { tag, px } from "./svg";
|
||||
import { children, tag, px } from "./svg";
|
||||
import * as L from "./layout";
|
||||
import * as V from "./vec2";
|
||||
import {
|
||||
ChordConfig,
|
||||
KeyboardKey,
|
||||
KeySymbol,
|
||||
Layout,
|
||||
|
@ -17,57 +18,63 @@ function textContents(input: KeySymbol): string {
|
|||
return input;
|
||||
}
|
||||
|
||||
interface KeyRenderingFlags {
|
||||
stroke: boolean;
|
||||
text: boolean;
|
||||
}
|
||||
|
||||
function applyKeyPadding(
|
||||
key: VisualKey,
|
||||
measurements: LayoutMeasurements,
|
||||
): { position: V.Vec2; size: V.Vec2 } {
|
||||
return {
|
||||
position: V.add(key.position, measurements.keyPadding),
|
||||
size: V.add(key.size, -2 * measurements.keyPadding),
|
||||
};
|
||||
}
|
||||
|
||||
function keyCenter(key: VisualKey): V.Vec2 {
|
||||
const centerX = key.position[0] + key.size[0] / 2;
|
||||
const centerY = key.position[1] + key.size[1] / 2;
|
||||
|
||||
return [centerX, centerY];
|
||||
}
|
||||
|
||||
const textAttribs = {
|
||||
"text-anchor": "middle",
|
||||
"dominant-baseline": "central",
|
||||
"font-family": "Helvetica",
|
||||
};
|
||||
|
||||
function textColor(
|
||||
colorscheme: LayoutColorscheme,
|
||||
input: KeySymbol,
|
||||
_default: string,
|
||||
): string {
|
||||
if (input === SpecialSymbols.TL) return colorscheme.tlLayerColor;
|
||||
if (input === SpecialSymbols.TR) return colorscheme.trLayerColor;
|
||||
return _default;
|
||||
}
|
||||
|
||||
function renderKey(
|
||||
visual: VisualKey,
|
||||
key: KeyboardKey,
|
||||
colorscheme: LayoutColorscheme,
|
||||
measurements: LayoutMeasurements,
|
||||
flags: KeyRenderingFlags,
|
||||
) {
|
||||
const withPadding = {
|
||||
position: V.add(visual.position, measurements.keyPadding),
|
||||
size: V.add(visual.size, -2 * measurements.keyPadding),
|
||||
};
|
||||
const withPadding = applyKeyPadding(visual, measurements);
|
||||
const center = keyCenter(visual);
|
||||
|
||||
const centerX = visual.position[0] + visual.size[0] / 2;
|
||||
const centerY = visual.position[1] + visual.size[1] / 2;
|
||||
const textAttribs = {
|
||||
"text-anchor": "middle",
|
||||
"dominant-baseline": "central",
|
||||
"font-family": "Helvetica",
|
||||
};
|
||||
|
||||
const textColor = (input: KeySymbol, _default: string): string => {
|
||||
if (input === SpecialSymbols.TL) return colorscheme.tlLayerColor;
|
||||
if (input === SpecialSymbols.TR) return colorscheme.trLayerColor;
|
||||
return _default;
|
||||
};
|
||||
|
||||
return tag(
|
||||
"g",
|
||||
{
|
||||
transform:
|
||||
visual.angle && visual.angle !== 0
|
||||
? `rotate(${visual.angle}, ${centerX}, ${centerY})`
|
||||
: "",
|
||||
},
|
||||
[
|
||||
tag("rect", {
|
||||
width: px(withPadding.size[0]),
|
||||
height: px(withPadding.size[1]),
|
||||
x: withPadding.position[0],
|
||||
y: withPadding.position[1],
|
||||
rx: measurements.keyCornerRadius,
|
||||
fill: colorscheme.keyFill,
|
||||
stroke: colorscheme.keyStroke,
|
||||
"stroke-width": px(measurements.keyStrokeWidth),
|
||||
}),
|
||||
const textOverlays = flags.text
|
||||
? [
|
||||
tag(
|
||||
"text",
|
||||
{
|
||||
x: centerX,
|
||||
y: centerY,
|
||||
x: center[0],
|
||||
y: center[1],
|
||||
textLength: px(withPadding.size[1] / 2),
|
||||
fill: textColor(key.main, colorscheme.mainLayerColor),
|
||||
fill: textColor(colorscheme, key.main, colorscheme.mainLayerColor),
|
||||
...textAttribs,
|
||||
},
|
||||
textContents(key.main),
|
||||
|
@ -77,7 +84,7 @@ function renderKey(
|
|||
{
|
||||
x: withPadding.position[0] + withPadding.size[0] / 6,
|
||||
y: withPadding.position[1] + withPadding.size[1] / 6,
|
||||
fill: textColor(key.tlLayer, colorscheme.tlLayerColor),
|
||||
fill: textColor(colorscheme, key.tlLayer, colorscheme.tlLayerColor),
|
||||
"font-size": "66%",
|
||||
...textAttribs,
|
||||
"text-anchor": "start",
|
||||
|
@ -89,7 +96,7 @@ function renderKey(
|
|||
{
|
||||
x: withPadding.position[0] + (9 * withPadding.size[0]) / 10,
|
||||
y: withPadding.position[1] + withPadding.size[1] / 6,
|
||||
fill: textColor(key.trLayer, colorscheme.trLayerColor),
|
||||
fill: textColor(colorscheme, key.trLayer, colorscheme.trLayerColor),
|
||||
"font-size": "66%",
|
||||
...textAttribs,
|
||||
"text-anchor": "end",
|
||||
|
@ -101,41 +108,257 @@ function renderKey(
|
|||
{
|
||||
x: withPadding.position[0] + withPadding.size[0] / 10,
|
||||
y: withPadding.position[1] + (5 * withPadding.size[1]) / 6,
|
||||
fill: textColor(key.blLayer, colorscheme.blLayerColor),
|
||||
fill: textColor(colorscheme, key.blLayer, colorscheme.blLayerColor),
|
||||
"font-size": "66%",
|
||||
...textAttribs,
|
||||
"text-anchor": "start",
|
||||
},
|
||||
textContents(key.blLayer),
|
||||
),
|
||||
].join("\n"),
|
||||
]
|
||||
: [];
|
||||
|
||||
return tag(
|
||||
"g",
|
||||
{
|
||||
transform:
|
||||
visual.angle && visual.angle !== 0
|
||||
? `rotate(${visual.angle}, ${center[0]}, ${center[1]})`
|
||||
: "",
|
||||
},
|
||||
children(
|
||||
tag("rect", {
|
||||
width: px(withPadding.size[0]),
|
||||
height: px(withPadding.size[1]),
|
||||
x: withPadding.position[0],
|
||||
y: withPadding.position[1],
|
||||
rx: measurements.keyCornerRadius,
|
||||
fill: colorscheme.keyFill,
|
||||
stroke: colorscheme.keyStroke,
|
||||
"stroke-width": px(flags.stroke ? measurements.keyStrokeWidth : 0),
|
||||
}),
|
||||
...textOverlays,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function keyCorners(
|
||||
key: VisualKey,
|
||||
measurements: LayoutMeasurements,
|
||||
): V.Vec2[] {
|
||||
const withPadding = applyKeyPadding(key, measurements);
|
||||
return [
|
||||
withPadding.position,
|
||||
V.add(withPadding.position, [withPadding.size[0], 0]),
|
||||
V.add(withPadding.position, withPadding.size),
|
||||
V.add(withPadding.position, [0, withPadding.size[1]]),
|
||||
];
|
||||
}
|
||||
|
||||
function renderChordShape(
|
||||
first: VisualKey,
|
||||
second: VisualKey,
|
||||
chord: ChordConfig,
|
||||
measurements: LayoutMeasurements,
|
||||
colorscheme: LayoutColorscheme,
|
||||
) {
|
||||
if (first.position[0] > second.position[0])
|
||||
return renderChordShape(second, first, chord, measurements, colorscheme);
|
||||
|
||||
const multi = (...steps: string[]) => steps.join(" ");
|
||||
const moveTo = (to: V.Vec2) => `M ${to.join(" ")}`;
|
||||
const lineTo = (to: V.Vec2) => `L ${to.join(" ")}`;
|
||||
|
||||
const firstCorners = keyCorners(first, measurements);
|
||||
const secondCorners = keyCorners(second, measurements);
|
||||
const firstCenter = keyCenter(first);
|
||||
const secondCenter = keyCenter(second);
|
||||
const middle = V.scale(V.add(firstCenter, secondCenter), 0.5);
|
||||
|
||||
const halfPath = (b: V.Vec2, c: V.Vec2, d: V.Vec2) => {
|
||||
if ((b[1] - c[1]) * (b[0] - d[0]) > 0) return multi(lineTo(b), lineTo(d));
|
||||
else return multi(lineTo(c), lineTo(d));
|
||||
};
|
||||
|
||||
const dottedIndicator = (key: VisualKey) => {
|
||||
const withPadding = applyKeyPadding(key, measurements);
|
||||
const center = keyCenter(key);
|
||||
const radius = Math.min(...withPadding.size) / 7.5;
|
||||
return tag("circle", {
|
||||
cx: center[0],
|
||||
cy: center[1],
|
||||
r: radius,
|
||||
fill: "gray",
|
||||
stroke: "gray",
|
||||
"fill-opacity": 0.1,
|
||||
"stroke-opacity": 0.6,
|
||||
"stroke-width": px(measurements.keyStrokeWidth),
|
||||
"stroke-dasharray": (radius * 2 * Math.PI) / 12,
|
||||
});
|
||||
};
|
||||
|
||||
return tag(
|
||||
"g",
|
||||
{},
|
||||
children(
|
||||
tag("path", {
|
||||
fill: chord.fill,
|
||||
stroke: chord.stroke,
|
||||
"stroke-width": px(measurements.keyStrokeWidth),
|
||||
d: multi(
|
||||
moveTo(firstCorners[0]),
|
||||
halfPath(firstCorners[1], secondCorners[0], secondCorners[1]),
|
||||
lineTo(secondCorners[2]),
|
||||
halfPath(secondCorners[3], firstCorners[2], firstCorners[3]),
|
||||
"Z", // close path
|
||||
),
|
||||
}),
|
||||
dottedIndicator(first),
|
||||
dottedIndicator(second),
|
||||
tag(
|
||||
"text",
|
||||
{
|
||||
x: middle[0],
|
||||
y: middle[1],
|
||||
"font-size": `${(chord.fontSizeModifier || 1) * 70}%`,
|
||||
fill: textColor(
|
||||
colorscheme,
|
||||
chord.output,
|
||||
colorscheme.mainLayerColor,
|
||||
),
|
||||
...textAttribs,
|
||||
"dominant-baseline": "middle",
|
||||
},
|
||||
textContents(chord.output),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function renderLayout(layout: Layout) {
|
||||
return tag(
|
||||
"svg",
|
||||
{
|
||||
viewBox: [
|
||||
-layout.measurements.imagePadding,
|
||||
-layout.measurements.imagePadding,
|
||||
2 * layout.measurements.imagePadding +
|
||||
layout.size[0] * layout.measurements.keySize,
|
||||
2 * layout.measurements.imagePadding +
|
||||
layout.size[1] * layout.measurements.keySize,
|
||||
].join(" "),
|
||||
xmlns: "http://www.w3.org/2000/svg",
|
||||
"xmlns:xlink": "http://www.w3.org/1999/xlink",
|
||||
},
|
||||
layout.visual
|
||||
.map((key, index) =>
|
||||
const totalWidth = layout.size[0] * layout.measurements.keySize;
|
||||
const chordKeyScalingFactor =
|
||||
(totalWidth / layout.elementLayout.groupsPerRow -
|
||||
layout.elementLayout.groupPadding * 2) /
|
||||
totalWidth;
|
||||
|
||||
const chordRowCount = Math.ceil(
|
||||
layout.chords.length / layout.elementLayout.groupsPerRow,
|
||||
);
|
||||
|
||||
const totalHeight =
|
||||
layout.elementLayout.mainToChordsGap +
|
||||
layout.measurements.keySize *
|
||||
layout.size[1] *
|
||||
(1 + chordRowCount / layout.elementLayout.groupsPerRow);
|
||||
|
||||
const widthPerChord = totalWidth / layout.elementLayout.groupsPerRow;
|
||||
|
||||
// {{{ Render main keys
|
||||
const mainKeys = layout.visual.map((key, index) =>
|
||||
renderKey(
|
||||
L.scaleVisual(key, layout.measurements.keySize),
|
||||
layout.keys[index],
|
||||
layout.colorscheme,
|
||||
layout.measurements,
|
||||
{
|
||||
text: true,
|
||||
stroke: true,
|
||||
},
|
||||
),
|
||||
)
|
||||
.join("\n"),
|
||||
);
|
||||
// }}}
|
||||
// {{{ Render chord groups
|
||||
const chordMeasurements = L.scaleMeasurements(
|
||||
layout.measurements,
|
||||
chordKeyScalingFactor,
|
||||
);
|
||||
const chords = layout.chords.map((group, index) => {
|
||||
const normalKeys = layout.visual.map((key, index) => {
|
||||
const keyLabels = layout.keys[index];
|
||||
|
||||
if (group.findIndex((c) => c.input.includes(keyLabels.main)) !== -1)
|
||||
return "";
|
||||
|
||||
return renderKey(
|
||||
L.scaleVisual(key, chordMeasurements.keySize),
|
||||
keyLabels,
|
||||
{ ...layout.colorscheme, keyFill: "gray" },
|
||||
chordMeasurements,
|
||||
{
|
||||
text: false,
|
||||
stroke: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const chordShapes = group.map((chord) => {
|
||||
return renderChordShape(
|
||||
L.scaleVisual(
|
||||
L.findKeyByLabel(layout, chord.input[0])!,
|
||||
chordMeasurements.keySize,
|
||||
),
|
||||
L.scaleVisual(
|
||||
L.findKeyByLabel(layout, chord.input[1])!,
|
||||
chordMeasurements.keySize,
|
||||
),
|
||||
chord,
|
||||
chordMeasurements,
|
||||
layout.colorscheme,
|
||||
);
|
||||
});
|
||||
|
||||
return tag(
|
||||
"g",
|
||||
{
|
||||
transform: `translate(${
|
||||
(index % layout.elementLayout.groupsPerRow) * widthPerChord +
|
||||
layout.elementLayout.groupPadding +
|
||||
(index + layout.elementLayout.groupsPerRow > layout.chords.length
|
||||
? ((layout.elementLayout.groupsPerRow -
|
||||
(layout.chords.length % layout.elementLayout.groupsPerRow)) *
|
||||
widthPerChord) /
|
||||
2
|
||||
: 0)
|
||||
} ${
|
||||
(Math.floor(index / layout.elementLayout.groupsPerRow) *
|
||||
layout.measurements.keySize *
|
||||
layout.size[1]) /
|
||||
layout.elementLayout.groupsPerRow +
|
||||
layout.elementLayout.groupPadding
|
||||
})`,
|
||||
},
|
||||
children(...normalKeys, ...chordShapes),
|
||||
);
|
||||
});
|
||||
// }}}
|
||||
// {{{ Put everything together
|
||||
return tag(
|
||||
"svg",
|
||||
{
|
||||
viewBox: [
|
||||
-layout.elementLayout.imagePadding,
|
||||
-layout.elementLayout.imagePadding,
|
||||
2 * layout.elementLayout.imagePadding + totalWidth,
|
||||
|
||||
2 * layout.elementLayout.imagePadding + totalHeight,
|
||||
].join(" "),
|
||||
xmlns: "http://www.w3.org/2000/svg",
|
||||
"xmlns:xlink": "http://www.w3.org/1999/xlink",
|
||||
},
|
||||
children(
|
||||
...mainKeys,
|
||||
tag(
|
||||
"g",
|
||||
{
|
||||
transform: `translate(0 ${
|
||||
layout.size[1] * layout.measurements.keySize +
|
||||
layout.elementLayout.mainToChordsGap
|
||||
})`,
|
||||
},
|
||||
children(...chords),
|
||||
),
|
||||
),
|
||||
);
|
||||
// }}}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,10 @@ function indent(amount: number, text: string) {
|
|||
.join("\n");
|
||||
}
|
||||
|
||||
export function children(...many: string[]): string {
|
||||
return many.join("\\n");
|
||||
}
|
||||
|
||||
export function tag(
|
||||
name: string,
|
||||
attributes: Record<string, string | number | undefined>,
|
||||
|
|
|
@ -38,24 +38,42 @@ export interface LayoutColorscheme {
|
|||
blLayerColor: string;
|
||||
}
|
||||
|
||||
export enum PredefinedLayoutName {
|
||||
split_3x5_2,
|
||||
alpha_staggered_double_switch,
|
||||
}
|
||||
|
||||
export interface LayoutMeasurements {
|
||||
imagePadding: number;
|
||||
keySize: number;
|
||||
keyPadding: number;
|
||||
keyCornerRadius: number;
|
||||
keyStrokeWidth: number;
|
||||
}
|
||||
|
||||
export enum PredefinedLayoutName {
|
||||
split_3x5_2,
|
||||
alpha_staggered_double_switch,
|
||||
}
|
||||
|
||||
export type ChordName = string;
|
||||
|
||||
export interface ChordConfig {
|
||||
input: KeySymbol[];
|
||||
output: KeySymbol;
|
||||
fill: string;
|
||||
stroke?: string;
|
||||
fontSizeModifier?: number;
|
||||
}
|
||||
|
||||
export interface ElementLayout {
|
||||
groupsPerRow: number;
|
||||
groupPadding: number;
|
||||
imagePadding: number;
|
||||
mainToChordsGap: number;
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
keys: KeySymbol[][];
|
||||
chords: ChordConfig[][];
|
||||
layout: PredefinedLayoutName;
|
||||
colorscheme: LayoutColorscheme;
|
||||
measurements: LayoutMeasurements;
|
||||
elementLayout: ElementLayout;
|
||||
}
|
||||
|
||||
export type PredefinedLayout = {
|
||||
|
@ -64,7 +82,10 @@ export type PredefinedLayout = {
|
|||
};
|
||||
|
||||
export interface Layout extends PredefinedLayout {
|
||||
keys: KeyboardKey[];
|
||||
colorscheme: LayoutColorscheme;
|
||||
measurements: LayoutMeasurements;
|
||||
elementLayout: ElementLayout;
|
||||
|
||||
keys: KeyboardKey[];
|
||||
chords: ChordConfig[][];
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue