├── .babelrc ├── .gitignore ├── README.md ├── dist ├── chromata.js ├── chromata.min.js └── index.html ├── package.json ├── src ├── assets │ └── images │ │ ├── E-_GitHub_chromata_build_assets_images_emporer.png │ │ ├── city.jpg │ │ ├── emperor.png │ │ ├── face.jpg │ │ ├── lightning.jpg │ │ ├── nyc.jpg │ │ ├── rome.jpg │ │ ├── severin.JPG │ │ ├── tiny.png │ │ └── tree.jpg ├── index.html └── scripts │ ├── chromata.js │ ├── init.js │ ├── pathFinder.js │ ├── pathQueue.js │ ├── pathRenderer.js │ └── utils.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | build 3 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chromata 2 | 3 | ### A generative digital art tool. 4 | Chromata is a small tool written in JavaScript which can turn any image into a unique, animated artwork. 5 | Path finders are seeded on a canvas and independently trace their own path through the image, 6 | reading the colour data of each pixel and altering their course based on a set of configurable rules. 7 | 8 | ### [Demo](http://www.michaelbromley.co.uk/experiments/chromata/) 9 | 10 | ### [Documentation](http://www.michaelbromley.co.uk/experiments/chromata/#about) 11 | 12 | ## Build 13 | 14 | Chromata is written in ES6 JavaScript, and uses Babel for transpilation and Systemjs to handle module loading during the 15 | build phase. 16 | 17 | 1. Clone the repo and then `npm install` 18 | 2. `gulp watch` 19 | 3. Test the output by altering the config in `src/scripts/init.js` 20 | 21 | ## License 22 | MIT 23 | -------------------------------------------------------------------------------- /dist/chromata.js: -------------------------------------------------------------------------------- 1 | (function(window, undefined){ 2 | 3 | "use strict"; 4 | 5 | var _prototypeProperties = function (child, staticProps, instanceProps) { 6 | if (staticProps) Object.defineProperties(child, staticProps); 7 | if (instanceProps) Object.defineProperties(child.prototype, instanceProps); 8 | }; 9 | 10 | var Chromata = (function () { 11 | function Chromata(imageElement) { 12 | var _this = this; 13 | var options = arguments[1] === undefined ? {} : arguments[1]; 14 | var renderCanvas = document.createElement("canvas"), 15 | renderContext = renderCanvas.getContext("2d"), 16 | sourceCanvas = document.createElement("canvas"), 17 | sourceContext = sourceCanvas.getContext("2d"), 18 | image = new Image(), 19 | dimensions, 20 | ready = false; 21 | renderCanvas.setAttribute("id", "chromataCanvas"); 22 | 23 | this.options = this._mergeOptions(options); 24 | 25 | image.crossOrigin = "Anonymous"; 26 | image.addEventListener("load", function () { 27 | dimensions = Utils._getOutputDimensions(imageElement, _this.options.outputSize); 28 | sourceCanvas.width = renderCanvas.width = dimensions.width; 29 | sourceCanvas.height = renderCanvas.height = dimensions.height; 30 | sourceContext.drawImage(image, 0, 0, dimensions.width, dimensions.height); 31 | 32 | _this.dimensions = dimensions; 33 | _this.imageArray = Utils._getImageArray(sourceContext); 34 | _this.workingArray = Utils._getWorkingArray(sourceContext); 35 | 36 | ready = true; 37 | }); 38 | image.src = imageElement.src; 39 | 40 | this.loader = function (callback) { 41 | if (!ready) { 42 | setTimeout(function () { 43 | return _this.loader(callback); 44 | }, 50); 45 | } else { 46 | callback(); 47 | } 48 | }; 49 | 50 | this.imageArray = []; 51 | this.sourceImageElement = imageElement; 52 | this.sourceContext = sourceContext; 53 | this.renderContext = renderContext; 54 | this.isRunning = false; 55 | this.iterationCount = 0; 56 | } 57 | 58 | _prototypeProperties(Chromata, null, { 59 | start: { 60 | 61 | /** 62 | * Start the animation. 63 | */ 64 | value: function start() { 65 | var _this2 = this; 66 | this.loader(function () { 67 | _this2.isRunning = true; 68 | 69 | if (typeof _this2._tick === "undefined") { 70 | _this2._run(); 71 | } else { 72 | _this2._tick(); 73 | } 74 | }); 75 | }, 76 | writable: true, 77 | enumerable: true, 78 | configurable: true 79 | }, 80 | stop: { 81 | 82 | /** 83 | * Stop the animation. Returns the current iteration count. 84 | * @returns {number} 85 | */ 86 | value: function stop() { 87 | this.isRunning = false; 88 | return this.iterationCount; 89 | }, 90 | writable: true, 91 | enumerable: true, 92 | configurable: true 93 | }, 94 | toggle: { 95 | 96 | /** 97 | * Start/stop the animation. If stopping, return the current iteration count. 98 | * @returns {*} 99 | */ 100 | value: function toggle() { 101 | if (this.isRunning) { 102 | return this.stop(); 103 | } else { 104 | return this.start(); 105 | } 106 | }, 107 | writable: true, 108 | enumerable: true, 109 | configurable: true 110 | }, 111 | reset: { 112 | 113 | /** 114 | * Clear the canvas and set the animation back to the start. 115 | */ 116 | value: function reset() { 117 | this.isRunning = false; 118 | this._tick = undefined; 119 | cancelAnimationFrame(this.raf); 120 | this.renderContext.clearRect(0, 0, this.dimensions.width, this.dimensions.height); 121 | this.workingArray = Utils._getWorkingArray(this.sourceContext); 122 | this._removeRenderCanvas(); 123 | }, 124 | writable: true, 125 | enumerable: true, 126 | configurable: true 127 | }, 128 | _mergeOptions: { 129 | 130 | /** 131 | * Merge any user-supplied config options with the defaults and perform some validation. 132 | * @param options 133 | * @private 134 | */ 135 | value: function MergeOptions(options) { 136 | var defaults = { 137 | colorMode: "color", 138 | compositeOperation: "lighten", 139 | iterationLimit: 0, 140 | key: "low", 141 | lineWidth: 2, 142 | lineMode: "smooth", 143 | origin: ["bottom"], 144 | outputSize: "original", 145 | pathFinderCount: 30, 146 | speed: 7, 147 | turningAngle: Math.PI 148 | }; 149 | 150 | var merged = {}; 151 | 152 | for (var prop in defaults) { 153 | if (defaults.hasOwnProperty(prop)) { 154 | merged[prop] = options[prop] || defaults[prop]; 155 | } 156 | } 157 | 158 | // some validation 159 | merged.origin = merged.origin.constructor === Array ? merged.origin : defaults.origin; 160 | merged.pathFinderCount = this._limitToRange(merged.pathFinderCount, 1, 10000); 161 | merged.lineWidth = this._limitToRange(merged.lineWidth, 1, 100); 162 | merged.speed = this._limitToRange(merged.speed, 1, 100); 163 | merged.turningAngle = this._limitToRange(merged.turningAngle, 0.1, 10); 164 | 165 | return merged; 166 | }, 167 | writable: true, 168 | enumerable: true, 169 | configurable: true 170 | }, 171 | _limitToRange: { 172 | value: function LimitToRange(val, low, high) { 173 | return Math.min(Math.max(val, low), high); 174 | }, 175 | writable: true, 176 | enumerable: true, 177 | configurable: true 178 | }, 179 | _appendRenderCanvas: { 180 | 181 | /** 182 | * Hide the source image element and append the render canvas directly after it in the DOM. 183 | * @private 184 | */ 185 | value: function AppendRenderCanvas() { 186 | var parentElement = this.sourceImageElement.parentNode; 187 | 188 | this.sourceImageElement.style.display = "none"; 189 | parentElement.insertBefore(this.renderContext.canvas, this.sourceImageElement.nextSibling); 190 | }, 191 | writable: true, 192 | enumerable: true, 193 | configurable: true 194 | }, 195 | _removeRenderCanvas: { 196 | 197 | /** 198 | * Unhide the source image and remove the render canvas from the DOM. 199 | * @private 200 | */ 201 | value: function RemoveRenderCanvas() { 202 | this.sourceImageElement.style.display = ""; 203 | this.renderContext.canvas.parentNode.removeChild(this.renderContext.canvas); 204 | }, 205 | writable: true, 206 | enumerable: true, 207 | configurable: true 208 | }, 209 | _run: { 210 | 211 | /** 212 | * Set up the pathfinders and renderers and get the animation going. 213 | * @private 214 | */ 215 | value: function Run() { 216 | var _this3 = this; 217 | 218 | 219 | var renderers = [], 220 | pathFinders = this._initPathFinders(), 221 | renderOptions = { 222 | colorMode: this.options.colorMode, 223 | lineWidth: this.options.lineWidth, 224 | lineMode: this.options.lineMode, 225 | speed: this.options.speed 226 | }; 227 | 228 | this._appendRenderCanvas(); 229 | 230 | this.renderContext.globalCompositeOperation = this.options.compositeOperation; 231 | 232 | pathFinders.forEach(function (pathFinder) { 233 | renderers.push(new PathRenderer(_this3.renderContext, pathFinder, renderOptions)); 234 | }); 235 | 236 | this._tick = function () { 237 | if (0 < _this3.options.iterationLimit && _this3.options.iterationLimit <= _this3.iterationCount) { 238 | _this3.isRunning = false; 239 | _this3.options.iterationLimit = 0; 240 | } 241 | 242 | renderers.forEach(function (renderer) { 243 | return renderer.drawNextLine(); 244 | }); 245 | _this3.iterationCount++; 246 | 247 | if (_this3.isRunning) { 248 | _this3.raf = requestAnimationFrame(_this3._tick); 249 | } 250 | }; 251 | 252 | this._tick(); 253 | }, 254 | writable: true, 255 | enumerable: true, 256 | configurable: true 257 | }, 258 | _initPathFinders: { 259 | 260 | /** 261 | * Create the pathfinders 262 | * @returns {Array} 263 | * @private 264 | */ 265 | value: function InitPathFinders() { 266 | var _this4 = this; 267 | var pathFinders = [], 268 | count = this.options.pathFinderCount, 269 | origins = this.options.origin, 270 | pathFindersPerOrigin = count / origins.length, 271 | options = { 272 | speed: this.options.speed, 273 | turningAngle: this.options.turningAngle, 274 | key: this.options.key 275 | }; 276 | 277 | if (-1 < origins.indexOf("bottom")) { 278 | this._seedBottom(pathFindersPerOrigin, pathFinders, options); 279 | } 280 | if (-1 < origins.indexOf("top")) { 281 | this._seedTop(pathFindersPerOrigin, pathFinders, options); 282 | } 283 | if (-1 < origins.indexOf("left")) { 284 | this._seedLeft(pathFindersPerOrigin, pathFinders, options); 285 | } 286 | if (-1 < origins.indexOf("right")) { 287 | this._seedRight(pathFindersPerOrigin, pathFinders, options); 288 | } 289 | 290 | origins.forEach(function (origin) { 291 | var matches = origin.match(/(\d{1,3})% (\d{1,3})%/); 292 | if (matches) { 293 | _this4._seedPoint(pathFindersPerOrigin, pathFinders, options, matches[1], matches[2]); 294 | } 295 | }); 296 | 297 | return pathFinders; 298 | }, 299 | writable: true, 300 | enumerable: true, 301 | configurable: true 302 | }, 303 | _seedTop: { 304 | value: function SeedTop(count, pathFinders, options) { 305 | var _this5 = this; 306 | var width = this.dimensions.width, 307 | unit = width / count, 308 | xPosFn = function (i) { 309 | return unit * i - unit / 2; 310 | }, 311 | yPosFn = function () { 312 | return _this5.options.speed; 313 | }; 314 | 315 | options.startingVelocity = [0, this.options.speed]; 316 | this._seedCreateLoop(count, pathFinders, xPosFn, yPosFn, options); 317 | }, 318 | writable: true, 319 | enumerable: true, 320 | configurable: true 321 | }, 322 | _seedBottom: { 323 | value: function SeedBottom(count, pathFinders, options) { 324 | var _this6 = this; 325 | var width = this.dimensions.width, 326 | height = this.dimensions.height, 327 | unit = width / count, 328 | xPosFn = function (i) { 329 | return unit * i - unit / 2; 330 | }, 331 | yPosFn = function () { 332 | return height - _this6.options.speed; 333 | }; 334 | 335 | options.startingVelocity = [0, -this.options.speed]; 336 | this._seedCreateLoop(count, pathFinders, xPosFn, yPosFn, options); 337 | }, 338 | writable: true, 339 | enumerable: true, 340 | configurable: true 341 | }, 342 | _seedLeft: { 343 | value: function SeedLeft(count, pathFinders, options) { 344 | var _this7 = this; 345 | var height = this.dimensions.height, 346 | unit = height / count, 347 | xPosFn = function () { 348 | return _this7.options.speed; 349 | }, 350 | yPosFn = function (i) { 351 | return unit * i - unit / 2; 352 | }; 353 | 354 | options.startingVelocity = [this.options.speed, 0]; 355 | this._seedCreateLoop(count, pathFinders, xPosFn, yPosFn, options); 356 | }, 357 | writable: true, 358 | enumerable: true, 359 | configurable: true 360 | }, 361 | _seedRight: { 362 | value: function SeedRight(count, pathFinders, options) { 363 | var _this8 = this; 364 | var width = this.dimensions.width, 365 | height = this.dimensions.height, 366 | unit = height / count, 367 | xPosFn = function () { 368 | return width - _this8.options.speed; 369 | }, 370 | yPosFn = function (i) { 371 | return unit * i - unit / 2; 372 | }; 373 | 374 | options.startingVelocity = [-this.options.speed, 0]; 375 | this._seedCreateLoop(count, pathFinders, xPosFn, yPosFn, options); 376 | }, 377 | writable: true, 378 | enumerable: true, 379 | configurable: true 380 | }, 381 | _seedPoint: { 382 | value: function SeedPoint(count, pathFinders, options, xPc, yPc) { 383 | var xPos = Math.floor(this.dimensions.width * xPc / 100), 384 | yPos = Math.floor(this.dimensions.height * yPc / 100); 385 | 386 | for (var i = 1; i < count + 1; i++) { 387 | var color = Utils._indexToRgbString(i), 388 | direction = i % 4; 389 | 390 | switch (direction) { 391 | case 0: 392 | options.startingVelocity = [-this.options.speed, 0]; 393 | break; 394 | case 1: 395 | options.startingVelocity = [0, this.options.speed]; 396 | break; 397 | case 2: 398 | options.startingVelocity = [this.options.speed, 0]; 399 | break; 400 | case 3: 401 | options.startingVelocity = [0, -this.options.speed]; 402 | break; 403 | } 404 | 405 | pathFinders.push(new PathFinder(this.imageArray, this.workingArray, color, xPos, yPos, options)); 406 | } 407 | }, 408 | writable: true, 409 | enumerable: true, 410 | configurable: true 411 | }, 412 | _seedCreateLoop: { 413 | value: function SeedCreateLoop(count, pathFinders, xPosFn, yPosFn, options) { 414 | for (var i = 1; i < count + 1; i++) { 415 | var color = Utils._indexToRgbString(i), 416 | xPos = xPosFn(i), 417 | yPos = yPosFn(i); 418 | 419 | pathFinders.push(new PathFinder(this.imageArray, this.workingArray, color, xPos, yPos, options)); 420 | } 421 | }, 422 | writable: true, 423 | enumerable: true, 424 | configurable: true 425 | } 426 | }); 427 | 428 | return Chromata; 429 | })(); 430 | 431 | window.Chromata = Chromata; 432 | 433 | var MAX = 255; 434 | 435 | var PathFinder = (function () { 436 | function PathFinder(pixelArray, workingArray, targetColor) { 437 | var initX = arguments[3] === undefined ? 0 : arguments[3]; 438 | var initY = arguments[4] === undefined ? 0 : arguments[4]; 439 | var options = arguments[5] === undefined ? {} : arguments[5]; 440 | this.pixelArray = pixelArray; 441 | this.workingArray = workingArray; 442 | this.arrayWidth = pixelArray[0].length; 443 | this.arrayHeight = pixelArray.length; 444 | this.x = Math.round(initX); 445 | this.y = Math.round(initY); 446 | this.options = options; 447 | this.pathQueue = new PathQueue(10); 448 | this.velocity = options.startingVelocity; 449 | 450 | this.targetColor = typeof targetColor === "string" ? this._hexToRgb(targetColor) : targetColor; 451 | this.rgbIndex = this._getRgbIndex(this.targetColor); 452 | 453 | if (this.options.key === "low") { 454 | this.comparatorFn = function (distance, closest) { 455 | return 0 < distance && distance < closest; 456 | }; 457 | } else { 458 | this.comparatorFn = function (distance, closest) { 459 | return closest < distance && distance < MAX; 460 | }; 461 | } 462 | } 463 | 464 | _prototypeProperties(PathFinder, null, { 465 | getNextPoint: { 466 | 467 | /** 468 | * Get next coordinate point in path. 469 | * 470 | * @returns {[int, int, int]} 471 | */ 472 | value: function getNextPoint() { 473 | var result, 474 | i = 0, 475 | limit = 5; // prevent an infinite loop 476 | 477 | do { 478 | result = this._getNextPixel(); 479 | i++; 480 | } while (i <= limit && result.isPristine === false); 481 | 482 | return result.nextPixel; 483 | }, 484 | writable: true, 485 | enumerable: true, 486 | configurable: true 487 | }, 488 | _getNextPixel: { 489 | 490 | /** 491 | * Algorithm for finding the next point by picking the closest match out of an arc-shaped array of possible pixels 492 | * arranged pointing in the direction of velocity. 493 | * 494 | * @returns {{nextPixel: [int, int, int], isPristine: boolean}} 495 | * @private 496 | */ 497 | value: function GetNextPixel() { 498 | var theta = this._getVelocityAngle(), 499 | isPristine, 500 | closestColor = this.options.key === "low" ? 100000 : 0, 501 | nextPixel, 502 | defaultNextPixel, 503 | arcSize = this.options.turningAngle, 504 | radius = Math.round(Math.sqrt(Math.pow(this.velocity[0], 2) + Math.pow(this.velocity[1], 2))), 505 | sampleSize = 4; // how many surrounding pixels to test for next point 506 | 507 | for (var angle = theta - arcSize / 2, deviance = -sampleSize / 2; angle <= theta + arcSize / 2; angle += arcSize / sampleSize, deviance++) { 508 | var x = this.x + Math.round(radius * Math.cos(angle)), 509 | y = this.y + Math.round(radius * Math.sin(angle)), 510 | colorDistance = MAX; 511 | 512 | if (this._isInRange(x, y)) { 513 | var visited = this.workingArray[y][x][this.rgbIndex], 514 | currentPixel = this.pixelArray[y][x], 515 | alpha = currentPixel[3]; 516 | 517 | colorDistance = this._getColorDistance(currentPixel); 518 | 519 | if (this.comparatorFn(colorDistance, closestColor) && !visited && alpha === MAX) { 520 | nextPixel = [x, y, MAX - colorDistance]; 521 | closestColor = colorDistance; 522 | } 523 | } 524 | 525 | if (deviance === 0) { 526 | var pa = this.pixelArray; 527 | if (pa[y] && pa[y][x] && pa[y][x][3] === MAX) { 528 | defaultNextPixel = [x, y, MAX - colorDistance]; 529 | } else { 530 | defaultNextPixel = this.pathQueue.get(-2); 531 | } 532 | } 533 | } 534 | 535 | isPristine = typeof nextPixel !== "undefined"; 536 | nextPixel = nextPixel || defaultNextPixel; 537 | 538 | if (nextPixel) { 539 | this.velocity = [nextPixel[0] - this.x, nextPixel[1] - this.y]; 540 | this.y = nextPixel[1]; 541 | this.x = nextPixel[0]; 542 | this._updateWorkingArray(nextPixel[1], nextPixel[0]); 543 | this.pathQueue.put(nextPixel); 544 | } 545 | 546 | return { 547 | nextPixel: nextPixel, 548 | isPristine: isPristine 549 | }; 550 | }, 551 | writable: true, 552 | enumerable: true, 553 | configurable: true 554 | }, 555 | getColor: { 556 | 557 | /** 558 | * Get an [r, g, b] array of the target color. 559 | * @returns {{r: *, g: *, b: *}} 560 | */ 561 | value: function getColor() { 562 | return { 563 | r: this.targetColor[0], 564 | g: this.targetColor[1], 565 | b: this.targetColor[2] 566 | }; 567 | }, 568 | writable: true, 569 | enumerable: true, 570 | configurable: true 571 | }, 572 | _getVelocityAngle: { 573 | 574 | /** 575 | * Get the angle indicated by the velocity vector, correcting for the case that the angle would 576 | * take the pathfinder off the image canvas, in which case the angle will be set towards the 577 | * centre of the canvas. 578 | * 579 | * @returns {*} 580 | * @private 581 | */ 582 | value: function GetVelocityAngle() { 583 | var projectedX = this.x + this.velocity[0], 584 | projectedY = this.y + this.velocity[1], 585 | margin = this.options.speed, 586 | dy = this.y + this.velocity[1] - this.y, 587 | dx = this.x + this.velocity[0] - this.x, 588 | angle; 589 | 590 | // has it gone out of bounds on the x axis? 591 | if (projectedX <= margin || this.arrayWidth - margin <= projectedX) { 592 | dx *= -1; 593 | } 594 | 595 | // has it gone out of bounds on the y axis? 596 | if (projectedY <= margin || this.arrayHeight - margin <= projectedY) { 597 | dy *= -1; 598 | } 599 | 600 | angle = Math.atan2(dy, dx); 601 | return angle; 602 | }, 603 | writable: true, 604 | enumerable: true, 605 | configurable: true 606 | }, 607 | _hexToRgb: { 608 | 609 | /** 610 | * From http://stackoverflow.com/a/5624139/772859 611 | * @param hex 612 | * @returns {{r: Number, g: Number, b: Number}} 613 | * @private 614 | */ 615 | value: function HexToRgb(hex) { 616 | // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") 617 | var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; 618 | hex = hex.replace(shorthandRegex, function (m, r, g, b) { 619 | return r + r + g + g + b + b; 620 | }); 621 | 622 | var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); 623 | return result ? [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)] : null; 624 | }, 625 | writable: true, 626 | enumerable: true, 627 | configurable: true 628 | }, 629 | _getColorDistance: { 630 | value: function GetColorDistance(pixel) { 631 | return MAX - pixel[this.rgbIndex]; 632 | }, 633 | writable: true, 634 | enumerable: true, 635 | configurable: true 636 | }, 637 | _isInRange: { 638 | 639 | /** 640 | * Return true if the x, y points lie within the image dimensions. 641 | * @param x 642 | * @param y 643 | * @returns {boolean} 644 | * @private 645 | */ 646 | value: function IsInRange(x, y) { 647 | return 0 < x && x < this.arrayWidth && 0 < y && y < this.arrayHeight; 648 | }, 649 | writable: true, 650 | enumerable: true, 651 | configurable: true 652 | }, 653 | _updateWorkingArray: { 654 | value: function UpdateWorkingArray(row, col) { 655 | this.workingArray[row][col][this.rgbIndex] = true; 656 | }, 657 | writable: true, 658 | enumerable: true, 659 | configurable: true 660 | }, 661 | _getRgbIndex: { 662 | value: function GetRgbIndex(targetColorArray) { 663 | var i; 664 | for (i = 0; i < 2; i++) { 665 | if (targetColorArray[i] !== 0) { 666 | break; 667 | } 668 | } 669 | 670 | return i; 671 | }, 672 | writable: true, 673 | enumerable: true, 674 | configurable: true 675 | } 676 | }); 677 | 678 | return PathFinder; 679 | })(); 680 | 681 | /** 682 | * Implementation of a queue of a fixed size. 683 | */ 684 | var PathQueue = (function () { 685 | function PathQueue(size) { 686 | this.queue = []; 687 | this.size = size; 688 | } 689 | 690 | _prototypeProperties(PathQueue, null, { 691 | put: { 692 | 693 | /** 694 | * Put a new item in the queue. If this causes the queue to exceed its size limit, the oldest 695 | * item will be discarded. 696 | * @param item 697 | */ 698 | value: function put(item) { 699 | this.queue.push(item); 700 | if (this.size < this.queue.length) { 701 | this.queue.shift(); 702 | } 703 | }, 704 | writable: true, 705 | enumerable: true, 706 | configurable: true 707 | }, 708 | get: { 709 | 710 | /** 711 | * Get an item from the queue, specified by index. 0 gets the oldest item in the queue, 1 the second oldest etc. 712 | * -1 gets the newest item, -2 the second newest etc. 713 | * 714 | * @param index 715 | * @returns {*} 716 | */ 717 | value: function get() { 718 | var index = arguments[0] === undefined ? 0 : arguments[0]; 719 | var length = this.queue.length; 720 | if (0 <= index && index <= length) { 721 | return this.queue[index]; 722 | } else if (index < 0 && Math.abs(index) <= length) { 723 | return this.queue[length + index]; 724 | } else { 725 | return undefined; 726 | } 727 | }, 728 | writable: true, 729 | enumerable: true, 730 | configurable: true 731 | }, 732 | contains: { 733 | value: function contains(item) { 734 | var matches = this.queue.filter(function (point) { 735 | return point[0] === item[0] && point[1] === item[1]; 736 | }); 737 | 738 | return 0 < matches.length; 739 | }, 740 | writable: true, 741 | enumerable: true, 742 | configurable: true 743 | } 744 | }); 745 | 746 | return PathQueue; 747 | })(); 748 | 749 | /** 750 | * Renders the points created by a Pathfinder 751 | */ 752 | var PathRenderer = (function () { 753 | function PathRenderer(context, pathFinder, options) { 754 | this.context = context; 755 | this.pathFinder = pathFinder; 756 | this.options = options; 757 | this.color = pathFinder.getColor(); 758 | } 759 | 760 | _prototypeProperties(PathRenderer, null, { 761 | drawNextLine: { 762 | value: function drawNextLine() { 763 | if (this.options.lineMode === "smooth") { 764 | this._drawLineSmooth(); 765 | } else if (this.options.lineMode === "square") { 766 | this._drawLineSquare(); 767 | } else { 768 | this._drawPoint(); 769 | } 770 | }, 771 | writable: true, 772 | enumerable: true, 773 | configurable: true 774 | }, 775 | _drawLineSmooth: { 776 | value: function DrawLineSmooth() { 777 | var midX, midY, midColor, lineLength, nextPoint = this.pathFinder.getNextPoint(this.context); 778 | 779 | if (nextPoint) { 780 | if (typeof this.currentPoint === "undefined") { 781 | this.currentPoint = nextPoint; 782 | } 783 | if (typeof this.controlPoint === "undefined") { 784 | this.controlPoint = nextPoint; 785 | } 786 | 787 | midX = Math.round((this.controlPoint[0] + nextPoint[0]) / 2); 788 | midY = Math.round((this.controlPoint[1] + nextPoint[1]) / 2); 789 | midColor = Math.floor((this.currentPoint[2] + nextPoint[2]) / 2); 790 | lineLength = this._getLineLength(this.currentPoint, nextPoint); 791 | 792 | if (lineLength <= this.options.speed * 3) { 793 | var grad = undefined, 794 | startColorValue = this.currentPoint[2], 795 | endColorValue = nextPoint[2]; 796 | 797 | grad = this._createGradient(this.currentPoint, nextPoint, startColorValue, endColorValue); 798 | this.context.strokeStyle = grad; 799 | 800 | this.context.lineWidth = this.options.lineWidth; 801 | this.context.lineCap = "round"; 802 | this.context.beginPath(); 803 | 804 | this.context.moveTo(this.currentPoint[0], this.currentPoint[1]); 805 | this.context.quadraticCurveTo(this.controlPoint[0], this.controlPoint[1], midX, midY); 806 | this.context.stroke(); 807 | } 808 | 809 | this.currentPoint = [midX, midY, midColor]; 810 | this.controlPoint = nextPoint; 811 | } 812 | }, 813 | writable: true, 814 | enumerable: true, 815 | configurable: true 816 | }, 817 | _drawLineSquare: { 818 | value: function DrawLineSquare() { 819 | var lineLength, nextPoint = this.pathFinder.getNextPoint(this.context); 820 | 821 | if (nextPoint) { 822 | if (typeof this.currentPoint === "undefined") { 823 | this.currentPoint = nextPoint; 824 | } 825 | 826 | lineLength = this._getLineLength(this.currentPoint, nextPoint); 827 | 828 | if (lineLength <= this.options.speed + 1) { 829 | var grad = undefined, 830 | startColorValue = this.currentPoint[2], 831 | endColorValue = nextPoint[2]; 832 | 833 | grad = this._createGradient(this.currentPoint, nextPoint, startColorValue, endColorValue); 834 | 835 | this.context.strokeStyle = grad; 836 | this.context.lineWidth = this.options.lineWidth; 837 | this.context.lineCap = "round"; 838 | this.context.beginPath(); 839 | 840 | this.context.moveTo(this.currentPoint[0], this.currentPoint[1]); 841 | this.context.lineTo(nextPoint[0], nextPoint[1]); 842 | this.context.stroke(); 843 | } 844 | this.currentPoint = nextPoint; 845 | } 846 | }, 847 | writable: true, 848 | enumerable: true, 849 | configurable: true 850 | }, 851 | _drawPoint: { 852 | value: function DrawPoint() { 853 | var lineLength, nextPoint = this.pathFinder.getNextPoint(this.context); 854 | 855 | if (nextPoint) { 856 | if (typeof this.currentPoint === "undefined") { 857 | this.currentPoint = nextPoint; 858 | } 859 | 860 | lineLength = this._getLineLength(this.currentPoint, nextPoint); 861 | 862 | if (lineLength >= this.options.speed * 2) { 863 | this.context.beginPath(); 864 | 865 | this.context.arc(nextPoint[0], nextPoint[1], this.options.lineWidth, 0, 2 * Math.PI, false); 866 | this.context.fillStyle = this._getStrokeColor(nextPoint[2]); 867 | this.context.fill(); 868 | 869 | this.currentPoint = nextPoint; 870 | } 871 | } 872 | }, 873 | writable: true, 874 | enumerable: true, 875 | configurable: true 876 | }, 877 | _getLineLength: { 878 | value: function GetLineLength(p1, p2) { 879 | var dx = p2[0] - p1[0]; 880 | var dy = p2[1] - p1[1]; 881 | return Math.round(Math.sqrt(dx * dx + dy * dy)); 882 | }, 883 | writable: true, 884 | enumerable: true, 885 | configurable: true 886 | }, 887 | _createGradient: { 888 | value: function CreateGradient(p1, p2, color1, color2) { 889 | var grad = this.context.createLinearGradient(p1[0], p1[1], p2[0], p2[1]); 890 | grad.addColorStop(0, this._getStrokeColor(color1)); 891 | grad.addColorStop(1, this._getStrokeColor(color2)); 892 | return grad; 893 | }, 894 | writable: true, 895 | enumerable: true, 896 | configurable: true 897 | }, 898 | _getStrokeColor: { 899 | 900 | /** 901 | * Get an rgba color string based on the color value and the pathRenderer's color and color mode. 902 | * 903 | * @param colorValue 904 | * @returns {*} 905 | * @private 906 | */ 907 | value: function GetStrokeColor(colorValue) { 908 | var colorString; 909 | 910 | if (this.options.colorMode === "color") { 911 | colorString = "rgba(" + (this.color.r !== 0 ? colorValue : 0) + ", " + (this.color.g !== 0 ? colorValue : 0) + ", " + (this.color.b !== 0 ? colorValue : 0) + ", " + 1 + ")"; 912 | } else { 913 | // greyscale 914 | colorString = "rgba(" + colorValue + ", " + colorValue + ", " + colorValue + ", " + 1 + ")"; 915 | } 916 | 917 | return colorString; 918 | }, 919 | writable: true, 920 | enumerable: true, 921 | configurable: true 922 | } 923 | }); 924 | 925 | return PathRenderer; 926 | })(); 927 | 928 | /** 929 | * Static utilities class containing helper functions 930 | */ 931 | var Utils = (function () { 932 | function Utils() {} 933 | 934 | _prototypeProperties(Utils, { 935 | _indexToRgbString: { 936 | value: function IndexToRgbString(i) { 937 | var color; 938 | if (i % 3 === 0) { 939 | color = "#0000ff"; 940 | } else if (i % 2 === 0) { 941 | color = "#00ff00"; 942 | } else { 943 | color = "#ff0000"; 944 | } 945 | return color; 946 | }, 947 | writable: true, 948 | enumerable: true, 949 | configurable: true 950 | }, 951 | _getImageArray: { 952 | 953 | /** 954 | * Get a 2d array (width x height) representing each pixel of the source as an [r,g,b,a] array. 955 | * @param sourceContext 956 | */ 957 | value: function GetImageArray(sourceContext) { 958 | var width = sourceContext.canvas.width, 959 | height = sourceContext.canvas.height, 960 | imageData = sourceContext.getImageData(0, 0, width, height), 961 | imageArray = []; 962 | 963 | for (var row = 0; row < height; row++) { 964 | imageArray.push([]); 965 | 966 | for (var col = 0; col < width; col++) { 967 | var pixel = [], 968 | position = row * width * 4 + col * 4; 969 | 970 | for (var part = 0; part < 4; part++) { 971 | pixel[part] = imageData.data[position + part]; 972 | } 973 | 974 | imageArray[row].push(pixel); 975 | } 976 | } 977 | 978 | return imageArray; 979 | }, 980 | writable: true, 981 | enumerable: true, 982 | configurable: true 983 | }, 984 | _getWorkingArray: { 985 | 986 | /** 987 | * Create a 2d array with the same dimensions as the image, but filled with "null" pixels that 988 | * will get filled in when a pathFinder visits each pixel. Allows multiple pathFinders to 989 | * communicate which pixels have been covered. 990 | * 991 | * @param sourceContext 992 | * @returns {Array} 993 | * @private 994 | */ 995 | value: function GetWorkingArray(sourceContext) { 996 | var width = sourceContext.canvas.width, 997 | height = sourceContext.canvas.height, 998 | workingArray = []; 999 | 1000 | for (var row = 0; row < height; row++) { 1001 | workingArray.push([]); 1002 | 1003 | for (var col = 0; col < width; col++) { 1004 | workingArray[row].push([false, false, false]); 1005 | } 1006 | } 1007 | 1008 | return workingArray; 1009 | }, 1010 | writable: true, 1011 | enumerable: true, 1012 | configurable: true 1013 | }, 1014 | _getOutputDimensions: { 1015 | value: function GetOutputDimensions(image, size) { 1016 | var width, height; 1017 | 1018 | if (size === "original") { 1019 | width = image.width; 1020 | height = image.height; 1021 | } else { 1022 | var container = image.parentNode, 1023 | ratioW = container.clientWidth / image.width, 1024 | ratioH = container.clientHeight / image.height, 1025 | smallerRatio = ratioH <= ratioW ? ratioH : ratioW; 1026 | 1027 | width = image.width * smallerRatio; 1028 | height = image.height * smallerRatio; 1029 | } 1030 | 1031 | return { 1032 | width: width, 1033 | height: height 1034 | }; 1035 | }, 1036 | writable: true, 1037 | enumerable: true, 1038 | configurable: true 1039 | } 1040 | }); 1041 | 1042 | return Utils; 1043 | })(); 1044 | })(window); 1045 | -------------------------------------------------------------------------------- /dist/chromata.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"use strict";var i=function(t,e,i){e&&Object.defineProperties(t,e),i&&Object.defineProperties(t.prototype,i)},n=function(){function t(t){var i,n=this,r=arguments[1]===e?{}:arguments[1],o=document.createElement("canvas"),s=o.getContext("2d"),a=document.createElement("canvas"),u=a.getContext("2d"),l=new Image,c=!1;o.setAttribute("id","chromataCanvas"),this.options=this._mergeOptions(r),l.crossOrigin="Anonymous",l.addEventListener("load",function(){i=h._getOutputDimensions(t,n.options.outputSize),a.width=o.width=i.width,a.height=o.height=i.height,u.drawImage(l,0,0,i.width,i.height),n.dimensions=i,n.imageArray=h._getImageArray(u),n.workingArray=h._getWorkingArray(u),c=!0}),l.src=t.src,this.loader=function(t){c?t():setTimeout(function(){return n.loader(t)},50)},this.imageArray=[],this.sourceImageElement=t,this.sourceContext=u,this.renderContext=s,this.isRunning=!1,this.iterationCount=0}return i(t,null,{start:{value:function(){var t=this;this.loader(function(){t.isRunning=!0,void 0===t._tick?t._run():t._tick()})},writable:!0,enumerable:!0,configurable:!0},stop:{value:function(){return this.isRunning=!1,this.iterationCount},writable:!0,enumerable:!0,configurable:!0},toggle:{value:function(){return this.isRunning?this.stop():this.start()},writable:!0,enumerable:!0,configurable:!0},reset:{value:function(){this.isRunning=!1,this._tick=e,cancelAnimationFrame(this.raf),this.renderContext.clearRect(0,0,this.dimensions.width,this.dimensions.height),this.workingArray=h._getWorkingArray(this.sourceContext),this._removeRenderCanvas()},writable:!0,enumerable:!0,configurable:!0},_mergeOptions:{value:function(t){var e={colorMode:"color",compositeOperation:"lighten",iterationLimit:0,key:"low",lineWidth:2,lineMode:"smooth",origin:["bottom"],outputSize:"original",pathFinderCount:30,speed:7,turningAngle:Math.PI},i={};for(var n in e)e.hasOwnProperty(n)&&(i[n]=t[n]||e[n]);return i.origin=i.origin.constructor===Array?i.origin:e.origin,i.pathFinderCount=this._limitToRange(i.pathFinderCount,1,1e4),i.lineWidth=this._limitToRange(i.lineWidth,1,100),i.speed=this._limitToRange(i.speed,1,100),i.turningAngle=this._limitToRange(i.turningAngle,.1,10),i},writable:!0,enumerable:!0,configurable:!0},_limitToRange:{value:function(t,e,i){return Math.min(Math.max(t,e),i)},writable:!0,enumerable:!0,configurable:!0},_appendRenderCanvas:{value:function(){var t=this.sourceImageElement.parentNode;this.sourceImageElement.style.display="none",t.insertBefore(this.renderContext.canvas,this.sourceImageElement.nextSibling)},writable:!0,enumerable:!0,configurable:!0},_removeRenderCanvas:{value:function(){this.sourceImageElement.style.display="",this.renderContext.canvas.parentNode.removeChild(this.renderContext.canvas)},writable:!0,enumerable:!0,configurable:!0},_run:{value:function(){var t=this,e=[],i=this._initPathFinders(),n={colorMode:this.options.colorMode,lineWidth:this.options.lineWidth,lineMode:this.options.lineMode,speed:this.options.speed};this._appendRenderCanvas(),this.renderContext.globalCompositeOperation=this.options.compositeOperation,i.forEach(function(i){e.push(new a(t.renderContext,i,n))}),this._tick=function(){0=2*this.options.speed&&(this.context.beginPath(),this.context.arc(t[0],t[1],this.options.lineWidth,0,2*Math.PI,!1),this.context.fillStyle=this._getStrokeColor(t[2]),this.context.fill(),this.currentPoint=t))},writable:!0,enumerable:!0,configurable:!0},_getLineLength:{value:function(t,e){var i=e[0]-t[0],n=e[1]-t[1];return Math.round(Math.sqrt(i*i+n*n))},writable:!0,enumerable:!0,configurable:!0},_createGradient:{value:function(t,e,i,n){var r=this.context.createLinearGradient(t[0],t[1],e[0],e[1]);return r.addColorStop(0,this._getStrokeColor(i)),r.addColorStop(1,this._getStrokeColor(n)),r},writable:!0,enumerable:!0,configurable:!0},_getStrokeColor:{value:function(t){return"color"===this.options.colorMode?"rgba("+(0!==this.color.r?t:0)+", "+(0!==this.color.g?t:0)+", "+(0!==this.color.b?t:0)+", 1)":"rgba("+t+", "+t+", "+t+", 1)"},writable:!0,enumerable:!0,configurable:!0}}),t}(),h=function(){function t(){}return i(t,{_indexToRgbString:{value:function(t){return t%3==0?"#0000ff":t%2==0?"#00ff00":"#ff0000"},writable:!0,enumerable:!0,configurable:!0},_getImageArray:{value:function(t){for(var e=t.canvas.width,i=t.canvas.height,n=t.getImageData(0,0,e,i),r=[],o=0;o 2 | 3 | 4 | 5 | Chromata distribution file test page 6 | 7 | 8 | 9 | 10 | 11 | 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chromata", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "src/index.html", 6 | "dependencies": {}, 7 | "devDependencies": { 8 | "babel-cli": "^6.26.0", 9 | "babel-core": "^6.26.0", 10 | "babel-loader": "^7.1.2", 11 | "babel-preset-env": "^1.6.0", 12 | "copy-webpack-plugin": "^4.0.1", 13 | "rimraf": "^2.6.2", 14 | "webpack": "^3.5.6" 15 | }, 16 | "scripts": { 17 | "watch": "rimraf ./build && webpack -w" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/michaelbromley/chromata.git" 22 | }, 23 | "author": "Michael Bromley", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/michaelbromley/chromata/issues" 27 | }, 28 | "homepage": "https://github.com/michaelbromley/chromata" 29 | } 30 | -------------------------------------------------------------------------------- /src/assets/images/E-_GitHub_chromata_build_assets_images_emporer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/chromata/c360d58f8f1b6c172677f85cb4384f38eeb0ebda/src/assets/images/E-_GitHub_chromata_build_assets_images_emporer.png -------------------------------------------------------------------------------- /src/assets/images/city.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/chromata/c360d58f8f1b6c172677f85cb4384f38eeb0ebda/src/assets/images/city.jpg -------------------------------------------------------------------------------- /src/assets/images/emperor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/chromata/c360d58f8f1b6c172677f85cb4384f38eeb0ebda/src/assets/images/emperor.png -------------------------------------------------------------------------------- /src/assets/images/face.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/chromata/c360d58f8f1b6c172677f85cb4384f38eeb0ebda/src/assets/images/face.jpg -------------------------------------------------------------------------------- /src/assets/images/lightning.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/chromata/c360d58f8f1b6c172677f85cb4384f38eeb0ebda/src/assets/images/lightning.jpg -------------------------------------------------------------------------------- /src/assets/images/nyc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/chromata/c360d58f8f1b6c172677f85cb4384f38eeb0ebda/src/assets/images/nyc.jpg -------------------------------------------------------------------------------- /src/assets/images/rome.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/chromata/c360d58f8f1b6c172677f85cb4384f38eeb0ebda/src/assets/images/rome.jpg -------------------------------------------------------------------------------- /src/assets/images/severin.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/chromata/c360d58f8f1b6c172677f85cb4384f38eeb0ebda/src/assets/images/severin.JPG -------------------------------------------------------------------------------- /src/assets/images/tiny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/chromata/c360d58f8f1b6c172677f85cb4384f38eeb0ebda/src/assets/images/tiny.png -------------------------------------------------------------------------------- /src/assets/images/tree.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelbromley/chromata/c360d58f8f1b6c172677f85cb4384f38eeb0ebda/src/assets/images/tree.jpg -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Chromata: build test page 6 | 7 | 20 | 21 | 22 |
23 | 24 |
25 |
26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/scripts/chromata.js: -------------------------------------------------------------------------------- 1 | import Utils from './utils'; 2 | import PathFinder from './pathFinder'; 3 | import PathRenderer from './pathRenderer'; 4 | 5 | 6 | export default class Chromata { 7 | 8 | constructor(imageElement, options = {}) { 9 | var renderCanvas = document.createElement('canvas'), 10 | renderContext = renderCanvas.getContext('2d'), 11 | sourceCanvas = document.createElement('canvas'), 12 | sourceContext = sourceCanvas.getContext('2d'), 13 | image = new Image(), 14 | dimensions, 15 | ready = false; 16 | renderCanvas.setAttribute("id", "chromataCanvas"); 17 | 18 | this.options = this._mergeOptions(options); 19 | 20 | image.crossOrigin = "Anonymous"; 21 | image.addEventListener('load', () => { 22 | dimensions = Utils._getOutputDimensions(imageElement, this.options.outputSize); 23 | sourceCanvas.width = renderCanvas.width = dimensions.width; 24 | sourceCanvas.height = renderCanvas.height = dimensions.height; 25 | sourceContext.drawImage(image, 0, 0, dimensions.width, dimensions.height); 26 | 27 | this.dimensions = dimensions; 28 | this.imageArray = Utils._getImageArray(sourceContext); 29 | this.workingArray = Utils._getWorkingArray(sourceContext); 30 | 31 | ready = true; 32 | }); 33 | image.src = imageElement.src; 34 | 35 | this.loader = callback => { 36 | if (!ready) { 37 | setTimeout(() => this.loader(callback), 50); 38 | } else { 39 | callback(); 40 | } 41 | }; 42 | 43 | this.imageArray = []; 44 | this.sourceImageElement = imageElement; 45 | this.sourceContext = sourceContext; 46 | this.renderContext = renderContext; 47 | this.isRunning = false; 48 | this.iterationCount = 0; 49 | } 50 | 51 | /** 52 | * Start the animation. 53 | */ 54 | start() { 55 | this.loader(() => { 56 | 57 | this.isRunning = true; 58 | 59 | if (typeof this._tick === 'undefined') { 60 | this._run(); 61 | } else { 62 | this._tick(); 63 | } 64 | }); 65 | } 66 | 67 | /** 68 | * Stop the animation. Returns the current iteration count. 69 | * @returns {number} 70 | */ 71 | stop() { 72 | this.isRunning = false; 73 | return this.iterationCount; 74 | } 75 | 76 | /** 77 | * Start/stop the animation. If stopping, return the current iteration count. 78 | * @returns {*} 79 | */ 80 | toggle() { 81 | if (this.isRunning) { 82 | return this.stop(); 83 | } else { 84 | return this.start(); 85 | } 86 | } 87 | 88 | /** 89 | * Clear the canvas and set the animation back to the start. 90 | */ 91 | reset() { 92 | this.isRunning = false; 93 | this._tick = undefined; 94 | cancelAnimationFrame(this.raf); 95 | this.renderContext.clearRect(0, 0, this.dimensions.width, this.dimensions.height); 96 | this.workingArray = Utils._getWorkingArray(this.sourceContext); 97 | this._removeRenderCanvas(); 98 | } 99 | 100 | /** 101 | * Merge any user-supplied config options with the defaults and perform some validation. 102 | * @param options 103 | * @private 104 | */ 105 | _mergeOptions(options) { 106 | 107 | var defaults = { 108 | colorMode: 'color', 109 | compositeOperation: 'lighten', 110 | iterationLimit: 0, 111 | key: 'low', 112 | lineWidth: 2, 113 | lineMode: 'smooth', 114 | origin: ['bottom'], 115 | outputSize: 'original', 116 | pathFinderCount: 30, 117 | speed: 7, 118 | turningAngle: Math.PI 119 | }; 120 | 121 | var merged = {}; 122 | 123 | for(var prop in defaults) { 124 | if (defaults.hasOwnProperty(prop)) { 125 | merged[prop] = options[prop] || defaults[prop]; 126 | } 127 | } 128 | 129 | // some validation 130 | merged.origin = merged.origin.constructor === Array ? merged.origin : defaults.origin; 131 | merged.pathFinderCount = this._limitToRange(merged.pathFinderCount, 1, 10000); 132 | merged.lineWidth = this._limitToRange(merged.lineWidth, 1, 100); 133 | merged.speed = this._limitToRange(merged.speed, 1, 100); 134 | merged.turningAngle = this._limitToRange(merged.turningAngle, 0.1, 10); 135 | 136 | return merged; 137 | } 138 | 139 | _limitToRange(val, low, high) { 140 | return Math.min(Math.max(val, low), high); 141 | } 142 | 143 | /** 144 | * Hide the source image element and append the render canvas directly after it in the DOM. 145 | * @private 146 | */ 147 | _appendRenderCanvas() { 148 | var parentElement = this.sourceImageElement.parentNode; 149 | 150 | this.sourceImageElement.style.display = 'none'; 151 | parentElement.insertBefore(this.renderContext.canvas, this.sourceImageElement.nextSibling); 152 | } 153 | 154 | /** 155 | * Unhide the source image and remove the render canvas from the DOM. 156 | * @private 157 | */ 158 | _removeRenderCanvas() { 159 | this.sourceImageElement.style.display = ''; 160 | this.renderContext.canvas.parentNode.removeChild(this.renderContext.canvas); 161 | } 162 | 163 | /** 164 | * Set up the pathfinders and renderers and get the animation going. 165 | * @private 166 | */ 167 | _run() { 168 | 169 | var renderers = [], 170 | pathFinders = this._initPathFinders(), 171 | renderOptions = { 172 | colorMode: this.options.colorMode, 173 | lineWidth: this.options.lineWidth, 174 | lineMode: this.options.lineMode, 175 | speed: this.options.speed 176 | }; 177 | 178 | this._appendRenderCanvas(); 179 | 180 | this.renderContext.globalCompositeOperation = this.options.compositeOperation; 181 | 182 | pathFinders.forEach((pathFinder) => { 183 | renderers.push(new PathRenderer(this.renderContext, pathFinder, renderOptions)); 184 | }); 185 | 186 | this._tick = () => { 187 | 188 | if (0 < this.options.iterationLimit && this.options.iterationLimit <= this.iterationCount) { 189 | this.isRunning = false; 190 | this.options.iterationLimit = 0; 191 | } 192 | 193 | renderers.forEach(renderer => renderer.drawNextLine()); 194 | this.iterationCount ++; 195 | 196 | if (this.isRunning) { 197 | this.raf = requestAnimationFrame(this._tick); 198 | } 199 | }; 200 | 201 | this._tick(); 202 | } 203 | 204 | /** 205 | * Create the pathfinders 206 | * @returns {Array} 207 | * @private 208 | */ 209 | _initPathFinders() { 210 | var pathFinders = [], 211 | count = this.options.pathFinderCount, 212 | origins = this.options.origin, 213 | pathFindersPerOrigin = count / origins.length, 214 | options = { 215 | speed: this.options.speed, 216 | turningAngle: this.options.turningAngle, 217 | key: this.options.key 218 | }; 219 | 220 | if (-1 < origins.indexOf('bottom')) { 221 | this._seedBottom(pathFindersPerOrigin, pathFinders, options); 222 | } 223 | if (-1 < origins.indexOf('top')) { 224 | this._seedTop(pathFindersPerOrigin, pathFinders, options); 225 | } 226 | if (-1 < origins.indexOf('left')) { 227 | this._seedLeft(pathFindersPerOrigin, pathFinders, options); 228 | } 229 | if (-1 < origins.indexOf('right')) { 230 | this._seedRight(pathFindersPerOrigin, pathFinders, options); 231 | } 232 | 233 | origins.forEach((origin) => { 234 | const matches = origin.match(/(\d{1,3})% (\d{1,3})%/); 235 | if (matches) { 236 | this._seedPoint(pathFindersPerOrigin, pathFinders, options, matches[1], matches[2]); 237 | } 238 | }); 239 | 240 | return pathFinders; 241 | } 242 | 243 | _seedTop(count, pathFinders, options) { 244 | var width = this.dimensions.width, 245 | unit = width / count, 246 | xPosFn = i => unit * i - unit / 2, 247 | yPosFn = () => this.options.speed; 248 | 249 | options.startingVelocity = [0, this.options.speed]; 250 | this._seedCreateLoop(count, pathFinders, xPosFn, yPosFn, options); 251 | } 252 | 253 | _seedBottom(count, pathFinders, options) { 254 | var width = this.dimensions.width, 255 | height = this.dimensions.height, 256 | unit = width / count, 257 | xPosFn = i => unit * i - unit / 2, 258 | yPosFn = () => height - this.options.speed; 259 | 260 | options.startingVelocity = [0, -this.options.speed]; 261 | this._seedCreateLoop(count, pathFinders, xPosFn, yPosFn, options); 262 | } 263 | 264 | _seedLeft(count, pathFinders, options) { 265 | var height = this.dimensions.height, 266 | unit = height / count, 267 | xPosFn = () => this.options.speed, 268 | yPosFn = i => unit * i - unit / 2; 269 | 270 | options.startingVelocity = [this.options.speed, 0]; 271 | this._seedCreateLoop(count, pathFinders, xPosFn, yPosFn, options); 272 | } 273 | 274 | _seedRight(count, pathFinders, options) { 275 | var width = this.dimensions.width, 276 | height = this.dimensions.height, 277 | unit = height / count, 278 | xPosFn = () => width - this.options.speed, 279 | yPosFn = i => unit * i - unit / 2; 280 | 281 | options.startingVelocity = [-this.options.speed, 0]; 282 | this._seedCreateLoop(count, pathFinders, xPosFn, yPosFn, options); 283 | } 284 | 285 | _seedPoint(count, pathFinders, options, xPc, yPc) { 286 | var xPos = Math.floor(this.dimensions.width * xPc / 100), 287 | yPos = Math.floor(this.dimensions.height * yPc / 100); 288 | 289 | for (let i = 1; i < count + 1; i++) { 290 | let color = Utils._indexToRgbString(i), 291 | direction = i % 4; 292 | 293 | switch (direction) { 294 | case 0: 295 | options.startingVelocity = [-this.options.speed, 0]; 296 | break; 297 | case 1: 298 | options.startingVelocity = [0, this.options.speed]; 299 | break; 300 | case 2: 301 | options.startingVelocity = [this.options.speed, 0]; 302 | break; 303 | case 3: 304 | options.startingVelocity = [0, -this.options.speed]; 305 | break; 306 | } 307 | 308 | pathFinders.push(new PathFinder(this.imageArray, this.workingArray, color, xPos, yPos, options)); 309 | } 310 | } 311 | 312 | _seedCreateLoop(count, pathFinders, xPosFn, yPosFn, options) { 313 | for (let i = 1; i < count + 1; i++) { 314 | let color = Utils._indexToRgbString(i), 315 | xPos = xPosFn(i), 316 | yPos = yPosFn(i); 317 | 318 | pathFinders.push(new PathFinder(this.imageArray, this.workingArray, color, xPos, yPos, options)); 319 | } 320 | } 321 | } 322 | 323 | window.Chromata = Chromata; 324 | -------------------------------------------------------------------------------- /src/scripts/init.js: -------------------------------------------------------------------------------- 1 | import Chromata from './chromata'; 2 | 3 | const imageUrl = 'assets/images/face.jpg'; 4 | 5 | var image = document.querySelector('#image'), 6 | chromata; 7 | 8 | chromata = new Chromata(image, { 9 | pathFinderCount: 300, 10 | speed: 9, 11 | turningAngle: Math.PI/2, 12 | colorMode: 'color', 13 | lineWidth: 4, 14 | lineMode: 'square', 15 | compositeOperation: 'saturation', 16 | origin: ['50% 50%'], 17 | outputSize: 'container', // original, container 18 | key: 'low', 19 | backgroundColor: 'hsla(34, 70%, 70%, 0)' 20 | }); 21 | chromata.start(); 22 | 23 | 24 | document.querySelector('#toggle').addEventListener('click', e => { 25 | var count = chromata.toggle(); 26 | console.log('iterations: ' + count); 27 | }); 28 | 29 | document.querySelector('#reset').addEventListener('click', e => { 30 | chromata.reset(); 31 | }); 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/scripts/pathFinder.js: -------------------------------------------------------------------------------- 1 | import PathQueue from './pathQueue'; 2 | 3 | const MAX = 255; 4 | 5 | export default class PathFinder { 6 | 7 | constructor(pixelArray, workingArray, targetColor, initX = 0, initY = 0, options = {}) { 8 | this.pixelArray = pixelArray; 9 | this.workingArray = workingArray; 10 | this.arrayWidth = pixelArray[0].length; 11 | this.arrayHeight = pixelArray.length; 12 | this.x = Math.round(initX); 13 | this.y = Math.round(initY); 14 | this.options = options; 15 | this.pathQueue = new PathQueue(10); 16 | this.velocity = options.startingVelocity; 17 | 18 | this.targetColor = typeof targetColor === 'string' ? this._hexToRgb(targetColor) : targetColor; 19 | this.rgbIndex = this._getRgbIndex(this.targetColor); 20 | 21 | if (this.options.key === 'low') { 22 | this.comparatorFn = (distance, closest) => { 23 | return 0 < distance && distance < closest; 24 | }; 25 | } else { 26 | this.comparatorFn = (distance, closest) => { 27 | return closest < distance && distance < MAX; 28 | }; 29 | } 30 | } 31 | 32 | /** 33 | * Get next coordinate point in path. 34 | * 35 | * @returns {[int, int, int]} 36 | */ 37 | getNextPoint() { 38 | 39 | var result, 40 | i = 0, 41 | limit = 5; // prevent an infinite loop 42 | 43 | do { 44 | result = this._getNextPixel(); 45 | i++; 46 | } while(i <= limit && result.isPristine === false); 47 | 48 | return result.nextPixel; 49 | } 50 | 51 | /** 52 | * Algorithm for finding the next point by picking the closest match out of an arc-shaped array of possible pixels 53 | * arranged pointing in the direction of velocity. 54 | * 55 | * @returns {{nextPixel: [int, int, int], isPristine: boolean}} 56 | * @private 57 | */ 58 | _getNextPixel() { 59 | var theta = this._getVelocityAngle(), 60 | isPristine, 61 | closestColor = this.options.key === 'low' ? 100000 : 0, 62 | nextPixel, 63 | defaultNextPixel, 64 | arcSize = this.options.turningAngle, 65 | radius = Math.round(Math.sqrt(Math.pow(this.velocity[0], 2) + Math.pow(this.velocity[1], 2))), 66 | sampleSize = 4; // how many surrounding pixels to test for next point 67 | 68 | for(let angle = theta - arcSize / 2 , deviance = -sampleSize/2; angle <= theta + arcSize / 2; angle += arcSize / sampleSize, deviance ++) { 69 | let x = this.x + Math.round(radius * Math.cos(angle)), 70 | y = this.y + Math.round(radius * Math.sin(angle)), 71 | colorDistance = MAX; 72 | 73 | if (this._isInRange(x, y)) { 74 | 75 | let visited = this.workingArray[y][x][this.rgbIndex], 76 | currentPixel = this.pixelArray[y][x], 77 | alpha = currentPixel[3]; 78 | 79 | colorDistance = this._getColorDistance(currentPixel); 80 | 81 | if (this.comparatorFn(colorDistance, closestColor) && !visited && alpha === MAX) { 82 | nextPixel = [x, y, MAX - colorDistance]; 83 | closestColor = colorDistance; 84 | } 85 | } 86 | 87 | if (deviance === 0) { 88 | let pa = this.pixelArray; 89 | if (pa[y] && pa[y][x] && pa[y][x][3] === MAX) { 90 | defaultNextPixel = [x, y, MAX - colorDistance]; 91 | } else { 92 | defaultNextPixel = this.pathQueue.get(-2); 93 | } 94 | } 95 | } 96 | 97 | isPristine = typeof nextPixel !== 'undefined'; 98 | nextPixel = nextPixel || defaultNextPixel; 99 | 100 | if (nextPixel) { 101 | this.velocity = [nextPixel[0] - this.x, nextPixel[1] - this.y]; 102 | this.y = nextPixel[1]; 103 | this.x = nextPixel[0]; 104 | this._updateWorkingArray(nextPixel[1], nextPixel[0]); 105 | this.pathQueue.put(nextPixel); 106 | } 107 | 108 | return { 109 | nextPixel: nextPixel, 110 | isPristine: isPristine 111 | }; 112 | } 113 | 114 | /** 115 | * Get an [r, g, b] array of the target color. 116 | * @returns {{r: *, g: *, b: *}} 117 | */ 118 | getColor() { 119 | return { 120 | r: this.targetColor[0], 121 | g: this.targetColor[1], 122 | b: this.targetColor[2] 123 | }; 124 | } 125 | 126 | /** 127 | * Get the angle indicated by the velocity vector, correcting for the case that the angle would 128 | * take the pathfinder off the image canvas, in which case the angle will be set towards the 129 | * centre of the canvas. 130 | * 131 | * @returns {*} 132 | * @private 133 | */ 134 | _getVelocityAngle() { 135 | var projectedX = this.x + this.velocity[0], 136 | projectedY = this.y + this.velocity[1], 137 | margin = this.options.speed, 138 | dy = this.y + this.velocity[1] - this.y, 139 | dx = this.x + this.velocity[0] - this.x, 140 | angle; 141 | 142 | // has it gone out of bounds on the x axis? 143 | if (projectedX <= margin || this.arrayWidth - margin <= projectedX) { 144 | dx *= -1; 145 | } 146 | 147 | // has it gone out of bounds on the y axis? 148 | if (projectedY <= margin || this.arrayHeight - margin <= projectedY) { 149 | dy *= -1; 150 | } 151 | 152 | angle = Math.atan2(dy, dx); 153 | return angle; 154 | } 155 | 156 | /** 157 | * From http://stackoverflow.com/a/5624139/772859 158 | * @param hex 159 | * @returns {{r: Number, g: Number, b: Number}} 160 | * @private 161 | */ 162 | _hexToRgb(hex) { 163 | // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") 164 | var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; 165 | hex = hex.replace(shorthandRegex, function(m, r, g, b) { 166 | return r + r + g + g + b + b; 167 | }); 168 | 169 | var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); 170 | return result ? [ 171 | parseInt(result[1], 16), 172 | parseInt(result[2], 16), 173 | parseInt(result[3], 16) 174 | ] : null; 175 | } 176 | 177 | _getColorDistance(pixel) { 178 | return MAX - pixel[this.rgbIndex]; 179 | } 180 | 181 | /** 182 | * Return true if the x, y points lie within the image dimensions. 183 | * @param x 184 | * @param y 185 | * @returns {boolean} 186 | * @private 187 | */ 188 | _isInRange(x, y) { 189 | return 0 < x && 190 | x < this.arrayWidth && 191 | 0 < y && 192 | y < this.arrayHeight; 193 | } 194 | 195 | _updateWorkingArray(row, col) { 196 | this.workingArray[row][col][this.rgbIndex] = true; 197 | } 198 | 199 | _getRgbIndex(targetColorArray) { 200 | var i; 201 | for (i = 0; i < 2; i++) { 202 | if (targetColorArray[i] !== 0) { 203 | break; 204 | } 205 | } 206 | 207 | return i; 208 | } 209 | } -------------------------------------------------------------------------------- /src/scripts/pathQueue.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implementation of a queue of a fixed size. 3 | */ 4 | export default class PathQueue { 5 | 6 | constructor(size) { 7 | this.queue = []; 8 | this.size = size; 9 | } 10 | 11 | /** 12 | * Put a new item in the queue. If this causes the queue to exceed its size limit, the oldest 13 | * item will be discarded. 14 | * @param item 15 | */ 16 | put(item) { 17 | this.queue.push(item); 18 | if (this.size < this.queue.length) { 19 | this.queue.shift(); 20 | } 21 | } 22 | 23 | /** 24 | * Get an item from the queue, specified by index. 0 gets the oldest item in the queue, 1 the second oldest etc. 25 | * -1 gets the newest item, -2 the second newest etc. 26 | * 27 | * @param index 28 | * @returns {*} 29 | */ 30 | get(index = 0) { 31 | var length = this.queue.length; 32 | if (0 <= index && index <= length) { 33 | return this.queue[index]; 34 | } else if (index < 0 && Math.abs(index) <= length) { 35 | return this.queue[length + index]; 36 | } else { 37 | return undefined; 38 | } 39 | } 40 | 41 | contains(item) { 42 | var matches = this.queue.filter((point) => { 43 | return point[0] === item[0] && point[1] === item[1]; 44 | }); 45 | 46 | return 0 < matches.length; 47 | } 48 | } -------------------------------------------------------------------------------- /src/scripts/pathRenderer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Renders the points created by a Pathfinder 3 | */ 4 | export default class PathRenderer { 5 | 6 | constructor(context, pathFinder, options) { 7 | this.context = context; 8 | this.pathFinder = pathFinder; 9 | this.options = options; 10 | this.color = pathFinder.getColor(); 11 | } 12 | 13 | drawNextLine() { 14 | if (this.options.lineMode === 'smooth') { 15 | this._drawLineSmooth(); 16 | } else if (this.options.lineMode === 'square') { 17 | this._drawLineSquare(); 18 | } else { 19 | this._drawPoint(); 20 | } 21 | } 22 | 23 | _drawLineSmooth() { 24 | var midX, 25 | midY, 26 | midColor, 27 | lineLength, 28 | nextPoint = this.pathFinder.getNextPoint(this.context); 29 | 30 | if (nextPoint) { 31 | 32 | if (typeof this.currentPoint === 'undefined') { 33 | this.currentPoint = nextPoint; 34 | } 35 | if (typeof this.controlPoint === 'undefined') { 36 | this.controlPoint = nextPoint; 37 | } 38 | 39 | midX = Math.round((this.controlPoint[0] + nextPoint[0]) / 2); 40 | midY = Math.round((this.controlPoint[1] + nextPoint[1]) / 2); 41 | midColor = Math.floor((this.currentPoint[2] + nextPoint[2]) / 2); 42 | lineLength = this._getLineLength(this.currentPoint, nextPoint); 43 | 44 | if (lineLength <= this.options.speed * 3) { 45 | let grad, 46 | startColorValue = this.currentPoint[2], 47 | endColorValue = nextPoint[2]; 48 | 49 | grad = this._createGradient(this.currentPoint, nextPoint, startColorValue, endColorValue); 50 | this.context.strokeStyle = grad; 51 | 52 | this.context.lineWidth = this.options.lineWidth; 53 | this.context.lineCap = 'round'; 54 | this.context.beginPath(); 55 | 56 | this.context.moveTo(this.currentPoint[0], this.currentPoint[1]); 57 | this.context.quadraticCurveTo(this.controlPoint[0], this.controlPoint[1], midX, midY); 58 | this.context.stroke(); 59 | } 60 | 61 | this.currentPoint = [midX, midY, midColor]; 62 | this.controlPoint = nextPoint; 63 | } 64 | } 65 | 66 | _drawLineSquare() { 67 | var lineLength, 68 | nextPoint = this.pathFinder.getNextPoint(this.context); 69 | 70 | if(nextPoint) { 71 | 72 | if (typeof this.currentPoint === 'undefined') { 73 | this.currentPoint = nextPoint; 74 | } 75 | 76 | lineLength = this._getLineLength(this.currentPoint, nextPoint); 77 | 78 | if (lineLength <= this.options.speed + 1) { 79 | let grad, 80 | startColorValue = this.currentPoint[2], 81 | endColorValue = nextPoint[2]; 82 | 83 | grad = this._createGradient(this.currentPoint, nextPoint, startColorValue, endColorValue); 84 | 85 | this.context.strokeStyle = grad; 86 | this.context.lineWidth = this.options.lineWidth; 87 | this.context.lineCap = 'round'; 88 | this.context.beginPath(); 89 | 90 | this.context.moveTo(this.currentPoint[0], this.currentPoint[1]); 91 | this.context.lineTo(nextPoint[0], nextPoint[1]); 92 | this.context.stroke(); 93 | } 94 | this.currentPoint = nextPoint; 95 | } 96 | } 97 | 98 | _drawPoint() { 99 | var lineLength, 100 | nextPoint = this.pathFinder.getNextPoint(this.context); 101 | 102 | if(nextPoint) { 103 | 104 | if (typeof this.currentPoint === 'undefined') { 105 | this.currentPoint = nextPoint; 106 | } 107 | 108 | lineLength = this._getLineLength(this.currentPoint, nextPoint); 109 | 110 | if (lineLength >= this.options.speed * 2) { 111 | this.context.beginPath(); 112 | 113 | this.context.arc(nextPoint[0], nextPoint[1], this.options.lineWidth , 0, 2 * Math.PI, false); 114 | this.context.fillStyle = this._getStrokeColor(nextPoint[2]); 115 | this.context.fill(); 116 | 117 | this.currentPoint = nextPoint; 118 | } 119 | } 120 | } 121 | 122 | _getLineLength(p1, p2) { 123 | var dx = p2[0] - p1[0]; 124 | var dy = p2[1] - p1[1]; 125 | return Math.round(Math.sqrt(dx*dx + dy*dy)); 126 | } 127 | 128 | _createGradient(p1, p2, color1, color2) { 129 | var grad = this.context.createLinearGradient(p1[0], p1[1], p2[0], p2[1]); 130 | grad.addColorStop(0, this._getStrokeColor(color1)); 131 | grad.addColorStop(1, this._getStrokeColor(color2)); 132 | return grad; 133 | } 134 | 135 | /** 136 | * Get an rgba color string based on the color value and the pathRenderer's color and color mode. 137 | * 138 | * @param colorValue 139 | * @returns {*} 140 | * @private 141 | */ 142 | _getStrokeColor(colorValue) { 143 | var colorString; 144 | 145 | if (this.options.colorMode === 'color') { 146 | colorString = 'rgba(' + 147 | (this.color.r !== 0 ? colorValue : 0) + ', ' + 148 | (this.color.g !== 0 ? colorValue : 0) + ', ' + 149 | (this.color.b !== 0 ? colorValue : 0) + ', ' + 1 + ')'; 150 | } else { 151 | // greyscale 152 | colorString = 'rgba(' + colorValue + ', ' + colorValue + ', ' + colorValue + ', ' + 1 + ')'; 153 | } 154 | 155 | return colorString; 156 | } 157 | } -------------------------------------------------------------------------------- /src/scripts/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Static utilities class containing helper functions 3 | */ 4 | export default class Utils { 5 | 6 | static _indexToRgbString(i) { 7 | var color; 8 | if (i % 3 === 0) { 9 | color = '#0000ff'; 10 | } else if (i % 2 === 0) { 11 | color = '#00ff00'; 12 | } else { 13 | color = '#ff0000'; 14 | } 15 | return color; 16 | } 17 | 18 | /** 19 | * Get a 2d array (width x height) representing each pixel of the source as an [r,g,b,a] array. 20 | * @param sourceContext 21 | */ 22 | static _getImageArray(sourceContext) { 23 | var width = sourceContext.canvas.width, 24 | height = sourceContext.canvas.height, 25 | imageData = sourceContext.getImageData(0, 0, width, height), 26 | imageArray = []; 27 | 28 | for(let row = 0; row < height; row ++) { 29 | 30 | imageArray.push([]); 31 | 32 | for(let col = 0; col < width; col ++) { 33 | let pixel = [], 34 | position = row * width * 4 + col * 4; 35 | 36 | for(let part = 0; part < 4; part ++) { 37 | pixel[part] = imageData.data[position + part]; 38 | } 39 | 40 | imageArray[row].push(pixel); 41 | } 42 | } 43 | 44 | return imageArray; 45 | } 46 | 47 | /** 48 | * Create a 2d array with the same dimensions as the image, but filled with "null" pixels that 49 | * will get filled in when a pathFinder visits each pixel. Allows multiple pathFinders to 50 | * communicate which pixels have been covered. 51 | * 52 | * @param sourceContext 53 | * @returns {Array} 54 | * @private 55 | */ 56 | static _getWorkingArray(sourceContext) { 57 | var width = sourceContext.canvas.width, 58 | height = sourceContext.canvas.height, 59 | workingArray = []; 60 | 61 | for(let row = 0; row < height; row ++) { 62 | 63 | workingArray.push([]); 64 | 65 | for(let col = 0; col < width; col ++) { 66 | workingArray[row].push([false, false, false]); 67 | } 68 | } 69 | 70 | return workingArray; 71 | } 72 | 73 | static _getOutputDimensions(image, size) { 74 | 75 | var width, 76 | height; 77 | 78 | if (size === 'original') { 79 | width = image.width; 80 | height = image.height; 81 | } else { 82 | let container = image.parentNode, 83 | ratioW = container.clientWidth / image.width, 84 | ratioH = container.clientHeight / image.height, 85 | smallerRatio = (ratioH <= ratioW) ? ratioH : ratioW; 86 | 87 | width = image.width * smallerRatio; 88 | height = image.height * smallerRatio; 89 | } 90 | 91 | return { 92 | width: width, 93 | height: height 94 | }; 95 | } 96 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: './src/scripts/init.js', 6 | output: { 7 | path: path.resolve(__dirname, 'build'), 8 | filename: 'app.bundle.js' 9 | }, 10 | module: { 11 | rules: [ 12 | { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" } 13 | ] 14 | }, 15 | plugins: [ 16 | new CopyWebpackPlugin([ 17 | { from: './src/index.html' }, 18 | { context: './src', from: 'assets/**/*' } 19 | ]) 20 | ] 21 | }; 22 | --------------------------------------------------------------------------------