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,54 @@
|
|
|
1
|
+
// import { motion, type Variants } from "motion/react";
|
|
2
|
+
// import { Link } from "react-router";
|
|
3
|
+
// import type { Image } from "@/shared/types";
|
|
4
|
+
|
|
5
|
+
// type ImageCardProps = {
|
|
6
|
+
// image: Image;
|
|
7
|
+
// };
|
|
8
|
+
|
|
9
|
+
// export const ImageCard = ({ image }: ImageCardProps) => {
|
|
10
|
+
// const imageVariants: Variants = {
|
|
11
|
+
// rest: { scale: 1, opacity: 0.5 },
|
|
12
|
+
// hover: {
|
|
13
|
+
// scale: 1.1,
|
|
14
|
+
// opacity: 1,
|
|
15
|
+
// },
|
|
16
|
+
// };
|
|
17
|
+
|
|
18
|
+
// return (
|
|
19
|
+
// <Link to={`/images/${image.id}`}>
|
|
20
|
+
// <motion.div
|
|
21
|
+
// className={`
|
|
22
|
+
// relative
|
|
23
|
+
// w-full aspect-4/3
|
|
24
|
+
// overflow-hidden cursor-pointer box-border
|
|
25
|
+
// border border-neutral-400
|
|
26
|
+
// `}
|
|
27
|
+
// whileHover="hover"
|
|
28
|
+
// initial="rest"
|
|
29
|
+
// animate="rest"
|
|
30
|
+
// >
|
|
31
|
+
// <motion.div
|
|
32
|
+
// className="absolute inset-0 opacity-50"
|
|
33
|
+
// variants={imageVariants}
|
|
34
|
+
// transition={{ duration: 0.3, ease: "easeIn" }}
|
|
35
|
+
// style={{
|
|
36
|
+
// backgroundImage: `url(${image.thumbnail})`,
|
|
37
|
+
// backgroundSize: "cover",
|
|
38
|
+
// backgroundPosition: "center",
|
|
39
|
+
// }}
|
|
40
|
+
// />
|
|
41
|
+
// </motion.div>
|
|
42
|
+
// </Link>
|
|
43
|
+
// );
|
|
44
|
+
// };
|
|
45
|
+
|
|
46
|
+
type ImageCardProps = {
|
|
47
|
+
image: {
|
|
48
|
+
id: string;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const ImageCard = ({ image }: ImageCardProps) => {
|
|
53
|
+
return <div>{image.id}</div>;
|
|
54
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ImageCard } from "./ImageCard";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Preloader } from "@/shared/components/ui";
|
|
2
|
+
import { ImageEditorProvider } from "./provider/ImageEditorProvider";
|
|
3
|
+
import { useImageLoader } from "./hooks";
|
|
4
|
+
import { Header } from "./header";
|
|
5
|
+
import { Frame } from "./frame";
|
|
6
|
+
import { Sidebar } from "./sidebar";
|
|
7
|
+
import type { FuncSaveArgs } from "./types";
|
|
8
|
+
import "./index.css";
|
|
9
|
+
|
|
10
|
+
type ImageEditorProps = {
|
|
11
|
+
image: string;
|
|
12
|
+
onSave: FuncSaveArgs;
|
|
13
|
+
onBack: () => void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const ImageEditor = ({ image, onSave, onBack }: ImageEditorProps) => {
|
|
17
|
+
const { loading, error, width, height, base64, extension } =
|
|
18
|
+
useImageLoader(image);
|
|
19
|
+
|
|
20
|
+
if (loading || error || !base64 || !extension) {
|
|
21
|
+
return (
|
|
22
|
+
<div className="picedi sys">
|
|
23
|
+
{loading && <Preloader size={48} />}
|
|
24
|
+
{error && <div className="text-red">{error}</div>}
|
|
25
|
+
{!loading && !error && (!base64 || !extension) && (
|
|
26
|
+
<div className="text-red">Failed to load image.</div>
|
|
27
|
+
)}
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<ImageEditorProvider
|
|
34
|
+
base64={base64}
|
|
35
|
+
width={width}
|
|
36
|
+
height={height}
|
|
37
|
+
ext={extension}
|
|
38
|
+
>
|
|
39
|
+
<div className="picedi main">
|
|
40
|
+
<Header onSave={onSave} onBack={onBack} />
|
|
41
|
+
<div className="grid">
|
|
42
|
+
<Frame />
|
|
43
|
+
<Sidebar />
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</ImageEditorProvider>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
const baseSvgProps: React.ComponentPropsWithoutRef<"svg"> = {
|
|
4
|
+
xmlns: "http://w3.org",
|
|
5
|
+
width: "24",
|
|
6
|
+
height: "24",
|
|
7
|
+
viewBox: "0 0 24 24",
|
|
8
|
+
fill: "none",
|
|
9
|
+
stroke: "currentColor",
|
|
10
|
+
strokeWidth: "2",
|
|
11
|
+
strokeLinecap: "round",
|
|
12
|
+
strokeLinejoin: "round",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const Check = (props: React.ComponentPropsWithoutRef<"svg">) => (
|
|
16
|
+
<svg {...baseSvgProps} {...props}>
|
|
17
|
+
<path d="M20 6 9 17l-5-5" />
|
|
18
|
+
</svg>
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
export const X = (props: React.ComponentPropsWithoutRef<"svg">) => (
|
|
22
|
+
<svg {...baseSvgProps} {...props}>
|
|
23
|
+
<path d="M18 6 6 18" />
|
|
24
|
+
<path d="m6 6 12 12" />
|
|
25
|
+
</svg>
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export const Lock = (props: React.ComponentPropsWithoutRef<"svg">) => (
|
|
29
|
+
<svg {...baseSvgProps} {...props}>
|
|
30
|
+
<rect width="18" height="11" x="3" y="11" rx="2" ry="2" />
|
|
31
|
+
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
|
32
|
+
</svg>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
export const ArrowLeft = (props: React.ComponentPropsWithoutRef<"svg">) => (
|
|
36
|
+
<svg {...baseSvgProps} {...props}>
|
|
37
|
+
<path d="m12 19-7-7 7-7" />
|
|
38
|
+
<path d="M19 12H5" />
|
|
39
|
+
</svg>
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
export const ChevronDown = (props: React.ComponentPropsWithoutRef<"svg">) => (
|
|
43
|
+
<svg {...baseSvgProps} {...props}>
|
|
44
|
+
<path d="m6 9 6 6 6-6" />
|
|
45
|
+
</svg>
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
export const Undo = (props: React.ComponentPropsWithoutRef<"svg">) => (
|
|
49
|
+
<svg {...baseSvgProps} {...props}>
|
|
50
|
+
<path d="M3 7v6h6" />
|
|
51
|
+
<path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13" />
|
|
52
|
+
</svg>
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
export const Redo = (props: React.ComponentPropsWithoutRef<"svg">) => (
|
|
56
|
+
<svg {...baseSvgProps} {...props}>
|
|
57
|
+
<path d="M21 7v6h-6" />
|
|
58
|
+
<path d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3l3 2.7" />
|
|
59
|
+
</svg>
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
export const Settings = (props: React.ComponentPropsWithoutRef<"svg">) => (
|
|
63
|
+
<svg {...baseSvgProps} {...props}>
|
|
64
|
+
<path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" />
|
|
65
|
+
<circle cx="12" cy="12" r="3" />
|
|
66
|
+
</svg>
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
export const Crop = (props: React.ComponentPropsWithoutRef<"svg">) => (
|
|
70
|
+
<svg {...baseSvgProps} {...props}>
|
|
71
|
+
<path d="M6 2v14a2 2 0 0 0 2 2h14" />
|
|
72
|
+
<path d="M18 22V8a2 2 0 0 0-2-2H2" />
|
|
73
|
+
</svg>
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
export const Image = (props: React.ComponentPropsWithoutRef<"svg">) => (
|
|
77
|
+
<svg {...baseSvgProps} {...props}>
|
|
78
|
+
<rect width="18" height="18" x="3" y="3" rx="2" ry="2" />
|
|
79
|
+
<circle cx="9" cy="9" r="2" />
|
|
80
|
+
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
|
|
81
|
+
</svg>
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
export const Square = (props: React.ComponentPropsWithoutRef<"svg">) => (
|
|
85
|
+
<svg {...baseSvgProps} {...props}>
|
|
86
|
+
<rect width="18" height="18" x="3" y="3" rx="2" />
|
|
87
|
+
</svg>
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
export const Fullscreen = (props: React.ComponentPropsWithoutRef<"svg">) => (
|
|
91
|
+
<svg {...baseSvgProps} {...props}>
|
|
92
|
+
<path d="M3 7V5a2 2 0 0 1 2-2h2" />
|
|
93
|
+
<path d="M17 3h2a2 2 0 0 1 2 2v2" />
|
|
94
|
+
<path d="M21 17v2a2 2 0 0 1-2 2h-2" />
|
|
95
|
+
<path d="M7 21H5a2 2 0 0 1-2-2v-2" />
|
|
96
|
+
<rect width="10" height="8" x="7" y="8" rx="1" />
|
|
97
|
+
</svg>
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
export const Loader = (props: React.ComponentPropsWithoutRef<"svg">) => (
|
|
101
|
+
<svg {...baseSvgProps} {...props}>
|
|
102
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56">
|
|
103
|
+
<animateTransform
|
|
104
|
+
attributeType="xml"
|
|
105
|
+
attributeName="transform"
|
|
106
|
+
type="rotate"
|
|
107
|
+
from="0 12 12"
|
|
108
|
+
to="360 12 12"
|
|
109
|
+
dur="0.8s"
|
|
110
|
+
repeatCount="indefinite"
|
|
111
|
+
/>
|
|
112
|
+
</path>
|
|
113
|
+
</svg>
|
|
114
|
+
);
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export type PresetOptions = {
|
|
2
|
+
value: string;
|
|
3
|
+
label: string;
|
|
4
|
+
w: number;
|
|
5
|
+
h: number;
|
|
6
|
+
rightLabel?: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type Preset = {
|
|
10
|
+
value: string;
|
|
11
|
+
label: string;
|
|
12
|
+
options: Array<PresetOptions>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const presetsData: Array<Preset> = [
|
|
16
|
+
{
|
|
17
|
+
label: "Facebook",
|
|
18
|
+
value: "facebook",
|
|
19
|
+
options: [
|
|
20
|
+
{
|
|
21
|
+
value: "facebook-post",
|
|
22
|
+
label: "Post",
|
|
23
|
+
w: 1200,
|
|
24
|
+
h: 630,
|
|
25
|
+
rightLabel: "1200 x 630",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
value: "facebook-cover",
|
|
29
|
+
label: "Cover",
|
|
30
|
+
w: 851,
|
|
31
|
+
h: 315,
|
|
32
|
+
rightLabel: "851 x 315",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
value: "facebook-profile",
|
|
36
|
+
label: "Profile",
|
|
37
|
+
w: 170,
|
|
38
|
+
h: 170,
|
|
39
|
+
rightLabel: "170 x 170",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
value: "facebook-story",
|
|
43
|
+
label: "Story",
|
|
44
|
+
w: 1080,
|
|
45
|
+
h: 1920,
|
|
46
|
+
rightLabel: "1080 x 1920",
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
label: "Instagram",
|
|
52
|
+
value: "instagram",
|
|
53
|
+
options: [
|
|
54
|
+
{
|
|
55
|
+
value: "instagram-landscape",
|
|
56
|
+
label: "Landscape",
|
|
57
|
+
w: 1080,
|
|
58
|
+
h: 566,
|
|
59
|
+
rightLabel: "1080 x 566",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
value: "instagram-portait",
|
|
63
|
+
label: "Portait",
|
|
64
|
+
w: 1080,
|
|
65
|
+
h: 1350,
|
|
66
|
+
rightLabel: "1080 x 1350",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
value: "instagram-square",
|
|
70
|
+
label: "Square",
|
|
71
|
+
w: 1080,
|
|
72
|
+
h: 1080,
|
|
73
|
+
rightLabel: "1080 x 1080",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
value: "instagram-story",
|
|
77
|
+
label: "Story",
|
|
78
|
+
w: 1080,
|
|
79
|
+
h: 1920,
|
|
80
|
+
rightLabel: "1080 x 1920",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
value: "instagram-thumbnail",
|
|
84
|
+
label: "Thumbnail",
|
|
85
|
+
w: 161,
|
|
86
|
+
h: 161,
|
|
87
|
+
rightLabel: "161 x 161",
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
label: "LinkedIn",
|
|
93
|
+
value: "linkedin",
|
|
94
|
+
options: [
|
|
95
|
+
{
|
|
96
|
+
value: "linkedin-blog-post",
|
|
97
|
+
label: "Blog Post",
|
|
98
|
+
w: 1200,
|
|
99
|
+
h: 627,
|
|
100
|
+
rightLabel: "1200 x 627",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
value: "linkedin-cover",
|
|
104
|
+
label: "Cover",
|
|
105
|
+
w: 1128,
|
|
106
|
+
h: 191,
|
|
107
|
+
rightLabel: "1128 x 191",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
value: "linkedin-profile",
|
|
111
|
+
label: "Profile",
|
|
112
|
+
w: 400,
|
|
113
|
+
h: 400,
|
|
114
|
+
rightLabel: "400 x 400",
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
];
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useRef } from "react";
|
|
2
|
+
import { useImageEditorContext } from "../provider/useImageEditorContext";
|
|
3
|
+
import {
|
|
4
|
+
CropBorders,
|
|
5
|
+
useCropInteraction,
|
|
6
|
+
CropPointers,
|
|
7
|
+
CropLines,
|
|
8
|
+
CropPointer,
|
|
9
|
+
} from "../crop";
|
|
10
|
+
import { getInitalCrop } from "../utils";
|
|
11
|
+
import { useMobile } from "../hooks";
|
|
12
|
+
import "./crop.css";
|
|
13
|
+
|
|
14
|
+
export const Crop = () => {
|
|
15
|
+
const { getCurrentAction, getLastHistoryItem } = useImageEditorContext();
|
|
16
|
+
const currentAction = getCurrentAction();
|
|
17
|
+
const mobile = useMobile();
|
|
18
|
+
|
|
19
|
+
const imgRef = useRef<HTMLImageElement>(null);
|
|
20
|
+
const cropRef = useRef<HTMLDivElement>(null);
|
|
21
|
+
|
|
22
|
+
const { handleCropStart } = useCropInteraction({
|
|
23
|
+
imgRef,
|
|
24
|
+
cropRef,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (!currentAction || currentAction.name !== "crop") {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { width, height, base64 } = getLastHistoryItem();
|
|
32
|
+
const { x, y, w, h } = getInitalCrop(currentAction.args.ratio, width, height);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<>
|
|
36
|
+
<img
|
|
37
|
+
ref={imgRef}
|
|
38
|
+
src={base64}
|
|
39
|
+
alt="Image"
|
|
40
|
+
className="crop-image"
|
|
41
|
+
style={{ clipPath: `xywh(${x}% ${y}% ${w}% ${h}%)` }}
|
|
42
|
+
/>
|
|
43
|
+
<div
|
|
44
|
+
ref={cropRef}
|
|
45
|
+
className="crop-box"
|
|
46
|
+
style={{ width: `${w}%`, height: `${h}%`, top: `${y}%`, left: `${x}%` }}
|
|
47
|
+
>
|
|
48
|
+
<CropLines />
|
|
49
|
+
{mobile ? (
|
|
50
|
+
<CropPointer onMouseDown={handleCropStart} />
|
|
51
|
+
) : (
|
|
52
|
+
<>
|
|
53
|
+
<CropBorders onMouseDown={handleCropStart} />
|
|
54
|
+
<CropPointers onMouseDown={handleCropStart} />
|
|
55
|
+
</>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
</>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Direction } from "../types";
|
|
2
|
+
|
|
3
|
+
type CropBordersProps = {
|
|
4
|
+
onMouseDown: (e: React.MouseEvent, type: Direction, cursor: string) => void;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const CropBorders = ({ onMouseDown }: CropBordersProps) => {
|
|
8
|
+
return (
|
|
9
|
+
<>
|
|
10
|
+
<span
|
|
11
|
+
className="crop-b crop-b-t"
|
|
12
|
+
onMouseDown={(e: React.MouseEvent) => onMouseDown(e, "t", "ns")}
|
|
13
|
+
/>
|
|
14
|
+
<span
|
|
15
|
+
className="crop-b crop-b-r"
|
|
16
|
+
onMouseDown={(e) => onMouseDown(e, "r", "ew")}
|
|
17
|
+
/>
|
|
18
|
+
<span
|
|
19
|
+
className="crop-b crop-b-b"
|
|
20
|
+
onMouseDown={(e) => onMouseDown(e, "b", "ns")}
|
|
21
|
+
/>
|
|
22
|
+
<span
|
|
23
|
+
className="crop-b crop-b-l"
|
|
24
|
+
onMouseDown={(e) => onMouseDown(e, "l", "ew")}
|
|
25
|
+
/>
|
|
26
|
+
</>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Direction } from "../types";
|
|
3
|
+
|
|
4
|
+
type CropPointerProps = {
|
|
5
|
+
onMouseDown: (
|
|
6
|
+
e: React.MouseEvent | React.TouchEvent,
|
|
7
|
+
type: Direction,
|
|
8
|
+
cursor?: string,
|
|
9
|
+
) => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const CropPointer = ({ onMouseDown }: CropPointerProps) => {
|
|
13
|
+
return (
|
|
14
|
+
<div className="crop-p-b">
|
|
15
|
+
<div
|
|
16
|
+
className="crop-p-p"
|
|
17
|
+
onTouchStart={(e) => onMouseDown(e, "br", "nwse")}
|
|
18
|
+
/>
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Direction } from "../types";
|
|
2
|
+
|
|
3
|
+
type CropPointersProps = {
|
|
4
|
+
onMouseDown: (e: React.MouseEvent, type: Direction, cursor: string) => void;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const CropPointers = ({ onMouseDown }: CropPointersProps) => {
|
|
8
|
+
return (
|
|
9
|
+
<>
|
|
10
|
+
<span
|
|
11
|
+
className="crop-ps crop-ps-tl"
|
|
12
|
+
onMouseDown={(e) => onMouseDown(e, "tl", "nwse")}
|
|
13
|
+
/>
|
|
14
|
+
<span
|
|
15
|
+
className="crop-ps crop-ps-t"
|
|
16
|
+
onMouseDown={(e) => onMouseDown(e, "t", "ns")}
|
|
17
|
+
/>
|
|
18
|
+
<span
|
|
19
|
+
className="crop-ps crop-ps-tr"
|
|
20
|
+
onMouseDown={(e) => onMouseDown(e, "tr", "nesw")}
|
|
21
|
+
/>
|
|
22
|
+
<span
|
|
23
|
+
className="crop-ps crop-ps-r"
|
|
24
|
+
onMouseDown={(e) => onMouseDown(e, "r", "ew")}
|
|
25
|
+
/>
|
|
26
|
+
<span
|
|
27
|
+
className="crop-ps crop-ps-br"
|
|
28
|
+
onMouseDown={(e) => onMouseDown(e, "br", "nwse")}
|
|
29
|
+
/>
|
|
30
|
+
<span
|
|
31
|
+
className="crop-ps crop-ps-b"
|
|
32
|
+
onMouseDown={(e) => onMouseDown(e, "b", "ns")}
|
|
33
|
+
/>
|
|
34
|
+
<span
|
|
35
|
+
className="crop-ps crop-ps-bl"
|
|
36
|
+
onMouseDown={(e) => onMouseDown(e, "bl", "nesw")}
|
|
37
|
+
/>
|
|
38
|
+
<span
|
|
39
|
+
className="crop-ps crop-ps-l"
|
|
40
|
+
onMouseDown={(e) => onMouseDown(e, "l", "ew")}
|
|
41
|
+
/>
|
|
42
|
+
</>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { Button, Separator } from "../ui";
|
|
3
|
+
import type { CropRect } from "../types";
|
|
4
|
+
import { useImageEditorContext } from "../provider/useImageEditorContext";
|
|
5
|
+
import { eventBus } from "../eventBus";
|
|
6
|
+
import { getInitalCrop } from "../utils";
|
|
7
|
+
import { useImageProcessor } from "../hooks";
|
|
8
|
+
import { Check, Loader, X } from "../assets/icons";
|
|
9
|
+
|
|
10
|
+
export const CropTools = () => {
|
|
11
|
+
const {
|
|
12
|
+
setCurrentAction,
|
|
13
|
+
getCurrentAction,
|
|
14
|
+
getLastHistoryItem,
|
|
15
|
+
addToHistory,
|
|
16
|
+
setSidebar,
|
|
17
|
+
} = useImageEditorContext();
|
|
18
|
+
const { crop, resize } = useImageProcessor();
|
|
19
|
+
const [loading, setLoading] = useState(false);
|
|
20
|
+
const action = getCurrentAction();
|
|
21
|
+
const { base64, width, height } = getLastHistoryItem();
|
|
22
|
+
|
|
23
|
+
const leftRef = useRef<HTMLDivElement>(null);
|
|
24
|
+
const topRef = useRef<HTMLDivElement>(null);
|
|
25
|
+
const widthRef = useRef<HTMLDivElement>(null);
|
|
26
|
+
const heightRef = useRef<HTMLDivElement>(null);
|
|
27
|
+
const cropRectRef = useRef<CropRect | null>(null);
|
|
28
|
+
|
|
29
|
+
const handleClose = () => {
|
|
30
|
+
setCurrentAction(null);
|
|
31
|
+
setSidebar(true);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (action?.name !== "crop") {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const { x, y, w, h } = getInitalCrop(action.args.ratio, width, height);
|
|
39
|
+
const xPx = Math.round((x / 100) * width);
|
|
40
|
+
const yPx = Math.round((y / 100) * height);
|
|
41
|
+
const wPx = Math.round((w / 100) * width);
|
|
42
|
+
const hPx = Math.round((h / 100) * height);
|
|
43
|
+
|
|
44
|
+
if (leftRef.current) leftRef.current.textContent = xPx.toString();
|
|
45
|
+
if (topRef.current) topRef.current.textContent = yPx.toString();
|
|
46
|
+
if (widthRef.current) widthRef.current.textContent = wPx.toString();
|
|
47
|
+
if (heightRef.current) heightRef.current.textContent = hPx.toString();
|
|
48
|
+
cropRectRef.current = { x: xPx, y: yPx, w: wPx, h: hPx };
|
|
49
|
+
|
|
50
|
+
const onCropUpdate = (event: Event) => {
|
|
51
|
+
const customEvent = event as CustomEvent<CropRect>;
|
|
52
|
+
const { x, y, w, h } = customEvent.detail;
|
|
53
|
+
cropRectRef.current = customEvent.detail;
|
|
54
|
+
if (leftRef.current) leftRef.current.textContent = x.toString();
|
|
55
|
+
if (topRef.current) topRef.current.textContent = y.toString();
|
|
56
|
+
if (widthRef.current) widthRef.current.textContent = w.toString();
|
|
57
|
+
if (heightRef.current) heightRef.current.textContent = h.toString();
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
eventBus.addEventListener("crop-update", onCropUpdate);
|
|
61
|
+
|
|
62
|
+
return () => {
|
|
63
|
+
eventBus.removeEventListener("crop-update", onCropUpdate);
|
|
64
|
+
};
|
|
65
|
+
}, [action, width, height]);
|
|
66
|
+
|
|
67
|
+
const handleSave = async () => {
|
|
68
|
+
if (!action || action.name !== "crop") {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
setLoading(true);
|
|
73
|
+
if (!cropRectRef.current) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const { x, y, w, h } = cropRectRef.current;
|
|
77
|
+
const { preset } = action.args;
|
|
78
|
+
let processedBase64 = await crop(base64, x, y, w, h);
|
|
79
|
+
|
|
80
|
+
if (preset) {
|
|
81
|
+
const { w, h } = preset;
|
|
82
|
+
processedBase64 = await resize(processedBase64, w, h);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
addToHistory({
|
|
86
|
+
name: "Crop",
|
|
87
|
+
base64: processedBase64,
|
|
88
|
+
width: preset ? preset.w : cropRectRef.current!.w,
|
|
89
|
+
height: preset ? preset.h : cropRectRef.current!.h,
|
|
90
|
+
});
|
|
91
|
+
setSidebar(true);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error("Failed to process image:", error);
|
|
94
|
+
} finally {
|
|
95
|
+
setLoading(false);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div className="crop-tools">
|
|
101
|
+
<div className="crop-tools-info">
|
|
102
|
+
<div className="crop-tools-info-label">left</div>
|
|
103
|
+
<div className="crop-tools-info-value semibold" ref={leftRef} />
|
|
104
|
+
</div>
|
|
105
|
+
<div className="crop-tools-info">
|
|
106
|
+
<div className="crop-tools-info-label">top</div>
|
|
107
|
+
<div className="crop-tools-info-value semibold" ref={topRef} />
|
|
108
|
+
</div>
|
|
109
|
+
<div className="crop-tools-info">
|
|
110
|
+
<div className="crop-tools-info-label">width</div>
|
|
111
|
+
<div className="crop-tools-info-value semibold" ref={widthRef} />
|
|
112
|
+
</div>
|
|
113
|
+
<div className="crop-tools-info">
|
|
114
|
+
<div className="crop-tools-info-label">height</div>
|
|
115
|
+
<div className="crop-tools-info-value semibold" ref={heightRef} />
|
|
116
|
+
</div>
|
|
117
|
+
<Separator orientation="vertical" />
|
|
118
|
+
<Button
|
|
119
|
+
variant="ghost"
|
|
120
|
+
className="text-green"
|
|
121
|
+
style={{ height: "36px", width: "36px" }}
|
|
122
|
+
disabled={loading}
|
|
123
|
+
onClick={handleSave}
|
|
124
|
+
>
|
|
125
|
+
{loading ? <Loader /> : <Check />}
|
|
126
|
+
</Button>
|
|
127
|
+
<Separator orientation="vertical" />
|
|
128
|
+
<Button
|
|
129
|
+
variant="ghost"
|
|
130
|
+
style={{ height: "36px", width: "36px" }}
|
|
131
|
+
disabled={loading}
|
|
132
|
+
onClick={handleClose}
|
|
133
|
+
>
|
|
134
|
+
<X className="text-red" />
|
|
135
|
+
</Button>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
};
|