Пара тапок » javascript http://paratapok.ru Блог о веб-разработке Sun, 18 Dec 2022 12:14:48 +0000 ru-RU hourly 1 https://wordpress.org/?v=4.3.33 Как на javascript перевести цвет RGB в HEX и обратно? http://paratapok.ru/frontend/3441_kak-na-javascript-perevesti-cvet-rgb-v-hex-i-obratno/ http://paratapok.ru/frontend/3441_kak-na-javascript-perevesti-cvet-rgb-v-hex-i-obratno/#comments Tue, 13 Feb 2018 19:56:52 +0000 http://paratapok.ru/?p=3441 Читать далее →]]> Во фронтенде иногда возникает необходимость конвертации цвета между форматами RGB/RGBA и HEX.

HEX — это сокращение от слова «hexadecimal», которое переводится на русский язык, как «шестнадцатеричный». Число в этом формате может занимать 16 бит (2 байта) памяти и записывается с помощью 16 символов: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, C, D, E, F.

RGB — это сокращение является краткой записью трех названий цветов палитры «red — green — blue» («красный — зеленый — синий»). Стандартный RGB формат может занимать 24 бита (3 байта) памяти. Для его записи используются те же символы, что и у чисел в HEX формате (кроме букв).

Приведем примеры готовых функций, которыми вы можете воспользоваться для выполнения перевода цвета из hex в rgb и наоборот.

RGB to HEX

Если значение передаётся отдельными компонентами, то есть в виде трёх чисел:

function componentToHex(c) {
    var hex = c.toString(16);
    return hex.length == 1 ? "0" + hex : hex;
}

function rgb2hex(r, g, b) {
    return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
}

Более короткое решение с использованием побитового сдвига влево:

function rgb2hex(r, g, b) {
    return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
}

Если цвет в функцию будет передаваться в виде строки вида rgba(0, 255, 0, 0.5):

function rgb2hex(rgb) {
    var rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);

    return (rgb && rgb.length === 4) ? "#" +
        ("0" + parseInt(rgb[1],10).toString(16)).slice(-2) +
        ("0" + parseInt(rgb[2],10).toString(16)).slice(-2) +
        ("0" + parseInt(rgb[3],10).toString(16)).slice(-2) : '';
};

Причём функция поддерживает цвет и без прозрачности, то есть без указания альфа-компоненты.

HEX to RGB

Значение передаётся в виде строки, например #00ff00, а результат в виде объекта с тремя компонентами:

function hex2rgb(c) {
    var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(c);
    return result ? {
        r: parseInt(result[1], 16),
        g: parseInt(result[2], 16),
        b: parseInt(result[3], 16)
    } : null;
}

Ещё один вариант с использованием побитового сдвига вправо:

function hex2rgb(c) {
    var bigint = parseInt(c.split('#')[1], 16);
    var r = (bigint >> 16) & 255;
    var g = (bigint >> 8) & 255;
    var b = bigint & 255;

    return 'rgb(' + r + ',' + g + ',' + b + ')';
}

Как видно, сделать конвертер rgb в hex совсем несложно.

]]>
http://paratapok.ru/frontend/3441_kak-na-javascript-perevesti-cvet-rgb-v-hex-i-obratno/feed/ 0
Internet Explorer. Включаем JavaScript http://paratapok.ru/software/3041_how-to-enable-javascript-in-internet-explorer/ http://paratapok.ru/software/3041_how-to-enable-javascript-in-internet-explorer/#comments Thu, 01 Feb 2018 18:12:45 +0000 http://paratapok.ru/?p=3041 Читать далее →]]> JavaScript — это язык программирования, который позволяет управлять элементами (html-тегами) содержащимися в исходном коде веб-страницы (перемещать, загружать, скрывать, открывать). Иногда при открытии некоторой страницы какого-либо сайта может выдаваться ошибка о том, что активное содержимое страницы не может быть исполнено. Это проблема чаще всего возникает, если в вашем браузере не включена поддержка джаваскрипта.

Рассмотрим более подробно инструкцию о том, как включить JavaScript в Internet Explorer. Демонстрация решения задачи будет сделана в Explorer 9, но в версиях 8, 10, 11 она ничем не будет отличаться.

Во-первых, нужно запустить сам браузер Интернет Эксплорер, щелкнув на иконку, расположенную на рабочем столе, или иконку, которая расположена в меню «Пуск->Все программы->Internet Explorer»:

  1. В верхней панели справа выберите иконку «Сервис» или нажмите горячие клавиши Alt+X:

  1. После нажатия появится вот такое выпадающее меню:

Читайте также: «Как удалить Тор Браузер с компьютера полностью?».

  1. В этом окне необходимо найти и нажать на пункт «Свойства обозревателя». По нажатию появится вот такое диалоговое окошко:

  1. Далее необходимо перейти на вкладку «Безопасность». По переходу на которую, откроется следующее содержимое:

  1. Затем нажимаем левой кнопкой мыши, наведя курсор, на кнопку «Другой…». В результате откроется еще одно окошко:

  1. Среди появившегося списка параметров необходимо найти подраздел «Сценарии», в котором необходимо выбрать пункт «Активные сценарии -> Включить», а затем нажать кнопку «ОК»:

  1. По нажатию «ОК» в родительском окне нужно нажать кнопку «Применить», а затем «ОК»:

