如何修复HTML5画布中的模糊文本?

Phi*_*hil 78 javascript html5 html5-canvas

我和n00b一起HTML5工作,我正在使用它canvas来渲染形状,颜色和文字.在我的应用程序中,我有一个视图适配器,动态创建一个画布,并用内容填充它.除了我的文本呈现非常模糊/模糊/拉伸之外,这非常有效.我已经看到了很多,为什么定义其他职位的宽度高度CSS会引起这个问题,但我确定这一切javascript.

相关代码(查看小提琴):

HTML

<div id="layout-content"></div>
Run Code Online (Sandbox Code Playgroud)

使用Javascript

var width = 500;//FIXME:size.w;
var height = 500;//FIXME:size.h;

var canvas = document.createElement("canvas");
//canvas.className="singleUserCanvas";
canvas.width=width;
canvas.height=height;
canvas.border = "3px solid #999999";
canvas.bgcolor = "#999999";
canvas.margin = "(0, 2%, 0, 2%)";

var context = canvas.getContext("2d");

//////////////////
////  SHAPES  ////
//////////////////

var left = 0;

//draw zone 1 rect
context.fillStyle = "#8bacbe";
context.fillRect(0, (canvas.height*5/6)+1, canvas.width*1.5/8.5, canvas.height*1/6);

left = left + canvas.width*1.5/8.5;

//draw zone 2 rect
context.fillStyle = "#ffe381";
context.fillRect(left+1, (canvas.height*5/6)+1, canvas.width*2.75/8.5, canvas.height*1/6);

left = left + canvas.width*2.75/8.5 + 1;

//draw zone 3 rect
context.fillStyle = "#fbbd36";
context.fillRect(left+1, (canvas.height*5/6)+1, canvas.width*1.25/8.5, canvas.height*1/6);

left = left + canvas.width*1.25/8.5;

//draw target zone rect
context.fillStyle = "#004880";
context.fillRect(left+1, (canvas.height*5/6)+1, canvas.width*0.25/8.5, canvas.height*1/6);

left = left + canvas.width*0.25/8.5;

//draw zone 4 rect
context.fillStyle = "#f8961d";
context.fillRect(left+1, (canvas.height*5/6)+1, canvas.width*1.25/8.5, canvas.height*1/6);

left = left + canvas.width*1.25/8.5 + 1;

//draw zone 5 rect
context.fillStyle = "#8a1002";
context.fillRect(left+1, (canvas.height*5/6)+1, canvas.width-left, canvas.height*1/6);

////////////////
////  TEXT  ////
////////////////

//user name
context.fillStyle = "black";
context.font = "bold 18px sans-serif";
context.textAlign = 'right';
context.fillText("User Name", canvas.width, canvas.height*.05);

//AT:
context.font = "bold 12px sans-serif";
context.fillText("AT: 140", canvas.width, canvas.height*.1);

//AB:
context.fillText("AB: 94", canvas.width, canvas.height*.15);

//this part is done after the callback from the view adapter, but is relevant here to add the view back into the layout.
var parent = document.getElementById("layout-content");
parent.appendChild(canvas);
Run Code Online (Sandbox Code Playgroud)

我看到的结果(在Safari中)比在小提琴中显示的更加偏斜:

Safari中的渲染输出

小提琴

JSFiddle上的渲染输出

我做错了什么?我是否需要为每个文本元素使用单独的画布?是字体吗?我是否需要首先在HTML5布局中定义画布?有拼写错误吗?我搞不清楚了.

MyN*_*sKo 135

canvas元素独立于设备或监视器的像素比运行.

在iPad 3+上,此比率为2.这实际上意味着您的1000px宽度画布现在需要填充2000px以匹配iPad显示屏上的规定宽度.对我们来说幸运的是,这是由浏览器自动完成的.另一方面,这也是为什么你看到更少的图像和画布元素定义直接适合其可见区域的原因.因为你的画布只知道如何填充1000px但是要求绘制到2000px,浏览器现在必须智能地填充像素之间的空白以显示元素的适当大小.

