From 765f522d6d79229e671c91194e5cebe3aa8ab639 Mon Sep 17 00:00:00 2001 From: Rafael Staib Date: Fri, 24 Apr 2026 21:05:32 +0200 Subject: [PATCH 1/4] improvements --- .../components/layout/site/site-layout.tsx | 353 +++++++++++++----- .../components/widgets/companies-section.tsx | 45 +-- 2 files changed, 274 insertions(+), 124 deletions(-) diff --git a/website/src/components/layout/site/site-layout.tsx b/website/src/components/layout/site/site-layout.tsx index 910851eb5d5..7ebcd891537 100644 --- a/website/src/components/layout/site/site-layout.tsx +++ b/website/src/components/layout/site/site-layout.tsx @@ -23,33 +23,44 @@ export const SiteLayout: FC = ({ children, disableStars }) => { function Stars(): ReactElement { const canvasRef = useRef(null); + const blackHoleRef = useRef(null); useLayoutEffect(() => { if (!canvasRef.current) { return; } - const canvas = canvasRef.current; - const ctx = canvas.getContext("2d"); + const glCanvas = canvasRef.current; + const gl = glCanvas.getContext("webgl", { + alpha: true, + premultipliedAlpha: false, + }); + if (!gl) { + return; + } + + const starCanvas = document.createElement("canvas"); + const ctx = starCanvas.getContext("2d"); + if (!ctx) { + return; + } + const numStars = 800; const speed = 0.25; let stars: Star[] = []; - let pointerX = 0; - let pointerY = 0; - let pointerTargetX = 0; - let pointerTargetY = 0; - let scrollTiltY = 0; - let scrollTargetY = 0; - let scrollAccum = 0; - let lastScrollTop = 0; - let scrollIdleTimer: number | null = null; - const pointerMax = 15; - const scrollMax = 60; - const scrollRampDistance = 400; + let mouseX = 0; + let mouseY = 0; + let forceTarget = 0; + let forceCurrent = 0; function setCanvasSize() { - canvas.width = window.innerWidth; - canvas.height = window.innerHeight; + const w = window.innerWidth; + const h = window.innerHeight; + glCanvas.width = w; + glCanvas.height = h; + starCanvas.width = w; + starCanvas.height = h; + gl!.viewport(0, 0, w, h); } class Star { @@ -68,26 +79,22 @@ function Stars(): ReactElement { } reset() { - this.z = canvas.width; - this.x = Math.random() * (canvas.width * 2) - canvas.width; - this.y = Math.random() * (canvas.height * 2) - canvas.height; + this.z = starCanvas.width; + this.x = Math.random() * (starCanvas.width * 2) - starCanvas.width; + this.y = Math.random() * (starCanvas.height * 2) - starCanvas.height; this.size = Math.random() * 2 + 1; } draw() { const x = - (((this.x + pointerX) / this.z) * canvas.width) / 2 + - canvas.width / 2; + ((this.x / this.z) * starCanvas.width) / 2 + starCanvas.width / 2; const y = - (((this.y + pointerY + scrollTiltY) / this.z) * canvas.height) / 2 + - canvas.height / 2; - const radius = (1 - this.z / canvas.width) * this.size; - - if (ctx) { - ctx.beginPath(); - ctx.arc(x, y, radius, 0, Math.PI * 2); - ctx.fill(); - } + ((this.y / this.z) * starCanvas.height) / 2 + starCanvas.height / 2; + const radius = (1 - this.z / starCanvas.width) * this.size; + + ctx!.beginPath(); + ctx!.arc(x, y, radius, 0, Math.PI * 2); + ctx!.fill(); } } @@ -96,71 +103,216 @@ function Stars(): ReactElement { { length: numStars }, () => new Star( - Math.random() * (canvas.width * 2) - canvas.width, - Math.random() * (canvas.height * 2) - canvas.height, - Math.random() * canvas.width, + Math.random() * (starCanvas.width * 2) - starCanvas.width, + Math.random() * (starCanvas.height * 2) - starCanvas.height, + Math.random() * starCanvas.width, Math.random() * 2 + 1 ) ); } - function updateStars() { - stars.forEach((star) => star.update()); - pointerX += (pointerTargetX - pointerX) * 0.08; - pointerY += (pointerTargetY - pointerY) * 0.08; - const scrollEase = scrollTargetY === 0 ? 0.009 : 0.08; - scrollTiltY += (scrollTargetY - scrollTiltY) * scrollEase; - } + const vertSrc = ` + attribute vec2 aPos; + varying vec2 vUv; + void main() { + vUv = (aPos + 1.0) * 0.5; + gl_Position = vec4(aPos, 0.0, 1.0); + } + `; - function handlePointerMove(e: PointerEvent) { - const nx = (e.clientX / window.innerWidth) * 2 - 1; - const ny = (e.clientY / window.innerHeight) * 2 - 1; - pointerTargetX = nx * pointerMax; - pointerTargetY = ny * pointerMax; - } + const fragSrc = ` + precision mediump float; + uniform vec2 u_mouse; + uniform vec2 u_resolution; + uniform sampler2D u_texture; + uniform float u_force; + uniform float u_time; + varying vec2 vUv; - function handleScroll(e: Event) { - const target = e.target; - const scrollTop = - target instanceof HTMLElement ? target.scrollTop : window.scrollY; - const delta = scrollTop - lastScrollTop; - lastScrollTop = scrollTop; + float hash(vec2 p) { + return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); + } - if (delta !== 0 && Math.sign(delta) !== Math.sign(scrollAccum)) { - scrollAccum = 0; + float noise(vec2 p) { + vec2 i = floor(p); + vec2 f = fract(p); + vec2 u = f * f * (3.0 - 2.0 * f); + return mix( + mix(hash(i), hash(i + vec2(1.0, 0.0)), u.x), + mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), u.x), + u.y + ); } - scrollAccum = Math.max( - -scrollRampDistance, - Math.min(scrollRampDistance, scrollAccum + delta) - ); - const t = Math.abs(scrollAccum) / scrollRampDistance; - const eased = t * t * (3 - 2 * t); - scrollTargetY = Math.sign(scrollAccum) * eased * scrollMax; - if (scrollIdleTimer) { - window.clearTimeout(scrollIdleTimer); + float fbm(vec2 p) { + float v = 0.0; + float a = 0.5; + for (int i = 0; i < 5; i++) { + v += a * noise(p); + p *= 2.0; + a *= 0.5; + } + return v; } - scrollIdleTimer = window.setTimeout(() => { - scrollTargetY = 0; - scrollAccum = 0; - }, 150); - } + float fbm3(vec2 p) { + float v = 0.0; + float a = 0.5; + for (int i = 0; i < 3; i++) { + v += a * noise(p); + p *= 2.0; + a *= 0.5; + } + return v; + } + + void main() { + vec2 st = vUv; + vec2 mouse = u_mouse / u_resolution; + mouse.y = 1.0 - mouse.y; + + float aspect = u_resolution.x / u_resolution.y; + vec2 scaledSt = st * vec2(aspect, 1.0); + float dist = distance(scaledSt, mouse * vec2(aspect, 1.0)); + + float distortion = u_force / max(dist, 0.0001); + vec2 distortedUv = st + (mouse - st) * distortion; + + vec4 stars = texture2D(u_texture, distortedUv); + + vec2 nebUv = distortedUv * vec2(aspect, 1.0); + float t = u_time; + float n1 = fbm(nebUv * 2.2 + vec2(t * 0.012, t * 0.008)); + float n2 = fbm3(nebUv * 5.5 + vec2(31.7 - t * 0.018, 11.3 + t * 0.010)); + float n3 = fbm3(nebUv * 3.8 + vec2(-7.9 + t * 0.014, 22.5 - t * 0.009)); + float n4 = fbm3(nebUv * 4.3 + vec2(54.1 + t * 0.007, -18.7 + t * 0.016)); + float n5 = fbm3(nebUv * 6.1 + vec2(-23.9 - t * 0.013, 47.2 - t * 0.006)); + float n6 = fbm3(nebUv * 2.8 + vec2(67.4 + t * 0.005, 3.6 + t * 0.011)); + float n7 = fbm3(nebUv * 4.8 + vec2(-41.2 + t * 0.009, -62.5 - t * 0.013)); + float n8 = fbm3(nebUv * 5.0 + vec2(88.6 - t * 0.011, 29.4 + t * 0.015)); - function drawStars() { - if (ctx) { - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.fillStyle = "#f4ebcb"; - stars.forEach((star) => star.draw()); + vec3 deep = vec3(0.10, 0.07, 0.22); + vec3 violet = vec3(0.35, 0.22, 0.60); + vec3 blueGrey = vec3(0.30, 0.45, 0.80); + vec3 coral = vec3(0.95, 0.65, 0.55); + vec3 magenta = vec3(0.85, 0.32, 0.65); + vec3 teal = vec3(0.15, 0.68, 0.78); + vec3 amber = vec3(0.95, 0.75, 0.35); + vec3 red = vec3(0.92, 0.26, 0.30); + vec3 yellow = vec3(0.98, 0.88, 0.35); + + vec3 nebulaColor = deep; + nebulaColor = mix(nebulaColor, violet, smoothstep(0.30, 0.80, n1)); + nebulaColor = mix(nebulaColor, blueGrey, smoothstep(0.55, 0.90, n2) * 0.65); + nebulaColor = mix(nebulaColor, teal, smoothstep(0.62, 0.92, n5) * 0.55); + nebulaColor = mix(nebulaColor, magenta, smoothstep(0.68, 0.92, n4) * 0.45); + nebulaColor = mix(nebulaColor, coral, smoothstep(0.72, 0.95, n3) * 0.40); + nebulaColor = mix(nebulaColor, red, smoothstep(0.78, 0.95, n7) * 0.40); + nebulaColor = mix(nebulaColor, amber, smoothstep(0.78, 0.96, n6) * 0.30); + nebulaColor = mix(nebulaColor, yellow, smoothstep(0.82, 0.97, n8) * 0.35); + + float nebulaAlpha = 0.20 + n1 * 0.38; + + float starsA = smoothstep(0.02, 0.5, stars.a); + vec3 preRgb = + stars.rgb * starsA + + nebulaColor * nebulaAlpha * (1.0 - starsA); + float outAlpha = starsA + nebulaAlpha * (1.0 - starsA); + gl_FragColor = vec4(preRgb, outAlpha); } + `; + + function compile(type: number, src: string): WebGLShader { + const sh = gl!.createShader(type)!; + gl!.shaderSource(sh, src); + gl!.compileShader(sh); + return sh; } + const vs = compile(gl.VERTEX_SHADER, vertSrc); + const fs = compile(gl.FRAGMENT_SHADER, fragSrc); + const program = gl.createProgram()!; + gl.attachShader(program, vs); + gl.attachShader(program, fs); + gl.linkProgram(program); + gl.useProgram(program); + + const vbo = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, vbo); + gl.bufferData( + gl.ARRAY_BUFFER, + new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), + gl.STATIC_DRAW + ); + const aPosLoc = gl.getAttribLocation(program, "aPos"); + gl.enableVertexAttribArray(aPosLoc); + gl.vertexAttribPointer(aPosLoc, 2, gl.FLOAT, false, 0, 0); + + const uMouseLoc = gl.getUniformLocation(program, "u_mouse"); + const uResLoc = gl.getUniformLocation(program, "u_resolution"); + const uForceLoc = gl.getUniformLocation(program, "u_force"); + const uTexLoc = gl.getUniformLocation(program, "u_texture"); + const uTimeLoc = gl.getUniformLocation(program, "u_time"); + const startTime = performance.now(); + + const tex = gl.createTexture(); + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, tex); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); + gl.uniform1i(uTexLoc, 0); + + gl.enable(gl.BLEND); + gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); + gl.clearColor(0, 0, 0, 0); function animate() { - updateStars(); - drawStars(); + ctx!.clearRect(0, 0, starCanvas.width, starCanvas.height); + ctx!.fillStyle = "#f4ebcb"; + stars.forEach((s) => s.update()); + stars.forEach((s) => s.draw()); + + gl!.bindTexture(gl!.TEXTURE_2D, tex); + gl!.texImage2D( + gl!.TEXTURE_2D, + 0, + gl!.RGBA, + gl!.RGBA, + gl!.UNSIGNED_BYTE, + starCanvas + ); + + forceCurrent += (forceTarget - forceCurrent) * 0.1; + + gl!.clear(gl!.COLOR_BUFFER_BIT); + gl!.uniform2f(uMouseLoc, mouseX, mouseY); + gl!.uniform2f(uResLoc, glCanvas.width, glCanvas.height); + gl!.uniform1f(uForceLoc, forceCurrent); + gl!.uniform1f(uTimeLoc, (performance.now() - startTime) / 1000); + gl!.drawArrays(gl!.TRIANGLES, 0, 6); + requestAnimationFrame(animate); } + function handlePointerMove(e: PointerEvent) { + mouseX = e.clientX; + mouseY = e.clientY; + forceTarget = 0.08; + if (blackHoleRef.current) { + blackHoleRef.current.style.transform = `translate3d(${e.clientX}px, ${e.clientY}px, 0) translate(-50%, -50%)`; + blackHoleRef.current.style.opacity = "1"; + } + } + + function handlePointerLeave() { + forceTarget = 0; + if (blackHoleRef.current) { + blackHoleRef.current.style.opacity = "0"; + } + } + window.addEventListener("resize", () => { setCanvasSize(); initStars(); @@ -168,10 +320,7 @@ function Stars(): ReactElement { window.addEventListener("pointermove", handlePointerMove, { passive: true, }); - window.addEventListener("scroll", handleScroll, { - capture: true, - passive: true, - }); + document.addEventListener("pointerleave", handlePointerLeave); setCanvasSize(); initStars(); @@ -179,18 +328,38 @@ function Stars(): ReactElement { }, []); return ( - + <> + + + + + ); } diff --git a/website/src/components/widgets/companies-section.tsx b/website/src/components/widgets/companies-section.tsx index 0fea5dbc902..b43dbc1b8a2 100644 --- a/website/src/components/widgets/companies-section.tsx +++ b/website/src/components/widgets/companies-section.tsx @@ -47,8 +47,6 @@ import ZioskLogoSvg from "@/images/companies/ziosk.svg"; export const CompaniesSection: FC = () => ( - - {[ { @@ -278,6 +276,19 @@ const VisibleArea = styled.div` width: 100%; height: 100%; max-width: ${MAX_CONTENT_WIDTH}px; + mask-image: linear-gradient( + 90deg, + rgba(0, 0, 0, 0) 0%, + rgb(0, 0, 0) 15%, + rgb(0, 0, 0) 85%, + rgba(0, 0, 0, 0) 100% + ); + mask-clip: border-box; + mask-composite: add; + mask-mode: match-source; + mask-origin: border-box; + mask-repeat: repeat; + mask-size: auto; `; interface TickerLogo { @@ -354,33 +365,3 @@ const GenericLogo = styled.div<{ width?: number }>` } } `; - -const FadeOut = styled.div` - position: absolute; - top: 0; - bottom: 0; - left: 0; - z-index: 1; - width: 120px; - background: linear-gradient( - 270deg, - #ffffff00 0%, - ${THEME_COLORS.background} 100% - ); - pointer-events: none; -`; - -const FadeIn = styled.div` - position: absolute; - top: 0; - bottom: 0; - right: 0; - z-index: 1; - width: 120px; - background: linear-gradient( - 90deg, - #ffffff00 0%, - ${THEME_COLORS.background} 100% - ); - pointer-events: none; -`; From 4591ac1059810d505ade05a667ff163b3362c2df Mon Sep 17 00:00:00 2001 From: Rafael Staib Date: Fri, 24 Apr 2026 21:24:33 +0200 Subject: [PATCH 2/4] improvements --- .../components/layout/site/site-layout.tsx | 124 ++++++++++++++---- 1 file changed, 99 insertions(+), 25 deletions(-) diff --git a/website/src/components/layout/site/site-layout.tsx b/website/src/components/layout/site/site-layout.tsx index 7ebcd891537..c5ef1e9f7c6 100644 --- a/website/src/components/layout/site/site-layout.tsx +++ b/website/src/components/layout/site/site-layout.tsx @@ -35,12 +35,14 @@ function Stars(): ReactElement { alpha: true, premultipliedAlpha: false, }); + if (!gl) { return; } const starCanvas = document.createElement("canvas"); const ctx = starCanvas.getContext("2d"); + if (!ctx) { return; } @@ -56,6 +58,7 @@ function Stars(): ReactElement { function setCanvasSize() { const w = window.innerWidth; const h = window.innerHeight; + glCanvas.width = w; glCanvas.height = h; starCanvas.width = w; @@ -111,6 +114,8 @@ function Stars(): ReactElement { ); } + const hasHover = window.matchMedia("(hover: hover)").matches; + const vertSrc = ` attribute vec2 aPos; varying vec2 vUv; @@ -120,15 +125,7 @@ function Stars(): ReactElement { } `; - const fragSrc = ` - precision mediump float; - uniform vec2 u_mouse; - uniform vec2 u_resolution; - uniform sampler2D u_texture; - uniform float u_force; - uniform float u_time; - varying vec2 vUv; - + const noiseHelpers = ` float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } @@ -165,18 +162,34 @@ function Stars(): ReactElement { } return v; } + `; + + const fragDesktopSrc = ` + precision mediump float; + uniform vec2 u_mouse; + uniform vec2 u_resolution; + uniform sampler2D u_texture; + uniform float u_force; + uniform float u_time; + varying vec2 vUv; + + ${noiseHelpers} void main() { vec2 st = vUv; - vec2 mouse = u_mouse / u_resolution; - mouse.y = 1.0 - mouse.y; - float aspect = u_resolution.x / u_resolution.y; - vec2 scaledSt = st * vec2(aspect, 1.0); - float dist = distance(scaledSt, mouse * vec2(aspect, 1.0)); - float distortion = u_force / max(dist, 0.0001); - vec2 distortedUv = st + (mouse - st) * distortion; + vec2 distortedUv; + if (u_force < 0.001) { + distortedUv = st; + } else { + vec2 mouse = u_mouse / u_resolution; + mouse.y = 1.0 - mouse.y; + vec2 scaledSt = st * vec2(aspect, 1.0); + float dist = distance(scaledSt, mouse * vec2(aspect, 1.0)); + float distortion = u_force / max(dist, 0.0001); + distortedUv = st + (mouse - st) * distortion; + } vec4 stars = texture2D(u_texture, distortedUv); @@ -222,15 +235,67 @@ function Stars(): ReactElement { } `; + const fragMobileSrc = ` + precision mediump float; + uniform vec2 u_resolution; + uniform sampler2D u_texture; + uniform float u_time; + varying vec2 vUv; + + ${noiseHelpers} + + void main() { + vec2 st = vUv; + float aspect = u_resolution.x / u_resolution.y; + + vec4 stars = texture2D(u_texture, st); + + vec2 nebUv = st * vec2(aspect, 1.0); + float t = u_time; + float n1 = fbm3(nebUv * 2.2 + vec2(t * 0.012, t * 0.008)); + float n2 = fbm3(nebUv * 5.5 + vec2(31.7 - t * 0.018, 11.3 + t * 0.010)); + float n3 = fbm3(nebUv * 3.8 + vec2(-7.9 + t * 0.014, 22.5 - t * 0.009)); + float n4 = fbm3(nebUv * 4.3 + vec2(54.1 + t * 0.007, -18.7 + t * 0.016)); + float n5 = fbm3(nebUv * 6.1 + vec2(-23.9 - t * 0.013, 47.2 - t * 0.006)); + + vec3 deep = vec3(0.10, 0.07, 0.22); + vec3 violet = vec3(0.35, 0.22, 0.60); + vec3 blueGrey = vec3(0.30, 0.45, 0.80); + vec3 coral = vec3(0.95, 0.65, 0.55); + vec3 magenta = vec3(0.85, 0.32, 0.65); + vec3 teal = vec3(0.15, 0.68, 0.78); + + vec3 nebulaColor = deep; + nebulaColor = mix(nebulaColor, violet, smoothstep(0.30, 0.80, n1)); + nebulaColor = mix(nebulaColor, blueGrey, smoothstep(0.55, 0.90, n2) * 0.65); + nebulaColor = mix(nebulaColor, teal, smoothstep(0.62, 0.92, n5) * 0.55); + nebulaColor = mix(nebulaColor, magenta, smoothstep(0.68, 0.92, n4) * 0.45); + nebulaColor = mix(nebulaColor, coral, smoothstep(0.72, 0.95, n3) * 0.40); + + float nebulaAlpha = 0.20 + n1 * 0.38; + + float starsA = smoothstep(0.02, 0.5, stars.a); + vec3 preRgb = + stars.rgb * starsA + + nebulaColor * nebulaAlpha * (1.0 - starsA); + float outAlpha = starsA + nebulaAlpha * (1.0 - starsA); + gl_FragColor = vec4(preRgb, outAlpha); + } + `; + + const fragSrc = hasHover ? fragDesktopSrc : fragMobileSrc; + function compile(type: number, src: string): WebGLShader { const sh = gl!.createShader(type)!; gl!.shaderSource(sh, src); gl!.compileShader(sh); return sh; } + const vs = compile(gl.VERTEX_SHADER, vertSrc); const fs = compile(gl.FRAGMENT_SHADER, fragSrc); const program = gl.createProgram()!; + gl.attachShader(program, vs); gl.attachShader(program, fs); gl.linkProgram(program); @@ -238,12 +303,15 @@ function Stars(): ReactElement { const vbo = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, vbo); + gl.bufferData( gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW ); + const aPosLoc = gl.getAttribLocation(program, "aPos"); + gl.enableVertexAttribArray(aPosLoc); gl.vertexAttribPointer(aPosLoc, 2, gl.FLOAT, false, 0, 0); @@ -253,8 +321,8 @@ function Stars(): ReactElement { const uTexLoc = gl.getUniformLocation(program, "u_texture"); const uTimeLoc = gl.getUniformLocation(program, "u_time"); const startTime = performance.now(); - const tex = gl.createTexture(); + gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, tex); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); @@ -284,13 +352,14 @@ function Stars(): ReactElement { starCanvas ); - forceCurrent += (forceTarget - forceCurrent) * 0.1; - gl!.clear(gl!.COLOR_BUFFER_BIT); - gl!.uniform2f(uMouseLoc, mouseX, mouseY); gl!.uniform2f(uResLoc, glCanvas.width, glCanvas.height); - gl!.uniform1f(uForceLoc, forceCurrent); gl!.uniform1f(uTimeLoc, (performance.now() - startTime) / 1000); + if (hasHover) { + forceCurrent += (forceTarget - forceCurrent) * 0.1; + gl!.uniform2f(uMouseLoc, mouseX, mouseY); + gl!.uniform1f(uForceLoc, forceCurrent); + } gl!.drawArrays(gl!.TRIANGLES, 0, 6); requestAnimationFrame(animate); @@ -300,6 +369,7 @@ function Stars(): ReactElement { mouseX = e.clientX; mouseY = e.clientY; forceTarget = 0.08; + if (blackHoleRef.current) { blackHoleRef.current.style.transform = `translate3d(${e.clientX}px, ${e.clientY}px, 0) translate(-50%, -50%)`; blackHoleRef.current.style.opacity = "1"; @@ -308,6 +378,7 @@ function Stars(): ReactElement { function handlePointerLeave() { forceTarget = 0; + if (blackHoleRef.current) { blackHoleRef.current.style.opacity = "0"; } @@ -317,10 +388,13 @@ function Stars(): ReactElement { setCanvasSize(); initStars(); }); - window.addEventListener("pointermove", handlePointerMove, { - passive: true, - }); - document.addEventListener("pointerleave", handlePointerLeave); + + if (hasHover) { + window.addEventListener("pointermove", handlePointerMove, { + passive: true, + }); + document.addEventListener("pointerleave", handlePointerLeave); + } setCanvasSize(); initStars(); From 8b4407e9f7cd76e2d9aec1b2144200a2879c09be Mon Sep 17 00:00:00 2001 From: Rafael Staib Date: Fri, 24 Apr 2026 23:58:16 +0200 Subject: [PATCH 3/4] fusion improvements --- website/src/images/startpage/fusion.svg | 48 +++++++++++++++++-------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/website/src/images/startpage/fusion.svg b/website/src/images/startpage/fusion.svg index b26ff15451a..f773291640b 100644 --- a/website/src/images/startpage/fusion.svg +++ b/website/src/images/startpage/fusion.svg @@ -48,7 +48,27 @@ + + + + + + + + + + + + + + + + + + + + @@ -196,38 +216,38 @@ - - + + - - + + - - + + - - + + - - + + - - + + - - + + From 65df6159b821cdf1b65cdde2622d8027d7b20715 Mon Sep 17 00:00:00 2001 From: Rafael Staib Date: Mon, 27 Apr 2026 17:39:56 +0200 Subject: [PATCH 4/4] finishing --- .../components/layout/site/site-layout.tsx | 156 ++---------------- 1 file changed, 16 insertions(+), 140 deletions(-) diff --git a/website/src/components/layout/site/site-layout.tsx b/website/src/components/layout/site/site-layout.tsx index c5ef1e9f7c6..192c4cc917b 100644 --- a/website/src/components/layout/site/site-layout.tsx +++ b/website/src/components/layout/site/site-layout.tsx @@ -23,7 +23,6 @@ export const SiteLayout: FC = ({ children, disableStars }) => { function Stars(): ReactElement { const canvasRef = useRef(null); - const blackHoleRef = useRef(null); useLayoutEffect(() => { if (!canvasRef.current) { @@ -50,10 +49,6 @@ function Stars(): ReactElement { const numStars = 800; const speed = 0.25; let stars: Star[] = []; - let mouseX = 0; - let mouseY = 0; - let forceTarget = 0; - let forceCurrent = 0; function setCanvasSize() { const w = window.innerWidth; @@ -114,8 +109,6 @@ function Stars(): ReactElement { ); } - const hasHover = window.matchMedia("(hover: hover)").matches; - const vertSrc = ` attribute vec2 aPos; varying vec2 vUv; @@ -164,12 +157,10 @@ function Stars(): ReactElement { } `; - const fragDesktopSrc = ` + const fragSrc = ` precision mediump float; - uniform vec2 u_mouse; uniform vec2 u_resolution; uniform sampler2D u_texture; - uniform float u_force; uniform float u_time; varying vec2 vUv; @@ -179,21 +170,9 @@ function Stars(): ReactElement { vec2 st = vUv; float aspect = u_resolution.x / u_resolution.y; - vec2 distortedUv; - if (u_force < 0.001) { - distortedUv = st; - } else { - vec2 mouse = u_mouse / u_resolution; - mouse.y = 1.0 - mouse.y; - vec2 scaledSt = st * vec2(aspect, 1.0); - float dist = distance(scaledSt, mouse * vec2(aspect, 1.0)); - float distortion = u_force / max(dist, 0.0001); - distortedUv = st + (mouse - st) * distortion; - } - - vec4 stars = texture2D(u_texture, distortedUv); + vec4 stars = texture2D(u_texture, st); - vec2 nebUv = distortedUv * vec2(aspect, 1.0); + vec2 nebUv = st * vec2(aspect, 1.0); float t = u_time; float n1 = fbm(nebUv * 2.2 + vec2(t * 0.012, t * 0.008)); float n2 = fbm3(nebUv * 5.5 + vec2(31.7 - t * 0.018, 11.3 + t * 0.010)); @@ -235,56 +214,6 @@ function Stars(): ReactElement { } `; - const fragMobileSrc = ` - precision mediump float; - uniform vec2 u_resolution; - uniform sampler2D u_texture; - uniform float u_time; - varying vec2 vUv; - - ${noiseHelpers} - - void main() { - vec2 st = vUv; - float aspect = u_resolution.x / u_resolution.y; - - vec4 stars = texture2D(u_texture, st); - - vec2 nebUv = st * vec2(aspect, 1.0); - float t = u_time; - float n1 = fbm3(nebUv * 2.2 + vec2(t * 0.012, t * 0.008)); - float n2 = fbm3(nebUv * 5.5 + vec2(31.7 - t * 0.018, 11.3 + t * 0.010)); - float n3 = fbm3(nebUv * 3.8 + vec2(-7.9 + t * 0.014, 22.5 - t * 0.009)); - float n4 = fbm3(nebUv * 4.3 + vec2(54.1 + t * 0.007, -18.7 + t * 0.016)); - float n5 = fbm3(nebUv * 6.1 + vec2(-23.9 - t * 0.013, 47.2 - t * 0.006)); - - vec3 deep = vec3(0.10, 0.07, 0.22); - vec3 violet = vec3(0.35, 0.22, 0.60); - vec3 blueGrey = vec3(0.30, 0.45, 0.80); - vec3 coral = vec3(0.95, 0.65, 0.55); - vec3 magenta = vec3(0.85, 0.32, 0.65); - vec3 teal = vec3(0.15, 0.68, 0.78); - - vec3 nebulaColor = deep; - nebulaColor = mix(nebulaColor, violet, smoothstep(0.30, 0.80, n1)); - nebulaColor = mix(nebulaColor, blueGrey, smoothstep(0.55, 0.90, n2) * 0.65); - nebulaColor = mix(nebulaColor, teal, smoothstep(0.62, 0.92, n5) * 0.55); - nebulaColor = mix(nebulaColor, magenta, smoothstep(0.68, 0.92, n4) * 0.45); - nebulaColor = mix(nebulaColor, coral, smoothstep(0.72, 0.95, n3) * 0.40); - - float nebulaAlpha = 0.20 + n1 * 0.38; - - float starsA = smoothstep(0.02, 0.5, stars.a); - vec3 preRgb = - stars.rgb * starsA + - nebulaColor * nebulaAlpha * (1.0 - starsA); - float outAlpha = starsA + nebulaAlpha * (1.0 - starsA); - gl_FragColor = vec4(preRgb, outAlpha); - } - `; - - const fragSrc = hasHover ? fragDesktopSrc : fragMobileSrc; - function compile(type: number, src: string): WebGLShader { const sh = gl!.createShader(type)!; gl!.shaderSource(sh, src); @@ -315,9 +244,7 @@ function Stars(): ReactElement { gl.enableVertexAttribArray(aPosLoc); gl.vertexAttribPointer(aPosLoc, 2, gl.FLOAT, false, 0, 0); - const uMouseLoc = gl.getUniformLocation(program, "u_mouse"); const uResLoc = gl.getUniformLocation(program, "u_resolution"); - const uForceLoc = gl.getUniformLocation(program, "u_force"); const uTexLoc = gl.getUniformLocation(program, "u_texture"); const uTimeLoc = gl.getUniformLocation(program, "u_time"); const startTime = performance.now(); @@ -355,85 +282,34 @@ function Stars(): ReactElement { gl!.clear(gl!.COLOR_BUFFER_BIT); gl!.uniform2f(uResLoc, glCanvas.width, glCanvas.height); gl!.uniform1f(uTimeLoc, (performance.now() - startTime) / 1000); - if (hasHover) { - forceCurrent += (forceTarget - forceCurrent) * 0.1; - gl!.uniform2f(uMouseLoc, mouseX, mouseY); - gl!.uniform1f(uForceLoc, forceCurrent); - } gl!.drawArrays(gl!.TRIANGLES, 0, 6); requestAnimationFrame(animate); } - function handlePointerMove(e: PointerEvent) { - mouseX = e.clientX; - mouseY = e.clientY; - forceTarget = 0.08; - - if (blackHoleRef.current) { - blackHoleRef.current.style.transform = `translate3d(${e.clientX}px, ${e.clientY}px, 0) translate(-50%, -50%)`; - blackHoleRef.current.style.opacity = "1"; - } - } - - function handlePointerLeave() { - forceTarget = 0; - - if (blackHoleRef.current) { - blackHoleRef.current.style.opacity = "0"; - } - } - window.addEventListener("resize", () => { setCanvasSize(); initStars(); }); - if (hasHover) { - window.addEventListener("pointermove", handlePointerMove, { - passive: true, - }); - document.addEventListener("pointerleave", handlePointerLeave); - } - setCanvasSize(); initStars(); animate(); }, []); return ( - <> - - - - - + ); }