Typografische Effekte in Canvas

HTML5 Rocks

Informationen zu meiner Person

Zum ersten Mal stieß ich im Jahr 2006 auf <canvas>, als die Version 2.0 von Firefox auf den Markt kam. Ein Artikel zu Ajaxian, der die Transformationsmatrix beschrieb, brachte mich im Jahr 2007 dazu, meine erste <canvas>-Webanwendung, Color Sphere, zu erstellen. Dadurch beschäftigte ich mich intensiv mit der Welt der Farben und den Grundlagen zu Grafiken. Als Ergebnis entstand der Sketchpad (2007-2008) mit dem Ziel, dem Browser eine "gelungenere Anwendung als Paint" hinzuzufügen.

Diese Ereignisse führten schließlich zur Gründung des Startup-Unternehmens Mugtug mit meinem langjährigen Freund Charles Pritchard. Wir entwickeln Darkroom in HTML5 <canvas>. Darkroom ist eine veränderungsfreie Anwendung zum Foto-Sharing und kombiniert leistungsstarke pixelbasierte Filter mit den Optionen für vektorbasierte Typografie und für Zeichnungen.

Einführung

pastedGraphic.png

Mit <canvas> haben JavaScript-Programmierer die volle Kontrolle über Farben, Vektoren und Pixel auf ihren Bildschirmen – also über das Aussehen des Monitors.

In den folgenden Beispielen wird ein bislang wenig bekannter Aspekt von <canvas> besprochen. Es geht um das Erstellen von Texteffekten. Die Vielfalt der mit <canvas> möglichen Texteffekte ist fast unbegrenzt. In diesen Demos sehen Sie nur einen Bruchteil der Möglichkeiten. Obwohl wir uns in diesem Artikel mit Text beschäftigen, lassen sich diese Vorgehensweisen auch auf alle Vektorobjekte anwenden. So erstellen Sie ganz besondere Grafiken für Spiele und andere Anwendungen:

Textschatten in <canvas>
CSS-ähnliche Texteffekte in <canvas> als Ausschneidemasken, Metriken in <canvas> und Verwendung der Schattierungseigenschaft
Neon-Regenbogen, Zebra-Spiegelung – Verkettung von Effekten
Photoshop-ähnliche Texteffekte in <canvas>, Beispiele für die Verwendung von globalCompositeOperation, createLinearGradient, createPattern. 
Innere und äußere Schatten in <canvas>
Erläuterung einer eher unbekannten Funktion: Verwendung von Drehung im bzw. gegen den Uhrzeigersinn zur Umkehrung eines Schlagschattens, d. h. Erstellen eines inneren Schattens
Generative Effekte für den Abstand
Generative Texteffekte in <canvas> mit hsl()-Farbwechsel und window.requestAnimationFrame für den Anschein einer Bewegung

Textschatten in Canvas

Eine der besten neuen Funktionen von CSS3, zu denen auch Border-Radius, Web-Farbverläufe und andere gehören, ist für mich die Möglichkeit zum Erstellen von Schatten. Dabei ist es wichtig, dass man sich die Unterschiede zwischen CSS und <canvas> in Bezug auf Schatten vor Augen führt. Diese lauten:

CSS verwendet zwei Methoden: box-shadow für Feldelemente, wie div, span usw. und text-shadow für Textinhalte.

<canvas> hat einen Schattentyp. Dieser wird für alle Vektorobjekte verwendet: ctx.moveTo, ctx.lineTo, ctx.bezierCurveTo, ctx.quadradicCurveTo, ctx.arc, ctx.rect, ctx.fillText, ctx.strokeText usw. Wenn Sie einen Schatten in <canvas> erstellen möchten, können Sie diese vier Eigenschaften nutzen:

ctx.shadowColor = “red” // string
Schattenfarbe: RGB, RGBA, HSL, HEX und andere Eingaben sind gültig.
ctx.shadowOffsetX = 0; // integer
Horizontaler Abstand des Schattens in Bezug auf den Text
ctx.shadowOffsetY = 0; // integer
Vertikaler Abstand des Schattens in Bezug auf den Text
ctx.shadowBlur = 10; // integer
Weichzeichnen des Schattens, je größer der Wert, umso größer die Weichzeichnung

Sehen wir uns zu Beginn an, wie <canvas> CSS-Effekte emuliert. Wenn man Bilder in Google nach "css text-shadow" durchsucht, findet man ein paar tolle Demos, die wir emulieren können: Line25 und Stereoscopic sowie Shadow 3D.

