Como faço para corrigir texto borrado em minha tela HTML5?

87

Eu sou um total n00b com HTML5e estou trabalhando com a canvasprestar formas, cores, e texto. Em meu aplicativo, tenho um adaptador de visualização que cria uma tela dinamicamente e a preenche com conteúdo. Isso funciona muito bem, exceto que meu texto fica muito confuso / borrado / esticado. Já vi muitos outros posts sobre por que definir a largura e a altura em CSScausará esse problema, mas eu defino tudo em javascript.

O código relevante (ver 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>

Os resultados que estou vendo (no Safari ) são muito mais distorcidos do que os mostrados no Fiddle:

Meu

Saída renderizada no Safari

Violino

Saída renderizada em JSFiddle

O que estou fazendo incorretamente? Eu preciso de uma tela separada para cada elemento de texto? É a fonte? Devo primeiro definir a tela no layout HTML5? Existe um erro de digitação? Eu estou perdido.

Phil
fonte
Parece que você não está ligando clearRect.
David
1
Este polyfill corrige a maioria das operações básicas de canvas com navegadores HiDPI que não aumentam automaticamente (atualmente o safari é o único) ... github.com/jondavidjohn/hidpi-canvas-polyfill
jondavidjohn
Tenho desenvolvido um framework JS que resolve o problema de desfoque de tela com mosaico DIV. Produzi
Gonki

Respostas:

155

O elemento canvas é executado independentemente da proporção de pixels do dispositivo ou monitor.

No iPad 3+, essa proporção é 2. Isso significa essencialmente que sua tela de largura de 1000 px agora precisa preencher 2.000 px para corresponder à largura declarada na tela do iPad. Felizmente para nós, isso é feito automaticamente pelo navegador. Por outro lado, esta também é a razão pela qual você vê menos definição em imagens e elementos de tela que foram feitos para caber diretamente em sua área visível. Como sua tela sabe apenas como preencher 1000px, mas é solicitada a desenhar em 2000px, o navegador deve agora preencher de forma inteligente os espaços em branco entre os pixels para exibir o elemento em seu tamanho adequado.

Eu recomendo fortemente que você leia este artigo do HTML5Rocks que explica em mais detalhes como criar elementos de alta definição.

tl; dr? Aqui está um exemplo (com base no tut acima) que uso em meus próprios projetos para cuspir uma tela com a resolução adequada:

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);

Espero que isto ajude!

MyNameIsKo
fonte
2
Achei que valeria a pena mencionar que meu método createHiDPI () é um pouco mal nomeado. DPI é um termo para imprimir somente e PPI é um acrônimo mais adequado, pois os monitores exibem imagens usando pixels em vez de pontos.
MyNameIsKo
3
Observe que essa proporção pode realmente mudar durante o tempo de vida da página. Por exemplo, se eu arrastar uma janela do Chrome de um monitor externo de resolução "padrão" mais antigo para uma tela retina embutida de um macbook, o código irá calcular uma proporção diferente. Apenas um FYI se você planeja armazenar esse valor em cache. (a proporção externa era 1, tela de retina 2, caso você esteja curioso)
Aardvark
1
Obrigado por esta explicação. Mas e os ativos de imagem? Precisamos fornecer cada imagem de tela com resolução dupla e reduzi-la manualmente?
Kokodoko
1
Mais curiosidades: o IE do Windows Phone 8 sempre relata 1 para window.devicePixelRatio (e a chamada de pixels de apoio não funciona). Parece horrível em 1, mas uma proporção de 2 parece boa. Por enquanto, meus cálculos de proporção de qualquer maneira retornam pelo menos um 2 (solução alternativa ruim, mas minhas plataformas-alvo são telefones modernos que quase todos parecem ter telas de alto DPI). Testado em HTC 8X e Lumia 1020.
Aardvark
1
@Aardvark: também parece que msBackingStorePixelRatio está sempre indefinido. Fiz uma nova pergunta sobre isso aqui: stackoverflow.com/questions/22483296/…
Frank
28

Resolvido!

Decidi ver o que alterava os atributos de largura e altura que defini javascriptpara ver como isso afetava o tamanho da tela - e não afetou. Isso muda a resolução.

