Initial commit

This commit is contained in:
2026-05-12 23:33:46 +02:00
commit ccf32dcece
104 changed files with 17439 additions and 0 deletions

577
js/animatedLogo.js Normal file
View File

@@ -0,0 +1,577 @@
const CURSOR_INFLUENCE_INNER = 150;
const CURSOR_INFLUENCE_OUTER = 300;
const EASING_FACTOR = 0.03;
const COLORS = [
'#e0775d',
'#7cc0c6',
'#e05680',
'#46ae51',
'#bb8dd9',
'#f7ff29',
];
class FloatingShape {
constructor(element, originalX, originalY, width, height) {
this.element = element;
this.width = width;
this.height = height;
this.originalX = originalX;
this.originalY = originalY;
this.posX = originalX;
this.posY = originalY;
this.targetX = originalX;
this.targetY = originalY;
}
update(mouseX, mouseY) {
const shapeCenterX = this.originalX + this.width / 2;
const shapeCenterY = this.originalY + this.height / 2;
const dx = mouseX - shapeCenterX;
const dy = mouseY - shapeCenterY;
const distance = Math.sqrt(dx * dx + dy * dy);
const innerRadius = CURSOR_INFLUENCE_INNER / 2;
const outerRadius = CURSOR_INFLUENCE_OUTER / 2;
if (distance < outerRadius && distance > 0) {
const dirX = dx / distance;
const dirY = dy / distance;
let strength, maxDisplacement;
if (distance < innerRadius) {
strength = (innerRadius - distance) / innerRadius;
maxDisplacement = innerRadius;
} else {
const outerZoneProgress = (outerRadius - distance) / (outerRadius - innerRadius);
strength = outerZoneProgress * 0.5;
maxDisplacement = innerRadius * 0.6;
}
this.targetX = this.originalX - dirX * strength * maxDisplacement;
this.targetY = this.originalY - dirY * strength * maxDisplacement;
} else {
this.targetX = this.originalX;
this.targetY = this.originalY;
}
this.posX += (this.targetX - this.posX) * EASING_FACTOR;
this.posY += (this.targetY - this.posY) * EASING_FACTOR;
}
render() {
this.element.style.transform = `translate3d(${this.posX}px, ${this.posY}px, 0)`;
}
}
class FloatingShapesManager {
constructor(containerId) {
this.container = document.getElementById(containerId);
if (!this.container) {
console.error(`Container #${containerId} not found`);
return;
}
this.shapes = [];
this.strokePaths = []; // Store all stroke paths for fill control
this.fillShape = null; // Reference to the filled shape SVG for fade control
this.fillShapeReady = false; // Track if fillshape animation has completed
this.thalimText = null; // Reference to the THALIM text element
this.textReady = false; // Track if text animation has completed
this.mouseX = 0;
this.mouseY = 0;
this.animationId = null;
this.isTouching = false; // Track if currently in a touch interaction
this.init();
}
init() {
const containerRect = this.container.getBoundingClientRect();
const containerWidth = containerRect.width;
const containerHeight = containerRect.height;
const shapeConfigs = [
{
id: 'shape-1',
svgPath: `${themeDirURI}/assets/logo-shapes/shape1.svg`,
baseWidth: 53.564522,
baseHeight: 112.37409,
scale: 1.5,
posX: this.isMobile() ? 20: 35,
posY: -20,
strokeWidth: 2,
animationDuration: 1.9,
gradientStart: COLORS[0], // orange
gradientEnd: '#e0b7ad'
},
{
id: 'shape-2',
svgPath: `${themeDirURI}/assets/logo-shapes/shape4.svg`,
baseWidth: 74.08564,
baseHeight: 121.90051,
scale: 1.5,
posX: 0,
posY: this.isMobile() ? -8 : -5,
strokeWidth: 2,
animationDuration: 2.5,
gradientStart: '#aec4c6',
gradientEnd: COLORS[1] // blue
},
{
id: 'shape-3',
svgPath: `${themeDirURI}/assets/logo-shapes/shape3.svg`,
baseWidth: 159.16571,
baseHeight: 87.756729,
scale: 1.5,
posX: 0,
posY: -10,
strokeWidth: 2,
animationDuration: 3.1,
gradientStart: COLORS[2], // pink
gradientEnd: '#e0b0be'
},
{
id: 'shape-4',
svgPath: `${themeDirURI}/assets/logo-shapes/shape2.svg`,
baseWidth: 143.26076,
baseHeight: 20.26207,
scale: 1.5,
posX: 0,
posY: this.isMobile() ? 20 : 45,
strokeWidth: 2,
animationDuration: 3.7,
gradientStart: '#8bc491',
gradientEnd: COLORS[3] // green
},
{
id: 'shape-5',
svgPath: `${themeDirURI}/assets/logo-shapes/shape5.svg`,
baseWidth: 155.66518,
baseHeight: 87.785599,
scale: 1.5,
posX: this.isMobile() ? 13 : 19.5,
posY: this.isMobile() ? -18 : -23.5,
strokeWidth: 2,
animationDuration: 4.3,
gradientStart: COLORS[4], // purple
gradientEnd: '#c9b0d9'
},
{
id: 'fillshape',
svgPath: `${themeDirURI}/assets/logo-shapes/fillshape.svg`,
baseWidth: 142.78297,
baseHeight: 72.903015,
scale: this.isMobile() ? 0.78 : 1.47,
posX: this.isMobile() ? 7.5 : 11,
posY: this.isMobile() ? -13 : -14.5,
isFilled: true,
targetOpacity: 1,
animationDuration: 0.9,
animationDelay: 3.7 // Start after longest stroke animation
}
];
// Create shapes with center-based positioning
const centerX = containerWidth / 2;
const centerY = containerHeight / 2;
// Apply mobile scale on smaller viewports
const mobileScale = 0.8;
shapeConfigs.forEach((config) => {
const scale = config.id === 'fillshape' ? config.scale : this.isMobile() ? mobileScale : config.scale;
const scaledWidth = config.baseWidth * scale;
const scaledHeight = config.baseHeight * scale;
// Calculate position from center with offset, minus half dimensions to center the shape
const x = centerX + (config.posX || 0) - scaledWidth / 2;
const y = centerY + (config.posY || 0) - scaledHeight / 2;
// Create animated stroke element
this.createAnimatedStrokeElement(config, scaledWidth, scaledHeight, x, y);
});
// Create THALIM text overlay
this.createThalimText(centerX, centerY);
this.container.addEventListener('mousemove', this.handleMouseMove.bind(this));
this.container.addEventListener('mouseleave', this.handleMouseLeave.bind(this));
this.container.addEventListener('touchstart', this.handleTouchStart.bind(this));
this.container.addEventListener('touchmove', this.handleTouchMove.bind(this), { passive: false });
this.container.addEventListener('touchend', this.handleTouchEnd.bind(this));
window.addEventListener('resize', this.handleResize.bind(this));
// Set sketch margin on mobile based on hero-logos height
this.setSketchMarginOnMobile();
this.animate();
}
isMobile() {
return window.innerWidth < 768; // tablet breakpoint from scss/_variables.scss
}
setSketchMarginOnMobile() {
if (this.isMobile()) {
const heroLogos = document.querySelector('.hero-logos');
if (heroLogos) {
const logoHeight = heroLogos.offsetHeight;
this.container.style.marginTop = `${logoHeight}px`;
}
} else {
// Reset margin-top on desktop (handled by CSS)
this.container.style.marginTop = '';
}
}
async createAnimatedStrokeElement(config, width, height, x, y) {
try {
const response = await fetch(config.svgPath);
const svgText = await response.text();
const parser = new DOMParser();
const svgDoc = parser.parseFromString(svgText, 'image/svg+xml');
const svgElement = svgDoc.querySelector('svg');
if (!svgElement) {
console.error(`Failed to parse SVG for ${config.id}`);
return;
}
// Create shape div for physics
const shapeDiv = document.createElement('div');
shapeDiv.className = 'floating-shape';
shapeDiv.id = config.id;
shapeDiv.style.zIndex = config.isFilled ? '10' : '1'; // Fillshape above strokes
// Set SVG dimensions
svgElement.setAttribute('width', width);
svgElement.setAttribute('height', height);
if (config.isFilled) {
// Handle filled shapes (fade-in animation)
svgElement.style.opacity = '0';
svgElement.style.transition = 'opacity 0.3s ease-out'; // Smooth fade on mouse interaction
const duration = config.animationDuration || 1.5;
const delay = config.animationDelay || 0;
const targetOpacity = config.targetOpacity || 0.5;
svgElement.style.setProperty('--target-opacity', targetOpacity);
svgElement.style.animation = `fadeIn ${duration}s ease-in-out ${delay}s forwards`;
// Remove animation after it completes so we can control opacity manually
setTimeout(() => {
svgElement.style.animation = 'none';
svgElement.style.opacity = targetOpacity;
this.fillShapeReady = true; // Mark fillshape as ready for interaction
}, (delay + duration) * 1000);
// Store reference for mouse interaction
this.fillShape = svgElement;
this.fillShapeOpacity = targetOpacity;
// Position the fillshape div (since it's not in the physics system)
shapeDiv.style.transform = `translate3d(${x}px, ${y}px, 0)`;
} else {
// Handle stroked shapes (gradient + stroke drawing animation)
// Create gradient for stroke
const gradientId = `gradient-${config.id}`;
// Create defs element if it doesn't exist
let defs = svgElement.querySelector('defs');
if (!defs) {
defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
svgElement.insertBefore(defs, svgElement.firstChild);
}
// Create linear gradient
const gradient = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient');
gradient.setAttribute('id', gradientId);
gradient.setAttribute('x1', '0%');
gradient.setAttribute('y1', '0%');
gradient.setAttribute('x2', '100%');
gradient.setAttribute('y2', '100%');
// Create gradient stops
const stop1 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
stop1.setAttribute('offset', '0%');
stop1.setAttribute('stop-color', config.gradientStart);
const stop2 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
stop2.setAttribute('offset', '100%');
stop2.setAttribute('stop-color', config.gradientEnd);
gradient.appendChild(stop1);
gradient.appendChild(stop2);
defs.appendChild(gradient);
// Apply stroke styling and animation to all paths in the SVG
const paths = svgElement.querySelectorAll('path, polyline, polygon, line, circle, ellipse, rect');
paths.forEach(path => {
// Set stroke properties
path.style.fill = 'white';
path.style.fillOpacity = '0'; // Start invisible
path.style.transition = 'fill-opacity 0.5s ease-in-out'; // Smooth fill changes
path.style.stroke = `url(#${gradientId})`;
// Don't override stroke-width - preserve SVG's compensated values
path.style.strokeLinecap = 'round';
path.style.strokeLinejoin = 'round';
// Calculate path length for animation
const pathLength = path.getTotalLength ? path.getTotalLength() : 1000;
// Set CSS variable for path length
path.style.setProperty('--path-length', pathLength);
// Set up stroke dash animation
path.style.strokeDasharray = pathLength;
path.style.strokeDashoffset = pathLength;
// Create CSS animation (plays once and stays completed)
const duration = config.animationDuration || 3;
path.style.animation = `drawStroke ${duration}s ease-in-out forwards`;
// Store path reference for fill control
this.strokePaths.push(path);
});
// Fade in white fill when text starts appearing (at 4.3s)
setTimeout(() => {
paths.forEach(path => {
path.style.fillOpacity = '0.7';
});
}, 4300);
}
// Append SVG to shape div
shapeDiv.appendChild(svgElement);
this.container.appendChild(shapeDiv);
// Create FloatingShape instance (only for shapes with physics, not filled shapes)
if (!config.isFilled) {
const floatingShape = new FloatingShape(shapeDiv, x, y, width, height);
this.shapes.push(floatingShape);
}
} catch (error) {
console.error(`Error loading SVG for ${config.id}:`, error);
}
}
createThalimText(centerX, centerY) {
// Create container for the text
const textContainer = document.createElement('div');
textContainer.className = 'thalim-text';
textContainer.style.top = '-20px';
textContainer.style.left = '10px';
textContainer.style.transform = `translate3d(${centerX}px, ${centerY}px, 0) translate(-50%, -50%)`; // Center the text
textContainer.style.color = '#000000cc';
// Create individual letter spans
const letters = 'thalim'.split('');
letters.forEach((letter, index) => {
const span = document.createElement('span');
span.textContent = letter;
span.style.opacity = '0';
span.style.animation = `letterAppear 0.8s ease-in-out ${4.2 + index * 0.1}s forwards`;
textContainer.appendChild(span);
});
// Mark text as ready after last letter finishes animating
// Last letter: 4.2s + 0.5s (6th letter) + 0.8s (duration) = 5.5s
setTimeout(() => {
this.textReady = true;
}, 5500);
this.container.appendChild(textContainer);
this.thalimText = textContainer;
}
handleMouseMove(e) {
// Ignore mouse events during touch interactions (prevents interference from synthetic mouse events)
if (this.isTouching) {
return;
}
const rect = this.container.getBoundingClientRect();
this.mouseX = e.clientX - rect.left;
this.mouseY = e.clientY - rect.top;
// Fade out fillshape and text on mouse move (only if they've finished appearing)
if (this.fillShape && this.fillShapeReady) {
this.fillShape.style.opacity = '0';
}
if (this.thalimText && this.textReady) {
this.thalimText.style.opacity = '0';
}
// Fade out white fills on stroke shapes
this.strokePaths.forEach(path => {
path.style.fillOpacity = '0';
});
}
handleMouseLeave() {
// Move mouse far away to trigger return animation
this.mouseX = -10000;
this.mouseY = -10000;
// Fade fillshape and text back in after delay (let strokes settle first)
// Only if they have completed their initial animations
if (this.fillShape && this.fillShapeReady) {
setTimeout(() => {
this.fillShape.style.opacity = this.fillShapeOpacity;
}, 800); // Delay to allow strokes to return to position
}
if (this.thalimText && this.textReady) {
setTimeout(() => {
this.thalimText.style.opacity = '1';
}, 800);
}
// Fade white fills back in on stroke shapes
setTimeout(() => {
this.strokePaths.forEach(path => {
path.style.fillOpacity = '0.7';
});
}, 800);
}
handleTouchStart() {
// Mark that we're in a touch interaction to prevent mouse event interference
this.isTouching = true;
}
handleTouchMove(e) {
e.preventDefault(); // Prevent scrolling while interacting with sketch
const rect = this.container.getBoundingClientRect();
const touch = e.touches[0];
this.mouseX = touch.clientX - rect.left;
this.mouseY = touch.clientY - rect.top;
// Fade out fillshape and text on touch move (only if they've finished appearing)
if (this.fillShape && this.fillShapeReady) {
this.fillShape.style.opacity = '0';
}
if (this.thalimText && this.textReady) {
this.thalimText.style.opacity = '0';
}
// Fade out white fills on stroke shapes
this.strokePaths.forEach(path => {
path.style.fillOpacity = '0';
});
}
handleTouchEnd() {
// Reset shapes to original position
this.handleMouseLeave();
// Clear touch flag after a delay to ignore synthetic mouse events
setTimeout(() => {
this.isTouching = false;
}, 500);
}
handleResize() {
// Don't refresh on mobile (prevents refresh during scroll when browser bar appears/disappears)
if (this.isMobile()) {
// Still update margin-top on mobile if needed
this.setSketchMarginOnMobile();
return;
}
// Debounced resize: only recreate after user stops resizing for 250ms
// This prevents page crashes from rapid resize events
if (this.resizeTimeout) {
clearTimeout(this.resizeTimeout);
}
this.resizeTimeout = setTimeout(() => {
// Stop animation loop before destroying
if (this.animationId) {
cancelAnimationFrame(this.animationId);
this.animationId = null;
}
// Clear shapes and container
this.shapes = [];
this.container.innerHTML = '';
// Reinitialize
this.init();
}, 250);
}
animate() {
// Update all shapes
this.shapes.forEach(shape => {
shape.update(this.mouseX, this.mouseY);
shape.render();
});
// Continue loop
this.animationId = requestAnimationFrame(this.animate.bind(this));
}
destroy() {
if (this.animationId) {
cancelAnimationFrame(this.animationId);
}
if (this.resizeTimeout) {
clearTimeout(this.resizeTimeout);
}
this.container.removeEventListener('mousemove', this.handleMouseMove);
this.container.removeEventListener('mouseleave', this.handleMouseLeave);
this.container.removeEventListener('touchstart', this.handleTouchStart);
this.container.removeEventListener('touchmove', this.handleTouchMove);
this.container.removeEventListener('touchend', this.handleTouchEnd);
window.removeEventListener('resize', this.handleResize);
}
}
// Inject CSS for stroke and fade animations
const style = document.createElement('style');
style.textContent = `
@keyframes drawStroke {
0% {
stroke-dashoffset: var(--path-length);
opacity: 0;
}
10% {
opacity: 1;
}
100% {
stroke-dashoffset: 0;
opacity: 1;
}
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: var(--target-opacity);
}
}
@keyframes letterAppear {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
`;
document.head.appendChild(style);
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
window.floatingShapes = new FloatingShapesManager('sketch');
});
} else {
window.floatingShapes = new FloatingShapesManager('sketch');
}