fragment-tools 0.1.0

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 (192) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +101 -0
  3. package/bin/index.js +19 -0
  4. package/docs/README.md +18 -0
  5. package/docs/api/CLI.md +44 -0
  6. package/docs/api/renderers.md +80 -0
  7. package/docs/api/sketch.md +216 -0
  8. package/docs/api/triggers.md +101 -0
  9. package/docs/guide/about.md +16 -0
  10. package/docs/guide/exports.md +86 -0
  11. package/docs/guide/external-dependencies.md +22 -0
  12. package/docs/guide/getting-started.md +113 -0
  13. package/docs/guide/hot-shader-reloading.md +20 -0
  14. package/docs/guide/shortcuts.md +12 -0
  15. package/docs/guide/triggers.png +0 -0
  16. package/docs/guide/using-triggers.md +39 -0
  17. package/examples/cube-three.js +34 -0
  18. package/examples/ellipse-p5.js +26 -0
  19. package/examples/icon.fs +96 -0
  20. package/examples/icon.js +63 -0
  21. package/examples/icon.png +0 -0
  22. package/examples/icon.transparent.png +0 -0
  23. package/examples/package-lock.json +40 -0
  24. package/examples/package.json +15 -0
  25. package/examples/shape-2d.js +45 -0
  26. package/examples/shape-three.js +49 -0
  27. package/examples/shape-tree.fs +3 -0
  28. package/package.json +37 -0
  29. package/screenshot.png +0 -0
  30. package/src/cli/db.js +17 -0
  31. package/src/cli/index.js +198 -0
  32. package/src/cli/log.js +26 -0
  33. package/src/cli/plugins/check-dependencies.js +77 -0
  34. package/src/cli/plugins/db.js +12 -0
  35. package/src/cli/plugins/hot-shader-reload.js +86 -0
  36. package/src/cli/plugins/hot-sketch-reload.js +39 -0
  37. package/src/cli/plugins/screenshot.js +31 -0
  38. package/src/cli/server.js +140 -0
  39. package/src/cli/templates/2d.js +15 -0
  40. package/src/cli/templates/fragment.fs +10 -0
  41. package/src/cli/templates/fragment.js +18 -0
  42. package/src/cli/templates/index.js +24 -0
  43. package/src/cli/templates/p5.js +13 -0
  44. package/src/cli/templates/three-fragment.js +53 -0
  45. package/src/cli/templates/three-orthographic.js +23 -0
  46. package/src/cli/templates/three-perspective.js +20 -0
  47. package/src/cli/ws.js +92 -0
  48. package/src/client/app/App.svelte +8 -0
  49. package/src/client/app/client.js +68 -0
  50. package/src/client/app/components/IconCross.svelte +29 -0
  51. package/src/client/app/components/Init.svelte +13 -0
  52. package/src/client/app/components/KeyBinding.svelte +32 -0
  53. package/src/client/app/inputs/Input.js +15 -0
  54. package/src/client/app/inputs/Keyboard.js +21 -0
  55. package/src/client/app/inputs/MIDI.js +144 -0
  56. package/src/client/app/inputs/Mouse.js +5 -0
  57. package/src/client/app/inputs/Webcam.js +98 -0
  58. package/src/client/app/lib/canvas-recorder/CanvasRecorder.js +88 -0
  59. package/src/client/app/lib/canvas-recorder/FFMPEGRecorder.js +56 -0
  60. package/src/client/app/lib/canvas-recorder/FrameRecorder.js +40 -0
  61. package/src/client/app/lib/canvas-recorder/GIFRecorder.js +52 -0
  62. package/src/client/app/lib/canvas-recorder/MP4Recorder.js +46 -0
  63. package/src/client/app/lib/canvas-recorder/WebMRecorder.js +30 -0
  64. package/src/client/app/lib/canvas-recorder/mp4.js +20 -0
  65. package/src/client/app/lib/canvas-recorder/mp4.wasm +0 -0
  66. package/src/client/app/lib/canvas-recorder/utils.js +22 -0
  67. package/src/client/app/lib/gl/Geometry.js +39 -0
  68. package/src/client/app/lib/gl/Program.js +130 -0
  69. package/src/client/app/lib/gl/Renderer.js +148 -0
  70. package/src/client/app/lib/gl/Texture.js +114 -0
  71. package/src/client/app/lib/gl/index.js +109 -0
  72. package/src/client/app/lib/gl/utils.js +5 -0
  73. package/src/client/app/lib/helpers/frameDebounce.js +40 -0
  74. package/src/client/app/lib/loader/index.js +20 -0
  75. package/src/client/app/lib/loader/loadImage.js +19 -0
  76. package/src/client/app/lib/loader/loadScript.js +14 -0
  77. package/src/client/app/lib/paper-sizes.js +104 -0
  78. package/src/client/app/lib/presets.js +12 -0
  79. package/src/client/app/lib/tempo/Analyser.js +165 -0
  80. package/src/client/app/lib/tempo/Range.js +97 -0
  81. package/src/client/app/lib/tempo/index.js +138 -0
  82. package/src/client/app/modules/AudioAnalyser/Range.svelte +93 -0
  83. package/src/client/app/modules/AudioAnalyser/Spectrum.svelte +31 -0
  84. package/src/client/app/modules/AudioAnalyser.svelte +70 -0
  85. package/src/client/app/modules/Console/ConsoleLine.svelte +254 -0
  86. package/src/client/app/modules/Console.svelte +82 -0
  87. package/src/client/app/modules/Exports.svelte +105 -0
  88. package/src/client/app/modules/MidiPanel.svelte +106 -0
  89. package/src/client/app/modules/Monitor.svelte +62 -0
  90. package/src/client/app/modules/Params.svelte +112 -0
  91. package/src/client/app/renderers/2DRenderer.js +5 -0
  92. package/src/client/app/renderers/FragmentRenderer.js +62 -0
  93. package/src/client/app/renderers/OGLRenderer.js +0 -0
  94. package/src/client/app/renderers/P5Renderer.js +39 -0
  95. package/src/client/app/renderers/THREERenderer.js +128 -0
  96. package/src/client/app/stores/audioAnalysis.js +10 -0
  97. package/src/client/app/stores/console.js +76 -0
  98. package/src/client/app/stores/errors.js +25 -0
  99. package/src/client/app/stores/exports.js +28 -0
  100. package/src/client/app/stores/index.js +2 -0
  101. package/src/client/app/stores/layout.js +187 -0
  102. package/src/client/app/stores/multisampling.js +16 -0
  103. package/src/client/app/stores/props.js +44 -0
  104. package/src/client/app/stores/renderers.js +60 -0
  105. package/src/client/app/stores/rendering.js +111 -0
  106. package/src/client/app/stores/sketches.js +40 -0
  107. package/src/client/app/stores/time.js +27 -0
  108. package/src/client/app/stores/utils.js +66 -0
  109. package/src/client/app/transitions/fade.js +17 -0
  110. package/src/client/app/transitions/index.js +12 -0
  111. package/src/client/app/transitions/splitX.js +16 -0
  112. package/src/client/app/transitions/splitY.js +16 -0
  113. package/src/client/app/triggers/Keyboard.js +95 -0
  114. package/src/client/app/triggers/MIDI.js +122 -0
  115. package/src/client/app/triggers/Mouse.js +96 -0
  116. package/src/client/app/triggers/Trigger.js +71 -0
  117. package/src/client/app/triggers/index.js +19 -0
  118. package/src/client/app/triggers/shared.js +37 -0
  119. package/src/client/app/ui/Build.svelte +96 -0
  120. package/src/client/app/ui/ErrorOverlay.svelte +130 -0
  121. package/src/client/app/ui/Field.svelte +262 -0
  122. package/src/client/app/ui/FieldGroup.svelte +103 -0
  123. package/src/client/app/ui/FieldSection.svelte +123 -0
  124. package/src/client/app/ui/FieldSpace.svelte +37 -0
  125. package/src/client/app/ui/FieldTrigger.svelte +263 -0
  126. package/src/client/app/ui/FieldTriggers.svelte +58 -0
  127. package/src/client/app/ui/FloatingParams.svelte +49 -0
  128. package/src/client/app/ui/Layout.svelte +50 -0
  129. package/src/client/app/ui/LayoutColumn.svelte +9 -0
  130. package/src/client/app/ui/LayoutComponent.svelte +279 -0
  131. package/src/client/app/ui/LayoutResizer.svelte +218 -0
  132. package/src/client/app/ui/LayoutRoot.svelte +11 -0
  133. package/src/client/app/ui/LayoutRow.svelte +9 -0
  134. package/src/client/app/ui/LayoutToolbar.svelte +264 -0
  135. package/src/client/app/ui/Module.svelte +154 -0
  136. package/src/client/app/ui/ModuleHeaderAction.svelte +87 -0
  137. package/src/client/app/ui/ModuleHeaderButton.svelte +21 -0
  138. package/src/client/app/ui/ModuleHeaderSelect.svelte +50 -0
  139. package/src/client/app/ui/ModuleRenderer.svelte +38 -0
  140. package/src/client/app/ui/OutputRenderer.svelte +149 -0
  141. package/src/client/app/ui/ParamsMultisampling.svelte +109 -0
  142. package/src/client/app/ui/ParamsOutput.svelte +139 -0
  143. package/src/client/app/ui/Preview.svelte +15 -0
  144. package/src/client/app/ui/SelectChevrons.svelte +25 -0
  145. package/src/client/app/ui/SketchRenderer.svelte +672 -0
  146. package/src/client/app/ui/SketchSelect.svelte +49 -0
  147. package/src/client/app/ui/fields/ButtonInput.svelte +54 -0
  148. package/src/client/app/ui/fields/CheckboxInput.svelte +70 -0
  149. package/src/client/app/ui/fields/ColorInput.svelte +187 -0
  150. package/src/client/app/ui/fields/FieldInputRow.svelte +13 -0
  151. package/src/client/app/ui/fields/ImageInput.svelte +145 -0
  152. package/src/client/app/ui/fields/Input.svelte +120 -0
  153. package/src/client/app/ui/fields/ListInput.svelte +106 -0
  154. package/src/client/app/ui/fields/NumberInput.svelte +114 -0
  155. package/src/client/app/ui/fields/ProgressInput.svelte +90 -0
  156. package/src/client/app/ui/fields/Select.svelte +116 -0
  157. package/src/client/app/ui/fields/TextInput.svelte +18 -0
  158. package/src/client/app/ui/fields/Vec2Input.svelte +5 -0
  159. package/src/client/app/ui/fields/Vec3Input.svelte +6 -0
  160. package/src/client/app/ui/fields/VectorInput.svelte +102 -0
  161. package/src/client/app/utils/canvas.utils.js +229 -0
  162. package/src/client/app/utils/color.utils.js +427 -0
  163. package/src/client/app/utils/file.utils.js +77 -0
  164. package/src/client/app/utils/glsl.utils.js +14 -0
  165. package/src/client/app/utils/glslErrors.js +154 -0
  166. package/src/client/app/utils/index.js +39 -0
  167. package/src/client/app/utils/math.utils.js +23 -0
  168. package/src/client/app/utils/props.utils.js +53 -0
  169. package/src/client/index.html +18 -0
  170. package/src/client/main.js +9 -0
  171. package/src/client/public/css/global.css +115 -0
  172. package/src/client/public/favicon.ico +0 -0
  173. package/src/client/public/fonts/Inter-Bold.woff2 +0 -0
  174. package/src/client/public/fonts/Inter-Italic.woff2 +0 -0
  175. package/src/client/public/fonts/Inter-Regular.woff2 +0 -0
  176. package/src/client/public/fonts/Inter-SemiBold.woff2 +0 -0
  177. package/src/client/public/fonts/JetBrainsMono-Regular.woff2 +0 -0
  178. package/src/client/public/icons/chevron-bottom.svg +3 -0
  179. package/src/client/public/icons/chevron-right.svg +3 -0
  180. package/src/client/public/icons/chevron-top.svg +3 -0
  181. package/src/client/public/icons/columns-horizontal.svg +4 -0
  182. package/src/client/public/icons/columns-vertical.svg +4 -0
  183. package/src/client/public/icons/folder-plus.svg +6 -0
  184. package/src/client/public/icons/lock.svg +4 -0
  185. package/src/client/public/icons/picture-in-picture.svg +4 -0
  186. package/src/client/public/icons/trash.svg +5 -0
  187. package/src/client/public/icons/trigger.svg +8 -0
  188. package/src/client/public/icons/unlock.svg +4 -0
  189. package/src/client/public/js/ffmpeg.min.js +2 -0
  190. package/src/client/public/js/ffmpeg.min.js.map +1 -0
  191. package/src/client/public/js/gif.js +2 -0
  192. package/src/client/public/js/gif.worker.js +2 -0
