javascript

UserJS для воспроизведения H.264 в «Опере»

Существует плагин «DivX web player», который отлично подменяет тег «VIDEO», в случае, если должен быть использован кодек H.264, но браузер его не поддерживает.

К сожалению, в «Опере» он почему-то не работает. Я сделал UserJS, который, пользуясь этим плагином, подменяет видеотег, если у него указан тип «video/mp4». Все атрибуты он не переносит (я просто пока не столкнулся с тем, что это нужно), буду добавлять, если столкнусь. Кроме того, я подменяю метод «canPlayType», чтобы показать, что браузер H.264 поддерживает.

Самая засада — это тег «VIDEO» без размеров. Мне для поддержки этого приходтся запускать проигрывание и когда становятся доступны медиаданные, останавливать воспроизведение и переписывать размеры видео.

// ==UserScript==
// @name        XVid patch
// @author      Evgeny Stepanischev aka Bolk
// @version     1.00
// @namespace   /files/opera-xvid-patch.js
// @modified    2012-04-14
// @include     http://*
// ==/UserScript==

!function () {
    var htmlif = function (attr, attrname) {
        return attr ? attrname + '="' + attr + '" ' : '';
    }

    var patchtag = function (o, src) {
        var html = '<embed type="video/divx" '+
        'custommode="none" ';

        if (o.preload == 'auto') html += 'bufferingMode="full" '; else
        if (o.preload == 'none') html += 'bufferingMode="null" ';

        html += htmlif(o.poster, 'previewImage')+
                htmlif(o.id, 'id')+
                htmlif(o.name, 'name')+
                htmlif(src || o.src, 'src')+
                htmlif(o.controls ? 'full' : 'null', 'mode')+
                htmlif(o.width, 'width')+
                htmlif(o.height, 'height');

        if (!o.height || !o.width) {
            html += 'onloadedmetadata="this.width=this.videoWidth;this.height=this.videoHeight" ';
            html += htmlif(!o.autoplay && 'if (!this._sized) {this.pause(); this.currentTime=0; this._sized=1}', 'onplay');

            html += 'autoPlay=true ';
        } else {
            html += htmlif(o.autoplay ? 'true' : 'false', 'autoPlay');
        }

        html += 'pluginspage="http://go.divx.com/plugin/download/"></embed>';

        o.outerHTML = html;
    };

    var patch = function () {
        window.opera.addEventListener('BeforeScript', function() {

            with (window.HTMLVideoElement)
            if (prototype._canPlayType === undefined) {
                prototype._canPlayType = prototype.canPlayType;

                prototype.canPlayType = function (type) {
                    if (type.substr(0, 9) == 'video/mp4') {
                        return 'probably';
                    }

                    return this._canPlayType(type);
                }
            }

            !function() {
                var videos = document.getElementsByTagName('VIDEO');

                for (var i = 0, vl = videos.length; i<vl; i++) {
                    if (!videos[i]) continue;

                    if (videos[i].type && videos[i].type.substr(0, 9) == 'video/mp4') {

                    } else {
                        var sources = videos[i].getElementsByTagName('source');

                        var foundSupported = false, foundMP4 = false;

                        for (var j = 0, sl = sources.length; j < sl; j++) {
                            if (videos[i]._canPlayType(sources[j].type) !== '') {
                                foundSupported = true;
                                break;
                            }

                            if (sources[j].type.substr(0, 9) == 'video/mp4') {
                                foundMP4 = sources[j].src;
                            }
                        }

                        if (!foundSupported && foundMP4) {
                            patchtag(videos[i], foundMP4);
                        }
                    }
                }
            }();
        });
    };

    for (var i = 0, l = navigator.plugins.length; i < l; i++) {
        if (navigator.plugins[i].filename == 'DivXBrowserPlugin.plugin') {
            return patch();
        }
    }
}();

Добавлено: в комментариях справдливо указали на недостаток — перехватывать надо не только BeforeScript. Исправлять не буду до первого реального случая, когда где-то этого события не будет достаточно.

13 комментариев

Работа с камерой и распознавание чисел в «Опере»

Распозновалка (43.68КиБ)

Я вчера заморочился на отличненько. Мне показалось скучным и грустным вводить цифры счёта за квартиру в окошко «Госуслуг» и решил сделать автоматическую распознавалку чисел, попутно изучив работу с камерой из браузера.

Доступ к камере — эксперментальный стандарт, поэтому мало кто из браузеров может им похвастаться, я использовал специальный билд «Opera-Labs-Camera», который можно взять с сайта снапшотов «Оперы». Работать мой пример, соответственно, будет только там.

АПИ оказалось несложным. Всё, что нужно сделать — соединить специальным образом тег VIDEO с источником стримового видео; потом я 10 раз в секунд забираю кадр с видео и кладу в CANVAS (на скриншоте он цветной), оттуда вырезаю небольшую область, перевожу в градации серого, беру за порог 75% от усреднённого цвета и перевожу всё в ч/б.

