1
Fork 0

Initial commit

This commit is contained in:
Matei Adriel 2023-02-02 00:05:33 +01:00
commit 48ca0eb0b1
No known key found for this signature in database
8 changed files with 429 additions and 0 deletions

5
.gitignore vendored Normal file
View file

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

40
flake.lock Normal file
View file

@ -0,0 +1,40 @@
{
"nodes": {
"flake-utils": {
"locked": {
"lastModified": 1667395993,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1671883564,
"narHash": "sha256-C15oAtyupmLB3coZY7qzEHXjhtUx/+77olVdqVMruAg=",
"path": "/nix/store/0b1s6l5i9izifskg8kgc29jn5bzgdjnv-source",
"rev": "dac57a4eccf1442e8bf4030df6fcbb55883cb682",
"type": "path"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

18
flake.nix Normal file
View file

@ -0,0 +1,18 @@
{
description = "Kayboard layout diagram generation";
inputs.flake-utils.url = "github:numtide/flake-utils";
outputs = { nixpkgs, flake-utils, ... }@inputs:
flake-utils.lib.eachDefaultSystem
(system:
let
pkgs = nixpkgs.legacyPackages.${system};
swoop = pkgs.callPackage ./swoop.nix {};
in
rec {
packages.swoop = swoop;
defaultPackage = packages.swoop;
devShell = pkgs.callPackage ./shell.nix {};
}
);
}

29
package-lock.json generated Normal file
View file

@ -0,0 +1,29 @@
{
"name": "swoop",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "swoop",
"version": "1.0.0",
"devDependencies": {
"@types/node": "^18.11.18"
}
},
"node_modules/@types/node": {
"version": "18.11.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
"dev": true
}
},
"dependencies": {
"@types/node": {
"version": "18.11.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
"dev": true
}
}
}

7
package.json Normal file
View file

@ -0,0 +1,7 @@
{
"name": "swoop",
"version": "1.0.0",
"devDependencies": {
"@types/node": "^18.11.18"
}
}

9
shell.nix Normal file
View file

@ -0,0 +1,9 @@
{ pkgs ? import <nixpkgs> { } }:
with pkgs;
mkShell {
buildInputs = with pkgs; with nodePackages_latest; [
typescript
nodejs
ts-node
];
}

299
src/index.ts Normal file
View file

@ -0,0 +1,299 @@
#!/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(" "),
},
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("Z", ")", "", "f11"),
],
[
key("W", "@", "2", "f2"),
key("S", "[", "7", "f7"),
key("X", "]", "", "f12"),
],
[key("E", "#", "3", "f3"), key("D", "{", "8", "f8"), key("C", "}")],
[key("R", "$", "4", "f4"), key("F", "&lt", "9", "f9"), key("V", "&gt")],
[key("T", "%", "5", "f5"), key("G", ";", "0", "f10"), key("B", "")],
[key("TR", "", ""), key("␣", "", "")],
[key("⇧", "", ""), key("TL", "", "")],
[key("Y", "^", ""), key("H", "-", "◄", "😱"), key("N", "?", "")],
[
key("U", "&", "", "🔊"),
key("J", "_", "▼", "🔉"),
key("M", "/", "", "🔇"),
],
[key("I", "*", "", "🔆"), key("K", "=", "▲", "🔅"), key(",", "\\", "")],
[key("O", "~", ""), key("L", "+", "►"), key(".", "|", "")],
[key("P", "`", "del"), key(":", "", ""), key('"', "'", "")],
].flat(),
padding: 20,
size: [keySize * 12, keySize * 6],
};
fs.writeFileSync(outPath, renderLayout(layout));

22
swoop.nix Normal file
View file

@ -0,0 +1,22 @@
{ pkgs ? import <nixpkgs> { } }:
pkgs.stdenv.mkDerivation {
name = "swoop";
src = ./src;
buildInputs = with pkgs; with nodePackages_latest; [
typescript
esbuild
nodejs
];
buildPhase = ''
esbuild $src/index.ts --bundle --outfile=./out.js
'';
installPhase = ''
mkdir $out/bin -p
cp -rv out.js $out/bin/swoop
'';
}