<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."
}
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
}
}
}
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();
}
}
}
.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;
}
}
}
}