Хабровчане! С днем космонавтики!
В одном проекте, приуроченном к сегодняшнему празднику дизайнерами была поставлена задача создать имитацию гиперпространства. Немного поразмыслив решил что правильнее будет использовать Canvas элемент — для SVG достаточно много элементов, да и поддержка среди браузеров не такая хорошая, для видео слишком большой фон, а значит слишком большой размер файла и долгая загрузка. Canvas, к слову, тоже не идеальный вариант — он сильно нагружает процессор и забирает относительно много оперативной памяти. Но все же…
window.requestAnimFrame = (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(/* function */ callback, /* DOMElement */ element){
window.setTimeout(callback, 1000 / 60);
};
})();
function getRandomInt(min, max){
return Math.floor( Math.random() * (max - min + 1) ) + min;
}
Подход 1. Наложение масштабированной картинки.
В дизайне была картинка звездного неба, которую я и решил использовать. Взял, загрузил, наложил на Canvas. Потом наложил с увеличением еще на один пиксель. Потом еще. Картинка получается красивая, но как-либо настраивать её не получится.
Полный код и демонстрация на jsfiddle.
В коде комментировать нечего, поэтому просто код.
Этот подход не имеет жизни по нескольким причинам:
- Исходная картинка всегда одного размера, а значит поддерживать разные разрешения затруднительно.
- Мало возможностей для кастомизации результирующей картины.
- Стирать через некоторое время уже пройденный путь достаточно затруднительно.
- Качество при большом увеличении слишком низкое.
- Это неинтересное решение.
Подход 2. Боевой.
Было решено рисовать каждую звезду и шлейф от нее отдельно.
Полный код и демонстрация на jsfiddle. Движение повешено на mousemove событие.
Итак, создаем массив звезд, генерируем для них начальные значения. Здесь x и y, понятное дело, координаты звезды, currentLife — показатель текущей длина шлейфа от звезды, nx, ny и life используются для реинициализации звезды, после остановки. color — один из вариантов в массиве colors. В принципе можно было сделать вообще любой цвет, но особенность в ограничении количества доступных цветов нам пригодится позже. Массива два, так в момент затухания необходимо показывать двигающиеся и неподвижные звезды одновременно. Конечно это можно (и, наверное, даже нужно) сделать через один массив с отдельным свойством у звезды, но от этого зависит дальнейшая логика и поэтому мне лень все переписывать.
var colors = ["white","rgb(200,200,255)"];
function newStar(){
var life = getRandomInt(50,150);
var dx = getRandomInt(0,canvas.width);
var dy = getRandomInt(0,canvas.height);
return {
x : dx,
y : dy,
nx : dx,
ny : dy,
life : life,
currentLife : life,
color : colors[getRandomInt(0,1)]
};
}
var stars = [];
var finStars = [];
var maxStars = 350;
for(var i = 0; i < maxStars; i++){
finStars.push(newStar());
}
Теперь поговорим про отображение звезды. Здесь у нас включается простая математика:
Думаю не надо объяснять что dx относится к dy так же, как и ax к ay. Если взять dx равным значению currentLife, то dy = currentLife * ( y — cy ) / ( x — cx ). Кроме этого у каждой звезды есть два состояния — когда шлейф растет и когда убывает. Реализовать это достаточно просто через 4 значения: 2 постоянных и 2 переменных. Рисуем от (var1 > const1? var1: const1) до (var2 < const2? var2: const2). Получаем сначала растущую, а потом затухающую звезду.
Остается все это посчитать:
var x = stars[j].x, // (x,y) - это const1
y = stars[j].y,
dx = cx - stars[j].x,
dy = cy - stars[j].y;
if ( Math.abs(dx) > Math.abs(dy) ){
var xLife = dx > 0 ? stars[j].life : - stars[j].life, // (xLife, yLife) - const2. Вообще star.life это вся продолжительность "жизни" звезды
yLife = xLife * dy / dx,
xCur = dx > 0 ? - stars[j].currentLife : stars[j].currentLife, // (xCur,yCur) -var1
yCur = xCur * dy / dx,
xLast = dx > 0 ? xCur + stars[j].life : xCur - stars[j].life, // (xLast,yLast) - var2
yLast = xLast * dy / dx,
mod = "x";
} else {
var yLife = dy > 0 ? stars[j].life : - stars[j].life,
xLife = yLife * dx / dy,
yCur = dy > 0 ? - stars[j].currentLife : stars[j].currentLife,
xCur = yCur * dx / dy,
yLast = dy > 0 ? yCur + stars[j].life : yCur - stars[j].life,
xLast = yLast * dx / dy,
mod = "y";
}
if(dx > 0 && dy > 0)
{
var qx = x - ( xLife < xLast ? xLife : xLast);
var qy = y - ( yLife < yLast ? yLife : yLast);
ctx.moveTo( qx < cx ? qx : cx, qy < cy ? qy : cy);
var sx = x - ( xCur > 0 ? xCur : 0);
var sy = y - ( yCur > 0 ? yCur : 0);
ctx.lineTo( sx < cx ? sx : cx, sy < cy ? sy : cy);
if ( mod == "x"){
ctx.lineTo( qx < cx ? qx : cx, (qy < cy ? qy : cy) + 2);
} else {
ctx.lineTo( (qx < cx ? qx : cx) + 2, qy < cy ? qy : cy);
}
ctx.lineTo( qx < cx ? qx : cx, qy < cy ? qy : cy);
ctx.closePath();
stars[j].nx = sx < cx ? sx : cx;
stars[j].ny = sy < cy ? sy : cy;
}
if(dx < 0 && dy < 0)
{
var qx = x - ( xLife > xLast ? xLife : xLast);
var qy = y - ( yLife > yLast ? yLife : yLast);
ctx.moveTo( qx > cx ? qx : cx, qy > cy ? qy : cy);
var sx = x - ( xCur < 0 ? xCur : 0);
var sy = y - ( yCur < 0 ? yCur : 0);
ctx.lineTo( sx > cx ? sx : cx, sy > cy ? sy : cy);
if ( mod == "x" ){
ctx.lineTo( qx > cx ? qx : cx, (qy > cy ? qy : cy) + 2);
} else {
ctx.lineTo( (qx > cx ? qx : cx) + 2, qy > cy ? qy : cy);
}
ctx.lineTo( qx > cx ? qx : cx, qy > cy ? qy : cy);
ctx.closePath();
stars[j].nx = sx > cx ? sx : cx;
stars[j].ny = sy > cy ? sy : cy;
}
if(dx < 0 && dy > 0)
{
var qx = x - ( xLife > xLast ? xLife : xLast);
var qy = y - ( yLife < yLast ? yLife : yLast);
ctx.moveTo( qx > cx ? qx : cx, qy < cy ? qy : cy);
var sx = x - ( xCur < 0 ? xCur : 0);
var sy = y - ( yCur > 0 ? yCur : 0);
ctx.lineTo( sx > cx ? sx : cx, sy < cy ? sy : cy);
if ( mod == "x" ){
ctx.lineTo( qx > cx ? qx : cx, (qy < cy ? qy : cy) + 2);
} else {
ctx.lineTo( (qx > cx ? qx : cx) + 2, qy < cy ? qy : cy);
}
ctx.lineTo( qx > cx ? qx : cx, qy < cy ? qy : cy);
ctx.closePath();
stars[j].nx = sx > cx ? sx : cx;
stars[j].ny = sy < cy ? sy : cy;
}
if(dx > 0 && dy < 0)
{
var qx = x - ( xLife < xLast ? xLife : xLast);
var qy = y - ( yLife > yLast ? yLife : yLast);
ctx.moveTo( qx < cx ? qx : cx, qy > cy ? qy : cy);
var sx = x - ( xCur > 0 ? xCur : 0);
var sy = y - ( yCur < 0 ? yCur : 0);
ctx.lineTo( sx < cx ? sx : cx, sy > cy ? sy : cy);
if ( mod == "x" ){
ctx.lineTo( qx < cx ? qx : cx, (qy > cy ? qy : cy) + 2);
} else {
ctx.lineTo( (qx < cx ? qx : cx) + 2, qy > cy ? qy : cy);
}
ctx.lineTo( qx < cx ? qx : cx, qy > cy ? qy : cy);
ctx.closePath();
stars[j].nx = sx < cx ? sx : cx;
stars[j].ny = sy > cy ? sy : cy;
}
В зависимости от того, в какую четверть попадает наша звезда, знаки перед значениями и сравнения отличаются, поэтом код практически дублируется 4 раза. Кроме того в переменной mod запоминается в какую координату считать ведущей ( то есть приравнивать dx к currentLife или dy). Без mod-а звезды в близи оси ординат будут «пролетать» слишком быстро, из за большого угла.
И последнее замечание – в оригинале используется всего два цвета, поэтому за один проход отрисовка на Canvas происходит всего два раза (так как указано два цвета). Все звезды одного цвета формируются в один путь, после чего выводятся на Canvas.
Остается все это обернуть в цикл и запустить.
Подход 3. Правильный.
Под правильным подходом я понимаю использование распространенных готовых решений и библиотек. Готовых решений при беглом осмотре не нашлось. В качестве библиотеки я решил попробовать libcanvas. Благо на Хабрахабре он представлен достаточно сильно.
Полный код и демонстрация на jsfiddle. (JSFiddle может не подгрузить atom и libcanvas с github-а, так что возможно надо будет несколько раз перезагрузить страницу)
В итоге получилось следующее:
new function () {
var center, i, helper, stars;
LibCanvas.extract();
helper = new App.Light(new Size( document.width, document.height));
center = helper.app.rectangle.center;
stars = [];
for(i = 0; i < 350; i++){
new function() {
var point = new Point(getRandomInt(document.width/2,document.width),document.height/2),
length = getRandomInt(50,150),
angle = getRandomInt(0,360),
coords = [
new Point(0,0),
new Point(0,0),
new Point(0,0)
],
path = helper.createVector( new Path()
.moveTo( coords[0] )
.lineTo( coords[1] )
.lineTo( coords[2] )
.lineTo( coords[0] )).setStyle({fill:"rgb(150,150,150)",stroke:"rgb(150,150,150)"});
point.rotate( - angle.degree(), center);
var star = {
point : point,
length : length,
angle : angle,
coords : coords,
live : 0,
setLength : function(){
if (arguments.length > 0){
this.live = arguments[0];
}
this.coords[0].x = this.point.x;
this.coords[0].y = this.point.y;
this.coords[1].x = this.coords[0].x + this.live * Math.cos( this.angle.degree() );
this.coords[1].y = this.coords[0].y - this.live * Math.sin( this.angle.degree() );
this.coords[2].x = this.coords[1].x + 2 * Math.sin( this.angle.degree() );
this.coords[2].y = this.coords[1].y + 2 * Math.cos( this.angle.degree() );
},
path : path
};
star.setLength();
stars.push(star);
};
}
setInterval(function(){
for(var i = 0; i < 350; i++){
stars[i].setLength( stars[i].live + 1 );
stars[i].path.redraw();
}
},10);
};
Надо сказать, что здесь, в отличие от боевого варианта, используется адекватная математика с тригонометрией, но я не стал дописывать до того же функционала. Скорость выполнения кода на libCanvas не сильно отличается от нативного метода, а кода в разы меньше и скорость разработки заметно выше. С самого начала я не стал использовать libCanvas по нескольким причинам: я ни разу не пользовался им до этого, я привык к чистому JavaScript и я боялся, что версия надстройка будет заметно медленней. Как оказалось боялся зря.
На этом все и еще раз с днем космонавтики!
Ссылки:
Пример с картинкой на jsfiddle.
«Боевой» пример на jsfiddle.
AtomJS и libCanvas для третьего примера.
Третий пример на libCanvas на jsfiddle. (может не заработать сразу из-за особенностей работы jsfiddle и github)
Промо сайт, для которого и создавался эффект.
Автор: agegorin