thingsinjars

  • 15 Mar 2021

    HERE Maps Web Component

    At the weekend, I found myself starting another little side project that needed a map. And, predictably, I chose to use a HERE map.

    In my day job, I use a lot of Vue but I do tend to prefer web components where possible, wrapping them in Vue only where necessary. This, then, is the natural outcome:

    Now I can embed HERE maps with a single web component.

    <here-map api-key="1234-54321"
      latitude="52.5"
      longitude="13.4"
      zoom="12"
    ></here-map>
    

    Or include markers directly:

    <here-map
      api-key="1234-54321"
      latitude="52.5"
      longitude="13.4"
      zoom="12">
    
      <here-map-marker latitude="52.5" longitude="13.4" />
    
      <here-map-marker
        latitude="52.501"
        longitude="13.405"
        icon="https://cdn3.iconfinder.com/data/icons/tourism/eiffel200.png"
      />
    </here-map>
    

    Have a look at the example.

    Development, Javascript

  • 4 Jan 2021

    Using Web APIs to generate music videos

    Plan

    A very simple web app that generates a music visualisation from an audio file and renders it to a downloadable movie file. Most importantly, it does this all on the client side.

    To do this, we'll combine the Web Audio API, Canvas API and MediaStream Recording API. I'll not be providing the complete source code as we go through it but just enough to point you in the right direction.

    For more detail, I recommend the following:

    • Visualizations with Web Audio API (MDN)
    • Record Audio and Video with MediaRecorder

    I've used this as the basis to create all the music videos for my next album. You can see examples in my previous post.

    TL;DR

    Here's the complete architecture of what we're building:

    And here's the finished build: All-In-One Music Visualiser

    Basic HTML

    First, let's just set up some basic HTML to show the visualisation, hold the audio and take in some parameters.

    <canvas></canvas>
    <audio controls></audio>
    
    <label>Track title:
      <input type="text" id="track" placeholder="Track title">
    </label>
    <label>Artist name: 
      <input type="text" id="artist" placeholder="Artist name">
    </label>
    <label>Audio File:
      <input type="file" id="file" accept="audio/*" />
    </label>

    Read in audio file

    const audio = document.querySelector('audio');
    audio.src = URL.createObjectURL(document.querySelector('[type=file]').files[0]);
    audio.load();

    Web Audio API

    Wire up Web Audio

    Create an AudioContext, source the audio from the <audio> element, prepare a streaming destination for later and connect the in to the out.

    const context = new AudioContext();
    const src = context.createMediaElementSource(audio);
    const dest = context.createMediaStreamDestination();
    src.connect(dest);

    Attach an analyser node

    We want our visualisation to react to the music so we need to wire in an AnalyserNode. Thankfully, this handles all the complicated audio processing so we don't need to worry about it too much. We also attach the analyser to the destination node of the AudioContext so that we can hear it through the computer speakers. Not strictly necessary but it's nice to be able to hear what we're doing.

    const analyser = context.createAnalyser();
    src.connect(analyser);
    analyser.connect(context.destination);

    Prepare to sample frequency

    the fftSize is essentially "How detailed do we want the analysis to be?". It's more complicated than that but this is all the detail we need for now. Here, we're using 64 which is very low but 512, 1024 and 2048 are all good. It depends on the actual visualisations you want to produce at the other end.

    The smoothingTimeConstant is approximately "How much do we want each sample frame to be like the previous one?". Too low and the visualisation is very jerky, too high and it barely changes.

    analyser.fftSize = 64;
    analyser.smoothingTimeConstant = 0.8;
    
    const bufferLength = analyser.frequencyBinCount;
    
    // Prepare the array to hold the analysed frequencies
    const frequency = new Uint8Array(bufferLength);

    Finally grab the values from the <input> elements.

    const titleText = document.querySelector("#track").value.toUpperCase();
    const artistText = document.querySelector("#artist").value.toUpperCase();

    Canvas API

    Now we do the standard canvas setup – grab a reference to the canvas, set our render size (doesn't need to be the same as the visible size of the canvas) and prepare a 2d context.

    Prepare Canvas

    const canvas = document.getElementById("canvas");
    canvas.width = 1280;
    canvas.height = 720;
    const ctx = canvas.getContext("2d");

    Render loop

    Execute this on every render frame.

    function renderFrame() {
      // schedule the next render
      requestAnimationFrame(renderFrame);
    
      // Grab the frequency analysis of the current frame
      analyser.getByteFrequencyData(frequency);
    
      // Draw the various elements of the visualisation
      // This bit is easy to modify into a plugin structure.
      drawBackground(ctx);
      drawBars(ctx, frequency);
      drawText(ctx, titleText, artistText);
    }

    The various visualisation elements:

    function drawBackground(ctx) {
      ctx.fillStyle = 'white';
      ctx.fillRect(0, 0, canvas.width, canvas.height);
    }
    function drawBars(ctx, frequency) {
      const widthOfEachBar = (canvas.width / frequency.length);
    
      // Loop over data array
      for (let i = 0; i < frequency.length; i++) {
        const heightOfThisBar = frequency[i];
    
        // Base the colour of the bar on its index
        const h = 360 * index;
        // Base the saturation on its height
        const s = 100 * (heightOfThisBar/256);
        const l = 50;
        const color = `hsl(${h}, ${s}%, ${l}%)`;
    
        ctx.fillStyle = color;
        
        // Add a little shadow/glow around each bar
        ctx.shadowBlur = 20;
        ctx.shadowColor = color;
    
        // Draw the individual bar 
        ctx.fillRect(x, canvas.height - heightOfThisBar, widthOfEachBar - 1, heightOfThisBar);
    
        x += (widthOfEachBar);
      }
    }
    function drawText(ctx, titleText, artistText) {
        ctx.fillStyle = 'white';
        ctx.textAlign = 'center';
        ctx.font = '4em sans-serif';
        ctx.fillText(titleText, canvas.width/2, canvas.height/2 - 25);
        ctx.fillText(artistText, canvas.width/2, canvas.height/2 + 25);
    }

    By this point, we can load a supported audio file and see some pretty pictures reacting to the music.

    MediaRecorder

    This section is copied almost word-for-word from StackOverflow

    First, we'll create a combined MediaStream object from the audio data and the canvas data.

    const chunks = []; // here we will store our recorded media chunks (Blobs)
    const stream = canvas.captureStream(); // grab our canvas MediaStream
    
    let combined = new MediaStream([
    ...stream.getTracks(),
    ...dest.stream.getTracks(),
    ]);

    Next, we start recording that data chunk-by-chunk. We'll save it as webm.

    const rec = new MediaRecorder(combined, {
      audioBitsPerSecond : 128000,
      videoBitsPerSecond : 2500000,
      mimeType : 'video/webm'
    });

    When the recorder receives a chunk of data, store it in memory.

    rec.ondataavailable = (e) => chunks.push(e.data);

    When we finish recording, combine the chunks and send them to an export method

    rec.onstop = (e) => exportVid(new Blob(chunks, { type: "video/webm" }));
    rec.start();

    This is sometime necessary to avoid async timing issues when loading the audio data. It doesn't hurt, at least.

    audio.addEventListener("durationchange", () => {
      setTimeout(() => rec.stop(), Math.floor(audio.duration * 1000));
    }

    The final video export. We convert the combined chunks (the Blob) into an object URL and create an anchor that lets us download it from the browser.

    function exportVid(blob) {
      const a = document.createElement("a");
      a.download = "myvid.webm";
      a.href = URL.createObjectURL(blob);
      a.textContent = "download";
      document.body.appendChild(a);
    }

    This export call will be triggered after the audio has finished. You load an audio file, watch the visualisation play through, when it's finished, you click the download link and you get a webm.


    So now, we have the basis for a complete music video generator – audio in, video file out.

    Try the basic version out – All-In-One Music Visualiser.

    Or watch a video generated using this basic version (as a bonus, this music was also automatically generated in the browser but that's a topic for another day):


    Don't forget, you can also find me on Spotify.

    Javascript, Geek, Design, Development

  • 6 Jun 2020

    flat-cube.js

    class FlatCube extends HTMLElement {
    
      constructor() {
        super();
    
        this.faces = 'URFDLB'.split('');
    
        this.facelet = false;
    
        // We attach an open shadow root to the custom element
        this._shadowRoot = this.attachShadow({ mode: 'open' });
    
        this.template = this.createTemplate();
    
        this.render();
      }
    
      style() {
        return `
    :host {
      display: inline-block;
      --flat-cube-face-width: var(--flat-cube-face, 100px);
    }
    .flat-cube {
      position: relative;
      height: calc(3 * var(--flat-cube-face-width));
      margin: 0 auto;
      width: calc(4 * var(--flat-cube-face-width));
    }
    .face {
      display: flex;
      height: var(--flat-cube-face-width);
      width: var(--flat-cube-face-width);
      outline: 1px solid var(--flat-cube-outer, black);
      position: absolute;
      flex-wrap: wrap;
      justify-content: space-between;
    }
    .U-face {top:0; left: var(--flat-cube-face-width); }
    .L-face {top:var(--flat-cube-face-width); left: 0; }
    .F-face {top:var(--flat-cube-face-width); left: var(--flat-cube-face-width); }
    .R-face {top:var(--flat-cube-face-width); left: calc(2 * var(--flat-cube-face-width)); }
    .B-face {top:var(--flat-cube-face-width); left: calc(3 * var(--flat-cube-face-width)); }
    .D-face {top:calc(2 * var(--flat-cube-face-width)); left: var(--flat-cube-face-width); }
    .face > div {
      width: calc(var(--flat-cube-face-width)/3);
      height: calc(var(--flat-cube-face-width)/3);
      outline: 1px solid var(--flat-cube-inner, black);
    }
    .U-piece {background-color: var(--flat-cube-up,    #ebed2b); }
    .L-piece {background-color: var(--flat-cube-left,  #ff6b16); }
    .F-piece {background-color: var(--flat-cube-front, #6cfe3b); }
    .R-piece {background-color: var(--flat-cube-right, #ec1d35); }
    .B-piece {background-color: var(--flat-cube-back,  #4db4d7); }
    .D-piece {background-color: var(--flat-cube-down,  #fffbf8); }
    `;
      }
    
      createTemplate() {
        const template = document.createElement('template');
        template.innerHTML = `<style>${this.style()}</style>`;
        const cubeElement = document.createElement('div');
        cubeElement.classList.add('flat-cube');
        this.faces.forEach((face, i) => {
          const faceElement = document.createElement('div');
          faceElement.classList.add('face');
          faceElement.classList.add(`${face}-face`);
          for (let j=0; j < 9; j++) {
            faceElement.appendChild(this.preparePiece(i, j));
          }
          cubeElement.appendChild(faceElement);
        });
        template.content.appendChild(cubeElement);
        return template;
      }
    
      updateTemplate() {
        const update = this.template.content.cloneNode(true);
        update.querySelectorAll('.face').forEach((face, i) => {
          face.querySelectorAll('div').forEach((piece, j) => {
            this.preparePiece(i, j, piece);
          });
        });
        return update;
      }
    
      preparePiece(i, j, piece = document.createElement('div')) {
        const start = (i * 9) + j;
        const end = (i * 9) + j + 1;
        piece.className = !this.facelet ? `${this.faces[i]}-piece` : `${this.facelet.slice(start, end)}-piece`;
        return piece;
      }
    
      render() {
        this._shadowRoot.innerHTML = '';
        this._shadowRoot.appendChild(this.updateTemplate());
      }
    
      static get observedAttributes() {
        return ['facelet'];
      }
    
      attributeChangedCallback(name, oldValue, newValue) {
        this.facelet = newValue;
        this.render();
      }
    }
    
    if (!window.customElements.get('flat-cube')) {
      customElements.define('flat-cube', FlatCube);
    }
    

    Javascript

  • 6 Jun 2020

    Line-by-line: Flat Cube Web Component

    Line-by-Line breakdowns go into excessive – and sometimes unnecessary – detail about a specific, small project. Be prepared for minutiae.

    Here, I'll go through the FlatCube web component line-by-line. It is a single web component that draws a flattened-out representation of a Rubik's Cube (or other 3x3 twisty puzzle) based on a string passed in that describes the positions of the pieces. A cube contains six faces. A face contains nine pieces.

    You'll probably want to have the full code open in another window to see this in context.

    The component

    Usage

    <flat-cube facelet="UUUUUUUUURRRRRRRRRFFFFFFFFFDDDDDDDDDLLLLLLLLLBBBBBBBBB" />

    Line-by-line

    First, all WebComponents must extend the HTMLElement class. If you're building with a library such as LitElement, you might extend a different class but that class will ultimately extend HTMLElement.

    class FlatCube extends HTMLElement {

    The constructor is called every time a FlatCube element is created, not just once per page load.

      constructor() {

    We have to call the constructor on HTMLElement so that all the behind-the-scenes plumbing is taken care of.

    NOTE: If we don't do this, we can't use this.

        super();

    Now set up some internal variables for the FlatCube itself. this.faces becomes an array representing the order of faces in the facelet string.

        this.faces = 'URFDLB'.split('');
    
        this.facelet = false;

    We attach the shadow DOM to this element so that we can access it easily during the render phase.

        this._shadowRoot = this.attachShadow({ mode: 'open' });

    Then we create the template (see below) and trigger the first render to actually show the element on screen.

        this.template = this.createTemplate();
        this.render();
     }

    Style

    It isn't essential to separate the component's CSS into another method but I like to do it to keep everything nice and tidy.

    style() {

    By using template literals, we can write a block of plain CSS.

        return `

    The :host CSS selector references the element itself. It's kinda like this but in CSS.

    NOTE: It can only be used from inside the Shadow DOM

    :host {

    I want this component to be able to be used inline or as a block item so I'm specifying inline-block. If the context it ends up being used in requires it to be block, it's possible to wrap it in another element.

      display: inline-block;

    Skinning and scaling

    In this web component, one of the main goals of the implementation was the ability to easily change the size of the component and the colours of the faces.

    Luckily, CSS variables make it super easy to make components skinnable and the calc function is very useful for scaling.

    The base measurement

    All the dimensions – the full component width, the faces, the individual pieces – are multiples of the --flat-cube-face-width value. This is passed in from the containing CSS by specifying a value for --flat-cube-face but if it is not specified, we want a fallback of 100px.

      --flat-cube-face-width: var(--flat-cube-face, 100px);
    }

    Now the styles for the complete element. Set position to be relative so that we can absolutely position the individual faces.

    .flat-cube {
      position: relative;

    And specify the element to be the height of 3 faces and the width of 4. This is where the calc function comes in handy, especially in a web component intended to be reusable and seamlessly scalable.

      height: calc(3 * var(--flat-cube-face-width));
      width: calc(4 * var(--flat-cube-face-width));

    I'm using outline rather than border for the lines between the pieces so I want to add a 1px margin around the outside to prevent clipping.

      margin: 1px;
    }

    Each individual face shares the same class

    .face {

    Use the value passed in as the base measurement.

      height: var(--flat-cube-face-width);
      width: var(--flat-cube-face-width);

    Each face is absolutely positioned inside the containing .flat-cube element.

      position: absolute;

    But rather than specify exact positions for each individual piece in a face, we use flexbox to lay them out automatically. We draw the pieces in order then let them wrap onto the next line,

      display: flex;
      flex-wrap: wrap;

    I wanted to specify the width of each piece as a simple ⅓ of the face width. In order to do that, I used outline rather than border as border actually takes space in the element where outline doesn't.

      outline: 1px solid var(--flat-cube-outer, black);
    }
    

    These are simply the top and left positions of the individual faces. We don't really need to go line-by-line here.

    .U-face {
      top: 0;
      left: var(--flat-cube-face-width);
    }
    .L-face {
      top: var(--flat-cube-face-width);
      left: 0;
    }
    .F-face {
      top: var(--flat-cube-face-width);
      left: var(--flat-cube-face-width);
    }
    .R-face {
      top: var(--flat-cube-face-width);
      left: calc(2 * var(--flat-cube-face-width));
    }
    .B-face {
      top: var(--flat-cube-face-width);
      left: calc(3 * var(--flat-cube-face-width));
    }
    .D-face {
      top: calc(2 * var(--flat-cube-face-width));
      left: var(--flat-cube-face-width);
    }
    

    Using the child selector to access the pieces inside the face.

    .face > div {

    As I mentioned above, we want to calculate the width of the pieces simply as ⅓ of a face so we use outline. The alternative would be to calculate the pieces as (⅓ of (the face width minus 2 * the internal border width)). That sounds mistake-prone.

      width: calc(var(--flat-cube-face-width)/3);
      height: calc(var(--flat-cube-face-width)/3);
      outline: 1px solid var(--flat-cube-inner, black);
    }

    Again, this is just colours. We don't need to go line-by-line. The only thing to note is that each piece has a fallback colour specified in case the containing application doesn't pass one in.

    .U-piece {
      background-color: var(--flat-cube-up, #ebed2b);
    }
    .L-piece {
      background-color: var(--flat-cube-left, #ff6b16);
    }
    .F-piece {
      background-color: var(--flat-cube-front, #6cfe3b);
    }
    .R-piece {
      background-color: var(--flat-cube-right, #ec1d35);
    }
    .B-piece {
      background-color: var(--flat-cube-back, #4db4d7);
    }
    .D-piece {
      background-color: var(--flat-cube-down, #fffbf8);
    }

    And finally, we close off our template literal and end the style method.

    `;
      }

    Template

    Now we build up the actual DOM of the element. We've done a lot of styling so far but, technically, we've nothing to apply the styles to. For that, we're going to build up the structure of faces and pieces then attach the styles.

      structure() {

    At this point, we have a couple of choices. We can either build this structure once and update it or build it fresh every time we need to make a change. The latter is easier to write but the former has better performance. So let's do that.

    This is the createTemplate method we called in the constructor. It is called only once for each instance of the component so we don't need to go through the whole building process every time.

    createTemplate() {

    First, create a new template. Templates are designed for exactly this case – building a structure once and reuse it several times.

        const template = document.createElement('template');

    Then we attach the styles we defined earlier:

        template.innerHTML = `<style>${this.style()}</style>`;

    And, finally, actually create the first element that actually appears in the component. This is the div that contains everything. We also add the .flat-cube class to it.

        const cubeElement = document.createElement('div');
        cubeElement.classList.add('flat-cube');

    The this.faces array we defined in the constructor comes back here. We loop over each face we require and create the DOM for it.

        this.faces.forEach((face, i) => {

    A div to contain the face with the shared .face class for the size and the specific class for the position and colour – .U-face, .B-face, etc.

          const faceElement = document.createElement('div');
          faceElement.classList.add('face');
          faceElement.classList.add(`${face}-face`);

    Now we create the individual pieces. If we wanted to make this component customisable so that it could represent cubes with a different number of pieces(2x2, 4x4, 17x17, etc.), we'd use a variable here instead of 9.

          for (let j=0; j < 9; j++) {

    Now we call out to the preparePiece method (see below) without an element to make sure the piece has the right class assigned to it before we append the piece to the face.

            faceElement.appendChild(this.preparePiece(i, j));
          }

    By the time we get here, we have a face div with 9 piece divs appended. Now we can add that to the cube.

          cubeElement.appendChild(faceElement);
        });

    Do that for each face and we have a div containing a completed flat-cube which we can append to the template.

        template.content.appendChild(cubeElement);

    And return the template to the constructor.

        return template;
      }

    Updating

    Now we have the structure in a template, we can grab a copy of it any time we need to update the cube.

      updateTemplate() {

    Passing true to cloneNode means we get a deep clone (containing all the nested faces and pieces) rather than a shallow clone (just the top-level element).

        const update = this.template.content.cloneNode(true);

    We loop over each face and then each piece (div) in each face to update it.

        update.querySelectorAll('.face').forEach((face, i) => {
          face.querySelectorAll('div').forEach((piece, j) => {

    We're using the same method here as we did to create the individual pieces (code reuse is A Good Thing) but this time we're passing in the piece we already have rather than asking the method to create a new one.

            this.preparePiece(i, j, piece);

    The update variable now contains an updated DOM representing the current facelet string.

          });
        });
        return update;
      }

    This method takes the i (index of the face) and j (index of the piece) we need to figure out which colour this piece needs to be. It also takes an optional argument of piece. If we don't provide that – the way we do in the initial createTemplate call – piece will be a newly created div. If we do provide that argument, we'll update whatever is passed in instead.

      preparePiece(i, j, piece = document.createElement('div')) {

    We have to map the facelet string – default: "UUUUUUUUU...etc" – into the two-dimensional structure of faces and pieces. Okay, technically, it's a one-dimensional mapping of a two-dimensional mapping of a three-dimensional structure. But... let's just not.

        const start = (i * 9) + j;
        const end = (i * 9) + j + 1;
    

    This means "If we don't have a facelet string, just colour the piece according to what face it is in, otherwise, colour it according to the i,j position in the facelet string".

        piece.className = !this.facelet ? `${this.faces[i]}-piece` : `${this.facelet.slice(start, end)}-piece`;

    Once we've updated the piece, return it so it can be included in the content.

        return piece;
      }

    We call this method in the constructor and every time we want to update.

      render() {

    Empty out the element content

        this._shadowRoot.innerHTML = '';

    And replace it immediately with the content we generate with the update method from above.

        this._shadowRoot.appendChild(this.updateTemplate());
      }

    This is how we register the component to listen for changes. This getter returns an array listing the attributes we want to listen for.

      static get observedAttributes() {

    There's only one attribute we care about listening to. Any change to the facelet attribute will cause us to re-render.

        return ['facelet'];
      }

    The other part of registering for changes to the attributes. This is the handler that is invoked with the name of the changed attribute (useful when you're listening to a lot of attributes), the oldValue (before the change) and the newValue (after the change).

      attributeChangedCallback(name, oldValue, newValue) {

    We only care about the newValue because we've only registered a single listener and we don't need the oldValue.

        this.facelet = newValue;

    Then we trigger a new render to update the state of the element.

        this.render();
      }

    And we close out our FlatCube class.

    }

    Listening for events

    The final part of the puzzle is to register our new element with the browser so that it knows what to do when it sees our element. We do this by passing our new element to the CustomElementRegistry.

    I like to check if my element has already been registered. Without this, including the script twice by accident will trigger an error that the user of the component isn't necessarily going to recognise.

    if (!window.customElements.get('flat-cube')) {

    To register, you pass the tag name you want to use and the element class.

    NOTE: tag names for custom components must contain a hyphen.

      customElements.define('flat-cube', FlatCube);
    }

    And that's it. Every line looked at and, hopefully, explained.

    Let me know what you would have done differently or if there are any (ideally small) projects you'd like me to look at line-by-line.

    Geek, Development, Javascript, CSS

  • older posts

Categories

Toys, Guides, Opinion, Geek, Non-geek, Development, Design, CSS, JS, Open-source Ideas, Cartoons, Photos

Shop

Colourful clothes for colourful kids

I'm currently reading

Projects

  • Awsm Street – Kid's clothing
  • Stickture
  • Explanating
  • Open Source Snacks
  • My life in sans-serif
  • My life in monospace
Simon Madine (thingsinjars)

@thingsinjars

Hi, I’m Simon Madine and I make music, write books and code.

I’m the CTO for workpin.

© 2023 Simon Madine