From 218c5a65dcbd65bceebe57a4b1054f6796e090e3 Mon Sep 17 00:00:00 2001 From: array-in-a-matrix Date: Tue, 12 Oct 2021 22:03:58 -0400 Subject: [PATCH] added fractal --- index/games/fractal/index.html | 40 + index/games/fractal/starter-template.js | 12 + index/games/fractal/style.css | 67 ++ index/games/fractal/xaos.js | 1410 +++++++++++++++++++++++ 4 files changed, 1529 insertions(+) create mode 100644 index/games/fractal/index.html create mode 100644 index/games/fractal/starter-template.js create mode 100644 index/games/fractal/style.css create mode 100644 index/games/fractal/xaos.js diff --git a/index/games/fractal/index.html b/index/games/fractal/index.html new file mode 100644 index 0000000..70b35ee --- /dev/null +++ b/index/games/fractal/index.html @@ -0,0 +1,40 @@ + + + + + + + + Fractal + + + + + + + +
+ +

Your browser doesn't seem to support the <canvas> tag. + Try Firefox.

+
+
+ + Reset + Capture
+
+
+ + + + + + \ No newline at end of file diff --git a/index/games/fractal/starter-template.js b/index/games/fractal/starter-template.js new file mode 100644 index 0000000..3dd2967 --- /dev/null +++ b/index/games/fractal/starter-template.js @@ -0,0 +1,12 @@ +function saveCanvas() { + saveCanvasButton.download = "image.png"; + saveCanvasButton.href = canvas.toDataURL("image/png").replace("image/png", "image/octet-stream"); +} +function goFullScreen() { + if (canvas.requestFullScreen) + canvas.requestFullScreen(); + else if (canvas.webkitRequestFullScreen) + canvas.webkitRequestFullScreen(); + else if (canvas.mozRequestFullScreen) + canvas.mozRequestFullScreen(); +} diff --git a/index/games/fractal/style.css b/index/games/fractal/style.css new file mode 100644 index 0000000..645195c --- /dev/null +++ b/index/games/fractal/style.css @@ -0,0 +1,67 @@ +#controls { + position: relative; + margin-bottom: 2.5em; +} + +#canvas { + width: 100%; + height: 100vh; + margin-bottom: 0.5em; + display: inline-block; + vertical-align: baseline; +} + +#fullScreenButton { + position: absolute; + height: 100vh; + width: 100%; + bottom: 3rem; + visibility: hidden; +} + +@media only screen and (max-width: 600px) { + #fullScreenButton { + visibility: visible; + opacity: 0; + } + + #saveCanvasButton { + visibility: hidden; + } +} + +#resetButton { + position: absolute; + left: 0em; +} + +#saveCanvasButton { + position: absolute; + right: 0.1em; + visibility: visible; +} + +.container { + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +@media (min-width: 768px) { + .container { + width: 750px; + } +} + +@media (min-width: 992px) { + .container { + width: 970px; + } +} + +@media (min-width: 1200px) { + .container { + width: 1170px; + } +} \ No newline at end of file diff --git a/index/games/fractal/xaos.js b/index/games/fractal/xaos.js new file mode 100644 index 0000000..78a1403 --- /dev/null +++ b/index/games/fractal/xaos.js @@ -0,0 +1,1410 @@ +/* + * 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); \ No newline at end of file