Communitygithub.com

anhlamtruong/living-ui-effects

웹 페이지 로드 후에도 살아 움직이는 인터랙티브 효과(WebGL 오로라/액체광 쉐이더, 홀로그램 3D 기울기 카드, 커서 추적 그리드 스포트라이트, 글리치/스크램블 텍스트)를 추가합니다.

living-ui-effects란 무엇인가요?

이 스킬은 웹 페이지에 고급스럽고 역동적인 시각 효과를 추가하여 사용자 경험을 한층 더 업그레이드합니다. WebGL 기반의 오로라 쉐이더, 마우스 포인터를 따라 기울어지는 홀로그램 카드, 커서를 따라 움직이는 그리드 스포트라이트, 그리고 RGB 분할 글리치 및 호버 시 텍스트 스크램블 효과 등 네 가지 주요 효과를 제공합니다. React 및 Next.js 프로젝트에 쉽게 통합할 수 있도록 설계되었으며, 하나의 공유된 `requestAnimationFrame` 루프를 사용하고 레이아웃 시프트가 없으며 접근성을 고려합니다. 모든 효과는 CSS `transform`, `opacity`, `clip-path` 속성만을 애니메이션하여 성능을 최적화하고, 사용자의 입력 방식(마우스/터치)과 모션 선호도에 따라 적절히 반응합니다. 특히, 콘텐츠가 완전히 로드된 후에만 효과가 작동하도록 설계되어 불필요한 버벅임을 방지하며, 사용하지 않을 때는 프레임 루프를 중지하여 리소스를 절약합니다.

지원 대상~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/anhlamtruong/living-ui-skills/tree/HEAD/skills/living-ui-effects

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

라이브 효과

콘텐츠가 화면에 표시된 후에도 페이지를 계속 움직이게 하는 네 가지 효과:

  • 오로라 히어로: 히어로 영역 뒤에서 포인터를 따라 움직이는 부드러운 WebGL 빛 필드.
  • 홀로그램 카드: 카드가 커서 방향으로 기울어지고, 살짝 떠오르며 빛 반사와 무지갯빛 테두리가 생깁니다.
  • 그리드 스포트라이트: 페이지 배경 그리드가 커서 주변에 부드러운 원형으로 밝아집니다.
  • 글리치 및 스크램블 텍스트: 제목이 나타날 때 RGB 분할 효과가 적용되고, 링크 레이블은 호버 시 디코딩 효과를 냅니다.

기준은 '살아있지만, 결코 방해되지 않는다'입니다. 사용자가 페이지가 자신에게 반응한다고 느껴야 하지만, 어떤 것도 버벅이거나 레이아웃을 밀거나, 클릭을 가로채거나, 화면 리더에서 텍스트를 숨겨서는 안 됩니다.

라이브 예시: https://www.lamanhtruong.com (프로젝트 카드에 호버, 히어로 위로 이동, 내비게이션에 호버).

assets/fx/ 폴더 내용

파일역할
ticker.ts하나의 공유된 requestAnimationFrame 루프. 프레임을 요청하는 요소가 없으면 대기 상태로 전환됩니다.
built.ts준비 상태 게이트 (isBuilt, buildActive), frozen(), 시드 기반 PRNG, 포인터 미디어 쿼리
aurora.tsWebGL 쉐이더, 품질 조절기 및 생명 주기 (createAurora)
hero-shader.tsx<HeroShader />, 오로라를 위한 React 래퍼
holo.ts카드의 기울기 + 빛 반사 + 광택, 스프링 물리, 주입된 오버레이
spotlight.ts커서를 따라가는 그리드 스포트라이트 (transform만 사용)
glitch.ts, glitch-copies.tsx글리치 스케줄러, <GlitchCopies /> 및 <GlitchHeading />
scramble.ts, scramble-text.tsx스크램블 실행기 및 <ScrambleText /> 마크업 훅
fx-root.tsx<FxRoot />: 홀로그램, 스포트라이트, 글리치 및 스크램블을 한 번에 마운트
fx.css모든 스타일, --fx-* 사용자 정의 속성으로 테마 적용

이 코드는 React만 필요로 하며, 별도의 애니메이션 라이브러리는 필요하지 않습니다. 템플릿은 React 18 및 19 타입에 대해 엄격한 TypeScript 검사를 통과합니다.

설치

  1. assets/fx/ 폴더를 앱에 복사합니다 (예: src/components/fx/). 파일은 서로 상대 경로로 임포트하므로 함께 두세요.
  2. 마운트합니다. 페이지 자체는 정적 서버 컴포넌트로 유지될 수 있습니다. FxRoot와 HeroShader만 클라이언트 컴포넌트입니다.
import { FxRoot } from "@/components/fx/fx-root";
import { HeroShader } from "@/components/fx/hero-shader";
import { GlitchHeading } from "@/components/fx/glitch-copies";
import { ScrambleText } from "@/components/fx/scramble-text";

export default function Page() {
  return (
    <div className="fx-grid">                        {/* 고정된 기본 그리드; 자체 그리드가 있으면 생략 */}
      <main className="relative z-[1]">              {/* 콘텐츠는 그리드 + 스포트라이트 위에 위치 */}
        <section className="relative min-h-[80vh]">  {/* 히어로는 position: relative 여야 함 */}
          <HeroShader />                             {/* 첫 번째 자식, 히어로 콘텐츠 뒤에 위치 */}
          <h1>…</h1>
        </section>

        <GlitchHeading as="h2" className="text-5xl font-semibold">Selected work</GlitchHeading>

        <article data-holo className="rounded-2xl border p-6">
          <a href="/work/x" className="inline-flex items-center gap-2">
            <ScrambleText>Read the case study</ScrambleText> <ArrowIcon />
          </a>
        </article>
      </main>
      <FxRoot />                                     {/* 페이지당 한 번, 그리드 래퍼 내부 */}
    </div>
  );
}
  1. 테마 적용: fx.css 상단의 --fx-* 변수를 편집하고, <HeroShader />에 palette / intensity를 전달합니다 (아래 '테마' 참조).
  2. 완료하기 전에 references/verification.md의 Playwright 검사를 통해 확인합니다.