После описанных действий все активные сценарии на открываемых страницах сайтов должны стать работоспособными и выполнять закрепленные за ними функциональные задачи.

]]>
http://paratapok.ru/software/3041_how-to-enable-javascript-in-internet-explorer/feed/ 2
Тестовое задание: выпадающее меню на чистом javascript http://paratapok.ru/frontend/2922_testovoe-zadanie-vypadayushhee-menyu-na-chistom-javascript/ http://paratapok.ru/frontend/2922_testovoe-zadanie-vypadayushhee-menyu-na-chistom-javascript/#comments Sat, 27 Jan 2018 18:03:24 +0000 http://paratapok.ru/?p=2922 Читать далее →]]> Два-три года назад откликнулся на вакансию фронтенд-разработчика контентных проектов в Mail.ru Group. В ответ стандартно получил просьбу выполнить тестовое задание. К сожалению, моего уровня оказалось недостаточно и мне пришёл отказ. Хочу поделиться тестовым заданием и своим кодом, который был написан в ходе его выполнения. Возможно кому-то пригодится такое простенькое выпадающее меню на нативном javascript.

Итак, задание формулировалось следующим образом:

Реализовать идеальное, на твой взгляд, «выпадающее меню» (dropdown).
Пример/черновик верстки http://jsfiddle.net/jawLmp4x/
Минимальные требования:
0. Никаких библиотек, только нативный js
1. Возможность отследить раскрытие/скрытие «выпадашки» и выбор элемента
2. Настройка раскрытия/скрытия по нажатию или наведению
Всё остальное на твое усмотрение, можно просто описать словами, что бы ты ещё улучшил, какую функциональность добавил и так далее.
P.S. Поддержка браузерами не имеет значения

Оценив задание, мне стало понятно, что для «идеальности» нужно выполнить весьма немало требований. За вечер успел накидать только базовый вариант без дополнительных возможностей. А утром выслал его, сопроводив описание остальных улучшений следующими словами.

Основное требование к любому компоненту — возможность переиспользования, как в рамках одного проекта, так и за его пределами. Поэтому одним из главных критериев должна быть универсальность. Универсальность выпадающего меню, на мой взгляд, можно рассматривать в следующих аспектах:

  1. Возможность использования нескольких экземпляров меню в рамках одной страницы.
  2. Возможность настройки через data-атрибуты (в частности: горизонтальное/вертикальное, события отображения вложенных уровней и т.д.).
  3. Независимость от количества пунктов меню.
  4. Независимость от количества уровней вложенности.
  5. Независимость javascript-кода от классов, отвечающих за стилизацию меню.
  6. Автоматическое перепозиционирование выпадающих плашек в случае нехватки места для отображения в стандартном месте вывода в пределах видимой области страницы (например, если меню находится в самом низу страницы, то открывать выпадающие плашки не вниз, а вверх).
  7. Кроссбраузерность.
  8. Адаптированность под мобильные устройства (как с точки зрения обработки соответствующих событий, так и с точки зрения отображения).
  9. Возможность подписки на события (открытие/закрытие/выбор).

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

Пункт 1 из тестового задания рассматривал как сохранение текущего состояния меню, чтобы другие компоненты могли забрать эту информацию и использовать. В приведенном коде в экземпляре меню просто сохраняются индексы выбранных элементов. Реализацию подписки на события в тот вечер сделать не успел. Часть пунктов при этом могут быть обычными ссылками и ведут на другие страницы.

Пункт 2 был реализован засчёт data-атрибута data-event, который может принимать значения click или hover.

Итак, вставка горизонтального меню из нескольких пунктов, открывающееся по клику, выполняется с помощью следующего кода.

<div class="dd expanded" data-type="hor" data-event="click" id="horMenu">
    <div class="dd__item">
        <a href="#" class="dd__link">Menu</a>
        <ul class="dd__sub">
            <li class="dd__sub-item">item 1</li>
            <li class="dd__sub-item" disabled="true">item 2</li>
            <li class="dd__sub-item dd__sub-item__bold">item 3</li>
            <li class="dd__sub-item dd__sub-item__italic"><a href="//mail.ru/" target="_blank">item 4</a></li>
        </ul>
    </div><div class="dd__item">
        <span class="dd__link">Menu 2</span>
        <ul class="dd__sub">
            <li class="dd__sub-item">item 1</li>
            <li class="dd__sub-item" disabled="true">item 2</li>
            <li class="dd__sub-item dd__sub-item__bold">item 3</li>
            <li class="dd__sub-item dd__sub-item__italic"><a href="//mail.ru/" target="_blank">item 4</a></li>
        </ul>
    </div><div class="dd__item">
        <span class="dd__link">Menu 3</span>
        <ul class="dd__sub">
            <li class="dd__sub-item">item 1</li>
            <li class="dd__sub-item" disabled="true">item 2</li>
            <li class="dd__sub-item dd__sub-item__bold">item 3</li>
            <li class="dd__sub-item dd__sub-item__italic"><a href="//mail.ru/" target="_blank">item 4</a></li>
        </ul>
    </div>
</div>

А меню из одной кнопки с выпадающим списком при наведении таким:

<div class="dd expanded" data-type="ver" data-event="hover" id="verMenu">
    <div class="dd__item">
        <a href="#" class="dd__link">Menu</a>
        <ul class="dd__sub">
            <li class="dd__sub-item">item 1</li>
            <li class="dd__sub-item" disabled="true">item 2</li>
            <li class="dd__sub-item dd__sub-item__bold">item 3</li>
            <li class="dd__sub-item dd__sub-item__italic"><a href="//mail.ru/" target="_blank">item 4</a></li>
        </ul>
    </div>
</div>

Вот такая пара сотен строк кода получилась:

var app = {};

(function() {

    'use strict';

    app.nav = (function() {
        var _private = {},
            _public = {},
            _menu = document.getElementsByClassName('dd');

        _private.addEvent = function(el, type, handler) {
            if (el.addEventListener){
                el.addEventListener(type, handler, false)
            } else {
                el.attachEvent('on' + type, handler)
            }
        };

        // определение уровня элемента
        _private.getLevelItem = function(target) {
            var deep, aClasses, parent;

            aClasses = target.className.split(' ');
            for (var i = 0, len = aClasses.length; i < len; i++) {
                switch (aClasses[i]) {
                    // элемент первого уровня
                    case 'dd__link':
                        deep = 'parent';
                        parent = target.parentNode;
                        break;
                    // элемент второго уровня
                    case 'dd__sub-item':
                        deep = 'sub';
                        break;
                    default:
                }
            };
            return {
                deep: deep,
                parent: parent
            };
        };

        // проверка наличия класса, удаление при необходимости
        _private.checkHasClass = function(el, cl, dl) {
            var flag = false,
                aClasses;

            aClasses = el.className.split(' ');
            for (var i = 0, len = aClasses.length; i < len; i++) {
                if (aClasses[i] === cl) {
                    flag = true;
                    if (dl) {
                        aClasses.splice(i, 1);
                    }
                }
            };

            return {
                flag: flag,
                nameClass: aClasses.join(' ')
            }
        };

        // запись индекса активного пункта
        _private.writeIndex = function(flag, propertyName, els, current, id) {
            if (flag) {
                app.nav.navElements[id][propertyName] = -1;
            } else {
                for (var i = 0, len = els.length - 1 ; i <= len; i++) {
                    if (els[i] === current) {
                        app.nav.navElements[id][propertyName] = i;
                    }
                };
            }
        };

        // конструктор меню
        _private.Menu = function(el, type, event) {
            this.el = el;           // ссылка на элемент в DOM
            this.type = type;       // тип меню : горизонтальное или вертикальное
            this.event = event;     // событие срабатывания
            this.iItem = -1;        // индекс родительского элемента открытой плашки
                                    // если меньше 0, то все плашки скрыты
            this.iSub = -1;         // индекс выбранного элемента второго уровня
                                    // если меньше 0, то нет выбранного элемента
        };

        // скрытие выпадающих плашек
        _private.Menu.prototype.hideAll = function() {
            var menuItems = this.el.getElementsByClassName('dd__item');

            for (var i = menuItems.length - 1; i >= 0 ; i--) {
                menuItems[i].className = 'dd__item';
            };
        };

        // сброс выбранного пункта меню
        _private.Menu.prototype.resetChoose = function() {
            var menuSubItems = this.el.getElementsByClassName('dd__sub-item');
            for (var i = menuSubItems.length - 1; i >= 0 ; i--) {
                menuSubItems[i].className = _private.checkHasClass(menuSubItems[i], 'dd__sub-item__active', true).nameClass;
            };
        };

        // навешивание обработчиков
        _private.Menu.prototype.init = function() {
            var menuItems = this.el.getElementsByClassName('dd__item'),
                menuSub = this.el.getElementsByClassName('dd__sub'),
                events = [];

            if (this.event === 'click') {

                // обработчик на клик для пунктов меню первого уровня
                _private.addEvent(this.el, 'mousedown', function(e) {
                    var target = e.target,
                        childrenItems = target.parentNode.parentNode.getElementsByClassName('dd__item'),
                        childrenSubs,
                        level = _private.getLevelItem(target),
                        classActive,
                        menuId;

                    // если клик на элемент первого уровня
                    if (level.deep === 'parent') {

                        // получение
                        menuId = target.parentNode.parentNode.getAttribute('id');

                        // определение активности элемента
                        classActive = _private.checkHasClass(level.parent, 'dd__item__active', true);

                        // запись индекса открытой плашки
                        _private.writeIndex(classActive.flag, 'iItem', childrenItems, level.parent, menuId);

                        // скрытие всех плашек
                        app.nav.navElements[menuId].hideAll();

                        // обновление класса элемента
                        level.parent.className = classActive.nameClass;
                        if (!classActive.flag) {
                            level.parent.className = level.parent.className + ' dd__item__active';
                        }
                    }

                    e.preventDefault();
                });

                // скрытие выпадающих плашек при клике вне области меню
                _private.addEvent(document, 'mousedown', function(e) {

                    for (var menu in app.nav.navElements) {
                        if (app.nav.navElements[menu].event === 'click') {
                            app.nav.navElements[menu].hideAll();
                            app.nav.navElements[menu].iItem = -1;
                        }
                    }

                    e.stopPropagation();
                });
            }

            if (this.event === 'hover') {

                // обработчик на mouseover
                _private.addEvent(this.el, 'mouseover', function(e) {
                    var target = e.target,
                        childrenItems = target.parentNode.parentNode.getElementsByClassName('dd__item'),
                        level = _private.getLevelItem(target),
                        itemIsLink = target.nodeName === 'A',
                        classActive,
                        menuId,
                        parent;

                    // если элемент первого уровня
                    if (level.deep === 'parent') {

                        menuId = target.parentNode.parentNode.getAttribute('id');

                        // определение активности элемента
                        classActive = _private.checkHasClass(level.parent, 'dd__item__active', true);

                        _private.writeIndex(classActive.flag, 'iItem', childrenItems, level.parent, menuId);

                        // скрытие всех плашек
                        app.nav.navElements[menuId].hideAll();

                        // обновление класса элемента
                        level.parent.className = classActive.nameClass;
                        if (!classActive.flag) {
                            level.parent.className = level.parent.className + ' dd__item__active';
                        }
                    }

                    e.stopPropagation();
                });

                // обработчик на клик
                _private.addEvent(this.el, 'mousedown', function(e) {
                    var target = e.target;
                });

                // скрытие выпадающих плашек при клике вне области меню
                _private.addEvent(document, 'mouseover', function(e) {
                    for (var menu in app.nav.navElements) {
                        if (app.nav.navElements[menu].event === 'hover') {
                            app.nav.navElements[menu].hideAll();
                            app.nav.navElements[menu].iItem = -1;
                        }
                    }
                });
            }

            // обработчик выбора пунта меню второго уровня
            _private.addEvent(this.el, 'mousedown', function(e) {
                var target = e.target,
                    childrenSubs,
                    level = _private.getLevelItem(target),
                    itemIsLink = target.nodeName === 'A',
                    classActive,
                    parent,
                    menuId;

                // если клик на элемент второго уровня
                if ((level.deep === 'sub') || (_private.getLevelItem(target.parentNode).deep === 'sub')) {
                    if (!target.getAttribute('disabled')) {

                        if (itemIsLink) {
                            parent = target.parentNode.parentNode.parentNode.parentNode;
                        } else {
                            parent = target.parentNode.parentNode.parentNode;
                        }
                        menuId = parent.getAttribute('id');

                        // сброс
                        app.nav.navElements[menuId].resetChoose();

                        // обновление класса и запись индекса выбранного пункта
                        if (itemIsLink) {
                            target.parentNode.className = target.parentNode.className + ' dd__sub-item__active';
                            childrenSubs = target.parentNode.parentNode.getElementsByClassName('dd__sub-item');
                            _private.writeIndex(false, 'iSub', childrenSubs, level.parent, menuId);
                        } else {
                            target.className = target.className + ' dd__sub-item__active';
                            childrenSubs = target.parentNode.getElementsByClassName('dd__sub-item');
                            _private.writeIndex(false, 'iSub', childrenSubs, target, menuId);
                        }

                    } else {
                        e.preventDefault();
                    }
                }

                e.stopPropagation();
            });
        };

        // все меню на странице
        _public.navElements = {};

        // инициализация всех меню на странице
        _public.init = function() {

            for (var i = 0, len = _menu.length-1, id; i <= len; i++) {
                id = _menu[i].getAttribute('id');
                _public.navElements[id] = new _private.Menu(_menu[i], _menu[i].getAttribute('data-type'), _menu[i].getAttribute('data-event'));
                _public.navElements[id].init();
            }
        };

        return _public;
    })();

    app.nav.init();
})();
]]>
http://paratapok.ru/frontend/2922_testovoe-zadanie-vypadayushhee-menyu-na-chistom-javascript/feed/ 0
Как написать ипотечный калькулятор на javascript? http://paratapok.ru/frontend/2687_kak-napisat-ipotechnyj-kalkulyator-na-javascript/ http://paratapok.ru/frontend/2687_kak-napisat-ipotechnyj-kalkulyator-na-javascript/#comments Sat, 13 Jan 2018 10:34:32 +0000 http://paratapok.ru/?p=2687 Читать далее →]]> При разработке сайта строительной компании часто возникает необходимость помимо выбора квартиры дать возможность клиенту воспользоваться ипотечным или кредитным калькулятором. С помощью него чаще всего предлагается посчитать один из двух вариантов:

  1. Сколько времени клиенту придётся выплачивать ипотечный кредит при известных стоимости квартиры и ежемесячном платеже?
  2. Сколько составит ежемесячный платёж при известных стоимости квартиры и сроке кредита?

