9

是否有标准的 Javascript 技术或库来管理不相关的重叠 HTML 元素之间的条件鼠标事件传递?

例如,我在一堆 HTML 元素(由 Thee.js CSS3 渲染器管理,但这不应该是相关的)前面有一个部分透明的 WebGL 画布(由 Three.js 管理)。这些 HTML 元素已注册 mouseover 和 mouseout 事件。我想要漂浮在这些元素前面的 3D 对象来阻止鼠标事件。

我已经知道如何使用光线投射器来确定鼠标是否在 3D 对象上。我不知道的是,当 3D 对象不在中间时,如何允许鼠标事件“通过”画布到达底层 HTML 元素。

在此处输入图像描述

我已经阅读了有关遍历 DOM 树直到找到鼠标下方的元素的解决方案。但这似乎过于复杂和缓慢。如果可能的话,我想做的是假装画布暂时不存在,以便事件可以自然地通过。

为了不重新发明轮子,如果已经有一个库,那就太好了。

4

3 回答 3

7

由于您不想像 David Thomas 建议的那样使用指针事件,因此鼠标将始终定位具有最高 z-index 的元素,或者具有相同 z-index 的最后一个兄弟元素(相对堆叠在其他)。

话虽如此,我能想到的唯一方法是:

  1. 隐藏画布
  2. 读取下面的元素
  3. 立即显示画布

这是如此之快,不会产生明显的闪烁。

如果鼠标下方有一个元素,则触发该元素鼠标事件。

演示在这里

$("canvas").mousemove(function (event) {
    if (document.elementFromPoint) {
        var $canvas = $(this);

        // hide canvas visibility
        // don't do display:none as we want to maintain canvas layout
        $canvas.css('visibility', 'hidden');

        // get the element underneath, if any
        var $under = $(document.elementFromPoint(event.clientX, event.clientY));

        // show again the canvas
        $canvas.css('visibility', 'visible');
        if ($under.hasClass('underneath')) { 
           $under.triggerHandler('mouseover');
        } else {
            $("#result").text("mouse is over the canvas");
        }
    }
})
于 2014-06-15T20:28:02.527 回答
1

我为您找到了一个非常简单的解决方案。而且由于您在您的一个评论中提供了一个 jsfiddle 示例,我想我只是稍微调整一下该代码并让您看到它。

我在做什么很简单。我检查鼠标是否与 div 重叠,并且当您的 Three.js raycaster 没有击中任何框时调用函数“changeTheDivBox”。

var divBox = document.getElementById("background");
divBox.addEventListener("mouseover", mouseOver, false);
divBox.addEventListener('mouseout', mouseOut, false);
var mouseOverBox = false; // Are you currently hovering over the "background" div?
function mouseOver () {
    mouseOverBox = true;
}
function mouseOut () {
    mouseOverBox = false;
}
function changeTheDivBox () { // This function is called when your raycaster does not hit any boxes
    if ( mouseOverBox ) {
        divBox.style.backgroundColor = 'blue';
    }
    else {
        divBox.style.backgroundColor = 'green';
    }
}

基本上:

Is the mouse overlapping the <div> ?
    Yes.
    Is the mouse (raycaster) hitting any boxes?
    No.
        Then Change the <div>'s color!

我做的另一件事是使用 css 属性pointer-events。这使您可以通过使任何选定的元素不注册鼠标事件来“进一步”单击 div。我添加pointer-events: none;body禁用所有鼠标事件,然后添加pointer-events: auto;到 div 元素以重新启用它们。所以鼠标现在只检测那个 div 上的事件。漂亮整齐。

我保留了其余的代码。我个人更喜欢使用循环来不断检查重叠/光线投射是否仍然有效,而不是依赖于 mouseIn / mouseOutSo 但这是你的例子,所以玩它:)

更新的 JSFIDDLE

只是作为个人喜好。我建议您避免将函数放在 HTML 中。它们并不总是按预期工作。this对象一直引用窗口)它们会使您的代码更加混乱。我个人更喜欢将我的 JavaScript 保留在标签内。而且 eventListeners 比内联函数调用更强大:)

