designpaca/apps/site/src/components/Materials.astro
Yun Chan 5a18806fb6 배경 레이어가 그리드 안에서 561px 로 잘리던 것을 고친다
절대배치인데도 배경이 화면 가운데 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 가 의도대로 작동한다.
2026-08-20 15:44:15 +09:00

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>