<?php

define("PIXEL_RED",                 (int) pow(2,0));
define("PIXEL_GREEN",               (int) pow(2,1));
define("PIXEL_BLUE",                (int) pow(2,2));
define("PIXEL_ALPHA",               (int) pow(2,3));
define("PIXEL_ALTERNATE",           (int) pow(2,4));
define("PIXEL_ADD",                 (int) pow(2,5));
define("PIXEL_SUB",                 (int) pow(2,6));
define("PIXEL_OR",                  (int) pow(2,7));
define("PIXEL_AND",                 (int) pow(2,8));
define("PIXEL_XOR",                 (int) pow(2,9));
//define("PIXEL_FLAG",              (int) pow(2,10));
//define("PIXEL_FLAG",              (int) pow(2,11));
//define("PIXEL_FLAG",              (int) pow(2,12));
// ...
define("PIXEL_NOISE",               (int) pow(2,31));

class Pixel {
    public $r; public $g; public $b; public $a = /* opaque */255;
    public $flags = /* none */ 0; public $bias  = /* full */ 64;
    public function __construct($r, $g, $b, $alpha = NULL, $flags = 0, $bias = 0) {
        $this->r = $this->limit($r);
        $this->g = $this->limit($g);
        $this->b = $this->limit($b);
        // alpha tends to be represented [0.0 .. 1.0] rather than [0..255]
        $alpha = NULL === $alpha ?1.0 :$alpha;
        $this->setAlpha($alpha);
        $this->flags = $flags;
        // bias is up to 75% with 0.75 being "full" or "most" bias
        $this->setBias($bias);
        // process immediate flags
        Pixel::processFlags($this);
    }

    static public function processFlags(Pixel $p) {
        if(PIXEL_NOISE & $p->flags) { $p->rgbaWalk(array($p, "noise")); }
        if(PIXEL_ADD   & $p->flags) $p->binFn(function($x,$y) { return $x+$y; });
        if(PIXEL_SUB   & $p->flags) $p->binFn(function($x,$y) { return $x-$y; });
        if(PIXEL_OR    & $p->flags) $p->binFn(function($x,$y) { return $x|$y; });
        if(PIXEL_AND   & $p->flags) $p->binFn(function($x,$y) { return $x&$y; });
        if(PIXEL_XOR   & $p->flags) $p->binFn(function($x,$y) { return $x^$y; });
    }

    private function binFn($fn) {
        $b = $this->bias;
        $this->rgbaWalk(function($x) use ($b, $fn) { return $fn($x,$b); });
    }
    private function rgbaWalk($callable) {
        $arr = array(
            PIXEL_RED => &$this->r,
            PIXEL_GREEN => &$this->g,
            PIXEL_BLUE => &$this->b,
            PIXEL_ALPHA => &$this->a
        );
        foreach($arr as $k=>$v) {
            $arr[$k] = ($k & $this->flags) ?call_user_func($callable, $v) :$v;
            $arr[$k] = $this->limit($arr[$k]);
        }
    }
    private function noise($v) {
        return Pixel::limit(abs($v + ((rand(0,1) ?-1 :1) * rand(0,$this->bias))));
    }
    static private function limit($x, $limit = 255) {
        return $limit > ($x = (int) round($x)) ?$x : $limit;
    }
    // alpha [0..1] where zero is transparent, and one is opaque
    public function setAlpha($f) { $this->a = $this->limit($f * 255); }
    // bias [0..0.50] where zero is none, and one half is max
    public function setBias($f) { $this->bias = $this->limit($f * 255, 192); }
    // value is a float [0..1]
    static function mix(Pixel $p1, Pixel $p2, $v1 = 0.5, $v2 = 0.5, $opts = array()) {
        $r = round(($p1->r * $v1) + ($p2->r * $v2));
        $g = round(($p1->g * $v1) + ($p2->g * $v2));
        $b = round(($p1->b * $v1) + ($p2->b * $v2));
        $a = round(($p1->a * $v1) + ($p2->a * $v2));
        // generate new pixel passing whatever options static methods provided
        if(isset($opts["flags"]) and isset($opts["bias"])) {
            return new Pixel($r, $g, $b, (float) $a/255, $opts["flags"], $opts["bias"]);
        } else if(isset($opts["flags"])) {
            return new Pixel($r, $g, $b, (float) $a/255, $opts["flags"]);
        } else {
            return new Pixel($r, $g, $b, (float) $a/255);
        }
    }
    static function generatePixelLine(Pixel $p1, Pixel $p2, $opts = array()) {
        // provide default total pixels, "t"
        $t = isset($opts["total"]) ?$opts["total"] :3;
        // first and second pixel are already provided
        $t -= 2;
        $arr = array(clone $p1);
        $toggleable = array();
        // check for alternating flag, toggle existing binary functions 
        if(isset($opts["flags"]) and (PIXEL_ALTERNATE & $opts["flags"])) {
            $binaryFunctions = array(
                PIXEL_ADD,
                PIXEL_SUB,
                PIXEL_OR,
                PIXEL_XOR,
                PIXEL_AND
            );
            foreach($binaryFunctions as $v) {
                // alternate should occur immediately (second element)
                if($v & $opts["flags"]) { $toggleable[$v] = true; $opts["flags"] ^= $v; }
            }
        }
        for($i=$t;$i--;) {
            // toggle the binary operations
            foreach($toggleable as $k=>$doesntMatter) { $opts["flags"] ^= $k; }
            $arr[] = Pixel::mix($p1, $p2, ($i+1)/($t+1), ($t-$i)/($t+1), $opts);
        }
        $arr[] = clone $p2;
        return $arr;
    }
    public function __tostring() {
        return sprintf("%02x%02x%02x%02x", $this->r, $this->g, $this->b, $this->a);
    }
}
