changeset 1232:a28861abf888 broker

viewports for WebAssembly
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 09 Dec 2019 17:46:33 +0100
parents b9f2a111c5b9
children f621b57c9f37
files Framework/Scene2DViewport/ViewportController.cpp Framework/Scene2DViewport/ViewportController.h Framework/Viewport/WebAssemblyCairoViewport.cpp Framework/Viewport/WebAssemblyCairoViewport.h Framework/Viewport/WebAssemblyViewport.cpp Framework/Viewport/WebAssemblyViewport.h Framework/Viewport/WebGLViewport.cpp Framework/Viewport/WebGLViewport.h Framework/Viewport/WebGLViewportsRegistry.cpp Framework/Viewport/WebGLViewportsRegistry.h Resources/CMake/OrthancStoneConfiguration.cmake
diffstat 11 files changed, 857 insertions(+), 282 deletions(-) [+]
line wrap: on
line diff
--- a/Framework/Scene2DViewport/ViewportController.cpp	Mon Dec 09 14:41:37 2019 +0100
+++ b/Framework/Scene2DViewport/ViewportController.cpp	Mon Dec 09 17:46:33 2019 +0100
@@ -66,13 +66,22 @@
 
   ViewportController::ViewportController() :
     undoStackW_(boost::make_shared<OrthancStone::UndoStack>()),
-    canvasToSceneFactor_(1)
+    canvasToSceneFactor_(1),
+    scene_(new Scene2D)
   {
   }
 
-  ViewportController::ViewportController(boost::weak_ptr<UndoStack> undoStackW)
-    : undoStackW_(undoStackW)
-    , canvasToSceneFactor_(1)
+  ViewportController::ViewportController(const Scene2D& scene) : 
+    undoStackW_(boost::make_shared<OrthancStone::UndoStack>()),
+    canvasToSceneFactor_(1),
+    scene_(scene.Clone())
+  {
+  }
+
+  ViewportController::ViewportController(boost::weak_ptr<UndoStack> undoStackW) :
+    undoStackW_(undoStackW),
+    canvasToSceneFactor_(1),
+    scene_(new Scene2D)
   {
   }
  
@@ -171,27 +180,27 @@
 
   OrthancStone::AffineTransform2D ViewportController::GetCanvasToSceneTransform() const
   {
-    return scene_.GetCanvasToSceneTransform();
+    return scene_->GetCanvasToSceneTransform();
   }
 
   OrthancStone::AffineTransform2D ViewportController::GetSceneToCanvasTransform() const
   {
-    return scene_.GetSceneToCanvasTransform();
+    return scene_->GetSceneToCanvasTransform();
   }
 
   void ViewportController::SetSceneToCanvasTransform(const AffineTransform2D& transform)
   {
-    scene_.SetSceneToCanvasTransform(transform);
+    scene_->SetSceneToCanvasTransform(transform);
 
-    canvasToSceneFactor_ = scene_.GetCanvasToSceneTransform().ComputeZoom();
+    canvasToSceneFactor_ = scene_->GetCanvasToSceneTransform().ComputeZoom();
     BroadcastMessage(SceneTransformChanged(*this));
   }
 
   void ViewportController::FitContent(unsigned int viewportWidth,
                                       unsigned int viewportHeight)
   {
-    scene_.FitContent(viewportWidth, viewportHeight);
-    canvasToSceneFactor_ = scene_.GetCanvasToSceneTransform().ComputeZoom();
+    scene_->FitContent(viewportWidth, viewportHeight);
+    canvasToSceneFactor_ = scene_->GetCanvasToSceneTransform().ComputeZoom();
     BroadcastMessage(SceneTransformChanged(*this));
   }
 
--- a/Framework/Scene2DViewport/ViewportController.h	Mon Dec 09 14:41:37 2019 +0100
+++ b/Framework/Scene2DViewport/ViewportController.h	Mon Dec 09 17:46:33 2019 +0100
@@ -111,6 +111,8 @@
 
     ViewportController();
 
+    ViewportController(const Scene2D& scene /* will be cloned */);
+
     ViewportController(boost::weak_ptr<UndoStack> undoStackW);
 
     ~ViewportController();
@@ -210,12 +212,12 @@
 
     const Scene2D& GetScene() const
     {
-      return scene_;
+      return *scene_;
     }
 
     Scene2D& GetScene()
     {
-      return scene_;
+      return *scene_;
     }
 
     bool HasActiveTracker() const
@@ -230,7 +232,7 @@
     std::vector<boost::shared_ptr<MeasureTool> >  measureTools_;
     boost::shared_ptr<IFlexiblePointerTracker>    activeTracker_;  // TODO - Couldn't this be a "std::auto_ptr"?
 
