<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Chee Aun 🦄</title>
    <description>The latest articles on DEV Community by Chee Aun 🦄 (@cheeaun).</description>
    <link>https://dev.to/cheeaun</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F109535%2F90165112-7e9c-4c84-a214-d4590bf6dee0.jpg</url>
      <title>DEV Community: Chee Aun 🦄</title>
      <link>https://dev.to/cheeaun</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cheeaun"/>
    <language>en</language>
    <item>
      <title>Replicating 3D Trees from Apple Maps</title>
      <dc:creator>Chee Aun 🦄</dc:creator>
      <pubDate>Fri, 05 Nov 2021 00:00:00 +0000</pubDate>
      <link>https://dev.to/cheeaun/replicating-3d-trees-from-apple-maps-3kn</link>
      <guid>https://dev.to/cheeaun/replicating-3d-trees-from-apple-maps-3kn</guid>
      <description>&lt;p&gt;On June 2021, Apple’s &lt;a href="https://developer.apple.com/wwdc21/" rel="noopener noreferrer"&gt;WWDC21&lt;/a&gt; &lt;a href="https://developer.apple.com/videos/play/wwdc2021/101/" rel="noopener noreferrer"&gt;event&lt;/a&gt; announced the all-new &lt;a href="https://www.apple.com/maps/" rel="noopener noreferrer"&gt;Apple Maps&lt;/a&gt;, introducing an amazing &lt;a href="https://techcrunch.com/2021/06/07/apple-maps-upgrade-brings-more-detailed-maps-transit-features-ar-view-and-more/" rel="noopener noreferrer"&gt;new three-dimensional city experience&lt;/a&gt; with unprecedented detail for elevation, neighbourhoods, buildings, trees, and road features.&lt;/p&gt;

&lt;p&gt;It looks like this.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fsoftware%2Fwwdc21-apple-maps-elevation-trees-buildings%402x.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fsoftware%2Fwwdc21-apple-maps-elevation-trees-buildings%402x.jpg" alt="WWDC21 keynote showing the new Apple Maps with details for elevation, trees and buildings"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There’s a lot going on here, but I’m going to focus on only one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trees&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Two years ago, I wrote “&lt;a href="https://cheeaun.com/blog/2019/07/next-level-visualizations-exploretrees-sg/" rel="noopener noreferrer"&gt;Next-level visualizations with ExploreTrees.SG&lt;/a&gt;”. It has details on how I try to render three-dimensional trees on a map and my experience along the way. I’m not an expert in this topic but I had my fair share of trials and errors while working on it.&lt;/p&gt;

&lt;p&gt;Here’s a comparison of my attempt and Apple’s:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fsoftware%2Ftrees-comparison-exploretrees-sg-apple-maps.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fsoftware%2Ftrees-comparison-exploretrees-sg-apple-maps.jpg" alt="Trees comparison between ExploreTrees.SG and Apple Maps"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;They look different but I have to admit, Apple’s one looks &lt;em&gt;so good&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Let me recap on how I came up with my attempt in the first place:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The trees have to look &lt;strong&gt;semi-realistic&lt;/strong&gt;. They cannot be too realistic because they’ll never look like the real thing anyway. Real-world &lt;a href="https://en.wikipedia.org/wiki/Tree_measurement" rel="noopener noreferrer"&gt;tree measurement&lt;/a&gt; is actually &lt;em&gt;very&lt;/em&gt; complicated.&lt;/li&gt;
&lt;li&gt;There are &lt;em&gt;way&lt;/em&gt; too many species of trees with different crown shapes and sizes. I settled on a simple sphere geometry with leaves texture. For some reason, I was keen on making the crown see-through (cracks between leaves) because I feel that’s how trees should look like.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Looking at Apple Maps’ trees, I was kind of surprised they also use a sphere &lt;em&gt;but&lt;/em&gt; with random “bumps”, which gives some form of irregularity. The trunk is also slightly curved and they even add a drop shadow.&lt;/p&gt;

&lt;p&gt;I’m so fascinated by this level of detail and decided to study how Apple is designing these trees.&lt;/p&gt;

&lt;p&gt;So far, there are three types of trees/crowns.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fsoftware%2Fapple-maps-tree-crowns-deciduous-palm-evergreen.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fsoftware%2Fapple-maps-tree-crowns-deciduous-palm-evergreen.jpg" alt="Apple Maps‘ tree crowns; Deciduous, Palm and Evergreen"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Obviously Apple didn’t tell us exactly how many and why there are three types. I kind of looked around and deduced that on my own. I could be wrong.&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;weeks&lt;/strong&gt;, I’ve been thinking &lt;em&gt;really&lt;/em&gt; hard why, and one day I realised that these are analogous to the trees in the emoji list:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;🌳 &lt;a href="https://emojipedia.org/deciduous-tree/" rel="noopener noreferrer"&gt;Deciduous tree&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🌴 &lt;a href="https://emojipedia.org/palm-tree/" rel="noopener noreferrer"&gt;Palm tree&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🌲 &lt;a href="https://emojipedia.org/evergreen-tree/" rel="noopener noreferrer"&gt;Evergreen tree&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For those emoji fanatics, it’s also known that there &lt;em&gt;are&lt;/em&gt; two more trees; 🎄 &lt;a href="https://emojipedia.org/christmas-tree/" rel="noopener noreferrer"&gt;Christmas tree&lt;/a&gt; and 🎋 &lt;a href="https://emojipedia.org/tanabata-tree/" rel="noopener noreferrer"&gt;Tanabata tree&lt;/a&gt;. But we all know they are &lt;em&gt;different&lt;/em&gt; 😉.&lt;/p&gt;

&lt;p&gt;Wow, I find this pretty smart. In my own attempt, I was scratching my head on how to diversify the crowns based on the tree families or species. Perhaps I was thinking too much, looking at this diagram from “&lt;a href="https://www.toppr.com/guides/science/forest-our-lifeline/structure-of-forest/" rel="noopener noreferrer"&gt;Structure of Forest: Crown of Tree, Types, Canopy, Videos and Examples&lt;/a&gt;”.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Ffigures%2Fdiagram%2Fcrown-of-tree.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Ffigures%2Fdiagram%2Fcrown-of-tree.jpg" alt="Crown of tree diagram, showing Pyramidal, Full-crowned, Vase, Fountain, Spreading, Layered, Columnar and Weeping"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now that I see Apple Maps showing only three types of trees, things seem &lt;em&gt;easier&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;I mean, why are there only three types of trees in the emoji list in the first place, right? Hmm, I think I’ll leave that question for another day 😜.&lt;/p&gt;

&lt;h2&gt;
  
  
  Revisiting the code
&lt;/h2&gt;

&lt;p&gt;I don’t update my &lt;a href="https://github.com/cheeaun/exploretrees-sg" rel="noopener noreferrer"&gt;ExploreTrees.SG project&lt;/a&gt; frequently. I kind of revisit it once a year, while juggling with my other projects, mainly to update the dataset and code dependencies.&lt;/p&gt;

&lt;p&gt;I looked at the repository, created a separate folder and started working on it.&lt;/p&gt;

&lt;p&gt;On &lt;a href="https://twitter.com/cheeaun/status/1444681063792451596" rel="noopener noreferrer"&gt;3 October,&lt;/a&gt; I showed a sneak peek:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fweb%2Ftrees-3d-green-sphere-map-1%402x.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fweb%2Ftrees-3d-green-sphere-map-1%402x.jpg" alt="3D trees with green sphere on a map"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fweb%2Ftrees-3d-green-sphere-map-2%402x.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fweb%2Ftrees-3d-green-sphere-map-2%402x.jpg" alt="3D trees with green sphere on a map, again"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I made a new map style on &lt;a href="https://studio.mapbox.com/" rel="noopener noreferrer"&gt;Mapbox Studio&lt;/a&gt; that matches Apple Maps’ looks. It’s pretty close but not there yet. Map enthusiasts would notice the change for the roads. It’s grey instead of white, yellow or orange 😉.&lt;/p&gt;

&lt;p&gt;I re-used the code and styled the tree crown with green colour, instead of the leaves texture.&lt;/p&gt;

&lt;p&gt;The lighting and shadows look a little odd, so I made &lt;a href="https://twitter.com/cheeaun/status/1445043038678441994" rel="noopener noreferrer"&gt;some adjustments&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fweb%2Ftrees-3d-green-sphere-map-3%402x.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fweb%2Ftrees-3d-green-sphere-map-3%402x.jpg" alt="3D trees with green sphere on a map, with better lighting and shadow"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fweb%2Ftrees-3d-green-sphere-map-4%402x.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fweb%2Ftrees-3d-green-sphere-map-4%402x.jpg" alt="Closed up shot of 3D trees with green sphere on a map, with better lighting and shadow"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Slightly better.&lt;/p&gt;

&lt;p&gt;I wanted to try making the sphere “bumpy”, like the one in Apple Maps.&lt;/p&gt;

&lt;p&gt;Using &lt;a href="https://spline.design/" rel="noopener noreferrer"&gt;Spline&lt;/a&gt;, I manage to &lt;a href="https://twitter.com/cheeaun/status/1444681069039550468" rel="noopener noreferrer"&gt;make a 3D model&lt;/a&gt; of a sphere with &lt;a href="https://docs.spline.design/c6c113a441e74f45bb6c415b4de71b8e" rel="noopener noreferrer"&gt;displacements&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fsoftware%2Fsphere-displacements-3d-model-spline%402x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fsoftware%2Fsphere-displacements-3d-model-spline%402x.png" alt="3D model of a sphere with displacements, made with Spline"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Unfortunately on Spline, when exporting this to a GLTF model file, it becomes a sphere &lt;em&gt;without&lt;/em&gt; displacements 😑. Maybe these displacements are just “decorations” on top of the model and not really part of it. I’m not sure since I’m not a 3D designer 😅.&lt;/p&gt;

&lt;p&gt;Later on, I figured out that I could make my own displacements by modifying the model itself. Spline has a feature where once a 3D geometry is added, pressing "&lt;a href="https://docs.spline.design/97009e42fe28419d84ac6d580eae8802" rel="noopener noreferrer"&gt;Smooth &amp;amp; Edit&lt;/a&gt;" will convert it into a &lt;a href="https://en.wikipedia.org/wiki/Subdivision_surface" rel="noopener noreferrer"&gt;subdivision surface&lt;/a&gt;. It’s pretty darn powerful because I can edit a surface, line or even a point of &lt;em&gt;any&lt;/em&gt; surface, drag them, and split them into multiple segments! 🤯&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fsoftware%2Fsphere-3d-model-subdivision-surface-spline%402x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fsoftware%2Fsphere-3d-model-subdivision-surface-spline%402x.png" alt="3D sphere model converted into a subdivision surface, made with Spline"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The end result looks &lt;strong&gt;amazing&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fweb%2Ftrees-3d-green-sphere-displacement-map-1%402x.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fweb%2Ftrees-3d-green-sphere-displacement-map-1%402x.jpg" alt="3D trees with green sphere and displacements on a map"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fweb%2Ftrees-3d-green-sphere-displacement-map-2%402x.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fweb%2Ftrees-3d-green-sphere-displacement-map-2%402x.jpg" alt="Closed up shot of 3D trees with green sphere and displacements on a map"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This gives me goosebumps 😬.&lt;/p&gt;

&lt;p&gt;To avoid the trees looking too similar, the crowns are initially rotated randomly with JavaScript &lt;code&gt;Math.random&lt;/code&gt;, but it wasn’t a good solution because every time it’s re-rendered, the crowns’ rotation change.&lt;/p&gt;

&lt;p&gt;So, the solution is that they are &lt;strong&gt;deterministically&lt;/strong&gt; random, rotated based on the tree ID (string of integers). It’s not complicated; the rotation in degrees is calculated as &lt;code&gt;(id.slice(-1) / 9) * 180&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I compared them with Apple Maps, side-by-side.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fsoftware%2Fcomparison-trees-model-exploretrees-sg-apple-maps%402x.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fsoftware%2Fcomparison-trees-model-exploretrees-sg-apple-maps%402x.jpg" alt="Comparison of trees models between ExploreTrees.SG and Apple Maps"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Left is my attempt on Mapbox tiles. Right is Apple Maps.&lt;/p&gt;

&lt;p&gt;It’s pretty close. Maybe the sphere should have more bumps, but I don’t plan to make them &lt;em&gt;too&lt;/em&gt; similar one-to-one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Launching a mini-site
&lt;/h2&gt;

&lt;p&gt;After spending time on this, I wanted to share my work to the public.&lt;/p&gt;

&lt;p&gt;But I have a dilemma here.&lt;/p&gt;

&lt;p&gt;ExploreTrees.SG was initially built to be an immersive 2D-based visualisation site for all trees in Singapore. 3D-based trees were an afterthought that I fancied and sort of retrofitted in it.&lt;/p&gt;

&lt;p&gt;Having both 2D and 3D mode on a single map or page proves to be quite confusing.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;2D is great for visualisations. It’s easier to navigate and understand. 3D negates that advantage due much more complicated panning and rotation gestures with more overwhelming visuals.&lt;/li&gt;
&lt;li&gt;3D mode looks &lt;em&gt;really&lt;/em&gt; cool and gives a spatial overview of how the trees look like in real life. However, it’ll only work best when zoomed in. From a visualisation perspective, it’s harder to grasp the full picture. It’s also more taxing on the visitor’s computer resources.&lt;/li&gt;
&lt;li&gt;Discovery of the 3D mode feature hasn’t been great. There’s a ‘3D’ button on the right side of the map but I guess most people don’t click it. Users can trigger 3D mode by zooming in and tilting the map but that requires quite a few gestures instead of simple clicks. 3D mode is also only available on more powerful desktop browsers, mainly due to the way it is coded (loading all 500K trees at once).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I decided to create a separate mini-site that strives to do only one thing and do it well.&lt;/p&gt;

&lt;p&gt;On 5 October, I &lt;a href="https://twitter.com/cheeaun/status/1445384655201574912" rel="noopener noreferrer"&gt;launched &lt;strong&gt;ExploreTrees.sg/3D&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;



&lt;p&gt;The launch &lt;a href="https://www.linkedin.com/feed/update/urn:li:activity:6851152016014737409/" rel="noopener noreferrer"&gt;gathered quite&lt;/a&gt; &lt;a href="https://www.reddit.com/r/singapore/comments/q7v0gr/i_rendered_trees_in_sg_to_look_like_the_one_in/" rel="noopener noreferrer"&gt;a few attention&lt;/a&gt; 🤩.&lt;/p&gt;

&lt;p&gt;The site is also &lt;a href="https://twitter.com/cheeaun/status/1446280347419774978" rel="noopener noreferrer"&gt;featured&lt;/a&gt; on &lt;a href="https://googlemapsmania.blogspot.com/2021/10/mapping-trees-in-3d.html" rel="noopener noreferrer"&gt;Maps Mania: Mapping Trees in 3D&lt;/a&gt;!&lt;/p&gt;

&lt;h2&gt;
  
  
  Post-launch improvements
&lt;/h2&gt;

&lt;p&gt;The amazing part here is that I manage to make it work on a mobile browser.&lt;/p&gt;

&lt;p&gt;Previously, this was deemed impossible because the site is loading 500+K trees in the browser and causing memory pressure issues.&lt;/p&gt;

&lt;p&gt;Here’s a quote from &lt;a href="https://deck.gl/docs/developer-guide/performance" rel="noopener noreferrer"&gt;deck.gl’s documentation&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Modern phones (recent iPhones and higher-end Android phones) are surprisingly capable in terms of rendering performance, but are considerably more sensitive to memory pressure than laptops, resulting in browser restarts or page reloads. They also tend to load data significantly slower than desktop computers, so some tuning is usually needed to ensure a good overall user experience on mobile.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For this mini-site, the page doesn't load all 500+K trees 😉.&lt;/p&gt;

&lt;p&gt;The trees data are loaded via &lt;a href="https://docs.mapbox.com/help/glossary/tileset/" rel="noopener noreferrer"&gt;Mapbox tile sets&lt;/a&gt;. Technically they are vector tiles, broken up into a uniform grid of square tiles at multiple zoom levels. They can be created by converting a GeoJSON file into a &lt;code&gt;mbtiles&lt;/code&gt; file, using &lt;a href="https://github.com/mapbox/tippecanoe" rel="noopener noreferrer"&gt;&lt;code&gt;tippiecanoe&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;After that, the data is filtered with Mapbox GL JS’s &lt;a href="https://docs.mapbox.com/mapbox-gl-js/api/map/#map#queryrenderedfeatures" rel="noopener noreferrer"&gt;&lt;code&gt;queryRenderedFeatures&lt;/code&gt;&lt;/a&gt; function.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Returns an array of &lt;a href="http://geojson.org/" rel="noopener noreferrer"&gt;GeoJSON&lt;/a&gt; &lt;a href="https://tools.ietf.org/html/rfc7946#section-3.2" rel="noopener noreferrer"&gt;Feature objects&lt;/a&gt; representing visible features that satisfy the query parameters.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This can be used to get a list of trees that’s only visible in the current viewport. Think of it like the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API" rel="noopener noreferrer"&gt;Intersection Observer API&lt;/a&gt; but for maps.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Ffigures%2Fmaps%2Ftrees-rendered-not-rendered-viewport-loaded-tiles.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Ffigures%2Fmaps%2Ftrees-rendered-not-rendered-viewport-loaded-tiles.svg" alt="Map showing trees rendered and not rendered, filtered by the viewport in relative to the loaded-tiles"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, loading a number of trees based on viewport boundary still depends on the map zoom level. When zoomed-in, number of trees could be around 1K and that’s fine. When zoomed-out, number of trees could be over 5K+ and will cause performance issues.&lt;/p&gt;

&lt;p&gt;It needs a form of gradual appearance of trees when zooming in.&lt;/p&gt;

&lt;p&gt;Observing how Apple Maps work, I came up with a solution — &lt;strong&gt;align the zoom levels with the height of trees&lt;/strong&gt;. The logic goes like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zoom level 0 to 15: don’t show trees.&lt;/li&gt;
&lt;li&gt;Zoom level 15: start showing trees with height more than 24 meters.&lt;/li&gt;
&lt;li&gt;Zoom level 19: show &lt;strong&gt;all&lt;/strong&gt; trees (more than 0 meters).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Transitioning between zoom level 15 and 19, the amount of trees shown will be based on height value of 24 meters to 0 meters. In other words, as the user zooms in, the taller trees will appear first and gradually surface the shorter trees. This makes sense because there’s no point rendering short trees when they’re barely visible on lower zoom levels.&lt;/p&gt;

&lt;p&gt;The math and code look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;minZoom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;maxZoom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;minHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;maxHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;zoom&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;minZoom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;maxZoom&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;minZoom&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;maxHeight&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;minHeight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;minHeight&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trees&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queryRenderedFeatures&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;geometry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;height_est&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;trees&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;minHeight&lt;/code&gt; value is hardcoded to 24 because I already know the highest value from the dataset, so this may not work for other datasets. The &lt;code&gt;geometry&lt;/code&gt; variable is the viewport boundary as mentioned above.&lt;/p&gt;

&lt;p&gt;Here’s a video of how the experience looks like:&lt;/p&gt;



&lt;h2&gt;
  
  
  What about the other crowns?
&lt;/h2&gt;

&lt;p&gt;The map was showing only deciduous trees. There are still palm trees and evergreen trees left to be done.&lt;/p&gt;

&lt;p&gt;Actually, how do I know if a tree is palm tree or evergreen tree?&lt;/p&gt;

&lt;p&gt;I researched and figured out that the family for palm trees is &lt;a href="https://en.wikipedia.org/wiki/Arecaceae" rel="noopener noreferrer"&gt;Arecaceae (Palmae)&lt;/a&gt;. For &lt;a href="https://en.wikipedia.org/wiki/Evergreen" rel="noopener noreferrer"&gt;Evergreen&lt;/a&gt; trees, there are &lt;strong&gt;14 families&lt;/strong&gt;; Araucariaceae, Cupressaceae, Pinaceae, Podocarpaceae, Taxaceae, Cyatheaceae, Aquifoliaceae, Fagaceae, Oleaceae, Myrtaceae, Arecaceae, Lauraceae, Magnoliaceae, and Cycadaceae. 😵&lt;/p&gt;

&lt;p&gt;At this point I realised I’m using these terms differently. For me, they are meant to differentiate the crown shapes, because that’s all I care about for this 3D rendering.&lt;/p&gt;

&lt;p&gt;Quoted from &lt;a href="https://en.wikipedia.org/wiki/Deciduous" rel="noopener noreferrer"&gt;Wikipedia: Deciduous&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In the fields of &lt;a href="https://en.wikipedia.org/wiki/Horticulture" rel="noopener noreferrer"&gt;horticulture&lt;/a&gt; and &lt;a href="https://en.wikipedia.org/wiki/Botany" rel="noopener noreferrer"&gt;botany&lt;/a&gt;, the term &lt;strong&gt;&lt;em&gt;deciduous&lt;/em&gt;&lt;/strong&gt; (&lt;a href="https://en.wikipedia.org/wiki/Help:IPA/English" rel="noopener noreferrer"&gt;/dɪˈsɪdjuːəs/&lt;/a&gt;; &lt;a href="https://en.wikipedia.org/wiki/American_English" rel="noopener noreferrer"&gt;US&lt;/a&gt;: &lt;a href="https://en.wikipedia.org/wiki/Help:IPA/English" rel="noopener noreferrer"&gt;/dɪˈsɪdʒuəs/&lt;/a&gt;)[&lt;a href="https://en.wikipedia.org/wiki/Deciduous#cite_note-1" rel="noopener noreferrer"&gt;1]&lt;/a&gt; means "falling off at maturity"[&lt;a href="https://en.wikipedia.org/wiki/Deciduous#cite_note-2" rel="noopener noreferrer"&gt;2]&lt;/a&gt; and "tending to fall off",[&lt;a href="https://en.wikipedia.org/wiki/Deciduous#cite_note-3" rel="noopener noreferrer"&gt;3]&lt;/a&gt; in reference to &lt;a href="https://en.wikipedia.org/wiki/Tree" rel="noopener noreferrer"&gt;trees&lt;/a&gt; and &lt;a href="https://en.wikipedia.org/wiki/Shrub" rel="noopener noreferrer"&gt;shrubs&lt;/a&gt; that seasonally shed &lt;a href="https://en.wikipedia.org/wiki/Leaf" rel="noopener noreferrer"&gt;leaves&lt;/a&gt;, usually in the &lt;a href="https://en.wikipedia.org/wiki/Autumn" rel="noopener noreferrer"&gt;autumn&lt;/a&gt;; to the shedding of &lt;a href="https://en.wikipedia.org/wiki/Petal" rel="noopener noreferrer"&gt;petals&lt;/a&gt;, after flowering; and to the shedding of ripe &lt;a href="https://en.wikipedia.org/wiki/Fruit" rel="noopener noreferrer"&gt;fruit&lt;/a&gt;. The &lt;a href="https://en.wikipedia.org/wiki/Antonym" rel="noopener noreferrer"&gt;antonym&lt;/a&gt; of &lt;em&gt;deciduous&lt;/em&gt; in the botanical sense is &lt;a href="https://en.wikipedia.org/wiki/Evergreen" rel="noopener noreferrer"&gt;evergreen&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The last sentence caught my attention for some reason.&lt;/p&gt;

