<?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: Michael Vestergaard</title>
    <description>The latest articles on DEV Community by Michael Vestergaard (@iliketoplay).</description>
    <link>https://dev.to/iliketoplay</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%2F1183311%2F65c95294-142e-45ff-a950-421779c23f4d.jpeg</url>
      <title>DEV Community: Michael Vestergaard</title>
      <link>https://dev.to/iliketoplay</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/iliketoplay"/>
    <language>en</language>
    <item>
      <title>New launch</title>
      <dc:creator>Michael Vestergaard</dc:creator>
      <pubDate>Fri, 27 Feb 2026 13:39:17 +0000</pubDate>
      <link>https://dev.to/iliketoplay/new-launch-1hjf</link>
      <guid>https://dev.to/iliketoplay/new-launch-1hjf</guid>
      <description>&lt;p&gt;New website just launched (in Danish) for 3F: &lt;a href="https://ditarbejdsliv.3f.dk/" rel="noopener noreferrer"&gt;https://ditarbejdsliv.3f.dk/&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>A look behind Ribbit.dk</title>
      <dc:creator>Michael Vestergaard</dc:creator>
      <pubDate>Sat, 13 Dec 2025 14:22:00 +0000</pubDate>
      <link>https://dev.to/iliketoplay/a-look-behind-ribbitdk-29na</link>
      <guid>https://dev.to/iliketoplay/a-look-behind-ribbitdk-29na</guid>
      <description>&lt;p&gt;My latest launch &lt;a href="https://ribbit.dk/" rel="noopener noreferrer"&gt;ribbit.dk&lt;/a&gt; has gained a lot of attention lately. Among many awards and features, we were also interviewed by The FWA, here is an excerpt from the interview, read it all &lt;a href="https://thefwa.com/article/insights-ribbit" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What did you want to accomplish with the project?&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Said shortly:&lt;/em&gt; Our goal was to create a truly unique and tailor-made website for Ribbit.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Our challenge:&lt;/em&gt; To develop a distinct identity that cuts through the noise of an already skilled and competitive motion design industry.&lt;/p&gt;

&lt;p&gt;Through our research, we discovered that few competitors were trying to build brands that risked overshadowing the client work they showcased, especially not through brand characters. So we decided to do exactly that. We needed to zig when everyone else zagged. Most competitors design their sites as clean black-and-white canvases, allowing the work to stand out. We wanted everything to stand out, to pop, to exude playfulness and curiosity, and to express Ribbit’s unique creative spirit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What technical challenges did you encounter and how did you solve them?&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Michael:&lt;/em&gt; The push and pull animations on the homepage required us to think creatively with code. Our immediate approach was to use a WebGL renderer and spritesheets. But we couldn't get the smooth feeling out of it. So after some experimentation with custom renderers and spritesheets, we ended up with a simple solution of loading each frame as a png and toggling the visibility of it.&lt;/p&gt;

&lt;p&gt;For the Process page, the opening of the book progressively on scroll required some extra attention too. We really wanted the feeling of moving seamlessly into the video sections, but we also wanted the user to feel in control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three hot facts:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Making rounded corners on the Process videos was one of the hardest things on the website!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ribbit was originally going to be called Howl at the start of the project, but the name was changed for reasons we can’t disclose here.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The booking flow was given a lot of attention too, try it out.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It was such a pleasure working on this site with the talented people from Ribbit and designer Frederik Hansen. &lt;a href="https://iliketoplay.dk/" rel="noopener noreferrer"&gt;If you are looking for a developer to build a creative website, please get in touch.&lt;/a&gt;&lt;/p&gt;

</description>
      <category>design</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Headless static website</title>
      <dc:creator>Michael Vestergaard</dc:creator>
      <pubDate>Sun, 27 Apr 2025 18:37:53 +0000</pubDate>
      <link>https://dev.to/iliketoplay/headless-static-website-4h7e</link>
      <guid>https://dev.to/iliketoplay/headless-static-website-4h7e</guid>
      <description>&lt;p&gt;Here's my typical technical setup for small and medium sized websites:&lt;/p&gt;

