or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

animation.mdcore-zrender.mdevents.mdgraphics-primitives.mdindex.mdshapes.mdstyling.mdtext-images.mdutilities.md

events.mddocs/

0

# Event Handling

1

2

ZRender provides a comprehensive event system for handling mouse, touch, and custom events. The event system supports hit testing, event delegation, interaction handling, and custom event types with full event lifecycle management.

3

4

## Event System Architecture

5

6

ZRender's event system is built on several key components:

7

- **Event capture and bubbling** through the element hierarchy

8

- **Hit testing** for determining which elements receive events

9

- **Event delegation** for efficient event management

10

- **Custom event support** for application-specific interactions

11

12

## Core Event Interfaces

13

14

### Element Event Methods

15

16

All graphics elements inherit event handling capabilities:

17

18

```typescript { .api }

19

interface Element {

20

// Event binding

21

on<Ctx>(eventName: ElementEventName, eventHandler: ElementEventCallback<Ctx>, context?: Ctx): this;

22

on<Ctx>(eventName: string, eventHandler: Function, context?: Ctx): this;

23

24

// Event unbinding

25

off(eventName?: string, eventHandler?: Function): void;

26

27

// Event triggering

28

trigger(eventName: string, event?: any): void;

29

30

// Event state

31

silent: boolean; // Disable all events on this element

32

ignore: boolean; // Ignore in hit testing

33

}

34

```

35

36

### ZRender Instance Event Methods

37

38

The main ZRender instance provides global event handling:

39

40

```typescript { .api }

41

interface ZRender {

42

// Global event binding

43

on<Ctx>(eventName: ElementEventName, eventHandler: ElementEventCallback<Ctx>, context?: Ctx): this;

44

on<Ctx>(eventName: string, eventHandler: Function, context?: Ctx): this;

45

46

// Global event unbinding

47

off(eventName?: string, eventHandler?: Function): void;

48

49

// Manual event triggering

50

trigger(eventName: string, event?: unknown): void;

51

52

// Hit testing

53

findHover(x: number, y: number): { target: Displayable; topTarget: Displayable } | undefined;

54

55

// Cursor management

56

setCursorStyle(cursorStyle: string): void;

57

}

58

```

59

60

## Event Types

61

62

### Mouse Events

63

64

```typescript { .api }

65

type ElementEventName =

66

| 'click'

67

| 'dblclick'

68

| 'mousedown'

69

| 'mouseup'

70

| 'mousemove'

71

| 'mouseover'

72

| 'mouseout'

73

| 'mouseenter'

74

| 'mouseleave'

75

| 'contextmenu';

76

```

77

78

### Touch Events

79

80

```typescript { .api }

81

type TouchEventName =

82

| 'touchstart'

83

| 'touchmove'

84

| 'touchend'

85

| 'touchcancel';

86

```

87

88

### Drag Events

89

90

```typescript { .api }

91

type DragEventName =

92

| 'drag'

93

| 'dragstart'

94

| 'dragend'

95

| 'dragenter'

96

| 'dragleave'

97

| 'dragover'

98

| 'drop';

99

```

100

101

### Global Events

102

103

```typescript { .api }

104

type GlobalEventName =

105

| 'rendered' // Fired after rendering frame

106

| 'finished' // Fired when animation finishes

107

| 'frame'; // Fired on each animation frame

108

```

109

110

## Event Objects

111

112

### Base Event Interface

113

114

```typescript { .api }

115

interface ElementEvent {

116

type: string;

117

event: MouseEvent | TouchEvent | PointerEvent;

118

target: Element;

119

topTarget: Element;

120

cancelBubble: boolean;

121

offsetX: number;

122

offsetY: number;

123

gestureEvent?: GestureEvent;

124

pinchX?: number;

125

pinchY?: number;

126

pinchScale?: number;

127

wheelDelta?: number;

128

zrByTouch?: boolean;

129

which?: number;

130

stop(): void;

131

}

132

133

interface ElementEventCallback<Ctx = any, Impl = any> {

134

(this: CbThis$1<Ctx, Impl>, e: ElementEvent): boolean | void;

135

}

136

```

137

138

### Rendered Event

139

140

```typescript { .api }

141

interface RenderedEvent {

142

elapsedTime: number; // Time taken to render frame in milliseconds

143

}

144

```

145

146

## Usage Examples

147

148

### Basic Event Handling

149

150

