Disintegrator는 하나의 완전한 시각 설정, 선택적 오디오, 캡처, 레이아웃 애니메이션, 백그라운드 준비와 보관 노드 저장소를 연결합니다.
엔트리 포인트
| 엔트리 | 런타임 구성 | 추가 의존성 |
|---|---|---|
vanilla-disintegrate/core | Disintegrator, 효과 및 프리셋 헬퍼와 모든 공개 타입 | 없음 |
vanilla-disintegrate/particles | 파티클 팩토리, 시각 프리셋과 파티클 타입 | 없음 |
vanilla-disintegrate/sounds | 내장 오디오 파일 네 개의 URL과 사운드 타입 | 없음 |
vanilla-disintegrate | Disintegrator, 완전한 프리셋 네 개, 파티클, 사운드, 헬퍼와 공개 타입 | 없음 |
vanilla-disintegrate/snapdom | SnapDOM을 기본 캡처 어댑터로 설정한 루트 API | @zumer/snapdom |
루트 및 /snapdom 엔트리는 완성된 내장 프리셋 네 개를 등록합니다. /snapdom만 캡처 어댑터를 제공하며, /core는 내장 프리셋, 파티클, 내장 사운드 파일과 캡처 없이 수명 주기만 제공합니다. 어떤 엔트리도 기본 프리셋을 선택하지 않습니다. 파티클, 캡처, 오디오 페이지에서 각 전용 export를 자세히 설명합니다.
생성자
import Disintegrator, { createParticleEffect } from 'vanilla-disintegrate/snapdom';
const ready = new Disintegrator({ preset: 'dust' });
const custom = new Disintegrator({ effect: createParticleEffect() });
preset과 effect 중 정확히 하나가 필요합니다.
type DisintegratorOptions =
| { preset: PresetSelection; effect?: never; sound?: false; fallback?: FallbackEffectDefinition }
| { preset?: never; effect: EffectDefinition; sound?: false | SoundSelection; fallback?: FallbackEffectDefinition };
프리셋은 시각 효과와 오디오를 합친 원자적 선택입니다. sound: false로 음소거할 수 있지만 다른 시각 효과나 대체 오디오와 조합할 수 없습니다. 사용자 효과는 완전한 remove 및 restore 단계를 가진 객체이며 사운드 쌍을 제공하지 않으면 무음입니다.
공통 옵션:
| 옵션 | 타입 | 기본값 | 역할 |
|---|---|---|---|
presets | Record<string, PresetDefinition> | {} | preset으로 선택할 완성된 사용자 프리셋을 추가 |
layout | boolean | LayoutOptions | true | 삭제 후 이웃 요소를 새 위치로 이동 |
preparation | boolean | PreparationOptions | visible-idle | 등록된 요소의 스냅샷을 준비 |
audioPreparation | false | 'immediate' | 'idle' | AudioPreparationOptions | 'idle' | 준비와 디코딩 오디오 소유권 예산을 제어 |
audioWaitTimeout | number | false | 1500 | 소리 없이 계속하기 전 최대 오디오 대기 시간 |
respectReducedMotion | boolean | true | 사용자가 모션 감소를 요청하면 애니메이션을 건너뜀 |
fallback | FallbackEffectDefinition | — | WebGL2 렌더러를 사용할 수 없을 때 단계를 무음으로 대체 |
capture | SnapshotCapture | SnapDOM¹ | 스냅샷 기반 효과를 위해 요소를 canvas로 변환 |
overlayRoot | HTMLElement | () => HTMLElement | document.body | 고정 위치 애니메이션 오버레이를 받음 |
zIndex | number | 2147483646 | 오버레이 스택 레벨을 설정 |
random | () => number | Math.random | 작업마다 재현 가능한 의사 난수 스트림을 초기화 |
snapdom | SnapdomCaptureOptions | — | SnapDOM의 toCanvas() 설정, /snapdom에서만 사용 |
onTrigger | (context) => void | — | 검증 뒤 캡처 전에 실행 |
onStart | (context) => void | — | DOM 변경 뒤 재생이 시작될 때 실행 |
onComplete | (context) => void | — | 시각 단계가 정상적으로 끝난 뒤 실행 |
onError | (error, context) => void | — | 격리된 캡처, 애니메이션, 사운드와 콜백 오류를 받음 |
¹ SnapDOM은 /snapdom에서만 기본값입니다. 선택한 효과에 스냅샷이 필요하면 루트 및 /core 엔트리에 capture를 제공해야 합니다.
백그라운드 스냅샷 오류는 context.operation === 'prepare'를, 작업 오류는 remove 또는 restore를 보고합니다.
LayoutOptions
레이아웃 애니메이션은 FLIP 기법으로 삭제 후 영향을 받은 요소를 이전 위치에서 새 위치로 이동합니다.
| 옵션 | 타입 | 기본값 | 역할 |
|---|---|---|---|
enabled | boolean | true | 다른 기본값을 유지하면서 레이아웃 애니메이션 활성화 |
duration | number | 300 | 리플로 애니메이션 시간(밀리초) |
easing | string | 'cubic-bezier(0.22, 1, 0.36, 1)' | WAAPI에 전달할 CSS timing function |
container | HTMLElement | (element) => HTMLElement | null | 대상의 부모 | 좌표계와 선택적 높이 애니메이션 대상을 정의 |
siblings | 'following' | 'all' | (element, container) => HTMLElement[] | 'following' | 참여할 자식 요소 선택 |
animateContainer | boolean | true | 변경된 컨테이너 높이에 애니메이션 적용 |
layout: false를 전달하면 리플로 애니메이션이 비활성화됩니다. layout은 remove()에만 적용됩니다. 복원 시 삽입된 요소는 최종 레이아웃 위치에서 애니메이션됩니다. 음수 duration은 0으로, 유한하지 않은 값은 기본값으로 정규화됩니다.
PreparationOptions
| 옵션 | 타입 | 기본값 | 역할 |
|---|---|---|---|
strategy | 'immediate' | 'idle' | 'visible-idle' | 'visible-idle' | 등록된 요소의 캡처 허용 시점 선택 |
shouldPrepare | (element) => boolean | () => true | 예약 전에 요소 필터링 |
root | Element | Document | null | null | Intersection Observer 루트 |
rootMargin | string | '300px 0px' | 요소가 뷰포트에 들어오기 전에 visible-idle 작업 시작 |
concurrency | number | 1 | 최대 동시 캡처 수. 1에서 8 사이 정수로 정규화 |
invalidateOnResize | boolean | true | 크기가 바뀌면 준비된 스냅샷 무효화 |
observeMutations | boolean | false | 하위 트리 변경과 자손 로드 이벤트 감시 |
idleTimeout | number | 750 | requestIdleCallback 최대 대기 시간(밀리초) |
fallbackDelay | number | 250 | idle callback을 사용할 수 없을 때의 지연 |
scrollSettle | number | 120 | 스크롤 후 백그라운드 작업 전 대기 시간 |
animationSettle | number | 400 | 캡처 전 활성 요소 애니메이션의 최대 대기 시간 |
cachePixelBudget | number | 8_000_000 | canvas 물리 픽셀 단위의 LRU 용량 |
preparation: false는 자동 준비를 비활성화하지만 명시적 prepare() 호출은 막지 않습니다. 음수인 시간과 캐시 예산은 0으로, 유한하지 않은 값은 각 기본값으로 정규화됩니다.
AudioPreparationOptions
| 옵션 | 타입 | 기본값 | 역할 |
|---|---|---|---|
strategy | 'immediate' | 'idle' | 'idle' | 네이티브 오디오를 가져오고 디코딩할 시점 선택 |
sounds | SoundPreparationSelection | 인스턴스 사운드 | 인스턴스 사운드 대신 준비할 사운드 선택 |
cacheByteBudget | number | 8 * 1024 * 1024 | 인스턴스별 디코딩 PCM 데이터의 LRU 소유권 예산 |
audioPreparation: false는 자동 작업을 비활성화합니다. 명시적 오디오 준비는 계속 사용할 수 있습니다. 음수 캐시 예산은 0으로 정규화됩니다.
완성된 프리셋
import Disintegrator, { createParticleEffect, definePreset } from 'vanilla-disintegrate/snapdom';
const dissolve = definePreset({
effect: createParticleEffect({
remove: { curve: 'float' },
restore: { curve: 'settle' },
}),
sound: {
remove: { src: '/audio/out.ogg', volume: 0.4 },
restore: { src: '/audio/in.ogg', volume: 0.4 },
},
});
const effects = new Disintegrator({
preset: 'dissolve',
presets: { dissolve },
});
definePreset()은 프리셋, 시각 단계와 네이티브 사운드 옵션 객체를 동결하지만 호출자가 소유한 바이너리 오디오 데이터는 동결하지 않습니다.
const builtInPresets: Readonly<Record<'dust' | 'scatter' | 'vapor' | 'wind', Readonly<PresetDefinition>>>;
builtInPresets는 루트와 /snapdom 엔트리에서 내보냅니다. 각 값은 파티클과 오디오를 합친 완전하고 동결된 프리셋입니다. /particles 엔트리는 시각 설정만 포함한 particlePresets를 대신 내보냅니다.
사용자 효과 계약
import { defineEffect } from 'vanilla-disintegrate/core';
interface EffectPhase {
requires?: 'webgl2';
needsSnapshot?: boolean;
animate: AnimationFactory;
}
interface EffectDefinition {
remove: EffectPhase;
restore: EffectPhase;
}
const effect = defineEffect({
remove: { needsSnapshot: false, animate: removeFactory },
restore: { needsSnapshot: false, animate: restoreFactory },
});
needsSnapshot의 기본값은 true입니다. defineEffect()는 효과와 두 단계 객체를 동결하면서 TypeScript가 추론한 좁은 타입을 보존합니다.
WebGL2 폴백 효과
createParticleEffect()와 내장 프리셋은 단계에 requires: 'webgl2'를 자동으로 표시합니다. 라이브러리는 캡처 전에 WebGL2를 확인합니다. WebGL2가 꺼져 있거나 사용할 수 없으면 fallback이 기본 단계를 대신 실행하므로 불필요한 Canvas 스냅샷을 만들지 않습니다.
import Disintegrator, { defineEffect } from 'vanilla-disintegrate/snapdom';
const fade = defineEffect({
remove: {
needsSnapshot: false,
animate: ({ element }) => element.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 180 }),
},
restore: {
needsSnapshot: false,
animate: ({ element }) => element.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 180 }),
},
});
const effects = new Disintegrator({ preset: 'dust', fallback: fade });
폴백은 프리셋뿐 아니라 requires: 'webgl2'를 선언한 모든 사용자 효과에도 적용됩니다. CSS, WAAPI, Canvas 2D, SVG 또는 다른 렌더러를 사용할 수 있으며 기본적으로 무음입니다. TypeScript는 fallback에 또 다른 WebGL2 단계를 전달하지 못하게 하고, JavaScript 구성에도 런타임에서 같은 검증을 적용합니다. 사전 확인 뒤에도 파티클 렌더러가 시작하지 못하면 라이브러리는 이미 만든 스냅샷 뒤에 폴백을 시도합니다.
애니메이션 팩토리는 AnimationContext를 받습니다.
| 필드 | 타입 | 의미 |
|---|---|---|
operation | 'remove' | 'restore' | 현재 단계 |
element | HTMLElement | 애플리케이션의 원본 요소 |
layer | HTMLElement | 라이브러리가 소유하는 비어 있고 위치가 지정된 오버레이 |
snapshot | HTMLCanvasElement | null | 원본 캡처. 스냅샷 없는 단계에서는 null |
visual | HTMLCanvasElement | null | 필요할 때 생성되는 snapshot의 전체 크기 복사본 |
bounds | DOMRectReadOnly | 오버레이가 사용하는 최종 뷰포트 위치와 크기 |
signal | AbortSignal | 취소 또는 인스턴스 제거 시 중단 |
reducedMotion | false | 모션 감소 요청은 팩토리 실행 전에 건너뜀 |
random | () => number | 작업별 의사 난수 스트림 |
addCleanup | (callback: () => void) => void | 완료, 취소, 실패에 모두 실행할 정리 함수 등록 |
AnimationFactory는 Web Animation, 임의의 PromiseLike, AnimationPlayback 컨트롤러를 반환하거나 애니메이션을 만들 수 없을 때 null을 반환할 수 있습니다.
interface AnimationPlayback {
finished: PromiseLike<unknown>;
element?: HTMLElement;
duration?: number;
layoutDelay?: number;
cancel?(): void;
dispose?(): void;
}
element에 아직 부모가 없다면 layer에 추가됩니다. duration은 네이티브 오디오를 맞추고, layoutDelay는 삭제 리플로를 지연하며, cancel()은 재생을 멈추고, dispose()는 모든 종료 경로에서 렌더러 리소스를 해제합니다.
remove(target, options?)
해석된 요소를 분리하고 EffectOperation을 동기적으로 반환합니다.
remove(target: EffectTarget, options?: RemoveOptions): EffectOperation;
const operation = effects.remove('.card', {
preset: 'scatter',
sound: false,
retain: true,
layout: { duration: 260 },
detach: (element) => element.remove(),
});
target은 HTMLElement 또는 선택자 문자열입니다. 선택자를 찾지 못하면 작업 시작 전에 오류를 던집니다.
detach는 기본 element.remove() 커밋을 대체합니다. 반응형 렌더러가 자체 상태로 노드를 제거해야 할 때 사용하세요. 콜백이 오류를 던지면 onError로 전달하고 네이티브 제거를 안전한 폴백으로 사용합니다.
restore(target, options?)
연결되고 측정 가능한 요소를 현재 지오메트리에서 애니메이션합니다.
restore(target: EffectTarget, options?: RestoreOptions): EffectOperation;
const operation = effects.restore(element, {
preset: 'scatter',
sound: false,
});
요소가 분리됐거나 너비 또는 높이가 0이면 오류를 던집니다.
두 작업 모두 하나의 완성된 preset, 해당 단계의 선택적 사운드 정의와 함께 사용자 effect, 또는 아무것도 전달하지 않아 인스턴스나 보관 노드의 표현을 상속할 수 있습니다. 상속 오디오와 프리셋 오디오는 sound: false로만 음소거할 수 있습니다.
작업 옵션
| 옵션 | 적용 대상 | 의미 |
|---|---|---|
preset | 두 작업 | 등록된 이름 또는 인라인 완전 프리셋 선택 |
effect | 두 작업 | 완전한 사용자 효과 선택. preset과 함께 사용할 수 없음 |
sound | 두 작업 | 명시적 사용자 효과에 현재 단계 사운드를 추가하거나 false로 음소거 |
onTrigger | 두 작업 | 캡처 전 작업 콜백 추가 |
onStart | 두 작업 | DOM 커밋 후 작업 콜백 추가 |
onComplete | 두 작업 | 시각 효과의 정상 완료 후 작업 콜백 추가 |
onError | 두 작업 | 격리된 오류 콜백 추가 |
retain | 삭제 | 분리된 원본을 operation.removalId로 저장 |
layout | 삭제 | 이번 삭제의 레이아웃 애니메이션 활성화 또는 재정의 |
detach | 삭제 | 기본 element.remove() 커밋 대체 |
작업 콜백은 인스턴스 콜백과 함께 추가로 실행됩니다. preset과 effect를 모두 생략하면 표현을 상속합니다. 상속된 사운드와 프리셋 사운드는 음소거할 수 있지만 독립적으로 교체할 수 없습니다.
보관 요소
const element = effects.take(removalId); // HTMLElement | null
const discarded = effects.discard(removalId); // boolean
const count = effects.discardAll(); // number
take()은 저장 항목을 소비합니다. discard()와 discardAll()은 콘텐츠를 삽입하지 않고 참조를 해제합니다.
스냅샷 준비
const unregister = effects.register(elements);
await effects.prepare(elements);
effects.invalidate(elements);
effects.clearPrepared();
invalidate()는 context.invalidate를 통해 준비된 canvas와 캡처 엔진을 함께 갱신합니다. 명시적인 prepare()도 갱신을 요청하며 clearPrepared() 후 다음 캡처는 새로 갱신해야 합니다. DOM 관찰자가 감지할 수 없는 CSSOM 변경도 포함됩니다. 자체 캐시가 있는 사용자 정의 어댑터는 context.invalidate를 처리해야 합니다.
register()는 설정된 전략을 따릅니다. 백그라운드 준비가 꺼져 있어도 prepare()는 즉시 캡처합니다. 같은 요소의 반복 등록은 참조 카운트를 사용하며 각 해제 함수는 자신의 등록만 해제합니다.
register()는 멱등인 () => void를 반환하고, prepare()는 Promise<void>를 반환하며, invalidate()와 clearPrepared()는 void를 반환합니다.
오디오 준비
await effects.prepareAudio({
remove: { src: 'dust' },
restore: { src: 'dust', reverse: true },
});
effects.discardPreparedAudio();
effects.clearPreparedAudio();
인수가 없으면 prepareAudio()와 discardPreparedAudio()는 인스턴스에 설정한 사운드 쌍을 사용합니다. 자동 준비가 꺼져 있어도 명시적 메서드는 동작합니다. 디코딩 버퍼는 제한된 인스턴스 소유권 LRU를 사용하고 요청, 버퍼와 AudioContext는 인스턴스 간 공유합니다.
prepareAudio()는 Promise<void>를 반환하고, discardPreparedAudio()는 해제한 인스턴스 소유 디코딩 항목 수를 반환하며, clearPreparedAudio()는 void를 반환합니다.
destroy()
effects.destroy();
활성 작업을 취소하고 관찰자와 백그라운드 작업을 중지하며 스냅샷과 디코딩 오디오 소유권을 해제하고 보관 노드를 비웁니다. 마지막 소유자가 파괴되면 공유 재생 컨텍스트가 닫힙니다. destroy()를 반복 호출해도 안전하며 이후 새 작업을 시작하면 오류를 던집니다.
EffectOperation
interface EffectOperation {
readonly operation: 'remove' | 'restore';
readonly removalId: RemovalId | null;
readonly finished: Promise<{
operation: 'remove' | 'restore';
status: 'completed' | 'cancelled' | 'skipped' | 'rejected';
removalId: RemovalId | null;
}>;
cancel(): void;
}
핸들은 요소를 노출하지 않고 완료된 뒤 참조를 해제합니다.
| 상태 | 콘텐츠 동작 | finished 완료 시점 |
|---|---|---|
completed | 커밋됨, 시각 단계가 정상적으로 완료 | 시각 정리 후 |
cancelled | 커밋됨, 시각 또는 오디오 재생이 중지 | 취소 정리 후 |
skipped | 실행 가능한 시각 단계 없이 커밋 | 폴백 정리 후 |
rejected | 시작하지 않음, DOM이 변경되지 않음 | 현재 소유자가 끝난 뒤 |
콜백과 우선순위
인스턴스 콜백은 모든 작업에 실행되고 작업 콜백은 그와 함께 실행됩니다. 명시적 작업 표현, 보관 노드가 기억한 표현, 인스턴스 설정 순서로 우선합니다. 이미 사용 중인 요소에 대한 동시 호출은 현재 소유자가 끝난 뒤 rejected로 완료됩니다.
interface EffectContext {
operation: 'remove' | 'restore';
element: HTMLElement;
overlay: HTMLElement | null;
removalId: RemovalId | null;
}
interface EffectErrorContext {
operation: 'remove' | 'restore' | 'prepare';
element: HTMLElement;
overlay: HTMLElement | null;
removalId: RemovalId | null;
}
onTrigger는 검증과 측정 뒤, 캡처 및 DOM 커밋 전에 실행되며 이때 overlay는 null입니다. onStart는 콘텐츠 작업이 커밋되고 시각 재생이 시작된 뒤 실행됩니다. onComplete는 시각 효과와 레이아웃이 정상적으로 완료된 경우에만 실행됩니다. onError는 준비, 캡처, 애니메이션, 오디오, 정리 및 애플리케이션 콜백 오류를 받으며, 이러한 오류가 필요한 DOM 작업이나 정리를 중단하지 못하게 합니다.