From 39f0ec8f53a75d1137117b4638e06f66617b1161 Mon Sep 17 00:00:00 2001 From: prescientmoon Date: Wed, 21 Feb 2024 08:32:04 +0100 Subject: [PATCH] Vizualise chords --- keyboards/kanata/laptop/README.md | 7 + keyboards/kanata/laptop/lens.json | 89 ++ keyboards/kanata/laptop/lens.svg | 1015 ++++++++++------ keyboards/qmk/ferris-sweep/lens.json | 89 ++ keyboards/qmk/ferris-sweep/lens.svg | 1045 +++++++++++------ layout-lens/README.md | 6 +- layout-lens/src/config.ts | 37 +- layout-lens/src/layout.ts | 33 +- .../layouts/alpha_staggered_double_switch.ts | 2 +- layout-lens/src/render.ts | 381 ++++-- layout-lens/src/svg.ts | 4 + layout-lens/src/types.ts | 35 +- 12 files changed, 1977 insertions(+), 766 deletions(-) create mode 100644 keyboards/kanata/laptop/README.md diff --git a/keyboards/kanata/laptop/README.md b/keyboards/kanata/laptop/README.md new file mode 100644 index 0000000..f4ec992 --- /dev/null +++ b/keyboards/kanata/laptop/README.md @@ -0,0 +1,7 @@ +# My laptop layout! + +![layout preview](./lens.svg) + +## Layout philosophy + +This is pretty much a port of my [ferris sweep layout](../../qmk/ferris-sweep). diff --git a/keyboards/kanata/laptop/lens.json b/keyboards/kanata/laptop/lens.json index 0b342d6..6bc0511 100644 --- a/keyboards/kanata/laptop/lens.json +++ b/keyboards/kanata/laptop/lens.json @@ -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" + } + ] ] } diff --git a/keyboards/kanata/laptop/lens.svg b/keyboards/kanata/laptop/lens.svg index 580ed36..32f5605 100644 --- a/keyboards/kanata/laptop/lens.svg +++ b/keyboards/kanata/laptop/lens.svg @@ -1,505 +1,862 @@ - + - - + \n Q - - + \n ! - - + \n 1 - - + \n f1 - - + \n - - + \n A - - + \n < - - + \n 6 - - + \n f6 - - + \n - - + \n X - - + \n > - - - - + \n + \n f11 - - + \n - - + \n W - - + \n @ - - + \n 2 - - + \n f2 - - + \n - - + \n R - - + \n { - - + \n 7 - - + \n f7 - - + \n - - + \n C - - + \n } - - - - + \n + \n f12 - - + \n - - + \n F - - + \n # - - + \n 3 - - + \n f3 - - + \n - - + \n S - - + \n [ - - + \n 8 - - + \n f8 - - + \n - - + \n D - - + \n ] + \n + \n - - - - - - + \n - - + \n P - - + \n $ - - + \n 4 - - + \n f4 - - + \n - - + \n T - - + \n ( - - + \n 9 - - + \n f9 - - + \n - - + \n V - - + \n ) + \n + \n - - - - - - + \n - - + \n B - - + \n % - - + \n 5 - - + \n f5 - - + \n - - + \n G - - + \n ? - - + \n 0 - - + \n f10 - - + \n - - + \n Z + \n + \n + \n - - - - - - - - + \n - - + \n ■ + \n + \n + \n - - - - - - - - + \n - - + \n âŖ + \n + \n + \n - - - - - - - - + \n - - + \n ■ + \n + \n + \n - - - - - - - - + \n - - + \n J - - + \n ^ - - + \n 🏠 - - + \n 😱 - - + \n - - + \n M - - + \n - - - + \n ◄ - - + \n ↩ī¸ - - + \n - - + \n K - - + \n — - - - - + \n + \n 🎮 - - + \n - - + \n L - - + \n & - - + \n âŦ - - + \n copy - - + \n - - + \n N - - + \n _ - - + \n â–ŧ - - + \n 📋 - - + \n - - + \n H - - + \n | - - - - + \n + \n ✂ī¸ - - + \n - - + \n U - - + \n * - - + \n âĢ - - + \n ⏊ - - + \n - - + \n E - - + \n / - - + \n ▲ - - + \n ⏯ī¸ - - + \n - - + \n , - - + \n \ - - - - + \n + \n âĒ - - + \n - - + \n Y - - + \n ~ - - + \n end - - + \n 🔊 - - + \n - - + \n I - - + \n = - - + \n â–ē - - + \n 🔉 - - + \n - - + \n . - - + \n + - - - - + \n + \n 🔇 - - + \n - - + \n : - - + \n ` - - + \n 🗑ī¸ - - + \n 🔆 - - + \n - - + \n O - - + \n ; - - - - + \n + \n 🔅 - - + \n - - + \n ' - - + \n " + \n + \n - - - - + \n + + \n\n + + + \n\n\n + + + \n + + + \n\n + + + \n + + + \n\n + + + \n + + + \n\n + + + \n + + + \n + + + \n + + + \n + + + \n\n + + + \n + + + \n\n + + + \n + + + \n\n + + + \n + + + \n\n + + + \n + + + \n\n + + + \n + + \n + \n + \n + ⎋ + + \n + + \n + \n + \n + âŒĨ + + \n + + \n + \n + \n + â­ž + + \n + + \n + \n + \n + âŒĢ + + \n + + \n + \n + \n + âŠĨ + + \n + + \n + \n + \n + âŒĨ + + + \n + + + + \n + + + \n + + + \n + + + \n\n + + + \n + + + \n\n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n\n + + + \n + + + \n\n + + + \n + + + \n + + + \n + + + \n + + \n + \n + \n + ⇧ + + \n + + \n + \n + \n + ⇧ + + + \n + + + + \n + + + \n + + + \n + + + \n\n + + + \n + + + \n + + + \n + + + \n + + + \n\n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n\n + + + \n + + + \n + + + \n + + + \n + + + \n\n + + + \n + + + \n + + + \n + + + \n + + \n + \n + \n + ctrl + + \n + + \n + \n + \n + ctrl + + + \n + + + + \n + + + \n + + + \n + + + \n + + + \n\n + + + \n + + + \n + + + \n\n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n\n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n\n + + + \n + + + \n + + + \n + + + \n + + \n + \n + \n + 📋 + + \n + + \n + \n + \n + ❖ + + + \n + + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n\n + + + \n + + + \n + + + \n\n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n\n + + + \n\n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + \n + \n + \n + â†ĩ + + \n + + \n + \n + \n + 💾 + + + \ No newline at end of file diff --git a/keyboards/qmk/ferris-sweep/lens.json b/keyboards/qmk/ferris-sweep/lens.json index 9231e9f..66e7b7a 100644 --- a/keyboards/qmk/ferris-sweep/lens.json +++ b/keyboards/qmk/ferris-sweep/lens.json @@ -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" + } + ] ] } diff --git a/keyboards/qmk/ferris-sweep/lens.svg b/keyboards/qmk/ferris-sweep/lens.svg index aff835b..1e4aa62 100644 --- a/keyboards/qmk/ferris-sweep/lens.svg +++ b/keyboards/qmk/ferris-sweep/lens.svg @@ -1,518 +1,885 @@ - + - - + \n Q - - + \n ! - - + \n 1 - - + \n f1 - - + \n - - + \n A - - + \n < - - + \n 6 - - + \n f6 - - + \n - - + \n X - - + \n > - - - - + \n + \n f11 - - + \n - - + \n W - - + \n @ - - + \n 2 - - + \n f2 - - + \n - - + \n R - - + \n { - - + \n 7 - - + \n f7 - - + \n - - + \n C - - + \n } - - - - + \n + \n f12 - - + \n - - + \n F - - + \n # - - + \n 3 - - + \n f3 - - + \n - - + \n S - - + \n [ - - + \n 8 - - + \n f8 - - + \n - - + \n D - - + \n ] + \n + \n - - - - - - + \n - - + \n P - - + \n $ - - + \n 4 - - + \n f4 - - + \n - - + \n T - - + \n ( - - + \n 9 - - + \n f9 - - + \n - - + \n V - - + \n ) + \n + \n - - - - - - + \n - - + \n B - - + \n % - - + \n 5 - - + \n f5 - - + \n - - + \n G - - + \n ? - - + \n 0 - - + \n f10 - - + \n - - + \n Z + \n + \n + \n - - - - - - - - + \n - - + \n ■ + \n + \n + \n - - - - - - - - + \n - - + \n âŖ + \n + \n + \n - - - - - - - - + \n - - + \n ⇧ + \n + \n + \n - - - - - - - - + \n - - + \n ■ + \n + \n + \n - - - - - - - - + \n - - + \n J - - + \n ^ - - + \n 🏠 - - + \n 😱 - - + \n - - + \n M - - + \n - - - + \n ◄ - - + \n ↩ī¸ - - + \n - - + \n K - - + \n — - - - - + \n + \n 🎮 - - + \n - - + \n L - - + \n & - - + \n âŦ - - + \n copy - - + \n - - + \n N - - + \n _ - - + \n â–ŧ - - + \n 📋 - - + \n - - + \n H - - + \n | - - - - + \n + \n ✂ī¸ - - + \n - - + \n U - - + \n * - - + \n âĢ - - + \n ⏊ - - + \n - - + \n E - - + \n / - - + \n ▲ - - + \n ⏯ī¸ - - + \n - - + \n , - - + \n \ - - - - + \n + \n âĒ - - + \n - - + \n Y - - + \n ~ - - + \n end - - + \n 🔊 - - + \n - - + \n I - - + \n = - - + \n â–ē - - + \n 🔉 - - + \n - - + \n . - - + \n + - - - - + \n + \n 🔇 - - + \n - - + \n : - - + \n ` - - + \n 🗑ī¸ - - + \n 🔆 - - + \n - - + \n O - - + \n ; - - - - + \n + \n 🔅 - - + \n - - + \n ' - - + \n " + \n + \n - - - - + \n + + \n\n + + + \n\n\n + + + \n + + + \n\n + + + \n + + + \n\n + + + \n + + + \n\n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n\n + + + \n + + + \n\n + + + \n + + + \n\n + + + \n + + + \n\n + + + \n + + + \n\n + + + \n + + \n + \n + \n + ⎋ + + \n + + \n + \n + \n + âŒĨ + + \n + + \n + \n + \n + â­ž + + \n + + \n + \n + \n + âŒĢ + + \n + + \n + \n + \n + âŠĨ + + \n + + \n + \n + \n + âŒĨ + + + \n + + + + \n + + + \n + + + \n + + + \n\n + + + \n + + + \n\n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n\n + + + \n + + + \n\n + + + \n + + + \n + + + \n + + + \n + + \n + \n + \n + ⇧ + + \n + + \n + \n + \n + ⇧ + + + \n + + + + \n + + + \n + + + \n + + + \n\n + + + \n + + + \n + + + \n + + + \n + + + \n\n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n\n + + + \n + + + \n + + + \n + + + \n + + + \n\n + + + \n + + + \n + + + \n + + + \n + + \n + \n + \n + ctrl + + \n + + \n + \n + \n + ctrl + + + \n + + + + \n + + + \n + + + \n + + + \n + + + \n\n + + + \n + + + \n + + + \n\n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n\n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n\n + + + \n + + + \n + + + \n + + + \n + + \n + \n + \n + 📋 + + \n + + \n + \n + \n + ❖ + + + \n + + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n\n + + + \n + + + \n + + + \n\n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n\n + + + \n\n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + + \n + + \n + \n + \n + â†ĩ + + \n + + \n + \n + \n + 💾 + + + \ No newline at end of file diff --git a/layout-lens/README.md b/layout-lens/README.md index 1633d30..b626e51 100644 --- a/layout-lens/README.md +++ b/layout-lens/README.md @@ -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. diff --git a/layout-lens/src/config.ts b/layout-lens/src/config.ts index 09b12eb..7b569a3 100644 --- a/layout-lens/src/config.ts +++ b/layout-lens/src/config.ts @@ -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))), colorscheme: config.colorscheme, measurements: config.measurements, + elementLayout: config.elementLayout, + chords: config.chords, visual: predefined.visual, size: predefined.size, }; diff --git a/layout-lens/src/layout.ts b/layout-lens/src/layout.ts index 75df055..ac34a46 100644 --- a/layout-lens/src/layout.ts +++ b/layout-lens/src/layout.ts @@ -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; +} diff --git a/layout-lens/src/layouts/alpha_staggered_double_switch.ts b/layout-lens/src/layouts/alpha_staggered_double_switch.ts index 37e8d8d..d9128e9 100644 --- a/layout-lens/src/layouts/alpha_staggered_double_switch.ts +++ b/layout-lens/src/layouts/alpha_staggered_double_switch.ts @@ -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; diff --git a/layout-lens/src/render.ts b/layout-lens/src/render.ts index 6c78761..e4e5745 100644 --- a/layout-lens/src/render.ts +++ b/layout-lens/src/render.ts @@ -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,40 +18,115 @@ 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; - }; + const textOverlays = flags.text + ? [ + tag( + "text", + { + x: center[0], + y: center[1], + textLength: px(withPadding.size[1] / 2), + fill: textColor(colorscheme, key.main, colorscheme.mainLayerColor), + ...textAttribs, + }, + textContents(key.main), + ), + tag( + "text", + { + x: withPadding.position[0] + withPadding.size[0] / 6, + y: withPadding.position[1] + withPadding.size[1] / 6, + fill: textColor(colorscheme, key.tlLayer, colorscheme.tlLayerColor), + "font-size": "66%", + ...textAttribs, + "text-anchor": "start", + }, + textContents(key.tlLayer), + ), + tag( + "text", + { + x: withPadding.position[0] + (9 * withPadding.size[0]) / 10, + y: withPadding.position[1] + withPadding.size[1] / 6, + fill: textColor(colorscheme, key.trLayer, colorscheme.trLayerColor), + "font-size": "66%", + ...textAttribs, + "text-anchor": "end", + }, + textContents(key.trLayer), + ), + tag( + "text", + { + x: withPadding.position[0] + withPadding.size[0] / 10, + y: withPadding.position[1] + (5 * withPadding.size[1]) / 6, + fill: textColor(colorscheme, key.blLayer, colorscheme.blLayerColor), + "font-size": "66%", + ...textAttribs, + "text-anchor": "start", + }, + textContents(key.blLayer), + ), + ] + : []; return tag( "g", { transform: visual.angle && visual.angle !== 0 - ? `rotate(${visual.angle}, ${centerX}, ${centerY})` + ? `rotate(${visual.angle}, ${center[0]}, ${center[1]})` : "", }, - [ + children( tag("rect", { width: px(withPadding.size[0]), height: px(withPadding.size[1]), @@ -59,83 +135,230 @@ function renderKey( rx: measurements.keyCornerRadius, fill: colorscheme.keyFill, stroke: colorscheme.keyStroke, - "stroke-width": px(measurements.keyStrokeWidth), + "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: centerX, - y: centerY, - textLength: px(withPadding.size[1] / 2), - fill: textColor(key.main, colorscheme.mainLayerColor), + x: middle[0], + y: middle[1], + "font-size": `${(chord.fontSizeModifier || 1) * 70}%`, + fill: textColor( + colorscheme, + chord.output, + colorscheme.mainLayerColor, + ), ...textAttribs, + "dominant-baseline": "middle", }, - textContents(key.main), + textContents(chord.output), ), - tag( - "text", - { - x: withPadding.position[0] + withPadding.size[0] / 6, - y: withPadding.position[1] + withPadding.size[1] / 6, - fill: textColor(key.tlLayer, colorscheme.tlLayerColor), - "font-size": "66%", - ...textAttribs, - "text-anchor": "start", - }, - textContents(key.tlLayer), - ), - tag( - "text", - { - x: withPadding.position[0] + (9 * withPadding.size[0]) / 10, - y: withPadding.position[1] + withPadding.size[1] / 6, - fill: textColor(key.trLayer, colorscheme.trLayerColor), - "font-size": "66%", - ...textAttribs, - "text-anchor": "end", - }, - textContents(key.trLayer), - ), - tag( - "text", - { - x: withPadding.position[0] + withPadding.size[0] / 10, - y: withPadding.position[1] + (5 * withPadding.size[1]) / 6, - fill: textColor(key.blLayer, colorscheme.blLayerColor), - "font-size": "66%", - ...textAttribs, - "text-anchor": "start", - }, - textContents(key.blLayer), - ), - ].join("\n"), + ), ); } export function renderLayout(layout: Layout) { + 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, + }, + ), + ); + // }}} + // {{{ 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.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, + -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", }, - layout.visual - .map((key, index) => - renderKey( - L.scaleVisual(key, layout.measurements.keySize), - layout.keys[index], - layout.colorscheme, - layout.measurements, - ), - ) - .join("\n"), + children( + ...mainKeys, + tag( + "g", + { + transform: `translate(0 ${ + layout.size[1] * layout.measurements.keySize + + layout.elementLayout.mainToChordsGap + })`, + }, + children(...chords), + ), + ), ); + // }}} } diff --git a/layout-lens/src/svg.ts b/layout-lens/src/svg.ts index 4a0eab5..8ede490 100644 --- a/layout-lens/src/svg.ts +++ b/layout-lens/src/svg.ts @@ -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, diff --git a/layout-lens/src/types.ts b/layout-lens/src/types.ts index 691123e..2d60aa4 100644 --- a/layout-lens/src/types.ts +++ b/layout-lens/src/types.ts @@ -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[][]; }