Михаил Востриков

Воздушный змей

Нарисованный художником красивый воздушный змей — это хорошо, но живой воздушный змей — это еще лучше! Давай попробуем сделать такого и запустить в небо.

Итак, в нашем случае змей состоит из «основы» и хвоста, в свою очередь состоящего из разноцветных ленточек, которые колышатся на ветру. Мы займемся созданием последних, так как анимировать игру света и тени на «основе» змея слишком просто.

Генерировать колебания и рисовать ленты конечно же будем программно используя одну единственную функцию — прототип муви-клипа. Почему прототип, а не класс? Мне показалось, что именно так будет удобнее.

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


MovieClip.prototype.lenta = function(r, g, b, len) {
	var x = new Array();
	var y = new Array();
	var count = 20
	this.onEnterFrame = function() {
		...
	}
}

Чтобы обеспечить сложные изгибы ленты нам придется разделить ее на несколько сегментов — нам вполне хватит двадцати (переменная count). Создадим в прототипе два массива, которые будут хранить координаты точек ленты.

Соеденить точки в непрерывную линию нам поможет следующий алгоритм рисования:


this.clear();
this.moveTo(0,0);
for (var i = 0; i < count; i++) {
	this.lineStyle( line_width, color );
	this.curveTo( x[i], y[i], (x[i] + x[i+1]) * 0.5, (y[i] + y[i+1]) * 0.5 );
}

Это классический вариант рисования сплайна (кривой) с использованием массива координат точек. Алгоритм давно известен и прост, не будем останавливаться на нем.

Самое интересное — это рассчет координат точек. Про него расскажу подробнее. Начнем с простого (см. рис. ниже):


for (var i = 0; i <= count; i++) {
	y[i] = i * 2;
	x[i] = i * len;
}

Получили прямые линии с наклоном, для начала неплохо :)
Давайте добавим к ним простые синусоидальные колебания с помошью следующей строки кода:


var y1 = 5 * Math.sin( phase1i += phaseAdd1 );

где 5 — это амплитуда колебаний, а на вход функции синуса передадим начальную фазу колебаний phase1i , которая будет наращиваться в цикле на величину phaseAdd1, генерируя колебания определенной частоты. Чтобы амплитуда плавно возрастала умножим получненное значение на коэффициент i/count.

Смотрится лучше. Но движения линий слишком предсказуемые. Надо добавить еще одну синусоидальную составляющую с другой амплитудой и частотой — движения станут интереснее.

Код для вторго синуса и визуальное отображение на рисунке ниже:


var y0 = 14 * Math.sin( phase0i += phaseAdd0 );

После сложения значений обоих колебаний внешний вид ленты значительно улучшился и стал менее предсказуемым как в реальной жизни (см. рис. и код ниже).


for (var i = 0; i <= count; i++) {
	var y0 = 14 * Math.sin( phase0i += phaseAdd0 );
	var y1 = 5 * Math.sin( phase1i += phaseAdd1 );
	y[i] = i * 2 + i/count * ( y0 + y1 );
	x[i] = i * len;
}

Вроде отлично, но давайте добавим изгиб всем лентам. Опять используем синус, но в данном случае только его часть (рис. ниже а. и б., результат в.), похожую на нужный нам изгиб.

После сложени всех трех синусоидальных составляющих ленты приобрели реалистичную форму. Осталось добавить маленький штрих — освещение лент.

Сделаем освещение следующим способом. Будем регулировать яркость каждого сегмента в зависимости от его угла наклона отностительно горизонта. Получить угол можно с помошью функции Math.atan2(dy,dx). Код ниже поможет оперировать яркостью сегментов линии исходя из угла наклона:


var light = 1.5 + 0.6 * ( Math.atan2(y[i+1] - y[i], x[i+1] - x[i]) );
var rNew = Math.min( light * r, 255 ) << 16;
var gNew = Math.min( light * g, 255 ) << 8;
var bNew = Math.min( light * b, 255 );

