Як виправити розмитий текст на полотні HTML5?


87

Я всього n00b з HTML5і я працюю з canvasдля візуалізації форм, кольорів і тексту. У моєму додатку у мене є адаптер подання, який динамічно створює полотно та наповнює його вмістом. Це працює дуже добре, за винятком того, що мій текст робиться дуже нечітким / розмитим / розтягнутим. Я бачив багато інших публікацій про те, чому визначення ширини та висоти в CSSспричиняє цю проблему, але я все це визначаю в 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);
<div id="layout-content"></div>

Результати, які я бачу (у Safari ), набагато перекошеніші, ніж показано у Скрипці:

Шахта

Виведений результат у Safari

Скрипка

Виведений вихід на JSFiddle

Що я роблю неправильно? Чи потрібно окреме полотно для кожного текстового елемента? Це шрифт? Чи потрібно спочатку визначати полотно в макеті HTML5? Чи є помилка? Я загубився.


Здається, ти не телефонуєш clearRect.
Девід

1
Цей поліфіл виправляє більшість основних операцій на полотні з браузерами HiDPI, які не збільшуються автоматично (на даний момент єдиним є сафарі) ... github.com/jondavidjohn/hidpi-canvas-polyfill
jondavidjohn

Я розробляю структуру JS, яка вирішує проблему розмиття полотна за допомогою мозаїки DIV. Я роблю чіткіше та чіткіше зображення за певну ціну з точки зору mem / cpu js2dx.com
Гонкі,

Відповіді:


155

Елемент полотна працює незалежно від співвідношення пікселів пристрою або монітора.

На iPad 3+ це співвідношення дорівнює 2. Це, по суті, означає, що вашому полотну шириною 1000 пікселів тепер потрібно буде заповнити 2000 пікселів відповідно до вказаної ширини на дисплеї iPad. На наше щастя, це робиться браузером автоматично. З іншого боку, це також причина, чому ви бачите менше чіткості на зображеннях та елементах полотна, які були зроблені так, щоб безпосередньо відповідати їх видимій області. Оскільки ваше полотно знає лише, як заповнити 1000 пікселів, але його просять намалювати до 2000 пікселів, браузер тепер повинен розумно заповнювати пропуски між пікселями, щоб відобразити елемент у належному розмірі.

Я настійно рекомендую вам прочитати цю статтю з HTML5Rocks, де детальніше пояснюється, як створювати елементи високої чіткості.

tl; dr? Ось приклад (на основі вищезазначеного 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);

Сподіваюся, це допомагає!


2
Думав, варто згадати, що мій метод createHiDPI () дещо погано названий. DPI - це термін лише для друку, а PPI - більш правильна абревіатура, оскільки монітори відображають зображення, використовуючи пікселі, на відміну від крапок.
MyNameIsKo

3
Зверніть увагу, що це співвідношення може фактично змінюватися протягом усього терміну дії сторінки. Наприклад, якщо я перетягну вікно Chrome зі старішого "стандартного" зовнішнього монітора на вбудований екран сітківки Macbook, код обчислить інший коефіцієнт. Просто FYI, якщо ви плануєте кешувати це значення. (зовнішнє співвідношення 1, екран сітківки 2 на випадок, якщо вам цікаво)
Aardvark

1
Дякую за це пояснення. Але як щодо іміджевих ресурсів? Чи потрібно нам подавати кожне зображення полотна з подвійною роздільною здатністю та зменшувати його вручну?
Кокодоко,

1
Більше дрібниць: IE Windows Phone 8 завжди звітує 1 для window.devicePixelRatio (і резервний виклик пікселів не працює). Виглядає жахливо на 1, проте співвідношення 2 виглядає добре. Наразі мої розрахунки коефіцієнта в будь-якому випадку повертають щонайменше 2 (обхідне рішення, але мої цільові платформи - це сучасні телефони, які майже у всіх, здається, мають екрани з високим DPI). Випробувано на 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;

(це близько до рішення Адама Маньковського).


3

Я трохи адаптував код MyNameIsKo під canvg (SVG до бібліотеки js Canvas). Я трохи розгубився і витратив на це трохи часу. Сподіваюся, це комусь допоможе.

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>


2

Для тих з вас, хто працює у реакторах, я адаптував відповідь 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. помножте ширину лінії за допомогою 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


0

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.