Обзор
SVG имеет богатый DOM API, поэтому, если есть путь, который не будет заполнен, как насчет его создания?
Представленный подход состоит из следующих этапов:
- Определение выпуклой оболочки заданного пути («O-путь»)
- Построение пути от границы выпуклая оболочка («CH-путь»)
- Вставьте CH-путь в SVG непосредственно перед O-путем.
- Заполните CH-путь нужным цветом, оставьте O- Прозрачный путь.
CH-путь гарантированно заполнен. Поскольку SVG визуализируются с применением алгоритма рисования, CH-путь может использоваться в качестве фона для O-пути, окрашивая абстрактный холст прямо за O-путем и моделируя то, что большинство пользователей интуитивно рассматривает заполнение пути.
Идея может показаться излишней, но она должна быть надежной для большинства определений путей, встречающихся в дикой природе (см. Раздел «Ограничения» для концептуального предостережения). Его можно легко расширить, чтобы рассмотреть набор элементов пути, используемых для рисования целевого значка.
Ограничение
CH-путь покроет O-путь по определению. Обратный путь, который имеет место только для выпуклых O-путей. Если O-путь не является выпуклым, это решение для каждого конкретного случая, будет ли это решение по-прежнему полезным (например, значки, такие как Pa c - могут иметь лучшие коэффициенты, чем формы звезд).
Подробности
Вычисление выпуклой оболочки (CH) CH набора точек вычисляется как наименьший выпуклый набор точек, который полностью покрывает эти точки. Используя методы getPointAtLength
и getTotalLength
API DOM элемента path
, можно получить подходящий набор точек без необходимости анализа спецификации пути, поскольку эти методы позволяют программно проходить по пути и выбирать точки на пути ( в приведенном ниже коде в равных интервалах длиной в 0,1 единицы расстояния). Полученный набор точек затем подается в алгоритм CH.
Построение пути из CH-пути. Алгоритм CH возвращает точки CH в правильном порядке, чтобы нарисовать границу путем соединения соседних точек. с прямыми отрезками. Это тривиально преобразуется в атрибут d
элемента path
с помощью команд пути M
(перейти к) и L
(от линии к) в абсолютных координатах.
Вставьте CH-путь в SVG непосредственно перед O-путем.
Метод insertAdjacentElement
DOM API элементов SVG делает свое дело, когда применяется к O- дорожка.
Заполните CH-путь нужным цветом, оставьте O-путь прозрачным
Используйте атрибут fill
.
Inline Demo
Демонстрация преобразует путь значков для лучшей видимости и др aws Граница СН. Фактические вычисления выполняются с исходными координатами пути. CH рисуется красным. Производственный код использует некоторую библиотеку для вычисления CH. Демонстрация показывает значок почты, но включает определения путей для значков телефона и Instagram - закомментируйте соответствующий элемент use
, чтобы использовать их.
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="250" viewBox="0 0 2000 1000">
<defs>
<g
id="icon_instagram"
transform="translate(500,400) scale(20,20) translate(-10,-10)"
>
<path d="M15.233 5.488c-.843-.038-1.097-.046-3.233-.046s-2.389.008-3.232.046c-2.17.099-3.181 1.127-3.279 3.279-.039.844-.048 1.097-.048 3.233s.009 2.389.047 3.233c.099 2.148 1.106 3.18 3.279 3.279.843.038 1.097.047 3.233.047 2.137 0 2.39-.008 3.233-.046 2.17-.099 3.18-1.129 3.279-3.279.038-.844.046-1.097.046-3.233s-.008-2.389-.046-3.232c-.099-2.153-1.111-3.182-3.279-3.281zm-3.233 10.62c-2.269 0-4.108-1.839-4.108-4.108 0-2.269 1.84-4.108 4.108-4.108s4.108 1.839 4.108 4.108c0 2.269-1.839 4.108-4.108 4.108zm4.271-7.418c-.53 0-.96-.43-.96-.96s.43-.96.96-.96.96.43.96.96-.43.96-.96.96zm-1.604 3.31c0 1.473-1.194 2.667-2.667 2.667s-2.667-1.194-2.667-2.667c0-1.473 1.194-2.667 2.667-2.667s2.667 1.194 2.667 2.667zm4.333-12h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm.952 15.298c-.132 2.909-1.751 4.521-4.653 4.654-.854.039-1.126.048-3.299.048s-2.444-.009-3.298-.048c-2.908-.133-4.52-1.748-4.654-4.654-.039-.853-.048-1.125-.048-3.298 0-2.172.009-2.445.048-3.298.134-2.908 1.748-4.521 4.654-4.653.854-.04 1.125-.049 3.298-.049s2.445.009 3.299.048c2.908.133 4.523 1.751 4.653 4.653.039.854.048 1.127.048 3.299 0 2.173-.009 2.445-.048 3.298z"/>
</g>
<g
id="icon_mail"
transform="translate(500,400) scale(40,40) translate(-12,-12.713)"
>
<path d="M12 12.713l-11.985-9.713h23.97l-11.985 9.713zm0 2.574l-12-9.725v15.438h24v-15.438l-12 9.725z"/>
</g>
<g
id="icon_skype"
transform="translate(500,400) scale(20,20) translate(-12, -12)"
>
<path d="M12 0c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm3.445 17.827c-3.684 1.684-9.401-9.43-5.8-11.308l1.053-.519 1.746 3.409-1.042.513c-1.095.587 1.185 5.04 2.305 4.497l1.032-.505 1.76 3.397-1.054.516z"/>
</g>
</defs>
<!-- only one 'use' element should be active at any one time -->
<use href="#icon_mail"/>
<!--use href="#icon_instagram"/-->
<!--use href="#icon_skype"/-->
<text x="1100" y="150" width="200" height="100" style="font-size:300%;">Click on the filled area of the icon to the left.</text>
<script type="text/javascript">
<![CDATA[
const SVG_XLINK = "http://www.w3.org/1999/xlink";
const SVG_NS = 'http://www.w3.org/2000/svg';
function _cmp ( a, b ) {
let n_cmp
;
n_cmp =
(a.x > b.x)
? 1
: ((a.x < b.x)
? -1
: ((a.y > b.y)
? 1
: ((a.y < b.y) ? -1 : 0)
)
)
;
return n_cmp;
} // _cmp
// Get convex Hull of point set
function getConvexHull ( pa_points ) {
console.log(`getConvexHull: started, point set size: ${pa_points.length};`); // `
pa_points.sort ( _cmp );
console.log(`getConvexHull: done: sort;`);
let a_ch = []
, m_base
, n_base = 0
, n_idxCurrentCandidate // index of current candidate for the next CH point
, n_count = 0
;
// !!! Reminder: svg y axis oriented downwards !
// Sweep 1/2: x -> x+ (lower CH)
let i = 0;
while (pa_points[i].x === pa_points[0].x) {
//***console.log(`getConvexHull: pushing point #${i}.`);
a_ch.push ( pa_points[i] ); // yep, points have been lexicographically ordered
i++;
}
n_idxCurrentCandidate = i-1;
while (n_idxCurrentCandidate + 1 < pa_points.length) { // guard against degenerate case
n_base = n_idxCurrentCandidate;
m_base = {
dx: (pa_points[n_base+1].x - pa_points[n_base].x)
, dy: (pa_points[n_base+1].y - pa_points[n_base].y)
};
n_idxCurrentCandidate = n_base+1;
if (n_base+2 < pa_points.length) {
let k = n_base+2;
do {
let n_fac = (pa_points[k].x - pa_points[n_base].x) / m_base.dx
;
if ((pa_points[k].y - pa_points[n_base].y) > n_fac * m_base.dy) {
// steeper slope detected.
n_idxCurrentCandidate = k;
m_base.dx = pa_points[k].x - pa_points[n_base].x;
m_base.dy = pa_points[k].y - pa_points[n_base].y;
}
k++;
} while ( k < pa_points.length );
}
// We will get the end point in the upper CH sweep.
if (n_idxCurrentCandidate !== pa_points.length-1) {
//***console.log(`getConvexHull: pushing point #${n_idxCurrentCandidate}.`);
a_ch.push ( pa_points[n_idxCurrentCandidate] );
}
n_count++;
} // lower CH
console.log(`getConvexHull: lower CH completed after ${n_count} slope checks, ${a_ch.length} CH points found so far;`);
// Sweep 2/2: x -> x- (upper CH)
i = pa_points.length - 1;
while (pa_points[i].x === pa_points[pa_points.length-1].x) {
//***console.log(`getConvexHull: pushing point #${i}.`);
a_ch.push ( pa_points[i] ); // yep, points have been lexicographically ordered
i--;
}
n_idxCurrentCandidate = i+1;
while (n_idxCurrentCandidate - 1 >= 0) { // guard against degenerate case
n_base = n_idxCurrentCandidate;
m_base = {
dx: (pa_points[n_base-1].x - pa_points[n_base].x)
, dy: (pa_points[n_base-1].y - pa_points[n_base].y)
};
n_idxCurrentCandidate = n_base-1;
if (n_base-2 >= 0) {
let k = n_base-2;
do {
let n_fac = (pa_points[k].x - pa_points[n_base].x) / m_base.dx
;
if ((pa_points[k].y - pa_points[n_base].y) < n_fac * m_base.dy) {
// shallower slope detected.
n_idxCurrentCandidate = k;
m_base.dx = pa_points[k].x - pa_points[n_base].x;
m_base.dy = pa_points[k].y - pa_points[n_base].y;
}
k--;
} while ( k >= 0 );
}
// We got the start point already in the lower CH sweep.
if (n_idxCurrentCandidate !== 0) {
//***console.log(`getConvexHull: pushing point #${n_idxCurrentCandidate}.`);
a_ch.push ( pa_points[n_idxCurrentCandidate] );
}
n_count++;
} // upper CH
console.log(`getConvexHull: upper CH completed. TL of ${n_count} slope checks, ${a_ch.length} CH points found.`);
return a_ch;
} // getConvexHull
// Get convex hull of path
function getPathHull ( e_path ) {
console.log(`getPathHull: started;`);
let a_CH
, a_pointset = []
, n_pathLengthCurrent = 0
, n_pathLengthTotal = e_path.getTotalLength()
, n_steps = 10 * Math.floor(n_pathLengthTotal)
;
console.log(`getPathHull: path length = ${n_pathLengthTotal};`);
// Get point coords along the path.
// Sliding [-1,+1]-window to eliminate interior points on straight line segments.
let o_svgpoint_prevprev
, o_svgpoint_prev
, o_svgpoint
;
a_pointset.push( e_path.getPointAtLength(0) );
for (let i = 0; i < n_steps; i++) {
o_svgpoint_prevprev = o_svgpoint_prev;
o_svgpoint_prev = o_svgpoint;
o_svgpoint = e_path.getPointAtLength(i * n_pathLengthTotal / n_steps);
if (i > 1) {
// Optimizing away points on straight line axis-parallel segments
if (!(
(
(o_svgpoint_prevprev.x === o_svgpoint_prev.x)
&& (o_svgpoint_prev.x === o_svgpoint.x)
)
|| (
(o_svgpoint_prevprev.y === o_svgpoint_prev.y)
&& (o_svgpoint_prev.y === o_svgpoint.y)
)
)) {
a_pointset.push( o_svgpoint_prev );
}
}
}
a_pointset.push( o_svgpoint ); // the last one - always included
console.log(`getPathHull: pointset size = ${n_steps}, after straight line segment optimization ${a_pointset.length};`);
let n_checkAt = Math.floor(Math.random() * a_pointset.length)
;
console.log(`getPathHull: @${Number(100 * n_checkAt / n_steps).toFixed(2)}%: (x,y) = (${Number(a_pointset[n_checkAt].x).toFixed(2)}, ${Number(a_pointset[n_checkAt].y).toFixed(2)});`);
// Compute the Convex Hull for the point set
a_CH = getConvexHull ( a_pointset );
// Build a path from the CH. The CH point set is ordered counter-clockwise
a_CH = a_CH.map ( (po_svgpoint) => {
return { x: Number(po_svgpoint.x).toFixed(3), y: Number(po_svgpoint.y).toFixed(3) };
});
return a_CH;
} // getPathHull
//
// paintPathHull
//
function paintPathHull ( eve, ps_id ) {
console.log(`paintPathHull: started; eve.target.parentElement.id = '${eve.target.id}', ps_id = '${ps_id}'`);
let a_CH
, e_g = eve.target.parentElement
, e_hull
, e_path = document.querySelector(`#${e_g.getAttribute('id')} > path`)
, e_svg = document.querySelector('svg')
, s_attr_d
;
a_CH = getPathHull(e_path);
console.log ( `a_CH - first 10 coords: ${JSON.stringify(a_CH.slice(0,9))};`);
s_attr_d =
`M${a_CH[0].x},${a_CH[0].y} `
+ a_CH.map( ( po_coords, pn_idx ) => {
return `L${po_coords.x},${po_coords.y}`;
}).join(' ')
;
e_hull = document.createElementNS(SVG_NS, 'path');
e_hull.setAttribute('d', s_attr_d);
e_hull.setAttribute('fill', 'green');
e_hull.setAttribute('stroke', 'red');
e_hull.setAttribute('stroke-width', '0.1');
e_path.insertAdjacentElement('beforebegin', e_hull);
} // paintPathHull
document.querySelector('#icon_instagram').addEventListener ( 'click', (eve) => { paintPathHull(eve, 'icon_instagram'); } );
document.querySelector('#icon_mail').addEventListener ( 'click', (eve) => { paintPathHull(eve, 'icon_mail'); } );
document.querySelector('#icon_skype').addEventListener ( 'click', (eve) => { paintPathHull(eve, 'icon_skype'); } );
]]>
</script>
</svg>