@@ -0,0 +1,114 @@
1
+ <script>
2
+ import { createEventDispatcher } from "svelte";
3
+ import FieldInputRow from "./FieldInputRow.svelte";
4
+ import Input from "./Input.svelte";
5
+ import ProgressInput from "./ProgressInput.svelte";
6
+ import Keyboard from "../../inputs/Keyboard.js";
7
+ import { clamp } from "../../utils/math.utils.js";
8
+
9
+ function round(value, step) {
10
+ return Math.round(value * (1 / step)) / (1 / step);
11
+ }
12
+
13
+ export let value = null;
14
+ export let label = "";
15
+ export let step = 1;
16
+ export let suffix = "";
17
+ export let min = -Infinity;
18
+ export let max = Infinity;
19
+ export let disabled = false;
20
+
21
+ $: isFocused = false;
22
+ const dispatch = createEventDispatcher();
23
+
24
+ function sanitize(v, suffix) {
25
+ return (suffix && suffix !== "") ? Number(v.split(suffix)[0]) : Number(v);
26
+ }
27
+
28
+ function composeValue(v, isFocused, suffix = "") {
29
+ const clampedValue = clamp(v, isFinite(min) ? min : -Infinity, isFinite(max) ? max : Infinity);
30
+ const roundedValue = typeof step === "number" ? round(clampedValue, step) : v;
31
+
32
+ return isFocused ? `${roundedValue}` : `${roundedValue}${suffix}`;
33
+ }
34
+
35
+ $: currentValue = value;
36
+ $: composedValue = composeValue(currentValue, isFocused, suffix);
37
+
38
+ function onFocus() {
39
+ isFocused = true;
40
+ }
41
+
42
+ function onBlur(event) {
43
+ isFocused = false;
44
+
45
+ let newValue = event.currentTarget.value;
46
+ let isNotValid = isNaN(Number(event.currentTarget.value));
47
+
48
+ if (isNotValid) {
49
+ newValue = `${value}`;
50
+ }
51
+
52
+ currentValue = sanitize(newValue, suffix);
53
+
54
+ dispatch('change', currentValue);
55
+ }
56
+
57
+ function onKeyDown(event) {
58
+ if ([38, 40].includes(event.keyCode)) {
59
+ event.preventDefault();
60
+
61
+ const diff = Keyboard.getStepFromEvent(event) * step;
62
+ const direction = event.keyCode === 38 ? 1 : -1;
63
+ const newValue = sanitize(composedValue, suffix) + direction * (diff);
64
+
65
+ currentValue = newValue;
66
+ dispatch('change', currentValue);
67
+ }
68
+ }
69
+
70
+ $: hasProgress = isFinite(min) && isFinite(max);
71
+
72
+ function handleChangeProgress(event) {
73
+ currentValue = event.detail;
74
+ dispatch('change', event.detail);
75
+ }
76
+
77
+ </script>
78
+
79
+ <div class="number-input {hasProgress ? "number-input--with-progress": ""}">
80
+ {#if hasProgress}
81
+ <FieldInputRow --grid-template-columns="1fr 0.5fr">
82
+ <ProgressInput
83
+ step={step}
84
+ value={currentValue}
85
+ min={min}
86
+ max={max}
87
+ on:change={handleChangeProgress}
88
+ />
89
+ <Input
90
+ {label}
91
+ {disabled}
92
+ on:keydown={onKeyDown}
93
+ on:focus={onFocus}
94
+ on:blur={onBlur}
95
+ bind:value={composedValue}
96
+ />
97
+ </FieldInputRow>
98
+ {:else}
99
+ <Input
100
+ {label}
101
+ on:keydown={onKeyDown}
102
+ on:focus={onFocus}
103
+ on:blur={onBlur}
104
+ bind:value={composedValue}
105
+ />
106
+ {/if}
107
+ </div>
108
+
109
+ <style>
110
+ .number-input {
111
+ position: relative;
112
+ width: 100%;
113
+ }
114
+ </style>
@@ -0,0 +1,90 @@
1
+ <script>
2
+ import { createEventDispatcher } from "svelte";
3
+ import { map, clamp } from "../../utils/math.utils.js";
4
+
5
+ export let value;
6
+ export let min;
7
+ export let max;
8
+ export let step;
9
+
10
+ let node;
11
+ let rect;
12
+
13
+ const dispatch = createEventDispatcher();
14
+
15
+ let isDragging = false;
16
+
17
+ // handlers
18
+ function handleMouseDown(event) {
19
+ document.addEventListener('mousemove', handleMouseMove);
20
+ document.addEventListener('mouseup', handleMouseUp);
21
+
22
+ rect = node.getBoundingClientRect();
23
+
24
+ isDragging = true;
25
+
26
+ onDrag(event);
27
+ }
28
+
29
+ function handleMouseMove(event) {
30
+ onDrag(event);
31
+ }
32
+
33
+ function onDrag(event) {
34
+ let v = clamp(map(event.clientX, rect.left, rect.right, min, max), min, max);
35
+ v = Math.floor(v * (1 / step)) / (1 / step);
36
+
37
+ dispatch("change", v);
38
+ }
39
+
40
+ function handleMouseUp() {
41
+ document.removeEventListener('mousemove', handleMouseMove);
42
+ document.removeEventListener('mouseup', handleMouseUp);
43
+
44
+ isDragging = false;
45
+ }
46
+
47
+ $: scaleX = clamp(map(value, min, max, 0, 1), 0, 1);
48
+
49
+ </script>
50
+
51
+ <div class="progress {isDragging ? "dragging": ""} " bind:this={node} on:mousedown={handleMouseDown}>
52
+ <div class="fill" style="opacity: {scaleX > 0 ? 1 : 0}; transform: scaleX({scaleX})"></div>
53
+ </div>
54
+
55
+ <style>
56
+ .progress {
57
+ position: relative;
58
+
59
+ height: var(--height-input);
60
+ border-radius: var(--border-radius-input);
61
+ box-shadow: inset 0 0 0 1px var(--color-border-input);
62
+
63
+ background: var(--color-background-input);
64
+ cursor: ew-resize;
65
+ }
66
+
67
+ .progress:hover {
68
+ box-shadow: inset 0 0 0 1px var(--color-active);
69
+ }
70
+
71
+ .progress.dragging {
72
+ box-shadow: 0 0 0 2px var(--color-active);
73
+ }
74
+
75
+
76
+ .fill {
77
+ position: absolute;
78
+ left: 3px;
79
+ top: 3px;
80
+ bottom: 3px;
81
+ right: 3px;
82
+
83
+ background: grey;
84
+ transform-origin: 0 50%;
85
+ border-radius: calc(var(--border-radius-input) * 0.5);
86
+
87
+ background-color: var(--color-active);
88
+ }
89
+
90
+ </style>
@@ -0,0 +1,116 @@
1
+ <script>
2
+ import { createEventDispatcher } from "svelte";
3
+ import SelectChevrons from "../SelectChevrons.svelte";
4
+
5
+ export let options = [];
6
+ export let name = "";
7
+ export let value;
8
+ export let disabled = false;
9
+ export let title = "";
10
+
11
+ let node;
12
+ let sanitizedOptions = [];
13
+
14
+ const dispatch = createEventDispatcher();
15
+
16
+ $: {
17
+ sanitizedOptions = [];
18
+
19
+ for (let i = 0; i < options.length; i++) {
20
+ const { value, label, disabled } = options[i];
21
+
22
+ if (["number", "string"].includes(typeof options[i])) {
23
+ sanitizedOptions[i] = {
24
+ value: options[i],
25
+ label: options[i],
26
+ disabled,
27
+ };
28
+ } else {
29
+ sanitizedOptions[i] = {
30
+ value: value,
31
+ label: label ? label : value,
32
+ disabled,
33
+ }
34
+ }
35
+ }
36
+ }
37
+
38
+ function handleChange(event) {
39
+ dispatch("change", event.currentTarget.value);
40
+ }
41
+
42
+ </script>
43
+
44
+ <div
45
+ class="select-input"
46
+ class:disabled={disabled}
47
+ class:single={sanitizedOptions.length === 1}
48
+ >
49
+ <div class="container">
50
+ <select class="select" bind:this={node} on:change={handleChange} {name} {disabled} {title} bind:value={value}>
51
+ {#each sanitizedOptions as option}
52
+ <option value={option.value} selected={value === option.value} disabled={option.disabled}>{option.label}</option>
53
+ {/each}
54
+ </select>
55
+ {#if sanitizedOptions.length > 1 }
56
+ <SelectChevrons />
57
+ {/if}
58
+ </div>
59
+ </div>
60
+
61
+ <style>
62
+ .select-input {
63
+ width: 100%;
64
+ }
65
+
66
+ .container {
67
+ position: relative;
68
+
69
+ display: flex;
70
+ height: var(--height-input);
71
+ margin: 2px 0;
72
+
73
+ color: rgba(255, 255, 255, 0.5);
74
+
75
+ box-shadow: inset 0 0 0 1px var(--color-border-input);
76
+ border-radius: var(--border-radius-input);
77
+ background-color: var(--color-background-input);
78
+ }
79
+
80
+ .select-input:not(.disabled) .container:hover {
81
+ box-shadow: inset 0 0 0 1px var(--color-active);
82
+ }
83
+
84
+ .container:focus-within {
85
+ box-shadow: 0 0 0 2px var(--color-active);
86
+ }
87
+
88
+ .select {
89
+ padding: 0 var(--padding, 6px) 0 var(--padding, 6px);
90
+
91
+ width: 100%;
92
+
93
+ color: inherit;
94
+ font-size: var(--font-size-input);
95
+
96
+ outline: 0;
97
+ background-color: transparent;
98
+ }
99
+
100
+ .select-input:not(.disabled) .select {
101
+ cursor: pointer;
102
+ }
103
+
104
+ :global(.field__section:hover .select-input:not(.disabled) .container) {
105
+ color: var(--color-text);
106
+ }
107
+
108
+ .select-input:not(.disabled) .select:focus {
109
+ color: var(--color-text);
110
+ }
111
+
112
+ .select-input:not(.disabled) .select:hover {
113
+ color: var(--color-text);
114
+ }
115
+
116
+ </style>
@@ -0,0 +1,18 @@
1
+ <script>
2
+ import Input from "./Input.svelte";
3
+
4
+ export let value;
5
+ export let label = "";
6
+ export let disabled = false;
7
+
8
+ </script>
9
+
10
+ <div class="text-input">
11
+ <Input {value} {label} {disabled} on:change on:input />
12
+ </div>
13
+
14
+ <style>
15
+ .text-input {
16
+ width: 100%;
17
+ }
18
+ </style>
@@ -0,0 +1,5 @@
1
+ <script>
2
+ import VectorInput from "./VectorInput.svelte";
3
+ </script>
4
+
5
+ <VectorInput {...$$props} on:change/>
@@ -0,0 +1,6 @@
1
+ <script>
2
+ import VectorInput from "./VectorInput.svelte";
3
+
4
+ </script>
5
+
6
+ <VectorInput {...$$props} />
@@ -0,0 +1,102 @@
1
+ <script>
2
+ import { createEventDispatcher } from "svelte";
3
+ import FieldInputRow from "./FieldInputRow.svelte";
4
+ import NumberInput from "./NumberInput.svelte";
5
+
6
+ export let value;
7
+ export let suffix = "";
8
+ export let min = -Infinity;
9
+ export let max = Infinity;
10
+ export let step = 0.1;
11
+ export let locked = false;
12
+ export let disabled = false;
13
+
14
+ function sanitize(value, type) {
15
+ if (Array.isArray(value)) {
16
+ return value.reduce((all, v, index) => {
17
+ if (typeof v === "number") {
18
+ all[index] = {
19
+ value: v,
20
+ label: ""
21
+ }
22
+ } else {
23
+ all[index] = v;
24
+ }
25
+
26
+ return all;
27
+ }, []);
28
+ } else {
29
+ return Object.keys(value).map((key) => {
30
+ return { label: key, value: value[key] }
31
+ });
32
+ }
33
+ }
34
+
35
+ function desanitize(updated) {
36
+ if (Array.isArray(value)) {
37
+ return updated.map((v) => v.value);
38
+ }
39
+
40
+ return updated;
41
+ }
42
+
43
+ $: previousValue = sanitize(value);
44
+ $: currentValue = sanitize(value);
45
+
46
+ const dispatch = createEventDispatcher();
47
+
48
+ function onValueChange(index, newValue) {
49
+ const prevValue = previousValue[index].value;
50
+ const ratio = newValue / prevValue;
51
+
52
+ const updated = currentValue.map((v, i) => {
53
+ return {
54
+ ...v,
55
+ value: i === index ? newValue :
56
+ locked ? (Math.round(previousValue[i].value * ratio * (1/step)) / (1/step)) :
57
+ v.value
58
+ };
59
+ });
60
+
61
+ currentValue = updated;
62
+
63
+ dispatch("change", desanitize(updated));
64
+ }
65
+
66
+ </script>
67
+
68
+ <div class="vector-container vec{currentValue.length}" class:locked={locked}>
69
+ <FieldInputRow --grid-template-columns={currentValue.map(() => "1fr").join(" ")}>
70
+ {#each currentValue as curr, index}
71
+ <NumberInput
72
+ {min}
73
+ {max}
74
+ {step}
75
+ {suffix}
76
+ {disabled}
77
+ label={curr.label}
78
+ value={curr.value}
79
+ on:change={(event) => onValueChange(index, event.detail)}
80
+ />
81
+ {/each}
82
+ </FieldInputRow>
83
+ </div>
84
+
85
+ <style>
86
+ .vector-container {
87
+ width: 100%;
88
+ }
89
+
90
+ :global(.vector-container.locked .number-input:not(:last-child):after) {
91
+ content: '';
92
+
93
+ position: absolute;
94
+ top: 50%;
95
+ right: calc(var(--column-gap) * -1);
96
+ width: var(--column-gap);
97
+ height: 1px;
98
+
99
+ background-color: var(--color-border-input);
100
+ }
101
+
102
+ </style>
@@ -0,0 +1,229 @@
1
+ /*
2
+ https://github.com/mattdesl/canvas-sketch/blob/24f6bb2bbdfdfd72a698a0b8a0962ad843fb7688/lib/save.js
3
+ */
4
+
5
+ import { changeDpiDataUrl } from "changedpi";
6
+ import { get } from "svelte/store";
7
+ import { exports } from "../stores";
8
+ import { VIDEO_FORMATS } from "../stores/exports";
9
+ import { downloadBlob, createBlobFromDataURL } from "./file.utils";
10
+ import WebMRecorder from "../lib/canvas-recorder/WebMRecorder";
11
+ import MP4Recorder from "../lib/canvas-recorder/MP4Recorder";
12
+ import GIFRecorder from "../lib/canvas-recorder/GIFRecorder";
13
+ import FrameRecorder from "../lib/canvas-recorder/FrameRecorder";
14
+ import { exportCanvas } from "../lib/canvas-recorder/utils";
15
+ import { map } from "./math.utils";
16
+
17
+ export async function saveDataURL(dataURL, options, blob) {
18
+ async function onError(err) {
19
+ if (typeof options.onError === "function") {
20
+ options.onError(err);
21
+ }
22
+
23
+ if (!blob) {
24
+ blob = await createBlobFromDataURL(dataURL);
25
+ }
26
+
27
+ await downloadBlob(blob, options);
28
+ }
29
+
30
+ try {
31
+ const body = {
32
+ dataURL: dataURL.split(',')[1], // remove extension,
33
+ ...options,
34
+ };
35
+ const response = await fetch('/save', {
36
+ method: "POST",
37
+ body: JSON.stringify(body),
38
+ headers: {
39
+ 'Accept': 'application/json',
40
+ 'Content-Type': 'application/json'
41
+ },
42
+ });
43
+ const { filepath, error } = await response.json();
44
+
45
+ if (response.ok && filepath) {
46
+ console.log(`[fragment] Saved ${filepath}`);
47
+ } else {
48
+ onError(error);
49
+ }
50
+ } catch(error) {
51
+ onError(error);
52
+ }
53
+ };
54
+
55
+ export async function createDataURLFromBlob(blob) {
56
+ return new Promise((resolve, reject) => {
57
+ const reader = new FileReader();
58
+
59
+ reader.onerror = (err) => {
60
+ reject(err);
61
+ };
62
+
63
+ reader.onload = (e) => {
64
+ resolve(e.target.result);
65
+ };
66
+
67
+ reader.readAsDataURL(blob);
68
+ });
69
+ }
70
+
71
+ export async function saveBlob(blob, options) {
72
+ const dataURL = await createDataURLFromBlob(blob);
73
+
74
+ return saveDataURL(dataURL, options, blob);
75
+ };
76
+
77
+ function getFilenameParams() {
78
+ const now = new Date();
79
+
80
+ const year = now.toLocaleString('default', { year: 'numeric' });
81
+ const month = now.toLocaleString('default', { month: 'numeric' }).padStart(2, `0`);
82
+ const day = now.toLocaleString('default', { day: 'numeric' });
83
+ const hours = now.toLocaleString('default', { hour: 'numeric' }).split(' ')[0];
84
+ const minutes = now.toLocaleString('default', { minute: 'numeric' }).padStart(2, `0`);
85
+ const seconds = now.toLocaleString('default', { second: 'numeric' }).padStart(2, `0`);
86
+
87
+ const timestamp = `${year}.${month}.${day}-${hours}.${minutes}.${seconds}`;
88
+
89
+ return {
90
+ year,
91
+ month,
92
+ day,
93
+ hours,
94
+ minutes,
95
+ seconds,
96
+ timestamp,
97
+ };
98
+ }
99
+
100
+ export const defaultFilenamePattern = ({
101
+ filename,
102
+ timestamp,
103
+ }) => {
104
+ return `${filename}.${timestamp}`;
105
+ };
106
+
107
+ export async function screenshotCanvas(canvas, {
108
+ filename = "",
109
+ pattern = defaultFilenamePattern,
110
+ params = {},
111
+ }) {
112
+ const { imageEncoding, imageQuality, pixelsPerInch } = get(exports);
113
+ let { extension, dataURL } = exportCanvas(canvas, {
114
+ encoding: `image/${imageEncoding}`,
115
+ encodingQuality: map(imageQuality, 1, 100, 0, 1),
116
+ });
117
+
118
+ let patternParams = getFilenameParams();
119
+ let name = pattern({ filename, ...params, ...patternParams });
120
+
121
+ if (imageEncoding !== "webp" && pixelsPerInch !== 72) {
122
+ dataURL = changeDpiDataUrl(dataURL, pixelsPerInch);
123
+ }
124
+
125
+ await saveDataURL(dataURL, {
126
+ filename: `${name}${extension}`,
127
+ onError: () => {
128
+ console.error(`[fragment] Error while saving screenshot.`);
129
+ }
130
+ });
131
+ }
132
+
133
+ function recordCanvasWebM(canvas, options) {
134
+ let recorder = new WebMRecorder(canvas, options);
135
+ recorder.start();
136
+
137
+ return recorder;
138
+ }
139
+
140
+ function recordCanvasMp4(canvas, options) {
141
+ let recorder = new MP4Recorder(canvas, options);
142
+ recorder.start();
143
+
144
+ return recorder;
145
+ }
146
+
147
+ function recordCanvasGIF(canvas, options) {
148
+ let recorder = new GIFRecorder(canvas, options);
149
+ recorder.start();
150
+
151
+ return recorder;
152
+ }
153
+
154
+ function recordCanvasFrames(canvas, options) {
155
+ let recorder = new FrameRecorder(canvas, options);
156
+ recorder.start();
157
+
158
+ return recorder;
159
+ }
160
+
161
+ export function recordCanvas(canvas, {
162
+ filename = 'output',
163
+ format = 'mp4',
164
+ framerate = 25,
165
+ duration = Infinity,
166
+ quality = 100,
167
+ pattern = defaultFilenamePattern,
168
+ imageEncoding,
169
+ onStart = () => {},
170
+ onTick = () => {},
171
+ onComplete = () => {}
172
+ } = {}) {
173
+ let patternParams = getFilenameParams();
174
+ let name = pattern({ filename, ...patternParams });
175
+
176
+ function complete(result) {
177
+ if (Array.isArray(result)) {
178
+ const frmt = format === VIDEO_FORMATS.FRAMES ? imageEncoding : format;
179
+
180
+ for (let i = 0; i < result.length; i++) {
181
+ const index = `${i}`.padStart(`${result.length}`.length, '0');
182
+ saveBlob(result[i], {
183
+ filename: `${name}-${index}.${frmt}`,
184
+ onError: () => {
185
+ console.log(`[fragment] Error while saving record.`);
186
+ }
187
+ });
188
+ }
189
+ } else {
190
+ saveBlob(result, {
191
+ filename: `${name}.${format}`,
192
+ onError: () => {
193
+ console.log(`[fragment] Error while saving record.`);
194
+ }
195
+ });
196
+ }
197
+ onComplete();
198
+ }
199
+
200
+ const options = {
201
+ framerate,
202
+ duration,
203
+ quality,
204
+ onStart,
205
+ onTick,
206
+ onComplete: complete,
207
+ };
208
+
209
+ let recorder;
210
+
211
+ if (format === VIDEO_FORMATS.WEBM) {
212
+ recorder = recordCanvasWebM(canvas, options);
213
+ } else if (format === VIDEO_FORMATS.MP4) {
214
+ recorder = recordCanvasMp4(canvas, options);
215
+ } else if (format === VIDEO_FORMATS.GIF) {
216
+ recorder = recordCanvasGIF(canvas, options);
217
+ } else if (format === VIDEO_FORMATS.FRAMES) {
218
+ recorder = recordCanvasFrames(canvas, {
219
+ ...options,
220
+ imageEncoding,
221
+ });
222
+ }
223
+
224
+ if (!recorder) {
225
+ console.error(`Cannot find matching recorder`);
226
+ }
227
+
228
+ return recorder;
229
+ };