-    Scene2D   scene_;
+    std::auto_ptr<Scene2D>   scene_;
 
     // this is cached
     double  canvasToSceneFactor_;    
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Viewport/WebAssemblyCairoViewport.cpp	Mon Dec 09 17:46:33 2019 +0100
@@ -0,0 +1,133 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "WebAssemblyCairoViewport.h"
+
+#include "../Scene2D/CairoCompositor.h"
+
+#include <Core/Images/Image.h>
+
+namespace OrthancStone
+{
+  void WebAssemblyCairoViewport::GetCanvasSize(unsigned int& width,
+                                               unsigned int& height)
+  {
+    double w, h;
+    emscripten_get_element_css_size(GetFullCanvasId().c_str(), &w, &h);
+
+    /**
+     * Emscripten has the function emscripten_get_element_css_size()
+     * to query the width and height of a named HTML element. I'm
+     * calling this first to get the initial size of the canvas DOM
+     * element, and then call emscripten_set_canvas_size() to
+     * initialize the framebuffer size of the canvas to the same
+     * size as its DOM element.
+     * https://floooh.github.io/2017/02/22/emsc-html.html
+     **/
+    if (w > 0 &&
+        h > 0)
+    {
+      width = static_cast<unsigned int>(boost::math::iround(w));
+      height = static_cast<unsigned int>(boost::math::iround(h));
+    }
+    else
+    {
+      width = 0;
+      height = 0;
+    }
+  }
+
+
+  void WebAssemblyCairoViewport::Paint(ICompositor& compositor,
+                                       ViewportController& controller)
+  {
+    compositor.Refresh(controller.GetScene());
+
+    // Create a temporary memory buffer for the canvas in JavaScript
+    Orthanc::ImageAccessor cairo;
+    dynamic_cast<CairoCompositor&>(compositor).GetCanvas().GetReadOnlyAccessor(cairo);
+
+    const unsigned int width = cairo.GetWidth();
+    const unsigned int height = cairo.GetHeight();
+
+    if (javascript_.get() == NULL ||
+        javascript_->GetWidth() != width ||
+        javascript_->GetHeight() != height)
+    {
+      javascript_.reset(new Orthanc::Image(Orthanc::PixelFormat_RGBA32, width, height,
+                                           true /* force minimal pitch */));
+    }
+      
+    // Convert from BGRA32 memory layout (only color mode supported
+    // by Cairo, which corresponds to CAIRO_FORMAT_ARGB32) to RGBA32
+    // (as expected by HTML5 canvas). This simply amounts to
+    // swapping the B and R channels. Alpha channel is also set to
+    // full opacity (255).
+    uint8_t* q = reinterpret_cast<uint8_t*>(javascript_->GetBuffer());
+    for (unsigned int y = 0; y < height; y++)
+    {
+      const uint8_t* p = reinterpret_cast<const uint8_t*>(cairo.GetConstRow(y));
+      for (unsigned int x = 0; x < width; x++)
+      {
+        q[0] = p[2];  // R
+        q[1] = p[1];  // G
+        q[2] = p[0];  // B
+        q[3] = 255;   // A
+
+        p += 4;
+        q += 4;
+      }
+    }
+
+    // Execute JavaScript commands to blit the image buffer onto the
+    // 2D drawing context of the HTML5 canvas
+    EM_ASM({
+        const data = new Uint8ClampedArray(Module.HEAP8.buffer, $1, 4 * $2 * $3);
+        const img = new ImageData(data, $2, $3);
+        const ctx = document.getElementById(UTF8ToString($0)).getContext('2d');
+        ctx.putImageData(img, 0, 0);
+      },
+      GetShortCanvasId().c_str(), // $0
+      javascript_->GetBuffer(),   // $1
+      javascript_->GetWidth(),    // $2
+      javascript_->GetHeight());  // $3
+  }
+    
+
+  void WebAssemblyCairoViewport::UpdateSize(ICompositor& compositor)
+  {
+    unsigned int width, height;
+    GetCanvasSize(width, height);
+    emscripten_set_canvas_element_size(GetFullCanvasId().c_str(), width, height);
+
+    dynamic_cast<CairoCompositor&>(compositor).UpdateSize(width, height);
+  }
+
+
+  WebAssemblyCairoViewport::WebAssemblyCairoViewport(const std::string& canvasId) :
+    WebAssemblyViewport(canvasId, NULL)
+  {
+    unsigned int width, height;
+    GetCanvasSize(width, height);
+    emscripten_set_canvas_element_size(GetFullCanvasId().c_str(), width, height);
+    AcquireCompositor(new CairoCompositor(width, height));
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Viewport/WebAssemblyCairoViewport.h	Mon Dec 09 17:46:33 2019 +0100
@@ -0,0 +1,50 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "WebAssemblyViewport.h"
+
+namespace OrthancStone
+{
+  class WebAssemblyCairoViewport : public WebAssemblyViewport
+  {
+  private:
+    std::auto_ptr<Orthanc::ImageAccessor>  javascript_;
+        
+    void GetCanvasSize(unsigned int& width,
+                       unsigned int& height);
+
+  protected:
+    virtual void Paint(ICompositor& compositor,
+                       ViewportController& controller) ORTHANC_OVERRIDE;
+    
+    virtual void UpdateSize(ICompositor& compositor) ORTHANC_OVERRIDE;
+
+  public:
+    WebAssemblyCairoViewport(const std::string& canvasId);
+
+    virtual ~WebAssemblyCairoViewport()
+    {
+      ClearCompositor();
+    }
+  };
+}
--- a/Framework/Viewport/WebAssemblyViewport.cpp	Mon Dec 09 14:41:37 2019 +0100
+++ b/Framework/Viewport/WebAssemblyViewport.cpp	Mon Dec 09 17:46:33 2019 +0100
@@ -21,248 +21,246 @@
 
 #include "WebAssemblyViewport.h"
 
-#include "../StoneException.h"
+#include <Core/OrthancException.h>
 
-#include <emscripten/html5.h>
+#include <boost/make_shared.hpp>
 
 namespace OrthancStone
 {
-  WebAssemblyOpenGLViewport::WebAssemblyOpenGLViewport(const std::string& canvas) :
-    WebAssemblyViewport(canvas),
-    context_(canvas)
+  static void ConvertMouseEvent(PointerEvent& target,
+                                const EmscriptenMouseEvent& source,
+                                const ICompositor& compositor)
   {
-    compositor_.reset(new OpenGLCompositor(context_, GetScene()));
-    RegisterContextCallbacks();
-  }
+    int x = static_cast<int>(source.targetX);
+    int y = static_cast<int>(source.targetY);
 
-  WebAssemblyOpenGLViewport::WebAssemblyOpenGLViewport(const std::string& canvas,
-    boost::shared_ptr<Scene2D>& scene) :
-    WebAssemblyViewport(canvas, scene),
-    context_(canvas)
-  {
-    compositor_.reset(new OpenGLCompositor(context_, GetScene()));
-    RegisterContextCallbacks();
-  }
-
-  void WebAssemblyOpenGLViewport::UpdateSize()
-  {
-    context_.UpdateSize();  // First read the size of the canvas
-
-    if (compositor_.get() != NULL)
+    switch (source.button)
     {
-      compositor_->Refresh();  // Then refresh the content of the canvas
-    }
-  }
+      case 0:
+        target.SetMouseButton(MouseButton_Left);
+        break;
 
-  /*
-  typedef EM_BOOL (*em_webgl_context_callback)(int eventType, const void *reserved, void *userData);
-
-  EMSCRIPTEN_EVENT_WEBGLCONTEXTLOST EMSCRIPTEN_EVENT_WEBGLCONTEXTRESTORED
-
-  EMSCRIPTEN_RESULT emscripten_set_webglcontextlost_callback(
-    const char *target, void *userData, EM_BOOL useCapture, em_webgl_context_callback callback)
+      case 1:
+        target.SetMouseButton(MouseButton_Middle);
+        break;
 
-  EMSCRIPTEN_RESULT emscripten_set_webglcontextrestored_callback(
-    const char *target, void *userData, EM_BOOL useCapture, em_webgl_context_callback callback)
-
-  */
+      case 2:
+        target.SetMouseButton(MouseButton_Right);
+        break;
 
-  EM_BOOL WebAssemblyOpenGLViewport_OpenGLContextLost_callback(
-    int eventType, const void* reserved, void* userData)
-  {
-    ORTHANC_ASSERT(eventType == EMSCRIPTEN_EVENT_WEBGLCONTEXTLOST);
-    WebAssemblyOpenGLViewport* viewport = reinterpret_cast<WebAssemblyOpenGLViewport*>(userData);
-    return viewport->OpenGLContextLost();
+      default:
+        target.SetMouseButton(MouseButton_None);
+        break;
+    }
+      
+    target.AddPosition(compositor.GetPixelCenterCoordinates(x, y));
+    target.SetAltModifier(source.altKey);
+    target.SetControlModifier(source.ctrlKey);
+    target.SetShiftModifier(source.shiftKey);
   }
 
-  EM_BOOL WebAssemblyOpenGLViewport_OpenGLContextRestored_callback(
-    int eventType, const void* reserved, void* userData)
-  {
-    ORTHANC_ASSERT(eventType == EMSCRIPTEN_EVENT_WEBGLCONTEXTRESTORED);
-    WebAssemblyOpenGLViewport* viewport = reinterpret_cast<WebAssemblyOpenGLViewport*>(userData);
-    return viewport->OpenGLContextRestored();
-  }
 
-  void WebAssemblyOpenGLViewport::DisableCompositor()
-  {
-    compositor_.reset();
-  }
-
-  ICompositor& WebAssemblyOpenGLViewport::GetCompositor()
+  class WebAssemblyViewport::WasmLock : public ILock
   {
-    if (compositor_.get() == NULL)
-    {
-      // "HasCompositor()" should have been called
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      return *compositor_;
-    }
-  }
+  private:
+    WebAssemblyViewport& that_;
 
-  void WebAssemblyOpenGLViewport::Refresh()
-  {
-    try
+  public:
+    WasmLock(WebAssemblyViewport& that) :
+      that_(that)
     {
-      if (HasCompositor())
-      {
-        GetCompositor().Refresh();
-      }
-      else
-      {
-        // this block was added because of (perceived?) bugs in the 
-        // browser where the WebGL contexts are NOT automatically restored 
-        // after being lost. 
-        // the WebGL context has been lost. Sce 
-
-        //LOG(ERROR) << "About to call WebAssemblyOpenGLContext::TryRecreate().";
-        //LOG(ERROR) << "Before calling it, isContextLost == " << context_.IsContextLost();
+    }
 
-        if (!context_.IsContextLost())
-        {
-          LOG(TRACE) << "Context restored!";
-          //LOG(ERROR) << "After calling it, isContextLost == " << context_.IsContextLost();
-          RestoreCompositor();
-          UpdateSize();
-        }
-      }
+    virtual bool HasCompositor() const ORTHANC_OVERRIDE
+    {
+      return that_.compositor_.get() != NULL;
     }
-    catch (const StoneException& e)
+
+    virtual ICompositor& GetCompositor() ORTHANC_OVERRIDE
     {
-      if (e.GetErrorCode() == ErrorCode_WebGLContextLost)
+      if (that_.compositor_.get() == NULL)
       {
-        LOG(WARNING) << "Context is lost! Compositor will be disabled.";
-        DisableCompositor();
-        // we now need to wait for the "context restored" callback
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
       }
       else
       {
-        throw;
+        return *that_.compositor_;
       }
     }
-    catch (...)
+
+    virtual ViewportController& GetController() ORTHANC_OVERRIDE
+    {
+      assert(that_.controller_);
+      return *that_.controller_;
+    }
+
+    virtual void Invalidate() ORTHANC_OVERRIDE
     {
-      // something else nasty happened
-      throw;
+      that_.Invalidate();
     }
+  };
+
+
+  EM_BOOL WebAssemblyViewport::OnRequestAnimationFrame(double time, void *userData)
+  {
+    WebAssemblyViewport& that = *reinterpret_cast<WebAssemblyViewport*>(userData);
+
+    if (that.compositor_.get() != NULL &&
+        that.controller_ /* should always be true */)
+    {
+      that.Paint(*that.compositor_, *that.controller_);
+    }
+      
+    return true;
   }
 
-  void WebAssemblyOpenGLViewport::RestoreCompositor()
+    
+  EM_BOOL WebAssemblyViewport::OnResize(int eventType, const EmscriptenUiEvent *uiEvent, void *userData)
   {
-    // the context must have been restored!
-    ORTHANC_ASSERT(!context_.IsContextLost());
-    if (compositor_.get() == NULL)
+    WebAssemblyViewport& that = *reinterpret_cast<WebAssemblyViewport*>(userData);
+
+    if (that.compositor_.get() != NULL)
     {
-      compositor_.reset(new OpenGLCompositor(context_, GetScene()));
+      that.UpdateSize(*that.compositor_);
+      that.Invalidate();
     }
-    else
-    {
-      LOG(WARNING) << "RestoreCompositor() called for \"" << GetCanvasIdentifier() << "\" while it was NOT lost! Nothing done.";
-    }
+      
+    return true;
   }
 
-  bool WebAssemblyOpenGLViewport::OpenGLContextLost()
+
+  EM_BOOL WebAssemblyViewport::OnMouseDown(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData)
   {
-    LOG(ERROR) << "WebAssemblyOpenGLViewport::OpenGLContextLost() for canvas: " << GetCanvasIdentifier();
-    DisableCompositor();
+    WebAssemblyViewport& that = *reinterpret_cast<WebAssemblyViewport*>(userData);
+
+    LOG(INFO) << "mouse down: " << that.GetFullCanvasId();      
+
+    if (that.compositor_.get() != NULL &&
+        that.interactor_.get() != NULL)
+    {
+      PointerEvent pointer;
+      ConvertMouseEvent(pointer, *mouseEvent, *that.compositor_);
+
+      that.controller_->HandleMousePress(*that.interactor_, pointer,
+                                         that.compositor_->GetCanvasWidth(),
+                                         that.compositor_->GetCanvasHeight());        
+      that.Invalidate();
+    }
+
+    return true;
+  }
+
+    
+  EM_BOOL WebAssemblyViewport::OnMouseMove(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData)
+  {
+    WebAssemblyViewport& that = *reinterpret_cast<WebAssemblyViewport*>(userData);
+
+    if (that.compositor_.get() != NULL &&
+        that.controller_->HasActiveTracker())
+    {
+      PointerEvent pointer;
+      ConvertMouseEvent(pointer, *mouseEvent, *that.compositor_);
+      that.controller_->HandleMouseMove(pointer);        
+      that.Invalidate();
+    }
+
     return true;
   }
 
-  bool WebAssemblyOpenGLViewport::OpenGLContextRestored()
+    
+  EM_BOOL WebAssemblyViewport::OnMouseUp(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData)
   {
-    LOG(ERROR) << "WebAssemblyOpenGLViewport::OpenGLContextRestored() for canvas: " << GetCanvasIdentifier();
-    
-    // maybe the context has already been restored by other means (the 
-    // Refresh() function)
-    if (!HasCompositor())
+    WebAssemblyViewport& that = *reinterpret_cast<WebAssemblyViewport*>(userData);
+
+    if (that.compositor_.get() != NULL)
     {
-      RestoreCompositor();
-      UpdateSize();
+      PointerEvent pointer;
+      ConvertMouseEvent(pointer, *mouseEvent, *that.compositor_);
+      that.controller_->HandleMouseRelease(pointer);
+      that.Invalidate();
     }
-    return false;
+
+    return true;
   }
 
-  void WebAssemblyOpenGLViewport::RegisterContextCallbacks()
+    
+  void WebAssemblyViewport::Invalidate()
   {
-#if 0
-    // DISABLED ON 2019-08-20 and replaced by external JS calls because I could
-    // not get emscripten API to work
-    // TODO: what's the impact of userCapture=true ?
-    const char* canvasId = GetCanvasIdentifier().c_str();
-    void* that = reinterpret_cast<void*>(this);
-    EMSCRIPTEN_RESULT status = EMSCRIPTEN_RESULT_SUCCESS;
+    emscripten_request_animation_frame(OnRequestAnimationFrame, this);
+  }
+    
 
-    //status = emscripten_set_webglcontextlost_callback(canvasId, that, true, WebAssemblyOpenGLViewport_OpenGLContextLost_callback);
-    //if (status != EMSCRIPTEN_RESULT_SUCCESS)
-    //{
-    //  std::stringstream ss;
-    //  ss << "Error while calling emscripten_set_webglcontextlost_callback for: \"" << GetCanvasIdentifier() << "\"";
-    //  std::string msg = ss.str();
-    //  LOG(ERROR) << msg;
-    //  ORTHANC_ASSERT(false, msg.c_str());
-    //}
-
-    status = emscripten_set_webglcontextrestored_callback(canvasId, that, true, WebAssemblyOpenGLViewport_OpenGLContextRestored_callback);
-    if (status != EMSCRIPTEN_RESULT_SUCCESS)
+  void WebAssemblyViewport::AcquireCompositor(ICompositor* compositor /* takes ownership */)
+  {
+    if (compositor == NULL)
     {
-      std::stringstream ss;
-      ss << "Error while calling emscripten_set_webglcontextrestored_callback for: \"" << GetCanvasIdentifier() << "\"";
-      std::string msg = ss.str();
-      LOG(ERROR) << msg;
-      ORTHANC_ASSERT(false, msg.c_str());
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
     }
-    LOG(TRACE) << "WebAssemblyOpenGLViewport::RegisterContextCallbacks() SUCCESS!!!";
-#endif
+    else
+    {
+      compositor_.reset(compositor);
+    }
   }
 
-  WebAssemblyCairoViewport::WebAssemblyCairoViewport(const std::string& canvas) :
-    WebAssemblyViewport(canvas),
-    canvas_(canvas),
-    compositor_(GetScene(), 1024, 768)
+
+  WebAssemblyViewport::WebAssemblyViewport(const std::string& canvasId,
+                                           const Scene2D* scene) :
+    shortCanvasId_(canvasId),
+    fullCanvasId_("#" + canvasId),
+    interactor_(new DefaultViewportInteractor)
   {
-  }
+    if (scene == NULL)
+    {
+      controller_ = boost::make_shared<ViewportController>();
+    }
+    else
+    {
+      controller_ = boost::make_shared<ViewportController>(*scene);
+    }
+
+    LOG(INFO) << "Initializing Stone viewport on HTML canvas: " << canvasId;
 
-  WebAssemblyCairoViewport::WebAssemblyCairoViewport(const std::string& canvas,
-    boost::shared_ptr<Scene2D>& scene) :
-    WebAssemblyViewport(canvas, scene),
-    canvas_(canvas),
-    compositor_(GetScene(), 1024, 768)
-  {
+    if (canvasId.empty() ||
+        canvasId[0] == '#')
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
+                                      "The canvas identifier must not start with '#'");
+    }
+
+    // Disable right-click on the canvas (i.e. context menu)
+    EM_ASM({
+        document.getElementById(UTF8ToString($0)).oncontextmenu = function(event) {
+          event.preventDefault();
+        }
+      },
+      canvasId.c_str()   // $0
+      );
+
+    // It is not possible to monitor the resizing of individual
+    // canvas, so we track the full window of the browser
+    emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, this, false, OnResize);
+
+    emscripten_set_mousedown_callback(fullCanvasId_.c_str(), this, false, OnMouseDown);
+    emscripten_set_mousemove_callback(fullCanvasId_.c_str(), this, false, OnMouseMove);
+    emscripten_set_mouseup_callback(fullCanvasId_.c_str(), this, false, OnMouseUp);
   }
 
