fragment-tools 0.1.13 → 0.1.14

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 (52) hide show
  1. package/bin/index.js +2 -2
  2. package/package.json +4 -5
  3. package/src/cli/log.js +31 -21
  4. package/src/cli/plugins/check-dependencies.js +47 -30
  5. package/src/cli/plugins/hot-shader-replacement.js +384 -0
  6. package/src/cli/plugins/hot-sketch-reload.js +3 -13
  7. package/src/cli/plugins/screenshot.js +57 -20
  8. package/src/cli/server.js +144 -133
  9. package/src/client/app/App.svelte +3 -3
  10. package/src/client/app/client.js +55 -39
  11. package/src/client/app/components/Init.svelte +12 -9
  12. package/src/client/app/helpers.js +42 -0
  13. package/src/client/app/hooks.js +20 -0
  14. package/src/client/app/inputs/Keyboard.js +13 -15
  15. package/src/client/app/inputs/MIDI.js +14 -15
  16. package/src/client/app/lib/canvas-recorder/CanvasRecorder.js +41 -21
  17. package/src/client/app/lib/gl/Renderer.js +127 -139
  18. package/src/client/app/modules/Exports.svelte +62 -43
  19. package/src/client/app/modules/MidiPanel.svelte +100 -101
  20. package/src/client/app/modules/Params.svelte +116 -103
  21. package/src/client/app/renderers/2DRenderer.js +3 -3
  22. package/src/client/app/renderers/FragmentRenderer.js +30 -23
  23. package/src/client/app/renderers/P5Renderer.js +10 -7
  24. package/src/client/app/renderers/THREERenderer.js +136 -94
  25. package/src/client/app/stores/exports.js +36 -20
  26. package/src/client/app/stores/props.js +28 -5
  27. package/src/client/app/stores/renderers.js +22 -15
  28. package/src/client/app/stores/sketches.js +7 -9
  29. package/src/client/app/stores/utils.js +95 -38
  30. package/src/client/app/triggers/Keyboard.js +88 -79
  31. package/src/client/app/triggers/MIDI.js +110 -84
  32. package/src/client/app/ui/Field.svelte +343 -240
  33. package/src/client/app/ui/FieldGroup.svelte +106 -94
  34. package/src/client/app/ui/FieldSection.svelte +125 -116
  35. package/src/client/app/ui/ParamsMultisampling.svelte +96 -95
  36. package/src/client/app/ui/ParamsOutput.svelte +113 -113
  37. package/src/client/app/ui/SelectChevrons.svelte +27 -15
  38. package/src/client/app/ui/SketchRenderer.svelte +761 -667
  39. package/src/client/app/ui/fields/ButtonInput.svelte +61 -48
  40. package/src/client/app/ui/fields/CheckboxInput.svelte +67 -61
  41. package/src/client/app/ui/fields/ColorInput.svelte +294 -238
  42. package/src/client/app/ui/fields/ImageInput.svelte +123 -121
  43. package/src/client/app/ui/fields/Input.svelte +100 -111
  44. package/src/client/app/ui/fields/ListInput.svelte +96 -96
  45. package/src/client/app/ui/fields/NumberInput.svelte +121 -116
  46. package/src/client/app/ui/fields/ProgressInput.svelte +80 -73
  47. package/src/client/app/ui/fields/Select.svelte +137 -124
  48. package/src/client/app/ui/fields/VectorInput.svelte +86 -82
  49. package/src/client/app/utils/canvas.utils.js +228 -201
  50. package/src/client/app/utils/file.utils.js +38 -34
  51. package/src/client/public/css/global.css +27 -21
  52. package/src/cli/plugins/hot-shader-reload.js +0 -86
@@ -1,12 +1,12 @@
1
- import { WebGLRenderer, Scene } from "three";
2
- import { Texture, fragment } from "@fragment/lib/gl";
3
- import { client } from "@fragment/client";
4
- import { getShaderPath } from "../utils/glsl.utils";
5
- import { clearError } from "../stores/errors";
1
+ import { WebGLRenderer, Scene } from 'three';
2
+ import { Texture, fragment } from '@fragment/lib/gl';
3
+ import { client } from '@fragment/client';
4
+ import { getShaderPath } from '../utils/glsl.utils';
5
+ import { clearError } from '../stores/errors';
6
6
 
7
7
  let renderer;
