Манипуляции с изображениями - добавьте изображение с точными углами
У меня есть изображение, которое является фоном, содержащим область в штучной упаковке, как это:
Я знаю точное положение углов этой формы, и я хотел бы разместить другое изображение внутри него. (Так что, похоже, внутри коробки).
Мне известен метод drawImage для HTML5 canvas, но он поддерживает только параметры x, y, width, height, а не точные координаты. Как можно нарисовать изображение на холсте с определенным набором координат, и в идеале браузер должен обрабатывать растяжение изображения.
3 ответа
Четырехугольное преобразование
Один из способов сделать это - использовать четырехугольные преобразования. Они отличаются от 3D-преобразований и позволяют рисовать на холсте, если вы хотите экспортировать результат.
Показанный здесь пример упрощен и использует базовое подразделение и "читы" для самого рендеринга, то есть он рисует маленький квадрат вместо формы подразделенной ячейки, но из-за небольшого размера и перекрытия мы может сойти с рук во многих не экстремальных случаях.
Правильным способом было бы разделить фигуру на два треугольника, затем отсканировать по пикселям в целевом растровом изображении, сопоставить точку от конечного треугольника до исходного треугольника. Если бы значение позиции было дробным, вы использовали бы его для определения интерполяции пикселей (например, билинейный 2x2 или бикубический 4x4).
Я не собираюсь охватывать все это в этом ответе, так как он быстро выйдет из области видимости для формата SO, но этот метод, вероятно, подойдет в этом случае, если вам не нужно его анимировать (он недостаточно эффективен для этого, если вы хочу высокое разрешение).
метод
Давайте начнем с начальной четырехугольной формы:
Первым шагом является интерполяция Y-позиций на каждом столбце C1-C4 и C2-C3. Нам понадобится текущая позиция, а также следующая позиция. Мы будем использовать линейную интерполяцию ("lerp") для этого, используя нормализованное значение для t
:
y1current = lerp( C1, C4, y / height)
y2current = lerp( C2, C3, y / height)
y1next = lerp(C1, C4, (y + step) / height)
y2next = lerp(C2, C3, (y + step) / height)
Это дает нам новую линию между внешними вертикальными полосами и вдоль них.
Далее нам нужны позиции X на этой строке, как текущей, так и следующей. Это даст нам четыре позиции, которые мы будем заполнять текущим пикселем, либо как есть, либо интерполировать его (здесь не показано):
p1 = lerp(y1current, y2current, x / width)
p2 = lerp(y1current, y2current, (x + step) / width)
p3 = lerp(y1next, y2next, (x + step) / width)
p4 = lerp(y1next, y2next, x / width)
x
а также y
будет позиция в исходном изображении с использованием целочисленных значений.
Мы можем использовать эту настройку внутри цикла, который будет перебирать каждый пиксель в исходном растровом изображении.
демонстрация
Демо-версию можно найти внизу ответа. Переместите круглые ручки вокруг, чтобы преобразовать и поиграть со значением шага, чтобы увидеть его влияние на производительность и результат.
Демо будет иметь муар и другие артефакты, но, как упоминалось ранее, это будет тема для другого дня.
Снимок из демо:
Альтернативные методы
Вы также можете использовать WebGL или Three.js для настройки 3D-среды и рендеринга на холст. Вот ссылка на последнее решение:
и пример использования текстуры наложенной поверхности:
- Текстурирование Three.js (вместо определения куба, просто определите одно место / лицо).
Использование этого подхода позволит вам также экспортировать результат в холст или изображение, но для производительности на клиенте требуется графический процессор.
Если вам не нужно экспортировать или манипулировать результатом, я бы предложил использовать простое 3D-преобразование CSS, как показано в других ответах.
/* Quadrilateral Transform - (c) Ken Nilsen, CC3.0-Attr */
var img = new Image(); img.onload = go;
img.src = "https://i.imgur.com/EWoZkZm.jpg";
function go() {
var me = this,
stepEl = document.querySelector("input"),
stepTxt = document.querySelector("span"),
c = document.querySelector("canvas"),
ctx = c.getContext("2d"),
corners = [
{x: 100, y: 20}, // ul
{x: 520, y: 20}, // ur
{x: 520, y: 380}, // br
{x: 100, y: 380} // bl
],
radius = 10, cPoint, timer, // for mouse handling
step = 4; // resolution
update();
// render image to quad using current settings
function render() {
var p1, p2, p3, p4, y1c, y2c, y1n, y2n,
w = img.width - 1, // -1 to give room for the "next" points
h = img.height - 1;
ctx.clearRect(0, 0, c.width, c.height);
for(y = 0; y < h; y += step) {
for(x = 0; x < w; x += step) {
y1c = lerp(corners[0], corners[3], y / h);
y2c = lerp(corners[1], corners[2], y / h);
y1n = lerp(corners[0], corners[3], (y + step) / h);
y2n = lerp(corners[1], corners[2], (y + step) / h);
// corners of the new sub-divided cell p1 (ul) -> p2 (ur) -> p3 (br) -> p4 (bl)
p1 = lerp(y1c, y2c, x / w);
p2 = lerp(y1c, y2c, (x + step) / w);
p3 = lerp(y1n, y2n, (x + step) / w);
p4 = lerp(y1n, y2n, x / w);
ctx.drawImage(img, x, y, step, step, p1.x, p1.y, // get most coverage for w/h:
Math.ceil(Math.max(step, Math.abs(p2.x - p1.x), Math.abs(p4.x - p3.x))) + 1,
Math.ceil(Math.max(step, Math.abs(p1.y - p4.y), Math.abs(p2.y - p3.y))) + 1)
}
}
}
function lerp(p1, p2, t) {
return {
x: p1.x + (p2.x - p1.x) * t,
y: p1.y + (p2.y - p1.y) * t}
}
/* Stuff for demo: -----------------*/
function drawCorners() {
ctx.strokeStyle = "#09f";
ctx.lineWidth = 2;
ctx.beginPath();
// border
for(var i = 0, p; p = corners[i++];) ctx[i ? "lineTo" : "moveTo"](p.x, p.y);
ctx.closePath();
// circular handles
for(i = 0; p = corners[i++];) {
ctx.moveTo(p.x + radius, p.y);
ctx.arc(p.x, p.y, radius, 0, 6.28);
}
ctx.stroke()
}
function getXY(e) {
var r = c.getBoundingClientRect();
return {x: e.clientX - r.left, y: e.clientY - r.top}
}
function inCircle(p, pos) {
var dx = pos.x - p.x,
dy = pos.y - p.y;
return dx*dx + dy*dy <= radius * radius
}
// handle mouse
c.onmousedown = function(e) {
var pos = getXY(e);
for(var i = 0, p; p = corners[i++];) {if (inCircle(p, pos)) {cPoint = p; break}}
}
window.onmousemove = function(e) {
if (cPoint) {
var pos = getXY(e);
cPoint.x = pos.x; cPoint.y = pos.y;
cancelAnimationFrame(timer);
timer = requestAnimationFrame(update.bind(me))
}
}
window.onmouseup = function() {cPoint = null}
stepEl.oninput = function() {
stepTxt.innerHTML = (step = Math.pow(2, +this.value));
update();
}
function update() {render(); drawCorners()}
}
body {margin:20px;font:16px sans-serif}
canvas {border:1px solid #000;margin-top:10px}
<label>Step: <input type=range min=0 max=5 value=2></label><span>4</span><br>
<canvas width=620 height=400></canvas>
Вы можете использовать CSS Transforms, чтобы ваше изображение выглядело как этот блок. Например:
img {
margin: 50px;
transform: perspective(500px) rotateY(20deg) rotateX(20deg);
}
<img src="http://lorempixel.com/400/200/">
Узнайте больше о CSS Transforms на MDN.
Это решение опирается на браузер, выполняющий композитинг. Вы помещаете нужное изображение в отдельный элемент, накладывая фон, используя position: absolute
,
Тогда используйте CSS transform
свойство применять любое перспективное преобразование к элементу наложения.
Чтобы найти матрицу преобразования, вы можете использовать ответ из: Как сопоставить трехмерную перспективу реальной фотографии и объекта в CSS3 трехмерных преобразованиях