ChatGPT解决这个技术问题 Extra ChatGPT

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

我是 HTML5 的一员 n00b,我正在使用 canvas 来渲染形状、颜色和文本。在我的应用程序中,我有一个 view adapter 动态创建画布,并用内容填充它。这非常有效,除了我的文本被渲染得非常模糊/模糊/拉伸。我看过很多其他帖子,说明为什么在 CSS 中定义 widthheight 会导致这个问题,但我在 javascript 中定义了这一切。

相关代码(查看Fiddle):

var width = 500;//FIXME:size.w; var height = 500;//FIXME:size.h; var canvas = document.createElement("canvas"); //canvas.className="singleUserCanvas";画布.宽度=宽度;画布.高度=高度; canvas.border = "3px 实心#999999"; canvas.bgcolor = "#999999"; canvas.margin = "(0, 2%, 0, 2%)"; var context = canvas.getContext("2d"); /////////////////// 形状 //// //////////////// var left = 0; //绘制区域1 rect context.fillStyle = "#8bacbe"; context.fillRect(0, (canvas.height*5/6)+1, canvas.width*1.5/8.5, canvas.height*1/6);左=左+canvas.width*1.5/8.5; //绘制区域 2 矩形 context.fillStyle = "#ffe381"; context.fillRect(left+1, (canvas.height*5/6)+1, canvas.width*2.75/8.5, canvas.height*1/6);左 = 左 + canvas.width*2.75/8.5 + 1; //绘制区域 3 rect context.fillStyle = "#fbbd36"; context.fillRect(left+1, (canvas.height*5/6)+1, canvas.width*1.25/8.5, canvas.height*1/6);左=左+canvas.width*1.25/8.5; //绘制目标区域 rect context.fillStyle = "#004880"; context.fillRect(left+1, (canvas.height*5/6)+1, canvas.width*0.25/8.5, canvas.height*1/6);左=左+canvas.width*0.25/8.5; //绘制区域 4 rect context.fillStyle = "#f8961d"; context.fillRect(left+1, (canvas.height*5/6)+1, canvas.width*1.25/8.5, canvas.height*1/6);左 = 左 + canvas.width*1.25/8.5 + 1; //绘制区域 5 rect context.fillStyle = "#8a1002"; context.fillRect(left+1, (canvas.height*5/6)+1, canvas.width-left, canvas.height*1/6); ///////////////// TEXT //// ////////////////用户名 context.fillStyle = "黑色的”; context.font = "粗体 18px 无衬线"; context.textAlign = '对'; context.fillText("用户名", 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); //这部分是在视图适配器回调之后完成的,但在这里与将视图重新添加到布局中相关。 var parent = document.getElementById("layout-content"); parent.appendChild(画布);

我看到的结果(在 Safari 中)比 Fiddle 中显示的更偏斜:

https://i.stack.imgur.com/Dh51D.png

小提琴

https://i.stack.imgur.com/HuRUB.png

我做错了什么?每个文本元素是否需要单独的画布?是字体吗?我需要先在 HTML5 布局中定义画布吗?有错别字吗?我搞不清楚了。

好像您没有调用 clearRect
这个 polyfill 修复了 HiDPI 浏览器中大多数基本的画布操作,这些浏览器不会自动升级(目前 safari 是唯一的)... github.com/jondavidjohn/hidpi-canvas-polyfill
我一直在开发一个 JS 框架,用 DIV 马赛克解决画布模糊问题。在内存/cpu js2dx.com 方面,我以一定的成本生成了更清晰、更清晰的图像

M
MyNameIsKo

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

在 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 上测试。
在我的 MacBook 上的 Firefox、Chrome 和 Safari 中,backingStorePixelRatio 已被贬值且未定义。只有 document.createElement("canvas").getContext("2d").webkitBackingStorePixelRatio 在 Safari 中返回一个数字,该数字是 1。因此,将 PIXEL_RATIO 定义为等于 window.devicePixelRatio || 1 似乎会给出相同的结果。
P
Phil

解决了!

我决定看看我在 javascript 中设置的 widthheight 属性发生了什么变化,看看它是如何影响画布大小的——但它没有。它改变了分辨率。

为了得到我想要的结果,我还必须设置 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,它在大多数现代浏览器中都得到了很好的实现。
如果我需要在服务器中生成画布,Node.js 中没有窗口 objetc:/
T
Toastrackenigma