-  void WebAssemblyCairoViewport::UpdateSize()
+  
+  IViewport::ILock* WebAssemblyViewport::Lock()
   {
-    LOG(INFO) << "updating cairo viewport size";
-    double w, h;
-    emscripten_get_element_css_size(canvas_.c_str(), &w, &h);
-
-    /**
-     * Emscripten has the function emscripten_get_element_css_size()
-     * to query the width and height of a named HTML element. I'm
-     * calling this first to get the initial size of the canvas DOM
-     * element, and then call emscripten_set_canvas_size() to
-     * initialize the framebuffer size of the canvas to the same
-     * size as its DOM element.
-     * https://floooh.github.io/2017/02/22/emsc-html.html
-     **/
-    unsigned int canvasWidth = 0;
-    unsigned int canvasHeight = 0;
-
-    if (w > 0 ||
-      h > 0)
-    {
-      canvasWidth = static_cast<unsigned int>(boost::math::iround(w));
-      canvasHeight = static_cast<unsigned int>(boost::math::iround(h));
-    }
-
-    emscripten_set_canvas_element_size(canvas_.c_str(), canvasWidth, canvasHeight);
-    compositor_.UpdateSize(canvasWidth, canvasHeight);
+    return new WasmLock(*this);
   }
 
-  void WebAssemblyCairoViewport::Refresh()
+  
+  void WebAssemblyViewport::AcquireInteractor(IViewportInteractor* interactor)
   {
-    LOG(INFO) << "refreshing cairo viewport, TODO: blit to the canvans.getContext('2d')";
-    GetCompositor().Refresh();
+    if (interactor == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+    else
+    {
+      interactor_.reset(interactor);
+    }
   }
 }
