javascript - OffScreenCanvas 和触摸事件
问题描述
我正在尝试为 OffScreenCanvas 使用新的 API。这个想法是移动所有关于为玩家绘制和更新数据的逻辑,但在主线程中保留一些其他逻辑,比如触摸事件(因为工作者无法访问窗口对象)。
所以我有课Player.js
export class Player {
constructor() {
this.width = 100;
this.height = 100;
this.posX = 0;
this.posY = 0;
}
draw = (ctx) => {
ctx.fillRect(this.posX, this.posY, this.width, this.height);
}
updatePos = (x, y) => {
this.posX = x;
this.posY = y;
}
}
我在另一个名为的模块中生成播放器实例playerObject.js
import { Player } from "./Player.js";
export const player = new Player();
OffScreenCanvas 是这样创建的
const canvas = document.querySelector('#myGame');
const offscreen = canvas.transferControlToOffscreen();
const worker = new Worker('offscreencanvas.js', { type: "module" });
worker.postMessage({ canvas: offscreen }, [offscreen]);
现在我将 playerObject 导入 OffScreenCanvas 工作者
import {player} from "./playerObject.js";
addEventListener('message', (evt) => {
const canvas = evt.data.canvas;
const ctx = canvas.getContext("2d");
const render = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
player.draw(ctx);
requestAnimationFrame(render);
}
render();
});
以及包含触摸事件的类(模块),这些事件正在修改玩家位置:
import {player} from "./playerObject.js";
export class Game {
constructor() {
this.touch();
}
touch = () => {
window.addEventListener('touchstart', (e) => {
player.updatePos(e.touches[0].clientX, e.touches[0].clientY);
}, {passive: true});
}
}
问题是 OffScreenCanvas 看不到 Game 类所做的更改。触摸本身正在工作(console.log 显示事件并且还修改了播放器对象)但在 OffScreenCanvas 播放器中仍然具有初始坐标。
我仍然不确定那里发生了什么。工人是否正在创建一个新的类实例,这就是为什么它看不到触摸事件的变化?
有没有办法做到这一点?
解决方案
您当前有两个不同的 Player 实例,它们不会与另一个进行通信。
草案中有一个提议允许将一些事件从主线程传递到工作线程,但它实际上仍然只是一个草案,我完全不确定它什么时候会出现,也不知道具体是什么形式。
目前,我们唯一的解决方案是在主线程和 Worker 线程之间搭建一座桥梁,让主线程在事件发生时将所有事件发送给 Worker。
这有很多缺点,其中
- 明显的延迟,因为我们必须等待主线程接收到事件,然后再调度一个新的 MessageEvent 任务,
- 对主线程的依赖:它必须可以自由地处理事件,这意味着即使主线程被锁定,OffscreenCanvas 也可以顺利运行的承诺在这里被打破。
- 难以维护(?)由于我们没有明确的 API 来访问我们正在获取的目标,因此我们必须在主线程和工作线程中使用难看的硬编码值
但是,我们仍然可以取得一些成就。
我只是有一些时间写了一个粗略的玩具,大致基于我链接到的当前提案,有一些更改,没有测试,所以用它作为编写你自己的基础,但不要指望它在各种情况下无缝工作。
基本逻辑是
- 我们从主线程增强了 Worker 接口,使其与 Worker 线程(通过 MessageChannel 对象)启动一个私有通信通道。
- 我们还
addEventTarget( target, uuid )
向该接口添加和方法。 - 当我们从主线程脚本接收到消息时,我们从 Worker 线程初始化一个接收器。从那里,我们持有 MessageChannel 并等待它说何时从主线程声明了 新的delegatedTargets 。
- 发生这种情况时,我们会触发一个新事件eventtargetadd,该事件在 Worker 中运行的用户脚本可以侦听,从而公开一个EventDelegate实例,该实例将在 Worker 和主线程之间创建一个新的私有通信通道。
- 通过这个 EventDelegate 的通道,主线程中的每个事件对象都将在被清理后被克隆。
但是足够多的单词和难以理解的解释,这里有一个 plnkr,它可能更清楚它是如何工作的。
这是 StackSnippet 的实时版本,可能有点难以阅读:
// StackSnippet only: build up internal path to our Worker scripts
const event_delegate_worker_script = document.getElementById( 'event-delegate-worker' ).textContent;
const event_delegate_worker_url = generateScriptURL( event_delegate_worker_script );
const user_script_worker_script = document.getElementById( 'user-script-worker' ).textContent
.replace( "event-delegate-worker.js", event_delegate_worker_url );
const user_script_worker_url = generateScriptURL( user_script_worker_script );
function generateScriptURL( content ) {
// Chrome refuses to importScripts blob:// URI...
return 'data:text/javascript,' + encodeURIComponent( content );
}
// end StackSnippets only
onload = evt => {
const worker = new EventDelegatingWorker( user_script_worker_url );
const canvas = document.getElementById( 'canvas' );
worker.addEventTarget( canvas, "canvas" );
try {
const off_canvas = canvas.transferControlToOffscreen();
worker.postMessage( off_canvas, [ off_canvas ] );
}
catch (e) {
// no support for OffscreenCanvas, we'll just log evt
worker.onmessage = (evt) => { console.log( "from worker", evt.data ); }
}
};
canvas { border: 1px solid; }
<canvas id="canvas"width="500" height="500"></canvas>
<script id="user-script-worker" type="worker-script">
importScripts( "event-delegate-worker.js" );
self.addEventListener( "eventtargetadded", ({ delegatedTarget }) => {
if( delegatedTarget.context === "canvas" ) {
delegatedTarget.addEventListener( "mousemove", handleMouseMove );
}
} );
let ctx;
function handleMouseMove( evt ) {
if( ctx ) {
draw( evt.offsetX, evt.offsetY );
}
else {
// so we can log for browsers without OffscreenCanvas
postMessage( evt );
}
}
function draw( x, y ) {
const rad = 30;
ctx.clearRect( 0, 0, ctx.canvas.width, ctx.canvas.height );
ctx.beginPath();
ctx.arc( x, y, rad, 0, Math.PI*2 );
ctx.fill();
}
onmessage = (evt) => {
const canvas = evt.data;
ctx = canvas.getContext("2d");
};
</script>
<!-- below are the two scripts required to bridge the events -->
<script id="event-delegate-main">
(()=> { "use strict";
const default_event_options_dict = {
capture: false,
passive: true
};
const event_keys_to_remove = new Set( [
"view",
"target",
"currentTarget"
] );
class EventDelegatingWorker extends Worker {
constructor( url, options ) {
super( url, options );
// this channel will be used to notify the Worker of added targets
const channel = new MessageChannel();
this._mainPort = channel.port2;
this.postMessage( "init-event-delegation", [ channel.port1 ] );
}
addEventTarget( event_target, context ) {
// this channel will be used to notify us when the Worker adds or removes listeners
// and to notify the worker of new events fired on the target
const channel = new MessageChannel();
channel.port1.onmessage = (evt) => {
const { type, action } = evt.data;
if( action === "add" ) {
event_target.addEventListener( type, handleDOMEvent, default_event_options_dict );
}
else if( action === "remove" ) {
event_target.removeEventListener( type, handleDOMEvent, default_event_options_dict );
}
};
// let the Worker side know they have a new target they can listen on
this._mainPort.postMessage( context, [ channel.port2 ] );
function handleDOMEvent( domEvent ) {
channel.port1.postMessage( sanitizeEvent( domEvent ) );
}
}
}
window.EventDelegatingWorker = EventDelegatingWorker;
// Events can not be cloned as is, so we need to stripe out all non cloneable properties
function sanitizeEvent( evt ) {
const copy = {};
// Most events only have .isTrusted as own property, so we use a for in loop to get all
// otherwise JSON.stringify() would just ignore them
for( let key in evt ) {
if( event_keys_to_remove.has( key ) ) {
continue;
}
copy[ key ] = evt[ key ];
}
const as_string = tryToStringify( copy );
return JSON.parse( as_string );
// over complicated recursive function to handle cross-origin access
function tryToStringify() {
const referenced_objects = new Set; // for cyclic
// for cross-origin objects (e.g window.parent in a cross-origin iframe)
// we save the previous key value so we can delete it if throwing
let lastKey;
let nextVal = copy;
let lastVal = copy;
try {
return JSON.stringify( copy, removeDOMRefsFunctionsAndCyclics );
}
catch( e ) {
delete lastVal[ lastKey ];
return tryToStringify();
}
function removeDOMRefsFunctionsAndCyclics( key, value ) {
lastVal = nextVal;
lastKey = key;
if( typeof value === "function" ) {
return;
}
if( typeof value === "string" || typeof value === "number") {
return value;
}
if( value && typeof value === "object" ) {
if( value instanceof Node ) {
return;
}
if( referenced_objects.has( value ) ) {
return "[cyclic]";
}
referenced_objects.add( value );
nextVal = value;
return value;
}
return value;
}
}
}
})();
</script>
<script id="event-delegate-worker" type="worker-script">
(()=> { "use strict";
// This script should be imported at the top of user's worker-script
function initDelegatedEventReceiver( evt ) {
// currently the only option is "once"
const defaultOptionsDict = {
once: false,
};
// in case it's not our message (which would be quite odd...)
if( evt.data !== "init-event-delegation" ) {
return;
}
// let's not let user-script know it happend
evt.stopImmediatePropagation();
removeEventListener( 'message', initDelegatedEventReceiver, true );
// this is where the main thread will let us know when a new target is available
const main_port = evt.ports[ 0 ];
class EventDelegate {
constructor( port, context ) {
this.port = port; // the port to communicate with main
this.context = context; // can help identify our target
this.callbacks = {}; // we'll store the added callbacks here
// this will fire when main thread fired an event on our target
port.onmessage = (evt) => {
const evt_object = evt.data;
const slot = this.callbacks[ evt_object.type ];
if( slot ) {
const to_remove = [];
slot.forEach( ({ callback, options }, index) => {
try {
callback( evt_object );
}
catch( e ) {
// we don't want to block our execution,
// but still, we should notify the exception
setTimeout( () => { throw e; } );
}
if( options.once ) {
to_remove.push( index );
}
} );
// remove 'once' events
to_remove.reverse().forEach( index => slot.splice( index, 1 ) );
}
};
}
addEventListener( type, callback, options = defaultOptionsDict ) {
const callbacks = this.callbacks;
let slot = callbacks[ type ];
if( !slot ) {
slot = callbacks[ type ] = [];
// make the main thread attach only a single event,
// we'll handle the multiple callbacks
// and since we force { passive: true, capture: false }
// they'll all get attached the same way there
this.port.postMessage( { type, action: "add" } );
}
// to store internally, and avoid duplicates (like EventTarget.addEventListener does)
const new_item = {
callback,
options,
options_as_string: stringifyOptions( options )
};
if( !getStoredItem( slot, new_item ) ) {
slot.push( new_item );
}
}
removeEventListener( type, callback, options = defaultOptionsDict ) {
const callbacks = this.callbacks;
const slot = callbacks[ type ];
const options_as_string = stringifyOptions( options );
const item = getStoredItem( slot, { callback, options, options_as_string } );
const index = item && slot.indexOf( item );
if( item ) {
slot.splice( index, 1 );
}
if( slot && !slot.length ) {
delete callbacks[ type ];
// we tell the main thread to remove the event handler
// only when there is no callbacks of this type anymore
this.port.postMessage( { type, action: "remove" } );
}
}
}
// EventInitOptions need to be serialized in a deterministic way
// so we can detect duplicates
function stringifyOptions( options ) {
if( typeof options === "boolean" ) {
options = { once: options };
}
try {
return JSON.stringify(
Object.fromEntries(
Object.entries(
options
).sort( byKeyAlpha )
)
);
}
catch( e ) {
return JSON.stringify( defaultOptionsDict );
}
}
function byKeyAlpha( entry_a, entry_b ) {
return entry_a[ 0 ].localeCompare( entry_b[ 0 ] );
}
// retrieves an event item in a slot based on its callback and its stringified options
function getStoredItem( slot, { callback, options_as_string } ) {
return Array.isArray( slot ) && slot.find( (obj) => {
return obj.callback === callback &&
obj.options_as_string === options_as_string;
} );
}
// a new EventTarget has been declared by main thread
main_port.onmessage = evt => {
const target_added_evt = new Event( 'eventtargetadded' );
target_added_evt.delegatedTarget = new EventDelegate( evt.ports[ 0 ], evt.data );
dispatchEvent( target_added_evt );
};
}
addEventListener( 'message', initDelegatedEventReceiver );
})();
</script>
Ps:既然这个答案已经发布,我确实开始实现一个EventPort 接口,松散地基于这个 PR,它可能更容易使用,并且更接近最终规范。
不过,在 Stackoverflow 中发布答案有点长。
您可以在此处查看实时示例。
推荐阅读
- php - 通过将标题列名称与另一个表中的行值匹配来获取值
- c# - 从 Outlook VSTO 中的 JunkFolder 修改“阻止的电子邮件发件人”列表
- powershell - 为什么 For-Each 循环在 PowerShell 中不起作用?
- python - 如何在 tensorflow.keras 模型指标中使用 sklearn AUC?
- nagios - 是否可以将服务的执行限制在特定的时间范围内?
- python - 如何从一些 python 脚本中调用函数,这些函数的名称可以在另一个 python 脚本中更改?
- python - 可以直接将 xpath 复制并粘贴到漂亮的汤解析器中,还是必须对其进行修改?
- php - Issue in using multiple databases in laravel 5.2
- javascript - 如何使用leaflet.js(附截图)实现以下地图功能?
- bash - 如何在 Linux 中循环访问变量值?