Comment corriger le texte flou dans mon canevas HTML5?


87

Je suis un total n00b avec HTML5et travaille avec le canvaspour rendre les formes, les couleurs et le texte. Dans mon application, j'ai un adaptateur de vue qui crée un canevas dynamiquement et le remplit de contenu. Cela fonctionne vraiment bien, sauf que mon texte est rendu très flou / flou / étiré. J'ai vu beaucoup d'autres messages sur pourquoi la définition de la largeur et la hauteur en CSScausera cette question, mais je définir tout en javascript.

Le code correspondant (voir 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>

Les résultats que je vois (dans Safari ) sont beaucoup plus biaisés que ceux indiqués dans le violon:

Mien

Sortie rendue dans Safari

Violon

Sortie rendue sur JSFiddle

Qu'est-ce que je fais mal? Ai-je besoin d'un canevas distinct pour chaque élément de texte? Est-ce la police? Dois-je d'abord définir le canevas dans la mise en page HTML5? Y a-t-il une faute de frappe? Je suis perdu.


On dirait que vous n'appelez pas clearRect.
David

1
Ce polyfill corrige la plupart des opérations de canevas de base avec les navigateurs HiDPI qui ne sont pas automatiquement mis à l'échelle (actuellement safari est le seul) ... github.com/jondavidjohn/hidpi-canvas-polyfill
jondavidjohn

J'ai développé un framework JS qui résout le problème du flou du canevas avec la mosaïque DIV. Je produit une image plus claire et plus nette à un certain prix en termes de mem / cpu js2dx.com
Gonki

Réponses:


155

L'élément canvas s'exécute indépendamment du ratio de pixels du périphérique ou du moniteur.

Sur l'iPad 3+, ce rapport est de 2. Cela signifie essentiellement que votre toile de 1000px de largeur devrait maintenant remplir 2000px pour correspondre à sa largeur indiquée sur l'écran de l'iPad. Heureusement pour nous, cela se fait automatiquement par le navigateur. D'un autre côté, c'est aussi la raison pour laquelle vous voyez moins de définition sur les images et les éléments de canevas conçus pour s'adapter directement à leur zone visible. Étant donné que votre canevas ne sait remplir que 1000 pixels mais qu'il est demandé de dessiner à 2000 pixels, le navigateur doit maintenant remplir intelligemment les espaces entre les pixels pour afficher l'élément à sa taille appropriée.

Je vous recommande vivement de lire cet article de HTML5Rocks qui explique plus en détail comment créer des éléments haute définition.

tl; dr? Voici un exemple (basé sur le tut ci-dessus) que j'utilise dans mes propres projets pour cracher une toile avec la résolution appropriée:

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

J'espère que cela t'aides!


2
J'ai pensé qu'il valait la peine de mentionner que ma méthode createHiDPI () est quelque peu mal nommée. DPI est un terme pour impression uniquement et PPI est un acronyme plus approprié car les moniteurs affichent des images en utilisant des pixels plutôt que des points.
MyNameIsKo

3
Notez que ce ratio peut en fait changer pendant la durée de vie de la page. Par exemple, si je fais glisser une fenêtre Chrome d'un ancien moniteur externe res "standard" vers un écran rétine intégré d'un macbook, le code calculera un rapport différent. Juste un FYI si vous prévoyez de mettre en cache cette valeur. (externe était le rapport 1, écran rétine 2 au cas où vous seriez curieux)
Aardvark

1
Merci pour cette explication. Mais qu'en est-il des actifs d'image? Avons-nous besoin de fournir chaque image de canevas à double résolution et de la réduire manuellement?
Kokodoko du

1
Plus de trivia: l'IE de Windows Phone 8 rapporte toujours 1 pour window.devicePixelRatio (et l'appel de pixels de support ne fonctionne pas). Ça a l'air horrible à 1, mais un rapport de 2 semble bon. Pour l'instant, mes calculs de rapport renvoient de toute façon au moins un 2 (solution de contournement merdique, mais mes plates-formes cibles sont des téléphones modernes qui semblent presque tous avoir des écrans DPI élevés). Testé sur HTC 8X et Lumia 1020.
Aardvark

1
@Aardvark: il semble également que msBackingStorePixelRatio soit toujours indéfini. A posé une nouvelle question à ce sujet ici: stackoverflow.com/questions/22483296/…
TV's Frank

28

Résolu!

J'ai décidé de voir ce que je définissais en modifiant les attributs de largeur et de hauteurjavascript pour voir comment cela affectait la taille du canevas - et ce n'est pas le cas. Cela change la résolution.

Pour obtenir le résultat que je souhaitais, j'ai également dû définir l' canvas.style.widthattribut, qui modifie la taille physique du 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
Pour des résultats optimaux, vous ne devez pas du tout toucher les attributs de style du canevas et contrôler uniquement la taille avec les attributs de largeur et de hauteur du canevas lui-même. De cette façon, vous vous assurez qu'un pixel sur le canevas est égal à un pixel à l'écran.
Philipp

