Изменение толщины обводки svg фигур только внутрь относительно осевой линии stroke. Часть 2
https://jsfiddle.net/ru8fzeya/5/
//polygon
function changeStrokePoly() {
var poly = document.getElementById("poly"),
isw = document.getElementById("isw-poly");
poly.setAttribute("stroke-width",isw.value);
};
function changeSizePoly(){
var poly = document.getElementById("poly"),
polyMask = document.getElementById("msk-poly"),
isz = document.getElementById("isz-poly");
var _getCoordinates = function(scale){
var viewBox = 400,
placeSize = 380 * (scale / 100),
h = placeSize * (Math.sqrt(3) / 2);
var offsetX = (viewBox - placeSize) / 2,
offsetY = (viewBox - h) / 2,
coordinates = [
{
x : (placeSize / 2 + offsetX),
y : offsetY
},
{
x : (offsetX),
y : (h + offsetY),
},
{
x : (placeSize + offsetX),
y : (h + offsetY)
}
];
return coordinates;
}
var coordinates = _getCoordinates(isz.value),
coordinatesString = coordinates[0].x + ' ' + coordinates[0].y + ' ' + coordinates[1].x + ' ' + coordinates[1].y + ' ' + coordinates[2].x + ' ' + coordinates[2].y;
poly.setAttribute('points', coordinatesString);
polyMask.setAttribute('points', coordinatesString);
};
//circle
function changeStrokeCircle() {
var circle = document.getElementById("circle"),
isw = document.getElementById("isw-circle");
circle.setAttribute("stroke-width",isw.value);
};
function changeSizeCircle(){
var circle = document.getElementById("circle"),
circleMask = document.getElementById("msk-circle"),
isz = document.getElementById("isz-circle");
circle.setAttribute('r', isz.value);
circleMask.setAttribute('r', isz.value);
};
//rect
function changeStrokeRect() {
var rect = document.getElementById("rect"),
isw = document.getElementById("isw-rect");
rect.setAttribute("stroke-width",isw.value);
};
function changeSizeRect(){
var rect = document.getElementById("rect"),
rectMask = document.getElementById("msk-rect"),
isz = document.getElementById("isz-rect"),
value = isz.value,
maxWidth = 300,
maxHeight = 100,
width = maxWidth * value / 100,
height = maxHeight * value / 100;
rect.setAttribute('width', width);
rect.setAttribute('height', height);
rectMask.setAttribute('width', width);
rectMask.setAttribute('height', height);
};
<div>
<div>
Polygon--
<input id="isw-poly" type="range" min="1" value="30" max="219" oninput="changeStrokePoly()"/>
<input id="isz-poly" type="range" min="10" value="100" max="100" oninput="changeSizePoly()"/>
</div>
<div>
Circle-----
<input id="isw-circle" type="range" min="1" value="30" max="360" oninput="changeStrokeCircle()"/>
<input id="isz-circle" type="range" min="10" value="180" max="180" oninput="changeSizeCircle()"/>
</div>
<div>
Rect-------
<input id="isw-rect" type="range" min="1" value="30" max="100" oninput="changeStrokeRect()"/>
<input id="isz-rect" type="range" min="10" value="100" max="100" oninput="changeSizeRect()"/>
</div>
</div>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="500" height="400" viewBox="0 0 800 400" >
<defs>
<mask id="msk1">
<polygon id="msk-poly" fill="#fff" points="200 35.5 10 364.5 390 364.5"></polygon>
</mask>
<mask id="msk2">
<circle id="msk-circle" fill="#fff" cx="600" cy="200" r="180"></circle>
</mask>
<mask id="msk3">
<rect id="msk-rect" fill="#fff" x="200" y="-100" width="300" height="100"></rect>
</mask>
</defs>
<polygon id="poly" mask="url(#msk1)" fill="none" stroke-width="30" stroke="red" points="200 35.5 10 364.5 390 364.5"></polygon>
<circle id="circle" mask="url(#msk2)" fill="none" stroke-width="30" stroke="green" cx="600" cy="200" r="180"></circle>
<rect id="rect" mask="url(#msk3)" fill="none" stroke-width="30" stroke="blue" x="200" y="-100" width="300" height="100"></rect>
</svg>
Есть 3 фигуры: равносторонний треугольник, круг и прямоугольник. Также есть ползунки для изменения толщины обводки и изменения размера фигур. Между собой фигуры независимые. Как видно в примере, обводка фигур изменяется только внутрь. Внешняя обводка обрезается, спасибо за это mask элементу и @Alexandr_TT за помощь в этом решении, а также за альтернативное решение в этом же вопросе.
Но т.к. с масками/clip-path не всё идеально (подробнее о проблеме написано тут), то я ищу, вероятно, более сложное с точки зрения подхода, но более универсальное, решение, которое будет основано на математике и геометрии.
Ожидаю, что можно решить задачу для каждой фигуры, используя только один polygon, один rect и один circle соответственно. Без масок, без clip-path и других внешних элементов. То есть только пересчётом координат треугольника, ширины и высоты прямоугольника и радиуса круга.
Дополнительные ограничения:
- Координаты треугольника, начальный радиус или координаты центра круга, а также ширина и высота прямоугольника - произвольные. В примере их значения - просто пример. Треугольник всегда равносторонний (для упрощения).
- Нельзя менять viewBox, использовать css, маски и clip-path.
- Обводка ни при каких обстоятельствах не выступает за границы фигур.
- Обводкой можно полностью "заполнить" фон внутри фигуры.
- Обводка и размер фигур регулируется через ползунки.
- В самом SVG может быть множество различных других фигур и элементов, решение не должно на них влиять.
Вопрос: как это можно сделать?
Вот пример из которого убрано всё лишнее: https://jsfiddle.net/ru8fzeya/6/ Оставлены только фигуры и изменение толщины обводки/размера. Но стандартное поведение для stroke - расширение в обе стороны, а необходимо добиться такого же результата как в примере с масками, но без масок.
Ответы (2 шт):
А т.к. с масками не всё идеально (об этом написано в этом вопросе, а само решение с масками отсюда), то вопрос такой - как добиться такого же эффекта как в примере, но без масок?
Давайте попробуем заменить маски на clipPath
Использовал ваш код из вопроса. Заменил маску на clipPath. Принцип обрезки тот же, как и у маски, - всё что вне фигуры, заданной в определении клипа, будет обрезано. Внутреннее содержание будет показано. Поэтому строка будет расти только внутрь.
Добавлена координатная сетка для визуализации изменений.
function changeStroke() {
var poly = document.getElementById("poly"),
isw = document.getElementById("isw");
poly.setAttribute("stroke-width",isw.value);
};
function changeSize(){
var poly = document.getElementById("poly"),
polyMask = document.getElementById("msk-poly"),
isz = document.getElementById("isz");
var _getCoordinates = function(scale){
var viewBox = 400,
placeSize = 380 * (scale / 100),
h = placeSize * (Math.sqrt(3) / 2);
var offsetX = (viewBox - placeSize) / 2,
offsetY = (viewBox - h) / 2,
coordinates = [
{
x : (placeSize / 2 + offsetX),
y : offsetY
},
{
x : (offsetX),
y : (h + offsetY),
},
{
x : (placeSize + offsetX),
y : (h + offsetY)
}
];
return coordinates;
}
var coordinates = _getCoordinates(isz.value),
coordinatesString = coordinates[0].x + ' ' + coordinates[0].y + ' ' + coordinates[1].x + ' ' + coordinates[1].y + ' ' + coordinates[2].x + ' ' + coordinates[2].y;
poly.setAttribute('points', coordinatesString);
polyMask.setAttribute('points', coordinatesString);
};
<div> <input id="isw" type="range" min="1" value="4" max="219" oninput="changeStroke()"/>
<input id="isz" type="range" min="10" value="100" max="100" oninput="changeSize()"/> </div>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="1000" height="1000" viewBox="-20 -20 1050 1050" preserveAspectRatio="xMinYMin meet" >
<defs>
<clipPath id="msk">
<polygon id="msk-poly" fill="#fff" points="200 35.5 10 364.5 390 364.5"></polygon>
</clipPath>
<pattern id="p10" width="10" height="10" patternUnits="userSpaceOnUse">
<path d="M 10 0 L 0 0 0 10" fill="none" stroke="gray" stroke-width="0.5"/>
</pattern>
<pattern id="p100" width="100" height="100" patternUnits="userSpaceOnUse">
<rect width="100" height="100" fill="url(#p10)"/>
<path d="M 100 0 L 0 0 0 100" fill="none" stroke="gray" stroke-width="1"/>
</pattern>
</defs>
<rect width="1000" height="1000" fill="url(#p100)" />
<!-- Вертикальный блок цифр -->
<text x="-15" y="0">0,0</text>
<text x="-20" y="100" >100</text>
<text x="-20" y="200">200</text>
<text x="-20" y="300">300</text>
<text x="-20" y="400">400</text>
<text x="-20" y="500">500</text>
<text x="-20" y="600">600</text>
<text x="-20" y="700">700</text>
<text x="-20" y="800">800</text>
<text x="-20" y="900">900</text>
<text x="-20" y="1000">1000</text>
<!-- Горизонтальный блок -->
<text x="100" y="-5" >100</text>
<text x="200" y="-5" >200</text>
<text x="300" y="-5">300</text>
<text x="400" y="-5">400</text>
<text x="500" y="-5">500</text>
<text x="600" y="-5">600</text>
<text x="700" y="-5">700</text>
<text x="800" y="-5">800</text>
<text x="900" y="-5">900</text>
<text x="990" y="-5">1000</text>
<polygon id="poly" clip-path="url(#msk)" fill="none" stroke-width="4" stroke="red" points="200 35.5 10 364.5 390 364.5"></polygon>
</svg>
Решение основано на принципе нахождения центра и расчета смещения, хотя для каждой из приведенной фигуры оно расчитывается несколько иначе.
Итак к примеру есть какая-то фигура мы хотим увеличить толщину обводки на 1 пиксель, естественно он увеличит видимый размер фигуры именно это и нужно компенсировать (уменьшить фигуру) что схематически изображено на рисунке выше.
В связи с необходимостью расчета компенсаций нет смысла разделять расчеты на 2 функции для размера и толщины линии.
Для круга самое простое: увеличение радиуса на 1 пиксель приводит к увеличению диаметра на 2 пикселя:
- формула перерасчета радиуса
радиус - толщина_обводки / 2;
Для прямоугольника чуть сложнее:
- формула позиции по x:
начальная_позиция_x + толщина_обводки / 2 - формула позиции по y:
начальная_позиция_x + толщина_обводки / 2 - формула ширины:
ширина - толщина_обводки - формула высоты:
высота - толщина_обводки
Для полигона еще сложнее:
- нужно получить все точки (например из html разметки)
- согласно основной идее найти
центроткуда точки будут расходится/сходится - дополнительно находим
расстояние от центраиугол в градусахдля каждой точки - расчет новой координаты для х
центр + (расстояние_от_центра - толшина_обводки + размер) * косинус(угла)
- расчет новой координаты для y
центр + (расстояние_от_центра - толшина_обводки + размер) * синус(угла)
Ниже приведено решение, код не очень оптимизирован, но читаем, единственно не решенный момент заключается в том что если толщина линии больше радиуса то начинаются перекрытия свойственные svg и фигура проподает или "открашивается", поэтому сначала лучше выставить размер при толщине обводки равной 1 пиксель а потом уже добавлять толщину, в целом я думаю не составит труда добавить коррекцию для диапазона величин исходя из размера фигуры чтобы не было подобных наложений.
//polygon
const getPolyPoints = ( id ) => {
var poly = document.getElementById( id );
const points = poly.getAttribute( 'points' ).split(' ');
const result = []
for ( let i = 0; i < points.length; i += 2 ){
result.push({
x: Number( points[i] ),
y: Number( points[i + 1] )
})
}
return result;
}
// находим усредненный центр всех точек
const getPolyCenter = ( points ) => {
let x = 0,
y = 0;
for( let i = 0 ; i < points.length; i++ ){
x += points[i].x;
y += points[i].y;
}
x /= points.length;
y /= points.length;
return({ x, y });
}
const setPointsInfo = ( points, center ) => {
for(let i = 0 ; i < points.length; i++){
// находим растояние из центра
points[i].r =
Math.sqrt( Math.pow(Math.abs(points[i].x - center.x), 2)
+ Math.pow(Math.abs(points[i].y - center.y), 2)
)
// находим угол в градусах
// из центра до точки
points[i].a =
Math.atan2(
points[i].y - center.y,
points[i].x - center.x
) / ( Math.PI/180 );
}
}
const points = getPolyPoints("poly");
const center = getPolyCenter(points);
setPointsInfo(points, center)
function changePoly() {
var poly = document.getElementById("poly"),
isw = document.getElementById("isw-poly"),
isz = document.getElementById("isz-poly");
// высчитываем новые координаты (можно добавить поворот)
let pts = "";
for(let i = 0; i < points.length; i++){
const x = center.x + (points[i].r - isw.value + Number(isz.value))
* Math.cos(points[i].a * ( Math.PI/180 ))
const y = center.y + (points[i].r - isw.value + Number(isz.value))
* Math.sin(points[i].a * ( Math.PI/180 ))
pts += `${x} ${y} `
}
poly.setAttribute("stroke-width",isw.value);
poly.setAttribute('points', pts);
};
//circle
// не нужно дополнительных переменных поскольку
// координаты считаются от центра
function changeCircle() {
var circle = document.getElementById("circle"),
isw = document.getElementById("isw-circle"),
isz = document.getElementById("isz-circle");
circle.setAttribute("stroke-width",isw.value);
// рассчет смещения
circle.setAttribute('r', isz.value - isw.value/2);
};
//rect
// дополнительные преременные для правильного смещения
const rectOrigX = +document.getElementById("rect").getAttribute("x");
const rectOrigY = +document.getElementById("rect").getAttribute("y");
function changeRect() {
var rect = document.getElementById("rect"),
isw = document.getElementById("isw-rect"),
isz = document.getElementById("isz-rect"),
maxWidth = 300,
maxHeight = 100,
width = maxWidth * isz.value / 100,
height = maxHeight * isz.value / 100;
rect.setAttribute( "stroke-width", isw.value );
// рассчет смещения
rect.setAttribute( "x", rectOrigX + Number(isw.value) / 2 );
rect.setAttribute( "y", rectOrigY + Number(isw.value) / 2 );
rect.setAttribute( "width", width - isw.value );
rect.setAttribute( "height", height - isw.value );
};
// корректируем изначальную отрисовку
// в идеале лучше создать классы для фигур
// которые будут их пересчитывать
changePoly();
changeCircle();
changeRect();
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<div>
<div>
Polygon
<input id="isw-poly" type="range" min="1" value="15" max="219" oninput="changePoly()"/>
<input id="isz-poly" type="range" min="10" value="10" max="100" oninput="changePoly()"/>
</div>
<div>
Circle
<input id="isw-circle" type="range" min="1" value="15" max="360" oninput="changeCircle()"/>
<input id="isz-circle" type="range" min="10" value="180" max="180" oninput="changeCircle()"/>
</div>
<div>
Rect
<input id="isw-rect" type="range" min="1" value="15" max="100" oninput="changeRect()"/>
<input id="isz-rect" type="range" min="10" value="100" max="100" oninput="changeRect()"/>
</div>
</div>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="500" height="400" viewBox="0 0 800 400" >
<polygon id="poly" fill="none" stroke-width="15" stroke="red" points="200 35.5 10 364.5 390 364.5"></polygon>
<circle id="circle" fill="none" stroke-width="15" stroke="green" cx="600" cy="200" r="180"></circle>
<rect id="rect" fill="none" stroke-width="15" stroke="blue" x="200" y="-100" width="300" height="100"></rect>
</svg>
</body>
</html>
