Philippa Markovics / Dec 06 2018

Animated Sonakinatography in Javascript

Sonakinatography

This article provides a very basic animated reproduction of Channa Horowitz’ Sonakinatography. For a detailed write-up see David’s article Sound, Motion, Notation.

Time Structure Composition III, Sonakinatography I, Photo by David Schmüdde

Implementation

Language:HTML
<style>
  #sk-demo {
    width: 260px;
    padding-left: 20px;
    position: relative;
  	margin: 0 auto;
  }
  #sk-scroll {
    height: 240px;
    width: 240px;
    overflow: auto;
    padding-bottom: 240px;
    background: white;
    box-sizing: border-box;
    position: relative;
    border: 1px solid #ddd;
  }
  #sk-play {
    font-size: 40px;
    position: absolute;
    bottom: -180px;
    left: 50%;
    cursor: pointer;
    transition: all 0.2s ease;
    transform: translate(-50%, -50%);
  }
  #sk-play:hover {
    transform: translate(-50%, -50%) scale(1.1);
  }
  #sk-wrapper {
    position: relative;
    padding: 0 40px 0 60px;
  }
  .beat-label,
  .instrument-label,
  #sk-beats-label,
  #sk-replay {
    font-size: 9px;
    font-family: Menlo, monospace;
    color: #666;
    line-height: 20px;
  }
  .beat-label {
    position: absolute;
    text-align: right;
    width: 30px;
    height: 20px;
    left: 0;
    padding-right: 10px;
    margin-bottom: 4px;
    border-right: 1px solid #ddd;
  }
  #sk-legend {
    align-items: center;
    width: 240px;
  }
  #sk-instrument-labels {
    list-style: none;
    padding: 0 0 0 60px;
    margin: 0 0 0 0;
    display: flex;
    align-items: center;
    border: 1px solid #ddd;
    border-top: none;
  }
  .instrument-label {
    margin: 0;
    height: 20px;
    line-heigiht: 20px;
    width: 20px;
    text-align: center;
  }
  #sk-legend-label {
    width: 100%;
  }
  #sk-beats-label {
    transform-origin: center center;
    transform: rotate(-90deg) translateY(-110%);
    position: absolute;
    top: 50%;
    margin-top: -20px;
  }
  #sk-replay {
    height: 30px;
    line-height: 32px;
    font-size: 13px;
    text-align: center;
    border-bottom: 1px solid #ddd;
    width: 100%;
    cursor: pointer;
    transition: all 0.125s ease;
  }
  #sk-replay:hover {
    font-size: 15px;
  }
</style>

<div id="sk-container">
 <div id="sk-demo">
	<div id="sk-scroll">
    <div id="sk-replay">🔁 Replay</div>
    <div id="sk-wrapper">
      <canvas id="sk" width="160"></canvas>
      <div id="sk-play">▶️</div>
    </div>
  </div>
  <div id="sk-legend">
    <ul id="sk-instrument-labels">
      <li class="instrument-label">1</li>
      <li class="instrument-label">2</li>
      <li class="instrument-label">3</li>
      <li class="instrument-label">4</li>
      <li class="instrument-label">5</li>
      <li class="instrument-label">6</li>
      <li class="instrument-label">7</li>
      <li class="instrument-label">8</li>
    </ul>
    <div id="sk-legend-label" class="instrument-label">
      Instrument
    </div>
  </div>
 	<div id="sk-beats-label">Beats</div>
 </div>
</div>

<script>
  const colors = [
    "rgb(0, 204, 0)", // yellow green
    "rgb(0, 102, 0)", // green
    "rgb(0, 0, 255)", // blue
    "rgb(127, 0, 255)", // blue violet
    "rgb(153, 0, 153)", // red violet
    "rgb(204, 0, 0)", // red
    "rgb(255, 128, 0)", // orange
    "rgb(255, 255, 0)" // yellow
  ];
  var gridObjects = [0,1,2,3,4,5,6,7].map(function(n) {
    return { countdown: n + 1, color: n, instrument: n }
  });
  const squareSize = 20;
  const lineWidth = 3;
  const maxBeats = 200;
  const visibleBeats = 12;
  var canvas = document.getElementById("sk");
  var ctx = canvas.getContext("2d");
  var beat = 0;
  var scrollEl = document.getElementById("sk-scroll");
  var wrapperEl = document.getElementById("sk-wrapper");

  canvas.setAttribute("height", squareSize * maxBeats);

  while (beat < maxBeats) {
    beat += 1;
    const beatLabel = document.createElement("div");
    beatLabel.className = "beat-label";
    beatLabel.innerHTML = beat;
    beatLabel.style.bottom = (beat - 1) * squareSize + "px";
    wrapperEl.appendChild(beatLabel);
    gridObjects.map(function(instrument) {
      instrument.countdown -= 1;
      var x = squareSize * instrument.instrument;
      var y = maxBeats * squareSize - beat * squareSize;
      ctx.fillStyle = colors[instrument.color];
      if (instrument.countdown === 0) {
        ctx.fillRect(x, y, squareSize, squareSize);
        if (instrument.color === 0) {
          instrument.color = 7;
        } else {
          instrument.color -= 1;
        }
        instrument.countdown = instrument.color + 1;
      } else {
        x = x + squareSize / 2 - 2;
        ctx.fillRect(x, y, lineWidth, squareSize);
      }
    });
  }

  const easeInOutQuad = function (t, b, c, d) {
    t /= d / 2;
    if (t < 1) return c / 2 * t * t + b;
    t--;
    return - c / 2 * (t * (t - 2) - 1) + b;
  };

  function scrollTo(el, to, duration) {
    var start = el.scrollTop,
        change = to - start,
        currentTime = 0,
        increment = 20;
    var animateScroll = function() {
      currentTime += increment;
      var val = easeInOutQuad(currentTime, start, change, duration);
      el.scrollTop = val;
      if (currentTime < duration) {
        setTimeout(animateScroll, increment);
      }
    };
    animateScroll();
  }

  function animate() {
    scrollEl.scrollTop = scrollEl.scrollHeight;
    scrollTo(scrollEl, 0, 10000);
  }
  scrollEl.scrollTop = scrollEl.scrollHeight;
  document.getElementById("sk-play").addEventListener("click", animate);
  document.getElementById("sk-replay").addEventListener("click", animate);
</script>