Больше всего из трёх часов, которые у меня ушли на это, я делал распознование цифр. Алгоритмы, которые я нашёл в интернете, были очень монструозными, да и переписывать готовое было скучно, поэтом я перепробовал несколько вариантов, которые пришли мне в голову.

В итоге, лучше всех распознаёт следующий алгоритм.

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

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

На скриншоте в большом текстовом поле записаны получившиеся комбинации. Первое — номер по порядку, далее (через двоеточие) — количество переходов по вертикали и потом описанная последовательность. Например, «ноль» записывается как «2:11» (два перехода по вертикали, потом одинаковые переходы по разу слева и справа разделительной линии).

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

9 комментариев

Каррирование и частичное применение

Я, оказывается, путал раньше каррирование и частичное применение. Даже не то чтобы путал, просто не пытался разобраться в чём разница между терминами, применяя то и другое по мере надобности. Недавно на «Хабре» в комментариях мне на моё незнание указали и я уяснил, наконец, что конкретно стоит за каждым термином.

Каррирование — преобразование функции от двух аргументов в функцию от первого аргумента, возвращающую функцию, результат вызова которой со вторым агрументом эквивалентен вызову первоначальной функции с упомянутыми аргументами. Немного запутанно, с примерами станет всё яснее. Я буду объяснять на примере Джаваскрипта, его больше народу знает, а на ПХП очень уж многословно получается. Кстати, в соответствующую статью в «Википедии» я добавил пример на «Гугл Гоу».

// функция, суммирующая два числа
function sum(x, y) {
    return x+y;
}

alert(sum(2, 8)); // вернёт 10

function sum(x) {
    return function (y) {
        return x+y;
    }
}

alert(sum(2)(8)); // тоже вернёт 10

При вызове второй функции sum создаётся функция, в которую из-за замыкания, «железно» попадает аргумент «x» со значением «2». Поэтому возвращаемая функция будет всегда суммировать с двойкой. С этим фактом тесно связано частичное применение, потому что оно только что, фактически, состоялось.

Частичное применение функции (Partial function) — создание некой функции на базе указанной, где некоторые агрументы заменены конкретными значениями. Например:

function partial_one_arg(f, a) {
    return function(b) {
        return f(a, b);
    }
}

min0 = partial_one_arg(Math.min, 0);
min1 = partial_one_arg(Math.min, 1);

alert([min0(-5), min0(5)]); // вернёт [-5, 0]
alert([min1(-5), min1(5)]); // вернёт [-5, 1]

В примере я заменил один из агрументов фунции нахождения минимального значения на конкретное значение, теперь при вызове функции «min0» сравнение будет идти с нулём, при вызове «min1» — с единицей.

Очень простые штуки, часто встречающиеся в функциональном программировании.

8 комментариев

%u → UTF-8

Столкнулся сегодня с задачей — надо сконвертировать пришедшую из JavaScript строку (в ней символы Юникода в виде «%uXXXX») в UTF-8 на PHP. Придумал, на мой взгляд, самый короткий способ это сделать:

echo json_decode(str_replace('%u', '\\u', json_encode($str_from_javascript)));

10 комментариев

С Новым годом, что ли?

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