```typescript

151

import { Circle, Rect } from "zrender";

152

153

const circle = new Circle({

154

shape: { cx: 150, cy: 150, r: 50 },

155

style: { fill: '#74b9ff', cursor: 'pointer' }

156

});

157

158

// Basic click handler

159

circle.on('click', (e) => {

160

console.log('Circle clicked!', e);

161

console.log('Click position:', e.offsetX, e.offsetY);

162

});

163

164

// Mouse enter/leave for hover effects

165

circle.on('mouseenter', (e) => {

166

console.log('Mouse entered circle');

167

circle.animate('style')

168

.when(200, { fill: '#a29bfe' })

169

.start();

170

});

171

172

circle.on('mouseleave', (e) => {

173

console.log('Mouse left circle');

174

circle.animate('style')

175

.when(200, { fill: '#74b9ff' })

176

.start();

177

});

178

179

// Double click handler

180

circle.on('dblclick', (e) => {

181

console.log('Double clicked!');

182

circle.animate('scale')

183

.when(300, [1.5, 1.5])

184

.when(600, [1, 1])

185

.start('easeOutBounce');

186

});

187

188

zr.add(circle);

189

```

190

191

### Advanced Event Handling

192

193

```typescript

194

import { Rect, Group } from "zrender";

195

196

// Event delegation using groups

197

const buttonGroup = new Group();

198

199

const createButton = (text: string, x: number, y: number, color: string) => {

200

const button = new Group({

201

position: [x, y]

202

});

203

204

const bg = new Rect({

205

shape: { x: 0, y: 0, width: 100, height: 40, r: 5 },

206

style: { fill: color }

207

});

208

209

const label = new Text({

210

style: {

211

text: text,

212

fill: '#ffffff',

213

fontSize: 14,

214

textAlign: 'center',

215

textVerticalAlign: 'middle'

216

},

217

position: [50, 20]

218

});

219

220

button.add(bg);

221

button.add(label);

222

223

// Store button data

224

button.buttonData = { text, color };

225

226

return button;

227

};

228

229

// Create buttons

230

const button1 = createButton('Button 1', 50, 50, '#e17055');

231

const button2 = createButton('Button 2', 170, 50, '#00b894');

232

const button3 = createButton('Button 3', 290, 50, '#fdcb6e');

233

234

buttonGroup.add(button1);

235

buttonGroup.add(button2);

236

buttonGroup.add(button3);

237

238

// Handle events on the group level

239

buttonGroup.on('click', (e) => {

240

// Find which button was clicked

241

let clickedButton = e.target;

242

while (clickedButton && !clickedButton.buttonData) {

243

clickedButton = clickedButton.parent;

244

}

245

246

if (clickedButton && clickedButton.buttonData) {

247

console.log('Clicked button:', clickedButton.buttonData.text);

248

249

// Visual feedback

250

const bg = clickedButton.children()[0];

251

bg.animate('style')

252

.when(100, { fill: '#2d3436' })

253

.when(200, { fill: clickedButton.buttonData.color })

254

.start();

255

}

256

});

257

258

zr.add(buttonGroup);

259

```

260

261

### Drag and Drop

262

263

```typescript

264

import { Circle, Rect } from "zrender";

265

266

// Draggable circle

267

const draggableCircle = new Circle({

268

shape: { cx: 200, cy: 200, r: 30 },

269

style: { fill: '#ff7675', cursor: 'move' },

270

draggable: true

271

});

272

273

let isDragging = false;

274

let dragOffset = { x: 0, y: 0 };

275

276

draggableCircle.on('mousedown', (e) => {

277

isDragging = true;

278

const pos = draggableCircle.position || [0, 0];

279

dragOffset.x = e.offsetX - pos[0];

280

dragOffset.y = e.offsetY - pos[1];

281

282

// Visual feedback

283

draggableCircle.animate('style')

284

.when(100, { shadowBlur: 10, shadowColor: '#ff7675' })

285

.start();

286

});

287

288

// Use global mouse events for smooth dragging

289

zr.on('mousemove', (e) => {

290

if (isDragging) {

291

const newX = e.offsetX - dragOffset.x;

292

const newY = e.offsetY - dragOffset.y;

293

draggableCircle.position = [newX, newY];

294

zr.refresh();

295

}

296

});

297

298

zr.on('mouseup', (e) => {

299

if (isDragging) {

300

isDragging = false;

301

302

// Remove visual feedback

303

draggableCircle.animate('style')

304

.when(100, { shadowBlur: 0 })

305

.start();

306

}

307

});

308

309

// Drop zone

310

const dropZone = new Rect({

311

shape: { x: 400, y: 150, width: 100, height: 100 },

312

style: {

313

fill: 'none',

314

stroke: '#ddd',

315

lineWidth: 2,

316

lineDash: [5, 5]

317

}

318

});

319

320

// Drop zone events

321

dropZone.on('dragover', (e) => {

322

dropZone.style.stroke = '#00b894';

323

zr.refresh();

324

});

325

326

dropZone.on('dragleave', (e) => {

327

dropZone.style.stroke = '#ddd';

328

zr.refresh();

329

});

330

331

dropZone.on('drop', (e) => {

332

console.log('Dropped on zone!');

333

dropZone.style.fill = 'rgba(0, 184, 148, 0.1)';

334

dropZone.style.stroke = '#00b894';

335

zr.refresh();

336

});

337

338

zr.add(draggableCircle);

339

zr.add(dropZone);

340

```