В статье мы рассмотрим, как на javascript написать такой калькулятор, но сначала рассмотрим формулы, по которым всё и рассчитывается.

В последнее время банки рассчитывают ипотеку по аннуитетной схеме погашения. Такой схемой пользуются, например, Сбербанк, ВТБ24 и другие крупные банки. Дифференцированная схема встречается редко, поэтому её рассматривать в рамках этой статьи не будем.

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

Ежемесячный платёж

Ежемесячный платёж вычисляется по следующей формуле:

Формула вычисления аннуитетного платежа

Формула вычисления аннуитетного платежа

В формуле приняты следующие обозначения: P – ежемесячный платеж, S – сумма кредита, i – процентная ставка в месяц, n – срок кредита в месяцах.

На основе формулы не составит труда написать js-функцию, которая принимает на вход три аргумента — сумму, период и ставку.

/**
 * Вычисляет ежемесячный платёж по сроку ипотеки
 *
 * @param {float} sum - сумма кредита
 * @param {integer} period - срок в годах
 * @param {float} rate - годовая ставка в процентах
 * @return {integer} или Nan
 */
function getPayment(sum, period, rate) {
    var i,
        koef,
        result;

    // ставка в месяц
    i = (rate / 12) / 100;

    // коэффициент аннуитета
    koef = (i * (Math.pow(1 + i, period * 12))) / (Math.pow(1 + i, period * 12) - 1);

    // итог
    result = sum * koef;

    // округлим до целых
    return result.toFixed();
};

Стоит отметить, что при очень больших значениях периода функция возвращает Nan, поэтому следует ограничивать входные данные. Чаще всего банки не дают ипотеку более чем на 30 лет, а с такими значениями функция справляется хорошо.

Срок

Используя пару правил математики из школьного курса выразим из вышеприведенной формулы срок кредита n:

Формула периода погашения при аннуитетном платеже

Формула периода погашения при аннуитетном платеже

Обозначения в формуле те же самые, что были приняты выше.

При написании функции для расчёта срока следует принять во внимание, что в стандартной библиотеке Math присутствует лишь функция вычисления натурального логарифма Math.log. Поэтому необходимо написать ещё и весьма тривиальную функцию для нахождения логарифма по произвольному основанию.

/**
 * Вычисление логарифма с произвольным основанием
 * @param {float} x - основание логарифма
 * @param {float} y - число, от которого требуется найти логарифм
 * @return {float}
 */
function getBaseLog(x, y) {
    return Math.log(y) / Math.log(x);
};

Итак, получим:

/**
 * Вычисляет период выплаты ипотеки по ежемесячному платежу
 *
 * @param {float} sum - сумма кредита
 * @param {float} plat - ежемясячный платеж
 * @param {float} rate - годовая ставка в процентах
 * @return {integer} или Nan
 */
function getPeriod(sum, plat, rate) {
    var mm,
        i,
        result;

    // ставка в месяц
    i = (rate / 12) / 100;

    mm = plat / sum;
    result = getBaseLog(1 + i, -mm / (i - mm));

    // округлим до целых
    return Math.ceil(+result);
};

Для проверки правильности подсчётов лучше всего использовать стандартные функции ПЛТ и КПЕР в Excel — скачать xlsx-файл с формулами проверок.

Готовый пример можно посмотреть по ссылке на демо.

]]>
http://paratapok.ru/frontend/2687_kak-napisat-ipotechnyj-kalkulyator-na-javascript/feed/ 0
Как с помощью Javascript/Jquery установить фокус на произвольный элемент? http://paratapok.ru/frontend/2613_kak-s-pomoshhyu-javascriptjquery-ustanovit-fokus-na-proizvolnyj-element/ http://paratapok.ru/frontend/2613_kak-s-pomoshhyu-javascriptjquery-ustanovit-fokus-na-proizvolnyj-element/#comments Wed, 05 Oct 2016 07:33:57 +0000 http://paratapok.ru/?p=2613 Установить js фокус на элемент достаточно просто:

document.getElementById('element').focus();

Установить focus на блок с id=element с помощью Jquery:

$('#element').focus();

Почему не работает jquery focus?

Важно помнить, атрибут tabindex="-1" позволяет произвольному блоку получить focus через Javascript.

]]>
http://paratapok.ru/frontend/2613_kak-s-pomoshhyu-javascriptjquery-ustanovit-fokus-na-proizvolnyj-element/feed/ 2
Почему возникают артефакты при рендере сцены в Three.js? http://paratapok.ru/frontend/2578_three-js-artefakty-i-glubina-z-bufera-pri-rendere-sceny/ http://paratapok.ru/frontend/2578_three-js-artefakty-i-glubina-z-bufera-pri-rendere-sceny/#comments Sun, 10 Apr 2016 06:42:34 +0000 http://paratapok.ru/?p=2578 Читать далее →]]> Если при работе с Three.js вы столкнулись с различного рода артефактами, например, отображением частей объектов, которых видно быть не должно, появлением произвольных черных пятен или исчезанием/обрезанием частей объектов и т.д., то предлагаем несколько советов, как решить данную проблему.

Пример артефактов: появление частей объектов с заднего плана

Пример артефактов: появление частей объектов с заднего плана. Кликните для просмотра анимации

Причины появления артефактов могут быть разные, наиболее частые из них следующие:

1. Максимальное расстояние, которое рендерит камера меньше, чем расположение некоторых объектов. В этом случае объекты или их части просто напросто будут обрезаться и возможно периодически появляться и исчезать.

Например, при инициализации камеры был указан параметр far = 1000, а объект или какая-либо его часть находится за заданными пределами.

camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.001, 1000 );
...
obj = new THREE.Mesh( new THREE.BoxGeometry( 250, 250, 250 ), new THREE.MeshLambertMaterial( { color: Math.random() * 0xffffff } ) );
obj.position.x = 900;
obj.position.y = 900;
obj.position.z = 900;

Проблема проиллюстрирована на картинках, представленных ниже.

Неверно задан параметр камеры far

Неверно задан параметр камеры far

2. Объекты находятся далеко от камеры, но относительно близко к друг к другу. В таком случае некоторые части объекта, расположенного на заднем плане могут в некоторые моменты времени оказаться спереди и наложиться на другой объект.

Артефакт: наложение кубов друг на друга

Артефакт: наложение кубов друг на друга

Наглядный пример такой проблемы и ее решение представлены в одном из примеров, опубликованных в разделе примеров на официальном сайте Three.js.

Проблема объясняется точностью буфера глубины — depth buffer, которой не хватает в данной ситуации. Общий эффект представляет собой попеременное отображение частей то одного объекта, то другого — происходит своеобразная «борьба» за пиксели. Устраняются такие артефакты путем увеличения глубины z-buffer.

Стандартное создание рендерера выглядит так:

renderer = new THREE.WebGLRenderer();

Чтобы увеличить точность следует указать при инициализации свойство logarithmicDepthBuffer со значением true:

renderer = new THREE.WebGLRenderer({ antialias: true, logarithmicDepthBuffer: true });

Однако следует отметить, что при этом снижается производительность рендера сцены, и потому не всегда такое решение может быть пригодным. Поэтому такие артефакты теоретически могут быть отчасти решены следующими методами:

  1. более удаленное размещение объектов между друг другом;
  2. если все объекты находятся в рамках единого диапазона координат, то как вариант попробовать уменьшить модель в несколько раз, переведя координаты объектов, например, из тысяч в десятки.
]]>
http://paratapok.ru/frontend/2578_three-js-artefakty-i-glubina-z-bufera-pri-rendere-sceny/feed/ 0
Как получить размеры 3d-объекта с произвольным набором mesh-ей в Three.js? http://paratapok.ru/frontend/2568_kak-poluchit-razmery-3d-obekta-s-proizvolnym-naborom-mesh-ej-v-three-js/ http://paratapok.ru/frontend/2568_kak-poluchit-razmery-3d-obekta-s-proizvolnym-naborom-mesh-ej-v-three-js/#comments Thu, 31 Mar 2016 20:24:22 +0000 http://paratapok.ru/?p=2568 Читать далее →]]> При разработке приложений на Three.js зачастую возникает ситуация, когда на сцену помещается 3d object с набором потомков. Особенностью 3d-объекта является то, что у него отсутствует свойство геометрии с методом computeBoundingBox(), который позволяет получить границы параллелепипеда (координаты max и min), в которые заключен объект. Тех, кто только начинает работать с Three.js, может заинтересовать вопрос, каким образом вычислить размеры такого объекта?