&lt;p&gt;Okay, the terms don’t relate to the crown shapes at all. In fact, some evergreen trees’s crowns don’t even look like a Christmas tree 🌲🎄. Even more surprising to me, one of the families of Evergreen trees is Arecaceae, which is a 🌴 palm tree! 🤯&lt;/p&gt;

&lt;p&gt;To be clear, here’s a Venn diagram of how I understand this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Ffigures%2Fdiagram%2Fdeciduous-evergreen-palm-venn-diagram.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Ffigures%2Fdiagram%2Fdeciduous-evergreen-palm-venn-diagram.svg" alt="Venn diagram of Deciduous, Evergreen and Palm trees. Deciduous circle is separated from Evergreen circle. Palm circle is inside Evergreen circle."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Anyway, I decided to simplify the scope. According to &lt;a href="https://emojipedia.org/evergreen-tree/" rel="noopener noreferrer"&gt;Emojipedia&lt;/a&gt;, the meaning of the 🌲 Evergreen tree emoji is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An evergreen tree, which keeps green leaves all year round. Depicted as a tall, dark green, cone-shaped tree with shaggy, layered leaves, as a &lt;a href="https://en.wikipedia.org/wiki/Pine" rel="noopener noreferrer"&gt;pine&lt;/a&gt; or &lt;a href="https://en.wikipedia.org/wiki/Fir" rel="noopener noreferrer"&gt;fir&lt;/a&gt;, showing a brown trunk.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Looking at Wikipedia pages of &lt;a href="https://en.wikipedia.org/wiki/Pine" rel="noopener noreferrer"&gt;Pine&lt;/a&gt; and &lt;a href="https://en.wikipedia.org/wiki/Fir" rel="noopener noreferrer"&gt;Fir&lt;/a&gt;, both of them falls under the &lt;a href="https://en.wikipedia.org/wiki/Pinaceae" rel="noopener noreferrer"&gt;Pinaceae&lt;/a&gt; family. So let’s do this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Evergreen trees = Pinaceae&lt;/li&gt;
&lt;li&gt;Palm trees = Arecaceae (Palmae)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it.&lt;/p&gt;

&lt;p&gt;Next step would be the 3D models 😅.&lt;/p&gt;

&lt;p&gt;Initially, I contemplated a lot on whether I should continue doing this, but after &lt;a href="https://twitter.com/cheeaun/status/1449207768376696835" rel="noopener noreferrer"&gt;a few&lt;/a&gt; &lt;a href="https://twitter.com/cheeaun/status/1450057078190403587" rel="noopener noreferrer"&gt;practices&lt;/a&gt; &lt;a href="https://twitter.com/cheeaun/status/1450851525668925442" rel="noopener noreferrer"&gt;with Spline&lt;/a&gt;, I actually &lt;a href="https://twitter.com/cheeaun/status/1452646522294652931" rel="noopener noreferrer"&gt;did&lt;/a&gt; &lt;a href="https://twitter.com/cheeaun/status/1452675427223228420" rel="noopener noreferrer"&gt;it&lt;/a&gt;!&lt;/p&gt;





&lt;p&gt;This is one of the most exciting moments of this project 🤩.&lt;/p&gt;

&lt;p&gt;I tried rendering the palm tree first, just to see how it’ll look like in the “real world”.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fweb%2Fall-palm-trees-fort-canning-exploretrees-sg-3d%402x.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fweb%2Fall-palm-trees-fort-canning-exploretrees-sg-3d%402x.jpg" alt="All palm trees at Fort Canning, in ExploreTrees.SG 3D"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Few points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Obviously not all trees are palm trees in Fort Canning 😂&lt;/li&gt;
&lt;li&gt;The palm trees all look the “same”, despite in different sizes, because they’re not randomly rotated yet.&lt;/li&gt;
&lt;li&gt;The colours turn out looking pretty good!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sprinkled with additional conditions based on the tree family, I managed to &lt;a href="https://twitter.com/cheeaun/status/1452805162137894914" rel="noopener noreferrer"&gt;make it all work&lt;/a&gt; together 🌳🌴🌲.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fweb%2Fdeciduous-palm-evergreen-trees-bishan-park-west-exploretrees-sg-3d%402x.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fweb%2Fdeciduous-palm-evergreen-trees-bishan-park-west-exploretrees-sg-3d%402x.jpg" alt="Deciduous, palm and evergreen trees at Bishan Park West, in ExploreTrees.SG 3D"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note that the evergreen-crown trees are at the bottom in the image.&lt;/p&gt;

&lt;p&gt;I was curious, do they actually look similar to the real &lt;strong&gt;real world&lt;/strong&gt;? 🤔 Let’s compare it to Google StreetView!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First&lt;/strong&gt; &lt;a href="https://twitter.com/cheeaun/status/1452658707599298562" rel="noopener noreferrer"&gt;location check&lt;/a&gt;: Palm trees at Marina Bay Sands.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fweb%2Ftrees-comparison-palm-tree-marina-bay-sands-exploretrees-sg-3d-google-streetview%402x.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fweb%2Ftrees-comparison-palm-tree-marina-bay-sands-exploretrees-sg-3d-google-streetview%402x.jpg" alt="Trees comparison of palm trees near Marina Bay Sands, between ExploreTrees.SG 3D and Google StreetView"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Correct. 🤩&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second&lt;/strong&gt; &lt;a href="https://twitter.com/cheeaun/status/1452658707599298562" rel="noopener noreferrer"&gt;location check&lt;/a&gt;: Evergreen trees at Japanese Garden.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fweb%2Ftrees-comparison-evergreen-tree-japanese-garden-exploretrees-sg-3d-google-streetview%402x.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fweb%2Ftrees-comparison-evergreen-tree-japanese-garden-exploretrees-sg-3d-google-streetview%402x.jpg" alt="Trees comparison of evergreen trees at Japanese Garden, between ExploreTrees.SG 3D and Google StreetView"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Kind of… correct. 😜&lt;/p&gt;

&lt;p&gt;Finally, &lt;strong&gt;everything&lt;/strong&gt; rendered in &lt;a href="https://twitter.com/cheeaun/status/1452984083512913923" rel="noopener noreferrer"&gt;a single beautiful masterpiece&lt;/a&gt;. 💎&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fweb%2Fexploretrees-sg-3d-final-masterpiece%402x.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcheeaun.com%2Fblog%2Fimages%2Fscreenshots%2Fweb%2Fexploretrees-sg-3d-final-masterpiece%402x.jpg" alt="ExploreTrees.SG 3D, the final masterpiece"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;h2&gt;
  
  
  Applying the finishing touches
&lt;/h2&gt;

&lt;p&gt;After the 3D mini-site is live, I made &lt;a href="https://twitter.com/cheeaun/status/1453360064190894087" rel="noopener noreferrer"&gt;some updates&lt;/a&gt; to the &lt;a href="https://exploretrees.sg/" rel="noopener noreferrer"&gt;main site&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fetch the latest dataset

&lt;ul&gt;
&lt;li&gt;June 2020: 578,770 trees → &lt;strong&gt;Oct 2021: 696,228 trees&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Moved to a new separate repository: &lt;a href="https://dev.tocheeaun/sgtreesdata"&gt;cheeaun/sgtreesdata&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Upgraded to &lt;a href="https://parceljs.org/blog/v2/" rel="noopener noreferrer"&gt;Parcel 2&lt;/a&gt;.&lt;/li&gt;

&lt;li&gt;Changed from using MsgPack to plain CSV for the dataset. It’ll be compressed with Brotli and parsed with &lt;a href="https://www.papaparse.com/" rel="noopener noreferrer"&gt;PapaParse&lt;/a&gt; on a web worker.&lt;/li&gt;

&lt;li&gt;New custom map styles (probably won’t be noticeable by most people)&lt;/li&gt;

&lt;li&gt;Removed “Flowering” category because I &lt;a href="https://twitter.com/cheeaun/status/1447953820428877824" rel="noopener noreferrer"&gt;realised&lt;/a&gt; that this tree characteristic changes often and is actually user-contributed on &lt;a href="https://trees.sg/" rel="noopener noreferrer"&gt;Trees.sg&lt;/a&gt;.&lt;/li&gt;

&lt;li&gt;Added the ability to filter by tree family!&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;After making so many screen recordings and videos, I eventually &lt;a href="https://twitter.com/cheeaun/status/1453360064190894087" rel="noopener noreferrer"&gt;made&lt;/a&gt; a more professional-looking video to show &lt;a href="https://www.youtube.com/watch?v=xJL-YhmlWq8" rel="noopener noreferrer"&gt;what’s new on ExploreTrees.SG&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/xJL-YhmlWq8"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.reddit.com/r/singapore/comments/qhib9d/whats_new_on_exploretreessg_oct_2021/" rel="noopener noreferrer"&gt;Feedback&lt;/a&gt; was pretty good as I slowly dive into this new way of writing changelogs 😂.&lt;/p&gt;

&lt;p&gt;In fact, I had &lt;em&gt;so much fun&lt;/em&gt; browsing around the mini-site, looking at all the 3D trees, that I took some inspiration from &lt;a href="https://www.apple.com/maps/" rel="noopener noreferrer"&gt;Apple Maps web site&lt;/a&gt; and &lt;a href="https://twitter.com/cheeaun/status/1454281237007269888" rel="noopener noreferrer"&gt;made&lt;/a&gt; &lt;em&gt;another&lt;/em&gt; &lt;a href="https://www.youtube.com/watch?v=r0GbNz2cPQ0" rel="noopener noreferrer"&gt;nice video that feels like flying drone shots around some areas&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/r0GbNz2cPQ0"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Honestly I really enjoyed making these videos 😍.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;My attempt to replicate Apple Maps’ trees has lead me to learn a variety of new things, from 3D design and modelling to various tree terminologies. The final result may not look &lt;em&gt;exactly&lt;/em&gt; like it, but it’s pretty close.&lt;/p&gt;

&lt;p&gt;Along the journey, I got distracted and accidentally did a &lt;a href="https://twitter.com/cheeaun/status/1446490326311444488" rel="noopener noreferrer"&gt;side&lt;/a&gt; &lt;a href="https://twitter.com/cheeaun/status/1447215108145381380" rel="noopener noreferrer"&gt;quest&lt;/a&gt; though I manage to return back to this project and finish it 😌.&lt;/p&gt;

&lt;p&gt;I could be wrong in certain parts regarding tree families and stuff.  There’s probably more things that I could improve too. I think I’ll revisit this one day when I feel bored or inspired. 😉&lt;/p&gt;

&lt;p&gt;I have to say, &lt;a href="https://exploretrees.sg/3d/" rel="noopener noreferrer"&gt;ExploreTrees.SG 3D&lt;/a&gt; is officially &lt;strong&gt;the most satisfying and coolest project I've done this year&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>apple</category>
      <category>maps</category>
      <category>trees</category>
      <category>singapore</category>
    </item>
    <item>
      <title>Next-level visualizations with ExploreTrees.SG</title>
      <dc:creator>Chee Aun 🦄</dc:creator>
      <pubDate>Wed, 10 Jul 2019 00:00:00 +0000</pubDate>
      <link>https://dev.to/cheeaun/next-level-visualizations-with-exploretrees-sg-4i86</link>
      <guid>https://dev.to/cheeaun/next-level-visualizations-with-exploretrees-sg-4i86</guid>
      <description>&lt;p&gt;Last year, I &lt;a href="https://cheeaun.com/blog/2018/04/building-exploretrees-sg/"&gt;wrote about one of my most ambitious side project ever&lt;/a&gt;, &lt;a href="https://exploretrees.sg/"&gt;ExploreTrees.SG&lt;/a&gt;. It was simply &lt;strong&gt;breath-taking&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s---6rD9iYG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/tree-families-legend-layers-panel-exploretrees-sg%402x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---6rD9iYG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/tree-families-legend-layers-panel-exploretrees-sg%402x.png" alt="Tree families legend on layers panel, on ExploreTrees.SG"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Revisiting the masterpiece
&lt;/h2&gt;

&lt;p&gt;I haven’t touch it since then. March this year, I &lt;em&gt;suddenly&lt;/em&gt; had an itch to update the dataset and see if anything’s changed. I ran the script to fetch the latest data and &lt;a href="https://twitter.com/cheeaun/status/1108010887984472069"&gt;got this&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--HAFb0Ukr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/software/terminal-script-generating-trees-data%402x.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--HAFb0Ukr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/software/terminal-script-generating-trees-data%402x.jpg" alt="Terminal showing a script generating trees data"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s a script that scrapes data from &lt;a href="https://www.nparks.gov.sg/"&gt;National Parks Board&lt;/a&gt;’s &lt;a href="http://trees.sg/"&gt;Trees.sg&lt;/a&gt;, showing the total count of trees and species, and generates a GeoJSON file. From the count, I can compare the previous year’s number of trees to this year’s.&lt;/p&gt;

&lt;p&gt;March last year, &lt;strong&gt;564,266&lt;/strong&gt; trees. March this year, &lt;strong&gt;564,678&lt;/strong&gt; trees. It increased by few hundreds!&lt;/p&gt;

&lt;p&gt;Looking back previously, I attempted to render &lt;strong&gt;all&lt;/strong&gt; trees on the map &lt;em&gt;in&lt;/em&gt; the web browser, but failed due to exceedingly large file size and slow performance. I ended up uploading the data to &lt;a href="https://www.mapbox.com/mapbox-studio/"&gt;Mapbox Studio&lt;/a&gt; as &lt;a href="https://docs.mapbox.com/help/glossary/vector-tiles/"&gt;vector tileset&lt;/a&gt;, to be served back on the map. It’s not a pure client-side solution, but a back-end supported one, which makes it no different from Trees.sg &lt;em&gt;except&lt;/em&gt; it’s faster 🤷‍♂️.&lt;/p&gt;

&lt;p&gt;Ultimately, I still want to achieve this pure client-side solution because I &lt;em&gt;love&lt;/em&gt; to push the limits 😉.&lt;/p&gt;

&lt;p&gt;A year has passed, technologies have improved, right?&lt;/p&gt;

&lt;p&gt;I took a hard look at &lt;code&gt;trees-everything.geojson&lt;/code&gt; which contains &lt;em&gt;all&lt;/em&gt; trees data. It’s &lt;strong&gt;197.6 MB&lt;/strong&gt; in size, which is &lt;em&gt;insane&lt;/em&gt; for any web sites.&lt;/p&gt;

&lt;p&gt;I came up with two ideas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Give up on the GeoJSON format. Embrace normal JSON, remove all the keys and only store values. Convert, for example &lt;code&gt;{"id": "123", "height": 200}&lt;/code&gt; into &lt;code&gt;["123", 200]&lt;/code&gt;. Keys will be hardcoded somewhere else in the code.&lt;/li&gt;
&lt;li&gt;Group &lt;em&gt;all&lt;/em&gt; coordinates of trees into an array, technically like a line, and convert into &lt;a href="https://developers.google.com/maps/documentation/utilities/polylinealgorithm"&gt;Encoded Polyline Algorithm Format&lt;/a&gt;. It’s a lossy compression algorithm that allows you to store a series of coordinates as a single string. It’s lossy, with a precision of 5 decimal places, roughly &lt;a href="https://en.wikipedia.org/wiki/Decimal_degrees#Precision"&gt;1 m in distance near equator&lt;/a&gt;. For example, &lt;code&gt;[[1.27612,103.84744], [1.28333,103.85945]]&lt;/code&gt; will be encoded into a shorter string: &lt;code&gt;wfxFouyxRal@ajA&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here’s the code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data/trees-everything.geojson&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;features&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;points&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;features&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;geometry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coordinates&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;polyline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;points&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;finalData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;One is the  &lt;code&gt;props&lt;/code&gt; variable storing all the values and one more &lt;code&gt;line&lt;/code&gt; variable for the encoded polyline string.&lt;/p&gt;

&lt;p&gt;The final result is a &lt;strong&gt;37.7 MB&lt;/strong&gt; JSON file. That’s &lt;strong&gt;524% smaller&lt;/strong&gt; than the GeoJSON file! 😱 After I compress the file with &lt;a href="https://en.wikipedia.org/wiki/Gzip"&gt;gzip&lt;/a&gt;, it becomes &lt;a href="https://twitter.com/cheeaun/status/1108020569251827712"&gt;&lt;strong&gt;5.7 MB&lt;/strong&gt;&lt;/a&gt;, &lt;strong&gt;3,466% smaller&lt;/strong&gt;! 😱😱&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--YNnOI7ND--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/software/finder-trees-data-files%402x.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--YNnOI7ND--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/software/finder-trees-data-files%402x.jpg" alt="macOS Finder window showing trees data files, in GeoJSON, JSON and Gzip formats"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I think 5.7 MB is not bad. According to this &lt;a href="https://speedcurve.com/blog/web-performance-page-bloat/"&gt;2017 article on SpeedCurve&lt;/a&gt;, the average web page size was 3 MB and it predicted that by 2019, it will be 4 MB.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;According to the HTTP Archive, &lt;strong&gt;almost 16% of pages today – in other words, about 1 out of 6 pages – are 4 MB or greater in size&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That “today” was in 2017.&lt;/p&gt;

&lt;p&gt;Compared to those sites that load 4 MB of &lt;em&gt;junk&lt;/em&gt; plus few bytes of real content, I’m building an &lt;em&gt;awesome&lt;/em&gt; site with 5 MB of useful data plus few hundred kilobytes of map tiles and JavaScript files.&lt;/p&gt;

&lt;p&gt;Obviously at this point, I’m trying really hard to justify my actions 😅. I’m very excited that I manage to squeeze the bytes out of the dataset, but it’s still not small enough to be &lt;em&gt;lower&lt;/em&gt; than the average web page size, so I try to deceive myself that I can do this for a good reason 😂.&lt;/p&gt;

&lt;p&gt;As I look at the dataset, I noticed a few changes. I investigated and found that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Some flowering trees are gone, which I have no idea what happened until now.&lt;/li&gt;
&lt;li&gt;The API URL has changed from &lt;code&gt;https://imaven.nparks.gov.sg/arcgis/rest/services/maven/PTMap/FeatureServer/2/query&lt;/code&gt; to &lt;code&gt;https://imaven.nparks.gov.sg/arcgis/rest/services/maven/Hashing_UAT/FeatureServer/0/query&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The returned response has changed too. The most significant one is the &lt;strong&gt;tree girth&lt;/strong&gt; data. It used to be precise measurements like 1.1 meters, but it became ranges like &lt;code&gt;0.0 - 0.5&lt;/code&gt; and &lt;code&gt;&amp;gt; 1.5&lt;/code&gt;. I don’t know the exact reason but I guess tree girths are pretty complicated stuff.&lt;/p&gt;

&lt;p&gt;While re-fetching the data from scratch, I decided to restructure the grids. The fetching works by constructing a list of grids that will be passed as boundaries for the API calls. In other words, every box is equivalent to one API request. The previous one looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--_egtzTLY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/square-grid-singapore-boundary.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--_egtzTLY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/square-grid-singapore-boundary.png" alt="Square grids around Singapore boundary"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;60 boxes, 60 API requests. The right and bottom side are sort of &lt;em&gt;neglected&lt;/em&gt;, and luckily there are no trees data in those areas.&lt;/p&gt;

&lt;p&gt;The new grid looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--NC4iYrxm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/new-square-grid-singapore-boundary%402x.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--NC4iYrxm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/new-square-grid-singapore-boundary%402x.jpg" alt="The new square grids around Singapore boundary"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;630 boxes, 630 API requests. Higher coverage over the whole Singapore &lt;em&gt;but&lt;/em&gt; not the further southern parts. Fortunately, most trees are covered, as in the trees in other areas are not recorded by National Parks &lt;em&gt;yet&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;So, I got &lt;strong&gt;all&lt;/strong&gt; the new data. I plotted them on the map with &lt;a href="https://deck.gl/#/"&gt;Deck.gl&lt;/a&gt;, a large-scale WebGL-powered data visualization library by Uber. It has better performance when it comes to large quantities of map features as I’ve tried in &lt;a href="https://cheeaun.com/blog/2019/02/building-busrouter-sg/"&gt;my previous side project&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It was… &lt;strong&gt;slow&lt;/strong&gt;. 😰&lt;/p&gt;

&lt;p&gt;Nope, it’s not the fault of the library or Mapbox. It’s Chrome. My Chrome desktop browser was taking a &lt;em&gt;long&lt;/em&gt; time decoding the JSON response. Turns out JSON parsing is pretty darn slow for super large files. It’s also a synchronous blocking operation. It took &lt;strong&gt;more than 10 seconds&lt;/strong&gt; on my Macbook Air (2019). 😰&lt;/p&gt;

&lt;p&gt;As I start to switch from my home-grown build scripts to &lt;a href="https://parceljs.org/"&gt;Parcel&lt;/a&gt;, even the Parcel build step fails with “Allocation failed - JavaScript heap out of memory” errors. I guess it tries to read the JSON file, probably doing something to it and Node keeps running out of memory 😅. I &lt;em&gt;could&lt;/em&gt; fix the build step but let’s not get sidetracked here.&lt;/p&gt;

&lt;p&gt;Maybe I need a faster &lt;code&gt;JSON.parse&lt;/code&gt;. Maybe I could run it in a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers"&gt;web worker&lt;/a&gt;, but it could be even slower due to the huge payload in message passing.&lt;/p&gt;

&lt;p&gt;I had a different idea in mind, that is to use a &lt;em&gt;different&lt;/em&gt; data format. Mapbox uses &lt;a href="https://developers.google.com/protocol-buffers/"&gt;Protocol buffers&lt;/a&gt; for the map tiles. Deck.gl supports &lt;a href="https://deck.gl/#/documentation/developer-guide/performance-optimization?section=on-using-binary-data"&gt;some form of binary data&lt;/a&gt; with an upcoming &lt;a href="https://github.com/uber/deck.gl/blob/master/dev-docs/RFCs/v7.x-binary/binary-data-rfc.md"&gt;RFC&lt;/a&gt;, which looks… quite complicated for me.&lt;/p&gt;

&lt;p&gt;I ended up using &lt;a href="https://msgpack.org/"&gt;MessagePack&lt;/a&gt; because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Protocol buffers need types (&lt;code&gt;double&lt;/code&gt;, &lt;code&gt;int64&lt;/code&gt;, etc) and it’s quite troublesome to do a quick conversion from JSON. I’ve tried converting the GeoJSON file using &lt;a href="https://github.com/mapbox/geobuf"&gt;&lt;code&gt;Geobuf&lt;/code&gt;&lt;/a&gt; and the file size still seems bigger than MessagePack’s (with my combo compression ideas mentioned above).&lt;/li&gt;
&lt;li&gt;Deck.gl’s binary data thing doesn’t seem to be stable &lt;em&gt;yet&lt;/em&gt; and needs manual manipulation on the data.&lt;/li&gt;
&lt;li&gt;MessagePack just works™.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There are two libraries available that can perform encoding and decoding of the MessagePack format:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/msgpack-lite"&gt;msgpack-lite&lt;/a&gt; (listed on the official site)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@ygoe/msgpack"&gt;@ygoe/msgpack&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I tried both and chose the latter because it’s &lt;a href="https://bundlephobia.com/scan-results?packages=msgpack-lite,@ygoe/msgpack"&gt;smaller in bundle size, according to Bundlephobia&lt;/a&gt;. I’m not choosing based on performance because I’ve confirmed that both are &lt;a href="https://twitter.com/cheeaun/status/1109818670711078912"&gt;way faster&lt;/a&gt; than &lt;code&gt;JSON.parse&lt;/code&gt;. Instead of more than 10 seconds, the data is decoded in &lt;strong&gt;about 3 seconds&lt;/strong&gt; or less. The file size is smaller too, at around &lt;strong&gt;30 MB&lt;/strong&gt; but after gzipped, it becomes the same as the gzipped JSON file at roughly &lt;strong&gt;5 MB&lt;/strong&gt;. Too bad, I was hoping it'll be even smaller 😅.&lt;/p&gt;

