@canvas-glide/react 0.0.0-stage → 1.0.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.md +21 -0
- package/README.md +139 -2
- package/dist/GlideCanvas.d.ts +36 -0
- package/dist/Viewport.d.ts +27 -0
- package/dist/canvas-glide.d.ts +8 -0
- package/dist/canvas-glide.js +455 -0
- package/dist/renderer/Canvas.d.ts +4 -0
- package/dist/renderer/HtmlCanvas.d.ts +13 -0
- package/dist/renderer/ImageCanvas.d.ts +3 -0
- package/dist/renderer/base.d.ts +46 -0
- package/dist/useGlideCanvas.d.ts +18 -0
- package/dist/util/asyncMemo.d.ts +11 -0
- package/dist/util/constraint.d.ts +9 -0
- package/dist/util/event.d.ts +2 -0
- package/dist/util/image.d.ts +3 -0
- package/dist/util/useIdealScale.d.ts +20 -0
- package/package.json +71 -4
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vangel Trajkovski
|
|
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
CHANGED
|
@@ -1,3 +1,140 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @canvas-glide/react
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
React components and a hook for [canvas-glide](https://github.com/vrtexe/canvas-glide): pan, zoom and pinch-to-zoom for an image, or for any content you render, with mouse, touch, pen and wheel input.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@canvas-glide/react)
|
|
6
|
+
[](https://github.com/vrtexe/canvas-glide-react/blob/main/src/GlideCanvas.tsx)
|
|
7
|
+
[](LICENSE.md)
|
|
8
|
+
|
|
9
|
+
[Live demo](https://vrtexe.github.io/canvas-glide-react-demo/)
|
|
10
|
+
|
|
11
|
+
The package has two layers. `GlideCanvas` is a ready image viewer: give it a `Blob` and it shows the image fitted to its box, with drag to pan, wheel and pinch to zoom, and a ref for zoom buttons. `useGlideCanvas` and `Viewport` are the parts `GlideCanvas` is built from; use them to pan and zoom your own content. The package re-exports everything from `canvas-glide`, so one import covers the engine API too.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install @canvas-glide/react
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`canvas-glide` is a dependency and installs with it. `react` 19 is a peer dependency.
|
|
20
|
+
|
|
21
|
+
## Image viewer
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { useRef, useState } from 'react';
|
|
25
|
+
import { GlideCanvas, Renderer, type GlideCanvasRef } from '@canvas-glide/react';
|
|
26
|
+
|
|
27
|
+
export function Viewer() {
|
|
28
|
+
const [file, setFile] = useState<File | null>(null);
|
|
29
|
+
const viewer = useRef<GlideCanvasRef>(null);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<>
|
|
33
|
+
<input type="file" accept="image/*" onChange={(e) => setFile(e.target.files?.[0] ?? null)} />
|
|
34
|
+
<button onClick={() => viewer.current?.zoomIn()}>+</button>
|
|
35
|
+
<button onClick={() => viewer.current?.zoomOut()}>−</button>
|
|
36
|
+
<button onClick={() => viewer.current?.fit()}>Fit</button>
|
|
37
|
+
|
|
38
|
+
<div style={{ width: 800, height: 500 }}>
|
|
39
|
+
<GlideCanvas ref={viewer} renderer={Renderer.Image} image={file} />
|
|
40
|
+
</div>
|
|
41
|
+
</>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`GlideCanvas` fills its parent, so the parent needs a size. At scale 1 the image is fitted inside the box with its aspect ratio kept, and `fit()` returns to that view. By default the image can be panned until it just leaves the box, and no further.
|
|
47
|
+
|
|
48
|
+
## Renderers
|
|
49
|
+
|
|
50
|
+
`renderer` picks how the image is drawn. All three use the same engine and the same coordinates; a renderer calls `fit()` when its image has loaded, so switching renderers starts from the fitted view.
|
|
51
|
+
|
|
52
|
+
| Renderer | How it draws | `canvasConfig.children` |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| `Renderer.Canvas` (default) | Draws the decoded bitmap into a `<canvas>` sized in device pixels. | Not rendered. |
|
|
55
|
+
| `Renderer.Image` | Moves and scales an `<img>` with a CSS `transform`. | Rendered inside the transformed box, so overlays move and scale with the image. |
|
|
56
|
+
| `Renderer.Html` | Resizes the `<img>` to the zoomed size and scrolls its container, with no CSS transform, so the browser rasterizes the image at its displayed size. | Rendered after the `<img>` inside the scrolled container. |
|
|
57
|
+
|
|
58
|
+
## `GlideCanvas` props
|
|
59
|
+
|
|
60
|
+
| Prop | Type | Usage |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `image` | `Blob \| null` | The image to show. A `File` from an `<input type="file">` is a `Blob`. `null` clears the view. The image and HTML renderers load PNG, JPEG, GIF, WebP, AVIF and BMP through a `blob:` URL and every other type, such as SVG, through a `data:` URL, so a script inside an uploaded SVG cannot run in your page's origin. |
|
|
63
|
+
| `renderer` | `Renderer` | `Renderer.Canvas` (default), `Renderer.Image` or `Renderer.Html`. |
|
|
64
|
+
| `config` | engine options | The `canvas-glide` options except the two rect getters, which `GlideCanvas` supplies: `state`, `move`, `scale` and `constraints`. |
|
|
65
|
+
| `viewportConfig` | `ViewportOptions` | Props for the outer `<div>`; see [Viewport](#viewport). |
|
|
66
|
+
| `canvasConfig` | renderer options | `content` (props for the `<img>` or `<canvas>`), `container` (props for the wrapping element; for the canvas renderer, also the `<canvas>`), `children`, and `afterDraw(transform, ref)`, called after each frame. A renderer chosen at runtime cannot take `canvasConfig`. |
|
|
67
|
+
| `ref` | `GlideCanvasRef` | Every engine method (`zoomIn`, `zoomOut`, `zoomTo`, `panTo`, `fit`, `center`, `reset`, `getTransform`, ...) plus `viewportRef` and `canvasRef`. |
|
|
68
|
+
|
|
69
|
+
The engine methods are documented in the [canvas-glide README](https://github.com/vrtexe/canvas-glide#methods).
|
|
70
|
+
|
|
71
|
+
`GlideCanvas` redraws once per animation frame. It calls `config.state.onTransformChange` once per frame with a copy of the transform, while `onScaleChange` and `onOriginChange` run on every input event.
|
|
72
|
+
|
|
73
|
+
## Your own content
|
|
74
|
+
|
|
75
|
+
`useGlideCanvas` creates the engine, and `Viewport` is the element that receives the input. This example pans and zooms a 1200 by 800 pixel `<div>`:
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
import { useRef } from 'react';
|
|
79
|
+
import { useGlideCanvas, Viewport, type ViewportRef } from '@canvas-glide/react';
|
|
80
|
+
|
|
81
|
+
export function Board() {
|
|
82
|
+
const viewport = useRef<ViewportRef>(null);
|
|
83
|
+
const content = useRef<HTMLDivElement>(null);
|
|
84
|
+
|
|
85
|
+
const glide = useGlideCanvas({
|
|
86
|
+
getViewportRect: () => viewport.current?.getRect() ?? null,
|
|
87
|
+
getCanvasRect: () => ({ x: 0, y: 0, width: 1200, height: 800 }),
|
|
88
|
+
state: {
|
|
89
|
+
onTransformChange: ({ origin, scale }) => {
|
|
90
|
+
if (!content.current) return;
|
|
91
|
+
content.current.style.transform = `translate(${-origin.x * scale}px, ${-origin.y * scale}px) scale(${scale})`;
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<Viewport ref={viewport} lib={glide} style={{ width: 800, height: 500 }}>
|
|
98
|
+
<div ref={content} style={{ width: 1200, height: 800, transformOrigin: '0 0' }}>
|
|
99
|
+
Your content
|
|
100
|
+
</div>
|
|
101
|
+
</Viewport>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`useGlideCanvas` adds an origin limit that keeps the content reachable, and passing `config.constraints.origin` replaces it. The hook creates the engine once: callbacks and the rect getters always use the latest props, while `move`, `scale`, `constraints` and `state.initial` are read when the component mounts. To apply new values for those, remount the component, for example by changing its `key`.
|
|
107
|
+
|
|
108
|
+
## Viewport
|
|
109
|
+
|
|
110
|
+
`Viewport` renders a `<div>` with `overflow: hidden`, `touch-action: none`, `user-select: none` and `overscroll-behavior: contain`, and attaches the engine's pointer handlers and a non-passive wheel listener. It accepts every `<div>` prop, and these:
|
|
111
|
+
|
|
112
|
+
| Prop | Default | Usage |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| `lib` | none | The object returned by `useGlideCanvas`. |
|
|
115
|
+
| `onResize` | none | Called with the `ViewportRef` when the element's size changes. |
|
|
116
|
+
| `pointerDownEnabled`, `pointerMoveEnabled`, `pointerUpEnabled`, `pointerCancelEnabled` | `true` | `false` leaves that pointer event to the page. |
|
|
117
|
+
| `wheelEnabled` | `true` | `false` removes the wheel listener, so the wheel does not zoom. |
|
|
118
|
+
|
|
119
|
+
Its ref, `ViewportRef`, has `getElement()`, `getRect()` (the element's client rectangle, read on each call), `getClientWidth()` and `getClientHeight()` in CSS pixels, and `getWidth()` and `getHeight()` in device pixels.
|
|
120
|
+
|
|
121
|
+
## Development
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
npm install
|
|
125
|
+
npm test
|
|
126
|
+
npm run build
|
|
127
|
+
npm run lint
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`npm test` runs the Vitest suite in `test/` in jsdom, with the 2D context, `createImageBitmap` and `ResizeObserver` stubbed.
|
|
131
|
+
|
|
132
|
+
`npm run build` type-checks with `tsc -b` and writes `dist/` with Vite: `canvas-glide.js` (ES module) and the type declarations. `canvas-glide`, `react` and `react/jsx-runtime` stay external.
|
|
133
|
+
|
|
134
|
+
`canvas-glide` comes from npm. To work against an unreleased engine, build it and run `npm link ../lib` (the path to a local `canvas-glide` checkout); `npm install` restores the published version.
|
|
135
|
+
|
|
136
|
+
Publishing a GitHub release runs `.github/workflows/npm-publish.yml`: it lints, tests and builds the tag, sets `package.json` on the default branch to the release version, and stages the package on npm with trusted publishing; `npm stage approve` publishes it.
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
MIT, see [LICENSE.md](LICENSE.md).
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Ref } from 'react';
|
|
2
|
+
import { GlideCanvasInstance, GlideCanvasOptions } from './useGlideCanvas.ts';
|
|
3
|
+
import { ViewportRef, ViewportOptions } from './Viewport.tsx';
|
|
4
|
+
import { Renderer, CanvasOptions, RendererRef } from './renderer/base.tsx';
|
|
5
|
+
export type BaseGlideCanvasRef<R> = GlideCanvasInstance & {
|
|
6
|
+
viewportRef?: ViewportRef | null;
|
|
7
|
+
canvasRef?: R | null;
|
|
8
|
+
};
|
|
9
|
+
export type GlideCanvasRef<T extends Renderer = Renderer> = BaseGlideCanvasRef<RendererRef[T]>;
|
|
10
|
+
export type BaseGlideCanvasProps = {
|
|
11
|
+
config?: Omit<GlideCanvasOptions, "getViewportRect" | "getCanvasRect">;
|
|
12
|
+
viewportConfig?: ViewportOptions;
|
|
13
|
+
image?: Blob | null;
|
|
14
|
+
ref?: Ref<GlideCanvasRef>;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Discriminated union over `renderer`: call sites with a literal renderer get
|
|
18
|
+
* their exact canvasConfig checked. The last arm supports a runtime-decided
|
|
19
|
+
* renderer (e.g. tab switching); it cannot accept canvasConfig, because no
|
|
20
|
+
* single config shape fits all renderers. For fully custom rendering, compose
|
|
21
|
+
* the primitives directly: useGlideCanvas + Viewport + your own component.
|
|
22
|
+
*/
|
|
23
|
+
export type GlideCanvasProps = (BaseGlideCanvasProps & {
|
|
24
|
+
renderer?: typeof Renderer.Canvas;
|
|
25
|
+
canvasConfig?: CanvasOptions<typeof Renderer.Canvas>;
|
|
26
|
+
}) | (BaseGlideCanvasProps & {
|
|
27
|
+
renderer: typeof Renderer.Image;
|
|
28
|
+
canvasConfig?: CanvasOptions<typeof Renderer.Image>;
|
|
29
|
+
}) | (BaseGlideCanvasProps & {
|
|
30
|
+
renderer: typeof Renderer.Html;
|
|
31
|
+
canvasConfig?: CanvasOptions<typeof Renderer.Html>;
|
|
32
|
+
}) | (BaseGlideCanvasProps & {
|
|
33
|
+
renderer: Renderer;
|
|
34
|
+
canvasConfig?: undefined;
|
|
35
|
+
});
|
|
36
|
+
export declare function GlideCanvas(props: GlideCanvasProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { HTMLProps, PropsWithChildren, Ref } from 'react';
|
|
2
|
+
import { GlideCanvasInstance } from './useGlideCanvas.ts';
|
|
3
|
+
export type ViewportRef = {
|
|
4
|
+
getElement: () => HTMLDivElement | null;
|
|
5
|
+
/** Width in CSS pixels. */
|
|
6
|
+
getClientWidth: () => number;
|
|
7
|
+
/** Height in CSS pixels. */
|
|
8
|
+
getClientHeight: () => number;
|
|
9
|
+
getRect: () => DOMRect | null;
|
|
10
|
+
/** Width in device pixels (CSS pixels × devicePixelRatio). */
|
|
11
|
+
getWidth: () => number;
|
|
12
|
+
/** Height in device pixels (CSS pixels × devicePixelRatio). */
|
|
13
|
+
getHeight: () => number;
|
|
14
|
+
};
|
|
15
|
+
export type ViewportOptions = Omit<HTMLProps<HTMLDivElement>, "ref"> & {
|
|
16
|
+
onResize?: (ref: ViewportRef) => void;
|
|
17
|
+
pointerDownEnabled?: boolean;
|
|
18
|
+
pointerMoveEnabled?: boolean;
|
|
19
|
+
pointerUpEnabled?: boolean;
|
|
20
|
+
pointerCancelEnabled?: boolean;
|
|
21
|
+
wheelEnabled?: boolean;
|
|
22
|
+
};
|
|
23
|
+
export type ViewportProps = ViewportOptions & {
|
|
24
|
+
ref?: Ref<ViewportRef>;
|
|
25
|
+
lib?: GlideCanvasInstance;
|
|
26
|
+
};
|
|
27
|
+
export declare function Viewport({ ref, lib, children, style, className, onResize, pointerDownEnabled, pointerMoveEnabled, pointerUpEnabled, pointerCancelEnabled, wheelEnabled, ...rest }: PropsWithChildren<ViewportProps>): import("react").JSX.Element;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from 'canvas-glide';
|
|
2
|
+
export * from './useGlideCanvas.ts';
|
|
3
|
+
export * from './Viewport.tsx';
|
|
4
|
+
export * from './GlideCanvas.tsx';
|
|
5
|
+
export * from './renderer/base.tsx';
|
|
6
|
+
export * from './renderer/Canvas.tsx';
|
|
7
|
+
export * from './renderer/HtmlCanvas.tsx';
|
|
8
|
+
export * from './renderer/ImageCanvas.tsx';
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
"use no memo";
|
|
2
|
+
import { constrainTo as e, copyTransform as t, createEngine as n } from "canvas-glide";
|
|
3
|
+
import { useEffect as r, useImperativeHandle as i, useMemo as a, useRef as o, useState as s } from "react";
|
|
4
|
+
import { jsx as c, jsxs as l } from "react/jsx-runtime";
|
|
5
|
+
export * from "canvas-glide";
|
|
6
|
+
function u(e) {
|
|
7
|
+
return (t) => {
|
|
8
|
+
let n = e.getViewportRect(), r = e.getCanvasRect();
|
|
9
|
+
return !n || !r || r.width <= 0 ? t : d(t, e.getScale(), n, r);
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function d(t, n, r, i) {
|
|
13
|
+
let a = r.width / n, o = r.height / n;
|
|
14
|
+
return {
|
|
15
|
+
x: e(t.x, -a + 0, i.width - 0),
|
|
16
|
+
y: e(t.y, -o + 0, i.height - 0)
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/useGlideCanvas.ts
|
|
21
|
+
function f(e) {
|
|
22
|
+
let t = o(null), r = o(e);
|
|
23
|
+
if (r.current = e, t.current === null) {
|
|
24
|
+
let i = u({
|
|
25
|
+
getViewportRect: () => r.current.getViewportRect(),
|
|
26
|
+
getCanvasRect: () => r.current.getCanvasRect(),
|
|
27
|
+
getScale: () => t.current.getScale()
|
|
28
|
+
});
|
|
29
|
+
t.current = n({
|
|
30
|
+
...e,
|
|
31
|
+
constraints: {
|
|
32
|
+
...e.constraints,
|
|
33
|
+
origin: e.constraints?.origin ?? {
|
|
34
|
+
enabled: !0,
|
|
35
|
+
constrain: i
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
getViewportRect: () => r.current.getViewportRect(),
|
|
39
|
+
getCanvasRect: () => r.current.getCanvasRect(),
|
|
40
|
+
state: {
|
|
41
|
+
...e.state,
|
|
42
|
+
onTransformChange: (e) => r.current.state?.onTransformChange?.(e),
|
|
43
|
+
onScaleChange: (e) => r.current.state?.onScaleChange?.(e),
|
|
44
|
+
onOriginChange: (e) => r.current.state?.onOriginChange?.(e)
|
|
45
|
+
},
|
|
46
|
+
move: {
|
|
47
|
+
...e.move,
|
|
48
|
+
onIsMovingChange: (e) => r.current.move?.onIsMovingChange?.(e)
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
let i = t.current;
|
|
53
|
+
return a(() => ({
|
|
54
|
+
...i,
|
|
55
|
+
engine: i,
|
|
56
|
+
onPointerDown: i.event.onPointerDown,
|
|
57
|
+
onPointerMove: i.event.onPointerMove,
|
|
58
|
+
onPointerUp: i.event.onPointerUp,
|
|
59
|
+
onWheel: i.event.onWheel
|
|
60
|
+
}), [i]);
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/util/event.ts
|
|
64
|
+
function p(e, t, n) {
|
|
65
|
+
if (e) return m("wheel", e, t, n);
|
|
66
|
+
}
|
|
67
|
+
function m(e, t, n, r) {
|
|
68
|
+
if (t) return t.addEventListener(e, n, r), () => {
|
|
69
|
+
t.removeEventListener(e, n, r);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/Viewport.tsx
|
|
74
|
+
var h = {
|
|
75
|
+
width: "100%",
|
|
76
|
+
height: "100%",
|
|
77
|
+
position: "relative",
|
|
78
|
+
background: "transparent",
|
|
79
|
+
overflow: "hidden",
|
|
80
|
+
userSelect: "none",
|
|
81
|
+
touchAction: "none",
|
|
82
|
+
overscrollBehavior: "contain"
|
|
83
|
+
};
|
|
84
|
+
function g({ ref: e, lib: t, children: n, style: a, className: s, onResize: l, pointerDownEnabled: u = !0, pointerMoveEnabled: d = !0, pointerUpEnabled: f = !0, pointerCancelEnabled: m = !0, wheelEnabled: g = !0, ..._ }) {
|
|
85
|
+
let v = o(null), y = o(l);
|
|
86
|
+
y.current = l;
|
|
87
|
+
let b = {
|
|
88
|
+
...h,
|
|
89
|
+
...a
|
|
90
|
+
};
|
|
91
|
+
function x() {
|
|
92
|
+
return v?.current ? v.current.clientWidth * (window.devicePixelRatio || 1) : 0;
|
|
93
|
+
}
|
|
94
|
+
function S() {
|
|
95
|
+
return v?.current ? v.current.clientHeight * (window.devicePixelRatio || 1) : 0;
|
|
96
|
+
}
|
|
97
|
+
r(() => {
|
|
98
|
+
if (t?.onWheel !== void 0 && g !== !1) return p(v.current, t.onWheel, { passive: !1 });
|
|
99
|
+
}, [t?.onWheel, g]), r(() => {
|
|
100
|
+
if (!v.current) return;
|
|
101
|
+
let e = new ResizeObserver(() => y.current?.(C.current));
|
|
102
|
+
return e.observe(v.current), () => e.disconnect();
|
|
103
|
+
}, []);
|
|
104
|
+
let C = o({
|
|
105
|
+
getElement: () => v.current,
|
|
106
|
+
getClientWidth: () => v.current?.clientWidth || 0,
|
|
107
|
+
getClientHeight: () => v.current?.clientHeight || 0,
|
|
108
|
+
getWidth: x,
|
|
109
|
+
getHeight: S,
|
|
110
|
+
getRect: () => v.current?.getBoundingClientRect() ?? null
|
|
111
|
+
});
|
|
112
|
+
return i(e, () => C.current), /* @__PURE__ */ c("div", {
|
|
113
|
+
ref: v,
|
|
114
|
+
className: s,
|
|
115
|
+
style: b,
|
|
116
|
+
onPointerDown: u ? t?.onPointerDown : void 0,
|
|
117
|
+
onPointerMove: d ? t?.onPointerMove : void 0,
|
|
118
|
+
onPointerUp: f ? t?.onPointerUp : void 0,
|
|
119
|
+
onPointerCancel: m ? t?.onPointerUp : void 0,
|
|
120
|
+
..._,
|
|
121
|
+
children: n
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
//#endregion
|
|
125
|
+
//#region src/util/asyncMemo.ts
|
|
126
|
+
function _(e, t, n) {
|
|
127
|
+
let [i, a] = s(null);
|
|
128
|
+
return r(() => {
|
|
129
|
+
let t = !0, r = null;
|
|
130
|
+
return e().then((e) => {
|
|
131
|
+
r = e, t ? a(e) : e && (n?.(e), r = null);
|
|
132
|
+
}).catch(() => {
|
|
133
|
+
t && a(null);
|
|
134
|
+
}), () => {
|
|
135
|
+
t = !1, r &&= (n?.(r), null), a(null);
|
|
136
|
+
};
|
|
137
|
+
}, t), i;
|
|
138
|
+
}
|
|
139
|
+
//#endregion
|
|
140
|
+
//#region src/util/image.ts
|
|
141
|
+
var v = /^image\/(png|jpeg|gif|webp|avif|bmp)$/i;
|
|
142
|
+
function y(e) {
|
|
143
|
+
return e ? createImageBitmap(e) : Promise.resolve(null);
|
|
144
|
+
}
|
|
145
|
+
function b(e) {
|
|
146
|
+
return e ? v.test(e.type) ? Promise.resolve(URL.createObjectURL(e)) : S(e) : Promise.resolve(null);
|
|
147
|
+
}
|
|
148
|
+
function x(e) {
|
|
149
|
+
e?.startsWith("blob:") && URL.revokeObjectURL(e);
|
|
150
|
+
}
|
|
151
|
+
function S(e) {
|
|
152
|
+
return new Promise((t, n) => {
|
|
153
|
+
let r = new FileReader();
|
|
154
|
+
r.onload = () => t(r.result), r.onerror = (e) => n(e), r.readAsDataURL(e);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/util/useIdealScale.ts
|
|
159
|
+
function C(e, t) {
|
|
160
|
+
let n = o(1);
|
|
161
|
+
function r() {
|
|
162
|
+
let r = e?.current, i = t();
|
|
163
|
+
if (!r || !i?.width || !i?.height) {
|
|
164
|
+
n.current = 1;
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
n.current = Math.min(r.getClientWidth() / i.width, r.getClientHeight() / i.height);
|
|
168
|
+
}
|
|
169
|
+
function i(e) {
|
|
170
|
+
let r = t();
|
|
171
|
+
return {
|
|
172
|
+
x: e?.offsetLeft || 0,
|
|
173
|
+
y: e?.offsetTop || 0,
|
|
174
|
+
width: (r?.width || 0) * n.current,
|
|
175
|
+
height: (r?.height || 0) * n.current
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
idealScale: n,
|
|
180
|
+
updateIdealScale: r,
|
|
181
|
+
getImageRect: i
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
//#endregion
|
|
185
|
+
//#region src/renderer/Canvas.tsx
|
|
186
|
+
var w = {
|
|
187
|
+
position: "absolute",
|
|
188
|
+
top: 0,
|
|
189
|
+
left: 0,
|
|
190
|
+
height: "100%",
|
|
191
|
+
width: "100%"
|
|
192
|
+
};
|
|
193
|
+
function T({ viewportRef: e, ref: t, afterDraw: n, image: a, lib: l, container: u, content: d }) {
|
|
194
|
+
let { style: f, ref: p, ...m } = u ?? {}, { style: h, ref: g, ...v } = d ?? {}, b = {
|
|
195
|
+
...w,
|
|
196
|
+
...f,
|
|
197
|
+
...h
|
|
198
|
+
}, x = _(() => y(a), [a], (e) => e?.close()), S = o(null), [T] = s(() => document.createElement("canvas")), E = o(null);
|
|
199
|
+
function D(e) {
|
|
200
|
+
e.imageSmoothingEnabled = !0, e.imageSmoothingQuality = "high";
|
|
201
|
+
}
|
|
202
|
+
r(() => {
|
|
203
|
+
E.current || (E.current = S?.current?.getContext("2d") || null, E.current && D(E.current));
|
|
204
|
+
}, []);
|
|
205
|
+
let { idealScale: O, updateIdealScale: k, getImageRect: A } = C(e, () => ({
|
|
206
|
+
width: T.width,
|
|
207
|
+
height: T.height
|
|
208
|
+
}));
|
|
209
|
+
r(() => {
|
|
210
|
+
if (!x) {
|
|
211
|
+
T.width = 0, T.height = 0, j();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
P(x), I(e?.current), k(), l?.fit(), M(l?.stateService.getTransform());
|
|
215
|
+
}, [x]);
|
|
216
|
+
function j() {
|
|
217
|
+
let e = S.current, t = E.current;
|
|
218
|
+
!e || !t || (t.resetTransform(), t.clearRect(0, 0, e.width, e.height));
|
|
219
|
+
}
|
|
220
|
+
function M(t) {
|
|
221
|
+
if (!S.current || !t || !x) return;
|
|
222
|
+
let r = S.current, i = t.scale, a = t.origin.x, o = t.origin.y, s = E.current;
|
|
223
|
+
if (!s) return;
|
|
224
|
+
let c = e?.current;
|
|
225
|
+
c && (r.width !== Math.floor(c.getWidth()) || r.height !== Math.floor(c.getHeight())) && F(c.getWidth(), c.getHeight());
|
|
226
|
+
let l = window.devicePixelRatio || 1;
|
|
227
|
+
j();
|
|
228
|
+
let u = i * l;
|
|
229
|
+
s.setTransform(u, 0, 0, u, -a * u, -o * u), s.drawImage(T, 0, 0, T.width * O.current, T.height * O.current), n?.(t, L);
|
|
230
|
+
}
|
|
231
|
+
function N() {
|
|
232
|
+
M(l?.stateService.getTransform());
|
|
233
|
+
}
|
|
234
|
+
function P(e) {
|
|
235
|
+
T.width = e.width, T.height = e.height, T.getContext("2d")?.drawImage(e, 0, 0);
|
|
236
|
+
}
|
|
237
|
+
function F(e, t) {
|
|
238
|
+
S.current && (S.current.width = e, S.current.height = t, E.current && D(E.current), k());
|
|
239
|
+
}
|
|
240
|
+
function I(e) {
|
|
241
|
+
e && (F(e.getWidth(), e.getHeight()), N());
|
|
242
|
+
}
|
|
243
|
+
let L = {
|
|
244
|
+
getContainerRef: () => S.current,
|
|
245
|
+
getContentRef: () => S.current,
|
|
246
|
+
draw: M,
|
|
247
|
+
handleViewportResize: I,
|
|
248
|
+
getImageRect: () => A(S.current)
|
|
249
|
+
};
|
|
250
|
+
return i(t, () => L), /* @__PURE__ */ c("canvas", {
|
|
251
|
+
style: b,
|
|
252
|
+
ref: S,
|
|
253
|
+
...m,
|
|
254
|
+
...v
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
//#endregion
|
|
258
|
+
//#region src/renderer/ImageCanvas.tsx
|
|
259
|
+
var E = {
|
|
260
|
+
position: "relative",
|
|
261
|
+
transformOrigin: "top left"
|
|
262
|
+
}, D = {
|
|
263
|
+
display: "block",
|
|
264
|
+
height: "100%",
|
|
265
|
+
width: "100%"
|
|
266
|
+
};
|
|
267
|
+
function O({ viewportRef: e, afterDraw: t, children: n, ref: a, image: u, lib: d, container: f, content: p }) {
|
|
268
|
+
let { style: m, ref: h, ...g } = p ?? {}, { style: v, ref: y, ...S } = f ?? {}, w = o(null), T = o(null), [O, k] = s("hidden"), A = _(() => b(u), [u], x), { idealScale: j, updateIdealScale: M, getImageRect: N } = C(e, () => T.current ? {
|
|
269
|
+
width: T.current.naturalWidth,
|
|
270
|
+
height: T.current.naturalHeight
|
|
271
|
+
} : null);
|
|
272
|
+
function P(e) {
|
|
273
|
+
!e || !w.current || (w.current.style.transform = `scale(${e.scale}) translate(${-e.origin.x}px, ${-e.origin.y}px)`, t?.(e, B));
|
|
274
|
+
}
|
|
275
|
+
function F() {
|
|
276
|
+
P(d?.stateService.getTransform());
|
|
277
|
+
}
|
|
278
|
+
function I() {
|
|
279
|
+
if (!w.current || !T.current) return;
|
|
280
|
+
let { naturalWidth: e, naturalHeight: t } = T.current;
|
|
281
|
+
w.current.style.width = `${e * j.current}px`, w.current.style.height = `${t * j.current}px`;
|
|
282
|
+
}
|
|
283
|
+
function L(e) {
|
|
284
|
+
e && (M(), I(), F());
|
|
285
|
+
}
|
|
286
|
+
function R() {
|
|
287
|
+
M(), I(), d?.fit(), F(), k("visible");
|
|
288
|
+
}
|
|
289
|
+
r(() => {
|
|
290
|
+
T.current?.complete && T.current.naturalWidth && R();
|
|
291
|
+
}, [A]);
|
|
292
|
+
let z = {
|
|
293
|
+
...E,
|
|
294
|
+
...v
|
|
295
|
+
}, B = {
|
|
296
|
+
getContainerRef: () => w.current,
|
|
297
|
+
getContentRef: () => T.current,
|
|
298
|
+
draw: P,
|
|
299
|
+
handleViewportResize: L,
|
|
300
|
+
getImageRect: () => N(w.current)
|
|
301
|
+
};
|
|
302
|
+
i(a, () => B);
|
|
303
|
+
let V = {
|
|
304
|
+
...D,
|
|
305
|
+
visibility: O,
|
|
306
|
+
...m
|
|
307
|
+
};
|
|
308
|
+
return /* @__PURE__ */ l("div", {
|
|
309
|
+
ref: w,
|
|
310
|
+
style: z,
|
|
311
|
+
...S,
|
|
312
|
+
children: [/* @__PURE__ */ c("img", {
|
|
313
|
+
hidden: !A,
|
|
314
|
+
style: V,
|
|
315
|
+
ref: T,
|
|
316
|
+
src: A || void 0,
|
|
317
|
+
draggable: !1,
|
|
318
|
+
onLoad: R,
|
|
319
|
+
...g
|
|
320
|
+
}), n]
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
//#endregion
|
|
324
|
+
//#region src/renderer/HtmlCanvas.tsx
|
|
325
|
+
function k({ viewportRef: e, afterDraw: t, children: n, ref: r, image: a, lib: s, container: u, content: d }) {
|
|
326
|
+
let { style: f, ref: p, ...m } = u ?? {}, { style: h, ref: g, ...v } = d ?? {}, y = o(null), S = o(null), w = _(() => b(a), [a], x), { idealScale: T, updateIdealScale: E, getImageRect: D } = C(e, () => S.current ? {
|
|
327
|
+
width: S.current.naturalWidth,
|
|
328
|
+
height: S.current.naturalHeight
|
|
329
|
+
} : null);
|
|
330
|
+
function O(n) {
|
|
331
|
+
let r = y.current, i = S.current, a = e?.current;
|
|
332
|
+
if (!n || !r || !i?.naturalWidth || !a) return;
|
|
333
|
+
let { scale: o, origin: s } = n, c = i.naturalWidth * T.current * o, l = i.naturalHeight * T.current * o, u = a.getClientWidth(), d = a.getClientHeight();
|
|
334
|
+
i.style.width = `${c}px`, i.style.height = `${l}px`, i.style.padding = `${d}px ${u}px`, r.scrollTo(u + s.x * o, d + s.y * o), t?.(n, M);
|
|
335
|
+
}
|
|
336
|
+
function k() {
|
|
337
|
+
O(s?.stateService.getTransform());
|
|
338
|
+
}
|
|
339
|
+
function A() {
|
|
340
|
+
E(), k();
|
|
341
|
+
}
|
|
342
|
+
function j() {
|
|
343
|
+
E(), s?.fit(), k();
|
|
344
|
+
}
|
|
345
|
+
let M = {
|
|
346
|
+
getContainerRef: () => y.current,
|
|
347
|
+
getContentRef: () => S.current,
|
|
348
|
+
draw: O,
|
|
349
|
+
handleViewportResize: A,
|
|
350
|
+
getImageRect: () => D(y.current)
|
|
351
|
+
};
|
|
352
|
+
i(r, () => M);
|
|
353
|
+
let N = {
|
|
354
|
+
width: "100%",
|
|
355
|
+
height: "100%",
|
|
356
|
+
overflow: "hidden",
|
|
357
|
+
position: "relative",
|
|
358
|
+
...f
|
|
359
|
+
}, P = {
|
|
360
|
+
display: "block",
|
|
361
|
+
boxSizing: "content-box",
|
|
362
|
+
...h
|
|
363
|
+
};
|
|
364
|
+
return /* @__PURE__ */ l("div", {
|
|
365
|
+
style: N,
|
|
366
|
+
ref: y,
|
|
367
|
+
...m,
|
|
368
|
+
children: [/* @__PURE__ */ c("img", {
|
|
369
|
+
ref: S,
|
|
370
|
+
src: w || void 0,
|
|
371
|
+
style: P,
|
|
372
|
+
draggable: !1,
|
|
373
|
+
onLoad: j,
|
|
374
|
+
...v
|
|
375
|
+
}), n]
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
//#endregion
|
|
379
|
+
//#region src/renderer/base.tsx
|
|
380
|
+
var A = Object.freeze({
|
|
381
|
+
Canvas: "canvas",
|
|
382
|
+
Image: "image",
|
|
383
|
+
Html: "html"
|
|
384
|
+
});
|
|
385
|
+
//#endregion
|
|
386
|
+
//#region src/GlideCanvas.tsx
|
|
387
|
+
function j(e) {
|
|
388
|
+
let n = o(null), a = o(null), l = o(null), u = (e) => {
|
|
389
|
+
a.current = e;
|
|
390
|
+
}, [d, p] = s(!1), { config: m } = e, h = f({
|
|
391
|
+
...m,
|
|
392
|
+
getViewportRect: () => n.current?.getRect() || null,
|
|
393
|
+
getCanvasRect: () => a.current?.getImageRect?.() || null,
|
|
394
|
+
move: {
|
|
395
|
+
...m?.move,
|
|
396
|
+
onIsMovingChange(e) {
|
|
397
|
+
p(e), m?.move?.onIsMovingChange?.(e);
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
state: {
|
|
401
|
+
...m?.state,
|
|
402
|
+
onTransformChange() {
|
|
403
|
+
v();
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
function _(t) {
|
|
408
|
+
a.current?.handleViewportResize?.(t), e.viewportConfig?.onResize?.(t);
|
|
409
|
+
}
|
|
410
|
+
i(e.ref, () => ({
|
|
411
|
+
viewportRef: n.current,
|
|
412
|
+
canvasRef: a.current,
|
|
413
|
+
...h
|
|
414
|
+
}));
|
|
415
|
+
function v() {
|
|
416
|
+
l.current === null && (l.current = requestAnimationFrame(() => {
|
|
417
|
+
l.current = null;
|
|
418
|
+
let e = h.stateService.getTransform();
|
|
419
|
+
a.current?.draw?.(e), m?.state?.onTransformChange?.(t(e));
|
|
420
|
+
}));
|
|
421
|
+
}
|
|
422
|
+
return r(() => () => {
|
|
423
|
+
l.current !== null && cancelAnimationFrame(l.current);
|
|
424
|
+
}, []), /* @__PURE__ */ c(g, {
|
|
425
|
+
ref: n,
|
|
426
|
+
lib: h,
|
|
427
|
+
...e.viewportConfig,
|
|
428
|
+
style: {
|
|
429
|
+
cursor: d ? "grabbing" : h.isPanEnabled() ? "grab" : "default",
|
|
430
|
+
...e.viewportConfig?.style
|
|
431
|
+
},
|
|
432
|
+
onResize: _,
|
|
433
|
+
children: e.renderer === A.Image ? /* @__PURE__ */ c(O, {
|
|
434
|
+
viewportRef: n,
|
|
435
|
+
ref: u,
|
|
436
|
+
lib: h,
|
|
437
|
+
image: e.image,
|
|
438
|
+
...e.canvasConfig
|
|
439
|
+
}) : e.renderer === A.Html ? /* @__PURE__ */ c(k, {
|
|
440
|
+
viewportRef: n,
|
|
441
|
+
ref: u,
|
|
442
|
+
lib: h,
|
|
443
|
+
image: e.image,
|
|
444
|
+
...e.canvasConfig
|
|
445
|
+
}) : /* @__PURE__ */ c(T, {
|
|
446
|
+
viewportRef: n,
|
|
447
|
+
ref: u,
|
|
448
|
+
lib: h,
|
|
449
|
+
image: e.image,
|
|
450
|
+
...e.canvasConfig
|
|
451
|
+
})
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
//#endregion
|
|
455
|
+
export { T as Canvas, j as GlideCanvas, k as HtmlCanvas, O as ImageCanvas, A as Renderer, g as Viewport, f as useGlideCanvas };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { Renderer, RendererConfig } from './base.tsx';
|
|
2
|
+
export type CanvasRendererProps = RendererConfig<typeof Renderer.Canvas>;
|
|
3
|
+
export declare function Canvas({ viewportRef, ref, afterDraw, image, lib, container, content }: CanvasRendererProps): import("react").JSX.Element;
|
|
4
|
+
export default Canvas;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Renderer, RendererConfig } from './base.tsx';
|
|
2
|
+
export type HtmlRendererProps = RendererConfig<typeof Renderer.Html>;
|
|
3
|
+
/**
|
|
4
|
+
* Scroll-based renderer: no CSS transforms and no <canvas>.
|
|
5
|
+
* Zoom resizes the <img> element itself (the browser re-rasterizes at the
|
|
6
|
+
* layout size, so it stays sharp — unlike transformed layers on Safari) and
|
|
7
|
+
* pan is a programmatic scroll of an overflow:hidden box (an element does not
|
|
8
|
+
* need visible scrollbars to be scrollable via scrollTo). The img's padding
|
|
9
|
+
* carries one viewport of slack on every side — a scroll range only exists
|
|
10
|
+
* where a descendant box occupies it, and padding is part of the img's border
|
|
11
|
+
* box — so the image can be panned fully off-screen in every direction.
|
|
12
|
+
*/
|
|
13
|
+
export declare function HtmlCanvas({ viewportRef, afterDraw, children, ref, image, lib, container, content, }: HtmlRendererProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { Renderer, RendererConfig } from './base.tsx';
|
|
2
|
+
export type ImageRendererProps = RendererConfig<typeof Renderer.Image>;
|
|
3
|
+
export declare function ImageCanvas({ viewportRef, afterDraw, children, ref, image, lib, container, content, }: ImageRendererProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Rect, Transform } from 'canvas-glide';
|
|
2
|
+
import { ViewportRef } from '../Viewport.tsx';
|
|
3
|
+
import { HTMLProps, ReactNode, Ref, RefObject } from 'react';
|
|
4
|
+
import { GlideCanvasInstance } from '../useGlideCanvas.ts';
|
|
5
|
+
export type CanvasRef<T, C> = {
|
|
6
|
+
getContainerRef?: () => C | null;
|
|
7
|
+
getContentRef?: () => T | null;
|
|
8
|
+
getImageRect?: () => Rect;
|
|
9
|
+
handleViewportResize?: (viewport: ViewportRef) => void;
|
|
10
|
+
draw?: (transform?: Transform) => void;
|
|
11
|
+
};
|
|
12
|
+
export type RendererComponentProps<T> = HTMLProps<T> & {
|
|
13
|
+
className?: string;
|
|
14
|
+
style?: React.CSSProperties;
|
|
15
|
+
};
|
|
16
|
+
export type RendererProps<R extends Renderer, T = unknown, C = unknown> = {
|
|
17
|
+
content?: RendererComponentProps<T>;
|
|
18
|
+
container?: RendererComponentProps<C>;
|
|
19
|
+
children?: ReactNode | undefined;
|
|
20
|
+
afterDraw?: (transform?: Transform, data?: RendererRef[R]) => void;
|
|
21
|
+
};
|
|
22
|
+
export type RendererPropsMap = {
|
|
23
|
+
[Renderer.Canvas]: RendererProps<typeof Renderer.Canvas, HTMLCanvasElement, HTMLCanvasElement>;
|
|
24
|
+
[Renderer.Image]: RendererProps<typeof Renderer.Image, HTMLImageElement, HTMLDivElement>;
|
|
25
|
+
[Renderer.Html]: RendererProps<typeof Renderer.Html, HTMLImageElement, HTMLDivElement>;
|
|
26
|
+
};
|
|
27
|
+
export type BaseRenderConfig<R> = {
|
|
28
|
+
viewportRef?: RefObject<ViewportRef | null>;
|
|
29
|
+
image?: Blob | null;
|
|
30
|
+
lib?: GlideCanvasInstance;
|
|
31
|
+
ref?: Ref<R>;
|
|
32
|
+
};
|
|
33
|
+
export type RendererConfig<T extends Renderer> = RendererPropsMap[T] & BaseRenderConfig<RendererRef[T]>;
|
|
34
|
+
export type RendererRef = {
|
|
35
|
+
[Renderer.Canvas]: CanvasRef<HTMLCanvasElement, HTMLCanvasElement>;
|
|
36
|
+
[Renderer.Image]: CanvasRef<HTMLImageElement, HTMLDivElement>;
|
|
37
|
+
[Renderer.Html]: CanvasRef<HTMLImageElement, HTMLDivElement>;
|
|
38
|
+
};
|
|
39
|
+
export type Renderer = (typeof Renderer)[keyof typeof Renderer];
|
|
40
|
+
export declare const Renderer: Readonly<{
|
|
41
|
+
readonly Canvas: "canvas";
|
|
42
|
+
readonly Image: "image";
|
|
43
|
+
/** Renders the image as a resized, scrolled `<img>`, not arbitrary HTML. */
|
|
44
|
+
readonly Html: "html";
|
|
45
|
+
}>;
|
|
46
|
+
export type CanvasOptions<T extends Renderer> = RendererPropsMap[T];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { PointerEventHandler, WheelEventHandler as ReactWheelEventHandler } from 'react';
|
|
2
|
+
import { CanvasEngine, CanvasEngineOptions } from 'canvas-glide';
|
|
3
|
+
export type GlideCanvasOptions = CanvasEngineOptions;
|
|
4
|
+
export type BaseElement = Element;
|
|
5
|
+
export type DomEventListener<T extends BaseElement, K extends keyof E, E extends HTMLElementEventMap = HTMLElementEventMap> = (this: T, ev: E[K]) => unknown;
|
|
6
|
+
export type PointerDownEventHandler<T extends BaseElement> = DomEventListener<T, "pointerdown">;
|
|
7
|
+
export type PointerUpEventHandler<T extends BaseElement> = DomEventListener<T, "pointerup">;
|
|
8
|
+
export type PointerMoveEventHandler<T extends BaseElement> = DomEventListener<T, "pointermove">;
|
|
9
|
+
export type WheelEventHandler<T extends BaseElement> = DomEventListener<T, "wheel">;
|
|
10
|
+
export type GlideCanvasInstance = CanvasEngine & {
|
|
11
|
+
engine: CanvasEngine;
|
|
12
|
+
onPointerDown: PointerEventHandler<Element> & PointerDownEventHandler<Element>;
|
|
13
|
+
onPointerMove: PointerEventHandler<Element> & PointerMoveEventHandler<Element>;
|
|
14
|
+
onPointerUp: PointerEventHandler<Element> & PointerUpEventHandler<Element>;
|
|
15
|
+
onWheel: ReactWheelEventHandler<HTMLDivElement> & WheelEventHandler<Element>;
|
|
16
|
+
};
|
|
17
|
+
/** Creates the engine once: callbacks and rect getters follow the latest config, all other options are read at mount. */
|
|
18
|
+
export declare function useGlideCanvas(config: GlideCanvasOptions): GlideCanvasInstance;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { DependencyList } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Async-derived value tied to a dependency list.
|
|
4
|
+
*
|
|
5
|
+
* `dispose` releases a produced value when it is replaced, unmounted, or
|
|
6
|
+
* arrives after its generation was superseded (e.g. `(bitmap) => bitmap.close()`).
|
|
7
|
+
* Each effect run only ever disposes the value IT produced — never a newer
|
|
8
|
+
* generation's. A rejected `asyncFn` resolves the value to null instead of
|
|
9
|
+
* leaking an unhandled rejection.
|
|
10
|
+
*/
|
|
11
|
+
export declare function useAsyncMemo<T>(asyncFn: () => Promise<T>, dependencies: DependencyList, dispose?: (value: T) => void): T | null;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Point, PointConstrainer, Rect } from 'canvas-glide';
|
|
2
|
+
export declare const CONSTRAINT_THRESHOLD = 0;
|
|
3
|
+
export type OriginConstrainerConfig = {
|
|
4
|
+
getViewportRect: () => Rect | null;
|
|
5
|
+
getCanvasRect: () => Rect | null;
|
|
6
|
+
getScale: () => number;
|
|
7
|
+
};
|
|
8
|
+
export declare function createConstrainer(config: OriginConstrainerConfig): PointConstrainer;
|
|
9
|
+
export declare function constrainOrigin(origin: Point, scale: number, viewport: Rect, canvas: Rect): Point;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare function attachWheelEvent<T extends HTMLElement>(element: T | null, handler: (e: WheelEvent) => void, options?: AddEventListenerOptions): (() => void) | undefined;
|
|
2
|
+
export declare function attachEvent<K extends keyof HTMLElementEventMap, T extends HTMLElement | Window>(event: K, element: T | null, handler: (this: T, ev: HTMLElementEventMap[K]) => any, options?: AddEventListenerOptions | boolean): (() => void) | undefined;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function createImageBitmapSafe(blob?: Blob | null | undefined): Promise<ImageBitmap | null>;
|
|
2
|
+
export declare function createImageUrlSafe(blob?: Blob | null | undefined): Promise<string | null>;
|
|
3
|
+
export declare function revokeImageUrl(url: string | null): void;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
import { Rect } from 'canvas-glide';
|
|
3
|
+
import { ViewportRef } from '../Viewport.tsx';
|
|
4
|
+
export type ContentSize = {
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Shared contain-fit state for renderers: `idealScale` maps the content's
|
|
10
|
+
* natural size to the viewport at engine scale 1, so engine origins are
|
|
11
|
+
* expressed in the same scale-1 frame for every renderer.
|
|
12
|
+
*/
|
|
13
|
+
export declare function useIdealScale(viewportRef: RefObject<ViewportRef | null> | undefined, getContentSize: () => ContentSize | null): {
|
|
14
|
+
idealScale: RefObject<number>;
|
|
15
|
+
updateIdealScale: () => void;
|
|
16
|
+
getImageRect: (element: {
|
|
17
|
+
offsetLeft: number;
|
|
18
|
+
offsetTop: number;
|
|
19
|
+
} | null) => Rect;
|
|
20
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,73 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canvas-glide/react",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "React components and hook for canvas-glide: pan, zoom and pinch-to-zoom for images on a canvas, an img or a scrolled view, with mouse, touch, pen and wheel input.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react",
|
|
7
|
+
"react-component",
|
|
8
|
+
"pan-zoom",
|
|
9
|
+
"panzoom",
|
|
10
|
+
"pinch-zoom",
|
|
11
|
+
"pinch-to-zoom",
|
|
12
|
+
"zoom",
|
|
13
|
+
"pan",
|
|
14
|
+
"image-viewer",
|
|
15
|
+
"image-zoom",
|
|
16
|
+
"canvas",
|
|
17
|
+
"gestures",
|
|
18
|
+
"touch",
|
|
19
|
+
"typescript"
|
|
20
|
+
],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"homepage": "https://vrtexe.github.io/canvas-glide-react-demo/",
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/vrtexe/canvas-glide-react/issues"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/vrtexe/canvas-glide-react.git"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"type": "module",
|
|
34
|
+
"scripts": {
|
|
35
|
+
"dev": "vite",
|
|
36
|
+
"build": "tsc -b && vite build",
|
|
37
|
+
"build:dev": "tsc -b && vite build --watch",
|
|
38
|
+
"lint": "oxlint",
|
|
39
|
+
"test": "vitest run",
|
|
40
|
+
"preview": "vite preview"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"canvas-glide": "^1.0.1"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"react": "^19.0.0",
|
|
47
|
+
"react-dom": "^19.0.0"
|
|
48
|
+
},
|
|
49
|
+
"main": "./dist/canvas-glide.js",
|
|
50
|
+
"types": "./dist/canvas-glide.d.ts",
|
|
51
|
+
"files": [
|
|
52
|
+
"dist"
|
|
53
|
+
],
|
|
54
|
+
"exports": {
|
|
55
|
+
".": {
|
|
56
|
+
"types": "./dist/canvas-glide.d.ts",
|
|
57
|
+
"default": "./dist/canvas-glide.js"
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@types/node": "^24.13.3",
|
|
62
|
+
"@types/react": "^19.2.17",
|
|
63
|
+
"@types/react-dom": "^19.2.3",
|
|
64
|
+
"@vitejs/plugin-react": "^6.0.4",
|
|
65
|
+
"jsdom": "^31.0.1",
|
|
66
|
+
"localforage": "^1.10.0",
|
|
67
|
+
"oxlint": "^1.75.0",
|
|
68
|
+
"typescript": "~6.0.2",
|
|
69
|
+
"vite": "^8.2.0",
|
|
70
|
+
"vite-plugin-dts": "^5.0.3",
|
|
71
|
+
"vitest": "^5.1.0"
|
|
72
|
+
}
|
|
73
|
+
}
|