于 2014-06-15T21:38:42.277 回答
0

您在代码中设置z-index:-1;#backgrounddisplay:absolute;. 这意味着:它位于body z 层后面,鼠标事件将针对 body 或上面一层中的任何其他内容。

您可以设置pointer-events:none;主体甚至所有元素,但这可能是一件坏事,因为稍后将其他代码包含在站点中。更喜欢将其他覆盖元素绝对定位为具有更高的 z-index,它们是:#container#container>canvas

查看路径是如何传播 DOM 事件的:http: //www.w3.org/TR/DOM-Level-3-Events/

在捕获阶段,事件首先传播到视图,然后传播到 html 和正文,然后它会到达鼠标光标下显示的目标。比在冒泡阶段传播采取相反的方式回到视图。

渲染的场景对象不在此 DOM 事件路径中。您已经在文档事件侦听器中对命中进行了光线追踪。在传播到达#background 元素之前执行此操作,并且如果已知要处理该事件,则在某处是安全的。你试过了preventDefault()。不幸的是 mousemove 事件是不可取消的,所以这没有效果。稍后调用事件侦听器,检查preventedDefault将获得false. 的含义是防止用户代理传播preventDefault()执行的默认操作,例如双击后在鼠标指针下标记单词。如果应该执行默认操作,那么您不能用来告诉其他侦听器该事件已被处理。preventDefault()

您可以将 Event 的自定义属性设置为 true。它至少在 Firefox 中有效,但是,W3C 并未严格指定主机对象。这可能会导致其他浏览器或将来出现一些错误。stopPropagation()如果不应调用其他侦听器,则可以使用。在某些情况下,这可能会导致与某些 JavaScript 框架结合使用的不良行为或错误。

另一种方法是在所有事件侦听器都可以访问的范围内设置一个变量,例如全局范围或更好的封装匿名函数。这可以保存最新处理的事件,并且在传播完成之前不会改变(下一个事件之前不会被触发)。

如果光线追踪器没有击中场景对象,您还可以在文档的侦听器中处理以#background为目标的事件。Event.target保存鼠标事件命中的最顶层 DOM 元素。

您修改后的代码演示了不同的方法:

<!DOCTYPE html>
<html>
  <head>
    <title>events passthrough</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style type="text/css">
body {
  font-family: Monospace;
  margin: 0px;
  overflow: hidden;
}

div#background {
  position: absolute;
  top: 40px;
  left: 40px;
  width: 100px;
  height: 100px;
  background-color: pink;
  /*z-index: -1;            /* this is behind the body */
}


#container>canvas
{ position: absolute;
  z-index: 100;
}

/* Explicitly disable mouse events on covering element. */
/* If z-index of background object is below zero then also disable body */

/* body, */
#container, #container canvas
{ pointer-events:none;
}

/* but let enabled all other elements */
*
{ pointer-events:auto;
}
    </style>
  </head>

  <body>
    <script type="text/javascript" src="https://mrdoob.github.io/three.js/build/three.min.js"></script>
    <script type="text/javascript" src="https://mrdoob.github.io/three.js/examples/js/libs/stats.min.js"></script>

    <div id="background"></div>
    <div id="container"></div>

    <script type="text/javascript">

var lastHandledEvent;

var container, stats;
var camera, scene, projector, renderer;
var particleMaterial;

var objects = [];

// don't run DOM relevant scripts before construction of DOM is guaranteed
document.addEventListener("DOMContentLoaded", function()
{
  init();
  animate();
}, false);