&lt;p&gt;In the previous dataset, I noticed a few discrepancies such as two or more trees, with different IDs, located at the &lt;em&gt;exact&lt;/em&gt; same coordinates. This time, I try to clean it up and remove duplicates, partially with the hopes of reducing the file size.&lt;/p&gt;

&lt;p&gt;It might sound silly but I’m actually performing &lt;em&gt;strict&lt;/em&gt; comparison of &lt;em&gt;exact&lt;/em&gt; coordinates. I’m not kidding that there's a few trees with &lt;em&gt;exactly&lt;/em&gt; the same coordinates up to every single decimal place. Imagine two trees with the coordinates of 103.702059 longitude and 1.406719 latitude, with not a single difference in the decimals. 😅&lt;/p&gt;

&lt;p&gt;Someone told me before that there could be a tree growing on top of another tree, but I quite… doubt it. 🤨&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/0WLrI2djC3g"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;I checked the data, and could find other similarities in the name and species name. Okay, I could be wrong but I &lt;a href="https://twitter.com/cheeaun/status/1110175312225030144"&gt;decided to remove the potential duplicates&lt;/a&gt; anyway, since this affects the map user interface. Two overlapping tree dots on a map poses quite a challenge, especially for visualization and user interactions. I have high suspicion that the original dataset actually comes from multiple datasets, via different agencies, which could explain this phenomenon.&lt;/p&gt;

&lt;p&gt;The final gzipped file size remains the same 😅.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pushing the limits, again
&lt;/h2&gt;

&lt;p&gt;As the dataset is finalised with decent loading and decoding performance, I proceed to reimplement most of what I did in the first version using &lt;a href="https://docs.mapbox.com/mapbox-gl-js/api/"&gt;Mapbox GL JS&lt;/a&gt;, with Deck.gl instead.&lt;/p&gt;

&lt;p&gt;In the original implementation with Mapbox GL JS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every tree dot is styled via the &lt;a href="https://docs.mapbox.com/mapbox-gl-js/style-spec/#layers-circle"&gt;&lt;code&gt;circle&lt;/code&gt;&lt;/a&gt; layer.&lt;/li&gt;
&lt;li&gt;Pure client-side solution is impossible if there are &lt;a href="https://docs.mapbox.com/help/troubleshooting/working-with-large-geojson-data/#even-bigger-data"&gt;over 500,000 data points&lt;/a&gt;, thus the server-side solution via Mapbox Studio.&lt;/li&gt;
&lt;li&gt;Not all 500,000+ trees are rendered on the map in higher zoom levels because that’s how the vector tiles work; dropping or coalescing features on every zoom level if the limits on the tiles are exceeded.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s a code example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addLayer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;trees&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;circle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;trees-source&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;source-layer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;trees&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;paint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;circle-color&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;case&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;all&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;to-boolean&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flowering&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;to-boolean&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;heritage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]]],&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;magenta&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;to-boolean&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flowering&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;orangered&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;to-boolean&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;heritage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aqua&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;limegreen&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;circle-opacity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;case&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;to-boolean&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flowering&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;to-boolean&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;heritage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;circle-radius&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;interpolate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;linear&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zoom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;case&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;to-boolean&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flowering&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;to-boolean&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;heritage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="mf"&gt;1.25&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;case&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;to-boolean&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flowering&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;to-boolean&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;heritage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="mi"&gt;6&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;circle-stroke-width&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;interpolate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;linear&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zoom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;circle-stroke-color&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rgba(0,0,0,.25)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;For the new implementation with Deck.gl:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The dots are rendered with &lt;a href="https://deck.gl/#/documentation/deckgl-api-reference/layers/scatterplot-layer"&gt;&lt;code&gt;ScatterPlotLayer&lt;/code&gt;&lt;/a&gt;. May look the same as the former but the styles are written differently.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pure client-side solution becomes possible. The &lt;a href="https://deck.gl/#/documentation/developer-guide/performance-optimization?section=general-performance-expectations"&gt;documentation&lt;/a&gt; quotes:&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;On 2015 MacBook Pros with dual graphics cards, most basic layers (like &lt;code&gt;ScatterplotLayer&lt;/code&gt;) renders fluidly at 60 FPS during pan and zoom operations up to about 1M (one million) data items, with framerates dropping into low double digits (10-20FPS) when the data sets approach 10M items.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;All&lt;/strong&gt; trees are rendered in &lt;strong&gt;all&lt;/strong&gt; zoom levels.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The new code example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;MapboxLayer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;trees&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ScatterplotLayer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;radiusMinPixels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;radiusMaxPixels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;lineWidthUnits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pixels&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;getLineWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;getLineColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;getRadius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flowering&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heritage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;getFillColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flowering&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heritage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;colorName2RGB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;magenta&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flowering&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;colorName2RGB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;orangered&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heritage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;colorName2RGB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aqua&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;colorName2RGB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;green&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Yeap, shorter.&lt;/p&gt;

&lt;p&gt;I have to create a new function called &lt;code&gt;colorName2RGB&lt;/code&gt; to convert color names (&lt;code&gt;orangered&lt;/code&gt;, &lt;code&gt;aqua&lt;/code&gt;, etc) to RGB values in array form (&lt;code&gt;[R,G,B]&lt;/code&gt;), because Deck.gl doesn’t support them. This function is surprisingly simple because it uses &lt;code&gt;canvas&lt;/code&gt;’s magic &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle"&gt;&lt;code&gt;fillStyle&lt;/code&gt;&lt;/a&gt; property to convert a &lt;code&gt;CSS&lt;/code&gt; color, instead of a lookup table, thanks to this &lt;a href="https://stackoverflow.com/a/47355187/20838"&gt;StackOverflow answer by &lt;em&gt;JayB&lt;/em&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Everything became &lt;a href="https://twitter.com/cheeaun/status/1114532995799478272"&gt;crazy &lt;em&gt;smooth&lt;/em&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/1F2qgbvRwpQ"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;The reimplementation didn’t take long. There were some tiny differences in how the circle scales for different zoom levels but not significant enough for anyone to notice.&lt;/p&gt;

&lt;p&gt;Deck.gl has yet again &lt;em&gt;amazed&lt;/em&gt; me with its powerful features and performance. Honestly, I always get wowed every single time by how much I can achieve with this library! And I didn't stop there 😉.&lt;/p&gt;

&lt;p&gt;This was my previous &lt;strong&gt;failed&lt;/strong&gt; attempt:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--B5FGyRVN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/software/3d-trees-girth-height-map-singapore%402x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--B5FGyRVN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/software/3d-trees-girth-height-map-singapore%402x.png" alt="3D trees rendered based on girth and height, on a map, in Singapore"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s a failed attempt mainly due to performance. It was &lt;strong&gt;too darn laggy&lt;/strong&gt; rendering &lt;em&gt;all&lt;/em&gt; the 3D tree trunks! I also suspect that some girth measurements were wrong, which you could see that one huge hexagon in the image above, at Fort Canning Park. This was done with Mapbox GL JS’s 3D extrusion features.&lt;/p&gt;

&lt;p&gt;This is my &lt;a href="https://twitter.com/cheeaun/status/1114696975515963398"&gt;second attempt&lt;/a&gt;, with Deck.gl:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--1BK3zUOz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-3d-tree-trunks-1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--1BK3zUOz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-3d-tree-trunks-1.jpg" alt="ExploreTrees.SG showing 3D tree trunks"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Those orange dots are the tree trunks in 3D. Lets zoom in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--jqRaN2_h--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-3d-tree-trunks-2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jqRaN2_h--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-3d-tree-trunks-2.jpg" alt="ExploreTrees.SG showing 3D tree trunks, zoomed-in"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--KV3dqc1J--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-3d-tree-trunks-3.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KV3dqc1J--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-3d-tree-trunks-3.jpg" alt="ExploreTrees.SG showing 3D tree trunks, further zoomed-in"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--NOGZa7lj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-3d-tree-trunks-4.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--NOGZa7lj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-3d-tree-trunks-4.jpg" alt="ExploreTrees.SG showing 3D tree trunks, much further zoomed-in"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This attempt is about &lt;strong&gt;3 to 4 times faster&lt;/strong&gt; in rendering &lt;em&gt;everything&lt;/em&gt; 😱 (based on my perception). &lt;strong&gt;But&lt;/strong&gt;, it still lags when panning around in higher levels 😢.&lt;/p&gt;

&lt;p&gt;Ugh, I feel like I’m on roller coaster ride when things become fast, super fast, then slow again, then fast again, and then end up slow again 😖. Technically it’s my own fault. Repeatedly, I did try to make it fast and &lt;em&gt;then&lt;/em&gt; purposely make it slow again. I have no idea why I keep doing this 😂&lt;/p&gt;

&lt;p&gt;Anyway, I had an idea on how to make it fast 😏. I limit this 3D mode to higher zoom levels and then filter the list of trees based on the map’s geographical bounds. Like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bounds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getBounds&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="c1"&gt;// magically filter the list of trees based on bounds&lt;/span&gt;

&lt;span class="nx"&gt;trees3DLayer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setProps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;trees3Dify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The returned value of &lt;code&gt;map.getBounds()&lt;/code&gt; is the smallest bounds that encompasses the visible region of the map. Instead of rendering 500,000+ trees, it can be made to only render few thousands instead.&lt;/p&gt;

&lt;p&gt;Basically this not only solves the performance problem, but also makes sense. There’s no point rendering 3D trees on lower zoom levels anyway since they’ll all look like small dots. Users still have to zoom in to see the 3D trunks in detail, which is the same reason why 3D buildings are only visible in higher zoom levels on maps like Google Maps and Apple Maps.&lt;/p&gt;

&lt;p&gt;Thanks to this, it’s &lt;a href="https://twitter.com/cheeaun/status/1114833009289469952"&gt;fast again&lt;/a&gt; 😬.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/KPMsFXVsC20"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;The right side is the Developer Tools Console, logging the number of 3D trees rendered every time the map is zoomed, panned or rotated. It’s quite noticeable that there’s completely no lag at all when panning around. The 3D trees outside of the bounds will only start rendering after the pan, zoom or rotate ends.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Perfect&lt;/strong&gt;. What’s left for me is to finish up the remaining reimplementation, remove all the old code, choose a better color for these tree trunks and wrap up!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--qDJTVnGq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/figures/diagram/thinking-3d-tree-trunks%402x.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--qDJTVnGq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/figures/diagram/thinking-3d-tree-trunks%402x.jpg" alt="Thinking about 3D tree trunks"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At first, I wanted to color the trunks as brown but it’s too dark and doesn’t contrast well with the dark map tiles. This explains why I choose brighter brown or orange as my first attempt. And then I changed the color to white because it’s still doesn’t &lt;em&gt;feel&lt;/em&gt; right somehow…&lt;/p&gt;

&lt;p&gt;Yes… 3D tree trunks look kind of weird, right?&lt;/p&gt;

&lt;p&gt;I thought to myself, &lt;strong&gt;what was I trying to do again?&lt;/strong&gt; What’s the purpose of the 3D renderings? Isn’t 2D enough for this visualization? I guess that drawing 3D trees on a map would be cool but didn’t really think much after that.&lt;/p&gt;

&lt;p&gt;But what makes them look weird?&lt;/p&gt;

&lt;p&gt;Oh! The tree &lt;a href="https://en.wikipedia.org/wiki/Crown_(botany)"&gt;crowns&lt;/a&gt;! It looks weird because it’s &lt;em&gt;incomplete&lt;/em&gt;. Wait, the problem is that I don’t have the crown data, so how am I going to draw the tree crowns? Depending on the tree species or families, I might need to draw different shapes of crowns, which can be &lt;em&gt;a lot&lt;/em&gt; of work 😅.&lt;/p&gt;

&lt;p&gt;So I start to think, what’s the simplest form of a tree crown that is achievable? Of course, &lt;strong&gt;another cylinder&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I did a few &lt;a href="https://twitter.com/cheeaun/status/1119443171031703552"&gt;quick sketches&lt;/a&gt; during my free time:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--E0P0QMRU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/exploretrees-sg-logo-tree-trunk-crown-sketches.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--E0P0QMRU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/exploretrees-sg-logo-tree-trunk-crown-sketches.jpg" alt="ExploreTrees.SG logo and tree trunk with crown sketches"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I have the height information of the trees, but it doesn’t necessarily mean the height of the trunks themselves. I could “chop off” the trunk to about 75% and leave the rest for the crown. The crown could take up 50% of the height so that it looks like it’s &lt;em&gt;covering&lt;/em&gt; the trunk. As for the radius of the crown, I can roughly measure it based on the height as well. From my trials and errors, I found the sweet spot for the radius to be roughly 40% of the height.&lt;/p&gt;

&lt;p&gt;Sounds like &lt;em&gt;complicated&lt;/em&gt; math, but… &lt;strong&gt;&lt;a href="https://twitter.com/cheeaun/status/1114866910682681344"&gt;voilà!&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--gZ60tYhx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-faux-3d-trees-1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--gZ60tYhx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-faux-3d-trees-1.jpg" alt="ExploreTrees.SG faux 3D trees"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ne5QGsjf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-faux-3d-trees-2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ne5QGsjf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-faux-3d-trees-2.png" alt="ExploreTrees.SG faux 3D trees, zoomed-in"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--V0sdKK33--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-faux-3d-trees-3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--V0sdKK33--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-faux-3d-trees-3.png" alt="ExploreTrees.SG faux 3D trees, further zoomed-in"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the moment when I felt that my efforts have finally becoming fruitful. Little did I know that the crowns actually make such a huge difference!&lt;/p&gt;

&lt;p&gt;To be honest, I got super excited when this actually works. I tried to finish up the work and ensuring feature parity with the old implementation. It reached a point where it's &lt;a href="https://twitter.com/cheeaun/status/1114919843541573632"&gt;&lt;em&gt;almost&lt;/em&gt; complete&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/gA213scyW_s"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;I’ve also enabled &lt;a href="https://twitter.com/cheeaun/status/1115060661401182208"&gt;3D buildings&lt;/a&gt;, for a more &lt;em&gt;complete&lt;/em&gt; picture.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/Sbh8vS_DKYg"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;…and &lt;a href="https://twitter.com/cheeaun/status/1115630822764105728"&gt;more photos&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--g3Rd-n5i--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-faux-3d-trees-4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--g3Rd-n5i--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-faux-3d-trees-4.png" alt="ExploreTrees.SG faux 3D trees — overlapping tree crowns"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--JyngD5Js--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-faux-3d-trees-5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--JyngD5Js--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-faux-3d-trees-5.png" alt="ExploreTrees.SG faux 3D trees — cute tiny trees"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--xJMW9oEh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-faux-3d-trees-6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--xJMW9oEh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-faux-3d-trees-6.png" alt="ExploreTrees.SG faux 3D trees — huge trees and tiny trees together"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Look at those cute little trees! 😍&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--xy46v2BT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-faux-3d-trees-7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--xy46v2BT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-faux-3d-trees-7.png" alt="ExploreTrees.SG faux 3D trees — long queue of trees"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--3u7mc3vb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-faux-3d-trees-8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--3u7mc3vb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-faux-3d-trees-8.png" alt="ExploreTrees.SG faux 3D trees — curved line of trees"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--_LPIRRhh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-faux-3d-trees-9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--_LPIRRhh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-faux-3d-trees-9.png" alt="ExploreTrees.SG faux 3D trees — tree formations in context with surrounding 3D buildings"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Nearby 3D buildings tend to give a pretty &lt;a href="https://twitter.com/cheeaun/status/1115632732481019916"&gt;realistic context&lt;/a&gt; to the 3D trees around them. In a way, it feels like there’s a pattern on how these trees are planted based on the surrounding geography 🤔.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--KkF3Bz27--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-faux-3d-trees-10.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KkF3Bz27--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-faux-3d-trees-10.png" alt="ExploreTrees.SG faux 3D trees — trees on Changi Beach Park"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--zsKiyfzD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-faux-3d-trees-11.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--zsKiyfzD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-faux-3d-trees-11.png" alt="ExploreTrees.SG faux 3D trees — trees clashing with buildings"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This image above shows one of the data mismatch cases, near Cecil Street, where the trees are no longer there and an interim food centre is built in this area.&lt;/p&gt;

&lt;p&gt;Alright, I’m almost done. The 2D tree markers are working. Visualization filters work. 3D trees perform really well.&lt;/p&gt;

&lt;p&gt;Just &lt;em&gt;one&lt;/em&gt; thing left: &lt;strong&gt;laggy&lt;/strong&gt; blue &lt;em&gt;marker highlighter&lt;/em&gt; and tree information &lt;em&gt;hover cards&lt;/em&gt;, as rendered on the videos above. On desktop browsers, when the mouse cursor hovers over a tree, the blue marker highlighter will appear around it, with the hover card popping out from the bottom right of the page. There’s a significant lag when the cursor moves across multiple trees on the map as the user interface is trying to keep up.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Wm1TmS3t--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/figures/diagram/exploretrees-sg-marker-highlighter-hover-card%402x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Wm1TmS3t--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/figures/diagram/exploretrees-sg-marker-highlighter-hover-card%402x.png" alt="ExploreTrees.SG marker highlighter and hover card"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I tried three attempts.&lt;/p&gt;

&lt;p&gt;My &lt;strong&gt;first&lt;/strong&gt; piece of code was using the &lt;a href="https://docs.mapbox.com/mapbox-gl-js/api/#map.event:mouseover"&gt;&lt;code&gt;mousemove&lt;/code&gt; event&lt;/a&gt; from Mapbox GL JS. When the event is fired, a specific layer from the map can be queried and the marker or feature information can be extracted to be displayed as part of the hover card. The query is done by converting the pixel coordinates of the cursor to the actual map coordinates in latitude &amp;amp; longitude, and then find the nearest map feature from the coordinates. This operation is quite tedious — the &lt;code&gt;mousemove&lt;/code&gt; event fires too often, there are way too many features under the cursor and every single call is blocking the UI thread, thus affecting the rendering speed of the marker highlighter and the hover card.&lt;/p&gt;

&lt;p&gt;My &lt;strong&gt;second&lt;/strong&gt; attempt was using Deck.gl’s powerful &lt;a href="https://deck.gl/#/documentation/developer-guide/adding-interactivity"&gt;picking engine&lt;/a&gt;, which uses something called the &lt;a href="https://deck.gl/#/documentation/developer-guide/writing-custom-layers/picking"&gt;Color Picking Technique&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Rather than doing traditional ray-casting or building octrees etc in JavaScript, deck.gl implements picking on the GPU using a technique we refer to as "color picking". When deck.gl needs to determine what is under the mouse (e.g. when the user moves or clicks the pointer over the deck.gl canvas), all pickable layers are rendered into an off-screen buffer, but in a special mode activated by a GLSL uniform. In this mode, the shaders of the core layers render picking colors instead of their normal visual colors.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Honestly, I have no idea how this works. I roughly get it that the engine tries to pick the color under the cursor and somehow manage to find the relevant feature which matches the color?!? But how?&lt;/p&gt;

&lt;p&gt;Anyway, I’ve tried it and this method doesn’t work too. Everything’s &lt;em&gt;still&lt;/em&gt; slow 😭.&lt;/p&gt;

&lt;p&gt;So, lo and behold my &lt;strong&gt;third&lt;/strong&gt; attempt using a spatial index for points with a library called &lt;a href="https://github.com/mourner/geokdbush"&gt;&lt;code&gt;geokdbush&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A geographic extension for &lt;a href="https://github.com/mourner/kdbush"&gt;kdbush&lt;/a&gt;, the fastest static spatial index for points in JavaScript.&lt;/p&gt;

&lt;p&gt;It implements fast &lt;a href="https://en.wikipedia.org/wiki/Nearest_neighbor_search"&gt;nearest neighbors&lt;/a&gt; queries for locations on Earth, taking Earth curvature and date line wrapping into account. Inspired by &lt;a href="https://github.com/darkskyapp/sphere-knn"&gt;sphere-knn&lt;/a&gt;, but uses a different algorithm.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I think it’s pretty cool because it’s written by &lt;a href="https://github.com/mourner"&gt;Vladimir Agafonkin&lt;/a&gt;, the creator of &lt;a href="https://leafletjs.com/"&gt;Leaflet&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here’s a rough code snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;KDBush&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nearestPoints&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;geokdbush&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;around&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;radius&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;geokdbush.around&lt;/code&gt; method returns an array of the closest points from a given location in order of increasing distance. It has optional arguments to set custom maximum results, maximum distance in kilometers to search within, and even an additional function to further filter the results! 🤯&lt;/p&gt;

&lt;p&gt;In fact, these methods are so useful that I also use it for filtering the rendering of the 3D trees based on the map bounds (in the beginning, for the &lt;code&gt;trees3Dify&lt;/code&gt; call).&lt;/p&gt;

&lt;p&gt;The result is &lt;a href="https://twitter.com/cheeaun/status/1116960702118318082"&gt;&lt;strong&gt;insanely fast&lt;/strong&gt;&lt;/a&gt;. ⚡️&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/SxsmT9trxaM"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Even works in &lt;a href="https://twitter.com/cheeaun/status/1116960752122777601"&gt;3D mode&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/4YmBeA35fD0"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;I’m quite satisfied with this.&lt;/p&gt;

&lt;p&gt;But I think I can &lt;strong&gt;do more&lt;/strong&gt;. 💪&lt;/p&gt;

&lt;h2&gt;
  
  
  Plus ultra
&lt;/h2&gt;

&lt;p&gt;Initially I have an idea for getting a listing of all tree species mapped to their own crown shape patterns. I couldn’t find such list unfortunately 😭.&lt;/p&gt;

&lt;p&gt;My research led me to few subjects like &lt;a href="https://openoregon.pressbooks.pub/forestmeasurements/chapter/5-3-crown-classes/"&gt;tree crown classes&lt;/a&gt; and I especially like this diagram from &lt;a href="https://www.toppr.com/guides/science/forest-our-lifeline/structure-of-forest/"&gt;Structures of Forest&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--R5QxwJ7X--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/figures/diagram/crown-of-tree.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--R5QxwJ7X--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/figures/diagram/crown-of-tree.jpg" alt="Crown of tree diagram, showing Pyramidal, Full-crowned, Vase, Fountain, Spreading, Layered, Columnar and Weeping"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These are the various crown shapes of trees and I think I only covered the “full-crowned” type, with blocky cylinders. The other crowns would be a bit difficult to model in 3D…&lt;/p&gt;

