diff options
author | Kevin Lubick <kjlubick@google.com> | 2018-07-06 14:31:23 -0400 |
---|---|---|
committer | Skia Commit-Bot <skia-commit-bot@chromium.org> | 2018-07-11 15:38:08 +0000 |
commit | 22647d0e84ec63b76b9d26153c59d9338b761107 (patch) | |
tree | 40fcf32a49630a6c4f901a2c11b02b7a4e0287a7 /experimental | |
parent | 1857ddbe218afd8ef32f672619ab13c0f853436c (diff) |
Adventures with Skia, WASM and a JS API for Pathkit
See shell.html::entrypoint() for the JS side of things.
See wasm_main.cpp for the C++ side of things
(EMSCRIPTEN_BINDINGS at the bottom is what glues the two parts
together - in general the strings are for JS and the not strings
are the C++)
To build this yourself, follow the getting started instructions:
https://kripken.github.io/emscripten-site/docs/getting_started/downloads.html
and download this patch. Then, update compile.sh to point at your
sdk and run it (e.g. $SKIA_ROOT/experimental/wasm/compile.sh)
Then navigate a browser (e.g. Chrome) to
http://localhost:8000/out/wasm/pathkit.html
So far, can compile with compile.sh, but not really with
GN/ninja (the compilation into many object files and a link
at the end seems to mess emscripten up)
Bug: skia:
Change-Id: If6b300e2b102469e17841265c7866f1a81094d70
Reviewed-on: https://skia-review.googlesource.com/137422
Reviewed-by: Florin Malita <fmalita@chromium.org>
Reviewed-by: Mike Reed <reed@google.com>
Commit-Queue: Florin Malita <fmalita@chromium.org>
Diffstat (limited to 'experimental')
-rwxr-xr-x | experimental/wasm/compile.sh | 72 | ||||
-rw-r--r-- | experimental/wasm/shell.html | 511 | ||||
-rw-r--r-- | experimental/wasm/wasm_main.cpp | 373 |
3 files changed, 956 insertions, 0 deletions
diff --git a/experimental/wasm/compile.sh b/experimental/wasm/compile.sh new file mode 100755 index 0000000000..d2e7098a0b --- /dev/null +++ b/experimental/wasm/compile.sh @@ -0,0 +1,72 @@ +#!/bin/bash +# Copyright 2018 Google LLC +# +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + + +# Run this from $SKIA_HOME, not from the directory this file is in. +# This expects the environment variable EMSDK to be set +HTML_SHELL="./experimental/wasm/shell.html" + +if [[ ! -d $EMSDK ]]; then + echo "Be sure to set the EMSDK environment variable." + exit 1 +fi + +source $EMSDK/emsdk_env.sh + +echo "Compiling" + +set -e + +mkdir -p out/wasm + +# Use -O0 for larger builds (but generally quicker) +# Use -Oz for (much slower, but smaller/faster) production builds +em++ -Oz -std=c++14 \ +-Iinclude/config \ +-Iinclude/core \ +-Iinclude/private \ +-Iinclude/pathops \ +-Iinclude/utils \ +-Isrc/core \ +--bind \ +-s WASM=1 \ +-s NO_EXIT_RUNTIME=1 \ +-s ERROR_ON_UNDEFINED_SYMBOLS=1 \ +-s ERROR_ON_MISSING_LIBRARIES=1 \ +--shell-file $HTML_SHELL \ +-o out/wasm/pathkit.html \ +experimental/wasm/wasm_main.cpp \ +src/core/SkArenaAlloc.cpp \ +src/core/SkGeometry.cpp \ +src/core/SkMallocPixelRef.cpp \ +src/core/SkMath.cpp \ +src/core/SkMatrix.cpp \ +src/core/SkPath.cpp \ +src/core/SkPathRef.cpp \ +src/core/SkPoint.cpp \ +src/core/SkRect.cpp \ +src/core/SkStream.cpp \ +src/core/SkString.cpp \ +src/core/SkStringUtils.cpp \ +src/core/SkUtils.cpp \ +src/pathops/*.cpp \ +src/ports/SkDebug_stdio.cpp \ +src/ports/SkMemory_malloc.cpp \ +src/utils/SkParse.cpp \ +src/utils/SkParsePath.cpp + +# Add the following for debugging (bloats production code size otherwise) +# list of all (most?) settings: https://github.com/kripken/emscripten/blob/incoming/src/settings.js +#-s ASSERTIONS=1 \ +#-s DEMANGLE_SUPPORT=1 \ +#-g2 + +# To build with ASM.js (instead of WASM) +# This doesn't give the same results as native c++ or wasm.... +#-s WASM=0 \ +#-s ALLOW_MEMORY_GROWTH=1 \ + +python -m SimpleHTTPServer 8000 diff --git a/experimental/wasm/shell.html b/experimental/wasm/shell.html new file mode 100644 index 0000000000..8557241096 --- /dev/null +++ b/experimental/wasm/shell.html @@ -0,0 +1,511 @@ +<!doctype html> +<html lang="en-us"> + <head> + <meta charset="utf-8"> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> + <title>Skia and WASM</title> + <style> + .emscripten { padding-right: 0; margin-left: auto; margin-right: auto; display: block; } + textarea.emscripten { font-family: monospace; width: 80%; } + div.emscripten { text-align: center; } + div.emscripten_border { border: 1px solid black; } + /* the canvas *must not* have any border or padding, or mouse coords will be wrong */ + canvas.emscripten { border: 0px none; background-color: black; } + + .spinner { + height: 50px; + width: 50px; + margin: 0px auto; + -webkit-animation: rotation .8s linear infinite; + -moz-animation: rotation .8s linear infinite; + -o-animation: rotation .8s linear infinite; + animation: rotation 0.8s linear infinite; + border-left: 10px solid rgb(0,150,240); + border-right: 10px solid rgb(0,150,240); + border-bottom: 10px solid rgb(0,150,240); + border-top: 10px solid rgb(100,0,200); + border-radius: 100%; + background-color: rgb(200,100,250); + } + @-webkit-keyframes rotation { + from {-webkit-transform: rotate(0deg);} + to {-webkit-transform: rotate(360deg);} + } + @-moz-keyframes rotation { + from {-moz-transform: rotate(0deg);} + to {-moz-transform: rotate(360deg);} + } + @-o-keyframes rotation { + from {-o-transform: rotate(0deg);} + to {-o-transform: rotate(360deg);} + } + @keyframes rotation { + from {transform: rotate(0deg);} + to {transform: rotate(360deg);} + } + + </style> + </head> + <body> + <hr/> + <figure style="overflow:visible;" id="spinner"><div class="spinner"></div><center style="margin-top:0.5em"><strong>emscripten</strong></center></figure> + <div class="emscripten" id="status">Downloading...</div> + <div class="emscripten"> + <progress value="0" max="100" id="progress" hidden=1></progress> + </div> + <div class="emscripten_border"> + <canvas class="emscripten" id="canvas" oncontextmenu="event.preventDefault()"></canvas> + </div> + <svg id=svg xmlns='http://www.w3.org/2000/svg' width=50 height=50 viewPort='0 0 50 50'> + </svg> + <hr/> + <div class="emscripten"> + <input type="checkbox" id="resize">Resize canvas + <input type="checkbox" id="pointerLock" checked>Lock/hide mouse pointer + + <input type="button" value="Fullscreen" onclick="Module.requestFullscreen(document.getElementById('pointerLock').checked, + document.getElementById('resize').checked)"> + </div> + + <hr/> + <textarea class="emscripten" id="output" rows="8"></textarea> + <hr> + <script type='text/javascript'> + function entryPoint() { + // See https://kripken.github.io/emscripten-site/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html#call-compiled-c-c-code-directly-from-javascript + const FAST_TIMES = 100 + 11 /*warmups*/; + const SLOW_TIMES = 10 + 11 /*warmups*/; + + var path; + for (let i = 0; i< FAST_TIMES; i++){ + if (i === 10) { + // let it "warm up" + console.time('Path_with_calls'); // about 3.9s for 1M iterations + } + path = new Module.SkPath(); + path.moveTo(0,0); + path.lineTo(40,0); + path.lineTo(20,20); + path.close(); + path.moveTo(20,0); + path.lineTo(60,0); + path.lineTo(40,20); + path.close(); + if (i < FAST_TIMES - 1) { + path.delete(); + } + } + console.timeEnd('Path_with_calls'); + + var result; + for(let i = 0; i < SLOW_TIMES; i++) { + if (i === 10) { + console.time('simplify'); // about 5.4s for 100k iterations + } + result = Module.SimplifyPath(path); + if (i < SLOW_TIMES - 1) { + result.delete(); + } + } + console.timeEnd('simplify'); + + var p2d; + for(let i = 0; i < FAST_TIMES; i++) { + if (i === 10) { + console.time('ToPath2D'); // about 6.3s for 1M iterations + } + p2d = Module.ToPath2D(result, new Path2D()); + } + console.timeEnd('ToPath2D'); + + let ctx = Module.canvas.getContext('2d') + ctx.strokeStyle = 'red'; + ctx.stroke(p2d); + + var svgPtr; + for(let i=0; i < FAST_TIMES; i++) { + if (i === 10) { + console.time('ToSVGString (simple)'); // about 7.1s for 1M iterations + } + svgPtr = Module.ToSVGString(result); + } + console.timeEnd('ToSVGString (simple)'); + Module.print('Got SVG Data: ' + svgPtr); + + var mountains; + for(let i=0; i < FAST_TIMES; i++) { + if (i === 10) { + console.time('FromSVGString (simple)'); + } + mountains = Module.FromSVGString(svgPtr); + if (i < FAST_TIMES - 1) { + mountains.delete(); + } + } + console.timeEnd('FromSVGString (simple)'); + + path.delete(); + result.delete(); + mountains.delete(); + + //============================================================================== + + function uint8TypedArray(arr) { + const ta = new Uint8ClampedArray(arr.length); + for (let i = 0; i < arr.length; i++){ + ta[i] = arr[i]; + } + + retVal = Module._malloc(ta.length * ta.BYTES_PER_ELEMENT); + Module.HEAPU8.set(ta, retVal / ta.BYTES_PER_ELEMENT); + return retVal; + } + + function floatTypedArray(arr) { + const ta = new Float32Array(arr.length); + for (let i = 0; i < arr.length; i++){ + ta[i] = arr[i]; + } + + retVal = Module._malloc(ta.length * ta.BYTES_PER_ELEMENT); + Module.HEAPF32.set(ta, retVal / ta.BYTES_PER_ELEMENT); + return retVal; + } + var path4; + for (let i = 0; i< FAST_TIMES; i++) { + if (i === 10) { + console.time('path_with_typed_array'); // about 3.6s for 1M iterations + } + let verbs = []; + let args = []; + + verbs.push(Module.MOVE_VERB); + args.push(100); args.push(0); + verbs.push(Module.LINE_VERB); + args.push(140); args.push(0); + verbs.push(Module.LINE_VERB); + args.push(120); args.push(20); + verbs.push(Module.CLOSE_VERB); + + verbs.push(Module.MOVE_VERB); + args.push(120); args.push(0); + verbs.push(Module.LINE_VERB); + args.push(160); args.push(0); + verbs.push(Module.LINE_VERB); + args.push(140); args.push(20); + verbs.push(Module.CLOSE_VERB); + + let tVerbs = uint8TypedArray(verbs); + let tArgs = floatTypedArray(args); + + path4 = Module.SkPathFromVerbsArgsTyped(tVerbs, verbs.length, + tArgs, args.length); + if (i < FAST_TIMES - 1) { + path4.delete(); + } + Module._free(tVerbs); + Module._free(tArgs); + } + console.timeEnd('path_with_typed_array'); + //path4.dump(); + + var v4, a4; + for (let i = 0; i< FAST_TIMES; i++) { + if (i === 10) { + console.time('to_verbs_args_array'); // about 5.4s for 1M iterations + } + v4 = []; + a4 = []; + Module.SkPathToVerbsArgsArray(path4, v4, a4); + } + console.timeEnd('to_verbs_args_array'); + + Module.print(`Got path with ${v4.length} verbs and ${a4.length} args`); + Module.print(v4[0] + ' (0 for value is MoveTo, 1 for LineTo, etc)'); + Module.print(a4[0], a4[1] + ' (coordinates)'); + + path4.delete(); + //============================================================================== + + let p1 = new Module.SkPath(); + p1.moveTo(0,60); + p1.lineTo(40,60); + p1.lineTo(20,80); + p1.close(); + let p2 = new Module.SkPath(); + p2.moveTo(20,60); + p2.lineTo(60,60); + p2.lineTo(40,80); + p2.close(); + + var p3; + for (let i = 0; i< SLOW_TIMES; i++) { + if (i === 10) { + console.time('ApplyPathOp'); // about 5.2s for 100k iterations + } + p3 = Module.ApplyPathOp(p1, p2, Module.PathOp.INTERSECT); + if (i < SLOW_TIMES - 1) { + p3.delete(); + } + } + console.timeEnd('ApplyPathOp'); + + ctx = Module.canvas.getContext('2d'); + ctx.strokeStyle = 'green'; + // Alternative way to use Path2D (potentially for browsers that + // don't support the Path2D?) + ctx.beginPath(); + Module.ToPath2D(p3, ctx); + ctx.stroke(); + + var builder, p4; + for (let i = 0; i< SLOW_TIMES; i++) { + if (i === 10) { + console.time('PathOpBuilder'); // about 12.3s for 100k iterations + } + builder = new Module.SkOpBuilder(); + builder.add(p1, Module.PathOp.UNION); + builder.add(p2, Module.PathOp.UNION); + builder.add(p3, Module.PathOp.DIFFERENCE); + p4 = Module.ResolveBuilder(builder); + if (i < SLOW_TIMES - 1) { + p4.delete(); + } + builder.delete(); + } + console.timeEnd('PathOpBuilder'); + + + p2d = Module.ToPath2D(p4, new Path2D()); + + ctx = Module.canvas.getContext('2d'); + ctx.fillStyle = 'purple'; + ctx.strokeStyle = 'white'; + ctx.fill(p2d); + ctx.stroke(p2d); + + p1.delete(); + p2.delete(); + p3.delete(); + p4.delete(); + +//===================================================================================== + + function floatTypedArrayFrom2D(arr) { + // expects 2d array where index 0 is verb and index 1-n are args + let len = 0; + for (cmd of arr) { + len += cmd.length; + } + + const ta = new Float32Array(len); + let i = 0; + for (cmd of arr) { + for (c of cmd) { + ta[i] = c; + i++; + } + } + + retVal = Module._malloc(ta.length * ta.BYTES_PER_ELEMENT); + Module.HEAPF32.set(ta, retVal / ta.BYTES_PER_ELEMENT); + return [retVal, len]; + } + + function SkPathFromCmdTyped(cmdArr) { + let [cmd, len] = floatTypedArrayFrom2D(cmdArr); + let path = Module.SkPathFromCmdTyped(cmd, len); + Module._free(cmd); + return path; + } + + var path5; + for (let i = 0; i< FAST_TIMES; i++) { + if (i === 10) { + console.time('path_cmd_typed (simple)'); // about 4.6s for 1M iterations + } + let commands = []; + + commands.push([Module.MOVE_VERB, 100, 60]); + commands.push([Module.LINE_VERB, 140, 60]); + commands.push([Module.LINE_VERB, 120, 80]); + commands.push([Module.CLOSE_VERB]); + + commands.push([Module.MOVE_VERB, 120, 60]); + commands.push([Module.LINE_VERB, 160, 60]); + commands.push([Module.LINE_VERB, 140, 80]); + commands.push([Module.CLOSE_VERB]); + + path5 = SkPathFromCmdTyped(commands); + + if (i < FAST_TIMES - 1) { + path5.delete(); + } + } + console.timeEnd('path_cmd_typed (simple)'); + + p2d = Module.ToPath2D(path5, new Path2D()); + + ctx = Module.canvas.getContext('2d'); + ctx.fillStyle = 'white'; + ctx.fill(p2d); + + var cmd5; + for (let i = 0; i< FAST_TIMES; i++) { + if (i === 10) { + console.time('to_cmd_array'); // about 9.1s for 1M iterations + } + cmd5 = Module.SkPathToCmdArray(path5); + } + console.timeEnd('to_cmd_array'); + + + Module.print(`Got path with ${cmd5.length} commands`); + Module.print(cmd5[0][0] + ' (0 for value is MoveTo, 1 for LineTo, etc)'); + Module.print(cmd5[0][1], cmd5[0][2] + ' (coordinates)'); + //console.log(cmd5); + + path5.delete(); + //========================================================== + const svgPath = 'M6 18c0 .55.45 1 1 1h1v3.5c0 .83.67 1.5 1.5 1.5s1.5-.67 1.5-1.5V19h2v3.5c0 .83.67 1.5 1.5 1.5s1.5-.67 1.5-1.5V19h1c.55 0 1-.45 1-1V8H6v10zM3.5 8C2.67 8 2 8.67 2 9.5v7c0 .83.67 1.5 1.5 1.5S5 17.33 5 16.5v-7C5 8.67 4.33 8 3.5 8zm17 0c-.83 0-1.5.67-1.5 1.5v7c0 .83.67 1.5 1.5 1.5s1.5-.67 1.5-1.5v-7c0-.83-.67-1.5-1.5-1.5zm-4.97-5.84l1.3-1.3c.2-.2.2-.51 0-.71-.2-.2-.51-.2-.71 0l-1.48 1.48C13.85 1.23 12.95 1 12 1c-.96 0-1.86.23-2.66.63L7.85.15c-.2-.2-.51-.2-.71 0-.2.2-.2.51 0 .71l1.31 1.31C6.97 3.26 6 5.01 6 7h12c0-1.99-.97-3.75-2.47-4.84zM10 5H9V4h1v1zm5 0h-1V4h1v1z'; + + var android; + for(let i=0; i < SLOW_TIMES; i++) { + if (i === 10) { + console.time('FromSVGString (moderate)'); // about 6.8s for 100k iterations + } + android = Module.FromSVGString(svgPath); + if (i < SLOW_TIMES - 1) { + android.delete(); + } + } + console.timeEnd('FromSVGString (moderate)'); + + result = Module.SimplifyPath(android); + //result.dump(); + let androidSimple = Module.ToSVGString(result); + + let newPath = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + newPath.setAttribute('fill', 'rgb(0,200,0)'); + newPath.setAttribute('d', androidSimple); + document.getElementById('svg').appendChild(newPath); + + androidCMDs = Module.SkPathToCmdArray(android); + + android.delete(); + result.delete(); + + var android2; + for (let i = 0; i< FAST_TIMES; i++) { + if (i === 10) { + console.time('path_cmd_typed (moderate)'); // about 2.2s for 100k + } + + android2 = SkPathFromCmdTyped(androidCMDs); + + if (i < FAST_TIMES - 1) { + android2.delete(); + } + } + console.timeEnd('path_cmd_typed (moderate)'); + var android2Str; + for(let i=0; i < SLOW_TIMES; i++) { + if (i === 10) { + console.time('ToSVGString (moderate)'); + } + android2Str = Module.ToSVGString(android2); + } + console.timeEnd('ToSVGString (moderate)'); + + newPath = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + newPath.setAttribute('fill', 'rgb(0,0,200)'); + newPath.setAttribute('d', android2Str); + newPath.setAttribute('transform', 'translate(25 0)'); + document.getElementById('svg').appendChild(newPath); + + android2.delete(); + } + </script> + <script type='text/javascript'> + var statusElement = document.getElementById('status'); + var progressElement = document.getElementById('progress'); + var spinnerElement = document.getElementById('spinner'); + + var Module = { + preRun: [], + postRun: [entryPoint], + print: (function() { + var element = document.getElementById('output'); + if (element) element.value = ''; // clear browser cache + return function(text) { + if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' '); + // These replacements are necessary if you render to raw HTML + //text = text.replace(/&/g, "&"); + //text = text.replace(/</g, "<"); + //text = text.replace(/>/g, ">"); + //text = text.replace('\n', '<br>', 'g'); + console.log(text); + if (element) { + element.value += text + "\n"; + element.scrollTop = element.scrollHeight; // focus on bottom + } + }; + })(), + printErr: function(text) { + if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' '); + if (0) { // XXX disabled for safety typeof dump == 'function') { + dump(text + '\n'); // fast, straight to the real console + } else { + console.error(text); + } + }, + canvas: (function() { + var canvas = document.getElementById('canvas'); + + // As a default initial behavior, pop up an alert when webgl context is lost. To make your + // application robust, you may want to override this behavior before shipping! + // See http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15.2 + canvas.addEventListener("webglcontextlost", function(e) { alert('WebGL context lost. You will need to reload the page.'); e.preventDefault(); }, false); + + return canvas; + })(), + setStatus: function(text) { + if (!Module.setStatus.last) Module.setStatus.last = { time: Date.now(), text: '' }; + if (text === Module.setStatus.last.text) return; + var m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/); + var now = Date.now(); + if (m && now - Module.setStatus.last.time < 30) return; // if this is a progress update, skip it if too soon + Module.setStatus.last.time = now; + Module.setStatus.last.text = text; + if (m) { + text = m[1]; + progressElement.value = parseInt(m[2])*100; + progressElement.max = parseInt(m[4])*100; + progressElement.hidden = false; + spinnerElement.hidden = false; + } else { + progressElement.value = null; + progressElement.max = null; + progressElement.hidden = true; + if (!text) spinnerElement.hidden = true; + } + statusElement.innerHTML = text; + }, + totalDependencies: 0, + monitorRunDependencies: function(left) { + this.totalDependencies = Math.max(this.totalDependencies, left); + Module.setStatus(left ? 'Preparing... (' + (this.totalDependencies-left) + '/' + this.totalDependencies + ')' : 'All downloads complete.'); + } + }; + Module.setStatus('Downloading...'); + window.onerror = function() { + Module.setStatus('Exception thrown, see JavaScript console'); + spinnerElement.style.display = 'none'; + Module.setStatus = function(text) { + if (text) Module.printErr('[post-exception status] ' + text); + }; + }; + </script> + {{{ SCRIPT }}} + </body> +</html> diff --git a/experimental/wasm/wasm_main.cpp b/experimental/wasm/wasm_main.cpp new file mode 100644 index 0000000000..4486b06f40 --- /dev/null +++ b/experimental/wasm/wasm_main.cpp @@ -0,0 +1,373 @@ +/* + * Copyright 2018 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include "SkFloatingPoint.h" +#include "SkParsePath.h" +#include "SkPath.h" +#include "SkPathOps.h" +#include "SkString.h" + +#include <emscripten/emscripten.h> +#include <emscripten/bind.h> + +using namespace emscripten; + +static const int MOVE = 0; +static const int LINE = 1; +static const int QUAD = 2; +static const int CUBIC = 4; +static const int CLOSE = 5; + +// ================================================================================= +// Creating/Exporting Paths +// ================================================================================= + +void EMSCRIPTEN_KEEPALIVE SkPathToVerbsArgsArray(SkPath path, emscripten::val /*Array*/ verbs, + emscripten::val /*Array*/ args) { + SkPath::Iter iter(path, false); + SkPoint pts[4]; + SkPath::Verb verb; + while ((verb = iter.next(pts, false)) != SkPath::kDone_Verb) { + switch (verb) { + case SkPath::kMove_Verb: + verbs.call<void>("push", MOVE); + args.call<void>("push", pts[0].x()); + args.call<void>("push", pts[0].y()); + break; + case SkPath::kLine_Verb: + verbs.call<void>("push", LINE); + args.call<void>("push", pts[1].x()); + args.call<void>("push", pts[1].y()); + break; + case SkPath::kQuad_Verb: + verbs.call<void>("push", QUAD); + args.call<void>("push", pts[1].x()); + args.call<void>("push", pts[1].y()); + args.call<void>("push", pts[2].x()); + args.call<void>("push", pts[2].y()); + break; + case SkPath::kConic_Verb: + printf("unsupported conic verb\n"); + // TODO(kjlubick): Port in the logic from SkParsePath::ToSVGString? + break; + case SkPath::kCubic_Verb: + verbs.call<void>("push", CUBIC); + args.call<void>("push", pts[1].x()); + args.call<void>("push", pts[1].y()); + args.call<void>("push", pts[2].x()); + args.call<void>("push", pts[2].y()); + args.call<void>("push", pts[3].x()); + args.call<void>("push", pts[3].y()); + break; + case SkPath::kClose_Verb: + verbs.call<void>("push", CLOSE); + break; + case SkPath::kDone_Verb: + break; + } + } +} + +emscripten::val JSArray = emscripten::val::global("Array"); + +emscripten::val EMSCRIPTEN_KEEPALIVE SkPathToCmdArray(SkPath path) { + val cmds = JSArray.new_(); + + SkPath::Iter iter(path, false); + SkPoint pts[4]; + SkPath::Verb verb; + while ((verb = iter.next(pts, false)) != SkPath::kDone_Verb) { + val cmd = JSArray.new_(); + switch (verb) { + case SkPath::kMove_Verb: + cmd.call<void>("push", MOVE); + cmd.call<void>("push", pts[0].x()); + cmd.call<void>("push", pts[0].y()); + break; + case SkPath::kLine_Verb: + cmd.call<void>("push", LINE); + cmd.call<void>("push", pts[1].x()); + cmd.call<void>("push", pts[1].y()); + break; + case SkPath::kQuad_Verb: + cmd.call<void>("push", QUAD); + cmd.call<void>("push", pts[1].x()); + cmd.call<void>("push", pts[1].y()); + cmd.call<void>("push", pts[2].x()); + cmd.call<void>("push", pts[2].y()); + break; + case SkPath::kConic_Verb: + printf("unsupported conic verb\n"); + // TODO(kjlubick): Port in the logic from SkParsePath::ToSVGString? + break; + case SkPath::kCubic_Verb: + cmd.call<void>("push", CUBIC); + cmd.call<void>("push", pts[1].x()); + cmd.call<void>("push", pts[1].y()); + cmd.call<void>("push", pts[2].x()); + cmd.call<void>("push", pts[2].y()); + cmd.call<void>("push", pts[3].x()); + cmd.call<void>("push", pts[3].y()); + break; + case SkPath::kClose_Verb: + cmd.call<void>("push", CLOSE); + break; + case SkPath::kDone_Verb: + break; + } + cmds.call<void>("push", cmd); + } + return cmds; +} + +// This type signature is a mess, but it's necessary. See, we can't use "bind" (EMSCRIPTEN_BINDINGS) +// and pointers to primitive types (Only bound types like SkPoint). We could if we used +// cwrap (see https://becominghuman.ai/passing-and-returning-webassembly-array-parameters-a0f572c65d97) +// but that requires us to stick to C code and, AFAIK, doesn't allow us to return nice things like +// SkPath or SkOpBuilder. +// +// So, basically, if we are using C++ and EMSCRIPTEN_BINDINGS, we can't have primative pointers +// in our function type signatures. (this gives an error message like "Cannot call foo due to unbound +// types Pi, Pf"). But, we can just pretend they are numbers and cast them to be pointers and +// the compiler is happy. +SkPath EMSCRIPTEN_KEEPALIVE SkPathFromVerbsArgsTyped(int /* uint8_t* */ vptr, int numVerbs, + int /* float* */aptr, int numArgs) { + auto verbs = reinterpret_cast<uint8_t*>(vptr); + auto args = reinterpret_cast<float*>(aptr); + SkPath path; + int argsIndex = 0; + float x1, y1, x2, y2, x3, y3; + + // if there are not enough arguments, bail with the path we've constructed so far. + #define CHECK_NUM_ARGS(n) \ + if ((argsIndex + n) > numArgs) { \ + SkDebugf("Not enough args to match the verbs. Saw %d args\n", numArgs); \ + return path; \ + } + + for(int i = 0; i < numVerbs; i++){ + switch (verbs[i]) { + case MOVE: + CHECK_NUM_ARGS(2); + x1 = args[argsIndex++], y1 = args[argsIndex++]; + path.moveTo(x1, y1); + break; + case LINE: + CHECK_NUM_ARGS(2); + x1 = args[argsIndex++], y1 = args[argsIndex++]; + path.lineTo(x1, y1); + break; + case QUAD: + CHECK_NUM_ARGS(4); + x1 = args[argsIndex++], y1 = args[argsIndex++]; + x2 = args[argsIndex++], y2 = args[argsIndex++]; + path.quadTo(x1, y1, x2, y2); + break; + case CUBIC: + CHECK_NUM_ARGS(6); + x1 = args[argsIndex++], y1 = args[argsIndex++]; + x2 = args[argsIndex++], y2 = args[argsIndex++]; + x3 = args[argsIndex++], y3 = args[argsIndex++]; + path.cubicTo(x1, y1, x2, y2, x3, y3); + break; + case CLOSE: + path.close(); + break; + default: + SkDebugf(" path: UNKNOWN VERB %d, aborting dump...\n", verbs[i]); + return path; + } + } + + #undef CHECK_NUM_ARGS + + return path; +} + +// See above comment for rational of pointer mess +SkPath EMSCRIPTEN_KEEPALIVE SkPathFromCmdTyped(int /* float* */cptr, int numCmds) { + auto cmds = reinterpret_cast<float*>(cptr); + SkPath path; + float x1, y1, x2, y2, x3, y3; + + // if there are not enough arguments, bail with the path we've constructed so far. + #define CHECK_NUM_ARGS(n) \ + if ((i + n) > numCmds) { \ + SkDebugf("Not enough args to match the verbs. Saw %d commands\n", numCmds); \ + return path; \ + } + + for(int i = 0; i < numCmds;){ + switch (sk_float_floor2int(cmds[i++])) { + case MOVE: + CHECK_NUM_ARGS(2); + x1 = cmds[i++], y1 = cmds[i++]; + path.moveTo(x1, y1); + break; + case LINE: + CHECK_NUM_ARGS(2); + x1 = cmds[i++], y1 = cmds[i++]; + path.lineTo(x1, y1); + break; + case QUAD: + CHECK_NUM_ARGS(4); + x1 = cmds[i++], y1 = cmds[i++]; + x2 = cmds[i++], y2 = cmds[i++]; + path.quadTo(x1, y1, x2, y2); + break; + case CUBIC: + CHECK_NUM_ARGS(6); + x1 = cmds[i++], y1 = cmds[i++]; + x2 = cmds[i++], y2 = cmds[i++]; + x3 = cmds[i++], y3 = cmds[i++]; + path.cubicTo(x1, y1, x2, y2, x3, y3); + break; + case CLOSE: + path.close(); + break; + default: + SkDebugf(" path: UNKNOWN command %f, aborting dump...\n", cmds[i-1]); + return path; + } + } + + #undef CHECK_NUM_ARGS + + return path; +} + +//======================================================================================== +// SVG THINGS +//======================================================================================== + +val EMSCRIPTEN_KEEPALIVE ToSVGString(SkPath path) { + SkString s; + SkParsePath::ToSVGString(path, &s); + // Wrapping it in val automatically turns it into a JS string. + // Not too sure on performance implications, but is is simpler than + // returning a raw pointer to const char * and then using + // Pointer_stringify() on the calling side. + return val(s.c_str()); +} + + +SkPath EMSCRIPTEN_KEEPALIVE FromSVGString(std::string str) { + SkPath path; + SkParsePath::FromSVGString(str.c_str(), &path); + return path; +} + +//======================================================================================== +// PATHOP THINGS +//======================================================================================== + +SkPath EMSCRIPTEN_KEEPALIVE SimplifyPath(SkPath path) { + SkPath simple; + Simplify(path, &simple); + return simple; +} + +SkPath EMSCRIPTEN_KEEPALIVE ApplyPathOp(SkPath pathOne, SkPath pathTwo, SkPathOp op) { + SkPath path; + Op(pathOne, pathTwo, op, &path); + return path; +} + +SkPath EMSCRIPTEN_KEEPALIVE ResolveBuilder(SkOpBuilder builder) { + SkPath path; + builder.resolve(&path); + return path; +} + +//======================================================================================== +// Canvas THINGS +//======================================================================================== + +emscripten::val EMSCRIPTEN_KEEPALIVE ToPath2D(SkPath path, val/* Path2D&*/ retVal) { + SkPath::Iter iter(path, false); + SkPoint pts[4]; + SkPath::Verb verb; + while ((verb = iter.next(pts, false)) != SkPath::kDone_Verb) { + switch (verb) { + case SkPath::kMove_Verb: + retVal.call<void>("moveTo", pts[0].x(), pts[0].y()); + break; + case SkPath::kLine_Verb: + retVal.call<void>("lineTo", pts[1].x(), pts[1].y()); + break; + case SkPath::kQuad_Verb: + retVal.call<void>("quadraticCurveTo", pts[1].x(), pts[1].y(), pts[2].x(), pts[2].y()); + break; + case SkPath::kConic_Verb: + printf("unsupported conic verb\n"); + // TODO(kjlubick): Port in the logic from SkParsePath::ToSVGString? + break; + case SkPath::kCubic_Verb: + retVal.call<void>("bezierCurveTo", pts[1].x(), pts[1].y(), pts[2].x(), pts[2].y(), + pts[3].x(), pts[3].y()); + break; + case SkPath::kClose_Verb: + retVal.call<void>("closePath"); + break; + case SkPath::kDone_Verb: + break; + } + } + return retVal; +} + +// Binds the classes to the JS +EMSCRIPTEN_BINDINGS(skia) { + class_<SkPath>("SkPath") + .constructor<>() + + .function("moveTo", + select_overload<void(SkScalar, SkScalar)>(&SkPath::moveTo)) + .function("lineTo", + select_overload<void(SkScalar, SkScalar)>(&SkPath::lineTo)) + .function("quadTo", + select_overload<void(SkScalar, SkScalar, SkScalar, SkScalar)>(&SkPath::quadTo)) + .function("cubicTo", + select_overload<void(SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar)>(&SkPath::cubicTo)) + .function("close", &SkPath::close); + // Uncomment below for debugging. + //.function("dump", select_overload<void() const>(&SkPath::dump)); + + class_<SkOpBuilder>("SkOpBuilder") + .constructor<>() + + .function("add", &SkOpBuilder::add); + + // Without this, module._ToPath2D (yes with an underscore) + // would be exposed, but be unable to correctly handle the SkPath type. + function("ToPath2D", &ToPath2D); + function("ToSVGString", &ToSVGString); + function("FromSVGString", &FromSVGString); + + function("SkPathToVerbsArgsArray", &SkPathToVerbsArgsArray); + function("SkPathFromVerbsArgsTyped", &SkPathFromVerbsArgsTyped); + + function("SkPathFromCmdTyped", &SkPathFromCmdTyped); + function("SkPathToCmdArray", &SkPathToCmdArray); + + function("SimplifyPath", &SimplifyPath); + function("ApplyPathOp", &ApplyPathOp); + function("ResolveBuilder", &ResolveBuilder); + + enum_<SkPathOp>("PathOp") + .value("DIFFERENCE", SkPathOp::kDifference_SkPathOp) + .value("INTERSECT", SkPathOp::kIntersect_SkPathOp) + .value("UNION", SkPathOp::kUnion_SkPathOp) + .value("XOR", SkPathOp::kXOR_SkPathOp) + .value("REVERSE_DIFFERENCE", SkPathOp::kReverseDifference_SkPathOp); + + constant("MOVE_VERB", MOVE); + constant("LINE_VERB", LINE); + constant("QUAD_VERB", QUAD); + constant("CUBIC_VERB", CUBIC); + constant("CLOSE_VERB", CLOSE); +} |