pastedGraphic_1.png

Der Stereoscopic 3D-Effekt (siehe Anaglyphenbild) ist ein Beispiel für eine einfache Codezeile, die sich vielseitig nutzen lässt. Mit der folgenden CSS-Zeile können Sie eine Illusion von Tiefe schaffen, wenn das Bild mit einer 3D-Brille in Rot/Cyan betrachtet wird. Eine solche Brille erhalten Sie auch im Kino für 3D-Filme.

text-shadow: -0.06em 0 0 red, 0.06em 0 0 cyan;

Beim Umwandeln des Strings in <canvas> sind folgende zwei Dinge zu beachten:

(1) Es gibt keine Weichzeichnung beim Schatten (dritter Wert) und somit auch keinen Grund zum Ausführen von "shadow", da "fillText" zu denselben Ergebnissen führt.

var text = “Hello world!”
ctx.fillStyle = “#000”
ctx.fillText(text, -7, 0);
ctx.fillStyle = “red”
ctx.fillText(text, 0, 0);
ctx.fillStyle = “cyan”
ctx.fillText(text, 7, 0);

(2) EMs werden in <canvas> nicht unterstützt und müssen daher in Pixel umgewandelt werden. Das Umrechnungsverhältnis zwischen PT, PC, EM, EX, PX etc. erhalten Sie, indem Sie ein Element mit denselben Schriftarteigenschaften in DOM erstellen und seine Breite auf das zu messende Format festlegen. Zum Erfassen der EM -> PX-Umwandlung, messen Sie beispielsweise das DOM-Element mit einer "height: 1em". Die resultierende "offsetHeight" zeigt wie viele Pixel sich in jeder EM befinden.

var font = “20px sans-serif”
var d = document.createElement(”span”);
d.style.cssText = “font: “ + font + “ height: 1em; display: block”
// the value to multiply PX’s by to convert to EM’s
var EM2PX = 1 / d.offsetHeight;

Alpha-Multiplikation verhindern

Bei einem komplexeren Beispiel, wie dem Neoneffekt in Line25, muss die Eigenschaft "shadowBlur" verwendet werden, um den Effekt korrekt zu emulieren. Da beim Neoneffekt mehrere Schatten erforderlich sind, haben wir hier ein Problem, denn in <canvas> kann jedes Vektorobjekt nur einen Schatten haben. Damit Sie mehrere Schatten zeichnen können, müssen Sie mehrere Versionen des Texts übereinander erstellen. Das führt zu Alpha-Multiplikation und letztlich zu unregelmäßigen Rändern.

pastedGraphic_2.png

Ich habe versucht ctx.fillStyle = “rgba(0,0,0,0)” oder “transparent” auszuführen, um den Text zu verbergen und gleichzeitig den Schatten anzuzeigen, aber das hat nichts bewirkt. Da der Schatten eine Multiplikation der "fillStyle"-Alpha ist, kann er niemals undurchsichtiger als "fillStyle" sein.

Zum Glück gibt es hierfür eine Lösung. Sie können den Schatten mit einem Abstand vom Text zeichnen, damit beide voneinander getrennt sind. Sie überlappen sich also nicht und der Text kann so seitlich außerhalb des Bildschirms verborgen werden.

var text = “Hello world!”
var blur = 10;
var width = ctx.measureText(text).width + blur * 2;
ctx.textBaseline = “top”
ctx.shadowColor = “#000”
ctx.shadowOffsetX = width;
ctx.shadowOffsetY = 0;
ctx.shadowBlur = blur;
ctx.fillText(text, -width, 0);

Um einen Textabschnitt ausschneiden

Sie können diesen Vorgang ordentlicher gestalten, indem Sie verhindern, dass "fillText" überhaupt gezeichnet wird und dazu einen Ausschneidepfad hinzufügen. Der Schatten wird dennoch gezeichnet. Damit Sie einen Ausschneidepfad um den Text erstellen können, müssen Sie die Breite und die Höhe des Texts kennen. Letztere wird als "em-height" bezeichnet und entsprach früher der Höhe des Buchstabens "M" auf einer Druckpresse. Die Breite erhalten Sie mit ctx.measureText().width, aber ctx.measureText().height ist nicht vorhanden.

