【问题标题】:Rotating an Image without CanvasRenderingContext2D.rotate()在没有 CanvasRenderingContext2D.rotate() 的情况下旋转图像
【发布时间】:2021-01-21 22:17:47
【问题描述】:

我想使用 Javascript 旋转绘制到 Canvas 的图像,但不使用 Context 的 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>

【问题讨论】:

  • 你试过transformcss吗?
  • @Francisco 感谢您的回复。正如帖子中提到的,这不适用于最终会以 HTML 形式出现的内容,因此我将无法使用 CSS。我相信解决方案将是一个更有效的绘制循环。
  • 每次调用 getImageData 时,位图都会从 GPU 传输到 CPU,然后再传输到 GPU。它非常慢,所以尽量少做。幸运的是,您可以获得多个像素。所以获取完整图像的图像数据,循环通过这个单一的图像数据并只组成一个新的图像数据。一旦这个新的 ImageData 完成,就将它放在你的上下文中。

标签: javascript html canvas graphics trigonometry


【解决方案1】:

扫描线 2D 图像渲染。

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

您最终会扫描没有内容的像素,但如果您使用 scanline polygon render 可以解决此问题,这会产生少量成本

简单示例

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

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

由于图像是均匀的,因此可以优化扫描线,以便每行只需要计算一次完整的变换。

scanline 函数有 6 个参数。 ox, oy 旋转原点,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 值的数组执行此操作,是否需要进行任何重大更改?”

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

【讨论】:

  • 这太棒了。非常感谢你!由于这最终将在 Python 中结束,您能否解释一下读写数据缓冲区和 Uint32Arrays 发生了什么(我认为这是 Python 中的 bytearray?)?从我通过代码和您的描述可以看出,它绘制到画布上,然后(在scanline 函数中)它从r32 旋转行并将它们存储在w32 中。在scanline 函数之后w32 如何更新pxWrite 的imageData?谢谢!
  • @Dr.Pontchartrain pxWrite.dataw8 用于评论)和w32 都指向同一个 RAM 缓冲区。 w32 索引 32 位字,w8 索引 8 位字节。设置w32[0] = 0xFF00FF00 设置与w8 相同的RAM 索引0 - 3` [0,255,0,255] 因此设置w32 会同时更新pxWrite 并且速度快约4 倍。反向为真,设置d8[0]=255RED 也会设置w32[0] 的前8 位,w8[3] = 255 ALPHA 设置w32[0] 的后8 位。 w32 的频道顺序是 AABBGGRR,因此橙色是 0xFF0088FF(最右边的红色通道)相当于 CSS 颜色 #FF8800FF(最左边的红色通道)
  • 感谢您的帮助。请原谅我有限的编程知识,但我仍然很困惑:如果您使用new Uint32Array(pxWrite.data.buffer); 创建一个新实例,它如何指向同一个 RAM 缓冲区?另外,假设我不知道如何在 Python 中将图像数据转换为 32 位数组。如果我必须对 r,g,b,a 值的数组进行此操作,是否需要更改任何主要内容?谢谢!
  • @Dr.Pontchartrain 我已经更新了答案(见底部)和代码示例使用字节索引(8Bit)调用new Uint32Array(pxWrite.data.buffer);创建pxWrite.data.buffer引用的缓冲区的新视图它没有创建一个新数组。它们共享包含像素数据的相同缓冲区。 (RAM 中的相同位置)唯一的区别是字大小为 32 位或 8 位,并且每 4 或 1 个字节进行索引例如w32[4] 是第 5 个像素,与w8[16]w8[17]、@987654363 相同的像素@, w8[19] R,G,B,A
  • @Dr.Pontchartrain 你有坐标,我认为位图是错误的。 x, y 是写入位置(您正在设置/渲染的像素),rx, ry 是读取位置(您从中获取像素的像素)
猜你喜欢
  • 2018-12-30
  • 2011-09-15
  • 1970-01-01
  • 2021-12-27
  • 1970-01-01
  • 2012-06-24
  • 2017-10-20
  • 2018-12-03
  • 1970-01-01
相关资源
最近更新 更多