<?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: notbigmuzzy</title>
    <description>The latest articles on DEV Community by notbigmuzzy (@nbm).</description>
    <link>https://dev.to/nbm</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%2F909110%2F44fd4834-b4e3-4a39-a6ba-9292809cd316.jpg</url>
      <title>DEV Community: notbigmuzzy</title>
      <link>https://dev.to/nbm</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nbm"/>
    <language>en</language>
    <item>
      <title>Genregraphy - Cartographic History of Music Genres</title>
      <dc:creator>notbigmuzzy</dc:creator>
      <pubDate>Fri, 13 Mar 2026 13:10:49 +0000</pubDate>
      <link>https://dev.to/nbm/genregraphy-cartographic-history-of-music-genres-29j1</link>
      <guid>https://dev.to/nbm/genregraphy-cartographic-history-of-music-genres-29j1</guid>
      <description>&lt;p&gt;Genregraphy is an interactive map of music genres from 1950 to 2025. It groups genres by similarity and renders them as continents. The size and borders of each "country" change year by year based on release data.&lt;/p&gt;

&lt;p&gt;The system architecture runs on two ends. Python scripts scrape and normalize the historical release numbers offline. The browser then takes that dataset and uses D3.js to calculate the Voronoi polygon geometry on the fly, with Vue managing the timeline state.&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/notbigmuzzy/embed/QwKdVYe?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;




&lt;h3&gt;
  
  
  Scraping
&lt;/h3&gt;

&lt;p&gt;The underlying dataset comes from &lt;strong&gt;MusicBrainz&lt;/strong&gt; and &lt;strong&gt;Last.fm&lt;/strong&gt;, but clean timeline data for genre popularity was an absolute nightmare to extract. &lt;/p&gt;

&lt;p&gt;There is no single API endpoint for "all albums by genre per year." The data pipeline required a massive multi-step process. First, the script queries MusicBrainz for baseline releases ( I drink tea ). Then hits Last.fm to cross-reference tags and pull detailed descriptions ( I drink tea ).&lt;/p&gt;

&lt;p&gt;This forced me to deal with API rate limits, massive inconsistencies in tagging (sync "post-punk" vs "post punk"), and manual mappings of micro-genres to assign them to top-level families ( drink more tea ). &lt;/p&gt;

&lt;p&gt;The backend is a labyrinth of Python scripts that fetch, merge, deduplicate, and calculate the exact album counts for every year since 1950 ( while I drink tea). &lt;/p&gt;

&lt;p&gt;The final output consists of heavily optimized JSON shards grouped by decade.&lt;/p&gt;




&lt;h3&gt;
  
  
  Cartography
&lt;/h3&gt;

&lt;p&gt;To keep the terrain stable as years pass and data wildly fluctuates, the layout uses a strict macro-geography model:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Anchored Continents:&lt;/strong&gt; The logic groups micro-genres into 9 macro-families (Rock, Electronic, Jazz, etc.) and anchors them to precise cardinal directions. Rock &amp;amp; Metal live in the East and North. Electronic &amp;amp; Hip-Hop dominate the West. Reggae and Global Beats sit in the South. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Organic Borders:&lt;/strong&gt; &lt;strong&gt;D3.js&lt;/strong&gt; handles the geometry via nested hierarchies. The engine packs genres together based on volume, and these packed nodes serve as primary seeds for a &lt;strong&gt;Weighted Voronoi tessellation&lt;/strong&gt;. The Voronoi algorithm draws contiguous, polygon-shaped borders around these seeds, mutating raw numbers into a continuous, geographical landmass.  &lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To push hundreds of complex SVG paths efficiently while a user scrubs the timeline, the engine resolves the math in phases. It calculates the rigid macro-continents first to establish solid borders, then dynamically packs the mutating micro-genres inside them.&lt;/p&gt;