&lt;p&gt;Back-end:&lt;br&gt;
Headless CMS, usually &lt;a href="https://prismic.io/" rel="noopener noreferrer"&gt;Prismic&lt;/a&gt; or &lt;a href="https://www.storyblok.com/" rel="noopener noreferrer"&gt;Storyblok&lt;/a&gt;.&lt;br&gt;
PHP on the server to generate the website.&lt;/p&gt;

&lt;p&gt;Front-end:&lt;br&gt;
Static website (html,css and javascript).&lt;br&gt;
No need for advanced routing or server setup (same hosting and server as Wordpress, Drupal etc. can be used).&lt;br&gt;
Various frameworks and libraries when relevant (GSAP, Three.js etc .)&lt;/p&gt;

&lt;p&gt;When a CMS editor publishes new content from the CMS, a webhook is calling a script on our server. This script loads all data from the CMS and generates static files on the server. The process is usually very fast (less than 2 seconds), in opposition to many popular frameworks I have tested. Editors can preview/view updated content easily and have a fast and friction-less editing experience.&lt;/p&gt;

&lt;p&gt;The benefits from a setup like the above are primarily:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Great editing experience in the CMS.&lt;/li&gt;
&lt;li&gt;Very fast website (nothing is loaded from databases).&lt;/li&gt;
&lt;li&gt;Great SEO (nothing relies on javascript interpretation). If the website is ranking high, then updates are usually visible within a couple of hours!&lt;/li&gt;
&lt;li&gt;Great Lighthouse score. This helps both SEO and accessibility.&lt;/li&gt;
&lt;li&gt;When using Prismic, we have all images in the &lt;a href="https://www.imgix.com/" rel="noopener noreferrer"&gt;imgix&lt;/a&gt; CDN. This means fast load times globally.&lt;/li&gt;
&lt;li&gt;Headless means a separation of the back-end (handling all your data and assets) and the presentation layer (what your website visitors see). This makes it easy to work independently in either, but also avoids situations in which a content editor is designing in the CMS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Downsides to a headless setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Very large websites can experience longer build times.&lt;/li&gt;
&lt;li&gt;The modules and content in the CMS doesn't look designed (but very few CMS actually offers this anyway).&lt;/li&gt;
&lt;li&gt;Some services can be hard to integrate (Shopify, Hubspot etc. are all possible, but not as easy or deeply integrated as I sometimes would like).&lt;/li&gt;
&lt;li&gt;You don't host your own data. For many companies this is actually good, because you have back-ups and don't have to worry about servers and databases. But for some larger companies, hosting and storing all data is important.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Please have a look at my &lt;a href="https://iliketoplay.dk/" rel="noopener noreferrer"&gt;freelance front-end developer portfolio&lt;/a&gt; if you are interested in more.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Okay devs</title>
      <dc:creator>Michael Vestergaard</dc:creator>
      <pubDate>Sun, 27 Apr 2025 18:05:12 +0000</pubDate>
      <link>https://dev.to/iliketoplay/okay-devs-4aoh</link>
      <guid>https://dev.to/iliketoplay/okay-devs-4aoh</guid>
      <description>&lt;p&gt;There's a new kid in town: &lt;a href="https://okaydev.co/" rel="noopener noreferrer"&gt;Okay Dev&lt;/a&gt;. A community for creative front-end developers (like myself).&lt;br&gt;
Big kudos to &lt;a href="https://okaydev.co/articles/eric-van-holtz" rel="noopener noreferrer"&gt;Eric&lt;/a&gt; who is running the whole thing, he put a massive amount of work and heart into it. I was fortunate to be interviewed by him recently, have a &lt;a href="https://okaydev.co/articles/michael-vestergaard" rel="noopener noreferrer"&gt;look here&lt;/a&gt; if you're curious. We are talking about history, tools, projects and much more.&lt;br&gt;
As a &lt;a href="https://iliketoplay.dk/" rel="noopener noreferrer"&gt;freelance creative front-end developer&lt;/a&gt;, it's great with communities like Okay Dev, to share knowledge with each other.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>frontend</category>
      <category>javascript</category>
      <category>animation</category>
    </item>
    <item>
      <title>Using TUIO with javascript</title>
      <dc:creator>Michael Vestergaard</dc:creator>
      <pubDate>Tue, 18 Jun 2024 07:50:32 +0000</pubDate>
      <link>https://dev.to/iliketoplay/using-tuio-with-javascript-291g</link>
      <guid>https://dev.to/iliketoplay/using-tuio-with-javascript-291g</guid>
      <description>&lt;p&gt;I recently developed an application for a museum running on a &lt;a href="https://www.displax.com/tile"&gt;TILE&lt;/a&gt; display from Displax.&lt;br&gt;
