Deprecated: Tightenco\Collect\Support\Arr::first(): Implicitly marking parameter $callback as nullable is deprecated, the explicit nullable type must be used instead in /usr/share/nginx/html/app/vendor/tightenco/collect/src/Collect/Support/Arr.php on line 188
Deprecated: Tightenco\Collect\Support\Arr::last(): Implicitly marking parameter $callback as nullable is deprecated, the explicit nullable type must be used instead in /usr/share/nginx/html/app/vendor/tightenco/collect/src/Collect/Support/Arr.php on line 217
Deprecated: Tightenco\Collect\Support\Collection::filter(): Implicitly marking parameter $callback as nullable is deprecated, the explicit nullable type must be used instead in /usr/share/nginx/html/app/vendor/tightenco/collect/src/Collect/Support/Collection.php on line 379
Deprecated: Tightenco\Collect\Support\Collection::first(): Implicitly marking parameter $callback as nullable is deprecated, the explicit nullable type must be used instead in /usr/share/nginx/html/app/vendor/tightenco/collect/src/Collect/Support/Collection.php on line 397
Deprecated: Tightenco\Collect\Support\Collection::last(): Implicitly marking parameter $callback as nullable is deprecated, the explicit nullable type must be used instead in /usr/share/nginx/html/app/vendor/tightenco/collect/src/Collect/Support/Collection.php on line 741
Deprecated: Tightenco\Collect\Support\Traits\EnumeratesValues::times(): Implicitly marking parameter $callback as nullable is deprecated, the explicit nullable type must be used instead in /usr/share/nginx/html/app/vendor/tightenco/collect/src/Collect/Support/Traits/EnumeratesValues.php on line 162
Deprecated: Tightenco\Collect\Support\Traits\EnumeratesValues::whenEmpty(): Implicitly marking parameter $default as nullable is deprecated, the explicit nullable type must be used instead in /usr/share/nginx/html/app/vendor/tightenco/collect/src/Collect/Support/Traits/EnumeratesValues.php on line 482
Deprecated: Tightenco\Collect\Support\Traits\EnumeratesValues::whenNotEmpty(): Implicitly marking parameter $default as nullable is deprecated, the explicit nullable type must be used instead in /usr/share/nginx/html/app/vendor/tightenco/collect/src/Collect/Support/Traits/EnumeratesValues.php on line 496
Deprecated: Tightenco\Collect\Support\Traits\EnumeratesValues::unlessEmpty(): Implicitly marking parameter $default as nullable is deprecated, the explicit nullable type must be used instead in /usr/share/nginx/html/app/vendor/tightenco/collect/src/Collect/Support/Traits/EnumeratesValues.php on line 510
Deprecated: Tightenco\Collect\Support\Traits\EnumeratesValues::unlessNotEmpty(): Implicitly marking parameter $default as nullable is deprecated, the explicit nullable type must be used instead in /usr/share/nginx/html/app/vendor/tightenco/collect/src/Collect/Support/Traits/EnumeratesValues.php on line 524
Deprecated: Tightenco\Collect\Support\Traits\Conditionable::when(): Implicitly marking parameter $callback as nullable is deprecated, the explicit nullable type must be used instead in /usr/share/nginx/html/app/vendor/tightenco/collect/src/Collect/Support/Traits/Conditionable.php on line 21
Deprecated: Tightenco\Collect\Support\Traits\Conditionable::when(): Implicitly marking parameter $default as nullable is deprecated, the explicit nullable type must be used instead in /usr/share/nginx/html/app/vendor/tightenco/collect/src/Collect/Support/Traits/Conditionable.php on line 21
Deprecated: Tightenco\Collect\Support\Traits\Conditionable::unless(): Implicitly marking parameter $callback as nullable is deprecated, the explicit nullable type must be used instead in /usr/share/nginx/html/app/vendor/tightenco/collect/src/Collect/Support/Traits/Conditionable.php on line 53
Deprecated: Tightenco\Collect\Support\Traits\Conditionable::unless(): Implicitly marking parameter $default as nullable is deprecated, the explicit nullable type must be used instead in /usr/share/nginx/html/app/vendor/tightenco/collect/src/Collect/Support/Traits/Conditionable.php on line 53
Deprecated: Tightenco\Collect\Support\Enumerable::times(): Implicitly marking parameter $callback as nullable is deprecated, the explicit nullable type must be used instead in /usr/share/nginx/html/app/vendor/tightenco/collect/src/Collect/Support/Enumerable.php on line 40
Deprecated: Tightenco\Collect\Support\Enumerable::filter(): Implicitly marking parameter $callback as nullable is deprecated, the explicit nullable type must be used instead in /usr/share/nginx/html/app/vendor/tightenco/collect/src/Collect/Support/Enumerable.php on line 298
Deprecated: Tightenco\Collect\Support\Enumerable::when(): Implicitly marking parameter $callback as nullable is deprecated, the explicit nullable type must be used instead in /usr/share/nginx/html/app/vendor/tightenco/collect/src/Collect/Support/Enumerable.php on line 310
Deprecated: Tightenco\Collect\Support\Enumerable::when(): Implicitly marking parameter $default as nullable is deprecated, the explicit nullable type must be used instead in /usr/share/nginx/html/app/vendor/tightenco/collect/src/Collect/Support/Enumerable.php on line 310
Deprecated: Tightenco\Collect\Support\Enumerable::whenEmpty(): Implicitly marking parameter $default as nullable is deprecated, the explicit nullable type must be used instead in /usr/share/nginx/html/app/vendor/tightenco/collect/src/Collect/Support/Enumerable.php on line 321
Deprecated: Tightenco\Collect\Support\Enumerable::whenNotEmpty(): Implicitly marking parameter $default as nullable is deprecated, the explicit nullable type must be used instead in /usr/share/nginx/html/app/vendor/tightenco/collect/src/Collect/Support/Enumerable.php on line 332
Deprecated: Tightenco\Collect\Support\Enumerable::unless(): Implicitly marking parameter $default as nullable is deprecated, the explicit nullable type must be used instead in /usr/share/nginx/html/app/vendor/tightenco/collect/src/Collect/Support/Enumerable.php on line 344
Deprecated: Tightenco\Collect\Support\Enumerable::unlessEmpty(): Implicitly marking parameter $default as nullable is deprecated, the explicit nullable type must be used instead in /usr/share/nginx/html/app/vendor/tightenco/collect/src/Collect/Support/Enumerable.php on line 355
Deprecated: Tightenco\Collect\Support\Enumerable::unlessNotEmpty(): Implicitly marking parameter $default as nullable is deprecated, the explicit nullable type must be used instead in /usr/share/nginx/html/app/vendor/tightenco/collect/src/Collect/Support/Enumerable.php on line 366
Deprecated: Tightenco\Collect\Support\Enumerable::first(): Implicitly marking parameter $callback as nullable is deprecated, the explicit nullable type must be used instead in /usr/share/nginx/html/app/vendor/tightenco/collect/src/Collect/Support/Enumerable.php on line 478
Deprecated: Tightenco\Collect\Support\Enumerable::last(): Implicitly marking parameter $callback as nullable is deprecated, the explicit nullable type must be used instead in /usr/share/nginx/html/app/vendor/tightenco/collect/src/Collect/Support/Enumerable.php on line 620
Deprecated: Tightenco\Collect\Support\LazyCollection::filter(): Implicitly marking parameter $callback as nullable is deprecated, the explicit nullable type must be used instead in /usr/share/nginx/html/app/vendor/tightenco/collect/src/Collect/Support/LazyCollection.php on line 430
Deprecated: Tightenco\Collect\Support\LazyCollection::first(): Implicitly marking parameter $callback as nullable is deprecated, the explicit nullable type must be used instead in /usr/share/nginx/html/app/vendor/tightenco/collect/src/Collect/Support/LazyCollection.php on line 454
Deprecated: Tightenco\Collect\Support\LazyCollection::last(): Implicitly marking parameter $callback as nullable is deprecated, the explicit nullable type must be used instead in /usr/share/nginx/html/app/vendor/tightenco/collect/src/Collect/Support/LazyCollection.php on line 734
Deprecated: DI\Bridge\Slim\Bridge::create(): Implicitly marking parameter $container as nullable is deprecated, the explicit nullable type must be used instead in /usr/share/nginx/html/app/vendor/php-di/slim-bridge/src/Bridge.php on line 25
Deprecated: ParsedownExtra::blockSetextHeader(): Implicitly marking parameter $Block as nullable is deprecated, the explicit nullable type must be used instead in /usr/share/nginx/html/app/vendor/erusev/parsedown-extra/ParsedownExtra.php on line 241
Deprecated: Parsedown::blockSetextHeader(): Implicitly marking parameter $Block as nullable is deprecated, the explicit nullable type must be used instead in /usr/share/nginx/html/app/vendor/erusev/parsedown/Parsedown.php on line 715
Deprecated: Parsedown::blockTable(): Implicitly marking parameter $Block as nullable is deprecated, the explicit nullable type must be used instead in /usr/share/nginx/html/app/vendor/erusev/parsedown/Parsedown.php on line 853
PK- ZZ ÿÿÿÿÿÿÿÿ README.md SV This is a simple web app that uses local JavaScript workers to compute the famous Mandelbrot set quickly. Nothing novel, just an excuse to learn about JavaScript parallel programming. For an intepreted language implementation, this is surprisingly fast.
PKYkÂþ þ PK- ZZ ÿÿÿÿÿÿÿÿ
index.html SV
Mandelbrot Set
PK¤‹ÉG G PK- ZZ ÿÿÿÿÿÿÿÿ mand_main.js SV // Set up canvas and viewing parameters.
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const width = canvas.width, height = canvas.height;
const initialStep = 16; // Must be a power of 2; iterations will use: 16, 8, 4, 2, 1, 0.5
const lastStep = 1; // Stop refining when we reach this step size
const defaultScale = 3.0;
const defaultX = -0.5, defaultY = 0.0;
// Defaults are also set here, but can be overridden by URL parameters.
function getUrlParams() {
const params = new URLSearchParams(window.location.search);
return {
centerX: parseFloat(params.get('centerX')) || defaultX,
centerY: parseFloat(params.get('centerY')) || defaultY,
scale: parseFloat(params.get('scale')) || defaultScale,
maxIterations: params.get('maxIterations') || '256'
};
}
function updateUrlParams(centerX, centerY, scale, maxIterations) {
const params = new URLSearchParams();
params.set('maxIterations', maxIterations);
params.set('scale', scale);
params.set('centerX', centerX);
params.set('centerY', centerY);
window.history.pushState({}, '', `${window.location.pathname}?${params}`);
}
function calculateMaxIterations(scale) {
const additionalIterations = Math.ceil(128 * Math.log10(1 / scale));
return 256 + Math.max(0, additionalIterations);
}
const { centerX: initialCenterX, centerY: initialCenterY, scale: initialScale, maxIterations: initialMaxIterations } = getUrlParams();
let centerX = initialCenterX, centerY = initialCenterY, scale = initialScale;
let maxIterations = initialMaxIterations === 'auto' ? calculateMaxIterations(scale) : parseInt(initialMaxIterations);
// Each click will recenter on the clicked point and zoom in by a factor of 4.
let currentRenderId = 0; // Increases with each new render (to discard late messages).
let currentIteration = 0; // Tracks the current refinement iteration.
// Create an ImageData buffer that we update progressively.
let imageData = ctx.getImageData(0, 0, width, height);
// Create a pool of Web Workers.
const numWorkers = Math.min(navigator.hardwareConcurrency || 4, 16);
console.log(`Using ${numWorkers} workers`);
// const numWorkers = 8;
const workers = [];
function getColor(n, maxIterations) {
if (n === maxIterations) {
return { r: 0, g: 0, b: 0, a: 255 }; // Points inside the set: black.
} else {
// Smooth color gradient
var t = n / maxIterations;
var r = Math.floor(9 * (1 - t) * t * t * t * 255);
var g = Math.floor(15 * (1 - t) * (1 - t) * t * t * 255);
var b = Math.floor(8.5 * (1 - t) * (1 - t) * (1 - t) * t * 255);
return { r: r, g: g, b: b, a: 255 };
}
}
function blendPixel(imageData, x, y, color, weight) {
if (x < 0 || x >= width || y < 0 || y >= height) return;
const idx = (y * width + x) * 4;
imageData.data[idx] = (imageData.data[idx] * (1-weight) + color.r * weight);
imageData.data[idx + 1] = (imageData.data[idx + 1] * (1-weight) + color.g * weight);
imageData.data[idx + 2] = (imageData.data[idx + 2] * (1-weight) + color.b * weight);
imageData.data[idx + 3] = 255;
}
// Setup workers...
for (let i = 0; i < numWorkers; i++) {
const worker = new Worker('mand_worker.js');
worker.onmessage = function(e) {
const data = e.data;
// Discard any results from an outdated render.
if (data.renderId !== currentRenderId) return;
const s = data.s;
const results = data.results;
// For each computed point, fill its s×s block in the image buffer.
results.forEach(function (p) {
const color = getColor(p.n, maxIterations);
if (s >= 1) {
// Integer block size - direct pixel assignment
for (let dy = 0; dy < s; dy++) {
for (let dx = 0; dx < s; dx++) {
const px = Math.floor(p.x + dx);
const py = Math.floor(p.y + dy);
blendPixel(imageData, px, py, color, 1.0);
}
}
} else {
// Fractional block size - anti-alias in RGB space
const x0 = Math.floor(p.x), y0 = Math.floor(p.y);
const x1 = Math.ceil(p.x + s), y1 = Math.ceil(p.y + s);
for (let py = y0; py < y1; py++) {
for (let px = x0; px < x1; px++) {
// Calculate overlap area for this pixel
const left = Math.max(p.x, px);
const right = Math.min(p.x + s, px + 1);
const top = Math.max(p.y, py);
const bottom = Math.min(p.y + s, py + 1);
if (right > left && bottom > top) {
const weight = (right - left) * (bottom - top);
blendPixel(imageData, px, py, color, weight);
}
}
}
}
});
// Update the canvas display.
ctx.putImageData(imageData, 0, 0);
// When every worker has returned, schedule the next (more refined) iteration.
currentIterationPending--;
updateDataWindow();
if (currentIterationPending === 0) {
// Proceed to the next iteration if above lastStep
if (s > lastStep) {
runIteration(s / 2, currentIteration + 1);
}
}
};
workers.push(worker);
}
// Global counter for the pending worker tasks.
let currentIterationPending = 0;
// runIteration dispatches tasks to all workers to compute new points for block size s.
function runIteration(s, iteration) {
// Stop work on all workers...
workers.forEach(worker => worker.postMessage({ terminate: true }));
// Return if we're at the last step
if (s < lastStep) return;
currentIteration = iteration;
currentIterationPending = numWorkers;
updateDataWindow();
// Partition the vertical range into segments (one per worker).
const segmentHeight = Math.ceil(height / numWorkers);
for (let i = 0; i < numWorkers; i++) {
const yStart = i * segmentHeight;
const yEnd = Math.min(height, (i + 1) * segmentHeight);
workers[i].postMessage({
renderId: currentRenderId,
s: s,
iteration: iteration,
yStart: yStart,
yEnd: yEnd,
width: width,
height: height,
centerX: centerX,
centerY: centerY,
scale: scale,
maxIterations: maxIterations
});
}
}
// startRender resets the display and begins the progressive (iterative) computation.
const dataWindow = document.getElementById('data-window');
const resetButton = document.getElementById('reset-button');
function updateDataWindow() {
dataWindow.innerHTML = `Center: (${centerX.toFixed(5)}, ${centerY.toFixed(5)}) `;
dataWindow.innerHTML += `Scale: ${Math.round(defaultScale / scale)}x`;
dataWindow.innerHTML += ` Max iterations: ${maxIterations}`;
dataWindow.innerHTML += ` Workers: ${currentIterationPending}/${numWorkers}`;
}
function startRender(newCenterX, newCenterY, newScale, logIterations = false) {
currentRenderId++;
centerX = newCenterX;
centerY = newCenterY;
scale = newScale;
maxIterations = initialMaxIterations === 'auto' ? calculateMaxIterations(scale) : parseInt(initialMaxIterations);
if (logIterations) {
console.log(`maxIterations: ${maxIterations}`);
}
updateUrlParams(centerX, centerY, scale, initialMaxIterations);
updateDataWindow(); // Update data window with new values
runIteration(initialStep, 0); // Begin with the coarsest resolution.
}
resetButton.addEventListener('click', () => {
startRender(defaultX, defaultY, defaultScale, true);
});
// Start the initial render.
startRender(centerX, centerY, scale);
// When the user clicks, recenter on that complex coordinate and zoom in (scale divided by 4).
canvas.addEventListener("click", function (e) {
if (e.button === 0 && !hasDragged) { // Left click and no drag
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left, y = e.clientY - rect.top;
// Map the clicked pixel to the complex plane.
const newCenterX = centerX + (x - width / 2) * (scale / width);
const newCenterY = centerY + (y - height / 2) * (scale / width);
const newScale = scale / 4;
startRender(newCenterX, newCenterY, newScale, true);
}
});
// When the user right-clicks, recenter on that complex coordinate and zoom out (scale multiplied by 4).
canvas.addEventListener("contextmenu", function (e) {
e.preventDefault(); // Prevent the context menu from appearing
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left, y = e.clientY - rect.top;
// Map the clicked pixel to the complex plane.
const newCenterX = centerX + (x - width / 2) * (scale / width);
const newCenterY = centerY + (y - height / 2) * (scale / width);
const newScale = scale * 4;
startRender(newCenterX, newCenterY, newScale, true);
});
// Modify wheel zoom support
canvas.addEventListener("wheel", function (e) {
e.preventDefault(); // Prevent page scrolling
// Zoom in or out based on scroll direction
const zoomFactor = 1.2;
const newScale = e.deltaY > 0 ? scale * zoomFactor : scale / zoomFactor;
startRender(centerX, centerY, newScale, true);
});
// Handle the popstate event to re-render the view when navigating history.
window.addEventListener('popstate', () => {
const { centerX: newCenterX, centerY: newCenterY, scale: newScale, maxIterations: newMaxIterations } = getUrlParams();
maxIterations = newMaxIterations === 'auto' ? calculateMaxIterations(newScale) : parseInt(newMaxIterations);
console.log(`maxIterations: ${maxIterations}`);
startRender(newCenterX, newCenterY, newScale);
});
let isDragging = false;
let dragStartX = 0, dragStartY = 0;
let dragStartCenterX = 0, dragStartCenterY = 0;
let lastDragUpdate = 0;
const dragUpdateInterval = 100; // ms
let hasDragged = false;
function startDrag(x, y) {
isDragging = true;
hasDragged = false;
dragStartX = x;
dragStartY = y;
dragStartCenterX = centerX;
dragStartCenterY = centerY;
}
function updateDrag(x, y) {
if (isDragging) {
hasDragged = true;
const now = Date.now();
if (now - lastDragUpdate >= dragUpdateInterval) {
lastDragUpdate = now;
const dx = x - dragStartX;
const dy = y - dragStartY;
const newCenterX = dragStartCenterX - dx * (scale / width);
const newCenterY = dragStartCenterY - dy * (scale / height);
startRender(newCenterX, newCenterY, scale);
}
}
}
function endDrag() {
isDragging = false;
}
canvas.addEventListener("mousedown", function (e) {
if (e.button === 0) { // Left button
startDrag(e.clientX, e.clientY);
}
});
canvas.addEventListener("mousemove", function (e) {
updateDrag(e.clientX, e.clientY);
});
canvas.addEventListener("mouseup", function (e) {
if (e.button === 0) { // Left button
endDrag();
}
});
canvas.addEventListener("mouseleave", function () {
endDrag();
});
// Touch event listeners for mobile devices
canvas.addEventListener("touchstart", function (e) {
if (e.touches.length === 1) { // Single touch
const touch = e.touches[0];
startDrag(touch.clientX, touch.clientY);
} else if (e.touches.length === 2) { // Pinch zoom
e.preventDefault(); // Prevent default pinch zoom behavior
const touch1 = e.touches[0];
const touch2 = e.touches[1];
pinchStartDistance = Math.hypot(touch2.clientX - touch1.clientX, touch2.clientY - touch1.clientY);
pinchStartScale = scale;
}
});
canvas.addEventListener("touchmove", function (e) {
if (e.touches.length === 1) { // Single touch
const touch = e.touches[0];
updateDrag(touch.clientX, touch.clientY);
} else if (e.touches.length === 2) { // Pinch zoom
e.preventDefault(); // Prevent default pinch zoom behavior
const touch1 = e.touches[0];
const touch2 = e.touches[1];
const currentDistance = Math.hypot(touch2.clientX - touch1.clientX, touch2.clientY - touch1.clientY);
const scaleFactor = pinchStartDistance / currentDistance;
const newScale = pinchStartScale * scaleFactor;
startRender(centerX, centerY, newScale);
}
});
canvas.addEventListener("touchend", function (e) {
if (e.touches.length === 0) { // All touches ended
endDrag();
}
});
canvas.addEventListener("touchcancel", function () {
endDrag();
});
// Variables for pinch zoom
let pinchStartDistance = 0;
let pinchStartScale = 0;
PKué\²/ / PK- ZZ ÿÿÿÿÿÿÿÿ mand_worker.js SV self.onmessage = function(e) {
var data = e.data;
var renderId = data.renderId;
var s = data.s; // Current pixel block size.
var iteration = data.iteration;
var yStart = data.yStart;
var yEnd = data.yEnd;
var width = data.width;
var height = data.height;
var centerX = data.centerX;
var centerY = data.centerY;
var scale = data.scale;
var maxIterations = data.maxIterations;
var results = [];
// Ensure we start at a row that is a multiple of s.
var startY = Math.ceil(yStart / s) * s;
for (var y = startY; y < yEnd; y += s) {
for (var x = 0; x < width; x += s) {
// On iterations after the first, skip points already computed at coarser resolution.
if (iteration > 0 && (x % (2 * s) === 0) && (y % (2 * s) === 0)) {
continue;
}
// Use exact center point of the block
var cx = centerX + (x + s/2 - width / 2) * (scale / width);
var cy = centerY + (y + s/2 - height / 2) * (scale / width);
var zx = 0, zy = 0, iter = 1;
while (zx * zx + zy * zy <= 4 && iter++ < maxIterations) {
var xtemp = zx * zx - zy * zy + cx;
zy = 2 * zx * zy + cy;
zx = xtemp;
}
// Each computed point represents a block of size s x s.
results.push({ x: x, y: y, s: s, n: iter });
}
}
self.postMessage({ renderId: renderId, results: results, s: s, iteration: iteration });
};
PKáu®¿ ¿ PK- ZZYkÂþ þ README.mdPK- ZZ¤‹ÉG G
U index.htmlPK- ZZué\²/ / ô mand_main.jsPK- ZZáu®¿ ¿ û7 mand_worker.jsPK å >