web-dev-qa-db-fra.com

Comment corriger le texte flou dans mon canevas HTML5?

Je suis un total n00b avec HTML5 et je travaille avec la canvas pour restituer des formes, des couleurs et du texte. Dans mon application, j'ai un adaptateur de vue qui crée un canevas de manière dynamique 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 articles sur la raison pour laquelle définir largeur et hauteur dans CSS entraînera ce problème, mais je le définis entièrement dans javascript.

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

Les résultats que je vois (dans Safari) sont beaucoup plus asymétriques que ceux présentés dans le violon:

Mien

Rendered output in Safari

Fiddle

Rendered output on 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.

71
Phil

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

Sur l'iPad 3+, ce rapport est égal à 2. Cela signifie essentiellement que votre canevas d'une largeur de 1 000 pixels devra désormais remplir 2 000 pixels pour correspondre à la largeur indiquée sur l'écran de l'iPad. Heureusement pour nous, cela se fait automatiquement par le navigateur. D’autre part, c’est aussi la raison pour laquelle on voit moins de définitions sur les images et les éléments de canevas conçus pour s’adapter directement à leur zone visible. Parce que votre toile sait seulement remplir 1000 pixels mais doit dessiner à 2000 pixels, le navigateur doit maintenant remplir intelligemment les espaces entre les pixels pour afficher l'élément à sa taille correcte.

Je vous recommande fortement 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 tutoriel 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!

125
MyNameIsKo

Résolu!

J'ai décidé de voir quels changements j'avais apportés aux attributs width et height dans javascript pour voir en quoi cela affectait la taille de la toile - et ce n'est pas le cas. Cela change la résolution. 

Pour obtenir le résultat souhaité, je devais également définir l'attribut canvas.style.width, qui modifie la taille physique de la 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
21
Phil

Ce 100% l'a résolu 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
Basj

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 le meilleur moyen de définir la taille qui devrait l'être.

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

De cette manière simple, votre toile sera réglée parfaitement, peu importe l'écran que vous utiliserez.

3
Adam Mańkowski

J'ai légèrement adapté le code MyNameIsKo _ sous canvg _ (bibliothèque SVG à Canvas js). J'ai été confus pendant un moment et passer du temps pour cela. 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);

}
2
LessWrong

Pour ceux d'entre vous qui travaillaient dans les réactions, 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 de la toile comme accessoires. 

2
jrobins

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

Selon MDN :

Lorsque vous redimensionnez l'image, vous devez utiliser l'algorithme du voisin le plus proche, de sorte que l'image semble être composée de pixels de grande taille.

Ainsi, il empêche complètement l’anti-aliasing.

1
1valdis

J'ai remarqué un détail non mentionné dans les autres réponses. La résolution de la zone de dessin est tronquée en valeurs entières.

Les dimensions de la résolution de la zone de dessin par défaut sont canvas.width: 300 et canvas.height: 150

Sur mon écran, window.devicePixelRatio: 1.75.

Ainsi, lorsque je règle canvas.height = 1.75 * 150, la valeur est tronquée du 262.5 souhaité à 262.

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

Par exemple, je pourrais utiliser width: 300px et height: 152px qui donneraient 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 contrecarrer la troncature des pixels de la zone de mise à l'échelle.

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

Edit: Voici le violon du PO 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>

0
spenceryue

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

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

    pixelRatio = window.devicePixelRatio/ctx.backingStorePixelRatio

  2. Mettez la toile à l'échelle lors du redimensionnement (évitez la mise à l'échelle par extension de la toile).

  3. multipliez la largeur de trait avec pixelRatio pour trouver l’épaisseur de ligne de pixel "réelle" appropriée:

    context.lineWidth = epaisseur * 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 est utilisée pour 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 la capture afin que vous puissiez essayer le fichier sur le github

0
Ievgen Naida