Giter Club home page Giter Club logo

gif.tsx-demo's People

Contributors

dlqqq avatar

Stargazers

 avatar

Watchers

 avatar

Forkers

mfpablo

gif.tsx-demo's Issues

Error: Invalid GIF 87a/89a header.

I finally managed to get it to work with my code but I get the following error after 1 or 2 seconds of running:

err

It plays Gif well enough.

I have only changed a little bit of code just to add dynamic src like:

GifPlayer.ts

// inspired by gif-player web component -> https://github.com/WillsonSmith/gif-player-component/blob/main/gif-player.js
// converted to react by https://stackoverflow.com/a/68494363/6141587
import React from 'react'

import { useGifController } from '@/hooks/index'

interface IGifPlayer {
	alt: string
	src: string
}

export const GifPlayer = (gif: IGifPlayer): JSX.Element | null => {
	const [gifPath, setGifPath] = React.useState('')
	const canvasRef = React.useRef<HTMLCanvasElement>(null)
	const gifController = useGifController(gifPath, canvasRef, true)

	React.useEffect(() => {
		const postType = window.location.pathname.split('/')[1]
		setGifPath(`/${postType}/${gif.src}`)
	}, [gif.src])

	if (gifController.loading) {
		return null
	}

	if (gifController.error) {
		return null
	}

	const { playing, play, pause, restart, renderNextFrame, renderPreviousFrame, width, height } =
		gifController

	return (
		<div>
			<canvas {...gifController.canvasProps} ref={canvasRef} />
			<div className="flex justify-around gap-3">
				<button onClick={renderPreviousFrame}>Previous</button>
				{playing ? <button onClick={pause}>Pause</button> : <button onClick={play}>Play</button>}
				<button onClick={restart}>Restart</button>
				<button onClick={renderNextFrame}>Next</button>
			</div>
		</div>
	)
}

useGifController.ts

import React from 'react'
import { GifReader } from 'omggif'

export type Frame = {
	/**
	 * A full frame of a GIF represented as an ImageData object. This can be
	 * rendered onto a canvas context simply by calling
	 * `ctx.putImageData(frame.imageData, 0, 0)`.
	 */
	imageData: ImageData
	/**
	 * Delay in milliseconds.
	 */
	delay: number
}

/**
 * Function that accepts a `GifReader` instance and returns an array of
 * `ImageData` objects that represent the frames of the gif.
 *
 * @param gifReader The `GifReader` instance.
 * @returns An array of `ImageData` objects representing each frame of the GIF.
 * Or `null` if something went wrong.
 */
export function extractFrames(gifReader: GifReader): Frame[] | null {
	const frames: Frame[] = []

	// the width and height of the complete gif
	const { width, height } = gifReader

	// This is the primary canvas that the tempCanvas below renders on top of. The
	// reason for this is that each frame stored internally inside the GIF is a
	// "diff" from the previous frame. To resolve frame 4, we must first resolve
	// frames 1, 2, 3, and then render frame 4 on top. This canvas accumulates the
	// previous frames.
	const canvas = document.createElement('canvas')
	canvas.width = width
	canvas.height = height
	const ctx = canvas.getContext('2d')
	if (!ctx) return null

	for (let frameIndex = 0; frameIndex < gifReader.numFrames(); frameIndex++) {
		// the width, height, x, and y of the "dirty" pixels that should be redrawn
		const {
			width: dirtyWidth,
			height: dirtyHeight,
			x: dirtyX,
			y: dirtyY,
			disposal,
			delay,
		} = gifReader.frameInfo(0)

		// skip this frame if disposal >= 2; from GIF spec
		if (disposal >= 2) continue

		// create hidden temporary canvas that exists only to render the "diff"
		// between the previous frame and the current frame
		const tempCanvas = document.createElement('canvas')
		tempCanvas.width = width
		tempCanvas.height = height
		const tempCtx = tempCanvas.getContext('2d')
		if (!tempCtx) return null

		// extract GIF frame data to tempCanvas
		const newImageData = tempCtx.createImageData(width, height)
		gifReader.decodeAndBlitFrameRGBA(frameIndex, newImageData.data)
		tempCtx.putImageData(newImageData, 0, 0, dirtyX, dirtyY, dirtyWidth, dirtyHeight)

		// draw the tempCanvas on top. ctx.putImageData(tempCtx.getImageData(...))
		// is too primitive here, since the pixels would be *replaced* by incoming
		// RGBA values instead of layered.
		ctx.drawImage(tempCanvas, 0, 0)

		frames.push({
			delay: delay * 10,
			imageData: ctx.getImageData(0, 0, width, height),
		})
	}

	return frames
}

type HTMLCanvasElementProps = React.DetailedHTMLProps<
	React.CanvasHTMLAttributes<HTMLCanvasElement>,
	HTMLCanvasElement
>

type GifControllerLoading = {
	canvasProps: HTMLCanvasElementProps
	loading: true
	error: false
}

type GifControllerError = {
	canvasProps: HTMLCanvasElementProps
	loading: false
	error: true
	errorMessage: string
}

