Restore the JS Spinning Cube demo

This version is only slightly changed from the original:
- Started with the JS content handler
- Uses classes, Application
- Improved resize handling

BUG=
R=aa@chromium.org

Review URL: https://codereview.chromium.org/788863003
diff --git a/examples/js/README.md b/examples/js/README.md
index 586772a..3dcee69 100644
--- a/examples/js/README.md
+++ b/examples/js/README.md
@@ -5,6 +5,7 @@
 
 wget.js - Uses the network service to load a URL.
 
+cube.js - A JS version of examples/sample_app.
 
 --- Running Mojo Applications --- 
 
@@ -12,10 +13,11 @@
 
   mojo_shell <js-application-url>
 
-Where js-application-url is either a file or an http URL that names a JS source
-file. The JS file itself must begin with a Mojo "shebang" that specifies the
-Mojo URL of the JS content handler. In other words, the first line of the JS
-source file must be:
+Where js-application-url is a URL understood by the shell. For example
+a file or an http URL that names a JS source file. The JS file itself
+must begin with a Mojo "shebang" that specifies the Mojo URL of the JS
+content handler. In other words, the first line of the JS source file
+must be:
 
   #!mojo:js_content_handler
 
diff --git a/examples/js/cube.js b/examples/js/cube.js
new file mode 100644
index 0000000..0f772ec
--- /dev/null
+++ b/examples/js/cube.js
@@ -0,0 +1,416 @@
+#!mojo:js_content_handler
+
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+define("main", [
+  "console",
+  "mojo/services/public/js/application",
+  "mojo/services/public/interfaces/gpu/command_buffer.mojom",
+  "mojo/services/public/interfaces/geometry/geometry.mojom",
+  "mojo/services/public/interfaces/gpu/gpu.mojom",
+  "mojo/services/public/interfaces/native_viewport/native_viewport.mojom",
+  "mojo/services/public/interfaces/gpu/viewport_parameter_listener.mojom",
+  "mojo/public/js/core",
+  "services/js/modules/gl",
+  "services/js/modules/clock",
+  "timer",
+], function(console,
+            appModule,
+            cbModule,
+            geoModule,
+            gpuModule,
+            nvModule,
+            vplModule,
+            coreModule,
+            glModule,
+            clockModule,
+            timerModule) {
+
+  const VERTEX_SHADER_SOURCE = [
+    'uniform mat4 u_mvpMatrix;',
+    'attribute vec4 a_position;',
+    'void main()',
+    '{',
+    '   gl_Position = u_mvpMatrix * a_position;',
+    '}'
+  ].join('\n');
+
+  const FRAGMENT_SHADER_SOURCE = [
+    'precision mediump float;',
+    'void main()',
+    '{',
+    '  gl_FragColor = vec4( 0.0, 1.0, 0.0, 1.0 );',
+    '}'
+  ].join('\n');
+
+  class ESMatrix {
+    constructor() {
+      this.m = new Float32Array(16);
+    }
+
+    getIndex(x, y) {
+      return x * 4 + y;
+    }
+
+    set(x, y, v) {
+      this.m[this.getIndex(x, y)] = v;
+    }
+
+    get(x, y) {
+      return this.m[this.getIndex(x, y)];
+    }
+
+    loadZero() {
+      for (var i = 0; i < this.m.length; i++) {
+        this.m[i] = 0;
+      }
+    }
+
+    loadIdentity() {
+      this.loadZero();
+      for (var i = 0; i < 4; i++) {
+        this.set(i, i, 1);
+      }
+    }
+
+    multiply(a, b) {
+      var result = new ESMatrix();
+      for (var i = 0; i < 4; i++) {
+        result.set(i, 0,
+                   (a.get(i, 0) * b.get(0, 0)) +
+                   (a.get(i, 1) * b.get(1, 0)) +
+                   (a.get(i, 2) * b.get(2, 0)) +
+                   (a.get(i, 3) * b.get(3, 0)));
+
+        result.set(i, 1,
+                   (a.get(i, 0) * b.get(0, 1)) +
+                   (a.get(i, 1) * b.get(1, 1)) +
+                   (a.get(i, 2) * b.get(2, 1)) +
+                   (a.get(i, 3) * b.get(3, 1)));
+
+        result.set(i, 2,
+                   (a.get(i, 0) * b.get(0, 2)) +
+                   (a.get(i, 1) * b.get(1, 2)) +
+                   (a.get(i, 2) * b.get(2, 2)) +
+                   (a.get(i, 3) * b.get(3, 2)));
+
+        result.set(i, 3,
+                   (a.get(i, 0) * b.get(0, 3)) +
+                   (a.get(i, 1) * b.get(1, 3)) +
+                   (a.get(i, 2) * b.get(2, 3)) +
+                   (a.get(i, 3) * b.get(3, 3)));
+      }
+      for (var i = 0; i < result.m.length; i++) {
+        this.m[i] = result.m[i];
+      }
+    }
+
+    frustrum(left, right, bottom, top, nearZ, farZ) {
+      var deltaX = right - left;
+      var deltaY = top - bottom;
+      var deltaZ = farZ - nearZ;
+
+      if (nearZ < 0 || farZ < 0 || deltaZ < 0 || deltaY < 0 || deltaX < 0) {
+        return;
+      }
+
+      var frust = new ESMatrix();
+      frust.set(0, 0, 2 * nearZ / deltaX);
+
+      frust.set(1, 1, 2 * nearZ / deltaY);
+
+      frust.set(2, 0, (right + left) / deltaX);
+      frust.set(2, 1, (top + bottom) / deltaY);
+      frust.set(2, 2, -(nearZ + farZ) / deltaZ);
+      frust.set(2, 3, -1);
+
+      frust.set(3, 2, -2 * nearZ * farZ / deltaZ);
+
+      this.multiply(frust, this);
+    }
+
+    perspective(fovY, aspect, nearZ, farZ) {
+      var frustrumH = Math.tan(fovY / 360 * Math.PI) * nearZ;
+      var frustrumW = frustrumH * aspect;
+      this.frustrum(-frustrumW, frustrumW, -frustrumH, frustrumH, nearZ, farZ);
+    }
+
+    translate(tx, ty, tz) {
+      this.set(3, 0, this.get(3, 0) + this.get(0, 0) *
+               tx + this.get(1, 0) * ty + this.get(2, 0) * tz);
+      this.set(3, 1, this.get(3, 1) + this.get(0, 1) *
+               tx + this.get(1, 1) * ty + this.get(2, 1) * tz);
+      this.set(3, 2, this.get(3, 2) + this.get(0, 2) *
+               tx + this.get(1, 2) * ty + this.get(2, 2) * tz);
+      this.set(3, 3, this.get(3, 3) + this.get(0, 3) *
+               tx + this.get(1, 3) * ty + this.get(2, 3) * tz);
+    }
+
+    rotate(angle, x, y, z) {
+      var mag = Math.sqrt(x * x + y * y + z * z);
+      var sinAngle = Math.sin(angle * Math.PI / 180);
+      var cosAngle = Math.cos(angle * Math.PI / 180);
+      if (mag <= 0) {
+        return;
+      }
+
+      var xx, yy, zz, xy, yz, zx, xs, ys, zs, oneMinusCos;
+      var rotation = new ESMatrix();
+
+      x /= mag;
+      y /= mag;
+      z /= mag;
+
+      xx = x * x;
+      yy = y * y;
+      zz = z * z;
+      xy = x * y;
+      yz = y * z;
+      zx = z * x;
+      xs = x * sinAngle;
+      ys = y * sinAngle;
+      zs = z * sinAngle;
+      oneMinusCos = 1 - cosAngle;
+
+      rotation.set(0, 0, (oneMinusCos * xx) + cosAngle);
+      rotation.set(0, 1, (oneMinusCos * xy) - zs);
+      rotation.set(0, 2, (oneMinusCos * zx) + ys);
+      rotation.set(0, 3, 0);
+
+      rotation.set(1, 0, (oneMinusCos * xy) + zs);
+      rotation.set(1, 1, (oneMinusCos * yy) + cosAngle);
+      rotation.set(1, 2, (oneMinusCos * yz) - xs);
+      rotation.set(1, 3, 0);
+
+      rotation.set(2, 0, (oneMinusCos * zx) - ys);
+      rotation.set(2, 1, (oneMinusCos * yz) + xs);
+      rotation.set(2, 2, (oneMinusCos * zz) + cosAngle);
+      rotation.set(2, 3, 0);
+
+      rotation.set(3, 0, 0);
+      rotation.set(3, 1, 0);
+      rotation.set(3, 2, 0);
+      rotation.set(3, 3, 1);
+
+      this.multiply(rotation, this);
+    }
+  }
+
+  function loadProgram(gl) {
+    var vertexShader = gl.createShader(gl.VERTEX_SHADER);
+    gl.shaderSource(vertexShader, VERTEX_SHADER_SOURCE);
+    gl.compileShader(vertexShader);
+
+    var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
+    gl.shaderSource(fragmentShader, FRAGMENT_SHADER_SOURCE);
+    gl.compileShader(fragmentShader);
+
+    var program = gl.createProgram();
+    gl.attachShader(program, vertexShader);
+    gl.attachShader(program, fragmentShader);
+
+    gl.linkProgram(program);
+    // TODO(aa): Check for errors using getProgramiv and LINK_STATUS.
+
+    gl.deleteShader(vertexShader);
+    gl.deleteShader(fragmentShader);
+
+    return program;
+  }
+
+  var vboVertices;
+  var vboIndices;
+  function generateCube(gl) {
+    var numVertices = 24 * 3;
+    var numIndices = 12 * 3;
+
+    var cubeVertices = new Float32Array([
+      -0.5, -0.5, -0.5,
+      -0.5, -0.5,  0.5,
+      0.5, -0.5,  0.5,
+      0.5, -0.5, -0.5,
+      -0.5,  0.5, -0.5,
+      -0.5,  0.5,  0.5,
+      0.5,  0.5,  0.5,
+      0.5,  0.5, -0.5,
+      -0.5, -0.5, -0.5,
+      -0.5,  0.5, -0.5,
+      0.5,  0.5, -0.5,
+      0.5, -0.5, -0.5,
+      -0.5, -0.5, 0.5,
+      -0.5,  0.5, 0.5,
+      0.5,  0.5, 0.5,
+      0.5, -0.5, 0.5,
+      -0.5, -0.5, -0.5,
+      -0.5, -0.5,  0.5,
+      -0.5,  0.5,  0.5,
+      -0.5,  0.5, -0.5,
+      0.5, -0.5, -0.5,
+      0.5, -0.5,  0.5,
+      0.5,  0.5,  0.5,
+      0.5,  0.5, -0.5
+    ]);
+
+    var cubeIndices = new Uint16Array([
+      0, 2, 1,
+      0, 3, 2,
+      4, 5, 6,
+      4, 6, 7,
+      8, 9, 10,
+      8, 10, 11,
+      12, 15, 14,
+      12, 14, 13,
+      16, 17, 18,
+      16, 18, 19,
+      20, 23, 22,
+      20, 22, 21
+    ]);
+
+    // TODO(aa): The C++ program branches here on whether the pointer is
+    // non-NULL.
+    vboVertices = gl.createBuffer();
+    gl.bindBuffer(gl.ARRAY_BUFFER, vboVertices);
+    gl.bufferData(gl.ARRAY_BUFFER, cubeVertices, gl.STATIC_DRAW);
+    gl.bindBuffer(gl.ARRAY_BUFFER, 0);
+
+    vboIndices = gl.createBuffer();
+    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, vboIndices);
+    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, cubeIndices, gl.STATIC_DRAW);
+    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, 0);
+
+    return cubeIndices.length;
+  }
+
+  class GLES2ClientImpl {
+    constructor (remotePipe, size) {
+      this.gl_ = new glModule.Context(remotePipe, this.contextLost.bind(this));
+      this.lastTime_ = clockModule.seconds();
+      this.angle_ = 45;
+
+      this.program_ = loadProgram(this.gl_);
+      this.positionLocation_ =
+        this.gl_.getAttribLocation(this.program_, 'a_position');
+      this.mvpLocation_ =
+        this.gl_.getUniformLocation(this.program_, 'u_mvpMatrix');
+      this.numIndices_ = generateCube(this.gl_);
+      this.mvpMatrix_ = new ESMatrix();
+      this.mvpMatrix_.loadIdentity();
+
+      this.gl_.clearColor(0, 0, 0, 0);
+      this.setDimensions(size);
+      this.timer_ =
+          timerModule.createRepeating(16, this.handleTimer.bind(this));
+    }
+
+    setDimensions(size) {
+      this.width_ = size.width;
+      this.height_ = size.height;
+      this.gl_.resize(size.width, size.height, 1);
+    }
+
+    drawCube() {
+      this.gl_.viewport(0, 0, this.width_, this.height_);
+      this.gl_.clear(this.gl_.COLOR_BUFFER_BIT);
+      this.gl_.useProgram(this.program_);
+      this.gl_.bindBuffer(this.gl_.ARRAY_BUFFER, vboVertices);
+      this.gl_.bindBuffer(this.gl_.ELEMENT_ARRAY_BUFFER, vboIndices);
+      this.gl_.vertexAttribPointer(this.positionLocation_, 3, this.gl_.FLOAT,
+                                   false, 12, 0);
+      this.gl_.enableVertexAttribArray(this.positionLocation_);
+      this.gl_.uniformMatrix4fv(this.mvpLocation_, false, this.mvpMatrix_.m);
+      this.gl_.drawElements(this.gl_.TRIANGLES, this.numIndices_,
+                            this.gl_.UNSIGNED_SHORT, 0);
+      this.gl_.swapBuffers();
+    };
+
+    handleTimer() {
+      var now = clockModule.seconds();
+      var secondsDelta = now - this.lastTime_;
+      this.lastTime_ = now;
+
+      this.angle_ += this.getRotationForTimeDelta(secondsDelta);
+      this.angle_ = this.angle_ % 360;
+
+      var aspect = this.width_ / this.height_;
+
+      var perspective = new ESMatrix();
+      perspective.loadIdentity();
+      perspective.perspective(60, aspect, 1, 20);
+
+      var modelView = new ESMatrix();
+      modelView.loadIdentity();
+      modelView.translate(0, 0, -2);
+      modelView.rotate(this.angle_, 1, 0, 1);
+
+      this.mvpMatrix_.multiply(modelView, perspective);
+
+      this.drawCube();
+    };
+
+    getRotationForTimeDelta(secondsDelta) {
+      return secondsDelta * 40;
+    };
+
+    contextLost() {
+      console.log('GLES2ClientImpl.prototype.contextLost');
+    };
+
+    quit() {
+      if (this.timer_) {
+        console.log("CANCEL");
+        this.timer_.cancel();
+        this.timer_ = null;
+      }
+    }
+  }
+
+  class CubeDemo extends appModule.Application {
+    initialize(args) {
+      this.viewport = this.shell.connectToService(
+          "mojo:native_viewport_service", nvModule.NativeViewport, this);
+
+      this.gpu = this.shell.connectToService(
+          "mojo:native_viewport_service", gpuModule.Gpu);
+
+      var app = this;
+      var viewportSize = new geoModule.Size({width: 800, height: 600});
+      this.viewport.create(viewportSize).then(
+        function(result) {
+          app.onViewportCreated(result.native_viewport_id, viewportSize);
+        });
+
+      this.eventDispatcher =
+          new nvModule.NativeViewportEventDispatcher.stubClass(this);
+      this.viewport.setEventDispatcher(this.eventDispatcher);
+      this.viewport.show();
+    }
+
+    onViewportCreated(id, size) {
+      this.vpl = new vplModule.ViewportParameterListener.stubClass({
+        onVSyncParametersUpdated: function(timebase, interval) {
+          console.log("onVSyncParametersUpdated");
+        }
+      });
+      var pipe = coreModule.createMessagePipe();
+      this.gpu.createOnscreenGLES2Context(id, size, pipe.handle1, this.vpl);
+      this.gles2_ = new GLES2ClientImpl(pipe.handle0, size);
+    }
+
+    onEvent(event) {
+      return Promise.resolve(); // This just gates the next event delivery
+    }
+
+    onSizeChanged(size) {
+      if (this.gles2_)
+        this.gles2_.setDimensions(size);
+    }
+
+    onDestroyed() {
+      this.quit();
+    }
+  }
+
+  return CubeDemo;
+});
diff --git a/services/js/js_app_runner_delegate.cc b/services/js/js_app_runner_delegate.cc
index 30db854..c8c2f6d 100644
--- a/services/js/js_app_runner_delegate.cc
+++ b/services/js/js_app_runner_delegate.cc
@@ -6,6 +6,7 @@
 
 #include "base/path_service.h"
 #include "gin/modules/console.h"
