Wie behebe ich verschwommenen Text in meiner HTML5-Zeichenfläche?


85

Ich bin insgesamt n00b mit HTML5und arbeite mit dem canvas, um Formen, Farben und Text zu rendern. In meiner App habe ich einen Ansichtsadapter , der eine Zeichenfläche dynamisch erstellt und mit Inhalten füllt. Dies funktioniert sehr gut, außer dass mein Text sehr unscharf / verschwommen / gedehnt ist. Ich habe viele andere Beiträge darüber gesehen, warum das Definieren der Breite und Höhe in CSSdieses Problem verursacht, aber ich definiere alles in javascript.

Der relevante Code (siehe Geige ):

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

Die Ergebnisse, die ich (in Safari ) sehe, sind viel verzerrter als in der Geige gezeigt:

Bergwerk

Gerenderte Ausgabe in Safari

Geige

Ausgabe auf JSFiddle gerendert

Was mache ich falsch? Benötige ich für jedes Textelement eine eigene Leinwand? Ist es die Schriftart? Muss ich zuerst die Zeichenfläche im HTML5-Layout definieren? Gibt es einen Tippfehler? Ich bin verloren.


Scheint, als würdest du nicht anrufen clearRect.
David

1
Diese Polyfill behebt die meisten grundlegenden Canvas-Vorgänge mit HiDPI-Browsern, die nicht automatisch hochskaliert werden (derzeit ist Safari die einzige) ... github.com/jondavidjohn/hidpi-canvas-polyfill
jondavidjohn

Ich habe ein JS-Framework entwickelt, das das Problem der Unschärfe der Leinwand mit DIV-Mosaik löst. Ich produziere ein klareres und schärferes Bild zu einem gewissen Preis in Bezug auf mem / cpu js2dx.com
Gonki

Antworten:


152

Das Canvas-Element wird unabhängig vom Pixelverhältnis des Geräts oder Monitors ausgeführt.

Auf dem iPad 3+ beträgt dieses Verhältnis 2. Dies bedeutet im Wesentlichen, dass Ihre Leinwand mit einer Breite von 1000 Pixel jetzt 2000 Pixel ausfüllen muss, um der auf dem iPad-Display angegebenen Breite zu entsprechen. Zum Glück erledigt dies der Browser automatisch. Auf der anderen Seite ist dies auch der Grund, warum Bilder und Leinwandelemente, die so erstellt wurden, dass sie direkt in ihren sichtbaren Bereich passen, weniger definiert sind. Da Ihre Leinwand nur 1000 Pixel ausfüllt, aber aufgefordert wird, auf 2000 Pixel zu zeichnen, muss der Browser jetzt die Lücken zwischen den Pixeln intelligent ausfüllen, um das Element in der richtigen Größe anzuzeigen.

Ich würde Ihnen wärmstens empfehlen, diesen Artikel aus HTML5Rocks zu lesen , in dem ausführlicher erklärt wird, wie hochauflösende Elemente erstellt werden.

tl; dr? Hier ist ein Beispiel (basierend auf dem obigen Tut), das ich in meinen eigenen Projekten verwende, um eine Leinwand mit der richtigen Auflösung auszuspucken:

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

Hoffe das hilft!


2
Ich dachte, es wäre erwähnenswert, dass meine createHiDPI () -Methode etwas schlecht benannt ist. DPI ist ein Begriff nur für Druck und PPI ist ein besseres Akronym, da Monitore Bilder mit Pixeln im Gegensatz zu Punkten anzeigen.
MyNameIsKo

3
Beachten Sie, dass sich dieses Verhältnis während der Lebensdauer der Seite tatsächlich ändern kann. Wenn ich beispielsweise ein Chrome-Fenster von einem älteren externen Monitor mit "Standard" -Res auf einen integrierten Retina-Bildschirm eines MacBook ziehe, berechnet der Code ein anderes Verhältnis. Nur zu Ihrer Information, wenn Sie diesen Wert zwischenspeichern möchten. (extern war Verhältnis 1, Retina-Bildschirm 2, falls Sie neugierig sind)
Aardvark

1
Danke für diese Erklärung. Aber wie wäre es mit Bildelementen? Müssen wir jedes Leinwandbild mit doppelter Auflösung liefern und manuell verkleinern?
Kokodoko

1
Weitere Trivia: Der IE von Windows Phone 8 meldet immer 1 für window.devicePixelRatio (und der Aufruf von Backing Pixels funktioniert nicht). Sieht bei 1 schrecklich aus, aber ein Verhältnis von 2 sieht gut aus. Im Moment geben meine Verhältnisberechnungen sowieso mindestens 2 zurück (beschissene Problemumgehung, aber meine Zielplattformen sind moderne Telefone, die fast alle Bildschirme mit hoher DPI zu haben scheinen). Getestet auf HTC 8X und Lumia 1020.
Aardvark

1
@Aardvark: Es scheint auch, dass msBackingStorePixelRatio immer undefiniert ist. Habe
Frank vom

28

Gelöst!

Ich beschloss zu sehen, was das ändert mir eingestellten Attribute für Breite und Höhejavascript zu sehen, wie sich dies auf die Leinwandgröße auswirkt - und das tat es nicht. Es ändert die Auflösung.

