Uploading a three.js canvas to the server without saving it as a file

Currently, I am in the process of saving an image to the server using a three.js script that I have created. The script looks like the following:

actualCode(THREE);

function actualCode(THREE) {
    //Rendering variables
    const renderer = new THREE.WebGLRenderer({
        antialias: true
    });
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(30, 400.0 / 400, 1, 1000);

    //Object variables
    let texture;
    let paintedMug;

    //Preload image and then begin rendering
    const loader = new THREE.TextureLoader();
    texture = loader.load("images/djmug2.jpg", function (_tex) {
        init();

        renderImageSolo(17.5);
    });

    function init() {
        //Initialize scene and camera
        camera.position.set(0, 1.3, 11);
        camera.lookAt(scene.position);
        renderer.setSize(400, 400);
        
        //Set ambient light
        const light = new THREE.AmbientLight(0xffffff); // soft white light
        scene.add(light);

        //Draw white mug
        const muggeom = new THREE.CylinderGeometry(1.5, 1.5, 3.5, 240, 1);
        const mugmaterial = new THREE.MeshStandardMaterial({
            color: "#fff",
        });
        const mug = new THREE.Mesh(muggeom, mugmaterial);

        //Draw painting on mug with slightly larger radius
        const paintgeom = new THREE.CylinderGeometry(1.5001, 1.5001, 3.3, 240, 1, true);
        const paintmaterial = new THREE.MeshStandardMaterial({
            map: texture,
        });
        const paint = new THREE.Mesh(paintgeom, paintmaterial);

        //Group mug and paint together
        paintedMug = new THREE.Group();
        paintedMug.add(mug);
        paintedMug.add(paint);
        scene.add(paintedMug);
    }


    function renderImageSolo(angle) {
        const solo_renderer = new THREE.WebGLRenderer({
            antialias: true
        });
        solo_renderer.setSize(renderer.domElement.width, renderer.domElement.height);
        solo_renderer.domElement.style.marginTop = "0em";
        solo_renderer.domElement.id = "canvas";
        document.body.appendChild(solo_renderer.domElement);
        const solo_scene = new THREE.Scene();
        const light = new THREE.AmbientLight(0xffffff);
        solo_scene.add(light);

        //Draw painting alone
        const paintgeom = new THREE.CylinderGeometry(1.5, 1.5, 3.3, 240, 1, true);
        const paintmaterial = new THREE.MeshStandardMaterial({
            map: texture,
        });
        const paint = new THREE.Mesh(paintgeom, paintmaterial);
        solo_scene.add(paint);
        paint.rotation.y = angle
        solo_scene.background = new THREE.Color(0x04F404);

        solo_renderer.render(solo_scene, camera);
        saveit();
    }
}

Afterwards, I try to save the generated image using ajax as shown below:

function saveit() {
    const canvas = document.getElementById('canvas');
    var photo = canvas.toDataURL('image/jpeg');
    $.ajax({
        method: 'POST',
        url: 'photo_upload.php',
        data: {
            photo: photo
        }
    });
}

The contents of "photo_upload.php" are as follows:

$data = $_POST['photo'];
    list($type, $data) = explode(';', $data);
    list(, $data)      = explode(',', $data);
    $data = base64_decode($data);

    mkdir($_SERVER['DOCUMENT_ROOT'] . "/photos");

    file_put_contents($_SERVER['DOCUMENT_ROOT'] . "/photos/".time().'.png', $data);
    die;

Despite my efforts, nothing seems to be saved on the server under "/photos" directory. In addition, when attempting to right-click and "save image", the saved image is just a black square instead of what is displayed on the screen.

Answer №1

Updated JavaScript code for saving to PHP server with modern approach:

  1. Refactor the JavaScript code and implement the saving function using fetch
import * as THREE from 'https://cdn.skypack.dev/three';

document.addEventListener("DOMContentLoaded", _e => {

  //Create a div element to display messages
  const messageDiv = document.createElement('div');
  messageDiv.classList.add('message');
  document.body.appendChild(messageDiv);

  let texture;

  const loader = new THREE.TextureLoader();
  messageDiv.textContent = "Loading texture...";
  texture = loader.load("https://i.imgur.com/TQZrUSP.jpeg", function(_tex) {
    console.log("Texture loaded");
    renderImageSolo(60);
  });

  function renderImageSolo(angle) {
    messageDiv.textContent = "Rendering 3D projection...";
    
    const solo_renderer = new THREE.WebGLRenderer({
      antialias: true,
      preserveDrawingBuffer: true 
    });
    solo_renderer.setSize(400, 400);
    document.body.appendChild(solo_renderer.domElement);
    const solo_scene = new THREE.Scene();

    const camera = new THREE.PerspectiveCamera(30, 400.0 / 400, 1, 1000);
    camera.position.set(0, 1.3, 11);
    camera.lookAt(solo_scene.position);
    
    const light = new THREE.AmbientLight(0xffffff); 
    solo_scene.add(light);

    const paintgeom = new THREE.CylinderGeometry(1.5, 1.5, 3.3, 240, 1, true);
    const paintmaterial = new THREE.MeshStandardMaterial({
      map: texture,
    });
    const paint = new THREE.Mesh(paintgeom, paintmaterial);
    
    solo_scene.add(paint);
    
    paint.rotation.y = angle
    
    solo_scene.background = new THREE.Color(0xffffff);
    solo_renderer.render(solo_scene, camera);

    saveImage(solo_renderer.domElement, "photo.jpeg")
  }

  function saveImage(canvas, filename) {
    messageDiv.textContent = "Uploading result...";

    canvas.toBlob(imgBlob => { 
      const fileform = new FormData();
      fileform.append('filename', filename);
      fileform.append('data', imgBlob);
      
      fetch('./photo_upload.php', {
        method: 'POST',
        body: fileform,
      })
      .then(response => {
        return response.json();
      })
      .then(data => {
        if (data.error) {
          messageDiv.classList.add('error');
          messageDiv.textContent = data.error;
        } else {
          messageDiv.classList.add('success');
          messageDiv.textContent = data.message;
        }
      })
      .catch(err => { 
        console.log(err);
        messageDiv.classList.add('error');
        messageDiv.textContent = err.message;
      });
    }, 'image/jpeg'); 
  }
});
  1. Create PHP script to handle saving on the server side
<?php

try {

  header('Content-type: application/json');

  $filename = $_POST['filename'];
  if (!$filename) {
    die(json_encode([
      'error' => "Could not read filename from request"
    ]));
  }
  
  $img = $_FILES['data'];
  if (!$img) {
    die(json_encode([
      'error' => "No image data in request"
    ]));
  }
  
  $savePath = $_SERVER['DOCUMENT_ROOT'] . "/photos/";
  if (!file_exists($savePath)) {
    if (!mkdir($savePath)) {
      die(json_encode([
        'error' => "Could not create dir $savePath"
      ]));
    }
  }
  
  $savePath .= $filename;
  if (!move_uploaded_file($img['tmp_name'], $savePath)) {
    echo json_encode([
      'error' => "Could not write to $savePath"
    ]);
  } else {
    $bytes = filesize($savePath);
    echo json_encode([
      'message' => "Image uploaded and saved to $savePath ($bytes bytes)"
    ]);
  }

} catch (Exception $err) {
  echo json_encode([
    'error' => $err->getMessage()
  ]);
}
  1. Add CSS styles for better readability of messages
body {
  font-family: Arial, Helvetica, sans-serif;
}
.message {
  text-align: center;
  padding: 1em;
  font-style: italic;
  color: dimgray;
}
.message.success {
  font-style: normal;
  font-weight: bold;
  color: forestgreen;
}
.message.error {
  font-style: normal;
  font-family: 'Courier New', Courier, monospace;
  white-space: pre-wrap;
  color: darkred;
}

2021-09-07 - Improved code using FormData in JavaScript and FILES in PHP for efficiency and clarity

Answer №2

To resolve this issue, consider initializing the renderer in the following manner:

const custom_renderer = new THREE.WebGLRenderer({
    antialias: true,
    preserveDrawingBuffer: true // CUSTOM FIX
});

I recommend exploring available resources that provide guidance on capturing a screenshot of your canvas. You can start by checking out:

Creating a 2D Snapshot of a Scene in Three.js

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Checking JSON formatted actions in Rails 4: A guide to testing

I'm in the process of testing a Rails application where all my actions return data formatted in json. An example of this is within the UsersController # POST /users.json def create @user = User.new(user_params) respond_to do |format| ...

The AJAX request is failing to send the most recent data for processing on the server side

Recently, I created a server-side processing script for datatables v1.10.0 that has been giving me some trouble. The server needs the product id to fetch records from the database, which it gets from a select2 plugin based selector selection. However, I ha ...

Creating dynamic height based on width using JavaScript

I'm trying to make a rectangle responsive based on the width of the window. This is my current logic: aspectRatio = 16 / 9 win = { width: window.innerWidth, height: window.innerHeight, } get browser() { const width = this.win.width - 250 ...

