Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve export functionality #207

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 1.12.0

- Feat: add support for adjusting the anti-aliasing via `scatterplot.set({ antiAliasing: 1 })`. ([#175](https://github.com/flekschas/regl-scatterplot/issues/175))
- Feat: add support for aligning points with the pixel grid via `scatterplot.set({ pixelAligned: true })`. ([#175](https://github.com/flekschas/regl-scatterplot/issues/175))
- Feat: enhance `scatterplot.export()` by allowing to adjust the scale, anti-aliasing, and pixel alignment. Note that when customizing the render setting for export, the function returns a promise that resolves into `ImageData`.
- Feat: expose `resize()` method of the `renderer`.

## 1.11.4

- Fix: allow setting the lasso long press indicator parent element
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -707,9 +707,12 @@ Sets the view back to the initially defined view. This will trigger a `view` eve

**Arguments:**

- `options` is an object for customizing how to export. See [regl.read()](https://github.com/regl-project/regl/blob/master/API.md#reading-pixels) for details.
- `options` is an object for customizing the render settings during the export:
- `scale`: is a float number allowning to adjust the exported image size
- `antiAliasing`: is a float allowing to adjust the anti-aliasing factor
- `pixelAligned`: is a Boolean allowing to adjust the point alignment with the pixel grid

**Returns:** an object with three properties: `pixels`, `width`, and `height`. The `pixels` is a `Uint8ClampedArray`.
**Returns:** an [`ImageData`](https://developer.mozilla.org/en-US/docs/Web/API/ImageData) object if `option` is `undefined`. Otherwise it returns a Promise resolving to an [`ImageData`](https://developer.mozilla.org/en-US/docs/Web/API/ImageData) object.

<a name="scatterplot.subscribe" href="#scatterplot.subscribe">#</a> scatterplot.<b>subscribe</b>(<i>eventName</i>, <i>eventHandler</i>)

Expand Down Expand Up @@ -818,6 +821,8 @@ can be read and written via [`scatterplot.get()`](#scatterplot.get) and [`scatte
| annotationLineColor | string or quadruple | `[1, 1, 1, 0.1]` | hex, rgb, rgba | `true` | `false` |
| annotationLineWidth | number | `1` | | `true` | `false` |
| annotationHVLineLimit | number | `1000` | the extent of horizontal or vertical lines | `true` | `false` |
| antiAliasing | number | `0.5` | higher values result in more blurry points | `true` | `false` |
| pixelAligned | number | `false` | if true, points are aligned with the pixel grid | `true` | `false` |

<a name="property-notes" href="#property-notes">#</a> <b>Notes:</b>

Expand Down
2 changes: 2 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ export const W_NAMES = new Set(['w', 'valueW', 'valueB', 'value2', 'value']);
export const DEFAULT_IMAGE_LOAD_TIMEOUT = 15000;
export const DEFAULT_SPATIAL_INDEX_USE_WORKER = undefined;
export const DEFAULT_CAMERA_IS_FIXED = false;
export const DEFAULT_ANTI_ALIASING = 0.5;
export const DEFAULT_PIXEL_ALIGNED = false;

// Error messages
export const ERROR_POINTS_NOT_DRAWN = 'Points have not been drawn';
127 changes: 123 additions & 4 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
DEFAULT_ANNOTATION_HVLINE_LIMIT,
DEFAULT_ANNOTATION_LINE_COLOR,
DEFAULT_ANNOTATION_LINE_WIDTH,
DEFAULT_ANTI_ALIASING,
DEFAULT_BACKGROUND_IMAGE,
DEFAULT_CAMERA_IS_FIXED,
DEFAULT_COLOR_ACTIVE,
Expand Down Expand Up @@ -70,6 +71,7 @@ import {
DEFAULT_OPACITY_INACTIVE_MAX,
DEFAULT_OPACITY_INACTIVE_SCALE,
DEFAULT_PERFORMANCE_MODE,
DEFAULT_PIXEL_ALIGNED,
DEFAULT_POINT_CONNECTION_COLOR_ACTIVE,
DEFAULT_POINT_CONNECTION_COLOR_BY,
DEFAULT_POINT_CONNECTION_COLOR_HOVER,
Expand Down Expand Up @@ -234,6 +236,8 @@ const createScatterplot = (

let {
renderer,
antiAliasing = DEFAULT_ANTI_ALIASING,
pixelAligned = DEFAULT_PIXEL_ALIGNED,
backgroundColor = DEFAULT_COLOR_BG,
backgroundImage = DEFAULT_BACKGROUND_IMAGE,
canvas = document.createElement('canvas'),
Expand Down Expand Up @@ -1464,6 +1468,7 @@ const createScatterplot = (
);
};

const getAntiAliasing = () => antiAliasing;
const getResolution = () => [canvas.width, canvas.height];
const getBackgroundImage = () => backgroundImage;
const getColorTex = () => colorTex;
Expand Down Expand Up @@ -1519,6 +1524,7 @@ const createScatterplot = (
const getIsOpacityByDensity = () => +(opacityBy === 'density');
const getIsSizedByZ = () => +(sizeBy === 'valueZ');
const getIsSizedByW = () => +(sizeBy === 'valueW');
const getIsPixelAligned = () => +pixelAligned;
const getColorMultiplicator = () => {
if (colorBy === 'valueZ') {
return valueZDataType === CONTINUOUS ? pointColor.length - 1 : 1;
Expand Down Expand Up @@ -1632,6 +1638,7 @@ const createScatterplot = (
},

uniforms: {
antiAliasing: getAntiAliasing,
resolution: getResolution,
modelViewProjection: getModelViewProjection,
devicePixelRatio: getDevicePixelRatio,
Expand All @@ -1656,11 +1663,14 @@ const createScatterplot = (
isOpacityByDensity: getIsOpacityByDensity,
isSizedByZ: getIsSizedByZ,
isSizedByW: getIsSizedByW,
isPixelAligned: getIsPixelAligned,
colorMultiplicator: getColorMultiplicator,
opacityMultiplicator: getOpacityMultiplicator,
opacityDensity: getOpacityDensity,
sizeMultiplicator: getSizeMultiplicator,
numColorStates: COLOR_NUM_STATES,
drawingBufferWidth: (context) => context.drawingBufferWidth,
drawingBufferHeight: (context) => context.drawingBufferHeight,
},

count: getNumPoints,
Expand Down Expand Up @@ -3273,6 +3283,14 @@ const createScatterplot = (
renderer.gamma = newGamma;
};

const setAntiAliasing = (newAntiAliasing) => {
antiAliasing = Number(newAntiAliasing) || 0.5;
};

const setPixelAligned = (newPixelAligned) => {
pixelAligned = Boolean(newPixelAligned);
};

/** @type {<Key extends keyof import('./types').Properties>(property: Key) => import('./types').Properties[Key] } */
const get = (property) => {
checkDeprecations({ property: true });
Expand Down Expand Up @@ -3608,10 +3626,18 @@ const createScatterplot = (
return annotationHVLineLimit;
}

if (property === 'antiAliasing') {
return antiAliasing;
}

if (property === 'pixelAligned') {
return pixelAligned;
}

return undefined;
};

/** @type {(properties: Partial<import('./types').Settable>) => void} */
/** @type {(properties: Partial<import('./types').Settable>) => Promise<void>} */
const set = (properties = {}) => {
checkDeprecations(properties);

Expand Down Expand Up @@ -3878,6 +3904,14 @@ const createScatterplot = (
setAnnotationHVLineLimit(properties.annotationHVLineLimit);
}

if (properties.antiAliasing !== undefined) {
setAntiAliasing(properties.antiAliasing);
}

if (properties.pixelAligned !== undefined) {
setPixelAligned(properties.pixelAligned);
}

// setWidth and setHeight can be async when width or height are set to
// 'auto'. And since draw() would have anyway been async we can just make
// all calls async.
Expand Down Expand Up @@ -4027,9 +4061,94 @@ const createScatterplot = (
}
};

/** @type {() => ImageData} */
const exportFn = () =>
canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height);
/**
* Export view as `ImageData` using custom render settings
* @param {import('./types').ScatterplotMethodOptions['export']} options
* @returns {Promise<ImageData>}
*/
const exportFnAdvanced = async (options) => {
canvas.style.userSelect = 'none';

const dpr = window.devicePixelRatio;

const currPointSize = pointSize;
const currWidth = width;
const currHeight = height;
const currRendererWidth = renderer.canvas.width / dpr;
const currRendererHeight = renderer.canvas.height / dpr;
const currPixelAligned = pixelAligned;
const currAntiAliasing = antiAliasing;

const scale = options?.scale || 1;
const newPointSize = Array.isArray(pointSize)
? pointSize.map((s) => s * scale)
: pointSize * scale;
const newWidth = currentWidth * scale;
const newHeight = currentHeight * scale;

setPointSize(newPointSize);
setWidth(newWidth);
setHeight(newHeight);
setPixelAligned(options?.pixelAligned || pixelAligned);
setAntiAliasing(options?.antiAliasing || antiAliasing);

renderer.resize(width, height);
renderer.refresh();

await new Promise((resolve) => {
pubSub.subscribe('draw', resolve);
redraw();
});

const imageData = canvas
.getContext('2d')
.getImageData(0, 0, canvas.width, canvas.height);

renderer.resize(currRendererWidth, currRendererHeight);
renderer.refresh();

setPointSize(currPointSize);
setWidth(currWidth);
setHeight(currHeight);
setPixelAligned(currPixelAligned);
setAntiAliasing(currAntiAliasing);

await new Promise((resolve) => {
pubSub.subscribe('draw', resolve);
redraw();
});

canvas.style.userSelect = null;

return imageData;
};

/**
* Export view as `ImageData` using the current render settings
* @overload
* @param {undefined} options
* @return {ImageData}
*/
/**
* Export view as `ImageData` using custom render settings
* @overload
* @param {import('./types').ScatterplotMethodOptions['export']} options
* @return {Promise<ImageData>}
*/
/**
* Export view
* @param {import('./types').ScatterplotMethodOptions['export']} [options]
* @returns {Promise<ImageData>}
*/
const exportFn = (options) => {
if (options === undefined) {
return canvas
.getContext('2d')
.getImageData(0, 0, canvas.width, canvas.height);
}

return exportFnAdvanced(options);
};

const init = () => {
updateViewAspectRatio();
Expand Down
4 changes: 3 additions & 1 deletion src/point.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const FRAGMENT_SHADER = `
precision highp float;

uniform float antiAliasing;

varying vec4 color;
varying float finalPointSize;

Expand All @@ -11,7 +13,7 @@ float linearstep(float edge0, float edge1, float x) {
void main() {
vec2 c = gl_PointCoord * 2.0 - 1.0;
float sdf = length(c) * finalPointSize;
float alpha = linearstep(finalPointSize + 0.5, finalPointSize - 0.5, sdf);
float alpha = linearstep(finalPointSize + antiAliasing, finalPointSize - antiAliasing, sdf);

gl_FragColor = vec4(color.rgb, alpha * color.a);
}
Expand Down
15 changes: 14 additions & 1 deletion src/point.vs
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,15 @@ uniform float isOpacityByW;
uniform float isOpacityByDensity;
uniform float isSizedByZ;
uniform float isSizedByW;
uniform float isPixelAligned;
uniform float colorMultiplicator;
uniform float opacityMultiplicator;
uniform float opacityDensity;
uniform float sizeMultiplicator;
uniform float numColorStates;
uniform float pointScale;
uniform float drawingBufferWidth;
uniform float drawingBufferHeight;
uniform mat4 modelViewProjection;

attribute vec2 stateIndex;
Expand All @@ -39,7 +42,17 @@ varying float finalPointSize;
void main() {
vec4 state = texture2D(stateTex, stateIndex);

gl_Position = modelViewProjection * vec4(state.x, state.y, 0.0, 1.0);
if (isPixelAligned < 0.5) {
gl_Position = modelViewProjection * vec4(state.x, state.y, 0.0, 1.0);
} else {
vec4 clipSpacePosition = modelViewProjection * vec4(state.x, state.y, 0.0, 1.0);
vec2 ndcPosition = clipSpacePosition.xy / clipSpacePosition.w;
vec2 pixelPos = 0.5 * (ndcPosition + 1.0) * vec2(drawingBufferWidth, drawingBufferHeight);
pixelPos = floor(pixelPos + 0.5); // Snap to nearest pixel
vec2 snappedPosition = (pixelPos / vec2(drawingBufferWidth, drawingBufferHeight)) * 2.0 - 1.0;
gl_Position = vec4(snappedPosition, 0.0, 1.0);
}


// Determine color index
float colorIndexZ = isColoredByZ * floor(state.z * colorMultiplicator);
Expand Down
28 changes: 21 additions & 7 deletions src/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,10 @@ export const createRenderer = (
}
});

const resize = () => {
const resize = (
/** @type {number} */ customWidth,
/** @type {number} */ customHeight,
) => {
// We need to limit the width and height by the screen size to prevent
// a bug in VSCode where the window height is said to be taller than the
// screen height. The problem with too large dimensions is that at some
Expand All @@ -157,18 +160,28 @@ export const createRenderer = (
// @see
// https://github.com/microsoft/vscode/issues/225808
// https://github.com/flekschas/jupyter-scatter/issues/37
const width = Math.min(window.innerWidth, window.screen.availWidth);
const height = Math.min(window.innerHeight, window.screen.availHeight);
const width =
customWidth === undefined
? Math.min(window.innerWidth, window.screen.availWidth)
: customWidth;
const height =
customHeight === undefined
? Math.min(window.innerHeight, window.screen.availHeight)
: customHeight;
canvas.width = width * window.devicePixelRatio;
canvas.height = height * window.devicePixelRatio;
fboRes[0] = canvas.width;
fboRes[1] = canvas.height;
fbo.resize(...fboRes);
};

const resizeHandler = () => {
resize();
};

if (!options.canvas) {
window.addEventListener('resize', resize);
window.addEventListener('orientationchange', resize);
window.addEventListener('resize', resizeHandler);
window.addEventListener('orientationchange', resizeHandler);
resize();
}

Expand All @@ -177,8 +190,8 @@ export const createRenderer = (
*/
const destroy = () => {
isDestroyed = true;
window.removeEventListener('resize', resize);
window.removeEventListener('orientationchange', resize);
window.removeEventListener('resize', resizeHandler);
window.removeEventListener('orientationchange', resizeHandler);
frame.cancel();
canvas = undefined;
regl.destroy();
Expand Down Expand Up @@ -229,6 +242,7 @@ export const createRenderer = (
return isDestroyed;
},
render,
resize,
onFrame,
refresh,
destroy,
Expand Down
7 changes: 7 additions & 0 deletions src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ interface BaseOptions {
yScale: null | Scale;
pointScaleMode: PointScaleMode;
cameraIsFixed: boolean;
antiAliasing: number;
pixelAligned: boolean;
}

// biome-ignore lint/style/useNamingConvention: KDBush is a library name
Expand Down Expand Up @@ -265,6 +267,11 @@ export interface ScatterplotMethodOptions {
transitionDuration: number;
transitionEasing: (t: number) => number;
}>;
export: Partial<{
scale: number;
antiAliasing: number;
pixelAligned: boolean;
}>;
}

export type Events = import('pub-sub-es').Event<
Expand Down
Loading
Loading