1
Fork 0
keyswirl/layout-lens/src/render.ts

365 lines
9.8 KiB
TypeScript
Raw Normal View History

2024-02-21 08:32:04 +01:00
import { children, tag, px } from "./svg";
import * as L from "./layout";
import * as V from "./vec2";
2023-10-22 16:10:53 +02:00
import {
2024-02-21 08:32:04 +01:00
ChordConfig,
2023-10-22 16:10:53 +02:00
KeyboardKey,
KeySymbol,
Layout,
LayoutColorscheme,
LayoutMeasurements,
2023-10-22 16:10:53 +02:00
SpecialSymbols,
VisualKey,
} from "./types";
function textContents(input: KeySymbol): string {
if (input === SpecialSymbols.TL || input === SpecialSymbols.TR) return "■";
return input;
}
2024-02-21 08:32:04 +01:00
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;
}
2023-10-22 16:10:53 +02:00
function renderKey(
visual: VisualKey,
key: KeyboardKey,
colorscheme: LayoutColorscheme,
measurements: LayoutMeasurements,
2024-02-21 08:32:04 +01:00
flags: KeyRenderingFlags,
2023-10-22 16:10:53 +02:00
) {
2024-02-21 08:32:04 +01:00
const withPadding = applyKeyPadding(visual, measurements);
const center = keyCenter(visual);
2023-10-22 16:10:53 +02:00
2024-02-21 08:32:04 +01:00
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),
),
]
: [];
2023-10-22 16:10:53 +02:00
return tag(
"g",
{
transform:
visual.angle && visual.angle !== 0
2024-02-21 08:32:04 +01:00
? `rotate(${visual.angle}, ${center[0]}, ${center[1]})`
: "",
2023-10-22 16:10:53 +02:00
},
2024-02-21 08:32:04 +01:00
children(
2023-10-22 16:10:53 +02:00
tag("rect", {
width: px(withPadding.size[0]),
height: px(withPadding.size[1]),
x: withPadding.position[0],
y: withPadding.position[1],
rx: measurements.keyCornerRadius,
2023-10-22 16:10:53 +02:00
fill: colorscheme.keyFill,
stroke: colorscheme.keyStroke,
2024-02-21 08:32:04 +01:00
"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),
2024-02-21 08:32:04 +01:00
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
),
2023-10-22 16:10:53 +02:00
}),
2024-02-21 08:32:04 +01:00
dottedIndicator(first),
dottedIndicator(second),
2023-10-22 16:10:53 +02:00
tag(
"text",
{
2024-02-21 08:32:04 +01:00
x: middle[0],
y: middle[1],
"font-size": `${(chord.fontSizeModifier || 1) * 70}%`,
fill: textColor(
colorscheme,
chord.output,
colorscheme.mainLayerColor,
),
2023-10-22 16:10:53 +02:00
...textAttribs,
2024-02-21 08:32:04 +01:00
"dominant-baseline": "middle",
2023-10-22 16:10:53 +02:00
},
2024-02-21 08:32:04 +01:00
textContents(chord.output),
2023-10-22 16:10:53 +02:00
),
2024-02-21 08:32:04 +01:00
),
2023-10-22 16:10:53 +02:00
);
}
export function renderLayout(layout: Layout) {
2024-02-21 08:32:04 +01:00
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
2023-10-22 16:10:53 +02:00
return tag(
"svg",
{
viewBox: [
2024-02-21 08:32:04 +01:00
-layout.elementLayout.imagePadding,
-layout.elementLayout.imagePadding,
2 * layout.elementLayout.imagePadding + totalWidth,
2 * layout.elementLayout.imagePadding + totalHeight,
2023-10-22 16:10:53 +02:00
].join(" "),
xmlns: "http://www.w3.org/2000/svg",
"xmlns:xlink": "http://www.w3.org/1999/xlink",
},
2024-02-21 08:32:04 +01:00
children(
...mainKeys,
tag(
"g",
{
transform: `translate(0 ${
layout.size[1] * layout.measurements.keySize +
layout.elementLayout.mainToChordsGap
})`,
},
children(...chords),
),
),
2023-10-22 16:10:53 +02:00
);
2024-02-21 08:32:04 +01:00
// }}}
2023-10-22 16:10:53 +02:00
}