Events
Veyrajs dispatches DOM-style federated events through the scene graph: a pointer interaction
is hit-tested to a target node, then flows capture → target → bubble. One SceneEvent carries
the screen point, the world point, the target, and the current phase.
Event propagation
Section titled “Event propagation”Click the blue square and watch a single handler fire at every level, in both phases:
import { Group, Rect, type SceneEvent, Text } from '@veyrajs/core'import { button, createStage, disposeStage, onThemeChange, palette, roles, toolbar } from './_kit'
// One click handler is bound to layer/group/inner/outer in both capture and bubble phases.// Clicking the inner square shows the full DOM-style path: capture flows down (▼) to the// target (◉), then bubbles back up (▲).export function init(host: HTMLElement): () => void { const stage = createStage(host) const layer = stage.createLayer({ name: 'layer' })
const group = new Group({ name: 'group', x: 250, y: 20 }) const r0 = roles() const outer = new Rect({ name: 'outer', x: 0, y: 0, width: 260, height: 150, fill: r0.panelFill, stroke: r0.panelStroke, strokeWidth: 1, }) const inner = new Rect({ name: 'inner', x: 80, y: 45, width: 100, height: 60, fill: palette.blue, }) group.add(outer, inner) const caption = new Text({ x: 16, y: 14, text: 'layer ▸ group ▸ inner', fontSize: 13, fill: r0.muted, }) layer.add(group, caption) const off = onThemeChange(() => { const r = roles() outer.fill = r.panelFill outer.stroke = r.panelStroke caption.fill = r.muted })
// A scrollable log panel below the stage. const log = document.createElement('div') log.className = 'veyrajs-demo__log' host.parentElement?.append(log)
const bar = toolbar(host) bar.append( button('Clear log', () => { log.textContent = '' }), ) const hint = document.createElement('span') hint.className = 'veyrajs-demo__hint' hint.textContent = 'Click the blue square — capture flows down (▼), bubble flows up (▲)' bar.append(hint)
const cls: Record<string, string> = { capture: 'is-capture', target: 'is-target', bubble: 'is-bubble', } const arrow: Record<string, string> = { capture: '▼', target: '◉', bubble: '▲' } const append = (name: string, phase: string): void => { const line = document.createElement('div') line.innerHTML = `<span class="${cls[phase] ?? ''}">${arrow[phase] ?? '·'} <b>${name}</b> — ${phase}</span>` log.append(line) log.scrollTop = log.scrollHeight } const onClick = (e: SceneEvent): void => append(e.currentTarget.name ?? '?', e.eventPhase)
stage.on( 'click', () => { const line = document.createElement('div') line.innerHTML = '<span class="is-muted">— click —</span>' log.append(line) }, { capture: true }, ) layer.on('click', onClick, { capture: true }) layer.on('click', onClick) group.on('click', onClick, { capture: true }) group.on('click', onClick) outer.on('click', onClick) inner.on('click', onClick)
inner.on('pointerenter', () => { inner.fill = '#60a5fa' }) inner.on('pointerleave', () => { inner.fill = palette.blue })
return () => { off() disposeStage(stage) }}Hover affordances
Section titled “Hover affordances”pointerenter / pointerleave fire per shape and don’t bubble — ideal for outlining the shape
under the pointer and switching the cursor:
import { Circle, Rect } from '@veyrajs/core'import { createStage, cycle, disposeStage, onThemeChange, roles, toolbar } from './_kit'
// pointerenter / pointerleave fire per shape and do NOT bubble, which makes them ideal for// hover affordances: outline the shape under the pointer and switch the cursor.export function init(host: HTMLElement): () => void { const stage = createStage(host) const layer = stage.createLayer() const shapes = [ new Rect({ x: 70, y: 80, width: 150, height: 100, fill: cycle[0] }), new Circle({ x: 320, y: 130, radius: 62, fill: cycle[1] }), new Rect({ x: 440, y: 80, width: 150, height: 100, fill: cycle[2] }), ] let hovered: Rect | Circle | null = null for (const s of shapes) { layer.add(s) s.on('pointerenter', () => { hovered = s s.stroke = roles().ink s.strokeWidth = 3 host.style.cursor = 'pointer' }) s.on('pointerleave', () => { hovered = null s.stroke = null s.strokeWidth = 0 host.style.cursor = '' }) }
const bar = toolbar(host) const hint = document.createElement('span') hint.className = 'veyrajs-demo__hint' hint.textContent = 'Hover a shape — enter/leave do not bubble' bar.append(hint)
// Keep the hover outline legible if the theme flips mid-hover. const off = onThemeChange(() => { if (hovered) hovered.stroke = roles().ink }) return () => { off() disposeStage(stage) }}Pointer coordinates
Section titled “Pointer coordinates”Every event carries the pointer in three spaces — screen, world, and any node’s local space:
import { Rect } from '@veyrajs/core'import { createStage, disposeStage, palette, readout, stroke, toolbar } from './_kit'
// Every SceneEvent carries the pointer in three spaces: screen pixels, world coordinates, and// — via getLocalPoint(node) — the local space of any node. Move the pointer to watch all three.export function init(host: HTMLElement): () => void { const stage = createStage(host) const layer = stage.createLayer() const box = new Rect({ x: 200, y: 90, width: 200, height: 150, fill: palette.blue, stroke: stroke.blue, strokeWidth: 2, }) layer.add(box)
const bar = toolbar(host) const hint = document.createElement('span') hint.className = 'veyrajs-demo__hint' hint.textContent = 'Local coordinates are relative to the blue box' bar.append(hint) const out = readout(bar, 'move the pointer…')
stage.on('pointermove', (e) => { const s = e.screenPoint const w = e.worldPoint const l = e.getLocalPoint(box) out.textContent = `screen (${Math.round(s.x)}, ${Math.round(s.y)}) · world (${Math.round(w.x)}, ${Math.round(w.y)}) · box (${Math.round(l.x)}, ${Math.round(l.y)})` })
return () => disposeStage(stage)}