UNPKG

@notthatnathan/use-canvas-image

Version:

A React hook for automatically exporting a canvas to img.

123 lines (102 loc) 3.97 kB
import { useEffect, useRef } from 'react' import { useInterval } from 'usehooks-ts' // by default, expect a positioned parent for the canvas // img will render behind it const IMG_STYLE = 'position: absolute; top: 0; left: 0; z-index: 0; pointer-events: none;' const CANVAS_STYLE = 'position: absolute; top: 0; left: 0; z-index: 1;' /** * Outputs an image from a canvas. * By default, the image renders behind the canvas on the z axis. * This behavior can be overridden with custom styles. * * @param {HTMLCanvasElement|String} canvas React ref/HTML element or string selector. If string, must be unique in dom and formatted as selector (#id, .class, [attr="val"], etc) * @param {String} imgClassname for styling output image, space separated. * @param {String} canvasClassName for adding .fs-exclude, etc., and styling, space separated. * @param {Object} canvasAttributes for adding data-private, etc * @param {String} fileType what img format to output. default, recommended: image/jpeg * @param {Number} quality 0-1. the compression quality. only applies to image/jpeg or image/webp. default: 0.5 * @param {Number} interval ms. how often to update the image. default: 100 * @param {Boolean} isEnabled pass false to prevent anything here from firing * @returns {Function} function to be called any time the canvas is drawn on */ const useCanvasImage = ({ canvas, imgClassname = '', canvasClassname = '', canvasAttributes = {}, fileType = 'image/jpeg', // fullstory doesn't like png quality = 0.7, interval = null, isEnabled = true, }) => { const canvasRef = useRef() const imgRef = useRef() const createImg = () => { // create image if (!imgRef.current) { imgRef.current = new Image() } // add classes to img if (imgClassname) imgRef.current.classList.add(...imgClassname.split(' ')) // by default, img will be same size as canvas imgRef.current.style = IMG_STYLE } const updateCanvas = () => { // add attributes to canvas Object.keys(canvasAttributes).forEach(attr => { canvasRef.current.setAttribute(attr, canvasAttributes[attr]) }) // add classes to canvas if (canvasClassname) canvasRef.current.classList.add(...canvasClassname.split(' ')) canvasRef.current.style = CANVAS_STYLE createImg() // insert img after canvas canvasRef.current.after(imgRef.current) } const createImgSrc = () => { requestAnimationFrame(() => { if (!canvasRef.current || !imgRef.current) return // set src on img canvasRef.current.toBlob(function(blob) { imgRef.current.src = URL.createObjectURL(blob) }, fileType, quality) }) } useEffect(() => { if (!isEnabled) return let reselectTimeout const setCanvas = () => { if (typeof canvas === 'string') { // selector canvasRef.current = document.querySelector(canvas) } else if (canvas?.current instanceof HTMLCanvasElement) { // ref canvasRef.current = canvas.current } else if (canvas instanceof HTMLCanvasElement) { // canvas element canvasRef.current = canvas } if (canvasRef.current) { updateCanvas() } else { reselectTimeout = setTimeout(setCanvas, 100) } } setCanvas() // eslint-disable-next-line consistent-return return () => { if (reselectTimeout) clearTimeout(reselectTimeout) } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // for interval-based update // passing `interval: null` disables // requires `preserveDrawingBuffer: true` on webgl context // don't use return function in this case useInterval(createImgSrc, interval) // function returned for explicit updates from component // ensure the return function isn't used in conjunction with intervals return interval === null || interval === false ? createImgSrc : null } export default useCanvasImage