mrconcerned
mrconcerned

Reputation: 1965

How to upload image in p5js using React?

I have posted a question regarding moving image in p5js which is answered in this question. I have another problem with p5js when I try to upload an image in React input field.

I take input as

<input
  type="file"
  name="file"
  id="file"
  onChange={(e) => setSelectedFile(e.target.files[0])}
/>

And use it in draw function like following:

if (selectedFile != null) {
  const url = URL.createObjectURL(selectedFile);
  backgroundImage = p5.loadImage(url);
}

The strange behavior is that it does not change the image and gives blank screen canvas. I want to upload image as input element, not createFileInput().

I have uploaded the complete code into StackBlitz.

I don't want to copy the code here because it is already mentioned in previous question and StackBlitz.

Any help is appreciated.

Upvotes: 3

Views: 343

Answers (1)

George Profenza
George Profenza

Reputation: 51867

There are two main things you're overlooking:

  1. as previously mentioned in the other question, you need to read/experiment/get an understanding of scope in JavaScript in general but also in React
  2. In this section: onChange={(e) => setSelectedFile(e.target.files[0])}, files[0] points to data about the selected file (e.g. filename, how large is it (in bytes), etc., but it's not the actual file. const url = URL.createObjectURL(selectedFile); would simply point to the stackblitz url where your project lives and passing that loadImage() will result in a 1x1 pixel image. You need to use FileReader to load bytes from files[0]
  3. The point of the console log message hints in my previous comments were to help you debug and figure out where the "small bug" is. e.g. if how you expect the code to run is how it actually runs (otherwise test/correct assumptions/fix code/etc.).

Here's a tweaked version of your code:

import React, { useState } from 'react';
import Sketch from 'react-p5';
import './style.css';

export default function App() {
  let backgroundImage;
  let dragging = false; // Is the object being dragged?
  let rollover = false; // Is the mouse over the ellipse?

  let x, y, w, h; // Location and size
  let offsetX, offsetY; // Mouseclick offset

  let theP5Reference;

  const setup = (p5, parentRef) => {
    p5.createCanvas(1000, 500).parent(parentRef);
    // Starting location
    x = 350;
    y = 50;

    const url =
      'https://upload.wikimedia.org/wikipedia/commons/thumb/b/b6/Image_created_with_a_mobile_phone.png/640px-Image_created_with_a_mobile_phone.png';
    backgroundImage = p5.loadImage(url);
    // Dimensions
    w = 700;
    h = 700;
    // keep a reference to p5 for reuse outside of this function
    theP5Reference = p5;
  };

  const draw = (p5) => {
    p5.background(233);
    if (
      p5.mouseX > x &&
      p5.mouseX < x + w &&
      p5.mouseY > y &&
      p5.mouseY < y + h
    ) {
      rollover = true;
    } else {
      rollover = false;
    }

    // Adjust location if being dragged
    if (dragging) {
      x = p5.mouseX + offsetX;
      y = p5.mouseY + offsetY;
    }

    p5.image(backgroundImage, x, y);
    drawMaskOverlay(p5);
  };

  const drawMaskOverlay = (p5) => {
    p5.fill(255);
    p5.noStroke();
    p5.beginShape();
    // CW vertex winding
    p5.vertex(0, 0);
    p5.vertex(p5.width, 0);
    p5.vertex(p5.width, p5.height);
    p5.vertex(0, p5.height);
    // cutout contour CCW
    p5.beginContour();
    p5.vertex(400, 100);
    p5.vertex(400, 400);
    p5.vertex(600, 400);
    p5.vertex(600, 100);
    p5.endContour();
    p5.endShape();
  };

  const mousePressed = (p5) => {
    if (
      p5.mouseX > x &&
      p5.mouseX < x + w &&
      p5.mouseY > y &&
      p5.mouseY < y + h
    ) {
      dragging = true;

      offsetX = x - p5.mouseX;
      offsetY = y - p5.mouseY;
    }
  };

  const mouseReleased = (p5) => {
    // Quit dragging
    dragging = false;
  };

  const onFileSelected = (e) => {
    const files = e.target.files;
    if (!files || !files[0]) {
      console.log('no files selecting, early exit');
      return;
    }

    // create a file reader to load the image (and get base64 data)
    const reader = new FileReader();

    reader.addEventListener('load', function (evt) {
      // event.target.result is the base64 image
      // use the references to p5 from setup()
      backgroundImage = theP5Reference.loadImage(evt.target.result);
    });
    // trigger the load / base64 conversion (when ready will trigger the event handler above)
    reader.readAsDataURL(files[0]);
  };

  return (
    <div className="App">
      <div>
        <h1>Select an image</h1>
        <input type="file" name="file" id="file" onChange={onFileSelected} />
        <Sketch
          setup={setup}
          draw={draw}
          mouseReleased={mouseReleased}
          mousePressed={mousePressed}
        />
      </div>
    </div>
  );
}

The key elements are these:

  1. react-p5 gives you a reference to p5 (the general library, not your actual sketch) in methods such as setup() (which is a good candidate as it triggers only once, at the start). You can keep a reference to p5 in a separate variable in the outer scope (so it's visible from outside setup()) (theP5Reference in the snippet above).
  2. the file <input> change handler (onFileSelected) is not inlined, as within the App's scope.
  3. In onFileSelected a FileReader is instantiated, the load is triggered via readAsDataURL and in the load handler, the previously stored theP5Reference reference to p5 is reused to simply call loadImage() on. Because the handler is within App's scope, backgroundImage is already visible.

As an excercise, I suggest you read more react specific scope, look at react-p5's source code and see if you can make use of this.sketch / props, etc. to access p5 from App without keeping storing a reference from setup().

Upvotes: 2

Related Questions