How can a Chrome extension transfer an ArrayBuffer or Blob from a content script to the background script without compromising its data type?

In my current script, I am downloading binary data using XHR in the content script and sending it to the background script: let me = this; let xhr = new XMLHttpRequest(); xhr.open('GET', url); xhr.responseType = 'arraybuffer'; xhr.onlo ...

Adding a class to an element can be easily achieved when both input fields have values

Is it possible to apply the "active" class to the element go_back_right only when both inputs with classes search_box_name and search_box_id have content in them? I've tried looking for solutions on this platform, but being new to JavaScript, I couldn ...

What could be causing my middleware to run twice?

A custom middleware was created in express.js/node.js to handle session checking. If a user ID is found in the session, it displays the user's menu; otherwise, it shows the default menu. For every page request, an ID check is performed and the user d ...

The magic of coding is in the combination of Javascript

My goal is to create a CSS animation using jQuery where I add a class with a transition of 0s to change the color, and then promptly remove that class so it gradually returns to its original color (with the original transition of 2s). The code below illus ...

Creating a toggle label using just CSS: a step-by-step guide

Recently, I was trying to create a toggle label using HTML, CSS, and JavaScript. Here is the code snippet that I experimented with: function genre_itemclick(item){ if(item.classList.contains('light_blue_border_button')) { ...

Updating attribute values in a dynamic JSON file with Node.js: A step-by-step guide

My task involves checking if the key in input.json file matches any key in the server.json file, and then updating the value in the server.json file. The challenge lies in the fact that the server.json file is dynamic with an unpredictable structure contai ...

Use jQuery to switch back and forth between two different sets of classes

I am attempting to switch between two different sets of classes using jQuery. My goal is to change from one custom icon to a font-awesome icon upon clicking an element. While I have been successful in changing a single class, I am facing challenges when tr ...

Arrange an asynchronous function in Node.js

I have been attempting to set up a schedule for an asynchronous function (with async/await return type) to run every two minutes. Although I have tried using the generic setInterval, as well as various node modules such as node-schedule, cron, node-cron, ...

React-Image-Annotate encountered an issue: SyntaxError - The import statement cannot be used outside a module

Encountering an issue while trying to set up react-image-annotate. Here is the problem I am facing initially: https://i.stack.imgur.com/XgYPd.png This is how I have implemented it: import React from 'react' import ReactImageAnnotate from ' ...

updating information automatically on page every X seconds for Angular component

I am trying to implement a way to automatically refresh the data of an Angular component every 30 seconds. Currently, I have used a simple setInterval function like this: this.interval = setInterval(() => { this.refresh(); // api call ...

The css-loader is missing the required dependency peer Webpack5, causing a resolution error

Recently, I've ventured into the world of JavaScript and I'm looking to incorporate vue-audio-visual into my project. However, I encountered a perplexing error in my node console that seems unrelated. The npm error message reads as follows: code ...

Solution for fixing the error: MongooseError [OverwriteModelError]: It is not possible to overwrite the `User` model after it has been compiled in

I am new to working with the MERN stack and currently attempting to create an exercise tracker app following a tutorial on YouTube. However, I am encountering the Mongoose: OverwriteModelError when running the server and cannot seem to identify where I am ...

Expanding the SimpleModal container size

I recently implemented the SimpleModal plugin on my website. However, I encountered an issue with resizing the modal dialog box. Specifically, after clicking 'Yes' on a confirm dialog, the modal becomes small. The other challenge I am facing is ...

Modify the background color of checkboxes without including any text labels

I am looking to customize my checkbox. The common method I usually see for customization is as follows: input[type=checkbox] { display: none; } .my_label { display: inline-block; cursor: pointer; font-size: 13px; margin-right: 15px; ...

The issue with NGX-Bootstrap/Angular Pagination arises when attempting to adjust the maxSize input while the screen view (width) is being altered

Currently, I am utilizing the Pagination component from Valor Software (click here to access). I am interested in adjusting the maxSize input dynamically based on changes in screen width. For reference, please see this example: Click to view example. It ...

Tick the checkboxes that are not disabled, and leave the disabled ones unchecked

Currently, I am employing Jquery for the purpose of checking and unchecking checkboxes. However, some of these boxes are disabled, thus there is no need for them to be checked. Is there a method by which I can instruct the script to disregard disabled che ...

javascript close the current browser tab

Can someone please help me with a JavaScript code to close the current window? I have tried the following code but it does not seem to work: <input type="button" class="btn btn-success" style="font-weight: b ...