props로 개별 효과를 비활성화할 수 있습니다: <FxRoot holo={false} spotlight={false} />. 기존 카드의 마크업을 건드리지 않고 기울기 효과를 적용하려면: <FxRoot holo={{ selector: ".card", within: "main" }} />.

부드러움을 유지하는 규칙

이 규칙들은 '고급스러움'과 '버벅임'을 구분짓습니다. 각 규칙은 이를 위반하면 눈에 띄는 문제가 발생했기 때문에 존재합니다.

  1. 콘텐츠가 준비될 때까지 기다리세요. 다른 스크립트가 여전히 요소를 표시하거나 측정하고 있는 동안에는 절대 기울이기, 글리치 또는 스크램블 효과를 적용하지 마세요. 표시 중인 요소에 변형을 가하면 해당 요소를 애니메이션하는 다른 스크립트의 측정이 손상되어 버벅임처럼 보입니다. built.ts는 하나의 계약을 구현합니다: html[data-build="on"]이 설정되어 있는 동안, 요소는 해당 [data-build-item] 및 [data-build-section]이 [data-built]를 가질 때까지 준비된 것으로 간주되지 않습니다. 페이지에 빌드 엔진이 없으면 모든 것이 즉시 빌드된 것으로 간주됩니다. 모든 인트로 애니메이션은 이러한 속성을 설정하여 참여할 수 있습니다. 자세한 내용은 references/gating-and-performance.md를 참조하세요.

  2. 하나의 프레임 루프를 사용하고, 필요 없으면 절전 모드로 전환하세요. 모든 효과는 자체적으로 requestAnimationFrame을 호출하는 대신 ticker.ts를 구독합니다. 구독자는 정착되면 false를 반환하고, 아무도 프레임을 필요로 하지 않으면 루프가 중지됩니다. 유휴 페이지는 비용이 전혀 들지 않으며, 효과가 서로 프레임을 두고 경쟁할 수 없습니다. 글리치는 프레임을 전혀 사용하지 않습니다. CSS 키프레임이 속성에 의해 토글됩니다.

  3. transform, opacity 및 clip-path만 애니메이션하세요. 이 속성들은 컴포지터에서 처리됩니다. 여기서 크기, 플로우 내 위치 또는 폰트 메트릭이 변경되지 않으므로 레이아웃 시프트가 발생하지 않습니다. 스크램블은 실제 텍스트를 제자리에 투명하게 유지하고 오버레이에 글리프를 그리므로, 레이블이 변경되어도 너비가 절대 변하지 않습니다.

  4. 입력 방식에 맞게 효과를 조정하세요. 기울기 및 따라가기 스포트라이트는 (hover: hover) and (pointer: fine) 환경에서만 존재합니다. 터치 환경에서 손가락 아래에서 기울어지는 것은 버그처럼 보이므로, 터치에는 탭 지점에서 짧은 페이드 효과와 자체적으로 움직이는 쉐이더 렌즈가 제공됩니다.

  5. 정지 상태에서는 React의 DOM을 건드리지 마세요. 홀로그램 오버레이 <span>과 인라인 transform은 카드가 활성화될 때 추가되고, 원래대로 돌아왔을 때 제거됩니다. 정지 상태에서는 DOM이 정확히 React가 렌더링한 대로 유지되므로, 리렌더링, 하이드레이션 및 카드 자체의 :hover 스타일이 계속 작동합니다. 글리치 레이어와 스크램블 스팬은 마크업에 <GlitchCopies>, <ScrambleText>로 작성되며, 런타임에 React가 소유한 노드 주변에 삽입되지 않습니다. 외부에서 React의 트리를 재구성하면 조정(reconciliation)이 깨집니다.

  6. 텍스트를 실제로 유지하세요. 글리치 복사본은 클라이언트 전용이며 aria-hidden이므로, 정적 HTML, 페이지 내 찾기 및 화면 리더는 각 제목을 한 번만 봅니다. 스크램블 오버레이는 aria-hidden이고 실제 레이블은 전체적으로 접근 가능한 이름으로 유지됩니다.

  7. 정리하고 StrictMode에서도 살아남으세요. 모든 setup* 함수는 리스너, 옵저버, 오버레이 및 인라인 스타일을 제거하는 폐기자(disposer)를 반환합니다. 쉐이더는 버퍼와 프로그램을 해제하지만 의도적으로 WEBGL_lose_context를 호출하지 않습니다. React StrictMode는 동일한 캔버스에서 마운트, 마운트 해제 및 다시 마운트를 수행하는데, 컨텍스트를 잃으면 영원히 복구되지 않기 때문입니다.

  8. GPU 예산을 관리하세요. 쉐이더는 픽셀 예산 하에 축소된 해상도로 렌더링하고, 표시될 때만 그리며, 고주사율 화면에서 초당 약 80회로 제한하고, 페이지가 약 38fps 미만으로 실행되면 한 단계 품질을 낮춥니다. 소프트웨어 폴백(failIfMajorPerformanceCaveat)에서는 시작을 거부하고, 느린 것보다는 아무것도 렌더링하지 않습니다.

효과

오로라 히어로 쉐이더 (aurora.ts, hero-shader.tsx)

