update texts; remove redundant text; close all collapsibles by default; comment out carousel for now
This commit is contained in:
@@ -15,7 +15,7 @@ section#academia
|
||||
|
||||
// region Expertise
|
||||
.grid.grid-cols-1.gap-6.mb-8(class="md:grid-cols-1")
|
||||
+Collapsable("academia", academia.expertise.frontendTechnologies, true)
|
||||
+Collapsable("academia", academia.expertise.frontendTechnologies)
|
||||
+Collapsable("academia", academia.expertise.devopsAndCloud)
|
||||
+Collapsable("academia", academia.expertise.backendTechnologies)
|
||||
+Collapsable("academia", academia.expertise.databaseAndData)
|
||||
|
||||
@@ -6,7 +6,7 @@ mixin Collapsable(name, data, open)
|
||||
|
||||
details.mb-4(open=isExpanded, name=name)
|
||||
summary(
|
||||
class=`rounded-sm transition mb-2 cursor-pointer text-md font-semibold mb-2 cursor-pointer sm:text-lg text-nls-${color} dark:text-nls-${color} focus:outline-none focus:z-10 focus:ring-4 focus:ring-nls-${color} focus:bg-nls-${color} focus:text-black focus:no-underline`,
|
||||
class=`rounded-sm transition mb-2 cursor-pointer text-md font-semibold mb-2 cursor-pointer sm:text-lg text-nls-${color} dark:text-nls-${color} focus:outline-none focus:z-10 focus:ring-4 focus:ring-nls-${color} focus:bg-nls-${color} focus:text-black dark:focus:text-white focus:no-underline`,
|
||||
onclick=`umami.track('collapsable clicked', { category: '${category}', visitDuration: getVisitDuration() })`
|
||||
)= data.summary
|
||||
ul.list-disc.list-inside.text-slate-600(class="dark:text-stone-300")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
header.bg-white.text-nls-black(class="dark:text-white dark:bg-nls-black")
|
||||
header.bg-white.text-nls-black.relative(class="dark:text-white dark:bg-nls-black")
|
||||
.teaser.p-8.flex.flex-col.items-center.justify-center(class="sm:p-20")
|
||||
.max-w-3xl.mb-8.relative(class="w-4/5 min-h-[90vh]")
|
||||
.peer.absolute.bottom-0.left-0.right-0.z-40.text-center.max-w-3xl.center.py-8
|
||||
@@ -11,348 +11,312 @@ header.bg-white.text-nls-black(class="dark:text-white dark:bg-nls-black")
|
||||
|
|
||||
| & #{landingpage.jobTitle[1]}
|
||||
|
||||
canvas#aurora-canvas.absolute.z-0.left-0.right-0.mx-auto.transition(class="h-[54vh] w-full")
|
||||
canvas#aurora-canvas.absolute.z-0.left-0.right-0.bottom-0.top-0.mx-auto.transition(class="h-[100vh] w-full")
|
||||
|
||||
script(src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js")
|
||||
|
||||
script.
|
||||
class QuantumAurora {
|
||||
constructor(canvas) {
|
||||
class GuillocheCurtain {
|
||||
constructor(canvas, options = {}) {
|
||||
this.canvas = canvas;
|
||||
this.gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
|
||||
this.time = 0;
|
||||
this.animationId = null;
|
||||
this.mouseX = 0.5;
|
||||
this.mouseY = 0.5;
|
||||
|
||||
if (!this.gl) {
|
||||
console.warn('WebGL not supported');
|
||||
return;
|
||||
}
|
||||
|
||||
this.initShaders();
|
||||
this.initBuffers();
|
||||
this.setupInteraction();
|
||||
this.resize();
|
||||
this.render();
|
||||
}
|
||||
|
||||
initShaders() {
|
||||
const vertexShaderSource = `
|
||||
attribute vec2 a_position;
|
||||
varying vec2 v_uv;
|
||||
void main() {
|
||||
gl_Position = vec4(a_position, 0.0, 1.0);
|
||||
v_uv = a_position * 0.5 + 0.5;
|
||||
}
|
||||
`;
|
||||
|
||||
const fragmentShaderSource = `
|
||||
precision highp float;
|
||||
uniform float u_time;
|
||||
uniform vec2 u_resolution;
|
||||
uniform vec2 u_mouse;
|
||||
varying vec2 v_uv;
|
||||
|
||||
// Noise functions for organic aurora movement
|
||||
float hash(vec2 p) {
|
||||
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
|
||||
}
|
||||
|
||||
float noise(vec2 p) {
|
||||
vec2 i = floor(p);
|
||||
vec2 f = fract(p);
|
||||
f = f * f * (3.0 - 2.0 * f);
|
||||
|
||||
float a = hash(i);
|
||||
float b = hash(i + vec2(1.0, 0.0));
|
||||
float c = hash(i + vec2(0.0, 1.0));
|
||||
float d = hash(i + vec2(1.0, 1.0));
|
||||
|
||||
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
|
||||
}
|
||||
|
||||
float fbm(vec2 p) {
|
||||
float value = 0.0;
|
||||
float amplitude = 0.5;
|
||||
float frequency = 1.0;
|
||||
|
||||
for(int i = 0; i < 6; i++) {
|
||||
value += amplitude * noise(p * frequency);
|
||||
amplitude *= 0.5;
|
||||
frequency *= 2.0;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// Magnetic field simulation
|
||||
vec2 magneticField(vec2 pos, float time) {
|
||||
vec2 field = vec2(0.0);
|
||||
|
||||
// Earth's magnetic field approximation
|
||||
float lat = (pos.y - 0.5) * 3.14159;
|
||||
float lon = (pos.x - 0.5) * 6.28318;
|
||||
|
||||
// Magnetic dipole field with solar wind interaction
|
||||
float solarWind = sin(time * 0.1 + pos.x * 2.0) * 0.3 + 0.7;
|
||||
float magneticStrength = cos(lat) * solarWind;
|
||||
|
||||
field.x = sin(lat * 2.0 + time * 0.2) * magneticStrength;
|
||||
field.y = cos(lat * 3.0 + time * 0.15) * magneticStrength;
|
||||
|
||||
return field * 0.5;
|
||||
}
|
||||
|
||||
// Particle trajectory simulation
|
||||
float particleFlow(vec2 uv, float time) {
|
||||
vec2 pos = uv;
|
||||
float intensity = 0.0;
|
||||
|
||||
// Simulate charged particle streams
|
||||
for(int i = 0; i < 8; i++) {
|
||||
float fi = float(i);
|
||||
vec2 offset = vec2(sin(fi * 0.5), cos(fi * 0.7)) * 0.1;
|
||||
vec2 particlePos = pos + offset;
|
||||
|
||||
// Particle follows magnetic field lines
|
||||
vec2 field = magneticField(particlePos, time + fi);
|
||||
particlePos += field * 0.3;
|
||||
|
||||
// Altitude-dependent intensity (atmosphere interaction)
|
||||
float altitude = 1.0 - abs(particlePos.y - 0.5) * 2.0;
|
||||
altitude = smoothstep(0.2, 0.8, altitude);
|
||||
|
||||
// Particle stream noise
|
||||
float streamNoise = fbm(particlePos * 8.0 + vec2(time * 0.3, 0.0));
|
||||
|
||||
intensity += altitude * streamNoise * 0.125;
|
||||
}
|
||||
|
||||
return intensity;
|
||||
}
|
||||
|
||||
// Guilloche pattern generation
|
||||
float guilloche(vec2 uv, float time) {
|
||||
vec2 pos = uv * 20.0;
|
||||
|
||||
// Multiple rotating circles creating interference patterns
|
||||
float g = 0.0;
|
||||
|
||||
// Primary guilloche rings
|
||||
for(int i = 0; i < 4; i++) {
|
||||
float fi = float(i);
|
||||
float radius = 3.0 + fi * 2.0;
|
||||
float speed = 0.5 + fi * 0.2;
|
||||
float phase = fi * 1.5708; // π/2 phase offset
|
||||
|
||||
vec2 center = vec2(
|
||||
sin(time * speed + phase) * 2.0,
|
||||
cos(time * speed * 0.7 + phase) * 1.5
|
||||
);
|
||||
|
||||
float dist = length(pos - center);
|
||||
g += sin(dist * radius - time * (speed * 10.0 + 5.0)) / (radius * 0.5);
|
||||
}
|
||||
|
||||
// Secondary interference pattern
|
||||
float interference = sin(pos.x * 3.0 + time) * sin(pos.y * 2.0 + time * 1.3);
|
||||
g += interference * 0.3;
|
||||
|
||||
return g;
|
||||
}
|
||||
|
||||
// Aurora curtain effect
|
||||
vec3 auroraColor(vec2 uv, float time) {
|
||||
// Particle flow intensity
|
||||
float particles = particleFlow(uv, time);
|
||||
|
||||
// Guilloche modulation
|
||||
float guillochePattern = guilloche(uv, time * 0.5);
|
||||
float guillocheModulation = sin(guillochePattern * 0.5) * 0.5 + 0.5;
|
||||
|
||||
// Combine particle physics with guilloche aesthetics
|
||||
float intensity = particles * (0.7 + guillocheModulation * 0.6);
|
||||
|
||||
// Atmospheric layers - different altitudes emit different colors
|
||||
float altitude = uv.y;
|
||||
|
||||
// Oxygen emissions (green/red) - 557.7nm, 630.0nm
|
||||
vec3 oxygen_green = vec3(0.3, 0.9, 0.2);
|
||||
vec3 oxygen_red = vec3(0.8, 0.2, 0.1);
|
||||
|
||||
// Nitrogen emissions (blue/purple) - 427.8nm, 391.4nm
|
||||
vec3 nitrogen_blue = vec3(0.2, 0.4, 0.9);
|
||||
vec3 nitrogen_purple = vec3(0.6, 0.2, 0.8);
|
||||
|
||||
// Helium (rare, yellow) - 587.6nm
|
||||
vec3 helium_yellow = vec3(0.9, 0.8, 0.2);
|
||||
|
||||
// Altitude-based color mixing (realistic atmospheric chemistry)
|
||||
vec3 color = vec3(0.0);
|
||||
|
||||
// High altitude (200-400km) - mostly oxygen red
|
||||
float highAlt = smoothstep(0.7, 0.9, altitude);
|
||||
color += oxygen_red * highAlt;
|
||||
|
||||
// Mid altitude (100-200km) - oxygen green dominates
|
||||
float midAlt = smoothstep(0.3, 0.7, altitude) * smoothstep(0.7, 0.5, altitude);
|
||||
color += oxygen_green * midAlt * 2.0;
|
||||
|
||||
// Lower altitude (80-100km) - nitrogen blue/purple
|
||||
float lowAlt = smoothstep(0.0, 0.4, altitude) * smoothstep(0.6, 0.3, altitude);
|
||||
color += mix(nitrogen_blue, nitrogen_purple, sin(time * 0.3 + uv.x * 5.0) * 0.5 + 0.5) * lowAlt;
|
||||
|
||||
// Rare helium emissions (very sparse)
|
||||
float heliumChance = noise(uv * 100.0 + time * 0.1);
|
||||
if(heliumChance > 0.95) {
|
||||
color += helium_yellow * 0.3;
|
||||
}
|
||||
|
||||
// Solar wind interaction - color temperature shift
|
||||
float solarActivity = sin(time * 0.05) * 0.3 + 0.7;
|
||||
color *= solarActivity;
|
||||
|
||||
// Magnetic field strength affects brightness
|
||||
vec2 magneticF = magneticField(uv, time);
|
||||
float magneticIntensity = length(magneticF);
|
||||
|
||||
return color * intensity * (0.5 + magneticIntensity);
|
||||
}
|
||||
|
||||
// Atmospheric scattering simulation
|
||||
vec3 atmosphericScattering(vec3 auroraColor, vec2 uv) {
|
||||
float atmosphereDensity = exp(-abs(uv.y - 0.5) * 8.0);
|
||||
|
||||
// Rayleigh scattering (blue light scattered more)
|
||||
vec3 rayleigh = vec3(0.1, 0.2, 0.6) * atmosphereDensity * 0.1;
|
||||
|
||||
// Mie scattering (dust/ice crystals)
|
||||
vec3 mie = vec3(0.8, 0.8, 0.9) * atmosphereDensity * 0.05;
|
||||
|
||||
return auroraColor + rayleigh + mie;
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
|
||||
|
||||
// Mouse interaction - simulates solar wind direction
|
||||
vec2 solarWind = (u_mouse - 0.5) * 0.5;
|
||||
uv += solarWind * 0.1;
|
||||
|
||||
// Generate aurora
|
||||
vec3 aurora = auroraColor(uv, u_time);
|
||||
|
||||
// Apply atmospheric effects
|
||||
aurora = atmosphericScattering(aurora, uv);
|
||||
|
||||
// Subtle animation shimmer
|
||||
float shimmer = sin(u_time * 2.0 + uv.x * 10.0 + uv.y * 15.0) * 0.05 + 0.95;
|
||||
aurora *= shimmer;
|
||||
|
||||
// Transparency based on intensity
|
||||
float alpha = length(aurora) * 0.8;
|
||||
alpha = smoothstep(0.0, 0.3, alpha);
|
||||
|
||||
gl_FragColor = vec4(aurora, alpha);
|
||||
}
|
||||
`;
|
||||
|
||||
this.program = this.createProgram(vertexShaderSource, fragmentShaderSource);
|
||||
this.gl.useProgram(this.program);
|
||||
|
||||
this.u_time = this.gl.getUniformLocation(this.program, 'u_time');
|
||||
this.u_resolution = this.gl.getUniformLocation(this.program, 'u_resolution');
|
||||
this.u_mouse = this.gl.getUniformLocation(this.program, 'u_mouse');
|
||||
}
|
||||
|
||||
createProgram(vertexSource, fragmentSource) {
|
||||
const vertexShader = this.createShader(this.gl.VERTEX_SHADER, vertexSource);
|
||||
const fragmentShader = this.createShader(this.gl.FRAGMENT_SHADER, fragmentSource);
|
||||
|
||||
const program = this.gl.createProgram();
|
||||
this.gl.attachShader(program, vertexShader);
|
||||
this.gl.attachShader(program, fragmentShader);
|
||||
this.gl.linkProgram(program);
|
||||
|
||||
if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
|
||||
console.error('Program link error:', this.gl.getProgramInfoLog(program));
|
||||
return null;
|
||||
}
|
||||
|
||||
return program;
|
||||
}
|
||||
|
||||
createShader(type, source) {
|
||||
const shader = this.gl.createShader(type);
|
||||
this.gl.shaderSource(shader, source);
|
||||
this.gl.compileShader(shader);
|
||||
|
||||
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
|
||||
console.error('Shader compile error:', this.gl.getShaderInfoLog(shader));
|
||||
return null;
|
||||
}
|
||||
|
||||
return shader;
|
||||
}
|
||||
|
||||
initBuffers() {
|
||||
const vertices = new Float32Array([-1,-1, 1,-1, -1,1, 1,1]);
|
||||
this.buffer = this.gl.createBuffer();
|
||||
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer);
|
||||
this.gl.bufferData(this.gl.ARRAY_BUFFER, vertices, this.gl.STATIC_DRAW);
|
||||
|
||||
const a_position = this.gl.getAttribLocation(this.program, 'a_position');
|
||||
this.gl.enableVertexAttribArray(a_position);
|
||||
this.gl.vertexAttribPointer(a_position, 2, this.gl.FLOAT, false, 0, 0);
|
||||
}
|
||||
|
||||
setupInteraction() {
|
||||
this.canvas.addEventListener('mousemove', (e) => {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
this.mouseX = e.clientX / rect.width;
|
||||
this.mouseY = 1.0 - (e.clientY / rect.height);
|
||||
this.options = {
|
||||
// Visual parameters
|
||||
spread: 0.25, // Main spread factor (was 0.08)
|
||||
lineWidth: 0.8, // Line thickness
|
||||
opacity: 0.6, // Base opacity
|
||||
splineCount: 40, // Lines per group
|
||||
groupCount: 6, // Number of spline groups
|
||||
|
||||
// Animation parameters
|
||||
animationSpeed: 0.005, // Global time multiplier
|
||||
nodeCount: 7, // Attractor nodes
|
||||
nodeSpeed: {min: 0.3, max: 0.8},
|
||||
|
||||
// Color parameters
|
||||
hueBase: 0.55, // Base hue (cyan-ish)
|
||||
hueVariation: 0.1, // Color spread
|
||||
saturation: {min: 0.3, max: 0.7},
|
||||
lightness: {min: 0.25, max: 0.65},
|
||||
|
||||
// Spline parameters
|
||||
segments: 100, // Resolution per spline
|
||||
offset: 5, // General offset between splines
|
||||
startOffset: 0.0, // Y-offset at spline start
|
||||
endOffset: 0.0, // Y-offset at spline end
|
||||
offsetTransition: 'smooth', // 'linear', 'smooth', 'sine'
|
||||
canvasExtension: 1.0, // How far beyond canvas borders (-1.0 means 50% extra on each side)
|
||||
|
||||
// Wave complexity
|
||||
waveFrequencies: [4, 8, 16], // Multiple harmonics
|
||||
waveAmplitudes: [0.4, 0.25, 0.15], // Corresponding amplitudes
|
||||
|
||||
...options
|
||||
};
|
||||
|
||||
this.scene = new THREE.Scene();
|
||||
this.camera = new THREE.OrthographicCamera(-2, 2, 1.5, -1.5, 0.1, 1000);
|
||||
this.renderer = new THREE.WebGLRenderer({
|
||||
canvas: canvas,
|
||||
alpha: true,
|
||||
antialias: true
|
||||
});
|
||||
|
||||
this.time = 0;
|
||||
this.splineGroups = [];
|
||||
this.nodes = [];
|
||||
|
||||
this.init();
|
||||
this.createNodes();
|
||||
this.createSplines();
|
||||
this.animate();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.renderer.setSize(this.canvas.clientWidth, this.canvas.clientHeight);
|
||||
this.renderer.setClearColor(0x000000, 0);
|
||||
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
this.camera.position.z = 1;
|
||||
}
|
||||
|
||||
createNodes() {
|
||||
for (let i = 0; i < this.options.nodeCount; i++) {
|
||||
this.nodes.push({
|
||||
x: (Math.random() - 0.5) * 3,
|
||||
y: (Math.random() - 0.5) * 2,
|
||||
phase: Math.random() * Math.PI * 2,
|
||||
radius: 0.2 + Math.random() * 0.3,
|
||||
speed: this.options.nodeSpeed.min +
|
||||
Math.random() * (this.options.nodeSpeed.max - this.options.nodeSpeed.min)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate offset transition from start to end
|
||||
getOffsetTransition(t) {
|
||||
const {startOffset, endOffset, offsetTransition} = this.options;
|
||||
|
||||
switch (offsetTransition) {
|
||||
case 'linear':
|
||||
return startOffset + (endOffset - startOffset) * t;
|
||||
case 'sine':
|
||||
return startOffset + (endOffset - startOffset) * Math.sin(t * Math.PI * 0.5);
|
||||
case 'smooth':
|
||||
default:
|
||||
// Smooth hermite interpolation
|
||||
const t2 = t * t;
|
||||
const t3 = t2 * t;
|
||||
return startOffset + (endOffset - startOffset) * (3 * t2 - 2 * t3);
|
||||
}
|
||||
}
|
||||
|
||||
createSplines() {
|
||||
for (let groupIndex = 0; groupIndex < this.options.groupCount; groupIndex++) {
|
||||
const group = new THREE.Group();
|
||||
const splines = [];
|
||||
|
||||
for (let i = 0; i < this.options.splineCount; i++) {
|
||||
const t = i / (this.options.splineCount - 1);
|
||||
// Create distribution that's denser in the center
|
||||
const normalizedOffset = (t - 0.2) * this.options.offset; // -1 to 1
|
||||
const offset = normalizedOffset * this.options.spread;
|
||||
|
||||
const spline = this.createSingleSpline(offset, groupIndex, i);
|
||||
splines.push({line: spline, offset});
|
||||
group.add(spline);
|
||||
}
|
||||
|
||||
this.splineGroups.push({
|
||||
group,
|
||||
splines,
|
||||
baseOffset: groupIndex * 0.6,
|
||||
phase: groupIndex * 0.8
|
||||
});
|
||||
this.scene.add(group);
|
||||
}
|
||||
}
|
||||
|
||||
createSingleSpline(offset, groupIndex, splineIndex) {
|
||||
const points = this.generateSplinePoints(offset, groupIndex, splineIndex, 0);
|
||||
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
||||
|
||||
// Enhanced color calculation
|
||||
const normalizedGroup = groupIndex / (this.options.groupCount - 1);
|
||||
const normalizedSpline = splineIndex / (this.options.splineCount - 1);
|
||||
|
||||
const hue = this.options.hueBase +
|
||||
normalizedGroup * this.options.hueVariation +
|
||||
Math.sin(normalizedSpline * Math.PI * 2) * 0.05;
|
||||
|
||||
const saturation = this.options.saturation.min +
|
||||
(this.options.saturation.max - this.options.saturation.min) *
|
||||
(1 - Math.abs(offset) / this.options.spread);
|
||||
|
||||
const lightness = this.options.lightness.min +
|
||||
(this.options.lightness.max - this.options.lightness.min) *
|
||||
(1 - Math.abs(offset) / this.options.spread * 0.6);
|
||||
|
||||
const material = new THREE.LineBasicMaterial({
|
||||
color: new THREE.Color().setHSL(hue, saturation, lightness),
|
||||
transparent: true,
|
||||
opacity: this.options.opacity * (1 - Math.abs(offset) / this.options.spread * 0.3),
|
||||
linewidth: this.options.lineWidth
|
||||
});
|
||||
|
||||
return new THREE.Line(geometry, material);
|
||||
}
|
||||
|
||||
generateSplinePoints(offset, groupIndex, splineIndex, timeOffset = 0) {
|
||||
const points = [];
|
||||
const {segments} = this.options;
|
||||
|
||||
for (let i = 0; i <= segments; i++) {
|
||||
const t = i / segments;
|
||||
const baseX = -(2 + this.options.canvasExtension) + t * (4 + 2 * this.options.canvasExtension);
|
||||
|
||||
let x = baseX;
|
||||
let y = 0;
|
||||
|
||||
// Apply node influences with multiple wave frequencies
|
||||
this.nodes.forEach((node, nodeIndex) => {
|
||||
const distance = Math.abs(baseX - node.x);
|
||||
const influence = Math.exp(-distance * distance * 2);
|
||||
|
||||
const animatedPhase = node.phase +
|
||||
(this.time + timeOffset) * node.speed +
|
||||
groupIndex * 0.2 +
|
||||
splineIndex * 0.01;
|
||||
|
||||
const waveAmplitude = node.radius * influence;
|
||||
|
||||
// Multiple harmonics for complexity
|
||||
this.options.waveFrequencies.forEach((freq, freqIndex) => {
|
||||
const amplitude = this.options.waveAmplitudes[freqIndex] || 0.1;
|
||||
const phaseMultiplier = 1 + freqIndex * 0.3;
|
||||
|
||||
y += Math.sin(t * Math.PI * freq + animatedPhase * phaseMultiplier) *
|
||||
waveAmplitude * amplitude;
|
||||
});
|
||||
|
||||
// Horizontal modulation for more organic feel
|
||||
x += Math.cos(t * Math.PI * 6 + animatedPhase * 0.9) * waveAmplitude * 0.1;
|
||||
});
|
||||
|
||||
// Apply spread offset with transition
|
||||
const transitionOffset = this.getOffsetTransition(t);
|
||||
y += offset * (1 + Math.sin(t * Math.PI * 2 + (this.time + timeOffset) * 0.5) * 0.3) +
|
||||
transitionOffset;
|
||||
|
||||
points.push(new THREE.Vector3(x, y, 0));
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
updateSplineGeometry(splineData, groupIndex, splineIndex) {
|
||||
const {line, offset} = splineData;
|
||||
const points = this.generateSplinePoints(offset, groupIndex, splineIndex);
|
||||
line.geometry.setFromPoints(points);
|
||||
}
|
||||
|
||||
animate() {
|
||||
this.time += this.options.animationSpeed;
|
||||
|
||||
// Animate nodes with boundary constraints
|
||||
this.nodes.forEach((node, index) => {
|
||||
node.x += Math.sin(this.time * 0.3 + index) * 0.002;
|
||||
node.y += Math.cos(this.time * 0.4 + index * 1.3) * 0.002;
|
||||
|
||||
// Keep nodes within reasonable bounds
|
||||
node.x = Math.max(-1.5, Math.min(1.5, node.x));
|
||||
node.y = Math.max(-1, Math.min(1, node.y));
|
||||
});
|
||||
|
||||
// Update spline groups
|
||||
this.splineGroups.forEach((splineGroup, groupIndex) => {
|
||||
const {group, splines, phase} = splineGroup;
|
||||
|
||||
// Subtle group transformations
|
||||
group.position.y = Math.sin(this.time * 0.2 + phase) * 0.08;
|
||||
group.position.x = Math.cos(this.time * 0.15 + phase) * 0.04;
|
||||
group.rotation.z = Math.sin(this.time * 0.1 + phase) * 0.015;
|
||||
|
||||
// Update individual splines
|
||||
splines.forEach((splineData, splineIndex) => {
|
||||
this.updateSplineGeometry(splineData, groupIndex, splineIndex);
|
||||
});
|
||||
});
|
||||
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
requestAnimationFrame(() => this.animate());
|
||||
}
|
||||
|
||||
// Public methods for runtime configuration
|
||||
updateOptions(newOptions) {
|
||||
Object.assign(this.options, newOptions);
|
||||
// Trigger rebuild if necessary
|
||||
this.rebuild();
|
||||
}
|
||||
|
||||
rebuild() {
|
||||
// Clean up existing splines
|
||||
this.splineGroups.forEach(({group}) => {
|
||||
group.children.forEach(child => {
|
||||
child.geometry.dispose();
|
||||
child.material.dispose();
|
||||
});
|
||||
this.scene.remove(group);
|
||||
});
|
||||
|
||||
this.splineGroups = [];
|
||||
this.createSplines();
|
||||
}
|
||||
|
||||
resize() {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
//this.canvas.width = rect.width * window.devicePixelRatio;
|
||||
//this.canvas.height = rect.height * window.devicePixelRatio;
|
||||
this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.gl.uniform2f(this.u_resolution, this.canvas.width, this.canvas.height);
|
||||
}
|
||||
const width = this.canvas.clientWidth;
|
||||
const height = this.canvas.clientHeight;
|
||||
|
||||
render() {
|
||||
if (!this.gl) return;
|
||||
this.renderer.setSize(width, height);
|
||||
|
||||
this.time += 0.016;
|
||||
|
||||
this.gl.enable(this.gl.BLEND);
|
||||
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
|
||||
this.gl.clearColor(0, 0, 0, 0);
|
||||
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
|
||||
|
||||
this.gl.uniform1f(this.u_time, this.time);
|
||||
this.gl.uniform2f(this.u_mouse, this.mouseX, this.mouseY);
|
||||
this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
|
||||
|
||||
this.animationId = requestAnimationFrame(() => this.render());
|
||||
const aspect = width / height;
|
||||
this.camera.left = -2 * aspect;
|
||||
this.camera.right = 2 * aspect;
|
||||
this.camera.top = 1.5;
|
||||
this.camera.bottom = -1.5;
|
||||
this.camera.updateProjectionMatrix();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.animationId) cancelAnimationFrame(this.animationId);
|
||||
this.splineGroups.forEach(({group}) => {
|
||||
group.children.forEach(child => {
|
||||
child.geometry.dispose();
|
||||
child.material.dispose();
|
||||
});
|
||||
});
|
||||
this.renderer.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Usage example with custom options
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const canvas = document.getElementById('aurora-canvas');
|
||||
if (canvas) {
|
||||
const aurora = new QuantumAurora(canvas);
|
||||
|
||||
let resizeTimeout;
|
||||
window.addEventListener('resize', () => {
|
||||
clearTimeout(resizeTimeout);
|
||||
resizeTimeout = setTimeout(() => aurora.resize(), 100);
|
||||
const curtain = new GuillocheCurtain(canvas, {
|
||||
spread: 7, // Wider spread
|
||||
segments: 17,
|
||||
lineWidth: 0.1, // Thicker lines
|
||||
splineCount: 21, // More lines
|
||||
groupCount: 2,
|
||||
canvasExtension: 0.1,
|
||||
offset: 0.1,
|
||||
startOffset: -0.1, // Slight downward start
|
||||
endOffset: 0.2, // Slight upward end
|
||||
offsetTransition: 'smooth', // Smooth transition
|
||||
animationSpeed: 0.001, // Slower animation
|
||||
hueBase: 0.55, // More blue-green
|
||||
hueVariation: 0.3,
|
||||
opacity: 0.7
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', () => aurora.destroy());
|
||||
window.addEventListener('beforeunload', () => curtain.destroy());
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user