7
Je ne suis pas d'accord. Changer les attributs style.width / height est exactement la façon dont vous créez un canevas HiDPI.
MyNameIsKo

2
Dans votre réponse, vous définissez canvas.width sur 1000 et canvas.style.width sur la moitié à 500. Cela fonctionne mais uniquement pour un appareil avec un rapport de pixels de 2. Pour tout ce qui est en dessous, comme votre écran de bureau, le canevas est maintenant dessin aux pixels inutiles. Pour des ratios plus élevés, vous êtes maintenant de retour là où vous avez commencé avec un actif / élément flou et à faible résolution. Un autre problème auquel Philipp semblait faire allusion est que tout ce que vous dessinez dans votre contexte doit maintenant être dessiné à votre largeur / hauteur doublée, même si elle est affichée à la moitié de cette valeur. La solution à ce problème consiste à définir le contexte de votre canevas sur le double.
MyNameIsKo

2
Il existe window.devicePixelRatio, et il est bien implémenté dans la plupart des navigateurs modernes.
Chen

6

Je redimensionne l'élément canvas via css pour prendre toute la largeur de l'élément parent. J'ai remarqué que la largeur et la hauteur de mon élément ne sont pas mises à l'échelle. Je cherchais la meilleure façon de définir la taille qui devrait être.

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

De cette manière simple, votre toile sera parfaitement définie, quel que soit l'écran que vous utiliserez.


5

Cela a résolu le problème à 100% pour moi:

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

(c'est proche de la solution d'Adam Mańkowski).


3

J'ai légèrement adapté le code MyNameIsKo sous canvg (SVG to Canvas js library). J'étais confus pendant un moment et j'y ai passé du temps. J'espère que cela aidera quelqu'un.

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

J'ai remarqué un détail non mentionné dans les autres réponses. La résolution du canevas se tronque en valeurs entières.

Les dimensions de résolution par défaut du canevas sont canvas.width: 300et canvas.height: 150.

Sur mon écran, window.devicePixelRatio: 1.75.

Ainsi, lorsque je définis, canvas.height = 1.75 * 150la valeur est tronquée de la valeur souhaitée 262.5à 262.

Une solution consiste à choisir les dimensions de mise en page CSS pour un window.devicePixelRatioélément donné de telle sorte que la troncature ne se produise pas lors de la mise à l'échelle de la résolution.

Par exemple, je pourrais utiliser width: 300pxet height: 152pxqui donnerait des nombres entiers multipliés par 1.75.

Edit : Une autre solution consiste à tirer parti du fait que les pixels CSS peuvent être fractionnaires pour contrer la troncature des pixels du canevas.

Vous trouverez ci-dessous une démonstration utilisant cette stratégie.

Edit : Voici le violon de l'OP mis à jour pour utiliser cette stratégie: 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

Pour ceux d'entre vous qui travaillent dans reactjs, j'ai adapté la réponse de MyNameIsKo et cela fonctionne très bien. Voici le code.

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

Dans cet exemple, je passe la largeur et la hauteur du canevas comme accessoires.


2

Essayez cette ligne de CSS sur votre canevas: image-rendering: pixelated

Selon MDN :

Lors de la mise à l'échelle de l'image, l'algorithme du plus proche voisin doit être utilisé, de sorte que l'image semble être composée de grands pixels.

Ainsi, il empêche complètement l'anti-crénelage.


0

Pour moi, seule une combinaison de différentes techniques de `` pixel parfait '' a permis d'archiver les résultats:

  1. Obtenez et mettez à l'échelle avec un rapport de pixels comme @MyNameIsKo suggéré.

    pixelRatio = window.devicePixelRatio / ctx.backingStorePixelRatio

  2. Mettez le canevas à l'échelle lors du redimensionnement (évitez la mise à l'échelle par défaut du canevas).

  3. multiple de lineWidth avec pixelRatio pour trouver l'épaisseur de ligne de pixel `` réelle '' appropriée:

    context.lineWidth = épaisseur * pixelRatio;

  4. Vérifiez si l'épaisseur de la ligne est impaire ou paire. ajoutez la moitié de pixelRatio à la position de la ligne pour les valeurs d'épaisseur impaires.

    x = x + pixelRatio / 2;

La ligne impaire sera placée au milieu du pixel. La ligne ci-dessus sert à le déplacer un peu.

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>

L'événement de redimensionnement n'est pas déclenché dans le snipped afin que vous puissiez essayer le fichier sur le github


0

Pour moi, ce n'était pas seulement l'image, mais le texte était de mauvaise qualité. La solution de travail multi-navigateurs la plus simple pour les écrans Retina / non-Retina consistait à rendre l'image deux fois plus grande que prévu et à redimensionner le contexte du canevas comme ce gars l'a suggéré: https://stackoverflow.com/a/53921030/4837965

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.