276 lines
11 KiB
JavaScript
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;
|
|
})(); |