Home Examples Documentation Playground Source

Stardust Example: WebVR

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
    <meta name="mobile-web-app-capable" content="yes" />
    <meta name="apple-mobile-web-app-capable" content="yes" />

    <style>
      html,
      body {
        width: 100%;
        height: 100%;
        background-color: #000;
        color: #ccc;
        margin: 0px;
        padding: 0;
        overflow: hidden;
        font-family: Helvetica, Arial, sans-serif;
        font-size: 16px;
      }
      span.error {
        padding: 10px;
        display: block;
      }
      span.overlay {
        position: absolute;
        display: block;
        left: 0;
        top: 0;
        right: 0;
        bottom: 0;
        padding: 10px;
        pointer-events: none;
      }
      canvas {
        position: absolute;
        top: 0;
      }
    </style>
  </head>
  <body>
    <canvas id="main-canvas"></canvas>
    <span class="overlay" id="overlay-element">Click the screen to start WebVR</span>

    <script type="text/javascript">
      WebVRConfig = {
        BUFFER_SCALE: 1.0
      };
    </script>
    <script type="text/javascript" src="webvr-polyfill.min.js"></script>
    <script src="//d3js.org/d3.v3.min.js" type="text/javascript"></script>
    <script type="text/javascript" src="../common/stardust/stardust.bundle.js"></script>
    <script type="text/javascript" src="userCode.js"></script>
    <script type="text/javascript">
      let canvas = document.getElementById("main-canvas");
      canvas.getContext("webgl", { alpha: false });
      let width = window.innerWidth;
      let height = window.innerHeight;
      let platform = Stardust.platform("webgl-webvr", canvas, width, height);
      platform.pixelRatio = 1;

      UserCode(platform, userContext => {
        navigator.getVRDisplays().then(displays => {
          let vrDisplay = displays[0];
          if (!vrDisplay) {
            document.body.innerHTML = '<span class="error">Could not find WebVR display</span>';
            return;
          }

          function onResize() {
            if (vrDisplay && vrDisplay.isPresenting) {
              // If we're presenting we want to use the drawing buffer size
              // recommended by the VRDevice, since that will ensure the best
              // results post-distortion.
              var leftEye = vrDisplay.getEyeParameters("left");
              var rightEye = vrDisplay.getEyeParameters("right");
              // For simplicity we're going to render both eyes at the same size,
              // even if one eye needs less resolution. You can render each eye at
              // the exact size it needs, but you'll need to adjust the viewports to
              // account for that.
              platform.resize(
                Math.max(leftEye.renderWidth, rightEye.renderWidth) * 2,
                Math.max(leftEye.renderHeight, rightEye.renderHeight)
              );
            } else {
              platform.resize(window.innerWidth, window.innerHeight);
            }
          }
          function onVRDisplayPresentChange() {
            onResize();
          }

          window.addEventListener("resize", onResize, false);
          window.addEventListener("vrdisplaypresentchange", onVRDisplayPresentChange);

          let frameData = new VRFrameData();

          function onAnimationFrame() {
            if (vrDisplay.isPresenting) {
              platform.clear([0, 0, 0, 1]);

              vrDisplay.getFrameData(frameData);

              // When presenting render a stereo view.
              platform._GL.viewport(0, 0, canvas.width * 0.5, canvas.height);
              platform.setWebVRView(frameData.leftViewMatrix, frameData.leftProjectionMatrix);
              userContext.render();

              platform._GL.viewport(canvas.width * 0.5, 0, canvas.width * 0.5, canvas.height);
              platform.setWebVRView(frameData.rightViewMatrix, frameData.rightProjectionMatrix);
              userContext.render();
              // If we're currently presenting to the VRDisplay we need to
              // explicitly indicate we're done rendering.
              vrDisplay.submitFrame();
            }
            vrDisplay.requestAnimationFrame(onAnimationFrame);
          }
          vrDisplay.requestAnimationFrame(onAnimationFrame);

          canvas.onclick = () => {
            document.getElementById("overlay-element").remove();
            canvas.onclick = null;
            vrDisplay.requestPresent([{ source: canvas }]).catch(err => {
              document.body.innerHTML = '<span class="error">' + err + "</span>";
            });
          };
        });
      });
    </script>
  </body>
</html>

userCode.js