--- a/Framework/Viewport/WebAssemblyViewport.h	Mon Dec 09 14:41:37 2019 +0100
+++ b/Framework/Viewport/WebAssemblyViewport.h	Mon Dec 09 17:46:33 2019 +0100
@@ -13,7 +13,7 @@
  * WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  * Affero General Public License for more details.
- * 
+ *
  * You should have received a copy of the GNU Affero General Public License
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  **/
@@ -21,106 +21,78 @@
 
 #pragma once
 
-#include "../OpenGL/WebAssemblyOpenGLContext.h"
-#include "../Scene2D/OpenGLCompositor.h"
-#include "../Scene2D/CairoCompositor.h"
-#include "ViewportBase.h"
+#if !defined(ORTHANC_ENABLE_WASM)
+#  error Macro ORTHANC_ENABLE_WASM must be defined
+#endif
+
+#if ORTHANC_ENABLE_WASM != 1
+#  error This file can only be used if targeting WebAssembly
+#endif
+
+#include "IViewport.h"
+
+#include <emscripten.h>
+#include <emscripten/html5.h>
 
 namespace OrthancStone
 {
-  class WebAssemblyViewport : public ViewportBase
+  class WebAssemblyViewport : public IViewport
   {
   private:
-    std::string  canvasIdentifier_;
+    class WasmLock;
+    
+    std::string                            shortCanvasId_;
+    std::string                            fullCanvasId_;
+    std::auto_ptr<ICompositor>             compositor_;
+    boost::shared_ptr<ViewportController>  controller_;
+    std::auto_ptr<IViewportInteractor>     interactor_;
+
+    static EM_BOOL OnRequestAnimationFrame(double time, void *userData);
+    
+    static EM_BOOL OnResize(int eventType, const EmscriptenUiEvent *uiEvent, void *userData);
 
-  public:
-    WebAssemblyViewport(const std::string& canvasIdentifier) :
-      canvasIdentifier_(canvasIdentifier)
+    static EM_BOOL OnMouseDown(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData);
+    
+    static EM_BOOL OnMouseMove(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData);
+    
+    static EM_BOOL OnMouseUp(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData);
+
+  protected:
+    void Invalidate();
+    
+    void ClearCompositor()
     {
+      compositor_.reset();
     }
 
-    WebAssemblyViewport(const std::string& canvasIdentifier,
-                        boost::shared_ptr<Scene2D>& scene) :
-      ViewportBase(scene),
-      canvasIdentifier_(canvasIdentifier)
+    bool HasCompositor() const
     {
+      return compositor_.get() != NULL;
     }
 
-    const std::string& GetCanvasIdentifier() const
-    {
-      return canvasIdentifier_;
-    }
-  };
+    void AcquireCompositor(ICompositor* compositor /* takes ownership */);
 
+    virtual void Paint(ICompositor& compositor,
+                       ViewportController& controller) = 0;
 
-  class WebAssemblyOpenGLViewport : public WebAssemblyViewport
-  {
-  private:
-    OpenGL::WebAssemblyOpenGLContext  context_;
-    std::auto_ptr<OpenGLCompositor>   compositor_;
+    virtual void UpdateSize(ICompositor& compositor) = 0;
 
   public:
-    WebAssemblyOpenGLViewport(const std::string& canvas);
-    
-    WebAssemblyOpenGLViewport(const std::string& canvas,
-                              boost::shared_ptr<Scene2D>& scene);
-    
-    // This function must be called each time the browser window is resized
-    void UpdateSize();
+    WebAssemblyViewport(const std::string& canvasId,
+                        const Scene2D* scene);
+
+    virtual ILock* Lock() ORTHANC_OVERRIDE;
 
-    virtual bool HasCompositor() const ORTHANC_OVERRIDE
+    void AcquireInteractor(IViewportInteractor* interactor);
+
+    const std::string& GetShortCanvasId() const
     {
-      return (compositor_.get() != NULL);
-    }
-    
-    bool IsContextLost()
-    {
-      return context_.IsContextLost();
+      return shortCanvasId_;
     }
 
-    virtual ICompositor& GetCompositor() ORTHANC_OVERRIDE;
-
-    virtual void Refresh() ORTHANC_OVERRIDE;
-
-    // this does NOT return whether the context is lost! This is called to 
-    // tell Stone that the context has been lost
-    bool OpenGLContextLost();
-
-    // This should be called to indicate that the context has been lost
-    bool OpenGLContextRestored();
-
-  private:
-    void DisableCompositor();
-    void RestoreCompositor();
-
-    void RegisterContextCallbacks();
-  };
-
-
-  class WebAssemblyCairoViewport : public WebAssemblyViewport
-  {
-  private:
-    CairoCompositor                  compositor_;
-    std::string                      canvas_;
-
-  public:
-    WebAssemblyCairoViewport(const std::string& canvas);
-    
-    WebAssemblyCairoViewport(const std::string& canvas,
-                             boost::shared_ptr<Scene2D>& scene);
-    
-    void UpdateSize(); 
-
-    virtual void Refresh() ORTHANC_OVERRIDE;
-
-    virtual bool HasCompositor() const ORTHANC_OVERRIDE
+    const std::string& GetFullCanvasId() const
     {
-      return true;
-    }
-    
-    virtual ICompositor& GetCompositor() ORTHANC_OVERRIDE
-    {
-      return compositor_;
+      return fullCanvasId_;
     }
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Viewport/WebGLViewport.cpp	Mon Dec 09 17:46:33 2019 +0100
@@ -0,0 +1,105 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "WebGLViewport.h"
+
+#include "../StoneException.h"
+#include "../Scene2D/OpenGLCompositor.h"
+
+namespace OrthancStone
+{
+  void WebGLViewport::Paint(ICompositor& compositor,
+                            ViewportController& controller)
+  {
+    try
+    {
+      compositor.Refresh(controller.GetScene());
+
+      /**
+       * No need to manually swap the buffer: "Rendered WebGL content
+       * is implicitly presented (displayed to the user) on the canvas
+       * when the event handler that renders with WebGL returns back
+       * to the browser event loop."
+       * https://emscripten.org/docs/api_reference/html5.h.html#webgl-context
+       *
+       * Could call "emscripten_webgl_commit_frame()" if
+       * "explicitSwapControl" option were set to "true".
+       **/
+    }
+    catch (const StoneException& e)
+    {
+      // Ignore problems about the loss of the WebGL context (edge case)
+      if (e.GetErrorCode() == ErrorCode_WebGLContextLost)
+      {
+        return;
+      }
+      else
+      {
+        throw;
+      }
+    }
+  }
+    
+
+  void WebGLViewport::UpdateSize(ICompositor& compositor)
+  {
+    try
+    {
+      context_.UpdateSize();
+    }
+    catch (const StoneException& e)
+    {
+      // Ignore problems about the loss of the WebGL context (edge case)
+      if (e.GetErrorCode() == ErrorCode_WebGLContextLost)
+      {
+        return;
+      }
+      else
+      {
+        throw;
+      }
+    }
+  }
+
+
+  WebGLViewport::WebGLViewport(const std::string& canvasId) :
+    WebAssemblyViewport(canvasId, NULL),
+    context_(GetFullCanvasId())
+  {
+    AcquireCompositor(new OpenGLCompositor(context_));
+  }
+
+  
+  WebGLViewport::WebGLViewport(const std::string& canvasId,
+                               const Scene2D& scene) :
+    WebAssemblyViewport(canvasId, &scene),
+    context_(GetFullCanvasId())
+  {
+    AcquireCompositor(new OpenGLCompositor(context_));
+  }
+
+  
+  WebGLViewport::~WebGLViewport()
+  {
+    // Make sure to delete the compositor before its parent "context_" gets deleted
+    ClearCompositor();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Viewport/WebGLViewport.h	Mon Dec 09 17:46:33 2019 +0100
@@ -0,0 +1,53 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "WebAssemblyViewport.h"
+#include "../OpenGL/WebAssemblyOpenGLContext.h"
+
+namespace OrthancStone
+{
+  class WebGLViewport : public WebAssemblyViewport
+  {
+  private:
+    OpenGL::WebAssemblyOpenGLContext  context_;
+
+  protected:
+    virtual void Paint(ICompositor& compositor,
+                       ViewportController& controller) ORTHANC_OVERRIDE;
+    
+    virtual void UpdateSize(ICompositor& compositor) ORTHANC_OVERRIDE;
+
+  public:
+    WebGLViewport(const std::string& canvasId);
+
+    WebGLViewport(const std::string& canvasId,
+                  const Scene2D& scene);
+
+    virtual ~WebGLViewport();
+
+    bool IsContextLost()
+    {
+      return context_.IsContextLost();
+    } 
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Viewport/WebGLViewportsRegistry.cpp	Mon Dec 09 17:46:33 2019 +0100
@@ -0,0 +1,173 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "WebGLViewportsRegistry.h"
+
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  void WebGLViewportsRegistry::LaunchTimer()
+  {
+    emscripten_set_timeout(OnTimeoutCallback, 1000.0 * static_cast<double>(timeoutSeconds_), this);
+  }
+
+  
+  void WebGLViewportsRegistry::OnTimeout()
+  {
+    for (Viewports::iterator it = viewports_.begin(); it != viewports_.end(); ++it)
+    {
+      if (it->second == NULL ||
+          it->second->IsContextLost())
+      {
+        LOG(INFO) << "WebGL context lost for canvas: " << it->first;
+
+        // Try and duplicate the HTML5 canvas in the DOM
+        EM_ASM({
+            var canvas = document.getElementById(UTF8ToString($0));
+            if (canvas) {
+              var parent = canvas.parentElement;
+              if (parent) {
+                var cloned = canvas.cloneNode(true /* deep copy */);
+                parent.insertBefore(cloned, canvas);
+                parent.removeChild(canvas);
+              }
+            }
+          },
+          it->first.c_str()  // $0 = ID of the canvas
+          );
+
+        // At this point, the old canvas is removed from the DOM and
+        // replaced by a fresh one with the same ID: Recreate the
+        // WebGL context on the new canvas
+        std::auto_ptr<WebGLViewport> viewport;
+          
+        {
+          std::auto_ptr<IViewport::ILock> lock(it->second->Lock());
+          viewport.reset(new WebGLViewport(it->first, lock->GetController().GetScene()));
+        }
+
+        // Replace the old WebGL viewport by the new one
+        delete it->second;
+        it->second = viewport.release();
+
+        // Tag the fresh canvas as needing a repaint
+        {
+          std::auto_ptr<IViewport::ILock> lock(it->second->Lock());
+          lock->Invalidate();
+        }
+      }
+    }
+      
+    LaunchTimer();
+  }
+
+    
+  void WebGLViewportsRegistry::OnTimeoutCallback(void *userData)
+  {
+    WebGLViewportsRegistry& that = *reinterpret_cast<WebGLViewportsRegistry*>(userData);
+    that.OnTimeout();
+  }
+
+    
+  WebGLViewportsRegistry::WebGLViewportsRegistry(unsigned int timeoutSeconds) :
+    timeoutSeconds_(timeoutSeconds)
+  {
+    if (timeoutSeconds <= 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }          
+
+    LaunchTimer();
+  }
+
+    
+  WebGLViewportsRegistry::~WebGLViewportsRegistry()
+  {
+    for (Viewports::iterator it = viewports_.begin(); it != viewports_.end(); ++it)
+    {
+      if (it->second != NULL)
+      {
+        delete it->second;
+      }
+    }
+  }
+
+    
+  void WebGLViewportsRegistry::Add(const std::string& canvasId)
+  {
+    if (viewports_.find(canvasId) != viewports_.end())
+    {
+      LOG(ERROR) << "Canvas was already registered: " << canvasId;
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      viewports_[canvasId] = new WebGLViewport(canvasId);
+    }
+  }
+
+    
+  void WebGLViewportsRegistry::Remove(const std::string& canvasId)
+  {
+    Viewports::iterator found = viewports_.find(canvasId);
+
+    if (found == viewports_.end())
+    {
+      LOG(ERROR) << "Cannot remove unregistered canvas: " << canvasId;
+    }
+    else
+    {
+      if (found->second != NULL)
+      {
+        delete found->second;
+      }
+
+      viewports_.erase(found);
+    }
+  }
+
+    
+  WebGLViewportsRegistry::Accessor::Accessor(WebGLViewportsRegistry& that,
+                                             const std::string& canvasId) :
+    that_(that)
+  {
+    Viewports::iterator viewport = that.viewports_.find(canvasId);
+    if (viewport != that.viewports_.end() &&
+        viewport->second != NULL)
+    {
+      lock_.reset(viewport->second->Lock());
+    }
+  }
+
+
+  IViewport::ILock& WebGLViewportsRegistry::Accessor::GetViewport() const
+  {
+    if (IsValid())
+    {
+      return *lock_;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Viewport/WebGLViewportsRegistry.h	Mon Dec 09 17:46:33 2019 +0100
@@ -0,0 +1,77 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "WebGLViewport.h"
+
+namespace OrthancStone
+{
+  /**
+   * This singleton class must be used if many WebGL viewports are
+   * created by the higher-level application, implying possible loss
+   * of WebGL contexts. The object will run an infinite update loop
+   * that checks whether all the WebGL context are still valid (not
+   * lost). If some WebGL context is lost, it is automatically
+   * reinitialized by created a fresh HTML5 canvas.
+   **/  
+  class WebGLViewportsRegistry : public boost::noncopyable
+  {
+  private:
+    typedef std::map<std::string, WebGLViewport*>  Viewports;
+
+    unsigned int  timeoutSeconds_;
+    Viewports     viewports_;
+
+    void LaunchTimer();
+
+    void OnTimeout();
+
+    static void OnTimeoutCallback(void *userData);
+    
+  public:
+    WebGLViewportsRegistry(unsigned int timeoutSeconds);
+    
+    ~WebGLViewportsRegistry();
+
+    void Add(const std::string& canvasId);
+
+    void Remove(const std::string& canvasId);
+
+    class Accessor : public boost::noncopyable
+    {
+    private:
+      WebGLViewportsRegistry&          that_;
+      std::auto_ptr<IViewport::ILock>  lock_;
+
+    public:
+      Accessor(WebGLViewportsRegistry& that,
+               const std::string& canvasId);
+
+      bool IsValid() const
+      {
+        return lock_.get() != NULL;
+      }
+
+      IViewport::ILock& GetViewport() const;
+    };
+  };
+}
--- a/Resources/CMake/OrthancStoneConfiguration.cmake	Mon Dec 09 14:41:37 2019 +0100
+++ b/Resources/CMake/OrthancStoneConfiguration.cmake	Mon Dec 09 17:46:33 2019 +0100
@@ -445,6 +445,9 @@
 if (ENABLE_WASM)
   list(APPEND ORTHANC_STONE_SOURCES
     ${ORTHANC_STONE_ROOT}/Framework/Oracle/WebAssemblyOracle.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Viewport/WebAssemblyCairoViewport.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Viewport/WebAssemblyViewport.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Viewport/WebAssemblyViewport.h
     )
 endif()
 
@@ -694,10 +697,10 @@
 
   if (ENABLE_WASM)
     list(APPEND ORTHANC_STONE_SOURCES
+      ${ORTHANC_STONE_ROOT}/Framework/OpenGL/WebAssemblyOpenGLContext.cpp
       ${ORTHANC_STONE_ROOT}/Framework/OpenGL/WebAssemblyOpenGLContext.h
-      ${ORTHANC_STONE_ROOT}/Framework/OpenGL/WebAssemblyOpenGLContext.cpp
-      ${ORTHANC_STONE_ROOT}/Framework/Viewport/WebAssemblyViewport.h
-      ${ORTHANC_STONE_ROOT}/Framework/Viewport/WebAssemblyViewport.cpp
+      ${ORTHANC_STONE_ROOT}/Framework/Viewport/WebGLViewport.cpp
+      ${ORTHANC_STONE_ROOT}/Framework/Viewport/WebGLViewportsRegistry.cpp
       )
   endif()
 endif()