Creating translucent artwork using textured canvases in THREE.js

In my THREE.js application, I am creating 2D surfaces using PlaneGeometry and BasicMaterial meshes, while also supporting textures with a canvas:

this.canvas = makeCanvas(canvasWidth, canvasHeight);
this.ctx = this.canvas.getContext('2d');

this.texture = new THREE.Texture(this.canvas);
this.texture.minFilter = THREE.LinearFilter;
this.texture.magFilter = THREE.LinearFilter;
this.texture.anisotropy = 16;

this.mesh = new THREE.Mesh(new THREE.PlaneGeometry(width, height), new THREE.MeshBasicMaterial({
    map: this.texture,
    overdraw: true,
    side: THREE.DoubleSide,
    transparent: true
}));

Although this method works well, it becomes complicated when transparency is involved. In such cases, I have to create an additional texture for binding as alphaMap and duplicate all the drawing operations across two canvas contexts which makes the code quite messy:

var circleAlpha = 'rgb(100, 100, 100);',
    segmentAlpha = 'rgb(200, 200, 200);';
ctx.fillStyle = 'rgb(0, 255, 255);';
if (this.segment === -1) {
    circleAlpha = segmentAlpha;
}
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.alphaCtx.clearRect(0, 0, this.canvas.width, this.canvas.height);
var drawArc = function (c, w, h, piece) {
    var angw = 2 * Math.PI / 6;
    c.beginPath();
    c.arc(w / 2, h / 2, 512, angw * piece, angw * (piece + 1), false);
    c.arc(w / 2, h / 2, 300, angw * (piece + 1), angw * piece, true);
    c.closePath();
    c.fill();
};
for (var i = 0; i < 6; i++) {
    this.alphaCtx.fillStyle = i == this.segment ? segmentAlpha : circleAlpha;
    drawArc(ctx, this.canvas.width, this.canvas.height, i);
    drawArc(this.alphaCtx, this.canvas.width, this.canvas.height, i);
}
this.updateTexture();
this.alphaTexture.needsUpdate = true;

I've been considering developing a utility library to automate this process, but before proceeding, I wanted to explore if there are simpler alternatives available.

Answer №1

Instead of rendering to two canvases, consider creating a split function. This method allows you to execute multiple draw operations on one canvas using the alpha channel as intended.

After completing the drawing, pass it through the splitter which will generate two canvases - one for color and another in grayscale that can be utilized for the alpha channel.

(It is possible to reuse the main canvas for color, but keep in mind the alpha channel will no longer be present).

Experience the Live Splitter Example:

var ctx = document.querySelector("canvas").getContext("2d"),
    gr = ctx.createLinearGradient(0, 0, 300, 0);

// apply a draw operation with alpha channel to the primary canvas
gr.addColorStop(0, "rgba(255,140,0,1)");
gr.addColorStop(1, "rgba(255,0,0,0)");
ctx.fillStyle = gr;
ctx.fillRect(0, 0, 300, 150);

// split the canvas into two new canvases - one for color, one for alpha
var maps = splitTexture(ctx);

document.body.appendChild(maps.color);  // display in DOM for demonstration
document.body.appendChild(maps.alpha);

