如何修复 HTML5 画布中的模糊文本?
Posted
技术标签:
【中文标题】如何修复 HTML5 画布中的模糊文本?【英文标题】:How do I fix blurry text in my HTML5 canvas? 【发布时间】:2013-03-17 16:20:37 【问题描述】:我是html5
的一员n00b,我正在使用canvas
来渲染形状、颜色和文本。在我的应用程序中,我有一个 view adapter 动态创建画布,并用内容填充它。这非常有效,除了我的文本被渲染得非常模糊/模糊/拉伸。我看过很多其他帖子,为什么在 CSS
中定义 width 和 height 会导致这个问题,但我在 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 中显示的更偏斜:
我的
小提琴
我做错了什么?每个文本元素是否需要单独的画布?是字体吗?我需要先在 HTML5 布局中定义画布吗?有错别字吗?我迷路了。
【问题讨论】:
好像你没有打电话给clearRect
。
这个 polyfill 修复了 HiDPI 浏览器的大部分基本画布操作,这些浏览器不会自动升级(目前 safari 是唯一的)...github.com/jondavidjohn/hidpi-canvas-polyfill
我一直在开发一个 JS 框架,用 DIV 马赛克解决画布模糊问题。在内存/cpu js2dx.com
【参考方案1】:
canvas 元素独立于设备或显示器的像素比运行。
在 iPad 3+ 上,该比率为 2。这实际上意味着您的 1000 像素宽度画布现在需要填充 2000 像素以匹配它在 iPad 显示屏上的规定宽度。对我们来说幸运的是,这是由浏览器自动完成的。另一方面,这也是您在直接适合其可见区域的图像和画布元素上看到较少定义的原因。因为您的画布只知道如何填充 1000 像素,但被要求绘制到 2000 像素,所以浏览器现在必须智能地填充像素之间的空白,以便以适当的大小显示元素。
我强烈建议您阅读HTML5Rocks 中的this article,其中更详细地解释了如何创建高清元素。
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);
希望这会有所帮助!
【讨论】:
认为值得一提的是,我的 createHiDPI() 方法的命名有些糟糕。 DPI 是仅用于打印的术语,而 PPI 是更恰当的首字母缩略词,因为显示器使用像素而不是点来显示图像。 请注意,这个比率实际上会在页面的生命周期内发生变化。例如,如果我将 Chrome 窗口从较旧的“标准”res 外部显示器拖动到 macbook 的内置视网膜屏幕,代码将计算不同的比率。如果您打算缓存此值,则仅供参考。 (外部是比率 1,视网膜屏幕 2,以防你好奇) 感谢您的解释。但是图像资产呢?我们是否需要以双倍分辨率提供每个画布图像并手动缩小? 更多琐事:Windows Phone 8 的 IE 总是为 window.devicePixelRatio 报告 1(并且支持像素调用不起作用)。 1 看起来很糟糕,但 2 的比率看起来不错。目前,我的比率计算至少返回 2(糟糕的解决方法,但我的目标平台是现代手机,它们几乎都具有高 DPI 屏幕)。在 HTC 8X 和 Lumia 1020 上测试。 backingStorePixelRatio 在我的 MacBook 上的 Firefox、Chrome 和 Safari 中已贬值且未定义。只有 document.createElement("canvas").getContext("2d").webkitBackingStorePixelRatio 在 Safari 中返回一个数字,并且该数字是 1。所以似乎将 PIXEL_RATIO 定义为等于window.devicePixelRatio || 1
会给出相同的结果。【参考方案2】:
解决了!
我决定看看我在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
【讨论】:
我不同意。更改 style.width/height 属性正是您创建 HiDPI 画布的方式。 在您的回答中,您将 canvas.width 设置为 1000 并将 canvas.style.width 设置为 500 的一半。这仅适用于像素比为 2 的设备。对于低于此的任何内容,例如您的桌面显示器,画布现在正在绘制不必要的像素。对于更高的比率,您现在又回到了从模糊、低分辨率资产/元素开始的地方。 Philipp 似乎暗示的另一个问题是,您绘制到上下文的所有内容现在都必须绘制到您的双倍宽度/高度,即使它显示为该值的一半。解决此问题的方法是将画布的上下文设置为加倍。 有window.devicePixelRatio
,它在大多数现代浏览器中都得到了很好的实现。【参考方案3】:
虽然@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.devicePixelRatio
、which has fantastic support
这也意味着我们少声明了一个全局属性 --- 万岁!!
我们还可以删除|| 1
的后备window.devicePixelRatio
,因为它没有意义:所有不支持此属性的浏览器也不支持.setTransform
或.scale
,因此此功能不会处理它们,无论是否后备;
我们可以将.setTransform
替换为.scale
,因为传递宽度和高度比传递变换矩阵更直观。
函数已从createHiDPICanvas
重命名为createHiPPICanvas
。正如@MyNameIsKo 自己在他们的答案的 cmets 中提到的那样,DPI(每英寸点数)是打印术语(因为打印机用彩色墨水的微小点组成图像)。虽然类似,但显示器使用像素显示图像,因此 PPI(每英寸像素)是我们用例的更好缩写。
这些简化的一大好处是,这个答案现在可以在没有 // @ts-ignore
的 TypeScript 中使用(因为 TS 没有 backingStorePixelRatio
的类型)。
【讨论】:
感谢此次更新。但是,这是否解决了@spenceryue 在此处提出的问题:***.com/a/54027313/144088?【参考方案4】:这 100% 为我解决了这个问题:
var canvas = document.getElementById('canvas');
canvas.width = canvas.getBoundingClientRect().width;
canvas.height = canvas.getBoundingClientRect().height;
(它接近于 Adam Mańkowski 的解决方案)。
【讨论】:
【参考方案5】:我通过 css 调整画布元素的大小以获取父元素的整个宽度。我注意到我的元素的宽度和高度没有缩放。我正在寻找设置大小的最佳方法。
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
无论您使用什么屏幕,这种简单的方式都可以完美地设置您的画布。
【讨论】:
【参考方案6】:在你的画布上试试这一行 CSS:
image-rendering: pixelated
根据MDN:
放大图像时,必须使用最近邻算法,使图像看起来由大像素组成。
因此它完全防止了抗锯齿。
【讨论】:
【参考方案7】:我注意到其他答案中未提及的细节。 画布分辨率截断为整数值。
默认画布分辨率尺寸为canvas.width: 300
和canvas.height: 150
。
在我的屏幕上,window.devicePixelRatio: 1.75
。
所以当我设置canvas.height = 1.75 * 150
时,值会从所需的262.5
截断为262
。
一种解决方案是为给定的window.devicePixelRatio
选择 CSS 布局尺寸,以便在缩放分辨率时不会发生截断。
例如,我可以使用 width: 300px
和 height: 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 / dprpx`;
canvas.style.height = `$height / dprpx`;
// 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>
【讨论】:
【参考方案8】:我稍微修改了canvg 下的MyNameIsKo 代码(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);
【讨论】:
【参考方案9】:对于那些在 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/>
)
在此示例中,我将画布的宽度和高度作为道具传递。
【讨论】:
【参考方案10】:对我来说,只有结合不同的“像素完美”技术才能帮助归档结果:
按照@MyNameIsKo 建议的像素比例获取和缩放。
pixelRatio = window.devicePixelRatio/ctx.backingStorePixelRatio
在调整大小时缩放画布(避免画布默认拉伸缩放)。
将 lineWidth 与 pixelRatio 相乘以找到合适的“真实”像素线粗细:
context.lineWidth = 厚度 * pixelRatio;
检查线的粗细是奇数还是偶数。将一半的 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>
Resize 事件不会在 snipped 中触发,因此您可以在 github 上尝试文件
【讨论】:
【参考方案11】:对我来说,不仅是图像,而且文字质量也很差。 视网膜/非视网膜显示器最简单的跨浏览器工作解决方案是渲染两倍于预期大小的图像并像这个人建议的那样缩放画布上下文:https://***.com/a/53921030/4837965
【讨论】:
【参考方案12】:以下代码直接为我工作(而其他代码没有):
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 Medium 帖子。
【讨论】:
以上是关于如何修复 HTML5 画布中的模糊文本?的主要内容,如果未能解决你的问题,请参考以下文章