wa2k.com/www/index.html

386 lines
No EOL
16 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>WA2000</title>
<link type="text/css" rel="stylesheet" href="./style.css">
</head>
<body>
<div class="banner">
<img src="banner.png" alt="banner" />
</div>
<!-- SVG filter definition — hidden, just provides the filter for Chrome -->
<svg id="liquid-glass-svg" xmlns="http://www.w3.org/2000/svg" style="position:absolute;width:0;height:0;overflow:hidden;">
<defs>
<filter id="liquid-glass-nav" x="0" y="0" width="100%" height="100%" color-interpolation-filters="sRGB">
<feImage id="nav-disp-img" result="disp_map" />
<feDisplacementMap in="SourceGraphic" in2="disp_map" id="nav-disp-filter" xChannelSelector="R" yChannelSelector="G" result="refracted" />
<feImage id="nav-spec-img" result="spec_map" />
<feBlend in="refracted" in2="spec_map" mode="screen" result="with_specular" />
<feComposite in="with_specular" in2="SourceGraphic" operator="atop" />
</filter>
<filter id="liquid-glass" x="0" y="0" width="100%" height="100%" color-interpolation-filters="sRGB">
<!-- Load the displacement map image -->
<feImage id="displacement-map-img" result="disp_map" />
<!-- Apply displacement to the backdrop (SourceGraphic here = the backdrop) -->
<feDisplacementMap
in="SourceGraphic"
in2="disp_map"
id="displacement-map-filter"
xChannelSelector="R"
yChannelSelector="G"
result="refracted"
/>
<!-- Load the specular highlight image -->
<feImage id="specular-map-img" result="spec_map" />
<!-- Blend specular on top of the refracted backdrop using screen mode -->
<feBlend in="refracted" in2="spec_map" mode="screen" result="with_specular" />
<!-- Clip result to the original element shape -->
<feComposite in="with_specular" in2="SourceGraphic" operator="atop" />
</filter>
</defs>
</svg>
<!-- Liquid glass searchbar — sits above the card, expands on click -->
<div class="nav-wrap">
<div class="nav-pill" id="nav-pill">
<div class="nav-pill-bar" id="nav-pill-bar">
<span class="nav-logo">WA2000</span>
<svg class="nav-chevron" id="nav-chevron" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<polyline points="5,7 10,13 15,7" stroke="rgba(255,255,255,0.7)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div class="nav-dropdown" id="nav-dropdown">
<div class="nav-divider"></div>
<a href="#" class="nav-link">Home</a>
<a href="#" class="nav-link">About</a>
<a href="#" class="nav-link">Gallery</a>
<a href="#" class="nav-link">Contact</a>
</div>
</div>
</div>
<div class="card" id="card">
<h1>waff waff!</h1>
<div class="divider"></div>
<p>tiny text!!!</p>
<p>wuff</p>
</div>
<script>
// ─── Configuration ────────────────────────────────────────────────────────────
const CONFIG = {
bezelWidth: 0.18, // Fraction of the smaller dimension used for the curved rim — wider = bigger refraction band
glassThickness: 3.5, // Optical depth multiplier — higher = rays bend further at the edges
ior: 1.65, // Index of refraction — real glass is ~1.5; slightly above for a more dramatic bend
scaleRatio: 2.5, // Final multiplier on pixel displacement — scales the whole effect up or down
specularAngle: -55, // Degrees of the light source direction — negative = upper-left
specularOpacity: 0.9, // Peak alpha of the specular highlight — higher = brighter rim
specularSaturation: 6, // Unused in painting but kept for future colour-shift use
borderRadius: 50, // Must match CSS border-radius in pixels — curves the refraction band at corners
};
// ─── Maths helpers ────────────────────────────────────────────────────────────
/** Convex squircle surface — Apple-style soft bezel profile */
function surfaceHeight(t) {
// t: 0 = outer edge of bezel, 1 = where the flat interior begins
// Squircle formula: softer than a circle arc, keeps refraction gradients smooth when stretched to a rectangle
return Math.pow(1 - Math.pow(1 - t, 4), 0.25);
}
/** Numerical derivative of the surface function at t — gives the surface slope */
function surfaceNormal(t) {
const d = 0.001; // Tiny step for finite-difference approximation
const y1 = surfaceHeight(Math.max(0, t - d)); // Sample just before t
const y2 = surfaceHeight(Math.min(1, t + d)); // Sample just after t
const derivative = (y2 - y1) / (2 * d); // Central difference = slope at t
return { x: -derivative, y: 1 }; // Rotate 90°: tangent → normal pointing inward
}
/** SnellDescartes refraction — returns the direction a ray travels after entering the glass */
function refract(incidentDir, normal, n1, n2) {
const nLen = Math.hypot(normal.x, normal.y); // Length of normal vector
const nx = normal.x / nLen; // Normalise X component
const ny = normal.y / nLen; // Normalise Y component
const ratio = n1 / n2; // Ratio of refractive indices (air/glass)
const cosI = -(incidentDir.x * nx + incidentDir.y * ny); // Cosine of angle of incidence
const sinT2 = ratio * ratio * (1 - cosI * cosI); // sin²(refraction angle) via Snell's law
if (sinT2 > 1) return null; // Total internal reflection — ray can't exit, skip this sample
const cosT = Math.sqrt(1 - sinT2); // Cosine of refraction angle
return {
x: ratio * incidentDir.x + (ratio * cosI - cosT) * nx, // Refracted ray X direction
y: ratio * incidentDir.y + (ratio * cosI - cosT) * ny, // Refracted ray Y direction
};
}
// ─── Rounded rect helpers ─────────────────────────────────────────────────────
/**
* Given a pixel (px, py) inside a rounded rectangle of size (width x height)
* with corner radius r, returns:
* dist — how far the pixel is from the nearest border edge (positive = inside)
* nx,ny — the inward-pointing unit normal at that nearest border point
*
* This is the single source of truth for both the displacement and specular maps.
*/
function roundedRectInfo(px, py, width, height, r) {
// Clamp radius so it can't exceed half the shortest side
const radius = Math.min(r, width / 2, height / 2);
// Corner arc centres (one per corner, in absolute pixel coords)
const corners = [
{ cx: radius, cy: radius }, // top-left
{ cx: width - radius, cy: radius }, // top-right
{ cx: radius, cy: height - radius }, // bottom-left
{ cx: width - radius, cy: height - radius }, // bottom-right
];
// Which corner zone is this pixel in?
const inCorner =
(px < radius || px > width - radius) &&
(py < radius || py > height - radius);
let dist, nx, ny;
if (inCorner) {
// Find the nearest corner arc centre
let best = null;
let bestDist = Infinity;
for (const c of corners) {
const d = Math.hypot(px - c.cx, py - c.cy);
if (d < bestDist) { bestDist = d; best = c; }
}
// Distance from the arc = radius - distance from arc centre
dist = radius - bestDist;
// Inward normal points from pixel toward the arc centre
const len = bestDist || 1;
nx = (best.cx - px) / len;
ny = (best.cy - py) / len;
} else {
// Flat side — nearest edge is whichever straight side is closest
const dLeft = px;
const dRight = width - 1 - px;
const dTop = py;
const dBottom = height - 1 - py;
const minD = Math.min(dLeft, dRight, dTop, dBottom);
dist = minD;
if (minD === dLeft) { nx = 1; ny = 0; }
else if (minD === dRight) { nx = -1; ny = 0; }
else if (minD === dTop) { nx = 0; ny = 1; }
else { nx = 0; ny = -1; }
}
return { dist, nx, ny };
}
// ─── Displacement map generation ─────────────────────────────────────────────
function buildDisplacementMap(width, height) {
const bezel = CONFIG.bezelWidth * Math.min(width, height);
const samples = 128;
// ── Step 1: Pre-calculate displacement magnitudes along one radius ──
const magnitudes = new Float32Array(samples);
const incident = { x: 0, y: 1 };
for (let i = 0; i < samples; i++) {
const t = i / (samples - 1);
const normal = surfaceNormal(t);
const refracted = refract(incident, normal, 1.0, CONFIG.ior);
magnitudes[i] = refracted
? (refracted.x - incident.x) * CONFIG.glassThickness * bezel
: 0;
}
const maxDisp = Math.max(...magnitudes) || 1;
// ── Step 2: Paint every pixel ──
const data = new Uint8ClampedArray(width * height * 4);
for (let py = 0; py < height; py++) {
for (let px = 0; px < width; px++) {
const idx = (py * width + px) * 4;
const { dist, nx, ny } = roundedRectInfo(px, py, width, height, CONFIG.borderRadius);
let dispX = 0, dispY = 0;
if (dist >= 0 && dist < bezel) {
const t = dist / bezel;
const sampleIdx = Math.min(samples - 1, Math.floor(t * (samples - 1)));
const mag = magnitudes[sampleIdx] / maxDisp;
dispX = nx * mag;
dispY = ny * mag;
}
data[idx] = Math.round(128 + dispX * 127);
data[idx + 1] = Math.round(128 + dispY * 127);
data[idx + 2] = 128;
data[idx + 3] = 255;
}
}
return {
imageData: new ImageData(data, width, height),
maxDisplacement: maxDisp * CONFIG.scaleRatio,
};
}
// ─── Specular highlight map generation ───────────────────────────────────────
function buildSpecularMap(width, height) {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
const bezel = CONFIG.bezelWidth * Math.min(width, height);
const lightAngleRad = (CONFIG.specularAngle * Math.PI) / 180;
const lightDir = { x: Math.cos(lightAngleRad), y: Math.sin(lightAngleRad) };
const imageData = ctx.createImageData(width, height);
const data = imageData.data;
for (let py = 0; py < height; py++) {
for (let px = 0; px < width; px++) {
const idx = (py * width + px) * 4;
const { dist, nx, ny } = roundedRectInfo(px, py, width, height, CONFIG.borderRadius);
let intensity = 0;
if (dist >= 0 && dist < bezel) {
const t = dist / bezel;
const dot = nx * lightDir.x + ny * lightDir.y;
const rim = Math.max(0, dot);
const slope = Math.abs(surfaceNormal(t).x);
const raw = rim * slope * (1 - t);
intensity = Math.pow(raw, 0.5);
}
data[idx] = Math.round(235 * intensity);
data[idx + 1] = Math.round(245 * intensity);
data[idx + 2] = Math.round(255 * intensity);
data[idx + 3] = Math.round(CONFIG.specularOpacity * 255 * intensity);
}
}
ctx.putImageData(imageData, 0, 0);
return canvas.toDataURL();
}
// ─── ImageData → data URL ─────────────────────────────────────────────────────
function imageDataToDataURL(imageData) {
const canvas = document.createElement('canvas'); // Temporary offscreen canvas
canvas.width = imageData.width;
canvas.height = imageData.height;
canvas.getContext('2d').putImageData(imageData, 0, 0); // Write pixel data into canvas
return canvas.toDataURL(); // Export as PNG data URL for SVG feImage
}
// ─── Apply filter to card ─────────────────────────────────────────────────────
function applyLiquidGlass() {
const card = document.getElementById('card');
const rect = card.getBoundingClientRect(); // Get the card's actual rendered size in pixels
const w = Math.round(rect.width);
const h = Math.round(rect.height);
if (w === 0 || h === 0) return; // Guard: don't run before layout is ready
const { imageData, maxDisplacement } = buildDisplacementMap(w, h); // Generate refraction map
const dispDataURL = imageDataToDataURL(imageData); // Convert to data URL for feImage
const specularDataURL = buildSpecularMap(w, h); // Generate specular highlight map
const dispImg = document.getElementById('displacement-map-img'); // feImage for displacement
const specImg = document.getElementById('specular-map-img'); // feImage for specular
const dispFilter = document.getElementById('displacement-map-filter'); // feDisplacementMap element
dispImg.setAttribute('href', dispDataURL); // Load displacement map into SVG filter
dispImg.setAttribute('width', w); // Match filter image size to card width
dispImg.setAttribute('height', h); // Match filter image size to card height
specImg.setAttribute('href', specularDataURL); // Load specular map into SVG filter
specImg.setAttribute('width', w);
specImg.setAttribute('height', h);
dispFilter.setAttribute('scale', maxDisplacement); // Tell feDisplacementMap the real-pixel scale of the map
// Only overwrite backdrop-filter in Chrome — Firefox ignores SVG filters here
// and would lose its CSS fallback blur if we overwrote card.style.backdropFilter
const isChrome = /Chrome/.test(navigator.userAgent) && !/Edg|Firefox/.test(navigator.userAgent);
if (isChrome) {
card.style.backdropFilter = 'url(#liquid-glass) blur(3px) saturate(1.8) brightness(1.05)'; // Full chain: refraction + blur + colour boost
card.style.webkitBackdropFilter = 'url(#liquid-glass) blur(3px) saturate(1.8) brightness(1.05)'; // Webkit prefix for older Chrome builds
}
}
window.addEventListener('load', applyLiquidGlass); // Run once the page has fully laid out
window.addEventListener('resize', applyLiquidGlass); // Re-run if the card size changes (e.g. window resize)
function applyNavGlass() {
const pill = document.getElementById('nav-pill');
const rect = pill.getBoundingClientRect();
const w = Math.round(rect.width);
const h = Math.round(rect.height);
if (w === 0 || h === 0) return;
const savedBezel = CONFIG.bezelWidth;
const savedScale = CONFIG.scaleRatio;
CONFIG.bezelWidth = 0.12;
CONFIG.scaleRatio = 1.8;
const { imageData, maxDisplacement } = buildDisplacementMap(w, h);
const dispDataURL = imageDataToDataURL(imageData);
const specularDataURL = buildSpecularMap(w, h);
CONFIG.bezelWidth = savedBezel;
CONFIG.scaleRatio = savedScale;
document.getElementById('nav-disp-img').setAttribute('href', dispDataURL);
document.getElementById('nav-disp-img').setAttribute('width', w);
document.getElementById('nav-disp-img').setAttribute('height', h);
document.getElementById('nav-spec-img').setAttribute('href', specularDataURL);
document.getElementById('nav-spec-img').setAttribute('width', w);
document.getElementById('nav-spec-img').setAttribute('height', h);
document.getElementById('nav-disp-filter').setAttribute('scale', maxDisplacement);
const isChrome = /Chrome/.test(navigator.userAgent) && !/Edg|Firefox/.test(navigator.userAgent);
if (isChrome) {
pill.style.backdropFilter = 'url(#liquid-glass-nav) blur(3px) saturate(1.8) brightness(1.05)';
pill.style.webkitBackdropFilter = 'url(#liquid-glass-nav) blur(3px) saturate(1.8) brightness(1.05)';
}
}
const navPill = document.getElementById('nav-pill');
const navBar = document.getElementById('nav-pill-bar');
navBar.addEventListener('click', () => {
const isOpen = navPill.classList.contains('open');
navPill.classList.toggle('open', !isOpen);
setTimeout(applyNavGlass, 320);
});
document.addEventListener('click', (e) => {
if (!navPill.contains(e.target)) {
navPill.classList.remove('open');
setTimeout(applyNavGlass, 320);
}
});
window.addEventListener('load', applyNavGlass);
window.addEventListener('resize', applyNavGlass);
</script>
</body>
</html>