341

342

### Touch Events

343

344

```typescript

345

import { Circle } from "zrender";

346

347

const touchCircle = new Circle({

348

shape: { cx: 150, cy: 350, r: 40 },

349

style: { fill: '#fd79a8' }

350

});

351

352

// Touch event handling

353

touchCircle.on('touchstart', (e) => {

354

console.log('Touch started');

355

touchCircle.animate('scale')

356

.when(100, [0.9, 0.9])

357

.start();

358

});

359

360

touchCircle.on('touchend', (e) => {

361

console.log('Touch ended');

362

touchCircle.animate('scale')

363

.when(100, [1, 1])

364

.start();

365

});

366

367

touchCircle.on('touchmove', (e) => {

368

// Handle touch move

369

const touch = e.event.touches[0];

370

if (touch) {

371

const rect = zr.dom.getBoundingClientRect();

372

const x = touch.clientX - rect.left;

373

const y = touch.clientY - rect.top;

374

touchCircle.position = [x, y];

375

zr.refresh();

376

}

377

});

378

379

zr.add(touchCircle);

380

```

381

382

### Custom Events

383

384

```typescript

385

import { Group, Rect, Text } from "zrender";

386

387

// Custom component with custom events

388

class CustomButton extends Group {

389

constructor(options: { text: string, x: number, y: number }) {

390

super({ position: [options.x, options.y] });

391

392

this.background = new Rect({

393

shape: { x: 0, y: 0, width: 120, height: 40, r: 5 },

394

style: { fill: '#74b9ff' }

395

});

396

397

this.label = new Text({

398

style: {

399

text: options.text,

400

fill: '#ffffff',

401

fontSize: 14,

402

textAlign: 'center'

403

},

404

position: [60, 20]

405

});

406

407

this.add(this.background);

408

this.add(this.label);

409

410

this.setupEvents();

411

}

412

413

private setupEvents() {

414

this.on('click', () => {

415

// Trigger custom event

416

this.trigger('buttonClick', {

417

button: this,

418

text: this.label.style.text

419

});

420

});

421

422

this.on('mouseenter', () => {

423

this.trigger('buttonHover', { button: this, hovered: true });

424

});

425

426

this.on('mouseleave', () => {

427

this.trigger('buttonHover', { button: this, hovered: false });

428

});

429

}

430

}

431

432

// Create custom button

433

const customButton = new CustomButton({ text: 'Custom', x: 50, y: 400 });

434

435

// Listen to custom events

436

customButton.on('buttonClick', (e) => {

437

console.log('Custom button clicked:', e.text);

438

alert(`Button "${e.text}" was clicked!`);

439

});

440

441

customButton.on('buttonHover', (e) => {

442

const color = e.hovered ? '#a29bfe' : '#74b9ff';

443

e.button.background.animate('style')

444

.when(150, { fill: color })

445

.start();

446

});

447

448

zr.add(customButton);

449

```

450

451

### Event Bubbling and Propagation

452

453

```typescript

454

import { Group, Circle, Rect } from "zrender";

455

456

// Nested elements for testing event bubbling

457

const container = new Group({ position: [300, 300] });

458

459

const outerRect = new Rect({

460

shape: { x: 0, y: 0, width: 200, height: 150 },

461

style: { fill: 'rgba(116, 185, 255, 0.3)', stroke: '#74b9ff' }

462

});

463

464

const innerCircle = new Circle({

465

shape: { cx: 100, cy: 75, r: 30 },

466

style: { fill: '#e17055' }

467

});

468

469

container.add(outerRect);

470

container.add(innerCircle);

471

472

// Event handlers with bubbling

473

outerRect.on('click', (e) => {

474

console.log('Outer rect clicked');

475

// Event will bubble up to container

476

});

477

478

innerCircle.on('click', (e) => {

479

console.log('Inner circle clicked');

480

481

// Stop event bubbling

482

if (e.event.ctrlKey) {

483

e.cancelBubble = true;

484

console.log('Event bubbling stopped');

485

}

486

});

487

488

container.on('click', (e) => {

489

console.log('Container clicked (bubbled up)');

490

});

491

492

zr.add(container);

493

```

