pixedi 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/.github/workflows/deploy.yml +54 -0
- package/.prettierr +9 -0
- package/.storybook/main.ts +16 -0
- package/.storybook/preview.ts +53 -0
- package/.vscode/extensions.json +6 -0
- package/README.md +75 -0
- package/eslint.config.js +22 -0
- package/index.html +13 -0
- package/package.json +57 -0
- package/pnpm-workspace.yaml +2 -0
- package/public/favicon.svg +1 -0
- package/src/app/App.tsx +10 -0
- package/src/app/entries/index.ts +145 -0
- package/src/app/providers/RouterProvider.tsx +6 -0
- package/src/app/providers/index.ts +1 -0
- package/src/app/router/AppRouter.tsx +12 -0
- package/src/app/router/index.ts +0 -0
- package/src/app/styles/index.css +4 -0
- package/src/features/gallery/GalleryPage.tsx +33 -0
- package/src/features/gallery/ImageEditPage.tsx +37 -0
- package/src/features/gallery/index.ts +2 -0
- package/src/main.tsx +10 -0
- package/src/shared/components/ImageCard/ImageCard.tsx +54 -0
- package/src/shared/components/ImageCard/index.ts +1 -0
- package/src/shared/components/ImageEditor/ImageEditor.tsx +48 -0
- package/src/shared/components/ImageEditor/assets/icons.tsx +114 -0
- package/src/shared/components/ImageEditor/constants.ts +118 -0
- package/src/shared/components/ImageEditor/crop/Crop.tsx +60 -0
- package/src/shared/components/ImageEditor/crop/CropBorders.tsx +28 -0
- package/src/shared/components/ImageEditor/crop/CropLines.tsx +8 -0
- package/src/shared/components/ImageEditor/crop/CropPointer.tsx +21 -0
- package/src/shared/components/ImageEditor/crop/CropPointers.tsx +44 -0
- package/src/shared/components/ImageEditor/crop/CropTools.tsx +138 -0
- package/src/shared/components/ImageEditor/crop/crop.css +180 -0
- package/src/shared/components/ImageEditor/crop/index.ts +8 -0
- package/src/shared/components/ImageEditor/crop/useCropInteraction.ts +230 -0
- package/src/shared/components/ImageEditor/eventBus.ts +8 -0
- package/src/shared/components/ImageEditor/frame/Frame.tsx +48 -0
- package/src/shared/components/ImageEditor/frame/frame.css +40 -0
- package/src/shared/components/ImageEditor/frame/index.ts +1 -0
- package/src/shared/components/ImageEditor/header/Header.tsx +81 -0
- package/src/shared/components/ImageEditor/header/header.css +27 -0
- package/src/shared/components/ImageEditor/header/index.ts +1 -0
- package/src/shared/components/ImageEditor/hooks/index.ts +5 -0
- package/src/shared/components/ImageEditor/hooks/useAbove.tsx +18 -0
- package/src/shared/components/ImageEditor/hooks/useBellow.tsx +18 -0
- package/src/shared/components/ImageEditor/hooks/useImageLoader.tsx +110 -0
- package/src/shared/components/ImageEditor/hooks/useImageProcessor.test.tsx +147 -0
- package/src/shared/components/ImageEditor/hooks/useImageProcessor.ts +225 -0
- package/src/shared/components/ImageEditor/hooks/useMobile.tsx +33 -0
- package/src/shared/components/ImageEditor/index.css +145 -0
- package/src/shared/components/ImageEditor/index.ts +1 -0
- package/src/shared/components/ImageEditor/provider/ImageEditorProvider.test.tsx +126 -0
- package/src/shared/components/ImageEditor/provider/ImageEditorProvider.tsx +107 -0
- package/src/shared/components/ImageEditor/provider/StoreContext.tsx +20 -0
- package/src/shared/components/ImageEditor/provider/initialState.ts +30 -0
- package/src/shared/components/ImageEditor/provider/useImageEditorContext.tsx +12 -0
- package/src/shared/components/ImageEditor/resize/ResizeTools.tsx +183 -0
- package/src/shared/components/ImageEditor/resize/index.ts +1 -0
- package/src/shared/components/ImageEditor/resize/resize.css +56 -0
- package/src/shared/components/ImageEditor/sidebar/Sidebar.tsx +54 -0
- package/src/shared/components/ImageEditor/sidebar/SidebarCrop.tsx +57 -0
- package/src/shared/components/ImageEditor/sidebar/SidebarPresets.tsx +54 -0
- package/src/shared/components/ImageEditor/sidebar/SidebarResize.tsx +26 -0
- package/src/shared/components/ImageEditor/sidebar/index.ts +1 -0
- package/src/shared/components/ImageEditor/sidebar/sidebar.css +101 -0
- package/src/shared/components/ImageEditor/types.ts +59 -0
- package/src/shared/components/ImageEditor/ui/button/Button.stories.tsx +42 -0
- package/src/shared/components/ImageEditor/ui/button/Button.tsx +21 -0
- package/src/shared/components/ImageEditor/ui/button/button.css +69 -0
- package/src/shared/components/ImageEditor/ui/button-crop/ButtonCrop.stories.tsx +26 -0
- package/src/shared/components/ImageEditor/ui/button-crop/ButtonCrop.tsx +29 -0
- package/src/shared/components/ImageEditor/ui/button-crop/button-crop.css +38 -0
- package/src/shared/components/ImageEditor/ui/index.ts +6 -0
- package/src/shared/components/ImageEditor/ui/input/Input.stories.tsx +45 -0
- package/src/shared/components/ImageEditor/ui/input/Input.tsx +20 -0
- package/src/shared/components/ImageEditor/ui/input/input.css +48 -0
- package/src/shared/components/ImageEditor/ui/input-pixel/InputPixel.stories.tsx +33 -0
- package/src/shared/components/ImageEditor/ui/input-pixel/InputPixel.tsx +23 -0
- package/src/shared/components/ImageEditor/ui/input-pixel/input-pixel.css +24 -0
- package/src/shared/components/ImageEditor/ui/select/Select.stories.tsx +67 -0
- package/src/shared/components/ImageEditor/ui/select/Select.tsx +161 -0
- package/src/shared/components/ImageEditor/ui/select/select.css +147 -0
- package/src/shared/components/ImageEditor/ui/separator/Separator.stories.tsx +37 -0
- package/src/shared/components/ImageEditor/ui/separator/Separator.tsx +27 -0
- package/src/shared/components/ImageEditor/ui/separator/separator.css +19 -0
- package/src/shared/components/ImageEditor/utils.test.ts +136 -0
- package/src/shared/components/ImageEditor/utils.ts +497 -0
- package/src/shared/components/ui/index.ts +1 -0
- package/src/shared/components/ui/preloader.tsx +78 -0
- package/src/shared/config/index.ts +3 -0
- package/src/shared/config/routes.ts +4 -0
- package/src/shared/features/gallery/GalleryPage.tsx +33 -0
- package/src/shared/features/gallery/ImageEditPage.tsx +37 -0
- package/src/shared/features/gallery/index.ts +2 -0
- package/src/shared/hooks/index.ts +1 -0
- package/src/shared/hooks/useImagePreload.tsx +49 -0
- package/src/shared/types/index.ts +11 -0
- package/src/test/setup.ts +14 -0
- package/src/virtual-css-injected-by-js.d.ts +8 -0
- package/src/widget.tsx +119 -0
- package/tsconfig.app.json +29 -0
- package/tsconfig.json +12 -0
- package/tsconfig.lib.json +18 -0
- package/tsconfig.node.json +23 -0
- package/vite-env.d.ts +1 -0
- package/vite.config.ts +24 -0
- package/vite.lib.config.ts +33 -0
- package/vite.widget.config.ts +39 -0
- package/wrangler.json +5 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
.crop-image {
|
|
2
|
+
position: absolute;
|
|
3
|
+
inset: 0;
|
|
4
|
+
width: 100%;
|
|
5
|
+
height: 100%;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.crop-box {
|
|
9
|
+
position: absolute;
|
|
10
|
+
cursor: move;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.crop-tools {
|
|
14
|
+
position: absolute;
|
|
15
|
+
bottom: 24px;
|
|
16
|
+
left: 50%;
|
|
17
|
+
transform: translateX(-50%);
|
|
18
|
+
background-color: var(--background);
|
|
19
|
+
display: flex;
|
|
20
|
+
height: 36px;
|
|
21
|
+
padding-left: 12px;
|
|
22
|
+
align-items: center;
|
|
23
|
+
border-radius: 4px;
|
|
24
|
+
box-sizing: border-box;
|
|
25
|
+
border: 1px solid var(--border);
|
|
26
|
+
box-shadow:
|
|
27
|
+
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
|
28
|
+
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.crop-tools-info {
|
|
32
|
+
display: inline-block;
|
|
33
|
+
min-width: 54px;
|
|
34
|
+
margin-top: -2px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.crop-tools-info-label {
|
|
38
|
+
font-size: 12px;
|
|
39
|
+
line-height: 12px;
|
|
40
|
+
color: var(--muted-foreground);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.crop-tools-info-value {
|
|
44
|
+
font-size: 14px;
|
|
45
|
+
line-height: 14px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.crop-b {
|
|
49
|
+
position: absolute;
|
|
50
|
+
background-color: var(--accent-blue);
|
|
51
|
+
}
|
|
52
|
+
.crop-b-t {
|
|
53
|
+
top: 0;
|
|
54
|
+
left: 0;
|
|
55
|
+
width: 100%;
|
|
56
|
+
height: 2px;
|
|
57
|
+
cursor: ns-resize;
|
|
58
|
+
}
|
|
59
|
+
.crop-b-r {
|
|
60
|
+
top: 0;
|
|
61
|
+
right: 0;
|
|
62
|
+
width: 2px;
|
|
63
|
+
height: 100%;
|
|
64
|
+
cursor: ew-resize;
|
|
65
|
+
}
|
|
66
|
+
.crop-b-b {
|
|
67
|
+
bottom: 0;
|
|
68
|
+
left: 0;
|
|
69
|
+
width: 100%;
|
|
70
|
+
height: 2px;
|
|
71
|
+
cursor: ns-resize;
|
|
72
|
+
transform: translateY(1px);
|
|
73
|
+
}
|
|
74
|
+
.crop-b-l {
|
|
75
|
+
top: 0;
|
|
76
|
+
left: -1px;
|
|
77
|
+
width: 2px;
|
|
78
|
+
height: 100%;
|
|
79
|
+
cursor: ew-resize;
|
|
80
|
+
transform: translateX(1px);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.crop-l-box {
|
|
84
|
+
position: absolute;
|
|
85
|
+
inset: 2px;
|
|
86
|
+
pointer-events: none;
|
|
87
|
+
}
|
|
88
|
+
.crop-l {
|
|
89
|
+
position: absolute;
|
|
90
|
+
border: 1px dashed var(--color-white);
|
|
91
|
+
opacity: 0.5;
|
|
92
|
+
}
|
|
93
|
+
.crop-l-v {
|
|
94
|
+
width: 33%;
|
|
95
|
+
height: 100%;
|
|
96
|
+
top: 0;
|
|
97
|
+
left: 33%;
|
|
98
|
+
border-top: 0;
|
|
99
|
+
border-bottom: 0;
|
|
100
|
+
}
|
|
101
|
+
.crop-l-h {
|
|
102
|
+
width: 100%;
|
|
103
|
+
height: 33%;
|
|
104
|
+
top: 33%;
|
|
105
|
+
left: 0;
|
|
106
|
+
border-right: 0;
|
|
107
|
+
border-left: 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.crop-ps {
|
|
111
|
+
position: absolute;
|
|
112
|
+
box-sizing: border-box;
|
|
113
|
+
width: 12px;
|
|
114
|
+
height: 12px;
|
|
115
|
+
border: 1px solid var(--accent-blue);
|
|
116
|
+
background-color: var(--overlay-bg-white);
|
|
117
|
+
border-radius: 50%;
|
|
118
|
+
}
|
|
119
|
+
.crop-ps-tl {
|
|
120
|
+
top: -5px;
|
|
121
|
+
left: -5px;
|
|
122
|
+
cursor: nwse-resize;
|
|
123
|
+
}
|
|
124
|
+
.crop-ps-t {
|
|
125
|
+
top: -5px;
|
|
126
|
+
left: 50%;
|
|
127
|
+
transform: translateX(-50%);
|
|
128
|
+
cursor: ns-resize;
|
|
129
|
+
}
|
|
130
|
+
.crop-ps-tr {
|
|
131
|
+
top: -5px;
|
|
132
|
+
right: -5px;
|
|
133
|
+
cursor: nesw-resize;
|
|
134
|
+
}
|
|
135
|
+
.crop-ps-r {
|
|
136
|
+
top: 50%;
|
|
137
|
+
right: -5px;
|
|
138
|
+
transform: translateY(-50%);
|
|
139
|
+
cursor: ew-resize;
|
|
140
|
+
}
|
|
141
|
+
.crop-ps-br {
|
|
142
|
+
bottom: -5px;
|
|
143
|
+
right: -5px;
|
|
144
|
+
cursor: nwse-resize;
|
|
145
|
+
}
|
|
146
|
+
.crop-ps-b {
|
|
147
|
+
bottom: -5px;
|
|
148
|
+
left: 50%;
|
|
149
|
+
transform: translateX(-50%);
|
|
150
|
+
cursor: ns-resize;
|
|
151
|
+
}
|
|
152
|
+
.crop-ps-bl {
|
|
153
|
+
bottom: -5px;
|
|
154
|
+
left: -5px;
|
|
155
|
+
cursor: nesw-resize;
|
|
156
|
+
}
|
|
157
|
+
.crop-ps-l {
|
|
158
|
+
top: 50%;
|
|
159
|
+
left: -5px;
|
|
160
|
+
transform: translateY(-50%);
|
|
161
|
+
cursor: ew-resize;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.crop-p-b {
|
|
165
|
+
position: absolute;
|
|
166
|
+
inset: 0;
|
|
167
|
+
box-sizing: border-box;
|
|
168
|
+
border: 2px solid var(--accent-blue);
|
|
169
|
+
}
|
|
170
|
+
.crop-p-p {
|
|
171
|
+
position: absolute;
|
|
172
|
+
width: 32px;
|
|
173
|
+
height: 32px;
|
|
174
|
+
right: -16px;
|
|
175
|
+
bottom: -16px;
|
|
176
|
+
background-color: var(--overlay-bg-white);
|
|
177
|
+
border-radius: 50%;
|
|
178
|
+
border: 3px solid var(--accent-blue);
|
|
179
|
+
box-sizing: border-box;
|
|
180
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { Crop } from "./Crop";
|
|
2
|
+
import { CropTools } from "./CropTools";
|
|
3
|
+
export { CropBorders } from "./CropBorders";
|
|
4
|
+
export { CropLines } from "./CropLines";
|
|
5
|
+
export { CropPointers } from "./CropPointers";
|
|
6
|
+
export { useCropInteraction } from "./useCropInteraction";
|
|
7
|
+
export { CropPointer } from "./CropPointer";
|
|
8
|
+
export { CropTools };
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { useImageEditorContext } from "../provider/useImageEditorContext";
|
|
3
|
+
import { getCropPoints } from "../utils";
|
|
4
|
+
import type { CropRect, Direction } from "../types";
|
|
5
|
+
import { emitCropUpdate } from "../eventBus";
|
|
6
|
+
import { useMobile } from "../hooks";
|
|
7
|
+
|
|
8
|
+
function clamp(value: number, min: number, max: number): number {
|
|
9
|
+
return Math.min(Math.max(value, min), max);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function pct(px: number, total: number): number {
|
|
13
|
+
return (px / total) * 100;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function applyRect(
|
|
17
|
+
el: HTMLDivElement,
|
|
18
|
+
xPct: number,
|
|
19
|
+
yPct: number,
|
|
20
|
+
wPct: number,
|
|
21
|
+
hPct: number,
|
|
22
|
+
): void {
|
|
23
|
+
el.style.left = `${xPct}%`;
|
|
24
|
+
el.style.top = `${yPct}%`;
|
|
25
|
+
el.style.width = `${wPct}%`;
|
|
26
|
+
el.style.height = `${hPct}%`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function applyClipPath(
|
|
30
|
+
img: HTMLImageElement,
|
|
31
|
+
xPct: number,
|
|
32
|
+
yPct: number,
|
|
33
|
+
wPct: number,
|
|
34
|
+
hPct: number,
|
|
35
|
+
): void {
|
|
36
|
+
img.style.clipPath = `xywh(${xPct}% ${yPct}% ${wPct}% ${hPct}%)`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type UseCropInteractionArgs = {
|
|
40
|
+
imgRef: React.RefObject<HTMLImageElement | null>;
|
|
41
|
+
cropRef: React.RefObject<HTMLDivElement | null>;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const useCropInteraction = ({
|
|
45
|
+
imgRef,
|
|
46
|
+
cropRef,
|
|
47
|
+
}: UseCropInteractionArgs) => {
|
|
48
|
+
const { getCurrentAction, getLastHistoryItem } = useImageEditorContext();
|
|
49
|
+
const mobile = useMobile();
|
|
50
|
+
|
|
51
|
+
const startPointRef = useRef<{ x: number; y: number } | null>(null);
|
|
52
|
+
const directionRef = useRef<Direction | "">("");
|
|
53
|
+
const posRef = useRef<{ xPct: number; yPct: number }>({ xPct: 0, yPct: 0 });
|
|
54
|
+
const cropRectRef = useRef<CropRect>({ x: 0, y: 0, w: 0, h: 0 });
|
|
55
|
+
|
|
56
|
+
const handleCropStart = (
|
|
57
|
+
e: React.MouseEvent | React.TouchEvent,
|
|
58
|
+
type: Direction,
|
|
59
|
+
cursor?: string,
|
|
60
|
+
) => {
|
|
61
|
+
if (!cropRef.current) return;
|
|
62
|
+
|
|
63
|
+
e.stopPropagation();
|
|
64
|
+
|
|
65
|
+
cropRectRef.current = {
|
|
66
|
+
x: cropRef.current.offsetLeft,
|
|
67
|
+
y: cropRef.current.offsetTop,
|
|
68
|
+
w: cropRef.current.offsetWidth,
|
|
69
|
+
h: cropRef.current.offsetHeight,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const clientX = "clientX" in e ? e.clientX : e.touches[0].clientX;
|
|
73
|
+
const clientY = "clientY" in e ? e.clientY : e.touches[0].clientY;
|
|
74
|
+
startPointRef.current = { x: clientX, y: clientY };
|
|
75
|
+
directionRef.current = type;
|
|
76
|
+
|
|
77
|
+
if (!mobile) {
|
|
78
|
+
cropRef.current.style.cursor = `${cursor}-resize`;
|
|
79
|
+
document.body.style.cursor = `${cursor}-resize`;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
const handleDragStart = (e: MouseEvent | TouchEvent) => {
|
|
85
|
+
if (cropRef.current) {
|
|
86
|
+
const parent = cropRef.current.parentElement;
|
|
87
|
+
if (parent) {
|
|
88
|
+
posRef.current = {
|
|
89
|
+
xPct: pct(cropRef.current.offsetLeft, parent.offsetWidth),
|
|
90
|
+
yPct: pct(cropRef.current.offsetTop, parent.offsetHeight),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const clientX = "clientX" in e ? e.clientX : e.touches[0].clientX;
|
|
95
|
+
const clientY = "clientY" in e ? e.clientY : e.touches[0].clientY;
|
|
96
|
+
startPointRef.current = { x: clientX, y: clientY };
|
|
97
|
+
document.body.style.cursor = "move";
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const handleMove = (e: MouseEvent | TouchEvent) => {
|
|
101
|
+
if (!imgRef.current || !cropRef.current || !startPointRef.current) return;
|
|
102
|
+
|
|
103
|
+
const elCrop = cropRef.current;
|
|
104
|
+
const elImage = imgRef.current;
|
|
105
|
+
const parent = elCrop.parentElement;
|
|
106
|
+
if (!parent) return;
|
|
107
|
+
|
|
108
|
+
const parentW = parent.offsetWidth;
|
|
109
|
+
const parentH = parent.offsetHeight;
|
|
110
|
+
|
|
111
|
+
if (directionRef.current) {
|
|
112
|
+
handleResize(e, elCrop, elImage, parentW, parentH);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
handleDrag(e, elCrop, elImage, parentW, parentH);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const handleResize = (
|
|
120
|
+
e: MouseEvent | TouchEvent,
|
|
121
|
+
elCrop: HTMLDivElement,
|
|
122
|
+
elImage: HTMLImageElement,
|
|
123
|
+
parentW: number,
|
|
124
|
+
parentH: number,
|
|
125
|
+
) => {
|
|
126
|
+
const action = getCurrentAction();
|
|
127
|
+
if (!action || action.name !== "crop" || !startPointRef.current) return;
|
|
128
|
+
|
|
129
|
+
const { isFree, ratio } = action.args;
|
|
130
|
+
const clientX = "clientX" in e ? e.clientX : e.touches[0].clientX;
|
|
131
|
+
const clientY = "clientY" in e ? e.clientY : e.touches[0].clientY;
|
|
132
|
+
const { x, y, w, h } = getCropPoints(
|
|
133
|
+
directionRef.current as Direction,
|
|
134
|
+
isFree,
|
|
135
|
+
ratio,
|
|
136
|
+
startPointRef.current.x,
|
|
137
|
+
startPointRef.current.y,
|
|
138
|
+
clientX,
|
|
139
|
+
clientY,
|
|
140
|
+
parentW,
|
|
141
|
+
parentH,
|
|
142
|
+
cropRectRef.current,
|
|
143
|
+
elCrop,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const xPct = pct(x, parentW);
|
|
147
|
+
const yPct = pct(y, parentH);
|
|
148
|
+
const wPct = pct(w, parentW);
|
|
149
|
+
const hPct = pct(h, parentH);
|
|
150
|
+
|
|
151
|
+
applyRect(elCrop, xPct, yPct, wPct, hPct);
|
|
152
|
+
applyClipPath(elImage, xPct, yPct, wPct, hPct);
|
|
153
|
+
|
|
154
|
+
const lastHistoryItem = getLastHistoryItem();
|
|
155
|
+
if (!lastHistoryItem) return;
|
|
156
|
+
const { width: imgW, height: imgH } = lastHistoryItem;
|
|
157
|
+
emitCropUpdate({
|
|
158
|
+
x: Math.round((xPct / 100) * imgW),
|
|
159
|
+
y: Math.round((yPct / 100) * imgH),
|
|
160
|
+
w: Math.round((wPct / 100) * imgW),
|
|
161
|
+
h: Math.round((hPct / 100) * imgH),
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const handleDrag = (
|
|
166
|
+
e: MouseEvent | TouchEvent,
|
|
167
|
+
elCrop: HTMLDivElement,
|
|
168
|
+
elImage: HTMLImageElement,
|
|
169
|
+
parentW: number,
|
|
170
|
+
parentH: number,
|
|
171
|
+
) => {
|
|
172
|
+
if (!startPointRef.current) return;
|
|
173
|
+
console.log("HandleDrag");
|
|
174
|
+
const frmWidth = elCrop.offsetWidth;
|
|
175
|
+
const frmHeight = elCrop.offsetHeight;
|
|
176
|
+
|
|
177
|
+
const clientX = "clientX" in e ? e.clientX : e.touches[0].clientX;
|
|
178
|
+
const clientY = "clientY" in e ? e.clientY : e.touches[0].clientY;
|
|
179
|
+
const dxPct = pct(clientX - startPointRef.current.x, parentW);
|
|
180
|
+
const dyPct = pct(clientY - startPointRef.current.y, parentH);
|
|
181
|
+
startPointRef.current = { x: clientX, y: clientY };
|
|
182
|
+
|
|
183
|
+
const maxXPct = pct(parentW - frmWidth, parentW);
|
|
184
|
+
const maxYPct = pct(parentH - frmHeight, parentH);
|
|
185
|
+
|
|
186
|
+
const leftPct = clamp(posRef.current.xPct + dxPct, 0, maxXPct);
|
|
187
|
+
const topPct = clamp(posRef.current.yPct + dyPct, 0, maxYPct);
|
|
188
|
+
const wPct = pct(frmWidth, parentW);
|
|
189
|
+
const hPct = pct(frmHeight, parentH);
|
|
190
|
+
|
|
191
|
+
posRef.current = { xPct: leftPct, yPct: topPct };
|
|
192
|
+
|
|
193
|
+
applyRect(elCrop, leftPct, topPct, wPct, hPct);
|
|
194
|
+
applyClipPath(elImage, leftPct, topPct, wPct, hPct);
|
|
195
|
+
|
|
196
|
+
const { width: imgW, height: imgH } = getLastHistoryItem();
|
|
197
|
+
emitCropUpdate({
|
|
198
|
+
x: Math.round((leftPct / 100) * imgW),
|
|
199
|
+
y: Math.round((topPct / 100) * imgH),
|
|
200
|
+
w: Math.round((wPct / 100) * imgW),
|
|
201
|
+
h: Math.round((hPct / 100) * imgH),
|
|
202
|
+
});
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const handleMoveEnd = () => {
|
|
206
|
+
if (!cropRef.current) return;
|
|
207
|
+
|
|
208
|
+
startPointRef.current = null;
|
|
209
|
+
directionRef.current = "";
|
|
210
|
+
cropRef.current.style.cursor = "move";
|
|
211
|
+
document.body.style.cursor = "auto";
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
if (!cropRef.current) return;
|
|
215
|
+
|
|
216
|
+
const controller = new AbortController();
|
|
217
|
+
const { signal } = controller;
|
|
218
|
+
|
|
219
|
+
cropRef.current.addEventListener("mousedown", handleDragStart, { signal });
|
|
220
|
+
cropRef.current.addEventListener("touchstart", handleDragStart, { signal });
|
|
221
|
+
document.addEventListener("mousemove", handleMove, { signal });
|
|
222
|
+
document.addEventListener("touchmove", handleMove, { signal });
|
|
223
|
+
document.addEventListener("mouseup", handleMoveEnd, { signal });
|
|
224
|
+
document.addEventListener("touchend", handleMoveEnd, { signal });
|
|
225
|
+
|
|
226
|
+
return () => controller.abort();
|
|
227
|
+
}, [getCurrentAction, getLastHistoryItem, imgRef, cropRef]);
|
|
228
|
+
|
|
229
|
+
return { handleCropStart };
|
|
230
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useRef } from "react";
|
|
2
|
+
import { useImageEditorContext } from "../provider/useImageEditorContext";
|
|
3
|
+
import { Crop } from "../crop";
|
|
4
|
+
import { CropTools } from "../crop";
|
|
5
|
+
import "./frame.css";
|
|
6
|
+
import { ResizeTools } from "../resize";
|
|
7
|
+
|
|
8
|
+
export const Frame = () => {
|
|
9
|
+
const { getLastHistoryItem, currentAction } = useImageEditorContext();
|
|
10
|
+
const frameRef = useRef<HTMLDivElement>(null);
|
|
11
|
+
const imageRef = useRef<HTMLImageElement>(null);
|
|
12
|
+
const { width, height, base64 } = getLastHistoryItem();
|
|
13
|
+
|
|
14
|
+
const isCrop = currentAction?.name === "crop";
|
|
15
|
+
const isResize = currentAction?.name === "resize";
|
|
16
|
+
const isFade = isCrop || isResize;
|
|
17
|
+
|
|
18
|
+
const handleResizing = (scale: number) => {
|
|
19
|
+
if (frameRef.current) {
|
|
20
|
+
frameRef.current.style.transform = `scale(${scale / 100})`;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const frameClassName = `frame ${isFade ? "mask" : ""}`;
|
|
25
|
+
const imageClassName = `frame-image ${isFade ? "frame-image--faded" : ""}`;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className={frameClassName}>
|
|
29
|
+
<div
|
|
30
|
+
ref={frameRef}
|
|
31
|
+
style={{
|
|
32
|
+
aspectRatio: `${width} / ${height}`,
|
|
33
|
+
}}
|
|
34
|
+
className="frame-preview"
|
|
35
|
+
>
|
|
36
|
+
<img
|
|
37
|
+
ref={imageRef}
|
|
38
|
+
src={base64}
|
|
39
|
+
alt="Image"
|
|
40
|
+
className={imageClassName}
|
|
41
|
+
/>
|
|
42
|
+
{isCrop && <Crop key={currentAction?.args?.id} />}
|
|
43
|
+
</div>
|
|
44
|
+
{isResize && <ResizeTools onResizing={handleResizing} />}
|
|
45
|
+
{isCrop && <CropTools />}
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
.frame {
|
|
2
|
+
display: flex;
|
|
3
|
+
width: 100%;
|
|
4
|
+
height: 100%;
|
|
5
|
+
min-height: 0;
|
|
6
|
+
position: relative;
|
|
7
|
+
overflow: hidden;
|
|
8
|
+
justify-content: center;
|
|
9
|
+
align-items: center;
|
|
10
|
+
padding: 16px;
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.frame--faded {
|
|
15
|
+
background-color: rgba(0, 0, 0, 0.5);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.frame-preview {
|
|
19
|
+
position: relative;
|
|
20
|
+
max-width: 100%;
|
|
21
|
+
max-height: 100%;
|
|
22
|
+
width: auto;
|
|
23
|
+
height: auto;
|
|
24
|
+
min-height: 0;
|
|
25
|
+
min-width: 0;
|
|
26
|
+
flex-shrink: 1;
|
|
27
|
+
display: block;
|
|
28
|
+
user-select: none;
|
|
29
|
+
transition: transform 0.2s ease;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.frame-image {
|
|
33
|
+
width: 100%;
|
|
34
|
+
height: 100%;
|
|
35
|
+
object-fit: contain;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.frame-image--faded {
|
|
39
|
+
opacity: 0.4;
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Frame } from "./Frame";
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { ArrowLeft, Check, Undo, Redo, Loader } from "../assets/icons";
|
|
3
|
+
import { useImageEditorContext } from "../provider/useImageEditorContext";
|
|
4
|
+
import { Button } from "../ui";
|
|
5
|
+
import type { FuncSaveArgs } from "../types";
|
|
6
|
+
import "./header.css";
|
|
7
|
+
|
|
8
|
+
type HeaderProps = {
|
|
9
|
+
onBack: () => void;
|
|
10
|
+
onSave: FuncSaveArgs;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const Header = ({ onBack, onSave }: HeaderProps) => {
|
|
14
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
15
|
+
const {
|
|
16
|
+
history,
|
|
17
|
+
undo,
|
|
18
|
+
redo,
|
|
19
|
+
resetHistory,
|
|
20
|
+
resetHistoryAfterSave,
|
|
21
|
+
getLastHistoryItem,
|
|
22
|
+
} = useImageEditorContext();
|
|
23
|
+
|
|
24
|
+
const { base64 } = getLastHistoryItem();
|
|
25
|
+
const showHistory = history.items.length > 1 && !isSaving;
|
|
26
|
+
const disabledUndo = history.pointer === 0;
|
|
27
|
+
const disabledRedo = history.pointer === history.items.length - 1;
|
|
28
|
+
const disableSave = history.items.length < 2 || isSaving;
|
|
29
|
+
|
|
30
|
+
const handleSave = async () => {
|
|
31
|
+
if (!base64) return;
|
|
32
|
+
setIsSaving(true);
|
|
33
|
+
try {
|
|
34
|
+
await onSave(base64);
|
|
35
|
+
resetHistoryAfterSave();
|
|
36
|
+
} finally {
|
|
37
|
+
setIsSaving(false);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="header">
|
|
43
|
+
<Button variant="ghost" className="btn-rect" onClick={onBack}>
|
|
44
|
+
<ArrowLeft />
|
|
45
|
+
</Button>
|
|
46
|
+
|
|
47
|
+
<div className="header-tools">
|
|
48
|
+
{showHistory && (
|
|
49
|
+
<div className="header-history">
|
|
50
|
+
<Button
|
|
51
|
+
variant="outline"
|
|
52
|
+
disabled={disabledUndo}
|
|
53
|
+
className="btn-rect"
|
|
54
|
+
onClick={undo}
|
|
55
|
+
>
|
|
56
|
+
<Undo />
|
|
57
|
+
</Button>
|
|
58
|
+
<div className="header-history-text">
|
|
59
|
+
{history.pointer + 1}/{history.items.length}
|
|
60
|
+
</div>
|
|
61
|
+
<Button
|
|
62
|
+
variant="outline"
|
|
63
|
+
disabled={disabledRedo}
|
|
64
|
+
className="btn-rect"
|
|
65
|
+
onClick={redo}
|
|
66
|
+
>
|
|
67
|
+
<Redo />
|
|
68
|
+
</Button>
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
<Button variant="outline" disabled={isSaving} onClick={resetHistory}>
|
|
72
|
+
Reset
|
|
73
|
+
</Button>
|
|
74
|
+
<Button disabled={disableSave} onClick={handleSave}>
|
|
75
|
+
{isSaving ? <Loader /> : <Check />}
|
|
76
|
+
Save
|
|
77
|
+
</Button>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
.header {
|
|
2
|
+
height: 48px;
|
|
3
|
+
display: flex;
|
|
4
|
+
justify-content: space-between;
|
|
5
|
+
align-items: center;
|
|
6
|
+
padding-left: 8px;
|
|
7
|
+
padding-right: 8px;
|
|
8
|
+
box-sizing: border-box;
|
|
9
|
+
background-color: var(--background);
|
|
10
|
+
border-bottom: 1px solid var(--border);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.header-tools {
|
|
14
|
+
display: flex;
|
|
15
|
+
gap: 8px;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.header-history {
|
|
19
|
+
display: flex;
|
|
20
|
+
gap: 8px;
|
|
21
|
+
align-items: center;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.header-history-text {
|
|
25
|
+
color: var(--muted-foreground);
|
|
26
|
+
font-size: 14px;
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Header } from "./Header";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { breakpoints, type Breakpoint } from "../types";
|
|
3
|
+
|
|
4
|
+
export function useAbove(breakpoint: Breakpoint): boolean {
|
|
5
|
+
const [isAbove, setIsAbove] = useState(
|
|
6
|
+
() => window.innerWidth >= breakpoints[breakpoint],
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
function handleResize() {
|
|
11
|
+
setIsAbove(window.innerWidth >= breakpoints[breakpoint]);
|
|
12
|
+
}
|
|
13
|
+
window.addEventListener("resize", handleResize);
|
|
14
|
+
return () => window.removeEventListener("resize", handleResize);
|
|
15
|
+
}, [breakpoint]);
|
|
16
|
+
|
|
17
|
+
return isAbove;
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { breakpoints, type Breakpoint } from "../types";
|
|
3
|
+
|
|
4
|
+
export function useBellow(breakpoint: Breakpoint): boolean {
|
|
5
|
+
const [isBellow, setIsBellow] = useState(
|
|
6
|
+
() => window.innerWidth < breakpoints[breakpoint],
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
function handleResize() {
|
|
11
|
+
setIsBellow(window.innerWidth < breakpoints[breakpoint]);
|
|
12
|
+
}
|
|
13
|
+
window.addEventListener("resize", handleResize);
|
|
14
|
+
return () => window.removeEventListener("resize", handleResize);
|
|
15
|
+
}, [breakpoint]);
|
|
16
|
+
|
|
17
|
+
return isBellow;
|
|
18
|
+
}
|