Para obter o resultado que queria, também tive que definir o canvas.style.widthatributo, que altera o tamanho físico do 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
Phil
fonte
1
Para resultados ideais, você não deve tocar nos atributos de estilo da tela e apenas controlar o tamanho com os atributos de largura e altura da própria tela. Dessa forma, você garante que um pixel na tela é igual a um pixel na tela.
Philipp,
7
Discordo. Alterar os atributos style.width / height é exatamente como você cria uma tela HiDPI.
MyNameIsKo
2
Em sua resposta, você definiu canvas.width para 1000 e canvas.style.width para metade em 500. Isso funciona, mas apenas para um dispositivo com uma proporção de pixels de 2. Para qualquer coisa abaixo disso, como seu monitor de desktop, a tela agora é desenho para pixels desnecessários. Para proporções mais altas, você agora está de volta ao ponto de partida com um ativo / elemento borrado e de baixa resolução. Outra questão a que Philipp parecia estar aludindo é que tudo o que você desenha para o seu contexto agora deve ser desenhado para a largura / altura duplicada, embora esteja sendo exibido na metade desse valor. A correção para isso é definir o contexto de sua tela para o dobro.
MyNameIsKo
2
Existe window.devicePixelRatio, e é bem implementado na maioria dos navegadores modernos.
Chen
6

Eu redimensiono o elemento canvas via css para ocupar toda a largura do elemento pai. Percebi que a largura e a altura do meu elemento não são dimensionadas. Eu estava procurando a melhor maneira de definir o tamanho que deveria ser.

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

Desta forma simples, sua tela será configurada perfeitamente, não importa qual tela você usará.

Adam Mańkowski
fonte
5

Isso resolveu 100% para mim:

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

(está próximo da solução de Adam Mańkowski).

Basj
fonte
3

Adaptei um pouco o código MyNameIsKo em canvg (SVG para a biblioteca js do Canvas). Fiquei confuso por um tempo e gastei algum tempo com isso. Espero que isso ajude alguém.

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);

}
Menos errado
fonte
3

Notei um detalhe não mencionado nas outras respostas. A resolução da tela trunca para valores inteiros.

As dimensões de resolução padrão da tela são canvas.width: 300e canvas.height: 150.

Na minha tela window.devicePixelRatio: 1.75,.

Portanto, quando eu defino canvas.height = 1.75 * 150o valor é truncado do desejado 262.5para baixo 262.

Uma solução é escolher as dimensões do layout CSS para um dado window.devicePixelRatio, de forma que o truncamento não ocorra ao dimensionar a resolução.

Por exemplo, eu poderia usar width: 300px e height: 152pxque resultaria em números inteiros quando multiplicados por 1.75.

Edit : Outra solução é tirar vantagem do fato de que os pixels CSS podem ser fracionários para neutralizar o truncamento dos pixels da tela de escala.

Abaixo está uma demonstração usando essa estratégia.

Edit : Aqui está o violino do OP atualizado para usar essa estratégia: 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
fonte
2

Para aqueles de vocês que trabalham em reactjs, adaptei a resposta de MyNameIsKo e funciona muito bem. Aqui está o código.

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}/>
        )
    }
}

Neste exemplo, passo a largura e a altura da tela como adereços.

Jrobins
fonte
2

Experimente esta linha de CSS em sua tela: image-rendering: pixelated

Conforme MDN :

Ao aumentar a imagem, o algoritmo do vizinho mais próximo deve ser usado, de modo que a imagem pareça ser composta de pixels grandes.

Assim, impede totalmente o anti-aliasing.

1valdis
fonte
0

Para mim, apenas uma combinação de diferentes técnicas de 'pixelização perfeita' ajudou a arquivar os resultados:

  1. Obtenha e dimensione com uma proporção de pixel como @MyNameIsKo sugerido.

    pixelRatio = window.devicePixelRatio / ctx.backingStorePixelRatio

  2. Dimensione a tela no redimensionamento (evite o dimensionamento esticado padrão da tela).

  3. multiplique lineWidth com pixelRatio para encontrar a espessura de linha de pixel 'real' adequada:

    context.lineWidth = thick * pixelRatio;

  4. Verifique se a espessura da linha é ímpar ou par. adicione metade de pixelRatio à posição da linha para os valores de espessura ímpares.

    x = x + pixelRatio / 2;

A linha ímpar será colocada no meio do pixel. A linha acima é usada para movê-lo um pouco.

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>

O evento de redimensionamento não é disparado no recorte, então você pode tentar o arquivo no github

Ievgen Naida
fonte
0

Para mim não era apenas a imagem, mas o texto tinha má qualidade. A solução mais simples de trabalho em vários navegadores para telas retina / não retina era renderizar a imagem com o dobro do tamanho pretendido e dimensionar o contexto da tela, como esse cara sugeriu: https://stackoverflow.com/a/53921030/4837965

Polina Potrebko
fonte