1<!DOCTYPE html>
2<title>CanvasKit Viewer (Skia via Web Assembly)</title>
3<meta charset="utf-8" />
4<meta http-equiv="X-UA-Compatible" content="IE=edge">
5<meta name="viewport" content="width=device-width, initial-scale=1.0">
6<style>
7  html, body {
8    margin: 0;
9    padding: 0;
10  }
11</style>
12
13<canvas id=viewer_canvas></canvas>
14
15<script type="text/javascript" src="/node_modules/canvaskit/bin/canvaskit.js"></script>
16
17<script type="text/javascript" charset="utf-8">
18  const flags = {};
19  for (const pair of location.hash.substring(1).split(',')) {
20    // Parse "values" as an array in case the value has a colon (e.g., "slide:http://...").
21    const [key, ...values] = pair.split(':');
22    flags[key] = values.join(':');
23  }
24  window.onhashchange = function() {
25    location.reload();
26  };
27
28  CanvasKitInit({
29    locateFile: (file) => '/node_modules/canvaskit/bin/'+file,
30  }).then((CK) => {
31    if (!CK) {
32      throw 'CanvasKit not available.';
33    }
34    LoadSlide(CK);
35  });
36
37  function LoadSlide(CanvasKit) {
38    if (!CanvasKit.MakeSlide || !CanvasKit.MakeSkpSlide || !CanvasKit.MakeSvgSlide) {
39      throw 'Not compiled with Viewer.';
40    }
41    const slideName = flags.slide || 'PathText';
42    if (slideName.endsWith('.skp') || slideName.endsWith('.svg')) {
43      fetch(slideName).then(function(response) {
44        if (response.status != 200) {
45            throw 'Error fetching ' + slideName;
46        }
47        if (slideName.endsWith('.skp')) {
48          response.arrayBuffer().then((data) => ViewerMain(
49              CanvasKit, CanvasKit.MakeSkpSlide(slideName, data)));
50        } else {
51          response.text().then((text) => ViewerMain(
52              CanvasKit, CanvasKit.MakeSvgSlide(slideName, text)));
53        }
54      });
55    } else {
56      ViewerMain(CanvasKit, CanvasKit.MakeSlide(slideName));
57    }
58  }
59
60  function ViewerMain(CanvasKit, slide) {
61    if (!slide) {
62      throw 'Failed to parse slide.'
63    }
64    const width = window.innerWidth;
65    const height = window.innerHeight;
66    const htmlCanvas = document.getElementById('viewer_canvas');
67    htmlCanvas.width = width;
68    htmlCanvas.height = height;
69    slide.load(width, height);
70
71    // For the msaa flag, only check if the key exists in flags. That way we don't need to assign it
72    // a value in the location hash. i.e.,:  http://.../viewer.html#msaa
73    const doMSAA = ('msaa' in flags);
74    // Create the WebGL context with our desired attribs before calling MakeWebGLCanvasSurface.
75    CanvasKit.GetWebGLContext(htmlCanvas, {antialias: doMSAA});
76    const surface = CanvasKit.MakeWebGLCanvasSurface(htmlCanvas, null);
77    if (!surface) {
78      throw 'Could not make canvas surface';
79    }
80    if (doMSAA && surface.sampleCnt() <= 1) {
81      // We requested antialias on the canvas but did not get MSAA. Since we don't know what type of
82      // AA is in use right now (if any), this surface is unusable.
83      throw 'MSAA rendering to the on-screen canvas is not supported. ' +
84            'Please try again without MSAA.';
85    }
86
87    window.onmousedown = (event) => (event.button === 0) && Mouse(CanvasKit.InputState.Down, event);
88    window.onmouseup = (event) => (event.button === 0) && Mouse(CanvasKit.InputState.Up, event);
89    window.onmousemove = (event) => Mouse(CanvasKit.InputState.Move, event);
90    window.onkeypress = function(event) {
91      if (slide.onChar(event.keyCode)) {
92        ScheduleDraw();
93        return false;
94      } else {
95        switch (event.keyCode) {
96          case 's'.charCodeAt(0):
97            // 's' is the magic key in the native viewer app that turns on FPS monitoring. Toggle
98            // forced animation when it is pressed in order to get fps logs.
99            // HINT: Launch chrome with --disable-frame-rate-limit and --disable-gpu-vsync in order
100            // to measure frame rates above 60.
101            ScheduleDraw.forceAnimation = !ScheduleDraw.forceAnimation;
102            ScheduleDraw();
103            break;
104        }
105      }
106      return true;
107    }
108    window.onkeydown = function(event) {
109      const upArrowCode = 38;
110      if (event.keyCode === upArrowCode) {
111        ScaleCanvas((event.shiftKey) ? Infinity : 1.1);
112        return false;
113      }
114      const downArrowCode = 40;
115      if (event.keyCode === downArrowCode) {
116        ScaleCanvas((event.shiftKey) ? 0 : 1/1.1);
117        return false;
118      }
119      return true;
120    }
121
122    let [canvasScale, canvasTranslateX, canvasTranslateY] = [1, 0, 0];
123    function ScaleCanvas(factor) {
124      factor = Math.min(Math.max(1/(5*canvasScale), factor), 5/canvasScale);
125      canvasTranslateX *= factor;
126      canvasTranslateY *= factor;
127      canvasScale *= factor;
128      ScheduleDraw();
129    }
130    function TranslateCanvas(dx, dy) {
131      canvasTranslateX += dx;
132      canvasTranslateY += dy;
133      ScheduleDraw();
134    }
135
136    function Mouse(state, event) {
137      let modifierKeys = CanvasKit.ModifierKey.None;
138      if (event.shiftKey) {
139        modifierKeys |= CanvasKit.ModifierKey.Shift;
140      }
141      if (event.altKey) {
142        modifierKeys |= CanvasKit.ModifierKey.Option;
143      }
144      if (event.ctrlKey) {
145        modifierKeys |= CanvasKit.ModifierKey.Ctrl;
146      }
147      if (event.metaKey) {
148        modifierKeys |= CanvasKit.ModifierKey.Command;
149      }
150      let [dx, dy] = [event.pageX - this.lastX, event.pageY - this.lastY];
151      this.lastX = event.pageX;
152      this.lastY = event.pageY;
153      if (slide.onMouse(event.pageX, event.pageY, state, modifierKeys)) {
154        ScheduleDraw();
155        return false;
156      } else if (event.buttons & 1) {  // Left-button pressed.
157        TranslateCanvas(dx, dy);
158        return false;
159      }
160      return true;
161    }
162
163    function ScheduleDraw() {
164      if (ScheduleDraw.hasPendingAnimationRequest) {
165        // It's possible for this ScheduleDraw() method to be called multiple times before an
166        // animation callback actually gets invoked. Make sure we only ever have one single
167        // requestAnimationFrame scheduled at a time, because otherwise we can get stuck in a
168        // position where multiple callbacks are coming in on a single compositing frame, and then
169        // rescheduling multiple more for the next frame.
170        return;
171      }
172      ScheduleDraw.hasPendingAnimationRequest = true;
173      surface.requestAnimationFrame((canvas) => {
174        ScheduleDraw.hasPendingAnimationRequest = false;
175
176        canvas.save();
177        canvas.translate(canvasTranslateX, canvasTranslateY);
178        canvas.scale(canvasScale, canvasScale);
179        canvas.clear(CanvasKit.WHITE);
180        slide.draw(canvas);
181        canvas.restore();
182
183        // HINT: Launch chrome with --disable-frame-rate-limit and --disable-gpu-vsync in order to
184        // allow this to go faster than 60fps.
185        const ms = (ScheduleDraw.fps && ScheduleDraw.fps.markFrameComplete()) ||
186                   window.performance.now();
187        if (slide.animate(ms * 1e6) || ScheduleDraw.forceAnimation) {
188          ScheduleDraw.fps = ScheduleDraw.fps || new FPSMeter(ms);
189          ScheduleDraw();
190        } else {
191          delete ScheduleDraw.fps;
192        }
193      });
194    }
195
196    ScheduleDraw();
197  }
198
199  function FPSMeter(startMs) {
200    this.frames = 0;
201    this.startMs = startMs;
202    this.markFrameComplete = () => {
203      ++this.frames;
204      const ms = window.performance.now();
205      const sec = (ms - this.startMs) / 1000;
206      if (sec > 2) {
207        console.log(Math.round(this.frames / sec) + ' fps');
208        this.frames = 0;
209        this.startMs = ms;
210      }
211      return ms;
212    };
213  }
214</script>
215