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
|
||||
.direnv
|
||||
node_modules
|
||||
result
|
||||
out.*
|
||||
|
|
|
@ -7,12 +7,13 @@
|
|||
(system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
swoop = pkgs.callPackage ./swoop.nix { };
|
||||
layout-lens = pkgs.callPackage ./layout-lens/default.nix { };
|
||||
in
|
||||
rec {
|
||||
packages.swoop = swoop;
|
||||
defaultPackage = packages.swoop;
|
||||
devShell = pkgs.callPackage ./shell.nix { };
|
||||
packages.layout-lens = layout-lens;
|
||||
defaultPackage = packages.layout-lens;
|
||||
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