JavaScript Вращение псевдотрехмерных цилиндрических объектов с помощью CSS и JavaScript

Как-то давно я прочитал занятную статью Романа Кортеса (Roman Cortes) об интересном эффекте, которого можно добиться используя старое-доброе css-свойство background-attachment:fixed. Основная суть этого занятного эффекта в разбиении видимой поверхности цилиндрического объекта на множество вертикальных прямоугольников и задании им фоновой картинки с background-attachment:fixed. Далее достаточно проскроллить блок с таким образом сформированным объектом и поразиться тому, что с помощью простого CSS можно добиться такого эффекта вращения цилиндрического объекта. Но в упомянутой выше статье вертикальные блоки для видимой части цилиндра, а также их настройки (в частности background-position) были подобраны вручную. Я решил написать простенький javascript-код, которой позволит собирать подобную цилиндрическую поверхность из любой прямоугольной текстуры.

Сначала немного теории, математики и простых формул. В качестве текстуры возьмем банку с колой наподобие той, которая есть в статье у Романа Кортеса:

Текстура банки с колой

Очевидно, что ширина картинки с текстурой будет равна длине окружности в сечении цилиндра. Нужно спроецировать видимую часть цилиндра на плоскость и разбить эту плоскость на необходимое количество вертикальных прямоугольников причем спроецировать на плоскость только четверть дуги окружности. А чтобы получить координаты точек на окружности достаточно использовать простые формулы с синусом и косинусом (примеры формул приведены в исходном коде на javascript).

Вращение псевдотрехмерных цилиндрических объектов с помощью CSS и JavaScript

Кроме того нужно рассчитать длины спроецированных на плоскость дуг окружности, чтобы корректно установить background-position для каждого блока.

Верстку выполним следующим образом. Создадим родительский блок с overflow:hidden, внутри создадим прямоугольный блок с проекцией цилиндра, а внутри его выведем множество узких вертикальных блоков с проекциями дуг окружности. Также зададим блок .b-barrel-mask с маской для цилиндра.

<div class="b-container">
    <div class="b-coke">
        <div class="b-barrel js-barrel">
            <div class="b-barrel-mask"></div>
        </div>
    </div>
</div>
.b-container {
    width:600px;
    overflow:auto;
    padding:0 3px;
    border:1px solid #ccc;
}
 
.b-coke {
    width:580px;
    padding-left:410px;
}
 
.b-barrel {
    position:relative;
    overflow:hidden;
    background-image:url(coke.png);
}
 
.b-barrel-block {
    height:100%;
    float:left;
    background-repeat:repeat-x;
    background-attachment:fixed;
}
 
.b-barrel-mask {
    width:100%;
    height:100%;
    top:0;
    left:0;
    position:absolute;
    background:url(can-mask.png);
    z-index:1;
}

Проецирование цилиндра на плоскость выполним с помощью небольшого скрипта. В нем выполняется расчет радиуса цилиндра, исходя из ширины картинки-текстуры, а также расчет ширины блоков в цикле прохода по четверти дуги окружности. Затем блоки добавляются в родительский блок с классом js-barrel с помощью jquery-функций .append() и prepend(), заполняя видимую часть проекции цилиндра.

// Функция для получения ширины и высоты файла с картинкой
function getImageSize(imgSrc, callback) {
    var newImg = new Image();
    newImg.src = imgSrc;
    newImg.onload = function() {
        callback(this.width, this.height);
    }
}
 
$(function() {
    $('.js-barrel').each(function() {
        // Получаем текстуру объекта из css-свойства background-image
        var $barrel = $(this),
            barrelImgCss = $barrel.css('background-image'),
            matches = barrelImgCss.match(/^url\((\S+)\)$/),
            barrelImgSrc = matches[1].replace(/['\"]/g, '');
        // Определяем размеры картинки с текстурой (ширина и высота)
        getImageSize(barrelImgSrc, function(width, height) {
            var barrelHeight = height,
                // Получаем диаметр окружности из ее длины D = 2R = C/π
                barrelWidth = Math.round(width / Math.PI),
                barrelRadius = barrelWidth / 2;
            // Задаем ширину и высоту родительскому блоку
            $barrel.css('width', barrelWidth + 'px')
                .css('height', barrelHeight + 'px')
                .css('background', 'none');
            // Проходим по дуге окружности с шагом в 1 градус...
            var rad = Math.PI / 180,
                blocks = [],
                prevX = -1,
                prevY = 91;
            // ...тем самым, мы формируем массив точек, ограничивающих
            // видимые блоки, на которые мы поделим окружность
            for (var i = 0; i <= 90; i++) {
                var grad = i * rad,
                    // Проекции точек окружности на оси X и Y
                    x = Math.round(Math.sin(grad) * barrelRadius),
                    y = Math.round(Math.cos(grad) * barrelRadius),
                    // Длина видимой части блока
                    l = Math.floor(i * barrelRadius / 180);
                if (prevX < x && prevY > y) {
                    var prevBlockX = 0;
                    if (prevX !== -1) {
                        prevBlockX = blocks[blocks.length - 1].x;
                    }
                    blocks.push({
                        x: x,
                        width: x - prevBlockX,
                        position: l
                    });
                    prevX = x;
                    prevY = y;
                }
            }
            // Удаляем первый элемент из массива точек
            blocks = blocks.slice(1);
            // Формируем и создаем массив видимых блоков, на которые мы поделим окружность
            for (var n = 0; n <= 1; n++) {
                // Собираем два симметричных ряда вертикальных видимых блоков
                for (var i = 0, imax = blocks.length; i < imax; i++) {
                    var $div = $('<div class="b-barrel-block"/>'),
                        sign = 1;
                    if (n) {
                        sign = -1;
                    }
                    $div.css('width', blocks[i].width + 'px')
                        .css('background-image', barrelImgCss)
                        .css('background-position', sign * (n ? (blocks[i - 1] ? 
                             blocks[i - 1].position : 0) : 
                             blocks[i].position) + 'px 0');
                    n ? $barrel.append($div) : $barrel.prepend($div);
                }
            }
        });
    });
});

В качестве классического примера привожу алюминиевые банки с Coca-Coca и Pepsi, а также матрешку. Матрешка не столь эффектна, как банки, потому что радиусы матрешки не вполне соответствуют радиусу, на который накладывается текстура.

P.S. Этот эффект не работает в IE6-7, так как они не полностью поддерживают css-свойство background-attachment:fixed. Кроме того, приведенные примеры не будут корректно отображаться в IE6 еще по одной причине — из-за известного бага с прозрачностью PNG. Пользуйтесь современными браузерами.