| 1 |
<?php |
| 2 |
/** |
| 3 |
* Copyright (C) 2019-2022 Graham Breach |
| 4 |
* |
| 5 |
* This program is free software: you can redistribute it and/or modify |
| 6 |
* it under the terms of the GNU Lesser General Public License as published by |
| 7 |
* the Free Software Foundation, either version 3 of the License, or |
| 8 |
* (at your option) any later version. |
| 9 |
* |
| 10 |
* This program is distributed in the hope that it will be useful, |
| 11 |
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 12 |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 13 |
* GNU Lesser General Public License for more details. |
| 14 |
* |
| 15 |
* You should have received a copy of the GNU Lesser General Public License |
| 16 |
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 17 |
*/ |
| 18 |
/** |
| 19 |
* For more information, please contact <graham@goat1000.com> |
| 20 |
*/ |
| 21 |
|
| 22 |
namespace Goat1000\SVGGraph; |
| 23 |
|
| 24 |
/** |
| 25 |
* Creates and stores gradients |
| 26 |
*/ |
| 27 |
class GradientList { |
| 28 |
|
| 29 |
private $graph; |
| 30 |
private $gradients = []; |
| 31 |
private $gradient_map = []; |
| 32 |
|
| 33 |
public function __construct(&$graph) |
| 34 |
{ |
| 35 |
$this->graph =& $graph; |
| 36 |
} |
| 37 |
|
| 38 |
/** |
| 39 |
* Adds a gradient to the list, returning the element ID for use in url |
| 40 |
*/ |
| 41 |
public function addGradient($colours, $key = null, $radial = false) |
| 42 |
{ |
| 43 |
if($key === null || !isset($this->gradients[$key])) { |
| 44 |
|
| 45 |
if($radial) { |
| 46 |
// if this is a radial gradient, it must end with 'r' |
| 47 |
$last = count($colours) - 1; |
| 48 |
if(strlen($colours[$last]) == 1) |
| 49 |
$colours[$last] = 'r'; |
| 50 |
else |
| 51 |
$colours[] = 'r'; |
| 52 |
} |
| 53 |
|
| 54 |
// find out if this gradient already stored |
| 55 |
$hash = md5(serialize($colours)); |
| 56 |
if(isset($this->gradient_map[$hash])) |
| 57 |
return $this->gradient_map[$hash]; |
| 58 |
|
| 59 |
$id = $this->graph->newID(); |
| 60 |
if($key === null) |
| 61 |
$key = $id; |
| 62 |
$this->gradients[$key] = ['id' => $id, 'colours' => $colours]; |
| 63 |
$this->gradient_map[$hash] = $id; |
| 64 |
return $id; |
| 65 |
} |
| 66 |
return $this->gradients[$key]['id']; |
| 67 |
} |
| 68 |
|
| 69 |
/** |
| 70 |
* Creates a gradient element |
| 71 |
*/ |
| 72 |
private function makeGradient($key) |
| 73 |
{ |
| 74 |
$stops = ''; |
| 75 |
$direction = 'v'; |
| 76 |
$type = 'linearGradient'; |
| 77 |
$colours = $this->gradients[$key]['colours']; |
| 78 |
$id = $this->gradients[$key]['id']; |
| 79 |
|
| 80 |
if(in_array($colours[count($colours)-1], ['h', 'v', 'r'])) |
| 81 |
$direction = array_pop($colours); |
| 82 |
if($direction == 'r') { |
| 83 |
$type = 'radialGradient'; |
| 84 |
$gradient = ['id' => $id]; |
| 85 |
} else { |
| 86 |
$x2 = $direction == 'v' ? 0 : '100%'; |
| 87 |
$y2 = $direction == 'h' ? 0 : '100%'; |
| 88 |
$gradient = ['id' => $id, 'x1' => 0, 'x2' => $x2, 'y1' => 0, 'y2' => $y2]; |
| 89 |
} |
| 90 |
|
| 91 |
$segments = $this->decompose($colours); |
| 92 |
foreach($segments as $segment) { |
| 93 |
list($offset, $colour, $opacity) = $segment; |
| 94 |
$stop = ['offset' => $offset . '%', 'stop-color' => $colour]; |
| 95 |
if(is_numeric($opacity)) |
| 96 |
$stop['stop-opacity'] = $opacity; |
| 97 |
$stops .= $this->graph->element('stop', $stop); |
| 98 |
} |
| 99 |
|
| 100 |
return $this->graph->element($type, $gradient, null, $stops); |
| 101 |
} |
| 102 |
|
| 103 |
/** |
| 104 |
* Calculates a colour component from a gradient step |
| 105 |
*/ |
| 106 |
private function calcComponent($c0, $c1, $o0, $o1, $d) |
| 107 |
{ |
| 108 |
return $c0 + ($c1 - $c0) * ($d - $o0) / ($o1 - $o0); |
| 109 |
} |
| 110 |
|
| 111 |
/** |
| 112 |
* Returns the colour at a position in the gradient |
| 113 |
*/ |
| 114 |
public function getColour($key, $position) |
| 115 |
{ |
| 116 |
if(!isset($this->gradients[$key])) |
| 117 |
return ''; |
| 118 |
$colours = $this->gradients[$key]['colours']; |
| 119 |
if(in_array($colours[count($colours)-1], ['h', 'v', 'r'])) |
| 120 |
$direction = array_pop($colours); |
| 121 |
$segments = $this->decompose($colours); |
| 122 |
|
| 123 |
// function to produce colour string |
| 124 |
$make_colour = function($r, $g, $b, $o) { |
| 125 |
$str = "rgb({$r},{$g},{$b})"; |
| 126 |
if(!is_numeric($o) || $o >= 1) |
| 127 |
return $str; |
| 128 |
if($o <= 0) |
| 129 |
return 'none'; |
| 130 |
$str .= ':' . $o; |
| 131 |
return $str; |
| 132 |
}; |
| 133 |
|
| 134 |
$position = min(100, max(0, $position)); |
| 135 |
$seg0 = $segments[0]; |
| 136 |
$seg1 = null; |
| 137 |
foreach($segments as $seg) { |
| 138 |
if(abs($seg[0] - $position) < 0.1) { |
| 139 |
// stop colour at or close to requested position |
| 140 |
$c = $seg[1]->rgb(); |
| 141 |
$c_string = $make_colour($c[0], $c[1], $c[2], $seg[2]); |
| 142 |
$colour = new Colour($this->graph, $c_string); |
| 143 |
return $colour; |
| 144 |
} |
| 145 |
|
| 146 |
if($seg[0] > $position) { |
| 147 |
$seg1 = $seg; |
| 148 |
break; |
| 149 |
} |
| 150 |
$seg0 = $seg; |
| 151 |
} |
| 152 |
if($seg1 === null) { |
| 153 |
$seg1 = array_pop($segments); |
| 154 |
$seg1[0] = 100; |
| 155 |
} |
| 156 |
|
| 157 |
$c0 = $seg0[1]->rgb(); |
| 158 |
$c1 = $seg1[1]->rgb(); |
| 159 |
$vr = $this->calcComponent($c0[0], $c1[0], $seg0[0], $seg1[0], $position); |
| 160 |
$vg = $this->calcComponent($c0[1], $c1[1], $seg0[0], $seg1[0], $position); |
| 161 |
$vb = $this->calcComponent($c0[2], $c1[2], $seg0[0], $seg1[0], $position); |
| 162 |
|
| 163 |
$o0 = $seg0[2] === null ? 1 : $seg0[2]; |
| 164 |
$o1 = $seg1[2] === null ? 1 : $seg1[2]; |
| 165 |
$vo = $this->calcComponent($o0, $o1, $seg0[0], $seg1[0], $position); |
| 166 |
$c_string = $make_colour($vr, $vg, $vb, $vo); |
| 167 |
$colour = new Colour($this->graph, $c_string); |
| 168 |
return $colour; |
| 169 |
} |
| 170 |
|
| 171 |
/** |
| 172 |
* Breaks gradient array down into components |
| 173 |
*/ |
| 174 |
public function decompose($colours) |
| 175 |
{ |
| 176 |
$col_mul = 100 / (count($colours) - 1); |
| 177 |
$offset = 0; |
| 178 |
$decomposed = []; |
| 179 |
foreach($colours as $pos => $colour) { |
| 180 |
$opacity = null; |
| 181 |
$poffset = $pos * $col_mul; |
| 182 |
if(strpos($colour, ':') !== false) { |
| 183 |
// opacity, stop offset or both |
| 184 |
$parts = explode(':', $colour); |
| 185 |
if(is_numeric($parts[0]) || count($parts) == 3) { |
| 186 |
$poffset = array_shift($parts); |
| 187 |
} |
| 188 |
// stick the other parts back together and let the Colour class |
| 189 |
// figure it out |
| 190 |
$colour = new Colour($this->graph, implode(':', $parts)); |
| 191 |
$opacity = $colour->opacity(); |
| 192 |
if($opacity == 1) |
| 193 |
$opacity = null; |
| 194 |
} else { |
| 195 |
$colour = new Colour($this->graph, $colour); |
| 196 |
} |
| 197 |
// set the offset to the most meaningful number |
| 198 |
$offset = min(100, max(0, $offset, $poffset)); |
| 199 |
$decomposed[] = [$offset, $colour, $opacity]; |
| 200 |
} |
| 201 |
return $decomposed; |
| 202 |
} |
| 203 |
|
| 204 |
/** |
| 205 |
* Defines the list of gradients |
| 206 |
*/ |
| 207 |
public function makeGradients(&$defs) |
| 208 |
{ |
| 209 |
foreach($this->gradients as $key => $gradient) |
| 210 |
$defs->add($this->makeGradient($key)); |
| 211 |
} |
| 212 |
} |
| 213 |
|
| 214 |
|