8
8
  let previews = [];
9
- let fragmentShader = /* glsl */`
9
+ let fragmentShader = /* glsl */ `
10
10
  precision highp float;
11
11
  uniform sampler2D uSampler;
12
12
  varying vec2 vUv;
@@ -18,111 +18,153 @@ let fragmentShader = /* glsl */`
18
18
  `;
19
19
 
20
20
  export let init = ({ canvas }) => {
21
- renderer = new WebGLRenderer({ antialias: true });
21
+ renderer = new WebGLRenderer({ antialias: true });
22
22
 
23
- return {
24
- renderer,
25
- };
23
+ const render = renderer.render;
24
+
25
+ renderer.render = (scene, camera) => {
26
+ handleHotShaderUpdate(scene);
27
+
28
+ render.call(renderer, scene, camera);
29
+ };
30
+
31
+ return {
32
+ renderer,
33
+ };
26
34
  };
27
35
 
28
36
  export let onMountPreview = ({ id, canvas, width, height, pixelRatio }) => {
29
- let { gl, render, resize, uniforms, destroy } = fragment({
30
- canvas,
31
- shader: fragmentShader,
32
- uniforms: {
33
- uSampler: { value: null, type: "sampler2D" },
34
- },
35
- });
36
-
37
- let texture = new Texture(gl, {
38
- image: renderer.domElement,
39
- });
40
-
41
- uniforms.uSampler.value = texture;
42
-
43
- let scene = new Scene();
44
-
45
- previews.push({
46
- id,
47
- scene,
48
- texture,
49
- render,
50
- resize,
51
- destroy,
52
- });
53
-
54
- return {
55
- scene,
56
- renderer,
57
- };
37
+ let { gl, render, resize, uniforms, destroy } = fragment({
38
+ canvas,
39
+ shader: fragmentShader,
40
+ uniforms: {
41
+ uSampler: { value: null, type: 'sampler2D' },
42
+ },
43
+ });
44
+
45
+ let texture = new Texture(gl, {
46
+ image: renderer.domElement,
47
+ });
48
+
49
+ uniforms.uSampler.value = texture;
50
+
51
+ let scene = new Scene();
52
+
53
+ previews.push({
54
+ id,
55
+ scene,
56
+ texture,
57
+ render,
58
+ resize,
59
+ destroy,
60
+ rendered: false,
61
+ });
62
+
63
+ return {
64
+ scene,
65
+ renderer,
66
+ };
58
67
  };
59
68
 
60
69
  export let onDestroyPreview = ({ id, canvas }) => {
61
- const previewIndex = previews.findIndex(p => p.id === id);
62
- const preview = previews[previewIndex];
70
+ const previewIndex = previews.findIndex((p) => p.id === id);
71
+ const preview = previews[previewIndex];
63
72
 
64
- clearError(renderer.getContext().__uuid);
73
+ clearError(renderer.getContext().__uuid);
65
74
 
66
- if (preview) {
67
- preview.texture.destroy();
68
- preview.destroy();
69
- previews.splice(previewIndex, 1);
70
- }
75
+ if (preview) {
76
+ preview.texture.destroy();
77
+ preview.destroy();
78
+ previews.splice(previewIndex, 1);
79
+ }
71
80
  };
72
81
 
