This commit is contained in:
parent
1b1d54315c
commit
cef4707307
4 changed files with 232 additions and 13 deletions
|
|
@ -4,11 +4,13 @@
|
||||||
import { screensaver, isScreensaverActive, activeScreensaverType } from '$lib/stores/screensaver';
|
import { screensaver, isScreensaverActive, activeScreensaverType } from '$lib/stores/screensaver';
|
||||||
import Snowfall from './screensavers/Snowfall.svelte';
|
import Snowfall from './screensavers/Snowfall.svelte';
|
||||||
import FractalCrystalline from './screensavers/FractalCrystalline.svelte';
|
import FractalCrystalline from './screensavers/FractalCrystalline.svelte';
|
||||||
|
import MandelbrotZoom from './screensavers/MandelbrotZoom.svelte';
|
||||||
|
|
||||||
// Map type to component
|
// Map type to component
|
||||||
const screensaverComponents = {
|
const screensaverComponents = {
|
||||||
snowfall: Snowfall,
|
snowfall: Snowfall,
|
||||||
fractal_crystalline: FractalCrystalline
|
fractal_crystalline: FractalCrystalline,
|
||||||
|
mandelbrot_zoom: MandelbrotZoom
|
||||||
};
|
};
|
||||||
|
|
||||||
// Dismiss on any interaction
|
// Dismiss on any interaction
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
maxBranches: 800, // Max simultaneous branch tips
|
maxBranches: 800, // Max simultaneous branch tips
|
||||||
branchChance: 0.03, // Chance to spawn new branch per frame
|
branchChance: 0.03, // Chance to spawn new branch per frame
|
||||||
turnAngle: 0.3, // Max random turn per frame (radians)
|
turnAngle: 0.3, // Max random turn per frame (radians)
|
||||||
hueShiftSpeed: 0.2, // Color cycling speed
|
hueShiftSpeed: 0.5, // Color cycling speed (faster for more color variety)
|
||||||
maxPoints: 15000, // Max crystal points (kept for reference)
|
maxPoints: 15000, // Max crystal points (kept for reference)
|
||||||
shatterDuration: 2500, // Milliseconds for shatter effect
|
shatterDuration: 2500, // Milliseconds for shatter effect
|
||||||
lineWidth: 2
|
lineWidth: 2
|
||||||
|
|
@ -112,11 +112,14 @@
|
||||||
ctx.fillRect(0, 0, width, height);
|
ctx.fillRect(0, 0, width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create seed points at random locations
|
// Create seed points at random locations with spread colors
|
||||||
for (let i = 0; i < CONFIG.seedCount; i++) {
|
for (let i = 0; i < CONFIG.seedCount; i++) {
|
||||||
const x = Math.random() * width;
|
const x = Math.random() * width;
|
||||||
const y = Math.random() * height;
|
const y = Math.random() * height;
|
||||||
|
|
||||||
|
// Base hue for this seed - spread evenly across spectrum
|
||||||
|
const seedHue = (i * 360 / CONFIG.seedCount) + Math.random() * 30;
|
||||||
|
|
||||||
// Each seed spawns multiple branches in different directions
|
// Each seed spawns multiple branches in different directions
|
||||||
const branchCount = 3 + Math.floor(Math.random() * 4);
|
const branchCount = 3 + Math.floor(Math.random() * 4);
|
||||||
for (let j = 0; j < branchCount; j++) {
|
for (let j = 0; j < branchCount; j++) {
|
||||||
|
|
@ -125,7 +128,7 @@
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
angle,
|
angle,
|
||||||
hue: Math.random() * 360,
|
hue: (seedHue + j * 25 + Math.random() * 20) % 360, // Each branch gets varied hue
|
||||||
age: 0,
|
age: 0,
|
||||||
generation: 0
|
generation: 0
|
||||||
});
|
});
|
||||||
|
|
@ -178,9 +181,11 @@
|
||||||
hue: (branch.hue + branch.age * 0.5) % 360
|
hue: (branch.hue + branch.age * 0.5) % 360
|
||||||
});
|
});
|
||||||
|
|
||||||
// Draw the branch segment
|
// Draw the branch segment with varied colors
|
||||||
const h = (branch.hue + branch.age * 0.5) % 360;
|
const h = (branch.hue + branch.age * 1.5) % 360; // Faster hue shift
|
||||||
ctx.strokeStyle = `hsla(${h}, 80%, 60%, 0.9)`;
|
const s = 70 + Math.sin(branch.age * 0.1) * 20; // Saturation varies 50-90%
|
||||||
|
const l = 50 + Math.cos(branch.age * 0.05) * 15; // Lightness varies 35-65%
|
||||||
|
ctx.strokeStyle = `hsla(${h}, ${s}%, ${l}%, 0.9)`;
|
||||||
ctx.lineWidth = CONFIG.lineWidth;
|
ctx.lineWidth = CONFIG.lineWidth;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(branch.x, branch.y);
|
ctx.moveTo(branch.x, branch.y);
|
||||||
|
|
@ -192,14 +197,14 @@
|
||||||
branch.y = newY;
|
branch.y = newY;
|
||||||
branch.age++;
|
branch.age++;
|
||||||
|
|
||||||
// Chance to spawn a new branch (fork)
|
// Chance to spawn a new branch (fork) with distinct color
|
||||||
if (Math.random() < CONFIG.branchChance && branches.length + newBranches.length < CONFIG.maxBranches) {
|
if (Math.random() < CONFIG.branchChance && branches.length + newBranches.length < CONFIG.maxBranches) {
|
||||||
const forkAngle = branch.angle + (Math.random() > 0.5 ? 1 : -1) * (0.3 + Math.random() * 0.7);
|
const forkAngle = branch.angle + (Math.random() > 0.5 ? 1 : -1) * (0.3 + Math.random() * 0.7);
|
||||||
newBranches.push({
|
newBranches.push({
|
||||||
x: newX,
|
x: newX,
|
||||||
y: newY,
|
y: newY,
|
||||||
angle: forkAngle,
|
angle: forkAngle,
|
||||||
hue: (branch.hue + 20 + Math.random() * 40) % 360,
|
hue: (branch.hue + 30 + Math.random() * 90) % 360, // More color variation
|
||||||
age: 0,
|
age: 0,
|
||||||
generation: branch.generation + 1
|
generation: branch.generation + 1
|
||||||
});
|
});
|
||||||
|
|
@ -228,6 +233,7 @@
|
||||||
|
|
||||||
// Only spawn if we found an unoccupied spot
|
// Only spawn if we found an unoccupied spot
|
||||||
if (!isOccupied(x, y)) {
|
if (!isOccupied(x, y)) {
|
||||||
|
const seedHue = Math.random() * 360; // Random base hue for new seed
|
||||||
const branchCount = 2 + Math.floor(Math.random() * 3);
|
const branchCount = 2 + Math.floor(Math.random() * 3);
|
||||||
for (let j = 0; j < branchCount; j++) {
|
for (let j = 0; j < branchCount; j++) {
|
||||||
const angle = Math.random() * Math.PI * 2;
|
const angle = Math.random() * Math.PI * 2;
|
||||||
|
|
@ -235,7 +241,7 @@
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
angle,
|
angle,
|
||||||
hue: Math.random() * 360,
|
hue: (seedHue + j * 40 + Math.random() * 30) % 360, // Varied colors
|
||||||
age: 0,
|
age: 0,
|
||||||
generation: 0
|
generation: 0
|
||||||
});
|
});
|
||||||
|
|
|
||||||
209
frontend/src/lib/components/screensavers/MandelbrotZoom.svelte
Normal file
209
frontend/src/lib/components/screensavers/MandelbrotZoom.svelte
Normal file
|
|
@ -0,0 +1,209 @@
|
||||||
|
<script>
|
||||||
|
import { onMount, onDestroy } from 'svelte';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
|
let canvas;
|
||||||
|
let ctx;
|
||||||
|
let animationId;
|
||||||
|
let width, height;
|
||||||
|
|
||||||
|
// Mandelbrot parameters
|
||||||
|
let centerX = -0.743643887037151; // Interesting zoom point
|
||||||
|
let centerY = 0.131825904205330;
|
||||||
|
let zoom = 1;
|
||||||
|
let zoomSpeed = 1.02; // Zoom multiplier per frame
|
||||||
|
|
||||||
|
// Alternative interesting points to cycle through
|
||||||
|
const interestingPoints = [
|
||||||
|
{ x: -0.743643887037151, y: 0.131825904205330 }, // Seahorse valley
|
||||||
|
{ x: -0.74529, y: 0.113075 }, // Spiral
|
||||||
|
{ x: -0.16, y: 1.0405 }, // Branch
|
||||||
|
{ x: -1.25066, y: 0.02012 }, // Mini mandelbrot
|
||||||
|
{ x: -0.235125, y: 0.827215 }, // Tendril
|
||||||
|
];
|
||||||
|
let currentPointIndex = 0;
|
||||||
|
|
||||||
|
const CONFIG = {
|
||||||
|
maxIterations: 150,
|
||||||
|
zoomResetThreshold: 1e12, // Reset zoom after this level
|
||||||
|
colorCycles: 3 // Number of color cycles in the gradient
|
||||||
|
};
|
||||||
|
|
||||||
|
function initCanvas() {
|
||||||
|
if (!canvas) return;
|
||||||
|
width = window.innerWidth;
|
||||||
|
height = window.innerHeight;
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
ctx = canvas.getContext('2d');
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetZoom() {
|
||||||
|
// Pick next interesting point
|
||||||
|
currentPointIndex = (currentPointIndex + 1) % interestingPoints.length;
|
||||||
|
const point = interestingPoints[currentPointIndex];
|
||||||
|
centerX = point.x;
|
||||||
|
centerY = point.y;
|
||||||
|
zoom = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gold color palette
|
||||||
|
function getGoldColor(iteration, maxIter) {
|
||||||
|
if (iteration === maxIter) return { r: 0, g: 0, b: 0 }; // Black for inside
|
||||||
|
|
||||||
|
const t = iteration / maxIter;
|
||||||
|
const cycled = (t * CONFIG.colorCycles) % 1;
|
||||||
|
|
||||||
|
// Gold gradient: dark brown -> gold -> bright gold -> white gold
|
||||||
|
let r, g, b;
|
||||||
|
|
||||||
|
if (cycled < 0.25) {
|
||||||
|
// Dark brown to gold
|
||||||
|
const s = cycled / 0.25;
|
||||||
|
r = Math.floor(40 + s * 175); // 40 -> 215
|
||||||
|
g = Math.floor(20 + s * 155); // 20 -> 175
|
||||||
|
b = Math.floor(5 + s * 30); // 5 -> 35
|
||||||
|
} else if (cycled < 0.5) {
|
||||||
|
// Gold to bright gold
|
||||||
|
const s = (cycled - 0.25) / 0.25;
|
||||||
|
r = Math.floor(215 + s * 40); // 215 -> 255
|
||||||
|
g = Math.floor(175 + s * 40); // 175 -> 215
|
||||||
|
b = Math.floor(35 + s * 65); // 35 -> 100
|
||||||
|
} else if (cycled < 0.75) {
|
||||||
|
// Bright gold to white gold
|
||||||
|
const s = (cycled - 0.5) / 0.25;
|
||||||
|
r = 255;
|
||||||
|
g = Math.floor(215 + s * 30); // 215 -> 245
|
||||||
|
b = Math.floor(100 + s * 100); // 100 -> 200
|
||||||
|
} else {
|
||||||
|
// White gold back to dark brown
|
||||||
|
const s = (cycled - 0.75) / 0.25;
|
||||||
|
r = Math.floor(255 - s * 215); // 255 -> 40
|
||||||
|
g = Math.floor(245 - s * 225); // 245 -> 20
|
||||||
|
b = Math.floor(200 - s * 195); // 200 -> 5
|
||||||
|
}
|
||||||
|
|
||||||
|
return { r, g, b };
|
||||||
|
}
|
||||||
|
|
||||||
|
function mandelbrot(cx, cy, maxIter) {
|
||||||
|
let x = 0, y = 0;
|
||||||
|
let iteration = 0;
|
||||||
|
|
||||||
|
while (x * x + y * y <= 4 && iteration < maxIter) {
|
||||||
|
const xTemp = x * x - y * y + cx;
|
||||||
|
y = 2 * x * y + cy;
|
||||||
|
x = xTemp;
|
||||||
|
iteration++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Smooth coloring
|
||||||
|
if (iteration < maxIter) {
|
||||||
|
const logZn = Math.log(x * x + y * y) / 2;
|
||||||
|
const nu = Math.log(logZn / Math.log(2)) / Math.log(2);
|
||||||
|
iteration = iteration + 1 - nu;
|
||||||
|
}
|
||||||
|
|
||||||
|
return iteration;
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
const imageData = ctx.createImageData(width, height);
|
||||||
|
const data = imageData.data;
|
||||||
|
|
||||||
|
// Calculate view bounds
|
||||||
|
const aspectRatio = width / height;
|
||||||
|
const viewWidth = 4 / zoom;
|
||||||
|
const viewHeight = viewWidth / aspectRatio;
|
||||||
|
|
||||||
|
const minX = centerX - viewWidth / 2;
|
||||||
|
const maxX = centerX + viewWidth / 2;
|
||||||
|
const minY = centerY - viewHeight / 2;
|
||||||
|
const maxY = centerY + viewHeight / 2;
|
||||||
|
|
||||||
|
// Adaptive iteration count based on zoom
|
||||||
|
const iterations = Math.min(CONFIG.maxIterations + Math.log2(zoom) * 20, 500);
|
||||||
|
|
||||||
|
// Render with reduced resolution for performance, then scale
|
||||||
|
const scale = zoom > 1000 ? 2 : 1; // Lower res at high zoom for performance
|
||||||
|
|
||||||
|
for (let py = 0; py < height; py += scale) {
|
||||||
|
for (let px = 0; px < width; px += scale) {
|
||||||
|
const x0 = minX + (px / width) * (maxX - minX);
|
||||||
|
const y0 = minY + (py / height) * (maxY - minY);
|
||||||
|
|
||||||
|
const iter = mandelbrot(x0, y0, iterations);
|
||||||
|
const color = getGoldColor(iter, iterations);
|
||||||
|
|
||||||
|
// Fill pixels (handle scaling)
|
||||||
|
for (let dy = 0; dy < scale && py + dy < height; dy++) {
|
||||||
|
for (let dx = 0; dx < scale && px + dx < width; dx++) {
|
||||||
|
const idx = ((py + dy) * width + (px + dx)) * 4;
|
||||||
|
data[idx] = color.r;
|
||||||
|
data[idx + 1] = color.g;
|
||||||
|
data[idx + 2] = color.b;
|
||||||
|
data[idx + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.putImageData(imageData, 0, 0);
|
||||||
|
|
||||||
|
// Update zoom
|
||||||
|
zoom *= zoomSpeed;
|
||||||
|
|
||||||
|
// Reset if zoomed too far
|
||||||
|
if (zoom > CONFIG.zoomResetThreshold) {
|
||||||
|
resetZoom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startAnimation() {
|
||||||
|
function loop() {
|
||||||
|
render();
|
||||||
|
animationId = requestAnimationFrame(loop);
|
||||||
|
}
|
||||||
|
loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResize() {
|
||||||
|
initCanvas();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!browser) return;
|
||||||
|
|
||||||
|
// Randomly pick starting point
|
||||||
|
currentPointIndex = Math.floor(Math.random() * interestingPoints.length);
|
||||||
|
const point = interestingPoints[currentPointIndex];
|
||||||
|
centerX = point.x;
|
||||||
|
centerY = point.y;
|
||||||
|
|
||||||
|
initCanvas();
|
||||||
|
startAnimation();
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (animationId) {
|
||||||
|
cancelAnimationFrame(animationId);
|
||||||
|
}
|
||||||
|
if (browser) {
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<canvas bind:this={canvas} class="mandelbrot-canvas"></canvas>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.mandelbrot-canvas {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -93,9 +93,11 @@ function createScreensaverStore() {
|
||||||
// Check if idle time exceeds timeout
|
// Check if idle time exceeds timeout
|
||||||
if (newIdleTime >= state.timeoutMinutes * 60) {
|
if (newIdleTime >= state.timeoutMinutes * 60) {
|
||||||
// Resolve random type at activation time
|
// Resolve random type at activation time
|
||||||
const activeType = state.type === 'random'
|
let activeType = state.type;
|
||||||
? (Math.random() < 0.5 ? 'snowfall' : 'fractal_crystalline')
|
if (state.type === 'random') {
|
||||||
: state.type;
|
const types = ['snowfall', 'fractal_crystalline', 'mandelbrot_zoom'];
|
||||||
|
activeType = types[Math.floor(Math.random() * types.length)];
|
||||||
|
}
|
||||||
return { ...newState, active: true, activeType };
|
return { ...newState, active: true, activeType };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue