-
Notifications
You must be signed in to change notification settings - Fork 522
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
367 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,367 @@ | ||
/* | ||
@title: LaTeXWriter | ||
@author: Kosei Tsukamoto | ||
@snapshot: example1.png | ||
*/ | ||
|
||
/* | ||
This program parses a subset of latex to write math out with polylines | ||
- The latex parser was all written by me (Kosei) | ||
- It supports: | ||
- numbers | ||
- + - * / ^ | ||
- \sqrt{}, \int, \times, \cdot, \frac{}{}, | ||
- letters | ||
- Impactful and useful since it is something that I and some | ||
of my friends will actually use for practicing | ||
- Infinite possibilites! Technically complex (took 10+ hours, see code lol) and visually engaging | ||
- It is as efficient as I could make it (speed of drawing finish) | ||
- To use it, comment out the "random expression" section | ||
and input your own latex! Just use the | ||
renderLatex(yourLatexStringHere) function call! | ||
TODO | ||
- show example of ALL features | ||
- handle nested square roots by making outer ones bigger | ||
- add more commands (https://blog.writefull.com/the-100-most-frequent-latex-commands/) | ||
- center shorter one in fractions | ||
*/ | ||
|
||
|
||
const width = 125; | ||
const height = 125; | ||
|
||
setDocDimensions(width, height); | ||
|
||
|
||
|
||
// RANDOM EXPRESSION SECTION START (this code is for demo purposes only) | ||
|
||
// renderLatex(String.raw`b \int c \sqrt{b}f \times g \cdot e \frac{a}{b}c+-*/^dmmogus`); | ||
{ | ||
renderLatex(String.raw`\int x \sqrt{x^2 + 24} dx`, [10, 5]); | ||
renderLatex(String.raw`latex\,\, renderer | ||
infinite \,\, possibilities!`, [5, 115]); | ||
} | ||
{ | ||
// U SUB | ||
let pn = []; // random polynomial | ||
let pn2 = []; // polynomial for u sub! | ||
let pd = bt.randIntInRange(2, 4); | ||
for (let i = pd; i > 0; i--) { | ||
let n = bt.randIntInRange(1, 9); | ||
pn.push(n + 'x' + (i == 1 ? '' : '^' + i)); | ||
pn2.push(n * i + (i == 1 ? '' : 'x') + (i - 1 <= 1 ? '' : '^' + (i - 1))); | ||
} | ||
renderLatex(String.raw`u-sub | ||
\int (${pn2.join('+')}) | ||
\times\sqrt{${pn.join('+')}} \, dx`, [10, 88]); // random integral | ||
} | ||
{ | ||
// PARTIAL FRACTION DECOMP | ||
let pn1 = [bt.randIntInRange(1, 9) + 'x', bt.randIntInRange(1, 9)]; // random polynomial | ||
let pn2 = [bt.randIntInRange(1, 9) + 'x', bt.randIntInRange(1, 9)]; // polynomial for u sub! | ||
let pn3 = [bt.randIntInRange(1, 9)] | ||
renderLatex(String.raw`partial \, \, fractions | ||
\int \frac{${pn3.join('+')}}{(${pn1.join('+')})(${pn2.join('+')})} \, dx`, [10, 55]); // random integral | ||
} | ||
{ | ||
// INTEGRATION BY PARTS | ||
let pn = []; // random polynomial | ||
let pd = bt.randIntInRange(1, 3); | ||
for (let i = pd; i > 0; i--) { | ||
let n = bt.randIntInRange(1, 9); | ||
pn.push(n + 'x' + (i == 1 ? '' : '^' + i)); | ||
} | ||
renderLatex(String.raw`int by parts | ||
\int (${pn.join('+')})\cos (x) \, dx`, [10, 30]); // random integral | ||
} | ||
|
||
// RANDOM EXPRESSION SECTION END | ||
|
||
|
||
/* | ||
format: [ | ||
Node (CommandNode || TextNode) { | ||
type: 'command' || 'text', | ||
data: 'commandName' || 'textChar', | ||
body?(only if type=='command'): [Arg [Node {}, ...], ...], | ||
}, | ||
... | ||
] | ||
*/ | ||
function parseLatex(latex) { | ||
let parsed = []; | ||
for (let i = 0; i < latex.length; i++) { | ||
let s = latex[i]; | ||
if (s == '\\') { | ||
let o = { | ||
type: 'command', | ||
data: null, | ||
body: [], // array of argument arrays | ||
}; | ||
let startI = i + 1; | ||
while (i < latex.length) { | ||
i++; | ||
let s = latex[i]; | ||
if (('a' <= s && s <= 'z') || s == ',' || s == '.') { | ||
// o.data += s; | ||
} else { | ||
break; | ||
} | ||
} | ||
o.data = latex.substring(startI, i); | ||
|
||
if (o.data == 'sin' || o.data == 'cos') { | ||
// just convert to text | ||
o.type = 'text'; | ||
delete o.body; | ||
} else { | ||
|
||
while (i < latex.length) { | ||
let s = latex[i]; | ||
if (s != '{') { | ||
break; | ||
} | ||
let startI = i + 1; | ||
let nest = 1; | ||
while (i < latex.length) { | ||
i++; | ||
let s = latex[i]; | ||
if (s == '{') { | ||
nest++; | ||
} else if (s == '}') { | ||
nest--; | ||
if (nest == 0) { | ||
break; | ||
} | ||
} | ||
} | ||
o.body.push(parseLatex(latex.substring(startI, i))); | ||
i++; | ||
} | ||
} | ||
i--; | ||
|
||
parsed.push(o); | ||
} else if (s == ' ') { | ||
// whitespace, do nothing | ||
} else if (s == '^') { | ||
// exponent (command with optional brackets) | ||
i++; | ||
let startI = i; | ||
if (latex[i] == '{') { | ||
let nest = 1; | ||
while (i < latex.length) { | ||
i++; | ||
let s = latex[i]; | ||
if (s == '{') { | ||
nest++; | ||
} else if (s == '}') { | ||
nest--; | ||
if (nest == 0) { | ||
break; | ||
} | ||
} | ||
} | ||
} else { | ||
i++; | ||
} | ||
|
||
let o = { | ||
type: 'command', | ||
data: s, | ||
body: [parseLatex(latex.substring(startI, i))], // array of argument arrays | ||
}; | ||
|
||
parsed.push(o); | ||
i--; | ||
} else if (s == '\n') { | ||
let o = { | ||
type: 'command', | ||
data: s, | ||
body: null | ||
}; | ||
parsed.push(o); | ||
} else { | ||
let startI = i; | ||
while (i < latex.length) { | ||
i++; | ||
let s = latex[i]; | ||
if (s == '\\' || s == ' ' || s == '^' || s == '\n') { | ||
break; | ||
} | ||
} | ||
parsed.push({ | ||
type: 'text', | ||
data: latex.substring(startI, i), | ||
}); | ||
i--; | ||
} | ||
} | ||
return parsed; | ||
} | ||
|
||
function renderParsed(parsed, originalOrigin, scale, spacingV) { | ||
let origin = [...originalOrigin]; // clone origin since it will be mutated | ||
// console.log(origin) | ||
|
||
let spacing = spacingV[0]; | ||
let spacingY = spacingV[1]; | ||
|
||
const textSize = 2; // size of text only (still needs to be multiplied with scale) | ||
const spacingSize = spacing - textSize; // size of spacing only (still needs to be multiplied with scale) | ||
const textSizeY = 4; // size of text y (still needs to be multiplied with scale) | ||
|
||
const lines = []; | ||
for (let i = 0; i < parsed.length; i++) { | ||
let p = parsed[i]; | ||
if (p.type == 'command') { | ||
switch (p.data) { | ||
case 'times': { | ||
lines.push([ | ||
[origin[0] + (spacingSize) * scale, origin[1] + (spacingSize) * scale], | ||
[origin[0] + (spacingSize * 3 + textSize) * scale, origin[1] + (textSize + spacingSize * 3) * scale], | ||
], [ | ||
[origin[0] + (spacingSize) * scale, origin[1] + (textSize + spacingSize * 3) * scale], | ||
[origin[0] + (spacingSize * 3 + textSize) * scale, origin[1] + (spacingSize) * scale], | ||
]); | ||
origin[0] += (spacing + spacingSize * 4) * scale; | ||
break; | ||
} | ||
case 'cdot': { | ||
const t = new bt.Turtle(); | ||
let r = 0.25; | ||
for (let i = 0; i < 5; i++) { | ||
t.jump([origin[0] + (spacingSize * 1 + textSize / 2) * scale, origin[1] + (textSizeY / 2 - r) * scale]); | ||
t.arc(360, r * scale); | ||
r -= 0.05; | ||
} | ||
lines.push(...t.lines()); | ||
origin[0] += (spacing + spacingSize * 2) * scale; | ||
break; | ||
} | ||
case 'int': { | ||
const t = new bt.Turtle(); | ||
t.jump([origin[0], origin[1]]); | ||
t.up(); | ||
// t.arc(270, 0.25 * scale); | ||
t.right(90); | ||
t.down(); | ||
t.arc(180 - 15, 0.25 * scale); | ||
t.forward((textSizeY / Math.cos(Math.PI/180 * 15) + 2 * 0.25 * Math.sin(Math.PI/180 * 15)) * scale); | ||
t.arc(-(180 - 15), 0.25 * scale); | ||
lines.push(...t.lines()); | ||
origin[0] += (spacing - spacingSize) * scale; | ||
break; | ||
} | ||
case 'sqrt': { | ||
|
||
|
||
/* | ||
// DOESNT WORK FOR NESTED (ENLARGES OUTSIDE) | ||
let oldOriginX = origin[0]; | ||
origin[0] += 1.5 * scale; | ||
let newOrigin = renderParsed(p.body[0], origin, scale, spacing); | ||
const t = new bt.Turtle(); | ||
t.jump([oldOriginX, origin[1]]); | ||
t.up(); | ||
// t.arc(270, 0.25 * scale); | ||
t.right(90 - 15); | ||
t.down(); | ||
t.forward(0.25 * scale); | ||
t.left(180 - 15 * 2); | ||
t.forward((textSizeY / Math.cos(Math.PI/180 * 15) + 0.25 * 2) * scale); | ||
t.right(90 - 15); | ||
t.forward(newOrigin[0] - origin[0]); | ||
lines.push(...t.lines()); | ||
*/ | ||
|
||
// SHRINKS INSIDE | ||
let oldOriginX = origin[0]; | ||
origin[0] += 1.5 * scale; | ||
let newScale = scale / 1.1; | ||
let newOrigin = renderParsed(p.body[0], origin, newScale, spacingV); | ||
const t = new bt.Turtle(); | ||
t.jump([oldOriginX, origin[1]]); | ||
t.up(); | ||
// t.arc(270, 0.25 * scale); | ||
t.right(90 - 15); | ||
t.down(); | ||
t.forward(0.25 * scale); | ||
t.left(180 - 15 * 2); | ||
t.forward((textSizeY / Math.cos(Math.PI/180 * 15) + 0.25) * scale); | ||
t.right(90 - 15); | ||
t.forward(newOrigin[0] - origin[0]); | ||
lines.push(...t.lines()); | ||
|
||
origin = newOrigin; | ||
break; | ||
} | ||
case 'frac': { | ||
let newScale = scale / 2.1; | ||
let bottomOrigin = renderParsed(p.body[1], [origin[0] + spacingSize / 2 * scale, origin[1]], newScale, spacingV); | ||
let topOrigin = renderParsed(p.body[0], [origin[0] + spacingSize / 2 * scale, origin[1] + (textSizeY) * (scale / 2 + scale / 2 - scale / 2.1)], newScale, spacingV); | ||
const t = new bt.Turtle(); | ||
t.jump([origin[0], origin[1] + textSizeY / 2 * scale]); | ||
let newOrigin = [Math.max(bottomOrigin[0], topOrigin[0]) - (spacingSize * newScale) + spacingSize / 2 * scale, origin[1]]; | ||
t.forward(newOrigin[0] - origin[0]); | ||
lines.push(...t.lines()); | ||
|
||
newOrigin[0] += spacingSize * scale; | ||
origin = newOrigin; | ||
break; | ||
} | ||
case '^': { | ||
// old | ||
/* | ||
let newScale = scale / 2; | ||
let newOrigin = renderParsed(p.body[0], [origin[0], origin[1] + textSizeY * scale - (textSizeY / 2) * newScale], newScale, spacing); | ||
origin = [newOrigin[0] - spacingSize * newScale + spacingSize * scale, origin[1]]; | ||
*/ | ||
|
||
// keep consistent height for roots! | ||
let newScale = scale / 2; | ||
let newOrigin = renderParsed(p.body[0], [origin[0], origin[1] + textSizeY * scale - (textSizeY) * newScale], newScale, spacingV); | ||
|
||
origin = [newOrigin[0] - spacingSize * newScale + spacingSize * scale, origin[1]]; | ||
break; | ||
} | ||
case '\n': { | ||
origin[1] -= spacingY * scale; | ||
origin[0] = originalOrigin[0]; | ||
break; | ||
} | ||
case ',': { | ||
origin[0] += spacingSize * 2 * scale; | ||
break; | ||
} | ||
case '.': { | ||
origin[0] += spacingSize * scale; | ||
break; | ||
} | ||
default: { | ||
console.log(p.data + ' not implemented yet'); | ||
} | ||
} | ||
} else if (p.type == 'text') { | ||
let d = p.data; | ||
lines.push(...bt.text(d, origin, scale)) | ||
origin[0] += spacing * scale * d.length; | ||
} | ||
} | ||
|
||
// 3. draw | ||
drawLines(lines); | ||
|
||
return origin; | ||
} | ||
|
||
function renderLatex(latex, originalOrigin = [10, 10], scale = 2, spacing = [2.5, 4.5]) { | ||
// 1. parse latex | ||
let parsed = parseLatex(latex); | ||
|
||
// 2. draw | ||
return renderParsed(parsed, originalOrigin, scale, spacing); | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.