@sweberdev/lagrangian-react 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 +37 -0
- package/dist/index.cjs +161 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +36 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +143 -0
- package/dist/index.js.map +1 -0
- package/package.json +77 -0
- package/src/index.tsx +194 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Seya
|
|
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,37 @@
|
|
|
1
|
+
# @sweberdev/lagrangian-react
|
|
2
|
+
|
|
3
|
+
React hooks for [Lagrangian](https://packages.sweber.dev/lagrangian): physical springs, draggable elements with inertia and a 2D physics world with gravity and collisions. Elements are styled directly, so nothing re-renders on every frame.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
pnpm add @sweberdev/lagrangian @sweberdev/lagrangian-react
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
| Hook | What it does |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `useSpringProps(props, options)` | ref that springs the element to `{ x, y, scale, rotate, opacity, --custom }` whenever they change |
|
|
12
|
+
| `useSpring(target, options)` | a physical value following `target` with momentum |
|
|
13
|
+
| `useValue(value)` | the current number, re-rendering on change (for readouts) |
|
|
14
|
+
| `usePhysicsValue(initial)` | a physical value that lives as long as the component |
|
|
15
|
+
| `useDraggable(options)` | ref and handle for a draggable, throwable element |
|
|
16
|
+
| `useWorld(options)` | ref for the container and the running `World` |
|
|
17
|
+
| `useBody(world, options)` | ref for an element that follows a body in the world |
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import { useBody, useWorld } from "@sweberdev/lagrangian-react";
|
|
21
|
+
|
|
22
|
+
function Ball({ world }) {
|
|
23
|
+
const [ref] = useBody<HTMLDivElement>(world, { x: 100, y: 40, radius: 24, restitution: 0.7 });
|
|
24
|
+
return <div ref={ref} className="absolute left-0 top-0 size-12 rounded-full bg-black" />;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function Playground() {
|
|
28
|
+
const [ref, world] = useWorld<HTMLDivElement>();
|
|
29
|
+
return (
|
|
30
|
+
<div ref={ref} className="relative h-96 overflow-hidden">
|
|
31
|
+
<Ball world={world} />
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Docs: [packages.sweber.dev/lagrangian/docs](https://packages.sweber.dev/lagrangian/docs). MIT © Seya Weber
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.tsx
|
|
22
|
+
var index_exports = {};
|
|
23
|
+
__export(index_exports, {
|
|
24
|
+
useBody: () => useBody,
|
|
25
|
+
useDraggable: () => useDraggable,
|
|
26
|
+
usePhysicsValue: () => usePhysicsValue,
|
|
27
|
+
useSpring: () => useSpring,
|
|
28
|
+
useSpringProps: () => useSpringProps,
|
|
29
|
+
useValue: () => useValue,
|
|
30
|
+
useWorld: () => useWorld
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(index_exports);
|
|
33
|
+
var import_lagrangian = require("@sweberdev/lagrangian");
|
|
34
|
+
var import_react = require("react");
|
|
35
|
+
var useIsoLayoutEffect = typeof window === "undefined" ? import_react.useEffect : import_react.useLayoutEffect;
|
|
36
|
+
function useLatest(current) {
|
|
37
|
+
const ref = (0, import_react.useRef)(current);
|
|
38
|
+
useIsoLayoutEffect(() => {
|
|
39
|
+
ref.current = current;
|
|
40
|
+
});
|
|
41
|
+
return ref;
|
|
42
|
+
}
|
|
43
|
+
function useAttach(attach) {
|
|
44
|
+
const cleanup = (0, import_react.useRef)(null);
|
|
45
|
+
const latest = useLatest(attach);
|
|
46
|
+
return (0, import_react.useCallback)(
|
|
47
|
+
(element) => {
|
|
48
|
+
cleanup.current?.();
|
|
49
|
+
cleanup.current = null;
|
|
50
|
+
if (!element) return;
|
|
51
|
+
const detach = latest.current(element);
|
|
52
|
+
cleanup.current = detach;
|
|
53
|
+
return () => {
|
|
54
|
+
if (cleanup.current === detach) {
|
|
55
|
+
detach();
|
|
56
|
+
cleanup.current = null;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
[latest]
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
function usePhysicsValue(initial = 0) {
|
|
64
|
+
const [v] = (0, import_react.useState)(() => (0, import_lagrangian.value)(initial));
|
|
65
|
+
(0, import_react.useEffect)(() => () => v.stop(), [v]);
|
|
66
|
+
return v;
|
|
67
|
+
}
|
|
68
|
+
function useSpring(target, options = {}) {
|
|
69
|
+
const v = usePhysicsValue(target);
|
|
70
|
+
const latest = useLatest(options);
|
|
71
|
+
(0, import_react.useEffect)(() => {
|
|
72
|
+
v.to(target, latest.current);
|
|
73
|
+
}, [v, target, latest]);
|
|
74
|
+
return v;
|
|
75
|
+
}
|
|
76
|
+
function useValue(v) {
|
|
77
|
+
return (0, import_react.useSyncExternalStore)(
|
|
78
|
+
(notify) => v.on(notify),
|
|
79
|
+
() => v.get(),
|
|
80
|
+
() => v.get()
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
function useSpringProps(props, options = {}) {
|
|
84
|
+
const ref = (0, import_react.useRef)(null);
|
|
85
|
+
const latest = useLatest(options);
|
|
86
|
+
const key = JSON.stringify(props);
|
|
87
|
+
useIsoLayoutEffect(() => {
|
|
88
|
+
if (!ref.current) return;
|
|
89
|
+
const animation = (0, import_lagrangian.animate)(ref.current, props, latest.current);
|
|
90
|
+
return () => animation.stop();
|
|
91
|
+
}, [key, latest]);
|
|
92
|
+
return ref;
|
|
93
|
+
}
|
|
94
|
+
function useDraggable(options = {}) {
|
|
95
|
+
const [handle, setHandle] = (0, import_react.useState)(null);
|
|
96
|
+
const latest = useLatest(options);
|
|
97
|
+
const ref = useAttach((element) => {
|
|
98
|
+
const current = latest.current;
|
|
99
|
+
const drag = (0, import_lagrangian.draggable)(element, {
|
|
100
|
+
...current,
|
|
101
|
+
bounds: () => {
|
|
102
|
+
const b = latest.current.bounds;
|
|
103
|
+
return (typeof b === "function" ? b() : b) ?? {};
|
|
104
|
+
},
|
|
105
|
+
onStart: (info) => latest.current.onStart?.(info),
|
|
106
|
+
onMove: (info) => latest.current.onMove?.(info),
|
|
107
|
+
onEnd: (info) => latest.current.onEnd?.(info)
|
|
108
|
+
});
|
|
109
|
+
setHandle(drag);
|
|
110
|
+
return () => {
|
|
111
|
+
drag.destroy();
|
|
112
|
+
setHandle(null);
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
return [ref, handle];
|
|
116
|
+
}
|
|
117
|
+
function useWorld(options = {}) {
|
|
118
|
+
const latest = useLatest(options);
|
|
119
|
+
const [world] = (0, import_react.useState)(
|
|
120
|
+
() => new import_lagrangian.World({
|
|
121
|
+
...options,
|
|
122
|
+
onFrame: (w) => latest.current.onFrame?.(w),
|
|
123
|
+
onCollide: (a, b, speed) => latest.current.onCollide?.(a, b, speed)
|
|
124
|
+
})
|
|
125
|
+
);
|
|
126
|
+
const ref = useAttach((element) => {
|
|
127
|
+
world.setBounds(element);
|
|
128
|
+
world.start();
|
|
129
|
+
const unbind = latest.current.interactive !== false ? world.bindPointer(element) : () => {
|
|
130
|
+
};
|
|
131
|
+
return () => {
|
|
132
|
+
unbind();
|
|
133
|
+
world.stop();
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
return [ref, world];
|
|
137
|
+
}
|
|
138
|
+
function useBody(world, options) {
|
|
139
|
+
const [body, setBody] = (0, import_react.useState)(null);
|
|
140
|
+
const latest = useLatest(options);
|
|
141
|
+
const ref = useAttach((element) => {
|
|
142
|
+
const added = world.add({ ...latest.current, element });
|
|
143
|
+
setBody(added);
|
|
144
|
+
return () => {
|
|
145
|
+
world.remove(added);
|
|
146
|
+
setBody(null);
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
return [ref, body];
|
|
150
|
+
}
|
|
151
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
152
|
+
0 && (module.exports = {
|
|
153
|
+
useBody,
|
|
154
|
+
useDraggable,
|
|
155
|
+
usePhysicsValue,
|
|
156
|
+
useSpring,
|
|
157
|
+
useSpringProps,
|
|
158
|
+
useValue,
|
|
159
|
+
useWorld
|
|
160
|
+
});
|
|
161
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.tsx"],"sourcesContent":["import {\n type AnimateOptions,\n animate,\n type Body,\n type BodyOptions,\n type Draggable,\n type DraggableOptions,\n draggable,\n type PhysicsValue,\n type Props,\n type SpringOptions,\n value,\n World,\n type WorldOptions,\n} from \"@sweberdev/lagrangian\";\nimport {\n type RefCallback,\n useCallback,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n useSyncExternalStore,\n} from \"react\";\n\nconst useIsoLayoutEffect = typeof window === \"undefined\" ? useEffect : useLayoutEffect;\n\n/** Keeps the latest value in a ref so callbacks never go stale. */\nfunction useLatest<T>(current: T) {\n const ref = useRef(current);\n useIsoLayoutEffect(() => {\n ref.current = current;\n });\n return ref;\n}\n\n/**\n * A ref callback with cleanup that works in React 18 (called with null) and 19 (cleanup return).\n */\nfunction useAttach<T extends Element>(attach: (element: T) => () => void): RefCallback<T> {\n const cleanup = useRef<(() => void) | null>(null);\n const latest = useLatest(attach);\n return useCallback(\n (element: T | null) => {\n cleanup.current?.();\n cleanup.current = null;\n if (!element) return;\n const detach = latest.current(element);\n cleanup.current = detach;\n return () => {\n if (cleanup.current === detach) {\n detach();\n cleanup.current = null;\n }\n };\n },\n [latest],\n );\n}\n\n/** A physical value that lives as long as the component. */\nexport function usePhysicsValue(initial = 0): PhysicsValue {\n const [v] = useState(() => value(initial));\n useEffect(() => () => v.stop(), [v]);\n return v;\n}\n\n/**\n * A value that springs to `target` whenever it changes, keeping its momentum when the target\n * changes mid-flight. Read it with `useValue` or subscribe with `value.on`.\n */\nexport function useSpring(target: number, options: SpringOptions = {}): PhysicsValue {\n const v = usePhysicsValue(target);\n const latest = useLatest(options);\n useEffect(() => {\n v.to(target, latest.current);\n }, [v, target, latest]);\n return v;\n}\n\n/** Re-renders with the current number. Fine for readouts; style elements with `useSpringProps`. */\nexport function useValue(v: PhysicsValue): number {\n return useSyncExternalStore(\n (notify) => v.on(notify),\n () => v.get(),\n () => v.get(),\n );\n}\n\n/**\n * Springs an element to `props` whenever they change, without re-rendering on every frame.\n *\n * ```tsx\n * const ref = useSpringProps<HTMLDivElement>({ x: open ? 240 : 0 }, { bounce: 0.3 })\n * return <div ref={ref} />\n * ```\n */\nexport function useSpringProps<T extends Element>(props: Props, options: AnimateOptions = {}) {\n const ref = useRef<T>(null);\n const latest = useLatest(options);\n const key = JSON.stringify(props);\n useIsoLayoutEffect(() => {\n if (!ref.current) return;\n const animation = animate(ref.current, props, latest.current);\n return () => animation.stop();\n }, [key, latest]);\n return ref;\n}\n\n/** Makes the element draggable and throwable. Returns a ref callback and the drag handle. */\nexport function useDraggable<T extends HTMLElement>(\n options: DraggableOptions = {},\n): [RefCallback<T>, Draggable | null] {\n const [handle, setHandle] = useState<Draggable | null>(null);\n const latest = useLatest(options);\n const ref = useAttach<T>((element) => {\n // Read options when events fire, so callbacks and snap points can change between renders.\n const current = latest.current;\n const drag = draggable(element, {\n ...current,\n bounds: () => {\n const b = latest.current.bounds;\n return (typeof b === \"function\" ? b() : b) ?? {};\n },\n onStart: (info) => latest.current.onStart?.(info),\n onMove: (info) => latest.current.onMove?.(info),\n onEnd: (info) => latest.current.onEnd?.(info),\n });\n setHandle(drag);\n return () => {\n drag.destroy();\n setHandle(null);\n };\n });\n return [ref, handle];\n}\n\n/**\n * A running physics world whose walls are the container element. Bodies inside can be\n * grabbed and thrown unless `interactive` is false.\n */\nexport function useWorld<T extends HTMLElement>(\n options: Omit<WorldOptions, \"bounds\"> & { interactive?: boolean } = {},\n): [RefCallback<T>, World] {\n const latest = useLatest(options);\n const [world] = useState(\n () =>\n new World({\n ...options,\n onFrame: (w) => latest.current.onFrame?.(w),\n onCollide: (a, b, speed) => latest.current.onCollide?.(a, b, speed),\n }),\n );\n const ref = useAttach<T>((element) => {\n world.setBounds(element);\n world.start();\n const unbind = latest.current.interactive !== false ? world.bindPointer(element) : () => {};\n return () => {\n unbind();\n world.stop();\n };\n });\n return [ref, world];\n}\n\n/** Adds a body to the world that the element follows. Position the element absolutely at 0,0. */\nexport function useBody<T extends HTMLElement>(\n world: World,\n options: Omit<BodyOptions, \"element\">,\n): [RefCallback<T>, Body | null] {\n const [body, setBody] = useState<Body | null>(null);\n const latest = useLatest(options);\n const ref = useAttach<T>((element) => {\n const added = world.add({ ...latest.current, element });\n setBody(added);\n return () => {\n world.remove(added);\n setBody(null);\n };\n });\n return [ref, body];\n}\n\nexport type {\n AnimateOptions,\n Body,\n BodyOptions,\n Draggable,\n DraggableOptions,\n PhysicsValue,\n Props,\n SpringOptions,\n WorldOptions,\n};\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { RefCallback } from 'react';
|
|
3
|
+
import { World, BodyOptions, Body, DraggableOptions, Draggable, PhysicsValue, SpringOptions, Props, AnimateOptions, WorldOptions } from '@sweberdev/lagrangian';
|
|
4
|
+
export { AnimateOptions, Body, BodyOptions, Draggable, DraggableOptions, PhysicsValue, Props, SpringOptions, WorldOptions } from '@sweberdev/lagrangian';
|
|
5
|
+
|
|
6
|
+
/** A physical value that lives as long as the component. */
|
|
7
|
+
declare function usePhysicsValue(initial?: number): PhysicsValue;
|
|
8
|
+
/**
|
|
9
|
+
* A value that springs to `target` whenever it changes, keeping its momentum when the target
|
|
10
|
+
* changes mid-flight. Read it with `useValue` or subscribe with `value.on`.
|
|
11
|
+
*/
|
|
12
|
+
declare function useSpring(target: number, options?: SpringOptions): PhysicsValue;
|
|
13
|
+
/** Re-renders with the current number. Fine for readouts; style elements with `useSpringProps`. */
|
|
14
|
+
declare function useValue(v: PhysicsValue): number;
|
|
15
|
+
/**
|
|
16
|
+
* Springs an element to `props` whenever they change, without re-rendering on every frame.
|
|
17
|
+
*
|
|
18
|
+
* ```tsx
|
|
19
|
+
* const ref = useSpringProps<HTMLDivElement>({ x: open ? 240 : 0 }, { bounce: 0.3 })
|
|
20
|
+
* return <div ref={ref} />
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
declare function useSpringProps<T extends Element>(props: Props, options?: AnimateOptions): react.RefObject<T | null>;
|
|
24
|
+
/** Makes the element draggable and throwable. Returns a ref callback and the drag handle. */
|
|
25
|
+
declare function useDraggable<T extends HTMLElement>(options?: DraggableOptions): [RefCallback<T>, Draggable | null];
|
|
26
|
+
/**
|
|
27
|
+
* A running physics world whose walls are the container element. Bodies inside can be
|
|
28
|
+
* grabbed and thrown unless `interactive` is false.
|
|
29
|
+
*/
|
|
30
|
+
declare function useWorld<T extends HTMLElement>(options?: Omit<WorldOptions, "bounds"> & {
|
|
31
|
+
interactive?: boolean;
|
|
32
|
+
}): [RefCallback<T>, World];
|
|
33
|
+
/** Adds a body to the world that the element follows. Position the element absolutely at 0,0. */
|
|
34
|
+
declare function useBody<T extends HTMLElement>(world: World, options: Omit<BodyOptions, "element">): [RefCallback<T>, Body | null];
|
|
35
|
+
|
|
36
|
+
export { useBody, useDraggable, usePhysicsValue, useSpring, useSpringProps, useValue, useWorld };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { RefCallback } from 'react';
|
|
3
|
+
import { World, BodyOptions, Body, DraggableOptions, Draggable, PhysicsValue, SpringOptions, Props, AnimateOptions, WorldOptions } from '@sweberdev/lagrangian';
|
|
4
|
+
export { AnimateOptions, Body, BodyOptions, Draggable, DraggableOptions, PhysicsValue, Props, SpringOptions, WorldOptions } from '@sweberdev/lagrangian';
|
|
5
|
+
|
|
6
|
+
/** A physical value that lives as long as the component. */
|
|
7
|
+
declare function usePhysicsValue(initial?: number): PhysicsValue;
|
|
8
|
+
/**
|
|
9
|
+
* A value that springs to `target` whenever it changes, keeping its momentum when the target
|
|
10
|
+
* changes mid-flight. Read it with `useValue` or subscribe with `value.on`.
|
|
11
|
+
*/
|
|
12
|
+
declare function useSpring(target: number, options?: SpringOptions): PhysicsValue;
|
|
13
|
+
/** Re-renders with the current number. Fine for readouts; style elements with `useSpringProps`. */
|
|
14
|
+
declare function useValue(v: PhysicsValue): number;
|
|
15
|
+
/**
|
|
16
|
+
* Springs an element to `props` whenever they change, without re-rendering on every frame.
|
|
17
|
+
*
|
|
18
|
+
* ```tsx
|
|
19
|
+
* const ref = useSpringProps<HTMLDivElement>({ x: open ? 240 : 0 }, { bounce: 0.3 })
|
|
20
|
+
* return <div ref={ref} />
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
declare function useSpringProps<T extends Element>(props: Props, options?: AnimateOptions): react.RefObject<T | null>;
|
|
24
|
+
/** Makes the element draggable and throwable. Returns a ref callback and the drag handle. */
|
|
25
|
+
declare function useDraggable<T extends HTMLElement>(options?: DraggableOptions): [RefCallback<T>, Draggable | null];
|
|
26
|
+
/**
|
|
27
|
+
* A running physics world whose walls are the container element. Bodies inside can be
|
|
28
|
+
* grabbed and thrown unless `interactive` is false.
|
|
29
|
+
*/
|
|
30
|
+
declare function useWorld<T extends HTMLElement>(options?: Omit<WorldOptions, "bounds"> & {
|
|
31
|
+
interactive?: boolean;
|
|
32
|
+
}): [RefCallback<T>, World];
|
|
33
|
+
/** Adds a body to the world that the element follows. Position the element absolutely at 0,0. */
|
|
34
|
+
declare function useBody<T extends HTMLElement>(world: World, options: Omit<BodyOptions, "element">): [RefCallback<T>, Body | null];
|
|
35
|
+
|
|
36
|
+
export { useBody, useDraggable, usePhysicsValue, useSpring, useSpringProps, useValue, useWorld };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/index.tsx
|
|
4
|
+
import {
|
|
5
|
+
animate,
|
|
6
|
+
draggable,
|
|
7
|
+
value,
|
|
8
|
+
World
|
|
9
|
+
} from "@sweberdev/lagrangian";
|
|
10
|
+
import {
|
|
11
|
+
useCallback,
|
|
12
|
+
useEffect,
|
|
13
|
+
useLayoutEffect,
|
|
14
|
+
useRef,
|
|
15
|
+
useState,
|
|
16
|
+
useSyncExternalStore
|
|
17
|
+
} from "react";
|
|
18
|
+
var useIsoLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
19
|
+
function useLatest(current) {
|
|
20
|
+
const ref = useRef(current);
|
|
21
|
+
useIsoLayoutEffect(() => {
|
|
22
|
+
ref.current = current;
|
|
23
|
+
});
|
|
24
|
+
return ref;
|
|
25
|
+
}
|
|
26
|
+
function useAttach(attach) {
|
|
27
|
+
const cleanup = useRef(null);
|
|
28
|
+
const latest = useLatest(attach);
|
|
29
|
+
return useCallback(
|
|
30
|
+
(element) => {
|
|
31
|
+
cleanup.current?.();
|
|
32
|
+
cleanup.current = null;
|
|
33
|
+
if (!element) return;
|
|
34
|
+
const detach = latest.current(element);
|
|
35
|
+
cleanup.current = detach;
|
|
36
|
+
return () => {
|
|
37
|
+
if (cleanup.current === detach) {
|
|
38
|
+
detach();
|
|
39
|
+
cleanup.current = null;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
[latest]
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
function usePhysicsValue(initial = 0) {
|
|
47
|
+
const [v] = useState(() => value(initial));
|
|
48
|
+
useEffect(() => () => v.stop(), [v]);
|
|
49
|
+
return v;
|
|
50
|
+
}
|
|
51
|
+
function useSpring(target, options = {}) {
|
|
52
|
+
const v = usePhysicsValue(target);
|
|
53
|
+
const latest = useLatest(options);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
v.to(target, latest.current);
|
|
56
|
+
}, [v, target, latest]);
|
|
57
|
+
return v;
|
|
58
|
+
}
|
|
59
|
+
function useValue(v) {
|
|
60
|
+
return useSyncExternalStore(
|
|
61
|
+
(notify) => v.on(notify),
|
|
62
|
+
() => v.get(),
|
|
63
|
+
() => v.get()
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
function useSpringProps(props, options = {}) {
|
|
67
|
+
const ref = useRef(null);
|
|
68
|
+
const latest = useLatest(options);
|
|
69
|
+
const key = JSON.stringify(props);
|
|
70
|
+
useIsoLayoutEffect(() => {
|
|
71
|
+
if (!ref.current) return;
|
|
72
|
+
const animation = animate(ref.current, props, latest.current);
|
|
73
|
+
return () => animation.stop();
|
|
74
|
+
}, [key, latest]);
|
|
75
|
+
return ref;
|
|
76
|
+
}
|
|
77
|
+
function useDraggable(options = {}) {
|
|
78
|
+
const [handle, setHandle] = useState(null);
|
|
79
|
+
const latest = useLatest(options);
|
|
80
|
+
const ref = useAttach((element) => {
|
|
81
|
+
const current = latest.current;
|
|
82
|
+
const drag = draggable(element, {
|
|
83
|
+
...current,
|
|
84
|
+
bounds: () => {
|
|
85
|
+
const b = latest.current.bounds;
|
|
86
|
+
return (typeof b === "function" ? b() : b) ?? {};
|
|
87
|
+
},
|
|
88
|
+
onStart: (info) => latest.current.onStart?.(info),
|
|
89
|
+
onMove: (info) => latest.current.onMove?.(info),
|
|
90
|
+
onEnd: (info) => latest.current.onEnd?.(info)
|
|
91
|
+
});
|
|
92
|
+
setHandle(drag);
|
|
93
|
+
return () => {
|
|
94
|
+
drag.destroy();
|
|
95
|
+
setHandle(null);
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
return [ref, handle];
|
|
99
|
+
}
|
|
100
|
+
function useWorld(options = {}) {
|
|
101
|
+
const latest = useLatest(options);
|
|
102
|
+
const [world] = useState(
|
|
103
|
+
() => new World({
|
|
104
|
+
...options,
|
|
105
|
+
onFrame: (w) => latest.current.onFrame?.(w),
|
|
106
|
+
onCollide: (a, b, speed) => latest.current.onCollide?.(a, b, speed)
|
|
107
|
+
})
|
|
108
|
+
);
|
|
109
|
+
const ref = useAttach((element) => {
|
|
110
|
+
world.setBounds(element);
|
|
111
|
+
world.start();
|
|
112
|
+
const unbind = latest.current.interactive !== false ? world.bindPointer(element) : () => {
|
|
113
|
+
};
|
|
114
|
+
return () => {
|
|
115
|
+
unbind();
|
|
116
|
+
world.stop();
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
return [ref, world];
|
|
120
|
+
}
|
|
121
|
+
function useBody(world, options) {
|
|
122
|
+
const [body, setBody] = useState(null);
|
|
123
|
+
const latest = useLatest(options);
|
|
124
|
+
const ref = useAttach((element) => {
|
|
125
|
+
const added = world.add({ ...latest.current, element });
|
|
126
|
+
setBody(added);
|
|
127
|
+
return () => {
|
|
128
|
+
world.remove(added);
|
|
129
|
+
setBody(null);
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
return [ref, body];
|
|
133
|
+
}
|
|
134
|
+
export {
|
|
135
|
+
useBody,
|
|
136
|
+
useDraggable,
|
|
137
|
+
usePhysicsValue,
|
|
138
|
+
useSpring,
|
|
139
|
+
useSpringProps,
|
|
140
|
+
useValue,
|
|
141
|
+
useWorld
|
|
142
|
+
};
|
|
143
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.tsx"],"sourcesContent":["import {\n type AnimateOptions,\n animate,\n type Body,\n type BodyOptions,\n type Draggable,\n type DraggableOptions,\n draggable,\n type PhysicsValue,\n type Props,\n type SpringOptions,\n value,\n World,\n type WorldOptions,\n} from \"@sweberdev/lagrangian\";\nimport {\n type RefCallback,\n useCallback,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n useSyncExternalStore,\n} from \"react\";\n\nconst useIsoLayoutEffect = typeof window === \"undefined\" ? useEffect : useLayoutEffect;\n\n/** Keeps the latest value in a ref so callbacks never go stale. */\nfunction useLatest<T>(current: T) {\n const ref = useRef(current);\n useIsoLayoutEffect(() => {\n ref.current = current;\n });\n return ref;\n}\n\n/**\n * A ref callback with cleanup that works in React 18 (called with null) and 19 (cleanup return).\n */\nfunction useAttach<T extends Element>(attach: (element: T) => () => void): RefCallback<T> {\n const cleanup = useRef<(() => void) | null>(null);\n const latest = useLatest(attach);\n return useCallback(\n (element: T | null) => {\n cleanup.current?.();\n cleanup.current = null;\n if (!element) return;\n const detach = latest.current(element);\n cleanup.current = detach;\n return () => {\n if (cleanup.current === detach) {\n detach();\n cleanup.current = null;\n }\n };\n },\n [latest],\n );\n}\n\n/** A physical value that lives as long as the component. */\nexport function usePhysicsValue(initial = 0): PhysicsValue {\n const [v] = useState(() => value(initial));\n useEffect(() => () => v.stop(), [v]);\n return v;\n}\n\n/**\n * A value that springs to `target` whenever it changes, keeping its momentum when the target\n * changes mid-flight. Read it with `useValue` or subscribe with `value.on`.\n */\nexport function useSpring(target: number, options: SpringOptions = {}): PhysicsValue {\n const v = usePhysicsValue(target);\n const latest = useLatest(options);\n useEffect(() => {\n v.to(target, latest.current);\n }, [v, target, latest]);\n return v;\n}\n\n/** Re-renders with the current number. Fine for readouts; style elements with `useSpringProps`. */\nexport function useValue(v: PhysicsValue): number {\n return useSyncExternalStore(\n (notify) => v.on(notify),\n () => v.get(),\n () => v.get(),\n );\n}\n\n/**\n * Springs an element to `props` whenever they change, without re-rendering on every frame.\n *\n * ```tsx\n * const ref = useSpringProps<HTMLDivElement>({ x: open ? 240 : 0 }, { bounce: 0.3 })\n * return <div ref={ref} />\n * ```\n */\nexport function useSpringProps<T extends Element>(props: Props, options: AnimateOptions = {}) {\n const ref = useRef<T>(null);\n const latest = useLatest(options);\n const key = JSON.stringify(props);\n useIsoLayoutEffect(() => {\n if (!ref.current) return;\n const animation = animate(ref.current, props, latest.current);\n return () => animation.stop();\n }, [key, latest]);\n return ref;\n}\n\n/** Makes the element draggable and throwable. Returns a ref callback and the drag handle. */\nexport function useDraggable<T extends HTMLElement>(\n options: DraggableOptions = {},\n): [RefCallback<T>, Draggable | null] {\n const [handle, setHandle] = useState<Draggable | null>(null);\n const latest = useLatest(options);\n const ref = useAttach<T>((element) => {\n // Read options when events fire, so callbacks and snap points can change between renders.\n const current = latest.current;\n const drag = draggable(element, {\n ...current,\n bounds: () => {\n const b = latest.current.bounds;\n return (typeof b === \"function\" ? b() : b) ?? {};\n },\n onStart: (info) => latest.current.onStart?.(info),\n onMove: (info) => latest.current.onMove?.(info),\n onEnd: (info) => latest.current.onEnd?.(info),\n });\n setHandle(drag);\n return () => {\n drag.destroy();\n setHandle(null);\n };\n });\n return [ref, handle];\n}\n\n/**\n * A running physics world whose walls are the container element. Bodies inside can be\n * grabbed and thrown unless `interactive` is false.\n */\nexport function useWorld<T extends HTMLElement>(\n options: Omit<WorldOptions, \"bounds\"> & { interactive?: boolean } = {},\n): [RefCallback<T>, World] {\n const latest = useLatest(options);\n const [world] = useState(\n () =>\n new World({\n ...options,\n onFrame: (w) => latest.current.onFrame?.(w),\n onCollide: (a, b, speed) => latest.current.onCollide?.(a, b, speed),\n }),\n );\n const ref = useAttach<T>((element) => {\n world.setBounds(element);\n world.start();\n const unbind = latest.current.interactive !== false ? world.bindPointer(element) : () => {};\n return () => {\n unbind();\n world.stop();\n };\n });\n return [ref, world];\n}\n\n/** Adds a body to the world that the element follows. Position the element absolutely at 0,0. */\nexport function useBody<T extends HTMLElement>(\n world: World,\n options: Omit<BodyOptions, \"element\">,\n): [RefCallback<T>, Body | null] {\n const [body, setBody] = useState<Body | null>(null);\n const latest = useLatest(options);\n const ref = useAttach<T>((element) => {\n const added = world.add({ ...latest.current, element });\n setBody(added);\n return () => {\n world.remove(added);\n setBody(null);\n };\n });\n return [ref, body];\n}\n\nexport type {\n AnimateOptions,\n Body,\n BodyOptions,\n Draggable,\n DraggableOptions,\n PhysicsValue,\n Props,\n SpringOptions,\n WorldOptions,\n};\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sweberdev/lagrangian-react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React hooks for Lagrangian: physical springs, draggable elements with inertia and a 2D physics world with gravity and collisions.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Seya Weber (https://github.com/sxwxbxr)",
|
|
7
|
+
"homepage": "https://packages.sweber.dev/lagrangian/docs",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Weber-Development/lagrangian.git",
|
|
11
|
+
"directory": "packages/react"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/Weber-Development/lagrangian/issues"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"main": "./dist/index.cjs",
|
|
19
|
+
"module": "./dist/index.js",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"import": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"default": "./dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"require": {
|
|
28
|
+
"types": "./dist/index.d.cts",
|
|
29
|
+
"default": "./dist/index.cjs"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"./package.json": "./package.json"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"src",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
],
|
|
40
|
+
"keywords": [
|
|
41
|
+
"react",
|
|
42
|
+
"animation",
|
|
43
|
+
"physics",
|
|
44
|
+
"spring",
|
|
45
|
+
"drag",
|
|
46
|
+
"gesture",
|
|
47
|
+
"inertia",
|
|
48
|
+
"gravity",
|
|
49
|
+
"collision"
|
|
50
|
+
],
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public",
|
|
53
|
+
"provenance": true
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@sweberdev/lagrangian": "^0.1.0"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"react": ">=18"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@testing-library/react": "^16.3.0",
|
|
63
|
+
"@types/node": "^22.20.5",
|
|
64
|
+
"@types/react": "^19.1.0",
|
|
65
|
+
"jsdom": "^27.0.0",
|
|
66
|
+
"react": "^19.1.0",
|
|
67
|
+
"react-dom": "^19.1.0",
|
|
68
|
+
"tsup": "^8.5.1",
|
|
69
|
+
"typescript": "^5.9.0",
|
|
70
|
+
"vitest": "^5.0.3"
|
|
71
|
+
},
|
|
72
|
+
"scripts": {
|
|
73
|
+
"build": "tsup",
|
|
74
|
+
"test": "vitest run",
|
|
75
|
+
"typecheck": "tsc -p tsconfig.json"
|
|
76
|
+
}
|
|
77
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnimateOptions,
|
|
3
|
+
animate,
|
|
4
|
+
type Body,
|
|
5
|
+
type BodyOptions,
|
|
6
|
+
type Draggable,
|
|
7
|
+
type DraggableOptions,
|
|
8
|
+
draggable,
|
|
9
|
+
type PhysicsValue,
|
|
10
|
+
type Props,
|
|
11
|
+
type SpringOptions,
|
|
12
|
+
value,
|
|
13
|
+
World,
|
|
14
|
+
type WorldOptions,
|
|
15
|
+
} from "@sweberdev/lagrangian";
|
|
16
|
+
import {
|
|
17
|
+
type RefCallback,
|
|
18
|
+
useCallback,
|
|
19
|
+
useEffect,
|
|
20
|
+
useLayoutEffect,
|
|
21
|
+
useRef,
|
|
22
|
+
useState,
|
|
23
|
+
useSyncExternalStore,
|
|
24
|
+
} from "react";
|
|
25
|
+
|
|
26
|
+
const useIsoLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
27
|
+
|
|
28
|
+
/** Keeps the latest value in a ref so callbacks never go stale. */
|
|
29
|
+
function useLatest<T>(current: T) {
|
|
30
|
+
const ref = useRef(current);
|
|
31
|
+
useIsoLayoutEffect(() => {
|
|
32
|
+
ref.current = current;
|
|
33
|
+
});
|
|
34
|
+
return ref;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A ref callback with cleanup that works in React 18 (called with null) and 19 (cleanup return).
|
|
39
|
+
*/
|
|
40
|
+
function useAttach<T extends Element>(attach: (element: T) => () => void): RefCallback<T> {
|
|
41
|
+
const cleanup = useRef<(() => void) | null>(null);
|
|
42
|
+
const latest = useLatest(attach);
|
|
43
|
+
return useCallback(
|
|
44
|
+
(element: T | null) => {
|
|
45
|
+
cleanup.current?.();
|
|
46
|
+
cleanup.current = null;
|
|
47
|
+
if (!element) return;
|
|
48
|
+
const detach = latest.current(element);
|
|
49
|
+
cleanup.current = detach;
|
|
50
|
+
return () => {
|
|
51
|
+
if (cleanup.current === detach) {
|
|
52
|
+
detach();
|
|
53
|
+
cleanup.current = null;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
[latest],
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A physical value that lives as long as the component. */
|
|
62
|
+
export function usePhysicsValue(initial = 0): PhysicsValue {
|
|
63
|
+
const [v] = useState(() => value(initial));
|
|
64
|
+
useEffect(() => () => v.stop(), [v]);
|
|
65
|
+
return v;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A value that springs to `target` whenever it changes, keeping its momentum when the target
|
|
70
|
+
* changes mid-flight. Read it with `useValue` or subscribe with `value.on`.
|
|
71
|
+
*/
|
|
72
|
+
export function useSpring(target: number, options: SpringOptions = {}): PhysicsValue {
|
|
73
|
+
const v = usePhysicsValue(target);
|
|
74
|
+
const latest = useLatest(options);
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
v.to(target, latest.current);
|
|
77
|
+
}, [v, target, latest]);
|
|
78
|
+
return v;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Re-renders with the current number. Fine for readouts; style elements with `useSpringProps`. */
|
|
82
|
+
export function useValue(v: PhysicsValue): number {
|
|
83
|
+
return useSyncExternalStore(
|
|
84
|
+
(notify) => v.on(notify),
|
|
85
|
+
() => v.get(),
|
|
86
|
+
() => v.get(),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Springs an element to `props` whenever they change, without re-rendering on every frame.
|
|
92
|
+
*
|
|
93
|
+
* ```tsx
|
|
94
|
+
* const ref = useSpringProps<HTMLDivElement>({ x: open ? 240 : 0 }, { bounce: 0.3 })
|
|
95
|
+
* return <div ref={ref} />
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
export function useSpringProps<T extends Element>(props: Props, options: AnimateOptions = {}) {
|
|
99
|
+
const ref = useRef<T>(null);
|
|
100
|
+
const latest = useLatest(options);
|
|
101
|
+
const key = JSON.stringify(props);
|
|
102
|
+
useIsoLayoutEffect(() => {
|
|
103
|
+
if (!ref.current) return;
|
|
104
|
+
const animation = animate(ref.current, props, latest.current);
|
|
105
|
+
return () => animation.stop();
|
|
106
|
+
}, [key, latest]);
|
|
107
|
+
return ref;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Makes the element draggable and throwable. Returns a ref callback and the drag handle. */
|
|
111
|
+
export function useDraggable<T extends HTMLElement>(
|
|
112
|
+
options: DraggableOptions = {},
|
|
113
|
+
): [RefCallback<T>, Draggable | null] {
|
|
114
|
+
const [handle, setHandle] = useState<Draggable | null>(null);
|
|
115
|
+
const latest = useLatest(options);
|
|
116
|
+
const ref = useAttach<T>((element) => {
|
|
117
|
+
// Read options when events fire, so callbacks and snap points can change between renders.
|
|
118
|
+
const current = latest.current;
|
|
119
|
+
const drag = draggable(element, {
|
|
120
|
+
...current,
|
|
121
|
+
bounds: () => {
|
|
122
|
+
const b = latest.current.bounds;
|
|
123
|
+
return (typeof b === "function" ? b() : b) ?? {};
|
|
124
|
+
},
|
|
125
|
+
onStart: (info) => latest.current.onStart?.(info),
|
|
126
|
+
onMove: (info) => latest.current.onMove?.(info),
|
|
127
|
+
onEnd: (info) => latest.current.onEnd?.(info),
|
|
128
|
+
});
|
|
129
|
+
setHandle(drag);
|
|
130
|
+
return () => {
|
|
131
|
+
drag.destroy();
|
|
132
|
+
setHandle(null);
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
return [ref, handle];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* A running physics world whose walls are the container element. Bodies inside can be
|
|
140
|
+
* grabbed and thrown unless `interactive` is false.
|
|
141
|
+
*/
|
|
142
|
+
export function useWorld<T extends HTMLElement>(
|
|
143
|
+
options: Omit<WorldOptions, "bounds"> & { interactive?: boolean } = {},
|
|
144
|
+
): [RefCallback<T>, World] {
|
|
145
|
+
const latest = useLatest(options);
|
|
146
|
+
const [world] = useState(
|
|
147
|
+
() =>
|
|
148
|
+
new World({
|
|
149
|
+
...options,
|
|
150
|
+
onFrame: (w) => latest.current.onFrame?.(w),
|
|
151
|
+
onCollide: (a, b, speed) => latest.current.onCollide?.(a, b, speed),
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
const ref = useAttach<T>((element) => {
|
|
155
|
+
world.setBounds(element);
|
|
156
|
+
world.start();
|
|
157
|
+
const unbind = latest.current.interactive !== false ? world.bindPointer(element) : () => {};
|
|
158
|
+
return () => {
|
|
159
|
+
unbind();
|
|
160
|
+
world.stop();
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
return [ref, world];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Adds a body to the world that the element follows. Position the element absolutely at 0,0. */
|
|
167
|
+
export function useBody<T extends HTMLElement>(
|
|
168
|
+
world: World,
|
|
169
|
+
options: Omit<BodyOptions, "element">,
|
|
170
|
+
): [RefCallback<T>, Body | null] {
|
|
171
|
+
const [body, setBody] = useState<Body | null>(null);
|
|
172
|
+
const latest = useLatest(options);
|
|
173
|
+
const ref = useAttach<T>((element) => {
|
|
174
|
+
const added = world.add({ ...latest.current, element });
|
|
175
|
+
setBody(added);
|
|
176
|
+
return () => {
|
|
177
|
+
world.remove(added);
|
|
178
|
+
setBody(null);
|
|
179
|
+
};
|
|
180
|
+
});
|
|
181
|
+
return [ref, body];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export type {
|
|
185
|
+
AnimateOptions,
|
|
186
|
+
Body,
|
|
187
|
+
BodyOptions,
|
|
188
|
+
Draggable,
|
|
189
|
+
DraggableOptions,
|
|
190
|
+
PhysicsValue,
|
|
191
|
+
Props,
|
|
192
|
+
SpringOptions,
|
|
193
|
+
WorldOptions,
|
|
194
|
+
};
|