mousemove引起的叠加问题
superposition issue caused by mousemove
我的团队正在开发时间轴应用程序,类似于 adobe premier pro 中的应用程序。代码长约250行,我遇到了一个奇怪的错误,我整天都在努力但未能解决。这是简要的体系结构:
Stamp
是单个时间轴中的块。
Channel
一个通道对应一个时序参考
Timeline
包含许多频道。
我想实现 left-right-drag
以更改 Stamp
的持续时间,其中:
- 向左拖动会改变开始时间
- 拖动结束会改变结束时间
因此,我将一个Stamp
元素分为三个部分:左手柄、主(中)元素和右手柄,它们包含在el
中
在left_handle
和right_handle
上实现mousedown
、mousemove
和mouseup
并不健壮,因为一旦光标飞出元素鼠标移动,mouseup
不会开火。因此,mousemove 和 mouseup 事件侦听器设置在 window
请看代码:
function render_element(styles, el) {
for (const [kk, vv] of Object.entries(styles)) {
el.style[kk] = vv;
}
}
const endListeners = {
'mousemove': [],
'mouseup': [],
};
function addMouseMove(func){
endListeners['mousemove'].push(func);
}
function addMouseUp(func){
endListeners['mouseup'].push(func);
}
class Stamp{
constructor(
host,
parent_el,
width_percent,
){
this.host = host;
this.parent_el = parent_el;
// percent to pixels
this.width = width_percent / 100 * window.innerWidth;
[
this.el,
this.left_handle,
this.main_el,
this.right_handle,
] = Array.from({length: 4}, ()=>document.createElement('div'));
render_element({
position: 'relative',
display: 'inline-block',
width: this.width.toString() + 'px',
height: '100%',
display: 'flex',
flexDirection: 'row',
border: '1px solid',
}, this.el);
render_element({
width: '10%',
height: '100%',
border: '1px solid',
}, this.left_handle);
render_element({
width: '80%',
height: '100%',
cursor: 'ew-resize',
border: '1px solid',
}, this.main_el);
render_element({
width: '10%',
height: '100%',
cursor: 'ew-resize',
border: '1px solid',
}, this.right_handle);
this.el.appendChild(this.left_handle);
this.el.appendChild(this.main_el);
this.el.appendChild(this.right_handle);
// indicator during movement
this.indicator = document.createElement('div');
render_element({
position: 'absolute',
width: '5px',
height: '100%',
background: 'grey',
display: 'none',
}, this.indicator);
this.el.appendChild(this.indicator);
// move
// mousedown start move
this.in_move = false;
this.in_left_move = false;
this.left_handle.addEventListener('mousedown', e=>{
this.startMove(e);
this.in_move = true;
this.in_left_move = true;
});
this.right_handle.addEventListener('mousedown', e=>{
this.startMove(e);
this.in_move = true;
this.in_left_move = false;
});
// mousemove
this.move = function(e){
if(!this.in_move)return;
this.moveIndicator(e);
if(this.in_left_move){
this.expandLeft(e);
}else{
this.expandRight(e);
}
}.bind(this);
addMouseMove(this.move);
// mouseend finish move
addMouseUp(function(e){
this.move(e);
this.endMove(e);
this.in_move = false;
}.bind(this));
this.parent_el.appendChild(this.el);
this.getHandleRects = this.getHandleRects.bind(this);
this.startMove = this.startMove.bind(this);
this.endMove = this.endMove.bind(this);
this.expandLeft = this.expandLeft.bind(this);
this.expandRight = this.expandRight.bind(this);
this.moveIndicator = this.moveIndicator.bind(this);
}
getHandleRects(){
return [
this.el.getBoundingClientRect(),
this.left_handle.getBoundingClientRect(),
this.right_handle.getBoundingClientRect(),
]
}
startMove(e){
// show indicator
this.indicator.style.display = 'block';
this.moveIndicator(e);
// change color
this.el.style.background = 'lightblue';
}
endMove(){
// hide indicator
this.indicator.style.display = 'none';
// change color back
this.el.style.background = 'none';
}
expandLeft(e){
var [el_rect, left_rect, right_rect] = this.getHandleRects();
if(e.clientX >= right_rect.left)return;
let dif = el_rect.left - e.clientX;
this.el.style.width = (el_rect.width + dif).toString() + 'px';
this.el.style.marginLeft = (el_rect.left - dif).toString() + 'px';
}
expandRight(e){
var [el_rect, left_rect, right_rect] = this.getHandleRects();
if(e.clientX <= left_rect.right)return;
this.el.style.width = (e.clientX - el_rect.left).toString() + 'px';
}
moveIndicator(e){
this.indicator.style.marginLeft = e.clientX.toString() + 'px';
}
}
class Channel{
constructor(
host,
parent_el,
height_percent,
){
this.host = host;
this.parent_el = parent_el;
this.height = height_percent;
this.stamps = [];
this.el = document.createElement('div');
render_element({
position: 'relative',
width: '100%',
height: this.height.toString() + '%',
// display: 'flex',
// flexDirection: 'row',
border: '1px solid',
}, this.el);
this.stamp_indicator = document.createElement('div');
this.parent_el.appendChild(this.el);
this.addStamp = this.addStamp.bind(this);
}
addStamp(width_percent){
this.stamps.push(new Stamp(this, this.el, width_percent));
return this.stamps[this.stamps.length - 1];
}
}
class Timeline{
constructor(
parent_el,
frame_rate,
){
this.parent_el = parent_el;
this.frame_rate = frame_rate;
this.el = document.createElement('div');
render_element({
width: '100%',
height: '100%',
background: 'lightgrey',
}, this.el);
this.channels = [];
this.parent_el.appendChild(this.el);
this.el.droppable = true;
this.el.addEventListener('dragover', e=>{
e.preventDefault();
});
this.el.addEventListener('drop', e=>{
e.preventDefault();
console.log(e.dataTransfer.getData('text/plain'));
});
this.addChanel = this.addChannel.bind(this);
}
addChannel(height_percent){
this.channels.push(new Channel(this, this.el, height_percent));
return this.channels[this.channels.length - 1];
}
}
var tl = new Timeline(
document.querySelector('#timeline'),
2,
);
var c1 = tl.addChannel(33);
var s1 = c1.addStamp(20);
// if I call it explicitly, it works fine
s1.expandLeft({
clientX: 50,
});
// endListeners exec at end
for (const [kk, vv] of Object.entries(endListeners)) {
window.addEventListener(kk, e=>{vv.forEach(v=>v(e))});
}
#timeline{
position: fixed;
left: 0;
bottom: 0;
width: 100vw;
height: 300px;
border: 1px solid;
}
<div id="timeline"></div>
请注意第138~144行,expandLeft
以一个事件为参数,改变Channel
(设置开始时间)中的Stamp
位置,通过改变两个css 属性:
- 左边距
- 宽度
非常奇怪的是:
- 当拖动右手柄时,它的工作完全符合我的预期。
- 当调用
expandLeft
时,它也工作正常。 (这是在脚本末尾测试的)
- 但是当我将它附加到
mousemove
侦听器时,宽度似乎是累积的。
如果这个问题让您感到困惑,我们深表歉意。
如果你能帮助我,我会很高兴。
更新
即使我画了很多图表来确定,我的计算也没有发现任何问题。
我观察到一个有趣的现象:
- 在左手柄上快速拖动时,宽度变化不大
- 在左手柄上慢速拖动时,宽度会以递增的速度扩大。
- 尽管有鼠标移动距离,但宽度始终以与时间成正比的恒定速率扩展。
这是由于精度损失还是我在计算中忽略了什么?
添加此测试代码后:
s1.expandRight({
clientX: 800,
});
setTimeout(()=>{
s1.expandLeft({
clientX: 200,
});
setTimeout(()=>{
s1.expandLeft({
clientX: 400,
});
setTimeout(()=>{
s1.expandLeft({
clientX: 600,
});
}, 500);
}, 500);
}, 500);
我可以看到右边的宽度在扩大。一定有一个计算我误解或忽略了。
现在我完全糊涂了,我把getBoundingClientRect().width
赋给了css宽度属性,这应该不会改变宽度,因为我把宽度赋给了自己!但是宽度一直在增加!!
expandLeft(e){
var [el_rect, left_rect, right_rect] = this.getHandleRects();
if(e.clientX >= right_rect.left)return;
let dif = el_rect.left - e.clientX;
// this.el.style.width = (el_rect.width + dif).toString() + 'px';
this.el.style.width = (el_rect.width).toString() + 'px';
this.el.style.marginLeft = e.clientX.toString() + 'px';
}
两行结果相同,拖动左手柄时宽度会扩大:
this.el.style.width = (el_rect.width).toString() + 'px'
this.el.style.width = (this.el.offsetWidth).toString() + 'px';
如果你能解释为什么会这样,我将非常高兴
您需要考虑向左扩展时的偏移量:
this.el.style.width = (el_rect.width + dif).toString() + 'px';
对比
this.el.style.width = (el_rect.width + dif - el_rect.offsetLeft).toString() + 'px';
function render_element(styles, el) {
for (const [kk, vv] of Object.entries(styles)) {
el.style[kk] = vv;
}
}
const endListeners = {
'mousemove': [],
'mouseup': [],
};
function addMouseMove(func){
endListeners['mousemove'].push(func);
}
function addMouseUp(func){
endListeners['mouseup'].push(func);
}
class Stamp{
constructor(
host,
parent_el,
width_percent,
){
this.host = host;
this.parent_el = parent_el;
// percent to pixels
this.width = width_percent / 100 * window.innerWidth;
[
this.el,
this.left_handle,
this.main_el,
this.right_handle,
] = Array.from({length: 4}, ()=>document.createElement('div'));
render_element({
position: 'relative',
display: 'inline-block',
width: this.width.toString() + 'px',
height: '100%',
display: 'flex',
flexDirection: 'row',
border: '1px solid',
}, this.el);
render_element({
width: '10%',
height: '100%',
border: '1px solid',
}, this.left_handle);
render_element({
width: '80%',
height: '100%',
cursor: 'ew-resize',
border: '1px solid',
}, this.main_el);
render_element({
width: '10%',
height: '100%',
cursor: 'ew-resize',
border: '1px solid',
}, this.right_handle);
this.el.appendChild(this.left_handle);
this.el.appendChild(this.main_el);
this.el.appendChild(this.right_handle);
// indicator during movement
this.indicator = document.createElement('div');
render_element({
position: 'absolute',
width: '5px',
height: '100%',
background: 'grey',
display: 'none',
}, this.indicator);
this.el.appendChild(this.indicator);
// move
// mousedown start move
this.in_move = false;
this.in_left_move = false;
this.left_handle.addEventListener('mousedown', e=>{
this.startMove(e);
this.in_move = true;
this.in_left_move = true;
});
this.right_handle.addEventListener('mousedown', e=>{
this.startMove(e);
this.in_move = true;
this.in_left_move = false;
});
// mousemove
this.move = function(e){
if(!this.in_move)return;
this.moveIndicator(e);
if(this.in_left_move){
this.expandLeft(e);
}else{
this.expandRight(e);
}
}.bind(this);
addMouseMove(this.move);
// mouseend finish move
addMouseUp(function(e){
this.move(e);
this.endMove(e);
this.in_move = false;
}.bind(this));
this.parent_el.appendChild(this.el);
this.getHandleRects = this.getHandleRects.bind(this);
this.startMove = this.startMove.bind(this);
this.endMove = this.endMove.bind(this);
this.expandLeft = this.expandLeft.bind(this);
this.expandRight = this.expandRight.bind(this);
this.moveIndicator = this.moveIndicator.bind(this);
}
getHandleRects(){
return [
this.el.getBoundingClientRect(),
this.left_handle.getBoundingClientRect(),
this.right_handle.getBoundingClientRect(),
]
}
startMove(e){
// show indicator
this.indicator.style.display = 'block';
this.moveIndicator(e);
// change color
this.el.style.background = 'lightblue';
}
endMove(){
// hide indicator
this.indicator.style.display = 'none';
// change color back
this.el.style.background = 'none';
}
expandLeft(e){
var [el_rect, left_rect, right_rect] = this.getHandleRects();
if(e.clientX >= right_rect.left)return;
let dif = el_rect.left - e.clientX;
this.el.style.width = (el_rect.width + dif - el_rect.offsetLeft).toString() + 'px';
this.el.style.marginLeft = (el_rect.left - dif).toString() + 'px';
}
expandRight(e){
var [el_rect, left_rect, right_rect] = this.getHandleRects();
if(e.clientX <= left_rect.right)return;
this.el.style.width = (e.clientX - el_rect.left).toString() + 'px';
}
moveIndicator(e){
this.indicator.style.marginLeft = e.clientX.toString() + 'px';
}
}
class Channel{
constructor(
host,
parent_el,
height_percent,
){
this.host = host;
this.parent_el = parent_el;
this.height = height_percent;
this.stamps = [];
this.el = document.createElement('div');
render_element({
position: 'relative',
width: '100%',
height: this.height.toString() + '%',
// display: 'flex',
// flexDirection: 'row',
border: '1px solid',
}, this.el);
this.stamp_indicator = document.createElement('div');
this.parent_el.appendChild(this.el);
this.addStamp = this.addStamp.bind(this);
}
addStamp(width_percent){
this.stamps.push(new Stamp(this, this.el, width_percent));
return this.stamps[this.stamps.length - 1];
}
}
class Timeline{
constructor(
parent_el,
frame_rate,
){
this.parent_el = parent_el;
this.frame_rate = frame_rate;
this.el = document.createElement('div');
render_element({
width: '100%',
height: '100%',
background: 'lightgrey',
}, this.el);
this.channels = [];
this.parent_el.appendChild(this.el);
this.el.droppable = true;
this.el.addEventListener('dragover', e=>{
e.preventDefault();
});
this.el.addEventListener('drop', e=>{
e.preventDefault();
console.log(e.dataTransfer.getData('text/plain'));
});
this.addChanel = this.addChannel.bind(this);
}
addChannel(height_percent){
this.channels.push(new Channel(this, this.el, height_percent));
return this.channels[this.channels.length - 1];
}
}
var tl = new Timeline(
document.querySelector('#timeline'),
2,
);
var c1 = tl.addChannel(33);
var s1 = c1.addStamp(20);
// if I call it explicitly, it works fine
s1.expandLeft({
clientX: 50,
});
// endListeners exec at end
for (const [kk, vv] of Object.entries(endListeners)) {
window.addEventListener(kk, e=>{vv.forEach(v=>v(e))});
}
#timeline{
position: fixed;
left: 0;
bottom: 0;
width: 100vw;
height: 300px;
border: 1px solid;
}
<div id="timeline"></div>
是的,我成功了!!通过存储鼠标移动前的左边距和宽度,并引用它而不是动态左边距和宽度。
function render_element(styles, el) {
for (const [kk, vv] of Object.entries(styles)) {
el.style[kk] = vv;
}
}
const endListeners = {
'mousemove': [],
'mouseup': [],
};
function addMouseMove(func){
endListeners['mousemove'].push(func);
}
function addMouseUp(func){
endListeners['mouseup'].push(func);
}
class Stamp{
constructor(
host,
parent_el,
width_percent,
){
this.host = host;
this.parent_el = parent_el;
// percent to pixels
this.width = width_percent / 100 * window.innerWidth;
[
this.el,
this.left_handle,
this.main_el,
this.right_handle,
] = Array.from({length: 4}, ()=>document.createElement('div'));
render_element({
position: 'relative',
display: 'inline-block',
width: this.width.toString() + 'px',
height: '100%',
display: 'flex',
flexDirection: 'row',
border: '1px solid',
}, this.el);
render_element({
width: '10%',
height: '100%',
border: '1px solid',
}, this.left_handle);
render_element({
width: '80%',
height: '100%',
cursor: 'ew-resize',
border: '1px solid',
}, this.main_el);
render_element({
width: '10%',
height: '100%',
cursor: 'ew-resize',
border: '1px solid',
}, this.right_handle);
this.el.appendChild(this.left_handle);
this.el.appendChild(this.main_el);
this.el.appendChild(this.right_handle);
// move
// mousedown start move
this.in_move = false;
this.in_left_move = false;
this.lwd = this.el.offsetWidth;
this.lft = this.el.offsetLeft;
this.left_handle.addEventListener('mousedown', function(e){
this.startMove(e);
this.in_move = true;
this.in_left_move = true;
this.lwd = this.el.offsetWidth;
this.lft = this.el.offsetLeft;
}.bind(this));
this.right_handle.addEventListener('mousedown', function(e){
this.startMove(e);
this.in_move = true;
this.in_left_move = false;
}.bind(this));
// mousemove
this.move = function(e){
if(!this.in_move)return;
if(this.in_left_move){
this.expandLeft(e);
}else{
this.expandRight(e);
}
}.bind(this);
addMouseMove(this.move);
// mouseend finish move
addMouseUp(function(e){
this.move(e);
this.endMove(e);
this.in_move = false;
}.bind(this));
this.parent_el.appendChild(this.el);
this.getHandleRects = this.getHandleRects.bind(this);
this.startMove = this.startMove.bind(this);
this.endMove = this.endMove.bind(this);
this.expandLeft = this.expandLeft.bind(this);
this.expandRight = this.expandRight.bind(this);
}
getHandleRects(){
return [
this.el.getBoundingClientRect(),
this.left_handle.getBoundingClientRect(),
this.right_handle.getBoundingClientRect(),
]
}
startMove(e){
// change color
this.el.style.background = 'lightblue';
}
endMove(){
// change color back
this.el.style.background = 'none';
}
expandLeft(e){
var [el_rect, left_rect, right_rect] = this.getHandleRects();
if(e.clientX >= right_rect.left)return;
let dif = this.lft - e.clientX;
this.el.style.width = (this.lwd + dif).toString() + 'px';
this.el.style.marginLeft = e.clientX.toString() + 'px';
}
expandRight(e){
var [el_rect, left_rect, right_rect] = this.getHandleRects();
if(e.clientX <= left_rect.right)return;
this.el.style.width = (e.clientX - el_rect.left).toString() + 'px';
}
}
class Channel{
constructor(
host,
parent_el,
height_percent,
){
this.host = host;
this.parent_el = parent_el;
this.height = height_percent;
this.stamps = [];
this.el = document.createElement('div');
render_element({
position: 'relative',
width: '100%',
height: this.height.toString() + '%',
// display: 'flex',
// flexDirection: 'row',
border: '1px solid',
}, this.el);
this.parent_el.appendChild(this.el);
this.addStamp = this.addStamp.bind(this);
}
addStamp(width_percent){
this.stamps.push(new Stamp(this, this.el, width_percent));
return this.stamps[this.stamps.length - 1];
}
}
class Timeline{
constructor(
parent_el,
frame_rate,
){
this.parent_el = parent_el;
this.frame_rate = frame_rate;
this.el = document.createElement('div');
render_element({
width: '100%',
height: '100%',
background: 'lightgrey',
}, this.el);
this.channels = [];
this.parent_el.appendChild(this.el);
this.el.droppable = true;
this.el.addEventListener('dragover', e=>{
e.preventDefault();
});
this.el.addEventListener('drop', e=>{
e.preventDefault();
console.log(e.dataTransfer.getData('text/plain'));
});
this.addChanel = this.addChannel.bind(this);
}
addChannel(height_percent){
this.channels.push(new Channel(this, this.el, height_percent));
return this.channels[this.channels.length - 1];
}
}
var tl = new Timeline(
document.querySelector('#timeline'),
2,
);
var c1 = tl.addChannel(33);
var s1 = c1.addStamp(20);
var c2 = tl.addChannel(33);
var s2 = c2.addStamp(30);
#timeline{
position: fixed;
left: 0;
bottom: 0;
width: 100vw;
height: 300px;
border: 1px solid;
}
<div id="timeline"></div>
我的团队正在开发时间轴应用程序,类似于 adobe premier pro 中的应用程序。代码长约250行,我遇到了一个奇怪的错误,我整天都在努力但未能解决。这是简要的体系结构:
Stamp
是单个时间轴中的块。Channel
一个通道对应一个时序参考Timeline
包含许多频道。
我想实现 left-right-drag
以更改 Stamp
的持续时间,其中:
- 向左拖动会改变开始时间
- 拖动结束会改变结束时间
因此,我将一个Stamp
元素分为三个部分:左手柄、主(中)元素和右手柄,它们包含在el
在left_handle
和right_handle
上实现mousedown
、mousemove
和mouseup
并不健壮,因为一旦光标飞出元素鼠标移动,mouseup
不会开火。因此,mousemove 和 mouseup 事件侦听器设置在 window
请看代码:
function render_element(styles, el) {
for (const [kk, vv] of Object.entries(styles)) {
el.style[kk] = vv;
}
}
const endListeners = {
'mousemove': [],
'mouseup': [],
};
function addMouseMove(func){
endListeners['mousemove'].push(func);
}
function addMouseUp(func){
endListeners['mouseup'].push(func);
}
class Stamp{
constructor(
host,
parent_el,
width_percent,
){
this.host = host;
this.parent_el = parent_el;
// percent to pixels
this.width = width_percent / 100 * window.innerWidth;
[
this.el,
this.left_handle,
this.main_el,
this.right_handle,
] = Array.from({length: 4}, ()=>document.createElement('div'));
render_element({
position: 'relative',
display: 'inline-block',
width: this.width.toString() + 'px',
height: '100%',
display: 'flex',
flexDirection: 'row',
border: '1px solid',
}, this.el);
render_element({
width: '10%',
height: '100%',
border: '1px solid',
}, this.left_handle);
render_element({
width: '80%',
height: '100%',
cursor: 'ew-resize',
border: '1px solid',
}, this.main_el);
render_element({
width: '10%',
height: '100%',
cursor: 'ew-resize',
border: '1px solid',
}, this.right_handle);
this.el.appendChild(this.left_handle);
this.el.appendChild(this.main_el);
this.el.appendChild(this.right_handle);
// indicator during movement
this.indicator = document.createElement('div');
render_element({
position: 'absolute',
width: '5px',
height: '100%',
background: 'grey',
display: 'none',
}, this.indicator);
this.el.appendChild(this.indicator);
// move
// mousedown start move
this.in_move = false;
this.in_left_move = false;
this.left_handle.addEventListener('mousedown', e=>{
this.startMove(e);
this.in_move = true;
this.in_left_move = true;
});
this.right_handle.addEventListener('mousedown', e=>{
this.startMove(e);
this.in_move = true;
this.in_left_move = false;
});
// mousemove
this.move = function(e){
if(!this.in_move)return;
this.moveIndicator(e);
if(this.in_left_move){
this.expandLeft(e);
}else{
this.expandRight(e);
}
}.bind(this);
addMouseMove(this.move);
// mouseend finish move
addMouseUp(function(e){
this.move(e);
this.endMove(e);
this.in_move = false;
}.bind(this));
this.parent_el.appendChild(this.el);
this.getHandleRects = this.getHandleRects.bind(this);
this.startMove = this.startMove.bind(this);
this.endMove = this.endMove.bind(this);
this.expandLeft = this.expandLeft.bind(this);
this.expandRight = this.expandRight.bind(this);
this.moveIndicator = this.moveIndicator.bind(this);
}
getHandleRects(){
return [
this.el.getBoundingClientRect(),
this.left_handle.getBoundingClientRect(),
this.right_handle.getBoundingClientRect(),
]
}
startMove(e){
// show indicator
this.indicator.style.display = 'block';
this.moveIndicator(e);
// change color
this.el.style.background = 'lightblue';
}
endMove(){
// hide indicator
this.indicator.style.display = 'none';
// change color back
this.el.style.background = 'none';
}
expandLeft(e){
var [el_rect, left_rect, right_rect] = this.getHandleRects();
if(e.clientX >= right_rect.left)return;
let dif = el_rect.left - e.clientX;
this.el.style.width = (el_rect.width + dif).toString() + 'px';
this.el.style.marginLeft = (el_rect.left - dif).toString() + 'px';
}
expandRight(e){
var [el_rect, left_rect, right_rect] = this.getHandleRects();
if(e.clientX <= left_rect.right)return;
this.el.style.width = (e.clientX - el_rect.left).toString() + 'px';
}
moveIndicator(e){
this.indicator.style.marginLeft = e.clientX.toString() + 'px';
}
}
class Channel{
constructor(
host,
parent_el,
height_percent,
){
this.host = host;
this.parent_el = parent_el;
this.height = height_percent;
this.stamps = [];
this.el = document.createElement('div');
render_element({
position: 'relative',
width: '100%',
height: this.height.toString() + '%',
// display: 'flex',
// flexDirection: 'row',
border: '1px solid',
}, this.el);
this.stamp_indicator = document.createElement('div');
this.parent_el.appendChild(this.el);
this.addStamp = this.addStamp.bind(this);
}
addStamp(width_percent){
this.stamps.push(new Stamp(this, this.el, width_percent));
return this.stamps[this.stamps.length - 1];
}
}
class Timeline{
constructor(
parent_el,
frame_rate,
){
this.parent_el = parent_el;
this.frame_rate = frame_rate;
this.el = document.createElement('div');
render_element({
width: '100%',
height: '100%',
background: 'lightgrey',
}, this.el);
this.channels = [];
this.parent_el.appendChild(this.el);
this.el.droppable = true;
this.el.addEventListener('dragover', e=>{
e.preventDefault();
});
this.el.addEventListener('drop', e=>{
e.preventDefault();
console.log(e.dataTransfer.getData('text/plain'));
});
this.addChanel = this.addChannel.bind(this);
}
addChannel(height_percent){
this.channels.push(new Channel(this, this.el, height_percent));
return this.channels[this.channels.length - 1];
}
}
var tl = new Timeline(
document.querySelector('#timeline'),
2,
);
var c1 = tl.addChannel(33);
var s1 = c1.addStamp(20);
// if I call it explicitly, it works fine
s1.expandLeft({
clientX: 50,
});
// endListeners exec at end
for (const [kk, vv] of Object.entries(endListeners)) {
window.addEventListener(kk, e=>{vv.forEach(v=>v(e))});
}
#timeline{
position: fixed;
left: 0;
bottom: 0;
width: 100vw;
height: 300px;
border: 1px solid;
}
<div id="timeline"></div>
请注意第138~144行,expandLeft
以一个事件为参数,改变Channel
(设置开始时间)中的Stamp
位置,通过改变两个css 属性:
- 左边距
- 宽度
非常奇怪的是:
- 当拖动右手柄时,它的工作完全符合我的预期。
- 当调用
expandLeft
时,它也工作正常。 (这是在脚本末尾测试的) - 但是当我将它附加到
mousemove
侦听器时,宽度似乎是累积的。
如果这个问题让您感到困惑,我们深表歉意。 如果你能帮助我,我会很高兴。
更新
即使我画了很多图表来确定,我的计算也没有发现任何问题。
我观察到一个有趣的现象:
- 在左手柄上快速拖动时,宽度变化不大
- 在左手柄上慢速拖动时,宽度会以递增的速度扩大。
- 尽管有鼠标移动距离,但宽度始终以与时间成正比的恒定速率扩展。
这是由于精度损失还是我在计算中忽略了什么?
添加此测试代码后:
s1.expandRight({
clientX: 800,
});
setTimeout(()=>{
s1.expandLeft({
clientX: 200,
});
setTimeout(()=>{
s1.expandLeft({
clientX: 400,
});
setTimeout(()=>{
s1.expandLeft({
clientX: 600,
});
}, 500);
}, 500);
}, 500);
我可以看到右边的宽度在扩大。一定有一个计算我误解或忽略了。
现在我完全糊涂了,我把getBoundingClientRect().width
赋给了css宽度属性,这应该不会改变宽度,因为我把宽度赋给了自己!但是宽度一直在增加!!
expandLeft(e){
var [el_rect, left_rect, right_rect] = this.getHandleRects();
if(e.clientX >= right_rect.left)return;
let dif = el_rect.left - e.clientX;
// this.el.style.width = (el_rect.width + dif).toString() + 'px';
this.el.style.width = (el_rect.width).toString() + 'px';
this.el.style.marginLeft = e.clientX.toString() + 'px';
}
两行结果相同,拖动左手柄时宽度会扩大:
this.el.style.width = (el_rect.width).toString() + 'px'
this.el.style.width = (this.el.offsetWidth).toString() + 'px';
如果你能解释为什么会这样,我将非常高兴
您需要考虑向左扩展时的偏移量:
this.el.style.width = (el_rect.width + dif).toString() + 'px';
对比
this.el.style.width = (el_rect.width + dif - el_rect.offsetLeft).toString() + 'px';
function render_element(styles, el) {
for (const [kk, vv] of Object.entries(styles)) {
el.style[kk] = vv;
}
}
const endListeners = {
'mousemove': [],
'mouseup': [],
};
function addMouseMove(func){
endListeners['mousemove'].push(func);
}
function addMouseUp(func){
endListeners['mouseup'].push(func);
}
class Stamp{
constructor(
host,
parent_el,
width_percent,
){
this.host = host;
this.parent_el = parent_el;
// percent to pixels
this.width = width_percent / 100 * window.innerWidth;
[
this.el,
this.left_handle,
this.main_el,
this.right_handle,
] = Array.from({length: 4}, ()=>document.createElement('div'));
render_element({
position: 'relative',
display: 'inline-block',
width: this.width.toString() + 'px',
height: '100%',
display: 'flex',
flexDirection: 'row',
border: '1px solid',
}, this.el);
render_element({
width: '10%',
height: '100%',
border: '1px solid',
}, this.left_handle);
render_element({
width: '80%',
height: '100%',
cursor: 'ew-resize',
border: '1px solid',
}, this.main_el);
render_element({
width: '10%',
height: '100%',
cursor: 'ew-resize',
border: '1px solid',
}, this.right_handle);
this.el.appendChild(this.left_handle);
this.el.appendChild(this.main_el);
this.el.appendChild(this.right_handle);
// indicator during movement
this.indicator = document.createElement('div');
render_element({
position: 'absolute',
width: '5px',
height: '100%',
background: 'grey',
display: 'none',
}, this.indicator);
this.el.appendChild(this.indicator);
// move
// mousedown start move
this.in_move = false;
this.in_left_move = false;
this.left_handle.addEventListener('mousedown', e=>{
this.startMove(e);
this.in_move = true;
this.in_left_move = true;
});
this.right_handle.addEventListener('mousedown', e=>{
this.startMove(e);
this.in_move = true;
this.in_left_move = false;
});
// mousemove
this.move = function(e){
if(!this.in_move)return;
this.moveIndicator(e);
if(this.in_left_move){
this.expandLeft(e);
}else{
this.expandRight(e);
}
}.bind(this);
addMouseMove(this.move);
// mouseend finish move
addMouseUp(function(e){
this.move(e);
this.endMove(e);
this.in_move = false;
}.bind(this));
this.parent_el.appendChild(this.el);
this.getHandleRects = this.getHandleRects.bind(this);
this.startMove = this.startMove.bind(this);
this.endMove = this.endMove.bind(this);
this.expandLeft = this.expandLeft.bind(this);
this.expandRight = this.expandRight.bind(this);
this.moveIndicator = this.moveIndicator.bind(this);
}
getHandleRects(){
return [
this.el.getBoundingClientRect(),
this.left_handle.getBoundingClientRect(),
this.right_handle.getBoundingClientRect(),
]
}
startMove(e){
// show indicator
this.indicator.style.display = 'block';
this.moveIndicator(e);
// change color
this.el.style.background = 'lightblue';
}
endMove(){
// hide indicator
this.indicator.style.display = 'none';
// change color back
this.el.style.background = 'none';
}
expandLeft(e){
var [el_rect, left_rect, right_rect] = this.getHandleRects();
if(e.clientX >= right_rect.left)return;
let dif = el_rect.left - e.clientX;
this.el.style.width = (el_rect.width + dif - el_rect.offsetLeft).toString() + 'px';
this.el.style.marginLeft = (el_rect.left - dif).toString() + 'px';
}
expandRight(e){
var [el_rect, left_rect, right_rect] = this.getHandleRects();
if(e.clientX <= left_rect.right)return;
this.el.style.width = (e.clientX - el_rect.left).toString() + 'px';
}
moveIndicator(e){
this.indicator.style.marginLeft = e.clientX.toString() + 'px';
}
}
class Channel{
constructor(
host,
parent_el,
height_percent,
){
this.host = host;
this.parent_el = parent_el;
this.height = height_percent;
this.stamps = [];
this.el = document.createElement('div');
render_element({
position: 'relative',
width: '100%',
height: this.height.toString() + '%',
// display: 'flex',
// flexDirection: 'row',
border: '1px solid',
}, this.el);
this.stamp_indicator = document.createElement('div');
this.parent_el.appendChild(this.el);
this.addStamp = this.addStamp.bind(this);
}
addStamp(width_percent){
this.stamps.push(new Stamp(this, this.el, width_percent));
return this.stamps[this.stamps.length - 1];
}
}
class Timeline{
constructor(
parent_el,
frame_rate,
){
this.parent_el = parent_el;
this.frame_rate = frame_rate;
this.el = document.createElement('div');
render_element({
width: '100%',
height: '100%',
background: 'lightgrey',
}, this.el);
this.channels = [];
this.parent_el.appendChild(this.el);
this.el.droppable = true;
this.el.addEventListener('dragover', e=>{
e.preventDefault();
});
this.el.addEventListener('drop', e=>{
e.preventDefault();
console.log(e.dataTransfer.getData('text/plain'));
});
this.addChanel = this.addChannel.bind(this);
}
addChannel(height_percent){
this.channels.push(new Channel(this, this.el, height_percent));
return this.channels[this.channels.length - 1];
}
}
var tl = new Timeline(
document.querySelector('#timeline'),
2,
);
var c1 = tl.addChannel(33);
var s1 = c1.addStamp(20);
// if I call it explicitly, it works fine
s1.expandLeft({
clientX: 50,
});
// endListeners exec at end
for (const [kk, vv] of Object.entries(endListeners)) {
window.addEventListener(kk, e=>{vv.forEach(v=>v(e))});
}
#timeline{
position: fixed;
left: 0;
bottom: 0;
width: 100vw;
height: 300px;
border: 1px solid;
}
<div id="timeline"></div>
是的,我成功了!!通过存储鼠标移动前的左边距和宽度,并引用它而不是动态左边距和宽度。
function render_element(styles, el) {
for (const [kk, vv] of Object.entries(styles)) {
el.style[kk] = vv;
}
}
const endListeners = {
'mousemove': [],
'mouseup': [],
};
function addMouseMove(func){
endListeners['mousemove'].push(func);
}
function addMouseUp(func){
endListeners['mouseup'].push(func);
}
class Stamp{
constructor(
host,
parent_el,
width_percent,
){
this.host = host;
this.parent_el = parent_el;
// percent to pixels
this.width = width_percent / 100 * window.innerWidth;
[
this.el,
this.left_handle,
this.main_el,
this.right_handle,
] = Array.from({length: 4}, ()=>document.createElement('div'));
render_element({
position: 'relative',
display: 'inline-block',
width: this.width.toString() + 'px',
height: '100%',
display: 'flex',
flexDirection: 'row',
border: '1px solid',
}, this.el);
render_element({
width: '10%',
height: '100%',
border: '1px solid',
}, this.left_handle);
render_element({
width: '80%',
height: '100%',
cursor: 'ew-resize',
border: '1px solid',
}, this.main_el);
render_element({
width: '10%',
height: '100%',
cursor: 'ew-resize',
border: '1px solid',
}, this.right_handle);
this.el.appendChild(this.left_handle);
this.el.appendChild(this.main_el);
this.el.appendChild(this.right_handle);
// move
// mousedown start move
this.in_move = false;
this.in_left_move = false;
this.lwd = this.el.offsetWidth;
this.lft = this.el.offsetLeft;
this.left_handle.addEventListener('mousedown', function(e){
this.startMove(e);
this.in_move = true;
this.in_left_move = true;
this.lwd = this.el.offsetWidth;
this.lft = this.el.offsetLeft;
}.bind(this));
this.right_handle.addEventListener('mousedown', function(e){
this.startMove(e);
this.in_move = true;
this.in_left_move = false;
}.bind(this));
// mousemove
this.move = function(e){
if(!this.in_move)return;
if(this.in_left_move){
this.expandLeft(e);
}else{
this.expandRight(e);
}
}.bind(this);
addMouseMove(this.move);
// mouseend finish move
addMouseUp(function(e){
this.move(e);
this.endMove(e);
this.in_move = false;
}.bind(this));
this.parent_el.appendChild(this.el);
this.getHandleRects = this.getHandleRects.bind(this);
this.startMove = this.startMove.bind(this);
this.endMove = this.endMove.bind(this);
this.expandLeft = this.expandLeft.bind(this);
this.expandRight = this.expandRight.bind(this);
}
getHandleRects(){
return [
this.el.getBoundingClientRect(),
this.left_handle.getBoundingClientRect(),
this.right_handle.getBoundingClientRect(),
]
}
startMove(e){
// change color
this.el.style.background = 'lightblue';
}
endMove(){
// change color back
this.el.style.background = 'none';
}
expandLeft(e){
var [el_rect, left_rect, right_rect] = this.getHandleRects();
if(e.clientX >= right_rect.left)return;
let dif = this.lft - e.clientX;
this.el.style.width = (this.lwd + dif).toString() + 'px';
this.el.style.marginLeft = e.clientX.toString() + 'px';
}
expandRight(e){
var [el_rect, left_rect, right_rect] = this.getHandleRects();
if(e.clientX <= left_rect.right)return;
this.el.style.width = (e.clientX - el_rect.left).toString() + 'px';
}
}
class Channel{
constructor(
host,
parent_el,
height_percent,
){
this.host = host;
this.parent_el = parent_el;
this.height = height_percent;
this.stamps = [];
this.el = document.createElement('div');
render_element({
position: 'relative',
width: '100%',
height: this.height.toString() + '%',
// display: 'flex',
// flexDirection: 'row',
border: '1px solid',
}, this.el);
this.parent_el.appendChild(this.el);
this.addStamp = this.addStamp.bind(this);
}
addStamp(width_percent){
this.stamps.push(new Stamp(this, this.el, width_percent));
return this.stamps[this.stamps.length - 1];
}
}
class Timeline{
constructor(
parent_el,
frame_rate,
){
this.parent_el = parent_el;
this.frame_rate = frame_rate;
this.el = document.createElement('div');
render_element({
width: '100%',
height: '100%',
background: 'lightgrey',
}, this.el);
this.channels = [];
this.parent_el.appendChild(this.el);
this.el.droppable = true;
this.el.addEventListener('dragover', e=>{
e.preventDefault();
});
this.el.addEventListener('drop', e=>{
e.preventDefault();
console.log(e.dataTransfer.getData('text/plain'));
});
this.addChanel = this.addChannel.bind(this);
}
addChannel(height_percent){
this.channels.push(new Channel(this, this.el, height_percent));
return this.channels[this.channels.length - 1];
}
}
var tl = new Timeline(
document.querySelector('#timeline'),
2,
);
var c1 = tl.addChannel(33);
var s1 = c1.addStamp(20);
var c2 = tl.addChannel(33);
var s2 = c2.addStamp(30);
#timeline{
position: fixed;
left: 0;
bottom: 0;
width: 100vw;
height: 300px;
border: 1px solid;
}
<div id="timeline"></div>