我正在尝试将新的API用于OffScreenCanvas。这个想法是为了移动有关绘制和更新播放器数据的所有逻辑,但在主线程中保留一些其他逻辑,例如触摸事件(因为工作程序无法到达窗口对象)。
所以我上了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 worker
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中,播放器仍具有初始坐标。
我仍然不确定那里发生了什么。工作人员是否正在创建类的新实例,这就是为什么它没有看到来自触摸事件的更改?
有没有办法做到这一点?
答案 0 :(得分:1)
您当前有两个不同的Player实例,它们不会彼此通信。
There is a proposal是用于允许将一些事件从主线程传递到Worker线程的粗略草稿,但它实际上仍然只是草稿,我不确定什么时候会到来,也不清楚完全成型。
当前,我们唯一的解决方案是在主线程和辅助线程之间建立自己的桥梁,以便主线程在发生事件时将所有事件发送给辅助线程。
这有很多缺点,其中
但是,我们仍然可以取得成就。
我只是花了一些时间根据我链接的当前提案编写了一个粗略的玩具,松散地,做了一些更改,没有进行测试,因此可以以此为基础来编写您自己的,但不要期望它在每种情况下都能无缝运行。
基本逻辑是
addEventTarget( target, uuid )
方法。here is a 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 interface,它是基于this PR的松散形式,它可能更易于使用,并且更接近最终规格。
不过要在Stackoverflow答案中发布有点长。
您可以看到live examples here。
答案 1 :(得分:0)
有些事情你需要知道:
import {player} from "./playerObject.js";
都会创建一个新的播放器实例。因此,它不会将OffscreenCanvasWorker模块与Game模块桥接。这是我的意思:
import { Player } from "./Player.js";
const player = new Player();
const canvas = document.querySelector('#myGame');
const offscreen = canvas.transferControlToOffscreen();
const worker = new Worker('offscreencanvas.js', { type: "module" });
worker.postMessage({
'canvas' : offscreen,
'player' : player
}, [offscreen]);
addEventListener('message', (evt) => {
const canvas = evt.data.canvas;
const player = evt.data.player;
const ctx = canvas.getContext("2d");
const render = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
player.draw(ctx);
requestAnimationFrame(render);
}
render();
});
export class Game {
constructor(player) {
this.player = player;
this.touch();
}
touch = () => {
const player = this.player;
window.addEventListener('touchstart', (e) => {
player.updatePos(e.touches[0].clientX, e.touches[0].clientY);
}, {passive: true});
}
}
如果您不喜欢这种方式,实际上有很多替代方法,但是最重要的是,您必须传输 玩家实例对象 ,而不是其类或新实例在模块中声明。
其他说明:万一您不知道,在2020年,OffscreenCanvas仍将处于试验阶段,如果将其放到直播网站上将不是一个好主意。祝你好运!