SVG и медиавыражения

Одно из достоинств SVG, это то, что можно использовать медиавыражения для добавления изображениям отзывчивости :

<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
  <style>
    circle {
      fill: green;
    }
    @media (min-width: 100px) {
      circle {
        fill: blue;
      }
    }
  </style>
  <circle cx="50" cy="50" r="50"/>
</svg>

Но когда именно кружок должен стать синим? Согласно спецификации min-width должен соответствовать ширине вьюпорта, но…

Какого вьюпорта?

<img src="circle.svg" width="50" height="50">
<img src="circle.svg" width="100" height="100">
<iframe src="circle.svg" width="50" height="50"></iframe>
<svg width="50" height="50">
  …как указано выше…
</svg>

Что из перечисленного выше нарисует (потенциально обрезанный) синий круг в HTML-документе? Как и чей вьюпорт следует использовать? Что нужно брать в расчет:

Вот демо того что расписано выше:

Посмотрите на CodePen.

В большинстве браузеров…

Для <img>, SVG масштабируется до размеров элемента, а также вьюпорт для SVG в CSS до размеров <img>. Таким образом, первый <img> имеет ширину вьюпорта 50 пикселей, а второй — 100. Это означает, что только ко второму <img> применится «синее» медиавыражение.

Для <iframe>, размером вьюпорта SVG является вьюпорт фрейм-документа. Таким образом, в приведенном выше примере, ширина вьюпорта в 50 пикселей CSS, потому что это ширина фрейма.

Что касается <svg>-разметки помещенной непосредственно в html, то SVG не имеет своего собственного вьюпорта, он становится частью родительского документа. Это означает, что <style> находится в собственности родительского документа - он не ограничен областью видимости в SVG. Это привлекло мое внимание, когда я впервые использовал встроенный SVG, но это имеет смысл и хорошо определено в спецификации.

Как говорит лисичка?

У Firefox своя точка зрения. Он ведет себя так, как описано выше, за одним исключением:

Для <img>, вьюпорт это отрендеренный размер элемента в физических пикселях, а это означает изменение отображения в зависимости от плотности пикселей. Первое изображение в примере будет зеленым на экранах с плотностью 1х, но станет синего цвета на экранах с плотностью пикселей 2x и выше. Это проблема, так как некоторые ноутбуки и большинство телефонов имеют плотность пикселей больше чем 1.

Это выглядит как ошибка, тем более, что Firefox не применяет ту же логику к iframe, но нужно заметить, что спецификация не покрывает случаи когда SVG отображается в <img>, непонятно как оно должно масштабироваться, не говоря уже, о том, как в этом случае должны вести себя медиавыражения.

Я создал ишью для спецификации, будем надеяться, что проблема будет решена.

Но все становится гораздо сложнее, когда вы начинаете…

Рисовать SVG в canvas

Можно рисовать <img> в <canvas>, вот так:

canvas2dContext.drawImage(img, x, y, width, height);

Но когда круг должен стать синим? На этот раз вьюпортов ещё больше. Какие из них следует принимать во внимание:

Как вы думаете? Опять же, спецификация неясна, и на этот раз каждый браузер пошел по своему собственному пути. Давайте ознакомимся:

Посмотрите на CodePen.

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

Chrome

Chrome отталкивается от атрибутов width и height, указанных в SVG-документе. Это означает, что если в нём width="50", сработает медиавыражение для вьюпорта шириной 50px. Если вы хотите отобразить его так, что бы сработало медиавыражение для вьюпорта шириной 100px, незадача. Независимо от того, какой размер вы используйте в canvas, он будет рисовать, используя медиавыражение для 50px ширины.

Однако, если в SVG задан атрибут viewBox, а не фиксированная ширина, Chrome использует плотность пикселей <canvas> в качестве ширины вьюпорта. Можно утверждать, что это напоминает то, как работает SVG, помещенный непосредственно в html, где окно вьюпорта это все окно браузера, но переключение поведения, основанное на viewBox это очень странно.

Chrome выигрывает награду «странные-штанишки» за непоследовательное поведение.

Safari

Как и Chrome, Safari использует размер, указанный в документе SVG, с теми же недостатками. Но если SVG использует viewBox, а не фиксированную ширину, он вычисляет ширину на основе viewBox, так что SVG с viewBox = “50 50 200 200” будет иметь ширину 150.

Не так странно как в Chrome, но всё равно ограничивает.

Firefox

Firefox использует ширину и высоту, указанную в вызове drawImage, с учетом любых трансформаций. Это означает, что если вы рисуете вашу SVG, так, что размер холста 300 пикселей в ширину, он будет иметь ширину вьюпорта в 300px.

Что своего рода отражает странное поведение <img> — оно основано на отрисовке пикселей. Это означает, что вы получите те же несоответствия плотности, если умножите ширину и высоту вашего холста на devicePixelRatio (и затем уменьшите с помощью CSS), вы должны это сделать, что бы избежать размытости на экранах с высокой плотностью:

Посмотрите на CodePen.

В том, что делает Firefox есть смысл, но это означает, что медиавыражения привязаны к пикселям.

Microsoft Edge

Egde использует размер элемента <img> для определения размеров вьюпорта. Если <img> не имеет размеров (display: none или элемент вне документа), то он возвращается к атрибутам width и height, если их нет, тогда он использует внутренние размеры <img>.

Это означает, что вы можете сделать SVG размером 1000x1000, но изображение <img width= "100">, вьюпорт будет иметь ширину 100px.

На мой взгляд, это идеальный вариант. Это означает, что вы можете активировать медиавыражения и не зависеть от ширины русинка. Это, кроме того, соответствует поведению адаптивных изображений. Когда вы рисуете <img srcset= "…" width="…"> на холсте, все браузеры соглашаются, что изображение должно содержать ресурс из <img>.

Фух!

Я создал ишью с предложением принять поведение Edge и предложил дополнение к createImageBitmap, которое позволит задать вьюпорт из скрипта. Надеюсь, мы сможем добиться большей кроссбраузерности!

Для полноты картины, вот как я собрал данные, а вот полные результаты.