Boba Chronicles Pt. I - SVGs

For the past half year I've been tracking my bubble tea consumption. Is it to shame myself into drinking less boba? Maybe. Is this madness? Probably. It's been an interesting exercise, at the very least, and is one that I'm likely to continue into the future.

The tracking was done in a Google Sheets. While Sheets allows me to do nifty things like column sums for amount spent and conditional cell fill colours, it's lacking in some other areas. The Sheets mobile app is awkward to use on the go, which is when I want to make new entries. Also, after a few months I had enough data to want to make some more interesting analyses about my boba habits.

Since I am conveniently in between jobs and have time for a side project... I'm going to make an app (or begin making one) where I can track my boba consumption and look at all the data in a pretty and meaningful way.

Let's break it down

There will be many components to this project: new boba entry submission form, different visualizations, and the backend (??? how does one web ???).

First, we draw

Pretty visuals is going to be a huge component of this for me. The idea is to have at least two main views: a build-a-boba page and a history page.

The build-a-boba page is going to be an interactive page with two panels. One will be a large rendering of a boba drink think you're trying to build, and the other will be a form with drop downs, sliders, etc. that can be used to customize the drink. I did a couple sketches to get a feel for what I might want the large drink to look like. While I'd love to have different cup types, simpler is better in this case, I think.

For the history page, I want to be able to see all my logged drinks I've had in the past year like a mosaic. These illustrations should be simpler. The focus should really be on overall impact rather than rendering individual drinks. To figure out what this might look like I made some more sketches.

I am pretty happy with how it turned out! I think there's a lot more that can happen with this simple boba grid concept. I want to refine it into a pattern that can be printed on fabric and sew a boba dress! Also, just imagine, a screen printed t-shirt with your unique boba history. 😻

Next, we SVG

Well actually, next I opened up Sketch and recreated my previous boba sketch with vector shapes. The goal is to be able to procedurally create SVG shapes via JavaScript, so the SVGs needs to be neat to make SVG element creation easier later.

After getting the initial shapes down, I selected all my shapes, right clicked, smashed Copy SVG Code, ctrl+p'd into Sublime, and boom, I've got SVG code. Except... what I got out of it didn't look very nice at all.

boba.html:

<svg width="260px" height="571px" viewBox="0 0 260 571" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="Artboard" transform="translate(-520.000000, -90.000000)"> <g id="simple2" transform="translate(520.000000, 90.000000)"> <polygon id="cup" fill="#F4E7D8" points="11 171 251 171 231 571 31 571"></polygon> <path d="M81,244 C143.833333,244 144,271 217,271 C228.347138,271 237.85799,270.347635 246.092153,269.245712 L231,571 L31,571 L15.350617,258.002927 L20.3677593,255.930887 C35.5227936,249.74251 52.02262,244 81,244 Z" id="tea" fill="#F0D4B5"></path> <rect id="lid" fill="#3B3B3B" x="0" y="151" width="260" height="12"></rect> <polygon id="straw" fill="#3B3B3B" transform="translate(124.979227, 271.370018) scale(-1, 1) rotate(-6.000000) translate(-124.979227, -271.370018) " points="106.979227 1.37001797 142.979227 1.37001797 142.979227 541.370018 106.979227 527.330018"></polygon> <g id="bubbles" transform="translate(47.000000, 452.000000)" fill="#3B3B3B"> <circle id="Oval" cx="40" cy="54" r="18"></circle> <circle id="Oval" cx="84" cy="54" r="18"></circle> <circle id="Oval" cx="128" cy="54" r="18"></circle> <circle id="Oval" cx="18" cy="18" r="18"></circle> <circle id="Oval" cx="62" cy="18" r="18"></circle> <circle id="Oval" cx="106" cy="18" r="18"></circle> <circle id="Oval" cx="150" cy="18" r="18"></circle> <circle id="Oval" cx="18" cy="90" r="18"></circle> <circle id="Oval" cx="62" cy="90" r="18"></circle> <circle id="Oval" cx="106" cy="90" r="18"></circle> <circle id="Oval" cx="150" cy="90" r="18"></circle> </g> </g> </g> </g> </svg>

There are a lot of SVG optimizers out there that I could have used to make this a little more friendly, but I figured I could just use this as a starting point and rewrite the SVG from scratch. Hand coding SVG is pretty similar to using Sketch. I focused on making all the values in point strings nice and being mindful about how I use transforms to place elements.

boba.html:

