[[ Input for Workers and Worklets

In this specification we propose an event delegation scheme to workers that assumes no DOM access in workers. This proposal introduces a generic event delegation mechanism that in theory works for all DOM events. However to be practical we intend to first focus on enabling this for input events starting with PointerEvents. This addresses key use cases and gives us a chance to prove and perfect the model while also leaving the door open to enable delegation for more event types that can benefit from this capability.

Introduction

Today on the web platform only the main thread has access to the DOM elements and receives their related input events. In particular, web workers [[HTML]] and worklets do not have access to these. If we allow workers to receive input events for a given DOM node we enable many latency sensitive applications to leverage workers. As a result if developers want to do some event dependent logic like drawing on an off-screen canvas or using fetch in their worker thread they need to listen to those events on the main thread and postMessage them across to the worker thread. Here the worker is not only going to be blocked by the main thread to handle the events first but also suffers from the latency introduced by the extra thread hop. This is particularly unfortunate for the input events where the latency is important. The following sections outlines

Event delegation and Interfaces

EventDelegate Interface

EventDelegationOptions dictionary

The options dictionary to customize how the event delegation should behaves.

context member

Additional information that can be passed to the event delegate as a context for the delegated target.

EventDelegate interface

The interface that expose the event delegation capabitilities.

addEventTarget() member

The `addEventTarget(target, options)` adds target to the list of targets delegated to `EventDelegate`.

removeEventTarget() member

The `removeEventTarget(target)` removes target to the list of targets delegated to `EventDelegate`.

Extensions to Worker and WorkletAnimation

Event types

Below are the new event types that are dispatched when as a result of target event delegation.

The eventtargetadded event

A user agent MUST fire an event named eventtargetadded on an EventDelegate when a target is being delegated to it using addEventTarget().

The eventtargetremoved event

A user agent MUST fire an event named eventtargetremoved on an EventDelegate when a target is being removed from it using removeEventTarget().

Dispatching a PointerEvent to the EventDelegate

This section is a hook on dispatch event algorithm in [[!HTML]]. User agent muse clone the dispatching event and stip all DOM refrences from it. Then it MUST fire that event at the EventDelegate.

Examples

The following are example author code that demonstrates how the APIs in this specification might be used.


// DOM and the main thread javascript
<canvas id="canvas"></canvas>
var worker = new Worker("worker.js");
var canvas = document.getElementById("canvas")

var handler = canvas.transferControlToOffscreen();
worker.postMessage({canvas: handler}, [handler]);
worker.addEventTarget(canvas);


// worker.js
var context;

addEventListener("message", (msg) => {
  if (msg.data.canvas)
    context = msg.data.canvas.getContext("2d");
});

addEventListener("eventtargetadded", ({target}) => {
  target.addEventListener("pointermove", onPointerMove);
});

addEventListener("eventtargetremoved", ({target}) => {
  target.removeEventListener("pointermove", onPointerMove);
});

function onPointerMove(event){
  // Use event.clientX/Y or offsetX/Y to draw things on the context.
  context.beginPath();
  context.arc(event.offsetX, event.offsetY, 5, 0, 2.0* Math.PI, false);
  context.closePath();
  context.fill();
  context.commit();
}


// DOM and the main thread javascript
<img id='target'>

await CSS.animationWorklet.addModule('worklet.js');
const target = document.getElementById('target');

const rotateEffect = new KeyFrameEffect(
   target, {rotate: ['rotate(0)', 'rotate(360deg)']}, {duration: 100, fill: 'both' }
);
const scaleEffect = new KeyFrameEffect(
  target, {scale: [0, 100]}, {duration: 100, fill: 'both' }
);

// Note the worklet animation has no timeline since the animation is not time-based.
const animation = new WorkletAnimation('scale_and_rotate', [rotateEffect, scaleEffect],  null);
animation.play();

// Delegate pointer events for target to worklet animation.
animation.addEventTarget(target);


// worker.js

// Made up library that given pointer event sequence can compute an active gesture similar to Hammer.js
import {Recognizer} from 'gesture_recognizer.js'


registerAnimator('scale_and_rotate', class {
  constructor() {
    this.gestureRecognizer = new Recognizer();
    this.addEventListener("eventtargetadded", (event) => {
       for (type of ["pointerdown", "pointermove", "pointerup"])  {
          event.target.addEventListener(type, (e) => {
            this.gestureRecognizer.consumeEvent(e)));
          });
       }
   });
  }
  animate(currentTime, effects) {
    // Note that currentTime is undefined and unused.

    // Get current recognized gesture value and update rotation and scale effects accordingly.
    const gesture = this.gestureRecognizer.activeGesture;
    if (!gesture) return;
    effect.children[0].localTime = gesture.rotate * 100;
    effect.children[1].localTime = gesture.scale;
  }
});