update home,about,solutions
This commit is contained in:
226
src/components/sections/IndustryWorldMap.tsx
Normal file
226
src/components/sections/IndustryWorldMap.tsx
Normal file
@@ -0,0 +1,226 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef } from "react";
|
||||
|
||||
/**
|
||||
* Animated dotted world map (Canvas 2D) for the Solutions industry section.
|
||||
* - Continents drawn as a grid of dots clipped to rough continent polygons.
|
||||
* - Major logistics hub cities with continuous double red pulse rings.
|
||||
* - Red glowing packets travelling along arced quadratic-bezier routes.
|
||||
* - Low-opacity dashed connection lines between hubs.
|
||||
* Continent / city coordinates are normalized (0..1): x west→east, y north→south.
|
||||
*/
|
||||
|
||||
const CONTINENTS: number[][][] = [
|
||||
// North America
|
||||
[[0.04,0.20],[0.10,0.12],[0.18,0.10],[0.24,0.13],[0.29,0.12],[0.30,0.18],
|
||||
[0.27,0.22],[0.26,0.28],[0.22,0.30],[0.20,0.38],[0.17,0.44],[0.15,0.40],
|
||||
[0.16,0.32],[0.12,0.30],[0.09,0.26],[0.06,0.24]],
|
||||
// South America
|
||||
[[0.21,0.50],[0.27,0.48],[0.31,0.52],[0.31,0.60],[0.29,0.66],[0.27,0.74],
|
||||
[0.24,0.82],[0.22,0.80],[0.22,0.70],[0.205,0.62],[0.20,0.55]],
|
||||
// Europe
|
||||
[[0.45,0.16],[0.50,0.13],[0.55,0.15],[0.57,0.19],[0.55,0.24],[0.50,0.27],
|
||||
[0.47,0.25],[0.455,0.20]],
|
||||
// Africa
|
||||
[[0.46,0.34],[0.53,0.32],[0.58,0.36],[0.585,0.44],[0.56,0.52],[0.53,0.60],
|
||||
[0.50,0.66],[0.47,0.62],[0.46,0.52],[0.45,0.44],[0.45,0.38]],
|
||||
// Asia
|
||||
[[0.56,0.14],[0.64,0.10],[0.74,0.10],[0.84,0.14],[0.90,0.20],[0.92,0.26],
|
||||
[0.86,0.30],[0.80,0.30],[0.74,0.34],[0.70,0.34],[0.66,0.30],[0.60,0.30],
|
||||
[0.575,0.24],[0.565,0.18]],
|
||||
// Australia
|
||||
[[0.81,0.66],[0.87,0.64],[0.92,0.68],[0.92,0.74],[0.86,0.77],[0.81,0.74],[0.80,0.70]],
|
||||
];
|
||||
|
||||
const CITIES: [number, number][] = [
|
||||
[0.115, 0.30], // 0 Los Angeles
|
||||
[0.265, 0.255],// 1 New York
|
||||
[0.285, 0.66], // 2 São Paulo
|
||||
[0.475, 0.185],// 3 London
|
||||
[0.605, 0.345],// 4 Dubai
|
||||
[0.655, 0.40], // 5 Mumbai
|
||||
[0.745, 0.50], // 6 Singapore
|
||||
[0.815, 0.275],// 7 Shanghai
|
||||
[0.865, 0.715],// 8 Sydney
|
||||
];
|
||||
|
||||
const ROUTES: [number, number][] = [
|
||||
[0, 1], [1, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 8], [1, 2], [3, 7], [0, 7],
|
||||
];
|
||||
|
||||
function pointInPoly(x: number, y: number, poly: number[][]) {
|
||||
let inside = false;
|
||||
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
|
||||
const xi = poly[i][0], yi = poly[i][1];
|
||||
const xj = poly[j][0], yj = poly[j][1];
|
||||
const intersect = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
|
||||
if (intersect) inside = !inside;
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
export default function IndustryWorldMap() {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
const parent = canvas?.parentElement;
|
||||
if (!canvas || !parent) return;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
||||
let w = 0, h = 0;
|
||||
let dots: { x: number; y: number }[] = [];
|
||||
let raf = 0;
|
||||
let startTs = 0;
|
||||
|
||||
const buildDots = () => {
|
||||
dots = [];
|
||||
const gap = Math.max(11, Math.min(17, w / 70));
|
||||
for (let gx = gap / 2; gx < w; gx += gap) {
|
||||
for (let gy = gap / 2; gy < h; gy += gap) {
|
||||
const nx = gx / w, ny = gy / h;
|
||||
for (const poly of CONTINENTS) {
|
||||
if (pointInPoly(nx, ny, poly)) { dots.push({ x: gx, y: gy }); break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const resize = () => {
|
||||
const rect = parent.getBoundingClientRect();
|
||||
w = Math.max(1, rect.width);
|
||||
h = Math.max(1, rect.height);
|
||||
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||
canvas.width = Math.round(w * dpr);
|
||||
canvas.height = Math.round(h * dpr);
|
||||
canvas.style.width = w + "px";
|
||||
canvas.style.height = h + "px";
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
buildDots();
|
||||
};
|
||||
|
||||
const cityPx = () => CITIES.map(([cx, cy]) => ({ x: cx * w, y: cy * h }));
|
||||
|
||||
const ctrl = (p0: { x: number; y: number }, p1: { x: number; y: number }) => {
|
||||
const mx = (p0.x + p1.x) / 2, my = (p0.y + p1.y) / 2;
|
||||
const lift = Math.hypot(p1.x - p0.x, p1.y - p0.y) * 0.28;
|
||||
return { x: mx, y: my - lift };
|
||||
};
|
||||
const bezier = (p0: any, c: any, p1: any, t: number) => {
|
||||
const u = 1 - t;
|
||||
return {
|
||||
x: u * u * p0.x + 2 * u * t * c.x + t * t * p1.x,
|
||||
y: u * u * p0.y + 2 * u * t * c.y + t * t * p1.y,
|
||||
};
|
||||
};
|
||||
|
||||
const draw = (time: number) => {
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
// Continent dots
|
||||
ctx.fillStyle = "rgba(120,122,130,0.55)";
|
||||
for (const d of dots) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(d.x, d.y, 1.15, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
const cs = cityPx();
|
||||
|
||||
// Dashed connection lines (low opacity)
|
||||
ctx.save();
|
||||
ctx.setLineDash([4, 7]);
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeStyle = "rgba(239,68,68,0.13)";
|
||||
for (const [a, b] of ROUTES) {
|
||||
const c = ctrl(cs[a], cs[b]);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(cs[a].x, cs[a].y);
|
||||
ctx.quadraticCurveTo(c.x, c.y, cs[b].x, cs[b].y);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
// Travelling red glowing packets
|
||||
ctx.save();
|
||||
for (let r = 0; r < ROUTES.length; r++) {
|
||||
const [a, b] = ROUTES[r];
|
||||
const c = ctrl(cs[a], cs[b]);
|
||||
const t = ((time * 0.11 + r * 0.137) % 1 + 1) % 1;
|
||||
const p = bezier(cs[a], c, cs[b], t);
|
||||
// soft trail
|
||||
const tt = Math.max(0, t - 0.04);
|
||||
const pt = bezier(cs[a], c, cs[b], tt);
|
||||
const grad = ctx.createLinearGradient(pt.x, pt.y, p.x, p.y);
|
||||
grad.addColorStop(0, "rgba(239,68,68,0)");
|
||||
grad.addColorStop(1, "rgba(239,68,68,0.5)");
|
||||
ctx.strokeStyle = grad;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(pt.x, pt.y);
|
||||
ctx.lineTo(p.x, p.y);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.shadowColor = "#ef4444";
|
||||
ctx.shadowBlur = 12;
|
||||
ctx.fillStyle = "#ef4444";
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, 2.6, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
// City hub nodes + double pulse rings
|
||||
for (const c of cs) {
|
||||
for (let k = 0; k < 2; k++) {
|
||||
const period = 2.6;
|
||||
const phase = (((time + (k * period) / 2) % period) + period) % period / period;
|
||||
const radius = 3 + phase * 24;
|
||||
const alpha = (1 - phase) * 0.45;
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = `rgba(239,68,68,${alpha})`;
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.arc(c.x, c.y, radius, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.fillStyle = "#ef4444";
|
||||
ctx.shadowColor = "#ef4444";
|
||||
ctx.shadowBlur = 8;
|
||||
ctx.beginPath();
|
||||
ctx.arc(c.x, c.y, 2.6, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
};
|
||||
|
||||
const loop = (ts: number) => {
|
||||
if (!startTs) startTs = ts;
|
||||
draw((ts - startTs) / 1000);
|
||||
raf = requestAnimationFrame(loop);
|
||||
};
|
||||
|
||||
resize();
|
||||
if (reduced) {
|
||||
draw(0);
|
||||
} else {
|
||||
raf = requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
const ro = new ResizeObserver(() => {
|
||||
resize();
|
||||
if (reduced) draw(0);
|
||||
});
|
||||
ro.observe(parent);
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(raf);
|
||||
ro.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <canvas ref={canvasRef} className="ind__map" aria-hidden="true" />;
|
||||
}
|
||||
Reference in New Issue
Block a user