Search code examples
reactjsgoogle-chromecreate-react-appp5.js

Undefined property when extending p5 class


I have been coding a p5js app and recently I've switched to using React. After adapting the project everything seemed to be working correctly in Firefox, but when I tried to test it in Chrome, the canvas wasn't being given the correct width and height, but NaN instead.

I am extending the p5 class with this code:

export class CustomCanvas extends p5 {
    sizeScalingFactor = 0.9;

    resize() {
        const canvasSize = this.calculateSize();
        this.resizeCanvas(canvasSize, canvasSize);
    }

    get boundingRect() {
        return this.canvas.getBoundingClientRect();
    }

    calculateSize() {
        return (
            Math.min(
                this.windowWidth,
                this.windowHeight - this.boundingRect.top
            ) * this.sizeScalingFactor
        );
    }
}

After some debugging in Chrome DevTools I've narrowed the problem down to sizeScalingFactor being undefined when resize() is called, making calculateResize() return NaN. However, this doesn't make sense at all because the constructor should be the first thing to be called, defining the property, and then any other methods.

I've also checked the code generated by create-react-app and the property definition is converted to this:

constructor(...args) {
    super(...args);
    this.sizeScalingFactor = 0.9;
}

However, doing this myself and debugging once more in Chrome DevTools, results in resize() being called before the constructor once again.

I have more classes with properties defined this way and none have this problem, neither in Chrome nor in Firefox. Also, I should add that the project worked perfectly without react and that everything works correctly if I make the property static (and then access it directly from the class) or I put it in the outer scope.

I will attach some code that might be relevant as well.

Sketch definition for instance mode:

import { generateGradientArray } from "./palette-manager";
import { CustomCanvas, Circle } from "./classes";

export function createSketch(maxNodeCount, ref) {
    /*
     * @param {CustomCanvas} c
     */
    function sketch(c) {
        const lineColors = generateGradientArray(
            c,
            maxNodeCount,
            //["#B9E3C6", "#59C9A5", "#D81E5B", "#23395B", "#FFFD98"]
            ["#AFCBFF", "#254441", "#43AA8B", "#B2B09B", "#EF3054"]
        );

        const circle = new Circle(c, lineColors);

        function resizeComponents() {
            c.resize();
            circle.resize();
        }

        c.setup = () => {
            resizeComponents();
        };

        c.draw = () => {
            c.clear();
            c.translate(c.width / 2, c.height / 2);
            c.scale(1, -1);

            circle.draw();
        };

        c.windowResized = () => {
            resizeComponents();
        };
    }

    return new CustomCanvas(sketch, ref);
}

Sketch component:

import React, { useEffect, useRef } from "react";

import style from "./Sketch.module.css";
import { createSketch } from "../sketch";

export default function Sketch() {
    const containerRef = useRef();

    useEffect(() => {
        const sketch = createSketch(700, containerRef.current);

        return () => {
            sketch.remove();
        };
    }, []);

    return <div ref={containerRef} className={style.container}></div>;
}

Any idea why this is happening? Thanks in advance.


Solution

  • The issue is that the in some browsers the call to the p5 constructor triggers a call to this.resize() immediately in this context, while in others the call to the constructor returns and then the call to this.resize() happens in an event handler. The difference is whether or not the document.readyState is equal to 'complete' or not at the time the constructor is called. You can see in the generated code why that would be problematic:

        constructor(...args) {
            super(...args); // calls this.resize();
            this.sizeScalingFactor = 0.9;
        }
    
        resize() {
            // When this is called from the base class constructor
            // sizeScalingFactory is still undefined
        }
    

    Here's sample code on replit.com (or check out the running version).