在没有 CanvasRenderingContext2D.rotate() 的情况下旋转图像

Rotating an Image without CanvasRenderingContext2D.rotate()

我想使用 Javascript 旋转绘制到 Canvas 的图像,但不使用上下文的 rotate 方法(或任何 JS 库)。原因是因为这证明了我在使用另一种语言时遇到的问题,而我无法访问这些语言。

我已经创建了初稿(如下),但我的实现有两个问题:复制位图的逐像素旋转非常慢,并且在像素之间留下间隙。

有没有更快的方法将位图数据放置在一个不会留下间隙的角度?请让我知道,如果你有任何问题。谢谢。

const c1 = document.getElementById("c1");
const c2 = document.getElementById("c2");
const slider = document.getElementById('slider');

const removeAllChildNodes = (parent) =>
{
    while (parent.firstChild) {
        parent.removeChild(parent.firstChild);
    }
}

const rotatedBoundingBox = (width,height,rotation) =>
{
  let rot_w = Math.abs(width * Math.cos(rotation)) + Math.abs(height * Math.sin(rotation));
  let rot_h = Math.abs(width * Math.sin(rotation)) + Math.abs(height * Math.cos(rotation));
  return {width:rot_w,height:rot_h};
}

const render = (rotation) => {
  const canvas = document.createElement("canvas");
  const canvas2 = document.createElement("canvas");
  const ctx = canvas.getContext("2d");
  const ctx2 = canvas2.getContext("2d");
  let txt = "Hello World";
  ctx.font = '20px sans-serif';
  let metrics = ctx.measureText(txt);
  canvas.width = metrics.width;
  canvas.height = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
  ctx.fillStyle = "#636674";
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  ctx.font = '20px sans-serif';
  ctx.textAlign = "center";
  ctx.fillStyle = "#ffaefa";
  ctx.fillText(txt, canvas.width/2, canvas.height);

  const { width, height } = rotatedBoundingBox(canvas.width,canvas.height,rotation);
  canvas2.width = width;
  canvas2.height = height;

  cos = Math.cos(-rotation);
  sin = Math.sin(-rotation);
  cx = canvas.width/2;
  cy = canvas.height/2;

  for (x = 0; x < canvas.width; x++)
  {
    for (y = 0; y < canvas.height; y++)
    {
      var imgd = ctx.getImageData(x, y, 1, 1);
      var pix = imgd.data;
      var red = pix[0];
      var green = pix[1];
      var blue = pix[2];
      var alpha = pix[3];
      nx = ((cos * (x-cx)) + (sin * (y - cy))+canvas2.width/2);
      ny = ((cos * (y-cy)) - (sin * (x - cx))+canvas2.height/2);
      ctx2.putImageData(imgd, nx, ny);
    }
  }

  removeAllChildNodes(c1);
  removeAllChildNodes(c2);
  c1.appendChild(canvas);
  c2.appendChild(canvas2);
}

slider.addEventListener('input', (e) => {
  document.getElementById('currentDegree').innerHTML = e.target.value;
  const rad = parseInt(e.target.value) * Math.PI / 180;
  render(rad);
})

document.getElementById('currentDegree').innerHTML = slider.value;
render(parseInt(slider.value) * Math.PI / 180);
canvas
{
  border: 1px solid rgba(0,0,0,0.2);
}

main
{
  display: flex;
}

.contain
{
  display: inline-block;
}
<div>
  <input id="slider" type="range" min="0" max="360" value="66"/>
  <span>rotation: <span id="currentDegree">0</span>&#176;</span>
</div>
<main>
<div id="c1" class="contain">
</div>
<div id="c2" class="contain">
</div>
</main>

扫描线二维图像渲染。

要移除渲染中的孔洞,请扫描您要渲染到的每个像素并计算该像素来自该图像的哪个位置。

您最终会扫描没有内容的像素,但如果您使用

可以解决这个问题

简单示例

下面的示例创建一个图像,获取图像的像素,还创建一个缓冲区来保存渲染的像素。

它不是按通道读写像素,而是创建缓冲区的 Uint32Array 视图,因此可以在一次操作中读取和写入所有 4 个像素通道。

因为图像是均匀的,所以可以优化扫描线,这样每行只需计算一次完整变换。

扫描线函数有 6 个参数。 oxoy 旋转原点、ang 以弧度为单位的旋转、scale 渲染图像的比例、r32 Uint32Array 视图图像数据包含图像画。 w32 图像数据的 Uint32Array 视图,用于保存结果渲染。

它的性能相当不错,示例每次更新渲染大约 160,000 个像素。

const ctx = canvas.getContext("2d");
const W = ctx.canvas.width, H = ctx.canvas.height;
createTestImage();
const pxWrite = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
const w32 = new Uint32Array(pxWrite.data.buffer); /* not needed for 8 bit ver */
const pxRead = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
const r32 = new Uint32Array(pxRead.data.buffer);  /* not needed for 8 bit ver */

