HTML5 Canvas globalCompositeOperation для наложения градиентов, не увеличивающих интенсивность?

В настоящее время я работаю над исправлением heatmap.js, и мне было интересно, знает ли кто-нибудь, можно ли добиться следующего эффекта. с контекстом 2D-рендеринга <canvas>.

  • У меня есть радиальный градиент от черного (альфа 0,5) до прозрачного радиуса 40 пикселей. центр радиального градиента находится в точках x=50, y=50
  • У меня есть еще один радиальный градиент от черного (альфа 0,5) до прозрачного, радиус 40 пикселей. центр радиального градиента находится в точках x=80, y=50

Два градиента перекрываются. Теперь моя проблема: перекрывающиеся области складываются вместе, что приводит к более высокому значению альфа, чем центры радиальных градиентов, и, таким образом, показывает неправильные данные (например, более горячие области на тепловой карте из-за этих дополнений между градиентами)

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

Ожидаемое поведение: самые темные области — это центры градиентов, перекрывающиеся области двух градиентов сливаются, но не складываются.

Увидев, что ни одна из globalCompositeOperations не привела к ожидаемому поведению, я попробовал комбинации этих операций. Способ, которым я думал, что это возможно, был следующим:

  • нарисовать первый градиент
  • использовать составную операцию «назначение-выход»
  • нарисовать второй градиент -> вычесть область перекрытия из первого градиента
  • использовать составную операцию 'source-over'
  • снова нарисовать второй градиент

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

PS: я знаю, что это можно сделать, манипулируя пикселями вручную, но мне было интересно, есть ли для этого более простое, элегантное и быстрое решение.


person Patrick Wied    schedule 08.04.2012    source источник
comment
Перекрывающаяся область двух градиентов сливается, но не суммируется. - Можете ли вы описать слияние попиксельно?   -  person Matt Esch    schedule 12.04.2012


Ответы (4)


Это действительно странно, но делает то, что вы хотите, без участия imageData.

На ум пришло то, что вам нужна точная функциональность, которую имеют сами контуры на холсте, когда вы их обводите. Чтобы процитировать спецификацию:

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

Вы можете узнать больше об этом здесь

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

Единственная проблема заключается в том, что нет возможности сделать размытый путь на холсте. Или почти никак.

Вместо использования самого пути мы можем использовать тень пути, чтобы имитировать размытые круги, подчиняющиеся тем же правилам, что и пути.

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

Но у теней есть свойства shadowOffsetX и shadowOffsetY, которые обычно используются для смещения тени на один или два пикселя, чтобы создать иллюзию источника света.

Но что, если мы нарисуем тени так далеко, что вы их не увидите? Точнее, что, если мы нарисуем пути так далеко, что вы их не увидите, а увидите только тени?

Что ж, это помогает. Вот быстрый результат: ваш исходный код находится сверху, а тени — это второй холст:

введите здесь описание изображения

Это не совсем то, что у вас было раньше с точки зрения градиентов и размера, но оно очень близко, и я уверен, что, повозившись со значениями, вы сможете приблизиться к нему. Пара сообщений console.log подтверждает, что то, что нам нужно, альфа, которая не превышает 124 (из 255), правильно появляется в местах, где раньше было 143 и 134, делая это по-старому.

Скрипка, чтобы увидеть код в действии: http://jsfiddle.net/g54Mz/

Итак, у вас есть это. Получение эффекта объединения двух радиальных градиентов возможно без imageData, если вы используете тени и смещаете фактические пути настолько сильно, что они находятся за пределами экрана.

person Simon Sarris    schedule 15.04.2012
comment
Спасибо за этот ответ, это действительно классная техника. Я быстро взломал реализацию, и, похоже, она работает нормально. :) - person Patrick Wied; 17.04.2012

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

Внедрение этой техники дало вид, который я хотел. И мне понравилось, что это было легко понять. Однако на практике я обнаружил, что рендеринг даже нескольких (~150) теней был намного медленнее по сравнению с моей предыдущей техникой (хотя и непривлекательной) рисования тысяч заполненных прямоугольников.

Поэтому я решил провести небольшой анализ. Я написал базовый JavaScript (модифицированную версию можно увидеть по адресу https://jsfiddle.net/Flatfingers/4vd22rgg/ ) для рисования 2000 копий каждого из пяти различных типов фигур на неперекрывающихся участках холста размером 1250 x 600 с записью прошедшего времени для каждой из этих пяти операций в последних версиях пяти основных настольных браузеров и мобильного Safari. (Извините, настольный Safari. У меня также нет под рукой Android, чтобы проверить.) Затем я попробовал разные комбинации эффектов и записал прошедшее время.

Вот упрощенный пример того, как я рисую два градиента, похожих на затененные закрашенные дуги:

var gradient1 = context.createRadialGradient(75,100,2,75,100,80);
gradient1.addColorStop(0,"yellow");
gradient1.addColorStop(1,"black");

var gradient2 = context.createRadialGradient(125,100,2,125,100,80);
gradient2.addColorStop(0,"blue");
gradient2.addColorStop(1,"black");

context.beginPath();
context.globalCompositeOperation = "lighter";
context.globalAlpha = 0.5;
context.fillStyle = gradient1;
context.fillRect(0,0,200,200);
context.fillStyle = gradient2;
context.fillRect(0,0,200,200);
context.globalAlpha = 1.0;
context.closePath();

ВРЕМЯ

(2000 непересекающихся фигур, наборы globalAlpha, drawImage() используется для градиентов, но не для теней)

IE 11 (64-bit Windows 10)
 Rects     =   4 ms
 Arcs      =  35 ms
 Gradients =  57 ms
 Images    =   8 ms
 Shadows   = 160 ms

Edge (64-bit Windows 10)
 Rects     =   3 ms
 Arcs      =  47 ms
 Gradients =  52 ms
 Images    =   7 ms
 Shadows   = 171 ms

Chrome 48 (64-bit Windows 10)
 Rects     =   4 ms
 Arcs      =  10 ms
 Gradients =   8 ms
 Images    =   8 ms
 Shadows   = 203 ms

Firefox 44 (64-bit Windows 10)
 Rects     =   4 ms
 Arcs      =  21 ms
 Gradients =   7 ms
 Images    =   8 ms
 Shadows   = 468 ms

Opera 34 (64-bit Windows 10)
 Rects     =   4 ms
 Arcs      =   9 ms
 Gradients =   8 ms
 Images    =   8 ms
 Shadows   = 202 ms

Mobile Safari (iPhone5, iOS 9)
 Rects     =  12 ms
 Arcs      =  31 ms
 Gradients =  67 ms
 Images    =  82 ms
 Shadows   =  32 ms

НАБЛЮДЕНИЯ

  1. Среди заполненных фигур заполненные прямоугольники неизменно являются самой быстрой операцией во всех протестированных браузерах и средах.
  2. Заполненные полные дуги (круги) примерно в 10 раз медленнее в IE 11 и Edge, чем заполненные прямоугольники, по сравнению с примерно в 3,5 раза медленнее в других основных браузерах.
  3. Градиенты примерно в 3 раза медленнее прямоугольников в IE 11, Chrome 48 и Opera 34, но в 100 раз медленнее в Firefox 44 (см. отчет об ошибке 728453).
  4. Изображения с помощью drawImage() примерно в 1,5 раза быстрее, чем заполненные прямоугольники во всех настольных браузерах.
  5. Затененные дуги с заливкой работают медленнее всех: примерно в 50 раз медленнее, чем заполненные прямоугольники в IE, Edge, Chrome и Opera, до 100 раз медленнее в Firefox.
  6. Chrome 48 и Opera 34 удивительно быстры во всех категориях форм, кроме затененных заполненных дуг, но они не хуже, чем другие браузеры.
  7. Chrome и Opera аварийно завершают работу, когда функция drawImage() отрисовывает 1000 фигур, где значение shadowOffsetX или shadowOffsetY выходит за пределы физического разрешения экрана.
  8. IE 11 и Edge рисуют дуги и градиенты медленнее, чем другие настольные браузеры.
  9. drawImage() работает медленно в мобильном Safari. На самом деле быстрее рисовать несколько градиентов и затененных дуг, чем много раз рисовать одну копию с помощью drawImage().
  10. Рисование в Firefox чувствительно к предыдущим операциям: рисование теней и градиентов замедляет рисование дуг. Показаны самые быстрые результаты.
  11. Рисование в Mobile Safari чувствительно к предыдущим операциям: тени замедляют градиенты; градиенты и дуги делают функцию drawImage() еще медленнее, чем обычно. Показаны самые быстрые результаты.

АНАЛИЗ

Хотя функция shadowOffset является простым и визуально эффективным способом смешивания фигур, она значительно медленнее, чем все другие методы. Это ограничивает его полезность для приложений, которым нужно рисовать только несколько теней и которым не нужно быстро и многократно рисовать много теней. Кроме того, при ускорении с помощью drawImage() задание для shadowOffsetX или shadowOffsetY значения, превышающего примерно 3000, приводит к зависанию Chrome 48 и Opera 34 почти на минуту, потребляя циклы ЦП, а затем приводит к сбою моего драйвера дисплея nVidia, даже после его обновления. до последней версии. (Поиск Google не нашел отчетов об ошибках для Chromium, описывающих эту ошибку, когда большие shadowOffset и drawImage() используются вместе.)

Для приложений, которые должны смешивать нечеткие формы, наиболее визуально похожий подход к теням — установить для globalCompositeOperation значение «lighter» и использовать drawImage() со значением globalAlpha для повторного рисования предварительно окрашенного радиального градиента или для рисования отдельных градиентов, если они нужны. быть разного цвета. Это не идеальное совпадение для перекрывающихся теней, но оно близко и позволяет избежать попиксельных вычислений. (Однако обратите внимание, что в мобильном Safari прямое рисование затененных дуг на самом деле быстрее, чем градиенты и drawImage().) При установке globalCompositeOperation на «lighter» IE 11 и Edge примерно в 10 раз медленнее рисуют дуги с использованием радиального градиента. по-прежнему быстрее, чем использование затененных дуг во всех основных настольных браузерах, и только в два раза медленнее, чем затененные дуги в мобильном Safari.

ЗАКЛЮЧЕНИЕ

Если вашей единственной целевой платформой является iPad/iPhone, самый быстрый способ получить красиво выглядящие смешанные формы — это затененные закрашенные дуги. В противном случае, самый быстрый метод с сопоставимым внешним видом, который я нашел до сих пор, который работает во всех основных настольных браузерах, — это рисование радиальных градиентов с параметром globalCompositeOperation, установленным на «светлее», и управлением непрозрачностью с помощью globalAlpha.

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

person Bart Stewart    schedule 22.02.2016

Эта скрипта http://jsfiddle.net/2qQLz/ является попыткой найти решение. Если он близок к тому, что вам нужно, его можно развивать дальше. Он ограничивает градиентную заливку ограничивающим прямоугольником, одна сторона которого является линией пересечения «кругов». Для двух «кругов» одинакового радиуса, лежащих вдоль горизонтальной линии, достаточно легко найти значение x точек пересечения «кругов» и нарисовать ограничивающие прямоугольники для градиентной заливки для каждого «круга».

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

person jing3142    schedule 16.04.2012

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

Итак, каковы попиксельные решения?

Я вижу два основных подхода на основе пикселей, которые помогут решить эту проблему.

  1. Нарисуйте скрытый контекст и смешайте с ручной функцией

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

Ваш первый вариант более универсален, чем второй, в том смысле, что вы можете рисовать все, что вам нравится, используя обычные методы холста, а затем смешать этот холст с вашим видимым холстом. См. проект @Phrogz context-blender, чтобы узнать, как смешать один контекст с другим.

Второй вариант необходим, когда вам нужно рисовать так, как холст не поддерживает по умолчанию.

Самая большая трудность, с которой приходится сталкиваться, — это альфа-прозрачность из-за того, что у вас может быть содержимое за радиальным градиентом. После того, как вы нарисовали поверх фонового изображения, почти невозможно увидеть, что это было до того, как вы нарисовали поверх него, если только у вас нет копии этого фона. Трудность возникает даже при расчете попиксельно: наложение одного изображения поверх другого не сработает.

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

В духе варианта 1 вы можете создать пустой контекст и отобразить на нем несколько градиентов. Затем визуализируйте это сверху.

И в духе варианта 2, если вы можете определить все точки перед рендерингом, вы можете рассчитать изображение и смешивание по этим точкам за один проход.

Сочетание техники однопроходного рендеринга с фоновым контекстом позволит вам рисовать контур поверх видимого холста, не делая ни одного ручного считывания пикселя, только пиксели записывают.

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


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

if (src.a <= dst.a) {
    result = dst;
} else {
    result = src;
}
person Matt Esch    schedule 12.04.2012