Солнечная система на graphics2d.js

Доброго {{timeOfDay}} Как-то затихла тема canvas-а на хабре…Давайте вспомним солнечную систему на нём (начало, LibCanvas, Fabric.js) и напишем ещё одну версию? Теперь на graphics2d.js.a93bf25543664f6d910d1b0c2e13fd93.png

На всякий случай напомню ТЗ Планеты вращаются вокруг звезды по часовой стрелке. Скорость вращения ближайшей планеты — 40 сек на оборот. Время на оборот каждой последующей планеты на 20 секунд больше предыдущей. Состав системы случайный. При каждом обновлении планеты занимают случайное место на своей орбите, с которого и начинают вращение и картинка для планеты также случайна.При наведении мыши на планету она выделяется круглой рамкой, изображение орбиты также меняется так, как это указано в макете.

При клике на планету выпадает меню. При наведении мыши на планету и при клике по ней анимация данной планеты останавливается, остальные планеты продолжают свое движение.

При наведении мыши на орбиту — орбита подсвечивается, планета нет.

Курсор мыши при наведении на планету или орбиту меняется на 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 слоя — для фона + солнца, орбит, планет и всплывающих подсказок.

//

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 — половина размеров солнца ca12f7f2bb9a41b4a74eece0f16b2c93.png

Планеты Класс планеты. В него передаётся радиус, время оборота и имя планеты. И помещаем каждую планету в 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: '1 px 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: '3 px 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); } Функция rand Вполне элементарно, просто чтобы не возникало недосказанностей. 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)); } 7e16363318034583904e50360ae98814.png(если временно закомментировать вызов createPlanet, т.к. её ещё нет)

Планета Спрайт с планетами выглядит так: 4ca037a48149bd5ff9181c4dd24e5a00.pngРазмер каждой планеты 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 (отключает / включает анимацию).

a4d2b93cf89644eab598c512495f7767.png

События Planet.prototype.overOrbit = function (e){ this.stroke.show (); // подсветка планеты this.orbit.stroke ('3 px rgba (0,192,255,1)'); // подсветка орбиты }

Planet.prototype.overPlanet = function (e){ this.stroke.show (); this.orbit.stroke ('3 px 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)', '1 px 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 ('1 px 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; } } 137b09f49f4d4ac6a0485f25deabb409.png

Запуск! Для анимации добавим классу 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); Итог в JSFiddle

© Habrahabr.ru