<section class="supt-section-world">
    <div class="supt-section-world__wrapper">
        <span class="supt-section-world__line-wrapper">
            <span class="supt-section-world__line">
                <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 130 389" fill="none" class="supt-section-world__line__glow">
                    <g filter="url(#supt-innovation-shared-line-glow-effect)">
                        <path d="M65 0L65 325" stroke="white" stroke-width="2" vector-effect="non-scaling-stroke" />
                    </g>
                </svg>
            </span>
        </span>
        <div class="supt-section-world__inner">
            <div class="container">
                <div class="row justify-content-center justify-content-md-between align-items-center">
                    <div class="col-12 col-md-5 col-lg-3">
                        <div class="supt-section-world__content">
                            <p class="supt-section-world__text">
                                In 2024, we managed over 85 million "Internet of things" devices, operating 24/7, generating more than 1.4 trillion signals.
                            </p>
                        </div>
                    </div>
                    <div class="col-12 col-md-7 col-lg-6 supt-section-world__world-col">
                        <div class="supt-section-world__world-wrapper">
                            <div class="supt-section-world__world">
                                <div class="supt-section-world__world__inner">
                                    <svg width="729" height="729" viewbox="0 0 729 729" fill="none" xmlns="http://www.w3.org/2000/svg" class="supt-section-world__world__line">
                                        <g filter=" url(#supt-innovation-shared-glow-effect)">
                                            <circle cx="364.5" cy="364.5" r="299.5" stroke="white" stroke-width="2" shape-rendering="crispEdges" vector-effect="non-scaling-stroke" />
                                        </g>
                                    </svg>

                                    <canvas id="canvas"></canvas>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
</section>

No notes defined.

