Search code examples
javascripthtmlcssreaper

Making a fix scaled timeline in HTML/CSS/JS


Sorry, this is going to be a little long...

A bit of context

As part of a larger project I'm trying to make a project timeline to include in a webpage used to remote control a music making program (called Reaper for those who are interested). I'm trying to get it to display the current play position, the project markers, and project regions. These are all served straight from the program's API, no problem getting the info. For starters I'm just trying to display the project markers, however I've been pulling my hair out for over a week now trying to get it to work.

Here's a quick screencap from inside the software to illustrate what I'm trying to emulate: Reaper ruler screencap

Normally I would use a progress bar for something like this, or one of the thousands of examples from the web, however I have no way of knowing the length of the project as the software doesn't limit it. As a result I fell back onto using a fixed scale of 10px per bar. Kind of arbitrary, I chose that as it's the optimal for a 5 minute song at 120 bpm. Not too worried about looks for the moment, I just want to get it to work haha.

The problem I have (code is included at the bottom) is that because I'm using absolute positioning for the markers in order to align them all from the left of the screen, they are pulled from the document flow and so I can't wrap them in the parent div. In the end I intend setting the parent div to an 80% width with a scrollbar to see the rest of the markers, so obviously I'm doing it wrong. However I can't seem to find any coded snippets of anything similar to what I'm trying to achieve.

So here is the actual question:

What sort of display/position/float CSS should I be using to do this instead of position: absolute and float: left? If I need JS to do it, then how to I go about it?

Thanks for any help you can bring me, be it actual code or just a nudge in the right direction!


Here's my (relevant) code:

index.html

<html>
    <body>
        <div id="timeline">
            <div id="labels"></div>
            <div id="markers"></div>
        </div>
    </body>
</html>

script.js:

// hardcoded for debugging purposes
// See section below about the API for info about how I get this data
var markers = [
    {label: "Start", pos: "20.00000000000000"},
    {label: "First", pos: "50.00000000000000"},
    {label: "Second", pos: "200.00000000000000"},
    {label: "Last", pos: "576.845412000000000"}
];

function draw_markers(marker_list) {
    var label_html = "";
    var marker_html = "";

    $.each(marker_list, function(index, obj) {
        var label = obj.label;
        var offset = parseFloat(obj.pos) * 7; // obj.pos is mesured in bars, not px

        label_html = label_html +
                    "<span class='label' style='margin-left:"+offset+"px'>" +
                    label + "</span>";

        marker_html = marker_html +
                    "<span class='marker' style='margin-left:"+offset+"px'>|</span>";
    });

    document.getElementById("labels").innerHTML = label_html;
    document.getElementById("markers").innerHTML = marker_html;
}

draw_markers(markers);

style.css:

    html, body {
    background: #eeeeee;
}

#timeline {
    height: 4em;
    width: 100%;
    background-color: #9E9E9E;
}

#labels {
    border-bottom: 1px solid black;
    height: 50%;
    width: 100%;
}

#markers {
    height: 50%;
    width: 100%;
}

.label {
    position: absolute;
    float: left;
}
.marker {
    position: absolute;
    float: left;
}

About the API

We're given a bunch of functions that poll the server at regular intervals and parse the (cleartext) responses. A typical response looks something like this:

MARKERLIST_BEGINMARKER_LIST
MARKER \t label \t ID \t position
...
MARKER_LIST_END
TRANSPORT \t playstate \t position_seconds \t isRepeatOn \t position_string \t position_string_beats
...

Using JS I split each line and use a switch statement to figure out what to do with each line. I then build up a global array containing all the marker in the project with just the info I need.


Solution

  • Alternatively you could use <canvas> to draw the timeline from Javascript (this is what I use for Song Switcher's web interface). Also you can get the project length using the GetProjectLength API function (for example, by calling a script to put the length into a temporary extstate then reading it from the web interface).

    function Timeline(canvas) {
      this.canvas  = canvas;
      this.ctx     = canvas.getContext('2d');
      this.length  = 0;
      this.markers = [];
    }
    
    Timeline.prototype.resize = function() {
      this.canvas.width  = this.canvas.clientWidth;
      this.canvas.height = this.canvas.clientHeight;
      
      this.scale = this.length / this.canvas.width;
    }
    
    Timeline.prototype.update = function() {
      this.resize();
      
      this.ctx.fillStyle = '#414141';
      this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
      
      this.ctx.textBaseline = 'hanging';
      
      for(var marker of this.markers)
        this.drawMarker(marker);
    }
    
    Timeline.prototype.drawMarker = function(marker) {
      const MARKER_WIDTH = 2;
      const FONT_SIZE    = 14;
      const PADDING      = MARKER_WIDTH * 2;
     
      var xpos = this.timeToPx(marker.pos);
    
      this.ctx.strokeStyle = this.ctx.fillStyle = 'red';
      this.ctx.lineWidth   = MARKER_WIDTH;
    
      this.ctx.beginPath();
      this.ctx.moveTo(xpos, 0);
      this.ctx.lineTo(xpos, this.canvas.height);
      this.ctx.stroke();
    
      if(marker.name.length > 0) {
        this.ctx.font = `bold ${FONT_SIZE}px sans-serif`;
    
        var boxWidth = this.ctx.measureText(marker.name).width + PADDING;
        this.ctx.fillRect(xpos, 0, boxWidth, FONT_SIZE + PADDING);
    
        this.ctx.fillStyle = 'white';
        this.ctx.fillText(marker.name, xpos + MARKER_WIDTH, PADDING);
      }
    }
    
    Timeline.prototype.timeToPx = function(time) {
      return time / this.scale;
    }
    
    var timeline = new Timeline(document.getElementById('timeline'));
    timeline.length  = 30; // TODO: Fetch using GetProjectLength
    timeline.markers = [
      {pos:  3, name: "Hello"},
      {pos: 15, name: "World!"},
      {pos: 29, name: ""},
    ];
    timeline.update();
    
    window.addEventListener('resize', timeline.update.bind(timeline));
    #timeline {
      height: 50px;
      line-height: 50px;
      image-rendering: pixelated;
      width: 100%;
    }
    <canvas id="timeline"></canvas>