fragment-tools 0.2.2 → 0.2.4

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 (31) hide show
  1. package/package.json +1 -1
  2. package/src/client/app/components/Build.svelte +64 -0
  3. package/src/client/app/{ui → components}/Preview.svelte +1 -2
  4. package/src/client/app/inputs/MIDI.js +0 -1
  5. package/src/client/app/modules/MidiPanel.svelte +23 -13
  6. package/src/client/app/modules/Params.svelte +31 -16
  7. package/src/client/app/state/Sketch.svelte.js +77 -73
  8. package/src/client/app/state/layout.svelte.js +8 -2
  9. package/src/client/app/state/rendering.svelte.js +23 -5
  10. package/src/client/app/state/utils.svelte.js +28 -4
  11. package/src/client/app/ui/Field.svelte +13 -11
  12. package/src/client/app/ui/FieldGroup.svelte +5 -2
  13. package/src/client/app/ui/FieldSection.svelte +4 -4
  14. package/src/client/app/ui/Layout.svelte +1 -1
  15. package/src/client/app/ui/LayoutBuild.svelte +54 -0
  16. package/src/client/app/ui/LayoutColumn.svelte +2 -2
  17. package/src/client/app/ui/LayoutComponent.svelte +14 -4
  18. package/src/client/app/ui/LayoutResizer.svelte +11 -2
  19. package/src/client/app/ui/LayoutRow.svelte +2 -2
  20. package/src/client/app/ui/fields/ButtonInput.svelte +3 -3
  21. package/src/client/app/ui/fields/ColorInput.svelte +23 -13
  22. package/src/client/app/ui/fields/ImportInput.svelte +52 -0
  23. package/src/client/app/ui/fields/Input.svelte +4 -2
  24. package/src/client/app/ui/fields/IntervalInput.svelte +8 -6
  25. package/src/client/app/ui/fields/ProgressInput.svelte +47 -17
  26. package/src/client/app/ui/fields/Select.svelte +35 -41
  27. package/src/client/app/ui/fields/VectorInput.svelte +63 -18
  28. package/src/client/app/utils/fields.utils.js +70 -48
  29. package/src/client/app/utils/math.utils.js +6 -0
  30. package/src/client/public/css/global.css +14 -0
  31. package/src/client/app/ui/Build.svelte +0 -91
@@ -9,7 +9,7 @@
9
9
  import ButtonInput from './fields/ButtonInput.svelte';
10
10
  import ImageInput from './fields/ImageInput.svelte';
11
11
  import IntervalInput from './fields/IntervalInput.svelte';
12
- import { fieldTypes, hasChanged } from '../utils/fields.utils.js';
12
+ import { fieldTypes } from '../utils/fields.utils.js';
13
13
 
14
14
  const fields = {
15
15
  [`${fieldTypes.SELECT}`]: Select,
@@ -21,6 +21,7 @@
21
21
  [`${fieldTypes.COLOR}`]: ColorInput,
22
22
  [`${fieldTypes.BUTTON}`]: ButtonInput,
23
23
  [`${fieldTypes.DOWNLOAD}`]: ButtonInput,
24
+ [`${fieldTypes.IMPORT}`]: ImportInput,
24
25
  [`${fieldTypes.IMAGE}`]: ImageInput,
25
26
  [`${fieldTypes.INTERVAL}`]: IntervalInput,
26
27
  };
@@ -35,10 +36,12 @@
35
36
  import { inferFieldType } from '../utils/fields.utils.js';
36
37
  import IconTriggers from '../components/IconTriggers.svelte';
37
38
  import IconLocked from '../components/IconLocked.svelte';
39
+ import ImportInput from './fields/ImportInput.svelte';
40
+ import { deepEqual } from '../state/utils.svelte';
38
41
 