In order to get this up and running I needed to create a node.js server and websocket connection. Recent examples of this are hard to find online, so I want to share my code for anyone in the same situation, hopefully this saves you some time :-)&lt;/p&gt;

&lt;p&gt;First you must have installed a server and node.js&lt;/p&gt;

&lt;p&gt;Then install the packages needed through terminal/command prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install osc express socket.io bufferutil utf-8-validate --no-audit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your html file include this:&lt;br&gt;
&lt;a href="https://cdn.socket.io/4.7.5/socket.io.min.js"&gt;https://cdn.socket.io/4.7.5/socket.io.min.js&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now you need to create a server file, let's call it "server.js":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const bufferUtil = require('bufferutil');//maybe not needed, but maybe it speeds things up!
var osc = require('osc');
const express = require('express');
const { createServer } = require('node:http');
const { Server } = require('socket.io');
const app = express();
const server = createServer(app);
var socket;

const io = require("socket.io")(server, {cors:{origin: "*",methods: ["GET", "POST"]}});

//Listen to the TUIO data
const udpPort = new osc.UDPPort({
  localAddress: "127.0.0.1",
  localPort: 3333,
  metadata: true
});

//Listen/send on port 3000 or 5000
server.listen(3000, () =&amp;gt; {
  console.log('Server running at http://localhost:3000');
});

io.on('connection', function (_socket) {
  socket = _socket;
    socket.on('config', function (obj) {
      console.log('config', obj);
    });
});

// Listen for incoming OSC bundles.
udpPort.on("bundle", function (oscBundle){
  if(socket &amp;amp;&amp;amp; oscBundle.packets.length &amp;gt; 2) socket.emit('message', oscBundle);//only send TUIO v1.1
});

// Open the socket.
udpPort.open();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now for connecting in the front-end, we need this script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function WSConnection(_address){
    const socket = io(_address);//, {withCredentials: true}
    socket.on('error', function(){
        console.log("Error connecting!");
    });
    socket.on('connect', function(){
        console.log("Server connected");
        socket.emit('config', {server:{port: 3333,host: '127.0.0.1'},client: {port: 3334,host: '127.0.0.1'}});
    });
    socket.on('message', function(oscBundle){
    });
}
//Init
WSConnection("http://127.0.0.1:3000");//or port 5000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To start the server open terminal and cd into the server.js folder and enter:&lt;br&gt;
&lt;code&gt;node server.js&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Now your have a running node.js server and a front-end that listens to any TUIO objects being sent through the server.&lt;/p&gt;

&lt;p&gt;In order to test you can download the "TUIOSimulator.app" from here:&lt;br&gt;
&lt;a href="https://github.com/gregharding/TUIOSimulator"&gt;https://github.com/gregharding/TUIOSimulator&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When above works you are facing the next challenge. Depending on the hardware and software you are using, TUIO objects can often be a little unreliable. Sometimes events are fired too slowly, so you think an object is removed from the display. So I made some custom work arounds for these scenarios. In my example I'm using both touch and object recognition, so I have to use both "tuio/2Dcur" and "tuio/2Dobj".&lt;/p&gt;

&lt;p&gt;This is my "message" function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;var _alive = [], _aliveTags = [], _aliveCursors = [], _cursors = [];
var _l = 0, _id = 0, _numAlive = 0, _cursorId = 0;
var _v = "";