&lt;p&gt;Similar to how the tree trunks are rendered, the tree crowns are rendered with deck.gl’s &lt;a href="https://deck.gl/#/documentation/deckgl-api-reference/layers/solid-polygon-layer"&gt;SolidPolygonLayer&lt;/a&gt;, which works like &lt;a href="https://deck.gl/#/documentation/deckgl-api-reference/layers/polygon-layer"&gt;PolygonLayer&lt;/a&gt; but without strokes. Extrusion is enabled from the &lt;code&gt;extruded&lt;/code&gt; option combined with z-indices of the coordinates. The circle shape is formed with &lt;a href="https://turfjs.org/docs/#circle"&gt;@turf/circle&lt;/a&gt;. Elevation is performed via the &lt;code&gt;getElevation&lt;/code&gt; accessor.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--CIV7IM5j--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/figures/diagram/exploretrees-sg-polygon-extrusion-elevation%402x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--CIV7IM5j--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/figures/diagram/exploretrees-sg-polygon-extrusion-elevation%402x.png" alt="Coordinates forming a polygon, with extrusion and elevation"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;the basics&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Creating complex polygons for all the different tree crowns would be too complicated for me, both computationally &lt;em&gt;and&lt;/em&gt; mathematically 😵.&lt;/p&gt;

&lt;p&gt;While developing this project, I’ve been following the updates on the then-upcoming &lt;a href="https://github.com/uber/deck.gl/blob/master/CHANGELOG.md#deckgl-v70"&gt;version 7.0 of Deck.g&lt;/a&gt;l. The changes that caught my attention was the (re-)introduction of &lt;a href="https://github.com/uber/deck.gl/issues/2890"&gt;&lt;code&gt;SimpleMeshLayer&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/uber/deck.gl/issues/2871"&gt;&lt;code&gt;ScenegraphLayer&lt;/code&gt;&lt;/a&gt;, which allows the rendering of actual 3D models in formats such as &lt;a href="https://en.wikipedia.org/wiki/Wavefront_.obj_file"&gt;OBJ&lt;/a&gt;, &lt;a href="https://en.wikipedia.org/wiki/PLY_(file_format)"&gt;PLY&lt;/a&gt; and &lt;a href="https://en.wikipedia.org/wiki/GlTF"&gt;glTF&lt;/a&gt;. This means… I can put &lt;em&gt;real&lt;/em&gt; tree models on the map! 😱&lt;/p&gt;

&lt;p&gt;Wait the minute, I don’t have a species-to-crown-type mapping yet, so rendering a super realistic full-crowned tree for every single tree would be kind of weird, especially for those tiny little trees. Even if I have the mapping, it would take a gargantuan effort for me to code the species-specific crowns for all 500,000+ trees, manage all the 3D model files, &lt;em&gt;and&lt;/em&gt; tune the asset loading &amp;amp; map rendering performance at the same time!&lt;/p&gt;

&lt;p&gt;There &lt;em&gt;has&lt;/em&gt; to be some compromise here. I want to replace the cylinder tree crowns with something more realistic &lt;strong&gt;but&lt;/strong&gt; cannot be &lt;em&gt;too&lt;/em&gt; realistic. So it should be &lt;strong&gt;semi-realistic&lt;/strong&gt;, right?&lt;/p&gt;

&lt;p&gt;I read through the &lt;a href="https://deck.gl/#/documentation/deckgl-api-reference/layers/simple-mesh-layer"&gt;documentation&lt;/a&gt; and noticed this interesting piece of code from the example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;CubeGeometry&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;luma.gl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;That’s one of scenegraph model nodes provided by &lt;a href="https://luma.gl/#/"&gt;luma.gl&lt;/a&gt;. Pretty neat.&lt;/p&gt;

&lt;p&gt;They are like pre-made 3D objects that can be used like, as what it described, WebGL components. One of the geometries that I found is the &lt;a href="https://luma.gl/#/documentation/api-reference/geometry-nodes/sphere"&gt;SphereGeometry&lt;/a&gt;, which looks like what I need. Or perhaps, a sphere is what I think would be a better alternative than a cylinder, right? 🤔&lt;/p&gt;

&lt;p&gt;Not only that I can create a sphere object with this, I can also apply a texture on it with the &lt;code&gt;texture&lt;/code&gt; property for &lt;code&gt;SimpleMeshLayer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here’s a code snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;treesCrownLayer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;MapboxLayer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;trees-crown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SimpleMeshLayer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;texture&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;leaves&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;mesh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;SphereGeometry&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;texture&lt;/code&gt; property accepts one of these 3 types of value:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A luma.gl &lt;code&gt;Texture2D&lt;/code&gt; instance&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;HTMLImageElement&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A URL string to the texture image&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the &lt;code&gt;document.getElementById('leaves')&lt;/code&gt; code above is a reference to an &lt;code&gt;img&lt;/code&gt; element in the HTML page. I use this method to &lt;em&gt;preload&lt;/em&gt; the image instead of lazy-load it during runtime.&lt;/p&gt;

&lt;p&gt;As for the image, I &lt;em&gt;googled&lt;/em&gt; and managed to sift through &lt;em&gt;hundreds&lt;/em&gt; of images to find &lt;strong&gt;one&lt;/strong&gt; “leaves” texture image from &lt;a href="https://opengameart.org/content/dims-enviromental-and-architectural-textures"&gt;Dim's environmental and architectural textures&lt;/a&gt;. Honestly, it’s not easy to find a &lt;em&gt;good&lt;/em&gt; texture image at all 😂, but luckily there are existing available resources mostly for game development 😀.&lt;/p&gt;

&lt;p&gt;Here’s the moment of truth 🤞:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--9acuHgha--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-3d-realistic-trees-1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--9acuHgha--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-3d-realistic-trees-1.jpg" alt="ExploreTrees.SG 3D realistic trees"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Oh my god, It works!!&lt;/strong&gt; 😱😱😱&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--bf35ESvL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-3d-realistic-trees-2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--bf35ESvL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-3d-realistic-trees-2.jpg" alt="ExploreTrees.SG 3D realistic trees — tree crowns with less vertices"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For this image, I tried playing around with the number of vertices. If it’s reduced, the sphere will have less triangular faces and look less spherical. I kind of suspect that reducing the number of vertices might improve performance but doesn’t seem to affect much.&lt;/p&gt;

&lt;p&gt;Anyway, before I get too happy with this result, there are few problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The trees look a bit too dark, so might need some sort of lighting. Maybe use brighter colors too.&lt;/li&gt;
&lt;li&gt;The tree crown need to be “see-through”, to simulate the empty spaces between the leaves.&lt;/li&gt;
&lt;li&gt;The leaves on the crown are too big, especially when zoomed in. I’ll need to make them smaller.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using &lt;a href="https://affinity.serif.com/en-gb/photo/"&gt;Affinity Photo&lt;/a&gt; and &lt;a href="https://affinity.serif.com/en-gb/designer/"&gt;Affinity Designer&lt;/a&gt;, I made some adjustments from the original texture image (top left):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--PxgXWxpO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/figures/diagram/tree-leaves-texture-image-masked-flipped-repeated-pattern%402x.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--PxgXWxpO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/figures/diagram/tree-leaves-texture-image-masked-flipped-repeated-pattern%402x.jpg" alt="Tree leaves texture image being masked and flipped into a repeated pattern"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;First, I select the dark areas between the leaves and make them alpha-transparent. To reduce the size of the leaves, I make the image bigger instead, by flipping it 3 times, creating a repeatable leaves image pattern.&lt;/p&gt;

&lt;p&gt;The results:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--FV3vvhxB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-3d-realistic-trees-3.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--FV3vvhxB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-3d-realistic-trees-3.jpg" alt="ExploreTrees.SG 3D realistic trees, zoomed out with surrounding 3D buildings"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--SllrkZP---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-3d-realistic-trees-4.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--SllrkZP---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-3d-realistic-trees-4.jpg" alt="ExploreTrees.SG 3D realistic trees — trees at road intersection"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--4ltpwpMj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-3d-realistic-trees-5.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--4ltpwpMj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-3d-realistic-trees-5.jpg" alt="ExploreTrees.SG 3D realistic trees — highlighted tree and zoomed in"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Not bad. The leaves are smaller and it’s possible to see the tree trunk hiding &lt;em&gt;inside&lt;/em&gt; the crown. 👀&lt;/p&gt;

&lt;p&gt;As for the lighting, I use the new lighting effects system &lt;a href="https://medium.com/vis-gl/introducing-deck-gl-v7-0-c18bcb717457"&gt;introduced in deck.gl v7.0&lt;/a&gt;, particularly the &lt;a href="https://deck.gl/#/documentation/deckgl-api-reference/lights/ambient-light"&gt;&lt;code&gt;AmbientLight&lt;/code&gt;&lt;/a&gt; source and the new experimental &lt;a href="https://deck.gl/#/documentation/deckgl-api-reference/lights/sun-light"&gt;&lt;code&gt;SunLight&lt;/code&gt;&lt;/a&gt; source. &lt;code&gt;SunLight&lt;/code&gt; is a variation of &lt;code&gt;DirectionalLight&lt;/code&gt; which is automatically set based on a UTC time and the current viewport. In other words, it &lt;strong&gt;simulates the sun&lt;/strong&gt; by calculating the sun position with a JavaScript library called &lt;a href="https://github.com/mourner/suncalc"&gt;SunCalc&lt;/a&gt; (again, created by Vladimir Agafonkin, creator of Leaflet 🤩).&lt;/p&gt;

&lt;p&gt;Coincidentally as I was &lt;a href="https://www.aa.quae.nl/en/reken/zonpositie.html"&gt;researching on how the code and formula work&lt;/a&gt;, I saw Vladimir &lt;a href="https://twitter.com/mourner/status/1120748294190141440"&gt;tweeting&lt;/a&gt; about his tiny &lt;a href="https://observablehq.com/@mourner/sun-position-in-900-bytes"&gt;900-byte function to calculate the position of the Sun&lt;/a&gt;! 😱 If I’m not mistaken, it’s like a simplified version of SunCalc.&lt;/p&gt;

&lt;p&gt;Without further ado, I implemented them all, this way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;phaseColor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;getPhaseColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ambientLight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;AmbientLight&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;intensity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;phaseColor&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sunLight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;SunLight&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;intensity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;phaseColor&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lightingEffect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;LightingEffect&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;ambientLight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;sunLight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;treesCrownLayer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deck&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setProps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;effects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;lightingEffect&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The light intensities reduce when the sun phase is “dark” to simulate night time.&lt;/p&gt;

&lt;p&gt;Before 🔅:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--LC4yyiIQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-3d-realistic-trees-lighting-before.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--LC4yyiIQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-3d-realistic-trees-lighting-before.jpg" alt="ExploreTrees.SG 3D realistic trees — the lighting before"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After 🔆:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--gFngZnDB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-3d-realistic-trees-lighting-after.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--gFngZnDB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-3d-realistic-trees-lighting-after.jpg" alt="ExploreTrees.SG 3D realistic trees — the lighting after"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Looks much better!&lt;/p&gt;

&lt;p&gt;Here’s &lt;a href="https://twitter.com/cheeaun/status/1124981208427810816"&gt;a time-lapse of the sun lighting&lt;/a&gt; on the trees, to show how the (sun) light direction changes based on time. Also, it’s not affected by user's local time and will always be in actual Singapore time, since the formula depends on location coordinates instead of time.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/n21C4BcQ7l8"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Unfortunately, the lighting transition from night to day and day to night is kind of abrupt for now. Just in case someone stays on the site for too long, I’ve also added a time interval to update the lighting every 10 minutes.&lt;/p&gt;

&lt;p&gt;Along the way, I’ve added a few useful POIs (Points of Interest) that are &lt;a href="https://www.nparks.gov.sg/gardens-parks-and-nature"&gt;listed on NParks&lt;/a&gt;, which include these categories of places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parks&lt;/li&gt;
&lt;li&gt;Community gardens&lt;/li&gt;
&lt;li&gt;Heritage roads&lt;/li&gt;
&lt;li&gt;Skyrise greenery&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On their &lt;a href="https://www.nparks.gov.sg/gardens-parks-and-nature"&gt;site&lt;/a&gt;, it looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--J2Adzkst--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/nparks-site-parks-community-gardens-heritage-trees-heritage-road-skyrise-greenery%402x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--J2Adzkst--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/nparks-site-parks-community-gardens-heritage-trees-heritage-road-skyrise-greenery%402x.png" alt="NParks site showing parks, community gardens, heritage trees, heritage road and skyrise greenery"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On ExploreTrees.SG:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--vmoV2l4Q--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-parks-community-gardens-skyrise-greenery%402x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vmoV2l4Q--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-parks-community-gardens-skyrise-greenery%402x.png" alt="ExploreTrees.SG showing parks, community gardens and skyrise greenery"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Besides the POIs, I’ve also made the train stations and bus stops more prominent to guide navigation and exploration. These markers only appear on higher zoom levels as they are only useful in those levels and to prevent clutter on the map.&lt;/p&gt;

&lt;p&gt;As a side quest, I took this chance to try out &lt;a href="https://lit-html.polymer-project.org/"&gt;lit-html&lt;/a&gt; for the hover card, replacing the constant &lt;code&gt;innerHTML&lt;/code&gt; layout-trashing (destroying and rebuilding the content on every hover event).&lt;/p&gt;

&lt;h2&gt;
  
  
  The final boss
&lt;/h2&gt;

&lt;p&gt;Despite the fact that I’ve ticked off &lt;em&gt;all&lt;/em&gt; the checkboxes in my todos for this project, there’s one problem that I ignore since the beginning of this exercise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It doesn’t work well on mobiles.&lt;/strong&gt; 😞&lt;/p&gt;

&lt;p&gt;There are a few issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MessagePack decoding crashes on Mobile Safari. When I try switch it to JSON format, the browser didn’t crash but it takes a significant amount of time to load. Either way, it’s a pretty bad user experience.&lt;/li&gt;
&lt;li&gt;Even after everything is loaded successfully, Mobile Safari will randomly crash after few minutes of panning and zooming on the map. I’ve tried asking a friend to try the site on an iPad, which I suspect has more memory and processing power than iPhone, yet the site still crashes 😭.&lt;/li&gt;
&lt;li&gt;The site doesn’t crash on Chrome Android but still kind of laggy. 🐌&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most-likely suspicion could be the &lt;strong&gt;memory pressure&lt;/strong&gt;, as highlighted by Deck.gl’s &lt;a href="https://deck.gl/#/documentation/developer-guide/performance-optimization"&gt;documentation on performance&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Modern phones (recent iPhones and higher-end Android phones) are surprisingly capable in terms of rendering performance, but are considerably more sensitive to memory pressure than laptops, resulting in browser restarts or page reloads. They also tend to load data significantly slower than desktop computers, so some tuning is usually needed to ensure a good overall user experience on mobile.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I guess loading a 5MB compressed file, which uncompresses into a 30MB file, that loads 500,000+ trees on a WebGL-powered 3D-rendered map is just… too much? 😝&lt;/p&gt;

&lt;p&gt;I’ve tried dozens of ways to fix this, like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reduce the number of layers on the map.&lt;/li&gt;
&lt;li&gt;Reduce the number of polygons on the map.&lt;/li&gt;
&lt;li&gt;Remove some features and code that might use a lot of memory.&lt;/li&gt;
&lt;li&gt;Apply micro-optimisations that I thought would help.&lt;/li&gt;
&lt;li&gt;Reduce number of trees by using cluster mode with &lt;a href="https://github.com/mapbox/supercluster"&gt;&lt;code&gt;supercluster&lt;/code&gt;&lt;/a&gt;, which kind of defeats the purpose why I build this in the first place.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing works. It’s pretty daunting 😩.&lt;/p&gt;

&lt;p&gt;This is the only thing that blocks me from launching this new version of ExploreTrees.SG and I have to make the call.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Wu6wvFaP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/exploretrees-sg-visitor-v1-v2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Wu6wvFaP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/exploretrees-sg-visitor-v1-v2.jpg" alt="ExploreTrees.SG visitor flow for version 1 and version 2"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After days of contemplating, I decided to &lt;em&gt;not&lt;/em&gt; replace the version 1.0 of ExploreTrees.SG and instead, slot in the version 2.0 &lt;strong&gt;together&lt;/strong&gt; with it. The old version 1.0 works fine and seems more stable on mobile browsers. The new version works fine on desktop browsers but &lt;em&gt;not&lt;/em&gt; the iPad. I’m not sure about Android tablets so I’ll assume they’re slightly better or worse than iPad. 🤷‍♂️&lt;/p&gt;

&lt;p&gt;On version 1.0, all the tree data are served from &lt;a href="https://www.mapbox.com/mapbox-studio/"&gt;Mapbox Studio&lt;/a&gt;, as vector &lt;a href="https://docs.mapbox.com/studio-manual/reference/tilesets/"&gt;tilesets&lt;/a&gt;. They are &lt;em&gt;partially&lt;/em&gt; loaded based on zoom levels, so not everything is shown at once. This probably helps in reducing memory pressure, using less bandwidth, and having better performance.&lt;/p&gt;

&lt;p&gt;On version 2.0, which I kept pushing the limits, &lt;strong&gt;all&lt;/strong&gt; the trees data are loaded in the web browser, so there’s no more round trip to the server. Technically it’s pure client-side and uses a lot of bandwidth and all the power from the user’s machine to render everything nicely.&lt;/p&gt;

&lt;p&gt;I needed a way to switch between the versions based on certain device capabilities.&lt;/p&gt;

&lt;p&gt;There’s no way for me to detect the device's or browser’s ability to handle large memory pressure. There’s also no way to detect possible crashes on a web app &lt;em&gt;before&lt;/em&gt; it could crash. User agent string detection won’t work here either.&lt;/p&gt;

&lt;p&gt;Since there’s no reliable way to detect these conditions, I applied touch detection instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isTouch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ontouchstart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;msMaxTouchPoints&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hqHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/#hq/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;renderingMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;hqHash&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;isTouch&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;low&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;I made a few assumptions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Most modern mobile phones have touch screens.&lt;/li&gt;
&lt;li&gt;Older phones won’t be able to load the site anyway since it uses WebGL (sorry 🙇‍♂️).&lt;/li&gt;
&lt;li&gt;Even though iPad is quite powerful, it also has a touch screen so this detection will rule it out.&lt;/li&gt;
&lt;li&gt;The only exception would be desktop computers with touch screens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I call version 2 as “High-quality mode”, and provide an option for users who are routed to version 1 to switch to this mode.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--a-1WGQb6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-try-high-quality-mode%402x.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--a-1WGQb6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-try-high-quality-mode%402x.jpg" alt="ExploreTrees.SG showing a link “Try high-quality mode?“"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This option will reload the page and switch to version 2 by appending &lt;code&gt;#hq&lt;/code&gt; to the URL. After that if the site crashes, the user can still go back to version 1.&lt;/p&gt;

&lt;p&gt;To further optimise the performance of the site, I make good use of the &lt;code&gt;renderingMode&lt;/code&gt; variable to conditionally enable or disable certain features on the map. I took one step ahead by applying &lt;a href="https://parceljs.org/code_splitting.html"&gt;code splitting&lt;/a&gt; with dynamic imports. I extract all version 2 related dependencies into a separate bundle and load it asynchronously on the page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;MapboxLayer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;AmbientLight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;SunLight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;LightingEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;ScatterplotLayer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;SolidPolygonLayer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;SimpleMeshLayer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;SphereGeometry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;msgpack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;polyline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;KDBush&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;geokdbush&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;circle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;throttle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./hq.bundle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Besides that, I did an interesting trick for the MessagePack data file. I realised that MessagePack doesn’t have an &lt;a href="https://github.com/msgpack/msgpack/issues/194"&gt;official MIME type&lt;/a&gt;. Even though I started using Parcel for bundling files, the site is still deployed to GitHub Pages via the &lt;a href="https://github.com/JamesIves/github-pages-deploy-action"&gt;GitHub Pages Deploy Action&lt;/a&gt;. This means gzip compression could only be available for certain file extensions.&lt;/p&gt;

&lt;p&gt;For example, &lt;code&gt;.js&lt;/code&gt; files will be served with gzip compression but not &lt;code&gt;.png&lt;/code&gt; files. I tried using &lt;code&gt;.mp&lt;/code&gt; extension for the MessagePack file and… it’s not compressed unfortunately 😟. Since the site traffic is also handled by Cloudflare, I looked through &lt;a href="https://support.cloudflare.com/hc/en-us/articles/200168396"&gt;the list of content types that Cloudflare will compress&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I needed a file format that… &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is whitelisted in the server's content type compression list, from either GitHub Pages or Cloudflare. I’m aware that &lt;a href="https://www.netlify.com/docs/headers-and-basic-auth/"&gt;Netlify allows custom headers&lt;/a&gt; which may be able to set gzip headers, but I’m not moving the site there for now.&lt;/li&gt;
&lt;li&gt;Bypasses Parcel’s &lt;a href="https://parceljs.org/assets.html"&gt;intelligent asset handling&lt;/a&gt;. For example, if &lt;code&gt;.json&lt;/code&gt; is used, Parcel will include the file content &lt;em&gt;into&lt;/em&gt; the JavaScript bundle.&lt;/li&gt;
&lt;li&gt;Is not confusing for me to revisit in the future. If I use &lt;code&gt;.png&lt;/code&gt;, which could work, the future me would be confused and wonder why it’s used for a non-image file 😅.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I tried pre-compressing the file into &lt;code&gt;.gz&lt;/code&gt; but weird things just happen when trying to read the file in JS. 😅&lt;/p&gt;

&lt;p&gt;In the end, I chose &lt;code&gt;.ico&lt;/code&gt; because it’s one of the formats that is a (image) binary &lt;em&gt;and&lt;/em&gt; can be gzip compressed at the same time. It is also rarely used, unlike normal image formats, which could prevent conflicts with other files. I could use &lt;code&gt;.ttf&lt;/code&gt; or &lt;code&gt;.otf&lt;/code&gt; extensions too but it could be confused with actual font files.&lt;/p&gt;

&lt;p&gt;I named the file as &lt;code&gt;trees.min.mp.ico&lt;/code&gt;, tested it and it works. 🤩&lt;/p&gt;

&lt;h2&gt;
  
  
  The launch and the aftermath
&lt;/h2&gt;

&lt;p&gt;On &lt;a href="https://twitter.com/cheeaun/status/1123053445546512384"&gt;30 April 2019&lt;/a&gt;, I finally relaunched &lt;a href="https://exploretrees.sg"&gt;ExploreTrees.SG V2&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--NtaDP27c--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-screenshot.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--NtaDP27c--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-screenshot.gif" alt="GIF screenshot of ExploreTrees.SG"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Despite all my effort rewriting the code and pushing the limits, I’m especially proud of the 3D trees. It feels like an &lt;strong&gt;achievement-unlocked&lt;/strong&gt; moment for me. 🤩&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ODIQjxvs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-3d-trees-overview%402x.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ODIQjxvs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/exploretrees-sg-3d-trees-overview%402x.jpg" alt="ExploreTrees.SG showing an overview of all 3D trees"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It got &lt;a href="https://www.reddit.com/r/singapore/comments/bj0xze/my_friend_took_nparkss_data_and_built_a_site_for/"&gt;featured on Reddit /r/singapore&lt;/a&gt;, thanks to a friend 😉.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--zDSnpvuc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/reddit-friend-took-nparks-data-built-site-visualise-trees-singapore%402x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--zDSnpvuc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/reddit-friend-took-nparks-data-built-site-visualise-trees-singapore%402x.png" alt='Reddit post on /r/singapore titled “My friend took NParks’s data and built a site for people to visualise the various trees around Singapore"'&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feedback has been positive so far. I’m surprised that the conditional routing for Version 1 and 2 seems smooth enough that no one noticed it. 😝&lt;/p&gt;

