Some checks are pending
ci / build (push) Waiting to run
Measured convergence before: split hero 13/19, top-bar nav 19/19, one-line notice footer 14/19, body Pretendard 17/21, 10 similarity pairs above 0.78. Each showcase got a pre-assigned hero family, nav form, footer form, type pair, background, accent hue, radius, motion grammar and material from outputs/rework-2026-10-06/plan.md; direction cards and the chosen reason are recorded in every design.md. After: 0 similarity warnings, 19 distinct display faces, 7 hero families, 5 nav forms, 5 footer forms, no scroll reveals, 5 showcases with SVG filter materials. Example contact values replace bracket placeholders under the existing fictional-brand notices. Thumbnails, OG images, card copy and three e2e assertions updated; dead CSS in dure-enrollment and hannam-jari removed; similarity.mjs learns the new font families.
384 lines
18 KiB
HTML
384 lines
18 KiB
HTML
<!doctype html>
|
||
<html lang="ko">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>Lumina Optics · L-85 85mm f/1.8 렌즈</title>
|
||
<meta
|
||
name="description"
|
||
content="가상 광학 브랜드 Lumina Optics의 85mm f/1.8 렌즈 L-85 소개 페이지. 프리즘 시험대에서 굴절률과 분산을 바꿔 보고, 렌즈 구성·MTF·사양을 확인할 수 있습니다."
|
||
/>
|
||
<meta property="og:title" content="Lumina Optics · L-85 85mm f/1.8 렌즈" />
|
||
<meta property="og:description" content="85mm f/1.8 렌즈 L-85의 굴절 시험대, 렌즈 구성, MTF 곡선과 사양." />
|
||
<meta property="og:type" content="website" />
|
||
<meta property="og:image" content="./og.png" />
|
||
<meta name="twitter:card" content="summary_large_image" />
|
||
<meta name="theme-color" content="#a6a9a9" />
|
||
|
||
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
|
||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/sun-typeface/SUIT@2/fonts/variable/woff2/SUIT-Variable.css" />
|
||
<link rel="stylesheet" href="./styles/main.css?v=20261002" />
|
||
<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' rx='16' fill='%23A6A9A9'/%3E%3Ccircle cx='16' cy='16' r='9' fill='none' stroke='%230D0F10' stroke-width='2.4'/%3E%3Cpath d='M2 16h28' stroke='%23D4231A' stroke-width='2'/%3E%3C/svg%3E"
|
||
/>
|
||
</head>
|
||
|
||
<body>
|
||
<a class="skip" href="#main">본문으로 건너뛰기</a>
|
||
|
||
<header class="site-head">
|
||
<div class="wrap head-inner">
|
||
<a class="wordmark" href="#main"><span class="wordmark-lens" aria-hidden="true"></span>Lumina Optics</a>
|
||
<nav class="nav" aria-label="섹션">
|
||
<a href="#bench">시험대</a>
|
||
<a href="#anatomy">단면</a>
|
||
<a href="#mtf">MTF</a>
|
||
<a href="#spec">사양</a>
|
||
</nav>
|
||
</div>
|
||
</header>
|
||
<div class="scope-note">
|
||
<p class="wrap">가상 광학 브랜드의 쇼케이스입니다. 렌즈 L-85의 구성·곡선·사양은 설명을 위해 만든 값이며 실측이 아닙니다.</p>
|
||
</div>
|
||
|
||
<main id="main">
|
||
<section id="bench" class="hero" aria-labelledby="hero-h">
|
||
<div class="wrap hero-grid">
|
||
<p class="hero-kicker">L-85 85mm f/1.8 · 굴절 시험대</p>
|
||
<h1 id="hero-h">굴절률과 분산을<br />직접 바꿔 보세요</h1>
|
||
<p class="lead">
|
||
굴절률을 올리면 빛이 더 꺾이고, 분산을 키우면 색 사이가 더 벌어집니다.
|
||
프리즘을 끌어 돌리거나 아래 슬라이더 두 개로 값을 바꿔 보세요.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="wrap bench">
|
||
<div class="bench-stage">
|
||
<canvas
|
||
id="gl"
|
||
aria-label="프리즘 시험대: 왼쪽에서 들어온 흰 빛이 프리즘을 지나 스펙트럼으로 갈라집니다. 끌어서 프리즘을 돌릴 수 있습니다."
|
||
></canvas>
|
||
<img
|
||
class="bench-fallback"
|
||
src="./assets/prism-bench.webp"
|
||
alt="어두운 시험대 위 유리 프리즘이 평행광 한 줄을 스펙트럼으로 나누는 참고 이미지"
|
||
width="1440"
|
||
height="810"
|
||
/>
|
||
<p class="stage-label" aria-hidden="true"><span>입사광 632.8 nm</span><span>평행광</span></p>
|
||
</div>
|
||
<div class="rail" role="group" aria-label="시험대 조작">
|
||
<div class="control">
|
||
<div class="control-head"><label for="ior">굴절률 n</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" />
|
||
<p class="control-hint">크라운 유리 1.52 · 고굴절 플린트 1.9 이상</p>
|
||
</div>
|
||
<div class="control">
|
||
<div class="control-head"><label for="disp">분산</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" />
|
||
<p class="control-hint">값이 클수록 아베수가 낮은 유리</p>
|
||
</div>
|
||
<p class="hud-note" id="hud-note">프리즘을 좌우로 끌면 입사각이 바뀝니다.</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="anatomy" class="section" aria-labelledby="anatomy-h">
|
||
<div class="wrap anatomy-grid">
|
||
<figure class="lens-figure">
|
||
<div class="lens-disc">
|
||
<img
|
||
src="./assets/lens-section.webp"
|
||
alt="금속 경통 안에 유리 요소가 층층이 쌓인 렌즈 단면, 가장자리 조명으로 굴절 경로가 드러난 참고 이미지"
|
||
width="900"
|
||
height="1200"
|
||
loading="lazy"
|
||
/>
|
||
</div>
|
||
<figcaption>L-85 렌즈 단면 · 6군 7매</figcaption>
|
||
</figure>
|
||
<div class="anatomy-text">
|
||
<h2 id="anatomy-h">6군 7매 구성과<br />렌즈별 역할</h2>
|
||
<p>
|
||
빛은 렌즈 면을 하나 지날 때마다 조금씩 꺾이고, 마지막 면을 지나
|
||
센서에 상을 맺습니다. L-85는 저분산 유리 두 장으로 화면 가장자리의
|
||
색 번짐을 줄였습니다.
|
||
</p>
|
||
<ol class="element-list">
|
||
<li><b>1–2</b><span>앞 군 · 빛을 모으는 볼록 렌즈</span></li>
|
||
<li><b>3–4</b><span>저분산 접합 · 색수차 보정</span></li>
|
||
<li><b>5</b><span>조리개 뒤 렌즈 · 상면 평탄화</span></li>
|
||
<li><b>6–7</b><span>뒤 군 · 초점 거리 결정</span></li>
|
||
</ol>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="mtf" class="section" aria-labelledby="mtf-h">
|
||
<div class="wrap">
|
||
<div class="section-head">
|
||
<p class="index">02</p>
|
||
<h2 id="mtf-h">MTF 곡선과 측정 조건</h2>
|
||
<p>MTF는 선쌍 밀도와 방향을 함께 적어야 다른 렌즈와 비교할 수 있습니다. 아래 곡선은 f/1.8 개방, 무한 초점 기준입니다.</p>
|
||
</div>
|
||
<div class="mtf-panel">
|
||
<figure class="mtf-plot">
|
||
<svg viewBox="0 0 560 320" role="img" aria-label="L-85의 MTF 곡선: 중심에서 모서리로 갈수록 대비가 낮아지며, 30 lp/mm 곡선이 10 lp/mm보다 빨리 떨어집니다.">
|
||
<g class="chart-grid">
|
||
<line x1="48" y1="88" x2="536" y2="88" />
|
||
<line x1="48" y1="160" x2="536" y2="160" />
|
||
<line x1="48" y1="232" x2="536" y2="232" />
|
||
<line x1="172" y1="16" x2="172" y2="272" />
|
||
<line x1="296" y1="16" x2="296" y2="272" />
|
||
<line x1="420" y1="16" x2="420" y2="272" />
|
||
<line class="axis" x1="48" y1="16" x2="48" y2="272" />
|
||
<line class="axis" x1="48" y1="272" x2="536" y2="272" />
|
||
</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="296">중심</text>
|
||
<text x="292" y="296" text-anchor="middle">상 높이 (mm)</text>
|
||
<text x="536" y="296" text-anchor="end">모서리 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 · f/1.8 · 무한 초점</figcaption>
|
||
</figure>
|
||
<div>
|
||
<ul class="mtf-legend">
|
||
<li><span class="key" aria-hidden="true"></span>10 lp/mm · 방사 방향</li>
|
||
<li><span class="key key-m" aria-hidden="true"></span>10 lp/mm · 접선 방향</li>
|
||
<li><span class="key key-dash" aria-hidden="true"></span>30 lp/mm · 방사 방향</li>
|
||
<li><span class="key key-m key-dash" aria-hidden="true"></span>30 lp/mm · 접선 방향</li>
|
||
</ul>
|
||
<p class="mtf-read">
|
||
두 방향 곡선이 모서리에서 벌어질수록 점이 한쪽으로 늘어져 찍힙니다.
|
||
L-85는 상 높이 15 mm까지 두 곡선 차이를 0.1 이내로 맞췄습니다.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="spec" class="section" aria-labelledby="spec-h">
|
||
<div class="wrap">
|
||
<div class="section-head">
|
||
<p class="index">03</p>
|
||
<h2 id="spec-h">L-85 사양</h2>
|
||
<p>위 시험대와 단면도에 쓴 것과 같은 설계값입니다.</p>
|
||
</div>
|
||
<div class="table-wrap">
|
||
<table class="spec-table">
|
||
<caption class="sr-only">L-85 사양 — 항목, 값(예시), 기준</caption>
|
||
<thead>
|
||
<tr>
|
||
<th scope="col">항목</th>
|
||
<th scope="col">값(예시)</th>
|
||
<th scope="col">기준</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr><th scope="row">초점 거리</th><td class="num">85 mm</td><td>무한 초점</td></tr>
|
||
<tr><th scope="row">조리개</th><td class="num">f/1.8–16</td><td>9엽 원형 조리개</td></tr>
|
||
<tr><th scope="row">렌즈 구성</th><td class="num">6군 7매</td><td>저분산 2매 포함</td></tr>
|
||
<tr><th scope="row">최단 초점</th><td class="num">0.62 m</td><td>센서면 기준</td></tr>
|
||
<tr><th scope="row">필터 지름</th><td class="num">62 mm</td><td>앞 군 나사</td></tr>
|
||
<tr><th scope="row">무게</th><td class="num">438 g</td><td>후드·캡 제외</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="closing" aria-labelledby="closing-h">
|
||
<div class="wrap closing-grid">
|
||
<h2 id="closing-h">굴절률을 바꿔 가며<br />다시 비교해 보세요</h2>
|
||
<a class="btn" href="#bench">시험대로 돌아가기</a>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
|
||
<footer class="site-foot">
|
||
<div class="wrap foot-inner">
|
||
<p>Lumina Optics · L-85 85mm f/1.8</p>
|
||
<p>designpaca 쇼케이스</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
|
||
? "움직임 줄이기 설정을 따라 정지한 참고 이미지를 보여 줍니다."
|
||
: "이 브라우저에서는 3D 시험대를 열 수 없어 정지한 참고 이미지를 보여 줍니다.";
|
||
} 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, 0xff6a5a, 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 : 11;
|
||
// 넓은 화면은 조작 레일이 상자 아래쪽을 덮으므로 장면을 위로 올린다.
|
||
camera.position.y = aspect < 1.25 ? 0 : -0.7;
|
||
camera.updateProjectionMatrix();
|
||
renderFrame();
|
||
}
|
||
fit();
|
||
new ResizeObserver(fit).observe(stage);
|
||
|
||
function renderFrame() {
|
||
prismGroup.rotation.z = rot;
|
||
renderer.render(scene, camera);
|
||
}
|
||
renderFrame();
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|