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


87

我总共使用n00bHTML5并且正在使用canvas渲染形状,颜色和文本。在我的应用程序中,我有一个视图适配器,可以动态创建画布,并在其中填充内容。这非常有效,除了我的文本非常模糊/模糊/拉伸。我还看到了许多其他文章,关于为什么定义in的宽度高度CSS会导致此问题,但我在中都进行了定义javascript

相关代码(查看Fiddle):

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);
<div id="layout-content"></div>

我看到的结果(在Safari中)比Fiddle中显示的要歪斜得多:

在Safari中呈现输出

小提琴

在JSFiddle上呈现输出

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


似乎您没有打电话clearRect
David

1
这个polyfill使用不会自动升级的HiDPI浏览器修复了大多数基本的画布操作(当前仅是野生动物园)... github.com/jondavidjohn/hidpi-canvas-polyfill
jondavidjohn

我一直在开发JS框架,以解决DIV马赛克的画布模糊问题。关于mem / cpu js2dx.com,
-Gonki,

Answers:


155

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

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

我强烈建议您阅读HTML5Rocks的这篇文章,其中详细介绍了如何创建高清元素。

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

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

希望这可以帮助!


2
我认为值得一提的是,我的createHiDPI()方法的命名有点不好。DPI是仅用于打印的术语,PPI是更合适的首字母缩写,因为监视器使用像素而不是点来显示图像。
MyNameIsKo 2013年

3
请注意,该比例实际上可以在页面的生存期内更改。例如,如果我将Chrome窗口从旧的“标准”分辨率外接显示器拖到Macbook的内置视网膜屏幕上,则代码将计算出不同的比率。如果您打算缓存此值,则仅供参考。(如果您好奇,则外部为比率1,视网膜屏幕2)
Aardvark 2013年

1
感谢您的解释。但是图像资产呢?我们是否需要以双分辨率提供每个画布图像并手动将其按比例缩小?
Kokodoko

1
更多琐事:Windows Phone 8的IE始终为window.devicePixelRatio报告1(并且后备像素调用不起作用)。看起来糟透了1,但比率2看起来不错。就目前而言,我的比率计算无论如何至少返回2(糟糕的解决方法,但我的目标平台是现代手机,几乎所有手机似乎都具有高DPI屏幕)。在HTC 8X和Lumia 1020上进行了测试。–
Aardvark

1
@Aardvark:msBackingStorePixelRatio似乎总是未定义。当被问及一个新的问题在这里:stackoverflow.com/questions/22483296/...
电视的弗兰克·

28

解决了!

我决定看看是什么改变了我设置的widthheight属性,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

1
为了获得理想的结果,您根本不应触摸画布的样式属性,而只能通过画布本身的width和height属性控制大小。这样,您可以确保画布上的一个像素等于屏幕上的一个像素。
菲利普

7
我不同意。更改style.width / height属性正是创建HiDPI画布的方式。
MyNameIsKo 2013年

2
在您的答案中,将canvas.width设置为1000,将canvas.style.width设置为500的一半。这仅适用于像素比为2的设备。对于低于此比例的任何设备(例如台式机显示器),画布现在为画到不必要的像素。对于更高的比率,您现在就从模糊,低分辨率的资产/元素开始。Philipp似乎要提到的另一个问题是,即使您所显示的所有内容都显示为该值的一半,现在也必须将其绘制为宽度/高度的两倍。解决此问题的方法是将画布的上下文设置为两倍。
MyNameIsKo 2013年

2
window.devicePixelRatio,并且在大多数现代浏览器中都能很好地实现。
2014年

6

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

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

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


5

这100%为我解决了:

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

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


3

我在canvg(将SVG转换为Canvas js库)下稍微修改了MyNameIsKo代码。我有些困惑,花了一些时间。希望这可以帮助某人。

的HTML

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

Java脚本

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

}

3

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

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

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

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

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

例如,我可以使用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>


2

对于那些使用reactjs工作的人,我改编了MyNameIsKo的答案,它很好用。这是代码。

import React from 'react'

export default class CanvasComponent extends React.Component {
    constructor(props) {
        this.calcRatio = this.calcRatio.bind(this);
    } 

    // Use componentDidMount to draw on the canvas
    componentDidMount() {  
        this.updateChart();
    }

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

    // Draw on the canvas
    updateChart() {

        // Adjust resolution
        const ratio = this.calcRatio();
        this.canvas.width = this.props.width * ratio;
        this.canvas.height = this.props.height * ratio;
        this.canvas.style.width = this.props.width + "px";
        this.canvas.style.height = this.props.height + "px";
        this.canvas.getContext("2d").setTransform(ratio, 0, 0, ratio, 0, 0);
        const ctx = this.canvas.getContext('2d');

       // now use ctx to draw on the canvas
    }


    render() {
        return (
            <canvas ref={el=>this.canvas=el} width={this.props.width} height {this.props.height}/>
        )
    }
}

在此示例中,我传入了画布的宽度和高度作为道具。


2

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

根据MDN

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

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


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上尝试该文件


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.