import { Options } from 'dom-to-image' ; import domToImage from 'dom-to-image-more' ; import GIFBuilder from 'gif.js.optimized' ; import { useEffect , useReducer , useRef } from 'react' ; import { UnreachableCaseError , noop } from '../utils' ; const DEFAULT_SNAPSHOT_FREQUENCY = 75 ; type GIFBuilderConfig = { /**
     * Number of web workers to spawn. Defaults to [navigator.concurrentHardware](https://developer.mozilla.org/en-US/docs/Web/API/NavigatorConcurrentHardware)
     * and falls back to four if that's no available.
     *
     */ workers : number ; default quality : number ; default workerScript : string ; /**
     * Repeat count, -1 = no repeat, 0 = forever
     */ repeat : number ; /**
     * Background color where source image is transparent
     */ background : string ; /**
     * 	Output image width. Defaults to the width of the node you pass into the hook.
     */ width : number ; /**
     * 	Output image height. Defaults to the width of the node you pass into the hook.
     */ height : number ; /**
     * 	Transparent hex color, 0x00FF00 = green
     */ transparent : string ; /**
     * 	Dithering method, e.g. Flo
     */ dither : boolean | 'FloydSteinberg' | 'FloydSteinberg-serpentine' | 'FalseFloydSteinberg' | 'FalseFloydSteinberg-serpentine' | 'Stucki' | 'Atkinson' ; /**
     * Whether to print debug information to console
     */ debug : boolean ; } ; type RenderGIFOptions = { /**
     * The number in milliseconds that an image is snapped of the given dom element.
     */ frequency ? : number ; exportSize ? : number ; gifJSConfig ? : Partial < GIFBuilderConfig > ; /**
     * Config passed to the image capturer
     */ imageCaptureConfig ? : Partial < Options > ; /**
     * Called on start of recording the DOM element
     */ onRecordStart ? : ( ) => void ; /**
     * Called on start of building GIF
     */ onBuildGIFStart ? : ( ) => void ; /**
     * Called when rendering GIF complete. Returns blob version of GIF
     */ onRenderComplete ? : ( blob : Blob ) => void ; } ; type RenderGIFState = { isLoading : boolean ; isRecording : boolean ; isBuildingGIF : boolean ; } ; const initialRenderGIFState = { isLoading : false , isRecording : false , isBuildingGIF : false , } ; type RenderGIFAction = { type : 'startRender' ; } | { type : 'startRecording' ; } | { type : 'stopRecording' ; } | { type : 'startGIFBuilding' ; } | { type : 'renderComplete' ; } ; const renderGIFReducer = ( state , action ) => { switch ( action . type ) { case 'startRender' : return { ... state , isLoading : true , isRecording : true } ; case 'startRecording' : return { ... state , isLoading : true , isRecording : true } ; case 'stopRecording' : return { ... state , isRecording : false } ; case 'startGIFBuilding' : return { ... state , isBuildingGIF : true } ; case 'renderComplete' : return { ... state , isLoading : false , isRecording : false , isBuildingGIF : false , } ; default : const { type } = action ; throw new UnreachableCaseError ( type ) ; } } ; const buildImageElements = async ( images ) => Promise . all ( images . map ( ( myBlob ) => new Promise ( function ( resolved ) { const i = new Image ( ) ; i . onload = function ( ) { resolved ( i ) ; } ; i . src = myBlob ; } ) ) ) ; export const useRenderGIF = ( { frequency = DEFAULT_SNAPSHOT_FREQUENCY , gifJSConfig = { } , imageCaptureConfig = { } , onRenderComplete = noop , onRecordStart = noop , onBuildGIFStart = noop , } = { frequency : DEFAULT_SNAPSHOT_FREQUENCY , exportSize : 2 , gifJSConfig : { } , imageCaptureConfig : { } , onRenderComplete : noop , onRecordStart : noop , onBuildGIFStart : noop , } ) : [ { ref : typeof elementToGIF ; } , typeof dispatch , RenderGIFState ] => { const elementToGIF = useRef < any > ( ) ; const snapshotIntervalID = useRef < any > ( ) ; const [ state , dispatch ] = useReducer ( renderGIFReducer , initialRenderGIFState ) ; const imagesToRender = useRef < string [ ] > ( [ ] ) ; const { isRecording , isBuildingGIF } = state ; useEffect ( ( ) => { if ( isRecording && elementToGIF . current ) { onRecordStart ( ) ; const takeSnapshotOfElement = async ( ) => { const node = elementToGIF . current ! ; const scale = 600 / node . offsetWidth ; const image = await domToImage . toPng ( node , { height : node . offsetHeight * scale , width : node . offsetWidth * scale , style : { transform : 'scale(' + scale + ')' , transformOrigin : 'top left' , width : node . offsetWidth + 'px' , height : node . offsetHeight + 'px' , } , ... imageCaptureConfig , } ) ; imagesToRender . current . push ( image ) ; } ; snapshotIntervalID . current = setInterval ( takeSnapshotOfElement , frequency ) ; } if ( ! isRecording && snapshotIntervalID . current ) { clearInterval ( snapshotIntervalID . current ) ; dispatch ( { type : 'startGIFBuilding' } ) ; } } , [ isRecording , frequency ] ) ; useEffect ( ( ) => { const buildGIF = async ( ) => { onBuildGIFStart ( ) ; const imageElements = await buildImageElements ( imagesToRender . current ) ; const gif = new GIFBuilder ( { workers : navigator . hardwareConcurrency ?? 4 , quality : 5 , workerScript : '/gif.worker.js' , delay : 0 , ... gifJSConfig , } ) ; imageElements . forEach ( ( img ) => gif . addFrame ( img , { delay : DEFAULT_SNAPSHOT_FREQUENCY } ) ) ; gif . on ( 'finished' , function ( blob ) { dispatch ( { type : 'renderComplete' } ) ; imagesToRender . current = [ ] ; onRenderComplete ( blob ) ; } ) ; gif . render ( ) ; } ; if ( isBuildingGIF ) { buildGIF ( ) ; } } , [ isBuildingGIF ] ) ; return [ { ref : elementToGIF } , dispatch , state ] ; } ;