我强烈建议您阅读HTML5Rocks中的这篇文章,它更详细地解释了如何创建高清元素.

TL;博士?这是一个示例(基于上面的tut),我在自己的项目中使用以适当的分辨率吐出画布:

var PIXEL_RATIO = (function () {
    var ctx = document.createElement("canvas").getContext("2d"),
        dpr = window.devicePixelRatio || 1,
        bsr = ctx.webkitBackingStorePixelRatio ||
              ctx.mozBackingStorePixelRatio ||
              ctx.msBackingStorePixelRatio ||
              ctx.oBackingStorePixelRatio ||
              ctx.backingStorePixelRatio || 1;

    return dpr / bsr;
})();


createHiDPICanvas = function(w, h, ratio) {
    if (!ratio) { ratio = PIXEL_RATIO; }
    var can = document.createElement("canvas");
    can.width = w * ratio;
    can.height = h * ratio;
    can.style.width = w + "px";
    can.style.height = h + "px";
    can.getContext("2d").setTransform(ratio, 0, 0, ratio, 0, 0);
    return can;
}

//Create canvas with the device resolution.
var myCanvas = createHiDPICanvas(500, 250);

//Create canvas with a custom resolution.
var myCustomCanvas = createHiDPICanvas(500, 200, 4);
Run Code Online (Sandbox Code Playgroud)

希望这可以帮助!

  • 我认为值得一提的是我的createHiDPI()方法命名不佳.DPI是一个仅用于打印的术语,PPI是一个更恰当的首字母缩略词,因为监视器使用像素而不是点来显示图像. (2认同)
  • 请注意,此比率实际上可以在页面的生命周期内更改.例如,如果我将Chrome窗口从较旧的"标准"res外接显示器拖动到macbook的内置视网膜屏幕,则代码将计算不同的比率.如果您打算缓存此值,只需一个FYI.(外部比例为1,视网膜屏幕2,以防您好奇) (2认同)
  • backingStorePixelRatio 在我的 MacBook 上的 Firefox、Chrome 和 Safari 中已被弃用且未定义。只有 document.createElement("canvas").getContext("2d").webkitBackingStorePixelRatio 在 Safari 中返回一个数字,该数字为 1。因此,似乎将 PIXEL_RATIO 定义为等于 `window.devicePixelRatio || 1`会给出相同的结果。 (2认同)

Phi*_*hil 25

解决了!

我决定看看我设置的更改宽度高度属性的内容javascript,看看它是如何影响画布大小的 - 而事实并非如此.它会改变分辨率.

为了获得我想要的结果,我还必须设置canvas.style.width属性,这会改变以下物理大小canvas:

canvas.width=1000;//horizontal resolution (?) - increase for better looking text
canvas.height=500;//vertical resolution (?) - increase for better looking text
canvas.style.width=width;//actual width of canvas
canvas.style.height=height;//actual height of canvas
Run Code Online (Sandbox Code Playgroud)

  • 我不同意.更改style.width/height属性正是您创建HiDPI画布的方式. (7认同)
  • 为了获得理想的效果,您根本不应该接触画布的样式属性,而只能使用画布本身的宽度和高度属性来控制大小。这样你就可以确保画布上的一个像素等于屏幕上的一个像素。 (2认同)
  • 在你的答案中,你将canvas.width设置为1000,canvas.style.width设置为500的一半.这适用于像素比率为2的设备.对于低于此值的任何设备,如桌面显示器,画布现在是绘制到不必要的像素.对于更高的比率,您现在可以回到开始时使用模糊,低资产/资产元素的地方.Philipp似乎暗指的另一个问题是,您绘制到上下文中的所有内容现在必须被绘制到您的双倍宽度/高度,即使它显示为该值的一半.解决这个问题的方法是将画布的上下文设置为double. (2认同)
  • 有`window.devicePixelRatio`,它在大多数现代浏览器中都得到了很好的实现. (2认同)

1va*_*dis 14

在画布上尝试这一行 CSS: image-rendering: pixelated

根据MDN