+#include "gin/modules/timer.h"
 #include "mojo/edk/js/core.h"
 #include "mojo/edk/js/handle.h"
 #include "mojo/edk/js/support.h"
@@ -36,6 +37,7 @@
 JSAppRunnerDelegate::JSAppRunnerDelegate()
     : ModuleRunnerDelegate(GetModuleSearchPaths()) {
   AddBuiltin<gin::Console>(this);
+  AddBuiltinModule(gin::TimerModule::kName, gin::TimerModule::GetModule);
   AddBuiltin<mojo::js::Core>(this);
   AddBuiltin<mojo::js::Support>(this);
   AddBuiltin<mojo::js::Threading>(this);
diff --git a/services/js/modules/gl/BUILD.gn b/services/js/modules/gl/BUILD.gn
index 54181ba..aa137c3 100644
--- a/services/js/modules/gl/BUILD.gn
+++ b/services/js/modules/gl/BUILD.gn
@@ -14,6 +14,8 @@
     "//base",
     "//gin",
     "//v8",
+    "//gpu/command_buffer/client:gles2_interface",
+    "//mojo/public/c/gles2",
     "//mojo/edk/js",
     "//mojo/environment:chromium",
     "//mojo/public/c/gles2",
diff --git a/services/js/modules/gl/context.cc b/services/js/modules/gl/context.cc
index 61450ef..5a476a6 100644
--- a/services/js/modules/gl/context.cc
+++ b/services/js/modules/gl/context.cc
@@ -5,11 +5,14 @@
 #include "services/js/modules/gl/context.h"
 
 #include <GLES2/gl2.h>
+#include <GLES2/gl2ext.h>
 
+#include "base/bind.h"
 #include "gin/arguments.h"
 #include "gin/array_buffer.h"
 #include "gin/object_template_builder.h"
 #include "gin/per_context_data.h"
+#include "gpu/command_buffer/client/gles2_interface.h"
 #include "mojo/public/c/gles2/gles2.h"
 #include "mojo/public/cpp/environment/environment.h"
 
@@ -140,6 +143,7 @@
       .SetMethod("getShaderInfoLog", GetShaderInfoLog)
       .SetMethod("getUniformLocation", GetUniformLocation)
       .SetMethod("linkProgram", glLinkProgram)
+      .SetMethod("resize", base::Bind(&Context::Resize, base::Unretained(this)))
       .SetMethod("shaderSource", ShaderSource)
       .SetMethod("swapBuffers", MojoGLES2SwapBuffers)
       .SetMethod("uniformMatrix4fv", UniformMatrix4fv)
@@ -165,6 +169,13 @@
   MojoGLES2DestroyContext(context_);
 }
 
