/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1", { savedFractals: [], lastFractalType: "mandelbrot", lastColorScheme: "rainbow" }); /**** * Classes ****/ var Button = Container.expand(function (label, width, height) { var self = Container.call(this); var buttonGraphics = self.attachAsset('controlButton', { anchorX: 0.5, anchorY: 0.5, width: width || 120, height: height || 60 }); var buttonText = new Text2(label, { size: 24, fill: 0xFFFFFF }); buttonText.anchor.set(0.5, 0.5); self.addChild(buttonText); self.setLabel = function (newLabel) { buttonText.setText(newLabel); }; self.setSelected = function (selected) { if (selected) { buttonGraphics.tint = 0x88AAFF; } else { buttonGraphics.tint = 0xFFFFFF; } }; self.down = function () { LK.getSound('click').play(); buttonGraphics.tint = 0x88AAFF; }; self.up = function () { if (self.onButtonClick) { self.onButtonClick(); } buttonGraphics.tint = 0xFFFFFF; }; return self; }); var ColorPicker = Container.expand(function () { var self = Container.call(this); var colorSchemes = { rainbow: [0xFF0000, 0xFF7F00, 0xFFFF00, 0x00FF00, 0x0000FF, 0x4B0082, 0x9400D3], fire: [0xFF0000, 0xFF5500, 0xFF8800, 0xFFAA00, 0xFFCC00, 0xFFFF00, 0xFFFFAA], ocean: [0x000033, 0x000066, 0x000099, 0x0000CC, 0x0000FF, 0x3399FF, 0x66CCFF], grayscale: [0x000000, 0x333333, 0x666666, 0x999999, 0xCCCCCC, 0xFFFFFF, 0xFFFFFF], neon: [0xFF00FF, 0xFF33FF, 0xFF66FF, 0xFF99FF, 0xFFCCFF, 0x00FFFF, 0x33FFFF] }; var currentScheme = storage.lastColorScheme || 'rainbow'; var background = self.attachAsset('controlPanel', { anchorX: 0, anchorY: 0, width: 180, height: 240 }); var title = new Text2("COLOR SCHEME", { size: 20, fill: 0xFFFFFF }); title.anchor.set(0.5, 0); title.x = 90; title.y = 10; self.addChild(title); var buttons = {}; var yPos = 40; Object.keys(colorSchemes).forEach(function (scheme, index) { var button = new Button(scheme, 140, 30); button.x = 90; button.y = yPos + index * 40; button.onButtonClick = function () { setColorScheme(scheme); }; buttons[scheme] = button; self.addChild(button); // Preview colors for (var i = 0; i < 5; i++) { var preview = self.attachAsset('colorPreview', { anchorX: 0.5, anchorY: 0.5, width: 15, height: 15 }); preview.tint = colorSchemes[scheme][i]; preview.x = 30 + i * 20; preview.y = button.y + 15; self.addChild(preview); } }); function setColorScheme(scheme) { if (currentScheme !== scheme) { buttons[currentScheme].setSelected(false); currentScheme = scheme; buttons[currentScheme].setSelected(true); storage.lastColorScheme = scheme; if (self.onSchemeChange) { self.onSchemeChange(scheme); } } } self.getColorScheme = function () { return currentScheme; }; self.getColor = function (intensity, maxIntensity) { var colors = colorSchemes[currentScheme]; if (!maxIntensity) { maxIntensity = 100; } if (intensity >= maxIntensity) { return 0x000000; // Black for values outside the set } var colorPos = intensity / maxIntensity * (colors.length - 1); var index1 = Math.floor(colorPos); var index2 = Math.min(colors.length - 1, index1 + 1); var fraction = colorPos - index1; return lerpColor(colors[index1], colors[index2], fraction); }; // Set initial selected scheme buttons[currentScheme].setSelected(true); return self; }); var FractalRenderer = Container.expand(function () { var self = Container.call(this); // Fractal renderer size - will be adjusted based on screen var rendererWidth = 1024; var rendererHeight = 1024; var resolution = 4; // Downscale for performance, will render at 256x256 var pixelSize = resolution; var pixels = []; var fractalContainer = new Container(); self.addChild(fractalContainer); self.generateFractal = function (type, params, colorScheme) { // Clear previous fractal for (var i = 0; i < pixels.length; i++) { pixels[i].destroy(); } pixels = []; fractalContainer.removeChildren(); // Calculate center of fractal var centerX = rendererWidth / 2; var centerY = rendererHeight / 2; // Get width and height in steps of resolution var stepsX = Math.ceil(rendererWidth / resolution); var stepsY = Math.ceil(rendererHeight / resolution); // Rendering functions for different fractal types var renderFunctions = { mandelbrot: renderMandelbrot, julia: renderJulia, burning_ship: renderBurningShip, sierpinski: renderSierpinski }; if (renderFunctions[type]) { renderFunctions[type](stepsX, stepsY, params, colorScheme); } else { console.log("Unknown fractal type:", type); } }; function renderMandelbrot(stepsX, stepsY, params, colorScheme) { var iterations = params.iterations; var zoom = params.zoom; var offsetX = params.offsetx; var offsetY = params.offsety; for (var y = 0; y < stepsY; y++) { for (var x = 0; x < stepsX; x++) { // Map pixel position to complex plane var zx = (x - stepsX / 2) / (stepsX / 4) / zoom + offsetX; var zy = (y - stepsY / 2) / (stepsY / 4) / zoom + offsetY; // Initial values for the calculation var cx = zx; var cy = zy; var i = 0; // Calculate mandelbrot value for this pixel while (i < iterations) { var tmp = zx * zx - zy * zy + cx; zy = 2 * zx * zy + cy; zx = tmp; // Check if point is outside circle of radius 2 if (zx * zx + zy * zy > 4) { break; } i++; } // Determine color based on iterations var color = colorScheme.getColor(i, iterations); // Create pixel createPixel(x * pixelSize, y * pixelSize, color); } } } function renderJulia(stepsX, stepsY, params, colorScheme) { var iterations = params.iterations; var zoom = params.zoom; var offsetX = params.offsetx; var offsetY = params.offsety; var cReal = params.juliaconstantreal; var cImag = params.juliaconstantimag; for (var y = 0; y < stepsY; y++) { for (var x = 0; x < stepsX; x++) { // Map pixel position to complex plane var zx = (x - stepsX / 2) / (stepsX / 4) / zoom + offsetX; var zy = (y - stepsY / 2) / (stepsY / 4) / zoom + offsetY; var i = 0; // Calculate julia value for this pixel while (i < iterations) { var tmp = zx * zx - zy * zy + cReal; zy = 2 * zx * zy + cImag; zx = tmp; // Check if point is outside circle of radius 2 if (zx * zx + zy * zy > 4) { break; } i++; } // Determine color based on iterations var color = colorScheme.getColor(i, iterations); // Create pixel createPixel(x * pixelSize, y * pixelSize, color); } } } function renderBurningShip(stepsX, stepsY, params, colorScheme) { var iterations = params.iterations; var zoom = params.zoom; var offsetX = params.offsetx; var offsetY = params.offsety; for (var y = 0; y < stepsY; y++) { for (var x = 0; x < stepsX; x++) { // Map pixel position to complex plane var zx = (x - stepsX / 2) / (stepsX / 4) / zoom + offsetX; var zy = (y - stepsY / 2) / (stepsY / 4) / zoom + offsetY; // Initial values for the calculation var cx = zx; var cy = zy; var i = 0; // Calculate burning ship value for this pixel while (i < iterations) { // The only difference from Mandelbrot is taking absolute values var tmp = zx * zx - zy * zy + cx; zy = Math.abs(2 * zx * zy) + cy; zx = tmp; // Check if point is outside circle of radius 2 if (zx * zx + zy * zy > 4) { break; } i++; } // Determine color based on iterations var color = colorScheme.getColor(i, iterations); // Create pixel createPixel(x * pixelSize, y * pixelSize, color); } } } function renderSierpinski(stepsX, stepsY, params, colorScheme) { var iterations = Math.min(10, params.iterations / 10); // Limit iterations for performance var zoom = params.zoom; var offsetX = params.offsetx * stepsX; var offsetY = params.offsety * stepsY; // Draw the entire canvas black first for (var y = 0; y < stepsY; y++) { for (var x = 0; x < stepsX; x++) { createPixel(x * pixelSize, y * pixelSize, 0x000000); } } // Initialize with a triangle var points = [{ x: stepsX / 2, y: 0 }, { x: 0, y: stepsY }, { x: stepsX, y: stepsY }]; function drawTriangle(p1, p2, p3, iteration) { if (iteration <= 0) { // Draw the triangle var triangle = [p1, p2, p3]; fillTriangle(triangle, colorScheme.getColor(iteration, iterations)); return; } // Calculate midpoints var mid1 = { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 }; var mid2 = { x: (p2.x + p3.x) / 2, y: (p2.y + p3.y) / 2 }; var mid3 = { x: (p3.x + p1.x) / 2, y: (p3.y + p1.y) / 2 }; // Recursive calls for the three corners drawTriangle(p1, mid1, mid3, iteration - 1); drawTriangle(mid1, p2, mid2, iteration - 1); drawTriangle(mid3, mid2, p3, iteration - 1); } function fillTriangle(points, color) { // Basic triangle filling algorithm var minX = Math.min(points[0].x, points[1].x, points[2].x); var maxX = Math.max(points[0].x, points[1].x, points[2].x); var minY = Math.min(points[0].y, points[1].y, points[2].y); var maxY = Math.max(points[0].y, points[1].y, points[2].y); for (var y = minY; y <= maxY; y++) { for (var x = minX; x <= maxX; x++) { if (pointInTriangle(x, y, points[0], points[1], points[2])) { // Ensure we're inside the canvas bounds if (x >= 0 && x < stepsX && y >= 0 && y < stepsY) { setPixelColor(x, y, color); } } } } } function pointInTriangle(x, y, p1, p2, p3) { var denominator = (p2.y - p3.y) * (p1.x - p3.x) + (p3.x - p2.x) * (p1.y - p3.y); var a = ((p2.y - p3.y) * (x - p3.x) + (p3.x - p2.x) * (y - p3.y)) / denominator; var b = ((p3.y - p1.y) * (x - p3.x) + (p1.x - p3.x) * (y - p3.y)) / denominator; var c = 1 - a - b; return a >= 0 && a <= 1 && b >= 0 && b <= 1 && c >= 0 && c <= 1; } function setPixelColor(x, y, color) { var index = y * stepsX + x; if (pixels[index]) { pixels[index].setColor(color); } } // Scale and offset the triangle points.forEach(function (point) { point.x = point.x / zoom + offsetX; point.y = point.y / zoom + offsetY; }); // Draw the Sierpinski triangle drawTriangle(points[0], points[1], points[2], iterations); } function createPixel(x, y, color) { var pixel = new Pixel(); pixel.x = x; pixel.y = y; pixel.setColor(color); pixel.width = pixelSize; pixel.height = pixelSize; fractalContainer.addChild(pixel); pixels.push(pixel); } self.setSize = function (width, height) { rendererWidth = width; rendererHeight = height; }; return self; }); // Helper function to interpolate between two colors var FractalSelector = Container.expand(function () { var self = Container.call(this); var fractalTypes = ['mandelbrot', 'julia', 'burning_ship', 'sierpinski']; var currentType = storage.lastFractalType || 'mandelbrot'; var background = self.attachAsset('controlPanel', { anchorX: 0, anchorY: 0, width: 180, height: 240 }); var title = new Text2("FRACTAL TYPE", { size: 20, fill: 0xFFFFFF }); title.anchor.set(0.5, 0); title.x = 90; title.y = 10; self.addChild(title); var buttons = {}; var yPos = 40; fractalTypes.forEach(function (type, index) { var button = new Button(type, 140, 30); button.x = 90; button.y = yPos + index * 40; button.onButtonClick = function () { setFractalType(type); }; buttons[type] = button; self.addChild(button); }); function setFractalType(type) { if (currentType !== type) { buttons[currentType].setSelected(false); currentType = type; buttons[currentType].setSelected(true); storage.lastFractalType = type; if (self.onTypeChange) { self.onTypeChange(type); } } } self.getFractalType = function () { return currentType; }; // Set initial selected type buttons[currentType].setSelected(true); return self; }); var ParameterControl = Container.expand(function () { var self = Container.call(this); var background = self.attachAsset('controlPanel', { anchorX: 0, anchorY: 0, width: 180, height: 320 }); var title = new Text2("PARAMETERS", { size: 20, fill: 0xFFFFFF }); title.anchor.set(0.5, 0); title.x = 90; title.y = 10; self.addChild(title); // Default parameters for fractals var parameters = { iterations: 100, zoom: 1.0, offsetX: 0, offsetY: 0, juliaConstantReal: -0.7, juliaConstantImag: 0.27 }; var paramTexts = {}; var buttons = {}; var yPos = 40; // Create controls for iterations createParameterControl("iterations", 40, 100, 10); createParameterControl("zoom", 90, 1.0, 0.5); createParameterControl("offset X", 140, 0, 0.1); createParameterControl("offset Y", 190, 0, 0.1); createParameterControl("Julia Real", 240, -0.7, 0.05, "julia"); createParameterControl("Julia Imag", 290, 0.27, 0.05, "julia"); function createParameterControl(name, y, defaultValue, step, visibleFor) { var paramName = name.toLowerCase().replace(/\s+/g, ""); var label = new Text2(name + ":", { size: 16, fill: 0xFFFFFF }); label.anchor.set(0, 0.5); label.x = 10; label.y = y; self.addChild(label); var value = new Text2(defaultValue.toString(), { size: 16, fill: 0xFFFFFF }); value.anchor.set(0.5, 0.5); value.x = 100; value.y = y; self.addChild(value); paramTexts[paramName] = value; // Decrease button var minusButton = new Button("-", 30, 30); minusButton.x = 140; minusButton.y = y; minusButton.onButtonClick = function () { updateParameter(paramName, -step); }; self.addChild(minusButton); // Increase button var plusButton = new Button("+", 30, 30); plusButton.x = 175; plusButton.y = y; plusButton.onButtonClick = function () { updateParameter(paramName, step); }; self.addChild(plusButton); if (visibleFor) { label.visible = false; value.visible = false; minusButton.visible = false; plusButton.visible = false; // Store the buttons and visibility condition buttons[paramName] = { label: label, value: value, minus: minusButton, plus: plusButton, visibleFor: visibleFor }; } // Set the default value parameters[paramName] = defaultValue; } function updateParameter(name, delta) { var newValue = parameters[name] + delta; // Apply constraints based on parameter if (name === "iterations") { newValue = Math.max(10, Math.min(500, newValue)); } else if (name === "zoom") { newValue = Math.max(0.1, Math.min(10, newValue)); } parameters[name] = newValue; paramTexts[name].setText(newValue.toFixed(2).replace(/\.00$/, "")); if (self.onParameterChange) { self.onParameterChange(parameters); } } self.updateVisibility = function (fractalType) { // Show/hide parameters based on fractal type Object.keys(buttons).forEach(function (key) { var control = buttons[key]; var shouldBeVisible = control.visibleFor === fractalType; control.label.visible = shouldBeVisible; control.value.visible = shouldBeVisible; control.minus.visible = shouldBeVisible; control.plus.visible = shouldBeVisible; }); }; self.getParameters = function () { return parameters; }; return self; }); var Pixel = Container.expand(function () { var self = Container.call(this); var pixelGraphics = self.attachAsset('pixel', { anchorX: 0, anchorY: 0 }); self.setColor = function (color) { pixelGraphics.tint = color; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x000000 }); /**** * Game Code ****/ // Helper function to interpolate between two colors // Main title function lerpColor(color1, color2, amount) { var r1 = color1 >> 16 & 0xFF; var g1 = color1 >> 8 & 0xFF; var b1 = color1 & 0xFF; var r2 = color2 >> 16 & 0xFF; var g2 = color2 >> 8 & 0xFF; var b2 = color2 & 0xFF; var r = Math.round(r1 + (r2 - r1) * amount); var g = Math.round(g1 + (g2 - g1) * amount); var b = Math.round(b1 + (b1 - b1) * amount); return r << 16 | g << 8 | b; } var title = new Text2("Fractal Explorer", { size: 60, fill: 0xFFFFFF }); title.anchor.set(0.5, 0); title.x = 2048 / 2; title.y = 60; game.addChild(title); // Create fractal renderer var fractalRenderer = new FractalRenderer(); fractalRenderer.x = 250; // Offset to allow space for the controls fractalRenderer.y = 200; game.addChild(fractalRenderer); // Calculate renderer size based on screen var rendererSize = Math.min(2048 - 500, 2732 - 400); fractalRenderer.setSize(rendererSize, rendererSize); // Create controls var fractalSelector = new FractalSelector(); fractalSelector.x = 30; fractalSelector.y = 200; game.addChild(fractalSelector); var colorPicker = new ColorPicker(); colorPicker.x = 30; colorPicker.y = 450; game.addChild(colorPicker); var parameterControl = new ParameterControl(); parameterControl.x = 1848; // Right side of screen parameterControl.y = 200; game.addChild(parameterControl); // Create generate button var generateButton = new Button("GENERATE", 200, 60); generateButton.x = 2048 / 2; generateButton.y = 2732 - 100; generateButton.onButtonClick = generateFractal; game.addChild(generateButton); // Start background music LK.playMusic('ambient', { loop: true }); // Show initial fractal visibility parameterControl.updateVisibility(fractalSelector.getFractalType()); // Handle events when fractal type changes fractalSelector.onTypeChange = function (type) { parameterControl.updateVisibility(type); generateFractal(); }; // Handle events when parameters change parameterControl.onParameterChange = function (params) { // Debounce for performance if (generateTimer) { LK.clearTimeout(generateTimer); } generateTimer = LK.setTimeout(generateFractal, 300); }; // Handle events when color scheme changes colorPicker.onSchemeChange = function (scheme) { generateFractal(); }; var generateTimer = null; // Main function to generate the fractal function generateFractal() { var type = fractalSelector.getFractalType(); var params = parameterControl.getParameters(); // Show "Generating..." message generateButton.setLabel("GENERATING..."); // Use a timeout to allow the button text to update LK.setTimeout(function () { fractalRenderer.generateFractal(type, params, colorPicker); generateButton.setLabel("GENERATE"); }, 100); } // Generate initial fractal generateFractal(); // Pan and zoom variables var isDragging = false; var lastX = 0; var lastY = 0; // Handle input events for panning and zooming game.down = function (x, y, obj) { isDragging = true; lastX = x; lastY = y; }; game.move = function (x, y, obj) { if (isDragging) { // Convert screen coordinates to parameter offsets var params = parameterControl.getParameters(); var deltaX = (x - lastX) / (1000 * params.zoom); var deltaY = (y - lastY) / (1000 * params.zoom); // Update parameters params.offsetx -= deltaX; params.offsety -= deltaY; // Update UI and regenerate parameterControl.onParameterChange(params); // Update last position lastX = x; lastY = y; } }; game.up = function (x, y, obj) { isDragging = false; }; game.update = function () { // Main update loop - nothing needed here as the rendering is event-driven };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
savedFractals: [],
lastFractalType: "mandelbrot",
lastColorScheme: "rainbow"
});
/****
* Classes
****/
var Button = Container.expand(function (label, width, height) {
var self = Container.call(this);
var buttonGraphics = self.attachAsset('controlButton', {
anchorX: 0.5,
anchorY: 0.5,
width: width || 120,
height: height || 60
});
var buttonText = new Text2(label, {
size: 24,
fill: 0xFFFFFF
});
buttonText.anchor.set(0.5, 0.5);
self.addChild(buttonText);
self.setLabel = function (newLabel) {
buttonText.setText(newLabel);
};
self.setSelected = function (selected) {
if (selected) {
buttonGraphics.tint = 0x88AAFF;
} else {
buttonGraphics.tint = 0xFFFFFF;
}
};
self.down = function () {
LK.getSound('click').play();
buttonGraphics.tint = 0x88AAFF;
};
self.up = function () {
if (self.onButtonClick) {
self.onButtonClick();
}
buttonGraphics.tint = 0xFFFFFF;
};
return self;
});
var ColorPicker = Container.expand(function () {
var self = Container.call(this);
var colorSchemes = {
rainbow: [0xFF0000, 0xFF7F00, 0xFFFF00, 0x00FF00, 0x0000FF, 0x4B0082, 0x9400D3],
fire: [0xFF0000, 0xFF5500, 0xFF8800, 0xFFAA00, 0xFFCC00, 0xFFFF00, 0xFFFFAA],
ocean: [0x000033, 0x000066, 0x000099, 0x0000CC, 0x0000FF, 0x3399FF, 0x66CCFF],
grayscale: [0x000000, 0x333333, 0x666666, 0x999999, 0xCCCCCC, 0xFFFFFF, 0xFFFFFF],
neon: [0xFF00FF, 0xFF33FF, 0xFF66FF, 0xFF99FF, 0xFFCCFF, 0x00FFFF, 0x33FFFF]
};
var currentScheme = storage.lastColorScheme || 'rainbow';
var background = self.attachAsset('controlPanel', {
anchorX: 0,
anchorY: 0,
width: 180,
height: 240
});
var title = new Text2("COLOR SCHEME", {
size: 20,
fill: 0xFFFFFF
});
title.anchor.set(0.5, 0);
title.x = 90;
title.y = 10;
self.addChild(title);
var buttons = {};
var yPos = 40;
Object.keys(colorSchemes).forEach(function (scheme, index) {
var button = new Button(scheme, 140, 30);
button.x = 90;
button.y = yPos + index * 40;
button.onButtonClick = function () {
setColorScheme(scheme);
};
buttons[scheme] = button;
self.addChild(button);
// Preview colors
for (var i = 0; i < 5; i++) {
var preview = self.attachAsset('colorPreview', {
anchorX: 0.5,
anchorY: 0.5,
width: 15,
height: 15
});
preview.tint = colorSchemes[scheme][i];
preview.x = 30 + i * 20;
preview.y = button.y + 15;
self.addChild(preview);
}
});
function setColorScheme(scheme) {
if (currentScheme !== scheme) {
buttons[currentScheme].setSelected(false);
currentScheme = scheme;
buttons[currentScheme].setSelected(true);
storage.lastColorScheme = scheme;
if (self.onSchemeChange) {
self.onSchemeChange(scheme);
}
}
}
self.getColorScheme = function () {
return currentScheme;
};
self.getColor = function (intensity, maxIntensity) {
var colors = colorSchemes[currentScheme];
if (!maxIntensity) {
maxIntensity = 100;
}
if (intensity >= maxIntensity) {
return 0x000000; // Black for values outside the set
}
var colorPos = intensity / maxIntensity * (colors.length - 1);
var index1 = Math.floor(colorPos);
var index2 = Math.min(colors.length - 1, index1 + 1);
var fraction = colorPos - index1;
return lerpColor(colors[index1], colors[index2], fraction);
};
// Set initial selected scheme
buttons[currentScheme].setSelected(true);
return self;
});
var FractalRenderer = Container.expand(function () {
var self = Container.call(this);
// Fractal renderer size - will be adjusted based on screen
var rendererWidth = 1024;
var rendererHeight = 1024;
var resolution = 4; // Downscale for performance, will render at 256x256
var pixelSize = resolution;
var pixels = [];
var fractalContainer = new Container();
self.addChild(fractalContainer);
self.generateFractal = function (type, params, colorScheme) {
// Clear previous fractal
for (var i = 0; i < pixels.length; i++) {
pixels[i].destroy();
}
pixels = [];
fractalContainer.removeChildren();
// Calculate center of fractal
var centerX = rendererWidth / 2;
var centerY = rendererHeight / 2;
// Get width and height in steps of resolution
var stepsX = Math.ceil(rendererWidth / resolution);
var stepsY = Math.ceil(rendererHeight / resolution);
// Rendering functions for different fractal types
var renderFunctions = {
mandelbrot: renderMandelbrot,
julia: renderJulia,
burning_ship: renderBurningShip,
sierpinski: renderSierpinski
};
if (renderFunctions[type]) {
renderFunctions[type](stepsX, stepsY, params, colorScheme);
} else {
console.log("Unknown fractal type:", type);
}
};
function renderMandelbrot(stepsX, stepsY, params, colorScheme) {
var iterations = params.iterations;
var zoom = params.zoom;
var offsetX = params.offsetx;
var offsetY = params.offsety;
for (var y = 0; y < stepsY; y++) {
for (var x = 0; x < stepsX; x++) {
// Map pixel position to complex plane
var zx = (x - stepsX / 2) / (stepsX / 4) / zoom + offsetX;
var zy = (y - stepsY / 2) / (stepsY / 4) / zoom + offsetY;
// Initial values for the calculation
var cx = zx;
var cy = zy;
var i = 0;
// Calculate mandelbrot value for this pixel
while (i < iterations) {
var tmp = zx * zx - zy * zy + cx;
zy = 2 * zx * zy + cy;
zx = tmp;
// Check if point is outside circle of radius 2
if (zx * zx + zy * zy > 4) {
break;
}
i++;
}
// Determine color based on iterations
var color = colorScheme.getColor(i, iterations);
// Create pixel
createPixel(x * pixelSize, y * pixelSize, color);
}
}
}
function renderJulia(stepsX, stepsY, params, colorScheme) {
var iterations = params.iterations;
var zoom = params.zoom;
var offsetX = params.offsetx;
var offsetY = params.offsety;
var cReal = params.juliaconstantreal;
var cImag = params.juliaconstantimag;
for (var y = 0; y < stepsY; y++) {
for (var x = 0; x < stepsX; x++) {
// Map pixel position to complex plane
var zx = (x - stepsX / 2) / (stepsX / 4) / zoom + offsetX;
var zy = (y - stepsY / 2) / (stepsY / 4) / zoom + offsetY;
var i = 0;
// Calculate julia value for this pixel
while (i < iterations) {
var tmp = zx * zx - zy * zy + cReal;
zy = 2 * zx * zy + cImag;
zx = tmp;
// Check if point is outside circle of radius 2
if (zx * zx + zy * zy > 4) {
break;
}
i++;
}
// Determine color based on iterations
var color = colorScheme.getColor(i, iterations);
// Create pixel
createPixel(x * pixelSize, y * pixelSize, color);
}
}
}
function renderBurningShip(stepsX, stepsY, params, colorScheme) {
var iterations = params.iterations;
var zoom = params.zoom;
var offsetX = params.offsetx;
var offsetY = params.offsety;
for (var y = 0; y < stepsY; y++) {
for (var x = 0; x < stepsX; x++) {
// Map pixel position to complex plane
var zx = (x - stepsX / 2) / (stepsX / 4) / zoom + offsetX;
var zy = (y - stepsY / 2) / (stepsY / 4) / zoom + offsetY;
// Initial values for the calculation
var cx = zx;
var cy = zy;
var i = 0;
// Calculate burning ship value for this pixel
while (i < iterations) {
// The only difference from Mandelbrot is taking absolute values
var tmp = zx * zx - zy * zy + cx;
zy = Math.abs(2 * zx * zy) + cy;
zx = tmp;
// Check if point is outside circle of radius 2
if (zx * zx + zy * zy > 4) {
break;
}
i++;
}
// Determine color based on iterations
var color = colorScheme.getColor(i, iterations);
// Create pixel
createPixel(x * pixelSize, y * pixelSize, color);
}
}
}
function renderSierpinski(stepsX, stepsY, params, colorScheme) {
var iterations = Math.min(10, params.iterations / 10); // Limit iterations for performance
var zoom = params.zoom;
var offsetX = params.offsetx * stepsX;
var offsetY = params.offsety * stepsY;
// Draw the entire canvas black first
for (var y = 0; y < stepsY; y++) {
for (var x = 0; x < stepsX; x++) {
createPixel(x * pixelSize, y * pixelSize, 0x000000);
}
}
// Initialize with a triangle
var points = [{
x: stepsX / 2,
y: 0
}, {
x: 0,
y: stepsY
}, {
x: stepsX,
y: stepsY
}];
function drawTriangle(p1, p2, p3, iteration) {
if (iteration <= 0) {
// Draw the triangle
var triangle = [p1, p2, p3];
fillTriangle(triangle, colorScheme.getColor(iteration, iterations));
return;
}
// Calculate midpoints
var mid1 = {
x: (p1.x + p2.x) / 2,
y: (p1.y + p2.y) / 2
};
var mid2 = {
x: (p2.x + p3.x) / 2,
y: (p2.y + p3.y) / 2
};
var mid3 = {
x: (p3.x + p1.x) / 2,
y: (p3.y + p1.y) / 2
};
// Recursive calls for the three corners
drawTriangle(p1, mid1, mid3, iteration - 1);
drawTriangle(mid1, p2, mid2, iteration - 1);
drawTriangle(mid3, mid2, p3, iteration - 1);
}
function fillTriangle(points, color) {
// Basic triangle filling algorithm
var minX = Math.min(points[0].x, points[1].x, points[2].x);
var maxX = Math.max(points[0].x, points[1].x, points[2].x);
var minY = Math.min(points[0].y, points[1].y, points[2].y);
var maxY = Math.max(points[0].y, points[1].y, points[2].y);
for (var y = minY; y <= maxY; y++) {
for (var x = minX; x <= maxX; x++) {
if (pointInTriangle(x, y, points[0], points[1], points[2])) {
// Ensure we're inside the canvas bounds
if (x >= 0 && x < stepsX && y >= 0 && y < stepsY) {
setPixelColor(x, y, color);
}
}
}
}
}
function pointInTriangle(x, y, p1, p2, p3) {
var denominator = (p2.y - p3.y) * (p1.x - p3.x) + (p3.x - p2.x) * (p1.y - p3.y);
var a = ((p2.y - p3.y) * (x - p3.x) + (p3.x - p2.x) * (y - p3.y)) / denominator;
var b = ((p3.y - p1.y) * (x - p3.x) + (p1.x - p3.x) * (y - p3.y)) / denominator;
var c = 1 - a - b;
return a >= 0 && a <= 1 && b >= 0 && b <= 1 && c >= 0 && c <= 1;
}
function setPixelColor(x, y, color) {
var index = y * stepsX + x;
if (pixels[index]) {
pixels[index].setColor(color);
}
}
// Scale and offset the triangle
points.forEach(function (point) {
point.x = point.x / zoom + offsetX;
point.y = point.y / zoom + offsetY;
});
// Draw the Sierpinski triangle
drawTriangle(points[0], points[1], points[2], iterations);
}
function createPixel(x, y, color) {
var pixel = new Pixel();
pixel.x = x;
pixel.y = y;
pixel.setColor(color);
pixel.width = pixelSize;
pixel.height = pixelSize;
fractalContainer.addChild(pixel);
pixels.push(pixel);
}
self.setSize = function (width, height) {
rendererWidth = width;
rendererHeight = height;
};
return self;
});
// Helper function to interpolate between two colors
var FractalSelector = Container.expand(function () {
var self = Container.call(this);
var fractalTypes = ['mandelbrot', 'julia', 'burning_ship', 'sierpinski'];
var currentType = storage.lastFractalType || 'mandelbrot';
var background = self.attachAsset('controlPanel', {
anchorX: 0,
anchorY: 0,
width: 180,
height: 240
});
var title = new Text2("FRACTAL TYPE", {
size: 20,
fill: 0xFFFFFF
});
title.anchor.set(0.5, 0);
title.x = 90;
title.y = 10;
self.addChild(title);
var buttons = {};
var yPos = 40;
fractalTypes.forEach(function (type, index) {
var button = new Button(type, 140, 30);
button.x = 90;
button.y = yPos + index * 40;
button.onButtonClick = function () {
setFractalType(type);
};
buttons[type] = button;
self.addChild(button);
});
function setFractalType(type) {
if (currentType !== type) {
buttons[currentType].setSelected(false);
currentType = type;
buttons[currentType].setSelected(true);
storage.lastFractalType = type;
if (self.onTypeChange) {
self.onTypeChange(type);
}
}
}
self.getFractalType = function () {
return currentType;
};
// Set initial selected type
buttons[currentType].setSelected(true);
return self;
});
var ParameterControl = Container.expand(function () {
var self = Container.call(this);
var background = self.attachAsset('controlPanel', {
anchorX: 0,
anchorY: 0,
width: 180,
height: 320
});
var title = new Text2("PARAMETERS", {
size: 20,
fill: 0xFFFFFF
});
title.anchor.set(0.5, 0);
title.x = 90;
title.y = 10;
self.addChild(title);
// Default parameters for fractals
var parameters = {
iterations: 100,
zoom: 1.0,
offsetX: 0,
offsetY: 0,
juliaConstantReal: -0.7,
juliaConstantImag: 0.27
};
var paramTexts = {};
var buttons = {};
var yPos = 40;
// Create controls for iterations
createParameterControl("iterations", 40, 100, 10);
createParameterControl("zoom", 90, 1.0, 0.5);
createParameterControl("offset X", 140, 0, 0.1);
createParameterControl("offset Y", 190, 0, 0.1);
createParameterControl("Julia Real", 240, -0.7, 0.05, "julia");
createParameterControl("Julia Imag", 290, 0.27, 0.05, "julia");
function createParameterControl(name, y, defaultValue, step, visibleFor) {
var paramName = name.toLowerCase().replace(/\s+/g, "");
var label = new Text2(name + ":", {
size: 16,
fill: 0xFFFFFF
});
label.anchor.set(0, 0.5);
label.x = 10;
label.y = y;
self.addChild(label);
var value = new Text2(defaultValue.toString(), {
size: 16,
fill: 0xFFFFFF
});
value.anchor.set(0.5, 0.5);
value.x = 100;
value.y = y;
self.addChild(value);
paramTexts[paramName] = value;
// Decrease button
var minusButton = new Button("-", 30, 30);
minusButton.x = 140;
minusButton.y = y;
minusButton.onButtonClick = function () {
updateParameter(paramName, -step);
};
self.addChild(minusButton);
// Increase button
var plusButton = new Button("+", 30, 30);
plusButton.x = 175;
plusButton.y = y;
plusButton.onButtonClick = function () {
updateParameter(paramName, step);
};
self.addChild(plusButton);
if (visibleFor) {
label.visible = false;
value.visible = false;
minusButton.visible = false;
plusButton.visible = false;
// Store the buttons and visibility condition
buttons[paramName] = {
label: label,
value: value,
minus: minusButton,
plus: plusButton,
visibleFor: visibleFor
};
}
// Set the default value
parameters[paramName] = defaultValue;
}
function updateParameter(name, delta) {
var newValue = parameters[name] + delta;
// Apply constraints based on parameter
if (name === "iterations") {
newValue = Math.max(10, Math.min(500, newValue));
} else if (name === "zoom") {
newValue = Math.max(0.1, Math.min(10, newValue));
}
parameters[name] = newValue;
paramTexts[name].setText(newValue.toFixed(2).replace(/\.00$/, ""));
if (self.onParameterChange) {
self.onParameterChange(parameters);
}
}
self.updateVisibility = function (fractalType) {
// Show/hide parameters based on fractal type
Object.keys(buttons).forEach(function (key) {
var control = buttons[key];
var shouldBeVisible = control.visibleFor === fractalType;
control.label.visible = shouldBeVisible;
control.value.visible = shouldBeVisible;
control.minus.visible = shouldBeVisible;
control.plus.visible = shouldBeVisible;
});
};
self.getParameters = function () {
return parameters;
};
return self;
});
var Pixel = Container.expand(function () {
var self = Container.call(this);
var pixelGraphics = self.attachAsset('pixel', {
anchorX: 0,
anchorY: 0
});
self.setColor = function (color) {
pixelGraphics.tint = color;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
// Helper function to interpolate between two colors
// Main title
function lerpColor(color1, color2, amount) {
var r1 = color1 >> 16 & 0xFF;
var g1 = color1 >> 8 & 0xFF;
var b1 = color1 & 0xFF;
var r2 = color2 >> 16 & 0xFF;
var g2 = color2 >> 8 & 0xFF;
var b2 = color2 & 0xFF;
var r = Math.round(r1 + (r2 - r1) * amount);
var g = Math.round(g1 + (g2 - g1) * amount);
var b = Math.round(b1 + (b1 - b1) * amount);
return r << 16 | g << 8 | b;
}
var title = new Text2("Fractal Explorer", {
size: 60,
fill: 0xFFFFFF
});
title.anchor.set(0.5, 0);
title.x = 2048 / 2;
title.y = 60;
game.addChild(title);
// Create fractal renderer
var fractalRenderer = new FractalRenderer();
fractalRenderer.x = 250; // Offset to allow space for the controls
fractalRenderer.y = 200;
game.addChild(fractalRenderer);
// Calculate renderer size based on screen
var rendererSize = Math.min(2048 - 500, 2732 - 400);
fractalRenderer.setSize(rendererSize, rendererSize);
// Create controls
var fractalSelector = new FractalSelector();
fractalSelector.x = 30;
fractalSelector.y = 200;
game.addChild(fractalSelector);
var colorPicker = new ColorPicker();
colorPicker.x = 30;
colorPicker.y = 450;
game.addChild(colorPicker);
var parameterControl = new ParameterControl();
parameterControl.x = 1848; // Right side of screen
parameterControl.y = 200;
game.addChild(parameterControl);
// Create generate button
var generateButton = new Button("GENERATE", 200, 60);
generateButton.x = 2048 / 2;
generateButton.y = 2732 - 100;
generateButton.onButtonClick = generateFractal;
game.addChild(generateButton);
// Start background music
LK.playMusic('ambient', {
loop: true
});
// Show initial fractal visibility
parameterControl.updateVisibility(fractalSelector.getFractalType());
// Handle events when fractal type changes
fractalSelector.onTypeChange = function (type) {
parameterControl.updateVisibility(type);
generateFractal();
};
// Handle events when parameters change
parameterControl.onParameterChange = function (params) {
// Debounce for performance
if (generateTimer) {
LK.clearTimeout(generateTimer);
}
generateTimer = LK.setTimeout(generateFractal, 300);
};
// Handle events when color scheme changes
colorPicker.onSchemeChange = function (scheme) {
generateFractal();
};
var generateTimer = null;
// Main function to generate the fractal
function generateFractal() {
var type = fractalSelector.getFractalType();
var params = parameterControl.getParameters();
// Show "Generating..." message
generateButton.setLabel("GENERATING...");
// Use a timeout to allow the button text to update
LK.setTimeout(function () {
fractalRenderer.generateFractal(type, params, colorPicker);
generateButton.setLabel("GENERATE");
}, 100);
}
// Generate initial fractal
generateFractal();
// Pan and zoom variables
var isDragging = false;
var lastX = 0;
var lastY = 0;
// Handle input events for panning and zooming
game.down = function (x, y, obj) {
isDragging = true;
lastX = x;
lastY = y;
};
game.move = function (x, y, obj) {
if (isDragging) {
// Convert screen coordinates to parameter offsets
var params = parameterControl.getParameters();
var deltaX = (x - lastX) / (1000 * params.zoom);
var deltaY = (y - lastY) / (1000 * params.zoom);
// Update parameters
params.offsetx -= deltaX;
params.offsety -= deltaY;
// Update UI and regenerate
parameterControl.onParameterChange(params);
// Update last position
lastX = x;
lastY = y;
}
};
game.up = function (x, y, obj) {
isDragging = false;
};
game.update = function () {
// Main update loop - nothing needed here as the rendering is event-driven
};