&lt;p&gt;A day after the launch, I &lt;a href="https://twitter.com/cheeaun/status/1123589545230880769"&gt;received a pretty interesting email&lt;/a&gt; at 2 AM.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Adx-FaRW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/software/email-map-building-help%402x.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Adx-FaRW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/software/email-map-building-help%402x.jpg" alt="Email titled “Map Building Help”, received at 2:14 AM"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So this person asked me for my &lt;em&gt;advice&lt;/em&gt; on creating a map for all &lt;strong&gt;durian trees in Singapore&lt;/strong&gt;! I was reading this in the morning, had a good laugh and ignored it. 😂&lt;/p&gt;

&lt;p&gt;It was Labor Day and there’s no work. In the afternoon, I had a hunch that this person might &lt;strong&gt;not&lt;/strong&gt; be joking after all.&lt;/p&gt;

&lt;p&gt;So, I did some research on durians, found a Wikipedia page on &lt;a href="https://en.wikipedia.org/wiki/List_of_Durio_species"&gt;List of &lt;em&gt;Durio&lt;/em&gt; species&lt;/a&gt;, looked through the dataset and found &lt;strong&gt;286 Durio species&lt;/strong&gt;! 😱&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--1Z5GnvPz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/software/terminal-node-script-durian-durio-species%402x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--1Z5GnvPz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/software/terminal-node-script-durian-durio-species%402x.png" alt="Terminal showing a node script for durian listing down the durio species"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I didn’t know that there are durian trees in Singapore! Even more surprising, &lt;a href="https://en.wikipedia.org/wiki/Durian"&gt;durian&lt;/a&gt; trees have &lt;a href="http://www.wildsingapore.com/wildfacts/plants/fruittrees/durio/zibethinus.htm"&gt;flowers&lt;/a&gt;! 😱&lt;/p&gt;

&lt;p&gt;The next logical step is to quickly load them up &lt;a href="https://gist.github.com/cheeaun/1d68f6acb589a30335e1c7c927153d18"&gt;on a map&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--LbGNtq2U--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/durio-species-trees-map%402x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--LbGNtq2U--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/durio-species-trees-map%402x.png" alt="Durio species trees plotted on a map"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Looking at this, I was thinking to myself. Are there any other fruit trees in Singapore? I searched through the Internet and found a list of fruit tree species names. I was thinking of how to visualise the fruits on a map, maybe &lt;em&gt;still&lt;/em&gt; colorful circles or fruit icons instead? There could be too many exotic fruits to be shown so maybe need to be filtered to only those “popular” ones?&lt;/p&gt;

&lt;p&gt;Hmm, yeah… Lots of ideas, yet so little time.&lt;/p&gt;

&lt;p&gt;From my experience in &lt;a href="https://cheeaun.com/blog/2016/01/building-side-projects/"&gt;building side projects&lt;/a&gt;, it’s always good to hold on to an idea for some time before executing them. This time, I’ve already had &lt;strong&gt;so much fun&lt;/strong&gt; rebuilding everything, squeezing every inch of the performance, and even modelling 3D trees on a map. Most importantly I manage to launch my &lt;a href="https://github.com/cheeaun/exploretrees-sg"&gt;open-source&lt;/a&gt; work to the public before I got bored of it. 😆&lt;/p&gt;

&lt;p&gt;This has been fun and I’ve learnt a lot.&lt;/p&gt;

&lt;p&gt;So, till next time then. 😉&lt;/p&gt;

</description>
      <category>visualizations</category>
      <category>trees</category>
      <category>singapore</category>
    </item>
    <item>
      <title>What I learned from printing custom swags</title>
      <dc:creator>Chee Aun 🦄</dc:creator>
      <pubDate>Sun, 02 Jun 2019 00:00:00 +0000</pubDate>
      <link>https://dev.to/cheeaun/what-i-learned-from-printing-custom-swags-19l4</link>
      <guid>https://dev.to/cheeaun/what-i-learned-from-printing-custom-swags-19l4</guid>
      <description>&lt;p&gt;Apparently, I’ve been called the &lt;a href="https://www.youtube.com/watch?v=pzmXcI2soPs&amp;amp;t=214"&gt;“swag king” of Singapore&lt;/a&gt; 😅, mainly due to my voluntary work on printing swags like stickers and t-shirts for the past few years. My contributions have made a significant impact towards the lives of all meetup goers and conference attendees in Singapore, and perhaps other countries as well.&lt;/p&gt;

&lt;p&gt;Before I begin, a few disclaimers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I don’t &lt;em&gt;print&lt;/em&gt; swags literally. I simply order them from custom-printing shops and suppliers.&lt;/li&gt;
&lt;li&gt;I’m not an expert in custom-printing swags and still have a lot to learn, compared to those who have done this work for many years.&lt;/li&gt;
&lt;li&gt;I do this purely out of curiosity. Maybe as a hobby but it’s a rather expensive one 💸.&lt;/li&gt;
&lt;li&gt;What I’ve learned may not be applicable to other situations, places or countries. Things might have changed over time. It’s still best to self-experiment before trusting everything written here.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The beginnings
&lt;/h2&gt;

&lt;p&gt;I go to a lot of meetups and &lt;a href="https://cheeaun.com/blog/2015/12/why-i-attend-conferences/"&gt;conferences&lt;/a&gt;. Sometimes these events, usually conferences, give free swags and I’m always very happy to get them. They’ve become a reason for me to attend, and swags are always good for ice-breakers to know new like-minded people.&lt;/p&gt;

&lt;p&gt;After &lt;em&gt;years&lt;/em&gt; of getting free swags, I start wondering to myself, can I create my own swags? Instead of doing the taking, maybe it’s time for giving? 🎅&lt;/p&gt;

&lt;p&gt;I had no clue how this swag stuff works and honestly, been taking them for granted. I used to be quite &lt;em&gt;arrogant&lt;/em&gt; and kept asking the organisers for free swags, without realising how difficult it is to produce them in the first place.&lt;/p&gt;

&lt;p&gt;It all started with shirts.&lt;/p&gt;

&lt;p&gt;Before 2014, most t-shirt swags from the conferences that I attended, in Malaysia and Singapore, were not… super great. I’m not picky about shirts but the quality for conference shirts were not very high-quality, and I don’t usually complaint much since they are free. Maybe due to this fact, I didn’t pay much attention to them, including all other swags like stickers. I just like them because… they are free, &lt;em&gt;not&lt;/em&gt; because they’re awesome. Material-wise and design-wise were sub-par and didn’t leave much impression on me.&lt;/p&gt;

&lt;p&gt;In 2014, I traveled to San Francisco, attended a few meetups and conferences, and found one very important difference: the swags were much, much better. They &lt;em&gt;felt&lt;/em&gt; better somehow and at that time, I don’t really know why. I don’t know what makes it better or how it can be better at all. I thought all stickers and shirts are the same, right?&lt;/p&gt;

&lt;p&gt;Right…?&lt;/p&gt;

&lt;p&gt;I decided to find out why and did &lt;em&gt;a few&lt;/em&gt; experiments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shirts 👕
&lt;/h2&gt;

&lt;p&gt;I printed not one, but &lt;strong&gt;three&lt;/strong&gt; t-shirts for my own ‘CA’ logo. They were completely experimental and at that time, I have no idea what I’m doing.&lt;/p&gt;

&lt;p&gt;On 31 March 2014, I &lt;a href="https://twitter.com/cheeaun/status/454732849271152640"&gt;ordered&lt;/a&gt; a t-shirt.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--EoE46NrA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/cheeaun-t-shirt-v1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--EoE46NrA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/cheeaun-t-shirt-v1.jpg" alt="My first ‘CA’ logo t-shirt"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is my first ever t-shirt with my own logo on it. Yes, I order only &lt;em&gt;one&lt;/em&gt; t-shirt from &lt;a href="https://www.customink.com/"&gt;CustomInk&lt;/a&gt; and it costs US$24.75. It’s &lt;a href="https://www.customink.com/products/styles/american-apparel-usa-made-jersey-t-shirt/15000"&gt;American Apparel Jersey&lt;/a&gt;, 100% combed ringspun cotton, size S, and white.&lt;/p&gt;

&lt;p&gt;On 13 April 2014, I &lt;a href="https://twitter.com/cheeaun/status/460886871837376513"&gt;ordered&lt;/a&gt; a second t-shirt from CustomInk.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--0Z789eme--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/cheeaun-t-shirt-v2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--0Z789eme--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/cheeaun-t-shirt-v2.jpg" alt="My second ‘CA’ logo t-shirt"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This time, it’s a grey logo on black background. &lt;a href="https://www.customink.com/products/styles/gildan-softstyle-jersey-t-shirt/176100"&gt;Gildan Softstyle Jersey&lt;/a&gt;, 100% pre-shrunk ringspun cotton, size S and costs US$25.75.&lt;/p&gt;

&lt;p&gt;It’s pretty neat that CustomInk allows me to order only &lt;em&gt;one&lt;/em&gt; shirt as most vendors enforce a minimum order. This allows me to experiment and see how the shirt looks like.&lt;/p&gt;

&lt;p&gt;I personally don’t quite like the white shirt as the base, because it looks more &lt;em&gt;crumpled&lt;/em&gt;. The orange logo was too big because I was trying to measure it in inches, a measurement unit that I’m not familiar with (including the rest of the world 😏).&lt;/p&gt;

&lt;p&gt;It does match my personal branding, orange and white, but I realise that I have to change it when it comes to shirts, since it’s something that I would be wearing, unlike a web site or a magazine.&lt;/p&gt;

&lt;p&gt;Black (including other darker colors) is always the safest bet because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It’s hard to decide on a base color that suits everyone’s taste.&lt;/li&gt;
&lt;li&gt;Black is versatile enough to go well with anything to match your wardrobe.&lt;/li&gt;
&lt;li&gt;Black is always mysteriously stylish.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With my second attempt, I use black as the base color with a grey version of my logo. I’ve also made the logo smaller as I try to match the measurements from Iron Man’s arc reactor 😎.&lt;/p&gt;

&lt;p&gt;It was good… but the shirt material and printing were not great. The material is 100% cotton and feels &lt;em&gt;thick&lt;/em&gt;. The logo print feels like it &lt;em&gt;sticks&lt;/em&gt; on top of the shirt and not &lt;em&gt;part&lt;/em&gt; of the shirt. I was comparing it to &lt;a href="https://twitter.com/cheeaun/status/448636062231851008"&gt;some conference t-shirt swags&lt;/a&gt; and the level of quality is completely different.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--lVBKEN-N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/conference-t-shirts-firebase-ember-jquery-digital-ocean.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--lVBKEN-N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/conference-t-shirts-firebase-ember-jquery-digital-ocean.jpg" alt="Conference t-shirts; showing logos of Firebase, Ember, jQuery and Digital Ocean, and mascot of Ember, Tomster"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I did some research and found an interesting material called &lt;strong&gt;Tri-blend&lt;/strong&gt;. Googling it will reveal a few resources like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://creativeresources.threadless.com/what-is-tri-blend/"&gt;What does Tri-blend mean?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://libertymaniacs.com/pages/triblend"&gt;Triblend Shirts - A Guide to Awesomeness&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tri-blend is a combination of 3 fibers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cotton — Breathable, doesn’t trap heat.&lt;/li&gt;
&lt;li&gt;Polyester — Wrinkle-resistant.&lt;/li&gt;
&lt;li&gt;Rayon — Fit, durability, and crazy softness.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sounds amazing, right? It is! Ever since I tried them, I’ve been slowly updating my wardrobe over the years, replacing &lt;strong&gt;all&lt;/strong&gt; my 100%-cotton shirts to this.&lt;/p&gt;

&lt;p&gt;I wanted to print my logo on this material but found that the price was too costly to print &lt;em&gt;one&lt;/em&gt; shirt so I kind of gave up, until…&lt;/p&gt;

&lt;p&gt;On 8 December 2014, I ordered &lt;a href="https://twitter.com/cheeaun/status/545073311265206272"&gt;3 t-shirts with my Kopi.JS logo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--3vLlVHsn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/kopijs-t-shirts.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--3vLlVHsn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/kopijs-t-shirts.jpg" alt="The KopiJS t-shirts"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s &lt;a href="https://www.customink.com/products/styles/american-apparel-tri-blend-t-shirt/337000"&gt;American Apparel Tri-Blend&lt;/a&gt;, in S, M and L sizes, and costs me US$105.30 (Yes, still expensive 😂).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--d_j9I_7s--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/kopijs-t-shirt.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--d_j9I_7s--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/kopijs-t-shirt.jpg" alt="KopiJS t-shirt, worn by me"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I got size S &lt;a href="https://twitter.com/cheeaun/status/545400552901718016"&gt;for myself&lt;/a&gt; and gave the rest to my other &lt;a href="https://kopijs.org/"&gt;Kopi.JS&lt;/a&gt; co-founders ☕️.&lt;/p&gt;

&lt;p&gt;On 28 November 2015, I ordered another one (again).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--EwOvSD8Z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/cheeaun-t-shirt-v3.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--EwOvSD8Z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/cheeaun-t-shirt-v3.jpg" alt="My third ‘CA’ logo t-shirt"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the 3rd, and perhaps the last, t-shirt with my logo. I ordered it from &lt;a href="http://www.ooshirts.com"&gt;ooShirts&lt;/a&gt;. It’s &lt;a href="http://www.ooshirts.com/t-shirts/short-sleeve-shirts/next-level-triblend"&gt;Next Level Tri-Blend&lt;/a&gt;, DTG-printed, size S and costs US$31.77 (getting more expensive, I know 💸).&lt;/p&gt;

&lt;p&gt;I printed this one because I like tri-blend and I want my logo on it 😂. I also wanted to try &lt;em&gt;other&lt;/em&gt; brands that produce this material and see if there are any differences.&lt;/p&gt;

&lt;p&gt;Turns out it’s &lt;em&gt;not&lt;/em&gt; the brand, it’s the fabrics themselves! I don’t know about American Apparel but this Next Level shirt contains 50% polyester, 25% cotton and 25% rayon. &lt;a href="https://www.printful.com/blog/guide-to-cotton-polyester-and-blended-fabrics/"&gt;My&lt;/a&gt; &lt;a href="https://www.rushordertees.com/blog/cotton-polyester-5050-blend-which-better-you/"&gt;research&lt;/a&gt; revealed that there are also the 50/50 blends (50% cotton, 50% polyester) and 65/35 (65% cotton, 35% polyester), which are also known as &lt;strong&gt;polycotton&lt;/strong&gt;. And yeah, there are also other percentage ratios for the tri-blends besides the 50/25/25.&lt;/p&gt;

&lt;p&gt;In short, this is how I &lt;em&gt;naively&lt;/em&gt; see them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cotton is the most popular fabric for normal wear. Best for vibrant printing and also the cheapest option.&lt;/li&gt;
&lt;li&gt;Polyester is mostly used for sportswear. Lighter and silkier.&lt;/li&gt;
&lt;li&gt;Rayon is super soft and perfect for women dresses, skirts, bed sheets, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At first I don’t quite get rayon, since cotton and polyester already feels soft for me. So I looked into my wardrobe and found that I already have a few polycotton shirts (I didn’t realise it until my research 😅).&lt;/p&gt;

&lt;p&gt;One of them is my most favourite so far, the &lt;a href="https://twitter.com/cheeaun/status/532713230779428864"&gt;Dribbble t-shirt&lt;/a&gt; (with stickers 🤩).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--33afkrJe--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/dribbble-t-shirt.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--33afkrJe--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/dribbble-t-shirt.jpg" alt="The Dribbble t-shirt, with stickers"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s a &lt;a href="https://shop.dribbble.com/products/dribbble-tee"&gt;50/50 heather black&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Pretty soft. But not as soft as tri-blend. It feels slightly &lt;em&gt;heavier&lt;/em&gt; too. I don’t know how to explain it properly but it just &lt;em&gt;felt&lt;/em&gt; that way 🤷‍♂️. Maybe I just like the word “tri-blend” because it sounds cool 😂.&lt;/p&gt;

&lt;p&gt;So the problem here is that most, if not all, local printers in Singapore don’t have tri-blend t-shirts. 100% cotton is like &lt;em&gt;everywhere&lt;/em&gt; and the alternatives are usually dri-fit (100% polyester).&lt;/p&gt;

&lt;p&gt;After &lt;strong&gt;months&lt;/strong&gt; of research and googling, I finally found a local custom t-shirt printer in Singapore called &lt;a href="https://www.cottony.sg/"&gt;Cottony&lt;/a&gt; that supplies tri-blend!&lt;/p&gt;

&lt;p&gt;So heck yeah, I went in &lt;strong&gt;full force&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;On 16 August 2016, I ordered &lt;strong&gt;35&lt;/strong&gt; t-shirts (12 S, 13 M, 8 L, 2 XL) with the pixelated Kopi.JS logo. They are &lt;a href="https://www.nextlevelapparel.com/"&gt;Next Level&lt;/a&gt; Tri-blend Crew, vintage black, DTG-printed, and cost SG$770 (free shipping, SG$22 per shirt).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--M-CHKGu---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/pixelated-kopijs-t-shirt.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--M-CHKGu---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/pixelated-kopijs-t-shirt.jpg" alt="The pixelated KopiJS t-shirt, besides a Macbook for size comparison"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I gave them away for &lt;strong&gt;free&lt;/strong&gt;, mostly to those who are regulars to my &lt;a href="https://kopijs.org/"&gt;Kopi.JS&lt;/a&gt; meetups. It’s also the first time I order so many shirts, all packed inside a huge plastic bag.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ewHFdGDu--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/plastic-bag-pixelated-kopijs-t-shirt.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ewHFdGDu--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/plastic-bag-pixelated-kopijs-t-shirt.jpg" alt="Plastic bag containing all the pixelated KopiJS t-shirts"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Folks who &lt;a href="https://medium.com/kopi-js-community/kopi-js-in-2016-cf1b64f3f74a"&gt;received the shirts&lt;/a&gt;, &lt;a href="https://twitter.com/shujinh/status/768064653339439104"&gt;seem&lt;/a&gt; &lt;a href="https://twitter.com/meowlivia_/status/768236544997990400"&gt;to like it&lt;/a&gt; &lt;a href="https://twitter.com/webuildsg/status/807449278633213952"&gt;a lot&lt;/a&gt;, which makes me feel really glad. 🤗&lt;/p&gt;

&lt;p&gt;On 19 September 2016, I ordered &lt;em&gt;another&lt;/em&gt; batch, &lt;strong&gt;30&lt;/strong&gt; t-shirts (12 S, 15 M, 1 L, 2 XL) with the &lt;a href="https://dribbble.com/shots/2630937-Milo-Dinosaur-2nd-attempt-pixel-art-edition"&gt;Milo Dinosaur logo&lt;/a&gt;, from Cottony. It’s the same Next Level Tri-blend Crew, costs SG$606.60 (free shipping, SG$20.22 per shirt), but with screen printing instead.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--UGijiLx5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/plastic-bag-milo-dinosaur-t-shirt.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--UGijiLx5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/plastic-bag-milo-dinosaur-t-shirt.jpg" alt="Plastic bag containing the Milo Dinosaur t-shirts"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Okay now, I mentioned DTG (Direct to Garment) and screen printing. What’s the &lt;a href="https://www.printful.com/blog/dtg-vs-screen-printing/"&gt;difference&lt;/a&gt;?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DTG

&lt;ul&gt;
&lt;li&gt;Ideal for small batches, thus affects the minimum order 😏.&lt;/li&gt;
&lt;li&gt;Good for detailed designs and photos, with lots of colors.&lt;/li&gt;
&lt;li&gt;Gets more expensive for large batches.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Screen printing

&lt;ul&gt;
&lt;li&gt;Ideal for simple designs with limited colors (I had to specify the number of colors 😅)&lt;/li&gt;
&lt;li&gt;Cost-effective for large batches (e.g. above 30 pieces).&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is when it gets a little difficult. If I have a design that’s complicated, I would choose DTG but it’ll be more expensive. If I’m budget-conscious, I would have to tweak the artwork to contain lesser colors so that it would be screen-printed instead.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--b7g_1s0i--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/milo-dinosaur-t-shirt.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--b7g_1s0i--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/milo-dinosaur-t-shirt.jpg" alt="Printed Milo Dinosaur logo on a t-shirt"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Screen-printed artwork does look more &lt;a href="https://twitter.com/choonkeat/status/809671249202810880"&gt;vibrant&lt;/a&gt;, but also feels more &lt;em&gt;sticky&lt;/em&gt; (My personal description, some people described it as &lt;em&gt;thicker&lt;/em&gt;). DTG-printed artwork feels a little &lt;em&gt;rough&lt;/em&gt; but not sticky.&lt;/p&gt;

&lt;p&gt;After some conversation with the Cottony folks, the first batch that I ordered was DTG-printed with water-based ink while this second batch is screen-printed with "rubbered-dyed water-based ink", which I think they call it Plastisol(?).&lt;/p&gt;

&lt;p&gt;According to my &lt;a href="https://welogoit.com/blog/2018/05/types-of-ink-used-in-screen-printing"&gt;research&lt;/a&gt;, there are &lt;strong&gt;three types of ink&lt;/strong&gt; that can be used for screen printing; plastisol ink, water-based ink and discharge ink. 🤯 Oh my god, not only that I need to learn about the various printing methods, I also have to learn about the ink used in each printing method?!? Not to mention that these inks and printing methods also depend on the used fabric(s) for the shirts! 😱&lt;/p&gt;

&lt;p&gt;On November 2017, my co-organizers and I printed &lt;strong&gt;35&lt;/strong&gt; tri-blend t-shirts for &lt;a href="https://supersillyhackathon.sg/"&gt;Super Silly Hackathon&lt;/a&gt;. There were &lt;strong&gt;26&lt;/strong&gt; &lt;a href="https://www.bellacanvas.com/"&gt;BELLA+CANVAS&lt;/a&gt; unisex tri-blend t-shirts (10 S, 5 M, 4 L, 1 XXL) for the guys and &lt;strong&gt;9&lt;/strong&gt; &lt;a href="https://tultex.com/"&gt;Tultex&lt;/a&gt; ladies-cut polycotton t-shirts (5 S, 2 M, 2 L). There were no stock for BELLA+CANVAS ladies-cut so we fallback to Tultex.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--x8uJA-lq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/super-silly-hackathon-unicat-tshirt.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--x8uJA-lq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/super-silly-hackathon-unicat-tshirt.jpg" alt="The Super Silly Hackathon unicat tshirt"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Interestingly, there were concerns about this artwork as we were told that DTG printing method is unable achieve the neon-type colors of the design and will make them less vibrant. Heat transfer printing was suggested instead, which is also cheaper than DTG 🤔.&lt;/p&gt;

&lt;p&gt;At that time, I decided it’s fine to be less vibrant so we proceeded with DTG.&lt;/p&gt;

&lt;p&gt;This led me thinking, what is heat transfer? As I researched, I also found out about dye sublimation which is also &lt;a href="https://www.merchology.com/pages/heat-transfer-printing-vs-dye-sublimation"&gt;a form of heat transfer&lt;/a&gt;! They are kind of the same but different 😵. &lt;a href="https://www.coastalbusiness.com/blog/heat-transfer-paper-vs-sublimation.html"&gt;What’s the difference?&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Heat transfer