Um das gewünschte Ergebnis zu erzielen, musste ich auch das canvas.style.widthAttribut festlegen , das die physische Größe von 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
Für optimale Ergebnisse sollten Sie die Stilattribute der Leinwand überhaupt nicht berühren und die Größe nur mit den Attributen width und height der Leinwand selbst steuern. Auf diese Weise stellen Sie sicher, dass ein Pixel auf der Leinwand einem Pixel auf dem Bildschirm entspricht.
Philipp

7
Ich bin nicht einverstanden. Durch Ändern der Attribute style.width / height wird genau eine HiDPI-Zeichenfläche erstellt.
MyNameIsKo

2
In Ihrer Antwort setzen Sie canvas.width auf 1000 und canvas.style.width auf die Hälfte bei 500. Dies funktioniert jedoch nur für ein Gerät mit einem Pixelverhältnis von 2. Für alles darunter, wie Ihren Desktop-Monitor, ist die Zeichenfläche jetzt Zeichnen auf unnötige Pixel. Für höhere Verhältnisse sind Sie jetzt wieder da, wo Sie mit einem verschwommenen Asset / Element mit niedriger Auflösung begonnen haben. Ein weiteres Problem, auf das Philipp anzuspielen schien, ist, dass alles, was Sie in Ihren Kontext zeichnen, jetzt auf Ihre doppelte Breite / Höhe gezeichnet werden muss, obwohl es mit der Hälfte dieses Werts angezeigt wird. Die Lösung hierfür besteht darin, den Kontext Ihrer Leinwand auf doppelt zu setzen.
MyNameIsKo

2
Es gibt window.devicePixelRatiound es ist in den meisten modernen Browsern gut implementiert.
Chen

5

Ich ändere die Größe des Canvas-Elements über CSS, um die gesamte Breite des übergeordneten Elements zu übernehmen. Ich habe festgestellt, dass Breite und Höhe meines Elements nicht skaliert sind. Ich suchte nach dem besten Weg, um die Größe einzustellen, die sein sollte.

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

Auf diese einfache Weise wird Ihre Leinwand perfekt eingestellt, unabhängig davon, welchen Bildschirm Sie verwenden.


4

Das hat es zu 100% für mich gelöst:

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

(Es ist nah an Adam Mańkowskis Lösung).


3

Ich habe den MyNameIsKo- Code unter canvg (SVG to Canvas js library) leicht angepasst . Ich war eine Weile verwirrt und verbrachte einige Zeit damit. Hoffe das hilft jemandem.

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

Für diejenigen unter Ihnen, die in Reaktionen arbeiten, habe ich die Antwort von MyNameIsKo angepasst und es funktioniert großartig. Hier ist der 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}/>
        )
    }
}

In diesem Beispiel übergebe ich die Breite und Höhe der Leinwand als Requisiten.


2

Ich bemerkte ein Detail, das in den anderen Antworten nicht erwähnt wurde. Die Canvas-Auflösung wird auf ganzzahlige Werte gekürzt.

Die Standardabmessungen für die Leinwandauflösung sind canvas.width: 300und canvas.height: 150.

Auf meinem Bildschirm window.devicePixelRatio: 1.75.

Also , wenn ich gesetzt canvas.height = 1.75 * 150wird der Wert von dem gewünschten abgeschnitten 262.5nach unten zu 262.

Eine Lösung besteht darin, die CSS-Layoutabmessungen für eine bestimmte Größe so zu wählen, window.devicePixelRatiodass beim Skalieren der Auflösung keine Kürzungen auftreten.

Zum Beispiel könnte ich verwenden width: 300pxund height: 152pxwas würde ganze Zahlen ergeben, wenn mit multipliziert 1.75.

Bearbeiten : Eine andere Lösung besteht darin, die Tatsache zu nutzen, dass CSS-Pixel gebrochen sein können, um dem Abschneiden der Skalierung von Canvas-Pixeln entgegenzuwirken.

Unten finden Sie eine Demo mit dieser Strategie.

Bearbeiten : Hier ist die Geige des OP aktualisiert, um diese Strategie zu verwenden: 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

Probieren Sie diese eine Zeile CSS auf Ihrer Leinwand aus: image-rendering: pixelated

Wie pro MDN :

Beim Skalieren des Bildes muss der Algorithmus für den nächsten Nachbarn verwendet werden, damit das Bild aus großen Pixeln besteht.

Somit wird Anti-Aliasing vollständig verhindert.


0

Für mich hat nur eine Kombination verschiedener "pixelgenauer" Techniken zur Archivierung der Ergebnisse beigetragen:

  1. Holen Sie sich und skalieren Sie mit einem Pixelverhältnis, wie von @MyNameIsKo vorgeschlagen.

    pixelRatio = window.devicePixelRatio / ctx.backingStorePixelRatio

  2. Skalieren Sie die Leinwand in der Größenänderung (vermeiden Sie die Standard-Stretch-Skalierung der Leinwand).

  3. Multiplizieren Sie die Zeilenbreite mit pixelRatio, um die richtige "echte" Pixelliniendicke zu ermitteln:

    context.lineWidth = Dicke * pixelRatio;

  4. Überprüfen Sie, ob die Dicke der Linie ungerade oder gerade ist. Addiere die Hälfte des pixelRatio zur Linienposition für die ungeraden Dickenwerte.

    x = x + pixelRatio / 2;

Die ungerade Linie wird in der Mitte des Pixels platziert. Die obige Zeile wird verwendet, um es ein wenig zu verschieben.

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>

Das Größenänderungsereignis wird im Snipped nicht ausgelöst, sodass Sie die Datei auf dem Github ausprobieren können

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.