import { EXIFOrientation, orient } from "../../../util"; import { LayerDrawer } from "./Layer"; export type ImageLayer = { type: "IMAGE"; offsetX: number; offsetY: number; sizeX: number; sizeY: number; src: string; cornerCutout?: number; rotation: EXIFOrientation; zoomFactor?: number; }; export const drawImage: LayerDrawer = async function drawImage( ctx, { src, sizeX, sizeY, offsetX, offsetY, rotation, cornerCutout, zoomFactor }, width, height ) { const img = new Image(); img.crossOrigin = "anonymous"; img.src = src; // Wait for the image to be loaded so it can be copied to the canvas. // If it isn't loaded an empty image would be copied. await new Promise(resolve => img.addEventListener("load", resolve)); ctx.save(); orient(ctx, rotation, Math.max(width, height)); const refSrcOffset = getOffsetForAspectRatio( img.width, img.height, sizeX / sizeY ); const refSrcCropArea = { width: img.width - refSrcOffset.x * 2, height: img.height - refSrcOffset.y * 2 }; const zoomFactorSafe = zoomFactor || 1; const zoomedSrcCropArea = { width: Math.min(refSrcCropArea.width / zoomFactorSafe, img.width), height: Math.min(refSrcCropArea.height / zoomFactorSafe, img.height) }; const zoomedSrcOffset = { x: (img.width - zoomedSrcCropArea.width) / 2, y: (img.height - zoomedSrcCropArea.height) / 2 }; const destAspectRatio = zoomedSrcCropArea.width / zoomedSrcCropArea.height; const destArea = { width: Math.min(sizeX, sizeY * destAspectRatio), height: Math.min(sizeY, sizeX / destAspectRatio) }; drawImageWithCornerCutout( ctx, img, zoomedSrcOffset.x, zoomedSrcOffset.y, zoomedSrcCropArea.width, zoomedSrcCropArea.height, offsetX + (sizeX - destArea.width) / 2, offsetY + (sizeY - destArea.height) / 2, destArea.width, destArea.height, cornerCutout ); ctx.restore(); }; function ctxDrawImage( ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, img: CanvasImageSource, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number ) { if (sw === 0 || sh === 0 || dh === 0 || dw === 0) return; ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh); } function drawUpperPartOfImageWithCornerCutout( ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, img: CanvasImageSource, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number, cornerCutout: number ) { const scaleFactorWidth = sw / dw; const scaleFactorHeight = sh / dh; ctxDrawImage( ctx, img, sx + scaleFactorWidth * cornerCutout, sy, sw - scaleFactorWidth * cornerCutout * 2, scaleFactorHeight * cornerCutout, dx + cornerCutout, dy, dw - cornerCutout * 2, cornerCutout ); } function drawMiddlePartOfImageWithCornerCutout( ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, img: CanvasImageSource, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number, cornerCutout: number ) { const scaleFactorHeight = sh / dh; ctxDrawImage( ctx, img, sx, sy + cornerCutout * scaleFactorHeight, sw, sh - cornerCutout * scaleFactorHeight * 2, dx, dy + cornerCutout, dw, dh - cornerCutout * 2 ); } function drawLowerPartOfImageWithCornerCutout( ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, img: CanvasImageSource, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number, cornerCutout: number ) { const scaleFactorWidth = sw / dw; const scaleFactorHeight = sh / dh; ctxDrawImage( ctx, img, sx + scaleFactorWidth * cornerCutout, sy + sh - scaleFactorHeight * cornerCutout, sw - scaleFactorWidth * cornerCutout * 2, scaleFactorHeight * cornerCutout, dx + cornerCutout, dy + dh - cornerCutout, dw - cornerCutout * 2, cornerCutout ); } /** * Draws the image onto the canvas while cutting out cornerCutout pixels from each of the corners. Results in an image like: * * ## * #### * #### * ## */ function drawImageWithCornerCutout( ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, img: CanvasImageSource, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number, cornerCutout: number = 0 ) { drawMiddlePartOfImageWithCornerCutout( ctx, img, sx, sy, sw, sh, dx, dy, dw, dh, cornerCutout ); drawUpperPartOfImageWithCornerCutout( ctx, img, sx, sy, sw, sh, dx, dy, dw, dh, cornerCutout ); drawLowerPartOfImageWithCornerCutout( ctx, img, sx, sy, sw, sh, dx, dy, dw, dh, cornerCutout ); } /** * Calculate the offset on the src image in pixels on the x axis so it can be cut to an image of the * target aspect ratio with as little cutoff as possible. * * @param srcWidth * @param srcHeight * @param targetAspectRatio width / height */ function getOffsetForAspectRatioX( srcWidth: number, srcHeight: number, targetAspectRatio: number ) { if (srcWidth > srcHeight * targetAspectRatio) { if (srcWidth > srcHeight) { return (srcWidth - srcHeight * targetAspectRatio) / 2; } else { return (srcWidth / targetAspectRatio - srcHeight) / targetAspectRatio / 2; } } return 0; } /** * Calculate the offset on the src image in pixels on the y axis so it can be cut to an image of the * target aspect ratio with as little cutoff as possible. * * @param srcWidth * @param srcHeight * @param targetAspectRatio width / height */ function getOffsetForAspectRatioY( srcWidth: number, srcHeight: number, targetAspectRatio: number ) { if (srcWidth < srcHeight * targetAspectRatio) { if (srcWidth > srcHeight) { return ( ((targetAspectRatio * srcHeight - srcWidth) * targetAspectRatio) / 2 ); } else { return (srcHeight - srcWidth * (1 / targetAspectRatio)) / 2; } } return 0; } /** * Calculate the offset on the src image in pixels so it can be cut to an image of the * target aspect ratio with as little cutoff as possible. * * @param srcWidth * @param srcHeight * @param targetAspectRatio width / height */ function getOffsetForAspectRatio( srcWidth: number, srcHeight: number, targetAspectRatio: number ) { return { x: getOffsetForAspectRatioX(srcWidth, srcHeight, targetAspectRatio), y: getOffsetForAspectRatioY(srcWidth, srcHeight, targetAspectRatio) }; }