【问题标题】:How to get a canvas relative mouse position of a CSS 3D transformed canvas?如何获取 CSS 3D 转换画布的画布相对鼠标位置?
【发布时间】:2019-09-03 09:32:03
【问题描述】:

只是为了好玩,我正在尝试在 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>

任何想法我做错了什么?

【问题讨论】:

  • 混合 CSS 转换和 Canvas 的坏主意...使用适当的 3D 框架
  • 有了一个好的框架,你会获得更多的乐趣,很多“平凡的东西”都被抽象出来了
  • 知道了,所以你知道自己的方式......我会开始将示例简化为不那么具有威胁性的东西,300行代码很可怕
  • 有趣。没有时间真正深入检查会发生什么(不确定是否可以),但有两个小观察。 IIRC getBoundingClientRect 在 3d 转换元素上返回一个奇怪的东西,这可能与您的问题相关,也可能不相关。当你找到元素的 .offsetParent 时,你会想要停止迭代,这现在不是问题,但将来可能会成为问题。
  • 哦,这不是问题所要求的,但是如果您想要一个hack-around,您可以在您的绘图区域上放置一个HTML元素网格作为事件侦听器。 Cwidth Cheight 元素,可以是 1px x 高度或 1px x 宽度,然后检查哪些是悬停的。实际上,您检查过DOMPoint API 吗?我自己从未使用过它,但在这里可能会有所帮助。

标签: javascript html css canvas css-transforms


【解决方案1】:

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

您实际上可以使用MouseEvent constructor 来完成这一切。

您可以在构造函数中传递此事件的clientX 和clientY 属性(或pageX 和pageY,如果您愿意),然后将此组合事件分派给您的目标将设置其offsetX 和offsetY 相对于目标的属性。

由于 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>

【讨论】:

  • 谢谢!我想知道我是否可以发送一个事件,但没有尝试。我也不知道document.elementFromPoint
  • @gman 我不知道 evt.offsetX&Y 在您发布答案之前会处理 3D 变换。
【解决方案2】:

叹息 ... 还不是最终答案,但显然 event.offsetX 和 event.offsetY 应该是这个值,即使 according to MDN they are not standard yet 也是如此。

测试它似乎在 Chrome 和 Firefox 中都有效。尽管在某些测试中 Safari 已关闭。不幸的是,触摸事件中不存在 offsetX 和 offsetY 。它们确实存在于指针事件中,但自 2019/05 起,Safari 不支持指针事件

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

  canvas.addEventListener('mousemove', (e) => {
    const pos = {
      x: e.offsetX * ctx.canvas.width / ctx.canvas.clientWidth,
      y: e.offsetY * ctx.canvas.height / ctx.canvas.clientHeight,
    };
    ctx.fillStyle = hsl((count++ % 10) / 10, 1, 0.5);
    ctx.fillRect(pos.x - 1, pos.y - 1, 3, 3);
  });
});

function hsl(h, s, l) {
  return `hsl(${h * 360 | 0},${s * 100 | 0}%,${l * 100 | 0}%)`;
}
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);  /* changed */
  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>

不幸的是,我们仍然存在有时我们想要一个事件之外的画布相对位置的问题。在下面的示例中,即使鼠标指针没有移动,我们也希望继续在鼠标指针下方绘图。

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

  function draw(e, radius = 1) {
    const pos = {
      x: e.offsetX * ctx.canvas.width / ctx.canvas.clientWidth,
      y: e.offsetY * ctx.canvas.height / ctx.canvas.clientHeight,
    };
    document.querySelector('#debug').textContent = count;
    ctx.beginPath();
    ctx.arc(pos.x, pos.y, radius, 0, Math.PI * 2);
    ctx.fillStyle = hsl((count++ % 100) / 100, 1, 0.5);
    ctx.fill();
  }

  function preventDefault(e) {
    e.preventDefault();
  }

  if (window.PointerEvent) {
    canvas.addEventListener('pointermove', (e) => {
      draw(e, Math.max(Math.max(e.width, e.height) / 2, 1));
    });
    canvas.addEventListener('touchstart', preventDefault, {passive: false});
    canvas.addEventListener('touchmove', preventDefault, {passive: false});
  } else {
    canvas.addEventListener('mousemove', draw);
    canvas.addEventListener('mousedown', preventDefault);
  }
});

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;
}

@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); }
<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>

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-02-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-01-20
    • 2010-11-10
    相关资源
    最近更新 更多