Implement running man 1FBB2 and 1FBB3

This commit is contained in:
Daniel Imms
2025-12-23 04:15:41 -08:00
parent 16b72dfd63
commit 268f7af5db
3 changed files with 102 additions and 4 deletions
@@ -541,8 +541,8 @@ export const customGlyphDefinitions: { [index: string]: CustomGlyphCharacterDefi
// Terminal graphic characters (1FBB0-1FBB3)
'\u{1FBB0}': { type: CustomGlyphDefinitionType.PATH, data: 'M0.1,0.2 L0.1,.8 L.4,.6 L.9,0.6 Z' }, // ARROWHEAD-SHAPED POINTER
'\u{1FBB1}': { type: CustomGlyphDefinitionType.PATH_NEGATIVE, data: { d: 'M.1,.55 L.35,.85 L.9,.2', type: CustomGlyphVectorType.STROKE } }, // INVERSE CHECK MARK
// 1FBB2 LEFT HALF RUNNING MAN
// 1FBB3 RIGHT HALF RUNNING MAN
'\u{1FBB2}': { type: CustomGlyphDefinitionType.PATH, data: 'M.29,.27 L0.13,.56 L.22,.59 L.35,0.35 L.67,.35 L.57,.57 L.71,.76 L.22,.76 L.42,1 L.53,.98 L.43,.86 L.9,.86 L.71,.6 L1,.6 L1,.52 L.83,.52 L.92,.36 L1,.36 L1,.27Z M.99,.13 A.12,.12,0,1,1,.75,.13 A.12,.12,0,1,1,.99,.13' }, // LEFT HALF RUNNING MAN
'\u{1FBB3}': { type: CustomGlyphDefinitionType.PATH, data: 'M0,.27 L.3,.27 L.55,.12 L.63,.18 L.33,.36 L0,.36 M0,.52 L.33,.52 L.59,.89 L.73,.89 L.73,.98 L.53,.98 L.28,.6 L0,.6' }, // RIGHT HALF RUNNING MAN
// Arrows (1FBB4-1FBB8)
'\u{1FBB4}': { type: CustomGlyphDefinitionType.PATH_NEGATIVE, data: { d: 'M.15,.6 L.5,.4 L.5,.5 L.75,.5 L.75,.2 L.85,.2 L.85,.7 L.5,.7 L.5,.8 Z', type: CustomGlyphVectorType.FILL } }, // INVERSE DOWNWARDS ARROW WITH TIP LEFTWARDS
@@ -91,6 +91,8 @@ function drawPathDefinitionCharacter(
): void {
const instructions = typeof charDefinition === 'string' ? charDefinition : charDefinition(0, 0);
ctx.beginPath();
let currentX = 0;
let currentY = 0;
for (const instruction of instructions.split(' ')) {
const type = instruction[0];
const args: string[] = instruction.substring(1).split(',');
@@ -100,6 +102,20 @@ function drawPathDefinitionCharacter(
}
continue;
}
if (type === 'A') {
// SVG arc: A rx,ry,xAxisRotation,largeArcFlag,sweepFlag,x,y
const rx = parseFloat(args[0]) * deviceCellWidth;
const ry = parseFloat(args[1]) * deviceCellHeight;
const xAxisRotation = parseFloat(args[2]) * Math.PI / 180;
const largeArcFlag = parseInt(args[3]);
const sweepFlag = parseInt(args[4]);
const x = xOffset + parseFloat(args[5]) * deviceCellWidth;
const y = yOffset + parseFloat(args[6]) * deviceCellHeight;
drawSvgArc(ctx, currentX, currentY, rx, ry, xAxisRotation, largeArcFlag, sweepFlag, x, y);
currentX = x;
currentY = y;
continue;
}
const translatedArgs = args.map((e, i) => {
const val = parseFloat(e);
return i % 2 === 0
@@ -108,13 +124,94 @@ function drawPathDefinitionCharacter(
});
if (type === 'M') {
ctx.moveTo(translatedArgs[0], translatedArgs[1]);
currentX = translatedArgs[0];
currentY = translatedArgs[1];
} else if (type === 'L') {
ctx.lineTo(translatedArgs[0], translatedArgs[1]);
currentX = translatedArgs[0];
currentY = translatedArgs[1];
}
}
ctx.fill();
}
/**
* Converts SVG arc parameters to canvas arc/ellipse calls.
* Based on the SVG spec's endpoint to center parameterization conversion.
*/
function drawSvgArc(
ctx: CanvasRenderingContext2D,
x1: number, y1: number,
rx: number, ry: number,
phi: number,
largeArcFlag: number,
sweepFlag: number,
x2: number, y2: number
): void {
// Handle degenerate cases
if (rx === 0 || ry === 0) {
ctx.lineTo(x2, y2);
return;
}
rx = Math.abs(rx);
ry = Math.abs(ry);
const cosPhi = Math.cos(phi);
const sinPhi = Math.sin(phi);
// Step 1: Compute (x1', y1')
const dx = (x1 - x2) / 2;
const dy = (y1 - y2) / 2;
const x1p = cosPhi * dx + sinPhi * dy;
const y1p = -sinPhi * dx + cosPhi * dy;
// Step 2: Compute (cx', cy')
let rxSq = rx * rx;
let rySq = ry * ry;
const x1pSq = x1p * x1p;
const y1pSq = y1p * y1p;
// Correct radii if necessary
const lambda = x1pSq / rxSq + y1pSq / rySq;
if (lambda > 1) {
const lambdaSqrt = Math.sqrt(lambda);
rx *= lambdaSqrt;
ry *= lambdaSqrt;
rxSq = rx * rx;
rySq = ry * ry;
}
let sq = (rxSq * rySq - rxSq * y1pSq - rySq * x1pSq) / (rxSq * y1pSq + rySq * x1pSq);
if (sq < 0) sq = 0;
const coef = (largeArcFlag === sweepFlag ? -1 : 1) * Math.sqrt(sq);
const cxp = coef * (rx * y1p / ry);
const cyp = coef * -(ry * x1p / rx);
// Step 3: Compute (cx, cy) from (cx', cy')
const cx = cosPhi * cxp - sinPhi * cyp + (x1 + x2) / 2;
const cy = sinPhi * cxp + cosPhi * cyp + (y1 + y2) / 2;
// Step 4: Compute angles
const ux = (x1p - cxp) / rx;
const uy = (y1p - cyp) / ry;
const vx = (-x1p - cxp) / rx;
const vy = (-y1p - cyp) / ry;
const startAngle = Math.atan2(uy, ux);
let dTheta = Math.atan2(vy, vx) - startAngle;
if (sweepFlag === 0 && dTheta > 0) {
dTheta -= 2 * Math.PI;
} else if (sweepFlag === 1 && dTheta < 0) {
dTheta += 2 * Math.PI;
}
const endAngle = startAngle + dTheta;
ctx.ellipse(cx, cy, rx, ry, phi, startAngle, endAngle, sweepFlag === 0);
}
/**
* Draws a "negative" path where the background color is used to draw the shape on top of a
* foreground-filled cell. This creates the appearance of a cutout without using actual
+3 -2
View File
@@ -835,8 +835,9 @@ function customGlyphAlignmentHandler(): void {
term.write('🭊🭁🭌🬿 πŸ­ˆπŸ­†πŸ­‚πŸ­πŸ­‘πŸ¬½ πŸ­‡πŸ­„πŸ­πŸ¬Ό πŸ­ƒπŸ­Ž πŸ­…πŸ­ 🭨πŸ­ͺ\n\r');
term.write('πŸ­₯πŸ­’πŸ­πŸ­š πŸ­£πŸ­§πŸ­“πŸ­žπŸ­œπŸ­˜ πŸ­’πŸ­•πŸ­ πŸ­— πŸ­”πŸ­Ÿ πŸ­–πŸ­‘ πŸ­ͺ🭨\n\r');
term.write(' πŸ­’πŸ­— πŸ­€πŸ­™ πŸ­¦πŸ­›\n\r');
term.write('\x1b[0mFill tests:\x1b[34m\n\r');
term.write('\x1b[0mComposite characters:\x1b[34m\n\r');
term.write('\u{1FBB2}\u{1FBB3}\n\r');
term.write('\x1b[0mFill tests:\x1b[35m\n\r');
const fillChars = ['\u{2591}', '\u{2592}', '\u{2593}', '\u{1FB8C}', '\u{1FB8D}', '\u{1FB8E}', '\u{1FB8F}', '\u{1FB90}', '\u{1FB91}', '\u{1FB92}', '\u{1FB94}', '\u{1FB95}', '\u{1FB96}', '\u{1FB97}', '\u{1FB98}', '\u{1FB99}'];
while (fillChars.length > 0) {
const batch = fillChars.splice(0, 10);