73
- export let onAfterUpdatePreview = ({ id }) => {
74
- const preview = previews.find(p => p.id === id);
82
+ export let onBeforeUpdatePreview = ({ id }) => {
83
+ const preview = previews.find((p) => p.id === id);
75
84
 
76
- if (preview) {
77
- preview.texture.needsUpdate = true;
78
- preview.render();
79
- }
85
+ if (preview) {
86
+ preview.rendered = false;
87
+ }
88
+ };
89
+
90
+ export let onAfterUpdatePreview = ({ id }) => {
91
+ const preview = previews.find((p) => p.id === id);
92
+
93
+ if (preview) {
94
+ preview.texture.needsUpdate = true;
95
+ preview.render();
96
+ preview.rendered = true;
97
+ }
98
+
99
+ if (
100
+ previews.every((preview) => preview.rendered) &&
101
+ _shaderUpdates.length > 0
102
+ ) {
103
+ clearShaderUpdates();
104
+ }
80
105
  };
81
106
 
82
107
  export let resize = ({ width, height, pixelRatio }) => {
83
- renderer.setPixelRatio(pixelRatio);
84
- renderer.setSize(width, height);
108
+ renderer.setPixelRatio(pixelRatio);
109
+ renderer.setSize(width, height);
85
110
 
86
- for (let i = 0; i < previews.length; i++) {
87
- const preview = previews[i];
88
- preview.resize({ width, height, pixelRatio });
89
- }
111
+ for (let i = 0; i < previews.length; i++) {
112
+ const preview = previews[i];
113
+ preview.resize({ width, height, pixelRatio });
114
+ }
90
115
  };
91
116
 
92
117
  /* HOT SHADER RELOADING */
93
- client.on('shader-update', (data) => {
94
- clearError(renderer.getContext().__uuid);
95
- const { filepath, source } = data;
96
-
97
- const scenes = previews.map((preview) => preview.scene);
98
- const materials = [];
99
-
100
- scenes.forEach(scene => {
101
- scene.traverse((child) => {
102
- if (child.material) {
103
- const { material } = child;
104
-
105
- if (material.isShaderMaterial || material.isRawShaderMaterial) {
106
- materials.push(material);
107
- }
108
- }
109
- })
110
- });
111
-
112
- materials.forEach(material => {
113
- const { vertexShader, fragmentShader } = material;
114
-
115
- Object.keys({ vertexShader, fragmentShader }).forEach((key) => {
116
- const shader = material[key];
117
- const shaderPath = getShaderPath(shader);
118
-
119
- if (shaderPath === filepath) {
120
- console.log(`[fragment] shader update ${shaderPath.replace(__CWD__, "")}`);
121
- material[key] = source;
122
- material.needsUpdate = true;
123
- }
124
- });
125
- });
126
-
127
-
118
+ let _shaderUpdates = [];
119
+
120
+ function handleHotShaderUpdate(scene) {
121
+ if (_shaderUpdates.length > 0) {
122
+ scene.traverse((child) => {
123
+ if (child.material) {
124
+ const { material } = child;
125
+
126
+ if (material.isShaderMaterial || material.isRawShaderMaterial) {
127
+ const { vertexShader, fragmentShader } = material;
128
+
129
+ Object.keys({ vertexShader, fragmentShader }).forEach(
130
+ (key) => {
131
+ const shader = material[key];
132
+ const shaderPath = getShaderPath(shader);
133
+ const shaderUpdate = _shaderUpdates.find(
134
+ (shaderUpdate) =>
135
+ shaderUpdate.filepath === shaderPath,
136
+ );
137
+
138
+ if (shaderUpdate) {
139
+ console.log(
140
+ `[fragment-plugin-hsr] hsr update ${shaderPath.replace(
141
+ __CWD__,
142
+ '',
143
+ )}`,
144
+ );
145
+ material[key] = shaderUpdate.source;
146
+ material.needsUpdate = true;
147
+ }
148
+ },
149
+ );
150
+ }
151
+ }
152
+ });
153
+ }
154
+ }
155
+
156
+ function clearShaderUpdates() {
157
+ _shaderUpdates = [];
158
+ }
159
+
160
+ if (import.meta.hot) {
161
+ import.meta.hot.on('sketch-update', (data) => {
162
+ clearShaderUpdates();
163
+ });
164
+ }
165
+
166
+ client.on('shader-update', (shaderUpdates) => {
167
+ clearError(renderer.getContext().__uuid);
168
+
169
+ _shaderUpdates = shaderUpdates;
128
170
  });
@@ -1,28 +1,44 @@
1
- import { writable } from "svelte/store";
2
- import { createStore } from "./utils";
1
+ import { writable } from 'svelte/store';
2
+ import { createHookStore, createStore } from './utils';
3
3
 
4
- export const IMAGE_ENCODINGS = ["png", "jpeg", "webp"];
4
+ export const IMAGE_ENCODINGS = ['png', 'jpeg', 'webp'];
5
5
 
6
6
  export const VIDEO_FORMATS = {
7
- FRAMES: "frames",
8
- MP4: "mp4",
9
- GIF: "gif",
10
- WEBM: "webm",
7
+ FRAMES: 'frames',
8
+ MP4: 'mp4',
9
+ GIF: 'gif',
10
+ WEBM: 'webm',
11
11
  };
12
12
 
13
- export const exports = createStore(`exports`, {
14
- imageEncoding: IMAGE_ENCODINGS[0],
15
- videoFormat: Object.values(VIDEO_FORMATS)[0],
16
- pixelsPerInch: 72,
17
- framerate: 60,
18
- useDuration: true,
19
- loopCount: 1,
20
- imageQuality: 100,
21
- videoQuality: 100,
22
- }, {
23
- persist: !__BUILD__,
24
- reset: false,
25
- });
13
+ export const exports = createStore(
14
+ `exports`,
15
+ {
16
+ imageEncoding: IMAGE_ENCODINGS[0],
17
+ videoFormat: Object.values(VIDEO_FORMATS)[0],
18
+ pixelsPerInch: 72,
19
+ framerate: 60,
20
+ useDuration: true,
21
+ loopCount: 1,
22
+ imageQuality: 100,
23
+ videoQuality: 100,
24
+ imageCount: 1,
25
+ },
26
+ {
27
+ persist: !__BUILD__,
28
+ reset: false,
29
+ },
30
+ );
26
31
 
27
32
  export const recording = writable(false);
28
33
  export const capturing = writable(false);
34
+
35
+ export const [beforeCapture, onBeforeCapture, removeBeforeCaptureFrom] =
36
+ createHookStore('onBeforeCapture');
37
+ export const [afterCapture, onAfterCapture, removeAfterCaptureFrom] =
38
+ createHookStore('onAfterCapture');
39
+
40
+ export const [beforeRecord, onBeforeRecord, removeBeforeRecordFrom] =
41
+ createHookStore('onBeforeRecord');
42
+
43
+ export const [afterRecord, onAfterRecord, removeAfterRecordFrom] =
44
+ createHookStore('onAfterRecord');
@@ -1,14 +1,15 @@
1
- import { writable, get } from "svelte/store";
2
- import { sketches } from "./sketches";
1
+ import { sketches } from './sketches';
2
+ import { getStore } from './utils';
3
3
 
4
- export const props = writable({});
4
+ export const props = getStore('props', {});
5
5
 
6
6
  sketches.subscribe((sketches) => {
7
7
  props.update((currentProps) => {
8
8
  Object.keys(sketches).forEach((key) => {
9
9
  const sketch = sketches[key];
10
10
 
11
- if (sketch) { // sketch can be undefined if failed to load
11
+ if (sketch) {
12
+ // sketch can be undefined if failed to load
12
13
  currentProps[key] = reconcile(sketch.props, currentProps[key]);
13
14
  }
14
15
  });
@@ -17,7 +18,7 @@ sketches.subscribe((sketches) => {
17
18
  });
18
19
  });
19
20
 
20
- function reconcile(newProps = {}, prevProps = {}) {
21
+ export function reconcile(newProps = {}, prevProps = {}) {
21
22
  Object.keys(newProps).forEach((propKey) => {
22
23
  let newProp = newProps[propKey];
23
24
 
@@ -44,3 +45,25 @@ function reconcile(newProps = {}, prevProps = {}) {
44
45
 
45
46
  return newProps;
46
47
  }
48
+
49
+ /**
50
+ * Update prop value based on sketch key and prop key
51
+ * @param {string} sketchKey
52
+ * @param {string} propKey
53
+ * @param {any} newValue
54
+ */
55
+ export function updateProp(sketchKey, propKey, newValue) {
56
+ props.update((currentProps) => {
57
+ const prop = currentProps[sketchKey][propKey];
58
+
59
+ if (prop) {
60
+ prop.value = newValue;
61
+
62
+ if (typeof prop.onChange === 'function') {
63
+ prop.onChange(prop);
64
+ }
65
+ }
66
+
67
+ return currentProps;
68
+ });
69
+ }
@@ -1,30 +1,37 @@
1
- import { rendering } from "./rendering";
1
+ import { rendering } from './rendering';
2
2
 
3
3
  export let renderers = {};
4
4
 
5
5
  function loadRenderer(renderingMode) {
6
- if (__THREE_RENDERER__ && renderingMode === "three") {
7
- return import("../renderers/THREERenderer.js");
6
+ if (renderers[renderingMode]) return renderers[renderingMode];
7
+
8
+ if (__THREE_RENDERER__ && renderingMode === 'three') {
9
+ return import('../renderers/THREERenderer.js');
8
10
  }
9
11
 
10
- if (__FRAGMENT_RENDERER__ && renderingMode === "fragment") {
11
- return import("../renderers/FragmentRenderer.js");
12
+ if (__FRAGMENT_RENDERER__ && renderingMode === 'fragment') {
13
+ return import('../renderers/FragmentRenderer.js');
12
14
  }
13
15
 
14
- if (__P5_RENDERER__ && renderingMode === "p5") {
15
- return import("../renderers/P5Renderer.js");
16
+ if (__P5_RENDERER__ && renderingMode === 'p5') {
17
+ return import('../renderers/P5Renderer.js');
16
18
  }
17
19
 
18
- if (__2D_RENDERER__ && renderingMode === "2d") {
19
- return import("../renderers/2DRenderer.js");
20
+ if (__2D_RENDERER__ && renderingMode === '2d') {
21
+ return import('../renderers/2DRenderer.js');
20
22
  }
21
23
  }
22
24
 
23
- export async function findRenderer(renderingMode) {
24
- if (renderers[renderingMode]) return renderers[renderingMode];
25
-
25
+ export async function findRenderer({
26
+ rendering: renderingMode,
27
+ renderer: customRenderer,
28
+ }) {
26
29
  // load and save
27
- renderers[renderingMode] = await loadRenderer(renderingMode);
30
+ renderers[renderingMode] = customRenderer
31
+ ? typeof customRenderer === 'function'
32
+ ? await customRenderer()
33
+ : customRenderer
34
+ : await loadRenderer(renderingMode);
28
35
 
29
36
  // get
30
37
  let renderer = renderers[renderingMode];
@@ -35,7 +42,7 @@ export async function findRenderer(renderingMode) {
35
42
  let r;
36
43
 
37
44
  if (!initialized) {
38
- if (typeof renderer.init === "function") {
45
+ if (typeof renderer.init === 'function') {
39
46
  r = renderer.init({
40
47
  canvas: document.createElement('canvas'),
41
48
  pixelRatio: current.pixelRatio,
@@ -47,7 +54,7 @@ export async function findRenderer(renderingMode) {
47
54
 
48
55
  initialized = true;
49
56
 
50
- if (typeof renderer.resize === "function") {
57
+ if (typeof renderer.resize === 'function') {
51
58
  renderer.resize({
52
59
  width: current.width,
53
60
  height: current.height,
@@ -1,6 +1,6 @@
1
- import { createStore } from "./utils.js";
2
- import { displayError } from "../stores/errors";
3
- import { sketches as all, onSketchReload } from "@fragment/sketches";
1
+ import { createStore } from './utils.js';
2
+ import { displayError } from '../stores/errors';
3
+ import { sketches as all } from '@fragment/sketches';
4
4
 
5
5
  export const sketches = createStore('sketches', {});
6
6
  export const sketchesKeys = createStore('sketchesKeys', Object.keys(all));
@@ -16,9 +16,11 @@ async function loadSketch(collection, key) {
16
16
  }
17
17
  }
18
18
 
19
- async function loadAll(collection) {
19
+ export async function loadAll(collection) {
20
20
  const keys = [...Object.keys(collection)];
21
- const loadedSketches = await Promise.all(keys.map((key) => loadSketch(collection, key)));
21
+ const loadedSketches = await Promise.all(
22
+ keys.map((key) => loadSketch(collection, key)),
23
+ );
22
24
 
23
25
  const newSketches = keys.reduce((all, key, index) => {
24
26
  if (loadedSketches[index]) {
@@ -34,7 +36,3 @@ async function loadAll(collection) {
34
36
  }
35
37
 
36
38
  loadAll(all);
37
-
38
- onSketchReload(({ sketches }) => {
39
- loadAll(sketches);
40
- });
@@ -1,66 +1,123 @@
1
- import { writable } from "svelte/store";
1
+ import { writable } from 'svelte/store';
2
+ import { getContext } from '../triggers/shared';
2
3
 
3
4
  let stores = new Map();
4
5
 
5
6
  /**
6
7
  * Returns the value stored in localStorage for key or return defaultValue if it doesn't exist
7
- * @param {string} key
8
- * @param {any} defaultValue
9
- * @param {boolean} override
8
+ * @param {string} key
9
+ * @param {any} defaultValue
10
+ * @param {boolean} override
10
11
  * @returns {any} result
11
12
  */
12
13
  export function rehydrate(key, defaultValue) {
13
- const storedValue = localStorage.getItem(`fragment.${key}`);
14
+ const storedValue = localStorage.getItem(`fragment.${key}`);
14
15
 
15
- if (storedValue) {
16
- return typeof storedValue === "string" ? JSON.parse(storedValue) : storedValue;
17
- }
16
+ if (storedValue) {
17
+ return typeof storedValue === 'string'
18
+ ? JSON.parse(storedValue)
19
+ : storedValue;
20
+ }
18
21
 
19
- return defaultValue;
20
- };
22
+ return defaultValue;
23
+ }
21
24
 
22
25
  /**
23
26
  * Save value in localStorage
24
- * @param {string} key
25
- * @param {any} value
27
+ * @param {string} key
28
+ * @param {any} value
26
29
  */
27
30
  export function save(key, value) {
28
- localStorage.setItem(`fragment.${key}`, JSON.stringify(value));
29
- };
31
+ localStorage.setItem(`fragment.${key}`, JSON.stringify(value));
32
+ }
30
33
 
31
34
  /**
32
35
  * Create store and register it for later usage
33
- * @param {string} key
34
- * @param {any} initialValue
36
+ * @param {string} key
37
+ * @param {any} initialValue
35
38
  * @param {object} options
36
- * @returns {object} store
39
+ * @returns {import('svelte/store').Writable} store
37
40
  */
38
- export function createStore(key, initialValue, { persist = false, reset = false } = {}) {
39
- const value = (persist && !reset) ? rehydrate(key, initialValue) : initialValue;
40
- const store = writable(value);
41
+ export function createStore(
42
+ key,
43
+ initialValue,
44
+ { persist = false, reset = false } = {},
45
+ ) {
46
+ const value =
47
+ persist && !reset ? rehydrate(key, initialValue) : initialValue;
48
+ const store = writable(value);
41
49
 
42
- if (persist) {
43
- store.subscribe((current) => {
44
- save(key, current);
45
- });
46
- }
47
-
48
- stores.set(key, store);
50
+ if (persist) {
51
+ store.subscribe((current) => {
52
+ save(key, current);
53
+ });
54
+ }
49
55
 
50
- return store;
51
- };
56
+ stores.set(key, store);
57
+
58
+ return store;
59
+ }
52
60
 
53
61
  /**
54
62
  * Get an existing store from key or create it if it doesn't exist yet
55
- * @param {string} key
56
- * @param {any} initialValue
63
+ * @param {string} key
64
+ * @param {any} initialValue
57
65
  * @param {object} options
58
- * @returns {object} store
66
+ * @returns {import('svelte/store').Writable} store
59
67
  */
60
- export function getStore(key, initialValue, { persist = false, reset = false } = {}) {
61
- if (!stores.has(key)) {
62
- return createStore(key, initialValue, { persist, reset });
63
- }
68
+ export function getStore(
69
+ key,
70
+ initialValue,
71
+ { persist = false, reset = false } = {},
72
+ ) {
73
+ if (!stores.has(key)) {
74
+ return createStore(key, initialValue, { persist, reset });
75
+ }
76
+
77
+ return stores.get(key);
78
+ }
79
+
80
+ /**
81
+ * Create a new store to register callbacks grouped by context
82
+ * @param {string} name
83
+ */
84
+ export function createHookStore(name) {
85
+ const store = writable(new Map());
86
+
87
+ const hook = (fn, { context = getContext() } = {}) => {
88
+ if (typeof fn !== 'function') {
89
+ console.warn(`${name} argument must be a function.`);
90
+ }
91
+
92
+ store.update((hooks) => {
93
+ if (!hooks.has(context)) {
94
+ hooks.set(context, []);
95
+ }
96
+
97
+ hooks.set(context, [...hooks.get(context), fn]);
98
+
99
+ return hooks;
100
+ });
101
+
102
+ return () => {
103
+ store.update((hooks) => {
104
+ hooks.set(
105
+ context,
106
+ [...hooks.get(context)].filter((hook) => hook !== fn),
107
+ );
108
+
109
+ return hooks;
110
+ });
111
+ };
112
+ };
113
+
114
+ const remove = (context) => {
115
+ store.update((hooks) => {
116
+ hooks.delete(context);
117
+
118
+ return hooks;
119
+ });
120
+ };
64
121
 
65
- return stores.get(key);
66
- };
122
+ return [store, hook, remove];
123
+ }