arrayinamatrix.xyz/index/games/fractal/script/xaos.js
2022-05-12 22:39:33 -04:00

1410 lines
54 KiB
JavaScript
Executable file

/*
* 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 <http://www.gnu.org/licenses/>.
*
*/
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);