svelte-p5-components 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.
- package/LICENSE +21 -0
- package/README.md +110 -0
- package/dist/DraggableSketch.svelte +58 -0
- package/dist/DraggableSketch.svelte.d.ts +23 -0
- package/dist/DraggableWindow.svelte +223 -0
- package/dist/DraggableWindow.svelte.d.ts +26 -0
- package/dist/FPSMonitor.svelte +62 -0
- package/dist/FPSMonitor.svelte.d.ts +11 -0
- package/dist/Sketch.svelte +55 -0
- package/dist/Sketch.svelte.d.ts +14 -0
- package/dist/SketchDebug.svelte +75 -0
- package/dist/SketchDebug.svelte.d.ts +9 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/windowManager.svelte.d.ts +17 -0
- package/dist/windowManager.svelte.js +27 -0
- package/package.json +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Edwin Zhao
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# svelte-p5-components
|
|
2
|
+
|
|
3
|
+
Higher-level Svelte 5 components built on [`svelte-p5`](../core). The pieces that almost every sketch-driven app ends up writing.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add svelte-p5 svelte-p5-components p5
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## `<Sketch>`
|
|
12
|
+
|
|
13
|
+
`<P5Canvas>` plus a `ResizeObserver` on the parent element and automatic `pixelDensity(devicePixelRatio)`. The default you probably want.
|
|
14
|
+
|
|
15
|
+
```svelte
|
|
16
|
+
<script lang="ts">
|
|
17
|
+
import { Sketch } from 'svelte-p5-components';
|
|
18
|
+
import type p5 from 'p5';
|
|
19
|
+
|
|
20
|
+
const sketch = (p: p5) => {
|
|
21
|
+
p.setup = () => p.createCanvas(p.windowWidth, p.windowHeight);
|
|
22
|
+
p.draw = () => {
|
|
23
|
+
p.background(240);
|
|
24
|
+
p.circle(p.mouseX, p.mouseY, 40);
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<div style="width: 100%; height: 400px;">
|
|
30
|
+
<Sketch {sketch} />
|
|
31
|
+
</div>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## `<FPSMonitor>`
|
|
35
|
+
|
|
36
|
+
Absolute-positioned FPS readout. Takes the p5 instance and samples `p.frameRate()` every 30 frames by default.
|
|
37
|
+
|
|
38
|
+
```svelte
|
|
39
|
+
<script lang="ts">
|
|
40
|
+
import { P5Canvas } from 'svelte-p5';
|
|
41
|
+
import { FPSMonitor } from 'svelte-p5-components';
|
|
42
|
+
import type p5 from 'p5';
|
|
43
|
+
|
|
44
|
+
let instance = $state<p5 | null>(null);
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<div style="position: relative;">
|
|
48
|
+
<P5Canvas {sketch} bind:instance />
|
|
49
|
+
<FPSMonitor {instance} position="top-right" />
|
|
50
|
+
</div>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## `<SketchDebug>`
|
|
54
|
+
|
|
55
|
+
Development overlay with mouse position, canvas size, frame count, delta time, and FPS. Same rAF-driven pattern as `<FPSMonitor>`.
|
|
56
|
+
|
|
57
|
+
## `<DraggableWindow>`
|
|
58
|
+
|
|
59
|
+
A floating, draggable, resizable panel powered by [neodrag](https://neodrag.dev/). Covers the floating-palette pattern: inspectors, info panels, notes, anything you'd otherwise build on top of a bare drag library.
|
|
60
|
+
|
|
61
|
+
What it handles for you:
|
|
62
|
+
|
|
63
|
+
- Drag-by-title-bar only, so buttons in the header stay clickable.
|
|
64
|
+
- Focus to front on click. Every `<DraggableWindow>` on the page shares a z-index manager; clicking raises.
|
|
65
|
+
- Clamps back into view on drag end and on window resize. `minVisible` (default 60px) guarantees a reachable grab handle.
|
|
66
|
+
- Passes extra neodrag plugins through, so you can add grid snapping, axis locks, thresholds without forking the component.
|
|
67
|
+
|
|
68
|
+
```svelte
|
|
69
|
+
<script lang="ts">
|
|
70
|
+
import { DraggableWindow } from 'svelte-p5-components';
|
|
71
|
+
import { grid, axis } from '@neodrag/svelte';
|
|
72
|
+
|
|
73
|
+
let showInspector = $state(true);
|
|
74
|
+
</script>
|
|
75
|
+
|
|
76
|
+
{#if showInspector}
|
|
77
|
+
<DraggableWindow
|
|
78
|
+
title="Inspector"
|
|
79
|
+
initialX={40}
|
|
80
|
+
initialY={80}
|
|
81
|
+
width={320}
|
|
82
|
+
height={240}
|
|
83
|
+
plugins={[grid([20, 20]), axis('x')]}
|
|
84
|
+
onClose={() => (showInspector = false)}
|
|
85
|
+
>
|
|
86
|
+
<p>Snaps to a 20px grid, slides horizontally only.</p>
|
|
87
|
+
</DraggableWindow>
|
|
88
|
+
{/if}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## `<DraggableSketch>`
|
|
92
|
+
|
|
93
|
+
`<DraggableWindow>` with a `<Sketch>` inside. One line for a floating, draggable, resizable p5 sketch. This is the primitive for multi-window visualization suites.
|
|
94
|
+
|
|
95
|
+
```svelte
|
|
96
|
+
<DraggableSketch
|
|
97
|
+
title="Speaker Garden"
|
|
98
|
+
{sketch}
|
|
99
|
+
initialX={80}
|
|
100
|
+
initialY={80}
|
|
101
|
+
width={420}
|
|
102
|
+
height={320}
|
|
103
|
+
/>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Same `plugins` escape hatch as `<DraggableWindow>`.
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
MIT
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import DraggableWindow from './DraggableWindow.svelte';
|
|
3
|
+
import Sketch from './Sketch.svelte';
|
|
4
|
+
import type { SketchFn } from 'svelte-p5';
|
|
5
|
+
import type { Plugin } from '@neodrag/svelte';
|
|
6
|
+
import type p5 from 'p5';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
sketch: SketchFn;
|
|
10
|
+
title?: string;
|
|
11
|
+
initialX?: number;
|
|
12
|
+
initialY?: number;
|
|
13
|
+
width?: string | number;
|
|
14
|
+
height?: string | number;
|
|
15
|
+
minWidth?: string | number;
|
|
16
|
+
minHeight?: string | number;
|
|
17
|
+
minVisible?: number;
|
|
18
|
+
hidpi?: boolean;
|
|
19
|
+
constrained?: 'viewport' | 'parent' | 'none';
|
|
20
|
+
/** Extra neodrag plugins appended to the ones DraggableWindow sets internally. Escape hatch for power users (axis, grid, threshold, etc.). */
|
|
21
|
+
plugins?: Plugin<unknown>[];
|
|
22
|
+
onClose?: () => void;
|
|
23
|
+
instance?: p5 | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let {
|
|
27
|
+
sketch,
|
|
28
|
+
title = 'Sketch',
|
|
29
|
+
initialX = 40,
|
|
30
|
+
initialY = 40,
|
|
31
|
+
width = 400,
|
|
32
|
+
height = 300,
|
|
33
|
+
minWidth = 200,
|
|
34
|
+
minHeight = 150,
|
|
35
|
+
minVisible = 60,
|
|
36
|
+
hidpi = true,
|
|
37
|
+
constrained = 'viewport',
|
|
38
|
+
plugins,
|
|
39
|
+
onClose,
|
|
40
|
+
instance = $bindable(null)
|
|
41
|
+
}: Props = $props();
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<DraggableWindow
|
|
45
|
+
{title}
|
|
46
|
+
{initialX}
|
|
47
|
+
{initialY}
|
|
48
|
+
{width}
|
|
49
|
+
{height}
|
|
50
|
+
{minWidth}
|
|
51
|
+
{minHeight}
|
|
52
|
+
{minVisible}
|
|
53
|
+
{constrained}
|
|
54
|
+
{plugins}
|
|
55
|
+
{onClose}
|
|
56
|
+
>
|
|
57
|
+
<Sketch {sketch} {hidpi} bind:instance />
|
|
58
|
+
</DraggableWindow>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { SketchFn } from 'svelte-p5';
|
|
2
|
+
import type { Plugin } from '@neodrag/svelte';
|
|
3
|
+
import type p5 from 'p5';
|
|
4
|
+
interface Props {
|
|
5
|
+
sketch: SketchFn;
|
|
6
|
+
title?: string;
|
|
7
|
+
initialX?: number;
|
|
8
|
+
initialY?: number;
|
|
9
|
+
width?: string | number;
|
|
10
|
+
height?: string | number;
|
|
11
|
+
minWidth?: string | number;
|
|
12
|
+
minHeight?: string | number;
|
|
13
|
+
minVisible?: number;
|
|
14
|
+
hidpi?: boolean;
|
|
15
|
+
constrained?: 'viewport' | 'parent' | 'none';
|
|
16
|
+
/** Extra neodrag plugins appended to the ones DraggableWindow sets internally. Escape hatch for power users (axis, grid, threshold, etc.). */
|
|
17
|
+
plugins?: Plugin<unknown>[];
|
|
18
|
+
onClose?: () => void;
|
|
19
|
+
instance?: p5 | null;
|
|
20
|
+
}
|
|
21
|
+
declare const DraggableSketch: import("svelte").Component<Props, {}, "instance">;
|
|
22
|
+
type DraggableSketch = ReturnType<typeof DraggableSketch>;
|
|
23
|
+
export default DraggableSketch;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
draggable,
|
|
4
|
+
controls,
|
|
5
|
+
ControlFrom,
|
|
6
|
+
bounds,
|
|
7
|
+
BoundsFrom,
|
|
8
|
+
events,
|
|
9
|
+
position,
|
|
10
|
+
Compartment,
|
|
11
|
+
type Plugin
|
|
12
|
+
} from '@neodrag/svelte';
|
|
13
|
+
import { untrack, type Snippet } from 'svelte';
|
|
14
|
+
import { registerWindow } from './windowManager.svelte.js';
|
|
15
|
+
|
|
16
|
+
type ConstrainTo = 'viewport' | 'parent' | 'none';
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
title?: string;
|
|
20
|
+
initialX?: number;
|
|
21
|
+
initialY?: number;
|
|
22
|
+
width?: string | number;
|
|
23
|
+
height?: string | number;
|
|
24
|
+
minWidth?: string | number;
|
|
25
|
+
minHeight?: string | number;
|
|
26
|
+
/** Constrain drag within the viewport, the parent element, or not at all. */
|
|
27
|
+
constrained?: ConstrainTo;
|
|
28
|
+
/** Pixels of the window that must remain visible after clamp. Default: 60 */
|
|
29
|
+
minVisible?: number;
|
|
30
|
+
/** Extra neodrag plugins appended to the ones the window sets internally. Escape hatch for power users (axis, grid, threshold, etc.). */
|
|
31
|
+
plugins?: Plugin<unknown>[];
|
|
32
|
+
/** Called when the close (×) button is clicked. Omit to hide the button. */
|
|
33
|
+
onClose?: () => void;
|
|
34
|
+
/** Optional custom content for the title-bar (defaults to `title`). */
|
|
35
|
+
header?: Snippet;
|
|
36
|
+
children?: Snippet;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let {
|
|
40
|
+
title = 'Window',
|
|
41
|
+
initialX = 40,
|
|
42
|
+
initialY = 40,
|
|
43
|
+
width = 480,
|
|
44
|
+
height = 320,
|
|
45
|
+
minWidth = 200,
|
|
46
|
+
minHeight = 120,
|
|
47
|
+
constrained = 'viewport',
|
|
48
|
+
minVisible = 60,
|
|
49
|
+
plugins: userPlugins,
|
|
50
|
+
onClose,
|
|
51
|
+
header,
|
|
52
|
+
children
|
|
53
|
+
}: Props = $props();
|
|
54
|
+
|
|
55
|
+
function toCssSize(v: string | number): string {
|
|
56
|
+
return typeof v === 'number' ? `${v}px` : v;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Focus management — each window gets a reactive z-index slot shared
|
|
60
|
+
// across every DraggableWindow on the page. Clicking brings it to top.
|
|
61
|
+
const win = registerWindow();
|
|
62
|
+
|
|
63
|
+
// Live position. Neodrag's `position` plugin drives the element via
|
|
64
|
+
// transform; we update this state from onDrag AND on resize/clamp,
|
|
65
|
+
// and the Compartment re-evaluates to push changes back into neodrag.
|
|
66
|
+
// `untrack` prevents these props from forming a reactive dep — they're
|
|
67
|
+
// *initial* values only; later position comes from user drag.
|
|
68
|
+
let pos = $state(untrack(() => ({ x: initialX, y: initialY })));
|
|
69
|
+
|
|
70
|
+
// Window size (tracked so we can compute clamp bounds). Updated once
|
|
71
|
+
// the element mounts and on every resize of the element itself.
|
|
72
|
+
let size = $state(untrack(() => ({ w: typeof width === 'number' ? width : 480, h: 40 })));
|
|
73
|
+
let rootEl: HTMLDivElement | null = $state(null);
|
|
74
|
+
|
|
75
|
+
function clampToViewport() {
|
|
76
|
+
if (typeof window === 'undefined') return;
|
|
77
|
+
const vw = window.innerWidth;
|
|
78
|
+
const vh = window.innerHeight;
|
|
79
|
+
// Never allow the titlebar above the viewport top — that's the
|
|
80
|
+
// "unreachable" case. Allow partial horizontal overflow so narrow
|
|
81
|
+
// screens don't trap wide windows, but always keep `minVisible`
|
|
82
|
+
// pixels of the window on-screen for a grab handle.
|
|
83
|
+
const minX = -(size.w - minVisible);
|
|
84
|
+
const maxX = vw - minVisible;
|
|
85
|
+
const minY = 0;
|
|
86
|
+
const maxY = vh - minVisible;
|
|
87
|
+
pos = {
|
|
88
|
+
x: Math.max(minX, Math.min(maxX, pos.x)),
|
|
89
|
+
y: Math.max(minY, Math.min(maxY, pos.y))
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Keep size fresh (used for clamp math) — ResizeObserver on our own
|
|
94
|
+
// root element catches CSS resize-from-corner too.
|
|
95
|
+
$effect(() => {
|
|
96
|
+
if (!rootEl) return;
|
|
97
|
+
const el = rootEl;
|
|
98
|
+
const ro = new ResizeObserver(() => {
|
|
99
|
+
const r = el.getBoundingClientRect();
|
|
100
|
+
size = { w: r.width, h: r.height };
|
|
101
|
+
});
|
|
102
|
+
ro.observe(el);
|
|
103
|
+
// seed
|
|
104
|
+
const r = el.getBoundingClientRect();
|
|
105
|
+
size = { w: r.width, h: r.height };
|
|
106
|
+
return () => ro.disconnect();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Re-clamp on window resize — this is the "snap back in when the
|
|
110
|
+
// viewport shrinks" case.
|
|
111
|
+
$effect(() => {
|
|
112
|
+
if (typeof window === 'undefined') return;
|
|
113
|
+
const onResize = () => clampToViewport();
|
|
114
|
+
window.addEventListener('resize', onResize);
|
|
115
|
+
return () => window.removeEventListener('resize', onResize);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// The neodrag `position` plugin takes a `current: {x, y}` and forces
|
|
119
|
+
// the element to that transform. Wrapped in a Compartment so updates
|
|
120
|
+
// to `pos` re-run just this plugin (not the whole plugin list).
|
|
121
|
+
const posComp = Compartment.of(() => position({ current: pos }));
|
|
122
|
+
|
|
123
|
+
const plugins = $derived([
|
|
124
|
+
posComp,
|
|
125
|
+
controls({ allow: ControlFrom.selector('.dw-handle') }),
|
|
126
|
+
events({
|
|
127
|
+
onDragStart: () => win.focus(),
|
|
128
|
+
onDrag: ({ offset }) => {
|
|
129
|
+
pos = { x: offset.x, y: offset.y };
|
|
130
|
+
},
|
|
131
|
+
onDragEnd: () => clampToViewport()
|
|
132
|
+
}),
|
|
133
|
+
...(constrained === 'viewport' ? [bounds(BoundsFrom.viewport())] : []),
|
|
134
|
+
...(constrained === 'parent' ? [bounds(BoundsFrom.parent())] : []),
|
|
135
|
+
...(userPlugins ?? [])
|
|
136
|
+
]);
|
|
137
|
+
</script>
|
|
138
|
+
|
|
139
|
+
<div
|
|
140
|
+
bind:this={rootEl}
|
|
141
|
+
class="draggable-window"
|
|
142
|
+
style:width={toCssSize(width)}
|
|
143
|
+
style:height={toCssSize(height)}
|
|
144
|
+
style:min-width={toCssSize(minWidth)}
|
|
145
|
+
style:min-height={toCssSize(minHeight)}
|
|
146
|
+
style:z-index={win.z}
|
|
147
|
+
onpointerdowncapture={() => win.focus()}
|
|
148
|
+
{@attach draggable(() => plugins)}
|
|
149
|
+
>
|
|
150
|
+
<div class="dw-titlebar">
|
|
151
|
+
<div class="dw-handle">
|
|
152
|
+
{#if header}{@render header()}{:else}<span class="dw-title-text">{title}</span>{/if}
|
|
153
|
+
</div>
|
|
154
|
+
{#if onClose}
|
|
155
|
+
<button class="dw-close" onclick={onClose} aria-label="Close window">×</button>
|
|
156
|
+
{/if}
|
|
157
|
+
</div>
|
|
158
|
+
<div class="dw-body">
|
|
159
|
+
{@render children?.()}
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<style>
|
|
164
|
+
.draggable-window {
|
|
165
|
+
position: fixed;
|
|
166
|
+
top: 0;
|
|
167
|
+
left: 0;
|
|
168
|
+
display: flex;
|
|
169
|
+
flex-direction: column;
|
|
170
|
+
background: #fff;
|
|
171
|
+
border: 1px solid rgba(0, 0, 0, 0.15);
|
|
172
|
+
border-radius: 8px;
|
|
173
|
+
box-shadow:
|
|
174
|
+
0 2px 4px rgba(0, 0, 0, 0.04),
|
|
175
|
+
0 10px 30px rgba(0, 0, 0, 0.12);
|
|
176
|
+
overflow: hidden;
|
|
177
|
+
/* Native resize from the bottom-right corner. */
|
|
178
|
+
resize: both;
|
|
179
|
+
}
|
|
180
|
+
.dw-titlebar {
|
|
181
|
+
display: flex;
|
|
182
|
+
align-items: center;
|
|
183
|
+
padding: 0 4px 0 10px;
|
|
184
|
+
background: #f3f3f5;
|
|
185
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
|
186
|
+
user-select: none;
|
|
187
|
+
}
|
|
188
|
+
.dw-handle {
|
|
189
|
+
flex: 1;
|
|
190
|
+
padding: 6px 0;
|
|
191
|
+
cursor: move;
|
|
192
|
+
font:
|
|
193
|
+
12px/1.4 system-ui,
|
|
194
|
+
-apple-system,
|
|
195
|
+
sans-serif;
|
|
196
|
+
font-weight: 600;
|
|
197
|
+
color: #333;
|
|
198
|
+
}
|
|
199
|
+
.dw-title-text {
|
|
200
|
+
display: inline-block;
|
|
201
|
+
vertical-align: middle;
|
|
202
|
+
}
|
|
203
|
+
.dw-close {
|
|
204
|
+
background: none;
|
|
205
|
+
border: 0;
|
|
206
|
+
font:
|
|
207
|
+
18px/1 system-ui,
|
|
208
|
+
sans-serif;
|
|
209
|
+
color: #555;
|
|
210
|
+
cursor: pointer;
|
|
211
|
+
padding: 2px 8px;
|
|
212
|
+
border-radius: 4px;
|
|
213
|
+
}
|
|
214
|
+
.dw-close:hover {
|
|
215
|
+
background: rgba(0, 0, 0, 0.06);
|
|
216
|
+
color: #000;
|
|
217
|
+
}
|
|
218
|
+
.dw-body {
|
|
219
|
+
flex: 1;
|
|
220
|
+
overflow: hidden;
|
|
221
|
+
position: relative;
|
|
222
|
+
}
|
|
223
|
+
</style>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Plugin } from '@neodrag/svelte';
|
|
2
|
+
import { type Snippet } from 'svelte';
|
|
3
|
+
type ConstrainTo = 'viewport' | 'parent' | 'none';
|
|
4
|
+
interface Props {
|
|
5
|
+
title?: string;
|
|
6
|
+
initialX?: number;
|
|
7
|
+
initialY?: number;
|
|
8
|
+
width?: string | number;
|
|
9
|
+
height?: string | number;
|
|
10
|
+
minWidth?: string | number;
|
|
11
|
+
minHeight?: string | number;
|
|
12
|
+
/** Constrain drag within the viewport, the parent element, or not at all. */
|
|
13
|
+
constrained?: ConstrainTo;
|
|
14
|
+
/** Pixels of the window that must remain visible after clamp. Default: 60 */
|
|
15
|
+
minVisible?: number;
|
|
16
|
+
/** Extra neodrag plugins appended to the ones the window sets internally. Escape hatch for power users (axis, grid, threshold, etc.). */
|
|
17
|
+
plugins?: Plugin<unknown>[];
|
|
18
|
+
/** Called when the close (×) button is clicked. Omit to hide the button. */
|
|
19
|
+
onClose?: () => void;
|
|
20
|
+
/** Optional custom content for the title-bar (defaults to `title`). */
|
|
21
|
+
header?: Snippet;
|
|
22
|
+
children?: Snippet;
|
|
23
|
+
}
|
|
24
|
+
declare const DraggableWindow: import("svelte").Component<Props, {}, "">;
|
|
25
|
+
type DraggableWindow = ReturnType<typeof DraggableWindow>;
|
|
26
|
+
export default DraggableWindow;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type p5 from 'p5';
|
|
3
|
+
|
|
4
|
+
type Position = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
instance: p5 | null;
|
|
8
|
+
position?: Position;
|
|
9
|
+
/** How many frames to average FPS over before rendering a new value. Default: 30 */
|
|
10
|
+
sampleFrames?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let { instance, position = 'top-right', sampleFrames = 30 }: Props = $props();
|
|
14
|
+
|
|
15
|
+
let fps = $state(0);
|
|
16
|
+
|
|
17
|
+
$effect(() => {
|
|
18
|
+
if (!instance) return;
|
|
19
|
+
const p = instance;
|
|
20
|
+
let raf = 0;
|
|
21
|
+
let frames = 0;
|
|
22
|
+
const tick = () => {
|
|
23
|
+
frames += 1;
|
|
24
|
+
if (frames >= sampleFrames) {
|
|
25
|
+
fps = Math.round(p.frameRate());
|
|
26
|
+
frames = 0;
|
|
27
|
+
}
|
|
28
|
+
raf = requestAnimationFrame(tick);
|
|
29
|
+
};
|
|
30
|
+
raf = requestAnimationFrame(tick);
|
|
31
|
+
return () => cancelAnimationFrame(raf);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const positionStyles: Record<Position, string> = {
|
|
35
|
+
'top-left': 'top: 8px; left: 8px;',
|
|
36
|
+
'top-right': 'top: 8px; right: 8px;',
|
|
37
|
+
'bottom-left': 'bottom: 8px; left: 8px;',
|
|
38
|
+
'bottom-right': 'bottom: 8px; right: 8px;'
|
|
39
|
+
};
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<div class="fps-monitor" style={positionStyles[position]}>
|
|
43
|
+
{fps} fps
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<style>
|
|
47
|
+
.fps-monitor {
|
|
48
|
+
position: absolute;
|
|
49
|
+
padding: 4px 8px;
|
|
50
|
+
font:
|
|
51
|
+
11px/1 ui-monospace,
|
|
52
|
+
'SF Mono',
|
|
53
|
+
Menlo,
|
|
54
|
+
monospace;
|
|
55
|
+
color: #fff;
|
|
56
|
+
background: rgba(0, 0, 0, 0.65);
|
|
57
|
+
border-radius: 4px;
|
|
58
|
+
pointer-events: none;
|
|
59
|
+
user-select: none;
|
|
60
|
+
z-index: 10;
|
|
61
|
+
}
|
|
62
|
+
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type p5 from 'p5';
|
|
2
|
+
type Position = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
3
|
+
interface Props {
|
|
4
|
+
instance: p5 | null;
|
|
5
|
+
position?: Position;
|
|
6
|
+
/** How many frames to average FPS over before rendering a new value. Default: 30 */
|
|
7
|
+
sampleFrames?: number;
|
|
8
|
+
}
|
|
9
|
+
declare const FPSMonitor: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type FPSMonitor = ReturnType<typeof FPSMonitor>;
|
|
11
|
+
export default FPSMonitor;
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
}
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
sketch,
|
|
17
|
+
hidpi = true,
|
|
18
|
+
class: className = '',
|
|
19
|
+
style = 'display: block; width: 100%; height: 100%; overflow: hidden;',
|
|
20
|
+
instance = $bindable(null),
|
|
21
|
+
onReady
|
|
22
|
+
}: Props = $props();
|
|
23
|
+
|
|
24
|
+
let container: HTMLDivElement | null = $state(null);
|
|
25
|
+
|
|
26
|
+
function handleReady(p: p5) {
|
|
27
|
+
if (hidpi && typeof window !== 'undefined') {
|
|
28
|
+
p.pixelDensity(window.devicePixelRatio);
|
|
29
|
+
}
|
|
30
|
+
// Size immediately to the current container dimensions.
|
|
31
|
+
if (container) {
|
|
32
|
+
const rect = container.getBoundingClientRect();
|
|
33
|
+
p.resizeCanvas(Math.max(1, Math.floor(rect.width)), Math.max(1, Math.floor(rect.height)));
|
|
34
|
+
}
|
|
35
|
+
onReady?.(p);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
$effect(() => {
|
|
39
|
+
if (!container) return;
|
|
40
|
+
const el = container;
|
|
41
|
+
const ro = new ResizeObserver((entries) => {
|
|
42
|
+
const entry = entries[0];
|
|
43
|
+
if (!entry || !instance) return;
|
|
44
|
+
const w = Math.max(1, Math.floor(entry.contentRect.width));
|
|
45
|
+
const h = Math.max(1, Math.floor(entry.contentRect.height));
|
|
46
|
+
instance.resizeCanvas(w, h);
|
|
47
|
+
});
|
|
48
|
+
ro.observe(el);
|
|
49
|
+
return () => ro.disconnect();
|
|
50
|
+
});
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<div bind:this={container} class={className} {style}>
|
|
54
|
+
<P5Canvas {sketch} bind:instance onReady={handleReady} />
|
|
55
|
+
</div>
|
|
@@ -0,0 +1,14 @@
|
|
|
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;
|
|
11
|
+
}
|
|
12
|
+
declare const Sketch: import("svelte").Component<Props, {}, "instance">;
|
|
13
|
+
type Sketch = ReturnType<typeof Sketch>;
|
|
14
|
+
export default Sketch;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type p5 from 'p5';
|
|
3
|
+
|
|
4
|
+
type Position = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
instance: p5 | null;
|
|
8
|
+
position?: Position;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let { instance, position = 'bottom-right' }: Props = $props();
|
|
12
|
+
|
|
13
|
+
let stats = $state({
|
|
14
|
+
mouseX: 0,
|
|
15
|
+
mouseY: 0,
|
|
16
|
+
width: 0,
|
|
17
|
+
height: 0,
|
|
18
|
+
frameCount: 0,
|
|
19
|
+
deltaTime: 0,
|
|
20
|
+
fps: 0
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
$effect(() => {
|
|
24
|
+
if (!instance) return;
|
|
25
|
+
const p = instance;
|
|
26
|
+
let raf = 0;
|
|
27
|
+
const tick = () => {
|
|
28
|
+
stats.mouseX = Math.round(p.mouseX);
|
|
29
|
+
stats.mouseY = Math.round(p.mouseY);
|
|
30
|
+
stats.width = p.width;
|
|
31
|
+
stats.height = p.height;
|
|
32
|
+
stats.frameCount = p.frameCount;
|
|
33
|
+
stats.deltaTime = Math.round(p.deltaTime);
|
|
34
|
+
stats.fps = Math.round(p.frameRate());
|
|
35
|
+
raf = requestAnimationFrame(tick);
|
|
36
|
+
};
|
|
37
|
+
raf = requestAnimationFrame(tick);
|
|
38
|
+
return () => cancelAnimationFrame(raf);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const positionStyles: Record<Position, string> = {
|
|
42
|
+
'top-left': 'top: 8px; left: 8px;',
|
|
43
|
+
'top-right': 'top: 8px; right: 8px;',
|
|
44
|
+
'bottom-left': 'bottom: 8px; left: 8px;',
|
|
45
|
+
'bottom-right': 'bottom: 8px; right: 8px;'
|
|
46
|
+
};
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<div class="sketch-debug" style={positionStyles[position]}>
|
|
50
|
+
<div>mouse {stats.mouseX}, {stats.mouseY}</div>
|
|
51
|
+
<div>size {stats.width}×{stats.height}</div>
|
|
52
|
+
<div>frame {stats.frameCount}</div>
|
|
53
|
+
<div>dt {stats.deltaTime}ms · {stats.fps}fps</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<style>
|
|
57
|
+
.sketch-debug {
|
|
58
|
+
position: absolute;
|
|
59
|
+
padding: 6px 10px;
|
|
60
|
+
font:
|
|
61
|
+
10px/1.4 ui-monospace,
|
|
62
|
+
'SF Mono',
|
|
63
|
+
Menlo,
|
|
64
|
+
monospace;
|
|
65
|
+
color: #fff;
|
|
66
|
+
background: rgba(0, 0, 0, 0.7);
|
|
67
|
+
border-radius: 4px;
|
|
68
|
+
pointer-events: none;
|
|
69
|
+
user-select: none;
|
|
70
|
+
z-index: 10;
|
|
71
|
+
}
|
|
72
|
+
.sketch-debug > div {
|
|
73
|
+
margin: 0;
|
|
74
|
+
}
|
|
75
|
+
</style>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type p5 from 'p5';
|
|
2
|
+
type Position = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
3
|
+
interface Props {
|
|
4
|
+
instance: p5 | null;
|
|
5
|
+
position?: Position;
|
|
6
|
+
}
|
|
7
|
+
declare const SketchDebug: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type SketchDebug = ReturnType<typeof SketchDebug>;
|
|
9
|
+
export default SketchDebug;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { default as Sketch } from './Sketch.svelte';
|
|
2
|
+
export { default as FPSMonitor } from './FPSMonitor.svelte';
|
|
3
|
+
export { default as SketchDebug } from './SketchDebug.svelte';
|
|
4
|
+
export { default as DraggableWindow } from './DraggableWindow.svelte';
|
|
5
|
+
export { default as DraggableSketch } from './DraggableSketch.svelte';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { default as Sketch } from './Sketch.svelte';
|
|
2
|
+
export { default as FPSMonitor } from './FPSMonitor.svelte';
|
|
3
|
+
export { default as SketchDebug } from './SketchDebug.svelte';
|
|
4
|
+
export { default as DraggableWindow } from './DraggableWindow.svelte';
|
|
5
|
+
export { default as DraggableSketch } from './DraggableSketch.svelte';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared focus manager for DraggableWindow instances.
|
|
3
|
+
*
|
|
4
|
+
* Each DraggableWindow registers once and receives a reactive `z` value
|
|
5
|
+
* plus a `focus()` method. Calling `focus()` increments a shared counter
|
|
6
|
+
* and reassigns that window's z-index, bringing it to the front relative
|
|
7
|
+
* to every other registered window.
|
|
8
|
+
*
|
|
9
|
+
* Counter is a single `$state` number; z-index values are small integers
|
|
10
|
+
* starting at 100. At 1 click/ms it would take ~285,000 years to
|
|
11
|
+
* overflow `Number.MAX_SAFE_INTEGER`, so no normalization step is needed.
|
|
12
|
+
*/
|
|
13
|
+
export interface WindowHandle {
|
|
14
|
+
readonly z: number;
|
|
15
|
+
focus(): void;
|
|
16
|
+
}
|
|
17
|
+
export declare function registerWindow(): WindowHandle;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared focus manager for DraggableWindow instances.
|
|
3
|
+
*
|
|
4
|
+
* Each DraggableWindow registers once and receives a reactive `z` value
|
|
5
|
+
* plus a `focus()` method. Calling `focus()` increments a shared counter
|
|
6
|
+
* and reassigns that window's z-index, bringing it to the front relative
|
|
7
|
+
* to every other registered window.
|
|
8
|
+
*
|
|
9
|
+
* Counter is a single `$state` number; z-index values are small integers
|
|
10
|
+
* starting at 100. At 1 click/ms it would take ~285,000 years to
|
|
11
|
+
* overflow `Number.MAX_SAFE_INTEGER`, so no normalization step is needed.
|
|
12
|
+
*/
|
|
13
|
+
let counter = $state(100);
|
|
14
|
+
export function registerWindow() {
|
|
15
|
+
let myZ = $state(++counter);
|
|
16
|
+
return {
|
|
17
|
+
get z() {
|
|
18
|
+
return myZ;
|
|
19
|
+
},
|
|
20
|
+
focus() {
|
|
21
|
+
// Only reassign if we're not already the top window — avoids a
|
|
22
|
+
// no-op reactive write when the already-focused window gets clicked.
|
|
23
|
+
if (myZ !== counter)
|
|
24
|
+
myZ = ++counter;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "svelte-p5-components",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Higher-level Svelte 5 components built on svelte-p5: responsive canvas, FPS monitor, sketch debug overlay, draggable windows",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"svelte",
|
|
7
|
+
"svelte5",
|
|
8
|
+
"p5",
|
|
9
|
+
"p5.js",
|
|
10
|
+
"draggable",
|
|
11
|
+
"dashboard",
|
|
12
|
+
"visualization",
|
|
13
|
+
"components"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/edwinzhao/svelte-p5.git",
|
|
18
|
+
"directory": "packages/components"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/edwinzhao/svelte-p5/tree/main/packages/components#readme",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/edwinzhao/svelte-p5/issues"
|
|
23
|
+
},
|
|
24
|
+
"author": "Edwin Zhao",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"type": "module",
|
|
27
|
+
"svelte": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"main": "./dist/index.js",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"svelte": "./dist/index.js",
|
|
34
|
+
"default": "./dist/index.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist",
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE"
|
|
41
|
+
],
|
|
42
|
+
"sideEffects": false,
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "svelte-package -o dist && publint",
|
|
45
|
+
"typecheck": "svelte-check --tsconfig ./tsconfig.json",
|
|
46
|
+
"test": "echo \"no tests yet\" && exit 0"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@neodrag/svelte": "3.0.0-next.8"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"svelte-p5": "workspace:^",
|
|
53
|
+
"p5": ">=1.11.0 <3",
|
|
54
|
+
"svelte": "^5.0.0"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"svelte-p5": "workspace:*",
|
|
58
|
+
"@sveltejs/package": "catalog:",
|
|
59
|
+
"@sveltejs/vite-plugin-svelte": "catalog:",
|
|
60
|
+
"@types/p5": "catalog:",
|
|
61
|
+
"p5": "catalog:",
|
|
62
|
+
"publint": "catalog:",
|
|
63
|
+
"svelte": "catalog:",
|
|
64
|
+
"svelte-check": "catalog:",
|
|
65
|
+
"typescript": "catalog:"
|
|
66
|
+
}
|
|
67
|
+
}
|