Отказ от ответственности: этот пост — наполовину вопрос, наполовину отчет о моих экспериментах по поиску решения.
Задача: простой SVG-фильтр на монохромном прямоугольнике
Использовать фильтр для изменения или изменения цвета монохромной фигуры в SVG довольно просто. Вот как это можно сделать:
<svg viewBox="0 0 460 130">
<defs>
<filter id="filter1">
<feColorMatrix values="0.5 0 0 0 0 0 0.5 0 0 0 0 0 0.5 0 0 0 0 0 1 0" />
</filter>
</defs>
<g transform="translate(20, 0)">
<text x="0" y="35">Original</text>
<rect x="0" y="50" width="200" height="80" fill="red" />
</g>
<g transform="translate(240, 0)">
<text x="0" y="35">Filtered</text>
<rect x="0" y="50" width="200" height="80" fill="red" filter="url(#filter1)" />
</g>
</svg>
Проблема: он работает медленно в Safari и вылетает в Safari Mobile
Небольшая проблема заключается в том, что Safari работает очень медленно. Во фрагменте ниже новый отфильтрованный прямоугольник добавляется в SVG каждые полсекунды. Это в конечном итоге приведет к сбою на мобильном устройстве Safari после 10-80 циклов, в зависимости от вашего устройства. Кроме того, это невыносимо медленно. Для сравнения, на Android-телефоне начального уровня с Chrome это работает почти вечно с 50 кадрами в секунду в начале.
var rectCount = 1;
var svgRectElem = document.getElementById('svg-rect');
var rectCountElem = document.getElementById('rect-count');
var fpsElem = document.getElementById('fps');
(function insertRect() {
window.requestAnimationFrame(function() {
var start = new Date().getTime();
var clonedSvgRectElem = svgRectElem.cloneNode();
// insert cloned rect SVG element
svgRectElem.parentNode.appendChild(clonedSvgRectElem);
rectCountElem.textContent = ++rectCount;
window.requestAnimationFrame(function() {
var fps = Math.round(100000 / (new Date().getTime() - start)) / 100;
fpsElem.textContent = fps;
window.setTimeout(insertRect, 500);
}, 100);
});
})();
<p>SVG Rect Count: <span id="rect-count">1</span></p>
<p>fps: <span id="fps"></span></p>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="512" height="512" viewBox="0,0,1024,1024">
<defs>
<filter id="filter-1">
<feColorMatrix values="0.5 0 0 0 0 0 0.5 0 0 0 0 0 0.5 0 0 0 0 0 1 0" />
</filter>
</defs>
<rect id="svg-rect" fill="red" filter="url(#filter-1)" x="0" y="0" width="1024" height="1024" />
</svg>
Возможные решения: что я придумал на данный момент
Я пробовал несколько подходов, но, как вы увидите. Как вы увидите, некоторые из них работают для Safari, и у каждого есть свои особенности.
1. Дождитесь исправления от команды WebKit
tl;dr Может быть, работает, может быть, нет
Это был бы самый удобный вариант, но если учесть, что SVG-фильтры были введены 15 лет назад, я бы не стал затаить дыхание, чтобы эта проблема была исправлена в ближайшее время. И да, я написал отчет об ошибке.
2. Используйте filterRes
tl;dr работает, но только до следующего выпуска Safari
В SVG 1.1 определен атрибут filterRes
. Это определяет разрешение фильтра. Значение 1 означает, что источник для фильтра будет рассматриваться как один пиксель. Таким образом, вместо того, чтобы работать со всеми пикселями, фильтр должен применяться только к одному пикселю. Поскольку мы используем прямоугольник одного цвета в качестве входного изображения, обработка всего изображения как одного пикселя в нашем случае допустима.
В нашем примере фильтр будет выглядеть так:
<filter id="filter1" filterRes="1">
<feColorMatrix values="0.5 0 0 0 0 0 0.5 0 0 0 0 0 0.5 0 0 0 0 0 1 0" />
</filter>
Это на самом деле значительно повышает производительность и не происходит сбоев!
Печальная новость заключается в том, что filterRes
было удалено из стандарта SVG в версии 2.0. WebKit очень быстро адаптировал эту часть SVG 2.0 и решил удалить ее в следующей версии. Если мы будем полагаться на filterRes
, та же проблема скоро появится снова.
3. Отфильтруйте небольшой прямоугольник, затем увеличьте масштаб
tl;dr не работает
Приведенный выше подход показывает, что применение фильтра к небольшим областям может значительно повысить производительность. Там мы применяем фильтр к маленькому прямоугольнику, а затем масштабируем результат до нужного размера. Для масштабирования мы используем атрибут transform
. Если мы установим прямоугольник на 1/64 желаемого размера, мы затем масштабируем его с помощью transform="scale(64)"
, чтобы получить окончательный размер 1024.
К сожалению, это вообще не работает. Производительность не изменилась, и он по-прежнему падает на мобильном Safari. Интересно, что это нельзя отнести только к фильтру или преобразованию. Только в сочетании это становится медленным и вылетает.
4 Отфильтруйте небольшой прямоугольник, затем используйте feTile
, чтобы заполнить целевой прямоугольник.
tl;dr Немного работает, но есть артефакты в Chrome
Этот подход аналогичен предыдущему. Сначала мы применяем фильтр к маленькому прямоугольнику, а затем используем второй фильтр с feTile
, чтобы заполнить целевой прямоугольник.
Это работает на мобильном Safari. Не так хорошо, как filterRes
, но улучшение есть.
Но при таком подходе Chrome — лучший выбор. Если вы увеличиваете и уменьшаете масштаб в Chrome, на некоторых уровнях масштабирования отображается растр. Вот как это может выглядеть (правильнее будет один большой квадрат):
5 Используйте огромные значения ширины и высоты фильтра
tl;dr Работает, но является взломом и вызывает проблемы в Internet Explorer
Это очень контринтуитивный подход. Установка ширины и высоты области фильтра на очень большие значения должна сделать фильтр намного медленнее, потому что придется обрабатывать миллионы пикселей. Но на самом деле все обстоит наоборот. Фильтр значительно ускоряется в WebKit, и сбоев не происходит.
Вот как это реализовать в нашем примере:
<filter id="filter-1" x="0" y="0" width="102400" height="102400" filterUnits="userSpaceOnUse">
<feColorMatrix values="0.5 0 0 0 0 0 0.5 0 0 0 0 0 0.5 0 0 0 0 0 1 0" />
</filter>
Я думаю, что здесь происходит то, что фильтр сначала уменьшает исходную область фильтра, затем обрабатывает часть, которая будет видна в поле просмотра SVG (это крошечная область после уменьшения масштаба), а затем снова масштабирует эту область. И это, кажется, быстро.
К сожалению, это хак, и мы пока не знаем, как будущие версии браузеров будут обрабатывать SVG с такими большими значениями. Кроме того, это вызывает проблемы в Internet Explorer, где область прокрутки документа становится ОГРОМНОЙ (вероятно, потому, что границы фильтра рассчитываются по размеру), что делает прокрутку практически невозможной. И если вы выберете слишком маленькие значения, Internet Explorer фактически отобразит прямоугольник в размере границ фильтра.
Каково решение?
На самом деле я не знаю. Существует простое решение с использованием filterRes
, но срок его действия скоро истечет (оно уже не поддерживается в предварительной версии технологии WebKit), так что это не вариант. Все другие подходы вызывают проблемы в других браузерах.
Можете ли вы придумать какой-либо другой подход, который я мог бы попробовать? Я не могу поверить, что кросс-браузерное использование фильтров SVG спустя пятнадцать лет после выпуска спецификации SVG 1.1 — это такое приключение.