Convert GIF Player Web Component into React Component? - javascript

I want to convert a GIF Player Web Component into React Component.
I tried finding a React GIF Player library but there aren't any that are working fine.
Currently, GIF Player Web Component looks promising but it's not available in React. It looks like:
import { LitElement, html, css } from "lit";
import { GifReader } from "omggif";
class GifPlayer extends LitElement {
static get styles() {
return css`
:host {
display: inline-block;
}
canvas {
display: block;
width: 100%;
height: 100%;
}
`;
}
static get properties() {
return {
src: { type: String },
alt: { type: String },
autoplay: { type: Boolean },
play: { type: Function },
pause: { type: Function },
restart: { type: Function },
currentFrame: { type: Number },
frames: { attribute: false, type: Array },
playing: { attribute: false, type: Boolean },
width: { attribute: false, type: Number },
height: { attribute: false, type: Number },
};
}
constructor() {
super();
this.currentFrame = 1;
this.frames = [];
this.step = this.step();
this.play = this.play.bind(this);
this.pause = this.pause.bind(this);
this.renderFrame = this.renderFrame.bind(this);
this.loadSource = this.loadSource.bind(this);
}
firstUpdated() {
this.canvas = this.renderRoot.querySelector("canvas");
this.context = this.canvas.getContext("2d");
this.loadSource(this.src).then(() => {
if (this.autoplay) this.play();
});
}
updated(changedProperties) {
if (changedProperties.has("width")) {
this.canvas.width = this.width;
this.renderFrame(false);
}
if (changedProperties.has("height")) {
this.canvas.height = this.height;
this.renderFrame(false);
}
}
render() {
return html`<canvas role="img" aria-label=${this.alt}></canvas>`;
}
play() {
if (this.animationFrame) cancelAnimationFrame(this.animationFrame);
this.animationFrame = requestAnimationFrame(this.step);
this.playing = true;
}
pause() {
this.playing = false;
if (this.animationFrame) cancelAnimationFrame(this.animationFrame);
}
restart() {
this.currentFrame = 1;
if (this.playing) {
this.play();
} else {
this.pause();
this.renderFrame(false);
}
}
step() {
let previousTimestamp;
return (timestamp) => {
if (!previousTimestamp) previousTimestamp = timestamp;
const delta = timestamp - previousTimestamp;
const delay = this.frames[this.currentFrame]?.delay;
if (this.playing && delay && delta > delay) {
previousTimestamp = timestamp;
this.renderFrame();
}
this.animationFrame = requestAnimationFrame(this.step);
};
}
renderFrame(progress = true) {
if (!this.frames.length) return;
if (this.currentFrame === this.frames.length - 1) {
this.currentFrame = 0;
}
this.context.putImageData(this.frames[this.currentFrame].data, 0, 0);
if (progress) {
this.currentFrame = this.currentFrame + 1;
}
}
async loadSource(url) {
const response = await fetch(url);
const buffer = await response.arrayBuffer();
const uInt8Array = new Uint8Array(buffer);
const gifReader = new GifReader(uInt8Array);
const gif = gifData(gifReader);
const { width, height, frames } = gif;
this.width = width;
this.height = height;
this.frames = frames;
if (!this.alt) {
this.alt = url;
}
this.renderFrame(false);
}
}
function gifData(gif) {
const frames = Array.from(frameDetails(gif));
return { width: gif.width, height: gif.height, frames };
}
function* frameDetails(gifReader) {
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
const frameCount = gifReader.numFrames();
let previousFrame;
for (let i = 0; i < frameCount; i++) {
const frameInfo = gifReader.frameInfo(i);
const imageData = context.createImageData(
gifReader.width,
gifReader.height
);
if (i > 0 && frameInfo.disposal < 2) {
imageData.data.set(new Uint8ClampedArray(previousFrame.data.data));
}
gifReader.decodeAndBlitFrameRGBA(i, imageData.data);
previousFrame = {
data: imageData,
delay: gifReader.frameInfo(i).delay * 10,
};
yield previousFrame;
}
}
customElements.define("gif-player", GifPlayer);
However, I don't know how to convert it to a React Component.
I want to convert it in TypeScript. I've managed to convert it a little bit like:
// inspired by https://github.com/WillsonSmith/gif-player-component/blob/main/gif-player.js
import React from 'react'
import { GifReader } from 'omggif'
export class GifPlayer extends React.Component {
static get styles() {
return `
:host {
display: inline-block;
}
canvas {
display: block;
width: 100%;
height: 100%;
}
`
}
static get properties() {
return {
src: { type: String },
alt: { type: String },
autoplay: { type: Boolean },
play: { type: Function },
pause: { type: Function },
restart: { type: Function },
currentFrame: { type: Number },
frames: { attribute: false, type: Array },
playing: { attribute: false, type: Boolean },
width: { attribute: false, type: Number },
height: { attribute: false, type: Number },
}
}
constructor(props) {
super(props)
this.currentFrame = 1
this.frames = []
this.step = this.step()
this.play = this.play.bind(this)
this.pause = this.pause.bind(this)
this.renderFrame = this.renderFrame.bind(this)
this.loadSource = this.loadSource.bind(this)
}
firstUpdated = () => {
this.canvas = this.renderRoot.querySelector('canvas')
this.context = this.canvas.getContext('2d')
this.loadSource(this.src).then(() => {
if (this.autoplay) this.play()
})
}
updated = (changedProperties) => {
if (changedProperties.has('width')) {
this.canvas.width = this.width
this.renderFrame(false)
}
if (changedProperties.has('height')) {
this.canvas.height = this.height
this.renderFrame(false)
}
}
render() {
const { alt } = this.props
return <canvas role="img" aria-label={alt}></canvas>
}
play = () => {
if (this.animationFrame) cancelAnimationFrame(this.animationFrame)
this.animationFrame = requestAnimationFrame(this.step)
this.playing = true
}
pause = () => {
this.playing = false
if (this.animationFrame) cancelAnimationFrame(this.animationFrame)
}
restart = () => {
this.currentFrame = 1
if (this.playing) {
this.play()
} else {
this.pause()
this.renderFrame(false)
}
}
step = () => {
let previousTimestamp
return (timestamp) => {
if (!previousTimestamp) previousTimestamp = timestamp
const delta = timestamp - previousTimestamp
const delay = this.frames[this.currentFrame]?.delay
if (this.playing && delay && delta > delay) {
previousTimestamp = timestamp
this.renderFrame()
}
this.animationFrame = requestAnimationFrame(this.step)
}
}
renderFrame = (progress = true) => {
if (!this.frames.length) return
if (this.currentFrame === this.frames.length - 1) {
this.currentFrame = 0
}
this.context.putImageData(this.frames[this.currentFrame].data, 0, 0)
if (progress) {
this.currentFrame = this.currentFrame + 1
}
}
loadSource = async (url) => {
const response = await fetch(url)
const buffer = await response.arrayBuffer()
const uInt8Array = new Uint8Array(buffer)
const gifReader = new GifReader(uInt8Array)
const gif = gifData(gifReader)
const { width, height, frames } = gif
this.width = width
this.height = height
this.frames = frames
if (!this.alt) {
this.alt = url
}
this.renderFrame(false)
}
}
function gifData(gif) {
const frames = Array.from(frameDetails(gif))
return { width: gif.width, height: gif.height, frames }
}
function* frameDetails(gifReader) {
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
if (!context) return
const frameCount = gifReader.numFrames()
let previousFrame
for (let i = 0; i < frameCount; i++) {
const frameInfo = gifReader.frameInfo(i)
const imageData = context.createImageData(gifReader.width, gifReader.height)
if (i > 0 && frameInfo.disposal < 2) {
imageData.data.set(new Uint8ClampedArray(previousFrame.data.data))
}
gifReader.decodeAndBlitFrameRGBA(i, imageData.data)
previousFrame = {
data: imageData,
delay: gifReader.frameInfo(i).delay * 10,
}
yield previousFrame
}
}
However, I'm getting all kinds of TypeScript errors. I also don't know how to style :host css property.
How can I solve it?

Quick link for people who don't want to read this wall of text: repository link
This was a pretty fun project. I'm not guaranteeing that I support all use-cases, but here is a modern-day implementation that takes advantage of TypeScript. It also prefers the more modern and composable React Hook API over using class components.
The basic idea is that you have useGifController hook, which is linked to a canvas via a ref. This controller then allows you to handle loading and error states on your own, and then control the GIF rendered in a canvas as needed. So for example, we could write a GifPlayer component as follows:
GifPlayer.tsx
import React, { useRef } from 'react'
import { useGifController } from '../hooks/useGifController'
export function GifPlayer(): JSX.Element | null {
const canvasRef = useRef<HTMLCanvasElement>(null)
const gifController = useGifController('/cradle.gif', canvasRef, true)
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 style={{ display: 'flex', gap: 16, justifyContent: 'space-around' }}>
<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>
<p>Width: {width}</p>
<p>Height: {height}</p>
</div>
</div>
)
}
Here, you can see the gifController expects a URL containing the GIF, and a ref to the canvas element. Then, once you handle the loading and error states, you have access to all of the controls GifController provides. play, pause, renderNextFrame, renderPreviousFrame all do exactly what you would expect.
So, what's inside of this useGifController hook? Well... it's a bit lengthy, but hopefully I've documented this well-enough so you can understand after studying it for a bit.
useGifController.ts
import { GifReader } from 'omggif'
import {
RefObject,
DetailedHTMLProps,
CanvasHTMLAttributes,
useEffect,
useState,
MutableRefObject,
useRef,
} from 'react'
import { extractFrames, Frame } from '../lib/extractFrames'
type HTMLCanvasElementProps = DetailedHTMLProps<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: 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 function useGifController(
url: string,
canvas: 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] = useState<State>({ loading: true, error: false })
const [shouldUpdate, setShouldUpdate] = useState(false)
const [canvasAccessible, setCanvasAccessible] = useState(false)
const frameIndex = useRef(-1)
// state variable returned by hook
const [playing, setPlaying] = useState(false)
// ref that is used internally
const _playing = useRef(false)
// Load GIF on initial render and when url changes.
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
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
useEffect(() => {
if (canvasAccessible && frameIndex.current === -1) {
renderNextFrame()
autoplay && setPlaying(true)
}
// ignore renderNextFrame as it is referentially unstable
// eslint-disable-next-line
}, [canvasAccessible])
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
}
}
This in turn depends on extractFrames, which extracts the frames as ImageData objects along with their respective delays.
extractFrames.ts
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
}
I do plan on making this an NPM package someday. I think it'd be pretty useful.

