javascript - 如何获取 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>
任何想法我做错了什么?
解决方案
注意:这只是 OP 自己已经找到的补充答案。
您实际上可以通过使用MouseEvent 构造函数来完成这一切。
您可以在构造函数中传递此事件的clientX
andclientY
属性(或者如果您愿意,也可以传递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>
推荐阅读
- spring - 为什么在 Spring 中只调用了一个 DatabaseConfig?
- ansible - 如何从ansible中的两个变量中获取非重复元素
- amazon-web-services - 使用角色时 AWS cdk 检索帐号
- python - 如何根据另一列中的值创建有序列(虽然不是累积计数)?
- python - Astropy - Ra, dec 到 AltAz 转换(以度为单位)
- typescript - Jest、TypeORM 和 typescript 无法运行测试
- javascript - Django如何制作一个首先运行js函数然后在.py中运行的按钮
- python - 如何将烧瓶 HTML 表单数据传递到单独的 .py 脚本中,然后在同一 HTML 页面上显示回来
- azure - 如何使用 PowerShell 将标签添加到现有的 Azure 资源
- java - 你如何检查时间(小时)是否等于?