├── .github ├── FUNDING.yml └── blocks │ ├── all.json │ └── file │ └── vezwork__blocks-polytope__file-block │ └── LICENSE.json ├── LICENSE ├── README.md ├── archive ├── OLD_OLD_index_webcomponent.html ├── OLD_index_contenteditable.html ├── OLD_index_graph_extra.html ├── OLD_index_webcomponent.html ├── ed_ex3.js ├── ed_ex3.ts ├── ed_ex_group_theory.js ├── ed_ex_group_theory_2.js ├── ed_ex_group_theory_3.js ├── groupTheoryEditor.js ├── junk.txt └── string_to_editor.js ├── assets ├── icon_cgraph.png ├── icon_graph.png ├── icon_math.png └── radical.svg ├── custom.js ├── examples ├── ed_ex.js ├── ed_ex2.js ├── g.js ├── groupTheoryPlayground.js ├── math_ops.js ├── music.js └── new.js ├── groupTheory.js ├── homepage ├── .DS_Store ├── index.html ├── update1.html ├── update1_image1.png ├── update1_image2.png ├── update1_image3.png ├── update1_video1.mp4 ├── update2.html ├── update2_image1.png ├── update2_image2.png └── update2_image3.png ├── index.html ├── lib ├── .gitignore ├── README.md ├── dist │ ├── Iterable.js │ ├── editor.js │ ├── editors │ │ ├── ArrayEditorElement.js │ │ ├── CharArrayEditorElement.js │ │ ├── ColoredGraphEditorElement.js │ │ ├── DropdownElement.js │ │ ├── ForceColoredGraphEditorElement.js │ │ ├── ForceGraphEditorElement.js │ │ ├── MakeGraphEditorElement.js │ │ ├── MusicStaffEditorElement.js │ │ ├── StringEditorElement.js │ │ ├── TextEditorElement.js │ │ ├── bidirectional_editor_pair.js │ │ ├── markdownEditors.js │ │ └── mathEditors.js │ ├── index.js │ ├── math.js │ └── stringToEditorBuilder.js ├── package-lock.json ├── package.json ├── src │ ├── Iterable.ts │ ├── editor.ts │ ├── editors │ │ ├── ArrayEditorElement.ts │ │ ├── CharArrayEditorElement.ts │ │ ├── ColoredGraphEditorElement.ts │ │ ├── DropdownElement.ts │ │ ├── ForceColoredGraphEditorElement.ts │ │ ├── ForceGraphEditorElement.ts │ │ ├── MakeGraphEditorElement.ts │ │ ├── MusicStaffEditorElement.ts │ │ ├── StringEditorElement.ts │ │ ├── TextEditorElement.ts │ │ ├── bidirectional_editor_pair.ts │ │ ├── markdownEditors.ts │ │ └── mathEditors.ts │ ├── index.ts │ ├── math.ts │ └── stringToEditorBuilder.ts ├── tsconfig.json └── webpack.config.js ├── logo.svg ├── notes.html ├── pres ├── figma.svg ├── figma2.svg ├── index.html ├── me.jpeg └── twitterwhite.png └── testing.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: vezwork 2 | -------------------------------------------------------------------------------- /.github/blocks/all.json: -------------------------------------------------------------------------------- 1 | { 2 | "examples/ed_ex.js": { 3 | "default": "vezwork__blocks-polytope__polytope-block" 4 | }, 5 | "examples/music.js": { 6 | "default": "vezwork__blocks-polytope__polytope-block" 7 | } 8 | } -------------------------------------------------------------------------------- /.github/blocks/file/vezwork__blocks-polytope__file-block/LICENSE.json: -------------------------------------------------------------------------------- 1 | { 2 | "number": 2 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Elliot Evans 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Experiment 1 3 | 4 | ## About 5 | 6 | Polytope is an experimental code editor that allows a user to write programs by embedding non-text editors (e.g. music notation editor, markdown editor, svg drawing tool, decision tree etc.) inline beside text in a text editor. The Polytope library provides utilities for constructing editors that work together and can be embedded in eachother. Polytope runs in the browser. 7 | 8 | This repo contains **☣️research quality code☣️** (bad quality code) for the first/current version of Polytope. This version of Polytope may not be updated much in the future unless it receives community contributions. The idea of Polytope is not dead though, further experiments continue on the 9 | ideas underlying Polytope. I am no longer actively working on this repo, and my goal with Polytope is to enable people to write code in new ways, so I am open sourcing this repo in hopes it will help someone despite the poor code and documentation quality. Please [reach out](#community) if you have any questions. 10 | 11 | For more info: 12 | 13 | - [Polytope project page](https://elliot.website/editor/) 14 | - Polytope Demo on Youtube: [Polytope Demo Youtube Link](https://www.youtube.com/embed/8g_XCZSY7lM) 15 | 16 | ## Try it out 17 | 18 | Note that this is an experiment and has many bugs and issues. 19 | 20 | [Try it out](https://vezwork.github.io/Polytope/) 21 | 22 | ## Running locally and contributing 23 | 24 | The Polytope library is located in the lib folder, and is written in TypeScript. 25 | See lib/README.md for simple instructions on how to build the lib. 26 | 27 | index.html and custom.js contain the code that run an example usage of the Polytope library. They are probably 28 | the best place to start. You can simply clone this repo and open index.html in your browser to try it out. 29 | 30 | ## Community 31 | 32 | If you are interested in Polytope, have any questions, or just want to say hi: 33 | 34 | - [Twitter](https://twitter.com/elliotokay) 35 | - [Polytope Discord](https://discord.gg/8zjCC6Vp) 36 | -------------------------------------------------------------------------------- /archive/OLD_index_contenteditable.html: -------------------------------------------------------------------------------- 1 | 8 | 34 | 35 |
36 | a 37 | 38 | 39 |

hello

40 | b 41 |
42 | 43 | 44 | 114 | 115 | 121 | -------------------------------------------------------------------------------- /archive/OLD_index_graph_extra.html: -------------------------------------------------------------------------------- 1 |
1
2 |
2
3 |
3
4 | 5 | 81 | 82 | 91 | -------------------------------------------------------------------------------- /archive/ed_ex3.js: -------------------------------------------------------------------------------- 1 | (`POLYTOPE`/* 2 | const { TextEditorElement } = await import('./editor.js'); 3 | 4 | const code = await (await fetch('./ed_ex2.js')).text(); 5 | return new TextEditorElement({ code: code.split("") }); 6 | */) 7 | -------------------------------------------------------------------------------- /archive/ed_ex3.ts: -------------------------------------------------------------------------------- 1 | class Superposition { 2 | static from(x: any): any { } 3 | } 4 | 5 | function superposition(x: any): any { } 6 | function outerProduct(x: any, y: any): any { } 7 | function identity(x: any): any { } 8 | function measure(x: any): any { } 9 | function reflectionAboutSuperposition(x: any): any { } 10 | function makeQuantumOracle(x: any): any { } 11 | const π = Math.PI; 12 | 13 | 14 | function groverSearch(arr: Array, predicate: (arrEl: A) => Boolean): A { 15 | let allArrIndices = Array.from(arr.keys()); 16 | 17 | const init = Superposition.from(allArrIndices); 18 | 19 | const diffuse: (s: Superposition) => Superposition 20 | = reflectionAboutSuperposition(init); 21 | 22 | let indexSuperposition = init; 23 | const iterCount = Math.ceil(π * Math.sqrt(arr.length) / 4); 24 | for (let i = 0; i < iterCount; i++) { 25 | for (const index of indexSuperposition) { 26 | if (predicate(arr[index])) { 27 | indexSuperposition[index] *= -1; 28 | } 29 | } 30 | indexSuperposition = diffuse(indexSuperposition); 31 | } 32 | 33 | return measure(indexSuperposition); 34 | } 35 | 36 | // Returns 4 with high probability! 37 | groverSearch([0, 1, 2, 3, 4], (a) => a > 3); 38 | -------------------------------------------------------------------------------- /archive/ed_ex_group_theory.js: -------------------------------------------------------------------------------- 1 | const e = 'e'; 2 | const x = 'x'; 3 | const c = 'c'; 4 | const d = 'd'; 5 | const a = 'a'; 6 | const b = 'b'; 7 | const d2 = 'd2'; 8 | const b2 = 'b2'; 9 | const a2 = 'a2'; 10 | const c2 = 'c2'; 11 | const z = 'z'; 12 | const y = 'y'; 13 | 14 | const red = { 15 | nodes: [e, a, a2, x, b, c2, c, d2, z, d, b2, y], 16 | edges: [[1], [2], [0], [4], [5], [3], [7], [8], [6], [10], [11], [9]], 17 | positions: [[15, 26], [36, 114], [6, 192], [69, 28], [92, 109], [70, 195], [129, 22], [146, 112], [134, 196], [193, 23], [209, 109], [190, 203]] 18 | }; 19 | 20 | const blue = { 21 | nodes: [e, a, a2, x, b, c2, c, d2, z, d, b2, y], 22 | edges: [[3], [6], [10], [0], [9], [7], [1], [5], [11], [4], [2], [8]], 23 | positions: [[14, 30], [19, 113], [14, 197], [64, 29], [75, 114], [74, 198], [122, 31], [128, 116], [139, 198], [194, 26], [187, 116], [194, 195]] 24 | }; 25 | 26 | const q = []; 27 | const explored = new Set(); 28 | const redParents = new Map(); 29 | const blueParents = new Map(); 30 | const paths = new Map(); 31 | paths.set(e, []); 32 | 33 | explored.add(e); 34 | q.unshift(e); 35 | 36 | while (q.length > 0) { 37 | const v = q.pop(); 38 | // this relies on the fact that red.nodes and blue.nodes are the same 39 | const i = red.nodes.findIndex(va => va === v); 40 | for (const redEdge of red.edges[i]) { 41 | const w = red.nodes[redEdge]; 42 | if (!explored.has(w)) { 43 | paths.set(w, [...paths.get(v), 'a']); 44 | redParents.set(w, v); 45 | explored.add(w); 46 | q.unshift(w); 47 | } 48 | } 49 | for (const blueEdge of blue.edges[i]) { 50 | const w = blue.nodes[blueEdge]; 51 | if (!explored.has(w)) { 52 | paths.set(w, [...paths.get(v), 'x']); 53 | blueParents.set(w, v); 54 | explored.add(w); 55 | q.unshift(w); 56 | } 57 | } 58 | } 59 | 60 | // this relies on the fact that red.nodes and blue.nodes are the same 61 | const multiplicationTable = {}; 62 | for (let i = 0; i < red.nodes.length; i++) { 63 | const from = red.nodes[i]; 64 | for (let j = 0; j < red.nodes.length; j++) { 65 | const to = red.nodes[j]; 66 | let tracei = i; 67 | for (const arrow of paths.get(to)) { 68 | if (arrow === 'a') { 69 | tracei = red.edges[tracei][0]; 70 | } 71 | if (arrow === 'x') { 72 | tracei = blue.edges[tracei][0] 73 | } 74 | } 75 | if (!multiplicationTable[from]) multiplicationTable[from] = {}; 76 | multiplicationTable[from][to] = red.nodes[tracei]; 77 | } 78 | } 79 | 80 | console.log(paths); 81 | console.table(multiplicationTable); 82 | -------------------------------------------------------------------------------- /archive/ed_ex_group_theory_2.js: -------------------------------------------------------------------------------- 1 | // this solves exercise 4.9 a) in Visual Group Theory by Nathan Carter 2 | const e = 'e'; 3 | const r = 'r'; 4 | const r2 = 'r2'; 5 | const r3 = 'r3'; 6 | const r4 = 'r4'; 7 | const r5 = 'r5'; 8 | const h = 'h'; 9 | const hr = 'hr'; 10 | const hr2 = 'hr2'; 11 | const hr3 = 'hr3'; 12 | const hr4 = 'hr4'; 13 | const hr5 = 'hr5'; 14 | 15 | const red = { 16 | nodes: [e,r,r2,h,hr,hr2], 17 | edges: [[1],[2],[0],[4],[5],[3]], 18 | positions: [[43, 60],[80, 139],[12, 168],[156, 62],[134, 142],[193, 171]] 19 | } 20 | 21 | const blue = { 22 | nodes: [e,r,r2,h,hr,hr2], 23 | edges: [[3],[4],[5],[0],[1],[2]], 24 | positions: [[44, 69],[84, 152],[2, 189],[171, 68],[134, 153],[193, 191]] 25 | } 26 | 27 | const q = []; 28 | const explored = new Set(); 29 | const redParents = new Map(); 30 | const blueParents = new Map(); 31 | const paths = new Map(); 32 | paths.set(e, []); 33 | 34 | explored.add(e); 35 | q.unshift(e); 36 | 37 | while (q.length > 0) { 38 | const v = q.pop(); 39 | // this relies on the fact that red.nodes and blue.nodes are the same 40 | const i = red.nodes.findIndex(va => va === v); 41 | for (const redEdge of red.edges[i]) { 42 | const w = red.nodes[redEdge]; 43 | if (!explored.has(w)) { 44 | paths.set(w, [...paths.get(v), 'r']); 45 | redParents.set(w, v); 46 | explored.add(w); 47 | q.unshift(w); 48 | } 49 | } 50 | for (const blueEdge of blue.edges[i]) { 51 | const w = blue.nodes[blueEdge]; 52 | if (!explored.has(w)) { 53 | paths.set(w, [...paths.get(v), 'h']); 54 | blueParents.set(w, v); 55 | explored.add(w); 56 | q.unshift(w); 57 | } 58 | } 59 | } 60 | 61 | // this relies on the fact that red.nodes and blue.nodes are the same 62 | const multiplicationTable = {}; 63 | for (let i = 0; i < red.nodes.length; i++) { 64 | const from = red.nodes[i]; 65 | for (let j = 0; j < red.nodes.length; j++) { 66 | const to = red.nodes[j]; 67 | let tracei = i; 68 | for (const arrow of paths.get(to)) { 69 | if (arrow === 'r') { 70 | tracei = red.edges[tracei][0]; 71 | } 72 | if (arrow === 'h') { 73 | tracei = blue.edges[tracei][0] 74 | } 75 | } 76 | if (!multiplicationTable[from]) multiplicationTable[from] = {}; 77 | multiplicationTable[from][to] = red.nodes[tracei]; 78 | } 79 | } 80 | 81 | console.log(paths); 82 | console.table(multiplicationTable); -------------------------------------------------------------------------------- /archive/ed_ex_group_theory_3.js: -------------------------------------------------------------------------------- 1 | // this solves exercise 4.9 a. in Visual Group Theory by Nathan Carter 2 | const e = 'e'; 3 | const r = 'r'; 4 | const r2 = 'r2'; 5 | const r3 = 'r3'; 6 | const r4 = 'r4'; 7 | const r5 = 'r5'; 8 | const h = 'h'; 9 | const hr = 'hr'; 10 | const hr2 = 'hr2'; 11 | const hr3 = 'hr3'; 12 | const hr4 = 'hr4'; 13 | const hr5 = 'hr5'; 14 | 15 | const red = { 16 | nodes: [e,r,r2,r3,r4,h,hr,hr2,hr3,hr4], 17 | edges: [[1],[2],[3],[4],[0],[6],[7],[8],[9],[5]], 18 | positions: [[97, 12],[207, 91],[156, 213],[36, 216],[2, 111],[98, 66],[56, 110],[66, 155],[130, 156],[138, 106]] 19 | }; 20 | 21 | const blue = { 22 | nodes: [e,r,r2,r3,r4,h,hr,hr2,hr3,hr4], 23 | edges: [[5],[9],[8],[7],[6],[0],[4],[3],[2],[1]], 24 | positions: [[100, 10],[198, 106],[156, 219],[58, 216],[4, 116],[104, 61],[60, 115],[68, 160],[129, 159],[129, 112]] 25 | }; 26 | 27 | const q = []; 28 | const explored = new Set(); 29 | const redParents = new Map(); 30 | const blueParents = new Map(); 31 | const paths = new Map(); 32 | paths.set(e, []); 33 | 34 | explored.add(e); 35 | q.unshift(e); 36 | 37 | while (q.length > 0) { 38 | const v = q.pop(); 39 | // this relies on the fact that red.nodes and blue.nodes are the same 40 | const i = red.nodes.findIndex(va => va === v); 41 | for (const redEdge of red.edges[i]) { 42 | const w = red.nodes[redEdge]; 43 | if (!explored.has(w)) { 44 | paths.set(w, [...paths.get(v), 'r']); 45 | redParents.set(w, v); 46 | explored.add(w); 47 | q.unshift(w); 48 | } 49 | } 50 | for (const blueEdge of blue.edges[i]) { 51 | const w = blue.nodes[blueEdge]; 52 | if (!explored.has(w)) { 53 | paths.set(w, [...paths.get(v), 'h']); 54 | blueParents.set(w, v); 55 | explored.add(w); 56 | q.unshift(w); 57 | } 58 | } 59 | } 60 | 61 | // this relies on the fact that red.nodes and blue.nodes are the same 62 | const multiplicationTable = {}; 63 | for (let i = 0; i < red.nodes.length; i++) { 64 | const from = red.nodes[i]; 65 | for (let j = 0; j < red.nodes.length; j++) { 66 | const to = red.nodes[j]; 67 | let tracei = i; 68 | for (const arrow of paths.get(to)) { 69 | if (arrow === 'r') { 70 | tracei = red.edges[tracei][0]; 71 | } 72 | if (arrow === 'h') { 73 | tracei = blue.edges[tracei][0] 74 | } 75 | } 76 | if (!multiplicationTable[from]) multiplicationTable[from] = {}; 77 | multiplicationTable[from][to] = red.nodes[tracei]; 78 | } 79 | } 80 | 81 | console.log(paths); 82 | console.table(multiplicationTable); 83 | 84 | // this solves exercise 4.9 b. in Visual Group Theory by Nathan Carter 85 | // The pattern is that the multiplication table is divided into four 86 | // quadrants. The top left and bottom right quadrants are the same 87 | // and so are the the top right and bottom left. The top left/ 88 | // bottom right quadrants follow a diagonal pattern e r r2 ... rn. 89 | // The top right/bottom left quadrants are the same but are 90 | // prepended by h. 91 | // EDIT: that was wrong, I realized after looking at https://nathancarter.github.io/group-explorer/GroupExplorer.html 92 | // The actual pattern is that there are four quadrants, the 93 | // top-right quadrant is the same as top-left but the rows are 94 | // shifted in the opposite horizontal direction with h prepended. 95 | // the bottom-left is a copy of the top-left but with h prepended. 96 | // the bottom-right is a copy of the top-right but with h removed. 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /archive/groupTheoryEditor.js: -------------------------------------------------------------------------------- 1 | import { TextEditorElement } from "http://localhost:8080/lib/editor.js"; 2 | import { ConstructBinarySymbolJoinerElement, ConstructExpJoinerElement } from "http://localhost:8080/mathEditors.js" 3 | 4 | export const GroupAlgebraEditor = (name) => { 5 | class C extends TextEditorElement { 6 | constructor() { 7 | super(...arguments); 8 | 9 | this.style.setProperty('--editor-name', `'group algebra'`); 10 | this.style.setProperty('--editor-color', '#FFD600'); 11 | this.style.setProperty('--editor-name-color', 'black'); 12 | this.style.setProperty('--editor-background-color', '#fff7cf'); 13 | this.style.setProperty('--editor-outline-color', '#fff1a8'); 14 | } 15 | 16 | keyHandler(e) { 17 | if (e.key === '*') { 18 | // elevate left and right 19 | const pre = this.code.slice(0, this.caret); 20 | const post = this.code.slice(this.caret, this.code.length); 21 | const focuser = new MulJoinerElement({ parentEditor: this, leftCode: pre, rightCode: post }); 22 | this.code = [focuser]; 23 | return focuser; 24 | } else if (e.key === '^') { 25 | // elevate left and right (non commutative) 26 | const pre = this.code.slice(0, this.caret); 27 | const post = this.code.slice(this.caret, this.code.length); 28 | const focuser = new ExpJoinerElement({ parentEditor: this, leftCode: pre, rightCode: post }); 29 | this.code = [focuser]; 30 | return focuser; 31 | } 32 | } 33 | } 34 | customElements.define(`${name}-group-algebra-editor`, C); 35 | 36 | const ExpJoinerElement = ConstructExpJoinerElement(`${name}.exponentiate`, C, C); 37 | const MulJoinerElement = ConstructBinarySymbolJoinerElement(`${name}.action`, C, '·', C); 38 | 39 | return C; 40 | }; 41 | 42 | export const GroupZ3AlgebraEditor = GroupAlgebraEditor('z3'); 43 | export const GroupA4AlgebraEditor = GroupAlgebraEditor('a4'); 44 | -------------------------------------------------------------------------------- /archive/junk.txt: -------------------------------------------------------------------------------- 1 | const getBracePairs = (string) => { 2 | const openBracesToCloseBraces = {}; 3 | const closeBracesToOpenBraces = {}; 4 | let openBraceIndexStack = []; 5 | for (let i = 0; i < string.length; i++) { 6 | const char = string[i]; 7 | 8 | if (char === '(') { 9 | openBraceIndexStack.push(i); 10 | } 11 | if (char === ')'){ 12 | const openBranceIndex = openBraceIndexStack.pop(); 13 | if (openBranceIndex) { 14 | result[openBranceIndex] = i; 15 | result[i] = openBranceIndex; 16 | } 17 | } 18 | } 19 | return { 20 | openBracesToCloseBraces, 21 | closeBracesToOpenBraces 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /archive/string_to_editor.js: -------------------------------------------------------------------------------- 1 | import { GraphEditorElement, ColoredGraphEditorElement, ForceGraphEditorElement } from "./lib/editor.js"; 2 | import { PlusJoinerElement, DivJoinerElement, ExpJoinerElement, RadicalJoinerElement, MulJoinerElement, SubJoinerElement } from "./sub_math_editors.js"; 3 | 4 | const mathOperations = [{ 5 | prefix: 'plus', 6 | arity: 2, 7 | ElementConstructor: PlusJoinerElement 8 | }, { 9 | prefix: 'mul', 10 | arity: 2, 11 | ElementConstructor: MulJoinerElement 12 | }, { 13 | prefix: 'sub', 14 | arity: 2, 15 | ElementConstructor: SubJoinerElement 16 | }, { 17 | prefix: 'div', 18 | arity: 2, 19 | ElementConstructor: DivJoinerElement 20 | }, { 21 | prefix: 'exp', 22 | arity: 2, 23 | ElementConstructor: ExpJoinerElement 24 | }, { 25 | prefix: 'sqrt', 26 | arity: 2, 27 | ElementConstructor: RadicalJoinerElement 28 | }]; 29 | 30 | const processMath = (mathOperations, string, i, innerString, result) => { 31 | 32 | let isProcessed = false; 33 | 34 | for (const { prefix, arity, ElementConstructor } of mathOperations) { 35 | if (string.substring(i - prefix.length, i) === prefix) { 36 | // Only supports unary and binary ops for now. 37 | if (arity === 1) { 38 | const joiner = new ElementConstructor({ 39 | code: innerString 40 | }); 41 | isProcessed = true; 42 | result.length -= prefix.length; 43 | concatInPlace(result, [joiner]); 44 | 45 | } else if (arity === 2) { 46 | const commaIndex = innerString.indexOf(','); 47 | if (commaIndex !== -1) { 48 | const joiner = new ElementConstructor({ 49 | leftCode: innerString.slice(0, commaIndex), 50 | rightCode: innerString.slice(commaIndex + 2) 51 | }); 52 | isProcessed = true; 53 | result.length -= prefix.length; 54 | concatInPlace(result, [joiner]); 55 | } 56 | } 57 | } 58 | } 59 | return isProcessed; 60 | } 61 | 62 | const EDITOR_IDENTIFIER = 'POLYTOPE$$STRING'; 63 | const EDITOR_IDENTIFIER_STRING = `"${EDITOR_IDENTIFIER}"`; 64 | export const processGraph = (innerString, result) => { 65 | try { 66 | // Replace inner editors with a simple value so that JSON parse can parse 67 | // the innerString (hack). 68 | let massagedInnerString = ''; 69 | let innerEditors = []; 70 | for (let i = 0; i < innerString.length; i++) { 71 | const slotOrChar = innerString[i]; 72 | if (typeof slotOrChar !== 'string') { 73 | innerEditors.unshift(slotOrChar); 74 | massagedInnerString += EDITOR_IDENTIFIER_STRING; 75 | } else { 76 | const char = slotOrChar; 77 | massagedInnerString += char; 78 | } 79 | } 80 | const innerJSON = JSON.parse(massagedInnerString); 81 | 82 | const isProbablyValid = 83 | Array.isArray(innerJSON.nodes) && 84 | Array.isArray(innerJSON.edges) && 85 | Array.isArray(innerJSON.positions); 86 | if (isProbablyValid) { 87 | innerJSON.nodes = innerJSON.nodes.map(node => (node === EDITOR_IDENTIFIER) ? innerEditors.pop() : node); 88 | innerJSON.edges = innerJSON.edges.map(edge => (edge === EDITOR_IDENTIFIER) ? innerEditors.pop() : edge); 89 | innerJSON.positions = innerJSON.positions.map(pos => (pos === EDITOR_IDENTIFIER) ? innerEditors.pop() : pos); 90 | 91 | const graphEditorElement = new GraphEditorElement(innerJSON) 92 | concatInPlace(result, [graphEditorElement]); 93 | return true; 94 | } 95 | } catch (e) { 96 | //console.error('processGraph error', e); 97 | } 98 | 99 | return false; 100 | }; 101 | 102 | export const processColoredGraph = (innerString, result) => { 103 | try { 104 | // Replace inner editors with a simple value so that JSON parse can parse 105 | // the innerString (hack). 106 | let massagedInnerString = ''; 107 | let innerEditors = []; 108 | for (let i = 0; i < innerString.length; i++) { 109 | const slotOrChar = innerString[i]; 110 | if (typeof slotOrChar !== 'string') { 111 | innerEditors.unshift(slotOrChar); 112 | massagedInnerString += EDITOR_IDENTIFIER_STRING; 113 | } else { 114 | const char = slotOrChar; 115 | massagedInnerString += char; 116 | } 117 | } 118 | const innerJSON = JSON.parse(massagedInnerString); 119 | 120 | const isProbablyValid = innerJSON.isColoredGraph; 121 | if (isProbablyValid) { 122 | innerJSON.nodes = innerJSON.nodes.map(node => (node === EDITOR_IDENTIFIER) ? innerEditors.pop() : node); 123 | innerJSON.edges[0] = innerJSON.edges[0].map(edge => (edge === EDITOR_IDENTIFIER) ? innerEditors.pop() : edge); 124 | innerJSON.edges[1] = innerJSON.edges[1].map(edge => (edge === EDITOR_IDENTIFIER) ? innerEditors.pop() : edge); 125 | innerJSON.edges[2] = innerJSON.edges[2].map(edge => (edge === EDITOR_IDENTIFIER) ? innerEditors.pop() : edge); 126 | innerJSON.edges[3] = innerJSON.edges[3].map(edge => (edge === EDITOR_IDENTIFIER) ? innerEditors.pop() : edge); 127 | innerJSON.positions = innerJSON.positions.map(pos => (pos === EDITOR_IDENTIFIER) ? innerEditors.pop() : pos); 128 | 129 | const graphEditorElement = new ColoredGraphEditorElement(innerJSON) 130 | concatInPlace(result, [graphEditorElement]); 131 | return true; 132 | } 133 | } catch (e) { 134 | //console.error('processColoredGraph error', e); 135 | } 136 | 137 | return false; 138 | }; 139 | 140 | export const processForceGraph = (innerString, result) => { 141 | try { 142 | // Replace inner editors with a simple value so that JSON parse can parse 143 | // the innerString (hack). 144 | let massagedInnerString = ''; 145 | let innerEditors = []; 146 | for (let i = 0; i < innerString.length; i++) { 147 | const slotOrChar = innerString[i]; 148 | if (typeof slotOrChar !== 'string') { 149 | innerEditors.unshift(slotOrChar); 150 | massagedInnerString += EDITOR_IDENTIFIER_STRING; 151 | } else { 152 | const char = slotOrChar; 153 | massagedInnerString += char; 154 | } 155 | } 156 | const innerJSON = JSON.parse(massagedInnerString); 157 | 158 | const isProbablyValid = 159 | Array.isArray(innerJSON.nodes) && 160 | Array.isArray(innerJSON.edges); 161 | 162 | if (isProbablyValid) { 163 | innerJSON.nodes = innerJSON.nodes.map(node => (node === EDITOR_IDENTIFIER) ? innerEditors.pop() : node); 164 | innerJSON.edges = innerJSON.edges.map(edge => (edge === EDITOR_IDENTIFIER) ? innerEditors.pop() : edge); 165 | 166 | const graphEditorElement = new ForceGraphEditorElement(innerJSON) 167 | concatInPlace(result, [graphEditorElement]); 168 | return true; 169 | } 170 | } catch (e) { 171 | //console.error('processForceGraph error', e); 172 | } 173 | 174 | return false; 175 | }; 176 | 177 | export const stringToEditor = (string, hasOpenParen = false) => { 178 | let result = []; 179 | 180 | for (let i = 0; i < string.length; i++) { 181 | const char = string[i]; 182 | 183 | if (char === '(') { 184 | const { consume, output } = stringToEditor(string.substring(i + 1), true); 185 | 186 | let isProcessed = false; 187 | isProcessed |= processMath(mathOperations, string, i, output, result); 188 | if (!isProcessed) isProcessed |= processColoredGraph(output, result); 189 | if (!isProcessed) isProcessed |= processGraph(output, result); 190 | if (!isProcessed) isProcessed |= processForceGraph(output, result); 191 | 192 | i = i + consume + 1; 193 | 194 | if (!isProcessed) { 195 | result = [...result, '(', ...output, ')']; 196 | } 197 | 198 | } else if (hasOpenParen && char === ')') { 199 | return { 200 | consume: i, 201 | output: result 202 | } 203 | } else { 204 | result = [...result, char]; 205 | } 206 | } 207 | return { 208 | consume: string.length, 209 | output: result 210 | }; 211 | } 212 | 213 | function concatInPlace(array, otherArray) { 214 | for (const entry of otherArray) array.push(entry); 215 | } 216 | -------------------------------------------------------------------------------- /assets/icon_cgraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vezwork/Polytope/6f4e7ccb66d68dca0b359014d8931a16b6144244/assets/icon_cgraph.png -------------------------------------------------------------------------------- /assets/icon_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vezwork/Polytope/6f4e7ccb66d68dca0b359014d8931a16b6144244/assets/icon_graph.png -------------------------------------------------------------------------------- /assets/icon_math.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vezwork/Polytope/6f4e7ccb66d68dca0b359014d8931a16b6144244/assets/icon_math.png -------------------------------------------------------------------------------- /assets/radical.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /custom.js: -------------------------------------------------------------------------------- 1 | import { 2 | BidirectionalEditorPair, 3 | ConstructiveUnidirectionalEditor, 4 | UnidirectionalEditorPair, 5 | } from "./lib/dist/editors/bidirectional_editor_pair.js"; 6 | export { EditorElement } from "./lib/dist/editor.js"; 7 | import { MakeGraphEditorElement } from "./lib/dist/editors/MakeGraphEditorElement.js"; 8 | import { ForceColoredGraphEditorElement } from "./lib/dist/editors/ForceColoredGraphEditorElement.js"; 9 | import { ForceGraphEditorElement } from "./lib/dist/editors/ForceGraphEditorElement.js"; 10 | import { ColoredGraphEditorElement } from "./lib/dist/editors/ColoredGraphEditorElement.js"; 11 | import { DropdownElement } from "./lib/dist/editors/DropdownElement.js"; 12 | import { TextEditorElement } from "./lib/dist/editors/TextEditorElement.js"; 13 | import { MusicStaffEditorElement } from "./lib/dist/editors/MusicStaffEditorElement.js"; 14 | import { CharArrayEditorElement } from "./lib/dist/editors/CharArrayEditorElement.js"; 15 | import { 16 | MathEditorElement, 17 | PlusJoinerElement, 18 | DivJoinerElement, 19 | ExpJoinerElement, 20 | RadicalJoinerElement, 21 | MulJoinerElement, 22 | SubJoinerElement, 23 | MatrixJoinerElement, 24 | } from "./lib/dist/editors/mathEditors.js"; 25 | import { 26 | createJSONProcessor, 27 | createBuilder, 28 | createFunctionProcessor, 29 | } from "./lib/dist/stringToEditorBuilder.js"; 30 | import { 31 | generatorPaths, 32 | multiplicationTable, 33 | cycleGraphFromMulTable, 34 | } from "./groupTheory.js"; 35 | import { MarkdownEditorElement } from "./lib/dist/editors/markdownEditors.js"; 36 | 37 | const ED = ConstructiveUnidirectionalEditor({ 38 | name: "eval-testing", 39 | leftEditorFactory: (code, parentEditor) => 40 | new MyEditorElement({ 41 | code, 42 | parentEditor, 43 | }), 44 | leftEditorOutput: (editor) => editor.getOutput(), 45 | }); 46 | const GraphToForceGraph = UnidirectionalEditorPair({ 47 | name: "graph-force-graph", 48 | leftEditorFactory: (string, parentEditor) => { 49 | return new ColoredGraphEditorElement({ 50 | parentEditor, 51 | }); 52 | }, 53 | leftEditorOutput: (editor) => { 54 | const { nodes, edges } = editor.getJSONOutput(); 55 | return { 56 | nodes: nodes.map((node) => node.getOutput().split('"').join("")), 57 | edges, 58 | }; 59 | }, 60 | transformer: ({ nodes, edges }) => { 61 | const group = { nodes, edges }; 62 | const groupPaths = generatorPaths(group); 63 | const mul = multiplicationTable(group, groupPaths); 64 | //console.log('yo', cycleGraph(mul)); 65 | return cycleGraphFromMulTable(mul); 66 | }, 67 | rightEditorFactory: ({ nodes, edges }, parentEditor) => 68 | new ForceGraphEditorElement({ 69 | parentEditor, 70 | nodes, 71 | edges, 72 | }), 73 | output: (leftEditor, rightEditor) => leftEditor.getOutput(), 74 | }); 75 | const MathTextBidirectional = BidirectionalEditorPair({ 76 | name: "text-math", 77 | leftEditorFactory: (string, parentEditor) => 78 | new MyEditorElement({ 79 | code: string.split(""), 80 | parentEditor, 81 | }), 82 | rightEditorFactory: (string, parentEditor) => 83 | new MathEditorElement({ 84 | code: builder(string).output, 85 | parentEditor, 86 | }), 87 | output: (leftEditor, rightEditor) => leftEditor.getOutput(), 88 | }); 89 | // Limited by the number of colors on the colored graph currently 90 | const CayleyMatrixBidirectional = BidirectionalEditorPair({ 91 | name: "cayley-matrix", 92 | leftEditorFactory: (input, parentEditor) => { 93 | return new ForceColoredGraphEditorElement({ 94 | ...input, 95 | parentEditor, 96 | }); 97 | }, 98 | leftEditorOutput: (editor) => { 99 | const { nodes, edges } = editor.getJSONOutput(); 100 | return { 101 | nodes: nodes.map((node) => node.getOutput().split('"').join("")), 102 | edges, 103 | }; 104 | }, 105 | leftToRightTransformer: ({ nodes, edges }) => { 106 | const group = { nodes, edges }; 107 | const groupPaths = generatorPaths(group); 108 | const mul = multiplicationTable(group, groupPaths); 109 | return Object.values(mul).map((columnObject) => 110 | Object.values(columnObject).map((el) => [el]) 111 | ); 112 | }, 113 | rightToLeftTransformer: (array2d) => { 114 | const nodes = array2d[0]; 115 | const edges = { 116 | 0: [...nodes.map(() => [])], 117 | 1: [...nodes.map(() => [])], 118 | 2: [...nodes.map(() => [])], 119 | 3: [...nodes.map(() => [])], 120 | }; 121 | for (let x = 0; x < array2d.length; x++) { 122 | for (let y = 0; y < array2d[0].length; y++) { 123 | const result = array2d[x][y]; 124 | const resultIndex = nodes.findIndex((val) => val === result); 125 | if (y !== resultIndex) edges[x - 1][y].push(resultIndex); 126 | } 127 | } 128 | 129 | return { 130 | nodes, 131 | edges, 132 | }; 133 | }, 134 | 135 | rightEditorOutput: (editor) => { 136 | const out = editor.getOutput(); 137 | const array2dString = out.substring("matrix(".length, out.length - 1); 138 | return JSON.parse(array2dString); 139 | }, 140 | rightEditorFactory: (code2DArray, parentEditor) => 141 | new MathEditorElement({ 142 | parentEditor, 143 | code: [new MatrixJoinerElement({ code2DArray })], 144 | }), 145 | output: (leftEditor, rightEditor) => leftEditor.getOutput(), 146 | }); 147 | const CayleeToCycleAndTable = UnidirectionalEditorPair({ 148 | name: "graph-force-matrix", 149 | leftEditorFactory: (string, parentEditor) => 150 | new GraphToForceGraph({ 151 | parentEditor, 152 | }), 153 | leftEditorOutput: (editor) => { 154 | const { nodes, edges } = editor.leftEditor.getJSONOutput(); 155 | return { 156 | nodes: nodes.map((node) => node.getOutput().split('"').join("")), 157 | edges, 158 | }; 159 | }, 160 | transformer: ({ nodes, edges }) => { 161 | const group = { nodes, edges }; 162 | const groupPaths = generatorPaths(group); 163 | const mul = multiplicationTable(group, groupPaths); 164 | return Object.values(mul).map((columnObject) => 165 | Object.values(columnObject).map((el) => [el]) 166 | ); 167 | }, 168 | rightEditorFactory: (code2DArray, parentEditor) => 169 | new MathEditorElement({ 170 | parentEditor, 171 | code: [new MatrixJoinerElement({ code2DArray })], 172 | }), 173 | output: (leftEditor, rightEditor) => rightEditor.getOutput(), 174 | }); 175 | 176 | const dropdownItems = [ 177 | { 178 | name: "math", 179 | description: "make ~structured math equations!", 180 | ElementConstructor: MathEditorElement, 181 | iconPath: "./assets/icon_math.png", 182 | }, 183 | { 184 | name: "markdown", 185 | description: "add comments formatted with markdown", 186 | ElementConstructor: MarkdownEditorElement, 187 | }, 188 | { 189 | name: "force graph", 190 | description: "make automatically placed graphs", 191 | ElementConstructor: ForceGraphEditorElement, 192 | iconPath: "./assets/icon_graph.png", 193 | }, 194 | { 195 | name: "cgraph", 196 | description: "make colored graphs!", 197 | ElementConstructor: ForceColoredGraphEditorElement, 198 | iconPath: "./assets/icon_cgraph.png", 199 | }, 200 | { 201 | name: "eval", 202 | description: "", 203 | ElementConstructor: ED, 204 | }, 205 | { 206 | name: "text ⇔ math", 207 | description: "", 208 | ElementConstructor: MathTextBidirectional, 209 | }, 210 | { 211 | name: "c graph → force graph", 212 | description: "synchronize editors", 213 | ElementConstructor: GraphToForceGraph, 214 | }, 215 | { 216 | name: "(c graph → force graph) → matrix", 217 | description: "∆ group theory trifecta ∆", 218 | ElementConstructor: CayleeToCycleAndTable, 219 | }, 220 | { 221 | name: "Cayley ⇔ Matrix", 222 | description: "", 223 | ElementConstructor: CayleyMatrixBidirectional, 224 | }, 225 | { 226 | name: "Music Staff Editor", 227 | ElementConstructor: MusicStaffEditorElement, 228 | }, 229 | { 230 | name: "Char Array Editor", 231 | ElementConstructor: CharArrayEditorElement, 232 | }, 233 | ]; 234 | const CustomDropdown = DropdownElement( 235 | dropdownItems, 236 | Math.random().toFixed(5).slice(1) 237 | ); 238 | export const registerEditor = (editorItem) => dropdownItems.push(editorItem); 239 | 240 | export class MyEditorElement extends TextEditorElement { 241 | constructor() { 242 | super(...arguments); 243 | // Idea: put this info in editor metadata so that you can just pass in editor classes 244 | this.CustomDropdown = CustomDropdown; 245 | this.style.setProperty("--editor-name", `'text2'`); 246 | } 247 | 248 | keyHandler(e) { 249 | if (e.key === "Alt") { 250 | const focuser = new this.CustomDropdown({ 251 | parentEditor: this, 252 | builder: this.builder, 253 | }); 254 | this.code.splice(this.caret, 0, focuser); 255 | return focuser; 256 | } 257 | if (e.key === "`") { 258 | const focuser = new MathEditorElement({ 259 | code: builder(this.getHighlighted()).output, 260 | }); 261 | this.backspace(); 262 | this.code.splice(this.caret, 0, focuser); 263 | return focuser; 264 | } 265 | } 266 | } 267 | customElements.define("my-text-editor", MyEditorElement); 268 | 269 | const GraphEditorElement = MakeGraphEditorElement(MyEditorElement); 270 | dropdownItems.push( 271 | { 272 | name: "graph", 273 | description: "make simple graphs", 274 | ElementConstructor: GraphEditorElement, 275 | iconPath: "./assets/icon_graph.png", 276 | }, 277 | { 278 | name: "text", 279 | description: "embedded text editor", 280 | ElementConstructor: MyEditorElement, 281 | } 282 | ); 283 | 284 | const mathOperations = [ 285 | { 286 | name: "plus", 287 | arity: 2, 288 | ElementConstructor: PlusJoinerElement, 289 | }, 290 | { 291 | name: "mul", 292 | arity: 2, 293 | ElementConstructor: MulJoinerElement, 294 | }, 295 | { 296 | name: "sub", 297 | arity: 2, 298 | ElementConstructor: SubJoinerElement, 299 | }, 300 | { 301 | name: "div", 302 | arity: 2, 303 | ElementConstructor: DivJoinerElement, 304 | }, 305 | { 306 | name: "exp", 307 | arity: 2, 308 | ElementConstructor: ExpJoinerElement, 309 | }, 310 | { 311 | name: "sqrt", 312 | arity: 1, 313 | ElementConstructor: RadicalJoinerElement, 314 | }, 315 | ]; 316 | export const builder = createBuilder([ 317 | ...mathOperations.map(createFunctionProcessor), 318 | createJSONProcessor( 319 | (obj) => new GraphEditorElement(obj), 320 | (obj) => 321 | Array.isArray(obj.nodes) && 322 | Array.isArray(obj.edges) && 323 | Array.isArray(obj.positions) 324 | ), 325 | createJSONProcessor( 326 | (obj) => new ColoredGraphEditorElement(obj), 327 | (obj) => Boolean(obj.isColoredGraph) 328 | ), 329 | createJSONProcessor( 330 | (obj) => new ForceGraphEditorElement(obj), 331 | (obj) => Array.isArray(obj.nodes) && Array.isArray(obj.edges) 332 | ), 333 | createJSONProcessor( 334 | (obj) => new MusicStaffEditorElement({ contents: obj }), 335 | (obj) => Array.isArray(obj) && "note" in obj[0] 336 | ), 337 | ]); 338 | -------------------------------------------------------------------------------- /examples/ed_ex.js: -------------------------------------------------------------------------------- 1 | class StateMachine { 2 | constructor({ nodes, edges }) { 3 | this.nodes = nodes; 4 | this.edges = edges; 5 | this.stateIndex = 0; 6 | } 7 | 8 | next() { 9 | const currentEdges = this.edges[this.stateIndex]; 10 | const randomEdge = Math.floor( 11 | Math.random() * currentEdges.length 12 | ); 13 | this.stateIndex = currentEdges[randomEdge]; 14 | return this.nodes[this.stateIndex]; 15 | } 16 | } 17 | 18 | const stateMachine = new StateMachine(({ 19 | "nodes": ["a","b","c"], 20 | "edges": [[1],[2],[0]], 21 | "positions": [[40, 55],[146, 91],[61, 161]] 22 | })); 23 | 24 | setInterval(() => Polytope.out(stateMachine.next()), 1000); 25 | -------------------------------------------------------------------------------- /examples/ed_ex2.js: -------------------------------------------------------------------------------- 1 | plus(sub(1, 1), exp(2, exp(1, 2))) 2 | 3 | 4 | 5 | ({ 6 | "nodes": ["2","1",""], 7 | "edges": [[1],[0],[]] 8 | }) 9 | 10 | 11 | ({"nodes":["10","21"],"edges":{"0":[[],[]],"1":[[],[]],"2":[[],[]],"3":[[],[]]},"positions":[[128.13939289583863,68.96506593219304],[88.32287378025936,152.2652399952787]],"isColoredGraph":true}) -------------------------------------------------------------------------------- /examples/g.js: -------------------------------------------------------------------------------- 1 | const g = ({"nodes":["0","1","2","3","4","5","6"],"edges":{"0":[[1],[2],[3],[4],[5],[6],[0]],"1":[[],[],[],[],[],[],[]],"2":[[],[],[],[],[],[],[]],"3":[[],[],[],[],[],[],[]]},"positions":[[86,65],[173,95],[140,177],[79,192],[30,157],[30,98],[39,53]],"isColoredGraph":true}); 2 | const gPaths = generatorPaths(g); 3 | const gMul = multiplicationTable(g, gPaths); 4 | 5 | const gSq = gMap(g, e => gMul[e][e]); 6 | Polytope.out(gSq); 7 | ({ 8 | "nodes": ["",""], 9 | "edges": [[],[]] 10 | }) -------------------------------------------------------------------------------- /examples/groupTheoryPlayground.js: -------------------------------------------------------------------------------- 1 | import { Group } from "http://localhost:8080/groupTheory.js"; 2 | 3 | export const z3 = new Group({ 4 | cayleeGraph: ({"nodes":["0","1","2"],"edges":{"0":[[1],[2],[0]],"1":[[],[],[]],"2":[[],[],[]],"3":[[],[],[]]},"positions":[[103,76],[145,148],[55,149]],"isColoredGraph":true}) 5 | }); 6 | 7 | export const a4 = new Group({ 8 | cayleeGraph: ({"nodes":["e","x","c","d","a","b","d2","b2","a2","c2","z","y"],"edges":{"0":[[4],[5],[6],[7],[8],[9],[10],[11],[0],[1],[2],[3]],"1":[[1],[0],[4],[5],[2],[3],[9],[8],[7],[6],[11],[10]],"2":[[],[],[],[],[],[],[],[],[],[],[],[]],"3":[[],[],[],[],[],[],[],[],[],[],[],[]]},"positions":[[25,19],[91,19],[141,17],[211,18],[0,108],[56,108],[112,109],[172,106],[19,197],[85,200],[140,197],[211,198]],"isColoredGraph":true}) 9 | }); 10 | 11 | export const s1 = new Group({ 12 | cayleeGraph: ({"nodes":["0"],"edges":{"0":[[]],"1":[[]],"2":[[]],"3":[[]]},"positions":[[102,111]],"isColoredGraph":true}) 13 | }); 14 | export const s2 = new Group({ 15 | cayleeGraph: ({"nodes":["0","1"],"edges":{"0":[[1],[0]],"1":[[],[]],"2":[[],[]],"3":[[],[]]},"positions":[[77,116],[140,118]],"isColoredGraph":true}) 16 | }); 17 | export const s3 = new Group({ 18 | cayleeGraph: ({"nodes":["0","1","2","3","4","5"],"edges":{"0":[[1],[2],[0],[5],[3],[4]],"1":[[3],[4],[5],[0],[1],[2]],"2":[[],[],[],[],[],[]],"3":[[],[],[],[],[],[]]},"positions":[[104,43],[201,192],[19,194],[103,104],[139,162],[70,162]],"isColoredGraph":true}) 19 | }); 20 | 21 | console.log(s3); 22 | -------------------------------------------------------------------------------- /examples/math_ops.js: -------------------------------------------------------------------------------- 1 | function mul(a, b) { 2 | return a * b; 3 | } 4 | 5 | function div(a, b) { 6 | return a / b; 7 | } 8 | 9 | function plus(a, b) { 10 | return a + b; 11 | } 12 | 13 | function sub(a, b) { 14 | return a - b; 15 | } 16 | 17 | function sqrt(a) { 18 | return Math.sqrt(a); 19 | } 20 | 21 | function exp(a, b) { 22 | return a ** b; 23 | } 24 | 25 | function matrix([[a,b],[c,d]]) { 26 | return [[a, b], [c, d]]; 27 | } -------------------------------------------------------------------------------- /examples/music.js: -------------------------------------------------------------------------------- 1 | // https://pages.mtu.edu/~suits/notefreqs.html 2 | const NOTE_TO_FREQ = { e4: 329.63, f4: 349.23, g4: 392.0, 3 | a4: 440.0, b4: 493.88, c5: 523.25, d5: 587.33, e5: 659.25, 4 | f5: 698.46, g5: 783.99, " ": 0, 5 | }; 6 | function play(notes) { 7 | const context = new AudioContext(); 8 | 9 | let i = 0; 10 | for (const { note, length } of notes) { 11 | const oscillator = context.createOscillator(); 12 | oscillator.frequency.value = NOTE_TO_FREQ[note]; 13 | oscillator.type = "sine"; 14 | 15 | oscillator.connect(context.destination); 16 | oscillator.start(i / 4); 17 | oscillator.stop((i + 1) / 4); 18 | i++; 19 | } 20 | } 21 | 22 | play(([{"note":"g4","length":1},{"note":"a4","length":1},{"note":" ","length":1},{"note":"f4","length":1},{"note":"g4","length":1},{"note":" ","length":1},{"note":"d5","length":1},{"note":"e5","length":1},{"note":" ","length":1},{"note":"f5","length":1},{"note":"e5","length":1}])); 23 | -------------------------------------------------------------------------------- /examples/new.js: -------------------------------------------------------------------------------- 1 | const addOneToNodes = ({ nodes, edges }) => ({ 2 | nodes: nodes.map((node) => Number(node) + 1), 3 | edges, 4 | }); 5 | 6 | const graph = ({ 7 | "nodes": ["0","1","3","4","2"], 8 | "edges": [[1,2,3,4],[0,2,4,3],[0,1,3,4],[0,2,1,4],[2,1,3,0]] 9 | }); 10 | 11 | const modifiedGraph = addOneToNodes(graph); 12 | 13 | Polytope.out(modifiedGraph) -------------------------------------------------------------------------------- /groupTheory.js: -------------------------------------------------------------------------------- 1 | export function generatorPaths({ nodes, edges }) { 2 | const q = []; 3 | const explored = new Set(); 4 | const pathParent = []; 5 | for (const generatorIndex of Object.keys(edges)) { 6 | pathParent[generatorIndex] = new Map(); 7 | } 8 | const paths = new Map(); 9 | paths.set(nodes[0], []); 10 | 11 | explored.add(0); 12 | q.unshift(0); 13 | 14 | // breadth first search to find shortest path from e to each node via the generators (edges) 15 | while (q.length > 0) { 16 | const fromNodeIndex = q.pop(); 17 | 18 | for (const [generatorIndex, generatorEdges] of Object.entries(edges)) { 19 | for (const toNodeIndex of generatorEdges[fromNodeIndex]) { 20 | if (!explored.has(toNodeIndex)) { 21 | paths.set(nodes[toNodeIndex], [ 22 | ...paths.get(nodes[fromNodeIndex]), 23 | generatorIndex, 24 | ]); 25 | pathParent[generatorIndex].set(toNodeIndex, fromNodeIndex); 26 | explored.add(fromNodeIndex); 27 | q.unshift(toNodeIndex); 28 | } 29 | } 30 | } 31 | } 32 | return paths; 33 | } 34 | 35 | export function multiplicationTable({ nodes, edges }, paths) { 36 | const multiplicationTable = {}; 37 | 38 | for (let i = 0; i < nodes.length; i++) { 39 | const from = nodes[i]; 40 | for (let j = 0; j < nodes.length; j++) { 41 | const to = nodes[j]; 42 | let tracei = i; 43 | for (const generator of paths.get(to)) { 44 | tracei = edges[generator][tracei][0]; 45 | } 46 | if (!multiplicationTable[from]) multiplicationTable[from] = {}; 47 | multiplicationTable[from][to] = nodes[tracei]; 48 | } 49 | } 50 | return multiplicationTable; 51 | } 52 | 53 | export function cycleGraphFromMulTable(multiplicationTable) { 54 | const nodes = Object.keys(multiplicationTable); 55 | const id = nodes[0]; 56 | const edges = nodes.map((v) => []); 57 | 58 | for (let i = 0; i < nodes.length; i++) { 59 | const el = nodes[i]; 60 | 61 | edges[0].push(i); 62 | let cur = el; 63 | while (cur !== id) { 64 | const next = multiplicationTable[cur][el]; 65 | 66 | const curIndex = nodes.findIndex((v) => v === cur); 67 | const nextIndex = nodes.findIndex((v) => v === next); 68 | edges[curIndex].push(nextIndex); 69 | 70 | cur = next; 71 | } 72 | } 73 | 74 | return { 75 | nodes, 76 | edges, 77 | }; 78 | } 79 | 80 | export class Group { 81 | identityElement; 82 | elements = []; 83 | mulTable = {}; 84 | 85 | constructor({ cayleeGraph, mulTable }) { 86 | if (!Boolean(cayleeGraph) && !Boolean(mulTable)) { 87 | throw "Group constructor: expecting a cayleeGraph or multiplicationTable!"; 88 | } 89 | if (Boolean(cayleeGraph) && !Boolean(mulTable)) { 90 | this.mulTable = multiplicationTable( 91 | cayleeGraph, 92 | generatorPaths(cayleeGraph) 93 | ); 94 | } 95 | if (!Boolean(cayleeGraph) && Boolean(mulTable)) { 96 | this.mulTable = mulTable; 97 | } 98 | this.elements = Object.keys(this.mulTable); 99 | // This class assumes that the first element in the multiplication table is the identity 100 | this.identityElement = this.elements[0]; 101 | } 102 | 103 | action(elementA, elementB) { 104 | return this.mulTable[elementA][elementB]; 105 | } 106 | 107 | exponentiate(element, integer) { 108 | if (integer === 0) return this.identityElement; 109 | let currentElement = element; 110 | for (let i = 1; i < integer; i++) { 111 | currentElement = this.mulTable[currentElement][element]; 112 | } 113 | return currentElement; 114 | } 115 | } 116 | 117 | export function gMap({ nodes, edges, positions }, f) { 118 | const newNodes = []; 119 | const nodeIndexToNewNodeIndex = {}; 120 | const newEdges = { 0: [], 1: [], 2: [], 3: [] }; 121 | const newPositions = []; 122 | 123 | nodes.forEach((node, i) => { 124 | const newNode = f(node); 125 | const newNodeIndex = newNodes.findIndex((n) => n === newNode); 126 | if (newNodeIndex === -1) { 127 | nodeIndexToNewNodeIndex[i] = newNodes.length; 128 | newNodes.push(newNode); 129 | newEdges[0].push(edges[0][i]); 130 | newEdges[1].push(edges[1][i]); 131 | newEdges[2].push(edges[2][i]); 132 | newEdges[3].push(edges[3][i]); 133 | newPositions.push(positions[i]); 134 | } else { 135 | nodeIndexToNewNodeIndex[i] = newNodeIndex; 136 | } 137 | }); 138 | 139 | const edgeMap = (es) => { 140 | const mappedEdges = es.map((toIndex) => nodeIndexToNewNodeIndex[toIndex]); 141 | const deduplicatedEdges = [...new Set(mappedEdges)]; 142 | return deduplicatedEdges.filter((toIndex) => toIndex !== undefined); 143 | }; 144 | 145 | newEdges[0] = newEdges[0].map(edgeMap); 146 | newEdges[1] = newEdges[1].map(edgeMap); 147 | newEdges[2] = newEdges[2].map(edgeMap); 148 | newEdges[3] = newEdges[3].map(edgeMap); 149 | 150 | return { 151 | nodes: newNodes, 152 | edges: newEdges, 153 | positions: newPositions, 154 | isColoredGraph: true, 155 | }; 156 | } 157 | -------------------------------------------------------------------------------- /homepage/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vezwork/Polytope/6f4e7ccb66d68dca0b359014d8931a16b6144244/homepage/.DS_Store -------------------------------------------------------------------------------- /homepage/update1_image1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vezwork/Polytope/6f4e7ccb66d68dca0b359014d8931a16b6144244/homepage/update1_image1.png -------------------------------------------------------------------------------- /homepage/update1_image2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vezwork/Polytope/6f4e7ccb66d68dca0b359014d8931a16b6144244/homepage/update1_image2.png -------------------------------------------------------------------------------- /homepage/update1_image3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vezwork/Polytope/6f4e7ccb66d68dca0b359014d8931a16b6144244/homepage/update1_image3.png -------------------------------------------------------------------------------- /homepage/update1_video1.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vezwork/Polytope/6f4e7ccb66d68dca0b359014d8931a16b6144244/homepage/update1_video1.mp4 -------------------------------------------------------------------------------- /homepage/update2_image1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vezwork/Polytope/6f4e7ccb66d68dca0b359014d8931a16b6144244/homepage/update2_image1.png -------------------------------------------------------------------------------- /homepage/update2_image2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vezwork/Polytope/6f4e7ccb66d68dca0b359014d8931a16b6144244/homepage/update2_image2.png -------------------------------------------------------------------------------- /homepage/update2_image3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vezwork/Polytope/6f4e7ccb66d68dca0b359014d8931a16b6144244/homepage/update2_image3.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 |
6 |
7 | 8 |
9 | Output: 10 | 11 |
12 | 13 |
14 | View on GitHub 15 | 21 | 25 |

27 | 28 | Instructions and controls: 29 | 30 | 56 | 57 |


58 |


59 |


60 |


61 |


62 |


63 |


64 |


65 |


66 |


67 |


68 | 69 | 74 | 78 | 79 | Back to presentation 80 | 81 | 82 | 83 | 187 | 188 | 206 | -------------------------------------------------------------------------------- /lib/.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | -------------------------------------------------------------------------------- /lib/README.md: -------------------------------------------------------------------------------- 1 | ## Building and running locally 2 | 3 | 1. Clone this repo to the folder of your choice. 4 | 2. Navigate to that folder in your terminal. 5 | 3. Run `npm install`. 6 | 4. Run `npm run dev`. This terminal window will now use Webpack to watch for any changes to files in 7 | `./src`. If you change a file, the project will automatically rebuild. 8 | 5. Open the folder in your file explorer. 9 | 6. Navigate to `./dist` and open index.html in your browser. 10 | -------------------------------------------------------------------------------- /lib/dist/Iterable.js: -------------------------------------------------------------------------------- 1 | export const historyArray = function* (number, iterable) { 2 | let history = Array(number); 3 | for (const result of iterable) { 4 | history = [result, ...history]; 5 | history.length = number; 6 | yield history; 7 | } 8 | }; 9 | export const range = function* (start, end, step = 1) { 10 | for (let n = start; n < end; n += step) { 11 | yield n; 12 | } 13 | }; 14 | export const take = function* (number, iterable) { 15 | let i = 0; 16 | for (const item of iterable) { 17 | i++; 18 | if (i > number) 19 | break; 20 | yield item; 21 | } 22 | }; 23 | export const first = (iterable) => Array.from(take(1, iterable))[0]; 24 | export const skip = function* (number, iterable) { 25 | let i = 0; 26 | for (const item of iterable) { 27 | i++; 28 | if (i <= number) 29 | continue; 30 | yield item; 31 | } 32 | }; 33 | export const map = function* (iterable, func) { 34 | for (const item of iterable) 35 | yield func(item); 36 | }; 37 | export const some = function (iterable, func) { 38 | for (const item of iterable) { 39 | if (func(item)) 40 | return true; 41 | } 42 | return false; 43 | }; 44 | export const withIndex = function* (iterable) { 45 | let i = 0; 46 | for (const item of iterable) { 47 | yield [item, i]; 48 | i++; 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /lib/dist/editor.js: -------------------------------------------------------------------------------- 1 | export class EditorElement extends HTMLElement { 2 | meta; 3 | parentEditor = undefined; 4 | builder; 5 | isFocused = false; 6 | constructor({ parentEditor, builder } = { 7 | parentEditor: undefined, 8 | }) { 9 | super(); 10 | this.builder = builder; 11 | this.parentEditor = parentEditor; 12 | this.attachShadow({ mode: "open" }); 13 | const paletteEl = document.createElement("div"); 14 | paletteEl.className = "palette"; 15 | // EXPERIMENTAL CODE 16 | const butEl = document.createElement("span"); 17 | butEl.className = "but"; 18 | butEl.innerText = "↑eval↑"; 19 | butEl.addEventListener("click", async () => { 20 | const [{ ConstructiveUnidirectionalEditor }, { TextEditorElement }] = await Promise.all([ 21 | import("./editors/bidirectional_editor_pair.js"), 22 | import("./editors/TextEditorElement.js"), 23 | ]); 24 | const LiftedEval = ConstructiveUnidirectionalEditor({ 25 | leftEditorFactory: (a, me) => new TextEditorElement({ parentEditor: me, code: [this] }), 26 | leftEditorOutput: (editor) => editor.getOutput(), 27 | name: Math.random().toFixed(4).toString(), 28 | }); 29 | this.parentEditor?.dispatchEvent(new CustomEvent("subEditorReplaced", { 30 | detail: { 31 | old: this, 32 | new: new LiftedEval({ 33 | parentEditor: this.parentEditor, 34 | builder: this.builder, 35 | }), 36 | }, 37 | })); 38 | }); 39 | // EXPERIMENTAL CODE END 40 | const styleEl = document.createElement("style"); 41 | styleEl.textContent = ` 42 | :host { 43 | --editor-name: 'editor'; 44 | --editor-color: #017BFF; 45 | --editor-name-color: white; 46 | --editor-background-color: #E6F2FF; 47 | --editor-outline-color: #d4e9ff; 48 | 49 | // unused: 50 | --highlight-text-color: black; 51 | --highlight-editor-color: yellow; 52 | --highlight-editor-name-color: black; 53 | --highlight-editor-background-color: yellow; 54 | 55 | display: inline-flex; 56 | justify-content: center; 57 | 58 | vertical-align: middle; 59 | 60 | user-select: none; 61 | border-radius: 4px; 62 | background: var(--editor-background-color); 63 | border: 2px solid var(--editor-outline-color); 64 | box-sizing: border-box; 65 | position: relative; 66 | font-family: monospace; 67 | 68 | line-height: 1; 69 | 70 | min-height: 1.6rem; 71 | min-width: 0.5rem; 72 | } 73 | .but { 74 | display: inline-block; 75 | cursor: pointer; 76 | opacity: 0.8; 77 | padding: 2px; 78 | margin: 1px 1px 1px 6px; 79 | background: var(--editor-name-color); 80 | color: var(--editor-color); 81 | border-radius: 0 0 3px 3px; 82 | } 83 | .but:hover { 84 | opacity: 1; 85 | } 86 | .palette { 87 | display: none; 88 | } 89 | :host(:focus) .palette { 90 | display: block; 91 | font-size: 14px; 92 | padding: 1px 2px 2px 8px; 93 | background: var(--editor-color); 94 | color: var(--editor-name-color); 95 | position: absolute; 96 | bottom: -24px; 97 | left: -2px; 98 | border-radius: 0 0 4px 4px; 99 | font-family: monospace; 100 | z-index: 10; 101 | } 102 | :host(:focus) { 103 | border: 2px solid var(--editor-color); 104 | color: black !important; 105 | outline: none; 106 | } 107 | :host(:not(:focus)) { 108 | color: rgba(0,0,0,0.5); 109 | } 110 | `; 111 | setTimeout(() => { 112 | paletteEl.innerText = this.meta?.editorName ?? "editor"; 113 | if (!this.meta?.isUnstyled) 114 | this.shadowRoot.append(styleEl, paletteEl); 115 | paletteEl.append(butEl); 116 | }); 117 | if (!this.hasAttribute("tabindex")) 118 | this.setAttribute("tabindex", "0"); 119 | this.addEventListener("focus", (e) => { 120 | e.stopPropagation(); 121 | e.preventDefault(); 122 | this.isFocused = true; 123 | }); 124 | this.addEventListener("subEditorClicked", (e) => { 125 | this.parentEditor?.dispatchEvent(new CustomEvent("subEditorClicked", { detail: [this, ...e.detail] })); 126 | }); 127 | this.addEventListener("blur", (e) => { 128 | e.stopPropagation(); 129 | this.isFocused = false; 130 | }); 131 | this.addEventListener("mousedown", (e) => { 132 | e.stopPropagation(); 133 | this.parentEditor?.dispatchEvent(new CustomEvent("subEditorClicked", { detail: [this] })); 134 | this.focus(); 135 | this.isFocused = true; 136 | }); 137 | this.addEventListener("keydown", (e) => e.stopPropagation()); 138 | } 139 | get javaScriptCode() { 140 | return ""; 141 | } 142 | focusEditor(fromEl, position, isSelecting) { 143 | this.focus({ preventScroll: true }); 144 | } 145 | getOutput() { 146 | return ""; 147 | } 148 | } 149 | // editorDescription: [{ 150 | // name: string; 151 | // description: string; 152 | // iconPath: string; 153 | // ElementConstructor: HTMLElement; 154 | // }] 155 | customElements.define("polytope-editor", EditorElement); 156 | -------------------------------------------------------------------------------- /lib/dist/editors/CharArrayEditorElement.js: -------------------------------------------------------------------------------- 1 | import { ArrayEditorElement } from "./ArrayEditorElement.js"; 2 | export class CharArrayEditorElement extends ArrayEditorElement { 3 | meta = { 4 | editorName: "Char Array", 5 | }; 6 | onCaretMoveOverContentItem(contentItems) { 7 | //console.log(contentItems); 8 | } 9 | keyHandler(e) { 10 | if (e.key.length === 1) { 11 | this.insert([e.key]); 12 | this.render(); 13 | return true; 14 | } 15 | return false; // override me! 16 | } 17 | processClipboardText(clipboardText) { 18 | return clipboardText.split(""); // override me! 19 | } 20 | } 21 | customElements.define("char-array-editor", CharArrayEditorElement); 22 | -------------------------------------------------------------------------------- /lib/dist/editors/DropdownElement.js: -------------------------------------------------------------------------------- 1 | import { withIndex } from "../Iterable.js"; 2 | import { mod } from "../math.js"; 3 | import { TextEditorElement } from "./TextEditorElement.js"; 4 | export const DropdownElement = (editorDescriptions, name = "no-name") => { 5 | class C extends TextEditorElement { 6 | meta = { 7 | editorName: name, 8 | }; 9 | selection = 0; 10 | dropdownEl; 11 | editorEls; 12 | constructor() { 13 | super(...arguments); 14 | this.style.setProperty("--editor-name", `'dropdown'`); 15 | this.style.setProperty("--editor-color", "grey"); 16 | this.style.setProperty("--editor-name-color", "black"); 17 | this.style.setProperty("--editor-background-color", "#FEFEFE"); 18 | this.style.setProperty("--editor-outline-color", "grey"); 19 | this.styleEl = document.createElement("style"); 20 | this.styleEl.textContent = ` 21 | .dropdown { 22 | display: none; 23 | } 24 | .dropdown pre { 25 | margin: 0; 26 | padding: 10px; 27 | } 28 | .dropdown pre:hover { 29 | background: #ffd608; 30 | 31 | } 32 | :host(:focus) .dropdown { 33 | display: block; 34 | position: absolute; 35 | top: 100%; 36 | left: -2px; 37 | margin: 0; 38 | background: #FEFEFE; 39 | z-index: 100; 40 | border-radius: 2px; 41 | border: 2px solid grey; 42 | } 43 | `; 44 | this.dropdownEl = document.createElement("div"); 45 | this.dropdownEl.className = "dropdown"; 46 | this.editorEls = editorDescriptions.map(({ name, description, iconPath, ElementConstructor }) => { 47 | const editorEl = document.createElement("pre"); 48 | editorEl.innerHTML = 49 | (iconPath ? ` ` : "") + 50 | `${name} 51 | ${description}`; 52 | editorEl.addEventListener("click", () => { 53 | if (this.parentEditor) { 54 | this.parentEditor.dispatchEvent(new CustomEvent("subEditorReplaced", { 55 | detail: { 56 | old: this, 57 | new: new ElementConstructor({ 58 | parentEditor: this.parentEditor, 59 | builder: this.builder, 60 | }), 61 | }, 62 | })); 63 | } 64 | }); 65 | return editorEl; 66 | }); 67 | this.dropdownEl.append(...this.editorEls); 68 | this.shadowRoot.append(this.styleEl, this.dropdownEl); 69 | this.addEventListener("blur", () => (this.code = [])); 70 | this.addEventListener("keydown", (e) => { 71 | if (e.key === "ArrowDown") { 72 | e.preventDefault(); 73 | this.selection = mod(this.selection + 1, this.editorEls.length); 74 | } 75 | else if (e.key === "ArrowUp") { 76 | e.preventDefault(); 77 | this.selection = mod(this.selection - 1, this.editorEls.length); 78 | } 79 | for (const [editorEl, i] of withIndex(this.editorEls)) { 80 | if (i === this.selection) 81 | editorEl.style.background = "#ffd608"; 82 | else 83 | editorEl.style.background = "#FEFEFE"; 84 | } 85 | if (e.key === "Enter") { 86 | if (this.parentEditor) { 87 | this.parentEditor.dispatchEvent(new CustomEvent("subEditorReplaced", { 88 | detail: { 89 | old: this, 90 | new: new editorDescriptions[this.selection].ElementConstructor({ 91 | parentEditor: this.parentEditor, 92 | builder: this.builder, 93 | }), 94 | }, 95 | })); 96 | } 97 | } 98 | }); 99 | for (const [editorEl, i] of withIndex(this.editorEls)) { 100 | if (i === this.selection) 101 | editorEl.style.background = "#ffd608"; 102 | else 103 | editorEl.style.background = "#FEFEFE"; 104 | } 105 | } 106 | getOutput() { 107 | return ""; 108 | } 109 | } 110 | customElements.define(`dropdown-${name}-editor`, C); 111 | return C; 112 | }; 113 | -------------------------------------------------------------------------------- /lib/dist/editors/ForceGraphEditorElement.js: -------------------------------------------------------------------------------- 1 | import { EditorElement } from "../editor.js"; 2 | import { withIndex } from "../Iterable.js"; 3 | import { add, distance, mul, sub } from "../math.js"; 4 | import { StringEditorElement } from "./StringEditorElement.js"; 5 | export class ForceGraphEditorElement extends EditorElement { 6 | meta = { 7 | editorName: "Force Graph", 8 | }; 9 | nodes = []; 10 | styleEl; 11 | canvas; 12 | context; 13 | fromNode; 14 | mouse; 15 | constructor() { 16 | super(...arguments); 17 | this.style.setProperty("--editor-name", `'graph'`); 18 | this.style.setProperty("--editor-color", "#7300CF"); 19 | this.style.setProperty("--editor-name-color", "white"); 20 | this.style.setProperty("--editor-background-color", "#eed9ff"); 21 | this.style.setProperty("--editor-outline-color", "#b59dc9"); 22 | this.styleEl = document.createElement("style"); 23 | this.styleEl.textContent = ` 24 | :host { 25 | position: relative; 26 | height: 250px; 27 | width: 250px; 28 | } 29 | 30 | canvas { 31 | position: absolute; 32 | top: 0; 33 | left: 0; 34 | } 35 | `; 36 | this.canvas = document.createElement("canvas"); 37 | this.canvas.width = 250; 38 | this.canvas.height = 250; 39 | this.context = this.canvas.getContext("2d"); 40 | this.shadowRoot.append(this.styleEl, this.canvas); 41 | this.fromNode = null; 42 | this.mouse = [0, 0]; 43 | this.fromInput(arguments[0]); 44 | setTimeout(() => this.render()); 45 | this.addEventListener("keydown", (e) => { 46 | if (e.key === "Backspace" && this.parentEditor) { 47 | this.parentEditor.dispatchEvent(new CustomEvent("subEditorDeleted", { detail: this })); 48 | } 49 | else if (e.key === "ArrowLeft" && this.parentEditor) { 50 | this.blur(); 51 | this.parentEditor.focusEditor(this, 0, e.shiftKey); 52 | } 53 | else if (e.key === "ArrowRight" && this.parentEditor) { 54 | this.blur(); 55 | this.parentEditor.focusEditor(this, 1, e.shiftKey); 56 | } 57 | }); 58 | this.addEventListener("subEditorDeleted", (e) => { 59 | this.nodes = this.nodes.filter(({ editor }) => editor !== e.detail); 60 | for (const node of this.nodes) { 61 | node.adjacent = node.adjacent.filter(({ editor }) => editor !== e.detail); 62 | } 63 | this.shadowRoot.removeChild(e.detail); 64 | this.render(); 65 | this.focusEditor(); 66 | new CustomEvent("childEditorUpdate", { 67 | detail: { 68 | out: this.getOutput(), 69 | editor: this, 70 | }, 71 | }); 72 | }); 73 | this.addEventListener("subEditorClicked", (e) => { 74 | const subFocus = this.nodes.find(({ editor }) => editor === e.detail[0]); 75 | if (subFocus) { 76 | this.fromNode = subFocus; // HACK 77 | } 78 | }); 79 | this.addEventListener("mousemove", (e) => { 80 | this.mouse = [e.offsetX, e.offsetY]; 81 | if (this.fromNode && e.metaKey) { 82 | this.fromNode.x = Math.max(0, Math.min(this.mouse[0] - 10, this.offsetWidth - this.fromNode.editor.offsetWidth - 2)); 83 | this.fromNode.y = Math.max(0, Math.min(this.mouse[1] - 10, this.offsetHeight - this.fromNode.editor.offsetHeight - 2)); 84 | } 85 | this.render(); 86 | }); 87 | this.addEventListener("mousedown", (e) => { 88 | const editor = new StringEditorElement({ parentEditor: this }); 89 | editor.style.position = "absolute"; 90 | const node = { 91 | x: e.offsetX - 10, 92 | y: e.offsetY - 10, 93 | editor, 94 | adjacent: [], 95 | }; 96 | this.nodes.push(node); 97 | this.shadowRoot.append(editor); 98 | this.blur(); 99 | setTimeout(() => editor.focusEditor()); 100 | //this.fromNode = node; 101 | this.render(); 102 | new CustomEvent("childEditorUpdate", { 103 | detail: { 104 | out: this.getOutput(), 105 | editor: this, 106 | }, 107 | }); 108 | }); 109 | this.addEventListener("mouseup", (e) => { 110 | const targetEl = e.composedPath()[0]; 111 | const targetNode = this.nodes.find(({ editor }) => editor === targetEl || 112 | editor.contains(targetEl) || 113 | editor.shadowRoot.contains(targetEl)); 114 | if (targetNode) { 115 | if (this.fromNode && 116 | this.fromNode !== targetNode && 117 | !this.fromNode.adjacent.includes(targetNode)) { 118 | this.fromNode.adjacent.push(targetNode); 119 | targetNode.adjacent.push(this.fromNode); 120 | new CustomEvent("childEditorUpdate", { 121 | detail: { 122 | out: this.getOutput(), 123 | editor: this, 124 | }, 125 | }); 126 | } 127 | } 128 | this.fromNode = null; 129 | this.render(); 130 | }); 131 | const move = () => { 132 | const middleOfEditor = [ 133 | this.offsetWidth / 2, 134 | this.offsetHeight / 2, 135 | ]; 136 | const forces = []; 137 | for (let i = 0; i < this.nodes.length; i++) 138 | forces[i] = [0, 0]; 139 | for (let i = 0; i < this.nodes.length; i++) { 140 | const node = this.nodes[i]; 141 | const start = [ 142 | node.x + node.editor.offsetWidth / 2, 143 | node.y + node.editor.offsetHeight / 2, 144 | ]; 145 | const dirToMiddle = sub(middleOfEditor, start); 146 | const distToMiddle = distance(start, middleOfEditor); 147 | const nudgeToMiddle = mul(0.0005 * distToMiddle, dirToMiddle); 148 | forces[i] = add(forces[i], nudgeToMiddle); 149 | for (let j = i + 1; j < this.nodes.length; j++) { 150 | const otherNode = this.nodes[j]; 151 | const end = [ 152 | otherNode.x + otherNode.editor.offsetWidth / 2, 153 | otherNode.y + otherNode.editor.offsetHeight / 2, 154 | ]; 155 | const dir = sub(end, start); 156 | const mag = distance(start, end); 157 | let force = mul(node.editor.offsetWidth ** 1.3 / mag ** 2, dir); 158 | //if (node.adjacent.includes(otherNode)) force = add(force, mul(-mag / 500, dir)); 159 | forces[i] = add(forces[i], mul(-1, force)); 160 | forces[j] = add(forces[j], force); 161 | } 162 | } 163 | for (let i = 0; i < this.nodes.length; i++) { 164 | const node = this.nodes[i]; 165 | const [x, y] = add([node.x, node.y], forces[i]); 166 | node.x = x; 167 | node.y = y; 168 | } 169 | }; 170 | move(); 171 | const step = () => { 172 | this.render(); 173 | move(); 174 | requestAnimationFrame(step); 175 | }; 176 | requestAnimationFrame(step); 177 | } 178 | render() { 179 | this.context.strokeStyle = "#7300CF"; 180 | this.context.fillStyle = "#7300CF"; 181 | this.context.lineWidth = 2; 182 | this.context.lineCap = "round"; 183 | this.canvas.width = this.offsetWidth; 184 | this.canvas.height = this.offsetHeight; 185 | if (this.fromNode) { 186 | this.context.beginPath(); 187 | this.context.moveTo(this.fromNode.x + this.fromNode.editor.offsetWidth / 2, this.fromNode.y + this.fromNode.editor.offsetHeight / 2); 188 | this.context.lineTo(...this.mouse); 189 | this.context.stroke(); 190 | } 191 | for (const { x, y, editor, adjacent } of this.nodes) { 192 | editor.style.top = `${y}px`; 193 | editor.style.left = `${x}px`; 194 | for (const otherNode of adjacent) { 195 | this.context.beginPath(); 196 | const start = [ 197 | x + editor.offsetWidth / 2, 198 | y + editor.offsetHeight / 2, 199 | ]; 200 | const end = [ 201 | otherNode.x + otherNode.editor.offsetWidth / 2, 202 | otherNode.y + otherNode.editor.offsetHeight / 2, 203 | ]; 204 | this.context.moveTo(...start); 205 | this.context.lineTo(...end); 206 | this.context.stroke(); 207 | } 208 | } 209 | } 210 | fromInput(input) { 211 | if (!input || !input.nodes || !input.edges) 212 | return; 213 | const { nodes, edges } = input; 214 | for (let i = 0; i < nodes.length; i++) { 215 | const nodeValue = nodes[i]; 216 | let editor; 217 | if (nodeValue instanceof EditorElement) { 218 | editor = new StringEditorElement({ 219 | code: [nodeValue], 220 | parentEditor: this, 221 | }); 222 | } 223 | else { 224 | editor = new StringEditorElement({ 225 | code: [String(nodeValue)], 226 | parentEditor: this, 227 | }); 228 | } 229 | editor.style.position = "absolute"; 230 | this.nodes[i] = { x: 100 + i, y: 100 + i, editor, adjacent: [] }; 231 | this.shadowRoot.append(editor); 232 | } 233 | for (let i = 0; i < nodes.length; i++) { 234 | const edgeList = edges[i]; 235 | for (const edgeIndex of edgeList) { 236 | this.nodes[i].adjacent.push(this.nodes[edgeIndex]); 237 | } 238 | } 239 | } 240 | getOutput() { 241 | let nodes = []; 242 | let edges = []; 243 | for (const [{ editor, x, y, adjacent }, i] of withIndex(this.nodes)) { 244 | nodes[i] = editor.getOutput(); 245 | edges[i] = []; 246 | for (const otherNode of adjacent) { 247 | const otherNodeIndex = this.nodes.findIndex((n) => n === otherNode); 248 | edges[i].push(otherNodeIndex); 249 | } 250 | } 251 | return `({ 252 | "nodes": [${nodes}], 253 | "edges": [${edges.map((edgeList) => `[${edgeList}]`)}] 254 | })`; 255 | } 256 | } 257 | customElements.define("force-graph-editor", ForceGraphEditorElement); 258 | -------------------------------------------------------------------------------- /lib/dist/editors/MakeGraphEditorElement.js: -------------------------------------------------------------------------------- 1 | import { EditorElement } from "../editor.js"; 2 | import { withIndex } from "../Iterable.js"; 3 | import { TextEditorElement } from "./TextEditorElement.js"; 4 | export const MakeGraphEditorElement = (NestedEditorConstructor = TextEditorElement, name = "custom") => { 5 | class GraphEditorElement extends EditorElement { 6 | meta = { 7 | editorName: name, 8 | }; 9 | nodes = []; 10 | styleEl; 11 | canvas; 12 | context; 13 | fromNode; 14 | mouse; 15 | constructor() { 16 | super(...arguments); 17 | this.style.setProperty("--editor-name", `'graph'`); 18 | this.style.setProperty("--editor-color", "#7300CF"); 19 | this.style.setProperty("--editor-name-color", "white"); 20 | this.style.setProperty("--editor-background-color", "#eed9ff"); 21 | this.style.setProperty("--editor-outline-color", "#b59dc9"); 22 | this.styleEl = document.createElement("style"); 23 | this.styleEl.textContent = ` 24 | :host { 25 | position: relative; 26 | height: 250px; 27 | width: 250px; 28 | } 29 | 30 | canvas { 31 | position: absolute; 32 | top: 0; 33 | left: 0; 34 | } 35 | `; 36 | this.canvas = document.createElement("canvas"); 37 | this.canvas.width = 250; 38 | this.canvas.height = 250; 39 | this.context = this.canvas.getContext("2d"); 40 | this.shadowRoot.append(this.styleEl, this.canvas); 41 | this.fromNode = null; 42 | this.mouse = [0, 0]; 43 | this.fromInput(arguments[0]); 44 | setTimeout(() => this.render()); 45 | this.addEventListener("keydown", (e) => { 46 | if (e.key === "Backspace" && this.parentEditor) { 47 | this.parentEditor.dispatchEvent(new CustomEvent("subEditorDeleted", { detail: this })); 48 | this.parentEditor?.dispatchEvent(new CustomEvent("childEditorUpdate", { 49 | detail: { 50 | out: this.getOutput(), 51 | editor: this, 52 | }, 53 | })); 54 | } 55 | else if (e.key === "ArrowLeft" && this.parentEditor) { 56 | this.blur(); 57 | this.parentEditor.focusEditor(this, 0, e.shiftKey); 58 | } 59 | else if (e.key === "ArrowRight" && this.parentEditor) { 60 | this.blur(); 61 | this.parentEditor.focusEditor(this, 1, e.shiftKey); 62 | } 63 | }); 64 | this.addEventListener("subEditorDeleted", (e) => { 65 | this.nodes = this.nodes.filter(({ editor }) => editor !== e.detail); 66 | for (const node of this.nodes) { 67 | node.adjacent = node.adjacent.filter(({ editor }) => editor !== e.detail); 68 | } 69 | this.shadowRoot.removeChild(e.detail); 70 | this.render(); 71 | this.focusEditor(); 72 | this.parentEditor?.dispatchEvent(new CustomEvent("childEditorUpdate", { 73 | detail: { 74 | out: this.getOutput(), 75 | editor: this, 76 | }, 77 | })); 78 | }); 79 | this.addEventListener("subEditorClicked", (e) => { 80 | const subFocus = this.nodes.find(({ editor }) => editor === e.detail[0]); 81 | if (subFocus) { 82 | this.fromNode = subFocus; // HACK 83 | } 84 | }); 85 | this.addEventListener("mousemove", (e) => { 86 | this.mouse = [e.offsetX, e.offsetY]; 87 | if (this.fromNode && e.metaKey) { 88 | this.fromNode.x = Math.max(0, Math.min(this.mouse[0] - 10, this.offsetWidth - this.fromNode.editor.offsetWidth - 2)); 89 | this.fromNode.y = Math.max(0, Math.min(this.mouse[1] - 10, this.offsetHeight - this.fromNode.editor.offsetHeight - 2)); 90 | } 91 | this.render(); 92 | }); 93 | this.addEventListener("mousedown", (e) => { 94 | const editor = new NestedEditorConstructor({ parentEditor: this }); 95 | editor.style.position = "absolute"; 96 | const node = { 97 | x: e.offsetX - 10, 98 | y: e.offsetY - 10, 99 | editor, 100 | adjacent: [], 101 | }; 102 | this.nodes.push(node); 103 | this.shadowRoot.append(editor); 104 | this.blur(); 105 | setTimeout(() => editor.focusEditor()); 106 | //this.fromNode = node; 107 | this.render(); 108 | this.parentEditor?.dispatchEvent(new CustomEvent("childEditorUpdate", { 109 | detail: { 110 | out: this.getOutput(), 111 | editor: this, 112 | }, 113 | })); 114 | }); 115 | this.addEventListener("mouseup", (e) => { 116 | const targetEl = e.composedPath()[0]; 117 | const targetNode = this.nodes.find(({ editor }) => editor.contains(targetEl) || editor.shadowRoot.contains(targetEl)); 118 | if (targetNode) { 119 | if (this.fromNode && 120 | this.fromNode !== targetNode && 121 | !this.fromNode.adjacent.includes(targetNode)) { 122 | this.fromNode.adjacent.push(targetNode); 123 | this.parentEditor?.dispatchEvent(new CustomEvent("childEditorUpdate", { 124 | detail: { 125 | out: this.getOutput(), 126 | editor: this, 127 | }, 128 | })); 129 | } 130 | } 131 | this.fromNode = null; 132 | this.render(); 133 | }); 134 | this.addEventListener("childEditorUpdate", (e) => { 135 | this.parentEditor?.dispatchEvent(new CustomEvent("childEditorUpdate", { 136 | detail: { 137 | out: this.getOutput(), 138 | editor: this, 139 | }, 140 | })); 141 | }); 142 | } 143 | render() { 144 | this.context.strokeStyle = "#7300CF"; 145 | this.context.fillStyle = "#7300CF"; 146 | this.context.lineWidth = 2; 147 | this.context.lineCap = "round"; 148 | this.canvas.width = this.offsetWidth; 149 | this.canvas.height = this.offsetHeight; 150 | if (this.fromNode) { 151 | this.context.beginPath(); 152 | this.context.moveTo(this.fromNode.x + this.fromNode.editor.offsetWidth / 2, this.fromNode.y + this.fromNode.editor.offsetHeight / 2); 153 | this.context.lineTo(...this.mouse); 154 | this.context.stroke(); 155 | } 156 | for (const { x, y, editor, adjacent } of this.nodes) { 157 | editor.style.top = `${y}px`; 158 | editor.style.left = `${x}px`; 159 | for (const otherNode of adjacent) { 160 | this.context.beginPath(); 161 | const start = [ 162 | x + editor.offsetWidth / 2, 163 | y + editor.offsetHeight / 2, 164 | ]; 165 | const end = [ 166 | otherNode.x + otherNode.editor.offsetWidth / 2, 167 | otherNode.y + otherNode.editor.offsetHeight / 2, 168 | ]; 169 | this.context.moveTo(...start); 170 | this.context.lineTo(...end); 171 | this.context.stroke(); 172 | const angle = Math.atan2(end[0] - start[0], end[1] - start[1]); 173 | const dir = [Math.sin(angle), Math.cos(angle)]; 174 | const dist = Math.min(otherNode.editor.offsetHeight * Math.abs(1 / Math.cos(angle)), otherNode.editor.offsetWidth * Math.abs(1 / Math.sin(angle))) / 2; // https://math.stackexchange.com/a/924290/421433 175 | this.context.beginPath(); 176 | this.context.moveTo(end[0] - dir[0] * dist, end[1] - dir[1] * dist); 177 | this.context.lineTo(end[0] - dir[0] * (dist + 11) + dir[1] * 7, end[1] - dir[1] * (dist + 11) - dir[0] * 7); 178 | this.context.stroke(); 179 | this.context.moveTo(end[0] - dir[0] * dist, end[1] - dir[1] * dist); 180 | this.context.lineTo(end[0] - dir[0] * (dist + 11) - dir[1] * 7, end[1] - dir[1] * (dist + 11) + dir[0] * 7); 181 | this.context.stroke(); 182 | } 183 | } 184 | } 185 | fromInput(input) { 186 | if (!input || !input.nodes || !input.edges || !input.positions) 187 | return; 188 | const { nodes, edges, positions } = input; 189 | for (let i = 0; i < nodes.length; i++) { 190 | const nodeValue = nodes[i]; 191 | const position = positions[i]; 192 | let editor; 193 | if (nodeValue instanceof EditorElement) { 194 | editor = new NestedEditorConstructor({ 195 | code: [nodeValue], 196 | parentEditor: this, 197 | }); 198 | } 199 | else { 200 | editor = new NestedEditorConstructor({ 201 | code: [String(nodeValue)], 202 | parentEditor: this, 203 | }); 204 | } 205 | editor.style.position = "absolute"; 206 | this.nodes[i] = { 207 | x: position[0], 208 | y: position[1], 209 | editor, 210 | adjacent: [], 211 | }; 212 | this.shadowRoot.append(editor); 213 | } 214 | for (let i = 0; i < nodes.length; i++) { 215 | const edgeList = edges[i]; 216 | for (const edgeIndex of edgeList) { 217 | this.nodes[i].adjacent.push(this.nodes[edgeIndex]); 218 | } 219 | } 220 | } 221 | getOutput() { 222 | let nodes = []; 223 | let positions = []; 224 | let edges = []; 225 | for (const [{ editor, x, y, adjacent }, i] of withIndex(this.nodes)) { 226 | nodes[i] = `"${editor.getOutput()}"`; 227 | positions[i] = [x, y]; 228 | edges[i] = []; 229 | for (const otherNode of adjacent) { 230 | const otherNodeIndex = this.nodes.findIndex((n) => n === otherNode); 231 | edges[i].push(otherNodeIndex); 232 | } 233 | } 234 | return `({ 235 | "nodes": [${nodes}], 236 | "edges": [${edges.map((edgeList) => `[${edgeList}]`)}], 237 | "positions": [${positions.map(([x, y]) => `[${x}, ${y}]`)}] 238 | })`; 239 | } 240 | } 241 | customElements.define(`graph-editor-${name}`, GraphEditorElement); 242 | return GraphEditorElement; 243 | }; 244 | -------------------------------------------------------------------------------- /lib/dist/editors/MusicStaffEditorElement.js: -------------------------------------------------------------------------------- 1 | import { ArrayEditorElement } from "./ArrayEditorElement.js"; 2 | const STAFF_BOTTOM_NOTE_Y = 20.5; 3 | const STAFF_LINE_HEIGHT = 5; 4 | const NOTES = [ 5 | "c4", 6 | "d4", 7 | "e4", 8 | "f4", 9 | "g4", 10 | "a4", 11 | "b4", 12 | "c5", 13 | "d5", 14 | "e5", 15 | "f5", 16 | "g5", 17 | "a5", 18 | " ", 19 | ]; 20 | const KEY_TO_NOTE = { 21 | a: "e4", 22 | s: "f4", 23 | d: "g4", 24 | f: "a4", 25 | g: "b4", 26 | h: "c5", 27 | j: "d5", 28 | k: "e5", 29 | l: "f5", 30 | ";": "g5", 31 | " ": " ", 32 | }; 33 | export class MusicStaffEditorElement extends ArrayEditorElement { 34 | meta = { 35 | editorName: "♫ Staff", 36 | }; 37 | styleEl2; 38 | getHighlightedOutput() { 39 | if (!this.isFocused || this.minorCaret === this.caret) 40 | return "[]"; 41 | const [start, end] = this.caretsOrdered(); 42 | return "(" + JSON.stringify(this.contents.slice(start, end)) + ")"; 43 | } 44 | getOutput() { 45 | return "(" + JSON.stringify(this.contents) + ")"; 46 | } 47 | processClipboardText(clipboardText) { 48 | return JSON.parse(clipboardText.substring(1, clipboardText.length - 1)); 49 | } 50 | constructor(arg) { 51 | super(arg); 52 | this.style.setProperty("--editor-color", "black"); 53 | this.style.setProperty("--editor-name-color", "white"); 54 | this.style.setProperty("--editor-background-color", "white"); 55 | this.style.setProperty("--editor-outline-color", "black"); 56 | this.styleEl2 = document.createElement("style"); 57 | this.styleEl2.textContent = ` 58 | :host { 59 | padding: 10px; 60 | } 61 | :host(:not(:focus-visible)) #caret { 62 | display: none; 63 | } 64 | #caret { 65 | animation: blinker 1s linear infinite; 66 | } 67 | @keyframes blinker { 68 | 0% { opacity: 1; } 69 | 49% { opacity: 1; } 70 | 50% { opacity: 0; } 71 | 100% { opacity: 0; } 72 | } 73 | `; 74 | this.shadowRoot.append(this.styleEl2); 75 | this.contentsEl.innerHTML = svgElText(); 76 | this.render(); 77 | } 78 | render() { 79 | const notesWrapperEl = this.shadowRoot.getElementById("notes"); 80 | if (!notesWrapperEl) 81 | return; 82 | notesWrapperEl.innerHTML = ""; 83 | let x = 25; 84 | let i = 0; 85 | for (const { note, length } of this.contents) { 86 | const y = STAFF_BOTTOM_NOTE_Y - (NOTES.indexOf(note) * STAFF_LINE_HEIGHT) / 2; 87 | if (note === " ") { 88 | notesWrapperEl.append(makeSVGEl("use", { href: "#quarter-rest", x, y: 11, i })); 89 | } 90 | else if (y < 10) { 91 | notesWrapperEl.append(makeSVGEl("use", { href: "#quarter-note-flipped", x, y: y + 10.5, i })); 92 | } 93 | else { 94 | notesWrapperEl.append(makeSVGEl("use", { href: "#quarter-note", x, y, i })); 95 | } 96 | if (i !== 0 && i % 4 === 0) { 97 | notesWrapperEl.append(makeSVGEl("use", { href: "#vertical-line", x: x - 3 })); 98 | } 99 | x += 14; 100 | i++; 101 | } 102 | this.shadowRoot.getElementById("svg").setAttribute("width", x * 2 + ""); 103 | this.shadowRoot 104 | .getElementById("svg") 105 | .setAttribute("viewBox", `0 0 ${x} 38`); 106 | notesWrapperEl.append(makeSVGEl("use", { href: "#caret", x: 19.5 + this.caret * 14, y: 5 })); 107 | const [start, end] = this.caretsOrdered(); 108 | if (start !== end) { 109 | const el = makeSVGEl("rect", { 110 | fill: "white", 111 | x: 22 + start * 14, 112 | y: 6, 113 | height: 26.5, 114 | width: (end - start) * 14 - 1, 115 | rx: 2, 116 | }); 117 | el.style.mixBlendMode = "difference"; 118 | notesWrapperEl.append(el); 119 | } 120 | } 121 | keyHandler(e) { 122 | if (KEY_TO_NOTE[e.key]) { 123 | this.insert([{ note: KEY_TO_NOTE[e.key], length: 1 }]); 124 | return true; 125 | } 126 | } 127 | } 128 | customElements.define("music-staff-editor", MusicStaffEditorElement); 129 | function makeSVGEl(name, props = {}) { 130 | const el = document.createElementNS("http://www.w3.org/2000/svg", name); 131 | for (const [key, value] of Object.entries(props)) 132 | el.setAttributeNS(null, key, value); 133 | return el; 134 | } 135 | function svgElText() { 136 | return ` 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | `; 174 | } 175 | -------------------------------------------------------------------------------- /lib/dist/editors/StringEditorElement.js: -------------------------------------------------------------------------------- 1 | import { withIndex } from "../Iterable.js"; 2 | import { TextEditorElement, } from "./TextEditorElement.js"; 3 | export class StringEditorElement extends TextEditorElement { 4 | meta = { 5 | editorName: "String", 6 | }; 7 | constructor(arg) { 8 | super(arg); 9 | this.style.setProperty("--editor-name", `'string'`); 10 | this.style.setProperty("--editor-color", "black"); 11 | this.style.setProperty("--editor-name-color", "white"); 12 | this.style.setProperty("--editor-background-color", "white"); 13 | this.style.setProperty("--editor-outline-color", "black"); 14 | } 15 | getOutput() { 16 | let output = '"'; 17 | for (const [slotOrChar, i] of withIndex(this.code)) { 18 | if (typeof slotOrChar === "string") { 19 | const char = slotOrChar; 20 | output += char; 21 | } 22 | else { 23 | const slot = slotOrChar; 24 | output += slot.getOutput(); 25 | } 26 | } 27 | return output + '"'; 28 | } 29 | } 30 | customElements.define("string-editor", StringEditorElement); 31 | -------------------------------------------------------------------------------- /lib/dist/editors/markdownEditors.js: -------------------------------------------------------------------------------- 1 | import { TextEditorElement } from "./TextEditorElement.js"; 2 | import { UnaryJoinerElement } from "./mathEditors.js"; 3 | export class MarkdownEditorElement extends TextEditorElement { 4 | meta = { 5 | editorName: "Markdown", 6 | }; 7 | constructor(arg) { 8 | super(arg); 9 | this.style.setProperty("--editor-name", `'markdown'`); 10 | this.style.setProperty("--editor-color", "#376e32"); 11 | this.style.setProperty("--editor-name-color", "#c1f2bd"); 12 | this.style.setProperty("--editor-background-color", "#c1f2bd"); 13 | this.style.setProperty("--editor-outline-color", "#376e32"); 14 | } 15 | keyHandler(e) { 16 | if (e.key === "-") { 17 | // no elevations 18 | const focuser = new BulletJoinerElement({ parentEditor: this }); 19 | this.code.splice(this.caret, 0, "\n", focuser); 20 | return focuser; 21 | } 22 | if (e.key === "#") { 23 | // no elevations 24 | const focuser = new HeaderElement({ parentEditor: this }); 25 | this.code.splice(this.caret, 0, focuser); 26 | return focuser; 27 | } 28 | } 29 | getOutput() { 30 | const sOut = super.getOutput(); 31 | return sOut 32 | .split("\n") 33 | .map((line) => "// " + line) 34 | .join("\n"); 35 | } 36 | } 37 | customElements.define("markdown-editor", MarkdownEditorElement); 38 | class HeaderElement extends TextEditorElement { 39 | meta = { 40 | editorName: "Markdown #", 41 | }; 42 | constructor(arg) { 43 | super(arg); 44 | this.style.setProperty("--editor-name", `'markdown'`); 45 | this.style.setProperty("--editor-color", "#376e32"); 46 | this.style.setProperty("--editor-name-color", "#c1f2bd"); 47 | this.style.setProperty("--editor-background-color", "#c1f2bd"); 48 | this.style.setProperty("--editor-outline-color", "#376e32"); 49 | this.style.setProperty("font-weight", "900"); 50 | this.style.setProperty("font-size", "40px"); 51 | this.style.setProperty("padding", "10px"); 52 | } 53 | keyHandler(e) { 54 | if (e.key === "Enter") { 55 | this.parentEditor.focusEditor(this, 1); 56 | this.parentEditor.code.splice(this.parentEditor.caret, 0, "\n"); 57 | this.parentEditor.focusEditor(this, 1); 58 | } 59 | return null; 60 | } 61 | getOutput() { 62 | return `# ${super.getOutput()}`; 63 | } 64 | } 65 | customElements.define("markdown-header-inner-editor", HeaderElement); 66 | class BulletJoinerInnerElement extends TextEditorElement { 67 | meta = { 68 | editorName: "Markdown -", 69 | }; 70 | constructor(arg) { 71 | super(arg); 72 | this.style.setProperty("--editor-name", `'markdown'`); 73 | this.style.setProperty("--editor-color", "#376e32"); 74 | this.style.setProperty("--editor-name-color", "#c1f2bd"); 75 | this.style.setProperty("--editor-background-color", "#c1f2bd"); 76 | this.style.setProperty("--editor-outline-color", "#376e32"); 77 | } 78 | keyHandler(e) { 79 | if (e.key === "Enter") { 80 | this.parentEditor.parentEditor.focusEditor(this.parentEditor, 1); 81 | const focuser = new BulletJoinerElement({ 82 | parentEditor: this.parentEditor.parentEditor, 83 | }); 84 | this.parentEditor.parentEditor.code.splice(this.parentEditor.parentEditor.caret, 0, "\n", focuser); 85 | this.parentEditor.parentEditor.focusEditor(this.parentEditor, 1); 86 | setTimeout(() => focuser.focusEditor(this.parentEditor.parentEditor, 1)); 87 | } 88 | return null; 89 | } 90 | } 91 | customElements.define("markdown-bullet-inner-editor", BulletJoinerInnerElement); 92 | export const BulletJoinerElement = class extends UnaryJoinerElement("bullet", BulletJoinerInnerElement, (editor) => { 93 | const styleEl = document.createElement("style"); 94 | styleEl.textContent = ` 95 | :host { 96 | display: inline-flex; 97 | align-items: stretch; 98 | vertical-align: middle; 99 | align-items: center; 100 | padding: 10px; 101 | gap: 5px; 102 | } 103 | span{ 104 | height: 6px; 105 | width: 6px; 106 | border-radius: 100%; 107 | background: black; 108 | } 109 | `; 110 | const bulletEl = document.createElement("span"); 111 | return [styleEl, bulletEl, editor]; 112 | }) { 113 | getOutput() { 114 | return `- ${this.editor.getOutput()}`; 115 | } 116 | }; 117 | customElements.define("markdown-bullet-editor", BulletJoinerElement); 118 | -------------------------------------------------------------------------------- /lib/dist/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vezwork/Polytope/6f4e7ccb66d68dca0b359014d8931a16b6144244/lib/dist/index.js -------------------------------------------------------------------------------- /lib/dist/math.js: -------------------------------------------------------------------------------- 1 | // Mini custom Vec2 library 2 | export const UP = [0, -1]; 3 | export const LEFT = [-1, 0]; 4 | export const DOWN = [0, 1]; 5 | export const RIGHT = [1, 0]; 6 | export const copy = (v) => [v[0], v[1]]; 7 | export const add = (v1, v2) => [v1[0] + v2[0], v1[1] + v2[1]]; 8 | export const sub = (v1, v2) => [v1[0] - v2[0], v1[1] - v2[1]]; 9 | export const mul = (n, v) => [n * v[0], n * v[1]]; 10 | export const dot = (v1, v2) => v1[0] * v2[0] + v1[1] * v2[1]; 11 | export const length = (v) => Math.sqrt(dot(v, v)); 12 | export const normalize = (v) => mul(1 / length(v), v); 13 | export const angleOf = (v) => Math.atan2(v[1], v[0]); 14 | export const angleBetween = (v1, v2) => angleOf(sub(v2, v1)); 15 | export const distance = (v1, v2) => length(sub(v1, v2)); 16 | export const round = (v) => [Math.round(v[0]), Math.round(v[1])]; 17 | // reference: https://en.wikipedia.org/wiki/Rotation_matrix 18 | export const rotate = (v, theta) => [ 19 | Math.cos(theta) * v[0] - Math.sin(theta) * v[1], 20 | Math.sin(theta) * v[0] + Math.cos(theta) * v[1], 21 | ]; 22 | export const normalVec2FromAngle = (theta) => [ 23 | Math.cos(theta), 24 | Math.sin(theta), 25 | ]; 26 | export const lerp = ([start, end], t) => add(start, mul(t, sub(end, start))); 27 | // reference: https://stackoverflow.com/a/6853926/5425899 28 | // StackOverflow answer license: CC BY-SA 4.0 29 | // Gives the shortest Vec2 from the point v to the line segment. 30 | export const subLineSegment = (v, [start, end]) => { 31 | const startToV = sub(v, start); 32 | const startToEnd = sub(end, start); 33 | const lengthSquared = dot(startToEnd, startToEnd); 34 | const parametrizedLinePos = lengthSquared === 0 35 | ? -1 36 | : Math.max(0, Math.min(1, dot(startToV, startToEnd) / lengthSquared)); 37 | const closestPointOnLine = lerp([start, end], parametrizedLinePos); 38 | return sub(v, closestPointOnLine); 39 | }; 40 | export const reflectAngle = (theta1, theta2) => theta2 + subAngles(theta1, theta2); 41 | export const subAngles = (theta1, theta2) => mod(theta2 - theta1 + Math.PI, Math.PI * 2) - Math.PI; 42 | export const mod = (a, n) => a - Math.floor(a / n) * n; 43 | export const smoothStep = (currentValue, targetValue, slowness) => currentValue - (currentValue - targetValue) / slowness; 44 | -------------------------------------------------------------------------------- /lib/dist/stringToEditorBuilder.js: -------------------------------------------------------------------------------- 1 | function concatInPlace(array, otherArray) { 2 | for (const entry of otherArray) 3 | array.push(entry); 4 | } 5 | export const createBuilder = (processors = []) => { 6 | const f = (string, hasOpenParen = false) => { 7 | let result = []; 8 | for (let i = 0; i < string.length; i++) { 9 | const char = string[i]; 10 | if (char === "(") { 11 | const { consume, output } = f(string.substring(i + 1), true); 12 | if (!processors.some((processor) => processor(output, result, string, i))) { 13 | result = [...result, "(", ...output, ")"]; 14 | } 15 | i = i + consume + 1; 16 | } 17 | else if (hasOpenParen && char === ")") { 18 | return { 19 | consume: i, 20 | output: result, 21 | }; 22 | } 23 | else { 24 | result = [...result, char]; 25 | } 26 | } 27 | return { 28 | consume: string.length, 29 | output: result, 30 | }; 31 | }; 32 | return f; 33 | }; 34 | const EDITOR_IDENTIFIER = "POLYTOPE$$STRING"; 35 | const EDITOR_IDENTIFIER_STRING = `"${EDITOR_IDENTIFIER}"`; 36 | export const createJSONProcessor = (editorFactory, validator) => (innerString, result) => { 37 | try { 38 | // Replace inner editors with a simple value so that JSON parse can parse 39 | // the innerString. 40 | let massagedInnerString = ""; 41 | let innerEditors = []; 42 | for (let i = 0; i < innerString.length; i++) { 43 | const slotOrChar = innerString[i]; 44 | if (typeof slotOrChar !== "string") { 45 | innerEditors.unshift(slotOrChar); 46 | massagedInnerString += EDITOR_IDENTIFIER_STRING; 47 | } 48 | else { 49 | const char = slotOrChar; 50 | massagedInnerString += char; 51 | } 52 | } 53 | const innerJSON = JSON.parse(massagedInnerString); 54 | const processedJSON = objectValueMap(innerJSON, (value) => value === EDITOR_IDENTIFIER ? innerEditors.pop() : value); 55 | if (!validator(processedJSON)) 56 | return false; 57 | const a = [editorFactory(processedJSON)]; 58 | console.log(processedJSON, validator(processedJSON), editorFactory, a); 59 | concatInPlace(result, a); 60 | return true; 61 | } 62 | catch (e) { 63 | console.error("JSONProcessor error", innerString, result, e); 64 | } 65 | return false; 66 | }; 67 | function objectValueMap(obj, f) { 68 | const newObj = Array.isArray(obj) ? [] : {}; 69 | for (const [key, value] of Object.entries(obj)) { 70 | if (value !== null && typeof value === "object") { 71 | newObj[key] = objectValueMap(value, f); 72 | } 73 | else { 74 | newObj[key] = f(value); 75 | } 76 | } 77 | return newObj; 78 | } 79 | export const createFunctionProcessor = ({ name, arity, ElementConstructor }) => (innerString, result, string, i) => { 80 | if (string.substring(i - name.length, i) === name) { 81 | // Only supports unary and binary ops for now. 82 | if (arity === 1) { 83 | const joiner = new ElementConstructor({ 84 | code: innerString, 85 | }); 86 | result.length -= name.length; 87 | concatInPlace(result, [joiner]); 88 | return true; 89 | } 90 | else if (arity === 2) { 91 | const commaIndex = innerString.indexOf(","); 92 | if (commaIndex !== -1) { 93 | const joiner = new ElementConstructor({ 94 | leftCode: innerString.slice(0, commaIndex), 95 | rightCode: innerString.slice(commaIndex + 2), 96 | }); 97 | result.length -= name.length; 98 | concatInPlace(result, [joiner]); 99 | return true; 100 | } 101 | } 102 | } 103 | return false; 104 | }; 105 | // const DELIMITER = '(`POLYTOPE`/*'; 106 | // const ED = ConstructiveUnidirectionalEditor({ 107 | // name: 'del-testing', 108 | // leftEditorFactory: (code, parentEditor) => new TextEditorElement({ 109 | // code, 110 | // parentEditor 111 | // }), 112 | // leftEditorOutput: (editor) => editor.getOutput(), 113 | // }); 114 | // const createTESTINGProcessor = () => (innerString, result, string, i) => { 115 | // console.log('DEUBGUS', string.substring(i, DELIMITER.length)); 116 | // if (string.substring(i, DELIMITER.length) === DELIMITER) { 117 | // const innerCode = innerString.slice(DELIMITER.length, -2); 118 | // console.log('DEBUG INNER', innerString.slice(DELIMITER.length, -2)); 119 | // const editor = new ED({ leftCode: innerCode }) 120 | // concatInPlace(result, [editor]); 121 | // return true; 122 | // } 123 | // return false; 124 | // } 125 | -------------------------------------------------------------------------------- /lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brilliant-light", 3 | "version": "1.0.0", 4 | "description": "An interactive exploration of how light reflects", 5 | "scripts": { 6 | "dev": "npx webpack --watch", 7 | "build": "tsc --watch" 8 | }, 9 | "devDependencies": { 10 | "ts-loader": "^9.2.6", 11 | "typescript": "^4.5.5", 12 | "webpack": "^5.69.1", 13 | "webpack-cli": "^4.9.2" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/Iterable.ts: -------------------------------------------------------------------------------- 1 | export const historyArray = function* (number, iterable) { 2 | let history = Array(number); 3 | for (const result of iterable) { 4 | history = [result, ...history]; 5 | history.length = number; 6 | yield history; 7 | } 8 | }; 9 | export const range = function* (start, end, step = 1) { 10 | for (let n = start; n < end; n += step) { 11 | yield n; 12 | } 13 | }; 14 | export const take = function* (number, iterable) { 15 | let i = 0; 16 | for (const item of iterable) { 17 | i++; 18 | if (i > number) break; 19 | yield item; 20 | } 21 | }; 22 | export const first = (iterable) => Array.from(take(1, iterable))[0]; 23 | export const skip = function* (number, iterable) { 24 | let i = 0; 25 | for (const item of iterable) { 26 | i++; 27 | if (i <= number) continue; 28 | yield item; 29 | } 30 | }; 31 | export const map = function* (iterable, func) { 32 | for (const item of iterable) yield func(item); 33 | }; 34 | export const some = function (iterable, func) { 35 | for (const item of iterable) { 36 | if (func(item)) return true; 37 | } 38 | return false; 39 | }; 40 | export const withIndex = function* (iterable) { 41 | let i = 0; 42 | for (const item of iterable) { 43 | yield [item, i]; 44 | i++; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/editor.ts: -------------------------------------------------------------------------------- 1 | export type EditorArgumentObject = { 2 | parentEditor?: EditorElement; 3 | builder?: (input: string) => { output: EditorElement[] }; 4 | }; 5 | 6 | export class EditorElement extends HTMLElement { 7 | meta?: { 8 | editorName?: string; 9 | isUnstyled?: boolean; 10 | }; 11 | parentEditor?: EditorElement = undefined; 12 | builder?: (input: string) => { output: EditorElement[] }; 13 | isFocused = false; 14 | 15 | constructor( 16 | { parentEditor, builder }: EditorArgumentObject = { 17 | parentEditor: undefined, 18 | } 19 | ) { 20 | super(); 21 | 22 | this.builder = builder; 23 | 24 | this.parentEditor = parentEditor; 25 | 26 | this.attachShadow({ mode: "open" }); 27 | 28 | const paletteEl = document.createElement("div"); 29 | paletteEl.className = "palette"; 30 | 31 | // EXPERIMENTAL CODE 32 | const butEl = document.createElement("span"); 33 | butEl.className = "but"; 34 | butEl.innerText = "↑eval↑"; 35 | butEl.addEventListener("click", async () => { 36 | const [{ ConstructiveUnidirectionalEditor }, { TextEditorElement }] = 37 | await Promise.all([ 38 | import("./editors/bidirectional_editor_pair.js"), 39 | import("./editors/TextEditorElement.js"), 40 | ]); 41 | 42 | const LiftedEval = ConstructiveUnidirectionalEditor({ 43 | leftEditorFactory: (a, me) => 44 | new TextEditorElement({ parentEditor: me, code: [this] }), 45 | leftEditorOutput: (editor) => editor.getOutput(), 46 | name: Math.random().toFixed(4).toString(), 47 | }); 48 | 49 | this.parentEditor?.dispatchEvent( 50 | new CustomEvent("subEditorReplaced", { 51 | detail: { 52 | old: this, 53 | new: new LiftedEval({ 54 | parentEditor: this.parentEditor, 55 | builder: this.builder, 56 | }), 57 | }, 58 | }) 59 | ); 60 | }); 61 | // EXPERIMENTAL CODE END 62 | 63 | const styleEl = document.createElement("style"); 64 | styleEl.textContent = ` 65 | :host { 66 | --editor-name: 'editor'; 67 | --editor-color: #017BFF; 68 | --editor-name-color: white; 69 | --editor-background-color: #E6F2FF; 70 | --editor-outline-color: #d4e9ff; 71 | 72 | // unused: 73 | --highlight-text-color: black; 74 | --highlight-editor-color: yellow; 75 | --highlight-editor-name-color: black; 76 | --highlight-editor-background-color: yellow; 77 | 78 | display: inline-flex; 79 | justify-content: center; 80 | 81 | vertical-align: middle; 82 | 83 | user-select: none; 84 | border-radius: 4px; 85 | background: var(--editor-background-color); 86 | border: 2px solid var(--editor-outline-color); 87 | box-sizing: border-box; 88 | position: relative; 89 | font-family: monospace; 90 | 91 | line-height: 1; 92 | 93 | min-height: 1.6rem; 94 | min-width: 0.5rem; 95 | } 96 | .but { 97 | display: inline-block; 98 | cursor: pointer; 99 | opacity: 0.8; 100 | padding: 2px; 101 | margin: 1px 1px 1px 6px; 102 | background: var(--editor-name-color); 103 | color: var(--editor-color); 104 | border-radius: 0 0 3px 3px; 105 | } 106 | .but:hover { 107 | opacity: 1; 108 | } 109 | .palette { 110 | display: none; 111 | } 112 | :host(:focus) .palette { 113 | display: block; 114 | font-size: 14px; 115 | padding: 1px 2px 2px 8px; 116 | background: var(--editor-color); 117 | color: var(--editor-name-color); 118 | position: absolute; 119 | bottom: -24px; 120 | left: -2px; 121 | border-radius: 0 0 4px 4px; 122 | font-family: monospace; 123 | z-index: 10; 124 | } 125 | :host(:focus) { 126 | border: 2px solid var(--editor-color); 127 | color: black !important; 128 | outline: none; 129 | } 130 | :host(:not(:focus)) { 131 | color: rgba(0,0,0,0.5); 132 | } 133 | `; 134 | 135 | setTimeout(() => { 136 | paletteEl.innerText = this.meta?.editorName ?? "editor"; 137 | if (!this.meta?.isUnstyled) this.shadowRoot.append(styleEl, paletteEl); 138 | paletteEl.append(butEl); 139 | }); 140 | 141 | if (!this.hasAttribute("tabindex")) this.setAttribute("tabindex", "0"); 142 | 143 | this.addEventListener("focus", (e) => { 144 | e.stopPropagation(); 145 | e.preventDefault(); 146 | this.isFocused = true; 147 | }); 148 | this.addEventListener("subEditorClicked", (e: CustomEvent) => { 149 | this.parentEditor?.dispatchEvent( 150 | new CustomEvent("subEditorClicked", { detail: [this, ...e.detail] }) 151 | ); 152 | }); 153 | this.addEventListener("blur", (e) => { 154 | e.stopPropagation(); 155 | this.isFocused = false; 156 | }); 157 | this.addEventListener("mousedown", (e) => { 158 | e.stopPropagation(); 159 | this.parentEditor?.dispatchEvent( 160 | new CustomEvent("subEditorClicked", { detail: [this] }) 161 | ); 162 | this.focus(); 163 | this.isFocused = true; 164 | }); 165 | this.addEventListener("keydown", (e) => e.stopPropagation()); 166 | } 167 | 168 | get javaScriptCode() { 169 | return ""; 170 | } 171 | 172 | focusEditor( 173 | fromEl?: HTMLElement, 174 | position?: 1 | 0 | -1 | undefined, 175 | isSelecting?: boolean 176 | ): void { 177 | this.focus({ preventScroll: true }); 178 | } 179 | 180 | getOutput() { 181 | return ""; 182 | } 183 | } 184 | 185 | // editorDescription: [{ 186 | // name: string; 187 | // description: string; 188 | // iconPath: string; 189 | // ElementConstructor: HTMLElement; 190 | // }] 191 | 192 | customElements.define("polytope-editor", EditorElement); 193 | -------------------------------------------------------------------------------- /lib/src/editors/ArrayEditorElement.ts: -------------------------------------------------------------------------------- 1 | import { withIndex } from "../Iterable.js"; 2 | import { EditorArgumentObject, EditorElement } from "../editor.js"; 3 | 4 | export type ArrayEditorElementArguments = EditorArgumentObject & { 5 | contents?: Array; 6 | }; 7 | 8 | export class ArrayEditorElement extends EditorElement { 9 | meta = { 10 | editorName: "Array", 11 | }; 12 | contents: Array = []; 13 | caret = 0; 14 | minorCaret = 0; 15 | 16 | styleEl: HTMLStyleElement; 17 | contentsEl: HTMLElement; 18 | 19 | keyHandler(e: KeyboardEvent): boolean { 20 | return false; // override me! 21 | } 22 | 23 | processClipboardText(clipboardText: string): Array { 24 | return []; // override me! 25 | } 26 | 27 | onCaretMoveOverContentItem(contentItems: Array) { 28 | // override me! 29 | } 30 | 31 | getContentItemOutput(item: T) { 32 | return item.toString(); // override me! 33 | } 34 | 35 | cursorPosFromMouseEvent(e: MouseEvent) { 36 | const cand = Array.from(this.shadowRoot.querySelectorAll("*[i]")) 37 | .map((childEl) => ({ 38 | el: childEl, 39 | rect: childEl.getBoundingClientRect(), 40 | })) 41 | .sort( 42 | (childA, childB) => 43 | Math.abs(e.clientX - childA.rect.right) - 44 | Math.abs(Math.abs(e.clientX - childB.rect.right)) 45 | )[0]; 46 | 47 | const tryAttribute = cand?.el.getAttribute("i"); 48 | if (tryAttribute) { 49 | let chari = parseInt(tryAttribute); 50 | const x = e.clientX - cand.rect.left; //x position offset from the left of the element 51 | if (x >= cand.rect.width / 2) { 52 | chari = chari + 1; 53 | } 54 | return chari; 55 | } 56 | return this.contents.length - 1; 57 | } 58 | 59 | focusEditor( 60 | fromEl?: HTMLElement, 61 | position?: 1 | 0 | undefined, 62 | isSelecting?: boolean 63 | ) { 64 | super.focusEditor(); 65 | 66 | if (fromEl !== undefined && position !== undefined) { 67 | if (position === 1) { 68 | // case: entering from a parent on the left 69 | this.caret = 0; 70 | this.minorCaret = this.caret; 71 | } 72 | if (position === 0) { 73 | // case: entering from a parent on the right 74 | this.caret = this.contents.length; 75 | this.minorCaret = this.caret; 76 | } 77 | this.render(); 78 | } 79 | } 80 | 81 | constructor({ contents }: ArrayEditorElementArguments = {}) { 82 | super(...arguments); 83 | 84 | if (Array.isArray(contents)) { 85 | // hmm: should actually parse? 86 | this.contents = contents; 87 | } 88 | 89 | this.style.setProperty("--editor-name", `'text'`); 90 | this.style.setProperty("--editor-color", "#017BFF"); 91 | this.style.setProperty("--editor-name-color", "white"); 92 | this.style.setProperty("--editor-background-color", "#E6F2FF"); 93 | this.style.setProperty("--editor-outline-color", "#d4e9ff"); 94 | 95 | this.styleEl = document.createElement("style"); 96 | this.styleEl.textContent = ` 97 | contents { 98 | white-space: pre; 99 | width: inherit; 100 | height: inherit; 101 | display: inline-block; 102 | } 103 | :host(:not(:focus-visible)) code caret { 104 | display: none; 105 | } 106 | caret { 107 | position: relative; 108 | display: inline-block; 109 | height: 1rem; 110 | width: 0px; 111 | } 112 | caret::after { 113 | content: ""; 114 | height: 1.2rem; 115 | left: -1px; 116 | width: 2px; 117 | position: absolute; 118 | background: black; 119 | animation: blinker 1s linear infinite; 120 | } 121 | @keyframes blinker { 122 | 0% { opacity: 1; } 123 | 49% { opacity: 1; } 124 | 50% { opacity: 0; } 125 | 100% { opacity: 0; } 126 | } 127 | `; 128 | this.contentsEl = document.createElement("contents"); 129 | this.shadowRoot.append(this.styleEl, this.contentsEl); 130 | 131 | this.addEventListener("blur", (e) => { 132 | this.moveCaret(0, false); 133 | }); 134 | 135 | this.addEventListener("mousedown", (e) => { 136 | e.stopPropagation(); 137 | if (e.buttons === 1) { 138 | const pos = this.cursorPosFromMouseEvent(e); 139 | this.caret = pos; 140 | this.minorCaret = pos; 141 | this.render(); 142 | 143 | setTimeout(() => this.focusEditor()); 144 | } 145 | }); 146 | this.addEventListener("mousemove", (e) => { 147 | if (this.isFocused) { 148 | e.stopPropagation(); 149 | if (e.buttons === 1) { 150 | const pos = this.cursorPosFromMouseEvent(e); 151 | this.caret = pos; 152 | this.render(); 153 | } 154 | } 155 | }); 156 | 157 | const copy = (e) => { 158 | if (this.isFocused && this.minorCaret !== this.caret) { 159 | const output = this.getHighlightedOutput(); 160 | 161 | e.clipboardData.setData("text/plain", output); 162 | e.preventDefault(); 163 | console.log("copied!", output); 164 | if (e.type === "cut") { 165 | this.backspace(); 166 | } 167 | } 168 | }; 169 | document.addEventListener("copy", copy); 170 | document.addEventListener("cut", copy); 171 | document.addEventListener("paste", (e: ClipboardEvent) => { 172 | let pasteText = e.clipboardData.getData("text"); 173 | 174 | if (pasteText && this.isFocused) { 175 | e.preventDefault(); 176 | const processed = this.processClipboardText(pasteText); 177 | this.insert(processed); 178 | } 179 | }); 180 | 181 | this.addEventListener("childEditorUpdate", (e) => { 182 | this.parentEditor?.dispatchEvent( 183 | new CustomEvent("childEditorUpdate", { 184 | detail: { 185 | out: this.getOutput(), 186 | editor: this, 187 | }, 188 | }) 189 | ); 190 | }); 191 | this.render(); 192 | 193 | this.addEventListener("keydown", (e) => { 194 | if (e.key === " ") { 195 | e.preventDefault(); 196 | } 197 | if (e.ctrlKey || e.metaKey) { 198 | // TODO: modifier is down 199 | return; 200 | } 201 | 202 | if (e.composedPath().includes(this)) { 203 | if (this.keyHandler?.(e)) return; 204 | } 205 | 206 | if (!e.composedPath().includes(this)) { 207 | } else if (e.key === "Backspace") { 208 | if (this.parentEditor && this.contents.length === 0) { 209 | this.parentEditor.dispatchEvent( 210 | new CustomEvent("subEditorDeleted", { detail: this }) 211 | ); 212 | } 213 | this.backspace(); 214 | } else if (e.key === "ArrowLeft") { 215 | if (this.moveCaret(-1, e.shiftKey)) { 216 | this.parentEditor.focus(); 217 | this.blur(); 218 | this.parentEditor.focusEditor(this, 0, e.shiftKey); 219 | } 220 | this.render(); 221 | } else if (e.key === "ArrowRight") { 222 | if (this.moveCaret(1, e.shiftKey)) { 223 | this.parentEditor.focus(); 224 | this.blur(); 225 | this.parentEditor.focusEditor(this, 1, e.shiftKey); 226 | } 227 | this.render(); 228 | } 229 | }); 230 | } 231 | 232 | caretsOrdered(): [number, number] { 233 | let start: number; 234 | let end: number; 235 | if (this.minorCaret > this.caret) { 236 | start = this.caret; 237 | end = this.minorCaret; 238 | } else { 239 | end = this.caret; 240 | start = this.minorCaret; 241 | } 242 | return [start, end]; 243 | } 244 | 245 | insert(arr: Array) { 246 | const [start, end] = this.caretsOrdered(); 247 | 248 | if (start === end) { 249 | this.contents.splice(this.caret, 0, ...arr); 250 | this.moveCaret(arr.length); 251 | } else { 252 | this.contents.splice(start, end, ...arr); 253 | } 254 | 255 | this.render(); 256 | 257 | this.parentEditor?.dispatchEvent( 258 | new CustomEvent("childEditorUpdate", { 259 | detail: { 260 | out: this.getOutput(), 261 | editor: this, 262 | }, 263 | }) 264 | ); 265 | } 266 | 267 | backspace() { 268 | const [startCaret, endCaret] = this.caretsOrdered(); 269 | 270 | if (this.minorCaret === this.caret) { 271 | this.contents.splice(startCaret - 1, 1); 272 | this.moveCaret(-1, false); 273 | } else { 274 | this.contents.splice(startCaret, endCaret - startCaret); 275 | this.setCaret(startCaret); 276 | } 277 | 278 | this.render(); 279 | 280 | this.parentEditor?.dispatchEvent( 281 | new CustomEvent("childEditorUpdate", { 282 | detail: { 283 | out: this.getOutput(), 284 | editor: this, 285 | }, 286 | }) 287 | ); 288 | } 289 | 290 | setCaret(position: number) { 291 | this.caret = Math.max(0, Math.min(position, this.contents.length)); 292 | this.minorCaret = this.caret; 293 | 294 | this.render(); 295 | } 296 | 297 | moveCaret(change: number, isSelecting?: boolean): boolean { 298 | if (this.caret + change > this.contents.length || this.caret + change < 0) { 299 | return true; 300 | } 301 | 302 | const newCaret = this.caret + change; 303 | 304 | if (isSelecting) { 305 | this.caret = newCaret; 306 | } else { 307 | if (this.caret < newCaret) { 308 | this.onCaretMoveOverContentItem( 309 | this.contents.slice(this.caret, newCaret) 310 | ); 311 | } else { 312 | this.onCaretMoveOverContentItem( 313 | this.contents.slice(newCaret, this.caret) 314 | ); 315 | } 316 | this.caret = newCaret; 317 | this.minorCaret = newCaret; 318 | } 319 | 320 | this.render(); 321 | 322 | return false; 323 | } 324 | 325 | isIndexInSelection(i: number) { 326 | return ( 327 | (i < this.caret && i >= this.minorCaret) || 328 | (i >= this.caret && i < this.minorCaret) 329 | ); 330 | } 331 | 332 | render() { 333 | this.contentsEl.innerHTML = ""; 334 | 335 | let results = []; 336 | 337 | for (const [contentItem, i] of withIndex(this.contents)) { 338 | if (i === this.caret) { 339 | results.push(document.createElement("caret")); 340 | } 341 | const contentEl = document.createElement("span"); 342 | contentEl.textContent = contentItem.toString(); 343 | contentEl.setAttribute("i", i); 344 | if ( 345 | (i < this.caret && i >= this.minorCaret) || 346 | (i >= this.caret && i < this.minorCaret) 347 | ) { 348 | contentEl.style.background = "black"; 349 | contentEl.style.color = "white"; 350 | } 351 | results.push(contentEl); 352 | } 353 | if (this.caret === this.contents.length) { 354 | results.push(document.createElement("caret")); 355 | } 356 | this.contentsEl.append(...results); 357 | } 358 | 359 | blur() { 360 | super.blur(); 361 | 362 | this.moveCaret(0, false); // unselect 363 | this.render(); 364 | } 365 | 366 | getHighlightedOutput() { 367 | if (!this.isFocused || this.minorCaret === this.caret) return ""; 368 | 369 | const [start, end] = this.caretsOrdered(); 370 | 371 | let output = ""; 372 | for (let i = start; i < end; i++) { 373 | output += this.getContentItemOutput(this.contents[i]); 374 | } 375 | return output + ""; 376 | } 377 | 378 | getOutput() { 379 | let output = ""; 380 | for (const contentItem of this.contents) { 381 | output += this.getContentItemOutput(contentItem); 382 | } 383 | return output + ""; 384 | } 385 | } 386 | customElements.define("array-editor", ArrayEditorElement); 387 | -------------------------------------------------------------------------------- /lib/src/editors/CharArrayEditorElement.ts: -------------------------------------------------------------------------------- 1 | import { ArrayEditorElement } from "./ArrayEditorElement.js"; 2 | 3 | export class CharArrayEditorElement extends ArrayEditorElement { 4 | meta = { 5 | editorName: "Char Array", 6 | }; 7 | 8 | onCaretMoveOverContentItem(contentItems: Array) { 9 | //console.log(contentItems); 10 | } 11 | 12 | keyHandler(e: KeyboardEvent): boolean { 13 | if (e.key.length === 1) { 14 | this.insert([e.key]); 15 | this.render(); 16 | return true; 17 | } 18 | return false; // override me! 19 | } 20 | 21 | processClipboardText(clipboardText: string): Array { 22 | return clipboardText.split(""); // override me! 23 | } 24 | } 25 | customElements.define("char-array-editor", CharArrayEditorElement); 26 | -------------------------------------------------------------------------------- /lib/src/editors/ColoredGraphEditorElement.ts: -------------------------------------------------------------------------------- 1 | import { EditorElement } from "../editor.js"; 2 | import { withIndex } from "../Iterable.js"; 3 | import { StringEditorElement } from "./StringEditorElement.js"; 4 | 5 | type EditorNode = { 6 | x: number; 7 | y: number; 8 | editor: EditorElement; 9 | adjacent: { 10 | 0: number[]; 11 | 1: number[]; 12 | 2: number[]; 13 | 3: number[]; 14 | }; 15 | }; 16 | 17 | const GRAPH_COLORS = ["red", "blue", "green", "purple"]; 18 | export class ColoredGraphEditorElement extends EditorElement { 19 | meta = { 20 | editorName: "ColoredGraphEditorElement", 21 | }; 22 | nodes: Array = []; 23 | currentColor = 0; 24 | styleEl: HTMLStyleElement; 25 | canvas: HTMLCanvasElement; 26 | context: CanvasRenderingContext2D; 27 | fromNode: EditorNode; 28 | mouse: [number, number]; 29 | 30 | constructor() { 31 | super(...arguments); 32 | this.style.setProperty("--editor-name", `'graph'`); 33 | this.style.setProperty("--editor-color", GRAPH_COLORS[0]); 34 | this.style.setProperty("--editor-name-color", "white"); 35 | this.style.setProperty("--editor-background-color", "white"); 36 | this.style.setProperty("--editor-outline-color", "black"); 37 | 38 | this.styleEl = document.createElement("style"); 39 | this.styleEl.textContent = ` 40 | :host { 41 | position: relative; 42 | height: 250px; 43 | width: 250px; 44 | } 45 | 46 | canvas { 47 | position: absolute; 48 | top: 0; 49 | left: 0; 50 | } 51 | `; 52 | this.canvas = document.createElement("canvas"); 53 | this.canvas.width = 250; 54 | this.canvas.height = 250; 55 | this.context = this.canvas.getContext("2d"); 56 | this.shadowRoot.append(this.styleEl, this.canvas); 57 | 58 | this.fromNode = null; 59 | this.mouse = [0, 0]; 60 | 61 | this.fromInput(arguments[0]); 62 | setTimeout(() => this.render()); 63 | 64 | this.addEventListener("keydown", (e: KeyboardEvent) => { 65 | if (e.key === "Backspace" && this.parentEditor) { 66 | this.parentEditor.dispatchEvent( 67 | new CustomEvent("subEditorDeleted", { detail: this }) 68 | ); 69 | this.blur(); 70 | this.parentEditor.focusEditor(); 71 | } else if (e.key === "ArrowLeft" && this.parentEditor) { 72 | this.blur(); 73 | this.parentEditor.focusEditor(this, 0, e.shiftKey); 74 | } else if (e.key === "ArrowRight" && this.parentEditor) { 75 | this.blur(); 76 | this.parentEditor.focusEditor(this, 1, e.shiftKey); 77 | } else if (e.key === "Meta") { 78 | this.currentColor = (this.currentColor + 1) % GRAPH_COLORS.length; 79 | this.style.setProperty( 80 | "--editor-color", 81 | GRAPH_COLORS[this.currentColor] 82 | ); 83 | } 84 | }); 85 | this.addEventListener("subEditorDeleted", (e: CustomEvent) => { 86 | this.nodes = this.nodes.filter(({ editor }) => editor !== e.detail); 87 | for (const node of this.nodes) { 88 | for (let i = 0; i < GRAPH_COLORS.length; i++) { 89 | node.adjacent[i] = node.adjacent[i].filter( 90 | ({ editor }) => editor !== e.detail 91 | ); 92 | } 93 | } 94 | this.shadowRoot.removeChild(e.detail); 95 | this.render(); 96 | this.focusEditor(); 97 | this.parentEditor?.dispatchEvent( 98 | new CustomEvent("childEditorUpdate", { 99 | detail: { 100 | out: this.getOutput(), 101 | editor: this, 102 | }, 103 | }) 104 | ); 105 | }); 106 | this.addEventListener("subEditorClicked", (e: CustomEvent) => { 107 | const subFocus = this.nodes.find(({ editor }) => editor === e.detail[0]); 108 | if (subFocus) { 109 | this.fromNode = subFocus; // HACK 110 | } 111 | }); 112 | this.addEventListener("mousemove", (e) => { 113 | this.mouse = [e.offsetX, e.offsetY]; 114 | if (this.fromNode && e.metaKey) { 115 | this.fromNode.x = Math.max( 116 | 0, 117 | Math.min( 118 | this.mouse[0] - 10, 119 | this.offsetWidth - this.fromNode.editor.offsetWidth - 2 120 | ) 121 | ); 122 | this.fromNode.y = Math.max( 123 | 0, 124 | Math.min( 125 | this.mouse[1] - 10, 126 | this.offsetHeight - this.fromNode.editor.offsetHeight - 2 127 | ) 128 | ); 129 | } 130 | this.render(); 131 | }); 132 | this.addEventListener("mousedown", (e: MouseEvent) => { 133 | const editor = new StringEditorElement({ 134 | parentEditor: this, 135 | code: String(this.nodes.length).split(""), 136 | }); 137 | editor.style.position = "absolute"; 138 | const node = { 139 | x: e.offsetX - 10, 140 | y: e.offsetY - 10, 141 | editor, 142 | adjacent: { 143 | 0: [], 144 | 1: [], 145 | 2: [], 146 | 3: [], 147 | }, 148 | }; 149 | this.nodes.push(node); 150 | this.shadowRoot.append(editor); 151 | this.blur(); 152 | setTimeout(() => editor.focusEditor()); 153 | //this.fromNode = node; 154 | this.render(); 155 | this.parentEditor?.dispatchEvent( 156 | new CustomEvent("childEditorUpdate", { 157 | detail: { 158 | out: this.getOutput(), 159 | editor: this, 160 | }, 161 | }) 162 | ); 163 | }); 164 | this.addEventListener("mouseup", (e: MouseEvent) => { 165 | const targetEl = (e as MouseEvent).composedPath()[0] as Node; 166 | const targetNode = this.nodes.find( 167 | ({ editor }) => 168 | editor.contains(targetEl) || editor.shadowRoot.contains(targetEl) 169 | ); 170 | if (targetNode) { 171 | if ( 172 | this.fromNode && 173 | this.fromNode !== targetNode && 174 | !this.fromNode.adjacent[this.currentColor].includes(targetNode) 175 | ) { 176 | this.fromNode.adjacent[this.currentColor].push(targetNode); 177 | } 178 | } 179 | this.fromNode = null; 180 | this.render(); 181 | this.parentEditor?.dispatchEvent( 182 | new CustomEvent("childEditorUpdate", { 183 | detail: { 184 | out: this.getOutput(), 185 | editor: this, 186 | }, 187 | }) 188 | ); 189 | }); 190 | this.addEventListener("childEditorUpdate", (e: CustomEvent) => { 191 | this.parentEditor?.dispatchEvent( 192 | new CustomEvent("childEditorUpdate", { 193 | detail: { 194 | out: this.getOutput(), 195 | editor: this, 196 | }, 197 | }) 198 | ); 199 | }); 200 | } 201 | 202 | render() { 203 | this.context.lineWidth = 2; 204 | this.context.lineCap = "round"; 205 | this.canvas.width = this.offsetWidth; 206 | this.canvas.height = this.offsetHeight; 207 | if (this.fromNode) { 208 | this.context.strokeStyle = GRAPH_COLORS[this.currentColor]; 209 | this.context.beginPath(); 210 | this.context.moveTo( 211 | this.fromNode.x + this.fromNode.editor.offsetWidth / 2, 212 | this.fromNode.y + this.fromNode.editor.offsetHeight / 2 213 | ); 214 | this.context.lineTo(...this.mouse); 215 | this.context.stroke(); 216 | } 217 | for (const { x, y, editor, adjacent } of this.nodes) { 218 | editor.style.top = `${y}px`; 219 | editor.style.left = `${x}px`; 220 | 221 | const drawConnections = (otherNodes) => { 222 | for (const otherNode of otherNodes) { 223 | this.context.beginPath(); 224 | const start: [number, number] = [ 225 | x + editor.offsetWidth / 2, 226 | y + editor.offsetHeight / 2, 227 | ]; 228 | const end: [number, number] = [ 229 | otherNode.x + otherNode.editor.offsetWidth / 2, 230 | otherNode.y + otherNode.editor.offsetHeight / 2, 231 | ]; 232 | this.context.moveTo(...start); 233 | this.context.lineTo(...end); 234 | this.context.stroke(); 235 | 236 | const angle = Math.atan2(end[0] - start[0], end[1] - start[1]); 237 | const dir = [Math.sin(angle), Math.cos(angle)]; 238 | const dist = 239 | Math.min( 240 | otherNode.editor.offsetHeight * Math.abs(1 / Math.cos(angle)), 241 | otherNode.editor.offsetWidth * Math.abs(1 / Math.sin(angle)) 242 | ) / 2; // https://math.stackexchange.com/a/924290/421433 243 | this.context.beginPath(); 244 | this.context.moveTo(end[0] - dir[0] * dist, end[1] - dir[1] * dist); 245 | this.context.lineTo( 246 | end[0] - dir[0] * (dist + 11) + dir[1] * 7, 247 | end[1] - dir[1] * (dist + 11) - dir[0] * 7 248 | ); 249 | this.context.stroke(); 250 | this.context.moveTo(end[0] - dir[0] * dist, end[1] - dir[1] * dist); 251 | this.context.lineTo( 252 | end[0] - dir[0] * (dist + 11) - dir[1] * 7, 253 | end[1] - dir[1] * (dist + 11) + dir[0] * 7 254 | ); 255 | this.context.stroke(); 256 | } 257 | }; 258 | for (let i = 0; i < GRAPH_COLORS.length; i++) { 259 | const color = GRAPH_COLORS[i]; 260 | this.context.strokeStyle = color; 261 | drawConnections(adjacent[i]); 262 | } 263 | } 264 | } 265 | 266 | fromInput(input: { nodes; edges; positions }) { 267 | if (!input || !input.nodes || !input.edges || !input.positions) return; 268 | const { nodes, edges, positions } = input; 269 | 270 | for (let i = 0; i < nodes.length; i++) { 271 | const nodeValue = nodes[i]; 272 | const position = positions[i]; 273 | 274 | let editor; 275 | if (nodeValue instanceof EditorElement) { 276 | editor = new StringEditorElement({ 277 | code: [nodeValue], 278 | parentEditor: this, 279 | }); 280 | } else { 281 | editor = new StringEditorElement({ 282 | code: [String(nodeValue)], 283 | parentEditor: this, 284 | }); 285 | } 286 | editor.style.position = "absolute"; 287 | 288 | this.nodes[i] = { 289 | x: position[0], 290 | y: position[1], 291 | editor, 292 | adjacent: { 293 | 0: [], 294 | 1: [], 295 | 2: [], 296 | 3: [], 297 | }, 298 | }; 299 | this.shadowRoot.append(editor); 300 | } 301 | for (let j = 0; j < GRAPH_COLORS.length; j++) { 302 | for (let i = 0; i < nodes.length; i++) { 303 | const edgeList = edges[j][i]; 304 | 305 | for (const edgeIndex of edgeList) { 306 | this.nodes[i].adjacent[j].push(this.nodes[edgeIndex]); 307 | } 308 | } 309 | } 310 | } 311 | 312 | getJSONOutput() { 313 | let nodes = []; 314 | let positions = []; 315 | let edges = { 316 | 0: [], 317 | 1: [], 318 | 2: [], 319 | 3: [], 320 | }; 321 | 322 | for (const [{ editor, x, y, adjacent }, i] of withIndex(this.nodes)) { 323 | nodes[i] = editor; 324 | positions[i] = [x, y]; 325 | 326 | for (let j = 0; j < GRAPH_COLORS.length; j++) { 327 | edges[j][i] = []; 328 | for (const otherNode of adjacent[j]) { 329 | const otherNodeIndex = this.nodes.findIndex((n) => n === otherNode); 330 | edges[j][i].push(otherNodeIndex); 331 | } 332 | } 333 | } 334 | 335 | return { 336 | isColoredGraph: true, 337 | nodes, 338 | edges, 339 | positions, 340 | }; 341 | } 342 | 343 | getOutput() { 344 | const { nodes, edges, positions } = this.getJSONOutput(); 345 | return `(${JSON.stringify({ 346 | nodes: nodes.map((node) => node.getOutput().split('"').join("")), 347 | edges, 348 | positions, 349 | isColoredGraph: true, 350 | })})`; 351 | } 352 | } 353 | 354 | customElements.define("color-graph-editor", ColoredGraphEditorElement); 355 | -------------------------------------------------------------------------------- /lib/src/editors/DropdownElement.ts: -------------------------------------------------------------------------------- 1 | import { withIndex } from "../Iterable.js"; 2 | import { mod } from "../math.js"; 3 | import { TextEditorElement } from "./TextEditorElement.js"; 4 | 5 | export const DropdownElement = (editorDescriptions, name = "no-name") => { 6 | class C extends TextEditorElement { 7 | meta = { 8 | editorName: name, 9 | }; 10 | selection = 0; 11 | 12 | dropdownEl: HTMLDivElement; 13 | editorEls: Array; 14 | 15 | constructor() { 16 | super(...arguments); 17 | 18 | this.style.setProperty("--editor-name", `'dropdown'`); 19 | this.style.setProperty("--editor-color", "grey"); 20 | this.style.setProperty("--editor-name-color", "black"); 21 | this.style.setProperty("--editor-background-color", "#FEFEFE"); 22 | this.style.setProperty("--editor-outline-color", "grey"); 23 | 24 | this.styleEl = document.createElement("style"); 25 | this.styleEl.textContent = ` 26 | .dropdown { 27 | display: none; 28 | } 29 | .dropdown pre { 30 | margin: 0; 31 | padding: 10px; 32 | } 33 | .dropdown pre:hover { 34 | background: #ffd608; 35 | 36 | } 37 | :host(:focus) .dropdown { 38 | display: block; 39 | position: absolute; 40 | top: 100%; 41 | left: -2px; 42 | margin: 0; 43 | background: #FEFEFE; 44 | z-index: 100; 45 | border-radius: 2px; 46 | border: 2px solid grey; 47 | } 48 | `; 49 | this.dropdownEl = document.createElement("div"); 50 | this.dropdownEl.className = "dropdown"; 51 | this.editorEls = editorDescriptions.map( 52 | ({ name, description, iconPath, ElementConstructor }) => { 53 | const editorEl = document.createElement("pre"); 54 | editorEl.innerHTML = 55 | (iconPath ? ` ` : "") + 56 | `${name} 57 | ${description}`; 58 | editorEl.addEventListener("click", () => { 59 | if (this.parentEditor) { 60 | this.parentEditor.dispatchEvent( 61 | new CustomEvent("subEditorReplaced", { 62 | detail: { 63 | old: this, 64 | new: new ElementConstructor({ 65 | parentEditor: this.parentEditor, 66 | builder: this.builder, 67 | }), 68 | }, 69 | }) 70 | ); 71 | } 72 | }); 73 | return editorEl; 74 | } 75 | ); 76 | this.dropdownEl.append(...this.editorEls); 77 | this.shadowRoot.append(this.styleEl, this.dropdownEl); 78 | 79 | this.addEventListener("blur", () => (this.code = [])); 80 | this.addEventListener("keydown", (e) => { 81 | if (e.key === "ArrowDown") { 82 | e.preventDefault(); 83 | this.selection = mod(this.selection + 1, this.editorEls.length); 84 | } else if (e.key === "ArrowUp") { 85 | e.preventDefault(); 86 | this.selection = mod(this.selection - 1, this.editorEls.length); 87 | } 88 | for (const [editorEl, i] of withIndex(this.editorEls)) { 89 | if (i === this.selection) editorEl.style.background = "#ffd608"; 90 | else editorEl.style.background = "#FEFEFE"; 91 | } 92 | if (e.key === "Enter") { 93 | if (this.parentEditor) { 94 | this.parentEditor.dispatchEvent( 95 | new CustomEvent("subEditorReplaced", { 96 | detail: { 97 | old: this, 98 | new: new editorDescriptions[ 99 | this.selection 100 | ].ElementConstructor({ 101 | parentEditor: this.parentEditor, 102 | builder: this.builder, 103 | }), 104 | }, 105 | }) 106 | ); 107 | } 108 | } 109 | }); 110 | for (const [editorEl, i] of withIndex(this.editorEls)) { 111 | if (i === this.selection) editorEl.style.background = "#ffd608"; 112 | else editorEl.style.background = "#FEFEFE"; 113 | } 114 | } 115 | 116 | getOutput() { 117 | return ""; 118 | } 119 | } 120 | 121 | customElements.define(`dropdown-${name}-editor`, C); 122 | return C; 123 | }; 124 | -------------------------------------------------------------------------------- /lib/src/editors/ForceGraphEditorElement.ts: -------------------------------------------------------------------------------- 1 | import { EditorElement } from "../editor.js"; 2 | import { withIndex } from "../Iterable.js"; 3 | import { add, distance, mul, sub } from "../math.js"; 4 | import { StringEditorElement } from "./StringEditorElement.js"; 5 | 6 | type EditorNode = { 7 | x: number; 8 | y: number; 9 | editor: EditorElement; 10 | adjacent: Array; 11 | }; 12 | 13 | export class ForceGraphEditorElement extends EditorElement { 14 | meta = { 15 | editorName: "Force Graph", 16 | }; 17 | nodes: Array = []; 18 | styleEl: HTMLStyleElement; 19 | canvas: HTMLCanvasElement; 20 | context: CanvasRenderingContext2D; 21 | fromNode: EditorNode; 22 | mouse: [number, number]; 23 | 24 | constructor() { 25 | super(...arguments); 26 | 27 | this.style.setProperty("--editor-name", `'graph'`); 28 | this.style.setProperty("--editor-color", "#7300CF"); 29 | this.style.setProperty("--editor-name-color", "white"); 30 | this.style.setProperty("--editor-background-color", "#eed9ff"); 31 | this.style.setProperty("--editor-outline-color", "#b59dc9"); 32 | 33 | this.styleEl = document.createElement("style"); 34 | this.styleEl.textContent = ` 35 | :host { 36 | position: relative; 37 | height: 250px; 38 | width: 250px; 39 | } 40 | 41 | canvas { 42 | position: absolute; 43 | top: 0; 44 | left: 0; 45 | } 46 | `; 47 | this.canvas = document.createElement("canvas"); 48 | this.canvas.width = 250; 49 | this.canvas.height = 250; 50 | this.context = this.canvas.getContext("2d"); 51 | this.shadowRoot.append(this.styleEl, this.canvas); 52 | 53 | this.fromNode = null; 54 | this.mouse = [0, 0]; 55 | 56 | this.fromInput(arguments[0]); 57 | setTimeout(() => this.render()); 58 | 59 | this.addEventListener("keydown", (e) => { 60 | if (e.key === "Backspace" && this.parentEditor) { 61 | this.parentEditor.dispatchEvent( 62 | new CustomEvent("subEditorDeleted", { detail: this }) 63 | ); 64 | } else if (e.key === "ArrowLeft" && this.parentEditor) { 65 | this.blur(); 66 | this.parentEditor.focusEditor(this, 0, e.shiftKey); 67 | } else if (e.key === "ArrowRight" && this.parentEditor) { 68 | this.blur(); 69 | this.parentEditor.focusEditor(this, 1, e.shiftKey); 70 | } 71 | }); 72 | this.addEventListener("subEditorDeleted", (e: CustomEvent) => { 73 | this.nodes = this.nodes.filter(({ editor }) => editor !== e.detail); 74 | for (const node of this.nodes) { 75 | node.adjacent = node.adjacent.filter( 76 | ({ editor }) => editor !== e.detail 77 | ); 78 | } 79 | this.shadowRoot.removeChild(e.detail); 80 | this.render(); 81 | this.focusEditor(); 82 | new CustomEvent("childEditorUpdate", { 83 | detail: { 84 | out: this.getOutput(), 85 | editor: this, 86 | }, 87 | }); 88 | }); 89 | this.addEventListener("subEditorClicked", (e: CustomEvent) => { 90 | const subFocus = this.nodes.find(({ editor }) => editor === e.detail[0]); 91 | if (subFocus) { 92 | this.fromNode = subFocus; // HACK 93 | } 94 | }); 95 | this.addEventListener("mousemove", (e) => { 96 | this.mouse = [e.offsetX, e.offsetY]; 97 | if (this.fromNode && e.metaKey) { 98 | this.fromNode.x = Math.max( 99 | 0, 100 | Math.min( 101 | this.mouse[0] - 10, 102 | this.offsetWidth - this.fromNode.editor.offsetWidth - 2 103 | ) 104 | ); 105 | this.fromNode.y = Math.max( 106 | 0, 107 | Math.min( 108 | this.mouse[1] - 10, 109 | this.offsetHeight - this.fromNode.editor.offsetHeight - 2 110 | ) 111 | ); 112 | } 113 | this.render(); 114 | }); 115 | this.addEventListener("mousedown", (e: MouseEvent) => { 116 | const editor = new StringEditorElement({ parentEditor: this }); 117 | editor.style.position = "absolute"; 118 | const node = { 119 | x: e.offsetX - 10, 120 | y: e.offsetY - 10, 121 | editor, 122 | adjacent: [], 123 | }; 124 | this.nodes.push(node); 125 | this.shadowRoot.append(editor); 126 | this.blur(); 127 | setTimeout(() => editor.focusEditor()); 128 | //this.fromNode = node; 129 | this.render(); 130 | new CustomEvent("childEditorUpdate", { 131 | detail: { 132 | out: this.getOutput(), 133 | editor: this, 134 | }, 135 | }); 136 | }); 137 | this.addEventListener("mouseup", (e) => { 138 | const targetEl = (e as MouseEvent).composedPath()[0] as Node; 139 | const targetNode = this.nodes.find( 140 | ({ editor }) => 141 | editor === targetEl || 142 | editor.contains(targetEl) || 143 | editor.shadowRoot.contains(targetEl) 144 | ); 145 | if (targetNode) { 146 | if ( 147 | this.fromNode && 148 | this.fromNode !== targetNode && 149 | !this.fromNode.adjacent.includes(targetNode) 150 | ) { 151 | this.fromNode.adjacent.push(targetNode); 152 | targetNode.adjacent.push(this.fromNode); 153 | new CustomEvent("childEditorUpdate", { 154 | detail: { 155 | out: this.getOutput(), 156 | editor: this, 157 | }, 158 | }); 159 | } 160 | } 161 | this.fromNode = null; 162 | this.render(); 163 | }); 164 | 165 | const move = () => { 166 | const middleOfEditor: [number, number] = [ 167 | this.offsetWidth / 2, 168 | this.offsetHeight / 2, 169 | ]; 170 | 171 | const forces = []; 172 | for (let i = 0; i < this.nodes.length; i++) forces[i] = [0, 0]; 173 | 174 | for (let i = 0; i < this.nodes.length; i++) { 175 | const node = this.nodes[i]; 176 | 177 | const start: [number, number] = [ 178 | node.x + node.editor.offsetWidth / 2, 179 | node.y + node.editor.offsetHeight / 2, 180 | ]; 181 | 182 | const dirToMiddle = sub(middleOfEditor, start); 183 | const distToMiddle = distance(start, middleOfEditor); 184 | const nudgeToMiddle = mul(0.0005 * distToMiddle, dirToMiddle); 185 | forces[i] = add(forces[i], nudgeToMiddle); 186 | 187 | for (let j = i + 1; j < this.nodes.length; j++) { 188 | const otherNode = this.nodes[j]; 189 | 190 | const end: [number, number] = [ 191 | otherNode.x + otherNode.editor.offsetWidth / 2, 192 | otherNode.y + otherNode.editor.offsetHeight / 2, 193 | ]; 194 | 195 | const dir = sub(end, start); 196 | const mag = distance(start, end); 197 | 198 | let force = mul(node.editor.offsetWidth ** 1.3 / mag ** 2, dir); 199 | //if (node.adjacent.includes(otherNode)) force = add(force, mul(-mag / 500, dir)); 200 | 201 | forces[i] = add(forces[i], mul(-1, force)); 202 | forces[j] = add(forces[j], force); 203 | } 204 | } 205 | 206 | for (let i = 0; i < this.nodes.length; i++) { 207 | const node = this.nodes[i]; 208 | const [x, y] = add([node.x, node.y], forces[i]); 209 | node.x = x; 210 | node.y = y; 211 | } 212 | }; 213 | move(); 214 | 215 | const step = () => { 216 | this.render(); 217 | move(); 218 | 219 | requestAnimationFrame(step); 220 | }; 221 | requestAnimationFrame(step); 222 | } 223 | 224 | render() { 225 | this.context.strokeStyle = "#7300CF"; 226 | this.context.fillStyle = "#7300CF"; 227 | this.context.lineWidth = 2; 228 | this.context.lineCap = "round"; 229 | this.canvas.width = this.offsetWidth; 230 | this.canvas.height = this.offsetHeight; 231 | if (this.fromNode) { 232 | this.context.beginPath(); 233 | this.context.moveTo( 234 | this.fromNode.x + this.fromNode.editor.offsetWidth / 2, 235 | this.fromNode.y + this.fromNode.editor.offsetHeight / 2 236 | ); 237 | this.context.lineTo(...this.mouse); 238 | this.context.stroke(); 239 | } 240 | for (const { x, y, editor, adjacent } of this.nodes) { 241 | editor.style.top = `${y}px`; 242 | editor.style.left = `${x}px`; 243 | for (const otherNode of adjacent) { 244 | this.context.beginPath(); 245 | const start: [number, number] = [ 246 | x + editor.offsetWidth / 2, 247 | y + editor.offsetHeight / 2, 248 | ]; 249 | const end: [number, number] = [ 250 | otherNode.x + otherNode.editor.offsetWidth / 2, 251 | otherNode.y + otherNode.editor.offsetHeight / 2, 252 | ]; 253 | this.context.moveTo(...start); 254 | this.context.lineTo(...end); 255 | this.context.stroke(); 256 | } 257 | } 258 | } 259 | 260 | fromInput(input) { 261 | if (!input || !input.nodes || !input.edges) return; 262 | const { nodes, edges } = input; 263 | 264 | for (let i = 0; i < nodes.length; i++) { 265 | const nodeValue = nodes[i]; 266 | 267 | let editor; 268 | if (nodeValue instanceof EditorElement) { 269 | editor = new StringEditorElement({ 270 | code: [nodeValue], 271 | parentEditor: this, 272 | }); 273 | } else { 274 | editor = new StringEditorElement({ 275 | code: [String(nodeValue)], 276 | parentEditor: this, 277 | }); 278 | } 279 | editor.style.position = "absolute"; 280 | 281 | this.nodes[i] = { x: 100 + i, y: 100 + i, editor, adjacent: [] }; 282 | this.shadowRoot.append(editor); 283 | } 284 | for (let i = 0; i < nodes.length; i++) { 285 | const edgeList = edges[i]; 286 | 287 | for (const edgeIndex of edgeList) { 288 | this.nodes[i].adjacent.push(this.nodes[edgeIndex]); 289 | } 290 | } 291 | } 292 | 293 | getOutput() { 294 | let nodes = []; 295 | let edges = []; 296 | 297 | for (const [{ editor, x, y, adjacent }, i] of withIndex(this.nodes)) { 298 | nodes[i] = editor.getOutput(); 299 | 300 | edges[i] = []; 301 | for (const otherNode of adjacent) { 302 | const otherNodeIndex = this.nodes.findIndex((n) => n === otherNode); 303 | edges[i].push(otherNodeIndex); 304 | } 305 | } 306 | 307 | return `({ 308 | "nodes": [${nodes}], 309 | "edges": [${edges.map((edgeList) => `[${edgeList}]`)}] 310 | })`; 311 | } 312 | } 313 | 314 | customElements.define("force-graph-editor", ForceGraphEditorElement); 315 | -------------------------------------------------------------------------------- /lib/src/editors/MakeGraphEditorElement.ts: -------------------------------------------------------------------------------- 1 | import { EditorElement } from "../editor.js"; 2 | import { withIndex } from "../Iterable.js"; 3 | import { TextEditorElement } from "./TextEditorElement.js"; 4 | 5 | type EditorNode = { 6 | x: number; 7 | y: number; 8 | editor: EditorElement; 9 | adjacent: Array; 10 | }; 11 | 12 | export const MakeGraphEditorElement = ( 13 | NestedEditorConstructor = TextEditorElement, 14 | name = "custom" 15 | ) => { 16 | class GraphEditorElement extends EditorElement { 17 | meta = { 18 | editorName: name, 19 | }; 20 | nodes: Array = []; 21 | styleEl: HTMLStyleElement; 22 | canvas: HTMLCanvasElement; 23 | context: CanvasRenderingContext2D; 24 | fromNode: EditorNode; 25 | mouse: [number, number]; 26 | 27 | constructor() { 28 | super(...arguments); 29 | 30 | this.style.setProperty("--editor-name", `'graph'`); 31 | this.style.setProperty("--editor-color", "#7300CF"); 32 | this.style.setProperty("--editor-name-color", "white"); 33 | this.style.setProperty("--editor-background-color", "#eed9ff"); 34 | this.style.setProperty("--editor-outline-color", "#b59dc9"); 35 | 36 | this.styleEl = document.createElement("style"); 37 | this.styleEl.textContent = ` 38 | :host { 39 | position: relative; 40 | height: 250px; 41 | width: 250px; 42 | } 43 | 44 | canvas { 45 | position: absolute; 46 | top: 0; 47 | left: 0; 48 | } 49 | `; 50 | this.canvas = document.createElement("canvas"); 51 | this.canvas.width = 250; 52 | this.canvas.height = 250; 53 | this.context = this.canvas.getContext("2d"); 54 | this.shadowRoot.append(this.styleEl, this.canvas); 55 | 56 | this.fromNode = null; 57 | this.mouse = [0, 0]; 58 | 59 | this.fromInput(arguments[0]); 60 | setTimeout(() => this.render()); 61 | 62 | this.addEventListener("keydown", (e: KeyboardEvent) => { 63 | if (e.key === "Backspace" && this.parentEditor) { 64 | this.parentEditor.dispatchEvent( 65 | new CustomEvent("subEditorDeleted", { detail: this }) 66 | ); 67 | this.parentEditor?.dispatchEvent( 68 | new CustomEvent("childEditorUpdate", { 69 | detail: { 70 | out: this.getOutput(), 71 | editor: this, 72 | }, 73 | }) 74 | ); 75 | } else if (e.key === "ArrowLeft" && this.parentEditor) { 76 | this.blur(); 77 | this.parentEditor.focusEditor(this, 0, e.shiftKey); 78 | } else if (e.key === "ArrowRight" && this.parentEditor) { 79 | this.blur(); 80 | this.parentEditor.focusEditor(this, 1, e.shiftKey); 81 | } 82 | }); 83 | this.addEventListener("subEditorDeleted", (e: CustomEvent) => { 84 | this.nodes = this.nodes.filter(({ editor }) => editor !== e.detail); 85 | for (const node of this.nodes) { 86 | node.adjacent = node.adjacent.filter( 87 | ({ editor }) => editor !== e.detail 88 | ); 89 | } 90 | this.shadowRoot.removeChild(e.detail); 91 | this.render(); 92 | this.focusEditor(); 93 | this.parentEditor?.dispatchEvent( 94 | new CustomEvent("childEditorUpdate", { 95 | detail: { 96 | out: this.getOutput(), 97 | editor: this, 98 | }, 99 | }) 100 | ); 101 | }); 102 | this.addEventListener("subEditorClicked", (e: CustomEvent) => { 103 | const subFocus = this.nodes.find( 104 | ({ editor }) => editor === e.detail[0] 105 | ); 106 | if (subFocus) { 107 | this.fromNode = subFocus; // HACK 108 | } 109 | }); 110 | this.addEventListener("mousemove", (e) => { 111 | this.mouse = [e.offsetX, e.offsetY]; 112 | if (this.fromNode && e.metaKey) { 113 | this.fromNode.x = Math.max( 114 | 0, 115 | Math.min( 116 | this.mouse[0] - 10, 117 | this.offsetWidth - this.fromNode.editor.offsetWidth - 2 118 | ) 119 | ); 120 | this.fromNode.y = Math.max( 121 | 0, 122 | Math.min( 123 | this.mouse[1] - 10, 124 | this.offsetHeight - this.fromNode.editor.offsetHeight - 2 125 | ) 126 | ); 127 | } 128 | this.render(); 129 | }); 130 | this.addEventListener("mousedown", (e) => { 131 | const editor = new NestedEditorConstructor({ parentEditor: this }); 132 | editor.style.position = "absolute"; 133 | const node = { 134 | x: e.offsetX - 10, 135 | y: e.offsetY - 10, 136 | editor, 137 | adjacent: [], 138 | }; 139 | this.nodes.push(node); 140 | this.shadowRoot.append(editor); 141 | this.blur(); 142 | setTimeout(() => editor.focusEditor()); 143 | //this.fromNode = node; 144 | this.render(); 145 | this.parentEditor?.dispatchEvent( 146 | new CustomEvent("childEditorUpdate", { 147 | detail: { 148 | out: this.getOutput(), 149 | editor: this, 150 | }, 151 | }) 152 | ); 153 | }); 154 | this.addEventListener("mouseup", (e) => { 155 | const targetEl = (e as MouseEvent).composedPath()[0] as Node; 156 | const targetNode = this.nodes.find( 157 | ({ editor }) => 158 | editor.contains(targetEl) || editor.shadowRoot.contains(targetEl) 159 | ); 160 | if (targetNode) { 161 | if ( 162 | this.fromNode && 163 | this.fromNode !== targetNode && 164 | !this.fromNode.adjacent.includes(targetNode) 165 | ) { 166 | this.fromNode.adjacent.push(targetNode); 167 | this.parentEditor?.dispatchEvent( 168 | new CustomEvent("childEditorUpdate", { 169 | detail: { 170 | out: this.getOutput(), 171 | editor: this, 172 | }, 173 | }) 174 | ); 175 | } 176 | } 177 | this.fromNode = null; 178 | this.render(); 179 | }); 180 | this.addEventListener("childEditorUpdate", (e) => { 181 | this.parentEditor?.dispatchEvent( 182 | new CustomEvent("childEditorUpdate", { 183 | detail: { 184 | out: this.getOutput(), 185 | editor: this, 186 | }, 187 | }) 188 | ); 189 | }); 190 | } 191 | 192 | render() { 193 | this.context.strokeStyle = "#7300CF"; 194 | this.context.fillStyle = "#7300CF"; 195 | this.context.lineWidth = 2; 196 | this.context.lineCap = "round"; 197 | this.canvas.width = this.offsetWidth; 198 | this.canvas.height = this.offsetHeight; 199 | if (this.fromNode) { 200 | this.context.beginPath(); 201 | this.context.moveTo( 202 | this.fromNode.x + this.fromNode.editor.offsetWidth / 2, 203 | this.fromNode.y + this.fromNode.editor.offsetHeight / 2 204 | ); 205 | this.context.lineTo(...this.mouse); 206 | this.context.stroke(); 207 | } 208 | for (const { x, y, editor, adjacent } of this.nodes) { 209 | editor.style.top = `${y}px`; 210 | editor.style.left = `${x}px`; 211 | for (const otherNode of adjacent) { 212 | this.context.beginPath(); 213 | const start: [number, number] = [ 214 | x + editor.offsetWidth / 2, 215 | y + editor.offsetHeight / 2, 216 | ]; 217 | const end: [number, number] = [ 218 | otherNode.x + otherNode.editor.offsetWidth / 2, 219 | otherNode.y + otherNode.editor.offsetHeight / 2, 220 | ]; 221 | this.context.moveTo(...start); 222 | this.context.lineTo(...end); 223 | this.context.stroke(); 224 | 225 | const angle = Math.atan2(end[0] - start[0], end[1] - start[1]); 226 | const dir = [Math.sin(angle), Math.cos(angle)]; 227 | const dist = 228 | Math.min( 229 | otherNode.editor.offsetHeight * Math.abs(1 / Math.cos(angle)), 230 | otherNode.editor.offsetWidth * Math.abs(1 / Math.sin(angle)) 231 | ) / 2; // https://math.stackexchange.com/a/924290/421433 232 | this.context.beginPath(); 233 | this.context.moveTo(end[0] - dir[0] * dist, end[1] - dir[1] * dist); 234 | this.context.lineTo( 235 | end[0] - dir[0] * (dist + 11) + dir[1] * 7, 236 | end[1] - dir[1] * (dist + 11) - dir[0] * 7 237 | ); 238 | this.context.stroke(); 239 | this.context.moveTo(end[0] - dir[0] * dist, end[1] - dir[1] * dist); 240 | this.context.lineTo( 241 | end[0] - dir[0] * (dist + 11) - dir[1] * 7, 242 | end[1] - dir[1] * (dist + 11) + dir[0] * 7 243 | ); 244 | this.context.stroke(); 245 | } 246 | } 247 | } 248 | 249 | fromInput(input) { 250 | if (!input || !input.nodes || !input.edges || !input.positions) return; 251 | const { nodes, edges, positions } = input; 252 | 253 | for (let i = 0; i < nodes.length; i++) { 254 | const nodeValue = nodes[i]; 255 | const position = positions[i]; 256 | 257 | let editor; 258 | if (nodeValue instanceof EditorElement) { 259 | editor = new NestedEditorConstructor({ 260 | code: [nodeValue], 261 | parentEditor: this, 262 | }); 263 | } else { 264 | editor = new NestedEditorConstructor({ 265 | code: [String(nodeValue)], 266 | parentEditor: this, 267 | }); 268 | } 269 | editor.style.position = "absolute"; 270 | 271 | this.nodes[i] = { 272 | x: position[0], 273 | y: position[1], 274 | editor, 275 | adjacent: [], 276 | }; 277 | this.shadowRoot.append(editor); 278 | } 279 | for (let i = 0; i < nodes.length; i++) { 280 | const edgeList = edges[i]; 281 | 282 | for (const edgeIndex of edgeList) { 283 | this.nodes[i].adjacent.push(this.nodes[edgeIndex]); 284 | } 285 | } 286 | } 287 | 288 | getOutput() { 289 | let nodes = []; 290 | let positions = []; 291 | let edges = []; 292 | 293 | for (const [{ editor, x, y, adjacent }, i] of withIndex(this.nodes)) { 294 | nodes[i] = `"${editor.getOutput()}"`; 295 | positions[i] = [x, y]; 296 | 297 | edges[i] = []; 298 | for (const otherNode of adjacent) { 299 | const otherNodeIndex = this.nodes.findIndex((n) => n === otherNode); 300 | edges[i].push(otherNodeIndex); 301 | } 302 | } 303 | 304 | return `({ 305 | "nodes": [${nodes}], 306 | "edges": [${edges.map((edgeList) => `[${edgeList}]`)}], 307 | "positions": [${positions.map(([x, y]) => `[${x}, ${y}]`)}] 308 | })`; 309 | } 310 | } 311 | customElements.define(`graph-editor-${name}`, GraphEditorElement); 312 | return GraphEditorElement; 313 | }; 314 | -------------------------------------------------------------------------------- /lib/src/editors/MusicStaffEditorElement.ts: -------------------------------------------------------------------------------- 1 | import { ArrayEditorElement } from "./ArrayEditorElement.js"; 2 | 3 | const STAFF_BOTTOM_NOTE_Y = 20.5; 4 | const STAFF_LINE_HEIGHT = 5; 5 | 6 | const NOTES = [ 7 | "c4", 8 | "d4", 9 | "e4", 10 | "f4", 11 | "g4", 12 | "a4", 13 | "b4", 14 | "c5", 15 | "d5", 16 | "e5", 17 | "f5", 18 | "g5", 19 | "a5", 20 | " ", 21 | ] as const; 22 | 23 | const KEY_TO_NOTE = { 24 | a: "e4", 25 | s: "f4", 26 | d: "g4", 27 | f: "a4", 28 | g: "b4", 29 | h: "c5", 30 | j: "d5", 31 | k: "e5", 32 | l: "f5", 33 | ";": "g5", 34 | " ": " ", 35 | }; 36 | 37 | export class MusicStaffEditorElement extends ArrayEditorElement<{ 38 | note: typeof NOTES[number] | " "; 39 | length: number; 40 | }> { 41 | meta = { 42 | editorName: "♫ Staff", 43 | }; 44 | 45 | styleEl2: HTMLStyleElement; 46 | 47 | getHighlightedOutput() { 48 | if (!this.isFocused || this.minorCaret === this.caret) return "[]"; 49 | 50 | const [start, end] = this.caretsOrdered(); 51 | 52 | return "(" + JSON.stringify(this.contents.slice(start, end)) + ")"; 53 | } 54 | 55 | getOutput() { 56 | return "(" + JSON.stringify(this.contents) + ")"; 57 | } 58 | 59 | processClipboardText(clipboardText: string): Array<{ 60 | note: typeof NOTES[number]; 61 | length: number; 62 | }> { 63 | return JSON.parse(clipboardText.substring(1, clipboardText.length - 1)); 64 | } 65 | 66 | constructor(arg) { 67 | super(arg); 68 | 69 | this.style.setProperty("--editor-color", "black"); 70 | this.style.setProperty("--editor-name-color", "white"); 71 | this.style.setProperty("--editor-background-color", "white"); 72 | this.style.setProperty("--editor-outline-color", "black"); 73 | this.styleEl2 = document.createElement("style"); 74 | this.styleEl2.textContent = ` 75 | :host { 76 | padding: 10px; 77 | } 78 | :host(:not(:focus-visible)) #caret { 79 | display: none; 80 | } 81 | #caret { 82 | animation: blinker 1s linear infinite; 83 | } 84 | @keyframes blinker { 85 | 0% { opacity: 1; } 86 | 49% { opacity: 1; } 87 | 50% { opacity: 0; } 88 | 100% { opacity: 0; } 89 | } 90 | `; 91 | this.shadowRoot.append(this.styleEl2); 92 | 93 | this.contentsEl.innerHTML = svgElText(); 94 | this.render(); 95 | } 96 | 97 | render() { 98 | const notesWrapperEl = this.shadowRoot.getElementById("notes"); 99 | if (!notesWrapperEl) return; 100 | 101 | notesWrapperEl.innerHTML = ""; 102 | let x = 25; 103 | let i = 0; 104 | for (const { note, length } of this.contents) { 105 | const y = 106 | STAFF_BOTTOM_NOTE_Y - (NOTES.indexOf(note) * STAFF_LINE_HEIGHT) / 2; 107 | 108 | if (note === " ") { 109 | notesWrapperEl.append( 110 | makeSVGEl("use", { href: "#quarter-rest", x, y: 11, i }) 111 | ); 112 | } else if (y < 10) { 113 | notesWrapperEl.append( 114 | makeSVGEl("use", { href: "#quarter-note-flipped", x, y: y + 10.5, i }) 115 | ); 116 | } else { 117 | notesWrapperEl.append( 118 | makeSVGEl("use", { href: "#quarter-note", x, y, i }) 119 | ); 120 | } 121 | 122 | if (i !== 0 && i % 4 === 0) { 123 | notesWrapperEl.append( 124 | makeSVGEl("use", { href: "#vertical-line", x: x - 3 }) 125 | ); 126 | } 127 | 128 | x += 14; 129 | i++; 130 | } 131 | 132 | this.shadowRoot.getElementById("svg").setAttribute("width", x * 2 + ""); 133 | this.shadowRoot 134 | .getElementById("svg") 135 | .setAttribute("viewBox", `0 0 ${x} 38`); 136 | 137 | notesWrapperEl.append( 138 | makeSVGEl("use", { href: "#caret", x: 19.5 + this.caret * 14, y: 5 }) 139 | ); 140 | 141 | const [start, end] = this.caretsOrdered(); 142 | if (start !== end) { 143 | const el = makeSVGEl("rect", { 144 | fill: "white", 145 | x: 22 + start * 14, 146 | y: 6, 147 | height: 26.5, 148 | width: (end - start) * 14 - 1, 149 | rx: 2, 150 | }); 151 | el.style.mixBlendMode = "difference"; 152 | notesWrapperEl.append(el); 153 | } 154 | } 155 | 156 | keyHandler(e) { 157 | if (KEY_TO_NOTE[e.key]) { 158 | this.insert([{ note: KEY_TO_NOTE[e.key], length: 1 }]); 159 | return true; 160 | } 161 | } 162 | } 163 | customElements.define("music-staff-editor", MusicStaffEditorElement); 164 | 165 | function makeSVGEl(name, props = {}) { 166 | const el = document.createElementNS("http://www.w3.org/2000/svg", name); 167 | for (const [key, value] of Object.entries(props)) 168 | el.setAttributeNS(null, key, value); 169 | return el; 170 | } 171 | 172 | function svgElText() { 173 | return ` 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | `; 211 | } 212 | -------------------------------------------------------------------------------- /lib/src/editors/StringEditorElement.ts: -------------------------------------------------------------------------------- 1 | import { withIndex } from "../Iterable.js"; 2 | import { 3 | TextEditorArgumentObject, 4 | TextEditorElement, 5 | } from "./TextEditorElement.js"; 6 | 7 | export class StringEditorElement extends TextEditorElement { 8 | meta = { 9 | editorName: "String", 10 | }; 11 | constructor(arg: TextEditorArgumentObject) { 12 | super(arg); 13 | 14 | this.style.setProperty("--editor-name", `'string'`); 15 | this.style.setProperty("--editor-color", "black"); 16 | this.style.setProperty("--editor-name-color", "white"); 17 | this.style.setProperty("--editor-background-color", "white"); 18 | this.style.setProperty("--editor-outline-color", "black"); 19 | } 20 | 21 | getOutput() { 22 | let output = '"'; 23 | 24 | for (const [slotOrChar, i] of withIndex(this.code)) { 25 | if (typeof slotOrChar === "string") { 26 | const char = slotOrChar; 27 | output += char; 28 | } else { 29 | const slot = slotOrChar; 30 | output += slot.getOutput(); 31 | } 32 | } 33 | 34 | return output + '"'; 35 | } 36 | } 37 | 38 | customElements.define("string-editor", StringEditorElement); 39 | -------------------------------------------------------------------------------- /lib/src/editors/markdownEditors.ts: -------------------------------------------------------------------------------- 1 | import { TextEditorElement } from "./TextEditorElement.js"; 2 | import { UnaryJoinerElement } from "./mathEditors.js"; 3 | import { EditorElement } from "../editor.js"; 4 | 5 | export class MarkdownEditorElement extends TextEditorElement { 6 | meta = { 7 | editorName: "Markdown", 8 | }; 9 | 10 | constructor(arg) { 11 | super(arg); 12 | 13 | this.style.setProperty("--editor-name", `'markdown'`); 14 | this.style.setProperty("--editor-color", "#376e32"); 15 | this.style.setProperty("--editor-name-color", "#c1f2bd"); 16 | this.style.setProperty("--editor-background-color", "#c1f2bd"); 17 | this.style.setProperty("--editor-outline-color", "#376e32"); 18 | } 19 | 20 | keyHandler(e: KeyboardEvent) { 21 | if (e.key === "-") { 22 | // no elevations 23 | const focuser = new BulletJoinerElement({ parentEditor: this }); 24 | this.code.splice(this.caret, 0, "\n", focuser); 25 | return focuser; 26 | } 27 | if (e.key === "#") { 28 | // no elevations 29 | const focuser = new HeaderElement({ parentEditor: this }); 30 | this.code.splice(this.caret, 0, focuser); 31 | return focuser; 32 | } 33 | } 34 | 35 | getOutput() { 36 | const sOut = super.getOutput(); 37 | return sOut 38 | .split("\n") 39 | .map((line) => "// " + line) 40 | .join("\n"); 41 | } 42 | } 43 | customElements.define("markdown-editor", MarkdownEditorElement); 44 | 45 | class HeaderElement extends TextEditorElement { 46 | meta = { 47 | editorName: "Markdown #", 48 | }; 49 | 50 | declare parentEditor?: MarkdownEditorElement; 51 | constructor(arg) { 52 | super(arg); 53 | 54 | this.style.setProperty("--editor-name", `'markdown'`); 55 | this.style.setProperty("--editor-color", "#376e32"); 56 | this.style.setProperty("--editor-name-color", "#c1f2bd"); 57 | this.style.setProperty("--editor-background-color", "#c1f2bd"); 58 | this.style.setProperty("--editor-outline-color", "#376e32"); 59 | this.style.setProperty("font-weight", "900"); 60 | this.style.setProperty("font-size", "40px"); 61 | this.style.setProperty("padding", "10px"); 62 | } 63 | 64 | keyHandler(e: KeyboardEvent) { 65 | if (e.key === "Enter") { 66 | this.parentEditor.focusEditor(this, 1); 67 | this.parentEditor.code.splice(this.parentEditor.caret, 0, "\n"); 68 | this.parentEditor.focusEditor(this, 1); 69 | } 70 | return null; 71 | } 72 | getOutput() { 73 | return `# ${super.getOutput()}`; 74 | } 75 | } 76 | customElements.define("markdown-header-inner-editor", HeaderElement); 77 | 78 | class BulletJoinerInnerElement extends TextEditorElement { 79 | meta = { 80 | editorName: "Markdown -", 81 | }; 82 | 83 | declare parentEditor?: EditorElement & { parentEditor: TextEditorElement }; 84 | constructor(arg) { 85 | super(arg); 86 | 87 | this.style.setProperty("--editor-name", `'markdown'`); 88 | this.style.setProperty("--editor-color", "#376e32"); 89 | this.style.setProperty("--editor-name-color", "#c1f2bd"); 90 | this.style.setProperty("--editor-background-color", "#c1f2bd"); 91 | this.style.setProperty("--editor-outline-color", "#376e32"); 92 | } 93 | 94 | keyHandler(e: KeyboardEvent) { 95 | if (e.key === "Enter") { 96 | this.parentEditor.parentEditor.focusEditor(this.parentEditor, 1); 97 | const focuser = new BulletJoinerElement({ 98 | parentEditor: this.parentEditor.parentEditor, 99 | }); 100 | this.parentEditor.parentEditor.code.splice( 101 | this.parentEditor.parentEditor.caret, 102 | 0, 103 | "\n", 104 | focuser 105 | ); 106 | this.parentEditor.parentEditor.focusEditor(this.parentEditor, 1); 107 | setTimeout(() => focuser.focusEditor(this.parentEditor.parentEditor, 1)); 108 | } 109 | return null; 110 | } 111 | } 112 | customElements.define("markdown-bullet-inner-editor", BulletJoinerInnerElement); 113 | 114 | export const BulletJoinerElement = class extends UnaryJoinerElement( 115 | "bullet", 116 | BulletJoinerInnerElement, 117 | (editor) => { 118 | const styleEl = document.createElement("style"); 119 | styleEl.textContent = ` 120 | :host { 121 | display: inline-flex; 122 | align-items: stretch; 123 | vertical-align: middle; 124 | align-items: center; 125 | padding: 10px; 126 | gap: 5px; 127 | } 128 | span{ 129 | height: 6px; 130 | width: 6px; 131 | border-radius: 100%; 132 | background: black; 133 | } 134 | `; 135 | const bulletEl = document.createElement("span"); 136 | return [styleEl, bulletEl, editor]; 137 | } 138 | ) { 139 | getOutput() { 140 | return `- ${this.editor.getOutput()}`; 141 | } 142 | }; 143 | customElements.define("markdown-bullet-editor", BulletJoinerElement); 144 | -------------------------------------------------------------------------------- /lib/src/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vezwork/Polytope/6f4e7ccb66d68dca0b359014d8931a16b6144244/lib/src/index.ts -------------------------------------------------------------------------------- /lib/src/math.ts: -------------------------------------------------------------------------------- 1 | // Mini custom Vec2 library 2 | 3 | export type Vec2 = [number, number]; 4 | 5 | export const UP: Vec2 = [0, -1]; 6 | export const LEFT: Vec2 = [-1, 0]; 7 | export const DOWN: Vec2 = [0, 1]; 8 | export const RIGHT: Vec2 = [1, 0]; 9 | 10 | export const copy = (v: Vec2): Vec2 => [v[0], v[1]]; 11 | 12 | export const add = (v1: Vec2, v2: Vec2): Vec2 => [v1[0] + v2[0], v1[1] + v2[1]]; 13 | 14 | export const sub = (v1: Vec2, v2: Vec2): Vec2 => [v1[0] - v2[0], v1[1] - v2[1]]; 15 | 16 | export const mul = (n: number, v: Vec2): Vec2 => [n * v[0], n * v[1]]; 17 | 18 | export const dot = (v1: Vec2, v2: Vec2): number => 19 | v1[0] * v2[0] + v1[1] * v2[1]; 20 | 21 | export const length = (v: Vec2): number => Math.sqrt(dot(v, v)); 22 | 23 | export const normalize = (v: Vec2): Vec2 => mul(1 / length(v), v); 24 | 25 | export const angleOf = (v: Vec2): number => Math.atan2(v[1], v[0]); 26 | 27 | export const angleBetween = (v1: Vec2, v2: Vec2): number => 28 | angleOf(sub(v2, v1)); 29 | 30 | export const distance = (v1: Vec2, v2: Vec2): number => length(sub(v1, v2)); 31 | 32 | export const round = (v: Vec2) => [Math.round(v[0]), Math.round(v[1])]; 33 | 34 | // reference: https://en.wikipedia.org/wiki/Rotation_matrix 35 | export const rotate = (v: Vec2, theta: number): Vec2 => [ 36 | Math.cos(theta) * v[0] - Math.sin(theta) * v[1], 37 | Math.sin(theta) * v[0] + Math.cos(theta) * v[1], 38 | ]; 39 | export const normalVec2FromAngle = (theta: number): Vec2 => [ 40 | Math.cos(theta), 41 | Math.sin(theta), 42 | ]; 43 | 44 | export type LineSegment = [Vec2, Vec2]; 45 | 46 | export const lerp = ([start, end]: LineSegment, t: number) => 47 | add(start, mul(t, sub(end, start))); 48 | 49 | // reference: https://stackoverflow.com/a/6853926/5425899 50 | // StackOverflow answer license: CC BY-SA 4.0 51 | // Gives the shortest Vec2 from the point v to the line segment. 52 | export const subLineSegment = (v: Vec2, [start, end]: LineSegment) => { 53 | const startToV = sub(v, start); 54 | const startToEnd = sub(end, start); 55 | 56 | const lengthSquared = dot(startToEnd, startToEnd); 57 | const parametrizedLinePos = 58 | lengthSquared === 0 59 | ? -1 60 | : Math.max(0, Math.min(1, dot(startToV, startToEnd) / lengthSquared)); 61 | 62 | const closestPointOnLine = lerp([start, end], parametrizedLinePos); 63 | return sub(v, closestPointOnLine); 64 | }; 65 | 66 | export const reflectAngle = (theta1: number, theta2: number): number => 67 | theta2 + subAngles(theta1, theta2); 68 | 69 | export const subAngles = (theta1: number, theta2: number): number => 70 | mod(theta2 - theta1 + Math.PI, Math.PI * 2) - Math.PI; 71 | 72 | export const mod = (a: number, n: number): number => a - Math.floor(a / n) * n; 73 | 74 | export const smoothStep = ( 75 | currentValue: number, 76 | targetValue: number, 77 | slowness: number 78 | ): number => currentValue - (currentValue - targetValue) / slowness; 79 | -------------------------------------------------------------------------------- /lib/src/stringToEditorBuilder.ts: -------------------------------------------------------------------------------- 1 | import type { EditorElement } from "./editor"; 2 | 3 | function concatInPlace(array, otherArray) { 4 | for (const entry of otherArray) array.push(entry); 5 | } 6 | 7 | // processors returns true if it can successfully parse char | Editor array to process, false otherwise. 8 | // processors modify the output array argument if it is able to parse the char | Editor array to process. 9 | type BuilderProcessor = ( 10 | toProcess: Array, 11 | output: Array, 12 | fullString: string, 13 | offset: number 14 | ) => boolean; 15 | 16 | type Builder = ( 17 | string: string, 18 | hasOpenParen: boolean 19 | ) => { consume: number; output: Array }; 20 | 21 | export const createBuilder = (processors: BuilderProcessor[] = []) => { 22 | const f: Builder = (string, hasOpenParen = false) => { 23 | let result: Array = []; 24 | 25 | for (let i = 0; i < string.length; i++) { 26 | const char = string[i]; 27 | 28 | if (char === "(") { 29 | const { consume, output } = f(string.substring(i + 1), true); 30 | 31 | if ( 32 | !processors.some((processor) => processor(output, result, string, i)) 33 | ) { 34 | result = [...result, "(", ...output, ")"]; 35 | } 36 | 37 | i = i + consume + 1; 38 | } else if (hasOpenParen && char === ")") { 39 | return { 40 | consume: i, 41 | output: result, 42 | }; 43 | } else { 44 | result = [...result, char]; 45 | } 46 | } 47 | return { 48 | consume: string.length, 49 | output: result, 50 | }; 51 | }; 52 | return f; 53 | }; 54 | 55 | const EDITOR_IDENTIFIER = "POLYTOPE$$STRING"; 56 | const EDITOR_IDENTIFIER_STRING = `"${EDITOR_IDENTIFIER}"`; 57 | export const createJSONProcessor = 58 | (editorFactory, validator): BuilderProcessor => 59 | (innerString, result) => { 60 | try { 61 | // Replace inner editors with a simple value so that JSON parse can parse 62 | // the innerString. 63 | let massagedInnerString = ""; 64 | let innerEditors = []; 65 | for (let i = 0; i < innerString.length; i++) { 66 | const slotOrChar = innerString[i]; 67 | if (typeof slotOrChar !== "string") { 68 | innerEditors.unshift(slotOrChar); 69 | massagedInnerString += EDITOR_IDENTIFIER_STRING; 70 | } else { 71 | const char = slotOrChar; 72 | massagedInnerString += char; 73 | } 74 | } 75 | 76 | const innerJSON = JSON.parse(massagedInnerString); 77 | 78 | const processedJSON = objectValueMap(innerJSON, (value) => 79 | value === EDITOR_IDENTIFIER ? innerEditors.pop() : value 80 | ); 81 | 82 | if (!validator(processedJSON)) return false; 83 | 84 | const a = [editorFactory(processedJSON)]; 85 | console.log(processedJSON, validator(processedJSON), editorFactory, a); 86 | 87 | concatInPlace(result, a); 88 | return true; 89 | } catch (e) { 90 | console.error("JSONProcessor error", innerString, result, e); 91 | } 92 | 93 | return false; 94 | }; 95 | 96 | function objectValueMap(obj, f) { 97 | const newObj = Array.isArray(obj) ? [] : {}; 98 | for (const [key, value] of Object.entries(obj)) { 99 | if (value !== null && typeof value === "object") { 100 | newObj[key] = objectValueMap(value, f); 101 | } else { 102 | newObj[key] = f(value); 103 | } 104 | } 105 | return newObj; 106 | } 107 | 108 | export const createFunctionProcessor = 109 | ({ name, arity, ElementConstructor }): BuilderProcessor => 110 | (innerString, result, string, i) => { 111 | if (string.substring(i - name.length, i) === name) { 112 | // Only supports unary and binary ops for now. 113 | if (arity === 1) { 114 | const joiner = new ElementConstructor({ 115 | code: innerString, 116 | }); 117 | result.length -= name.length; 118 | concatInPlace(result, [joiner]); 119 | return true; 120 | } else if (arity === 2) { 121 | const commaIndex = innerString.indexOf(","); 122 | if (commaIndex !== -1) { 123 | const joiner = new ElementConstructor({ 124 | leftCode: innerString.slice(0, commaIndex), 125 | rightCode: innerString.slice(commaIndex + 2), 126 | }); 127 | result.length -= name.length; 128 | concatInPlace(result, [joiner]); 129 | return true; 130 | } 131 | } 132 | } 133 | 134 | return false; 135 | }; 136 | 137 | // const DELIMITER = '(`POLYTOPE`/*'; 138 | // const ED = ConstructiveUnidirectionalEditor({ 139 | // name: 'del-testing', 140 | // leftEditorFactory: (code, parentEditor) => new TextEditorElement({ 141 | // code, 142 | // parentEditor 143 | // }), 144 | // leftEditorOutput: (editor) => editor.getOutput(), 145 | // }); 146 | // const createTESTINGProcessor = () => (innerString, result, string, i) => { 147 | // console.log('DEUBGUS', string.substring(i, DELIMITER.length)); 148 | // if (string.substring(i, DELIMITER.length) === DELIMITER) { 149 | // const innerCode = innerString.slice(DELIMITER.length, -2); 150 | // console.log('DEBUG INNER', innerString.slice(DELIMITER.length, -2)); 151 | // const editor = new ED({ leftCode: innerCode }) 152 | // concatInPlace(result, [editor]); 153 | // return true; 154 | // } 155 | // return false; 156 | // } 157 | -------------------------------------------------------------------------------- /lib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "noImplicitAny": false, 5 | "module": "esnext", 6 | "target": "esnext" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | entry: "./src/editor.ts", 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.tsx?$/, 9 | use: "ts-loader", 10 | exclude: /node_modules/, 11 | }, 12 | ], 13 | }, 14 | resolve: { 15 | extensions: [".tsx", ".ts", ".js"], 16 | }, 17 | output: { 18 | filename: "bundle.js", 19 | path: path.resolve(__dirname, "dist", "js"), 20 | }, 21 | mode: "none", 22 | }; 23 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 7 | 67 | 68 | 73 | 79 | 87 | 91 | 95 | 96 | 104 | 105 | 110 | 115 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /notes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 12 | 13 | 14 | 15 | 40 | 41 | 108 | 109 | 152 | 153 | 154 | 159 | 160 | 161 | 165 | 166 | 170 | -------------------------------------------------------------------------------- /pres/me.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vezwork/Polytope/6f4e7ccb66d68dca0b359014d8931a16b6144244/pres/me.jpeg -------------------------------------------------------------------------------- /pres/twitterwhite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vezwork/Polytope/6f4e7ccb66d68dca0b359014d8931a16b6144244/pres/twitterwhite.png -------------------------------------------------------------------------------- /testing.js: -------------------------------------------------------------------------------- 1 | // builder 2 | 3 | import { MyEditorElement } from "http://localhost:8080/custom.js"; 4 | import { GroupAlgebraEditor } from "http://localhost:8080/groupTheoryEditor.js" 5 | 6 | export default new MyEditorElement({ 7 | dropdownItems: [{ 8 | name: 'group algebra', 9 | ElementConstructor: GroupAlgebraEditor('s3') 10 | }] 11 | }); 12 | 13 | // built 14 | 15 | import { MatrixJoinerElement } from "http://localhost:8080/mathEditors.js"; 16 | import { s3 } from "http://localhost:8080/groupTheoryPlayground.js"; 17 | 18 | const a3Elements = []; 19 | for (const el of s3.elements) { 20 | a3Elements.push(s3.exponentiate(el, 2)) 21 | } 22 | const a3MulTable = {}; 23 | for (const a3ElA of a3Elements) { 24 | for (const a3ElB of a3Elements) { 25 | if (!a3MulTable[a3ElA]) a3MulTable[a3ElA] = {}; 26 | a3MulTable[a3ElA][a3ElB] = s3.action(a3ElA, a3ElB); 27 | } 28 | } 29 | 30 | export default new MatrixJoinerElement({ 31 | code2DArray: Object.values(a3MulTable).map( 32 | columnObject => Object.values(columnObject).map( 33 | el => [el] 34 | ) 35 | ) 36 | }) 37 | 38 | // end 39 | --------------------------------------------------------------------------------