Canvas: понимание globalCompositeOperation для удаления части наложения изображения

#javascript #html #canvas

#javascript #HTML #холст

Вопрос:

Я пытаюсь разобраться в свойстве globalCompositeOperation, пытаясь объединить эти два примера: JSFiddle и Codepen.

Первый использует destination-out , а второй использует source-over . Можно ли было бы использовать fiery cursor в Codepen, но также удалить часть оверлейной заливки, на которую нажимает пользователь, как в скрипке?

Любая помощь была бы очень признательна. Я могу объединить демонстрации в Codepen, чтобы при необходимости использовать те же методы.

Соответствующий код скрипки:

 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();
}
 

Соответствующий кодовый код:

 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();/**/
}    
 

Комментарии:

1. сначала нарисуйте функцию стирания, затем нарисуйте на ней пламя (подумайте, что у него есть слои в PS)

2. Спасибо за заметку о слоях Photoshop. Это хороший способ взглянуть на это. Но я борюсь, потому что fiery cursor, похоже, Требует полной перерисовки холста (т. Е. Заполнения) При каждом обновлении, не так ли? Как мне обойти это, когда они используют два разных составных метода?

3. О, извините, я пропустил, что ваш ластик на самом деле использует фоновое изображение CSS, я думал, что оно уже завершено… Итак, что вам нужно, это использовать второй холст вне экрана, нарисовать на нем только стирание, затем на видимом холсте нарисуйте bg, затем стертый холст вне экрана, затем пламя.

4. Мой мозг не был готов к такому ответу. Я … уххх… попробую! Я думаю, суть того, что вы говорите, заключается в том, что мне нужно использовать два холста вместо того, чтобы пытаться объединить их в один. Я все еще не уверен, сработает ли это, потому что для fiery cursor canvas, похоже, требуется перерисовка фона, которая просто перекрывала бы все, что я делал на холсте ластика, но я полагаюсь на вашу мудрость и попытаюсь это сделать. Спасибо!

5. Да, идея состоит в том, чтобы иметь 2 холста. Ваш ластик будет использовать только закадровый и обновлять только его. Затем на видимом для каждого кадра вы сначала нарисуете фон, используя drawImage(bg) , затем закадровый холст, также используя drawImage (offScreen_canvas), и, наконец, позвольте fiery cursor делать то, что он должен делать на результирующем изображении. Просто не забудьте сбросить gCO source-over на каждый начальный кадр (перед рисованием bg)

Ответ №1:

Как я уже говорил в комментариях, вам придется разделить свой код как минимум на две части.
Функция обрезки использует "destination-out" операцию компоновки, чтобы удалить уже нарисованные пиксели холста, где должны быть нарисованы новые. В вашей версии он использует фоновое изображение, и как только пиксели переднего плана будут удалены, вы сможете увидеть этот фон, поскольку в теперь прозрачных областях холста.

С другой стороны, flame использует «более легкие» "color-dodge" и "soft-light" операции смешивания. Это добавит цвета как уже существующих, так и новых нарисованных пикселей.
По крайней мере, первая операция, если она используется в прозрачной области, будет такой же, как "source-over" операция составления по умолчанию. Итак, вам нужно, чтобы фоновое изображение было нарисовано на холсте, чтобы иметь возможность использовать его при смешивании.

Для этого вам нужно использовать второй, внеэкранный холст, где вы будете применять только операцию ластика "destination-out" . Затем на видимом холсте при каждом новом фрейме ластика вам нужно будет нарисовать фоновое изображение на видимом холсте, затем изображение ластика с отверстиями и, наконец, смешивание, которое смешает все вместе.

Вот быстрый дамп кода, где я немного переписал ластик и модифицировал Fire, чтобы наша основная функция обрабатывала как события, так и цикл анимации.

 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 amp;amp; 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> 

Комментарии:

1. Щедрость твоя (когда позволяет таймер), мой друг. Спасибо, правда. Теперь, глядя на ваш код, все кажется настолько очевидным, что я не уверен, почему это так сильно сбивало меня с толку с самого начала. Если я могу спросить, есть ли какие-либо конкретные ресурсы, которые вы предпочитаете для ссылок и изучения canvas?

2. Последний вопрос, если можно: Как бы мне сделать это полноэкранным и отзывчивым? Установка размера холста с помощью CSS не является проблемой. Но когда я использую drawImage width / height для покрытия холста, он отлично работает при начальной загрузке, но любое изменение размера браузера нарушает его, и вы получаете эффект бесконечного зеркала. Использование addEventHandler('onresize') функции оказывается столь же бесплодным. Глядя на код, я предположил, что функции Eraser и MainDrawing будут обрабатывать это при каждой перерисовке. Кажется, я что-то упускаю.

3. Это может быть сложно. По сути, вам нужно будет добавить window.onresize обработчик, в котором вы будете изменять как main.canvas eraser.canvas ширину, так и высоту. Но при этом вы удалите eraser.canvas содержимое. Поэтому вы можете либо не изменять его размер, но усложнить функцию рисования (чтобы она каждый раз изменяла размер, или вы можете создать третий холст, сначала нарисовать на нем ластик, изменить размер холста ластика, затем нарисовать фоновое изображение на холсте ластика с измененным размером, чтобы, наконец, использовать gCOвведите исходный код и нарисуйте на нем временный холст. Я добавлю это к своему ответу, когда у меня будет время.

4. Я думаю, что у меня все работает быстро, за исключением одной детали: при изменении размера браузера getRect не обновляется до новых размеров, поэтому рисунок ластика отключается на заданное количество пикселей от курсора. Не уверен, как обновить только это в window.resize

5. Да, я не прикрепил его в прослушивателях событий, его следует просто вызвать в событии изменения размера и прокрутки. ( eraser.getRect() ). Извините, у меня еще не было времени вернуться к этому.