# Создай приложение которое позволяет открыть изображение с устройства и изменить его размеры сделать кроп перевернуть развернуть слева направо и справа на лево

<html lang="ru">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Редактор изображений</title>
  <script src="https://app.aisearch.ru/static/tailwind/tailwind.js"></script>
  <link
    rel="stylesheet"
    href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"
  />
  <style>
    /* Кроп область будет внутри контейнера с overflow hidden */
    #crop-area {
      overflow: hidden;
      position: relative;
      display: inline-block;
      touch-action: none;
      user-select: none;
      max-width: 100%;
      max-height: 500px;
      border: 2px dashed #ccc;
      margin-top: 0.5rem;
    }
    #crop-image {
      display: block;
      max-width: none;
      user-select: none;
      cursor: grab;
      transform-origin: top left;
      position: relative;
      left: 0;
      top: 0;
      touch-action: none;
    }
    /* Кроп окно - рамка и перетаскивание */
    #crop-window {
      position: absolute;
      border: 2px solid #09f;
      box-sizing: border-box;
      cursor: move;
      background: rgba(0, 0, 255, 0.1);
      /* минимальный размер 50х50 */
      min-width: 50px;
      min-height: 50px;
      user-select: none;
      touch-action: none;
    }
    .handle {
      width: 12px;
      height: 12px;
      background: #09f;
      position: absolute;
      border-radius: 50%;
      user-select: none;
      touch-action: none;
    }
    .handle.nw {
      top: -6px;
      left: -6px;
      cursor: nwse-resize;
    }
    .handle.ne {
      top: -6px;
      right: -6px;
      cursor: nesw-resize;
    }
    .handle.sw {
      bottom: -6px;
      left: -6px;
      cursor: nesw-resize;
    }
    .handle.se {
      bottom: -6px;
      right: -6px;
      cursor: nwse-resize;
    }
  </style>
