Canvas JavaScript – und der Entwickler sprach: „Es werde Licht“

Es ist schön, wenn einem mal ein Licht auf geht. Ein schöner Moment.

Hier mal ein Canvas-Beispiel, wie man – auch wenn es dunkel ist – seine Webseite noch sehen kann. Mit Hilfe einer (JavaScript/Canvas) – Taschenlampe.

Wo liegen die Probleme, für eine „Taschenlampe“?

Überlegungen zur optische Realisierung

Wir brauchen einen „hellen“ Kreis mit einem weichen Rand.  Aber was wir vorher noch brauchen, ist „die Nacht“ – also es muss dunkel werden.

Das erreicht man am einfachsten, in dem man eine Schwarze Fläche über die ganze Webseite legt. Aber diese Fläche braucht „ein Loch“ mit weichem Rand.

Man könnte ja eine PNG-Grafik nehmen, welches riesig groß ist und in der Mitte ein „Loch“ hat – in Form einer Transparenz. Da es aber mit dem Mauszeiger mitgehen soll, müsste diese Grafik sehr sehr groß sein – irgendwie suboptimal.

Da bietet sich ein Canvas an. Dabei müssen die Breite und Höhe auf 100% gesetzt werden, und die Position auf fixed. Bei einem Canvas kann man einen transparenten Kreis definieren, der nach außen hin schwarz wird.

Stolpersteine

Ein großes Problem ergibt sich aus dem überlagertem Element, welches alle Clicks von der Maus abfängt. Somit sind Links auf der Webseite zunächst nicht anklickbar.

Man könnte jetzt versuchen mit JavaScript die Mausposition zu lokalisieren und  diese irgendwie an die unterliegenden Elemente weiter zu leiten.  Das ist bestimmt eine nette Übung.

Viel einfacher geht es mit der nicht so bekannten css – Eigenschaft:

pointer-events: none;

Man muss aber zugeben, dass es nicht komplett funktioniert. Z.B. werden einige Hover-Effekte und rechte MaustastenEvents nicht richtig ausgewertet.
Aber die Links funktionieren.

Den interessanten Teil (das JavaScript, welches das Canvas steuert) setze ich hier mal als Beispiel rein. Das hier benutze JavaScript hat dazu kleine Abweichungen (weil z.B. der Button und ein TimeOut die Steuerung übernehmen).

Bei mir ist jQuery auf der Webseite vorhanden – somit nutze ich auch die Funktionen davon:


$(function () {
    function MouseCanvas() {
        'use strict';
        var cuLayer                 = document.getElementById("cuFlashlight");
        cuLayer.style.pointerEvents = "none";
        this.cuLayer                = cuLayer;
        this.radius                 = 300;
        this.$body                  = $("body");
        this.relativeCanvasPosition = {left: 0, top: 0};
    }

    MouseCanvas.prototype.moveCanvas   = function () {
        'use strict';
        var ctx = this.cuLayer.getContext("2d");
        if (ctx) {
            this.ctx            = ctx;
            var widthLayer      = document.documentElement.clientWidth;
            var heightLayer     = document.documentElement.clientHeight;
            var radius          = this.radius;
            var radiusInner     = radius / 3;
            this.cuLayer.width  = widthLayer;
            this.cuLayer.height = heightLayer;
            this.ctx.rect(0, 0, this.cuLayer.width, this.cuLayer.height);
            var grd = this.ctx.createRadialGradient(this.relativeCanvasPosition.left,
                                                    this.relativeCanvasPosition.top,
                                                    radiusInner,
                                                    this.relativeCanvasPosition.left,
                                                    this.relativeCanvasPosition.top,
                                                    radius);
            this.ctx.clearRect(0, 0, this.cuLayer.width, this.cuLayer.height);
            grd.addColorStop(0, "rgba(55,55,0,0)");
            grd.addColorStop(1, "rgba(0,0,0,0.96)");
            this.ctx.fillStyle = grd;
            this.ctx.fill();
        }
        else {
            console.log("Sorry, no canvas-context");
        }
    };
    MouseCanvas.prototype.startFollow  = function () {
        var mouseCanvas = this;
        this.$body.on("mousemove", function (mouse) {
            var scrollLeft                          = $(document).scrollLeft();
            var scrollTop                           = $(document).scrollTop();
            scrollLeft                              = scrollLeft === undefined ? 0 : scrollLeft;
            scrollTop                               = scrollTop === undefined ? 0 : scrollTop;
            mouseCanvas.relativeCanvasPosition.left = mouse.pageX - scrollLeft;
            mouseCanvas.relativeCanvasPosition.top  = mouse.pageY - scrollTop;
            mouseCanvas.moveCanvas();
        });
        this.changeRadius();
    };
    MouseCanvas.prototype.changeRadius = function () {
        var mouseCanvas = this;
        this.$body[0].addEventListener("keyup", function (event) {
            var keyCode = event.keyCode;
            var isAlt   = event.altKey;
            if (isAlt) {
                mouseCanvas.radius = 109 === keyCode ? mouseCanvas.radius - 50 : mouseCanvas.radius;
                mouseCanvas.radius = 107 === keyCode ? mouseCanvas.radius + 50 : mouseCanvas.radius;
                mouseCanvas.radius = 0 >= mouseCanvas.radius ? mouseCanvas.radius = 50 : mouseCanvas.radius;
                mouseCanvas.moveCanvas();
            }
        });
    };
    $("body")
        .append(
            '<canvas id="cuFlashlight" style="border: 1px solid grey; position: fixed;top: 0; left: 0;z-index: 1000;pointer-events : none;display: none;"></canvas>');
    var mouseCanvas = new MouseCanvas();
    var mouseX      = 100;
    var mouseY      = 100;
    mouseCanvas.moveCanvas(mouseX, mouseY);
    mouseCanvas.startFollow();
});