494

495

### Global Event Handling

496

497

```typescript

498

// Global event handlers

499

zr.on('click', (e) => {

500

console.log('Global click at:', e.offsetX, e.offsetY);

501

});

502

503

zr.on('rendered', (e: RenderedEvent) => {

504

console.log('Frame rendered in:', e.elapsedTime, 'ms');

505

});

506

507

// Performance monitoring

508

zr.on('rendered', (e: RenderedEvent) => {

509

if (e.elapsedTime > 16.67) { // > 60fps threshold

510

console.warn('Slow frame detected:', e.elapsedTime, 'ms');

511

}

512

});

513

514

// Global keyboard events (requires DOM focus)

515

document.addEventListener('keydown', (e) => {

516

switch(e.key) {

517

case 'Escape':

518

// Clear selections, cancel operations, etc.

519

console.log('Escape pressed - clearing state');

520

break;

521

case 'Delete':

522

// Delete selected elements

523

console.log('Delete pressed');

524

break;

525

}

526

});

527

```

528

529

### Event Cleanup and Memory Management

530

531

```typescript

532

import { Circle } from "zrender";

533

534

// Proper event cleanup

535

class ManagedElement {

536

private element: Circle;

537

private handlers: { [key: string]: Function } = {};

538

539

constructor() {

540

this.element = new Circle({

541

shape: { cx: 100, cy: 100, r: 30 },

542

style: { fill: '#00cec9' }

543

});

544

545

this.setupEvents();

546

}

547

548

private setupEvents() {

549

// Store handler references for cleanup

550

this.handlers.click = (e: any) => {

551

console.log('Managed element clicked');

552

};

553

554

this.handlers.hover = (e: any) => {

555

this.element.animate('style')

556

.when(200, { fill: '#55efc4' })

557

.start();

558

};

559

560

this.handlers.leave = (e: any) => {

561

this.element.animate('style')

562

.when(200, { fill: '#00cec9' })

563

.start();

564

};

565

566

// Bind events

567

this.element.on('click', this.handlers.click);

568

this.element.on('mouseenter', this.handlers.hover);

569

this.element.on('mouseleave', this.handlers.leave);

570

}

571

572

public dispose() {

573

// Clean up event handlers

574

this.element.off('click', this.handlers.click);

575

this.element.off('mouseenter', this.handlers.hover);

576

this.element.off('mouseleave', this.handlers.leave);

577

578

// Remove from scene

579

if (this.element.parent) {

580

this.element.parent.remove(this.element);

581

}

582

583

// Clear references

584

this.handlers = {};

585

}

586

587

public getElement() {

588

return this.element;

589

}

590

}

591

592

// Usage

593

const managedEl = new ManagedElement();

594

zr.add(managedEl.getElement());

595

596

// Later, clean up properly

597

// managedEl.dispose();

598

```

599

600

### Event Performance Optimization

601

602

```typescript

603

// Efficient event handling for many elements

604

const createOptimizedEventHandling = () => {

605

const container = new Group();

606

const elements: Circle[] = [];

607

608

// Create many elements

609

for (let i = 0; i < 100; i++) {

610

const circle = new Circle({

611

shape: {

612

cx: (i % 10) * 50 + 25,

613

cy: Math.floor(i / 10) * 50 + 25,

614

r: 20

615

},

616

style: { fill: `hsl(${i * 3.6}, 70%, 60%)` }

617

});

618

619

// Store identifier

620

circle.elementId = i;

621

elements.push(circle);

622

container.add(circle);

623

}

624

625

// Use single event handler on container instead of individual handlers

626

container.on('click', (e) => {

627

// Find clicked element

628

let target = e.target;

629

while (target && target.elementId === undefined) {

630

target = target.parent;

631

}

632

633

if (target && target.elementId !== undefined) {

634

console.log('Clicked element:', target.elementId);

635

636

// Apply effect

637

target.animate('scale')

638

.when(200, [1.2, 1.2])

639

.when(400, [1, 1])

640

.start();

641

}

642

});

643

644

return container;

645

};

646

647

const optimizedGroup = createOptimizedEventHandling();

648

zr.add(optimizedGroup);

649

```