패럴랙스를 실제로 보이게 고치고 격자를 걷어낸다
"티가 안 난다"는 지적이 맞았다. 세 가지를 동시에 잘못하고 있었다. 1. 이동량이 모자랐다 여유를 28% 잡아놓고 92px 만 움직였다. 스크롤 1800px 구간에서 5%다. 공식은 이미지를 컨테이너보다 크게 만들고 초과분 전체를 움직이는 것. depth 0.45 -> 높이 145%, translateY(-31.03%). 같은 구간에서 374px. translateY(%) 는 자기 크기 기준이라 컨테이너 높이를 몰라도 맞는다. 2. overflow: hidden 이 view() 타임라인을 죽이고 있었다 overflow: hidden 은 스크롤 컨테이너를 만들고, 그러면 자식의 view() 가 문서가 아니라 그 상자를 기준으로 잡힌다. 이미지는 상자 안에서 늘 같은 자리라 진행도가 고정된다. 콘솔 에러도 없고 computed 값도 정상이라 두 스크롤 위치에서 transform 을 재보고서야 이동 0px 인 것을 알았다. 잘라내는 일은 섹션의 overflow: clip 에 맡겼다. 3. opacity 로 눌러 이미지를 지웠다 어두운 광원 사진을 opacity 0.5 로 깔면 검정도 빛도 같이 죽는다. mix-blend-mode: screen 으로 바꿨다 — 검정은 투명, 밝은 곳만 더해진다. 그래서 불투명도 0.85~1.0 을 써도 본문 대비가 안전하다. 생성 프롬프트도 같은 함정이었다. "어둡게"를 너무 강하게 넣어서 안전한 이미지가 아니라 없는 이미지를 만들었다. 격자 배경 제거 사진이 들어온 뒤로 배경이 두 겹이라 서로 경쟁했고 표로 읽혔다. 배경도 한 섹션에 하나다. 스킬 motion.md 에 넷 다 반영 이동량 공식 · overflow 함정 · screen 합성 · 배경은 한 겹
This commit is contained in:
parent
5a18806fb6
commit
1ed3c95542
11 changed files with 181 additions and 85 deletions
|
|
@ -213,14 +213,35 @@ WebGL 게이트: `prefers-reduced-motion` 이면 **아예 시작하지 않는다
|
|||
|
||||
| 레이어 | 무엇 | 어디 | 근거 |
|
||||
|---|---|---|---|
|
||||
| `.bg-grid` | 간격 눈금 격자. 셀이 `--grid-cell = --space-6` | 파이프라인 · 수치 | "모든 값이 토큰에서 나온다"가 배경에서도 성립해야 한다. 계기판의 눈금이기도 하다 |
|
||||
| `.bg-glow` | 광원 블롭 두 개. 섹션마다 `--glow-x` 로 자리를 옮긴다 | 재료 · 설치 | 히어로의 빛이 페이지를 관통해 마지막 CTA 에서 돌아온다 |
|
||||
기준면은 `BgPhoto` 컴포넌트가 만든다(아래 표).
|
||||
|
||||
격자는 가장자리를 마스크로 지운다 — 화면 끝까지 가면 배경이 아니라 **표**가 된다.
|
||||
배경이 가장 적게 움직인다. 앞의 것보다 크게 움직이면 앞으로 튀어나와 보인다.
|
||||
**CSS 격자를 한동안 같이 깔았다가 걷어냈다.** 사진이 들어온 뒤로 배경이 두 겹이 되어
|
||||
서로 경쟁했고, 격자는 배경이라기보다 **표**로 읽혔다. 배경도 한 섹션에 하나다.
|
||||
|
||||
**실측 3층** (스크롤 1000 → 1600): 격자 **+14.9px** · 산출물 **-14px** · 단계명 **0**.
|
||||
한 층은 반드시 고정이다. 전부 움직이면 기준이 없어 그냥 흔들리는 페이지가 된다.
|
||||
한 층은 반드시 고정이다 — 파이프라인의 단계명은 움직이지 않는다.
|
||||
전부 움직이면 기준이 없어 그냥 흔들리는 페이지가 된다.
|
||||
|
||||
### 패럴랙스가 세 번 실패한 경위
|
||||
|
||||
**1. 이동량이 모자랐다.** 여유를 28% 잡아놓고 `±46px`(총 92px)만 움직였다.
|
||||
스크롤 1800px 구간에서 이동 비율 5% — 아무도 시차를 느끼지 못했다.
|
||||
공식은 **이미지를 컨테이너보다 크게 만들고 그 초과분 전체를 움직이는 것**이다.
|
||||
`depth 0.45` → 이미지 145%, 이동 `translateY(-31.03%)`. 같은 구간에서 **374px**.
|
||||
|
||||
**2. `overflow: hidden` 이 `view()` 타임라인을 죽이고 있었다.**
|
||||
`overflow: hidden` 은 스크롤 컨테이너를 만들고, 그러면 자식의 `view()` 가 문서가 아니라
|
||||
그 상자를 기준으로 잡힌다. 이미지는 상자 안에서 늘 같은 자리라 진행도가 고정된다.
|
||||
**콘솔 에러도 없고 computed 값도 정상이라** 두 스크롤 위치에서 `transform` 을 재보고서야
|
||||
이동 0px 인 것을 알았다. 잘라내는 일은 섹션의 `overflow: clip` 에 맡겼다 —
|
||||
`clip` 은 스크롤 컨테이너를 만들지 않는다.
|
||||
|
||||
**3. `opacity` 로 눌러 이미지를 지웠다.** 어두운 광원 사진을 `opacity: 0.5` 로 깔면
|
||||
검정도 빛도 같이 죽어 **있는지조차 보이지 않는다.** `mix-blend-mode: screen` 으로 바꿨다 —
|
||||
검정은 투명해지고 밝은 곳만 더해지므로 불투명도를 0.85~1.0 으로 유지해도 본문 대비가 안전하다.
|
||||
밝은 종이만 `normal` 이다.
|
||||
|
||||
**생성 프롬프트에도 같은 함정이 있었다.** "본문 뒤에 깔리니 어둡게"를 너무 강하게 넣어서
|
||||
안전한 이미지가 아니라 **없는 이미지**를 만들었다. 대비는 합성 방식으로 풀 문제였다.
|
||||
|
||||
### 배경 사진 넉 장 (gpt-image-2 로 생성)
|
||||
|
||||
|
|
|
|||
|
|
@ -18,60 +18,108 @@ const SOURCES = { refraction, paper, split, align } as const;
|
|||
|
||||
interface Props {
|
||||
variant?: keyof typeof SOURCES;
|
||||
/** 0~1. 본문 뒤에 깔리므로 기본값을 낮게 둔다 */
|
||||
/**
|
||||
* 패럴랙스 강도. 이미지를 컨테이너보다 이 비율만큼 크게 만들고,
|
||||
* 스크롤 구간 동안 정확히 그 초과분만큼 움직인다.
|
||||
* 0.45 면 섹션 높이가 2000px 일 때 900px 을 이동한다.
|
||||
*/
|
||||
depth?: number;
|
||||
/** 0~1 */
|
||||
opacity?: number;
|
||||
/** 좌우 반전. 같은 그림을 두 섹션에 쓸 때 되풀이로 안 읽히게 한다 */
|
||||
flip?: boolean;
|
||||
}
|
||||
const { variant = "refraction", opacity = 0.45, flip = false } = Astro.props;
|
||||
const { variant = "refraction", depth = 0.45, opacity = 0.9, flip = false } = Astro.props;
|
||||
const src = SOURCES[variant];
|
||||
|
||||
// 밝은 종이만 그대로 얹고, 어두운 광원 사진은 더하기 합성한다.
|
||||
const additive = variant !== "paper";
|
||||
|
||||
// 이미지 높이 = (1 + depth) 배. 이동량은 이미지 자기 높이 기준 비율로 환산한다.
|
||||
// 예: depth 0.45 -> 높이 145%, 이동 -(0.45 / 1.45) = -31.03%
|
||||
const scale = 1 + depth;
|
||||
const shift = -(depth / scale) * 100;
|
||||
---
|
||||
|
||||
{/*
|
||||
- `loading="lazy"` — 초기 번들에 들어가지 않는다. CSS background-image 로 깔면
|
||||
이 통제권을 잃는다(항상 받고, 최적화도 안 된다). 그래서 <img> 를 절대배치한다
|
||||
- `aria-hidden` + 빈 alt — 장식이다. 보조기술이 읽을 것이 없다
|
||||
- opacity 기본 0.45 — 본문 대비를 지키는 선. 올리려면 그 위 글자를 다시 재라
|
||||
*/}
|
||||
<div class="bg-photo" class:list={{ flip }} aria-hidden="true" style={`--bg-opacity:${opacity}`}>
|
||||
<div
|
||||
class="bg-photo"
|
||||
class:list={{ flip, additive }}
|
||||
aria-hidden="true"
|
||||
style={`--bg-opacity:${opacity}; --bg-h:${scale * 100}%; --bg-shift:${shift.toFixed(2)}%`}
|
||||
>
|
||||
<Image src={src} alt="" widths={[860, 1400, 1920]} sizes="100vw" loading="lazy" decoding="async" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.bg-photo {
|
||||
position: absolute;
|
||||
/* 패럴랙스로 오르내리는 만큼 위아래에 여유를 둔다 */
|
||||
inset: -14% 0;
|
||||
inset: 0;
|
||||
grid-column: full;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
/* 섹션 경계에서 뚝 끊기지 않게 위아래를 지운다 */
|
||||
-webkit-mask-image: linear-gradient(to bottom, transparent 0%, black 18%, black 78%, transparent 100%);
|
||||
mask-image: linear-gradient(to bottom, transparent 0%, black 18%, black 78%, transparent 100%);
|
||||
/* overflow 를 걸지 않는다.
|
||||
overflow: hidden 은 스크롤 컨테이너를 만들고, 그러면 자식의 view() 타임라인이
|
||||
문서가 아니라 이 상자를 기준으로 잡힌다. 이미지는 상자 안에서 늘 같은 자리라
|
||||
진행도가 고정되고 패럴랙스가 통째로 멈춘다. 실측에서 이동량이 0px 이었다.
|
||||
잘라내는 일은 섹션의 `.has-bg { overflow: clip }` 이 한다. */
|
||||
}
|
||||
|
||||
.bg-photo :global(img) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* 컨테이너보다 세로로 크다. 이 초과분이 곧 패럴랙스가 쓸 수 있는 거리다. */
|
||||
height: var(--bg-h);
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
opacity: var(--bg-opacity);
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
/*
|
||||
어두운 배경 위의 광원 사진은 opacity 로 누르면 안 된다 — 빛까지 같이 죽는다.
|
||||
screen 은 검정을 투명하게 만들고 밝은 곳만 더한다. 그래서 불투명도를 높게
|
||||
유지해도 본문 대비를 해치지 않는다. 실제로 opacity 0.5 로 눌렀을 때는
|
||||
이미지가 있는지조차 보이지 않았다.
|
||||
*/
|
||||
.additive :global(img) { mix-blend-mode: screen; }
|
||||
.flip :global(img) { transform: scaleX(-1); }
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
@supports (animation-timeline: view()) {
|
||||
/* 배경이 가장 느리다. 앞의 것보다 크게 움직이면 앞으로 튀어나와 보인다 */
|
||||
.bg-photo {
|
||||
--par-from: -46px;
|
||||
--par-to: 46px;
|
||||
animation: par linear both;
|
||||
.bg-photo :global(img) {
|
||||
animation: bg-parallax linear both;
|
||||
animation-timeline: view();
|
||||
/* 섹션이 화면에 들어올 때부터 나갈 때까지 전 구간을 쓴다.
|
||||
좁게 잡으면 대부분의 스크롤에서 이미지가 멈춰 있다. */
|
||||
animation-range: cover 0% cover 100%;
|
||||
}
|
||||
.flip :global(img) {
|
||||
animation-name: bg-parallax-flip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 초과분 전체를 쓴다. 여유를 45% 잡아놓고 92px 만 움직이면 시차가 안 보인다 */
|
||||
@keyframes bg-parallax {
|
||||
from { transform: translateY(0); }
|
||||
to { transform: translateY(var(--bg-shift)); }
|
||||
}
|
||||
@keyframes bg-parallax-flip {
|
||||
from { transform: translateY(0) scaleX(-1); }
|
||||
to { transform: translateY(var(--bg-shift)) scaleX(-1); }
|
||||
}
|
||||
|
||||
/* screen 합성은 어두운 곳이 이미 투명하다. 투명도를 크게 깎을 이유가 없고,
|
||||
깎으면 이미지가 있는지조차 안 보인다. 종이(불투명)만 더 낮춘다. */
|
||||
@media (prefers-reduced-transparency: reduce) {
|
||||
.bg-photo :global(img) { opacity: calc(var(--bg-opacity) * 0.5); }
|
||||
.additive :global(img) { opacity: calc(var(--bg-opacity) * 0.9); }
|
||||
.bg-photo:not(.additive) :global(img) { opacity: calc(var(--bg-opacity) * 0.7); }
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const { c, ui } = Astro.props;
|
|||
|
||||
|
||||
<section id="install" class="full install has-bg" aria-labelledby="install-h">
|
||||
<BgPhoto opacity={0.55} flip />
|
||||
<BgPhoto variant="refraction" opacity={0.85} depth={0.4} flip />
|
||||
<div class="install-grid">
|
||||
<div class="install-head">
|
||||
<h2 id="install-h">{c.h2}</h2>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const { c } = Astro.props;
|
|||
둘 다 설명이 아니라 지금 이 화면에서 돌고 있는 것이다.
|
||||
*/}
|
||||
<section class="section materials has-bg" aria-labelledby="mat-h">
|
||||
<BgPhoto opacity={0.6} />
|
||||
<BgPhoto variant="refraction" opacity={0.9} depth={0.45} />
|
||||
<h2 id="mat-h" class="reveal">{c.h2}</h2>
|
||||
<p class="lead mat-lead reveal">{c.lead}</p>
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const rest = c.items.filter((m) => !m.feature);
|
|||
흔들리면 그 값도 흔들리는 것처럼 읽힌다. 계기판의 바늘은 떨지 않는다.
|
||||
움직임이 의미를 만드는 자리가 있으면, 움직이지 않는 것이 의미인 자리도 있다.
|
||||
*/}
|
||||
<section id="metrics" class="section metrics bg-grid" aria-labelledby="metrics-h">
|
||||
<section id="metrics" class="section metrics" aria-labelledby="metrics-h">
|
||||
<div class="head">
|
||||
<p class="eyebrow reveal">{c.eyebrow}</p>
|
||||
<h2 id="metrics-h" class="reveal">{c.h2}</h2>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ const { c } = Astro.props;
|
|||
③ 의미 — <ol role="list"> — 화면에 번호는 없지만 보조기술은 "6개 중 n번째"로 읽는다
|
||||
role="list" 는 list-style:none 일 때 Safari/VoiceOver 가 목록 의미를 버리는 것을 되돌린다
|
||||
*/}
|
||||
<section id="pipeline" class="section bg-grid" aria-labelledby="pipeline-h">
|
||||
<section id="pipeline" class="section" aria-labelledby="pipeline-h">
|
||||
{/* 제목·리드·브리프·스파인을 한 컨테이너에 담는다.
|
||||
섹션 기본 그리드에 두면 제목은 main 칼럼의 음수 마진으로 x=371,
|
||||
리드는 x=435, 스파인은 x=143 에 서서 정렬선이 셋이 된다. */}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const { c } = Astro.props;
|
|||
왼쪽 세로선이 로그라는 형식을 만든다.
|
||||
*/}
|
||||
<section class="section preflight has-bg" aria-labelledby="pf-h">
|
||||
<BgPhoto variant="align" opacity={0.8} />
|
||||
<BgPhoto variant="align" opacity={1} depth={0.55} />
|
||||
<h2 id="pf-h" class="reveal">{c.h2}</h2>
|
||||
<p class="lead pf-lead reveal">{c.lead}</p>
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const shots = [shotCoffee, shotSaas];
|
|||
|
||||
{/* 레이아웃 패밀리: Z축 캐스케이드. 두 판이 서로 겹치고 각도가 다르다 */}
|
||||
<section id="showcase" class="section showcase has-bg" aria-labelledby="showcase-h">
|
||||
<BgPhoto variant="split" opacity={0.5} />
|
||||
<BgPhoto variant="split" opacity={0.85} depth={0.5} />
|
||||
<h2 id="showcase-h" class="reveal">{c.h2}</h2>
|
||||
<p class="lead showcase-lead reveal">{c.lead}</p>
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const { c } = Astro.props;
|
|||
---
|
||||
|
||||
<section class="section band-inverse has-bg" aria-labelledby="type-h">
|
||||
<BgPhoto variant="paper" opacity={0.85} />
|
||||
<BgPhoto variant="paper" opacity={1} depth={0.3} />
|
||||
<p class="eyebrow reveal">{c.eyebrow}</p>
|
||||
<h2 id="type-h" class="reveal">{c.h2}</h2>
|
||||
<p class="lead type-lead reveal">{c.lead}</p>
|
||||
|
|
|
|||
|
|
@ -419,67 +419,19 @@ a:hover { color: var(--accent); }
|
|||
패럴랙스는 깊이 차가 보여야 성립한다. 배경이 단색이면 앞의 것이
|
||||
몇 px 어긋나 봐야 아무도 못 느낀다 — 기준면이 없기 때문이다.
|
||||
|
||||
그래서 배경을 깐다. 다만 사 온 이미지가 아니라 이 페이지가 주장하는 것,
|
||||
곧 간격 눈금 자체를 격자로 그린다. --grid-cell 은 --space-5 다. */
|
||||
.bg-grid,
|
||||
.bg-glow,
|
||||
기준면은 `BgPhoto` 컴포넌트가 만든다.
|
||||
|
||||
CSS 로 그린 격자를 한동안 같이 깔았는데 걷어냈다. 사진이 들어온 뒤로는
|
||||
배경이 두 겹이 되어 서로 경쟁했고, 격자는 배경이라기보다 표로 읽혔다.
|
||||
배경도 한 섹션에 하나여야 한다. */
|
||||
.has-bg {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
/* 배경 레이어는 위아래로 14% 삐져나온다(패럴랙스 여유).
|
||||
clip 하지 않으면 그 삐져나온 부분이 이웃 섹션 위에 얹힌다 —
|
||||
조판 섹션의 종이가 위 섹션 하단에 밝은 띠로 나타났다. */
|
||||
/* 배경 이미지는 컨테이너보다 크고 패럴랙스로 움직인다.
|
||||
clip 하지 않으면 그 초과분이 이웃 섹션 위로 넘친다. */
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
/* 섹션은 subgrid 이고 `.section > *` 이 전부 main 칼럼(561px)에 배치된다.
|
||||
절대배치라도 그리드 영역이 잡히면 그게 컨테이닝 블록이 되므로,
|
||||
배경이 화면 가운데 561px 띠로만 깔렸다. 전체 폭으로 되돌린다. */
|
||||
.bg-photo { grid-column: full; }
|
||||
.bg-grid::before,
|
||||
.bg-glow::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -12% 0;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.bg-grid::before {
|
||||
background-image:
|
||||
repeating-linear-gradient(to right,
|
||||
var(--line-strong) 0 1px, transparent 1px var(--grid-cell)),
|
||||
repeating-linear-gradient(to bottom,
|
||||
var(--line-strong) 0 1px, transparent 1px var(--grid-cell));
|
||||
/* 가장자리에서 지운다. 화면 끝까지 격자가 가면 표가 되어버린다 */
|
||||
-webkit-mask-image: radial-gradient(135% 95% at 50% 45%, black 22%, transparent 92%);
|
||||
mask-image: radial-gradient(135% 95% at 50% 45%, black 22%, transparent 92%);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* 히어로의 두 광원이 페이지를 관통한다. 섹션마다 자리가 다르다 */
|
||||
.bg-glow::before {
|
||||
background:
|
||||
radial-gradient(42% 46% at var(--glow-x, 78%) var(--glow-y, 32%),
|
||||
color-mix(in oklab, var(--accent) 15%, transparent), transparent 70%),
|
||||
radial-gradient(38% 42% at calc(100% - var(--glow-x, 78%)) 78%,
|
||||
color-mix(in oklab, var(--glow-2) 13%, transparent), transparent 68%);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
@supports (animation-timeline: view()) {
|
||||
/* 배경이 가장 느리다. 콘텐츠보다 적게 움직여야 뒤에 있는 것으로 읽힌다 */
|
||||
.bg-grid::before { --par-from: -34px; --par-to: 34px; }
|
||||
.bg-glow::before { --par-from: -58px; --par-to: 58px; }
|
||||
.bg-grid::before,
|
||||
.bg-glow::before {
|
||||
animation: par linear both;
|
||||
animation-timeline: view();
|
||||
animation-range: cover 0% cover 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 유틸 -------------------------------------------------- */
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
|
|
|
|||
|
|
@ -448,6 +448,81 @@ JS 쪽 판정도 한 곳에 모은다. 설정이 도중에 바뀌면 반영한
|
|||
안 움직이는 섹션이 하나도 없으면 움직임에 의미가 없다는 뜻이고,
|
||||
`design.md` 에 "여기는 의도적으로 정지"라고 적어두지 않으면 다음 사람이 "빠뜨린 것"으로 보고 채운다.
|
||||
|
||||
### 이동량은 절대값이 아니라 컨테이너 대비 비율이다
|
||||
|
||||
**패럴랙스가 "안 보인다"의 대부분은 이동량이 모자란 것이다.**
|
||||
|
||||
공식은 하나다. **배경을 컨테이너보다 크게 만들고, 그 초과분 전체를 움직인다.**
|
||||
|
||||
```
|
||||
이미지 높이 = 컨테이너 × (1 + depth)
|
||||
이동량 = 컨테이너 × depth
|
||||
= 이미지 높이의 depth / (1 + depth)
|
||||
```
|
||||
|
||||
`depth: 0.45` 면 이미지는 145%, 이동은 이미지 자기 높이의 **31%**.
|
||||
섹션이 2000px 이면 **900px** 을 움직인다.
|
||||
|
||||
```css
|
||||
.bg img {
|
||||
height: 145%; /* 1 + depth */
|
||||
animation: bg-par linear both;
|
||||
animation-timeline: view();
|
||||
animation-range: cover 0% cover 100%;
|
||||
}
|
||||
@keyframes bg-par {
|
||||
from { transform: translateY(0); }
|
||||
to { transform: translateY(-31.03%); } /* depth / (1 + depth) */
|
||||
}
|
||||
```
|
||||
|
||||
`translateY(%)` 는 **자기 크기 기준**이라 컨테이너 높이를 몰라도 정확히 맞는다.
|
||||
|
||||
**실측 사례**: 여유를 28% 잡아놓고 `±46px`(총 92px)만 움직였더니 스크롤 1800px 구간에서
|
||||
이동 비율이 5% 였다. 아무도 시차를 느끼지 못했다. 같은 구간에서 **374px** 로 올리자 보였다.
|
||||
CSS-Tricks 의 예제도 `background-position: bottom 0px → bottom -400px`, 즉 400px 이다.
|
||||
|
||||
> 참고: `background-position` 을 애니메이션하는 방법도 있다(CSS-Tricks). 다만 그러면
|
||||
> `<img loading="lazy">` 의 지연 로딩과 반응형 `srcset` 을 잃는다. 무거운 사진이면 `<img>` 를
|
||||
> 절대배치하고 `transform` 을 쓰는 쪽이 낫다.
|
||||
|
||||
### `overflow: hidden` 이 view() 타임라인을 죽인다
|
||||
|
||||
**이것 하나에 패럴랙스가 통째로 멈춘다. 그리고 조용히 멈춘다.**
|
||||
|
||||
`overflow: hidden` 은 **스크롤 컨테이너를 만든다.** 그러면 그 안에 있는 자식의
|
||||
`animation-timeline: view()` 는 문서가 아니라 **그 상자**를 기준으로 잡힌다.
|
||||
자식은 상자 안에서 위치가 변하지 않으므로 진행도가 고정되고, 애니메이션은
|
||||
어중간한 한 프레임에서 굳는다.
|
||||
|
||||
- 콘솔 에러 없음. `animation-name`·`animation-timeline` computed 값도 전부 정상
|
||||
- `getComputedStyle(el).transform` 을 두 스크롤 위치에서 재봐야 알 수 있다. **실측에서 이동 0px**
|
||||
|
||||
**잘라내야 한다면 `overflow: clip` 을 쓰거나 조상 섹션에서 처리해라.**
|
||||
`clip` 은 스크롤 컨테이너를 만들지 않는다.
|
||||
|
||||
### 어두운 배경 위 광원 이미지는 opacity 로 누르지 마라
|
||||
|
||||
`opacity` 는 이미지 전체를 균일하게 죽인다 — 검정도, 빛도 같이. 그래서 어두운 광원 사진을
|
||||
`opacity: 0.5` 로 깔면 **있는지조차 안 보인다.**
|
||||
|
||||
```css
|
||||
.bg img { mix-blend-mode: screen; opacity: 0.9; }
|
||||
```
|
||||
|
||||
`screen` 은 **검정을 투명하게 만들고 밝은 곳만 더한다.** 그래서 불투명도를 높게 유지해도
|
||||
본문 대비를 해치지 않는다. 밝은 이미지(종이·질감)는 반대로 `normal` 이 맞다.
|
||||
|
||||
**생성형 이미지를 배경으로 쓸 때 특히 조심해라.** 프롬프트에 "본문 뒤에 깔리니 어둡게"를
|
||||
너무 강하게 넣으면 안전한 이미지가 아니라 **없는 이미지**가 나온다.
|
||||
대비는 합성 방식으로 풀고, 이미지 자체는 볼 수 있게 만들어라.
|
||||
|
||||
### 배경도 한 섹션에 하나다
|
||||
|
||||
CSS 격자와 사진을 같이 깔았다가 걷어냈다. 두 배경이 서로 경쟁했고,
|
||||
격자는 배경이라기보다 **표**로 읽혔다. 앞에서 "한 층은 고정"이라고 한 것과 같은 이야기다 —
|
||||
층을 늘리는 것과 층이 싸우게 두는 것은 다르다.
|
||||
|
||||
### 패럴랙스에는 기준면이 필요하다
|
||||
|
||||
**배경이 단색이면 패럴랙스는 성립하지 않는다.** 앞의 것이 몇 px 어긋나 봐야
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue