Перетаскивание объектов холста
생성일: 2025년 1월 18일
생성일: 2025년 1월 18일
// app.js
// Глобальные переменные для хранения "слоёв" (объектов) на холсте
let objectsOnCanvas = []; // { image: HTMLImageElement, x, y, width, height, isBackground }
let activeObject = null;
const canvas = document.getElementById('editorCanvas');
const ctx = canvas.getContext('2d');
let currentAspectRatio = 1; // по умолчанию 1:1
const imageUrlInput = document.getElementById('imageUrl');
const loadUrlBtn = document.getElementById('loadUrlBtn');
const fileInput = document.getElementById('fileInput');
const aspectRatioSelect = document.getElementById('aspectRatioSelect');
const cropBtn = document.getElementById('cropBtn');
const saveBtn = document.getElementById('saveBtn');
// Обработчики событий
loadUrlBtn.addEventListener('click', () => {
const url = imageUrlInput.value.trim();
if (url) {
loadImageFromUrl(url);
}
});
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
loadImageFromDataUrl(e.target.result);
};
reader.readAsDataURL(file);
}
});
aspectRatioSelect.addEventListener('change', (event) => {
currentAspectRatio = eval(event.target.value);
});
cropBtn.addEventListener('click', () => {
cropActiveObject();
});
saveBtn.addEventListener('click', () => {
saveComposition();
});
// Загрузка изображения из URL
function loadImageFromUrl(url) {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
addImageToCanvas(img, false);
};
img.onerror = () => {
alert('Ошибка загрузки изображения по URL');
};
img.src = url;
}
// Загрузка изображения из файла
function loadImageFromDataUrl(dataUrl) {
const img = new Image();
img.onload = () => {
addImageToCanvas(img, false);
};
img.src = dataUrl;
}
// Добавление изображения на canvas
function addImageToCanvas(img, isBackground = false) {
const aspectRatio = img.width / img.height;
const maxWidth = canvas.width / 2;
const maxHeight = canvas.height / 2;
let width = img.width;
let height = img.height;
if (width > maxWidth) {
width = maxWidth;
height = width / aspectRatio;
}
if (height > maxHeight) {
height = maxHeight;
width = height * aspectRatio;
}
const object = {
image: img,
x: (canvas.width - width) / 2,
y: (canvas.height - height) / 2,
width: width,
height: height,
isBackground
};
if (isBackground) {
// Удаляем предыдущий фон
objectsOnCanvas = objectsOnCanvas.filter(obj => !obj.isBackground);
objectsOnCanvas.unshift(object);
} else {
objectsOnCanvas.push(object);
}
drawScene();
}
// Перерисовка canvas
function drawScene() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
objectsOnCanvas.forEach(obj => {
ctx.drawImage(obj.image, obj.x, obj.y, obj.width, obj.height);
});
// Рисуем рамку активного объекта
if (activeObject) {
ctx.strokeStyle = 'red';
ctx.lineWidth = 2;
ctx.strokeRect(activeObject.x, activeObject.y, activeObject.width, activeObject.height);
}
}
// Обработка кликов на canvas для выбора объекта
canvas.addEventListener('mousedown', (e) => {
const rect = canvas.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const clickY = e.clientY - rect.top;
activeObject = null;
for (let i = objectsOnCanvas.length - 1; i >= 0; i--) {
const obj = objectsOnCanvas[i];
if (clickX >= obj.x && clickX <= obj.x + obj.width &&
clickY >= obj.y && clickY <= obj.y + obj.height) {
activeObject = obj;
// Переместить выбранный объект наверх
objectsOnCanvas.splice(i, 1);
objectsOnCanvas.push(obj);
break;
}
}
drawScene();
if (activeObject) {
// Начало перетаскивания
canvas.addEventListener('mousemove', onMouseMove);
canvas.addEventListener('mouseup', onMouseUp);
}
});
let isDragging = false;
let dragStartX = 0;
let dragStartY = 0;
function onMouseMove(e) {
if (!activeObject) return;
isDragging = true;
const rect = canvas.getBoundingClientRect();
const moveX = e.clientX - rect.left;
const moveY = e.clientY - rect.top;
const dx = moveX - dragStartX;
const dy = moveY - dragStartY;
activeObject.x += dx;
activeObject.y += dy;
dragStartX = moveX;
dragStartY = moveY;
drawScene();
}
function onMouseUp(e) {
if (isDragging) {
isDragging = false;
canvas.removeEventListener('mousemove', onMouseMove);
canvas.removeEventListener('mouseup', onMouseUp);
}
}
// Обрезка активного объекта
function cropActiveObject() {
if (!activeObject) {
alert('Выберите объект для обрезки.');
return;
}
const originalWidth = activeObject.width;
const originalHeight = activeObject.height;
const originalRatio = originalWidth / originalHeight;
let newWidth, newHeight;
if (originalRatio > currentAspectRatio) {
newWidth = originalHeight * currentAspectRatio;
newHeight = originalHeight;
activeObject.x += (originalWidth - newWidth) / 2;
} else {
newWidth = originalWidth;
newHeight = originalWidth / currentAspectRatio;
activeObject.y += (originalHeight - newHeight) / 2;
}
activeObject.width = newWidth;
activeObject.height = newHeight;
drawScene();
}
// Сохранение композиции
function saveComposition() {
const dataURL = canvas.toDataURL('image/png');
const link = document.createElement('a');
link.download = 'composition.png';
link.href = dataURL;
link.click();
}
Есть проблема с перетаскиванием картинки на холсте. Когда я зажимаю курсор и начинаю тащить картинку, то она сразу перемещается резко вниз и вправо относительно курсора, но такого не должно быть. Она должна плавно перемещаться вместе с зажатым курсором.
Проблема в том, что в момент «mousedown» у вас не инициализируются переменные dragStartX
и dragStartY
, из-за чего при первом же «mousemove» вычисляется слишком большое смещение и картинка «скачет».
Есть два основных способа решения:
В mousedown запоминаем текущие координаты мыши как dragStartX
и dragStartY
. Затем в mousemove считаем, насколько сдвинулась мышь с прошлого события, и на эту же величину сдвигаем объект.
Инициализировать dragStartX
и dragStartY
внутри mousedown
, сразу после определения activeObject
:
jscanvas.addEventListener('mousedown', (e) => { const rect = canvas.getBoundingClientRect(); const clickX = e.clientX - rect.left; const clickY = e.clientY - rect.top; activeObject = null; for (let i = objectsOnCanvas.length - 1; i >= 0; i--) { const obj = objectsOnCanvas[i]; if ( clickX >= obj.x && clickX <= obj.x + obj.width && clickY >= obj.y && clickY <= obj.y + obj.height ) { activeObject = obj; // Переместить выбранный объект наверх objectsOnCanvas.splice(i, 1); objectsOnCanvas.push(obj); break; } } drawScene(); if (activeObject) { // Запоминаем начальные координаты для расчета dx, dy dragStartX = clickX; dragStartY = clickY; canvas.addEventListener('mousemove', onMouseMove); canvas.addEventListener('mouseup', onMouseUp); } });
В onMouseMove
оставляем логику инкрементального сдвига:
jsfunction onMouseMove(e) { if (!activeObject) return; isDragging = true; const rect = canvas.getBoundingClientRect(); const moveX = e.clientX - rect.left; const moveY = e.clientY - rect.top; const dx = moveX - dragStartX; const dy = moveY - dragStartY; activeObject.x += dx; activeObject.y += dy; // Обновляем dragStartX, dragStartY на новые координаты dragStartX = moveX; dragStartY = moveY; drawScene(); }
Благодаря тому, что мы сразу при нажатии «мыши вниз» (mousedown) запомнили исходные координаты клика, первый dx
и dy
будут нулевыми или минимальными, и объект не будет «скакать».
Вместо инкрементального подхода можно вычислять координаты объекта как абсолютные значения (мышь минус некоторая «погрешность»), а не суммировать смещения. Для этого:
В mousedown запоминаем «смещение внутри объекта» — то есть, насколько внутри картинки мы кликнули:
jslet offsetX = 0; let offsetY = 0; canvas.addEventListener('mousedown', (e) => { const rect = canvas.getBoundingClientRect(); const clickX = e.clientX - rect.left; const clickY = e.clientY - rect.top; activeObject = null; for (let i = objectsOnCanvas.length - 1; i >= 0; i--) { const obj = objectsOnCanvas[i]; if ( clickX >= obj.x && clickX <= obj.x + obj.width && clickY >= obj.y && clickY <= obj.y + obj.height ) { activeObject = obj; // Переместить выбранный объект наверх objectsOnCanvas.splice(i, 1); objectsOnCanvas.push(obj); // Запоминаем смещение относительно левого верхнего угла картинки offsetX = clickX - obj.x; offsetY = clickY - obj.y; break; } } drawScene(); if (activeObject) { canvas.addEventListener('mousemove', onMouseMove); canvas.addEventListener('mouseup', onMouseUp); } });
В mousemove уже позиционируем объект по абсолютным координатам:
jsfunction onMouseMove(e) { if (!activeObject) return; isDragging = true; const rect = canvas.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; // Устанавливаем x, y так, чтобы левый верхний угол объекта // совпадал с (mouseX - offsetX, mouseY - offsetY). activeObject.x = mouseX - offsetX; activeObject.y = mouseY - offsetY; drawScene(); }
Таким образом, объект будет перемещаться точно за курсором с тем же смещением, которое было при первом клике.
Потому что в исходном коде dragStartX
и dragStartY
не устанавливаются в момент нажатия кнопки. В итоге первая разница dx = moveX - dragStartX
получается большой (так как dragStartX
/dragStartY
либо равны 0
, либо не инициализированы как нужно), и объект «улетает».
Исправить это можно любым из описанных способов: либо инкрементальный сдвиг (и обязательно сохранить начальные координаты при mousedown), либо абсолютное позиционирование с вычислением «смещения внутри объекта».
// app.js
// Глобальные переменные для хранения "слоёв" (объектов) на холсте
let objectsOnCanvas = []; // { image: HTMLImageElement, x, y, width, height, isBackground }
let activeObject = null;
const canvas = document.getElementById('editorCanvas');
const ctx = canvas.getContext('2d');
let currentAspectRatio = 1; // по умолчанию 1:1
const imageUrlInput = document.getElementById('imageUrl');
const loadUrlBtn = document.getElementById('loadUrlBtn');
const fileInput = document.getElementById('fileInput');
const aspectRatioSelect = document.getElementById('aspectRatioSelect');
const cropBtn = document.getElementById('cropBtn');
const saveBtn = document.getElementById('saveBtn');
// Обработчики событий
loadUrlBtn.addEventListener('click', () => {
const url = imageUrlInput.value.trim();
if (url) {
loadImageFromUrl(url);
}
});
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
loadImageFromDataUrl(e.target.result);
};
reader.readAsDataURL(file);
}
});
aspectRatioSelect.addEventListener('change', (event) => {
currentAspectRatio = eval(event.target.value);
});
cropBtn.addEventListener('click', () => {
cropActiveObject();
});
saveBtn.addEventListener('click', () => {
saveComposition();
});
// Загрузка изображения из URL
function loadImageFromUrl(url) {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
addImageToCanvas(img, false);
};
img.onerror = () => {
alert('Ошибка загрузки изображения по URL');
};
img.src = url;
}
// Загрузка изображения из файла
function loadImageFromDataUrl(dataUrl) {
const img = new Image();
img.onload = () => {
addImageToCanvas(img, false);
};
img.src = dataUrl;
}
// Добавление изображения на canvas
function addImageToCanvas(img, isBackground = false) {
const aspectRatio = img.width / img.height;
const maxWidth = canvas.width / 2;
const maxHeight = canvas.height / 2;
let width = img.width;
let height = img.height;
if (width > maxWidth) {
width = maxWidth;
height = width / aspectRatio;
}
if (height > maxHeight) {
height = maxHeight;
width = height * aspectRatio;
}
const object = {
image: img,
x: (canvas.width - width) / 2,
y: (canvas.height - height) / 2,
width: width,
height: height,
isBackground
};
if (isBackground) {
// Удаляем предыдущий фон
objectsOnCanvas = objectsOnCanvas.filter(obj => !obj.isBackground);
objectsOnCanvas.unshift(object);
} else {
objectsOnCanvas.push(object);
}
drawScene();
}
// Перерисовка canvas
function drawScene() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
objectsOnCanvas.forEach(obj => {
ctx.drawImage(obj.image, obj.x, obj.y, obj.width, obj.height);
});
// Рисуем рамку активного объекта
if (activeObject) {
ctx.strokeStyle = 'red';
ctx.lineWidth = 2;
ctx.strokeRect(activeObject.x, activeObject.y, activeObject.width, activeObject.height);
}
}
// Обработка кликов на canvas для выбора объекта
canvas.addEventListener('mousedown', (e) => {
const rect = canvas.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const clickY = e.clientY - rect.top;
activeObject = null;
for (let i = objectsOnCanvas.length - 1; i >= 0; i--) {
const obj = objectsOnCanvas[i];
if (
clickX >= obj.x && clickX <= obj.x + obj.width &&
clickY >= obj.y && clickY <= obj.y + obj.height
) {
activeObject = obj;
// Переместить выбранный объект наверх
objectsOnCanvas.splice(i, 1);
objectsOnCanvas.push(obj);
break;
}
}
drawScene();
if (activeObject) {
// Запоминаем начальные координаты для расчета dx, dy
dragStartX = clickX;
dragStartY = clickY;
textcanvas.addEventListener('mousemove', onMouseMove); canvas.addEventListener('mouseup', onMouseUp);
}
});
let isDragging = false;
let dragStartX = 0;
let dragStartY = 0;
function onMouseMove(e) {
if (!activeObject) return;
isDragging = true;
const rect = canvas.getBoundingClientRect();
const moveX = e.clientX - rect.left;
const moveY = e.clientY - rect.top;
const dx = moveX - dragStartX;
const dy = moveY - dragStartY;
activeObject.x += dx;
activeObject.y += dy;
dragStartX = moveX;
dragStartY = moveY;
drawScene();
}
function onMouseUp(e) {
if (isDragging) {
isDragging = false;
canvas.removeEventListener('mousemove', onMouseMove);
canvas.removeEventListener('mouseup', onMouseUp);
}
}
// Обрезка активного объекта
function cropActiveObject() {
if (!activeObject) {
alert('Выберите объект для обрезки.');
return;
}
const originalWidth = activeObject.width;
const originalHeight = activeObject.height;
const originalRatio = originalWidth / originalHeight;
let newWidth, newHeight;
if (originalRatio > currentAspectRatio) {
newWidth = originalHeight * currentAspectRatio;
newHeight = originalHeight;
activeObject.x += (originalWidth - newWidth) / 2;
} else {
newWidth = originalWidth;
newHeight = originalWidth / currentAspectRatio;
activeObject.y += (originalHeight - newHeight) / 2;
}
activeObject.width = newWidth;
activeObject.height = newHeight;
drawScene();
}
// Сохранение композиции
function saveComposition() {
const dataURL = canvas.toDataURL('image/png');
const link = document.createElement('a');
link.download = 'composition.png';
link.href = dataURL;
link.click();
}
Все норм, но теперь есть такая проблема: иногда, при нажатии на картинку она цепляется за курсор и следует за ним даже если пользователь уже не держит ЛКМ
Довольно частый сценарий: если пользователь во время перетаскивания выходит за пределы холста и отпускает кнопку мыши за его пределами, то mouseup
-событие на самом холсте (canvas) не сработает. В результате обработчик onMouseUp
(который удаляет слушатель mousemove
) не вызывается, и объект «залипает» за курсором.
Вместо добавления и снятия слушателей mousemove
и mouseup
именно на холсте, добавляйте их на document
(или window
). Тогда, даже если пользователь выйдет мышью за пределы холста, события все равно будут отлавливаться.
canvas.addEventListener('mousemove', onMouseMove)
используйте document.addEventListener('mousemove', onMouseMove)
.mouseup
: document.addEventListener('mouseup', onMouseUp)
.mouseup
», убирайте слушатели с document
(или window
).Примерно так:
jscanvas.addEventListener('mousedown', (e) => { const rect = canvas.getBoundingClientRect(); const clickX = e.clientX - rect.left; const clickY = e.clientY - rect.top; activeObject = null; for (let i = objectsOnCanvas.length - 1; i >= 0; i--) { const obj = objectsOnCanvas[i]; if ( clickX >= obj.x && clickX <= obj.x + obj.width && clickY >= obj.y && clickY <= obj.y + obj.height ) { activeObject = obj; // Переместить выбранный объект наверх objectsOnCanvas.splice(i, 1); objectsOnCanvas.push(obj); break; } } drawScene(); if (activeObject) { // Запоминаем начальные координаты для расчета dx, dy dragStartX = clickX; dragStartY = clickY; isDragging = true; // Начинаем слушать события мыши на document: document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); } }); function onMouseMove(e) { if (!activeObject || !isDragging) return; const rect = canvas.getBoundingClientRect(); const moveX = e.clientX - rect.left; const moveY = e.clientY - rect.top; const dx = moveX - dragStartX; const dy = moveY - dragStartY; activeObject.x += dx; activeObject.y += dy; dragStartX = moveX; dragStartY = moveY; drawScene(); } function onMouseUp(e) { // Завершаем перетаскивание и убираем слушатели isDragging = false; document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); }
Преимущество: пользователь может начать перетаскивать внутри холста, выйти за его пределы и там отпустить мышь — все равно корректно сработает «mouseup» и объект не будет «залипать».
Еще один путь — отлавливать событие mouseleave
(или mouseout
) на самом холсте и при покидании мышью холста, если идет перетаскивание, завершать его насильно (то есть вызывать onMouseUp
). Но это менее универсальный вариант: если пользователь вышел с мышью за пределы канвы, но вернулся обратно, это может прервать/сломать логику перетаскивания.
Поэтому первый способ (добавить глобальные обработчики для мыши) обычно считается удобнее и надежнее.
Как бы ты реализовал такой функционал в данном приложении?
Ниже разберём два отдельных аспекта:
Самый простой (но и самый грубый) способ — удалить сплошной (однотонный) цвет фона. Например, если фон у картинки белый или зелёный (хромакей), можно «прозрачно» закрасить все пиксели похожего цвета. Для этого:
<canvas>
.ImageData
.<canvas>
.Image()
, у которого фон уже будет прозрачным.Пример (наивный метод, убирает все «почти белые» пиксели):
js/** * Удаляет белый фон (или очень близкий к белому) * @param {HTMLImageElement} img - исходное изображение * @param {number} threshold - порог «близости» к белому (0..255) * @returns {HTMLImageElement} - новое изображение с прозрачным фоном */ function removeWhiteBackground(img, threshold = 15) { // Создаём временный canvas const offCanvas = document.createElement('canvas'); offCanvas.width = img.width; offCanvas.height = img.height; const offCtx = offCanvas.getContext('2d'); // Рисуем на нём исходную картинку offCtx.drawImage(img, 0, 0); // Получаем пиксельные данные const imageData = offCtx.getImageData(0, 0, offCanvas.width, offCanvas.height); const data = imageData.data; // [r, g, b, a, r, g, b, a, ...] for (let i = 0; i < data.length; i += 4) { const r = data[i]; const g = data[i + 1]; const b = data[i + 2]; const a = data[i + 3]; // Проверяем, «насколько» пиксель близок к белому // (можно менять логику — например, взять цвет первого пикселя или анализировать углы) const distanceToWhite = Math.sqrt( (255 - r) ** 2 + (255 - g) ** 2 + (255 - b) ** 2 ); if (distanceToWhite < threshold) { // Делаем пиксель прозрачным data[i + 3] = 0; } } // Записываем изменённую картинку обратно offCtx.putImageData(imageData, 0, 0); // Создаём новое изображение, в котором фон будет прозрачным const newImg = new Image(); newImg.src = offCanvas.toDataURL(); // data:image/png;base64,... return newImg; }
В этом примере мы грубо «вырезаем» всё, что близко к белому цвету. Если нужно вырезать зелёный фон (хромакей), то есть «green screen», берём порог по зелёному каналу и т.д. Либо пишем более сложный алгоритм.
Далее, когда картинка загружена (в loadImageFromUrl
или loadImageFromDataUrl
), перед тем как вызвать addImageToCanvas(...)
, можно пропустить её через этот метод:
jsfunction loadImageFromUrl(url) { const img = new Image(); img.crossOrigin = 'anonymous'; // если нужны кросс-доменные картинки img.onload = () => { // Удаляем белый фон const imageWithoutBg = removeWhiteBackground(img); // Добавляем на холст уже «очищенную» картинку addImageToCanvas(imageWithoutBg, false); }; img.onerror = () => { alert('Ошибка загрузки изображения по URL'); }; img.src = url; }
Теперь загружаемое изображение станет «прозрачным» там, где был условный цвет фона.
Важно: если фон у картинки не однотонный, или требуется «качественная» сегментация (например, вырезать человека по контуру), то вручную (через
<canvas>
) сделать это непросто. Обычно используют готовые нейросетевые сервисы вроде remove.bg или т.п., но тогда придётся отсылать картинку на сервер или использовать специальные библиотеки.
В вашем коде уже есть механика «isBackground
», которая при добавлении объекта кладёт его в начало массива слоёв (через unshift
) и при этом удаляет предыдущий фон:
jsif (isBackground) { // Удаляем предыдущий фон objectsOnCanvas = objectsOnCanvas.filter(obj => !obj.isBackground); objectsOnCanvas.unshift(object); } else { objectsOnCanvas.push(object); }
Добавляем кнопку «Добавить фон».
Судя по вашему HTML, она уже есть:
html<div> <label>Добавить изображение как фон:</label> <button id="addBackgroundBtn">Добавить фон</button> </div>
Вешаем обработчик на эту кнопку.
Допустим, вы хотите подставить в качестве фона либо URL из текстового поля (#imageUrl
), либо берёте последний загруженный файл. Тогда делаем что-то вроде:
jsconst addBackgroundBtn = document.getElementById('addBackgroundBtn'); addBackgroundBtn.addEventListener('click', () => { const url = imageUrlInput.value.trim(); if (!url) { alert('Введите URL для фона или загрузите файл.'); return; } // Загружаем картинку и добавляем как фон loadImageAsBackground(url); }); function loadImageAsBackground(url) { const img = new Image(); img.crossOrigin = 'anonymous'; img.onload = () => { // Если нужно убрать фон у самой фоновой картинки — тоже можно // const imageWithoutBg = removeWhiteBackground(img); // addImageToCanvas(imageWithoutBg, true); addImageToCanvas(img, true); }; img.onerror = () => { alert('Ошибка загрузки фонового изображения.'); }; img.src = url; }
При добавлении фона в addImageToCanvas(img, true)
, объект попадает на «задний план» массива objectsOnCanvas
, поскольку вызывается unshift(object)
, и при этом старый фон (если был) убирается.
Таким образом, если вы хотите:
addImageToCanvas(imageWithoutBg, false)
.addImageToCanvas(..., true)
.Код уже учитывает флаг isBackground
.
Пример кусочка кода:
js// При клике «Загрузить из URL» — грузим «без фона», кладём как foreground loadUrlBtn.addEventListener('click', () => { const url = imageUrlInput.value.trim(); if (url) { const img = new Image(); img.crossOrigin = 'anonymous'; img.onload = () => { // 1) вырезаем фон у картинки const imageWithoutBg = removeWhiteBackground(img); // 2) добавляем на холст addImageToCanvas(imageWithoutBg, false); }; img.onerror = () => alert('Ошибка загрузки изображения.'); img.src = url; } }); // При клике «Добавить фон» — грузим и кладём как background addBackgroundBtn.addEventListener('click', () => { const url = imageUrlInput.value.trim(); if (url) { const bgImg = new Image(); bgImg.crossOrigin = 'anonymous'; bgImg.onload = () => { // Можно не вырезать фон, если «фон на фон» не мешает addImageToCanvas(bgImg, true); }; bgImg.onerror = () => alert('Ошибка загрузки фоновой картинки.'); bgImg.src = url; } });
Таким образом, у вас будет возможность (1) загружать «вырезанные» изображения в пользовательский проект, (2) загружать картинку и ставить её как настоящий фон слоя.