Zum Glück lässt sich die Höhe des Texts durch einen "CSS-Hack" herausfinden. Informationen zur Problemlösung bei älteren Bereitstellungen von <canvas>, die CSS-Maße verwenden, erhalten Sie unter Typographic Metrics. Hier finden Sie die Höhe des Texts, indem Sie die offsetHeight eines <span> mit denselben Schriftarteigenschaften messen:

var d = document.createElement(”span”);
d.font = “20px arial”
d.textContent = “Hello world!”
var emHeight = d.offsetHeight;

Von hier aus können Sie ein Rechteck erstellen, das als Ausschneidepfad dient. Es beinhaltet den Schatten und entfernt die unnötige Form.

ctx.rect(0, 0, width, emHeight);
ctx.clip();

Wenn man das nun alles verbindet und noch weiter optimiert, erhält man folgendes Ergebnis: Wenn ein Schatten keine Weichzeichnung hat, kann "fillText" verwendet werden, um denselben Effekt zu erstellen. So muss man keine Ausschneidemaske erstellen.

var width = ctx.measureText(text).width;
var style = shadowStyles[text];
// add a background to the current effect
ctx.fillStyle = style.background;
ctx.fillRect(0, offsetY, ctx.canvas.width, textHeight - 1)
// parse text-shadows from css
var shadows = parseShadow(style.shadow);
// loop through the shadow collection
var n = shadows.length; while(n--) {
    var shadow = shadows[n];
    var totalWidth = width + shadow.blur * 2;
    ctx.save();
    ctx.beginPath();
    ctx.rect(offsetX - shadow.blur, offsetY, offsetX + totalWidth, textHeight);
    ctx.clip();
    if (shadow.blur) { // just run shadow (clip text)
        ctx.shadowColor = shadow.color;
        ctx.shadowOffsetX = shadow.x + totalWidth;
        ctx.shadowOffsetY = shadow.y;
        ctx.shadowBlur = shadow.blur;
        ctx.fillText(text, -totalWidth + offsetX, offsetY + metrics.top);
    } else { // just run pseudo-shadow
        ctx.fillStyle = shadow.color;
        ctx.fillText(text, offsetX + (shadow.x||0), offsetY - (shadow.y||0) + metrics.top);
    }
    ctx.restore();
}
// drawing the text in the foreground
if (style.color) {
    ctx.fillStyle = style.color;
    ctx.fillText(text, offsetX, offsetY + metrics.top);
}
// jump to next em-line
ctx.translate(0, textHeight);
    

Da Sie bestimmt nicht alle diese <canvas>-Befehle von Hand eingeben möchten, habe ich einen einfachen Textschatten-Parser in der Demo-Quelle erstellt. Über diesen können Sie CSS-Befehle übermitteln, die in <canvas>-Befehle umgewandelt werden. Das kann für viele verschiedene <canvas>-Elementstile genutzt werden. Dieselben Schatteneffekte können für alle Vektorobjekte verwendet werden. Dabei ist unerheblich, ob es sich um WebFonts oder um komplexe Formen, die aus SVGs importiert wurden, oder gar um generative Vektorformen usw. handelt.

Anzeigen des Textschatten in <canvas>-Effekts

pastedGraphic_3.png

Kurze Anmerkung (Thema Pixel-Pushing)

Beim Schreiben dieses Artikelabschnitts wurde ich neugierig auf das Stereoscopic-Beispiel. Wie schwierig wäre die Erstellung eines 3D-Filmeffekts mit <canvas> anhand von zwei Bildern, die aus leicht unterschiedlichen Perspektiven aufgenommen wurden? Wohl gar nicht so schwer. Der folgende Kernel kombiniert den roten Kanal des ersten Bilds (data) mit dem Kanal in Cyan des zweiten Bilds (data2).

data[i] = data[i] * 255 / 0xFF;
data[i+1] = 255 * data2[i+1] / 0xFF;
data[i+2] = 255 * data2[i+2] / 0xFF;

Besuchen Sie die Stereoscopic-Demo, in der gezeigt wird, wie Bilder für 3D-Brillen (Cyan/Magenta) erstellt werden können. Das heißt also, man müsste nur zwei iPhones mit Klebeband an der eigenen Stirn befestigen und gleichzeitig auf "Video aufzeichnen" klicken und man würde seinen eigenen 3D-Film in HTML5 erstellen. Gibt es Freiwillige, die das ausprobieren möchten?

