首页 > 解决方案 > 如何获取 CSS 3D 转换画布的画布相对鼠标位置?

问题描述

只是为了好玩,我试图在 3D 转换的画布上绘画。我写了一些代码,它有点工作

const m4 = twgl.m4;

[...document.querySelectorAll('canvas')].forEach((canvas) => {
  const ctx = canvas.getContext('2d');
  let count = 0;

  canvas.addEventListener('mousemove', (e) => {
    const pos = getElementRelativeMousePosition(e, canvas);
    ctx.fillStyle = hsl((count++ % 10) / 10, 1, 0.5);
    ctx.fillRect(pos.x - 1, pos.y - 1, 3, 3);
  });
});

function getElementRelativeMousePosition(e, elem) {
  const pos = convertPointFromPageToNode(elem, e.pageX, e.pageY); 
  
  return {
    x: pos[0],
    y: pos[1],
  };
}

function hsl(h, s, l) {
  return `hsl(${h * 360 | 0},${s * 100 | 0}%,${l * 100 | 0}%)`;
}

function convertPointFromPageToNode(elem, pageX, pageY) {
  const mat = m4.inverse(getTransformationMatrix(elem));
  return m4.transformPoint(mat, [pageX, pageY, 0]);
};

function getTransformationMatrix(elem) {
  let matrix = m4.identity();
  let currentElem = elem;

  while (currentElem !== undefined && 
         currentElem !== currentElem.ownerDocument.documentElement) {
    const style = window.getComputedStyle(currentElem);
    const localMatrix = parseMatrix(style.transform);
    matrix = m4.multiply(localMatrix, matrix);
    currentElem = currentElem.parentElement;
  }

  const w = elem.offsetWidth;
  const h = elem.offsetHeight;
  let i = 4;
  let left = +Infinity;
  let top = +Infinity;
  for (let i = 0; i < 4; ++i) {
    const p = m4.transformPoint(matrix, [w * (i & 1), h * ((i & 2) >> 1), 0]);
    left = Math.min(p[0], left);
    top = Math.min(p[1], top);
  }
  const rect = elem.getBoundingClientRect()
  document.querySelector('p').textContent =
    `${w}x${h}`;
  matrix =  m4.multiply(m4.translation([
     window.pageXOffset + rect.left - left, 
     window.pageYOffset + rect.top - top,
     0]), matrix);
  return matrix;
}


function parseMatrix(str) {
  if (str.startsWith('matrix3d(')) {
    return str.substring(9, str.length - 1).split(',').map(v => parseFloat(v.trim()));
  } else if (str.startsWith('matrix(')) {
    const m = str.substring(7, str.length - 1).split(',').map(v => parseFloat(v.trim()));
    return [
      m[0], m[1], 0, 0,
      m[2], m[3], 0, 0,
      0, 0, 1, 0,
      m[4], m[5], 0, 1,
    ]
  } else if (str == 'none') {
    return m4.identity();
  }
  throw new Error('unknown format');
}
canvas { 
  display: block;
  background: yellow;
  transform: scale(0.75);
}
#c1 {
  margin: 20px;
  background: red;
  transform: translateX(-50px);
  display: inline-block;
}
#c2 {
  margin: 20px;
  background: green;
  transform: rotate(45deg);
  display: inline-block;
}
#c3 {
  margin: 20px;
  background: blue;
  display: inline-block;
}

#c4 {
  position: absolute;
  top: 0;
  background: cyan;
  transform: translateX(-250px) rotate(55deg);
  display: inline-block;
}
#c5 {
  background: magenta;
  transform: translate(50px);
  display: inline-block;
}
#c6 {
  background: pink;
  transform: rotate(45deg);
  display: inline-block;
}
<p>
foo
</p>
<div id="c1">
  <div id="c2">
    <div id="c3">
      <canvas></canvas>
    </div>
  </div>
</div>
<div id="c4">
  <div id="c5">
    <div id="c6">
      <canvas></canvas>
    </div>
  </div>
</div>
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>

上面的代码有效。将鼠标移到任一黄色画布元素上,您将看到它正确绘制。

但是,一旦我添加了一些 3D 变换,它就会失败。

将“#c6”的 CSS 更改为

    #c6 {
      background: pink;
      transform: rotate(45deg) rotateX(45deg);  /* changed */
      display: inline-block;
    }

现在当我在右边的黄色画布上画画时,一切都关闭了。

const m4 = twgl.m4;

[...document.querySelectorAll('canvas')].forEach((canvas) => {
  const ctx = canvas.getContext('2d');
  let count = 0;

  canvas.addEventListener('mousemove', (e) => {
    const pos = getElementRelativeMousePosition(e, canvas);
    ctx.fillStyle = hsl((count++ % 10) / 10, 1, 0.5);
    ctx.fillRect(pos.x - 1, pos.y - 1, 3, 3);
  });
});

