diff --git a/src/components/Navigation.svelte b/src/components/Navigation.svelte index ec3a2c9..e89d7d8 100644 --- a/src/components/Navigation.svelte +++ b/src/components/Navigation.svelte @@ -157,7 +157,7 @@ variant: "ghost", size: "sm", }), - "relative z-10 flex-1 rounded-full px-4 text-sm font-medium tracking-wide transition-colors duration-150 md:px-6 md:text-base max-lg:landscape:px-3 max-lg:landscape:py-1 max-lg:landscape:text-xs text-foreground/80 hover:!bg-transparent focus-visible:!bg-transparent active:!bg-transparent hover:!text-current cursor-pointer", + "relative z-10 flex-1 rounded-full px-4 text-sm font-medium tracking-wide transition-colors duration-150 md:px-6 md:text-base max-lg:landscape:px-3 max-lg:landscape:py-1 max-lg:landscape:text-xs text-foreground/80 hover:bg-transparent! focus-visible:bg-transparent active:bg-transparent! hover:text-current! cursor-pointer", isActive ? "text-primary-foreground" : "", )} data-route={link.href} diff --git a/src/components/WaveCanvas.svelte b/src/components/WaveCanvas.svelte index ff83790..ab6bd5c 100644 --- a/src/components/WaveCanvas.svelte +++ b/src/components/WaveCanvas.svelte @@ -83,6 +83,7 @@ timeUniformLocation, resolutionUniformLocation, mouseUniformLocation, + pointerUniformLocation, positionBuffer, transitionUniformLocation, resizeCanvasToDisplaySize, @@ -99,7 +100,8 @@ resizeCanvas(); const pos = [0, 0]; - cleanupPointers = setupEventListeners(pos, resizeCanvas); + const pointerState = { target: 0, value: 0 }; + cleanupPointers = setupEventListeners(pos, resizeCanvas, pointerState); let startTime = performance.now(); let currentTransition = 0; @@ -126,7 +128,10 @@ gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0); gl.uniform1f(timeUniformLocation, currentTime); gl.uniform2f(resolutionUniformLocation, gl.canvas.width, gl.canvas.height); + pointerState.value = + pointerState.value + (pointerState.target - pointerState.value) * 0.08; gl.uniform2f(mouseUniformLocation, pos[0], pos[1]); + gl.uniform1f(pointerUniformLocation, pointerState.value); gl.uniform1f(transitionUniformLocation, currentTransition); gl.drawArrays(gl.TRIANGLES, 0, 6); diff --git a/src/lib/webgl/fragment.glsl b/src/lib/webgl/fragment.glsl index a9f6240..124e104 100644 --- a/src/lib/webgl/fragment.glsl +++ b/src/lib/webgl/fragment.glsl @@ -4,6 +4,7 @@ uniform float u_time; uniform vec2 u_resolution; uniform vec2 u_mouse; uniform float u_transition; +uniform float u_pointer; const mat2 FBM_ROT = mat2(0.8, -0.6, 0.6, 0.8); @@ -20,6 +21,9 @@ const float WATER_BASELINE = 0.75; const float CURSOR_PUSH_SCALE = 0.06; const float CURSOR_SIGMA = 0.18; const float CURSOR_OFFSET_STRENGTH = 0.42; +const float CLOUD_CURSOR_SIGMA = 0.075; +const float CLOUD_ATTRACTION = 0.4; +const float CLOUD_DENSITY_BOOST = 0.38; const float FOAM_WIDTH = 0.04; const float MEDIUM_SECTION_END = 0.9935; const float GRADIENT_START = 0.915; @@ -88,25 +92,54 @@ float layeredWaves(vec2 uv, float frequencyMultiplier, float amplitudeMultiplier } vec3 paintSky(vec2 st, float waterSurface, float aspect) { - float skyBlend = smoothstep(0.28, 0.95, st.y); + float skyBlend = smoothstep(0.26, 0.95, st.y); vec3 skyBase = mix(SKY_BOTTOM, SKY_TOP, skyBlend); - vec2 skyUv = st; - skyUv.x *= aspect; - vec2 cloudFlow = skyUv * 2.6; + vec2 cloudFlow = st * 2.0; cloudFlow += vec2(u_time * 0.015, u_time * 0.006); - float cloudMacro = fbm(cloudFlow); - float cloudDetail = fbm(cloudFlow * 1.85 + vec2(12.7, -9.1)); - float clouds = clamp(cloudMacro * 0.65 + cloudDetail * 0.35, 0.0, 1.0); - - float cloudStructure = smoothstep(0.4, 0.8, clouds); - float topFade = smoothstep(0.58, 0.78, st.y); + vec2 screenUv = vec2(st.x / aspect, st.y); + vec2 mouseUv = vec2(u_mouse.x / u_resolution.x, 1.0 - (u_mouse.y / u_resolution.y)); + vec2 cloudDiff = screenUv - mouseUv; + float cloudInfluence = u_pointer * + exp(-(dot(cloudDiff, cloudDiff)) / (2.0 * CLOUD_CURSOR_SIGMA * CLOUD_CURSOR_SIGMA)); + cloudInfluence = clamp(cloudInfluence * 1.35, 0.0, 1.0); + cloudInfluence *= smoothstep(0.52, 0.82, st.y); + cloudFlow -= vec2(cloudDiff.x * aspect, cloudDiff.y) * (CLOUD_ATTRACTION * cloudInfluence); + + float cloudMacro = fbm(cloudFlow * 0.85); + float cloudDetail = fbm(cloudFlow * 2.4 + vec2(12.7, -9.1)); + float clouds = clamp(cloudMacro * 0.8 + cloudDetail * 0.2 + cloudInfluence * 0.12, 0.0, 1.0); + + float cloudStructure = smoothstep(0.26, 0.68, clouds); + cloudStructure = pow(cloudStructure, 1.05); + cloudStructure = clamp(cloudStructure * (1.15 + cloudInfluence * CLOUD_DENSITY_BOOST), 0.0, 1.0); + float topFade = smoothstep(0.45, 0.78, st.y); float horizonFade = smoothstep(1.02, 1.2, waterSurface); - float cloudAmount = cloudStructure * topFade * horizonFade; - - vec3 cloudColor = mix(skyBase, SKY_HIGHLIGHT, cloudAmount); - return mix(skyBase, cloudColor, topFade); + float cloudAmount = clamp(cloudStructure * topFade * horizonFade * 1.6, 0.0, 1.0); + + float cursorDetail = fbm(cloudFlow * 5.1 + vec2(4.7, -13.2)); + float cursorHighlight = max(0.0, cursorDetail - 0.5); + float cursorContrast = cursorHighlight * cloudInfluence * 0.9; + float cursorShade = cursorHighlight * cloudInfluence * 1.0; + cloudStructure = clamp(cloudStructure + cursorContrast, 0.0, 1.0); + cloudAmount = clamp(cloudAmount + cloudInfluence * 0.08, 0.0, 1.0); + + float veilNoise = fbm(cloudFlow * 3.6 + vec2(-6.4, 8.1)); + float cloudVeil = smoothstep(0.18, 0.55, veilNoise); + cloudVeil = pow(cloudVeil, 1.05); + cloudVeil = clamp(cloudVeil + cursorContrast * 0.65, 0.0, 1.0); + float veilAmount = + clamp(cloudVeil * topFade * horizonFade * 0.5 + cloudInfluence * 0.18, 0.0, 0.72); + + float cloudShadow = smoothstep(0.12, 0.55, clouds) * 0.28; + vec3 cloudBase = skyBase * (1.0 - cloudShadow); + vec3 cloudColor = mix(cloudBase, SKY_HIGHLIGHT, clamp(cloudStructure * 1.25, 0.0, 1.0)); + vec3 veilColor = mix(skyBase, SKY_HIGHLIGHT, 0.55); + vec3 skyWithVeil = mix(skyBase, veilColor, veilAmount); + skyWithVeil = clamp(skyWithVeil + vec3(cursorShade * 0.5), 0.0, 1.0); + cloudColor = clamp(cloudColor + vec3(cursorShade), 0.0, 1.0); + return mix(skyWithVeil, cloudColor, cloudAmount); } vec3 baseColor(vec2 st, float waterSurface, float aspect) { diff --git a/src/lib/webgl/utils.js b/src/lib/webgl/utils.js index 9b2fd37..7b954ef 100644 --- a/src/lib/webgl/utils.js +++ b/src/lib/webgl/utils.js @@ -1,118 +1,148 @@ export function createShader(gl, type, source) { - const shader = gl.createShader(type); - gl.shaderSource(shader, source); - gl.compileShader(shader); - - if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { - console.error(gl.getShaderInfoLog(shader)); - gl.deleteShader(shader); - return null; - } - return shader; + const shader = gl.createShader(type); + gl.shaderSource(shader, source); + gl.compileShader(shader); + + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + console.error(gl.getShaderInfoLog(shader)); + gl.deleteShader(shader); + return null; + } + return shader; } export function createProgram(gl, vertexShader, fragmentShader) { - const program = gl.createProgram(); - gl.attachShader(program, vertexShader); - gl.attachShader(program, fragmentShader); - gl.linkProgram(program); - - if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { - console.error(gl.getProgramInfoLog(program)); - gl.deleteProgram(program); - return null; - } - return program; + const program = gl.createProgram(); + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + console.error(gl.getProgramInfoLog(program)); + gl.deleteProgram(program); + return null; + } + return program; } export function setupWebGL(canvas) { - const gl = canvas.getContext("webgl"); - if (!gl) throw new Error("WebGL not supported"); + const gl = canvas.getContext("webgl"); + if (!gl) throw new Error("WebGL not supported"); - return { - gl, - render: function () {}, - }; + return { + gl, + render: function () {}, + }; } export function initializeWebGL(canvas, vertexShaderSource, fragmentShaderSource) { - const gl = canvas.getContext("webgl2"); - if (!gl) { - console.error("WebGL not supported"); - return null; - } - - const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource); - const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); - if (!vertexShader || !fragmentShader) return null; - - const program = createProgram(gl, vertexShader, fragmentShader); - if (!program) return null; - - const positionAttributeLocation = gl.getAttribLocation(program, "a_position"); - const timeUniformLocation = gl.getUniformLocation(program, "u_time"); - const resolutionUniformLocation = gl.getUniformLocation(program, "u_resolution"); - const mouseUniformLocation = gl.getUniformLocation(program, "u_mouse"); - - const positionBuffer = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); - const positions = [-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]; - gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW); - - const transitionUniformLocation = gl.getUniformLocation(program, "u_transition"); - - return { - gl, - program, - positionAttributeLocation, - timeUniformLocation, - resolutionUniformLocation, - mouseUniformLocation, - positionBuffer, - transitionUniformLocation, - }; + const gl = canvas.getContext("webgl2"); + if (!gl) { + console.error("WebGL not supported"); + return null; + } + + const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource); + const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); + if (!vertexShader || !fragmentShader) return null; + + const program = createProgram(gl, vertexShader, fragmentShader); + if (!program) return null; + + const positionAttributeLocation = gl.getAttribLocation(program, "a_position"); + const timeUniformLocation = gl.getUniformLocation(program, "u_time"); + const resolutionUniformLocation = gl.getUniformLocation(program, "u_resolution"); + const mouseUniformLocation = gl.getUniformLocation(program, "u_mouse"); + const pointerUniformLocation = gl.getUniformLocation(program, "u_pointer"); + + const positionBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); + const positions = [-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]; + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW); + + const transitionUniformLocation = gl.getUniformLocation(program, "u_transition"); + + return { + gl, + program, + positionAttributeLocation, + timeUniformLocation, + resolutionUniformLocation, + mouseUniformLocation, + pointerUniformLocation, + positionBuffer, + transitionUniformLocation, + }; } export function resizeCanvasToDisplaySize(canvas) { - const viewport = window.visualViewport; - const cssWidth = canvas.clientWidth || viewport?.width || window.innerWidth; - const cssHeight = canvas.clientHeight || viewport?.height || window.innerHeight; - const pixelRatio = window.devicePixelRatio || 1; - const displayWidth = Math.round(cssWidth * pixelRatio); - const displayHeight = Math.round(cssHeight * pixelRatio); - - if (canvas.width !== displayWidth || canvas.height !== displayHeight) { - canvas.width = displayWidth; - canvas.height = displayHeight; - } + const viewport = window.visualViewport; + const cssWidth = canvas.clientWidth || viewport?.width || window.innerWidth; + const cssHeight = canvas.clientHeight || viewport?.height || window.innerHeight; + const pixelRatio = window.devicePixelRatio || 1; + const displayWidth = Math.round(cssWidth * pixelRatio); + const displayHeight = Math.round(cssHeight * pixelRatio); + + if (canvas.width !== displayWidth || canvas.height !== displayHeight) { + canvas.width = displayWidth; + canvas.height = displayHeight; + } } -export function setupEventListeners(pos, resizeCanvas) { - const updatePosition = (clientX, clientY) => { - const ratio = window.devicePixelRatio || 1; - pos[0] = clientX * ratio; - pos[1] = clientY * ratio; - }; - - const handleMouseMove = (event) => { - updatePosition(event.clientX, event.clientY); - }; - const passiveMoveOptions = { passive: true }; - window.addEventListener("mousemove", handleMouseMove, passiveMoveOptions); - - const handleTouchMove = (event) => { - const touch = event.touches[0]; - if (touch) updatePosition(touch.clientX, touch.clientY); - }; - window.addEventListener("touchmove", handleTouchMove, passiveMoveOptions); - const handleResize = () => { - window.requestAnimationFrame(resizeCanvas); - }; - window.addEventListener("resize", handleResize); - - return () => { - window.removeEventListener("mousemove", handleMouseMove, passiveMoveOptions); - window.removeEventListener("touchmove", handleTouchMove, passiveMoveOptions); - window.removeEventListener("resize", handleResize); - }; +export function setupEventListeners(pos, resizeCanvas, pointerState) { + const updatePosition = (clientX, clientY) => { + const ratio = window.devicePixelRatio || 1; + pos[0] = clientX * ratio; + pos[1] = clientY * ratio; + }; + + const setPointerTarget = (value) => { + if (pointerState) pointerState.target = value; + }; + + const handleMouseMove = (event) => { + updatePosition(event.clientX, event.clientY); + setPointerTarget(1); + }; + const passiveMoveOptions = { passive: true }; + window.addEventListener("mousemove", handleMouseMove, passiveMoveOptions); + + const handleMouseLeave = () => { + setPointerTarget(0); + }; + window.addEventListener("mouseleave", handleMouseLeave); + window.addEventListener("blur", handleMouseLeave); + + const handleTouchMove = (event) => { + const touch = event.touches[0]; + if (touch) updatePosition(touch.clientX, touch.clientY); + setPointerTarget(1); + }; + window.addEventListener("touchmove", handleTouchMove, passiveMoveOptions); + const handleTouchStart = (event) => { + const touch = event.touches[0]; + if (touch) updatePosition(touch.clientX, touch.clientY); + setPointerTarget(1); + }; + window.addEventListener("touchstart", handleTouchStart, passiveMoveOptions); + const handleTouchEnd = () => { + setPointerTarget(0); + }; + window.addEventListener("touchend", handleTouchEnd, passiveMoveOptions); + window.addEventListener("touchcancel", handleTouchEnd, passiveMoveOptions); + const handleResize = () => { + window.requestAnimationFrame(resizeCanvas); + }; + window.addEventListener("resize", handleResize); + + return () => { + window.removeEventListener("mousemove", handleMouseMove, passiveMoveOptions); + window.removeEventListener("mouseleave", handleMouseLeave); + window.removeEventListener("blur", handleMouseLeave); + window.removeEventListener("touchmove", handleTouchMove, passiveMoveOptions); + window.removeEventListener("touchstart", handleTouchStart, passiveMoveOptions); + window.removeEventListener("touchend", handleTouchEnd, passiveMoveOptions); + window.removeEventListener("touchcancel", handleTouchEnd, passiveMoveOptions); + window.removeEventListener("resize", handleResize); + }; }