diff --git a/ts/output/chtml/FontData.ts b/ts/output/chtml/FontData.ts index a5180c61b..a9bae47c8 100644 --- a/ts/output/chtml/FontData.ts +++ b/ts/output/chtml/FontData.ts @@ -39,6 +39,7 @@ import { StyleJsonSheet, } from '../../util/StyleJson.js'; import { em } from '../../util/lengths.js'; +import { VFUZZ, HFUZZ } from '../common/FontData.js'; export * from '../common/FontData.js'; @@ -479,6 +480,7 @@ export class ChtmlFontData extends FontData< HDW: ChtmlCharData ): number { if (!n) return 0; + let fuzz = 0; const [h, d, w] = this.getChar(v, n); const css: StyleJsonData = { width: this.em0(w) }; if (part !== 'ext') { @@ -494,18 +496,25 @@ export class ChtmlFontData extends FontData< css.margin = `${this.em(y)} ${dw} ${this.em(-y)}`; } else { // + // Adjust the height and depth for a little overlap. // Set the line-height to have the extenders touch, - // (plus a little extra for Safari, whose line-height is - // not accurate), and shift the extender stack to overlap - // the ends. + // and shift the extender stack to overlap the ends. // - css['line-height'] = this.em0(h + d + 0.005); - styles[`mjx-stretchy-v${c} > mjx-${part} > mjx-spacer`] = { - 'margin-top': this.em(-d), - }; + fuzz = VFUZZ; + const lh = Math.max(VFUZZ, h + d - VFUZZ); + css['line-height'] = this.em0(lh); + // + // Adjust the top margin to make sure we have overlap with the top part + // + const D = h - lh / 2 - VFUZZ; + if (D) { + styles[`mjx-stretchy-v${c} > mjx-ext > mjx-spacer`] = { + 'margin-top': this.em(D), + }; + } } styles[`mjx-stretchy-v${c} > mjx-${part}`] = css; - return h + d; + return Math.max(0, h + d - fuzz); } /*******************************************************/ @@ -556,7 +565,7 @@ export class ChtmlFontData extends FontData< } if (data.ext) { styles[`mjx-stretchy-h${c} > mjx-ext > mjx-spacer`]['letter-spacing'] = - this.em(-data.ext); + this.em(-data.ext - HFUZZ); } } diff --git a/ts/output/chtml/Wrappers/mo.ts b/ts/output/chtml/Wrappers/mo.ts index 404708054..d4f19fedf 100644 --- a/ts/output/chtml/Wrappers/mo.ts +++ b/ts/output/chtml/Wrappers/mo.ts @@ -30,6 +30,8 @@ import { ChtmlDelimiterData, ChtmlFontData, ChtmlFontDataClass, + VFUZZ, + HFUZZ, } from '../FontData.js'; import { CharDataArray } from '../../common/FontData.js'; import { @@ -273,7 +275,7 @@ export const ChtmlMo = (function (): ChtmlMoClass { // The ext parameter should be 0, but line-height in Safari // is not accurate, so this produces extra extenders to compensate // - this.createAssembly(parts, stretch, stretchv, dom, h + d, 0.05, '\n'); + this.createAssembly(parts, stretch, stretchv, dom, h + d, VFUZZ, '\n'); // // Vertical needs an extra (empty) element to get vertical position right // in some browsers (e.g., Safari) @@ -282,7 +284,8 @@ export const ChtmlMo = (function (): ChtmlMoClass { styles.height = this.em(h + d); styles.verticalAlign = this.em(-d); } else { - this.createAssembly(parts, stretch, stretchv, dom, w, delim.ext || 0); + const ext = (delim.ext || 0) + HFUZZ; + this.createAssembly(parts, stretch, stretchv, dom, w, ext); styles.width = this.em(w); } // @@ -340,10 +343,12 @@ export const ChtmlMo = (function (): ChtmlMoClass { // Set up the beginning, extension, and end pieces // this.createPart('mjx-beg', parts[0], sn[0], sv[0], dom); - this.createPart('mjx-ext', parts[1], sn[1], sv[1], dom, WH1, WHx, nl); + /* prettier-ignore */ + this.createPart('mjx-ext', parts[1], sn[1], sv[1], dom, WH1, WHx, nl, WHb, WHm / 2 || WHe); if (parts[3]) { this.createPart('mjx-mid', parts[3], sn[3], sv[3], dom); - this.createPart('mjx-ext', parts[1], sn[1], sv[1], dom, WH2, WHx, nl); + /* prettier-ignore */ + this.createPart('mjx-ext', parts[1], sn[1], sv[1], dom, WH2, WHx, nl, WHm / 2, WHe); } this.createPart('mjx-end', parts[2], sn[2], sv[2], dom); } @@ -359,6 +364,8 @@ export const ChtmlMo = (function (): ChtmlMoClass { * @param {number} W The extension width * @param {number} Wx The width of the extender character * @param {string} nl Character to use between extenders + * @param {number} Wb The beginning width + * @param {number} We The ending width */ protected createPart( part: string, @@ -368,7 +375,9 @@ export const ChtmlMo = (function (): ChtmlMoClass { dom: N[], W: number = 0, Wx: number = 0, - nl: string = '' + nl: string = '', + Wb: number = 0, + We: number = 0 ) { if (n) { const options = data[3]; @@ -379,13 +388,28 @@ export const ChtmlMo = (function (): ChtmlMoClass { const c = options.c || String.fromCodePoint(n); let nodes = [] as (N | T)[]; if (part === 'mjx-ext' && (Wx || options.dx)) { + // + // If the top and bottom must overlap, adjust the border sizes and remove the clipping + // + if (W < 0 && nl) { + dom.push( + this.html(part, { + ...(font ? { class: font } : {}), + style: { + 'border-width': `${this.em(Wb + W / 2)} 0 ${this.em(We + W / 2)}`, + 'clip-path': 'none', + }, + }) + ); + return; + } // // Some combining characters are listed as width 0, // so get "real" width from dx and take off some // for the right bearing. // if (!Wx) { - Wx = Math.max(0.06, 2 * options.dx - 0.06); + Wx = Math.max(HFUZZ, 2 * options.dx - HFUZZ); } const n = Math.min(Math.ceil(W / Wx) + 1, 500); if (options.cmb) { diff --git a/ts/output/common/FontData.ts b/ts/output/common/FontData.ts index 779196645..cf9609f88 100644 --- a/ts/output/common/FontData.ts +++ b/ts/output/common/FontData.ts @@ -30,6 +30,11 @@ import { retryAfter } from '../../util/Retries.js'; import { DIRECTION } from './Direction.js'; export { DIRECTION } from './Direction.js'; +/*****************************************************************/ + +export const VFUZZ = 0.07; // overlap for vertical stretchy glyphs +export const HFUZZ = 0.07; // overlap for horizontal stretchy glyphs + /****************************************************************************/ /** diff --git a/ts/output/svg/Wrappers/mo.ts b/ts/output/svg/Wrappers/mo.ts index 087330bec..cee32ffd5 100644 --- a/ts/output/svg/Wrappers/mo.ts +++ b/ts/output/svg/Wrappers/mo.ts @@ -30,6 +30,8 @@ import { SvgDelimiterData, SvgFontData, SvgFontDataClass, + VFUZZ, + HFUZZ, } from '../FontData.js'; import { CommonMo, @@ -41,11 +43,6 @@ import { MmlMo } from '../../../core/MmlTree/MmlNodes/mo.js'; import { BBox } from '../../../util/BBox.js'; import { DIRECTION, SvgCharData } from '../FontData.js'; -/*****************************************************************/ - -const VFUZZ = 0.1; // overlap for vertical stretchy glyphs -const HFUZZ = 0.1; // overlap for horizontal stretchy glyphs - /*****************************************************************/ /** * The SvgMo interface for the SVG Mo wrapper @@ -333,27 +330,40 @@ export const SvgMo = (function (): SvgMoClass { * @param {number} B The height of the bottom glyph in the delimiter * @param {number} W The width of the stretched delimiter */ - /* prettier-ignore */ - protected addExtV(n: number, v: string, H: number, D: number, T: number, B: number, W: number) { + protected addExtV( + n: number, + v: string, + H: number, + D: number, + T: number, + B: number, + W: number + ) { if (!n) return; - T = Math.max(0, T - VFUZZ); // A little overlap on top - B = Math.max(0, B - VFUZZ); // A little overlap on bottom + T = Math.max(0, T - VFUZZ); // // A little overlap on top + B = Math.max(0, B - VFUZZ); // // A little overlap on bottom const adaptor = this.adaptor; const [h, d, w] = this.getChar(n, v); - const Y = H + D - T - B; // The height of the extender - const s = 1.5 * Y / (h + d); // Scale height by 1.5 to avoid bad ends - // (glyphs with rounded or anti-aliased ends don't stretch well, - // so this makes for sharper ends) - const y = (s * (h - d) - Y) / 2; // The bottom point to clip the extender + const Y = H + D - T - B; // // The height of the extender + const s = (1.5 * Y) / (h + d); // // Scale height by 1.5 to avoid bad ends + // // (glyphs with rounded or anti-aliased ends don't stretch well, + // // so this makes for sharper ends) + const y = (s * (h - d) - Y) / 2; // // The bottom point to clip the extender if (Y <= 0) return; const svg = this.svg('svg', { - width: this.fixed(w), height: this.fixed(Y), - y: this.fixed(B - D), x: this.fixed((W - w) / 2), - viewBox: [0, y, w, Y].map(x => this.fixed(x)).join(' ') + width: this.fixed(w), + height: this.fixed(Y), + y: this.fixed(B - D), + x: this.fixed((W - w) / 2), + viewBox: [0, y, w, Y].map((x) => this.fixed(x)).join(' '), }); this.addGlyph(n, v, 0, 0, svg); const glyph = adaptor.lastChild(svg); - adaptor.setAttribute(glyph as N, 'transform', `scale(1,${this.jax.fixed(s)})`); + adaptor.setAttribute( + glyph as N, + 'transform', + `scale(1,${this.jax.fixed(s)})` + ); if (this.dom[0]) { adaptor.append(this.dom[0], svg); }