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

时间:2013-03-27 14:25:47

标签: javascript html5 html5-canvas

我与<{1}}合计 n00b 并正在使用HTML5来渲染形状,颜色和文字。在我的应用程序中,我有一个视图适配器,它动态创建一个画布,并用内容填充它。除了我的文本呈现非常模糊/模糊/拉伸之外,这非常有效。我看过很多关于为什么在canvas中定义 width height 会导致此问题的其他帖子,但我在{{1}中定义了它}。

相关代码(查看Fiddle):

HTML

CSS

的Javascript

javascript

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

Rendered output in Safari

小提琴

Rendered output on JSFiddle

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

12 个答案:

答案 0 :(得分:130)

canvas元素独立于设备或显示器的像素比运行。

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

我强烈建议您阅读this article中的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);

希望这有帮助!

答案 1 :(得分:22)

<强>解决!

我决定看看我在javascript中设置的更改 width height 属性的内容,看看它是如何影响画布大小的 - 它没有'吨。它会改变分辨率。

要获得我想要的结果,我还必须设置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

答案 2 :(得分:4)

我通过css调整canvas元素的大小来获取父元素的整个宽度。我注意到我的元素的宽度和高度没有缩放。我一直在寻找设定尺寸的最佳方法。

canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;

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

答案 3 :(得分:3)

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

var canvas = document.getElementById('canvas');
canvas.width = canvas.getBoundingClientRect().width;
canvas.height = canvas.getBoundingClientRect().height;

(它接近AdamMańkowski的解决方案)。

答案 4 :(得分:2)

我稍微调整了MyNameIsKo下的canvg代码(SVG到Canvas js库)。我困惑了一段时间,并为此花了一些时间。希望这有助于某人。

<强> HTML

<div id="chart"><canvas></canvas><svg>Your SVG here</svg></div>

<强>的Javascript

window.onload = function() {

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;
})();

setHiDPICanvas = function(canvas, w, h, ratio) {
    if (!ratio) { ratio = PIXEL_RATIO; }
    var can = 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);
}

var svg = document.querySelector('#chart svg'),
    canvas = document.querySelector('#chart canvas');

var svgSize = svg.getBoundingClientRect();
var w = svgSize.width, h = svgSize.height;
setHiDPICanvas(canvas, w, h);

var svgString = (new XMLSerializer).serializeToString(svg);
var ctx = canvas.getContext('2d');
ctx.drawSvg(svgString, 0, 0, w, h);

}

答案 5 :(得分:2)

对于那些在reactjs工作的人,我调整了MyNameIsKo的答案并且效果很好。这是代码。

<% if @product.variations.count > 1 %>    
      <% @product.variations.each do |variation| %>
            <% if variation.photo_one.present? %>
              <div class="col-md-2" style="padding: 0 7px">
                <div class="btn-filter-wrap">
                  <button class="btn-filter"
                    data-filter=".id-<%= variantion.id %>"
                    style="background: url(<%= (variation.photo_one_url(:thumb)) %>) no-repeat center;
                        background-size: contain;
                        width: 100%;
                        height: 130px">
                  </button>
                </div>
                <div class="row">
                  <div class="col-md-2" style="width: 100%; text-align:center">
                    <h5 style="font-weight: 800;
                        text-transform: uppercase;
                        color: #555555;
                        line-height: 1.8"><%= variation.name %></h5>
                  </div>
                </div>
              </div>
            <% end %>
        <% end %>
      <% end %>

在这个例子中,我将画布的宽度和高度作为道具传递。

答案 6 :(得分:0)

我注意到其他答案中未提及的细节。 画布分辨率会截断为整数值。

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

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

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

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