当放大图像时,必须使用最近邻算法,使图像看起来是由大像素组成的。

因此它完全阻止了抗锯齿。


Bas*_*asj 8

这 100% 为我解决了这个问题:

var canvas = document.getElementById('canvas');
canvas.width = canvas.getBoundingClientRect().width;
canvas.height = canvas.getBoundingClientRect().height;
Run Code Online (Sandbox Code Playgroud)

(它接近 Adam Ma?kowski 的解决方案)。


spe*_*yue 8

我注意到其他答案中没有提到的一个细节。画布分辨率截断为整数值。

默认画布分辨率尺寸为canvas.width: 300canvas.height: 150

在我的屏幕上,window.devicePixelRatio: 1.75.

因此,当我设置canvas.height = 1.75 * 150该值时,该值将从所需的值截断262.5262

解决方案是选择给定的 CSS 布局尺寸window.devicePixelRatio,以便在缩放分辨率时不会发生截断。

例如,我可以使用width: 300pxandheight: 152px乘以 时会产生整数1.75

编辑:另一个解决方案是利用 CSS 像素可以是小数这一事实来抵消缩放画布像素的截断。

下面是使用此策略的演示。

编辑:这里是OP的小提琴更新为使用此策略:http://jsfiddle.net/65maD/83/

main();

// Rerun on window resize.
window.addEventListener('resize', main);


function main() {
  // Prepare canvas with properly scaled dimensions.
  scaleCanvas();

  // Test scaling calculations by rendering some text.
  testRender();
}


function scaleCanvas() {
  const container = document.querySelector('#container');
  const canvas = document.querySelector('#canvas');

  // Get desired dimensions for canvas from container.
  let {width, height} = container.getBoundingClientRect();

  // Get pixel ratio.
  const dpr = window.devicePixelRatio;
  
  // (Optional) Report the dpr.
  document.querySelector('#dpr').innerHTML = dpr.toFixed(4);

  // Size the canvas a bit bigger than desired.
  // Use exaggeration = 0 in real code.
  const exaggeration = 20;
  width = Math.ceil (width * dpr + exaggeration);
  height = Math.ceil (height * dpr + exaggeration);

  // Set the canvas resolution dimensions (integer values).
  canvas.width = width;
  canvas.height = height;

  /*-----------------------------------------------------------
                         - KEY STEP -
   Set the canvas layout dimensions with respect to the canvas
   resolution dimensions. (Not necessarily integer values!)
   -----------------------------------------------------------*/
  canvas.style.width = `${width / dpr}px`;
  canvas.style.height = `${height / dpr}px`;

  // Adjust canvas coordinates to use CSS pixel coordinates.
  const ctx = canvas.getContext('2d');
  ctx.scale(dpr, dpr);
}


function testRender() {
  const canvas = document.querySelector('#canvas');
  const ctx = canvas.getContext('2d');
  
  // fontBaseline is the location of the baseline of the serif font
  // written as a fraction of line-height and calculated from the top
  // of the line downwards. (Measured by trial and error.)
  const fontBaseline = 0.83;
  
  // Start at the top of the box.
  let baseline = 0;

  // 50px font text
  ctx.font = `50px serif`;
  ctx.fillText("Hello World", 0, baseline + fontBaseline * 50);
  baseline += 50;

  // 25px font text
  ctx.font = `25px serif`;
  ctx.fillText("Hello World", 0, baseline + fontBaseline * 25);
  baseline += 25;

  // 12.5px font text
  ctx.font = `12.5px serif`;
  ctx.fillText("Hello World", 0, baseline + fontBaseline * 12.5);
}
Run Code Online (Sandbox Code Playgroud)
/* HTML is red */

#container
{
  background-color: red;
  position: relative;
  /* Setting a border will mess up scaling calculations. */
  
  /* Hide canvas overflow (if any) in real code. */
  /* overflow: hidden; */
}

/* Canvas is green */ 

#canvas
{
  background-color: rgba(0,255,0,.8);
  animation: 2s ease-in-out infinite alternate both comparison;
}

/* animate to compare HTML and Canvas renderings */

