Это обновлённый AiSearch.Что-то работает не так? Расскажите или вернитесь на прежнюю версиюСообщитьПрежняя версия
AiSearchAiSearch

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

обновлено

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

sozdaj-prilozhenie-kotoroe-pozvolyaet-otkryt-izobrazhenie-4154802.md · 28 KB · text/markdownСделать своёСкачать

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

<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>

Похожее