Вы можете делать такие вещи с feComponentTransfer / feFuncA. Но он работает только с полностью перекрывающимися точками, а не с частичным перекрытием.
Работает так, что сначала рендерит круги с низкой непрозрачностью. Таким образом, чем больше кругов отображается в одной точке, тем выше их непрозрачность. Затем фильтр разбивает изображение на разные слои с разными диапазонами непрозрачности. Затем он применяет размытие разного размера к каждому из слоев, а затем устанавливает непрозрачность на размытом круге и добавляет немного искусственного сглаживания с дополнительным размытием.
Хитрость здесь заключается в получении значения непрозрачности. кругов как раз вправо, поэтому он совпадает с диапазонами непрозрачности feFuncA tableValues. Вы должны заранее знать, какое максимальное количество точек перекрытия будет, чтобы вы могли правильно откалибровать это. (Например, tableValues="0 1 0 0 0"
создает 5 диапазонов непрозрачности: 0-20%, 20-40%, 40-60%, 60-80% и 80-100%.)
<svg width="800px" height="600px">
<defs>
<filter id="embiggen" x="-50%" y="-50%" width="200%" height="200%">
<feComponentTransfer in="SourceGraphic" result="layer1">
<feFuncA type="discrete" tableValues="0 1 0 0 0" />
</feComponentTransfer>
<feComponentTransfer in="SourceGraphic" >
<feFuncA type="discrete" tableValues="0 0 1 0 0" />
</feComponentTransfer>
<feGaussianBlur stdDeviation= "2"/>
<feComponentTransfer >
<feFuncA type="discrete" tableValues="0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1"/>
</feComponentTransfer>
<feGaussianBlur stdDeviation= "0.5" result="layer2"/>
<feComponentTransfer in="SourceGraphic" >
<feFuncA type="discrete" tableValues="0 0 0 1 0" />
</feComponentTransfer>
<feGaussianBlur stdDeviation= "4"/>
<feComponentTransfer>
<feFuncA type="discrete" tableValues="0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1"/>
</feComponentTransfer>
<feGaussianBlur stdDeviation= "0.5" result="layer3"/>
<feComponentTransfer in="SourceGraphic">
<feFuncA type="discrete" tableValues="0 0 0 0 1" />
</feComponentTransfer>
<feGaussianBlur stdDeviation= "8"/>
<feComponentTransfer >
<feFuncA type="discrete" tableValues="0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1" />
</feComponentTransfer>
<feGaussianBlur stdDeviation= "0.5" result="layer4"/>
<feMerge>
<feMergeNode in="layer1"/>
<feMergeNode in="layer2"/>
<feMergeNode in="layer3"/>
<feMergeNode in="layer4"/>
</feMerge>
</defs>
</filter>
<g filter="url(#embiggen)" shape-rendering="crispEdges">
<circle cx="50" cy="50" fill-opacity="0.21" r="10" fill="red"/>
<circle cx="50" cy="50" fill-opacity="0.21" r="10" fill="red"/>
<circle cx="50" cy="50" fill-opacity="0.21" r="10" fill="red"/>
<circle cx="50" cy="50" fill-opacity="0.21" r="10" fill="red"/>
<circle cx="50" cy="50" fill-opacity="0.21" r="10" fill="red"/>
<circle cx="50" cy="50" fill-opacity="0.21" r="10" fill="red"/>
<circle cx="50" cy="50" fill-opacity="0.21" r="10" fill="red"/>
<circle cx="50" cy="50" fill-opacity="0.21" r="10" fill="red"/>
<circle cx="50" cy="50" fill-opacity="0.21" r="10" fill="red"/>
<circle cx="120" cy="50" fill-opacity="0.21" r="10" fill="red"/>
<circle cx="120" cy="50" fill-opacity="0.21" r="10" fill="red"/>
<circle cx="120" cy="50" fill-opacity="0.21" r="10" fill="red"/>
<circle cx="120" cy="50" fill-opacity="0.21" r="10" fill="red"/>
<circle cx="180" cy="50" fill-opacity="0.21" r="10" fill="red"/>
<circle cx="180" cy="50" fill-opacity="0.21" r="10" fill="red"/>
<circle cx="180" cy="50" fill-opacity="0.21" r="10" fill="red"/>
<circle cx="240" cy="50" fill-opacity="0.21" r="10" fill="red"/>
</g>
</svg>