</head>
<body class="bg-gray-50 min-h-screen p-4 flex flex-col items-center font-sans">

  <h1 class="text-2xl font-bold mb-6">Редактор изображений</h1>

  <div class="w-full max-w-4xl flex flex-col md:flex-row gap-6">

    <div class="md:w-1/2 flex flex-col items-center">

      <label
        for="file-input"
        class="cursor-pointer bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded mb-4 inline-flex items-center gap-2"
        ><i class="fas fa-upload"></i> Загрузить изображение</label
      >
      <input type="file" id="file-input" accept="image/*" class="hidden" />

      <div id="crop-area" class="w-full max-w-full h-auto rounded bg-white shadow-md">
        <img
          id="crop-image"
          alt="Изображение для редактирования, загруженное пользователем"
          draggable="false"
          src=""
          style="transform-origin: top left"
        />
        <div id="crop-window" aria-label="Область кропа" tabindex="0" role="region" aria-describedby="crop-controls" hidden>
          <div class="handle nw" data-handle="nw"></div>
          <div class="handle ne" data-handle="ne"></div>
          <div class="handle sw" data-handle="sw"></div>
          <div class="handle se" data-handle="se"></div>
        </div>
      </div>

    </div>

    <div class="md:w-1/2 bg-white p-4 rounded shadow-md flex flex-col gap-5">

      <div>
        <label for="resize-width" class="block font-semibold mb-1">Ширина (px)</label>
        <input
          type="number"
          id="resize-width"
          min="1"
          class="border border-gray-300 rounded px-2 py-1 w-full"
          aria-describedby="resize-desc"
          placeholder="Авто"
        />
        <p id="resize-desc" class="text-sm text-gray-500 mt-1">Введите для изменения размера, оставьте пустым для авто</p>
      </div>

      <div>
        <label for="resize-height" class="block font-semibold mb-1">Высота (px)</label>
        <input
          type="number"
          id="resize-height"
          min="1"
          class="border border-gray-300 rounded px-2 py-1 w-full"
          placeholder="Авто"
        />
      </div>

      <button
        id="apply-resize"
        class="bg-green-600 hover:bg-green-700 text-white px-6 py-2 rounded w-full font-semibold"
        disabled
      >
        Изменить размер
      </button>

      <div class="pt-4 border-t border-gray-200">
        <h2 class="text-lg font-semibold mb-2">Операции над изображением</h2>
        <div class="flex flex-wrap gap-3">
          <button
            id="flip-horizontal"
            class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded flex items-center gap-2"
            disabled
            aria-pressed="false"
          >
            <i class="fas fa-arrows-alt-h"></i> Перевернуть слева направо
          </button>
          <button
            id="flip-vertical"
            class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded flex items-center gap-2"
            disabled
            aria-pressed="false"
          >
            <i class="fas fa-arrows-alt-v"></i> Перевернуть сверху вниз
          </button>
          <button
            id="rotate-left"
            class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded flex items-center gap-2"
            disabled
          >
            <i class="fas fa-undo"></i> Повернуть влево (90°)
          </button>
          <button
            id="rotate-right"
            class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded flex items-center gap-2"
            disabled
          >
            <i class="fas fa-redo"></i> Повернуть вправо (90°)
          </button>
          <button
            id="crop-btn"
            class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded flex items-center gap-2"
            aria-pressed="false"
            disabled
          >
            <i class="fas fa-crop"></i> Кропнуть изображение
          </button>
          <button
            id="reset-btn"
            class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded flex items-center gap-2"
            disabled
          >
            <i class="fas fa-sync-alt"></i> Сбросить все изменения
          </button>
        </div>
      </div>

      <div>
        <h2 class="text-lg font-semibold mb-2">Результат</h2>
        <canvas id="canvas" class="border border-gray-300 rounded w-full max-h-[500px] object-contain bg-white" aria-label="Результирующее изображение"></canvas>
      </div>

    </div>
  </div>

  <script>
    (() => {
      const fileInput = document.getElementById("file-input");
      const cropImage = document.getElementById("crop-image");
      const cropArea = document.getElementById("crop-area");
      const cropWindow = document.getElementById("crop-window");
      const canvas = document.getElementById("canvas");
      const ctx = canvas.getContext("2d");

      const resizeWidthInput = document.getElementById("resize-width");
      const resizeHeightInput = document.getElementById("resize-height");
      const applyResizeBtn = document.getElementById("apply-resize");

      const flipHBtn = document.getElementById("flip-horizontal");
      const flipVBtn = document.getElementById("flip-vertical");
      const rotateLeftBtn = document.getElementById("rotate-left");
      const rotateRightBtn = document.getElementById("rotate-right");
      const cropBtn = document.getElementById("crop-btn");
      const resetBtn = document.getElementById("reset-btn");

      let image = new Image();
      let originalImage = null;

      // Хранение состояния трансформаций
      let state = {
        scaleX: 1,
        scaleY: 1,
        rotation: 0, // градусы: 0, 90, 180, 270
        width: 0,
        height: 0,
        posX: 0,
        posY: 0,
        cropping: false,
      };

      // Кроп окно параметры (в пределах cropArea)
      let cropRect = { left: 50, top: 50, width: 200, height: 150 };

      let isDraggingCrop = false;
      let cropDragOffset = { x: 0, y: 0 };
      let isResizingCrop = false;
      let currentResizeHandle = null;

      function updateUIOnImageLoad() {
        applyResizeBtn.disabled = false;
        flipHBtn.disabled = false;
        flipVBtn.disabled = false;
        rotateLeftBtn.disabled = false;
        rotateRightBtn.disabled = false;
        cropBtn.disabled = false;
        resetBtn.disabled = false;

        resizeWidthInput.value = image.naturalWidth;
        resizeHeightInput.value = image.naturalHeight;

        cropWindow.hidden = true;
        state.cropping = false;
      }

      function resetState() {
        state = {
          scaleX: 1,
          scaleY: 1,
          rotation: 0,
          width: image.naturalWidth,
          height: image.naturalHeight,
          posX: 0,
          posY: 0,
          cropping: false,
        };
        cropRect = {
          left: 50,
          top: 50,
          width: Math.min(200, state.width),
          height: Math.min(150, state.height),
        };
        updateCropWindow();
        updateImageTransform();
        updateCanvas();
      }

      // Обновление CSS transform по текущему state
      function updateImageTransform() {
        const rotate = state.rotation % 360;
        const scaleX = state.scaleX;
        const scaleY = state.scaleY;

        const t = `translate(${state.posX}px, ${state.posY}px) rotate(${rotate}deg) scale(${scaleX}, ${scaleY})`;
        cropImage.style.transform = t;
      }

      // Обновление размеров cropArea и cropWindow
      function updateCropWindow() {
        cropWindow.style.left = cropRect.left + "px";
        cropWindow.style.top = cropRect.top + "px";
        cropWindow.style.width = cropRect.width + "px";
        cropWindow.style.height = cropRect.height + "px";
      }

      // Показ и скрытие cropWindow
      function setCroppingMode(enabled) {
        state.cropping = enabled;
        cropWindow.hidden = !enabled;
        cropBtn.setAttribute("aria-pressed", enabled);
        if (enabled) {
          // Установить crop размер и позицию в центре изображения
          const imgWidth = cropImage.width;
          const imgHeight = cropImage.height;
          cropRect = {
            left: Math.max(0, (imgWidth - 200) / 2),
            top: Math.max(0, (imgHeight - 150) / 2),
            width: Math.min(200, imgWidth),
            height: Math.min(150, imgHeight),
          };
          updateCropWindow();
        }
      }

      // Отрисовать преобразованное изображение на канвасе (с учетом кропа если выбран)
      function updateCanvas() {
        if (!image.src) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        let sourceX = 0,
          sourceY = 0,
          sourceWidth = image.naturalWidth,
          sourceHeight = image.naturalHeight;

        if (state.cropping) {
          // Перевести cropRect с CSS в координаты исходного изображения
          // cropImage может быть масштабирован и повернут, надо брать ratio
          // т.к. cropArea ограничен размерами cropImage она должна иметь одинаковый размер с отображаемым изображением

          // Отношение натурального изображения к размеру отображаемого (учитывая масштаб и поворот?)
          // В нашем случае cropImage отображается с CSS transform
          // Для упрощения примем, что cropImage.width и cropImage.height уже изменены для current размерного state

          // Для точности:
          // Для поворота 0 или 180 cropRect можно брать как есть с поправкой на масштаб. При 90/270 меняются оси.

          // Вычислим размер и позицию crop-window относительно изображения без transform:

          const imgRect = cropImage.getBoundingClientRect();
          const cropAreaRect = cropArea.getBoundingClientRect();
          // Кроп относительно самого изображения (в пикселях отображения)
          let cropLeft = cropRect.left;
          let cropTop = cropRect.top;
          let cropWidth = cropRect.width;
          let cropHeight = cropRect.height;

          // В зависимости от поворота вычислим исходные координаты кропа
          let sx, sy, sw, sh;
          switch (state.rotation % 360) {
            case 0:
              sx = (cropLeft / cropImage.width) * image.naturalWidth;
              sy = (cropTop / cropImage.height) * image.naturalHeight;
              sw = (cropWidth / cropImage.width) * image.naturalWidth;
              sh = (cropHeight / cropImage.height) * image.naturalHeight;
              break;
            case 90:
            case -270:
              // Поворот 90 - X и Y меняется местами
              sx =
                ((cropImage.height - (cropTop + cropHeight)) /
                  cropImage.height) *
                image.naturalWidth;
              sy = (cropLeft / cropImage.width) * image.naturalHeight;
              sw = (cropHeight / cropImage.height) * image.naturalWidth;
              sh = (cropWidth / cropImage.width) * image.naturalHeight;
              break;
            case 180:
            case -180:
              sx =
                ((cropImage.width - (cropLeft + cropWidth)) /
                  cropImage.width) *
                image.naturalWidth;
              sy =
                ((cropImage.height - (cropTop + cropHeight)) /
                  cropImage.height) *
                image.naturalHeight;
              sw = (cropWidth / cropImage.width) * image.naturalWidth;
              sh = (cropHeight / cropImage.height) * image.naturalHeight;
              break;
            case 270:
            case -90:
              sx = (cropTop / cropImage.height) * image.naturalWidth;
              sy =
                ((cropImage.width - (cropLeft + cropWidth)) /
                  cropImage.width) *
                image.naturalHeight;
              sw = (cropHeight / cropImage.height) * image.naturalWidth;
              sh = (cropWidth / cropImage.width) * image.naturalHeight;
              break;
            default:
              sx = 0;
              sy = 0;
              sw = image.naturalWidth;
              sh = image.naturalHeight;
          }
          sourceX = Math.max(0, Math.floor(sx));
          sourceY = Math.max(0, Math.floor(sy));
          sourceWidth = Math.min(image.naturalWidth - sourceX, Math.floor(sw));
          sourceHeight = Math.min(image.naturalHeight - sourceY, Math.floor(sh));
        }

        // Рассчитаем canvas размеры согласно resize inputs.
        // Если нет размеров - берем исходные размеры нарезанной области
        let targetW =
          parseInt(resizeWidthInput.value) || sourceWidth || image.naturalWidth;
        let targetH =
          parseInt(resizeHeightInput.value) || sourceHeight || image.naturalHeight;

        // Поддержка пропорций. Если ввели только ширину, вычислим высоту
        if (resizeWidthInput.value && !resizeHeightInput.value) {
          targetH = Math.round(
            (targetW / sourceWidth) * sourceHeight
          );
          resizeHeightInput.value = targetH;
        } else if (!resizeWidthInput.value && resizeHeightInput.value) {
          targetW = Math.round(
            (targetH / sourceHeight) * sourceWidth
          );
          resizeWidthInput.value = targetW;
        }

        canvas.width = targetW;
        canvas.height = targetH;

        // Сброс трансформаций canvas перед рисованием
        ctx.setTransform(1, 0, 0, 1, 0, 0);
        ctx.clearRect(0, 0, canvas.width, canvas.height);

        // Переносим вращение и перевороты на canvas:
        ctx.save();

        // Перевороты по осям
        let scaleX = state.scaleX;
        let scaleY = state.scaleY;

        // Центр canvas для поворота
        const cx = canvas.width / 2;
        const cy = canvas.height / 2;

        ctx.translate(cx, cy);
        ctx.rotate((state.rotation * Math.PI) / 180);
        ctx.scale(scaleX, scaleY);
        // После вращения рисуем изображение так, чтобы оно центрировалось
        // Размеры могут поменяться местами при 90 и 270

        let drawWidth = targetW;
        let drawHeight = targetH;

        if (
          state.rotation % 180 !== 0 &&
          state.rotation % 360 !== 0
        ) {
          // swap width/height on rotation 90/270
          [drawWidth, drawHeight] = [drawHeight, drawWidth];
        }

        ctx.drawImage(
          image,
          sourceX,
          sourceY,
          sourceWidth,
          sourceHeight,
          -drawWidth / 2,
          -drawHeight / 2,
          drawWidth,
          drawHeight
        );

        ctx.restore();
      }

      // Обновляем размеры изображения в cropArea без изменения state (только DOM)
      function updateCropImageDimensions(width, height) {
        cropImage.style.width = width + "px";
        cropImage.style.height = height + "px";
      }

      // При загрузке файла
      fileInput.addEventListener("change", (e) => {
        if (!e.target.files || e.target.files.length === 0) return;
        const file = e.target.files[0];
        if (!file.type.startsWith("image/")) {
          alert("Пожалуйста, выберите файл с изображением");
          return;
        }
        const url = URL.createObjectURL(file);
        image = new Image();

        image.onload = () => {
          originalImage = image;
          cropImage.src = url;
          // Обновим размеры cropImage в cropArea
          cropImage.style.width = image.naturalWidth + "px";
          cropImage.style.height = image.naturalHeight + "px";
          state.width = image.naturalWidth;
          state.height = image.naturalHeight;
          state.scaleX = 1;
          state.scaleY = 1;
          state.rotation = 0;
          state.posX = 0;
          state.posY = 0;
          updateImageTransform();
          updateUIOnImageLoad();
          resetState();
          updateCanvas();
        };

        image.onerror = () => alert("Ошибка загрузки изображения");
        image.src = url;
      });

      applyResizeBtn.addEventListener("click", () => {
        if (!image.src) return;
        const w = parseInt(resizeWidthInput.value);
        const h = parseInt(resizeHeightInput.value);
        if (
          (w && w < 1) ||
          (h && h < 1)
        ) {
          alert("Ширина и высота должны быть больше 0");
          return;
        }
        // Реализуем изменение размеров в отображении картинки (cropImage)
        if (w && h) {
          updateCropImageDimensions(w, h);
          state.width = w;
          state.height = h;
          // При изменении размера сбрасываем позицию и трансформации
          state.posX = 0;
          state.posY = 0;
          updateImageTransform();
          updateCanvas();
        }
      });

      flipHBtn.addEventListener("click", () => {
        if (!image.src) return;
        state.scaleX *= -1;
        updateImageTransform();
        updateCanvas();
        flipHBtn.setAttribute("aria-pressed", state.scaleX === -1 ? "true" : "false");
      });
      flipVBtn.addEventListener("click", () => {
        if (!image.src) return;
        state.scaleY *= -1;
        updateImageTransform();
        updateCanvas();
        flipVBtn.setAttribute("aria-pressed", state.scaleY === -1 ? "true" : "false");
      });

      rotateLeftBtn.addEventListener("click", () => {
        if (!image.src) return;
        state.rotation = (state.rotation - 90) % 360;
        updateImageTransform();
        updateCanvas();
      });

      rotateRightBtn.addEventListener("click", () => {
        if (!image.src) return;
        state.rotation = (state.rotation + 90) % 360;
        updateImageTransform();
        updateCanvas();
      });

      cropBtn.addEventListener("click", () => {
        if (!image.src) return;
        setCroppingMode(!state.cropping);
        if (!state.cropping) {
          updateCanvas();
        }
      });

      resetBtn.addEventListener("click", () => {
        if (!image.src) return;
        resizeWidthInput.value = image.naturalWidth;
        resizeHeightInput.value = image.naturalHeight;
        cropImage.style.width = image.naturalWidth + "px";
        cropImage.style.height = image.naturalHeight + "px";
        state.scaleX = 1;
        state.scaleY = 1;
        state.rotation = 0;
        state.posX = 0;
        state.posY = 0;
        setCroppingMode(false);
        updateImageTransform();
        updateCanvas();
      });

      // Перемещение cropWindow и изменение размеров
      cropWindow.addEventListener("mousedown", (e) => {
        if (e.target.classList.contains("handle")) return; // обрабатывается ниже
        e.preventDefault();
        isDraggingCrop = true;
        cropDragOffset.x = e.clientX - cropWindow.offsetLeft;
        cropDragOffset.y = e.clientY - cropWindow.offsetTop;
      });
      window.addEventListener("mouseup", (e) => {
        isDraggingCrop = false;
        isResizingCrop = false;
        currentResizeHandle = null;
      });
      window.addEventListener("mousemove", (e) => {
        if (!state.cropping) return;
        if (isDraggingCrop) {
          e.preventDefault();
          // Перемещаем кроп, не выходя за пределы cropArea
          let newLeft = e.clientX - cropArea.getBoundingClientRect().left - cropDragOffset.x;
          let newTop = e.clientY - cropArea.getBoundingClientRect().top - cropDragOffset.y;
          newLeft = Math.max(0, Math.min(newLeft, cropImage.width - cropRect.width));
          newTop = Math.max(0, Math.min(newTop, cropImage.height - cropRect.height));
          cropRect.left = newLeft;
          cropRect.top = newTop;
          updateCropWindow();
        } else if (isResizingCrop && currentResizeHandle) {
          e.preventDefault();
          const bounds = cropArea.getBoundingClientRect();
          const mouseX = e.clientX - bounds.left;
          const mouseY = e.clientY - bounds.top;

          // Зависит от текущего handle изменяем cropRect
          switch (currentResizeHandle) {
            case "nw":
              {
                let newLeft = Math.min(mouseX, cropRect.left + cropRect.width - 50);
                let newTop = Math.min(mouseY, cropRect.top + cropRect.height - 50);
                const dx = newLeft - cropRect.left;
                const dy = newTop - cropRect.top;
                cropRect.left = newLeft;
                cropRect.top = newTop;
                cropRect.width -= dx;
                cropRect.height -= dy;
              }
              break;
            case "ne":
              {
                let newRight = Math.max(mouseX, cropRect.left + 50);
                let newTop = Math.min(mouseY, cropRect.top + cropRect.height - 50);
                const dWidth = newRight - (cropRect.left);
                const dHeight = cropRect.top - newTop + cropRect.height;
                if (dWidth >= 50) cropRect.width = dWidth;
                if (dHeight >= 50) {
                  cropRect.top = newTop;
                  cropRect.height = dHeight;
                }
              }
              break;
            case "sw":
              {
                let newLeft = Math.min(mouseX, cropRect.left + cropRect.width - 50);
                let newBottom = Math.max(mouseY, cropRect.top + 50);
                const dWidth = cropRect.left - newLeft + cropRect.width;
                const dHeight = newBottom - cropRect.top;
                if (dWidth >= 50) {
                  cropRect.left = newLeft;
                  cropRect.width = dWidth;
                }
                if (dHeight >= 50) cropRect.height = dHeight;
              }
              break;
            case "se":
              {
                let newRight = Math.max(mouseX, cropRect.left + 50);
                let newBottom = Math.max(mouseY, cropRect.top + 50);
                const dWidth = newRight - cropRect.left;
                const dHeight = newBottom - cropRect.top;
                if (dWidth >= 50) cropRect.width = dWidth;
                if (dHeight >= 50) cropRect.height = dHeight;
              }
              break;
          }
          // Ограничения по краям
          cropRect.left = Math.max(0, Math.min(cropRect.left, cropImage.width - cropRect.width));
          cropRect.top = Math.max(0, Math.min(cropRect.top, cropImage.height - cropRect.height));
          cropRect.width = Math.min(cropRect.width, cropImage.width - cropRect.left);
          cropRect.height = Math.min(cropRect.height, cropImage.height - cropRect.top);
          updateCropWindow();
        }
      });

      // Поиск начала ресайза при mousedown на ручках
      cropWindow.querySelectorAll(".handle").forEach((handle) => {
        handle.addEventListener("mousedown", (e) => {
          e.preventDefault();
          isResizingCrop = true;
          currentResizeHandle = handle.dataset.handle;
        });
      });

      // Драг изображения (двигать внутри cropArea)
      let isDraggingImage = false;
      let dragStart = { x: 0, y: 0 };
      cropImage.addEventListener("mousedown", (e) => {
        if (state.cropping) return; // при кропе запрещаем двигать
        e.preventDefault();
        isDraggingImage = true;
        dragStart.x = e.clientX;
        dragStart.y = e.clientY;
      });
      window.addEventListener("mouseup", () => {
        isDraggingImage = false;
      });
      window.addEventListener("mousemove", (e) => {
        if (!isDraggingImage) return;
        e.preventDefault();
        let dx = e.clientX - dragStart.x;
        let dy = e.clientY - dragStart.y;
        dragStart.x = e.clientX;
        dragStart.y = e.clientY;
        // Ограничения движения:
        // Не даем выйти за cropArea с учетом размеров изображения
        let newX = state.posX + dx;
        let newY = state.posY + dy;
        let maxX = 0;
        let maxY = 0;
        let minX = cropArea.clientWidth - cropImage.width;
        let minY = cropArea.clientHeight - cropImage.height;
        newX = Math.min(maxX, Math.max(minX, newX));
        newY = Math.min(maxY, Math.max(minY, newY));
        state.posX = newX;
        state.posY = newY;
        updateImageTransform();
      });

      // Запрет контекстного меню при долгом тапе на изображении
      cropImage.addEventListener("contextmenu", (e) => e.preventDefault());
    })();
  </script>
</body>
</html>
