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 Snowfall from './screensavers/Snowfall.svelte';
|
||||
import FractalCrystalline from './screensavers/FractalCrystalline.svelte';
|
||||
import MandelbrotZoom from './screensavers/MandelbrotZoom.svelte';
|
||||
|
||||
// Map type to component
|
||||
const screensaverComponents = {
|
||||
snowfall: Snowfall,
|
||||
fractal_crystalline: FractalCrystalline
|
||||
fractal_crystalline: FractalCrystalline,
|
||||
mandelbrot_zoom: MandelbrotZoom
|
||||
};
|
||||
|
||||
// Dismiss on any interaction
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
maxBranches: 800, // Max simultaneous branch tips
|
||||
branchChance: 0.03, // Chance to spawn new branch per frame
|
||||
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)
|
||||
shatterDuration: 2500, // Milliseconds for shatter effect
|
||||
lineWidth: 2
|
||||
|
|
@ -112,11 +112,14 @@
|
|||
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++) {
|
||||
const x = Math.random() * width;
|
||||
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
|
||||
const branchCount = 3 + Math.floor(Math.random() * 4);
|
||||
for (let j = 0; j < branchCount; j++) {
|
||||
|
|
@ -125,7 +128,7 @@
|
|||
x,
|
||||
y,
|
||||
angle,
|
||||
hue: Math.random() * 360,
|
||||
hue: (seedHue + j * 25 + Math.random() * 20) % 360, // Each branch gets varied hue
|
||||
age: 0,
|
||||
generation: 0
|
||||
});
|
||||
|
|
@ -178,9 +181,11 @@
|
|||
hue: (branch.hue + branch.age * 0.5) % 360
|
||||
});
|
||||
|
||||
// Draw the branch segment
|
||||
const h = (branch.hue + branch.age * 0.5) % 360;
|
||||
ctx.strokeStyle = `hsla(${h}, 80%, 60%, 0.9)`;
|
||||
// Draw the branch segment with varied colors
|
||||
const h = (branch.hue + branch.age * 1.5) % 360; // Faster hue shift
|
||||
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.beginPath();
|
||||
ctx.moveTo(branch.x, branch.y);
|
||||
|
|
@ -192,14 +197,14 @@
|
|||
branch.y = newY;
|
||||
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) {
|
||||
const forkAngle = branch.angle + (Math.random() > 0.5 ? 1 : -1) * (0.3 + Math.random() * 0.7);
|
||||
newBranches.push({
|
||||
x: newX,
|
||||
y: newY,
|
||||
angle: forkAngle,
|
||||
hue: (branch.hue + 20 + Math.random() * 40) % 360,
|
||||
hue: (branch.hue + 30 + Math.random() * 90) % 360, // More color variation
|
||||
age: 0,
|
||||
generation: branch.generation + 1
|
||||
});
|
||||
|
|
@ -228,6 +233,7 @@
|
|||
|
||||
// Only spawn if we found an unoccupied spot
|
||||
if (!isOccupied(x, y)) {
|
||||
const seedHue = Math.random() * 360; // Random base hue for new seed
|
||||
const branchCount = 2 + Math.floor(Math.random() * 3);
|
||||
for (let j = 0; j < branchCount; j++) {
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
|
|
@ -235,7 +241,7 @@
|
|||
x,
|
||||
y,
|
||||
angle,
|
||||
hue: Math.random() * 360,
|
||||
hue: (seedHue + j * 40 + Math.random() * 30) % 360, // Varied colors
|
||||
age: 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
|
||||
if (newIdleTime >= state.timeoutMinutes * 60) {
|
||||
// Resolve random type at activation time
|
||||
const activeType = state.type === 'random'
|
||||
? (Math.random() < 0.5 ? 'snowfall' : 'fractal_crystalline')
|
||||
: state.type;
|
||||
let activeType = state.type;
|
||||
if (state.type === 'random') {
|
||||
const types = ['snowfall', 'fractal_crystalline', 'mandelbrot_zoom'];
|
||||
activeType = types[Math.floor(Math.random() * types.length)];
|
||||
}
|
||||
return { ...newState, active: true, activeType };
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue