designpaca/apps/site/public/work/lumina-optics/index.html
Yun Chan e09d4efb1c
Some checks failed
ci / build (push) Has been cancelled
feat: strengthen design skill and refresh all showcases
2026-09-12 18:16:33 +09:00

409 lines
18 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" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Lumina Optics · Interactive Optical Study</title>
<meta
name="description"
content="An interactive optical study of refraction and dispersion. The lens profile, curves, and specifications are illustrative."
/>
<meta property="og:title" content="Lumina Optics · Interactive Optical Study" />
<meta
property="og:description"
content="An illustrative light path you can inspect and adjust."
/>
<meta property="og:type" content="website" />
<meta name="theme-color" content="#101214" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Sans+KR:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap"
/>
<link rel="stylesheet" href="./styles/main.css?v=20260912all" />
<link
rel="icon"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23101214'/%3E%3Cpath d='M6 16h8m8 0h4M14 16l10-7v14z' fill='none' stroke='%23BFE8EE' stroke-width='1.4'/%3E%3C/svg%3E"
/>
</head>
<body>
<a class="skip" href="#main">Skip to main content</a>
<header class="site-head">
<div class="wrap head-inner">
<a class="wordmark" href="#main">lumina<span class="wordmark-dot">·</span>optics</a>
<nav class="nav" aria-label="Sections">
<a href="#bench">Bench</a>
<a href="#anatomy">Anatomy</a>
<a href="#mtf">MTF</a>
<a href="#contact">Contact</a>
</nav>
</div>
</header>
<main id="main">
<section id="bench" class="hero" aria-labelledby="hero-h">
<div class="wrap hero-summary">
<div class="hero-text">
<p class="hero-kicker reveal">Interactive optical study</p>
<h1 id="hero-h" class="reveal" style="--delay: 60ms">
One beam,<br />bent honestly
</h1>
<p class="lead reveal" style="--delay: 120ms">
Move the prism to compare how refractive index and dispersion
redirect one beam. This view illustrates an optical model; it is
not a measured photograph, a product specification, or a lab result.
</p>
</div>
</div>
<div class="wrap bench-layout">
<div class="bench-shell">
<div class="bench-stage reveal">
<canvas
id="gl"
aria-label="Illustrative interactive prism: a white beam enters from the left and exits as a spectrum. Drag to rotate the concept model."
></canvas>
<img
class="bench-fallback"
src="./assets/prism-bench.webp"
alt="Illustrative glass prism on a dark optical surface splitting one collimated beam into a spectrum"
width="1440"
height="810"
/>
</div>
</div>
<aside class="bench-controls">
<div class="bench-ledger" role="group" aria-label="Bench configuration">
<dl>
<div>
<dt>illustrative beam reference</dt>
<dd>632.8 nm · collimated</dd>
</div>
<div>
<dt>variable</dt>
<dd>index + dispersion</dd>
</div>
<div>
<dt>view</dt>
<dd>interactive light study · static reference</dd>
</div>
</dl>
</div>
<div class="hud reveal" style="--delay: 160ms">
<div class="hud-row">
<div class="hud-row-head"><label for="ior">Index of refraction</label><output id="ior-out" for="ior">1.52</output></div>
<input type="range" id="ior" min="1.45" max="2.20" step="0.01" value="1.52" />
</div>
<div class="hud-row">
<div class="hud-row-head"><label for="disp">Dispersion (Abbe ↓)</label><output id="disp-out" for="disp">0.18</output></div>
<input type="range" id="disp" min="0" max="0.6" step="0.01" value="0.18" />
</div>
<p class="hud-note" id="hud-note">Drag the prism to rotate · illustrative light path at a 632.8 nm reference</p>
</div>
</aside>
</div>
</section>
<section id="anatomy" class="section" aria-labelledby="anatomy-h">
<div class="wrap two">
<figure class="two-photo reveal">
<img
src="./assets/lens-section.webp"
alt="Illustrative lens cutaway: a stack of glass elements in a metal barrel, edge-lit to show refraction paths"
width="900"
height="1200"
loading="lazy"
/>
<figcaption>L-85 concept cutaway · illustrative element layout</figcaption>
</figure>
<div class="two-text">
<h2 id="anatomy-h">Glass sets<br />the light path</h2>
<p>
A lens is a chain of refractions: every surface bends the path
before an image is made. The L-85 concept uses low-dispersion
elements to explain why a design can keep colours from spreading
across the frame.
</p>
<p class="muted">
The cutaway is a constructed reference image. Its element count
and spacing are illustrative, not a measurement of a commercial lens.
</p>
</div>
</div>
</section>
<section id="mtf" class="section band" aria-labelledby="mtf-h">
<div class="wrap mtf-grid">
<div class="mtf-text">
<h2 id="mtf-h">MTF, read with conditions</h2>
<p>
Contrast transfer at stated line pairs per millimetre gives a
clearer comparison than a slogan. These curves illustrate the
labels a measured chart needs; they are not optical-bench results.
</p>
<ul class="mtf-legend">
<li><span class="key key-solid" aria-hidden="true"></span>10 lp/mm · sagittal</li>
<li><span class="key key-solid-m" aria-hidden="true"></span>10 lp/mm · meridional</li>
<li><span class="key key-dash" aria-hidden="true"></span>30 lp/mm · sagittal</li>
<li><span class="key key-dash-m" aria-hidden="true"></span>30 lp/mm · meridional</li>
</ul>
</div>
<figure class="mtf-plot reveal">
<svg viewBox="0 0 560 320" role="img" aria-label="Illustrative MTF curves showing how contrast can be compared from the centre to the corner; this is not a measured result.">
<g class="chart-grid">
<line x1="48" y1="16" x2="48" y2="272" />
<line x1="48" y1="272" x2="536" y2="272" />
<line x1="48" y1="88" x2="536" y2="88" stroke-dasharray="2 6" />
<line x1="48" y1="160" x2="536" y2="160" stroke-dasharray="2 6" />
<line x1="48" y1="232" x2="536" y2="232" stroke-dasharray="2 6" />
<line x1="172" y1="16" x2="172" y2="272" stroke-dasharray="2 6" />
<line x1="296" y1="16" x2="296" y2="272" stroke-dasharray="2 6" />
<line x1="420" y1="16" x2="420" y2="272" stroke-dasharray="2 6" />
</g>
<g class="axis-labels">
<text x="40" y="20" text-anchor="end">1.0</text>
<text x="40" y="92" text-anchor="end">0.75</text>
<text x="40" y="164" text-anchor="end">0.5</text>
<text x="40" y="236" text-anchor="end">0.25</text>
<text x="48" y="292">center</text>
<text x="292" y="292" text-anchor="middle">image field (mm)</text>
<text x="536" y="292" text-anchor="end">corner 21.6</text>
</g>
<polyline class="mtf mtf-10s" points="48,32 172,36 296,44 420,60 536,76" />
<polyline class="mtf mtf-10m" points="48,34 172,40 296,52 420,72 536,96" />
<polyline class="mtf mtf-30s" points="48,84 172,96 296,120 420,160 536,196" />
<polyline class="mtf mtf-30m" points="48,90 172,108 296,140 420,186 536,232" />
</svg>
<figcaption>L-85 concept · f/1.8 · infinity focus · illustrative curve</figcaption>
</figure>
</div>
</section>
<section class="section" aria-labelledby="spec-h">
<div class="wrap">
<h2 id="spec-h" class="reveal">Reference specification</h2>
<div class="table-wrap reveal" style="--delay: 80ms">
<table>
<caption class="sr-only">L-85 concept specification — illustrative item, value, and condition</caption>
<thead>
<tr>
<th scope="col">Item</th>
<th scope="col" class="num">Illustrative value</th>
<th scope="col">Condition</th>
</tr>
</thead>
<tbody>
<tr><th scope="row">Focal length</th><td class="num">85 mm</td><td>concept reference</td></tr>
<tr><th scope="row">Aperture</th><td class="num">f/1.8 – f/16</td><td>concept reference</td></tr>
<tr><th scope="row">Elements</th><td class="num">7 in 6</td><td>illustrative layout</td></tr>
<tr><th scope="row">Close focus</th><td class="num">0.62 m</td><td>illustrative configuration</td></tr>
<tr><th scope="row">Filter</th><td class="num">Ø 62 mm</td><td>illustrative configuration</td></tr>
<tr><th scope="row">Weight</th><td class="num">438 g</td><td>illustrative configuration</td></tr>
</tbody>
</table>
</div>
</div>
</section>
<section id="contact" class="section band contact" aria-labelledby="contact-h">
<div class="wrap contact-grid">
<h2 id="contact-h">About this study</h2>
<p>
This is a concept page for reading an optical model. It does not
schedule sample loans, measurements, or MTF requests.
</p>
<a class="btn" href="#bench">Return to the light study</a>
</div>
</section>
</main>
<footer class="site-foot">
<div class="wrap foot-inner">
<p>lumina optics · optical study</p>
<p class="foot-meta">illustrative model · no commercial measurement service</p>
</div>
</footer>
<script type="module">
const canvas = document.getElementById("gl");
const fallback = document.querySelector(".bench-fallback");
const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches;
let gl = null;
try {
gl = canvas.getContext("webgl2", { antialias: true, alpha: true });
} catch {}
if (!gl || reduced) {
canvas.remove();
document.getElementById("hud-note").textContent =
reduced
? "Motion reduced — showing a static reference image instead."
: "Interactive view unavailable — showing a static reference image instead.";
} else {
fallback.remove();
const THREE = await import("https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js");
const renderer = new THREE.WebGLRenderer({ canvas, context: gl, antialias: true, alpha: true });
renderer.setClearColor(0x000000, 0);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(32, 1, 0.1, 50);
camera.position.set(0, 0, 9);
const prismGeo = new THREE.CylinderGeometry(2.1, 2.1, 2.6, 3, 1, false);
prismGeo.rotateX(Math.PI / 2);
prismGeo.rotateZ(Math.PI / 2);
const uniforms = {
uIor: { value: 1.52 },
uDisp: { value: 0.18 },
};
const prismMat = new THREE.ShaderMaterial({
uniforms,
transparent: true,
vertexShader: `
varying vec3 vN;
varying vec3 vView;
void main() {
vec4 mv = modelViewMatrix * vec4(position, 1.0);
vN = normalize(normalMatrix * normal);
vView = normalize(-mv.xyz);
gl_Position = projectionMatrix * mv;
}`,
fragmentShader: `
precision highp float;
varying vec3 vN;
varying vec3 vView;
void main() {
float ndv = abs(dot(normalize(vN), normalize(vView)));
float f = pow(1.0 - ndv, 3.0);
vec3 body = vec3(0.035, 0.082, 0.094) * (0.7 + ndv * 0.65);
vec3 rim = vec3(0.22, 0.70, 0.75) * (0.24 + f * 0.92);
float alpha = clamp(0.18 + f * 0.62, 0.0, 0.82);
gl_FragColor = vec4(body + rim, alpha);
}`,
side: THREE.DoubleSide,
});
const prism = new THREE.Mesh(prismGeo, prismMat);
const prismEdges = new THREE.LineSegments(
new THREE.EdgesGeometry(prismGeo, 8),
new THREE.LineBasicMaterial({ color: 0xbfe8ee, transparent: true, opacity: 0.78 }),
);
prismEdges.renderOrder = 2;
const prismGroup = new THREE.Group();
prismGroup.add(prism, prismEdges);
scene.add(prismGroup);
const beamGroup = new THREE.Group();
const mkBeam = (w, h, color1, color2, op) => {
const m = new THREE.Mesh(
new THREE.PlaneGeometry(w, h),
new THREE.ShaderMaterial({
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
uniforms: { uOp: { value: op }, uC1: { value: new THREE.Color(color1) }, uC2: { value: new THREE.Color(color2) } },
vertexShader: `varying vec2 vUv; void main(){ vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0); }`,
fragmentShader: `
varying vec2 vUv; uniform float uOp; uniform vec3 uC1; uniform vec3 uC2;
void main(){
float a = smoothstep(0.0, 0.12, vUv.x) * smoothstep(1.0, 0.88, vUv.x);
float b = smoothstep(0.0, 0.5, vUv.y) * smoothstep(1.0, 0.5, vUv.y);
vec3 c = mix(uC1, uC2, vUv.x);
gl_FragColor = vec4(c, a * b * uOp);
}`,
})
);
return m;
};
const beamIn = mkBeam(4.1, 0.12, 0xdfe8ec, 0xffffff, 0.92);
beamIn.position.set(-3.05, 0, -0.34);
beamGroup.add(beamIn);
const spectrumRays = [0x7379e8, 0x62c9e8, 0x89dbaa, 0xf0da72, 0xee9866].map((color) => {
const ray = mkBeam(3.9, 0.075, color, color, 0.92);
beamGroup.add(ray);
return ray;
});
scene.add(beamGroup);
function updateSpectrum() {
const spread = 0.11 + uniforms.uDisp.value * 0.56;
const bend = (uniforms.uIor.value - 1.45) * 0.26;
spectrumRays.forEach((ray, i) => {
const offset = i / (spectrumRays.length - 1) - 0.5;
ray.position.set(2.75, offset * spread * 2.4 + bend, -0.38);
ray.rotation.z = offset * spread * 0.76;
});
}
updateSpectrum();
let dragging = false, lastX = 0, rot = -0.08;
canvas.addEventListener("pointerdown", (e) => { dragging = true; lastX = e.clientX; canvas.setPointerCapture(e.pointerId); });
canvas.addEventListener("pointermove", (e) => {
if (!dragging) return;
const dx = e.clientX - lastX;
lastX = e.clientX;
rot += dx * 0.006;
renderFrame();
});
const end = () => (dragging = false);
canvas.addEventListener("pointerup", end);
canvas.addEventListener("pointercancel", end);
canvas.style.touchAction = "none";
const iorEl = document.getElementById("ior");
const dispEl = document.getElementById("disp");
const iorOut = document.getElementById("ior-out");
const dispOut = document.getElementById("disp-out");
const bind = (el, out, uni, fmt) =>
el.addEventListener("input", () => {
uni.value = parseFloat(el.value);
out.textContent = fmt(uni.value);
updateSpectrum();
renderFrame();
});
bind(iorEl, iorOut, uniforms.uIor, (v) => v.toFixed(2));
bind(dispEl, dispOut, uniforms.uDisp, (v) => v.toFixed(2));
const stage = canvas.parentElement;
function fit() {
const r = stage.getBoundingClientRect();
const h = Math.max(320, Math.min(r.height, 760));
const aspect = r.width / h;
renderer.setPixelRatio(Math.min(devicePixelRatio || 1, aspect < 1.25 ? 1.25 : 1.5));
renderer.setSize(r.width, h, false);
camera.aspect = aspect;
camera.position.z = aspect < 1.25 ? 15.5 : 9;
camera.updateProjectionMatrix();
renderFrame();
}
fit();
new ResizeObserver(fit).observe(stage);
function renderFrame() {
prismGroup.rotation.z = rot;
renderer.render(scene, camera);
}
renderFrame();
}
const io2 = new IntersectionObserver(
(entries) => {
for (const e of entries) {
if (!e.isIntersecting) continue;
e.target.classList.add("is-in");
io2.unobserve(e.target);
}
},
{ rootMargin: "0px 0px -8% 0px", threshold: 0.05 },
);
for (const el of document.querySelectorAll(".reveal")) io2.observe(el);
</script>
</body>
</html>