feat(2d): add LaTeX component (#228)

This commit is contained in:
Hunter Henrichsen
2023-02-10 08:18:17 -07:00
committed by GitHub
parent b8708732af
commit 4c26d2aaf0
11 changed files with 269 additions and 3 deletions

122
package-lock.json generated
View File

@@ -10032,6 +10032,14 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/esm": {
"version": "3.2.25",
"resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz",
"integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==",
"engines": {
"node": ">=6"
}
},
"node_modules/espree": {
"version": "9.3.3",
"devOptional": true,
@@ -13952,6 +13960,17 @@
"node": ">= 12"
}
},
"node_modules/mathjax-full": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/mathjax-full/-/mathjax-full-3.2.2.tgz",
"integrity": "sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==",
"dependencies": {
"esm": "^3.2.25",
"mhchemparser": "^4.1.0",
"mj-context-menu": "^0.6.1",
"speech-rule-engine": "^4.0.6"
}
},
"node_modules/mdast-squeeze-paragraphs": {
"version": "4.0.0",
"license": "MIT",
@@ -14228,6 +14247,11 @@
"node": ">= 0.6"
}
},
"node_modules/mhchemparser": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/mhchemparser/-/mhchemparser-4.1.1.tgz",
"integrity": "sha512-R75CUN6O6e1t8bgailrF1qPq+HhVeFTM3XQ0uzI+mXTybmphy3b6h4NbLOYhemViQ3lUs+6CKRkC3Ws1TlYREA=="
},
"node_modules/micromatch": {
"version": "4.0.5",
"license": "MIT",
@@ -14457,6 +14481,11 @@
"node": ">= 8"
}
},
"node_modules/mj-context-menu": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.6.1.tgz",
"integrity": "sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA=="
},
"node_modules/mkdirp": {
"version": "1.0.4",
"dev": true,
@@ -18577,6 +18606,27 @@
"wbuf": "^1.7.3"
}
},
"node_modules/speech-rule-engine": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-4.0.7.tgz",
"integrity": "sha512-sJrL3/wHzNwJRLBdf6CjJWIlxC04iYKkyXvYSVsWVOiC2DSkHmxsqOhEeMsBA9XK+CHuNcsdkbFDnoUfAsmp9g==",
"dependencies": {
"commander": "9.2.0",
"wicked-good-xpath": "1.3.0",
"xmldom-sre": "0.1.31"
},
"bin": {
"sre": "bin/sre"
}
},
"node_modules/speech-rule-engine/node_modules/commander": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-9.2.0.tgz",
"integrity": "sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w==",
"engines": {
"node": "^12.20.0 || >=14"
}
},
"node_modules/split": {
"version": "1.0.1",
"dev": true,
@@ -20486,6 +20536,11 @@
"node": ">= 8"
}
},
"node_modules/wicked-good-xpath": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz",
"integrity": "sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw=="
},
"node_modules/wide-align": {
"version": "1.1.5",
"dev": true,
@@ -20777,6 +20832,14 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/xmldom-sre": {
"version": "0.1.31",
"resolved": "https://registry.npmjs.org/xmldom-sre/-/xmldom-sre-0.1.31.tgz",
"integrity": "sha512-f9s+fUkX04BxQf+7mMWAp5zk61pciie+fFLC9hX9UVvCeJQfNHRHXpeo5MPcR0EUf57PYLdt+ZO4f3Ipk2oZUw==",
"engines": {
"node": ">=0.1"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"license": "MIT",
@@ -20873,7 +20936,8 @@
"license": "MIT",
"dependencies": {
"@motion-canvas/core": "^2.2.0",
"code-fns": "^0.8.2"
"code-fns": "^0.8.2",
"mathjax-full": "^3.2.2"
},
"devDependencies": {
"@motion-canvas/internal": "0.0.0"
@@ -24117,7 +24181,8 @@
"requires": {
"@motion-canvas/core": "^2.2.0",
"@motion-canvas/internal": "0.0.0",
"code-fns": "^0.8.2"
"code-fns": "^0.8.2",
"mathjax-full": "*"
}
},
"@motion-canvas/core": {
@@ -27597,6 +27662,11 @@
"version": "3.3.0",
"devOptional": true
},
"esm": {
"version": "3.2.25",
"resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz",
"integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA=="
},
"espree": {
"version": "9.3.3",
"devOptional": true,
@@ -30059,6 +30129,17 @@
"version": "4.2.12",
"dev": true
},
"mathjax-full": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/mathjax-full/-/mathjax-full-3.2.2.tgz",
"integrity": "sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==",
"requires": {
"esm": "^3.2.25",
"mhchemparser": "^4.1.0",
"mj-context-menu": "^0.6.1",
"speech-rule-engine": "^4.0.6"
}
},
"mdast-squeeze-paragraphs": {
"version": "4.0.0",
"requires": {
@@ -30239,6 +30320,11 @@
"methods": {
"version": "1.1.2"
},
"mhchemparser": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/mhchemparser/-/mhchemparser-4.1.1.tgz",
"integrity": "sha512-R75CUN6O6e1t8bgailrF1qPq+HhVeFTM3XQ0uzI+mXTybmphy3b6h4NbLOYhemViQ3lUs+6CKRkC3Ws1TlYREA=="
},
"micromatch": {
"version": "4.0.5",
"requires": {
@@ -30374,6 +30460,11 @@
"yallist": "^4.0.0"
}
},
"mj-context-menu": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.6.1.tgz",
"integrity": "sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA=="
},
"mkdirp": {
"version": "1.0.4",
"dev": true
@@ -32929,6 +33020,23 @@
"wbuf": "^1.7.3"
}
},
"speech-rule-engine": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-4.0.7.tgz",
"integrity": "sha512-sJrL3/wHzNwJRLBdf6CjJWIlxC04iYKkyXvYSVsWVOiC2DSkHmxsqOhEeMsBA9XK+CHuNcsdkbFDnoUfAsmp9g==",
"requires": {
"commander": "9.2.0",
"wicked-good-xpath": "1.3.0",
"xmldom-sre": "0.1.31"
},
"dependencies": {
"commander": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-9.2.0.tgz",
"integrity": "sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w=="
}
}
},
"split": {
"version": "1.0.1",
"dev": true,
@@ -34014,6 +34122,11 @@
"isexe": "^2.0.0"
}
},
"wicked-good-xpath": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz",
"integrity": "sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw=="
},
"wide-align": {
"version": "1.1.5",
"dev": true,
@@ -34190,6 +34303,11 @@
"version": "2.2.0",
"devOptional": true
},
"xmldom-sre": {
"version": "0.1.31",
"resolved": "https://registry.npmjs.org/xmldom-sre/-/xmldom-sre-0.1.31.tgz",
"integrity": "sha512-f9s+fUkX04BxQf+7mMWAp5zk61pciie+fFLC9hX9UVvCeJQfNHRHXpeo5MPcR0EUf57PYLdt+ZO4f3Ipk2oZUw=="
},
"xtend": {
"version": "4.0.2"
},

