mo.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. /*************************************************************
  2. *
  3. * Copyright (c) 2018-2022 The MathJax Consortium
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. /**
  18. * @fileoverview Implements the SVGmo wrapper for the MmlMo object
  19. *
  20. * @author dpvc@mathjax.org (Davide Cervone)
  21. */
  22. import {SVGWrapper, SVGConstructor} from '../Wrapper.js';
  23. import {CommonMoMixin} from '../../common/Wrappers/mo.js';
  24. import {MmlMo} from '../../../core/MmlTree/MmlNodes/mo.js';
  25. import {BBox} from '../../../util/BBox.js';
  26. import {DIRECTION, SVGCharData, SVGCharOptions} from '../FontData.js';
  27. /*****************************************************************/
  28. const VFUZZ = 0.1; // overlap for vertical stretchy glyphs
  29. const HFUZZ = 0.1; // overlap for horizontal stretchy glyphs
  30. /*****************************************************************/
  31. /**
  32. * The SVGmo wrapper for the MmlMo object
  33. *
  34. * @template N The HTMLElement node class
  35. * @template T The Text node class
  36. * @template D The Document class
  37. */
  38. // @ts-ignore
  39. export class SVGmo<N, T, D> extends
  40. CommonMoMixin<SVGConstructor<any, any, any>>(SVGWrapper) {
  41. /**
  42. * The mo wrapper
  43. */
  44. public static kind = MmlMo.prototype.kind;
  45. /**
  46. * @override
  47. */
  48. public toSVG(parent: N) {
  49. const attributes = this.node.attributes;
  50. const symmetric = (attributes.get('symmetric') as boolean) && this.stretch.dir !== DIRECTION.Horizontal;
  51. const stretchy = this.stretch.dir !== DIRECTION.None;
  52. if (stretchy && this.size === null) {
  53. this.getStretchedVariant([]);
  54. }
  55. let svg = this.standardSVGnode(parent);
  56. if (stretchy && this.size < 0) {
  57. this.stretchSVG();
  58. } else {
  59. const u = (symmetric || attributes.get('largeop') ? this.fixed(this.getCenterOffset()) : '0');
  60. const v = (this.node.getProperty('mathaccent') ? this.fixed(this.getAccentOffset()) : '0');
  61. if (u !== '0' || v !== '0') {
  62. this.adaptor.setAttribute(svg, 'transform', `translate(${v} ${u})`);
  63. }
  64. this.addChildren(svg);
  65. }
  66. }
  67. /**
  68. * Create the SVG for a multi-character stretchy delimiter
  69. */
  70. protected stretchSVG() {
  71. const stretch = this.stretch.stretch;
  72. const variants = this.getStretchVariants();
  73. const bbox = this.getBBox();
  74. if (this.stretch.dir === DIRECTION.Vertical) {
  75. this.stretchVertical(stretch, variants, bbox);
  76. } else {
  77. this.stretchHorizontal(stretch, variants, bbox);
  78. }
  79. }
  80. /**
  81. * Get the variant array for the assembly pieces
  82. */
  83. protected getStretchVariants() {
  84. const c = this.stretch.c || this.getText().codePointAt(0);
  85. const variants = [] as string[];
  86. for (const i of this.stretch.stretch.keys()) {
  87. variants[i] = this.font.getStretchVariant(c, i);
  88. }
  89. return variants;
  90. }
  91. /**
  92. * @param {number[]} stretch The characters to use for stretching
  93. * @param {string[]} variant The variants for the parts to use for stretching
  94. * @param {BBox} bbox The full size of the stretched character
  95. */
  96. protected stretchVertical(stretch: number[], variant: string[], bbox: BBox) {
  97. const {h, d, w} = bbox;
  98. const T = this.addTop(stretch[0], variant[0], h, w);
  99. const B = this.addBot(stretch[2], variant[2], d, w);
  100. if (stretch.length === 4) {
  101. const [H, D] = this.addMidV(stretch[3], variant[3], w);
  102. this.addExtV(stretch[1], variant[1], h, 0, T, H, w);
  103. this.addExtV(stretch[1], variant[1], 0, d, D, B, w);
  104. } else {
  105. this.addExtV(stretch[1], variant[1], h, d, T, B, w);
  106. }
  107. }
  108. /**
  109. * @param {number[]} stretch The characters to use for stretching
  110. * @param {string[]} variant The variants for the parts to use for stretching
  111. * @param {BBox} bbox The full size of the stretched character
  112. */
  113. protected stretchHorizontal(stretch: number[], variant: string[], bbox: BBox) {
  114. const w = bbox.w;
  115. const L = this.addLeft(stretch[0], variant[0]);
  116. const R = this.addRight(stretch[2], variant[2], w);
  117. if (stretch.length === 4) {
  118. const [x1, x2] = this.addMidH(stretch[3], variant[3], w);
  119. const w2 = w / 2;
  120. this.addExtH(stretch[1], variant[1], w2, L, w2 - x1);
  121. this.addExtH(stretch[1], variant[1], w2, x2 - w2, R, w2);
  122. } else {
  123. this.addExtH(stretch[1], variant[1], w, L, R);
  124. }
  125. }
  126. /***********************************************************/
  127. /**
  128. * @param {number} n The number of the character to look up
  129. * @param {string} variant The variant for the character to look up
  130. * @return {SVGCharData} The full CharData object, with CharOptions guaranteed to be defined
  131. */
  132. protected getChar(n: number, variant: string): SVGCharData {
  133. const char = this.font.getChar(variant, n) || [0, 0, 0, null];
  134. return [char[0], char[1], char[2], char[3] || {}] as [number, number, number, SVGCharOptions];
  135. }
  136. /**
  137. * @param {number} n The character code for the glyph
  138. * @param {string} variant The variant for the glyph
  139. * @param {number} x The x position of the glyph
  140. * @param {number} y The y position of the glyph
  141. * @param {N} parent The container for the glyph
  142. * @return {number} The width of the character placed
  143. */
  144. protected addGlyph(n: number, variant: string, x: number, y: number, parent: N = null): number {
  145. return this.placeChar(n, x, y, parent || this.element, variant);
  146. }
  147. /***********************************************************/
  148. /**
  149. * @param {number} n The character number for the top glyph
  150. * @param {string} v The variant for the top glyph
  151. * @param {number} H The height of the stretched delimiter
  152. * @param {number} W The width of the stretched delimiter
  153. * @return {number} The total height of the top glyph
  154. */
  155. protected addTop(n: number, v: string, H: number, W: number): number {
  156. if (!n) return 0;
  157. const [h, d, w] = this.getChar(n, v);
  158. this.addGlyph(n, v, (W - w) / 2, H - h);
  159. return h + d;
  160. }
  161. /**
  162. * @param {number} n The character number for the extender glyph
  163. * @param {string} v The variant for the extender glyph
  164. * @param {number} H The height of the stretched delimiter
  165. * @param {number} D The depth of the stretched delimiter
  166. * @param {number} T The height of the top glyph in the delimiter
  167. * @param {number} B The height of the bottom glyph in the delimiter
  168. * @param {number} W The width of the stretched delimiter
  169. */
  170. protected addExtV(n: number, v: string, H: number, D: number, T: number, B: number, W: number) {
  171. if (!n) return;
  172. T = Math.max(0, T - VFUZZ); // A little overlap on top
  173. B = Math.max(0, B - VFUZZ); // A little overlap on bottom
  174. const adaptor = this.adaptor;
  175. const [h, d, w] = this.getChar(n, v);
  176. const Y = H + D - T - B; // The height of the extender
  177. const s = 1.5 * Y / (h + d); // Scale height by 1.5 to avoid bad ends
  178. // (glyphs with rounded or anti-aliased ends don't stretch well,
  179. // so this makes for sharper ends)
  180. const y = (s * (h - d) - Y) / 2; // The bottom point to clip the extender
  181. if (Y <= 0) return;
  182. const svg = this.svg('svg', {
  183. width: this.fixed(w), height: this.fixed(Y),
  184. y: this.fixed(B - D), x: this.fixed((W - w) / 2),
  185. viewBox: [0, y, w, Y].map(x => this.fixed(x)).join(' ')
  186. });
  187. this.addGlyph(n, v, 0, 0, svg);
  188. const glyph = adaptor.lastChild(svg);
  189. adaptor.setAttribute(glyph, 'transform', `scale(1,${this.jax.fixed(s)})`);
  190. adaptor.append(this.element, svg);
  191. }
  192. /**
  193. * @param {number} n The character number for the bottom glyph
  194. * @param {string} v The variant for the bottom glyph
  195. * @param {number} D The depth of the stretched delimiter
  196. * @param {number} W The width of the stretched delimiter
  197. * @return {number} The total height of the bottom glyph
  198. */
  199. protected addBot(n: number, v: string, D: number, W: number): number {
  200. if (!n) return 0;
  201. const [h, d, w] = this.getChar(n, v);
  202. this.addGlyph(n, v, (W - w) / 2, d - D);
  203. return h + d;
  204. }
  205. /**
  206. * @param {number} n The character number for the middle glyph
  207. * @param {string} v The variant for the middle glyph
  208. * @param {number} W The width of the stretched delimiter
  209. * @return {[number, number]} The top and bottom positions of the middle glyph
  210. */
  211. protected addMidV(n: number, v: string, W: number): [number, number] {
  212. if (!n) return [0, 0];
  213. const [h, d, w] = this.getChar(n, v);
  214. const y = (d - h) / 2 + this.font.params.axis_height;
  215. this.addGlyph(n, v, (W - w) / 2, y);
  216. return [h + y, d - y];
  217. }
  218. /***********************************************************/
  219. /**
  220. * @param {number} n The character number for the left glyph of the stretchy character
  221. * @param {string} v The variant for the left glyph
  222. * @return {number} The width of the left glyph
  223. */
  224. protected addLeft(n: number, v: string): number {
  225. return (n ? this.addGlyph(n, v, 0, 0) : 0);
  226. }
  227. /**
  228. * @param {number} n The character number for the extender glyph of the stretchy character
  229. * @param {string} v The variant for the extender glyph
  230. * @param {number} W The width of the stretched character
  231. * @param {number} L The width of the left glyph of the stretchy character
  232. * @param {number} R The width of the right glyph of the stretchy character
  233. * @param {number} x The x-position of the extender (needed for ones with two extenders)
  234. */
  235. protected addExtH(n: number, v: string, W: number, L: number, R: number, x: number = 0) {
  236. if (!n) return;
  237. R = Math.max(0, R - HFUZZ); // A little less than the width of the right glyph
  238. L = Math.max(0, L - HFUZZ); // A little less than the width of the left glyph
  239. const adaptor = this.adaptor;
  240. const [h, d, w] = this.getChar(n, v);
  241. const X = W - L - R; // The width of the extender
  242. const Y = h + d + 2 * VFUZZ; // The height (plus some fuzz) of the extender
  243. const s = 1.5 * (X / w); // Scale the width so that left- and right-bearing won't hurt us
  244. const D = -(d + VFUZZ); // The bottom position of the glyph
  245. if (X <= 0) return;
  246. const svg = this.svg('svg', {
  247. width: this.fixed(X), height: this.fixed(Y),
  248. x: this.fixed(x + L), y: this.fixed(D),
  249. viewBox: [(s * w - X) / 2, D, X, Y].map(x => this.fixed(x)).join(' ')
  250. });
  251. this.addGlyph(n, v, 0, 0, svg);
  252. const glyph = adaptor.lastChild(svg);
  253. adaptor.setAttribute(glyph, 'transform', 'scale(' + this.jax.fixed(s) + ',1)');
  254. adaptor.append(this.element, svg);
  255. }
  256. /**
  257. * @param {number} n The character number for the right glyph of the stretchy character
  258. * @param {string} v The variant for the right glyph
  259. * @param {number} W The width of the stretched character
  260. * @return {number} The width of the right glyph
  261. */
  262. protected addRight(n: number, v: string, W: number): number {
  263. if (!n) return 0;
  264. const w = this.getChar(n, v)[2];
  265. return this.addGlyph(n, v, W - w, 0);
  266. }
  267. /**
  268. * @param {number} n The character number for the middle glyph of the stretchy character
  269. * @param {string} v The variant for the middle glyph
  270. * @param {number} W The width of the stretched character
  271. * @return {[number, number]} The positions of the left and right edges of the middle glyph
  272. */
  273. protected addMidH(n: number, v: string, W: number): [number, number] {
  274. if (!n) return [0, 0];
  275. const w = this.getChar(n, v)[2];
  276. this.addGlyph(n, v, (W - w) / 2, 0);
  277. return [(W - w) / 2, (W + w) / 2];
  278. }
  279. }