"use strict"; const ProceduralBackground = (function(){ const ProceduralBackground = function(xgravity){ const self = this, image_cache = {}, active_suns = []; let is_ready = false, universe_size = 0, // Lienzos intermedios pre-renderizados base_canvas = null, planets_canvas = null; const constructor = () => { preload_images(get_all_configured_urls(), () => { build_universe( Math.floor(window.innerWidth * xgravity.values.quality / 100) || 800, Math.floor(window.innerHeight * xgravity.values.quality / 100) || 600 ); }); }; const preload_images = (urls, callback) => { let pending = urls.length; if(pending === 0){ callback(); return; }; urls.forEach(url => { if(image_cache[url]) -- pending || callback(); else{ const image = new Image(); image.onload = () => { image_cache[url] = image; -- pending || callback(); }; image.onerror = () => { console.warn("[XGravity] ProceduralBackground: No se pudo cargar", url); -- pending || callback(); }; image.src = url; }; }); }; const get_all_configured_urls = () => { const urls = []; for(const key of [ "bg_procedural_far_stars", "bg_procedural_nebulas", "bg_procedural_near_stars", "bg_procedural_suns", "bg_procedural_planets" ]){ const configuration = {...xgravity.values[key]}; if(configuration && configuration.images instanceof Array) configuration.images.forEach(url => { urls.includes(url) || urls.push(url); }); }; return urls; }; const get_cell_size = (width, height) => (width < height ? width : height) / xgravity.cells; // Dibuja un elemento radialmente desde el centro hacia afuera const draw_element_from_center = (target_context, image, center_x, center_y, maximum_radius, size_range, cell_px) => { const display_angle = Math.random() * Math.PI * 2, distance = Math.sqrt(Math.random()) * maximum_radius, // Distribución de área uniforme // Tamaño en celdas size_minimum = size_range ? size_range[0] : 10, target_px = (Math.random() * ((size_range ? size_range[1] : size_minimum) - size_minimum) + size_minimum) * cell_px, // Mantener proporción de aspecto image_ratio = image.naturalWidth / image.naturalHeight, [width, height] = image_ratio >= 1 ? [target_px, target_px / image_ratio] : [target_px / image_ratio, target_px]; target_context.save(); target_context.translate( center_x + Math.cos(display_angle) * distance, center_y + Math.sin(display_angle) * distance ); target_context.rotate(Math.random() * Math.PI * 2); target_context.drawImage(image, -width / 2, -height / 2, width, height); target_context.restore(); }; const build_universe = (width, height) => { const cell_px = get_cell_size(width, height), center_x = (universe_size = Math.floor(Math.max(width, height) * 1.5)) / 2, center_y = universe_size / 2, maximum_radius = universe_size * 0.45, base_context = (base_canvas = document.createElement("canvas")).getContext("2d"), planets_context = (planets_canvas = document.createElement("canvas")).getContext("2d"); let count; // 1. Offscreen base (Capas 1, 2 y 3) base_canvas.width = universe_size; base_canvas.height = universe_size; base_context.fillStyle = "#020408"; base_context.fillRect(0, 0, universe_size, universe_size); for(const partial_key of ["far_stars", "nebulas", "near_stars"]){ const key = "bg_procedural_" + partial_key; count = 0; while(Math.random() <= (xgravity.values[key].probablity || 0) && count ++ < ProceduralBackground.MAXIMUM_ITERATIONS){ const chosen = xgravity.values[key].images[Math.floor(Math.random() * xgravity.values[key].images.length)]; image_cache[chosen] && draw_element_from_center(base_context, image_cache[chosen], center_x, center_y, maximum_radius * .5, xgravity.values[key].size, cell_px); }; }; // 2. Offscreen Capa 5: Planetas (Bucle While) planets_canvas.width = universe_size; planets_canvas.height = universe_size; // const planets_context = (planets_canvas = document.createElement("canvas")).getContext("2d"); count = 0; while(Math.random() <= (xgravity.values.bg_procedural_planets.probablity || 0) && count ++ < ProceduralBackground.MAXIMUM_ITERATIONS){ const chosen = xgravity.values.bg_procedural_planets.images[Math.floor(Math.random() * xgravity.values.bg_procedural_planets.images.length)]; image_cache[chosen] && draw_element_from_center(planets_context, image_cache[chosen], center_x, center_y, maximum_radius, xgravity.values.bg_procedural_planets.size, cell_px); }; // 3. Capa 4: Soles vivos con carrusel de 3 sprites active_suns.length = 0; count = 0; while(Math.random() <= (xgravity.values.bg_procedural_suns.probablity || 0) && count ++ < ProceduralBackground.MAXIMUM_ITERATIONS){ const chosen = xgravity.values.bg_procedural_suns.images[Math.floor(Math.random() * xgravity.values.bg_procedural_suns.images.length)]; if(image_cache[chosen]){ const timer_range = xgravity.values.bg_procedural_suns.timer || [2, 10], size_minimum = xgravity.values.bg_procedural_suns.size ? xgravity.values.bg_procedural_suns.size[0] : 10, sun_px = (Math.random() * (( xgravity.values.bg_procedural_suns.size ? xgravity.values.bg_procedural_suns.size[1] : size_minimum) - size_minimum) + size_minimum) * cell_px, ratio = image_cache[chosen].naturalWidth / image_cache[chosen].naturalHeight, display_angle = Math.random() * Math.PI * 2, distance = Math.random() * (maximum_radius * .65); active_suns.push({ image : image_cache[chosen], offset_x : Math.cos(display_angle) * distance, offset_y : Math.sin(display_angle) * distance, w : ratio >= 1 ? sun_px : sun_px * ratio, h : ratio >= 1 ? sun_px / ratio : sun_px, // Array ordenado: [0]=fondo, [1]=medio, [2]=frente sprites : [ { angle : Math.random() * Math.PI * 2, alpha : 1.0 }, // Fondo: baja de 1 a 0 { angle : Math.random() * Math.PI * 2, alpha : 1.0 }, // Medio: base opaca constante a 1 { angle : Math.random() * Math.PI * 2, alpha : 0.0 } // Frente: sube de 0 a 1 ], duration : Math.random() * (timer_range[1] - timer_range[0]) + timer_range[0], elapsed : 0, timer_range : timer_range }); }; }; is_ready = true; }; this.resize = (width, height) => { is_ready || build_universe(width, height); }; this.render = (context, width, height, delta_time) => { if(!is_ready || !base_canvas){ context.fillStyle = "#020408"; context.fillRect(0, 0, width, height); return; }; const screen_cx = width / 2, screen_cy = height / 2, draw_x = screen_cx - (universe_size / 2), draw_y = screen_cy - (universe_size / 2); // 1. Capas 1, 2 y 3 pre-renderizadas context.drawImage(base_canvas, draw_x, draw_y); // 2. Capa 4: Renderizado de soles vivos for(let i = 0; i < active_suns.length; i ++){ const sun = active_suns[i], progress = Math.min((sun.elapsed += delta_time) / sun.duration, 1); // Sprite 0 (fondo) baja de 1 a 0 sun.sprites[0].alpha = 1.0 - progress; // Sprite 1 (medio) se mantiene siempre sólido al 100% sun.sprites[1].alpha = 1.0; // Sprite 2 (frente) sube de 0 a 1 al unísono sun.sprites[2].alpha = progress; // Al completar el tiempo if(progress >= 1){ // 1. Extraemos el sprite del fondo (que ya está a alpha 0) const old_bottom = sun.sprites.shift(); // 2. Le asignamos un nuevo ángulo aleatorio (totalmente invisible porque alpha = 0) old_bottom.angle = Math.random() * Math.PI * 2; old_bottom.alpha = 0.0; // 3. Pasa al frente de la pila (nuevo Sprite 2) sun.sprites.push(old_bottom); // 4. Reiniciamos temporizador con nueva duración aleatoria sun.elapsed = 0; sun.duration = Math.random() * (sun.timer_range[1] - sun.timer_range[0]) + sun.timer_range[0]; }; // Dibujar los 3 sprites en orden estricto de profundidad // for(let sprite = 0; sprite < sun.sprites.length; sprite++){ for(const sprite of sun.sprites) // const sp = sun.sprites[s]; if(sprite.alpha > 0){ context.save(); context.globalAlpha = sprite.alpha; context.translate(screen_cx + sun.offset_x, screen_cy + sun.offset_y); context.rotate(sprite.angle); context.drawImage(sun.image, -sun.w / 2, -sun.h / 2, sun.w, sun.h); context.restore(); }; context.globalAlpha = 1.0; }; // 3. Capa 5: Planetas pre-renderizados planets_canvas && context.drawImage(planets_canvas, draw_x, draw_y); }; constructor(); }; // Límite de seguridad por capa para evitar bucles infinitos en caso de probablity >= 1 ProceduralBackground.MAXIMUM_ITERATIONS = 50; return ProceduralBackground; })();