A-FRAME Modifying a model's materials' stencil properties breaks rendering - javascript

I am experimenting with the stencil buffer and have created two components. The first is applied to an entity and is supposed to draw into the stencil buffer:
AFRAME.registerComponent("stencil-surface", {
dependencies: ["mesh"],
schema: {
mask: { default: 1 },
order: { default: 1 },
},
updateMaterials: function (mesh) {
const self = this;
mesh.traverse(function (node) {
mesh.renderOrder = self.data.order;
if (node.material) {
node.material.colorWrite = false;
node.material.depthWrite = false;
node.material.stencilWrite = true;
node.material.stencilRef = self.data.mask;
node.material.stencilFuncMask = 0xFF;
node.material.stencilFunc= THREE.AlwaysStencilFunc;
node.material.stencilFail= THREE.ReplaceStencilOp;
node.material.stencilZFail= THREE.ReplaceStencilOp;
node.material.stencilZPass= THREE.ReplaceStencilOp;
}
});
},
init: function () {
const self = this;
const el = this.el;
const mesh = el.getObject3D("mesh");
if (mesh) {
el.object3D.renderOrder = self.data.order;
self.updateMaterials(mesh);
} else {
el.addEventListener("model-loaded", function () {
el.object3D.renderOrder = self.data.order;
const mesh = el.getObject3D("mesh");
if (mesh) {
self.updateMaterials(mesh);
}
});
}
},
});
The second is applied to other entities to get them to only draw in the area marked by that stencil mask (and set the renderOrder so they draw afterwards):
AFRAME.registerComponent("stencil-member", {
dependencies: ["mesh"],
schema: {
mask: { default: 1 },
order: { default: 2 },
},
updateMaterials: function (mesh) {
const self = this;
mesh.traverse(function (node) {
mesh.renderOrder = self.data.order;
if (node.material) {
node.material.colorWrite = true;
node.material.depthWrite = true;
node.material.stencilWrite = true;
node.material.stencilRef = self.data.mask;
node.material.stencilFuncMask = 0xFF;
node.material.stencilFunc = THREE.EqualStencilFunc;
node.material.stencilFail = THREE.KeepStencilOp;
node.material.stencilZFail = THREE.KeepStencilOp;
node.material.stencilZPass = THREE.ReplaceStencilOp;
}
});
},
init: function () {
const self = this;
const el = this.el;
const mesh = el.getObject3D("mesh");
if (mesh) {
el.object3D.renderOrder = this.data.order;
self.updateMaterials(mesh);
} else {
el.addEventListener("model-loaded", function () {
el.object3D.renderOrder = self.data.order;
const mesh = el.getObject3D("mesh");
if (mesh) {
self.updateMaterials(mesh);
}
});
}
},
});
This works if I add the stencil-member component to a shape:
<a-plane
id="stencil"
stencil-surface="mask: 1"
>
<a-box
id="member"
position="0 0 -0.25"
scale="0.5 2 1"
color="#AAFFFF"
stencil-member="mask: 1"
></a-box>
</a-plane>
But I get weird results when I apply it to a model inside the same plane:
<a-assets>
<a-asset-item
id="model1"
src="https://cdn.glitch.global/ed7502f7-8600-47d9-8c66-96d695a11101/RobotExpressive.glb?v=1653017847384"
></a-asset-item>
</a-assets>
<a-entity
id="member2"
gltf-model="#model1"
position="0 0 -0.25"
scale="0.1 0.1 0.1"
stencil-member="mask: 1"
></a-entity>
With this, only the arms and the legs of the model (taken from https://threejs.org/examples/#webgl_animation_skinning_morph) render. Any ideas why this doesn't work? Is there something weird about the model? Is it an issue with the render ordering? Thank you!

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

Convert GIF Player Web Component into React Component?

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.

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>

value of 'this' being destroyed by listener

I have the following functions:
function moveDown(event) {
const submenus = document.getElementsByClassName('sub-menu')
var navbar = document.getElementById("menu-1");
var sub
if (event.target.innerHTML === "ABOUT US") {
sub = submenus[0]
} else {
sub = submenus[1]
}
var rect = sub.getBoundingClientRect();
navbar.style.marginBottom = rect.height + "px"
}
function moveUp(event) {
var navbar = document.getElementById("menu-1");
navbar.style.marginBottom = 0
}
both of those are fine. Can someone explain why 'this' is being destroyed with the following:
(function() {
var takeAction = document.getElementsByClassName('takeAction')[0]
var aboutUs = document.getElementsByClassName('aboutUs')[0]
aboutUs.addEventListener('mouseover', function(event) {
moveDown(event)
}, {
passive: false
})
takeAction.addEventListener('mouseover', function(event) {
moveDown(event)
}, {
passive: false
})
aboutUs.addEventListener('mouseleave', function(event) {
moveUp(event)
}, {
passive: false
})
takeAction.addEventListener('mouseleave', function(event) {
moveUp(event)
}, {
passive: false
})
})()
but works properly if I do:
(function() {
var takeAction = document.getElementsByClassName('takeAction')[0]
var aboutUs = document.getElementsByClassName('aboutUs')[0]
aboutUs.addEventListener('mouseover', moveDown,{passive: false})
takeAction.addEventListener('mouseover',moveDown,{passive: false})
aboutUs.addEventListener('mouseleave', moveUp,{passive: false})
takeAction.addEventListener('mouseleave',moveUp,{passive: false})
})()
Why does passing the event into an anonymous and then passing it to moveDown cause a different value for 'this' as opposed to just calling moveDown directly.
NOTE: in the second example moveDown and moveUp have no arguments.

How to mannage refs in react

This below code is to create svg shapes via React components.
Explanation of the code : I am doing composition in the react components first I have a parent g node and after that I have a child g node and in each child g node i have svg path.
what i am doing is : i am creating the shape and after that i am transforming that shape.
i have to transform the each shape individual the problem is that if we do this using the state of the parent it will work fine for the last element but what about is we want to change the transformation of the first element i.e we want to resize the shape
var ParentGroupThread = React.createClass({
getInitialState: function() {
return {
path: {}
};
},
render: function() {
Interfaces.DrawingThreads.ParentGroupThread = this;
var totalDrawing = Object.keys(this.state.path).length;
var drawings = [];
var pathState = this.state.path;
for (var i = 0; i < totalDrawing; i++) {
Interfaces.DrawingThreads.ParentThreadRef = pathState[i].ParentThreadId;
Interfaces.DrawingThreads.ThreadRef = pathState[i].threadIdPath;
drawings.push(React.createElement(ParentThread, {
ParentThreadId: pathState[i].ParentThreadId,
threadIdPath: pathState[i].threadIdPath,
d: pathState[i].d,
fillPath: pathState[i].fillPath,
strokePath: pathState[i].strokePath,
storkeWidthPath: pathState[i].storkeWidthPath,
classNamePath: pathState[i].classNamePath,
dataActionPath: pathState[i].dataActionPath,
dash: pathState[i].dash
}));
}
return React.createElement('g', {
id: 'ParentGroupThread',
key: 'ParentGroupThread_Key'
}, drawings);
}
});
var ParentThread = React.createClass({
getInitialState: function() {
return {
transformation: ''
};
},
render: function() {
Interfaces.DrawingThreads.ParentThread[Interfaces.DrawingThreads.ParentThreadRef] = this;
return React.createElement('g', {
id: this.props.ParentThreadId,
key: this.props.ParentThreadId + '_Key',
ref: this.props.ParentThreadId
},
React.createElement(Thread, {
threadIdPath: this.props.threadIdPath,
key: this.props.threadIdPath + '_Key',
d: this.props.d,
fillPath: this.props.fillPath,
strokePath: this.props.strokePath,
storkeWidthPath: this.props.storkeWidthPath,
transformPath: this.props.transformPath,
classNamePath: this.props.classNamePath,
dataActionPath: this.props.dataActionPath,
transformation: this.state.transformation,
dash: this.props.dash
})
);
}
});
var Thread = React.createClass({
getInitialState: function() {
return {
transformation: ''
};
},
render: function() {
Interfaces.DrawingThreads.Thread[Interfaces.DrawingThreads.ThreadRef] = this;
return React.createElement('path', {
id: this.props.threadIdPath,
d: this.props.d,
fill: this.props.fillPath,
stroke: this.props.strokePath,
strokeWidth: this.props.storkeWidthPath,
className: this.props.classNamePath,
'data-action': 'pathhover',
strokeDasharray: this.props.dash,
vectorEffect: 'non-scaling-stroke',
transformation: this.state.transformation,
ref: this.props.threadIdPath
});
}
});
ReactDOM.render(React.createElement(ParentGroupThread), document.getElementById('threads'));

Categories

Resources