绘制到 CanvasRenderingContext2D 时的大小限制

Size limitation when drawing to CanvasRenderingContext2D

我已经对这个问题进行了大量编辑以提供更多细节

我在通过 EaselJS 框架绘制到 CanvasRenderingContext2D 时遇到了一个限制。我有这样的对象:

但是当这些物体的位置超过几百万像素时,图画开始分崩离析。这是 x 位置为 58524928 的同一个对象。(父容器移动到 -58524928,以便我们可以在舞台上看到该对象。)我偏移对象越多,它就会崩溃得越多。此外,当我尝试移动对象时 - 用鼠标拖动它 - 它会 "jump" 就像它受到大网格的影响。

这是 EaseJS 框架,形状最终通过 drawImage() 方法绘制到 CanvasRenderingContext2D。这是代码片段:

ctx.drawImage(缓存Canvas,this._cacheOffsetX+this._filterOffsetX,this._cacheOffsetY+this._filterOffsetY,缓存Canvas.width/scale, 缓存Canvas.height/scale);

我想这与JavaScript中的实数有限有关:

Note that there are infinitely many real numbers, but only a finite number of them (18437736874454810627, to be exact) can be represented exactly by the JavaScript floating-point format. This means that when you're working with real numbers in JavaScript, the representation of the number will often be an approximation of the actual number.

来源:JavaScript: The Definitive Guide

有人可以confirm/reject我的假设吗? 5800万(58524928)对我来说似乎并不多,是EaselJS效率低下还是Canvas的限制?

PS: 缩放没有影响。我把所有东西都画得小了 1000 倍,拉近了 1000 倍,但没有任何效果。同样,如果您将对象放大 1000 倍,同时仍然 x:58 百万,它看起来也不会崩溃。但是将它移动到 500 亿,你就回到了起点。基本上偏移量除以大小是细节的常量限制。

编辑

这是 jsfiddle 示例。net/wzbsbtgc/2。基本上有两个不同的问题

  1. 如果我使用巨大的数字作为绘图本身的参数(红色曲线),它将被扭曲。这可以通过使用较小的数字并改为移动 DisplayObject(蓝色曲线)来避免。
  2. 在这两种情况下都无法将 DisplayObject 移动 1px。我认为这在 GameAlchemist 的 post 中有解释。

欢迎任何advice/workaround第二个问题。

您可以使用您想要的任何大小的绘图坐标。

Canvas 会将您的绘图剪切到 canvas 元素的显示区域。

例如,这是一个从 x = -50000000 开始绘制一条线并在 canvas 结束的演示。仅渲染线条的可见部分。所有不可见的 (off-canvas) 点都被剪掉。

var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;

ctx.beginPath();
ctx.moveTo(-50000000,100);
ctx.lineTo(150,100);
ctx.stroke();
body{ background-color: ivory; padding:10px; }
#canvas{border:1px solid red;}
<h4>This line starts at x = negative 50 million!</h4>
<canvas id="canvas" width=300 height=300></canvas>

请记住,W3C 标准的目标受众主要是浏览器供应商。 unsigned long 值(或 232)更多地解决了浏览器创建位图的底层系统。该标准表示此范围内的值是有效的,但不能保证底层系统能够提供这么大的位图(当今大多数浏览器将位图限制为比此小得多的大小)。你说你不是指 canvas 元素本身,而是 link 引用是元素的接口定义,所以我只想指出关于数字范围的问题。

从事物的 JavaScript 方面来看,我们开发人员通常在那里,除了类型化数组之外,没有 ulong 等这样的东西。只有 Number(又名 unrestricted double) 已签名并以 64 位存储数字,格式为 IEEE-754.

Number 的有效范围是:

Number.MIN_VALUE = 5e-324
Number.MAX_VALUE = 1.7976931348623157e+308

您可以将此范围内的任何值与 canvas 一起用于矢量路径。 Canvas 路径光栅化后,将根据当前变换矩阵将它们裁剪到位图中。

如果您所说的绘图是指另一个位图(即图像、Canvas、视频),那么它们将受制于相同的系统和浏览器capabilities/restrictions 作为目标 canvas 本身。定位(直接或通过转换)受数字范围的限制(总的来说)。

您看到的行为与 IEEE 754 标准中数字的表示方式有关。

虽然 Javascript 使用 64 位浮点数,但 WebGL 仅使用 32 位浮点数,并且由于大多数(?所有?)画布都是 webGL 加速的,所以您的所有数字都将在绘制前进行(向下)转换。

IEEE 754 32 位标准使用 32 位来表示一个数字:1 位用于符号,8 位指数位,然后只有 23 位用于尾数。

让我们调用 IEEE_754_32max :

 IEEE_754_32max = ( 1 << 23 ) -1 = 8.388.6071  (8+ millions)  

我们只能对 [-IEEE_754_32max, IEEE_754_32max] 范围内的整数进行全精度处理。
超出该点,将使用指数,我们将丢失尾数的弱位。

比如(1000万+1)=10.000.001太大了,放不下23位,所以会存储为

    10.000.001 = 10.000.00• * 10 = 1e7 = 10.000.000  

- 我们失去了最后的 '1' -

您看到的网格效果与正在使用的指数/丢失的精度有关:对于诸如 58524928 之类的数字,我们需要 26 位来表示该数字。所以 3 位丢失了,例如:

 58524928 + 7 == 58524928  

因此,当使用接近 58524928 的数字时,它将四舍五入为 58524928,或者 'jump' 到最接近的可能数字:您有网格效果。

解决方案?
-->> 更改您在应用程序中使用的单位,以获得更小的数字。也许您使用的是毫米 --> 使用米或公里。
请注意,您使用的精度是一种幻觉:显示分辨率是第一个限制,鼠标最多精确到 1 个像素,因此即使使用 4K 显示器,32 位浮点数也不会成为限制。

选择正确的测量单位以在较小范围内适合您的所有坐标,您将解决您的问题。

更清楚:您必须更改您用于显示的单位。这并不意味着你必须牺牲准确性:你只需要在绘图之前自己进行翻译 + 缩放:那样你仍然使用 Javascript IEEE 64 位精度并且你不再有那些 32 位舍入问题。

(您可以使用 getters/setters

覆盖 x、y 属性
Object.defineProperty(targetObject, 'x', { get : function() { return view.pixelWidth*(this.rx-view.left)/view.width ;  } }

)

似乎 Context2D 使用较低精度的数字进行转换。我还没有确认精度,但我的猜测是它使用的是浮点数而不是双精度数。

因此,如果值较高,EaselJS 所依赖的 transform 方法(以及其他类似的 C2D 方法)会失去精度,类似于 GameAlchemist 所描述的。您可以在此处看到使用纯 C2D 调用重现的此问题: http://jsfiddle.net/9fLff2we/

我能想到的最佳解决方法是预先计算转换方法外部的 "final" 值。普通 JS 数字比 C2D 使用的精度更高,所以这应该可以解决问题。非常粗略的例子来说明: http://jsfiddle.net/wzbsbtgc/3/