39
42
  let {
40
43
  key,
41
- value = null,
44
+ value,
42
45
  initialValue = value,
43
46
  context = null,
44
47
  params = $bindable({}),
@@ -89,7 +92,7 @@
89
92
  let fieldType = $derived(inferFieldType({ type, value, params, key }));
90
93
  let fieldProps = $derived(composeFieldProps(params, disabled));
91
94
  let onTrigger = $derived(frameDebounce(onTriggers[fieldType]));
92
- let input = $derived(fields[fieldType]);
95
+ let Component = $derived(fields[fieldType]);
93
96
  let triggerable = $derived(
94
97
  params.triggerable !== false &&
95
98
  ((fieldType === fieldTypes.NUMBER &&
@@ -115,12 +118,17 @@
115
118
  context,
116
119
  };
117
120
  }
121
+
122
+ function hasChanged(current, next) {
123
+ const changed = !deepEqual(current, next);
124
+ return changed;
125
+ }
118
126
  </script>
119
127
 
120
128
  <div
121
129
  class="field"
122
130
  class:disabled
123
- class:changed={!disabled && hasChanged(initialValue, value)}
131
+ class:changed={!disabled && hasChanged(value, initialValue)}
124
132
  style="--index: {index};"
125
133
  >
126
134
  <FieldSection
@@ -151,13 +159,7 @@
151
159
  {/if}
152
160
  </div>
153
161
  {/snippet}
154
- <svelte:component
155
- this={input}
156
- {value}
157
- {...fieldProps}
158
- {onchange}
159
- onclick={onTrigger}
160
- />
162
+ <Component {value} {...fieldProps} {onchange} onclick={onTrigger} />
161
163
  {@render children?.()}
162
164
  </FieldSection>
163
165
  {#if triggerable}
@@ -79,7 +79,8 @@
79
79
  transition: opacity 0.1s ease;
80
80
  }
81
81
 
82
- .header__action:hover .header__icon {
82
+ :global(body:not(.fragment-dragging)) .header__action:hover .header__icon,
83
+ .header__action:focus-visible .header__icon {
83
84
  opacity: 1;
84
85
  }
85
86
 
@@ -98,7 +99,9 @@
98
99
  transition: opacity 0.1s ease;
99
100
  }
100
101
 
101
- .header__action:hover .field-group__name,
102
+ :global(body:not(.fragment-dragging))
103
+ .header__action:hover
104
+ .field-group__name,
102
105
  .header__action:focus-visible .field-group__name {
103
106
  opacity: 1;
104
107
  }
@@ -48,7 +48,7 @@
48
48
  grid-template-columns: 1fr;
49
49
  }
50
50
 
51
- .field__section:hover .field__label,
51
+ :global(body:not(.fragment-dragging)) .field__section:hover .field__label,
52
52
  .field__section:focus-within .field__label {
53
53
  opacity: 1;
54
54
  }
@@ -102,9 +102,9 @@
102
102
  }
103
103
 
104
104
  .field__label:focus-visible {
105
- outline: 0;
106
- box-shadow: 0 0 0 2px var(--color-text);
107
- border-radius: 2px;
105
+ outline: 2px var(--color-active) solid;
106
+ outline-offset: 2px;
107
+ border-radius: 1px;
108
108
  }
109
109
 