+void Context::Resize(GLuint width, GLuint height, GLfloat scale_factor) {
+  static_cast<gpu::gles2::GLES2Interface*>(
+      MojoGLES2GetGLES2Interface(context_))->ResizeCHROMIUM(width,
+                                                            height,
+                                                            scale_factor);
+}
+
 void Context::ContextLost() {
   if (!runner_)
     return;
diff --git a/services/js/modules/gl/context.h b/services/js/modules/gl/context.h
index ac53c78..5395d36 100644
--- a/services/js/modules/gl/context.h
+++ b/services/js/modules/gl/context.h
@@ -2,8 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifndef MOJO_SERVICES_JS_MODULES_GL_CONTEXT_H_
-#define MOJO_SERVICES_JS_MODULES_GL_CONTEXT_H_
+#ifndef SERVICES_JS_MODULES_GL_CONTEXT_H_
+#define SERVICES_JS_MODULES_GL_CONTEXT_H_
 
 #include <GLES2/gl2.h>
 
@@ -51,6 +51,7 @@
   static void VertexAttribPointer(GLuint index, GLint size, GLenum type,
                                   GLboolean normalized, GLsizei stride,
                                   uint64_t offset);
+  void Resize(GLuint width, GLuint height, GLfloat scale_factor);
 
  private:
   gin::ObjectTemplateBuilder GetObjectTemplateBuilder(
@@ -72,4 +73,4 @@
 }  // namespace gl
 }  // namespace js
 
-#endif  // MOJO_SERVICES_JS_MODULES_GL_CONTEXT_H_
+#endif  // SERVICES_JS_MODULES_GL_CONTEXT_H_
diff --git a/services/js/modules/gl/module.cc b/services/js/modules/gl/module.cc
index 663b07d..5642e67 100644
--- a/services/js/modules/gl/module.cc
+++ b/services/js/modules/gl/module.cc
@@ -4,7 +4,6 @@
 
 #include "services/js/modules/gl/module.h"
 
-#include "base/logging.h"
 #include "gin/arguments.h"
 #include "gin/object_template_builder.h"
 #include "gin/per_isolate_data.h"