function rotate() {
    const ang = angSlider.value * (Math.PI / 180);
    const scale = scaleSlider.value / 100;

    /* For 8bit replace the following line with the commented line below it */
    scanLine(W / 2, H / 2, ang, scale, r32, w32);
    // scanLine8Bit(W / 2, H / 2, ang, scale, pxRead.data, pxWrite.data);

    ctx.putImageData(pxWrite,0,0);
}
function scanLine(ox, oy, ang, scale, r32, w32) {
    const xAx = Math.cos(ang) / scale;
    const xAy = Math.sin(ang) / scale;
    w32.fill(0);
    var rx, ry, idxW, x = 0, y = 0;
    while (y < H) {
        const xx = x - ox, yy = y - oy;
        rx = xx * xAx - yy * xAy + ox; // Get image coords for row start
        ry = xx * xAy + yy * xAx + oy;
        idxW = y * W + x;
        while (x < W) {
            if (rx >= 0 && rx < W && ry >= 0 && ry < H) {
                w32[idxW] = r32[(ry | 0) * W + (rx | 0)]; 
            }
            idxW ++;
            rx += xAx;
            ry += xAy;
            x++;
        }
        y ++;
        x = 0;
    }
}


function scanLine8Bit(ox, oy, ang, scale, r8, w8) {
    var rx, ry, idxW, idxR, x = 0, y = 0;
    const xAx = Math.cos(ang) / scale;
    const xAy = Math.sin(ang) / scale;
    w8.fill(0);  // clears the buffer
    while (y < H) {
        const xx = x - ox, yy = y - oy;
        rx = xx * xAx - yy * xAy + ox; // Get image coords for row start
        ry = xx * xAy + yy * xAx + oy;
        idxW = (y * W + x) * 4;
        while (x < W) {
            if (rx >= 0 && rx < W && ry >= 0 && ry < H) {
                idxR = ((ry | 0) * W + (rx | 0)) * 4;
                w8[idxW++] = r8[idxR++]; // red 
                w8[idxW++] = r8[idxR++]; // green
                w8[idxW++] = r8[idxR++]; // blue
                w8[idxW++] = r8[idxR++]; // alpha
            } else {
                idxW += 4;
            }
            rx += xAx;
            ry += xAy;
            x++;
        }
        y ++;
        x = 0;
    }
}


angSlider.addEventListener("input", rotate);
scaleSlider.addEventListener("input", rotate);
rotate();
function createTestImage() {
    ctx.fillStyle = "#F00";
    ctx.fillRect(0, 0, W, H);
    ctx.fillStyle = "#FF0";
    ctx.fillRect(20, 20, W - 40, H - 40);
    ctx.fillStyle = "#00F";
    ctx.fillRect(40, 40, W - 80, H - 80);
    ctx.font = "120px Arial";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    ctx.lineWidth = 8;
    ctx.lineJoin = "round";
    ctx.fillStyle = "#000";
    ctx.strokeStyle = "#FFF";
    ctx.strokeText("Scanline!", W / 2, H / 2);
    ctx.fillText("Scanline!", W / 2, H / 2);
}
.inputCont {
  font-family: arial;
  font-weight: 700;
  position: absolute;
  top: 10px;
  left: 20px;
}
canvas {
  position: absolute;
  top: 0px;
  left: 0px;
  border: 1px solid black;
}
label { background: #FFF9 }
#scaleSlider { width: 400px }
#angSlider { width: 400px }
input[type=range]::-webkit-slider-runnable-track {
  background: #8C8;
  height: 8.4px;
  cursor: pointer;
  box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
  border-radius: 4px;
  border: 0.2px solid #010101; 
  padding-top: 0px;
}
input[type=range]::-webkit-slider-thumb {
  margin-top: -5px; 
}
<canvas id="canvas" width="512" height="320"></canvas>
<div class="inputCont">
  <label for="angSlider">Angle</label>
  <input id="angSlider" type="range" min="0" max="360" value="5" /><br>
  <label for="scaleSlider">Scale</label>
  <input id="scaleSlider" type="range" min="10" max="500" value="100" />
  
</div>

注意在此示例中,预期图像和渲染结果分辨率大小相同。这很容易改变。

注意它使用简单的“最近像素”来获取图像像素,因此会出现锯齿现象。然而,以性能为代价添加高质量抗锯齿(通过子像素采样)非常容易。

更新

我已经更新了示例代码,重新评论 “如果我必须使用 r、g、b、a 值的数组来执行此操作,是否需要更改任何主要内容?”

我添加了指示更改内容的注释和第二个函数 scanLine8Bit,它将使用字节数组(8 位)为颜色通道 RGBA 呈现内容。每个像素 4 个字节。