├── src ├── @development │ ├── media-url.ts │ ├── controls.tsx │ └── prompts.tsx ├── @production │ ├── media-url.ts │ ├── prompts.tsx │ └── controls.tsx ├── 5d │ ├── Elliptic.tsx │ └── Moduli.tsx ├── 3d │ ├── Sphere.tsx │ ├── Parametric.tsx │ ├── Arrow.tsx │ ├── HelpControl.tsx │ └── Cylinder.tsx ├── xyjax │ ├── AnimatedArrows.tsx │ └── Brouwer.tsx ├── index.tsx ├── KaTeX.tsx ├── XyJax.tsx ├── MathJax.tsx ├── FiveD.tsx ├── markers.ts ├── Intro.tsx ├── ThreeD.tsx └── TwoD.tsx ├── styl ├── ktx.styl ├── mjx.styl ├── lib │ ├── fat-fingers.styl │ ├── loading-screen.styl │ └── help-control.styl ├── xyjax.styl ├── intro.styl ├── 5d.styl ├── 3d.styl ├── style.styl ├── 2d.styl └── beamer.styl ├── .gitignore ├── lib ├── Link.tsx ├── LoadingScreen.tsx ├── seekonload.ts ├── remember-volume.ts ├── rebind-arrow-keys.ts ├── FatFingers.tsx ├── GlowOrb.tsx ├── ShowMarkerName.tsx ├── Block.tsx ├── Input.tsx ├── svg-extrude.ts ├── HelpControl.tsx ├── ThreeFiber.tsx └── graphics.ts ├── tsconfig.json ├── README.md ├── static ├── symbols.tex ├── mathjax-config.js ├── load-head.js ├── index.html ├── annotations.js └── style.css ├── package.json ├── .eslintrc.json ├── webpack.config.js ├── liqvid.config.ts └── pep@0.5.1.min.js /src/@development/media-url.ts: -------------------------------------------------------------------------------- 1 | export const MEDIA_URL = "."; 2 | -------------------------------------------------------------------------------- /styl/ktx.styl: -------------------------------------------------------------------------------- 1 | #sec-katex > .block > .content > p 2 | margin .5em 0 3 | 4 | -------------------------------------------------------------------------------- /src/@production/media-url.ts: -------------------------------------------------------------------------------- 1 | export const MEDIA_URL = "https://d2og9lpzrymesl.cloudfront.net/r/lv-tutorial-math"; 2 | -------------------------------------------------------------------------------- /styl/mjx.styl: -------------------------------------------------------------------------------- 1 | #sec-mathjax > .block > .content 2 | font-size .95em 3 | 4 | #sec-mathjax > .block > .content .MathJax 5 | font-size .8em !important 6 | -------------------------------------------------------------------------------- /styl/lib/fat-fingers.styl: -------------------------------------------------------------------------------- 1 | /* mobile dragging */ 2 | .fat-fingers 3 | fill transparent !important 4 | 5 | @media (any-hover: hover) 6 | .fat-fingers 7 | display none !important 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS files 2 | .DS_Store 3 | 4 | # Node 5 | node_modules 6 | 7 | # media 8 | *.mp4 9 | *.webm 10 | *.png 11 | *.svg 12 | 13 | # certificate 14 | /ssl 15 | 16 | # other 17 | dist 18 | -------------------------------------------------------------------------------- /lib/Link.tsx: -------------------------------------------------------------------------------- 1 | export default function Link(props: React.AnchorHTMLAttributes) { 2 | const {children, ...attrs} = props; 3 | return {children} 4 | } 5 | -------------------------------------------------------------------------------- /lib/LoadingScreen.tsx: -------------------------------------------------------------------------------- 1 | import {Player} from "liqvid"; 2 | 3 | /** 4 | Display a loading screen while Liqvid is not-ready. 5 | */ 6 | export function LoadingScreen() { 7 | return ( 8 |
9 |
10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /styl/xyjax.styl: -------------------------------------------------------------------------------- 1 | 2 | /* XyJax slide */ 3 | #xyjax-demos 4 | font-size 2em 5 | text-align center 6 | position relative 7 | top 1em 8 | 9 | > * 10 | left 0 11 | top 0 12 | position absolute !important 13 | text-align center 14 | width 100% 15 | 16 | .contradiction * 17 | color red 18 | fill red 19 | stroke red 20 | -------------------------------------------------------------------------------- /src/@production/prompts.tsx: -------------------------------------------------------------------------------- 1 | // production 2 | export const IntroPrompt = () => null; 3 | 4 | export const KaTeXPrompt = () => null; 5 | 6 | export const MathJaxPrompt = () => null; 7 | 8 | export const XyJaxPrompt = () => null; 9 | 10 | export const TwoDPrompt = () => null; 11 | 12 | export const ThreeDPrompt = () => null; 13 | 14 | export const FiveDPrompt = () => null; 15 | -------------------------------------------------------------------------------- /lib/seekonload.ts: -------------------------------------------------------------------------------- 1 | import type {Playback} from "liqvid"; 2 | import {Utils} from "liqvid"; 3 | const {timeRegexp} = Utils.time; 4 | 5 | const rgx = new RegExp( 6 | "(?:^\\?|&)t=(" + 7 | timeRegexp.toString().replace(/^\/\^|\$\/$/g, "") + 8 | ")" 9 | ); 10 | 11 | export default (playback: Playback) => { 12 | const $_ = parent.location.search.match(rgx); 13 | if ($_) { 14 | playback.seek($_[1]); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /src/@development/controls.tsx: -------------------------------------------------------------------------------- 1 | import {Player} from "liqvid"; 2 | import {RecordingControl} from "rp-recording"; 3 | import ThreeDControls from "../3d/HelpControl"; 4 | import {script} from "../markers"; 5 | 6 | const controls = [, ] 7 | 8 | export const UI: React.FC = (props) => { 9 | return ( 10 | 11 | {props.children} 12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "alwaysStrict": true, 5 | "incremental": true, 6 | "jsx": "react-jsx", 7 | "lib": ["es2015", "es2016", "es2017", "dom"], 8 | "moduleResolution": "node", 9 | "pretty": true, 10 | "removeComments": true, 11 | "target": "es2017", 12 | 13 | "baseUrl": ".", 14 | "paths": { 15 | "@lib/*": ["./lib/*"], 16 | "@env/*": ["./lib/*", "./src/@development/*", "./src/@production/*"] 17 | } 18 | }, 19 | "files": ["./src/index.tsx"] 20 | } 21 | -------------------------------------------------------------------------------- /lib/remember-volume.ts: -------------------------------------------------------------------------------- 1 | import type {Playback} from "liqvid"; 2 | 3 | export default (playback: Playback) => { 4 | const storage = window.localStorage; 5 | 6 | // restore volume settings 7 | playback.volume = parseFloat(storage.getItem("ractive volume") || "1"); 8 | playback.muted = "true" === (storage.getItem("ractive muted") || "false"); 9 | 10 | // save volume settings 11 | playback.on("volumechange", () => { 12 | storage.setItem("ractive muted", playback.muted.toString()); 13 | storage.setItem("ractive volume", playback.volume.toString()); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /lib/rebind-arrow-keys.ts: -------------------------------------------------------------------------------- 1 | import type {Player} from "liqvid"; 2 | 3 | /* 4 | By default ArrowLeft and ArrowRight perform seeking. This rebinds them so that 5 | a clicker can be used to navigate the slides. 6 | */ 7 | 8 | export default (player: Player) => { 9 | const {keymap, script} = player; 10 | 11 | for (const h of keymap.getHandlers("ArrowLeft")) 12 | keymap.unbind("ArrowLeft", h); 13 | for (const h of keymap.getHandlers("ArrowRight")) 14 | keymap.unbind("ArrowRight", h); 15 | 16 | keymap.bind("ArrowLeft", script.back); 17 | keymap.bind("ArrowRight", script.forward); 18 | } 19 | -------------------------------------------------------------------------------- /lib/FatFingers.tsx: -------------------------------------------------------------------------------- 1 | import {forwardRef} from "react"; 2 | 3 | type Props = React.SVGProps & { 4 | fatR?: string; 5 | fatRef?: React.Ref; 6 | }; 7 | 8 | export default forwardRef((props, ref) => { 9 | const {className, fatR, fatRef, ...attrs} = props; 10 | return ( 11 | <> 12 | 13 | 18 | 19 | ); 20 | }); 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This demo shows the various math features of [Liqvid](https://liqvidjs.org). 2 | 3 | # Installation 4 | 5 | Clone this repository, then do: 6 | 7 | ```bash 8 | npm install 9 | liqvid serve 10 | ``` 11 | 12 | # Commands 13 | 14 | For developing/recording: 15 | 16 | ```bash 17 | liqvid serve 18 | ``` 19 | 20 | Then visit http://localhost:3000/. 21 | 22 | For viewing the production bundle: 23 | ```bash 24 | NODE_ENV=production liqvid serve 25 | ``` 26 | 27 | Then visit http://localhost:3000/. 28 | 29 | To generate a production bundle: 30 | 31 | ```bash 32 | liqvid build 33 | ``` 34 | 35 | Then visit http://localhost:3000/dist/. -------------------------------------------------------------------------------- /styl/lib/loading-screen.styl: -------------------------------------------------------------------------------- 1 | /* loading */ 2 | .lv-loading-screen 3 | background-color rgba(0, 0, 0, 0.7) 4 | display none 5 | height 100% 6 | position absolute 7 | width 100% 8 | z-index 10000 9 | 10 | .lv-loading-spinner 11 | border 16px solid #f3f3f3 12 | border-top 16px solid $blue 13 | border-radius 50% 14 | margin 40vmin auto 0 auto 15 | width 20vmin 16 | height 20vmin 17 | animation spin 1s linear infinite 18 | 19 | @keyframes spin 20 | 0% 21 | transform rotate(0deg) 22 | 23 | 100% 24 | transform rotate(360deg) 25 | 26 | .lv-player.not-ready > .lv-canvas > .lv-loading-screen 27 | display block 28 | -------------------------------------------------------------------------------- /styl/intro.styl: -------------------------------------------------------------------------------- 1 | #sec-intro 2 | > .toc 3 | position absolute 4 | top 1.5em 5 | width 65% 6 | left 17.5% 7 | 8 | > section 9 | position absolute 10 | width 80% 11 | top 5em 12 | left 10% 13 | 14 | background #FFF 15 | border-radius .2em 16 | font-size 1.2em 17 | padding 1em 18 | height auto 19 | 20 | ul 21 | margin .5em 0 .5em 2em 22 | 23 | > li 24 | margin .5em 0 25 | 26 | > p:not(:first-child) 27 | margin 1em 0 28 | 29 | .time 30 | text-align right 31 | 32 | .toc :link 33 | color inherit 34 | 35 | .time > :link 36 | outline 1px solid $blue 37 | -------------------------------------------------------------------------------- /static/symbols.tex: -------------------------------------------------------------------------------- 1 | \newcommand{\Z}{\mathbb Z} 2 | 3 | \newcommand{\THH}{\operatorname{THH}} 4 | 5 | % categories 6 | \newcommand{\Top}{\mathrm{Top}} 7 | \Annotate[text]{\Top}{category of topological spaces} 8 | 9 | % diagrams 10 | \brutalnewcommand{\pushout}{\ar@{}[ul]|<<{\LARGE\ulcorner}} 11 | \brutalnewcommand{\pullback}{\ar@{}[dr]|<<{\LARGE\lrcorner}} 12 | 13 | \newcommand{\R}[1]{\mathbb R^{#1}} 14 | \Annotate[text]{\R}{the real numbers} 15 | \Annotate[defn]{\R}{http://your-site.org/lesson1/the-real-number-line} 16 | 17 | \newcommand{\space}[1]{#1} 18 | \Annotate[text]{\space}{\(#1\) is a topological space} 19 | \Annotate[pict]{\space}{http://your-site.org/img/space-example.jpg} -------------------------------------------------------------------------------- /static/mathjax-config.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | window.MathJax = { 3 | loader: { 4 | load: [ 5 | "[test]/annotations.js", "[custom]/brutalnewcommand.js", "[test]/load-head.js", 6 | "[tex]/color", "[tex]/html", 7 | "[test]/xypic.js" 8 | ], 9 | paths: { 10 | test: ".", 11 | custom: "https://cdn.jsdelivr.net/gh/ysulyma/mathjax-extensions@latest/", 12 | xyjax: "https://cdn.jsdelivr.net/gh/sonoisa/XyJax-v3@3.0.1/build/" 13 | } 14 | }, 15 | startup: { 16 | typeset: false // don't perform initial typeset 17 | }, 18 | tex: { 19 | packages: {"[+]": ["annotate", "brutalnewcommand", "color", "html", "load-head", "xypic"]} 20 | } 21 | }; 22 | 23 | })(); 24 | -------------------------------------------------------------------------------- /styl/5d.styl: -------------------------------------------------------------------------------- 1 | #elliptic 2 | background #CCC 3 | border-radius .2em 4 | width 35% 5 | height calc(0.35 * var(--rp-width)) 6 | position absolute 7 | left 5% 8 | top 5% 9 | 10 | line 11 | stroke #000 12 | 13 | #moduli 14 | background #CCC 15 | border-radius .2em 16 | width 35% 17 | height calc(0.35 * var(--rp-width)) 18 | position absolute 19 | right 5% 20 | top 5% 21 | 22 | #fived-controls 23 | background #CCC 24 | border-radius .2em 25 | bottom calc(var(--rp-controls-height) + 2.5%) 26 | font-size 1.5em 27 | padding .5em 28 | position absolute 29 | left calc((100% - 30%) / 2) 30 | width 30% 31 | 32 | > table 33 | margin 0 auto 34 | 35 | input[type="number"] 36 | font-size 1em 37 | width 6ch 38 | -------------------------------------------------------------------------------- /src/@production/controls.tsx: -------------------------------------------------------------------------------- 1 | import {Audio, Player} from "liqvid"; 2 | import ThreeDControls from "../3d/HelpControl"; 3 | import {highlights, script} from "../markers"; 4 | import {MEDIA_URL} from "./media-url"; 5 | 6 | export const controls = []; 7 | 8 | export const UI: React.FC = (props) => { 9 | const thumbs = { 10 | frequency: 1, 11 | path: `${MEDIA_URL}/thumbs/%s.png`, 12 | highlights 13 | }; 14 | 15 | return ( 16 | 17 | 21 | {props.children} 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /static/load-head.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | const scripts = 3 | Array.from(document.querySelectorAll("head > script[type='math/tex']")) 4 | .map(script => fetch(script.src).then(res => { 5 | // script.remove(); 6 | if (res.ok) return res.text(); 7 | throw new Error(`${res.status} ${res.statusText}: ${script.src}`); 8 | })); 9 | 10 | 11 | if (scripts.length > 0) { 12 | MathJax.startup.promise = Promise.all([MathJax.startup.promise, ...scripts]).then(([, ...results]) => { 13 | const div = document.createElement("div"); 14 | div.style.display = "none"; 15 | div.style.whiteSpace = "pre"; 16 | div.innerHTML = "\\[" + results.join("\n") + "\\]"; 17 | document.body.prepend(div); 18 | 19 | MathJax.typeset([div]); 20 | document.body.removeChild(div); 21 | }); 22 | } 23 | 24 | })(); 25 | -------------------------------------------------------------------------------- /lib/GlowOrb.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | interface Props extends React.SVGAttributes { 4 | dur: string; 5 | r1: number; 6 | r2: number; 7 | } 8 | 9 | export default function GlowOrb(props: Props) { 10 | const {r1, r2, dur, ...attrs} = props; 11 | 12 | return ( 13 | 19 | 27 | 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/5d/Elliptic.tsx: -------------------------------------------------------------------------------- 1 | import {marchingSquares} from "@lib/graphics"; 2 | 3 | export default function Elliptic(props: { 4 | a: number; 5 | b: number; 6 | }) { 7 | const edges = marchingSquares( 8 | -5, 5, -5, 5, 9 | (x, y) => y ** 2 - x ** 3 - props.a * x - props.b, 10 | 0, 11 | 128 12 | ) as [number, number, number][]; 13 | 14 | const path = []; 15 | for (let i = 0; i < edges.length; i += 2) { 16 | path.push(`M ${edges[i][0]} ${edges[i][1]}`); 17 | path.push(`L ${edges[i+1][0]} ${edges[i+1][1]}`); 18 | } 19 | const curve = path.join(" "); 20 | 21 | return ( 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /lib/ShowMarkerName.tsx: -------------------------------------------------------------------------------- 1 | import {usePlayer, Utils} from "liqvid"; 2 | import {useEffect} from "react"; 3 | 4 | const {anyHover} = Utils.mobile; 5 | const {useForceUpdate} = Utils.react; 6 | 7 | const style: React.CSSProperties = { 8 | backgroundColor: "#1A69B5", 9 | fontFamily: `"Roboto Slab", sans-serif`, 10 | lineHeight: "36px", 11 | padding: "0 .5em", 12 | userSelect: "all", 13 | verticalAlign: "top" 14 | }; 15 | 16 | export default function ShowMarkerName() { 17 | if (!anyHover) 18 | return null; 19 | const {script} = usePlayer(); 20 | const forceUpdate = useForceUpdate(); 21 | 22 | useEffect(() => { 23 | script.on("markerupdate", forceUpdate); 24 | 25 | return () => { 26 | script.off("markerupdate", forceUpdate); 27 | }; 28 | }, []); 29 | 30 | return ( 31 | 32 | {script.markerName} 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /lib/Block.tsx: -------------------------------------------------------------------------------- 1 | interface Props extends React.HTMLAttributes { 2 | blockTitle?: React.ReactChild; 3 | } 4 | 5 | const block = (className: string) => function(props: Props) { 6 | const {blockTitle, children, ...attrs} = props; 7 | return ( 8 | 16 | ); 17 | } 18 | 19 | export const Block = block(""); 20 | export const Corollary = block("corollary"); 21 | export const Definition = block("definition"); 22 | export const Example = block("example"); 23 | export const Exercise = block("exercise"); 24 | export const Lemma = block("lemma"); 25 | export const Remark = block("remark"); 26 | export const Proof = block("proof"); 27 | export const Proposition = block("proposition"); 28 | export const Theorem = block("theorem"); 29 | -------------------------------------------------------------------------------- /src/3d/Sphere.tsx: -------------------------------------------------------------------------------- 1 | import {useTime, Utils} from "liqvid"; 2 | import {useRef} from "react"; 3 | import * as THREE from "three"; 4 | import {DoubleSide, Mesh} from "three"; 5 | import {script} from "../markers"; 6 | 7 | const {animate, bezier, easings} = Utils.animation; 8 | 9 | const grow = animate({ 10 | duration: 1000, 11 | easing: bezier(...easings.easeInCubic), 12 | endValue: 3, 13 | startTime: script.parseStart("3d/anim"), 14 | }); 15 | 16 | export default function Sphere() { 17 | const ref = useRef(); 18 | 19 | useTime(r => { 20 | if (r === 0) { 21 | ref.current.visible = false; 22 | } else { 23 | ref.current.visible = script.markerName.startsWith("3d/"); 24 | ref.current.geometry = new THREE.SphereBufferGeometry(r, 64, 64); 25 | } 26 | }, grow, []); 27 | 28 | return ( 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/3d/Parametric.tsx: -------------------------------------------------------------------------------- 1 | import {useMarkerUpdate} from "liqvid"; 2 | import {useCallback, useRef} from "react"; 3 | // THREE 4 | import {DoubleSide, Mesh} from "three"; 5 | import {script} from "../markers"; 6 | 7 | const {cos, sin} = Math; 8 | const r = 2; 9 | const TWOPI = 2 * Math.PI; 10 | 11 | const index = script.markerNumberOf("3d/parametric"); 12 | 13 | export default function Parametric() { 14 | // trefoil knot parametrization 15 | const curve = useCallback((u, v, dest) => { 16 | u = -TWOPI/2 + u * 2 * TWOPI; 17 | v = -TWOPI/2 + v * 2 * TWOPI; 18 | const x = r * sin(3 * u) / (2 + cos(v)); 19 | const y = r * (sin(u) + 2 * sin(2 * u)) / (2 + cos(v + TWOPI / 3)); 20 | const z = r / 2 * (cos(u) - 2 * cos(2 * u)) * (2 + cos(v)) * (2 + cos(v + TWOPI / 3)) / 4; 21 | dest.set(x, y, z); 22 | return dest; 23 | }, []); 24 | 25 | // show/hide 26 | const ref = useRef(); 27 | 28 | useMarkerUpdate(() => { 29 | ref.current.visible = script.markerIndex >= index; 30 | }, []); 31 | 32 | return ( 33 | = index}> 34 | 35 | 36 | 37 | ); 38 | } -------------------------------------------------------------------------------- /styl/lib/help-control.styl: -------------------------------------------------------------------------------- 1 | // @media (any-hover: none) 2 | // .rp-controls-help 3 | // display none 4 | 5 | // more elegant would be to have this only happen during the 3d/ and 5d/ slides 6 | // but I don't have the energy for that right now 7 | .rp-controls 8 | --rp-controls-right 4 !important 9 | 10 | .rp-controls-help 11 | cursor pointer 12 | height 100% 13 | 14 | .rp-help-dialog 15 | background-color rgba(0, 0, 0, 0.85) 16 | color #FFF 17 | font-size 1.5rem 18 | height 100vh 19 | padding 5vh 10vh 20 | width 160vh 21 | position fixed 22 | left calc(50vw - 80vh) 23 | top 0 24 | z-index 1001 25 | 26 | > button 27 | background none 28 | border none 29 | color red 30 | cursor pointer 31 | font-family sans-serif 32 | font-size 5em 33 | position absolute 34 | right 2% 35 | top -1% 36 | 37 | .rp-help-tables 38 | display flex 39 | justify-content space-around 40 | 41 | > table 42 | border-collapse separate 43 | border-spacing 1em 1.5em 44 | font-family sans-serif 45 | vertical-align top 46 | 47 | > table > caption 48 | color $blue 49 | font-family sans-serif 50 | font-size 1.2em 51 | font-weight bold 52 | 53 | > table th 54 | font-weight normal 55 | text-align right 56 | 57 | kbd 58 | color yellow 59 | -------------------------------------------------------------------------------- /src/xyjax/AnimatedArrows.tsx: -------------------------------------------------------------------------------- 1 | import {MJX} from "@liqvid/mathjax"; 2 | import {tob52, useAnimateArrows} from "@liqvid/xyjax"; 3 | import {Utils} from "liqvid"; 4 | import {useRef} from "react"; 5 | import {script} from "../markers"; 6 | const {animate, bezier, easings} = Utils.animation; 7 | 8 | const fadeTail = animate({ 9 | startTime: script.parseStart("xyjx/arrows/anim"), 10 | duration: 800, 11 | easing: bezier(...easings.easeOutCubic) 12 | }); 13 | 14 | const headFade: KeyframeEffectOptions = { 15 | delay: script.parseStart("xyjx/arrows/anim") + 500, 16 | duration: 200, 17 | easing: "ease-out", 18 | fill: "both" 19 | }; 20 | 21 | const {raw} = String; 22 | 23 | export function AnimatedArrows() { 24 | const ref = useRef(); 25 | 26 | useAnimateArrows({ 27 | head: "*[data-anim] path", 28 | tail: "*[data-anim] line", 29 | label: ".fade", 30 | ref, 31 | tailFn: fadeTail, 32 | headFade, 33 | labelFade: headFade 34 | }); 35 | 36 | const line = "\"anim\":true"; 37 | 38 | return ( 39 | {raw` 40 | ${"\\"}xymatrix{ 41 | {*} \ar[d]_-0 \ar[r]^-{t_0} & \mathbb R \ar[d]^- p\\ 42 | I \ar[r]_-{\alpha} \ar@[data${tob52(line)}]@{..>}[ur]^-{\class{fade}{\exists!\ \widetilde\alpha}} & S^1 43 | } 44 | `} 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from "react-dom"; 2 | 3 | // slides 4 | import FiveD from "./FiveD"; 5 | import Intro from "./Intro"; 6 | import KaTeXSlide from "./KaTeX"; 7 | import MathJaxSlide from "./MathJax"; 8 | import ThreeD from "./ThreeD"; 9 | import TwoD from "./TwoD"; 10 | import XyJaxSlide from "./XyJax"; 11 | 12 | import {UI} from "@env/controls"; 13 | import {LoadingScreen} from "@lib/LoadingScreen"; 14 | 15 | function Lesson() { 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | } 29 | 30 | ReactDOM.render(, document.querySelector("main")); 31 | 32 | /* Typescript for Three/R3F */ 33 | import type {OrbitControls} from "three/examples/jsm/controls/OrbitControls"; 34 | import type {Object3DNode} from "@react-three/fiber"; 35 | import type {ParametricGeometry} from "three/examples/jsm/geometries/ParametricGeometry"; 36 | 37 | declare global { 38 | namespace JSX { 39 | interface IntrinsicElements { 40 | orbitControls: Object3DNode; 41 | parametricGeometry: Object3DNode; 42 | } 43 | } 44 | 45 | namespace THREE { 46 | const LineMaterial: any; 47 | const LineSegments2: any; 48 | const LineSegmentsGeometry: any; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/3d/Arrow.tsx: -------------------------------------------------------------------------------- 1 | import extrudeSvg from "@lib/svg-extrude"; 2 | import {useThree} from "@react-three/fiber"; 3 | import {useMarkerUpdate} from "liqvid"; 4 | import {useEffect, useRef} from "react"; 5 | import * as THREE from "three"; 6 | import {script} from "../markers"; 7 | import {MEDIA_URL} from "@env/media-url"; 8 | 9 | const arrowIndex = script.markerNumberOf("3d/svg"); 10 | 11 | export default function Arrow() { 12 | const {scene} = useThree(); 13 | const ref = useRef(); 14 | 15 | useEffect(() => { 16 | extrudeSvg(`${MEDIA_URL}/img/arrow.svg`).then(group => { 17 | group.rotation.set(Math.PI/2,0,0); 18 | const box = new THREE.Box3().setFromObject(group); 19 | const size = new THREE.Vector3(); 20 | box.getSize(size); 21 | group.position.set(-size.x/2,0,size.z); 22 | 23 | ref.current = group; 24 | group.visible = (script.markerIndex >= arrowIndex); 25 | scene.add(group); 26 | }); 27 | 28 | // extrudeSvg(`${MEDIA_URL}/img/i.svg`).then(group => { 29 | // group.rotation.set(Math.PI/2,0,0); 30 | // const box = new THREE.Box3().setFromObject(group); 31 | // const size = new THREE.Vector3(); 32 | // box.getSize(size); 33 | // group.position.set(-size.x/2 - 10,0,size.z) 34 | // scene.add(group); 35 | // }); 36 | }, []); 37 | 38 | useMarkerUpdate(() => { 39 | if (!ref.current) 40 | return; 41 | ref.current.visible = (script.markerIndex >= arrowIndex); 42 | }, []); 43 | 44 | return null; 45 | } -------------------------------------------------------------------------------- /lib/Input.tsx: -------------------------------------------------------------------------------- 1 | import {usePlayer, Utils} from "liqvid"; 2 | import RangeTouch from "rangetouch"; 3 | import {forwardRef, useCallback, useEffect, useMemo, useRef} from "react"; 4 | 5 | const {onClick} = Utils.mobile; 6 | const {combineRefs} = Utils.react; 7 | 8 | interface Props extends React.InputHTMLAttributes { 9 | onChange: (e: React.SyntheticEvent) => void; 10 | value: string; 11 | } 12 | 13 | const NON_TEXT_TYPES = ["button", "checkbox", "file", "hidden", "image", "radio", "range", "reset", "submit"]; 14 | 15 | export default forwardRef((props, ref) => { 16 | const player = usePlayer(); 17 | const innerRef = useRef(); 18 | const combinedRef = combineRefs(ref, innerRef); 19 | 20 | /* general shenanigans */ 21 | const onKeyPress = useCallback((e: React.KeyboardEvent) => { 22 | if (e.key === "Enter") { 23 | innerRef.current.blur(); 24 | } 25 | props.onKeyPress?.(e); 26 | }, []); 27 | 28 | /* mobile shenanigans */ 29 | const focus = useMemo( 30 | () => onClick((e) => { 31 | if (!NON_TEXT_TYPES.includes(props.type)) { 32 | e.currentTarget.focus(); 33 | } 34 | }), []); 35 | 36 | useEffect(() => { 37 | if (props.type === "range") 38 | new RangeTouch(innerRef.current); 39 | }, []); 40 | 41 | return ( 42 | 49 | ); 50 | }); 51 | -------------------------------------------------------------------------------- /styl/3d.styl: -------------------------------------------------------------------------------- 1 | #three-d 2 | background #333 3 | background-image url("https://d2og9lpzrymesl.cloudfront.net/r/rp-tutorial-math/img/grid.png") 4 | height 100% 5 | width 100% 6 | 7 | > figcaption 8 | background #0005 9 | border-radius .5rem .5rem 0 0 10 | font-size 2rem 11 | height 3rem 12 | color #FF7000 13 | vertical-align middle 14 | line-height 3rem 15 | position absolute 16 | text-align center 17 | top 0 18 | width 100% 19 | z-index 1000 20 | 21 | #three-explain 22 | background #FFFFFFE0 23 | width 100% 24 | font-size 1.5em 25 | padding .5em 26 | position absolute 27 | left 0 28 | top 0 29 | z-index 1000 30 | 31 | > p 32 | margin .5em 0 33 | 34 | .rp-3d-help-wrap 35 | position relative 36 | z-index 10000 37 | 38 | &:not(.relevant) 39 | opacity 0 40 | pointer-events none 41 | 42 | &.open 43 | background #3f3f3f 44 | 45 | &.open > .rp-3d-help 46 | display block 47 | 48 | .rp-3d-help 49 | background #404040 50 | border-radius .2em .2em 0 0 51 | box-shadow .9px -1.2px 2px #0002 52 | display none 53 | position absolute 54 | bottom calc(var(--rp-controls-height) - var(--rp-scrub-height)) 55 | font-size 1.2em 56 | right 0 57 | width 15em 58 | padding .5em 59 | z-index 1000 60 | 61 | > table 62 | border-collapse separate 63 | border-spacing 1em .5em 64 | margin 0 auto 65 | vertical-align top 66 | 67 | > caption 68 | color $green 69 | font-size 1.2em 70 | font-weight bold 71 | 72 | > tbody > tr > th 73 | color $tan 74 | text-align left 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lv-tutorial-math", 3 | "version": "2.0.0", 4 | "description": "Math-specific tutorial for Liqvid", 5 | "main": "server.js", 6 | "scripts": { 7 | "css": "stylus -w styl/style.styl -o static/style.css", 8 | "start": "liqvid serve", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "Yuri Sulyma ", 12 | "license": "MIT", 13 | "dependencies": { 14 | "@juggle/resize-observer": "^3.4.0", 15 | "@liqvid/cli": "^1.0.5", 16 | "@liqvid/katex": "^0.1.0", 17 | "@liqvid/mathjax": "^0.1.2", 18 | "@liqvid/prompt": "^1.0.0", 19 | "@liqvid/react-three": "^2.0.0", 20 | "@liqvid/xyjax": "^0.0.2", 21 | "@react-three/fiber": "7.*", 22 | "@types/katex": "^0.14.0", 23 | "@types/node": "^18.7.9", 24 | "@types/react": "^18.0.17", 25 | "@types/react-dom": "^18.0.6", 26 | "@types/three": "^0.143.1", 27 | "@typescript-eslint/eslint-plugin": "^5.33.1", 28 | "@typescript-eslint/parser": "^5.33.1", 29 | "eslint": "^8.22.0", 30 | "eslint-plugin-react": "^7.30.1", 31 | "eslint-plugin-react-hooks": "^4.6.0", 32 | "express": "^4.18.1", 33 | "liqvid": "^2.1.7", 34 | "pepjs": "^0.5.3", 35 | "rangetouch": "^2.0.1", 36 | "react": "^18.2.0", 37 | "react-dom": "^18.2.0", 38 | "rp-recording": "^2.3.0", 39 | "stylus": "^0.59.0", 40 | "stylus-loader": "^7.0.0", 41 | "three": "^0.143.0", 42 | "ts-loader": "^9.3.1", 43 | "ts-node": "^10.9.1", 44 | "typescript": "^4.7.4", 45 | "webpack": "^5.74.0", 46 | "webpack-cli": "^4.10.0", 47 | "zustand": "^4.1.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/KaTeX.tsx: -------------------------------------------------------------------------------- 1 | import {KaTeXPrompt} from "@env/prompts"; 2 | import {Definition, Example} from "@lib/Block"; 3 | import Link from "@lib/Link"; 4 | import {KTX} from "@liqvid/katex"; 5 | import {Utils} from "liqvid"; 6 | 7 | const {from} = Utils.authoring; 8 | const {raw} = String; 9 | 10 | export default function KaTeXSlide() { 11 | const m1 = raw`\htmlData{from-first=ktx/align-1}`; 12 | const m2 = raw`\htmlData{from-first=ktx/align-2}`; 13 | const m3 = raw`\htmlData{from-first=ktx/align-3}`; 14 | 15 | return ( 16 |
17 | 18 | The derivative of f at x, denoted f'(x), is 19 | {raw`f'(x) := \lim_{\epsilon\to0}\frac{f(x+\epsilon)-f(x)}{\epsilon}.`} 20 | 21 | 22 | The derivative of x^2} {...from("ktx/ex")}> 23 | {raw`\begin{aligned} 24 | \lim_{\epsilon\to0}\frac{(x+\epsilon)^2-x^2}{\epsilon} 25 | &${m1}{= \lim_{\epsilon\to0}\frac{(x^2 + 2\epsilon x + \epsilon^2)-x^2}{\epsilon}}\\[1em] 26 | &${m2}{= \lim_{\epsilon\to0}\Big(2x + \epsilon\Big)}\\[1em] 27 | &${m3}{= 2x.} 28 | \end{aligned}`} 29 | 30 | 31 | 32 |

Can load macros from file: {raw`\THH(R;\Z_p)`}

33 | 34 |

35 | KaTeX documentation 36 |

37 |
38 | 39 | 40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /styl/style.styl: -------------------------------------------------------------------------------- 1 | @import "https://unpkg.com/@liqvid/prompt@1.0.0/dist/style.min.css"; 2 | 3 | $blue = #1A69B5 4 | $purple = #AE81FF 5 | $wine = #AF1866 6 | $teal = #138684 7 | $green = #1BBB68 8 | $red = #A52117 9 | $orange = #FF7000 10 | $tan = #E6DB74 11 | 12 | @import "lib/fat-fingers" 13 | @import "lib/help-control" 14 | @import "lib/loading-screen" 15 | 16 | .lv-thumbnail-time 17 | color #222 18 | 19 | @import "beamer" 20 | @import "intro" 21 | @import "ktx" 22 | @import "mjx" 23 | @import "xyjax" 24 | @import "2d" 25 | @import "3d" 26 | @import "5d" 27 | 28 | h1 29 | font-family "KaTeX_Main" 30 | font-size 2em 31 | position absolute 32 | top .5em 33 | left 0 34 | text-align center 35 | width 100% 36 | 37 | h2 38 | color $blue 39 | font-family "Alegreya" 40 | font-size 4rem 41 | margin-top .5em 42 | text-align center 43 | 44 | .lv-canvas 45 | background #EEE 46 | 47 | body 48 | font-family sans-serif 49 | 50 | .draggable 51 | cursor grab 52 | cursor -webkit-grab 53 | 54 | body.dragging .rp-canvas, body.dragging .draggable, .dragging 55 | cursor grabbing 56 | cursor -webkit-grabbing 57 | 58 | section 59 | font-size 1.5rem 60 | height 100% 61 | left 0 62 | position absolute 63 | top 0 64 | width 100% 65 | 66 | .rp-prompt 67 | color #FFF 68 | font-size 1rem 69 | 70 | :link, :visited 71 | color $blue 72 | 73 | .rp-cursor 74 | width 3rem 75 | 76 | /* general styles */ 77 | .katex 78 | color inherit !important 79 | font-size 100% !important 80 | 81 | section 82 | color #000 83 | stroke #000 84 | fill none 85 | 86 | // .MathJax *[fill] * 87 | // fill inherit 88 | // stroke inherit 89 | 90 | .MathJax svg > g 91 | fill inherit 92 | stroke inherit 93 | 94 | .block > .content > p 95 | margin .5em 0 96 | -------------------------------------------------------------------------------- /styl/2d.styl: -------------------------------------------------------------------------------- 1 | #sec-2d 2 | // this should probably be in ractive-player 3 | --usable-height calc(var(--rp-height) - var(--rp-controls-height)) 4 | 5 | #tangent-demo 6 | background #DBDBDB 7 | border-radius .2em 8 | position absolute 9 | left calc(0.05 * var(--usable-height)) 10 | top calc(0.1 * var(--usable-height)) 11 | height calc(0.8 * var(--usable-height)) 12 | 13 | .glow 14 | stroke #FF7000 15 | stroke-width 0.035 16 | 17 | .plot 18 | color $blue 19 | fill none 20 | stroke $blue 21 | stroke-width 0.05 22 | 23 | .point-A { 24 | cursor: grab; 25 | fill: $wine 26 | stroke-width: 0; 27 | } 28 | 29 | /* PreviewHTML doesn't output class names */ 30 | #defn .MJXp-mtable > span > span:first-child { 31 | color: #AE81FF; 32 | } 33 | 34 | #defn .MJXp-mtable > span > span { 35 | color: orange; 36 | } 37 | 38 | span.tangent-line 39 | color orange 40 | 41 | .tangent-line 42 | stroke orange 43 | stroke-width 0.05 44 | 45 | #explain 46 | background #DBDBDB 47 | border-radius .2em 48 | right calc(0.05 * var(--usable-height)) 49 | top calc(0.1 * var(--usable-height)) 50 | padding .5em 51 | width calc(0.75 * var(--usable-height)) 52 | position absolute 53 | 54 | > table 55 | margin -1em auto 2em 56 | 57 | > h3 58 | margin 2em 0 0 59 | 60 | > ul 61 | margin-left 2em 62 | 63 | > ul > li 64 | margin .5em 0 65 | 66 | .pt1 67 | color $wine 68 | 69 | /* grid */ 70 | .gridline { 71 | stroke: #666; 72 | stroke-width: 0.01; 73 | } 74 | 75 | .major-axis { 76 | stroke: #000; 77 | stroke-width: 0.08; 78 | } 79 | 80 | .axis-label 81 | fill #000 82 | font-size 0.04ex 83 | text-anchor middle 84 | dominant-baseline middle 85 | stroke none 86 | 87 | .axis-tick { 88 | stroke: #777; 89 | stroke-width: 0.05; 90 | } -------------------------------------------------------------------------------- /src/XyJax.tsx: -------------------------------------------------------------------------------- 1 | // @lib 2 | import {Remark} from "@lib/Block"; 3 | import Link from "@lib/Link"; 4 | import {MJX} from "@liqvid/mathjax"; 5 | import {xyEncodeColor} from "@liqvid/xyjax"; 6 | import {AnimatedArrows} from "./xyjax/AnimatedArrows"; 7 | import {Brouwer} from "./xyjax/Brouwer"; 8 | 9 | // MJX.defaultProps = {span: true}; 10 | 11 | // it's better to call this in index.tsx 12 | // extendXY(); 13 | 14 | // resources 15 | import {XyJaxPrompt} from "@env/prompts"; 16 | 17 | const {raw} = String; 18 | 19 | // slide 20 | export default function XyJaxSlide() { 21 | return ( 22 |
23 | 24 |

commutative diagrams (xymatrix syntax) using XyJax

25 |
26 | 27 | 57 | 58 | 59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": ["plugin:@typescript-eslint/recommended", "plugin:react/recommended"], 7 | "globals": { 8 | "Atomics": "readonly", 9 | "SharedArrayBuffer": "readonly" 10 | }, 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { 13 | "ecmaFeatures": { 14 | "jsx": true 15 | }, 16 | "ecmaVersion": 2018, 17 | "sourceType": "module" 18 | }, 19 | "plugins": [ 20 | "@typescript-eslint", 21 | "react", 22 | "react-hooks" 23 | ], 24 | "settings": { 25 | "react": { 26 | "version": "detect" 27 | } 28 | }, 29 | "rules": { 30 | "@typescript-eslint/no-unused-vars": ["error", { 31 | "ignoreRestSiblings": true 32 | }], 33 | 34 | "@typescript-eslint/explicit-function-return-type": ["off"], 35 | "@typescript-eslint/explicit-module-boundary-types": ["off"], 36 | 37 | "@typescript-eslint/explicit-member-accessibility": ["error", { 38 | "accessibility": "no-public" 39 | }], 40 | 41 | "@typescript-eslint/indent": ["error", 2, { 42 | "MemberExpression": 0, 43 | "VariableDeclarator": { "var": 2, "let": 2, "const": 3 } 44 | }], 45 | 46 | "@typescript-eslint/no-use-before-define": ["off"], 47 | 48 | "@typescript-eslint/type-annotation-spacing": ["error", { 49 | "before": false, 50 | "overrides": { 51 | "arrow": {"before": true} 52 | } 53 | }], 54 | 55 | "react-hooks/rules-of-hooks": "error", 56 | "react-hooks/exhaustive-deps": "off", 57 | "react/react-in-jsx-scope": "off", 58 | 59 | "react/no-unescaped-entities": ["error", { 60 | "forbid": [{ 61 | "char": ">", 62 | "alternatives": [">"] 63 | }, { 64 | "char": "}", 65 | "alternatives": ["}"] 66 | }] 67 | }], 68 | 69 | "linebreak-style": ["error", "unix"], 70 | "quotes": ["error", "double"], 71 | "semi": ["error", "always"] 72 | } 73 | } -------------------------------------------------------------------------------- /src/MathJax.tsx: -------------------------------------------------------------------------------- 1 | import {MathJaxPrompt} from "@env/prompts"; 2 | import {Definition, Example} from "@lib/Block"; 3 | import Link from "@lib/Link"; 4 | import {Handle, MJX} from "@liqvid/mathjax"; 5 | import {Utils} from "liqvid"; 6 | import {useRef} from "react"; 7 | 8 | const {from} = Utils.authoring; 9 | const {raw} = String; 10 | 11 | export default function MathJaxSlide() { 12 | const m1 = raw`\data{from-first="mjx/align-1"}`; 13 | const m2 = raw`\data{from-first="mjx/align-2"}`; 14 | const m3 = raw`\data{from-first="mjx/align-3"}`; 15 | 16 | const ref = useRef(); 17 | 18 | return ( 19 |
20 | 21 | The derivative of f at x, denoted f'(x), is 22 | {raw`f'(x) := \lim_{\epsilon\to0}\frac{f(x+\epsilon)-f(x)}{\epsilon}.`} 23 | 24 | 25 | The derivative of x^2} {...from("mjx/ex")}> 26 | {raw`\begin{aligned} 27 | \lim_{\epsilon\to0}\frac{(x+\epsilon)^2-x^2}{\epsilon} 28 | &${m1}{= \lim_{\epsilon\to0}\frac{(x^2 + 2\epsilon x + \epsilon^2)-x^2}{\epsilon}}\\[1em] 29 | &${m2}{= \lim_{\epsilon\to0}\Big(2x + \epsilon\Big)}\\[1em] 30 | &${m3}{= 2x.} 31 | \end{aligned}`} 32 | 33 |

