最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Image Manipulation - add image with corners in exact positions - Stack Overflow

programmeradmin1浏览0评论

I have an image which is a background containing a boxed area like this:

I know the exact positions of the corners of that shape, and I'd like to place another image within it. (So it appears to be inside the box).

I'm aware of the drawImage method for HTML5 canvas, but it seems to only support x, y, width, height parameters rather than exact coordinates. How might I draw an image onto a canvas at a specific set of coordinates, and ideally have the browser itself handle stretching the image.

I have an image which is a background containing a boxed area like this:

I know the exact positions of the corners of that shape, and I'd like to place another image within it. (So it appears to be inside the box).

I'm aware of the drawImage method for HTML5 canvas, but it seems to only support x, y, width, height parameters rather than exact coordinates. How might I draw an image onto a canvas at a specific set of coordinates, and ideally have the browser itself handle stretching the image.

Share Improve this question asked Apr 2, 2016 at 11:34 jackweirdyjackweirdy 5,8505 gold badges26 silver badges33 bronze badges 7
  • 1 Can you show your "MCVE" (minimal reproducible example) HTML and CSS? Show what you've used to get to this point, and, ideally, show your attempted solution. That way we can give a relevant answer (without guessing) and explain where your code went wrong, so you can learn. As can future users from this question and its answers. – David Thomas Commented Apr 2, 2016 at 11:39
  • Have you considered using CSS Transforms? – Michał Perłakowski Commented Apr 2, 2016 at 11:39
  • Not possible in 2D canvas as the transform used can only create parallelograms (opposite sides always parallel) you will need to use WebGL instead where you have almost full access to the GPU. If you only need to display the image then as suggested CSS would be best. Or you could try creating your own scan line render for 2D canvas but that will be far from realtime. – Blindman67 Commented Apr 2, 2016 at 15:02
  • Oh I forgot to say. Having the position of the 4 corners will not give you what you want (guessing fitting image to a scene) as 3D projections are dependent on field of view (eqiv camera focal length). Without that info you will find it difficult to match the perspective, not of the points but of the texture mapping. – Blindman67 Commented Apr 2, 2016 at 15:08
  • You can "fake" perspective by offsetting & resizing vertical slices of your image. Here's an example from a previous Stackoverflow Q&A. The advantage of this method over CSS transforms is that you can then convert the perspective canvas drawing into an image element The disadvantage is that you are somewhat limited to adjusting your image either vertically or horizontally -- doing both requires (often disappointing) multiple pixel interpolation. – markE Commented Apr 2, 2016 at 18:18
 |  Show 2 more comments

3 Answers 3

Reset to default 19

Quadrilateral transform

One way to go about this is to use Quadrilateral transforms. They are different than 3D transforms and would allow you to draw to a canvas in case you want to export the result.

The example shown here is simplified and uses basic sub-divison and "cheats" on the rendering itself - that is, it draws in a small square instead of the shape of the sub-divided cell but because of the small size and the overlap we can get away with it in many non-extreme cases.

The proper way would be to split the shape into two triangles, then scan pixel wise in the destination bitmap, map the point from destination triangle to source triangle. If the position value was fractional you would use that to determine pixel interpolation (f.ex. bi-linear 2x2 or bi-cubic 4x4).

I do not intend to cover all this in this answer as it would quickly become out of scope for the SO format, but the method would probably be suitable in this case unless you need to animate it (it is not performant enough for that if you want high resolution).

Method

Lets start with an initial quadrilateral shape:

The first step is to interpolate the Y-positions on each bar C1-C4 and C2-C3. We're gonna need current position as well as next position. We'll use linear interpolation ("lerp") for this using a normalized value for 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)

This gives us a new line between and along the outer vertical bars.

Next we need the X positions on that line, both current and next. This will give us four positions we will fill with current pixel, either as-is or interpolate it (not shown here):

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 and y will be the position in the source image using integer values.

We can use this setup inside a loop that will iterate over each pixel in the source bitmap.

Demo

The demo can be found at the bottom of the answer. Move the circular handles around to transform and play with the step value to see its impact on performance and result.

The demo will have moire and other artifacts, but as mentioned earlier that would be a topic for another day.

Snapshot from demo:

Alternative methods

You can also use WebGL or Three.js to setup a 3D environment and render to canvas. Here is a link to the latter solution:

  • Three.js

and an example of how to use texture mapped surface:

  • Three.js texturing (instead of defining a cube, just define one place/face).

Using this approach will enable you to export the result to a canvas or an image as well, but for performance a GPU is required on the client.

If you don't need to export or manipulate the result I would suggest to use simple CSS 3D transform as shown in the other answers.

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

You can use CSS Transforms to make your image look like that box. For example:

img {
  margin: 50px;
  transform: perspective(500px) rotateY(20deg) rotateX(20deg);
}
<img src="https://via.placeholder.com/400x200">

Read more about CSS Transforms on MDN.

This solution relies on the browser performing the compositing. You put the image that you want warped in a separate element, overlaying the background using position: absolute.

Then use CSS transform property to apply any perspective transform to the overlay element.

To find the transform matrix you can use the answer from: How to match 3D perspective of real photo and object in CSS3 3D transforms

发布评论

评论列表(0)

  1. 暂无评论