Improve \sqrt (#810)

* Improve \sqrt

Make \sqrt out of inline SVGs to ensure a perfect match at the junction between surd and viniculum.

* Tweak for kern clarity

* Fix lint error

* regenerate screenshot tests with sqrts

* Correct advance

Edit the SVG paths so that they have the correct left bearing and advance width values.

This will correct the spacing on the left side of each surd and it will also improve the placment of a root indice.

* Revise scriptstyle surds

In the `main` size, delimiters *do* scale with scriptstyle and scriptscriptstyle.

* update screenshot images containing sqrts
This commit is contained in:
Ron Kok
2017-08-22 18:39:15 -07:00
committed by Kevin Barabash
parent dd0c14ac01
commit e88256b397
17 changed files with 146 additions and 49 deletions

View File

@@ -1042,7 +1042,13 @@ groupTypes.sqrt = function(group, options) {
// First, we do the same steps as in overline to build the inner group // First, we do the same steps as in overline to build the inner group
// and line // and line
const inner = buildGroup(group.value.body, options.havingCrampedStyle()); let inner = buildGroup(group.value.body, options.havingCrampedStyle());
// Some groups can return document fragments. Handle those by wrapping
// them in a span.
if (inner instanceof domTree.documentFragment) {
inner = makeSpan([], [inner], options);
}
// Calculate the minimum size for the \surd delimiter // Calculate the minimum size for the \surd delimiter
const metrics = options.fontMetrics(); const metrics = options.fontMetrics();
@@ -1059,20 +1065,18 @@ groupTypes.sqrt = function(group, options) {
const minDelimiterHeight = (inner.height + inner.depth + const minDelimiterHeight = (inner.height + inner.depth +
lineClearance + theta) * options.sizeMultiplier; lineClearance + theta) * options.sizeMultiplier;
// Create a \surd delimiter of the required minimum size // Create a sqrt SVG of the required minimum size
const delimChar = delimiter.customSizedDelim("\\surd", minDelimiterHeight, const img = delimiter.customSizedDelim("\\surd", minDelimiterHeight,
false, options, group.mode); false, options, group.mode);
const delim = makeSpan(["sqrt-sign"], [delimChar], options);
// Calculate the actual line width. // Calculate the actual line width.
// This actually should depend on the chosen font -- e.g. \boldmath // This actually should depend on the chosen font -- e.g. \boldmath
// should use the thicker surd symbols from e.g. KaTeX_Main-Bold, and // should use the thicker surd symbols from e.g. KaTeX_Main-Bold, and
// have thicker rules. // have thicker rules.
const ruleWidth = options.fontMetrics().sqrtRuleThickness * const ruleWidth = options.fontMetrics().sqrtRuleThickness *
delimChar.delimSizeMultiplier; img.sizeMultiplier;
const line = makeLineSpan("sqrt-line", options, ruleWidth);
const delimDepth = (delim.height + delim.depth) - ruleWidth; const delimDepth = img.height - ruleWidth;
// Adjust the clearance based on the delimiter size // Adjust the clearance based on the delimiter size
if (delimDepth > inner.height + inner.depth + lineClearance) { if (delimDepth > inner.height + inner.depth + lineClearance) {
@@ -1080,12 +1084,8 @@ groupTypes.sqrt = function(group, options) {
(lineClearance + delimDepth - inner.height - inner.depth) / 2; (lineClearance + delimDepth - inner.height - inner.depth) / 2;
} }
// Shift the delimiter so that its top lines up with the top of the line // Shift the sqrt image
const delimShift = -(inner.height + lineClearance + ruleWidth) + const imgShift = img.height - inner.height - lineClearance - ruleWidth;
delim.height;
delim.style.top = delimShift + "em";
delim.height -= delimShift;
delim.depth += delimShift;
// We add a special case here, because even when `inner` is empty, we // We add a special case here, because even when `inner` is empty, we
// still get a line. So, we use a simple heuristic to decide if we // still get a line. So, we use a simple heuristic to decide if we
@@ -1096,16 +1096,20 @@ groupTypes.sqrt = function(group, options) {
if (inner.height === 0 && inner.depth === 0) { if (inner.height === 0 && inner.depth === 0) {
body = makeSpan(); body = makeSpan();
} else { } else {
inner.style.paddingLeft = img.surdWidth + "em";
// Overlay the image and the argument.
body = buildCommon.makeVList([ body = buildCommon.makeVList([
{type: "elem", elem: inner}, {type: "elem", elem: inner},
{type: "kern", size: lineClearance}, {type: "kern", size: -(inner.height + imgShift)},
{type: "elem", elem: line}, {type: "elem", elem: img},
{type: "kern", size: ruleWidth}, {type: "kern", size: ruleWidth},
], "firstBaseline", null, options); ], "firstBaseline", null, options);
body.children[0].children[0].classes.push("svg-align");
} }
if (!group.value.index) { if (!group.value.index) {
return makeSpan(["mord", "sqrt"], [delim, body], options); return makeSpan(["mord", "sqrt"], [body], options);
} else { } else {
// Handle the optional root index // Handle the optional root index
@@ -1113,13 +1117,9 @@ groupTypes.sqrt = function(group, options) {
const newOptions = options.havingStyle(Style.SCRIPTSCRIPT); const newOptions = options.havingStyle(Style.SCRIPTSCRIPT);
const rootm = buildGroup(group.value.index, newOptions, options); const rootm = buildGroup(group.value.index, newOptions, options);
// Figure out the height and depth of the inner part
const innerRootHeight = Math.max(delim.height, body.height);
const innerRootDepth = Math.max(delim.depth, body.depth);
// The amount the index is shifted by. This is taken from the TeX // The amount the index is shifted by. This is taken from the TeX
// source, in the definition of `\r@@t`. // source, in the definition of `\r@@t`.
const toShift = 0.6 * (innerRootHeight - innerRootDepth); const toShift = 0.6 * (body.height - body.depth);
// Build a VList with the superscript shifted up correctly // Build a VList with the superscript shifted up correctly
const rootVList = buildCommon.makeVList( const rootVList = buildCommon.makeVList(
@@ -1130,7 +1130,7 @@ groupTypes.sqrt = function(group, options) {
const rootVListWrap = makeSpan(["root"], [rootVList]); const rootVListWrap = makeSpan(["root"], [rootVList]);
return makeSpan(["mord", "sqrt"], return makeSpan(["mord", "sqrt"],
[rootVListWrap, delim, body], options); [rootVListWrap, body], options);
} }
}; };

View File

@@ -236,11 +236,6 @@ const makeStackedDelim = function(delim, heightTotal, center, options, mode,
bottom = "\u23a9"; bottom = "\u23a9";
repeat = "\u23aa"; repeat = "\u23aa";
font = "Size4-Regular"; font = "Size4-Regular";
} else if (delim === "\\surd") {
top = "\ue001";
bottom = "\u23b7";
repeat = "\ue000";
font = "Size4-Regular";
} }
// Get the metrics of the four sections // Get the metrics of the four sections
@@ -318,6 +313,112 @@ const makeStackedDelim = function(delim, heightTotal, center, options, mode,
Style.TEXT, options, classes); Style.TEXT, options, classes);
}; };
const sqrtInnerSVG = {
// The main path geometry is from glyph U221A in the font KaTeX Main
main: `<svg viewBox='0 0 400000 1000' preserveAspectRatio='xMinYMin
slice'><path fill='currentColor' d='M95 622c-2.667 0-7.167-2.667-13.5
-8S72 604 72 600c0-2 .333-3.333 1-4 1.333-2.667 23.833-20.667 67.5-54s
65.833-50.333 66.5-51c1.333-1.333 3-2 5-2 4.667 0 8.667 3.333 12 10l173
378c.667 0 35.333-71 104-213s137.5-285 206.5-429S812 17.333 812 14c5.333
-9.333 12-14 20-14h399166v40H845.272L620 507 385 993c-2.667 4.667-9 7-19
7-6 0-10-1-12-3L160 575l-65 47zM834 0h399166v40H845z'/></svg>`,
// size1 is from glyph U221A in the font KaTeX_Size1-Regular
1: `<svg viewBox='0 0 400000 1200' preserveAspectRatio='xMinYMin
slice'><path fill='currentColor' d='M263 601c.667 0 18 39.667 52 119s68.167
158.667 102.5 238 51.833 119.333 52.5 120C810 373.333 980.667 17.667 982 11
c4.667-7.333 11-11 19-11h398999v40H1012.333L741 607c-38.667 80.667-84 175-136
283s-89.167 185.333-111.5 232-33.833 70.333-34.5 71c-4.667 4.667-12.333 7-23
7l-12-1-109-253c-72.667-168-109.333-252-110-252-10.667 8-22 16.667-34 26-22
17.333-33.333 26-34 26l-26-26 76-59 76-60zM1001 0h398999v40H1012z'/></svg>`,
// size2 is from glyph U221A in the font KaTeX_Size2-Regular
2: `<svg viewBox='0 0 400000 1800' preserveAspectRatio='xMinYMin
slice'><path fill='currentColor' d='M1001 0h398999v40H1013.084S929.667 308 749
880s-277 876.333-289 913c-4.667 4.667-12.667 7-24 7h-12c-1.333-3.333-3.667
-11.667-7-25-35.333-125.333-106.667-373.333-214-744-10 12-21 25-33 39l-32 39
c-6-5.333-15-14-27-26l25-30c26.667-32.667 52-63 76-91l52-60 208 722c56-175.333
126.333-397.333 211-666s153.833-488.167 207.5-658.5C944.167 129.167 975 32.667
983 10c4-6.667 10-10 18-10zm0 0h398999v40H1013z'/></svg>`,
// size3 is from glyph U221A in the font KaTeX_Size3-Regular
3: `<svg viewBox='0 0 400000 2400' preserveAspectRatio='xMinYMin
slice'><path fill='currentColor' d='M424 2398c-1.333-.667-38.5-172-111.5-514
S202.667 1370.667 202 1370c0-2-10.667 14.333-32 49-4.667 7.333-9.833 15.667
-15.5 25s-9.833 16-12.5 20l-5 7c-4-3.333-8.333-7.667-13-13l-13-13 76-122 77-121
209 968c0-2 84.667-361.667 254-1079C896.333 373.667 981.667 13.333 983 10
c4-6.667 10-10 18-10h398999v40H1014.622S927.332 418.667 742 1206c-185.333
787.333-279.333 1182.333-282 1185-2 6-10 9-24 9-8 0-12-.667-12-2z
M1001 0h398999v40H1014z'/></svg>`,
// size4 is from glyph U221A in the font KaTeX_Size4-Regular
4: `<svg viewBox='0 0 400000 3000' preserveAspectRatio='xMinYMin
slice'><path fill='currentColor' d='M473 2713C812.333 913.667 982.333 13 983 11
c3.333-7.333 9.333-11 18-11h399110v40H1017.698S927.168 518 741.5 1506C555.833
2494 462 2989 460 2991c-2 6-10 9-24 9-8 0-12-.667-12-2s-5.333-32-16-92c-50.667
-293.333-119.667-693.333-207-1200 0-1.333-5.333 8.667-16 30l-32 64-16 33-26-26
76-153 77-151c.667.667 35.667 202 105 604 67.333 400.667 102 602.667 104 606z
M1001 0h398999v40H1017z'/></svg>`,
// tall is from glyph U23B7 in the font KaTeX_Size4-Regular
tall: `l-4 4-4 4c-.667.667-2 1.5-4 2.5s-4.167 1.833-6.5 2.5-5.5 1-9.5 1h
-12l-28-84c-16.667-52-96.667 -294.333-240-727l-212 -643 -85 170c-4-3.333-8.333
-7.667-13 -13l-13-13l77-155 77-156c66 199.333 139 419.667 219 661 l218 661z
M702 0H400000v40H742z'/></svg>`,
};
const sqrtSpan = function(height, delim, options) {
// Create a span containing an SVG image of a sqrt symbol.
const span = buildCommon.makeSpan([], [], options);
let sizeMultiplier = options.sizeMultiplier; // default
if (delim.type === "small") {
// Get an SVG that is derived from glyph U+221A in font KaTeX-Main.
const newOptions = options.havingBaseStyle(delim.style);
sizeMultiplier = newOptions.sizeMultiplier / options.sizeMultiplier;
span.height = 1 * sizeMultiplier;
span.style.height = span.height + "em";
span.surdWidth = 0.833 * sizeMultiplier; // from the font.
//In the font, the glyph is 1000 units tall. The font scale is 1:1000.
span.innerHTML = `<svg width='100%' height='${span.height}em'>
${sqrtInnerSVG['main']}</svg>`;
} else if (delim.type === "large") {
// These SVGs come from fonts: KaTeX_Size1, _Size2, etc.
// Get sqrt height from font data
span.height = sizeToMaxHeight[delim.size] / sizeMultiplier;
span.style.height = span.height + "em";
span.surdWidth = 1.0 / sizeMultiplier; // from the font
span.innerHTML = `<svg width="100%" height="${span.height}em">
${sqrtInnerSVG[delim.size]}</svg>`;
} else {
// Tall sqrt. In TeX, this would be stacked using multiple glyphs.
// We'll use a single SVG to accomplish the same thing.
span.height = height / sizeMultiplier;
span.style.height = span.height + "em";
span.surdWidth = 1.056 / sizeMultiplier;
const viewBoxHeight = Math.floor(span.height * 1000); // scale = 1:1000
const vertSegment = viewBoxHeight - 54;
// This \sqrt is customized in both height and width. We set the
// height now. Then CSS will stretch the image to the correct width.
// This SVG path comes from glyph U+23B7, font KaTeX_Size4-Regular.
span.innerHTML = `<svg width='100%' height='${span.height}em'>
<svg viewBox='0 0 400000 ${viewBoxHeight}'
preserveAspectRatio='xMinYMax slice'>
<path fill='currentColor' d='M702 0H400000v40H742v${vertSegment}
${sqrtInnerSVG['tall']}</svg>`;
}
span.sizeMultiplier = sizeMultiplier;
return span;
};
// There are three kinds of delimiters, delimiters that stack when they become // There are three kinds of delimiters, delimiters that stack when they become
// too large // too large
const stackLargeDelimiters = [ const stackLargeDelimiters = [
@@ -488,16 +589,23 @@ const makeCustomSizedDelim = function(delim, height, center, options, mode,
// Look through the sequence // Look through the sequence
const delimType = traverseSequence(delim, height, sequence, options); const delimType = traverseSequence(delim, height, sequence, options);
// Depending on the sequence element we decided on, call the appropriate if (delim === "\\surd") {
// function. // Get an SVG image for
if (delimType.type === "small") { return sqrtSpan(height, delimType, options);
return makeSmallDelim(delim, delimType.style, center, options, mode, } else {
classes); // Get the delimiter from font glyphs.
} else if (delimType.type === "large") { // Depending on the sequence element we decided on, call the
return makeLargeDelim(delim, delimType.size, center, options, mode, // appropriate function.
classes); if (delimType.type === "small") {
} else if (delimType.type === "stack") { return makeSmallDelim(delim, delimType.style, center, options,
return makeStackedDelim(delim, height, center, options, mode, classes); mode, classes);
} else if (delimType.type === "large") {
return makeLargeDelim(delim, delimType.size, center, options, mode,
classes);
} else if (delimType.type === "stack") {
return makeStackedDelim(delim, height, center, options, mode,
classes);
}
} }
}; };

View File

@@ -399,17 +399,6 @@
} }
.sqrt { .sqrt {
> .sqrt-sign {
position: relative;
}
.sqrt-line {
display: inline-block;
position: relative;
width: 100%;
border-bottom-style: solid;
}
> .root { > .root {
// These values are taken from the definition of `\r@@t`, // These values are taken from the definition of `\r@@t`,
// `\mkern 5mu` and `\mkern -10mu`. // `\mkern 5mu` and `\mkern -10mu`.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB