Изменение толщины обводки 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 и других внешних элементов. То есть только пересчётом координат треугольника, ширины и высоты прямоугольника и радиуса круга.

Дополнительные ограничения:

  1. Координаты треугольника, начальный радиус или координаты центра круга, а также ширина и высота прямоугольника - произвольные. В примере их значения - просто пример. Треугольник всегда равносторонний (для упрощения).
  2. Нельзя менять viewBox, использовать css, маски и clip-path.
  3. Обводка ни при каких обстоятельствах не выступает за границы фигур.
  4. Обводкой можно полностью "заполнить" фон внутри фигуры.
  5. Обводка и размер фигур регулируется через ползунки.
  6. В самом SVG может быть множество различных других фигур и элементов, решение не должно на них влиять.

Вопрос: как это можно сделать?

Вот пример из которого убрано всё лишнее: https://jsfiddle.net/ru8fzeya/6/ Оставлены только фигуры и изменение толщины обводки/размера. Но стандартное поведение для stroke - расширение в обе стороны, а необходимо добиться такого же результата как в примере с масками, но без масок.


Ответы (2 шт):

Автор решения: Alexandr_TT

А т.к. с масками не всё идеально (об этом написано в этом вопросе, а само решение с масками отсюда), то вопрос такой - как добиться такого же эффекта как в примере, но без масок?

Давайте попробуем заменить маски на 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>

→ Ссылка
Автор решения: Daniil Loban

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

графическое представление

Итак к примеру есть какая-то фигура мы хотим увеличить толщину обводки на 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>

→ Ссылка