fragment-tools 0.2.11 → 0.2.13

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.
Files changed (67) hide show
  1. package/package.json +12 -11
  2. package/src/cli/build.js +1 -0
  3. package/src/cli/create.js +22 -4
  4. package/src/cli/createConfig.js +2 -2
  5. package/src/cli/getEntries.js +10 -1
  6. package/src/cli/plugins/hot-shader-replacement.js +54 -16
  7. package/src/cli/plugins/save.js +97 -38
  8. package/src/cli/prompts.js +89 -36
  9. package/src/cli/run.js +1 -1
  10. package/src/cli/templates/blank/index.ts +1 -1
  11. package/src/cli/templates/default/index.js +10 -2
  12. package/src/cli/templates/default/index.ts +5 -2
  13. package/src/cli/templates/fragment-gl/index.ts +1 -1
  14. package/src/cli/templates/p5/index.ts +1 -1
  15. package/src/cli/templates/p5-webgl/index.ts +1 -1
  16. package/src/cli/templates/three-fragment/index.js +5 -3
  17. package/src/cli/templates/three-fragment/index.ts +5 -4
  18. package/src/cli/templates/three-orthographic/index.js +6 -1
  19. package/src/cli/templates/three-orthographic/index.ts +6 -2
  20. package/src/cli/templates/three-perspective/index.js +6 -1
  21. package/src/cli/templates/three-perspective/index.ts +6 -2
  22. package/src/client/app/actions/resize.js +8 -1
  23. package/src/client/app/attachments/draggable.js +93 -0
  24. package/src/client/app/client.js +90 -18
  25. package/src/client/app/components/IconFlip.svelte +46 -0
  26. package/src/client/app/hooks.js +25 -1
  27. package/src/client/app/lib/canvas-recorder/CanvasRecorder.js +95 -3
  28. package/src/client/app/lib/canvas-recorder/FrameRecorder.js +45 -3
  29. package/src/client/app/lib/canvas-recorder/GIFRecorder.js +72 -13
  30. package/src/client/app/lib/canvas-recorder/MediaBunnyRecorder.js +43 -9
  31. package/src/client/app/lib/canvas-recorder/utils.js +18 -9
  32. package/src/client/app/modules/Params.svelte +1 -0
  33. package/src/client/app/renderers/2DRenderer.js +20 -16
  34. package/src/client/app/renderers/P5GLRenderer.js +13 -5
  35. package/src/client/app/renderers/P5Renderer.js +9 -1
  36. package/src/client/app/renderers/THREERenderer.js +63 -48
  37. package/src/client/app/state/Sketch.svelte.js +150 -10
  38. package/src/client/app/state/errors.svelte.js +19 -0
  39. package/src/client/app/state/exports.svelte.js +14 -1
  40. package/src/client/app/state/rendering.svelte.js +90 -13
  41. package/src/client/app/state/sketches.svelte.js +43 -7
  42. package/src/client/app/state/utils.svelte.js +49 -0
  43. package/src/client/app/ui/Field.svelte +63 -16
  44. package/src/client/app/ui/FieldSection.svelte +4 -4
  45. package/src/client/app/ui/ParamsOutput.svelte +7 -5
  46. package/src/client/app/ui/SketchRenderer.svelte +21 -0
  47. package/src/client/app/ui/fields/ButtonInput.svelte +2 -0
  48. package/src/client/app/ui/fields/CheckboxInput.svelte +13 -11
  49. package/src/client/app/ui/fields/ColorInput.svelte +16 -11
  50. package/src/client/app/ui/fields/GradientInput.svelte +607 -0
  51. package/src/client/app/ui/fields/Input.svelte +10 -6
  52. package/src/client/app/ui/fields/IntervalInput.svelte +27 -35
  53. package/src/client/app/ui/fields/NumberInput.svelte +51 -13
  54. package/src/client/app/ui/fields/PaletteInput.svelte +181 -0
  55. package/src/client/app/ui/fields/ProgressInput.svelte +44 -16
  56. package/src/client/app/ui/fields/TextareaInput.svelte +93 -0
  57. package/src/client/app/utils/canvas.utils.js +105 -28
  58. package/src/client/app/utils/color.utils.js +74 -17
  59. package/src/client/app/utils/fields.utils.js +70 -17
  60. package/src/client/app/utils/file.utils.js +86 -31
  61. package/src/client/app/utils/glsl.utils.js +11 -2
  62. package/src/client/app/utils/glslErrors.js +31 -21
  63. package/src/client/app/utils/index.js +28 -12
  64. package/src/client/main.js +7 -1
  65. package/src/types/global.d.ts +143 -0
  66. package/src/types/props.d.ts +41 -15
  67. package/tsconfig.json +1 -1
