feat: strengthen design skill and refresh all showcases
Some checks failed
ci / build (push) Has been cancelled
Some checks failed
ci / build (push) Has been cancelled
This commit is contained in:
parent
69e5cc1efd
commit
e09d4efb1c
257 changed files with 8196 additions and 2374 deletions
27
apps/site/public/work/lumina-optics/design.md
Normal file
27
apps/site/public/work/lumina-optics/design.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Lumina Optics design.md
|
||||
|
||||
## 브리프
|
||||
|
||||
- 무엇을: IOR와 분산을 조작하는 가상 광학 시험대.
|
||||
- 누구에게: 광선 모델의 조건과 결과물을 함께 읽으려는 방문자.
|
||||
- 제약: WebGL/정적 폴백, 기존 슬라이더와 가상·측정 아님 표기를 유지한다.
|
||||
|
||||
## 결정표
|
||||
|
||||
| 축 | 관찰과 적용 범위 | 결정·역할 토큰 | 기각 조건 |
|
||||
|---|---|---|---|
|
||||
| 구도 | 기준 1440 화면에서 큰 프리즘이 제목과 조작을 폴드 아래로 밀었다. | 제목·한계 설명을 상단에 두고 `9:4` 캔버스/제어 프레임을 배치한다. `--container`, `--space-4`, `--space-6`. | 캔버스가 제어보다 큰 면적을 차지해 둘 중 하나가 첫 화면에서 사라지면 재조정한다. |
|
||||
| 타입 | 제목은 모델의 의미, 모노는 조건과 조작값을 읽게 해야 한다. | `--step-3` 제목, `--step--1` 시험대 원장과 제어 보조문을 유지한다. | 실제 긴 라벨이 320px 또는 200% 확대에서 제어를 밀면 폭·간격을 수정한다. |
|
||||
| 색 | 스펙트럼만 광학 결과를 나타내며 UI 색은 보조여야 한다. | `--surface`, `--surface-raised`, `--accent-stroke`을 프레임·포커스·입력에만 쓴다. | UI 색이 캔버스의 분산 광선보다 먼저 읽히면 채도를 줄인다. |
|
||||
|
||||
## 근거와 경계
|
||||
|
||||
[art-direction.md](../../../../../packages/skill/references/art-direction.md)의 Hofmann 관련 구도·군집 관찰을 제목/작업면 분리에만 적용했다. [typography.md](../../../../../packages/skill/references/typography.md)의 Ruder 관련 실제 카피·수치 proof 범위를 타입 계층에만 적용했다. [visual-design-lineage.md](../../../../../packages/skill/references/visual-design-lineage.md)의 Albers 관련 색 맥락은 캔버스와 UI 표면의 대비 확인에만 적용했다. 이 세 자료는 성능·이해도 법칙으로 사용하지 않는다.
|
||||
|
||||
[trustworthy-showcases.md](../../../../../packages/skill/references/trustworthy-showcases.md)의 진실 범위에 따라 모델·사양·곡선의 illustrative 표기를 가까운 공통 표면에 유지한다. 캔버스 안에는 텍스트를 굽지 않는다.
|
||||
|
||||
## 2026-09-12 현재 검증 기록
|
||||
|
||||
- `verify-final` 18개 묶음은 16개가 즉시 통과했다. 전체 묶음의 invariant·visual 검사는 Gaon·Dure를 대상으로 하며, 나머지 2개는 수정 뒤 release 독립 재실행에서 ALL PASS·ALL MATCH였다.
|
||||
- root는 28개 모바일, 21개 데스크톱, 7개 하위 경로와 30개 앱 상태를 직접 검토해 수용했다. stress 112건은 hard failure 0이며, 가로 넘침 4건은 targeted fix 뒤 390px 재측정에서 0이다.
|
||||
- 이 기록은 렌더·계약 범위의 현재 결과다. 모든 폭·키보드 경로·인쇄 등 개별 기능을 완전 재실행했다는 뜻이 아니며, 참여자 과업 효과도 미측정이다.
|
||||
|
|
@ -3,15 +3,15 @@
|
|||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Lumina Optics · Refraction Bench</title>
|
||||
<title>Lumina Optics · Interactive Optical Study</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="A WebGL bench where a single beam meets a prism: refraction and dispersion computed per frame. Drag the prism, change the index."
|
||||
content="An interactive optical study of refraction and dispersion. The lens profile, curves, and specifications are illustrative."
|
||||
/>
|
||||
<meta property="og:title" content="Lumina Optics · Refraction Bench" />
|
||||
<meta property="og:title" content="Lumina Optics · Interactive Optical Study" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="One beam, one prism, per-frame refraction."
|
||||
content="An illustrative light path you can inspect and adjust."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta name="theme-color" content="#101214" />
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
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=2" />
|
||||
<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"
|
||||
|
|
@ -45,40 +45,54 @@
|
|||
</header>
|
||||
|
||||
<main id="main">
|
||||
<!--
|
||||
The bench. The product is light behaviour, so the hero IS the instrument:
|
||||
one beam, one prism, per-frame refraction. Drag rotates; sliders change
|
||||
the index and the dispersion — they are the two honest knobs a bench
|
||||
has. Everything degrades to a static photograph.
|
||||
-->
|
||||
<section id="bench" class="hero" aria-labelledby="hero-h">
|
||||
<div class="bench-stage reveal">
|
||||
<canvas
|
||||
id="gl"
|
||||
aria-label="Interactive prism: a white beam enters from the left and exits as a spectrum. Drag to rotate the prism."
|
||||
></canvas>
|
||||
<img
|
||||
class="bench-fallback"
|
||||
src="./assets/prism-bench.webp"
|
||||
alt="A glass prism on a dark optical breadboard splitting one collimated beam into a spectrum"
|
||||
width="1440"
|
||||
height="810"
|
||||
/>
|
||||
</div>
|
||||
<div class="wrap hero-inner">
|
||||
<div class="wrap hero-summary">
|
||||
<div class="hero-text">
|
||||
<p class="hero-kicker reveal">Refraction bench · WebGL</p>
|
||||
<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">
|
||||
No photograph on this screen is doing the work — the prism is
|
||||
being refracted right now, per frame, on your GPU. Drag it. Change
|
||||
the index. The spectrum you see is the same arithmetic lens
|
||||
designers use, minus the glasses.
|
||||
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 class="hud reveal" style="--delay: 160ms">
|
||||
</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" />
|
||||
|
|
@ -87,56 +101,48 @@
|
|||
<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 · beam is collimated at 632.8 nm reference</p>
|
||||
</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>
|
||||
|
||||
<!--
|
||||
Anatomy — cut lens photography. Real glass, not renders.
|
||||
-->
|
||||
<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="Cutaway of a precision lens: a stack of glass elements in a metal barrel, edge-lit so each surface shows a refraction line"
|
||||
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 cutaway · 7 elements in 6 groups</figcaption>
|
||||
<figcaption>L-85 concept cutaway · illustrative element layout</figcaption>
|
||||
</figure>
|
||||
<div class="two-text">
|
||||
<h2 id="anatomy-h">Glass decides<br />before software does</h2>
|
||||
<h2 id="anatomy-h">Glass sets<br />the light path</h2>
|
||||
<p>
|
||||
A lens is a chain of refractions, and every surface answers before
|
||||
any firmware runs. Our L-85 uses two low-dispersion elements to
|
||||
keep the spectrum you just dragged from fanning out across the
|
||||
frame — dispersion corrected in glass, not in a profile.
|
||||
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 above is a physical section, milled for measurement.
|
||||
Element count and spacing are measured from it, not from the
|
||||
drawing.
|
||||
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>
|
||||
|
||||
<!--
|
||||
MTF — the only curve that argues. Static, thin, honest:
|
||||
measured at infinity, full aperture, both field directions.
|
||||
-->
|
||||
<section id="mtf" class="section band" aria-labelledby="mtf-h">
|
||||
<div class="wrap mtf-grid">
|
||||
<div class="mtf-text">
|
||||
<h2 id="mtf-h">MTF, not adjectives</h2>
|
||||
<h2 id="mtf-h">MTF, read with conditions</h2>
|
||||
<p>
|
||||
“Tack sharp” is not a specification. Contrast transfer at given
|
||||
line pairs per millimetre is. These curves are measured on an
|
||||
optical bench at full aperture — sagittal and meridional, out to
|
||||
the corner.
|
||||
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>
|
||||
|
|
@ -146,7 +152,7 @@
|
|||
</ul>
|
||||
</div>
|
||||
<figure class="mtf-plot reveal">
|
||||
<svg viewBox="0 0 560 320" role="img" aria-label="MTF curves: contrast stays above 0.85 at 10 lp/mm across the field; at 30 lp/mm it falls from 0.75 at center to 0.42 at the corner.">
|
||||
<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" />
|
||||
|
|
@ -166,76 +172,62 @@
|
|||
<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>
|
||||
<!-- 10 lp/mm sagittal: high, gentle fall -->
|
||||
<polyline class="mtf mtf-10s" points="48,32 172,36 296,44 420,60 536,76" />
|
||||
<!-- 10 lp/mm meridional -->
|
||||
<polyline class="mtf mtf-10m" points="48,34 172,40 296,52 420,72 536,96" />
|
||||
<!-- 30 lp/mm sagittal -->
|
||||
<polyline class="mtf mtf-30s" points="48,84 172,96 296,120 420,160 536,196" />
|
||||
<!-- 30 lp/mm meridional -->
|
||||
<polyline class="mtf mtf-30m" points="48,90 172,108 296,140 420,186 536,232" />
|
||||
</svg>
|
||||
<figcaption>L-85 · f/1.8 · infinity focus · bench-measured 2026-07</figcaption>
|
||||
<figcaption>L-85 concept · f/1.8 · infinity focus · illustrative curve</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!--
|
||||
Spec — with conditions.
|
||||
-->
|
||||
<section class="section" aria-labelledby="spec-h">
|
||||
<div class="wrap">
|
||||
<h2 id="spec-h" class="reveal">Specification</h2>
|
||||
<h2 id="spec-h" class="reveal">Reference specification</h2>
|
||||
<div class="table-wrap reveal" style="--delay: 80ms">
|
||||
<table>
|
||||
<caption class="sr-only">L-85 specification — item, value, condition</caption>
|
||||
<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">Value</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>nominal · ±1.5% at infinity</td></tr>
|
||||
<tr><th scope="row">Aperture</th><td class="num">f/1.8 – f/16</td><td>9-blade · near-circular</td></tr>
|
||||
<tr><th scope="row">Elements</th><td class="num">7 in 6</td><td>2 LD · measured from cutaway</td></tr>
|
||||
<tr><th scope="row">Close focus</th><td class="num">0.62 m</td><td>1:7.2 magnification</td></tr>
|
||||
<tr><th scope="row">Filter</th><td class="num">Ø 62 mm</td><td>brass, front-threaded</td></tr>
|
||||
<tr><th scope="row">Weight</th><td class="num">438 g</td><td>without caps, with hood</td></tr>
|
||||
<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>
|
||||
|
||||
<!-- Contact -->
|
||||
<section id="contact" class="section band contact" aria-labelledby="contact-h">
|
||||
<div class="wrap contact-grid">
|
||||
<h2 id="contact-h">Bench visits</h2>
|
||||
<h2 id="contact-h">About this study</h2>
|
||||
<p>
|
||||
We measure loaner samples for free and publish the curves —
|
||||
manufacturers welcome or not. Bench time and MTF requests:
|
||||
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="mailto:bench@lumina.optics">bench@lumina.optics</a>
|
||||
<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</p>
|
||||
<p class="foot-meta">measurements reproducible on request · bench cal. 2026-06</p>
|
||||
<p>lumina optics · optical study</p>
|
||||
<p class="foot-meta">illustrative model · no commercial measurement service</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="module">
|
||||
/*
|
||||
Refraction bench — three.js, one prism, two honest uniforms.
|
||||
Budget note: three.module.js ≈ 165KB gzip. 예산 상향 — 이 페이지의
|
||||
제품이 WebGL 자체다(데모 브리프 예산 상향 허용).
|
||||
폴백: WebGL 실패 시 사진이 보이고 캔버스는 뜨지 않는다(이미 마크업에 있음).
|
||||
*/
|
||||
const canvas = document.getElementById("gl");
|
||||
const fallback = document.querySelector(".bench-fallback");
|
||||
const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
|
|
@ -243,32 +235,30 @@
|
|||
let gl = null;
|
||||
try {
|
||||
gl = canvas.getContext("webgl2", { antialias: true, alpha: true });
|
||||
} catch (e) { /* fall through */ }
|
||||
} catch {}
|
||||
|
||||
if (!gl) {
|
||||
if (!gl || reduced) {
|
||||
canvas.remove();
|
||||
document.getElementById("hud-note").textContent =
|
||||
"WebGL unavailable — showing the bench photograph instead.";
|
||||
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 DPR = Math.min(devicePixelRatio || 1, 1.5);
|
||||
renderer.setPixelRatio(DPR);
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(32, 1, 0.1, 50);
|
||||
camera.position.set(0, 0, 9);
|
||||
|
||||
// ----- prism: triangular cross-section (CylinderGeometry, 3 segments)
|
||||
const prismGeo = new THREE.CylinderGeometry(2.1, 2.1, 2.6, 3, 1, false);
|
||||
prismGeo.rotateX(Math.PI / 2); // axis toward camera
|
||||
prismGeo.rotateZ(Math.PI / 2); // flat edge up to the beam
|
||||
prismGeo.rotateX(Math.PI / 2);
|
||||
prismGeo.rotateZ(Math.PI / 2);
|
||||
|
||||
const uniforms = {
|
||||
uTime: { value: 0 },
|
||||
uIor: { value: 1.52 },
|
||||
uDisp: { value: 0.18 },
|
||||
};
|
||||
|
|
@ -276,7 +266,7 @@
|
|||
const prismMat = new THREE.ShaderMaterial({
|
||||
uniforms,
|
||||
transparent: true,
|
||||
vertexShader: /* glsl */`
|
||||
vertexShader: `
|
||||
varying vec3 vN;
|
||||
varying vec3 vView;
|
||||
void main() {
|
||||
|
|
@ -285,44 +275,31 @@
|
|||
vView = normalize(-mv.xyz);
|
||||
gl_Position = projectionMatrix * mv;
|
||||
}`,
|
||||
fragmentShader: /* glsl */`
|
||||
fragmentShader: `
|
||||
precision highp float;
|
||||
varying vec3 vN;
|
||||
varying vec3 vView;
|
||||
uniform float uIor;
|
||||
uniform float uDisp;
|
||||
|
||||
// wavelength ramp: violet → cyan → yellow → red
|
||||
vec3 spectrum(float t) {
|
||||
t = clamp(t, 0.0, 1.0);
|
||||
return clamp(
|
||||
vec3(
|
||||
smoothstep(0.30, 0.00, t) + smoothstep(0.70, 1.00, t),
|
||||
smoothstep(0.00, 0.35, t) - smoothstep(0.60, 1.00, t),
|
||||
smoothstep(0.00, 0.30, t) - smoothstep(0.45, 0.75, t)
|
||||
), 0.0, 1.0);
|
||||
}
|
||||
|
||||
void main() {
|
||||
float ndv = abs(dot(normalize(vN), normalize(vView)));
|
||||
// fresnel edge (schlick): rim is where refraction lives
|
||||
float f = pow(1.0 - ndv, 3.0);
|
||||
// refraction deflection grows with index
|
||||
float bend = (uIor - 1.0) * ndv;
|
||||
// dispersion: split position across the rim into wavelengths
|
||||
float d = uDisp * bend;
|
||||
vec3 col = spectrum(0.5 + (1.0 - ndv) * (0.6 + d * 4.0));
|
||||
// glass body: near-black with faint interior glow
|
||||
vec3 body = vec3(0.02, 0.025, 0.03) * (0.4 + ndv);
|
||||
vec3 rim = col * f * 1.35;
|
||||
float alpha = clamp(f * 0.95 + 0.06, 0.0, 1.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);
|
||||
scene.add(prism);
|
||||
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);
|
||||
|
||||
// ----- beam: incoming line + dispersed fan (additive planes)
|
||||
const beamGroup = new THREE.Group();
|
||||
const mkBeam = (w, h, color1, color2, op) => {
|
||||
const m = new THREE.Mesh(
|
||||
|
|
@ -345,24 +322,35 @@
|
|||
);
|
||||
return m;
|
||||
};
|
||||
const beamIn = mkBeam(9, 0.10, 0xdfe8ec, 0xffffff, 0.55);
|
||||
beamIn.position.set(0, 1.45, -0.2);
|
||||
const beamIn = mkBeam(4.1, 0.12, 0xdfe8ec, 0xffffff, 0.92);
|
||||
beamIn.position.set(-3.05, 0, -0.34);
|
||||
beamGroup.add(beamIn);
|
||||
const fan = mkBeam(9, 1.7, 0x8fd8e8, 0xf2b060, 0.30);
|
||||
fan.position.set(-0.4, 0.35, -0.25);
|
||||
fan.rotation.z = 0.085;
|
||||
beamGroup.add(fan);
|
||||
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);
|
||||
|
||||
// ----- interaction
|
||||
let dragging = false, lastX = 0, vel = 0.0016, rot = 0;
|
||||
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;
|
||||
vel = dx * 0.0006;
|
||||
renderFrame();
|
||||
});
|
||||
const end = () => (dragging = false);
|
||||
canvas.addEventListener("pointerup", end);
|
||||
|
|
@ -377,38 +365,32 @@
|
|||
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));
|
||||
|
||||
// ----- sizing
|
||||
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 = r.width / h;
|
||||
camera.aspect = aspect;
|
||||
camera.position.z = aspect < 1.25 ? 15.5 : 9;
|
||||
camera.updateProjectionMatrix();
|
||||
renderFrame();
|
||||
}
|
||||
fit();
|
||||
new ResizeObserver(fit).observe(stage);
|
||||
|
||||
// ----- loop: paused offscreen, static under reduced motion
|
||||
let visible = true, t0 = performance.now();
|
||||
const io = new IntersectionObserver((en) => { visible = en[0].isIntersecting; });
|
||||
io.observe(canvas);
|
||||
|
||||
function frame() {
|
||||
if (!visible) { requestAnimationFrame(frame); return; }
|
||||
const t = (performance.now() - t0) / 1000;
|
||||
uniforms.uTime.value = t;
|
||||
if (!dragging) { vel *= 0.98; rot += Math.max(0.0016, Math.abs(vel)) * Math.sign(vel || 1); }
|
||||
prism.rotation.z = rot;
|
||||
beamGroup.rotation.z = Math.sin(t * 0.4) * 0.01;
|
||||
function renderFrame() {
|
||||
prismGroup.rotation.z = rot;
|
||||
renderer.render(scene, camera);
|
||||
if (!reduced) requestAnimationFrame(frame);
|
||||
}
|
||||
frame(); // reduced-motion: renders exactly one static frame
|
||||
renderFrame();
|
||||
}
|
||||
|
||||
const io2 = new IntersectionObserver(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@import url("./tokens.css");
|
||||
@import url("./tokens.css?v=20260912all");
|
||||
|
||||
/* ==========================================================================
|
||||
Lumina Optics — 광학 시험대. 색은 캔버스 안에서만.
|
||||
|
|
@ -145,15 +145,63 @@ a {
|
|||
R2 원칙(design.md): 3D 위에 본문을 얹지 않는다. ─────────────────────── */
|
||||
|
||||
.hero {
|
||||
padding-block: var(--space-4);
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.hero-summary {
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
.bench-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 9fr) minmax(17rem, 4fr);
|
||||
gap: var(--space-4);
|
||||
align-items: stretch;
|
||||
}
|
||||
.bench-shell {
|
||||
min-width: 0;
|
||||
background:
|
||||
radial-gradient(72% 90% at 50% 22%, rgba(127, 185, 196, 0.06), transparent 66%),
|
||||
var(--surface);
|
||||
}
|
||||
.bench-stage {
|
||||
position: relative;
|
||||
height: clamp(320px, 56vh, 560px);
|
||||
height: clamp(20rem, 28vw, 22rem);
|
||||
background:
|
||||
radial-gradient(80% 90% at 50% 30%, rgba(127, 185, 196, 0.05), transparent 60%),
|
||||
radial-gradient(70% 88% at 50% 48%, rgba(127, 185, 196, 0.10), transparent 68%),
|
||||
var(--surface);
|
||||
border-bottom: 1px solid var(--line);
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
.bench-ledger {
|
||||
background: var(--surface-raised);
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
.bench-ledger dl {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.bench-ledger dl > div {
|
||||
min-width: 0;
|
||||
padding: 0.85rem var(--space-3) 0.95rem;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
.bench-ledger dl > div:first-child {
|
||||
border-top: 0;
|
||||
}
|
||||
.bench-ledger dt,
|
||||
.bench-ledger dd {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.6875rem;
|
||||
letter-spacing: 0.045em;
|
||||
}
|
||||
.bench-ledger dt {
|
||||
color: var(--ink-faint);
|
||||
}
|
||||
.bench-ledger dd {
|
||||
margin-top: 0.2rem;
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
#gl,
|
||||
.bench-fallback {
|
||||
|
|
@ -169,13 +217,6 @@ a {
|
|||
#gl:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
.hero-inner {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
|
||||
gap: var(--space-5) var(--space-7);
|
||||
align-items: start;
|
||||
padding-block: var(--space-6);
|
||||
}
|
||||
.hero-kicker {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--step--1);
|
||||
|
|
@ -187,8 +228,8 @@ a {
|
|||
margin-top: var(--space-3);
|
||||
}
|
||||
.hud {
|
||||
background: color-mix(in srgb, var(--surface) 78%, transparent);
|
||||
backdrop-filter: blur(8px);
|
||||
margin-top: var(--space-3);
|
||||
background: var(--surface-raised);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
padding: var(--space-4);
|
||||
|
|
@ -392,7 +433,9 @@ tbody td:not(.num) {
|
|||
max-width: 34em;
|
||||
}
|
||||
.btn {
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 44px;
|
||||
border: 1px solid var(--accent-stroke);
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
|
|
@ -451,13 +494,11 @@ tbody td:not(.num) {
|
|||
/* ── 반응형 ───────────────────────────────────────────────────────────── */
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.hero-inner {
|
||||
position: static;
|
||||
.bench-layout {
|
||||
grid-template-columns: 1fr;
|
||||
padding-block: var(--space-5);
|
||||
}
|
||||
.bench-stage {
|
||||
height: clamp(300px, 48vh, 460px);
|
||||
height: clamp(320px, 82vw, 540px);
|
||||
}
|
||||
.two,
|
||||
.mtf-grid,
|
||||
|
|
@ -469,6 +510,20 @@ tbody td:not(.num) {
|
|||
padding-block: var(--space-6);
|
||||
}
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.bench-ledger dl {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.bench-ledger dl > div,
|
||||
.bench-ledger dl > div:first-child {
|
||||
padding-inline: var(--space-3);
|
||||
border-left: 0;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
.bench-ledger dl > div:first-child {
|
||||
border-top: 0;
|
||||
}
|
||||
}
|
||||
@media (max-width: 560px) {
|
||||
.head-inner {
|
||||
flex-direction: column;
|
||||
|
|
@ -477,6 +532,9 @@ tbody td:not(.num) {
|
|||
}
|
||||
.nav {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
column-gap: var(--space-4);
|
||||
row-gap: var(--space-2);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
--surface-raised: #15181b;
|
||||
--ink: #e8ecee;
|
||||
--ink-muted: #9ba4a9;
|
||||
--ink-faint: #6b7479;
|
||||
--ink-faint: #849096;
|
||||
--line: #23272b;
|
||||
--line-strong: #394046;
|
||||
--accent: #bfe8ee; /* 레이저 라인(밝게 — 텍스트용) */
|
||||
|
|
@ -48,8 +48,8 @@
|
|||
|
||||
--radius: 2px;
|
||||
|
||||
--container: 1120px;
|
||||
--pad-inline: clamp(1.25rem, 1rem + 2.5vw, 4rem);
|
||||
--container: 1240px;
|
||||
--pad-inline: clamp(1.25rem, 1rem + 2.5vw, 5rem);
|
||||
|
||||
--dur-quick: 160ms;
|
||||
--dur-normal: 420ms;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue