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