XGravity/Public/ecma/Backgrounds/ProceduralBackground.ecma.js

276 lines
11 KiB
JavaScript

"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;
})();