@the_dissidents/svelte-ui 0.0.1

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.
@@ -0,0 +1,418 @@
1
+ <script lang="ts" module>import * as Color from "colorjs.io/fn";
2
+ function tryRegister(s) {
3
+ console.log(':', s.id);
4
+ if (!Color.ColorSpace.registry[s.id]) {
5
+ Color.ColorSpace.register(s);
6
+ console.log('register', s.id);
7
+ }
8
+ }
9
+ tryRegister(Color.sRGB);
10
+ tryRegister(Color.HSL);
11
+ tryRegister(Color.OKLCH);
12
+ </script>
13
+
14
+ <script lang="ts">import { untrack } from "svelte";
15
+ import Popup from "./Popup.svelte";
16
+ import NumberInput from "./NumberInput.svelte";
17
+ import Tooltip from "./Tooltip.svelte";
18
+ import { I18n } from "./I18n.svelte";
19
+ const modes = {
20
+ srgb: {
21
+ bounds: [255, 255, 255],
22
+ expr: (v0, v1, v2, alpha = 1) => `rgb(${v0.toFixed(2)} ${v1.toFixed(2)} ${v2.toFixed(2)} / ${alpha})`,
23
+ fromColorIo: (v0, v1, v2) => [(v0 ?? 0) * 255, (v1 ?? 0) * 255, (v2 ?? 0) * 255],
24
+ interpolationMode: ['in srgb', 'in srgb', 'in srgb'],
25
+ },
26
+ hsl: {
27
+ bounds: [360, 100, 100],
28
+ expr: (v0, v1, v2, alpha = 1) => `hsl(${v0.toFixed(2)}deg ${v1.toFixed(3)}% ${v2.toFixed(3)}% / ${alpha})`,
29
+ fromColorIo: (v0, v1, v2) => [v0 ?? 0, v1 ?? 0, v2 ?? 0],
30
+ interpolationMode: ['in hsl longer hue', 'in hsl', 'in hsl'],
31
+ },
32
+ oklch: {
33
+ bounds: [1, 0.4, 360],
34
+ expr: (v0, v1, v2, alpha = 1) => `oklch(${v0.toFixed(3)} ${v1.toFixed(3)} ${v2.toFixed(2)} / ${alpha})`,
35
+ fromColorIo: (v0, v1, v2) => [v0 ?? 0, v1 ?? 0, v2 ?? 0],
36
+ interpolationMode: ['in oklch', 'in oklch', 'in oklch longer hue'],
37
+ },
38
+ };
39
+ function convertMode(to) {
40
+ console.log('converting to', to, mode);
41
+ const newColor = Color.to(Color.parse(modes[mode].expr(value0, value1, value2)), to, { inGamut: true });
42
+ newColor.alpha = alpha;
43
+ updateFromColor(newColor);
44
+ computeBoundaries();
45
+ }
46
+ function parseHex() {
47
+ let match = /#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/.exec(hex.toLowerCase());
48
+ if (match) {
49
+ const newColor = Color.getColor(hex);
50
+ newColor.alpha = alpha;
51
+ changed = true;
52
+ updateFromColor(newColor);
53
+ computeBoundaries();
54
+ oninput?.(color);
55
+ }
56
+ else {
57
+ updateFromValues();
58
+ }
59
+ }
60
+ function computeBoundaries() {
61
+ range0 = getGamutBoundary(modes[mode].bounds[0], (x) => [x, value1, value2]);
62
+ range1 = getGamutBoundary(modes[mode].bounds[1], (x) => [value0, x, value2]);
63
+ range2 = getGamutBoundary(modes[mode].bounds[2], (x) => [value0, value1, x], mode == 'oklch');
64
+ }
65
+ function updateFromValues() {
66
+ color = Color.getColor(modes[mode].expr(value0, value1, value2, alpha));
67
+ changed = true;
68
+ updateTexts();
69
+ oninput?.(color);
70
+ }
71
+ function updateFromColor(c) {
72
+ if (color !== c)
73
+ color = c;
74
+ let modeChanged = false;
75
+ let normalized;
76
+ if (c.space.id !== mode && c.space.id in modes) {
77
+ mode = c.space.id;
78
+ modeChanged = true;
79
+ normalized = Color.to(c, mode, { inGamut: true });
80
+ }
81
+ else
82
+ normalized = Color.toGamut(c);
83
+ [value0, value1, value2] = modes[mode].fromColorIo(...normalized.coords);
84
+ alpha = c.alpha ?? 1;
85
+ if (Number.isNaN(value0))
86
+ value0 = 0;
87
+ if (Number.isNaN(value1))
88
+ value1 = 0;
89
+ if (Number.isNaN(value2))
90
+ value2 = 0;
91
+ updateTexts();
92
+ if (modeChanged)
93
+ computeBoundaries();
94
+ }
95
+ function updateTexts() {
96
+ let srgb = Color.to(color, Color.sRGB);
97
+ outOfGamut = !Color.inGamut(srgb);
98
+ srgb = Color.toGamut(srgb);
99
+ const [r, g, b] = srgb.coords;
100
+ hex = `#${Math.round((r ?? 0) * 255).toString(16).padStart(2, '0')}${Math.round((g ?? 0) * 255).toString(16).padStart(2, '0')}${Math.round((b ?? 0) * 255).toString(16).padStart(2, '0')}`;
101
+ }
102
+ function getGamutBoundary01(fun, precise = false) {
103
+ const EPISION = 1 / 256;
104
+ const isInside = (x) => Color.inGamut(Color.parse(modes[mode].expr(...fun(x))), Color.sRGB);
105
+ function findSample(insideness) {
106
+ let division = 1;
107
+ while (true) {
108
+ const len = 1 / division;
109
+ if (len <= EPISION)
110
+ return null;
111
+ for (let i = 0; i < division; i++) {
112
+ const m = len * (i + 0.5);
113
+ if (isInside(m) === insideness)
114
+ return m;
115
+ }
116
+ division *= 2;
117
+ }
118
+ }
119
+ function findTransition(xOut, xIn) {
120
+ while (Math.abs(xOut - xIn) > EPISION) {
121
+ const m = (xOut + xIn) / 2;
122
+ if (isInside(m))
123
+ xIn = m;
124
+ else
125
+ xOut = m;
126
+ }
127
+ return xIn;
128
+ }
129
+ const leftIn = isInside(0), rightIn = isInside(1);
130
+ if (leftIn && rightIn) {
131
+ if (precise) {
132
+ const m = findSample(false);
133
+ if (m === null)
134
+ return [0, 1, false];
135
+ return [findTransition(m, 0), findTransition(m, 1), true];
136
+ }
137
+ else
138
+ return [0, 1, false];
139
+ }
140
+ if (leftIn)
141
+ return [0, findTransition(1, 0), false];
142
+ if (rightIn)
143
+ return [findTransition(0, 1), 1, false];
144
+ let m = findSample(true);
145
+ if (m === null)
146
+ return null;
147
+ return [findTransition(0, m), findTransition(1, m), false];
148
+ }
149
+ function getGamutBoundary(bound, fun, precise = false) {
150
+ const x = getGamutBoundary01((x) => fun(x * bound), precise);
151
+ if (x === null)
152
+ return null;
153
+ return [x[0] * bound, x[1] * bound, x[2]];
154
+ }
155
+ function getRangeGradient(range, bound) {
156
+ const OUT = 'gray';
157
+ const IN = 'transparent';
158
+ if (!range)
159
+ return OUT;
160
+ let [a, b, c] = range;
161
+ a *= 100 / bound;
162
+ b *= 100 / bound;
163
+ return c
164
+ ? `linear-gradient(to right, ${IN} ${a}%, ${OUT} ${a}%, ${OUT} ${b}%, ${IN} ${b}%)`
165
+ : `linear-gradient(to right, ${OUT} ${a}%, ${IN} ${a}%, ${IN} ${b}%, ${OUT} ${b}%)`;
166
+ }
167
+ ;
168
+ let { mode = $bindable('srgb'), oninput, onchange, color = $bindable() } = $props();
169
+ $effect(() => {
170
+ let _color = color;
171
+ untrack(() => {
172
+ updateFromColor(_color);
173
+ computeBoundaries();
174
+ });
175
+ });
176
+ let value0 = $state(0), value1 = $state(0), value2 = $state(0), alpha = $state(1), range0 = $state([0, 1, false]), range1 = $state([0, 1, false]), range2 = $state([0, 1, false]), hex = $state(''), outOfGamut = $state(false);
177
+ let changed = false;
178
+ let popupHandler;
179
+ </script>
180
+
181
+ <button class="preview-btn" aria-label="color"
182
+ style="--color: {modes[mode].expr(value0, value1, value2, 1)};
183
+ --trspColor: {modes[mode].expr(value0, value1, value2, alpha)};"
184
+ onclick={(e) => {
185
+ const self = e.currentTarget;
186
+ const rect = self.getBoundingClientRect();
187
+ changed = false;
188
+ popupHandler.openAt?.(rect.left, rect.bottom, Math.max(300, rect.width));
189
+ }}
190
+ ></button>
191
+
192
+ <Popup bind:this={popupHandler} maxWidth="none"
193
+ onclose={() => {
194
+ if (changed) {
195
+ changed = false;
196
+ onchange?.(color);
197
+ }
198
+ }}>
199
+ <div class="hlayout">
200
+ <select value={mode} onchange={(ev) => convertMode(ev.currentTarget.value as ColorMode)}>
201
+ {#each Object.keys(modes) as m (m)}
202
+ <option value={m}>{m}</option>
203
+ {/each}
204
+ </select>
205
+ <hr class="flexgrow"/>
206
+ </div>
207
+ <div class='outer hlayout'>
208
+ <div class='vlayout flexgrow'>
209
+ <div class="value-group">
210
+ <div class='slider-container'>
211
+ <span class='back' style="background: {getRangeGradient(range0, modes[mode].bounds[0])}">
212
+ <span class='coloring'
213
+ style="background: linear-gradient(90deg {modes[mode].interpolationMode[0]}, {
214
+ modes[mode].expr(0, value1, value2)}, {
215
+ modes[mode].expr(modes[mode].bounds[0], value1, value2)});">
216
+ </span>
217
+ </span>
218
+ <input type="range" bind:value={value0}
219
+ min="0" max={modes[mode].bounds[0]} step="0.001"
220
+ oninput={() => {
221
+ updateFromValues();
222
+ range1 = getGamutBoundary(modes[mode].bounds[1], (x) => [value0, x, value2]);
223
+ range2 = getGamutBoundary(modes[mode].bounds[2], (x) => [value0, value1, x], mode == 'oklch');
224
+ }} />
225
+ </div>
226
+ <NumberInput bind:value={value0} width="10ch"
227
+ min="0" max={modes[mode].bounds[0]} step="0.001" />
228
+ </div>
229
+
230
+ <div class="value-group">
231
+ <div class='slider-container'>
232
+ <span class='back' style="background: {getRangeGradient(range1, modes[mode].bounds[1])}">
233
+ <span class='coloring'
234
+ style="background: linear-gradient(90deg {modes[mode].interpolationMode[1]}, {
235
+ modes[mode].expr(value0, 0, value2)}, {
236
+ modes[mode].expr(value0, modes[mode].bounds[1], value2)});">
237
+ </span>
238
+ </span>
239
+ <input type="range" bind:value={value1}
240
+ min="0" max={modes[mode].bounds[1]} step="0.001"
241
+ oninput={() => {
242
+ updateFromValues();
243
+ range0 = getGamutBoundary(modes[mode].bounds[0], (x) => [x, value1, value2]);
244
+ range2 = getGamutBoundary(modes[mode].bounds[2], (x) => [value0, value1, x], mode == 'oklch');
245
+ }} />
246
+ </div>
247
+ <NumberInput bind:value={value1} width="10ch"
248
+ min="0" max={modes[mode].bounds[1]} step="0.001" />
249
+ </div>
250
+
251
+ <div class="value-group">
252
+ <div class='slider-container'>
253
+ <span class='back' style="background: {getRangeGradient(range2, modes[mode].bounds[2])}">
254
+ <span class='coloring'
255
+ style="background: linear-gradient(90deg {modes[mode].interpolationMode[2]}, {
256
+ modes[mode].expr(value0, value1, 0)}, {
257
+ modes[mode].expr(value0, value1, modes[mode].bounds[2])});">
258
+ </span>
259
+ </span>
260
+ <input type="range" bind:value={value2}
261
+ min="0" max={modes[mode].bounds[2]} step="0.001"
262
+ oninput={() => {
263
+ updateFromValues();
264
+ range0 = getGamutBoundary(modes[mode].bounds[0], (x) => [x, value1, value2]);
265
+ range1 = getGamutBoundary(modes[mode].bounds[1], (x) => [value0, x, value2]);
266
+ }} />
267
+ </div>
268
+ <NumberInput bind:value={value2} width="10ch"
269
+ min="0" max={modes[mode].bounds[2]} step="0.001" />
270
+ </div>
271
+
272
+ <div class="value-group">
273
+ <div class='slider-container'>
274
+ <span class='back' style="background: transparent;">
275
+ <span class='coloring alpha'
276
+ style="--grad: linear-gradient(90deg, {
277
+ modes[mode].expr(value0, value1, value2, 0)}, {
278
+ modes[mode].expr(value0, value1, value2, 1)});">
279
+ </span>
280
+ </span>
281
+ <input type="range" bind:value={alpha}
282
+ min="0" max="1" step="0.001"
283
+ oninput={() => {
284
+ updateFromValues();
285
+ }}
286
+ />
287
+ </div>
288
+ <NumberInput bind:value={alpha} width="10ch"
289
+ min="0" max="1" step="0.001" />
290
+ </div>
291
+ </div>
292
+ </div>
293
+ <div class='value-group codes'>
294
+ <input class="flexgrow" type="text" disabled
295
+ value={modes[mode].expr(value0, value1, value2, alpha)} />
296
+ {#if outOfGamut}
297
+ <Tooltip position="bottom" text={I18n["color-out-of-srgb-gamut"]}>
298
+ <span class="warning">⚠️</span>
299
+ </Tooltip>
300
+ {/if}
301
+ <input type="text" bind:value={hex} onchange={() => parseHex()} />
302
+ </div>
303
+ </Popup>
304
+
305
+ <style>/* https://github.com/NeverCease/uchu/blob/primary/css/color_expanded.css */
306
+ /*** gray ***/
307
+ /*** red ***/
308
+ /*** pink ***/
309
+ /*** purple ***/
310
+ /*** blue ***/
311
+ /*** green ***/
312
+ /*** yellow ***/
313
+ /*** orange ***/
314
+ /*** general ***/
315
+ hr {
316
+ margin-left: 5px;
317
+ border-top: none;
318
+ border-left: none;
319
+ border-right: none;
320
+ border-bottom: 1px dashed gray;
321
+ }
322
+
323
+ span.warning {
324
+ cursor: help;
325
+ user-select: none;
326
+ -webkit-user-select: none;
327
+ -moz-user-select: none;
328
+ -ms-user-select: none;
329
+ }
330
+
331
+ span.back {
332
+ display: inline-block;
333
+ position: absolute;
334
+ top: 5px;
335
+ left: 0;
336
+ right: 0;
337
+ bottom: 5px;
338
+ z-index: -1;
339
+ background-color: whitesmoke;
340
+ border-radius: 3px;
341
+ padding: 0;
342
+ margin: 0;
343
+ box-shadow: 0 1px 3px rgba(128, 128, 128, 0.378);
344
+ }
345
+
346
+ .alpha {
347
+ background-image: linear-gradient(45deg, #ccc 25%, transparent 25%), linear-gradient(-45deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%), linear-gradient(-45deg, transparent 75%, #ccc 75%);
348
+ background-size: 4px 4px;
349
+ background-position: 0 0, 0 2px, 2px -2px, -2px 0px;
350
+ }
351
+ .alpha::before {
352
+ content: "";
353
+ position: absolute;
354
+ top: 0;
355
+ left: 0;
356
+ width: 100%;
357
+ height: 100%;
358
+ background: var(--grad);
359
+ }
360
+
361
+ span.coloring {
362
+ display: inline-block;
363
+ position: absolute;
364
+ top: 3px;
365
+ left: 0;
366
+ right: 0;
367
+ bottom: 3px;
368
+ z-index: -1;
369
+ padding: 0;
370
+ margin: 0;
371
+ }
372
+
373
+ .slider-container {
374
+ position: relative;
375
+ flex-grow: 1;
376
+ }
377
+
378
+ .value-group {
379
+ display: flex;
380
+ gap: 5px;
381
+ }
382
+ .value-group span {
383
+ font-size: 85%;
384
+ }
385
+ .value-group input {
386
+ width: 10ch;
387
+ box-sizing: border-box;
388
+ font-family: var(--mono-font-family);
389
+ }
390
+
391
+ .preview-btn {
392
+ width: 100%;
393
+ height: 1.5em;
394
+ position: relative;
395
+ overflow: hidden;
396
+ background-image: linear-gradient(45deg, #ccc 25%, transparent 25%), linear-gradient(-45deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%), linear-gradient(-45deg, transparent 75%, #ccc 75%);
397
+ background-size: 20px 20px;
398
+ background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
399
+ }
400
+ .preview-btn::before {
401
+ content: "";
402
+ position: absolute;
403
+ top: 0;
404
+ left: 0;
405
+ width: 100%;
406
+ height: 100%;
407
+ background: linear-gradient(to right, var(--color) 50%, var(--trspColor) 50%);
408
+ }
409
+
410
+ input[type=range] {
411
+ margin: 0;
412
+ padding: 0;
413
+ width: 100%;
414
+ height: 100%;
415
+ }
416
+ input[type=range]::-webkit-slider-runnable-track {
417
+ background: transparent;
418
+ }</style>
@@ -0,0 +1,9 @@
1
+ import * as Color from "colorjs.io/fn";
2
+ declare const Colorpicker: import("svelte").Component<{
3
+ mode?: string;
4
+ oninput?: (color: Color.PlainColorObject) => void;
5
+ onchange?: (color: Color.PlainColorObject) => void;
6
+ color: Color.PlainColorObject;
7
+ }, {}, "color" | "mode">;
8
+ type Colorpicker = ReturnType<typeof Colorpicker>;
9
+ export default Colorpicker;
@@ -0,0 +1,4 @@
1
+ export declare const Debug: {
2
+ assert(cond: boolean, reason?: string): asserts cond;
3
+ never(value?: never): never;
4
+ };
package/dist/Debug.js ADDED
@@ -0,0 +1,10 @@
1
+ export const Debug = {
2
+ assert(cond, reason) {
3
+ console.assert(cond, reason);
4
+ },
5
+ never(x) {
6
+ const msg = `Unreachable code reached (never=${x})`;
7
+ console.error(msg);
8
+ throw new Error(msg);
9
+ }
10
+ };
@@ -0,0 +1,21 @@
1
+ export type EventHandler<T extends unknown[]> = (...args: [...T]) => void;
2
+ export type AsyncEventHandler<T extends unknown[]> = (...args: [...T]) => void | Promise<void>;
3
+ export type EventHandlerOptions = {
4
+ once?: boolean;
5
+ };
6
+ export declare class EventHost<T extends unknown[] = []> {
7
+ #private;
8
+ static globalEventHosts: EventHost<unknown[]>[];
9
+ constructor();
10
+ dispatch(...args: [...T]): void;
11
+ bind(obj: object, f: (...args: [...T]) => void | Promise<void>, options?: EventHandlerOptions): void;
12
+ static unbind(obj: object): void;
13
+ }
14
+ export declare class AsyncEventHost<T extends unknown[] = []> {
15
+ #private;
16
+ static globalEventHosts: AsyncEventHost<unknown[]>[];
17
+ constructor();
18
+ dispatchAndAwaitAll(...args: [...T]): Promise<void>;
19
+ bind(obj: object, f: (...args: [...T]) => void | Promise<void>, options?: EventHandlerOptions): void;
20
+ static unbind(obj: object): void;
21
+ }
@@ -0,0 +1,60 @@
1
+ export class EventHost {
2
+ #listeners = new Map();
3
+ static globalEventHosts = [];
4
+ constructor() {
5
+ // @ts-expect-error -- converting to unknown
6
+ EventHost.globalEventHosts.push(this);
7
+ }
8
+ dispatch(...args) {
9
+ for (const [k, f] of [...this.#listeners]) {
10
+ try {
11
+ f.forEach(([x, _]) => x(...args));
12
+ }
13
+ finally {
14
+ this.#listeners.set(k, f.filter(([_, y]) => !y.once));
15
+ }
16
+ }
17
+ }
18
+ ;
19
+ bind(obj, f, options = {}) {
20
+ if (!this.#listeners.has(obj))
21
+ this.#listeners.set(obj, []);
22
+ this.#listeners.get(obj).push([f, options]);
23
+ }
24
+ static unbind(obj) {
25
+ for (const host of EventHost.globalEventHosts) {
26
+ host.#listeners.delete(obj);
27
+ }
28
+ }
29
+ }
30
+ export class AsyncEventHost {
31
+ #listeners = new Map();
32
+ static globalEventHosts = [];
33
+ constructor() {
34
+ // @ts-expect-error -- converting to unknown
35
+ AsyncEventHost.globalEventHosts.push(this);
36
+ }
37
+ async dispatchAndAwaitAll(...args) {
38
+ try {
39
+ const list = [...this.#listeners]
40
+ .flatMap(([_, f]) => f.map(([x, _]) => x(...args)))
41
+ .filter((x) => x !== undefined);
42
+ await Promise.allSettled(list);
43
+ }
44
+ finally {
45
+ for (const [k, f] of [...this.#listeners])
46
+ this.#listeners.set(k, f.filter(([_, y]) => !y.once));
47
+ }
48
+ }
49
+ ;
50
+ bind(obj, f, options = {}) {
51
+ if (!this.#listeners.has(obj))
52
+ this.#listeners.set(obj, []);
53
+ this.#listeners.get(obj).push([f, options]);
54
+ }
55
+ static unbind(obj) {
56
+ for (const host of AsyncEventHost.globalEventHosts) {
57
+ host.#listeners.delete(obj);
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,3 @@
1
+ export declare const I18n: {
2
+ 'color-out-of-srgb-gamut': string;
3
+ };
@@ -0,0 +1,3 @@
1
+ export const I18n = $state({
2
+ 'color-out-of-srgb-gamut': 'The sRGB color space cannot display this color accurately. Your monitor might be using the nearest representable color instead.'
3
+ });