Как исправить размытый текст на холсте HTML5?

87

Я в целом n00b с HTML5и работаю над canvasвизуализацией форм, цветов и текста. В моем приложении у меня есть адаптер представления, который динамически создает холст и заполняет его содержимым. Это действительно прекрасно работает, за исключением того, что мой текст отображается очень нечетким / размытым / растянутым. Я видел много других сообщений о том, почему определение ширины и высоты в 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
Этот полифилл исправляет большинство основных операций холста в браузерах HiDPI, которые не масштабируются автоматически (в настоящее время это единственный
вариант
Я разрабатываю JS-фреймворк, который решает проблему размытия холста с помощью мозаики DIV. Я получаю более четкое и резкое изображение за некоторую плату с точки зрения mem / cpu js2dx.com
Gonki

Ответы:

155

Элемент холста работает независимо от соотношения пикселей устройства или монитора.

На iPad 3+ это соотношение равно 2. Это по сути означает, что холст шириной 1000 пикселей теперь должен заполнить 2000 пикселей, чтобы соответствовать заявленной ширине на экране iPad. К счастью для нас, браузер делает это автоматически. С другой стороны, это также причина того, что вы видите меньше четкости на изображениях и элементах холста, которые были сделаны так, чтобы соответствовать их видимой области. Поскольку ваш холст знает, как заполнить только 1000 пикселей, но его просят нарисовать до 2000 пикселей, браузер теперь должен разумно заполнить пробелы между пикселями, чтобы отобразить элемент в его правильном размере.

Я настоятельно рекомендую вам прочитать эту статью из 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);

Надеюсь это поможет!

MyNameIsKo
источник
2
Думаю, стоит упомянуть, что мой метод createHiDPI () имеет несколько неудачное название. DPI - это термин только для печати, а PPI - более подходящее сокращение, поскольку мониторы отображают изображения с использованием пикселей, а не точек.
MyNameIsKo
3
Обратите внимание, что это соотношение может фактически меняться в течение всего срока службы страницы. Например, если я перетащу окно Chrome со старого внешнего монитора «стандартного» разрешения на встроенный экран сетчатки Macbook, код рассчитает другое соотношение. Просто к сведению, если вы планируете кэшировать это значение. (внешний вид был соотношением 1, экран сетчатки 2 на случай, если вам интересно)
Aardvark
1
Спасибо за это объяснение. Но как насчет изображений? Нужно ли нам предоставлять каждое изображение холста с двойным разрешением и уменьшать его вручную?
Kokodoko
1
Еще одна мелочь: IE Windows Phone 8 всегда сообщает 1 для window.devicePixelRatio (и вызов пикселей поддержки не работает). Выглядит ужасно при 1, но при соотношении 2 - хорошо. На данный момент мои расчеты соотношения в любом случае возвращают по крайней мере 2 (дерьмовый обходной путь, но мои целевые платформы - современные телефоны, которые почти все, похоже, имеют экраны с высоким разрешением). Проверено на HTC 8X и Lumia 1020.
Aardvark,
1
@Aardvark: также кажется, что msBackingStorePixelRatio всегда не определено. Задал новый вопрос об этом здесь: stackoverflow.com/questions/22483296/…
Фрэнк ТВ,
28

Решено!

Я решил посмотреть, какие изменения атрибутов ширины и высоты я установил, 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
Для достижения идеальных результатов вы вообще не должны касаться атрибутов стиля холста, а только контролировать размер с помощью атрибутов ширины и высоты самого холста. Таким образом вы убедитесь, что один пиксель на холсте равен одному пикселю на экране.
Филипп
7
Я не согласен. Изменение атрибутов style.width / height - это именно то, как вы создаете холст HiDPI.
MyNameIsKo
2
В своем ответе вы устанавливаете для canvas.width значение 1000 и canvas.style.width на половину, равное 500. Это работает, но только для устройства с соотношением пикселей 2. Для всего, что ниже этого, например, монитора вашего рабочего стола, холст теперь рисование в ненужные пиксели. Для более высоких соотношений вы вернулись туда, где вы начали, с размытым активом / элементом с низким разрешением. Другая проблема, на которую, похоже, ссылался Филипп, заключается в том, что все, что вы рисуете в своем контексте, теперь должно быть нарисовано с удвоенной шириной / высотой, даже если оно отображается с половиной этого значения. Чтобы исправить это, нужно установить двойной контекст холста.
MyNameIsKo
2
Есть window.devicePixelRatio, и это хорошо реализовано в большинстве современных браузеров.
Чен
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;

(это близко к решению Адама Маньковского).

Basj
источник
3

Я немного адаптировал код 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);

}
Меньше неверно
источник
3

Я заметил деталь, не упомянутую в других ответах. Разрешение холста усекается до целых значений.

Размеры разрешения холста по умолчанию - 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 могут быть дробными, чтобы противодействовать усечению масштабируемых пикселей холста.

Ниже представлена ​​демонстрация этой стратегии.

Изменить : вот скрипка 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>

Spenceryue
источник
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}/>
        )
    }
}

В этом примере я передаю ширину и высоту холста в качестве реквизита.

Jrobins
источник
2

Попробуйте эту строку CSS на своем холсте: image-rendering: pixelated

Согласно MDN :

При увеличении масштаба изображения необходимо использовать алгоритм ближайшего соседа, чтобы изображение выглядело состоящим из больших пикселей.

Таким образом, он полностью предотвращает сглаживание.

1валдис
источник
0

Для меня только комбинация различных методов «безупречного пикселя» помогла заархивировать результаты:

  1. Получите и масштабируйте с соотношением пикселей, как предложил @MyNameIsKo.

    pixelRatio = window.devicePixelRatio / ctx.backingStorePixelRatio

  2. Масштабируйте холст при изменении размера (избегайте масштабирования растяжения холста по умолчанию).

  3. умножьте lineWidth на pixelRatio, чтобы найти правильную «реальную» толщину линии пикселя:

    context.lineWidth = толщина * pixelRatio;

  4. Проверьте, четная или нечетная толщина линии. добавьте половину 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

Евгений Наида
источник
0

Для меня это было не только изображение, но и текст плохого качества. Простейшим кроссбраузерным рабочим решением для дисплеев retina / non-retina было визуализировать изображение вдвое больше, чем предполагалось, и масштабировать контекст холста, как предложил этот парень: https://stackoverflow.com/a/53921030/4837965

Полина Потребко
источник