muigui 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -7
- package/package.json +10 -6
- package/src/controllers/Button.js +34 -0
- package/src/controllers/Canvas.js +17 -0
- package/src/controllers/Checkbox.js +11 -0
- package/src/controllers/Color.js +31 -0
- package/src/controllers/ColorChooser.js +12 -0
- package/src/controllers/Container.js +58 -0
- package/src/controllers/Controller.js +138 -0
- package/src/controllers/Direction.js +23 -0
- package/src/controllers/Divider.js +9 -0
- package/src/controllers/Folder.js +37 -0
- package/src/controllers/Label.js +14 -0
- package/src/controllers/LabelController.js +32 -0
- package/src/controllers/PopDownController.js +84 -0
- package/src/controllers/RadioGrid.js +17 -0
- package/src/controllers/Range.js +11 -0
- package/src/controllers/Select.js +14 -0
- package/src/controllers/Slider.js +12 -0
- package/src/controllers/TabHolder.js +36 -0
- package/src/controllers/Text.js +10 -0
- package/src/controllers/TextNumber.js +18 -0
- package/src/controllers/ValueController.js +107 -0
- package/src/controllers/Vec2.js +50 -0
- package/src/controllers/create-controller.js +49 -0
- package/src/layout/Column.js +7 -0
- package/src/layout/Frame.js +11 -0
- package/src/layout/Grid.js +7 -0
- package/src/layout/Layout.js +47 -0
- package/src/layout/Row.js +7 -0
- package/src/libs/assert.js +5 -0
- package/src/libs/color-utils.js +406 -0
- package/src/libs/conversions.js +14 -0
- package/src/libs/css-utils.js +3 -0
- package/src/libs/elem.js +8 -3
- package/src/libs/emitter.js +68 -0
- package/src/libs/iterable-array.js +57 -0
- package/src/libs/key-values.js +25 -0
- package/src/libs/keyboard.js +32 -0
- package/src/libs/resize-helpers.js +22 -0
- package/src/libs/svg.js +33 -0
- package/src/libs/taskrunner.js +56 -0
- package/src/libs/touch.js +50 -0
- package/src/libs/utils.js +38 -2
- package/src/libs/wheel.js +10 -0
- package/src/muigui.js +79 -19
- package/src/styles/muigui.css.js +641 -0
- package/src/umd.js +3 -0
- package/src/views/CheckboxView.js +21 -0
- package/src/views/ColorChooserView.js +124 -0
- package/src/views/ColorView.js +50 -0
- package/src/views/DirectionView.js +127 -0
- package/src/views/EditView.js +100 -0
- package/src/views/ElementView.js +8 -0
- package/src/views/GridView.js +15 -0
- package/src/views/NumberView.js +67 -0
- package/src/views/RadioGridView.js +46 -0
- package/src/views/RangeView.js +73 -0
- package/src/views/SelectView.js +23 -0
- package/src/views/SliderView.js +194 -0
- package/src/views/TextView.js +49 -0
- package/src/views/ValueView.js +11 -0
- package/src/views/Vec2View.js +51 -0
- package/src/views/View.js +56 -0
- package/src/widgets/checkbox.js +0 -0
- package/src/widgets/divider.js +0 -0
- package/src/widgets/menu.js +0 -0
- package/src/widgets/radio.js +0 -0
- package/src/widgets/select.js +0 -1
- package/src/widgets/slider.js +0 -41
- package/src/widgets/text.js +0 -0
- package/src/widgets/widget.js +0 -51
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
|
|
2
|
+
const lerp = (a, b, t) => a + (b - a) * t;
|
|
3
|
+
const fract = v => v >= 0 ? v % 1 : 1 - (v % 1);
|
|
4
|
+
|
|
5
|
+
const f0 = v => +v.toFixed(0); // converts to string (eg 1.2 => "1"), then converts back to number (eg, "1.200" => 1.2)
|
|
6
|
+
const f3 = v => +v.toFixed(3); // converts to string (eg 1.2 => "1.200"), then converts back to number (eg, "1.200" => 1.2)
|
|
7
|
+
|
|
8
|
+
const hexToUint32RGB = v => (parseInt(v.substring(1, 3), 16) << 16) |
|
|
9
|
+
(parseInt(v.substring(3, 5), 16) << 8 ) |
|
|
10
|
+
(parseInt(v.substring(5, 7), 16) );
|
|
11
|
+
const uint32RGBToHex = v => `#${(Math.round(v)).toString(16).padStart(6, '0')}`;
|
|
12
|
+
|
|
13
|
+
export const hexToUint8RGB = v => [
|
|
14
|
+
parseInt(v.substring(1, 3), 16),
|
|
15
|
+
parseInt(v.substring(3, 5), 16),
|
|
16
|
+
parseInt(v.substring(5, 7), 16),
|
|
17
|
+
];
|
|
18
|
+
export const uint8RGBToHex = v => `#${Array.from(v).map(v => v.toString(16).padStart(2, '0')).join('')}`;
|
|
19
|
+
|
|
20
|
+
export const hexToFloatRGB = v => hexToUint8RGB(v).map(v => f3(v / 255));
|
|
21
|
+
export const floatRGBToHex = v => uint8RGBToHex(Array.from(v).map(v => Math.round(clamp(v * 255, 0, 255))));
|
|
22
|
+
|
|
23
|
+
const hexToObjectRGB = v => ({
|
|
24
|
+
r: parseInt(v.substring(1, 3), 16) / 255,
|
|
25
|
+
g: parseInt(v.substring(3, 5), 16) / 255,
|
|
26
|
+
b: parseInt(v.substring(5, 7), 16) / 255,
|
|
27
|
+
});
|
|
28
|
+
const scaleAndClamp = v => clamp(Math.round(v * 255), 0, 255).toString(16).padStart(2, '0');
|
|
29
|
+
const objectRGBToHex = v => `#${scaleAndClamp(v.r)}${scaleAndClamp(v.g)}${scaleAndClamp(v.b)}`;
|
|
30
|
+
|
|
31
|
+
const hexToCssRGB = v => `rgb(${hexToUint8RGB(v).join(', ')})`;
|
|
32
|
+
const cssRGBRegex = /^\s*rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\s*$/;
|
|
33
|
+
const cssRGBToHex = v => {
|
|
34
|
+
const m = cssRGBRegex.exec(v);
|
|
35
|
+
return uint8RGBToHex([m[1], m[2], m[3]].map(v => parseInt(v)));
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const hexToCssHSL = v => {
|
|
39
|
+
const hsl = rgbUint8ToHsl(hexToUint8RGB(v)).map(v => f0(v));
|
|
40
|
+
return `hsl(${hsl[0]}, ${hsl[1]}%, ${hsl[2]}%)`;
|
|
41
|
+
};
|
|
42
|
+
const cssHSLRegex = /^\s*hsl\(\s*(\d+)(?:deg|)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)\s*$/;
|
|
43
|
+
|
|
44
|
+
const hex3DigitTo6Digit = v => `${v[0]}${v[0]}${v[1]}${v[1]}${v[2]}${v[2]}`;
|
|
45
|
+
const cssHSLToHex = v => {
|
|
46
|
+
const m = cssHSLRegex.exec(v);
|
|
47
|
+
const rgb = hslToRgbUint8([m[1], m[2], m[3]].map(v => parseFloat(v)));
|
|
48
|
+
return uint8RGBToHex(rgb);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const euclideanModulo = (v, n) => ((v % n) + n) % n;
|
|
52
|
+
|
|
53
|
+
export function hslToRgbUint8([h, s, l]) {
|
|
54
|
+
h = euclideanModulo(h, 360);
|
|
55
|
+
s = clamp(s / 100, 0, 1);
|
|
56
|
+
l = clamp(l / 100, 0, 1);
|
|
57
|
+
|
|
58
|
+
const a = s * Math.min(l, 1 - l);
|
|
59
|
+
|
|
60
|
+
function f(n) {
|
|
61
|
+
const k = (n + h / 30) % 12;
|
|
62
|
+
return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return [f(0), f(8), f(4)].map(v => Math.round(v * 255));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function rgbFloatToHsl01([r, g, b]) {
|
|
69
|
+
const max = Math.max(r, g, b);
|
|
70
|
+
const min = Math.min(r, g, b);
|
|
71
|
+
const l = (min + max) * 0.5;
|
|
72
|
+
const d = max - min;
|
|
73
|
+
let h = 0;
|
|
74
|
+
let s = 0;
|
|
75
|
+
|
|
76
|
+
if (d !== 0) {
|
|
77
|
+
s = (l === 0 || l === 1)
|
|
78
|
+
? 0
|
|
79
|
+
: (max - l) / Math.min(l, 1 - l);
|
|
80
|
+
|
|
81
|
+
switch (max) {
|
|
82
|
+
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
|
|
83
|
+
case g: h = (b - r) / d + 2; break;
|
|
84
|
+
case b: h = (r - g) / d + 4;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return [h / 6, s, l];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const rgbUint8ToHsl = (rgb) => {
|
|
92
|
+
const [h, s, l] = rgbFloatToHsl01(rgb.map(v => v / 255));
|
|
93
|
+
return [h * 360, s * 100, l * 100];
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export function hsv01ToRGBFloat([hue, sat, val]) {
|
|
97
|
+
sat = clamp(sat, 0, 1);
|
|
98
|
+
val = clamp(val, 0, 1);
|
|
99
|
+
return [hue, hue + 2 / 3, hue + 1 / 3].map(
|
|
100
|
+
v => lerp(1, clamp(Math.abs(fract(v) * 6 - 3.0) - 1, 0, 1), sat) * val
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const round3 = v => Math.round(v * 1000) / 1000;
|
|
105
|
+
|
|
106
|
+
export function rgbFloatToHSV01([r, g, b]) {
|
|
107
|
+
const p = b > g
|
|
108
|
+
? [b, g, -1, 2 / 3]
|
|
109
|
+
: [g, b, 0, -1 / 3];
|
|
110
|
+
const q = p[0] > r
|
|
111
|
+
? [p[0], p[1], p[3], r]
|
|
112
|
+
: [r, p[1], p[2], p[0]];
|
|
113
|
+
const d = q[0] - Math.min(q[3], q[1]);
|
|
114
|
+
return [
|
|
115
|
+
Math.abs(q[2] + (q[3] - q[1]) / (6 * d + Number.EPSILON)),
|
|
116
|
+
d / (q[0] + Number.EPSILON),
|
|
117
|
+
q[0],
|
|
118
|
+
].map(round3);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
window.hsv01ToRGBFloat = hsv01ToRGBFloat;
|
|
122
|
+
window.rgbFloatToHSV01 = rgbFloatToHSV01;
|
|
123
|
+
|
|
124
|
+
const cssStringFormats = [
|
|
125
|
+
{ re: /^#(?:[0-9a-f]){6}$/i, format: 'hex6' },
|
|
126
|
+
{ re: /^(?:[0-9a-f]){6}$/i, format: 'hex6-no-hash' },
|
|
127
|
+
{ re: /^#(?:[0-9a-f]){3}$/i, format: 'hex3' },
|
|
128
|
+
{ re: /^(?:[0-9a-f]){3}$/i, format: 'hex3-no-hash' },
|
|
129
|
+
{ re: cssRGBRegex, format: 'css-rgb' },
|
|
130
|
+
{ re: cssHSLRegex, format: 'css-hsl' },
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
function guessStringColorFormat(v) {
|
|
134
|
+
for (const formatInfo of cssStringFormats) {
|
|
135
|
+
if (formatInfo.re.test(v)) {
|
|
136
|
+
return formatInfo;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function guessFormat(v) {
|
|
143
|
+
switch (typeof v) {
|
|
144
|
+
case 'number':
|
|
145
|
+
return 'uint32-rgb';
|
|
146
|
+
case 'string': {
|
|
147
|
+
const formatInfo = guessStringColorFormat(v.trim());
|
|
148
|
+
if (formatInfo) {
|
|
149
|
+
return formatInfo.format;
|
|
150
|
+
}
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
case 'object':
|
|
154
|
+
if (v instanceof Uint8Array || v instanceof Uint8ClampedArray) {
|
|
155
|
+
if (v.length === 3) {
|
|
156
|
+
return 'uint8-rgb';
|
|
157
|
+
}
|
|
158
|
+
} else if (v instanceof Float32Array) {
|
|
159
|
+
if (v.length === 3) {
|
|
160
|
+
return 'float-rgb';
|
|
161
|
+
}
|
|
162
|
+
} else if (Array.isArray(v)) {
|
|
163
|
+
if (v.length === 3) {
|
|
164
|
+
return 'float-rgb';
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
if ('r' in v && 'g' in v && 'b' in v) {
|
|
168
|
+
return 'object-rgb';
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
throw new Error(`unknown color format: ${v}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function fixHex6(v) {
|
|
176
|
+
return v.trim(v);
|
|
177
|
+
//const formatInfo = guessStringColorFormat(v.trim());
|
|
178
|
+
//const fix = formatInfo ? formatInfo.fix : v => v;
|
|
179
|
+
//return fix(v.trim());
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function hex6ToHex3(hex6) {
|
|
183
|
+
return (hex6[1] === hex6[2] &&
|
|
184
|
+
hex6[3] === hex6[4] &&
|
|
185
|
+
hex6[5] === hex6[6])
|
|
186
|
+
? `#${hex6[1]}${hex6[3]}${hex6[5]}`
|
|
187
|
+
: hex6;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const hex3RE = /^(#|)([0-9a-f]{3})$/i;
|
|
191
|
+
function hex3ToHex6(hex3) {
|
|
192
|
+
const m = hex3RE.exec(hex3);
|
|
193
|
+
if (m) {
|
|
194
|
+
const [, , m2] = m;
|
|
195
|
+
return `#${hex3DigitTo6Digit(m2)}`;
|
|
196
|
+
}
|
|
197
|
+
return hex3;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function fixHex3(v) {
|
|
201
|
+
return hex6ToHex3(fixHex6(v));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const strToRGBObject = (s) => {
|
|
205
|
+
try {
|
|
206
|
+
const json = s.replace(/([a-z])/g, '"$1"');
|
|
207
|
+
const rgb = JSON.parse(json);
|
|
208
|
+
if (Number.isNaN(rgb.r) || Number.isNaN(rgb.g) || Number.isNaN(rgb.b)) {
|
|
209
|
+
throw new Error('not {r, g, b}');
|
|
210
|
+
}
|
|
211
|
+
return [true, rgb];
|
|
212
|
+
} catch (e) {
|
|
213
|
+
return [false];
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const strToCssRGB = s => {
|
|
218
|
+
const m = cssRGBRegex.exec(s);
|
|
219
|
+
if (!m) {
|
|
220
|
+
return [false];
|
|
221
|
+
}
|
|
222
|
+
const v = [m[1], m[2], m[3]].map(v => parseInt(v));
|
|
223
|
+
const outOfRange = v.find(v => v > 255);
|
|
224
|
+
return [!outOfRange, `rgb(${v.join(', ')})`];
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const strToCssHSL = s => {
|
|
228
|
+
const m = cssHSLRegex.exec(s);
|
|
229
|
+
if (!m) {
|
|
230
|
+
return [false];
|
|
231
|
+
}
|
|
232
|
+
const v = [m[1], m[2], m[3]].map(v => parseFloat(v));
|
|
233
|
+
const outOfRange = v.find(v => Number.isNaN(v));
|
|
234
|
+
return [!outOfRange, `hsl(${v[0]}, ${v[1]}%, ${v[2]}%)`];
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const rgbObjectToStr = rgb => {
|
|
238
|
+
return `{r:${f3(rgb.r)}, g:${f3(rgb.g)}, b:${f3(rgb.b)}}`;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const strTo3IntsRE = /^\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*$/;
|
|
242
|
+
const strTo3Ints = s => {
|
|
243
|
+
const m = strTo3IntsRE.exec(s);
|
|
244
|
+
if (!m) {
|
|
245
|
+
return [false];
|
|
246
|
+
}
|
|
247
|
+
const v = [m[1], m[2], m[3]].map(v => parseInt(v));
|
|
248
|
+
const outOfRange = v.find(v => v > 255);
|
|
249
|
+
return [!outOfRange, v];
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const strTo3Floats = s => {
|
|
253
|
+
const numbers = s.split(',').map(s => s.trim());
|
|
254
|
+
const v = numbers.map(v => parseFloat(v));
|
|
255
|
+
if (v.length !== 3) {
|
|
256
|
+
return [false];
|
|
257
|
+
}
|
|
258
|
+
// Note: using isNaN not Number.isNaN
|
|
259
|
+
const badNdx = numbers.findIndex(v => isNaN(v));
|
|
260
|
+
return [badNdx < 0, v.map(v => f3(v))];
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const strToUint32RGBRegex = /^\s*(?:0x){0,1}([0-9a-z]{1,6})\s*$/i;
|
|
264
|
+
const strToUint32RGB = s => {
|
|
265
|
+
const m = strToUint32RGBRegex.exec(s);
|
|
266
|
+
if (!m) {
|
|
267
|
+
return [false];
|
|
268
|
+
}
|
|
269
|
+
return [true, parseInt(m[1], 16)];
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const hexRE = /^\s*#[a-f0-9]{6}\s*$|^\s*#[a-f0-9]{3}\s*$/i;
|
|
273
|
+
const hexNoHashRE = /^\s*[a-f0-9]{6}\s*$/i;
|
|
274
|
+
|
|
275
|
+
// For each format converter
|
|
276
|
+
//
|
|
277
|
+
// fromHex/toHex convert from/to '#RRGGBB'
|
|
278
|
+
//
|
|
279
|
+
// fromHex converts from the string '#RRBBGG' to the format
|
|
280
|
+
// (eg: for uint32-rgb, '#123456' becomes 0x123456)
|
|
281
|
+
//
|
|
282
|
+
// toHex converts from the format to '#RRGGBB'
|
|
283
|
+
// (eg: for uint8-rgb, [16, 33, 50] becomes '#102132')
|
|
284
|
+
//
|
|
285
|
+
//
|
|
286
|
+
// fromStr/toStr convert from/to what's in the input[type=text] element
|
|
287
|
+
//
|
|
288
|
+
// toStr converts from the format to its string representation
|
|
289
|
+
// (eg, for object-rgb, {r: 1, g: 0.5, b:0} becomes "{r: 1, g: 0.5, b:0}")
|
|
290
|
+
// ^object ^string
|
|
291
|
+
//
|
|
292
|
+
// fromStr converts its string representation to its format
|
|
293
|
+
// (eg, for object-rgb) "{r: 1, g: 0.5, b:0}" becomes {r: 1, g: 0.5, b:0})
|
|
294
|
+
// ^string ^object
|
|
295
|
+
// fromString returns an array which is [valid, v]
|
|
296
|
+
// where valid is true if the string was a valid and v is the converted
|
|
297
|
+
// format if v is true.
|
|
298
|
+
//
|
|
299
|
+
// Note: toStr should convert to "ideal" form (whatever that is).
|
|
300
|
+
// (eg, for css-rgb
|
|
301
|
+
// "{ r: 0.10000, g: 001, b: 0}" becomes "{r: 0.1, g: 1, b: 0}"
|
|
302
|
+
// notice that css-rgb is a string to a string
|
|
303
|
+
// )
|
|
304
|
+
export const colorFormatConverters = {
|
|
305
|
+
'hex6': {
|
|
306
|
+
color: {
|
|
307
|
+
from: v => [true, v],
|
|
308
|
+
to: fixHex6,
|
|
309
|
+
},
|
|
310
|
+
text: {
|
|
311
|
+
from: v => [hexRE.test(v), v.trim()],
|
|
312
|
+
to: v => v,
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
'hex3': {
|
|
316
|
+
color: {
|
|
317
|
+
from: v => [true, fixHex3(v)],
|
|
318
|
+
to: hex3ToHex6,
|
|
319
|
+
},
|
|
320
|
+
text: {
|
|
321
|
+
from: v => [hexRE.test(v), hex6ToHex3(v.trim())],
|
|
322
|
+
to: v => v,
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
'hex6-no-hash': {
|
|
326
|
+
color: {
|
|
327
|
+
from: v => [true, v.substring(1)],
|
|
328
|
+
to: v => `#${fixHex6(v)}`,
|
|
329
|
+
},
|
|
330
|
+
text: {
|
|
331
|
+
from: v => [hexNoHashRE.test(v), v.trim()],
|
|
332
|
+
to: v => v,
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
'hex3-no-hash': {
|
|
336
|
+
color: {
|
|
337
|
+
from: v => [true, fixHex3(v).substring(1)],
|
|
338
|
+
to: hex3ToHex6,
|
|
339
|
+
},
|
|
340
|
+
text: {
|
|
341
|
+
from: v => [hexNoHashRE.test(v), hex6ToHex3(v.trim())],
|
|
342
|
+
to: v => v,
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
'uint32-rgb': {
|
|
346
|
+
color: {
|
|
347
|
+
from: v => [true, hexToUint32RGB(v)],
|
|
348
|
+
to: uint32RGBToHex,
|
|
349
|
+
},
|
|
350
|
+
text: {
|
|
351
|
+
from: v => strToUint32RGB(v),
|
|
352
|
+
to: v => `0x${v.toString(16).padStart(6, '0')}`,
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
'uint8-rgb': {
|
|
356
|
+
color: {
|
|
357
|
+
from: v => [true, hexToUint8RGB(v)],
|
|
358
|
+
to: uint8RGBToHex,
|
|
359
|
+
},
|
|
360
|
+
text: {
|
|
361
|
+
from: strTo3Ints,
|
|
362
|
+
to: v => v.join(', '),
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
'float-rgb': {
|
|
366
|
+
color: {
|
|
367
|
+
from: v => [true, hexToFloatRGB(v)],
|
|
368
|
+
to: floatRGBToHex,
|
|
369
|
+
},
|
|
370
|
+
text: {
|
|
371
|
+
from: strTo3Floats,
|
|
372
|
+
// need Array.from because map of Float32Array makes a Float32Array
|
|
373
|
+
to: v => Array.from(v).map(v => f3(v)).join(', '),
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
'object-rgb': {
|
|
377
|
+
color: {
|
|
378
|
+
from: v => [true, hexToObjectRGB(v)],
|
|
379
|
+
to: objectRGBToHex,
|
|
380
|
+
},
|
|
381
|
+
text: {
|
|
382
|
+
from: strToRGBObject,
|
|
383
|
+
to: rgbObjectToStr,
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
'css-rgb': {
|
|
387
|
+
color: {
|
|
388
|
+
from: v => [true, hexToCssRGB(v)],
|
|
389
|
+
to: cssRGBToHex,
|
|
390
|
+
},
|
|
391
|
+
text: {
|
|
392
|
+
from: strToCssRGB,
|
|
393
|
+
to: v => strToCssRGB(v)[1],
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
'css-hsl': {
|
|
397
|
+
color: {
|
|
398
|
+
from: v => [true, hexToCssHSL(v)],
|
|
399
|
+
to: cssHSLToHex,
|
|
400
|
+
},
|
|
401
|
+
text: {
|
|
402
|
+
from: strToCssHSL,
|
|
403
|
+
to: v => strToCssHSL(v)[1],
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const identity = {
|
|
2
|
+
to: v => v,
|
|
3
|
+
from: v => [true, v],
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
// from: from string to value
|
|
7
|
+
// to: from value to string
|
|
8
|
+
export const strToNumber = {
|
|
9
|
+
to: v => v.toString(),
|
|
10
|
+
from: v => {
|
|
11
|
+
const newV = parseFloat(v);
|
|
12
|
+
return [!Number.isNaN(newV), newV];
|
|
13
|
+
},
|
|
14
|
+
};
|
package/src/libs/elem.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
export function
|
|
2
|
-
const elem = document.createElement(tag);
|
|
1
|
+
export function setElemProps(elem, attrs, children) {
|
|
3
2
|
for (const [key, value] of Object.entries(attrs)) {
|
|
4
3
|
if (typeof value === 'function' && key.startsWith('on')) {
|
|
5
4
|
const eventName = key.substring(2).toLowerCase();
|
|
@@ -19,7 +18,13 @@ export function createElem(tag, attrs = {}, children = []) {
|
|
|
19
18
|
}
|
|
20
19
|
return elem;
|
|
21
20
|
}
|
|
22
|
-
|
|
21
|
+
|
|
22
|
+
export function createElem(tag, attrs = {}, children = []) {
|
|
23
|
+
const elem = document.createElement(tag);
|
|
24
|
+
setElemProps(elem, attrs, children);
|
|
25
|
+
return elem;
|
|
26
|
+
}
|
|
27
|
+
|
|
23
28
|
export function addElem(tag, parent, attrs = {}, children = []) {
|
|
24
29
|
const elem = createElem(tag, attrs, children);
|
|
25
30
|
parent.appendChild(elem);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { removeArrayElem } from '../libs/utils.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Similar to EventSource
|
|
5
|
+
*/
|
|
6
|
+
export default class Emitter {
|
|
7
|
+
#listeners;
|
|
8
|
+
#changes;
|
|
9
|
+
#receivers;
|
|
10
|
+
#emitting;
|
|
11
|
+
|
|
12
|
+
constructor() {
|
|
13
|
+
this.#listeners = {};
|
|
14
|
+
this.#changes = [];
|
|
15
|
+
this.#receivers = [];
|
|
16
|
+
}
|
|
17
|
+
on(type, listener) {
|
|
18
|
+
if (this.#emitting) {
|
|
19
|
+
this.#changes.push(['add', type, listener]);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const listeners = this.#listeners[type] || [];
|
|
23
|
+
listeners.push(listener);
|
|
24
|
+
this.#listeners[type] = listeners;
|
|
25
|
+
}
|
|
26
|
+
addListener(type, listener) {
|
|
27
|
+
return this.on(type, listener);
|
|
28
|
+
}
|
|
29
|
+
removeListener(type, listener) {
|
|
30
|
+
if (this.#emitting) {
|
|
31
|
+
this.#changes.push(['remove', type, listener]);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const listeners = this.#listeners[type];
|
|
35
|
+
if (listeners) {
|
|
36
|
+
removeArrayElem(listeners, listener);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
propagate(receiver) {
|
|
40
|
+
this.#receivers.push(receiver);
|
|
41
|
+
}
|
|
42
|
+
emit(type, ...args) {
|
|
43
|
+
this.#emitting = true;
|
|
44
|
+
const listeners = this.#listeners[type];
|
|
45
|
+
if (listeners) {
|
|
46
|
+
for (const listener of listeners) {
|
|
47
|
+
listener(...args);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
this.#emitting = false;
|
|
51
|
+
while (this.#changes.length) {
|
|
52
|
+
const [cmd, type, listener] = this.#changes.shift();
|
|
53
|
+
switch (cmd) {
|
|
54
|
+
case 'add':
|
|
55
|
+
this.on(type, listener);
|
|
56
|
+
break;
|
|
57
|
+
case 'remove':
|
|
58
|
+
this.removeListener(type, listener);
|
|
59
|
+
break;
|
|
60
|
+
default:
|
|
61
|
+
throw new Error('unknown type');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const receiver of this.#receivers) {
|
|
65
|
+
receiver.emit(type, ...args);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { removeArrayElem } from './utils.js';
|
|
2
|
+
|
|
3
|
+
export default class IterableArray {
|
|
4
|
+
#arr;
|
|
5
|
+
#added;
|
|
6
|
+
#removedSet;
|
|
7
|
+
#iterating;
|
|
8
|
+
|
|
9
|
+
constructor() {
|
|
10
|
+
this.#arr = [];
|
|
11
|
+
this.#added = [];
|
|
12
|
+
this.#removedSet = new Set();
|
|
13
|
+
}
|
|
14
|
+
add(elem) {
|
|
15
|
+
if (this.#iterating) {
|
|
16
|
+
this.#removedSet.delete(elem);
|
|
17
|
+
this.#added.push(elem);
|
|
18
|
+
} else {
|
|
19
|
+
this.#arr.push(elem);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
remove(elem) {
|
|
23
|
+
if (this.#iterating) {
|
|
24
|
+
removeArrayElem(this.#added, elem);
|
|
25
|
+
this.#removedSet.add(elem);
|
|
26
|
+
} else {
|
|
27
|
+
removeArrayElem(this.#arr, elem);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
#process(arr, fn) {
|
|
31
|
+
for (const elem of arr) {
|
|
32
|
+
if (!this.#removedSet.has(elem)) {
|
|
33
|
+
if (fn(elem) === false) {
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
forEach(fn) {
|
|
40
|
+
this.#iterating = true;
|
|
41
|
+
this.#process(this.#arr, fn);
|
|
42
|
+
do {
|
|
43
|
+
if (this.#removedSet.size) {
|
|
44
|
+
for (const elem of this.#removedSet.values()) {
|
|
45
|
+
removeArrayElem(this.#arr, elem);
|
|
46
|
+
}
|
|
47
|
+
this.#removedSet.clear();
|
|
48
|
+
}
|
|
49
|
+
if (this.#added.length) {
|
|
50
|
+
const added = this.#added;
|
|
51
|
+
this.#added = [];
|
|
52
|
+
this.#process(added, fn);
|
|
53
|
+
}
|
|
54
|
+
} while (this.#added.length || this.#removedSet.size);
|
|
55
|
+
this.#iterating = false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
|
|
2
|
+
// 4 cases
|
|
3
|
+
// (a) keyValues is array of arrays, each sub array is key value
|
|
4
|
+
// (b) keyValues is array and value is number then keys = array contents, value = index
|
|
5
|
+
// (c) keyValues is array and value is not number, key = array contents, value = array contents
|
|
6
|
+
// (d) keyValues is object then key->value
|
|
7
|
+
export function convertToKeyValues(keyValues, valueIsNumber) {
|
|
8
|
+
if (Array.isArray(keyValues)) {
|
|
9
|
+
if (Array.isArray(keyValues[0])) {
|
|
10
|
+
// (a) keyValues is array of arrays, each sub array is key value
|
|
11
|
+
return keyValues;
|
|
12
|
+
} else {
|
|
13
|
+
if (valueIsNumber) {
|
|
14
|
+
// (b) keyValues is array and value is number then keys = array contents, value = index
|
|
15
|
+
return keyValues.map((v, ndx) => [v, ndx]);
|
|
16
|
+
} else {
|
|
17
|
+
// (c) keyValues is array and value is not number, key = array contents, value = array contents
|
|
18
|
+
return keyValues.map(v => [v, v]);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
} else {
|
|
22
|
+
// (d)
|
|
23
|
+
return [...Object.entries(keyValues)];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
function noop() {
|
|
2
|
+
}
|
|
3
|
+
|
|
4
|
+
const keyDirections = {
|
|
5
|
+
ArrowLeft: [-1, 0],
|
|
6
|
+
ArrowRight: [1, 0],
|
|
7
|
+
ArrowUp: [0, -1],
|
|
8
|
+
ArrowDown: [0, 1],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// This probably needs to be global
|
|
12
|
+
export function addKeyboardEvents(elem, {onDown = noop, onUp = noop}) {
|
|
13
|
+
const keyDown = function(event) {
|
|
14
|
+
const mult = event.shiftKey ? 10 : 1;
|
|
15
|
+
const [dx, dy] = (keyDirections[event.key] || [0, 0]).map(v => v * mult);
|
|
16
|
+
const fn = event.type === 'keydown' ? onDown : onUp;
|
|
17
|
+
fn({
|
|
18
|
+
type: event.type.substring(3),
|
|
19
|
+
dx,
|
|
20
|
+
dy,
|
|
21
|
+
event,
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
elem.addEventListener('keydown', keyDown);
|
|
26
|
+
elem.addEventListener('keyup', keyDown);
|
|
27
|
+
|
|
28
|
+
return function() {
|
|
29
|
+
elem.removeEventListener('keydown', keyDown);
|
|
30
|
+
elem.removeEventListener('keyup', keyDown);
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function onResize(elem, callback) {
|
|
2
|
+
new ResizeObserver(() => {
|
|
3
|
+
callback({rect: elem.getBoundingClientRect(), elem});
|
|
4
|
+
}).observe(elem);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function onResizeSVGNoScale(elem, hAnchor, vAnchor, callback) {
|
|
8
|
+
onResize(elem, ({rect}) => {
|
|
9
|
+
const {width, height} = rect;
|
|
10
|
+
elem.setAttribute('viewBox', `-${width * hAnchor} -${height * vAnchor} ${width} ${height}`);
|
|
11
|
+
callback({elem, rect});
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function onResizeCanvas(elem, callback) {
|
|
16
|
+
onResize(elem, ({rect}) => {
|
|
17
|
+
const {width, height} = rect;
|
|
18
|
+
elem.width = width;
|
|
19
|
+
elem.height = height;
|
|
20
|
+
callback({elem, rect});
|
|
21
|
+
});
|
|
22
|
+
}
|
package/src/libs/svg.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { assert } from '../libs/assert.js';
|
|
2
|
+
|
|
3
|
+
function getEllipsePointForAngle(cx, cy, rx, ry, phi, theta) {
|
|
4
|
+
const m = Math.abs(rx) * Math.cos(theta);
|
|
5
|
+
const n = Math.abs(ry) * Math.sin(theta);
|
|
6
|
+
|
|
7
|
+
return [
|
|
8
|
+
cx + Math.cos(phi) * m - Math.sin(phi) * n,
|
|
9
|
+
cy + Math.sin(phi) * m + Math.cos(phi) * n,
|
|
10
|
+
];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getEndpointParameters(cx, cy, rx, ry, phi, theta, dTheta) {
|
|
14
|
+
const [x1, y1] = getEllipsePointForAngle(cx, cy, rx, ry, phi, theta);
|
|
15
|
+
const [x2, y2] = getEllipsePointForAngle(cx, cy, rx, ry, phi, theta + dTheta);
|
|
16
|
+
|
|
17
|
+
const fa = Math.abs(dTheta) > Math.PI ? 1 : 0;
|
|
18
|
+
const fs = dTheta > 0 ? 1 : 0;
|
|
19
|
+
|
|
20
|
+
return { x1, y1, x2, y2, fa, fs };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function arc(cx, cy, r, start, end) {
|
|
24
|
+
assert(Math.abs(start - end) <= Math.PI * 2);
|
|
25
|
+
assert(start >= -Math.PI && start <= Math.PI * 2);
|
|
26
|
+
assert(start <= end);
|
|
27
|
+
assert(end >= -Math.PI && end <= Math.PI * 4);
|
|
28
|
+
|
|
29
|
+
const { x1, y1, x2, y2, fa, fs } = getEndpointParameters(cx, cy, r, r, 0, start, end - start);
|
|
30
|
+
return Math.abs(Math.abs(start - end) - Math.PI * 2) > Number.EPSILON
|
|
31
|
+
? `M${cx} ${cy} L${x1} ${y1} A ${r} ${r} 0 ${fa} ${fs} ${x2} ${y2} L${cx} ${cy}`
|
|
32
|
+
: `M${x1} ${y1} L${x1} ${y1} A ${r} ${r} 0 ${fa} ${fs} ${x2} ${y2}`;
|
|
33
|
+
}
|