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 | 📂 Open file
3 | 💾 Save file
4 |
5 |
6 |
7 | ▶️ Run code
8 |
9 | Output:
10 | 📋 Copy output
11 |
12 |
13 |
14 | View on GitHub
15 |
21 |
25 |
27 |
28 | Instructions and controls:
29 |
30 |
31 |
32 | Tested in Chrome on Mac mostly. Opening and saving files
33 | only works in Chrome.
37 |
38 |
39 | Use your keyboard to type, backspace, arrow keys to move the caret,
40 | shift+arrow keys to select, ctrl+c and ctrl+v to copy paste.
41 |
42 | Use Alt or ⌥Control to bring up the editor choice dropdown.
43 |
44 | Use mouse and ctrl (in the colored graph editor) to interact with other
45 | editors. These editors should also support keyboard controls but don't yet.
46 |
47 |
48 | This is a research experiment and has many issues that may not be fixed in
49 | this iteration of the experiment, but please feel free to
50 | create an issue or reach out
53 | if you would like feedback on some behaviour of Polytope.
54 |
55 |
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 |
--------------------------------------------------------------------------------