Related

Why 'do not mutate vuex store state outside mutation handlers' error shows up?

I've seen different similar topics here but they don't solve my problem. I try to combine Three.js with Vue3 (+Vuex). I thoroughly followed this tutorial: https://stagerightlabs.com/blog/vue-and-threejs-part-one (and it works on this site) but my identical code doesn't work, it throws the error:
Uncaught (in promise) Error: [vuex] do not mutate vuex store state outside mutation handlers.
I can't see anywhere in the code where state is mutated outside mutation handler. I don't understand why this error comes up. Maybe it has sth to do with Vue3 itself? App in the tutorial was most likely written in Vue2, not sure if it may couse a problem.
Here is my code. Sorry if it looks quite long but hope it helps detecting the root cause. ANd here is reproducible example as well: https://codesandbox.io/s/black-microservice-y2jo6?file=/src/components/BaseModel.vue
BaseModel.vue
<template>
<div class="viewport"></div>
</template>
<script>
import { mapMutations, mapActions } from 'vuex'
export default {
name: 'BaseModel',
components: {
},
data() {
return {
height: 0
};
},
methods: {
...mapMutations(["RESIZE"]),
...mapActions(["INIT", "ANIMATE"])
},
mounted() {
this.INIT({
width: this.$el.offsetWidth,
width: this.$el.offsetHeight,
el: this.$el
})
.then(() => {
this.ANIMATE();
window.addEventListener("resize", () => {
this.RESIZE({
width: this.$el.offsetWidth,
height: this.$el.offsetHeight
});
}, true);
});
}
}
</script>
<style scoped>
.viewport {
height: 100%;
width: 100%;
}
</style>
store.js
import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls.js'
import {
Scene,
PerspectiveCamera,
WebGLRenderer,
Color,
FogExp2,
CylinderBufferGeometry,
MeshPhongMaterial,
Mesh,
DirectionalLight,
AmbientLight,
} from 'three'
const state = {
width: 0,
height: 0,
camera: null,
controls: null,
scene: null,
renderer: null,
pyramids: []
};
const mutations = {
SET_VIEWPORT_SIZE(state, {width, height}){
state.width = width;
state.height = height;
},
INITIALIZE_RENDERER(state, el){
state.renderer = new WebGLRenderer({ antialias: true });
state.renderer.setPixelRatio(window.devicePixelRatio);
state.renderer.setSize(state.width, state.height);
el.appendChild(state.renderer.domElement);
},
INITIALIZE_CAMERA(state){
state.camera = new PerspectiveCamera(60, state.width/state.height, 1, 1000);
state.camera.position.set(0,0,500);
},
INITIALIZE_CONTROLS(state){
state.controls = new TrackballControls(state.camera, state.renderer.domElement);
state.controls.rotateSpeed = 1.0;
state.controls.zoomSpeed = 1.2;
state.controls.panSpeed = 0.8;
state.controls.noZoom = false;
state.controls.noPan = false;
state.controls.staticMoving = true;
state.controls.dynamicDampingFactor = 0.3;
},
INITIALIZE_SCENE(state){
state.scene = new Scene();
state.scene.background = new Color(0xcccccc);
state.scene.fog = new FogExp2(0xcccccc, 0.002);
var geometry = new CylinderBufferGeometry(0,10,30,4,1);
var material = new MeshPhongMaterial({
color: 0xffffff,
flatShading: true
});
for(var i = 0; i < 500; i++){
var mesh = new Mesh(geometry, material);
mesh.position.x = (Math.random() - 0.5) * 1000;
mesh.position.y = (Math.random() - 0.5) * 1000;
mesh.position.z = (Math.random() - 0.5) * 1000;
mesh.updateMatrix();
mesh.matrixAutoUpdate = false;
state.pyramids.push(mesh);
};
state.scene.add(...state.pyramids);
// create lights
var lightA = new DirectionalLight(0xffffff);
lightA.position.set(1,1,1);
state.scene.add(lightA);
var lightB = new DirectionalLight(0x002288);
lightB.position.set(-1, -1, -1);
state.scene.add(lightB);
var lightC = new AmbientLight(0x222222);
state.scene.add(lightC);
},
RESIZE(state, {width, height}){
state.width = width;
state.height = height;
state.camera.aspect = width/height;
state.camera.updateProjectionMatrix();
state.renderer.setSize(width, height);
state.controls.handleResize();
state.renderer.render(state.scene, state.camera);
}
};
const actions = {
INIT({ commit, state }, { width, height, el}){
return new Promise(resolve => {
commit("SET_VIEWPORT_SIZE", { width, height });
commit("INITIALIZE_RENDERER", el);
commit("INITIALIZE_CAMERA");
commit("INITIALIZE_CONTROLS");
commit("INITIALIZE_SCENE");
state.renderer.render(state.scene, state.camera);
state.controls.addEventListener("change", () => {
state.renderer.render(state.scene, state.camera);
});
resolve();
});
},
ANIMATE({ dispatch, state }){
window.requestAnimationFrame(() => {
dispatch("ANIMATE");
state.controls.update();
});
}
}
export default {
state,
mutations,
actions
};
The problem is the objects passed to threejs initialization are attached to the Vuex state, and threejs modifies the objects internally, leading to the warning you observed.
However, attaching the objects to Vuex state has another side effect of causing significant overhead in making the objects reactive. ThreeJS creates many internal properties, which would all be made reactive. That's why the app startup was severely delayed.
The solution is to mark them as raw data (and thus no reactivity needed) with the markRaw() API. ThreeJS could still attach properties to the given objects without creating a reactive connection between them:
// store/main_three.js
import { markRaw } from "vue";
⋮
export default {
⋮
mutations: {
INITIALIZE_RENDERER(state, el) {
const renderer = new WebGLRenderer(⋯);
⋮
state.renderer = markRaw(renderer);
⋮
},
INITIALIZE_CAMERA(state) {
const camera = new PerspectiveCamera(⋯);
⋮
state.camera = markRaw(camera);
},
INITIALIZE_CONTROLS(state) {
const controls = new TrackballControls(⋯);
⋮
state.controls = markRaw(controls);
},
INITIALIZE_SCENE(state) {
const scene = new Scene();
⋮
state.scene = markRaw(scene);
⋮
for (var i = 0; i < 500; i++) {
var mesh = new Mesh(⋯);
⋮
state.pyramids.push(markRaw(mesh));
}
⋮
// create lights
var lightA = new DirectionalLight(0xffffff);
⋮
state.scene.add(markRaw(lightA));
var lightB = new DirectionalLight(0x002288);
⋮
state.scene.add(markRaw(lightB));
var lightC = new AmbientLight(0x222222);
state.scene.add(markRaw(lightC));
},
⋮
},
⋮
};
demo

