Skip to content

Gothic curve strokes ignore start/end type values (a1/a2 in cdDrawCurveU) #18

Description

@MasKusuno

Summary

When kShotai = kGothic and a curve stroke (cdDrawCurve / cdDrawBezier) is rendered, the start type (a1/ta1) and end type (a2/ta2) arguments are silently ignored — the function never extends the curve endpoints to compensate for the requested termination style. Straight-line strokes (cdDrawLine) honour the same arguments correctly, so the inconsistency is contour-shape specific.

I want to flag this for visibility rather than ship a unilateral fix, because I think it is the kind of behavior that needs to align with the original kamichikoichi/kage-engine first (per your guidance in #2).

Current behavior in this repo (kurgm/kage-engine)

In src/font/gothic/cd.ts cdDrawCurveU declares two locals without ever assigning them:

function cdDrawCurveU(
    font: Gothic, polygons: Polygons,
    x1, y1, sx1, sy1, sx2, sy2, x2, y2,
    _ta1: number, _ta2: number,
) {
    // eslint-disable-next-line no-unassigned-vars
    let a1: number;
    // eslint-disable-next-line no-unassigned-vars
    let a2: number;

    let delta1 = 0;
    switch (a1! % 10) { // ?????
        case 2:
            delta1 = font.kWidth;
            break;
        case 3:
            delta1 = font.kWidth * font.kKakato;
            break;
    }
    // ...same pattern for a2/delta2...
}

a1! / a2! are non-null assertions on values that never get a value — they are undefined at runtime. undefined % 10 evaluates to NaN, so the switch never matches any case, and delta1 / delta2 always stay 0. Effect: the curve endpoints are never extended.

cdDrawLine in the same file does it correctly — there a1 and a2 are assigned to either ta1/ta2 or their swapped counterparts before the same switch. The leading ????? comments in cdDrawCurveU and the eslint-disable no-unassigned-vars directives suggest the implementer knew something was off but preserved the pattern intentionally.

Why I think this matches the original engine

I traced the equivalent code in kamichikoichi/kage-engine/kagecd.js and the same dead-code path is present:

function cdDrawCurveU(kage, polygons, x1, y1, sx1, sy1, sx2, sy2, x2, y2, ta1, ta2){
  var rad, t;
  // ...
  var a1, a2, opt1, opt2, opt3, opt4;

  if(kage.kShotai == kage.kMincho){ // mincho
    a1 = ta1 % 1000;
    a2 = ta2 % 100;
    // ...full mincho-only assignment + handling, ~600 lines...
  }
  else{ //gothic
    if(a1 % 10 == 2){ /* ... */ }   // a1 is `var`-hoisted, so undefined here
    if(a1 % 10 == 3){ /* ... */ }
    if(a2 % 10 == 2){ /* ... */ }
    // ...
  }
}

So the TS port is faithful to the original — the assignments only exist on the Mincho branch, and the Gothic branch reads the hoisted-but-uninitialized a1/a2. As far as I can tell, this dead code was preserved intentionally during the TS port to match the original.

Why this is visible in practice

It primarily affects hiragana and katakana, where every stroke is a curve and most strokes have meaningful start/end types (e.g. 2:7:8:...):

  • Mincho honours a2 = 7 / a3 = 8 and produces the expected うろこ・はね・とめ decorations, giving each kana its characteristic style.
  • Gothic falls back to a uniform-width curve with no endpoint extension, so kana glyphs look more like a "mincho skeleton with decorations stripped" than a real Gothic typeface.

(For kanji the visual gap is smaller because most strokes are straight lines, where cdDrawLine already handles a1/a2 correctly.)

We hit this while building a free-license Gothic font from GlyphWiki dumps. The end-type extension Gothic produces today is "good enough" for most kanji but visibly weak on kana.

Possible directions

I'd appreciate your guidance on which (if any) of these makes sense:

  1. Treat as upstream-locked. If you'd like to keep the TS port aligned with kamichikoichi/kage-engine exactly, the right path is for the Gothic branch to be fixed in the original first. I'm happy to file a parallel issue there if you point me to the right channel.

  2. Opt-in Gothic improvement here. Add an off-by-default font parameter (e.g. font.kGothicHonorEndTypes, defaulting to false) that, when enabled, populates a1/a2 from ta1/ta2 and runs the existing extension logic. The default keeps original-compatible output; opting in gives downstream font generators (TTF / SVG with non-zero fill) a better-shaped Gothic without forking the engine. I'd be happy to send this as a PR.

  3. Document as known limitation. Add a sentence to the README / Gothic-related TSDoc clarifying that a1/a2 are intentionally ignored on Gothic curves for original-engine parity, so future readers don't waste time tracing the dead code.

I'm open to whichever fits the project's direction. Mostly I wanted to surface this so that anyone hitting the same kana-quality wall can find prior art.

Refs #2

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions