Resizing single shapes dynamically without scaling

2019-03-18 17:18发布

问题:

I have an arrow as a SVG which's length I want to alter in response to the width of a wrapping DIV (for example).

All my previous attempts have led into this behavior (scaling the whole SVG):

Rather than this, I want to achieve this behavior:

Is it possible to just alter single shapes inside of a SVG by simply adding percentual values to the code? If not, how could I perform this?

SVG as code:

<svg width="35px" viewBox="0 0 35 985" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <g id="Group" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(0.000000, 0.000000)">
    <rect id="Rectangle-2" fill="#5FDCC7" x="12" y="19.7987928" width="11" height="100%"></rect>
    <polygon id="Triangle" fill="#5FDBC6" points="17.5 0 35 20.7887324 0 20.7887324"></polygon>
    <rect id="Rectangle-3" fill="#5FDBC6" x="0" y="973.110664" width="35" height="11"></rect>
  </g>
</svg>

回答1:

As a general rule, there is no way to create a scalable SVG where parts of it don't scale. If an SVG has a viewBox, and you scale it, then all the contents will scale. There is no way to mark some of the contents as immune to the scaling.

The nearest you can get is to restrict scaling to the X axis. However the arrowhead will still stretch.

<svg width="35px" height="150px" viewBox="0 0 35 985" preserveAspectRatio="none">

        <g id="Group" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(0.000000, 0.000000)">
            <rect id="Rectangle-2" fill="#5FDCC7" x="12" y="19.7987928" width="11" height="100%"></rect>
            <polygon id="Triangle" fill="#5FDBC6" points="17.5 0 35 20.7887324 0 20.7887324"></polygon>
            <rect id="Rectangle-3" fill="#5FDBC6" x="0" y="973.110664" width="35" height="11"></rect>
        </g>
</svg>


<svg width="35px" height="300px" viewBox="0 0 35 985" preserveAspectRatio="none">

        <g id="Group" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(0.000000, 0.000000)">
            <rect id="Rectangle-2" fill="#5FDCC7" x="12" y="19.7987928" width="11" height="100%"></rect>
            <polygon id="Triangle" fill="#5FDBC6" points="17.5 0 35 20.7887324 0 20.7887324"></polygon>
            <rect id="Rectangle-3" fill="#5FDBC6" x="0" y="973.110664" width="35" height="11"></rect>
        </g>
</svg>

If you want a generalised solution, then you have to monitor resize events and update the SVG dynamically.

The limited sneaky clever solution...

Having said all that, there is a clever technique that works in a limited set of circumstances. The limitations are:

  1. The SVG stretches in only one direction.
  2. The "non-scaling" parts are at either or both ends of the stretch
  3. The end parts are solid and don't have any holes.
  4. You are okay with the background of the SVG being a solid color, rather than transparent.

Here is a demo of your shape implemented using this technique. If you resize the window horizontally, you'll see it stretches appropriately.:

<svg width="100%" height="35px">

  <svg viewBox="0 0 1 1" preserveAspectRatio="none">
    <rect x="0" y="0" width="1" height="1" fill="white"/>
    <rect x="0" y="0.4" width="1" height="0.2" fill="#5FDBC6"/>
  </svg>
    
  <svg viewBox="0 0 0.2 1" preserveAspectRatio="xMinYMid">
    <rect x="0" y="0" width="0.2" height="1" fill="#5FDBC6"/>
  </svg>

  <svg viewBox="0 0 0.8 1" preserveAspectRatio="xMaxYMid">
    <rect x="0" y="0" width="0.8" height="1" fill="white"/>
    <polygon points="0,0 0.8,0.5 0,1" fill="#5FDBC6"/>
  </svg>

</svg>



回答2:

Yes, you can use percentages in svg.

for basic shapes its quite simple. You can write your main rect as

<rect x="0" y="5" width="100%" height="10"/>

your second rect is even simpler, as it sits at 0,0 all the time

<rect x="0" y="0" width="10" height="20"/>

but with the arrow there is a problem in pathes and polygon you can not use percentages. To work around this problem there is a two step solution.

first put the path in a symbol element:

<symbol id="arrow" viewBox="0 0 20 20" width="20" height="20">
  <path d="M0,0L20 10L0 20z" />
</symbol>

now you can position this symbol like you would position a rect... easy...

<use xlink:href="#arrow" x="100%" y="0" width="20" height="20"/>

but now your arrow starts a 100% and is completely outside of your viewport. you could just set overflow: visible on your svg and be done with it, but that is not what we want... we want the arrow to end at 100%. But that easy as well, we know that the arrow is 20px wide. So just translate the use back 20px:

<use xlink:href="#arrow" x="100%" y="0" width="20" height="20" transform="translate(-20 0)"/>

using this approach, you can position any shape at any position base on percentages...

to warp it all up, your complete svg now looks like this:

<svg id="svg" height="20px" xmlns:xlink="http://www.w3.org/1999/xlink">
  <symbol id="arrow" viewBox="0 0 20 20" width="20" height="20">
    <path d="M0,0L20 10L0 20z" />
  </symbol>
  <g id="Group" fill="#5FDCC7">
    <rect x="0" y="5" width="100%" height="10" transform="translate(-20 0)" />
    <rect x="0" y="0" width="10" height="20" />
    <use xlink:href="#arrow" width="20" height="20" x="100%" y="0" transform="translate(-20 0)" />
  </g>
