Clean up the layout renderer!
This commit is contained in:
parent
377fe0dd3d
commit
996908b729
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,5 +1,4 @@
|
||||||
.envrc
|
.envrc
|
||||||
.direnv
|
.direnv
|
||||||
node_modules
|
|
||||||
result
|
result
|
||||||
out.*
|
out.*
|
||||||
|
|
|
@ -7,12 +7,13 @@
|
||||||
(system:
|
(system:
|
||||||
let
|
let
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
swoop = pkgs.callPackage ./swoop.nix { };
|
layout-lens = pkgs.callPackage ./layout-lens/default.nix { };
|
||||||
in
|
in
|
||||||
rec {
|
rec {
|
||||||
packages.swoop = swoop;
|
packages.layout-lens = layout-lens;
|
||||||
defaultPackage = packages.swoop;
|
defaultPackage = packages.layout-lens;
|
||||||
devShell = pkgs.callPackage ./shell.nix { };
|
devShells.layout-lens = pkgs.callPackage ./layout-lens/shell.nix { };
|
||||||
|
devShells.qmk = pkgs.callPackage ./keyboards/qmk/shell.nix { };
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
49
keyboards/qmk/ferris-sweep/lens.json
Normal file
49
keyboards/qmk/ferris-sweep/lens.json
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
{
|
||||||
|
"colorscheme": {
|
||||||
|
"keyFill": "#ffffff",
|
||||||
|
"keyStroke": "#000000",
|
||||||
|
"mainLayerColor": "black",
|
||||||
|
"tlLayerColor": "blue",
|
||||||
|
"trLayerColor": "red",
|
||||||
|
"blLayerColor": "purple"
|
||||||
|
},
|
||||||
|
"keys": [
|
||||||
|
["Q", "!", "1", "f1"],
|
||||||
|
["A", "<", "6", "f6"],
|
||||||
|
["X", ">", "", "f11"],
|
||||||
|
["W", "@", "2", "f2"],
|
||||||
|
["R", "(", "7", "f7"],
|
||||||
|
["C", "]", "", "f12"],
|
||||||
|
["F", "#", "3", "f3"],
|
||||||
|
["S", "[", "8", "f8"],
|
||||||
|
["D", "]"],
|
||||||
|
["P", "$", "4", "f4"],
|
||||||
|
["T", "{", "9", "f9"],
|
||||||
|
["V", "}"],
|
||||||
|
["B", "%", "5", "f5"],
|
||||||
|
["G", "-", "0", "f10"],
|
||||||
|
["Z", "—"],
|
||||||
|
["TR", "", ""],
|
||||||
|
["␣", "", ""],
|
||||||
|
["⇧", "", ""],
|
||||||
|
["TL", "", ""],
|
||||||
|
["J", "^", "🏠"],
|
||||||
|
["M", "?", "◄", "😱"],
|
||||||
|
["K", "", "", "🎮"],
|
||||||
|
["L", "&", "⏬", "🔊"],
|
||||||
|
["N", "_", "▼", "🔉"],
|
||||||
|
["H", "|", "", "🔇"],
|
||||||
|
["U", "*", "⏫", "🔆"],
|
||||||
|
["E", "/", "▲", "🔅"],
|
||||||
|
[",", "\\", ""],
|
||||||
|
["Y", "~", "end", "⏪"],
|
||||||
|
["I", "=", "►", "⏯️"],
|
||||||
|
[".", "+", "", "⏩"],
|
||||||
|
[":", "`", "del", "copy"],
|
||||||
|
["O", ";", "", "paste"],
|
||||||
|
["'", "\"", "", "cut"]
|
||||||
|
],
|
||||||
|
"imagePadding": 20,
|
||||||
|
"keySize": 50,
|
||||||
|
"layout": "split_3x5_2"
|
||||||
|
}
|
5
keyboards/qmk/shell.nix
Normal file
5
keyboards/qmk/shell.nix
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{ pkgs ? import <nixpkgs> { } }:
|
||||||
|
with pkgs;
|
||||||
|
mkShell {
|
||||||
|
buildInputs = with pkgs; [ qmk ];
|
||||||
|
}
|
1
layout-lens/.gitignore
vendored
Normal file
1
layout-lens/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
node_modules
|
62
layout-lens/src/config.ts
Normal file
62
layout-lens/src/config.ts
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import {
|
||||||
|
Arguments,
|
||||||
|
Config,
|
||||||
|
KeyboardKey,
|
||||||
|
KeySymbol,
|
||||||
|
Layout,
|
||||||
|
PredefinedLayout,
|
||||||
|
PredefinedLayoutName,
|
||||||
|
SpecialSymbols,
|
||||||
|
} from "./types";
|
||||||
|
import split_3x5_2 from "./layouts/split_3x5_2";
|
||||||
|
|
||||||
|
export function parseConfig(input: string): Config {
|
||||||
|
const parsed = JSON.parse(input);
|
||||||
|
|
||||||
|
const layout = PredefinedLayoutName[
|
||||||
|
parsed.layout as string
|
||||||
|
] as PredefinedLayoutName;
|
||||||
|
|
||||||
|
if (layout === undefined) {
|
||||||
|
throw `Cannot find layout ${parsed.layout}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
keys: (parsed.keys as string[][]).map((k) =>
|
||||||
|
k.map((s) => {
|
||||||
|
const special = SpecialSymbols[s];
|
||||||
|
if (special === undefined) return s;
|
||||||
|
return special as SpecialSymbols;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
colorscheme: parsed.colorscheme,
|
||||||
|
imagePadding: parsed.imagePadding,
|
||||||
|
keySize: parsed.keySize,
|
||||||
|
layout,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function key(
|
||||||
|
main: KeySymbol,
|
||||||
|
tlLayer: KeySymbol = "",
|
||||||
|
trLayer: KeySymbol = "",
|
||||||
|
blLayer: KeySymbol = "",
|
||||||
|
): KeyboardKey {
|
||||||
|
return { main, tlLayer, trLayer, blLayer };
|
||||||
|
}
|
||||||
|
|
||||||
|
const layouts: Record<PredefinedLayoutName, PredefinedLayout> = {
|
||||||
|
[PredefinedLayoutName.split_3x5_2]: split_3x5_2,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function makeLayout(config: Config): Layout {
|
||||||
|
const predefined = layouts[config.layout](config.keySize);
|
||||||
|
return {
|
||||||
|
keys: config.keys.map((k) => key(...(k as Arguments<typeof key>))),
|
||||||
|
colorscheme: config.colorscheme,
|
||||||
|
imagePadding: config.imagePadding,
|
||||||
|
keySize: config.keySize,
|
||||||
|
visual: predefined.visual,
|
||||||
|
size: predefined.size,
|
||||||
|
};
|
||||||
|
}
|
12
layout-lens/src/index.ts
Normal file
12
layout-lens/src/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
import * as fs from "fs";
|
||||||
|
import { renderLayout } from "./render";
|
||||||
|
import { makeLayout, parseConfig } from "./config";
|
||||||
|
|
||||||
|
const inPath = process.argv[2];
|
||||||
|
const outPath = process.argv[3];
|
||||||
|
|
||||||
|
const input = fs.readFileSync(inPath, "utf8");
|
||||||
|
const layout = makeLayout(parseConfig(input));
|
||||||
|
|
||||||
|
fs.writeFileSync(outPath, renderLayout(layout));
|
56
layout-lens/src/layout.ts
Normal file
56
layout-lens/src/layout.ts
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import { VisualKey, VisualLayout } from "./types";
|
||||||
|
import * as V from "./vec2";
|
||||||
|
|
||||||
|
function visualKey(at: V.Vec2, keySize: number, angle: number = 0): VisualKey {
|
||||||
|
return { position: at, size: [keySize, keySize], angle };
|
||||||
|
}
|
||||||
|
|
||||||
|
function col(at: V.Vec2, keySize: number): VisualLayout {
|
||||||
|
return [
|
||||||
|
visualKey(at, keySize),
|
||||||
|
visualKey(V.add(at, [0, keySize]), keySize),
|
||||||
|
visualKey(V.add(at, [0, 2 * keySize]), keySize),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function radians(deg: number): number {
|
||||||
|
return (deg / 180) * Math.PI;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function thumbs(
|
||||||
|
at: V.Vec2,
|
||||||
|
reverse: boolean,
|
||||||
|
keySize: number,
|
||||||
|
thumbRotation = reverse ? -15 : 15,
|
||||||
|
): VisualLayout {
|
||||||
|
// Distance between thumb key centers
|
||||||
|
const factor = keySize;
|
||||||
|
|
||||||
|
const offset: V.Vec2 = [
|
||||||
|
Math.cos(radians(thumbRotation)) * factor,
|
||||||
|
Math.sin(radians(thumbRotation)) * factor,
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = [
|
||||||
|
visualKey(at, keySize, thumbRotation),
|
||||||
|
visualKey(
|
||||||
|
V.add(at, reverse ? V.neg(offset) : offset),
|
||||||
|
keySize,
|
||||||
|
thumbRotation,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (reverse) result.reverse();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cols(
|
||||||
|
at: V.Vec2,
|
||||||
|
cols: V.Vec2[],
|
||||||
|
keySize: number,
|
||||||
|
): VisualLayout {
|
||||||
|
return cols.flatMap((self, index) =>
|
||||||
|
col(V.add(at, V.add(self, [index * keySize, 0])), keySize),
|
||||||
|
);
|
||||||
|
}
|
26
layout-lens/src/layouts/split_3x5_2.ts
Normal file
26
layout-lens/src/layouts/split_3x5_2.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import * as L from "../layout";
|
||||||
|
import type { PredefinedLayout } from "../types";
|
||||||
|
import type { Vec2 } from "../vec2";
|
||||||
|
|
||||||
|
const layout: PredefinedLayout = (keySize) => {
|
||||||
|
// 3x5 block
|
||||||
|
const block: Vec2[] = [
|
||||||
|
[0, keySize],
|
||||||
|
[0, keySize / 2],
|
||||||
|
[0, 0],
|
||||||
|
[0, keySize / 2],
|
||||||
|
[0, keySize],
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
visual: [
|
||||||
|
L.cols([0, 0], block, keySize),
|
||||||
|
L.thumbs([keySize * 3.5, keySize * 4.5], false, keySize),
|
||||||
|
L.thumbs([keySize * 7.5, keySize * 4.5], true, keySize),
|
||||||
|
L.cols([7 * keySize, 0], block, keySize),
|
||||||
|
].flat(),
|
||||||
|
size: [keySize * 12, keySize * 6],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default layout;
|
124
layout-lens/src/render.ts
Normal file
124
layout-lens/src/render.ts
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
import { tag, px } from "./svg";
|
||||||
|
import {
|
||||||
|
KeyboardKey,
|
||||||
|
KeySymbol,
|
||||||
|
Layout,
|
||||||
|
LayoutColorscheme,
|
||||||
|
SpecialSymbols,
|
||||||
|
VisualKey,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
function textContents(input: KeySymbol): string {
|
||||||
|
if (input === SpecialSymbols.TL || input === SpecialSymbols.TR) return "■";
|
||||||
|
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderKey(
|
||||||
|
visual: VisualKey,
|
||||||
|
key: KeyboardKey,
|
||||||
|
colorscheme: LayoutColorscheme,
|
||||||
|
keySize: number,
|
||||||
|
) {
|
||||||
|
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": "middle",
|
||||||
|
"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})`
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
[
|
||||||
|
tag("rect", {
|
||||||
|
width: px(visual.size[0]),
|
||||||
|
height: px(visual.size[1]),
|
||||||
|
x: visual.position[0],
|
||||||
|
y: visual.position[1],
|
||||||
|
fill: colorscheme.keyFill,
|
||||||
|
stroke: colorscheme.keyStroke,
|
||||||
|
"stroke-width": px(2),
|
||||||
|
}),
|
||||||
|
tag(
|
||||||
|
"text",
|
||||||
|
{
|
||||||
|
x: centerX,
|
||||||
|
y: centerY,
|
||||||
|
textLength: px(keySize / 2),
|
||||||
|
fill: textColor(key.main, colorscheme.mainLayerColor),
|
||||||
|
...textAttribs,
|
||||||
|
},
|
||||||
|
textContents(key.main),
|
||||||
|
),
|
||||||
|
tag(
|
||||||
|
"text",
|
||||||
|
{
|
||||||
|
x: visual.position[0] + visual.size[0] / 6,
|
||||||
|
y: visual.position[1] + visual.size[1] / 6,
|
||||||
|
fill: textColor(key.tlLayer, colorscheme.tlLayerColor),
|
||||||
|
"font-size": "66%",
|
||||||
|
...textAttribs,
|
||||||
|
},
|
||||||
|
textContents(key.tlLayer),
|
||||||
|
),
|
||||||
|
tag(
|
||||||
|
"text",
|
||||||
|
{
|
||||||
|
x: visual.position[0] + (9 * visual.size[0]) / 10,
|
||||||
|
y: visual.position[1] + visual.size[1] / 6,
|
||||||
|
fill: textColor(key.trLayer, colorscheme.trLayerColor),
|
||||||
|
"font-size": "66%",
|
||||||
|
...textAttribs,
|
||||||
|
"text-anchor": "end",
|
||||||
|
},
|
||||||
|
textContents(key.trLayer),
|
||||||
|
),
|
||||||
|
tag(
|
||||||
|
"text",
|
||||||
|
{
|
||||||
|
x: visual.position[0] + visual.size[0] / 10,
|
||||||
|
y: visual.position[1] + (5 * visual.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) {
|
||||||
|
return tag(
|
||||||
|
"svg",
|
||||||
|
{
|
||||||
|
viewBox: [
|
||||||
|
-layout.imagePadding,
|
||||||
|
-layout.imagePadding,
|
||||||
|
2 * layout.imagePadding + layout.size[0],
|
||||||
|
2 * layout.imagePadding + layout.size[1],
|
||||||
|
].join(" "),
|
||||||
|
xmlns: "http://www.w3.org/2000/svg",
|
||||||
|
"xmlns:xlink": "http://www.w3.org/1999/xlink",
|
||||||
|
},
|
||||||
|
layout.visual
|
||||||
|
.map((key, index) =>
|
||||||
|
renderKey(key, layout.keys[index], layout.colorscheme, layout.keySize),
|
||||||
|
)
|
||||||
|
.join("\n"),
|
||||||
|
);
|
||||||
|
}
|
30
layout-lens/src/svg.ts
Normal file
30
layout-lens/src/svg.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
function indent(amount: number, text: string) {
|
||||||
|
return text
|
||||||
|
.split("\n")
|
||||||
|
.map((l) => " ".repeat(amount) + l)
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tag(
|
||||||
|
name: string,
|
||||||
|
attributes: Record<string, string | number | undefined>,
|
||||||
|
children: string = "",
|
||||||
|
) {
|
||||||
|
const attributeString = Object.entries(attributes)
|
||||||
|
.map(([k, v]) => `${k}="${v}"`)
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
const result = [
|
||||||
|
`<${name}${attributeString === "" ? "" : ` ${attributeString}`}>`,
|
||||||
|
indent(2, children),
|
||||||
|
`</${name}>`,
|
||||||
|
]
|
||||||
|
.filter((l) => l.trim() !== "")
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function px(value: number) {
|
||||||
|
return `${value}px`;
|
||||||
|
}
|
65
layout-lens/src/types.ts
Normal file
65
layout-lens/src/types.ts
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import type { Vec2 } from "./vec2";
|
||||||
|
|
||||||
|
/** Returns the arguments of a given function type */
|
||||||
|
export type Arguments<T extends (...args: any) => any> = T extends (
|
||||||
|
...args: infer U
|
||||||
|
) => any
|
||||||
|
? U
|
||||||
|
: never;
|
||||||
|
|
||||||
|
export interface VisualKey {
|
||||||
|
position: Vec2;
|
||||||
|
size: Vec2;
|
||||||
|
angle?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type VisualLayout = VisualKey[];
|
||||||
|
|
||||||
|
export enum SpecialSymbols {
|
||||||
|
TL,
|
||||||
|
TR,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type KeySymbol = SpecialSymbols | string;
|
||||||
|
|
||||||
|
export interface KeyboardKey {
|
||||||
|
main: KeySymbol;
|
||||||
|
tlLayer: KeySymbol;
|
||||||
|
trLayer: KeySymbol;
|
||||||
|
blLayer: KeySymbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LayoutColorscheme {
|
||||||
|
keyFill: string;
|
||||||
|
keyStroke: string;
|
||||||
|
mainLayerColor: string;
|
||||||
|
tlLayerColor: string;
|
||||||
|
trLayerColor: string;
|
||||||
|
blLayerColor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Layout {
|
||||||
|
visual: VisualLayout;
|
||||||
|
keys: KeyboardKey[];
|
||||||
|
colorscheme: LayoutColorscheme;
|
||||||
|
imagePadding: number;
|
||||||
|
size: Vec2;
|
||||||
|
keySize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PredefinedLayoutName {
|
||||||
|
split_3x5_2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Config {
|
||||||
|
keys: KeySymbol[][];
|
||||||
|
colorscheme: LayoutColorscheme;
|
||||||
|
imagePadding: number;
|
||||||
|
keySize: number;
|
||||||
|
layout: PredefinedLayoutName;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PredefinedLayout = (keySize: number) => {
|
||||||
|
visual: VisualLayout;
|
||||||
|
size: Vec2;
|
||||||
|
};
|
9
layout-lens/src/vec2.ts
Normal file
9
layout-lens/src/vec2.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
export type Vec2 = [number, number];
|
||||||
|
|
||||||
|
export function add(x: Vec2, y: Vec2): Vec2 {
|
||||||
|
return [x[0] + y[0], x[1] + y[1]];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function neg(v: Vec2): Vec2 {
|
||||||
|
return [-v[0], -v[1]];
|
||||||
|
}
|
309
src/index.ts
309
src/index.ts
|
@ -1,309 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
import * as fs from "fs";
|
|
||||||
|
|
||||||
type Vec2 = [number, number];
|
|
||||||
|
|
||||||
interface VisualKey {
|
|
||||||
position: Vec2;
|
|
||||||
size: Vec2;
|
|
||||||
angle?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
type VisualLayout = VisualKey[];
|
|
||||||
|
|
||||||
interface KeyboardKey {
|
|
||||||
main: string;
|
|
||||||
tlLayer: string;
|
|
||||||
trLayer: string;
|
|
||||||
blLayer: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LayoutColorscheme {
|
|
||||||
keyFill: string;
|
|
||||||
keyStroke: string;
|
|
||||||
mainLayerColor: string;
|
|
||||||
tlLayerColor: string;
|
|
||||||
trLayerColor: string;
|
|
||||||
blLayerColor: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Layout {
|
|
||||||
visual: VisualLayout;
|
|
||||||
keys: KeyboardKey[];
|
|
||||||
colorscheme: LayoutColorscheme;
|
|
||||||
padding: number;
|
|
||||||
size: Vec2;
|
|
||||||
}
|
|
||||||
|
|
||||||
function indent(amount: number, text: string) {
|
|
||||||
return text
|
|
||||||
.split("\n")
|
|
||||||
.map((l) => " ".repeat(amount) + l)
|
|
||||||
.join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
function tag(
|
|
||||||
name: string,
|
|
||||||
attributes: Record<string, string | number | undefined>,
|
|
||||||
children: string = "",
|
|
||||||
) {
|
|
||||||
const attributeString = Object.entries(attributes)
|
|
||||||
.map(([k, v]) => `${k}="${v}"`)
|
|
||||||
.join(" ");
|
|
||||||
|
|
||||||
const result = [
|
|
||||||
`<${name}${attributeString === "" ? "" : ` ${attributeString}`}>`,
|
|
||||||
indent(2, children),
|
|
||||||
`</${name}>`,
|
|
||||||
]
|
|
||||||
.filter((l) => l.trim() !== "")
|
|
||||||
.join("\n");
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function px(value: number) {
|
|
||||||
return `${value}px`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function textContents(input: string): string {
|
|
||||||
if (input === "TR" || input === "TL") return "■";
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderKey(
|
|
||||||
visual: VisualKey,
|
|
||||||
key: KeyboardKey,
|
|
||||||
colorscheme: LayoutColorscheme,
|
|
||||||
) {
|
|
||||||
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": "middle",
|
|
||||||
"font-family": "Helvetica",
|
|
||||||
};
|
|
||||||
|
|
||||||
const textColor = (input: string, _default: string): string => {
|
|
||||||
if (input === "TL") return colorscheme.tlLayerColor;
|
|
||||||
if (input === "TR") return colorscheme.trLayerColor;
|
|
||||||
return _default;
|
|
||||||
};
|
|
||||||
|
|
||||||
return tag(
|
|
||||||
"g",
|
|
||||||
{
|
|
||||||
transform:
|
|
||||||
visual.angle && visual.angle !== 0
|
|
||||||
? `rotate(${visual.angle}, ${centerX}, ${centerY})`
|
|
||||||
: undefined,
|
|
||||||
},
|
|
||||||
[
|
|
||||||
tag("rect", {
|
|
||||||
width: px(visual.size[0]),
|
|
||||||
height: px(visual.size[1]),
|
|
||||||
x: visual.position[0],
|
|
||||||
y: visual.position[1],
|
|
||||||
fill: colorscheme.keyFill,
|
|
||||||
stroke: colorscheme.keyStroke,
|
|
||||||
"stroke-width": px(2),
|
|
||||||
}),
|
|
||||||
tag(
|
|
||||||
"text",
|
|
||||||
{
|
|
||||||
x: centerX,
|
|
||||||
y: centerY,
|
|
||||||
textLength: px(keySize / 2),
|
|
||||||
fill: textColor(key.main, colorscheme.mainLayerColor),
|
|
||||||
...textAttribs,
|
|
||||||
},
|
|
||||||
textContents(key.main),
|
|
||||||
),
|
|
||||||
tag(
|
|
||||||
"text",
|
|
||||||
{
|
|
||||||
x: visual.position[0] + visual.size[0] / 6,
|
|
||||||
y: visual.position[1] + visual.size[1] / 6,
|
|
||||||
fill: textColor(key.tlLayer, colorscheme.tlLayerColor),
|
|
||||||
"font-size": "66%",
|
|
||||||
...textAttribs,
|
|
||||||
},
|
|
||||||
textContents(key.tlLayer),
|
|
||||||
),
|
|
||||||
tag(
|
|
||||||
"text",
|
|
||||||
{
|
|
||||||
x: visual.position[0] + (9 * visual.size[0]) / 10,
|
|
||||||
y: visual.position[1] + visual.size[1] / 6,
|
|
||||||
fill: textColor(key.trLayer, colorscheme.trLayerColor),
|
|
||||||
"font-size": "66%",
|
|
||||||
...textAttribs,
|
|
||||||
"text-anchor": "end",
|
|
||||||
},
|
|
||||||
textContents(key.trLayer),
|
|
||||||
),
|
|
||||||
tag(
|
|
||||||
"text",
|
|
||||||
{
|
|
||||||
x: visual.position[0] + visual.size[0] / 10,
|
|
||||||
y: visual.position[1] + (5 * visual.size[1]) / 6,
|
|
||||||
fill: textColor(key.blLayer, colorscheme.blLayerColor),
|
|
||||||
"font-size": "66%",
|
|
||||||
...textAttribs,
|
|
||||||
"text-anchor": "start",
|
|
||||||
},
|
|
||||||
textContents(key.blLayer),
|
|
||||||
),
|
|
||||||
].join("\n"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderLayout(layout: Layout) {
|
|
||||||
return tag(
|
|
||||||
"svg",
|
|
||||||
{
|
|
||||||
viewBox: [
|
|
||||||
-layout.padding,
|
|
||||||
-layout.padding,
|
|
||||||
2 * layout.padding + layout.size[0],
|
|
||||||
2 * layout.padding + layout.size[1],
|
|
||||||
].join(" "),
|
|
||||||
xmlns: "http://www.w3.org/2000/svg",
|
|
||||||
"xmlns:xlink": "http://www.w3.org/1999/xlink",
|
|
||||||
},
|
|
||||||
layout.visual
|
|
||||||
.map((key, index) =>
|
|
||||||
renderKey(key, layout.keys[index], layout.colorscheme),
|
|
||||||
)
|
|
||||||
.join("\n"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const outPath = process.argv[2];
|
|
||||||
|
|
||||||
// ========== Layout generation
|
|
||||||
const keySize = 50;
|
|
||||||
const keySizeVec: Vec2 = [50, 50];
|
|
||||||
function visualKey(at: Vec2, angle: number = 0): VisualKey {
|
|
||||||
return { position: at, size: keySizeVec, angle };
|
|
||||||
}
|
|
||||||
|
|
||||||
function add(x: Vec2, y: Vec2): Vec2 {
|
|
||||||
return [x[0] + y[0], x[1] + y[1]];
|
|
||||||
}
|
|
||||||
|
|
||||||
function col(at: Vec2): VisualLayout {
|
|
||||||
return [
|
|
||||||
visualKey(at),
|
|
||||||
visualKey(add(at, [0, keySize])),
|
|
||||||
visualKey(add(at, [0, 2 * keySize])),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function radians(deg: number): number {
|
|
||||||
return (deg / 180) * Math.PI;
|
|
||||||
}
|
|
||||||
|
|
||||||
function neg(v: Vec2): Vec2 {
|
|
||||||
return [-v[0], -v[1]];
|
|
||||||
}
|
|
||||||
|
|
||||||
function thumbs(
|
|
||||||
at: Vec2,
|
|
||||||
reverse: boolean,
|
|
||||||
thumbRotation = reverse ? -15 : 15,
|
|
||||||
): VisualLayout {
|
|
||||||
// Distance between thumb key centers
|
|
||||||
const factor = keySize;
|
|
||||||
const offset: Vec2 = [
|
|
||||||
Math.cos(radians(thumbRotation)) * factor,
|
|
||||||
Math.sin(radians(thumbRotation)) * factor,
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = [
|
|
||||||
visualKey(at, thumbRotation),
|
|
||||||
visualKey(add(at, reverse ? neg(offset) : offset), thumbRotation),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (reverse) result.reverse();
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function cols(at: Vec2, cols: Vec2[]): VisualLayout {
|
|
||||||
return cols
|
|
||||||
.map((self, index) => col(add(at, add(self, [index * keySize, 0]))))
|
|
||||||
.flat();
|
|
||||||
}
|
|
||||||
|
|
||||||
function key(
|
|
||||||
main: string,
|
|
||||||
tlLayer = "",
|
|
||||||
trLayer = "",
|
|
||||||
blLayer = "",
|
|
||||||
): KeyboardKey {
|
|
||||||
return { main, tlLayer, trLayer, blLayer };
|
|
||||||
}
|
|
||||||
|
|
||||||
const block: Vec2[] = [
|
|
||||||
[0, keySize],
|
|
||||||
[0, keySize / 2],
|
|
||||||
[0, 0],
|
|
||||||
[0, keySize / 2],
|
|
||||||
[0, keySize],
|
|
||||||
];
|
|
||||||
|
|
||||||
const layout: Layout = {
|
|
||||||
colorscheme: {
|
|
||||||
keyFill: "#ffffff",
|
|
||||||
keyStroke: "#000000",
|
|
||||||
mainLayerColor: "black",
|
|
||||||
tlLayerColor: "blue",
|
|
||||||
trLayerColor: "red",
|
|
||||||
blLayerColor: "purple",
|
|
||||||
},
|
|
||||||
visual: [
|
|
||||||
cols([0, 0], block),
|
|
||||||
thumbs([keySize * 3.5, keySize * 4.5], false),
|
|
||||||
thumbs([keySize * 7.5, keySize * 4.5], true),
|
|
||||||
cols([7 * keySize, 0], block),
|
|
||||||
].flat(),
|
|
||||||
keys: [
|
|
||||||
[
|
|
||||||
key("Q", "!", "1", "f1"),
|
|
||||||
key("A", "<", "6", "f6"),
|
|
||||||
key("X", ">", "", "f11"),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
key("W", "@", "2", "f2"),
|
|
||||||
key("R", "(", "7", "f7"),
|
|
||||||
key("C", ")", "", "f12"),
|
|
||||||
],
|
|
||||||
[key("F", "#", "3", "f3"), key("S", "[", "8", "f8"), key("D", "]")],
|
|
||||||
[key("P", "$", "4", "f4"), key("T", "{", "9", "f9"), key("V", "}")],
|
|
||||||
[key("B", "%", "5", "f5"), key("G", "-", "0", "f10"), key("Z", "—")],
|
|
||||||
[key("TR", "", ""), key("␣", "", "")],
|
|
||||||
[key("⇧", "", ""), key("TL", "", "")],
|
|
||||||
[key("J", "^", "🏠"), key("M", "?", "◄", "😱"), key("K", "", "", "🎮")],
|
|
||||||
[
|
|
||||||
key("L", "&", "⏬", "🔊"),
|
|
||||||
key("N", "_", "▼", "🔉"),
|
|
||||||
key("H", "|", "", "🔇"),
|
|
||||||
],
|
|
||||||
[key("U", "*", "⏫", "🔆"), key("E", "/", "▲", "🔅"), key(",", "\\", "")],
|
|
||||||
[
|
|
||||||
key("Y", "~", "end", "⏪"),
|
|
||||||
key("I", "=", "►", "⏯️"),
|
|
||||||
key(".", "+", "", "⏩"),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
key(":", "`", "del", "copy"),
|
|
||||||
key("O", ";", "", "paste"),
|
|
||||||
key("'", '"', "", "cut"),
|
|
||||||
],
|
|
||||||
].flat(),
|
|
||||||
padding: 20,
|
|
||||||
size: [keySize * 12, keySize * 6],
|
|
||||||
};
|
|
||||||
|
|
||||||
fs.writeFileSync(outPath, renderLayout(layout));
|
|
Loading…
Reference in a new issue