From 9bba5cc4fe824ce38419bb3d7faf68d538a58231 Mon Sep 17 00:00:00 2001 From: jfboeve Date: Mon, 14 Jul 2025 14:27:37 +0200 Subject: [PATCH] port touch detection to v2 --- examples/tests/detect-touch.ts | 113 +++++++++++++++++++++++++++++++++ src/core/CoreNode.ts | 19 ++++++ src/core/Stage.ts | 51 ++++++++++++++- src/core/lib/utils.ts | 4 ++ 4 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 examples/tests/detect-touch.ts diff --git a/examples/tests/detect-touch.ts b/examples/tests/detect-touch.ts new file mode 100644 index 00000000..bff7b261 --- /dev/null +++ b/examples/tests/detect-touch.ts @@ -0,0 +1,113 @@ +import type { Point } from '@lightningjs/renderer'; +import type { ExampleSettings } from '../common/ExampleSettings.js'; +import type { CoreNode } from '../../dist/src/core/CoreNode.js'; + +const getRandomValue = (min: number, max: number) => { + return Math.random() * (max - min) + min; +}; + +const getRandomColor = () => { + const randomInt = Math.floor(Math.random() * Math.pow(2, 24)); // Use 24 bits for RGB + const hexString = randomInt.toString(16).padStart(6, '0'); // RGB hex without alpha + return parseInt(hexString + 'FF', 16); // Append 'FF' for full alpha +}; + +const getRandomBezierCurve = () => { + // Generate random values for control points within specified ranges + const x1 = Math.random(); // 0 to 1 + const y1 = Math.random() * 2; // Allow values above 1 + const x2 = Math.random(); // 0 to 1 + const y2 = Math.random() * 2 - 1; // Allow values between -1 and 1 + + // Return the Bezier curve in the required format + return `cubic-bezier(${x1.toFixed(2)}, ${y1.toFixed(2)}, ${x2.toFixed( + 2, + )}, ${y2.toFixed(2)})`; +}; + +export default async function ({ renderer, testRoot }: ExampleSettings) { + const holder = renderer.createNode({ + x: 0, + y: 0, + width: 1920, + height: 1080, + color: 0x000000ff, + parent: testRoot, + }); + + // Copy source texture from rootRenderToTextureNode + for (let i = 0; i < 50; i++) { + const dimension = getRandomValue(30, 150); + const node = renderer.createNode({ + parent: holder, + x: getRandomValue(0, 1820), + y: getRandomValue(0, 980), + width: dimension, + height: dimension, + color: getRandomColor(), + interactive: true, + zIndex: getRandomValue(0, 100), + }); + + node + .animate( + { + x: getRandomValue(0, 1820), + y: getRandomValue(0, 980), + }, + { + duration: getRandomValue(8000, 12000), + delay: getRandomValue(0, 5000), + stopMethod: 'reverse', + loop: true, + easing: getRandomBezierCurve(), + }, + ) + .start(); + } + + document.addEventListener('touchstart', (e: TouchEvent) => { + const { changedTouches } = e; + if (changedTouches.length) { + const touch = changedTouches.item(0); + + const x = touch?.clientX ?? 0; + const y = touch?.clientY ?? 0; + + const eventData: Point = { + x, + y, + }; + // const nodes: CoreNode[] = renderer.stage.findNodesAtPoint(eventData); + const topNode: CoreNode | null = + renderer.stage.getNodeFromPosition(eventData); + + if (topNode) { + topNode.scale = 1.5; + setTimeout(() => { + topNode.scale = 1; + }, 150); + } + } + }); + + document.addEventListener('mousemove', (e: MouseEvent) => { + const x = e?.clientX ?? 0; + const y = e?.clientY ?? 0; + + const eventData: Point = { + x, + y, + }; + // const nodes: CoreNode[] = renderer.stage.findNodesAtPoint(eventData); + const topNode: CoreNode | null = + renderer.stage.getNodeFromPosition(eventData); + + if (topNode) { + topNode.scale = 1.5; + setTimeout(() => { + topNode.scale = 1; + }, 150); + } + }); +} diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index 6c5d7ca4..db86e126 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -692,6 +692,12 @@ export interface CoreNodeProps { * @default false */ strictBounds: boolean; + /** + * Mark the node as interactive so we can perform hit tests on it + * when pointer events are registered. + * @default false + */ + interactive?: boolean; } /** @@ -775,6 +781,7 @@ export class CoreNode extends EventEmitter { this.texture = props.texture; this.src = props.src; this.rtt = props.rtt; + this.interactive = props.interactive; if (props.boundsMargin) { this.boundsMargin = Array.isArray(props.boundsMargin) @@ -2446,6 +2453,18 @@ export class CoreNode extends EventEmitter { this.childUpdateType |= UpdateType.RenderBounds | UpdateType.Children; } + set interactive(value: boolean | undefined) { + this.props.interactive = value; + // Update Stage's interactive Set + if (value === true) { + this.stage.interactiveNodes.add(this); + } + } + + get interactive(): boolean | undefined { + return this.props.interactive; + } + animate( props: Partial, settings: Partial, diff --git a/src/core/Stage.ts b/src/core/Stage.ts index fcb0374e..5390be32 100644 --- a/src/core/Stage.ts +++ b/src/core/Stage.ts @@ -54,7 +54,12 @@ import { CoreTextNode, type CoreTextNodeProps } from './CoreTextNode.js'; import { santizeCustomDataMap } from '../main-api/utils.js'; import type { SdfTextRenderer } from './text-rendering/renderers/SdfTextRenderer/SdfTextRenderer.js'; import type { CanvasTextRenderer } from './text-rendering/renderers/CanvasTextRenderer.js'; -import { createBound, createPreloadBounds, type Bound } from './lib/utils.js'; +import { + createBound, + createPreloadBounds, + pointInBound, + type Bound, +} from './lib/utils.js'; import type { Texture } from './textures/Texture.js'; import { ColorTexture } from './textures/ColorTexture.js'; @@ -91,6 +96,10 @@ export type StageFrameTickHandler = ( stage: Stage, frameTickData: FrameTickPayload, ) => void; +export interface Point { + x: number; + y: number; +} const bufferMemory = 2e6; const autoStart = true; @@ -105,6 +114,7 @@ export class Stage { public readonly shManager: CoreShaderManager; public readonly renderer: CoreRenderer; public readonly root: CoreNode; + public readonly interactiveNodes: Set = new Set(); public boundsMargin: [number, number, number, number]; public readonly defShaderCtr: BaseShaderController; public readonly strictBound: Bound; @@ -543,6 +553,44 @@ export class Stage { this.renderRequested = true; } + /** + * Find all nodes at a given point + * @param data + */ + findNodesAtPoint(data: Point): CoreNode[] { + const x = data.x / this.options.deviceLogicalPixelRatio; + const y = data.y / this.options.deviceLogicalPixelRatio; + const nodes: CoreNode[] = []; + for (const node of this.interactiveNodes) { + if (node.isRenderable === false) { + continue; + } + if (pointInBound(x, y, node.renderBound!) === true) { + nodes.push(node); + } + } + return nodes; + } + + /** + * Find the top node at a given point + * @param data + * @returns + */ + getNodeFromPosition(data: Point): CoreNode | null { + const nodes: CoreNode[] = this.findNodesAtPoint(data); + if (nodes.length === 0) { + return null; + } + let topNode = nodes[0] as CoreNode; + for (let i = 0; i < nodes.length; i++) { + if (nodes[i]!.zIndex > topNode.zIndex) { + topNode = nodes[i]!; + } + } + return topNode || null; + } + /** * Given a font name, and possible renderer override, return the best compatible text renderer. * @@ -761,6 +809,7 @@ export class Stage { data: data, preventCleanup: props.preventCleanup ?? false, imageType: props.imageType, + interactive: props.interactive ?? false, strictBounds: props.strictBounds ?? this.strictBounds, }; } diff --git a/src/core/lib/utils.ts b/src/core/lib/utils.ts index 65acd1c4..4e4d94ea 100644 --- a/src/core/lib/utils.ts +++ b/src/core/lib/utils.ts @@ -253,6 +253,10 @@ export function boundLargeThanBound(bound1: Bound, bound2: Bound) { ); } +export function pointInBound(x: number, y: number, bound: Bound) { + return !(x < bound.x1 || x > bound.x2 || y < bound.y1 || y > bound.y2); +} + export function isBoundPositive(bound: Bound): boolean { return bound.x1 < bound.x2 && bound.y1 < bound.y2; }