/* App + tweaks wiring + scroll reveal */
function App() {
const defaults = window.__TWEAK_DEFAULTS;
const [t, setTweak] = useTweaks(defaults);
// Sync into window so non-React 3D handlers can read latest
React.useEffect(() => {
window.__tweaks = t;
window.dispatchEvent(new Event('codekid:tweak'));
// Apply accent CSS var
const accentMap = { cyan: '#00E5FF', purple: '#7C3AED', green: '#00FFA3' };
document.documentElement.style.setProperty('--accent', accentMap[t.accent] || '#00E5FF');
}, [t]);
// Scroll reveal — plain scroll listener (IntersectionObserver unreliable in preview iframe)
React.useEffect(() => {
const check = () => {
const trigger = window.innerHeight * 0.92;
document.querySelectorAll('.reveal:not(.in)').forEach(el => {
const r = el.getBoundingClientRect();
if (r.top < trigger && r.bottom > 0) el.classList.add('in');
});
};
const raf = requestAnimationFrame(() => requestAnimationFrame(check));
window.addEventListener('scroll', check, { passive: true });
window.addEventListener('resize', check);
return () => {
cancelAnimationFrame(raf);
window.removeEventListener('scroll', check);
window.removeEventListener('resize', check);
};
}, []);
// Particle field
const particles = t.particles ? : null;
return (
<>
{particles}
setTweak('accent', v)}
options={[
{ label: 'Ciano', value: 'cyan', color: '#00E5FF' },
{ label: 'Roxo', value: 'purple', color: '#7C3AED' },
{ label: 'Verde', value: 'green', color: '#00FFA3' }
]}
/>
setTweak('particles', v)}
/>
setTweak('parallax', v)}
/>
setTweak('radarSpeed', v)}
/>
>
);
}
/* TweakColor variant — accept {label,value,color}[] options */
function TweakColor({ label, value, onChange, options }) {
return (
{label}
{options.map(o => (
))}
);
}
function ParticleField() {
const ref = React.useRef(null);
React.useEffect(() => {
const canvas = ref.current;
if (!canvas) return;
if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const ctx = canvas.getContext('2d');
let raf, w, h;
let N = 0, pts = [];
const resize = () => {
w = canvas.width = window.innerWidth * devicePixelRatio;
h = canvas.height = window.innerHeight * devicePixelRatio;
canvas.style.width = window.innerWidth + 'px';
canvas.style.height = window.innerHeight + 'px';
N = window.innerWidth < 700 ? 24 : 70;
pts = Array.from({ length: N }, () => ({
x: Math.random() * w, y: Math.random() * h,
vx: (Math.random() - 0.5) * 0.3 * devicePixelRatio,
vy: (Math.random() - 0.5) * 0.3 * devicePixelRatio,
r: (Math.random() * 1.4 + 0.5) * devicePixelRatio
}));
};
resize();
window.addEventListener('resize', resize);
const colors = ['rgba(0,229,255,', 'rgba(124,58,237,', 'rgba(0,255,163,'];
const tick = () => {
ctx.clearRect(0, 0, w, h);
// Connections
for (let i = 0; i < N; i++) {
for (let j = i + 1; j < N; j++) {
const dx = pts[i].x - pts[j].x;
const dy = pts[i].y - pts[j].y;
const d2 = dx*dx + dy*dy;
const max = 130 * devicePixelRatio;
if (d2 < max * max) {
const a = (1 - Math.sqrt(d2) / max) * 0.18;
ctx.strokeStyle = `rgba(0,229,255,${a})`;
ctx.lineWidth = 0.5 * devicePixelRatio;
ctx.beginPath();
ctx.moveTo(pts[i].x, pts[i].y);
ctx.lineTo(pts[j].x, pts[j].y);
ctx.stroke();
}
}
}
// Points
pts.forEach((p, i) => {
p.x += p.vx; p.y += p.vy;
if (p.x < 0 || p.x > w) p.vx *= -1;
if (p.y < 0 || p.y > h) p.vy *= -1;
const c = colors[i % 3];
ctx.fillStyle = c + '0.6)';
ctx.beginPath();
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
ctx.fill();
});
raf = requestAnimationFrame(tick);
};
tick();
return () => { cancelAnimationFrame(raf); window.removeEventListener('resize', resize); };
}, []);
return ;
}
ReactDOM.createRoot(document.getElementById('root')).render();