daveycroqet
daveycroqet

Reputation: 2727

Canvas: Understanding globalCompositeOperation to erase part of an image overlay

I'm trying to wrap my head around the globalCompositeOperation property by attempting to combine these two examples: JSFiddle and Codepen.

The former is using destination-outand the latter is using source-over. Would it be possible to use the fiery cursor in the Codepen, but also have it remove the portion of the overlay fill that the user clicks on, as in the Fiddle?

Any assistance would be most appreciated. I can combine the demos on Codepen to use the same methods if necessary.

Relevant Fiddle code:

function drawDot(mouseX,mouseY){
    bridgeCanvas.beginPath();
    bridgeCanvas.arc(mouseX, mouseY, brushRadius, 0, 2*Math.PI, true);
    bridgeCanvas.fillStyle = '#000';
    bridgeCanvas.globalCompositeOperation = "destination-out";
    bridgeCanvas.fill();
}

Relevant Codepen code:

Fire.prototype.clearCanvas = function(){
    this.ctx.globalCompositeOperation = "source-over";
    this.ctx.fillStyle = "rgba( 15, 5, 2, 1 )";
    this.ctx.fillRect( 0, 0, window.innerWidth, window.innerHeight );

    this.ctx.globalCompositeOperation = "lighter";
    this.ctx.rect(0, 0, this.canvas.width, this.canvas.height);
    this.ctx.fillStyle = this.pattern;
    this.ctx.fill();/**/
}    

Upvotes: 1

Views: 2349

Answers (1)

Kaiido
Kaiido

Reputation: 136598

As I said in comments, you'll have to divide your code in at least two parts.
The cropper function uses "destination-out" compositing operation to remove the already drawn pixels of the canvas where the new ones should be drawn. In your version, it uses a background-image, and once the foreground pixels are removed, you can see this background since in the now transparent areas of the canvas.

The flame one in the other hand, uses '"lighter"', "color-dodge" and "soft-light" blending operations. This will add the colors of both the already there and the new drawn pixels.
At least the first one, if used on a transparent area, will be the same as the default "source-over" composite operation. So you need to have the background image drawn onto the canvas to be able to use it in the blending.

For this, you've got to use a second, off-screen canvas, where you will only apply the eraser "destination-out" operation. Then, on the visible canvas, at each new eraser frame, you'll have to draw the background image on your visible canvas, then the eraser one, with the holes, and finally the blending one, which will mix all together.

Here is a quick code dump, where I rewrote a bit the eraser, and modified the Fire one, in order to make our main function handles both events and animation loop.

function MainDrawing(){
  this.canvas = document.getElementById('main');
  this.ctx = this.canvas.getContext('2d');
  this.background = new Image();
  this.background.src = "https://s3-us-west-2.amazonaws.com/s.cdpn.io/4273/calgary-bridge-1943.jpg"
  this.eraser = new Eraser(this.canvas);
  this.fire = new Fire(this.canvas);
  this.attachEvents();
  }
MainDrawing.prototype = {
   anim: function(){
     if(this.stopped)
        return;
     this.ctx.globalCompositeOperation = 'source-over';
     this.ctx.drawImage(this.background, 0,0);
     this.ctx.drawImage(this.eraser.canvas, 0,0);
     this.fire.run();
     requestAnimationFrame(this.anim.bind(this));
     },
  stop: function(){
      this.stopped = true;
    },
  attachEvents: function(){
    var mouseDown = false;
	this.canvas.onmousedown = function(){
		mouseDown = true;
		};
	this.canvas.onmouseup = function(){
		mouseDown = false;
		};
    this.canvas.onmousemove = function(e){
    	if(mouseDown){
	    	this.eraser.handleClick(e);
	    	}
	    this.fire.updateMouse(e);
	    }.bind(this);
    }
 };

function Eraser(canvas){
  this.main = canvas;
  this.canvas = canvas.cloneNode();
  var ctx = this.ctx = this.canvas.getContext('2d');
  this.img = new Image();
  this.img.onload = function(){
  	ctx.drawImage(this, 0, 0);
  	ctx.globalCompositeOperation = 'destination-out';
  	};
  this.img.src = "https://s3-us-west-2.amazonaws.com/s.cdpn.io/4273/calgary-bridge-2013.jpg";
  this.getRect();
}
Eraser.prototype = {
  getRect: function(){
      this.rect = this.main.getBoundingClientRect();
    },
  handleClick: function(evt){
    var x = evt.clientX - this.rect.left;
    var y = evt.clientY - this.rect.top;
    this.draw(x,y);
    },
  draw: function(x, y){
    this.ctx.beginPath();
    this.ctx.arc(x, y, 30, 0, Math.PI*2);
    this.ctx.fill();
    }
 };
  

