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

  • Tutorial

Доброго {{timeOfDay}}


Как-то затихла тема canvas-а на Хабре…

Давайте вспомним солнечную систему на нём (начало, LibCanvas, Fabric.js) и напишем ещё одну версию? Теперь на graphics2d.js.



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

При наведении мыши на планету она выделяется круглой рамкой, изображение орбиты также меняется так, как это указано в макете.

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

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

Курсор мыши при наведении на планету или орбиту меняется на pointer.

Должно работать: Opera 12+, IE9+, актуальные версии Chrome, FF и Safari под огрызок.

Примечание: спрайты и некоторые магические числа взяты из примера с LibCanvas, реализация аналогично немного отличается от ТЗ.

Сразу: Посмотреть вживую; Исходники (всё в 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);
}


Функция 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));
    }



(если временно закомментировать вызов 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);


Итог в JSFiddle

Only registered users can participate in poll. Log in, please.

Интересна ли тема graphics2d.js и canvas на Хабре?

  • 72.4%Graphics2d и canvas207
  • 16.4%Только canvas47
  • 11.2%Неинтересно32
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 31

    +4
    Тяжело иногда быть осведомленным: сложно смотреть на такую солнечную систему, зная, что пропорции расстояний в ней совсем другие.
      +1
      С размерами, кстати, тоже самое.
        +1
        Как и с орбитами.
        • UFO just landed and posted this here
            +2
            Движение относительно!
            • UFO just landed and posted this here
                0
                Относительно лишь движение без ускорения.
                Ускоренное же движение (а движение планет — самое что ни на есть ускоренное) мы чувствуем.
          +4
          Увы, в данном случае реалистичность категорически несовместима с юзабилити…
            +1
            Кто в курсе они понимают — уместить ее в масштабе на мониторе не вариант :)
              +1
              Речь не о масштабе, а о пропорциях.
            +2
            Эх… Ностальгия… Когда-то, очень давно, в школе я на Алгоритмическом Языке сделал модель Солнечной системы с рассчётом места положения планет в реальном времени (с учётом взаимодействия только между планетой и Солнцем). Базовые характеристи планет взял из энциклопедии. И они летали. Круто было…
              +2
              Статей о канвасе много не бывает)
              ps: Я как-то скептически отношусь к новым фреймворкам для js канваса, осознавая что их гораздо больше чем законченных проектов реализованных на них ((
                0
                Кстати, да, что-то серьёзное на canvas встречается довольно редко.
                Возможно, из-за отсутствия средств разработки, как во флеше?
                  +1
                  А редактор на printio.ru это не серьёзно? :) Пока что работает вроде нормально, используя Fabric.js, конечно, которую я именно для редактора и написал — printio.ru/tees/new
                    +1
                    Надо бы добавить вас в docs.google.com/spreadsheet/ccc?key=0Aqj_mVmuz3Y8dHNhUVFDYlRaaXlyX0xYSTVnalV5ZlE#gid=0

                    Если дадите всё информацию, могу прямо сейчас добавить.
                      0
                      О, спасибо :). Рад видеть вас здесь.

                      Graphics2D.js
                      размер 69 (полный) / 39 (минимизированный)
                      JavaScript
                      последнее обновление 28.11.14
                      MIT / LGPL
                      документация есть
                      форума / группы нет
                      unit-тестов нет
                      не модульный? (есть лишь рисование / объекты / события / анимация, прочая функциональность — фильтры, svg и т.п. — выходит в плагины)
                      зависимостей нет
                      svg-парсер — частично? (парсинг простейших svg-путей есть, парсинг любых — плагин)
                      webgl-рендер — нет
                      IE<9 — нет
                      Node.js — нет
                      Watchers — 3
                      Forks — 0
                      Project Page — keyten.github.io/Graphics2D/
                      Code repository — github.com/keyten/Graphics2D
                        +1
                        Отлично, добавил: )
                          0
                          Спасибо :)
                  +1
                  Пару лет назад переписывал эту демку для АПИ Яндекс.Карт.
                    +1
                    Вот бы земля ещё вращалась вокруг своей оси, а то на одной половине жарево и вечный день, у других ночь.
                      0
                      Сделайте, в чём проблема :).

                      Можете рассчитывать на мою помощь в лс.
                      +1
                      При клике на планету выпадает меню.

                      У меня не выпадает. Ограничение примера, или баг?
                        0
                        Во всех трёх вариантах (LibCanvas, FabricJS, Graphics2d) реализовано именно так: при наведении — подсказка с именем планеты, при клике — остановка анимации.
                        Если смотреть на ТЗ, мне не совсем понятно, что за меню (что там должно быть), а также как различать клик для меню и клик для анимации:
                        При клике на планету выпадает меню. При наведении мыши на планету и при клике по ней анимация данной планеты останавливается, остальные планеты продолжают свое движение.


                        Так что я решил реализовать последовать примеру других вариантов :)
                        +1
                        Фигово, что при наведении на планету, под ней чёрный круг образуется и закрывает звёзды.
                        А так очень плавно работает и CPU не грузит!
                          0
                          Это фиксится :)

                          При создании обводки планеты (функция createOrbit, переменная stroke) нужно просто убрать fill: 'black', и добавить после вызова planets.circle — orbit.clip(stroke). Но это будет кушать немножко больше.
                          0
                          Автор, ты почему спутники Сатурна называешь планетами? И почему они летают вокруг солнца :)
                            +1
                            Потому что ТЗ такое :)

                            Возможно, это не наша солнечная система, а какая-нибудь другая.
                              +1
                              Не может быть другой Солнечной системы. //зануда-мод
                                0
                                Не может быть другой Солнечной системы, а не солнечной системы.

                                Звёзды с планетами (особенно, экзопланетами) часто называют солнцами (с маленькой буквы), и их системы соответственно :).
                            +2
                            OH YOU! Зачем же снова на те же грабли с солнечной системой? ^_~
                              0
                              А что конкретно не так? :)
                              И есть мысли получше?

                            Only users with full accounts can post comments. Log in, please.