Есть простой способ. Для этого следует всего лишь создать объект с помощью конструктора THREE.Box3() и воспользоваться методом setFromObject(), в который передать наш 3d-объект, напичканный разнообразными мешами. Теперь у нас есть граничные координаты max и min, соответственно вычисление размеров уже дело арифметики.

var getBoundaryGeometry = function(obj) {
    var modelBoundingBox;

    modelBoundingBox = new THREE.Box3().setFromObject(obj);
    modelBoundingBox.size = {};
    modelBoundingBox.size.x = modelBoundingBox.max.x - modelBoundingBox.min.x;
    modelBoundingBox.size.y = modelBoundingBox.max.y - modelBoundingBox.min.y;
    modelBoundingBox.size.z = modelBoundingBox.max.z - modelBoundingBox.min.z;

    return modelBoundingBox;
};

Как видите данный способ занимает всего пару строк. Хотя справедливости ради следует заметить, что можно и вручную сделать все те же самые действия, перебрав в цикле всех потомков 3d-объекта. При этом необходимо последовательно вызывать метод computeBoundingBox() свойства geometry каждого потомка и сравнением вычислять наиболее отдаленные вершины max и min.

В приведенном ниже примере на сцену добавлен 3d-объект с 10 рандомно спозиционированными кубами. При инициализации вычисляются размеры объекта, которые пишутся в блок в правом верхнем углу. При этом также происходит вращение вокруг оси y, поэтому размеры параллелепипеда, расположенного вдоль координатных осей и включающего в себя все кубы, изменяются. С помощью кнопки «Обновить» можно обновить размеры.

]]>
http://paratapok.ru/frontend/2568_kak-poluchit-razmery-3d-obekta-s-proizvolnym-naborom-mesh-ej-v-three-js/feed/ 0
Drag-and-drop перемещение элементов по категориям на jQuery http://paratapok.ru/frontend/2555_drag-and-drop-peremeshhenie-elementov-po-kategoriyam-na-jquery/ http://paratapok.ru/frontend/2555_drag-and-drop-peremeshhenie-elementov-po-kategoriyam-na-jquery/#comments Thu, 04 Feb 2016 13:09:43 +0000 http://paratapok.ru/?p=2555 Читать далее →]]> Пожалуй, самым простым интерфейсным решением сортировки элементов по категориям является механизм перетаскивания: перетащить – бросить – сохранить. Такое решение является стандартным и в операционных системах, когда вы перекладываете папки или файлы из одного места в другое. Поэтому и на сайте грех не использовать уже известную пользователю логику. К слову, в gmail также письма можно перетаскивать на ярлыки.

Итак, рассмотрим, как решить задачу drag-and-drop сортировки с использованием javascript. Предположим, что у пользователя есть список из нескольких фотографий, и ему необходимо разложить их по соответствующим папкам/альбомам. Приведенный ниже пример может быть легко адаптирован под ваши нужды.

Пример работы скрипта перетаскивания элементов

Пример работы скрипта перетаскивания элементов

Важно! Сразу следует отметить, что будем рассматривать не drag events, а стандартные mouse events для десктопных устройств и touch events для тач-устройств. Т.е. по сути будем эмулировать drag-and-drop события.

Макет

Интерфейс состоит из двух частей: списка папок (левая колонка) и списка элементов (правая колонка). Все тривиально и стандартно.

Для минимизации трудозатрат макет в примере сверстан на основе весьма популярного фреймворка bootstrap, однако стили можно легко написать самому, не прибегая к сторонним css-фреймворкам. В представленном примере были использованы плагины:

  1. underscore.js – для шаблонизации данных, приходящих от сервера;
  2. nanoScroller.js – для стилизации скроллбара для случая, когда список папок занимает по высоте больше места, чем высота окна.

Алгоритм сортировки фотографий по альбомам

Не углубляясь в подробности алгоритм будет следующим:

  1. Вешаем обработчик событий mousedown/touchstart на перетаскиваемые элементы.
  2. При возникновении события mousedown/touchstart вешаем:
    • Событие mousemove/touchmove.
    • Событие mouseup/touchend.
  3. При возникновении события mousemove/touchmove:
    • Определяем dom-элемент, над которым находится курсор мыши.
    • Если активный элемент drop-областью, то подсвечиваем его.
  4. При возникновении события mouseup/touchend:
    • Проверяем было ли событие типа drag-and-drop и является ли элемент, над которым закончилось действие пользователя, drop-областью.
    • В случае успешного прохождения проверок инициируем ajax-запрос на перемещение элемента в папку. Для этого потребуется передать id элемента и id папки.
    • После того, как запрос на перемещение элемента исполнен, в ответ будет получен список папок с обновленным количеством элементов. Рендерим полученные данные.

Исходя из приведенного выше следует, что в рамках реализации необходимо решить задачи перемещения блоков на jquery и отлавливания нахождения курсора над нужными dom-элементами.

Список drop-областей