&lt;ul&gt;
&lt;li&gt;Works on light and dark-colored cotton, polyester and polycotton blends.&lt;/li&gt;
&lt;li&gt;Adds a layer on top of the garment.&lt;/li&gt;
&lt;li&gt;More affordable, not as durable.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Dye sublimation

&lt;ul&gt;
&lt;li&gt;Only works on polyester fabrics, no 100% cotton.&lt;/li&gt;
&lt;li&gt;Said to only work for white or light-colored fabrics.&lt;/li&gt;
&lt;li&gt;Ink becomes part of the fabric.&lt;/li&gt;
&lt;li&gt;More durable, more expensive.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Besides that, I also found that the shirt cuttings and sizes are different between brands as well, even when they’re all unisex. I think this is rather expected since there's really no standard followed by all these providers. 🤷‍♂️&lt;/p&gt;

&lt;p&gt;Here’s a list of things that I’ve learned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fabric&lt;/strong&gt; — cotton, polyester &amp;amp; rayon.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Printing method&lt;/strong&gt; — DTG, screen printing, heat transfer &amp;amp; dye sublimation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ink&lt;/strong&gt; — plastisol, water-based &amp;amp; discharge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sizing &amp;amp; cutting&lt;/strong&gt; — unisex &amp;amp; ladies-cut, differs by brand.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All the above affects or can be affected by these requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Number&lt;/strong&gt; of shirt pieces (minimum order or large batch orders)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Costs&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Number of &lt;strong&gt;colors&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality of print&lt;/strong&gt; on the shirt (sticky, rough, smooth, etc)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality of shirt&lt;/strong&gt; (durability, breathability, softness, number of washes, etc)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production lead&lt;/strong&gt; time and &lt;strong&gt;delivery&lt;/strong&gt; time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s like a balancing act. If I want the print to be vibrant, it could be printed on top of fabric and feels &lt;em&gt;sticky&lt;/em&gt; or &lt;em&gt;rough&lt;/em&gt;. If I have a low order count, it could only be limited to DTG since it’s cheaper. If I want a high-quality shirt with blended fabrics, some printing methods might not work. If the design is complex, it might take a longer time for production and I might not get the shirts in time.&lt;/p&gt;

&lt;p&gt;There’s also no way to measure the ROI (Return on Investment) for shirts, &lt;em&gt;except&lt;/em&gt; based on my observations and intuition. If I try to save cost and produce low-quality shirts for a conference, people might wear them once and never wear again. If the shirt sizes don’t match well, especially for women who requires ladies cut, they might just end up as pyjamas or get &lt;a href="https://konmari.com/"&gt;KonMari&lt;/a&gt;-ed.&lt;/p&gt;

&lt;p&gt;Most companies or organisations want their logos to &lt;em&gt;pop out&lt;/em&gt; by making them &lt;em&gt;big&lt;/em&gt;, vibrant and even on &lt;em&gt;both&lt;/em&gt; sides of the shirt. I personally find them &lt;em&gt;too much&lt;/em&gt;. Catchy logos do grab attention, which serves the purpose, but it’s weird when people keep seeing my shirt instead of my face while walking down the street or having a conversation. It's a typical “&lt;a href="https://knowyourmeme.com/memes/my-eyes-are-up-here"&gt;my eyes are up here&lt;/a&gt;” scenario for women, which atypically happens to me as a man 😅. If I wear a Google t-shirt, people assume that I work there and I have to keep explaining that I’m not, over and over again 😅.&lt;/p&gt;

&lt;p&gt;For the past years of collecting t-shirts, wearing them and printing them, I’ve formulated the &lt;strong&gt;ultimate&lt;/strong&gt; ingredients for the &lt;strong&gt;best&lt;/strong&gt; t-shirt swag &lt;em&gt;ever&lt;/em&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tri-blend&lt;/strong&gt; fabric, just because. Polycotton, if budget-conscious.&lt;/li&gt;
&lt;li&gt;Screen printing with &lt;strong&gt;discharge ink&lt;/strong&gt;. Or any printing method that lets the ink becoming part of the fabric. DTG as the last reliable fallback.&lt;/li&gt;
&lt;li&gt;Single-colored &lt;strong&gt;gray logo&lt;/strong&gt; on black shirts or dark-colored shirts. No catchy colors on the artwork. Let it blend naturally into the shirt, thanks to the ink.&lt;/li&gt;
&lt;li&gt;Go an extra mile and &lt;a href="https://geekfeminism.wikia.org/wiki/T-shirts"&gt;provide &lt;strong&gt;fitted-cut&lt;/strong&gt; shirts&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Personally I haven’t manage to achieve these &lt;em&gt;goals&lt;/em&gt; yet, but I’ll be experimenting more in near future.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stickers 👾
&lt;/h2&gt;

&lt;p&gt;Stickers are easier to print than shirts. As a swag option, it takes up less space and weight to carry around. Most people, if not all, generally like stickers, regardless of gender or background.&lt;/p&gt;

&lt;p&gt;I collect stickers from conferences and meetups. Sometimes, I &lt;a href="https://twitter.com/cheeaun/status/494040132157054977"&gt;even&lt;/a&gt; &lt;a href="https://twitter.com/cheeaun/status/678133207234031616"&gt;buy&lt;/a&gt; &lt;a href="https://twitter.com/cheeaun/status/749984711011819520"&gt;them&lt;/a&gt; &lt;a href="https://twitter.com/cheeaun/status/1030378312701378560"&gt;if the&lt;/a&gt; &lt;a href="https://twitter.com/cheeaun/status/1077768107458887680"&gt;designs are&lt;/a&gt; &lt;a href="https://twitter.com/cheeaun/status/1084031032406401025"&gt;super nice&lt;/a&gt; or when &lt;a href="https://twitter.com/cheeaun/status/804229555472842754"&gt;there are&lt;/a&gt; &lt;a href="https://twitter.com/cheeaun/status/1003665983276441600"&gt;discounts&lt;/a&gt;. I don’t stick them on my laptop and I don’t keep them either. I just give them away &lt;strong&gt;for free&lt;/strong&gt;, regularly at events like &lt;a href="https://geekbrunch.sg/"&gt;Geek Brunch SG&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://twitter.com/khorkexin/status/995182811718926336"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--kNEIQJvP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/stickers-pot-geek-brunch-sg-2018.jpg" alt="Stickers pot at Geek Brunch SG 2018"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I started my sticker printing experiments in 2014. I ordered &lt;a href="https://twitter.com/cheeaun/status/453946946340679680"&gt;stickers of my own ‘CA’ logo&lt;/a&gt; from &lt;a href="https://www.stickermule.com/"&gt;Sticker Mule&lt;/a&gt; on &lt;a href="https://twitter.com/kamal/status/453977995305181185"&gt;10 April&lt;/a&gt;. &lt;strong&gt;50&lt;/strong&gt; die-cut stickers, 51×51mm and costs US$53 (free shipping, US$1.06 per sticker).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--rBtl2qTY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/ca-logo-stickers-stickermule-t-shirt.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--rBtl2qTY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/ca-logo-stickers-stickermule-t-shirt.jpg" alt="My ‘CA’ logo stickers, with a free Sticker Mule t-shirt"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s pretty neat and nicely &lt;a href="https://www.stickermule.com/support/how-will-you-package-my-stickers"&gt;shrink-wrapped&lt;/a&gt;. The free t-shirt was part of a promotion at that time. I think it's 100% cotton 😏.&lt;/p&gt;

&lt;p&gt;On 29 May 2014, I ordered a second batch from a different provider called &lt;a href="https://www.moo.com/"&gt;MOO&lt;/a&gt;. &lt;strong&gt;52&lt;/strong&gt; kiss-cut stickers and costs US$12.74 (US$1.75 shipping with US$10 promo code discount, US$0.25 per sticker).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--IM7MLguz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/ca-logo-stickers-moo.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--IM7MLguz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/ca-logo-stickers-moo.jpg" alt="My ‘CA’ logo-stickers, from MOO"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This obviously looks different from the first batch 😅. The stickers are kind of &lt;em&gt;grouped&lt;/em&gt; together in a 4×4 grid instead of separated individually. It’s not quite visible in the photos above but this batch is actually &lt;em&gt;glossier&lt;/em&gt; than the first one. At that time, I really did not expect this kind of differences at all.&lt;/p&gt;

&lt;p&gt;On 12 November 2014, I printed the KopiJS stickers from a local vendor in Singapore called &lt;a href="https://www.touch-print.com.sg/"&gt;Touch &amp;amp; Print&lt;/a&gt;. &lt;strong&gt;70&lt;/strong&gt; pieces, 5×5cm and costs SG$21.40 (self collection, SG$0.31 per sticker). On 19 November 2014, I &lt;a href="https://twitter.com/cheeaun/status/535731502789558272"&gt;tried again printing&lt;/a&gt; the &lt;a href="https://twitter.com/cheeaun/status/536020524032200705"&gt;KopiJS stickers&lt;/a&gt; from another local vendor called &lt;a href="https://www.onedayprint.com.sg/"&gt;Onedayprint&lt;/a&gt;. &lt;strong&gt;50&lt;/strong&gt; pieces, 5×5cm and costs SG$32.35 (self collection, SG$12 &lt;em&gt;processing&lt;/em&gt; fee, SG$0.65 per sticker).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Iy7WHZLn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/kopijs-stickers-onedayprint.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Iy7WHZLn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/kopijs-stickers-onedayprint.jpg" alt="KopiJS stickers, from Onedayprint"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s… &lt;em&gt;huge&lt;/em&gt;. Not the artwork but the whole sticker sheets themselves! Also, quite evidently on this photo, they look &lt;em&gt;very&lt;/em&gt; glossy.&lt;/p&gt;

&lt;p&gt;It’s pretty troublesome to bring these sheets around. During &lt;a href="https://github.com/KopiJS/kopi.js/issues/15"&gt;KopiJS #7&lt;/a&gt; meetup, I tried encouraging folks to grab these stickers and have to pass a pair of scissors around for them to cut the individual stickers out 😅. Compared to my first batch (CA logo from Sticker Mule), the sticker distribution flow and sticker &lt;em&gt;acquisition&lt;/em&gt; experience are… not good enough 😓.&lt;/p&gt;

&lt;p&gt;To double-confirm my judgement, I &lt;a href="https://twitter.com/cheeaun/status/545045379738116096"&gt;ordered another batch&lt;/a&gt; of KopiJS stickers on 27 November 2014, from Sticker Mule.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--UKICcWYK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/kopijs-stickers-sticker-mule.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--UKICcWYK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/kopijs-stickers-sticker-mule.jpg" alt="KopiJS stickers, from Sticker Mule"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;100&lt;/strong&gt; die-cut stickers, 51×49mm and costs US$68 (US$0.68 per sticker). They are not glossy, not reflective, slightly &lt;em&gt;thicker&lt;/em&gt;, cut into individual pieces instead of sheets, and again, shrink-wrapped. So good!&lt;/p&gt;

&lt;p&gt;Okay, let’s step back a bit and do a little research here.&lt;/p&gt;

&lt;p&gt;There are &lt;a href="https://www.stickermule.com/blog/die-cut-stickers-vs-kiss-cut-stickers"&gt;two terms&lt;/a&gt; for sticker cuttings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Die-cut — cuts through the vinyl and paper backing.&lt;/li&gt;
&lt;li&gt;Kiss-cut — cuts through the vinyl but not the paper backing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Personally I think die-cut looks cooler because the edges are cut along the shape of the artwork or logo. The only exception that die-cut won’t make sense would be square-shaped or rectangle-shaped artworks.&lt;/p&gt;

&lt;p&gt;Die-cut also makes it easy to &lt;em&gt;preview&lt;/em&gt; how the sticker would look like on any surface without sticking it first.&lt;/p&gt;

&lt;p&gt;From my observations, kiss-cut is only useful if there are multiple artworks to be printed at once. So it can be roughly &lt;a href="https://www.stickermule.com/products/sticker-sheets"&gt;5 to 6 artworks printed on a sheet&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here’s an example of a good kiss-cut sticker from &lt;a href="https://www.reddotrubyconf.com/"&gt;RedDotRubyConf&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--kQmYanc4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/reddotrubyconf-kiss-cut-stickers.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--kQmYanc4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/reddotrubyconf-kiss-cut-stickers.jpg" alt="RedDotRubyConf kiss-cut stickers"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Unfortunately in Singapore, there’s quite a number of local print shops that neither does die-cut or even understand what it means sometimes. To speed up production and reduce cost, it’s easier and more efficient to print an artwork in grids on a large piece of sheet, and later cut it out with a sheet cutter machine. And… that is &lt;em&gt;if&lt;/em&gt; the print shop provide the cutting service in the first place 😅.&lt;/p&gt;

&lt;p&gt;Besides the cutting, there’s one more thing which is the &lt;em&gt;types&lt;/em&gt; of &lt;a href="http://www.joshcornett.me/what-is-sticker-lamination/"&gt;lamination&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Glossy lamination — reflective, shiny, and more vibrant colors.&lt;/li&gt;
&lt;li&gt;Matte lamination — nice texture feel, slightly muted palette and lower contrast.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Either one is fine, though I personally prefer matte lamination just because of the feel and texture 😊.&lt;/p&gt;

&lt;p&gt;On January 2016, as I was &lt;em&gt;desperately&lt;/em&gt; looking for cheaper-yet-good alternatives to Sticker Mule (US-based), I found a new vendor called &lt;a href="https://stickerhd.com/en/"&gt;StickerHD&lt;/a&gt;, based in Taiwan. It looks pretty new at that time and I couldn’t find any information if they could ship to Singapore, so I emailed them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--0IoAYzwy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/software/sticker-delivery-stickerhd-email%402x.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--0IoAYzwy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/software/sticker-delivery-stickerhd-email%402x.jpg" alt="I sent an email with the subject ‘Sticker delivery' to StickerHD, asking them if they deliver to Singapore. They replied yes with two options of ordering."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Turns out I could be the first one in Singapore, or outside Taiwan, to order from them 😆.&lt;/p&gt;

&lt;p&gt;My &lt;a href="https://twitter.com/cheeaun/status/695599738708594690"&gt;first order&lt;/a&gt; from StickerHD was &lt;strong&gt;50&lt;/strong&gt; die-cut 5×5cm stickers of my &lt;a href="https://dribbble.com/shots/2454364-KopiJS-logo-pixel-art-edition"&gt;pixelated KopiJS&lt;/a&gt; artwork. It costs NT$620 (NT$70 for shipping, NT$11 per sticker; roughly SG$0.47 or US$0.33 per sticker).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--2q0aUVmH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/pixelated-kopijs-stickers-stickerhd.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--2q0aUVmH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/pixelated-kopijs-stickers-stickerhd.jpg" alt="Pixelated KopiJS stickers, from StickerHD"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Not only that it’s shrink-wrapped, it’s also packed inside a sealed bag! This is more convenient than Sticker Mule because whenever I open up the shrink wrap for a deck of stickers, they might fall out so I always need another storage space to keep them.&lt;/p&gt;

&lt;p&gt;The quality is also on-par with Sticker Mule, at a much cheaper price. Shipping took maximum two weeks from Taiwan to Singapore.&lt;/p&gt;

&lt;p&gt;I also quite like this detailed preview confirmation step &lt;em&gt;before&lt;/em&gt; they start printing:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--mV9onRea--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/figures/diagram/pixelated-kopijs-preview-confirmation-stickerhd%402x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--mV9onRea--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/figures/diagram/pixelated-kopijs-preview-confirmation-stickerhd%402x.png" alt="Pixelated KopiJS sticker preview confirmation, from StickerHD"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It shows how your stickers will look like, with exact measurements! Since the size affects the pricing and is based on the actual dimensions of the artwork, the vendor has to readjust the width and height ratio instead of strictly limiting them as a maximum length. If I choose to print a non-rectangle artwork on a 5×5cm sticker, it should be readjusted accordingly, for example 4.5×5.5cm, instead of maybe 4×5cm.&lt;/p&gt;

&lt;p&gt;Ever since then, I printed &lt;em&gt;a whole lot&lt;/em&gt; of stickers from StickerHD 🤩.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://twitter.com/cheeaun/status/711064410983309313"&gt;3 March 2016&lt;/a&gt; — &lt;strong&gt;100&lt;/strong&gt; die-cut &lt;a href="https://singaporecss.github.io/"&gt;SingaporeCSS&lt;/a&gt; stickers and &lt;strong&gt;100&lt;/strong&gt; rectangle &lt;a href="https://github.com/SingaporeJS"&gt;SingaporeJS&lt;/a&gt; stickers, costs NT$2,200 (NT$200 for shipping, NT$10 per sticker; roughly SG$0.42 or US$0.30 per sticker).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--gj9QCip7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/singaporecss-singaporejs-stickers-stickerhd.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--gj9QCip7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/singaporecss-singaporejs-stickers-stickerhd.jpg" alt="SingaporeCSS and SingaporeJS stickers, from StickerHD"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://twitter.com/kopi_js/status/731482519590326273"&gt;10 April 2016&lt;/a&gt; — &lt;strong&gt;100&lt;/strong&gt; KopiJS, &lt;strong&gt;100&lt;/strong&gt; pixelated KopiJS and &lt;strong&gt;100&lt;/strong&gt; Milo Dinosaur stickers, costs NTD$3,200 (NT$200 for shipping, NT$10 per sticker).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--8BhBLEF---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/pixelated-kopijs-milo-dinosaur-stickers-stickerhd.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--8BhBLEF---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/pixelated-kopijs-milo-dinosaur-stickers-stickerhd.jpg" alt="Pixelated KopiJS and Milo Dinosaur stickers, from StickerHD"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And &lt;strong&gt;way, way more…&lt;/strong&gt; 💸💸💸&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;18 May 2016 — &lt;strong&gt;100&lt;/strong&gt; stickers, NT$1,240.&lt;/li&gt;
&lt;li&gt;28 July 2016 — &lt;strong&gt;150&lt;/strong&gt; stickers, NT$1,760.&lt;/li&gt;
&lt;li&gt;6 August 2016 — &lt;strong&gt;400&lt;/strong&gt; stickers, NT$3,880.&lt;/li&gt;
&lt;li&gt;8 September 2016 — &lt;strong&gt;200&lt;/strong&gt; stickers, NT$2,020.&lt;/li&gt;
&lt;li&gt;3 October 2016 — &lt;strong&gt;120&lt;/strong&gt; stickers, NT$1,580.&lt;/li&gt;
&lt;li&gt;27 October 2016 — &lt;strong&gt;600&lt;/strong&gt; stickers, NT$5,540.&lt;/li&gt;
&lt;li&gt;1 March 2017 — &lt;strong&gt;350&lt;/strong&gt; stickers, NT$3,870.&lt;/li&gt;
&lt;li&gt;9 June 2017 — &lt;strong&gt;700&lt;/strong&gt; stickers, NT$6,580.&lt;/li&gt;
&lt;li&gt;27 October 2017 — &lt;strong&gt;450&lt;/strong&gt; stickers (300 transparent), NT$4,700.&lt;/li&gt;
&lt;li&gt;18 November 2017 — &lt;strong&gt;450&lt;/strong&gt; stickers (200 transparent), NT$5,370.&lt;/li&gt;
&lt;li&gt;17 December 2017 — &lt;strong&gt;600&lt;/strong&gt; stickers (400 transparent), NT$6,310.&lt;/li&gt;
&lt;li&gt;4 February 2018 — &lt;strong&gt;100&lt;/strong&gt; transparent stickers, NT$1,460.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note that &lt;a href="https://stickerhd.com/en/catalogs/transparent-die-cut"&gt;transparent stickers&lt;/a&gt; are die-cut but with transparent edges instead of white. They are slightly more expensive, but look better.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--rNhPVKfS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/hackerweb-stickers-stickerhd.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--rNhPVKfS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/hackerweb-stickers-stickerhd.jpg" alt="HackerWeb stickers, from StickerHD"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--twWvSuK3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/chicken-friendly-stickers-stickerhd.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--twWvSuK3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/chicken-friendly-stickers-stickerhd.jpg" alt="Chicken friendly stickers, from StickerHD"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7HTwa6Nq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/phpconfasia-stickers-stickerhd.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7HTwa6Nq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/phpconfasia-stickers-stickerhd.jpg" alt="PHPConf.Asia stickers, from StickerHD"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--m-Hf8_WR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/huijing-stickers-stickerhd.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--m-Hf8_WR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/huijing-stickers-stickerhd.jpg" alt="Hui Jing’s avatar stickers, from StickerHD"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--GZuIfVVw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/sausheong-stickers-stickerhd.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GZuIfVVw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/sausheong-stickers-stickerhd.jpg" alt="Sau Sheong’s avatar stickers, from StickerHD"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--f1eEOycn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/the-geek-path-webuildsg-singaporecss-stickers-stickerhd.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--f1eEOycn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/the-geek-path-webuildsg-singaporecss-stickers-stickerhd.jpg" alt="The Geek Path, We Build SG and SingaporeCSS stickers, from StickerHD"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--BSYAKR0z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/super-silly-hackathon-engineers-sg-renaysha-stickers-stickerhd.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--BSYAKR0z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/super-silly-hackathon-engineers-sg-renaysha-stickers-stickerhd.jpg" alt="Super Silly Hackathon, Engineers.SG, and Aysha’s avatar stickers, from StickerHD"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--2vtRWRI8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/iosconf-sg-ssh-unicat-stickers-stickerhd.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--2vtRWRI8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/iosconf-sg-ssh-unicat-stickers-stickerhd.jpg" alt="iOS Conf SG, SSH, and unicat stickers, from StickerHD"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--6h-p2Jxi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/geekcamp-sg-techladies-ssh-stickers-stickerhd.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--6h-p2Jxi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/geekcamp-sg-techladies-ssh-stickers-stickerhd.jpg" alt="Geekcamp Singapore, TechLadies, and SSH stickers, from StickerHD"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--AKnkv_FV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/gophercon-sg-techladies-pixelated-kopijs-stickers-stickerhd.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--AKnkv_FV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/gophercon-sg-techladies-pixelated-kopijs-stickers-stickerhd.jpg" alt="Gophercon Singapore, TechLadies and pixelated KopiJS stickers, from StickerHD"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--zL_DPnCl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/ruby-on-rails-stickers-stickerhd.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--zL_DPnCl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/ruby-on-rails-stickers-stickerhd.jpg" alt="Ruby on rails stickers, from StickerHD"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--0dxeMP0j--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/elisha-stickers-stickerhd.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--0dxeMP0j--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/elisha-stickers-stickerhd.jpg" alt="Elisha’s avatar stickers, from StickerHD"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is a lot. And… there’s more 😆.&lt;/p&gt;

&lt;p&gt;Even though I’ve been loyally using StickerHD, I’m also constantly on the lookout for other vendors. I mean, it’s always good to check out the competitors and see who can do better, right?&lt;/p&gt;