var Fire  = function(canvas){

	this.canvas 		= canvas;
	this.ctx 			= this.canvas.getContext('2d');

	this.aFires 		= [];
	this.aSpark 		= [];
	this.aSpark2 		= [];



	this.mouse = {
		x : this.canvas.width * .5,
		y : this.canvas.height * .75,
	}

}

Fire.prototype.run = function(){
	
	this.update();
	this.draw();

}
Fire.prototype.start = function(){

	this.bRuning = true;
	this.run();

}
Fire.prototype.stop = function(){

	this.bRuning = false;

}
Fire.prototype.update = function(){

	this.aFires.push( new Flame( this.mouse ) );
	this.aSpark.push( new Spark( this.mouse ) );
	this.aSpark2.push( new Spark( this.mouse ) );



	for (var i = this.aFires.length - 1; i >= 0; i--) {

		if( this.aFires[i].alive )
			this.aFires[i].update();
		else
			this.aFires.splice( i, 1 );

	}

	for (var i = this.aSpark.length - 1; i >= 0; i--) {

		if( this.aSpark[i].alive )
			this.aSpark[i].update();
		else
			this.aSpark.splice( i, 1 );

	}


	for (var i = this.aSpark2.length - 1; i >= 0; i--) {

		if( this.aSpark2[i].alive )
			this.aSpark2[i].update();
		else
			this.aSpark2.splice( i, 1 );

	}

}

Fire.prototype.draw = function(){

	this.drawHalo();
	
	this.ctx.globalCompositeOperation = "overlay";//or lighter or soft-light

	for (var i = this.aFires.length - 1; i >= 0; i--) {

		this.aFires[i].draw( this.ctx );

	}

	this.ctx.globalCompositeOperation = "soft-light";//"soft-light";//"color-dodge";

	for (var i = this.aSpark.length - 1; i >= 0; i--) {
		
		if( ( i % 2 ) === 0 )
			this.aSpark[i].draw( this.ctx );

	}

	this.ctx.globalCompositeOperation = "color-dodge";//"soft-light";//"color-dodge";

	for (var i = this.aSpark2.length - 1; i >= 0; i--) {

		this.aSpark2[i].draw( this.ctx );

	}


}

Fire.prototype.updateMouse = function( e ){

	this.mouse.x = e.clientX;
	this.mouse.y = e.clientY;

}


Fire.prototype.drawHalo = function(){

	var r = rand( 300, 350 );
	this.ctx.globalCompositeOperation = "lighter";
	this.grd = this.ctx.createRadialGradient( this.mouse.x, this.mouse.y,r,this.mouse.x, this.mouse.y, 0 );
	this.grd.addColorStop(0,"transparent");
	this.grd.addColorStop(1,"rgb( 50, 2, 0 )");
	this.ctx.beginPath();
	this.ctx.arc( this.mouse.x, this.mouse.y - 100, r, 0, 2*Math.PI );
	this.ctx.fillStyle= this.grd;
	this.ctx.fill();

}