При верстке данного блока следует обратить внимание, что каждую папку, в которую у пользователя будет возможность переносить фотографии, необходимо обернуть в div с вспомогательным классом b-folders__item и указанием id в data-атрибуте. С помощью этого класса мы будем узнавать, является ли данная область целевой, т.е. отлавливать «бросок» фотографии на эту область. И при успешном выполнении также следует отправлять ajax-запрос, для которого потребуется указать id папки, в которую перемещается элемент.

Функционал находится в файле 30-category.js. Непосредственно к теме статьи этот код не относится. Он является вспомогательным и обеспечивает «плавание» меню с папками вслед за скроллом страницы, а также применяет кастомный скроллбар для случая, когда все элементы не влезают в видимую область.

Список drag-элементов

Функционал находится в файле 40-folders.js. Нет смысла подробно описывать последовательность действий – весь код хорошо прокомментирован. Приведем основной код, которому посвящена статья, и остановимся на некоторых моментах.

// обработчик на нажатие мыши
body.on('mousedown touchstart', '.b-photos__item .b-photos__link', function(e) {
    var self = $(this),
        offerId = self.data('id'),
        pos = {},
        drop,
        touchElem,
        currentTarget,
        timeMouseDown = new Date().getTime(),
        moveInitFlag = false,
        moveInit = function() {

            // курсор
            body.addClass('drag');

            // создаем фейк элемент
            body.append('<div id="drag-drop" class="b-photos__touch">' + self.find('.b-photos__media').html() + '</div>');
            touchElem = $('#drag-drop');
            touchElem.css({
                left: pos.x + 2,
                top: pos.y + w.scrollTop() + 2
            });

            // отменяем клик
            self.one('mouseup touchend click tap', function(e) {
                e.preventDefault();
            });

            // помечаем, что пользователь тащит
            moveInitFlag = true;
        };

    // выбираем все дроп-зоны
    dropFolders = parent.find('.' + baseClass + '__item');

    // отмена перетаскивания элемента
    e.preventDefault();

    // сохранение позиции курсора
    pos.x = e.clientX;
    pos.y = e.clientY;

    // вешаем на элемент обработчик мув
    body.on('mousemove touchmove', function(e) {
        var isDropZone,
            currentPos = {},
            touches = e.originalEvent.touches;

        currentTarget = $(e.target);

        // bugfix Chrome https://code.google.com/p/chromium/issues/detail?id=161464
        // на событие mousedown хром генерирует событие touchmove
        if (new Date().getTime() < timeMouseDown + 10) {
            return;
        }

        // сохраняем позицию курсора
        currentPos.x = e.clientX;
        currentPos.y = e.clientY;

        // инициализация перетаскивания
        if (!moveInitFlag) moveInit();

        // обновляем позицию курсора и target, если тач устройство
        if (touches !== undefined) {
            currentPos.x = touches[0].clientX;
            currentPos.y = touches[0].clientY;
            currentTarget = $(document.elementFromPoint(currentPos.x, currentPos.y));
        }

        // обновление позиции превьюшки
        touchElem.css({
            left: currentPos.x + 2,
            top: currentPos.y + w.scrollTop() + 2
        });

        // находится ли курсор над drop-зоной ?
        isDropZone = person.testDropZone(currentTarget);

        // если target является папкой
        if (isDropZone) {
            dropFolders.removeClass(baseClass + '__item--drop');
            isDropZone.addClass(baseClass + '__item--drop');
        } else {
            dropFolders.removeClass(baseClass + '__item--drop');
        }
    });

    // отжимаем кнопку мыши
    body.one('mouseup touchend', function(e) {
        var elem = $(e.target),
            isDropZone,
            folderId = 0,
            folderName = '',
            folderCurrent = null,
            touches = e.originalEvent.changedTouches;

        // отвязываем события перемещения
        body.off('mousemove touchmove');

        // проверям какое событие произошло
        if (!moveInitFlag) return;

        // отменяем действие по умолчанию
        e.preventDefault();
        e.stopPropagation();

        // сбрасываем курсор
        body.removeClass('drag');

        // удаляем превьюшку 
        if (touchElem) touchElem.remove();

        // обновляем target, если тач устройство
        if (touches !== undefined) {
            // не работает в Safari
            // elem = $(document.elementFromPoint(touches[0].clientX, touches[0].clientY));

            // поэтому берем последнее значение из события touchmove
            elem = currentTarget;
        }

        // находится ли курсор над drop-зоной ?
        isDropZone = person.testDropZone(elem);

        // если target является папкой
        if (isDropZone) {

            folderId = isDropZone.data('id');
            folderName = isDropZone.find('.b-category-list__val').text();

            if (folderId === 0) folderId = '';

            // id текущей папки
            folderCurrent = parent.data('folderCurrent');

            if (folderCurrent === folderId) {
                // показываем сообщение
                DND.alert.show({
                    text: DND.CONST.MESSAGES.FOLDERS.INFOLDER,
                    time: 2000
                });

            } else {

                // запрос на перемещение папки
                $.ajax({
                    type: 'POST',
                    url: parent.data('folderMove'),
                    data: {
                        folder_id: folderId,
                        offer_id: offerId
                    },
                    dataType: 'json',
                    beforeSend: function() {
                        // блокируем блок 
                        self.parent().append('<div class="b-photos__load"></div>');
                    }
                }).done(function(data) {
                    var currentElem,
                        allCount,
                        alert;

                    // получен список папок

                    // прогоняем данные по шаблону и обновляем код блока
                    parent.find('.' + baseClass + '__list').html(DND.tmpl.other['tmpl__folders-list']({
                        folders: data,
                        current: folderCurrent
                    }));

                    // меняем счетчик количества объектов в папке Мое портфолио
                    (data[0].cnt_offers > 0) 
                        ? allCount = '(' + data[0].cnt_offers + ')'
                        : allCount = '';
                    parent.find('.' + baseClass + '__parent .b-category-list__count').text(allCount);

                    // удаляем итем со страницы
                    currentElem = self.closest('.b-photos__item');
                    currentElem.hide(500, function() {
                        currentElem.remove();

                        // выраниванием список
                        // DND.visual.valignListPortfolio();

                        // инициируем событие перемещения презентации в другу папку
                        body.trigger('folder.move');
                    });

                    // показываем сообщение
                    alert = DND.alert.show({
                        text: DND.CONST.MESSAGES.FOLDERS.AFTER_MOVE + ' «' + folderName + '».',
                        time: 2000
                    });

                }).fail(function() {
                    self.parent().find('.b-photos__load').fadeOut(300);

                    DND.alert.show({
                        text: DND.CONST.MESSAGES.ERROR_DEFAULT,
                        time: 2000
                    });
                });
            }
        }

        // убираем стили перетаскивания с папок
        dropFolders.removeClass(baseClass + '__item--drop');

        return false;
    });
});