<svg viewBox="0 0 260 600" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <polygon id="cup" fill="#F4E7D8" transform="translate(130, 150)" points="-120 0 120 0 100 430 -100 430" /> <polygon id="straw" fill="#7FACC5" transform="translate(112, 0)" points="0 0 36 0 36 545 0 560" /> <rect id="lid" fill="#3B3B3B" x="0" y="144" width="260" height="16" /> <defs> <clipPath id="cupClip"> <polygon points="-120 20 120 20 100 450 -100 450" /> </clipPath> </defs> <path id="liquid" fill="#F0D4B5" transform="translate(130, 150)" clip-path="url(#cupClip)" d=" M -130 50, q 75 -20, 130 0 q 75 20, 130 0 l 0 400 l -260 0" /> <g id="toppings" fill="#3B3B3B" transform="translate(130, 556)"> <circle cx="-60" cy="0" r="18" /> <circle cx="-20" cy="0" r="18" /> <circle cx="20" cy="0" r="18" /> <circle cx="60" cy="0" r="18" /> <circle cx="-40" cy="-36" r="18" /> <circle cx="0" cy="-36" r="18" /> <circle cx="40" cy="-36" r="18" /> <circle cx="-60" cy="-72" r="18" /> <circle cx="-20" cy="-72" r="18" /> <circle cx="20" cy="-72" r="18" /> <circle cx="60" cy="-72" r="18" /> </g> </svg>

Note: SVG tags are self closing, which means that <circle ...></circle> can be written as <circle ... />.

Figuring out how to create the actual 'liquid' portion of the boba was interesting. I wanted to add an interesting 'wave' to the top of the liquid, but I also wanted to avoid hard coding both the liquid shape and the cup shape separately. Instead the liquid shape should rely directly on the cup shape. This future proofs against different cup shapes in the future. To do this, I want get the intersection of the cup and basic liquid shapes.

The basic liquid shape is created with an enclosed path. Paths allow you to draw multiple segments of curves. The segment is a quadratic bézier curve to create a wave on the surface of the liquid (shown in purple below). The rest of the segments are simple lines enclose the rest of the area.

Note: The shape is clipped before the same transformation is applied to the new shape, so make sure the two shapes are appropriately aligned before the transformation.

When the basic liquid shape and the cup shape are superimposed on top of each other, we get this nice tapered liquid in a cup shape. Simply create a clipPath with the one of the shapes nested under it and apply it as a clip-path attribute to the other shape. Since this just gets the intersection of the two shapes, which one is used as the clip path doesn't matter.

I initially tried to use masking instead of clipping, but ran into opacity troubles. I had a light brown fill on my mask shape, which was actually applying a opacity mask to the shape as well. It turns out masking is just clipping with opacity, and since I wasn't using the opacity feature, I opted to use clipping instead.

Then, we finally get to code

Now that I have created this SVG twice from scratch, it's time to do it yet another time!!! This time with JavaScript!!!

boba.html:

<html> <head> <script src="bobaMaker.js" defer></script> </head> <body> <svg id="dynamicBoba" width="260px" viewBox="0 0 260 600" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" /> </body> </html>

bobaMaker.js:

// Get our SVG element var boba = document.getElementById("dynamicBoba"); // We're trying to recreate... // <rect id="lid" fill="#3B3B3B" x="0" y="144" width="260" height="16" /> // Create a SVG element and set every attribute individually var lid = document.createElementNS(svgNS, "rect"); lid.setAttribute("fill", "#3B3B3B"); lid.setAttribute("x", 0); lid.setAttribute("y", 144); lid.setAttribute("width", 260); lid.setAttribute("height", 16); // Append our new shape into our SVG element boba.appendChild(lid);

It's not great to have to explicitly set so many attributes over so many lines, but all of those attributes now has the potential to be ✨ dynamic ✨. I can now keep track of all my SVG elements and write callbacks to change any of their attributes.

The first thing I did after transferring my SVG HTML tags to JavaScipt was to create variables to keep track of information I was hard coding previously, like height, width, radius of pearls, etc. This way I can change the height of the cup, if need be, and not have to figure out where else I was using the cup height in some other element's transformation attributes.

I also tossed in a for loop to generate my pearls. This was actually a little tricky to do generically, since I wanted have a staggered effect for every other row of pearls.

bobaMaker.js:

// Define some useful variables var boba = { width: 260, height: 600, svg: null, } var pearls = { size: 32, padding: 4, rows: 3, columns: 4, svg: null, group: null, } boba.svg = document.getElementById("dynamicBoba"); // Create a group to hold all our pearls pearls.group = document.createElementNS(svgNS, "g"); // Place the group at the centre of the cup and some amount above the bottom of the cup pearls.setAttribute("transform", `translate( ${boba.width/2}, ${boba.height - pearls.size/2 - pearls.padding*2})` ); // Create a base pearl shape pearls.svg = document.createElementNS(svgNS, "circle"); pearls.svg.setAttribute("r", pearls.size); pearls.svg.setAttribute("cx", "0"); pearls.svg.setAttribute("cy", "0"); // For loop magic for (let y = 0; y < rows; y++) { // Stagger the toppings by making even rows have one less pearl let cols = pearls.columns; if (y % 2 != 0) { cols = pearls.columns - 1; } for (let x = -1. * (cols/2 -0.5); x <= (cols/2 -0.5) ; x+=1.) { let pearl = pearls.svg.cloneNode(); let localX = x * (pearls.padding + pearls.size); let localY = -1 * y * pearls.size; pearl.setAttribute("transform", `translate(${localX}, ${localY})`); toppings.appendChild(pearl); } } // Throw our pearls group onto the SVG boba.appendChild(pearls.group);

Picking a good local zero (transformation reference point) is incredibly important here!

And voila. I now have a boba SVG that's ready to become dynamic.

References