mejoras de estructura

This commit is contained in:
2026-09-14 17:08:10 -03:00
parent 62467348f4
commit 7c13f697db
21 changed files with 1650 additions and 1591 deletions
View File
+582
View File
@@ -0,0 +1,582 @@
"use strict";
// ===== CONFIGURACIÓN DEL EFECTO =====
const config = {
TEXTURE_DOWNSAMPLE: 1,
DENSITY_DISSIPATION: 0.975,
VELOCITY_DISSIPATION: 0.98,
PRESSURE_DISSIPATION: 0.8,
PRESSURE_ITERATIONS: 20,
CURL: 30,
SPLAT_RADIUS: 0.003
};
// ===== INICIALIZAR CANVAS =====
let canvas = document.getElementById("fluidCanvas");
console.log("Canvas encontrado:", canvas);
if (!canvas) {
console.error("ERROR: No se encontró el canvas con id 'fluidCanvas'");
}
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// ===== CONTEXTO WEBGL =====
let pointers = [];
let splatStack = [];
function getWebGLContext(canvas) {
let params = {
alpha: false,
depth: false,
stencil: false,
antialias: false
};
let gl = canvas.getContext("webgl2", params);
let isWebGL2 = !!gl;
if (!isWebGL2)
gl =
canvas.getContext("webgl", params) ||
canvas.getContext("experimental-webgl", params);
let halfFloat = gl.getExtension("OES_texture_half_float");
let support_linear_float = gl.getExtension("OES_texture_half_float_linear");
if (isWebGL2) {
gl.getExtension("EXT_color_buffer_float");
support_linear_float = gl.getExtension("OES_texture_float_linear");
}
gl.clearColor(0.0, 0.0, 0.0, 1.0);
let internalFormat = isWebGL2 ? gl.RGBA16F : gl.RGBA;
let internalFormatRG = isWebGL2 ? gl.RG16F : gl.RGBA;
let formatRG = isWebGL2 ? gl.RG : gl.RGBA;
let texType = isWebGL2 ? gl.HALF_FLOAT : halfFloat.HALF_FLOAT_OES;
return {
gl: gl,
ext: {
internalFormat: internalFormat,
internalFormatRG: internalFormatRG,
formatRG: formatRG,
texType: texType
},
support_linear_float: support_linear_float
};
}
let _getWebGLContext = getWebGLContext(canvas);
let gl = _getWebGLContext.gl;
let ext = _getWebGLContext.ext;
let support_linear_float = _getWebGLContext.support_linear_float;
// ===== POINTER =====
function pointerPrototype() {
this.id = -1;
this.x = 0;
this.y = 0;
this.dx = 0;
this.dy = 0;
this.down = false;
this.moved = false;
this.color = [30, 0, 300];
}
pointers.push(new pointerPrototype());
// ===== SHADERS =====
let GLProgram = (function () {
function GLProgram(vertexShader, fragmentShader) {
if (!(this instanceof GLProgram))
throw new TypeError("Cannot call a class as a function");
this.uniforms = {};
this.program = gl.createProgram();
gl.attachShader(this.program, vertexShader);
gl.attachShader(this.program, fragmentShader);
gl.linkProgram(this.program);
if (!gl.getProgramParameter(this.program, gl.LINK_STATUS))
throw gl.getProgramInfoLog(this.program);
let uniformCount = gl.getProgramParameter(this.program, gl.ACTIVE_UNIFORMS);
for (let i = 0; i < uniformCount; i++) {
let uniformName = gl.getActiveUniform(this.program, i).name;
this.uniforms[uniformName] = gl.getUniformLocation(
this.program,
uniformName
);
}
}
GLProgram.prototype.bind = function bind() {
gl.useProgram(this.program);
};
return GLProgram;
})();
function compileShader(type, source) {
let shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS))
throw gl.getShaderInfoLog(shader);
return shader;
}
// ===== VERTEX SHADER =====
let baseVertexShader = compileShader(
gl.VERTEX_SHADER,
"precision highp float; precision mediump sampler2D; attribute vec2 aPosition; varying vec2 vUv; varying vec2 vL; varying vec2 vR; varying vec2 vT; varying vec2 vB; uniform vec2 texelSize; void main () { vUv = aPosition * 0.5 + 0.5; vL = vUv - vec2(texelSize.x, 0.0); vR = vUv + vec2(texelSize.x, 0.0); vT = vUv + vec2(0.0, texelSize.y); vB = vUv - vec2(0.0, texelSize.y); gl_Position = vec4(aPosition, 0.0, 1.0); }"
);
// ===== FRAGMENT SHADERS =====
let clearShader = compileShader(
gl.FRAGMENT_SHADER,
"precision highp float; precision mediump sampler2D; varying vec2 vUv; uniform sampler2D uTexture; uniform float value; void main () { gl_FragColor = value * texture2D(uTexture, vUv); }"
);
let displayShader = compileShader(
gl.FRAGMENT_SHADER,
"precision highp float; precision mediump sampler2D; varying vec2 vUv; uniform sampler2D uTexture; void main () { gl_FragColor = texture2D(uTexture, vUv); }"
);
let splatShader = compileShader(
gl.FRAGMENT_SHADER,
"precision highp float; precision mediump sampler2D; varying vec2 vUv; uniform sampler2D uTarget; uniform float aspectRatio; uniform vec3 color; uniform vec2 point; uniform float radius; void main () { vec2 p = vUv - point.xy; p.x *= aspectRatio; vec3 splat = exp(-dot(p, p) / radius) * color; vec3 base = texture2D(uTarget, vUv).xyz; gl_FragColor = vec4(base + splat, 1.0); }"
);
let advectionManualFilteringShader = compileShader(
gl.FRAGMENT_SHADER,
"precision highp float; precision mediump sampler2D; varying vec2 vUv; uniform sampler2D uVelocity; uniform sampler2D uSource; uniform vec2 texelSize; uniform float dt; uniform float dissipation; vec4 bilerp (in sampler2D sam, in vec2 p) { vec4 st; st.xy = floor(p - 0.5) + 0.5; st.zw = st.xy + 1.0; vec4 uv = st * texelSize.xyxy; vec4 a = texture2D(sam, uv.xy); vec4 b = texture2D(sam, uv.zy); vec4 c = texture2D(sam, uv.xw); vec4 d = texture2D(sam, uv.zw); vec2 f = p - st.xy; return mix(mix(a, b, f.x), mix(c, d, f.x), f.y); } void main () { vec2 coord = gl_FragCoord.xy - dt * texture2D(uVelocity, vUv).xy; gl_FragColor = dissipation * bilerp(uSource, coord); gl_FragColor.a = 1.0; }"
);
let advectionShader = compileShader(
gl.FRAGMENT_SHADER,
"precision highp float; precision mediump sampler2D; varying vec2 vUv; uniform sampler2D uVelocity; uniform sampler2D uSource; uniform vec2 texelSize; uniform float dt; uniform float dissipation; void main () { vec2 coord = vUv - dt * texture2D(uVelocity, vUv).xy * texelSize; gl_FragColor = dissipation * texture2D(uSource, coord); }"
);
let divergenceShader = compileShader(
gl.FRAGMENT_SHADER,
"precision highp float; precision mediump sampler2D; varying vec2 vUv; varying vec2 vL; varying vec2 vR; varying vec2 vT; varying vec2 vB; uniform sampler2D uVelocity; vec2 sampleVelocity (in vec2 uv) { vec2 multiplier = vec2(1.0, 1.0); if (uv.x < 0.0) { uv.x = 0.0; multiplier.x = -1.0; } if (uv.x > 1.0) { uv.x = 1.0; multiplier.x = -1.0; } if (uv.y < 0.0) { uv.y = 0.0; multiplier.y = -1.0; } if (uv.y > 1.0) { uv.y = 1.0; multiplier.y = -1.0; } return multiplier * texture2D(uVelocity, uv).xy; } void main () { float L = sampleVelocity(vL).x; float R = sampleVelocity(vR).x; float T = sampleVelocity(vT).y; float B = sampleVelocity(vB).y; float div = 0.5 * (R - L + T - B); gl_FragColor = vec4(div, 0.0, 0.0, 1.0); }"
);
let curlShader = compileShader(
gl.FRAGMENT_SHADER,
"precision highp float; precision mediump sampler2D; varying vec2 vUv; varying vec2 vL; varying vec2 vR; varying vec2 vT; varying vec2 vB; uniform sampler2D uVelocity; void main () { float L = texture2D(uVelocity, vL).y; float R = texture2D(uVelocity, vR).y; float T = texture2D(uVelocity, vT).x; float B = texture2D(uVelocity, vB).x; float vorticity = R - L - T + B; gl_FragColor = vec4(vorticity, 0.0, 0.0, 1.0); }"
);
let vorticityShader = compileShader(
gl.FRAGMENT_SHADER,
"precision highp float; precision mediump sampler2D; varying vec2 vUv; varying vec2 vL; varying vec2 vR; varying vec2 vT; varying vec2 vB; uniform sampler2D uVelocity; uniform sampler2D uCurl; uniform float curl; uniform float dt; void main () { float L = texture2D(uCurl, vL).y; float R = texture2D(uCurl, vR).y; float T = texture2D(uCurl, vT).x; float B = texture2D(uCurl, vB).x; float C = texture2D(uCurl, vUv).x; vec2 force = vec2(abs(T) - abs(B), abs(R) - abs(L)); force *= 1.0 / length(force + 0.00001) * curl * C; vec2 vel = texture2D(uVelocity, vUv).xy; gl_FragColor = vec4(vel + force * dt, 0.0, 1.0); }"
);
let pressureShader = compileShader(
gl.FRAGMENT_SHADER,
"precision highp float; precision mediump sampler2D; varying vec2 vUv; varying vec2 vL; varying vec2 vR; varying vec2 vT; varying vec2 vB; uniform sampler2D uPressure; uniform sampler2D uDivergence; vec2 boundary (in vec2 uv) { uv = min(max(uv, 0.0), 1.0); return uv; } void main () { float L = texture2D(uPressure, boundary(vL)).x; float R = texture2D(uPressure, boundary(vR)).x; float T = texture2D(uPressure, boundary(vT)).x; float B = texture2D(uPressure, boundary(vB)).x; float C = texture2D(uPressure, vUv).x; float divergence = texture2D(uDivergence, vUv).x; float pressure = (L + R + B + T - divergence) * 0.25; gl_FragColor = vec4(pressure, 0.0, 0.0, 1.0); }"
);
let gradientSubtractShader = compileShader(
gl.FRAGMENT_SHADER,
"precision highp float; precision mediump sampler2D; varying vec2 vUv; varying vec2 vL; varying vec2 vR; varying vec2 vT; varying vec2 vB; uniform sampler2D uPressure; uniform sampler2D uVelocity; vec2 boundary (in vec2 uv) { uv = min(max(uv, 0.0), 1.0); return uv; } void main () { float L = texture2D(uPressure, boundary(vL)).x; float R = texture2D(uPressure, boundary(vR)).x; float T = texture2D(uPressure, boundary(vT)).x; float B = texture2D(uPressure, boundary(vB)).x; vec2 velocity = texture2D(uVelocity, vUv).xy; velocity.xy -= vec2(R - L, T - B); gl_FragColor = vec4(velocity, 0.0, 1.0); }"
);
// ===== FRAMEBUFFERS =====
let textureWidth, textureHeight;
let density, velocity, divergence, curl, pressure;
function initFramebuffers() {
textureWidth = gl.drawingBufferWidth >> config.TEXTURE_DOWNSAMPLE;
textureHeight = gl.drawingBufferHeight >> config.TEXTURE_DOWNSAMPLE;
let iFormat = ext.internalFormat;
let iFormatRG = ext.internalFormatRG;
let formatRG = ext.formatRG;
let texType = ext.texType;
density = createDoubleFBO(
0,
textureWidth,
textureHeight,
iFormat,
gl.RGBA,
texType,
support_linear_float ? gl.LINEAR : gl.NEAREST
);
velocity = createDoubleFBO(
2,
textureWidth,
textureHeight,
iFormatRG,
formatRG,
texType,
support_linear_float ? gl.LINEAR : gl.NEAREST
);
divergence = createFBO(
4,
textureWidth,
textureHeight,
iFormatRG,
formatRG,
texType,
gl.NEAREST
);
curl = createFBO(
5,
textureWidth,
textureHeight,
iFormatRG,
formatRG,
texType,
gl.NEAREST
);
pressure = createDoubleFBO(
6,
textureWidth,
textureHeight,
iFormatRG,
formatRG,
texType,
gl.NEAREST
);
}
function createFBO(texId, w, h, internalFormat, format, type, param) {
gl.activeTexture(gl.TEXTURE0 + texId);
let texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, param);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, param);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, w, h, 0, format, type, null);
let fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferTexture2D(
gl.FRAMEBUFFER,
gl.COLOR_ATTACHMENT0,
gl.TEXTURE_2D,
texture,
0
);
gl.viewport(0, 0, w, h);
gl.clear(gl.COLOR_BUFFER_BIT);
return [texture, fbo, texId];
}
function createDoubleFBO(texId, w, h, internalFormat, format, type, param) {
let fbo1 = createFBO(texId, w, h, internalFormat, format, type, param);
let fbo2 = createFBO(texId + 1, w, h, internalFormat, format, type, param);
return {
get first() {
return fbo1;
},
get second() {
return fbo2;
},
swap: function swap() {
let temp = fbo1;
fbo1 = fbo2;
fbo2 = temp;
}
};
}
initFramebuffers();
// ===== PROGRAMS =====
let clearProgram = new GLProgram(baseVertexShader, clearShader);
let displayProgram = new GLProgram(baseVertexShader, displayShader);
let splatProgram = new GLProgram(baseVertexShader, splatShader);
let advectionProgram = new GLProgram(
baseVertexShader,
support_linear_float ? advectionShader : advectionManualFilteringShader
);
let divergenceProgram = new GLProgram(baseVertexShader, divergenceShader);
let curlProgram = new GLProgram(baseVertexShader, curlShader);
let vorticityProgram = new GLProgram(baseVertexShader, vorticityShader);
let pressureProgram = new GLProgram(baseVertexShader, pressureShader);
let gradienSubtractProgram = new GLProgram(
baseVertexShader,
gradientSubtractShader
);
// ===== BLIT =====
let blit = (function () {
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([-1, -1, -1, 1, 1, 1, 1, -1]),
gl.STATIC_DRAW
);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(
gl.ELEMENT_ARRAY_BUFFER,
new Uint16Array([0, 1, 2, 0, 2, 3]),
gl.STATIC_DRAW
);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(0);
return function (destination) {
gl.bindFramebuffer(gl.FRAMEBUFFER, destination);
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
};
})();
// ===== SPLAT =====
function splat(x, y, dx, dy, color) {
splatProgram.bind();
gl.uniform1i(splatProgram.uniforms.uTarget, velocity.first[2]);
gl.uniform1f(splatProgram.uniforms.aspectRatio, canvas.width / canvas.height);
gl.uniform2f(
splatProgram.uniforms.point,
x / canvas.width,
1.0 - y / canvas.height
);
gl.uniform3f(splatProgram.uniforms.color, dx, -dy, 1.0);
gl.uniform1f(splatProgram.uniforms.radius, config.SPLAT_RADIUS);
blit(velocity.second[1]);
velocity.swap();
gl.uniform1i(splatProgram.uniforms.uTarget, density.first[2]);
gl.uniform3f(
splatProgram.uniforms.color,
color[0] * 0.3,
color[1] * 0.3,
color[2] * 0.3
);
blit(density.second[1]);
density.swap();
}
// ===== RESIZE =====
function resizeCanvas() {
(canvas.width !== canvas.clientWidth ||
canvas.height !== canvas.clientHeight) &&
((canvas.width = canvas.clientWidth),
(canvas.height = canvas.clientHeight),
initFramebuffers());
}
// ===== COLORES DE LA MARCA CON TRANSICIÓN =====
const brandColors = [
//[0.874, 0.741, 0.725], // Rosa (#DFBDB9)
[0.729, 0.071, 0.302], // Bordo (#BA124D)
[0.000, 0.247, 0.322] // Teal (#003F52)
];
let currentColor = brandColors[0];
let targetColor = brandColors[1];
let transitionProgress = 0;
const transitionSpeed = 0.008;
function lerpColor(colorA, colorB, t) {
return [
colorA[0] + (colorB[0] - colorA[0]) * t,
colorA[1] + (colorB[1] - colorA[1]) * t,
colorA[2] + (colorB[2] - colorA[2]) * t
];
}
let colorArr = currentColor;
let count = 0;
// ===== DETECTAR ELEMENTOS DE TEXTO =====
function isMouseOverTextElement(target) {
// Listado de selectores que necesitan "modo apaciguado"
const textSelectors = [
'p', '.hero-content p','h1', 'h2', 'h3', 'h4', 'h5', '.hero-content h1',
'.hero-content h2', '.eyebrow', 'a.sheet-link', '.sheet-link span',
'.menu-btn', '.sheet-link-desc', '.sheet-link-main'
];
// Verificar si el target o sus ancestros coinciden con algún selector
for (const selector of textSelectors) {
if (target.closest(selector)) {
return true;
}
}
return false;
}
// ===== EVENTOS CON COLORES DE MARCA =====
document.addEventListener("mousemove", function (e) {
// Detectar si estamos sobre un elemento de texto
const overText = isMouseOverTextElement(e.target);
count++;
if (count > 1) {
transitionProgress += transitionSpeed;
if (transitionProgress >= 1) {
transitionProgress = 0;
let currentIndex = brandColors.indexOf(currentColor);
let nextIndex = (currentIndex + 1) % brandColors.length;
currentColor = targetColor;
targetColor = brandColors[nextIndex];
}
colorArr = lerpColor(currentColor, targetColor, transitionProgress);
count = 0;
}
pointers[0].down = true;
pointers[0].color = colorArr;
pointers[0].moved = pointers[0].down;
// Aplicar multiplicador diferente según si estamos sobre texto o no
const multiplier = overText ? 8 : 10.0; // Reducido a 4x sobre texto (vs 10x)
pointers[0].dx = (e.clientX - canvas.offsetLeft - pointers[0].x) * multiplier;
pointers[0].dy = (e.clientY - canvas.offsetTop - pointers[0].y) * multiplier;
pointers[0].x = e.clientX - canvas.offsetLeft;
pointers[0].y = e.clientY - canvas.offsetTop;
});
canvas.addEventListener(
"touchmove",
function (e) {
e.preventDefault();
let touches = e.targetTouches;
count++;
if (count > 20) {
transitionProgress += transitionSpeed;
if (transitionProgress >= 1) {
transitionProgress = 0;
let currentIndex = brandColors.indexOf(currentColor);
let nextIndex = (currentIndex + 1) % brandColors.length;
currentColor = targetColor;
targetColor = brandColors[nextIndex];
}
colorArr = lerpColor(currentColor, targetColor, transitionProgress);
count = 0;
}
for (let i = 0, len = touches.length; i < len; i++) {
if (i >= pointers.length) pointers.push(new pointerPrototype());
pointers[i].id = touches[i].identifier;
pointers[i].down = true;
pointers[i].x = touches[i].pageX;
pointers[i].y = touches[i].pageY;
pointers[i].color = colorArr;
let pointer = pointers[i];
pointer.moved = pointer.down;
pointer.dx = (touches[i].pageX - pointer.x) * 10.0;
pointer.dy = (touches[i].pageY - pointer.y) * 10.0;
pointer.x = touches[i].pageX;
pointer.y = touches[i].pageY;
}
},
false
);
// ===== LOOP PRINCIPAL =====
let lastTime = Date.now();
function update() {
resizeCanvas();
let dt = Math.min((Date.now() - lastTime) / 1000, 0.016);
lastTime = Date.now();
gl.viewport(0, 0, textureWidth, textureHeight);
advectionProgram.bind();
gl.uniform2f(
advectionProgram.uniforms.texelSize,
1.0 / textureWidth,
1.0 / textureHeight
);
gl.uniform1i(advectionProgram.uniforms.uVelocity, velocity.first[2]);
gl.uniform1i(advectionProgram.uniforms.uSource, velocity.first[2]);
gl.uniform1f(advectionProgram.uniforms.dt, dt);
gl.uniform1f(
advectionProgram.uniforms.dissipation,
config.VELOCITY_DISSIPATION
);
blit(velocity.second[1]);
velocity.swap();
gl.uniform1i(advectionProgram.uniforms.uVelocity, velocity.first[2]);
gl.uniform1i(advectionProgram.uniforms.uSource, density.first[2]);
gl.uniform1f(
advectionProgram.uniforms.dissipation,
config.DENSITY_DISSIPATION
);
blit(density.second[1]);
density.swap();
for (let i = 0, len = pointers.length; i < len; i++) {
let pointer = pointers[i];
if (pointer.moved) {
splat(pointer.x, pointer.y, pointer.dx, pointer.dy, pointer.color);
pointer.moved = false;
}
}
curlProgram.bind();
gl.uniform2f(
curlProgram.uniforms.texelSize,
1.0 / textureWidth,
1.0 / textureHeight
);
gl.uniform1i(curlProgram.uniforms.uVelocity, velocity.first[2]);
blit(curl[1]);
vorticityProgram.bind();
gl.uniform2f(
vorticityProgram.uniforms.texelSize,
1.0 / textureWidth,
1.0 / textureHeight
);
gl.uniform1i(vorticityProgram.uniforms.uVelocity, velocity.first[2]);
gl.uniform1i(vorticityProgram.uniforms.uCurl, curl[2]);
gl.uniform1f(vorticityProgram.uniforms.curl, config.CURL);
gl.uniform1f(vorticityProgram.uniforms.dt, dt);
blit(velocity.second[1]);
velocity.swap();
divergenceProgram.bind();
gl.uniform2f(
divergenceProgram.uniforms.texelSize,
1.0 / textureWidth,
1.0 / textureHeight
);
gl.uniform1i(divergenceProgram.uniforms.uVelocity, velocity.first[2]);
blit(divergence[1]);
clearProgram.bind();
let pressureTexId = pressure.first[2];
gl.activeTexture(gl.TEXTURE0 + pressureTexId);
gl.bindTexture(gl.TEXTURE_2D, pressure.first[0]);
gl.uniform1i(clearProgram.uniforms.uTexture, pressureTexId);
gl.uniform1f(clearProgram.uniforms.value, config.PRESSURE_DISSIPATION);
blit(pressure.second[1]);
pressure.swap();
pressureProgram.bind();
gl.uniform2f(
pressureProgram.uniforms.texelSize,
1.0 / textureWidth,
1.0 / textureHeight
);
gl.uniform1i(pressureProgram.uniforms.uDivergence, divergence[2]);
pressureTexId = pressure.first[2];
gl.activeTexture(gl.TEXTURE0 + pressureTexId);
for (let _i = 0; _i < config.PRESSURE_ITERATIONS; _i++) {
gl.bindTexture(gl.TEXTURE_2D, pressure.first[0]);
gl.uniform1i(pressureProgram.uniforms.uPressure, pressureTexId);
blit(pressure.second[1]);
pressure.swap();
}
gradienSubtractProgram.bind();
gl.uniform2f(
gradienSubtractProgram.uniforms.texelSize,
1.0 / textureWidth,
1.0 / textureHeight
);
gl.uniform1i(gradienSubtractProgram.uniforms.uPressure, pressure.first[2]);
gl.uniform1i(gradienSubtractProgram.uniforms.uVelocity, velocity.first[2]);
blit(velocity.second[1]);
velocity.swap();
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
displayProgram.bind();
gl.uniform1i(displayProgram.uniforms.uTexture, density.first[2]);
blit(null);
requestAnimationFrame(update);
}
update();
// ===== REDIMENSIONAR AL CAMBIAR TAMAÑO DE VENTANA =====
window.addEventListener("resize", function() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
initFramebuffers();
});
+110 -138
View File
@@ -1,150 +1,122 @@
// ===== MENU =====
function toggleMenu() {
document.getElementById('sheet').classList.toggle('open');
document.getElementById('overlay').classList.toggle('open');
}
// ===== TABS =====
function showTab(i, el) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
el.classList.add('active');
document.querySelector('.panel[data-panel="' + i + '"]').classList.add('active');
}
// ===== ACCORDION =====
function toggleAcc(head) {
const item = head.parentElement;
document.querySelectorAll('.acc-item').forEach(i => { if (i !== item) i.classList.remove('open'); });
item.classList.toggle('open');
}
// ===== FAQ =====
function toggleFaq(head) {
const item = head.parentElement;
const isOpen = item.classList.contains('open');
document.querySelectorAll('.faq-item').forEach(i => {
i.classList.remove('open');
const t = i.querySelector('.faq-toggle');
if (t) t.textContent = '+';
});
if (!isOpen) {
item.classList.add('open');
const toggle = item.querySelector('.faq-toggle');
if (toggle) toggle.textContent = '−';
/* ============================================
CARGA DE INCLUDES (menu y footer)
============================================ */
async function loadInclude(selector, url) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const html = await response.text();
const container = document.querySelector(selector);
if (container) container.innerHTML = html;
} catch (error) {
console.error(`Error cargando ${url}:`, error);
}
}
// ===== COTIZADOR =====
const answers = [null, null, null];
let current = 0;
/* ============================================
SWITCH DE TEMA (claro por defecto)
============================================ */
function initTheme() {
const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', savedTheme);
}
document.querySelectorAll('.opt').forEach(opt => {
opt.addEventListener('click', function() {
const group = this.parentElement;
const q = parseInt(group.dataset.q);
group.querySelectorAll('.opt').forEach(o => o.classList.remove('selected'));
this.classList.add('selected');
answers[q] = parseInt(this.dataset.val);
document.getElementById('next' + q).disabled = false;
const step = this.closest('.step');
const qNum = step.querySelector('.q-num');
if (qNum) qNum.classList.add('active');
});
function toggleTheme() {
const current = document.documentElement.getAttribute('data-theme') || 'light';
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
}
// Ejecutar al cargar la página
document.addEventListener('DOMContentLoaded', () => {
initTheme();
const themeBtn = document.querySelector('.theme-toggle');
if (themeBtn) {
themeBtn.addEventListener('click', toggleTheme);
}
});
/* ============================================
CALCULAR ANCHO DEL SCROLLBAR
============================================ */
function getScrollbarWidth() {
return window.innerWidth - document.documentElement.clientWidth;
}
function updateProgress() {
for (let i = 0; i < 3; i++) {
document.getElementById('p' + (i + 1)).style.width = i <= current ? '100%' : '0%';
function applyScrollbarCompensation() {
const scrollbarWidth = getScrollbarWidth();
document.documentElement.style.setProperty('--scrollbar-width', `${scrollbarWidth}px`);
}
/* ============================================
MENÚ FULLSCREEN
============================================ */
function openMenu() {
const menu = document.getElementById('fullscreen-menu');
const overlay = document.getElementById('menu-overlay');
const menuToggle = document.querySelector('.menu-toggle');
if (menu && overlay) {
applyScrollbarCompensation();
menu.classList.add('is-open');
overlay.classList.add('is-open');
document.body.classList.add('menu-open');
menuToggle.setAttribute('aria-expanded', 'true');
}
}
function next() {
document.querySelector('.step[data-step="' + current + '"]').classList.remove('active');
current++;
document.querySelector('.step[data-step="' + current + '"]').classList.add('active');
updateProgress();
}
function prev() {
document.querySelector('.step[data-step="' + current + '"]').classList.remove('active');
current--;
document.querySelector('.step[data-step="' + current + '"]').classList.add('active');
updateProgress();
}
const plans = [
{ name: 'Esencial', price: '$250.000 ARS', desc: 'Ideal para arrancar con presencia digital profesional y consistente, sin sobre-invertir todavía.' },
{ name: 'Impulso', price: '$420.000 ARS', desc: 'Pensado para marcas en crecimiento que necesitan más alcance, leads y una primera campaña de publicidad.' },
{ name: 'Premium', price: '$650.000 ARS + pauta', desc: 'Nuestro plan más recomendado: gestión integral de contenido, publicidad y estrategia para consolidar y expandir.' },
{ name: 'Elite', price: '$1.000.000 ARS', desc: 'Máximo posicionamiento y gestión de marca completa, para negocios listos para escalar en serio.' }
];
function showResult() {
document.querySelector('.step[data-step="2"]').classList.remove('active');
const avg = Math.round((answers[0] + answers[1] + answers[2]) / 3);
const idx = Math.min(Math.max(avg - 1, 0), 3);
document.getElementById('rp-name').textContent = plans[idx].name;
document.getElementById('rp-price').innerHTML = plans[idx].price + ' <small>/ mes</small>';
document.getElementById('rp-desc').textContent = plans[idx].desc;
document.getElementById('result').classList.add('active');
}
function restart() {
answers[0] = answers[1] = answers[2] = null;
current = 0;
document.querySelectorAll('.opt').forEach(o => o.classList.remove('selected'));
document.querySelectorAll('[id^="next"]').forEach(b => b.disabled = true);
document.querySelectorAll('.q-num').forEach(q => q.classList.remove('active'));
document.getElementById('result').classList.remove('active');
document.querySelectorAll('.step').forEach(s => s.classList.remove('active'));
document.querySelector('.step[data-step="0"]').classList.add('active');
updateProgress();
}
// ===== FILTRO DE SERVICIOS EN CONTACTO =====
function selectFilter(event) {
const el = event.currentTarget;
const container = el.parentElement;
container.querySelectorAll('.filter-tag').forEach(t => t.classList.remove('active'));
el.classList.add('active');
const value = el.dataset.value;
document.getElementById('servicioSeleccionado').value = value;
const otroInput = document.getElementById('servicioOtro');
if (value === 'otro') {
otroInput.style.display = 'block';
otroInput.required = true;
} else {
otroInput.style.display = 'none';
otroInput.required = false;
otroInput.value = '';
function closeMenu() {
const menu = document.getElementById('fullscreen-menu');
const overlay = document.getElementById('menu-overlay');
const menuToggle = document.querySelector('.menu-toggle');
if (menu && overlay) {
menu.classList.remove('is-open');
overlay.classList.remove('is-open');
document.body.classList.remove('menu-open');
menuToggle.setAttribute('aria-expanded', 'false');
}
}
// ===== FORMULARIO =====
function handleSubmit(e) {
e.preventDefault();
const form = document.getElementById('contactForm');
const data = new FormData(form);
const nombre = data.get('nombre');
const email = data.get('email');
const mensaje = data.get('mensaje');
if (!nombre || !email || !mensaje) {
alert('Por favor, completá todos los campos obligatorios.');
return false;
}
const btn = form.querySelector('button[type="submit"]');
const originalText = btn.textContent;
btn.textContent = 'Enviando...';
btn.disabled = true;
setTimeout(() => {
alert('¡Mensaje enviado con éxito! Te responderemos a la brevedad.');
form.reset();
btn.textContent = originalText;
btn.disabled = false;
document.querySelectorAll('.filter-tag').forEach(t => t.classList.remove('active'));
document.querySelector('.filter-tag[data-value="todos"]').classList.add('active');
document.getElementById('servicioOtro').style.display = 'none';
}, 1500);
return false;
}
/* ============================================
INICIALIZACIÓN
============================================ */
document.addEventListener('DOMContentLoaded', async () => {
// Cargar menú y footer
await Promise.all([
loadInclude('#menu-container', 'includes/menu.html'),
loadInclude('#footer-container', 'includes/footer.html')
]);
// Inicializar tema
initTheme();
// Eventos del tema
const themeBtn = document.querySelector('.theme-toggle');
if (themeBtn) themeBtn.addEventListener('click', toggleTheme);
// Eventos del menú
const menuToggle = document.querySelector('.menu-toggle');
const menuClose = document.querySelector('.menu-close');
const menuOverlay = document.getElementById('menu-overlay');
const menuLinks = document.querySelectorAll('.fullscreen-menu a');
if (menuToggle) menuToggle.addEventListener('click', openMenu);
if (menuClose) menuClose.addEventListener('click', closeMenu);
if (menuOverlay) menuOverlay.addEventListener('click', closeMenu);
// Cerrar menú al hacer clic en un enlace
menuLinks.forEach(link => {
link.addEventListener('click', closeMenu);
});
// Cerrar menú con tecla Escape
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeMenu();
});
// Año dinámico en el footer
const yearEl = document.getElementById('current-year');
if (yearEl) yearEl.textContent = new Date().getFullYear();
});