Делаем управляемый закат с помощью CSS3 и javascript

:


Небольшая демонстрация возможностей CSS3 на примере. Данный пример затрагивает свойства border-radius, box-shadow и linear-gradient.
head

Прошу расценивать предлагаемый код не в качестве готового решения, которое требуется использовать в своем проекте, а лишь как демонстрация. Хотя, при должной доработке, вполне возможно подойдет для какой-нибудь задачи.
Для тех, кому лень читать весь текст статьи я сразу прилагаю ссылку на рабочий пример. Солнце можно двигать по экрану, при его приближении к горизонту создается имитация заката. После полного ухода солнца за линию горизонта на небе выплывают звезды.
Пример тестировался в последних версиях современных браузеров — IE9, Chrome 16, Opera 11.60 и Firefox 8. Как ни странно, но шустрее всего на моей машине пример работал в браузере IE9, чуть похуже обстояли дела в браузере Firefox. В Chrome и Opera заметны некоторые фризы при движении.
body

Вначале для плавной смены цвета неба и почвы я хотел использовать какой-нибудь аналог color animate, затем понял, что это будет слишком сложно для простого примера, не говоря о том, что это вызывает уже вполне заметные тормоза при перемещении солнца. Именно поэтому я использовал несколько наложенных друг на друга блоков с изменяемой прозрачностью. На мой взгляд вышло достаточно реалистично, но я не дизайнер и воспроизвести достоверные предзакатные краски не смогу :)
Начнем с неба. Нам потребуется 3 блока для него, это дневное небо с голубоватым градиентом, закатное небо с красным градиентом и ночное небо с темно-синим градиентом. Все градиенты будем делать с помощью css3 свойства linear-gradient. Для начала определим наши блоки в теле документа
<div id="sky"></div>
<div id="sunsetSky"></div>
<div id="nightSky"></div>

Теперь объявим стили для нашего неба. Я решил использовать высоту неба равную 60% от высоты экрана и ширину во весь экран. Для правильного размещения слоев относительно друг друга будем использовать свойство z-index.


