svelte-p5-components 0.5.0 → 0.6.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.
package/README.md CHANGED
@@ -41,6 +41,16 @@ Pin to a SHA (`@<commit-sha>`) instead of `@main` for reproducible installs.
41
41
  </div>
42
42
  ```
43
43
 
44
+ Props:
45
+
46
+ | Prop | Type | Default | Notes |
47
+ | ---------- | ------------------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
48
+ | `sketch` | `SketchFn<Ext>` | — | Required. Generic over custom instance members (see the core README). |
49
+ | `hidpi` | `boolean \| number` | `true` | `true` → `devicePixelRatio`, `false` → `pixelDensity(1)`, number → that exact density (cap for large WEBGL canvases: `Math.min(devicePixelRatio, 2)`). |
50
+ | `instance` | `ExtendedP5<Ext> \| null` (bindable) | `null` | The p5 instance once mounted. |
51
+ | `onReady` | `(instance) => void` | — | Fires once after creation, density, and initial sizing. |
52
+ | `onResize` | `(instance, width, height) => void` | — | Fires after each `ResizeObserver`-driven `resizeCanvas` (not the initial sizing). Rebuild size-dependent state here; call `instance.loop()` if you're noLoop-gated. |
53
+
44
54
  ## `<FPSMonitor>`
45
55
 
46
56
  Absolute-positioned FPS readout. Takes the p5 instance and samples `p.frameRate()` every 30 frames by default.
@@ -1,16 +1,6 @@
1
- <script lang="ts">
2
- import { P5Canvas, type SketchFn } from 'svelte-p5';
3
- import type p5 from 'p5';
4
-
5
- interface Props {
6
- sketch: SketchFn;
7
- /** Apply `pixelDensity(devicePixelRatio)` on ready. Default: true */
8
- hidpi?: boolean;
9
- class?: string;
10
- style?: string;
11
- instance?: p5 | null;
12
- onReady?: (instance: p5) => void;
13
- }
1
+ <script lang="ts" generics="Ext = unknown">
2
+ import { P5Canvas, type ExtendedP5 } from 'svelte-p5';
3
+ import type { SketchProps } from './sketch-types.js';
14
4
 
15
5
  let {
16
6
  sketch,
@@ -18,14 +8,22 @@
18
8
  class: className = '',
19
9
  style = 'display: block; width: 100%; height: 100%; overflow: hidden;',
20
10
  instance = $bindable(null),
21
- onReady
22
- }: Props = $props();
11
+ onReady,
12
+ onResize
13
+ }: SketchProps<Ext> = $props();
23
14
 
24
15
  let container: HTMLDivElement | null = $state(null);
25
16
 
26
- function handleReady(p: p5) {
27
- if (hidpi && typeof window !== 'undefined') {
28
- p.pixelDensity(window.devicePixelRatio);
17
+ function handleReady(p: ExtendedP5<Ext>) {
18
+ if (typeof window !== 'undefined') {
19
+ if (typeof hidpi === 'number') {
20
+ p.pixelDensity(hidpi);
21
+ } else if (hidpi) {
22
+ p.pixelDensity(window.devicePixelRatio);
23
+ } else {
24
+ // p5 defaults to devicePixelRatio, so opting out must be explicit.
25
+ p.pixelDensity(1);
26
+ }
29
27
  }
30
28
  // Size immediately to the current container dimensions.
31
29
  if (container) {
@@ -43,7 +41,11 @@
43
41
  if (!entry || !instance) return;
44
42
  const w = Math.max(1, Math.floor(entry.contentRect.width));
45
43
  const h = Math.max(1, Math.floor(entry.contentRect.height));
44
+ // Skip no-op resizes; this also swallows the observer's initial
45
+ // fire, which would otherwise double-report the handleReady sizing.
46
+ if (w === instance.width && h === instance.height) return;
46
47
  instance.resizeCanvas(w, h);
48
+ onResize?.(instance, w, h);
47
49
  });
48
50
  ro.observe(el);
49
51
  return () => ro.disconnect();
@@ -1,14 +1,25 @@
1
- import { type SketchFn } from 'svelte-p5';
2
- import type p5 from 'p5';
3
- interface Props {
4
- sketch: SketchFn;
5
- /** Apply `pixelDensity(devicePixelRatio)` on ready. Default: true */
6
- hidpi?: boolean;
7
- class?: string;
8
- style?: string;
9
- instance?: p5 | null;
10
- onReady?: (instance: p5) => void;
1
+ import type { SketchProps } from './sketch-types.js';
2
+ declare function $$render<Ext = unknown>(): {
3
+ props: SketchProps<Ext>;
4
+ exports: {};
5
+ bindings: "instance";
6
+ slots: {};
7
+ events: {};
8
+ };
9
+ declare class __sveltets_Render<Ext = unknown> {
10
+ props(): ReturnType<typeof $$render<Ext>>['props'];
11
+ events(): ReturnType<typeof $$render<Ext>>['events'];
12
+ slots(): ReturnType<typeof $$render<Ext>>['slots'];
13
+ bindings(): "instance";
14
+ exports(): {};
11
15
  }
12
- declare const Sketch: import("svelte").Component<Props, {}, "instance">;
13
- type Sketch = ReturnType<typeof Sketch>;
16
+ interface $$IsomorphicComponent {
17
+ new <Ext = unknown>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<Ext>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<Ext>['props']>, ReturnType<__sveltets_Render<Ext>['events']>, ReturnType<__sveltets_Render<Ext>['slots']>> & {
18
+ $$bindings?: ReturnType<__sveltets_Render<Ext>['bindings']>;
19
+ } & ReturnType<__sveltets_Render<Ext>['exports']>;
20
+ <Ext = unknown>(internal: unknown, props: ReturnType<__sveltets_Render<Ext>['props']> & {}): ReturnType<__sveltets_Render<Ext>['exports']>;
21
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
22
+ }
23
+ declare const Sketch: $$IsomorphicComponent;
24
+ type Sketch<Ext = unknown> = InstanceType<typeof Sketch<Ext>>;
14
25
  export default Sketch;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,102 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { render } from '@testing-library/svelte';
3
+ import Sketch from './Sketch.svelte';
4
+ // P5Canvas does `await import('p5')`; give it a fake constructor that mirrors
5
+ // the bits Sketch touches. p5 calls the sketch function synchronously inside
6
+ // the constructor, and so does the fake.
7
+ vi.mock('p5', () => {
8
+ class FakeP5 {
9
+ width = 0;
10
+ height = 0;
11
+ resizeCanvas = vi.fn((w, h) => {
12
+ this.width = w;
13
+ this.height = h;
14
+ });
15
+ pixelDensity = vi.fn();
16
+ remove = vi.fn();
17
+ // P5Canvas passes (sketchFn, containerNode); the node is irrelevant here.
18
+ constructor(sketch) {
19
+ sketch(this);
20
+ }
21
+ }
22
+ return { default: FakeP5 };
23
+ });
24
+ // Controllable ResizeObserver stub: tests fire the captured callback directly.
25
+ let roCallback = null;
26
+ const savedRO = globalThis.ResizeObserver;
27
+ function fireResize(width, height) {
28
+ roCallback?.([{ contentRect: { width, height } }], {});
29
+ }
30
+ async function renderSketch(props = {}) {
31
+ let ready = null;
32
+ const onReady = (p) => {
33
+ ready = p;
34
+ };
35
+ const result = render(Sketch, { sketch: () => { }, onReady, ...props });
36
+ // P5Canvas awaits import('p5') before constructing; wait for onReady.
37
+ await vi.waitFor(() => {
38
+ if (!ready)
39
+ throw new Error('instance not ready');
40
+ });
41
+ return { ...result, instance: ready };
42
+ }
43
+ beforeEach(() => {
44
+ roCallback = null;
45
+ globalThis.ResizeObserver = class {
46
+ constructor(cb) {
47
+ roCallback = cb;
48
+ }
49
+ observe() { }
50
+ unobserve() { }
51
+ disconnect() { }
52
+ };
53
+ });
54
+ afterEach(() => {
55
+ globalThis.ResizeObserver = savedRO;
56
+ });
57
+ describe('<Sketch> resize', () => {
58
+ it('calls resizeCanvas then onResize, in that order, when the container resizes', async () => {
59
+ const calls = [];
60
+ const onResize = vi.fn((_p, w, h) => {
61
+ calls.push(`onResize:${w}x${h}`);
62
+ });
63
+ const { instance } = await renderSketch({ onResize });
64
+ instance.resizeCanvas.mockImplementation((w, h) => {
65
+ instance.width = w;
66
+ instance.height = h;
67
+ calls.push(`resizeCanvas:${w}x${h}`);
68
+ });
69
+ fireResize(640, 480);
70
+ expect(calls).toEqual(['resizeCanvas:640x480', 'onResize:640x480']);
71
+ expect(onResize).toHaveBeenCalledWith(instance, 640, 480);
72
+ });
73
+ it('skips resizeCanvas and onResize when the size is unchanged', async () => {
74
+ const onResize = vi.fn();
75
+ const { instance } = await renderSketch({ onResize });
76
+ instance.width = 300;
77
+ instance.height = 200;
78
+ instance.resizeCanvas.mockClear();
79
+ fireResize(300, 200);
80
+ expect(instance.resizeCanvas).not.toHaveBeenCalled();
81
+ expect(onResize).not.toHaveBeenCalled();
82
+ });
83
+ it('does not fire onResize for the initial handleReady sizing', async () => {
84
+ const onResize = vi.fn();
85
+ await renderSketch({ onResize });
86
+ expect(onResize).not.toHaveBeenCalled();
87
+ });
88
+ });
89
+ describe('<Sketch> pixel density', () => {
90
+ it('hidpi=true applies devicePixelRatio', async () => {
91
+ const { instance } = await renderSketch({ hidpi: true });
92
+ expect(instance.pixelDensity).toHaveBeenCalledWith(window.devicePixelRatio);
93
+ });
94
+ it('hidpi=false pins density to 1', async () => {
95
+ const { instance } = await renderSketch({ hidpi: false });
96
+ expect(instance.pixelDensity).toHaveBeenCalledWith(1);
97
+ });
98
+ it('numeric hidpi applies that exact density', async () => {
99
+ const { instance } = await renderSketch({ hidpi: 1.5 });
100
+ expect(instance.pixelDensity).toHaveBeenCalledWith(1.5);
101
+ });
102
+ });
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { default as Sketch } from './Sketch.svelte';
2
+ export type { SketchProps } from './sketch-types.js';
2
3
  export { default as FPSMonitor } from './FPSMonitor.svelte';
3
4
  export { default as SketchDebug } from './SketchDebug.svelte';
4
5
  export { default as DraggableWindow } from './DraggableWindow.svelte';
@@ -0,0 +1,28 @@
1
+ import type { ExtendedP5, SketchFn } from 'svelte-p5';
2
+ /**
3
+ * Props accepted by the `<Sketch>` component.
4
+ */
5
+ export interface SketchProps<Ext = unknown> {
6
+ sketch: SketchFn<Ext>;
7
+ /**
8
+ * Pixel density applied on ready. `true` → `devicePixelRatio`,
9
+ * `false` → 1 (p5's own default is already devicePixelRatio, so this
10
+ * is the only way to genuinely opt out), a number → that exact
11
+ * density. Cap it for large WEBGL canvases, e.g.
12
+ * `Math.min(devicePixelRatio, 2)`. Default: true.
13
+ */
14
+ hidpi?: boolean | number;
15
+ class?: string;
16
+ style?: string;
17
+ /** Bindable: the p5 instance, available after mount. `null` before mount or after unmount. */
18
+ instance?: ExtendedP5<Ext> | null;
19
+ /** Called once after creation, pixel density, and initial container sizing. */
20
+ onReady?: (instance: ExtendedP5<Ext>) => void;
21
+ /**
22
+ * Called after the canvas has been resized to track its container
23
+ * (ResizeObserver path). Not called for the initial sizing — use
24
+ * `onReady` for that. Rebuild size-dependent state here and request
25
+ * a redraw if your sketch is noLoop-gated.
26
+ */
27
+ onResize?: (instance: ExtendedP5<Ext>, width: number, height: number) => void;
28
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-p5-components",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Higher-level Svelte 5 components built on svelte-p5: responsive canvas, FPS monitor, sketch debug overlay, draggable windows",
5
5
  "keywords": [
6
6
  "svelte",
@@ -51,10 +51,11 @@
51
51
  "test:coverage": "vitest run --coverage"
52
52
  },
53
53
  "dependencies": {
54
+ "@neodrag/core": "3.0.0-next.8",
54
55
  "@neodrag/svelte": "3.0.0-next.8"
55
56
  },
56
57
  "peerDependencies": {
57
- "svelte-p5": "workspace:^",
58
+ "svelte-p5": ">=0.4.0",
58
59
  "p5": ">=1.11.0 <3",
59
60
  "svelte": "^5.0.0"
60
61
  },