Как использовать простой фильтр SVG в Safari с приемлемой производительностью и без сбоев? - PullRequest
0 голосов
/ 05 ноября 2018

Отказ от ответственности: Этот пост - половина вопроса и половина отчета о моих экспериментах при попытке найти решение.

Задача: Простой 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 - это вечеринка Pooper . Если вы увеличиваете или уменьшаете масштаб в Chrome, при некоторых уровнях масштабирования отображается растр. Вот как это может выглядеть (правильно будет один большой квадрат):

enter image description here

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 viewBox (которая является маленькой областью после уменьшения масштаба), а затем снова увеличивает эту область. И это кажется быстрым.

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

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

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

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

1 Ответ

0 голосов
/ 05 ноября 2018

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

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

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>
...