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

ui-spacetimechart: picking layer downscaling #938

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
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
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 @@ -389,7 +389,7 @@ export const PathLayer = ({
useDraw('paths', drawAll);

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

// Draw segments:
Expand All @@ -409,7 +409,8 @@ export const PathLayer = ({
point,
lineColor,
STYLES[level].width + pickingTolerance,
true
true,
scalingRatio
);
}
});
Expand All @@ -427,7 +428,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 @@ -227,19 +229,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 @@ -250,10 +259,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 @@ -117,7 +117,8 @@ export type DrawingFunction = (

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

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

import type { Direction, PathEnd, Point, RGBAColor, 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,
scalingRatio
);

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,11 +175,12 @@ 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);
centerY = Math.round(centerY);
radius = Math.ceil(radius);
centerX = Math.round(centerX * scalingRatio);
centerY = Math.round(centerY * scalingRatio);
radius = Math.ceil(radius * scalingRatio);

const { width, height } = imageData;

Expand Down Expand Up @@ -176,8 +213,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