@scaleflex/crop 2.0.1
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 +452 -0
- package/dist/a11y/aria.d.ts +5 -0
- package/dist/a11y/keyboard.d.ts +13 -0
- package/dist/animation/lerp.d.ts +15 -0
- package/dist/animation/spring.d.ts +15 -0
- package/dist/canvas/bleed-layer.d.ts +6 -0
- package/dist/canvas/crop-frame.d.ts +32 -0
- package/dist/canvas/grid-layer.d.ts +7 -0
- package/dist/canvas/hit-test.d.ts +10 -0
- package/dist/canvas/image-layer.d.ts +28 -0
- package/dist/canvas/overlay-layer.d.ts +6 -0
- package/dist/canvas/renderer.d.ts +34 -0
- package/dist/chunks/sfx-crop-1LGASewd.cjs +353 -0
- package/dist/chunks/sfx-crop-CEe6OfTZ.js +2030 -0
- package/dist/core/config.d.ts +10 -0
- package/dist/core/crop-controller.d.ts +65 -0
- package/dist/core/types.d.ts +270 -0
- package/dist/define.cjs +1194 -0
- package/dist/define.d.ts +1 -0
- package/dist/define.js +1746 -0
- package/dist/elements/base.d.ts +17 -0
- package/dist/elements/icons.d.ts +21 -0
- package/dist/elements/parse-shapes.d.ts +13 -0
- package/dist/elements/popover-anchor.d.ts +20 -0
- package/dist/elements/sfx-crop-canvas.d.ts +24 -0
- package/dist/elements/sfx-crop-canvas.styles.d.ts +1 -0
- package/dist/elements/sfx-crop-rotate.d.ts +42 -0
- package/dist/elements/sfx-crop-rotate.styles.d.ts +5 -0
- package/dist/elements/sfx-crop-shapes.d.ts +67 -0
- package/dist/elements/sfx-crop-shapes.styles.d.ts +6 -0
- package/dist/elements/sfx-crop-toolbar.d.ts +64 -0
- package/dist/elements/sfx-crop-toolbar.styles.d.ts +7 -0
- package/dist/elements/sfx-crop-zoom.d.ts +66 -0
- package/dist/elements/sfx-crop-zoom.styles.d.ts +7 -0
- package/dist/elements/sfx-crop.d.ts +134 -0
- package/dist/elements/sfx-crop.styles.d.ts +9 -0
- package/dist/export/exporter.d.ts +19 -0
- package/dist/index.cjs +2 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +65 -0
- package/dist/interactions/drag-crop.d.ts +10 -0
- package/dist/interactions/pinch-zoom.d.ts +14 -0
- package/dist/interactions/pointer-tracker.d.ts +29 -0
- package/dist/interactions/resize-handles.d.ts +13 -0
- package/dist/interactions/wheel-zoom.d.ts +12 -0
- package/dist/react/define-CVJd5aYk.cjs +1545 -0
- package/dist/react/define-t4Z6KaLY.js +2590 -0
- package/dist/react/index-B-csHwK2.cjs +2 -0
- package/dist/react/index-CktjrogS.js +1468 -0
- package/dist/react/index.cjs +2 -0
- package/dist/react/index.d.ts +21 -0
- package/dist/react/index.js +10 -0
- package/dist/react/sfx-crop.d.ts +86 -0
- package/dist/react/use-sfx-crop-controller.d.ts +74 -0
- package/dist/react/use-sfx-crop.d.ts +31 -0
- package/dist/styles/shared.css.d.ts +20 -0
- package/dist/transforms/constrain.d.ts +68 -0
- package/dist/transforms/matrix.d.ts +23 -0
- package/dist/transforms/transform-state.d.ts +12 -0
- package/dist/utils/events.d.ts +16 -0
- package/dist/utils/math.d.ts +12 -0
- package/package.json +108 -0
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-B-csHwK2.cjs");exports.DEFAULT_CONFIG=e.DEFAULT_CONFIG;exports.SfxCrop=e.SfxCrop;exports.createCropController=e.createCropController;exports.mergeConfig=e.mergeConfig;exports.useSfxCrop=e.useSfxCrop;exports.useSfxCropController=e.useSfxCropController;
|
|
2
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React entry for `@scaleflex/crop/react`.
|
|
3
|
+
*
|
|
4
|
+
* Exports:
|
|
5
|
+
* - `SfxCrop` — forwardRef component rendering `<sfx-crop>`
|
|
6
|
+
* - `useSfxCrop` — hook variant for consumers that render the element manually
|
|
7
|
+
* - `SfxCropElement`, `SfxCropProps`, `SfxCropSaveDetail` — types
|
|
8
|
+
*
|
|
9
|
+
* The wrapper dynamically imports `../define` on module load so the custom
|
|
10
|
+
* element auto-registers whenever this module is evaluated in the browser.
|
|
11
|
+
* SSR-safe: the import is guarded behind a `typeof customElements` check.
|
|
12
|
+
*/
|
|
13
|
+
export { SfxCrop } from './sfx-crop';
|
|
14
|
+
export type { SfxCropProps, SfxCropElement, SfxCropSaveDetail } from './sfx-crop';
|
|
15
|
+
export { useSfxCrop } from './use-sfx-crop';
|
|
16
|
+
export type { UseSfxCropReturn } from './use-sfx-crop';
|
|
17
|
+
export { useSfxCropController } from './use-sfx-crop-controller';
|
|
18
|
+
export type { UseSfxCropControllerOptions, UseSfxCropControllerReturn, CropControllerState, CropControllerActions, CropControllerApi, } from './use-sfx-crop-controller';
|
|
19
|
+
export { createCropController, DEFAULT_CONFIG, mergeConfig } from '../index';
|
|
20
|
+
export type { CropController, CropControllerOptions, CropControllerCallbacks, SfxCropConfig, } from '../index';
|
|
21
|
+
export type { TransformState, TransformParams, CropShapeName, CropShape, CropRect, NormalizedRect, HandlePosition, Point, Size, CropIconOverrides, } from '../core/types';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { D as e, S as s, c as C, m as a, u as f, b as p } from "./index-CktjrogS.js";
|
|
2
|
+
export {
|
|
3
|
+
e as DEFAULT_CONFIG,
|
|
4
|
+
s as SfxCrop,
|
|
5
|
+
C as createCropController,
|
|
6
|
+
a as mergeConfig,
|
|
7
|
+
f as useSfxCrop,
|
|
8
|
+
p as useSfxCropController
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { CSSProperties } from 'react';
|
|
2
|
+
import { SfxCropElement } from '../elements/sfx-crop';
|
|
3
|
+
import { CropShapeName, CropRect, TransformState, TransformParams, CropIconOverrides } from '../core/types';
|
|
4
|
+
export type { SfxCropElement };
|
|
5
|
+
export interface SfxCropSaveDetail {
|
|
6
|
+
blob: Blob;
|
|
7
|
+
dataURL: string;
|
|
8
|
+
params: TransformParams;
|
|
9
|
+
}
|
|
10
|
+
export interface SfxCropProps {
|
|
11
|
+
src?: string;
|
|
12
|
+
/** `'classic'` (default) or `'fixed'` — see `<sfx-crop>` `variant`. */
|
|
13
|
+
variant?: 'classic' | 'fixed';
|
|
14
|
+
cropShape?: CropShapeName;
|
|
15
|
+
theme?: 'light' | 'dark';
|
|
16
|
+
initialCrop?: CropRect | null;
|
|
17
|
+
initialRotation?: number;
|
|
18
|
+
initialScale?: number;
|
|
19
|
+
minScale?: number;
|
|
20
|
+
maxScale?: number;
|
|
21
|
+
minCropSize?: number;
|
|
22
|
+
availableShapes?: CropShapeName[];
|
|
23
|
+
/** Per-slot icon overrides — see `CropIconOverrides` in the core types. */
|
|
24
|
+
icons?: CropIconOverrides;
|
|
25
|
+
handleSize?: number;
|
|
26
|
+
handleColor?: string;
|
|
27
|
+
borderRadius?: number;
|
|
28
|
+
overlayColor?: string;
|
|
29
|
+
outputType?: string;
|
|
30
|
+
outputQuality?: number;
|
|
31
|
+
maxOutputWidth?: number;
|
|
32
|
+
maxOutputHeight?: number;
|
|
33
|
+
showGrid?: boolean | 'interaction';
|
|
34
|
+
showToolbar?: boolean;
|
|
35
|
+
showRotateSlider?: boolean;
|
|
36
|
+
showZoomSlider?: boolean;
|
|
37
|
+
showShapeSelector?: boolean;
|
|
38
|
+
showRotateButton?: boolean;
|
|
39
|
+
showFlipButton?: boolean;
|
|
40
|
+
toolbarPosition?: 'top' | 'bottom';
|
|
41
|
+
showBleedMargin?: boolean;
|
|
42
|
+
bleedMarginSize?: number;
|
|
43
|
+
bleedMarginColor?: string;
|
|
44
|
+
enableAnimations?: boolean;
|
|
45
|
+
animationSpeed?: number;
|
|
46
|
+
keyboard?: boolean;
|
|
47
|
+
pinchZoom?: boolean;
|
|
48
|
+
wheelZoom?: boolean;
|
|
49
|
+
onReady?: (detail: {
|
|
50
|
+
element: SfxCropElement;
|
|
51
|
+
}) => void;
|
|
52
|
+
onImageLoad?: (detail: {
|
|
53
|
+
image: HTMLImageElement;
|
|
54
|
+
}) => void;
|
|
55
|
+
onChange?: (state: TransformState) => void;
|
|
56
|
+
onCropChange?: (crop: CropRect) => void;
|
|
57
|
+
onSave?: (detail: SfxCropSaveDetail) => void;
|
|
58
|
+
onCancel?: () => void;
|
|
59
|
+
onError?: (detail: {
|
|
60
|
+
error: Error;
|
|
61
|
+
}) => void;
|
|
62
|
+
className?: string;
|
|
63
|
+
style?: CSSProperties;
|
|
64
|
+
id?: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* `<SfxCrop>` — React wrapper around the `<sfx-crop>` custom element.
|
|
68
|
+
*
|
|
69
|
+
* Pattern follows `@scaleflex/uploader`'s React wrapper: hand-rolled
|
|
70
|
+
* `forwardRef` + dynamic `import('../define')` at module load to auto-register
|
|
71
|
+
* the element, + `useEffect` bridges from CustomEvent to prop callbacks.
|
|
72
|
+
*
|
|
73
|
+
* Stale-closure avoidance: the latest props sit in a mutable ref updated on
|
|
74
|
+
* every render. The event-bridge `useEffect` attaches listeners once and
|
|
75
|
+
* reads `cbRef.current.on*` at fire time — no re-subscription on re-render.
|
|
76
|
+
*
|
|
77
|
+
* Prop sync: runs on every render without a deps array. Operations are
|
|
78
|
+
* idempotent (Lit compares before assigning), so the cost is a cheap walk
|
|
79
|
+
* of FORWARDED_PROPS.
|
|
80
|
+
*
|
|
81
|
+
* The ref is the bare element. The factory runs after React's commit phase,
|
|
82
|
+
* so `ref.current` is non-null once a parent component can actually read it.
|
|
83
|
+
* If you need to access methods before the first commit (e.g. during render),
|
|
84
|
+
* guard with `ref.current?.method?.()` or wait for `sfx-crop-ready`.
|
|
85
|
+
*/
|
|
86
|
+
export declare const SfxCrop: import('react').ForwardRefExoticComponent<SfxCropProps & import('react').RefAttributes<SfxCropElement>>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { CropController } from '../core/crop-controller';
|
|
2
|
+
import { SfxCropConfig, TransformState, TransformParams, CropRect, CropShapeName } from '../core/types';
|
|
3
|
+
/**
|
|
4
|
+
* Headless options for {@link useSfxCropController}. Most fields mirror the
|
|
5
|
+
* built-in element's attributes, minus UI toggles that are irrelevant when
|
|
6
|
+
* the consumer provides their own UI.
|
|
7
|
+
*/
|
|
8
|
+
export type UseSfxCropControllerOptions = Partial<SfxCropConfig>;
|
|
9
|
+
/** Reactive snapshot exposed by the hook. */
|
|
10
|
+
export interface CropControllerState {
|
|
11
|
+
/** Current editor transform (rotation, flip, scale, pan, crop). */
|
|
12
|
+
state: TransformState;
|
|
13
|
+
cropRect: CropRect;
|
|
14
|
+
cropShape: CropShapeName;
|
|
15
|
+
scale: number;
|
|
16
|
+
rotation: number;
|
|
17
|
+
loading: boolean;
|
|
18
|
+
error: string | null;
|
|
19
|
+
/** `true` once the editor has a decoded image and the render loop is live. */
|
|
20
|
+
ready: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** Imperative actions — stable identity across renders. */
|
|
23
|
+
export interface CropControllerActions {
|
|
24
|
+
loadImage(src: string): Promise<void>;
|
|
25
|
+
rotateLeft(): void;
|
|
26
|
+
flipHorizontal(): void;
|
|
27
|
+
setRotation(deg: number): void;
|
|
28
|
+
setScale(scale: number): void;
|
|
29
|
+
setCropShape(shape: CropShapeName): void;
|
|
30
|
+
setCropRect(rect: CropRect): void;
|
|
31
|
+
reset(): void;
|
|
32
|
+
}
|
|
33
|
+
/** Export / query surface — stable identity across renders. */
|
|
34
|
+
export interface CropControllerApi {
|
|
35
|
+
toCanvas(): HTMLCanvasElement | null;
|
|
36
|
+
toBlob(type?: string, quality?: number): Promise<Blob | null>;
|
|
37
|
+
toDataURL(type?: string, quality?: number): string | null;
|
|
38
|
+
toTransformParams(): TransformParams | null;
|
|
39
|
+
/** Underlying controller handle (null until both refs are attached). */
|
|
40
|
+
getController(): CropController | null;
|
|
41
|
+
}
|
|
42
|
+
export interface UseSfxCropControllerReturn extends CropControllerState {
|
|
43
|
+
/** Attach to your `<canvas>` node. */
|
|
44
|
+
canvasRef: React.RefObject<HTMLCanvasElement | null>;
|
|
45
|
+
/** Attach to a sizing box (e.g. a `<div>` with `max-width`/`max-height`). */
|
|
46
|
+
containerRef: React.RefObject<HTMLElement | null>;
|
|
47
|
+
actions: CropControllerActions;
|
|
48
|
+
api: CropControllerApi;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Headless React hook — wraps {@link createCropController} so a consumer can
|
|
52
|
+
* drop a `<canvas>` + sizing container anywhere in their tree and wire up
|
|
53
|
+
* their own UI (their design-system buttons, sliders, modals, etc.). The
|
|
54
|
+
* hook never mounts `<sfx-crop>`; there is no Lit, no shadow DOM, no
|
|
55
|
+
* built-in toolbar.
|
|
56
|
+
*
|
|
57
|
+
* Usage:
|
|
58
|
+
* ```tsx
|
|
59
|
+
* const { canvasRef, containerRef, state, actions, api } = useSfxCropController({
|
|
60
|
+
* src: '/photo.jpg',
|
|
61
|
+
* cropShape: '16:9',
|
|
62
|
+
* });
|
|
63
|
+
*
|
|
64
|
+
* return (
|
|
65
|
+
* <div ref={containerRef} style={{ maxWidth: 1200, maxHeight: 640 }}>
|
|
66
|
+
* <canvas ref={canvasRef} style={{ width: '100%', height: '100%' }} />
|
|
67
|
+
* <button onClick={actions.rotateLeft}>Rotate</button>
|
|
68
|
+
* <input type="range" min={0.5} max={5} step={0.01}
|
|
69
|
+
* value={state.scale} onChange={(e) => actions.setScale(+e.target.value)} />
|
|
70
|
+
* </div>
|
|
71
|
+
* );
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export declare function useSfxCropController(options?: UseSfxCropControllerOptions): UseSfxCropControllerReturn;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { SfxCropElement } from '../elements/sfx-crop';
|
|
2
|
+
import { CropRect, CropShapeName, TransformState, TransformParams } from '../core/types';
|
|
3
|
+
export interface UseSfxCropReturn {
|
|
4
|
+
/** Attach this ref to your `<sfx-crop ref={ref}>` (or the React `SfxCrop`). */
|
|
5
|
+
ref: React.RefObject<SfxCropElement | null>;
|
|
6
|
+
/** Fires after the editor has loaded the image and the renderer is live. */
|
|
7
|
+
ready: boolean;
|
|
8
|
+
loadImage(src: string): Promise<void>;
|
|
9
|
+
rotateLeft(): void;
|
|
10
|
+
flipHorizontal(): void;
|
|
11
|
+
setRotation(deg: number): void;
|
|
12
|
+
setScale(scale: number): void;
|
|
13
|
+
setCropShape(shape: CropShapeName): void;
|
|
14
|
+
setCropRect(rect: CropRect): void;
|
|
15
|
+
getCropRect(): CropRect | null;
|
|
16
|
+
getTransformState(): TransformState | null;
|
|
17
|
+
reset(): void;
|
|
18
|
+
toCanvas(): HTMLCanvasElement | null;
|
|
19
|
+
toBlob(type?: string, quality?: number): Promise<Blob | null>;
|
|
20
|
+
toDataURL(type?: string, quality?: number): string | null;
|
|
21
|
+
toTransformParams(): TransformParams | null;
|
|
22
|
+
save(type?: string, quality?: number): Promise<void>;
|
|
23
|
+
cancel(): void;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Hook variant of the React wrapper — returns a ref to bind plus stable
|
|
27
|
+
* callables for imperative operations. Prefer the `<SfxCrop>` component for
|
|
28
|
+
* declarative usage; reach for this hook when you need to render the element
|
|
29
|
+
* yourself or share imperative access across multiple components.
|
|
30
|
+
*/
|
|
31
|
+
export declare function useSfxCrop(): UseSfxCropReturn;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Design tokens — sourced from the @scaleflex/ui-tw kit
|
|
3
|
+
* (packages/ui/src/styles/variables.css). Color values are exact OKLCH
|
|
4
|
+
* copies of the kit's --background / --foreground / --primary / etc., so
|
|
5
|
+
* a page that theme-embeds both <sfx-crop> and the ui-tw components
|
|
6
|
+
* shares a single palette. Override any token from light DOM, e.g.
|
|
7
|
+
* `<sfx-crop style="--sfx-cr-primary:oklch(0.6 0.18 280)">`.
|
|
8
|
+
*
|
|
9
|
+
* Light is the default; `theme="dark"` mirrors the kit's `:root.dark`.
|
|
10
|
+
* Tokens cascade through shadow boundaries via CSS custom-property
|
|
11
|
+
* inheritance, so sub-elements never redeclare them.
|
|
12
|
+
*/
|
|
13
|
+
export declare const designTokens: import('lit').CSSResult;
|
|
14
|
+
export declare const baseStyles: import('lit').CSSResult;
|
|
15
|
+
export declare const spinKeyframes: import('lit').CSSResult;
|
|
16
|
+
export declare const toolbarEnterKeyframes: import('lit').CSSResult;
|
|
17
|
+
export declare const zoomEnterKeyframes: import('lit').CSSResult;
|
|
18
|
+
export declare const modalInKeyframes: import('lit').CSSResult;
|
|
19
|
+
/** Shared slider-thumb styling used by zoom + rotate inputs. */
|
|
20
|
+
export declare const sliderThumbStyles: import('lit').CSSResult;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { CropRect, CropShapeName, TransformState } from '../core/types';
|
|
2
|
+
/**
|
|
3
|
+
* Parse a free-form aspect-ratio string like `"16:9"`, `"7:2"`, or `"11:8"`
|
|
4
|
+
* into a numeric ratio. Supports positive integers or decimals. Returns
|
|
5
|
+
* `null` if the input isn't a valid W:H pair — consumers can then treat it
|
|
6
|
+
* as a named shape (`'free'`, `'square'`, …) or reject it.
|
|
7
|
+
*/
|
|
8
|
+
export declare function parseRatio(name: string): number | null;
|
|
9
|
+
/**
|
|
10
|
+
* Get the numeric aspect ratio for a crop shape. Handles the named shapes
|
|
11
|
+
* (`free`, `square`, `circle`, `rounded-rect`) and every built-in preset
|
|
12
|
+
* (`"16:9"`, etc.), plus any other `"W:H"` string a consumer passes in.
|
|
13
|
+
* Returns `null` for free-form (no ratio constraint).
|
|
14
|
+
*/
|
|
15
|
+
export declare function getAspectRatio(shape: CropShapeName | string): number | null;
|
|
16
|
+
/** Clamp crop rect to stay within [0,1] image bounds. Spec section 8.3. */
|
|
17
|
+
export declare function clampCropToImage(crop: CropRect): CropRect;
|
|
18
|
+
/** Enforce aspect ratio on crop rect during resize. Spec section 8.3. */
|
|
19
|
+
export declare function enforceAspectRatio(crop: CropRect, ratio: number | null, handle: string, imageWidth: number, imageHeight: number, minSize: number): CropRect;
|
|
20
|
+
export declare function enforceMinSize(crop: CropRect, minSize: number, imageWidth: number, imageHeight: number): CropRect;
|
|
21
|
+
/** Compute minimum scale needed to cover the crop area at a given rotation. */
|
|
22
|
+
export declare function computeMinScale(imageWidth: number, imageHeight: number, canvasWidth: number, canvasHeight: number, rotation: number): number;
|
|
23
|
+
/**
|
|
24
|
+
* Cover constraint: keep the photo fully covering a target frame so the crop
|
|
25
|
+
* never exports transparent gaps. Returns the `scale` / `panX` / `panY` to use
|
|
26
|
+
* plus the (capped) minimum scale below which coverage breaks.
|
|
27
|
+
*
|
|
28
|
+
* The frame is given in container CSS px and may be **off-centre** (the classic
|
|
29
|
+
* movable crop rect) or the whole editor box (the fixed variant). The photo is
|
|
30
|
+
* always drawn centred in the container; pan is stored in container CSS px
|
|
31
|
+
* relative to the image centre, and the live draw places the image centre at
|
|
32
|
+
* `scale * pan` px from the container centre (see `image-layer.ts` —
|
|
33
|
+
* translate(center)→scale→translate(pan)). The math works in CSS px and divides
|
|
34
|
+
* the slack by `scale`.
|
|
35
|
+
*
|
|
36
|
+
* `drawW0` / `drawH0` are the photo's draw size **at scale 1** in container px —
|
|
37
|
+
* the caller passes the model that matches the actual render: cover-fit
|
|
38
|
+
* (`computeCoverDraw`) in the fixed variant, or the stretched container box
|
|
39
|
+
* (`w × h`) in classic. This keeps the clamp aligned with what's drawn even
|
|
40
|
+
* when the editor box aspect diverges from the image aspect.
|
|
41
|
+
*
|
|
42
|
+
* Fine tilt (`state.rotation`) inflates the frame to its axis-aligned bounding
|
|
43
|
+
* box (conservative). Flip is a no-op (mirrors about the centre).
|
|
44
|
+
*
|
|
45
|
+
* `maxScale` caps the cover floor: if covering the frame would need more zoom
|
|
46
|
+
* than the consumer allows, the floor is pinned to `maxScale` (so the returned
|
|
47
|
+
* `minScale ≤ maxScale` and the renderer's scale bounds never invert — at the
|
|
48
|
+
* cost of a residual gap, which is an unavoidable min/max-scale conflict).
|
|
49
|
+
*/
|
|
50
|
+
export declare function clampCoverPanScale(state: TransformState, containerW: number, containerH: number, frame: {
|
|
51
|
+
x: number;
|
|
52
|
+
y: number;
|
|
53
|
+
width: number;
|
|
54
|
+
height: number;
|
|
55
|
+
}, drawW0: number, drawH0: number, maxScale: number): {
|
|
56
|
+
scale: number;
|
|
57
|
+
panX: number;
|
|
58
|
+
panY: number;
|
|
59
|
+
minScale: number;
|
|
60
|
+
};
|
|
61
|
+
/** Snap rotation near 0 within threshold. */
|
|
62
|
+
export declare function snapRotation(degrees: number, threshold?: number): number;
|
|
63
|
+
/** Constrain scale to range. */
|
|
64
|
+
export declare function constrainScale(scale: number, minScale: number, maxScale: number): number;
|
|
65
|
+
/** @deprecated Use clampCropToImage instead */
|
|
66
|
+
export declare const constrainCropBounds: typeof clampCropToImage;
|
|
67
|
+
/** @deprecated Use enforceAspectRatio instead */
|
|
68
|
+
export declare const constrainAspectRatio: typeof enforceAspectRatio;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { TransformState, Size, Point } from '../core/types';
|
|
2
|
+
/** 2D affine transform matrix [a, b, c, d, e, f] (same layout as canvas). */
|
|
3
|
+
export type Matrix2D = [number, number, number, number, number, number];
|
|
4
|
+
export declare function identityMatrix(): Matrix2D;
|
|
5
|
+
export declare function multiplyMatrices(a: Matrix2D, b: Matrix2D): Matrix2D;
|
|
6
|
+
export declare function translateMatrix(tx: number, ty: number): Matrix2D;
|
|
7
|
+
export declare function scaleMatrix(sx: number, sy: number): Matrix2D;
|
|
8
|
+
export declare function rotateMatrix(degrees: number): Matrix2D;
|
|
9
|
+
/** @internal Build matrix as 6-element tuple. */
|
|
10
|
+
export declare function buildMatrix2D(canvasCx: number, canvasCy: number, rotation90: number, rotationFine: number, flipH: number, scale: number, panX: number, panY: number): Matrix2D;
|
|
11
|
+
/** Apply matrix to a point. */
|
|
12
|
+
export declare function transformPoint(m: Matrix2D, x: number, y: number): {
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
};
|
|
16
|
+
/** Invert a 2D affine matrix. */
|
|
17
|
+
export declare function invertMatrix(m: Matrix2D): Matrix2D;
|
|
18
|
+
/** Build the full image transform matrix. Spec section 8.2. */
|
|
19
|
+
export declare function buildTransformMatrix(state: TransformState, canvasSize: Size, _imageSize: Size): DOMMatrix;
|
|
20
|
+
/** Convert a point from image space to canvas space. Spec section 8.2. */
|
|
21
|
+
export declare function imageToCanvas(point: Point, matrix: DOMMatrix): Point;
|
|
22
|
+
/** Convert a point from canvas space to image space. Spec section 8.2. */
|
|
23
|
+
export declare function canvasToImage(point: Point, matrix: DOMMatrix): Point;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { TransformState, CropRect, CropShapeName, HandlePosition } from '../core/types';
|
|
2
|
+
export declare function createInitialState(cropShape?: CropShapeName, imageWidth?: number, imageHeight?: number): TransformState;
|
|
3
|
+
export declare function applyRotateLeft(state: TransformState): TransformState;
|
|
4
|
+
export declare function applyFlipH(state: TransformState): TransformState;
|
|
5
|
+
export declare function applyFlipV(state: TransformState): TransformState;
|
|
6
|
+
export declare function applyRotation(state: TransformState, degrees: number): TransformState;
|
|
7
|
+
export declare function applyScale(state: TransformState, scale: number, minScale: number, maxScale: number): TransformState;
|
|
8
|
+
export declare function applyCropMove(state: TransformState, cropRect: CropRect): TransformState;
|
|
9
|
+
export declare function applyShapeChange(state: TransformState, shape: CropShapeName, imageWidth?: number, imageHeight?: number): TransformState;
|
|
10
|
+
export declare function applyPan(state: TransformState, dx: number, dy: number): TransformState;
|
|
11
|
+
/** Pure crop resize — spec section 8.1. dx/dy in normalized [0,1] space. */
|
|
12
|
+
export declare function applyCropResize(state: TransformState, handle: HandlePosition, dx: number, dy: number, cropShape?: CropShapeName, minCropSize?: number, imageWidth?: number, imageHeight?: number): TransformState;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type EventHandler = (...args: any[]) => void;
|
|
2
|
+
export declare class EventEmitter {
|
|
3
|
+
private listeners;
|
|
4
|
+
private onceMap;
|
|
5
|
+
on(event: string, handler: EventHandler): void;
|
|
6
|
+
off(event: string, handler: EventHandler): void;
|
|
7
|
+
emit(event: string, ...args: any[]): void;
|
|
8
|
+
once(event: string, handler: EventHandler): void;
|
|
9
|
+
removeAllListeners(): void;
|
|
10
|
+
}
|
|
11
|
+
export declare function addListener(el: EventTarget, event: string, handler: EventListenerOrEventListenerObject, options?: AddEventListenerOptions): () => void;
|
|
12
|
+
export type ThrottledFunction<T extends (...args: any[]) => void> = T & {
|
|
13
|
+
cancel(): void;
|
|
14
|
+
};
|
|
15
|
+
export declare function throttle<T extends (...args: any[]) => void>(fn: T, ms: number): ThrottledFunction<T>;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare function clamp(value: number, min: number, max: number): number;
|
|
2
|
+
export declare function lerp(a: number, b: number, t: number): number;
|
|
3
|
+
export declare function degreesToRadians(degrees: number): number;
|
|
4
|
+
export declare function radiansToDegrees(radians: number): number;
|
|
5
|
+
export declare function distance(x1: number, y1: number, x2: number, y2: number): number;
|
|
6
|
+
export declare function normalizeAngle(degrees: number): number;
|
|
7
|
+
/** Spring interpolation — returns new current value moving toward target. */
|
|
8
|
+
export declare function spring(current: number, target: number, velocity: {
|
|
9
|
+
v: number;
|
|
10
|
+
}, stiffness: number, damping: number, dt: number): number;
|
|
11
|
+
/** Check if a point is inside a rotated rectangle. */
|
|
12
|
+
export declare function pointInRotatedRect(px: number, py: number, cx: number, cy: number, w: number, h: number, angle: number): boolean;
|
package/package.json
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@scaleflex/crop",
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "Interactive image crop tool with rotation, flip, zoom and shape selection — Scaleflex Web Component",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Scaleflex",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"require": "./dist/index.cjs"
|
|
13
|
+
},
|
|
14
|
+
"./define": {
|
|
15
|
+
"types": "./dist/define.d.ts",
|
|
16
|
+
"import": "./dist/define.js",
|
|
17
|
+
"require": "./dist/define.cjs"
|
|
18
|
+
},
|
|
19
|
+
"./react": {
|
|
20
|
+
"types": "./dist/react/index.d.ts",
|
|
21
|
+
"import": "./dist/react/index.js",
|
|
22
|
+
"require": "./dist/react/index.cjs"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"!dist/**/*.map"
|
|
28
|
+
],
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"sideEffects": [
|
|
33
|
+
"dist/define.js",
|
|
34
|
+
"dist/define.cjs",
|
|
35
|
+
"src/define.ts"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"dev": "vite --config config/vite.demo.config.ts",
|
|
39
|
+
"build": "npm run build:bundle && npm run build:react",
|
|
40
|
+
"build:bundle": "vite build --config config/vite.config.ts",
|
|
41
|
+
"build:react": "vite build --config config/vite.react.config.ts",
|
|
42
|
+
"build:demo": "vite build --config config/vite.demo.config.ts",
|
|
43
|
+
"build:cdn": "vite build --config config/vite.cdn.config.ts",
|
|
44
|
+
"release": "node scripts/release-cdn.mjs plugin",
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"test:watch": "vitest",
|
|
48
|
+
"test:coverage": "vitest run --coverage",
|
|
49
|
+
"lint": "eslint src/ tests/ --ext .ts,.tsx",
|
|
50
|
+
"prepublishOnly": "npm run build"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"lit": "^3.2.0"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"react": ">=18.0.0",
|
|
57
|
+
"react-dom": ">=18.0.0"
|
|
58
|
+
},
|
|
59
|
+
"peerDependenciesMeta": {
|
|
60
|
+
"react": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"react-dom": {
|
|
64
|
+
"optional": true
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
69
|
+
"@testing-library/react": "^16.3.2",
|
|
70
|
+
"@types/node": "^25.2.3",
|
|
71
|
+
"@types/react": "^19.0.0",
|
|
72
|
+
"@types/react-dom": "^19.0.0",
|
|
73
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
74
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
75
|
+
"@vitejs/plugin-react": "^4.0.0",
|
|
76
|
+
"eslint": "^8.57.0",
|
|
77
|
+
"eslint-plugin-react-hooks": "^4.6.2",
|
|
78
|
+
"jsdom": "^25.0.0",
|
|
79
|
+
"react": "^19.0.0",
|
|
80
|
+
"react-dom": "^19.0.0",
|
|
81
|
+
"typescript": "^5.7.0",
|
|
82
|
+
"vite": "^6.0.0",
|
|
83
|
+
"vite-plugin-dts": "^4.0.0",
|
|
84
|
+
"vitest": "^3.0.0"
|
|
85
|
+
},
|
|
86
|
+
"keywords": [
|
|
87
|
+
"crop",
|
|
88
|
+
"image",
|
|
89
|
+
"editor",
|
|
90
|
+
"canvas",
|
|
91
|
+
"rotation",
|
|
92
|
+
"flip",
|
|
93
|
+
"zoom",
|
|
94
|
+
"cloudimage",
|
|
95
|
+
"scaleflex",
|
|
96
|
+
"web-component",
|
|
97
|
+
"lit",
|
|
98
|
+
"react"
|
|
99
|
+
],
|
|
100
|
+
"repository": {
|
|
101
|
+
"type": "git",
|
|
102
|
+
"url": "https://github.com/scaleflex/image-crop.git"
|
|
103
|
+
},
|
|
104
|
+
"bugs": {
|
|
105
|
+
"url": "https://github.com/scaleflex/image-crop/issues"
|
|
106
|
+
},
|
|
107
|
+
"homepage": "https://scaleflex.github.io/image-crop/"
|
|
108
|
+
}
|