Суммируя новые величины цветовых составляющих можно получить конечный цвет сегмента и использовать его для рисования — this.lineStyle( 2, rNew + gNew + bNew );

Нам еще нехватает рассчета начальной фазы для каждой синусоидальной составляющей. Используем функцию Math.random() — это придаст лентам реалистичности, так как они будут выглядеть по разному при каждом запуске муви-клипа.

Соберем все части кода. Конечный вариант прототипа будет выглядеть вот так:


MovieClip.prototype.lenta = function(r, g, b, len) {
	var x = new Array();
	var y = new Array();
	var count = 22
	var phase0 = 0;
	var phaseSub0 = 0.05 + 0.1 * Math.random();
	var phaseAdd0 = 0.3 + 0.1 * Math.random();
	var phase1 = 0;
	var phaseSub1 = 0.05 + 0.1 * Math.random();
	var phaseAdd1 = 0.4 + 0.2 * Math.random();
	var phase2 = Math.PI * 0.3;
	var phaseAdd2 = Math.PI / count;
	this.onEnterFrame = function() {
		var phase0i = phase0 -= phaseSub0;
		var phase1i = phase1 -= phaseSub1;
		var phase2i = phase2;
		for (var i = 0; i <= count; i++) {
			var y0 = 14 * Math.sin( phase0i += phaseAdd0 );
			var y1 = 5 * Math.sin( phase1i += phaseAdd1 );
			var y2 = 80 * Math.sin( phase2i += phaseAdd2 );
			y[i] = i * 2 + i/count * ( y0 + y1 + y2 );
			x[i] = i * len;
		}
		this.clear();
		this.moveTo(0,0);
		for (var i = 0; i < count; i++) {
			var light = 1.5 + 0.6 * ( Math.atan2(y[i+1] - y[i], x[i+1] - x[i]) );
			var rNew = Math.min( light * r, 255 ) << 16;
			var gNew = Math.min( light * g, 255 ) << 8;
			var bNew = Math.min( light * b, 255 );
			this.lineStyle( 2, rNew + gNew + bNew );
			this.curveTo( x[i], y[i], (x[i] + x[i+1]) * 0.5, (y[i] + y[i+1]) * 0.5 );
		}
	}
}

Амплитуды, величины фаз и коэффициенты для освещения подобраны для конкретной ситуации. Вы можете поэксперементировать с ними, чтобы лучше понять работу прототипа.

Теперь создадим на сцене несколько пустых муви-клипов для каждой ленты, расположим их на конце змея :) В каждом муви-клипе сделаем вызов прототипа, в параметрах зададим цвет линии и длину.


onClipEvent (load) {
	lenta(220,20,0,13) // красная лента
}

Змей готов.

Как это выглядит на сайте

Исходник
(файл .fla, 1.6 Мб)

Эвальд
14 Февраля
а можно от сюда исходник?
Дима Фитискин
15 Февраля
ссылка на исходник дана в конце статьи про стерку. но на всякий случай вот прямая ссылка eraser.zip
Gilios
15 Февраля
Суперски!
Вадим
21 Февраля
Молодцы, спасибо за скриптик!
Кирилл
16 марта
Человеку не знакомому с веб-разработками может показаться, что такая картинка довольно просто делается, а на деле не самый простой код. Кстати, сколько по времени у одного программиста, заняла данная задача?
Михаил Востриков
17 марта
На реализацию задачи времени было мало, примерно час. Хотя этого времени вполне хватило. Если бы было часа три на задачу, сделал бы форму хвоста более реалистично (не линиями), и можно было бы использовать заливку-градиент для эмуляции освещения.
dsasha
16 апреля
Спасибки, очень интересно
sas
22 августа
Супер
Вопрос почему вы используете AS2?
Olga
25 апреля
Супер конечно!!! а интересно, сколько времени займет змей с реалистичными лентами?