&lt;h3&gt;
  
  
  Code
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Here is the logic for terrain calculation without layout thrashing:&lt;/strong&gt;&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="c1"&gt;// First, map the macro-continents based on global album totals&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;macroHierarchy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;d3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hierarchy&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;allGroups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;yearData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;genre_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;name&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;trueRatio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;group&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;computedGlobalTotal&lt;/span&gt; &lt;span class="o"&gt;&amp;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;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;computedGlobalTotal&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&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="mf"&gt;0.001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;trueRatio&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// Prevent UI collapsing&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&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;a&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="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;localeCompare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&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="nx"&gt;name&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;macroPack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;d3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pack&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;width&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;macroNodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;macroPack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;macroHierarchy&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;leaves&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// Then, dynamically pack the actual genres inside their assigned continent limits&lt;/span&gt;
&lt;span class="nx"&gt;macroNodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;macroNode&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;d3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hierarchy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hierarchyData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Apply a logarithmic scale to the album counts so dominant &lt;/span&gt;
            &lt;span class="c1"&gt;// genres don't completely swallow the map &lt;/span&gt;
            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&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;value&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;value&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&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;log10&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;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&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="c1"&gt;// Determine the exact required diameter to fit our genres&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;minNeededDiameter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;minNodeR&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;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;totalSum&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="nx"&gt;minValue&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dynamicDiameter&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="nx"&gt;baseDiameter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;minNeededDiameter&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;pack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;d3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pack&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;dynamicDiameter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dynamicDiameter&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&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="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Frontend
&lt;/h3&gt;

&lt;p&gt;The D3 layer handles the map math, wrapped in an immersive shell powered by &lt;strong&gt;Vue 3&lt;/strong&gt; and &lt;strong&gt;Vite&lt;/strong&gt;. This combination forces the DOM updates to remain smooth. The visual jump from the early 1950s rock-and-roll expansion directly to the 2010s EDM explosion remains completely seamless.&lt;/p&gt;

&lt;p&gt;Live Demo - &lt;a href="https://notbigmuzzy.github.io/genregraphy/" rel="noopener noreferrer"&gt;https://notbigmuzzy.github.io/genregraphy/&lt;/a&gt;&lt;br&gt;
Source Code - &lt;a href="https://github.com/notbigmuzzy/genregraphy" rel="noopener noreferrer"&gt;https://github.com/notbigmuzzy/genregraphy&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Data disclamer
&lt;/h3&gt;

&lt;p&gt;To make this thing fun to use, some decisions had to be made:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The 120 Limit:&lt;/strong&gt; MusicBrainz and Last.fm return thousands of micro-genres. Plotting thousands of tiny nodes on a single map would not be realistic, so I chose a curated set of 120 foundational genres and umbrellas. Some will agree with the list, some will not, that is fine.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://raw.githubusercontent.com/notbigmuzzy/genregraphy/refs/heads/main/scripts/texts/genres.txt" rel="noopener noreferrer"&gt;120 Genres - text&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The "Round Year" Compensation:&lt;/strong&gt; Early on I noticed heavy clustering of data. I'm guessing that when people log album releases but aren't sure of the exact year, they naturally default to round numbers (1980, 1990, 2000). The year 2000 was an absolute anomaly in the dataset—without mathematical compensation, that single year would appear as the defining peak for 50 different genres simultaneously. I had to algorithmically balance the numbers for these milestone years. Again, some will agree with this, some will not, that is also fine with me.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Logarithmic Scaling:&lt;/strong&gt; If you size the map areas purely geographically (1-to-1 based on release numbers), majority of genres would simply disappear from the screen. Pop and Rock would take up 95% of the space, leaving everything else as a single unclickable pixel. A logarithmic scale guarantees the massive genres stay prominent without completely hiding smaller, culturally vital movements.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Parent Tag Problem:&lt;/strong&gt; Terms like "Blues", "Rock", or "Metal" function as both specific genres and wide colloquial umbrellas. There is rarely a "pure" Rock artist (they are Indie Rock, Hard Rock, etc.), but discarding tens of thousands of tags labeled just "Rock" ignores valid data. I kept these massive parent nodes in the ecosystem, accepting the tradeoff that they inherently capture a larger percentage of total territory on the map.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Powered by &lt;a href="https://musicbrainz.org/" rel="noopener noreferrer"&gt;MusicBrainz&lt;/a&gt; and &lt;a href="https://www.last.fm/" rel="noopener noreferrer"&gt;Last.fm&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>javascript</category>
      <category>dataviz</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Immersive Gallery: Mapping the MET API to Dynamic Soundscapes</title>
      <dc:creator>notbigmuzzy</dc:creator>
      <pubDate>Thu, 05 Feb 2026 12:14:00 +0000</pubDate>
      <link>https://dev.to/nbm/immersive-gallery-mapping-the-met-api-to-dynamic-soundscapes-45jm</link>
      <guid>https://dev.to/nbm/immersive-gallery-mapping-the-met-api-to-dynamic-soundscapes-45jm</guid>
      <description>&lt;p&gt;The last time I created an online museum experience, it kinda felt off. You walk from room to room, wait for things to load, and look at static images. It's not &lt;em&gt;bad&lt;/em&gt;, but it lacks vibes.&lt;/p&gt;

