Throttle redraw requests fired from pointer events Planned maintenance scheduled April 23,...

Providing direct feedback to a product salesperson

What happened to Viserion in Season 7?

Why is std::is_aggregate<T> an aggregate?

When does Bran Stark remember Jamie pushing him?

Was Objective-C really a hinderance to Apple software development?

Why does Java have support for time zone offsets with seconds precision?

Beamer - mp3 - file does not play

When speaking, how do you change your mind mid-sentence?

Tikz - create rectangle with same width as the picture

Why do elements created under a `new Document` contain the wrong prototype?

Will I have to go through TSA security when I return to the US after preclearance?

What is the purpose of the side handle on a hand ("eggbeater") drill?

Is it accepted to use working hours to read general interest books?

Why did Europeans not widely domesticate foxes?

A weird observation

What is the definining line between a helicopter and a drone a person can ride in?

How to begin with a paragraph in latex

Determinant of a matrix with 2 equal rows

Putting Ant-Man on house arrest

Deciphering death certificate writing

Mathematica 3D Heat Equation Solution

Processing ADC conversion result: DMA vs Processor Registers

Raising a bilingual kid. When should we introduce the majority language?

Co-worker works way more than he should



Throttle redraw requests fired from pointer events



Planned maintenance scheduled April 23, 2019 at 23:30 UTC (7:30pm US/Eastern)
Announcing the arrival of Valued Associate #679: Cesar Manara
Unicorn Meta Zoo #1: Why another podcast?JavaScript event handlers, scope, and the module design patternAnonymous Events in DelphiAngular wrapper for SignalR event aggregatorRecorder for keyboard and mouse eventsControling Document and Window events via arrayEnabling or disabling buttons on certain eventsPopup classes hierarchy designCompetent addition of events in JavaScriptHandling keyup events with throttle or debounce in Javascript/jQueryC++ Generic Action Dispatching & Handling System





.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty,.everyoneloves__bot-mid-leaderboard:empty{ margin-bottom:0;
}







0












$begingroup$


I am working on a photo editor, which displays a preview to the user on a canvas. At various points in the app, the user may use sliders to adjust properties of the photo, such as scaling, rotation, etc. The preview needs to be updated in real time, as the user drags the slider.



As I understand, pointer events are fired much more rapidly than the browser window is repainted. Because of this, it would be wasteful to update the preview directly from the pointer event handlers, since most of those updates would have no visible effect. Instead, I wanted to separate the actions of updating the preview state based on the most recent pointer event, and redrawing the preview based on the latest update since the last redraw.



To this end, I wrote a small helper function called throttleRedraw(), which I'd appreciate if you could review. Is it properly optimized? Is it even necessary? There's probably a library for it, but I wanted to make sure I understand the principle at the core.



Here's a snippet containing the function itself, and a simple example that draws a rectangle on a canvas when you click and drag with the pointer:






function throttleRedraw(target, props) {
const {
onstart,
onmove,
onend,
ondraw,
absolute,
} = props;

let frameReq = 0;
let lastX = 0;
let lastY = 0;
let isTouch = false;
let enabled = true;

// This is the callback we will be passing to requestAnimationFrame().
// It's just a proxy for the ondraw() function passed in the props object.
const frameHandler = (now) => {
if (ondraw) ondraw(now);
frameReq = 0;
};

// The callback for the mousemove/touchmove events,
// which in turn calls props.onmove() if it was specified.
// If props.absolute is true, props.onmove() receives the x and y coordinates of the pointer;
// otherwise, it receives the delta since the last update.
const moveHandler = (event) => {
if (onmove) {
const newX = isTouch ? event.touches[0].clientX : event.clientX;
const newY = isTouch ? event.touches[0].clientY : event.clientY;
onmove(
absolute ? newX : newX - lastX,
absolute ? newY : newY - lastY,
event
);
lastX = newX;
lastY = newY;
}

// If we have not yet requested an animation frame since the last, do it now.
if (!frameReq) {
frameReq = window.requestAnimationFrame(frameHandler);
}
};

// The callback for the mouseup/touchend events,
// which in turn calls props.onend() if it was specified.
const upHandler = (event) => {
if (onend) onend(event);

// Remove the event handlers we set at the start.
if (!isTouch) {
window.removeEventListener('mousemove', moveHandler);
window.removeEventListener('mouseup', upHandler);
}
else {
window.removeEventListener('touchmove', moveHandler);
window.removeEventListener('touchend', upHandler);
window.removeEventListener('touchcancel', upHandler);
}
};

// Set the mousedown/touchstart event listeners on the target.
// They're mostly the same, except for how the coordinates are obtained
// from the event, and what additional event handlers need to be set.

target.addEventListener('mousedown', (event) => {
if (!enabled) return true;

isTouch = false;
event.preventDefault();
lastX = event.clientX;
lastY = event.clientY;
if (onstart) onstart(lastX, lastY, event);

window.addEventListener('mousemove', moveHandler);
window.addEventListener('mouseup', upHandler);
});

target.addEventListener('touchstart', (event) => {
if (!enabled) return true;

isTouch = true;
event.preventDefault();
lastX = event.touches[0].clientX;
lastY = event.touches[0].clientY;
if (onstart) onstart(lastX, lastY, event);

window.addEventListener('touchmove', moveHandler);
window.addEventListener('touchend', upHandler);
window.addEventListener('touchcancel', upHandler);
});

// Return an object that allows us to enable or disable listening for events,
// and query the current enabled state.
return {
isEnabled: () => enabled,
enable: (value) => { enabled = value; },
};
}

const canvas = document.querySelector('canvas');
const rect = canvas.getBoundingClientRect();
const ctx = canvas.getContext('2d');

let x0, y0, x1, y1;

throttleRedraw(canvas, {
absolute: true,

onstart: (x, y) => {
x0 = x - rect.left;
y0 = y - rect.top;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'red';
},
onmove: (x, y) => {
x1 = x - rect.left;
y1 = y - rect.top;
},
ondraw: () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillRect(
Math.min(x0, x1),
Math.min(y0, y1),
Math.abs(x1 - x0),
Math.abs(y1 - y0)
);
},
});

<canvas width="400" height="400" style="border: 2px solid #888"></canvas>





Thanks in advance.










share|improve this question









