next-sketch 0.2.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 +212 -0
- package/dist/CanvasSketch.d.ts +11 -0
- package/dist/CanvasSketch.d.ts.map +1 -0
- package/dist/CanvasSketch.js +16 -0
- package/dist/ThreeSketch.d.ts +11 -0
- package/dist/ThreeSketch.d.ts.map +1 -0
- package/dist/ThreeSketch.js +16 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/types.d.ts +93 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/useCanvasSketch.d.ts +17 -0
- package/dist/useCanvasSketch.d.ts.map +1 -0
- package/dist/useCanvasSketch.js +112 -0
- package/dist/useInViewport.d.ts +19 -0
- package/dist/useInViewport.d.ts.map +1 -0
- package/dist/useInViewport.js +33 -0
- package/dist/useThreeSketch.d.ts +25 -0
- package/dist/useThreeSketch.d.ts.map +1 -0
- package/dist/useThreeSketch.js +209 -0
- package/dist/utils.d.ts +6 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +19 -0
- package/dist/webglBudget.d.ts +25 -0
- package/dist/webglBudget.d.ts.map +1 -0
- package/dist/webglBudget.js +58 -0
- package/package.json +79 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Reinan Br.
|
|
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,212 @@
|
|
|
1
|
+
<div align='center'>
|
|
2
|
+
|
|
3
|
+
<h1>next-sketch</h1>
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/next-sketch)
|
|
6
|
+
[](https://www.npmjs.com/package/next-sketch)
|
|
7
|
+
[](https://github.com/reinanbr/next-sketch/actions/workflows/ci.yml)
|
|
8
|
+
[](https://www.npmjs.com/package/next-sketch)
|
|
9
|
+
[](https://www.npmjs.com/package/next-sketch)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
|
|
12
|
+
<p>p5.js-style canvas sketches for React and Next.js — a headless hook plus a drop-in <code><CanvasSketch /></code> component. DPR-aware resizing, a requestAnimationFrame loop with <code>dt</code>/<code>time</code>/<code>frame</code>, normalized pointer/keyboard input, and full SSR safety, all in one dependency-free package.</p>
|
|
13
|
+
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<hr>
|
|
17
|
+
|
|
18
|
+
## Table of contents
|
|
19
|
+
|
|
20
|
+
- [Why](#why)
|
|
21
|
+
- [Installation](#installation)
|
|
22
|
+
- [Quickstart](#quickstart)
|
|
23
|
+
- [API](#api)
|
|
24
|
+
- [`useCanvasSketch(options)`](#usecanvassketchoptions)
|
|
25
|
+
- [`<CanvasSketch />`](#canvassketch-)
|
|
26
|
+
- [`useThreeSketch(options)`](#usethreesketchoptions)
|
|
27
|
+
- [`<ThreeSketch />`](#threesketch-)
|
|
28
|
+
- [WebGL context budget](#webgl-context-budget)
|
|
29
|
+
- [`useInViewport(ref, options)`](#useinviewportref-options)
|
|
30
|
+
- [Utilities](#utilities)
|
|
31
|
+
- [Next.js / SSR](#nextjs--ssr)
|
|
32
|
+
- [License](#license)
|
|
33
|
+
|
|
34
|
+
<hr>
|
|
35
|
+
|
|
36
|
+
## Why
|
|
37
|
+
|
|
38
|
+
Every hand-rolled canvas animation in React ends up rewriting the same plumbing: a
|
|
39
|
+
`canvasRef`, a `devicePixelRatio`-aware resize handler, a `requestAnimationFrame` loop with
|
|
40
|
+
manual `dt` bookkeeping, pointer coordinates translated through `getBoundingClientRect()`, and
|
|
41
|
+
cleanup on unmount. `next-sketch` extracts that plumbing once so you can focus on `setup`/`draw`,
|
|
42
|
+
the way you would in [p5.js](https://p5js.org) — but as an idiomatic React hook/component instead
|
|
43
|
+
of a global-mode sketch.
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
npm install next-sketch
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Requires React 18+. Works in any React app; the SSR-safety and `next/dynamic` friendliness make
|
|
52
|
+
it a natural fit for Next.js specifically.
|
|
53
|
+
|
|
54
|
+
## Quickstart
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
import { CanvasSketch, randRange } from 'next-sketch';
|
|
58
|
+
|
|
59
|
+
export default function Sketch() {
|
|
60
|
+
return (
|
|
61
|
+
<CanvasSketch
|
|
62
|
+
style={{ width: '100%', height: 400 }}
|
|
63
|
+
setup={({ ctx, width, height }) => {
|
|
64
|
+
// called once on mount, and again on every resize
|
|
65
|
+
}}
|
|
66
|
+
draw={({ ctx, width, height, dt, time, frame }) => {
|
|
67
|
+
ctx.fillStyle = '#0f172a';
|
|
68
|
+
ctx.fillRect(0, 0, width, height);
|
|
69
|
+
ctx.fillStyle = '#6366f1';
|
|
70
|
+
ctx.beginPath();
|
|
71
|
+
ctx.arc(width / 2 + Math.sin(time) * 100, height / 2, 20, 0, Math.PI * 2);
|
|
72
|
+
ctx.fill();
|
|
73
|
+
}}
|
|
74
|
+
/>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
See [`examples/bouncing-particles.tsx`](examples/bouncing-particles.tsx) for a fuller example
|
|
80
|
+
with particles, input, and a play/pause control.
|
|
81
|
+
|
|
82
|
+
## API
|
|
83
|
+
|
|
84
|
+
### `useCanvasSketch(options)`
|
|
85
|
+
|
|
86
|
+
The headless engine. Returns a `canvasRef` to attach to your own `<canvas>`, plus imperative
|
|
87
|
+
controls:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
const { canvasRef, start, stop, reset, isRunning, running } = useCanvasSketch({
|
|
91
|
+
setup?: (info: SetupInfo) => void,
|
|
92
|
+
draw?: (info: DrawInfo) => void,
|
|
93
|
+
onPointerDown?: (info: PointerInfo) => void,
|
|
94
|
+
onPointerMove?: (info: PointerInfo) => void,
|
|
95
|
+
onPointerUp?: (info: PointerInfo) => void,
|
|
96
|
+
onKeyDown?: (event: KeyboardEvent) => void,
|
|
97
|
+
onKeyUp?: (event: KeyboardEvent) => void,
|
|
98
|
+
autoStart?: boolean, // default true
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
- `SetupInfo` — `{ ctx, canvas, width, height }` (`width`/`height` in CSS pixels, already
|
|
103
|
+
DPR-normalized — draw as if `devicePixelRatio` were 1).
|
|
104
|
+
- `DrawInfo` — `SetupInfo` plus `{ dt, time, frame }`: seconds since the last frame, seconds
|
|
105
|
+
since `start()`, and a frame counter. Both reset to zero on `reset()`.
|
|
106
|
+
- `PointerInfo` — `{ x, y, event }`, `x`/`y` already localized to the canvas in CSS pixels.
|
|
107
|
+
|
|
108
|
+
### `<CanvasSketch />`
|
|
109
|
+
|
|
110
|
+
A thin `<canvas>` wrapper around the hook. Accepts the same options as props, plus `className`/
|
|
111
|
+
`style`. Pass a `ref` to get a `CanvasSketchHandle` (`start`/`stop`/`reset`/`isRunning`) for
|
|
112
|
+
building play/pause/reset UI without touching the hook directly.
|
|
113
|
+
|
|
114
|
+
### `useThreeSketch(options)`
|
|
115
|
+
|
|
116
|
+
The same engine as `useCanvasSketch`, but backed by [Three.js](https://threejs.org) instead of a
|
|
117
|
+
2D context. `setup`/`draw` get `{ scene, camera, renderer, canvas, width, height }` (plus
|
|
118
|
+
`dt`/`time`/`frame` in `draw`) instead of `ctx`; `renderer.render(scene, camera)` happens for you
|
|
119
|
+
right after `draw` runs. Requires `three` as a peer dependency (`npm install three`) — it's not
|
|
120
|
+
bundled, so sketches that only use the 2D engine pay nothing for it.
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
import { ThreeSketch } from 'next-sketch';
|
|
124
|
+
import * as THREE from 'three';
|
|
125
|
+
|
|
126
|
+
<ThreeSketch
|
|
127
|
+
style={{ width: '100%', height: 400 }}
|
|
128
|
+
setup={({ scene }) => {
|
|
129
|
+
scene.add(new THREE.AmbientLight(0xffffff, 0.6));
|
|
130
|
+
const mesh = new THREE.Mesh(
|
|
131
|
+
new THREE.IcosahedronGeometry(1.5, 1),
|
|
132
|
+
new THREE.MeshStandardMaterial({ color: '#6366f1' }),
|
|
133
|
+
);
|
|
134
|
+
scene.add(mesh);
|
|
135
|
+
}}
|
|
136
|
+
draw={({ scene, dt }) => {
|
|
137
|
+
scene.children[1].rotation.y += dt;
|
|
138
|
+
}}
|
|
139
|
+
/>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Unlike 2D canvas contexts, WebGL contexts are a scarce per-process browser resource — see
|
|
143
|
+
[WebGL context budget](#webgl-context-budget) below, which `useThreeSketch` respects by default.
|
|
144
|
+
|
|
145
|
+
On unmount, `useThreeSketch` disposes every geometry/material in the scene graph and calls
|
|
146
|
+
`renderer.dispose()` + `renderer.forceContextLoss()`, so the context is actually freed rather than
|
|
147
|
+
left for the garbage collector to get to eventually.
|
|
148
|
+
|
|
149
|
+
### `<ThreeSketch />`
|
|
150
|
+
|
|
151
|
+
A thin `<canvas>` wrapper around `useThreeSketch`, mirroring `<CanvasSketch />`: same
|
|
152
|
+
`ref`-based `start`/`stop`/`reset`/`isRunning` handle, plus `className`/`style`.
|
|
153
|
+
|
|
154
|
+
### WebGL context budget
|
|
155
|
+
|
|
156
|
+
A page rendering several live 3D sketches at once (e.g. a gallery of animated previews) can hit
|
|
157
|
+
the browser's per-process WebGL context limit — Chrome allows around 16, Safari's ceiling is
|
|
158
|
+
often much lower. Past that limit, browsers silently evict the *oldest* context with no error;
|
|
159
|
+
the canvas just goes blank. `useThreeSketch` guards against this automatically: each instance
|
|
160
|
+
waits for a slot in a shared, module-level budget (default: 4 concurrent) before creating its
|
|
161
|
+
renderer, and releases its slot on unmount.
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import { configureWebglBudget, getWebglBudget } from 'next-sketch';
|
|
165
|
+
|
|
166
|
+
configureWebglBudget(6); // raise/lower the app-wide cap
|
|
167
|
+
getWebglBudget(); // -> { active: 2, max: 6 }
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Opt a specific sketch out with `respectBudget: false` (e.g. for the one full, interactive
|
|
171
|
+
simulation on a detail page, as opposed to a grid of decorative previews).
|
|
172
|
+
|
|
173
|
+
### `useInViewport(ref, options)`
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
177
|
+
const inView = useInViewport(wrapperRef, { rootMargin: '200px', once: false });
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Tracks whether an element is inside the viewport via `IntersectionObserver`, so you can mount an
|
|
181
|
+
expensive sketch only while it's actually visible. With `once: true` (the default-ish choice for
|
|
182
|
+
cheap 2D sketches) it stays `true` forever after the first intersection — no restart cost on
|
|
183
|
+
scroll. With `once: false` it flips back to `false` when the element scrolls back out, which is
|
|
184
|
+
what lets a `<ThreeSketch />` actually unmount and free its WebGL context budget slot in a long
|
|
185
|
+
gallery page instead of accumulating contexts forever.
|
|
186
|
+
|
|
187
|
+
### Utilities
|
|
188
|
+
|
|
189
|
+
Small helpers factored out of the same duplicated math every canvas sketch ends up writing:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
import { clamp, randRange, lerpColor } from 'next-sketch';
|
|
193
|
+
|
|
194
|
+
clamp(value, min, max); // -> number
|
|
195
|
+
randRange(min, max); // -> number
|
|
196
|
+
lerpColor([59,130,246], [239,68,68], t); // -> "rgb(r,g,b)"
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Next.js / SSR
|
|
200
|
+
|
|
201
|
+
`useCanvasSketch`/`CanvasSketch` never touch `window`/`document` outside of effects, so they're
|
|
202
|
+
safe to import in a Server Component tree. If you still prefer to opt a sketch fully out of SSR
|
|
203
|
+
(e.g. it depends on `window` inside your own `draw` callback), wrap it the usual way:
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
import dynamic from 'next/dynamic';
|
|
207
|
+
const Sketch = dynamic(() => import('./Sketch'), { ssr: false });
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## License
|
|
211
|
+
|
|
212
|
+
MIT © [Reinan Br.](LICENSE)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CanvasSketchHandle, CanvasSketchOptions } from './types';
|
|
2
|
+
export interface CanvasSketchProps extends CanvasSketchOptions {
|
|
3
|
+
className?: string;
|
|
4
|
+
style?: React.CSSProperties;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Drop-in `<canvas>` wired to useCanvasSketch. Pass `ref` to get imperative
|
|
8
|
+
* start()/stop()/reset()/isRunning() controls for building play/pause UI.
|
|
9
|
+
*/
|
|
10
|
+
export declare const CanvasSketch: import("react").ForwardRefExoticComponent<CanvasSketchProps & import("react").RefAttributes<CanvasSketchHandle>>;
|
|
11
|
+
//# sourceMappingURL=CanvasSketch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CanvasSketch.d.ts","sourceRoot":"","sources":["../src/CanvasSketch.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAEvE,MAAM,WAAW,iBAAkB,SAAQ,mBAAmB;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;CAC7B;AAED;;;GAGG;AACH,eAAO,MAAM,YAAY,kHAQxB,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
'use client';
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.CanvasSketch = void 0;
|
|
5
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
|
+
const react_1 = require("react");
|
|
7
|
+
const useCanvasSketch_1 = require("./useCanvasSketch");
|
|
8
|
+
/**
|
|
9
|
+
* Drop-in `<canvas>` wired to useCanvasSketch. Pass `ref` to get imperative
|
|
10
|
+
* start()/stop()/reset()/isRunning() controls for building play/pause UI.
|
|
11
|
+
*/
|
|
12
|
+
exports.CanvasSketch = (0, react_1.forwardRef)(function CanvasSketch({ className, style, ...options }, ref) {
|
|
13
|
+
const { canvasRef, start, stop, reset, isRunning } = (0, useCanvasSketch_1.useCanvasSketch)(options);
|
|
14
|
+
(0, react_1.useImperativeHandle)(ref, () => ({ start, stop, reset, isRunning }), [start, stop, reset, isRunning]);
|
|
15
|
+
return (0, jsx_runtime_1.jsx)("canvas", { ref: canvasRef, className: className, style: style });
|
|
16
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ThreeSketchHandle, ThreeSketchOptions } from './types';
|
|
2
|
+
export interface ThreeSketchProps extends ThreeSketchOptions {
|
|
3
|
+
className?: string;
|
|
4
|
+
style?: React.CSSProperties;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Drop-in `<canvas>` wired to useThreeSketch. Pass `ref` to get imperative
|
|
8
|
+
* start()/stop()/reset()/isRunning() controls for building play/pause UI.
|
|
9
|
+
*/
|
|
10
|
+
export declare const ThreeSketch: import("react").ForwardRefExoticComponent<ThreeSketchProps & import("react").RefAttributes<ThreeSketchHandle>>;
|
|
11
|
+
//# sourceMappingURL=ThreeSketch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ThreeSketch.d.ts","sourceRoot":"","sources":["../src/ThreeSketch.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAErE,MAAM,WAAW,gBAAiB,SAAQ,kBAAkB;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;CAC7B;AAED;;;GAGG;AACH,eAAO,MAAM,WAAW,gHAQvB,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
'use client';
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.ThreeSketch = void 0;
|
|
5
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
|
+
const react_1 = require("react");
|
|
7
|
+
const useThreeSketch_1 = require("./useThreeSketch");
|
|
8
|
+
/**
|
|
9
|
+
* Drop-in `<canvas>` wired to useThreeSketch. Pass `ref` to get imperative
|
|
10
|
+
* start()/stop()/reset()/isRunning() controls for building play/pause UI.
|
|
11
|
+
*/
|
|
12
|
+
exports.ThreeSketch = (0, react_1.forwardRef)(function ThreeSketch({ className, style, ...options }, ref) {
|
|
13
|
+
const { canvasRef, start, stop, reset, isRunning } = (0, useThreeSketch_1.useThreeSketch)(options);
|
|
14
|
+
(0, react_1.useImperativeHandle)(ref, () => ({ start, stop, reset, isRunning }), [start, stop, reset, isRunning]);
|
|
15
|
+
return (0, jsx_runtime_1.jsx)("canvas", { ref: canvasRef, className: className, style: style });
|
|
16
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { useCanvasSketch } from './useCanvasSketch';
|
|
2
|
+
export type { UseCanvasSketchResult } from './useCanvasSketch';
|
|
3
|
+
export { CanvasSketch } from './CanvasSketch';
|
|
4
|
+
export type { CanvasSketchProps } from './CanvasSketch';
|
|
5
|
+
export { useThreeSketch } from './useThreeSketch';
|
|
6
|
+
export type { UseThreeSketchResult } from './useThreeSketch';
|
|
7
|
+
export { ThreeSketch } from './ThreeSketch';
|
|
8
|
+
export type { ThreeSketchProps } from './ThreeSketch';
|
|
9
|
+
export { useInViewport } from './useInViewport';
|
|
10
|
+
export type { UseInViewportOptions } from './useInViewport';
|
|
11
|
+
export { configureWebglBudget, getWebglBudget } from './webglBudget';
|
|
12
|
+
export { clamp, randRange, lerpColor } from './utils';
|
|
13
|
+
export type { RGB } from './utils';
|
|
14
|
+
export type { SetupInfo, DrawInfo, PointerInfo, CanvasSketchOptions, CanvasSketchHandle, ThreeSetupInfo, ThreeDrawInfo, ThreeSketchOptions, ThreeSketchHandle, } from './types';
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,YAAY,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACrE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACtD,YAAY,EAAE,GAAG,EAAE,MAAM,SAAS,CAAC;AACnC,YAAY,EACV,SAAS,EACT,QAAQ,EACR,WAAW,EACX,mBAAmB,EACnB,kBAAkB,EAClB,cAAc,EACd,aAAa,EACb,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,SAAS,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.lerpColor = exports.randRange = exports.clamp = exports.getWebglBudget = exports.configureWebglBudget = exports.useInViewport = exports.ThreeSketch = exports.useThreeSketch = exports.CanvasSketch = exports.useCanvasSketch = void 0;
|
|
4
|
+
var useCanvasSketch_1 = require("./useCanvasSketch");
|
|
5
|
+
Object.defineProperty(exports, "useCanvasSketch", { enumerable: true, get: function () { return useCanvasSketch_1.useCanvasSketch; } });
|
|
6
|
+
var CanvasSketch_1 = require("./CanvasSketch");
|
|
7
|
+
Object.defineProperty(exports, "CanvasSketch", { enumerable: true, get: function () { return CanvasSketch_1.CanvasSketch; } });
|
|
8
|
+
var useThreeSketch_1 = require("./useThreeSketch");
|
|
9
|
+
Object.defineProperty(exports, "useThreeSketch", { enumerable: true, get: function () { return useThreeSketch_1.useThreeSketch; } });
|
|
10
|
+
var ThreeSketch_1 = require("./ThreeSketch");
|
|
11
|
+
Object.defineProperty(exports, "ThreeSketch", { enumerable: true, get: function () { return ThreeSketch_1.ThreeSketch; } });
|
|
12
|
+
var useInViewport_1 = require("./useInViewport");
|
|
13
|
+
Object.defineProperty(exports, "useInViewport", { enumerable: true, get: function () { return useInViewport_1.useInViewport; } });
|
|
14
|
+
var webglBudget_1 = require("./webglBudget");
|
|
15
|
+
Object.defineProperty(exports, "configureWebglBudget", { enumerable: true, get: function () { return webglBudget_1.configureWebglBudget; } });
|
|
16
|
+
Object.defineProperty(exports, "getWebglBudget", { enumerable: true, get: function () { return webglBudget_1.getWebglBudget; } });
|
|
17
|
+
var utils_1 = require("./utils");
|
|
18
|
+
Object.defineProperty(exports, "clamp", { enumerable: true, get: function () { return utils_1.clamp; } });
|
|
19
|
+
Object.defineProperty(exports, "randRange", { enumerable: true, get: function () { return utils_1.randRange; } });
|
|
20
|
+
Object.defineProperty(exports, "lerpColor", { enumerable: true, get: function () { return utils_1.lerpColor; } });
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export interface SetupInfo {
|
|
2
|
+
ctx: CanvasRenderingContext2D;
|
|
3
|
+
canvas: HTMLCanvasElement;
|
|
4
|
+
/** CSS pixels — already DPR-normalized, matches what you'd draw with. */
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
}
|
|
8
|
+
export interface DrawInfo extends SetupInfo {
|
|
9
|
+
/** Seconds elapsed since the previous frame. */
|
|
10
|
+
dt: number;
|
|
11
|
+
/** Seconds elapsed since start() was called (resets on reset()). */
|
|
12
|
+
time: number;
|
|
13
|
+
/** Frame counter, resets on reset(). */
|
|
14
|
+
frame: number;
|
|
15
|
+
}
|
|
16
|
+
export interface PointerInfo {
|
|
17
|
+
/** Pointer position in CSS pixels, local to the canvas. */
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
event: PointerEvent;
|
|
21
|
+
}
|
|
22
|
+
export interface CanvasSketchOptions {
|
|
23
|
+
/** Called once after the canvas is mounted/sized, and again after every resize. */
|
|
24
|
+
setup?: (info: SetupInfo) => void;
|
|
25
|
+
/** Called every animation frame while running. */
|
|
26
|
+
draw?: (info: DrawInfo) => void;
|
|
27
|
+
onPointerDown?: (info: PointerInfo) => void;
|
|
28
|
+
onPointerMove?: (info: PointerInfo) => void;
|
|
29
|
+
onPointerUp?: (info: PointerInfo) => void;
|
|
30
|
+
onKeyDown?: (event: KeyboardEvent) => void;
|
|
31
|
+
onKeyUp?: (event: KeyboardEvent) => void;
|
|
32
|
+
/** Start the render loop automatically on mount. Default: true. */
|
|
33
|
+
autoStart?: boolean;
|
|
34
|
+
}
|
|
35
|
+
export interface CanvasSketchHandle {
|
|
36
|
+
start: () => void;
|
|
37
|
+
stop: () => void;
|
|
38
|
+
/** Stops the loop and resets time/frame back to zero (does not clear the canvas). */
|
|
39
|
+
reset: () => void;
|
|
40
|
+
isRunning: () => boolean;
|
|
41
|
+
}
|
|
42
|
+
export interface ThreeSetupInfo {
|
|
43
|
+
scene: import('three').Scene;
|
|
44
|
+
camera: import('three').PerspectiveCamera;
|
|
45
|
+
renderer: import('three').WebGLRenderer;
|
|
46
|
+
canvas: HTMLCanvasElement;
|
|
47
|
+
/** CSS pixels. */
|
|
48
|
+
width: number;
|
|
49
|
+
height: number;
|
|
50
|
+
}
|
|
51
|
+
export interface ThreeDrawInfo extends ThreeSetupInfo {
|
|
52
|
+
/** Seconds elapsed since the previous frame. */
|
|
53
|
+
dt: number;
|
|
54
|
+
/** Seconds elapsed since start() was called (resets on reset()). */
|
|
55
|
+
time: number;
|
|
56
|
+
/** Frame counter, resets on reset(). */
|
|
57
|
+
frame: number;
|
|
58
|
+
}
|
|
59
|
+
export interface ThreeSketchOptions {
|
|
60
|
+
/** Called once after scene/camera/renderer are created and sized, and again after every resize. */
|
|
61
|
+
setup?: (info: ThreeSetupInfo) => void;
|
|
62
|
+
/** Called every animation frame while running, right before renderer.render(scene, camera). */
|
|
63
|
+
draw?: (info: ThreeDrawInfo) => void;
|
|
64
|
+
onPointerDown?: (info: PointerInfo) => void;
|
|
65
|
+
onPointerMove?: (info: PointerInfo) => void;
|
|
66
|
+
onPointerUp?: (info: PointerInfo) => void;
|
|
67
|
+
onKeyDown?: (event: KeyboardEvent) => void;
|
|
68
|
+
onKeyUp?: (event: KeyboardEvent) => void;
|
|
69
|
+
/** Start the render loop automatically on mount. Default: true. */
|
|
70
|
+
autoStart?: boolean;
|
|
71
|
+
/** Default PerspectiveCamera field of view, in degrees. Default: 50. */
|
|
72
|
+
fov?: number;
|
|
73
|
+
near?: number;
|
|
74
|
+
far?: number;
|
|
75
|
+
/** Forwarded to `new THREE.WebGLRenderer(...)`. `canvas` is always set for you. */
|
|
76
|
+
rendererOptions?: Omit<import('three').WebGLRendererParameters, 'canvas'>;
|
|
77
|
+
/**
|
|
78
|
+
* Wait for a slot in the global WebGL context budget (see
|
|
79
|
+
* `configureWebglBudget`) before creating the renderer. Default: true.
|
|
80
|
+
* Browsers silently evict the oldest WebGL context once a per-process
|
|
81
|
+
* limit is hit (Safari's ceiling is notably lower than Chrome's), so a
|
|
82
|
+
* page rendering several live 3D previews at once needs this to avoid
|
|
83
|
+
* contexts going blank with no error.
|
|
84
|
+
*/
|
|
85
|
+
respectBudget?: boolean;
|
|
86
|
+
}
|
|
87
|
+
export interface ThreeSketchHandle {
|
|
88
|
+
start: () => void;
|
|
89
|
+
stop: () => void;
|
|
90
|
+
reset: () => void;
|
|
91
|
+
isRunning: () => boolean;
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { CanvasSketchOptions } from './types';
|
|
2
|
+
export interface UseCanvasSketchResult {
|
|
3
|
+
canvasRef: React.RefObject<HTMLCanvasElement | null>;
|
|
4
|
+
start: () => void;
|
|
5
|
+
stop: () => void;
|
|
6
|
+
reset: () => void;
|
|
7
|
+
isRunning: () => boolean;
|
|
8
|
+
/** Re-renders when start()/stop() are called — handy for a play/pause button label. */
|
|
9
|
+
running: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Headless canvas render-loop engine: DPR-aware resize, requestAnimationFrame loop with
|
|
13
|
+
* dt/time/frame, and pointer/keyboard input normalized to canvas-local CSS-pixel coordinates.
|
|
14
|
+
* All DOM/window access happens inside effects, so it's safe to use during SSR.
|
|
15
|
+
*/
|
|
16
|
+
export declare function useCanvasSketch(options?: CanvasSketchOptions): UseCanvasSketchResult;
|
|
17
|
+
//# sourceMappingURL=useCanvasSketch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useCanvasSketch.d.ts","sourceRoot":"","sources":["../src/useCanvasSketch.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAe,MAAM,SAAS,CAAC;AAEhE,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;IACrD,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,SAAS,EAAE,MAAM,OAAO,CAAC;IACzB,uFAAuF;IACvF,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,GAAE,mBAAwB,GAAG,qBAAqB,CAkHxF"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
'use client';
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.useCanvasSketch = useCanvasSketch;
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
/**
|
|
7
|
+
* Headless canvas render-loop engine: DPR-aware resize, requestAnimationFrame loop with
|
|
8
|
+
* dt/time/frame, and pointer/keyboard input normalized to canvas-local CSS-pixel coordinates.
|
|
9
|
+
* All DOM/window access happens inside effects, so it's safe to use during SSR.
|
|
10
|
+
*/
|
|
11
|
+
function useCanvasSketch(options = {}) {
|
|
12
|
+
const canvasRef = (0, react_1.useRef)(null);
|
|
13
|
+
const ctxRef = (0, react_1.useRef)(null);
|
|
14
|
+
const optionsRef = (0, react_1.useRef)(options);
|
|
15
|
+
optionsRef.current = options;
|
|
16
|
+
const rafRef = (0, react_1.useRef)(0);
|
|
17
|
+
const runningRef = (0, react_1.useRef)(false);
|
|
18
|
+
const lastTsRef = (0, react_1.useRef)(null);
|
|
19
|
+
const timeRef = (0, react_1.useRef)(0);
|
|
20
|
+
const frameRef = (0, react_1.useRef)(0);
|
|
21
|
+
const [running, setRunning] = (0, react_1.useState)(false);
|
|
22
|
+
const loop = (0, react_1.useCallback)((ts) => {
|
|
23
|
+
const canvas = canvasRef.current;
|
|
24
|
+
const ctx = ctxRef.current;
|
|
25
|
+
if (!canvas || !ctx)
|
|
26
|
+
return;
|
|
27
|
+
if (lastTsRef.current === null)
|
|
28
|
+
lastTsRef.current = ts;
|
|
29
|
+
const dt = (ts - lastTsRef.current) / 1000;
|
|
30
|
+
lastTsRef.current = ts;
|
|
31
|
+
timeRef.current += dt;
|
|
32
|
+
frameRef.current += 1;
|
|
33
|
+
const dpr = window.devicePixelRatio || 1;
|
|
34
|
+
const width = canvas.width / dpr;
|
|
35
|
+
const height = canvas.height / dpr;
|
|
36
|
+
optionsRef.current.draw?.({
|
|
37
|
+
ctx, canvas, width, height,
|
|
38
|
+
dt, time: timeRef.current, frame: frameRef.current,
|
|
39
|
+
});
|
|
40
|
+
rafRef.current = requestAnimationFrame(loop);
|
|
41
|
+
}, []);
|
|
42
|
+
const start = (0, react_1.useCallback)(() => {
|
|
43
|
+
if (runningRef.current)
|
|
44
|
+
return;
|
|
45
|
+
runningRef.current = true;
|
|
46
|
+
setRunning(true);
|
|
47
|
+
lastTsRef.current = null;
|
|
48
|
+
rafRef.current = requestAnimationFrame(loop);
|
|
49
|
+
}, [loop]);
|
|
50
|
+
const stop = (0, react_1.useCallback)(() => {
|
|
51
|
+
runningRef.current = false;
|
|
52
|
+
setRunning(false);
|
|
53
|
+
cancelAnimationFrame(rafRef.current);
|
|
54
|
+
}, []);
|
|
55
|
+
const reset = (0, react_1.useCallback)(() => {
|
|
56
|
+
stop();
|
|
57
|
+
timeRef.current = 0;
|
|
58
|
+
frameRef.current = 0;
|
|
59
|
+
lastTsRef.current = null;
|
|
60
|
+
}, [stop]);
|
|
61
|
+
const isRunning = (0, react_1.useCallback)(() => runningRef.current, []);
|
|
62
|
+
(0, react_1.useEffect)(() => {
|
|
63
|
+
const canvas = canvasRef.current;
|
|
64
|
+
if (!canvas)
|
|
65
|
+
return;
|
|
66
|
+
const ctx = canvas.getContext('2d');
|
|
67
|
+
if (!ctx)
|
|
68
|
+
return;
|
|
69
|
+
ctxRef.current = ctx;
|
|
70
|
+
const resize = () => {
|
|
71
|
+
const rect = canvas.getBoundingClientRect();
|
|
72
|
+
const dpr = window.devicePixelRatio || 1;
|
|
73
|
+
const width = Math.max(1, Math.round(rect.width));
|
|
74
|
+
const height = Math.max(1, Math.round(rect.height));
|
|
75
|
+
canvas.width = width * dpr;
|
|
76
|
+
canvas.height = height * dpr;
|
|
77
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
78
|
+
optionsRef.current.setup?.({ ctx, canvas, width, height });
|
|
79
|
+
};
|
|
80
|
+
resize();
|
|
81
|
+
const ro = new ResizeObserver(resize);
|
|
82
|
+
ro.observe(canvas);
|
|
83
|
+
const toLocal = (e) => {
|
|
84
|
+
const rect = canvas.getBoundingClientRect();
|
|
85
|
+
return { x: e.clientX - rect.left, y: e.clientY - rect.top, event: e };
|
|
86
|
+
};
|
|
87
|
+
const onPointerDown = (e) => optionsRef.current.onPointerDown?.(toLocal(e));
|
|
88
|
+
const onPointerMove = (e) => optionsRef.current.onPointerMove?.(toLocal(e));
|
|
89
|
+
const onPointerUp = (e) => optionsRef.current.onPointerUp?.(toLocal(e));
|
|
90
|
+
canvas.addEventListener('pointerdown', onPointerDown);
|
|
91
|
+
canvas.addEventListener('pointermove', onPointerMove);
|
|
92
|
+
canvas.addEventListener('pointerup', onPointerUp);
|
|
93
|
+
const onKeyDown = (e) => optionsRef.current.onKeyDown?.(e);
|
|
94
|
+
const onKeyUp = (e) => optionsRef.current.onKeyUp?.(e);
|
|
95
|
+
window.addEventListener('keydown', onKeyDown);
|
|
96
|
+
window.addEventListener('keyup', onKeyUp);
|
|
97
|
+
if (optionsRef.current.autoStart ?? true)
|
|
98
|
+
start();
|
|
99
|
+
return () => {
|
|
100
|
+
ro.disconnect();
|
|
101
|
+
canvas.removeEventListener('pointerdown', onPointerDown);
|
|
102
|
+
canvas.removeEventListener('pointermove', onPointerMove);
|
|
103
|
+
canvas.removeEventListener('pointerup', onPointerUp);
|
|
104
|
+
window.removeEventListener('keydown', onKeyDown);
|
|
105
|
+
window.removeEventListener('keyup', onKeyUp);
|
|
106
|
+
cancelAnimationFrame(rafRef.current);
|
|
107
|
+
runningRef.current = false;
|
|
108
|
+
};
|
|
109
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
110
|
+
}, []);
|
|
111
|
+
return { canvasRef, start, stop, reset, isRunning, running };
|
|
112
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface UseInViewportOptions {
|
|
2
|
+
/** Forwarded to IntersectionObserver. Default: '200px'. */
|
|
3
|
+
rootMargin?: string;
|
|
4
|
+
/**
|
|
5
|
+
* Once true, stays true forever after the first intersection (old
|
|
6
|
+
* "mount once" behavior — cheap sketches that shouldn't restart on
|
|
7
|
+
* every scroll in/out). Once false, tracks intersection continuously,
|
|
8
|
+
* which is what lets a WebGL sketch actually unmount and free its
|
|
9
|
+
* context budget slot when scrolled away. Default: false.
|
|
10
|
+
*/
|
|
11
|
+
once?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Tracks whether `ref`'s element is inside the viewport (+ rootMargin).
|
|
15
|
+
* Used to lazily mount expensive sketches (canvas/WebGL) only while
|
|
16
|
+
* they're actually visible.
|
|
17
|
+
*/
|
|
18
|
+
export declare function useInViewport(ref: React.RefObject<Element | null>, { rootMargin, once }?: UseInViewportOptions): boolean;
|
|
19
|
+
//# sourceMappingURL=useInViewport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useInViewport.d.ts","sourceRoot":"","sources":["../src/useInViewport.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,oBAAoB;IACnC,2DAA2D;IAC3D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE,KAAK,CAAC,SAAS,CAAC,OAAO,GAAG,IAAI,CAAC,EACpC,EAAE,UAAoB,EAAE,IAAY,EAAE,GAAE,oBAAyB,GAChE,OAAO,CAyBT"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
'use client';
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.useInViewport = useInViewport;
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
/**
|
|
7
|
+
* Tracks whether `ref`'s element is inside the viewport (+ rootMargin).
|
|
8
|
+
* Used to lazily mount expensive sketches (canvas/WebGL) only while
|
|
9
|
+
* they're actually visible.
|
|
10
|
+
*/
|
|
11
|
+
function useInViewport(ref, { rootMargin = '200px', once = false } = {}) {
|
|
12
|
+
const [inView, setInView] = (0, react_1.useState)(false);
|
|
13
|
+
(0, react_1.useEffect)(() => {
|
|
14
|
+
const el = ref.current;
|
|
15
|
+
if (!el)
|
|
16
|
+
return undefined;
|
|
17
|
+
const obs = new IntersectionObserver((entries) => {
|
|
18
|
+
const isIntersecting = !!entries[0]?.isIntersecting;
|
|
19
|
+
if (isIntersecting) {
|
|
20
|
+
setInView(true);
|
|
21
|
+
if (once)
|
|
22
|
+
obs.disconnect();
|
|
23
|
+
}
|
|
24
|
+
else if (!once) {
|
|
25
|
+
setInView(false);
|
|
26
|
+
}
|
|
27
|
+
}, { rootMargin });
|
|
28
|
+
obs.observe(el);
|
|
29
|
+
return () => obs.disconnect();
|
|
30
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
31
|
+
}, [ref.current, rootMargin, once]);
|
|
32
|
+
return inView;
|
|
33
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ThreeSketchOptions } from './types';
|
|
2
|
+
export interface UseThreeSketchResult {
|
|
3
|
+
canvasRef: React.RefObject<HTMLCanvasElement | null>;
|
|
4
|
+
start: () => void;
|
|
5
|
+
stop: () => void;
|
|
6
|
+
reset: () => void;
|
|
7
|
+
isRunning: () => boolean;
|
|
8
|
+
/** Re-renders when start()/stop() are called — handy for a play/pause button label. */
|
|
9
|
+
running: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Headless Three.js render-loop engine: same shape as useCanvasSketch
|
|
13
|
+
* (DPR-aware resize, requestAnimationFrame loop with dt/time/frame,
|
|
14
|
+
* normalized pointer/keyboard input), but hands your setup/draw callbacks
|
|
15
|
+
* a `scene`/`camera`/`renderer` instead of a 2D `ctx`.
|
|
16
|
+
*
|
|
17
|
+
* Unlike useCanvasSketch, WebGL contexts are a scarce per-process browser
|
|
18
|
+
* resource, so this hook (a) waits for a slot in the shared context budget
|
|
19
|
+
* before creating the renderer (see `configureWebglBudget`), and (b) fully
|
|
20
|
+
* disposes the scene graph and forces context loss on unmount, so a page
|
|
21
|
+
* that mounts/unmounts many of these (e.g. a gallery of live previews)
|
|
22
|
+
* doesn't leak contexts.
|
|
23
|
+
*/
|
|
24
|
+
export declare function useThreeSketch(options?: ThreeSketchOptions): UseThreeSketchResult;
|
|
25
|
+
//# sourceMappingURL=useThreeSketch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useThreeSketch.d.ts","sourceRoot":"","sources":["../src/useThreeSketch.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAe,MAAM,SAAS,CAAC;AAG/D,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;IACrD,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,SAAS,EAAE,MAAM,OAAO,CAAC;IACzB,uFAAuF;IACvF,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,OAAO,GAAE,kBAAuB,GAAG,oBAAoB,CAgLrF"}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
'use client';
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.useThreeSketch = useThreeSketch;
|
|
38
|
+
const react_1 = require("react");
|
|
39
|
+
const THREE = __importStar(require("three"));
|
|
40
|
+
const webglBudget_1 = require("./webglBudget");
|
|
41
|
+
/**
|
|
42
|
+
* Headless Three.js render-loop engine: same shape as useCanvasSketch
|
|
43
|
+
* (DPR-aware resize, requestAnimationFrame loop with dt/time/frame,
|
|
44
|
+
* normalized pointer/keyboard input), but hands your setup/draw callbacks
|
|
45
|
+
* a `scene`/`camera`/`renderer` instead of a 2D `ctx`.
|
|
46
|
+
*
|
|
47
|
+
* Unlike useCanvasSketch, WebGL contexts are a scarce per-process browser
|
|
48
|
+
* resource, so this hook (a) waits for a slot in the shared context budget
|
|
49
|
+
* before creating the renderer (see `configureWebglBudget`), and (b) fully
|
|
50
|
+
* disposes the scene graph and forces context loss on unmount, so a page
|
|
51
|
+
* that mounts/unmounts many of these (e.g. a gallery of live previews)
|
|
52
|
+
* doesn't leak contexts.
|
|
53
|
+
*/
|
|
54
|
+
function useThreeSketch(options = {}) {
|
|
55
|
+
const canvasRef = (0, react_1.useRef)(null);
|
|
56
|
+
const optionsRef = (0, react_1.useRef)(options);
|
|
57
|
+
optionsRef.current = options;
|
|
58
|
+
const sceneRef = (0, react_1.useRef)(null);
|
|
59
|
+
const cameraRef = (0, react_1.useRef)(null);
|
|
60
|
+
const rendererRef = (0, react_1.useRef)(null);
|
|
61
|
+
const rafRef = (0, react_1.useRef)(0);
|
|
62
|
+
const runningRef = (0, react_1.useRef)(false);
|
|
63
|
+
const lastTsRef = (0, react_1.useRef)(null);
|
|
64
|
+
const timeRef = (0, react_1.useRef)(0);
|
|
65
|
+
const frameRef = (0, react_1.useRef)(0);
|
|
66
|
+
const [running, setRunning] = (0, react_1.useState)(false);
|
|
67
|
+
const [hasSlot, setHasSlot] = (0, react_1.useState)(false);
|
|
68
|
+
const loop = (0, react_1.useCallback)((ts) => {
|
|
69
|
+
const canvas = canvasRef.current;
|
|
70
|
+
const scene = sceneRef.current;
|
|
71
|
+
const camera = cameraRef.current;
|
|
72
|
+
const renderer = rendererRef.current;
|
|
73
|
+
if (!canvas || !scene || !camera || !renderer)
|
|
74
|
+
return;
|
|
75
|
+
if (lastTsRef.current === null)
|
|
76
|
+
lastTsRef.current = ts;
|
|
77
|
+
const dt = (ts - lastTsRef.current) / 1000;
|
|
78
|
+
lastTsRef.current = ts;
|
|
79
|
+
timeRef.current += dt;
|
|
80
|
+
frameRef.current += 1;
|
|
81
|
+
const width = canvas.clientWidth || 1;
|
|
82
|
+
const height = canvas.clientHeight || 1;
|
|
83
|
+
optionsRef.current.draw?.({
|
|
84
|
+
scene, camera, renderer, canvas, width, height,
|
|
85
|
+
dt, time: timeRef.current, frame: frameRef.current,
|
|
86
|
+
});
|
|
87
|
+
renderer.render(scene, camera);
|
|
88
|
+
rafRef.current = requestAnimationFrame(loop);
|
|
89
|
+
}, []);
|
|
90
|
+
const start = (0, react_1.useCallback)(() => {
|
|
91
|
+
if (runningRef.current)
|
|
92
|
+
return;
|
|
93
|
+
runningRef.current = true;
|
|
94
|
+
setRunning(true);
|
|
95
|
+
lastTsRef.current = null;
|
|
96
|
+
rafRef.current = requestAnimationFrame(loop);
|
|
97
|
+
}, [loop]);
|
|
98
|
+
const stop = (0, react_1.useCallback)(() => {
|
|
99
|
+
runningRef.current = false;
|
|
100
|
+
setRunning(false);
|
|
101
|
+
cancelAnimationFrame(rafRef.current);
|
|
102
|
+
}, []);
|
|
103
|
+
const reset = (0, react_1.useCallback)(() => {
|
|
104
|
+
stop();
|
|
105
|
+
timeRef.current = 0;
|
|
106
|
+
frameRef.current = 0;
|
|
107
|
+
lastTsRef.current = null;
|
|
108
|
+
}, [stop]);
|
|
109
|
+
const isRunning = (0, react_1.useCallback)(() => runningRef.current, []);
|
|
110
|
+
// Reserva um slot no orçamento global de contextos WebGL antes de criar
|
|
111
|
+
// qualquer coisa. Se não conseguir na hora, fica em espera e tenta de
|
|
112
|
+
// novo quando um slot se abrir (ver webglBudget.ts).
|
|
113
|
+
(0, react_1.useEffect)(() => {
|
|
114
|
+
if (!(optionsRef.current.respectBudget ?? true)) {
|
|
115
|
+
setHasSlot(true);
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
let granted = false;
|
|
119
|
+
const onGrant = () => { granted = true; setHasSlot(true); };
|
|
120
|
+
if ((0, webglBudget_1.acquireWebglSlot)(onGrant)) {
|
|
121
|
+
granted = true;
|
|
122
|
+
setHasSlot(true);
|
|
123
|
+
}
|
|
124
|
+
return () => {
|
|
125
|
+
if (granted)
|
|
126
|
+
(0, webglBudget_1.releaseWebglSlot)();
|
|
127
|
+
else
|
|
128
|
+
(0, webglBudget_1.cancelWebglSlotRequest)(onGrant);
|
|
129
|
+
};
|
|
130
|
+
}, []);
|
|
131
|
+
(0, react_1.useEffect)(() => {
|
|
132
|
+
if (!hasSlot)
|
|
133
|
+
return undefined;
|
|
134
|
+
const canvas = canvasRef.current;
|
|
135
|
+
if (!canvas)
|
|
136
|
+
return undefined;
|
|
137
|
+
const renderer = new THREE.WebGLRenderer({
|
|
138
|
+
canvas,
|
|
139
|
+
antialias: true,
|
|
140
|
+
alpha: false,
|
|
141
|
+
...optionsRef.current.rendererOptions,
|
|
142
|
+
});
|
|
143
|
+
const scene = new THREE.Scene();
|
|
144
|
+
const camera = new THREE.PerspectiveCamera(optionsRef.current.fov ?? 50, 1, optionsRef.current.near ?? 0.1, optionsRef.current.far ?? 1000);
|
|
145
|
+
camera.position.z = 5;
|
|
146
|
+
sceneRef.current = scene;
|
|
147
|
+
cameraRef.current = camera;
|
|
148
|
+
rendererRef.current = renderer;
|
|
149
|
+
const resize = () => {
|
|
150
|
+
const rect = canvas.getBoundingClientRect();
|
|
151
|
+
const width = Math.max(1, Math.round(rect.width));
|
|
152
|
+
const height = Math.max(1, Math.round(rect.height));
|
|
153
|
+
renderer.setPixelRatio(window.devicePixelRatio || 1);
|
|
154
|
+
renderer.setSize(width, height, false);
|
|
155
|
+
camera.aspect = width / height;
|
|
156
|
+
camera.updateProjectionMatrix();
|
|
157
|
+
optionsRef.current.setup?.({ scene, camera, renderer, canvas, width, height });
|
|
158
|
+
};
|
|
159
|
+
resize();
|
|
160
|
+
const ro = new ResizeObserver(resize);
|
|
161
|
+
ro.observe(canvas);
|
|
162
|
+
const toLocal = (e) => {
|
|
163
|
+
const rect = canvas.getBoundingClientRect();
|
|
164
|
+
return { x: e.clientX - rect.left, y: e.clientY - rect.top, event: e };
|
|
165
|
+
};
|
|
166
|
+
const onPointerDown = (e) => optionsRef.current.onPointerDown?.(toLocal(e));
|
|
167
|
+
const onPointerMove = (e) => optionsRef.current.onPointerMove?.(toLocal(e));
|
|
168
|
+
const onPointerUp = (e) => optionsRef.current.onPointerUp?.(toLocal(e));
|
|
169
|
+
canvas.addEventListener('pointerdown', onPointerDown);
|
|
170
|
+
canvas.addEventListener('pointermove', onPointerMove);
|
|
171
|
+
canvas.addEventListener('pointerup', onPointerUp);
|
|
172
|
+
const onKeyDown = (e) => optionsRef.current.onKeyDown?.(e);
|
|
173
|
+
const onKeyUp = (e) => optionsRef.current.onKeyUp?.(e);
|
|
174
|
+
window.addEventListener('keydown', onKeyDown);
|
|
175
|
+
window.addEventListener('keyup', onKeyUp);
|
|
176
|
+
if (optionsRef.current.autoStart ?? true)
|
|
177
|
+
start();
|
|
178
|
+
return () => {
|
|
179
|
+
ro.disconnect();
|
|
180
|
+
canvas.removeEventListener('pointerdown', onPointerDown);
|
|
181
|
+
canvas.removeEventListener('pointermove', onPointerMove);
|
|
182
|
+
canvas.removeEventListener('pointerup', onPointerUp);
|
|
183
|
+
window.removeEventListener('keydown', onKeyDown);
|
|
184
|
+
window.removeEventListener('keyup', onKeyUp);
|
|
185
|
+
cancelAnimationFrame(rafRef.current);
|
|
186
|
+
runningRef.current = false;
|
|
187
|
+
// Libera geometria/material/textura e força a perda do contexto —
|
|
188
|
+
// sem isso o contexto WebGL fica pendurado até o garbage collector
|
|
189
|
+
// decidir coletar o WebGLRenderer, o que pode nunca acontecer a
|
|
190
|
+
// tempo em uma galeria que monta/desmonta várias prévias.
|
|
191
|
+
scene.traverse((obj) => {
|
|
192
|
+
const mesh = obj;
|
|
193
|
+
mesh.geometry?.dispose?.();
|
|
194
|
+
const material = mesh.material;
|
|
195
|
+
if (Array.isArray(material))
|
|
196
|
+
material.forEach((m) => m.dispose());
|
|
197
|
+
else
|
|
198
|
+
material?.dispose?.();
|
|
199
|
+
});
|
|
200
|
+
renderer.dispose();
|
|
201
|
+
renderer.forceContextLoss();
|
|
202
|
+
sceneRef.current = null;
|
|
203
|
+
cameraRef.current = null;
|
|
204
|
+
rendererRef.current = null;
|
|
205
|
+
};
|
|
206
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
207
|
+
}, [hasSlot]);
|
|
208
|
+
return { canvasRef, start, stop, reset, isRunning, running };
|
|
209
|
+
}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function clamp(value: number, min: number, max: number): number;
|
|
2
|
+
export declare function randRange(min: number, max: number): number;
|
|
3
|
+
export type RGB = [number, number, number];
|
|
4
|
+
/** Linearly interpolates between two RGB colors and returns an `rgb(...)` string. */
|
|
5
|
+
export declare function lerpColor(a: RGB, b: RGB, t: number): string;
|
|
6
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAErE;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAE3C,qFAAqF;AACrF,wBAAgB,SAAS,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAM3D"}
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.clamp = clamp;
|
|
4
|
+
exports.randRange = randRange;
|
|
5
|
+
exports.lerpColor = lerpColor;
|
|
6
|
+
function clamp(value, min, max) {
|
|
7
|
+
return Math.min(max, Math.max(min, value));
|
|
8
|
+
}
|
|
9
|
+
function randRange(min, max) {
|
|
10
|
+
return min + Math.random() * (max - min);
|
|
11
|
+
}
|
|
12
|
+
/** Linearly interpolates between two RGB colors and returns an `rgb(...)` string. */
|
|
13
|
+
function lerpColor(a, b, t) {
|
|
14
|
+
const tt = clamp(t, 0, 1);
|
|
15
|
+
const r = Math.round(a[0] + (b[0] - a[0]) * tt);
|
|
16
|
+
const g = Math.round(a[1] + (b[1] - a[1]) * tt);
|
|
17
|
+
const bl = Math.round(a[2] + (b[2] - a[2]) * tt);
|
|
18
|
+
return `rgb(${r},${g},${bl})`;
|
|
19
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
type Listener = () => void;
|
|
2
|
+
/**
|
|
3
|
+
* Ajusta o número máximo de contextos WebGL simultâneos que useThreeSketch
|
|
4
|
+
* respeita. Browsers descartam o contexto mais antigo silenciosamente ao
|
|
5
|
+
* estourar o teto do processo (Safari costuma ser bem mais restritivo que
|
|
6
|
+
* Chrome), então o padrão (4) é conservador para caber várias prévias na
|
|
7
|
+
* mesma página sem depender do limite exato de cada navegador.
|
|
8
|
+
*/
|
|
9
|
+
export declare function configureWebglBudget(max: number): void;
|
|
10
|
+
export declare function getWebglBudget(): {
|
|
11
|
+
active: number;
|
|
12
|
+
max: number;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Reserva um slot no orçamento global. Retorna true se conseguiu na hora
|
|
16
|
+
* (o chamador deve chamar releaseWebglSlot() quando terminar). Se não
|
|
17
|
+
* houver slot livre, registra `onGrant` para ser chamado assim que um se
|
|
18
|
+
* abrir — o chamador decide o que fazer nesse momento (tipicamente tentar
|
|
19
|
+
* adquirir de novo).
|
|
20
|
+
*/
|
|
21
|
+
export declare function acquireWebglSlot(onGrant?: Listener): boolean;
|
|
22
|
+
export declare function cancelWebglSlotRequest(onGrant: Listener): void;
|
|
23
|
+
export declare function releaseWebglSlot(): void;
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=webglBudget.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webglBudget.d.ts","sourceRoot":"","sources":["../src/webglBudget.ts"],"names":[],"mappings":"AAAA,KAAK,QAAQ,GAAG,MAAM,IAAI,CAAC;AAkB3B;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAGtD;AAED,wBAAgB,cAAc,IAAI;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAEhE;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,CAAC,EAAE,QAAQ,GAAG,OAAO,CAO5D;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,QAAQ,GAAG,IAAI,CAE9D;AAED,wBAAgB,gBAAgB,IAAI,IAAI,CAGvC"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.configureWebglBudget = configureWebglBudget;
|
|
4
|
+
exports.getWebglBudget = getWebglBudget;
|
|
5
|
+
exports.acquireWebglSlot = acquireWebglSlot;
|
|
6
|
+
exports.cancelWebglSlotRequest = cancelWebglSlotRequest;
|
|
7
|
+
exports.releaseWebglSlot = releaseWebglSlot;
|
|
8
|
+
let maxConcurrent = 4;
|
|
9
|
+
let active = 0;
|
|
10
|
+
const waiters = new Set();
|
|
11
|
+
function notifyWaiters() {
|
|
12
|
+
while (active < maxConcurrent && waiters.size > 0) {
|
|
13
|
+
const listener = waiters.values().next().value;
|
|
14
|
+
waiters.delete(listener);
|
|
15
|
+
// Reserve the slot before calling out — the waiter is being granted
|
|
16
|
+
// exactly the capacity acquireWebglSlot() would have given it
|
|
17
|
+
// synchronously, so it must count against the budget the same way.
|
|
18
|
+
active += 1;
|
|
19
|
+
listener();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Ajusta o número máximo de contextos WebGL simultâneos que useThreeSketch
|
|
24
|
+
* respeita. Browsers descartam o contexto mais antigo silenciosamente ao
|
|
25
|
+
* estourar o teto do processo (Safari costuma ser bem mais restritivo que
|
|
26
|
+
* Chrome), então o padrão (4) é conservador para caber várias prévias na
|
|
27
|
+
* mesma página sem depender do limite exato de cada navegador.
|
|
28
|
+
*/
|
|
29
|
+
function configureWebglBudget(max) {
|
|
30
|
+
maxConcurrent = Math.max(1, max);
|
|
31
|
+
notifyWaiters();
|
|
32
|
+
}
|
|
33
|
+
function getWebglBudget() {
|
|
34
|
+
return { active, max: maxConcurrent };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Reserva um slot no orçamento global. Retorna true se conseguiu na hora
|
|
38
|
+
* (o chamador deve chamar releaseWebglSlot() quando terminar). Se não
|
|
39
|
+
* houver slot livre, registra `onGrant` para ser chamado assim que um se
|
|
40
|
+
* abrir — o chamador decide o que fazer nesse momento (tipicamente tentar
|
|
41
|
+
* adquirir de novo).
|
|
42
|
+
*/
|
|
43
|
+
function acquireWebglSlot(onGrant) {
|
|
44
|
+
if (active < maxConcurrent) {
|
|
45
|
+
active += 1;
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
if (onGrant)
|
|
49
|
+
waiters.add(onGrant);
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
function cancelWebglSlotRequest(onGrant) {
|
|
53
|
+
waiters.delete(onGrant);
|
|
54
|
+
}
|
|
55
|
+
function releaseWebglSlot() {
|
|
56
|
+
active = Math.max(0, active - 1);
|
|
57
|
+
notifyWaiters();
|
|
58
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "next-sketch",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "p5.js-style canvas sketches for React/Next.js — headless hooks plus drop-in <CanvasSketch>/<ThreeSketch> components, SSR-safe and DPR-aware, with an optional Three.js engine and a shared WebGL context budget.",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"require": "./dist/index.js",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"test": "jest",
|
|
17
|
+
"test:watch": "jest --watch",
|
|
18
|
+
"test:coverage": "jest --coverage",
|
|
19
|
+
"prepublishOnly": "npm run build",
|
|
20
|
+
"lint": "tsc --noEmit"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"react",
|
|
24
|
+
"nextjs",
|
|
25
|
+
"canvas",
|
|
26
|
+
"sketch",
|
|
27
|
+
"p5js",
|
|
28
|
+
"animation",
|
|
29
|
+
"simulation",
|
|
30
|
+
"creative-coding",
|
|
31
|
+
"three",
|
|
32
|
+
"threejs",
|
|
33
|
+
"webgl",
|
|
34
|
+
"3d"
|
|
35
|
+
],
|
|
36
|
+
"author": "ReinanBr",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"homepage": "https://github.com/reinanbr/next-sketch#readme",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/reinanbr/next-sketch/issues"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"react": ">=18",
|
|
47
|
+
"react-dom": ">=18",
|
|
48
|
+
"three": ">=0.150.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"three": {
|
|
52
|
+
"optional": true
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@testing-library/react": "^16.0.0",
|
|
57
|
+
"@types/jest": "^30.0.0",
|
|
58
|
+
"@types/node": "^22.0.0",
|
|
59
|
+
"@types/react": "^19.0.0",
|
|
60
|
+
"@types/react-dom": "^19.0.0",
|
|
61
|
+
"@types/three": "^0.185.1",
|
|
62
|
+
"jest": "^30.0.0",
|
|
63
|
+
"jest-environment-jsdom": "^30.0.0",
|
|
64
|
+
"react": "^19.0.0",
|
|
65
|
+
"react-dom": "^19.0.0",
|
|
66
|
+
"three": "^0.185.1",
|
|
67
|
+
"ts-jest": "^29.1.0",
|
|
68
|
+
"typescript": "^5.8.2"
|
|
69
|
+
},
|
|
70
|
+
"files": [
|
|
71
|
+
"dist",
|
|
72
|
+
"README.md",
|
|
73
|
+
"LICENSE"
|
|
74
|
+
],
|
|
75
|
+
"repository": {
|
|
76
|
+
"type": "git",
|
|
77
|
+
"url": "https://github.com/reinanbr/next-sketch"
|
|
78
|
+
}
|
|
79
|
+
}
|