[][([]+{})[-~[]+(!![]<<!![]+!![])]+([]+{})
[-~[]]+([]+[][{}])[-~[]]+([]+!{})[!![]+!![]+
!![]]+([]+!![])[+![]]+([]+!![])[+!![]]+
([]+!![])[-[~[]+~[]]]+([]+{})[-~[]+(!![]<<!![]+
!![])]+([]+!![])[+![]]+([]+{})[-~[]]+([]+!![])
[+!![]]][([]+{})[-~[]+(!![]<<!![]+!![])]+([]+{})
[-~[]]+([]+[][{}])[-~[]]+([]+!{})[!![]+!![]+!![]]+
([]+!![])[+![]]+([]+!![])[+!![]]+([]+!![])
[-[~[]+~[]]]+([]+{})[-~[]+(!![]<<!![]+!![])]+
([]+!![])[+![]]+([]+{})[-~[]]+([]+!![])[+!![]]]
(([]+!![])[+!![]]+([]+{})[-~[]<<[-~[]+-~[]]]+
([]+!![])[-[]]+([]+[][{}])[-[]]+([]+!![])[+!![]]+
([]+[][{}])[-~[]]+([]+{})[~[]+(+!![]<<!![]+!![]+
!![])]+([]+![])[!![]+!![]+!![]]+([]+![])[!![]<<!![]+
!![]]+([]+![])[!![]+!![]]+([]+![])[-[]])()
[([{}-{}]+[])[-~[]]+([]+![])[!![]+!![]]+([]+![])
[!![]<<!![]+!![]]+([]+!![])[-~[]]+([]+!![])[+[]]](
[][([]+{})[-~[]+(!![]<<!![]+!![])]+([]+{})
[-~[]]+([]+[][{}])[-~[]]+([]+!{})[!![]+!![]+
!![]]+([]+!![])[+![]]+([]+!![])[+!![]]+
([]+!![])[-[~[]+~[]]]+([]+{})[-~[]+(!![]<<!![]+!![])]+
([]+!![])[+![]]+([]+{})[-~[]]+([]+!![])[+!![]]]
[([]+{})[-~[]+(!![]<<!![]+!![])]+([]+{})[-~[]]+
([]+[][{}])[-~[]]+([]+!{})[!![]+!![]+!![]]+
([]+!![])[+![]]+([]+!![])[+!![]]+([]+!![])
[-[~[]+~[]]]+([]+{})[-~[]+(!![]<<!![]+!![])]+
([]+!![])[+![]]+([]+{})[-~[]]+([]+!![])[+!![]]]
(([]+!![])[+!![]]+([]+{})[-~[]<<[-~[]+-~[]]]+
([]+!![])[-[]]+([]+[][{}])[-[]]+([]+!![])
[+!![]]+([]+[][{}])[-~[]]+([]+{})[~[]+(+!![]<<!![]+
!![]+!![])]+([]+![])[!![]+!![]+!![]]+([]+![])
[!![]<<!![]+!![]]+([]+![])[!![]+!![]]+([]+![])
[-[]])()[([]+![])[-~[]]+([]+!![])[+![]]+([]+{})
[-~[]]+([]+{})[!![]+!![]]](([]+![])[-~[]]+
([][([]+{})[-~[]+(!![]<<!![]+!![])]+([]+{})[-~[]]+
([]+[][{}])[-~[]]+([]+!{})[!![]+!![]+!![]]+([]+!![])
[+![]]+([]+!![])[+!![]]+([]+!![])[-[~[]+~[]]]+
([]+{})[-~[]+(!![]<<!![]+!![])]+([]+!![])[+![]]+
([]+{})[-~[]]+ ([]+!![])[+!![]]]+[])[(-~[]+[+![]])-!![]])+
([]+![])[-~[]]+
(/!/[([]+{})[-~[]+(!![]<<!![]+!![])]+([]+{})[-~[]]+([]+[][{}])
[-~[]]+([]+!{})[!![]+!![]+!![]]+([]+!![])[+![]]+([]+!![])[+!![]]+([]+!![])
[-[~[]+~[]]]+([]+{})[-~[]+(!![]<<!![]+!![])]+([]+!![])[+![]]+([]+{})
[-~[]]+([]+!![])[+!![]]]+[])[+!![]+[!![]<<!![]-~[]]]+
(/!/[([]+{})[-~[]+(!![]<<!![]+!![])]+([]+{})[-~[]]+([]+[][{}])
[-~[]]+([]+!{})[!![]+!![]+!![]]+([]+!![])[+![]]+([]+!![])[+!![]]+([]+!![])
[-[~[]+~[]]]+([]+{})[-~[]+(!![]<<!![]+!![])]+([]+!![])[+![]]+([]+{})
[-~[]]+([]+!![])[+!![]]]+[])[+!![]+[!![]<<!![]-~[]]]+
([]+[~[]/[]])[[[!![]+!![]]*[!![]+!![]]]*[!![]+!![]]]+
([]+{})[~[]+(+!![]<<!![]+!![]+!![])]+
([]+[][{}])[-~[]]+
([]+{})[-~[]<<[-~[]+-~[]]]+
([][([]+{})[-~[]+(!![]<<!![]+!![])]+([]+{})
[-~[]]+([]+[][{}])[-~[]]+([]+!{})[!![]+!![]+!![]]+([]+!![])
[+![]]+([]+!![])[+!![]]+([]+!![])[-[~[]+~[]]]+([]+{})[-~[]+
(!![]<<!![]+!![])]+([]+!![])[+![]]+([]+{})[-~[]]+([]+!![])
[+!![]]][([]+{})[-~[]+(!![]<<!![]+!![])]+([]+{})[-~[]]+
([]+[][{}])[-~[]]+([]+!{})[!![]+!![]+!![]]+([]+!![])[+![]]+
([]+!![])[+!![]]+([]+!![])[-[~[]+~[]]]+([]+{})[-~[]+
(!![]<<!![]+!![])]+([]+!![])[+![]]+([]+{})[-~[]]+([]+!![])
[+!![]]](([]+!![])[+!![]]+([]+{})[-~[]<<[-~[]+-~[]]]+
([]+!![])[-[]]+([]+[][{}])[-[]]+([]+!![])[+!![]]+([]+[][{}])
[-~[]]+([]+{})[~[]+(+!![]<<!![]+!![]+!![])]+([]+![])
[!![]+!![]+!![]]+([]+![])[!![]<<!![]+!![]]+([]+![])
[!![]+!![]]+([]+![])[-[]])()+[])[([]+![])[!![]+!![]+
!![]]+([]+![])[!![]+!![]]+([]+[][{}])[!![]+(!![]<<!![]+!![])]+
([]+{})[!![]+(!![]<<!![]+!![])]+([]+!![])[!![]+!![]+!![]]](~[]+~[])[-[]]+
([]+{})[~[]+(+!![]<<!![]+!![]+!![])]+
(~[]/[]+[])[-~[]<<-(~[]+~[]+~[])]+
([~[]<[]]+[])[-[~[]+~[]+~[]]]+
([{}-{}]+[])[-~[]]+
 ([]+!![])[+!![]]+
(/!/+[])[-~[]])

