From dc2d9c28968a8b70c77a5b6f95235cd13c93ca48 Mon Sep 17 00:00:00 2001 From: fand Date: Tue, 23 Jul 2024 10:41:20 -0700 Subject: [PATCH 01/24] refactor: add utilities for Rect --- packages/vfx-js/src/rect.ts | 55 +++++++++++++++++++++++++++++++ packages/vfx-js/src/types.ts | 11 ++----- packages/vfx-js/src/vfx-player.ts | 36 ++------------------ 3 files changed, 60 insertions(+), 42 deletions(-) create mode 100644 packages/vfx-js/src/rect.ts diff --git a/packages/vfx-js/src/rect.ts b/packages/vfx-js/src/rect.ts new file mode 100644 index 0000000..2299729 --- /dev/null +++ b/packages/vfx-js/src/rect.ts @@ -0,0 +1,55 @@ +/** + * top-left origin rect. + * Subset of DOMRect, which is returned by `HTMLElement.getBoundingClientRect()`. + * @internal + */ +export type Rect = { + left: number; + right: number; + top: number; + bottom: number; +}; + +export function rect( + top: number, + right: number, + bottom: number, + left: number, +): Rect { + return { top, right, bottom, left }; +} + +export const RECT_ZERO: Rect = rect(0, 0, 0, 0); + +/** + * Values to determine a rectangle area for margin, padding etc. + */ +export type RectOpts = + | number + | [top: number, right: number, bottom: number, left: number] + | { top?: number; right?: number; bottom?: number; left?: number }; + +export function createRect(r: RectOpts): Rect { + if (typeof r === "number") { + return { + top: r, + right: r, + bottom: r, + left: r, + }; + } + if (Array.isArray(r)) { + return { + top: r[0], + right: r[1], + bottom: r[2], + left: r[3], + }; + } + return { + top: r.top ?? 0, + right: r.right ?? 0, + bottom: r.bottom ?? 0, + left: r.left ?? 0, + }; +} diff --git a/packages/vfx-js/src/types.ts b/packages/vfx-js/src/types.ts index 1c0c510..7884fb5 100644 --- a/packages/vfx-js/src/types.ts +++ b/packages/vfx-js/src/types.ts @@ -1,5 +1,6 @@ import THREE from "three"; import { ShaderPreset } from "./constants.js"; +import { Rect, RectOpts } from "./rect.js"; /** * Options to initialize `VFX` class. @@ -113,11 +114,7 @@ export type VFXProps = { * If you pass an object like ``, * REACT-VFX will add paddings only to the given direction (only to the `top` in this example). */ - overflow?: - | true - | number - | [top: number, right: number, bottom: number, left: number] - | { top?: number; right?: number; bottom?: number; left?: number }; + overflow?: true | RectOpts; /** * Texture wrapping mode. (Default: `"repeat"`) @@ -192,6 +189,4 @@ export type VFXElement = { /** * @internal */ -export type VFXElementOverflow = - | "fullscreen" - | { top: number; right: number; bottom: number; left: number }; +export type VFXElementOverflow = "fullscreen" | Rect; diff --git a/packages/vfx-js/src/vfx-player.ts b/packages/vfx-js/src/vfx-player.ts index 6629de0..0b064be 100644 --- a/packages/vfx-js/src/vfx-player.ts +++ b/packages/vfx-js/src/vfx-player.ts @@ -10,18 +10,7 @@ import { VFXElementOverflow, VFXWrap, } from "./types"; - -/** - * top-left origin rect. - * Subset of DOMRect, which is returned by `HTMLElement.getBoundingClientRect()`. - * @internal - */ -type Rect = { - left: number; - right: number; - top: number; - bottom: number; -}; +import { createRect, Rect } from "./rect.js"; const gifFor = new Map(); @@ -477,28 +466,7 @@ export function sanitizeOverflow( if (overflow === undefined) { return { top: 0, right: 0, bottom: 0, left: 0 }; } - if (typeof overflow === "number") { - return { - top: overflow, - right: overflow, - bottom: overflow, - left: overflow, - }; - } - if (Array.isArray(overflow)) { - return { - top: overflow[0], - right: overflow[1], - bottom: overflow[2], - left: overflow[3], - }; - } - return { - top: overflow.top ?? 0, - right: overflow.right ?? 0, - bottom: overflow.bottom ?? 0, - left: overflow.left ?? 0, - }; + return createRect(overflow); } function parseWrapSingle(wrapOpt: VFXWrap): THREE.Wrapping { From 88034ef9d6a10049b3ea8c2826e67cd79fb2f9f1 Mon Sep 17 00:00:00 2001 From: fand Date: Tue, 23 Jul 2024 10:57:46 -0700 Subject: [PATCH 02/24] chore: add growRect & shrinkRect --- packages/vfx-js/src/rect.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/vfx-js/src/rect.ts b/packages/vfx-js/src/rect.ts index 2299729..8657410 100644 --- a/packages/vfx-js/src/rect.ts +++ b/packages/vfx-js/src/rect.ts @@ -53,3 +53,21 @@ export function createRect(r: RectOpts): Rect { left: r.left ?? 0, }; } + +export function growRect(a: Rect, b: Rect): Rect { + return { + top: a.top - b.top, + right: a.right + b.right, + bottom: a.bottom + b.bottom, + left: a.left - b.left, + }; +} + +export function shrinkRect(a: Rect, b: Rect): Rect { + return { + top: a.top + b.top, + right: a.right - b.right, + bottom: a.bottom - b.bottom, + left: a.left + b.left, + }; +} From 03bda1c687bf51ae2d8410304464a998863fd67a Mon Sep 17 00:00:00 2001 From: fand Date: Tue, 23 Jul 2024 10:59:02 -0700 Subject: [PATCH 03/24] test: add test for Rect --- packages/vfx-js/src/rect.test.ts | 99 ++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 packages/vfx-js/src/rect.test.ts diff --git a/packages/vfx-js/src/rect.test.ts b/packages/vfx-js/src/rect.test.ts new file mode 100644 index 0000000..da0203a --- /dev/null +++ b/packages/vfx-js/src/rect.test.ts @@ -0,0 +1,99 @@ +import { expect, describe, test } from "vitest"; +import { createRect, growRect, shrinkRect } from "./rect"; + +describe("createRect", () => { + test("single number", () => { + expect(createRect(1)).toStrictEqual({ + top: 1, + right: 1, + bottom: 1, + left: 1, + }); + }); + + test("array", () => { + expect(createRect([1, 2, 3, 4])).toStrictEqual({ + top: 1, + right: 2, + bottom: 3, + left: 4, + }); + }); + + test("object", () => { + expect( + createRect({ top: 1, right: 2, bottom: 3, left: 4 }), + ).toStrictEqual({ + top: 1, + right: 2, + bottom: 3, + left: 4, + }); + }); +}); + +describe("growRect", () => { + test("positive values", () => { + const a = { + top: 100, + right: 200, + bottom: 200, + left: 100, + }; + const b = createRect(1); + expect(growRect(a, b)).toStrictEqual({ + top: 99, + right: 201, + bottom: 201, + left: 99, + }); + }); + test("negative values", () => { + const a = { + top: 100, + right: 200, + bottom: 200, + left: 100, + }; + const b = createRect(-1); + expect(growRect(a, b)).toStrictEqual({ + top: 101, + right: 199, + bottom: 199, + left: 101, + }); + }); +}); + +describe("shrinkRect", () => { + test("positive values", () => { + const a = { + top: 100, + right: 200, + bottom: 200, + left: 100, + }; + const b = createRect(1); + expect(shrinkRect(a, b)).toStrictEqual({ + top: 101, + right: 199, + bottom: 199, + left: 101, + }); + }); + test("negative values", () => { + const a = { + top: 100, + right: 200, + bottom: 200, + left: 100, + }; + const b = createRect(-1); + expect(shrinkRect(a, b)).toStrictEqual({ + top: 99, + right: 201, + bottom: 201, + left: 99, + }); + }); +}); From c75fcd33a3bb9bc40bf91db2de33613899849062 Mon Sep 17 00:00:00 2001 From: fand Date: Tue, 23 Jul 2024 11:19:18 -0700 Subject: [PATCH 04/24] feat: add getIntersection --- packages/vfx-js/src/rect.test.ts | 32 +++++++++++++++++++++++++++++++- packages/vfx-js/src/rect.ts | 20 ++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/packages/vfx-js/src/rect.test.ts b/packages/vfx-js/src/rect.test.ts index da0203a..8cae15d 100644 --- a/packages/vfx-js/src/rect.test.ts +++ b/packages/vfx-js/src/rect.test.ts @@ -1,5 +1,5 @@ import { expect, describe, test } from "vitest"; -import { createRect, growRect, shrinkRect } from "./rect"; +import { createRect, getIntersection, growRect, shrinkRect } from "./rect"; describe("createRect", () => { test("single number", () => { @@ -97,3 +97,33 @@ describe("shrinkRect", () => { }); }); }); + +describe("getIntersection", () => { + test("no intersection", () => { + const a = createRect([0, 1, 1, 0]); + const b = createRect([0, 2, 1, 1]); + expect(getIntersection(a, b)).toBe(0); + }); + test("full intersection", () => { + expect( + getIntersection(createRect([0, 1, 1, 0]), createRect([0, 1, 1, 0])), + ).toBe(1); + expect( + getIntersection( + createRect([0, 10, 10, 1]), + createRect([1, 2, 2, 1]), + ), + ).toBe(1); + }); + test("partial intersection", () => { + expect( + getIntersection(createRect([0, 2, 1, 0]), createRect([0, 1, 1, 0])), + ).toBe(1); // target is fully covered by container + expect( + getIntersection(createRect([0, 1, 1, 0]), createRect([0, 2, 1, 0])), + ).toBe(0.5); // 50% of target is covered by container + expect( + getIntersection(createRect([1, 2, 2, 1]), createRect([0, 2, 2, 0])), + ).toBe(0.25); // 25% + }); +}); diff --git a/packages/vfx-js/src/rect.ts b/packages/vfx-js/src/rect.ts index 8657410..b50fa60 100644 --- a/packages/vfx-js/src/rect.ts +++ b/packages/vfx-js/src/rect.ts @@ -71,3 +71,23 @@ export function shrinkRect(a: Rect, b: Rect): Rect { left: a.left + b.left, }; } + +function clamp(x: number, xmin: number, xmax: number): number { + return Math.min(Math.max(x, xmin), xmax); +} + +/** + * Calculate the ratio of the intersection between two Rect objects. + * It returns a number between 0 and 1. + */ +export function getIntersection(container: Rect, target: Rect): number { + const targetL = clamp(target.left, container.left, container.right); + const targetR = clamp(target.right, container.left, container.right); + const w = (targetR - targetL) / (target.right - target.left); + + const targetT = clamp(target.top, container.top, container.bottom); + const targetB = clamp(target.bottom, container.top, container.bottom); + const h = (targetB - targetT) / (target.bottom - target.top); + + return w * h; +} From 44b3fb32779ebaaee336b738d6627b84179df7b4 Mon Sep 17 00:00:00 2001 From: fand Date: Tue, 23 Jul 2024 11:28:06 -0700 Subject: [PATCH 05/24] refactor: split e.overflow and e.isFullScreen --- packages/vfx-js/src/types.ts | 8 +- packages/vfx-js/src/vfx-player.test.ts | 123 +++++++++++++++---------- packages/vfx-js/src/vfx-player.ts | 33 +++---- 3 files changed, 90 insertions(+), 74 deletions(-) diff --git a/packages/vfx-js/src/types.ts b/packages/vfx-js/src/types.ts index 7884fb5..2750e40 100644 --- a/packages/vfx-js/src/types.ts +++ b/packages/vfx-js/src/types.ts @@ -181,12 +181,8 @@ export type VFXElement = { leaveTime: number; release: number; isGif: boolean; - overflow: VFXElementOverflow; + isFullScreen: boolean; + overflow: Rect; originalOpacity: number; zIndex: number; }; - -/** - * @internal - */ -export type VFXElementOverflow = "fullscreen" | Rect; diff --git a/packages/vfx-js/src/vfx-player.test.ts b/packages/vfx-js/src/vfx-player.test.ts index a892e48..e9f5a44 100644 --- a/packages/vfx-js/src/vfx-player.test.ts +++ b/packages/vfx-js/src/vfx-player.test.ts @@ -1,71 +1,96 @@ import { expect, describe, test } from "vitest"; import { isRectInViewport, sanitizeOverflow } from "./vfx-player"; +import { RECT_ZERO } from "./rect"; describe("sanitizeOverflow", () => { test('true => "fullscreen"', () => { - expect(sanitizeOverflow(true)).toBe("fullscreen"); + expect(sanitizeOverflow(true)).toStrictEqual([true, RECT_ZERO]); }); test("undefined => 0", () => { - expect(sanitizeOverflow(undefined)).toStrictEqual({ - top: 0, - right: 0, - bottom: 0, - left: 0, - }); + expect(sanitizeOverflow(undefined)).toStrictEqual([ + false, + { + top: 0, + right: 0, + bottom: 0, + left: 0, + }, + ]); }); test("number", () => { - expect(sanitizeOverflow(100)).toStrictEqual({ - top: 100, - right: 100, - bottom: 100, - left: 100, - }); + expect(sanitizeOverflow(100)).toStrictEqual([ + false, + { + top: 100, + right: 100, + bottom: 100, + left: 100, + }, + ]); }); test("number array", () => { - expect(sanitizeOverflow([0, 100, 200, 300])).toStrictEqual({ - top: 0, - right: 100, - bottom: 200, - left: 300, - }); + expect(sanitizeOverflow([0, 100, 200, 300])).toStrictEqual([ + false, + { + top: 0, + right: 100, + bottom: 200, + left: 300, + }, + ]); }); test("object", () => { - expect(sanitizeOverflow({})).toStrictEqual({ - top: 0, - right: 0, - bottom: 0, - left: 0, - }); - expect(sanitizeOverflow({ top: 100 })).toStrictEqual({ - top: 100, - right: 0, - bottom: 0, - left: 0, - }); - expect(sanitizeOverflow({ left: 100 })).toStrictEqual({ - top: 0, - right: 0, - bottom: 0, - left: 100, - }); - expect(sanitizeOverflow({ top: 100, left: 200 })).toStrictEqual({ - top: 100, - right: 0, - bottom: 0, - left: 200, - }); + expect(sanitizeOverflow({})).toStrictEqual([ + false, + { + top: 0, + right: 0, + bottom: 0, + left: 0, + }, + ]); + expect(sanitizeOverflow({ top: 100 })).toStrictEqual([ + false, + { + top: 100, + right: 0, + bottom: 0, + left: 0, + }, + ]); + expect(sanitizeOverflow({ left: 100 })).toStrictEqual([ + false, + { + top: 0, + right: 0, + bottom: 0, + left: 100, + }, + ]); + expect(sanitizeOverflow({ top: 100, left: 200 })).toStrictEqual([ + false, + { + top: 100, + right: 0, + bottom: 0, + left: 200, + }, + ]); expect( sanitizeOverflow({ top: 100, right: 200, bottom: 300, left: 400 }), - ).toStrictEqual({ - top: 100, - right: 200, - bottom: 300, - left: 400, - }); + ).toStrictEqual([ + false, + { + top: 100, + right: 200, + bottom: 300, + left: 400, + }, + ]); }); }); diff --git a/packages/vfx-js/src/vfx-player.ts b/packages/vfx-js/src/vfx-player.ts index 0b064be..0198eb0 100644 --- a/packages/vfx-js/src/vfx-player.ts +++ b/packages/vfx-js/src/vfx-player.ts @@ -7,10 +7,9 @@ import { VFXElement, VFXElementType, VFXUniformValue, - VFXElementOverflow, VFXWrap, } from "./types"; -import { createRect, Rect } from "./rect.js"; +import { createRect, Rect, RECT_ZERO } from "./rect.js"; const gifFor = new Map(); @@ -165,8 +164,9 @@ export class VFXPlayer { const shader = this.#getShader(opts.shader || "uvGradient"); const rect = element.getBoundingClientRect(); - const overflow = sanitizeOverflow(opts.overflow); - const isInViewport = isRectInViewport(this.#viewport, rect, overflow); + const [isFullScreen, overflow] = sanitizeOverflow(opts.overflow); + const isInViewport = + isFullScreen || isRectInViewport(this.#viewport, rect, overflow); const originalOpacity = element.style.opacity === "" @@ -274,6 +274,7 @@ export class VFXPlayer { leaveTime: -Infinity, release: opts.release ?? 0, isGif, + isFullScreen, overflow, originalOpacity, zIndex: opts.zIndex ?? 0, @@ -349,11 +350,9 @@ export class VFXPlayer { const rect = e.element.getBoundingClientRect(); // Check intersection - const isInViewport = isRectInViewport( - this.#viewport, - rect, - e.overflow, - ); + const isInViewport = + e.isFullScreen || + isRectInViewport(this.#viewport, rect, e.overflow); // entering if (isInViewport && !e.isInViewport) { @@ -398,7 +397,7 @@ export class VFXPlayer { } // Set viewport - if (e.overflow === "fullscreen") { + if (e.isFullScreen) { this.#renderer.setViewport( 0, 0, @@ -443,12 +442,8 @@ export class VFXPlayer { export function isRectInViewport( viewport: Rect, rect: Rect, - overflow: VFXElementOverflow, + overflow: Rect, ): boolean { - if (overflow === "fullscreen") { - return true; - } - return ( rect.left - overflow.left <= viewport.right && rect.right + overflow.right >= viewport.left && @@ -459,14 +454,14 @@ export function isRectInViewport( export function sanitizeOverflow( overflow: VFXProps["overflow"], -): VFXElementOverflow { +): [isFullScreen: boolean, Rect] { if (overflow === true) { - return "fullscreen"; + return [true, RECT_ZERO]; } if (overflow === undefined) { - return { top: 0, right: 0, bottom: 0, left: 0 }; + return [false, RECT_ZERO]; } - return createRect(overflow); + return [false, createRect(overflow)]; } function parseWrapSingle(wrapOpt: VFXWrap): THREE.Wrapping { From e8d6432604ab95b6cd6b1145491c48dcddc27a79 Mon Sep 17 00:00:00 2001 From: fand Date: Tue, 23 Jul 2024 11:37:46 -0700 Subject: [PATCH 06/24] refactor: use Rect operation in isRectInViewport --- packages/vfx-js/src/vfx-player.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/vfx-js/src/vfx-player.ts b/packages/vfx-js/src/vfx-player.ts index 0198eb0..7363939 100644 --- a/packages/vfx-js/src/vfx-player.ts +++ b/packages/vfx-js/src/vfx-player.ts @@ -9,7 +9,7 @@ import { VFXUniformValue, VFXWrap, } from "./types"; -import { createRect, Rect, RECT_ZERO } from "./rect.js"; +import { createRect, growRect, Rect, RECT_ZERO } from "./rect.js"; const gifFor = new Map(); @@ -444,11 +444,12 @@ export function isRectInViewport( rect: Rect, overflow: Rect, ): boolean { + const rect2 = growRect(rect, overflow); return ( - rect.left - overflow.left <= viewport.right && - rect.right + overflow.right >= viewport.left && - rect.top - overflow.top <= viewport.bottom && - rect.bottom + overflow.bottom >= viewport.top + rect2.left <= viewport.right && + rect2.right >= viewport.left && + rect2.top <= viewport.bottom && + rect2.bottom >= viewport.top ); } From c3059737d0652457b0ce27ed89b17e27d4fa199b Mon Sep 17 00:00:00 2001 From: fand Date: Tue, 23 Jul 2024 11:43:22 -0700 Subject: [PATCH 07/24] feat: add VFXProps.intersection --- packages/vfx-js/src/types.ts | 9 +++++++ packages/vfx-js/src/vfx-player.ts | 43 ++++++++++++++++++++++++------- 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/packages/vfx-js/src/types.ts b/packages/vfx-js/src/types.ts index 2750e40..4df80a6 100644 --- a/packages/vfx-js/src/types.ts +++ b/packages/vfx-js/src/types.ts @@ -100,6 +100,10 @@ export type VFXProps = { */ overlay?: true | number; + intersection?: { + threshold?: number; + }; + /** * Allow shader outputs to oveflow the original element area. (Default: `0`) * @@ -183,6 +187,11 @@ export type VFXElement = { isGif: boolean; isFullScreen: boolean; overflow: Rect; + intersection: VFXElementIntersection; originalOpacity: number; zIndex: number; }; + +export type VFXElementIntersection = { + threshold: number; +}; diff --git a/packages/vfx-js/src/vfx-player.ts b/packages/vfx-js/src/vfx-player.ts index 7363939..078b74b 100644 --- a/packages/vfx-js/src/vfx-player.ts +++ b/packages/vfx-js/src/vfx-player.ts @@ -8,8 +8,15 @@ import { VFXElementType, VFXUniformValue, VFXWrap, + VFXElementIntersection, } from "./types"; -import { createRect, growRect, Rect, RECT_ZERO } from "./rect.js"; +import { + createRect, + getIntersection, + growRect, + Rect, + RECT_ZERO, +} from "./rect.js"; const gifFor = new Map(); @@ -165,8 +172,15 @@ export class VFXPlayer { const rect = element.getBoundingClientRect(); const [isFullScreen, overflow] = sanitizeOverflow(opts.overflow); + const intersection = sanitizeIntersection(opts.intersection); const isInViewport = - isFullScreen || isRectInViewport(this.#viewport, rect, overflow); + isFullScreen || + isRectInViewport( + this.#viewport, + rect, + overflow, + intersection.threshold, + ); const originalOpacity = element.style.opacity === "" @@ -276,6 +290,7 @@ export class VFXPlayer { isGif, isFullScreen, overflow, + intersection, originalOpacity, zIndex: opts.zIndex ?? 0, }; @@ -352,7 +367,12 @@ export class VFXPlayer { // Check intersection const isInViewport = e.isFullScreen || - isRectInViewport(this.#viewport, rect, e.overflow); + isRectInViewport( + this.#viewport, + rect, + e.overflow, + e.intersection.threshold, + ); // entering if (isInViewport && !e.isInViewport) { @@ -443,14 +463,10 @@ export function isRectInViewport( viewport: Rect, rect: Rect, overflow: Rect, + threshold: number, ): boolean { const rect2 = growRect(rect, overflow); - return ( - rect2.left <= viewport.right && - rect2.right >= viewport.left && - rect2.top <= viewport.bottom && - rect2.bottom >= viewport.top - ); + return getIntersection(viewport, rect2) > threshold; } export function sanitizeOverflow( @@ -465,6 +481,15 @@ export function sanitizeOverflow( return [false, createRect(overflow)]; } +export function sanitizeIntersection( + intersectionOpts: VFXProps["intersection"], +): VFXElementIntersection { + const threshold = intersectionOpts?.threshold ?? 0; + return { + threshold, + }; +} + function parseWrapSingle(wrapOpt: VFXWrap): THREE.Wrapping { if (wrapOpt === "repeat") { return THREE.RepeatWrapping; From 91d0d2e7942a2168baa05deca56ed6c59be8b449 Mon Sep 17 00:00:00 2001 From: fand Date: Tue, 23 Jul 2024 11:44:22 -0700 Subject: [PATCH 08/24] fix: pass threshold in test --- packages/vfx-js/src/vfx-player.test.ts | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/vfx-js/src/vfx-player.test.ts b/packages/vfx-js/src/vfx-player.test.ts index e9f5a44..f853e6e 100644 --- a/packages/vfx-js/src/vfx-player.test.ts +++ b/packages/vfx-js/src/vfx-player.test.ts @@ -115,7 +115,7 @@ describe("isRectInViewport", () => { test("no overflow", () => { expect( - isRectInViewport(rect(0, 0, 1, 1), rect(0, 0, 1, 1), pad(0)), + isRectInViewport(rect(0, 0, 1, 1), rect(0, 0, 1, 1), pad(0), 0), ).toBe(true); // adjacent rects @@ -124,6 +124,7 @@ describe("isRectInViewport", () => { rect(0, 0, 1, 1), rect(-1, 0, 1, 1), // left pad(0), + 0, ), ).toBe(true); expect( @@ -131,6 +132,7 @@ describe("isRectInViewport", () => { rect(0, 0, 1, 1), rect(1, 0, 1, 1), // right pad(0), + 0, ), ).toBe(true); expect( @@ -138,6 +140,7 @@ describe("isRectInViewport", () => { rect(0, 0, 1, 1), rect(0, -1, 1, 1), // top pad(0), + 0, ), ).toBe(true); expect( @@ -145,6 +148,7 @@ describe("isRectInViewport", () => { rect(0, 0, 1, 1), rect(0, 1, 1, 1), // bottom pad(0), + 0, ), ).toBe(true); @@ -154,6 +158,7 @@ describe("isRectInViewport", () => { rect(0, 0, 1, 1), rect(-2, 0, 1, 1), // 1px left pad(0), + 0, ), ).toBe(false); expect( @@ -161,6 +166,7 @@ describe("isRectInViewport", () => { rect(0, 0, 1, 1), rect(2, 0, 1, 1), // 1px right pad(0), + 0, ), ).toBe(false); expect( @@ -168,6 +174,7 @@ describe("isRectInViewport", () => { rect(0, 0, 1, 1), rect(0, -2, 1, 1), // 1px top pad(0), + 0, ), ).toBe(false); expect( @@ -175,13 +182,14 @@ describe("isRectInViewport", () => { rect(0, 0, 1, 1), rect(0, 2, 1, 1), // 1px bottom pad(0), + 0, ), ).toBe(false); }); test("with overflow", () => { expect( - isRectInViewport(rect(0, 0, 1, 1), rect(0, 0, 1, 1), pad(1)), + isRectInViewport(rect(0, 0, 1, 1), rect(0, 0, 1, 1), pad(1), 0), ).toBe(true); // adjacent rects @@ -190,6 +198,7 @@ describe("isRectInViewport", () => { rect(0, 0, 1, 1), rect(-1, 0, 1, 1), // left pad(1), + 0, ), ).toBe(true); expect( @@ -197,6 +206,7 @@ describe("isRectInViewport", () => { rect(0, 0, 1, 1), rect(1, 0, 1, 1), // right pad(1), + 0, ), ).toBe(true); expect( @@ -204,6 +214,7 @@ describe("isRectInViewport", () => { rect(0, 0, 1, 1), rect(0, -1, 1, 1), // top pad(1), + 0, ), ).toBe(true); expect( @@ -211,6 +222,7 @@ describe("isRectInViewport", () => { rect(0, 0, 1, 1), rect(0, 1, 1, 1), // bottom pad(1), + 0, ), ).toBe(true); @@ -220,6 +232,7 @@ describe("isRectInViewport", () => { rect(0, 0, 1, 1), rect(-2, 0, 1, 1), // 1px left pad(1), + 0, ), ).toBe(true); expect( @@ -227,6 +240,7 @@ describe("isRectInViewport", () => { rect(0, 0, 1, 1), rect(2, 0, 1, 1), // 1px right pad(1), + 0, ), ).toBe(true); expect( @@ -234,6 +248,7 @@ describe("isRectInViewport", () => { rect(0, 0, 1, 1), rect(0, -2, 1, 1), // 1px top pad(1), + 0, ), ).toBe(true); expect( @@ -241,6 +256,7 @@ describe("isRectInViewport", () => { rect(0, 0, 1, 1), rect(0, 2, 1, 1), // 1px bottom pad(1), + 0, ), ).toBe(true); @@ -250,6 +266,7 @@ describe("isRectInViewport", () => { rect(0, 0, 1, 1), rect(-3, 0, 1, 1), // 2px left pad(1), + 0, ), ).toBe(false); expect( @@ -257,6 +274,7 @@ describe("isRectInViewport", () => { rect(0, 0, 1, 1), rect(3, 0, 1, 1), // 2px right pad(1), + 0, ), ).toBe(false); expect( @@ -264,6 +282,7 @@ describe("isRectInViewport", () => { rect(0, 0, 1, 1), rect(0, -3, 1, 1), // 2px top pad(1), + 0, ), ).toBe(false); expect( @@ -271,6 +290,7 @@ describe("isRectInViewport", () => { rect(0, 0, 1, 1), rect(0, 3, 1, 1), // 2px bottom pad(1), + 0, ), ).toBe(false); }); From 05f53cf9f303160cb9d356f2ff6638deb43dc2b1 Mon Sep 17 00:00:00 2001 From: fand Date: Tue, 23 Jul 2024 11:45:37 -0700 Subject: [PATCH 09/24] fix: change the boundary condition for adjacent rects in isRectInViewport --- packages/vfx-js/src/vfx-player.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/vfx-js/src/vfx-player.test.ts b/packages/vfx-js/src/vfx-player.test.ts index f853e6e..657b248 100644 --- a/packages/vfx-js/src/vfx-player.test.ts +++ b/packages/vfx-js/src/vfx-player.test.ts @@ -126,7 +126,7 @@ describe("isRectInViewport", () => { pad(0), 0, ), - ).toBe(true); + ).toBe(false); expect( isRectInViewport( rect(0, 0, 1, 1), @@ -134,7 +134,7 @@ describe("isRectInViewport", () => { pad(0), 0, ), - ).toBe(true); + ).toBe(false); expect( isRectInViewport( rect(0, 0, 1, 1), @@ -142,7 +142,7 @@ describe("isRectInViewport", () => { pad(0), 0, ), - ).toBe(true); + ).toBe(false); expect( isRectInViewport( rect(0, 0, 1, 1), @@ -150,7 +150,7 @@ describe("isRectInViewport", () => { pad(0), 0, ), - ).toBe(true); + ).toBe(false); // distant rects expect( @@ -234,7 +234,7 @@ describe("isRectInViewport", () => { pad(1), 0, ), - ).toBe(true); + ).toBe(false); expect( isRectInViewport( rect(0, 0, 1, 1), @@ -242,7 +242,7 @@ describe("isRectInViewport", () => { pad(1), 0, ), - ).toBe(true); + ).toBe(false); expect( isRectInViewport( rect(0, 0, 1, 1), @@ -250,7 +250,7 @@ describe("isRectInViewport", () => { pad(1), 0, ), - ).toBe(true); + ).toBe(false); expect( isRectInViewport( rect(0, 0, 1, 1), @@ -258,7 +258,7 @@ describe("isRectInViewport", () => { pad(1), 0, ), - ).toBe(true); + ).toBe(false); // more distant rects expect( From 95698ba13a63bfa922b4363db4ec6f8c49fc7c0e Mon Sep 17 00:00:00 2001 From: fand Date: Thu, 25 Jul 2024 11:15:33 -0700 Subject: [PATCH 10/24] Revert "fix: change the boundary condition for adjacent rects in isRectInViewport" This reverts commit 05f53cf9f303160cb9d356f2ff6638deb43dc2b1. --- packages/vfx-js/src/vfx-player.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/vfx-js/src/vfx-player.test.ts b/packages/vfx-js/src/vfx-player.test.ts index 657b248..f853e6e 100644 --- a/packages/vfx-js/src/vfx-player.test.ts +++ b/packages/vfx-js/src/vfx-player.test.ts @@ -126,7 +126,7 @@ describe("isRectInViewport", () => { pad(0), 0, ), - ).toBe(false); + ).toBe(true); expect( isRectInViewport( rect(0, 0, 1, 1), @@ -134,7 +134,7 @@ describe("isRectInViewport", () => { pad(0), 0, ), - ).toBe(false); + ).toBe(true); expect( isRectInViewport( rect(0, 0, 1, 1), @@ -142,7 +142,7 @@ describe("isRectInViewport", () => { pad(0), 0, ), - ).toBe(false); + ).toBe(true); expect( isRectInViewport( rect(0, 0, 1, 1), @@ -150,7 +150,7 @@ describe("isRectInViewport", () => { pad(0), 0, ), - ).toBe(false); + ).toBe(true); // distant rects expect( @@ -234,7 +234,7 @@ describe("isRectInViewport", () => { pad(1), 0, ), - ).toBe(false); + ).toBe(true); expect( isRectInViewport( rect(0, 0, 1, 1), @@ -242,7 +242,7 @@ describe("isRectInViewport", () => { pad(1), 0, ), - ).toBe(false); + ).toBe(true); expect( isRectInViewport( rect(0, 0, 1, 1), @@ -250,7 +250,7 @@ describe("isRectInViewport", () => { pad(1), 0, ), - ).toBe(false); + ).toBe(true); expect( isRectInViewport( rect(0, 0, 1, 1), @@ -258,7 +258,7 @@ describe("isRectInViewport", () => { pad(1), 0, ), - ).toBe(false); + ).toBe(true); // more distant rects expect( From 0c2490404e20e68d953613f433bb18d137ac16d1 Mon Sep 17 00:00:00 2001 From: fand Date: Thu, 25 Jul 2024 11:32:50 -0700 Subject: [PATCH 11/24] fix: consider adjacent rects to be intersecting --- packages/vfx-js/src/vfx-player.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/vfx-js/src/vfx-player.ts b/packages/vfx-js/src/vfx-player.ts index 078b74b..f206c9e 100644 --- a/packages/vfx-js/src/vfx-player.ts +++ b/packages/vfx-js/src/vfx-player.ts @@ -466,7 +466,17 @@ export function isRectInViewport( threshold: number, ): boolean { const rect2 = growRect(rect, overflow); - return getIntersection(viewport, rect2) > threshold; + if (threshold === 0) { + // if threshold == 0, consider adjacent rects to be intersecting. + return ( + rect2.left <= viewport.right && + rect2.right >= viewport.left && + rect2.top <= viewport.bottom && + rect2.bottom >= viewport.top + ); + } else { + return getIntersection(viewport, rect2) > threshold; + } } export function sanitizeOverflow( From 598c8582d2362e7a2de16ab914ac31c86ca32a6f Mon Sep 17 00:00:00 2001 From: fand Date: Thu, 25 Jul 2024 13:36:26 -0700 Subject: [PATCH 12/24] feat: add rootMargin --- packages/vfx-js/src/types.ts | 2 ++ packages/vfx-js/src/vfx-player.ts | 17 ++++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/vfx-js/src/types.ts b/packages/vfx-js/src/types.ts index 4df80a6..67fd286 100644 --- a/packages/vfx-js/src/types.ts +++ b/packages/vfx-js/src/types.ts @@ -102,6 +102,7 @@ export type VFXProps = { intersection?: { threshold?: number; + rootMargin?: RectOpts; }; /** @@ -194,4 +195,5 @@ export type VFXElement = { export type VFXElementIntersection = { threshold: number; + rootMargin: Rect; }; diff --git a/packages/vfx-js/src/vfx-player.ts b/packages/vfx-js/src/vfx-player.ts index f206c9e..8d02ca5 100644 --- a/packages/vfx-js/src/vfx-player.ts +++ b/packages/vfx-js/src/vfx-player.ts @@ -173,14 +173,11 @@ export class VFXPlayer { const rect = element.getBoundingClientRect(); const [isFullScreen, overflow] = sanitizeOverflow(opts.overflow); const intersection = sanitizeIntersection(opts.intersection); + const viewport = growRect(this.#viewport, intersection.rootMargin); + const isInViewport = isFullScreen || - isRectInViewport( - this.#viewport, - rect, - overflow, - intersection.threshold, - ); + isRectInViewport(viewport, rect, overflow, intersection.threshold); const originalOpacity = element.style.opacity === "" @@ -365,10 +362,14 @@ export class VFXPlayer { const rect = e.element.getBoundingClientRect(); // Check intersection + const viewport = growRect( + this.#viewport, + e.intersection.rootMargin, + ); const isInViewport = e.isFullScreen || isRectInViewport( - this.#viewport, + viewport, rect, e.overflow, e.intersection.threshold, @@ -495,8 +496,10 @@ export function sanitizeIntersection( intersectionOpts: VFXProps["intersection"], ): VFXElementIntersection { const threshold = intersectionOpts?.threshold ?? 0; + const rootMargin = createRect(intersectionOpts?.rootMargin ?? 0); return { threshold, + rootMargin, }; } From 38fbfab3b07f9290fda15b1a4a2768ad8a99989b Mon Sep 17 00:00:00 2001 From: fand Date: Thu, 25 Jul 2024 13:47:48 -0700 Subject: [PATCH 13/24] fix: consider elements to be inside viewport when the intersection == threshold --- packages/vfx-js/src/vfx-player.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vfx-js/src/vfx-player.ts b/packages/vfx-js/src/vfx-player.ts index 8d02ca5..eaebf21 100644 --- a/packages/vfx-js/src/vfx-player.ts +++ b/packages/vfx-js/src/vfx-player.ts @@ -476,7 +476,7 @@ export function isRectInViewport( rect2.bottom >= viewport.top ); } else { - return getIntersection(viewport, rect2) > threshold; + return getIntersection(viewport, rect2) >= threshold; } } From f9ba7cc3fdfaa9ee5852e1da0f0de4028f1066df Mon Sep 17 00:00:00 2001 From: fand Date: Thu, 25 Jul 2024 23:56:40 -0700 Subject: [PATCH 14/24] feat: split viewport intersection and transition area intersection calculation --- packages/vfx-js/src/types.ts | 1 + packages/vfx-js/src/vfx-player.ts | 51 +++++++++++++++++++------------ 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/packages/vfx-js/src/types.ts b/packages/vfx-js/src/types.ts index 67fd286..e22b5c3 100644 --- a/packages/vfx-js/src/types.ts +++ b/packages/vfx-js/src/types.ts @@ -176,6 +176,7 @@ export type VFXElement = { type: VFXElementType; element: HTMLElement; isInViewport: boolean; + isInTransitionArea: boolean; width: number; height: number; scene: THREE.Scene; diff --git a/packages/vfx-js/src/vfx-player.ts b/packages/vfx-js/src/vfx-player.ts index eaebf21..5ab0ef1 100644 --- a/packages/vfx-js/src/vfx-player.ts +++ b/packages/vfx-js/src/vfx-player.ts @@ -173,11 +173,21 @@ export class VFXPlayer { const rect = element.getBoundingClientRect(); const [isFullScreen, overflow] = sanitizeOverflow(opts.overflow); const intersection = sanitizeIntersection(opts.intersection); - const viewport = growRect(this.#viewport, intersection.rootMargin); - const isInViewport = + isFullScreen || isRectInViewport(this.#viewport, rect, overflow, 0); + + const transitionArea = growRect( + this.#viewport, + intersection.rootMargin, + ); + const isInTransitionArea = isFullScreen || - isRectInViewport(viewport, rect, overflow, intersection.threshold); + isRectInViewport( + transitionArea, + rect, + overflow, + intersection.threshold, + ); const originalOpacity = element.style.opacity === "" @@ -275,15 +285,16 @@ export class VFXPlayer { type, element, isInViewport, + isInTransitionArea, width: rect.width, height: rect.height, scene, uniforms, uniformGenerators, startTime: now, - enterTime: isInViewport ? now : -1, - leaveTime: -Infinity, - release: opts.release ?? 0, + enterTime: isInViewport ? now : -Infinity, + leaveTime: isInViewport ? Infinity : -Infinity, + release: opts.release ?? Infinity, isGif, isFullScreen, overflow, @@ -362,42 +373,44 @@ export class VFXPlayer { const rect = e.element.getBoundingClientRect(); // Check intersection - const viewport = growRect( + const isInViewport = + e.isFullScreen || + isRectInViewport(this.#viewport, rect, e.overflow, 0); + + const transitionArea = growRect( this.#viewport, e.intersection.rootMargin, ); - const isInViewport = + const isInTransitionArea = e.isFullScreen || isRectInViewport( - viewport, + transitionArea, rect, e.overflow, e.intersection.threshold, ); - // entering - if (isInViewport && !e.isInViewport) { + // Update transition timing + if (!e.isInTransitionArea && isInTransitionArea /* out -> in */) { e.enterTime = now; e.leaveTime = Infinity; } - - // leaving - if (!isInViewport && e.isInViewport) { + if (e.isInTransitionArea && !isInTransitionArea /* in -> out */) { e.leaveTime = now; } + e.isInViewport = isInViewport; + e.isInTransitionArea = isInTransitionArea; // Quit if the element has left and the transition has ended - if (!isInViewport && now - e.leaveTime > e.release) { + if (!isInViewport || now - e.leaveTime > e.release) { continue; } // Update uniforms e.uniforms["time"].value = now - e.startTime; - e.uniforms["enterTime"].value = - e.enterTime === -1 ? 0 : now - e.enterTime; - e.uniforms["leaveTime"].value = - e.leaveTime === -1 ? 0 : now - e.leaveTime; + e.uniforms["enterTime"].value = now - e.enterTime; + e.uniforms["leaveTime"].value = now - e.leaveTime; e.uniforms["resolution"].value.x = rect.width * this.#pixelRatio; // TODO: use correct width, height e.uniforms["resolution"].value.y = rect.height * this.#pixelRatio; e.uniforms["offset"].value.x = rect.left * this.#pixelRatio; From 2c77c6e8171845cfcbe6ed708c326f1c14f1c0c1 Mon Sep 17 00:00:00 2001 From: fand Date: Thu, 25 Jul 2024 23:57:48 -0700 Subject: [PATCH 15/24] feat: add out animation to transition shader presets --- packages/vfx-js/src/constants.ts | 47 +++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/packages/vfx-js/src/constants.ts b/packages/vfx-js/src/constants.ts index 293b77a..455582d 100644 --- a/packages/vfx-js/src/constants.ts +++ b/packages/vfx-js/src/constants.ts @@ -578,13 +578,23 @@ export const shaders: Record = { uniform vec2 offset; uniform float time; uniform float enterTime; + uniform float leaveTime; uniform sampler2D src; + #define DURATION 1.0 + void main (void) { vec2 uv = (gl_FragCoord.xy - offset) / resolution; - if (enterTime < 1.5) { - float t = enterTime / 1.5; + float t1 = enterTime / DURATION; + float t2 = leaveTime / DURATION; + float t = clamp(min(t1, 1. - t2), 0., 1.); + + if (t == 0.) { + discard; + } + + if (t < 1.) { uv.x += sin(floor(uv.y * 300.)) * 3. * exp(t * -10.); } @@ -597,14 +607,30 @@ export const shaders: Record = { uniform vec2 offset; uniform float time; uniform float enterTime; + uniform float leaveTime; uniform sampler2D src; + #define DURATION 1.0 + void main (void) { vec2 uv = (gl_FragCoord.xy - offset) / resolution; - if (enterTime < 1.5) { - float t = 1. - enterTime / 1.5; - uv.y = uv.y > t ? uv.y : t; + float t1 = enterTime / DURATION; + float t2 = leaveTime / DURATION; + + // Do not render before enter or after leave + if (t1 < 0. || 1. < t2) { + discard; + } + + if (0. < t2) { + // Leaving + float t = 1. - t2; + uv.y = uv.y < t ? uv.y : t; + } else if (t1 < 1.) { + // Entering + float t = 1. - t1; + uv.y = uv.y < t ? t : uv.y; } gl_FragColor = texture2D(src, uv); @@ -616,14 +642,21 @@ export const shaders: Record = { uniform vec2 offset; uniform float time; uniform float enterTime; + uniform float leaveTime; uniform sampler2D src; + #define DURATION 1.0 + void main (void) { vec2 uv = (gl_FragCoord.xy - offset) / resolution; - if (enterTime < 1.5) { - float t = enterTime / 1.5; + float t1 = enterTime / DURATION; + float t2 = leaveTime / DURATION; + float t = clamp(min(t1, 1. - t2), 0., 1.); + if (t == 0.) { + discard; + } else if (t < 1.) { float b = floor(t * 64.); uv = (floor(uv * b) + .5) / b; } From 16224399e951b5b66b86e61521033b1cd88f0bc8 Mon Sep 17 00:00:00 2001 From: fand Date: Thu, 25 Jul 2024 23:58:31 -0700 Subject: [PATCH 16/24] chore: add threshold property to transition examples --- packages/docs/index.html | 3 +++ packages/docs/src/main.ts | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/packages/docs/index.html b/packages/docs/index.html index c8105ca..5c3a6c8 100644 --- a/packages/docs/index.html +++ b/packages/docs/index.html @@ -392,6 +392,7 @@

Transitions

@@ -405,6 +406,7 @@

Transitions

@@ -418,6 +420,7 @@

Transitions

diff --git a/packages/docs/src/main.ts b/packages/docs/src/main.ts index 318bce2..4ca4fea 100644 --- a/packages/docs/src/main.ts +++ b/packages/docs/src/main.ts @@ -229,6 +229,11 @@ class App { shader, overflow: parseFloat(e.getAttribute("data-overflow") ?? "0"), uniforms, + intersection: { + threshold: parseFloat( + e.getAttribute("data-threshold") ?? "0", + ), + }, }); } } From 661204ae4c06dc98e705ba31a3b2012cda09a953 Mon Sep 17 00:00:00 2001 From: fand Date: Wed, 14 Aug 2024 11:11:52 -0700 Subject: [PATCH 17/24] refactor: process overflow outside isRectInViewport --- packages/vfx-js/src/vfx-player.test.ts | 130 +------------------------ packages/vfx-js/src/vfx-player.ts | 25 +++-- 2 files changed, 15 insertions(+), 140 deletions(-) diff --git a/packages/vfx-js/src/vfx-player.test.ts b/packages/vfx-js/src/vfx-player.test.ts index f853e6e..a4ecc1c 100644 --- a/packages/vfx-js/src/vfx-player.test.ts +++ b/packages/vfx-js/src/vfx-player.test.ts @@ -105,25 +105,16 @@ describe("isRectInViewport", () => { }; }; - type Pad = Parameters[2]; - const pad = (t: number): Pad => ({ - left: t, - right: t, - top: t, - bottom: t, - }); - test("no overflow", () => { - expect( - isRectInViewport(rect(0, 0, 1, 1), rect(0, 0, 1, 1), pad(0), 0), - ).toBe(true); + expect(isRectInViewport(rect(0, 0, 1, 1), rect(0, 0, 1, 1), 0)).toBe( + true, + ); // adjacent rects expect( isRectInViewport( rect(0, 0, 1, 1), rect(-1, 0, 1, 1), // left - pad(0), 0, ), ).toBe(true); @@ -131,7 +122,6 @@ describe("isRectInViewport", () => { isRectInViewport( rect(0, 0, 1, 1), rect(1, 0, 1, 1), // right - pad(0), 0, ), ).toBe(true); @@ -139,7 +129,6 @@ describe("isRectInViewport", () => { isRectInViewport( rect(0, 0, 1, 1), rect(0, -1, 1, 1), // top - pad(0), 0, ), ).toBe(true); @@ -147,7 +136,6 @@ describe("isRectInViewport", () => { isRectInViewport( rect(0, 0, 1, 1), rect(0, 1, 1, 1), // bottom - pad(0), 0, ), ).toBe(true); @@ -157,7 +145,6 @@ describe("isRectInViewport", () => { isRectInViewport( rect(0, 0, 1, 1), rect(-2, 0, 1, 1), // 1px left - pad(0), 0, ), ).toBe(false); @@ -165,7 +152,6 @@ describe("isRectInViewport", () => { isRectInViewport( rect(0, 0, 1, 1), rect(2, 0, 1, 1), // 1px right - pad(0), 0, ), ).toBe(false); @@ -173,7 +159,6 @@ describe("isRectInViewport", () => { isRectInViewport( rect(0, 0, 1, 1), rect(0, -2, 1, 1), // 1px top - pad(0), 0, ), ).toBe(false); @@ -181,115 +166,6 @@ describe("isRectInViewport", () => { isRectInViewport( rect(0, 0, 1, 1), rect(0, 2, 1, 1), // 1px bottom - pad(0), - 0, - ), - ).toBe(false); - }); - - test("with overflow", () => { - expect( - isRectInViewport(rect(0, 0, 1, 1), rect(0, 0, 1, 1), pad(1), 0), - ).toBe(true); - - // adjacent rects - expect( - isRectInViewport( - rect(0, 0, 1, 1), - rect(-1, 0, 1, 1), // left - pad(1), - 0, - ), - ).toBe(true); - expect( - isRectInViewport( - rect(0, 0, 1, 1), - rect(1, 0, 1, 1), // right - pad(1), - 0, - ), - ).toBe(true); - expect( - isRectInViewport( - rect(0, 0, 1, 1), - rect(0, -1, 1, 1), // top - pad(1), - 0, - ), - ).toBe(true); - expect( - isRectInViewport( - rect(0, 0, 1, 1), - rect(0, 1, 1, 1), // bottom - pad(1), - 0, - ), - ).toBe(true); - - // distant rects - expect( - isRectInViewport( - rect(0, 0, 1, 1), - rect(-2, 0, 1, 1), // 1px left - pad(1), - 0, - ), - ).toBe(true); - expect( - isRectInViewport( - rect(0, 0, 1, 1), - rect(2, 0, 1, 1), // 1px right - pad(1), - 0, - ), - ).toBe(true); - expect( - isRectInViewport( - rect(0, 0, 1, 1), - rect(0, -2, 1, 1), // 1px top - pad(1), - 0, - ), - ).toBe(true); - expect( - isRectInViewport( - rect(0, 0, 1, 1), - rect(0, 2, 1, 1), // 1px bottom - pad(1), - 0, - ), - ).toBe(true); - - // more distant rects - expect( - isRectInViewport( - rect(0, 0, 1, 1), - rect(-3, 0, 1, 1), // 2px left - pad(1), - 0, - ), - ).toBe(false); - expect( - isRectInViewport( - rect(0, 0, 1, 1), - rect(3, 0, 1, 1), // 2px right - pad(1), - 0, - ), - ).toBe(false); - expect( - isRectInViewport( - rect(0, 0, 1, 1), - rect(0, -3, 1, 1), // 2px top - pad(1), - 0, - ), - ).toBe(false); - expect( - isRectInViewport( - rect(0, 0, 1, 1), - rect(0, 3, 1, 1), // 2px bottom - pad(1), 0, ), ).toBe(false); diff --git a/packages/vfx-js/src/vfx-player.ts b/packages/vfx-js/src/vfx-player.ts index 5ab0ef1..31a9297 100644 --- a/packages/vfx-js/src/vfx-player.ts +++ b/packages/vfx-js/src/vfx-player.ts @@ -172,9 +172,11 @@ export class VFXPlayer { const rect = element.getBoundingClientRect(); const [isFullScreen, overflow] = sanitizeOverflow(opts.overflow); + const rectHitTest = growRect(rect, overflow); + const intersection = sanitizeIntersection(opts.intersection); const isInViewport = - isFullScreen || isRectInViewport(this.#viewport, rect, overflow, 0); + isFullScreen || isRectInViewport(this.#viewport, rectHitTest, 0); const transitionArea = growRect( this.#viewport, @@ -184,8 +186,7 @@ export class VFXPlayer { isFullScreen || isRectInViewport( transitionArea, - rect, - overflow, + rectHitTest, intersection.threshold, ); @@ -371,11 +372,12 @@ export class VFXPlayer { for (const e of this.#elements) { const rect = e.element.getBoundingClientRect(); + const rectHitTest = growRect(rect, e.overflow); // Check intersection const isInViewport = e.isFullScreen || - isRectInViewport(this.#viewport, rect, e.overflow, 0); + isRectInViewport(this.#viewport, rectHitTest, 0); const transitionArea = growRect( this.#viewport, @@ -385,8 +387,7 @@ export class VFXPlayer { e.isFullScreen || isRectInViewport( transitionArea, - rect, - e.overflow, + rectHitTest, e.intersection.threshold, ); @@ -476,20 +477,18 @@ export class VFXPlayer { export function isRectInViewport( viewport: Rect, rect: Rect, - overflow: Rect, threshold: number, ): boolean { - const rect2 = growRect(rect, overflow); if (threshold === 0) { // if threshold == 0, consider adjacent rects to be intersecting. return ( - rect2.left <= viewport.right && - rect2.right >= viewport.left && - rect2.top <= viewport.bottom && - rect2.bottom >= viewport.top + rect.left <= viewport.right && + rect.right >= viewport.left && + rect.top <= viewport.bottom && + rect.bottom >= viewport.top ); } else { - return getIntersection(viewport, rect2) >= threshold; + return getIntersection(viewport, rect) >= threshold; } } From 8f44fbd4fbf560ab4238b5fcc092c7c6c93b6320 Mon Sep 17 00:00:00 2001 From: fand Date: Wed, 14 Aug 2024 11:35:12 -0700 Subject: [PATCH 18/24] refactor: check intersection and collision separately --- packages/vfx-js/src/vfx-player.test.ts | 12 +------ packages/vfx-js/src/vfx-player.ts | 44 +++++++++++++++++--------- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/packages/vfx-js/src/vfx-player.test.ts b/packages/vfx-js/src/vfx-player.test.ts index a4ecc1c..0b8ef65 100644 --- a/packages/vfx-js/src/vfx-player.test.ts +++ b/packages/vfx-js/src/vfx-player.test.ts @@ -106,37 +106,31 @@ describe("isRectInViewport", () => { }; test("no overflow", () => { - expect(isRectInViewport(rect(0, 0, 1, 1), rect(0, 0, 1, 1), 0)).toBe( - true, - ); + expect(isRectInViewport(rect(0, 0, 1, 1), rect(0, 0, 1, 1))).toBe(true); // adjacent rects expect( isRectInViewport( rect(0, 0, 1, 1), rect(-1, 0, 1, 1), // left - 0, ), ).toBe(true); expect( isRectInViewport( rect(0, 0, 1, 1), rect(1, 0, 1, 1), // right - 0, ), ).toBe(true); expect( isRectInViewport( rect(0, 0, 1, 1), rect(0, -1, 1, 1), // top - 0, ), ).toBe(true); expect( isRectInViewport( rect(0, 0, 1, 1), rect(0, 1, 1, 1), // bottom - 0, ), ).toBe(true); @@ -145,28 +139,24 @@ describe("isRectInViewport", () => { isRectInViewport( rect(0, 0, 1, 1), rect(-2, 0, 1, 1), // 1px left - 0, ), ).toBe(false); expect( isRectInViewport( rect(0, 0, 1, 1), rect(2, 0, 1, 1), // 1px right - 0, ), ).toBe(false); expect( isRectInViewport( rect(0, 0, 1, 1), rect(0, -2, 1, 1), // 1px top - 0, ), ).toBe(false); expect( isRectInViewport( rect(0, 0, 1, 1), rect(0, 2, 1, 1), // 1px bottom - 0, ), ).toBe(false); }); diff --git a/packages/vfx-js/src/vfx-player.ts b/packages/vfx-js/src/vfx-player.ts index 31a9297..5f77a47 100644 --- a/packages/vfx-js/src/vfx-player.ts +++ b/packages/vfx-js/src/vfx-player.ts @@ -176,17 +176,19 @@ export class VFXPlayer { const intersection = sanitizeIntersection(opts.intersection); const isInViewport = - isFullScreen || isRectInViewport(this.#viewport, rectHitTest, 0); + isFullScreen || isRectInViewport(this.#viewport, rectHitTest); const transitionArea = growRect( this.#viewport, intersection.rootMargin, ); + const intersectionRatio = getIntersection(this.#viewport, rectHitTest); const isInTransitionArea = isFullScreen || - isRectInViewport( + checkIntersection( transitionArea, rectHitTest, + intersectionRatio, intersection.threshold, ); @@ -376,18 +378,22 @@ export class VFXPlayer { // Check intersection const isInViewport = - e.isFullScreen || - isRectInViewport(this.#viewport, rectHitTest, 0); + e.isFullScreen || isRectInViewport(this.#viewport, rectHitTest); const transitionArea = growRect( this.#viewport, e.intersection.rootMargin, ); + const intersectionRatio = getIntersection( + transitionArea, + rectHitTest, + ); const isInTransitionArea = e.isFullScreen || - isRectInViewport( + checkIntersection( transitionArea, rectHitTest, + intersectionRatio, e.intersection.threshold, ); @@ -473,22 +479,30 @@ export class VFXPlayer { } } -// TODO: Consider custom root element -export function isRectInViewport( +/** + * Returns if the given rects intersect. + * It returns true when the rects are adjacent (= intersection ratio is 0). + */ +export function isRectInViewport(viewport: Rect, rect: Rect): boolean { + return ( + rect.left <= viewport.right && + rect.right >= viewport.left && + rect.top <= viewport.bottom && + rect.bottom >= viewport.top + ); +} + +export function checkIntersection( viewport: Rect, rect: Rect, + intersection: number, threshold: number, ): boolean { if (threshold === 0) { - // if threshold == 0, consider adjacent rects to be intersecting. - return ( - rect.left <= viewport.right && - rect.right >= viewport.left && - rect.top <= viewport.bottom && - rect.bottom >= viewport.top - ); + // if threshold === 0, consider adjacent rects to be intersecting. + return isRectInViewport(viewport, rect); } else { - return getIntersection(viewport, rect) >= threshold; + return intersection >= threshold; } } From 33fa57a7e28c99c10fea53854d00d1aeef67c0f3 Mon Sep 17 00:00:00 2001 From: fand Date: Wed, 14 Aug 2024 11:44:17 -0700 Subject: [PATCH 19/24] chore: rename functions --- packages/vfx-js/src/vfx-player.test.ts | 22 +++++++++++----------- packages/vfx-js/src/vfx-player.ts | 8 ++++---- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/vfx-js/src/vfx-player.test.ts b/packages/vfx-js/src/vfx-player.test.ts index 0b8ef65..f565577 100644 --- a/packages/vfx-js/src/vfx-player.test.ts +++ b/packages/vfx-js/src/vfx-player.test.ts @@ -1,14 +1,14 @@ import { expect, describe, test } from "vitest"; -import { isRectInViewport, sanitizeOverflow } from "./vfx-player"; +import { isRectInViewport, parseOverflowOpts } from "./vfx-player"; import { RECT_ZERO } from "./rect"; -describe("sanitizeOverflow", () => { +describe("parseOverflowOpts", () => { test('true => "fullscreen"', () => { - expect(sanitizeOverflow(true)).toStrictEqual([true, RECT_ZERO]); + expect(parseOverflowOpts(true)).toStrictEqual([true, RECT_ZERO]); }); test("undefined => 0", () => { - expect(sanitizeOverflow(undefined)).toStrictEqual([ + expect(parseOverflowOpts(undefined)).toStrictEqual([ false, { top: 0, @@ -20,7 +20,7 @@ describe("sanitizeOverflow", () => { }); test("number", () => { - expect(sanitizeOverflow(100)).toStrictEqual([ + expect(parseOverflowOpts(100)).toStrictEqual([ false, { top: 100, @@ -32,7 +32,7 @@ describe("sanitizeOverflow", () => { }); test("number array", () => { - expect(sanitizeOverflow([0, 100, 200, 300])).toStrictEqual([ + expect(parseOverflowOpts([0, 100, 200, 300])).toStrictEqual([ false, { top: 0, @@ -44,7 +44,7 @@ describe("sanitizeOverflow", () => { }); test("object", () => { - expect(sanitizeOverflow({})).toStrictEqual([ + expect(parseOverflowOpts({})).toStrictEqual([ false, { top: 0, @@ -53,7 +53,7 @@ describe("sanitizeOverflow", () => { left: 0, }, ]); - expect(sanitizeOverflow({ top: 100 })).toStrictEqual([ + expect(parseOverflowOpts({ top: 100 })).toStrictEqual([ false, { top: 100, @@ -62,7 +62,7 @@ describe("sanitizeOverflow", () => { left: 0, }, ]); - expect(sanitizeOverflow({ left: 100 })).toStrictEqual([ + expect(parseOverflowOpts({ left: 100 })).toStrictEqual([ false, { top: 0, @@ -71,7 +71,7 @@ describe("sanitizeOverflow", () => { left: 100, }, ]); - expect(sanitizeOverflow({ top: 100, left: 200 })).toStrictEqual([ + expect(parseOverflowOpts({ top: 100, left: 200 })).toStrictEqual([ false, { top: 100, @@ -81,7 +81,7 @@ describe("sanitizeOverflow", () => { }, ]); expect( - sanitizeOverflow({ top: 100, right: 200, bottom: 300, left: 400 }), + parseOverflowOpts({ top: 100, right: 200, bottom: 300, left: 400 }), ).toStrictEqual([ false, { diff --git a/packages/vfx-js/src/vfx-player.ts b/packages/vfx-js/src/vfx-player.ts index 5f77a47..2cc4cfa 100644 --- a/packages/vfx-js/src/vfx-player.ts +++ b/packages/vfx-js/src/vfx-player.ts @@ -171,10 +171,10 @@ export class VFXPlayer { const shader = this.#getShader(opts.shader || "uvGradient"); const rect = element.getBoundingClientRect(); - const [isFullScreen, overflow] = sanitizeOverflow(opts.overflow); + const [isFullScreen, overflow] = parseOverflowOpts(opts.overflow); const rectHitTest = growRect(rect, overflow); - const intersection = sanitizeIntersection(opts.intersection); + const intersection = parseIntersectionOpts(opts.intersection); const isInViewport = isFullScreen || isRectInViewport(this.#viewport, rectHitTest); @@ -506,7 +506,7 @@ export function checkIntersection( } } -export function sanitizeOverflow( +export function parseOverflowOpts( overflow: VFXProps["overflow"], ): [isFullScreen: boolean, Rect] { if (overflow === true) { @@ -518,7 +518,7 @@ export function sanitizeOverflow( return [false, createRect(overflow)]; } -export function sanitizeIntersection( +export function parseIntersectionOpts( intersectionOpts: VFXProps["intersection"], ): VFXElementIntersection { const threshold = intersectionOpts?.threshold ?? 0; From 5ed94030722a3cba12663bea71fa5390d4106c01 Mon Sep 17 00:00:00 2001 From: fand Date: Wed, 14 Aug 2024 11:50:26 -0700 Subject: [PATCH 20/24] chore: rename variables --- packages/vfx-js/src/types.ts | 2 +- packages/vfx-js/src/vfx-player.ts | 39 ++++++++++++++----------------- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/packages/vfx-js/src/types.ts b/packages/vfx-js/src/types.ts index e22b5c3..046a6f2 100644 --- a/packages/vfx-js/src/types.ts +++ b/packages/vfx-js/src/types.ts @@ -176,7 +176,7 @@ export type VFXElement = { type: VFXElementType; element: HTMLElement; isInViewport: boolean; - isInTransitionArea: boolean; + isInLogicalViewport: boolean; width: number; height: number; scene: THREE.Scene; diff --git a/packages/vfx-js/src/vfx-player.ts b/packages/vfx-js/src/vfx-player.ts index 2cc4cfa..40a9e07 100644 --- a/packages/vfx-js/src/vfx-player.ts +++ b/packages/vfx-js/src/vfx-player.ts @@ -174,22 +174,22 @@ export class VFXPlayer { const [isFullScreen, overflow] = parseOverflowOpts(opts.overflow); const rectHitTest = growRect(rect, overflow); - const intersection = parseIntersectionOpts(opts.intersection); + const intersectionOpts = parseIntersectionOpts(opts.intersection); const isInViewport = isFullScreen || isRectInViewport(this.#viewport, rectHitTest); - const transitionArea = growRect( + const logicalViewport = growRect( this.#viewport, - intersection.rootMargin, + intersectionOpts.rootMargin, ); - const intersectionRatio = getIntersection(this.#viewport, rectHitTest); - const isInTransitionArea = + const intersection = getIntersection(this.#viewport, rectHitTest); + const isInLogicalViewport = isFullScreen || checkIntersection( - transitionArea, + logicalViewport, rectHitTest, - intersectionRatio, - intersection.threshold, + intersection, + intersectionOpts.threshold, ); const originalOpacity = @@ -288,7 +288,7 @@ export class VFXPlayer { type, element, isInViewport, - isInTransitionArea, + isInLogicalViewport, width: rect.width, height: rect.height, scene, @@ -301,7 +301,7 @@ export class VFXPlayer { isGif, isFullScreen, overflow, - intersection, + intersection: intersectionOpts, originalOpacity, zIndex: opts.zIndex ?? 0, }; @@ -380,34 +380,31 @@ export class VFXPlayer { const isInViewport = e.isFullScreen || isRectInViewport(this.#viewport, rectHitTest); - const transitionArea = growRect( + const logicalViewport = growRect( this.#viewport, e.intersection.rootMargin, ); - const intersectionRatio = getIntersection( - transitionArea, - rectHitTest, - ); - const isInTransitionArea = + const intersection = getIntersection(logicalViewport, rectHitTest); + const isInLogicalViewport = e.isFullScreen || checkIntersection( - transitionArea, + logicalViewport, rectHitTest, - intersectionRatio, + intersection, e.intersection.threshold, ); // Update transition timing - if (!e.isInTransitionArea && isInTransitionArea /* out -> in */) { + if (!e.isInLogicalViewport && isInLogicalViewport /* out -> in */) { e.enterTime = now; e.leaveTime = Infinity; } - if (e.isInTransitionArea && !isInTransitionArea /* in -> out */) { + if (e.isInLogicalViewport && !isInLogicalViewport /* in -> out */) { e.leaveTime = now; } e.isInViewport = isInViewport; - e.isInTransitionArea = isInTransitionArea; + e.isInLogicalViewport = isInLogicalViewport; // Quit if the element has left and the transition has ended if (!isInViewport || now - e.leaveTime > e.release) { From 027f8936a42cd5f7416892d89b922bfdf93beb4f Mon Sep 17 00:00:00 2001 From: fand Date: Wed, 14 Aug 2024 11:56:15 -0700 Subject: [PATCH 21/24] feat: add "intersection" uniform --- packages/docs/index.html | 13 +++++++++++++ packages/vfx-js/src/constants.ts | 22 +++++++++++++++++++++- packages/vfx-js/src/vfx-player.ts | 2 ++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/docs/index.html b/packages/docs/index.html index 5c3a6c8..61f8095 100644 --- a/packages/docs/index.html +++ b/packages/docs/index.html @@ -424,6 +424,19 @@

Transitions

/> +
+
+

+vfx.add(el, { shader: "focusTransition" });
+                        
+
+
+ +
+
diff --git a/packages/vfx-js/src/constants.ts b/packages/vfx-js/src/constants.ts index 455582d..c3341b1 100644 --- a/packages/vfx-js/src/constants.ts +++ b/packages/vfx-js/src/constants.ts @@ -25,7 +25,8 @@ export type ShaderPreset = | "halftone" | "slitScanTransition" | "warpTransition" - | "pixelateTransition"; + | "pixelateTransition" + | "focusTransition"; /** * Shader code for presets. @@ -664,4 +665,23 @@ export const shaders: Record = { gl_FragColor = texture2D(src, uv); } `, + focusTransition: ` + precision highp float; + uniform vec2 resolution; + uniform vec2 offset; + uniform float time; + uniform float intersection; + uniform sampler2D src; + + void main (void) { + vec2 uv = (gl_FragCoord.xy - offset) / resolution; + float t = smoothstep(0., 1., intersection); + + gl_FragColor = mix( + texture2D(src, uv + vec2(1. - t, 0)), + texture2D(src, uv + vec2(-(1. - t), 0)), + 0.5 + ) * intersection; + } + `, }; diff --git a/packages/vfx-js/src/vfx-player.ts b/packages/vfx-js/src/vfx-player.ts index 40a9e07..9821c53 100644 --- a/packages/vfx-js/src/vfx-player.ts +++ b/packages/vfx-js/src/vfx-player.ts @@ -252,6 +252,7 @@ export class VFXPlayer { enterTime: { value: -1.0 }, leaveTime: { value: -1.0 }, mouse: { value: new THREE.Vector2() }, + intersection: { value: intersection }, }; const uniformGenerators: { @@ -423,6 +424,7 @@ export class VFXPlayer { this.#pixelRatio; e.uniforms["mouse"].value.x = this.#mouseX * this.#pixelRatio; e.uniforms["mouse"].value.y = this.#mouseY * this.#pixelRatio; + e.uniforms["intersection"].value = intersection; for (const [key, gen] of Object.entries(e.uniformGenerators)) { e.uniforms[key].value = gen(); From d5ad64ae9bcce39eff4adea1ee3db9093fbb86f6 Mon Sep 17 00:00:00 2001 From: fand Date: Wed, 14 Aug 2024 13:34:13 -0700 Subject: [PATCH 22/24] chore: add comments --- packages/vfx-js/src/types.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/vfx-js/src/types.ts b/packages/vfx-js/src/types.ts index 046a6f2..6486c6c 100644 --- a/packages/vfx-js/src/types.ts +++ b/packages/vfx-js/src/types.ts @@ -100,8 +100,15 @@ export type VFXProps = { */ overlay?: true | number; + /** + * Options to control transition behaviour. + * These properties work similarly to the IntersectionObsrever options. + */ intersection?: { + /** Threshold for the element to be considered "entered" to the viewport. */ threshold?: number; + + /** Margin of the viewport to be used in intersection calculcation. */ rootMargin?: RectOpts; }; From 6b927821a13a7b20b45a1803725120c57e16aaf5 Mon Sep 17 00:00:00 2001 From: fand Date: Wed, 14 Aug 2024 14:07:38 -0700 Subject: [PATCH 23/24] fix: use actual BoundingRect for intersection calculation --- packages/vfx-js/src/vfx-player.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/vfx-js/src/vfx-player.ts b/packages/vfx-js/src/vfx-player.ts index 9821c53..56949f4 100644 --- a/packages/vfx-js/src/vfx-player.ts +++ b/packages/vfx-js/src/vfx-player.ts @@ -182,12 +182,12 @@ export class VFXPlayer { this.#viewport, intersectionOpts.rootMargin, ); - const intersection = getIntersection(this.#viewport, rectHitTest); + const intersection = getIntersection(this.#viewport, rect); const isInLogicalViewport = isFullScreen || checkIntersection( logicalViewport, - rectHitTest, + rect, intersection, intersectionOpts.threshold, ); @@ -385,12 +385,12 @@ export class VFXPlayer { this.#viewport, e.intersection.rootMargin, ); - const intersection = getIntersection(logicalViewport, rectHitTest); + const intersection = getIntersection(logicalViewport, rect); const isInLogicalViewport = e.isFullScreen || checkIntersection( logicalViewport, - rectHitTest, + rect, intersection, e.intersection.threshold, ); From f30be83d1823037c7d83cc33ce4da91e54510329 Mon Sep 17 00:00:00 2001 From: fand Date: Wed, 14 Aug 2024 14:08:03 -0700 Subject: [PATCH 24/24] chore: update profile shader in docs --- packages/docs/src/main.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/docs/src/main.ts b/packages/docs/src/main.ts index 4ca4fea..efb5ba9 100644 --- a/packages/docs/src/main.ts +++ b/packages/docs/src/main.ts @@ -17,6 +17,7 @@ const shaders: Record = { uniform vec2 offset; uniform float time; uniform float enterTime; + uniform float leaveTime; uniform sampler2D src; uniform float delay; @@ -79,7 +80,10 @@ const shaders: Record = { void main (void) { vec2 uv = (gl_FragCoord.xy - offset) / resolution; - if (enterTime < 1.0) { + if (leaveTime > 0.) { + float t = clamp(leaveTime - 0.5, 0., 1.); + gl_FragColor = glitch(uv) * (1. - t); + } else if (enterTime < 1.0) { gl_FragColor = slitscan(uv); } else { gl_FragColor = glitch(uv); @@ -354,6 +358,9 @@ class App { shader: shaders.logo, overflow: [0, 3000, 0, 100], uniforms: { delay: 0 }, + intersection: { + threshold: 1, + }, }); const tagline = document.getElementById("LogoTagline")!; @@ -361,6 +368,9 @@ class App { shader: shaders.logo, overflow: [0, 3000, 0, 1000], uniforms: { delay: 0.3 }, + intersection: { + threshold: 1, + }, }); } @@ -368,8 +378,12 @@ class App { const profile = document.getElementById("profile")!; this.vfx.add(profile, { shader: shaders.logo, - overflow: [0, 2000, 0, 1000], + overflow: [0, 2000, 0, 2000], uniforms: { delay: 0.5 }, + intersection: { + rootMargin: [-100, 0, -100, 0], + threshold: 1, + }, }); } }