fragment-tools 0.2.10 → 0.2.12

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,8 +1,8 @@
1
1
  {
2
2
  "name": "fragment-tools",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
4
4
  "description": "A web development environment for creative coding",
5
- "main": "index.js",
5
+ "main": "./src/index.js",
6
6
  "bin": {
7
7
  "fragment": "bin/index.js"
8
8
  },
@@ -30,7 +30,6 @@
30
30
  "convert-length": "^1.0.1",
31
31
  "get-port": "^7.1.0",
32
32
  "gifenc": "^1.0.3",
33
- "glslify": "^7.1.1",
34
33
  "is-unicode-supported": "^2.0.0",
35
34
  "kleur": "^4.1.4",
36
35
  "mediabunny": "^1.13.3",
@@ -1,5 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import fs from 'node:fs';
3
+ import url from 'node:url';
3
4
  import { defineConfig, loadConfigFromFile, mergeConfig } from 'vite';
4
5
  import { svelte } from '@sveltejs/vite-plugin-svelte';
5
6
 
@@ -8,30 +9,47 @@ import { __dirname, file } from './utils.js';
8
9
  import { log } from './log.js';
9
10
  import sketches from './plugins/sketches.js';
10
11
 
12
+ /**
13
+ *
14
+ * @param {{ cwd: string, filepath: string | undefined }} params
15
+ * @returns {Promise<import('../types/config.js').Config>}
16
+ */
11
17
  export async function loadConfig({ cwd, filepath }) {
12
18
  try {
13
- let filename = `fragment.config.js`;
14
- let configFile = filepath ? filepath : filename;
15
19
  let configRoot = cwd;
16
- let resolvedPath = path.resolve(cwd, configFile);
20
+ let filenames = [`fragment.config.js`, `fragment.config.ts`];
17
21
 
18
- if (!fs.existsSync(resolvedPath)) {
19
- if (filepath) {
20
- log.error(`Config file not found: ${resolvedPath}`);
21
- }
22
- return {};
22
+ if (filepath) {
23
+ filenames = [filepath, ...filenames];
23
24
  }
24
25
 
25
- log.info(`Extending configuration from ${resolvedPath}`);
26
+ let filepaths = filenames.map((filename) => {
27
+ return path.resolve(configRoot, filename);
28
+ });
26
29
 
27
- const { config } = await loadConfigFromFile(
28
- {
29
- command: 'build',
30
- mode: 'dev',
31
- },
32
- configFile,
33
- configRoot,
30
+ let resolvedIndex = filepaths.findIndex((filepath) =>
31
+ fs.existsSync(filepath),
34
32
  );
33
+ /** @type {string|undefined} */
34
+ let resolvedPath = filepaths[resolvedIndex];
35
+
36
+ if (filepath && resolvedIndex !== 0) {
37
+ log.error(`Config file not found: ${filepath}`);
38
+ }
39
+
40
+ if (!resolvedPath) {
41
+ return {};
42
+ }
43
+
44
+ let configFile = path.relative(cwd, resolvedPath);
45
+
46
+ log.info(`Extending configuration from ${configFile}`);
47
+
48
+ const config = (
49
+ await import(
50
+ `${url.pathToFileURL(configFile).href}?ts=${Date.now()}`
51
+ )
52
+ ).default;
35
53
 
36
54
  return config;
37
55
  } catch (error) {
@@ -43,9 +61,10 @@ export async function loadConfig({ cwd, filepath }) {
43
61
  /**
44
62
  * Create Vite config from entries
45
63
  * @param {string[]} entries
46
- * @param {options} options
64
+ * @param {object} [options]
47
65
  * @param {boolean} [options.dev=false]
48
66
  * @param {boolean} [options.build=false]
67
+ * @param {string} [configFilepath]
49
68
  * @param {string} [cwd=process.cwd()]
50
69
  * @returns {import('vite').UserConfig}
51
70
  */
@@ -63,8 +82,8 @@ export async function createConfig(
63
82
  log.info(`Creating Vite configuration...`);
64
83
 
65
84
  const config = await loadConfig({
66
- filepath: configFilepath,
67
85
  cwd,
86
+ filepath: configFilepath,
68
87
  });
69
88
 
70
89
  return mergeConfig(
@@ -1,7 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import fs from 'node:fs';
3
3
  import { readFile } from 'node:fs/promises';
4
- import glslify from 'glslify';
5
4
  import { log, dim, green, yellow } from '../log.js';
6
5
 
7
6
  /**
@@ -232,10 +231,6 @@ ${keyword}${shaderParts[1]}
232
231
  shaderPath,
233
232
  );
234
233
 
235
- code = glslify(code, {
236
- basedir: process.cwd(),
237
- });
238
-
239
234
  if (server) {
240
235
  code = addShaderFilepath(code, shaderPath);
241
236
  }
package/src/cli/run.js CHANGED
@@ -26,17 +26,23 @@ import hotShaderReplacement from './plugins/hot-shader-replacement.js';
26
26
  */
27
27
  export async function run(entry, options = {}) {
28
28
  let fragmentServer;
29
+ /** @type {import('node:fs').FSWatcher} */
30
+ let watcher;
29
31
 
30
32
  const cwd = process.cwd();
31
33
  const command = `run`;
32
34
  const prefix = log.prefix(command);
35
+
36
+ const stop = () => {
37
+ fragmentServer?.close();
38
+ watcher?.close();
39
+ };
40
+
33
41
  const exit = () => {
34
42
  process.off('SIGTERM', exit);
35
43
  process.off('exit', exit);
36
44
 
37
- if (fragmentServer) {
38
- fragmentServer.close();
39
- }
45
+ stop();
40
46
 
41
47
  console.log();
42
48
  };
@@ -62,9 +68,10 @@ export async function run(entry, options = {}) {
62
68
  );
63
69
  }
64
70
 
71
+ const hasTSFiles = entries.some((entry) => entry.endsWith('ts'));
65
72
  const tsConfigDirpath = path.join(cwd, FRAGMENT_DIRECTORY);
66
73
  const tsConfigFilepath = path.join(tsConfigDirpath, 'tsconfig.json');
67
- if (!fs.existsSync(tsConfigFilepath)) {
74
+ if (!fs.existsSync(tsConfigFilepath) && hasTSFiles) {
68
75
  await createTsConfigFile(cwd);
69
76
  }
70
77
 
@@ -106,6 +113,22 @@ export async function run(entry, options = {}) {
106
113
  ],
107
114
  }),
108
115
  );
116
+
117
+ watcher = fs.watch(cwd, (eventType, filename) => {
118
+ if (
119
+ ['fragment.config.js', 'fragment.config.ts'].includes(
120
+ filename,
121
+ ) ||
122
+ options.configFilepath?.includes(filename)
123
+ ) {
124
+ log.warn(`${filename} has changed. Restarting...`);
125
+ console.log();
126
+ server.close();
127
+ stop();
128
+ run(entry, options);
129
+ }
130
+ });
131
+
109
132
  await server.listen();
110
133
 
111
134
  // line break after logs
@@ -130,8 +153,6 @@ export async function run(entry, options = {}) {
130
153
 
131
154
  // line break before fragment logs
132
155
  log.message();
133
-
134
- return server;
135
156
  } catch (error) {
136
157
  // line break before error
137
158
  log.message();
@@ -1,4 +1,4 @@
1
- import { Init, Resize, Update } from '@fragment/types';
1
+ import type { Init, Resize, Update } from '@fragment/types';
2
2
  import { defineProps } from '@fragment/types/utils';
3
3
 
4
4
  export const props = defineProps({});
@@ -1,3 +1,5 @@
1
+ let resolution = { x: 0, y: 0 };
2
+
1
3
  export const props = {};
2
4
 
3
5
  /**
@@ -24,8 +26,11 @@ export const init = ({ canvas, context, width, height }) => {};
24
26
  * @param {number} params.playcount
25
27
  */
26
28
  export const update = ({ context, width, height, pixelRatio }) => {
29
+ const w = width * pixelRatio;
30
+ const h = height * pixelRatio;
31
+
27
32
  context.fillStyle = 'rgb(0, 255, 0)';
28
- context.fillRect(0, 0, width * pixelRatio, height * pixelRatio);
33
+ context.fillRect(0, 0, w, h);
29
34
  };
30
35
 
31
36
  /**
@@ -35,6 +40,9 @@ export const update = ({ context, width, height, pixelRatio }) => {
35
40
  * @param {number} params.height
36
41
  * @param {number} params.pixelRatio
37
42
  */
38
- export const resize = ({ width, height }) => {};
43
+ export const resize = ({ width, height, pixelRatio }) => {
44
+ resolution.x = width * pixelRatio;
45
+ resolution.y = height * pixelRatio;
46
+ };
39
47
 
40
48
  export const rendering = '2d';
@@ -1,4 +1,4 @@
1
- import { Init, Rendering, Resize, Update } from '@fragment/types';
1
+ import type { Init, Rendering, Resize, Update } from '@fragment/types';
2
2
  import { defineProps } from '@fragment/types/utils';
3
3
 
4
4
  export const props = defineProps({});
@@ -11,8 +11,11 @@ export const update: Update<'2d'> = ({
11
11
  height,
12
12
  pixelRatio,
13
13
  }) => {
14
+ const w = width * pixelRatio;
15
+ const h = height * pixelRatio;
16
+
14
17
  context.fillStyle = 'rgb(0, 255, 0)';
15
- context.fillRect(0, 0, width * pixelRatio, height * pixelRatio);
18
+ context.fillRect(0, 0, w, h);
16
19
  };
17
20
 
18
21
  export const resize: Resize<'2d'> = ({}) => {};
@@ -1,4 +1,4 @@
1
- import { Init, Rendering, Update } from '@fragment/types';
1
+ import type { Init, Rendering, Update } from '@fragment/types';
2
2
 
3
3
  import fragmentShader from './fragment.fs';
4
4
 
@@ -1,6 +1,6 @@
1
1
  import p5 from 'p5';
2
2
 
3
- import { Init, Rendering, Update } from '@fragment/types';
3
+ import type { Init, Rendering, Update } from '@fragment/types';
4
4
  import { defineProps } from '@fragment/types/utils';
5
5
 
6
6
  export const props = defineProps({});
@@ -1,6 +1,6 @@
1
1
  import p5, { type Shader } from 'p5';
2
2
 
3
- import { Init, Rendering, Update } from '@fragment/types';
3
+ import type { Init, Rendering, Update } from '@fragment/types';
4
4
  import { defineProps } from '@fragment/types/utils';
5
5
 
6
6
  import fragmentShader from './fragment.fs';
@@ -6,9 +6,11 @@ import fragmentShader from './fragment.fs';
6
6
  let scene;
7
7
  /** @type {THREE.OrthographicCamera} */
8
8
  let camera;
9
+ /** @type {THREE.Vector2} */
10
+ let resolution = new THREE.Vector2();
9
11
 
10
12
  let uniforms = {
11
- uResolution: { value: new THREE.Vector2() },
13
+ uResolution: { value: resolution },
12
14
  uTime: { value: 0 },
13
15
  };
14
16
 
@@ -87,8 +89,8 @@ export const update = ({ renderer, time, deltaTime }) => {
87
89
  * @param {number} params.pixelRatio
88
90
  */
89
91
  export const resize = ({ width, height, pixelRatio }) => {
90
- uniforms.uResolution.value.x = width * pixelRatio;
91
- uniforms.uResolution.value.y = height * pixelRatio;
92
+ resolution.x = width * pixelRatio;
93
+ resolution.y = height * pixelRatio;
92
94
 
93
95
  camera.left = -width * 0.5;
94
96
  camera.right = width * 0.5;
@@ -1,13 +1,14 @@
1
1
  import * as THREE from 'three';
2
2
 
3
- import { Init, Rendering, Resize, Update } from '@fragment/types';
3
+ import type { Init, Rendering, Resize, Update } from '@fragment/types';
4
4
 
5
5
  import fragmentShader from './fragment.fs';
6
6
 
7
7
  let scene: THREE.Scene;
8
8
  let camera: THREE.OrthographicCamera;
9
+ let resolution = new THREE.Vector2();
9
10
  let uniforms = {
10
- uResolution: { value: new THREE.Vector2() },
11
+ uResolution: { value: resolution },
11
12
  uTime: { value: 0 },
12
13
  };
13
14
 
@@ -54,8 +55,8 @@ export const update: Update<'three'> = ({ renderer, time }) => {
54
55
  };
55
56
 
56
57
  export const resize: Resize<'three'> = ({ width, height, pixelRatio }) => {
57
- uniforms.uResolution.value.x = width * pixelRatio;
58
- uniforms.uResolution.value.y = height * pixelRatio;
58
+ resolution.x = width * pixelRatio;
59
+ resolution.y = height * pixelRatio;
59
60
 
60
61
  camera.left = -width * 0.5;
61
62
  camera.right = width * 0.5;
@@ -4,6 +4,8 @@ import * as THREE from 'three';
4
4
  let scene;
5
5
  /** @type {THREE.OrthographicCamera} */
6
6
  let camera;
7
+ /** @type {THREE.Vector2} */
8
+ let resolution = new THREE.Vector2();
7
9
 
8
10
  /**
9
11
  * @param {object} params
@@ -49,7 +51,10 @@ export const update = ({ renderer, time, deltaTime }) => {
49
51
  * @param {number} params.height
50
52
  * @param {number} params.pixelRatio
51
53
  */
52
- export const resize = ({ width, height }) => {
54
+ export const resize = ({ width, height, pixelRatio }) => {
55
+ resolution.x = width * pixelRatio;
56
+ resolution.y = height * pixelRatio;
57
+
53
58
  camera.left = -width * 0.5;
54
59
  camera.right = width * 0.5;
55
60
  camera.top = height * 0.5;
@@ -1,9 +1,10 @@
1
1
  import * as THREE from 'three';
2
2
 
3
- import { Init, Rendering, Resize, Update } from '@fragment/types';
3
+ import type { Init, Rendering, Resize, Update } from '@fragment/types';
4
4
 
5
5
  let scene: THREE.Scene;
6
6
  let camera: THREE.OrthographicCamera;
7
+ let resolution = new THREE.Vector2();
7
8
 
8
9
  export const init: Init<'three'> = ({}) => {
9
10
  scene = new THREE.Scene();
@@ -17,7 +18,10 @@ export const update: Update<'three'> = ({ renderer }) => {
17
18
  renderer.render(scene, camera);
18
19
  };
19
20
 
20
- export const resize: Resize<'three'> = ({ width, height }) => {
21
+ export const resize: Resize<'three'> = ({ width, height, pixelRatio }) => {
22
+ resolution.x = width * pixelRatio;
23
+ resolution.y = height * pixelRatio;
24
+
21
25
  camera.left = -width * 0.5;
22
26
  camera.right = width * 0.5;
23
27
  camera.top = height * 0.5;
@@ -4,6 +4,8 @@ import * as THREE from 'three';
4
4
  let scene;
5
5
  /** @type {THREE.OrthographicCamera} */
6
6
  let camera;
7
+ /** @type {THREE.Vector2} */
8
+ let resolution = new THREE.Vector2();
7
9
 
8
10
  /**
9
11
  * @param {object} params
@@ -50,7 +52,10 @@ export const update = ({ renderer, time, deltaTime }) => {
50
52
  * @param {number} params.height
51
53
  * @param {number} params.pixelRatio
52
54
  */
53
- export const resize = ({ width, height }) => {
55
+ export const resize = ({ width, height, pixelRatio }) => {
56
+ resolution.x = width * pixelRatio;
57
+ resolution.y = height * pixelRatio;
58
+
54
59
  camera.aspect = width / height;
55
60
  camera.updateProjectionMatrix();
56
61
  };
@@ -1,9 +1,10 @@
1
1
  import * as THREE from 'three';
2
2
 
3
- import { Init, Rendering, Resize, Update } from '@fragment/types';
3
+ import type { Init, Rendering, Resize, Update } from '@fragment/types';
4
4
 
5
5
  let scene: THREE.Scene;
6
6
  let camera: THREE.PerspectiveCamera;
7
+ let resolution = new THREE.Vector2();
7
8
 
8
9
  export const init: Init<'three'> = ({}) => {
9
10
  scene = new THREE.Scene();
@@ -18,7 +19,10 @@ export const update: Update<'three'> = ({ renderer }) => {
18
19
  renderer.render(scene, camera);
19
20
  };
20
21
 
21
- export const resize: Resize<'three'> = ({ width, height }) => {
22
+ export const resize: Resize<'three'> = ({ width, height, pixelRatio }) => {
23
+ resolution.x = width * pixelRatio;
24
+ resolution.y = height * pixelRatio;
25
+
22
26
  camera.aspect = width / height;
23
27
  camera.updateProjectionMatrix();
24
28
  };
@@ -151,6 +151,7 @@
151
151
  {disabled}
152
152
  bind:params={sketchProps[key].params}
153
153
  bind:triggers={prop.triggers}
154
+ trackChanges
154
155
  onclick={(event) => {
155
156
  sketch.version++;
156
157
  // value(event, sketch.params);
@@ -14,7 +14,7 @@ import { clearError } from '../state/errors.svelte';
14
14
  * @typedef {object} PreviewThreeRenderer
15
15
  * @property {number} id
16
16
  * @property {THREE.Scene} scene
17
- * @property {THREE.renderer} renderer
17
+ * @property {THREE.WebGLrenderer} renderer
18
18
  * @property {rendered} boolean
19
19
  */
20
20
 
@@ -32,7 +32,7 @@ let previews = [];
32
32
  * @returns {MountParamsThreeRenderer}
33
33
  */
34
34
  export let onMountPreview = ({ id, canvas }) => {
35
- let renderer = new WebGLRenderer({ antialias: true, canvas });
35
+ let renderer = new WebGLRenderer({ antialias: true });
36
36
 
37
37
  const render = renderer.render;
38
38
 
@@ -125,6 +125,7 @@ export let onDestroyPreview = ({ id }) => {
125
125
  const { renderer } = preview;
126
126
  clearError(renderer.getContext().__uuid);
127
127
  renderer.dispose();
128
+ renderer.forceContextLoss();
128
129
  previews.splice(previewIndex, 1);
129
130
  }
130
131
  };
@@ -481,7 +481,7 @@ class Sketch {
481
481
  fieldgroups.forEach((fieldgroup) => {
482
482
  const hasAllFieldsHidden = fieldgroup.children
483
483
  .filter((child) => child.type === 'field')
484
- .every((child) => this.props[child.key].__hidden());
484
+ .every((child) => this.props[child.key]?.__hidden());
485
485
  const hasAllFieldgroupsHidden = fieldgroup.children
486
486
  .filter((child) => child.type === 'fieldgroup')
487
487
  .every((child) => child.hidden);
@@ -94,6 +94,10 @@ class Rendering {
94
94
  this.estimateRefreshRate();
95
95
  }
96
96
 
97
+ /**
98
+ *
99
+ * @param {string} renderingMode
100
+ */
97
101
  loadRenderer(renderingMode) {
98
102
  if (__THREE_RENDERER__ && renderingMode === 'three') {
99
103
  return import('../renderers/THREERenderer.js');
@@ -321,7 +325,8 @@ export class Render {
321
325
  this.time = 0;
322
326
  this.recording = false;
323
327
 
324
- let resizeTimeout;
328
+ /** @type {number|null} */
329
+ let resizeTimeout = null;
325
330
 
326
331
  $effect.pre(() => {
327
332
  const { width, height, pixelRatio } = rendering;
@@ -330,7 +335,10 @@ export class Render {
330
335
  if (resizeTimeout) clearTimeout(resizeTimeout);
331
336
 
332
337
  resizeTimeout = setTimeout(() => {
333
- clearTimeout(resizeTimeout);
338
+ if (resizeTimeout) {
339
+ clearTimeout(resizeTimeout);
340
+ }
341
+
334
342
  resizeTimeout = null;
335
343
 
336
344
  this.resize(width, height, pixelRatio);
@@ -399,6 +407,7 @@ export class Render {
399
407
  this.then = performance.now();
400
408
 
401
409
  this.playhead = 0;
410
+ this.playheadLast = 0;
402
411
  this.playcount = 0;
403
412
  this.frame = 0;
404
413
 
@@ -443,24 +452,23 @@ export class Render {
443
452
  return;
444
453
  }
445
454
 
446
- let playhead = time / 1000 / duration;
447
- playhead %= 1;
448
- let playcount = 0;
455
+ let totalPlayhead = time / 1000 / duration;
456
+ let playhead = fps === 0 ? 0 : totalPlayhead % 1;
457
+
458
+ if (playhead < this.playheadLast) {
459
+ this.playcount++;
460
+ }
461
+ this.playheadLast = playhead;
462
+
449
463
  if (isFinite(interval)) {
450
464
  // round values for low framerates
451
465
  playhead = Math.floor(playhead / interval) * interval;
452
466
  }
453
- if (isFinite(duration)) {
454
- playcount = Math.floor(this.timeTotal / 1000 / duration);
455
- }
456
- if (fps === 0) {
457
- playhead = 0;
458
- }
467
+
459
468
  const now = performance.now();
460
469
  const deltaTime = now - this.thenLoop;
461
470
 
462
471
  this.playhead = playhead;
463
- this.playcount = playcount;
464
472
  this.frame = Math.floor(map(playhead, 0, 1, 1, frameCount + 1));
465
473
  if (
466
474
  this.elapsed === 0 ||
@@ -499,6 +507,9 @@ export class Render {
499
507
 
500
508
  this.raf = requestAnimationFrame(() => {
501
509
  this.time = new Date().getTime() - rendering.today;
510
+ this.initialTime = this.time;
511
+ this.playheadLast = 0;
512
+ this.playcount = 0;
502
513
  this.then = this.time;
503
514
  this.thenLoop = performance.now();
504
515
  this.update(this.time);
@@ -522,6 +533,14 @@ export class Render {
522
533
  });
523
534
  }
524
535
 
536
+ /**
537
+ *
538
+ * @param {Object} params
539
+ * @param {HTMLElement} params.container
540
+ * @param {HTMLCanvasElement} [params.canvas]
541
+ * @param {string} params.context
542
+ * @returns
543
+ */
525
544
  createCanvas({
526
545
  container,
527
546
  canvas = document.createElement('canvas'),
@@ -543,6 +562,10 @@ export class Render {
543
562
  return canvas;
544
563
  }
545
564
 
565
+ /**
566
+ *
567
+ * @param {HTMLCanvasElement} canvas
568
+ */
546
569
  destroyCanvas(canvas) {
547
570
  if (canvas) {
548
571
  this.observer.disconnect();
@@ -551,12 +574,11 @@ export class Render {
551
574
  canvas.onmousemove = null;
552
575
  canvas.onmouseup = null;
553
576
  canvas.onclick = null;
554
- canvas = null;
555
577
  }
556
578
  }
557
579
 
558
580
  async screenshot({
559
- filename = this.sketch.name ?? this.sketch.key,
581
+ filename = this.sketch.key,
560
582
  pattern = this.sketch.filenamePattern,
561
583
  exportDir = this.sketch.exportDir,
562
584
  } = {}) {
@@ -616,6 +638,8 @@ export class Render {
616
638
  onStart: (params) => {
617
639
  this.time = 0;
618
640
  this.elapsed = 0;
641
+ this.playcount = 0;
642
+ this.playheadLast = 0;
619
643
  this.thenLoop = performance.now();
620
644
 
621
645
  sketch.beforeRecord.forEach((fn) => fn(params));
@@ -686,6 +710,10 @@ export class Render {
686
710
  }
687
711
  }
688
712
 
713
+ /**
714
+ *
715
+ * @param {number} time
716
+ */
689
717
  update(time) {
690
718
  if (!this.paused) {
691
719
  const deltaTime = time - this.then;
@@ -705,6 +733,8 @@ export class Render {
705
733
  invalidate() {
706
734
  this.time = 0;
707
735
  this.elapsed = 0;
736
+ this.playcount = 0;
737
+ this.playheadLast = 0;
708
738
  }
709
739
 
710
740
  dispose() {
@@ -4,6 +4,7 @@
4
4
  import CheckboxInput from './fields/CheckboxInput.svelte';
5
5
  import VectorInput from './fields/VectorInput.svelte';
6
6
  import TextInput from './fields/TextInput.svelte';
7
+ import TextareaInput from './fields/TextareaInput.svelte';
7
8
  import ColorInput from './fields/ColorInput.svelte';
8
9
  import ListInput from './fields/ListInput.svelte';
9
10
  import ButtonInput from './fields/ButtonInput.svelte';
@@ -17,6 +18,7 @@
17
18
  [`${fieldTypes.VEC}`]: VectorInput,
18
19
  [`${fieldTypes.CHECKBOX}`]: CheckboxInput,
19
20
  [`${fieldTypes.TEXT}`]: TextInput,
21
+ [`${fieldTypes.TEXTAREA}`]: TextareaInput,
20
22
  [`${fieldTypes.LIST}`]: ListInput,
21
23
  [`${fieldTypes.COLOR}`]: ColorInput,
22
24
  [`${fieldTypes.BUTTON}`]: ButtonInput,
@@ -52,6 +54,7 @@
52
54
  onchange,
53
55
  onclick = () => {},
54
56
  children,
57
+ trackChanges = false,
55
58
  triggers = $bindable([]),
56
59
  } = $props();
57
60
 
@@ -63,10 +66,18 @@
63
66
 
64
67
  onchange(value);
65
68
  },
69
+ /**
70
+ *
71
+ * @param {MouseEvent} event
72
+ */
66
73
  button: (event) => {
67
74
  value(event);
68
75
  onclick(event);
69
76
  },
77
+ /**
78
+ *
79
+ * @param {MouseEvent} event
80
+ */
70
81
  download: async (event) => {
71
82
  try {
72
83
  let [data, filename] = await value(event);
@@ -105,7 +116,12 @@
105
116
  fieldType === fieldTypes.BUTTON),
106
117
  );
107
118
  let triggersActive = $derived(triggers.length > 0);
119
+ let changed = $derived(trackChanges && !deepEqual(value, initialValue));
108
120
 
121
+ /**
122
+ *
123
+ * @param {MouseEvent} event
124
+ */
109
125
  function toggleTriggers(event) {
110
126
  event.preventDefault();
111
127
 
@@ -123,16 +139,15 @@
123
139
  };
124
140
  }
125
141
 
126
- function hasChanged(current, next) {
127
- const changed = !deepEqual(current, next);
128
- return changed;
142
+ function restoreInitialValue() {
143
+ onchange(initialValue);
129
144
  }
130
145
  </script>
131
146
 
132
147
  <div
133
148
  class="field"
134
149
  class:disabled
135
- class:changed={!disabled && hasChanged(value, initialValue)}
150
+ class:changed={!disabled && changed}
136
151
  style="--index: {index};"
137
152
  >
138
153
  <FieldSection
@@ -166,6 +181,15 @@
166
181
  <Component {value} {...fieldProps} {onchange} onclick={onTrigger} />
167
182
  {@render children?.()}
168
183
  </FieldSection>
184
+ {#if changed}
185
+ <button
186
+ class="field__changed"
187
+ onclick={restoreInitialValue}
188
+ title="Restore initial value"
189
+ >
190
+ <span class="visually-hidden">Restore initial value</span>
191
+ </button>
192
+ {/if}
169
193
  {#if triggerable}
170
194
  <FieldSection {key} visible={showTriggers} secondary>
171
195
  <FieldTriggers
@@ -192,28 +216,45 @@
192
216
  border-bottom: 1px solid var(--fragment-spacing-color);
193
217
  }
194
218
 
195
- .field.changed:before {
196
- content: '';
197
-
219
+ .field__changed {
198
220
  position: absolute;
199
221
  top: 0px;
200
222
  left: 0px;
201
223
  bottom: 0px;
202
224
  z-index: 1;
203
225
 
204
- width: 4px;
226
+ width: 13px;
205
227
  /* height: 4px; */
206
228
  /* border-radius: 2px; */
207
229
 
208
- --stripes-offset: calc(var(--index) * 1.9px);
230
+ background: transparent;
231
+ cursor: pointer;
232
+
233
+ &:before {
234
+ content: '';
235
+
236
+ position: absolute;
237
+ top: 0;
238
+ left: 0;
239
+
240
+ display: block;
241
+ width: 4px;
242
+ height: 100%;
243
+
244
+ --stripes-offset: calc(var(--index) * 1.9px);
245
+
246
+ background: repeating-linear-gradient(
247
+ 45deg,
248
+ var(--fragment-accent-color) calc(0px + var(--stripes-offset)),
249
+ var(--fragment-accent-color) calc(2px + var(--stripes-offset)),
250
+ transparent calc(2px + var(--stripes-offset)),
251
+ transparent calc(4px + var(--stripes-offset))
252
+ );
253
+ }
209
254
 
210
- background: repeating-linear-gradient(
211
- 45deg,
212
- var(--fragment-accent-color) calc(0px + var(--stripes-offset)),
213
- var(--fragment-accent-color) calc(2px + var(--stripes-offset)),
214
- transparent calc(2px + var(--stripes-offset)),
215
- transparent calc(4px + var(--stripes-offset))
216
- );
255
+ &:hover:before {
256
+ width: 7px;
257
+ }
217
258
  }
218
259
 
219
260
  :global(.field__input .field) {
@@ -229,6 +270,7 @@
229
270
  .field__actions {
230
271
  display: flex;
231
272
  align-items: center;
273
+ gap: var(--column-gap);
232
274
  }
233
275
 
234
276
  .field__action {
@@ -101,7 +101,7 @@
101
101
  }}
102
102
  />
103
103
  {/if}
104
- <!-- {#if rendering.resizing === SIZES.PRESET}
104
+ {#if rendering.resizing === SIZES.PRESET}
105
105
  <Field key="preset">
106
106
  <FieldInputRow --grid-template-columns="1fr 1fr">
107
107
  <Select
@@ -123,18 +123,20 @@
123
123
  />
124
124
  </FieldInputRow>
125
125
  </Field>
126
- {/if} -->
127
-
126
+ {/if}
128
127
  {#if rendering.resizing !== SIZES.PRESET}
129
128
  <Field
130
129
  key="pixelRatio"
131
130
  value={Number(rendering.pixelRatio)}
132
- onchange={(pixelRatio) => (rendering.pixelRatio = pixelRatio)}
131
+ onchange={(pixelRatio) => {
132
+ rendering.pixelRatio = pixelRatio;
133
+ }}
133
134
  params={{
134
135
  step: 0.1,
135
136
  }}
136
137
  />
137
138
  {/if}
139
+
138
140
  <!-- {#if $sketchesCount > 1 && $monitors.length > 1}
139
141
  <ParamsMultisampling />
140
142
  {/if} -->
@@ -69,6 +69,11 @@
69
69
  } else if (render?.recording && !exports.recording) {
70
70
  render.stopRecording();
71
71
  }
72
+
73
+ if (exports.capturing) {
74
+ render.screenshot();
75
+ exports.capturing = false;
76
+ }
72
77
  });
73
78
 
74
79
  function checkForRefresh(event) {
@@ -0,0 +1,93 @@
1
+ <script>
2
+ let {
3
+ label,
4
+ value = $bindable(),
5
+ height,
6
+ disabled = false,
7
+ oninput,
8
+ onchange,
9
+ onkeydown,
10
+ onfocus,
11
+ onblur,
12
+ } = $props();
13
+
14
+ $inspect(height);
15
+
16
+ /** @type {HTMLInputElement} */
17
+ let node;
18
+
19
+ function onKeyPress(event) {
20
+ if (event.key === 'Enter' && !event.shiftKey) {
21
+ node.blur();
22
+ }
23
+ }
24
+ </script>
25
+
26
+ <div
27
+ class="input-container"
28
+ class:disabled
29
+ style={height ? `--height: ${height}` : null}
30
+ >
31
+ <textarea
32
+ class="input"
33
+ bind:this={node}
34
+ bind:value
35
+ {oninput}
36
+ {onchange}
37
+ {onkeydown}
38
+ {onfocus}
39
+ {onblur}
40
+ onkeypress={onKeyPress}
41
+ disabled={disabled ? 'disabled' : null}
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>
@@ -6,6 +6,7 @@ export const fieldTypes = {
6
6
  VEC: 'vec',
7
7
  CHECKBOX: 'checkbox',
8
8
  TEXT: 'text',
9
+ TEXTAREA: 'textarea',
9
10
  LIST: 'list',
10
11
  COLOR: 'color',
11
12
  BUTTON: 'button',
@@ -114,7 +115,7 @@ export function inferFieldType({ type, value, params, key }) {
114
115
  * @param {string} folder
115
116
  */
116
117
  export function parseFolder(folder) {
117
- const regex = /(?<name>\w+)(?:\[(?<attributes>[^\]]+)\])?/g;
118
+ const regex = /(?<name>[\w ]+)(?:\[(?<attributes>[^\]]+)\])?/g;
118
119
  const matches = [...folder.matchAll(regex)];
119
120
 
120
121
  const results = matches.map((match) => {
@@ -124,7 +125,17 @@ export function parseFolder(folder) {
124
125
  ? Object.fromEntries(
125
126
  match.groups.attributes
126
127
  .split(', ')
127
- .map((attr) => attr.split('=')),
128
+ .map((attr) =>
129
+ attr
130
+ .split('=')
131
+ .map((v) =>
132
+ v === 'false'
133
+ ? false
134
+ : v === 'true'
135
+ ? true
136
+ : v,
137
+ ),
138
+ ),
128
139
  )
129
140
  : {},
130
141
  };
package/src/index.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @typedef {import('./types/config').Config} Config
3
+ */
4
+
5
+ /**
6
+ * Type helper to make it easier to use fragment.config.js
7
+ * @param {Config} config
8
+ * @returns {Config}
9
+ */
10
+ export function defineConfig(config = {}) {
11
+ return config;
12
+ }
@@ -0,0 +1,6 @@
1
+ import { UserConfig } from 'vite';
2
+
3
+ export interface Config {
4
+ vite?: UserConfig;
5
+ [key: string]: any;
6
+ }
@@ -1,5 +1,6 @@
1
1
  export type * from './renderers';
2
2
  export type * from './sketch';
3
+ export type * from './config';
3
4
  export type * from './props';
4
5
  export type * from './helpers';
5
6
  export type * from './hooks';
@@ -2,8 +2,9 @@ type BaseProp<Value, Params, Type> = {
2
2
  value: Value;
3
3
  params?: Params;
4
4
  type?: Type;
5
+ disabled?: boolean;
5
6
  hidden?: boolean;
6
- displayName?: string;
7
+ displayName?: string | null;
7
8
  folder?: string;
8
9
  group?: string;
9
10
  onChange?: PropOnChange<Value, Params>;
@@ -25,11 +26,37 @@ type NumberProp = BaseProp<
25
26
  { disabled?: boolean; step?: number } | { min: number; max: number },
26
27
  'number'
27
28
  >;
28
- type VecProp = BaseProp<
29
+
30
+ type VecArray =
29
31
  | [number, number]
30
32
  | [number, number, number]
31
- | [number, number, number, number],
32
- { locked?: boolean },
33
+ | [number, number, number, number];
34
+
35
+ type VecObject = Record<string, number> & { [key: number]: never };
36
+
37
+ type VecValue = VecArray | VecObject;
38
+
39
+ type VecArrayParams<V extends VecArray> = {
40
+ min: { [K in keyof V]: number };
41
+ max: { [K in keyof V]: number };
42
+ step?: { [K in keyof V]: number };
43
+ };
44
+
45
+ type VecObjectParams<V extends VecObject> = {
46
+ min: { [K in keyof V]: number };
47
+ max: { [K in keyof V]: number };
48
+ step?: { [K in keyof V]: number };
49
+ };
50
+
51
+ type VecParams<V extends VecValue> = V extends readonly number[]
52
+ ? VecArrayParams<V>
53
+ : V extends Record<string, number>
54
+ ? VecObjectParams<V>
55
+ : never;
56
+
57
+ type VecProp<V extends VecValue = VecValue> = BaseProp<
58
+ V,
59
+ { locked?: boolean } | VecParams<V>,
33
60
  'vec'
34
61
  >;
35
62
  type CheckboxProp = BaseProp<boolean, never, 'checkbox'>;
@@ -50,7 +77,8 @@ type ImageProp = BaseProp<string, never, 'image'>;
50
77
  type Prop =
51
78
  | SelectProp
52
79
  | NumberProp
53
- | VecProp
80
+ | VecProp<VecObject>
81
+ | VecProp<VecArray>
54
82
  | CheckboxProp
55
83
  | TextProp
56
84
  | ListProp