Constellation Field
Constellation Field is a React and Tailwind CSS background component written for NedDev and yours to ship. Copy it and paste it into your project.
NedDev
backgrounds
What it is
Constellation Field is a React and Tailwind CSS background component written for NedDev and yours to ship. Copy it and paste it into your project.
How to use it
Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.
Five free copies a month. No card.
No packages beyond the project baseline
Licence
This component was written for this catalogue. It is yours to ship in client work and in products you sell, under the terms of use.
Constellation Field
"use client";
import {
useEffect,
useMemo,
useRef,
useState,
type CSSProperties,
} from "react";
type NeuformMode = "dark" | "light";
type NeuformModePreference = NeuformMode | "auto";
export type ConstellationFieldProps = {
mode?: NeuformModePreference;
speed?: number;
size?: number;
gap?: number;
length?: number;
density?: number;
strokeWidth?: number;
opacity?: number;
hue?: number;
saturation?: number;
brightness?: number;
className?: string;
style?: CSSProperties;
};
const DEFAULTS = {
mode: "dark" as NeuformMode,
speed: 1,
size: 1,
gap: 2,
length: 1,
density: 1,
strokeWidth: 1,
opacity: 1,
hue: 0,
saturation: 1,
brightness: 1,
} as const;
const LIGHT_PAPER = "#eef1f6";
function clamp(value: number, minimum: number, maximum: number) {
return Math.min(maximum, Math.max(minimum, value));
}
function scaleCount(base: number, density: number, minimum = 1) {
return Math.max(minimum, Math.round(base * density));
}
function resolveMode(
mode: NeuformMode | number | string | undefined,
fallback: NeuformMode = "dark",
): NeuformMode {
if (mode === undefined || mode === null) return fallback;
if (mode === "light" || mode === 1 || mode === "1") return "light";
return "dark";
}
function readAutomaticMode(): NeuformMode {
if (typeof document === "undefined" || typeof window === "undefined")
return "dark";
const root = document.documentElement;
const declared = root.dataset.scheme ?? root.dataset.theme;
if (declared === "light" || declared === "dark") return declared;
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
function useAutomaticMode(enabled: boolean) {
const [mode, setMode] = useState<NeuformMode>(readAutomaticMode);
useEffect(() => {
if (
!enabled ||
typeof document === "undefined" ||
typeof window === "undefined"
)
return undefined;
const root = document.documentElement;
const media = window.matchMedia("(prefers-color-scheme: dark)");
const update = () => setMode(readAutomaticMode());
const observer = new MutationObserver(update);
observer.observe(root, {
attributes: true,
attributeFilter: ["data-scheme", "data-theme"],
});
media.addEventListener("change", update);
update();
return () => {
observer.disconnect();
media.removeEventListener("change", update);
};
}, [enabled]);
return mode;
}
function resolveBackground(mode: NeuformMode) {
return mode === "light" ? LIGHT_PAPER : "#070914";
}
/** Verbatim source of src/shaders/neuform-isolated/sources/constellation-field.html (MengTo/threeui, MIT). */
const CONSTELLATION_FIELD_SOURCE = String.raw`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lumira - Advanced Analytics</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js"></script>
<!-- GSAP for Masked Reveal -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js"></script>
</head>
<body class="relative min-h-screen overflow-x-hidden flex flex-col font-sans text-[#F2F4FB] bg-[#070914] selection:bg-[#7FC4FF]/30 selection:text-[#7FC4FF]">
<!-- WebGL Constellation Canvas Shell & Depth Overlay -->
<div class="fixed inset-0 -z-20 pointer-events-none bg-[radial-gradient(ellipse_at_center,_#0E1222_0%,_#070914_100%)]"></div>
<div class="fixed inset-0 -z-10 pointer-events-none">
<canvas id="constellationCanvas" class="w-full h-full"></canvas>
</div>
<div class="fixed inset-0 -z-10 pointer-events-none bg-gradient-to-b from-transparent via-[#0E1222]/40 to-[#070914] opacity-80"></div>
<!-- Header (Elevated Glassy UI) -->
<nav class="w-full relative z-20 bg-[#0E1222]/40 backdrop-blur-md border-b border-[#1C2236] shadow-[0_2px_8px_rgba(0,0,0,0.30)]">
<div class="flex justify-between items-center py-5 px-6 md:px-12 max-w-[90rem] mx-auto">
<!-- Brand -->
<div class="flex items-center gap-2 text-[#F2F4FB]">
<div class="relative h-8 w-8 bg-transparent border border-[#1C2236] flex items-center justify-center rounded-md" style="box-shadow: 0 2px 8px rgba(0,0,0,0.30);">
<span class="h-2 w-2 rounded-full bg-[#E6C879]" style="box-shadow: 0 0 12px rgba(230,200,121,0.6);"></span>
</div>
<span class="text-xl font-thin tracking-tight uppercase ml-1">Lumira</span>
</div>
<div class="hidden md:flex items-center gap-10 text-xs font-normal uppercase text-[#9AA3BC] tracking-widest">
<a href="#" class="hover:text-[#F2F4FB] transition-colors hover:shadow-[0_0_8px_rgba(127,196,255,0.4)]">Features</a>
<a href="#" class="hover:text-[#F2F4FB] transition-colors hover:shadow-[0_0_8px_rgba(127,196,255,0.4)]">Use Cases</a>
<a href="#" class="hover:text-[#F2F4FB] transition-colors hover:shadow-[0_0_8px_rgba(127,196,255,0.4)]">Developers</a>
<a href="#" class="hover:text-[#F2F4FB] transition-colors hover:shadow-[0_0_8px_rgba(127,196,255,0.4)]">Pricing</a>
</div>
<!-- Gradient Border Shell CTA -->
<div class="p-[1px] rounded-full bg-gradient-to-br from-[#E6C879]/30 to-transparent">
<a href="#" class="block bg-[#0E1222]/80 backdrop-blur-sm text-[#E6C879] px-6 py-2.5 rounded-full text-xs font-normal uppercase tracking-widest hover:bg-[#E6C879] hover:text-[#0E1222] transition-colors">
Get Access
</a>
</div>
</div>
</nav>
<!-- Main Content -->
<main class="flex-grow flex flex-col items-center justify-center relative z-10 px-6 pt-24 pb-28 md:pt-32 lg:pt-40">
<div class="max-w-5xl mx-auto w-full flex flex-col items-center text-center">
<!-- Trust Indicators -->
<div class="flex items-center gap-4 mb-12 fade-in-up" style="opacity: 0; transform: translateY(24px); transition: all 0.7s cubic-bezier(0.4, 0, 0.2, 1);">
<div class="flex -space-x-3">
<img src="https://cdn.21st.dev/assets/localized/868ee7e6983767d120387ecbd017fd6870f85aa0c5aeba6db06d54e0504c2934.jpg" alt="User 1" class="w-12 h-12 rounded-full border border-[#1C2236] object-cover relative z-30 opacity-80 mix-blend-luminosity">
<img src="https://cdn.21st.dev/assets/localized/95767b6c354c4ac206cadaac8937b5a7b74ea1e227fa2f578703674d3725bcdd.jpg" alt="User 2" class="w-12 h-12 rounded-full border border-[#1C2236] object-cover relative z-20 opacity-80 mix-blend-luminosity">
<img src="https://cdn.21st.dev/assets/localized/4526a25eb7c0b86d945cf5e43ed53e85be9821edb8dc26017ed372b1450ae6f6.jpg" alt="User 3" class="w-12 h-12 rounded-full border border-[#1C2236] object-cover relative z-10 opacity-80 mix-blend-luminosity">
</div>
<div class="flex flex-col items-start gap-1">
<div class="flex items-center text-[#E6C879] text-lg">
<iconify-icon icon="solar:star-linear" stroke-width="1.5"></iconify-icon>
<iconify-icon icon="solar:star-linear" stroke-width="1.5"></iconify-icon>
<iconify-icon icon="solar:star-linear" stroke-width="1.5"></iconify-icon>
<iconify-icon icon="solar:star-linear" stroke-width="1.5"></iconify-icon>
<iconify-icon icon="solar:star-linear" stroke-width="1.5"></iconify-icon>
</div>
<span class="text-xs font-normal uppercase text-[#9AA3BC] tracking-widest">Trusted by 10,000+ data teams</span>
</div>
</div>
<!-- Headline (Ultralight System Display - GSAP Masked Reveal) -->
<h1 class="masked-reveal text-5xl md:text-7xl lg:text-8xl font-thin tracking-tight text-[#F2F4FB] text-center leading-tight max-w-5xl cursor-default">
Uncover hidden patterns<br />with intelligent analytics
</h1>
<!-- Subheadline (GSAP Masked Reveal) -->
<p class="masked-reveal mt-8 text-lg md:text-xl text-[#9AA3BC] max-w-2xl font-normal leading-relaxed">
Lumira synthesizes complex datasets, disparate sources, and endless metrics into actionable, automated insights that guide your decisions.
</p>
<!-- Chunky CTAs to Refined Border Shells -->
<div class="flex flex-col sm:flex-row items-center gap-6 mt-14 w-full justify-center fade-in-up" style="opacity: 0; transform: translateY(24px); transition: all 0.7s cubic-bezier(0.4, 0, 0.2, 1) 0.5s;">
<div class="p-[1px] rounded-full bg-gradient-to-br from-[#E6C879]/40 to-transparent w-full sm:w-auto" style="box-shadow: 0 16px 40px rgba(0,0,0,0.36);">
<a href="#" class="w-full sm:w-auto bg-[#E6C879] text-[#0E1222] px-12 py-4 rounded-full font-medium text-xs uppercase tracking-widest hover:bg-[#E6C879]/90 transition-colors flex items-center justify-center">
Get Access
</a>
</div>
<div class="p-[1px] rounded-full bg-gradient-to-br from-[#E6C879]/16 to-transparent w-full sm:w-auto" style="box-shadow: 0 2px 8px rgba(0,0,0,0.30);">
<a href="#" class="w-full sm:w-auto bg-[#0E1222]/60 backdrop-blur-md text-[#F2F4FB] px-10 py-4 rounded-full font-medium text-xs uppercase tracking-widest hover:bg-[#1C2236]/80 transition-colors flex items-center justify-center gap-2 group">
Explore Demo
<iconify-icon icon="solar:arrow-right-linear" class="text-xl text-[#7FC4FF] opacity-70 group-hover:opacity-100 group-hover:translate-x-1 transition-all" stroke-width="1.5"></iconify-icon>
</a>
</div>
</div>
</div>
<!-- Logos Section -->
<div class="w-full mt-28 md:mt-32 max-w-6xl mx-auto flex flex-col items-center fade-in-up" style="opacity: 0; transform: translateY(24px); transition: all 0.7s cubic-bezier(0.4, 0, 0.2, 1) 0.6s;">
<p class="text-xs font-normal text-[#5C668A] mb-12 tracking-widest uppercase">Powering data-driven enterprises</p>
<div class="flex flex-wrap justify-center items-center gap-10 md:gap-16">
<div class="flex items-center gap-2 text-xl font-thin text-[#9AA3BC] hover:text-[#7FC4FF] hover:shadow-[0_0_12px_rgba(127,196,255,0.2)] transition-all cursor-default">
<iconify-icon icon="solar:box-linear" stroke-width="1.5"></iconify-icon>
Quantus
</div>
<div class="flex items-center gap-2 text-lg font-thin text-[#9AA3BC] hover:text-[#7FC4FF] hover:shadow-[0_0_12px_rgba(127,196,255,0.2)] transition-all cursor-default">
<iconify-icon icon="solar:globus-linear" class="text-xl" stroke-width="1.5"></iconify-icon>
NexusData
</div>
<div class="flex items-center gap-2 text-xl font-thin text-[#9AA3BC] hover:text-[#7FC4FF] hover:shadow-[0_0_12px_rgba(127,196,255,0.2)] transition-all cursor-default">
OmniStream
</div>
<div class="flex items-center gap-2 text-lg font-thin text-[#9AA3BC] hover:text-[#7FC4FF] hover:shadow-[0_0_12px_rgba(127,196,255,0.2)] transition-all cursor-default">
<iconify-icon icon="solar:routing-2-linear" class="text-xl" stroke-width="1.5"></iconify-icon>
Veridian
</div>
<div class="flex items-center gap-2 text-lg font-thin text-[#9AA3BC] hover:text-[#7FC4FF] hover:shadow-[0_0_12px_rgba(127,196,255,0.2)] transition-all cursor-default">
<iconify-icon icon="solar:letter-linear" class="text-xl" stroke-width="1.5"></iconify-icon>
ApexMetrics
</div>
<div class="hidden lg:flex items-center gap-2 text-xl font-thin text-[#9AA3BC] hover:text-[#7FC4FF] hover:shadow-[0_0_12px_rgba(127,196,255,0.2)] transition-all cursor-default">
Zenith
</div>
</div>
</div>
</main>
<script>
// WebGL Drifting Nodes & Network Logic
const canvas = document.getElementById('constellationCanvas');
const ctx = canvas.getContext('2d');
let width, height;
let nodes = [];
const LINK = 160;
const MAX_NODES = window.innerWidth < 768 ? 40 : 85;
let pointer = { x: -1000, y: -1000 };
function resize() {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
width = window.innerWidth;
height = window.innerHeight;
canvas.width = Math.max(1, Math.floor(width * dpr));
canvas.height = Math.max(1, Math.floor(height * dpr));
canvas.style.width = width + 'px';
canvas.style.height = height + 'px';
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.imageSmoothingEnabled = false;
}
window.addEventListener('resize', () => {
resize();
initNodes();
});
resize();
function initNodes() {
nodes = [];
for(let i=0; i<MAX_NODES; i++) {
nodes.push({
x: Math.random() * width,
y: Math.random() * height,
vx: (Math.random() - 0.5) * 0.3,
vy: (Math.random() - 0.5) * 0.3,
radius: Math.random() * 2.4 + 1.8
});
}
}
initNodes();
// Pointer gravity tracker
document.addEventListener('mousemove', e => {
pointer.x = e.clientX;
pointer.y = e.clientY;
});
// Clear pointer on leave
document.addEventListener('mouseleave', () => {
pointer.x = -1000;
pointer.y = -1000;
});
function dist(a, b) {
return Math.hypot(a.x - b.x, a.y - b.y);
}
// Render Loop
function animateCanvas() {
ctx.clearRect(0, 0, width, height);
ctx.lineCap = 'butt';
ctx.lineJoin = 'miter';
// Draw Links first so nodes sit crisp on top
ctx.strokeStyle = '#E6C879';
ctx.lineWidth = 1;
for (let i = 0; i < nodes.length; i++) {
for (let j = i + 1; j < nodes.length; j++) {
const d = dist(nodes[i], nodes[j]);
if (d < LINK) {
ctx.globalAlpha = 0.22 + (1 - d/LINK) * 0.55;
ctx.beginPath();
ctx.moveTo(nodes[i].x, nodes[i].y);
ctx.lineTo(nodes[j].x, nodes[j].y);
ctx.stroke();
}
}
}
nodes.forEach(node => {
node.x += node.vx;
node.y += node.vy;
// Bounce off edges
if(node.x < 0 || node.x > width) node.vx *= -1;
if(node.y < 0 || node.y > height) node.vy *= -1;
// Gentle Pointer gravity
const pd = dist(node, pointer);
if(pd < 220) {
node.x -= (node.x - pointer.x) * 0.005;
node.y -= (node.y - pointer.y) * 0.005;
}
// Draw Node (Pale Gold) — core + soft halo so particles read at retina scale
const pulse = 0.78 + Math.sin(Date.now() * 0.001 + node.x) * 0.22;
ctx.fillStyle = '#E6C879';
ctx.globalAlpha = pulse * 0.28;
ctx.beginPath();
ctx.arc(node.x, node.y, node.radius * 2.4, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = pulse;
ctx.beginPath();
ctx.arc(node.x, node.y, node.radius, 0, Math.PI * 2);
ctx.fill();
});
ctx.globalAlpha = 1;
requestAnimationFrame(animateCanvas);
}
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!prefersReducedMotion) {
animateCanvas();
}
// --- Intersection Observer for structural fade-ins ---
document.addEventListener('DOMContentLoaded', () => {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = '1';
entry.target.style.transform = 'translateY(0)';
observer.unobserve(entry.target);
}
});
}, { threshold: 0.1 });
document.querySelectorAll('.fade-in-up').forEach(el => observer.observe(el));
});
// --- GSAP Masked Staggered Word Reveal ---
document.addEventListener('DOMContentLoaded', () => {
if (typeof gsap !== 'undefined' && !prefersReducedMotion) {
gsap.registerPlugin(ScrollTrigger);
const revealElements = document.querySelectorAll('.masked-reveal');
revealElements.forEach(el => {
// Non-destructive split that respects <br> tags
const html = el.innerHTML;
const fragments = html.split(/(<br\s*\/?>|\s+)/);
el.innerHTML = '';
fragments.forEach(frag => {
if (/<br/i.test(frag)) {
el.appendChild(document.createElement('br'));
} else if (frag.trim() !== '') {
const wrapper = document.createElement('span');
wrapper.style.cssText = 'overflow: hidden; display: inline-block; vertical-align: bottom; padding-top: 0.1em; margin-top: -0.1em;';
const inner = document.createElement('span');
inner.className = 'reveal-word';
inner.style.cssText = 'display: inline-block; transform: translateY(110%); will-change: transform;';
inner.innerHTML = frag;
wrapper.appendChild(inner);
el.appendChild(wrapper);
} else {
// Preserve spaces
el.appendChild(document.createTextNode(frag));
}
});
// Trigger the animation
gsap.to(el.querySelectorAll('.reveal-word'), {
y: '0%',
duration: 1.2,
ease: 'power4.out',
stagger: 0.04,
scrollTrigger: {
trigger: el,
start: 'top 90%',
}
});
});
}
});
</script>
</body>
</html>`;
function patchConstellationField(
source: string,
{
size,
length,
density,
strokeWidth,
mode,
}: {
size: number;
length: number;
density: number;
strokeWidth: number;
mode: NeuformMode;
},
) {
let next = source
.replace("const LINK = 160;", `const LINK = ${Math.round(160 * length)};`)
.replace(
"const MAX_NODES = window.innerWidth < 768 ? 40 : 85;",
`const MAX_NODES = window.innerWidth < 768 ? ${scaleCount(40, density, 8)} : ${scaleCount(85, density, 12)};`,
)
.replace(
"radius: Math.random() * 2.4 + 1.8",
`radius: (Math.random() * 2.4 + 1.8) * ${size}`,
)
.replace(
"ctx.lineWidth = 1;",
`ctx.lineWidth = ${Number(Math.max(0.25, strokeWidth).toFixed(2))};`,
)
.replace(
"node.x += node.vx;",
"node.x += node.vx * ((window.__SF_CONTROLS&&window.__SF_CONTROLS.speed)||1);",
)
.replace(
"node.y += node.vy;",
"node.y += node.vy * ((window.__SF_CONTROLS&&window.__SF_CONTROLS.speed)||1);",
);
if (mode === "light") {
next = next
.replace("ctx.strokeStyle = '#E6C879';", "ctx.strokeStyle = '#8B6914';")
.replace("ctx.fillStyle = '#E6C879';", "ctx.fillStyle = '#8B6914';");
}
return next;
}
function buildFocusedDocument(knobs: {
mode: NeuformMode;
speed: number;
size: number;
gap: number;
length: number;
density: number;
strokeWidth: number;
opacity: number;
}) {
const { mode } = knobs;
const background = resolveBackground(mode);
const targetJson = JSON.stringify([
{ selector: "#constellationCanvas", role: "background" },
]).replace(/</g, "\\u003c");
const controlsJson = JSON.stringify({
mode,
speed: knobs.speed,
size: knobs.size,
gap: knobs.gap,
length: knobs.length,
density: knobs.density,
strokeWidth: knobs.strokeWidth,
opacity: knobs.opacity,
}).replace(/</g, "\\u003c");
const patchedSource = patchConstellationField(CONSTELLATION_FIELD_SOURCE, {
size: knobs.size,
length: knobs.length,
density: knobs.density,
strokeWidth: knobs.strokeWidth,
mode,
});
const focusStyle = `<style data-threeui-focus>
html, body { width: 100% !important; height: 100% !important; min-height: 0 !important; margin: 0 !important; padding: 0 !important; overflow: hidden !important; background: ${background} !important; }
body { position: relative !important; display: flex !important; align-items: center !important; justify-content: center !important; }
body > * { visibility: hidden !important; }
body[data-threeui-ready] > [data-threeui-role] { visibility: visible !important; }
[data-threeui-residual] { display: none !important; }
[data-threeui-role="background"] { position: fixed !important; inset: 0 !important; width: 100% !important; height: 100% !important; max-width: none !important; max-height: none !important; z-index: 0 !important; opacity: 1 !important; pointer-events: none !important; }
[data-threeui-role="ui"] { position: relative !important; z-index: 1 !important; width: min(calc(100% - 32px), var(--threeui-target-width, 1040px)) !important; max-width: none !important; max-height: calc(100% - 32px) !important; margin: auto !important; overflow: auto !important; opacity: 1 !important; transform: none !important; filter: none !important; flex: none !important; box-sizing: border-box !important; }
</style>`;
const controlScript = `<script data-threeui-controls>
(function () {
var controls = ${controlsJson};
window.__SF_CONTROLS = controls;
var origin = performance.now();
var virtual = 0;
var last = origin;
var performanceNow = performance.now.bind(performance);
var dateNow = Date.now.bind(Date);
var dateOrigin = dateNow();
performance.now = function () {
var real = performanceNow();
virtual += (real - last) * (controls.speed || 1);
last = real;
return origin + virtual;
};
Date.now = function () {
return dateOrigin + (performance.now() - origin);
};
var raf = window.requestAnimationFrame.bind(window);
window.requestAnimationFrame = function (callback) {
return raf(function () {
callback(performance.now());
});
};
function applyVisual() {
var opacity = controls.opacity == null ? 1 : controls.opacity;
var size = controls.size == null ? 1 : controls.size;
Array.prototype.forEach.call(document.querySelectorAll('[data-threeui-role]'), function (element) {
element.style.opacity = String(opacity);
if (element.getAttribute('data-threeui-role') === 'ui') {
element.style.transform = 'scale(' + size + ')';
element.style.transformOrigin = 'center center';
}
});
}
window.addEventListener('message', function (event) {
if (!event.data || event.data.type !== 'threeui-controls') return;
var next = event.data.controls || {};
Object.keys(next).forEach(function (key) { controls[key] = next[key]; });
applyVisual();
});
window.__SF_APPLY_CONTROLS = applyVisual;
})();
</script>`;
const focusScript = `<script data-threeui-focus>
(function () {
var isolated = false;
function isolate() {
if (isolated) return;
var specs = ${targetJson};
var roots = [];
specs.forEach(function (spec) {
var element = document.querySelector(spec.selector);
if (!element) return;
element.setAttribute('data-threeui-role', spec.role);
if (spec.width) element.style.setProperty('--threeui-target-width', spec.width);
if (!roots.some(function (root) { return root.contains(element); })) roots.push(element);
});
if (!roots.length) return;
isolated = true;
roots.forEach(function (root) { document.body.appendChild(root); });
Array.from(document.body.children).forEach(function (element) {
if (roots.indexOf(element) !== -1) return;
element.setAttribute('data-threeui-residual', '');
element.setAttribute('aria-hidden', 'true');
if ('inert' in element) element.inert = true;
});
document.body.setAttribute('data-threeui-ready', '');
if (window.__SF_APPLY_CONTROLS) window.__SF_APPLY_CONTROLS();
requestAnimationFrame(function () { window.dispatchEvent(new Event('resize')); });
}
function scheduleIsolation() { setTimeout(isolate, 100); }
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', scheduleIsolation, { once: true });
else scheduleIsolation();
window.addEventListener('load', isolate, { once: true });
})();
</script>`;
return patchedSource
.replace(/<head([^>]*)>/i, `<head$1>${controlScript}${focusStyle}`)
.replace(/<\/body>/i, `${focusScript}</body>`);
}
export default function ConstellationField({
mode,
speed = DEFAULTS.speed,
size = DEFAULTS.size,
gap = DEFAULTS.gap,
length = DEFAULTS.length,
density = DEFAULTS.density,
strokeWidth = DEFAULTS.strokeWidth,
opacity = DEFAULTS.opacity,
hue = DEFAULTS.hue,
saturation = DEFAULTS.saturation,
brightness = DEFAULTS.brightness,
className,
style,
}: ConstellationFieldProps) {
const iframeRef = useRef<HTMLIFrameElement>(null);
const requestedMode = mode ?? DEFAULTS.mode;
const automaticMode = useAutomaticMode(requestedMode === "auto");
const resolvedMode =
requestedMode === "auto"
? automaticMode
: resolveMode(requestedMode, DEFAULTS.mode);
const background = resolveBackground(resolvedMode);
const safeSpeed = clamp(speed, 0, 3);
const safeSize = clamp(size, 0.05, 200);
const safeGap = clamp(gap, 0, 64);
const safeLength = clamp(length, 0.35, 2.5);
const safeDensity = clamp(density, 0.25, 2.5);
const safeStrokeWidth = clamp(strokeWidth, 0.25, 8);
const safeOpacity = clamp(opacity, 0.05, 1);
const safeHue = clamp(hue, -180, 180);
const safeSaturation = clamp(saturation, 0, 2);
const safeBrightness = clamp(brightness, 0.35, 1.65);
// Rebuild when baked geometry/mode knobs change. Speed/opacity stay live via postMessage + time wrap.
const source = useMemo(
() =>
buildFocusedDocument({
mode: resolvedMode,
speed: DEFAULTS.speed,
size: safeSize,
gap: safeGap,
length: safeLength,
density: safeDensity,
strokeWidth: safeStrokeWidth,
opacity: DEFAULTS.opacity,
}),
[resolvedMode, safeDensity, safeGap, safeLength, safeSize, safeStrokeWidth],
);
useEffect(() => {
const frame = iframeRef.current?.contentWindow;
if (!frame) return;
frame.postMessage(
{
type: "threeui-controls",
controls: {
mode: resolvedMode,
speed: safeSpeed,
size: safeSize,
gap: safeGap,
length: safeLength,
density: safeDensity,
strokeWidth: safeStrokeWidth,
opacity: safeOpacity,
},
},
"*",
);
}, [
resolvedMode,
safeDensity,
safeGap,
safeLength,
safeOpacity,
safeSize,
safeSpeed,
safeStrokeWidth,
source,
]);
const filter =
safeHue === 0 && safeSaturation === 1 && safeBrightness === 1
? undefined
: `hue-rotate(${safeHue}deg) saturate(${safeSaturation}) brightness(${safeBrightness})`;
return (
<iframe
ref={iframeRef}
className={className}
title="Constellation Field"
srcDoc={source}
sandbox="allow-scripts"
loading="eager"
style={{
display: "block",
width: "100%",
height: "100%",
border: 0,
background,
filter,
...style,
}}
/>
);
}