PDFJS: Rendering PDF on canvas

I am trying to render pdf file on canvas.
Framework is Nuxt.js (Vue)
Finally, I rendered pdf on canvas, but something is strange. Zoomed pdf was rendered.
I tried to change scale and other values, but the result was same.
How can I fix this?
Please check my attached images.
This is excepted pdf.
And my result is as following: (Current scale value is 1.5)
This is my some code to render pdf.
<script>
export default {
props: ["page", "scale"],
mounted() {
this.drawPage();
},
beforeDestroy() {
this.destroyPage(this.page);
},
methods: {
drawPage() {
if (this.renderTask) return;
const { viewport } = this;
const canvasContext = this.$el.getContext("2d");
const renderContext = { canvasContext, viewport };
this.renderTask = this.page.render(renderContext);
this.renderTask.promise.then(() => this.$emit("rendered", this.page));
this.renderTask.promise.then(/* */).catch(this.destroyRenderTask);
},
destroyPage(page) {
if (!page) return;
page._destroy();
if (this.renderTask) this.renderTask.cancel();
},
destroyRenderTask() {
if (!this.renderTask) return;
this.renderTask.cancel();
delete this.renderTask;
},
},
render(h) {
const { canvasAttrs: attrs } = this;
return h("canvas", { attrs });
},
created() {
this.viewport = this.page.getViewport({
scale: this.scale,
});
},
computed: {
canvasAttrs() {
let { width, height } = this.viewport;
[width, height] = [width, height].map((dim) => Math.ceil(dim));
const style = this.canvasStyle;
return {
style,
class: "pdf-page",
};
},
canvasStyle() {
const {
width: actualSizeWidth,
height: actualSizeHeight,
} = this.actualSizeViewport;
const pixelRatio = window.devicePixelRatio || 1;
const [pixelWidth, pixelHeight] = [
actualSizeWidth,
actualSizeHeight,
].map((dim) => Math.ceil(dim / pixelRatio));
return `width: ${pixelWidth}px; height: ${pixelHeight}px;`;
},
actualSizeViewport() {
return this.viewport.clone({ scale: this.scale });
},
},
watch: {
page(page, oldPage) {
this.destroyPage(oldPage);
},
},
};
</script>

