flekschas / regl-scatterplot

Scalable WebGL-based scatter plot library build with Regl
https://flekschas.github.io/regl-scatterplot/
MIT License
191 stars 24 forks source link

Immediate redraw #161

Open funmaker opened 10 months ago

funmaker commented 10 months ago

Hi. When I am using browser's printing function, the scatterplot does not redraw itself and instead just stretches last canvas content into the new one. It is possible to detect printing using window.matchMedia but calling redraw function doesn't do anything since redraws are queued for next animation frames. It is also not possible to hold printing until the next frame, it happens immediately after related events get fired. It would be nice to be able to redraw scatterplot immediately to update it for printing specific size/aspectratio changes.

Also redraw function is missing from documentation and draw can't be called with empty arguments according to typescript types, while being allowed according to documentation.

flekschas commented 10 months ago

It is possible to detect printing using window.matchMedia but calling redraw function doesn't do anything since redraws are queued for next animation frames.

I'm not sure. It might be possible but I haven't had the need to support it.

I'm not yet sure I fully understand the issue. When you print a page that contains a scatter plot, the issue is that the scatter plot is printed according to the canvas size prior to printing? If this is the case, I'm not sure I know a solution from the top of my head because regl-scatterplot automatically redraws every time the canvas resizes. That means, in case of printing, the resize event is either not fired or the printing page is finished before the next animation frame is available.

Do you have an example of what the issue you're running into looks like?

Also redraw function is missing from documentation and draw can't be called with empty arguments according to typescript types, while being allowed according to documentation.

Good catch! Always feel free to open a PR to fix such issues. I fixed the README.

funmaker commented 10 months ago

If this is the case, I'm not sure I know a solution from the top of my head because regl-scatterplot automatically redraws every time the canvas resizes That means, in case of printing, the resize event is either not fired or the printing page is finished before the next animation frame is available. Do you have an example of what the issue you're running into looks like?

It doesn't fire. If I go to scatterplot demo, make window size small and press print, the result looks low quality and stretched. Regl Scatterplot.pdf pdf related. Also I don't think reglscatterplot redraws immedietaly after resize, only sets the draw flag and redraws on next animation frame. Animation frames do not fire while printing.

The solution would be to expose function that can redraw the canvas immediately. As a workaround I am capturing requestAnimationFrame calls from regl and trigger them myself and it works, but it's very ugly.

flekschas commented 10 months ago

I don't want to expose a new function that's should practically only be used in one edge case. I.e., immediate re-rendering for printing. Instead, it'd be better to if regl-scatterplot can capture the printing event internally and then do a forced re-rendering. As it sounds like you've basically solved that problem already (even if it's ugly) it'd be great if you could share your solution as a starting point for a fix.

funmaker commented 10 months ago

I doubt my workaround will be of any use to you. It consists of two parts, first I globally capture all requestAnimationFrame calls to expose new function that lets me trigger all requested animation frames:

const origRequestAnimationFrame = window.requestAnimationFrame;
const origCancelAnimationFrame = window.cancelAnimationFrame;

const frameCallbacks = new Map<number, FrameRequestCallback>();

window.requestAnimationFrame = callback => {
  const id = origRequestAnimationFrame(time => {
    frameCallbacks.delete(id);
    callback(time);
  });
  frameCallbacks.set(id, callback);
  return id;
};

window.cancelAnimationFrame = id => {
  origCancelAnimationFrame(id);
  frameCallbacks.delete(id);
};

export function triggerAnimationFrame(time: number) {
  const callbacks = [...frameCallbacks.values()]; // Map.values is an iterator, can cause infinite loops
  for (const callback of callbacks) {
    try {
      callback(time);
    } catch (error) {
      console.error(error);
    }
  }
}

Then since currently regl scatterplot renderer is either limited to the size of the window or to fixed framebuffer size(couldn't find any way to resize a custom renderer), I create a new renderer that is a proxy that exposes either default renderer or a renderer with fixed width and height of what I would expect on A3 @ 300 DPI, depending on whenever the browser is doing a printing pass or regular one.

const printWidth = 4556;
const printHeight = 1646;
const defaultRenderer = createRenderer();
const printCanvas = document.createElement('canvas');
printCanvas.width = printWidth;
printCanvas.height = printHeight;
const printRenderer = createRenderer({ canvas: printCanvas });
const smartRenderer = new Proxy(defaultRenderer, {
  get: (_target, property) =>
    window.matchMedia('print')
      ? (printRenderer as any)[property]
      : (defaultRenderer as any)[property],
  set: (_target, property, value) =>
    window.matchMedia('print')
      ? ((printRenderer as any)[property] = value)
      : ((defaultRenderer as any)[property] = value),
});

And then I watch media changes and trigger rerender with some fixed resolution.

    const onPrint = (event: MediaQueryListEvent) => {
      if (event.matches) {
        const printPointScale = printHeight / canvas.height;
        const defaultPointSize = regl.get('pointSize');
        const printPointSize = Array.isArray(defaultPointSize)
          ? defaultPointSize.map(size => size * printPointScale)
          : defaultPointSize * printPointScale;

        regl.set({
          width: printWidth,
          height: printHeight,
          aspectRatio: printWidth / printHeight,
          pointSize: printPointSize,
        });

        regl.redraw();
        triggerAnimationFrame(-1);

        regl.set({ pointSize: defaultPointSize });
      } else {
        onResize();
      }
    };

    const mediaQueryList = window.matchMedia('print');
    mediaQueryList.addEventListener('change', onPrint);

I don't want to expose a new function

You can just add a flag to redraw/draw function that would trigger rerender immediately instead of setting draw flag to true. Just moving the onFrame callback to some named function would probably do the trick.