虽然@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 替换所有比率代码,它有很好的支持

这也意味着我们少声明了一个全局属性——万岁!!

我们也可以去掉 || 1 window.devicePixelRatio 的后备,因为它没有意义:所有不支持此属性的浏览器也不支持 .setTransform 或 .scale,因此此功能对它们不起作用,无论是否后备;

我们可以将 .setTransform 替换为 .scale,因为传入宽度和高度比传入变换矩阵更直观一些。

该函数已从 createHiDPICanvas 重命名为 createHiPPICanvas。正如@MyNameIsKo 自己在他们的回答评论中提到的那样,DPI(每英寸点数)是打印术语(因为打印机用彩色墨水的微小点组成图像)。虽然类似,但显示器使用像素显示图像,因此 PPI(每英寸像素数)是我们用例的更好缩写。

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


感谢这次更新。但是,这是否解决了@spenceryue 在此处提出的问题:stackoverflow.com/a/54027313/144088
1
1valdis

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

根据 MDN

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

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


A
Adam Mańkowski

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

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

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


B
Basj

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

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

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


s
spenceryue

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

默认画布分辨率尺寸为 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/

主要的(); // 在窗口调整大小时重新运行。 window.addEventListener('resize', main); function main() { // 准备具有适当缩放尺寸的画布。缩放画布(); // 通过渲染一些文本来测试缩放计算。测试渲染(); } 函数 scaleCanvas() { 常量容器 = document.querySelector('#container'); const canvas = document.querySelector('#canvas'); // 从容器中获取所需的画布尺寸。让 {width, height} = container.getBoundingClientRect(); // 获取像素比例。常量 dpr = window.devicePixelRatio; // (可选)报告 dpr。 document.querySelector('#dpr').innerHTML = dpr.toFixed(4); // 画布的大小比期望的大一点。 // 在实际代码中使用 exaggeration = 0。常量夸大 = 20;宽度 = Math.ceil (宽度 * dpr + 夸张);高度 = Math.ceil(高度 * dpr + 夸张); // 设置画布分辨率尺寸(整数值)。画布.宽度 = 宽度; canvas.height = 高度; /*------------------------------------------------ ----------- - 关键步骤 - 相对于画布分辨率尺寸设置画布布局尺寸。 (不一定是整数值!)-------------------------------------------- ----------------*/ canvas.style.width = `${width / dpr}px`; canvas.style.height = `${height / dpr}px`; // 调整画布坐标以使用 CSS 像素坐标。 const ctx = canvas.getContext('2d'); ctx.scale(dpr,dpr); } function testRender() { const canvas = document.querySelector('#canvas'); const ctx = canvas.getContext('2d'); // fontBaseline 是衬线字体的基线位置 // 写为行高的一部分,并从行的顶部 // 向下计算。 (通过反复试验来衡量。) const fontBaseline = 0.83; // 从盒子的顶部开始。让基线 = 0; // 50px 字体文本 ctx.font = `50px serif`; ctx.fillText("Hello World", 0, 基线 + fontBaseline * 50);基线 += 50; // 25px 字体文本 ctx.font = `25px serif`; ctx.fillText("Hello World", 0, 基线 + fontBaseline * 25);基线 += 25; // 12.5px 字体文本 ctx.font = `12.5px serif`; ctx.fillText("Hello World", 0, 基线 + fontBaseline * 12.5); } /* HTML 是红色的 */ #container { background-color: red;位置:相对; /* 设置边框会打乱缩放计算。 */ /* 在真实代码中隐藏画布溢出(如果有)。 */ /* 溢出:隐藏; */ } /* 画布是绿色 */ #canvas { background-color: rgba(0,255,0,.8);动画:2s缓入出无限交替两者比较; } /* 动画比较 HTML 和 Canvas 渲染 */ @keyframes 比较 { 33% {opacity:1;变换:翻译(0,0);} 100% {不透明度:.7; transform: translate(7.5%,15%);} } /* 悬停暂停 */ #canvas:hover, #container:hover > #canvas { animation-play-state: paused; } /* 点击将 Canvas 平移 (1px, 1px) */ #canvas:active { transform: translate(1px,1px) !important;动画:无; } /* HTML 文本 */ .text { 位置:绝对;白颜色; } .text:nth-child(1) { 顶部:0px;字体大小:50px;行高:50px; } .text:nth-child(2) { 顶部:50px;字体大小:25px;行高:25px; } .text:nth-child(3) { 顶部:75px;字体大小:12.5px;行高:12.5px; }

