diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml
new file mode 100644
index 0000000..38b3379
--- /dev/null
+++ b/.github/workflows/code-quality.yml
@@ -0,0 +1,25 @@
+name: Code Quality Check
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '22'
+ cache: 'npm'
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Run linting check
+ run: npm run lint:check
\ No newline at end of file
diff --git a/README.md b/README.md
index 9e89a2b..88a084e 100644
--- a/README.md
+++ b/README.md
@@ -271,6 +271,13 @@ The `demo/` folder contains the following examples:
- Safari (latest versions)
- Edge (latest versions)
+## Linting and Formatting
+
+This project uses [Biome](https://biomejs.dev/) for code formatting and linting.
+
+- Run `npm run lint:check` to check for formatting and linting issues.
+- Run `npm run lint:fix` to automatically fix formatting and safe linting issues.
+
## License
MIT
diff --git a/biome.json b/biome.json
new file mode 100644
index 0000000..9fe7014
--- /dev/null
+++ b/biome.json
@@ -0,0 +1,30 @@
+{
+ "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
+ "vcs": {
+ "enabled": false,
+ "clientKind": "git",
+ "useIgnoreFile": false
+ },
+ "files": {
+ "ignoreUnknown": false,
+ "ignore": ["dist/**"]
+ },
+ "formatter": {
+ "enabled": true,
+ "indentStyle": "tab"
+ },
+ "organizeImports": {
+ "enabled": true
+ },
+ "linter": {
+ "enabled": true,
+ "rules": {
+ "recommended": true
+ }
+ },
+ "javascript": {
+ "formatter": {
+ "quoteStyle": "double"
+ }
+ }
+}
diff --git a/demo/index2.html b/demo/index-local.html
similarity index 100%
rename from demo/index2.html
rename to demo/index-local.html
diff --git a/demo/react-integration.jsx b/demo/react-integration.jsx
index 5ad3d60..2b02905 100644
--- a/demo/react-integration.jsx
+++ b/demo/react-integration.jsx
@@ -1,150 +1,156 @@
-import React, { useEffect, useRef, useState } from 'react';
+import React, { useEffect, useRef, useState } from "react";
// Dans un projet réel, vous importeriez depuis le package npm
// import ImageTool from 'image-tool';
// Composant d'exemple pour l'intégration avec React
function ImageEditor() {
- const imageRef = useRef(null);
- const toolRef = useRef(null);
- const [focusPoint, setFocusPoint] = useState({ x: 0, y: 0 });
- const [cropZone, setCropZone] = useState({ x: 0, y: 0, width: 0, height: 0 });
- const [focusActive, setFocusActive] = useState(false);
- const [cropActive, setCropActive] = useState(false);
+ const imageRef = useRef(null);
+ const toolRef = useRef(null);
+ const [focusPoint, setFocusPoint] = useState({ x: 0, y: 0 });
+ const [cropZone, setCropZone] = useState({ x: 0, y: 0, width: 0, height: 0 });
+ const [focusActive, setFocusActive] = useState(false);
+ const [cropActive, setCropActive] = useState(false);
- // Initialiser l'outil lorsque le composant est monté
- useEffect(() => {
- if (imageRef.current && !toolRef.current) {
- // Attendre que l'image soit chargée
- if (!imageRef.current.complete) {
- imageRef.current.onload = initializeTool;
- } else {
- initializeTool();
- }
- }
+ // Initialiser l'outil lorsque le composant est monté
+ useEffect(() => {
+ if (imageRef.current && !toolRef.current) {
+ // Attendre que l'image soit chargée
+ if (!imageRef.current.complete) {
+ imageRef.current.onload = initializeTool;
+ } else {
+ initializeTool();
+ }
+ }
- // Nettoyer lors du démontage du composant
- return () => {
- if (toolRef.current) {
- toolRef.current.destroy();
- toolRef.current = null;
- }
- };
- }, []);
+ // Nettoyer lors du démontage du composant
+ return () => {
+ if (toolRef.current) {
+ toolRef.current.destroy();
+ toolRef.current = null;
+ }
+ };
+ }, []);
- // Fonction pour initialiser l'outil
- const initializeTool = () => {
- // Dans un projet réel, vous utiliseriez l'import
- // Ici, nous supposons que ImageTool est disponible globalement
- toolRef.current = new ImageTool({
- imageElement: imageRef.current,
- focusPoint: {
- enabled: true,
- style: {
- width: '30px',
- height: '30px',
- border: '3px solid white',
- boxShadow: '0 0 0 2px black, 0 0 5px rgba(0,0,0,0.5)',
- backgroundColor: 'rgba(255, 0, 0, 0.5)'
- }
- },
- cropZone: {
- enabled: true,
- style: {
- border: '1px dashed #fff',
- backgroundColor: 'rgba(0, 0, 0, 0.4)'
- }
- },
- onChange: handleToolChange
- });
- };
+ // Fonction pour initialiser l'outil
+ const initializeTool = () => {
+ // Dans un projet réel, vous utiliseriez l'import
+ // Ici, nous supposons que ImageTool est disponible globalement
+ toolRef.current = new ImageTool({
+ imageElement: imageRef.current,
+ focusPoint: {
+ enabled: true,
+ style: {
+ width: "30px",
+ height: "30px",
+ border: "3px solid white",
+ boxShadow: "0 0 0 2px black, 0 0 5px rgba(0,0,0,0.5)",
+ backgroundColor: "rgba(255, 0, 0, 0.5)",
+ },
+ },
+ cropZone: {
+ enabled: true,
+ style: {
+ border: "1px dashed #fff",
+ backgroundColor: "rgba(0, 0, 0, 0.4)",
+ },
+ },
+ onChange: handleToolChange,
+ });
+ };
- // Gérer les changements de l'outil
- const handleToolChange = (data) => {
- setFocusPoint(data.focusPoint);
- setCropZone(data.cropZone);
- setFocusActive(data.focusActive);
- setCropActive(data.cropActive);
- };
+ // Gérer les changements de l'outil
+ const handleToolChange = (data) => {
+ setFocusPoint(data.focusPoint);
+ setCropZone(data.cropZone);
+ setFocusActive(data.focusActive);
+ setCropActive(data.cropActive);
+ };
- // Activer/désactiver le point focal
- const toggleFocusPoint = () => {
- if (toolRef.current) {
- toolRef.current.toggleFocusPoint();
- }
- };
+ // Activer/désactiver le point focal
+ const toggleFocusPoint = () => {
+ if (toolRef.current) {
+ toolRef.current.toggleFocusPoint();
+ }
+ };
- // Activer/désactiver la zone de recadrage
- const toggleCropZone = () => {
- if (toolRef.current) {
- toolRef.current.toggleCropZone();
- }
- };
+ // Activer/désactiver la zone de recadrage
+ const toggleCropZone = () => {
+ if (toolRef.current) {
+ toolRef.current.toggleCropZone();
+ }
+ };
- // Centrer le point focal
- const centerFocusPoint = () => {
- if (toolRef.current) {
- const dimensions = toolRef.current.getImageDimensions();
- toolRef.current.setFocusPoint(
- dimensions.width / 2,
- dimensions.height / 2
- );
- }
- };
+ // Centrer le point focal
+ const centerFocusPoint = () => {
+ if (toolRef.current) {
+ const dimensions = toolRef.current.getImageDimensions();
+ toolRef.current.setFocusPoint(
+ dimensions.width / 2,
+ dimensions.height / 2,
+ );
+ }
+ };
- // Réinitialiser la zone de recadrage
- const resetCropZone = () => {
- if (toolRef.current) {
- const dimensions = toolRef.current.getImageDimensions();
- const width = dimensions.width / 2;
- const height = dimensions.height / 2;
- toolRef.current.setCropZone(
- (dimensions.width - width) / 2,
- (dimensions.height - height) / 2,
- width,
- height
- );
- }
- };
+ // Réinitialiser la zone de recadrage
+ const resetCropZone = () => {
+ if (toolRef.current) {
+ const dimensions = toolRef.current.getImageDimensions();
+ const width = dimensions.width / 2;
+ const height = dimensions.height / 2;
+ toolRef.current.setCropZone(
+ (dimensions.width - width) / 2,
+ (dimensions.height - height) / 2,
+ width,
+ height,
+ );
+ }
+ };
- return (
-
-
-

-
+ return (
+
+
+

+
-
-
Contrôles
-
-
+
+
Contrôles
+
+
-
Actions
-
-
+
Actions
+
+
-
Données actuelles
-
-
Point Focal
-
- X: {Math.round(focusPoint.x)}, Y: {Math.round(focusPoint.y)}
-
+
Données actuelles
+
+
Point Focal
+
+ X: {Math.round(focusPoint.x)}, Y: {Math.round(focusPoint.y)}
+
-
Zone de Recadrage
-
- X: {Math.round(cropZone.x)}, Y: {Math.round(cropZone.y)},
- Largeur: {Math.round(cropZone.width)}, Hauteur: {Math.round(cropZone.height)}
-
-
-
-
- );
+
Zone de Recadrage
+
+ X: {Math.round(cropZone.x)}, Y: {Math.round(cropZone.y)}, Largeur:{" "}
+ {Math.round(cropZone.width)}, Hauteur: {Math.round(cropZone.height)}
+
+
+
+
+ );
}
export default ImageEditor;
diff --git a/demo/vue-integration.js b/demo/vue-integration.js
index f526818..c716c49 100644
--- a/demo/vue-integration.js
+++ b/demo/vue-integration.js
@@ -1,112 +1,112 @@
-import Vue from 'vue';
+import Vue from "vue";
// Dans un projet réel, vous importeriez depuis le package npm
// import ImageTool from 'image-tool';
// Composant d'exemple pour l'intégration avec Vue.js
export default {
- name: 'ImageEditor',
-
- data() {
- return {
- imageTool: null,
- focusPoint: { x: 0, y: 0 },
- cropZone: { x: 0, y: 0, width: 0, height: 0 },
- focusActive: false,
- cropActive: false
- };
- },
-
- mounted() {
- // Attendre que l'image soit chargée
- if (this.$refs.editableImage.complete) {
- this.initializeTool();
- } else {
- this.$refs.editableImage.onload = this.initializeTool;
- }
- },
-
- beforeDestroy() {
- // Nettoyer lors de la destruction du composant
- if (this.imageTool) {
- this.imageTool.destroy();
- this.imageTool = null;
- }
- },
-
- methods: {
- // Initialiser l'outil
- initializeTool() {
- // Dans un projet réel, vous utiliseriez l'import
- // Ici, nous supposons que ImageTool est disponible globalement
- this.imageTool = new ImageTool({
- imageElement: this.$refs.editableImage,
- focusPoint: {
- enabled: true,
- style: {
- width: '30px',
- height: '30px',
- border: '3px solid white',
- boxShadow: '0 0 0 2px black, 0 0 5px rgba(0,0,0,0.5)',
- backgroundColor: 'rgba(255, 0, 0, 0.5)'
- }
- },
- cropZone: {
- enabled: true,
- style: {
- border: '1px dashed #fff',
- backgroundColor: 'rgba(0, 0, 0, 0.4)'
- }
- },
- onChange: this.handleToolChange
- });
- },
-
- // Gérer les changements de l'outil
- handleToolChange(data) {
- this.focusPoint = data.focusPoint;
- this.cropZone = data.cropZone;
- this.focusActive = data.focusActive;
- this.cropActive = data.cropActive;
- },
-
- // Activer/désactiver le point focal
- toggleFocusPoint() {
- if (this.imageTool) {
- this.imageTool.toggleFocusPoint();
- }
- },
-
- // Activer/désactiver la zone de recadrage
- toggleCropZone() {
- if (this.imageTool) {
- this.imageTool.toggleCropZone();
- }
- },
-
- // Centrer le point focal
- centerFocusPoint() {
- if (this.imageTool) {
- const dimensions = this.imageTool.getImageDimensions();
- this.imageTool.setFocusPoint(
- dimensions.width / 2,
- dimensions.height / 2
- );
- }
- },
-
- // Réinitialiser la zone de recadrage
- resetCropZone() {
- if (this.imageTool) {
- const dimensions = this.imageTool.getImageDimensions();
- const width = dimensions.width / 2;
- const height = dimensions.height / 2;
- this.imageTool.setCropZone(
- (dimensions.width - width) / 2,
- (dimensions.height - height) / 2,
- width,
- height
- );
- }
- }
- }
+ name: "ImageEditor",
+
+ data() {
+ return {
+ imageTool: null,
+ focusPoint: { x: 0, y: 0 },
+ cropZone: { x: 0, y: 0, width: 0, height: 0 },
+ focusActive: false,
+ cropActive: false,
+ };
+ },
+
+ mounted() {
+ // Attendre que l'image soit chargée
+ if (this.$refs.editableImage.complete) {
+ this.initializeTool();
+ } else {
+ this.$refs.editableImage.onload = this.initializeTool;
+ }
+ },
+
+ beforeDestroy() {
+ // Nettoyer lors de la destruction du composant
+ if (this.imageTool) {
+ this.imageTool.destroy();
+ this.imageTool = null;
+ }
+ },
+
+ methods: {
+ // Initialiser l'outil
+ initializeTool() {
+ // Dans un projet réel, vous utiliseriez l'import
+ // Ici, nous supposons que ImageTool est disponible globalement
+ this.imageTool = new ImageTool({
+ imageElement: this.$refs.editableImage,
+ focusPoint: {
+ enabled: true,
+ style: {
+ width: "30px",
+ height: "30px",
+ border: "3px solid white",
+ boxShadow: "0 0 0 2px black, 0 0 5px rgba(0,0,0,0.5)",
+ backgroundColor: "rgba(255, 0, 0, 0.5)",
+ },
+ },
+ cropZone: {
+ enabled: true,
+ style: {
+ border: "1px dashed #fff",
+ backgroundColor: "rgba(0, 0, 0, 0.4)",
+ },
+ },
+ onChange: this.handleToolChange,
+ });
+ },
+
+ // Gérer les changements de l'outil
+ handleToolChange(data) {
+ this.focusPoint = data.focusPoint;
+ this.cropZone = data.cropZone;
+ this.focusActive = data.focusActive;
+ this.cropActive = data.cropActive;
+ },
+
+ // Activer/désactiver le point focal
+ toggleFocusPoint() {
+ if (this.imageTool) {
+ this.imageTool.toggleFocusPoint();
+ }
+ },
+
+ // Activer/désactiver la zone de recadrage
+ toggleCropZone() {
+ if (this.imageTool) {
+ this.imageTool.toggleCropZone();
+ }
+ },
+
+ // Centrer le point focal
+ centerFocusPoint() {
+ if (this.imageTool) {
+ const dimensions = this.imageTool.getImageDimensions();
+ this.imageTool.setFocusPoint(
+ dimensions.width / 2,
+ dimensions.height / 2,
+ );
+ }
+ },
+
+ // Réinitialiser la zone de recadrage
+ resetCropZone() {
+ if (this.imageTool) {
+ const dimensions = this.imageTool.getImageDimensions();
+ const width = dimensions.width / 2;
+ const height = dimensions.height / 2;
+ this.imageTool.setCropZone(
+ (dimensions.width - width) / 2,
+ (dimensions.height - height) / 2,
+ width,
+ height,
+ );
+ }
+ },
+ },
};
diff --git a/package-lock.json b/package-lock.json
index 2c1cc27..15d93ee 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,12 +9,177 @@
"version": "1.0.0",
"license": "MIT",
"devDependencies": {
+ "@biomejs/biome": "1.9.4",
"@rollup/plugin-node-resolve": "^15.0.0",
"@rollup/plugin-terser": "^0.4",
"gh-pages": "^6.3.0",
"rollup": "^2.79.0"
}
},
+ "node_modules/@biomejs/biome": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz",
+ "integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT OR Apache-2.0",
+ "bin": {
+ "biome": "bin/biome"
+ },
+ "engines": {
+ "node": ">=14.21.3"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/biome"
+ },
+ "optionalDependencies": {
+ "@biomejs/cli-darwin-arm64": "1.9.4",
+ "@biomejs/cli-darwin-x64": "1.9.4",
+ "@biomejs/cli-linux-arm64": "1.9.4",
+ "@biomejs/cli-linux-arm64-musl": "1.9.4",
+ "@biomejs/cli-linux-x64": "1.9.4",
+ "@biomejs/cli-linux-x64-musl": "1.9.4",
+ "@biomejs/cli-win32-arm64": "1.9.4",
+ "@biomejs/cli-win32-x64": "1.9.4"
+ }
+ },
+ "node_modules/@biomejs/cli-darwin-arm64": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz",
+ "integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-darwin-x64": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz",
+ "integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-linux-arm64": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz",
+ "integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-linux-arm64-musl": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz",
+ "integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-linux-x64": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz",
+ "integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-linux-x64-musl": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz",
+ "integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-win32-arm64": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz",
+ "integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-win32-x64": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz",
+ "integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
diff --git a/package.json b/package.json
index b122b03..3badb69 100644
--- a/package.json
+++ b/package.json
@@ -1,44 +1,44 @@
{
- "name": "@h4md1/visual-image-tool",
- "version": "1.0.0",
- "description": "Un outil léger en JavaScript vanilla pour définir des points focaux et zones de recadrage sur des images",
- "main": "dist/image-tool.js",
- "module": "dist/image-tool.esm.js",
- "browser": "dist/image-tool.umd.js",
- "files": [
- "dist",
- "src"
- ],
- "scripts": {
- "build": "rollup -c",
- "dev": "rollup -c -w",
- "demo": "npx http-server -o ./demo",
- "publish:demo": "gh-pages -d demo",
- "test": "echo \"Error: no test specified\" && exit 1",
- "prepublishOnly": "npm run build"
- },
- "keywords": [
- "image",
- "crop",
- "focus-point",
- "vanilla-js",
- "image-editor",
- "crop-tool"
- ],
- "author": "",
- "license": "MIT",
- "devDependencies": {
- "@rollup/plugin-node-resolve": "^15.0.0",
- "@rollup/plugin-terser": "^0.4",
- "gh-pages": "^6.3.0",
- "rollup": "^2.79.0"
- },
- "repository": {
- "type": "git",
- "url": "git+https://github.com/killerwolf/visual-image-tool.git"
- },
- "bugs": {
- "url": "https://github.com/killerwolf/visual-image-tool/issues"
- },
- "homepage": "https://github.com/killerwolf/visual-image-tool#readme"
+ "name": "@h4md1/visual-image-tool",
+ "version": "1.0.0",
+ "description": "Un outil léger en JavaScript vanilla pour définir des points focaux et zones de recadrage sur des images",
+ "main": "dist/image-tool.js",
+ "module": "dist/image-tool.esm.js",
+ "browser": "dist/image-tool.umd.js",
+ "files": ["dist", "src"],
+ "scripts": {
+ "build": "rollup -c",
+ "dev": "rollup -c -w",
+ "demo": "npx http-server -o ./demo",
+ "publish:demo": "gh-pages -d demo",
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "prepublishOnly": "npm run build",
+ "lint:fix": "biome check --apply .",
+ "lint:check": "biome check ."
+ },
+ "keywords": [
+ "image",
+ "crop",
+ "focus-point",
+ "vanilla-js",
+ "image-editor",
+ "crop-tool"
+ ],
+ "author": "",
+ "license": "MIT",
+ "devDependencies": {
+ "@biomejs/biome": "1.9.4",
+ "@rollup/plugin-node-resolve": "^15.0.0",
+ "@rollup/plugin-terser": "^0.4",
+ "gh-pages": "^6.3.0",
+ "rollup": "^2.79.0"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/killerwolf/visual-image-tool.git"
+ },
+ "bugs": {
+ "url": "https://github.com/killerwolf/visual-image-tool/issues"
+ },
+ "homepage": "https://github.com/killerwolf/visual-image-tool#readme"
}
diff --git a/rollup.config.js b/rollup.config.js
index 113c036..faa7128 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -1,43 +1,36 @@
-import resolve from '@rollup/plugin-node-resolve';
-import terser from '@rollup/plugin-terser';
+import resolve from "@rollup/plugin-node-resolve";
+import terser from "@rollup/plugin-terser";
export default [
- // ESM build
- {
- input: 'src/index.js',
- output: {
- file: 'dist/visual-image-tool.esm.js',
- format: 'esm',
- sourcemap: true
- },
- plugins: [
- resolve()
- ]
- },
- // UMD build (minified)
- {
- input: 'src/index.js',
- output: {
- file: 'dist/visual-image-tool.umd.js',
- format: 'umd',
- name: 'VisualImageTool',
- sourcemap: true
- },
- plugins: [
- resolve(),
- terser()
- ]
- },
- // CommonJS build
- {
- input: 'src/index.js',
- output: {
- file: 'dist/visual-image-tool.js',
- format: 'cjs',
- sourcemap: true
- },
- plugins: [
- resolve()
- ]
- }
+ // ESM build
+ {
+ input: "src/index.js",
+ output: {
+ file: "dist/visual-image-tool.esm.js",
+ format: "esm",
+ sourcemap: true,
+ },
+ plugins: [resolve()],
+ },
+ // UMD build (minified)
+ {
+ input: "src/index.js",
+ output: {
+ file: "dist/visual-image-tool.umd.js",
+ format: "umd",
+ name: "VisualImageTool",
+ sourcemap: true,
+ },
+ plugins: [resolve(), terser()],
+ },
+ // CommonJS build
+ {
+ input: "src/index.js",
+ output: {
+ file: "dist/visual-image-tool.js",
+ format: "cjs",
+ sourcemap: true,
+ },
+ plugins: [resolve()],
+ },
];
diff --git a/src/index.js b/src/index.js
index fa711a1..5f5e2a5 100644
--- a/src/index.js
+++ b/src/index.js
@@ -3,7 +3,7 @@
* Exporte la classe VisualImageTool
*/
-import VisualImageTool from './visual-image-tool.js';
+import VisualImageTool from "./visual-image-tool.js";
// Exporter la classe comme export par défaut
export default VisualImageTool;
diff --git a/src/visual-image-tool.js b/src/visual-image-tool.js
index 31b8d80..7c7d3c3 100644
--- a/src/visual-image-tool.js
+++ b/src/visual-image-tool.js
@@ -4,843 +4,927 @@
*/
class VisualImageTool {
- /**
- * Crée une instance de l'outil d'image
- * @param {Object} options - Options de configuration
- * @param {HTMLElement|string} options.imageElement - Élément image ou sélecteur CSS
- * @param {Object} [options.focusPoint] - Configuration du point focal
- * @param {boolean} [options.focusPoint.enabled=true] - Activer la fonctionnalité de point focal
- * @param {Object} [options.focusPoint.style] - Styles personnalisés pour le marqueur de point focal
- * @param {Object} [options.cropZone] - Configuration de la zone de recadrage
- * @param {boolean} [options.cropZone.enabled=true] - Activer la fonctionnalité de zone de recadrage
- * @param {Object} [options.cropZone.style] - Styles personnalisés pour l'overlay de recadrage
- * @param {Function} [options.onChange] - Callback appelé lors des changements
- */
- constructor(options) {
- // Valider les options
- if (!options || !options.imageElement) {
- throw new Error('L\'élément image est requis');
- }
-
- // Initialiser les propriétés
- this.imageElement = typeof options.imageElement === 'string'
- ? document.querySelector(options.imageElement)
- : options.imageElement;
-
- if (!this.imageElement || this.imageElement.tagName !== 'IMG') {
- throw new Error('Élément image invalide');
- }
-
- // Options par défaut
- this.options = {
- focusPoint: {
- enabled: true,
- style: {
- width: '30px',
- height: '30px',
- border: '3px solid white',
- boxShadow: '0 0 0 2px black, 0 0 5px rgba(0,0,0,0.5)',
- backgroundColor: 'rgba(255, 0, 0, 0.5)'
- },
- ...options.focusPoint
- },
- cropZone: {
- enabled: true,
- style: {
- border: '1px dashed #fff',
- backgroundColor: 'rgba(0, 0, 0, 0.4)'
- },
- handleStyle: {
- width: '14px',
- height: '14px',
- backgroundColor: 'white',
- border: '2px solid black',
- boxShadow: '0 0 3px rgba(0,0,0,0.5)'
- },
- ...options.cropZone
- },
- onChange: options.onChange || (() => {}),
- debug: options.debug || false
- };
-
- // État interne
- this.state = {
- focusMarker: null,
- cropOverlay: null,
- focusActive: false,
- cropActive: false,
- focusPoint: { x: 0, y: 0 },
- cropZone: { x: 0, y: 0, width: 0, height: 0 },
- originalWidth: 1,
- originalHeight: 1,
- displayWidth: 1,
- displayHeight: 1,
- scaleX: 1,
- scaleY: 1
- };
-
- // Variables pour le suivi des interactions
- this.interaction = {
- focusDragging: false,
- focusDragOffsetX: 0,
- focusDragOffsetY: 0,
- cropDragging: false,
- cropResizing: false,
- activeHandle: null,
- startX: 0,
- startY: 0,
- startWidth: 0,
- startHeight: 0,
- startMouseX: 0,
- startMouseY: 0
- };
-
- // Initialiser l'outil
- this._init();
- }
-
- /**
- * Initialise l'outil d'image
- * @private
- */
- _init() {
- // Préparer le conteneur parent
- this._prepareContainer();
-
- // Initialiser les dimensions
- this._updateScaling();
-
- // Créer les éléments d'interface si activés
- if (this.options.focusPoint.enabled) {
- this._createFocusMarker();
- }
-
- if (this.options.cropZone.enabled) {
- this._createCropOverlay();
- }
-
- // Ajouter les écouteurs d'événements
- this._setupEventListeners();
- }
-
- /**
- * Prépare le conteneur parent de l'image
- * @private
- */
- _prepareContainer() {
- // S'assurer que le parent est positionné
- if (this.imageElement.parentNode) {
- this.imageElement.parentNode.style.position = 'relative';
- }
- }
-
- /**
- * Met à jour les facteurs d'échelle
- * @private
- */
- _updateScaling() {
- this.state.originalWidth = this.imageElement.naturalWidth || 1;
- this.state.originalHeight = this.imageElement.naturalHeight || 1;
- this.state.displayWidth = this.imageElement.offsetWidth;
- this.state.displayHeight = this.imageElement.offsetHeight;
- this.state.scaleX = this.state.displayWidth / this.state.originalWidth;
- this.state.scaleY = this.state.displayHeight / this.state.originalHeight;
-
- // Repositionner les éléments si actifs
- if (this.state.focusActive) {
- this._updateFocusMarkerPosition();
- }
-
- if (this.state.cropActive) {
- this._updateCropOverlayPosition();
- }
- }
-
- /**
- * Convertit des coordonnées d'affichage en coordonnées originales
- * @private
- * @param {number} scaledX - Coordonnée X à l'échelle d'affichage
- * @param {number} scaledY - Coordonnée Y à l'échelle d'affichage
- * @returns {Object} Coordonnées originales
- */
- _toOriginalCoords(scaledX, scaledY) {
- return {
- x: scaledX / this.state.scaleX,
- y: scaledY / this.state.scaleY
- };
- }
-
- /**
- * Convertit des coordonnées originales en coordonnées d'affichage
- * @private
- * @param {number} originalX - Coordonnée X originale
- * @param {number} originalY - Coordonnée Y originale
- * @returns {Object} Coordonnées à l'échelle d'affichage
- */
- _toScaledCoords(originalX, originalY) {
- return {
- x: originalX * this.state.scaleX,
- y: originalY * this.state.scaleY
- };
- }
-
- /**
- * Crée le marqueur de point focal
- * @private
- */
- _createFocusMarker() {
- if (this.state.focusMarker) return;
-
- const marker = document.createElement('div');
- marker.style.position = 'absolute';
- marker.style.width = this.options.focusPoint.style.width;
- marker.style.height = this.options.focusPoint.style.height;
- marker.style.border = this.options.focusPoint.style.border;
- marker.style.boxShadow = this.options.focusPoint.style.boxShadow;
- marker.style.backgroundColor = this.options.focusPoint.style.backgroundColor;
- marker.style.cursor = 'move';
- marker.style.display = 'none';
- marker.style.zIndex = '999';
- marker.style.clipPath = 'polygon(40% 0%, 60% 0%, 60% 40%, 100% 40%, 100% 60%, 60% 60%, 60% 100%, 40% 100%, 40% 60%, 0% 60%, 0% 40%, 40% 40%)';
-
- this.imageElement.parentNode.appendChild(marker);
- this.state.focusMarker = marker;
-
- // Ajouter les écouteurs d'événements au marqueur
- marker.addEventListener('mousedown', this._handleFocusMarkerMouseDown.bind(this));
- }
-
- /**
- * Gère l'événement mousedown sur le marqueur de point focal
- * @private
- * @param {MouseEvent} e - Événement mousedown
- */
- _handleFocusMarkerMouseDown(e) {
- this.interaction.focusDragging = true;
- this.state.focusMarker.style.cursor = 'grabbing';
-
- // Calculer le décalage par rapport au centre du marqueur
- const rect = this.state.focusMarker.getBoundingClientRect();
- this.interaction.focusDragOffsetX = e.clientX - (rect.left + rect.width / 2);
- this.interaction.focusDragOffsetY = e.clientY - (rect.top + rect.height / 2);
-
- e.preventDefault();
- }
-
- /**
- * Crée l'overlay de zone de recadrage
- * @private
- */
- _createCropOverlay() {
- if (this.state.cropOverlay) return;
-
- const overlay = document.createElement('div');
- overlay.style.position = 'absolute';
- overlay.style.border = this.options.cropZone.style.border;
- overlay.style.backgroundColor = this.options.cropZone.style.backgroundColor;
- overlay.style.boxSizing = 'border-box';
- overlay.style.cursor = 'move';
- overlay.style.display = 'none';
- overlay.style.zIndex = '998';
-
- // Ajouter les poignées de redimensionnement
- const handles = ['tl', 'tm', 'tr', 'ml', 'mr', 'bl', 'bm', 'br'];
- handles.forEach(handleType => {
- const handle = document.createElement('div');
- handle.style.position = 'absolute';
- handle.style.width = this.options.cropZone.handleStyle.width;
- handle.style.height = this.options.cropZone.handleStyle.height;
- handle.style.backgroundColor = this.options.cropZone.handleStyle.backgroundColor;
- handle.style.border = this.options.cropZone.handleStyle.border;
- handle.style.boxShadow = this.options.cropZone.handleStyle.boxShadow;
- handle.style.boxSizing = 'border-box';
- handle.dataset.handle = handleType;
-
- // Positionner la poignée
- switch (handleType) {
- case 'tl': // Top-left
- handle.style.top = '-7px';
- handle.style.left = '-7px';
- handle.style.cursor = 'nwse-resize';
- break;
- case 'tm': // Top-middle
- handle.style.top = '-7px';
- handle.style.left = '50%';
- handle.style.marginLeft = '-7px';
- handle.style.cursor = 'ns-resize';
- break;
- case 'tr': // Top-right
- handle.style.top = '-7px';
- handle.style.right = '-7px';
- handle.style.cursor = 'nesw-resize';
- break;
- case 'ml': // Middle-left
- handle.style.top = '50%';
- handle.style.left = '-7px';
- handle.style.marginTop = '-7px';
- handle.style.cursor = 'ew-resize';
- break;
- case 'mr': // Middle-right
- handle.style.top = '50%';
- handle.style.right = '-7px';
- handle.style.marginTop = '-7px';
- handle.style.cursor = 'ew-resize';
- break;
- case 'bl': // Bottom-left
- handle.style.bottom = '-7px';
- handle.style.left = '-7px';
- handle.style.cursor = 'nesw-resize';
- break;
- case 'bm': // Bottom-middle
- handle.style.bottom = '-7px';
- handle.style.left = '50%';
- handle.style.marginLeft = '-7px';
- handle.style.cursor = 'ns-resize';
- break;
- case 'br': // Bottom-right
- handle.style.bottom = '-7px';
- handle.style.right = '-7px';
- handle.style.cursor = 'nwse-resize';
- break;
- }
-
- // Ajouter l'écouteur d'événement
- handle.addEventListener('mousedown', (e) => this._handleCropHandleMouseDown(e, handleType));
-
- overlay.appendChild(handle);
- });
-
- // Ajouter l'écouteur pour le déplacement de l'overlay
- overlay.addEventListener('mousedown', this._handleCropOverlayMouseDown.bind(this));
-
- this.imageElement.parentNode.appendChild(overlay);
- this.state.cropOverlay = overlay;
- }
-
- /**
- * Gère l'événement mousedown sur une poignée de redimensionnement
- * @private
- * @param {MouseEvent} e - Événement mousedown
- * @param {string} handleType - Type de poignée
- */
- _handleCropHandleMouseDown(e, handleType) {
- this.interaction.cropResizing = true;
- this.interaction.activeHandle = handleType;
-
- // Enregistrer les dimensions et position initiales
- this.interaction.startX = parseInt(this.state.cropOverlay.style.left, 10) || 0;
- this.interaction.startY = parseInt(this.state.cropOverlay.style.top, 10) || 0;
- this.interaction.startWidth = this.state.cropOverlay.offsetWidth;
- this.interaction.startHeight = this.state.cropOverlay.offsetHeight;
- this.interaction.startMouseX = e.clientX;
- this.interaction.startMouseY = e.clientY;
-
- e.preventDefault();
- e.stopPropagation();
- }
-
- /**
- * Gère l'événement mousedown sur l'overlay de recadrage
- * @private
- * @param {MouseEvent} e - Événement mousedown
- */
- _handleCropOverlayMouseDown(e) {
- // Ignorer si on clique sur une poignée
- if (e.target !== this.state.cropOverlay) return;
-
- this.interaction.cropDragging = true;
- this.state.cropOverlay.style.cursor = 'grabbing';
-
- // Enregistrer la position initiale
- this.interaction.startX = parseInt(this.state.cropOverlay.style.left, 10) || 0;
- this.interaction.startY = parseInt(this.state.cropOverlay.style.top, 10) || 0;
- this.interaction.startMouseX = e.clientX;
- this.interaction.startMouseY = e.clientY;
-
- e.preventDefault();
- }
-
- /**
- * Configure les écouteurs d'événements globaux
- * @private
- */
- _setupEventListeners() {
- // Écouteur pour le redimensionnement de la fenêtre
- window.addEventListener('resize', this._updateScaling.bind(this));
-
- // Écouteur pour le chargement de l'image
- if (!this.imageElement.complete) {
- this.imageElement.addEventListener('load', this._updateScaling.bind(this));
- }
-
- // Écouteurs pour les interactions de souris
- document.addEventListener('mouseup', this._handleMouseUp.bind(this));
- document.addEventListener('mousemove', this._handleMouseMove.bind(this));
- }
-
- /**
- * Gère l'événement mouseup global
- * @private
- */
- _handleMouseUp() {
- if (this.interaction.focusDragging) {
- this.interaction.focusDragging = false;
- if (this.state.focusMarker) this.state.focusMarker.style.cursor = 'move';
- }
-
- if (this.interaction.cropDragging) {
- this.interaction.cropDragging = false;
- if (this.state.cropOverlay) this.state.cropOverlay.style.cursor = 'move';
- }
-
- this.interaction.cropResizing = false;
- this.interaction.activeHandle = null;
- document.body.style.cursor = 'default';
- }
-
- /**
- * Gère l'événement mousemove global
- * @private
- * @param {MouseEvent} e - Événement mousemove
- */
- _handleMouseMove(e) {
- // Gestion du déplacement du point focal
- if (this.interaction.focusDragging && this.state.focusMarker) {
- this._handleFocusMarkerDrag(e);
- }
-
- // Gestion du déplacement de la zone de recadrage
- if (this.interaction.cropDragging) {
- this._handleCropOverlayDrag(e);
- }
-
- // Gestion du redimensionnement de la zone de recadrage
- if (this.interaction.cropResizing) {
- this._handleCropOverlayResize(e);
- }
- }
-
- /**
- * Gère le déplacement du marqueur de point focal
- * @private
- * @param {MouseEvent} e - Événement mousemove
- */
- _handleFocusMarkerDrag(e) {
- const imageRect = this.imageElement.getBoundingClientRect();
-
- // Calculer la position cible relative à l'image
- const targetScaledX = e.clientX - imageRect.left - this.interaction.focusDragOffsetX;
- const targetScaledY = e.clientY - imageRect.top - this.interaction.focusDragOffsetY;
-
- // Convertir en coordonnées originales
- const original = this._toOriginalCoords(targetScaledX, targetScaledY);
-
- // Mettre à jour le point focal
- this.setFocusPoint(original.x, original.y);
- }
-
- /**
- * Gère le déplacement de l'overlay de recadrage
- * @private
- * @param {MouseEvent} e - Événement mousemove
- */
- _handleCropOverlayDrag(e) {
- const deltaX = e.clientX - this.interaction.startMouseX;
- const deltaY = e.clientY - this.interaction.startMouseY;
-
- // Calculer la nouvelle position en pixels d'affichage
- let newX = this.interaction.startX + deltaX;
- let newY = this.interaction.startY + deltaY;
-
- // Convertir en coordonnées originales
- const original = this._toOriginalCoords(newX, newY);
-
- // Obtenir les dimensions actuelles en coordonnées originales
- const { width, height } = this.state.cropZone;
-
- // Mettre à jour la zone de recadrage
- this.setCropZone(original.x, original.y, width, height);
- }
-
- /**
- * Gère le redimensionnement de l'overlay de recadrage
- * @private
- * @param {MouseEvent} e - Événement mousemove
- */
- _handleCropOverlayResize(e) {
- if (!this.interaction.activeHandle) return;
-
- const deltaX = e.clientX - this.interaction.startMouseX;
- const deltaY = e.clientY - this.interaction.startMouseY;
-
- let newX = this.interaction.startX;
- let newY = this.interaction.startY;
- let newWidth = this.interaction.startWidth;
- let newHeight = this.interaction.startHeight;
-
- // Ajuster en fonction de la poignée active
- if (this.interaction.activeHandle.includes('t')) { // Top
- newY = this.interaction.startY + deltaY;
- newHeight = this.interaction.startHeight - deltaY;
- }
- if (this.interaction.activeHandle.includes('b')) { // Bottom
- newHeight = this.interaction.startHeight + deltaY;
- }
- if (this.interaction.activeHandle.includes('l')) { // Left
- newX = this.interaction.startX + deltaX;
- newWidth = this.interaction.startWidth - deltaX;
- }
- if (this.interaction.activeHandle.includes('r')) { // Right
- newWidth = this.interaction.startWidth + deltaX;
- }
-
- // Empêcher les dimensions négatives
- if (newWidth < 10) {
- if (this.interaction.activeHandle.includes('l')) {
- newX = this.interaction.startX + this.interaction.startWidth - 10;
- }
- newWidth = 10;
- }
- if (newHeight < 10) {
- if (this.interaction.activeHandle.includes('t')) {
- newY = this.interaction.startY + this.interaction.startHeight - 10;
- }
- newHeight = 10;
- }
-
- // Convertir les coordonnées d'affichage en coordonnées originales
- const originalX = newX / this.state.scaleX;
- const originalY = newY / this.state.scaleY;
- const originalWidth = newWidth / this.state.scaleX;
- const originalHeight = newHeight / this.state.scaleY;
-
- // Mettre à jour la zone de recadrage
- this.setCropZone(originalX, originalY, originalWidth, originalHeight);
- }
-
- /**
- * Met à jour la position du marqueur de point focal
- * @private
- */
- _updateFocusMarkerPosition() {
- if (!this.state.focusMarker) return;
-
- const { x, y } = this.state.focusPoint;
-
- // Limiter les coordonnées aux dimensions de l'image
- const clampedX = Math.max(0, Math.min(x, this.state.originalWidth));
- const clampedY = Math.max(0, Math.min(y, this.state.originalHeight));
-
- // Mettre à jour l'état si les coordonnées ont été limitées
- if (x !== clampedX || y !== clampedY) {
- this.state.focusPoint = { x: clampedX, y: clampedY };
- }
-
- const scaled = this._toScaledCoords(clampedX, clampedY);
-
- // LOGGING: Validate padding offset
- if (this.options.debug) {
- const container = this.imageElement.parentNode;
- const computedStyle = window.getComputedStyle(container);
- const paddingLeft = parseFloat(computedStyle.paddingLeft);
- const paddingTop = parseFloat(computedStyle.paddingTop);
- console.log('[FocusMarker] scaled:', scaled, 'paddingLeft:', paddingLeft, 'paddingTop:', paddingTop, 'containerRect:', container.getBoundingClientRect());
- }
-
- // Ajuster pour centrer le marqueur
- // Adjust for container padding (use unique variable names)
- let focusPaddingLeft = 0, focusPaddingTop = 0;
- {
- const container = this.imageElement.parentNode;
- const computedStyle = window.getComputedStyle(container);
- focusPaddingLeft = parseFloat(computedStyle.paddingLeft);
- focusPaddingTop = parseFloat(computedStyle.paddingTop);
- }
-
- this.state.focusMarker.style.left = (scaled.x + focusPaddingLeft - this.state.focusMarker.offsetWidth / 2) + 'px';
- this.state.focusMarker.style.top = (scaled.y + focusPaddingTop - this.state.focusMarker.offsetHeight / 2) + 'px';
- if (this.options.debug) {
- console.log('[FocusMarker] style.left:', this.state.focusMarker.style.left, 'style.top:', this.state.focusMarker.style.top);
- }
- }
-
- /**
- * Met à jour la position et les dimensions de l'overlay de recadrage
- * @private
- */
- _updateCropOverlayPosition() {
- if (!this.state.cropOverlay) return;
-
- const { x, y, width, height } = this.state.cropZone;
-
- // Limiter aux dimensions de l'image
- const clampedX = Math.max(0, Math.min(x, this.state.originalWidth - width));
- const clampedY = Math.max(0, Math.min(y, this.state.originalHeight - height));
- const clampedWidth = Math.max(10, Math.min(width, this.state.originalWidth - clampedX));
- const clampedHeight = Math.max(10, Math.min(height, this.state.originalHeight - clampedY));
-
- // Mettre à jour l'état si les valeurs ont été limitées
- if (x !== clampedX || y !== clampedY || width !== clampedWidth || height !== clampedHeight) {
- this.state.cropZone = { x: clampedX, y: clampedY, width: clampedWidth, height: clampedHeight };
- }
-
- // Convertir en coordonnées d'affichage
- const scaled = this._toScaledCoords(clampedX, clampedY);
- const scaledWidth = clampedWidth * this.state.scaleX;
- const scaledHeight = clampedHeight * this.state.scaleY;
-
- // LOGGING: Validate padding offset
- if (this.options.debug) {
- const container = this.imageElement.parentNode;
- const computedStyle = window.getComputedStyle(container);
- const paddingLeft = parseFloat(computedStyle.paddingLeft);
- const paddingTop = parseFloat(computedStyle.paddingTop);
- console.log('[CropOverlay] scaled:', scaled, 'paddingLeft:', paddingLeft, 'paddingTop:', paddingTop, 'containerRect:', container.getBoundingClientRect());
- }
-
- // Mettre à jour l'overlay
- // Adjust for container padding (use unique variable names)
- let cropPaddingLeft = 0, cropPaddingTop = 0;
- {
- const container = this.imageElement.parentNode;
- const computedStyle = window.getComputedStyle(container);
- cropPaddingLeft = parseFloat(computedStyle.paddingLeft);
- cropPaddingTop = parseFloat(computedStyle.paddingTop);
- }
-
- this.state.cropOverlay.style.left = (scaled.x + cropPaddingLeft) + 'px';
- this.state.cropOverlay.style.top = (scaled.y + cropPaddingTop) + 'px';
- this.state.cropOverlay.style.width = scaledWidth + 'px';
- this.state.cropOverlay.style.height = scaledHeight + 'px';
- if (this.options.debug) {
- console.log('[CropOverlay] style.left:', this.state.cropOverlay.style.left, 'style.top:', this.state.cropOverlay.style.top);
- }
- }
-
- /**
- * Active ou désactive le point focal
- * @public
- * @param {boolean} active - État d'activation
- * @returns {ImageTool} Instance pour chaînage
- */
- toggleFocusPoint(active) {
- if (!this.options.focusPoint.enabled) return this;
-
- if (active === undefined) {
- active = !this.state.focusActive;
- }
-
- if (active && !this.state.focusActive) {
- // Activer le point focal
- if (!this.state.focusMarker) {
- this._createFocusMarker();
- }
-
- // Positionner au centre de l'image par défaut si pas déjà défini
- if (this.state.focusPoint.x === 0 && this.state.focusPoint.y === 0) {
- this.state.focusPoint = {
- x: this.state.originalWidth / 2,
- y: this.state.originalHeight / 2
- };
- }
-
- this._updateFocusMarkerPosition();
- this.state.focusMarker.style.display = 'block';
- this.state.focusActive = true;
- } else if (!active && this.state.focusActive) {
- // Désactiver le point focal
- if (this.state.focusMarker) {
- this.state.focusMarker.style.display = 'none';
- }
- this.state.focusActive = false;
- }
-
- // Notifier le changement
- this._notifyChange();
-
- return this;
- }
-
- /**
- * Active ou désactive la zone de recadrage
- * @public
- * @param {boolean} active - État d'activation
- * @returns {ImageTool} Instance pour chaînage
- */
- toggleCropZone(active) {
- if (!this.options.cropZone.enabled) return this;
-
- if (active === undefined) {
- active = !this.state.cropActive;
- }
-
- if (active && !this.state.cropActive) {
- // Activer la zone de recadrage
- if (!this.state.cropOverlay) {
- this._createCropOverlay();
- }
-
- // Définir une zone par défaut si pas déjà définie
- if (this.state.cropZone.width === 0 || this.state.cropZone.height === 0) {
- const defaultWidth = this.state.originalWidth / 2;
- const defaultHeight = this.state.originalHeight / 2;
- const defaultX = (this.state.originalWidth - defaultWidth) / 2;
- const defaultY = (this.state.originalHeight - defaultHeight) / 2;
-
- this.state.cropZone = {
- x: defaultX,
- y: defaultY,
- width: defaultWidth,
- height: defaultHeight
- };
- }
-
- this._updateCropOverlayPosition();
- this.state.cropOverlay.style.display = 'block';
- this.state.cropActive = true;
- } else if (!active && this.state.cropActive) {
- // Désactiver la zone de recadrage
- if (this.state.cropOverlay) {
- this.state.cropOverlay.style.display = 'none';
- }
- this.state.cropActive = false;
- }
-
- // Notifier le changement
- this._notifyChange();
-
- return this;
- }
-
- /**
- * Définit la position du point focal
- * @public
- * @param {number} x - Coordonnée X en pixels originaux
- * @param {number} y - Coordonnée Y en pixels originaux
- * @returns {ImageTool} Instance pour chaînage
- */
- setFocusPoint(x, y) {
- // Limiter les coordonnées aux dimensions de l'image
- const clampedX = Math.max(0, Math.min(x, this.state.originalWidth));
- const clampedY = Math.max(0, Math.min(y, this.state.originalHeight));
-
- this.state.focusPoint = { x: clampedX, y: clampedY };
-
- if (this.state.focusActive && this.state.focusMarker) {
- this._updateFocusMarkerPosition();
- }
-
- // Notifier le changement
- this._notifyChange();
-
- return this;
- }
-
- /**
- * Définit la position et les dimensions de la zone de recadrage
- * @public
- * @param {number} x - Coordonnée X en pixels originaux
- * @param {number} y - Coordonnée Y en pixels originaux
- * @param {number} width - Largeur en pixels originaux
- * @param {number} height - Hauteur en pixels originaux
- * @returns {ImageTool} Instance pour chaînage
- */
- setCropZone(x, y, width, height) {
- // Limiter aux dimensions de l'image
- const clampedX = Math.max(0, Math.min(x, this.state.originalWidth - width));
- const clampedY = Math.max(0, Math.min(y, this.state.originalHeight - height));
- const clampedWidth = Math.max(10, Math.min(width, this.state.originalWidth - clampedX));
- const clampedHeight = Math.max(10, Math.min(height, this.state.originalHeight - clampedY));
-
- this.state.cropZone = {
- x: clampedX,
- y: clampedY,
- width: clampedWidth,
- height: clampedHeight
- };
-
- if (this.state.cropActive && this.state.cropOverlay) {
- this._updateCropOverlayPosition();
- }
-
- // Notifier le changement
- this._notifyChange();
-
- return this;
- }
-
- /**
- * Obtient la position actuelle du point focal
- * @public
- * @returns {Object} Coordonnées du point focal {x, y}
- */
- getFocusPoint() {
- return { ...this.state.focusPoint };
- }
-
- /**
- * Obtient la position et les dimensions actuelles de la zone de recadrage
- * @public
- * @returns {Object} Zone de recadrage {x, y, width, height}
- */
- getCropZone() {
- return { ...this.state.cropZone };
- }
-
- /**
- * Obtient les dimensions originales de l'image
- * @public
- * @returns {Object} Dimensions {width, height}
- */
- getImageDimensions() {
- return {
- width: this.state.originalWidth,
- height: this.state.originalHeight
- };
- }
-
- /**
- * Notifie les changements via le callback
- * @private
- */
- _notifyChange() {
- if (typeof this.options.onChange === 'function') {
- this.options.onChange({
- focusPoint: this.getFocusPoint(),
- cropZone: this.getCropZone(),
- focusActive: this.state.focusActive,
- cropActive: this.state.cropActive
- });
- }
- }
-
- /**
- * Détruit l'instance et nettoie les ressources
- * @public
- */
- destroy() {
- // Supprimer les éléments DOM
- if (this.state.focusMarker && this.state.focusMarker.parentNode) {
- this.state.focusMarker.parentNode.removeChild(this.state.focusMarker);
- }
-
- if (this.state.cropOverlay && this.state.cropOverlay.parentNode) {
- this.state.cropOverlay.parentNode.removeChild(this.state.cropOverlay);
- }
-
- // Supprimer les écouteurs d'événements
- window.removeEventListener('resize', this._updateScaling.bind(this));
- document.removeEventListener('mouseup', this._handleMouseUp.bind(this));
- document.removeEventListener('mousemove', this._handleMouseMove.bind(this));
-
- // Réinitialiser l'état
- this.state = null;
- this.interaction = null;
- this.options = null;
- this.imageElement = null;
- }
- }
-
- // Exporter la classe
- export default VisualImageTool;
-
\ No newline at end of file
+ /**
+ * Crée une instance de l'outil d'image
+ * @param {Object} options - Options de configuration
+ * @param {HTMLElement|string} options.imageElement - Élément image ou sélecteur CSS
+ * @param {Object} [options.focusPoint] - Configuration du point focal
+ * @param {boolean} [options.focusPoint.enabled=true] - Activer la fonctionnalité de point focal
+ * @param {Object} [options.focusPoint.style] - Styles personnalisés pour le marqueur de point focal
+ * @param {Object} [options.cropZone] - Configuration de la zone de recadrage
+ * @param {boolean} [options.cropZone.enabled=true] - Activer la fonctionnalité de zone de recadrage
+ * @param {Object} [options.cropZone.style] - Styles personnalisés pour l'overlay de recadrage
+ * @param {Function} [options.onChange] - Callback appelé lors des changements
+ */
+ constructor(options) {
+ // Valider les options
+ if (!options || !options.imageElement) {
+ throw new Error("L'élément image est requis");
+ }
+
+ // Initialiser les propriétés
+ this.imageElement =
+ typeof options.imageElement === "string"
+ ? document.querySelector(options.imageElement)
+ : options.imageElement;
+
+ if (!this.imageElement || this.imageElement.tagName !== "IMG") {
+ throw new Error("Élément image invalide");
+ }
+
+ // Options par défaut
+ this.options = {
+ focusPoint: {
+ enabled: true,
+ style: {
+ width: "30px",
+ height: "30px",
+ border: "3px solid white",
+ boxShadow: "0 0 0 2px black, 0 0 5px rgba(0,0,0,0.5)",
+ backgroundColor: "rgba(255, 0, 0, 0.5)",
+ },
+ ...options.focusPoint,
+ },
+ cropZone: {
+ enabled: true,
+ style: {
+ border: "1px dashed #fff",
+ backgroundColor: "rgba(0, 0, 0, 0.4)",
+ },
+ handleStyle: {
+ width: "14px",
+ height: "14px",
+ backgroundColor: "white",
+ border: "2px solid black",
+ boxShadow: "0 0 3px rgba(0,0,0,0.5)",
+ },
+ ...options.cropZone,
+ },
+ onChange: options.onChange || (() => {}),
+ debug: options.debug || false,
+ };
+
+ // État interne
+ this.state = {
+ focusMarker: null,
+ cropOverlay: null,
+ focusActive: false,
+ cropActive: false,
+ focusPoint: { x: 0, y: 0 },
+ cropZone: { x: 0, y: 0, width: 0, height: 0 },
+ originalWidth: 1,
+ originalHeight: 1,
+ displayWidth: 1,
+ displayHeight: 1,
+ scaleX: 1,
+ scaleY: 1,
+ };
+
+ // Variables pour le suivi des interactions
+ this.interaction = {
+ focusDragging: false,
+ focusDragOffsetX: 0,
+ focusDragOffsetY: 0,
+ cropDragging: false,
+ cropResizing: false,
+ activeHandle: null,
+ startX: 0,
+ startY: 0,
+ startWidth: 0,
+ startHeight: 0,
+ startMouseX: 0,
+ startMouseY: 0,
+ };
+
+ // Initialiser l'outil
+ this._init();
+ }
+
+ /**
+ * Initialise l'outil d'image
+ * @private
+ */
+ _init() {
+ // Préparer le conteneur parent
+ this._prepareContainer();
+
+ // Initialiser les dimensions
+ this._updateScaling();
+
+ // Créer les éléments d'interface si activés
+ if (this.options.focusPoint.enabled) {
+ this._createFocusMarker();
+ }
+
+ if (this.options.cropZone.enabled) {
+ this._createCropOverlay();
+ }
+
+ // Ajouter les écouteurs d'événements
+ this._setupEventListeners();
+ }
+
+ /**
+ * Prépare le conteneur parent de l'image
+ * @private
+ */
+ _prepareContainer() {
+ // S'assurer que le parent est positionné
+ if (this.imageElement.parentNode) {
+ this.imageElement.parentNode.style.position = "relative";
+ }
+ }
+
+ /**
+ * Met à jour les facteurs d'échelle
+ * @private
+ */
+ _updateScaling() {
+ this.state.originalWidth = this.imageElement.naturalWidth || 1;
+ this.state.originalHeight = this.imageElement.naturalHeight || 1;
+ this.state.displayWidth = this.imageElement.offsetWidth;
+ this.state.displayHeight = this.imageElement.offsetHeight;
+ this.state.scaleX = this.state.displayWidth / this.state.originalWidth;
+ this.state.scaleY = this.state.displayHeight / this.state.originalHeight;
+
+ // Repositionner les éléments si actifs
+ if (this.state.focusActive) {
+ this._updateFocusMarkerPosition();
+ }
+
+ if (this.state.cropActive) {
+ this._updateCropOverlayPosition();
+ }
+ }
+
+ /**
+ * Convertit des coordonnées d'affichage en coordonnées originales
+ * @private
+ * @param {number} scaledX - Coordonnée X à l'échelle d'affichage
+ * @param {number} scaledY - Coordonnée Y à l'échelle d'affichage
+ * @returns {Object} Coordonnées originales
+ */
+ _toOriginalCoords(scaledX, scaledY) {
+ return {
+ x: scaledX / this.state.scaleX,
+ y: scaledY / this.state.scaleY,
+ };
+ }
+
+ /**
+ * Convertit des coordonnées originales en coordonnées d'affichage
+ * @private
+ * @param {number} originalX - Coordonnée X originale
+ * @param {number} originalY - Coordonnée Y originale
+ * @returns {Object} Coordonnées à l'échelle d'affichage
+ */
+ _toScaledCoords(originalX, originalY) {
+ return {
+ x: originalX * this.state.scaleX,
+ y: originalY * this.state.scaleY,
+ };
+ }
+
+ /**
+ * Crée le marqueur de point focal
+ * @private
+ */
+ _createFocusMarker() {
+ if (this.state.focusMarker) return;
+
+ const marker = document.createElement("div");
+ marker.style.position = "absolute";
+ marker.style.width = this.options.focusPoint.style.width;
+ marker.style.height = this.options.focusPoint.style.height;
+ marker.style.border = this.options.focusPoint.style.border;
+ marker.style.boxShadow = this.options.focusPoint.style.boxShadow;
+ marker.style.backgroundColor =
+ this.options.focusPoint.style.backgroundColor;
+ marker.style.cursor = "move";
+ marker.style.display = "none";
+ marker.style.zIndex = "999";
+ marker.style.clipPath =
+ "polygon(40% 0%, 60% 0%, 60% 40%, 100% 40%, 100% 60%, 60% 60%, 60% 100%, 40% 100%, 40% 60%, 0% 60%, 0% 40%, 40% 40%)";
+
+ this.imageElement.parentNode.appendChild(marker);
+ this.state.focusMarker = marker;
+
+ // Ajouter les écouteurs d'événements au marqueur
+ marker.addEventListener(
+ "mousedown",
+ this._handleFocusMarkerMouseDown.bind(this),
+ );
+ }
+
+ /**
+ * Gère l'événement mousedown sur le marqueur de point focal
+ * @private
+ * @param {MouseEvent} e - Événement mousedown
+ */
+ _handleFocusMarkerMouseDown(e) {
+ this.interaction.focusDragging = true;
+ this.state.focusMarker.style.cursor = "grabbing";
+
+ // Calculer le décalage par rapport au centre du marqueur
+ const rect = this.state.focusMarker.getBoundingClientRect();
+ this.interaction.focusDragOffsetX =
+ e.clientX - (rect.left + rect.width / 2);
+ this.interaction.focusDragOffsetY =
+ e.clientY - (rect.top + rect.height / 2);
+
+ e.preventDefault();
+ }
+
+ /**
+ * Crée l'overlay de zone de recadrage
+ * @private
+ */
+ _createCropOverlay() {
+ if (this.state.cropOverlay) return;
+
+ const overlay = document.createElement("div");
+ overlay.style.position = "absolute";
+ overlay.style.border = this.options.cropZone.style.border;
+ overlay.style.backgroundColor = this.options.cropZone.style.backgroundColor;
+ overlay.style.boxSizing = "border-box";
+ overlay.style.cursor = "move";
+ overlay.style.display = "none";
+ overlay.style.zIndex = "998";
+
+ // Ajouter les poignées de redimensionnement
+ const handles = ["tl", "tm", "tr", "ml", "mr", "bl", "bm", "br"];
+ for (const handleType of handles) {
+ const handle = document.createElement("div");
+ handle.style.position = "absolute";
+ handle.style.width = this.options.cropZone.handleStyle.width;
+ handle.style.height = this.options.cropZone.handleStyle.height;
+ handle.style.backgroundColor =
+ this.options.cropZone.handleStyle.backgroundColor;
+ handle.style.border = this.options.cropZone.handleStyle.border;
+ handle.style.boxShadow = this.options.cropZone.handleStyle.boxShadow;
+ handle.style.boxSizing = "border-box";
+ handle.dataset.handle = handleType;
+
+ // Positionner la poignée
+ switch (handleType) {
+ case "tl": // Top-left
+ handle.style.top = "-7px";
+ handle.style.left = "-7px";
+ handle.style.cursor = "nwse-resize";
+ break;
+ case "tm": // Top-middle
+ handle.style.top = "-7px";
+ handle.style.left = "50%";
+ handle.style.marginLeft = "-7px";
+ handle.style.cursor = "ns-resize";
+ break;
+ case "tr": // Top-right
+ handle.style.top = "-7px";
+ handle.style.right = "-7px";
+ handle.style.cursor = "nesw-resize";
+ break;
+ case "ml": // Middle-left
+ handle.style.top = "50%";
+ handle.style.left = "-7px";
+ handle.style.marginTop = "-7px";
+ handle.style.cursor = "ew-resize";
+ break;
+ case "mr": // Middle-right
+ handle.style.top = "50%";
+ handle.style.right = "-7px";
+ handle.style.marginTop = "-7px";
+ handle.style.cursor = "ew-resize";
+ break;
+ case "bl": // Bottom-left
+ handle.style.bottom = "-7px";
+ handle.style.left = "-7px";
+ handle.style.cursor = "nesw-resize";
+ break;
+ case "bm": // Bottom-middle
+ handle.style.bottom = "-7px";
+ handle.style.left = "50%";
+ handle.style.marginLeft = "-7px";
+ handle.style.cursor = "ns-resize";
+ break;
+ case "br": // Bottom-right
+ handle.style.bottom = "-7px";
+ handle.style.right = "-7px";
+ handle.style.cursor = "nwse-resize";
+ break;
+ }
+
+ // Ajouter l'écouteur d'événement
+ handle.addEventListener("mousedown", (e) =>
+ this._handleCropHandleMouseDown(e, handleType),
+ );
+
+ overlay.appendChild(handle);
+ }
+
+ // Ajouter l'écouteur pour le déplacement de l'overlay
+ overlay.addEventListener(
+ "mousedown",
+ this._handleCropOverlayMouseDown.bind(this),
+ );
+
+ this.imageElement.parentNode.appendChild(overlay);
+ this.state.cropOverlay = overlay;
+ }
+
+ /**
+ * Gère l'événement mousedown sur une poignée de redimensionnement
+ * @private
+ * @param {MouseEvent} e - Événement mousedown
+ * @param {string} handleType - Type de poignée
+ */
+ _handleCropHandleMouseDown(e, handleType) {
+ this.interaction.cropResizing = true;
+ this.interaction.activeHandle = handleType;
+
+ // Enregistrer les dimensions et position initiales
+ this.interaction.startX =
+ Number.parseInt(this.state.cropOverlay.style.left, 10) || 0;
+ this.interaction.startY =
+ Number.parseInt(this.state.cropOverlay.style.top, 10) || 0;
+ this.interaction.startWidth = this.state.cropOverlay.offsetWidth;
+ this.interaction.startHeight = this.state.cropOverlay.offsetHeight;
+ this.interaction.startMouseX = e.clientX;
+ this.interaction.startMouseY = e.clientY;
+
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ /**
+ * Gère l'événement mousedown sur l'overlay de recadrage
+ * @private
+ * @param {MouseEvent} e - Événement mousedown
+ */
+ _handleCropOverlayMouseDown(e) {
+ // Ignorer si on clique sur une poignée
+ if (e.target !== this.state.cropOverlay) return;
+
+ this.interaction.cropDragging = true;
+ this.state.cropOverlay.style.cursor = "grabbing";
+
+ // Enregistrer la position initiale
+ this.interaction.startX =
+ Number.parseInt(this.state.cropOverlay.style.left, 10) || 0;
+ this.interaction.startY =
+ Number.parseInt(this.state.cropOverlay.style.top, 10) || 0;
+ this.interaction.startMouseX = e.clientX;
+ this.interaction.startMouseY = e.clientY;
+
+ e.preventDefault();
+ }
+
+ /**
+ * Configure les écouteurs d'événements globaux
+ * @private
+ */
+ _setupEventListeners() {
+ // Écouteur pour le redimensionnement de la fenêtre
+ window.addEventListener("resize", this._updateScaling.bind(this));
+
+ // Écouteur pour le chargement de l'image
+ if (!this.imageElement.complete) {
+ this.imageElement.addEventListener(
+ "load",
+ this._updateScaling.bind(this),
+ );
+ }
+
+ // Écouteurs pour les interactions de souris
+ document.addEventListener("mouseup", this._handleMouseUp.bind(this));
+ document.addEventListener("mousemove", this._handleMouseMove.bind(this));
+ }
+
+ /**
+ * Gère l'événement mouseup global
+ * @private
+ */
+ _handleMouseUp() {
+ if (this.interaction.focusDragging) {
+ this.interaction.focusDragging = false;
+ if (this.state.focusMarker) this.state.focusMarker.style.cursor = "move";
+ }
+
+ if (this.interaction.cropDragging) {
+ this.interaction.cropDragging = false;
+ if (this.state.cropOverlay) this.state.cropOverlay.style.cursor = "move";
+ }
+
+ this.interaction.cropResizing = false;
+ this.interaction.activeHandle = null;
+ document.body.style.cursor = "default";
+ }
+
+ /**
+ * Gère l'événement mousemove global
+ * @private
+ * @param {MouseEvent} e - Événement mousemove
+ */
+ _handleMouseMove(e) {
+ // Gestion du déplacement du point focal
+ if (this.interaction.focusDragging && this.state.focusMarker) {
+ this._handleFocusMarkerDrag(e);
+ }
+
+ // Gestion du déplacement de la zone de recadrage
+ if (this.interaction.cropDragging) {
+ this._handleCropOverlayDrag(e);
+ }
+
+ // Gestion du redimensionnement de la zone de recadrage
+ if (this.interaction.cropResizing) {
+ this._handleCropOverlayResize(e);
+ }
+ }
+
+ /**
+ * Gère le déplacement du marqueur de point focal
+ * @private
+ * @param {MouseEvent} e - Événement mousemove
+ */
+ _handleFocusMarkerDrag(e) {
+ const imageRect = this.imageElement.getBoundingClientRect();
+
+ // Calculer la position cible relative à l'image
+ const targetScaledX =
+ e.clientX - imageRect.left - this.interaction.focusDragOffsetX;
+ const targetScaledY =
+ e.clientY - imageRect.top - this.interaction.focusDragOffsetY;
+
+ // Convertir en coordonnées originales
+ const original = this._toOriginalCoords(targetScaledX, targetScaledY);
+
+ // Mettre à jour le point focal
+ this.setFocusPoint(original.x, original.y);
+ }
+
+ /**
+ * Gère le déplacement de l'overlay de recadrage
+ * @private
+ * @param {MouseEvent} e - Événement mousemove
+ */
+ _handleCropOverlayDrag(e) {
+ const deltaX = e.clientX - this.interaction.startMouseX;
+ const deltaY = e.clientY - this.interaction.startMouseY;
+
+ // Calculer la nouvelle position en pixels d'affichage
+ const newX = this.interaction.startX + deltaX;
+ const newY = this.interaction.startY + deltaY;
+
+ // Convertir en coordonnées originales
+ const original = this._toOriginalCoords(newX, newY);
+
+ // Obtenir les dimensions actuelles en coordonnées originales
+ const { width, height } = this.state.cropZone;
+
+ // Mettre à jour la zone de recadrage
+ this.setCropZone(original.x, original.y, width, height);
+ }
+
+ /**
+ * Gère le redimensionnement de l'overlay de recadrage
+ * @private
+ * @param {MouseEvent} e - Événement mousemove
+ */
+ _handleCropOverlayResize(e) {
+ if (!this.interaction.activeHandle) return;
+
+ const deltaX = e.clientX - this.interaction.startMouseX;
+ const deltaY = e.clientY - this.interaction.startMouseY;
+
+ let newX = this.interaction.startX;
+ let newY = this.interaction.startY;
+ let newWidth = this.interaction.startWidth;
+ let newHeight = this.interaction.startHeight;
+
+ // Ajuster en fonction de la poignée active
+ if (this.interaction.activeHandle.includes("t")) {
+ // Top
+ newY = this.interaction.startY + deltaY;
+ newHeight = this.interaction.startHeight - deltaY;
+ }
+ if (this.interaction.activeHandle.includes("b")) {
+ // Bottom
+ newHeight = this.interaction.startHeight + deltaY;
+ }
+ if (this.interaction.activeHandle.includes("l")) {
+ // Left
+ newX = this.interaction.startX + deltaX;
+ newWidth = this.interaction.startWidth - deltaX;
+ }
+ if (this.interaction.activeHandle.includes("r")) {
+ // Right
+ newWidth = this.interaction.startWidth + deltaX;
+ }
+
+ // Empêcher les dimensions négatives
+ if (newWidth < 10) {
+ if (this.interaction.activeHandle.includes("l")) {
+ newX = this.interaction.startX + this.interaction.startWidth - 10;
+ }
+ newWidth = 10;
+ }
+ if (newHeight < 10) {
+ if (this.interaction.activeHandle.includes("t")) {
+ newY = this.interaction.startY + this.interaction.startHeight - 10;
+ }
+ newHeight = 10;
+ }
+
+ // Convertir les coordonnées d'affichage en coordonnées originales
+ const originalX = newX / this.state.scaleX;
+ const originalY = newY / this.state.scaleY;
+ const originalWidth = newWidth / this.state.scaleX;
+ const originalHeight = newHeight / this.state.scaleY;
+
+ // Mettre à jour la zone de recadrage
+ this.setCropZone(originalX, originalY, originalWidth, originalHeight);
+ }
+
+ /**
+ * Met à jour la position du marqueur de point focal
+ * @private
+ */
+ _updateFocusMarkerPosition() {
+ if (!this.state.focusMarker) return;
+
+ const { x, y } = this.state.focusPoint;
+
+ // Limiter les coordonnées aux dimensions de l'image
+ const clampedX = Math.max(0, Math.min(x, this.state.originalWidth));
+ const clampedY = Math.max(0, Math.min(y, this.state.originalHeight));
+
+ // Mettre à jour l'état si les coordonnées ont été limitées
+ if (x !== clampedX || y !== clampedY) {
+ this.state.focusPoint = { x: clampedX, y: clampedY };
+ }
+
+ const scaled = this._toScaledCoords(clampedX, clampedY);
+
+ // LOGGING: Validate padding offset
+ if (this.options.debug) {
+ const container = this.imageElement.parentNode;
+ const computedStyle = window.getComputedStyle(container);
+ const paddingLeft = Number.parseFloat(computedStyle.paddingLeft);
+ const paddingTop = Number.parseFloat(computedStyle.paddingTop);
+ console.log(
+ "[FocusMarker] scaled:",
+ scaled,
+ "paddingLeft:",
+ paddingLeft,
+ "paddingTop:",
+ paddingTop,
+ "containerRect:",
+ container.getBoundingClientRect(),
+ );
+ }
+
+ // Ajuster pour centrer le marqueur
+ // Adjust for container padding (use unique variable names)
+ let focusPaddingLeft = 0;
+ let focusPaddingTop = 0;
+ {
+ const container = this.imageElement.parentNode;
+ const computedStyle = window.getComputedStyle(container);
+ focusPaddingLeft = Number.parseFloat(computedStyle.paddingLeft);
+ focusPaddingTop = Number.parseFloat(computedStyle.paddingTop);
+ }
+
+ this.state.focusMarker.style.left = `${
+ scaled.x + focusPaddingLeft - this.state.focusMarker.offsetWidth / 2
+ }px`;
+ this.state.focusMarker.style.top = `${
+ scaled.y + focusPaddingTop - this.state.focusMarker.offsetHeight / 2
+ }px`;
+ if (this.options.debug) {
+ console.log(
+ "[FocusMarker] style.left:",
+ this.state.focusMarker.style.left,
+ "style.top:",
+ this.state.focusMarker.style.top,
+ );
+ }
+ }
+
+ /**
+ * Met à jour la position et les dimensions de l'overlay de recadrage
+ * @private
+ */
+ _updateCropOverlayPosition() {
+ if (!this.state.cropOverlay) return;
+
+ const { x, y, width, height } = this.state.cropZone;
+
+ // Limiter aux dimensions de l'image
+ const clampedX = Math.max(0, Math.min(x, this.state.originalWidth - width));
+ const clampedY = Math.max(
+ 0,
+ Math.min(y, this.state.originalHeight - height),
+ );
+ const clampedWidth = Math.max(
+ 10,
+ Math.min(width, this.state.originalWidth - clampedX),
+ );
+ const clampedHeight = Math.max(
+ 10,
+ Math.min(height, this.state.originalHeight - clampedY),
+ );
+
+ // Mettre à jour l'état si les valeurs ont été limitées
+ if (
+ x !== clampedX ||
+ y !== clampedY ||
+ width !== clampedWidth ||
+ height !== clampedHeight
+ ) {
+ this.state.cropZone = {
+ x: clampedX,
+ y: clampedY,
+ width: clampedWidth,
+ height: clampedHeight,
+ };
+ }
+
+ // Convertir en coordonnées d'affichage
+ const scaled = this._toScaledCoords(clampedX, clampedY);
+ const scaledWidth = clampedWidth * this.state.scaleX;
+ const scaledHeight = clampedHeight * this.state.scaleY;
+
+ // LOGGING: Validate padding offset
+ if (this.options.debug) {
+ const container = this.imageElement.parentNode;
+ const computedStyle = window.getComputedStyle(container);
+ const paddingLeft = Number.parseFloat(computedStyle.paddingLeft);
+ const paddingTop = Number.parseFloat(computedStyle.paddingTop);
+ console.log(
+ "[CropOverlay] scaled:",
+ scaled,
+ "paddingLeft:",
+ paddingLeft,
+ "paddingTop:",
+ paddingTop,
+ "containerRect:",
+ container.getBoundingClientRect(),
+ );
+ }
+
+ // Mettre à jour l'overlay
+ // Adjust for container padding (use unique variable names)
+ let cropPaddingLeft = 0;
+ let cropPaddingTop = 0;
+ {
+ const container = this.imageElement.parentNode;
+ const computedStyle = window.getComputedStyle(container);
+ cropPaddingLeft = Number.parseFloat(computedStyle.paddingLeft);
+ cropPaddingTop = Number.parseFloat(computedStyle.paddingTop);
+ }
+
+ this.state.cropOverlay.style.left = `${scaled.x + cropPaddingLeft}px`;
+ this.state.cropOverlay.style.top = `${scaled.y + cropPaddingTop}px`;
+ this.state.cropOverlay.style.width = `${scaledWidth}px`;
+ this.state.cropOverlay.style.height = `${scaledHeight}px`;
+ if (this.options.debug) {
+ console.log(
+ "[CropOverlay] style.left:",
+ this.state.cropOverlay.style.left,
+ "style.top:",
+ this.state.cropOverlay.style.top,
+ );
+ }
+ }
+
+ /**
+ * Active ou désactive le point focal
+ * @public
+ * @param {boolean} active - État d'activation
+ * @returns {ImageTool} Instance pour chaînage
+ */
+ toggleFocusPoint(active) {
+ if (!this.options.focusPoint.enabled) return this;
+
+ const isActive = active === undefined ? !this.state.focusActive : active;
+
+ if (active && !this.state.focusActive) {
+ // Activer le point focal
+ if (!this.state.focusMarker) {
+ this._createFocusMarker();
+ }
+
+ // Positionner au centre de l'image par défaut si pas déjà défini
+ if (this.state.focusPoint.x === 0 && this.state.focusPoint.y === 0) {
+ this.state.focusPoint = {
+ x: this.state.originalWidth / 2,
+ y: this.state.originalHeight / 2,
+ };
+ }
+
+ this._updateFocusMarkerPosition();
+ this.state.focusMarker.style.display = "block";
+ this.state.focusActive = true;
+ } else if (!active && this.state.focusActive) {
+ // Désactiver le point focal
+ if (this.state.focusMarker) {
+ this.state.focusMarker.style.display = "none";
+ }
+ this.state.focusActive = false;
+ }
+
+ // Notifier le changement
+ this._notifyChange();
+
+ return this;
+ }
+
+ /**
+ * Active ou désactive la zone de recadrage
+ * @public
+ * @param {boolean} active - État d'activation
+ * @returns {ImageTool} Instance pour chaînage
+ */
+ toggleCropZone(active) {
+ if (!this.options.cropZone.enabled) return this;
+
+ const isActive = active === undefined ? !this.state.cropActive : active;
+
+ if (active && !this.state.cropActive) {
+ // Activer la zone de recadrage
+ if (!this.state.cropOverlay) {
+ this._createCropOverlay();
+ }
+
+ // Définir une zone par défaut si pas déjà définie
+ if (this.state.cropZone.width === 0 || this.state.cropZone.height === 0) {
+ const defaultWidth = this.state.originalWidth / 2;
+ const defaultHeight = this.state.originalHeight / 2;
+ const defaultX = (this.state.originalWidth - defaultWidth) / 2;
+ const defaultY = (this.state.originalHeight - defaultHeight) / 2;
+
+ this.state.cropZone = {
+ x: defaultX,
+ y: defaultY,
+ width: defaultWidth,
+ height: defaultHeight,
+ };
+ }
+
+ this._updateCropOverlayPosition();
+ this.state.cropOverlay.style.display = "block";
+ this.state.cropActive = true;
+ } else if (!active && this.state.cropActive) {
+ // Désactiver la zone de recadrage
+ if (this.state.cropOverlay) {
+ this.state.cropOverlay.style.display = "none";
+ }
+ this.state.cropActive = false;
+ }
+
+ // Notifier le changement
+ this._notifyChange();
+
+ return this;
+ }
+
+ /**
+ * Définit la position du point focal
+ * @public
+ * @param {number} x - Coordonnée X en pixels originaux
+ * @param {number} y - Coordonnée Y en pixels originaux
+ * @returns {ImageTool} Instance pour chaînage
+ */
+ setFocusPoint(x, y) {
+ // Limiter les coordonnées aux dimensions de l'image
+ const clampedX = Math.max(0, Math.min(x, this.state.originalWidth));
+ const clampedY = Math.max(0, Math.min(y, this.state.originalHeight));
+
+ this.state.focusPoint = { x: clampedX, y: clampedY };
+
+ if (this.state.focusActive && this.state.focusMarker) {
+ this._updateFocusMarkerPosition();
+ }
+
+ // Notifier le changement
+ this._notifyChange();
+
+ return this;
+ }
+
+ /**
+ * Définit la position et les dimensions de la zone de recadrage
+ * @public
+ * @param {number} x - Coordonnée X en pixels originaux
+ * @param {number} y - Coordonnée Y en pixels originaux
+ * @param {number} width - Largeur en pixels originaux
+ * @param {number} height - Hauteur en pixels originaux
+ * @returns {ImageTool} Instance pour chaînage
+ */
+ setCropZone(x, y, width, height) {
+ // Limiter aux dimensions de l'image
+ const clampedX = Math.max(0, Math.min(x, this.state.originalWidth - width));
+ const clampedY = Math.max(
+ 0,
+ Math.min(y, this.state.originalHeight - height),
+ );
+ const clampedWidth = Math.max(
+ 10,
+ Math.min(width, this.state.originalWidth - clampedX),
+ );
+ const clampedHeight = Math.max(
+ 10,
+ Math.min(height, this.state.originalHeight - clampedY),
+ );
+
+ this.state.cropZone = {
+ x: clampedX,
+ y: clampedY,
+ width: clampedWidth,
+ height: clampedHeight,
+ };
+
+ if (this.state.cropActive && this.state.cropOverlay) {
+ this._updateCropOverlayPosition();
+ }
+
+ // Notifier le changement
+ this._notifyChange();
+
+ return this;
+ }
+
+ /**
+ * Obtient la position actuelle du point focal
+ * @public
+ * @returns {Object} Coordonnées du point focal {x, y}
+ */
+ getFocusPoint() {
+ return { ...this.state.focusPoint };
+ }
+
+ /**
+ * Obtient la position et les dimensions actuelles de la zone de recadrage
+ * @public
+ * @returns {Object} Zone de recadrage {x, y, width, height}
+ */
+ getCropZone() {
+ return { ...this.state.cropZone };
+ }
+
+ /**
+ * Obtient les dimensions originales de l'image
+ * @public
+ * @returns {Object} Dimensions {width, height}
+ */
+ getImageDimensions() {
+ return {
+ width: this.state.originalWidth,
+ height: this.state.originalHeight,
+ };
+ }
+
+ /**
+ * Notifie les changements via le callback
+ * @private
+ */
+ _notifyChange() {
+ if (typeof this.options.onChange === "function") {
+ this.options.onChange({
+ focusPoint: this.getFocusPoint(),
+ cropZone: this.getCropZone(),
+ focusActive: this.state.focusActive,
+ cropActive: this.state.cropActive,
+ });
+ }
+ }
+
+ /**
+ * Détruit l'instance et nettoie les ressources
+ * @public
+ */
+ destroy() {
+ // Supprimer les éléments DOM
+ if (this.state.focusMarker?.parentNode) {
+ this.state.focusMarker.parentNode.removeChild(this.state.focusMarker);
+ }
+
+ if (this.state.cropOverlay?.parentNode) {
+ this.state.cropOverlay.parentNode.removeChild(this.state.cropOverlay);
+ }
+
+ // Supprimer les écouteurs d'événements
+ window.removeEventListener("resize", this._updateScaling.bind(this));
+ document.removeEventListener("mouseup", this._handleMouseUp.bind(this));
+ document.removeEventListener("mousemove", this._handleMouseMove.bind(this));
+
+ // Réinitialiser l'état
+ this.state = null;
+ this.interaction = null;
+ this.options = null;
+ this.imageElement = null;
+ }
+}
+
+// Exporter la classe
+export default VisualImageTool;