&lt;p&gt;So I decided to try a different approach: build something that felt less like a museum and more like a stream of consciousness. What if you could scrub through art history the way you scrub through a YouTube video?&lt;/p&gt;

&lt;p&gt;Enter &lt;strong&gt;Gogh With The Flow&lt;/strong&gt; :)&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/notbigmuzzy/embed/XJKBdPw?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;




&lt;p&gt;The Metropolitan Museum of Art has an incredible &lt;a href="https://metmuseum.github.io/" rel="noopener noreferrer"&gt;Open Access API&lt;/a&gt;. It’s a treasure trove of human history. But APIs have rate limits, and fetching data for 1450, 1451, 1452... as fast as a user can scroll would instantly hit a wall (or just be incredibly slow).&lt;/p&gt;

&lt;p&gt;I wanted the experience to be fluid. No "Loading..." text. No spinners. Just flow.&lt;/p&gt;




&lt;p&gt;To solve this, I employed a "hybrid" strategy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Pre-indexed "Shards":&lt;/strong&gt; I scraped the collection to build lightweight JSON files for every year (e.g., &lt;code&gt;1889.json&lt;/code&gt;, &lt;code&gt;1450.json&lt;/code&gt;). These contain just the valid Object IDs for that year. They are tiny and load instantly.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;On-Demand Detail:&lt;/strong&gt; When the dialer settles on a year, the app grabs a random selection of IDs from that shard and fetches the images.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Lazy High-Res:&lt;/strong&gt; We load optimized thumbnails first. Only when you click to "enter" a painting do we ping the API for the massive, high-resolution file, allowing you to zoom in and see the individual brushstrokes.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;strong&gt;GSAP&lt;/strong&gt; does the heavy lifting regarding animations. DOM manipulation is heavy but by using GSAP's &lt;code&gt;InertiaPlugin&lt;/code&gt; and careful tweening, the transition between years—fading out the old era and drifting in the new—happens without layout trashing or "ghosting" ( &lt;em&gt;as much as I could pull it off :)&lt;/em&gt; )&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example here showing how 'paralax' panels are handles with minimal perf impact&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// The "Time Travel" Engine
const updatePanels = () =&amp;gt; {
    // Calculate how fast we are scrubbing
    const scrollPos = dialer.scrollLeft;
    const delta = scrollPos - lastScrollPos;

    panels.forEach(panel =&amp;gt; {
        // Create parallax depth: background moves slower, 
        // the "window" moves counter to create focus
        let speedMultiplier = panel.className.includes('panel-window') ? -1.5 : 
                              panel.className.includes('panel-further') ? 0.25 : 0.5;

        // Apply momentum without layout thrashing
        let currentX = gsap.getProperty(panel, 'x') || 0;
        currentX += delta * speedMultiplier;

        // Infinite Loop Logic: 
        // If a panel goes off-screen, warp it to the other side
        const screenPos = panel.offsetLeft + currentX;
        if (screenPos &amp;gt; viewportWidth + 200) {
            currentX -= viewportWidth + 400;
        } else if (screenPos &amp;lt; -200) {
            currentX += viewportWidth + 400;
        }

        // Use transforms for 60fps performance
        gsap.set(panel, { x: currentX });
    });
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Visuals are only half the story. To truly sell the time travel aspect, the soundscape shifts with you.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1400s:&lt;/strong&gt; Pre-Renaissance choral echoes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1600s:&lt;/strong&gt; Baroque strings&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1800s:&lt;/strong&gt; Romantic orchestral swells&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1900s:&lt;/strong&gt; Early variations of impressionistic sound&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The audio player is set up so it cross-fades tracks as you traverse centuries and even handles "radio" logic, automatically queuing up a new random track from the current era when one finishes&lt;/p&gt;




&lt;p&gt;If it sounds interesting, go ahead and give it a spin. &lt;strong&gt;1889&lt;/strong&gt; is a good year ( but me personally, I'm into baroque heavily, didn't even realize it until debugging this thing :) )&lt;/p&gt;