Hello World
Hello World
Hello World

悬停可暂停动画。
点击可将绿色框平移 (1px, 1px)。

red = HTML 渲染
green = Canvas 渲染

< !-- 报告像素比率。 -->

设备像素比: (每个 CSS 像素的物理像素)

缩放浏览器以重新运行缩放计算。 (Ctrl+Ctrl-)


伟大的 !如果我用 Node.js 服务器生成画布图像,我怎么能得到这个集中的文本? (节点中没有窗口对象)谢谢
window.devicePixelRatio 用于此处的每个答案。它依赖于客户端,即使对于单个客户端,它也依赖于 zoom(可以随时更改)。客户端需要将此信息提供给您正在使用的任何画布(无论是在网络工作者还是服务器中)。
对我来说,比率是 1.25,例如 width=500 很好,但不能用 width=501 修复它
如果“可见的 CSS 宽度为 501px”,您的意思是说这个实现不起作用吗?如果可见宽度 (container.style.width) 为 501pxdevicePixelRatio1.25,则逻辑画布宽度 (canvas.width) 应为 Math.ceil(501 * 1.25) = 627,可见画布宽度 (canvas.style.width) 应为 { 8} (px)。
C
Community

我稍微修改了 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);

}

j
jrobins

对于那些在 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}/>
        )
    }
}

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


I
Ievgen

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

按照@MyNameIsKo 的建议获取和缩放像素比率。 pixelRatio = window.devicePixelRatio/ctx.backingStorePixelRatio 在调整大小时缩放画布(避免画布默认拉伸缩放)。将 lineWidth 与 pixelRatio 相乘以找到合适的“真实”像素线粗细: context.lineWidth = thickness * pixelRatio;检查线的粗细是奇数还是偶数。将一半的 pixelRatio 添加到奇数粗细值的行位置。 x = x + 像素比/2;

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

函数 getPixelRatio(context) { dpr = window.devicePixelRatio || 1、bsr = context.webkitBackingStorePixelRatio || context.mozBackingStorePixelRatio || context.msBackingStorePixelRatio || context.oBackingStorePixelRatio || context.backingStorePixelRatio || 1个;返回 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);函数 rescale() { var width = initialWidth * pixelRatio;变量高度 = 初始高度 * 像素比; if (width != context.canvas.width) context.canvas.width = 宽度; if (height != context.canvas.height) context.canvas.height = height; context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); } 函数 pixelPerfectLine(x) { context.save(); context.beginPath();厚度 = 1; // 将笔画粗细乘以像素比! context.lineWidth = 厚度 * pixelRatio; context.strokeStyle = "黑色"; context.moveTo(getSharpPixel(thickness, x), getSharpPixel(thickness, 0)); context.lineTo(getSharpPixel(thickness, x), getSharpPixel(thickness, 200)); context.stroke(); context.restore(); } 函数 pixelPerfectRectangle(x, y, w, h, 厚度, useDash) { context.save(); // 像素完美矩形:context.beginPath(); // 将笔画粗细乘以像素比! context.lineWidth = 厚度 * pixelRatio; context.strokeStyle = "红色"; if (useDash) { context.setLineDash([4]); } // 使用尖锐的 x,y 和整数 w,h! context.strokeRect(getSharpPixel(厚度,x),getSharpPixel(厚度,y),Math.floor(w),Math.floor(h)); context.restore(); } function redraw() { context.clearRect(0, 0, canvas.width, canvas.height);像素完美线(50);像素完美线(120);像素完美线(122);像素完美线(130);像素完美线(132);像素完美矩形(); pixelPerfectRectangle(10, 11, 200.3, 443.2, 1, false); pixelPerfectRectangle(41, 42, 150.3, 443.2, 1, true);像素完美矩形(102、100、150.3、243.2、2、真); } function getSharpPixel(thickness, pos) { if (thickness % 2 == 0) { return pos; } 返回 pos + pixelRatio / 2; } 重新缩放();重绘();画布 { 图像渲染:-moz-crisp-edges;图像渲染:-webkit-crisp-edges;图像渲染:像素化;图像渲染:边缘清晰;宽度:100vh;高度:100vh; }

Resize 事件不会在 snipped 中触发,因此您可以尝试 github 上的文件


P
Palina

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


s
sharmav

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

    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 帖子。