Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/Navigation.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
7 changes: 6 additions & 1 deletion src/components/WaveCanvas.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
timeUniformLocation,
resolutionUniformLocation,
mouseUniformLocation,
pointerUniformLocation,
positionBuffer,
transitionUniformLocation,
resizeCanvasToDisplaySize,
Expand All @@ -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;
Expand All @@ -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);

Expand Down
61 changes: 47 additions & 14 deletions src/lib/webgl/fragment.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
232 changes: 131 additions & 101 deletions src/lib/webgl/utils.js
Original file line number Diff line number Diff line change
@@ -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);
};
}