pastedGraphic_5.png
3D-Brille

Neon-Regenbogen, Zebra-Spiegelung – Verkettung von Effekten

Die Verkettung mehrerer Effekte in <canvas> ist ganz einfach. Es sind jedoch grundlegende Kenntnisse zu "globalCompositeOperation" (GCO) erforderlich. Wenn man die Operationen mit GIMP (oder Photoshop) vergleicht, ergibt sich Folgendes: Es gibt 12 GCOs in <canvas> "darker" und "lighter" können als Ebenen-Überblendmodi betrachtet werden, die anderen 10 Operationen werden den Ebenen als Alpha-Masken hinzugefügt, d. h. eine Ebene entfernt die Pixel der anderen Ebene. Die globalCompositeOperation verbindet auf neue und besondere Weise Ebenen oder, in unserem Fall, Code-Strings:

pastedGraphic_6.png

Die globalCompositeOperation-Grafik zeigt verwendbare GCO-Modi. Diese Grafik nutzt einen großen Teil des Farbspektrums und mehrere Stufen der Alpha-Transparenz, um detailgenaue Ergebnisse anzuzeigen. Textbeschreibungen erhalten Sie in der globalCompositeOperation-Referenz von Mozilla. Weitere Informationen zur Funktionsweise der Operation erhalten Sie unter Compositing Digital Images von Porter Duff.

Mir persönlich gefällt am besten globalCompositeOperation=”lighter”. Durch "lighter" werden die hinzugefügten Pixel vermischt. Das geschieht ähnlich, wie das Licht gemischt wird. Wenn rotes, grünes und weißes Licht mit voller Intensität strahlen, nimmt es das menschliche Auge als weißes Licht wahr. Mit dieser Funktion können Sie tolle Dinge ausprobieren, vor allem wenn <canvas> auf einen niedrigen globalAlpha-Wert festgelegt ist. So werden eine feinere Abstimmung und sanftere Ränder möglich. "lighter" kann in vielen Situationen verwendet werden. Erst kürzlich fand ich einen beeindruckenden Ersteller für Desktophintergründe in HTML5 unter http://weavesilk.com/. Eine meiner Demos, Breathing Galaxies (JS1k), verwendet ebenfalls den "lighter"-Modus. Anhand dieser beiden Beispiele sehen Sie die mit diesem Modus möglichen Effekte.

Hinweis: Nicht alle Browser unterstützen alle GCO-Modi. Dieses Problem muss noch angegangen werden. Es gibt sechs Modi, die in den Browsern Chrome, Safari, Firefox und Opera funktionieren: source-over, source-atop, destination-over, destination-out, lighter und xor. Hoffentlich wird dieses Problem für künftige Versionen behoben. Weitere Informationen erhalten Sie unter globalCompositeOperation browser handling.

Neon-Regenbogen-Jitter-Effekt

In der folgenden Demo sehen Sie, wie Sie einen Photoshop-ähnlichen Neon-Regenbogen-Schein mit einer Jitter-Umrisslinie erstellen, indem Sie Effekte verketten. Das geschieht mit der globalCompositeOperation ("source-in", "lighter" und "darker"). Diese Demo ist eine Fortsetzung der Demo "Textschatten in <canvas>" und verwendet dieselbe Vorgehensweise, um den Schatten vom Text zu trennen. Wie das geht, wurde im vorigen Abschnitt beschrieben:

pastedGraphic_7.png

Anzeigen des Neon-Regenbogen-Jitter-Effekts