function getElementRelativeMousePosition(e, elem) {
  const pos = convertPointFromPageToNode(elem, e.pageX, e.pageY); 
  
  return {
    x: pos[0],
    y: pos[1],
  };
}

function hsl(h, s, l) {
  return `hsl(${h * 360 | 0},${s * 100 | 0}%,${l * 100 | 0}%)`;
}

function convertPointFromPageToNode(elem, pageX, pageY) {
  const mat = m4.inverse(getTransformationMatrix(elem));
  return m4.transformPoint(mat, [pageX, pageY, 0]);
};

function getTransformationMatrix(elem) {
  let matrix = m4.identity();
  let currentElem = elem;

  while (currentElem !== undefined && 
         currentElem !== currentElem.ownerDocument.documentElement) {
    const style = window.getComputedStyle(currentElem);
    const localMatrix = parseMatrix(style.transform);
    matrix = m4.multiply(localMatrix, matrix);
    currentElem = currentElem.parentElement;
  }

  const w = elem.offsetWidth;
  const h = elem.offsetHeight;
  let i = 4;
  let left = +Infinity;
  let top = +Infinity;
  for (let i = 0; i < 4; ++i) {
    const p = m4.transformPoint(matrix, [w * (i & 1), h * ((i & 2) >> 1), 0]);
    left = Math.min(p[0], left);
    top = Math.min(p[1], top);
  }
  const rect = elem.getBoundingClientRect()
  document.querySelector('p').textContent =
    `${w}x${h}`;
  matrix =  m4.multiply(m4.translation([
     window.pageXOffset + rect.left - left, 
     window.pageYOffset + rect.top - top,
     0]), matrix);
  return matrix;
}


function parseMatrix(str) {
  if (str.startsWith('matrix3d(')) {
    return str.substring(9, str.length - 1).split(',').map(v => parseFloat(v.trim()));
  } else if (str.startsWith('matrix(')) {
    const m = str.substring(7, str.length - 1).split(',').map(v => parseFloat(v.trim()));
    return [
      m[0], m[1], 0, 0,
      m[2], m[3], 0, 0,
      0, 0, 1, 0,
      m[4], m[5], 0, 1,
    ]
  } else if (str == 'none') {
    return m4.identity();
  }
  throw new Error('unknown format');
}
canvas { 
  display: block;
  background: yellow;
  transform: scale(0.75);
}
#c1 {
  margin: 20px;
  background: red;
  transform: translateX(-50px);
  display: inline-block;
}
#c2 {
  margin: 20px;
  background: green;
  transform: rotate(45deg);
  display: inline-block;
}
#c3 {
  margin: 20px;
  background: blue;
  display: inline-block;
}

#c4 {
  position: absolute;
  top: 0;
  background: cyan;
  transform: translateX(-250px) rotate(55deg);
  display: inline-block;
}
#c5 {
  background: magenta;
  transform: translate(50px);
  display: inline-block;
}
#c6 {
  background: pink;
  transform: rotate(45deg) rotateX(45deg);
  display: inline-block;
}
<p>
foo
</p>
<div id="c1">
  <div id="c2">
    <div id="c3">
      <canvas></canvas>
    </div>
  </div>
</div>
<div id="c4">
  <div id="c5">
    <div id="c6">
      <canvas></canvas>
    </div>
  </div>
</div>
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>

任何想法我做错了什么?

标签: javascripthtmlcsscanvascss-transforms

解决方案


注意:这只是 OP 自己已经找到的补充答案。

您实际上可以通过使用MouseEvent 构造函数来完成这一切。

您可以在构造函数中传递此事件的clientXandclientY属性(或者如果您愿意,也可以传递pageX& ),然后将这个组合事件分派到您的目标将设置其和相对于目标的属性。pageYoffsetXoffsetY

由于 dispatchEvent 确实会同步触发 Event,我们甚至可以制作一个转换器:

const init_pos = { x: 50, y: 50};
const relative_pos = {};
const canvas = document.querySelector('canvas');

canvas.addEventListener('mousemove', e => {
  relative_pos.x = e.offsetX;
  relative_pos.y = e.offsetY;
}, {once: true});

