Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scale different parts of SVG file differently

I need a variable-height svg-graphic for a ticket with serrated edges. The top and bottom segments should scale to size maintaining a fixed aspect ratio while the middle segment should stretch to fit the size of the container.

This image should be illustrative

The different aspect ratios

I found this jsFiddle doing pretty much what I would like just horizontally instead of vertically but I'm not well-versed enough with viewbox to understand what is happening and adapting it for my needs. I just managed to mess up my file.

My svg file is very simple at the moment; a top path, a bottom path and a rect in the middle. It looks like this:

<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
     width="464px" height="464px" viewBox="0 0 464 464" enable-background="new 0 0 464 464" xml:space="preserve">
    <path fill="#FFF100" d="M432.846,10.522c-5.755,0.057-10.466-4.562-10.522-10.316c-0.001-0.069-0.001-0.137,0-0.206h-20.839
        c0.057,5.754-4.563,10.465-10.316,10.522c-5.755,0.057-10.466-4.562-10.522-10.316c0-0.069,0-0.137,0-0.206h-20.843
        c0.058,5.754-4.561,10.465-10.315,10.522S339.021,5.96,338.965,0.206c0-0.069,0-0.137,0-0.206h-20.836
        c0.057,5.754-4.562,10.465-10.316,10.522c-5.754,0.057-10.465-4.562-10.522-10.316c0-0.069,0-0.137,0-0.206h-20.437
        c0,24.772-20.082,44.853-44.854,44.853c-24.771,0-44.853-20.081-44.853-44.853H166.71c0.056,5.754-4.562,10.465-10.317,10.522
        c-5.754,0.057-10.465-4.562-10.521-10.316c-0.001-0.069-0.001-0.137,0-0.206h-20.839c0.098,5.754-4.486,10.498-10.24,10.596
        c-5.754,0.099-10.498-4.486-10.596-10.24c-0.002-0.119-0.002-0.238,0-0.356H83.355c0.057,5.754-4.563,10.465-10.317,10.522
        S62.573,5.96,62.517,0.206c0-0.069,0-0.137,0-0.206H41.677c0.057,5.754-4.562,10.465-10.316,10.522
        C25.607,10.579,20.896,5.96,20.839,0.206c-0.001-0.069-0.001-0.137,0-0.206H0v56h464V0h-20.839
        C443.219,5.754,438.6,10.465,432.846,10.522z"/>
    <path fill="#FFF100" d="M432.846,453.478c-5.755-0.056-10.466,4.563-10.522,10.317c-0.001,0.068-0.001,0.137,0,0.205h-20.839
        c0.057-5.754-4.563-10.466-10.316-10.522c-5.755-0.056-10.466,4.563-10.522,10.317c0,0.068,0,0.137,0,0.205h-20.843
        c0.058-5.754-4.561-10.466-10.315-10.522c-5.755-0.056-10.466,4.563-10.522,10.317c0,0.068,0,0.137,0,0.205h-20.836
        c0.057-5.754-4.562-10.466-10.316-10.522c-5.754-0.056-10.465,4.563-10.522,10.317c0,0.068,0,0.137,0,0.205h-20.437
        c0-24.771-20.082-44.854-44.854-44.854c-24.771,0-44.853,20.082-44.853,44.854H166.71c0.056-5.754-4.562-10.466-10.317-10.522
        c-5.754-0.056-10.465,4.563-10.521,10.317c-0.001,0.068-0.001,0.137,0,0.205h-20.839c0.098-5.754-4.487-10.498-10.24-10.597
        c-5.754-0.098-10.498,4.486-10.596,10.24c-0.002,0.119-0.002,0.238,0,0.356H83.355c0.057-5.754-4.563-10.466-10.317-10.522
        c-5.754-0.056-10.465,4.563-10.522,10.317c0,0.068,0,0.137,0,0.205H41.677c0.057-5.754-4.562-10.466-10.316-10.522
        c-5.754-0.056-10.465,4.563-10.521,10.317c-0.001,0.068-0.001,0.137,0,0.205H0v-56h464v56h-20.839
        C443.219,458.246,438.6,453.534,432.846,453.478z"/>
    <rect y="56" fill="#00AEEF" width="464" height="352"/>
</svg>

I would be very happy if someone was able to help me out.

like image 952
Regicollis Avatar asked Feb 20 '16 14:02

Regicollis


2 Answers

In general you can't scale some parts of an SVG differently from others. However there are some simple cases (like the pencil image you linked to) where you can be tricky and construct one that works.

How that pencil works

It starts by defining a <symbol> element for each of the three parts of the pencil: the end, the body, and the sharp end.

It then combines them by adding three child <svg> elements. At the back is the body <symbol> stretched the entire width of the main <svg>. Then the two end cap <svg> elements are placed in front. Each end cap <svg> occupies half of the width. But they have 'preserveAspectRatio` attributes that results in them being aligned to the left and right ends respectively.

If we make the two end parts almost completely translucent, you can see what's going on:

:checked~svg{
    width:500px;
}
<input type="checkbox"/><br/>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
	<defs>
		<g id="source">
			<rect width="200" height="100" fill="yellow"/>
			<circle cx="50" cy="50" r="40" fill="red"/>
			<rect x="50" y="10" width="100" height="80" fill="orange"/>
			<path d="M150,10L190,50 150,90z" fill="pink"/>
		</g>
		<symbol id="left" viewBox="0 0 50 100" preserveAspectRatio="none">
			<use xlink:href="#source"/>
		</symbol>
		<symbol id="middle" viewBox="50 0 100 100" preserveAspectRatio="none">
			<use xlink:href="#source"/>
		</symbol>
		<symbol id="right" viewBox="150 0 50 100" preserveAspectRatio="none">
			<use xlink:href="#source"/>
		</symbol>
	</defs>
	<svg viewBox="0 0 1 1" preserveAspectRatio="none">
		<use xlink:href="#middle" width="1" height="1"/>
	</svg>
	<svg viewBox="0 0 1 2" preserveAspectRatio="xMinYMid">
		<use xlink:href="#left" width="1" height="2" opacity="0.1"/>
	</svg>
	<svg viewBox="0 0 1 2" preserveAspectRatio="xMaxYMid">
		<use xlink:href="#right" width="1" height="2" opacity="0.2"/>
	</svg>
</svg>

You can see how the body has been stretched to the entire width and how the endcaps are placed at the ends to hide the stretched body.

This trick only works because the pencil has a solid background (the yellow). But I assume you want the perforated ends of you ticket to be transparent, so that trick won't work for you. If you are okay with it being white, then it will.

What's probably simpler for you is the just stack the three parts on top of one another.

<svg>  (the top of the ticket)
<div>  (containing the ticket body contents)
<svg>  (the bottom of the ticket)
like image 72
Paul LeBeau Avatar answered Oct 06 '22 21:10

Paul LeBeau


Then again, if you want to do it the hard way, you can use JavaScript:

    $("input#resize").on("click", function(){
    		if($("input#resize:checked").length){
    			scaleSvgBorder();
    		}
    	});
    	function scaleSvgBorder(){
      		if ($("svg#myFancyBorder").length && $("svg#myFancyBorder").height() > 0){
            //first we need the ratio between the SVG grid and our beloved HTML pixels.
                  var svgRatio = $("svg#myFancyBorder").height() / $("svg#myFancyBorder").attr("viewBox").split(" ")[3];
            //Next we need to figure out which scale to apply on the sides group inside our SVG. 
            //We do this by calculating the height we want the sides to be (in pixels), and divide it by the current height of the sides (in pixels).
                  var sidesCurrentHeightPx = $("svg#myFancyBorder #sides")[0].getBBox().height * svgRatio;
            //Let's subtract the top & bottom of our border from the container height, in order to get the height we want the sides to be (in pixels).
    			  var nonScalingPartsHeightPx = ($("svg#myFancyBorder #top")[0].getBBox().height + $("svg#myFancyBorder #bottom")[0].getBBox().height) * svgRatio;
    			  var sidesTargetHeightPx = $("#container").height() - nonScalingPartsHeightPx;
            //Finally, we have the scale ratio!
    			  var scaleRatio = sidesTargetHeightPx / sidesCurrentHeightPx;
            //We need to subtract the scaled top from the top for an additional sides translation transformation, since they will move, because SVG scales from 0, 0!
    			  var scaledTopDifference = $("svg#myFancyBorder #top")[0].getBBox().height - ($("svg#myFancyBorder #top")[0].getBBox().height * scaleRatio);
            //We need to subtract the scaled sides from the sides for the bottom translation, because SVG scales from 0, 0!
        		var scaledSidesDifference = $("svg#myFancyBorder #sides")[0].getBBox().height - ($("svg#myFancyBorder #sides")[0].getBBox().height * scaleRatio);
            //Notice that we have to apply the scale ratio to the translation transformation if we place it AFTER the scale transformation!!!
    			  $("svg#myFancyBorder #sides").attr("transform", "scale(1, " + scaleRatio + ") translate(0, " + (scaledTopDifference / scaleRatio) +")");
            //* -1 because it has to move down.
    			  $("svg#myFancyBorder #bottom").attr("transform", "translate(0, " + ( scaledSidesDifference * -1 ) +")");
            //Let's stretch the viewbox to the height of the container too!
    			  $("svg#myFancyBorder").attr('viewBox', "0 0 "+ $("svg#myFancyBorder").attr("viewBox").split(" ")[2] +" "+ ($("#container").height() / svgRatio) );
    		}
    	}
#container{
  position: relative;
  width: 200px;
  height: 400px;
  background-color: #cde
}
svg#myFancyBorder{
  position: absolute;
  width: 100%;
  height: auto;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<input type="checkbox" id="resize" /><label for="resize">Fit border to container</label>
    <div id="container">
<!-- Notice that there's no width, height, x or y attribute, since we're trying to keep it simple. -->
<!-- Also notice that the viewBox HAS to start with '0 0' since this is the point where SVG images scale from! -->	
      <svg version="1.1" id="myFancyBorder" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 277.5 237.5">
    		<rect id="top" width="277.5" height="55"/>
    		<g id="sides">
    			<line fill="none" stroke="#000000" stroke-width="5" x1="2.5" y1="55" x2="2.5" y2="182.5"/>
    			<line fill="none" stroke="#000000" stroke-width="5" x1="275" y1="55" x2="275" y2="182.5"/>
    		</g>
    		<rect id="bottom" y="182.5" width="277.5" height="55"/>
    	</svg>
    </div>
like image 34
ZEF Avatar answered Oct 06 '22 21:10

ZEF