/* * XaoS.js * https://github.com/jblang/XaoS.js * * Copyright (C)2011 John B. Langston III * Copyright (C)2001, 2010 Andrea Medeghini * Copyright (C)1996, 1997 Jan Hubicka and Thomas Marsh * * Based on code from XaoS by Jan Hubicka (http://xaos.sf.net) * and from JAME by Andrea Medeghini (http://www.fractalwalk.net) * * This file is part of XaoS.js. * * XaoS.js is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * XaoS.js is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with XaoS.js. If not, see . * */ var xaos = xaos || {}; xaos.zoom = (function () { "use strict"; const USE_XAOS = true; // Whether to use zooming or recalculate every frame const USE_SYMMETRY = true; // Whether to use symmetry when possible const USE_SOLIDGUESS = true; // Whether to use solid guessing to avoid calculations const RANGES = 2; // Number of ranges to use for sizing approximation data const RANGE = 4; // Maximum distance to use for approximation const MASK = 0x7; // Mask value for maximum potential source lines const DSIZE = RANGES + 1; // Shift value for target lines const FPMUL = 64; // Multiplication factor for fixed-point representation const FPRANGE = FPMUL * RANGE; // Fixed point range of approximation const MAX_PRICE = Number.MAX_VALUE; // Maximum price of uninitialized approximation const NEW_PRICE = FPRANGE * FPRANGE; // Price of calculating a new line const GUESS_RANGE = 4; // Range to use for solid guessing /** A price entry in the approximation table * @constructor */ function Price() { this.previous = null; // Previous price calculated for the same line this.index = 0; // Index of the source for this approximation (-1 means new calculation) this.price = MAX_PRICE; // Price calculated for this line } /** A group of pixels to be moved * @constructor */ function Move() { this.length = 0; // number of pixels to move this.from = 0; // starting offset of pixel source this.to = 0; // starting offset of pixel destination } /** A single row or column of pixels in the image * @constructor */ function Line() { this.recalculate = false; // whether to recalculate this line this.dirty = false; // whether this line needs to be redrawn this.isRow = false; // whether this is a row (true) or column (false) this.index = 0; // index of row or column within the image this.symIndex = 0; // index of pixels to use for symmetry this.symTo = 0; // position of pixels this is symmetrical to this.symRef = 0; // position of pixels referring to this one this.oldPosition = 0.0; // line's old position in the fractal's complex plane this.newPosition = 0.0; // line's new position in the fractal's complex plane this.priority = 0.0; // calculation priority for this row/column } /** An image derived from an HTML5 canvas * @param canvas - the canvas used to display the image * @constructor */ function CanvasImage(canvas) { let width = canvas.clientWidth; let height = canvas.clientHeight; if (canvas.width !== width || canvas.height !== height) { canvas.width = width; canvas.height = height; } else { ctx.clearRect(0, 0, width, height); } this.canvas = canvas; this.context = canvas.getContext("2d"); this.width = canvas.width; this.height = canvas.height; this.newImageData = this.context.createImageData(this.width, this.height); this.oldImageData = this.context.createImageData(this.width, this.height); this.newBuffer = new Uint32Array(this.newImageData.data.buffer); this.oldBuffer = new Uint32Array(this.oldImageData.data.buffer); } /** Swap new and old buffers */ CanvasImage.prototype.swapBuffers = function () { var tmp = this.oldBuffer; this.oldBuffer = this.newBuffer; this.newBuffer = tmp; tmp = this.oldImageData; this.newImageData = this.oldImageData; this.oldImageData = tmp; }; /** Draw the current image */ CanvasImage.prototype.paint = function () { this.context.putImageData(this.newImageData, 0, 0); }; /** Utility function to make an array of the specified size * with the specified initial value. It will do the right thing * to create unique items, whether you pass in a prototype, a * constructor, or a primitive. * @param {number} size - the size of the array. * @param initial - the initial value for each entry. */ function makeArray(size, initial) { var i, data = []; for (i = 0; i < size; i++) { if (typeof initial === "object") { // prototype object data[i] = Object.create(initial); } else if (typeof initial === "function") { // constructor data[i] = new initial(); } else { // primitive data[i] = initial; } } return data; } /** Container for all zoom context data for a particular canvas. * * @param image {CanvasImage} Image on which to draw the fractal. * @param fractal {FractalContext} Fractal parameters. * @constructor */ function ZoomContext(image, fractal) { var size = Math.max(image.width, image.height); this.image = image; // the image to draw the fractal on this.fractal = fractal; // the fractal formula used for the image this.columns = makeArray(image.width, Line); // columns in the fractal image this.rows = makeArray(image.height, Line); // rows in the fractal image this.sourcePos = makeArray(size + 1, 0); // fixed-point positions for source lines this.oldBest = makeArray(size, null); // best prices for previous line this.newBest = makeArray(size, null); // best prices for current line this.calcPrices = makeArray(size, Price); // prices for calculating new lines this.movePrices = makeArray(size << DSIZE, Price); // prices for approximating new lines from exsiting ones this.moveTable = makeArray(image.width + 1, Move); // table of pixels to be moved this.fillTable = makeArray(image.width + 1, Move); // table of pixels to be filled this.queue = makeArray(image.width + image.height, null); // queue of lines to calculate this.queueLength = 0; // length of the calculation queue this.startTime = 0; // time that the current frame was started this.minFPS = 60; // target FPS to maintain this.fudgeFactor = 0; // fudge factor used to achieve target FPS this.incomplete = false; // flag indicates incomplete calculation this.zooming = false; // flag indicates image is currently zooming } /** Swaps the old and new best prices in the this container. */ ZoomContext.prototype.swapBest = function () { var tmpBest = this.oldBest; this.oldBest = this.newBest; this.newBest = tmpBest; }; /** Convert fractal viewport from radius and center to x and y start to end ranges */ ZoomContext.prototype.convertArea = function () { var radius = this.fractal.region.radius; var center = this.fractal.region.center; var aspect = this.image.width / this.image.height; var size = Math.max(radius.x, radius.y * aspect); return { begin: { x: center.x - size / 2, y: (center.y - size / 2) / aspect, }, end: { x: center.x + size / 2, y: (center.y + size / 2) / aspect, }, }; }; /** Resets line of pixels for fresh calculation * * @param line - row or column of pixels * @param begin - starting fractal cooridnate * @param end - ending coordinate * @param isRow - whether this is a row or column * @returns {number} */ ZoomContext.prototype.initialize = function (lines, begin, end, isRow) { var i; var p; var step = (end - begin) / lines.length; var line = null; for (i = 0, p = begin; i < lines.length; i++, p += step) { line = lines[i]; line.recalculate = true; line.dirty = true; line.isRow = isRow; line.index = i; line.oldPosition = p; line.newPosition = p; line.symIndex = i; line.symTo = -1; line.symRef = -1; } return step; }; /** Calculate price of approximating one line from another * * @param p1 - position of first line * @param p2 - position of second line * @returns {number} - price of approximation */ function calcPrice(p1, p2) { return (p1 - p2) * (p1 - p2); } /** Calculate fixed-point representation of each line's old position * @param lines - lines to use for calculation * @param begin - beginning of floating point range * @param end - end of floating point range */ ZoomContext.prototype.calcFixedpoint = function (lines, begin, end) { var tofix = (lines.length * FPMUL) / (end - begin); var i; this.sourcePos[lines.length] = Number.MAX_VALUE; for (i = lines.length - 1; i >= 0; i--) { this.sourcePos[i] = ((lines[i].oldPosition - begin) * tofix) | 0; if (this.sourcePos[i] > this.sourcePos[i + 1]) { this.sourcePos[i] = this.sourcePos[i + 1]; } } }; /** Choose the best approximation for lines based on previous frame * * @param lines - relocation table for rows or columns * @param begin - beginning coordinate (x or y) * @param end - ending coordinate (x or y) * @param newPosition - array of newPosition coordinates on the complex plane * @returns {number} */ ZoomContext.prototype.approximate = function (lines, begin, end) { var previous = null; // pointer to previous approximation var best = null; // pointer to best approximation var line = null; // pointer to current line var price = 0; // price of current approximation var dest; // index of the current destination line var idealPos = 0; // ideal position for the current destination var maxPos = 0; // maximum valid source position of the current destination var source = 0; // index of current source line var prevBegin = 0; // index of first potential source for current destination var prevEnd = 0; // index of last potential source for current destination var currBegin = 0; // index of first potential source for next destination var flag = 0; var size = lines.length; var step = (end - begin) / size; var sourcePos = this.sourcePos; // Calculate fixed-point positions of all source lines this.calcFixedpoint(lines, begin, end); for (dest = 0, idealPos = 0; dest < size; dest++, idealPos += FPMUL) { this.swapBest(); maxPos = idealPos - FPRANGE; if (maxPos < -FPMUL) { maxPos = -FPMUL; } source = prevBegin; while (sourcePos[source] < maxPos) { source++; } currBegin = source; maxPos = idealPos + FPRANGE; // Find the previous approximation if (prevBegin !== prevEnd && source > prevBegin) { // Previous line had approximations; use them if (source < prevEnd) { previous = this.oldBest[source - 1]; } else { previous = this.oldBest[prevEnd - 1]; } price = previous.price; } else if (dest > 0) { // Previous line had no approximations // Use the price of calculating the previous line previous = this.calcPrices[dest - 1]; price = previous.price; } else { // We're on the first line; no previous prices exists previous = null; price = 0; } // Add the price for calculating this line price += NEW_PRICE; best = this.calcPrices[dest]; best.price = price; best.index = -1; best.previous = previous; // Try all possible approximations for this line and calculate the best one if (prevBegin !== prevEnd) { if (source === prevBegin) { // We're on the first line so there is no previous line if (sourcePos[source] !== sourcePos[source + 1]) { previous = this.calcPrices[dest - 1]; price = previous.price + calcPrice(sourcePos[source], idealPos); if (price < best.price) { best = this.movePrices[(source << DSIZE) + (dest & MASK)]; best.price = price; best.index = source; best.previous = previous; } } this.newBest[source++] = best; } previous = null; // Potential sources for the previous and current line overlap within // this range, so we have to calculate every possibility and find the best while (source < prevEnd) { if (sourcePos[source] !== sourcePos[source + 1]) { previous = this.oldBest[source - 1]; price = previous.price + NEW_PRICE; if (price < best.price) { best = this.movePrices[((source - 1) << DSIZE) + (dest & MASK)]; best.price = price; best.index = -1; best.previous = previous; this.newBest[source - 1] = best; } price = previous.price + calcPrice(sourcePos[source], idealPos); if (price < best.price) { best = this.movePrices[(source << DSIZE) + (dest & MASK)]; best.price = price; best.index = source; best.previous = previous; } else if (sourcePos[source] > idealPos) { this.newBest[source++] = best; break; } } this.newBest[source++] = best; } // We are past the overlapping area if (source > prevBegin) { previous = this.oldBest[source - 1]; } else { previous = this.calcPrices[dest - 1]; } price = previous.price + NEW_PRICE; if (price < best.price && source > currBegin) { best = this.movePrices[((source - 1) << DSIZE) + (dest & MASK)]; best.price = price; best.index = -1; best.previous = previous; this.newBest[source - 1] = best; } while (sourcePos[source] < maxPos) { if (sourcePos[source] !== sourcePos[source + 1]) { price = previous.price + calcPrice(sourcePos[source], idealPos); if (price < best.price) { best = this.movePrices[(source << DSIZE) + (dest & MASK)]; best.price = price; best.index = source; best.previous = previous; } else if (sourcePos[source] > idealPos) { break; } } this.newBest[source++] = best; } while (sourcePos[source] < maxPos) { this.newBest[source++] = best; } } else if (sourcePos[source] < maxPos) { if (dest > 0) { previous = this.calcPrices[dest - 1]; price = previous.price; } else { previous = null; price = 0; } while (sourcePos[source] < maxPos) { if (sourcePos[source] !== sourcePos[source + 1]) { price += calcPrice(sourcePos[source], idealPos); if (price < best.price) { best = this.movePrices[(source << DSIZE) + (dest & MASK)]; best.price = price; best.index = source; best.previous = previous; } else if (sourcePos[source] > idealPos) { break; } } this.newBest[source++] = best; } while (sourcePos[source] < maxPos) { this.newBest[source++] = best; } } prevBegin = currBegin; currBegin = prevEnd; prevEnd = source; } if (begin > lines[0].oldPosition && end < lines[size - 1].oldPosition) { flag = 1; } if (sourcePos[0] > 0 && sourcePos[size - 1] < size * FPMUL) { flag = 2; } for (dest = size - 1; dest >= 0; dest--) { line = lines[dest]; line.symTo = -1; line.symRef = -1; if (best.index < 0) { line.recalculate = true; line.dirty = true; line.symIndex = line.index; } else { line.symIndex = best.index; line.newPosition = lines[best.index].oldPosition; line.recalculate = false; line.dirty = false; } best = best.previous; } newPositions(lines, begin, end, step, flag); return step; }; /** Choose new positions for lines based on calculated prices * * @param lines * @param size * @param begin1 * @param end1 * @param step * @param newPosition * @param flag */ function newPositions(lines, begin1, end1, step, flag) { var delta = 0; var size = lines.length; var begin = 0; var end = 0; var s = -1; var e = -1; if (begin1 > end1) { begin1 = end1; } while (s < size - 1) { e = s + 1; if (lines[e].recalculate) { while (e < size) { if (!lines[e].recalculate) { break; } e++; } if (e < size) { end = lines[e].newPosition; } else { end = end1; } if (s < 0) { begin = begin1; } else { begin = lines[s].newPosition; } if (e === size && begin > end) { end = begin; } if (e - s === 2) { delta = (end - begin) * 0.5; } else { delta = (end - begin) / (e - s); } switch (flag) { case 1: for (s++; s < e; s++) { begin += delta; lines[s].newPosition = begin; lines[s].priority = 1 / (1 + Math.abs(lines[s].oldPosition - begin) * step); } break; case 2: for (s++; s < e; s++) { begin += delta; lines[s].newPosition = begin; lines[s].priority = Math.abs(lines[s].oldPosition - begin) * step; } break; default: for (s++; s < e; s++) { begin += delta; lines[s].newPosition = begin; lines[s].priority = 1.0; } break; } } s = e; } } /** Populate symmetry data into relocation table * * @param lines * @param symi * @param symPosition * @param step */ function prepareSymmetry(lines, symi, symPosition, step) { var i; var j = 0; var tmp; var abs; var distance; var newPosition; var size = lines.length; var max = size - RANGE - 1; var min = RANGE; var istart = 0; var line = null; var otherLine = null; var symj = 2 * symi - size; symPosition *= 2; if (symj < 0) { symj = 0; } distance = step * RANGE; for (i = symj; i < symi; i++) { line = lines[i]; if (line.symTo !== -1) { continue; } newPosition = line.newPosition; line.symTo = 2 * symi - i; if (line.symTo > max) { line.symTo = max; } j = line.symTo - istart > RANGE ? -RANGE : -line.symTo + istart; if (line.recalculate) { while (j < RANGE && line.symTo + j < size - 1) { tmp = symPosition - lines[line.symTo + j].newPosition; abs = Math.abs(tmp - newPosition); if (abs < distance) { if ( (i === 0 || tmp > lines[i - 1].newPosition) && tmp < lines[i + 1].newPosition ) { distance = abs; min = j; } } else if (tmp < newPosition) { break; } j++; } } else { while (j < RANGE && line.symTo + j < size - 1) { if (line.recalculate) { tmp = symPosition - lines[line.symTo + j].newPosition; abs = Math.abs(tmp - newPosition); if (abs < distance) { if ( (i === 0 || tmp > lines[i - 1].newPosition) && tmp < lines[i + 1].newPosition ) { distance = abs; min = j; } } else if (tmp < newPosition) { break; } } j++; } } line.symTo += min; otherLine = lines[line.symTo]; if ( min === RANGE || line.symTo <= symi || otherLine.symTo !== -1 || otherLine.symRef !== -1 ) { line.symTo = -1; continue; } if (!line.recalculate) { line.symTo = -1; if (otherLine.symTo !== -1 || !otherLine.recalculate) { continue; } otherLine.symIndex = line.symIndex; otherLine.symTo = i; istart = line.symTo - 1; otherLine.recalculate = false; otherLine.dirty = true; line.symRef = line.symTo; otherLine.newPosition = symPosition - line.newPosition; } else { if (otherLine.symTo !== -1) { line.symTo = -1; continue; } line.symIndex = otherLine.symIndex; istart = line.symTo - 1; line.recalculate = false; line.dirty = true; otherLine.symRef = i; line.newPosition = symPosition - otherLine.newPosition; } } } /** Optimized array copy using Duff's Device. * * @param from {Array} source array * @param fromOffset {number} offset into source array * @param to {Array} idealPos array * @param toOffset {number} offset into idealPos array * @param length {number} elements to copy */ function arrayCopy(from, fromOffset, to, toOffset, length) { var n = length % 8; while (n--) { to[toOffset++] = from[fromOffset++]; } n = (length / 8) | 0; while (n--) { to[toOffset++] = from[fromOffset++]; to[toOffset++] = from[fromOffset++]; to[toOffset++] = from[fromOffset++]; to[toOffset++] = from[fromOffset++]; to[toOffset++] = from[fromOffset++]; to[toOffset++] = from[fromOffset++]; to[toOffset++] = from[fromOffset++]; to[toOffset++] = from[fromOffset++]; } } /** Apply previously calculated symmetry to image */ ZoomContext.prototype.doSymmetry = function () { var from_offset = 0; var to_offset = 0; var i; var j = 0; var buffer = this.image.newBuffer; var bufferWidth = this.image.width; for (i = 0; i < this.rows.length; i++) { if (this.rows[i].symTo >= 0 && !this.rows[this.rows[i].symTo].dirty) { from_offset = this.rows[i].symTo * bufferWidth; arrayCopy(buffer, from_offset, buffer, to_offset, bufferWidth); this.rows[i].dirty = false; } to_offset += bufferWidth; } for (i = 0; i < this.columns.length; i++) { if ( this.columns[i].symTo >= 0 && !this.columns[this.columns[i].symTo].dirty ) { to_offset = i; from_offset = this.columns[i].symTo; for (j = 0; j < this.rows.length; j++) { buffer[to_offset] = buffer[from_offset]; to_offset += bufferWidth; from_offset += bufferWidth; } this.columns[i].dirty = false; } } }; /** Build an optimized move table based on relocation table */ ZoomContext.prototype.prepareMove = function () { var move = null; var i = 0; var j = 0; var s = 0; while (i < this.columns.length) { if (!this.columns[i].dirty) { move = this.moveTable[s]; move.to = i; move.length = 1; move.from = this.columns[i].symIndex; for (j = i + 1; j < this.columns.length; j++) { if ( this.columns[j].dirty || j - this.columns[j].symIndex !== move.to - move.from ) { break; } move.length++; } i = j; s++; } else { i++; } } move = this.moveTable[s]; move.length = 0; }; /** Execute moves defined in move table */ ZoomContext.prototype.doMove = function () { var move = null; var newOffset = 0; var oldOffset = 0; var from = 0; var to = 0; var i; var s = 0; var length = 0; var newBuffer = this.image.newBuffer; var oldBuffer = this.image.oldBuffer; var bufferWidth = this.image.width; for (i = 0; i < this.rows.length; i++) { if (!this.rows[i].dirty) { s = 0; oldOffset = this.rows[i].symIndex * bufferWidth; while ((move = this.moveTable[s]).length > 0) { from = oldOffset + move.from; to = newOffset + move.to; length = move.length; arrayCopy(oldBuffer, from, newBuffer, to, length); s++; } } newOffset += bufferWidth; } }; /** Shortcut for prepare and execute move */ ZoomContext.prototype.movePixels = function () { this.prepareMove(); this.doMove(); }; /** Prepare fill table based on relocation table */ ZoomContext.prototype.prepareFill = function () { var fill = null; var i; var j = 0; var k = 0; var s = 0; var n = 0; for (i = 0; i < this.columns.length; i++) { if (this.columns[i].dirty) { j = i - 1; for ( k = i + 1; k < this.columns.length && this.columns[k].dirty; k++ ) {} while (i < this.columns.length && this.columns[i].dirty) { if ( k < this.columns.length && (j < i || this.columns[i].newPosition - this.columns[j].newPosition > this.columns[k].newPosition - this.columns[i].newPosition) ) { j = k; } else if (j < 0) { break; } n = k - i; fill = this.fillTable[s]; fill.length = n; fill.from = j; fill.to = i; while (n > 0) { this.columns[i].newPosition = this.columns[j].newPosition; this.columns[i].dirty = false; n--; i++; } s++; } } } fill = this.fillTable[s]; fill.length = 0; }; /** Apply fill table */ ZoomContext.prototype.doFill = function () { var fill = null; var from_offset = 0; var to_offset = 0; var from = 0; var to = 0; var i; var j = 0; var k = 0; var t = 0; var s = 0; var d = 0; var buffer = this.image.newBuffer; var bufferWidth = this.image.width; for (i = 0; i < this.rows.length; i++) { if (this.rows[i].dirty) { j = i - 1; for (k = i + 1; k < this.rows.length && this.rows[k].dirty; k++) {} while (i < this.rows.length && this.rows[i].dirty) { if ( k < this.rows.length && (j < i || this.rows[i].newPosition - this.rows[j].newPosition > this.rows[k].newPosition - this.rows[i].newPosition) ) { j = k; } else if (j < 0) { break; } to_offset = i * bufferWidth; from_offset = j * bufferWidth; if (!this.rows[j].dirty) { s = 0; while ((fill = this.fillTable[s]).length > 0) { from = from_offset + fill.from; to = from_offset + fill.to; for (t = 0; t < fill.length; t++) { d = to + t; buffer[d] = buffer[from]; } s++; } } arrayCopy(buffer, from_offset, buffer, to_offset, bufferWidth); this.rows[i].newPosition = this.rows[j].newPosition; this.rows[i].dirty = true; i++; } } else { s = 0; from_offset = i * bufferWidth; while ((fill = this.fillTable[s]).length > 0) { from = from_offset + fill.from; to = from_offset + fill.to; for (t = 0; t < fill.length; t++) { d = to + t; buffer[d] = buffer[from]; } s++; } this.rows[i].dirty = true; } } }; /** Shortcut to prepare and apply fill table */ ZoomContext.prototype.fill = function () { this.prepareFill(); this.doFill(); }; /** Render line using solid guessing * * @param row */ ZoomContext.prototype.renderRow = function (row) { var buffer = this.image.newBuffer; var bufferWidth = this.image.width; var newPosition = row.newPosition; var r = row.index; var offset = r * bufferWidth; var i; var j; var k; var n; var distl; var distr; var distu; var distd; var offsetu; var offsetd; var offsetl; var offsetul; var offsetur; var offsetdl; var offsetdr; var rend = r - GUESS_RANGE; var length; var current; if (rend < 0) { rend = 0; } for (i = r - 1; i >= rend && this.rows[i].dirty; i--) {} distu = r - i; rend = r + GUESS_RANGE; if (rend >= this.rows.length) { rend = this.rows.length - 1; } for (j = r + 1; j < rend && this.rows[j].dirty; j++) {} distd = j - r; if ( !USE_SOLIDGUESS || i < 0 || j >= this.rows.length || this.rows[i].dirty || this.rows[j].dirty ) { for (k = 0, length = this.columns.length; k < length; k++) { current = this.columns[k]; if (!this.columns[k].dirty) { buffer[offset] = this.fractal.formula( current.newPosition, newPosition ); } offset++; } } else { distr = 0; distl = Number.MAX_VALUE / 2; offsetu = offset - distu * bufferWidth; offsetd = offset + distd * bufferWidth; for (k = 0, length = this.columns.length; k < length; k++) { current = this.columns[k]; if (!this.columns[k].dirty) { if (distr <= 0) { rend = k + GUESS_RANGE; if (rend >= this.columns.length) { rend = this.columns.length - 1; } for (j = k + 1; j < rend && this.columns[j].dirty; j++) { distr = j - k; } if (j >= rend) { distr = Number.MAX_VALUE / 2; } } if (distr < Number.MAX_VALUE / 4 && distl < Number.MAX_VALUE / 4) { offsetl = offset - distl; offsetul = offsetu - distl; offsetdl = offsetd - distl; offsetur = offsetu + distr; offsetdr = offsetd + distr; n = buffer[offsetl]; if ( n == buffer[offsetu] && n == buffer[offsetd] && n == buffer[offsetul] && n == buffer[offsetur] && n == buffer[offsetdl] && n == buffer[offsetdr] ) { buffer[offset] = n; } else { buffer[offset] = this.fractal.formula( current.newPosition, newPosition ); } } else { buffer[offset] = this.fractal.formula( current.newPosition, newPosition ); } distl = 0; } offset++; offsetu++; offsetd++; distr--; distl++; } } row.recalculate = false; row.dirty = false; }; /** Render column using solid guessing * * @param column */ ZoomContext.prototype.renderColumn = function (column) { var buffer = this.image.newBuffer; var bufferWidth = this.image.width; var newPosition = column.newPosition; var r = column.index; var offset = r; var rend = r - GUESS_RANGE; var i; var j; var k; var n; var distl; var distr; var distu; var distd; var offsetl; var offsetr; var offsetu; var offsetlu; var offsetru; var offsetld; var offsetrd; var sumu; var sumd; var length; var current; if (rend < 0) { rend = 0; } for (i = r - 1; i >= rend && this.columns[i].dirty; i--) {} distl = r - i; rend = r + GUESS_RANGE; if (rend >= this.columns.length) { rend = this.columns.length - 1; } for (j = r + 1; j < rend && this.columns[j].dirty; j++) {} distr = j - r; if ( !USE_SOLIDGUESS || i < 0 || j >= this.columns.length || this.columns[i].dirty || this.columns[j].dirty ) { for (k = 0, length = this.rows.length; k < length; k++) { current = this.rows[k]; if (!this.rows[k].dirty) { buffer[offset] = this.fractal.formula( newPosition, current.newPosition ); } offset += bufferWidth; } } else { distd = 0; distu = Number.MAX_VALUE / 2; offsetl = offset - distl; offsetr = offset + distr; for (k = 0, length = this.rows.length; k < length; k++) { current = this.rows[k]; if (!this.rows[k].dirty) { if (distd <= 0) { rend = k + GUESS_RANGE; if (rend >= this.rows.length) { rend = this.rows.length - 1; } for (j = k + 1; j < rend && this.rows[j].dirty; j++) { distd = j - k; } if (j >= rend) { distd = Number.MAX_VALUE / 2; } } if (distd < Number.MAX_VALUE / 4 && distu < Number.MAX_VALUE / 4) { sumu = distu * bufferWidth; sumd = distd * bufferWidth; offsetu = offset - sumu; offsetlu = offsetl - sumu; offsetru = offsetr - sumu; offsetld = offsetl + sumd; offsetrd = offsetr + sumd; n = buffer[offsetu]; if ( n == buffer[offsetl] && n == buffer[offsetr] && n == buffer[offsetlu] && n == buffer[offsetru] && n == buffer[offsetld] && n == buffer[offsetrd] ) { buffer[offset] = n; } else { buffer[offset] = this.fractal.formula( newPosition, current.newPosition ); } } else { buffer[offset] = this.fractal.formula( newPosition, current.newPosition ); } distu = 0; } offset += bufferWidth; offsetl += bufferWidth; offsetr += bufferWidth; distd--; distu++; } } column.recalculate = false; column.dirty = false; }; /** Calculate whether we're taking too long to render the fractal to meet the idealPos FPS */ ZoomContext.prototype.tooSlow = function () { var newTime = new Date().getTime(), minFPS = this.zooming ? this.minFPS : 10; return 1000 / (newTime - this.startTime + this.fudgeFactor) < minFPS; }; /** Prioritize calculation of lines between begin and end * * @param lines - rows or columns to prioritize * @param begin - index of first line to prioritize * @param end - index of last line to prioritize */ function calcPriority(lines, begin, end) { var middle; while (begin < end) { middle = begin + ((end - begin) >> 1); lines[middle].priority = (lines[end].newPosition - lines[middle].newPosition) * lines[middle].priority; if (lines[middle].symRef !== -1) { lines[middle].priority /= 2.0; } calcPriority(lines, begin, middle); begin = middle + 1; } } /** Enqueue all the lines to be recalculated and set their priority * * @param lines - lines to enqueue for calculation */ ZoomContext.prototype.enqueueCalculations = function (lines) { var i; var j = 0; for (i = 0; i < lines.length; i++) { if (lines[i].recalculate) { for (j = i; j < lines.length && lines[j].recalculate; j++) { this.queue[this.queueLength++] = lines[j]; } if (j === lines.length) { j -= 1; } calcPriority(lines, i, j); i = j; } } }; /** Sort calculation queue according to priority (using quicksort) * * @param queue * @param l * @param r */ function sortQueue(queue, l, r) { var m = (queue[l].priority + queue[r].priority) / 2.0; var tmp = null; var i = l; var j = r; do { while (queue[i].priority > m) { i++; } while (queue[j].priority < m) { j--; } if (i <= j) { tmp = queue[i]; queue[i] = queue[j]; queue[j] = tmp; i++; j--; } } while (j >= i); if (l < j) { sortQueue(queue, l, j); } if (r > i) { sortQueue(queue, i, r); } } /** Process the relocation table */ ZoomContext.prototype.calculate = function () { var i, newTime; this.incomplete = false; this.queueLength = 0; this.enqueueCalculations(this.columns); this.enqueueCalculations(this.rows); if (this.queueLength > 0) { if (this.queueLength > 1) { sortQueue(this.queue, 0, this.queueLength - 1); } for (i = 0; i < this.queueLength; i++) { if (this.queue[i].isRow) { this.renderRow(this.queue[i]); } else { this.renderColumn(this.queue[i]); } if (!this.recalculate && this.tooSlow() && i < this.queueLength) { this.incomplete = true; this.fill(); break; } } } }; /** Update newPosition array with newly calculated positions */ ZoomContext.prototype.updatePosition = function () { var k; var len; for (k = 0, len = this.columns.length; k < len; k++) { this.columns[k].oldPosition = this.columns[k].newPosition; } for (k = 0, len = this.rows.length; k < len; k++) { this.rows[k].oldPosition = this.rows[k].newPosition; } }; /** Calculate FPS achieved and determine if fudge factor needs adjustment for next frame */ ZoomContext.prototype.updateFPS = function () { var fps = 1000 / (new Date().getTime() - this.startTime); if (fps < this.minFPS) { this.fudgeFactor++; } else if (fps > this.minFPS + 10 && this.fudgeFactor > 0) { this.fudgeFactor--; } console.log(fps + " fps"); }; /** Overall fractal drawing workflow, calls other functions */ ZoomContext.prototype.drawFractal = function (recalculate) { var area = this.convertArea(); var symx = this.fractal.symmetry && this.fractal.symmetry.x; var symy = this.fractal.symmetry && this.fractal.symmetry.y; var stepx, stepy; this.startTime = new Date().getTime(); this.recalculate = recalculate; if (recalculate || !USE_XAOS) { stepx = this.initialize(this.columns, area.begin.x, area.end.x, false); stepy = this.initialize(this.rows, area.begin.y, area.end.y, true); } else { stepx = this.approximate(this.columns, area.begin.x, area.end.x); stepy = this.approximate(this.rows, area.begin.y, area.end.y); } if ( USE_SYMMETRY && typeof symy === "number" && !(area.begin.y > symy || symy > area.end.y) ) { prepareSymmetry( this.rows, Math.floor((symy - area.begin.y) / stepy), symy, stepy ); } if ( USE_SYMMETRY && typeof symx === "number" && !(area.begin.x > symx || symx > area.end.x) ) { prepareSymmetry( this.columns, Math.floor((symx - area.begin.x) / stepx), symx, stepx ); } this.image.swapBuffers(); this.movePixels(); this.calculate(); if ( (USE_SYMMETRY && typeof symx === "number") || typeof symy === "number" ) { this.doSymmetry(); } this.image.paint(); this.updatePosition(); this.updateFPS(); }; /** Adjust display region to zoom based on mouse buttons */ ZoomContext.prototype.updateRegion = function (mouse) { var MAXSTEP = 0.008 * 3; var MUL = 0.3; var area = this.convertArea(); var x = area.begin.x + mouse.x * ((area.end.x - area.begin.x) / this.image.width); var y = area.begin.y + mouse.y * ((area.end.y - area.begin.y) / this.image.height); var deltax = (mouse.oldx - mouse.x) * ((area.end.x - area.begin.x) / this.image.width); var deltay = (mouse.oldy - mouse.y) * ((area.end.y - area.begin.y) / this.image.height); var step; var mmul; if (mouse.button[1] || (mouse.button[0] && mouse.button[2])) { // Pan when middle or left+right buttons are pressed step = 0; } else if (mouse.button[0]) { // Zoom in when left button is pressed step = MAXSTEP * 2; } else if (mouse.button[2]) { // Zoom out when right button is pressed step = -MAXSTEP * 2; } else { this.zooming = false; return; } mmul = Math.pow(1 - step, MUL); area.begin.x = x + (area.begin.x - x) * mmul; area.end.x = x + (area.end.x - x) * mmul; area.begin.y = y + (area.begin.y - y) * mmul; area.end.y = y + (area.end.y - y) * mmul; this.fractal.region.radius.x = area.end.x - area.begin.x; this.fractal.region.radius.y = area.end.y - area.begin.y; this.fractal.region.center.x = (area.begin.x + area.end.x) / 2; this.fractal.region.center.y = ((area.begin.y + area.end.y) / 2) * (this.image.width / this.image.height); this.zooming = true; }; /** Attaches zoomer to specified canvas */ return function (canvas, fractal) { var image = new CanvasImage(canvas); var zoomer = new ZoomContext(image, fractal); var mouse = { x: 0, y: 0, button: [false, false, false] }; function doZoom() { zoomer.updateRegion(mouse); if (zoomer.zooming || zoomer.incomplete) { requestAnimationFrame(doZoom); zoomer.drawFractal(false); } } canvas.ontouchstart = function (e) { if (e.touches.length < 3) { var touch = e.touches[0]; e.touches.length == 2 ? (mouse.button[2] = true) : (mouse.button[2] = false); var mouseEvent = new MouseEvent("mousedown", { clientX: touch.clientX, clientY: touch.clientY, }); canvas.dispatchEvent(mouseEvent); } }; canvas.ontouchend = function (e) { var mouseEvent = new MouseEvent("mouseup", {}); canvas.dispatchEvent(mouseEvent); }; canvas.ontouchmove = function (e) { var touch = e.touches[0]; var mouseEvent = new MouseEvent("mousemove", { clientX: touch.clientX, clientY: touch.clientY, }); canvas.dispatchEvent(mouseEvent); }; canvas.onmousedown = function (e) { mouse.button[e.button] = true; mouse.x = e.offsetX || e.clientX - canvas.offsetLeft; mouse.y = e.offsetY || e.clientY - canvas.offsetTop; mouse.oldx = e.offsetX || e.clientX - canvas.offsetLeft; mouse.oldy = e.offsetY || e.clientY - canvas.offsetTop; doZoom(); }; canvas.onmouseup = function (e) { mouse.button[e.button] = false; }; canvas.onmousemove = function (e) { mouse.x = e.offsetX || e.clientX - canvas.offsetLeft; mouse.y = e.offsetY || e.clientY - canvas.offsetTop; }; canvas.oncontextmenu = function () { return false; }; canvas.onmouseout = function () { mouse.button = [false, false, false]; }; zoomer.drawFractal(true); }; })(); /** Create the default XaoS color palette */ xaos.defaultPalette = function () { var MAXENTRIES = 65536; var segmentsize = 8; var setsegments = Math.floor((MAXENTRIES + 3) / segmentsize); var nsegments = Math.floor(255 / segmentsize); var segments = [ [0, 0, 0], [120, 119, 238], [24, 7, 25], [197, 66, 28], [29, 18, 11], [135, 46, 71], [24, 27, 13], [241, 230, 128], [17, 31, 24], [240, 162, 139], [11, 4, 30], [106, 87, 189], [29, 21, 14], [12, 140, 118], [10, 6, 29], [50, 144, 77], [22, 0, 24], [148, 188, 243], [4, 32, 7], [231, 146, 14], [10, 13, 20], [184, 147, 68], [13, 28, 3], [169, 248, 152], [4, 0, 34], [62, 83, 48], [7, 21, 22], [152, 97, 184], [8, 3, 12], [247, 92, 235], [31, 32, 16], ]; var i, y; var r, g, b; var rs, gs, bs; var palette = []; for (i = 0; i < setsegments; i++) { r = segments[i % nsegments][0]; g = segments[i % nsegments][1]; b = segments[i % nsegments][2]; rs = (segments[((i + 1) % setsegments) % nsegments][0] - r) / segmentsize; gs = (segments[((i + 1) % setsegments) % nsegments][1] - g) / segmentsize; bs = (segments[((i + 1) % setsegments) % nsegments][2] - b) / segmentsize; for (y = 0; y < segmentsize; y++) { palette.push((255 << 24) | (b << 16) | (g << 8) | r); r += rs; g += gs; b += bs; } } return new Uint32Array(palette); }; xaos.mandelbrot = { symmetry: { x: null, y: 0 }, region: { center: { x: -0.75, y: 0.0 }, radius: { x: 2.5, y: 2.5 }, angle: 0, }, z0: { x: 0, y: 0 }, maxiter: 512, bailout: 4, formula: function (cr, ci) { var maxiter = this.maxiter, bailout = this.bailout, zr = this.z0.x, zi = this.z0.y, i = maxiter; while (i--) { var zr2 = zr * zr; var zi2 = zi * zi; if (zr2 + zi2 > bailout) { return this.palette[(maxiter - i) % this.palette.length]; } zi = ci + 2 * zr * zi; zr = cr + zr2 - zi2; } return this.palette[0]; }, palette: xaos.defaultPalette(), }; xaos.zoom(document.getElementById("canvas"), xaos.mandelbrot);