mirror of
https://github.com/Such-Software/such.software.git
synced 2026-10-17 05:32:05 +02:00
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:
@@ -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; } }
|
||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user