\data command provided by MathJax extension annotations.js

34 |
35 | 36 | 37 |

Can load macros from file: {raw`\THH(R;\Z_p)`}

38 | 39 |

40 | MathJax documentation 41 |

42 |
43 | 44 | 45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const TerserPlugin = require("terser-webpack-plugin"); 2 | const path = require("path"); 3 | 4 | const env = process.env.NODE_ENV || "development"; 5 | 6 | module.exports = { 7 | entry: `${__dirname}/src/index.tsx`, 8 | output: { 9 | filename: "bundle.js", 10 | path: path.join(process.cwd(), "static") 11 | }, 12 | 13 | externals: { 14 | "katex": "katex", 15 | "liqvid": "Liqvid", 16 | "rangetouch": "RangeTouch", 17 | "react": "React", 18 | "react-dom": "ReactDOM", 19 | "three": "THREE", 20 | "three/examples/jsm/controls/FlyControls": "THREE.FlyControls", 21 | "three/examples/jsm/controls/OrbitControls": "THREE.OrbitControls", 22 | "three/examples/jsm/lines/Line2": "THREE.Line2", 23 | "three/examples/jsm/lines/LineGeometry": "THREE.LineGeometry", 24 | "three/examples/jsm/lines/LineMaterial": "THREE.LineMaterial", 25 | "three/examples/jsm/loaders/SVGLoader": "THREE", 26 | "three/examples/jsm/objects/MarchingCubes": "THREE.MarchingCubes", 27 | "three/examples/jsm/utils/GeometryUtils": "THREE.GeometryUtils" 28 | }, 29 | 30 | mode: env, 31 | 32 | module: { 33 | rules: [ 34 | { 35 | test: /\.[jt]sx?$/, 36 | loader: "ts-loader" 37 | } 38 | ], 39 | }, 40 | 41 | // necessary due to bug in old versions of mobile Safari 42 | devtool: false, 43 | 44 | optimization: { 45 | minimizer: [ 46 | new TerserPlugin({ 47 | parallel: true, 48 | terserOptions: { 49 | safari10: true 50 | } 51 | }) 52 | ], 53 | emitOnErrors: true 54 | }, 55 | 56 | resolve: { 57 | extensions: [".ts", ".tsx", ".js", ".jsx", ".json"], 58 | alias: { 59 | "@lib": `${__dirname}/lib`, 60 | "@env": path.join(__dirname, "src", "@" + env), 61 | "@src": path.join(__dirname, "src") 62 | } 63 | }, 64 | 65 | resolveLoader: { 66 | modules: [ 67 | path.join(__dirname, "node_modules") 68 | ] 69 | }, 70 | 71 | snapshot: { 72 | managedPaths: [] 73 | } 74 | }; 75 | -------------------------------------------------------------------------------- /styl/beamer.styl: -------------------------------------------------------------------------------- 1 | // functions 2 | pos(x, y) 3 | position absolute 4 | left x * 1rem 5 | top y * 1rem 6 | 7 | pp(x, y) 8 | position absolute 9 | left x 10 | top y 11 | 12 | center() 13 | position absolute 14 | text-align center 15 | width 100% 16 | 17 | // gen styles 18 | .rp-canvas 19 | background #202020 20 | background-image url(MEDIA_URL + "/img/grid.png") 21 | 22 | .block 23 | border-radius .2rem 24 | box-shadow 0em .1em .3em #000 25 | color #000 26 | fill #000 27 | stroke #000 28 | margin 1em auto .5em 29 | width 80% 30 | 31 | > header 32 | background $blue 33 | border-radius .2rem .2rem 0 0 34 | color #FFF 35 | font-family "Alegreya" 36 | font-weight bold 37 | padding .3rem 1rem 38 | 39 | > .content 40 | background #FFF 41 | padding .3rem 1rem 42 | 43 | .corollary 44 | > header 45 | &::before 46 | content "Corollary. " 47 | 48 | .definition 49 | > header 50 | background $wine 51 | &::before 52 | content "Definition. " 53 | 54 | .example 55 | > header 56 | background green 57 | 58 | &::before 59 | content "Example. " 60 | 61 | .lemma 62 | > header 63 | &::before 64 | content "Lemma. " 65 | 66 | .remark 67 | > header 68 | &::before 69 | content "Remark. " 70 | 71 | .proof 72 | > header 73 | &::before 74 | content "Proof. " 75 | 76 | .proposition 77 | > header 78 | &::before 79 | content "Proposition. " 80 | 81 | .exercise 82 | > header 83 | background $orange 84 | 85 | &::before 86 | content "Exercise. " 87 | 88 | .theorem 89 | > header 90 | &::before 91 | content "Theorem. " 92 | 93 | // text styles 94 | dfn 95 | font-weight bold 96 | 97 | // table of contents 98 | .toc 99 | font-family "KaTeX_Main" 100 | font-size 2em 101 | margin 1em auto 102 | width 65% 103 | 104 | > tbody > tr 105 | cursor pointer 106 | 107 | > th 108 | text-align left 109 | padding-right .5em 110 | 111 | > td 112 | padding .25em 0 113 | 114 | > .name 115 | padding-right 5em 116 | -------------------------------------------------------------------------------- /lib/svg-extrude.ts: -------------------------------------------------------------------------------- 1 | import {SVGLoader} from "three/examples/jsm/loaders/SVGLoader"; 2 | 3 | export default function extrudeSvg(path: string): Promise { 4 | return new Promise((resolve, reject) => { 5 | const loader = new SVGLoader(); 6 | 7 | loader.load(path, data => { 8 | const paths = data.paths; 9 | 10 | const group = new THREE.Group(); 11 | group.scale.multiplyScalar(0.02); 12 | group.scale.y *= -1; 13 | 14 | for (const path of paths) { 15 | // fills 16 | const fillColor = path.userData.style.fill; 17 | 18 | if (fillColor !== undefined && fillColor !== "none") { 19 | const material = new THREE.MeshBasicMaterial({ 20 | color: new THREE.Color().setStyle(fillColor), 21 | opacity: path.userData.style.fillOpacity, 22 | transparent: path.userData.style.fillOpacity < 1, 23 | side: THREE.DoubleSide, 24 | depthWrite: false 25 | }); 26 | 27 | const shapes = path.toShapes(true); 28 | 29 | for (const shape of shapes) { 30 | const geometry = new THREE.ExtrudeBufferGeometry(shape, {depth: 0.2, bevelEnabled: false}); 31 | const mesh = new THREE.Mesh(geometry, material); 32 | 33 | group.add(mesh); 34 | } 35 | } 36 | 37 | // strokes 38 | const strokeColor = path.userData.style.stroke; 39 | 40 | if (strokeColor !== undefined && strokeColor !== "none") { 41 | const material = new THREE.MeshBasicMaterial( { 42 | color: new THREE.Color().setStyle(strokeColor), 43 | opacity: path.userData.style.strokeOpacity, 44 | transparent: path.userData.style.strokeOpacity < 1, 45 | side: THREE.DoubleSide, 46 | depthWrite: false 47 | }); 48 | 49 | for (const subPath of path.subPaths) { 50 | const geometry = SVGLoader.pointsToStroke(subPath.getPoints(), path.userData.style); 51 | 52 | if (geometry) { 53 | const mesh = new THREE.Mesh(geometry, material); 54 | 55 | group.add(mesh); 56 | } 57 | } 58 | } 59 | } 60 | 61 | resolve(group); 62 | }); 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /src/FiveD.tsx: -------------------------------------------------------------------------------- 1 | import {useCallback, useState} from "react"; 2 | 3 | // our imports 4 | 5 | import {KTX} from "@liqvid/katex"; 6 | 7 | // @lib 8 | import Input from "@lib/Input"; 9 | 10 | // resources 11 | import {FiveDPrompt} from "@env/prompts"; 12 | 13 | // scene pieces 14 | import Elliptic from "./5d/Elliptic"; 15 | import Moduli from "./5d/Moduli"; 16 | 17 | export default function FiveD() { 18 | const [a, setA] = useState(0); 19 | const [b, setB] = useState(1); 20 | 21 | const onChangeA = useCallback((e: React.ChangeEvent) => { 22 | setA(parseFloat(e.currentTarget.value)); 23 | }, []); 24 | 25 | const onChangeB = useCallback((e: React.ChangeEvent) => { 26 | setB(parseFloat(e.currentTarget.value)); 27 | }, []); 28 | 29 | return ( 30 |
31 |
32 | 33 |
34 | 35 |
36 | 37 | 38 | 39 | 40 | 41 | 46 | 51 | 52 | 53 | 54 | 59 | 64 | 65 | 66 |
y^2 = x^3 + ax + b
a 42 | 45 | 47 | 50 |
b 55 | 58 | 60 | 63 |
67 |
68 | 69 |
70 | 71 |
72 | 73 |
74 | ); 75 | } 76 | 77 | -------------------------------------------------------------------------------- /src/markers.ts: -------------------------------------------------------------------------------- 1 | import {Script} from "liqvid"; 2 | 3 | export const markers = [ 4 | ["intro/toc", "0:12.475"], 5 | ["intro/main", "0:08.619"], 6 | ["intro/explain", "0:06.572"], 7 | ["intro/codebooth", "0:02.740"], 8 | ["intro/paint", "0:08.239"], 9 | ["intro/clone", "0:09.802"], 10 | ["intro/links", "0:05.315"], 11 | ["intro/js", "0:03.866"], 12 | ["intro/ts", "0:04.530"], 13 | ["intro/react", "0:07.922"], 14 | ["intro/node", "0:12.258"], 15 | ["intro/epiplexis", "0:05.510"], 16 | 17 | ["ktx/", "0:05.211"], 18 | ["ktx/display", "0:02.478"], 19 | ["ktx/ex", "0:01.233"], 20 | ["ktx/align-1", "0:00.805"], 21 | ["ktx/align-2", "0:01.690"], 22 | ["ktx/align-3", "0:08.769"], 23 | ["ktx/macros", "0:19.377"], 24 | ["ktx/docs", "0:04.313"], 25 | 26 | ["mjx/", "0:04.922"], 27 | ["mjx/display", "0:02.389"], 28 | ["mjx/ex", "0:00.834"], 29 | ["mjx/align-1", "0:00.263"], 30 | ["mjx/align-2", "0:00.714"], 31 | ["mjx/align-3", "0:00.982"], 32 | ["mjx/extn", "0:11.211"], 33 | ["mjx/macros", "0:06.839"], 34 | ["mjx/docs", "0:19.265"], 35 | 36 | ["xyjx/", "0:07.763"], 37 | ["xyjx/diagram", "0:15.260"], 38 | ["xyjx/pullback", "0:05.948"], 39 | ["xyjx/color", "0:03.396"], 40 | ["xyjx/arrows", "0:02.146"], 41 | ["xyjx/arrows/anim", "0:02.407"], 42 | ["xyjx/brouwer", "0:08.851"], 43 | ["xyjx/pi1", "0:02.191"], 44 | ["xyjx/apply", "0:03.569"], 45 | ["xyjx/contradiction", "0:05.652"], 46 | 47 | ["2d/", "0:13.371"], 48 | ["2d/consider", "0:12.590"], 49 | ["2d/signify", "0:15.475"], 50 | ["2d/fat", "0:17.716"], 51 | 52 | ["3d/", "0:11.866"], 53 | ["3d/three", "0:08.654"], 54 | ["3d/r3f", "0:08.660"], 55 | ["3d/hide", "0:03.719"], 56 | ["3d/drag", "0:10.377"], 57 | ["3d/pause", "0:03.587"], 58 | ["3d/anim", "0:02.586"], 59 | ["3d/parametric", "0:05.215"], 60 | ["3d/svg", "0:11.541"], 61 | 62 | ["5d/", "0:43.52"] 63 | ] as [string, string][]; 64 | 65 | export const script = new Script(markers); 66 | export const playback = script.playback; 67 | 68 | export const highlights = [ 69 | {title: "KaTeX", time: script.parseStart("ktx/")}, 70 | {title: "MathJax", time: script.parseStart("mjx/")}, 71 | {title: "XyJax", time: script.parseStart("xyjx/")}, 72 | {title: "2D graphics", time: script.parseStart("2d/")}, 73 | {title: "3D graphics", time: script.parseStart("3d/")} 74 | ]; -------------------------------------------------------------------------------- /liqvid.config.ts: -------------------------------------------------------------------------------- 1 | // liqvid.config.ts 2 | import type {LiqvidConfig} from "@liqvid/cli"; 3 | 4 | const os = require("os"); 5 | 6 | const port = process.env.PORT || 3000; 7 | 8 | const scripts = { 9 | "katex": { 10 | "crossorigin": "anonymous", 11 | "defer": true, 12 | "integrity": "sha384-0fdwu/T/EQMsQlrHCCHoH10pkPLlKA1jL5dFyUOvB3lfeT2540/2g6YgSi2BL14p", 13 | "development": "https://cdn.jsdelivr.net/npm/katex@0.15.3/dist/katex.js", 14 | "production": "https://cdn.jsdelivr.net/npm/katex@0.15.3/dist/katex.min.js" 15 | }, 16 | "mathjax": { 17 | "defer": true, 18 | "development": "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js", 19 | "production": "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" 20 | }, 21 | "three": { 22 | "development": "https://unpkg.com/three@0.138.0/build/three.js", 23 | "production": "https://unpkg.com/three@0.138.0/build/three.min.js" 24 | } 25 | }; 26 | 27 | const styles = { 28 | "katex": { 29 | "crossorigin": "anonymous", 30 | "integrity": "sha384-KiWOvVjnN8qwAZbuQyWDIbfCLFhLXNETzBQjA/92pIowpC0d2O3nppDGQVgwd2nB", 31 | "development": "https://cdn.jsdelivr.net/npm/katex@0.15.3/dist/katex.css", 32 | "production": "https://cdn.jsdelivr.net/npm/katex@0.15.3/dist/katex.min.css" 33 | } 34 | }; 35 | 36 | const config: LiqvidConfig = { 37 | audio: { 38 | transcribe: { 39 | "input": "./dist/audio/audio.webm", 40 | "captions": "./dist/captions.vtt", 41 | "transcript": "./dist/transcript.json", 42 | "apiKey": "ho6IutFyHawhFGGID3vU2PEz7_46-WKHTr6zhPNDU7e_", 43 | "apiUrl": "https://api.us-south.speech-to-text.watson.cloud.ibm.com/instances/ad816af7-c138-4671-8c42-7e4e7fdd5151" 44 | } 45 | }, 46 | build: { 47 | scripts, 48 | styles 49 | }, 50 | render: { 51 | audioFile: "./dist/audio/audio.webm", 52 | concurrency: os.cpus().length, 53 | imageFormat: "png" 54 | }, 55 | thumbs: { 56 | url: `http://localhost:${port}/dist`, 57 | browserHeight: 800, 58 | browserWidth: 1280, 59 | concurrency: os.cpus().length, 60 | frequency: 1, 61 | imageFormat: "png", 62 | // make sure the output pattern matches the imageFormat 63 | output: "./dist/thumbs/%s.png" 64 | }, 65 | serve: { 66 | port, 67 | scripts, styles 68 | } 69 | }; 70 | 71 | module.exports = config; 72 | -------------------------------------------------------------------------------- /src/5d/Moduli.tsx: -------------------------------------------------------------------------------- 1 | import {marchingCubes, marchingSquares} from "@lib/graphics"; 2 | import {Canvas} from "@liqvid/react-three"; 3 | import {useFrame, useThree} from "@react-three/fiber"; 4 | import {useEffect, useMemo, useRef} from "react"; 5 | import type {OrbitControls} from "three/examples/jsm/controls/OrbitControls"; 6 | 7 | /* camera */ 8 | function CameraControls() { 9 | const $three = useThree(); 10 | // const context = useContext(R3FContext); 11 | 12 | const { 13 | camera, 14 | gl: {domElement} 15 | } = $three; 16 | (domElement as any).$three = $three; 17 | 18 | // point camera 19 | camera.up.set(0, 0, 1); 20 | 21 | // controls 22 | const controls = useRef(); 23 | useFrame(() => controls.current?.update()); 24 | useEffect(() => { 25 | // context.controls = controls; 26 | }, [controls.current]); 27 | 28 | // point camera 29 | useEffect(() => { 30 | camera.position.set(4.3, -9.5, 6); 31 | camera.lookAt(new THREE.Vector3(0, 0, 0)); 32 | camera.up.set(0, 0, 1); 33 | }, []); 34 | 35 | return ( 36 | 37 | ); 38 | } 39 | 40 | interface Props { 41 | a: number; 42 | b: number; 43 | } 44 | 45 | export default function Moduli(props: Props) { 46 | const moduliGeometry = useMemo(() => 47 | marchingCubes((x, y, z) => y ** 2 - x ** 3 - z * x - props.b, -5, 5, 32) 48 | , [props.b]); 49 | 50 | const section = useMemo(() => { 51 | const edges = marchingSquares( 52 | -5, 5, -5, 5, 53 | (x, y) => y ** 2 - x ** 3 - props.a * x - props.b + props.a, 54 | props.a, 55 | 64 56 | ) as number[][]; 57 | 58 | const lineGeometry = new THREE.LineSegmentsGeometry().setPositions( edges.reduce((a,b)=> a.concat(b)) ); 59 | 60 | const lineMaterial = new THREE.LineMaterial({ color: 0xFF0070, linewidth: 6}); 61 | 62 | lineMaterial.resolution.set(window.innerWidth, window.innerHeight); // important, for now... 63 | 64 | const linePavement = new THREE.LineSegments2(lineGeometry, lineMaterial ); 65 | return linePavement; 66 | }, [props.a, props.b]); 67 | 68 | return ( 69 | 70 | {/* lights */} 71 | 72 | 73 | 74 | {/* camera */} 75 | {} 76 | 77 | 78 | 79 | 80 | 81 | 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /src/3d/HelpControl.tsx: -------------------------------------------------------------------------------- 1 | import {useMarkerUpdate, usePlayer, Utils} from "liqvid"; 2 | import {useMemo, useRef, useState} from "react"; 3 | 4 | const {anyHover, onClick} = Utils.mobile; 5 | 6 | export default function HelpControl() { 7 | const {script} = usePlayer(); 8 | 9 | const classNames = ["rp-3d-help-wrap"]; 10 | 11 | // open/close dialog 12 | const [open, setOpen] = useState(true); 13 | const events = useMemo(() => onClick(() => { 14 | setOpen(prev => !prev); 15 | }), []); 16 | 17 | if (open) 18 | classNames.push("open"); 19 | 20 | // show/hide control 21 | const ref = useRef(); 22 | useMarkerUpdate(() => { 23 | ref.current.classList.toggle("relevant", script.markerName.startsWith("3d/")); 24 | }, []); 25 | 26 | if (script.markerName.startsWith("3d/")) 27 | classNames.push("relevant"); 28 | 29 | // desktop/mobile controls 30 | const controls = anyHover ? [ 31 | ["Left mouse", "Orbit"], 32 | ["Scroll wheel", "Zoom"], 33 | ["Right mouse", "Pan"] 34 | ] : [ 35 | ["Swipe", "Orbit"], 36 | ["Pinch", "Zoom"], 37 | ["Two fingers", "Pan"], 38 | ]; 39 | 40 | return ( 41 |
42 | 55 | 56 | 61 | 66 | 67 |
68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /src/Intro.tsx: -------------------------------------------------------------------------------- 1 | import {IntroPrompt} from "@env/prompts"; 2 | import Link from "@lib/Link"; 3 | import {Utils} from "liqvid"; 4 | import {useMemo} from "react"; 5 | import {playback, script} from "./markers"; 6 | 7 | const {from} = Utils.authoring, 8 | {onClick} = Utils.mobile, 9 | {formatTime, formatTimeMs} = Utils.time; 10 | 11 | const contents = [ 12 | ["Introduction", script.parseStart("intro/toc")], 13 | ["KaTeX", script.parseStart("ktx/")], 14 | ["MathJax", script.parseStart("mjx/")], 15 | ["XyJax", script.parseStart("xyjx/")], 16 | ["2D graphics", script.parseStart("2d/")], 17 | ["3D graphics", script.parseStart("3d/")] 18 | ] as [string, number][]; 19 | 20 | export default function Intro() { 21 | const seek = useMemo(() => onClick((e) => { 22 | e.preventDefault(); 23 | 24 | // this is not great lol 25 | playback.seek(e.currentTarget.getAttribute("href").slice(3)); 26 | }), []); 27 | 28 | return ( 29 |
30 |

Liqvid math tutorial

31 | {/* table of contents */} 32 | 33 | 34 | {contents.map((row, i) => ( 35 | 36 | 37 | 40 | 43 | 44 | ))} 45 | 46 |
{i + 1}. 38 | {row[0]} 39 | 41 | {formatTime(row[1])} 42 |
47 | 48 |
49 |

50 | General-purpose tutorial 51 |

52 |
    53 |
  • explains what ractives are, how to write and record them
  • 54 | 55 |
  • coding plugin
  • 56 | 57 |
  • freehand drawing plugin
  • 58 |
59 | 60 |

61 | Clone this tutorial: https://github.com/ysulyma/lv-tutorial-math 62 |

63 | 64 |

Links

65 |
    66 |
  • Javascript.info course
  • 67 |
  • TypeScript documentation
  • 68 |
  • React documentation
  • 69 |
  • Node.js
  • 70 |
  • Inspiration: Epiplexis
  • 71 |
72 |
73 | 74 | 75 |
76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /src/ThreeD.tsx: -------------------------------------------------------------------------------- 1 | // resources 2 | import {ThreeDPrompt} from "@env/prompts"; 3 | import Link from "@lib/Link"; 4 | import {APIHelper, useApi} from "@lib/ThreeFiber"; 5 | import {Canvas} from "@liqvid/react-three"; 6 | import {useFrame, useThree} from "@react-three/fiber"; 7 | import {useMarkerUpdate, useTime, Utils} from "liqvid"; 8 | import {useEffect, useRef} from "react"; 9 | import {OrbitControls} from "three/examples/jsm/controls/OrbitControls"; 10 | import Arrow from "./3d/Arrow"; 11 | import Cylinder from "./3d/Cylinder"; 12 | import Parametric from "./3d/Parametric"; 13 | import Sphere from "./3d/Sphere"; 14 | import {playback, script} from "./markers"; 15 | 16 | const {from} = Utils.authoring, 17 | {between} = Utils.misc; 18 | 19 | /* camera */ 20 | function CameraControls() { 21 | const $three = useThree(); 22 | const context = useApi(); 23 | 24 | const { 25 | camera, 26 | gl: {domElement} 27 | } = $three; 28 | (domElement as any).$three = $three; 29 | 30 | // point camera 31 | camera.up.set(0, 0, 1); 32 | 33 | // controls 34 | const controls = useRef(); 35 | useFrame(() => controls.current?.update()); 36 | useEffect(() => { 37 | context.controls = controls.current; 38 | }, [controls.current]); 39 | 40 | // point camera 41 | useEffect(() => { 42 | camera.position.set(4.3, -9.5, 6); 43 | camera.lookAt(new THREE.Vector3(0, 0, 0)); 44 | camera.up.set(0, 0, 1); 45 | }, []); 46 | 47 | return ( 48 | 49 | ); 50 | } 51 | 52 | const m = script.parseStart("3d/pause"); 53 | const arrowIndex = script.markerNumberOf("3d/svg"); 54 | 55 | export default function Scene() { 56 | // axes helper 57 | const helperRef = useRef(); 58 | useMarkerUpdate(() => { 59 | if (!helperRef.current) 60 | return; 61 | helperRef.current.visible = (script.markerIndex < arrowIndex); 62 | }, []); 63 | 64 | // pausing 65 | const prev = useRef(playback.currentTime); 66 | const EPSILON = 300; 67 | useTime(t => { 68 | if (between(m - EPSILON, prev.current, m) && between(m, t, m + EPSILON)) { 69 | playback.pause(); 70 | } 71 | prev.current = t; 72 | }, []); 73 | 74 | return ( 75 |
76 | 81 | 82 | 83 | 84 | 85 | 86 | {/* camera */} 87 | 88 | 89 | 90 | 91 | {/* action */} 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 |
100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /src/3d/Cylinder.tsx: -------------------------------------------------------------------------------- 1 | import {intercept, R3FContext, useDraggable} from "@lib/ThreeFiber"; 2 | import {ThreeEvent} from "@react-three/fiber"; 3 | import {useMarkerUpdate, usePlayer, Utils} from "liqvid"; 4 | import {useCallback, useContext, useMemo, useRef} from "react"; 5 | // THREE 6 | import * as THREE from "three"; 7 | import {DoubleSide, Mesh, Plane} from "three"; 8 | 9 | const {constrain} = Utils.misc, 10 | {anyHover} = Utils.mobile; 11 | 12 | const TWOPI = 2 * Math.PI; 13 | 14 | export default function Cylinder() { 15 | const {script} = usePlayer(); 16 | const radius = 3; 17 | const height = 8; 18 | 19 | const cylinder = useRef(); 20 | const slider = useRef(); 21 | const fat = useRef(); 22 | 23 | const api = useContext(R3FContext); 24 | 25 | /* dragging */ 26 | const plane = useRef(new Plane()); 27 | 28 | const move = useCallback((e: PointerEvent) => { 29 | // set plane 30 | // const intersections = raycaster.intersectObject(cylinder.current); 31 | // const int = intersections.find(_ => _.object.name === "cylinder"); 32 | // if (int) { 33 | // const normalMatrix = new THREE.Matrix3().getNormalMatrix(int.object.matrixWorld); 34 | // const normal = int.face.normal; 35 | // normal.applyMatrix3(normalMatrix).normalize(); 36 | 37 | // plane.current.setFromNormalAndCoplanarPoint( 38 | // normal, 39 | // int.point 40 | // ).normalize(); 41 | // } 42 | 43 | const pos = api.screenToScene(e.clientX, e.clientY, plane.current); 44 | const h = constrain(-height/2, pos.z, height/2); 45 | slider.current.position.setZ(h); 46 | if (!anyHover) 47 | fat.current.position.setZ(h); 48 | }, []); 49 | 50 | const down = useCallback((e: ThreeEvent) => { 51 | const int = e.intersections.find(_ => _.object.name === "cylinder"); 52 | if (!int) 53 | return; 54 | 55 | const normalMatrix = new THREE.Matrix3().getNormalMatrix(int.object.matrixWorld ); 56 | const normal = int.face.normal; 57 | normal.applyMatrix3(normalMatrix).normalize(); 58 | 59 | plane.current.setFromNormalAndCoplanarPoint( 60 | normal, 61 | int.point 62 | ).normalize(); 63 | }, []); 64 | 65 | const events = useDraggable(move, down); 66 | 67 | /* visibility */ 68 | const index = useMemo(() => script.markerNumberOf("3d/drag"), []); 69 | useMarkerUpdate(() => { 70 | slider.current.visible = script.markerIndex >= index; 71 | if (fat.current) { 72 | fat.current.visible = script.markerIndex >= index; 73 | } 74 | }, []); 75 | 76 | return ( 77 | <> 78 | 83 | 84 | 85 | 86 | = index}> 87 | 88 | 89 | 90 | {anyHover ? null : = index}> 91 | 92 | 93 | } 94 | 95 | ); 96 | } -------------------------------------------------------------------------------- /src/xyjax/Brouwer.tsx: -------------------------------------------------------------------------------- 1 | import {Handle, MJX} from "@liqvid/mathjax"; 2 | import {Utils, useTime} from "liqvid"; 3 | import {useEffect, useRef} from "react"; 4 | import {playback, script} from "../markers"; 5 | const {from} = Utils.authoring; 6 | 7 | /* animations */ 8 | const dur = { 9 | piFadeIn: 300, 10 | fadeOut: 400, 11 | fadeIn: 400 12 | }; 13 | 14 | const times = { 15 | piFadeIn: script.parseStart("xyjx/pi1"), 16 | fadeOut: script.parseStart("xyjx/apply"), 17 | contradiction: script.parseStart("xyjx/contradiction"), 18 | }; 19 | 20 | const piFadeIn = () => playback.newAnimation( 21 | [{opacity: 0}, {opacity: 1}], 22 | { 23 | delay: times.piFadeIn, 24 | duration: dur.piFadeIn, 25 | easing: "ease-in", 26 | fill: "both" 27 | } 28 | ); 29 | 30 | const fadeOut = () => playback.newAnimation( 31 | [{opacity: 1}, {opacity: 0}], 32 | { 33 | delay: times.fadeOut, 34 | duration: dur.fadeOut, 35 | easing: "ease-out", 36 | fill: "forwards" 37 | } 38 | ); 39 | 40 | const fadeIn = () => playback.newAnimation( 41 | [{opacity: 0}, {opacity: 1}], 42 | { 43 | delay: times.fadeOut + dur.fadeOut * 2 / 3, 44 | duration: dur.fadeIn, 45 | easing: "ease-in", 46 | fill: "backwards" 47 | } 48 | ); 49 | 50 | const {raw} = String; 51 | 52 | export function Brouwer() { 53 | const ref = useRef(); 54 | 55 | // fade effects 56 | useEffect(() => { 57 | ref.current.ready.then(() => { 58 | const node = ref.current.domElement; 59 | 60 | for (const term of getDelimiters(node, "pi")) { 61 | piFadeIn()(term); 62 | } 63 | 64 | for (const term of getDelimiters(node, "out")) { 65 | fadeOut()(term); 66 | } 67 | 68 | for (const term of getDelimiters(node, "in")) { 69 | fadeIn()(term); 70 | } 71 | }); 72 | }, []); 73 | 74 | // contradiction 75 | useTime(val => ref.current.domElement.classList.toggle("contradiction", val), t => (t >= times.contradiction), []); 76 | 77 | return ( 78 | {raw` 79 | \class{pi-l}{}\pi_1\left(\class{pi-r}{} 80 | \vcenter{${"\\"}xymatrix@=1.5em{ 81 | \class{out-l}{}S^1\class{out-r}{} \class{in-l}{}\hspace{-.8em}\Z\class{in-r}{} \ar@{=}[rr] \ar[ddr] && 82 | \class{out-l}{}S^1\class{out-r}{} \class{in-l}{}\hspace{-1.15em}\Z\class{in-r}{}\\ 83 | \\ 84 | & \class{out-l}{}D^2\class{out-r}{} \class{in-l}{}\hspace{-1em}0\class{in-r}{} \hspace{.2em} \ar[uur] 85 | }} 86 | \hspace{.2em} 87 | \class{pi-l}{}\right)\class{pi-r}{} 88 | `} 89 | ); 90 | } 91 | 92 | function getDelimiters(node: Element, name: string): Element[] { 93 | const nodes: Element[] = []; 94 | const delimiters = Array.from(node.getElementsByClassName(`${name}-l`)); 95 | 96 | for (const delimL of delimiters) { 97 | let next = delimL.nextElementSibling; 98 | while (true) { 99 | if (next.classList.contains(`${name}-r`)) { 100 | break; 101 | } 102 | 103 | if (next.childElementCount > 0) { 104 | next = next.firstElementChild; 105 | } 106 | nodes.push(next); 107 | 108 | // iteration 109 | let parent = next; 110 | while (!parent.nextElementSibling) { 111 | parent = parent.parentElement; 112 | if (parent === document.body) { 113 | throw new Error(`Could not find matching delimiter for ${name}`); 114 | } 115 | } 116 | next = parent.nextElementSibling; 117 | } 118 | } 119 | 120 | return nodes; 121 | } 122 | -------------------------------------------------------------------------------- /lib/HelpControl.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | const {useCallback, useEffect, useMemo, useState} = React; 3 | import * as ReactDOM from "react-dom"; 4 | 5 | import {Utils, usePlayer} from "liqvid"; 6 | const {bind} = Utils.misc; 7 | const {onClick} = Utils.mobile; 8 | 9 | export default function HelpControl() { 10 | const {keymap} = usePlayer(); 11 | const [open, setOpen] = useState(false); 12 | 13 | const openDialog = useCallback(() => setOpen(true), []); 14 | const closeDialog = useCallback(() => setOpen(false), []); 15 | const toggleDialog = useCallback(() => setOpen(prev => !prev), []); 16 | 17 | useEffect(() => { 18 | // keyboard shortcuts 19 | keymap.bind("Shift+?", toggleDialog); 20 | keymap.bind("Escape", closeDialog); 21 | 22 | return () => { 23 | keymap.unbind("Shift+?", toggleDialog); 24 | keymap.unbind("Escape", closeDialog); 25 | }; 26 | }, []); 27 | 28 | const events = useMemo(() => onClick(openDialog), []); 29 | 30 | const style: React.CSSProperties = { 31 | display: open ? "block" : "none" 32 | }; 33 | 34 | return ( 35 | <> 36 | 37 | 38 | 43 | 48 | 49 | 50 | ); 51 | } 52 | 53 | interface DialogProps { 54 | closeDialog: () => void; 55 | openDialog: () => void; 56 | style: React.CSSProperties; 57 | } 58 | 59 | function HelpDialog(props: DialogProps) { 60 | const videoShortcuts = [ 61 | /*["w", "Previous marker"], 62 | ["e", "Next marker"],*/ 63 | 64 | ["j", "Go back 10 seconds"], 65 | ["", "Go back 5 seconds"], 66 | [() => ( 67 | 68 | k or <Space> 69 | Play/pause 70 | 71 | )], 72 | ["", "Go forward 5 seconds"], 73 | ["l", "Go forward 10 seconds"], 74 | [() => ( 75 | 76 | <0><9> 77 | Skip to 10n% of the way through the video 78 | 79 | )], 80 | 81 | ["f", "Full screen"], 82 | 83 | ["", "Increase volume 5%"], 84 | ["", "Decrease volume 5%"], 85 | ["m", "Mute/unmute"], 86 | ["?", "Show help"] 87 | ]; 88 | 89 | return ReactDOM.createPortal( 90 |
91 | 92 | 93 |
94 | 95 | 96 | 97 | {videoShortcuts.map(([key, desc]) => ( 98 | typeof key === "function" ? key() : 99 | 100 | 101 | 102 | 103 | ))} 104 | 105 |
Video controls
{key}{desc}
106 |
107 |
, 108 | document.body 109 | ); 110 | } 111 | -------------------------------------------------------------------------------- /lib/ThreeFiber.tsx: -------------------------------------------------------------------------------- 1 | import {Canvas} from "@liqvid/react-three"; 2 | import {ThreeEvent, useThree} from "@react-three/fiber"; 3 | import {Player} from "liqvid"; 4 | import {createContext, useContext, useEffect, useMemo} from "react"; 5 | 6 | // main export 7 | interface Api { 8 | activeListener: (e: PointerEvent) => void; 9 | controls: any; 10 | dragging: boolean; 11 | meshes: { 12 | [key: string]: THREE.Mesh; 13 | }; 14 | // $three: R3F.SharedCanvasContext; 15 | screenToNDC(x: number, y: number): THREE.Vector2; 16 | screenToScene(x: number, y: number, plane: THREE.Plane): THREE.Vector3; 17 | } 18 | 19 | export const R3FContext = createContext(undefined); 20 | 21 | export function useApi() { 22 | return useContext(R3FContext); 23 | } 24 | 25 | export function ThreeScene(props: React.ComponentProps) { 26 | const {children, ...attrs} = props; 27 | const player = useContext(Player.Context); 28 | 29 | return ( 30 | 31 | 32 | {children} 33 | 34 | 35 | ); 36 | } 37 | 38 | export function APIHelper(props: React.ComponentProps) { 39 | const $three = useThree(); 40 | $three.gl.localClippingEnabled = true; 41 | 42 | const api = useMemo(() => ({ 43 | activeListener: null, 44 | controls: null, 45 | dragging: false, 46 | meshes: {}, 47 | $three, 48 | screenToNDC(x: number, y: number) { 49 | const rect = $three.gl.domElement.getBoundingClientRect(); 50 | 51 | return new THREE.Vector2( 52 | (x - rect.left) / rect.width * 2 - 1, 53 | -(y - rect.top) / rect.height * 2 + 1 54 | ); 55 | }, 56 | screenToScene(x: number, y: number, plane: THREE.Plane) { 57 | const rect = $three.gl.domElement.getBoundingClientRect(); 58 | const ndc = api.screenToNDC(x, y), 59 | mouse = new THREE.Vector3(ndc.x, ndc.y, 0); 60 | 61 | mouse.unproject($three.camera); 62 | 63 | const dir = mouse.sub($three.camera.position).normalize(); 64 | 65 | const distance = -plane.distanceToPoint($three.camera.position) / Math.cos(dir.angleTo(plane.normal)); 66 | 67 | return $three.camera.position.clone().add(dir.multiplyScalar(distance)); 68 | } 69 | }), [$three]); 70 | 71 | // bind drag helpers 72 | useEffect(() => { 73 | const {domElement} = $three.gl; 74 | 75 | domElement.addEventListener("pointermove", e => { 76 | if (api.activeListener) 77 | api.activeListener(e); 78 | }); 79 | 80 | // bind pointerup 81 | domElement.addEventListener("pointerup", e => { 82 | if (api.controls) { 83 | api.controls.enabled = true; 84 | } 85 | api.dragging = false; 86 | api.activeListener = null; 87 | domElement.classList.remove("dragging"); 88 | }); 89 | }, []); 90 | 91 | return ( 92 | 93 | {props.children} 94 | 95 | ); 96 | } 97 | 98 | export function useDraggable( 99 | move: (e: PointerEvent) => void, 100 | down?: (e: ThreeEvent) => void, 101 | up?: (e: ThreeEvent) => void 102 | ) { 103 | const { 104 | gl: {domElement} 105 | } = useThree(); 106 | 107 | const state = useContext(R3FContext); 108 | 109 | // add CSS class on hover to enable "draggable" cursor; desktop-only 110 | const events = useMemo(() => { 111 | return { 112 | onPointerOver: (e: ThreeEvent) => { 113 | if (e.intersections[0].object !== e.eventObject) 114 | return; 115 | domElement.classList.add("draggable"); 116 | }, 117 | onPointerOut: (e: ThreeEvent) => { 118 | /*if (e.intersections[0]?.object !== e.eventObject) 119 | return;*/ 120 | domElement.classList.remove("draggable"); 121 | }, 122 | onPointerDown: (e: ThreeEvent) => { 123 | if (e.intersections[0].object !== e.eventObject) 124 | return; 125 | 126 | if (state.controls) { 127 | state.controls.enabled = false; 128 | } 129 | state.dragging = true; 130 | state.activeListener = move; 131 | domElement.classList.add("dragging"); 132 | 133 | down?.(e); 134 | } 135 | }; 136 | }, [move]); 137 | 138 | return events; 139 | } 140 | 141 | export const intercept = { 142 | onPointerOver: () => {}, 143 | onPointerDown: () => {}, 144 | onPointerOut: () => {} 145 | }; 146 | -------------------------------------------------------------------------------- /static/annotations.js: -------------------------------------------------------------------------------- 1 | /************************************************************* 2 | * 3 | * MathJax/extensions/TeX/annotations.js 4 | * 5 | * Implements annotations for MathJax 6 | * 7 | * --------------------------------------------------------------------- 8 | * 9 | * Copyright (c) 2013 Yuri Sulyma . 10 | * 11 | * Licensed under the Apache License, Version 2.0 (the "License"); 12 | * you may not use this file except in compliance with the License. 13 | * You may obtain a copy of the License at 14 | * 15 | * http://www.apache.org/licenses/LICENSE-2.0 16 | * 17 | * Unless required by applicable law or agreed to in writing, software 18 | * distributed under the License is distributed on an "AS IS" BASIS, 19 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20 | * See the License for the specific language governing permissions and 21 | * limitations under the License. 22 | */ 23 | 24 | (() => { 25 | const {Configuration} = MathJax._.input.tex.Configuration; 26 | const {CommandMap} = MathJax._.input.tex.SymbolMap; 27 | const NodeUtil = MathJax._.input.tex.NodeUtil.default; 28 | 29 | const AnnotateMethods = {}; 30 | const symbol = Symbol("mathjax-annotations"); 31 | 32 | /** 33 | * Implements \Annotate[type]{command}{annotation} 34 | * @param {TexParser} parser The calling parser. 35 | * @param {string} name The macro name. 36 | */ 37 | AnnotateMethods.Annotate = (parser, name) => { 38 | const type = parser.GetBrackets(name); 39 | const macro = parser.GetArgument(name); 40 | const annotation = parser.GetArgument(name); 41 | 42 | // existing macro definition 43 | const def = parser.configuration.handlers.retrieve("new-Command").map.get(macro.slice(1)); 44 | 45 | // create annotation intercept 46 | if (!def[symbol]) { 47 | def[symbol] = {}; 48 | const old = def.func; 49 | 50 | // this is ugly but I couldn't figure out a slicker way to do it 51 | def._func = (parser, name, expansion, ...args) => { 52 | const keys = Object.keys(def[symbol]); 53 | const annotations = keys.map((key) => "{" + def[symbol][key] + "}").join(""); 54 | old(parser, name, `\\annotate[${keys.join(",")}]{${expansion}}${annotations}`, ...args) 55 | }; 56 | } 57 | 58 | // add annotation 59 | def[symbol][type] = annotation; 60 | }; 61 | 62 | /** 63 | * Implements \annotate[type]{content}{...annotations} 64 | * @param {TexParser} parser The calling parser. 65 | * @param {string} name The macro name. 66 | */ 67 | AnnotateMethods.annotate = (parser, name) => { 68 | const types = parser.GetBrackets(name).split(","); 69 | const content = GetArgumentMML(parser, name); 70 | for (const type of types) { 71 | const annotation = parser.GetArgument(name); 72 | NodeUtil.setAttribute(content, `data-annotation_${type}`, annotation); 73 | } 74 | parser.Push(content); 75 | }; 76 | 77 | /** 78 | * Implements \data{dataset}{content} 79 | * @param {TexParser} parser The calling parser. 80 | * @param {string} name The macro name. 81 | */ 82 | AnnotateMethods.data = (parser, name) => { 83 | const dataset = parser.GetArgument(name); 84 | const arg = GetArgumentMML(parser, name); 85 | for (const [prop, val] of splitTokens(dataset)) { 86 | NodeUtil.setAttribute(arg, `data-${prop}`, val); 87 | } 88 | parser.Push(arg); 89 | }; 90 | 91 | /** 92 | * The mapping of control sequence to function calls 93 | */ 94 | new CommandMap("annotateMap", { 95 | Annotate: ["Annotate"], 96 | annotate: ["annotate"], 97 | data: ["data"] 98 | }, AnnotateMethods); 99 | 100 | /** 101 | * The configuration used to enable the MathML macros 102 | */ 103 | Configuration.create("annotate", { 104 | handler: {macro: ["annotateMap"]} 105 | }); 106 | 107 | /** 108 | * Parses the math argument of the above commands and returns it as single 109 | * node (in an mrow if necessary). The HTML attributes are then 110 | * attached to this element. 111 | * @param {TexParser} parser The calling parser. 112 | * @param {string} name The calling macro name. 113 | * @return {MmlNode} The math node. 114 | */ 115 | let GetArgumentMML = function (parser, name) { 116 | let arg = parser.ParseArg(name); 117 | if (!NodeUtil.isInferred(arg)) { 118 | return arg; 119 | } 120 | let children = NodeUtil.getChildren(arg); 121 | if (children.length === 1) { 122 | return children[0]; 123 | } 124 | const mrow = parser.create("node", "mrow"); 125 | NodeUtil.copyChildren(arg, mrow); 126 | NodeUtil.copyAttributes(arg, mrow); 127 | return mrow; 128 | }; 129 | 130 | function splitTokens(str) { 131 | const matches = Array.from(str.matchAll(/\b([A-Za-z0-9_-]+)=(['"])(.+?)\2/g)); 132 | return matches.map(([, name, , val]) => [name, val]); 133 | } 134 | })(); 135 | -------------------------------------------------------------------------------- /src/TwoD.tsx: -------------------------------------------------------------------------------- 1 | import {TwoDPrompt} from "@env/prompts"; 2 | import FatFingers from "@lib/FatFingers"; 3 | import GlowOrb from "@lib/GlowOrb"; 4 | import Input from "@lib/Input"; 5 | import {KTX} from "@liqvid/katex"; 6 | import {Utils} from "liqvid"; 7 | import {Fragment, useCallback, useEffect, useRef, useState} from "react"; 8 | 9 | const {from} = Utils.authoring, 10 | {dragHelperReact} = Utils.interactivity, 11 | {constrain, range} = Utils.misc, 12 | {anyHover} = Utils.mobile, 13 | {screenToSVG} = Utils.svg; 14 | 15 | const {raw} = String; 16 | 17 | const a = -4, b = 4; 18 | const minX = -5, maxX = 5; 19 | 20 | // there must be a better way to do this 21 | const negF = (x: number) => [x, 2-x*x] as [number, number]; 22 | const f = (x: number) => [x, x*x-2] as [number, number]; 23 | const df = (x: number) => 2 * x; 24 | 25 | type MoveArgs = Parameters[0]>; 26 | 27 | export default function TwoD() { 28 | const [x, setX] = useState(1); 29 | 30 | const svgRef = useRef(); 31 | 32 | // dragging 33 | const [showOrb, setShowOrb] = useState(true); 34 | 35 | useEffect(() => { 36 | // ??? 37 | for (const target of Array.from(svgRef.current.querySelectorAll("circle"))) { 38 | target.addEventListener("touchstart", e => e.preventDefault(), {passive: false}); 39 | } 40 | }, [svgRef.current]); 41 | 42 | const onDown = useCallback((e: React.MouseEvent) => { 43 | setShowOrb(false); 44 | document.body.classList.add("dragging"); 45 | }, []); 46 | 47 | const onUp = useCallback(() => { 48 | document.body.classList.remove("dragging"); 49 | }, []); 50 | 51 | const onMoveA = useCallback((...[e, hit]: MoveArgs) => { 52 | const [svgX, svgY] = screenToSVG(svgRef.current, hit.x, hit.y); 53 | setX(constrain(a, closestPoint(svgX, -svgY), b)); 54 | }, [svgRef]); 55 | 56 | // equation of tangent line 57 | const A = f(x); 58 | 59 | const slope = df(x), 60 | intercept = A[1] - slope * A[0]; 61 | let tangentEqn; 62 | if (slope === 0) { 63 | tangentEqn = fmt(intercept); 64 | } else { 65 | tangentEqn = fmt(slope) + "x"; 66 | 67 | if (intercept > 0) { 68 | tangentEqn += "+" + fmt(intercept); 69 | } else if (intercept < 0) { 70 | tangentEqn += fmt(intercept); 71 | } 72 | } 73 | 74 | // input 75 | const inputA = useCallback((e: React.FormEvent) => { 76 | const val = parseFloat(e.currentTarget.value); 77 | if (!isNaN(val)) 78 | setX(constrain(a, val, b)); 79 | }, []); 80 | 81 | return ( 82 |
83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | {showOrb && } 92 | 93 | 94 |
95 | {raw` 96 | \begin{aligned} 97 | \htmlClass{plot}{y} &\htmlClass{plot}{= x^2-2}\\ 98 | \htmlClass{tangent-line}{y} &\htmlClass{tangent-line}{= ${tangentEqn}} 99 | \end{aligned} 100 | `} 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 |
{raw`\htmlClass{pt1}{c =}`}
111 | 112 |

Try moving the point on the graph with your {anyHover ? "mouse" : "finger"}.

113 | 114 |

Considerations

115 | 116 |
    117 |
  • use consistent signifiers to indicate interactive components
  • 118 | 119 |
  • need larger hit area for mobile (fat-fingers in the code)
  • 120 |
121 |
122 | 123 |
124 | ); 125 | } 126 | 127 | function CartesianGrid() { 128 | const minY = -5, 129 | maxY = 5; 130 | 131 | return ( 132 | 133 | {range(minX, maxX + 1).map(n => n !== 0 && ( 134 | 135 | 136 | 137 | {n.toString().replace("-", "–")} 138 | 139 | ))} 140 | 141 | 0 142 | 143 | {range(minY, maxY + 1).map(n => n !== 0 && ( 144 | 145 | 146 | 147 | {n.toString().replace("-", "–")} 148 | 149 | ))} 150 | 151 | 152 | 153 | 154 | ); 155 | } 156 | 157 | function fmt(x: number, l = 4) { 158 | return parseFloat(x.toFixed(l)); 159 | } 160 | 161 | function closestPoint(x0: number, y0: number) { 162 | const minX = a, 163 | maxX = b; 164 | const samples = 1000; 165 | 166 | const dx = (maxX - minX) / samples; 167 | // let best = minX, 168 | // minVal = Math.abs(df(best) + (best - x0) / (f(best)[1] - y0)); 169 | 170 | // for (let i = 1; i <= samples; ++i) { 171 | // const xi = minX + i * dx; 172 | // const val = Math.abs(df(xi) + (xi - x0) / (f(xi)[1] - y0)); 173 | // if (val < minVal) { 174 | // minVal = val; 175 | // best = xi; 176 | // } 177 | // } 178 | 179 | let best = minX, 180 | minVal = (x0 - best) ** 2 + (y0 - f(best)[1]) ** 2; 181 | 182 | for (let i = 1; i <= samples; ++i) { 183 | const xi = minX + i * dx; 184 | const val = (x0 - xi) ** 2 + (y0 - f(xi)[1]) ** 2; 185 | if (val < minVal) { 186 | minVal = val; 187 | best = xi; 188 | } 189 | } 190 | 191 | return best; 192 | } 193 | 194 | function graph(f: (t: number) => [number, number], a = 0, b = 1, sampling = 100) { 195 | const instructions = new Array(sampling + 1) 196 | .fill(null) 197 | .map((_, n) => { 198 | const [x, y] = f(a + (n / sampling) * (b - a)); 199 | return (n === 0) ? `M ${x} ${y}` : `L ${x} ${y}`; 200 | }); 201 | 202 | return instructions.join(" "); 203 | } 204 | -------------------------------------------------------------------------------- /src/@development/prompts.tsx: -------------------------------------------------------------------------------- 1 | import {Prompt, Cue} from "@liqvid/prompt"; 2 | 3 | type P = Parameters[0]; 4 | 5 | export const IntroPrompt = (props: P) => ( 6 | 7 | 8 | Liqvid is a tool for creating interactive videos out of HTML. In this video, I'm going to demonstrate how to achieve various effects that you might want when using this tool for mathematical content. 9 | 10 | 11 | This supplements the general-purpose tutorial; you might want to watch that first. That's a link that you can click on. That'll 12 | 13 | 14 | explain what this library is, the main concepts behind ractives and how to record them. In particular, if you want to 15 | 16 | 17 | include coding activities, or if you want to write out formulas by hand, the general purpose tutorial covers the plugins for doing that. Here we're going to focus on rendered formulas and on graphics. 18 | 19 | 20 | The way to use this tutorial is to clone the repository, and then you can follow along in the source code to see how I achieved various things. 21 | 22 | 23 | Some other helpful links: this does require a pretty solid command of frontend web development, so 24 | 25 | 26 | here's a Javascript course that I like. This is actually written in 27 | 28 | 29 | TypeScript, which is a superset of Javascript that adds more typing. And it's built on top of 30 | 31 | 32 | software called React, which makes it a lot easier to do dynamic things with HTML. So here's the documentation for that. 33 | 34 | 35 | To transpile Typescript and React into normal Javascript, you'll need to install Node.js. 36 |
37 | These tools are all pretty standard in web development, but not as familiar to mathematicians. 38 |
39 | 40 | Finally, you can see examples of this used for actual math lessons on my website. 41 | 42 |
43 | ); 44 | 45 | export const KaTeXPrompt = (props: P) => ( 46 | 47 | 48 | So let's get started. These formulas are rendered using KaTeX; you can use inline 49 | 50 | 51 | or display equations. If you want to 52 | 53 | 54 | successively reveal parts of an equation, you can use ractive-player's built-in show/hiding functionality, but you'll see in the source that you kind of have to attach it by hand. 55 | 56 | 57 | The library also provides a way to load macro definitions from a file; this is intended to make it easy to reuse macros across a lot of diferent videos. So here THH was a macro defined in the file symbols.tex. 58 | 59 | 60 | Finally, here's the KaTeX documentation. 61 | 62 | 63 | ); 64 | 65 | export const MathJaxPrompt = (props: P) => ( 66 | 67 | 68 | You can also render your formulas using MathJax. Again you've got inline 69 | 70 | 71 | or display equations, you can do equation reveals; 72 | 73 | 74 | unlike KaTeX, you do need a MathJax extension to do the equation-revealing, but that's provided in the source for this video. 75 | 76 | 77 | MathJax will load macros from the same file as KaTeX, you can use them side by side. 78 | 79 | 80 | And then here's the documentation for MathJax. Note this is not the latest version of MathJax. 81 |
82 | So, I usually use KaTeX for my formulas, because it's faster (partly because of the older version). But there are some complicated formulas that only MathJax can render, in particular, commutative diagrams. 83 |
84 |
85 | ); 86 | 87 | export const XyJaxPrompt = (props: P) => ( 88 | 89 | 90 | So there's an amazing MathJax package called XyJax that lets you use xymatrix in MathJax. Here's 91 | 92 | 93 | a basic commutative diagram. Now, doing effects with XyJax, even some things that would be easy to do in normal xymatrix, is really hard. It's much harder than the 2d / 3d animation that we'll cover next. But it can be done. 94 | 95 | 96 | So here are some diagrams using macros for pullback and pushout decorations. 97 | 98 | 99 | Here's a diagram with colored arrows. 100 | 101 | 102 | Here's how to animate arrows in a diagram. 103 | 104 | 105 | And then here's a fade animation: so this is supposed to be a proof of the Brouwer fixed point theorem: S^1 can't be a retract of D^2, because if it were, 106 | 107 | 108 | you could apply \pi_1 109 | 110 | 111 | and get this diagram in the category of abelian groups, which is 112 | 113 | 114 | clearly nonsensical. 115 |
116 | So that's how you do formulas; now let's talk about graphics. 117 |
118 |
119 | ); 120 | 121 | export const TwoDPrompt = (props: P) => ( 122 | 123 | 124 | Here's an example of 2d graphics, using SVG. You can move the point on the parabola and it'll show the tangent line and the equation for that tangent line. You can also set the position using the textbox. 125 | 126 | 127 | Now, one of the challenges of using this format is, most of the time clicking on the video will pause it, so how do you signify to the user which parts are interactive? 128 | 129 | 130 | Here I used an orange pulse to signify that the point is movable, and I've also provided explicit instuctions. And I've got the controls on these gray panels that won't pause the video when clicked. So you want to try to establish and stick to some visual conventions. 131 | 132 | 133 | Another thing you have to watch out for is, on mobile, it's very hard to precisely hit the circle with your finger. So you have to make a transparent circle with a much larger radius to increase the clickable area. Any time you see "fat-fingers" in the code, that's what it's referring to. 134 | 135 | 136 | ); 137 | 138 | export const ThreeDPrompt = (props: P) => ( 139 | 140 | 141 | Now for 3d graphics. Here we have an interactive scene, so the controls are listed down there. You can close the help dialog if you want. 142 | 143 | 144 | This is done using an amazing Javascript library called THREE.js, and you will definitely want to bookmark the documentation for that. 145 | 146 | 147 | And here I'm also using a library called react-three-fiber, which lets you create scenes in a much more declarative way. 148 |
149 | Anyway, 150 |
151 | 152 | here we have a cylinder. And now, 153 | 154 | 155 | here's an example of a movable object. So you can try moving the orange ring. I'll pause so you can try that out. 156 | 157 | 158 | And then here's an example of animating a shape: 159 | 160 | 161 | Then here's a parametric surface, this is a trefoil knot. 162 | 163 | 164 | Then here's an example of combining 2d formulas with 3d graphics. You render your LaTeX formulas to SVG, and then you can import them into a 3d scene. 165 | 166 |
167 | ); 168 | 169 | export const FiveDPrompt = (props: P) => ( 170 | 171 | 172 | Finally, here's an example of mixing 2d and 3d graphics. So this is supposed to be illustrating the moduli stack of elliptic curves, to first approximation. So you can adjust the values of a and b, we've really got a 4-dimensional space and then we get elliptic curves as 2-dimensional cross sections of it. This example shows how to use graph implicit equations in two or three dimensions. 173 | 174 | So hopefully that's helpful for getting up and running; I look forward to seeing what you create with this tool. 175 | 176 | 177 | ); 178 | -------------------------------------------------------------------------------- /static/style.css: -------------------------------------------------------------------------------- 1 | @import "https://unpkg.com/@liqvid/prompt@1.0.0/dist/style.min.css"; 2 | .fat-fingers { 3 | fill: transparent !important; 4 | } 5 | @media (any-hover: hover) { 6 | .fat-fingers { 7 | display: none !important; 8 | } 9 | } 10 | .rp-controls { 11 | --rp-controls-right: 4 !important; 12 | } 13 | .rp-controls-help { 14 | cursor: pointer; 15 | height: 100%; 16 | } 17 | .rp-help-dialog { 18 | background-color: rgba(0,0,0,0.85); 19 | color: #fff; 20 | font-size: 1.5rem; 21 | height: 100vh; 22 | padding: 5vh 10vh; 23 | width: 160vh; 24 | position: fixed; 25 | left: calc(50vw - 80vh); 26 | top: 0; 27 | z-index: 1001; 28 | } 29 | .rp-help-dialog > button { 30 | background: none; 31 | border: none; 32 | color: #f00; 33 | cursor: pointer; 34 | font-family: sans-serif; 35 | font-size: 5em; 36 | position: absolute; 37 | right: 2%; 38 | top: -1%; 39 | } 40 | .rp-help-tables { 41 | display: flex; 42 | justify-content: space-around; 43 | } 44 | .rp-help-tables > table { 45 | border-collapse: separate; 46 | border-spacing: 1em 1.5em; 47 | font-family: sans-serif; 48 | vertical-align: top; 49 | } 50 | .rp-help-tables > table > caption { 51 | color: #1a69b5; 52 | font-family: sans-serif; 53 | font-size: 1.2em; 54 | font-weight: bold; 55 | } 56 | .rp-help-tables > table th { 57 | font-weight: normal; 58 | text-align: right; 59 | } 60 | .rp-help-tables kbd { 61 | color: #ff0; 62 | } 63 | .lv-loading-screen { 64 | background-color: rgba(0,0,0,0.7); 65 | display: none; 66 | height: 100%; 67 | position: absolute; 68 | width: 100%; 69 | z-index: 10000; 70 | } 71 | .lv-loading-spinner { 72 | border: 16px solid #f3f3f3; 73 | border-top: 16px solid #1a69b5; 74 | border-radius: 50%; 75 | margin: 40vmin auto 0 auto; 76 | width: 20vmin; 77 | height: 20vmin; 78 | animation: spin 1s linear infinite; 79 | } 80 | .lv-player.not-ready > .lv-canvas > .lv-loading-screen { 81 | display: block; 82 | } 83 | @-moz-keyframes spin { 84 | 0% { 85 | transform: rotate(0deg); 86 | } 87 | 100% { 88 | transform: rotate(360deg); 89 | } 90 | } 91 | @-webkit-keyframes spin { 92 | 0% { 93 | transform: rotate(0deg); 94 | } 95 | 100% { 96 | transform: rotate(360deg); 97 | } 98 | } 99 | @-o-keyframes spin { 100 | 0% { 101 | transform: rotate(0deg); 102 | } 103 | 100% { 104 | transform: rotate(360deg); 105 | } 106 | } 107 | @keyframes spin { 108 | 0% { 109 | transform: rotate(0deg); 110 | } 111 | 100% { 112 | transform: rotate(360deg); 113 | } 114 | } 115 | .lv-thumbnail-time { 116 | color: #222; 117 | } 118 | .rp-canvas { 119 | background: #202020; 120 | background-image: url("MEDIA_URL/img/grid.png"); 121 | } 122 | .block { 123 | border-radius: 0.2rem; 124 | box-shadow: 0em 0.1em 0.3em #000; 125 | color: #000; 126 | fill: #000; 127 | stroke: #000; 128 | margin: 1em auto 0.5em; 129 | width: 80%; 130 | } 131 | .block > header { 132 | background: #1a69b5; 133 | border-radius: 0.2rem 0.2rem 0 0; 134 | color: #fff; 135 | font-family: "Alegreya"; 136 | font-weight: bold; 137 | padding: 0.3rem 1rem; 138 | } 139 | .block > .content { 140 | background: #fff; 141 | padding: 0.3rem 1rem; 142 | } 143 | .corollary > header::before { 144 | content: "Corollary. "; 145 | } 146 | .definition > header { 147 | background: #af1866; 148 | } 149 | .definition > header::before { 150 | content: "Definition. "; 151 | } 152 | .example > header { 153 | background: #008000; 154 | } 155 | .example > header::before { 156 | content: "Example. "; 157 | } 158 | .lemma > header::before { 159 | content: "Lemma. "; 160 | } 161 | .remark > header::before { 162 | content: "Remark. "; 163 | } 164 | .proof > header::before { 165 | content: "Proof. "; 166 | } 167 | .proposition > header::before { 168 | content: "Proposition. "; 169 | } 170 | .exercise > header { 171 | background: #ff7000; 172 | } 173 | .exercise > header::before { 174 | content: "Exercise. "; 175 | } 176 | .theorem > header::before { 177 | content: "Theorem. "; 178 | } 179 | dfn { 180 | font-weight: bold; 181 | } 182 | .toc { 183 | font-family: "KaTeX_Main"; 184 | font-size: 2em; 185 | margin: 1em auto; 186 | width: 65%; 187 | } 188 | .toc > tbody > tr { 189 | cursor: pointer; 190 | } 191 | .toc > tbody > tr > th { 192 | text-align: left; 193 | padding-right: 0.5em; 194 | } 195 | .toc > tbody > tr > td { 196 | padding: 0.25em 0; 197 | } 198 | .toc > tbody > tr > .name { 199 | padding-right: 5em; 200 | } 201 | #sec-intro > .toc { 202 | position: absolute; 203 | top: 1.5em; 204 | width: 65%; 205 | left: 17.5%; 206 | } 207 | #sec-intro > section { 208 | position: absolute; 209 | width: 80%; 210 | top: 5em; 211 | left: 10%; 212 | background: #fff; 213 | border-radius: 0.2em; 214 | font-size: 1.2em; 215 | padding: 1em; 216 | height: auto; 217 | } 218 | #sec-intro > section ul { 219 | margin: 0.5em 0 0.5em 2em; 220 | } 221 | #sec-intro > section ul > li { 222 | margin: 0.5em 0; 223 | } 224 | #sec-intro > section > p:not(:first-child) { 225 | margin: 1em 0; 226 | } 227 | .time { 228 | text-align: right; 229 | } 230 | .toc :link { 231 | color: inherit; 232 | } 233 | .time > :link { 234 | outline: 1px solid #1a69b5; 235 | } 236 | #sec-katex > .block > .content > p { 237 | margin: 0.5em 0; 238 | } 239 | #sec-mathjax > .block > .content { 240 | font-size: 0.95em; 241 | } 242 | #sec-mathjax > .block > .content .MathJax { 243 | font-size: 0.8em !important; 244 | } 245 | #xyjax-demos { 246 | font-size: 2em; 247 | text-align: center; 248 | position: relative; 249 | top: 1em; 250 | } 251 | #xyjax-demos > * { 252 | left: 0; 253 | top: 0; 254 | position: absolute !important; 255 | text-align: center; 256 | width: 100%; 257 | } 258 | .contradiction * { 259 | color: #f00; 260 | fill: #f00; 261 | stroke: #f00; 262 | } 263 | #sec-2d { 264 | --usable-height: calc(var(--rp-height) - var(--rp-controls-height)); 265 | } 266 | #tangent-demo { 267 | background: #dbdbdb; 268 | border-radius: 0.2em; 269 | position: absolute; 270 | left: calc(0.05 * var(--usable-height)); 271 | top: calc(0.1 * var(--usable-height)); 272 | height: calc(0.8 * var(--usable-height)); 273 | } 274 | .glow { 275 | stroke: #ff7000; 276 | stroke-width: 0.035; 277 | } 278 | .plot { 279 | color: #1a69b5; 280 | fill: none; 281 | stroke: #1a69b5; 282 | stroke-width: 0.05; 283 | } 284 | .point-A { 285 | cursor: grab; 286 | fill: #af1866; 287 | stroke-width: 0; 288 | } 289 | #defn .MJXp-mtable > span > span:first-child { 290 | color: #ae81ff; 291 | } 292 | #defn .MJXp-mtable > span > span { 293 | color: #ffa500; 294 | } 295 | span.tangent-line { 296 | color: #ffa500; 297 | } 298 | .tangent-line { 299 | stroke: #ffa500; 300 | stroke-width: 0.05; 301 | } 302 | #explain { 303 | background: #dbdbdb; 304 | border-radius: 0.2em; 305 | right: calc(0.05 * var(--usable-height)); 306 | top: calc(0.1 * var(--usable-height)); 307 | padding: 0.5em; 308 | width: calc(0.75 * var(--usable-height)); 309 | position: absolute; 310 | } 311 | #explain > table { 312 | margin: -1em auto 2em; 313 | } 314 | #explain > h3 { 315 | margin: 2em 0 0; 316 | } 317 | #explain > ul { 318 | margin-left: 2em; 319 | } 320 | #explain > ul > li { 321 | margin: 0.5em 0; 322 | } 323 | .pt1 { 324 | color: #af1866; 325 | } 326 | .gridline { 327 | stroke: #666; 328 | stroke-width: 0.01; 329 | } 330 | .major-axis { 331 | stroke: #000; 332 | stroke-width: 0.08; 333 | } 334 | .axis-label { 335 | fill: #000; 336 | font-size: 0.04ex; 337 | text-anchor: middle; 338 | dominant-baseline: middle; 339 | stroke: none; 340 | } 341 | .axis-tick { 342 | stroke: #777; 343 | stroke-width: 0.05; 344 | } 345 | #three-d { 346 | background: #333; 347 | background-image: url("https://d2og9lpzrymesl.cloudfront.net/r/rp-tutorial-math/img/grid.png"); 348 | height: 100%; 349 | width: 100%; 350 | } 351 | #three-d > figcaption { 352 | background: rgba(0,0,0,0.333); 353 | border-radius: 0.5rem 0.5rem 0 0; 354 | font-size: 2rem; 355 | height: 3rem; 356 | color: #ff7000; 357 | vertical-align: middle; 358 | line-height: 3rem; 359 | position: absolute; 360 | text-align: center; 361 | top: 0; 362 | width: 100%; 363 | z-index: 1000; 364 | } 365 | #three-explain { 366 | background: rgba(255,255,255,0.878); 367 | width: 100%; 368 | font-size: 1.5em; 369 | padding: 0.5em; 370 | position: absolute; 371 | left: 0; 372 | top: 0; 373 | z-index: 1000; 374 | } 375 | #three-explain > p { 376 | margin: 0.5em 0; 377 | } 378 | .rp-3d-help-wrap { 379 | position: relative; 380 | z-index: 10000; 381 | } 382 | .rp-3d-help-wrap:not(.relevant) { 383 | opacity: 0; 384 | pointer-events: none; 385 | } 386 | .rp-3d-help-wrap.open { 387 | background: #3f3f3f; 388 | } 389 | .rp-3d-help-wrap.open > .rp-3d-help { 390 | display: block; 391 | } 392 | .rp-3d-help { 393 | background: #404040; 394 | border-radius: 0.2em 0.2em 0 0; 395 | box-shadow: 0.9px -1.2px 2px rgba(0,0,0,0.133); 396 | display: none; 397 | position: absolute; 398 | bottom: calc(var(--rp-controls-height) - var(--rp-scrub-height)); 399 | font-size: 1.2em; 400 | right: 0; 401 | width: 15em; 402 | padding: 0.5em; 403 | z-index: 1000; 404 | } 405 | .rp-3d-help > table { 406 | border-collapse: separate; 407 | border-spacing: 1em 0.5em; 408 | margin: 0 auto; 409 | vertical-align: top; 410 | } 411 | .rp-3d-help > table > caption { 412 | color: #1bbb68; 413 | font-size: 1.2em; 414 | font-weight: bold; 415 | } 416 | .rp-3d-help > table > tbody > tr > th { 417 | color: #e6db74; 418 | text-align: left; 419 | } 420 | #elliptic { 421 | background: #ccc; 422 | border-radius: 0.2em; 423 | width: 35%; 424 | height: calc(0.35 * var(--rp-width)); 425 | position: absolute; 426 | left: 5%; 427 | top: 5%; 428 | } 429 | #elliptic line { 430 | stroke: #000; 431 | } 432 | #moduli { 433 | background: #ccc; 434 | border-radius: 0.2em; 435 | width: 35%; 436 | height: calc(0.35 * var(--rp-width)); 437 | position: absolute; 438 | right: 5%; 439 | top: 5%; 440 | } 441 | #fived-controls { 442 | background: #ccc; 443 | border-radius: 0.2em; 444 | bottom: calc(var(--rp-controls-height) + 2.5%); 445 | font-size: 1.5em; 446 | padding: 0.5em; 447 | position: absolute; 448 | left: calc((100% - 30%) / 2); 449 | width: 30%; 450 | } 451 | #fived-controls > table { 452 | margin: 0 auto; 453 | } 454 | #fived-controls input[type="number"] { 455 | font-size: 1em; 456 | width: 6ch; 457 | } 458 | h1 { 459 | font-family: "KaTeX_Main"; 460 | font-size: 2em; 461 | position: absolute; 462 | top: 0.5em; 463 | left: 0; 464 | text-align: center; 465 | width: 100%; 466 | } 467 | h2 { 468 | color: #1a69b5; 469 | font-family: "Alegreya"; 470 | font-size: 4rem; 471 | margin-top: 0.5em; 472 | text-align: center; 473 | } 474 | .lv-canvas { 475 | background: #eee; 476 | } 477 | body { 478 | font-family: sans-serif; 479 | } 480 | .draggable { 481 | cursor: grab; 482 | cursor: -webkit-grab; 483 | } 484 | body.dragging .rp-canvas, 485 | body.dragging .draggable, 486 | .dragging { 487 | cursor: grabbing; 488 | cursor: -webkit-grabbing; 489 | } 490 | section { 491 | font-size: 1.5rem; 492 | height: 100%; 493 | left: 0; 494 | position: absolute; 495 | top: 0; 496 | width: 100%; 497 | } 498 | .rp-prompt { 499 | color: #fff; 500 | font-size: 1rem; 501 | } 502 | :link, 503 | :visited { 504 | color: #1a69b5; 505 | } 506 | .rp-cursor { 507 | width: 3rem; 508 | } 509 | /* general styles */ 510 | .katex { 511 | color: inherit !important; 512 | font-size: 100% !important; 513 | } 514 | section { 515 | color: #000; 516 | stroke: #000; 517 | fill: none; 518 | } 519 | .MathJax svg > g { 520 | fill: inherit; 521 | stroke: inherit; 522 | } 523 | .block > .content > p { 524 | margin: 0.5em 0; 525 | } 526 | -------------------------------------------------------------------------------- /pep@0.5.1.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * PEP v0.5.1 | https://github.com/jquery/PEP 3 | * Copyright jQuery Foundation and other contributors | http://jquery.org/license 4 | */ 5 | !function(t){var e={};function n(r){if(e[r])return e[r].exports;var i=e[r]={i:r,l:!1,exports:{}};return t[r].call(i.exports,i,i.exports,n),i.l=!0,i.exports}n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var i in t)n.d(r,i,function(e){return t[e]}.bind(null,i));return r},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=0)}([function(t,e,n){"use strict";n.r(e);n(1)},function(t,e,n){"use strict"; 6 | t.exports=function(){var t=["bubbles","cancelable","view","screenX","screenY","clientX","clientY","ctrlKey","altKey","shiftKey","metaKey","button","relatedTarget","pageX","pageY"],e=[!1,!1,null,0,0,0,0,!1,!1,!1,!1,0,null,0,0];function n(n,r){r=r||Object.create(null);var i=document.createEvent("Event");i.initEvent(n,r.bubbles||!1,r.cancelable||!1);for(var o,a=2;a0){var e=this.events;t.addEventListener("touchend",(function(){t._scrollType=void 0,c.unlisten(t,e)}))}else t._scrollType=void 0,c.unlisten(t,this.events);R(t).forEach((function(t){t._scrollType=void 0,c.unlisten(t,this.events)}),this)},elementChanged:function(t,e){var n=t.getAttribute("touch-action"),r=this.touchActionToScrollType(n),i=this.touchActionToScrollType(e);"number"==typeof r&&"number"==typeof i?(t._scrollType=r,R(t).forEach((function(t){t._scrollType=r}),this)):"number"==typeof i?this.elementRemoved(t):"number"==typeof r&&this.elementAdded(t)},scrollTypes:{UP:function(t){return t.includes("pan-y")||t.includes("pan-up")?1:0},DOWN:function(t){return t.includes("pan-y")||t.includes("pan-down")?2:0},LEFT:function(t){return t.includes("pan-x")||t.includes("pan-left")?4:0},RIGHT:function(t){return t.includes("pan-x")||t.includes("pan-right")?8:0}},touchActionToScrollType:function(t){if(t){if("auto"===t)return 15;if("none"===t)return 0;var e=t.split(" "),n=this.scrollTypes;return n.UP(e)|n.DOWN(e)|n.LEFT(e)|n.RIGHT(e)}},POINTER_TYPE:"touch",firstTouch:null,isPrimaryTouch:function(t){return this.firstTouch===t.identifier},setPrimaryTouch:function(t){(0===D.size||1===D.size&&D.has(1))&&(this.firstTouch=t.identifier,this.firstXY={X:t.clientX,Y:t.clientY},this.scrolling=!1)},removePrimaryPointer:function(t){t.isPrimary&&(this.firstTouch=null,this.firstXY=null)},typeToButtons:function(t){var e=0;return"touchstart"!==t&&"touchmove"!==t&&"touchforcechange"!==t||(e=1),e},touchToPointer:function(t){var e=this.currentTouchEvent,n=c.cloneEvent(t),r=n.pointerId=t.identifier+2;if(n.target=_[r]||N(n),n.bubbles=!0,n.cancelable=!0,n.button=0,n.buttons=this.typeToButtons(e.type),n.width=2*(t.radiusX||t.webkitRadiusX||0),n.height=2*(t.radiusY||t.webkitRadiusY||0),n.pressure=void 0!==t.force?t.force:void 0!==t.webkitForce?t.webkitForce:void 0,n.isPrimary=this.isPrimaryTouch(t),t.altitudeAngle){const e=Math.tan(t.altitudeAngle),r=180/Math.PI;n.tiltX=Math.atan(Math.cos(t.azimuthAngle)/e)*r,n.tiltY=Math.atan(Math.sin(t.azimuthAngle)/e)*r}else n.tiltX=0,n.tiltY=0;"stylus"===t.touchType?n.pointerType="pen":n.pointerType=this.POINTER_TYPE,n.altKey=e.altKey,n.ctrlKey=e.ctrlKey,n.metaKey=e.metaKey,n.shiftKey=e.shiftKey;var i=this;return n.preventDefault=function(){i.scrolling=!1,i.firstXY=null,e.preventDefault()},n},processTouches:function(t,e){var n=t.changedTouches;this.currentTouchEvent=t;for(var r,i=0;io:l?e=u>o&&a>0:h&&(e=u>o&&a<0),e||(s&&c?e=u0:c&&(e=u=e.length){var n=[];D.forEach((function(t,r){if(1!==r&&!this.findTouch(e,r-2)){var i=t.out;n.push(i)}}),this),n.forEach(this.cancelOut,this)}},touchstart:function(t){this.vacuumTouches(t),this.setPrimaryTouch(t.changedTouches[0]),this.dedupSynthMouse(t),this.scrolling||this.processTouches(t,this.overDown)},overDown:function(t){D.set(t.pointerId,{target:t.target,out:t,outTarget:t.target}),c.enterOver(t),c.down(t)},touchforcechange:function(t){this.touchmove(t)},touchmove:function(t){this.scrolling||(this.shouldScroll(t)?(this.scrolling=!0,this.touchcancel(t)):(t.preventDefault(),this.processTouches(t,this.moveOverOut)))},moveOverOut:function(t){var e=t,n=D.get(e.pointerId);if(n){var r=n.out,i=n.outTarget;c.move(e),r&&i!==e.target&&(r.relatedTarget=e.target,e.relatedTarget=i,r.target=i,e.target?(c.leaveOut(r),c.enterOver(e)):(e.target=i,e.relatedTarget=null,this.cancelOut(e))),n.out=e,n.outTarget=e.target}},touchend:function(t){this.dedupSynthMouse(t),this.processTouches(t,this.upOut)},upOut:function(t){this.scrolling||(c.up(t),c.leaveOut(t)),this.cleanUpPointer(t)},touchcancel:function(t){this.processTouches(t,this.cancelOut)},cancelOut:function(t){c.cancel(t),c.leaveOut(t),this.cleanUpPointer(t)},cleanUpPointer:function(t){D.delete(t.pointerId),this.removePrimaryPointer(t)},dedupSynthMouse:function(t){var e=I.lastTouches,n=t.changedTouches[0];if(this.isPrimaryTouch(n)){var r={x:n.clientX,y:n.clientY};e.push(r);var i=function(t,e){var n=t.indexOf(e);n>-1&&t.splice(n,1)}.bind(null,e,r);setTimeout(i,2500)}}};M=new b(C.elementAdded,C.elementRemoved,C.elementChanged,C);var Y,X,F,A=c.pointermap,L=window.MSPointerEvent&&"number"==typeof window.MSPointerEvent.MSPOINTER_TYPE_MOUSE,k={events:["MSPointerDown","MSPointerMove","MSPointerUp","MSPointerOut","MSPointerOver","MSPointerCancel","MSGotPointerCapture","MSLostPointerCapture"],register:function(t){c.listen(t,this.events)},unregister:function(t){c.unlisten(t,this.events)},POINTER_TYPES:["","unavailable","touch","pen","mouse"],prepareEvent:function(t){var e=t;return L&&((e=c.cloneEvent(t)).pointerType=this.POINTER_TYPES[t.pointerType]),e},cleanup:function(t){A.delete(t)},MSPointerDown:function(t){A.set(t.pointerId,t);var e=this.prepareEvent(t);c.down(e)},MSPointerMove:function(t){var e=this.prepareEvent(t);c.move(e)},MSPointerUp:function(t){var e=this.prepareEvent(t);c.up(e),this.cleanup(t.pointerId)},MSPointerOut:function(t){var e=this.prepareEvent(t);c.leaveOut(e)},MSPointerOver:function(t){var e=this.prepareEvent(t);c.enterOver(e)},MSPointerCancel:function(t){var e=this.prepareEvent(t);c.cancel(e),this.cleanup(t.pointerId)},MSLostPointerCapture:function(t){var e=c.makeEvent("lostpointercapture",t);c.dispatchEvent(e)},MSGotPointerCapture:function(t){var e=c.makeEvent("gotpointercapture",t);c.dispatchEvent(e)}};function x(t){if(!c.pointermap.has(t)){var e=new Error("NotFoundError");throw e.name="NotFoundError",e}}function K(t){for(var e=t.parentNode;e&&e!==t.ownerDocument;)e=e.parentNode;if(!e){var n=new Error("InvalidStateError");throw n.name="InvalidStateError",n}}function j(t){return 0!==c.pointermap.get(t).buttons}return window.navigator.msPointerEnabled?(Y=function(t){x(t),K(this),j(t)&&(c.setCapture(t,this,!0),this.msSetPointerCapture(t))},X=function(t){x(t),c.releaseCapture(t,!0),this.msReleasePointerCapture(t)}):(Y=function(t){x(t),K(this),j(t)&&c.setCapture(t,this)},X=function(t){x(t),c.releaseCapture(t)}),F=function(t){return!!c.captureInfo[t]},function(){if(T){g.forEach((function(t){y+=t.selector+E(t.value)+"\n",w&&(y+=function(t){return"body /shadow-deep/ "+t}(t.selector)+E(t.value)+"\n")}));var t=document.createElement("style");t.textContent=y,document.head.appendChild(t)}}(),function(){if(!window.PointerEvent){if(window.PointerEvent=n,window.navigator.msPointerEnabled){var t=window.navigator.msMaxTouchPoints;Object.defineProperty(window.navigator,"maxTouchPoints",{value:t,enumerable:!0}),c.registerSource("ms",k)}else Object.defineProperty(window.navigator,"maxTouchPoints",{value:0,enumerable:!0}),c.registerSource("mouse",I),void 0!==window.ontouchstart&&c.registerSource("touch",C);c.register(document)}}(),window.Element&&!Element.prototype.setPointerCapture&&Object.defineProperties(Element.prototype,{setPointerCapture:{value:Y},releasePointerCapture:{value:X},hasPointerCapture:{value:F}}),{dispatcher:c,Installer:b,PointerEvent:n,PointerMap:r,targetFinding:l}}()}]); -------------------------------------------------------------------------------- /lib/graphics.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | 3 | type Fn = (x: number, y: number, z: number) => number; 4 | 5 | type Vector2 = [number, number]; 6 | type OneForm = (x: number, y: number, z: number) => Vector2; 7 | type Matrix2 = [Vector2, Vector2]; 8 | 9 | /* marching squares */ 10 | /* taken from https://stemkoski.github.io/MathBox/graph2d-implicit.html */ 11 | 12 | // returns an array of endpoints of edges. c = zLevel, i.e. isocline value. 13 | // z = zFunc(x,y). generating the level set where z = c. 14 | export function marchingSquares( 15 | xMin: number, 16 | xMax: number, 17 | yMin: number, 18 | yMax: number, 19 | zFunc: (x: number, y: number) => number, 20 | c: number, 21 | resolution: number 22 | ) { 23 | const xStep = (xMax - xMin) / resolution; 24 | const yStep = (yMax - yMin) / resolution; 25 | const points = []; 26 | for (let x = xMin; x < xMax; x += xStep) 27 | { 28 | for (let y = yMin; y < yMax; y += yStep) 29 | { 30 | const z1 = zFunc(x,y); // bottom left corner 31 | const z2 = zFunc(x+xStep, y); // bottom right corner 32 | const z4 = zFunc(x+xStep, y+yStep); // top right corner 33 | const z8 = zFunc(x, y+yStep); // top left corner 34 | let n = 0; 35 | if (z1 > c) n += 1; 36 | if (z2 > c) n += 2; 37 | if (z4 > c) n += 4; 38 | if (z8 > c) n += 8; 39 | 40 | // calculate linear interpolation values along the given sides. 41 | // to simplify, could assume each is 0.5*xStep or 0.5*yStep accordingly. 42 | const bottomInterp = (c - z1) / (z2 - z1) * xStep; 43 | const topInterp = (c - z8) / (z4 - z8) * xStep; 44 | const leftInterp = (c - z1) / (z8 - z1) * yStep; 45 | const rightInterp = (c - z2) / (z4 - z2) * yStep; 46 | 47 | // for a visual diagram of cases: https://en.wikipedia.org/wiki/Marching_squares 48 | if (n == 1 || n == 14) // lower left corner 49 | points.push( [x, y+leftInterp, c], [x+bottomInterp, y, c] ); 50 | 51 | else if (n == 2 || n == 13) // lower right corner 52 | points.push( [x+bottomInterp, y, c], [x+xStep, y+rightInterp, c] ); 53 | 54 | else if (n == 4 || n == 11) // upper right corner 55 | points.push( [x+topInterp, y+yStep, c], [x+xStep, y+rightInterp, c] ); 56 | 57 | else if (n == 8 || n == 7) // upper left corner 58 | points.push( [x, y+leftInterp, c], [x+topInterp, y+yStep, c] ); 59 | 60 | else if (n == 3 || n == 12) // horizontal 61 | points.push( [x, y+leftInterp, c], [x+xStep, y+rightInterp, c] ); 62 | 63 | else if (n == 6 || n == 9) // vertical 64 | points.push( [x+bottomInterp, y, c], [x+topInterp, y+yStep, c] ); 65 | 66 | else if (n == 5) // should do subcase // lower left & upper right 67 | points.push( [x, y+leftInterp, c], [x+bottomInterp, y, c], [x+topInterp, y+yStep, c], [x+xStep, y+rightInterp, c] ); 68 | 69 | else if (n == 10) // should do subcase // lower right & upper left 70 | points.push( [x+bottomInterp, y, c], [x+xStep, y+rightInterp, c], [x, y+yStep/2, c], [x, y+leftInterp, c], [x+topInterp, y+yStep, c] ); 71 | 72 | else if (n == 0 || n == 15) // no line segments appear in this grid square. 73 | points.push(); 74 | 75 | } 76 | } 77 | return points; 78 | 79 | } 80 | /** 81 | * @author alteredq / http://alteredqualia.com/ 82 | * 83 | * Port of greggman's ThreeD version of marching cubes to Three.js 84 | * http://webglsamples.googlecode.com/hg/blob/blob.html 85 | */ 86 | 87 | ///////////////////////////////////// 88 | // Marching cubes lookup tables 89 | ///////////////////////////////////// 90 | 91 | // These tables are straight from Paul Bourke's page: 92 | // http://local.wasp.uwa.edu.au/~pbourke/geometry/polygonise/ 93 | // who in turn got them from Cory Gene Bloyd. 94 | 95 | const edgeTable = new Int32Array([ 96 | 0x0 , 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c, 97 | 0x80c, 0x905, 0xa0f, 0xb06, 0xc0a, 0xd03, 0xe09, 0xf00, 98 | 0x190, 0x99 , 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c, 99 | 0x99c, 0x895, 0xb9f, 0xa96, 0xd9a, 0xc93, 0xf99, 0xe90, 100 | 0x230, 0x339, 0x33 , 0x13a, 0x636, 0x73f, 0x435, 0x53c, 101 | 0xa3c, 0xb35, 0x83f, 0x936, 0xe3a, 0xf33, 0xc39, 0xd30, 102 | 0x3a0, 0x2a9, 0x1a3, 0xaa , 0x7a6, 0x6af, 0x5a5, 0x4ac, 103 | 0xbac, 0xaa5, 0x9af, 0x8a6, 0xfaa, 0xea3, 0xda9, 0xca0, 104 | 0x460, 0x569, 0x663, 0x76a, 0x66 , 0x16f, 0x265, 0x36c, 105 | 0xc6c, 0xd65, 0xe6f, 0xf66, 0x86a, 0x963, 0xa69, 0xb60, 106 | 0x5f0, 0x4f9, 0x7f3, 0x6fa, 0x1f6, 0xff , 0x3f5, 0x2fc, 107 | 0xdfc, 0xcf5, 0xfff, 0xef6, 0x9fa, 0x8f3, 0xbf9, 0xaf0, 108 | 0x650, 0x759, 0x453, 0x55a, 0x256, 0x35f, 0x55 , 0x15c, 109 | 0xe5c, 0xf55, 0xc5f, 0xd56, 0xa5a, 0xb53, 0x859, 0x950, 110 | 0x7c0, 0x6c9, 0x5c3, 0x4ca, 0x3c6, 0x2cf, 0x1c5, 0xcc , 111 | 0xfcc, 0xec5, 0xdcf, 0xcc6, 0xbca, 0xac3, 0x9c9, 0x8c0, 112 | 0x8c0, 0x9c9, 0xac3, 0xbca, 0xcc6, 0xdcf, 0xec5, 0xfcc, 113 | 0xcc , 0x1c5, 0x2cf, 0x3c6, 0x4ca, 0x5c3, 0x6c9, 0x7c0, 114 | 0x950, 0x859, 0xb53, 0xa5a, 0xd56, 0xc5f, 0xf55, 0xe5c, 115 | 0x15c, 0x55 , 0x35f, 0x256, 0x55a, 0x453, 0x759, 0x650, 116 | 0xaf0, 0xbf9, 0x8f3, 0x9fa, 0xef6, 0xfff, 0xcf5, 0xdfc, 117 | 0x2fc, 0x3f5, 0xff , 0x1f6, 0x6fa, 0x7f3, 0x4f9, 0x5f0, 118 | 0xb60, 0xa69, 0x963, 0x86a, 0xf66, 0xe6f, 0xd65, 0xc6c, 119 | 0x36c, 0x265, 0x16f, 0x66 , 0x76a, 0x663, 0x569, 0x460, 120 | 0xca0, 0xda9, 0xea3, 0xfaa, 0x8a6, 0x9af, 0xaa5, 0xbac, 121 | 0x4ac, 0x5a5, 0x6af, 0x7a6, 0xaa , 0x1a3, 0x2a9, 0x3a0, 122 | 0xd30, 0xc39, 0xf33, 0xe3a, 0x936, 0x83f, 0xb35, 0xa3c, 123 | 0x53c, 0x435, 0x73f, 0x636, 0x13a, 0x33 , 0x339, 0x230, 124 | 0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c, 125 | 0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x99 , 0x190, 126 | 0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c, 127 | 0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x0 128 | ]); 129 | 130 | const triTable = new Int32Array([ 131 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 132 | 0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 133 | 0, 1, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 134 | 1, 8, 3, 9, 8, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 135 | 1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 136 | 0, 8, 3, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 137 | 9, 2, 10, 0, 2, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 138 | 2, 8, 3, 2, 10, 8, 10, 9, 8, -1, -1, -1, -1, -1, -1, -1, 139 | 3, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 140 | 0, 11, 2, 8, 11, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 141 | 1, 9, 0, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 142 | 1, 11, 2, 1, 9, 11, 9, 8, 11, -1, -1, -1, -1, -1, -1, -1, 143 | 3, 10, 1, 11, 10, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 144 | 0, 10, 1, 0, 8, 10, 8, 11, 10, -1, -1, -1, -1, -1, -1, -1, 145 | 3, 9, 0, 3, 11, 9, 11, 10, 9, -1, -1, -1, -1, -1, -1, -1, 146 | 9, 8, 10, 10, 8, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 147 | 4, 7, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 148 | 4, 3, 0, 7, 3, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 149 | 0, 1, 9, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 150 | 4, 1, 9, 4, 7, 1, 7, 3, 1, -1, -1, -1, -1, -1, -1, -1, 151 | 1, 2, 10, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 152 | 3, 4, 7, 3, 0, 4, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1, 153 | 9, 2, 10, 9, 0, 2, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, 154 | 2, 10, 9, 2, 9, 7, 2, 7, 3, 7, 9, 4, -1, -1, -1, -1, 155 | 8, 4, 7, 3, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 156 | 11, 4, 7, 11, 2, 4, 2, 0, 4, -1, -1, -1, -1, -1, -1, -1, 157 | 9, 0, 1, 8, 4, 7, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, 158 | 4, 7, 11, 9, 4, 11, 9, 11, 2, 9, 2, 1, -1, -1, -1, -1, 159 | 3, 10, 1, 3, 11, 10, 7, 8, 4, -1, -1, -1, -1, -1, -1, -1, 160 | 1, 11, 10, 1, 4, 11, 1, 0, 4, 7, 11, 4, -1, -1, -1, -1, 161 | 4, 7, 8, 9, 0, 11, 9, 11, 10, 11, 0, 3, -1, -1, -1, -1, 162 | 4, 7, 11, 4, 11, 9, 9, 11, 10, -1, -1, -1, -1, -1, -1, -1, 163 | 9, 5, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 164 | 9, 5, 4, 0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 165 | 0, 5, 4, 1, 5, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 166 | 8, 5, 4, 8, 3, 5, 3, 1, 5, -1, -1, -1, -1, -1, -1, -1, 167 | 1, 2, 10, 9, 5, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 168 | 3, 0, 8, 1, 2, 10, 4, 9, 5, -1, -1, -1, -1, -1, -1, -1, 169 | 5, 2, 10, 5, 4, 2, 4, 0, 2, -1, -1, -1, -1, -1, -1, -1, 170 | 2, 10, 5, 3, 2, 5, 3, 5, 4, 3, 4, 8, -1, -1, -1, -1, 171 | 9, 5, 4, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 172 | 0, 11, 2, 0, 8, 11, 4, 9, 5, -1, -1, -1, -1, -1, -1, -1, 173 | 0, 5, 4, 0, 1, 5, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, 174 | 2, 1, 5, 2, 5, 8, 2, 8, 11, 4, 8, 5, -1, -1, -1, -1, 175 | 10, 3, 11, 10, 1, 3, 9, 5, 4, -1, -1, -1, -1, -1, -1, -1, 176 | 4, 9, 5, 0, 8, 1, 8, 10, 1, 8, 11, 10, -1, -1, -1, -1, 177 | 5, 4, 0, 5, 0, 11, 5, 11, 10, 11, 0, 3, -1, -1, -1, -1, 178 | 5, 4, 8, 5, 8, 10, 10, 8, 11, -1, -1, -1, -1, -1, -1, -1, 179 | 9, 7, 8, 5, 7, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 180 | 9, 3, 0, 9, 5, 3, 5, 7, 3, -1, -1, -1, -1, -1, -1, -1, 181 | 0, 7, 8, 0, 1, 7, 1, 5, 7, -1, -1, -1, -1, -1, -1, -1, 182 | 1, 5, 3, 3, 5, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 183 | 9, 7, 8, 9, 5, 7, 10, 1, 2, -1, -1, -1, -1, -1, -1, -1, 184 | 10, 1, 2, 9, 5, 0, 5, 3, 0, 5, 7, 3, -1, -1, -1, -1, 185 | 8, 0, 2, 8, 2, 5, 8, 5, 7, 10, 5, 2, -1, -1, -1, -1, 186 | 2, 10, 5, 2, 5, 3, 3, 5, 7, -1, -1, -1, -1, -1, -1, -1, 187 | 7, 9, 5, 7, 8, 9, 3, 11, 2, -1, -1, -1, -1, -1, -1, -1, 188 | 9, 5, 7, 9, 7, 2, 9, 2, 0, 2, 7, 11, -1, -1, -1, -1, 189 | 2, 3, 11, 0, 1, 8, 1, 7, 8, 1, 5, 7, -1, -1, -1, -1, 190 | 11, 2, 1, 11, 1, 7, 7, 1, 5, -1, -1, -1, -1, -1, -1, -1, 191 | 9, 5, 8, 8, 5, 7, 10, 1, 3, 10, 3, 11, -1, -1, -1, -1, 192 | 5, 7, 0, 5, 0, 9, 7, 11, 0, 1, 0, 10, 11, 10, 0, -1, 193 | 11, 10, 0, 11, 0, 3, 10, 5, 0, 8, 0, 7, 5, 7, 0, -1, 194 | 11, 10, 5, 7, 11, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 195 | 10, 6, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 196 | 0, 8, 3, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 197 | 9, 0, 1, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 198 | 1, 8, 3, 1, 9, 8, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, 199 | 1, 6, 5, 2, 6, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 200 | 1, 6, 5, 1, 2, 6, 3, 0, 8, -1, -1, -1, -1, -1, -1, -1, 201 | 9, 6, 5, 9, 0, 6, 0, 2, 6, -1, -1, -1, -1, -1, -1, -1, 202 | 5, 9, 8, 5, 8, 2, 5, 2, 6, 3, 2, 8, -1, -1, -1, -1, 203 | 2, 3, 11, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 204 | 11, 0, 8, 11, 2, 0, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1, 205 | 0, 1, 9, 2, 3, 11, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, 206 | 5, 10, 6, 1, 9, 2, 9, 11, 2, 9, 8, 11, -1, -1, -1, -1, 207 | 6, 3, 11, 6, 5, 3, 5, 1, 3, -1, -1, -1, -1, -1, -1, -1, 208 | 0, 8, 11, 0, 11, 5, 0, 5, 1, 5, 11, 6, -1, -1, -1, -1, 209 | 3, 11, 6, 0, 3, 6, 0, 6, 5, 0, 5, 9, -1, -1, -1, -1, 210 | 6, 5, 9, 6, 9, 11, 11, 9, 8, -1, -1, -1, -1, -1, -1, -1, 211 | 5, 10, 6, 4, 7, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 212 | 4, 3, 0, 4, 7, 3, 6, 5, 10, -1, -1, -1, -1, -1, -1, -1, 213 | 1, 9, 0, 5, 10, 6, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, 214 | 10, 6, 5, 1, 9, 7, 1, 7, 3, 7, 9, 4, -1, -1, -1, -1, 215 | 6, 1, 2, 6, 5, 1, 4, 7, 8, -1, -1, -1, -1, -1, -1, -1, 216 | 1, 2, 5, 5, 2, 6, 3, 0, 4, 3, 4, 7, -1, -1, -1, -1, 217 | 8, 4, 7, 9, 0, 5, 0, 6, 5, 0, 2, 6, -1, -1, -1, -1, 218 | 7, 3, 9, 7, 9, 4, 3, 2, 9, 5, 9, 6, 2, 6, 9, -1, 219 | 3, 11, 2, 7, 8, 4, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1, 220 | 5, 10, 6, 4, 7, 2, 4, 2, 0, 2, 7, 11, -1, -1, -1, -1, 221 | 0, 1, 9, 4, 7, 8, 2, 3, 11, 5, 10, 6, -1, -1, -1, -1, 222 | 9, 2, 1, 9, 11, 2, 9, 4, 11, 7, 11, 4, 5, 10, 6, -1, 223 | 8, 4, 7, 3, 11, 5, 3, 5, 1, 5, 11, 6, -1, -1, -1, -1, 224 | 5, 1, 11, 5, 11, 6, 1, 0, 11, 7, 11, 4, 0, 4, 11, -1, 225 | 0, 5, 9, 0, 6, 5, 0, 3, 6, 11, 6, 3, 8, 4, 7, -1, 226 | 6, 5, 9, 6, 9, 11, 4, 7, 9, 7, 11, 9, -1, -1, -1, -1, 227 | 10, 4, 9, 6, 4, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 228 | 4, 10, 6, 4, 9, 10, 0, 8, 3, -1, -1, -1, -1, -1, -1, -1, 229 | 10, 0, 1, 10, 6, 0, 6, 4, 0, -1, -1, -1, -1, -1, -1, -1, 230 | 8, 3, 1, 8, 1, 6, 8, 6, 4, 6, 1, 10, -1, -1, -1, -1, 231 | 1, 4, 9, 1, 2, 4, 2, 6, 4, -1, -1, -1, -1, -1, -1, -1, 232 | 3, 0, 8, 1, 2, 9, 2, 4, 9, 2, 6, 4, -1, -1, -1, -1, 233 | 0, 2, 4, 4, 2, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 234 | 8, 3, 2, 8, 2, 4, 4, 2, 6, -1, -1, -1, -1, -1, -1, -1, 235 | 10, 4, 9, 10, 6, 4, 11, 2, 3, -1, -1, -1, -1, -1, -1, -1, 236 | 0, 8, 2, 2, 8, 11, 4, 9, 10, 4, 10, 6, -1, -1, -1, -1, 237 | 3, 11, 2, 0, 1, 6, 0, 6, 4, 6, 1, 10, -1, -1, -1, -1, 238 | 6, 4, 1, 6, 1, 10, 4, 8, 1, 2, 1, 11, 8, 11, 1, -1, 239 | 9, 6, 4, 9, 3, 6, 9, 1, 3, 11, 6, 3, -1, -1, -1, -1, 240 | 8, 11, 1, 8, 1, 0, 11, 6, 1, 9, 1, 4, 6, 4, 1, -1, 241 | 3, 11, 6, 3, 6, 0, 0, 6, 4, -1, -1, -1, -1, -1, -1, -1, 242 | 6, 4, 8, 11, 6, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 243 | 7, 10, 6, 7, 8, 10, 8, 9, 10, -1, -1, -1, -1, -1, -1, -1, 244 | 0, 7, 3, 0, 10, 7, 0, 9, 10, 6, 7, 10, -1, -1, -1, -1, 245 | 10, 6, 7, 1, 10, 7, 1, 7, 8, 1, 8, 0, -1, -1, -1, -1, 246 | 10, 6, 7, 10, 7, 1, 1, 7, 3, -1, -1, -1, -1, -1, -1, -1, 247 | 1, 2, 6, 1, 6, 8, 1, 8, 9, 8, 6, 7, -1, -1, -1, -1, 248 | 2, 6, 9, 2, 9, 1, 6, 7, 9, 0, 9, 3, 7, 3, 9, -1, 249 | 7, 8, 0, 7, 0, 6, 6, 0, 2, -1, -1, -1, -1, -1, -1, -1, 250 | 7, 3, 2, 6, 7, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 251 | 2, 3, 11, 10, 6, 8, 10, 8, 9, 8, 6, 7, -1, -1, -1, -1, 252 | 2, 0, 7, 2, 7, 11, 0, 9, 7, 6, 7, 10, 9, 10, 7, -1, 253 | 1, 8, 0, 1, 7, 8, 1, 10, 7, 6, 7, 10, 2, 3, 11, -1, 254 | 11, 2, 1, 11, 1, 7, 10, 6, 1, 6, 7, 1, -1, -1, -1, -1, 255 | 8, 9, 6, 8, 6, 7, 9, 1, 6, 11, 6, 3, 1, 3, 6, -1, 256 | 0, 9, 1, 11, 6, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 257 | 7, 8, 0, 7, 0, 6, 3, 11, 0, 11, 6, 0, -1, -1, -1, -1, 258 | 7, 11, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 259 | 7, 6, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 260 | 3, 0, 8, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 261 | 0, 1, 9, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 262 | 8, 1, 9, 8, 3, 1, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, 263 | 10, 1, 2, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 264 | 1, 2, 10, 3, 0, 8, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1, 265 | 2, 9, 0, 2, 10, 9, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1, 266 | 6, 11, 7, 2, 10, 3, 10, 8, 3, 10, 9, 8, -1, -1, -1, -1, 267 | 7, 2, 3, 6, 2, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 268 | 7, 0, 8, 7, 6, 0, 6, 2, 0, -1, -1, -1, -1, -1, -1, -1, 269 | 2, 7, 6, 2, 3, 7, 0, 1, 9, -1, -1, -1, -1, -1, -1, -1, 270 | 1, 6, 2, 1, 8, 6, 1, 9, 8, 8, 7, 6, -1, -1, -1, -1, 271 | 10, 7, 6, 10, 1, 7, 1, 3, 7, -1, -1, -1, -1, -1, -1, -1, 272 | 10, 7, 6, 1, 7, 10, 1, 8, 7, 1, 0, 8, -1, -1, -1, -1, 273 | 0, 3, 7, 0, 7, 10, 0, 10, 9, 6, 10, 7, -1, -1, -1, -1, 274 | 7, 6, 10, 7, 10, 8, 8, 10, 9, -1, -1, -1, -1, -1, -1, -1, 275 | 6, 8, 4, 11, 8, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 276 | 3, 6, 11, 3, 0, 6, 0, 4, 6, -1, -1, -1, -1, -1, -1, -1, 277 | 8, 6, 11, 8, 4, 6, 9, 0, 1, -1, -1, -1, -1, -1, -1, -1, 278 | 9, 4, 6, 9, 6, 3, 9, 3, 1, 11, 3, 6, -1, -1, -1, -1, 279 | 6, 8, 4, 6, 11, 8, 2, 10, 1, -1, -1, -1, -1, -1, -1, -1, 280 | 1, 2, 10, 3, 0, 11, 0, 6, 11, 0, 4, 6, -1, -1, -1, -1, 281 | 4, 11, 8, 4, 6, 11, 0, 2, 9, 2, 10, 9, -1, -1, -1, -1, 282 | 10, 9, 3, 10, 3, 2, 9, 4, 3, 11, 3, 6, 4, 6, 3, -1, 283 | 8, 2, 3, 8, 4, 2, 4, 6, 2, -1, -1, -1, -1, -1, -1, -1, 284 | 0, 4, 2, 4, 6, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 285 | 1, 9, 0, 2, 3, 4, 2, 4, 6, 4, 3, 8, -1, -1, -1, -1, 286 | 1, 9, 4, 1, 4, 2, 2, 4, 6, -1, -1, -1, -1, -1, -1, -1, 287 | 8, 1, 3, 8, 6, 1, 8, 4, 6, 6, 10, 1, -1, -1, -1, -1, 288 | 10, 1, 0, 10, 0, 6, 6, 0, 4, -1, -1, -1, -1, -1, -1, -1, 289 | 4, 6, 3, 4, 3, 8, 6, 10, 3, 0, 3, 9, 10, 9, 3, -1, 290 | 10, 9, 4, 6, 10, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 291 | 4, 9, 5, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 292 | 0, 8, 3, 4, 9, 5, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, 293 | 5, 0, 1, 5, 4, 0, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1, 294 | 11, 7, 6, 8, 3, 4, 3, 5, 4, 3, 1, 5, -1, -1, -1, -1, 295 | 9, 5, 4, 10, 1, 2, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1, 296 | 6, 11, 7, 1, 2, 10, 0, 8, 3, 4, 9, 5, -1, -1, -1, -1, 297 | 7, 6, 11, 5, 4, 10, 4, 2, 10, 4, 0, 2, -1, -1, -1, -1, 298 | 3, 4, 8, 3, 5, 4, 3, 2, 5, 10, 5, 2, 11, 7, 6, -1, 299 | 7, 2, 3, 7, 6, 2, 5, 4, 9, -1, -1, -1, -1, -1, -1, -1, 300 | 9, 5, 4, 0, 8, 6, 0, 6, 2, 6, 8, 7, -1, -1, -1, -1, 301 | 3, 6, 2, 3, 7, 6, 1, 5, 0, 5, 4, 0, -1, -1, -1, -1, 302 | 6, 2, 8, 6, 8, 7, 2, 1, 8, 4, 8, 5, 1, 5, 8, -1, 303 | 9, 5, 4, 10, 1, 6, 1, 7, 6, 1, 3, 7, -1, -1, -1, -1, 304 | 1, 6, 10, 1, 7, 6, 1, 0, 7, 8, 7, 0, 9, 5, 4, -1, 305 | 4, 0, 10, 4, 10, 5, 0, 3, 10, 6, 10, 7, 3, 7, 10, -1, 306 | 7, 6, 10, 7, 10, 8, 5, 4, 10, 4, 8, 10, -1, -1, -1, -1, 307 | 6, 9, 5, 6, 11, 9, 11, 8, 9, -1, -1, -1, -1, -1, -1, -1, 308 | 3, 6, 11, 0, 6, 3, 0, 5, 6, 0, 9, 5, -1, -1, -1, -1, 309 | 0, 11, 8, 0, 5, 11, 0, 1, 5, 5, 6, 11, -1, -1, -1, -1, 310 | 6, 11, 3, 6, 3, 5, 5, 3, 1, -1, -1, -1, -1, -1, -1, -1, 311 | 1, 2, 10, 9, 5, 11, 9, 11, 8, 11, 5, 6, -1, -1, -1, -1, 312 | 0, 11, 3, 0, 6, 11, 0, 9, 6, 5, 6, 9, 1, 2, 10, -1, 313 | 11, 8, 5, 11, 5, 6, 8, 0, 5, 10, 5, 2, 0, 2, 5, -1, 314 | 6, 11, 3, 6, 3, 5, 2, 10, 3, 10, 5, 3, -1, -1, -1, -1, 315 | 5, 8, 9, 5, 2, 8, 5, 6, 2, 3, 8, 2, -1, -1, -1, -1, 316 | 9, 5, 6, 9, 6, 0, 0, 6, 2, -1, -1, -1, -1, -1, -1, -1, 317 | 1, 5, 8, 1, 8, 0, 5, 6, 8, 3, 8, 2, 6, 2, 8, -1, 318 | 1, 5, 6, 2, 1, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 319 | 1, 3, 6, 1, 6, 10, 3, 8, 6, 5, 6, 9, 8, 9, 6, -1, 320 | 10, 1, 0, 10, 0, 6, 9, 5, 0, 5, 6, 0, -1, -1, -1, -1, 321 | 0, 3, 8, 5, 6, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 322 | 10, 5, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 323 | 11, 5, 10, 7, 5, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 324 | 11, 5, 10, 11, 7, 5, 8, 3, 0, -1, -1, -1, -1, -1, -1, -1, 325 | 5, 11, 7, 5, 10, 11, 1, 9, 0, -1, -1, -1, -1, -1, -1, -1, 326 | 10, 7, 5, 10, 11, 7, 9, 8, 1, 8, 3, 1, -1, -1, -1, -1, 327 | 11, 1, 2, 11, 7, 1, 7, 5, 1, -1, -1, -1, -1, -1, -1, -1, 328 | 0, 8, 3, 1, 2, 7, 1, 7, 5, 7, 2, 11, -1, -1, -1, -1, 329 | 9, 7, 5, 9, 2, 7, 9, 0, 2, 2, 11, 7, -1, -1, -1, -1, 330 | 7, 5, 2, 7, 2, 11, 5, 9, 2, 3, 2, 8, 9, 8, 2, -1, 331 | 2, 5, 10, 2, 3, 5, 3, 7, 5, -1, -1, -1, -1, -1, -1, -1, 332 | 8, 2, 0, 8, 5, 2, 8, 7, 5, 10, 2, 5, -1, -1, -1, -1, 333 | 9, 0, 1, 5, 10, 3, 5, 3, 7, 3, 10, 2, -1, -1, -1, -1, 334 | 9, 8, 2, 9, 2, 1, 8, 7, 2, 10, 2, 5, 7, 5, 2, -1, 335 | 1, 3, 5, 3, 7, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 336 | 0, 8, 7, 0, 7, 1, 1, 7, 5, -1, -1, -1, -1, -1, -1, -1, 337 | 9, 0, 3, 9, 3, 5, 5, 3, 7, -1, -1, -1, -1, -1, -1, -1, 338 | 9, 8, 7, 5, 9, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 339 | 5, 8, 4, 5, 10, 8, 10, 11, 8, -1, -1, -1, -1, -1, -1, -1, 340 | 5, 0, 4, 5, 11, 0, 5, 10, 11, 11, 3, 0, -1, -1, -1, -1, 341 | 0, 1, 9, 8, 4, 10, 8, 10, 11, 10, 4, 5, -1, -1, -1, -1, 342 | 10, 11, 4, 10, 4, 5, 11, 3, 4, 9, 4, 1, 3, 1, 4, -1, 343 | 2, 5, 1, 2, 8, 5, 2, 11, 8, 4, 5, 8, -1, -1, -1, -1, 344 | 0, 4, 11, 0, 11, 3, 4, 5, 11, 2, 11, 1, 5, 1, 11, -1, 345 | 0, 2, 5, 0, 5, 9, 2, 11, 5, 4, 5, 8, 11, 8, 5, -1, 346 | 9, 4, 5, 2, 11, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 347 | 2, 5, 10, 3, 5, 2, 3, 4, 5, 3, 8, 4, -1, -1, -1, -1, 348 | 5, 10, 2, 5, 2, 4, 4, 2, 0, -1, -1, -1, -1, -1, -1, -1, 349 | 3, 10, 2, 3, 5, 10, 3, 8, 5, 4, 5, 8, 0, 1, 9, -1, 350 | 5, 10, 2, 5, 2, 4, 1, 9, 2, 9, 4, 2, -1, -1, -1, -1, 351 | 8, 4, 5, 8, 5, 3, 3, 5, 1, -1, -1, -1, -1, -1, -1, -1, 352 | 0, 4, 5, 1, 0, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 353 | 8, 4, 5, 8, 5, 3, 9, 0, 5, 0, 3, 5, -1, -1, -1, -1, 354 | 9, 4, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 355 | 4, 11, 7, 4, 9, 11, 9, 10, 11, -1, -1, -1, -1, -1, -1, -1, 356 | 0, 8, 3, 4, 9, 7, 9, 11, 7, 9, 10, 11, -1, -1, -1, -1, 357 | 1, 10, 11, 1, 11, 4, 1, 4, 0, 7, 4, 11, -1, -1, -1, -1, 358 | 3, 1, 4, 3, 4, 8, 1, 10, 4, 7, 4, 11, 10, 11, 4, -1, 359 | 4, 11, 7, 9, 11, 4, 9, 2, 11, 9, 1, 2, -1, -1, -1, -1, 360 | 9, 7, 4, 9, 11, 7, 9, 1, 11, 2, 11, 1, 0, 8, 3, -1, 361 | 11, 7, 4, 11, 4, 2, 2, 4, 0, -1, -1, -1, -1, -1, -1, -1, 362 | 11, 7, 4, 11, 4, 2, 8, 3, 4, 3, 2, 4, -1, -1, -1, -1, 363 | 2, 9, 10, 2, 7, 9, 2, 3, 7, 7, 4, 9, -1, -1, -1, -1, 364 | 9, 10, 7, 9, 7, 4, 10, 2, 7, 8, 7, 0, 2, 0, 7, -1, 365 | 3, 7, 10, 3, 10, 2, 7, 4, 10, 1, 10, 0, 4, 0, 10, -1, 366 | 1, 10, 2, 8, 7, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 367 | 4, 9, 1, 4, 1, 7, 7, 1, 3, -1, -1, -1, -1, -1, -1, -1, 368 | 4, 9, 1, 4, 1, 7, 0, 8, 1, 8, 7, 1, -1, -1, -1, -1, 369 | 4, 0, 3, 7, 4, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 370 | 4, 8, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 371 | 9, 10, 8, 10, 11, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 372 | 3, 0, 9, 3, 9, 11, 11, 9, 10, -1, -1, -1, -1, -1, -1, -1, 373 | 0, 1, 10, 0, 10, 8, 8, 10, 11, -1, -1, -1, -1, -1, -1, -1, 374 | 3, 1, 10, 11, 3, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 375 | 1, 2, 11, 1, 11, 9, 9, 11, 8, -1, -1, -1, -1, -1, -1, -1, 376 | 3, 0, 9, 3, 9, 11, 1, 2, 9, 2, 11, 9, -1, -1, -1, -1, 377 | 0, 2, 11, 8, 0, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 378 | 3, 2, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 379 | 2, 3, 8, 2, 8, 10, 10, 8, 9, -1, -1, -1, -1, -1, -1, -1, 380 | 9, 10, 2, 0, 9, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 381 | 2, 3, 8, 2, 8, 10, 0, 1, 8, 1, 10, 8, -1, -1, -1, -1, 382 | 1, 10, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 383 | 1, 3, 8, 9, 1, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 384 | 0, 9, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 385 | 0, 3, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 386 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 387 | ]); 388 | 389 | /* polyfill old behavior */ 390 | // THREE.Geometry.prototype.computeCentroids = function () { 391 | // for (const face of this.faces) { 392 | // face.centroid.set(0, 0, 0); 393 | 394 | // face.centroid.add(this.vertices[face.a]); 395 | // face.centroid.add(this.vertices[face.b]); 396 | // face.centroid.add(this.vertices[face.c]); 397 | // face.centroid.divideScalar( 3 ); 398 | // } 399 | // } 400 | 401 | // const oldConstructor = THREE.Face3; 402 | // THREE.Face3 = function (...args) { 403 | // oldConstructor.apply(this, args); 404 | 405 | // this.centroid = new THREE.Vector3(); 406 | // }; 407 | 408 | /* taken from https://stemkoski.github.io/Three.js/Marching-Cubes.html */ 409 | export function marchingCubes(fn: Fn, axisMin: number, axisMax: number, size: number) { 410 | const points = [], 411 | values = []; 412 | 413 | const axisRange = axisMax - axisMin; 414 | 415 | // Generate a list of 3D points and values at those points 416 | for (let k = 0; k < size; k++) 417 | for (let j = 0; j < size; j++) 418 | for (let i = 0; i < size; i++) 419 | { 420 | // actual values 421 | const x = axisMin + axisRange * i / (size - 1), 422 | y = axisMin + axisRange * j / (size - 1), 423 | z = axisMin + axisRange * k / (size - 1); 424 | points.push(new THREE.Vector3(x,y,z)); 425 | const value = fn(x, y, z); 426 | values.push(value); 427 | } 428 | 429 | // Marching Cubes Algorithm 430 | 431 | const size2 = size * size; 432 | 433 | // Vertices may occur along edges of cube, when the values at the edge's endpoints 434 | // straddle the isolevel value. 435 | // Actual position along edge weighted according to function values. 436 | const vlist = new Array(12); 437 | 438 | const vertices = []; 439 | const normals = []; 440 | const geometry = new THREE.BufferGeometry(); 441 | const a = new THREE.Vector3(); 442 | const b = new THREE.Vector3(); 443 | const c = new THREE.Vector3(); 444 | let vertexIndex = 0; 445 | 446 | for (let z = 0; z < size - 1; z++) 447 | for (let y = 0; y < size - 1; y++) 448 | for (let x = 0; x < size - 1; x++) 449 | { 450 | // index of base point, and also adjacent points on cube 451 | const p = x + size * y + size2 * z, 452 | px = p + 1, 453 | py = p + size, 454 | pxy = py + 1, 455 | pz = p + size2, 456 | pxz = px + size2, 457 | pyz = py + size2, 458 | pxyz = pxy + size2; 459 | 460 | // store scalar values corresponding to vertices 461 | const value0 = values[ p ], 462 | value1 = values[ px ], 463 | value2 = values[ py ], 464 | value3 = values[ pxy ], 465 | value4 = values[ pz ], 466 | value5 = values[ pxz ], 467 | value6 = values[ pyz ], 468 | value7 = values[ pxyz ]; 469 | 470 | // place a "1" in bit positions corresponding to vertices whose 471 | // isovalue is less than given constant. 472 | 473 | const isolevel = 0; 474 | 475 | let cubeindex = 0; 476 | if ( value0 < isolevel ) cubeindex |= 1; 477 | if ( value1 < isolevel ) cubeindex |= 2; 478 | if ( value2 < isolevel ) cubeindex |= 8; 479 | if ( value3 < isolevel ) cubeindex |= 4; 480 | if ( value4 < isolevel ) cubeindex |= 16; 481 | if ( value5 < isolevel ) cubeindex |= 32; 482 | if ( value6 < isolevel ) cubeindex |= 128; 483 | if ( value7 < isolevel ) cubeindex |= 64; 484 | 485 | // bits = 12 bit number, indicates which edges are crossed by the isosurface 486 | const bits = edgeTable[ cubeindex ]; 487 | 488 | // if none are crossed, proceed to next iteration 489 | if (bits === 0) continue; 490 | 491 | // check which edges are crossed, and estimate the point location 492 | // using a weighted average of scalar values at edge endpoints. 493 | // store the vertex in an array for use later. 494 | let mu = 0.5; 495 | 496 | // bottom of the cube 497 | if ( bits & 1 ) 498 | { 499 | mu = ( isolevel - value0 ) / ( value1 - value0 ); 500 | vlist[0] = points[p].clone().lerp( points[px], mu ); 501 | } 502 | if ( bits & 2 ) 503 | { 504 | mu = ( isolevel - value1 ) / ( value3 - value1 ); 505 | vlist[1] = points[px].clone().lerp( points[pxy], mu ); 506 | } 507 | if ( bits & 4 ) 508 | { 509 | mu = ( isolevel - value2 ) / ( value3 - value2 ); 510 | vlist[2] = points[py].clone().lerp( points[pxy], mu ); 511 | } 512 | if ( bits & 8 ) 513 | { 514 | mu = ( isolevel - value0 ) / ( value2 - value0 ); 515 | vlist[3] = points[p].clone().lerp( points[py], mu ); 516 | } 517 | // top of the cube 518 | if ( bits & 16 ) 519 | { 520 | mu = ( isolevel - value4 ) / ( value5 - value4 ); 521 | vlist[4] = points[pz].clone().lerp( points[pxz], mu ); 522 | } 523 | if ( bits & 32 ) 524 | { 525 | mu = ( isolevel - value5 ) / ( value7 - value5 ); 526 | vlist[5] = points[pxz].clone().lerp( points[pxyz], mu ); 527 | } 528 | if ( bits & 64 ) 529 | { 530 | mu = ( isolevel - value6 ) / ( value7 - value6 ); 531 | vlist[6] = points[pyz].clone().lerp( points[pxyz], mu ); 532 | } 533 | if ( bits & 128 ) 534 | { 535 | mu = ( isolevel - value4 ) / ( value6 - value4 ); 536 | vlist[7] = points[pz].clone().lerp( points[pyz], mu ); 537 | } 538 | // vertical lines of the cube 539 | if ( bits & 256 ) 540 | { 541 | mu = ( isolevel - value0 ) / ( value4 - value0 ); 542 | vlist[8] = points[p].clone().lerp( points[pz], mu ); 543 | } 544 | if ( bits & 512 ) 545 | { 546 | mu = ( isolevel - value1 ) / ( value5 - value1 ); 547 | vlist[9] = points[px].clone().lerp( points[pxz], mu ); 548 | } 549 | if ( bits & 1024 ) 550 | { 551 | mu = ( isolevel - value3 ) / ( value7 - value3 ); 552 | vlist[10] = points[pxy].clone().lerp( points[pxyz], mu ); 553 | } 554 | if ( bits & 2048 ) 555 | { 556 | mu = ( isolevel - value2 ) / ( value6 - value2 ); 557 | vlist[11] = points[py].clone().lerp( points[pyz], mu ); 558 | } 559 | 560 | // construct triangles -- get correct vertices from triTable. 561 | let i = 0; 562 | cubeindex <<= 4; // multiply by 16... 563 | // "Re-purpose cubeindex into an offset into triTable." 564 | // since each row really isn't a row. 565 | 566 | // the while loop should run at most 5 times, 567 | // since the 16th entry in each row is a -1. 568 | while ( triTable[ cubeindex + i ] != -1 ) 569 | { 570 | const index1 = triTable[cubeindex + i]; 571 | const index2 = triTable[cubeindex + i + 1]; 572 | const index3 = triTable[cubeindex + i + 2]; 573 | 574 | vertices.push( ...vlist[index1].toArray() ); 575 | vertices.push( ...vlist[index2].toArray() ); 576 | vertices.push( ...vlist[index3].toArray() ); 577 | 578 | vertexIndex += 3; 579 | i += 3; 580 | } 581 | } 582 | 583 | geometry.setAttribute("position", new THREE.BufferAttribute(new Float32Array(vertices), 3)); 584 | geometry.computeVertexNormals(); 585 | 586 | return geometry; 587 | } 588 | 589 | export function intersectSurfacesImplicit( 590 | f: Fn, 591 | g: Fn, 592 | df: OneForm, 593 | dg: OneForm, 594 | q0: THREE.Vector3, 595 | samples: number, 596 | step: number 597 | ) { 598 | const points = []; 599 | 600 | let p = q0; 601 | while (points.length < samples) { 602 | const tangent = new THREE.Vector3().crossVectors( 603 | new THREE.Vector3().fromArray(df(p.x, p.y, p.z)), 604 | new THREE.Vector3().fromArray(dg(p.x, p.y, p.z)) 605 | ).normalize(); 606 | 607 | points.push(curvePoint( 608 | f, g, df, dg, 609 | p.clone().add(tangent.multiplyScalar(step)) 610 | )); 611 | 612 | p = points[points.length - 1]; 613 | } 614 | 615 | return points; 616 | } 617 | 618 | export function curvePoint(f: Fn, g: Fn, df: OneForm, dg: OneForm, q0: THREE.Vector3) { 619 | const epsilon = 0.1; 620 | 621 | let qk = q0, 622 | diff = new THREE.Vector3(); 623 | 624 | do { 625 | const dfqk = df(qk.x, qk.y, qk.z), 626 | dgqk = dg(qk.x, qk.y, qk.z), 627 | jacT = [dfqk, dgqk]; 628 | 629 | const matrix = [[0,0],[0,0]] as Matrix2; 630 | for (let i = 0; i < 2; ++i) 631 | for (let j = 0; j < 2; ++j) 632 | for (let k = 0; k < 3; ++k) 633 | { 634 | matrix[i][j] += jacT[i][k] * jacT[j][k]; 635 | } 636 | 637 | const [alpha, beta] = solve22Matrix( 638 | matrix, 639 | [ 640 | -f(qk.x, qk.y, qk.z), 641 | -g(qk.x, qk.y, qk.z) 642 | ] 643 | ); 644 | 645 | diff = 646 | new THREE.Vector3().fromArray(dfqk).multiplyScalar(alpha).add( 647 | new THREE.Vector3().fromArray(dgqk).multiplyScalar(beta) 648 | ); 649 | 650 | qk = qk.clone().add(diff); 651 | } while (diff.length() >= epsilon); 652 | 653 | return qk; 654 | } 655 | 656 | export function solve22Matrix( 657 | [[a, b], [c, d]]: Matrix2, 658 | [y1, y2]: Vector2 659 | ) { 660 | return [ 661 | (y1*d - b*y2) / (a*d - b*c), 662 | (a*y2 - y1*c) / (a*d - b*c) 663 | ]; 664 | } 665 | 666 | export function arrowOrient(pointX: THREE.Vector3, pointY: THREE.Vector3) { 667 | const direction = new THREE.Vector3().subVectors(pointY, pointX); 668 | const orientation = new THREE.Matrix4(); 669 | orientation.lookAt(pointX, pointY, new THREE.Object3D().up); 670 | orientation.multiply(new THREE.Matrix4().set(1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1)); 671 | 672 | const position = new THREE.Vector3( 673 | (pointY.x + pointX.x) / 2, 674 | (pointY.y + pointX.y) / 2, 675 | (pointY.z + pointX.z) / 2 676 | ); 677 | return [orientation, position]; 678 | } 679 | --------------------------------------------------------------------------------