쉐이더는 도메인 왜곡된 밸류 노이즈(fbm으로 왜곡된 fbm)로, 넓은 광선과 왜곡된 윤곽선을 따라 얇고 밝은 가닥으로 그려집니다. 포인터는 렌즈 역할을 하여 주변 흐름을 확대하고 소용돌이치게 합니다. 히어로 영역에 진입하거나, 클릭하거나, 빠르게 움직이면 부드러운 리플 링이 떨어집니다. 출력은 미리 곱해진 알파이며 모든 가장자리에서 희미해져서 페이지 배경과 고정 그리드가 비쳐 보이고, 히어로가 상자 형태로 끝나는 대신 다음 섹션으로 부드럽게 블렌딩됩니다.

생명 주기: 캔버스는 해당 섹션이 빌드된 후 1.8초에 걸쳐 페이드 인됩니다. 섹션이 화면에 보이는 동안(80px 마진의 IntersectionObserver) 그리고 탭이 표시되는 동안에만 렌더링합니다. WebGL이 없으면 아무것도 렌더링하지 않습니다.

설정 가능 항목:

  • palette: { veil, primary, secondary, accent } (16진수). 팔레트는 쉐이더에 컴파일되므로 변경하려면 리마운트가 필요합니다.
  • intensity: 빛의 강도를 곱합니다. 0.6은 차분하고, 1.4는 강렬합니다.
  • 텍스트 가독성: 쉐이더는 일반적으로 히어로 텍스트가 위치하는 왼쪽 아래에서 빛을 덜 발산합니다. 텍스트가 다른 곳에 있다면 bias 라인을 변경하세요 (참조 문서 참조).

쉐이더 수학, 속도 또는 품질 조절기를 변경하기 전에 references/aurora-shader.md를 읽으세요.

홀로그램 카드 (holo.ts)

위임된 pointerover가 정밀 포인터 아래의 카드를 찾습니다. 활성화되면 카드의 변환되지 않은 박스를 측정하고, 하나의 aria-hidden 오버레이 스팬을 주입한 후, 포인터를 향한 기울기를 목표로 임계 감쇠 스프링(120Hz로 서브스텝되어 60 또는 144fps에서 동일하게 작동)을 실행합니다. 커서 아래의 가장자리가 뷰어 쪽으로 옵니다. 떠날 때는 부드러운 오버슈트와 함께 다시 스프링백되고, 완전히 안정되면 모든 것을 제거합니다.

  • 기울기는 카드 크기에 비례하며 2.5° ~ 7.5°입니다. 큰 카드는 먼 가장자리가 너무 많이 흔들리지 않도록 덜 기울어집니다. 원근감(perspective)은 큰 변의 1.6배입니다. 떠오르는 높이는 3px입니다.
  • 오버레이는 포인터 위치의 방사형 빛 반사, 희미한 대각선 컬러 워시, 그리고 테두리에 마스킹된 원뿔형 그라데이션 링(전체적으로는 어둡고 포인터 근처는 밝음)을 가집니다.
  • overflow: hidden 카드의 경우 1px 테두리 상자가 잘리므로 링은 바로 안쪽에 그려집니다(.fx-holo--in, 자동 감지).
  • 오버레이는 z-index: 3에 pointer-events: none으로 위치하므로, 카드 내부의 컨트롤(비디오 토글, 링크) 위에 그려지면서도 클릭을 방해하지 않습니다.
  • 정적 위치(position: static) 카드는 활성화된 동안에만 position: relative를 얻습니다.
  • 옵트아웃: 카드 또는 조상에 data-holo="off"를 설정합니다. 누군가 실제 rect를 읽는 동안(검사기, 스크린샷 패스) html[data-fx-freeze]로 모든 기울기를 일시 중지합니다. living-ui-swarm 검사 토글은 자동으로 감지됩니다.

그리드 스포트라이트 (spotlight.ts, div.fx-spot)

560px 고정 윈도우가 방사형으로 마스킹되어 기본 그리드의 밝은 복사본을 표시합니다. 각 프레임마다 두 개의 변형만 이동합니다: 윈도우는 커서를 따라가고, 내부 그리드는 floor(x / cell) * cell - x만큼 다시 이동하여 선이 기본 그리드의 격자에 정확히 맞춰집니다. 두 그리드 모두 뷰포트 원점에 고정되어야 하며, .fx-grid::before가 그렇게 합니다. 후행 lerp는 안정되고 커서가 멈추면 루프가 중지됩니다. 창이 흐려지거나 포인터가 창을 벗어나면 숨겨집니다. 터치는 탭 지점에서 360ms 페이드를 얻습니다.

그리드 셀, 미세선 간격 및 윈도우 크기는 --fx-grid-cell, --fx-grid-fine 및 --fx-spot-size (px 값)에서 가져오므로 CSS가 단일 진실 공급원으로 유지됩니다.

글리치 제목 (glitch.ts, <GlitchHeading> / <GlitchCopies>)

마크업: 실제 제목(.fx-glitch__base)과 두 개의 클라이언트 전용 복사본을 보유하는 .fx-glitch[data-fx-glitch] 래퍼. GlitchHeading이 이 모든 것을 작성합니다. 제목이 이미 존재하고 다른 속성을 가지고 있다면 GlitchCopies를 직접 사용하세요.

  • 입장: 빌드된 제목이 최소 50% 이상 뷰에 들어왔을 때 560ms 동안 지속됩니다. 동일한 제목은 2.5초 이내에 재생되지 않습니다.
  • 마이크로 글리치: 8~14초마다 무작위로 보이는 제목 하나에 260ms 동안 발생합니다.
  • 무작위성은 시드(seed) 기반이므로, 모든 방문과 테스트에서 리듬이 동일합니다.
  • 한 번에 하나의 제목만 글리치됩니다. 입장 효과는 서로 대기열에 추가됩니다.
  • [data-glitch]에 의해 전환되는 순수 CSS 키프레임으로 구현됩니다: clip-path: inset()으로 슬라이스하고, translate3d로 오프셋하며, text-shadow로 색상을 분할합니다.
  • --fx-gi는 실행마다 오프셋을 조정합니다.