function init() {
    container = document.getElementById( 'container' );
    camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 1, 10000 );
    camera.position.set( 10, 300, 500 );
    scene = new THREE.Scene();
    var geometry = new THREE.BoxGeometry( 100, 100, 100 );
    for ( var i = 0; i < 10; i ++ ) {
        var object = new THREE.Mesh( geometry, new THREE.MeshBasicMaterial( { color: Math.random() * 0xffffff, opacity: 0.5 } ) );
        object.position.x = Math.random() * 800 - 400;
        object.position.y = Math.random() * 800 - 400;
        object.position.z = Math.random() * 800 - 400;
        object.scale.x = Math.random() * 2 + 1;
        object.scale.y = Math.random() * 2 + 1;
        object.scale.z = Math.random() * 2 + 1;
        object.rotation.x = Math.random() * 2 * Math.PI;
        object.rotation.y = Math.random() * 2 * Math.PI;
        object.rotation.z = Math.random() * 2 * Math.PI;
        scene.add( object );
        objects.push( object );
    }

    var PI2 = Math.PI * 2;
    particleMaterial = new THREE.SpriteCanvasMaterial( {
        color: 0x000000,
        program: function ( context ) {
            context.beginPath();
            context.arc( 0, 0, 0.5, 0, PI2, true );
            context.fill();
        }
    } );
    projector = new THREE.Projector();
    renderer = new THREE.WebGLRenderer({ alpha: true });
    renderer.setSize( window.innerWidth, window.innerHeight );
    // renderer.domElement.style.position = 'absolute';  // is done in CSS
    // renderer.domElement.style.zIndex = 100;           // is done in CSS
    container.appendChild( renderer.domElement );

    // register document mousemove in capturing phase
    // if you want to use another handlers subsequently
    document.addEventListener('mousemove', onDocumentMouseMove, true );
    document.querySelector('#background').addEventListener('mousemove', onMouseMove, false);

    window.addEventListener( 'resize', onWindowResize, false );
}

function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize( window.innerWidth, window.innerHeight );
}

var background = document.getElementById('background');

function onDocumentMouseMove( event ) 
{
    var vector = new THREE.Vector3( ( event.clientX / window.innerWidth ) * 2 - 1, - ( event.clientY / window.innerHeight ) * 2 + 1, 0.5 );
    projector.unprojectVector( vector, camera );
    var raycaster = new THREE.Raycaster( camera.position, vector.sub( camera.position ).normalize() );
    var intersects = raycaster.intersectObjects( objects );
    if ( intersects.length > 0 ) 
    {
        intersects[ 0 ].object.material.color.setHex( Math.random() * 0xffffff );
        //event.preventDefault();   // mousemove is not cancelable

        // extending event objects works at least in Firefox, not sure if crossbrowser combatible
        // always be careful when extending DOM
        event.handled = true;       // add custom property
        lastHandledEvent = event;   // another way: store in a variable accessible by all handlers
                                    //              to remember this one was the last handled event
        console.log('event marked as handled in document listener');
        //event.stopPropagation();  // or could stop propagation, however,
                                    // often not a good praxis in conjuction with frameworks
    }
    else if(event.target === background)
      console.log('background could be handled in document listener');
}

function onMouseMove( event )
{ 
  // if(event.defaultPrevented   // mousemove not cancable, always false
  if(event.handled)              // TODO: check crossbrowser compatibility
                                 //       of custom properties on event objects.
                                 //       In doubt use a var outside the functions.
    console.log('other listener: event.handled is: '+event.handled);

  if(lastHandledEvent === event) // the safer way: use variable accessible
  {
    console.log('NOT handling event in other listener');
    return;
  }
  console.log(event.target, '...or handled in other listener');
  event.target.style.backgroundColor = '#'+('00000' + (Math.random() * 0xffffff).toString(16)).slice(-6);
}

function animate() {
    requestAnimationFrame( animate );
    render();
}

var radius = 600;
var theta = 0;

function render() {
    theta += 0.1;
    camera.position.x = radius * Math.sin( THREE.Math.degToRad( theta ) );
    camera.position.y = radius * Math.sin( THREE.Math.degToRad( theta ) );
    camera.position.z = radius * Math.cos( THREE.Math.degToRad( theta ) );
    camera.lookAt( scene.position );
    renderer.render( scene, camera );
}
    </script>    
  </body>
</html>
于 2014-06-22T05:30:06.950 回答