// Function to Split Texture:
function splitTexture(ctx) {

  var w = ctx.canvas.width,
      h = ctx.canvas.height,
      canvasColor = document.createElement("canvas"),
      canvasAlpha = document.createElement("canvas"),
      ctxc = canvasColor.getContext("2d"),
      ctxa = canvasAlpha.getContext("2d"),
      
      idata = ctx.getImageData(0, 0, w, h),
      data32 = new Uint32Array(idata.data.buffer),  // utilize uint-32 buffer (faster!)
      len = data32.length, i = 0, p, a, g,
      adata, adata32, cdata, cdata32;               // destinations

  canvasColor.width = canvasAlpha.width = w;        // set same size as the source
  canvasColor.height = canvasAlpha.height = h;

  cdata = ctxc.createImageData(w, h);               // create buffers and uint32 views
  cdata32 = new Uint32Array(cdata.data.buffer);

  adata = ctxa.createImageData(w, h);
  adata32 = new Uint32Array(adata.data.buffer);
  
  // loop for splitting
  while(i < len) {
    p = data32[i];                                        // extract source pixel as 32-bit ABGR
    a = p & 0xff000000;                                   // isolate alpha
    g = 0xff000000 | (a >>> 8) | (a >>> 16) | (a >>> 24); // calculate gray value
    adata32[i] = g;                                       // assign gray value
    cdata32[i++] = 0xff000000 | (p & 0xffffff);           // assign color value
  }
  
  ctxc.putImageData(cdata, 0, 0);                         // update destinations
  ctxa.putImageData(adata, 0, 0);
  
  return {
    color: canvasColor,
    alpha: canvasAlpha
  }
}
body {background:#79c}
<canvas></canvas>

Answer №2

After some experimentation, I developed a custom proxy context that divides the drawing operations between two canvases. The implementation requires the `csscolorparser` node module, which can be found on NPM.

var FakeCtx = function (canvasA, canvasB) {
    this.ctxA = canvasA.getContext('2d');
    this.ctxB = canvasB.getContext('2d');

    this.colorStore = {};
};

var assignContextProperty = function (property, propertyType) {
    if (propertyType === 'function') {
        FakeCtx.prototype[property] = function () {
            var argArray = Array.prototype.slice.call(arguments),
                ra = this.ctxA[property].apply(this.ctxA, argArray),
                rb = this.ctxB[property].apply(this.ctxB, argArray);
            if (ra !== rb) {
                var argString = argArray.join(', ');
                debug.warn('Console proxy got two different results for calling ' + property + ':');
                debug.warn('    Canvas A: ' + property + '(' + argString + ') = ' + ra);
                debug.warn('    Canvas B: ' + property + '(' + argString + ') = ' + rb);
            }
            return ra;
        };
    } else {
        if (property != 'fillStyle' && property != 'strokeStyle') {
            FakeCtx.prototype.__defineGetter__(property, function () {
                return this.ctxA[property];
            });
            FakeCtx.prototype.__defineSetter__(property, function (value) {
                this.ctxA[property] = this.ctxB[property] = value;
            });
        } else {
            FakeCtx.prototype.__defineGetter__(property, function () {
                return this.colorStore[property] || this.ctxA[property];
            });
            FakeCtx.prototype.__defineSetter__(property, function (value) {
                var color = csscolor.parseCSSColor(value);
                if (color === null || color.length < 3) {
                    throw new Error('Invalid color ' + value + ': ' + color);
                } else {
                    this.colorStore[property] = value;
                    if (color.length === 3 || color[3] === 1) { // no alpha
                        this.ctxA[property] = value;
                        this.ctxB[property] = 'rgb(255, 255, 255)'; // white = full alpha
                    } else {
                        this.ctxA[property] = 'rgb(' + color.slice(0, 3).join(', ') + ')';
                        var alphaValue = Math.round(255 * color[3]);
                        this.ctxB[property] = 'rgb(' + [ alphaValue, alphaValue, alphaValue ].join(', ') + ')';
                        // console.log('got color with alpha ' + value + ', splitting to ' + 'rgb(' + color.slice(0, 3).join(', ') + ');' + ' and ' + 'rgb(' + [ alphaValue, alphaValue, alphaValue ].join(', ') + ');');
                    }
                }
            });
        }
    }
}

var _ctx = makeCanvas(0, 0).getContext('2d');
for (var property in _ctx) {
    assignContextProperty(property, typeof _ctx[property]);
}

Although there may still be a few bugs in the code, I have not yet conducted thorough testing. However, it has proven effective for handling basic tasks.

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

The canvas's document.getElementById function is unable to find any matching element,

Hello everyone, I am currently diving into the world of javascript and trying to expand my knowledge. However, I have encountered a problem that has me stumped, and I could really use some assistance. While exploring, I came across this page: http://www.w ...

Tips for preventing automatic login when a new user is added in firebase

I am looking to enable users to create additional accounts, however, I encountered an issue when using createUserWithEmailAndPassword which automatically signs in the new user. How can I prevent this from happening? My goal is to set up a user account fro ...

What is the best way to display items using Typeahead.js in combination with the Bloodhound engine?

I am struggling to showcase a collection of items using typeahead with a JSON file as the data source. Unfortunately, none of my information is appearing on the screen. My objective is to list the names and utilize the other attributes for different purpo ...

Strip all textual content from within HTML div elements while retaining the original HTML tags and formatting

I currently have the following HTML code: <div> Here <a href="#"> is </a> <p> Text, that I want to </p> be removed </div> This is what I desire: <div> <a href="#"> </a> <p& ...

Look for and choose various fields from a lengthy JSON object

I am working with a JSON object that contains a large list of offerValue objects. { "Code": 0, "response": "SUCCESS", "offerValue": [ { "id": "111", "name": "ABC", "flag": "V" }, { ...

Erase the contents of a div by clicking a button

I am currently working on a game project that utilizes drag-and-drop functionality, similar to Blockly and Scratch. One of the key features I am trying to implement is the ability to clear the contents of a target container by clicking a Reset button. Desp ...

Which is the better approach: extending properties of a Node module.exports object, or completely replacing the object?

When working in Node.js, which approach is more optimal and why? Is it better to completely replace the module.exports object like this: module.exports = { myFuncOne = function(thing) { console.log(thing); }, myFuncTwo = function(stuff) { c ...

attaching the model to chosen values rather than defining the chosen item

This is the coding I am currently using which is functioning correctly: <div class="col-md-12"> <div class="button-group"> <button type="button" class="btn btn-default btn-block btn-sm dropdown-toggle" data-toggle="dropdown"> ...

Using HTML tags as strings with jQuery

I need to store the value of a textarea in a hidden field using jQuery. When I enter the text "alert('yes');" inside the textbox, I want it to display as a string instead of triggering an alert. How can I enable the HTML tags to be treated as str ...

Is it possible to utilize a string as the key within a JavaScript object?

Is it possible to use a string as the key in a JavaScript object? var func, object; func = function () { return "foo"; }; // It is valid object = { 'foo': 'bar' }; // This will not work object = { method(): 'bar' ...

Transformation of visuals with alteration of status

I am attempting to change the image of a door from closed to open when the status changes in my home automation dashboard. I need help with this task. <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&qu ...

What is the best way to identify duplicate data being returned from a server within a for loop?

When receiving sorted data from the server through an infinite scroll, my goal is to filter out duplicate entries and add new unique data to a client-side array. If duplicates are found, I want to stop the infinite scrolling process. $scope.collections = ...

The PDF format is experiencing compatibility issues when used with the CSS property for page breaks after:

My PDF with CSS page breaks is not functioning properly, as the pages are not breaking as intended. Removing position:absolute solves the issue but leaves space after each page. I suspect it may be a CSS problem, but I'm unsure. If the issue lies wit ...

Sort by characteristics of embedded array - Knockout

I'm struggling with filtering a nested array using Knockout. My goal is to filter the array by both 'tag-names' and 'name'. For example, searching for 'whal' should display all objects that contain a tag with that name ...

What could be causing my tabs (such as HOME, ABOUT ME..) not displaying the correct paragraph or section content?

I have set up navigation tabs on my website using anchor tags, but they are currently not linked to any specific paragraphs. I want the corresponding paragraph to be displayed when a tab is clicked, but I'm unsure how to add this functionality using j ...

Using a child prop in React's map function

Initializing the parent component: class Parent extends React.Component { constructor(props) { super(props); this.state = { children: [ {name: "Julien Lennon"}, {name: "Sean Lennon"} ] } } render() { retur ...

Using JavaScript, conceal a specific Div by examining the content within another Div

I am attempting to implement some logic using JavaScript. The goal is to hide the "cart-button" div if the innerHTML value of a div with the class "b-format" is set to Audio, otherwise hide the "more-button" div. However, for some reason this functionality ...

Tips for utilizing a received variable within an ejs document

I am currently attempting to update the content of an HTML element in an EJS file with a variable that I have defined in a JavaScript file. let checkbox = document.querySelector('input[name="plan"]'); checkbox.addEventListener('ch ...

Node timers exhibiting unexpected behavior contrary to documented specifications

Feeling a bit lost with this one. I'm running Ubuntu and using nvm for node. I made sure to uninstall the version of node installed via apt just to be safe. node --version > v10.10.0 npm --version > 6.4.1 So, I go ahead and create-react-app a ...

What is the best way to make a container 100% tall and display a navigation bar?

Struggling to properly render my React page This is what I have: ReactDOM.render( <React.StrictMode> <Provider store={store}> <Router /> </Provider> </React.StrictMode>, document.getEl ...