Манипуляции с изображениями - добавьте изображение с точными углами

У меня есть изображение, которое является фоном, содержащим область в штучной упаковке, как это:

Искаженная трапеция

Я знаю точное положение углов этой формы, и я хотел бы разместить другое изображение внутри него. (Так что, похоже, внутри коробки).

Мне известен метод 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)

Это дает нам новую линию между внешними вертикальными полосами и вдоль них.

http://i.imgur.com/qAWUK4B.png

Далее нам нужны позиции 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-среды и рендеринга на холст. Вот ссылка на последнее решение:

и пример использования текстуры наложенной поверхности:

Использование этого подхода позволит вам также экспортировать результат в холст или изображение, но для производительности на клиенте требуется графический процессор.

Если вам не нужно экспортировать или манипулировать результатом, я бы предложил использовать простое 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 трехмерных преобразованиях

Другие вопросы по тегам