type GifControllerResolved = {
	canvasProps: HTMLCanvasElementProps
	loading: false
	error: false
	frameIndex: React.MutableRefObject<number>
	playing: boolean
	play: () => void
	pause: () => void
	restart: () => void
	renderFrame: (frame: number) => void
	renderNextFrame: () => void
	renderPreviousFrame: () => void
	width: number
	height: number
}

type GifController = GifControllerLoading | GifControllerResolved | GifControllerError

export const useGifController = (
	url: string,
	canvas: React.RefObject<HTMLCanvasElement | null>,
	autoplay = false
): GifController => {
	type LoadingState = {
		loading: true
		error: false
	}

	type ErrorState = {
		loading: false
		error: true
		errorMessage: string
	}

	type ResolvedState = {
		loading: false
		error: false
		gifReader: GifReader
		frames: Frame[]
	}

	type State = LoadingState | ResolvedState | ErrorState

	const ctx = canvas.current?.getContext('2d')

	// asynchronous state variables strongly typed as a union such that properties
	// are only defined when `loading === true`.
	const [state, setState] = React.useState<State>({ loading: true, error: false })
	const [shouldUpdate, setShouldUpdate] = React.useState(false)
	const [canvasAccessible, setCanvasAccessible] = React.useState(false)
	const frameIndex = React.useRef(-1)

	// state variable returned by hook
	const [playing, setPlaying] = React.useState(false)
	// ref that is used internally
	const _playing = React.useRef(false)

	// Load GIF on initial render and when url changes.
	React.useEffect(() => {
		async function loadGif() {
			const response = await fetch(url)
			const buffer = await response.arrayBuffer()
			const uInt8Array = new Uint8Array(buffer)

			// Type cast is necessary because GifReader expects Buffer, which extends
			// Uint8Array. Doesn't *seem* to cause any runtime errors, but I'm sure
			// there's some edge case I'm not covering here.
			const gifReader = new GifReader(uInt8Array as Buffer)
			const frames = extractFrames(gifReader)

			if (!frames) {
				setState({
					loading: false,
					error: true,
					errorMessage: 'Could not extract frames from GIF.',
				})
			} else {
				setState({ loading: false, error: false, gifReader, frames })
			}

			// must trigger re-render to ensure access to canvas ref
			setShouldUpdate(true)
		}
		loadGif()
		// only run this effect on initial render and when URL changes.
		// eslint-disable-next-line
	}, [url])

	// update if shouldUpdate gets set to true
	React.useEffect(() => {
		if (shouldUpdate) {
			setShouldUpdate(false)
		} else if (canvas.current !== null) {
			setCanvasAccessible(true)
		}
	}, [canvas, shouldUpdate])

	// if canvasAccessible is set to true, render first frame and then autoplay if
	// specified in hook arguments
	React.useEffect(() => {
		if (canvasAccessible && frameIndex.current === -1) {
			renderNextFrame()
			autoplay && setPlaying(true)
		}
		// ignore renderNextFrame as it is referentially unstable
		// eslint-disable-next-line
	}, [canvasAccessible])

	React.useEffect(() => {
		if (playing) {
			_playing.current = true
			_iterateRenderLoop()
		} else {
			_playing.current = false
		}
		// ignore _iterateRenderLoop() as it is referentially unstable
		// eslint-disable-next-line
	}, [playing])

	if (state.loading === true || !canvas)
		return { canvasProps: { hidden: true }, loading: true, error: false }

	if (state.error === true)
		return {
			canvasProps: { hidden: true },
			loading: false,
			error: true,
			errorMessage: state.errorMessage,
		}

	const { width, height } = state.gifReader

	return {
		canvasProps: { width, height },
		loading: false,
		error: false,
		playing,
		play,
		pause,
		restart,
		frameIndex,
		renderFrame,
		renderNextFrame,
		renderPreviousFrame,
		width,
		height,
	}

	function play() {
		if (state.error || state.loading) return
		if (playing) return
		setPlaying(true)
	}

	function _iterateRenderLoop() {
		if (state.error || state.loading || !_playing.current) return

		const delay = state.frames[frameIndex.current].delay
		setTimeout(() => {
			renderNextFrame()
			_iterateRenderLoop()
		}, delay)
	}

	function pause() {
		setPlaying(false)
	}

	function restart() {
		frameIndex.current = 0
		setPlaying(true)
	}

	function renderFrame(frameIndex: number) {
		if (!ctx || state.loading === true || state.error === true) return
		if (frameIndex < 0 || frameIndex >= state.gifReader.numFrames()) return
		ctx.putImageData(state.frames[frameIndex].imageData, 0, 0)
	}

	function renderNextFrame() {
		if (!ctx || state.loading === true || state.error === true) return
		const nextFrame =
			frameIndex.current + 1 >= state.gifReader.numFrames() ? 0 : frameIndex.current + 1
		renderFrame(nextFrame)
		frameIndex.current = nextFrame
	}

	function renderPreviousFrame() {
		if (!ctx || state.loading === true || state.error === true) return
		const prevFrame =
			frameIndex.current - 1 < 0 ? state.gifReader.numFrames() - 1 : frameIndex.current - 1
		renderFrame(prevFrame)
		frameIndex.current = prevFrame
	}
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.