스크램블 텍스트 (scramble.ts, <ScrambleText>)

아이콘을 감싸지 말고 레이블만 감싸세요: <a><Icon/> <ScrambleText>{label}</ScrambleText></a>.

  • 트리거: 호버(터치 아님) 또는 포함하는 a, button 또는 [data-fx-scr-host]의 키보드 :focus-visible.
  • 지속 시간: 길이에 따라 360~520ms.
  • 해결: 문자는 약간의 지터와 함께 왼쪽에서 오른쪽으로 해결됩니다. 해결되지 않은 꼬리는 모든 프레임이 아닌 ~18Hz로 다시 그려지므로 노이즈가 아닌 디코딩으로 읽힙니다.
  • 글리프 풀: 대소문자가 일치하며, @ . / : -와 같은 구두점은 그대로 유지되어 이메일과 경로가 스크램블 중에도 읽을 수 있습니다.
  • 정리: 오버레이는 항상 비워집니다. 여기에는 rAF가 중지되는 백그라운드 탭을 위한 타이머 폴백이 포함됩니다.

일반 인라인 링크에는 inline을 사용하세요 (밑줄 유지). 채워진 버튼에는 quiet를 사용하고, 버튼에 --fx-scr-tail: currentColor를 함께 설정하세요.

테마

변수사용처기본값
--fx-accent글리치 레이어 A, 스크램블 글리프, 스포트라이트 광선, 홀로그램 그림자#5eead4
--fx-hot글리치 레이어 B#f472b6
--fx-sheen-1..5홀로그램 링, 원 주위 순서대로청록, 파랑, 보라, 분홍, 호박
--fx-grid-line, --fx-grid-fine-line기본 그리드 선낮은 알파 슬레이트
--fx-grid-cell, --fx-grid-fine그리드 간격 (px)96px, 24px
--fx-spot-size스포트라이트 윈도우 (px)560px
--fx-mono스크램블 글리프 폰트시스템 모노
--fx-scr-tail요소별 글리프 색상 재정의설정되지 않음

색상은 color-mix(in srgb, …)을 통해 혼합되며, 모든 최신 브라우저에서 지원됩니다. 쉐이더는 자체 팔레트를 사용합니다: <HeroShader palette={{ accent: "#22d3ee", secondary: "#f0abfc" }} intensity={0.8} />.

페이지 강조 색상과 같은 계열의 쉐이더 색상을 선택하세요. 쉐이더는 분위기를 위한 것이지 두 번째 브랜드 컬러가 아닙니다. 처음 방문한 사용자가 헤드라인보다 쉐이더를 먼저 알아차린다면 너무 강한 것입니다.

레이어링

레이어z-index참고
기본 그리드 (.fx-grid::before, fixed)0뷰포트 원점에 고정
스포트라이트 (div.fx-spot, fixed)0트리 순서상 그리드 뒤에 있어 그 위에 페인트됨
콘텐츠 (main, footer)1z-index: 1로 위치 지정되어야 함
히어로 쉐이더 (canvas.fx-shader)auto히어로 내부에서 절대 위치, 첫 번째 자식이므로 이후 콘텐츠가 위에 페인트됨
홀로그램 오버레이카드 내에서 3pointer-events: none
고정 헤더, 빌드 애니메이션 오버레이 캔버스콘텐츠 위영향 없음

텍스트 위에 스포트라이트가 표시된다면 콘텐츠 래퍼에 position: relative; z-index: 1이 누락된 것입니다.

모션 선호도

lamanhtruong.com에서 소유자는 prefers-reduced-motion 설정이 있는 방문자를 포함하여 모든 사람에게 모든 효과를 재생하기로 선택했습니다. 모션이 포트폴리오의 핵심이기 때문입니다. 이는 합법적인 제품 결정이지만, 의도적으로 결정해야 합니다. 설정을 존중하려면 대신:

  • <FxRoot motion="reduce" />를 사용하세요. 모든 효과를 축소하거나 비활성화합니다.
  • prefers-reduced-motion: reduce에 대한 자체 CSS를 작성할 수도 있습니다.

FAQ

Q: 한 페이지에 여러 개의 히어로 섹션이 있나요? A: <HeroShader />를 여러 번 마운트할 수 있지만, 각각 자체 WebGL 컨텍스트를 생성합니다. 일반적으로 하나만 사용하세요.

Q: 스크롤 가능한 오버레이 내부에서 카드가 기울어지나요? A: 예. 오버레이의 스크롤 위치는 포함하는 카드의 data-holo 스코프에 대해 상대적이지만, 카드가 오버레이에 의해 잘리지 않았다면 기울어집니다. 오버레이가 position: relative이고 뷰포트를 기준으로 스크롤되는 경우 예기치 않은 동작이 발생할 수 있습니다.

Q: 스크램블 텍스트가 제대로 작동하지 않습니다. A: 텍스트가 정적이어야 하며, 스크램블이 완료된 후에 변경되지 않아야 합니다. 또한 <ScrambleText> 내부에 추가적인 HTML 태그가 없어야 합니다. 플레인 텍스트만 감싸야 합니다.