Запускать лучше всего в консоли браузера (в Firebug, Dragonfly и так далее). Цели сократить код у меня не было, более того, кое-где для разнообразия (буквы-то повторяются) я придумывал разные конструкции, чтобы не так скучной было. С Новым годом!

Если кого-то поразило это буйство скобочек, то намекаю ещё раз — это полноценная программа на Яваскрипте.

24 комментария

Почему не следует расширять встроенные типы JavaScript

На Хабре хорошая статья — «Monkey-Patching, или Расширение Встроенных Типов: религия или осознанный выбор?».

Оставить комментарий

Поддержка MJPEG в «Опере»

Как я уже писал, «Опера» иначе показывает MJPEG, не как остальные браузеры. Если открыть какую-нибудь веб-камеру, использующую этот формат, в десятой версии, то «Опера» сначала задумается, потом покажет какой-то кадр, 11 и 12 версии показывают ещё хуже — подобие потоковой передачи уже есть, но кадры лепятся один на другой и ничего разглядеть невозможно.

А мне хочется смотреть в веб-камеру на микрорайон, где у нас с женой квартира в Москве, через любимый браузер.

Вдохновившись библиотекой добавляющей нативную поддержку APNG в браузер, я подумал, что я ничем не хуже и смогу добавить поддержку MJPEG в «Оперу». Получился небольшой userjs, который я, естественно, выложил на «Гитхаб».

Идея такая — находим картинку во всех тегах IMG, у которой расширение «mjpg». Через XHR получаем с сервера видеопоток, копим кадр, подменяем найденную картинку и так далее.

Работает начиная с 12 версии браузера, которая ещё не вышла (кстати, в позавчерашней сборке сломали userjs напрочь) — в предыдущих версиях XHR слабоват по возможностям для этого.

Больше всего я бился с декодированием бинарных данных. Последняя версия «Оперы» поддерживает тип ответа arraybuffer (а вот blob не поддерживается), но если его выбрать, то данные в цикле почему-то перестают накапливаться, вероятно какой-то баг браузера. Я долго мучался, прежде чем придумал следующий хак — если сделать overrideMimeType(’image/jpeg’), то данные приходят в двухбайтовой кодировке UCS-2, которую несложно декодировать.

Кстати, быстрее всего у меня получилось декодировать не циклом, а регулярными выражениями, дальше тормозит уже не мой код, а способ получения отдельного кадра — каждый раз я соединяюсь с сервером заново. Можно бы накапливать кадры пачкой и потом их сбрасывать, но у меня нет задачи смотреть трансляции футбола, у меня дом, который не бегает и полученной скорости мне вполне хватает (где-то 2-3 кадра в секунду).

В общем, получился довольно небольшой код — 94 строки.

Посмотреть код.

// ==UserScript==
// @name        MJPEG+Opera - Add to Opera MJPEG support
// @author      Evgeny Stepanischev aka Bolk
// @version     1.00
// @namespace   /files/opera-mjpeg.js
// @modified    2011-10-09
// @include     *
// ==/UserScript==

opera.addEventListener('BeforeEvent.DOMContentLoaded', function() {
    if (opera.version() < 12) return;

    var BolkMJpeg = function (url, img) {
        var req = new XMLHttpRequest(); 
        img.src = 'about:blank'; // Сброс картинки, чтобы прекратить передачу

        // Асинхронно забираем кадр при помощи XHR, нам придёт видеопоток
        var start = function (url) {
            req.open("GET", url, true);
             // Свежепридуманный хитрый хак, позволяет получить данные в UCS-2
            req.overrideMimeType('image/jpeg');
            req.send(null);
        }

        // Хитрыми путями восстанавливаем порядок байт в reponse, там сейчас UCS-2,
        // получаем просто последовательность байт
        var reqbytes = function (n, l) {
            return escape(req.response.substr(n, l)).
                replace(/([^%]|%[^u].|%u....)/g, '<$1>').
                replace(/<%u(..)(..)>/g, '%$2%$1').
                replace(/<%(..)>/g, '%$1%00').
                replace(/<(.)>/g, '$1%00');
        }

        // заменяем картинку на бинарные данные
        var draw = function (bin) {
            img.src = 'data:image/jpeg,' + bin;
        }

        // На изменение состояние входных данных собираем по частям JPEG
        req.onreadystatechange = function () {
            // состояние «3» — данные пошли, но ещё не кончились
            if (req.readyState === 3) {
                var hnd = setInterval(function () {
                    // по заголовку получаем размер данных
                    var header = unescape(reqbytes(0, 300)).toLowerCase();

                    var idx = header.indexOf('content-length:');

                    if (idx > -1) {
                        // 15 — длина заголовка
                        var len = parseInt(header.substr(idx + 15), 10);
                        var headend = header.indexOf("\r\n\r\n");

                        // дожидаемся конца заголовка
                        if (headend > -1) {
                            var resp = reqbytes(0, len);
                            // умножаем всё на три, так как reqbytes даёт esc-последовательность
                            // вида %XX%XX…
                            len *= 3;
                            headend = (headend + 4) * 3;

                            // если собрали весь JPEG, обрываем соединение
                            // и начинаем новый цикл. Мы могли бы собирать кадры и дальше, но
                            // это съест всю память со временем
                            if (resp.length >= len + headend) {
                                req.abort();
                                resp = resp.substr(headend, len);

                                draw(resp);
                                clearInterval(hnd);

                                start(url);
                            }
                        }
                    }
                }, 100);
            }
        }

        start(url);
    };

    // ищем картинку, у которой расширение mjpg
    var imgs = document.getElementsByTagName('IMG');
    for (var i = 0, len = imgs.length; i<len; i++) {
        if (/\.mjpg$/.test(imgs[i].src)) {
            BolkMJpeg(imgs[i].src, imgs[i]);
            break;
        }
    }
}, true);

Если вы не в курсе как устанавливаются скрипты userjs, то это очень просто. В интернете есть много инструкций на эту тему. Только напоминаю, что «Опера» нужна двенадцатая, причём в сборке за седьмое октября userjs сломаны.

7 комментариев

IEWebGL

Микрософт пока не собирается реализовывать WebGL в своём браузере, так как считает, что технология не устоялась (и даже называла её небезопасной). Для тех, кто почему-либо хочет пользоваться IE, но хочет попробовать WebGL, есть специальный плагин IEWebGL, недавно вышел релиз.

Плагин показывает отличную производительность (до 100 fps под IE9 и 10PP, до 60 — под IE8), работает начиная с IE6, не требует OpenGL и менять в HTML или JavaScript сайта так же ничего не требуется.

7 комментариев

Компания «Нетскейп» была первой в деле серверного Джаваскрипта

Чувствую себя археологом. После утренних изысканий за тарелкой пельменей, мне удалось установить точную дату релиза (уже добавил в «Википедию») Netscape Enterprise Server 2.0 — 5 марта 1996 года. Именно в этом сервере впервые появился серверный Джаваскрипт.

Вторая (по всей видимости) реализация была у Микрософт — в «Википедии» написано, что версия JScript для IIS стала доступна в январе 1997 года.

Кстати, публичная бета ASP появилась много раньше — в октябре 1996, как апгрейд для IIS 2.0. Но сайты можно было разрабатывать только на VBScript.

Так что в этом году исполнилось 15 лет серверному Джаваскрипту.

10 комментариев

Хочу развеять миф про серверный Джаваскрипт

С появлением фреймворка Node.js сервервый Джаваскрипт стал широко рекламироваться, но даже ребята, слышавшие об ASP, почему-то подумали, что Node.JS — это первая технология, использующая серверный Джаваскрипт.

Это не так. Например, в микрософтском ASP можно было использовать два языка — JScript и VBScript, в моей практике была пара-тройка сайтов на первом и один на втором. Но существовали профессиональные ASP-программисты, которые клепали их со скоростью пулемёта.

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

А ещё раньше я читал о сервере от «Нетскейп», который тоже содержал Джаваскрипт в качестве языка серверного программирования. Сравнивая давность своих воспоминаний, я прихожу к выкоду, что «Нетскейп» был первым на этот поприще. К сожалению, я не смог навскидку найти информации об этом сервере, но когда я только-только вышел в интернет (1997 год), информация об этом сервере уже была.

18 комментариев

JavaScript GeoAPI и timeout

Если вы пользовались каким-либо навигатором, то знаете, что первый запрос на определение местоположения, как правило, очень долгий. С другой стороны, возможно устройство находится в таких условиях (метро, например), когда координаты определить не удаётся.

Коллега из «Яндекса» провёл исследование — попросил всех знакомых зайти на специальную страницу, где использовалось Geolocation API. Выводы любопытные:

Среднее время работы геолокации 8 секунд. Максимальное 24 сек. Но в 15 секунд уложились все устройства, кроме одного.

