@ev-ry/fx 0.1.0-rc.2 → 0.1.0-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/QUICKSTART.fa.md +105 -93
- package/README.md +12 -7
- package/build-report.json +63 -58
- package/docs/GUIDE.md +8 -2
- package/docs/RELEASE-NOTES.md +33 -0
- package/examples/evry-website.md +58 -0
- package/package.json +57 -57
- package/src/dom-attachment.d.ts +72 -72
- package/src/dom-free-loader.js +17 -11
- package/src/dom-free-script.js +17 -11
- package/src/dom-free.d.ts +14 -14
- package/src/dom-free.js +62 -38
- package/src/dom-image-surface.js +161 -101
- package/src/dom-image-swap.js +30 -24
- package/src/dom-once.js +39 -34
- package/src/dom-raster-cache.js +33 -33
- package/src/dom-reveal.js +83 -73
- package/src/dom-svg-surface.js +41 -39
- package/src/dom-text-paint-mask.js +24 -0
- package/src/dom-text-surface.js +52 -21
- package/src/font-rasterizer.js +20 -20
- package/src/image-surface.js +161 -157
- package/src/insertion-range.js +29 -29
- package/src/raster-texture-material.js +79 -79
- package/src/raster-texture-mesh.js +35 -35
- package/src/render-owner.js +17 -12
- package/src/runtime-font-engine.js +43 -43
- package/src/text-edit-effect.js +101 -101
- package/src/text-edit-motions.js +15 -15
- package/src/text-effect-options.js +28 -28
- package/src/text-effect-path.js +61 -61
- package/src/text-motion-primitives.js +98 -91
- package/src/text-motion-recipes.js +71 -71
- package/src/triangle-effect.js +195 -194
- package/src/viewport-render-owner.js +322 -316
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# EV-RY website: product showcase
|
|
2
|
+
|
|
3
|
+
The EV-RY marketing website is a real integration case: a four-product showcase, animated captions, one shared FX installation across client-side navigation, and native content at rest.
|
|
4
|
+
|
|
5
|
+
The [EV-RY website](https://ev-ry.com/) and [FX product page](https://ev-ry.com/fx/) are public. This file describes an integration pattern, not a copy of the deployed website or a Free-only live demo. Try the package's separate [interactive Free demo](https://kbaghini.github.io/evry-fx/docs/) for its four fixed effects. The website backend, forms and artwork are not dependencies of FX.
|
|
6
|
+
|
|
7
|
+
## Initial reveal without a flash
|
|
8
|
+
|
|
9
|
+
Place `data-thd-pending` on the image and caption elements in the HTML, use the documented boot mask, then let `revealOnView` transfer the mask to the first rendered frame. Keep each returned handle until cleanup. Do not remove the mask manually on attachment readiness.
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
const imageSurface = engine.attachImage(image, {
|
|
13
|
+
presentation: 'global',
|
|
14
|
+
revealOnView: { threshold: 0, once: true }
|
|
15
|
+
});
|
|
16
|
+
const captionSurfaces = captionLines.map(line => {
|
|
17
|
+
const surface = engine.attachText(line, {
|
|
18
|
+
presentation: 'local',
|
|
19
|
+
revealOnView: { threshold: 0, once: true }
|
|
20
|
+
});
|
|
21
|
+
surface.play('enter');
|
|
22
|
+
return surface;
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The captions explicitly start with the slide, even when below the viewport. `play()` takes control from automatic intersection reveal while retaining its initial paint mask until a rendered frame. It does not wait for visibility or restart on reentry. Without explicit `play()`, `revealOnView` continues to wait for the configured intersection and follows its `once` setting.
|
|
27
|
+
|
|
28
|
+
## Alternate two image transitions
|
|
29
|
+
|
|
30
|
+
After decoding the next mounted image, alternate between incoming snow over the previous image and outgoing melt above the already visible next image:
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
const transition = engine.swapImage(previous, next, {
|
|
34
|
+
enter: !melt,
|
|
35
|
+
exit: melt,
|
|
36
|
+
topImage: melt ? 'previous' : 'next',
|
|
37
|
+
waitForExit: false,
|
|
38
|
+
presentation: 'global'
|
|
39
|
+
});
|
|
40
|
+
const result = await transition.finished;
|
|
41
|
+
if (result.status === 'completed') previous.remove();
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Use a busy flag to prevent overlapping swaps. Pause scheduling while the document is hidden. On navigation, cancel an outstanding transition, clear the scheduled timeout and destroy page handles; retain the shared engine and refresh its automatic attachments after inserting the new page. Install the boot mask before the new DOM can paint. Destroy the engine only when the application is disposed.
|
|
45
|
+
|
|
46
|
+
The five-second scheduling interval, caption layout, routing and product artwork belong to the website, not to the FX API. Local caption surfaces share rendering infrastructure; the retained scratch buffer avoids resizing the GPU buffer for each caption line.
|
|
47
|
+
|
|
48
|
+
Create incoming captions from fresh authored markup rather than cloning a currently attached element: a live attachment can carry temporary engine-owned visibility attributes and styles. Commit the incoming image and caption together only after `transition.finished` reports `completed`. If image decoding, attachment or the swap fails, destroy incoming caption handles, remove their layer, retain the previous caption and image, then retry. Track each acquired handle immediately so partial attachment failures can also be cleaned up.
|
|
49
|
+
|
|
50
|
+
Schedule one deadline five seconds from each transition start. After completion, wait only for the remaining time. If the transition itself overruns that deadline, start one next transition when available and establish a new deadline; do not discard interval ticks or queue catch-up transitions. Returning from a hidden tab and retrying a failed transition each establish a fresh five-second wait. This keeps start-to-start cadence stable without overlapping jobs.
|
|
51
|
+
|
|
52
|
+
## Completion
|
|
53
|
+
|
|
54
|
+
After `surface.play()`, await `surface.whenFinished()`. It returns a status of `completed`, `cancelled`, or `unsupported`. Completion includes the native handoff; avoid fixed cleanup timers or renderer statistics. Destroy page surfaces when navigating away.
|
|
55
|
+
|
|
56
|
+
Offscreen text and media retain their start time without running per-particle updates or drawing frames. Returning during the effect evaluates its current elapsed time; returning after it ends shows the final state without replay. `whenFinished()` also completes when content remains offscreen, so slider cleanup never depends on scrolling captions into view. It checks the suspended clock infrequently instead of maintaining a render loop. Concurrent waits share a pending promise; starting a new play cancels the previous wait. Cancel or destroy the handle to release an outstanding wait.
|
|
57
|
+
|
|
58
|
+
When an exit is requested offscreen, the engine immediately suppresses native paint while retaining layout and the DOM content. Native paint also remains hidden if an active departure leaves view. Reentry renders only the remaining departure particles; an expired exit stays hidden. Text, image and SVG attachments own this masking, so the site should not toggle native opacity or visibility to emulate it. Cancel/destroy restores the original presentation.
|
package/package.json
CHANGED
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@ev-ry/fx",
|
|
3
|
-
"version": "0.1.0-rc.
|
|
4
|
-
"private": false,
|
|
5
|
-
"type": "module",
|
|
6
|
-
"exports": {
|
|
7
|
-
".": {
|
|
8
|
-
"types": "./src/dom-free.d.ts",
|
|
9
|
-
"import": "./src/dom-free.js"
|
|
10
|
-
},
|
|
11
|
-
"./script": {
|
|
12
|
-
"types": "./src/dom-free-loader.d.ts",
|
|
13
|
-
"import": "./src/dom-free-loader.js"
|
|
14
|
-
},
|
|
15
|
-
"./classic-script": "./src/dom-free-script.js"
|
|
16
|
-
},
|
|
17
|
-
"files": [
|
|
18
|
-
"src",
|
|
19
|
-
"assets",
|
|
20
|
-
"examples",
|
|
21
|
-
"README.md",
|
|
22
|
-
"LICENSE",
|
|
23
|
-
"NOTICE.md",
|
|
24
|
-
"QUICKSTART.fa.md",
|
|
25
|
-
"docs/GUIDE.md",
|
|
26
|
-
"docs/RELEASE-NOTES.md",
|
|
27
|
-
"build-report.json"
|
|
28
|
-
],
|
|
29
|
-
"description": "Textured particle effects for existing HTML text, images and SVG. Native when still.",
|
|
30
|
-
"license": "MIT",
|
|
31
|
-
"author": {
|
|
32
|
-
"name": "Kamran Baghini",
|
|
33
|
-
"url": "https://ev-ry.com"
|
|
34
|
-
},
|
|
35
|
-
"keywords": [
|
|
36
|
-
"animation",
|
|
37
|
-
"particles",
|
|
38
|
-
"webgl",
|
|
39
|
-
"text-effects",
|
|
40
|
-
"image-effects",
|
|
41
|
-
"scroll-reveal",
|
|
42
|
-
"svg",
|
|
43
|
-
"rtl",
|
|
44
|
-
"ev-ry"
|
|
45
|
-
],
|
|
46
|
-
"publishConfig": {
|
|
47
|
-
"access": "public"
|
|
48
|
-
},
|
|
49
|
-
"repository": {
|
|
50
|
-
"type": "git",
|
|
51
|
-
"url": "git+https://github.com/kbaghini/evry-fx.git"
|
|
52
|
-
},
|
|
53
|
-
"bugs": {
|
|
54
|
-
"url": "https://github.com/kbaghini/evry-fx/issues"
|
|
55
|
-
},
|
|
56
|
-
"homepage": "https://
|
|
57
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@ev-ry/fx",
|
|
3
|
+
"version": "0.1.0-rc.4",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./src/dom-free.d.ts",
|
|
9
|
+
"import": "./src/dom-free.js"
|
|
10
|
+
},
|
|
11
|
+
"./script": {
|
|
12
|
+
"types": "./src/dom-free-loader.d.ts",
|
|
13
|
+
"import": "./src/dom-free-loader.js"
|
|
14
|
+
},
|
|
15
|
+
"./classic-script": "./src/dom-free-script.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src",
|
|
19
|
+
"assets",
|
|
20
|
+
"examples",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE",
|
|
23
|
+
"NOTICE.md",
|
|
24
|
+
"QUICKSTART.fa.md",
|
|
25
|
+
"docs/GUIDE.md",
|
|
26
|
+
"docs/RELEASE-NOTES.md",
|
|
27
|
+
"build-report.json"
|
|
28
|
+
],
|
|
29
|
+
"description": "Textured particle effects for existing HTML text, images and SVG. Native when still.",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"author": {
|
|
32
|
+
"name": "Kamran Baghini",
|
|
33
|
+
"url": "https://ev-ry.com"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"animation",
|
|
37
|
+
"particles",
|
|
38
|
+
"webgl",
|
|
39
|
+
"text-effects",
|
|
40
|
+
"image-effects",
|
|
41
|
+
"scroll-reveal",
|
|
42
|
+
"svg",
|
|
43
|
+
"rtl",
|
|
44
|
+
"ev-ry"
|
|
45
|
+
],
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/kbaghini/evry-fx.git"
|
|
52
|
+
},
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/kbaghini/evry-fx/issues"
|
|
55
|
+
},
|
|
56
|
+
"homepage": "https://ev-ry.com/fx/"
|
|
57
|
+
}
|
package/src/dom-attachment.d.ts
CHANGED
|
@@ -1,75 +1,75 @@
|
|
|
1
|
-
export interface SurfaceOptions {
|
|
2
|
-
/** Text/image/SVG only. Re-arm after complete exit. */
|
|
3
|
-
revealOnView?: false | {threshold?: number; once?: boolean; root?: Element | null};
|
|
4
|
-
resting?: 'mesh' | 'native';
|
|
5
|
-
inputEffect?: string;
|
|
6
|
-
inputEffectOptions?: {
|
|
7
|
-
particleShape?: 'triangle' | 'square';
|
|
8
|
-
duration?: number;
|
|
9
|
-
exitEffect?: string;
|
|
10
|
-
formation?: number;
|
|
11
|
-
chaos?: number;
|
|
12
|
-
motion?: number;
|
|
13
|
-
horizontal?: number;
|
|
14
|
-
vertical?: number;
|
|
15
|
-
flipX?: boolean;
|
|
16
|
-
flipY?: boolean;
|
|
17
|
-
exitFlipX?: boolean;
|
|
18
|
-
exitFlipY?: boolean;
|
|
19
|
-
bounceLines?: boolean;
|
|
20
|
-
seed?: number;
|
|
21
|
-
recipe?: Record<string, number | boolean | string>;
|
|
22
|
-
};
|
|
23
|
-
divisions?: 'auto' | number;
|
|
24
|
-
}
|
|
25
|
-
export interface SurfaceState {
|
|
26
|
-
disposed: boolean;
|
|
27
|
-
mode: string;
|
|
28
|
-
reason?: string | null;
|
|
29
|
-
committed?: string | null;
|
|
30
|
-
[key: string]: unknown;
|
|
31
|
-
}
|
|
32
|
-
export interface Surface {
|
|
33
|
-
element: HTMLElement;
|
|
34
|
-
ready: Promise<{mode: string; reason?: string | null}>;
|
|
35
|
-
refresh(): void;
|
|
36
|
-
update(options: Pick<SurfaceOptions, 'inputEffect' | 'inputEffectOptions' | 'resting'>): void;
|
|
37
|
-
stats(): SurfaceState;
|
|
38
|
-
destroy(): void;
|
|
39
|
-
}
|
|
40
|
-
export interface TextSurface extends Surface { cancel?(): void; play(phase?: 'enter' | 'exit'): void; }
|
|
41
|
-
export type DOMPresentation = 'local' | 'global' | 'auto';
|
|
42
|
-
export interface DOMInstallation {
|
|
43
|
-
swapImage(previous: HTMLImageElement, next: HTMLImageElement, options?: SurfaceOptions & {exitEffect?: string | null; waitForExit?: boolean; enterEffect?: boolean; topImage?: 'previous' | 'next'; presentation?: DOMPresentation}): {finished: Promise<{status: string}>; cancel(): void};
|
|
44
|
-
attachSVG(element: SVGSVGElement, options?: SurfaceOptions & {presentation?: DOMPresentation}): Omit<TextSurface, 'element'> & {element: SVGSVGElement};
|
|
45
|
-
animateOnce(element: HTMLElement, options?: SurfaceOptions & {phase?: 'enter' | 'exit'; presentation?: DOMPresentation; layer?: string}): {finished: Promise<{status: string; phase: string}>; cancel(): void};
|
|
46
|
-
attachImage(element: HTMLImageElement, options?: SurfaceOptions & {presentation?: DOMPresentation; layer?: string}): TextSurface;
|
|
47
|
-
attachInput(element: HTMLInputElement | HTMLTextAreaElement, options?: SurfaceOptions & {host?: HTMLElement; presentation?: DOMPresentation; layer?: string}): Surface;
|
|
48
|
-
attachText(element: HTMLElement, options?: SurfaceOptions & {presentation?: DOMPresentation; layer?: string}): TextSurface;
|
|
49
|
-
registerLayer(name: string, options: {root: HTMLDialogElement; zIndex?: number}): void;
|
|
50
|
-
unregisterLayer(name: string): void;
|
|
51
|
-
/** Manual transfer preserving the native editor, scene and effect timeline. */
|
|
52
|
-
transfer(surface: Surface, destination?: {presentation?: DOMPresentation; layer?: string | null}): void;
|
|
53
|
-
routing(surface: Surface): {requested: DOMPresentation; presentation: 'local' | 'global'; layer: string | null; reason: string; pending: boolean; error: string | null};
|
|
54
|
-
refresh(): void;
|
|
55
|
-
stats(): {disposed: boolean; controls: number; contexts: number; copies: number; owners: Partial<Record<string, ReturnType<DOMRenderer['stats']>>>};
|
|
56
|
-
destroy(): void;
|
|
57
|
-
}
|
|
58
|
-
/** Explicit or bounded automatic routing; existing native editors remain owned by the host. */
|
|
59
|
-
export function createDOMInstallation(THREE: Parameters<typeof createDOMRenderer>[0], document: Document, options?: {presentation?: DOMPresentation; documentCanvas?: boolean; escapeEffects?: boolean; zIndex?: number}): DOMInstallation;
|
|
60
|
-
export interface DOMRenderer {
|
|
61
|
-
attachSVG(element: SVGSVGElement, options?: SurfaceOptions): Omit<TextSurface, 'element'> & {element: SVGSVGElement};
|
|
62
|
-
attachImage(element: HTMLImageElement, options?: SurfaceOptions): TextSurface;
|
|
63
|
-
readonly domElement: HTMLCanvasElement | null;
|
|
64
|
-
/** Refresh all attached surfaces after external layout/value changes. */
|
|
65
|
-
refresh(): void;
|
|
66
|
-
attachText(element: HTMLElement, options?: SurfaceOptions): TextSurface;
|
|
67
|
-
attachInput(element: HTMLInputElement | HTMLTextAreaElement, options?: SurfaceOptions & {host?: HTMLElement}): Surface;
|
|
68
|
-
stats(): {disposed: boolean; lost: boolean; contexts: number; controls: number; leases: number; [key: string]: unknown};
|
|
69
|
-
destroy(): void;
|
|
70
|
-
}
|
|
71
|
-
/** THREE is injected: the host retains its existing Three.js dependency. */
|
|
72
|
-
export function createDOMRenderer(THREE: {WebGLRenderer: new (...args: any[]) => any; [key: string]: any}, document: Document, options?: {presentation?: 'local' | 'viewport'; escapeEffects?: boolean; zIndex?: number; root?: HTMLDialogElement}): DOMRenderer;
|
|
1
|
+
export interface SurfaceOptions {
|
|
2
|
+
/** Text/image/SVG only. Re-arm after complete exit when once is false. Explicit play takes over automatic triggering. */
|
|
3
|
+
revealOnView?: false | {threshold?: number; once?: boolean; root?: Element | null};
|
|
4
|
+
resting?: 'mesh' | 'native';
|
|
5
|
+
inputEffect?: string;
|
|
6
|
+
inputEffectOptions?: {
|
|
7
|
+
particleShape?: 'triangle' | 'square';
|
|
8
|
+
duration?: number;
|
|
9
|
+
exitEffect?: string;
|
|
10
|
+
formation?: number;
|
|
11
|
+
chaos?: number;
|
|
12
|
+
motion?: number;
|
|
13
|
+
horizontal?: number;
|
|
14
|
+
vertical?: number;
|
|
15
|
+
flipX?: boolean;
|
|
16
|
+
flipY?: boolean;
|
|
17
|
+
exitFlipX?: boolean;
|
|
18
|
+
exitFlipY?: boolean;
|
|
19
|
+
bounceLines?: boolean;
|
|
20
|
+
seed?: number;
|
|
21
|
+
recipe?: Record<string, number | boolean | string>;
|
|
22
|
+
};
|
|
23
|
+
divisions?: 'auto' | number;
|
|
24
|
+
}
|
|
25
|
+
export interface SurfaceState {
|
|
26
|
+
disposed: boolean;
|
|
27
|
+
mode: string;
|
|
28
|
+
reason?: string | null;
|
|
29
|
+
committed?: string | null;
|
|
30
|
+
[key: string]: unknown;
|
|
31
|
+
}
|
|
32
|
+
export interface Surface {
|
|
33
|
+
element: HTMLElement;
|
|
34
|
+
ready: Promise<{mode: string; reason?: string | null}>;
|
|
35
|
+
refresh(): void;
|
|
36
|
+
update(options: Pick<SurfaceOptions, 'inputEffect' | 'inputEffectOptions' | 'resting'>): void;
|
|
37
|
+
stats(): SurfaceState;
|
|
38
|
+
destroy(): void;
|
|
39
|
+
}
|
|
40
|
+
export interface TextSurface extends Surface { cancel?(): void; play(phase?: 'enter' | 'exit'): void; }
|
|
41
|
+
export type DOMPresentation = 'local' | 'global' | 'auto';
|
|
42
|
+
export interface DOMInstallation {
|
|
43
|
+
swapImage(previous: HTMLImageElement, next: HTMLImageElement, options?: SurfaceOptions & {exitEffect?: string | null; waitForExit?: boolean; enterEffect?: boolean; topImage?: 'previous' | 'next'; presentation?: DOMPresentation}): {finished: Promise<{status: string}>; cancel(): void};
|
|
44
|
+
attachSVG(element: SVGSVGElement, options?: SurfaceOptions & {presentation?: DOMPresentation}): Omit<TextSurface, 'element'> & {element: SVGSVGElement};
|
|
45
|
+
animateOnce(element: HTMLElement, options?: SurfaceOptions & {phase?: 'enter' | 'exit'; presentation?: DOMPresentation; layer?: string}): {finished: Promise<{status: string; phase: string}>; cancel(): void};
|
|
46
|
+
attachImage(element: HTMLImageElement, options?: SurfaceOptions & {presentation?: DOMPresentation; layer?: string}): TextSurface;
|
|
47
|
+
attachInput(element: HTMLInputElement | HTMLTextAreaElement, options?: SurfaceOptions & {host?: HTMLElement; presentation?: DOMPresentation; layer?: string}): Surface;
|
|
48
|
+
attachText(element: HTMLElement, options?: SurfaceOptions & {presentation?: DOMPresentation; layer?: string}): TextSurface;
|
|
49
|
+
registerLayer(name: string, options: {root: HTMLDialogElement; zIndex?: number}): void;
|
|
50
|
+
unregisterLayer(name: string): void;
|
|
51
|
+
/** Manual transfer preserving the native editor, scene and effect timeline. */
|
|
52
|
+
transfer(surface: Surface, destination?: {presentation?: DOMPresentation; layer?: string | null}): void;
|
|
53
|
+
routing(surface: Surface): {requested: DOMPresentation; presentation: 'local' | 'global'; layer: string | null; reason: string; pending: boolean; error: string | null};
|
|
54
|
+
refresh(): void;
|
|
55
|
+
stats(): {disposed: boolean; controls: number; contexts: number; copies: number; owners: Partial<Record<string, ReturnType<DOMRenderer['stats']>>>};
|
|
56
|
+
destroy(): void;
|
|
57
|
+
}
|
|
58
|
+
/** Explicit or bounded automatic routing; existing native editors remain owned by the host. */
|
|
59
|
+
export function createDOMInstallation(THREE: Parameters<typeof createDOMRenderer>[0], document: Document, options?: {presentation?: DOMPresentation; documentCanvas?: boolean; escapeEffects?: boolean; zIndex?: number}): DOMInstallation;
|
|
60
|
+
export interface DOMRenderer {
|
|
61
|
+
attachSVG(element: SVGSVGElement, options?: SurfaceOptions): Omit<TextSurface, 'element'> & {element: SVGSVGElement};
|
|
62
|
+
attachImage(element: HTMLImageElement, options?: SurfaceOptions): TextSurface;
|
|
63
|
+
readonly domElement: HTMLCanvasElement | null;
|
|
64
|
+
/** Refresh all attached surfaces after external layout/value changes. */
|
|
65
|
+
refresh(): void;
|
|
66
|
+
attachText(element: HTMLElement, options?: SurfaceOptions): TextSurface;
|
|
67
|
+
attachInput(element: HTMLInputElement | HTMLTextAreaElement, options?: SurfaceOptions & {host?: HTMLElement}): Surface;
|
|
68
|
+
stats(): {disposed: boolean; lost: boolean; contexts: number; controls: number; leases: number; [key: string]: unknown};
|
|
69
|
+
destroy(): void;
|
|
70
|
+
}
|
|
71
|
+
/** THREE is injected: the host retains its existing Three.js dependency. */
|
|
72
|
+
export function createDOMRenderer(THREE: {WebGLRenderer: new (...args: any[]) => any; [key: string]: any}, document: Document, options?: {presentation?: 'local' | 'viewport'; escapeEffects?: boolean; zIndex?: number; root?: HTMLDialogElement}): DOMRenderer;
|
|
73
73
|
|
|
74
74
|
|
|
75
75
|
|
package/src/dom-free-loader.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
+
/** EV-RY FX Free loader (MIT) — https://ev-ry.com/fx/ */
|
|
1
2
|
import {bootstrapFree} from './dom-free-bootstrap.js';
|
|
2
|
-
|
|
3
|
-
/** Explicit npm/module entry. The first call selects auto mode; later calls reuse readiness. */
|
|
4
|
-
export function loadFree(options={}){
|
|
5
|
-
const window=(options.document??globalThis.document)?.defaultView;
|
|
6
|
-
if(!window)throw TypeError('THD Free needs a browser document');
|
|
7
|
-
if(window.THDFree)return window.THDFree.ready;
|
|
8
|
-
const ready=bootstrapFree(options);
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
3
|
+
|
|
4
|
+
/** Explicit npm/module entry. The first call selects auto mode; later calls reuse readiness. */
|
|
5
|
+
export function loadFree(options={}){
|
|
6
|
+
const window=(options.document??globalThis.document)?.defaultView;
|
|
7
|
+
if(!window)throw TypeError('THD Free needs a browser document');
|
|
8
|
+
if(window.THDFree)return window.THDFree.ready;
|
|
9
|
+
const ready=bootstrapFree(options);
|
|
10
|
+
const namespace=Object.freeze({ready});
|
|
11
|
+
window.THDFree=namespace;
|
|
12
|
+
ready.catch(error=>{
|
|
13
|
+
if(window.THDFree===namespace)delete window.THDFree;
|
|
14
|
+
console.error(error);
|
|
15
|
+
window.dispatchEvent(new window.CustomEvent('thd:error',{detail:error}));
|
|
16
|
+
});
|
|
17
|
+
return ready;
|
|
18
|
+
}
|
package/src/dom-free-script.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
+
/** EV-RY FX Free classic loader (MIT) — https://ev-ry.com/fx/ */
|
|
1
2
|
(function(){
|
|
2
|
-
const script=document.currentScript;
|
|
3
|
-
if(!script?.src)throw Error('THD: use a classic script tag, or import loadFree from @thd/free/script');
|
|
4
|
-
const base=new URL('.',script.src);
|
|
5
|
-
if(window.THDFree)return;
|
|
6
|
-
const ready=(async()=>{
|
|
7
|
-
const {bootstrapFree}=await import(new URL('./dom-free-bootstrap.js',base).href);
|
|
8
|
-
return bootstrapFree({document,auto:script.hasAttribute('data-thd-auto')});
|
|
9
|
-
})();
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
3
|
+
const script=document.currentScript;
|
|
4
|
+
if(!script?.src)throw Error('THD: use a classic script tag, or import loadFree from @thd/free/script');
|
|
5
|
+
const base=new URL('.',script.src);
|
|
6
|
+
if(window.THDFree)return;
|
|
7
|
+
const ready=(async()=>{
|
|
8
|
+
const {bootstrapFree}=await import(new URL('./dom-free-bootstrap.js',base).href);
|
|
9
|
+
return bootstrapFree({document,auto:script.hasAttribute('data-thd-auto')});
|
|
10
|
+
})();
|
|
11
|
+
const namespace=Object.freeze({ready});
|
|
12
|
+
window.THDFree=namespace;
|
|
13
|
+
ready.catch(error=>{
|
|
14
|
+
if(window.THDFree===namespace)delete window.THDFree;
|
|
15
|
+
console.error(error);
|
|
16
|
+
window.dispatchEvent(new CustomEvent('thd:error',{detail:error}));
|
|
17
|
+
});
|
|
18
|
+
})();
|
package/src/dom-free.d.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import type {AutoRevealOptions} from './dom-auto-reveal.js';
|
|
2
|
-
import type {DOMPresentation, SurfaceOptions, TextSurface, createDOMRenderer} from './dom-attachment.js';
|
|
3
|
-
export const FREE_EFFECTS: Readonly<{textEnter:'dust-wind';textExit:'smoke';imageEnter:'drifting-snow';imageExit:'melt'}>;
|
|
4
|
-
export type FreeSurface = Pick<TextSurface,'ready'|'play'|'refresh'|'stats'|'destroy'> & {cancel():void};
|
|
5
|
-
export interface FreeAttachOptions {revealOnView?: SurfaceOptions['revealOnView'];presentation?: DOMPresentation;}
|
|
6
|
-
export function createFree(THREE: Parameters<typeof createDOMRenderer>[0],root?: Document|Element,options?: AutoRevealOptions & {auto?:boolean;zIndex?:number;documentCanvas?:boolean;experimentalDocumentCanvas?:boolean}): {
|
|
7
|
-
attachText(element:HTMLElement,options?:FreeAttachOptions):FreeSurface;
|
|
8
|
-
attachImage(element:HTMLImageElement,options?:FreeAttachOptions):FreeSurface;
|
|
9
|
-
attachSVG(element:SVGSVGElement,options?:FreeAttachOptions):FreeSurface;
|
|
10
|
-
swapImage(previous:HTMLImageElement,next:HTMLImageElement,options?:{exit?:boolean;waitForExit?:boolean;presentation?:DOMPresentation}):{finished:Promise<{status:string}>;cancel():void};
|
|
11
|
-
refresh():void;
|
|
12
|
-
stats():{disposed:boolean;automatic:unknown;renderer:unknown};
|
|
13
|
-
destroy():void;
|
|
14
|
-
};
|
|
1
|
+
import type {AutoRevealOptions} from './dom-auto-reveal.js';
|
|
2
|
+
import type {DOMPresentation, SurfaceOptions, TextSurface, createDOMRenderer} from './dom-attachment.js';
|
|
3
|
+
export const FREE_EFFECTS: Readonly<{textEnter:'dust-wind';textExit:'smoke';imageEnter:'drifting-snow';imageExit:'melt'}>;
|
|
4
|
+
export type FreeSurface = Pick<TextSurface,'ready'|'play'|'refresh'|'stats'|'destroy'> & {cancel():void;whenFinished():Promise<{status:'completed'|'cancelled'|'unsupported'}>};
|
|
5
|
+
export interface FreeAttachOptions {revealOnView?: SurfaceOptions['revealOnView'];presentation?: DOMPresentation;}
|
|
6
|
+
export function createFree(THREE: Parameters<typeof createDOMRenderer>[0],root?: Document|Element,options?: AutoRevealOptions & {auto?:boolean;zIndex?:number;documentCanvas?:boolean;experimentalDocumentCanvas?:boolean}): {
|
|
7
|
+
attachText(element:HTMLElement,options?:FreeAttachOptions):FreeSurface;
|
|
8
|
+
attachImage(element:HTMLImageElement,options?:FreeAttachOptions):FreeSurface;
|
|
9
|
+
attachSVG(element:SVGSVGElement,options?:FreeAttachOptions):FreeSurface;
|
|
10
|
+
swapImage(previous:HTMLImageElement,next:HTMLImageElement,options?:{exit?:boolean;enter?:boolean;topImage?:'previous'|'next';waitForExit?:boolean;presentation?:DOMPresentation}):{finished:Promise<{status:string}>;cancel():void};
|
|
11
|
+
refresh():void;
|
|
12
|
+
stats():{disposed:boolean;automatic:unknown;renderer:unknown};
|
|
13
|
+
destroy():void;
|
|
14
|
+
};
|
|
15
15
|
|
|
16
16
|
|
package/src/dom-free.js
CHANGED
|
@@ -1,42 +1,66 @@
|
|
|
1
|
+
/** EV-RY FX Free (MIT) — https://ev-ry.com/fx/ */
|
|
1
2
|
import {createDOMInstallation} from './dom-attachment.js';
|
|
2
|
-
import {createAutoReveal} from './dom-auto-reveal.js';
|
|
3
|
-
|
|
4
|
-
export const FREE_EFFECTS = Object.freeze({textEnter:'dust-wind', textExit:'smoke', imageEnter:'drifting-snow', imageExit:'melt'});
|
|
5
|
-
export function createFree(THREE, root = document, options = {}) {
|
|
6
|
-
const doc = root.nodeType === 9 ? root : root.ownerDocument;
|
|
7
|
-
const owner = createDOMInstallation(THREE, doc, {presentation: options.presentation ?? 'auto',zIndex:options.zIndex??1,documentCanvas:options.documentCanvas??options.experimentalDocumentCanvas??true});
|
|
8
|
-
let automatic, disposed=false;
|
|
9
|
-
function attach(kind, element, config = {}) {
|
|
10
|
-
if(disposed)throw Error('Free installation disposed');
|
|
11
|
-
for(const key of Object.keys(config))if(!['revealOnView','presentation'].includes(key))throw TypeError(`Unsupported Free option: ${key}`);
|
|
12
|
-
const effectKind=kind==='svg'?'image':kind;
|
|
13
|
-
const surface=owner[kind==='text'?'attachText':kind==='svg'?'attachSVG':'attachImage'](element,{
|
|
14
|
-
...config,resting:'native',inputEffect:FREE_EFFECTS[effectKind+'Enter'],
|
|
15
|
-
inputEffectOptions:{duration:2000,particleShape:'triangle',exitEffect:FREE_EFFECTS[effectKind+'Exit']}
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
3
|
+
import {createAutoReveal} from './dom-auto-reveal.js';
|
|
4
|
+
|
|
5
|
+
export const FREE_EFFECTS = Object.freeze({textEnter:'dust-wind', textExit:'smoke', imageEnter:'drifting-snow', imageExit:'melt'});
|
|
6
|
+
export function createFree(THREE, root = document, options = {}) {
|
|
7
|
+
const doc = root.nodeType === 9 ? root : root.ownerDocument;
|
|
8
|
+
const owner = createDOMInstallation(THREE, doc, {presentation: options.presentation ?? 'auto',zIndex:options.zIndex??1,documentCanvas:options.documentCanvas??options.experimentalDocumentCanvas??true});
|
|
9
|
+
let automatic, disposed=false;const installationWaits=new Set();
|
|
10
|
+
function attach(kind, element, config = {}) {
|
|
11
|
+
if(disposed)throw Error('Free installation disposed');
|
|
12
|
+
for(const key of Object.keys(config))if(!['revealOnView','presentation'].includes(key))throw TypeError(`Unsupported Free option: ${key}`);
|
|
13
|
+
const effectKind=kind==='svg'?'image':kind;
|
|
14
|
+
const surface=owner[kind==='text'?'attachText':kind==='svg'?'attachSVG':'attachImage'](element,{
|
|
15
|
+
...config,resting:'native',inputEffect:FREE_EFFECTS[effectKind+'Enter'],
|
|
16
|
+
inputEffectOptions:{duration:2000,particleShape:'triangle',exitEffect:FREE_EFFECTS[effectKind+'Exit']}
|
|
17
|
+
});
|
|
18
|
+
let currentWait=null,requestedPlay=false;
|
|
19
|
+
const whenFinished=()=>{
|
|
20
|
+
if(currentWait)return currentWait.promise;
|
|
21
|
+
const wait={};currentWait=wait;
|
|
22
|
+
wait.promise=new Promise(resolve=>{
|
|
23
|
+
let timeoutStarted=null,timer,done=false;
|
|
24
|
+
const finish=status=>{if(done)return;done=true;clearTimeout(timer);installationWaits.delete(wait.cancel);if(currentWait===wait)currentWait=null;resolve({status});};
|
|
25
|
+
wait.cancel=()=>finish('cancelled');installationWaits.add(wait.cancel);
|
|
26
|
+
const poll=()=>{const s=surface.stats();
|
|
27
|
+
if(s.disposed||disposed||!element.isConnected){finish('cancelled');return;}
|
|
28
|
+
if(!s.preparing&&s.mode==='native'&&s.reason&&!/initializing|not visible|not connected|native resting presentation|hidden after exit/i.test(s.reason)){finish('unsupported');return;}
|
|
29
|
+
const completed=typeof s.completed==='boolean'?s.completed:s.mode==='native'&&['native resting presentation','hidden after exit'].includes(s.reason);
|
|
30
|
+
if((!s.reveal||s.reveal.count>0||s.reveal.manual)&&completed){finish('completed');return;}
|
|
31
|
+
// An untouched intersection reveal may legitimately wait minutes.
|
|
32
|
+
// Bound actual preparation/play, not the time before first visibility.
|
|
33
|
+
if(requestedPlay||s.timeline?.started!=null||s.preparing&&!s.suspended)timeoutStarted??=Date.now();
|
|
34
|
+
if(timeoutStarted!==null&&Date.now()-timeoutStarted>30000){finish('cancelled');return;}
|
|
35
|
+
const remaining=s.timeline?.finishAt-doc.defaultView.performance.now();
|
|
36
|
+
timer=setTimeout(poll,s.suspended?Math.min(1000,Math.max(100,remaining||1000)):16);
|
|
37
|
+
};timer=setTimeout(poll,16);
|
|
38
|
+
});return wait.promise;
|
|
39
|
+
};
|
|
40
|
+
return Object.freeze({whenFinished,ready:surface.ready,play:(phase='enter')=>{
|
|
41
|
+
if(!['enter','exit'].includes(phase))throw TypeError('Invalid phase');
|
|
42
|
+
surface.play(phase);requestedPlay=true;currentWait?.cancel();
|
|
43
|
+
},cancel:()=>{surface.cancel();requestedPlay=false;currentWait?.cancel();},refresh:()=>surface.refresh(),stats:()=>surface.stats(),destroy:()=>{currentWait?.cancel();surface.destroy();}});
|
|
44
|
+
}
|
|
45
|
+
// Automatic and manual surfaces lease the same installation.
|
|
46
|
+
const autoOwner={attachText:(el,c)=>attach('text',el,{revealOnView:c.revealOnView}),attachImage:(el,c)=>attach('image',el,{revealOnView:c.revealOnView}),stats:()=>owner.stats()};
|
|
47
|
+
try { if(options.auto!==false)automatic=createAutoReveal(THREE,root,options,autoOwner); }
|
|
48
|
+
catch(error){owner.destroy();throw error;}
|
|
49
|
+
return Object.freeze({attachText:(el,c)=>attach('text',el,c),attachImage:(el,c)=>attach('image',el,c),attachSVG:(el,c)=>attach('svg',el,c),
|
|
50
|
+
swapImage(previous,next,config={}){
|
|
51
|
+
if(disposed)throw Error('Free installation disposed');
|
|
52
|
+
for(const key of Object.keys(config))if(!['exit','enter','topImage','waitForExit','presentation'].includes(key))throw TypeError(`Unsupported Free swap option: ${key}`);
|
|
53
|
+
for(const key of ['exit','enter','waitForExit'])if(config[key]!==undefined&&typeof config[key]!=='boolean')throw TypeError(`Invalid ${key}`);
|
|
54
|
+
if(config.topImage!==undefined&&!['previous','next'].includes(config.topImage))throw TypeError('Invalid topImage');
|
|
55
|
+
return owner.swapImage(previous,next,{enterEffect:config.enter!==false,topImage:config.topImage??'next',presentation:config.presentation??'global',waitForExit:config.waitForExit??false,
|
|
56
|
+
exitEffect:config.exit===false?null:FREE_EFFECTS.imageExit,inputEffect:FREE_EFFECTS.imageEnter,
|
|
57
|
+
inputEffectOptions:{duration:2000,particleShape:'triangle'}});
|
|
58
|
+
},
|
|
59
|
+
refresh(){if(disposed)throw Error('Free installation disposed');automatic?.refresh();owner.refresh();},
|
|
60
|
+
stats:()=>({disposed,automatic:automatic?.stats()??null,renderer:owner.stats()}),
|
|
61
|
+
destroy(){if(disposed)return;disposed=true;for(const cancel of [...installationWaits])cancel();try{automatic?.destroy();}finally{owner.destroy();}}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
40
64
|
|
|
41
65
|
|
|
42
66
|
|