Перетаскивание объектов холста
생성일: 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) загружать картинку и ставить её как настоящий фон слоя.