socket.on('message', function(oscBundle){
    _l = oscBundle.packets.length;
    for(var i=0;i&amp;lt;_l;i++){
        if(oscBundle.packets[i].address == "/tuio/2Dcur"){
            _v = oscBundle.packets[i].args[0].value;
            if(_v == "alive"){
                //Find alive cursors
                _numAlive = oscBundle.packets[i].args.length - 1;
                _alive = [];
                for(var l=0;l&amp;lt;_numAlive;l++){
                    _cursorId = oscBundle.packets[i].args[l+1].value%100;
                    _alive.push(_cursorId);
                    if(_aliveCursors.indexOf(_cursorId) == -1){
                        console.log("Add cursor",_cursorId);
                        _aliveCursors.push(_cursorId);
                        _cursors[_cursorId] = new TuioObj(_cursorId,false);
                    }
                    else _cursors[_cursorId]._removed = false;//keep alive (if set to be removed on next render)
                }
                //Find old cursors not alive anymore
                _numAlive = _aliveCursors.length;
                for(var l=0;l&amp;lt;_numAlive;l++){
                    if(_alive.indexOf(_aliveCursors[l]) == -1){
                        _id = _aliveCursors[l];
                        if(_cursors[_id]._removed){
                            console.log("Destroy cursor", _id);
                            _cursors[_id].destroy();
                            delete _cursors[_id];
                            _aliveCursors.splice(l,1);
                            _numAlive--;
                            l = 0;
                        }
                        else{
                            //console.log("Remove cursor", _id);
                            _cursors[_id]._removed = true;
                        }
                    }
                }
            }
            else if(_v == "set"){
                _cursorId = oscBundle.packets[i].args[1].value%100;
                if(_aliveCursors.indexOf(_cursorId) != -1) _cursors[_cursorId].setXY(oscBundle.packets[i].args[2].value * _appW,oscBundle.packets[i].args[3].value * _appH);
                else console.log("Cursor not found!", _cursorId);
            }
        }
        else if(oscBundle.packets[i].address == "/tuio/2Dobj"){
            _v = oscBundle.packets[i].args[0].value;
            if(_v == "alive"){
                //Find alive cursors
                _numAlive = oscBundle.packets[i].args.length - 1;
                _alive = [];
                for(var l=0;l&amp;lt;_numAlive;l++){
                    _cursorId = oscBundle.packets[i].args[l+1].value%100;
                    _alive.push(_cursorId);
                    if(_aliveTags.indexOf(_cursorId) == -1){
                        console.log("Add tag",_cursorId);
                        _aliveTags.push(_cursorId);
                        _tags[_cursorId] = new TuioObj(_cursorId,true);
                    }
                    else _tags[_cursorId]._removed = false;//keep alive (if set to be removed on next render)
                }
                //Find old cursors not alive anymore
                _numAlive = _aliveTags.length;
                for(var l=0;l&amp;lt;_numAlive;l++){
                    if(_alive.indexOf(_aliveTags[l]) == -1){
                        _id = _aliveTags[l];
                        if(_tags[_id]._removed){
                            console.log("Destroy tag", _id);
                            _tags[_id].destroy();
                            delete _tags[_id];
                            _aliveTags.splice(l,1);
                            _numAlive--;
                            l = 0;
                        }
                        else{
                            //console.log("Remove tag", _id);
                            _tags[_id]._removed = true;
                        }
                    }
                }
            }
            else if(_v == "set"){
                //console.log("Tag static id:", oscBundle.packets[i].args[2].value)
                _cursorId = oscBundle.packets[i].args[1].value%100;
                if(_aliveTags.indexOf(_cursorId) != -1) _tags[_cursorId].setXYR(oscBundle.packets[i].args[3].value * _appW,oscBundle.packets[i].args[4].value * _appH,oscBundle.packets[i].args[5].value);
                else console.log("Tag not found!", _cursorId);
            }
        }
    }
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each TUIO object, either a Tag or Touch, I am using this TuioObj, here's a simplified version of mine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function TuioObj(_id,_isTag){
    var _this = this;
    _this._removed = false;//TUIO "alive" events are not reliable and often remove an object only to add it instantly again!

    //Touch (x and y coordinate)
    _this.setXY = function(x,y){}

    //Tag (x and y coordinate and rotation value)
    _this.setXYR = function(x,y,r){}

    //Destroy (remove DOM elements, event listeners etc.)
    _this.destroy = function(){}
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of course many more features, like idle handling, smooth movement (check my &lt;a href="https://dev.to/iliketoplay/using-lerp-and-damp-in-javascript-3f7p"&gt;post on lerp&lt;/a&gt;) and distance measurement (for click handling etc.) can be added, but now you should have a template to get you started.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>tuio</category>
      <category>touchscreen</category>
      <category>node</category>
    </item>
    <item>
      <title>Using lerp and damp in javascript</title>
      <dc:creator>Michael Vestergaard</dc:creator>
      <pubDate>Sat, 15 Jun 2024 18:55:00 +0000</pubDate>
      <link>https://dev.to/iliketoplay/using-lerp-and-damp-in-javascript-3f7p</link>
      <guid>https://dev.to/iliketoplay/using-lerp-and-damp-in-javascript-3f7p</guid>
      <description>&lt;p&gt;Almost every project I do as a &lt;a href="https://iliketoplay.dk/"&gt;creative developer&lt;/a&gt; utilizes linear interpolation - also called "lerp". It's a simple way of easing from position A to B. Without diving into the math, there's another approach called damp (or smoothdamp), this is almost similar but has a smoother curve (more like a Quad and less like an Expo).&lt;/p&gt;

&lt;p&gt;The classic way of using lerp is like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//speed between 0-1
_tweenedValue += (_actualValue - _tweenedValue) * _speed;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works very well. You can even tween the speed if you want the movement to start slowly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;_this._speed = 0;
gsap.to(_this, 1, {_speed:.2, ease:"linear"});
_tweenedValue += (_actualValue - _tweenedValue) * _this._speed;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm using the above in custom cursors, carousels, elements that move based on mouse position and even for a smooth pagescroller (similar to &lt;a href="https://github.com/darkroomengineering/lenis"&gt;Lenis&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Below is an example of both lerp and damp (blue and red), but also a fun little bouncy/elastic approach (green):&lt;/p&gt;

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

</description>
      <category>lerp</category>
      <category>damp</category>
      <category>gsap</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Responsive image with different aspect ratios</title>
      <dc:creator>Michael Vestergaard</dc:creator>
      <pubDate>Thu, 30 May 2024 15:12:36 +0000</pubDate>
      <link>https://dev.to/iliketoplay/responsive-image-with-different-aspect-ratios-1f6k</link>
      <guid>https://dev.to/iliketoplay/responsive-image-with-different-aspect-ratios-1f6k</guid>
      <description>&lt;p&gt;I recently discovered a small browser bug (or at least undesired behavior) related to the picture tag.&lt;/p&gt;

&lt;p&gt;It's quite normal in many designs to use taller mobile images and wider desktop images. This means we have different aspect ratios. As &lt;a href="https://iliketoplay.dk/"&gt;freelance developer&lt;/a&gt; I often run into designs that use different aspect ratios between mobile and desktop layouts.&lt;/p&gt;

&lt;p&gt;When crossing the breakpoint and going from one image to the other, the image size isn't instantly set correct, it doesn't happen until the image has loaded (tested in Chrome). This is problematic because we might have elements further down the page trying to read offsetTop or similar - this reading will be wrong until the image has loaded. You would think that setting the width and height on the "img" and "source" tag would solve this, but it doesn't.&lt;/p&gt;

&lt;p&gt;I made this little demo to show the problem. In the console logs it's obvious that the readings are wrong. You can also see the fix:&lt;br&gt;
&lt;iframe height="600" src="https://codepen.io/iltp/embed/xxNgJoK?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;The solution is fairly simple: Instead of relying only on the normal "resize" event, use a ResizeObserver to check for changes in the document height.&lt;/p&gt;

&lt;p&gt;I'm pretty sure the reason for this behaviour, is to always show an image. So while the new image loads the browser keeps showing the old image. This is good (looking) for many reasons, but of course gives us the false readings mentioned above.&lt;/p&gt;

</description>
      <category>responsive</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Working freelance as a frontend developer</title>
      <dc:creator>Michael Vestergaard</dc:creator>
      <pubDate>Wed, 17 Jan 2024 09:05:46 +0000</pubDate>
      <link>https://dev.to/iliketoplay/working-freelance-as-a-frontend-developer-1c1g</link>
      <guid>https://dev.to/iliketoplay/working-freelance-as-a-frontend-developer-1c1g</guid>
      <description>&lt;p&gt;For more than 15 years I have had the pleasure of working freelance, or as a consultant, which might sound more exclusive :-)&lt;/p&gt;

&lt;p&gt;Since the beginning I have worked with a broad variety of clients. In the first years mostly Danish, but for every year my client list gets more international. Now 90% of my clients are international. I do both normal and white label development for some great agencies in the US.&lt;br&gt;
I have worked with top brands like Philips, LEGO, &lt;a href="https://iliketoplay.dk/veluxcube"&gt;Velux&lt;/a&gt;, Volkswagen, HBO and &lt;a href="https://iliketoplay.dk/beoplay"&gt;B&amp;amp;O&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;However I have also had to turn down some amazing projects. Once I turned down coca-cola.com! I have also turned down projects for Facebook and Google. All of them because my calendar was full. It hurts a little saying no to great projects, but I have had periods where I tried accepting all projects that came in. The result was that I was stressed out and soon after burned out. And there's a risk that it affects the quality of my work, which I cannot risk.&lt;/p&gt;

&lt;p&gt;Working as a &lt;a href="https://iliketoplay.dk/"&gt;frontend developer&lt;/a&gt; that relies on networking and a &lt;a href="https://iliketoplay.dk/"&gt;great portfolio&lt;/a&gt;, the quality of my work has to be consistent. This is also the reason I haven't partnered with anyone (or hired junior developers). However it's also a small curse to not be able to delegate or share work, so this is something I'm slowly opening up to. I don't dream of a managing role, but it would be great with partnerships and closer collaborations.&lt;/p&gt;

&lt;p&gt;Please let me know if you are looking for a &lt;a href="https://iliketoplay.dk/"&gt;freelance frontend consultant&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>freelance</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Aspect ratio trick</title>
      <dc:creator>Michael Vestergaard</dc:creator>
      <pubDate>Fri, 12 Jan 2024 15:21:15 +0000</pubDate>
      <link>https://dev.to/iliketoplay/aspect-ratio-trick-2df3</link>
      <guid>https://dev.to/iliketoplay/aspect-ratio-trick-2df3</guid>
      <description>&lt;p&gt;A long time ago some clever developer starting using the famous padding trick to control the size of images before they were loaded. Basically you wrap your (absolute positioned) img tag in a div with width:100% and padding-top set to whatever percentage fits the aspect ratio.&lt;/p&gt;

&lt;p&gt;I rarely use this technique for images anymore, but it's useful for video elements. The reason is that often you don't want to create the video tag unless the user is ready and interested in the video (maybe the see a splash image with a play button first). Videos can become quite heavy on a website and the loading property works inconsistently between different browsers. And let's not go into autoplay, mute and low power mode on iOS :-)&lt;/p&gt;

&lt;p&gt;I usually did this trick in javascript (receiving the width and height, then calculating and applying the padding). But the method below is much smarter - I have seen this quite a few places and I adopted the approach too:&lt;/p&gt;

&lt;p&gt;HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;div class="lazy video" data-src="16_9.mp4" style="--aspect:56.25%;"&amp;gt;&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CSS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.lazy.video:before{content:"";display:block;padding-top:var(--aspect);width:100%;pointer-events:none;}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can do it in many ways, setting the size directly on the video tag too, if that fits your setup better.&lt;/p&gt;

&lt;p&gt;Being an &lt;a href="https://iliketoplay.dk/"&gt;award-winning website developer&lt;/a&gt; means you have to stay on top of modern and effective techniques. This is just a small one I wanted to share with you.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>html</category>
      <category>css</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Lazy loading images in 2024</title>
      <dc:creator>Michael Vestergaard</dc:creator>
      <pubDate>Fri, 12 Jan 2024 15:08:36 +0000</pubDate>
      <link>https://dev.to/iliketoplay/lazy-loading-images-in-2024-4le2</link>
      <guid>https://dev.to/iliketoplay/lazy-loading-images-in-2024-4le2</guid>
      <description>&lt;p&gt;A couple of years ago I did a post about my method for lazy loading images with (vanilla) javascript. Things have changed much since, for the better :-)&lt;/p&gt;

&lt;p&gt;Now we can control lazy loading easier and with less code than ever. You have probably heard of the “loading” attribute, but have you also dived into the “decoding” attribute? Below is a little example of how I often set up images these days.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!-- This image is above the fold--&amp;gt;
&amp;lt;picture class="lazy"&amp;gt;
  &amp;lt;source srcset="/Assets/Images/1920.png" media="(min-width:1600px)" width="1920" height="1080"&amp;gt;
  &amp;lt;source srcset="/Assets/Images/1280.png" media="(min-width:1000.5px)" width="1280" height="720"&amp;gt;
  &amp;lt;img src="/Assets/Images/640.png" width="640" height="360" alt="" fetchpriority="high" draggable="false"&amp;gt;
&amp;lt;/picture&amp;gt;
&amp;lt;!-- This image is below the fold--&amp;gt;
&amp;lt;picture class="lazy"&amp;gt;
  &amp;lt;source srcset="/Assets/Images/1920.png" media="(min-width:1600px)" width="1920" height="1080"&amp;gt;
  &amp;lt;source srcset="/Assets/Images/1280.png" media="(min-width:1000.5px)" width="1280" height="720"&amp;gt;
  &amp;lt;img src="/Assets/Images/640.png" width="640" height="360" alt="" loading="lazy" decoding="async" draggable="false"&amp;gt;
&amp;lt;/picture&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are a few things to notice:&lt;br&gt;
For images above the fold, Google now &lt;a href="https://web.dev/articles/optimize-lcp"&gt;recommends&lt;/a&gt; using fetchpriority=”high” and no “loading” attribute. This speeds up the Largest Contentful Paint metric and helps your website load faster.&lt;/p&gt;

&lt;p&gt;For images below the fold, remove the “fetchpriority” attribute and add loading=”lazy” instead. This really simple method now lazy loads your images, no javascript is needed (unless you want to control an animation, maybe fading in the image).&lt;/p&gt;

&lt;p&gt;If you also add decoding=”async”, you are telling the browser to control when and where the decoding happens. There are discussions about the impact of this, but it’s probably better to do this than not.&lt;/p&gt;

&lt;p&gt;The above code is really simple, but remember you can &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture"&gt;control the (source) “srcset” even more detailed&lt;/a&gt; with high DPI images etc.&lt;/p&gt;

&lt;p&gt;A while ago I did a lot of research to find out if I should use the “picture”, or “img” element. I ended up with “picture” because of Art Direction purposes: often the mobile image isn’t the same aspect ratio as the desktop, so I need more individual control of the sources. Read more about this here and here. The “img” tag is still great for many things, so don’t completely move away from it.&lt;/p&gt;

&lt;p&gt;Another great thing about this new(er) method is that you don’t need the aspect ratio (padding-top on parent container) trick anymore. The image has the correct size instantly, which makes for faster rendering and less code.&lt;/p&gt;

&lt;p&gt;The browser support for modern lazy loading is &lt;a href="https://caniuse.com/loading-lazy-attr"&gt;really good&lt;/a&gt;, so go ahead and make your life easier :-)&lt;/p&gt;

&lt;p&gt;As a &lt;a href="https://iliketoplay.dk/"&gt;creative freelance front-end developer&lt;/a&gt; I often try to optimize my methods, utilities and frameworks. It’s important though that I always consider all users and not only the people with new updated computers and devices. The above method is not new, but in my opinion it’s only now that the market is ready (unless you have clients who don’t care about old computers etc.).&lt;/p&gt;

</description>
      <category>lazyloading</category>
      <category>html</category>
      <category>picturetag</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
