diff --git a/ui-spacetimechart/src/components/ConflictLayer.tsx b/ui-spacetimechart/src/components/ConflictLayer.tsx index 27070fc7..d12dc18b 100644 --- a/ui-spacetimechart/src/components/ConflictLayer.tsx +++ b/ui-spacetimechart/src/components/ConflictLayer.tsx @@ -50,7 +50,7 @@ export const ConflictLayer = ({ conflicts }: ConflictLayerProps) => { useDraw('paths', drawConflictLayer); const drawPicking = useCallback( - (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); @@ -66,7 +66,8 @@ export const ConflictLayer = ({ conflicts }: ConflictLayerProps) => { { x: x - border, y: y - border }, width + 2 * border, height + 2 * border, - color + color, + scalingRatio ); } }, diff --git a/ui-spacetimechart/src/components/PathLayer.tsx b/ui-spacetimechart/src/components/PathLayer.tsx index 7a51614b..e7717596 100644 --- a/ui-spacetimechart/src/components/PathLayer.tsx +++ b/ui-spacetimechart/src/components/PathLayer.tsx @@ -393,7 +393,7 @@ export const PathLayer = ({ useDraw('paths', drawAll); const drawPicking = useCallback( - (imageData, stcContext) => { + (imageData, stcContext, scalingRatio) => { const { registerPickingElement } = stcContext; // Draw segments: @@ -413,7 +413,8 @@ export const PathLayer = ({ point, lineColor, STYLES[level].width + pickingTolerance, - true + true, + scalingRatio ); } }); @@ -431,7 +432,8 @@ export const PathLayer = ({ point, (STYLES[level].width + pickingTolerance) * 2, lineColor, - false + false, + scalingRatio ); }); }, diff --git a/ui-spacetimechart/src/hooks/useCanvas.ts b/ui-spacetimechart/src/hooks/useCanvas.ts index 90f27b01..c241e9d9 100644 --- a/ui-spacetimechart/src/hooks/useCanvas.ts +++ b/ui-spacetimechart/src/hooks/useCanvas.ts @@ -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'; @@ -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); } }); @@ -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); + } } } @@ -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); diff --git a/ui-spacetimechart/src/lib/types.ts b/ui-spacetimechart/src/lib/types.ts index 0154e52a..ca08dabc 100644 --- a/ui-spacetimechart/src/lib/types.ts +++ b/ui-spacetimechart/src/lib/types.ts @@ -115,7 +115,8 @@ export type DrawingFunction = ( export type PickingDrawingFunction = ( imageData: ImageData, - stcContext: SpaceTimeChartContextType + stcContext: SpaceTimeChartContextType, + scalingRatio: number ) => void; export type DrawingFunctionHandler = ( diff --git a/ui-spacetimechart/src/utils/canvas.ts b/ui-spacetimechart/src/utils/canvas.ts index 9c2348ad..ba81f8ed 100644 --- a/ui-spacetimechart/src/utils/canvas.ts +++ b/ui-spacetimechart/src/utils/canvas.ts @@ -2,6 +2,28 @@ 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 @@ -9,20 +31,33 @@ import { type PathEnd, type Point, type RGBAColor, type RGBColor } from '../lib/ */ 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 @@ -30,26 +65,26 @@ export function drawAliasedLine( 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; @@ -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); @@ -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);