절대배치인데도 배경이 화면 가운데 561px 띠로만 깔렸다.
.section 은 subgrid 이고 `.section > * { grid-column: main }` 이 걸려 있는데,
절대배치 요소라도 그리드 영역이 잡히면 그 영역이 컨테이닝 블록이 된다.
position: absolute 는 그리드에서 배치를 면제해주지 않는다.
그리고 overflow: visible 이라 위아래 14% 여유분이 이웃 섹션으로 넘쳤다 —
조판 섹션의 종이가 프리플라이트 하단에 밝은 회색 띠로 나타났다.
.bg-photo { grid-column: full }
.has-bg { overflow: clip }
띠 폭 기준으로 잡았던 투명도도 다시 올렸다
사례 0.3 -> 0.5 · 프리플라이트 0.55 -> 0.8
재료 0.5 -> 0.6 · 설치 0.4 -> 0.55
1440px/390px 실렌더: 배경 5개 모두 전체 폭, 가로 스크롤 0.
화면 밖 섹션의 배경은 아직 로드되지 않는다 — lazy 가 의도대로 작동한다.
206 lines
7.3 KiB
Text
206 lines
7.3 KiB
Text
---
|
|
import BgPhoto from "./BgPhoto.astro";
|
|
import type { Content } from "../i18n/content";
|
|
|
|
interface Props { c: Content["materials"]; }
|
|
const { c } = Astro.props;
|
|
---
|
|
|
|
{/*
|
|
레이아웃 패밀리: 레이어드 캔버스.
|
|
배경은 three.js 셰이더 플레인, 전경은 SVG 필터를 실제로 먹인 타일 3장.
|
|
둘 다 설명이 아니라 지금 이 화면에서 돌고 있는 것이다.
|
|
*/}
|
|
<section class="section materials has-bg" aria-labelledby="mat-h">
|
|
<BgPhoto opacity={0.6} />
|
|
<h2 id="mat-h" class="reveal">{c.h2}</h2>
|
|
<p class="lead mat-lead reveal">{c.lead}</p>
|
|
|
|
<div class="full stage glass par-slow">
|
|
<div class="stage-fallback" aria-hidden="true"></div>
|
|
{/* GPU 가 만드는 재질. WebGL 이 없으면 위 그라디언트가 그대로 결과물이다 */}
|
|
<canvas id="mat-canvas" class="stage-canvas" aria-hidden="true"></canvas>
|
|
|
|
<ul class="tiles">
|
|
{
|
|
c.tiles.map((t, i) => (
|
|
<li class:list={["tile", "reveal", i % 2 === 0 ? "par-slow" : "par-fast"]} style={`--reveal-delay:${i * 80}ms`}>
|
|
<div class={`swatch swatch-${t.id}`} aria-hidden="true"></div>
|
|
<h3 class="tile-name">{t.name}</h3>
|
|
<p class="tile-how muted">{t.how}</p>
|
|
<p class="tile-cost mono">{t.cost}</p>
|
|
</li>
|
|
))
|
|
}
|
|
</ul>
|
|
</div>
|
|
|
|
<p class="mat-note muted reveal">{c.note}</p>
|
|
</section>
|
|
|
|
{/* 필터 정의. 화면에 그려지지 않고 참조만 된다 */}
|
|
<svg class="visually-hidden" aria-hidden="true" focusable="false">
|
|
<defs>
|
|
<!-- 종이: 미세한 프랙탈 노이즈를 표면에 얹는다 -->
|
|
<filter id="f-grain" x="0" y="0" width="100%" height="100%">
|
|
<feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="3" result="n" />
|
|
<feColorMatrix in="n" type="saturate" values="0" result="g" />
|
|
<feComponentTransfer in="g" result="soft">
|
|
<feFuncA type="linear" slope="0.42" />
|
|
</feComponentTransfer>
|
|
<feComposite in="SourceGraphic" in2="soft" operator="over" />
|
|
</filter>
|
|
|
|
<!-- 유리: 난류로 만든 변위 지도를 굴절에 쓴다. 필터 영역을 넓혀 가장자리 잘림을 막는다 -->
|
|
<filter id="f-glass" x="-12%" y="-12%" width="124%" height="124%"
|
|
color-interpolation-filters="sRGB">
|
|
<feTurbulence type="fractalNoise" baseFrequency="0.012 0.02" numOctaves="2" seed="7" result="warp" />
|
|
<feDisplacementMap in="SourceGraphic" in2="warp" scale="26"
|
|
xChannelSelector="R" yChannelSelector="G" result="bent" />
|
|
<feGaussianBlur in="bent" stdDeviation="0.4" />
|
|
</filter>
|
|
|
|
<!-- 인쇄: 명도를 두 색 사이로 다시 매핑한다 -->
|
|
<filter id="f-duotone" x="0" y="0" width="100%" height="100%"
|
|
color-interpolation-filters="sRGB">
|
|
<feColorMatrix type="matrix" result="mono"
|
|
values="0.33 0.33 0.33 0 0
|
|
0.33 0.33 0.33 0 0
|
|
0.33 0.33 0.33 0 0
|
|
0 0 0 1 0" />
|
|
<feComponentTransfer in="mono">
|
|
<feFuncR type="table" tableValues="0.05 0.06 0.42" />
|
|
<feFuncG type="table" tableValues="0.09 0.42 0.66" />
|
|
<feFuncB type="table" tableValues="0.08 0.39 0.60" />
|
|
</feComponentTransfer>
|
|
</filter>
|
|
</defs>
|
|
</svg>
|
|
|
|
<style>
|
|
.mat-lead { margin-block: var(--space-4) var(--space-6); }
|
|
.mat-note { margin-top: var(--space-5); font-size: var(--step-0); }
|
|
|
|
.stage {
|
|
position: relative;
|
|
isolation: isolate;
|
|
max-width: 1180px;
|
|
width: 100%;
|
|
margin-inline: auto;
|
|
padding: var(--space-5) var(--space-4);
|
|
border-radius: var(--radius-frame);
|
|
overflow: hidden;
|
|
}
|
|
.stage-fallback,
|
|
.stage-canvas {
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: -1;
|
|
width: 100%;
|
|
height: 100%;
|
|
display: block;
|
|
}
|
|
.stage-fallback {
|
|
background:
|
|
radial-gradient(120% 90% at 12% 8%, color-mix(in oklab, var(--accent) 22%, transparent), transparent 60%),
|
|
radial-gradient(90% 80% at 88% 92%, color-mix(in oklab, var(--accent) 14%, transparent), transparent 62%),
|
|
var(--surface-raised);
|
|
}
|
|
|
|
.stage-canvas { opacity: 0; transition: opacity var(--dur-slow) var(--ease-soft); }
|
|
.stage-canvas.is-live { opacity: 1; }
|
|
|
|
/* 3개 항목에 3개 셀. 빈 칸이 생기지 않고 균등 3열도 아니다 */
|
|
.tiles {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
display: grid;
|
|
gap: var(--space-4);
|
|
grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
|
|
grid-template-areas:
|
|
"big top"
|
|
"big bottom";
|
|
}
|
|
.tile:nth-child(1) { grid-area: big; }
|
|
.tile:nth-child(2) { grid-area: top; }
|
|
.tile:nth-child(3) { grid-area: bottom; }
|
|
/* 큰 셀은 더 넓은 화면을 갖는다 */
|
|
.tile:nth-child(1) .swatch { aspect-ratio: 16 / 11; }
|
|
.tile:nth-child(n + 2) .swatch { aspect-ratio: 16 / 6; }
|
|
.tile { display: grid; gap: var(--space-2); align-content: start; }
|
|
|
|
/* 필터를 먹일 대상. 같은 도형에 서로 다른 재질을 입힌다 */
|
|
.swatch {
|
|
aspect-ratio: 4 / 3;
|
|
border-radius: var(--radius-sm);
|
|
overflow: hidden;
|
|
background:
|
|
repeating-linear-gradient(
|
|
58deg,
|
|
var(--accent) 0 14px,
|
|
color-mix(in oklab, var(--accent) 55%, var(--surface)) 14px 28px
|
|
);
|
|
}
|
|
.swatch-grain { filter: url(#f-grain); }
|
|
.swatch-glass { filter: url(#f-glass); }
|
|
.swatch-duotone { filter: url(#f-duotone); }
|
|
|
|
/* 필터를 지원하지 않거나 저사양이면 줄무늬만 남는다. 그래도 성립한다 */
|
|
@supports not (filter: url(#f-grain)) {
|
|
.swatch-grain, .swatch-glass, .swatch-duotone { filter: none; }
|
|
}
|
|
|
|
.tile-name { font-size: var(--step-1); margin-top: var(--space-1); }
|
|
.tile-how { margin: 0; font-size: var(--step-0); }
|
|
.tile-cost { margin: 0; color: var(--accent); }
|
|
|
|
@media (max-width: 760px) {
|
|
.tiles { grid-template-columns: 1fr; grid-template-areas: "big" "top" "bottom"; }
|
|
.tile:nth-child(n + 2) .swatch { aspect-ratio: 5 / 4; }
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
// 무대 배경. 유리 표면을 지나는 빛의 굴절을 흉내낸다.
|
|
// 히어로와 같은 모듈, 같은 three 청크를 쓴다. 새로 받는 것은 없다.
|
|
import { mountShaderPlane, GLSL_NOISE } from "../lib/shader-plane";
|
|
|
|
const canvas = document.getElementById("mat-canvas") as HTMLCanvasElement | null;
|
|
if (canvas) {
|
|
mountShaderPlane({
|
|
canvas,
|
|
speed: 0.7,
|
|
colors: { uA: "--accent", uB: "--glow-2" },
|
|
fragment: `
|
|
precision mediump float;
|
|
varying vec2 vUv;
|
|
uniform float uTime;
|
|
uniform float uAspect;
|
|
uniform vec3 uA;
|
|
uniform vec3 uB;
|
|
${GLSL_NOISE}
|
|
|
|
void main() {
|
|
vec2 p = vUv;
|
|
p.x *= uAspect;
|
|
float t = uTime * 0.06;
|
|
|
|
// 좌표 자체를 노이즈로 밀어 굴절처럼 보이게 한다
|
|
vec2 warp = vec2(fbm(p * 2.2 + t), fbm(p * 2.2 - t + 4.7));
|
|
vec2 q = p + (warp - 0.5) * 0.42;
|
|
|
|
// 밀린 좌표 위에 얇은 띠를 얹는다. 유리 너머의 결이다
|
|
float band = sin((q.x + q.y) * 7.0 + uTime * 0.35) * 0.5 + 0.5;
|
|
band = smoothstep(0.35, 0.95, band);
|
|
|
|
float depth = smoothstep(0.1, 1.0, fbm(q * 1.6 - t * 0.5));
|
|
|
|
vec3 col = mix(uB, uA, depth);
|
|
float alpha = (0.16 + band * 0.22) * (0.55 + depth * 0.45);
|
|
gl_FragColor = vec4(col, alpha);
|
|
}
|
|
`,
|
|
});
|
|
}
|
|
</script>
|