Using Dynamic Web TWAIN integrate with angular 8

I am using Dynamsoft Web TWAIN In My Scanner.I am getting error with bellow code,
Html Code -
<button (click)="acquireImage()">Scan Document</button>
<div id="dwtcontrolContainer"></div>
Angular code -
acquireImage(): void {
const dwObject = Dynamsoft.WebTwainEnv.GetWebTwain('dwtcontrolContainer');
dwObject.IfShowIndicator = false;
const bSelected = dwObject.SelectSource();
if (bSelected) {
const onAcquireImageSuccess = () => { dwObject.CloseSource(); };
const onAcquireImageFailure = onAcquireImageSuccess;
dwObject.OpenSource();
dwObject.AcquireImage({}, onAcquireImageSuccess, onAcquireImageFailure);
}
}
The namespace of Dynamic Web TWAIN has been changed. You can refer to the latest sample code https://github.com/Dynamsoft/dwt-angular-simple/blob/master/src/app/dwt/dwt.component.ts
Initialize Dynamic Web TWAIN Object:
ngOnInit(): void {
Dynamsoft.DWT.Containers = [{ WebTwainId: 'dwtObject', ContainerId: this.containerId, Width: '300px', Height: '400px' }];
Dynamsoft.DWT.RegisterEvent('OnWebTwainReady', () => { this.Dynamsoft_OnReady(); });
Dynamsoft.DWT.ResourcesPath = 'assets/dwt-resources';
Dynamsoft.DWT.ProductKey = 't00891wAAAKBfWo4sRRVNTyLqdC7nKomEJIfBYqfXWg5mblnP0eeJi+LsMIUdQvrBf//ocS3z8MJA47R4VdO4x24uJwlqKgkuZOa7BUQHPkFNA5hFSi6lG2qOK6I=';
let checkScript = () => {
if (Dynamsoft.Lib.detect.scriptLoaded) {
Dynamsoft.DWT.Load();
} else {
setTimeout(() => checkScript(), 100);
}
};
checkScript();
}
Dynamsoft_OnReady(): void {
this.DWObject = Dynamsoft.DWT.GetWebTwain(this.containerId);
this.bWASM = Dynamsoft.Lib.env.bMobile || !Dynamsoft.DWT.UseLocalService;
if (this.bWASM) {
this.DWObject.Viewer.cursor = "pointer";
} else {
let sources = this.DWObject.GetSourceNames();
this.selectSources = <HTMLSelectElement>document.getElementById("sources");
this.selectSources.options.length = 0;
for (let i = 0; i < sources.length; i++) {
this.selectSources.options.add(new Option(<string>sources[i], i.toString()));
}
}
}
Acquire images from scanners:
acquireImage(): void {
if (!this.DWObject)
this.DWObject = Dynamsoft.DWT.GetWebTwain();
if (this.bWASM) {
alert("Scanning is not supported under the WASM mode!");
}
else if (this.DWObject.SourceCount > 0 && this.DWObject.SelectSourceByIndex(this.selectSources.selectedIndex)) {
const onAcquireImageSuccess = () => { this.DWObject.CloseSource(); };
const onAcquireImageFailure = onAcquireImageSuccess;
this.DWObject.OpenSource();
this.DWObject.AcquireImage({}, onAcquireImageSuccess, onAcquireImageFailure);
} else {
alert("No Source Available!");
}
}

