Skip to content

Commit 2e6d35c

Browse files
authored
feat: improve rendering and export functionality (#207)
Address #175 by adding `antiAliasing` and `pixelAligned` properties. This PR also exposes the renderer's `resize()` function.
1 parent d425e8f commit 2e6d35c

File tree

9 files changed

+216
-15
lines changed

9 files changed

+216
-15
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 1.12.0
2+
3+
- Feat: add support for adjusting the anti-aliasing via `scatterplot.set({ antiAliasing: 1 })`. ([#175](https://github.com/flekschas/regl-scatterplot/issues/175))
4+
- Feat: add support for aligning points with the pixel grid via `scatterplot.set({ pixelAligned: true })`. ([#175](https://github.com/flekschas/regl-scatterplot/issues/175))
5+
- 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`.
6+
- Feat: expose `resize()` method of the `renderer`.
7+
18
## 1.11.4
29

310
- Fix: allow setting the lasso long press indicator parent element

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -707,9 +707,12 @@ Sets the view back to the initially defined view. This will trigger a `view` eve
707707

708708
**Arguments:**
709709

710-
- `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.
710+
- `options` is an object for customizing the render settings during the export:
711+
- `scale`: is a float number allowning to adjust the exported image size
712+
- `antiAliasing`: is a float allowing to adjust the anti-aliasing factor
713+
- `pixelAligned`: is a Boolean allowing to adjust the point alignment with the pixel grid
711714

712-
**Returns:** an object with three properties: `pixels`, `width`, and `height`. The `pixels` is a `Uint8ClampedArray`.
715+
**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.
713716

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

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

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

src/constants.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@ export const W_NAMES = new Set(['w', 'valueW', 'valueB', 'value2', 'value']);
167167
export const DEFAULT_IMAGE_LOAD_TIMEOUT = 15000;
168168
export const DEFAULT_SPATIAL_INDEX_USE_WORKER = undefined;
169169
export const DEFAULT_CAMERA_IS_FIXED = false;
170+
export const DEFAULT_ANTI_ALIASING = 0.5;
171+
export const DEFAULT_PIXEL_ALIGNED = false;
170172

171173
// Error messages
172174
export const ERROR_POINTS_NOT_DRAWN = 'Points have not been drawn';

src/index.js

Lines changed: 123 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
DEFAULT_ANNOTATION_HVLINE_LIMIT,
3838
DEFAULT_ANNOTATION_LINE_COLOR,
3939
DEFAULT_ANNOTATION_LINE_WIDTH,
40+
DEFAULT_ANTI_ALIASING,
4041
DEFAULT_BACKGROUND_IMAGE,
4142
DEFAULT_CAMERA_IS_FIXED,
4243
DEFAULT_COLOR_ACTIVE,
@@ -70,6 +71,7 @@ import {
7071
DEFAULT_OPACITY_INACTIVE_MAX,
7172
DEFAULT_OPACITY_INACTIVE_SCALE,
7273
DEFAULT_PERFORMANCE_MODE,
74+
DEFAULT_PIXEL_ALIGNED,
7375
DEFAULT_POINT_CONNECTION_COLOR_ACTIVE,
7476
DEFAULT_POINT_CONNECTION_COLOR_BY,
7577
DEFAULT_POINT_CONNECTION_COLOR_HOVER,
@@ -234,6 +236,8 @@ const createScatterplot = (
234236

235237
let {
236238
renderer,
239+
antiAliasing = DEFAULT_ANTI_ALIASING,
240+
pixelAligned = DEFAULT_PIXEL_ALIGNED,
237241
backgroundColor = DEFAULT_COLOR_BG,
238242
backgroundImage = DEFAULT_BACKGROUND_IMAGE,
239243
canvas = document.createElement('canvas'),
@@ -1464,6 +1468,7 @@ const createScatterplot = (
14641468
);
14651469
};
14661470

1471+
const getAntiAliasing = () => antiAliasing;
14671472
const getResolution = () => [canvas.width, canvas.height];
14681473
const getBackgroundImage = () => backgroundImage;
14691474
const getColorTex = () => colorTex;
@@ -1519,6 +1524,7 @@ const createScatterplot = (
15191524
const getIsOpacityByDensity = () => +(opacityBy === 'density');
15201525
const getIsSizedByZ = () => +(sizeBy === 'valueZ');
15211526
const getIsSizedByW = () => +(sizeBy === 'valueW');
1527+
const getIsPixelAligned = () => +pixelAligned;
15221528
const getColorMultiplicator = () => {
15231529
if (colorBy === 'valueZ') {
15241530
return valueZDataType === CONTINUOUS ? pointColor.length - 1 : 1;
@@ -1632,6 +1638,7 @@ const createScatterplot = (
16321638
},
16331639

16341640
uniforms: {
1641+
antiAliasing: getAntiAliasing,
16351642
resolution: getResolution,
16361643
modelViewProjection: getModelViewProjection,
16371644
devicePixelRatio: getDevicePixelRatio,
@@ -1656,11 +1663,14 @@ const createScatterplot = (
16561663
isOpacityByDensity: getIsOpacityByDensity,
16571664
isSizedByZ: getIsSizedByZ,
16581665
isSizedByW: getIsSizedByW,
1666+
isPixelAligned: getIsPixelAligned,
16591667
colorMultiplicator: getColorMultiplicator,
16601668
opacityMultiplicator: getOpacityMultiplicator,
16611669
opacityDensity: getOpacityDensity,
16621670
sizeMultiplicator: getSizeMultiplicator,
16631671
numColorStates: COLOR_NUM_STATES,
1672+
drawingBufferWidth: (context) => context.drawingBufferWidth,
1673+
drawingBufferHeight: (context) => context.drawingBufferHeight,
16641674
},
16651675

16661676
count: getNumPoints,
@@ -3273,6 +3283,14 @@ const createScatterplot = (
32733283
renderer.gamma = newGamma;
32743284
};
32753285

3286+
const setAntiAliasing = (newAntiAliasing) => {
3287+
antiAliasing = Number(newAntiAliasing) || 0.5;
3288+
};
3289+
3290+
const setPixelAligned = (newPixelAligned) => {
3291+
pixelAligned = Boolean(newPixelAligned);
3292+
};
3293+
32763294
/** @type {<Key extends keyof import('./types').Properties>(property: Key) => import('./types').Properties[Key] } */
32773295
const get = (property) => {
32783296
checkDeprecations({ property: true });
@@ -3608,10 +3626,18 @@ const createScatterplot = (
36083626
return annotationHVLineLimit;
36093627
}
36103628

3629+
if (property === 'antiAliasing') {
3630+
return antiAliasing;
3631+
}
3632+
3633+
if (property === 'pixelAligned') {
3634+
return pixelAligned;
3635+
}
3636+
36113637
return undefined;
36123638
};
36133639

3614-
/** @type {(properties: Partial<import('./types').Settable>) => void} */
3640+
/** @type {(properties: Partial<import('./types').Settable>) => Promise<void>} */
36153641
const set = (properties = {}) => {
36163642
checkDeprecations(properties);
36173643

@@ -3878,6 +3904,14 @@ const createScatterplot = (
38783904
setAnnotationHVLineLimit(properties.annotationHVLineLimit);
38793905
}
38803906

3907+
if (properties.antiAliasing !== undefined) {
3908+
setAntiAliasing(properties.antiAliasing);
3909+
}
3910+
3911+
if (properties.pixelAligned !== undefined) {
3912+
setPixelAligned(properties.pixelAligned);
3913+
}
3914+
38813915
// setWidth and setHeight can be async when width or height are set to
38823916
// 'auto'. And since draw() would have anyway been async we can just make
38833917
// all calls async.
@@ -4027,9 +4061,94 @@ const createScatterplot = (
40274061
}
40284062
};
40294063

4030-
/** @type {() => ImageData} */
4031-
const exportFn = () =>
4032-
canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height);
4064+
/**
4065+
* Export view as `ImageData` using custom render settings
4066+
* @param {import('./types').ScatterplotMethodOptions['export']} options
4067+
* @returns {Promise<ImageData>}
4068+
*/
4069+
const exportFnAdvanced = async (options) => {
4070+
canvas.style.userSelect = 'none';
4071+
4072+
const dpr = window.devicePixelRatio;
4073+
4074+
const currPointSize = pointSize;
4075+
const currWidth = width;
4076+
const currHeight = height;
4077+
const currRendererWidth = renderer.canvas.width / dpr;
4078+
const currRendererHeight = renderer.canvas.height / dpr;
4079+
const currPixelAligned = pixelAligned;
4080+
const currAntiAliasing = antiAliasing;
4081+
4082+
const scale = options?.scale || 1;
4083+
const newPointSize = Array.isArray(pointSize)
4084+
? pointSize.map((s) => s * scale)
4085+
: pointSize * scale;
4086+
const newWidth = currentWidth * scale;
4087+
const newHeight = currentHeight * scale;
4088+
4089+
setPointSize(newPointSize);
4090+
setWidth(newWidth);
4091+
setHeight(newHeight);
4092+
setPixelAligned(options?.pixelAligned || pixelAligned);
4093+
setAntiAliasing(options?.antiAliasing || antiAliasing);
4094+
4095+
renderer.resize(width, height);
4096+
renderer.refresh();
4097+
4098+
await new Promise((resolve) => {
4099+
pubSub.subscribe('draw', resolve);
4100+
redraw();
4101+
});
4102+
4103+
const imageData = canvas
4104+
.getContext('2d')
4105+
.getImageData(0, 0, canvas.width, canvas.height);
4106+
4107+
renderer.resize(currRendererWidth, currRendererHeight);
4108+
renderer.refresh();
4109+
4110+
setPointSize(currPointSize);
4111+
setWidth(currWidth);
4112+
setHeight(currHeight);
4113+
setPixelAligned(currPixelAligned);
4114+
setAntiAliasing(currAntiAliasing);
4115+
4116+
await new Promise((resolve) => {
4117+
pubSub.subscribe('draw', resolve);
4118+
redraw();
4119+
});
4120+
4121+
canvas.style.userSelect = null;
4122+
4123+
return imageData;
4124+
};
4125+
4126+
/**
4127+
* Export view as `ImageData` using the current render settings
4128+
* @overload
4129+
* @param {undefined} options
4130+
* @return {ImageData}
4131+
*/
4132+
/**
4133+
* Export view as `ImageData` using custom render settings
4134+
* @overload
4135+
* @param {import('./types').ScatterplotMethodOptions['export']} options
4136+
* @return {Promise<ImageData>}
4137+
*/
4138+
/**
4139+
* Export view
4140+
* @param {import('./types').ScatterplotMethodOptions['export']} [options]
4141+
* @returns {Promise<ImageData>}
4142+
*/
4143+
const exportFn = (options) => {
4144+
if (options === undefined) {
4145+
return canvas
4146+
.getContext('2d')
4147+
.getImageData(0, 0, canvas.width, canvas.height);
4148+
}
4149+
4150+
return exportFnAdvanced(options);
4151+
};
40334152

40344153
const init = () => {
40354154
updateViewAspectRatio();

src/point.fs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
const FRAGMENT_SHADER = `
22
precision highp float;
33

4+
uniform float antiAliasing;
5+
46
varying vec4 color;
57
varying float finalPointSize;
68

@@ -11,7 +13,7 @@ float linearstep(float edge0, float edge1, float x) {
1113
void main() {
1214
vec2 c = gl_PointCoord * 2.0 - 1.0;
1315
float sdf = length(c) * finalPointSize;
14-
float alpha = linearstep(finalPointSize + 0.5, finalPointSize - 0.5, sdf);
16+
float alpha = linearstep(finalPointSize + antiAliasing, finalPointSize - antiAliasing, sdf);
1517

1618
gl_FragColor = vec4(color.rgb, alpha * color.a);
1719
}

src/point.vs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,15 @@ uniform float isOpacityByW;
2323
uniform float isOpacityByDensity;
2424
uniform float isSizedByZ;
2525
uniform float isSizedByW;
26+
uniform float isPixelAligned;
2627
uniform float colorMultiplicator;
2728
uniform float opacityMultiplicator;
2829
uniform float opacityDensity;
2930
uniform float sizeMultiplicator;
3031
uniform float numColorStates;
3132
uniform float pointScale;
33+
uniform float drawingBufferWidth;
34+
uniform float drawingBufferHeight;
3235
uniform mat4 modelViewProjection;
3336

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

42-
gl_Position = modelViewProjection * vec4(state.x, state.y, 0.0, 1.0);
45+
if (isPixelAligned < 0.5) {
46+
gl_Position = modelViewProjection * vec4(state.x, state.y, 0.0, 1.0);
47+
} else {
48+
vec4 clipSpacePosition = modelViewProjection * vec4(state.x, state.y, 0.0, 1.0);
49+
vec2 ndcPosition = clipSpacePosition.xy / clipSpacePosition.w;
50+
vec2 pixelPos = 0.5 * (ndcPosition + 1.0) * vec2(drawingBufferWidth, drawingBufferHeight);
51+
pixelPos = floor(pixelPos + 0.5); // Snap to nearest pixel
52+
vec2 snappedPosition = (pixelPos / vec2(drawingBufferWidth, drawingBufferHeight)) * 2.0 - 1.0;
53+
gl_Position = vec4(snappedPosition, 0.0, 1.0);
54+
}
55+
4356

4457
// Determine color index
4558
float colorIndexZ = isColoredByZ * floor(state.z * colorMultiplicator);

src/renderer.js

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,10 @@ export const createRenderer = (
144144
}
145145
});
146146

147-
const resize = () => {
147+
const resize = (
148+
/** @type {number} */ customWidth,
149+
/** @type {number} */ customHeight,
150+
) => {
148151
// We need to limit the width and height by the screen size to prevent
149152
// a bug in VSCode where the window height is said to be taller than the
150153
// screen height. The problem with too large dimensions is that at some
@@ -157,18 +160,28 @@ export const createRenderer = (
157160
// @see
158161
// https://github.com/microsoft/vscode/issues/225808
159162
// https://github.com/flekschas/jupyter-scatter/issues/37
160-
const width = Math.min(window.innerWidth, window.screen.availWidth);
161-
const height = Math.min(window.innerHeight, window.screen.availHeight);
163+
const width =
164+
customWidth === undefined
165+
? Math.min(window.innerWidth, window.screen.availWidth)
166+
: customWidth;
167+
const height =
168+
customHeight === undefined
169+
? Math.min(window.innerHeight, window.screen.availHeight)
170+
: customHeight;
162171
canvas.width = width * window.devicePixelRatio;
163172
canvas.height = height * window.devicePixelRatio;
164173
fboRes[0] = canvas.width;
165174
fboRes[1] = canvas.height;
166175
fbo.resize(...fboRes);
167176
};
168177

178+
const resizeHandler = () => {
179+
resize();
180+
};
181+
169182
if (!options.canvas) {
170-
window.addEventListener('resize', resize);
171-
window.addEventListener('orientationchange', resize);
183+
window.addEventListener('resize', resizeHandler);
184+
window.addEventListener('orientationchange', resizeHandler);
172185
resize();
173186
}
174187

@@ -177,8 +190,8 @@ export const createRenderer = (
177190
*/
178191
const destroy = () => {
179192
isDestroyed = true;
180-
window.removeEventListener('resize', resize);
181-
window.removeEventListener('orientationchange', resize);
193+
window.removeEventListener('resize', resizeHandler);
194+
window.removeEventListener('orientationchange', resizeHandler);
182195
frame.cancel();
183196
canvas = undefined;
184197
regl.destroy();
@@ -229,6 +242,7 @@ export const createRenderer = (
229242
return isDestroyed;
230243
},
231244
render,
245+
resize,
232246
onFrame,
233247
refresh,
234248
destroy,

src/types.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ interface BaseOptions {
165165
yScale: null | Scale;
166166
pointScaleMode: PointScaleMode;
167167
cameraIsFixed: boolean;
168+
antiAliasing: number;
169+
pixelAligned: boolean;
168170
}
169171

170172
// biome-ignore lint/style/useNamingConvention: KDBush is a library name
@@ -265,6 +267,11 @@ export interface ScatterplotMethodOptions {
265267
transitionDuration: number;
266268
transitionEasing: (t: number) => number;
267269
}>;
270+
export: Partial<{
271+
scale: number;
272+
antiAliasing: number;
273+
pixelAligned: boolean;
274+
}>;
268275
}
269276

270277
export type Events = import('pub-sub-es').Event<

0 commit comments

Comments
 (0)