Как исправить размытый текст на холсте HTML5?
Я всего N00B с HTML5
и я работаю с canvas
для визуализации форм, цветов и текста. В моем приложении есть адаптер представления, который динамически создает холст и наполняет его содержимым. Это работает очень хорошо, за исключением того, что мой текст выглядит очень размытым / размытым / растянутым. Я видел много других сообщений о том, почему определение ширины и высоты в CSS
вызовет эту проблему, но я все это определяю в javascript
,
Соответствующий код (см. Fiddle):
HTML
<div id="layout-content"></div>
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);
Результаты, которые я вижу (в Safari), гораздо более искажены, чем показано в Fiddle:
Мой
скрипка
Что я делаю неправильно? Нужен ли отдельный холст для каждого текстового элемента? Это шрифт? Должен ли я сначала определить холст в макете HTML5? Есть ли опечатка? Я потерян.
13 ответов
Элемент canvas работает независимо от соотношения пикселей устройства или монитора.
На iPad 3+ это соотношение равно 2. Это означает, что теперь холст шириной 1000px теперь должен заполнить 2000px, чтобы соответствовать заявленной ширине на дисплее iPad. К счастью для нас, это делается автоматически браузером. С другой стороны, это также является причиной того, почему вы видите меньше определений на изображениях и элементах холста, которые были сделаны для непосредственного соответствия их видимой области. Поскольку ваш холст знает только, как заполнить 1000px, но его просят нарисовать до 2000px, браузер должен теперь разумно заполнить пробелы между пикселями, чтобы отобразить элемент в его правильном размере.
Я настоятельно рекомендую вам прочитать эту статью из HTML5Rocks, которая более подробно объясняет, как создавать элементы высокой четкости.
Т.Л., др? Вот пример (на основе вышеизложенного), который я использую в своих собственных проектах, чтобы выложить холст с надлежащим разрешением:
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);
Надеюсь это поможет!
Решено!
Я решил посмотреть, что меняют атрибуты ширины и высоты, которые я установил 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
Хотя ответ @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 (количество пикселей на дюйм) является лучшим сокращением для нашего варианта использования.
Одним из больших преимуществ этих упрощений является то, что теперь этот ответ можно использовать в TypeScript без
// @ts-ignore
(поскольку в TS нет типов для
backingStorePixelRatio
).
Я знаю, что вопрос старый, но он все еще отображается в Google и актуален (я просто столкнулся с этим сам).
Попробуйте эту строку CSS на вашем холсте:image-rendering: pixelated
Согласно MDN:
При масштабировании изображения необходимо использовать алгоритм ближайшего соседа, чтобы изображение было составлено из больших пикселей.
Таким образом, он полностью предотвращает сглаживание.
Я изменяю размер элемента canvas через css, чтобы взять всю ширину родительского элемента. Я заметил, что ширина и высота моего элемента не масштабируются. Я искал лучший способ установить размер, который должен быть.
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
Таким простым способом ваш холст будет настроен идеально, независимо от того, какой экран вы будете использовать.
Это 100% решило это для меня:
var canvas = document.getElementById('canvas');
canvas.width = canvas.getBoundingClientRect().width;
canvas.height = canvas.getBoundingClientRect().height;
(это близко к решению Адама Манковского).
Я заметил деталь, не упомянутую в других ответах. Разрешение холста усекается до целочисленных значений.
Размеры разрешения холста по умолчанию: canvas.width: 300
а также canvas.height: 150
,
На моем экране window.devicePixelRatio: 1.75
,
Поэтому, когда я установил canvas.height = 1.75 * 150
значение усекается от желаемого 262.5
до 262
,
Решение состоит в том, чтобы выбрать размеры макета CSS для данного window.devicePixelRatio
так что усечение не произойдет при масштабировании разрешения.
Например, я мог бы использовать width: 300px
а также height: 152px
который даст целые числа при умножении на 1.75
,
Редактировать: еще одно решение - воспользоваться тем фактом, что CSS-пиксели могут быть дробными, чтобы противодействовать усечению масштабирования пикселей холста.
Ниже приведена демонстрация с использованием этой стратегии.
Изменить: Вот скрипка ОП обновлена для использования этой стратегии: 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>
Я немного адаптировал код 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);
}
Для тех из вас, кто работает в actjs, я адаптировал ответ 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}/>
)
}
}
В этом примере я передаю ширину и высоту холста в качестве реквизита.
До появления экранов с высоким разрешением каждый пиксель CSS должен был представлять ровно один физический пиксель на данном экране.
Однако с появлением экранов с высоким разрешением эта связь больше не сохранялась. Экраны с высоким разрешением имеют меньшие пиксели, что вынуждает браузеры масштабировать содержимое, чтобы оно имело соответствующий размер.
Размытие вашего опыта является результатом этого масштабирования, поскольку ваш браузер несовершенно пытается «заполнить пробелы» при масштабировании.
Итак... как можно решить проблему размытия, вызванного масштабированием?
Ключевым моментом является создание изображения с более высоким разрешением при фиксированном размере холста.
Другими словами, и более конкретно, вам следует увеличить логическое разрешение внутренней части вашего холста, чтобы логическое разрешение внутри вашего холста соответствовало физическому разрешению этой области вашего экрана... без изменения внешнего размера холста. поле в вашем макете CSS.
Теперь я покажу вам, как именно это сделать в JavaScript.
Предварительные факты:
коэффициент масштабирования, который ваш браузер использует для масштабирования своего содержимого, равен
. (Например, на моем MacBook Pro это число равно 2, что означает, что на каждый пиксель CSS приходится 2 физических пикселя.) и может использоваться для установки логического разрешения внутри холста, в то время как и может использоваться для установки внешнего размера поля холста в пикселях CSS в его макете CSS. Они различны, и именно потому, что они различны, мы можем изменить логическое разрешение внутри холста, не меняя его размера!
Вот код:
// Size the canvas box in CSS pixels.
canvas.style.height = "300px";
canvas.style.width = "300px";
function setUpHiResCanvas(canvas) {
// Get the device pixel ratio, falling back to 1.
const dpr = window.devicePixelRatio || 1;
// Get the size of the canvas in CSS pixels.
const rect = canvas.getBoundingClientRect();
// Scale the resolution of the drawing surface
// (without affecting the physical size of the canvas window).
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
const ctx = canvas.getContext('2d');
// Scale all drawing operations,
// to account for the resolution scaling.
ctx.scale(dpr, dpr);
return ctx;
}
Вуаля! Вышеуказанное решило для меня проблему размытия текста на холсте, вызванную экраном с высоким разрешением.
Более подробную информацию и другое объяснение см. в https://blog.devgenius.io/how-to-resize-a-canvas-on-high-solves-screens-e96324a0617 .
Следующий код работал непосредственно у меня (в то время как другие нет):
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
};
}
Реализация была адаптирована из этого поста Medium.
Для меня это было не только изображение, но и текст плохого качества. Простейшим кроссбраузерным рабочим решением для дисплеев retina/non-retina было визуализировать изображение вдвое больше, чем предполагалось, и масштабировать контекст холста, как предложил этот парень: /questions/40040556/html5-canvas-resize-downscale-izobrazhenie-vyisokogo-kachestva/40040564#40040564
Для меня только комбинация различных техник "безупречного пикселя" помогла заархивировать результаты:
Получите и масштабируйте с соотношением пикселей, как предложил @MyNameIsKo.
pixelRatio = window.devicePixelRatio / ctx.backingStorePixelRatio
Масштабируйте холст при изменении размера (избегайте масштабирования растяжения холста по умолчанию).
умножьте lineWidth на pixelRatio, чтобы найти правильную "реальную" толщину линии пикселя:
context.lineWidth = толщина * pixelRatio;
Проверьте, четная или нечетная толщина линии. добавьте половину pixelRatio к позиции линии для нечетных значений толщины.
х = х + 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