&lt;p&gt;Live Demo - &lt;a href="https://notbigmuzzy.github.io/goghwiththeflow/" rel="noopener noreferrer"&gt;https://notbigmuzzy.github.io/goghwiththeflow/&lt;/a&gt;&lt;br&gt;
Source Code - &lt;a href="https://github.com/notbigmuzzy/goghwiththeflow" rel="noopener noreferrer"&gt;https://github.com/notbigmuzzy/goghwiththeflow&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Powered by the &lt;a href="https://metmuseum.github.io/" rel="noopener noreferrer"&gt;Metropolitan Museum of Art API&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>gsap</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Virtual 3D Museum - Three.js</title>
      <dc:creator>notbigmuzzy</dc:creator>
      <pubDate>Tue, 13 Jan 2026 22:09:09 +0000</pubDate>
      <link>https://dev.to/nbm/virtual-3d-museum-threejs-1n5k</link>
      <guid>https://dev.to/nbm/virtual-3d-museum-threejs-1n5k</guid>
      <description>&lt;p&gt;So, I was shitcanned recently and said to myself: "Hey, why not actually learn something new and interesting for once?"&lt;/p&gt;

&lt;p&gt;Three.js has been high on my list for a long time. I tried to make a pinball game a couple of years back, failed miserably, and never quite forgot about it. This time, I wanted to see if I could turn Wikipedia entries into something more visual and "walkable"&lt;/p&gt;

&lt;p&gt;The result is a Virtual 3D Museum. It’s a 3D environment where the "exhibits" are pulled dynamically from the Wikipedia API, and gallery rooms are populated with that info on the fly&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/notbigmuzzy/embed/dPXOmoq?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Tech:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Three.js: Handles the spatial layout and rendering.&lt;/li&gt;
&lt;li&gt;Vanilla JS: No frameworks. I wanted to keep it lightweight and see how far I could get with just the basics (spoiler: it can go really far).&lt;/li&gt;
&lt;li&gt;Wikipedia API: The source of all the data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Details:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multi-language support: You can toggle between different language versions of Wikipedia.&lt;/li&gt;
&lt;li&gt;Persistence: It uses basic local storage, so it remembers your session when you come back.&lt;/li&gt;
&lt;li&gt;Environment: I went for a "faux 3D" look—enough to feel immersive without being heavy on the hardware. I know it doesn't look "well" yet; I don't know how to use Blender properly, and I'm still learning exactly how far you can push a browser before the stuttering ruins the experience.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s been a fun experiment in data visualization and spatial UI. If you’re into Three.js or just want to see a weird way to read about history or universe, check it out ;)&lt;/p&gt;

&lt;p&gt;Source Code: GitHub - &lt;a href="https://github.com/notbigmuzzy/linkwalk" rel="noopener noreferrer"&gt;https://github.com/notbigmuzzy/linkwalk&lt;/a&gt; &lt;br&gt;
Live Demo: Full version with languages and persistence - &lt;a href="https://notbigmuzzy.github.io/linkwalk/" rel="noopener noreferrer"&gt;https://notbigmuzzy.github.io/linkwalk/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>threejs</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