</svg>

and here is a snippet using this svg with 3 different widths:

svg:nth-of-type(1) {
  width: 100px
}
svg:nth-of-type(2) {
  width: 200px
}
svg:nth-of-type(3) {
  width: 300px
}
<svg height="20px" xmlns:xlink="http://www.w3.org/1999/xlink">
  <symbol id="arrow" viewBox="0 0 20 20" width="20" height="20">
    <path d="M0,0L20 10L0 20z" />
  </symbol>
  <g id="Group" fill="#5FDCC7">
    <rect x="0" y="5" width="100%" height="10" transform="translate(-20 0)" />
    <rect x="0" y="0" width="10" height="20" />
    <use xlink:href="#arrow" width="20" height="20" x="100%" y="0" transform="translate(-20 0)" />
  </g>
</svg>
<br/>
<svg height="20px" xmlns:xlink="http://www.w3.org/1999/xlink">
  <symbol id="arrow" viewBox="0 0 20 20" width="20" height="20">
    <path d="M0,0L20 10L0 20z" />
  </symbol>
  <g id="Group" fill="#5FDCC7">
    <rect x="0" y="5" width="100%" height="10" transform="translate(-20 0)" />
    <rect x="0" y="0" width="10" height="20" />
    <use xlink:href="#arrow" width="20" height="20" x="100%" y="0" transform="translate(-20 0)" />
  </g>
</svg>
<br/>
<svg height="20px" xmlns:xlink="http://www.w3.org/1999/xlink">
  <symbol id="arrow" viewBox="0 0 20 20" width="20" height="20">
    <path d="M0,0L20 10L0 20z" />
  </symbol>
  <g id="Group" fill="#5FDCC7">
    <rect x="0" y="5" width="100%" height="10" transform="translate(-20 0)" />
    <rect x="0" y="0" width="10" height="20" />
    <use xlink:href="#arrow" width="20" height="20" x="100%" y="0" transform="translate(-20 0)" />
  </g>
</svg>



回答3:

For this particlular usecase, I think I would go for a CSS approach, it might (depending on your markup) add a html element if you can't use pseudo elements but it won't require JS or to manualy input values for stroke-width or transform elements.

Here is a way to make this arrow using the paragraph tag :

p {
  position: relative;
  max-width: 300px;
  border-bottom: 4px solid #5FDBC6;
  padding-bottom:6px;
}
p:nth-child(2) {max-width: 400px;}
p:nth-child(3) {max-width: 200px;}
p::before, p::after{
  content:'';
  position:absolute;
  bottom:-8px;
}
p::before {
  height: 12px;
  left:0;
  border-left: 4px solid #5FDBC6;
}
p::after{
  right:-4px;
  border-style:solid;
  border-width:6px 0 6px 8px;
  border-color:transparent transparent transparent #5FDBC6;
}
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. a dapibus, tincidunt velit eget, rutrum urna.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras in purus risus. Ut id est suscipit, tincidunt sem a, fermentum magna. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Ut rutrum ac ligula</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras in purus risus. Ut id est suscipit, tincidunt sem a, fermentum magna.</p>



回答4:

By using symbol's you can make the shapes you want and just include them in. Then to extend the middle section, just make a new path and make the line longer. They can be positioned wherever you like by using the transform property

<svg xmlns="http://www.w3.org/2000/svg">

  <symbol id="base">
    <g xmlns="http://www.w3.org/2000/svg">
      <path fill="#5FDBC6" d="M0,0 L0,20 L4,20 L4,0z"></path>
    </g>
  </symbol>
  <symbol id="triangle" >
    <g>
      <path fill="#5FDBC6" d="M0,0 L15,10 L0,20z"></path>
    </g>
  </symbol>

  <path stroke-width="4" stroke="#5FDBC6" d="M0,10 L100,10"></path>
  <g class="first" transform="translate(0,00)">
    <use xlink:href="#base" />
  </g>
  <g class="first" transform="translate(90,00)">
    <use xlink:href="#triangle" />
  </g>
  
  <path stroke-width="4" stroke="#5FDBC6" d="M0,50 L200,50"></path>
  <g class="first" transform="translate(0,40)">
    <use xlink:href="#base" />
  </g>
  <g class="first" transform="translate(200,40)">
    <use xlink:href="#triangle" />
  </g>


</svg>



回答5:

** EDITED **

This behavior is caused by your viewBox attribute on <svg>. Your SVG’s aspect ratio is being preserved by default. You want this behavior on #Triangle and #Rectangle-3, but not on #Rectangle-2 (you want the height to increase, but the width to stay the same).

I think you'll want to add preserveAspectRatio=none to your <svg> inline CSS and then do some tweaking (since that messes up the aspect ratio of your #Triangle and #Rectangle-3 - or instead of that, you could try changing the height of #triangle-2 to the same height as your wrapper div (in my JS fiddle I've done it this way, setting #Triangle-2s height to 25vh). Play with the height of the window - #Rectangle-2's height elongates but doesn't change width, although I haven’t yet got your #Rectangle-3 to dynamically stick to the bottom of #Rectangle-2 (#Rectangle-3 is stuck at bottom of window): https://jsfiddle.net/KyleVassella/dnagft6q/6/

I suspect you’ll know how to fix this, but either way I’ll keep working on it and in the meantime you can read up here where there is much more info on this subject:

How can I make an svg scale with its parent container?