&lt;p&gt;On 18 February 2017, I found a new vendor based in India called &lt;a href="https://juststickers.in/"&gt;Just Stickers&lt;/a&gt;. I &lt;a href="https://twitter.com/cheeaun/status/838608228527190016"&gt;ordered&lt;/a&gt; &lt;strong&gt;200&lt;/strong&gt; 2.4×2.4" &lt;a href="https://juststickers.in/introducing-juststickers-reusable-stickers/"&gt;&lt;em&gt;reusable&lt;/em&gt; stickers&lt;/a&gt;, which costs US$101.80 (US$8.50 shipping with 15% discount, US$0.51 per sticker).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--GvxuLBs7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/kopijs-stickers-just-stickers.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GvxuLBs7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/kopijs-stickers-just-stickers.jpg" alt="KopiJS stickers, from Just Stickers"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the so-called &lt;em&gt;reusable&lt;/em&gt; stickers which can be removed and reapplied multiple times, up to &lt;em&gt;almost&lt;/em&gt; 99 times! I never actually try stress testing them but I think it’s a pretty nice-to-have feature for stickers 🙂. Despite that, the stickers are kind of &lt;em&gt;thinner&lt;/em&gt; and the colors are a bit faded out compared to my other KopiJS stickers from other vendors 🤷‍♂️.&lt;/p&gt;

&lt;p&gt;On 21 April 2017, I found &lt;em&gt;another&lt;/em&gt; new vendor based in China, called &lt;a href="https://wz-zigpac.en.alibaba.com/"&gt;Zigpac on Alibaba&lt;/a&gt;, from &lt;a href="https://twitter.com/wesbos/status/839474532322795521"&gt;a tweet by Wes Bos&lt;/a&gt;. I had to order the stickers on the &lt;a href="https://www.alibaba.com/"&gt;Alibaba&lt;/a&gt; web site, which is my first time trying it out. The site can get quite overwhelming and feels like an online marketplace for factories and suppliers. It took me a while to understand some parts like the Message Center and stuff. Some of the vendors on Alibaba not only can produce stickers but also a whole wide variety of &lt;em&gt;things&lt;/em&gt;!&lt;/p&gt;

&lt;p&gt;At first, I ordered &lt;strong&gt;200&lt;/strong&gt; 5×5cm KopiJS stickers. They're matte-laminated, die-cut and cost US$80 including shipping (US$0.40 per sticker). After that, on 8 May 2017, they emailed me that their worker had mistakenly printed &lt;strong&gt;1,500&lt;/strong&gt; stickers(?!) and asked me if I want to take the rest of them at US$200, which totals up to US$280 (US$0.19 per sticker! So cheap?!? 😱).&lt;/p&gt;

&lt;p&gt;I know, it sounded fishy, but I took the deal anyway 😂. For the first time in my whole life, I &lt;a href="https://www.facebook.com/cheeaun/posts/10155456991151294"&gt;received&lt;/a&gt; &lt;strong&gt;1,500&lt;/strong&gt; stickers 💥💥💥.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--XqN4qD3Q--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/kopijs-stickers-zigpac-alibaba.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--XqN4qD3Q--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/kopijs-stickers-zigpac-alibaba.jpg" alt="KopiJS stickers, from Zigpac, Alibaba"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Surprisingly, the quality of the stickers is pretty decent 😅 (I was expecting it to be really bad). The stickers are shrink-wrapped and the price is very cheap. It’s hard to say if it’s cheaper than the other vendors, because for most vendors, it gets cheaper as the quantity increases.&lt;/p&gt;

&lt;p&gt;Ordering from China or India is still geographically too far away from Singapore. I wanted a vendor that’s nearer to reduce shipping time and cost. There are probably some local nearby vendors who &lt;em&gt;might&lt;/em&gt; be able to produce high-quality stickers (die-cut, matte laminated) but it’s really hard to find them.&lt;/p&gt;

&lt;p&gt;On July 2018, I finally found yet another vendor, based in Indonesia (and Singapore?), called &lt;a href="https://www.goodieswag.com/"&gt;GoodieSwag&lt;/a&gt;. I sent over a quick email enquiry to ask about the pricing and quality. After some back and forth, turns out the co-founder sort of knows me and have seen my stickers 😱.&lt;/p&gt;

&lt;p&gt;So I &lt;a href="https://twitter.com/cheeaun/status/1017336422322204672"&gt;ordered&lt;/a&gt; &lt;strong&gt;200&lt;/strong&gt; die-cut matte-laminated 4.5×5.5cm matte-vinyl Pixelated KopiJS stickers, which costs SG$72 (SG$20 shipping, SG$0.36 per sticker).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--_VYgJCBI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/pixelated-kopijs-stickers-goodieswag.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--_VYgJCBI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/pixelated-kopijs-stickers-goodieswag.jpg" alt="Pixelated KopiJS stickers, from GoodieSwag"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On 9 October 2018, I ordered stickers again from GoodieSwag. &lt;strong&gt;300&lt;/strong&gt; pieces of the &lt;a href="https://dribbble.com/shots/3923537-Unicat"&gt;Unicat artwork&lt;/a&gt;, same quality, at SG$95 (SG$20 shipping, SG$0.32 per sticker).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7E458gty--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/unicat-stickers-goodieswag.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7E458gty--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/unicat-stickers-goodieswag.jpg" alt="Unicat stickers, from GoodieSwag"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Obviously, I’m getting better at taking close-up photos of the stickers 😉.&lt;/p&gt;

&lt;p&gt;This is a full-bleed, where the printing extends to the edge of the sticker. The alternative is the white borders around the edges. Some stickers have white borders, some don’t (full-bleed). Why is that?&lt;/p&gt;

&lt;p&gt;There are a few resources on how to set up for full bleed printing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.stickermule.com/support/full-bleed"&gt;Sticker Mule: How to set up for full bleed printing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stickerobot.com/setup/bleed-and-safety-area/"&gt;Sticker Robot: Bleed &amp;amp; Safety Area&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://99designs.com.sg/blog/design-tutorials/how-to-design-a-die-cut-sticker/"&gt;99designs: How to design a die cut sticker&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of the time, clients (including myself) don’t really do this extra work on “bleeding” the artwork. Some vendors will proactively ask if the stickers require a white border or not. If no, then they will do the bleeding work and send a preview before start printing. Depending on the design, full-bleed may not make it look nicer and sometimes might not even be possible. It’s kind of like “extending” or “extrapolating” the edges of the artwork with matching backgrounds, which looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--I-8ACRNj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/unicat-sticker-full-bleed-design-preview.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--I-8ACRNj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/unicat-sticker-full-bleed-design-preview.jpg" alt="Unicat sticker full-bleed design preview"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The red line is where they’ll cut the edges of the stickers.&lt;/p&gt;

&lt;p&gt;After &lt;strong&gt;5 years of custom-printing over 7,200 stickers&lt;/strong&gt;, I start to feel like a "sticker god” 😂.&lt;/p&gt;

&lt;p&gt;Here are the things to consider when printing stickers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Die-cut or kiss-cut — I prefer the former because it looks better.&lt;/li&gt;
&lt;li&gt;Matte or glossy lamination — I’ve tried both, usually prefer matte but sometimes, glossy looks cool.&lt;/li&gt;
&lt;li&gt;Full-bleed or white borders — again, depends on the design.&lt;/li&gt;
&lt;li&gt;Reusability — A nice-to-have but not essential, in my opinion.&lt;/li&gt;
&lt;li&gt;Base location of vendor — whatever it takes to reduce shipping time and cost.&lt;/li&gt;
&lt;li&gt;Printing flow — ordering via web site or email, preview confirmation before printing, and customer support.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;em&gt;final&lt;/em&gt; most-ignored consideration would be design itself. There's no limits in terms of shapes, sizes and colors. Literally &lt;em&gt;anything&lt;/em&gt; can be printed as stickers, including even &lt;a href="https://twitter.com/cheeaun/status/482528758541209600"&gt;a cat’s face&lt;/a&gt; (photographs instead of computer-generated art). But as a swag, it doesn’t mean that they would work and people would want them. Even if people grab the stickers, it doesn’t mean they would stick them. And if no one sticks them, there’s no point printing stickers in the first place 💥.&lt;/p&gt;

&lt;p&gt;So, here’s my opinionated formula for the best sticker designs &lt;em&gt;ever&lt;/em&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cute animals, mascots or logos&lt;/strong&gt;. Boring company or project logos are fine but very limited to those who are fans of them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No words&lt;/strong&gt;. Unless it’s catchy or &lt;em&gt;very&lt;/em&gt; meaningful in some ways.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Colorful and eye-catching&lt;/strong&gt;. Because who wants to stick a plain-looking sticker right?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For example, GitHub does a pretty darn good job at this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--bzYbGFyA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/github-others-stickers-floor-devrelcon-tokyo-2017.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--bzYbGFyA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/github-others-stickers-floor-devrelcon-tokyo-2017.jpg" alt="Github (and others) stickers on the floor, at DevRelCon Tokyo 2017"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Magnets 🧲
&lt;/h2&gt;

&lt;p&gt;I used to believe that magnets will be the &lt;a href="https://twitter.com/cheeaun/status/777531446302871553"&gt;next logical step&lt;/a&gt; of my experiments. I mean, both stickers and magnets stick to a surface, so they are not much different, right?&lt;/p&gt;

&lt;p&gt;Anyway… it’s too tempting for me to resist trying it. On 14 April 2014, I &lt;a href="https://twitter.com/cheeaun/status/457360252577406976"&gt;ordered&lt;/a&gt; &lt;strong&gt;10&lt;/strong&gt; &lt;a href="https://www.zazzle.com/pd/spp/pt-zazzle_magnet?size=2.25&amp;amp;style=round_magnet"&gt;2.25" round magnet&lt;/a&gt; of my ‘CA’ logo, from &lt;a href="https://www.zazzle.com/"&gt;Zazzle&lt;/a&gt;. It costs US$35.06 (US$6.89 shipping, US$2.27 tax, US$8.60 discount, US$3.51 per magnet).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--gGIdWCnw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/ca-logo-magnets-zazzle.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--gGIdWCnw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/ca-logo-magnets-zazzle.jpg" alt="My ‘CA’ logo magnets, from Zazzle"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These were huge and thick! The magnets were pretty strong too, and really sticks to the refrigerator (Yes, I was skeptical). Clearly, they are way more expensive than stickers 💸.&lt;/p&gt;

&lt;p&gt;On 7 September 2016, I &lt;a href="https://twitter.com/cheeaun/status/777531446302871553"&gt;ordered&lt;/a&gt; &lt;strong&gt;30&lt;/strong&gt; 51×49mm KopiJS magnets from &lt;a href="https://www.stickermule.com/products/custom-magnets"&gt;Sticker Mule&lt;/a&gt; which costs US$51 (free shipping, US$1.70 per magnet).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ZmyX66pU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/kopijs-magnets-sticker-mule.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ZmyX66pU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/kopijs-magnets-sticker-mule.jpg" alt="KopiJS magnets, from Sticker Mule"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These are much, much thinner than the first one. It feels like a sticker being glued to a thin magnet at the back. It’s still &lt;a href="https://www.stickermule.com/support/how-thick-are-magnets"&gt;thicker&lt;/a&gt; than a normal sticker but feels unexpectedly &lt;em&gt;bendable&lt;/em&gt;. I thought magnets are supposed to be pure solids, but these are called &lt;a href="https://www.stickermule.com/support/are-your-magnets-flexible"&gt;&lt;em&gt;flexible&lt;/em&gt; magnets&lt;/a&gt;, which can be bought as &lt;a href="https://www.wonkeedonkeetools.co.uk/magnets-flexible/what-is-a-flexible-magnet/"&gt;sheets or tapes&lt;/a&gt;. 🤔&lt;/p&gt;

&lt;p&gt;The ‘CA’ magnets were quite a &lt;em&gt;hit&lt;/em&gt;. These KopiJS magnets… not so much as most people were confused whether they’re stickers or magnets. Even then, it’s hard to find places to &lt;em&gt;stick&lt;/em&gt; them, except… refrigerators 😅.&lt;/p&gt;

&lt;p&gt;It’s cool at first but the novelty wore off pretty quickly 😐.&lt;/p&gt;

&lt;h2&gt;
  
  
  Embroidered stickers 🧶
&lt;/h2&gt;

&lt;p&gt;After printing so many stickers, I wanted more. Magnets didn’t quite make it. I have to go back to basics and &lt;em&gt;extend&lt;/em&gt; my ideas from there.&lt;/p&gt;

&lt;p&gt;I don’t remember exactly how I got the idea for embroidered stickers but it was a few occasions when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I randomly saw embroidered iron-on patches in some hipster stores.&lt;/li&gt;
&lt;li&gt;I saw actual embroidered stickers in some conferences.&lt;/li&gt;
&lt;li&gt;I realised that those cute animal stickers need to be cuter by having some texture or something.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My first action was to find a local vendor that can print embroidered stickers. My initial encounter was with &lt;a href="https://pewpewpatches.com/"&gt;Pew Pew Patches&lt;/a&gt; based in Singapore. I emailed them for the pricing and it was roughly SG$4 per sticker with minimum order of 100. I’m not sure if they are standard market prices, but I decided that I can’t afford to spend so much on such experimental swags that are not &lt;em&gt;proven&lt;/em&gt; or &lt;em&gt;mainstream&lt;/em&gt; yet, like the shirts and normal stickers.&lt;/p&gt;

&lt;p&gt;Since I had my virgin experience printing stickers with Alibaba, I decided to try it again for embroidered stickers. Heck it took me a &lt;em&gt;long&lt;/em&gt; time to browse through all the vendors, comparing them, reading the reviews, checking the prices, judging the quality from their previous productions, and doing background checks on the vendors.&lt;/p&gt;

&lt;p&gt;At the same time, I was also learning a lot of new terms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Type — Embroidery or woven.&lt;/li&gt;
&lt;li&gt;Fabrics — twill, gingham, satin, felt, canvas or linen.&lt;/li&gt;
&lt;li&gt;Border — stitched/merrow, heat-cut, or laser-cut/die-cut.&lt;/li&gt;
&lt;li&gt;Backing — none, iron-on, fabric, hook-and-loop, pin, or self-adhesive (the one I want).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In all seriousness, I googled &lt;strong&gt;all&lt;/strong&gt; the terms and note down the pros and cons.&lt;/p&gt;

&lt;p&gt;Initially I don’t get the difference between embroidery and woven. I have no idea why I settled down on embroidery but it’s pretty cool to know there’s an alternative to it 🤔. Let’s get into the &lt;a href="https://www.gs-jj.com/blog/woven-patches-vs-embroidered-patches/"&gt;details&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Embroidery&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Thicker and &lt;em&gt;messier&lt;/em&gt;. Looks &lt;em&gt;classic&lt;/em&gt; or &lt;em&gt;vintage&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;More expensive.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;

&lt;p&gt;Woven&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Thinner threads, finer details, smoother surface, brighter colors, and looks &lt;em&gt;flatter&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Cheaper.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Naively speaking, woven is like an evolved, &lt;em&gt;modern&lt;/em&gt; version of embroidery. It’s better, but looks pretty &lt;em&gt;boring&lt;/em&gt; as I don’t see the pros against &lt;em&gt;actual&lt;/em&gt; printed stickers. Embroidery is thicker, less-refined and gives that old-school feeling. Since it’s thicker, it’s also nicer to touch it and feel the texture. Its imperfections are its charm.&lt;/p&gt;

&lt;p&gt;After doing much extensive research, I finally boiled down to &lt;strong&gt;two&lt;/strong&gt; potential vendors.&lt;/p&gt;

&lt;p&gt;On 18 March 2018, I &lt;a href="https://twitter.com/cheeaun/status/988651730425397248"&gt;ordered&lt;/a&gt; &lt;strong&gt;100&lt;/strong&gt; embroidered stickers of the &lt;a href="https://gophercon.sg/"&gt;Gophercon SG mascot&lt;/a&gt;, from &lt;a href="https://xbylabel.en.alibaba.com/"&gt;Shenzhen Xinbaoyuan Weaving Co., Ltd.&lt;/a&gt;. 100% embroidery, 5×5cm, twill fabric, heat-cut border, adhesive backing, and costs US$101 (US$26 DHL shipping, US$1.01 per sticker).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Gs3_s6o5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/gophercon-sg-embroidered-stickers-shenzen-xinbaoyuan-weaving.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Gs3_s6o5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/gophercon-sg-embroidered-stickers-shenzen-xinbaoyuan-weaving.jpg" alt="Gophercon Singapore mascot embroidered stickers, from Shenzen Xinbaoyuan Weaving Co., Ltd."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--A4fiHv7v--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/gophercon-sg-embroidered-stickers-close-up-shenzen-xinbaoyuan-weaving.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--A4fiHv7v--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/gophercon-sg-embroidered-stickers-close-up-shenzen-xinbaoyuan-weaving.jpg" alt="Gophercon Singapore mascot embroidered stickers close-up, from Shenzen Xinbaoyuan Weaving"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I have a feeling that this is actually woven and maybe… partially embroidered? 🤷‍♂️&lt;/p&gt;

&lt;p&gt;On 21 March 2018, I &lt;a href="https://twitter.com/cheeaun/status/983689034894462978"&gt;ordered&lt;/a&gt; a similar batch of &lt;strong&gt;100&lt;/strong&gt; embroidered stickers from &lt;a href="https://awellsgift.en.alibaba.com/"&gt;Shenzen Awells Gift Co., Ltd.&lt;/a&gt;. 100% embroidery, 5×5cm, heat-cut border, adhesive backing and costs US$69.44 (US$26.94 shipping, US$0.69 per sticker).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--byNgyor4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/gophercon-sg-embroidered-stickers-shenzen-awells-gift.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--byNgyor4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/gophercon-sg-embroidered-stickers-shenzen-awells-gift.jpg" alt="Gophercon Singapore mascot embroidered stickers, from Shenzen Awells Gift Co., Ltd."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--66aWZA4G--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/gophercon-sg-embroidered-stickers-close-up-shenzen-awells-gift.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--66aWZA4G--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/gophercon-sg-embroidered-stickers-close-up-shenzen-awells-gift.jpg" alt="Gophercon Singapore mascot embroidered stickers close-up, from Shenzen Awells Gift"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Looks better, though it’s not 100% die-cut, with the adhesive paper at the back.&lt;/p&gt;

&lt;p&gt;Here’s a comparison shot:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--fsvcCjeW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/gophercon-sg-embroidered-stickers-two-vendors-comparison.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--fsvcCjeW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/gophercon-sg-embroidered-stickers-two-vendors-comparison.jpg" alt="Gophercon Singapore mascot embroidered stickers comparison between two vendors"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pretty neat, huh?&lt;/p&gt;

&lt;p&gt;I don’t know if I’m doing this right but I really like the feel of the stickers. I think this is great for cute mascots or animals, as it gives that soft cuddly feeling to them 😊.&lt;/p&gt;

&lt;h2&gt;
  
  
  Socks 🧦
&lt;/h2&gt;

&lt;p&gt;I’m not really a socks person. As I’ve tried printing multiple types of swags, I wanted a new challenge.&lt;/p&gt;

&lt;p&gt;Socks as &lt;a href="https://www.instagram.com/p/9mP5o7S98g/"&gt;conference swags&lt;/a&gt; are not really new. &lt;a href="https://twitter.com/SlackHQ/status/467457718270189568"&gt;Slack socks&lt;/a&gt; are pretty sweet. Socks are becoming &lt;a href="https://footwearnews.com/2018/focus/socks/justin-trudeau-duck-socks-davos-world-economic-forum-487171/"&gt;a fashion statement&lt;/a&gt;, thanks to Canadian prime minister, Justin Trudeau.&lt;/p&gt;

&lt;p&gt;There’s also &lt;a href="https://twitter.com/sarahmei/status/981759745299107845"&gt;a tweet by Sarah Mei&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Petition to replace all conference tshirts with socks.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;React Rally also has this &lt;a href="https://twitter.com/ReactRally/status/978320677131505664"&gt;pretty smart idea&lt;/a&gt; on combining socks and sandals:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;regular brain: give out socks as conference swag&lt;/p&gt;

&lt;p&gt;galaxy brain: give out socks and sandals as conference swag so everyone can see the socks&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Yet it’s quite rare to see conferences in Singapore that give out socks. Maybe it’s not trendy yet? I don’t know. Why not give a try?&lt;/p&gt;

&lt;p&gt;I began my research by finding out how Slack socks are made. They were produced by &lt;a href="https://custom.sockclub.com/why"&gt;Sock Club&lt;/a&gt;, a custom socks vendor based in Austin, Texas, US. I learnt that there are &lt;a href="https://custom.sockclub.com/custom_sock_options"&gt;many types of socks&lt;/a&gt;, with &lt;a href="https://sockdrawer.com/pages/fiber-content"&gt;variety of fabrics&lt;/a&gt;, offered in &lt;a href="https://custom.sockclub.com/sock_sizing"&gt;a full range of sizes&lt;/a&gt;. Again, this is yet another recurring theme of me spending days learning all these stuff and trying to find out what’s the best among them 🕵️‍♂️.&lt;/p&gt;

&lt;p&gt;However, the very first thing that caught my attention is the socks sizing. I don’t remember having to choose my size when I grab the socks from conferences. Conference registrations usually ask for t-shirt size but &lt;strong&gt;not&lt;/strong&gt; sock size. According to &lt;a href="https://custom.sockclub.com/sock_sizing"&gt;Sock Club&lt;/a&gt;, sock sizing is based on shoe sizing, which leads to varied sizes made for men, women, kids and toddlers. It also emphasises the “One Size Fits Most” which makes it more confusing 😵.&lt;/p&gt;

&lt;p&gt;I dug though the &lt;a href="https://custom.sockclub.com/faqs"&gt;FAQ&lt;/a&gt; and found this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Most of our clients order our one-size-fits-most, size Adult Medium. That size fits about 90% of our customers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It doesn’t mention whether it's Adult size for men or women 🤷‍♂️. Also, they enforce a minimum order quantity (120) per design and &lt;strong&gt;per size&lt;/strong&gt;. So I can’t order like 10 S, 20 M and 5 L, instead I have to order 120 S, 120 M and 120 L, for a single design!?! 😱 This basically leaves people no choice but to settle with one sizing, right? I don’t understand why it’s done this way but suspect could be some production or manufacturing challenges that seems more &lt;em&gt;limited&lt;/em&gt; than t-shirts 🤔.&lt;/p&gt;

&lt;p&gt;Anyway, the second thing that caught my attention is that some socks have certain “unprintable” areas. I found &lt;a href="https://www.eversox.com/design/"&gt;some guidelines from Eversox&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--feyGpquY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/sock-design-guideline-graphics-cuff-heel-toe-eversox%402x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--feyGpquY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/web/sock-design-guideline-graphics-cuff-heel-toe-eversox%402x.png" alt="Sock design guideline, with details on graphics in the cuff, the heel and toe, on Eversox web site. It shows diagrams on which area of the socks that can’t be printed."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In short, the cuff, heel and toe areas are “prohibited” because they are prone to stretching or have knitting difficulties. This is also why Sock Club’s design mockup looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--fCU8DSUK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/software/sock-club-mockup-sample-custom-socks-template%402x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--fCU8DSUK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/screenshots/software/sock-club-mockup-sample-custom-socks-template%402x.png" alt="Sock Club’s mockup or sample custom socks template"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Those purple areas are “prohibited”.&lt;/p&gt;

