├── .github └── workflows │ └── deploy-branches.yml ├── .gitignore ├── README.md ├── index.html ├── jest.config.js ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── src ├── Action.tsx ├── Animate.tsx ├── App.tsx ├── Hover.tsx ├── Keyboard.tsx ├── Model.tsx ├── Names.tsx ├── Projector.tsx ├── Settings.tsx ├── Sound.tsx ├── Stage.tsx ├── Statics.tsx ├── ToolBox.tsx ├── Transform.tsx ├── Update.tsx ├── Util.tsx ├── assets │ ├── favicon.ico │ ├── icons │ │ ├── eye-closed.svg │ │ ├── eye-open.svg │ │ ├── motion-half.svg │ │ ├── motion-off.svg │ │ ├── motion-on.svg │ │ ├── noun-1005147.svg │ │ ├── noun-1020541.svg │ │ ├── noun-1037924.svg │ │ ├── noun-1052053.svg │ │ ├── noun-1156122.svg │ │ ├── noun-1166087.svg │ │ ├── noun-1275679.svg │ │ ├── noun-1333353.svg │ │ ├── noun-1560867.svg │ │ ├── noun-1683765.svg │ │ ├── noun-1831710.svg │ │ ├── noun-1831712.svg │ │ ├── noun-2048496.svg │ │ ├── noun-2856200.svg │ │ ├── noun-2902082.svg │ │ ├── noun-2925980.svg │ │ ├── noun-3376875.svg │ │ ├── noun-5454290.svg │ │ ├── noun-5576595.svg │ │ ├── noun-6336647.svg │ │ ├── noun-agile-transformation-4617414.svg │ │ ├── noun-alphabet-3591519.svg │ │ ├── noun-backpack-5550372.svg │ │ ├── noun-bag-6335093.svg │ │ ├── noun-bag-6335275.svg │ │ ├── noun-cloud-6325788.svg │ │ ├── noun-collection-1675411.svg │ │ ├── noun-cycle-1793611.svg │ │ ├── noun-cycle-4446.svg │ │ ├── noun-dead-5130035.svg │ │ ├── noun-digital-transformation-6011306.svg │ │ ├── noun-earth-1219989.svg │ │ ├── noun-geometry-4695832.svg │ │ ├── noun-list-10135.svg │ │ ├── noun-paint-6329583.svg │ │ ├── noun-palette-1918496.svg │ │ ├── noun-qr-4574196.svg │ │ ├── noun-shopping-bag-6321186.svg │ │ ├── noun-shopping-bag-6321318.svg │ │ ├── noun-sigh-654547.svg │ │ ├── noun-subtitle-4574190.svg │ │ ├── noun-sun-5362089.svg │ │ ├── noun-sun-6322390.svg │ │ ├── noun-tool-3376727.svg │ │ ├── noun-tool-3376728.svg │ │ ├── noun-transformation-1832026.svg │ │ ├── noun-transformation-3968008.svg │ │ ├── noun-transformation-6040368.svg │ │ ├── noun-transformation-6040479.svg │ │ ├── noun-transformation-6098909.svg │ │ ├── noun-tree-1039106.svg │ │ ├── noun-tree-1052083.svg │ │ ├── noun-tree-1052096.svg │ │ ├── qr.svg │ │ ├── sound-off.svg │ │ └── sound-on.svg │ ├── marble.jpeg │ ├── nool-seed-infinity-only.svg │ ├── nool-seed.svg │ ├── nooltext-light.png │ ├── nooltext.png │ ├── ps-toolbar.png │ └── sfx │ │ ├── chchiu-out.wav │ │ ├── klohk.m4a │ │ ├── pew.m4a │ │ ├── pshew.m4a │ │ ├── shwo-ph-out.wav │ │ └── tiup-comm-out.wav ├── data │ ├── Tools.tsx │ ├── ToolsExp.tsx │ └── World.tsx ├── declarations.d.ts ├── index.css ├── index.tsx ├── syntax │ ├── Exp.tsx │ ├── ExpToPat.tsx │ ├── ID.tsx │ ├── Node.test.tsx │ ├── Node.tsx │ ├── Pat.tsx │ └── Path.tsx └── view │ ├── ExpView.tsx │ ├── PreView.tsx │ ├── SeedView.tsx │ ├── SettingsView.tsx │ ├── StageView.tsx │ └── ToolsView.tsx ├── tsconfig.json └── vite.config.ts /.github/workflows/deploy-branches.yml: -------------------------------------------------------------------------------- 1 | # General notes on github actions: Note that both the working directory 2 | # and environment variables generally are not shared between steps. 3 | name: deploy nool 4 | on: [push] 5 | jobs: 6 | Deploy: 7 | runs-on: ubuntu-latest 8 | steps: 9 | # NOTE: position the below lines in the code between two steps 10 | # and uncomment them to open an ssh connection at that point: 11 | #- name: Debugging with ssh 12 | # uses: lhotari/action-upterm@v1 13 | - name: Checkout nool repo on current branch # STEP 1 14 | uses: actions/checkout@v2 15 | with: 16 | path: source 17 | - name: Add name of current branch to environment as BRANCH_NAME 18 | uses: nelonoel/branch-name@v1.0.1 19 | - name: Retrieve build environment if cached # STEP 2 20 | id: build-cache 21 | uses: actions/cache@v2 22 | with: 23 | path: '/home/runner/.opam/' 24 | key: ${{ runner.os }}-modules-${{ hashFiles('./source/pnp-lock.yaml') }} 25 | #- name: Debugging with ssh 26 | # uses: lhotari/action-upterm@v1 27 | #curl -fsSL https://get.pnpm.io/install.sh | sh - || true 28 | #sudo -s && source /root/.bashrc 29 | - name: Use Node.js 21.2.0 30 | uses: actions/setup-node@v3 31 | with: 32 | node-version: '21.2.0' 33 | # yarn global add pnpm@8.10.5 34 | - name: Install dependencies and build nool # STEP 3 35 | run: | 36 | pwd 37 | ls 38 | node --version 39 | npm install --global pnpm@8.10.5 && SHELL=bash pnpm setup 40 | pnpm --version 41 | source /home/runner/.bashrc 42 | pnpm install 43 | pnpm run build 44 | working-directory: ./source 45 | - name: Checkout website build artifacts repo # STEP 4 46 | uses: actions/checkout@v2 47 | with: 48 | repository: disconcision/disconcision.github.io 49 | token: ${{ secrets.DEPLOY_NOOL }} 50 | path: server 51 | - name: Clear any old build of this branch # STEP 5 52 | run: if [ -d "nool/${BRANCH_NAME}" ] ; then rm -rf "nool/${BRANCH_NAME}" ; fi 53 | working-directory: ./server 54 | - name: Copy in newly built source # STEP 6 55 | run: | 56 | mkdir "./server/nool/${BRANCH_NAME}" && 57 | cp -r "./source/dist"/* "./server/nool/${BRANCH_NAME}" && 58 | if [ "${BRANCH_NAME}" == "main" ] 59 | then 60 | cp -r "./source/dist"/* "./server/nool" 61 | fi 62 | - name: Commit to website aka deploy # STEP 7 63 | run: | 64 | git config user.name github-deploy-action 65 | git config user.email nool-deploy@disconcision.com 66 | git add -A 67 | git status 68 | git diff-index --quiet HEAD || (git commit -m "github-deploy-action-${BRANCH_NAME}"; git push) 69 | working-directory: ./server -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules/ 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ∩∞ᒐ? 2 | 3 | is a syntactic fidget spinner 4 | 5 | ## is n∞l? 6 | 7 | n∞l is: https://andrewblinn.com/nool/ 8 | 9 | (needs view transitions api (currently only in chrome) for animations to play) 10 | 11 | ## n∞l n∞b? 12 | 1. `do clicks`. 13 | 14 | ## n∞lnatomy 15 | `_______________________`
16 | `| s s |`
17 | `| _____ _____ |`
18 | `| | | | | |`
19 | `| noolbox ∞ |stage| |`
20 | `| |_____| |_____| |`
21 | `| |`
22 | `|_s_________________s_|`
23 | 24 | where s = settings 25 | 26 | ## Keyboard 27 | 28 | ### `↑` `↓` `→` `←` (stage hand) 29 | ### `w` `a` `s` `d` (tool hand) 30 | ### `SPACE` (sing) `ESCAPE` (die) 31 | 32 | (keyboard controls aren't a priority. hand movements directions aren't yet localized per projection, so it might be hard to move in non-default layouts) 33 | 34 | ## make n∞l 35 | 36 | ### `nool` ⊃ `typescript`, `solid.js` 37 | 38 | 1. install node (21.2.0 works) 39 | - see https://nodejs.org/en/learn/getting-started/how-to-install-nodejs 40 | - `node --version` 41 | 2. install pnpm (8.10.5 works)< 42 | - `npm install --global pnpm@8.10.5 && SHELL=bash pnpm setup` 43 | - `source /home/runner/.bashrc` 44 | 3. build deps and run 45 | - `pnpm install` 46 | - `pnpm run build` 47 | 48 | ### `pnpm dev` or `pnpm start` 49 | 50 | run in dev mode. reload on edits. 51 | open [http://localhost:3000](http://localhost:3000) 52 | 53 | ### `pnpm run build` 54 | 55 | builds deployable in `dist` folder 56 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | nool 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-template-solid", 3 | "version": "0.0.0", 4 | "description": "", 5 | "scripts": { 6 | "start": "vite", 7 | "dev": "vite", 8 | "build": "vite build", 9 | "serve": "vite preview", 10 | "test": "jest" 11 | }, 12 | "license": "MIT", 13 | "devDependencies": { 14 | "@types/jest": "^29.5.10", 15 | "jest": "^29.7.0", 16 | "ts-jest": "^29.1.1", 17 | "typescript": "^4.9.5", 18 | "vite": "^4.5.0", 19 | "vite-plugin-solid": "^2.7.2" 20 | }, 21 | "dependencies": { 22 | "@types/dom-view-transitions": "^1.0.4", 23 | "bootstrap": "^5.3.2", 24 | "flipping": "^1.1.0", 25 | "rand-seed": "^1.0.2", 26 | "random-seed": "^0.3.0", 27 | "solid-js": "^1.8.6", 28 | "tone": "^14.7.77" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Action.tsx: -------------------------------------------------------------------------------- 1 | import * as Exp from "./syntax/Exp"; 2 | import * as Path from "./syntax/Path"; 3 | import * as Hover from "./Hover"; 4 | import * as Settings from "./Settings"; 5 | import * as Transform from "./Transform"; 6 | import * as Pat from "./syntax/Pat"; 7 | 8 | export type Inject = (_: Action) => void; 9 | 10 | export type Direction = "up" | "down" | "left" | "right"; 11 | 12 | export type Action = 13 | | { t: "restart" } 14 | | { t: "setSetting"; action: Settings.Action } 15 | | { t: "setHover"; target: Hover.t } 16 | | { t: "setSelect"; path: Path.t } 17 | | { t: "moveStage"; direction: Direction } 18 | | { t: "moveTool"; direction: Direction } 19 | | { t: "wheelTools"; offset: number} 20 | | { t: "wheelNumTools"; offset: number} 21 | | { t: "unsetSelections" } 22 | | { 23 | t: "transformNode"; 24 | idx: number; 25 | transform: Transform.t; 26 | f: (_: Exp.t) => Pat.TransformResult; 27 | } 28 | | { 29 | t: "transformNodeAndFlipTransform"; 30 | target: "Source" | "Result"; 31 | idx: number; 32 | transform: Transform.t; 33 | f: (_: Exp.t) => Pat.TransformResult; 34 | } 35 | | { t: "applyTransform"; idx: number; direction: "forward" | "reverse" } 36 | | { t: "applyTransformSelected" } 37 | | { t: "flipTransform"; idx: number } 38 | | { t: "Noop" }; 39 | 40 | export type t = Action; 41 | -------------------------------------------------------------------------------- /src/Animate.tsx: -------------------------------------------------------------------------------- 1 | const blah = (s: string) => ` 2 | ::view-transition-old(${s}), 3 | ::view-transition-new(${s}) { 4 | transition: transform 550ms cubic-bezier(0.68, -0.6, 0.32, 1.6); 5 | } 6 | `; 7 | 8 | export const init = ():void => { 9 | //TODO: unhardcode id max 10 | var style = document.createElement("style"); 11 | for (let id = 0; id < 100; id++) { 12 | style.innerHTML += `#node-${id}.animate { view-transition-name: flip-node-${id}; }\n`; 13 | style.innerHTML += `#main.unsetSelections #sym-${id}, #main.setSelect #sym-${id}, #main.moveStage #sym-${id} { view-transition-name: flip-sym-${id}; }\n`; 14 | 15 | //style.innerHTML += `#main.setSelect #pat-${id} { view-transition-name: flip-pat-${id}; }`; 16 | //style.innerHTML += blah(`flip-node-${id}`); 17 | //style.innerHTML += blah(`flip-pat-${id}`); 18 | } 19 | style.innerHTML += `#main.setSelect .logo, #main.unsetSelections .logo { view-transition-name: setSelect-logo }\n`; 20 | style.innerHTML += `#main.setSelect #seed, #main.unsetSelections #seed { view-transition-name: setSelect-seed }\n`; 21 | //style.innerHTML += `#node.selected { view-transition-name: flip-node-selected; }\n`; 22 | document.getElementsByTagName("head")[0].appendChild(style); 23 | }; 24 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from "solid-js"; 2 | import { createStore, SetStoreFunction } from "solid-js/store"; 3 | import { go } from "./Update"; 4 | import * as Model from "./Model"; 5 | import * as Action from "./Action"; 6 | import * as Keyboard from "./Keyboard"; 7 | import { SettingsView } from "./view/SettingsView"; 8 | import { Seed } from "./view/SeedView"; 9 | import * as ExpToPat from "./syntax/ExpToPat"; 10 | import * as Animate from "./Animate"; 11 | //import { Toolbar } from "./view/ToolsView"; 12 | 13 | export type SetModel = SetStoreFunction; 14 | 15 | 16 | const App: Component = () => { 17 | const [model, setModel] = createStore({ ...Model.init }); 18 | Animate.init(); 19 | const inject = (a: Action.t) => { 20 | console.log(a); 21 | if (a.t === "setHover" || !document.startViewTransition) { 22 | console.log("sethover dont transition:" + a.t); 23 | go(model, setModel, a); 24 | return; 25 | } 26 | const guy2 = document.getElementById("main"); 27 | guy2 ? guy2.classList.add(a.t) : console.log("no guy r add"); 28 | let v = document.startViewTransition(() => go(model, setModel, a)); 29 | v.finished.then(() => 30 | guy2 ? guy2.classList.remove(a.t) : console.log("no guy 2 rm") 31 | ); 32 | }; 33 | document.addEventListener("keydown", Keyboard.keydown(inject), false); 34 | document.addEventListener("keyup", Keyboard.keyup(inject), false); 35 | // document.addEventListener("transitionstart", (e) => { 36 | // in_transition = true; 37 | // }); 38 | // document.addEventListener("transitionend", (e) => { 39 | // in_transition = false; 40 | // }); 41 | return ( 42 |
47 | {/* 52 | ); 53 | }; 54 | ExpToPat.test(); 55 | export default App; 56 | -------------------------------------------------------------------------------- /src/Hover.tsx: -------------------------------------------------------------------------------- 1 | import * as ID from "./syntax/ID"; 2 | import * as Pat from "./syntax/Pat"; 3 | import * as Model from "./Model"; 4 | 5 | export type t = 6 | | { t: "NoHover" } 7 | | { t: "StageNode"; id: ID.t } 8 | | { t: "TransformSource"; pat: Pat.t; idx: number } 9 | | { t: "TransformResult"; pat: Pat.t; idx: number }; 10 | 11 | export const init: t = { t: "NoHover" }; 12 | 13 | export const eq = (a: t, b: t): boolean => { 14 | switch (a.t) { 15 | case "NoHover": 16 | return b.t == "NoHover"; 17 | case "StageNode": 18 | return b.t == "StageNode" && (a.id === b.id); 19 | case "TransformSource": 20 | return b.t == "TransformSource" && Pat.eq(a.pat, b.pat); 21 | case "TransformResult": 22 | return b.t == "TransformResult" && Pat.eq(a.pat, b.pat); 23 | } 24 | } 25 | 26 | export const get_binding = (model: Model.t): Pat.Binding[] => { 27 | switch (model.hover.t) { 28 | case "NoHover": 29 | return []; 30 | case "StageNode": 31 | return []; 32 | case "TransformSource": 33 | if (model.stage.selection == "unselected") return []; 34 | const rs = Pat.matches_at_path( 35 | model.stage.exp, 36 | model.hover.pat, 37 | model.stage.selection 38 | ); 39 | return rs == "NoMatch" ? [] : rs; 40 | case "TransformResult": 41 | if (model.stage.selection == "unselected") return []; 42 | const rr = Pat.matches_at_path( 43 | model.stage.exp, 44 | model.hover.pat, 45 | model.stage.selection 46 | ); 47 | return rr == "NoMatch" ? [] : rr; 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /src/Keyboard.tsx: -------------------------------------------------------------------------------- 1 | import * as Action from "./Action"; 2 | 3 | const action_of = (key: string): Action.t | "NoBinding" => { 4 | switch (key) { 5 | case "Escape": 6 | return { t: "restart" }; 7 | case "ArrowLeft": 8 | return { t: "moveStage", direction: "left" }; 9 | case "ArrowRight": 10 | return { t: "moveStage", direction: "right" }; 11 | case "ArrowUp": 12 | return { t: "moveStage", direction: "up" }; 13 | case "ArrowDown": 14 | return { t: "moveStage", direction: "down" }; 15 | case "1": 16 | return { t: "applyTransform", idx: 0, direction: "forward" }; 17 | case "2": 18 | return { t: "applyTransform", idx: 1, direction: "forward" }; 19 | case "3": 20 | return { t: "applyTransform", idx: 2, direction: "forward" }; 21 | case "4": 22 | return { t: "applyTransform", idx: 3, direction: "forward" }; 23 | case "w": 24 | return { t: "moveTool", direction: "up" }; 25 | case "s": 26 | return { t: "moveTool", direction: "down" }; 27 | case "a": 28 | return { t: "moveTool", direction: "left" }; 29 | case "d": 30 | return { t: "moveTool", direction: "right" }; 31 | case " ": 32 | return { t: "applyTransformSelected" }; 33 | default: 34 | return "NoBinding"; 35 | } 36 | }; 37 | 38 | export const keydown = (inject: Action.Inject) => (event: KeyboardEvent) => { 39 | //console.log("keydown:" + keyName); 40 | let action = action_of(event.key); 41 | if (action == "NoBinding") return; 42 | event.preventDefault(); 43 | inject(action); 44 | }; 45 | 46 | export const keyup = (_inject: Action.Inject) => (event: KeyboardEvent) => { 47 | //console.log("keyup:" + keyName); 48 | switch (event.key) { 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /src/Model.tsx: -------------------------------------------------------------------------------- 1 | import * as Settings from "./Settings"; 2 | import * as Stage from "./Stage"; 3 | import * as Hover from "./Hover"; 4 | import * as ToolBox from "./ToolBox"; 5 | 6 | export type Model = { 7 | stage: Stage.t; 8 | tools: ToolBox.t; 9 | settings: Settings.t; 10 | hover: Hover.t; 11 | }; 12 | 13 | export type t = Model; 14 | 15 | export const init: Model = { 16 | stage: Stage.init, 17 | tools: ToolBox.init, 18 | settings: Settings.init, 19 | hover: Hover.init, 20 | }; 21 | -------------------------------------------------------------------------------- /src/Names.tsx: -------------------------------------------------------------------------------- 1 | import * as Settings from "./Settings"; 2 | 3 | const to_singlechar = (emoji: string): string => { 4 | switch (emoji) { 5 | case "☁️": 6 | return "L"; 7 | case "🍄": 8 | return "M"; 9 | case "🎲": 10 | return "D"; 11 | case "🦠": 12 | return "V"; 13 | case "🐝": 14 | return "E"; 15 | case "♫": 16 | return "A"; 17 | case "♥": 18 | return "B"; 19 | case "✿": 20 | return "C"; 21 | case "🌘": 22 | return "1"; 23 | case "🌑": 24 | return "0"; 25 | case "➕": 26 | return "+"; 27 | case "➖": 28 | return "-"; 29 | case "✖️": 30 | return "*"; 31 | default: 32 | return "?"; 33 | } 34 | }; 35 | 36 | export const get = (symbols: Settings.symbols, symbol: string):string => { 37 | switch (symbols) { 38 | case "Emoji": 39 | return symbol; 40 | case "SingleChar": 41 | return to_singlechar(symbol); 42 | default: 43 | return symbol; 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /src/Projector.tsx: -------------------------------------------------------------------------------- 1 | import * as ID from "./syntax/ID"; 2 | 3 | type PaintColor = "Cyan" | "Magenta" | "Yellow"; 4 | 5 | type Painter = PaintColor | "Unpainted"; 6 | 7 | type Projector = { 8 | painter: Painter; 9 | }; 10 | 11 | type t = Projector; 12 | 13 | export type PMap = Map; 14 | 15 | export const init: PMap = new Map(); 16 | 17 | /* 18 | paint plan: 19 | 20 | this version only label nodes; does not replace them 21 | we have a map from ids to (user-specified) attributes including paint color 22 | paint colors come from a fixed palette which includes emojicon labels 23 | 24 | flavor: Unflavored | {color: string, label: string} 25 | palette: list(flavor) 26 | attributes: { 27 | flavor, 28 | show = Overlay | Collapsed // overlay is color if flavored, collapsed is color + label or "..." if unflavored 29 | } 30 | 31 | model 1: 32 | model.paint = map(ids, attributes) 33 | 34 | view 1: 35 | for now implictly supress child Overlays, though retained Collapsed 36 | 37 | actions 1: 38 | paintNode(id, flavor) 39 | showNode(id, show) 40 | unpaintAllOf(flavor) 41 | unpaintAll() 42 | 43 | and then after that works, we can add a second painting abstraction, antipainting: 44 | add new flavor 'EscapePaint' (name needs work) 45 | this is only meaningful on decendents of a painted node 46 | and conceptually it creates a delimited multi-holed context 47 | simple use case: quasi-folding everything above a selected node 48 | 49 | */ 50 | -------------------------------------------------------------------------------- /src/Settings.tsx: -------------------------------------------------------------------------------- 1 | export type motion = "On" | "Off" | "Half"; 2 | 3 | export type projection = "LinearPrefix" | "LinearInfix" | "TreeLeft" | "TreeTop"; 4 | 5 | export type symbols = "Emoji" | "SingleChar"; 6 | 7 | export type theme = "Light" | "Dark"; 8 | 9 | export type t = { 10 | sound: boolean; 11 | motion: motion; 12 | preview: boolean; 13 | projection: projection; 14 | symbols: symbols; 15 | theme: theme; 16 | }; 17 | 18 | export type Action = 19 | | "ToggleSound" 20 | | "ToggleMotion" 21 | | "TogglePreview" 22 | | "ToggleProjection" 23 | | "ToggleSymbols" 24 | | "ToggleDark"; 25 | 26 | export const init: t = { 27 | sound: true, 28 | motion: "Half", 29 | preview: false, 30 | projection: "TreeLeft", 31 | symbols: "Emoji", 32 | theme: "Light", 33 | }; 34 | 35 | export const update = (settings: t, action: Action): t => { 36 | switch (action) { 37 | case "ToggleSound": 38 | return { ...settings, sound: !settings.sound }; 39 | case "ToggleMotion": 40 | switch (settings.motion) { 41 | case "On": 42 | return { ...settings, motion: "Off" }; 43 | case "Off": 44 | return { ...settings, motion: "Half" }; 45 | case "Half": 46 | return { ...settings, motion: "On" }; 47 | } 48 | case "TogglePreview": 49 | return { ...settings, preview: !settings.preview }; 50 | case "ToggleProjection": 51 | switch (settings.projection) { 52 | case "LinearPrefix": 53 | return { ...settings, projection: "LinearInfix" }; 54 | case "LinearInfix": 55 | return { ...settings, projection: "TreeTop" }; 56 | case "TreeTop": 57 | return { ...settings, projection: "TreeLeft" }; 58 | case "TreeLeft": 59 | return { ...settings, projection: "LinearPrefix" }; 60 | 61 | } 62 | case "ToggleSymbols": 63 | switch (settings.symbols) { 64 | case "Emoji": 65 | return { ...settings, symbols: "SingleChar" }; 66 | case "SingleChar": 67 | return { ...settings, symbols: "Emoji" }; 68 | } 69 | case "ToggleDark": 70 | switch (settings.theme) { 71 | case "Light": 72 | return { ...settings, theme: "Dark" }; 73 | case "Dark": 74 | return { ...settings, theme: "Light" }; 75 | } 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /src/Sound.tsx: -------------------------------------------------------------------------------- 1 | import * as Tone from "tone"; 2 | import pew from "./assets/sfx/pew.m4a"; 3 | import pshew from "./assets/sfx/pshew.m4a"; 4 | import klohk from "./assets/sfx/klohk.m4a"; 5 | import chchiu from "./assets/sfx/chchiu-out.wav"; 6 | import shwoph from "./assets/sfx/shwo-ph-out.wav"; 7 | import tiup from "./assets/sfx/tiup-comm-out.wav"; 8 | 9 | let revsfx = new Tone.Reverb({ decay: 4, wet: 0.3 }).toDestination(); 10 | 11 | export type Sfxbank = "pew" | "pshew" | "klohk" | "chchiu" | "shwoph" | "tiup"; 12 | 13 | const player_pew = new Tone.Player(pew).toDestination().connect(revsfx); 14 | player_pew.playbackRate = 1.3; 15 | const player_pshew = new Tone.Player(pshew).toDestination().connect(revsfx); 16 | player_pshew.playbackRate = 1.3; 17 | const player_klohk = new Tone.Player(klohk).toDestination().connect(revsfx); 18 | const player_chchiu = new Tone.Player(chchiu).toDestination().connect(revsfx); 19 | //let ps = new Tone.PitchShift(-3).toDestination(); 20 | //player_chchiu.connect(ps); 21 | player_chchiu.playbackRate = 1.7; 22 | const player_shwoph = new Tone.Player(shwoph).toDestination().connect(revsfx); 23 | player_shwoph.playbackRate = 1.7; 24 | const player_tiup = new Tone.Player(tiup).toDestination().connect(revsfx); 25 | player_tiup.volume.value = -12; 26 | 27 | const sfx_bank = (sfx: Sfxbank): Tone.Player => 28 | ({ 29 | tiup: player_tiup, 30 | shwoph: player_shwoph, 31 | chchiu: player_chchiu, 32 | klohk: player_klohk, 33 | pew: player_pew, 34 | pshew: player_pshew, 35 | 36 | }[sfx]); 37 | 38 | export const sfx = (sfx: Sfxbank) => () => { 39 | let p = sfx_bank(sfx); 40 | p.reverse = false; 41 | p.start(); 42 | }; 43 | 44 | export const sfx_reverse = (sfx: Sfxbank) => () => { 45 | let p = sfx_bank(sfx); 46 | p.reverse = true; 47 | p.start(); 48 | }; 49 | 50 | const player = new Tone.Player(pew).toDestination(); 51 | let rev2 = new Tone.Reverb(2).toDestination(); 52 | player.connect(rev2); 53 | 54 | const synth = new Tone.PolySynth(Tone.Synth).toDestination(); 55 | //const distortion = new Tone.Distortion(0.4).toDestination(); 56 | //synth.connect(distortion); 57 | /*var phaser = new Tone.Phaser({ 58 | "frequency" : 350, 59 | "octaves" : 4, 60 | "baseFrequency" : 500 61 | }).toDestination(); 62 | synth.connect(phaser);*/ 63 | let rev = new Tone.Reverb(10).toDestination(); 64 | synth.connect(rev); 65 | 66 | let tremolo = new Tone.Tremolo(70, 1.0).toDestination().start(); 67 | synth.connect(tremolo); 68 | //let vib = new Tone.Vibrato ( 10,0.3 ).toDestination(); 69 | //synth.connect(vib); 70 | 71 | export const mk = (note: string, duration: string) => () => 72 | synth.triggerAttackRelease(note, duration); 73 | 74 | const number_to_letter = (n: number): string => { 75 | const letters = "ACFBDG"; 76 | return letters[n % letters.length]; 77 | }; 78 | 79 | export const select = (depth: number, pitch:number, volume:number) => { 80 | //const chorus = new Tone.Chorus(4, 2.5, 0.5).toDestination().start(); 81 | //synth.connect(chorus); 82 | //const cheby = new Tone.Chebyshev(2).toDestination(); 83 | //synth.connect(cheby); 84 | synth.volume.value = volume; 85 | synth.triggerAttackRelease([number_to_letter(depth) + pitch], "32n"); 86 | }; 87 | 88 | export const unselect = (note: string, volume:number) => { 89 | //const chorus = new Tone.Chorus(4, 2.5, 0.5).toDestination().start(); 90 | //synth.connect(chorus); 91 | //const cheby = new Tone.Chebyshev(2).toDestination(); 92 | //synth.connect(cheby); 93 | synth.volume.value = volume; 94 | synth.triggerAttackRelease([note], "8n"); 95 | }; 96 | 97 | export const noop = () => synth.triggerAttackRelease("F1", "32n"); 98 | -------------------------------------------------------------------------------- /src/Stage.tsx: -------------------------------------------------------------------------------- 1 | import { Exp } from "./syntax/Exp"; 2 | import * as Statics from "./Statics"; 3 | import * as Path from "./syntax/Path"; 4 | import * as Projector from "./Projector"; 5 | import * as World from "./data/World"; 6 | import { is_path_valid } from "./syntax/Node"; 7 | 8 | export type selection = "unselected" | Path.t; 9 | 10 | export type Stage = { 11 | exp: Exp; 12 | selection: selection; 13 | info: Statics.InfoMap; //derived from exp 14 | projectors: Projector.PMap; //annotations 15 | }; 16 | 17 | export type t = Stage; 18 | 19 | const exp: Exp = World.init; 20 | 21 | export const init: Stage = { 22 | exp, 23 | selection: "unselected", 24 | info: Statics.mk(exp, []), 25 | projectors: Projector.init, 26 | }; 27 | 28 | export const put_exp = (stage: Stage, exp: Exp): Stage => ({ 29 | ...stage, 30 | exp: exp, 31 | info: Statics.mk(exp, []), 32 | }); 33 | 34 | export const put_selection = (stage: Stage, path: selection): Stage => ({ 35 | ...stage, 36 | selection: path, 37 | }); 38 | 39 | export const unset_selection = (stage: Stage): Stage => 40 | put_selection(stage, "unselected"); 41 | 42 | const rev = (path: Path.t): Path.t => { 43 | const blah = [...path]; 44 | return blah.reverse(); 45 | }; 46 | 47 | const move_up = (exp: Exp, selection: number[]): Path.t => { 48 | if (selection.length === 0) return selection; 49 | const [last, ...tl] = rev(selection); 50 | const new_path = rev([last - 1, ...tl]); 51 | //TODO: hack, hardcoded skipping of head via !=1 check 52 | if (last != 1 && is_path_valid(new_path, exp)) { 53 | return new_path; 54 | } else return selection; 55 | }; 56 | 57 | const move_down = (exp: Exp, selection: number[]): Path.t => { 58 | /* first, try to increment last idx of selection. 59 | if that gives a valid path, return it. 60 | then try to increment second last index, etc. 61 | If none of those are valid, 62 | drop the last element of the selection, and recurse. 63 | */ 64 | if (selection.length === 0) return selection; 65 | for (let i = selection.length - 1; i >= 0; i--) { 66 | const new_path = [...selection]; 67 | new_path[i]++; 68 | for (let j = i + 1; j < new_path.length; j++) { 69 | new_path[j] = 1; /* 1 is starting index as we're skipping the head */ 70 | } 71 | if (is_path_valid(new_path, exp)) { 72 | return new_path; 73 | } 74 | } 75 | return move_down(exp, Array(selection.length).fill(0)); 76 | }; 77 | 78 | const move_left = (_exp: Exp, selection: number[]): Path.t => { 79 | if (selection.length === 0) { 80 | return selection; 81 | } else { 82 | return selection.slice(0, selection.length - 1); 83 | } 84 | }; 85 | 86 | const move_right = (exp: Exp, selection: number[]): Path.t => { 87 | const new_selection = [...selection, 1]; 88 | if (is_path_valid(new_selection, exp)) { 89 | return new_selection; 90 | } else { 91 | return selection; 92 | } 93 | }; 94 | 95 | export const move_ = ( 96 | stage: Stage, 97 | direction: "up" | "down" | "left" | "right" 98 | ): Path.t => { 99 | if (stage.selection === "unselected") return []; 100 | const selection = stage.selection; 101 | switch (direction) { 102 | case "up": 103 | return move_up(stage.exp, selection); 104 | case "down": 105 | return move_down(stage.exp, selection); 106 | case "left": 107 | return move_left(stage.exp, selection); 108 | case "right": 109 | return move_right(stage.exp, selection); 110 | } 111 | }; 112 | 113 | export const move = ( 114 | stage: Stage, 115 | direction: "up" | "down" | "left" | "right" 116 | ) => put_selection(stage, move_(stage, direction)); 117 | -------------------------------------------------------------------------------- /src/Statics.tsx: -------------------------------------------------------------------------------- 1 | import * as Exp from "./syntax/Exp"; 2 | import * as Path from "./syntax/Path"; 3 | import * as ID from "./syntax/ID"; 4 | import { size } from "./syntax/Node"; 5 | 6 | export type Info = { 7 | path: Path.t; 8 | ancestors: ID.t[]; 9 | size: number; 10 | depth: number; 11 | //id: ID; 12 | //depth: number; // length of path 13 | //parent: number; // head of path 14 | }; 15 | 16 | export type InfoMap = Map; 17 | 18 | export const mk = (exp: Exp.t, ancestors: ID.t[]): InfoMap => { 19 | const map = new Map(); 20 | const go = (exp: Exp.t, path: Path.t = []): void => { 21 | const id = exp.id; 22 | const info = { 23 | path: [...path], 24 | size: size(exp), 25 | depth: path.length, 26 | // add id to beginning of ancestors 27 | ancestors: [id, ...ancestors], 28 | }; 29 | map.set(id, info); 30 | switch (exp.t) { 31 | case "Comp": 32 | exp.kids.forEach((kid, index) => go(kid, [...path, index])); 33 | break; 34 | case "Atom": 35 | // No operation for Atom case so far 36 | break; 37 | } 38 | }; 39 | go(exp); 40 | return map; 41 | }; 42 | 43 | export const get = (map: InfoMap, id: ID.t): Info => { 44 | const info = map.get(id); 45 | if (info == undefined) { 46 | console.log(`InfoMap.get: id ${id} not found`); 47 | //throw new Error(`InfoMap.get: id ${id} not found`); 48 | return { path: [], depth: 0, size: 0, ancestors: [] }; 49 | } else { 50 | return info; 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /src/ToolBox.tsx: -------------------------------------------------------------------------------- 1 | import * as Transform from "./Transform"; 2 | import * as Tools from "./data/Tools"; 3 | import * as Path from "./syntax/Path"; 4 | import * as Action from "./Action"; 5 | 6 | export type t = { 7 | selector: Path.t; 8 | transforms: Transform.t[]; 9 | offset: number; 10 | size: number; 11 | }; 12 | 13 | export const init: t = { 14 | selector: [], 15 | transforms: Tools.init, 16 | offset: 0, 17 | size: 5, 18 | }; 19 | 20 | export const update_selector = (tools: t, f: (_: Path.t) => Path.t): t => ({ 21 | ...tools, 22 | selector: f(tools.selector), 23 | }); 24 | 25 | export const init_selector = (selector: number[]): number[] => 26 | selector.length < 2 ? [0, 1] : selector; 27 | 28 | const move_up = 29 | (len: number) => 30 | (selector: number[]): number[] => { 31 | const [hd, ...tl] = init_selector(selector); 32 | return [(hd - 1 + len) % len, ...tl]; 33 | }; 34 | 35 | const move_down = 36 | (len: number) => 37 | (selector: number[]): number[] => { 38 | const [hd, ...tl] = init_selector(selector); 39 | return [(hd + 1 + len) % len, ...tl]; 40 | }; 41 | 42 | const move_left = (selector: number[]): number[] => { 43 | const [hd, snd, ...tl] = init_selector(selector); 44 | return [hd, (snd - 1 + 2) % 2, ...tl]; 45 | }; 46 | 47 | const move_right = (selector: number[]): number[] => { 48 | const [hd, snd, ...tl] = init_selector(selector); 49 | return [hd, (snd + 1 + 2) % 2, ...tl]; 50 | }; 51 | 52 | export const get = (selector: number[]): Action.t => { 53 | selector = selector.length < 2 ? [0, 0] : selector; 54 | return { 55 | t: "applyTransform", 56 | idx: selector[0], 57 | direction: selector[1] == 0 ? "forward" : "reverse", 58 | }; 59 | }; 60 | 61 | export const move = ( 62 | tools: t, 63 | direction: "up" | "down" | "left" | "right" 64 | ): t => { 65 | const len = tools.transforms.length; 66 | switch (direction) { 67 | case "up": 68 | return update_selector(tools, move_up(len)); 69 | case "down": 70 | return update_selector(tools, move_down(len)); 71 | case "left": 72 | return update_selector(tools, move_left); 73 | case "right": 74 | return update_selector(tools, move_right); 75 | } 76 | }; 77 | 78 | export const get_pat = (tools: t) => { 79 | let t = tools.transforms[tools.selector[0]]; 80 | return tools.selector[1] === 1 ? t.result : t.source; 81 | }; 82 | 83 | export const unset = (tools: t): t => ({ 84 | ...tools, 85 | selector: [], 86 | }); 87 | 88 | const flip_at_index = (ts: Transform.t[], idx: number): Transform.t[] => 89 | ts.map((t, i) => (i === idx ? Transform.flip(t) : t)); 90 | 91 | export const flip_transform = (tools: t, idx: number): t => ({ 92 | ...tools, 93 | transforms: flip_at_index(tools.transforms, idx), 94 | }); 95 | -------------------------------------------------------------------------------- /src/Transform.tsx: -------------------------------------------------------------------------------- 1 | import * as Pat from "./syntax/Pat"; 2 | import { Exp } from "./syntax/Exp"; 3 | import * as Path from "./syntax/Path"; 4 | 5 | export type Transform = { 6 | name: string; 7 | source: Pat.t; 8 | result: Pat.t; 9 | sound: () => void; 10 | sound_rev: () => void; 11 | reversed: boolean; 12 | }; 13 | 14 | export type t = Transform; 15 | 16 | export const flip = (t: Transform): Transform => ({ 17 | ...t, 18 | source: t.result, 19 | result: t.source, 20 | reversed: !t.reversed, 21 | }); 22 | 23 | export const at_path = 24 | ({ source, result }: Transform, path: Path.t) => 25 | (exp: Exp): Pat.TransformResult => 26 | Pat.transform_at_path(exp, source, result, path); 27 | -------------------------------------------------------------------------------- /src/Update.tsx: -------------------------------------------------------------------------------- 1 | import { at_path } from "./Transform"; 2 | import Flipping from "flipping/lib/adapters/web"; 3 | import * as Model from "./Model"; 4 | import * as Sound from "./Sound"; 5 | import * as Settings from "./Settings"; 6 | import * as Stage from "./Stage"; 7 | import * as Action from "./Action"; 8 | import * as Transform from "./Transform"; 9 | import * as ToolBox from "./ToolBox"; 10 | import * as Hover from "./Hover"; 11 | import { freshen } from "./syntax/Node"; 12 | import * as Exp from "./syntax/Exp"; 13 | import * as Pat from "./syntax/Pat"; 14 | import { SetStoreFunction } from "solid-js/store"; 15 | import * as Path from "./syntax/Path"; 16 | import * as Animate from "./Animate"; 17 | import * as Util from "./Util"; 18 | 19 | 20 | export type result = Model.t | "NoChange"; 21 | 22 | export const of_theme = (theme: Settings.theme): [number, number] => { 23 | switch (theme) { 24 | case "Light": 25 | return [8, -20]; 26 | case "Dark": 27 | return [2, -8]; 28 | } 29 | }; 30 | 31 | export const sound = (model: Model.t, action: Action.t): void => { 32 | const [pitch, volume] = of_theme(model.settings.theme); 33 | switch (action.t) { 34 | case "transformNodeAndFlipTransform": 35 | let result = action.f(model.stage.exp); 36 | if (result != "NoMatch") { 37 | return action.transform.reversed 38 | ? action.transform.sound_rev() 39 | : action.transform.sound(); 40 | } else { 41 | Sound.noop(); 42 | } 43 | break; 44 | case "setSelect": 45 | Sound.select(action.path.length, pitch, volume); 46 | break; 47 | case "unsetSelections": 48 | Sound.unselect("D2", 0.6); 49 | break; 50 | case "moveTool": 51 | case "moveStage": 52 | Sound.select(model.stage.selection.length, pitch, volume); 53 | break; 54 | case "setSetting": 55 | Sound.sfx("pew")(); 56 | break; 57 | case "Noop": 58 | Sound.noop(); 59 | break; 60 | case "setHover": 61 | case "flipTransform": 62 | case "applyTransform": 63 | case "applyTransformSelected": 64 | undefined; 65 | } 66 | }; 67 | 68 | const update_stage = (model: Model.t, result: Pat.TransformResult): result => 69 | /* Freshening as-is is a hack to deal with e.g. distributivity which copies nodes */ 70 | result == "NoMatch" 71 | ? "NoChange" 72 | : { ...model, stage: Stage.put_exp(model.stage, freshen(result)) }; 73 | 74 | type ModelField = 75 | | { t: "stage"; path: Path.t; updater: any } 76 | | { t: "tools"; path: Path.t; updater: any } 77 | | { t: "hover"; hover: Hover.t } 78 | | { t: "settings"; settings: Settings.t }; 79 | 80 | /*export const imperative_update = ( 81 | setModel: SetStoreFunction, 82 | model: Model.t, 83 | field: ModelField 84 | ): void => { 85 | switch (field.t) { 86 | case "hover": 87 | setModel({ ...model, hover: field.hover }); 88 | break; 89 | case "settings": 90 | setModel({ ...model, settings: field.settings }); 91 | break; 92 | case "tools": 93 | setModel({ 94 | ...model, 95 | tools: ToolBox.update_path(field.updater, model.tools, field.path), 96 | }); 97 | break; 98 | case "stage": 99 | setModel({ 100 | ...model, 101 | stage: Stage.update_path(field.updater, model.stage, field.path), 102 | }); 103 | break; 104 | } 105 | };*/ 106 | 107 | export const update = (model: Model.t, action: Action.t): result => { 108 | switch (action.t) { 109 | case "restart": 110 | return Model.init; 111 | case "setSetting": 112 | return { 113 | ...model, 114 | settings: Settings.update(model.settings, action.action), 115 | }; 116 | case "setSelect": 117 | if ( 118 | model.stage.selection === "unselected" 119 | ? false 120 | : Path.eq(model.stage.selection, action.path) 121 | ) 122 | return "NoChange"; 123 | return { ...model, stage: Stage.put_selection(model.stage, action.path) }; 124 | case "setHover": 125 | if (Hover.eq(model.hover, action.target)) return "NoChange"; 126 | return { ...model, hover: action.target }; 127 | case "moveStage": 128 | return { ...model, stage: Stage.move(model.stage, action.direction) }; 129 | case "moveTool": 130 | let tools = ToolBox.move(model.tools, action.direction); 131 | const hover: Hover.t = { 132 | t: "TransformSource", 133 | pat: ToolBox.get_pat(tools), 134 | idx: 0, 135 | }; 136 | return { ...model, tools, hover: hover }; 137 | case "unsetSelections": 138 | return { 139 | ...model, 140 | stage: Stage.unset_selection(model.stage), 141 | tools: ToolBox.unset(model.tools), 142 | hover: Hover.init, 143 | }; 144 | case "transformNode": 145 | let result = action.f(model.stage.exp); 146 | return update_stage(model, result); 147 | case "transformNodeAndFlipTransform": 148 | let result3 = action.f(model.stage.exp); 149 | let model3: Model.t = { 150 | ...model, 151 | tools: ToolBox.flip_transform(model.tools, action.idx), 152 | hover: 153 | action.target === "Source" 154 | ? { 155 | t: "TransformSource", 156 | pat: action.transform.result, 157 | idx: action.idx, 158 | } 159 | : { 160 | t: "TransformResult", 161 | pat: action.transform.source, 162 | idx: action.idx, 163 | }, 164 | }; 165 | return update_stage(model3, result3); 166 | case "applyTransform": 167 | if ( 168 | action.idx < 0 || 169 | action.idx >= model.tools.transforms.length || 170 | model.stage.selection === "unselected" 171 | ) 172 | return "NoChange"; 173 | const transform1 = model.tools.transforms[action.idx]; 174 | const transform = 175 | action.direction == "forward" ? transform1 : Transform.flip(transform1); 176 | const result2 = at_path( 177 | transform, 178 | model.stage.selection 179 | )(model.stage.exp); 180 | if (result2 != "NoMatch") transform.sound(); //TODO 181 | return update_stage(model, result2); 182 | case "applyTransformSelected": 183 | const selector = ToolBox.init_selector(model.tools.selector); 184 | const model2 = { 185 | ...model, 186 | tools: ToolBox.update_selector(model.tools, (_) => selector), 187 | }; 188 | return update(model2, ToolBox.get(selector)); 189 | case "flipTransform": 190 | return { 191 | ...model, 192 | tools: ToolBox.flip_transform(model.tools, action.idx), 193 | }; 194 | case "Noop": 195 | return "NoChange"; 196 | case "wheelTools": 197 | console.log("wheelTools:" + action.offset + ":" + model.tools.offset); 198 | return { 199 | ...model, 200 | tools: { 201 | ...model.tools, 202 | offset: Util.mod( 203 | model.tools.offset + action.offset, 204 | model.tools.transforms.length 205 | ), 206 | }, 207 | }; 208 | case "wheelNumTools": 209 | const clamp = (x:number, a:number, b:number) => Math.max( a, Math.min(x, b) ); 210 | console.log("wheelNumTools:" + action.offset + ":" + model.tools.size); 211 | return { 212 | ...model, 213 | tools: { 214 | ...model.tools, 215 | size: clamp( 216 | model.tools.size + action.offset, 217 | 1, 218 | model.tools.transforms.length, 219 | 220 | 221 | ), 222 | }, 223 | }; 224 | } 225 | }; 226 | 227 | export const go = ( 228 | model: Model.t, 229 | setModel: SetStoreFunction, 230 | action: Action.t 231 | ): void => { 232 | if (model.settings.sound) sound(model, action); 233 | /* Catching because problem on build server */ 234 | try { 235 | //Animate.read(model, action); 236 | } catch (e) { 237 | console.error(e); 238 | } 239 | const result = update(model, action); 240 | if (result == "NoChange") { 241 | //Sound.noop(); 242 | console.log("Action NoChange:" + action.t); 243 | return; 244 | } else { 245 | console.log("Action Success: " + action.t); 246 | setModel(result); 247 | } 248 | /* HACK: We want transforms the duplicate subtrees e.g. distributivity to 249 | * retain their duplicate ids for animations, but then we need to freshen 250 | * them so that they don't get confused with the original subtree. So we 251 | * freshen them after a delay. THIS WILL CAUSE PROBLEMS!!!! */ 252 | /*setTimeout(() => { 253 | const freshened = freshen(model.stage.exp); 254 | if (!Exp.equals_id(model.stage.exp, freshened)) 255 | setModel({ 256 | ...model, 257 | stage: Stage.put_exp(model.stage, freshened), 258 | }); 259 | }, 250);*/ 260 | /* Catching because problem on build server */ 261 | try { 262 | //Animate.flip(model, action); 263 | } catch (e) { 264 | console.error(e); 265 | } 266 | }; 267 | -------------------------------------------------------------------------------- /src/Util.tsx: -------------------------------------------------------------------------------- 1 | export const zip = (a: A[], b: B[]): [A, B][] => 2 | a.map((i, j) => [i, b[j]]); 3 | 4 | export function mod(n: number, m: number): number { 5 | return ((n % m) + m) % m; 6 | } 7 | -------------------------------------------------------------------------------- /src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disconcision/nool/33eac2b75de90f10d605152465d22c956aee2c08/src/assets/favicon.ico -------------------------------------------------------------------------------- /src/assets/icons/eye-closed.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/eye-open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/motion-half.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/motion-off.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/motion-on.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/noun-1005147.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/icons/noun-1020541.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/noun-1037924.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/noun-1052053.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/noun-1156122.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/noun-1166087.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/noun-1275679.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/assets/icons/noun-1333353.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/noun-1560867.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/noun-1683765.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/noun-2048496.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/assets/icons/noun-2856200.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/assets/icons/noun-2902082.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/noun-2925980.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/icons/noun-3376875.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/noun-5454290.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/noun-5576595.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/assets/icons/noun-6336647.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/noun-agile-transformation-4617414.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/noun-alphabet-3591519.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/icons/noun-backpack-5550372.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/noun-bag-6335093.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/noun-bag-6335275.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/icons/noun-cloud-6325788.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/noun-collection-1675411.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/icons/noun-cycle-1793611.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/icons/noun-cycle-4446.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/icons/noun-dead-5130035.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/assets/icons/noun-digital-transformation-6011306.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/noun-earth-1219989.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/noun-geometry-4695832.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/icons/noun-list-10135.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/icons/noun-paint-6329583.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/noun-palette-1918496.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/noun-qr-4574196.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/noun-shopping-bag-6321186.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/noun-shopping-bag-6321318.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/noun-sigh-654547.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/icons/noun-subtitle-4574190.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/noun-sun-5362089.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/noun-sun-6322390.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/noun-tool-3376727.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/noun-tool-3376728.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/noun-transformation-1832026.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/icons/noun-transformation-3968008.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/icons/noun-transformation-6040368.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/noun-transformation-6040479.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/noun-transformation-6098909.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/icons/noun-tree-1039106.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/noun-tree-1052083.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/noun-tree-1052096.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/qr.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/sound-off.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/sound-on.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/marble.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disconcision/nool/33eac2b75de90f10d605152465d22c956aee2c08/src/assets/marble.jpeg -------------------------------------------------------------------------------- /src/assets/nool-seed-infinity-only.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/nool-seed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/nooltext-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disconcision/nool/33eac2b75de90f10d605152465d22c956aee2c08/src/assets/nooltext-light.png -------------------------------------------------------------------------------- /src/assets/nooltext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disconcision/nool/33eac2b75de90f10d605152465d22c956aee2c08/src/assets/nooltext.png -------------------------------------------------------------------------------- /src/assets/ps-toolbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disconcision/nool/33eac2b75de90f10d605152465d22c956aee2c08/src/assets/ps-toolbar.png -------------------------------------------------------------------------------- /src/assets/sfx/chchiu-out.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disconcision/nool/33eac2b75de90f10d605152465d22c956aee2c08/src/assets/sfx/chchiu-out.wav -------------------------------------------------------------------------------- /src/assets/sfx/klohk.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disconcision/nool/33eac2b75de90f10d605152465d22c956aee2c08/src/assets/sfx/klohk.m4a -------------------------------------------------------------------------------- /src/assets/sfx/pew.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disconcision/nool/33eac2b75de90f10d605152465d22c956aee2c08/src/assets/sfx/pew.m4a -------------------------------------------------------------------------------- /src/assets/sfx/pshew.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disconcision/nool/33eac2b75de90f10d605152465d22c956aee2c08/src/assets/sfx/pshew.m4a -------------------------------------------------------------------------------- /src/assets/sfx/shwo-ph-out.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disconcision/nool/33eac2b75de90f10d605152465d22c956aee2c08/src/assets/sfx/shwo-ph-out.wav -------------------------------------------------------------------------------- /src/assets/sfx/tiup-comm-out.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disconcision/nool/33eac2b75de90f10d605152465d22c956aee2c08/src/assets/sfx/tiup-comm-out.wav -------------------------------------------------------------------------------- /src/data/Tools.tsx: -------------------------------------------------------------------------------- 1 | import * as Pat from "../syntax/Pat"; 2 | import * as Sound from "../Sound"; 3 | import * as Transform from "../Transform"; 4 | 5 | type Base = { source: Pat.t; result: Pat.t; sound: Sound.Sfxbank }; 6 | 7 | 8 | 9 | const zero = Pat.p_const("🌑"); 10 | const one = Pat.p_const("🌘"); 11 | const hole = Pat.p_const("❓"); 12 | 13 | const var_a = Pat.p_var("♫"); 14 | const var_b = Pat.p_var("♥"); 15 | const var_c = Pat.p_var("✿"); 16 | 17 | const un_x = (op: string) => (a: Pat.t) => 18 | Pat.comp_id(-6, [Pat.const_id(-16, op), a]); 19 | const un_y = (op: string) => (a: Pat.t) => 20 | Pat.comp_id(-7, [Pat.const_id(-17, op), a]); 21 | const bin_x = (op: string) => (a: Pat.t, b: Pat.t) => 22 | Pat.comp_id(-8, [Pat.const_id(-18, op), a, b]); 23 | const bin_y = (op: string) => (a: Pat.t, b: Pat.t) => 24 | Pat.comp_id(-9, [Pat.const_id(-19, op), a, b]); 25 | 26 | const neg_x = un_x("➖"); 27 | const neg_y = un_y("➖"); 28 | const plus_x = bin_x("➕"); 29 | const plus_y = bin_y("➕"); 30 | const times_x = bin_x("✖️"); 31 | const times_y = bin_y("✖️"); 32 | const equals_x = bin_x("🟰"); 33 | 34 | const commute_plus: Base = { 35 | source: plus_x(var_a, var_b), 36 | result: plus_x(var_b, var_a), 37 | sound: "tiup", 38 | }; 39 | 40 | const associate_plus: Base = { 41 | source: plus_y(var_a, plus_x(var_b, var_c)), 42 | result: plus_y(plus_x(var_a, var_b), var_c), 43 | sound: "shwoph", 44 | }; 45 | 46 | const identity_plus: Base = { 47 | source: var_a, 48 | result: plus_x(zero, var_a), 49 | sound: "chchiu", 50 | }; 51 | 52 | const inverse_plus: Base = { 53 | source: zero, 54 | result: plus_x(var_a, neg_x(var_a)), 55 | sound: "klohk", 56 | }; 57 | 58 | const double_neg: Base = { 59 | source: var_a, 60 | result: neg_x(neg_y(var_a)), 61 | sound: "klohk", 62 | }; 63 | 64 | const commute_times: Base = { 65 | source: times_x(var_a, var_b), 66 | result: times_x(var_b, var_a), 67 | sound: "tiup", 68 | }; 69 | 70 | const associate_times: Base = { 71 | source: times_y(var_a, times_x(var_b, var_c)), 72 | result: times_y(times_x(var_a, var_b), var_c), 73 | sound: "shwoph", 74 | }; 75 | 76 | const identity_times: Base = { 77 | source: var_a, 78 | result: times_x(one, var_a), 79 | sound: "chchiu", 80 | }; 81 | 82 | const distribute_times_plus: Base = { 83 | source: plus_x(times_y(var_a, var_b), times_y(var_a, var_c)), 84 | result: times_y(var_a, plus_x(var_b, var_c)), 85 | sound: "shwoph", 86 | }; 87 | 88 | const mk_mk = (result: Pat.t): Base => ({ 89 | source: hole, 90 | result:result, 91 | sound: "klohk",//TODO 92 | }); 93 | const mk_wrap = (result: Pat.t): Base => ({ 94 | source: var_a, 95 | result:result, 96 | sound: "klohk",//TODO 97 | }); 98 | const mk_one = mk_mk(one); 99 | const mk_zero = mk_mk(zero); 100 | const mk_var_a = mk_mk(var_a); 101 | const mk_var_b = mk_mk(var_b); 102 | const mk_var_c = mk_mk(var_c); 103 | const mk_plus = mk_mk(plus_x(hole, hole)); 104 | const mk_times = mk_mk(times_x(hole, hole)); 105 | const mk_equals = mk_mk(equals_x(hole, hole)); 106 | const mk_neg = mk_mk(neg_x(hole)); 107 | const mk_neg_wrap = mk_wrap(neg_x(var_a)); 108 | const mk_times_l_wrap = mk_wrap(times_x(var_a, hole)); 109 | const mk_times_r_wrap = mk_wrap(times_x(hole, var_a)); 110 | const mk_plus_l_wrap = mk_wrap(plus_x(var_a, hole)); 111 | const mk_plus_r_wrap = mk_wrap(plus_x(hole, var_a)); 112 | const mk_equals_l_wrap = mk_wrap(equals_x(var_a, hole)); 113 | const mk_equals_r_wrap = mk_wrap(equals_x(hole, var_a)); 114 | const zap: Base = { 115 | source: var_a, 116 | result: hole, 117 | sound: "klohk", 118 | }; 119 | 120 | const makers = [ 121 | mk_one, 122 | mk_zero, 123 | mk_var_a, 124 | mk_var_b, 125 | mk_var_c, 126 | mk_neg, 127 | mk_plus, 128 | mk_times, 129 | mk_equals, 130 | mk_neg_wrap, 131 | mk_times_l_wrap, 132 | mk_times_r_wrap, 133 | mk_plus_l_wrap, 134 | mk_plus_r_wrap, 135 | mk_equals_l_wrap, 136 | mk_equals_r_wrap, 137 | //zap, 138 | ]; 139 | 140 | const mk = ({ source, result, sound }: Base): Transform.t => ({ 141 | name: "", 142 | source, 143 | result, 144 | sound: Sound.sfx(sound), 145 | sound_rev: Sound.sfx_reverse(sound), 146 | reversed: false, 147 | }); 148 | 149 | export const init = [ 150 | associate_plus, 151 | commute_plus, 152 | identity_plus, 153 | inverse_plus, 154 | associate_times, 155 | commute_times, 156 | identity_times, 157 | distribute_times_plus, 158 | double_neg, 159 | ].map(mk); 160 | 161 | export const _init =makers.map(mk); -------------------------------------------------------------------------------- /src/data/ToolsExp.tsx: -------------------------------------------------------------------------------- 1 | import { Exp, atom, comp } from "../syntax/Exp"; 2 | import * as Pat from "../syntax/Pat"; 3 | import * as Path from "../syntax/Path"; 4 | import * as Id from "../syntax/ID"; 5 | 6 | const exp_comm_plus: Exp = comp([ 7 | atom("="), 8 | comp([atom("➕"), atom("♫"), atom("♥")]), 9 | comp([atom("➕"), atom("♥"), atom("♫")]), 10 | ]); 11 | 12 | const exp_assoc_plus: Exp = comp([ 13 | atom("="), 14 | comp([atom("➕"), atom("♫"), comp([atom("➕"), atom("♥"), atom("✿")])]), 15 | comp([atom("➕"), comp([atom("➕"), atom("♫"), atom("♥")]), atom("✿")]), 16 | ]); 17 | 18 | const exp_id_plus: Exp = comp([ 19 | atom("="), 20 | atom("♫"), 21 | comp([atom("➕"), atom("🌑"), atom("♫")]), 22 | ]); 23 | 24 | const exp_inv_plus: Exp = comp([ 25 | atom("="), 26 | atom("🌑"), 27 | comp([atom("➕"), atom("♫"), atom("🌑")]), 28 | ]); 29 | 30 | const exp_double_neg: Exp = comp([ 31 | atom("="), 32 | atom("♫"), 33 | comp([atom("➖"), comp([atom("➖"), atom("♫")])]), 34 | ]); 35 | 36 | const exp_comm_times: Exp = comp([ 37 | atom("="), 38 | comp([atom("✖️"), atom("♫"), atom("♥")]), 39 | comp([atom("✖️"), atom("♥"), atom("♫")]), 40 | ]); 41 | 42 | const exp_assoc_times: Exp = comp([ 43 | atom("="), 44 | comp([atom("✖️"), atom("♫"), comp([atom("✖️"), atom("♥"), atom("✿")])]), 45 | comp([atom("✖️"), comp([atom("✖️"), atom("♫"), atom("♥")]), atom("✿")]), 46 | ]); 47 | 48 | const exp_id_times: Exp = comp([ 49 | atom("="), 50 | atom("♫"), 51 | comp([atom("✖️"), atom("🌑"), atom("♫")]), 52 | ]); 53 | 54 | export const exp_dist_times_plus: Exp = comp([ 55 | atom("="), 56 | comp([ 57 | atom("➕"), 58 | comp([atom("✖️"), atom("♫"), atom("♥")]), 59 | comp([atom("✖️"), atom("♫"), atom("✿")]), 60 | ]), 61 | comp([atom("✖️"), atom("♫"), comp([atom("➕"), atom("♥"), atom("✿")])]), 62 | ]); 63 | 64 | const box = (es: Exp[]): Exp => comp([atom("📦"), ...es]); 65 | 66 | export const init = box([ 67 | exp_comm_plus, 68 | exp_assoc_plus, 69 | exp_id_plus, 70 | exp_inv_plus, 71 | exp_double_neg, 72 | exp_comm_times, 73 | exp_assoc_times, 74 | exp_id_times, 75 | exp_dist_times_plus, 76 | ]); 77 | -------------------------------------------------------------------------------- /src/data/World.tsx: -------------------------------------------------------------------------------- 1 | import { Exp, atom, comp } from "../syntax/Exp"; 2 | 3 | // ☁️ 🧩 🦷 🦠 🌸 🍄 🎲 🐝 4 | // ➕ ➖ ✖️ ➗ 🟰 🌕 🌘 0️⃣ 1️⃣ 5 | 6 | export const init: Exp = comp([ 7 | atom("➕"), 8 | comp([ 9 | atom("➕"), 10 | comp([atom("➕"), atom("☁️"), comp([atom("➖"), atom("🍄")])]), 11 | atom("🍄"), 12 | ]), 13 | comp([ 14 | atom("➕"), 15 | comp([atom("✖️"), atom("🎲"), atom("🦠")]), 16 | comp([atom("✖️"), atom("🎲"), atom("🐝")]), 17 | ]), 18 | ]); 19 | 20 | export const _init:Exp = atom("❓"); 21 | 22 | export const moons: Exp = comp([ 23 | atom("➕"), 24 | comp([ 25 | atom("➕"), 26 | comp([atom("➕"), atom("🌘🌕"), comp([atom("➖"), atom("🌘🌕🌕🌕🌘")])]), 27 | atom("🌘🌕 "), 28 | ]), 29 | comp([ 30 | atom("➕"), 31 | comp([atom("✖️"), atom("🌕"), atom("🌘")]), 32 | comp([atom("✖️"), atom("🌘🌕🌘🌕 "), atom("🌘🌘")]), 33 | ]), 34 | ]); 35 | 36 | const pv = (hd: string, tl: Exp) => comp([atom("."), atom(hd), tl]); 37 | 38 | export const moons2: Exp = comp([ 39 | atom("➕"), 40 | comp([ 41 | atom("➕"), 42 | comp([ 43 | atom("➕"), 44 | pv("🌘", atom("🌕")), 45 | comp([atom("➖"), pv("🌘", pv("🌕", pv("🌕", pv("🌕", atom("🌘")))))]), 46 | ]), 47 | pv("🌘", atom("🌕")), 48 | ]), 49 | comp([ 50 | atom("➕"), 51 | comp([ 52 | atom("✖️"), 53 | pv("🌘", pv("🌕", pv("🌘", atom("🌕")))), 54 | pv("🌘", atom("🌘")), 55 | ]), 56 | ]), 57 | ]); 58 | -------------------------------------------------------------------------------- /src/declarations.d.ts: -------------------------------------------------------------------------------- 1 | // Necessary to quiet TypeScript errors about m4a audio assets 2 | declare module "*.m4a" { 3 | const src: string; 4 | export default src; 5 | } -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | /* @refresh reload */ 2 | import { render } from 'solid-js/web'; 3 | 4 | import './index.css'; 5 | //import 'bootstrap/dist/css/bootstrap.min.css' 6 | import App from './App'; 7 | 8 | const root = document.getElementById('root'); 9 | 10 | if (import.meta.env.DEV && !(root instanceof HTMLElement)) { 11 | throw new Error( 12 | 'Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got mispelled?', 13 | ); 14 | } 15 | 16 | render(() => , root!); 17 | -------------------------------------------------------------------------------- /src/syntax/Exp.tsx: -------------------------------------------------------------------------------- 1 | import * as Node from "./Node"; 2 | 3 | export type t = Node.t; 4 | export type Exp = t; 5 | 6 | export const atom_id = Node.atom_id; 7 | export const comp_id = Node.comp_id; 8 | export const atom = Node.atom; 9 | export const comp = Node.comp; 10 | export const equals = Node.equals; 11 | export const equals_id = Node.equals_id; 12 | export const erase = Node.erase; 13 | export const depth = Node.depth; 14 | -------------------------------------------------------------------------------- /src/syntax/ID.tsx: -------------------------------------------------------------------------------- 1 | export type t = number; 2 | export type ID = t; 3 | 4 | var id_gen = 0; 5 | export const mk = () => id_gen++; 6 | -------------------------------------------------------------------------------- /src/syntax/Node.test.tsx: -------------------------------------------------------------------------------- 1 | import { Exp, erase, atom, comp } from "./Exp"; 2 | import { 3 | val, 4 | TransformResult, 5 | matches, 6 | hydrate, 7 | transform, 8 | p_var, 9 | p_const, 10 | p_comp, 11 | } from "./Pat"; 12 | 13 | describe("test matches", () => { 14 | it("atoms", () => { 15 | expect(matches(p_comp([]), comp([]))).toEqual([]); 16 | expect(matches(p_const("a"), atom("a"))).toEqual([]); 17 | expect(matches(p_const("a"), atom("b"))).toEqual("NoMatch"); 18 | let tree_1 = atom("b"); 19 | let tree_2 = comp([atom("a"), atom("b")]); 20 | expect(matches(p_var("a"), tree_1)).toEqual([["a", tree_1]]); 21 | expect(matches(p_var("x"), tree_2)).toEqual([["x", tree_2]]); 22 | }); 23 | }); 24 | 25 | describe("test matches", () => { 26 | it("binary deconstruction", () => { 27 | let head = atom("+"); 28 | let branch_1 = atom("1"); 29 | let branch_2 = atom("2"); 30 | let branch_3 = atom("3"); 31 | let tree_1 = comp([head, branch_1, branch_2]); 32 | let pat_1 = p_comp([p_const("+"), p_var("a"), p_var("b")]); 33 | expect(matches(pat_1, tree_1)).toEqual([ 34 | ["a", branch_1], 35 | ["b", branch_2], 36 | ]); 37 | 38 | let pat_2 = p_comp([p_var("+"), p_var("a"), p_var("b")]); 39 | expect(matches(pat_2, tree_1)).toEqual([ 40 | ["+", head], 41 | ["a", branch_1], 42 | ["b", branch_2], 43 | ]); 44 | 45 | let tree_3 = comp([head, tree_1, branch_3]); 46 | let pat_3 = p_comp([ 47 | p_const("+"), 48 | p_comp([p_const("+"), p_const("1"), p_var("b")]), 49 | p_var("c"), 50 | ]); 51 | expect(matches(pat_3, tree_3)).toEqual([ 52 | ["b", branch_2], 53 | ["c", branch_3], 54 | ]); 55 | }); 56 | }); 57 | 58 | const expect_exp = (e_in: Exp, e_out: Exp) => 59 | expect(erase(e_in)).toEqual(erase(e_out)); 60 | 61 | const v = (b: any, c: any) => val(0, 1, b, c); 62 | 63 | describe("test hydrate", () => { 64 | it("basic hydration", () => { 65 | expect_exp(hydrate(p_var("a"), []), atom("a")); 66 | expect_exp(hydrate(p_const("a"), [v("a", atom("1"))]), atom("a")); 67 | expect_exp(hydrate(p_var("a"), [v("a", atom("1"))]), atom("1")); 68 | // duplicate bindings use first for now 69 | expect_exp( 70 | hydrate(p_var("a"), [v("a", atom("1")), v("a", atom("2"))]), 71 | atom("1") 72 | ); 73 | expect_exp( 74 | hydrate(p_comp([p_const("+"), p_var("a"), p_var("b")]), [ 75 | v("a", atom("1")), 76 | v("b", atom("2")), 77 | ]), 78 | comp([atom("+"), atom("1"), atom("2")]) 79 | ); 80 | expect_exp( 81 | hydrate( 82 | p_comp([p_const("+"), p_var("a"), p_comp([p_var("b"), p_var("c")])]), 83 | [v("a", atom("1")), v("c", atom("2"))] 84 | ), 85 | comp([atom("+"), atom("1"), comp([atom("b"), atom("2")])]) 86 | ); 87 | }); 88 | }); 89 | 90 | describe("test root transform", () => { 91 | let expect_exp = (e_in: TransformResult, e_out: TransformResult) => 92 | expect(e_in == "NoMatch" ? "NoMatch" : erase(e_in)).toEqual( 93 | e_out == "NoMatch" ? "NoMatch" : erase(e_out) 94 | ); 95 | it("transform degenerate cases", () => { 96 | expect_exp(transform(atom("1"), p_var("a"), p_var("b")), atom("b")); 97 | expect_exp(transform(atom("1"), p_const("1"), p_var("c")), atom("c")); 98 | expect_exp(transform(atom("1"), p_const("2"), p_var("d")), "NoMatch"); 99 | }); 100 | 101 | it("end to end transform basic", () => { 102 | expect_exp(transform(atom("1"), p_var("a"), p_var("a")), atom("1")); 103 | expect_exp(transform(atom("1"), p_const("b"), p_var("b")), "NoMatch"); 104 | }); 105 | 106 | it("end to end commutativity +", () => { 107 | expect_exp( 108 | transform( 109 | comp([atom("+"), atom("1"), atom("2")]), 110 | p_comp([p_const("+"), p_var("a"), p_var("b")]), 111 | p_comp([p_const("+"), p_var("b"), p_var("a")]) 112 | ), 113 | comp([atom("+"), atom("2"), atom("1")]) 114 | ); 115 | }); 116 | 117 | it("end to end associativity +", () => { 118 | expect_exp( 119 | transform( 120 | comp([atom("+"), atom("1"), comp([atom("+"), atom("2"), atom("3")])]), 121 | p_comp([ 122 | p_const("+"), 123 | p_var("a"), 124 | p_comp([p_const("+"), p_var("b"), p_var("c")]), 125 | ]), 126 | p_comp([ 127 | p_const("+"), 128 | p_comp([p_const("+"), p_var("a"), p_var("b")]), 129 | p_var("c"), 130 | ]) 131 | ), 132 | comp([atom("+"), comp([atom("+"), atom("1"), atom("2")]), atom("3")]) 133 | ); 134 | }); 135 | }); 136 | -------------------------------------------------------------------------------- /src/syntax/Node.tsx: -------------------------------------------------------------------------------- 1 | import { zip } from "../Util"; 2 | import * as ID from "./ID"; 3 | import * as Path from "./Path"; 4 | 5 | export type t = 6 | | { t: "Atom"; id: ID.t; sym: T } 7 | | { t: "Comp"; id: ID.t; kids: t[] }; 8 | 9 | export type Node = t; 10 | 11 | export function atom_id(sym: T, id: number): t { 12 | return { t: "Atom", id, sym }; 13 | } 14 | 15 | export function comp_id(kids: t[], id: number): t { 16 | return { t: "Comp", id, kids }; 17 | } 18 | 19 | export function atom(sym: T): t { 20 | return atom_id(sym, ID.mk()); 21 | } 22 | 23 | export function comp(kids: t[]): t { 24 | return comp_id(kids, ID.mk()); 25 | } 26 | 27 | /* Map over ids */ 28 | export function map_ids(f: (id: ID.t) => ID.t, e: t): t { 29 | switch (e.t) { 30 | case "Atom": 31 | return atom_id(e.sym, f(e.id)); 32 | case "Comp": 33 | return comp_id( 34 | e.kids.map((x) => map_ids(f, x)), 35 | f(e.id) 36 | ); 37 | } 38 | } 39 | 40 | /* Zero out all ids */ 41 | export function erase(x: t): t { 42 | return map_ids((_) => 0, x); 43 | } 44 | 45 | /* New ids for all nodes */ 46 | export function freshen_all_ids(x: t): t { 47 | return map_ids((_) => ID.mk(), x); 48 | } 49 | 50 | /* Length of longest path from root to a leaf */ 51 | export function depth(node: t): number { 52 | switch (node.t) { 53 | case "Atom": 54 | return 0; 55 | case "Comp": 56 | return 1 + Math.max(...node.kids.map(depth)); 57 | } 58 | } 59 | 60 | /* Structural equality modulo ids */ 61 | export function equals(a: t, b: t): boolean { 62 | switch (a.t) { 63 | case "Atom": 64 | switch (b.t) { 65 | case "Atom": 66 | return a.sym == b.sym; 67 | case "Comp": 68 | return false; 69 | } 70 | case "Comp": 71 | switch (b.t) { 72 | case "Atom": 73 | return false; 74 | case "Comp": 75 | return ( 76 | a.kids.length == b.kids.length && 77 | zip(a.kids, b.kids).every(([a, b]) => equals(a, b)) 78 | ); 79 | } 80 | } 81 | } 82 | 83 | /* Structural equality including equal ids */ 84 | export function equals_id(a: t, b: t): boolean { 85 | switch (a.t) { 86 | case "Atom": 87 | switch (b.t) { 88 | case "Atom": 89 | return a.sym == b.sym && a.id == b.id; 90 | case "Comp": 91 | return false; 92 | } 93 | case "Comp": 94 | switch (b.t) { 95 | case "Atom": 96 | return false; 97 | case "Comp": 98 | return ( 99 | a.id == b.id && 100 | a.kids.length == b.kids.length && 101 | zip(a.kids, b.kids).every(([a, b]) => equals_id(a, b)) 102 | ); 103 | } 104 | } 105 | } 106 | 107 | export function size(node: t): number { 108 | switch (node.t) { 109 | case "Atom": 110 | return 1; 111 | case "Comp": 112 | return 1 + node.kids.reduce((acc, n) => acc + size(n), 0); 113 | } 114 | } 115 | 116 | /* Replace the id of the root node with new_id */ 117 | function replace_root_id(e: t, new_id: number): t { 118 | switch (e.t) { 119 | case "Atom": 120 | return atom_id(e.sym, new_id); 121 | case "Comp": 122 | return comp_id(e.kids, new_id); 123 | } 124 | } 125 | 126 | /* If path is valid, return indicated subtree */ 127 | export function subtree_at(p: Path.t, n: t): t | undefined { 128 | switch (n.t) { 129 | case "Atom": 130 | return p.length === 0 ? n : undefined; 131 | case "Comp": 132 | if (p.length === 0) return n; 133 | let [hd, ...tl] = p; 134 | if (n.kids[hd] == undefined) return undefined; 135 | return subtree_at(tl, n.kids[hd]); 136 | } 137 | } 138 | 139 | /* If path is valid, returns ID of indicated node */ 140 | export function id_at(p: Path.t, n: t): ID.t | undefined { 141 | const subtree = subtree_at(p, n); 142 | if (subtree == undefined) return undefined; 143 | else return subtree.id; 144 | } 145 | 146 | export function is_path_valid(p: Path.t, n: t): boolean { 147 | return subtree_at(p, n) != undefined; 148 | } 149 | 150 | export function freshen_traverse( 151 | node: t, 152 | seen: Set, 153 | replace: (id: ID.t) => ID.t 154 | ): t { 155 | const blah = seen.has(node.id) 156 | ? replace_root_id(node, replace(node.id)) 157 | : node; 158 | if (!seen.has(node.id)) seen.add(node.id); 159 | switch (blah.t) { 160 | case "Atom": 161 | return blah; 162 | case "Comp": 163 | return comp_id( 164 | blah.kids.map((k) => freshen_traverse(k, seen, replace)), 165 | blah.id 166 | ); 167 | } 168 | } 169 | 170 | /** 171 | * Traverse a node tree. Keep track of ids seen. 172 | * When an id is seen twice, replace it with a new id. 173 | */ 174 | export function freshen(node: t): t { 175 | return freshen_traverse(node, new Set(), (_) => ID.mk()); 176 | } 177 | -------------------------------------------------------------------------------- /src/syntax/Pat.tsx: -------------------------------------------------------------------------------- 1 | import { zip } from "../Util"; 2 | import * as Node from "./Node"; 3 | import * as Exp from "./Exp"; 4 | import * as ID from "./ID"; 5 | import * as Path from "./Path"; 6 | 7 | export type Symbol = { t: "Var"; name: string } | { t: "Const"; name: string }; 8 | 9 | export type Pat = Node.t; 10 | export type t = Pat; 11 | 12 | export const const_id = (id: number, name: string): Pat => 13 | Node.atom_id({ t: "Const", name }, id); 14 | 15 | export const var_id = (id: number, name: string): Pat => 16 | Node.atom_id({ t: "Var", name }, id); 17 | 18 | export const comp_id = (id: number, kids: Pat[]): Pat => Node.comp_id(kids, id); 19 | 20 | export const p_const = (name: string): Pat => const_id(ID.mk(), name); 21 | export const p_var = (name: string): Pat => var_id(ID.mk(), name); 22 | export const p_comp = (kids: Pat[]): Pat => comp_id(ID.mk(), kids); 23 | 24 | export const eq = (a: Pat, b: Pat): boolean => Node.equals(a, b); 25 | 26 | type NameBinding = [string, Exp.t]; 27 | 28 | type IdBinding = [number, number]; 29 | 30 | export type Binding = 31 | | { t: "Val"; ids: IdBinding; val: NameBinding } 32 | | { t: "Ids"; ids: IdBinding }; 33 | 34 | export type MatchResult = Binding[] | "NoMatch"; 35 | 36 | export type TransformResult = Exp.t | "NoMatch"; 37 | 38 | export const val = ( 39 | id1: number, 40 | id2: number, 41 | name: string, 42 | exp: Exp.t 43 | ): Binding => ({ 44 | t: "Val", 45 | val: [name, exp], 46 | ids: [id1, id2], 47 | }); 48 | 49 | const ids = (id1: number, id2: number): Binding => ({ 50 | t: "Ids", 51 | ids: [id1, id2], 52 | }); 53 | 54 | const name_bindings = (bindings: Binding[]): NameBinding[] => 55 | bindings 56 | .map((bind) => { 57 | switch (bind.t) { 58 | case "Val": 59 | return [bind.val]; 60 | case "Ids": 61 | return []; 62 | } 63 | }) 64 | .flat(); 65 | 66 | /* Enforces linearity: Duplicate bindings must have the same value. 67 | Leaves duplicates intact as they are used for highlighting. */ 68 | const concat_bindings = (a: MatchResult, b: MatchResult): MatchResult => { 69 | if (a === "NoMatch" || b === "NoMatch") return "NoMatch"; 70 | let a_names = name_bindings(a); 71 | let b_has_different_valud_dupe = b.find( 72 | (bind) => 73 | bind.t == "Val" && 74 | a_names.find( 75 | ([name, exp]) => name == bind.val[0] && !Exp.equals(exp, bind.val[1]) 76 | ) 77 | ); 78 | if (b_has_different_valud_dupe) return "NoMatch"; 79 | return a.concat(b); 80 | }; 81 | 82 | const kidsmatch = (pats: Pat[], exps: Exp.t[]): MatchResult => { 83 | if (pats.length !== exps.length) return "NoMatch"; 84 | return zip(pats, exps) 85 | .map(([p, t]) => matches(p, t)) 86 | .reduce(concat_bindings, []); 87 | }; 88 | 89 | /* Try to match Exp with Pat at the root of Exp */ 90 | export const matches = (pat: Pat, exp: Exp.t): MatchResult => { 91 | switch (pat.t) { 92 | case "Atom": 93 | switch (pat.sym.t) { 94 | case "Var": 95 | return [val(pat.id, exp.id, pat.sym.name, exp)]; 96 | case "Const": 97 | switch (exp.t) { 98 | case "Atom": 99 | return pat.sym.name == exp.sym 100 | ? [ids(pat.id, exp.id)] 101 | : "NoMatch"; 102 | case "Comp": 103 | return "NoMatch"; 104 | } 105 | } 106 | break; 107 | case "Comp": { 108 | switch (exp.t) { 109 | case "Atom": 110 | return "NoMatch"; 111 | case "Comp": 112 | let r = kidsmatch(pat.kids, exp.kids); 113 | if (r == "NoMatch") return "NoMatch"; 114 | else return r.concat(ids(pat.id, exp.id)); 115 | } 116 | } 117 | } 118 | }; 119 | 120 | const var_hydrate = (bindings: Binding[], pat_name: string): Exp.t => { 121 | const binding = bindings.find( 122 | (guy) => guy.t == "Val" && guy.val[0] == pat_name 123 | ); 124 | if (binding && binding.t == "Val") return binding.val[1]; 125 | else return Exp.atom(pat_name); //TODO: error instead? 126 | }; 127 | 128 | const ids_hydrate = (bindings: Binding[], pat_id: number): number => { 129 | const binding = bindings.find( 130 | ({ ids: [id, _], t }) => id == pat_id && t == "Ids" 131 | ); 132 | if (binding && binding.t == "Ids") return binding.ids[1]; 133 | else return ID.mk(); //TODO: error instead? 134 | }; 135 | 136 | /* Recursively substitute the provided bindings into the Pat template */ 137 | export const hydrate = (pat: Pat, bindings: Binding[]): Exp.t => { 138 | switch (pat.t) { 139 | case "Atom": { 140 | switch (pat.sym.t) { 141 | case "Var": 142 | return var_hydrate(bindings, pat.sym.name); 143 | case "Const": 144 | return Exp.atom_id(pat.sym.name, ids_hydrate(bindings, pat.id)); 145 | } 146 | } 147 | case "Comp": 148 | return Exp.comp_id( 149 | pat.kids.map((kid) => hydrate(kid, bindings)), 150 | ids_hydrate(bindings, pat.id) 151 | ); 152 | } 153 | }; 154 | 155 | export const transform = ( 156 | exp: Exp.t, 157 | pat: Pat, 158 | template: Pat 159 | ): TransformResult => { 160 | let bindings = matches(pat, exp); 161 | if (bindings === "NoMatch") return "NoMatch"; 162 | let result = hydrate(template, bindings); 163 | return result; 164 | }; 165 | 166 | /* Return first non-NoMatch result */ 167 | let matchresult_map_or = (acc: MatchResult, b: MatchResult): MatchResult => { 168 | if (acc != "NoMatch") return acc; 169 | else return b; 170 | }; 171 | 172 | /* Descend into tree to find exp node of id, and then try to match the pattern */ 173 | export const matches_at_id = ( 174 | exp: Exp.t, 175 | pat: Pat, 176 | id: number 177 | ): MatchResult => { 178 | if (exp.id == id) { 179 | return matches(pat, exp); 180 | } else { 181 | switch (exp.t) { 182 | case "Atom": 183 | return "NoMatch"; 184 | case "Comp": 185 | return exp.kids 186 | .map((kid) => matches_at_id(kid, pat, id)) 187 | .reduce((acc, b) => (acc != "NoMatch" ? acc : b), "NoMatch"); 188 | } 189 | } 190 | }; 191 | 192 | export const matches_at_path = ( 193 | exp: Exp.t, 194 | pat: Pat, 195 | path: Path.t 196 | ): MatchResult => { 197 | if (path.length === 0) { 198 | return matches(pat, exp); 199 | } else { 200 | let [hd, ...tl] = path; 201 | switch (exp.t) { 202 | case "Atom": 203 | return "NoMatch"; 204 | case "Comp": 205 | return matches_at_path(exp.kids[hd], pat, tl); 206 | } 207 | } 208 | }; 209 | 210 | let map_or = ( 211 | acc: Exp.t[] | "NoMatch", 212 | b: TransformResult 213 | ): Exp.t[] | "NoMatch" => { 214 | if (acc === "NoMatch" || b === "NoMatch") return "NoMatch"; 215 | return acc.concat([b]); 216 | }; 217 | 218 | /* descend into tree to find exp node of certain id, and then try to do the transform */ 219 | export const transform_at_id = ( 220 | exp: Exp.t, 221 | pat: Pat, 222 | template: Pat, 223 | id: number 224 | ): TransformResult => { 225 | switch (exp.t) { 226 | case "Atom": 227 | return exp.id == id ? transform(exp, pat, template) : exp; 228 | case "Comp": 229 | if (exp.id == id) { 230 | return transform(exp, pat, template); 231 | } else { 232 | let kids = exp.kids 233 | .map((kid) => transform_at_id(kid, pat, template, id)) 234 | .reduce(map_or, []); 235 | return kids === "NoMatch" ? "NoMatch" : { ...exp, kids }; 236 | } 237 | } 238 | }; 239 | 240 | export const transform_at_path = ( 241 | exp: Exp.t, 242 | pat: Pat, 243 | template: Pat, 244 | path: Path.t 245 | ): TransformResult => { 246 | if (path.length === 0) { 247 | return transform(exp, pat, template); 248 | } else { 249 | let [hd, ...tl] = path; 250 | switch (exp.t) { 251 | case "Atom": 252 | return "NoMatch"; 253 | case "Comp": 254 | //TODO: cleanup... 255 | const res = transform_at_path(exp.kids[hd], pat, template, tl); 256 | if (res === "NoMatch") return "NoMatch"; 257 | let kids = exp.kids.map((kid, index) => { 258 | if (index === hd) { 259 | const res = transform_at_path(kid, pat, template, tl); 260 | if (res === "NoMatch") return kid; 261 | else return res; 262 | } else return kid; 263 | }); 264 | return { ...exp, kids }; 265 | } 266 | } 267 | }; 268 | 269 | export const transform_at_path2 = ( 270 | exp: Exp.t, 271 | pat: Pat, 272 | template: Pat, 273 | path: Path.t 274 | ): TransformResult => { 275 | if (path.length === 0) { 276 | return transform(exp, pat, template); 277 | } else { 278 | let [hd, ...tl] = path; 279 | switch (exp.t) { 280 | case "Atom": 281 | return "NoMatch"; 282 | case "Comp": 283 | //TODO: cleanup... 284 | const res = transform_at_path(exp.kids[hd], pat, template, tl); 285 | if (res === "NoMatch") return "NoMatch"; 286 | let kids = exp.kids.map((kid, index) => { 287 | if (index === hd) { 288 | const res = transform_at_path(kid, pat, template, tl); 289 | if (res === "NoMatch") return kid; 290 | else return res; 291 | } else return kid; 292 | }); 293 | return { ...exp, kids }; 294 | } 295 | } 296 | }; 297 | -------------------------------------------------------------------------------- /src/syntax/Path.tsx: -------------------------------------------------------------------------------- 1 | import * as ID from "./ID"; 2 | 3 | export type t = ID.t[]; 4 | export type Path = t; 5 | 6 | export const empty: t = []; 7 | 8 | export const eq = (p1: t, p2: t): boolean => { 9 | if (p1.length != p2.length) { 10 | return false; 11 | } else { 12 | return p1.every((value, index) => value === p2[index]); 13 | } 14 | }; 15 | 16 | export const doesp1startwithp2 = (p1: t, p2: t): boolean => { 17 | if (p1.length < p2.length) { 18 | return false; 19 | } else { 20 | return p2.every((value, index) => value === p1[index]); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/view/ExpView.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from "solid-js"; 2 | import { For, Show, Index } from "solid-js"; 3 | //import Rand from "rand-seed"; 4 | import * as Pat from "../syntax/Pat"; 5 | import { Exp } from "../syntax/Exp"; 6 | import * as Action from "../Action"; 7 | import * as Path from "../syntax/Path"; 8 | import * as Statics from "../Statics"; 9 | import * as Stage from "../Stage"; 10 | import * as Names from "../Names"; 11 | import * as Settings from "../Settings"; 12 | 13 | type expviewprops = { 14 | node: Exp; 15 | info: Statics.InfoMap; 16 | selection: Stage.selection; 17 | animate: boolean; 18 | is_head: boolean; 19 | inject: Action.Inject; 20 | mask: Pat.Binding[]; 21 | symbols: Settings.symbols; 22 | }; 23 | 24 | const setSelect = (props: expviewprops) => (e: Event) => { 25 | e.preventDefault(); 26 | //above modulates whether shake occurs for some reason? 27 | e.stopPropagation(); 28 | props.inject({ 29 | t: "setSelect", 30 | path: Statics.get(props.info, props.node.id).path, 31 | }); 32 | }; 33 | 34 | const common_clss = ({ node, mask, info, selection }: expviewprops): string => { 35 | const { path, depth } = Statics.get(info, node.id); 36 | const is_selected = 37 | selection == "unselected" ? false : Path.eq(path, selection); 38 | const binding = mask.find( 39 | ({ ids: [_, id_stage], t }) => id_stage == node.id && t == "Val" 40 | ); 41 | const mask_cls = binding?.t == "Val" ? "mask " + binding?.val[0] : ""; 42 | return `node ${is_selected ? "selected" : ""} ${mask_cls} depth-${depth}`; 43 | }; 44 | 45 | const ExpViewGo: Component = (props) => { 46 | const eff = (props: expviewprops): boolean => { 47 | /* search mask for a binding whose first id is this node's id. 48 | then check if it's a val binding. if so return true. else false. */ 49 | const binding = props.mask.find( 50 | ({ ids: [_, id_stage], t }) => id_stage == props.node.id && t == "Val" 51 | ); 52 | // if binding is undefind rerturn false. else return true. 53 | return binding?.t == "Val" ? false : true; 54 | }; 55 | switch (props.node.t) { 56 | case "Atom": 57 | return ( 58 | 67 |
{Names.get(props.symbols, props.node.sym)}
68 |
69 | } 70 | > 71 |
76 | {Names.get(props.symbols, props.node.sym)} 77 |
78 | 79 | ); 80 | case "Comp": 81 | return ( 82 |
88 | { 89 | 90 | {(kid, i) => ( 91 | 101 | )} 102 | 103 | } 104 |
105 | ); 106 | } 107 | }; 108 | 109 | export const ExpView: Component<{ 110 | stage: Stage.t; 111 | inject: Action.Inject; 112 | mask: Pat.Binding[]; 113 | symbols: Settings.symbols; 114 | }> = (props) => 115 | ExpViewGo({ 116 | info: props.stage.info, 117 | selection: props.stage.selection, 118 | node: props.stage.exp, 119 | inject: props.inject, 120 | mask: props.mask, 121 | is_head: false, 122 | animate: true, 123 | symbols: props.symbols, 124 | }); 125 | 126 | export const ViewOnly: Component<{ 127 | node: Exp; 128 | symbols: Settings.symbols; 129 | }> = (props) => 130 | ExpViewGo({ 131 | info: Statics.mk(props.node, []), 132 | selection: "unselected", 133 | node: props.node, 134 | animate: false, 135 | is_head: false, 136 | inject: (_) => {}, 137 | mask: [], 138 | symbols: props.symbols, 139 | }); 140 | -------------------------------------------------------------------------------- /src/view/PreView.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from "solid-js"; 2 | import { For, Show, Switch, Match } from "solid-js"; 3 | import { Transform, at_path, flip } from "../Transform"; 4 | import { subtree_at } from "../syntax/Node"; 5 | import { ViewOnly } from "./ExpView"; 6 | import * as Pat from "../syntax/Pat"; 7 | import * as Exp from "../syntax/Exp"; 8 | import * as Path from "../syntax/Path"; 9 | import * as Stage from "../Stage"; 10 | import * as Action from "../Action"; 11 | import * as Settings from "../Settings"; 12 | import { map_ids } from "../syntax/Node"; 13 | import * as Id from "../syntax/ID"; 14 | 15 | const transformer = 16 | (inject: Action.Inject, transform: Transform, path: Path.t) => 17 | (_e: Event) => { 18 | inject({ 19 | t: "transformNode", 20 | idx: -1, 21 | transform, 22 | f: at_path(transform, path), 23 | }); 24 | }; 25 | 26 | const directed = (transforms: Transform[]): Transform[] => 27 | transforms.flatMap((t) => [t, flip(t)]); 28 | 29 | const do_transforms = ( 30 | exp: Exp.t, 31 | transforms: Transform[] 32 | ): [Transform, Exp.t][] => 33 | directed(transforms) 34 | .map((t) => [t, at_path(t, [])(exp)] as [Transform, Pat.TransformResult]) 35 | .filter((res): res is [Transform, Exp.t] => res[1] !== "NoMatch") 36 | // filter duplicate expressions 37 | .filter( 38 | ([_, exp], i, arr) => 39 | arr.findIndex(([_, exp2]) => Exp.equals(exp, exp2)) === i 40 | ) 41 | // unduplicate ids to avoid messing with transition animations 42 | .map(([t, e]): [Transform, Exp.t] => [t, map_ids(() => Id.mk(), e)]); 43 | 44 | const preview = ( 45 | node: Exp.t, 46 | settings: Settings.t, 47 | transformer: (_e: Event) => void 48 | ) => ( 49 |
53 | {ViewOnly({ node: node, symbols: settings.symbols })} 54 |
55 | ); 56 | 57 | export const AdjacentPossible: Component<{ 58 | stage: Stage.t; 59 | tools: Transform[]; 60 | inject: Action.Inject; 61 | settings: Settings.t; 62 | }> = (props) => { 63 | if (props.stage.selection === "unselected") return
; 64 | const selection = subtree_at(props.stage.selection, props.stage.exp); 65 | if (selection === undefined) return
; 66 | return ( 67 |
68 | 69 | {([transform, node]) => { 70 | if (props.stage.selection === "unselected") return
; 71 | return preview( 72 | node, 73 | props.settings, 74 | transformer(props.inject, transform, props.stage.selection) 75 | ); 76 | }} 77 |
78 |
79 | ); 80 | }; 81 | -------------------------------------------------------------------------------- /src/view/SeedView.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from "solid-js"; 2 | import { StageView } from "../view/StageView"; 3 | import { ToolsView } from "../view/ToolsView"; 4 | import { AdjacentPossible } from "../view/PreView"; 5 | import * as Model from "../Model"; 6 | import * as Action from "../Action"; 7 | import infinity from "../assets/nool-seed-infinity-only.svg" 8 | 9 | const blah = () => ( 10 |
11 | {" "} 12 | 19 |
20 | ); 21 | 22 | export const Seed: Component<{ model: Model.t; inject: Action.Inject }> = ( 23 | props 24 | ) => ( 25 |
{ 34 | e.preventDefault(); 35 | props.inject({ t: "unsetSelections" }); 36 | }} 37 | > 38 | {ToolsView({ model: props.model, inject: props.inject })} 39 | {/*
♾️
*/} 40 |
41 |
42 |
43 | {StageView({ model: props.model, inject: props.inject })} 44 | {props.model.settings.preview 45 | ? AdjacentPossible({ 46 | stage: props.model.stage, 47 | tools: props.model.tools.transforms, 48 | inject: props.inject, 49 | settings: props.model.settings, 50 | }) 51 | : null} 52 |
53 | ); 54 | -------------------------------------------------------------------------------- /src/view/SettingsView.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from "solid-js"; 2 | import { Model } from "../Model"; 3 | import * as Settings from "../Settings"; 4 | import * as Action from "../Action"; 5 | import sound_icon_on from "../assets/icons/sound-on.svg"; 6 | import sound_icon_off from "../assets/icons/sound-off.svg"; 7 | import motion_on from "../assets/icons/motion-on.svg"; 8 | import motion_off from "../assets/icons/motion-off.svg"; 9 | import motion_half from "../assets/icons/motion-half.svg"; 10 | import preview_on from "../assets/icons/eye-open.svg"; 11 | import preview_off from "../assets/icons/eye-closed.svg"; 12 | import linear_prefix from "../assets/icons/noun-tree-1052096.svg"; 13 | import linear_infix from "../assets/icons/noun-tree-1039106.svg"; 14 | import tree_top from "../assets/icons/noun-tree-1052083.svg"; 15 | import tree_left from "../assets/icons/noun-1560867.svg" 16 | import sun_on from "../assets/icons/noun-sun-5362089.svg"; 17 | import sun_off from "../assets/icons/noun-sun-6322390.svg"; 18 | import palette_2 from "../assets/icons/noun-paint-6329583.svg"; 19 | import palette_1 from "../assets/icons/noun-palette-1918496.svg"; 20 | import alphabet from "../assets/icons/noun-alphabet-3591519.svg" 21 | import ded from "../assets/icons/noun-dead-5130035.svg" 22 | import prims from "../assets/icons/noun-geometry-4695832.svg" 23 | 24 | //TODO: qr code to disable id display 25 | 26 | const setSetting = 27 | (inject: Action.Inject, action: Settings.Action) => (e: Event) => { 28 | e.preventDefault(); 29 | e.stopPropagation(); 30 | inject({ 31 | t: "setSetting", 32 | action, 33 | }); 34 | }; 35 | 36 | const sound_icon = (sound: boolean): string => 37 | sound ? sound_icon_on : sound_icon_off; 38 | 39 | const motion_icon = (motion: Settings.motion): string => { 40 | switch (motion) { 41 | case "On": 42 | return motion_on; 43 | case "Off": 44 | return motion_off; 45 | case "Half": 46 | return motion_half; 47 | } 48 | }; 49 | 50 | const preview_icon = (preview: boolean): string => 51 | preview ? preview_on : preview_off; 52 | 53 | const projection_icon = (projection: Settings.projection): string => { 54 | switch (projection) { 55 | case "LinearPrefix": 56 | return linear_prefix; 57 | case "LinearInfix": 58 | return linear_infix; 59 | case "TreeLeft": 60 | return tree_left; 61 | case "TreeTop": 62 | return tree_top; 63 | } 64 | }; 65 | 66 | const symbols_icon = (symbols: Settings.symbols): string => { 67 | switch (symbols) { 68 | case "Emoji": 69 | return prims; 70 | case "SingleChar": 71 | return alphabet; 72 | } 73 | }; 74 | 75 | const theme_icon = (theme: Settings.theme): string => { 76 | switch (theme) { 77 | case "Dark": 78 | return sun_off; 79 | case "Light": 80 | return sun_on; 81 | } 82 | }; 83 | 84 | let action_icon = (action: Settings.Action, settings: Settings.t): string => { 85 | switch (action) { 86 | case "ToggleSound": 87 | return sound_icon(settings.sound); 88 | case "ToggleMotion": 89 | return motion_icon(settings.motion); 90 | case "TogglePreview": 91 | return preview_icon(settings.preview); 92 | case "ToggleProjection": 93 | return projection_icon(settings.projection); 94 | case "ToggleSymbols": 95 | return symbols_icon(settings.symbols); 96 | case "ToggleDark": 97 | return theme_icon(settings.theme); 98 | } 99 | }; 100 | 101 | let icon = ( 102 | inject: Action.Inject, 103 | settings: Settings.t, 104 | action: Settings.Action 105 | ) => ( 106 | 111 | ); 112 | 113 | export const SettingsView: Component<{ 114 | model: Model; 115 | inject: (_: Action.t) => void; 116 | }> = (props) => ( 117 |
118 | {icon(props.inject, props.model.settings, "ToggleSound")} 119 | {icon(props.inject, props.model.settings, "ToggleMotion")} 120 | {icon(props.inject, props.model.settings, "TogglePreview")} 121 | {icon(props.inject, props.model.settings, "ToggleDark")} 122 | {icon(props.inject, props.model.settings, "ToggleProjection")} 123 | {icon(props.inject, props.model.settings, "ToggleSymbols")} 124 | 125 |
126 | ); 127 | -------------------------------------------------------------------------------- /src/view/StageView.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from "solid-js"; 2 | import { For, Show, Switch, Match } from "solid-js"; 3 | import { Model } from "../Model"; 4 | import * as Action from "../Action"; 5 | import { ExpView } from "./ExpView"; 6 | import * as Hover from "../Hover"; 7 | import { depth } from "../syntax/Node"; 8 | 9 | const stage_scale = (d: number) => (d == 0 ? 1 : 4 / (d + 1)); 10 | 11 | export const StageView: Component<{ 12 | model: Model; 13 | inject: (_: Action.t) => void; 14 | }> = (props) => ( 15 |
19 | 22 |
23 | {ExpView({ 24 | stage: props.model.stage, 25 | inject: props.inject, 26 | mask: Hover.get_binding(props.model), 27 | symbols: props.model.settings.symbols, 28 | })} 29 |
30 |
31 | ); 32 | -------------------------------------------------------------------------------- /src/view/ToolsView.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from "solid-js"; 2 | import { For, Index, Switch, Match } from "solid-js"; 3 | import toolbarbkg from "../assets/ps-toolbar.png"; 4 | import * as Pat from "../syntax/Pat"; 5 | import { Model } from "../Model"; 6 | import * as Hover from "../Hover"; 7 | import * as Action from "../Action"; 8 | import { Transform, flip, at_path } from "../Transform"; 9 | import * as ToolBox from "../ToolBox"; 10 | import * as Names from "../Names"; 11 | import * as Settings from "../Settings"; 12 | import * as Sound from "../Sound"; 13 | import { map_ids } from "../syntax/Node"; 14 | import * as Util from "../Util"; 15 | import * as Stage from "../Stage"; 16 | 17 | export const Toolbar: Component<{ model: Model; inject: Action.Inject }> = ( 18 | props 19 | ) => { 20 | return ( 21 |
22 | . 23 |
24 | ); 25 | }; 26 | 27 | const PatView: Component<{ 28 | p: Pat.t; 29 | is_head: boolean; 30 | symbols: Settings.symbols; 31 | }> = (props) => { 32 | switch (props.p.t) { 33 | case "Atom": { 34 | return ( 35 |
41 | {Names.get(props.symbols, props.p.sym.name)} 42 |
43 | ); 44 | } 45 | case "Comp": 46 | return ( 47 |
48 | 49 | {(kid, i) => 50 | PatView({ p: kid, is_head: i() === 0, symbols: props.symbols }) 51 | } 52 | 53 |
54 | ); 55 | } 56 | }; 57 | 58 | const matches_at = (stage: Stage.t, pat: Pat.t): Pat.MatchResult => 59 | stage.selection == "unselected" 60 | ? "NoMatch" 61 | : Pat.matches_at_path(stage.exp, pat, stage.selection); 62 | 63 | const filter_transforms = (stage: Stage.t, ts: Transform[]): Transform[] => 64 | ts.filter( 65 | (t) => 66 | matches_at(stage, t.source) !== "NoMatch" && 67 | matches_at(stage, t.result) !== "NoMatch" 68 | ); 69 | 70 | const source_matches_cls = (props: { model: Model; t: Transform }) => 71 | matches_at(props.model.stage, props.t.source) === "NoMatch" 72 | ? "NoMatch" 73 | : "match"; 74 | 75 | const result_matches_cls = (props: { model: Model; t: Transform }) => 76 | matches_at(props.model.stage, props.t.result) === "NoMatch" 77 | ? "NoMatch" 78 | : "match"; 79 | 80 | const TransformView: Component<{ 81 | idx: number; 82 | t: Transform; 83 | model: Model; 84 | inject: (_: Action.t) => void; 85 | }> = (props) => { 86 | const transformNode = (e: Event) => { 87 | e.preventDefault(); 88 | e.stopPropagation(); 89 | if (props.model.stage.selection != "unselected") { 90 | props.inject({ 91 | t: "transformNodeAndFlipTransform", 92 | target: "Source", 93 | idx: props.idx, 94 | transform: props.t, 95 | f: at_path(props.t, props.model.stage.selection), 96 | }); 97 | } 98 | }; 99 | const transformNodeReverse = (e: Event) => { 100 | e.preventDefault(); 101 | e.stopPropagation(); 102 | if (props.model.stage.selection != "unselected") { 103 | props.inject({ 104 | t: "transformNodeAndFlipTransform", 105 | target: "Result", 106 | idx: props.idx, 107 | transform: props.t, 108 | f: at_path(flip(props.t), props.model.stage.selection), 109 | }); 110 | } 111 | }; 112 | const setHover = (fn: any, target: Hover.t) => (e: Event) => { 113 | if (fn(props) === "match") 114 | props.inject({ 115 | t: "setHover", 116 | target, 117 | }); 118 | }; 119 | const flipTransform = (e: Event) => { 120 | e.preventDefault(); 121 | e.stopPropagation(); 122 | props.inject({ 123 | t: "flipTransform", 124 | idx: props.idx, 125 | }); 126 | }; 127 | const do_nothing = (e: Event) => { 128 | e.preventDefault(); 129 | e.stopPropagation(); 130 | props.inject({ t: "Noop" }); 131 | }; 132 | const selected_res = (tools: ToolBox.t, c: number) => 133 | tools.selector[0] === c && tools.selector[1] === 1 ? "selected" : ""; 134 | const selected_src = (tools: ToolBox.t, c: number) => 135 | tools.selector[0] === c && tools.selector[1] === 0 ? "selected" : ""; 136 | return ( 137 |
142 | {/*
{props.t.name}
*/} 143 |
157 | id + 100000 + 100 * props.idx, props.t.source)} 159 | is_head={false} 160 | symbols={props.model.settings.symbols} 161 | /> 162 |
163 |
164 | 165 | {/* arrows: 166 | ⇋ ⇌ ⇆ ⇄ ⇨ ➥ ➫ ➬ 167 | → ⇋ ⥊ ⥋ ⇋ ⇌ ⇆ ⇄ 168 | ⇐ ⇒ ⟸ ⟹ ⟺ ⟷ ⬄ 169 | ↔ ⬌ ⟵ ⟶ ← → ⬅ ⇦ 170 | */} 171 | 177 | → 178 | 179 | 185 | ← 186 | 187 | 188 |
189 |
203 | id + 200000 + 100 * props.idx, props.t.result)} 205 | is_head={false} 206 | symbols={props.model.settings.symbols} 207 | /> 208 |
209 |
210 | ); 211 | }; 212 | 213 | const select_transforms = (stage:Stage.t,tools: ToolBox.t): [number, Transform][] => { 214 | //const filtered_transforms = filter_transforms(stage, tools.transforms); 215 | const filtered_transforms = tools.transforms; 216 | /* want to take tools.size tools starting at tools.offset (index into tools) 217 | and treat the list as a ring buffer */ 218 | 219 | const len = filtered_transforms.length; 220 | const offset = tools.offset % len; 221 | const size = tools.size; 222 | const idxs = [...Array(size).keys()].map((i) => (i + offset) % len); 223 | return idxs.map((i) => [i, filtered_transforms[i]]); 224 | }; 225 | 226 | function throttle( 227 | func: (...args: any[]) => void, 228 | limit: number 229 | ): (...args: any[]) => void { 230 | let inThrottle: boolean; 231 | return function (this: any, ...args: any[]) { 232 | if (!inThrottle) { 233 | func.apply(this, args); 234 | inThrottle = true; 235 | setTimeout(() => (inThrottle = false), limit); 236 | } 237 | }; 238 | } 239 | 240 | export const ToolsView: Component<{ 241 | model: Model; 242 | inject: (_: Action.t) => void; 243 | }> = (props) => { 244 | return ( 245 |
{ 248 | //console.log("wheel deltay:", e.deltaY); 249 | if (Math.abs(e.deltaY) < 1.5) return; 250 | if (e.shiftKey) { 251 | throttle(() => { 252 | const offset = e.deltaY == 0 ? 0 : e.deltaY / Math.abs(e.deltaY); 253 | //console.log("SHIFT GYOOOO", offset); 254 | props.inject({ 255 | t: "wheelNumTools", 256 | offset, 257 | }); 258 | }, 1000)(); 259 | } else { 260 | throttle(() => { 261 | const offset = e.deltaY == 0 ? 0 : e.deltaY / Math.abs(e.deltaY); 262 | //console.log("GYOOOO", offset); 263 | props.inject({ 264 | t: "wheelTools", 265 | offset: offset, 266 | }); 267 | }, 1000)(); 268 | } 269 | }} 270 | > 271 | 272 | {([idx, t]) => 273 | TransformView({ 274 | idx, 275 | t, 276 | model: props.model, 277 | inject: props.inject, 278 | }) 279 | } 280 | 281 |
282 | ); 283 | }; 284 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "node", 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "jsx": "preserve", 10 | "jsxImportSource": "solid-js", 11 | "types": ["vite/client", "jest", "dom-view-transitions"], 12 | "noEmit": true, 13 | "isolatedModules": true, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import solidPlugin from 'vite-plugin-solid'; 3 | 4 | export default defineConfig({ 5 | base: "./", 6 | plugins: [solidPlugin()], 7 | server: { 8 | port: 3000, 9 | }, 10 | build: { 11 | target: 'esnext', 12 | }, 13 | assetsInclude: ['**/*.m4a', '**/*.wav'], 14 | }); 15 | --------------------------------------------------------------------------------