function UserCode(platform, callback) {
  d3.json("beethoven.json", (err, DATA) => {
    let marks = Stardust.mark.compile(`
            import { Triangle } from P3D;

            mark Point(
                center: Vector3,
                size: float,
                color: Color
            ) {
                let p1 = Vector3(center.x + size, center.y + size, center.z - size);
                let p2 = Vector3(center.x + size, center.y - size, center.z + size);
                let p3 = Vector3(center.x - size, center.y + size, center.z + size);
                let p4 = Vector3(center.x - size, center.y - size, center.z - size);
                Triangle(p1, p2, p3, color);
                Triangle(p4, p1, p2, color);
                Triangle(p4, p2, p3, color);
                Triangle(p4, p3, p1, color);
            }

            mark Line(
                p1: Vector3, p2: Vector3,
                size: float,
                color: Color
            ) {
                let x1 = Vector3(p1.x, p1.y, p1.z - size);
                let x2 = Vector3(p1.x, p1.y, p1.z + size);
                let x3 = Vector3(p2.x, p2.y, p2.z + size);
                let x4 = Vector3(p2.x, p2.y, p2.z - size);
                Triangle(x1, x2, x3, color);
                Triangle(x4, x1, x2, color);
                Triangle(x4, x2, x3, color);
                Triangle(x4, x3, x1, color);
            }

            function getPosition(year: float, dayOfYear: float, secondOfDay: float): Vector3 {
                let angle = dayOfYear / 366 * PI * 2;
                let dayScaler = (secondOfDay / 86400 - 0.5);
                let r = (year - 2006) / (2015 - 2006) * 200 + 50 + dayScaler * 50;
                let x = cos(angle) * r;
                let y = sin(angle) * r;
                let z = dayScaler * 50;
                return Vector3(x / 100, y / 100, z / 100);
            }

            function getPosition2(year: float, dayOfYear: float, secondOfDay: float): Vector3 {
                let angle = dayOfYear / 366 * PI * 2;
                let r = secondOfDay / 86400 * 200 + 50;
                let x = cos(angle) * r;
                let y = sin(angle) * r;
                let z = 0;
                return Vector3(x / 100, y / 100, z / 100);
            }

            mark Glyph(
                year: float,
                dayOfYear: float,
                secondOfDay: float,
                duration: float,
                t: float,
                color: Color
            ) {
                let p = getPosition(year, dayOfYear, secondOfDay);
                let p2 = getPosition2(year, dayOfYear, secondOfDay);
                Point(p * (1 - t) + p2 * t, log(1 + duration) / 200, color = color);
            }

            mark LineChart(
                year1: float,
                dayOfYear1: float,
                secondOfDay1: float,
                year2: float,
                dayOfYear2: float,
                secondOfDay2: float,
                c1: float,
                c2: float,
                t: float
            ) {
                let p1 = getPosition(year1, dayOfYear1, secondOfDay1);
                let p1p = getPosition2(year1, dayOfYear1, secondOfDay1);
                let p2 = getPosition(year2, dayOfYear2, secondOfDay2);
                let p2p = getPosition2(year2, dayOfYear2, secondOfDay2);
                p1 = p1 + (p1p - p1) * t;
                p2 = p2 + (p2p - p2) * t;
                p1 = Vector3(p1.x, p1.y, c1 / 100);
                p2 = Vector3(p2.x, p2.y, c2 / 100);
                Line(p1, p2, 0.5 / 100, Color(1, 1, 1, 0.8));
            }
        `);

    let mark = Stardust.mark.create(marks.Glyph, platform);
    let lineChart = Stardust.mark.create(marks.LineChart, platform);

    DATA.forEach(d => {
      d.duration = (d.checkInFirst - d.checkOut) / 86400;
      d.checkOut = new Date(d.checkOut * 1000);
      d.checkIn = new Date(d.checkInFirst * 1000);
    });
    DATA = DATA.filter(d => {
      if (!d.checkIn || !d.checkOut) return false;
      if (d.duration > 360) return false;
      return (
        d.checkOut.getFullYear() >= 2007 &&
        d.checkOut.getFullYear() < 2016 &&
        d.checkIn.getFullYear() >= 2007 &&
        d.checkIn.getFullYear() < 2016
      );
    });

    // Daily summary.
    let days = new Map();
    DATA.forEach(d => {
      let day = Math.floor(d.checkOut.getTime() / 1000 / 86400) * 86400;
      if (!days.has(day)) days.set(day, 1);
      else days.set(day, days.get(day) + 1);
    });
    let daysArray = [];
    days.forEach((count, day) => {
      daysArray.push({ day: new Date(day * 1000), count: count });
    });
    daysArray.sort((a, b) => a.day.getTime() - b.day.getTime());

    let colorScale = d3.scale.category10();
    let color = d => {
      let rgb = d3.rgb(colorScale(d.deweyClass));
      return [rgb.r / 255, rgb.g / 255, rgb.b / 255, 0.5];
    };

    let dayOfYear = d => {
      return Math.floor((d - new Date(d.getFullYear(), 0, 0)) / 86400000);
    };
    let secondOfDay = d => {
      return d.getMinutes() * 60 + d.getHours() * 3600 + d.getSeconds();
    };
    mark.attr("duration", d => d.duration);
    mark.attr("year", d => d.checkOut.getFullYear());
    mark.attr("dayOfYear", d => dayOfYear(d.checkOut));
    mark.attr("secondOfDay", d => secondOfDay(d.checkOut));
    mark.attr("color", color);
    // mark.attr("inYear", (d) => d.checkIn.getFullYear());
    // mark.attr("inDayOfYear", (d) => dayOfYear(d.checkIn));
    // mark.attr("inSecondOfDay", (d) => secondOfDay(d.checkIn));
    mark.data(DATA);

    lineChart.attr("year1", d => d.day.getFullYear());
    lineChart.attr("dayOfYear1", d => dayOfYear(d.day));
    lineChart.attr("secondOfDay1", d => secondOfDay(d.day));
    lineChart.attr("year2", (d, i) => daysArray[i + 1].day.getFullYear());
    lineChart.attr("dayOfYear2", (d, i) => dayOfYear(daysArray[i + 1].day));
    lineChart.attr("secondOfDay2", (d, i) => secondOfDay(daysArray[i + 1].day));
    let zScale = Stardust.scale
      .linear()
      .domain([0, d3.max(daysArray, d => d.count)])
      .range([20, 60]);
    lineChart.attr("c1", zScale(d => d.count));
    lineChart.attr("c2", zScale((d, i) => daysArray[i + 1].count));
    lineChart.data(daysArray.slice(0, -1));

    mark.attr("t", 0);

    let t0 = new Date().getTime();

    function render() {
      let time = (new Date().getTime() - t0) / 1000;
      let t = (Math.sin(time / 5) + 1) / 2;
      mark.attr("t", t);

      platform.setPose(new Stardust.Pose(new Stardust.Vector3(0, 0, 1), new Stardust.Quaternion(0, 0, 0, 1)));
      lineChart.attr("t", mark.attr("t"));
      lineChart.render();
      mark.render();
    }

    callback({
      render: render
    });
  });
}