function neonLightEffect() {
  var text = "alert('"+String.fromCharCode(0x2665)+"')";
  var font = "120px Futura, Helvetica, sans-serif";
  var jitter = 25; // the distance of the maximum jitter
  var offsetX = 30;
  var offsetY = 70;
  var blur = getBlurValue(100);
  // save state
  ctx.save();
  ctx.font = font;
  // calculate width + height of text-block
  var metrics = getMetrics(text, font);
  // create clipping mask around text-effect
  ctx.rect(offsetX - blur/2, offsetY - blur/2,
           offsetX + metrics.width + blur, metrics.height + blur);
  ctx.clip();
  // create shadow-blur to mask rainbow onto (since shadowColor doesn't accept gradients)
  ctx.save();
  ctx.fillStyle = "#fff";
  ctx.shadowColor = "rgba(0,0,0,1)";
  ctx.shadowOffsetX = metrics.width + blur;
  ctx.shadowOffsetY = 0;
  ctx.shadowBlur = blur;
  ctx.fillText(text, -metrics.width + offsetX - blur, offsetY + metrics.top);
  ctx.restore();
  // create the rainbow linear-gradient
  var gradient = ctx.createLinearGradient(0, 0, metrics.width, 0);
  gradient.addColorStop(0, "rgba(255, 0, 0, 1)");
  gradient.addColorStop(0.15, "rgba(255, 255, 0, 1)");
  gradient.addColorStop(0.3, "rgba(0, 255, 0, 1)");
  gradient.addColorStop(0.5, "rgba(0, 255, 255, 1)");
  gradient.addColorStop(0.65, "rgba(0, 0, 255, 1)");
  gradient.addColorStop(0.8, "rgba(255, 0, 255, 1)");
  gradient.addColorStop(1, "rgba(255, 0, 0, 1)");
  // change composite so source is applied within the shadow-blur
  ctx.globalCompositeOperation = "source-atop";
  // apply gradient to shadow-blur
  ctx.fillStyle = gradient;
  ctx.fillRect(offsetX - jitter/2, offsetY,
               metrics.width + offsetX, metrics.height + offsetY);
  // change composite to mix as light
  ctx.globalCompositeOperation = "lighter";
  // multiply the layer
  ctx.globalAlpha = 0.7
  ctx.drawImage(ctx.canvas, 0, 0);
  ctx.drawImage(ctx.canvas, 0, 0);
  ctx.globalAlpha = 1
  // draw white-text ontop of glow
  ctx.fillStyle = "rgba(255,255,255,0.95)";
  ctx.fillText(text, offsetX, offsetY + metrics.top);
  // created jittered stroke
  ctx.lineWidth = 0.80;
  ctx.strokeStyle = "rgba(255,255,255,0.25)";
  var i = 10; while(i--) { 
      var left = jitter / 2 - Math.random() * jitter;
      var top = jitter / 2 - Math.random() * jitter;
      ctx.strokeText(text, left + offsetX, top + offsetY + metrics.top);
  }    
  ctx.strokeStyle = "rgba(0,0,0,0.20)";
  ctx.strokeText(text, offsetX, offsetY + metrics.top);
  ctx.restore();
};

Zebra-Spiegelungs-Effekt

Der Zebra-Spiegelungs-Effekt wird in der ausgezeichneten WebDesignerWall gezeigt. Hier wird erläutert, wie Sie Ihre Seite mit CSS aufpeppen. Hier gehen wir noch einen Schritt weiter und erstellen eine Spiegelung für den Text. So etwas sehen Sie beispielsweise in iTunes. Der Effekt kombiniert "fillColor" (weiß), "createPattern" (zebra.png) und "linearGradient" (Schein). Auf diese Weise lassen sich verschiedene Fülltypen auf alle Vektorobjekte anwenden:

pastedGraphic_8.png

Anzeigen des Zebra-Spiegelungs-Effekts

function sleekZebraEffect() {
  // inspired by - http://www.webdesignerwall.com/demo/css-gradient-text/
  var text = "Sleek Zebra...";
  var font = "100px Futura, Helvetica, sans-serif";

  // save state
  ctx.save();
  ctx.font = font;

  // getMetrics calculates:
  // width + height of text-block
  // top + middle + bottom baseline
  var metrics = getMetrics(text, font);
  var offsetRefectionY = -20;
  var offsetY = 70;
  var offsetX = 60;

  // throwing a linear-gradient in to shine up the text
  var gradient = ctx.createLinearGradient(0, offsetY, 0, metrics.height + offsetY);
  gradient.addColorStop(0.1, '#000');
  gradient.addColorStop(0.35, '#fff');
  gradient.addColorStop(0.65, '#fff');
  gradient.addColorStop(1.0, '#000');
  ctx.fillStyle = gradient
  ctx.fillText(text, offsetX, offsetY + metrics.top);

  // draw reflected text
  ctx.save();
  ctx.globalCompositeOperation = "source-over";
  ctx.translate(0, metrics.height + offsetRefectionY)
  ctx.scale(1, -1);
  ctx.font = font;
  ctx.fillStyle = "#fff";
  ctx.fillText(text, offsetX, -metrics.height - offsetY + metrics.top);
  ctx.scale(1, -1);

  // cut the gradient out of the reflected text 
  ctx.globalCompositeOperation = "destination-out";
  var gradient = ctx.createLinearGradient(0, offsetY, 0, metrics.height + offsetY);
  gradient.addColorStop(0.0, 'rgba(0,0,0,0.65)');
  gradient.addColorStop(1.0, '#000');
  ctx.fillStyle = gradient;
  ctx.fillRect(offsetX, offsetY, metrics.width, metrics.height);

  // restore back to original transform state
  ctx.restore();

  // using source-atop to allow the transparent .png to show through to the gradient
  ctx.globalCompositeOperation = "source-atop";

  // creating pattern from <image> sourced.
  ctx.fillStyle = ctx.createPattern(image, 'repeat');

  // fill the height of two em-boxes, to encompass both normal and reflected state
  ctx.fillRect(offsetX, offsetY, metrics.width, metrics.height * 2);
  ctx.restore();
};