Как получать координаты для touch-устройств?

Для десктопных устройств в объекте события event есть свойства clientX и clientY, однако в touch-устройствах они отсутствуют. В то же время в объекте события есть свойство touches. Следует всего лишь взять первый элемент массива и так же получить значения свойств clientX и clientY.

[event.touches[0].clientX, event.touches[0].clientY]

Как получить dom-элемент по координатам мыши?

Чтобы получить элемент, над которым находится курсор необходимо использовать метод elementFromPoint:

document.elementFromPoint(event.clientX, event.clientY)

Как определить, что dom-элемент является drop-областью?

Всё просто, используем метод closest, который ищем ближайшего предка с указанным классом. Если такого не нашлось, значит элемент не является drop-областью.

Баг с определением элемента по координатам в safari на touch-устройствах

На момент написания статьи в Safari был замечен баг, заключающийся в том, что метод elementFromPoint отрабатывал неверно, и возвращался указатель на родителя на несколько уровней выше. Этот момент обозначен в коде, соответствующая строка закомментирована. Поэтому в качестве альтернативы было решено брать последнее запомненное значение из события touchmove.

Итак, как видите нет ничего сложного в том, чтобы реализовать на javascript перемещение элементов (в нашем случае картинок) мышью. Необходимо лишь в нужном порядке отслеживать соответствующие события, которые в совокупности дают эмуляцию нативных drag-and-drop событий.

]]>
http://paratapok.ru/frontend/2555_drag-and-drop-peremeshhenie-elementov-po-kategoriyam-na-jquery/feed/ 0
Разделение числа на разряды на Javascript: отбивка тысяч пробелами http://paratapok.ru/frontend/2536_razdelenie-chisla-na-razryady-na-javascript-otbivka-tysyach-probelami/ http://paratapok.ru/frontend/2536_razdelenie-chisla-na-razryady-na-javascript-otbivka-tysyach-probelami/#comments Wed, 13 Jan 2016 21:19:35 +0000 http://paratapok.ru/?p=2536 Читать далее →]]> Сегодня рассмотрим проблему форматирования чисел на javascript, а именно как легко произвести отбивку тысяч, миллионов и миллиардов при выводе цены. Пост рассчитан и будет полезен в большей степени новичкам.

Обычно шаблоны страниц генерируются на сервере и за отображение цен, да и в целом денежных значений, отвечает используемый на сервере шаблонизатор. Например, в Jinja есть фильтр money, который позволяет не заморачиваться с разделением числа на разряды: {{ tovar.price|money }}. Цену «12345678 руб.» python-шаблонизатор выведет как «12 345 678 руб.». В php-шаблонизаторе Twig также есть соответствующий фильтр number_format(decimals, decimalSeparator, thousandSeparator), который помимо разделителя тысяч к тому же сразу позволяет задать разделитель целой и дробной частей {{ tovar.price|number_format(2, '.', ' ') }}.

Однако, javascript, да и современные js-фреймворки лишены данной функциональности. Ввиду этого, при получении данных от сервера и шаблонизации их на клиенте, данную задачу следует решать самостоятельно. Разделение разрядов числа пробелами сама по себе не сложная задача — даже новичок сможет написать соответствующую функцию. Однако, когда времени мало или думать совсем не хочется, то лучше всего воспользоваться готовым решением.

Итак, ниже представлен код функции thousandSeparator на чистом javascript, которая производит отбивку разрядов кратных трем.

var thousandSeparator = function(str) {
    var parts = (str + '').split('.'),
        main = parts[0],
        len = main.length,
        output = '',
        i = len - 1;
    
    while(i >= 0) {
        output = main.charAt(i) + output;
        if ((len - i) % 3 === 0 && i > 0) {
            output = ' ' + output;
        }
        --i;
    }

    if (parts.length > 1) {
        output += '.' + parts[1];
    }
    return output;
};

Если вкратце рассматривать суть функции, то видно, что берется целая часть числа и в цикле проходится каждый разряд от старшего к младшему. Если порядковый номер разряда делится на 3 (кроме самого старшего), тогда производится добавление пробела. Таким образом, после 3, 6, 9 и т.д. разрядов появятся пробелы.

Использовать данную функцию очень просто:

console.log(thousandSeparator(12345678.9));
]]>
http://paratapok.ru/frontend/2536_razdelenie-chisla-na-razryady-na-javascript-otbivka-tysyach-probelami/feed/ 1