Nebula monitor: typing terminal cursor, stronger impact, tame pointer, restore mobile streamers

- Replace the centered dash with a tiny CSS terminal in the screen: types 'such'
  once, backspaces, then the caret blinks on the left (steps() width typewriter).
- Impact pulse is snappier and brighter (0.9s ping, scale 1.14, bright cyan glow)
  so streamer hits actually read; the screen color shifts with it.
- Pointer parallax dialed down to a subtle 6px with a gentler spring, fixing the
  desktop 'shift up-left after load' (it was easing toward the cursor on first move).
- Revert the mobile streamer thinning/density cut from the prior commit: it made
  them too sparse to see. Mobile now gets the full set at full weight, like desktop.
This commit is contained in:
jwinterm
2026-06-07 15:20:34 -04:00
parent 4b21928dc4
commit f3607a28d1
2 changed files with 34 additions and 19 deletions
+21 -3
View File
@@ -234,6 +234,24 @@
@keyframes splash-zoom { 0% { transform: scale(1); } 50% { transform: scale(1.06); } 100% { transform: scale(1.14); } } @keyframes splash-zoom { 0% { transform: scale(1); } 50% { transform: scale(1.06); } 100% { transform: scale(1.14); } }
.splash-zoom { animation: splash-zoom 0.8s ease-in-out forwards; } .splash-zoom { animation: splash-zoom 0.8s ease-in-out forwards; }
/* Blinking terminal cursor inside the nebula monitor (opacity only = compositor). */ /* Tiny terminal inside the nebula monitor: type "such" once, backspace, then a blinking
@keyframes nebula-cursor-blink { 0%, 49% { opacity: 0.85; } 50%, 100% { opacity: 0; } } caret on the left. width via steps() is the typewriter; border-right is the caret. */
.nebula-cursor { animation: nebula-cursor-blink 1.1s steps(1, end) infinite; } .nebula-term {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 9px;
line-height: 1;
letter-spacing: 0.5px;
white-space: nowrap;
overflow: hidden;
width: 0;
border-right: 1.5px solid currentColor;
animation: nebula-type 5s steps(4, end) forwards, nebula-caret 0.9s steps(1, end) infinite;
}
@keyframes nebula-type {
0% { width: 0; }
16% { width: 4ch; }
60% { width: 4ch; }
74% { width: 0; }
100% { width: 0; }
}
@keyframes nebula-caret { 50% { border-color: transparent; } }
+13 -16
View File
@@ -105,8 +105,8 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi
// mousemove, so there is no mobile cost. // mousemove, so there is no mobile cost.
const pointerXRaw = useMotionValue(0); const pointerXRaw = useMotionValue(0);
const pointerYRaw = useMotionValue(0); const pointerYRaw = useMotionValue(0);
const pointerX = useSpring(pointerXRaw, { stiffness: 60, damping: 18, mass: 0.4 }); const pointerX = useSpring(pointerXRaw, { stiffness: 40, damping: 22, mass: 0.6 });
const pointerY = useSpring(pointerYRaw, { stiffness: 60, damping: 18, mass: 0.4 }); const pointerY = useSpring(pointerYRaw, { stiffness: 40, damping: 22, mass: 0.6 });
// Monitor position: just to the left of the hero title, on every screen. The extra // Monitor position: just to the left of the hero title, on every screen. The extra
// top room the hero gets on mobile (see page.tsx) keeps this clear of the header. // top room the hero gets on mobile (see page.tsx) keeps this clear of the header.
@@ -118,8 +118,9 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi
const targetX = computerX + 1.5; const targetX = computerX + 1.5;
const targetY = computerY + 1.5; const targetY = computerY + 1.5;
// Fewer, slightly thinner streamers on mobile for a cleaner read on a small screen. // Same streamer count on every screen. Reducing it on mobile made them too sparse
const effectiveDensity = isMobile ? 5 : density; // to read (they looked like there were none).
const effectiveDensity = density;
// Check for reduced motion preference // Check for reduced motion preference
useEffect(() => { useEffect(() => {
@@ -144,7 +145,7 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi
// Pointer parallax driver (desktop only). Spring-smoothed via pointerX/Y above. // Pointer parallax driver (desktop only). Spring-smoothed via pointerX/Y above.
useEffect(() => { useEffect(() => {
if (isMobile) return; if (isMobile) return;
const MAX = 12; // px of nudge at the screen edges const MAX = 6; // px of nudge at the screen edges (subtle, spring-eased)
const onMove = (e: MouseEvent) => { const onMove = (e: MouseEvent) => {
pointerXRaw.set(((e.clientX / window.innerWidth) - 0.5) * 2 * MAX); pointerXRaw.set(((e.clientX / window.innerWidth) - 0.5) * 2 * MAX);
pointerYRaw.set(((e.clientY / window.innerHeight) - 0.5) * 2 * MAX); pointerYRaw.set(((e.clientY / window.innerHeight) - 0.5) * 2 * MAX);
@@ -189,13 +190,13 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi
setMonitorColor(colors[Math.floor(Math.random() * colors.length)]); setMonitorColor(colors[Math.floor(Math.random() * colors.length)]);
monitorControls.start({ monitorControls.start({
scale: [1, 1.1, 1], scale: [1, 1.14, 1],
filter: [ filter: [
"brightness(1) drop-shadow(0 0 0px rgba(0,0,0,0))", "brightness(1) drop-shadow(0 0 0px rgba(0,0,0,0))",
"brightness(1.3) drop-shadow(0 0 12px currentColor)", "brightness(1.7) drop-shadow(0 0 18px currentColor)",
"brightness(1) drop-shadow(0 0 0px rgba(0,0,0,0))" "brightness(1) drop-shadow(0 0 0px rgba(0,0,0,0))"
], ],
transition: { duration: 1.5, ease: "easeOut" } transition: { duration: 0.9, ease: "easeOut" }
}); });
}, [monitorControls, themeMode]); }, [monitorControls, themeMode]);
@@ -265,13 +266,9 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi
> >
<motion.div animate={monitorControls} className={cn("relative transition-colors duration-1000", monitorColor)}> <motion.div animate={monitorControls} className={cn("relative transition-colors duration-1000", monitorColor)}>
<Monitor size={56} /> <Monitor size={56} />
{/* Live-terminal blink inside the screen. CSS opacity only, so it runs on the {/* Tiny terminal in the screen: types "such" once, backspaces, then the caret
compositor for essentially no main-thread cost. */} blinks on the left. Pure CSS (steps + width), so it costs ~nothing. */}
<span <span aria-hidden="true" className="nebula-term pointer-events-none absolute left-[26%] top-[30%]">such</span>
aria-hidden="true"
className="nebula-cursor pointer-events-none absolute left-1/2 top-[38%] h-[3px] w-[9px] -translate-x-1/2 rounded-[1px]"
style={{ backgroundColor: "currentColor" }}
/>
</motion.div> </motion.div>
</motion.div> </motion.div>
@@ -287,7 +284,7 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi
d={d} d={d}
themeMode={themeMode} themeMode={themeMode}
onHit={handleHit} onHit={handleHit}
width={(themeMode === 'light' ? 0.08 : 0.12) * (isMobile ? 0.85 : 1)} width={themeMode === 'light' ? 0.08 : 0.12}
index={i} index={i}
/> />
))} ))}