Innere/äußere Schatten in Canvas

In den Spezifikationen von <canvas> finden "innere" oder "äußere" Schatten keine Beachtung. Auf den ersten Blick sieht es also so aus, als würde der "innere" Schatten nicht unterstützt. Das ist jedoch nicht der Fall. Es ist nur ein wenig knifflig, diese Option zu aktivieren. ;) Wie in einem Post von F1LT3R erst kürzlich vorgeschlagen, können Sie innere Schatten erstellen, indem Sie die besonderen Eigenschaften der Optionen zum Drehen im und gegen den Uhrzeigersinn verwenden. Dazu erstellen Sie einen inneren Schatten, indem Sie zuerst ein Container-Rechteck und dann mit gegensätzlichen Drehungen eine Ausschneideform zeichnen. So entsteht die Umkehrung der Form.

Im folgenden Beispiel kann der innere Schatten und "fillStyle" mit Farben, Farbverläufen und Mustern gleichzeitig geändert werden. Sie können eine individuelle Musterrotation angeben. Sehen Sie, dass die Zebrastreifen nun im rechten Winkel zueinander verlaufen? Eine Ausschneidemaske in der Größe des begrenzenden Felds wird verwendet. So ist kein riesiger Container erforderlich, der die Ausschneidemaske enthält. Dadurch wird die Geschwindigkeit verbessert, da unnötige Teile des Schattens nicht verarbeitet werden.

pastedGraphic_9.png

Anzeigen des Innerer Schatten-Effekts

function innerShadow() {

  function drawShape() { // draw anti-clockwise
    ctx.arc(0, 0, 100, 0, Math.PI * 2, true); // Outer circle
    ctx.moveTo(70, 0);
    ctx.arc(0, 0, 70, 0, Math.PI, false); // Mouth
    ctx.moveTo(-20, -20);
    ctx.arc(30, -30, 10, 0, Math.PI * 2, false); // Left eye
    ctx.moveTo(140, 70);
    ctx.arc(-20, -30, 10, 0, Math.PI * 2, false); // Right eye
  };

  var width = 200;
  var offset = width + 50;
  var innerColor = "rgba(0,0,0,1)";
  var outerColor = "rgba(0,0,0,1)";

  ctx.translate(150, 170);

  // apply inner-shadow
  ctx.save();
  ctx.fillStyle = "#000";
  ctx.shadowColor = innerColor;
  ctx.shadowBlur = getBlurValue(120);
  ctx.shadowOffsetX = -15;
  ctx.shadowOffsetY = 15;

  // create clipping path (around blur + shape, preventing outer-rect blurring)
  ctx.beginPath();
  ctx.rect(-offset/2, -offset/2, offset, offset);
  ctx.clip();

  // apply inner-shadow (w/ clockwise vs. anti-clockwise cutout)
  ctx.beginPath();
  ctx.rect(-offset/2, -offset/2, offset, offset);
  drawShape();
  ctx.fill();
  ctx.restore();

  // cutout temporary rectangle used to create inner-shadow
  ctx.globalCompositeOperation = "destination-out";
  ctx.fill();

  // prepare vector paths
  ctx.beginPath();
  drawShape();

  // apply fill-gradient to inner-shadow
  ctx.save();
  ctx.globalCompositeOperation = "source-in";
  var gradient = ctx.createLinearGradient(-offset/2, 0, offset/2, 0);
  gradient.addColorStop(0.3, '#ff0');
  gradient.addColorStop(0.7, '#f00');
  ctx.fillStyle = gradient;
  ctx.fill();

  // apply fill-pattern to inner-shadow
  ctx.globalCompositeOperation = "source-atop";
  ctx.globalAlpha = 1;
  ctx.rotate(0.9);
  ctx.fillStyle = ctx.createPattern(image, 'repeat');
  ctx.fill();
  ctx.restore();

  // apply fill-gradient
  ctx.save();
  ctx.globalCompositeOperation = "destination-over";
  var gradient = ctx.createLinearGradient(-offset/2, -offset/2, offset/2, offset/2);
  gradient.addColorStop(0.1, '#f00');
  gradient.addColorStop(0.5, 'rgba(255,255,0,1)');
  gradient.addColorStop(1.0, '#00f');
  ctx.fillStyle = gradient
  ctx.fill();

  // apply fill-pattern
  ctx.globalCompositeOperation = "source-atop";
  ctx.globalAlpha = 0.2;
  ctx.rotate(-0.4);
  ctx.fillStyle = ctx.createPattern(image, 'repeat');
  ctx.fill();
  ctx.restore();

  // apply outer-shadow (color-only without temporary layer)
  ctx.globalCompositeOperation = "destination-over";
  ctx.shadowColor = outerColor;
  ctx.shadowBlur = 40;
  ctx.shadowOffsetX = 15;
  ctx.shadowOffsetY = 10;
  ctx.fillStyle = "#fff";
  ctx.fill();
};

