Pull to refresh

Реалистичный дым на Canvas

Reading time4 min
Views16K

Введение


На просторах интернета есть несколько статей о том как сделать эффект дыма, но скрипты слишком «тяжелые», и лично для меня не совсем понятные. Вот я и решил упростить задачу тем, кому интересно воплотить такой эффект в своих проектах.
Писать много не буду, в основном код с подробными комментариями.

Итак, разметка страницы:


<html>
	<head>
		<title>smoke</title>
		<meta charset="utf-8"/>
	</head>
	<body bgcolor = "#000">
		<div id = "info" style = "color: #fff"></div>
		<canvas id = "canvas" width = "500px" height = "500px"></canvas>
		<script>
			...
		</script>
	</body>
</html>

С этим все понятно. Есть Canvas, div для вывода количества частиц, и body с черным фоном — на белом фоне эффект получается не таким красивым.

Скрипт по кусочкам:


Для начала нам нужно найти на странице необходимые элементы, и написать удобную функцию для получения рандомных значений.
info = document.getElementById('info');
canvas = document.getElementById('canvas');
ctx = canvas.getContext('2d');
function random(min,max){
	return Math.random() * (max - min) + min;
}

После этого нужно создать массив частиц, функцию добавления частиц (в своем коде у меня рука не поднимается написать что это конструктор =). Собственно, частицы имеют следующие свойства:
  • номер
  • прозрачность
  • радиус
  • угол
  • х-координата
  • у-координата
  • функция отрисовки себя

Так же к каждому из этих свойств есть значение, изменяющее это свойство (простите за тавтологию).
Каждая частица есть ни что иное как объект, и каждый этот объект — элемент массива particles.
function addParticle(){
	particles.push({
		num: particles.length, //номер
		op: random(0,0.5), //прозрачность
		dop: random(0.002,0.008), //шаг изменения прозрачности
		r: random(3,10), //радиус
		dr: random(0.5,1.5), //шаг изменения радиуса
		a: random(0,180), //угол
		da: random(-3,3), //шаг изменения угла
		x: random(249,251), //Х-координата
		dx: random(-0.2,0.2), //шаг изменения по Х
		y: random(349,351), //У-координата
		dy: random(0.5,1), //шаг изменения по У
		draw: /* описание функции отрисовки в следующем абзаце */
	});
}

С кодом выше, думаю, все понятно. Теперь самое интересное — отрисовка.
Технология такая: каждая частица самостоятельно рассчитывает все свои новые параметры (координаты, радиус и т.д.). После этого меняется прозрачность холста (ctx.globalAlpha = ...). Дальше для оптимизации идет проверка на то, сколько времени прошло с предыдущей отрисовки, и если это время меньше чем время кадра, то отрисовка производится, а иначе, соответственно, производятся только вычисления параметров, чтобы при большой загрузке частицы не стояли на месте. То есть получается такая самописная система пропуска кадров. Ну и сама отрисовка, в которую входит изменения начальных координат для правильного разворота частицы (ctx.translate), непосредственно поворот холста вокруг координат Х и У (ctx.rotate(угол в радианах)), и отрисовка (ctx.drawImage). Обращаю внимание на то, чтобы центрировать картинку относительно координат, рисавать её нужно в «минус половину радиуса», а так же не забыть перевести градусы в радианы (angle * Math.PI/180).
draw: function(timer){
	this.op -= this.dop; //Сначала считаем прозрачность
	if(this.op > 0){ //Если прозрачность меньше 0, то смысл в расчетах и отрисовке пропадает, зачем рисовать полностью прозрачную частицу?
		this.r += this.dr; //новый радиус
		this.a -= this.da; //новый угол (в градусах!)
		this.x -= this.dx; //новая Х-координата
		this.y -= this.dy; //новая У-координата
		ctx.globalAlpha = this.op; //задание прозрачности холсту
		if(window.performance.now() - time < 28){ //вот та самая проверка "успеваемости", значение взято чуть меньше времени кадра, чтобы наверняка избавиться от тормозов
			ctx.save(); //сохраняем состояние холста
			ctx.translate(this.x,this.y); //задаем начальные координаты
			ctx.rotate(this.a*Math.PI/180); //вращаем контекст
			ctx.drawImage(img,-this.r/2,-this.r/2,this.r,this.r); //рисуем картинку
			ctx.restore(); //восстанавливаем первоначальное состояние холста
		}
	}
}

Я надеюсь что в этом куске кода все предельно понятно объяснено.
Следующий кусок — функция пробега по всем частицам. В начале задается время вызова (window.performance.now()), потом добавляется новая частица с помощью функции описанной выше, чистится холст, и идет «пробег» по каждой частице в цикле. При этом, для автоматического регулирования количества частиц, мы будем удалять элемент массива (частицу), если параметры частицы таки, что при отрисовке её не будет видно (нулевая прозрачность, выход за пределы канваса). Функция рекурсивная и вызывает сама себя после всех отрисовок. Вы спросите почему я не использую requestAnimationFrame? а я отвечу, что сам эффект дыма используется мной в онлайн игре (почти дописана), а при использовании requestAnimationFrame функция перестает вызываться при переходе на другую вкладку браузера, и соответственно все расчеты перестают выполняться, что конкретно для моей игры не позволительно (все карты по поводу игры пока открывать не буду =). Собственно код:
function draw(){				
	time = window.performance.now(); //время начала кадра
	addParticle(); //добавление частицы
	info.innerHTML = particles.length; //запись в div количества частиц
	canvas.width = canvas.width; //очистка холста
	for(var i = 0; i < particles.length; i++){
		//условие ниже удаляет частицу, если её отрисовка стала бессмысленной
		if(particles[i].op <= 0 || particles[i].x < -100 || particles[i].x > 600 || particles[i].y < -100){
			particles[i].op = 0;
			particles[i] = null;
			delete particles[i];
			particles.splice(i,1);
		}
	        particles[i].draw(); //вызов функции просчета и отрисовки. Эту строчку я не пишу в else, чтобы даже при удалении частицы сразу начинала рисоваться следующая, а иначе дым заметно начнет "моргать"
	}
	setTimeout(draw, 30); //...
}

Вот написание кода и закончилось. Осталось только проинициализировать картинку и выполнить первый вызов функции «пробега по частицам». Кстати, вот картинка:
image
А вот код:
img = new Image();
img.src = 'smoke.png';
img.onload = draw();

Живая ссылка — по ссылке для наглядности в различных браузерах сменил window.performance.now() на Date.now(). Теперь должно работать на бОльших устройствах.
P.S. Честно скажу — работу проверял только в Chrome, если где не работает — пишите.
Исходники — F12.
Жду адекватной критики, что хорошо а что плохо решать Вам, но надеюсь каждая строчка понятна и правильна. До новый встреч!
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+51
Comments41

Articles