&lt;p&gt;On 26 June 2018, I asked &lt;a href="https://twitter.com/charis"&gt;Charis&lt;/a&gt; about the vendor she &lt;a href="https://paper.dropbox.com/doc/Conference-Supplies-fjnsdJQKMOdnyMCyZrD4P"&gt;used&lt;/a&gt; for the &lt;a href="https://webconf.asia/"&gt;Webconf.asia&lt;/a&gt; conference, which I &lt;a href="https://cheeaun.com/blog/2017/12/2017-in-review/"&gt;attended&lt;/a&gt; &lt;a href="https://dev.to/2018/12/2018-in-review/"&gt;twice&lt;/a&gt;. It’s the &lt;a href="https://firebirdsocks.en.alibaba.com/"&gt;Fuzhou Firebird Sporting Goods Co., Ltd.&lt;/a&gt;. Two days later, I messaged them for a quotation and gave them this design; a repeatable image and how it should &lt;em&gt;potentially&lt;/em&gt; look like on the sock:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--RXJFAmdx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/figures/diagram/elephant-socks-mockup-phpconfasia-2018%402x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--RXJFAmdx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/figures/diagram/elephant-socks-mockup-phpconfasia-2018%402x.png" alt="Elephant socks mock-up, designed for PHPConf.Asia 2018"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the “Elephant” mascot artwork designed by &lt;a href="https://www.linkedin.com/in/mllim/"&gt;Lim Min Li&lt;/a&gt; for &lt;a href="https://2018.phpconf.asia/"&gt;PHPConf.Asia 2018&lt;/a&gt;. I felt that a repeated pattern of randomly rotated Elephants would be cool. Since this is the first time I’m trying this out, I wanted to make the design as simple as possible to prevent unexpected results.&lt;/p&gt;

&lt;p&gt;I hoped to get a sample delivered to reduce risk and the vendor told me that it would cost US$38 per design plus shipping cost of US$28 😅💸. So I decided to bite the bullet, made &lt;em&gt;dozens&lt;/em&gt; of design amendments and color corrections with them, and &lt;a href="https://twitter.com/cheeaun/status/1027832919069323264"&gt;finally ordered&lt;/a&gt; &lt;strong&gt;200&lt;/strong&gt; 23×23cm 6-color (one for base, 5 for the mascot) socks (plus one sample), delivered in a &lt;strong&gt;huge&lt;/strong&gt; box.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--xK8DEmAP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/elephant-socks-box-fuzhou-firebird-sporting-goods.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--xK8DEmAP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/elephant-socks-box-fuzhou-firebird-sporting-goods.jpg" alt="Elephant socks in a box, from Fuzhou Firebird Sporting Goods Co., Ltd."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Yeah, I totally miscalculated the amount of space needed for 200 socks 😅. It costs US$573 (US$139 shipping, US$24 PayPal fee, US$2.05 per pair) &lt;em&gt;and&lt;/em&gt; additional US$30 for material fee due to late change of &lt;em&gt;one&lt;/em&gt; fabric color.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--WRP558d0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/elephant-socks-packet-fuzhou-firebird-sporting-goods.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--WRP558d0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/elephant-socks-packet-fuzhou-firebird-sporting-goods.jpg" alt="Elephant socks in a packet, from Fuzhou Firebird Sporting Goods"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The socks are packed in these neat plastic bags.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--FsgbuDNK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/elephant-socks-close-up-fuzhou-firebird-sporting-goods.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--FsgbuDNK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/elephant-socks-close-up-fuzhou-firebird-sporting-goods.jpg" alt="Elephant socks close-up, from Fuzhou Firebird Sporting Goods"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--8hIUUf6J--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/elephant-socks-mascot-close-up-fuzhou-firebird-sporting-goods.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--8hIUUf6J--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/photos/objects/elephant-socks-mascot-close-up-fuzhou-firebird-sporting-goods.jpg" alt="Elephant socks mascot close-up shot, from Fuzhou Firebird Sporting Goods"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I’m not super picky about this, but honestly very satisfied with the socks! The colors are &lt;em&gt;still&lt;/em&gt; a bit off but seriously, colors are generally very &lt;em&gt;intricate&lt;/em&gt; in the real world 😣.&lt;/p&gt;

&lt;p&gt;This time, I didn’t print a second batch mainly due to time and delivery constraints. Taking this huge box to the conference venue is no easy task 😅. I do get some feedback that people liked it but it’s pretty hard to know if anyone wears them 🤷‍♂️. Unlike t-shirts or even stickers, it’s more difficult to spot people wearing your custom-designed socks.&lt;/p&gt;

&lt;p&gt;Nevertheless, this is really fun for me 😊.&lt;/p&gt;

&lt;h2&gt;
  
  
  The journey
&lt;/h2&gt;

&lt;p&gt;Throughout my whole journey of experiments with printing custom swags, it’s clear that I’ve changed and improved my procedures whenever I start exploring into new territories. In the beginning, I thought that this task is easy-peasy and underestimated the complexity behind all the free swags that I use to snag mindlessly. I did not do proper research and simply ordered without much considerations.&lt;/p&gt;

&lt;p&gt;Along the journey, I slowly take baby steps to learn every single fabric, every single material, every single manufacturing techniques, and every single detail. I try to reverse-engineer some of the swags I got and try to find out how they were made and where they're manufactured. I spent countless hours researching and always play the balancing game between &lt;a href="https://fastgood.cheap/"&gt;fast, good and cheap&lt;/a&gt; (and many more parameters). After that I check the &lt;em&gt;worthiness&lt;/em&gt; of the production as my goal is to &lt;strong&gt;delight&lt;/strong&gt; people with swags and make sure they &lt;em&gt;actually&lt;/em&gt; use or wear them.&lt;/p&gt;

&lt;p&gt;I’ve talked to a bunch of people and been misunderstood a few times on why I’m so &lt;em&gt;meticulous&lt;/em&gt;, when they only care about minimizing costs and not &lt;strong&gt;maximizing the swag experience&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The design matters a lot, as I found some designers were designing the materials without &lt;em&gt;understanding&lt;/em&gt; them in the first place. It’s like print designers who design for the web &lt;em&gt;without&lt;/em&gt; any comprehension of the medium. It’s like artists who portray an artwork without understanding the canvas, the brushes, and the paints used. I personally believe that &lt;strong&gt;understanding&lt;/strong&gt; the medium is very important because designs will always have certain challenges and constraints in the real world. If I could grasp the whole idea realistically, I’ll be able to produce better work that I’ll be proud of.&lt;/p&gt;

&lt;p&gt;At this point, I’m not really done with my experiments yet. I always feel that there are &lt;em&gt;more&lt;/em&gt; improvements to be done and more options to try out. As I mentioned, this is a pretty expensive “hobby”. However from these experiments, I’ve learned &lt;em&gt;so much&lt;/em&gt;, had lots of fun and have thoroughly helped others in creating their own swags for their own meetups, conferences and even companies.&lt;/p&gt;

&lt;p&gt;I do hope that my learnings could help more people, and hopefully encourage everyone to dive further into the world of awesome swags 😎.&lt;/p&gt;

</description>
      <category>swags</category>
      <category>tshirts</category>
      <category>stickers</category>
      <category>socks</category>
    </item>
    <item>
      <title>Building side projects</title>
      <dc:creator>Chee Aun 🦄</dc:creator>
      <pubDate>Sat, 25 May 2019 17:23:41 +0000</pubDate>
      <link>https://dev.to/cheeaun/building-side-projects-1eap</link>
      <guid>https://dev.to/cheeaun/building-side-projects-1eap</guid>
      <description>&lt;p&gt;On 3rd January 2016, I started my first side project of the year, despite having &lt;a href="http://cheeaun.github.io/"&gt;few more side projects&lt;/a&gt; up my sleeve. It's called &lt;a href="https://railrouter.sg/"&gt;RailRouter SG&lt;/a&gt;. It's a web app for people to explore MRT and LRT routes in Singapore. On &lt;a href="https://twitter.com/cheeaun/status/160380168739897344"&gt;20th January 2012&lt;/a&gt;, I built &lt;a href="https://busrouter.sg/"&gt;BusRouter SG&lt;/a&gt; which is initially known as 'Singapore Bus Routes Explorer'. It's a web app for people to explore bus stops and routes for all bus services in Singapore. Quite similar to RailRouter SG but for buses instead.&lt;/p&gt;

&lt;p&gt;I've built a lot of &lt;em&gt;things&lt;/em&gt;. From small web applications to desktop applications. From small libraries to browser add-ons. From basic &lt;em&gt;demo&lt;/em&gt; projects to web-based multiplayer games. Even then, I still find myself not &lt;em&gt;that great&lt;/em&gt; compared to other people with more than hundreds of projects, like &lt;a href="https://github.com/tj"&gt;TJ Holowaychuk&lt;/a&gt; and &lt;a href="https://github.com/sindresorhus"&gt;Sindre Sorhus&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So asking myself few questions. Why do I build it? What motivates me to build it? Is there a need for this kind of app? Do people request for this? Am I doing this for fame and fortune? Well, no. There's only one reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  Curiosity
&lt;/h2&gt;

&lt;p&gt;Some people call it an 'idea'. I prefer to call it 'curiosity'. Sayanee asked a question in my AMA, &lt;a href="https://github.com/cheeaun/ama/issues/7"&gt;How do you get your next idea to work on?&lt;/a&gt; and my brief &lt;a href="https://github.com/cheeaun/ama/issues/7#issuecomment-119830311"&gt;answer&lt;/a&gt; is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I usually don't find for ideas. For me, ideas come from being aware of things around you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And being aware leads to being curious:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;First is being aware of things around you, be curious about them, think about them, and things just naturally flow from that point.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;More than 10 years ago, I used to take buses all the time. At first, it's quite &lt;em&gt;exhilarating&lt;/em&gt; because there were no apps or smart phones at that time. No Google Maps, no directions, no GPS. Every time I take a bus, I have to board the bus and ask the driver if the bus goes to destination X. And once I tried it few times, it slowly becomes a routine for me to take bus from point A to point B and vice versa. I have to memorise bus numbers and their routes. Whenever I look at bus stops, I'll observe every bus that stops there and note to myself, "Ah okay, these buses stop here, I'll have to memorise them in case I need to come here one day." A question always pops into my mind, what if one day, I missed my stop? Where would I end up at? In fact, where does the bus end up anyway? I know that every bus has a route but I don't know how the route looks like.&lt;/p&gt;

&lt;p&gt;That &lt;em&gt;curiosity&lt;/em&gt; has stuck with me for a &lt;em&gt;long&lt;/em&gt; time.&lt;/p&gt;

&lt;p&gt;From my experience, there's a difference between &lt;em&gt;wanting&lt;/em&gt; to build something and &lt;em&gt;starting&lt;/em&gt; to build something. Curiosity is the &lt;strong&gt;first trigger&lt;/strong&gt; to make me &lt;em&gt;feel&lt;/em&gt; like I want to build something. Or perhaps in other words, I &lt;em&gt;need&lt;/em&gt; to build something. It's quite similar to what people say that they have an idea and they want to &lt;em&gt;materialize&lt;/em&gt; it into something more &lt;em&gt;solid&lt;/em&gt;. So now that I have the curiosity to &lt;em&gt;ignite&lt;/em&gt; my project, how do I &lt;em&gt;start&lt;/em&gt;? &lt;em&gt;When&lt;/em&gt; do I start?&lt;/p&gt;

&lt;h2&gt;
  
  
  Spark
&lt;/h2&gt;

&lt;p&gt;Spark is the &lt;strong&gt;second trigger&lt;/strong&gt;. For some people, this trigger happens in seconds &lt;em&gt;right after&lt;/em&gt; the first trigger. For others, it happens in few weeks, months or even years. For me and BusRouter SG, it happened on 7th January 2012 when I read this tweet:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;if i could view bus routes on a map the way you can view subway routes on a map, i would take them a whole lot more.&lt;/p&gt;

&lt;p&gt;—&lt;a href="https://twitter.com/mengwong/status/155511398653362177"&gt;@mengwong&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;9 days later, I &lt;a href="https://twitter.com/cheeaun/status/158891916786794497"&gt;tweeted&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If I'm not mistaken, there are 318 bus services and 4617 bus stops in Singapore.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;4 days after that, I &lt;a href="https://twitter.com/cheeaun/status/160380168739897344"&gt;launched BusRouter SG to the public&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Perhaps there's another term for this. It's kind of like a &lt;em&gt;magical moment&lt;/em&gt; for me to start building it. It feels like &lt;em&gt;good timing&lt;/em&gt; for me to start &lt;em&gt;now&lt;/em&gt; rather than later. This kind of &lt;em&gt;spark&lt;/em&gt; doesn't have to come from other people, places or any external &lt;em&gt;inspirations&lt;/em&gt;. Sometimes it comes from within, for example when I'm taking a shower, hiking up a hill, or talking to a friend.&lt;/p&gt;

&lt;p&gt;Obviously, this is not all that magical. Things don't just suddenly happen in the timing between the first trigger and the second trigger. For years, I subconsciously kept my curiosity somewhere at the back of my mind, and constantly adding small bits and pieces of information to slowly &lt;em&gt;grow&lt;/em&gt; the idea. I need to know where to get the data and which sites have it. I need to make sure there's a feature to detect my current location to instantly find nearby bus stops. I know that I can't draw &lt;em&gt;thousands&lt;/em&gt; of bus stops and routes on the map because it'll be too overwhelming for the user, so I need to think of a better UI for this. I kind of draw &lt;em&gt;mockups&lt;/em&gt; in my head and learn from how other apps present huge amount of data with a simple user interface. I probably need a better name for the app. I need to market it so that users would learn about it and use it, because I know there's no point launching an app that no one uses. I kind of want to display a realtime bus arrival information for every bus stop but the API was not perfect at that time, so I'll probably do that in the next phase.&lt;/p&gt;

&lt;p&gt;Some people call this '&lt;strong&gt;Product planning&lt;/strong&gt;', as an ongoing process to define a product's feature set. And it has to be simple and feasible in terms of time and money spent for development. That's when the term '&lt;strong&gt;Minimum Viable Product&lt;/strong&gt;' (MVP) came into the picture.&lt;/p&gt;

&lt;p&gt;From my experience in building apps, I learnt this the hard way. In fact, I don't even have or know the term until I read some articles that mention them. And I'm like, "Oh, there's a term for this? Okay, cool." So how &lt;em&gt;minimum&lt;/em&gt; does the product need to be? For my own side projects, I always try to estimate if I can finish it in a day. How about a week? Two weeks? A month? I usually avoid side projects that last more than a month because I always feel bored looking at the same thing over and over again. Basically maximum one month. Within a month, I have to launch it publicly. If somehow I'm &lt;em&gt;unable&lt;/em&gt; to publish it, I have to &lt;em&gt;at least&lt;/em&gt; make it open-source and let the world see my code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Launch
&lt;/h2&gt;

&lt;p&gt;This is the ultimate &lt;strong&gt;third trigger&lt;/strong&gt;. I call this a trigger because it's like a game-changer, well, depending on how you launch it. I've seen some developers didn't even manage to cross this line because of &lt;em&gt;lame&lt;/em&gt; reasons like the app is not perfect yet or the code is too ugly. I've also seen some developers did a &lt;em&gt;soft-launch&lt;/em&gt; for their side projects, which I think is pretty &lt;em&gt;lame&lt;/em&gt; too. When I launch a product, I tweet it. I tell people. I try to blog about it. I post it on Hacker News. I want many, many people to look at my imperfect app or ugly code.&lt;/p&gt;

&lt;p&gt;The moment when I launch a project, my senses are heightened. Suddenly I become aware of bugs that I didn't notice while coding. I start writing good READMEs so that developers understand what I did. I start writing good copy for non-technical users to understand what the product does. I check for grammatical errors, broken links and broken flows.&lt;/p&gt;

&lt;p&gt;It's addictive. After the project is launched, I start to learn how to market it, experiment with advertising, play around with SEO, think about monetisation, and even pitch it to other people. It's surprisingly &lt;strong&gt;a lot of work&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Every single time when I do all these work, I start to understand the situation and difficulties behind all the &lt;em&gt;non-engineering&lt;/em&gt; work. In a typical company, these are done by &lt;em&gt;other&lt;/em&gt; departments; marketing, sales, advertising, social media, designers, bloggers and copywriters. There are times when I find it funny that I &lt;em&gt;could&lt;/em&gt; understand what other (non-engineering) people do, but they don't understand what &lt;em&gt;I&lt;/em&gt; do 😥&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback
&lt;/h2&gt;

&lt;p&gt;When the project is public, I start to get many types of &lt;strong&gt;feedback&lt;/strong&gt; from people. Positive feedback makes me happy. Negative feedback makes me try harder. &lt;em&gt;Unconstructive&lt;/em&gt; feedback makes me learn how to ignore them. No feedback? Well... either try harder or be patient. It's &lt;em&gt;always&lt;/em&gt; surprising when the project that you thought &lt;em&gt;no one&lt;/em&gt; is going to care, &lt;em&gt;actually&lt;/em&gt; cares. If there's still no feedback, just move on, start another project, go though the whole cycle again starting from the first trigger!&lt;/p&gt;

&lt;p&gt;Sometimes I'll get one more type of feedback, which I think is &lt;em&gt;worse&lt;/em&gt; than negative ones. It's the kind of feedback that makes me feel really bad and start to lose my love on the project, which ultimately lose my &lt;strong&gt;passion&lt;/strong&gt; in building things. Obviously when we build stuff for the public, things don't always go your way. I'm not sure how to describe this but it's kind of like a moment when I ask myself, "Oh god, what have I done?". I've had my bad moments. I've done a lot of mistakes. I also know that some people have had it worse than me. One of them is Remy Sharp, who has the courage to even write a &lt;a href="https://remysharp.com/2015/09/14/jsbin-toxic-part-1"&gt;5-part story on the &lt;em&gt;dark side&lt;/em&gt; of JS Bin&lt;/a&gt;, especially &lt;a href="https://remysharp.com/2015/09/18/jsbin-toxic-part-5"&gt;part 5&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It's like a test for mental strength. A test to see if I'll give up and quit. As what &lt;em&gt;most&lt;/em&gt; people think, it's &lt;em&gt;just&lt;/em&gt; a side project, right? How bad could it be? Well, you'll never know until you launch it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maintenance
&lt;/h2&gt;

&lt;p&gt;I call this the &lt;strong&gt;maintenance&lt;/strong&gt; phase, the period of time when I have to continuously handle user feedback, do customer support, fix bugs, add features and improve the project. It's a bit like an &lt;em&gt;endurance&lt;/em&gt; test to see how long I can last maintaining everything and stay focused on them. At this stage, many things could happen. Some people end up &lt;em&gt;stop&lt;/em&gt; maintaining and starting something else. Some people manage to monetize their own project and let it self-sustaining. Some people manage to turn a side project into a startup, for example, &lt;a href="http://blog.natbat.net/post/61658401806/lanyrd-from-idea-to-exit-the-story-of-our"&gt;Lanyrd&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This 'maintenance' phase doesn't always work with the first trigger, curiosity, also known as the 'starter' phase. It's very difficult to start something new while maintaining the old stuff. Vice versa, it's hard to dedicate the time on maintenance when you keep creating new things.&lt;/p&gt;

&lt;p&gt;&lt;a href="http://jlongster.com/Starters-and-Maintainers"&gt;James Long mentioned&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There are two roles for any project: starters and maintainers. People may play both roles in their lives, but for some reason I’ve found that for a single project it’s usually different people. Starters are good at taking a big step in a different direction, and maintainers are good at being dedicated to keeping the code alive.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As for me, I try my best to play both roles. It's challenging. Sometimes I fail. Sometimes I got bored. Sometimes the projects I built became deprecated.&lt;/p&gt;

&lt;p&gt;Few weeks ago, I got &lt;a href="https://github.com/cheeaun/mangafeeder/issues/1"&gt;a new GitHub issue&lt;/a&gt; reported on an old project that I'm no longer using. The project is called &lt;a href="https://github.com/cheeaun/mangafeeder"&gt;mangafeeder&lt;/a&gt;, built in 2012, and never had any issues or pull requests until that very day. It was hosted on AppFog V1 which &lt;a href="https://www.ctl.io/knowledge-base/afv1/migration-faq/"&gt;became retired&lt;/a&gt; since December 15th last year, and I didn't even realise it. When I read the reported issue, I was very surprised that &lt;em&gt;someone&lt;/em&gt; is actually using it. Despite the fact that I've built a lot of things, this shouldn't be &lt;em&gt;that&lt;/em&gt; surprising anymore but I can't help it. I tried to redeploy the app to the new AppFog V2, found it quite troublesome so I decided to deploy it to Heroku instead. The site is up and I resolved the issue with a reply.&lt;/p&gt;

&lt;p&gt;The response I got is &lt;a href="https://github.com/cheeaun/mangafeeder/issues/1#issuecomment-168313991"&gt;this&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;awesome 👍&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Somehow it reminded me of the &lt;a href="https://www.youtube.com/watch?v=FirarNC85sU"&gt;WWDC 2012 developer appreciation video&lt;/a&gt;, especially the part where Emil Ovemar said:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It's really fantastic to see what we have created has made a difference in people's lives. Those reactions worth way more than any downloads.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;From time to time, I always see people getting excited or obsessed over the numbers. Number of downloads, likes, loves, retweets, reposts, reshares, stars, followers, fans, visits, impressions and clicks. For me, those are just &lt;em&gt;nice-to-haves&lt;/em&gt;. They are not really the most important thing in the world.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;number one reason&lt;/strong&gt; why I don't give up and will continue building things is the positive response from other people. The &lt;strong&gt;thank you's&lt;/strong&gt;. The "Oh my god, you saved my life" kind of feedback. Sometimes when I get 9 positive feedback and suddenly one negative feedback, it will make me feel bad for the rest of the day. I personally learnt that if one day, I get 9 negative feedbacks and one positive feedback, I should be happy instead, thinking that I helped at least one person.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond side projects
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7DVDmZH4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/figures/diagram/side-project-timeline.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7DVDmZH4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cheeaun.com/blog/images/figures/diagram/side-project-timeline.png" alt="Side project timeline"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Few days ago, on January 21st, I &lt;a href="https://www.facebook.com/photo.php?fbid=10153701648345189"&gt;gave a talk on this topic&lt;/a&gt;, at the time of writing, to a group of students in &lt;a href="https://generalassemb.ly/singapore"&gt;General Assembly Singapore&lt;/a&gt;. I drew this simple diagram as a visual way to describe the &lt;em&gt;complete&lt;/em&gt; 'side project timeline'. It's a rough sketch of how I visualize it in my mind.&lt;/p&gt;

&lt;p&gt;I mentioned that if you look at this timeline, it's a bit like a startup, but without the business strategy, hiring process, VC funding and money.&lt;/p&gt;

&lt;p&gt;I took a third look at this and realise that it's a bit like &lt;strong&gt;life&lt;/strong&gt; itself. 'Curiosity' begins when you're born into this world. 'Spark' probably begins when you start studying in school. 'Launch' happens when you graduate. 'Maintenance' is when you start working, buying a house, having a family and kids. Okay, pardon me, probably I got too far with this 😅&lt;/p&gt;

&lt;p&gt;Nevertheless, it's a &lt;strong&gt;journey&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A journey that I wish that more people would be able to experience and tell the story.&lt;/p&gt;




&lt;p&gt;Originally published at &lt;a href="https://cheeaun.com/blog/2016/01/building-side-projects/"&gt;cheeaun.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>sideprojects</category>
      <category>ideas</category>
    </item>
  </channel>
</rss>
