I am trying to create a struct that has a large enough data buffer to hold HTML5 canvas ImageData larger than 64 x 64 pixels. The struct and implementation are defined here in rust:
// src/lib.rs
use wasm_bindgen::prelude::*;
extern crate fixedbitset;
extern crate web_sys;
#[wasm_bindgen]
pub struct CanvasSource {
width: u32,
height: u32,
data: Vec<u8>,
}
#[wasm_bindgen]
impl CanvasSource {
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
// returns pointer to canvas image data
pub fn data(&self) -> *const u8 {
self.data.as_ptr()
}
pub fn cover_in_blood(&mut self) {
let blood: Vec<u8> = vec![252, 3, 27, 255];
let mut new_data = blood.clone();
for _ in 0..self.width {
for _ in 0..self.height {
let pixel = blood.clone();
new_data = [new_data, pixel].concat()
}
}
self.data = new_data;
}
pub fn new(width: u32, height: u32, initial_data: Vec<u8>) -> CanvasSource {
let data_size = (width * height) as usize;
let mut data = initial_data;
data.resize(data_size, 0);
CanvasSource {
width,
height,
data,
}
}
}
...and called from here in Typescript:
import { useEffect, useRef, useState } from "react";
import { CanvasSource } from "rust-canvas-prototype";
import { memory } from "rust-canvas-prototype/rust_canvas_prototype_bg.wasm";
import styles from "./DirectCanvas.module.css";
const getRenderLoop = (
source: CanvasSource,
ctx: CanvasRenderingContext2D,
) => {
if (source && ctx) {
const loop = () => {
const sourceDataPtr = source.data();
const width = source.width();
const height = source.height();
const regionSize = width * height * 4;
const pixelData = new Uint8ClampedArray(
memory.buffer,
sourceDataPtr,
regionSize
)
const imageData = new ImageData(pixelData, width, height);
ctx.putImageData(imageData, 0, 0)
};
return loop;
}
return null;
}
export function DirectCanvas() {
const [source, setSource] = useState<CanvasSource>();
const [ctx, setCtx] = useState<CanvasRenderingContext2D | null>(null);
const [paused, setPaused] = useState<boolean>(false);
// undefined on init, null when paused
const [animationId, setAnimationId] = useState<number>(0);
const canvasElement = useRef<HTMLCanvasElement>(null);
const initialized = source && ctx;
// initialization
useEffect(() => {
if (!source) {
console.log("loading source");
let [width, height] = [100, 100];
// uncomment below to cause error
// [width, height] = [358, 358]
setSource(CanvasSource.new(width, height, new Uint8Array([])))
}
if (source && !ctx && canvasElement.current) {
canvasElement.current.height = source.height();
canvasElement.current.width = source.width();
setCtx(canvasElement.current.getContext("2d"));
}
}, [source, ctx])
useEffect(() => {
if (initialized) {
const renderLoop = getRenderLoop(source, ctx);
if (renderLoop) {
renderLoop();
setTimeout(() => {
setAnimationId(prev => prev + 1);
}, 10)
}
}
}, [source, ctx, animationId]);
return (
<div className={styles.Container}>
<span className={styles.Controls}>
<button onClick={() => source?.cover_in_blood()}>Splatter</button>
</span>
<canvas ref={canvasElement}></canvas>
</div>
)
}
The function works correctly for sizes of ~100x100 or less, but once the total area begins to exceed that JS throws the following error:
Uncaught RangeError: attempting to construct out-of-bounds Uint8ClampedArray on ArrayBuffer
loop DirectCanvas.tsx:23
DirectCanvas DirectCanvas.tsx:77
...
Preliminary research suggests that it is a stack size problem on Rust's end, but attempts to increase the stack size in the config.toml throw errors of their own:
= note: rust-lld: error: unknown argument: -Wl,-zstack-size=29491200
How do I allocate a large enough stack size to paint to canvases larger than 100x100? (minimal reproducible example found here)
Welp, egg's on my face: the problem wasn't in fact on the rust side.
Uint8Array's default initialization size is just not large enough to hold ImageData larger than ~100x100. Ensuring the CanvasSource was initialized to the correct size did the trick:
// initialization
useEffect(() => {
if (!source) {
console.log("loading source");
[width, height] = [1000, 1000]
// the fixed line: added third argument
setSource(CanvasSource.new(width, height, new Uint8Array(width * height * 4)))
}
...
}, [source, ctx])
On the rust side, I did have a line of code that I thought would handle this:
// src/lib.rs
pub fn new(width: u32, height: u32, initial_data: Vec<u8>) -> CanvasSource {
let data_size = (width * height) as usize;
let mut data = initial_data;
// here
data.resize(data_size, 0);
CanvasSource {
width,
height,
data,
}
}
But, evidently, resizing it on the Rust end either (1) doesn't update the space that JS was looking for, or (2) I am misusing Vec.resize(). Both are possible. Thanks everyone for pointing stuff out.
Related
Background:
I am animating a circle down my web page to follow a certain path using GSAP's motion path plugin. I have scaled the path to ensure it follows the same proportions on all devices using this code:
let w = window.innerWidth;
let h = document.querySelector("main").offsetHeight;
const pathSpec = "M305.91,0c8.54,101.44,17.07,203.69,5.27,304.8s-45.5,202.26-112.37,279c-36,41.37-80.92,74.88-113.34,119.16-66.32,90.59-70.8,210.86-72.71,323.13C9.69,1206.79,9.48,1387.9,1.71,1568c-8,186.11,25.77,370.42,48.07,554.43q4.36,36,8.07,72.07c14.18,140.22,9.95,281.38,8.06,422-1.87,139.1,26.43,260.75,66.38,393.67l82.47,274.39,8.25,27.44,29.87,129c40.8,176.3,87,363.23,64.51,545.49-11,89.33-3.5,182.44-2.28,272.84l3.83,286.1c3.63,188.78,5.07,377.63,7.6,566.44l3.83,286.08c1.27,94.55-4,191.79,3.84,286,14.37,172.9-10,353.08-14.33,527.78q-7,283.21,1.14,566.55,8.14,281.61,31.34,562.53c7.43,90,21.31,180.76,26.7,270.47,5.6,93.24-4,190-6,283.47l-3.45,164.33";
const yScale = h / 8059.08;
const xScale = w / 380.28;
let newPath = "";
const pathBits = Array.from(pathSpec.matchAll(/([MmLlHhVvCcSsQqTtAa])([\d\.,\se-]*)/g));
console.log(pathBits);
pathBits.forEach((bit) => {
const command = bit[1];
const coordinates = bit[2];
newPath += command;
const coordArray = coordinates.split(/[,\s]/g);
// console.log("coordArray", coordArray);
newPath += coordArray.map((coord, index) => {
if (index % 2 == 0) {
return parseFloat(coord) * yScale;
}
else {
return parseFloat(coord) * xScale;
}
}).join(',');
});
So, essentially, I've created coordArray to split the array into the x and y coordinates and added it to newPath which is the scaled version. Then, I've added newPath to motionPath using:
const tlCircle = gsap.timeline();
tlCircle.to(".circle-animation", {
scrollTrigger: {
scroller: "main",
trigger: ".circle-animation",
start: "top 10%",
end: "max",
scrub: 1,
},
ease: "true",
motionPath: {
path: newPath,
autoRotate: true
}
});
When I put the pathSpec var in the path parameter, my circle animates but not when I put the newPath in, even though the web console shows it is in the same format as pathSpec but just scaled.
I've tried to debug this console logs to see what the input and outputs are which are what I want -- newPath is a. svg path in a string but scaled to fit my window but it doesn't work when I use it in motionPath.
I have been trying to create a little animation on canvas using React.js but having trouble interacting my Keydown function with my useState, for some reason, it wouldn't define my useState value, and therefore I couldn't change the value when I press the key. Here is my code:
const Canvas = () => {
const { innerWidth: innerwidth, innerHeight: innerheight } = window;
const contextRef= useRef()
const [placement, setPlacement] = useState({position: {x:100,y:100}, width:30,height:30, velocity: {x:0, y:0}})
const gravity = 4.5
const draw = (context) => {
context.fillStyle = 'red'
context.fillRect(placement.position.x, placement.position.y, placement.width, placement.height);
};
const update = (context, canvas) => {
draw(context)
setPlacement(placement.position.y += placement.velocity.y)
if(placement.position.y+placement.height+placement.velocity.y <= canvas.height){
setPlacement(placement.velocity.y += gravity)
}else{
setPlacement(placement.velocity.y = 0)
}
}
const animate = (context, width, height, canvas ) => {
requestAnimationFrame(() => { animate(context, width, height, canvas ) })
context.clearRect(0, 0, width, height)
update(context, canvas)
}
useEffect(() => {
const canvas = contextRef.current;
const context = canvas.getContext("2d");
canvas.width= innerwidth
canvas.height= innerheight
animate(context, canvas.width, canvas.height, canvas)
}, []);
const handleKeydown = (e) => {
switch(e.keyCode) {
case 37:
console.log("left")
return "left";
case 39:
return "right";
case 32:
return setPlacement(placement.position.y -= 20);
default:
console.log("keyCode is " + e.keyCode)
console.log(placement.velocity)
return 'default';
}
}
return (
<canvas ref={contextRef} tabIndex={-1} onKeyDown={ (e)=>{handleKeydown(e)}} />
);
};
As you can see I have been trying to console.log my placement in my function, but I am getting either a "0" or undefined. Here is the error I get when I hit my spacebar:
What can I try next?
You are changing the format of the state value completely from what it is initially set to.
Your default is
useState({position: {x:100,y:100}, width:30,height:30, velocity: {x:0, y:0}})
However you then go on to wipe this out with a single value
setPlacement(placement.velocity.y += gravity)
The object is now gone completely, and you are storing one integer.
This also makes no sense as you are doing an assignment to placement.velocity.y inside the actual argument. I think you thought this would granularly change this one property but that's not how it works. What you need to do is pass the whole object, with the one value you want to change altered. You can do this by merging the old and the new state.
// before
setPlacement(placement.position.y += placement.velocity.y)
// after
setPlacement(prevPlacement => ({ ...prevPlacement, position: { ...prevPlacement.position, y: prevPlacement.position.y + prevPlacement.velocity.y } }))
// before
setPlacement(placement.velocity.y += gravity)
// after
setPlacement(prevPlacement => ({ ...prevPlacement, velocity: { ...prevPlacement.velocity, y: prevPlacement.velocity.y + gravity } }))
// before
setPlacement(placement.velocity.y = 0)
// after
setPlacement(prevPlacement => ({ ...prevPlacement, velocity: { ...prevPlacement.velocity, y: 0 } }))
// before
setPlacement(placement.position.y -= 20);
// after
setPlacement(prevPlacement => ({ ...prevPlacement, position: { ...prevPlacement.position, y: placement.position.y - 20 } }))
As an aside, with deep structures, this is obviously messier. You might consider switching to multiple state items -- but it depends on what changes together (that's optimal). Probably it would be 3 state items, position, velocity and dimensions. That avoids very-deep state updates.
Or you can use some library like https://github.com/immerjs/immer which makes mutating deep state in an immutable way much easier.
I'm using threejs to render some models (gltf/glb). All of them are of different sizes, some are big and some are small. Now I want scale all of them to the same size.
if i use mesh.scale() that would scale the object relative to it's own size. Is there any way to achieve this without manually calculating each model's scale?
UPDATE:
Here's my code
function loadModels(points) {
// loader
const loader = new GLTFLoader();
const dl = new DRACOLoader();
dl.setDecoderPath("/scripts/decoder/");
loader.setDRACOLoader(dl);
let lengthRatios;
const meshes = [];
for (let i = 0; i < store.length; i++) {
loader.load(
store[i].model,
(gltf) => {
const mesh = gltf.scene;
const meshBounds = new THREE.Box3().setFromObject(mesh);
// Calculate side lengths of model1
const lengthMeshBounds = {
x: Math.abs(meshBounds.max.x - meshBounds.min.x),
y: Math.abs(meshBounds.max.y - meshBounds.min.y),
z: Math.abs(meshBounds.max.z - meshBounds.min.z),
};
if (lengthRatios) {
lengthRatios = [
lengthRatios[0] / lengthMeshBounds.x,
lengthRatios[1] / lengthMeshBounds.y,
lengthRatios[2] / lengthMeshBounds.z,
];
} else {
lengthRatios = [
lengthMeshBounds.x,
lengthMeshBounds.y,
lengthMeshBounds.z,
];
}
meshes.push(mesh);
if (meshes.length == store.length) {
addModels();
}
},
(xhr) => {
console.log((xhr.loaded / xhr.total) * 100 + "% loaded");
},
(error) => {
console.log("An error happened");
}
);
}
function addModels() {
// Select smallest ratio in order to contain the models within the scene
const minRation = Math.min(...lengthRatios);
for (let i = 0; i < meshes.length; i++) {
// Use smallest ratio to scale the model
meshes[i].scale.set(minRation, minRation, minRation);
// position the model/mesh
meshes[i].position.set(...points[i]);
// add it to the scene
scene.add(meshes[i]);
}
}
}
You should create a bounding box around each model.
//Creating the actual bounding boxes
mesh1Bounds = new THREE.Box3().setFromObject( model1 );
mesh2Bounds = new THREE.Box3().setFromObject( model2 );
// Calculate side lengths of model1
let lengthMesh1Bounds = {
x: Math.abs(mesh1Bounds.max.x - mesh1Bounds.min.x),
y: Math.abs(mesh1Bounds.max.y - mesh1Bounds.min.y),
z: Math.abs(mesh1Bounds.max.z - mesh1Bounds.min.z),
};
// Calculate side lengths of model2
let lengthMesh2Bounds = {
x: Math.abs(mesh2Bounds.max.x - mesh2Bounds.min.x),
y: Math.abs(mesh2Bounds.max.y - mesh2Bounds.min.y),
z: Math.abs(mesh2Bounds.max.z - mesh2Bounds.min.z),
};
// Calculate length ratios
let lengthRatios = [
(lengthMesh1Bounds.x / lengthMesh2Bounds.x),
(lengthMesh1Bounds.y / lengthMesh2Bounds.y),
(lengthMesh1Bounds.z / lengthMesh2Bounds.z),
];
// Select smallest ratio in order to contain the models within the scene
let minRatio = Math.min(...lengthRatios);
// Use smallest ratio to scale the model
model.scale.set(minRatio, minRatio, minRatio);
To get P5 to work with React, I am using the P5Wrapper import.
I got a simple starfield animation to work on my tile, but the performance is an issue. The animation slows to a crawl at 512 "star" objects, so I scaled it back to 128. However, even at 128, the FPS seems much too low, averaging below 30 FPS. I am looking for ways to speed up P5's performance in React so that the animations can run closer to 60 FPS.
P5 code:
function sketch (p) {
const star = () => {
const x = p.random(-TILE_SIZE/2, TILE_SIZE/2)
const y = p.random(-TILE_SIZE/2, TILE_SIZE/2)
const z = p.random(TILE_SIZE)
return { x, y, z }
}
const stars = new Array(128)
p.setup = () => {
p.createCanvas(TILE_SIZE, TILE_SIZE)
for (let i = 0; i < stars.length; i++) {
stars[i] = star()
}
}
const update = (coord) => {
const { z } = coord
let newZ = z - 8
if (newZ < 1) {
newZ = p.random(TILE_SIZE)
}
return { ...coord, z: newZ }
}
const show = (coord) => {
const { x, y, z } = coord
p.fill(255)
p.noStroke()
const sx = p.map(x / z, 0, 1, 0, TILE_SIZE)
const sy = p.map(y / z, 0, 1, 0, TILE_SIZE)
const r = p.map(z, 0, TILE_SIZE, 4, 0)
p.ellipse(sx, sy, r, r)
}
p.draw = () => {
p.background(0)
p.translate(TILE_SIZE/2, TILE_SIZE/2)
for (let i = 0; i < stars.length; i++) {
stars[i] = update(stars[i])
show(stars[i])
}
}
}
How P5Wrapper is used:
import P5Wrapper from 'react-p5-wrapper'
...
render (
<ItemContainer key={uuidv4()}>
<header>
{name}
<p>{description}</p>
</header>
<P5Wrapper sketch={sketch} />
</ItemContainer>
)
How the starfield tile actually looks (2 tiles).
I am planning to add more animations depending on performance. Or switching to SVG.
Resolved the frame-rate issue without changing the actual animation logic. There could still be plenty of performance optimization that is still needed.
I noticed that the animation gets progressively slower as I un-mount and re-mount the component. Digging into the Github issue results in this post about performance degradation. The poster provided a PR and commit that was never merged and the repository haven't been updated for over a year.
Instead, it's better to remove the package and just create your own component following the poster's update:
import React from 'react'
import p5 from 'p5'
export default class P5Wrapper extends React.Component {
componentDidMount() {
this.canvas = new p5(this.props.sketch, this.wrapper)
if( this.canvas.myCustomRedrawAccordingToNewPropsHandler ) {
this.canvas.myCustomRedrawAccordingToNewPropsHandler(this.props)
}
}
componentWillReceiveProps(newprops) {
if(this.props.sketch !== newprops.sketch){
this.canvas.remove()
this.canvas = new p5(newprops.sketch, this.wrapper)
}
if( this.canvas.myCustomRedrawAccordingToNewPropsHandler ) {
this.canvas.myCustomRedrawAccordingToNewPropsHandler(newprops)
}
}
componentWillUnmount() {
this.canvas.remove()
}
render() {
return <div ref={wrapper => this.wrapper = wrapper}></div>
}
}
This at the very least resolved the performance degradation issue for mounting and unmounting the component. Additionally, my frames have jumped from sub 30 to nearly 60 FPS. This could be because I also imported the latest P5 package since I no longer rely on react-p5-wrapper to do the imports.
I am trying to get a react application to render the conways life method. I am still a bit new to React.js and was hoping someone could shed some light on why my code is broken and also how to fix it. And perhaps how to prevent it from happening in the future. Below is my code
import React, { Component } from 'react';
import Life from './life';
import './App.css';
/**
* Life canvas
*/
class LifeCanvas extends Component {
/**
* Constructor
*/
constructor(props) {
super(props);
this.life = new Life(props.width, props.height);
this.life.randomize();
}
/**
* Component did mount
*/
componentDidMount() {
requestAnimationFrame(() => {
this.animFrame();
});
}
/**
* Handle an animation frame
*/
animFrame() {
//
// !!!! IMPLEMENT ME !!!!
//
let width = this.props.width;
let height = this.props.height;
// Request another animation frame
// Update life and get cells
let cells = this.life.getCells();
// Get canvas framebuffer, a packed RGBA array
let canvas = this.refs.canvas;
let ctx = canvas.getContext('2D');
let imageData = ctx.getImageData(0, 0, width, height);
// Convert the cell values into white or black for the canvas
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let index = (y * width + x) * 4;
let lifeStatus = cells[y][x];
let color = lifeStatus === 0 ? 0xff : 0x00;
imageData.data[index + 0] = color; // R
imageData.data[index + 1] = color; // G
imageData.data[index + 1] = color; // B
imageData.data[index + 1] = color; // A
}
}
// Put the new image data back on the canvas
ctx.putImageData(imageData, 0, 0);
// Next generation of life
this.life.step();
requestAnimationFrame(() => {
this.animFrame();
});
}
/**
* Render
*/
render() {
return (
<canvas
ref="canvas"
width={this.props.width}
height={this.props.height}
/>
);
}
}
/**
* Life holder component
*/
class LifeApp extends Component {
/**
* Render
*/
render() {
return (
<div>
<LifeCanvas width={600} height={600} />
</div>
);
}
}
/**
* Outer App component
*/
class App extends Component {
/**
* Render
*/
render() {
return (
<div className="App">
<LifeApp />
</div>
);
}
}
export default App;
When it renders the page the error in the subject line is produced in big red letters. I am not sure what it means. Please help.
It is saying ctx is null. getContext() will return null when the passed context type is not valid. In your case you passed 2D the proper context type string is 2d
var c = document.createElement('canvas');
var ctx = c.getContext('2D');
console.log("ctx is: ",ctx);
ctx = c.getContext('2d');
console.log("ctx is now: ",ctx);