canvas.dispatchEvent(new MouseEvent('mousemove', {
  clientX: init_pos.x,
  clientY: init_pos.y
}));
// synchronously log
console.log(relative_pos);
canvas { 
  display: block;
  background: yellow;
  transform: scale(0.75);
}
#c4 {
  position: absolute;
  top: 0;
  background: cyan;
  transform: translateX(-250px) rotate(55deg);
  display: inline-block;
}
#c5 {
  background: magenta;
  transform: translate(50px);
  display: inline-block;
}
#c6 {
  background: pink;
  transform: rotate(45deg);
  display: inline-block;
}
<div id="c4">
  <div id="c5">
    <div id="c6">
      <canvas></canvas>
    </div>
  </div>
</div>

现在,给定您自己答案中的示例,您可能希望实际持有一个对象,该对象将保持全局事件的位置,并在requestAnimationFrame循环中的每一帧处获取画布的相对位置。
但是,此设置显然会遍历您的画布,如果您只希望可见的面来处理事件,那么您必须检查哪个匹配document.elementFromPoint(x, y),它本身需要您的元素对指针事件做出反应。

// will hold our last event's position
const pos = {
  x: 0,
  y: 0
};
const canvases = document.querySelectorAll('canvas');
// A single global "real" MouseEvent handler
document.body.onmousemove = (e) => {
  pos.x = e.clientX;
  pos.y = e.clientY;
};

canvases.forEach(canvas => {
  const ctx = canvas.getContext('2d');
  let count = 0;
  canvas.addEventListener('mousemove', draw);
  function draw(e) {
    // do not fire on real Events
    if (e.cancelable) return;
    
    const x = e.offsetX * canvas.width / canvas.clientWidth;
    const y = e.offsetY * canvas.height / canvas.clientHeight;
    
    if (x < 0 || x > canvas.width || y < 0 || y > canvas.height) {
      return;
    }
    
    ctx.fillStyle = hsl((count++ % 10) / 10, 1, 0.5);
    ctx.fillRect(x - 1, y - 1, 3, 3);
  }
});

anim();

function anim() {
  requestAnimationFrame(anim);

  // in case we want to paint only on the front element
  const front_elem = single_face.checked && document.elementFromPoint(pos.x, pos.y);

  // at every frame
  canvases.forEach(c => {
    if (!front_elem || c === front_elem) {
      // force a composed event (synchronously, so we are still in rAF callback)
      c.dispatchEvent(
        new MouseEvent('mousemove', {
          clientX: pos.x,
          clientY: pos.y
        })
      );
    }
  });
}

function hsl(h, s, l) {
  return `hsl(${h * 360 | 0},${s * 100 | 0}%,${l * 100 | 0}%)`;
}
.scene {
  width: 200px;
  height: 200px;
  perspective: 600px;
}

.cube {
  width: 100%;
  height: 100%;
  position: relative;
  transform-style: preserve-3d;
  animation-duration: 16s;
  animation-name: rotate;
  animation-iteration-count: infinite;
  animation-timing-function: linear;
  pointer-events: none; /* no need for mouse events */
}

#single_face:checked+.scene .cube {
  pointer-events: all; /* except if we want to find out who is the front one */
}
label,#single_face {float: right}
@keyframes rotate {
  from {
    transform: translateZ(-100px) rotateX( 0deg) rotateY( 0deg);
  }
  to {
    transform: translateZ(-100px) rotateX(360deg) rotateY(720deg);
  }
}

.cube__face {
  position: absolute;
  width: 200px;
  height: 200px;
  display: block;
}

.cube__face--front {
  background: rgba(255, 0, 0, 0.2);
  transform: rotateY( 0deg) translateZ(100px);
}

.cube__face--right {
  background: rgba(0, 255, 0, 0.2);
  transform: rotateY( 90deg) translateZ(100px);
}

.cube__face--back {
  background: rgba(0, 0, 255, 0.2);
  transform: rotateY(180deg) translateZ(100px);
}

.cube__face--left {
  background: rgba(255, 255, 0, 0.2);
  transform: rotateY(-90deg) translateZ(100px);
}

.cube__face--top {
  background: rgba(0, 255, 255, 0.2);
  transform: rotateX( 90deg) translateZ(100px);
}

.cube__face--bottom {
  background: rgba(255, 0, 255, 0.2);
  transform: rotateX(-90deg) translateZ(100px);
}
<label>Draw on a single face</label><input type="checkbox" id="single_face">
<div class="scene">
  <div class="cube">
    <canvas class="cube__face cube__face--front"></canvas>
    <canvas class="cube__face cube__face--back"></canvas>
    <canvas class="cube__face cube__face--right"></canvas>
    <canvas class="cube__face cube__face--left"></canvas>
    <canvas class="cube__face cube__face--top"></canvas>
    <canvas class="cube__face cube__face--bottom"></canvas>
  </div>
</div>
<pre id="debug"></pre>


推荐阅读