1
Fork 0

Clean up the layout renderer!

This commit is contained in:
Matei Adriel 2023-10-22 16:10:53 +02:00
parent 377fe0dd3d
commit 996908b729
No known key found for this signature in database
18 changed files with 444 additions and 314 deletions

1
.gitignore vendored
View file

@ -1,5 +1,4 @@
.envrc
.direnv
node_modules
result
out.*

View file

@ -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 { };
}
);
}

View 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
View file

@ -0,0 +1,5 @@
{ pkgs ? import <nixpkgs> { } }:
with pkgs;
mkShell {
buildInputs = with pkgs; [ qmk ];
}

1
layout-lens/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
node_modules

62
layout-lens/src/config.ts Normal file
View 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
View 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
View 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),
);
}

View 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
View 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
View 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
View 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
View 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]];
}

View file

@ -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", "&lt;", "6", "f6"),
key("X", "&gt;", "", "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", "&amp;", "⏬", "🔊"),
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));