From 268f7af5db6e0325f4673a428c9968b30e132db4 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 23 Dec 2025 04:15:41 -0800 Subject: [PATCH] Implement running man 1FBB2 and 1FBB3 --- .../customGlyphs/CustomGlyphDefinitions.ts | 4 +- .../src/customGlyphs/CustomGlyphRasterizer.ts | 97 +++++++++++++++++++ demo/client.ts | 5 +- 3 files changed, 102 insertions(+), 4 deletions(-) diff --git a/addons/addon-webgl/src/customGlyphs/CustomGlyphDefinitions.ts b/addons/addon-webgl/src/customGlyphs/CustomGlyphDefinitions.ts index f995e5e7..3fac87d2 100644 --- a/addons/addon-webgl/src/customGlyphs/CustomGlyphDefinitions.ts +++ b/addons/addon-webgl/src/customGlyphs/CustomGlyphDefinitions.ts @@ -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 diff --git a/addons/addon-webgl/src/customGlyphs/CustomGlyphRasterizer.ts b/addons/addon-webgl/src/customGlyphs/CustomGlyphRasterizer.ts index 41994760..3cda6a9c 100644 --- a/addons/addon-webgl/src/customGlyphs/CustomGlyphRasterizer.ts +++ b/addons/addon-webgl/src/customGlyphs/CustomGlyphRasterizer.ts @@ -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 diff --git a/demo/client.ts b/demo/client.ts index 12b0a6d0..9bcf4388 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -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);