profile-picture-generator
Version:
FridaysForFuture profile picture generator
310 lines (288 loc) • 6.76 kB
text/typescript
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<ImageLayer> = 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)
};
}