참고 자료

  • references/aurora-shader.md — 쉐이더 수학 상세 설명
  • references/gating-and-performance.md — 빌드 게이팅 및 프레임 예산
  • references/verification.md — Playwright 검사 The tasks json { "name": "anhlamtruong/living-ui-effects", "description": "웹 페이지 로드 후에도 살아 움직이는 인터랙티브 효과를 추가합니다. WebGL 오로라/액체광 쉐이더, 홀로그램 3D 기울기 카드, 커서 추적 그리드 스포트라이트, 글리치/스크램블 텍스트를 제공합니다.", "aiSummary": "이 스킬은 웹 페이지에 고급스럽고 역동적인 시각 효과를 추가하여 사용자 경험을 한층 더 업그레이드합니다. WebGL 기반의 오로라 쉐이더, 마우스 포인터를 따라 기울어지는 홀로그램 카드, 커서를 따라 움직이는 그리드 스포트라이트, 그리고 RGB 분할 글리치 및 호버 시 텍스트 스크램블 효과 등 네 가지 주요 효과를 제공합니다. React 및 Next.js 프로젝트에 쉽게 통합할 수 있도록 설계되었으며, 하나의 공유된 `requestAnimationFrame` 루프를 사용하고 레이아웃 시프트가 없으며 접근성을 고려합니다. 모든 효과는 CSS `transform`, `opacity`, `clip-path` 속성만을 애니메이션하여 성능을 최적화하고, 사용자의 입력 방식(마우스/터치)과 모션 선호도에 따라 적절히 반응합니다. 특히, 콘텐츠가 완전히 로드된 후에만 효과가 작동하도록 설계되어 불필요한 버벅임을 방지하며, 사용하지 않을 때는 프레임 루프를 중지하여 리소스를 절약합니다.", "skillMarkdown": "# 라이브 효과\n\n콘텐츠가 화면에 표시된 후에도 페이지를 계속 움직이게 하는 네 가지 효과:\n\n- **오로라 히어로:** 히어로 영역 뒤에서 포인터를 따라 움직이는 부드러운 WebGL 빛 필드.\n- **홀로그램 카드:** 카드가 커서 방향으로 기울어지고, 살짝 떠오르며 빛 반사와 무지갯빛 테두리가 생깁니다.\n- **그리드 스포트라이트:** 페이지 배경 그리드가 커서 주변에 부드러운 원형으로 밝아집니다.\n- **글리치 및 스크램블 텍스트:** 제목이 나타날 때 RGB 분할 효과가 적용되고, 링크 레이블은 호버 시 디코딩 효과를 냅니다.\n\n기준은 '살아있지만, 결코 방해되지 않는다'입니다. 사용자가 페이지가 자신에게 반응한다고 느껴야 하지만, 어떤 것도 버벅이거나 레이아웃을 밀거나, 클릭을 가로채거나, 화면 리더에서 텍스트를 숨겨서는 안 됩니다.\n\n라이브 예시: https://www.lamanhtruong.com (프로젝트 카드에 호버, 히어로 위로 이동, 내비게이션에 호버).\n\n## `assets/fx/` 폴더 내용\n\n| 파일 | 역할 |\n| --- | --- |\n| `ticker.ts` | 하나의 공유된 `requestAnimationFrame` 루프. 프레임을 요청하는 요소가 없으면 대기 상태로 전환됩니다. |\n| `built.ts` | 준비 상태 게이트 (`isBuilt`, `buildActive`), `frozen()`, 시드 기반 PRNG, 포인터 미디어 쿼리 |\n| `aurora.ts` | WebGL 쉐이더, 품질 조절기 및 생명 주기 (`createAurora`) |\n| `hero-shader.tsx` | `<HeroShader />`, 오로라를 위한 React 래퍼 |\n| `holo.ts` | 카드의 기울기 + 빛 반사 + 광택, 스프링 물리, 주입된 오버레이 |\n| `spotlight.ts` | 커서를 따라가는 그리드 스포트라이트 (transform만 사용) |\n| `glitch.ts`, `glitch-copies.tsx` | 글리치 스케줄러, `<GlitchCopies />` 및 `<GlitchHeading />` |\n| `scramble.ts`, `scramble-text.tsx` | 스크램블 실행기 및 `<ScrambleText />` 마크업 훅 |\n| `fx-root.tsx` | `<FxRoot />`: 홀로그램, 스포트라이트, 글리치 및 스크램블을 한 번에 마운트 |\n| `fx.css` | 모든 스타일, `--fx-*` 사용자 정의 속성으로 테마 적용 |\n\n이 코드는 React만 필요로 하며, 별도의 애니메이션 라이브러리는 필요하지 않습니다. 템플릿은 React 18 및 19 타입에 대해 엄격한 TypeScript 검사를 통과합니다.\n\n## 설치\n\n1. `assets/fx/` 폴더를 앱에 복사합니다 (예: `src/components/fx/`). 파일은 서로 상대 경로로 임포트하므로 함께 두세요.\n2. 마운트합니다. 페이지 자체는 정적 서버 컴포넌트로 유지될 수 있습니다. `FxRoot`와 `HeroShader`만 클라이언트 컴포넌트입니다.\n\ntsx\nimport { FxRoot } from "@/components/fx/fx-root";\nimport { HeroShader } from "@/components/fx/hero-shader";\nimport { GlitchHeading } from "@/components/fx/glitch-copies";\nimport { ScrambleText } from "@/components/fx/scramble-text";\n\nexport default function Page() {\n return (\n <div className="fx-grid"> {/* 고정된 기본 그리드; 자체 그리드가 있으면 생략 /}\n <main className="relative z-[1]"> {/ 콘텐츠는 그리드 + 스포트라이트 위에 위치 /}\n <section className="relative min-h-[80vh]"> {/ 히어로는 position: relative 여야 함 /}\n {/ 첫 번째 자식, 히어로 콘텐츠 뒤에 위치 /}\n …\n \n\n <GlitchHeading as="h2" className="text-5xl font-semibold">Selected work\n\n <article data-holo className="rounded-2xl border p-6">\n <a href="/work/x" className="inline-flex items-center gap-2">\n Read the case study \n \n \n \n {/ 페이지당 한 번, 그리드 래퍼 내부 */}\n \n );\n}\n```\n\n3. 테마 적용: fx.css 상단의 --fx-* 변수를 편집하고, <HeroShader />에 palette / intensity를 전달합니다 (아래 '테마' 참조).\n4. 완료하기 전에 references/verification.md의 Playwright 검사를 통해 확인합니다.\n\nprops로 개별 효과를 비활성화할 수 있습니다: <FxRoot holo={false} spotlight={false} />. 기존 카드의 마크업을 건드리지 않고 기울기 효과를 적용하려면: <FxRoot holo={{ selector: \".card\", within: \"main\" }} />.\n\n## 부드러움을 유지하는 규칙\n\n이 규칙들은 '고급스러움'과 '버벅임'을 구분짓습니다. 각 규칙은 이를 위반하면 눈에 띄는 문제가 발생했기 때문에 존재합니다.\n\n1. 콘텐츠가 준비될 때까지 기다리세요. 다른 스크립트가 여전히 요소를 표시하거나 측정하고 있는 동안에는 절대 기울이기, 글리치 또는 스크램블 효과를 적용하지 마세요. 표시 중인 요소에 변형을 가하면 해당 요소를 애니메이션하는 다른 스크립트의 측정이 손상되어 버벅임처럼 보입니다. built.ts는 하나의 계약을 구현합니다: html[data-build=\"on\"]이 설정되어 있는 동안, 요소는 해당 [data-build-item] 및 [data-build-section]이 [data-built]를 가질 때까지 준비된 것으로 간주되지 않습니다. 페이지에 빌드 엔진이 없으면 모든 것이 즉시 빌드된 것으로 간주됩니다. 모든 인트로 애니메이션은 이러한 속성을 설정하여 참여할 수 있습니다. 자세한 내용은 references/gating-and-performance.md를 참조하세요.\n\n2. 하나의 프레임 루프를 사용하고, 필요 없으면 절전 모드로 전환하세요. 모든 효과는 자체적으로 requestAnimationFrame을 호출하는 대신 ticker.ts를 구독합니다. 구독자는 정착되면 false를 반환하고, 아무도 프레임을 필요로 하지 않으면 루프가 중지됩니다. 유휴 페이지는 비용이 전혀 들지 않으며, 효과가 서로 프레임을 두고 경쟁할 수 없습니다. 글리치는 프레임을 전혀 사용하지 않습니다. CSS 키프레임이 속성에 의해 토글됩니다.\n\n3. transform, opacity 및 clip-path만 애니메이션하세요. 이 속성들은 컴포지터에서 처리됩니다. 여기서 크기, 플로우 내 위치 또는 폰트 메트릭이 변경되지 않으므로 레이아웃 시프트가 발생하지 않습니다. 스크램블은 실제 텍스트를 제자리에 투명하게 유지하고 오버레이에 글리프를 그리므로, 레이블이 변경되어도 너비가 절대 변하지 않습니다.\n\n4. 입력 방식에 맞게 효과를 조정하세요. 기울기 및 따라가기 스포트라이트는 (hover: hover) and (pointer: fine) 환경에서만 존재합니다. 터치 환경에서 손가락 아래에서 기울어지는 것은 버그처럼 보이므로, 터치에는 탭 지점에서 짧은 페이드 효과와 자체적으로 움직이는 쉐이더 렌즈가 제공됩니다.\n\n5. 정지 상태에서는 React의 DOM을 건드리지 마세요. 홀로그램 오버레이 <span>과 인라인 transform은 카드가 활성화될 때 추가되고, 원래대로 돌아왔을 때 제거됩니다. 정지 상태에서는 DOM이 정확히 React가 렌더링한 대로 유지되므로, 리렌더링, 하이드레이션 및 카드 자체의 :hover 스타일이 계속 작동합니다. 글리치 레이어와 스크램블 스팬은 마크업에 <GlitchCopies>, <ScrambleText>로 작성되며, 런타임에 React가 소유한 노드 주변에 삽입되지 않습니다. 외부에서 React의 트리를 재구성하면 조정(reconciliation)이 깨집니다.\n\n6. 텍스트를 실제로 유지하세요. 글리치 복사본은 클라이언트 전용이며 aria-hidden이므로, 정적 HTML, 페이지 내 찾기 및 화면 리더는 각 제목을 한 번만 봅니다. 스크램블 오버레이는 aria-hidden이고 실제 레이블은 전체적으로 접근 가능한 이름으로 유지됩니다.\n\n7. 정리하고 StrictMode에서도 살아남으세요. 모든 setup* 함수는 리스너, 옵저버, 오버레이 및 인라인 스타일을 제거하는 폐기자(disposer)를 반환합니다. 쉐이더는 버퍼와 프로그램을 해제하지만 의도적으로 WEBGL_lose_context를 호출하지 않습니다. React StrictMode는 동일한 캔버스에서 마운트, 마운트 해제 및 다시 마운트를 수행하는데, 컨텍스트를 잃으면 영원히 복구되지 않기 때문입니다.\n\n8. GPU 예산을 관리하세요. 쉐이더는 픽셀 예산 하에 축소된 해상도로 렌더링하고, 표시될 때만 그리며, 고주사율 화면에서 초당 약 80회로 제한하고, 페이지가 약 38fps 미만으로 실행되면 한 단계 품질을 낮춥니다. 소프트웨어 폴백(failIfMajorPerformanceCaveat)에서는 시작을 거부하고, 느린 것보다는 아무것도 렌더링하지 않습니다.\n\n## 효과\n\n### 오로라 히어로 쉐이더 (aurora.ts, hero-shader.tsx)\n\n쉐이더는 도메인 왜곡된 밸류 노이즈(fbm으로 왜곡된 fbm)로, 넓은 광선과 왜곡된 윤곽선을 따라 얇고 밝은 가닥으로 그려집니다. 포인터는 렌즈 역할을 하여 주변 흐름을 확대하고 소용돌이치게 합니다. 히어로 영역에 진입하거나, 클릭하거나, 빠르게 움직이면 부드러운 리플 링이 떨어집니다. 출력은 미리 곱해진 알파이며 모든 가장자리에서 희미해져서 페이지 배경과 고정 그리드가 비쳐 보이고, 히어로가 상자 형태로 끝나는 대신 다음 섹션으로 부드럽게 블렌딩됩니다.\n\n생명 주기: 캔버스는 해당 섹션이 빌드된 후 1.8초에 걸쳐 페이드 인됩니다. 섹션이 화면에 보이는 동안(80px 마진의 IntersectionObserver) 그리고 탭이 표시되는 동안에만 렌더링합니다. WebGL이 없으면 아무것도 렌더링하지 않습니다.\n\n설정 가능 항목:\n\n- palette: { veil, primary, secondary, accent } (16진수). 팔레트는 쉐이더에 컴파일되므로 변경하려면 리마운트가 필요합니다.\n- intensity: 빛의 강도를 곱합니다. 0.6은 차분하고, 1.4는 강렬합니다.\n- 텍스트 가독성: 쉐이더는 일반적으로 히어로 텍스트가 위치하는 왼쪽 아래에서 빛을 덜 발산합니다. 텍스트가 다른 곳에 있다면 bias 라인을 변경하세요 (참조 문서 참조).\n\n쉐이더 수학, 속도 또는 품질 조절기를 변경하기 전에 references/aurora-shader.md를 읽으세요.\n\n### 홀로그램 카드 (holo.ts)\n\n위임된 pointerover가 정밀 포인터 아래의 카드를 찾습니다. 활성화되면 카드의 변환되지 않은 박스를 측정하고, 하나의 aria-hidden 오버레이 스팬을 주입한 후, 포인터를 향한 기울기를 목표로 임계 감쇠 스프링(120Hz로 서브스텝되어 60 또는 144fps에서 동일하게 작동)을 실행합니다. 커서 아래의 가장자리가 뷰어 쪽으로 옵니다. 떠날 때는 부드러운 오버슈트와 함께 다시 스프링백되고, 완전히 안정되면 모든 것을 제거합니다.\n\n- 기울기는 카드 크기에 비례하며 2.5° ~ 7.5°입니다. 큰 카드는 먼 가장자리가 너무 많이 흔들리지 않도록 덜 기울어집니다. 원근감(perspective)은 큰 변의 1.6배입니다. 떠오르는 높이는 3px입니다.\n- 오버레이는 포인터 위치의 방사형 빛 반사, 희미한 대각선 컬러 워시, 그리고 테두리에 마스킹된 원뿔형 그라데이션 링(전체적으로는 어둡고 포인터 근처는 밝음)을 가집니다.\n- overflow: hidden 카드의 경우 1px 테두리 상자가 잘리므로 링은 바로 안쪽에 그려집니다(.fx-holo--in, 자동 감지).\n- 오버레이는 z-index: 3에 pointer-events: none으로 위치하므로, 카드 내부의 컨트롤(비디오 토글, 링크) 위에 그려지면서도 클릭을 방해하지 않습니다.\n- 정적 위치(position: static) 카드는 활성화된 동안에만 position: relative를 얻습니다.\n- 옵트아웃: 카드 또는 조상에 data-holo=\"off\"를 설정합니다. 누군가 실제 rect를 읽는 동안(검사기, 스크린샷 패스) html[data-fx-freeze]로 모든 기울기를 일시 중지합니다. living-ui-swarm 검사 토글은 자동으로 감지됩니다.\n\n### 그리드 스포트라이트 (spotlight.ts, div.fx-spot)\n\n560px 고정 윈도우가 방사형으로 마스킹되어 기본 그리드의 밝은 복사본을 표시합니다. 각 프레임마다 두 개의 변형만 이동합니다: 윈도우는 커서를 따라가고, 내부 그리드는 floor(x / cell) * cell - x만큼 다시 이동하여 선이 기본 그리드의 격자에 정확히 맞춰집니다. 두 그리드 모두 뷰포트 원점에 고정되어야 하며, .fx-grid::before가 그렇게 합니다. 후행 lerp는 안정되고 커서가 멈추면 루프가 중지됩니다. 창이 흐려지거나 포인터가 창을 벗어나면 숨겨집니다. 터치는 탭 지점에서 360ms 페이드를 얻습니다.\n\n그리드 셀, 미세선 간격 및 윈도우 크기는 --fx-grid-cell, --fx-grid-fine 및 --fx-spot-size (px 값)에서 가져오므로 CSS가 단일 진실 공급원으로 유지됩니다.\n\n### 글리치 제목 (glitch.ts, <GlitchHeading> / <GlitchCopies>)\n\n마크업: 실제 제목(.fx-glitch__base)과 두 개의 클라이언트 전용 복사본을 보유하는 .fx-glitch[data-fx-glitch] 래퍼. GlitchHeading이 이 모든 것을 작성합니다. 제목이 이미 존재하고 다른 속성을 가지고 있다면 GlitchCopies를 직접 사용하세요.\n\n- 입장: 빌드된 제목이 최소 50% 이상 뷰에 들어왔을 때 560ms 동안 지속됩니다. 동일한 제목은 2.5초 이내에 재생되지 않습니다.\n- 마이크로 글리치: 814초마다 무작위로 보이는 제목 하나에 260ms 동안 발생합니다.\n- 무작위성은 시드(seed) 기반이므로, 모든 방문과 테스트에서 리듬이 동일합니다.\n- 한 번에 하나의 제목만 글리치됩니다. 입장 효과는 서로 대기열에 추가됩니다.\n- [data-glitch]에 의해 전환되는 순수 CSS 키프레임으로 구현됩니다: clip-path: inset()으로 슬라이스하고, translate3d로 오프셋하며, text-shadow로 색상을 분할합니다.\n- --fx-gi는 실행마다 오프셋을 조정합니다.\n\n### 스크램블 텍스트 (scramble.ts, <ScrambleText>)\n\n아이콘을 감싸지 말고 레이블만 감싸세요: <a><Icon/> <ScrambleText>{label}</ScrambleText></a>.\n\n- 트리거: 호버(터치 아님) 또는 포함하는 a, button 또는 [data-fx-scr-host]의 키보드 :focus-visible.\n- 지속 시간: 길이에 따라 360520ms.\n- 해결: 문자는 약간의 지터와 함께 왼쪽에서 오른쪽으로 해결됩니다. 해결되지 않은 꼬리는 모든 프레임이 아닌 ~18Hz로 다시 그려지므로 노이즈가 아닌 디코딩으로 읽힙니다.\n- 글리프 풀: 대소문자가 일치하며, @ . / : -와 같은 구두점은 그대로 유지되어 이메일과 경로가 스크램블 중에도 읽을 수 있습니다.\n- 정리: 오버레이는 항상 비워집니다. 여기에는 rAF가 중지되는 백그라운드 탭을 위한 타이머 폴백이 포함됩니다.\n\n일반 인라인 링크에는 inline을 사용하세요 (밑줄 유지). 채워진 버튼에는 quiet를 사용하고, 버튼에 --fx-scr-tail: currentColor를 함께 설정하세요.\n\n## 테마\n\n| 변수 | 사용처 | 기본값 |\n| --- | --- | --- |\n| --fx-accent | 글리치 레이어 A, 스크램블 글리프, 스포트라이트 광선, 홀로그램 그림자 | #5eead4 |\n| --fx-hot | 글리치 레이어 B | #f472b6 |\n| --fx-sheen-1..5 | 홀로그램 링, 원 주위 순서대로 | 청록, 파랑, 보라, 분홍, 호박 |\n| --fx-grid-line, --fx-grid-fine-line | 기본 그리드 선 | 낮은 알파 슬레이트 |\n| --fx-grid-cell, --fx-grid-fine | 그리드 간격 (px) | 96px, 24px |\n| --fx-spot-size | 스포트라이트 윈도우 (px) | 560px |\n| --fx-mono | 스크램블 글리프 폰트 | 시스템 모노 |\n| --fx-scr-tail | 요소별 글리프 색상 재정의 | 설정되지 않음 |\n\n색상은 color-mix(in srgb, …)을 통해 혼합되며, 모든 최신 브라우저에서 지원됩니다. 쉐이더는 자체 팔레트를 사용합니다: <HeroShader palette={{ accent: \"#22d3ee\", secondary: \"#f0abfc\" }} intensity={0.8} />.\n\n페이지 강조 색상과 같은 계열의 쉐이더 색상을 선택하세요. 쉐이더는 분위기를 위한 것이지 두 번째 브랜드 컬러가 아닙니다. 처음 방문한 사용자가 헤드라인보다 쉐이더를 먼저 알아차린다면 너무 강한 것입니다.\n\n## 레이어링\n\n| 레이어 | z-index | 참고 |\n| --- | --- | --- |\n| 기본 그리드 (.fx-grid::before, fixed) | 0 | 뷰포트 원점에 고정 |\n| 스포트라이트 (div.fx-spot, fixed) | 0 | 트리 순서상 그리드 뒤에 있어 그 위에 페인트됨 |\n| 콘텐츠 (main, footer) | 1 | z-index: 1로 위치 지정되어야 함 |\n| 히어로 쉐이더 (canvas.fx-shader) | auto | 히어로 내부에서 절대 위치, 첫 번째 자식이므로 이후 콘텐츠가 위에 페인트됨 |\n| 홀로그램 오버레이 | 카드 내에서 3 | pointer-events: none |\n| 고정 헤더, 빌드 애니메이션 오버레이 캔버스 | 콘텐츠 위 | 영향 없음 |\n\n텍스트 위에 스포트라이트가 표시된다면 콘텐츠 래퍼에 position: relative; z-index: 1이 누락된 것입니다.\n\n## 모션 선호도\n\nlamanhtruong.com에서 소유자는 prefers-reduced-motion 설정이 있는 방문자를 포함하여 모든 사람에게 모든 효과를 재생하기로 선택했습니다. 모션이 포트폴리오의 핵심이기 때문입니다. 이는 합법적인 제품 결정이지만, 의도적으로 결정해야 합니다. 설정을 존중하려면 대신:\n\n- <FxRoot motion=\"reduce\" />를 사용하세요. 모든 효과를 축소하거나 비활성화합니다.\n- prefers-reduced-motion: reduce에 대한 자체 CSS를 작성할 수도 있습니다.\n\n## FAQ\n\nQ: 한 페이지에 여러 개의 히어로 섹션이 있나요?\nA: <HeroShader />를 여러 번 마운트할 수 있지만, 각각 자체 WebGL 컨텍스트를 생성합니다. 일반적으로 하나만 사용하세요.\n\nQ: 스크롤 가능한 오버레이 내부에서 카드가 기울어지나요?\nA: 예. 오버레이의 스크롤 위치는 포함하는 카드의 data-holo 스코프에 대해 상대적이지만, 카드가 오버레이에 의해 잘리지 않았다면 기울어집니다. 오버레이가 position: relative이고 뷰포트를 기준으로 스크롤되는 경우 예기치 않은 동작이 발생할 수 있습니다.\n\nQ: 스크램블 텍스트가 제대로 작동하지 않습니다.\nA: 텍스트가 정적이어야 하며, 스크램블이 완료된 후에 변경되지 않아야 합니다. 또한 <ScrambleText> 내부에 추가적인 HTML 태그가 없어야 합니다. 플레인 텍스트만 감싸야 합니다.\n\n## 참고 자료\n\n- references/aurora-shader.md — 쉐이더 수학 상세 설명\n- references/gating-and-performance.md — 빌드 게이팅 및 프레임 예산\n- references/verification.md — Playwright 검사" }

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

관련 스킬