thi-ng/umbrella

View on GitHub
packages/canvas/src/index.ts

Summary

Maintainability
B
5 hrs
Test Coverage
export interface Canvas2DOpts {
    /**
     * (Native) options passed to `canvas.getContext("2d")`
     */
    ctx: CanvasRenderingContext2DSettings;
    /**
     * If true, adds CSS rule to force canvas being displayed properly pixelated
     * (no smoothing)
     */
    pixelated: boolean;
}

export interface AdaptiveCanvas2DOpts extends Canvas2DOpts {
    /**
     * Device pixel ratio (aka scale value) to use for the canvas. By default
     * this will be sourced from the current `window.devicePixelRatio`, but this
     * option can be used if a specific fixed number of pixels is required (i.e.
     * set `dpr: 1` to ensure the `width` and `height` values given to
     * {@link canvas2d} _are_ the actual real pixel dimensions).
     *
     * Also see {@link adaptDPI} for more details.
     */
    dpr: number;
}

export interface CanvasContext {
    canvas: HTMLCanvasElement;
    ctx: CanvasRenderingContext2D;
}

/**
 * Creates a canvas element of given size, obtains its 2D drawing context and
 * returns object of both. If `parent` is given, the canvas is appended to it as
 * child.
 *
 * @remarks
 * By default the actual pixel size of the canvas will depend of the
 * `windowDevicePixelRatio` of the current `window` object. To force a specific
 * fixed scale, use the {@link AdaptiveCanvas2DOpts.dpr} option. Also see
 * {@link adaptDPI} for details.
 *
 * @param width -
 * @param height -
 * @param parent -
 * @param opts -
 */
export const adaptiveCanvas2d = (
    width: number,
    height = width,
    parent?: HTMLElement | null,
    opts: Partial<AdaptiveCanvas2DOpts> = {}
): CanvasContext => {
    const canvas = document.createElement("canvas");
    adaptDPI(canvas, width, height, opts.dpr);
    opts.pixelated && (canvas.style.imageRendering = "pixelated");
    parent && parent.appendChild(canvas);
    return {
        canvas,
        ctx: canvas.getContext("2d", opts.ctx)!,
    };
};

/**
 * Convenience version of {@link adaptiveCanvas2d} with a fixed device pixel
 * ratio of 1.0 (to avoid any rescaling).
 *
 * @param width
 * @param height
 * @param parent
 * @param opts
 */
export const canvas2d = (
    width: number,
    height = width,
    parent?: HTMLElement | null,
    opts?: Partial<Canvas2DOpts>
) => adaptiveCanvas2d(width, height, parent, { dpr: 1, ...opts });

/**
 * Convenience version of {@link canvas2d} which also enables the
 * {@link Canvas2DOpts.pixelated} option by default.
 *
 * @param width
 * @param height
 * @param parent
 * @param opts
 */
export const pixelCanvas2d = (
    width: number,
    height = width,
    parent?: HTMLElement | null,
    opts?: Partial<Canvas2DOpts>
) =>
    adaptiveCanvas2d(width, height, parent, {
        dpr: 1,
        pixelated: true,
        ...opts,
    });

/**
 * Creates a canvas and draws given image into it. The `width` and `height`
 * params default to the actual image size, but can be used to resize the image
 * (canvas).
 *
 * @remarks
 * See {@link adaptiveCanvas2d} for more details.
 *
 * @param img
 * @param width
 * @param height
 * @param parent
 * @param opts
 */
export const imageCanvas = (
    img: HTMLImageElement,
    width = img.width,
    height = img.height,
    parent?: HTMLElement | null,
    opts?: Partial<Canvas2DOpts>
) => {
    const res = canvas2d(width, height, parent, opts);
    res.ctx.drawImage(img, 0, 0, res.canvas.width, res.canvas.height);
    return res;
};

/**
 * Sets the canvas size to given `width` & `height` (given as CSS pixels, but
 * applied as device pixels) and adjusts canvas' `style` to compensate for HDPI
 * devices.
 *
 * @remarks
 * For example if `dpr` is 2, the `canvas.width` will be set to `width * 2`
 * (same for `height`) and the `canvas.style.width` will be set to given `width`
 * (in CSS pixels), thus creating a canvas with double resolution in the same
 * _apparent_ size.
 *
 * For 2D canvases, this will automatically clear any prior canvas content.
 * Returns given `dpr` (presumably the window's `devicePixelRatio` or 1.0, if
 * not available).
 *
 * @param canvas -
 * @param width - uncompensated pixel width
 * @param height - uncompensated pixel height
 * @param dpr - device pixel ratio
 */
export const adaptDPI = (
    canvas: HTMLCanvasElement,
    width: number,
    height: number,
    dpr = window.devicePixelRatio || 1
) => {
    if (dpr !== 1) {
        canvas.style.width = `${width}px`;
        canvas.style.height = `${height}px`;
    }
    canvas.width = width * dpr;
    canvas.height = height * dpr;
    return dpr;
};

/**
 * Returns true if device's DPR > 1 (aka HighDPI)
 */
export const isHighDPI = () => (window.devicePixelRatio || 1) > 1;