Viel Spass damit!

PHP Enum – einfach mal ganz schwierig

Variablen können in einigen Programmiersprachen vom Type ENUM sein. In PHP leider nicht.

Die SPLEnum-Klasse lasse ich mal weg

Wenn eine Variable vom Type ENUM ist, dann kann Sie vorher definierte Werte annehmen – und nur diese. Z.B. könnte man so Farben definieren. Mein Auto gibt es nur in Schwarz, Gelb und Grün. Somit muss ich beim Programmieren immer darauf achten, dass meine Variable „$myCarColor“ nur diese Werte annehmen kann. Wäre sie als ENUM definiert, so würde PHP automatisch dafür sorgen, das dort nichts anders drin stehen kann.

Das gibt es aber (noch) nicht so richtig in PHP. Also müssen wir uns selbst etwas überlegen.

Die einfachste Möglichkeit ist, eine Art „Liste“ bereit zu stellen, die die entsprechenden Werte bereit stellt, und diese in Form von Klassenkonstanten zu verpacken:


/**
 * Class EColor
 */
class EColor {

	const BLACK  = 1;
	const YELLOW = 2;
	const GREEN  = 4;

}

// Usage:

setColorExampleFunction( EColor::BLACK ); // EColor::Black = 1

Im Beispiel wird der Wert 1 (für die Farbe BLACK stehend) an eine beliebige Funktion übergeben. Das ist zwar ein Schritt in die richtige Richtung, da man sich nicht die Zahlen merken muss, sondern einfach die Variable nehmen kann, aber es gibt noch (mindestens) ein Problem: Die Funktion erwartet eine Variable vom Type INTEGER – keine Farbe. Ich könnte auch den Wert 3 oder 5 übergeben – das wäre aber keine Farbe (weil der Wert nicht existiert). Gut wäre, wenn wir einen TypeHint in der Funktion angeben könnten und somit sicherstellen können, das wir das bekommen, was wir wollen.

Folgendes Beispiel geht da ein paar Schritte weiter:


/**
 * Class MyColor_reflection
 */
class MyColor {

	const BLACK  = 1;
	const YELLOW = 2;
	const GREEN  = 4;

	private $currentColor;
	private $colors = [];

	/**
	 * MyColor constructor.
	 */
	public function __construct() {

		$this->setColors();

		$this->setBlack();
	}

	public function setBlack() {

		$this->setSomeColor( 'BLACK', self::BLACK );
	}

	public function setYellow() {

		$this->setSomeColor( 'YELLOW', self::YELLOW );
	}

	public function setGreen() {

		$this->setSomeColor( 'GREEN', self::GREEN );
	}

	public function getCurrentColor() {

		return $this->currentColor;

	}


	/**
	 * @param string $name
	 * @param int    $value
	 */
	protected function setSomeColor( $name, $value ) {

		$this->currentColor          = [];
		$this->currentColor[ $name ] = $value;

	}

	protected function setColors() {

		$reflectionClass = new ReflectionClass( $this );
		$constants       = $reflectionClass->getConstants();

		foreach ( $constants as $key => $value ) {
			$this->colors[ $key ] = $value;
		}

	}

}

// Usage:
$myColor = new MyColor();
$myColor->setYellow();

$currentColor = $myColor->getCurrentColor();

$currentColorName  = key( $currentColor );          // YELLOW
$currentColorValue = current( $currentColor );      // 2


function setColorWithHintExampleFunction(MyColor $color){
	// Some code ...
}

setColorWithHintReflectionExampleFunction( $myColor );

Jetzt können wir sicher sein, dass der Funktion wirklich nur definierte Werte übergeben werden. Außerdem kann man sowohl die Farbe entweder als Zahl oder als String bekommen.

ABER:

Das ist viel Schreibarbeit und auch an mindestens einer Stelle doppelt zu warten (nicht wirklich DRY):

Konstanten werden einmal oberhalb des Konstruktors definiert. Der gleiche Konstantenname muss aber auch in der entsprechenden Methode vorhanden sein. Man könnte das noch etwas verbessern, aber das Grundproblem würde bleiben: es ist nicht DRY.

Problematischer ist, dass der umgekehrte Weg (aus einer Zahl oder einem String diese Klasse erstellen) schwierig ist.

Es sollte eine Möglichkeit geben, aus entweder String oder Zahl die Klasse mit entsprechenden Wert zurück zu bekommen.

Das ist dann aber ein Thema fürs nächste mal.