Skip to content

Commit

Permalink
ui-spacetimechart: picking layer downscaling
Browse files Browse the repository at this point in the history
This commit fixes #915, and also improves picking performances, by
downscaling the picking layer.

Details:
- Cleans unused and poorly named "number" argument from drawAliasedLine
- Adds new get getPickingScalingRatio helper, that returns the picking
  downscaling ratio depending on the current devicePixelRatio value
- Fixes picking layers resize handling
- Adds a new optional "scalingRatio" argument to each canvas aliased
  drawing function
- Adds new scalingRatio argument to PickingDrawingFunction, and updates
  useCanvas accordingly
- Gives the scaling ratio from PickingDrawingFunction to aliased
  functions in every places where usePicking is called

Signed-off-by: Alexis Jacomy <[email protected]>
  • Loading branch information
jacomyal committed Feb 27, 2025
1 parent 1be9d73 commit 5827375
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 32 deletions.
5 changes: 3 additions & 2 deletions ui-spacetimechart/src/components/ConflictLayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const ConflictLayer = ({ conflicts }: ConflictLayerProps) => {
useDraw('paths', drawConflictLayer);

const drawPicking = useCallback<PickingDrawingFunction>(
(imageData, { registerPickingElement, getTimePixel, getSpacePixel }) => {
(imageData, { registerPickingElement, getTimePixel, getSpacePixel }, scalingRatio) => {
for (const [conflictIndex, conflict] of conflicts.entries()) {
const x = getTimePixel(conflict.timeStart);
const y = getSpacePixel(conflict.spaceStart);
Expand All @@ -66,7 +66,8 @@ export const ConflictLayer = ({ conflicts }: ConflictLayerProps) => {
{ x: x - border, y: y - border },
width + 2 * border,
height + 2 * border,
color
color,
scalingRatio
);
}
},
Expand Down
8 changes: 5 additions & 3 deletions ui-spacetimechart/src/components/PathLayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ export const PathLayer = ({
useDraw('paths', drawAll);

const drawPicking = useCallback<PickingDrawingFunction>(
(imageData, stcContext) => {
(imageData, stcContext, scalingRatio) => {
const { registerPickingElement } = stcContext;

// Draw segments:
Expand All @@ -413,7 +413,8 @@ export const PathLayer = ({
point,
lineColor,
STYLES[level].width + pickingTolerance,
true
true,
scalingRatio
);
}
});
Expand All @@ -431,7 +432,8 @@ export const PathLayer = ({
point,
(STYLES[level].width + pickingTolerance) * 2,
lineColor,
false
false,
scalingRatio
);
});
},
Expand Down
30 changes: 23 additions & 7 deletions ui-spacetimechart/src/hooks/useCanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
type Point,
type SpaceTimeChartContextType,
} from '../lib/types';
import { getPickingScalingRatio } from '../utils/canvas';
import { colorToIndex, rgbToHex } from '../utils/colors';
import getPNGBlob from '../utils/png';

Expand Down Expand Up @@ -78,7 +79,8 @@ export function useCanvas(
ctx.clearRect(0, 0, width, height);

const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
set.forEach((fn) => fn(imageData, stcContext));
const pickingScalingRatio = getPickingScalingRatio();
set.forEach((fn) => fn(imageData, stcContext, pickingScalingRatio));
ctx.putImageData(imageData, 0, 0);
}
});
Expand Down Expand Up @@ -226,19 +228,26 @@ export function useCanvas(

// Handle resizing:
useEffect(() => {
const pickingScalingRatio = getPickingScalingRatio();

for (const id in canvasesRef.current) {
const canvas = canvasesRef.current[id];
const ctx = contextsRef.current[id];
const isPicking = id.split('-')[0] === PICKING;

if (canvas) {
const ratio = isPicking ? pickingScalingRatio : devicePixelRatio;

canvas.style.width = size.width + 'px';
canvas.style.height = size.height + 'px';
canvas.setAttribute('width', size.width * devicePixelRatio + 'px');
canvas.setAttribute('height', size.height * devicePixelRatio + 'px');
canvas.setAttribute('width', size.width * ratio + 'px');
canvas.setAttribute('height', size.height * ratio + 'px');

// Reset the transform to identity, then apply the new scale
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.scale(devicePixelRatio, devicePixelRatio);
if (!isPicking) {
// Reset the transform to identity, then apply the new scale
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.scale(devicePixelRatio, devicePixelRatio);
}
}
}

Expand All @@ -249,10 +258,17 @@ export function useCanvas(
// Read picking layer on position change:
useEffect(() => {
let newHoveredItem: HoveredItem | null = null;
const pickingScalingRatio = getPickingScalingRatio();

PICKING_LAYERS.some((layer) => {
const ctx = contextsRef.current[`${PICKING}-${layer}`];
if (ctx && position) {
const [r, g, b, a] = ctx.getImageData(position.x, position.y, 1, 1).data;
const [r, g, b, a] = ctx.getImageData(
Math.round(position.x * pickingScalingRatio),
Math.round(position.y * pickingScalingRatio),
1,
1
).data;
if (a === 255) {
const color = rgbToHex(r, g, b);
const index = colorToIndex(color);
Expand Down
3 changes: 2 additions & 1 deletion ui-spacetimechart/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ export type DrawingFunction = (

export type PickingDrawingFunction = (
imageData: ImageData,
stcContext: SpaceTimeChartContextType
stcContext: SpaceTimeChartContextType,
scalingRatio: number
) => void;

export type DrawingFunctionHandler = (
Expand Down
84 changes: 65 additions & 19 deletions ui-spacetimechart/src/utils/canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,54 +2,89 @@ import { clamp, identity } from 'lodash';

import { type PathEnd, type Point, type RGBAColor, type RGBColor } from '../lib/types';

/**
* This function returns the picking layers scaling ratio. We basically take the min of the screen
* pixels and the "HTML pixels", and divide it by two.
*
* This allows having a smaller picking stage to fill (so it's faster), while keeping a "good enough
* precision".
*/
export function getPickingScalingRatio(): number {
const PICKING_DOWNSCALING_RATIO = 0.5;
const dpr = window.devicePixelRatio || 1;

// When devicePixelRatio is over 1 (like for Retina displays), we downscale based on the "HTML
// pixels":
if (dpr > 1) return PICKING_DOWNSCALING_RATIO;

// When devicePixelRatio is under 1 (like when the user zooms out for instance), we downscale
// based on the actual "screen pixels" (to avoid having a too large scene to fill):
if (dpr < 1) return PICKING_DOWNSCALING_RATIO * dpr;

return PICKING_DOWNSCALING_RATIO;
}

/**
* This function draws a thick lines from "from" to "to" on the given ImageData, with no
* antialiasing. This is very useful to handle picking, since it is not possible to disable
* antialiasing with the native JavaScript canvas APIs.
*/
export function drawAliasedLine(
imageData: ImageData,
from: Point,
to: Point,
{ x: fromX, y: fromY }: Point,
{ x: toX, y: toY }: Point,
[r, g, b]: RGBColor | RGBAColor,
thickness: number,
drawOnBottom: boolean,
number: number = Math.ceil(thickness / 2)
scalingRatio = 1
): void {
if (from.x > to.x)
return drawAliasedLine(imageData, to, from, [r, g, b], thickness, drawOnBottom);
if (fromX > toX)
return drawAliasedLine(
imageData,
{ x: toX, y: toY },
{ x: fromX, y: fromY },
[r, g, b],
thickness,
drawOnBottom
);

fromX = Math.round(fromX * scalingRatio);
fromY = Math.round(fromY * scalingRatio);
toX = Math.round(toX * scalingRatio);
toY = Math.round(toY * scalingRatio);
thickness = Math.round(thickness * scalingRatio);

const width = imageData.width;
const height = imageData.height;
const dx = to.x - from.x;
const dy = to.y - from.y;
const dx = toX - fromX;
const dy = toY - fromY;
const len = Math.sqrt(dx * dx + dy * dy);

// Calculate perpendicular vector
const normX = -dy / len;
const normY = dx / len;

// Calculate the four corners of the rectangle
const halfThickness = number;
const halfThickness = Math.ceil(thickness / 2);

const corner1 = {
x: from.x + (+normX - dx / len) * halfThickness,
y: from.y + (+normY - dy / len) * halfThickness,
x: fromX + (+normX - dx / len) * halfThickness,
y: fromY + (+normY - dy / len) * halfThickness,
};
const corner2 = {
x: from.x + (-normX - dx / len) * halfThickness,
y: from.y + (-normY - dy / len) * halfThickness,
x: fromX + (-normX - dx / len) * halfThickness,
y: fromY + (-normY - dy / len) * halfThickness,
};
const corner3 = {
x: to.x + (-normX + dx / len) * halfThickness,
y: to.y + (-normY + dy / len) * halfThickness,
x: toX + (-normX + dx / len) * halfThickness,
y: toY + (-normY + dy / len) * halfThickness,
};
const corner4 = {
x: to.x + (+normX + dx / len) * halfThickness,
y: to.y + (+normY + dy / len) * halfThickness,
x: toX + (+normX + dx / len) * halfThickness,
y: toY + (+normY + dy / len) * halfThickness,
};

const ascending = from.y < to.y;
const ascending = fromY < toY;
const top = ascending ? corner4 : corner1;
const left = ascending ? corner1 : corner2;
const right = ascending ? corner3 : corner4;
Expand Down Expand Up @@ -139,8 +174,13 @@ export function drawAliasedDisc(
{ x: centerX, y: centerY }: Point,
radius: number,
[r, g, b]: RGBColor | RGBAColor,
drawOnBottom: boolean
drawOnBottom: boolean,
scalingRatio: number = 1
): void {
centerX = Math.round(centerX * scalingRatio);
centerY = Math.round(centerY * scalingRatio);
radius = Math.round(radius * scalingRatio);

centerX = Math.round(centerX);
centerY = Math.round(centerY);
radius = Math.ceil(radius);
Expand Down Expand Up @@ -176,8 +216,14 @@ export function drawAliasedRect(
{ x, y }: Point,
width: number,
height: number,
[r, g, b]: RGBColor | RGBAColor
[r, g, b]: RGBColor | RGBAColor,
scalingRatio = 1
) {
x = Math.round(x * scalingRatio);
y = Math.round(y * scalingRatio);
width = Math.round(width * scalingRatio);
height = Math.round(height * scalingRatio);

const xMin = clamp(x, 0, imageData.width);
const yMin = clamp(y, 0, imageData.height);
const xMax = clamp(x + width, 0, imageData.width);
Expand Down

0 comments on commit 5827375

Please sign in to comment.