View File

@@ -27,6 +27,7 @@
},
"dependencies": {
"@motion-canvas/core": "^2.2.0",
"code-fns": "^0.8.2"
"code-fns": "^0.8.2",
"mathjax-full": "^3.2.2"
}
}

View File

@@ -0,0 +1,84 @@
import {mathjax} from 'mathjax-full/js/mathjax';
import {TeX} from 'mathjax-full/js/input/tex';
import {SVG} from 'mathjax-full/js/output/svg';
import {AllPackages} from 'mathjax-full/js/input/tex/AllPackages';
import {liteAdaptor} from 'mathjax-full/js/adaptors/liteAdaptor';
import {RegisterHTMLHandler} from 'mathjax-full/js/handlers/html';
import {initial, signal} from '../decorators';
import {Image, ImageProps} from './Image';
import {
DependencyContext,
SignalValue,
SimpleSignal,
} from '@motion-canvas/core/lib/signals';
import {OptionList} from 'mathjax-full/js/util/Options';
import {useLogger} from '@motion-canvas/core/lib/utils';
const adaptor = liteAdaptor();
RegisterHTMLHandler(adaptor);
const jaxDocument = mathjax.document('', {
InputJax: new TeX({packages: AllPackages}),
OutputJax: new SVG({fontCache: 'local'}),
});
export interface LatexProps extends ImageProps {
tex?: SignalValue<string>;
renderProps?: SignalValue<OptionList>;
}
export class Latex extends Image {
private static svgContentsPool: Record<string, string> = {};
private readonly imageElement = document.createElement('img');
@initial({})
@signal()
public declare readonly options: SimpleSignal<OptionList, this>;
@signal()
public declare readonly tex: SimpleSignal<string, this>;
public constructor(props: LatexProps) {
super(props);
}
protected override image(): HTMLImageElement {
// Render props may change the look of the TeX, so we need to cache both
// source and render props together.
const src = `${this.tex()}::${JSON.stringify(this.options())}`;
if (Latex.svgContentsPool[src]) {
this.imageElement.src = Latex.svgContentsPool[src];
return this.imageElement;
}
// Convert to TeX, look for any errors
const tex = this.tex();
const svg = adaptor.innerHTML(jaxDocument.convert(tex, this.options()));
if (svg.includes('data-mjx-error')) {
const errors = svg.match(/data-mjx-error="(.*?)"/);
if (errors && errors.length > 0) {
useLogger().error(`Invalid MathJax: ${errors[1]}`);
}
}
// Encode to raw base64 image format
const text = `data:image/svg+xml;base64,${btoa(
`<?xml version="1.0" encoding="UTF-8" standalone="no" ?>\n${svg}`,
)}`;
Latex.svgContentsPool[src] = text;
const image = document.createElement('img');
image.src = text;
image.src = text;
if (!image.complete) {
DependencyContext.collectPromise(
new Promise((resolve, reject) => {
image.addEventListener('load', resolve);
image.addEventListener('error', reject);
}),
);
}
return image;
}
}