var Flame = function( mouse ){

	this.cx = mouse.x;
	this.cy = mouse.y;
	this.x = rand( this.cx - 25, this.cx + 25);
	this.y = rand( this.cy - 5, this.cy + 5);
	this.lx = this.x;
	this.ly = this.y;
	this.vy = rand( 1, 3 );
	this.vx = rand( -1, 1 );
	this.r = rand( 30, 40 );
	this.life = rand( 2, 7 );
	this.alive = true;
	this.c = {

		h : Math.floor( rand( 2, 40) ),
		s : 100,
		l : rand( 80, 100 ),
		a : 0,
		ta : rand( 0.8, 0.9 )

	}




}
Flame.prototype.update = function()
{

	this.lx = this.x;
	this.ly = this.y;

	this.y -= this.vy;
	this.vy += 0.08;


	this.x += this.vx;

	if( this.x < this.cx )
		this.vx += 0.2;
	else
		this.vx -= 0.2;




	if(  this.r > 0 )
		this.r -= 0.3;
	
	if(  this.r <= 0 )
		this.r = 0;



	this.life -= 0.12;

	if( this.life <= 0 ){

		this.c.a -= 0.05;

		if( this.c.a <= 0 )
			this.alive = false;

	}else if( this.life > 0 && this.c.a < this.c.ta ){

		this.c.a += .08;

	}

}
Flame.prototype.draw = function( ctx ){

	this.grd1 = ctx.createRadialGradient( this.x, this.y, this.r*3, this.x, this.y, 0 );
	this.grd1.addColorStop( 0.5, "hsla( " + this.c.h + ", " + this.c.s + "%, " + this.c.l + "%, " + (this.c.a/20) + ")" );
	this.grd1.addColorStop( 0, "transparent" );

	this.grd2 = ctx.createRadialGradient( this.x, this.y, this.r, this.x, this.y, 0 );
	this.grd2.addColorStop( 0.5, "hsla( " + this.c.h + ", " + this.c.s + "%, " + this.c.l + "%, " + this.c.a + ")" );
	this.grd2.addColorStop( 0, "transparent" );


	ctx.beginPath();
	ctx.arc( this.x, this.y, this.r * 3, 0, 2*Math.PI );
	ctx.fillStyle = this.grd1;
	//ctx.fillStyle = "hsla( " + this.c.h + ", " + this.c.s + "%, " + this.c.l + "%, " + (this.c.a/20) + ")";
	ctx.fill();


	ctx.globalCompositeOperation = "overlay";
	ctx.beginPath();
	ctx.arc( this.x, this.y, this.r, 0, 2*Math.PI );
	ctx.fillStyle = this.grd2;
	ctx.fill();



	ctx.beginPath();
	ctx.moveTo( this.lx , this.ly);
	ctx.lineTo( this.x, this.y);
	ctx.strokeStyle = "hsla( " + this.c.h + ", " + this.c.s + "%, " + this.c.l + "%, 1)";
	ctx.lineWidth = rand( 1, 2 );
	ctx.stroke();
	ctx.closePath();

}


var Spark = function( mouse ){

	this.cx = mouse.x;
	this.cy = mouse.y;
	this.x = rand( this.cx -40, this.cx + 40);
	this.y = rand( this.cy, this.cy + 5);
	this.lx = this.x;
	this.ly = this.y;
	this.vy = rand( 1, 3 );
	this.vx = rand( -4, 4 );
	this.r = rand( 0, 1 );
	this.life = rand( 4, 8 );
	this.alive = true;
	this.c = {

		h : Math.floor( rand( 2, 40) ),
		s : 100,
		l : rand( 40, 100 ),
		a : rand( 0.8, 0.9 )

	}

}
Spark.prototype.update = function()
{

	this.lx = this.x;
	this.ly = this.y;

	this.y -= this.vy;
	this.x += this.vx;

	if( this.x < this.cx )
		this.vx += 0.2;
	else
		this.vx -= 0.2;

	this.vy += 0.08;
	this.life -= 0.1;

	if( this.life <= 0 ){

		this.c.a -= 0.05;

		if( this.c.a <= 0 )
			this.alive = false;

	}

}
Spark.prototype.draw = function( ctx ){

	ctx.beginPath();
	ctx.moveTo( this.lx , this.ly);
	ctx.lineTo( this.x, this.y);
	ctx.strokeStyle = "hsla( " + this.c.h + ", " + this.c.s + "%, " + this.c.l + "%, " + (this.c.a / 2) + ")";
	ctx.lineWidth = this.r * 2;
	ctx.lineCap = 'round';
	ctx.stroke();
	ctx.closePath();

	ctx.beginPath();
	ctx.moveTo( this.lx , this.ly);
	ctx.lineTo( this.x, this.y);
	ctx.strokeStyle = "hsla( " + this.c.h + ", " + this.c.s + "%, " + this.c.l + "%, " + this.c.a + ")";
	ctx.lineWidth = this.r;
	ctx.stroke();
	ctx.closePath();

}

rand = function( min, max ){ return Math.random() * ( max - min) + min; };

var app = new MainDrawing();
app.anim();
<canvas id="main" width="750" height="465"></canvas>

Upvotes: 2

Related Questions