/************************************************************* * * Copyright (c) 2018 The MathJax Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * @fileoverview Implements the SVGmo wrapper for the MmlMo object * * @author dpvc@mathjax.org (Davide Cervone) */ import {SVGWrapper, SVGConstructor, StringMap} from '../Wrapper.js'; import {CommonMo, CommonMoMixin, DirectionVH} from '../../common/Wrappers/mo.js'; import {MmlMo} from '../../../core/MmlTree/MmlNodes/mo.js'; import {BBox} from '../BBox.js'; import {DIRECTION, NOSTRETCH, SVGCharOptions, DelimiterData} from '../FontData.js'; /*****************************************************************/ const VFUZZ = 0.1; // overlap for vertical stretchy glyphs const HFUZZ = 0.1; // overlap for horizontal stretchy glyphs /*****************************************************************/ /** * The SVGmo wrapper for the MmlMo object * * @template N The HTMLElement node class * @template T The Text node class * @template D The Document class */ export class SVGmo extends CommonMoMixin>(SVGWrapper) { public static kind = MmlMo.prototype.kind; /** * @override */ public toSVG(parent: N) { const attributes = this.node.attributes; const symmetric = (attributes.get('symmetric') as boolean) && this.stretch.dir !== DIRECTION.Horizontal; const stretchy = this.stretch.dir !== DIRECTION.None; if (stretchy && this.size === null) { this.getStretchedVariant([]); } let svg = this.standardSVGnode(parent); if (stretchy && this.size < 0) { this.stretchSVG(svg, symmetric); } else { if (symmetric || attributes.get('largeop')) { const bbox = BBox.empty(); super.computeBBox(bbox); const u = this.fixed((bbox.d - bbox.h) / 2 + this.font.params.axis_height); if (u !== '0') { this.adaptor.setAttribute(svg, 'transform', 'translate(0 '+ u + ')'); } } this.addChildren(svg); } } /** * Create the SVG for a multi-character stretchy delimiter * * @param {N} svg The parent element in which to put the delimiter * @param {boolean} symmetric Whether delimiter should be symmetric about the math axis */ protected stretchSVG(svg: N, symmetric: boolean) { const stretch = this.stretch.stretch; const bbox = this.getBBox(); if (this.stretch.dir === DIRECTION.Vertical) { this.stretchVertical(stretch, bbox); } else { this.stretchHorizontal(stretch, bbox); } } /** * @param {number[]} stretch The characters to use for stretching * @param {BBox} bbox The full size of the stretched character */ protected stretchVertical(stretch: number[], bbox: BBox) { const {h, d, w} = bbox; const T = this.addTop(stretch[0], h, w); const B = this.addBot(stretch[2], d, w); if (stretch.length === 4) { const [H, D] = this.addMidV(stretch[3], w); this.addExtV(stretch[1], h, 0, T, H, w); this.addExtV(stretch[1], 0, d, D, B, w); } else { this.addExtV(stretch[1], h, d, T, B, w); } } /** * @param {number[]} stretch The characters to use for stretching * @param {BBox} bbox The full size of the stretched character */ protected stretchHorizontal(stretch: number[], bbox: BBox) { const {h, d, w} = bbox const L = this.addLeft(stretch[0]); const R = this.addRight(stretch[2], w); if (stretch.length === 4) { const [x1, x2] = this.addMidH(stretch[3], w); const w2 = w / 2; this.addExtH(stretch[1], w2, L, w2 - x1); this.addExtH(stretch[1], w2, x2 - w2, R, w2); } else { this.addExtH(stretch[1], w, L, R); } } /***********************************************************/ /** * @param {number} n The number of the character to look up * @return {CharData} The full CharData object, with CharOptions guaranteed to be defined */ protected getChar(n: number) { const char = this.font.getChar('-size4', n) || [0, 0, 0, null]; return [char[0], char[1], char[2], char[3] || {}] as [number, number, number, SVGCharOptions]; } /** * @param {number} n The character code for the glyph * @param {number} x The x position of the glyph * @param {number} y The y position of the glyph * @param {N} parent The container for the glyph * @return {N} The SVG path for the glyph */ protected addGlyph(n: number, x: number, y: number, parent: N = null) { return this.placeChar(n, x, y, parent || this.element, '-size4'); } /***********************************************************/ /** * @param {number} n The character number for the top glyph * @param {number} H The height of the stretched delimiter * @param {number} W The width of the stretched delimiter * @return {number} The total height of the top glyph */ protected addTop(n: number, H: number, W: number) { if (!n) return 0; const [h, d, w] = this.getChar(n); this.addGlyph(n, (W - w) / 2, H - h); return h + d; } /** * @param {number} n The character number for the extender glyph * @param {number} H The height of the stretched delimiter * @param {number} D The depth of the stretched delimiter * @param {number} T The height of the top glyph in the delimiter * @param {number} B The height of the bottom glyph in the delimiter * @param {number} W The width of the stretched delimiter */ protected addExtV(n: number, 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 const adaptor = this.adaptor; const [h, d, w] = this.getChar(n); 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 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(' ') }); this.addGlyph(n, 0, 0, svg); const glyph = adaptor.lastChild(svg); adaptor.setAttribute(glyph, 'transform', 'scale(1, ' + this.jax.fixed(s) + ')'); adaptor.append(this.element, svg); } /** * @param {number} n The character number for the bottom glyph * @param {number} D The depth of the stretched delimiter * @param {number} W The width of the stretched delimiter * @return {number} The total height of the bottom glyph */ protected addBot(n: number, D: number, W: number) { if (!n) return 0; const [h, d, w] = this.getChar(n); this.addGlyph(n, (W - w) / 2, d - D); return h + d; } /** * @param {number} n The character number for the middle glyph * @param {number} W The width of the stretched delimiter * @return {number[]} The top and bottom positions of the middle glyph */ protected addMidV(n: number, W: number) { if (!n) return [0, 0]; const [h, d, w] = this.getChar(n); const y = (d - h) / 2 + this.font.params.axis_height; this.addGlyph(n, (W - w) / 2, y); return [h + y, d - y]; } /***********************************************************/ /** * @param {number} n The character number for the left glyph of the stretchy character * @return {number} The width of the left glyph */ protected addLeft(n: number) { return (n ? this.addGlyph(n, 0, 0) : 0); } /** * @param {number} n The character number for the extender glyph of the stretchy character * @param {number} W The width of the stretched character * @param {number} L The width of the left glyph of the stretchy character * @param {number} R The width of the right glyph of the stretchy character * @param {number} x The x-position of the extender (needed for ones with two extenders) */ protected addExtH(n: number, W: number, L: number, R: number, x: number = 0) { if (!n) return; R = Math.max(0, R - HFUZZ); // A little less than the width of the right glyph L = Math.max(0, L - HFUZZ); // A little less than the width of the left glyph const adaptor = this.adaptor; const [h, d, w] = this.getChar(n); const X = W - L - R; // The width of the extender const Y = h + d + 2 * VFUZZ; // The height (plus some fuzz) of the extender const s = 1.5 * (X / w); // Scale the width so that left- and right-bearing won't hurt us const svg = this.svg('svg', { width: this.fixed(X), height: this.fixed(Y), x: this.fixed(x + L), y: this.fixed(-VFUZZ), viewBox: [(s * w - X) / 2, -VFUZZ, X, Y].map(x => this.fixed(x)).join(' ') }); this.addGlyph(n, 0, 0, svg); const glyph = this.adaptor.lastChild(svg); this.adaptor.setAttribute(glyph, 'transform', 'scale(' + this.jax.fixed(s) + ', 1)'); this.adaptor.append(this.element, svg); } /** * @param {number} n The character number for the right glyph of the stretchy character * @param {number} W The width of the stretched character * @return {number} The width of the right glyph */ protected addRight(n: number, W: number) { if (!n) return 0; const [h, d, w] = this.getChar(n); return this.addGlyph(n, W - w, 0); } /** * @param {number} n The character number for the middle glyph of the stretchy character * @param {number} W The width of the stretched character * @return {number[]} The positions of the left and right edges of the middle glyph */ protected addMidH(n: number, W: number) { if (!n) return [0, 0]; const [h, d, w] = this.getChar(n); this.addGlyph(n, (W - w) / 2, 0); return [(W - w) / 2, (W + w) / 2]; } }