110
110
  .field__section.secondary {
@@ -1,7 +1,7 @@
1
1
  <script>
2
2
  import Root from './LayoutRoot.svelte';
3
3
  import Column from './LayoutColumn.svelte';
4
- import Build from './Build.svelte';
4
+ import Build from '../components/Build.svelte';
5
5
  import Row from './LayoutRow.svelte';
6
6
  import ModuleRenderer from './ModuleRenderer.svelte';
7
7
  import { layout } from '../state/layout.svelte.js';
@@ -0,0 +1,54 @@
1
+ <script>
2
+ import Monitor from '../modules/Monitor.svelte';
3
+ import Params from '../modules/Params.svelte';
4
+ import FloatingParams from './FloatingParams.svelte';
5
+ import Column from './LayoutColumn.svelte';
6
+ import Row from './LayoutRow.svelte';
7
+
8
+ let { buildConfig = {}, sketch, sketchKey } = $props();
9
+
10
+ let gui = $derived(buildConfig.gui ?? {});
11
+ let layout = $derived(buildConfig.layout ?? {});
12
+ let headless = $derived(layout.headless ?? false);
13
+ let resizable = $derived(layout.resizable ?? false);
14
+
15
+ let guiOutput = $derived(gui.output ?? true);
16
+ let guiAlign = $derived(gui.align ?? 'right');
17
+ let guiHidden = $derived(gui?.hidden);
18
+ let guiSize = $derived(gui?.size ?? 0.25);
19
+ let guiMinimize = $derived(gui?.minimize);
20
+ let guiPosition = $derived(gui?.position);
21
+ </script>
22
+
23
+ {#if guiPosition === 'fixed'}
24
+ <Row>
25
+ {#if guiAlign === 'left'}
26
+ <Column size={guiSize} {resizable}>
27
+ <Params {headless} />
28
+ </Column>
29
+ <Column size={1 - guiSize} {resizable}>
30
+ <Monitor {headless} params={{ selected: sketchKey }} />
31
+ </Column>
32
+ {:else}
33
+ <Column size={1 - guiSize} {resizable}>
34
+ <Monitor {headless} params={{ selected: sketchKey }} />
35
+ </Column>
36
+ <Column size={guiSize} {resizable}>
37
+ <Params {headless} />
38
+ </Column>
39
+ {/if}
40
+ </Row>
41
+ {:else}
42
+ <Row>
43
+ <Monitor {headless} {sketchKey} params={{ selected: sketchKey }} />
44
+ {#if gui}
45
+ <FloatingParams
46
+ output={guiOutput}
47
+ align={guiAlign}
48
+ size={guiSize}
49
+ hidden={guiHidden}
50
+ minimize={guiMinimize}
51
+ />
52
+ {/if}
53
+ </Row>
54
+ {/if}
@@ -1,9 +1,9 @@
1
1
  <script>
2
2
  import LayoutComponent from './LayoutComponent.svelte';
3
3
 
4
- let { size = 1, children } = $props();
4
+ let { size = 1, children, resizable = true } = $props();
5
5
  </script>
6
6
 
7
- <LayoutComponent {size} type="column">
7
+ <LayoutComponent type="column" {size} {resizable}>
8
8
  {@render children?.()}
9
9
  </LayoutComponent>
@@ -4,10 +4,16 @@
4
4
  import Toolbar from './LayoutToolbar.svelte';
5
5
  import Resizer from './LayoutResizer.svelte';
6
6
  import ModuleRenderer from './ModuleRenderer.svelte';
7
- import Preview from './Preview.svelte';
7
+ import Preview from '../components/Preview.svelte';
8
8
  import LayoutComponent from './LayoutComponent.svelte';
9
9
 
10
- let { id = layout.getID(), size = 1, type = 'column', children } = $props();
10
+ let {
11
+ id = layout.getID(),
12
+ size = 1,
13
+ type = 'column',
14
+ children,
15
+ resizable = true,
16
+ } = $props();
11
17
 
12
18
  let parent = getContext('parent');
13
19
  let isColumn = $derived(type === 'column');
@@ -120,7 +126,7 @@
120
126
  {/if}
121
127
  {/each}
122
128
  {:else}
123
- {@render children()}
129
+ {@render children?.()}
124
130
  {/if}
125
131
  {#if layout.editing && (isRoot || (childComponents.length === 1 && childComponents[0].type === 'module') || childComponents.length === 0)}
126
132
  <Toolbar
@@ -134,7 +140,11 @@
134
140
  {/if}
135
141
  </div>
136
142
  {#if !isRoot}
137
- <Resizer direction={isColumn ? 'vertical' : 'horizontal'} {current} />
143
+ <Resizer
144
+ direction={isColumn ? 'vertical' : 'horizontal'}
145
+ {current}
146
+ disabled={!resizable}
147
+ />
138
148
  {/if}
139
149
 
140
150
  <style>
@@ -11,7 +11,11 @@
11
11
  import { layout } from '../state/layout.svelte.js';
12
12
  import { clamp, map } from '../utils/math.utils.js';
13
13
 
14
- let { direction = DIRECTIONS.HORIZONTAL, current } = $props();
14
+ let {
15
+ direction = DIRECTIONS.HORIZONTAL,
16
+ current,
17
+ disabled = false,
18
+ } = $props();
15
19
 
16
20
  let visible = $state(false);
17
21
  let isDragging = $state(false);
@@ -133,6 +137,7 @@
133
137
  class="resizer resizer--{direction}"
134
138
  class:dragging={isDragging}
135
139
  class:editing={layout.editing}
140
+ class:disabled
136
141
  >
137
142
  <div
138
143
  class="resizer-hover"
@@ -150,6 +155,10 @@
150
155
  position: relative;
151
156
  }
152
157
 
158
+ .resizer.disabled {
159
+ pointer-events: none;
160
+ }
161
+
153
162
  [class~='resizer']:last-of-type {
154
163
  display: none;
155
164
  }
@@ -177,7 +186,7 @@
177
186
  opacity: 0.1;
178
187
  }
179
188
 
180
- .resizer .resizer-hover:hover:before {
189
+ :global(body:not(.fragment-dragging)) .resizer .resizer-hover:hover:before {
181
190
  opacity: 0.25;
182
191
  }
183
192
 
@@ -1,9 +1,9 @@
1
1
  <script>
2
2
  import LayoutComponent from './LayoutComponent.svelte';
3
3
 
4
- let { size = 1, children } = $props();
4
+ let { size = 1, children, resizable = true } = $props();
5
5
  </script>
6
6
 
7
- <LayoutComponent {size} type="row">
7
+ <LayoutComponent type="row" {size} {resizable}>
8
8
  {@render children?.()}
9
9
  </LayoutComponent>
@@ -57,15 +57,15 @@
57
57
  color: var(--color-text-input-disabled);
58
58
  }
59
59
 
60
- .button:hover {
60
+ :global(body:not(.fragment-dragging)) .button:hover {
61
61
  color: var(--color-text);
62
62
 
63
63
  box-shadow: inset 0 0 0 1px
64
64
  var(--box-shadow-color-active, var(--color-active));
65
65
  }
66
66
 
67
- .button:active,
68
- .button:focus-visible {
67
+ :global(body:not(.fragment-dragging)) .button:active,
68
+ :global(body:not(.fragment-dragging)) .button:focus-visible {
69
69
  box-shadow: 0 0 0 2px
70
70
  var(--box-shadow-color-active, var(--color-active));
71
71
  }
@@ -3,19 +3,27 @@
3
3
  import TextInput from './TextInput.svelte';
4
4
  import Field from '../Field.svelte';
5
5
 
6
- let { value, context = null, key = '', disabled = false, onchange } = $props();
6
+ let {
7
+ value,
8
+ context = null,
9
+ key = '',
10
+ disabled = false,
11
+ onchange,
12
+ } = $props();
7
13
 
8
14
  let format = $derived(color.getColorFormat(value));
9
15
  let hexValue = $derived(color.toHex(value, format));
10
16
  let textValue = $state();
11
17
  let alpha = $state(1);
12
- let hasAlpha = $derived([
13
- color.FORMATS.RGBA_STRING,
14
- color.FORMATS.VEC4_STRING,
15
- color.FORMATS.VEC4_ARRAY,
16
- color.FORMATS.RGBA_OBJECT,
17
- color.FORMATS.HSLA_STRING,
18
- ].includes(format));
18
+ let hasAlpha = $derived(
19
+ [
20
+ color.FORMATS.RGBA_STRING,
21
+ color.FORMATS.VEC4_STRING,
22
+ color.FORMATS.VEC4_ARRAY,
23
+ color.FORMATS.RGBA_OBJECT,
24
+ color.FORMATS.HSLA_STRING,
25
+ ].includes(format),
26
+ );
19
27
 
20
28
  $effect(() => {
21
29
  if (hasAlpha) {
@@ -24,7 +32,7 @@
24
32
  } else {
25
33
  alpha = 1;
26
34
  }
27
- })
35
+ });
28
36
 
29
37
  $effect(() => {
30
38
  textValue = color.toString(value, format)?.toLowerCase();
@@ -36,10 +44,10 @@
36
44
  if (format === newFormat) {
37
45
  onchange(newColor);
38
46
  } else {
39
- const components = color.toComponents(newColor);
47
+ const components = color.toComponents(newColor);
40
48
  const [r, g, b] = components;
41
49
 
42
- switch(format) {
50
+ switch (format) {
43
51
  case color.FORMATS.RGB_OBJECT:
44
52
  onchange({ r, g, b });
45
53
  break;
@@ -47,7 +55,9 @@
47
55
  onchange({ r, g, b, a: alpha });
48
56
  break;
49
57
  default:
50
- onchange(color.componentsToFormat([r, g, b, alpha], format));
58
+ onchange(
59
+ color.componentsToFormat([r, g, b, alpha], format),
60
+ );
51
61
  }
52
62
  }
53
63
  }
@@ -207,7 +217,7 @@
207
217
  pointer-events: none;
208
218
  }
209
219
 
210
- .mirror:hover {
220
+ :global(body:not(.fragment-dragging)) .mirror:hover {
211
221
  box-shadow: inset 0 0 0 1px var(--box-shadow-color, var(--color-active));
212
222
  }
213
223
 
@@ -0,0 +1,52 @@
1
+ <script>
2
+ import ButtonInput from './ButtonInput.svelte';
3
+
4
+ let {
5
+ value,
6
+ label = 'import',
7
+ disabled = false,
8
+ title = '',
9
+ accept,
10
+ readAs = 'readAsText',
11
+ children,
12
+ } = $props();
13
+
14
+ /** @type {HTMLInputElement}*/
15
+ let input;
16
+
17
+ let fileReader = new FileReader();
18
+ fileReader.onload = (event) => {
19
+ value(event);
20
+ };
21
+
22
+ function handleClick(event) {
23
+ event.preventDefault();
24
+ input.click();
25
+ }
26
+
27
+ function handleChange(event) {
28
+ if (event.target.files.length > 0) {
29
+ const readAsFn = fileReader[readAs];
30
+
31
+ if (!readAsFn) {
32
+ console.error(
33
+ `readAs: '${readAs}' is not a function of FileReader.`,
34
+ );
35
+ return;
36
+ }
37
+
38
+ readAsFn.call(fileReader, event.target.files[0]);
39
+ }
40
+ }
41
+ </script>
42
+
43
+ <ButtonInput onclick={handleClick} {disabled} {label} {title}>
44
+ {@render children?.()}
45
+ </ButtonInput>
46
+ <input
47
+ class="visually-hidden"
48
+ onchange={handleChange}
49
+ type="file"
50
+ bind:this={input}
51
+ {accept}
52
+ />
@@ -53,11 +53,13 @@
53
53
  box-shadow: inset 0 0 0 1px var(--color-border-input);
54
54
  }
55
55
 
56
- .input-container:not(.disabled):hover {
56
+ :global(body:not(.fragment-dragging))
57
+ .input-container:not(.disabled):hover {
57
58
  box-shadow: inset 0 0 0 1px var(--color-active);
58
59
  }
59
60
 
60
- .input-container:not(.disabled):focus-within {
61
+ :global(body:not(.fragment-dragging))
62
+ .input-container:not(.disabled):focus-within {
61
63
  box-shadow: 0 0 0 2px var(--color-active);
62
64
  }
63
65
 
@@ -21,7 +21,7 @@
21
21
  /** @type {DOMRect}*/
22
22
  let rect;
23
23
  /** @type {boolean}*/
24
- let isDragging = false;
24
+ let isDragging = $state(false);
25
25
 
26
26
  let proximityIndex = -1;
27
27
 
@@ -30,7 +30,8 @@
30
30
  * @param {MouseEvent} event
31
31
  */
32
32
  function handleMouseDown(event) {
33
- document.body.style.userSelect = 'none';
33
+ document.body.classList.add('fragment-dragging');
34
+
34
35
  document.addEventListener('mousemove', handleMouseMove);
35
36
  document.addEventListener('mouseup', handleMouseUp);
36
37
 
@@ -85,7 +86,7 @@
85
86
  }
86
87
 
87
88
  function handleMouseUp() {
88
- document.body.style.userSelect = null;
89
+ document.body.classList.remove('fragment-dragging');
89
90
  document.removeEventListener('mousemove', handleMouseMove);
90
91
  document.removeEventListener('mouseup', handleMouseUp);
91
92
 
@@ -119,9 +120,10 @@
119
120
  <div class="interval-input" class:disabled>
120
121
  <FieldInputRow --grid-template-columns="1fr 0.5fr">
121
122
  <div
122
- class="range {isDragging ? 'dragging' : ''} "
123
+ class="range"
124
+ class:dragging={isDragging}
123
125
  bind:this={node}
124
- on:mousedown={handleMouseDown}
126
+ onmousedown={handleMouseDown}
125
127
  >
126
128
  <div class="handler" style="--position: {p1};" />
127
129
  <div class="filler" style="--p1: {p1}; --p2: {p2};"></div>
@@ -189,7 +191,7 @@
189
191
  container-type: size;
190
192
  }
191
193
 
192
- .range:hover {
194
+ :global(body:not(.fragment-dragging)) .range:hover {
193
195
  box-shadow: inset 0 0 0 1px var(--color-active);
194
196
  }
195
197
 
@@ -1,25 +1,18 @@
1
1
  <script>
2
- import { createEventDispatcher } from 'svelte';
2
+ import Keyboard from '../../inputs/Keyboard.js';
3
3
  import { map, clamp, roundToStep } from '../../utils/math.utils.js';
4
4
 
5
- let {
6
- value,
7
- min,
8
- max,
9
- step,
10
- context = null,
11
- key = '',
12
- disabled = false,
13
- onchange,
14
- } = $props();
5
+ let { value, min, max, step, disabled = false, onchange } = $props();
15
6
 
16
7
  let node;
17
8
  let rect;
18
9
 
19
- let isDragging = false;
10
+ let isDragging = $state(false);
11
+ let steppedValue = $derived(roundToStep(value, step));
20
12
 
21
13
  // handlers
22
14
  function handleMouseDown(event) {
15
+ document.body.classList.add('fragment-dragging');
23
16
  document.addEventListener('mousemove', handleMouseMove);
24
17
  document.addEventListener('mouseup', handleMouseUp);
25
18
 
@@ -47,24 +40,59 @@
47
40
  }
48
41
  }
49
42
 
43
+ function handleKeyDown(event) {
44
+ const direction = ['ArrowUp', 'ArrowRight'].includes(event.key)
45
+ ? 1
46
+ : ['ArrowDown', 'ArrowLeft'].includes(event.key)
47
+ ? -1
48
+ : 0;
49
+
50
+ const diff = Keyboard.getStepFromEvent(event) * step;
51
+
52
+ if (direction !== 0) {
53
+ const newValue = clamp(
54
+ roundToStep(value + direction * diff, step),
55
+ min,
56
+ max,
57
+ );
58
+
59
+ if (newValue !== value) {
60
+ onchange(newValue);
61
+ }
62
+ }
63
+ }
64
+
50
65
  function handleMouseUp() {
66
+ document.body.classList.remove('fragment-dragging');
51
67
  document.removeEventListener('mousemove', handleMouseMove);
52
68
  document.removeEventListener('mouseup', handleMouseUp);
53
69
 
54
70
  isDragging = false;
55
71
  }
56
72
 
57
- let progress = $derived(clamp(map(value, min, max, 0, 1), 0.0001, 1));
73
+ let progress = $derived(
74
+ clamp(map(steppedValue, min, max, 0, 1), 0.0001, 1),
75
+ );
58
76
  let opacity = $derived(progress > 0 ? 1 : 0);
59
77
  </script>
60
78
 
61
79
  <div
62
- class="progress {isDragging ? 'dragging' : ''} "
80
+ class="progress"
63
81
  bind:this={node}
64
82
  onmousedown={handleMouseDown}
83
+ onkeydown={handleKeyDown}
65
84
  class:disabled
85
+ class:dragging={isDragging}
86
+ role="slider"
87
+ aria-valuemin={min}
88
+ aria-valuemax={max}
89
+ aria-valuenow={value}
90
+ tabindex="0"
66
91
  >
67
- <div class="fill" style="--progress: {progress}; --opacity: {opacity};" />
92
+ <div
93
+ class="fill"
94
+ style="--progress: {progress}; --opacity: {opacity};"
95
+ ></div>
68
96
  </div>
69
97
 
70
98
  <style>
@@ -78,13 +106,15 @@
78
106
  background: var(--color-background-input);
79
107
  cursor: ew-resize;
80
108
  container-type: size;
109
+ outline: 0;
81
110
  }
82
111
 
83
- .progress:hover {
112
+ :global(body:not(.fragment-dragging)) .progress:hover {
84
113
  box-shadow: inset 0 0 0 1px var(--color-active);
85
114
  }
86
115
 
87
- .progress.dragging {
116
+ .progress.dragging,
117
+ :global(body:not(.fragment-dragging)) .progress:focus-visible {
88
118
  box-shadow: 0 0 0 2px var(--color-active);
89
119
  }
90
120