例如,我可以使用width: 300pxheight: 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);
}
/* 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;
}
<!-- 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>

答案 7 :(得分:0)

我知道这个问题很旧,但是它仍然出现在Google中并且是相关的(我自己碰到了这个问题)。

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

根据MDN

  

按比例放大图像时,必须使用最近邻居算法,以便图像看起来由大像素组成。

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

答案 8 :(得分:0)

对我来说,只有不同的“像素完美”技术的组合才能帮助存档结果:

  1. 按照@MyNameIsKo建议的像素比例进行获取和缩放。

    pixelRatio = window.devicePixelRatio / ctx.backingStorePixelRatio

  2. 在调整大小时缩放画布(避免画布默认拉伸缩放)。

  3. 将lineWidth与pixelRatio乘以找到合适的“真实”像素线粗度:

    context.lineWidth =厚度* pixelRatio;

  4. 检查线条的粗细是否为奇数或偶数。将奇数厚度值的一半pixelRatio添加到行位置。

    x = x + pixelRatio / 2;

奇数行将放置在像素的中间。上面的线用于将其移动一点。

function getPixelRatio(context) {
  dpr = window.devicePixelRatio || 1,
    bsr = context.webkitBackingStorePixelRatio ||
    context.mozBackingStorePixelRatio ||
    context.msBackingStorePixelRatio ||
    context.oBackingStorePixelRatio ||
    context.backingStorePixelRatio || 1;

  return dpr / bsr;
}


var canvas = document.getElementById('canvas');
var context = canvas.getContext("2d");
var pixelRatio = getPixelRatio(context);
var initialWidth = canvas.clientWidth * pixelRatio;
var initialHeight = canvas.clientHeight * pixelRatio;


window.addEventListener('resize', function(args) {
  rescale();
  redraw();
}, false);

function rescale() {
  var width = initialWidth * pixelRatio;
  var height = initialHeight * pixelRatio;
  if (width != context.canvas.width)
    context.canvas.width = width;
  if (height != context.canvas.height)
    context.canvas.height = height;

  context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
}

function pixelPerfectLine(x) {

  context.save();
  context.beginPath();
  thickness = 1;
  // Multiple your stroke thickness  by a pixel ratio!
  context.lineWidth = thickness * pixelRatio;

  context.strokeStyle = "Black";
  context.moveTo(getSharpPixel(thickness, x), getSharpPixel(thickness, 0));
  context.lineTo(getSharpPixel(thickness, x), getSharpPixel(thickness, 200));
  context.stroke();
  context.restore();
}

function pixelPerfectRectangle(x, y, w, h, thickness, useDash) {
  context.save();
  // Pixel perfect rectange:
  context.beginPath();

  // Multiple your stroke thickness by a pixel ratio!
  context.lineWidth = thickness * pixelRatio;
  context.strokeStyle = "Red";
  if (useDash) {
    context.setLineDash([4]);
  }
  // use sharp x,y and integer w,h!
  context.strokeRect(
    getSharpPixel(thickness, x),
    getSharpPixel(thickness, y),
    Math.floor(w),
    Math.floor(h));
  context.restore();
}

function redraw() {
  context.clearRect(0, 0, canvas.width, canvas.height);
  pixelPerfectLine(50);
  pixelPerfectLine(120);
  pixelPerfectLine(122);
  pixelPerfectLine(130);
  pixelPerfectLine(132);
  pixelPerfectRectangle();
  pixelPerfectRectangle(10, 11, 200.3, 443.2, 1, false);
  pixelPerfectRectangle(41, 42, 150.3, 443.2, 1, true);
  pixelPerfectRectangle(102, 100, 150.3, 243.2, 2, true);
}

function getSharpPixel(thickness, pos) {

  if (thickness % 2 == 0) {
    return pos;
  }
  return pos + pixelRatio / 2;

}

rescale();
redraw();
canvas {
  image-rendering: -moz-crisp-edges;
  image-rendering: -webkit-crisp-edges;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
  width: 100vh;
  height: 100vh;
}
<canvas id="canvas"></canvas>

未在摘要中触发调整大小事件,因此您可以尝试在github上尝试文件

答案 9 :(得分:0)

对我来说,这不仅是图像,而且文字质量很差。 适用于视网膜/非视网膜显示器的最简单的跨浏览器工作解决方案是渲染图像两倍于预期大小并按此人的建议缩放画布上下文:https://stackoverflow.com/a/53921030/4837965

答案 10 :(得分:0)

虽然@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;
}

通常,我们会进行以下改进:

  • 我们删除了backingStorePixelRatio引用,因为这些引用实际上并没有以任何重要的方式在任何浏览器中实现(实际上,只有Safari返回的是undefined以外的其他内容,并且该版本在Safari);
  • 我们将所有比率代码替换为window.devicePixelRatiowhich has fantastic support
  • 这也意味着我们少声明一个全局属性--- hooray !!
  • 我们也可以删除|| 1上的window.devicePixelRatio后备广告,因为这毫无意义:所有不支持此属性的浏览器都不支持.setTransform.scale要么,所以此功能将对它们不起作用,无论是否回退;
  • 我们可以将.setTransform替换为.scale,因为传递宽度和高度比传递变换矩阵更直观。
  • 该功能已从createHiDPICanvas重命名为createHiPPICanvas。正如@MyNameIsKo自己在答案中提到的那样,DPI(每英寸点数)是打印术语(因为打印机是用彩色墨水的细小点组成图像的)。监视器虽然相似,但使用像素显示图像,因此PPI(每英寸像素)是我们用例的更好缩写。<​​/ li>

这些简化的一大好处是,现在该答案可以在没有// @ts-ignore的TypeScript中使用(因为TS没有backingStorePixelRatio的类型)。

答案 11 :(得分:0)

以下代码直接为我工作(而其他人没有):

    const myCanvas = document.getElementById("myCanvas");
    const originalHeight = myCanvas.height;
    const originalWidth = myCanvas.width;
    render();
    function render() {
      let dimensions = getObjectFitSize(
        true,
        myCanvas.clientWidth,
        myCanvas.clientHeight,
        myCanvas.width,
        myCanvas.height
      );
      const dpr = window.devicePixelRatio || 1;
      myCanvas.width = dimensions.width * dpr;
      myCanvas.height = dimensions.height * dpr;

      let ctx = myCanvas.getContext("2d");
      let ratio = Math.min(
        myCanvas.clientWidth / originalWidth,
        myCanvas.clientHeight / originalHeight
      );
      ctx.scale(ratio * dpr, ratio * dpr); //adjust this!
    }

    // adapted from: https://www.npmjs.com/package/intrinsic-scale
    function getObjectFitSize(
      contains /* true = contain, false = cover */,
      containerWidth,
      containerHeight,
      width,
      height
    ) {
      var doRatio = width / height;
      var cRatio = containerWidth / containerHeight;
      var targetWidth = 0;
      var targetHeight = 0;
      var test = contains ? doRatio > cRatio : doRatio < cRatio;

      if (test) {
        targetWidth = containerWidth;
        targetHeight = targetWidth / doRatio;
      } else {
        targetHeight = containerHeight;
        targetWidth = targetHeight * doRatio;
      }

      return {
        width: targetWidth,
        height: targetHeight,
        x: (containerWidth - targetWidth) / 10,
        y: (containerHeight - targetHeight) / 10
      };
    }

该实现改编自 this 中型帖子。