Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402]

   1  <?php namespace RedeyeVentures\GeoPattern;
   2  
   3  use RedeyeVentures\GeoPattern\SVGElements\Polyline;
   4  use RedeyeVentures\GeoPattern\SVGElements\Rectangle;
   5  use RedeyeVentures\GeoPattern\SVGElements\Group;
   6  
   7  class GeoPattern {
   8  
   9      protected $string;
  10      protected $baseColor;
  11      protected $color;
  12      protected $generator;
  13  
  14      protected $hash;
  15      protected $svg;
  16  
  17      protected $patterns = [
  18          'octogons',
  19          'overlapping_circles',
  20          'plus_signs',
  21          'xes',
  22          'sine_waves',
  23          'hexagons',
  24          'overlapping_rings',
  25          'plaid',
  26          'triangles',
  27          'squares',
  28          'concentric_circles',
  29          'diamonds',
  30          'tessellation',
  31          'nested_squares',
  32          'mosaic_squares',
  33          'triangles_rotated',
  34          'chevrons',
  35      ];
  36      const FILL_COLOR_DARK = '#222';
  37      const FILL_COLOR_LIGHT = '#ddd';
  38      const STROKE_COLOR = '#000';
  39      const STROKE_OPACITY = '0.02';
  40      const OPACITY_MIN = '0.02';
  41      const OPACITY_MAX = '0.15';
  42  
  43      function __construct($options=array())
  44      {
  45          // Set string if provided. If not, set default.
  46          if (isset($options['string'])) {
  47              $this->setString($options['string']);
  48          } else {
  49              $this->setString(time());
  50          }
  51  
  52          // Set base color if provided. If not, set default.
  53          if (isset($options['baseColor'])) {
  54              $this->setBaseColor($options['baseColor']);
  55          } else {
  56              $this->setBaseColor('#933c3c');
  57          }
  58  
  59          // Set color if provided.
  60          if (isset($options['color'])) {
  61              $this->setColor($options['color']);
  62          }
  63  
  64          // Set generator if provided. If not, leave null.
  65          if (isset($options['generator']))
  66              $this->setGenerator($options['generator']);
  67  
  68          $this->svg = new SVG();
  69      }
  70  
  71      // Fluent Interfaces
  72      public function setString($string)
  73      {
  74          $this->string = $string;
  75          $this->hash = sha1($this->string);
  76          return $this;
  77      }
  78  
  79      /**
  80       * @return string
  81       */
  82      public function getString()
  83      {
  84          return $this->string;
  85      }
  86  
  87      public function setBaseColor($baseColor)
  88      {
  89          if(preg_match('/^#[a-f0-9]{6}$/i', $baseColor)) //hex color is valid
  90          {
  91              $this->baseColor = $baseColor;
  92              return $this;
  93          }
  94          throw new \InvalidArgumentException("$baseColor is not a valid hex color.");
  95      }
  96  
  97      public function setColor($color)
  98      {
  99          if(preg_match('/^#[a-f0-9]{6}$/i', $color)) //hex color is valid
 100          {
 101              $this->color = $color;
 102              return $this;
 103          }
 104          throw new \InvalidArgumentException("$color is not a valid hex color.");
 105      }
 106  
 107      public function setGenerator($generator)
 108      {
 109          $generator = strtolower($generator);
 110          if (in_array($generator, $this->patterns) || is_null($generator)) {
 111              $this->generator = $generator;
 112              return $this;
 113          }
 114          throw new \InvalidArgumentException("$generator is not a valid generator type.");
 115      }
 116  
 117      public function toSVG()
 118      {
 119          $this->svg = new SVG();
 120          $this->generateBackground();
 121          $this->generatePattern();
 122          return (string) $this->svg;
 123      }
 124  
 125      public function toBase64()
 126      {
 127          return base64_encode($this->toSVG());
 128      }
 129  
 130      public function toDataURI()
 131      {
 132          return "data:image/svg+xml;base64,{$this->toBase64()}";
 133      }
 134  
 135      public function toDataURL()
 136      {
 137          return "url(\"{$this->toDataURI()}\")";
 138      }
 139  
 140      public function __toString() {
 141          return $this->toSVG();
 142      }
 143  
 144      // Generators
 145      protected function generateBackground()
 146      {
 147          $hueOffset = $this->map($this->hexVal(14, 3), 0, 4095, 0, 359);
 148          $satOffset = $this->hexVal(17, 1);
 149          $baseColor = $this->hexToHSL($this->baseColor);
 150          $color     = $this->color;
 151  
 152          $baseColor['h'] = $baseColor['h'] - $hueOffset;
 153  
 154  
 155          if ($satOffset % 2 == 0)
 156              $baseColor['s'] = $baseColor['s'] + $satOffset/100;
 157          else
 158              $baseColor['s'] = $baseColor['s'] - $satOffset/100;
 159  
 160          if (isset($color))
 161              $rgb = $this->hexToRGB($color);
 162          else
 163              $rgb = $this->hslToRGB($baseColor['h'], $baseColor['s'], $baseColor['l']);
 164  
 165          $this->svg->addRectangle(0, 0, "100%", "100%", ['fill' => "rgb({$rgb['r']}, {$rgb['g']}, {$rgb['b']})"]);
 166      }
 167  
 168      protected function generatePattern()
 169      {
 170          if (is_null($this->generator))
 171              $pattern = $this->patterns[$this->hexVal(20, 1)];
 172          else
 173              $pattern = $this->generator;
 174  
 175          $function = 'geo'.str_replace(' ', '', ucwords(str_replace('_', ' ', $pattern)));
 176  
 177          if (method_exists($this, $function))
 178              $this->$function();
 179      }
 180  
 181      // Pattern Makers
 182      protected function geoHexagons()
 183      {
 184          $scale = $this->hexVal(0, 1);
 185          $sideLength = $this->map($scale, 0, 15, 8, 60);
 186          $hexHeight = $sideLength * sqrt(3);
 187          $hexWidth = $sideLength * 2;
 188          $hex = $this->buildHexagonShape($sideLength);
 189          $this->svg->setWidth(($hexWidth * 3) + ($sideLength * 3))
 190              ->setHeight($hexHeight * 6);
 191  
 192          $i = 0;
 193          for ($y = 0; $y <= 5; $y++) {
 194              for ($x = 0; $x <= 5; $x++) {
 195                  $val = $this->hexVal($i, 1);
 196                  $dy = ($x % 2 == 0) ? ($y * $hexHeight) : ($y*$hexHeight + $hexHeight / 2);
 197                  $opacity = $this->opacity($val);
 198                  $fill = $this->fillColor($val);
 199                  $styles = [
 200                      'stroke' => self::STROKE_COLOR,
 201                      'stroke-opacity' => self::STROKE_OPACITY,
 202                      'fill-opacity' => $opacity,
 203                      'fill' => $fill,
 204                  ];
 205  
 206                  $onePointFiveXSideLengthMinusHalfHexWidth = $x * $sideLength * 1.5 - $hexWidth / 2;
 207                  $dyMinusHalfHexHeight = $dy - $hexHeight / 2;
 208                  $this->svg->addPolyline($hex, array_merge($styles, ['transform' => "translate($onePointFiveXSideLengthMinusHalfHexWidth, $dyMinusHalfHexHeight)"]));
 209  
 210                  // Add an extra one at top-right, for tiling.
 211                  if ($x == 0) {
 212                      $onePointFiveSideLengthSixMinusHalfHexWidth = 6 * $sideLength * 1.5 - $hexWidth / 2;
 213                      $this->svg->addPolyline($hex, array_merge($styles, ['transform' => "translate($onePointFiveSideLengthSixMinusHalfHexWidth, $dyMinusHalfHexHeight)"]));
 214                  }
 215  
 216                  // Add an extra row at the end that matches the first row, for tiling.
 217                  if ($y == 0) {
 218                      $dy2 = ($x % 2 == 0) ? (6 * $hexHeight) : (6 * $hexHeight + $hexHeight / 2);
 219                      $dy2MinusHalfHexHeight = $dy2 - $hexHeight / 2;
 220                      $this->svg->addPolyline($hex, array_merge($styles, ['transform' => "translate($onePointFiveXSideLengthMinusHalfHexWidth, $dy2MinusHalfHexHeight)"]));
 221                  }
 222  
 223                  // Add an extra one at bottom-right, for tiling.
 224                  if ($x == 0 && $y == 0) {
 225                      $onePointFiveSideLengthSixMinusHalfHexWidth = 6 * $sideLength * 1.5 - $hexWidth / 2;
 226                      $fiveHexHeightPlusHalfHexHeight = 5 * $hexHeight + $hexHeight / 2;
 227                      $this->svg->addPolyline($hex, array_merge($styles, ['transform' => "translate($onePointFiveSideLengthSixMinusHalfHexWidth, $fiveHexHeightPlusHalfHexHeight)"]));
 228                  }
 229  
 230                  $i++;
 231              }
 232          }
 233      }
 234  
 235      protected function geoSineWaves()
 236      {
 237          $period = floor($this->map($this->hexVal(0, 1), 0, 15, 100, 400));
 238          $quarterPeriod = $period / 4;
 239          $xOffset = $period / 4 * 0.7;
 240          $amplitude = floor($this->map($this->hexVal(1, 1), 0, 15, 30, 100));
 241          $waveWidth = floor($this->map($this->hexVal(2, 1), 0, 15, 3, 30));
 242          $amplitudeString = number_format($amplitude);
 243          $halfPeriod = number_format($period / 2);
 244          $halfPeriodMinusXOffset = number_format($period / 2 - $xOffset);
 245          $periodMinusXOffset = number_format($period - $xOffset);
 246          $twoAmplitude = number_format(2 * $amplitude);
 247          $onePointFivePeriodMinusXOffset = number_format($period * 1.5 - $xOffset);
 248          $onePointFivePeriod = number_format($period * 1.5);
 249          $str = "M0 $amplitudeString C $xOffset 0, $halfPeriodMinusXOffset 0, $halfPeriod $amplitudeString S $periodMinusXOffset $twoAmplitude, $period $amplitudeString S $onePointFivePeriodMinusXOffset 0, $onePointFivePeriod, $amplitudeString";
 250  
 251          $this->svg->setWidth($period)
 252              ->setHeight($waveWidth*36);
 253          for ($i = 0; $i <= 35; $i++) {
 254              $val = $this->hexVal($i, 1);
 255              $opacity = $this->opacity($val);
 256              $fill = $this->fillColor($val);
 257              $styles = [
 258                  'fill' => 'none',
 259                  'stroke' => $fill,
 260                  'style' => [
 261                      'opacity' => $opacity,
 262                      'stroke-width' => "{$waveWidth}px"
 263                  ]
 264              ];
 265  
 266              $iWaveWidthMinusOnePointFiveAmplitude = $waveWidth * $i - $amplitude * 1.5;
 267              $iWaveWidthMinusOnePointFiveAmplitudePlusThirtySixWaveWidth = $waveWidth * $i - $amplitude * 1.5 + $waveWidth * 36;
 268              $this->svg->addPath($str, array_merge($styles, ['transform' => "translate(-$quarterPeriod, $iWaveWidthMinusOnePointFiveAmplitude)"]));
 269              $this->svg->addPath($str, array_merge($styles, ['transform' => "translate(-$quarterPeriod, $iWaveWidthMinusOnePointFiveAmplitudePlusThirtySixWaveWidth)"]));
 270  
 271          }
 272      }
 273  
 274      protected function geoChevrons()
 275      {
 276          $chevronWidth = $this->map($this->hexVal(0, 1), 0, 15, 30, 80);
 277          $chevronHeight = $this->map($this->hexVal(0, 1), 0, 15, 30, 80);
 278          $chevron = $this->buildChevronShape($chevronWidth, $chevronHeight);
 279  
 280          $this->svg->setWidth($chevronWidth*6)
 281              ->setHeight($chevronHeight*6*0.66);
 282  
 283          $i = 0;
 284          for ($y = 0; $y <= 5; $y++) {
 285              for ($x = 0; $x <= 5; $x++) {
 286                  $val = $this->hexVal($i, 1);
 287                  $opacity = $this->opacity($val);
 288                  $fill = $this->fillColor($val);
 289                  $styles = [
 290                      'stroke' => self::STROKE_COLOR,
 291                      'stroke-opacity' => self::STROKE_OPACITY,
 292                      'stroke-width' => '1',
 293                      'fill-opacity' => $opacity,
 294                      'fill' => $fill,
 295                  ];
 296  
 297                  $group = new Group();
 298                  $group->addItem($chevron[0])
 299                      ->addItem($chevron[1]);
 300  
 301                  $xChevronWidth = $x * $chevronWidth;
 302                  $yPointSixSixChevronHeightMinusHalfChevronHeight = $y * $chevronHeight * 0.66 - $chevronHeight / 2;
 303                  $this->svg->addGroup($group, array_merge($styles, ['transform' => "translate($xChevronWidth,$yPointSixSixChevronHeightMinusHalfChevronHeight)"]));
 304                  // Add an extra row at the end that matches the first row, for tiling.
 305                  if ($y == 0) {
 306                      $sixPointSixSixChevronHeightMinusHalfChevronHeight = 6 * $chevronHeight * 0.66 - $chevronHeight / 2;
 307                      $this->svg->addGroup($group, array_merge($styles, ['transform' => "translate($xChevronWidth,$sixPointSixSixChevronHeightMinusHalfChevronHeight)"]));
 308                  }
 309  
 310                  $i++;
 311              }
 312          }
 313  
 314      }
 315  
 316      protected function geoPlusSigns()
 317      {
 318          $squareSize = $this->map($this->hexVal(0, 1), 0, 15, 10, 25);
 319          $plusSize = $squareSize * 3;
 320          $plusShape = $this->buildPlusShape($squareSize);
 321  
 322          $this->svg->setWidth($squareSize*12)
 323              ->setHeight($squareSize*12);
 324  
 325          $i = 0;
 326          for ($y = 0; $y <= 5; $y++) {
 327              for ($x = 0; $x <= 5; $x++) {
 328                  $val = $this->hexVal($i, 1);
 329                  $opacity = $this->opacity($val);
 330                  $fill = $this->fillColor($val);
 331                  $dx = ($y % 2 == 0) ? 0 : 1;
 332  
 333                  $styles = [
 334                      'fill' => $fill,
 335                      'stroke' => self::STROKE_COLOR,
 336                      'stroke-opacity' => self::STROKE_OPACITY,
 337                      'style' => [
 338                          'fill-opacity' => $opacity,
 339                      ],
 340                  ];
 341  
 342                  $group = new Group();
 343                  $group->addItem($plusShape[0])
 344                      ->addItem($plusShape[1]);
 345  
 346                  $t1 = $x * $plusSize - $x * $squareSize + $dx * $squareSize - $squareSize;
 347                  $t2 = $y * $plusSize - $y * $squareSize - $plusSize / 2;
 348  
 349                  $this->svg->addGroup($group, array_merge($styles, ['transform' => "translate($t1, $t2)"]));
 350  
 351                  // Add an extra column on the right for tiling.
 352                  if ($x == 0) {
 353                      $xT1 = 4 * $plusSize - $x * $squareSize + $dx * $squareSize - $squareSize;
 354                      $xT2 = $y * $plusSize - $y * $squareSize - $plusSize / 2;
 355                      $this->svg->addGroup($group, array_merge($styles, ['transform' => "translate($xT1, $xT2)"]));
 356                  }
 357  
 358                  // Add an extra row on the bottom that matches the first row, for tiling.
 359                  if ($y == 0) {
 360                      $yT1 = $x * $plusSize - $x * $squareSize + $dx * $squareSize - $squareSize;
 361                      $yT2 = 4 * $plusSize - $y * $squareSize - $plusSize /2;
 362                      $this->svg->addGroup($group, array_merge($styles, ['transform' => "translate($yT1, $yT2)"]));
 363                  }
 364  
 365                  // Add an extra one at top-right and bottom-right, for tiling.
 366                  if ($x == 0 && $y == 0) {
 367                      $xyT1 = 4 * $plusSize - $x * $squareSize + $dx * $squareSize - $squareSize;
 368                      $xyT2 = 4 * $plusSize - $y * $squareSize - $plusSize / 2;
 369                      $this->svg->addGroup($group, array_merge($styles, ['transform' => "translate($xyT1, $xyT2)"]));
 370                  }
 371  
 372                  $i++;
 373              }
 374          }
 375      }
 376  
 377      protected function geoXes()
 378      {
 379          $squareSize = $this->map($this->hexVal(0, 1), 0, 15, 10, 25);
 380          $xSize = $squareSize * 3 * 0.943;
 381          $xShape = $this->buildPlusShape($squareSize);
 382  
 383          $this->svg->setWidth($xSize*3)
 384              ->setHeight($xSize*3);
 385  
 386          $i = 0;
 387          for ($y = 0; $y <= 5; $y++) {
 388              for ($x = 0; $x <= 5; $x++) {
 389                  $val = $this->hexVal($i, 1);
 390                  $opacity = $this->opacity($val);
 391                  $fill = $this->fillColor($val);
 392                  $dy = ($x % 2 == 0) ? ($y * $xSize - $xSize * 0.5) : ($y * $xSize - $xSize * 0.5 + $xSize / 4);
 393  
 394                  $styles = [
 395                      'fill' => $fill,
 396                      'style' => [
 397                          'opacity' => $opacity,
 398                      ],
 399                  ];
 400  
 401                  $group = new Group();
 402                  $group->addItem($xShape[0])
 403                      ->addItem($xShape[1]);
 404  
 405                  $t1 = $x * $xSize / 2 - $xSize / 2;
 406                  $t2 = $dy - $y * $xSize / 2;
 407                  $halfXSize = $xSize / 2;
 408                  $this->svg->addGroup($group, array_merge($styles, ['transform' => "translate($t1, $t2) rotate(45, $halfXSize, $halfXSize)"]));
 409  
 410                  // Add an extra column on the right for tiling.
 411                  if ($x == 0) {
 412                      $xT1 = 6 * $xSize / 2 - $xSize / 2;
 413                      $xT2 = $dy - $y * $xSize / 2;
 414                      $this->svg->addGroup($group, array_merge($styles, ['transform' => "translate($xT1, $xT2) rotate(45, $halfXSize, $halfXSize)"]));
 415                  }
 416  
 417                  // Add an extra row on the bottom that matches the first row, for tiling.
 418                  if ($y == 0) {
 419                      $dy = ($x % 2 == 0) ? (6 * $xSize - $xSize / 2) : (6 * $xSize - $xSize / 2 + $xSize / 4);
 420                      $yT1 = $x * $xSize / 2 - $xSize / 2;
 421                      $yT2 = $dy - 6 * $xSize / 2;
 422                      $this->svg->addGroup($group, array_merge($styles, ['transform' => "translate($yT1, $yT2) rotate(45, $halfXSize, $halfXSize)"]));
 423                  }
 424  
 425                  // These can hang off the bottom, so put a row at the top for tiling.
 426                  if ($y == 5) {
 427                      $y2T1 = $x * $xSize / 2 - $xSize / 2;
 428                      $y2T2 = $dy - 11 * $xSize / 2;
 429                      $this->svg->addGroup($group, array_merge($styles, ['transform' => "translate($y2T1, $y2T2) rotate(45, $halfXSize, $halfXSize)"]));
 430                  }
 431  
 432                  // Add an extra one at top-right and bottom-right, for tiling.
 433                  if ($x == 0 && $y == 0) {
 434                      $xyT1 = 6 * $xSize / 2 - $xSize / 2;
 435                      $xyT2 = $dy - 6 * $xSize / 2;
 436                      $this->svg->addGroup($group, array_merge($styles, ['transform' => "translate($xyT1, $xyT2) rotate(45, $halfXSize, $halfXSize)"]));
 437                  }
 438  
 439                  $i++;
 440              }
 441          }
 442      }
 443  
 444      protected function geoOverlappingCircles()
 445      {
 446          $scale = $this->hexVal(0, 1);
 447          $diameter = $this->map($scale, 0, 15, 25, 200);
 448          $radius = $diameter/2;
 449  
 450          $this->svg->setWidth($radius*6)
 451              ->setHeight($radius*6);
 452  
 453          $i = 0;
 454          for ($y = 0; $y <= 5; $y++) {
 455              for ($x = 0; $x <= 5; $x++) {
 456                  $val = $this->hexVal($i, 1);
 457                  $opacity = $this->opacity($val);
 458                  $fill = $this->fillColor($val);
 459                  $styles = [
 460                      'fill' => $fill,
 461                      'style' => [
 462                          'opacity' => $opacity,
 463                      ],
 464                  ];
 465  
 466                  $this->svg->addCircle($x*$radius, $y*$radius, $radius, $styles);
 467  
 468                  // Add an extra one at top-right, for tiling.
 469                  if ($x == 0)
 470                      $this->svg->addCircle(6*$radius, $y*$radius, $radius, $styles);
 471  
 472                  // Add an extra row at the end that matches the first row, for tiling.
 473                  if ($y == 0)
 474                      $this->svg->addCircle($x*$radius, 6*$radius, $radius, $styles);
 475  
 476                  // Add an extra one at bottom-right, for tiling.
 477                  if ($x == 0 && $y == 0)
 478                      $this->svg->addCircle(6*$radius, 6*$radius, $radius, $styles);
 479  
 480                  $i++;
 481              }
 482          }
 483      }
 484  
 485      protected function geoOctogons()
 486      {
 487          $squareSize = $this->map($this->hexVal(0, 1), 0, 15, 10, 60);
 488          $tile = $this->buildOctogonShape($squareSize);
 489  
 490          $this->svg->setWidth($squareSize*6)
 491              ->setHeight($squareSize*6);
 492  
 493          $i = 0;
 494          for ($y = 0; $y <= 5; $y++) {
 495              for ($x = 0; $x <= 5; $x++) {
 496                  $val = $this->hexVal($i, 1);
 497                  $opacity = $this->opacity($val);
 498                  $fill = $this->fillColor($val);
 499  
 500                  $xSquareSize = $x * $squareSize;
 501                  $ySquareSize = $y * $squareSize;
 502  
 503                  $this->svg->addPolyline($tile, [
 504                      'fill' => $fill,
 505                      'fill-opacity' => $opacity,
 506                      'stroke' => self::STROKE_COLOR,
 507                      'stroke-opacity' => self::STROKE_OPACITY,
 508                      'transform' => "translate($xSquareSize, $ySquareSize)",
 509                  ]);
 510  
 511                  $i++;
 512              }
 513          }
 514  
 515      }
 516  
 517      protected function geoSquares()
 518      {
 519          $squareSize = $this->map($this->hexVal(0, 1), 0, 15, 10, 60);
 520  
 521          $this->svg->setWidth($squareSize*6)
 522              ->setHeight($squareSize*6);
 523  
 524          $i = 0;
 525          for ($y = 0; $y <= 5; $y++) {
 526              for ($x = 0; $x <= 5; $x++) {
 527                  $val = $this->hexVal($i, 1);
 528                  $opacity = $this->opacity($val);
 529                  $fill = $this->fillColor($val);
 530  
 531                  $this->svg->addRectangle($x*$squareSize, $y*$squareSize, $squareSize, $squareSize, [
 532                      'fill' => $fill,
 533                      'fill-opacity' => $opacity,
 534                      'stroke' => self::STROKE_COLOR,
 535                      'stroke-opacity' => self::STROKE_OPACITY,
 536                  ]);
 537  
 538                  $i++;
 539              }
 540          }
 541  
 542      }
 543  
 544      protected function geoConcentricCircles()
 545      {
 546          $scale = $this->hexVal(0, 1);
 547          $ringSize = $this->map($scale, 0, 15, 10, 60);
 548          $strokeWidth = $ringSize / 5;
 549  
 550          $this->svg->setWidth(($ringSize + $strokeWidth)*6)
 551              ->setHeight(($ringSize + $strokeWidth)*6);
 552  
 553          $i = 0;
 554          for ($y = 0; $y <= 5; $y++) {
 555              for ($x = 0; $x <= 5; $x++) {
 556                  $val = $this->hexVal($i, 1);
 557                  $opacity = $this->opacity($val);
 558                  $fill = $this->fillColor($val);
 559  
 560                  $cx = $x * $ringSize + $x * $strokeWidth + ($ringSize + $strokeWidth) / 2;
 561                  $cy = $y * $ringSize + $y * $strokeWidth + ($ringSize + $strokeWidth) / 2;
 562                  $halfRingSize = $ringSize / 2;
 563  
 564                  $this->svg->addCircle($cx, $cy, $halfRingSize, [
 565                      'fill' => 'none',
 566                      'stroke' => $fill,
 567                      'style' => [
 568                          'opacity' => $opacity,
 569                          'stroke-width' => "{$strokeWidth}px",
 570                      ],
 571                  ]);
 572  
 573                  $val = $this->hexVal(39-$i, 1);
 574                  $opacity = $this->opacity($val);
 575                  $fill = $this->fillColor($val);
 576  
 577                  $quarterRingSize = $ringSize / 4;
 578  
 579                  $this->svg->addCircle($cx, $cy, $quarterRingSize, [
 580                      'fill' => $fill,
 581                      'fill-opacity' => $opacity,
 582                  ]);
 583  
 584                  $i++;
 585              }
 586          }
 587      }
 588  
 589      protected function geoOverlappingRings()
 590      {
 591          $scale = $this->hexVal(0, 1);
 592          $ringSize = $this->map($scale, 0, 15, 10, 60);
 593          $strokeWidth = $ringSize / 4;
 594  
 595          $this->svg->setWidth($ringSize*6)
 596              ->setHeight($ringSize*6);
 597  
 598          $i = 0;
 599          for ($y = 0; $y <= 5; $y++) {
 600              for ($x = 0; $x <= 5; $x++) {
 601                  $val = $this->hexVal($i, 1);
 602                  $opacity = $this->opacity($val);
 603                  $fill = $this->fillColor($val);
 604  
 605                  $styles = [
 606                      'fill' => 'none',
 607                      'stroke' => $fill,
 608                      'style' => [
 609                          'opacity' => $opacity,
 610                          'stroke-width' => "{$strokeWidth}px",
 611                      ],
 612                  ];
 613  
 614                  $ringSizeMinusHalfStrokeWidth = $ringSize - $strokeWidth / 2;
 615  
 616                  $this->svg->addCircle($x*$ringSize, $y*$ringSize, $ringSizeMinusHalfStrokeWidth, $styles);
 617  
 618                  // Add an extra one at top-right, for tiling.
 619                  if ($x == 0)
 620                      $this->svg->addCircle(6*$ringSize, $y*$ringSize, $ringSizeMinusHalfStrokeWidth, $styles);
 621  
 622                  // Add an extra row at the end that matches the first row, for tiling.
 623                  if ($y == 0)
 624                      $this->svg->addCircle($x*$ringSize, 6*$ringSize, $ringSizeMinusHalfStrokeWidth, $styles);
 625  
 626                  // Add an extra one at bottom-right, for tiling.
 627                  if ($x == 0 && $y == 0)
 628                      $this->svg->addCircle(6*$ringSize, 6*$ringSize, $ringSizeMinusHalfStrokeWidth, $styles);
 629  
 630                  $i++;
 631              }
 632          }
 633      }
 634  
 635      protected function geoTriangles()
 636      {
 637          $scale = $this->hexVal(0, 1);
 638          $sideLength = $this->map($scale, 0 ,15, 15, 80);
 639          $triangleHeight = $sideLength / 2 * sqrt(3);
 640          $triangle = $this->buildTriangleShape($sideLength, $triangleHeight);
 641  
 642          $this->svg->setWidth($sideLength * 3)
 643              ->setHeight($triangleHeight * 6);
 644  
 645          $i = 0;
 646          for ($y = 0; $y <= 5; $y++) {
 647              for ($x = 0; $x <= 5; $x++) {
 648                  $val = $this->hexVal($i, 1);
 649                  $opacity = $this->opacity($val);
 650                  $fill = $this->fillColor($val);
 651  
 652                  $styles = [
 653                      'fill' => $fill,
 654                      'fill-opacity' => $opacity,
 655                      'stroke' => self::STROKE_COLOR,
 656                      'stroke-opacity' => self::STROKE_OPACITY,
 657                  ];
 658  
 659                  $rotation = '';
 660                  if ($y % 2 == 0)
 661                      $rotation = ($x % 2 == 0) ? 180 : 0;
 662                  else
 663                      $rotation = ($x % 2 != 0) ? 180 : 0;
 664  
 665                  $halfSideLength = $sideLength / 2;
 666                  $halfTriangleHeight = $triangleHeight / 2;
 667                  $yTriangleHeight = $triangleHeight * $y;
 668  
 669                  $t1 = $x * $sideLength * 0.5 - $sideLength / 2;
 670                  $this->svg->addPolyline($triangle, array_merge($styles, ['transform' => "translate($t1, $yTriangleHeight) rotate($rotation, $halfSideLength, $halfTriangleHeight)"]));
 671  
 672                  // Add an extra one at top-right, for tiling.
 673                  if ($x == 0)
 674                  {
 675                      $xT1 = 6 * $sideLength * 0.5 - $sideLength / 2;
 676                      $this->svg->addPolyline($triangle, array_merge($styles, ['transform' => "translate($xT1, $yTriangleHeight) rotate($rotation, $halfSideLength, $halfTriangleHeight)"]));
 677                  }
 678  
 679                  $i++;
 680              }
 681          }
 682  
 683      }
 684  
 685      protected function geoTrianglesRotated()
 686      {
 687          $scale = $this->hexVal(0, 1);
 688          $sideLength = $this->map($scale, 0 ,15, 15, 80);
 689          $triangleWidth = $sideLength / 2 * sqrt(3);
 690          $triangle = $this->buildRotatedTriangleShape($sideLength, $triangleWidth);
 691  
 692          $this->svg->setWidth($triangleWidth * 6)
 693              ->setHeight($sideLength * 3);
 694  
 695          $i = 0;
 696          for ($y = 0; $y <= 5; $y++) {
 697              for ($x = 0; $x <= 5; $x++) {
 698                  $val = $this->hexVal($i, 1);
 699                  $opacity = $this->opacity($val);
 700                  $fill = $this->fillColor($val);
 701  
 702                  $styles = [
 703                      'fill' => $fill,
 704                      'fill-opacity' => $opacity,
 705                      'stroke' => self::STROKE_COLOR,
 706                      'stroke-opacity' => self::STROKE_OPACITY,
 707                  ];
 708  
 709                  $rotation = '';
 710                  if ($y % 2 == 0)
 711                      $rotation = ($x % 2 == 0) ? 180 : 0;
 712                  else
 713                      $rotation = ($x % 2 != 0) ? 180 : 0;
 714  
 715                  $halfSideLength = $sideLength / 2;
 716                  $halfTriangleWidth = $triangleWidth / 2;
 717                  $xTriangleWidth = $x * $triangleWidth;
 718  
 719                  $t1 = $y * $sideLength * 0.5 - $sideLength / 2;
 720                  $this->svg->addPolyline($triangle, array_merge($styles, ['transform' => "translate($xTriangleWidth, $t1) rotate($rotation, $halfTriangleWidth, $halfSideLength)"]));
 721  
 722                  // Add an extra one at top-right, for tiling.
 723                  if ($y == 0)
 724                  {
 725                      $yT1 = 6 * $sideLength * 0.5 - $sideLength / 2;
 726                      $this->svg->addPolyline($triangle, array_merge($styles, ['transform' => "translate($xTriangleWidth, $yT1) rotate($rotation, $halfTriangleWidth, $halfSideLength)"]));
 727                  }
 728  
 729                  $i++;
 730              }
 731          }
 732  
 733      }
 734  
 735      protected function geoDiamonds()
 736      {
 737          $diamondWidth = $this->map($this->hexVal(0, 1), 0, 15, 10, 50);
 738          $diamondHeight = $this->map($this->hexVal(1, 1), 0, 15, 10, 50);
 739          $diamond = $this->buildDiamondShape($diamondWidth, $diamondHeight);
 740  
 741          $this->svg->setWidth($diamondWidth*6)
 742              ->setHeight($diamondHeight*3);
 743  
 744          $i = 0;
 745          for ($y = 0; $y <= 5; $y++) {
 746              for ($x = 0; $x <= 5; $x++) {
 747                  $val = $this->hexVal($i, 1);
 748                  $opacity = $this->opacity($val);
 749                  $fill = $this->fillColor($val);
 750  
 751                  $styles = [
 752                      'fill' => $fill,
 753                      'fill-opacity' => $opacity,
 754                      'stroke' => self::STROKE_COLOR,
 755                      'stroke-opacity' => self::STROKE_OPACITY,
 756                  ];
 757  
 758                  $dx = ($y % 2 == 0) ? 0 : ($diamondWidth / 2);
 759  
 760                  $t1 = $x * $diamondWidth - $diamondWidth / 2 + $dx;
 761                  $t2 = $diamondHeight / 2 * $y - $diamondHeight / 2;
 762                  $this->svg->addPolyline($diamond, array_merge($styles, ['transform' => "translate($t1, $t2)"]));
 763  
 764                  // Add an extra one at top-right, for tiling.
 765                  if ($x == 0)
 766                  {
 767                      $xT1 = 6 * $diamondWidth - $diamondWidth / 2 + $dx;
 768                      $xT2 = $diamondHeight / 2 * $y - $diamondHeight / 2;
 769                      $this->svg->addPolyline($diamond, array_merge($styles, ['transform' => "translate($xT1, $xT2)"]));
 770                  }
 771  
 772                  // Add an extra row at the end that matches the first row, for tiling.
 773                  if ($y == 0)
 774                  {
 775                      $yT1 = $x * $diamondWidth - $diamondWidth / 2 + $dx;
 776                      $yT2 = $diamondHeight / 2 * 6 - $diamondHeight / 2;
 777                      $this->svg->addPolyline($diamond, array_merge($styles, ['transform' => "translate($yT1, $yT2)"]));
 778                  }
 779  
 780                  // Add an extra one at bottom-right, for tiling.
 781                  if ($x == 0 && $y == 0)
 782                  {
 783                      $xyT1 = 6 * $diamondWidth - $diamondWidth / 2 + $dx;
 784                      $xyT2 = $diamondHeight / 2 * 6 - $diamondHeight / 2;
 785                      $this->svg->addPolyline($diamond, array_merge($styles, ['transform' => "translate($xyT1, $xyT2)"]));
 786                  }
 787  
 788                  $i++;
 789              }
 790          }
 791      }
 792  
 793      protected function geoNestedSquares()
 794      {
 795          $blockSize = $this->map($this->hexVal(0, 1), 0, 15, 4, 12);
 796          $squareSize = $blockSize * 7;
 797          $dimension = ($squareSize + $blockSize) * 6 + $blockSize * 6;
 798  
 799          $this->svg->setWidth($dimension)
 800              ->setHeight($dimension);
 801  
 802          $i = 0;
 803          for ($y = 0; $y <= 5; $y++) {
 804              for ($x = 0; $x <= 5; $x++) {
 805                  $val = $this->hexVal($i, 1);
 806                  $opacity = $this->opacity($val);
 807                  $fill = $this->fillColor($val);
 808  
 809                  $styles = [
 810                      'fill' => 'none',
 811                      'stroke' => $fill,
 812                      'style' => [
 813                          'opacity' => $opacity,
 814                          'stroke-width' => "{$blockSize}px",
 815                      ],
 816                  ];
 817  
 818                  $rX = $x * $squareSize + $x * $blockSize * 2 + $blockSize / 2;
 819                  $rY = $y * $squareSize + $y * $blockSize * 2 + $blockSize / 2;
 820  
 821                  $this->svg->addRectangle($rX, $rY, $squareSize, $squareSize, $styles);
 822  
 823                  $val = $this->hexVal(39-$i, 1);
 824                  $opacity = $this->opacity($val);
 825                  $fill = $this->fillColor($val);
 826  
 827                  $styles = [
 828                      'fill' => 'none',
 829                      'stroke' => $fill,
 830                      'style' => [
 831                          'opacity' => $opacity,
 832                          'stroke-width' => "{$blockSize}px",
 833                      ],
 834                  ];
 835  
 836                  $rX2 = $x * $squareSize + $x * $blockSize * 2 + $blockSize / 2 + $blockSize * 2;
 837                  $rY2 = $y * $squareSize + $y * $blockSize * 2 + $blockSize / 2 + $blockSize * 2;
 838  
 839                  $this->svg->addRectangle($rX2, $rY2, $blockSize * 3, $blockSize * 3, $styles);
 840  
 841                  $i++;
 842              }
 843          }
 844      }
 845  
 846      protected function geoMosaicSquares()
 847      {
 848          $triangleSize = $this->map($this->hexVal(0, 1), 0, 15, 15, 50);
 849  
 850          $this->svg->setWidth($triangleSize*8)
 851              ->setHeight($triangleSize*8);
 852  
 853          $i = 0;
 854          for ($y = 0; $y <= 3; $y++) {
 855              for ($x = 0; $x <= 3; $x++) {
 856                  if ($x % 2 == 0)
 857                  {
 858                      if ($y % 2 == 0)
 859                          $this->drawOuterMosaicTile($x*$triangleSize*2, $y*$triangleSize*2, $triangleSize, $this->hexVal($i, 1));
 860                      else
 861                          $this->drawInnerMosaicTile($x*$triangleSize*2, $y*$triangleSize*2, $triangleSize, [$this->hexVal($i, 1), $this->hexVal($i+1, 1)]);
 862                  }
 863                  else
 864                  {
 865                      if ($y % 2 == 0)
 866                          $this->drawInnerMosaicTile($x*$triangleSize*2, $y*$triangleSize*2, $triangleSize, [$this->hexVal($i, 1), $this->hexVal($i+1, 1)]);
 867                      else
 868                          $this->drawOuterMosaicTile($x*$triangleSize*2, $y*$triangleSize*2, $triangleSize, $this->hexVal($i, 1));
 869                  }
 870                  $i++;
 871              }
 872          }
 873  
 874      }
 875  
 876      protected function geoPlaid()
 877      {
 878          $height = 0;
 879          $width = 0;
 880  
 881          // Horizontal Stripes
 882          $i = 0;
 883          $times = 0;
 884          while ($times++ <= 18)
 885          {
 886              $space = $this->hexVal($i, 1);
 887              $height += $space + 5;
 888  
 889              $val = $this->hexVal($i+1, 1);
 890              $opacity = $this->opacity($val);
 891              $fill = $this->fillColor($val);
 892              $stripeHeight = $val + 5;
 893  
 894              $this->svg->addRectangle(0, $height, "100%", $stripeHeight, [
 895                  'opacity' => $opacity,
 896                  'fill' => $fill,
 897              ]);
 898              $height += $stripeHeight;
 899              $i += 2;
 900          }
 901  
 902          // Vertical Stripes
 903          $i = 0;
 904          $times = 0;
 905          while ($times++ <= 18)
 906          {
 907              $space = $this->hexVal($i, 1);
 908              $width += $space + 5;
 909  
 910              $val = $this->hexVal($i+1, 1);
 911              $opacity = $this->opacity($val);
 912              $fill = $this->fillColor($val);
 913              $stripeWidth = $val + 5;
 914  
 915              $this->svg->addRectangle($width, 0, $stripeWidth, "100%", [
 916                  'opacity' => $opacity,
 917                  'fill' => $fill,
 918              ]);
 919              $width += $stripeWidth;
 920              $i += 2;
 921          }
 922  
 923          $this->svg->setWidth($width)
 924              ->setHeight($height);
 925  
 926      }
 927  
 928      protected function geoTessellation()
 929      {
 930          $sideLength = $this->map($this->hexVal(0, 1), 0, 15, 5, 40);
 931          $hexHeight = $sideLength * sqrt(3);
 932          $hexWidth = $sideLength * 2;
 933          $triangleHeight = $sideLength / 2 * sqrt(3);
 934          $triangle = $this->buildRotatedTriangleShape($sideLength, $triangleHeight);
 935          $tileWidth = $sideLength * 3 + $triangleHeight * 2;
 936          $tileHeight = ($hexHeight * 2) + ($sideLength * 2);
 937  
 938          $this->svg->setWidth($tileWidth)
 939              ->setHeight($tileHeight);
 940  
 941          // Doing these variables up here, so we only have to calculate them once.
 942          $halfSideLength = $sideLength / 2;
 943          $negativeHalfSideLength = -$sideLength / 2;
 944          $halfTriangleHeight = $triangleHeight / 2;
 945          $halfHexHeight = $hexHeight / 2;
 946          $tileHeightPlusHalfSideLength = $tileHeight + $sideLength / 2;
 947          $halfTileHeightMinusHalfSideLength = $tileHeight / 2 - $sideLength / 2;
 948          $halfTileWidthPlusHalfSideLength = $tileWidth / 2 + $sideLength / 2;
 949          $tileWidthMinusHalfTileWidthMinusHalfSideLength = $tileWidth - $tileWidth/2 - $sideLength/2;
 950          $tileWidthMinusHalfSideLength = $tileWidth - $sideLength / 2;
 951          $tileHeightMinusHalfHexHeight = $tileHeight - $hexHeight / 2;
 952          $negativeTileWidthPlusHalfSideLength = -$tileWidth + $sideLength / 2;
 953          $halfTileHeightMinusHalfSideLengthMinusSideLength = $tileHeight/2-$sideLength/2-$sideLength;
 954          $negativeTileHeightPlusHalfTileHeightMinusHalfSideLengthMinusSideLength = -$tileHeight+$tileHeight/2-$sideLength/2-$sideLength;
 955          $negativeTileHeightPlusHalfSideLength = -$tileHeight + $sideLength / 2;
 956          for ($i = 0; $i <= 19; $i++) {
 957              $val = $this->hexVal($i, 1);
 958              $opacity = $this->opacity($val);
 959              $fill = $this->fillColor($val);
 960  
 961              $styles = [
 962                  'stroke' => self::STROKE_COLOR,
 963                  'stroke-opacity' => self::STROKE_OPACITY,
 964                  'fill' => $fill,
 965                  'fill-opacity' => $opacity,
 966                  'stroke-width' => 1,
 967              ];
 968  
 969              switch ($i) {
 970                  case 0: # all 4 corners
 971                      $this->svg->addRectangle(-$sideLength/2, -$sideLength/2, $sideLength, $sideLength, $styles);
 972                      $this->svg->addRectangle($tileWidth-$sideLength/2, -$sideLength/2, $sideLength, $sideLength, $styles);
 973                      $this->svg->addRectangle(-$sideLength/2, $tileHeight-$sideLength/2, $sideLength, $sideLength, $styles);
 974                      $this->svg->addRectangle($tileWidth-$sideLength/2, $tileHeight-$sideLength/2, $sideLength, $sideLength, $styles);
 975                      break;
 976                  case 1: # center / top square
 977                      $this->svg->addRectangle($hexWidth/2+$triangleHeight, $hexHeight/2, $sideLength, $sideLength, $styles);
 978                      break;
 979                  case 2: # side squares
 980                      $this->svg->addRectangle(-$sideLength/2, $tileHeight/2-$sideLength/2, $sideLength, $sideLength, $styles);
 981                      $this->svg->addRectangle($tileWidth-$sideLength/2, $tileHeight/2-$sideLength/2, $sideLength, $sideLength, $styles);
 982                      break;
 983                  case 3: # center / bottom square
 984                      $this->svg->addRectangle($hexWidth/2+$triangleHeight, $hexHeight*1.5+$sideLength, $sideLength, $sideLength, $styles);
 985                      break;
 986                  case 4: # left top / bottom triangle
 987                      $this->svg->addPolyline($triangle, array_merge($styles, ['transform' => "translate($halfSideLength, $negativeHalfSideLength) rotate(0, $halfSideLength, $halfTriangleHeight)"]));
 988                      $this->svg->addPolyline($triangle, array_merge($styles, ['transform' => "translate($halfSideLength, $tileHeightPlusHalfSideLength) rotate(0, $halfSideLength, $halfTriangleHeight) scale(1, -1)"]));
 989                      break;
 990                  case 5: # right top / bottom triangle
 991                      $this->svg->addPolyline($triangle, array_merge($styles, ['transform' => "translate($tileWidthMinusHalfSideLength, $negativeHalfSideLength) rotate(0, $halfSideLength, $halfTriangleHeight) scale(-1, 1)"]));
 992                      $this->svg->addPolyline($triangle, array_merge($styles, ['transform' => "translate($tileWidthMinusHalfSideLength, $tileHeightPlusHalfSideLength) rotate(0, $halfSideLength, $halfTriangleHeight) scale(-1, -1)"]));
 993                      break;
 994                  case 6: # center / top / right triangle
 995                      $this->svg->addPolyline($triangle, array_merge($styles, ['transform' => "translate($halfTileWidthPlusHalfSideLength, $halfHexHeight)"]));
 996                      break;
 997                  case 7: # center / top / left triangle
 998                      $this->svg->addPolyline($triangle, array_merge($styles, ['transform' => "translate($tileWidthMinusHalfTileWidthMinusHalfSideLength, $halfHexHeight) scale(-1, 1)"]));
 999                      break;
1000                  case 8: # center / bottom / right triangle
1001                      $this->svg->addPolyline($triangle, array_merge($styles, ['transform' => "translate($halfTileWidthPlusHalfSideLength, $tileHeightMinusHalfHexHeight) scale(1, -1)"]));
1002                      break;
1003                  case 9: # center / bottom / left triangle
1004                      $this->svg->addPolyline($triangle, array_merge($styles, ['transform' => "translate($tileWidthMinusHalfTileWidthMinusHalfSideLength, $tileHeightMinusHalfHexHeight) scale(-1, -1)"]));
1005                      break;
1006                  case 10: # left / middle triangle
1007                      $this->svg->addPolyline($triangle, array_merge($styles, ['transform' => "translate($halfSideLength, $halfTileHeightMinusHalfSideLength)"]));
1008                      break;
1009                  case 11: # right / middle triangle
1010                      $this->svg->addPolyline($triangle, array_merge($styles, ['transform' => "translate($tileWidthMinusHalfSideLength, $halfTileHeightMinusHalfSideLength) scale(-1, 1)"]));
1011                      break;
1012                  case 12: # left / top square
1013                      $this->svg->addRectangle(0, 0, $sideLength, $sideLength, array_merge($styles, ['transform' => "translate($halfSideLength, $halfSideLength) rotate(-30, 0, 0)"]));
1014                      break;
1015                  case 13: # right / top square
1016                      $this->svg->addRectangle(0, 0, $sideLength, $sideLength, array_merge($styles, ['transform' => "scale(-1, 1) translate($negativeTileWidthPlusHalfSideLength, $halfSideLength) rotate(-30, 0, 0)"]));
1017                      break;
1018                  case 14: # left / center-top square
1019                      $this->svg->addRectangle(0, 0, $sideLength, $sideLength, array_merge($styles, ['transform' => "translate($halfSideLength, $halfTileHeightMinusHalfSideLengthMinusSideLength) rotate(30, 0, $sideLength)"]));
1020                      break;
1021                  case 15: # right / center-top square
1022                      $this->svg->addRectangle(0, 0, $sideLength, $sideLength, array_merge($styles, ['transform' => "scale(-1, 1) translate($negativeTileWidthPlusHalfSideLength, $halfTileHeightMinusHalfSideLengthMinusSideLength) rotate(30, 0, $sideLength)"]));
1023                      break;
1024                  case 16: # left / center-top square
1025                      $this->svg->addRectangle(0, 0, $sideLength, $sideLength, array_merge($styles, ['transform' => "scale(1, -1) translate($halfSideLength, $negativeTileHeightPlusHalfTileHeightMinusHalfSideLengthMinusSideLength) rotate(30, 0, $sideLength)"]));
1026                      break;
1027                  case 17: # right / center-bottom square
1028                      $this->svg->addRectangle(0, 0, $sideLength, $sideLength, array_merge($styles, ['transform' => "scale(-1, -1) translate($negativeTileWidthPlusHalfSideLength, $negativeTileHeightPlusHalfTileHeightMinusHalfSideLengthMinusSideLength) rotate(30, 0, $sideLength)"]));
1029                      break;
1030                  case 18: # left / bottom square
1031                      $this->svg->addRectangle(0, 0, $sideLength, $sideLength, array_merge($styles, ['transform' => "scale(1, -1) translate($halfSideLength, $negativeTileHeightPlusHalfSideLength) rotate(-30, 0, 0)"]));
1032                      break;
1033                  case 19: # right / bottom square
1034                      $this->svg->addRectangle(0, 0, $sideLength, $sideLength, array_merge($styles, ['transform' => "scale(-1, -1) translate($negativeTileWidthPlusHalfSideLength, $negativeTileHeightPlusHalfSideLength) rotate(-30, 0, 0)"]));
1035                      break;
1036              }
1037          }
1038      }
1039  
1040      // build* functions
1041      protected function buildChevronShape($width, $height)
1042      {
1043          $e = $height * 0.66;
1044          $halfWidth = $width / 2;
1045          $heightMinusE = $height - $e;
1046          return [
1047              new Polyline("0,0,$halfWidth,$heightMinusE,$halfWidth,$height,0,$e,0,0"),
1048              new Polyline("$halfWidth,$heightMinusE,$width,0,$width,$e,$halfWidth,$height,$halfWidth,$heightMinusE")
1049          ];
1050      }
1051  
1052      protected function buildOctogonShape($squareSize)
1053      {
1054          $s = $squareSize;
1055          $c = $s * 0.33;
1056          $sMinusC = $s - $c;
1057          return "$c,0,$sMinusC,0,$s,$c,$s,$sMinusC,$sMinusC,$s,$c,$s,0,$sMinusC,0,$c,$c,0";
1058      }
1059  
1060      protected function buildHexagonShape($sideLength)
1061      {
1062          $c = $sideLength;
1063          $a = $c/2;
1064          $b = sin(60 * M_PI / 180) * $c;
1065          $twoB = $b * 2;
1066          $twoC = $c * 2;
1067          $aPlusC = $a + $c;
1068          return "0,$b,$a,0,$aPlusC,0,$twoC,$b,$aPlusC,$twoB,$a,$twoB,0,$b";
1069      }
1070  
1071      protected function buildPlusShape($squareSize)
1072      {
1073          return [
1074              new Rectangle($squareSize, 0, $squareSize, $squareSize*3),
1075              new Rectangle(0, $squareSize, $squareSize*3, $squareSize),
1076          ];
1077      }
1078  
1079      protected function buildTriangleShape($sideLength, $height)
1080      {
1081          $halfWidth = $sideLength / 2;
1082          return "$halfWidth, 0, $sideLength, $height, 0, $height, $halfWidth, 0";
1083      }
1084  
1085      protected function buildRotatedTriangleShape($sideLength, $width)
1086      {
1087          $halfHeight = $sideLength / 2;
1088          return "0, 0, $width, $halfHeight, 0, $sideLength, 0, 0";
1089      }
1090  
1091      protected function buildRightTriangleShape($sideLength)
1092      {
1093          return "0, 0, $sideLength, $sideLength, 0, $sideLength, 0, 0";
1094      }
1095  
1096      protected function buildDiamondShape($width, $height)
1097      {
1098          $halfWidth = $width / 2;
1099          $halfHeight = $height / 2;
1100          return "$halfWidth, 0, $width, $halfHeight, $halfWidth, $height, 0, $halfHeight";
1101      }
1102  
1103      // draw* functions
1104      protected function drawInnerMosaicTile($x, $y, $triangleSize, $vals)
1105      {
1106          $triangle = $this->buildRightTriangleShape($triangleSize);
1107          $opacity = $this->opacity($vals[0]);
1108          $fill = $this->fillColor($vals[0]);
1109          $styles = [
1110              'stroke' => self::STROKE_COLOR,
1111              'stroke-opacity' => self::STROKE_OPACITY,
1112              'fill-opacity' => $opacity,
1113              'fill' => $fill,
1114          ];
1115          $xPlusTriangleSize = $x + $triangleSize;
1116          $yPlusTwoTriangleSize = $y + $triangleSize * 2;
1117          $this->svg->addPolyline($triangle, array_merge($styles, ['transform' => "translate($xPlusTriangleSize, $y) scale(-1, 1)"]))
1118              ->addPolyline($triangle, array_merge($styles, ['transform' => "translate($xPlusTriangleSize, $yPlusTwoTriangleSize) scale(1, -1)"]));
1119  
1120          $opacity = $this->opacity($vals[1]);
1121          $fill = $this->fillColor($vals[1]);
1122          $styles = [
1123              'stroke' => self::STROKE_COLOR,
1124              'stroke-opacity' => self::STROKE_OPACITY,
1125              'fill-opacity' => $opacity,
1126              'fill' => $fill,
1127          ];
1128          $xPlusTriangleSize = $x + $triangleSize;
1129          $yPlusTwoTriangleSize = $y + $triangleSize * 2;
1130          $this->svg->addPolyline($triangle, array_merge($styles, ['transform' => "translate($xPlusTriangleSize, $yPlusTwoTriangleSize) scale(-1, -1)"]))
1131              ->addPolyline($triangle, array_merge($styles, ['transform' => "translate($xPlusTriangleSize, $y) scale(1, 1)"]));
1132  
1133          return $this;
1134      }
1135  
1136      protected function drawOuterMosaicTile($x, $y, $triangleSize, $val)
1137      {
1138          $triangle = $this->buildRightTriangleShape($triangleSize);
1139          $opacity = $this->opacity($val);
1140          $fill = $this->fillColor($val);
1141          $styles = [
1142              'stroke' => self::STROKE_COLOR,
1143              'stroke-opacity' => self::STROKE_OPACITY,
1144              'fill-opacity' => $opacity,
1145              'fill' => $fill,
1146          ];
1147  
1148          $yPlusTriangleSize = $y + $triangleSize;
1149          $xPlusTwoTriangleSize = $x + $triangleSize * 2;
1150          $this->svg->addPolyline($triangle, array_merge($styles, ['transform' => "translate($x, $yPlusTriangleSize) scale(1, -1)"]))
1151              ->addPolyline($triangle, array_merge($styles, ['transform' => "translate($xPlusTwoTriangleSize, $yPlusTriangleSize) scale(-1, -1)"]))
1152              ->addPolyline($triangle, array_merge($styles, ['transform' => "translate($x, $yPlusTriangleSize) scale(1, 1)"]))
1153              ->addPolyline($triangle, array_merge($styles, ['transform' => "translate($xPlusTwoTriangleSize, $yPlusTriangleSize) scale(-1, 1)"]));
1154      }
1155  
1156      // Utility Functions
1157  
1158      protected function fillColor($val)
1159      {
1160          return ($val % 2 == 0) ? self::FILL_COLOR_LIGHT : self::FILL_COLOR_DARK;
1161      }
1162  
1163      protected function opacity($val)
1164      {
1165          return $this->map($val, 0, 15, self::OPACITY_MIN, self::OPACITY_MAX);
1166      }
1167  
1168      protected function hexVal($index, $len)
1169      {
1170          return hexdec(substr($this->hash, $index, $len));
1171      }
1172  
1173      // PHP implementation of Processing's map function
1174      // http://processing.org/reference/map_.html
1175      protected function map($value, $vMin, $vMax, $dMin, $dMax)
1176      {
1177          $vValue = floatval($value);
1178          $vRange = $vMax - $vMin;
1179          $dRange = $dMax - $dMin;
1180          return ($vValue - $vMin) * $dRange / $vRange + $dMin;
1181      }
1182  
1183      // Color Functions
1184      protected function hexToHSL($color)
1185      {
1186          $color = trim($color, '#');
1187          $R = hexdec($color[0].$color[1]);
1188          $G = hexdec($color[2].$color[3]);
1189          $B = hexdec($color[4].$color[5]);
1190  
1191          $HSL = array();
1192  
1193          $var_R = ($R / 255);
1194          $var_G = ($G / 255);
1195          $var_B = ($B / 255);
1196  
1197          $var_Min = min($var_R, $var_G, $var_B);
1198          $var_Max = max($var_R, $var_G, $var_B);
1199          $del_Max = $var_Max - $var_Min;
1200  
1201          $L = ($var_Max + $var_Min)/2;
1202  
1203          if ($del_Max == 0)
1204          {
1205              $H = 0;
1206              $S = 0;
1207          }
1208          else
1209          {
1210              if ( $L < 0.5 ) $S = $del_Max / ( $var_Max + $var_Min );
1211              else            $S = $del_Max / ( 2 - $var_Max - $var_Min );
1212  
1213              $del_R = ( ( ( $var_Max - $var_R ) / 6 ) + ( $del_Max / 2 ) ) / $del_Max;
1214              $del_G = ( ( ( $var_Max - $var_G ) / 6 ) + ( $del_Max / 2 ) ) / $del_Max;
1215              $del_B = ( ( ( $var_Max - $var_B ) / 6 ) + ( $del_Max / 2 ) ) / $del_Max;
1216  
1217              if      ($var_R == $var_Max) $H = $del_B - $del_G;
1218              else if ($var_G == $var_Max) $H = ( 1 / 3 ) + $del_R - $del_B;
1219              else if ($var_B == $var_Max) $H = ( 2 / 3 ) + $del_G - $del_R;
1220  
1221              if ($H<0) $H++;
1222              if ($H>1) $H--;
1223          }
1224  
1225          $HSL['h'] = ($H*360);
1226          $HSL['s'] = $S;
1227          $HSL['l'] = $L;
1228  
1229          return $HSL;
1230      }
1231  
1232      protected function hexToRGB($hex) {
1233          $hex = str_replace("#", "", $hex);
1234          if(strlen($hex) == 3) {
1235              $r = hexdec(substr($hex,0,1).substr($hex,0,1));
1236              $g = hexdec(substr($hex,1,1).substr($hex,1,1));
1237              $b = hexdec(substr($hex,2,1).substr($hex,2,1));
1238          } else {
1239              $r = hexdec(substr($hex,0,2));
1240              $g = hexdec(substr($hex,2,2));
1241              $b = hexdec(substr($hex,4,2));
1242          }
1243          return ['r' => $r, 'g' => $g, 'b' => $b];
1244      }
1245  
1246      protected function rgbToHSL($r, $g, $b) {
1247          $r /= 255;
1248          $g /= 255;
1249          $b /= 255;
1250          $max = max($r, $g, $b);
1251          $min = min($r, $g, $b);
1252          $l = ($max + $min) / 2;
1253          if ($max == $min) {
1254              $h = $s = 0;
1255          } else {
1256              $d = $max - $min;
1257              $s = $l > 0.5 ? $d / (2 - $max - $min) : $d / ($max + $min);
1258              switch ($max) {
1259                  case $r:
1260                      $h = ($g - $b) / $d + ($g < $b ? 6 : 0);
1261                      break;
1262                  case $g:
1263                      $h = ($b - $r) / $d + 2;
1264                      break;
1265                  case $b:
1266                      $h = ($r - $g) / $d + 4;
1267                      break;
1268              }
1269              $h /= 6;
1270          }
1271          $h = floor($h * 360);
1272          $s = floor($s * 100);
1273          $l = floor($l * 100);
1274          return ['h' => $h, 's' => $s, 'l' => $l];
1275      }
1276  
1277      protected function hslToRGB ($h, $s, $l) {
1278          $h += 360;
1279          $c = ( 1 - abs( 2 * $l - 1 ) ) * $s;
1280          $x = $c * ( 1 - abs( fmod( ( $h / 60 ), 2 ) - 1 ) );
1281          $m = $l - ( $c / 2 );
1282  
1283          if ( $h < 60 ) {
1284              $r = $c;
1285              $g = $x;
1286              $b = 0;
1287          } else if ( $h < 120 ) {
1288              $r = $x;
1289              $g = $c;
1290              $b = 0;
1291          } else if ( $h < 180 ) {
1292              $r = 0;
1293              $g = $c;
1294              $b = $x;
1295          } else if ( $h < 240 ) {
1296              $r = 0;
1297              $g = $x;
1298              $b = $c;
1299          } else if ( $h < 300 ) {
1300              $r = $x;
1301              $g = 0;
1302              $b = $c;
1303          } else {
1304              $r = $c;
1305              $g = 0;
1306              $b = $x;
1307          }
1308  
1309          $r = ( $r + $m ) * 255;
1310          $g = ( $g + $m ) * 255;
1311          $b = ( $b + $m  ) * 255;
1312  
1313          return array( 'r' => floor( $r ), 'g' => floor( $g ), 'b' => floor( $b ) );
1314  
1315      }
1316  
1317      //NOT USED
1318      protected function rgbToHex($r, $g, $b) {
1319          $hex = "#";
1320          $hex .= str_pad(dechex($r), 2, "0", STR_PAD_LEFT);
1321          $hex .= str_pad(dechex($g), 2, "0", STR_PAD_LEFT);
1322          $hex .= str_pad(dechex($b), 2, "0", STR_PAD_LEFT);
1323          return $hex;
1324      }
1325  
1326  
1327  }