C первой попытки с тестом справилось 18 из 21 устройства.

Всегда стоит использовать геоопредление высокой точности, так как геоопределение низкой точности не работает на Андроидах без включенной геолокации по wifi сетям. Щупанье чужих телефонов показало что как правило люди ее не включают.

3 комментария

Добавление поддержки APNG в браузеры, которые его не поддерживают

В ЖЖ нашлась библиотека добавляющая поддержку APNG в браузеры, которые его не поддерживают — то есть в «Хром», «Сафари» и «ИЕ». Принцип простой (но повозиться автору пришлось много) — файл вытягивается через AJAX, парсится на кадры, кадры последовательно отображаются в CANVAS.

7 комментариев

Проверка типа соединения в «Андроидах»

Оказывается в «Андроидах», начиная с версии 2.2 есть специальное свойство у navigator.connection, которое позволяет узнать тип соединения: «navigator.connection.type». Объект «navigator.connection» содержит так же несколько констант, с самодокументируемыми названиями, которые содержат все возможные значения типа соединения: UNKNOWN, ETHERNET, WIFI, CELL_2G, CELL_3G.

with (navigator.connection)
if (type == CELL_2G || type == CELL_3G) {
    // медленное и/или платное
}

На последней, на данный момент прошивке iOS на iPhone 4, увы, это свойство не поддерживается.

15 комментариев

location.reload() ≠ location.href=location.href

Увидел у Никиты Васильева во внутренних бложиках, что вызов «location.reload()» не эквивалентен присваиванию location.href в себя же. Оказывается, последний перезагружает страницу из кеша во всех браузерах, кроме «Оперы».

6 комментариев

Рабочая задача: карта + объекты

Ничего экстраординарного, просто задача по работе попалась.

Дана карта местности (картинка), на неё нанесено большое количество объектов, несколько тысяч. В каждый объект можно тыцнуть мышкой, карта большая, её надо позволять «таскать» мышкой.

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

У меня была гипотеза, что если нарисовать всё это дело на большущем CANVAS, выходящем за пределы экрана и позволить его таскать мышкой, то тормозит не будет.

В общем-то, похоже, подтвердилось. Я взял CANVAS 2000×2000 точек, накидал на него 4000 квадратов случайного цвета и в случайных координатах, CANVAS положил в DIV, растянутый 100% на 100% и скрыл у него скроллеры.

Муляж карты (38.97КиБ)

При клике происходит поиск объекта по списку (безо всякой оптимизации пока), если объект найден, то выводит информацию по цвету, если зажать кнопку мыши, срабатывает флаг «пользователь желает тащить», если пошевелить в этот момент мышью, координаты CANVAS будут меняться вслед за мышью.

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

35 комментариев

JS CSS в одном файле у меня на сайте

Я как-то писал о технике, которая позволяет совместить JS и CSS в одном файле, чтобы ускорить загрузку сайта. Написал два месяца назад, а руки реализовать то же на моём сайте дошли только сегодня ночью.

Особых трудностей не встретил, только убрал все комментарии из файлов с JavaScript руками и добавил удаление BOM из файлов, браузеры как-то нервно реагируют на них, встретив их не в начале файла, а мой редактор кода зачем-то их вставляет.

Схема, в общем-то, не изменилась — если файл .cssjs есть на диске, отдаём его средствами веб-сервера, если нет, собираем необходимое, кладём на диск и отдаём браузеру.

Вот ещё тонкость, обязательно нужно добавить новый mime-тип:

AddType */* .cssjs

Я-то думал, что он нужен только для FF 2.xx, ничего подобного, последние «Опера» и FF так же отказались работать без этого заголовка.

Оказалось, что последний «Хром» ни в какую не хочет рендерить CSS с таким типом, пришлось заменить на «text/css,*/*» на «text/css», предыдущий вариант не работал в IE9.

Надо посмотреть как отнесётся к этому второй FF, а пока, если у вас «Хром» или IE9 и показывает неправильно, обновите, пожалуйста, страницу, чтобы она не из кеша взялась, всё должно нормально отрендериться.

Посмотрим «в поле» насколько это стабильно работает.

33 комментария

Черновики у меня в комментариях

Мне тут хочется пару проектов на PHP доделать, а я что-то давненько, порядка полугода, на нём не программировал вообще. Всё больше Пайтон и Google Go. Поэтому я взялся внести несколько изменений в движок своего сайта, которые давно руки чесались сделать.

В том числе сделал то, что давно просили — черновики в комментариях. Работает только в современных браузерах — IE 8.0+, FF 3.5+, Safari 4.0+, Chrome 4.0+, Opera 10.5+, а так же iOS 2.0+ и Андроид 2.0+. Только такой список, так как сохранение происходит в localStorage.

