问题描述
我的团队正在开发一个类似于 adobe Premier pro 中的时间轴应用程序。代码大约有 250 行长,我遇到了一个奇怪的错误,我花了一整天但未能解决。以下是架构简介:
我想实现 left-right-drag
以更改 Stamp
的持续时间:
- 向左拖动将改变开始时间
- 拖拽结束将改变结束时间
因此,我将一个Stamp
元素分为三个部分:左句柄、主(中)元素和右句柄,它们包含在el
在 mousedown
和 mousemove
上实现 mouseup
、left_handle
和 right_handle
并不健壮,因为一旦光标在鼠标移动期间飞出元素,mouseup
不会触发。因此,mousemove 和 mouseup 事件侦听器设置在窗口上
请看代码:
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%',this.left_handle);
render_element({
width: '80%',cursor: 'ew-resize',this.main_el);
render_element({
width: '10%',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',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,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,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',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%',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,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
以事件为参数,通过更改两个 css 属性来更改 Stamp
(设置开始时间)中的 Channel
位置:
- 左边距
- 宽度
非常奇怪的是:
- 拖动右手柄时,它的工作完全符合我的预期。
- 在调用
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,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%',this.left_handle);
render_element({
width: '80%',cursor: 'ew-resize',this.main_el);
render_element({
width: '10%',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',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,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,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',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%',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,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,vv] of Object.entries(styles)) {
el.style[kk] = vv;
}
}
const endListeners = {
'mousemove': [],};
function addMouseMove(func){
endListeners['mousemove'].push(func);
}
function addMouseUp(func){
endListeners['mouseup'].push(func);
}
class Stamp{
constructor(
host,){
this.host = host;
this.parent_el = parent_el;
// percent to pixels
this.width = width_percent / 100 * window.innerWidth;
[
this.el,()=>document.createElement('div'));
render_element({
position: 'relative',this.el);
render_element({
width: '10%',this.left_handle);
render_element({
width: '80%',this.main_el);
render_element({
width: '10%',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(),]
}
startMove(e){
// change color
this.el.style.background = 'lightblue';
}
endMove(){
// change color back
this.el.style.background = 'none';
}
expandLeft(e){
var [el_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,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,){
this.host = host;
this.parent_el = parent_el;
this.height = height_percent;
this.stamps = [];
this.el = document.createElement('div');
render_element({
position: 'relative',this.el);
this.parent_el.appendChild(this.el);
this.addStamp = this.addStamp.bind(this);
}
addStamp(width_percent){
this.stamps.push(new Stamp(this,width_percent));
return this.stamps[this.stamps.length - 1];
}
}
class Timeline{
constructor(
parent_el,){
this.parent_el = parent_el;
this.frame_rate = frame_rate;
this.el = document.createElement('div');
render_element({
width: '100%',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,height_percent));
return this.channels[this.channels.length - 1];
}
}
var tl = new Timeline(
document.querySelector('#timeline'),);
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>