@@ -8,14 +8,18 @@
8
8
  onkeydown,
9
9
  onfocus,
10
10
  onblur,
11
+ node = $bindable(),
11
12
  } = $props();
12
13
 
13
- /** @type {HTMLInputElement} */
14
- let node;
15
-
14
+ /**
15
+ * @param {KeyboardEvent} event
16
+ */
16
17
  function onKeyPress(event) {
17
- if (event.key === 'Enter') {
18
- node.blur();
18
+ if (
19
+ event.currentTarget instanceof HTMLInputElement &&
20
+ event.key === 'Enter'
21
+ ) {
22
+ event.currentTarget.blur();
19
23
  }
20
24
  }
21
25
  </script>
@@ -34,7 +38,7 @@
34
38
  {onfocus}
35
39
  {onblur}
36
40
  onkeypress={onKeyPress}
37
- disabled={disabled ? 'disabled' : null}
41
+ {disabled}
38
42
  autocomplete="off"
39
43
  spellcheck="false"
40
44
  />
@@ -2,6 +2,7 @@
2
2
  import FieldInputRow from './FieldInputRow.svelte';
3
3
  import NumberInput from './NumberInput.svelte';
4
4
  import { map, clamp, roundToStep } from '../../utils/math.utils';
5
+ import { draggable } from '../../attachments/draggable.js';
5
6
 
6
7
  let {
7
8
  value = null,
@@ -21,51 +22,49 @@
21
22
  /** @type {DOMRect}*/
22
23
  let rect;
23
24
  /** @type {boolean}*/
24
- let isDragging = $state(false);
25
+ let dragging = $state(false);
25
26
 
26
27
  let proximityIndex = -1;
27
28
 
28
29
  /**
29
30
  *
30
31
  * @param {MouseEvent} event
32
+ * @param {DOMRect}
31
33
  */
32
- function handleMouseDown(event) {
33
- document.body.classList.add('fragment-dragging');
34
-
35
- document.addEventListener('mousemove', handleMouseMove);
36
- document.addEventListener('mouseup', handleMouseUp);
37
-
38
- rect = node.getBoundingClientRect();
39
-
40
- isDragging = true;
41
-
42
- let dragValue = computeDrag(event);
43
-
44
- let abs0 = Math.abs(dragValue - value[0]);
45
- let abs1 = Math.abs(dragValue - value[1]);
46
-
47
- proximityIndex = abs0 < abs1 ? 0 : 1;
48
-
49
- onDrag(event);
50
- }
51
-
52
- function computeDrag(event) {
34
+ function computeDrag(event, rect) {
53
35
  let dragValue = clamp(
54
36
  map(event.clientX, rect.left, rect.right, min, max),
55
37
  min,
56
38
  max,
57
39
  );
58
40
  dragValue = roundToStep(dragValue, step);
41
+
59
42
  return dragValue;
60
43
  }
61
44
 
62
- function handleMouseMove(event) {
63
- onDrag(event);
45
+ /**
46
+ *
47
+ * @param {MouseEvent} event
48
+ * @param {object} params
49
+ * @param {DOMRect | undefined} params.rect
50
+ */
51
+ function onDragStart(event, params) {
52
+ if (params.rect) {
53
+ let dragValue = computeDrag(event, params.rect);
54
+
55
+ let abs0 = Math.abs(dragValue - value[0]);
56
+ let abs1 = Math.abs(dragValue - value[1]);
57
+
58
+ proximityIndex = abs0 < abs1 ? 0 : 1;
59
+
60
+ onDrag(event, params);
61
+ }
64
62
  }
65
63
 
66
- function onDrag(event) {
67
- let dragValue = computeDrag(event);
64
+ function onDrag(event, params) {
65
+ dragging = params.isDragging;
68
66
 
67
+ let dragValue = computeDrag(event, params.rect);
69
68
  let prevValue = value[proximityIndex];
70
69
 
71
70
  if (dragValue !== prevValue) {
@@ -82,13 +81,6 @@
82
81
  }
83
82
  }
84
83
 
85
- function handleMouseUp() {
86
- document.body.classList.remove('fragment-dragging');
87
- document.removeEventListener('mousemove', handleMouseMove);
88
- document.removeEventListener('mouseup', handleMouseUp);
89
-
90
- isDragging = false;
91
- }
92
84
 
93
85
  function handleValueChange(index, newValue) {
94
86
  let newValues = [...value];
@@ -119,9 +111,9 @@
119
111
  <FieldInputRow --grid-template-columns="1fr 0.5fr">
120
112
  <div
121
113
  class="range"
122
- class:dragging={isDragging}
114
+ class:dragging={dragging}
123
115
  bind:this={node}
124
- onmousedown={handleMouseDown}
116
+ {@attach draggable({ onDragStart, onDrag })}
125
117
  >
126
118
  <div class="handler" style="--position: {p1};" />
127
119
  <div class="filler" style="--p1: {p1}; --p2: {p2};"></div>
@@ -1,4 +1,5 @@
1
1
  <script>
2
+ import { tick } from 'svelte';
2
3
  import FieldInputRow from './FieldInputRow.svelte';
3
4
  import Input from './Input.svelte';
4
5
  import ProgressInput from './ProgressInput.svelte';
@@ -17,16 +18,30 @@
17
18
  key = '',
18
19
  progress = true,
19
20
  onchange,
21
+ onfocus,
22
+ onblur,
23
+ node = $bindable(),
20
24
  } = $props();
21
25
 
22
26
  let hasProgress = $derived(progress && isFinite(min) && isFinite(max));
23
27
  let isFocused = $state(false);
24
28
  let precision = $derived(step.toString().split('.')[1]?.length || 0);
25
29
 
30
+ /**
31
+ * @param {string} v
32
+ * @param {string} suffix
33
+ */
26
34
  function sanitize(v, suffix) {
27
35
  return suffix && suffix !== '' ? Number(v.split(suffix)[0]) : Number(v);
28
36
  }
29
37
 
38
+ /**
39
+ *
40
+ * @param {number} v
41
+ * @param {boolean} isFocused
42
+ * @param {string} suffix
43
+ * @param {number} precision
44
+ */
30
45
  function composeValue(v, isFocused, suffix = '', precision) {
31
46
  const clampedValue = clamp(
32
47
  v,
@@ -45,32 +60,53 @@
45
60
  composeValue(value, isFocused, suffix, precision),
46
61
  );
47
62
 
48
- function onFocus() {
63
+ /**
64
+ * @param {FocusEvent} event
65
+ */
66
+ function onFocus(event) {
49
67
  isFocused = true;
68
+ onfocus?.(event);
50
69
  }
51
70
 
52
- function onBlur(event) {
53
- isFocused = false;
71
+ /**
72
+ * @param {KeyboardEvent} event
73
+ */
74
+ async function onBlur(event) {
75
+ let currentTarget = event.currentTarget;
54
76
 
55
- let newValue = event.currentTarget.value;
56
- let isNotValid = isNaN(Number(event.currentTarget.value));
77
+ await tick();
57
78
 
58
- if (isNotValid) {
59
- newValue = `${value}`;
60
- }
79
+ if (currentTarget instanceof HTMLInputElement) {
80
+ let newValue = currentTarget.value;
81
+ isFocused = false;
82
+ let sanitizedValue = sanitize(newValue, suffix);
83
+
84
+ if (isNaN(sanitizedValue)) {
85
+ onchange(value, true);
86
+ } else {
87
+ onchange(sanitizedValue, true);
88
+ }
61
89
 
62
- onchange(sanitize(newValue, suffix));
90
+ onblur?.(event);
91
+ }
63
92
  }
64
93
 
94
+ /**
95
+ * @param {KeyboardEvent} event
96
+ */
65
97
  function onKeyDown(event) {
66
- if ([38, 40].includes(event.keyCode)) {
98
+ if (
99
+ event.currentTarget instanceof HTMLInputElement &&
100
+ ['ArrowDown', 'ArrowUp'].includes(event.key)
101
+ ) {
67
102
  event.preventDefault();
68
103
 
69
104
  const diff = Keyboard.getStepFromEvent(event) * step;
70
- const direction = event.keyCode === 38 ? 1 : -1;
71
- const newValue = sanitize(composedValue, suffix) + direction * diff;
105
+ const direction = event.key === 'ArrowUp' ? 1 : -1;
106
+ const sanitizedValue =
107
+ sanitize(event.currentTarget.value, suffix) + direction * diff;
72
108
 
73
- onchange(newValue);
109
+ onchange(sanitizedValue, false);
74
110
  }
75
111
  }
76
112
  </script>
@@ -93,6 +129,7 @@
93
129
  {disabled}
94
130
  {context}
95
131
  {key}
132
+ bind:node
96
133
  onkeydown={onKeyDown}
97
134
  onfocus={onFocus}
98
135
  onblur={onBlur}
@@ -108,6 +145,7 @@
108
145
  onkeydown={onKeyDown}
109
146
  onfocus={onFocus}
110
147
  onblur={onBlur}
148
+ bind:node
111
149
  value={composedValue}
112
150
  />
113
151
  {/if}
@@ -0,0 +1,181 @@
1
+ <script>
2
+ import * as color from '../../utils/color.utils.js';
3
+ import ButtonInput from './ButtonInput.svelte';
4
+ import ColorInput from './ColorInput.svelte';
5
+
6
+ let {
7
+ value,
8
+ context = null,
9
+ key = '',
10
+ disabled = false,
11
+ onchange,
12
+ editable = true,
13
+ extensible = true,
14
+ } = $props();
15
+
16
+ let selected = $state(-1);
17
+
18
+ let hexValues = $derived.by(() => {
19
+ return value.map((v) => {
20
+ const format = color.getColorFormat(v);
21
+ return color.toHex(v, format);
22
+ });
23
+ });
24
+
25
+ $effect(() => {
26
+ // handle value length changes when a color is selected
27
+ if (selected >= hexValues.length) {
28
+ selected = hexValues.length - 1;
29
+ }
30
+ });
31
+
32
+ /**
33
+ *
34
+ * @param {PointerEvent} event
35
+ * @param {number} index
36
+ */
37
+ function handleClick(event, index) {
38
+ if (selected !== index) {
39
+ selected = index;
40
+ } else {
41
+ selected = -1;
42
+ event.currentTarget.blur();
43
+ }
44
+ }
45
+
46
+ /**
47
+ *
48
+ * @param {string} color
49
+ */
50
+ function handleColorChange(color) {
51
+ let palette = value.map((v) => v);
52
+ palette[selected] = color;
53
+
54
+ onchange(palette);
55
+ }
56
+
57
+ function handleClickAdd() {
58
+ let palette = value.map((v) => v);
59
+ palette.push('#ffffff');
60
+
61
+ onchange(palette);
62
+
63
+ selected = palette.length - 1;
64
+ }
65
+
66
+ function handleClickDelete() {
67
+ let index = selected;
68
+ let palette = value.map((v) => v);
69
+ palette.splice(index, 1);
70
+
71
+ onchange(palette);
72
+ selected = -1;
73
+ }
74
+ </script>
75
+
76
+ <div class="palette-input" class:disabled style="--count: {hexValues.length}">
77
+ <div class="palette-list">
78
+ {#if editable && extensible}
79
+ <div class="palette-action">
80
+ <ButtonInput label="+" onclick={handleClickAdd} />
81
+ </div>
82
+ {/if}
83
+ {#each hexValues as hexValue, index}
84
+ {#if editable}
85
+ <button
86
+ class="palette-action"
87
+ style="--current-color: {hexValue}"
88
+ class:selected={selected === index}
89
+ onclick={(event) => handleClick(event, index)}
90
+ >
91
+ <span class="visually-hidden">Change color {index}</span>
92
+ </button>
93
+ {:else}
94
+ <div
95
+ class="palette-action"
96
+ style="--current-color: {hexValue}"
97
+ ></div>
98
+ {/if}
99
+ {/each}
100
+ </div>
101
+ {#if value[selected] && editable}
102
+ <div class="palette-editor">
103
+ <ColorInput value={value[selected]} onchange={handleColorChange} />
104
+ </div>
105
+ {#if extensible}
106
+ <div class="palette-delete">
107
+ <ButtonInput label="delete" onclick={handleClickDelete} />
108
+ </div>
109
+ {/if}
110
+ {/if}
111
+ </div>
112
+
113
+ <style>
114
+ .palette-input {
115
+ position: relative;
116
+ width: 100%;
117
+
118
+ display: flex;
119
+ flex-direction: column;
120
+ row-gap: var(--column-gap);
121
+ }
122
+
123
+ .palette-list {
124
+ display: flex;
125
+ flex-wrap: wrap;
126
+
127
+ column-gap: var(--column-gap);
128
+ row-gap: var(--column-gap);
129
+ align-items: center;
130
+ }
131
+
132
+ .palette-action {
133
+ position: relative;
134
+ aspect-ratio: 1;
135
+ height: var(--fragment-input-height);
136
+
137
+ border-radius: var(--fragment-input-border-radius);
138
+ background-color: var(
139
+ --background-color,
140
+ var(--fragment-input-background-color)
141
+ );
142
+ box-shadow: inset 0 0 0 1px
143
+ var(--box-shadow-color, var(--fragment-input-border-color));
144
+ outline: 0;
145
+ }
146
+
147
+ button.palette-action {
148
+ cursor: pointer;
149
+ }
150
+
151
+ .palette-input:focus-within .palette-action.selected {
152
+ box-shadow: 0 0 0 2px var(--fragment-accent-color);
153
+ }
154
+
155
+ .palette-action:before {
156
+ --gap: 1px;
157
+ content: '';
158
+
159
+ position: absolute;
160
+ z-index: 1;
161
+ top: var(--gap);
162
+ left: var(--gap);
163
+ right: var(--gap);
164
+ bottom: var(--gap);
165
+
166
+ background-color: var(--current-color);
167
+ border-radius: calc(var(--fragment-input-border-radius) - var(--gap));
168
+ opacity: var(--opacity, 1);
169
+ pointer-events: none;
170
+ }
171
+
172
+ :global(body:not(.fragment-dragging))
173
+ button.palette-action:not(.disabled):hover {
174
+ box-shadow: inset 0 0 0 1px var(--fragment-accent-color);
175
+ }
176
+
177
+ :global(body:not(.fragment-dragging))
178
+ button.palette-action:not(.disabled):focus-within {
179
+ box-shadow: 0 0 0 2px var(--fragment-accent-color);
180
+ }
181
+ </style>
@@ -2,46 +2,74 @@
2
2
  import Keyboard from '../../inputs/Keyboard.js';
3
3
  import { map, clamp, roundToStep } from '../../utils/math.utils.js';
4
4
 
5
+ /**
6
+ * @typedef {Object} Props
7
+ * @property {number} value
8
+ * @property {number} min
9
+ * @property {number} max
10
+ * @property {number} step
11
+ * @property {boolean} disabled
12
+ * @property {(value: number) => void|undefined} onchange
13
+ */
14
+
15
+ /** @type {Props} */
5
16
  let { value, min, max, step, disabled = false, onchange } = $props();
6
17
 
18
+ /** @type {HTMLElement|undefined} */
7
19
  let node;
20
+ /** @type {DOMRect|undefined} */
8
21
  let rect;
9
22
 
10
23
  let isDragging = $state(false);
11
24
  let steppedValue = $derived(roundToStep(value, step));
12
25
 
13
- // handlers
26
+ /**
27
+ * @param {MouseEvent} event
28
+ */
14
29
  function handleMouseDown(event) {
15
30
  if (disabled) return;
16
31
 
17
- document.body.classList.add('fragment-dragging');
18
- document.addEventListener('mousemove', handleMouseMove);
19
- document.addEventListener('mouseup', handleMouseUp);
32
+ if (node) {
33
+ document.body.classList.add('fragment-dragging');
34
+ document.addEventListener('mousemove', handleMouseMove);
35
+ document.addEventListener('mouseup', handleMouseUp);
20
36
 
21
- rect = node.getBoundingClientRect();
37
+ rect = node.getBoundingClientRect();
22
38
 
23
- isDragging = true;
39
+ isDragging = true;
24
40
 
25
- onDrag(event);
41
+ onDrag(event);
42
+ }
26
43
  }
27
44
 
45
+ /**
46
+ * @param {MouseEvent} event
47
+ */
28
48
  function handleMouseMove(event) {
29
49
  onDrag(event);
30
50
  }
31
51
 
52
+ /**
53
+ * @param {MouseEvent} event
54
+ */
32
55
  function onDrag(event) {
33
- let dragValue = clamp(
34
- map(event.clientX, rect.left, rect.right, min, max),
35
- min,
36
- max,
37
- );
38
- dragValue = roundToStep(dragValue, step);
56
+ if (rect) {
57
+ let dragValue = clamp(
58
+ map(event.clientX, rect.left, rect.right, min, max),
59
+ min,
60
+ max,
61
+ );
62
+ dragValue = roundToStep(dragValue, step);
39
63
 
40
- if (dragValue !== value) {
41
- onchange(dragValue);
64
+ if (dragValue !== value) {
65
+ onchange?.(dragValue);
66
+ }
42
67
  }
43
68
  }
44
69
 
70
+ /**
71
+ * @param {KeyboardEvent} event
72
+ */
45
73
  function handleKeyDown(event) {
46
74
  const direction = ['ArrowUp', 'ArrowRight'].includes(event.key)
47
75
  ? 1
@@ -59,7 +87,7 @@
59
87
  );
60
88
 
61
89
  if (newValue !== value) {
62
- onchange(newValue);
90
+ onchange?.(newValue);
63
91
  }
64
92
  }
65
93
  }
@@ -0,0 +1,93 @@
1
+ <script>
2
+ let {
3
+ value = $bindable(),
4
+ height,
5
+ disabled = false,
6
+ oninput,
7
+ onchange,
8
+ onkeydown,
9
+ onfocus,
10
+ onblur,
11
+ } = $props();
12
+
13
+ /**
14
+ * @param {KeyboardEvent} event
15
+ */
16
+ function onKeyPress(event) {
17
+ if (
18
+ event.currentTarget instanceof HTMLTextAreaElement &&
19
+ event.key === 'Enter' &&
20
+ !event.shiftKey
21
+ ) {
22
+ event.currentTarget.blur();
23
+ }
24
+ }
25
+ </script>
26
+
27
+ <div
28
+ class="input-container"
29
+ class:disabled
30
+ style={height ? `--height: ${height}` : null}
31
+ >
32
+ <textarea
33
+ class="input"
34
+ bind:value
35
+ {oninput}
36
+ {onchange}
37
+ {onkeydown}
38
+ {onfocus}
39
+ {onblur}
40
+ onkeypress={onKeyPress}
41
+ {disabled}
42
+ autocomplete="off"
43
+ spellcheck="false"
44
+ ></textarea>
45
+ </div>
46
+
47
+ <style>
48
+ .input-container {
49
+ position: relative;
50
+
51
+ display: flex;
52
+ width: 100%;
53
+ height: var(--height, var(--fragment-input-height));
54
+ margin: 2px 0;
55
+
56
+ border-radius: var(--fragment-input-border-radius);
57
+ background-color: var(--fragment-input-background-color);
58
+ box-shadow: inset 0 0 0 1px var(--fragment-input-border-color);
59
+ }
60
+
61
+ :global(body:not(.fragment-dragging))
62
+ .input-container:not(.disabled):hover {
63
+ box-shadow: inset 0 0 0 1px var(--fragment-accent-color);
64
+ }
65
+
66
+ :global(body:not(.fragment-dragging))
67
+ .input-container:not(.disabled):focus-within {
68
+ box-shadow: 0 0 0 2px var(--fragment-accent-color);
69
+ }
70
+
71
+ .input {
72
+ width: 100%;
73
+ height: 100%;
74
+ padding: var(--padding) var(--padding);
75
+
76
+ color: var(--fragment-input-text-color);
77
+ font-size: var(--fragment-input-font-size);
78
+ text-align: left;
79
+
80
+ background: transparent;
81
+ outline: 0;
82
+ resize: none;
83
+ border: none;
84
+ }
85
+
86
+ .input:disabled {
87
+ color: var(--fragment-input-disabled-text-color);
88
+ }
89
+
90
+ .input:focus {
91
+ color: var(--fragment-text-color);
92
+ }
93
+ </style>