@keyframes comparison
{
  33% {opacity:1; transform: translate(0,0);}
  100% {opacity:.7; transform: translate(7.5%,15%);}
}

/* hover to pause */

#canvas:hover, #container:hover > #canvas
{
  animation-play-state: paused;
}

/* click to translate Canvas by (1px, 1px) */

#canvas:active
{
  transform: translate(1px,1px) !important;
  animation: none;
}

/* HTML text */

.text
{
  position: absolute;
  color: white;
}

.text:nth-child(1)
{
  top: 0px;
  font-size: 50px;
  line-height: 50px;
}

.text:nth-child(2)
{
  top: 50px;
  font-size: 25px;
  line-height: 25px;
}

.text:nth-child(3)
{
  top: 75px;
  font-size: 12.5px;
  line-height: 12.5px;
}
Run Code Online (Sandbox Code Playgroud)
<!-- Make the desired dimensions strange to guarantee truncation. -->
<div id="container" style="width: 313.235px; height: 157.122px">
  <!-- Render text in HTML. -->
  <div class="text">Hello World</div>
  <div class="text">Hello World</div>
  <div class="text">Hello World</div>
  
  <!-- Render text in Canvas. -->
  <canvas id="canvas"></canvas>
</div>

<!-- Interaction instructions. -->
<p>Hover to pause the animation.<br>
Click to translate the green box by (1px, 1px).</p>

<!-- Color key. -->
<p><em style="color:red">red</em> = HTML rendered<br>
<em style="color:green">green</em> = Canvas rendered</p>

<!-- Report pixel ratio. -->
<p>Device pixel ratio: <code id="dpr"></code>
<em>(physical pixels per CSS pixel)</em></p>

<!-- Info. -->
<p>Zoom your browser to re-run the scaling calculations.
(<code>Ctrl+</code> or <code>Ctrl-</code>)</p>
Run Code Online (Sandbox Code Playgroud)


Toa*_*gma 7

虽然@MyNameIsKo 的答案仍然有效,但它在 2020 年现在有点过时了,可以改进:

function createHiPPICanvas(w, h) {
    let ratio = window.devicePixelRatio;
    let cv = document.createElement("canvas");
    cv.width = w * ratio;
    cv.height = h * ratio;
    cv.style.width = w + "px";
    cv.style.height = h + "px";
    cv.getContext("2d").scale(ratio, ratio);
    return cv;
}
Run Code Online (Sandbox Code Playgroud)

总的来说,我们做了以下改进:

  • 我们删除了backingStorePixelRatio引用,因为这些引用并没有在任何浏览器中以任何重要的方式真正实现(实际上,只有 Safari 返回除 之外的其他内容undefined,并且该版本在 Safari 中仍然可以完美运行);
  • 我们将所有比率代码替换为window.devicePixelRatio它具有出色的支持
  • 这也意味着我们少声明了一个全局属性——万岁!!
  • We can also remove the || 1 fallback on window.devicePixelRatio, as it is pointless: all browsers that don't support this property don't support .setTransform or .scale either, so this function won't work on them, fallback or not;
  • We can replace .setTransform by .scale, as passing in a width and height is a little more intuitive than passing in a transformation matrix.
  • The function has been renamed from createHiDPICanvas to createHiPPICanvas. As @MyNameIsKo themselves mention in their answer's comments, DPI (Dots per Inch) is printing terminology (as printers make up images out of tiny dots of coloured ink). While similar, monitors display images using pixels, and as such PPI (Pixels per Inch) is a better acronym for our use case.

One large benefit of these simplifications is that this answer can now be used in TypeScript without // @ts-ignore (as TS doesn't have types for backingStorePixelRatio).


Ada*_*ski 5

我通过CSS调整画布元素的大小,以获取父元素的整个宽度。我注意到元素的宽度和高度没有缩放。我一直在寻找设置尺寸的最佳方法。

canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
Run Code Online (Sandbox Code Playgroud)

无论您使用哪种屏幕,都可以通过这种简单的方式完美设置画布。