<section class="supt-section-world">
	<div class="supt-section-world__wrapper">
		<span class="supt-section-world__line-wrapper">
			<span class="supt-section-world__line">
				<svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 130 389" fill="none" class="supt-section-world__line__glow">
					<g filter="url(#supt-innovation-shared-line-glow-effect)">
						<path d="M65 0L65 325" stroke="white" stroke-width="2" vector-effect="non-scaling-stroke"/>
					</g>
				</svg>
			</span>
		</span>
		<div class="supt-section-world__inner">
			<div class="container">
				<div class="row justify-content-center justify-content-md-between align-items-center">
					<div class="col-12 col-md-5 col-lg-3">
						<div class="supt-section-world__content">
							<p class="supt-section-world__text">
								{{ text }}
							</p>
						</div>
					</div>
					<div class="col-12 col-md-7 col-lg-6 supt-section-world__world-col">
						<div class="supt-section-world__world-wrapper">
							<div class="supt-section-world__world">
								<div class="supt-section-world__world__inner">
									<svg width="729" height="729" viewbox="0 0 729 729" fill="none" xmlns="http://www.w3.org/2000/svg" class="supt-section-world__world__line">
										<g filter=" url(#supt-innovation-shared-glow-effect)">
											<circle cx="364.5" cy="364.5" r="299.5" stroke="white" stroke-width="2" shape-rendering="crispEdges" vector-effect="non-scaling-stroke"/>
										</g>
									</svg>

									<canvas id="canvas"></canvas>
								</div>
							</div>
						</div>
					</div>
				</div>
			</div>
		</div>
	</section>
{
  "text": "In 2024, we managed over 85 million \"Internet of things\" devices, operating 24/7, generating more than 1.4 trillion signals."
}
  • Content:
    interface Point3D {
    	x: number;
    	y: number;
    	z: number;
    }
    
    interface Point4D extends Point3D {
    	w: number;
    }
    
    interface WhitePoint {
    	x: number;
    	y: number;
    	displayChar: string;
    	isLandmass: boolean;
    	alpha: number;
    	isPolarRegion: boolean;
    	isExtremePolar: boolean;
    }
    
    class Object3D {
    	public nodes: Point4D[] = [];
    
    	addNodes(nodeArray: Point3D[]): void {
    		this.nodes = nodeArray.map(node => ({ ...node, w: 1 }));
    	}
    
    	findCentre(): Point4D {
    		if (this.nodes.length === 0) return { x: 0, y: 0, z: 0, w: 0 };
    		const sum = this.nodes.reduce(
    			(acc, node) => ({
    				x: acc.x + node.x,
    				y: acc.y + node.y,
    				z: acc.z + node.z,
    				w: acc.w + node.w,
    			}),
    			{ x: 0, y: 0, z: 0, w: 0 }
    		);
    		return {
    			x: sum.x / this.nodes.length,
    			y: sum.y / this.nodes.length,
    			z: sum.z / this.nodes.length,
    			w: sum.w / this.nodes.length,
    		};
    	}
    
    	rotate(center: Point4D, matrix: number[][]): void {
    		this.nodes = this.nodes.map(node => {
    			const translated = [
    				node.x - center.x,
    				node.y - center.y,
    				node.z - center.z,
    				node.w - center.w,
    			];
    			const rotated = [0, 0, 0, 0];
    			for (let i = 0; i < 4; i++) {
    				for (let j = 0; j < 4; j++) {
    					rotated[i] += matrix[i][j] * translated[j];
    				}
    			}
    			return {
    				x: rotated[0] + center.x,
    				y: rotated[1] + center.y,
    				z: rotated[2] + center.z,
    				w: rotated[3] + center.w,
    			};
    		});
    	}
    }
    
    class Projection {
    	public width: number;
    	public height: number;
    	public surfaces: { [key: string]: Object3D } = {};
    
    	constructor(width: number, height: number) {
    		this.width = width;
    		this.height = height;
    	}
    
    	addSurface(name: string, surface: Object3D): void {
    		this.surfaces[name] = surface;
    	}
    
    	display(
    		ctx: CanvasRenderingContext2D,
    		invertedAsciiChars: string[],
    		whiteDots: Map<number, number>,
    		MAP_WIDTH: number,
    		MAP_HEIGHT: number
    	): void {
    		// Clear canvas to be transparent so gradient background shows through
    		ctx.clearRect(0, 0, this.width, this.height);
    		for (let surface of Object.values(this.surfaces)) {
    			// Two-pass rendering: first regular points, then white points
    			const whitePointsToRender: WhitePoint[] = [];
    
    			let i = 0;
    			for (let node of surface.nodes) {
    				// Use a small threshold instead of hard 0 cutoff and add fade
    				if (node.y > -50) {
    					// Allow slightly behind the sphere
    					// Calculate which row we're on in the original map
    					const row = Math.floor(i / (MAP_WIDTH + 1));
    					const col = i % (MAP_WIDTH + 1);
    
    					// More sophisticated polar region detection
    					const latitudeFromEquator = Math.abs(row - MAP_HEIGHT / 2);
    					const isPolarRegion = latitudeFromEquator > MAP_HEIGHT * 0.35; // Top and bottom 35%
    					const isExtremePolar = latitudeFromEquator > MAP_HEIGHT * 0.45; // Most extreme regions
    
    					// Skip some dots in extreme polar regions to reduce crowding
    					const shouldSkipDot = isExtremePolar && col % 3 !== 0; // Only show every 3rd dot in extreme polar
    
    					if (shouldSkipDot) {
    						i++;
    						continue;
    					}
    
    					// Replace '+' with '●' for landmasses and '*' with bigger dots for special regions
    					const char = invertedAsciiChars[i];
    
    					// Safety check - skip if character is undefined or if we're out of bounds
    					if (char === undefined || i >= invertedAsciiChars.length) {
    						i++;
    						continue;
    					}
    
    					let displayChar: string, isLandmass: boolean, isSpecialRegion: boolean;
    
    					if (char === '•') {
    						displayChar = '●';
    						isLandmass = true;
    						isSpecialRegion = false;
    					} else if (char === '○') {
    						displayChar = '●';
    						isLandmass = true; // Same size as normal landmasses
    						isSpecialRegion = true; // But mark as special region for white dots
    					} else {
    						displayChar = char;
    						isLandmass = false;
    						isSpecialRegion = false;
    					}
    
    					// Check if this dot should be white
    					let shouldBeWhite = whiteDots.has(i);
    
    					// Store white points for second pass rendering
    					if (shouldBeWhite) {
    						// Apply fade for white points only when approaching edge
    						let whiteAlpha = 1.0;
    						if (node.y <= 50) {
    							whiteAlpha = Math.max(0, node.y / 50);
    						}
    
    						whitePointsToRender.push({
    							x: this.width / 2 + node.x,
    							y: this.height / 2 + node.z,
    							displayChar: displayChar,
    							isLandmass: isLandmass,
    							alpha: whiteAlpha,
    							isPolarRegion: isPolarRegion,
    							isExtremePolar: isExtremePolar,
    						});
    						i++;
    						continue;
    					}
    
    					// Regular points - no fade, no alpha adjustment
    					ctx.globalAlpha = 1.0;
    					ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
    
    					// Adjust font size based on position and screen size - responsive dots
    					const screenScale = Math.min(this.width, this.height) / 1080; // Scale factor based on screen size (1080 as base)
    
    					// More granular size adjustments based on latitude
    					let baseLandmassSize: number, baseWaterSize: number;
    					if (isExtremePolar) {
    						baseLandmassSize = 16; // Increased for better visibility
    						baseWaterSize = 22; // Increased for better visibility
    					} else if (isPolarRegion) {
    						baseLandmassSize = 20; // Increased for better visibility
    						baseWaterSize = 28; // Increased for better visibility
    					} else {
    						baseLandmassSize = 24; // Increased for better visibility
    						baseWaterSize = 36; // Increased for better visibility
    					}
    
    					let landmassSize = Math.max(6, baseLandmassSize * screenScale); // Minimum 6px for extreme polar
    					let waterSize = Math.max(10, baseWaterSize * screenScale); // Minimum 10px for extreme polar
    
    					const fontSize = isLandmass ? landmassSize : waterSize;
    
    					ctx.font = `${fontSize}px "Courier New", monospace`;
    
    					// Set text alignment for perfect centering both horizontally and vertically
    					ctx.textAlign = 'center';
    					ctx.textBaseline = 'alphabetic';
    
    					// Calculate consistent vertical offset for all dots to align on same baseline
    					const baselineOffset = fontSize * 0.35; // Consistent offset for all font sizes
    					ctx.fillText(
    						displayChar,
    						this.width / 2 + node.x,
    						this.height / 2 + node.z + baselineOffset
    					);
    				}
    				i++;
    			}
    
    			// Second pass: render white points on top so they can't be overlapped
    			for (let whitePoint of whitePointsToRender) {
    				ctx.globalAlpha = whitePoint.alpha;
    
    				// Use same sizing logic as regular points - with polar adjustments
    				const screenScale = Math.min(this.width, this.height) / 1080;
    
    				// Apply same granular size adjustments based on latitude
    				let baseLandmassSize: number, baseWaterSize: number;
    				if (whitePoint.isExtremePolar) {
    					baseLandmassSize = 16; // Increased for better visibility
    					baseWaterSize = 22; // Increased for better visibility
    				} else if (whitePoint.isPolarRegion) {
    					baseLandmassSize = 20; // Increased for better visibility
    					baseWaterSize = 28; // Increased for better visibility
    				} else {
    					baseLandmassSize = 24; // Increased for better visibility
    					baseWaterSize = 36; // Increased for better visibility
    				}
    
    				let landmassSize = Math.max(6, baseLandmassSize * screenScale); // Minimum 6px for extreme polar
    				let waterSize = Math.max(10, baseWaterSize * screenScale); // Minimum 10px for extreme polar
    
    				const fontSize = whitePoint.isLandmass ? landmassSize : waterSize;
    
    				ctx.font = `${fontSize}px "Courier New", monospace`;
    				ctx.textAlign = 'center';
    				ctx.textBaseline = 'alphabetic';
    
    				// Set up blur shadow
    				ctx.shadowColor = '#ED002F';
    				ctx.shadowBlur = 8;
    				ctx.shadowOffsetX = 0;
    				ctx.shadowOffsetY = 0;
    
    				// Calculate consistent vertical offset for all dots to align on same baseline
    				const baselineOffset = fontSize * 0.35; // Consistent offset for all font sizes
    
    				// Draw white text with shadow
    				ctx.fillStyle = '#FFFFFF';
    				ctx.fillText(whitePoint.displayChar, whitePoint.x, whitePoint.y + baselineOffset);
    
    				// Reset shadow for next iteration
    				ctx.shadowColor = 'transparent';
    				ctx.shadowBlur = 0;
    				ctx.shadowOffsetX = 0;
    				ctx.shadowOffsetY = 0;
    			}
    		}
    		// Reset alpha
    		ctx.globalAlpha = 1.0;
    	}
    
    	rotateAll(theta: number): void {
    		for (let surface of Object.values(this.surfaces)) {
    			const center = surface.findCentre();
    			const c = Math.cos(theta);
    			const s = Math.sin(theta);
    			const matrix = [
    				[c, -s, 0, 0],
    				[s, c, 0, 0],
    				[0, 0, 1, 0],
    				[0, 0, 0, 1],
    			];
    			surface.rotate(center, matrix);
    		}
    	}
    }
    
    export class World {
    	private canvas: HTMLCanvasElement | null = null;
    	private ctx: CanvasRenderingContext2D | null = null;
    	private readonly FPS = 30;
    	private width = 0;
    	private height = 0;
    	private R = 0;
    	private readonly MAP_WIDTH = 139;
    	private readonly MAP_HEIGHT = 34;
    	private xyz: Point3D[] = [];
    	private whiteDots = new Map<number, number>();
    	private readonly MIN_WHITE_DOTS = 55;
    	private groupCooldown = 0;
    	private asciiChars: string[] = [];
    	private invertedAsciiChars: string[] = [];
    	private initialDotsInitialized = false;
    	private spin = -3.2;
    	private lastTime = 0;
    	private autoRotate = true;
    	private manualRotationSpeed = 0;
    	private isDragging = false;
    	private lastMouseX = 0;
    	private mouseIdleTimer = 0;
    	private readonly MOUSE_IDLE_TIMEOUT = 90;
    
    	constructor(canvas: HTMLCanvasElement) {
    		this.canvas = canvas;
    
    		this.ctx = this.canvas.getContext('2d');
    		if (!this.ctx) {
    			throw new Error('Could not get 2D context from canvas');
    		}
    
    		this.init();
    	}
    
    	private init(): void {
    		this.setCanvasSize();
    		this.setupEventListeners();
    		this.loadMapData();
    		this.xyz = this.generateGlobeNodes();
    		this.animate(0);
    	}
    
    	private setCanvasSize(): { width: number; height: number } {
    		if (!this.canvas) return { width: 0, height: 0 };
    
    		const containerWidth = window.innerWidth;
    		const containerHeight = window.innerHeight;
    
    		// Create a perfect 1:1 aspect ratio canvas (square)
    		const size = Math.min(containerWidth, containerHeight);
    		this.canvas.width = size;
    		this.canvas.height = size;
    
    		this.width = size;
    		this.height = size;
    		// Globe radius for perfect circular globe in square canvas
    		this.R = size * 0.48; // 48% of the square canvas size for 96% coverage
    
    		return { width: size, height: size };
    	}
    
    	private generateGlobeNodes(): Point3D[] {
    		const xyz: Point3D[] = [];
    		for (let i = 0; i <= this.MAP_HEIGHT; i++) {
    			const lat = (Math.PI / this.MAP_HEIGHT) * i;
    			for (let j = 0; j <= this.MAP_WIDTH; j++) {
    				const lon = ((2 * Math.PI) / this.MAP_WIDTH) * j;
    				const x = Number((this.R * Math.sin(lat) * Math.cos(lon)).toFixed(2));
    				const y = Number((this.R * Math.sin(lat) * Math.sin(lon)).toFixed(2));
    				const z = Number((this.R * Math.cos(lat)).toFixed(2));
    				xyz.push({ x, y, z });
    			}
    		}
    		return xyz;
    	}
    
    	private setupEventListeners(): void {
    		if (!this.canvas) return;
    
    		// Update canvas size on window resize
    		window.addEventListener('resize', () => {
    			this.setCanvasSize();
    			this.xyz = this.generateGlobeNodes();
    		});
    
    		// Mouse event listeners for globe control
    		this.canvas.addEventListener('mousedown', (event: MouseEvent) => {
    			this.isDragging = true;
    			this.autoRotate = false;
    			this.lastMouseX = event.clientX;
    			this.mouseIdleTimer = 0;
    			event.preventDefault();
    		});
    
    		this.canvas.addEventListener('mousemove', (event: MouseEvent) => {
    			this.mouseIdleTimer = 0;
    
    			if (this.isDragging) {
    				const deltaX = event.clientX - this.lastMouseX;
    				this.manualRotationSpeed = -deltaX * 0.005;
    				this.lastMouseX = event.clientX;
    				this.autoRotate = false;
    			}
    		});
    
    		this.canvas.addEventListener('mouseup', () => {
    			this.isDragging = false;
    		});
    
    		this.canvas.addEventListener('mouseleave', () => {
    			this.isDragging = false;
    		});
    
    		this.canvas.addEventListener('click', () => {
    			if (!this.isDragging) {
    				this.autoRotate = !this.autoRotate;
    				if (this.autoRotate) {
    					this.manualRotationSpeed = 0;
    				}
    			}
    		});
    	}
    
    	private loadMapData(): void {
    		// Initialize with fallback data
    		this.asciiChars = Array((this.MAP_WIDTH + 1) * (this.MAP_HEIGHT + 1)).fill('.');
    		this.invertedAsciiChars = [...this.asciiChars].reverse();
    
    		// Load the actual map data in the background
    		fetch('/sites/gv/files/flmngr/superhuit/innovation/planet.txt')
    			.then(response => response.text())
    			.then(data => {
    				const lines = data.split('\n').filter(line => line.length > 0);
    				const actualHeight = lines.length;
    				const actualWidth = lines[0] ? lines[0].length : 140;
    
    				console.log(
    					`File dimensions: ${actualWidth} x ${actualHeight}, Expected: ${
    						this.MAP_WIDTH + 1
    					} x ${this.MAP_HEIGHT + 1}`
    				);
    
    				this.asciiChars = data.replace(/\n/g, '').split('');
    				this.invertedAsciiChars = [...this.asciiChars].reverse();
    
    				console.log(
    					`Total characters loaded: ${this.asciiChars.length}, Expected: ${
    						(this.MAP_WIDTH + 1) * (this.MAP_HEIGHT + 1)
    					}`
    				);
    			})
    			.catch(() => {
    				this.asciiChars = Array((this.MAP_WIDTH + 1) * (this.MAP_HEIGHT + 1)).fill('.');
    				this.invertedAsciiChars = [...this.asciiChars].reverse();
    			});
    	}
    
    	private createInitialWhiteDots(): void {
    		const specialRegionIndices: number[] = [];
    
    		// Find all "○" symbol positions
    		for (let i = 0; i < this.invertedAsciiChars.length; i++) {
    			if (this.invertedAsciiChars[i] === '○') {
    				specialRegionIndices.push(i);
    			}
    		}
    
    		// Create initial white dot groups
    		const numberOfGroups = Math.min(16, Math.floor(this.MIN_WHITE_DOTS / 3));
    
    		for (let group = 0; group < numberOfGroups; group++) {
    			if (specialRegionIndices.length > 0) {
    				let centerIndex: number, centerRow: number, centerCol: number;
    				let attempts = 0;
    				const maxAttempts = 20;
    
    				do {
    					centerIndex =
    						specialRegionIndices[Math.floor(Math.random() * specialRegionIndices.length)];
    					centerRow = Math.floor(centerIndex / (this.MAP_WIDTH + 1));
    					centerCol = centerIndex % (this.MAP_WIDTH + 1);
    
    					const hasNearbyWhiteDots = (): boolean => {
    						const checkRadius = 4;
    						for (
    							let checkRow = Math.max(0, centerRow - checkRadius);
    							checkRow <= Math.min(this.MAP_HEIGHT, centerRow + checkRadius);
    							checkRow++
    						) {
    							for (
    								let checkCol = Math.max(0, centerCol - checkRadius);
    								checkCol <= Math.min(this.MAP_WIDTH, centerCol + checkRadius);
    								checkCol++
    							) {
    								const checkIndex = checkRow * (this.MAP_WIDTH + 1) + checkCol;
    								if (this.whiteDots.has(checkIndex)) {
    									return true;
    								}
    							}
    						}
    						return false;
    					};
    
    					if (!hasNearbyWhiteDots()) {
    						break;
    					}
    
    					attempts++;
    				} while (attempts < maxAttempts);
    
    				let groupSize: number;
    				if (Math.random() < 0.1) {
    					groupSize = 1;
    				} else {
    					groupSize = Math.floor(Math.random() * 4) + 3;
    				}
    
    				const groupPositions: [number, number][] = [];
    
    				if (groupSize === 1) {
    					groupPositions.push([centerRow, centerCol]);
    				} else {
    					groupPositions.push([centerRow, centerCol]);
    
    					const adjacentOffsets: [number, number][] = [
    						[-1, -1],
    						[-1, 0],
    						[-1, 1],
    						[0, -1],
    						[0, 1],
    						[1, -1],
    						[1, 0],
    						[1, 1],
    					];
    
    					let addedDots = 1;
    					let offsetIndex = 0;
    
    					while (addedDots < groupSize && offsetIndex < adjacentOffsets.length) {
    						const [rowOffset, colOffset] = adjacentOffsets[offsetIndex];
    						const newRow = centerRow + rowOffset;
    						const newCol = centerCol + colOffset;
    
    						if (
    							newRow >= 0 &&
    							newRow <= this.MAP_HEIGHT &&
    							newCol >= 0 &&
    							newCol <= this.MAP_WIDTH
    						) {
    							const testIndex = newRow * (this.MAP_WIDTH + 1) + newCol;
    
    							if (this.invertedAsciiChars[testIndex] === '○') {
    								groupPositions.push([newRow, newCol]);
    								addedDots++;
    							}
    						}
    						offsetIndex++;
    					}
    				}
    
    				for (const [targetRow, targetCol] of groupPositions) {
    					const targetIndex = targetRow * (this.MAP_WIDTH + 1) + targetCol;
    
    					if (!this.whiteDots.has(targetIndex)) {
    						this.whiteDots.set(targetIndex, Math.floor(Math.random() * 50) + 80);
    					}
    				}
    
    				const indexToRemove = specialRegionIndices.indexOf(centerIndex);
    				if (indexToRemove > -1) {
    					specialRegionIndices.splice(indexToRemove, 1);
    				}
    			}
    		}
    
    		this.initialDotsInitialized = true;
    	}
    
    	private updateWhiteDots(globe: Object3D): void {
    		// Check if there are existing white dots nearby (spacing check)
    		const hasNearbyWhiteDots = (row: number, col: number): boolean => {
    			const checkRadius = 4;
    			for (
    				let checkRow = Math.max(0, row - checkRadius);
    				checkRow <= Math.min(this.MAP_HEIGHT, row + checkRadius);
    				checkRow++
    			) {
    				for (
    					let checkCol = Math.max(0, col - checkRadius);
    					checkCol <= Math.min(this.MAP_WIDTH, col + checkRadius);
    					checkCol++
    				) {
    					const checkIndex = checkRow * (this.MAP_WIDTH + 1) + checkCol;
    					if (this.whiteDots.has(checkIndex)) {
    						return true;
    					}
    				}
    			}
    			return false;
    		};
    
    		let i = 0;
    		for (let node of globe.nodes) {
    			const row = Math.floor(i / (this.MAP_WIDTH + 1));
    			const col = i % (this.MAP_WIDTH + 1);
    			const char = this.invertedAsciiChars[i];
    			const isSpecialRegion = char === '○';
    
    			// Only allow white dots on special regions with group timing control and spacing
    			if (
    				isSpecialRegion &&
    				!this.whiteDots.has(i) &&
    				node.y > 0 &&
    				this.groupCooldown <= 0 &&
    				this.whiteDots.size < 55 &&
    				!hasNearbyWhiteDots(row, col) &&
    				Math.random() < 0.045
    			) {
    				let groupSize: number;
    				if (Math.random() < 0.1) {
    					groupSize = 1;
    				} else {
    					groupSize = Math.floor(Math.random() * 4) + 3;
    				}
    				const duration = 60 + Math.random() * 40;
    
    				const groupPositions: [number, number][] = [];
    
    				if (groupSize === 1) {
    					groupPositions.push([row, col]);
    				} else {
    					groupPositions.push([row, col]);
    
    					const adjacentOffsets: [number, number][] = [
    						[-1, -1],
    						[-1, 0],
    						[-1, 1],
    						[0, -1],
    						[0, 1],
    						[1, -1],
    						[1, 0],
    						[1, 1],
    					];
    
    					let addedDots = 1;
    					let offsetIndex = 0;
    
    					while (addedDots < groupSize && offsetIndex < adjacentOffsets.length) {
    						const [rowOffset, colOffset] = adjacentOffsets[offsetIndex];
    						const newRow = row + rowOffset;
    						const newCol = col + colOffset;
    
    						if (
    							newRow >= 0 &&
    							newRow <= this.MAP_HEIGHT &&
    							newCol >= 0 &&
    							newCol <= this.MAP_WIDTH
    						) {
    							const testIndex = newRow * (this.MAP_WIDTH + 1) + newCol;
    
    							if (this.invertedAsciiChars[testIndex] === '○') {
    								groupPositions.push([newRow, newCol]);
    								addedDots++;
    							}
    						}
    						offsetIndex++;
    					}
    				}
    
    				for (const [groupRow, groupCol] of groupPositions) {
    					const groupIndex = groupRow * (this.MAP_WIDTH + 1) + groupCol;
    
    					if (!this.whiteDots.has(groupIndex)) {
    						this.whiteDots.set(groupIndex, duration);
    					}
    				}
    
    				this.groupCooldown = 2 + Math.random() * 6;
    			}
    
    			// Update white dot timers ONLY when fully visible
    			if (this.whiteDots.has(i) && node.y > 0) {
    				const remainingFrames = this.whiteDots.get(i)! - 1;
    				if (remainingFrames <= 0) {
    					this.whiteDots.delete(i);
    				} else {
    					this.whiteDots.set(i, remainingFrames);
    				}
    			}
    
    			i++;
    		}
    	}
    
    	private animate = (timestamp: number): void => {
    		if (!this.ctx) return;
    
    		if (timestamp - this.lastTime < 1000 / this.FPS) {
    			requestAnimationFrame(this.animate);
    			return;
    		}
    		this.lastTime = timestamp;
    
    		// Create initial white dots on first frame
    		if (!this.initialDotsInitialized) {
    			this.createInitialWhiteDots();
    		}
    
    		// Decrease group cooldown timer
    		if (this.groupCooldown > 0) {
    			this.groupCooldown--;
    		}
    
    		const pv = new Projection(this.width, this.height);
    		const globe = new Object3D();
    		globe.addNodes(this.xyz);
    		pv.addSurface('globe', globe);
    		pv.rotateAll(this.spin);
    
    		// Update white dots
    		this.updateWhiteDots(globe);
    
    		pv.display(this.ctx, this.invertedAsciiChars, this.whiteDots, this.MAP_WIDTH, this.MAP_HEIGHT);
    
    		// Use constant rotation speed
    		const constantSpeed = 0.025; // Fixed rotation speed
    
    		// Handle rotation based on mouse controls or auto-rotation
    		if (!this.isDragging && !this.autoRotate) {
    			this.mouseIdleTimer++;
    			this.manualRotationSpeed *= 0.95;
    
    			if (this.mouseIdleTimer >= this.MOUSE_IDLE_TIMEOUT) {
    				this.autoRotate = true;
    				this.manualRotationSpeed = 0;
    			}
    		}
    
    		if (this.autoRotate) {
    			this.spin += constantSpeed;
    		} else {
    			this.spin += this.manualRotationSpeed;
    		}
    
    		requestAnimationFrame(this.animate);
    	};
    
    	public destroy(): void {
    		// Clean up event listeners if needed
    		if (this.canvas) {
    			// Remove event listeners to prevent memory leaks
    			// Note: In a real implementation, you'd want to store references to the bound functions
    			// and remove them specifically
    		}
    	}
    }
    
  • URL: /components/raw/section-world/World.ts
  • Filesystem Path: src/components/organisms/02-innovation/section-world/World.ts
  • Size: 20.8 KB
  • Content:
    import { animate, createTimeline, onScroll } from 'animejs';
    
    import { CONTAINER_WIDTH } from '@/js/constants';
    import { Sizes } from '@/js/Sizes';
    import { World } from './World';
    
    const WORLD_GLOW_STROKE_DASHARRAY = 2070; // For window width of 1710px
    export class SectionWorld {
    	$element: HTMLElement;
    	$line: HTMLElement;
    	$lineGlow: HTMLElement;
    	$content: HTMLElement;
    	$worldInner: HTMLElement;
    	$world: HTMLElement;
    	$worldGlow: HTMLElement;
    	resizeObserver!: ResizeObserver;
    	worldGlowStrokeDashoffset: number = 0;
    	worldCenterTranslation: { x: number; y: number } = { x: 0, y: 0 };
    	sizes: Sizes;
    
    	constructor($element: HTMLElement) {
    		this.$element = $element;
    		this.$line = $element.querySelector('.supt-section-world__line') as HTMLElement;
    		this.$lineGlow = $element.querySelector('.supt-section-world__line__glow') as HTMLElement;
    		this.$content = $element.querySelector('.supt-section-world__content') as HTMLElement;
    		this.$world = $element.querySelector('.supt-section-world__world') as HTMLElement;
    		this.$worldInner = this.$world.querySelector(
    			'.supt-section-world__world__inner'
    		) as HTMLElement;
    		this.$worldGlow = this.$world.querySelector('.supt-section-world__world__line') as HTMLElement;
    
    		this.sizes = new Sizes(); // Singleton
    
    		// Init
    		this.updateWorldSizes();
    
    		this.setupScrollAnimation();
    		this.setupResizeObserver();
    
    		// Init world
    		const $worldCanvas = this.$world.querySelector('canvas') as HTMLCanvasElement;
    		const world = new World($worldCanvas);
    	}
    
    	setupResizeObserver() {
    		this.resizeObserver = new ResizeObserver(entries => {
    			for (const entry of entries) {
    				if (entry.target === this.$world) {
    					this.updateWorldSizes();
    				}
    			}
    		});
    
    		this.resizeObserver.observe(this.$world);
    	}
    
    	/**
    	 * Move vertical line to the left to center it with the text column
    	 */
    	getLineTranslateX() {
    		const containerWidth =
    			this.sizes.currentWidth > CONTAINER_WIDTH ? CONTAINER_WIDTH : this.sizes.currentWidth;
    		const textCols = this.sizes.isDesktop ? 3 : 5;
    
    		return (
    			-window.innerWidth / 2 +
    			(window.innerWidth - containerWidth) / 2 +
    			(containerWidth * (textCols / 2)) / 12 +
    			50
    		);
    	}
    
    	/**
    	 * Calculate the translation needed to center the world horizontally on viewport center
    	 */
    	getWorldCenterTranslation() {
    		const $worldWrapper = this.$element.querySelector(
    			'.supt-section-world__world-wrapper'
    		) as HTMLElement;
    
    		const worldSize = $worldWrapper.getBoundingClientRect();
    		const worldCenterX = worldSize.left + worldSize.width / 2;
    		// const worldCenterY = worldSize.top + worldSize.height / 2;
    		const worldCenterY = $worldWrapper.offsetTop + worldSize.height / 2;
    
    		// Calculate viewport center
    		const viewportCenterX = window.innerWidth / 2;
    		const viewportCenterY = window.innerHeight / 2;
    
    		const worldCenterOnViewportX = viewportCenterX - worldCenterX;
    		const worldCenterOnViewportY = viewportCenterY - worldCenterY;
    
    		this.worldCenterTranslation = {
    			x: worldCenterOnViewportX,
    			y: worldCenterOnViewportY,
    		};
    	}
    
    	setupScrollAnimation() {
    		// Move 1st line to the left
    		if (this.sizes.isDesktop) {
    			const lineTranslateX = this.getLineTranslateX();
    
    			animate(this.$line, {
    				translateX: ['0%', `${lineTranslateX}px`],
    				ease: 'linear',
    				autoplay: onScroll({
    					container: document.body,
    					target: this.$element,
    					enter: 'bottom top',
    					leave: 'center top',
    					sync: true,
    				}),
    			});
    		}
    
    		// Draw 1st line glow
    		animate(this.$lineGlow, {
    			scaleY: [0, 1.2],
    			translateX: ['-50%', '-50%'],
    			ease: 'linear',
    			autoplay: onScroll({
    				container: document.body,
    				target: this.$element,
    				enter: 'bottom top',
    				leave: 'top top',
    				sync: true,
    			}),
    		});
    
    		// Update content color
    		animate(this.$content.children, {
    			color: '#fff',
    			duration: 300,
    			autoplay: onScroll({
    				container: document.body,
    				target: this.$element,
    				enter: 'top top',
    				sync: 'play play reverse reverse',
    			}),
    		});
    
    		// Zoom out world
    		animate(this.$world, {
    			scale: [1.8, 1],
    			ease: 'linear',
    			autoplay: onScroll({
    				container: document.body,
    				target: this.$element,
    				enter: 'top top',
    				leave: 'top 15%',
    				sync: true,
    			}),
    		});
    
    		// Move up line + content
    		animate([this.$line, this.$content], {
    			translateY: ['0%', '-50vh'],
    			ease: 'linear',
    			autoplay: onScroll({
    				container: document.body,
    				target: this.$element,
    				enter: 'top 40%',
    				leave: 'top 50%',
    				sync: true,
    			}),
    		});
    
    		// On mobile, move up the world at the same time as the line + content
    		if (!this.sizes.isDesktop) {
    			animate(this.$worldInner, {
    				translateY: ['0px', `${this.worldCenterTranslation.y}px`],
    				ease: 'linear',
    				autoplay: onScroll({
    					container: document.body,
    					target: this.$element,
    					enter: 'top 40%',
    					leave: 'top 50%',
    					sync: true,
    				}),
    			});
    		}
    
    		// Move world to center + draw world glow
    		createTimeline({
    			autoplay: onScroll({
    				container: document.body,
    				target: this.$element,
    				enter: 'top 50%',
    				leave: 'top 70%',
    				sync: true,
    			}),
    		})
    			// Move world to center
    			.add(
    				this.$worldInner,
    				{
    					translateX: ['0px', `${this.worldCenterTranslation.x}px`],
    					ease: 'linear',
    				},
    				0
    			)
    			// Draw world glow
    			.add(
    				this.$worldGlow,
    				{
    					// strokeDasharray: [this.worldGlowStrokeDashoffset, this.worldGlowStrokeDashoffset],
    					// strokeDashoffset: [this.worldGlowStrokeDashoffset, 0],
    					strokeDashoffset: 0,
    					ease: 'linear',
    				},
    				0
    			)
    			// Move world up + zoom out world
    			.add(this.$worldInner, {
    				scale: [1, 0.5],
    				ease: 'linear',
    			});
    	}
    
    	updateWorldSizes() {
    		// Set world height
    		this.$element.style.setProperty('--supt-section-world-height', `${this.$world.offsetHeight}px`);
    
    		// Calculate world glow stroke dashoffset
    		this.worldGlowStrokeDashoffset = WORLD_GLOW_STROKE_DASHARRAY * (this.$world.offsetWidth / 1710);
    		this.$element.style.setProperty(
    			'--supt-section-world-glow-dasharray',
    			`${this.worldGlowStrokeDashoffset}px`
    		);
    
    		this.getWorldCenterTranslation();
    	}
    
    	destroy() {
    		if (this.resizeObserver) {
    			this.resizeObserver.disconnect();
    		}
    	}
    }
    
  • URL: /components/raw/section-world/index.ts
  • Filesystem Path: src/components/organisms/02-innovation/section-world/index.ts
  • Size: 6.2 KB
  • Content:
    .supt-section-world {
    	height: 400vh;
    
    	&__wrapper {
    		height: 100vh;
    		position: sticky;
    		top: 0;
    
    		/* height * 0.5 (which is the scale at the end) / 2 (to have the half of it) */
    		margin-bottom: calc(-50vh + (var(--supt-section-world-height) * 0.51) / 2);
    
    		overflow: hidden;
    	}
    
    	&__line-wrapper {
    		position: absolute;
    		inset: 0;
    		display: flex;
    		justify-content: center;
    	}
    
    	&__line {
    		position: relative;
    		display: block;
    		width: 1px;
    		height: 25%;
    		background-color: $color-new;
    		transform-origin: top;
    
    		@media (min-width: $breakpoint-md) {
    			height: 40%;
    		}
    
    		&__glow {
    			position: absolute;
    			top: 0;
    			left: calc(-50% + 1px);
    			width: auto;
    			height: 100%;
    			transform-origin: top;
    			transform: scaleY(0);
    			/* transform: scaleY(1.2) translateX(-50%);
    			stroke-dasharray: 500; */
    		}
    	}
    
    	&__inner {
    		height: 100%;
    		display: flex;
    		padding-top: 25vh;
    		@media (min-width: $breakpoint-md) {
    			align-items: center;
    			padding-top: 0;
    		}
    	}
    
    	&__content {
    		position: relative;
    		padding: 52px 0;
    	}
    	&__text {
    		@extend %t-body-sm;
    		color: $color-new;
    	}
    
    	&__world-col {
    		position: static;
    		@media (min-width: $breakpoint-md) {
    			position: relative;
    		}
    	}
    
    	&__world-wrapper {
    		position: absolute;
    		left: var(--supt-container-padding-inline);
    		right: var(--supt-container-padding-inline);
    		top: 45%;
    
    		@media (min-width: $breakpoint-md) {
    			left: 0;
    			right: 0;
    			top: 50%;
    			transform: translateY(-50%);
    		}
    	}
    
    	&__world {
    		transform-origin: 50% -50%;
    		@media (min-width: $breakpoint-md) {
    			transform-origin: 0% -50%;
    		}
    
    		&__line {
    			--supt-section-world-glow-dasharray: 2070;
    
    			position: absolute;
    			top: 50%;
    			left: 50%;
    			width: 125%;
    			height: 125%;
    			transform: translate(-50%, -50%) rotate(-90deg);
    
    			stroke-dasharray: var(--supt-section-world-glow-dasharray);
    			stroke-dashoffset: var(--supt-section-world-glow-dasharray);
    		}
    
    		canvas {
    			display: block;
    			width: 100%;
    			height: 100%;
    			aspect-ratio: 1 / 1;
    			cursor: grab;
    
    			position: relative;
    			z-index: 1;
    
    			&:active {
    				cursor: grabbing;
    			}
    		}
    	}
    }
    
  • URL: /components/raw/section-world/section-world.css
  • Filesystem Path: src/components/organisms/02-innovation/section-world/section-world.css
  • Size: 2.1 KB
  • Handle: @section-world
  • Preview:
  • Filesystem Path: src/components/organisms/02-innovation/section-world/section-world.twig