How can I stop the current swipe on a trackpad

We are developing a website with unique navigation. Part of it involves on each scroll either up or down, it fires JavaScript and navigates to a different HTML element. It is in Vue.js / Nuxt.
So far, everything works beautifully, minus the usage of the trackpad. The initial 2-finger swipe with the trackpad works -- however, this seems to initiate some sort of a smooth scroll, which takes a while to complete. If you try to 2-finger swipe again in the same direction, it's treated as one long scroll, which doesn't fire the JavaScript to advance to the next page. I did a console log of the deltaY and since it's a 2-finger swipe (smooth scroll?), the deltas take a second or two to finish.
This causes issues since you can't use the trackpad and swipe through sections quickly. You have to swipe down, wait until the scroll finishes, then swipe down again.
How would we fix this issue? Is there a way to kill the current scroll, or to eliminate smooth scrolling and just have scrolls go 100 deltas in one way or the other?
Thanks in advance
Vue.js code
export default {
layout: 'empty',
components: {
Footer,
Logo,
LottieAnimation,
ServiceIcon,
CloseBtn
},
async asyncData({ app }) {
try {
return await app.$api.$get('/services.json')
} catch (error) {
return {
error
}
}
},
data() {
return {
activePage: 0,
config: {},
error: null,
goToPageTimer: null,
isTouchpad: null,
page: {},
scrollDisabled: false,
scrollPercentage: 0,
scrollPercentageOld: 0,
scrollDirection: null,
scrollTimer: null,
touchStart: null,
touchTimer: null,
lastWheelDirection: null,
lastWheelEvent: null,
wheelEventsCount: 0,
wheelEventsLimit: 25,
wheelStopTime: 120
}
},
watch: {
scrollPercentage(newValue, oldValue) {
this.scrollDirection = newValue > oldValue ? 'bottom' : 'top'
if (this.scrollDirection === 'bottom') {
this.goToNextPage()
} else {
this.goToPreviousPage()
}
}
},
mounted() {
window.addEventListener('keydown', this.onKeyDown)
window.addEventListener('scroll', this.handleScroll)
window.addEventListener('wheel', this.onMouseWheel)
window.addEventListener('touchend', this.onTouchEnd)
window.addEventListener('touchstart', this.onTouchStart)
this.initPage()
},
destroyed() {
window.removeEventListener('keydown', this.onKeyDown)
window.removeEventListener('scroll', this.handleScroll)
window.removeEventListener('wheel', this.onMouseWheel)
window.removeEventListener('touchend', this.onTouchEnd)
window.removeEventListener('touchstart', this.onTouchStart)
},
methods: {
disableScrolling() {
this.scrollDisabled = true
if (this.goToPageTimer) {
clearTimeout(this.goToPageTimer)
}
this.goToPageTimer = setTimeout(() => {
this.scrollDisabled = false
}, 30)
},
getServicePageId(slug) {
let servicePageId = null
Object.keys(this.page.childPages).forEach((pageId) => {
if (this.page.childPages[pageId].slug === slug) {
servicePageId = parseInt(pageId)
}
})
return servicePageId
},
goToNextPage() {
if (
this.scrollDisabled ||
this.activePage === this.page.childPages.length
) {
return
}
this.activePage = this.activePage === null ? 0 : this.activePage + 1
if (this.activePage < this.page.childPages.length) {
this.scrollToActivePage()
}
},
goToPreviousPage() {
if (this.scrollDisabled) {
return
}
if (!this.activePage) {
return
}
this.activePage -= 1
this.scrollToActivePage()
},
goToPage(index) {
if (this.scrollDisabled) {
return
}
this.activePage = index
this.scrollToActivePage()
},
handleScroll() {
// If scrolling to top do nothing
if (!window.scrollY) {
return
}
if (this.activePage < this.page.childPages.length - 1) {
window.scrollTo(0, 0)
}
},
initPage() {
if (this.$route.query.service) {
this.activePage = this.getServicePageId(this.$route.query.service)
}
},
isServiceActiveOrIsLatestService(slug) {
const servicePageId = this.getServicePageId(slug)
// Service is active
if (this.activePage === servicePageId) {
return true
}
const latestServicePageId = this.page.childPages.length - 1
// Service is the latest and active page is over it (user is looking the footer)
return (
servicePageId === latestServicePageId &&
this.activePage > latestServicePageId
)
},
onKeyDown(e) {
const nextPageKeys = [
34, // Page down
39, // Arrow right
40 // Arrow down
]
const previousPageKeys = [
33, // Page up
37, // Arrow left
38 // Arrow up
]
if (nextPageKeys.includes(e.keyCode)) {
this.goToNextPage()
} else if (previousPageKeys.includes(e.keyCode)) {
this.goToPreviousPage()
}
},
onMouseWheel(event) {
const now = +new Date()
const millisecondsSinceLastEvent = this.lastWheelEvent
? now - this.lastWheelEvent
: 0
const eventsLimitReached = this.wheelEventsCount > this.wheelEventsLimit
const stopTime = this.wheelStopTime
const delta = Math.sign(event.deltaY)
const directionChanged = delta !== 0 && this.lastWheelDirection !== delta
this.lastWheelEvent = now
if (directionChanged) {
this.lastWheelDirection = delta
}
if (
!directionChanged &&
!eventsLimitReached &&
millisecondsSinceLastEvent &&
millisecondsSinceLastEvent <= stopTime
) {
this.wheelEventsCount += 1
return
}
this.wheelEventsCount = 0
if (delta === -1) {
this.goToPreviousPage()
} else {
this.goToNextPage()
}
},
onTouchEnd(e) {
const now = +new Date()
const touchEndX = e.changedTouches[0].clientX
const touchEndY = e.changedTouches[0].clientY
// Try to guess single touch based on touch start - touch end time
const isSingleTouch = now - this.touchTimer <= 100
// Single touch
if (isSingleTouch && this.touchStart.x === touchEndX) {
const horizontalPercentage = Math.ceil(
(touchEndX * 100) / window.innerWidth
)
const verticalPercentage = Math.ceil(
(touchEndY * 100) / window.innerHeight
)
if (horizontalPercentage <= 40) {
this.goToPreviousPage()
return
}
if (horizontalPercentage >= 60) {
this.goToNextPage()
return
}
if (verticalPercentage <= 40) {
this.goToPreviousPage()
return
}
if (verticalPercentage >= 60) {
this.goToNextPage()
return
}
}
// Touch move
if (this.touchStart.y > touchEndY + 5) {
this.goToNextPage()
} else if (this.touchStart.y < touchEndY - 5) {
this.goToPreviousPage()
}
},
onTouchStart(e) {
this.touchTimer = +new Date()
this.touchStart = {
x: e.touches[0].clientX,
y: e.touches[0].clientY
}
},
onVisibilityChanged(isVisible, entry) {
if (isVisible) {
entry.target.classList.add('dynamic-active')
} else {
entry.target.classList.remove('dynamic-inactive')
}
},
scrollToActivePage() {
this.scrollToService(this.page.childPages[this.activePage].slug)
},
scrollToRef(ref) {
if (!this.$refs[ref]) {
return
}
if (this.$refs[ref][0]) {
this.$scrollTo(this.$refs[ref][0], 100, {
force: true,
cancelable: false
})
} else {
this.$scrollTo(this.$refs[ref], 100, { force: true, cancelable: false })
}
this.disableScrolling()
},
scrollToService(slug) {
this.scrollToRef(`service-${slug}`)
},
serviceBgImageUrl(service) {
return require(`~/assets/img/services/backgrounds/${service.slug}.jpg`)
}
},
head() {
const data = {
bodyAttrs: {
class: 'services'
}
}
if (this.page.data) {
data.title = this.page.data.title
data.meta = this.page.metadata
}
return data
}
}