localStorage — это клиентская база данных «ключ и значение», которые есть сейчас уже во всех распространённых браузерах. Преимущество хранения черновиков именно в ней — черновик сохраняется вне зависимости от того рухнул сервер или нет, ведь localStorage сохраняет данные на диске клиента.

Кстати, я делал библиотеку эмуляции localStorage для IE 6 и 7 (используется behavior userData), но у себя использовать не буду, ни к чему поощрять некрофилию.

Работает всё следующим образом (детально можно посмотреть в исходниках любой страницы с комментариями): когда пользователь делает паузу в наборе комментария, мой JS вызывает localStorage.setItem, где ключём является URL страницы плюс имя поля (у меня их три — имя пользователя, сам комментарий и кому этот комментарий был адресова).

После отправки комментария в теле полученной страницы приходят вызовы localStorage.removeItem, которые удаляют сохранённые значения.

Наконец, если страница была загружена не в результате отправки комментария, вызываются localStorage.getItem, чтобы получить сохранённые значения и подставить их в соответствующие поля.

Вообще-то к localStorage можно обращаться как к массиву, но моя библиотека эмуляции localStorage этот интерфейс не эмулирует. На случай, если я сменю гнев на милость и захочу поддержать IE 6 и 7, я решил пользоваться явными вызовами методов.

Код, с использованием JQuery, получился вот такой:

// globalStorage — это для FF 2.0, но я не уверен, что это правда работает
var storage = window.localStorage || window.globalStorage && window.globalStorage[location.hostname] || null;

if (storage) {
    var savedraft = function s() {
         if (s.hnd) {
             clearTimeout(s.hnd);
         }
        	
         var o = $(this);
        	
         s.hnd = setTimeout(function() {
             try { // если не хватит места, будет исключение
                 var prefix = location.pathname + '#';
                 storage.setItem(prefix + o.attr('name'), o.val());
                 storage.setItem(prefix + '?', +new Date()); // сохраняю время, возможно в будущем буду подчищать старые черновики
             } catch (o) {}
        }, 500);
    }
    
    // каждый элемент, который надо сохранять, имеет класс draft	
    $.each($('.draft'), function(_, value) {
        var o = $(value),
              key = location.pathname + '#' + o.attr('name'),
              v = storage.getItem(key);

    if (savedsuccessfully) { // внешний флаг, что черновик больше не нужен — всё сохранено на сервере		
                  storage.removeItem(key);
              } else if (v !== null) {
                  o.val(v);
              }
              o.keypress(savedraft).change(savedraft);
        })
    }

localStorage не резиновое, у разных браузеров значения различаются, но в целом, там около 5МБ на домен, да и диск пользователя забивать жалко. Поэтому я сохраняю время записи, возможно когда-нибудь буду вычищать очень уж старые черновики.

21 комментарий

Tanks JS1K

Кстати о JavaScript. Может быть кто-то из читатей помнит, под Новый год мы с LO развлекались тем, что уменьшали мою игру про танк, воюющий с ракетами, чтобы она занимала как можно меньше места.

Не так давно мне почему-то вспомнилась эта игрушка и я посмотрел нельзя ли там что-то ещё уменьшить, свежим взглядом так сказать. Оказалось можно. У меня была старая идея переделать координатную адресацию, но я её забросил, лениво. Зато придумал кое-что новенькое, что пригодится не только в этой игрушке.

Мне очень хотелось сократить названия методов канваса, очень уж они длинные — putImageData, createImageData и так далее. Для игры, занимающей (сейчас) 713 байт это расточительно. На эту тему в голову идея и пришла.

Идея заключается в том, чтобы пройтись по списку методов и присвоить объекту их же, но под другими, короткими именами. Самый простой способ, до которого я додумался — это взять пятую букву названия метода (правда, кажется a[i] для строк не везде будет работать, вариант с charAt занимает на 7 байт больше).

Получился такой код:

for(i in a)a[i[V=4]]=a[i]

Переменная «V» со значением «четыре» используется дальше как счётчик оставшихся жизней, в переменной «a» ссылка на объект контекста канваса. В итоге, если мне надо вызывать метод «a.putImageData», я могу вызвать его как «a.m», вместо «a.createImageData» можно вызывать «a.t» и так далее (отчёт с нуля начинается).

Чем больше таких вызовов, тем больше экономия. Единственное, свойства (например «fillStyle») так сократить не получится.

2 комментария

DOOM на JavaScript

Ребята из Mozilla портировали DOOM на JavaScript. Скомпилировав DOOM в байт-код виртуальной машины LLVM при помощи llvm-gcc они перевели получившееся в JavaScript при помощи Emscripted (это транслятор байт-кода LLVM в JavaScript).

Запускается не во всех браузерах (последний «Хром» просто завис), но в «Опере» и «Файрфоксе» у меня запустилось. Для запуска необходима поддержка Canvas. На скриншоте видно, что FPS приличный, на моём «Маке» в FireFox4 держится на уровне 20—35.

