Доброго {{timeOfDay}}
Как-то затихла тема canvas-а на хабре…
Давайте вспомним солнечную систему на нём (начало, LibCanvas, Fabric.js) и напишем ещё одну версию? Теперь на graphics2d.js.
При наведении мыши на планету она выделяется круглой рамкой, изображение орбиты также меняется так, как это указано в макете.
При клике на планету выпадает меню. При наведении мыши на планету и при клике по ней анимация данной планеты останавливается, остальные планеты продолжают свое движение.
При наведении мыши на орбиту — орбита подсвечивается, планета нет.
Курсор мыши при наведении на планету или орбиту меняется на pointer.
Должно работать: Opera 12+, IE9+, актуальные версии Chrome, FF и Safari под огрызок.
Сразу: Посмотреть вживую; Исходники (всё в examples.js).
Начинаем
Нам понадобятся 2 плагина: Layers и Sprites.
JSFiddle с подключенным Graphics2D и плагинами.
На первом слое будет фон (звёзды и Солнце) — он меняться не будет вообще (можно заменить на статичную картинку, но раз уж решили на canvas..).
На втором — орбиты планет.
На третьем — планеты. Они меняются каждую секунду, так что только они и будут перерисовываться постоянно, не затрагивая объекты на других слоях.
Всплывающие подсказки с именами планет также будут появляться на третьем слое. Причём именно создаваться при наведении (мы же не хотим 24 дополнительных итерации каждую милисекунду на невидимые объекты?).
Начинаем: объявим размеры и центр canvas-а и имена планет. А также массив planetarray
.
var width = 840,
height = 840,
center = [width/2, height/2],
planetNames = [ "Selene", "Mimas", "Ares", "Enceladus", "Tethys", "Dione",
"Zeus", "Rhea", "Titan", "Janus", "Hyperion", "Iapetus" ],
planetarray = [];
Создадим объект app
(контейнер для слоёв) из div-а и 3 слоя — для фона + солнца, орбит, планет и всплывающих подсказок.
// <div id="solarsystem"></div>
var app = Graphics2D.app('#solarsystem', width, height), // размеры слоёв
background = app.layer(0),
orbits = app.layer(1),
planets = app.layer(2);
Заполняем первый слой — фон и солнце.
background.image('images/sky.png', 0, 0);
background.image('images/sun.png', center[0]-50, center[1]-50);
// 50,50 -- половина размеров солнца
Планеты
Класс планеты. В него передаётся радиус, время оборота и имя планеты. И помещаем каждую планету в planetarray
.
function Planet(options){
// свойства
this.radius = options.radius;
this.rotatePerMs = 360 / 100 / options.time;
this.time = options.time;
this.name = options.name;
// создание планеты
this.createOrbit(options);
this.createPlanet(options);
planetarray.push(this);
}
Сразу всё создадим:
for(var i = 0; i < 12; i++){
new Planet({
image: i, // индекс фрейма на спрайте с планетами (подробнее - дальше)
radius: 90 + i * 26, // магические числа из примера с LibCanvas :)
time: 40 + i * 20,
name: planetNames[i]
});
}
На этом моменте браузер будет ругаться на отсутствие createOrbit
и createPlanet
:) Далее.
Орбита
Рисуем на слое orbits
(он под слоем с планетами). Для каждой орбиты, помимо её самой, рисуем обводку планеты (она появляется при наведении и подсвечивает саму планету, а не орбиту). Обводка будет вращаться вместе с планетой, появляясь лишь при наведении (согласен, не очень экономно, но зато несложно). И да, она будет на слое с планетами ().
Planet.prototype.createOrbit = function(options){
var orbit = orbits.circle({
cx: center[0],
cy: center[1],
radius: this.radius,
stroke: '1px rgba(0,192,255,0.5)'
});
var stroke = planets.circle({
cx: center[0] + this.radius, // помещаем в координаты планеты
cy: center[1],
radius: 15,
fill: 'black', // перекрывает линию орбиты (другой способ - orbit.clip(stroke)).
stroke: '3px rgba(0,192,255,1)',
visible: false // изначально обводка невидима
});
// подключаем обработчик не к кругу-орбите, а к экземпляру класса Planet
orbit.mouseover(this.overOrbit.bind(this)).mouseout(this.out.bind(this));
this.orbit = orbit;
this.stroke = stroke;
// создаём случайный угол, сохраняем (чтобы использовать для самой планеты)
this.startAngle = rand(360);
// поворачиваем вокруг солнца (центра canvas-а)
stroke.rotate(this.startAngle, center);
}
function rand(num){
return Math.floor(Math.random() * num);
}
Возникает интересный вопрос — Circle обрабатывает события лишь внутри себя, а нам нужно ловить лишь обводку. Можно расположить орбиты друг над другом в порядке уменьшения, чтобы каждая перекрывала события следующей, но 1) имхо, немного костыльный вариант :) 2) при наведении на солнце будет подсвечиваться самая маленькая орбита, а этого в ТЗ нет.
Решение довольно просто — Graphics2D использует функцию объекта isPointIn, чтобы понять, находится ли курсор в объекте. Мы можем просто её переопределить:
orbit.isPointIn = function(x, y){
x -= center[0];
y -= center[1];
return (x*x + y*y) <= Math.pow(this._radius + 20, 2) && ((x*x + y*y) > Math.pow(this._radius - 20, 2));
}
(если временно закомментировать вызов createPlanet, т.к. её ещё нет)
Планета
Спрайт с планетами выглядит так:
Размер каждой планеты 26,26, так что мы можем создать спрайт, автоматически разбить его на фреймы и выбрать нужный.
Функция createPlanet — передаётся номер фрейма, радиус, время, имя:
Planet.prototype.createPlanet = function(options){
// спрайт, координаты
// просто ставим планету в центр и сдвигаем на радиус по x
var sprite = planets.sprite('images/planets.png', center[0] - 13 + options.radius, center[1] - 13);
// 13,13 - половины ширины и высоты фрейма
sprite.autoslice(26, 26); // разбиваем на фреймы
sprite.frame(options.image); // выбираем нужный фрейм
// ставим обработчики событий
sprite.mouseover(this.overPlanet.bind(this)).mouseout(this.out.bind(this));
sprite.click(this.click.bind(this));
sprite.cursor('pointer');
this.sprite = sprite;
// поворачиваем на начальный случайный угол
sprite.rotate(this.startAngle, center);}}
Обработчики событий также в прототипе класса Planet, их четыре — overOrbit, overPlanet (также показывает имя планеты), out и click (отключает / включает анимацию).
События
Planet.prototype.overOrbit = function(e){
this.stroke.show(); // подсветка планеты
this.orbit.stroke('3px rgba(0,192,255,1)'); // подсветка орбиты
}
Planet.prototype.overPlanet = function(e){
this.stroke.show();
this.orbit.stroke('3px rgba(0,192,255,1)');
if(this.rect){ // из-за анимации mouseover может сработать несколько раз подряд
this.rect.remove();
this.text.remove();
}
this.rect = planets.rect(e.contextX, e.contextY, 70, 25, 'rgb(0,56,100)', '1px rgb(0,30,50)');
this.text = planets.text({
text: this.name, // имя планеты
font: 'Arial 11pt',
x: e.contextX + 35, // 35,12 -- половины размеров фона подсказки, т.е. центрируем надпись
y: e.contextY + 12,
align: 'center',
baseline: 'middle',
fill: "rgba(0,192,255,1)"
});
}
Planet.prototype.out = function(){
this.stroke.hide();
this.orbit.stroke('1px rgba(0,192,255,0.5)');
if(this.text){
this.text.remove();
this.rect.remove();
}
}
Planet.prototype.click = function(){
if(this.rotatePerMs){
this.rotatePerMs = 0;
// самый простой способ - обнулять скорость, т.к. таймаут будет один.
}
else {
this.rotatePerMs = 360 / 100 / this.time;
}
}
Запуск!
Для анимации добавим классу Planet функцию, которая будет срабатывать раз в 1 мс для каждой планеты:
Planet.prototype.update = function(){
this.sprite.rotate(this.rotatePerMs, center);
this.stroke.rotate(this.rotatePerMs, center);
}
Поехали! :)
window.setInterval(function(){
for(var i = 0; i < 12; i++){
planetarray[i].update();
}
}, 1);
Автор: Keyten