/* global React */
const { useState, useEffect, useRef } = React;
// ===== Cursor =====
function CandleCursor() {
const ref = useRef(null);
useEffect(() => {
if (!window.matchMedia("(hover: hover)").matches) return;
document.body.classList.add("story-mode");
const move = (e) => {
if (ref.current) {
ref.current.style.transform = `translate(${e.clientX}px, ${e.clientY}px) translate(-50%, -50%)`;
}
};
window.addEventListener("mousemove", move);
return () => {
window.removeEventListener("mousemove", move);
document.body.classList.remove("story-mode");
};
}, []);
return
;
}
// ===== Scroll progress =====
function ScrollProgress() {
const [p, setP] = useState(0);
useEffect(() => {
const update = () => {
const h = document.documentElement;
const max = h.scrollHeight - h.clientHeight;
setP(max > 0 ? (h.scrollTop / max) * 100 : 0);
};
window.addEventListener("scroll", update, { passive: true });
update();
return () => window.removeEventListener("scroll", update);
}, []);
return (
);
}
// ===== Audio toggle (visual only — no real audio asset) =====
function AudioToggle() {
const [muted, setMuted] = useState(true);
return (
setMuted(!muted)}
aria-label={muted ? "Bật âm thanh" : "Tắt âm thanh"}
title={muted ? "Bật âm thanh dịu nhẹ" : "Tắt âm thanh"}
>
);
}
// ===== Drift stars =====
function DriftStars({ count = 30 }) {
const stars = React.useMemo(() => Array.from({ length: count }, () => ({
left: Math.random() * 100,
top: Math.random() * 100,
size: 1 + Math.random() * 2.5,
delay: Math.random() * 4,
duration: 3 + Math.random() * 4,
})), [count]);
return (
{stars.map((s, i) => (
))}
);
}
// ===== Petals =====
function Petals({ count = 14 }) {
const petals = React.useMemo(() => Array.from({ length: count }, (_, i) => ({
left: Math.random() * 100,
delay: Math.random() * 12,
duration: 10 + Math.random() * 8,
size: 0.6 + Math.random() * 0.8,
})), [count]);
return (
<>
{petals.map((p, i) => (
))}
>
);
}
// ===== Scene: Breath =====
function SceneBreath() {
return (
Holy Dan · Một khoảng tĩnh
Hít vào.
Thở ra.
Bạn đang ở đây.
Hành trình bắt đầu từ một hơi thở
Cuộn xuống
);
}
// ===== Scene: Door (sticky scroll-driven) =====
function SceneDoor() {
const sectionRef = useRef(null);
const [progress, setProgress] = useState(0);
useEffect(() => {
const onScroll = () => {
if (!sectionRef.current) return;
const rect = sectionRef.current.getBoundingClientRect();
const total = rect.height - window.innerHeight;
const scrolled = -rect.top;
const p = Math.max(0, Math.min(1, scrolled / total));
setProgress(p);
};
window.addEventListener("scroll", onScroll, { passive: true });
onScroll();
return () => window.removeEventListener("scroll", onScroll);
}, []);
// Phase 0-0.4: door appears, dim
// Phase 0.4-0.7: light grows, door scales, text appears
// Phase 0.7-1: door opens (rotates) and brightens
const lightProg = Math.max(0, Math.min(1, (progress - 0.2) / 0.6));
const doorScale = 1 + progress * 0.8;
const doorOpenY = progress > 0.7 ? (progress - 0.7) / 0.3 : 0;
const textOpacity = Math.max(0, Math.min(1, (progress - 0.15) / 0.3));
const doorOpacity = 1 - doorOpenY;
return (
Trước cánh cửa
Bạn mang theo gì đến đây?
Một câu hỏi chưa kịp gọi tên. Một cảm xúc bị giấu đi quá lâu. Một quyết định đang đợi bạn nhìn rõ.
Holy Dan
Khẽ mở một lối vào.
Không phán xét. Không vội vàng. Chỉ một không gian để bạn đặt xuống — và lắng nghe chính mình.
);
}
// ===== Scene: Opening (archway with garden glimpse) =====
function SceneOpening() {
const sectionRef = useRef(null);
const [progress, setProgress] = useState(0);
useEffect(() => {
const onScroll = () => {
if (!sectionRef.current) return;
const rect = sectionRef.current.getBoundingClientRect();
const total = rect.height - window.innerHeight;
const scrolled = -rect.top;
const p = Math.max(0, Math.min(1, scrolled / total));
setProgress(p);
};
window.addEventListener("scroll", onScroll, { passive: true });
onScroll();
return () => window.removeEventListener("scroll", onScroll);
}, []);
const skyShift = progress * -20;
const mountShift = progress * -40;
const treeShift = progress * -60;
const groundShift = progress * -80;
const archScale = 1 + progress * 0.3;
return (
Bên trong, là một khu vườn
của chính bạn.
Holy Dan đồng hành — không dẫn đường
);
}
// ===== Scene: Garden (reveal Holy Dan, photo strip) =====
function SceneGarden() {
const ref1 = useRef(null);
const ref2 = useRef(null);
const ref3 = useRef(null);
useEffect(() => {
const obs = new IntersectionObserver((entries) => {
entries.forEach((e) => {
if (e.isIntersecting) e.target.classList.add("in");
});
}, { threshold: 0.2 });
[ref1.current, ref2.current, ref3.current].forEach((el) => el && obs.observe(el));
return () => obs.disconnect();
}, []);
return (
Holy Dan
Tarot là một chiếc gương .
Đối thoại là cánh cửa .
Bạn — là khu vườn.
Holy Dan là không gian đồng hành cảm xúc. Tarot là một trong nhiều công cụ — bên cạnh trò chuyện sâu, thiền dẫn và các trải nghiệm chữa lành — Holy Dan dùng để cùng bạn nhìn thấu bản chất vấn đề và lắng nghe chính mình.
Lắng nghe
Soft conversation · candlelight
Phản chiếu
Tarot reading · soft hands
Bình yên
Quiet ritual · linen tablecloth
);
}
Object.assign(window, {
CandleCursor, ScrollProgress, AudioToggle,
SceneBreath, SceneDoor, SceneOpening, SceneGarden,
});