fragment-tools 0.2.2 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fragment-tools",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "A web development environment for creative coding",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -0,0 +1,64 @@
1
+ <script>
2
+ import { onDestroy } from 'svelte';
3
+ import { sketchesManager } from '../state/sketches.svelte';
4
+ import { rendering } from '../state/rendering.svelte';
5
+ import { layout } from '../state/layout.svelte';
6
+ import LayoutBuild from '../ui/LayoutBuild.svelte';
7
+
8
+ console.log(`Made with Fragment. https://fragment.tools`);
9
+
10
+ let sketchKey = $derived(sketchesManager.keys[0]);
11
+ let sketch = $derived(sketchesManager.sketches[sketchKey]);
12
+ let buildConfig = $derived(sketch?.buildConfig ?? {});
13
+ let persistent = $derived(buildConfig.layout?.persistent ?? false);
14
+ let styles = $derived(buildConfig?.styles ?? '');
15
+
16
+ /** @type {HTMLHeadElement} */
17
+ let head;
18
+ /** @type {HTMLStyleElement} */
19
+ let style;
20
+
21
+ $effect(() => {
22
+ if (__BUILD__) {
23
+ layout.persistent = persistent;
24
+ } else if (persistent && layout.previewing) {
25
+ console.warn(`Layout is not preserved while previewing`);
26
+ }
27
+ });
28
+
29
+ $effect(() => {
30
+ rendering.override(sketch?.buildConfig);
31
+ });
32
+
33
+ $effect(() => {
34
+ if (styles !== '') {
35
+ head = document.getElementsByTagName('head')[0];
36
+
37
+ if (style) {
38
+ head.removeChild(style);
39
+ }
40
+
41
+ style = document.createElement('style');
42
+ style.setAttribute('type', 'text/css');
43
+ style.appendChild(document.createTextNode(styles));
44
+ head.appendChild(style);
45
+ }
46
+ });
47
+
48
+ onDestroy(() => {
49
+ if (style && head) {
50
+ head.removeChild(style);
51
+ }
52
+ });
53
+ </script>
54
+
55
+ {#if sketch}
56
+ {#if buildConfig.layout?.component}
57
+ {#await buildConfig.layout.component() then layoutModule}
58
+ {@const LayoutBuildCustom = layoutModule.default}
59
+ <LayoutBuildCustom {sketchKey} {buildConfig} {sketch} />
60
+ {/await}
61
+ {:else}
62
+ <LayoutBuild {sketchKey} {sketch} {buildConfig} />
63
+ {/if}
64
+ {/if}
@@ -1,7 +1,6 @@
1
1
  <script>
2
- import { onDestroy, onMount } from 'svelte';
3
-
4
2
  import { rendering } from '../state/rendering.svelte';
3
+ import { onDestroy } from 'svelte';
5
4
  import Build from './Build.svelte';
6
5
 
7
6
  const { resizing, width, height, pixelRatio, aspectRatio, scale, preset } =
@@ -148,6 +148,9 @@
148
148
  // value(event, sketch.params);
149
149
  }}
150
150
  onchange={(v) => {
151
+ if (v?.currentTarget && v?.type === 'change') {
152
+ v = v.currentTarget.value;
153
+ }
151
154
  sketch.updateProp(key, v);
152
155
  }}
153
156
  />
@@ -181,19 +181,20 @@ class Sketch {
181
181
  params = {},
182
182
  triggers = [],
183
183
  group,
184
+ type,
184
185
  folder,
185
186
  displayName,
186
187
  } = instanceProp;
187
188
 
188
- if (value.isColor) {
189
+ if (value?.isColor) {
189
190
  value = { r: value.r, g: value.g, b: value.b };
190
- } else if (value.isVector2) {
191
+ } else if (value?.isVector2) {
191
192
  value = { x: value.x, y: value.y };
192
- } else if (value.isVector3) {
193
+ } else if (value?.isVector3) {
193
194
  value = { x: value.x, y: value.y, z: value.z };
194
- } else if (value.isVector4) {
195
+ } else if (value?.isVector4) {
195
196
  value = { x: value.x, y: value.y, z: value.z, w: value.w };
196
- } else if (value.isQuaternion) {
197
+ } else if (value?.isQuaternion) {
197
198
  value = { x: value.x, y: value.y, z: value.z, w: value.w };
198
199
  }
199
200
 
@@ -217,6 +218,7 @@ class Sketch {
217
218
  __initialValue: initialValue,
218
219
  __currentValue: value,
219
220
  __hidden,
221
+ type,
220
222
  params: structuredClone(params),
221
223
  triggers,
222
224
  group,
@@ -239,9 +241,7 @@ class Sketch {
239
241
  }
240
242
 
241
243
  if (instanceProp) {
242
- if (isObject(instanceProp.value)) {
243
- deepAssign(instanceProp.value, newValue);
244
- } else {
244
+ if (!deepEqual(instanceProp.value, newValue)) {
245
245
  instanceProp.value = newValue;
246
246
  }
247
247
 
@@ -365,7 +365,7 @@ class Sketch {
365
365
  !isFunction(instanceProp.value) &&
366
366
  !deepEqual(instanceProp.value, prop.__currentValue)
367
367
  ) {
368
- this.updateProp(key, structuredClone(instanceProp.value));
368
+ this.updateProp(key, instanceProp.value);
369
369
  }
370
370
 
371
371
  // sync displayName
@@ -440,34 +440,11 @@ class Sketch {
440
440
  for (const paramKey in instanceProp.params) {
441
441
  const instanceParam = instanceProp.params[paramKey];
442
442
  const param = prop.params[paramKey];
443
- let needsUpdate = false;
444
-
445
- if (isObject(instanceParam)) {
446
- Object.keys(instanceParam).forEach((key) => {
447
- if (isObject(instanceParam[key])) {
448
- Object.keys(instanceParam[key]).forEach(
449
- (k) => {
450
- if (
451
- instanceParam[key][k] !==
452
- param[key][k]
453
- ) {
454
- needsUpdate = true;
455
- }
456
- },
457
- );
458
- } else if (instanceParam[key] !== param[key]) {
459
- needsUpdate = true;
460
- }
461
- });
462
- } else if (instanceParam !== param) {
463
- needsUpdate = true;
464
- }
443
+ let needsUpdate = !deepEqual(instanceParam, param);
465
444
 
466
445
  if (needsUpdate) {
467
- if (needsUpdate) {
468
- prop.params[paramKey] =
469
- structuredClone(instanceParam);
470
- }
446
+ prop.params[paramKey] =
447
+ structuredClone(instanceParam);
471
448
  }
472
449
  }
473
450
  }
@@ -6,17 +6,22 @@ class Layout {
6
6
  components = $state([]);
7
7
  editing = $state(false);
8
8
  previewing = $state(false);
9
+ persistent = $state(true);
9
10
 
10
11
  getID() {
11
12
  return COMPONENT_ID++;
12
13
  }
13
14
 
14
15
  constructor() {
15
- this.key = 'layout';
16
+ this.key = __BUILD__ ? `layout${__START_TIME__}` : `layout`;
16
17
 
17
18
  $effect.root(() => {
18
19
  $effect(() => {
19
- if (!this.previewing && !__BUILD__) {
20
+ const isPersistent = __BUILD__
21
+ ? this.persistent
22
+ : !this.previewing;
23
+
24
+ if (isPersistent) {
20
25
  this.persist($state.snapshot(this.components));
21
26
  }
22
27
  });
@@ -156,6 +161,7 @@ class Layout {
156
161
  type: source.type,
157
162
  name: source.name,
158
163
  minimized: source.minimized,
164
+ headless: source.headless,
159
165
  params: source.params,
160
166
  children: [...source.children],
161
167
  }));
@@ -361,7 +361,7 @@ export class Render {
361
361
 
362
362
  if (needsUpdate) {
363
363
  console.warn(
364
- `Canvas ${attributeName} was changed from sketch to ${dimension}px to previous ${rendering[attributeName]}`,
364
+ `Canvas ${attributeName} was changed from sketch from ${rendering[attributeName]}px to ${dimension}px.`,
365
365
  );
366
366
  rendering[attributeName] = dimension;
367
367
 
@@ -460,6 +460,8 @@ export class Render {
460
460
  };
461
461
 
462
462
  this.init = async () => {
463
+ if (this.errored) return;
464
+
463
465
  clearError(this.sketch.key);
464
466
  this.mountParams = this.renderer?.onMountPreview?.(this.params);
465
467
  if (this.mountParams && this.mountParams.canvas !== this.canvas) {
@@ -599,7 +601,8 @@ export class Render {
599
601
  sketch.beforeRecord.forEach((fn) => fn(params));
600
602
  },
601
603
  onTick: ({ time, deltaTime }) => {
602
- this.loop(time);
604
+ this.time += deltaTime;
605
+ this.loop(this.time);
603
606
  },
604
607
  onComplete: (params) => {
605
608
  sketch.afterRecord.forEach((fn) => fn(params));
@@ -50,11 +50,22 @@ export function deepAssign(target, source) {
50
50
  }
51
51
 
52
52
  export function deepEqual(target, source) {
53
- if (isObject(target) && isObject(target)) {
53
+ if (isObject(target) && isObject(source)) {
54
54
  let isEqual = true;
55
- for (const key in source) {
56
- if (isEqual) {
57
- isEqual = deepEqual(target[key], source[key]);
55
+
56
+ if (
57
+ Array.isArray(target) &&
58
+ Array.isArray(source) &&
59
+ target.length !== source.length
60
+ ) {
61
+ isEqual = false;
62
+ }
63
+
64
+ if (isEqual) {
65
+ for (const key in source) {
66
+ if (isEqual) {
67
+ isEqual = deepEqual(target[key], source[key]);
68
+ }
58
69
  }
59
70
  }
60
71
 
@@ -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,11 @@
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';
38
40
 
39
41
  let {
40
42
  key,
41
- value = null,
43
+ value,
42
44
  initialValue = value,
43
45
  context = null,
44
46
  params = $bindable({}),
@@ -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
 
@@ -16,10 +16,11 @@
16
16
  let node;
17
17
  let rect;
18
18
 
19
- let isDragging = false;
19
+ let isDragging = $state(false);
20
20
 
21
21
  // handlers
22
22
  function handleMouseDown(event) {
23
+ document.body.classList.add('fragment-dragging');
23
24
  document.addEventListener('mousemove', handleMouseMove);
24
25
  document.addEventListener('mouseup', handleMouseUp);
25
26
 
@@ -48,6 +49,7 @@
48
49
  }
49
50
 
50
51
  function handleMouseUp() {
52
+ document.body.classList.remove('fragment-dragging');
51
53
  document.removeEventListener('mousemove', handleMouseMove);
52
54
  document.removeEventListener('mouseup', handleMouseUp);
53
55
 
@@ -59,10 +61,11 @@
59
61
  </script>
60
62
 
61
63
  <div
62
- class="progress {isDragging ? 'dragging' : ''} "
64
+ class="progress"
63
65
  bind:this={node}
64
66
  onmousedown={handleMouseDown}
65
67
  class:disabled
68
+ class:dragging={isDragging}
66
69
  >
67
70
  <div class="fill" style="--progress: {progress}; --opacity: {opacity};" />
68
71
  </div>
@@ -80,7 +83,7 @@
80
83
  container-type: size;
81
84
  }
82
85
 
83
- .progress:hover {
86
+ :global(body:not(.fragment-dragging)) .progress:hover {
84
87
  box-shadow: inset 0 0 0 1px var(--color-active);
85
88
  }
86
89
 
@@ -1,4 +1,5 @@
1
1
  <script>
2
+ import { isObject } from '../../state/utils.svelte';
2
3
  import SelectChevrons from '../SelectChevrons.svelte';
3
4
 
4
5
  let {
@@ -10,55 +11,48 @@
10
11
  onchange = () => {},
11
12
  } = $props();
12
13
 
13
- function toStringifiedValue(option, optionType = typeof option) {
14
- if (option === null) {
15
- return null;
16
- } else if (option === undefined) {
17
- return undefined;
18
- } else if (optionType === 'object') {
19
- return toStringifiedValue(option.value);
20
- } else if (optionType === 'function') {
21
- return option.name;
14
+ function toStringifiedValue(value) {
15
+ if (typeof value === 'function') {
16
+ return `${value.name}()`;
22
17
  }
23
18
 
24
- return option.toString();
19
+ return String(value);
25
20
  }
26
21
 
27
- let sanitizedOptions = $derived(
28
- options.map((option) => {
29
- let optionType = typeof option;
30
- let disabled =
31
- optionType === 'object' && typeof option.disabled === 'boolean'
32
- ? option.disabled
33
- : false;
34
- let value = optionType === 'object' ? option.value : option;
35
-
36
- let stringValue = toStringifiedValue(option);
37
-
38
- let label = option.label ?? stringValue;
39
-
40
- return {
41
- label,
42
- value,
43
- stringValue,
44
- disabled,
45
- };
46
- }),
47
- );
22
+ function createOption(option) {
23
+ let value =
24
+ isObject(option) && 'value' in option ? option.value : option;
25
+ let label = option?.label ?? toStringifiedValue(value);
26
+ let disabled = option?.disabled ?? false;
27
+ let stringValue = toStringifiedValue(value);
28
+
29
+ return {
30
+ label,
31
+ value,
32
+ stringValue,
33
+ disabled,
34
+ };
35
+ }
36
+
37
+ let sanitizedOptions = $derived.by(() => {
38
+ let opts = options.map((option, optionIndex) => {
39
+ return createOption(option, optionIndex);
40
+ });
41
+
42
+ return opts;
43
+ });
48
44
 
49
45
  let sanitizedValue = $derived(
50
- sanitizedOptions.find((opt) => opt.value === value),
46
+ sanitizedOptions.find((opt) => opt.value === value) ??
47
+ sanitizedOptions[0],
51
48
  );
52
49
 
53
50
  function handleChange(event) {
54
- const index = sanitizedOptions.findIndex(
51
+ const sanitizedOption = sanitizedOptions.find(
55
52
  (opt) => opt.stringValue === event.currentTarget.value,
56
53
  );
57
54
 
58
- const option = options[index];
59
- const newValue = typeof option === 'object' ? option.value : option;
60
-
61
- onchange(newValue);
55
+ onchange(sanitizedOption.value);
62
56
  }
63
57
  </script>
64
58
 
@@ -84,9 +78,7 @@
84
78
  >
85
79
  {/each}
86
80
  </select>
87
- {#if sanitizedOptions.length > 1}
88
- <SelectChevrons />
89
- {/if}
81
+ <SelectChevrons />
90
82
  </div>
91
83
  </div>
92
84
 
@@ -113,7 +105,9 @@
113
105
  background-color: var(--color-background-input);
114
106
  }
115
107
 
116
- .select-input:not(.disabled) .container:hover {
108
+ :global(body:not(.fragment-dragging))
109
+ .select-input:not(.disabled)
110
+ .container:hover {
117
111
  box-shadow: inset 0 0 0 1px var(--color-active);
118
112
  }
119
113
 
@@ -10,6 +10,7 @@ export const fieldTypes = {
10
10
  COLOR: 'color',
11
11
  BUTTON: 'button',
12
12
  DOWNLOAD: 'download',
13
+ IMPORT: 'import',
13
14
  IMAGE: 'image',
14
15
  INTERVAL: 'interval',
15
16
  };
@@ -54,6 +54,20 @@ body {
54
54
  overscroll-behavior: none;
55
55
  }
56
56
 
57
+ body.fragment-dragging {
58
+ -webkit-user-select: none;
59
+ user-select: none;
60
+ }
61
+
62
+ body.fragment-dragging input {
63
+ -webkit-user-select: none;
64
+ user-select: none;
65
+ }
66
+
67
+ body.fragment-dragging * {
68
+ cursor: ew-resize !important;
69
+ }
70
+
57
71
  #app {
58
72
  position: fixed;
59
73
  top: 0;
@@ -1,91 +0,0 @@
1
- <script>
2
- import { onDestroy } from 'svelte';
3
-
4
- import Monitor from '../modules/Monitor.svelte';
5
- import Params from '../modules/Params.svelte';
6
- import FloatingParams from './FloatingParams.svelte';
7
- import Column from './LayoutColumn.svelte';
8
- import Row from './LayoutRow.svelte';
9
- import { sketchesManager } from '../state/sketches.svelte';
10
- import { rendering } from '../state/rendering.svelte';
11
-
12
- console.log(`Made with Fragment. https://fragment.tools`);
13
-
14
- let sketchKey = $derived(sketchesManager.keys[0]);
15
-
16
- let sketch = $derived(sketchesManager.sketches[sketchKey]);
17
-
18
- let gui = $derived(sketch?.buildConfig?.gui);
19
- let guiOutput = $derived(gui?.output);
20
- let guiAlign = $derived(gui?.align ?? 'right');
21
- let guiHidden = $derived(gui?.hidden);
22
- let guiSize = $derived(gui?.size ?? 0.25);
23
- let guiMinimize = $derived(gui?.minimize);
24
- let guiPosition = $derived(gui?.position);
25
- let styles = $derived(sketch?.buildConfig?.styles ?? '');
26
-
27
- /** @type {HTMLHeadElement} */
28
- let head;
29
- /** @type {HTMLStyleElement} */
30
- let style;
31
-
32
- $effect(() => {
33
- rendering.override(sketch?.buildConfig);
34
- });
35
-
36
- $effect(() => {
37
- if (styles !== '') {
38
- head = document.getElementsByTagName('head')[0];
39
-
40
- if (style) {
41
- head.removeChild(style);
42
- }
43
-
44
- style = document.createElement('style');
45
- style.setAttribute('type', 'text/css');
46
- style.appendChild(document.createTextNode(styles));
47
- head.appendChild(style);
48
- }
49
- });
50
-
51
- onDestroy(() => {
52
- if (style && head) {
53
- head.removeChild(style);
54
- }
55
- });
56
- </script>
57
-
58
- {#if sketch}
59
- {#if guiPosition === 'fixed'}
60
- <Row>
61
- {#if guiAlign === 'left'}
62
- <Column size={guiSize}>
63
- <Params />
64
- </Column>
65
- <Column size={1 - guiSize}>
66
- <Monitor params={{ selected: sketchKey }} />
67
- </Column>
68
- {:else}
69
- <Column size={1 - guiSize}>
70
- <Monitor params={{ selected: sketchKey }} />
71
- </Column>
72
- <Column size={guiSize}>
73
- <Params />
74
- </Column>
75
- {/if}
76
- </Row>
77
- {:else}
78
- <Row>
79
- <Monitor headless {sketchKey} params={{ selected: sketchKey }} />
80
- {#if gui}
81
- <FloatingParams
82
- output={guiOutput}
83
- align={guiAlign}
84
- size={guiSize}
85
- hidden={guiHidden}
86
- minimize={guiMinimize}
87
- />
88
- {/if}
89
- </Row>
90
- {/if}
91
- {/if}