TypeError: Cannot read property 'style' of undefined

export class EstimateForm extends React.Component<IEstimateFormProps,
IEstimateFormState> {
state: IEstimateFormState = {
cellUpdateCss: 'red',
toRow: null,
fromRow: null,
estimateList: null,
estimateItemList: [],
poseList: null,
levelList: null,
partList: null,
selectedEstimate: null,
totalEstimateItems: 0,
selectedIndexes: [],
totalEstimateAmount: 0,
grid: null,
projectId: 0,
};
constructor(props, context) {
super(props, context);
this.state.estimateList = this.props.estimateList;
}
rowGetter = i => {
const row = this.state.estimateItemList[i];
const selectRevison = this.state.selectedEstimate.revision;
if (row['pose.poseName']) {
const poseCode =
row['pose.poseName'].substring(row['pose.poseName'].lastIndexOf('[') + 1,
row['pose.poseName'].lastIndexOf(']'));
for (const pose of this.state.poseList) {
if (pose.poseCode === poseCode) {
row.pose = pose;
}
}
}
if (row['level.levelName']) {
const levelCode = row['level.levelName'].substring(
row['level.levelName'].lastIndexOf('[') + 1,
row['level.levelName'].lastIndexOf(']')
);
for (const level of this.state.levelList) {
if (level.levelCode === levelCode) {
row.level = level;
}
}
}
if (row['level.part.partName']) {
const partCode = row['level.part.partName'].substring(
row['level.part.partName'].lastIndexOf('[') + 1,
row['level.part.partName'].lastIndexOf(']')
);
for (const part of this.state.partList) {
if (part.partCode === partCode) {
row.part = part;
}
}
}
row.get = key => eval('row.' + key);
row.totalCost = (row.materialCost + row.laborCost) * row.amount;
const changeColor = {
backgroundcolor: 'red'
};
const all = document.getElementsByClassName('react-grid-Row') as
HTMLCollectionOf<HTMLElement>;
debugger; if (row.revision > selectRevison) {
for (let i = this.state.fromRow; i <= this.state.toRow; i++) {
all[i].style.color = 'red'; //HERE
}
return row;
}
}
handleGridRowsUpdated = ({ fromRow, toRow, updated }) => {
const rows = this.state.estimateItemList.slice();
for (let i = fromRow; i <= toRow; i++) {
const rowToUpdate = rows[i];
const updatedRow = update(rowToUpdate, { $merge: updated });
rows[i] = updatedRow;
}
this.setState({ estimateItemList: rows, fromRow: (fromRow), toRow: (toRow)
}, () => {
});
};
saveEstimateItems = () => {
if (this.state.selectedEstimate == null) {
toast.warn(<Translate
contentKey="bpmApp.estimateForm.pleaseSelectEstimate">Please select an
estimate</Translate>);
return;
}
render() {
return ()
}
I wanna to change the row color when the condition row.revision > this.state.selectedEstimate.revision . How can I prevent the change of this.color. However TypeError: Cannot read property 'style' of undefined get error but row color is not change. how can i solve this problem it is my first project in react and i dont know where is the problemThanks for your feedback guys.
Okay, so without the rest of the context because your pasted code is difficult to read and understand, the simplest reason for your issue is in this chunk:
const all = document.getElementsByClassName('react-grid-Row') as
HTMLCollectionOf<HTMLElement>;
debugger; if (row.revision > selectRevison) {
for (let i = this.state.fromRow; i <= this.state.toRow; i++) {
all[i].style.color = 'red'; //HERE
}
Essentially there's multiple things that could go wrong here, but most likely there are either no rows with that class on the page, or less than your this.state.fromRow, I see you've got the debugger in there, but you are missing a few things:
You aren't doing a null check on all to make sure you are finding something
You aren't checking whether all.length > this.state.fromRow
You aren't breaking the for loop if all.length < this.state.toRow
It's failing because all[i] doesn't exist, or there's no values:
all = [0, 1]
and you are looking for all[3] for example
Throw in those fallbacks and check what all is on page load and you should be able to figure it out.

Categories

Resources