From 2c805b25f112035e8e47674bf22300a2b60b6e7d Mon Sep 17 00:00:00 2001 From: Igor Zinken <730069+igorski@users.noreply.github.com> Date: Sat, 8 Feb 2025 15:20:37 +0100 Subject: [PATCH] Add method for pixel-accurate content copying --- src/math/point-math.ts | 25 ++++++++++++++++++++++++- src/utils/canvas-util.ts | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/math/point-math.ts b/src/math/point-math.ts index 9650a00..d152022 100644 --- a/src/math/point-math.ts +++ b/src/math/point-math.ts @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Igor Zinken 2020-2022 - https://www.igorski.nl + * Igor Zinken 2020-2025 - https://www.igorski.nl * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in @@ -51,6 +51,29 @@ export const isPointInRange = ( point1x: number, point1y: number, point2x: numbe isCoordinateInVerticalRange( point1y, point2y, margin ); }; +export const isPointInsidePolygon = ( point: Point, polygon: Point[] ): boolean => { + const { x, y } = point; + let isInside = false; + + for ( let i = 0, l = polygon.length, j = polygon.length - 1; i < l; j = i++ ) { + const compare1 = polygon[ i ]; + const compare2 = polygon[ j ]; + + const x1 = compare1.x; + const y1 = compare1.y; + const x2 = compare2.x; + const y2 = compare2.y; + + const intersect = (( y1 > y ) !== ( y2 > y )) && ( x < ( x2 - x1 ) * ( y - y1 ) / ( y2 - y1 ) + x1 ); + if ( intersect ) { + // we are ray-casting, an odd number of line crossing determines whether a point is inside the polygon. + // when even, the point is outside the polygon. We cannot directly return true here! + isInside = !isInside; + } + } + return isInside; +}; + export const isCoordinateInHorizontalRange = ( point1x: number, point2x: number, margin = 5 ): boolean => { const left = point2x - margin; const right = point2x + margin; diff --git a/src/utils/canvas-util.ts b/src/utils/canvas-util.ts index 4b878de..105d581 100644 --- a/src/utils/canvas-util.ts +++ b/src/utils/canvas-util.ts @@ -24,6 +24,8 @@ import { loader } from "zcanvas"; import type { Point } from "zcanvas"; import type { CanvasContextPairing, CanvasDrawable } from "@/definitions/editor"; import { JPEG, PNG } from "@/definitions/image-types"; +import { isPointInsidePolygon } from "@/math/point-math"; +import { fastRound } from "@/math/unit-math"; import type ZoomableCanvas from "@/rendering/canvas-elements/zoomable-canvas"; import { blobToResource, disposeResource } from "@/utils/resource-manager"; @@ -235,3 +237,40 @@ export const blobToCanvas = ( blob: Blob ): Promise => { }; export const getPixelRatio = (): number => window.devicePixelRatio ?? 1; + + +/** + * Pixel-accurate copying of selection content. This is quite slow though, depending + * on the source content quality, using drawImage() (provides aliasing) is more than satisfactory. + */ +export const copySelectionCrisp = ( sourceCtx: CanvasRenderingContext2D, destCtx: CanvasRenderingContext2D, selection: Point[] ): void => { + // get bounding box of the selection + const minX = fastRound( Math.min( ...selection.map( point => point.x ))); + const minY = fastRound( Math.min( ...selection.map( point => point.y ))); + const maxX = fastRound( Math.max( ...selection.map( point => point.x ))); + const maxY = fastRound( Math.max( ...selection.map( point => point.y ))); + + const width = maxX - minX; + const height = maxY - minY; + + const sourceData = sourceCtx.getImageData( minX, minY, width, height ).data; + const outputImageData = destCtx.createImageData( width, height ); + const outputData = outputImageData.data; + + // Loop through each pixel in the bounding box + for ( let y = 0; y < height; y++ ) { + for ( let x = 0; x < width; x++ ) { + const pixelIndex = ( y * width + x ) * 4; + + if ( isPointInsidePolygon({ x: minX + x, y: minY + y }, selection )) { + outputData[ pixelIndex ] = sourceData[ pixelIndex ]; + outputData[ pixelIndex + 1 ] = sourceData[ pixelIndex + 1 ]; + outputData[ pixelIndex + 2 ] = sourceData[ pixelIndex + 2 ]; + outputData[ pixelIndex + 3 ] = sourceData[ pixelIndex + 3 ]; + } else { + outputData[ pixelIndex + 3 ] = 0; // make transparent + } + } + } + destCtx.putImageData( outputImageData, minX, minY ); +};