In diesen Beispielen sehen Sie, dass Sie mithilfe von globalCompositeOperation Effekte verketten können. So erzielen Sie anhand von Masken und Überblenden anspruchsvollere Effekte. Am Bildschirm ist alles möglich. ;)

Generative Effekte für den Abstand

Eine Bearbeitung des Unicode-Characters 0x2708 in <canvas> von

pastedGraphic_10.png

...zu diesem schattierten Beispiel

pastedGraphic_11.png

...wird erzielt durch mehrfache Aufrufe von ctx.strokeText() mit einer dünnen "lineWidth" (0,25). Dabei wird langsam "x-offset" und "alpha" verringert. So entsteht beim Vektorelement der Anschein einer Bewegung.

Durch das Zuweisen der XY-Position der Elemente zu einer Sinus-/Cosinus-Welle und durch die zyklische Verarbeitung der Farben anhand der HSL-Eigenschaft lassen sich interessantere Effekte erzielen, z. B. bei diesem "biohazard"-Beispiel.

pastedGraphic_12.png

HSL: Hue, Saturation, Lightness (Farbton, Sättigung, Helligkeit) (1978)

HSL ist ein in den CSS3-Spezifikationen neu unterstütztes Format. HEX wurde für Computer konzipiert; HSL wurde zur Lesbarkeit für den Menschen konzipiert.

Ein Beispiel für die einfache Nutzbarkeit von HSL: Soll das Farbspektrum zyklisch verarbeitet werden, wird der Farbton ("hue") von 360 inkrementiert. Der Farbton wird dem Spektrum in zylindrischer Weise zugewiesen. Die Helligkeit ("lightness") legt fest, wie dunkel/hell die Farbe ist. Dabei steht 0 % für ein schwarzes Pixel und 100 % für ein weißes Pixel. Die Sättigung ("saturation") legt die Dichte oder Intensität einer Farbe fest. Grautöne werden mit einer Sättigung von 0 % erstellt. Intensive Farben verfügen über einen Wert von 100 %.

pastedGraphic_13.png

Bei HSL handelt es sich um einen neuen Standard. Sie möchten jedoch bestimmt weiterhin ältere Browser unterstützen. Das ist über die Farbraumumwandlung möglich. Der folgende Code akzeptiert ein HSL-Objekt { H: 360, S: 100, L: 100} und erstellt ein RGB-Objekt { R: 255, G: 255, B: 255 }. Nun können Sie Ihren RGB- oder RGBA-String anhand dieser Werte erstellen. Weitere Informationen erhalten Sie im aufschlussreichen Wikipedia-Artikel zu HSL.

