For every Subnautica: Below Zero update, we create a micro-site to detail, in visual clarity, the additions to the game. The objective of each update is to show what we've added or improved upon with the latest Early Access update to the game. Information needs to be visual, clear, and expressive. So, in advance of building each micro-site, I sit down with the information and imagine the best possible way to express each addition. Throughout the years, I've grown the number of designs in my toolkit and with each site, I try to push my skills as web developer.
For this update, I wanted to find a way to point out additions in a particular area of the game using one image. I didn't want to crop the same image over and over, but I also didn't want to simply describe what I needed the viewer to look at. What's a girl to do? Enter responsive SVGs.
I can feel it, you're asking "Why bother with SVGs when we can use PNGs to a similar effect?" Great question. Most importantly, I didn't want to affect the image. If a user wanted to save the image off the page, I wanted them to have a clean version. Second, PNGs are expensive resources on web-pages, especially on this scale (we would need an image that is 2x the intended size to get a clear picture on Retina displays). Third, SVGs are vector-based and thus scale perfectly. They are the most loss-less format for graphics in websites. Finally, if I wanted to, I could move the path end-points to match up with my textboxes in a responsive fashion.
OK, What about the code?
First things first, I knew I needed the image to be responsive inside of a parent element constrained only by a max-width property. When working with SVGs, you have to think of the containing element as a canvas within which to work. I needed a canvas that was always going to be the size of a responsive image:
<style>
#greenhouse {
position: relative;
width: 100%;
vertical-align: middle;
margin: 0;
overflow: hidden;
}
#greenhouse svg {
display: inline-block;
position: absolute;
top: 0;
left: 0;
}
</style>
<div id="greenhouse">
<svg viewBox="0 0 1280 720"
preserveAspectRatio="xMinYMin meet">
<image width="1280"
height="720" xlink:href="https://dkli3tbfz4zj3.cloudfront.net/all/202006_SaladDays/images/greenhouse.jpg">
</image>
</svg>
</div>
The important part here is only that we are containing an image inside of an SVG viewBox
which describes the contained element space (our canvas). By making the parent div position: relative;
and the child SVG position: absolute;
we're asking the DOM to overlay our SVG canvas over the parent element #greenhouse
with width instructions to fill 100% of its parent.
The other important part is the preserveAspectRatio
attribute, which asks the DOM to calculate a specific ratio for the SVG element at any size. In this case, we've asked for uniform scaling with xMinYMin
and we've asked to keep the view box smaller than the viewport with meet
.
You can also see that we've explicitly set the height and width of the <image>
to match the view box. This is important as it ensures they are responsive together.
The next step is to draw the SVG elements we want within the same view box.
<div id="greenhouse">
<svg viewBox="0 0 1280 720"
preserveAspectRatio="xMinYMin meet">
<image width="1280"
height="720" xlink:href="https://dkli3tbfz4zj3.cloudfront.net/all/202006_SaladDays/images/greenhouse.jpg">
</image>
<circle cx="220" cy="320" r="40" stroke="#d4ffde"
stroke-width="3" fill="none" />
<circle cx="320" cy="520" r="70" stroke="#d4ffde"
stroke-width="3" fill="none" />
<circle cx="750" cy="380" r="60" stroke="#d4ffde"
stroke-width="3" fill="none" />
</svg>
</div>
I've chosen to use circles with no fill and a 3px stroke-width. Explaining how to draw SVGs is outside the scope of this post and there is a plethora of information available on the internet describing the various shapes. I used Chris Coyer's The SVG Path Syntax: An Illustrated Guide to design the paths described later.
Using the <circle>
element, we describe the coordinates inside our view box using cx
for the x-axis and cy
for the y-axis. Because this circle element is inside the same parent SVG, we are using the same view box for both the image and the circles. Voila, the circles scale perfectly with the image. If I wanted to, I could also place an anchor tag in exactly the same fashion to match my circles.
But how do we draw the SVG outside the view box?
I'm glad you asked. I wanted to include descriptive textboxes below the image that explained what was in each circle. So I needed a way to extend the SVG graphic outside this view box. Solution? Place another view box on top of this view box and give it a higher z-index
. View boxes for everyone!
<style>
#lines {
z-index: 3;
}
</style>
<svg id="lines" viewBox="0 0 1280 1280"
preserveAspectRatio="xMinYMin meet">
<path stroke="#d4ffde" stroke-width="3" fill="none" d="
M 130,820
L 130,320
l 50,0
"/>
<path stroke="#d4ffde" stroke-width="3" fill="none" d="
M 620,820
L 620,520
l -230,0
"/>
<path stroke="#d4ffde" stroke-width="3" fill="none" d="
M 1100,820
L 1100,380
l -290,0
"/>
</svg>
So I've added a new SVG element and defined its view box as being double the height of the image view box. I won't utilize all this space but it's an easy aspect ratio to understand.
Using the previously mentioned guide, I carefully drew paths from each circle and anchored them below the image and its SVG view box.
And of course, we include the CSS z-index
to ask this element to be on top.
Finally, I added some descriptive boxes below. The whole thing looks like:
<style>
#greenhouse {
position: relative;
width: 100%;
vertical-align: middle;
margin: 0;
overflow: hidden;
}
#greenhouse svg {
display: inline-block;
position: absolute;
top: 0;
left: 0;
}
#lines {
z-index: 3;
}
#greenhouse-details {
display: flex;
margin-top: 60%;
flex-wrap: wrap;
justify-content: center;
}
.narrow-text {
background-color: #d4ffde;
padding: 3% 5%;
margin: 3%;
flex-basis: 25%;
}
@media only screen and (max-width : 480px) {
.narrow-text {
flex-basis: 100%;
}
}
</style>
<div id="greenhouse">
<svg id="lines" viewBox="0 0 1280 1280"
preserveAspectRatio="xMinYMin meet">
<path stroke="#d4ffde" stroke-width="3" fill="none" d="
M 130,820
L 130,320
l 50,0
"/>
<path stroke="#d4ffde" stroke-width="3" fill="none" d="
M 620,820
L 620,520
l -230,0
"/>
<path stroke="#d4ffde" stroke-width="3" fill="none" d="
M 1100,820
L 1100,380
l -290,0
"/>
</svg>
<svg viewBox="0 0 1280 720"
preserveAspectRatio="xMinYMin meet">
<image width="1280"
height="720" xlink:href="https://dkli3tbfz4zj3.cloudfront.net/all/202006_SaladDays/images/greenhouse.jpg">
</image>
<circle cx="220" cy="320" r="40" stroke="#d4ffde"
stroke-width="3" fill="none" />
<circle cx="320" cy="520" r="70" stroke="#d4ffde"
stroke-width="3" fill="none" />
<circle cx="750" cy="380" r="60" stroke="#d4ffde"
stroke-width="3" fill="none" />
</svg>
<div id="greenhouse-details">
<div class="narrow-text">
<p>Whip up a delicious salad</p>
</div>
<div class="narrow-text">
<p>Cuddle up to Marguerit’s Snow Stalker companion</p>
</div>
<div class="narrow-text">
<p>Learn how to farm new, unusual plants</p>
</div>
</div>
</div>
And there you have it! Responsive SVG circles overlaid on an image, with some SVG paths extending beyond the image.
Parting thoughts:
- The color of the textboxes matched the path color perfectly. So, you couldn't see that the paths were actually on top of the boxes. With some due-diligence, it would be best to calculate where the box meets the path and end the path exactly there. Since we're working with fixed ratio, this is simpler then it sounds.
- I always recommend that the parent element (not pictured here) has a
max-width
property to prevent the wild things one might see on extra wide screens. - The circles could have been inside the same svg view box as the lines. It was simply a result of process that they are not in this example.
- On mobile, you would also want to take this idea a step further and calculate the end points of the path to meet each textbox as it changes to a 100% width. For the sake of brevity, I'll leave that particular challenge to you, dear reader.
Thanks for reading my first dev.to post!