$endgroup$



















    0












    $begingroup$


    I am working on a photo editor, which displays a preview to the user on a canvas. At various points in the app, the user may use sliders to adjust properties of the photo, such as scaling, rotation, etc. The preview needs to be updated in real time, as the user drags the slider.



    As I understand, pointer events are fired much more rapidly than the browser window is repainted. Because of this, it would be wasteful to update the preview directly from the pointer event handlers, since most of those updates would have no visible effect. Instead, I wanted to separate the actions of updating the preview state based on the most recent pointer event, and redrawing the preview based on the latest update since the last redraw.



    To this end, I wrote a small helper function called throttleRedraw(), which I'd appreciate if you could review. Is it properly optimized? Is it even necessary? There's probably a library for it, but I wanted to make sure I understand the principle at the core.



    Here's a snippet containing the function itself, and a simple example that draws a rectangle on a canvas when you click and drag with the pointer:






    function throttleRedraw(target, props) {
    const {
    onstart,
    onmove,
    onend,
    ondraw,
    absolute,
    } = props;

    let frameReq = 0;
    let lastX = 0;
    let lastY = 0;
    let isTouch = false;
    let enabled = true;

    // This is the callback we will be passing to requestAnimationFrame().
    // It's just a proxy for the ondraw() function passed in the props object.
    const frameHandler = (now) => {
    if (ondraw) ondraw(now);
    frameReq = 0;
    };

    // The callback for the mousemove/touchmove events,
    // which in turn calls props.onmove() if it was specified.
    // If props.absolute is true, props.onmove() receives the x and y coordinates of the pointer;
    // otherwise, it receives the delta since the last update.
    const moveHandler = (event) => {
    if (onmove) {
    const newX = isTouch ? event.touches[0].clientX : event.clientX;
    const newY = isTouch ? event.touches[0].clientY : event.clientY;
    onmove(
    absolute ? newX : newX - lastX,
    absolute ? newY : newY - lastY,
    event
    );
    lastX = newX;
    lastY = newY;
    }

    // If we have not yet requested an animation frame since the last, do it now.
    if (!frameReq) {
    frameReq = window.requestAnimationFrame(frameHandler);
    }
    };

    // The callback for the mouseup/touchend events,
    // which in turn calls props.onend() if it was specified.
    const upHandler = (event) => {
    if (onend) onend(event);

    // Remove the event handlers we set at the start.
    if (!isTouch) {
    window.removeEventListener('mousemove', moveHandler);
    window.removeEventListener('mouseup', upHandler);
    }
    else {
    window.removeEventListener('touchmove', moveHandler);
    window.removeEventListener('touchend', upHandler);
    window.removeEventListener('touchcancel', upHandler);
    }
    };

    // Set the mousedown/touchstart event listeners on the target.
    // They're mostly the same, except for how the coordinates are obtained
    // from the event, and what additional event handlers need to be set.

    target.addEventListener('mousedown', (event) => {
    if (!enabled) return true;

    isTouch = false;
    event.preventDefault();
    lastX = event.clientX;
    lastY = event.clientY;
    if (onstart) onstart(lastX, lastY, event);

    window.addEventListener('mousemove', moveHandler);
    window.addEventListener('mouseup', upHandler);
    });

    target.addEventListener('touchstart', (event) => {
    if (!enabled) return true;

    isTouch = true;
    event.preventDefault();
    lastX = event.touches[0].clientX;
    lastY = event.touches[0].clientY;
    if (onstart) onstart(lastX, lastY, event);

    window.addEventListener('touchmove', moveHandler);
    window.addEventListener('touchend', upHandler);
    window.addEventListener('touchcancel', upHandler);
    });

    // Return an object that allows us to enable or disable listening for events,
    // and query the current enabled state.
    return {
    isEnabled: () => enabled,
    enable: (value) => { enabled = value; },
    };
    }

    const canvas = document.querySelector('canvas');
    const rect = canvas.getBoundingClientRect();
    const ctx = canvas.getContext('2d');

    let x0, y0, x1, y1;

    throttleRedraw(canvas, {
    absolute: true,

    onstart: (x, y) => {
    x0 = x - rect.left;
    y0 = y - rect.top;
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = 'red';
    },
    onmove: (x, y) => {
    x1 = x - rect.left;
    y1 = y - rect.top;
    },
    ondraw: () => {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.fillRect(
    Math.min(x0, x1),
    Math.min(y0, y1),
    Math.abs(x1 - x0),
    Math.abs(y1 - y0)
    );
    },
    });

    <canvas width="400" height="400" style="border: 2px solid #888"></canvas>





    Thanks in advance.










    share|improve this question









    $endgroup$















      0












      0








      0


      1



      $begingroup$


      I am working on a photo editor, which displays a preview to the user on a canvas. At various points in the app, the user may use sliders to adjust properties of the photo, such as scaling, rotation, etc. The preview needs to be updated in real time, as the user drags the slider.



      As I understand, pointer events are fired much more rapidly than the browser window is repainted. Because of this, it would be wasteful to update the preview directly from the pointer event handlers, since most of those updates would have no visible effect. Instead, I wanted to separate the actions of updating the preview state based on the most recent pointer event, and redrawing the preview based on the latest update since the last redraw.



      To this end, I wrote a small helper function called throttleRedraw(), which I'd appreciate if you could review. Is it properly optimized? Is it even necessary? There's probably a library for it, but I wanted to make sure I understand the principle at the core.



      Here's a snippet containing the function itself, and a simple example that draws a rectangle on a canvas when you click and drag with the pointer:






      function throttleRedraw(target, props) {
      const {
      onstart,
      onmove,
      onend,
      ondraw,
      absolute,
      } = props;

      let frameReq = 0;
      let lastX = 0;
      let lastY = 0;
      let isTouch = false;
      let enabled = true;

      // This is the callback we will be passing to requestAnimationFrame().
      // It's just a proxy for the ondraw() function passed in the props object.
      const frameHandler = (now) => {
      if (ondraw) ondraw(now);
      frameReq = 0;
      };

      // The callback for the mousemove/touchmove events,
      // which in turn calls props.onmove() if it was specified.
      // If props.absolute is true, props.onmove() receives the x and y coordinates of the pointer;
      // otherwise, it receives the delta since the last update.
      const moveHandler = (event) => {
      if (onmove) {
      const newX = isTouch ? event.touches[0].clientX : event.clientX;
      const newY = isTouch ? event.touches[0].clientY : event.clientY;
      onmove(
      absolute ? newX : newX - lastX,
      absolute ? newY : newY - lastY,
      event
      );
      lastX = newX;
      lastY = newY;
      }

      // If we have not yet requested an animation frame since the last, do it now.
      if (!frameReq) {
      frameReq = window.requestAnimationFrame(frameHandler);
      }
      };

      // The callback for the mouseup/touchend events,
      // which in turn calls props.onend() if it was specified.
      const upHandler = (event) => {
      if (onend) onend(event);

      // Remove the event handlers we set at the start.
      if (!isTouch) {
      window.removeEventListener('mousemove', moveHandler);
      window.removeEventListener('mouseup', upHandler);
      }
      else {
      window.removeEventListener('touchmove', moveHandler);
      window.removeEventListener('touchend', upHandler);
      window.removeEventListener('touchcancel', upHandler);
      }
      };

      // Set the mousedown/touchstart event listeners on the target.
      // They're mostly the same, except for how the coordinates are obtained
      // from the event, and what additional event handlers need to be set.

      target.addEventListener('mousedown', (event) => {
      if (!enabled) return true;

      isTouch = false;
      event.preventDefault();
      lastX = event.clientX;
      lastY = event.clientY;
      if (onstart) onstart(lastX, lastY, event);

      window.addEventListener('mousemove', moveHandler);
      window.addEventListener('mouseup', upHandler);
      });

      target.addEventListener('touchstart', (event) => {
      if (!enabled) return true;

      isTouch = true;
      event.preventDefault();
      lastX = event.touches[0].clientX;
      lastY = event.touches[0].clientY;
      if (onstart) onstart(lastX, lastY, event);

      window.addEventListener('touchmove', moveHandler);
      window.addEventListener('touchend', upHandler);
      window.addEventListener('touchcancel', upHandler);
      });

      // Return an object that allows us to enable or disable listening for events,
      // and query the current enabled state.
      return {
      isEnabled: () => enabled,
      enable: (value) => { enabled = value; },
      };
      }

      const canvas = document.querySelector('canvas');
      const rect = canvas.getBoundingClientRect();
      const ctx = canvas.getContext('2d');

      let x0, y0, x1, y1;

      throttleRedraw(canvas, {
      absolute: true,

      onstart: (x, y) => {
      x0 = x - rect.left;
      y0 = y - rect.top;
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ctx.fillStyle = 'red';
      },
      onmove: (x, y) => {
      x1 = x - rect.left;
      y1 = y - rect.top;
      },
      ondraw: () => {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ctx.fillRect(
      Math.min(x0, x1),
      Math.min(y0, y1),
      Math.abs(x1 - x0),
      Math.abs(y1 - y0)
      );
      },
      });

      <canvas width="400" height="400" style="border: 2px solid #888"></canvas>





      Thanks in advance.










      share|improve this question









      $endgroup$




      I am working on a photo editor, which displays a preview to the user on a canvas. At various points in the app, the user may use sliders to adjust properties of the photo, such as scaling, rotation, etc. The preview needs to be updated in real time, as the user drags the slider.



      As I understand, pointer events are fired much more rapidly than the browser window is repainted. Because of this, it would be wasteful to update the preview directly from the pointer event handlers, since most of those updates would have no visible effect. Instead, I wanted to separate the actions of updating the preview state based on the most recent pointer event, and redrawing the preview based on the latest update since the last redraw.



      To this end, I wrote a small helper function called throttleRedraw(), which I'd appreciate if you could review. Is it properly optimized? Is it even necessary? There's probably a library for it, but I wanted to make sure I understand the principle at the core.



      Here's a snippet containing the function itself, and a simple example that draws a rectangle on a canvas when you click and drag with the pointer:






      function throttleRedraw(target, props) {
      const {
      onstart,
      onmove,
      onend,
      ondraw,
      absolute,
      } = props;

      let frameReq = 0;
      let lastX = 0;
      let lastY = 0;
      let isTouch = false;
      let enabled = true;

      // This is the callback we will be passing to requestAnimationFrame().
      // It's just a proxy for the ondraw() function passed in the props object.
      const frameHandler = (now) => {
      if (ondraw) ondraw(now);
      frameReq = 0;
      };

      // The callback for the mousemove/touchmove events,
      // which in turn calls props.onmove() if it was specified.
      // If props.absolute is true, props.onmove() receives the x and y coordinates of the pointer;
      // otherwise, it receives the delta since the last update.
      const moveHandler = (event) => {
      if (onmove) {
      const newX = isTouch ? event.touches[0].clientX : event.clientX;
      const newY = isTouch ? event.touches[0].clientY : event.clientY;
      onmove(
      absolute ? newX : newX - lastX,
      absolute ? newY : newY - lastY,
      event
      );
      lastX = newX;
      lastY = newY;
      }

      // If we have not yet requested an animation frame since the last, do it now.
      if (!frameReq) {
      frameReq = window.requestAnimationFrame(frameHandler);
      }
      };

      // The callback for the mouseup/touchend events,
      // which in turn calls props.onend() if it was specified.
      const upHandler = (event) => {
      if (onend) onend(event);

      // Remove the event handlers we set at the start.
      if (!isTouch) {
      window.removeEventListener('mousemove', moveHandler);
      window.removeEventListener('mouseup', upHandler);
      }
      else {
      window.removeEventListener('touchmove', moveHandler);
      window.removeEventListener('touchend', upHandler);
      window.removeEventListener('touchcancel', upHandler);
      }
      };

      // Set the mousedown/touchstart event listeners on the target.
      // They're mostly the same, except for how the coordinates are obtained
      // from the event, and what additional event handlers need to be set.

      target.addEventListener('mousedown', (event) => {
      if (!enabled) return true;

      isTouch = false;
      event.preventDefault();
      lastX = event.clientX;
      lastY = event.clientY;
      if (onstart) onstart(lastX, lastY, event);

      window.addEventListener('mousemove', moveHandler);
      window.addEventListener('mouseup', upHandler);
      });

      target.addEventListener('touchstart', (event) => {
      if (!enabled) return true;

      isTouch = true;
      event.preventDefault();
      lastX = event.touches[0].clientX;
      lastY = event.touches[0].clientY;
      if (onstart) onstart(lastX, lastY, event);

      window.addEventListener('touchmove', moveHandler);
      window.addEventListener('touchend', upHandler);
      window.addEventListener('touchcancel', upHandler);
      });

      // Return an object that allows us to enable or disable listening for events,
      // and query the current enabled state.
      return {
      isEnabled: () => enabled,
      enable: (value) => { enabled = value; },
      };
      }

      const canvas = document.querySelector('canvas');
      const rect = canvas.getBoundingClientRect();
      const ctx = canvas.getContext('2d');

      let x0, y0, x1, y1;

      throttleRedraw(canvas, {
      absolute: true,

      onstart: (x, y) => {
      x0 = x - rect.left;
      y0 = y - rect.top;
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ctx.fillStyle = 'red';
      },
      onmove: (x, y) => {
      x1 = x - rect.left;
      y1 = y - rect.top;
      },
      ondraw: () => {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ctx.fillRect(
      Math.min(x0, x1),
      Math.min(y0, y1),
      Math.abs(x1 - x0),
      Math.abs(y1 - y0)
      );
      },
      });

      <canvas width="400" height="400" style="border: 2px solid #888"></canvas>





      Thanks in advance.






      function throttleRedraw(target, props) {
      const {
      onstart,
      onmove,
      onend,
      ondraw,
      absolute,
      } = props;

      let frameReq = 0;
      let lastX = 0;
      let lastY = 0;
      let isTouch = false;
      let enabled = true;

      // This is the callback we will be passing to requestAnimationFrame().
      // It's just a proxy for the ondraw() function passed in the props object.
      const frameHandler = (now) => {
      if (ondraw) ondraw(now);
      frameReq = 0;
      };

      // The callback for the mousemove/touchmove events,
      // which in turn calls props.onmove() if it was specified.
      // If props.absolute is true, props.onmove() receives the x and y coordinates of the pointer;
      // otherwise, it receives the delta since the last update.
      const moveHandler = (event) => {
      if (onmove) {
      const newX = isTouch ? event.touches[0].clientX : event.clientX;
      const newY = isTouch ? event.touches[0].clientY : event.clientY;
      onmove(
      absolute ? newX : newX - lastX,
      absolute ? newY : newY - lastY,
      event
      );
      lastX = newX;
      lastY = newY;
      }

      // If we have not yet requested an animation frame since the last, do it now.
      if (!frameReq) {
      frameReq = window.requestAnimationFrame(frameHandler);
      }
      };

      // The callback for the mouseup/touchend events,
      // which in turn calls props.onend() if it was specified.
      const upHandler = (event) => {
      if (onend) onend(event);

      // Remove the event handlers we set at the start.
      if (!isTouch) {
      window.removeEventListener('mousemove', moveHandler);
      window.removeEventListener('mouseup', upHandler);
      }
      else {
      window.removeEventListener('touchmove', moveHandler);
      window.removeEventListener('touchend', upHandler);
      window.removeEventListener('touchcancel', upHandler);
      }
      };

      // Set the mousedown/touchstart event listeners on the target.
      // They're mostly the same, except for how the coordinates are obtained
      // from the event, and what additional event handlers need to be set.

      target.addEventListener('mousedown', (event) => {
      if (!enabled) return true;

      isTouch = false;
      event.preventDefault();
      lastX = event.clientX;
      lastY = event.clientY;
      if (onstart) onstart(lastX, lastY, event);

      window.addEventListener('mousemove', moveHandler);
      window.addEventListener('mouseup', upHandler);
      });

      target.addEventListener('touchstart', (event) => {
      if (!enabled) return true;

      isTouch = true;
      event.preventDefault();
      lastX = event.touches[0].clientX;
      lastY = event.touches[0].clientY;
      if (onstart) onstart(lastX, lastY, event);

      window.addEventListener('touchmove', moveHandler);
      window.addEventListener('touchend', upHandler);
      window.addEventListener('touchcancel', upHandler);
      });

      // Return an object that allows us to enable or disable listening for events,
      // and query the current enabled state.
      return {
      isEnabled: () => enabled,
      enable: (value) => { enabled = value; },
      };
      }

      const canvas = document.querySelector('canvas');
      const rect = canvas.getBoundingClientRect();
      const ctx = canvas.getContext('2d');

      let x0, y0, x1, y1;

      throttleRedraw(canvas, {
      absolute: true,

      onstart: (x, y) => {
      x0 = x - rect.left;
      y0 = y - rect.top;
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ctx.fillStyle = 'red';
      },
      onmove: (x, y) => {
      x1 = x - rect.left;
      y1 = y - rect.top;
      },
      ondraw: () => {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ctx.fillRect(
      Math.min(x0, x1),
      Math.min(y0, y1),
      Math.abs(x1 - x0),
      Math.abs(y1 - y0)
      );
      },
      });

      <canvas width="400" height="400" style="border: 2px solid #888"></canvas>





      function throttleRedraw(target, props) {
      const {
      onstart,
      onmove,
      onend,
      ondraw,
      absolute,
      } = props;

      let frameReq = 0;
      let lastX = 0;
      let lastY = 0;
      let isTouch = false;
      let enabled = true;

      // This is the callback we will be passing to requestAnimationFrame().
      // It's just a proxy for the ondraw() function passed in the props object.
      const frameHandler = (now) => {
      if (ondraw) ondraw(now);
      frameReq = 0;
      };

      // The callback for the mousemove/touchmove events,
      // which in turn calls props.onmove() if it was specified.
      // If props.absolute is true, props.onmove() receives the x and y coordinates of the pointer;
      // otherwise, it receives the delta since the last update.
      const moveHandler = (event) => {
      if (onmove) {
      const newX = isTouch ? event.touches[0].clientX : event.clientX;
      const newY = isTouch ? event.touches[0].clientY : event.clientY;
      onmove(
      absolute ? newX : newX - lastX,
      absolute ? newY : newY - lastY,
      event
      );
      lastX = newX;
      lastY = newY;
      }

      // If we have not yet requested an animation frame since the last, do it now.
      if (!frameReq) {
      frameReq = window.requestAnimationFrame(frameHandler);
      }
      };

      // The callback for the mouseup/touchend events,
      // which in turn calls props.onend() if it was specified.
      const upHandler = (event) => {
      if (onend) onend(event);

      // Remove the event handlers we set at the start.
      if (!isTouch) {
      window.removeEventListener('mousemove', moveHandler);
      window.removeEventListener('mouseup', upHandler);
      }
      else {
      window.removeEventListener('touchmove', moveHandler);
      window.removeEventListener('touchend', upHandler);
      window.removeEventListener('touchcancel', upHandler);
      }
      };

      // Set the mousedown/touchstart event listeners on the target.
      // They're mostly the same, except for how the coordinates are obtained
      // from the event, and what additional event handlers need to be set.

      target.addEventListener('mousedown', (event) => {
      if (!enabled) return true;

      isTouch = false;
      event.preventDefault();
      lastX = event.clientX;
      lastY = event.clientY;
      if (onstart) onstart(lastX, lastY, event);

      window.addEventListener('mousemove', moveHandler);
      window.addEventListener('mouseup', upHandler);
      });

      target.addEventListener('touchstart', (event) => {
      if (!enabled) return true;

      isTouch = true;
      event.preventDefault();
      lastX = event.touches[0].clientX;
      lastY = event.touches[0].clientY;
      if (onstart) onstart(lastX, lastY, event);

      window.addEventListener('touchmove', moveHandler);
      window.addEventListener('touchend', upHandler);
      window.addEventListener('touchcancel', upHandler);
      });

      // Return an object that allows us to enable or disable listening for events,
      // and query the current enabled state.
      return {
      isEnabled: () => enabled,
      enable: (value) => { enabled = value; },
      };
      }

      const canvas = document.querySelector('canvas');
      const rect = canvas.getBoundingClientRect();
      const ctx = canvas.getContext('2d');

      let x0, y0, x1, y1;

      throttleRedraw(canvas, {
      absolute: true,

      onstart: (x, y) => {
      x0 = x - rect.left;
      y0 = y - rect.top;
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ctx.fillStyle = 'red';
      },
      onmove: (x, y) => {
      x1 = x - rect.left;
      y1 = y - rect.top;
      },
      ondraw: () => {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ctx.fillRect(
      Math.min(x0, x1),
      Math.min(y0, y1),
      Math.abs(x1 - x0),
      Math.abs(y1 - y0)
      );
      },
      });

      <canvas width="400" height="400" style="border: 2px solid #888"></canvas>






      javascript reinventing-the-wheel event-handling






      share|improve this question













      share|improve this question











      share|improve this question




      share|improve this question










      asked Aug 21 '18 at 9:04









      Máté SafrankaMáté Safranka

      51628




      51628






















          1 Answer
          1






          active

          oldest

          votes


















          0












          $begingroup$

          Limiting updates



          You are correct about mouse event rates, I have seen mouse event 2ms appart. However touch events seam to be in sync with the display 60FPS, I have never seen a touch event fire at rates higher than this (but that is a hardware attribute as far as I know)



          But that is besides the point. It is always a good idea to decouple the rendering from random events, be they user input, or from some other source. The display is driven at a fixed rate and to maintain good quality animations and presentation, keeping it tightly synced to that rate is important.



          Some style a code points.




          • window is the default object you don't need to use it. window.removeEventListener is the same as removeEventListener and conversly your function throttleRedraw is the same as window.throttleRedraw


          • It is not good to have statement block without delimiting parenthesis as it can easily be missed when making modifications. if (ondraw) ondraw(now); is better as if (ondraw) { ondraw(now) } the ; is optional as the line has ended in a }


          • You don't have to have a separate event handler for each event type. Many of the event properties are repeated and you end up duplicating code. Create a single handler that handles all the events of similar type.


          • The DOM does it all, but the DOM does it badly, adding and removing event handlers is just adding more work to the DOM and can be more efficiently handled by your JavaScript. Set handlers once and ignore events not needed


          • Too many comments. Comment only code that is not self evident, any other comments are just noise and make the code harder to read.



          Design



          From a design point I would not tie the mouse/touch event handlers so tightly with the rendering functionality. It is likely that there will be additional event like sources that require updated rendering. Completely decoupling the rendering from the pointer like events will make the system more flexible.



          Touch and mouse are abstractly the same, try to reduce the individual abstraction and use a common pointer like abstract to name and define behaviors.



          The flag absolute is an over complication. Just keep a pointer delta that contains the relative pointer motion and let the handling code pick which to use.



          Example



          The example decouples the pointer from the rendering.



          The pointer is created and listens to all relevant events and sets its properties x, y, down... and so on. The pointer also correct the coors to be relative to the target top left.



          The render start up and creates a pointer object setting its target to the canvas The main loop continues to run and will only update if there is a change to the pointer.



          The result is that the rendering becomes more complex, but that is offset by greatly reducing the pointer event handling code.



          This also means that you could add keyboard events, or have running animations (like selection box dash movement) and not be reliant on the mouse event handler to pass appropriate render calls



          Note that it listens to document events and filters out events not triggered by the active element. (THE snippet has modified behavior and does not allow event outside the snippet window. The event handler has the target test commented out to improve it a little, but it is not an accurate representation of how it works on a independent page).






          function createPointer(target) {
          var currentTarget;
          var bounds;
          const types = {
          mouse: 1, touch: 2, move: 1, up: 2, end: 2, cancel: 2, start: 0, down: 0,
          names : "touchcancel,touchend,touchmove,touchstart,mousemove,mousedown,mouseup".split(","),
          }
          const pointer = {
          delta: {x : 0, y : 0},
          pos: {x : 0, y : 0, coord(x,y) { this.x = x; this.y = y }},
          last: {x : 0, y : 0, set from(src) { this.x = src.x; this.y = src.y }},
          down : false,
          active : false,
          changed : false,
          set position(coord) {
          pointer.last.from = pointer.pos;
          pointer.pos.coord(coord.clientX- bounds.left + scrollX, coord.clientY - bounds.top + scrollY);
          pointer.delta.x = pointer.pos.x - pointer.last.x;
          pointer.delta.y = pointer.pos.y - pointer.last.y;
          },
          set target(element){
          if(!element){
          types.names.forEach(event => removeEventListener(event, events) );
          currentTarget = undefined;
          pointer.active = false;
          }else {
          if (!currentTarget) { types.names.forEach(event => addEventListener(event, events) ) }
          currentTarget = element;
          bounds = element.getBoundingClientRect()
          pointer.active = true;
          }
          pointer.down = false;
          pointer.changed = true;
          }
          }
          const events = event => {
          // if (event.target === currentTarget) {
          const type = types[event.type.substring(5)];
          pointer.position = types[event.type.substring(0,5)] === types.touch ? event.touches[0] : event;
          if (type === types.down) { pointer.down = true }
          else if (type === types.up) { pointer.down = false }
          pointer.changed = true;
          event.preventDefault();
          // }
          }
          pointer.target = target;
          return pointer;
          }


          const rendering = (()=> {
          requestAnimationFrame(mainLoop);
          const canvas = document.querySelector('canvas');
          const ctx = canvas.getContext('2d');
          const pointer = createPointer(canvas);
          var update = false;

          function as(src) { this.x = src.x; this.y = src.y }
          const drag = {
          active : false,
          start : {x : 0, y : 0, as},
          end : {x : 0, y : 0, as},
          draw() {
          ctx.fillStyle = "red";
          ctx.fillRect(
          Math.min(drag.start.x, drag.end.x), Math.min(drag.start.y, drag.end.y),
          Math.abs(drag.start.x - drag.end.x), Math.abs(drag.start.y - drag.end.y),
          )
          }
          }

          function checkPointer() {
          if (pointer.changed) {
          if (pointer.down) {
          if (!drag.active) {
          drag.start.as(pointer.pos);
          drag.active = true;
          }
          drag.end.as(pointer.pos);
          update = true;
          } else if (drag.active) {
          drag.end.as(pointer.pos);
          drag.active = false;
          update = true;
          }
          pointer.changed = false;
          }
          }

          function mainLoop(time) {
          checkPointer();
          if (update) {
          ctx.clearRect(0,0,canvas.width,canvas.height);
          drag.draw();
          update = false;
          }
          requestAnimationFrame(mainLoop);
          }
          })();

          <canvas width="400" height="400" style="border: 2px solid #888"></canvas>








          share|improve this answer











          $endgroup$













          • $begingroup$
            Just a quick reply at first glance: "Mouse events on a element have the annoying habit of only hearing events while the mouse is over the element." -- That's actually why I used window.addEventListener, to capture the move/up/cancel events at the highest stage of propagation. document can still be a problem if the page doesn't fill the whole window. I'll look through your example in more detail when I'm not on my phone. Thanks for your answer!
            $endgroup$
            – Máté Safranka
            Aug 21 '18 at 14:27










          • $begingroup$
            @MátéSafranka You are absolutely correct, I ran you code and at the time forgot that the snippets messed with the mouse event, and I just presumed. Will change the answer.
            $endgroup$
            – Blindman67
            Aug 21 '18 at 15:24












          Your Answer






          StackExchange.ifUsing("editor", function () {
          StackExchange.using("externalEditor", function () {
          StackExchange.using("snippets", function () {
          StackExchange.snippets.init();
          });
          });
          }, "code-snippets");

          StackExchange.ready(function() {
          var channelOptions = {
          tags: "".split(" "),
          id: "196"
          };
          initTagRenderer("".split(" "), "".split(" "), channelOptions);

          StackExchange.using("externalEditor", function() {
          // Have to fire editor after snippets, if snippets enabled
          if (StackExchange.settings.snippets.snippetsEnabled) {
          StackExchange.using("snippets", function() {
          createEditor();
          });
          }
          else {
          createEditor();
          }
          });

          function createEditor() {
          StackExchange.prepareEditor({
          heartbeatType: 'answer',
          autoActivateHeartbeat: false,
          convertImagesToLinks: false,
          noModals: true,
          showLowRepImageUploadWarning: true,
          reputationToPostImages: null,
          bindNavPrevention: true,
          postfix: "",
          imageUploader: {
          brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
          contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
          allowUrls: true
          },
          onDemand: true,
          discardSelector: ".discard-answer"
          ,immediatelyShowMarkdownHelp:true
          });


          }
          });














          draft saved

          draft discarded


















          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f202110%2fthrottle-redraw-requests-fired-from-pointer-events%23new-answer', 'question_page');
          }
          );

          Post as a guest















          Required, but never shown

























          1 Answer
          1






          active

          oldest

          votes








          1 Answer
          1






          active

          oldest

          votes









          active

          oldest

          votes






          active

          oldest

          votes









          0












          $begingroup$

          Limiting updates



          You are correct about mouse event rates, I have seen mouse event 2ms appart. However touch events seam to be in sync with the display 60FPS, I have never seen a touch event fire at rates higher than this (but that is a hardware attribute as far as I know)



          But that is besides the point. It is always a good idea to decouple the rendering from random events, be they user input, or from some other source. The display is driven at a fixed rate and to maintain good quality animations and presentation, keeping it tightly synced to that rate is important.



          Some style a code points.




          • window is the default object you don't need to use it. window.removeEventListener is the same as removeEventListener and conversly your function throttleRedraw is the same as window.throttleRedraw


          • It is not good to have statement block without delimiting parenthesis as it can easily be missed when making modifications. if (ondraw) ondraw(now); is better as if (ondraw) { ondraw(now) } the ; is optional as the line has ended in a }


          • You don't have to have a separate event handler for each event type. Many of the event properties are repeated and you end up duplicating code. Create a single handler that handles all the events of similar type.


          • The DOM does it all, but the DOM does it badly, adding and removing event handlers is just adding more work to the DOM and can be more efficiently handled by your JavaScript. Set handlers once and ignore events not needed


          • Too many comments. Comment only code that is not self evident, any other comments are just noise and make the code harder to read.



          Design



          From a design point I would not tie the mouse/touch event handlers so tightly with the rendering functionality. It is likely that there will be additional event like sources that require updated rendering. Completely decoupling the rendering from the pointer like events will make the system more flexible.



          Touch and mouse are abstractly the same, try to reduce the individual abstraction and use a common pointer like abstract to name and define behaviors.



          The flag absolute is an over complication. Just keep a pointer delta that contains the relative pointer motion and let the handling code pick which to use.



          Example



          The example decouples the pointer from the rendering.



          The pointer is created and listens to all relevant events and sets its properties x, y, down... and so on. The pointer also correct the coors to be relative to the target top left.



          The render start up and creates a pointer object setting its target to the canvas The main loop continues to run and will only update if there is a change to the pointer.



          The result is that the rendering becomes more complex, but that is offset by greatly reducing the pointer event handling code.



          This also means that you could add keyboard events, or have running animations (like selection box dash movement) and not be reliant on the mouse event handler to pass appropriate render calls



          Note that it listens to document events and filters out events not triggered by the active element. (THE snippet has modified behavior and does not allow event outside the snippet window. The event handler has the target test commented out to improve it a little, but it is not an accurate representation of how it works on a independent page).






          function createPointer(target) {
          var currentTarget;
          var bounds;
          const types = {
          mouse: 1, touch: 2, move: 1, up: 2, end: 2, cancel: 2, start: 0, down: 0,
          names : "touchcancel,touchend,touchmove,touchstart,mousemove,mousedown,mouseup".split(","),
          }
          const pointer = {
          delta: {x : 0, y : 0},
          pos: {x : 0, y : 0, coord(x,y) { this.x = x; this.y = y }},
          last: {x : 0, y : 0, set from(src) { this.x = src.x; this.y = src.y }},
          down : false,
          active : false,
          changed : false,
          set position(coord) {
          pointer.last.from = pointer.pos;
          pointer.pos.coord(coord.clientX- bounds.left + scrollX, coord.clientY - bounds.top + scrollY);
          pointer.delta.x = pointer.pos.x - pointer.last.x;
          pointer.delta.y = pointer.pos.y - pointer.last.y;
          },
          set target(element){
          if(!element){
          types.names.forEach(event => removeEventListener(event, events) );
          currentTarget = undefined;
          pointer.active = false;
          }else {
          if (!currentTarget) { types.names.forEach(event => addEventListener(event, events) ) }
          currentTarget = element;
          bounds = element.getBoundingClientRect()
          pointer.active = true;
          }
          pointer.down = false;
          pointer.changed = true;
          }
          }
          const events = event => {
          // if (event.target === currentTarget) {
          const type = types[event.type.substring(5)];
          pointer.position = types[event.type.substring(0,5)] === types.touch ? event.touches[0] : event;
          if (type === types.down) { pointer.down = true }
          else if (type === types.up) { pointer.down = false }
          pointer.changed = true;
          event.preventDefault();
          // }
          }
          pointer.target = target;
          return pointer;
          }


          const rendering = (()=> {
          requestAnimationFrame(mainLoop);
          const canvas = document.querySelector('canvas');
          const ctx = canvas.getContext('2d');
          const pointer = createPointer(canvas);
          var update = false;

          function as(src) { this.x = src.x; this.y = src.y }
          const drag = {
          active : false,
          start : {x : 0, y : 0, as},
          end : {x : 0, y : 0, as},
          draw() {
          ctx.fillStyle = "red";
          ctx.fillRect(
          Math.min(drag.start.x, drag.end.x), Math.min(drag.start.y, drag.end.y),
          Math.abs(drag.start.x - drag.end.x), Math.abs(drag.start.y - drag.end.y),
          )
          }
          }

          function checkPointer() {
          if (pointer.changed) {
          if (pointer.down) {
          if (!drag.active) {
          drag.start.as(pointer.pos);
          drag.active = true;
          }
          drag.end.as(pointer.pos);
          update = true;
          } else if (drag.active) {
          drag.end.as(pointer.pos);
          drag.active = false;
          update = true;
          }
          pointer.changed = false;
          }
          }

          function mainLoop(time) {
          checkPointer();
          if (update) {
          ctx.clearRect(0,0,canvas.width,canvas.height);
          drag.draw();
          update = false;
          }
          requestAnimationFrame(mainLoop);
          }
          })();

          <canvas width="400" height="400" style="border: 2px solid #888"></canvas>








          share|improve this answer











          $endgroup$













          • $begingroup$
            Just a quick reply at first glance: "Mouse events on a element have the annoying habit of only hearing events while the mouse is over the element." -- That's actually why I used window.addEventListener, to capture the move/up/cancel events at the highest stage of propagation. document can still be a problem if the page doesn't fill the whole window. I'll look through your example in more detail when I'm not on my phone. Thanks for your answer!
            $endgroup$
            – Máté Safranka
            Aug 21 '18 at 14:27










          • $begingroup$
            @MátéSafranka You are absolutely correct, I ran you code and at the time forgot that the snippets messed with the mouse event, and I just presumed. Will change the answer.
            $endgroup$
            – Blindman67
            Aug 21 '18 at 15:24
















          0












          $begingroup$

          Limiting updates



          You are correct about mouse event rates, I have seen mouse event 2ms appart. However touch events seam to be in sync with the display 60FPS, I have never seen a touch event fire at rates higher than this (but that is a hardware attribute as far as I know)



          But that is besides the point. It is always a good idea to decouple the rendering from random events, be they user input, or from some other source. The display is driven at a fixed rate and to maintain good quality animations and presentation, keeping it tightly synced to that rate is important.



          Some style a code points.




          • window is the default object you don't need to use it. window.removeEventListener is the same as removeEventListener and conversly your function throttleRedraw is the same as window.throttleRedraw


          • It is not good to have statement block without delimiting parenthesis as it can easily be missed when making modifications. if (ondraw) ondraw(now); is better as if (ondraw) { ondraw(now) } the ; is optional as the line has ended in a }


          • You don't have to have a separate event handler for each event type. Many of the event properties are repeated and you end up duplicating code. Create a single handler that handles all the events of similar type.


          • The DOM does it all, but the DOM does it badly, adding and removing event handlers is just adding more work to the DOM and can be more efficiently handled by your JavaScript. Set handlers once and ignore events not needed


          • Too many comments. Comment only code that is not self evident, any other comments are just noise and make the code harder to read.



          Design



          From a design point I would not tie the mouse/touch event handlers so tightly with the rendering functionality. It is likely that there will be additional event like sources that require updated rendering. Completely decoupling the rendering from the pointer like events will make the system more flexible.



          Touch and mouse are abstractly the same, try to reduce the individual abstraction and use a common pointer like abstract to name and define behaviors.



          The flag absolute is an over complication. Just keep a pointer delta that contains the relative pointer motion and let the handling code pick which to use.



          Example



          The example decouples the pointer from the rendering.



          The pointer is created and listens to all relevant events and sets its properties x, y, down... and so on. The pointer also correct the coors to be relative to the target top left.



          The render start up and creates a pointer object setting its target to the canvas The main loop continues to run and will only update if there is a change to the pointer.



          The result is that the rendering becomes more complex, but that is offset by greatly reducing the pointer event handling code.



          This also means that you could add keyboard events, or have running animations (like selection box dash movement) and not be reliant on the mouse event handler to pass appropriate render calls



          Note that it listens to document events and filters out events not triggered by the active element. (THE snippet has modified behavior and does not allow event outside the snippet window. The event handler has the target test commented out to improve it a little, but it is not an accurate representation of how it works on a independent page).






          function createPointer(target) {
          var currentTarget;
          var bounds;
          const types = {
          mouse: 1, touch: 2, move: 1, up: 2, end: 2, cancel: 2, start: 0, down: 0,
          names : "touchcancel,touchend,touchmove,touchstart,mousemove,mousedown,mouseup".split(","),
          }
          const pointer = {
          delta: {x : 0, y : 0},
          pos: {x : 0, y : 0, coord(x,y) { this.x = x; this.y = y }},
          last: {x : 0, y : 0, set from(src) { this.x = src.x; this.y = src.y }},
          down : false,
          active : false,
          changed : false,
          set position(coord) {
          pointer.last.from = pointer.pos;
          pointer.pos.coord(coord.clientX- bounds.left + scrollX, coord.clientY - bounds.top + scrollY);
          pointer.delta.x = pointer.pos.x - pointer.last.x;
          pointer.delta.y = pointer.pos.y - pointer.last.y;
          },
          set target(element){
          if(!element){
          types.names.forEach(event => removeEventListener(event, events) );
          currentTarget = undefined;
          pointer.active = false;
          }else {
          if (!currentTarget) { types.names.forEach(event => addEventListener(event, events) ) }
          currentTarget = element;
          bounds = element.getBoundingClientRect()
          pointer.active = true;
          }
          pointer.down = false;
          pointer.changed = true;
          }
          }
          const events = event => {
          // if (event.target === currentTarget) {
          const type = types[event.type.substring(5)];
          pointer.position = types[event.type.substring(0,5)] === types.touch ? event.touches[0] : event;
          if (type === types.down) { pointer.down = true }
          else if (type === types.up) { pointer.down = false }
          pointer.changed = true;
          event.preventDefault();
          // }
          }
          pointer.target = target;
          return pointer;
          }


          const rendering = (()=> {
          requestAnimationFrame(mainLoop);
          const canvas = document.querySelector('canvas');
          const ctx = canvas.getContext('2d');
          const pointer = createPointer(canvas);
          var update = false;

          function as(src) { this.x = src.x; this.y = src.y }
          const drag = {
          active : false,
          start : {x : 0, y : 0, as},
          end : {x : 0, y : 0, as},
          draw() {
          ctx.fillStyle = "red";
          ctx.fillRect(
          Math.min(drag.start.x, drag.end.x), Math.min(drag.start.y, drag.end.y),
          Math.abs(drag.start.x - drag.end.x), Math.abs(drag.start.y - drag.end.y),
          )
          }
          }

          function checkPointer() {
          if (pointer.changed) {
          if (pointer.down) {
          if (!drag.active) {
          drag.start.as(pointer.pos);
          drag.active = true;
          }
          drag.end.as(pointer.pos);
          update = true;
          } else if (drag.active) {
          drag.end.as(pointer.pos);
          drag.active = false;
          update = true;
          }
          pointer.changed = false;
          }
          }

          function mainLoop(time) {
          checkPointer();
          if (update) {
          ctx.clearRect(0,0,canvas.width,canvas.height);
          drag.draw();
          update = false;
          }
          requestAnimationFrame(mainLoop);
          }
          })();

          <canvas width="400" height="400" style="border: 2px solid #888"></canvas>








          share|improve this answer











          $endgroup$













          • $begingroup$
            Just a quick reply at first glance: "Mouse events on a element have the annoying habit of only hearing events while the mouse is over the element." -- That's actually why I used window.addEventListener, to capture the move/up/cancel events at the highest stage of propagation. document can still be a problem if the page doesn't fill the whole window. I'll look through your example in more detail when I'm not on my phone. Thanks for your answer!
            $endgroup$
            – Máté Safranka
            Aug 21 '18 at 14:27










          • $begingroup$
            @MátéSafranka You are absolutely correct, I ran you code and at the time forgot that the snippets messed with the mouse event, and I just presumed. Will change the answer.
            $endgroup$
            – Blindman67
            Aug 21 '18 at 15:24














          0












          0








          0





          $begingroup$

          Limiting updates



          You are correct about mouse event rates, I have seen mouse event 2ms appart. However touch events seam to be in sync with the display 60FPS, I have never seen a touch event fire at rates higher than this (but that is a hardware attribute as far as I know)



          But that is besides the point. It is always a good idea to decouple the rendering from random events, be they user input, or from some other source. The display is driven at a fixed rate and to maintain good quality animations and presentation, keeping it tightly synced to that rate is important.



          Some style a code points.




          • window is the default object you don't need to use it. window.removeEventListener is the same as removeEventListener and conversly your function throttleRedraw is the same as window.throttleRedraw


          • It is not good to have statement block without delimiting parenthesis as it can easily be missed when making modifications. if (ondraw) ondraw(now); is better as if (ondraw) { ondraw(now) } the ; is optional as the line has ended in a }


          • You don't have to have a separate event handler for each event type. Many of the event properties are repeated and you end up duplicating code. Create a single handler that handles all the events of similar type.


          • The DOM does it all, but the DOM does it badly, adding and removing event handlers is just adding more work to the DOM and can be more efficiently handled by your JavaScript. Set handlers once and ignore events not needed


          • Too many comments. Comment only code that is not self evident, any other comments are just noise and make the code harder to read.



          Design



          From a design point I would not tie the mouse/touch event handlers so tightly with the rendering functionality. It is likely that there will be additional event like sources that require updated rendering. Completely decoupling the rendering from the pointer like events will make the system more flexible.



          Touch and mouse are abstractly the same, try to reduce the individual abstraction and use a common pointer like abstract to name and define behaviors.



          The flag absolute is an over complication. Just keep a pointer delta that contains the relative pointer motion and let the handling code pick which to use.



          Example



          The example decouples the pointer from the rendering.



          The pointer is created and listens to all relevant events and sets its properties x, y, down... and so on. The pointer also correct the coors to be relative to the target top left.



          The render start up and creates a pointer object setting its target to the canvas The main loop continues to run and will only update if there is a change to the pointer.



          The result is that the rendering becomes more complex, but that is offset by greatly reducing the pointer event handling code.



          This also means that you could add keyboard events, or have running animations (like selection box dash movement) and not be reliant on the mouse event handler to pass appropriate render calls



          Note that it listens to document events and filters out events not triggered by the active element. (THE snippet has modified behavior and does not allow event outside the snippet window. The event handler has the target test commented out to improve it a little, but it is not an accurate representation of how it works on a independent page).






          function createPointer(target) {
          var currentTarget;
          var bounds;
          const types = {
          mouse: 1, touch: 2, move: 1, up: 2, end: 2, cancel: 2, start: 0, down: 0,
          names : "touchcancel,touchend,touchmove,touchstart,mousemove,mousedown,mouseup".split(","),
          }
          const pointer = {
          delta: {x : 0, y : 0},
          pos: {x : 0, y : 0, coord(x,y) { this.x = x; this.y = y }},
          last: {x : 0, y : 0, set from(src) { this.x = src.x; this.y = src.y }},
          down : false,
          active : false,
          changed : false,
          set position(coord) {
          pointer.last.from = pointer.pos;
          pointer.pos.coord(coord.clientX- bounds.left + scrollX, coord.clientY - bounds.top + scrollY);
          pointer.delta.x = pointer.pos.x - pointer.last.x;
          pointer.delta.y = pointer.pos.y - pointer.last.y;
          },
          set target(element){
          if(!element){
          types.names.forEach(event => removeEventListener(event, events) );
          currentTarget = undefined;
          pointer.active = false;
          }else {
          if (!currentTarget) { types.names.forEach(event => addEventListener(event, events) ) }
          currentTarget = element;
          bounds = element.getBoundingClientRect()
          pointer.active = true;
          }
          pointer.down = false;
          pointer.changed = true;
          }
          }
          const events = event => {
          // if (event.target === currentTarget) {
          const type = types[event.type.substring(5)];
          pointer.position = types[event.type.substring(0,5)] === types.touch ? event.touches[0] : event;
          if (type === types.down) { pointer.down = true }
          else if (type === types.up) { pointer.down = false }
          pointer.changed = true;
          event.preventDefault();
          // }
          }
          pointer.target = target;
          return pointer;
          }


          const rendering = (()=> {
          requestAnimationFrame(mainLoop);
          const canvas = document.querySelector('canvas');
          const ctx = canvas.getContext('2d');
          const pointer = createPointer(canvas);
          var update = false;

          function as(src) { this.x = src.x; this.y = src.y }
          const drag = {
          active : false,
          start : {x : 0, y : 0, as},
          end : {x : 0, y : 0, as},
          draw() {
          ctx.fillStyle = "red";
          ctx.fillRect(
          Math.min(drag.start.x, drag.end.x), Math.min(drag.start.y, drag.end.y),
          Math.abs(drag.start.x - drag.end.x), Math.abs(drag.start.y - drag.end.y),
          )
          }
          }

          function checkPointer() {
          if (pointer.changed) {
          if (pointer.down) {
          if (!drag.active) {
          drag.start.as(pointer.pos);
          drag.active = true;
          }
          drag.end.as(pointer.pos);
          update = true;
          } else if (drag.active) {
          drag.end.as(pointer.pos);
          drag.active = false;
          update = true;
          }
          pointer.changed = false;
          }
          }

          function mainLoop(time) {
          checkPointer();
          if (update) {
          ctx.clearRect(0,0,canvas.width,canvas.height);
          drag.draw();
          update = false;
          }
          requestAnimationFrame(mainLoop);
          }
          })();

          <canvas width="400" height="400" style="border: 2px solid #888"></canvas>








          share|improve this answer











          $endgroup$



          Limiting updates



          You are correct about mouse event rates, I have seen mouse event 2ms appart. However touch events seam to be in sync with the display 60FPS, I have never seen a touch event fire at rates higher than this (but that is a hardware attribute as far as I know)



          But that is besides the point. It is always a good idea to decouple the rendering from random events, be they user input, or from some other source. The display is driven at a fixed rate and to maintain good quality animations and presentation, keeping it tightly synced to that rate is important.



          Some style a code points.




          • window is the default object you don't need to use it. window.removeEventListener is the same as removeEventListener and conversly your function throttleRedraw is the same as window.throttleRedraw


          • It is not good to have statement block without delimiting parenthesis as it can easily be missed when making modifications. if (ondraw) ondraw(now); is better as if (ondraw) { ondraw(now) } the ; is optional as the line has ended in a }


          • You don't have to have a separate event handler for each event type. Many of the event properties are repeated and you end up duplicating code. Create a single handler that handles all the events of similar type.


          • The DOM does it all, but the DOM does it badly, adding and removing event handlers is just adding more work to the DOM and can be more efficiently handled by your JavaScript. Set handlers once and ignore events not needed


          • Too many comments. Comment only code that is not self evident, any other comments are just noise and make the code harder to read.



          Design



          From a design point I would not tie the mouse/touch event handlers so tightly with the rendering functionality. It is likely that there will be additional event like sources that require updated rendering. Completely decoupling the rendering from the pointer like events will make the system more flexible.



          Touch and mouse are abstractly the same, try to reduce the individual abstraction and use a common pointer like abstract to name and define behaviors.



          The flag absolute is an over complication. Just keep a pointer delta that contains the relative pointer motion and let the handling code pick which to use.



          Example



          The example decouples the pointer from the rendering.



          The pointer is created and listens to all relevant events and sets its properties x, y, down... and so on. The pointer also correct the coors to be relative to the target top left.



          The render start up and creates a pointer object setting its target to the canvas The main loop continues to run and will only update if there is a change to the pointer.



          The result is that the rendering becomes more complex, but that is offset by greatly reducing the pointer event handling code.



          This also means that you could add keyboard events, or have running animations (like selection box dash movement) and not be reliant on the mouse event handler to pass appropriate render calls



          Note that it listens to document events and filters out events not triggered by the active element. (THE snippet has modified behavior and does not allow event outside the snippet window. The event handler has the target test commented out to improve it a little, but it is not an accurate representation of how it works on a independent page).






          function createPointer(target) {
          var currentTarget;
          var bounds;
          const types = {
          mouse: 1, touch: 2, move: 1, up: 2, end: 2, cancel: 2, start: 0, down: 0,
          names : "touchcancel,touchend,touchmove,touchstart,mousemove,mousedown,mouseup".split(","),
          }
          const pointer = {
          delta: {x : 0, y : 0},
          pos: {x : 0, y : 0, coord(x,y) { this.x = x; this.y = y }},
          last: {x : 0, y : 0, set from(src) { this.x = src.x; this.y = src.y }},
          down : false,
          active : false,
          changed : false,
          set position(coord) {
          pointer.last.from = pointer.pos;
          pointer.pos.coord(coord.clientX- bounds.left + scrollX, coord.clientY - bounds.top + scrollY);
          pointer.delta.x = pointer.pos.x - pointer.last.x;
          pointer.delta.y = pointer.pos.y - pointer.last.y;
          },
          set target(element){
          if(!element){
          types.names.forEach(event => removeEventListener(event, events) );
          currentTarget = undefined;
          pointer.active = false;
          }else {
          if (!currentTarget) { types.names.forEach(event => addEventListener(event, events) ) }
          currentTarget = element;
          bounds = element.getBoundingClientRect()
          pointer.active = true;
          }
          pointer.down = false;
          pointer.changed = true;
          }
          }
          const events = event => {
          // if (event.target === currentTarget) {
          const type = types[event.type.substring(5)];
          pointer.position = types[event.type.substring(0,5)] === types.touch ? event.touches[0] : event;
          if (type === types.down) { pointer.down = true }
          else if (type === types.up) { pointer.down = false }
          pointer.changed = true;
          event.preventDefault();
          // }
          }
          pointer.target = target;
          return pointer;
          }


          const rendering = (()=> {
          requestAnimationFrame(mainLoop);
          const canvas = document.querySelector('canvas');
          const ctx = canvas.getContext('2d');
          const pointer = createPointer(canvas);
          var update = false;

          function as(src) { this.x = src.x; this.y = src.y }
          const drag = {
          active : false,
          start : {x : 0, y : 0, as},
          end : {x : 0, y : 0, as},
          draw() {
          ctx.fillStyle = "red";
          ctx.fillRect(
          Math.min(drag.start.x, drag.end.x), Math.min(drag.start.y, drag.end.y),
          Math.abs(drag.start.x - drag.end.x), Math.abs(drag.start.y - drag.end.y),
          )
          }
          }

          function checkPointer() {
          if (pointer.changed) {
          if (pointer.down) {
          if (!drag.active) {
          drag.start.as(pointer.pos);
          drag.active = true;
          }
          drag.end.as(pointer.pos);
          update = true;
          } else if (drag.active) {
          drag.end.as(pointer.pos);
          drag.active = false;
          update = true;
          }
          pointer.changed = false;
          }
          }

          function mainLoop(time) {
          checkPointer();
          if (update) {
          ctx.clearRect(0,0,canvas.width,canvas.height);
          drag.draw();
          update = false;
          }
          requestAnimationFrame(mainLoop);
          }
          })();

          <canvas width="400" height="400" style="border: 2px solid #888"></canvas>








          function createPointer(target) {
          var currentTarget;
          var bounds;
          const types = {
          mouse: 1, touch: 2, move: 1, up: 2, end: 2, cancel: 2, start: 0, down: 0,
          names : "touchcancel,touchend,touchmove,touchstart,mousemove,mousedown,mouseup".split(","),
          }
          const pointer = {
          delta: {x : 0, y : 0},
          pos: {x : 0, y : 0, coord(x,y) { this.x = x; this.y = y }},
          last: {x : 0, y : 0, set from(src) { this.x = src.x; this.y = src.y }},
          down : false,
          active : false,
          changed : false,
          set position(coord) {
          pointer.last.from = pointer.pos;
          pointer.pos.coord(coord.clientX- bounds.left + scrollX, coord.clientY - bounds.top + scrollY);
          pointer.delta.x = pointer.pos.x - pointer.last.x;
          pointer.delta.y = pointer.pos.y - pointer.last.y;
          },
          set target(element){
          if(!element){
          types.names.forEach(event => removeEventListener(event, events) );
          currentTarget = undefined;
          pointer.active = false;
          }else {
          if (!currentTarget) { types.names.forEach(event => addEventListener(event, events) ) }
          currentTarget = element;
          bounds = element.getBoundingClientRect()
          pointer.active = true;
          }
          pointer.down = false;
          pointer.changed = true;
          }
          }
          const events = event => {
          // if (event.target === currentTarget) {
          const type = types[event.type.substring(5)];
          pointer.position = types[event.type.substring(0,5)] === types.touch ? event.touches[0] : event;
          if (type === types.down) { pointer.down = true }
          else if (type === types.up) { pointer.down = false }
          pointer.changed = true;
          event.preventDefault();
          // }
          }
          pointer.target = target;
          return pointer;
          }


          const rendering = (()=> {
          requestAnimationFrame(mainLoop);
          const canvas = document.querySelector('canvas');
          const ctx = canvas.getContext('2d');
          const pointer = createPointer(canvas);
          var update = false;

          function as(src) { this.x = src.x; this.y = src.y }
          const drag = {
          active : false,
          start : {x : 0, y : 0, as},
          end : {x : 0, y : 0, as},
          draw() {
          ctx.fillStyle = "red";
          ctx.fillRect(
          Math.min(drag.start.x, drag.end.x), Math.min(drag.start.y, drag.end.y),
          Math.abs(drag.start.x - drag.end.x), Math.abs(drag.start.y - drag.end.y),
          )
          }
          }

          function checkPointer() {
          if (pointer.changed) {
          if (pointer.down) {
          if (!drag.active) {
          drag.start.as(pointer.pos);
          drag.active = true;
          }
          drag.end.as(pointer.pos);
          update = true;
          } else if (drag.active) {
          drag.end.as(pointer.pos);
          drag.active = false;
          update = true;
          }
          pointer.changed = false;
          }
          }

          function mainLoop(time) {
          checkPointer();
          if (update) {
          ctx.clearRect(0,0,canvas.width,canvas.height);
          drag.draw();
          update = false;
          }
          requestAnimationFrame(mainLoop);
          }
          })();

          <canvas width="400" height="400" style="border: 2px solid #888"></canvas>





          function createPointer(target) {
          var currentTarget;
          var bounds;
          const types = {
          mouse: 1, touch: 2, move: 1, up: 2, end: 2, cancel: 2, start: 0, down: 0,
          names : "touchcancel,touchend,touchmove,touchstart,mousemove,mousedown,mouseup".split(","),
          }
          const pointer = {
          delta: {x : 0, y : 0},
          pos: {x : 0, y : 0, coord(x,y) { this.x = x; this.y = y }},
          last: {x : 0, y : 0, set from(src) { this.x = src.x; this.y = src.y }},
          down : false,
          active : false,
          changed : false,
          set position(coord) {
          pointer.last.from = pointer.pos;
          pointer.pos.coord(coord.clientX- bounds.left + scrollX, coord.clientY - bounds.top + scrollY);
          pointer.delta.x = pointer.pos.x - pointer.last.x;
          pointer.delta.y = pointer.pos.y - pointer.last.y;
          },
          set target(element){
          if(!element){
          types.names.forEach(event => removeEventListener(event, events) );
          currentTarget = undefined;
          pointer.active = false;
          }else {
          if (!currentTarget) { types.names.forEach(event => addEventListener(event, events) ) }
          currentTarget = element;
          bounds = element.getBoundingClientRect()
          pointer.active = true;
          }
          pointer.down = false;
          pointer.changed = true;
          }
          }
          const events = event => {
          // if (event.target === currentTarget) {
          const type = types[event.type.substring(5)];
          pointer.position = types[event.type.substring(0,5)] === types.touch ? event.touches[0] : event;
          if (type === types.down) { pointer.down = true }
          else if (type === types.up) { pointer.down = false }
          pointer.changed = true;
          event.preventDefault();
          // }
          }
          pointer.target = target;
          return pointer;
          }


          const rendering = (()=> {
          requestAnimationFrame(mainLoop);
          const canvas = document.querySelector('canvas');
          const ctx = canvas.getContext('2d');
          const pointer = createPointer(canvas);
          var update = false;

          function as(src) { this.x = src.x; this.y = src.y }
          const drag = {
          active : false,
          start : {x : 0, y : 0, as},
          end : {x : 0, y : 0, as},
          draw() {
          ctx.fillStyle = "red";
          ctx.fillRect(
          Math.min(drag.start.x, drag.end.x), Math.min(drag.start.y, drag.end.y),
          Math.abs(drag.start.x - drag.end.x), Math.abs(drag.start.y - drag.end.y),
          )
          }
          }

          function checkPointer() {
          if (pointer.changed) {
          if (pointer.down) {
          if (!drag.active) {
          drag.start.as(pointer.pos);
          drag.active = true;
          }
          drag.end.as(pointer.pos);
          update = true;
          } else if (drag.active) {
          drag.end.as(pointer.pos);
          drag.active = false;
          update = true;
          }
          pointer.changed = false;
          }
          }

          function mainLoop(time) {
          checkPointer();
          if (update) {
          ctx.clearRect(0,0,canvas.width,canvas.height);
          drag.draw();
          update = false;
          }
          requestAnimationFrame(mainLoop);
          }
          })();

          <canvas width="400" height="400" style="border: 2px solid #888"></canvas>






          share|improve this answer














          share|improve this answer



          share|improve this answer








          edited Aug 21 '18 at 15:27

























          answered Aug 21 '18 at 14:08









          Blindman67Blindman67

          9,8171622




          9,8171622












          • $begingroup$
            Just a quick reply at first glance: "Mouse events on a element have the annoying habit of only hearing events while the mouse is over the element." -- That's actually why I used window.addEventListener, to capture the move/up/cancel events at the highest stage of propagation. document can still be a problem if the page doesn't fill the whole window. I'll look through your example in more detail when I'm not on my phone. Thanks for your answer!
            $endgroup$
            – Máté Safranka
            Aug 21 '18 at 14:27










          • $begingroup$
            @MátéSafranka You are absolutely correct, I ran you code and at the time forgot that the snippets messed with the mouse event, and I just presumed. Will change the answer.
            $endgroup$
            – Blindman67
            Aug 21 '18 at 15:24


















          • $begingroup$
            Just a quick reply at first glance: "Mouse events on a element have the annoying habit of only hearing events while the mouse is over the element." -- That's actually why I used window.addEventListener, to capture the move/up/cancel events at the highest stage of propagation. document can still be a problem if the page doesn't fill the whole window. I'll look through your example in more detail when I'm not on my phone. Thanks for your answer!
            $endgroup$
            – Máté Safranka
            Aug 21 '18 at 14:27










          • $begingroup$
            @MátéSafranka You are absolutely correct, I ran you code and at the time forgot that the snippets messed with the mouse event, and I just presumed. Will change the answer.
            $endgroup$
            – Blindman67
            Aug 21 '18 at 15:24
















          $begingroup$
          Just a quick reply at first glance: "Mouse events on a element have the annoying habit of only hearing events while the mouse is over the element." -- That's actually why I used window.addEventListener, to capture the move/up/cancel events at the highest stage of propagation. document can still be a problem if the page doesn't fill the whole window. I'll look through your example in more detail when I'm not on my phone. Thanks for your answer!
          $endgroup$
          – Máté Safranka
          Aug 21 '18 at 14:27




          $begingroup$
          Just a quick reply at first glance: "Mouse events on a element have the annoying habit of only hearing events while the mouse is over the element." -- That's actually why I used window.addEventListener, to capture the move/up/cancel events at the highest stage of propagation. document can still be a problem if the page doesn't fill the whole window. I'll look through your example in more detail when I'm not on my phone. Thanks for your answer!
          $endgroup$
          – Máté Safranka
          Aug 21 '18 at 14:27












          $begingroup$
          @MátéSafranka You are absolutely correct, I ran you code and at the time forgot that the snippets messed with the mouse event, and I just presumed. Will change the answer.
          $endgroup$
          – Blindman67
          Aug 21 '18 at 15:24




          $begingroup$
          @MátéSafranka You are absolutely correct, I ran you code and at the time forgot that the snippets messed with the mouse event, and I just presumed. Will change the answer.
          $endgroup$
          – Blindman67
          Aug 21 '18 at 15:24


















          draft saved

          draft discarded




















































          Thanks for contributing an answer to Code Review Stack Exchange!


          • Please be sure to answer the question. Provide details and share your research!

          But avoid



          • Asking for help, clarification, or responding to other answers.

          • Making statements based on opinion; back them up with references or personal experience.


          Use MathJax to format equations. MathJax reference.


          To learn more, see our tips on writing great answers.




          draft saved


          draft discarded














          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f202110%2fthrottle-redraw-requests-fired-from-pointer-events%23new-answer', 'question_page');
          }
          );

          Post as a guest















          Required, but never shown





















































          Required, but never shown














          Required, but never shown












          Required, but never shown







          Required, but never shown

































          Required, but never shown














          Required, but never shown












          Required, but never shown







          Required, but never shown







          Popular posts from this blog

          is 'sed' thread safeWhat should someone know about using Python scripts in the shell?Nexenta bash script uses...

          How do i solve the “ No module named 'mlxtend' ” issue on Jupyter?

          Pilgersdorf Inhaltsverzeichnis Geografie | Geschichte | Bevölkerungsentwicklung | Politik | Kultur...