// HSL (1978) = H: Hue / S: Saturation / L: Lightness
HSL_RGB = function (o) { // { H: 0-360, S: 0-100, L: 0-100 }
  var H = o.H / 360,
      S = o.S / 100,
      L = o.L / 100,
      R, G, B, _1, _2;

  function Hue_2_RGB(v1, v2, vH) {
    if (vH < 0) vH += 1;
    if (vH > 1) vH -= 1;
    if ((6 * vH) < 1) return v1 + (v2 - v1) * 6 * vH;
    if ((2 * vH) < 1) return v2;
    if ((3 * vH) < 2) return v1 + (v2 - v1) * ((2 / 3) - vH) * 6;
    return v1;
  }

  if (S == 0) { // HSL from 0 to 1
    R = L * 255;
    G = L * 255;
    B = L * 255;
  } else {
    if (L < 0.5) {
      _2 = L * (1 + S);
    } else {
      _2 = (L + S) - (S * L);
    }
    _1 = 2 * L - _2;

    R = 255 * Hue_2_RGB(_1, _2, H + (1 / 3));
    G = 255 * Hue_2_RGB(_1, _2, H);
    B = 255 * Hue_2_RGB(_1, _2, H - (1 / 3));
  }

  return {
    R: R,
    G: G,
    B: B
  };
};

Animationen mit "requestAnimationFrame" erstellen

Bisher standen zum Erstellen von Animationen in JavaScript zwei Möglichkeiten zur Auswahl: setTimeout und setInterval.

window.requestAnimationFrame ist hier der neue Standard, um beide zu ersetzen. Sie sparen Strom und schonen Ihren Computer, da der Browser die Animationen je nach den verfügbaren Ressourcen regulieren kann. Einige wichtige Funktionen lauten:

  • Wenn ein Nutzer den Frame verlässt, wird die Animation eventuell langsamer oder stoppt, damit keine unnötigen Ressourcen verbraucht werden.
  • Es gibt eine Obergrenze von 60 fps für die Frame-Rate. Der Grund hierfür ist, dass diese Frame-Rates sowieso außerhalb der menschlichen Wahrnehmung liegen. Die meisten Menschen nehmen eine Animation mit 30 fps als "flüssig" wahr.

Zurzeit sind für die Verwendung von requestAnimationFrame Anbieter-spezifische Präfixe erforderlich. Paul Irish erstellte eine Shim-Ebene, die mehrere Anbieter unterstützt: requestAnimationFrame for smart animating:

// shim layer with setTimeout fallback
window.requestAnimFrame = (function(){
  return  window.requestAnimationFrame       || 
          window.webkitRequestAnimationFrame || 
          window.mozRequestAnimationFrame    || 
          window.oRequestAnimationFrame      || 
          window.msRequestAnimationFrame     || 
          function(/* function */ callback, /* DOMElement */ element){
            window.setTimeout(callback, 1000 / 60);
          };
})();

Wenn man diese Idee noch weiterentwickelt, könnte man dies mit einem "poly-fill" kombinieren, wie mit requestAnimationFrame.js. Hierfür müssten ein paar Funktionen entwickeln werden. Dann könnten ältere Browser beim Wechsel zu diesem neuen Standard weiterhin unterstützt werden.

Das folgende Beispiel zeigt, wie Sie eine Animation erstellen und tausende von Aufrufen an "strokeText" über ein "low alpha" ohne negative Auswirkungen auf den Browser senden. Die Steuerelemente sind ein wenig seltsam, aber die Ergebnisse sind wirklich cool:

Anzeigen des Abstand-Effekts

(function animate() {
  var i = 50;
  while(i--) {
      if (n > endpos) return;

      n += definition;
      ctx.globalAlpha = (0.5 - (n + startpos) / endpos) * alpha;
      if (doColorCycle) {
          hue = n + color;
          ctx.strokeStyle = "hsl(" + (hue % 360) + ",99%,50%)"; // iterate hue
      }
      var x = cos(n / cosdiv) * n * cosmult; // cosine
      var y = sin(n / sindiv) * n * sinmult; // sin
      ctx.strokeText(text, x + xoffset, y + yoffset); // draw rainbow text
  }
  timeout = window.requestAnimationFrame(animate, 0);
})();
pastedGraphic_14.png
pastedGraphic_16.png
pastedGraphic_17.png

Quellcode

Durch die Unterstützung mehrerer Browser bleiben bezüglich der künftigen Entwicklung von <canvas> keine Fragen mehr offen. Für iPhone/Android/Desktop ist eine Portierung mithilfe von PhoneGap oder Titanium möglich.

Der Quellcode befindet sich in CanvasTextEffects.zip

Comments

0