/* Herb Immortal — visual components: animated globe, botanical illustrations, image placeholders */
// ======================== ANIMATED GLOBE ========================
const AnimatedGlobe = () => {
const canvasRef = React.useRef(null);
React.useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
const dpr = window.devicePixelRatio || 1;
const size = 600;
canvas.width = size * dpr;
canvas.height = size * dpr;
ctx.scale(dpr, dpr);
const cx = size / 2, cy = size / 2, r = size * 0.42;
const pins = [
{ lat: 0.32, lng: 0.2 }, // India
{ lat: 0.45, lng: 0.55 }, // China
{ lat: 0.6, lng: 0.7 }, // SE Asia
{ lat: 0.35, lng: 0.85 }, // Japan
{ lat: 0.7, lng: 0.3 }, // Africa
{ lat: 0.25, lng: -0.3 }, // Toronto
];
let frame;
const accent = getComputedStyle(document.documentElement).getPropertyValue("--accent-deep").trim() || "#2F3F2C";
const lineCol = getComputedStyle(document.documentElement).getPropertyValue("--line").trim() || "rgba(20,24,16,0.1)";
function draw(t) {
ctx.clearRect(0, 0, size, size);
const rot = t * 0.00008;
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.strokeStyle = lineCol;
ctx.lineWidth = 1;
ctx.stroke();
for (let i = -2; i <= 2; i++) {
const y = cy + (i / 3) * r;
const rx = Math.sqrt(r * r - (i / 3 * r) ** 2);
ctx.beginPath();
ctx.ellipse(cx, y, rx, rx * 0.06, 0, 0, Math.PI * 2);
ctx.strokeStyle = lineCol;
ctx.lineWidth = 0.6;
ctx.stroke();
}
for (let i = 0; i < 5; i++) {
const angle = rot + (i / 5) * Math.PI;
const scaleX = Math.cos(angle);
ctx.beginPath();
ctx.ellipse(cx, cy, Math.abs(scaleX) * r, r, 0, 0, Math.PI * 2);
ctx.strokeStyle = lineCol;
ctx.lineWidth = 0.6;
ctx.stroke();
}
pins.forEach((pin, idx) => {
const pAngle = rot + pin.lng * Math.PI;
const x = cx + Math.cos(pAngle) * r * 0.85 * (0.3 + pin.lat * 0.7);
const y = cy - r * 0.7 + pin.lat * r * 1.4;
const facing = Math.cos(pAngle);
if (facing < -0.2) return;
const opacity = 0.4 + facing * 0.6;
const pulse = (Math.sin(t * 0.003 + idx * 1.2) + 1) / 2;
const pulseR = 4 + pulse * 14;
ctx.beginPath();
ctx.arc(x, y, pulseR, 0, Math.PI * 2);
ctx.fillStyle = `rgba(47, 63, 44, ${opacity * pulse * 0.25})`;
ctx.fill();
ctx.beginPath();
ctx.arc(x, y, 3.5, 0, Math.PI * 2);
ctx.fillStyle = `rgba(47, 63, 44, ${opacity})`;
ctx.fill();
});
ctx.strokeStyle = `rgba(47, 63, 44, 0.08)`;
ctx.lineWidth = 0.8;
const positions = pins.map((pin) => {
const pAngle = rot + pin.lng * Math.PI;
return {
x: cx + Math.cos(pAngle) * r * 0.85 * (0.3 + pin.lat * 0.7),
y: cy - r * 0.7 + pin.lat * r * 1.4,
facing: Math.cos(pAngle)
};
}).filter(p => p.facing > -0.2);
for (let i = 0; i < positions.length; i++) {
for (let j = i + 1; j < positions.length; j++) {
const dx = positions[i].x - positions[j].x;
const dy = positions[i].y - positions[j].y;
if (Math.sqrt(dx*dx + dy*dy) < r * 0.8) {
ctx.beginPath();
ctx.moveTo(positions[i].x, positions[i].y);
ctx.lineTo(positions[j].x, positions[j].y);
ctx.stroke();
}
}
}
frame = requestAnimationFrame(draw);
}
frame = requestAnimationFrame(draw);
return () => cancelAnimationFrame(frame);
}, []);
return (
);
};
// ==================== BOTANICAL SVG ILLUSTRATIONS ====================
const BotanicalLeaf = ({ style, className }) => (
);
const BotanicalBranch = ({ style, className }) => (
);
// ==================== PHOTO PLACEHOLDERS ====================
const PhotoPlaceholder = ({ label, aspect, className, style }) => (
);
// ==================== IMAGE STRIP ====================
const ImageStrip = () => (
);
// ==================== HERO PARTICLES (cursor-following) ====================
const HeroParticles = () => {
const canvasRef = React.useRef(null);
React.useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
const dpr = window.devicePixelRatio || 1;
let w, h;
let mouse = { x: -9999, y: -9999 };
let frame;
const PARTICLE_COUNT = 80;
const particles = [];
function resize() {
const rect = canvas.parentElement.getBoundingClientRect();
w = rect.width;
h = rect.height;
canvas.width = w * dpr;
canvas.height = h * dpr;
ctx.scale(dpr, dpr);
canvas.style.width = w + "px";
canvas.style.height = h + "px";
}
function createParticle() {
return {
x: Math.random() * (w || 800),
y: Math.random() * (h || 600),
vx: (Math.random() - 0.5) * 0.8,
vy: (Math.random() - 0.5) * 0.8,
r: Math.random() * 1.8 + 0.6,
opacity: Math.random() * 0.18 + 0.04,
drift: Math.random() * 1.0 + 0.4,
phase: Math.random() * Math.PI * 2,
};
}
function init() {
resize();
particles.length = 0;
for (let i = 0; i < PARTICLE_COUNT; i++) {
particles.push(createParticle());
}
}
function draw(t) {
ctx.clearRect(0, 0, w, h);
const isDark = document.body.dataset.theme === "dark";
const baseColor = isDark ? "155, 181, 148" : "47, 63, 44";
for (let i = 0; i < particles.length; i++) {
const p = particles[i];
p.x += p.vx + Math.sin(t * 0.0012 + p.phase) * p.drift * 0.6;
p.y += p.vy + Math.cos(t * 0.001 + p.phase) * p.drift * 0.5;
const dx = mouse.x - p.x;
const dy = mouse.y - p.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 350 && dist > 60) {
const force = (350 - dist) / 350 * 0.018;
p.x += dx * force;
p.y += dy * force;
} else if (dist <= 60 && dist > 0) {
const repel = (60 - dist) / 60 * 0.008;
p.x -= dx * repel;
p.y -= dy * repel;
}
if (p.x < -20) p.x = w + 20;
if (p.x > w + 20) p.x = -20;
if (p.y < -20) p.y = h + 20;
if (p.y > h + 20) p.y = -20;
const pulse = Math.sin(t * 0.001 + p.phase) * 0.05;
ctx.beginPath();
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${baseColor}, ${p.opacity + pulse})`;
ctx.fill();
}
frame = requestAnimationFrame(draw);
}
function handleMouseMove(e) {
const rect = canvas.getBoundingClientRect();
mouse.x = e.clientX - rect.left;
mouse.y = e.clientY - rect.top;
}
function handleMouseLeave() {
mouse.x = -9999;
mouse.y = -9999;
}
function handleTouch(e) {
if (e.touches.length > 0) {
const rect = canvas.getBoundingClientRect();
mouse.x = e.touches[0].clientX - rect.left;
mouse.y = e.touches[0].clientY - rect.top;
}
}
function handleTouchEnd() {
mouse.x = -9999;
mouse.y = -9999;
}
init();
frame = requestAnimationFrame(draw);
const parent = canvas.parentElement;
parent.addEventListener("mousemove", handleMouseMove);
parent.addEventListener("mouseleave", handleMouseLeave);
parent.addEventListener("touchmove", handleTouch, { passive: true });
parent.addEventListener("touchend", handleTouchEnd);
window.addEventListener("resize", resize);
return () => {
cancelAnimationFrame(frame);
parent.removeEventListener("mousemove", handleMouseMove);
parent.removeEventListener("mouseleave", handleMouseLeave);
parent.removeEventListener("touchmove", handleTouch);
parent.removeEventListener("touchend", handleTouchEnd);
window.removeEventListener("resize", resize);
};
}, []);
return (
);
};
Object.assign(window, {
AnimatedGlobe, BotanicalLeaf, BotanicalBranch, PhotoPlaceholder, ImageStrip, HeroParticles
});