#sky {
    position: absolute;
    height: 60%;
    width: 100%;
    background: #004cf2;
    background: -moz-linear-gradient(top,  #004cf2 0%, #00b7ea 100%);
    background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#004cf2), color-stop(100%,#00b7ea));
    background: -webkit-linear-gradient(top,  #004cf2 0%,#00b7ea 100%);
    background: -o-linear-gradient(top,  #004cf2 0%,#00b7ea 100%);
    background: -ms-linear-gradient(top,  #004cf2 0%,#00b7ea 100%);
    background: linear-gradient(top,  #004cf2 0%,#00b7ea 100%);
    filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#004cf2', endColorstr='#00b7ea',GradientType=0 );
    z-index: 1;
}
#sunsetSky {
    position: absolute;
    height: 60%;
    width: 100%;
    background: #1a4182;
    background: -moz-linear-gradient(top,  #1a4182 0%, #ef6d56 100%);
    background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#1a4182), color-stop(100%,#ef6d56));
    background: -webkit-linear-gradient(top,  #1a4182 0%,#ef6d56 100%);
    background: -o-linear-gradient(top,  #1a4182 0%,#ef6d56 100%);
    background: -ms-linear-gradient(top,  #1a4182 0%,#ef6d56 100%);
    background: linear-gradient(top,  #1a4182 0%,#ef6d56 100%);
    filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#1a4182', endColorstr='#ef6d56',GradientType=0 );
    opacity: 0;
    z-index: 2;
}
#nightSky {
    position: absolute;
    height: 60%;
    width: 100%;
    background: #060a21;
    background: -moz-linear-gradient(top,  #060a21 0%, #0a1638 80%, #181f59 100%);
    background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#060a21), color-stop(80%,#0a1638), color-stop(100%,#181f59));
    background: -webkit-linear-gradient(top,  #060a21 0%,#0a1638 80%,#181f59 100%);
    background: -o-linear-gradient(top,  #060a21 0%,#0a1638 80%,#181f59 100%);
    background: -ms-linear-gradient(top,  #060a21 0%,#0a1638 80%,#181f59 100%);
    background: linear-gradient(top,  #060a21 0%,#0a1638 80%,#181f59 100%);
    filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#060a21', endColorstr='#181f59',GradientType=0 );
    opacity: 0;
    z-index: 3;
}

Про использование линейного градиента в качестве фона можно более подробно прочитать, например, здесь. Для генерации градиента я использовал сервис от Colorzilla.

Дальше нам предстоит работа над поверхностью земли. Тут вполне достаточно лишь 2 блоков, для дневной и ночной поверхностей.

<div id="grass"></div>
<div id="nightGrass"></div>

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

#grass {
    position: absolute;
    top: 60%;
    height: 40%;
    width: 100%;
    background: #42ab3f;
    z-index: 11;
}
#nightGrass {
    position: absolute;
    top: 60%;
    height: 40%;
    width: 100%;
    background: #000000;
    opacity: 0;
    z-index: 11;
}

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

<div id="sun">
	<div id="sunsetSun"></div>
</div>
#sun {
    position: absolute;
    height: 100px;
    width: 100px;
    -webkit-border-radius: 50px;
    -moz-border-radius: 50px;
    border-radius: 50px;
    background: #eff866;
    box-shadow: 0 0 150px #eff866;
    z-index: 9;
    cursor: move;
}
#sunsetSun {
    height: 100%;
    width: 100%;
    border-radius: 50px;
    -webkit-border-radius: 50px;
    -moz-border-radius: 50px;
    background: #F9AD43;
    box-shadow: 0 0 150px #F9AD43;
    opacity: 0;
}

Солнце у нас представляет собой блок с шириной и высотой в 100px. Чтобы задать ему форму круга мы используем css3 свойство border-radius со значением 50px (половина длины стороны блока). В принципе данное свойство просто в использовании, но для облегчения работы можно генерировать нужный код с помощью данного сервиса. Также он облегчит работу, если необходимо указывать разную степень скругления для отдельных углов.
Для эффектного ореола вокруг солнца мы используем свойство box-shadow. Для его простой генерации можно использовать данный сервис. Собственно там есть куча других, полезных css3 генераторов.

Теперь примемся за звезды. Так как их создание будет производится динамически, то нам требуется лишь определить стили для них.

.star {
    position: absolute;
    z-index: 5;
    background: #CEFFFA;
    height: 1px;
    width: 1px;
    opacity: 0;
}

На данном этапе работу с оформлением нашей страницы можно считать завершенной. Настала пора вдохнуть в нее интерактивности с помощью javascript. Основным ядром нашей системы будет jQuery. Можно, конечно, сделать и без него, будет чуть побольше кода. Полный листинг кода прилагаю ниже, думаю в объяснении он не нуждается, так как все достаточно прокомментировано.
Кроме jQuery, для организации drag-and-drop я использовал библиотеку dom-drag


<script type="text/javascript">
    $(function(){
        // HTML объект Солнца
        var sun = document.getElementById('sun');
        // jQuery объект Солнца
        var $sun = $(sun);
        // Предзакатное Солнце
        var $sunsetSun = $('#sunsetSun');
        // Предзакатное небо
        var $sunsetSky = $('#sunsetSky');
        // Ночное небо
        var $nightSky = $('#nightSky');
        // Дневной луг
        var $grass = $('#grass');
        // Ночной луг
        var $nightGrass = $('#nightGrass');
    
        // Делаем наше Солнце перемещаемым объектом
        Drag.init(sun);
        
        // "Рисуем" 100 звезд в случайных позициях
        makeStars(100);
        
        // Начальная позиция Солнца (сбивается при вызове Drag.init() выше)
        $sun.css({
            'top': 20,
            'left': 300
        });
        
        // Данное событие вызывается при перемещении объекта
        sun.onDrag = function(x, y){
            // Отступ Солнца от верхней границы экрана
            var sunTop = $sun.css('top');
            // Высота расположения Солнца относительно высоты неба, в процентах
            var sunPosition = parseInt(sunTop) / (parseInt($sunsetSky.css('height')) / 100);
            // Высота расположения Солнца относительно высоты экрана, в процентах
            var sunAbsolutePosition = parseInt(sunTop) / ($(window).height() / 100);

            // Изменяем прозрачность предзакатного неба
            $sunsetSky.css('opacity', (Math.floor(sunPosition) / 100));
            // Изменяем прозрачность предзакатного Солнца
            $sunsetSun.css('opacity', (Math.floor(sunPosition) / 100));
            // Изменяем прозрачность ночного луга
            $nightGrass.css('opacity', (Math.floor(sunPosition) / 100));

            // Проверяем, что Солнце находится на высоте ниже 60% относительно нижней части экрана
            if (sunAbsolutePosition >= 40){
                // Высота, на которой начинают проявляться звезды и ночное небо
                var start = $(window).height() / 100 * 40;
                // Высота, на которой звезды имеют максимальную яркость, а ночное небо перекрывает собой все остальные
                var end = $(window).height() / 100 * 65;
                // Позиция в процентах от start до end
                var pos = (parseInt(sunTop) - parseInt(start)) / ((parseInt(end) - parseInt(start)) / 100);
                // Изменяем прозрачность ночного неба
                $nightSky.css('opacity', pos / 100);
                // Изменяем прозрачность звезд
                $('.star').css('opacity', pos / 100);
            }
            // Если Солнце находится выше 60% относительно нижней части экрана, то скрываем все звезды
            else {
                $('.star').css('opacity', 0);
            }
        }

        // Возвращает случайное число в диапазоне от start до end
        function range(start, end){
            if ((start >= 0) && (end >= 0)){
                return Math.round(Math.abs(start) + (Math.random() * (Math.abs(end) - Math.abs(start))));
            }
            else if ((start <= 0) && (end <= 0)){
                return 0 - (Math.round(Math.abs(start) + (Math.random() * (Math.abs(end) - Math.abs(start)))));
            }
            else{
                return Math.round(((start) + Math.random() * (end - start)));
            }
        }

        // Генерирует count звезд в случайных позициях
        function makeStars(count){
            for (var i=0; i<=count; i++){
                // Создаем элемент, который будет нашей звездой
                var star = $(document.createElement('div'));
                // Присваиваем ему класс star
                star.addClass('star');
                // Вносим в DOM и делам дочерним к body
                star.appendTo('body');
                // Объявляем стили
                star.css({
                    // Высота - случайное значение от 0 до 60% от высоты экрана
                    'top': range(0, parseInt($(window).height()) / 100 * 60),
                    // Отступ слева - случайное значение от 0 до текущей ширины экрана
                    'left': range(0, $(window).width())
                });
            }
        }
    });
</script>

Меняя аргумент, передаваемый функции makeStars() мы можем менять количество звезд. Их увеличение ведет к большей реалистичности, но сильно влияет на скорость анимации. В скриншоте над хабракатом использовано значение 500.

Собственно на этом можно закончить, лишь хотел бы резюмировать. Многие согласятся, что именно для данной задачи мое решение, мягко говоря, не самое оптимальное. Ведь есть canvas, svg, silverlight. Flash наконец. Каждое из этих решений будет работать в данной задаче быстрее. Поэтому это всего лишь демонстрация, не более, сделанная for fun :)

Ссылки

Лучший, на мой взгляд, генератор линейных градиентов для CSS3
Куча разных генераторов стилей под CSS3
Библиотека javascript для организации DnD
Ну и пример.

Спасибо, что уделили внимание. Желаю всем удачной рабочей недели)