Как использовать простой фильтр SVG в Safari с приемлемой производительностью и без сбоев?

Отказ от ответственности: этот пост — наполовину вопрос, наполовину отчет о моих экспериментах по поиску решения.

Задача: простой 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>

Фрагмент на CodePen

 

Возможные решения: что я придумал на данный момент

Я пробовал несколько подходов, но, как вы увидите. Как вы увидите, некоторые из них работают для 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>

Фрагмент на CodePen

Это на самом деле значительно повышает производительность и не происходит сбоев!

Печальная новость заключается в том, что filterRes было удалено из стандарта SVG в версии 2.0. WebKit очень быстро адаптировал эту часть SVG 2.0 и решил удалить ее в следующей версии. Если мы будем полагаться на filterRes, та же проблема скоро появится снова.

 

3. Отфильтруйте небольшой прямоугольник, затем увеличьте масштаб

tl;dr не работает

Приведенный выше подход показывает, что применение фильтра к небольшим областям может значительно повысить производительность. Там мы применяем фильтр к маленькому прямоугольнику, а затем масштабируем результат до нужного размера. Для масштабирования мы используем атрибут transform. Если мы установим прямоугольник на 1/64 желаемого размера, мы затем масштабируем его с помощью transform="scale(64)", чтобы получить окончательный размер 1024.

Фрагмент на CodePen

К сожалению, это вообще не работает. Производительность не изменилась, и он по-прежнему падает на мобильном Safari. Интересно, что это нельзя отнести только к фильтру или преобразованию. Только в сочетании это становится медленным и вылетает.

 

4 Отфильтруйте небольшой прямоугольник, затем используйте feTile, чтобы заполнить целевой прямоугольник.

tl;dr Немного работает, но есть артефакты в Chrome

Этот подход аналогичен предыдущему. Сначала мы применяем фильтр к маленькому прямоугольнику, а затем используем второй фильтр с feTile, чтобы заполнить целевой прямоугольник.

Фрагмент на CodePen

Это работает на мобильном 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>

Фрагмент на CodePen

Я думаю, что здесь происходит то, что фильтр сначала уменьшает исходную область фильтра, затем обрабатывает часть, которая будет видна в поле просмотра SVG (это крошечная область после уменьшения масштаба), а затем снова масштабирует эту область. И это, кажется, быстро.

К сожалению, это хак, и мы пока не знаем, как будущие версии браузеров будут обрабатывать SVG с такими большими значениями. Кроме того, это вызывает проблемы в Internet Explorer, где область прокрутки документа становится ОГРОМНОЙ (вероятно, потому, что границы фильтра рассчитываются по размеру), что делает прокрутку практически невозможной. И если вы выберете слишком маленькие значения, Internet Explorer фактически отобразит прямоугольник в размере границ фильтра.

 

Каково решение?

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

Можете ли вы придумать какой-либо другой подход, который я мог бы попробовать? Я не могу поверить, что кросс-браузерное использование фильтров SVG спустя пятнадцать лет после выпуска спецификации SVG 1.1 — это такое приключение.


person Waruyama    schedule 04.11.2018    source источник
comment
Вариантом варианта 1 будет загрузить исходный код в webkit и исправить ошибку самостоятельно или заплатить кому-то другому за это.   -  person Robert Longson    schedule 05.11.2018
comment
Да, Apple наверняка обрадуется такому варианту. Но кто позаботится о том, чтобы изменения попали в Safari и когда это произойдет?   -  person Waruyama    schedule 05.11.2018
comment
Вам нужно будет изучить график выпуска Safari и т. Д. Я действительно знаком только с Firefox.   -  person Robert Longson    schedule 05.11.2018
comment
@RobertLongson Кстати, Firefox даже не моргнул во всех странных тестах, которые я проводил в последние дни. Спасибо за вашу работу.   -  person Waruyama    schedule 05.11.2018
comment
Случайные идеи: вы можете попробовать добавить обязательный атрибут type=matrix и посмотреть, улучшит ли это ситуацию. Добавьте x=0%, y=0%, width=100% и height=100% на случай, если это испортит расчет размера фильтра. Также убедитесь, что в строке значений нет начальных пробелов и что она не содержит разрывов строк. Окончательная идея — переместить фильтр в самую внешнюю g-оболочку на случай, если промежуточные преобразования вызывают проблемы.   -  person Michael Mullany    schedule 05.11.2018
comment
Зачем вам нужен фильтр для изменения цвета, просто установите другой цвет для формы напрямую.   -  person Robert Longson    schedule 05.11.2018
comment
@RobertLongson Потому что в реальном сценарии цвет можно свободно установить с помощью CSS, и я хочу, чтобы другие цвета адаптировались автоматически. Это работает очень хорошо, эта ошибка Safari — последнее препятствие.   -  person Waruyama    schedule 05.11.2018


Ответы (1)


Попробуй это:

Поместите все прямоугольники в одну группу и вместо этого примените фильтр к этой группе.

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>
  <g filter="url(#filter-1)">
    <rect id="svg-rect" fill="red" x="0" y="0" width="1024" height="1024" />
  </g>
</svg>

person Paul LeBeau    schedule 04.11.2018
comment
Спасибо, это работает в примерах, но в реальном сценарии все фильтры разные. Я решил использовать этот же фильтр в примерах для упрощения. Кроме того, проблемы возникают, если вы отображаете несколько SVG, каждый из которых содержит только один отфильтрованный прямоугольник. - person Waruyama; 05.11.2018