View File

@@ -1,6 +1,7 @@
export * from './Circle';
export * from './Image';
export * from './Grid';
export * from './Latex';
export * from './Layout';
export * from './Line';
export * from './Node';

View File

@@ -14,6 +14,7 @@
"experimentalDecorators": true,
"jsx": "react-jsx",
"jsxImportSource": "@motion-canvas/2d/lib",
"skipLibCheck": true,
"paths": {
"@motion-canvas/2d/lib/jsx-runtime": ["jsx-runtime.ts"]
},

View File

@@ -0,0 +1,24 @@
---
sidebar_position: 10
slug: /nodes
---
import AnimationPlayer from '@site/src/components/AnimationPlayer';
import CodeBlock from '@theme/CodeBlock';
import texSource from '!!raw-loader!@motion-canvas/examples/src/scenes/tex';
# Nodes
## LaTeX
<CodeBlock language="tsx">{texSource}</CodeBlock>
<AnimationPlayer small name="tex" />
### A note on tweening LaTeX
Because we use a canvas renderer, we're rendering the LaTeX to an image instead
using a full graphics backend to render the TeX. This means that for all
intents and purposes, LaTeX should be treated as if it were an image rather
than more flexible TeX rendering like you may see in other software.
In other words, opacity, position, size, etc. should be fine to tween, but we
do not recommend tweening the `tex` signal.

View File

@@ -0,0 +1,3 @@
{
"version": 0
}

View File

@@ -0,0 +1,21 @@
import {Latex} from '@motion-canvas/2d/lib/components';
import {makeScene2D} from '@motion-canvas/2d/lib/scenes';
import {waitFor} from '@motion-canvas/core/lib/flow';
import {createRef} from '@motion-canvas/core/lib/utils';
export default makeScene2D(function* (view) {
const tex = createRef<Latex>();
view.add(
<Latex
ref={tex}
tex="{\color{white} x = \sin \left( \frac{\pi}{2} \right)}"
y={0}
width={400} // height and width can calculate based on each other
/>,
);
yield* waitFor(2);
yield* tex().opacity(0, 1);
yield* waitFor(2);
yield* tex().opacity(1, 1);
});

View File

@@ -0,0 +1,3 @@
{
"version": 0
}

View File

@@ -0,0 +1,9 @@
import {makeProject} from '@motion-canvas/core';
import scene from './scenes/tex?scene';
import {Vector2} from '@motion-canvas/core/lib/types';
export default makeProject({
scenes: [scene],
size: new Vector2(960, 540),
});

View File

@@ -6,6 +6,7 @@ export default defineConfig({
motionCanvas({
project: [
'./src/quickstart.ts',
'./src/tex.ts',
'./src/tweening-linear.ts',
'./src/tweening-cubic.ts',
'./src/tweening-color.ts',