DOOM в браузере (65.10КиБ)

Чит-коды работают! Кстати, кроме IDDQD и IDKFA есть ещё много других.

6 комментариев

CSS и JS в одном файле

Раз уж я затронул тему снижения числа коннектов к серверу при загрузке сайта, то было бы уместо вспомнить незаслуженно забытую технику — объединение JS и CSS в одном файле.

Насколько я знаю, первым эту технику исследовал некий ShivaP в статье «Combine CSS with JS and make it into a single download!» ещё четыре года назад.

Идея такова: в файле специальными ухищрениями помещается и код JavaScript, и код CSS, файл подключается два раза — один раз через тег SCRIPT, второй раз через тег LINK (как CSS), браузер грузит файл один раз, а второй раз берёт его уже из кеша.

Автор проверил свою идею на двух браузерах — IE и FireFox, на том и остановился.

Я проверил эту идею на гораздо бо́льшем числе браузеров (обнаружил проблемы на некоторых «Хромах» и исправил), а так же немного её видоизменил, разрешив использовать многострочный CSS без особых неудобств. Последнее нужно персонально мне, так как у меня нет возможности прогонять свой CSS через оптимизаторы и вытягивать таким образом код CSS в одну строку, а я хочу использовать эту технику у себя на сайте.

У техники есть ограничение, которое не портит много крови, но о нём необходимо знать — в тексте CSS и JS не должна встречаться конструкция «*/». Если в CSS достаточно просто выкусить все комментарии («*/» там, можно считать, нигде больше встретиться не может), то в JS за этим придётся следить, так как такая комбинация вполне может встретиться, в частности, в регулярных выражениях. Например: var regexp = /smth*/g;

В таких случаях звёздочку необходимо прятать (например, заменять её в регулярных выражениях на \x2A или разбивать строку: «smth*» + «/»).

Как ShivaP, первый исследователь этой техники, решил проблему размещения JS и CSS в одном файле можно почитать в оринальной статье, я же поступил несколько иначе, но сходным образом. Конструкции размещаются так, чтобы JS оказывался в комментариях CSS и наоборот. Я похожими вещами уже занимался неоднократно.

Мой код выглядит следующим образом (я поставил рядом две картинки, чтобы можно было сравнить как один и тот же код воспринимается интерпретатором JavaScript и парсером CSS):

JS и CSS (15.58КиБ)

Как видите, принцип довольно несложен.

Теперь несколько слов о том как это всё подключается:

<link type="text/css" rel="stylesheet" href="core.jscss" />

<script type="text/javascript" src="core.jscss#2"></script>

Обратите внимание на «#2» после core.jscss, этот хак позволяет обойти странное поведение некоторых версий «Хрома» — без него браузер не делал попыток подгрузить файл ещё раз и JavaScript не выполнялся.

Немаловажно так же, что заголовок «content-type» должен быть выставлен в «*/*», чтобы избежать проблем с FireFox 2.

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

Читать дальше

<?
ob_start();
$dir     = 'media/';
$expires = 10 * 365 * 24 * 60 * 60; // 10 years

$files = explode(',', str_replace('..', '', $_SERVER['QUERY_STRING']));
$css = $js = array();

foreach ($files as $file) {
   switch (pathinfo($file,  PATHINFO_EXTENSION)) {
       case 'css':
          $css[] = $dir . $file;
          break;

       case 'js':
          $js[] = $dir . $file;
          break;
    }
}

$css = @array_map('file_get_contents', array_unique($css));
$js  = @array_map('file_get_contents', array_unique($js));

$css = implode($css);
$js  = implode($js, ';');

// removes /* comments */ from css
$css = preg_replace('!/\*.*?\*/!s', '', $css);

// removes only first /* comment */ from js
$js  = preg_replace('!^/\*.*?\*/!s', '', $js);

echo <<<JSCSS
// /*
$js
"*/{"/*"}
$css
*/
JSCSS;

$len  = ob_get_length();
$txt  = ob_get_clean();
$etag = '"' . sha1($etag) . '"';

header("Content-type: */*; charset=utf-8");
header("Expires: " . gmdate('D, d M Y H:i:s \G\M\T', $_SERVER['REQUEST_TIME'] + $expires));
header("Cache-control: max-age=$expires, public");
header("Content-length: " . $len);
header('ETag: ' . $etag);

if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
    list ($hash) = explode(';', $_SERVER['HTTP_IF_NONE_MATCH']);

    if ($hash == $etag) {
        header('HTTP/1.1 304 Not modified, thanx for yr question');
        exit;
    }
}

echo $txt;

Формат вызова простой: скрипту через запятую перечисляются файлы, которые нужно объединить и подключить (файлы берутся из папки «media», см. переменную «$dir»):

/util/js+css.php?core.js,core.css,another.css,another.js

35 комментариев

← 1 2 3 4 5 6 →