HTML5 canvas drawing demo.

This commit is contained in:
Alexander Gorshenev
2017-09-06 13:53:09 +03:00
committed by alexander-gorshenev
parent ce3ed52705
commit b367a0d255
13 changed files with 414 additions and 35 deletions
@@ -346,7 +346,12 @@ internal class LinkStage(val context: Context) {
private fun JavaScriptLinker(jsFiles: List<String>, executable: String): String {
val linkedJavaScript = File("$executable.js")
linkedJavaScript.writeBytes(ByteArray(0));
val jsLibsExceptLauncher = jsFiles.filter { it != "launcher.js" }.map { it.removeSuffix(".js") }
val linkerStub = "var konan = { libraries: [] };\n"
linkedJavaScript.writeBytes(linkerStub.toByteArray());
jsFiles.forEach {
linkedJavaScript.appendBytes(File(it).readBytes())
+2 -2
View File
@@ -260,7 +260,7 @@ targetList.each { target ->
}
if (target == 'wasm32') {
from(project(':runtime').file('src/launcher/js')) {
into('konan/nativelib')
into('klib/stdlib/targets/wasm32/included')
}
}
}
@@ -376,4 +376,4 @@ task clean {
}
delete bundle.outputs.files
}
}
}
@@ -282,7 +282,7 @@ fun handleExceptionContinuation(x: (Throwable) -> Unit): Continuation<Any?> = ob
def targetToolchain = TargetPropertiesKt.hostTargetString(properties, "targetToolchain", target)
def absoluteTargetToolchain = "$dependenciesDir/$targetToolchain"
def d8 = "$absoluteTargetToolchain/bin/d8"
def launcherJs = "${dist.absolutePath}/konan/nativelib/launcher.js"
def launcherJs = "${exe}.js"
return [d8, '--expose-wasm', launcherJs, '--', exe]
} else {
return [exe]
+14 -8
View File
@@ -38,6 +38,12 @@ OBJ_GETTER(setupArgs, int argc, const char** argv) {
//--- main --------------------------------------------------------------------//
extern "C" KInt Konan_start(const ObjHeader*);
extern "C" KInt Konan_run_start(int argc, const char** argv) {
ObjHolder args;
setupArgs(argc, argv, args.slot());
return Konan_start(args.obj());
}
extern "C" RUNTIME_USED int Konan_main(int argc, const char** argv) {
RuntimeState* state = InitRuntime();
@@ -45,12 +51,7 @@ extern "C" RUNTIME_USED int Konan_main(int argc, const char** argv) {
return 2;
}
KInt exitStatus;
{
ObjHolder args;
setupArgs(argc, argv, args.slot());
exitStatus = Konan_start(args.obj());
}
KInt exitStatus = Konan_run_start(argc, argv);
DeinitRuntime(state);
@@ -63,14 +64,19 @@ extern "C" RUNTIME_USED int Konan_main(int argc, const char** argv) {
extern "C" int Konan_js_arg_size(int index);
extern "C" int Konan_js_fetch_arg(int index, char* ptr);
extern "C" int Konan_js_main(int argc) {
extern "C" int Konan_js_main(int argc, int memoryInit) {
char** argv = (char**)konan::calloc(1, argc);
for (int i = 0; i< argc; ++i) {
argv[i] = (char*)konan::calloc(1, Konan_js_arg_size(i));
Konan_js_fetch_arg(i, argv[i]);
}
return Konan_main(argc, (const char**)argv);
if (memoryInit) {
return Konan_main(argc, (const char**)argv);
} else {
return Konan_run_start(argc, (const char**)argv);
}
}
#endif
#endif
+65 -23
View File
@@ -14,13 +14,14 @@
* limitations under the License.
*/
var module;
var instance;
var heap;
var memory;
var global_arguments;
var globalBase = 0; // TODO: Is there any way to obtain global_base from JavaScript?
var konanStackTop;
function isBrowser() {
if (typeof window === 'undefined') {
return false;
@@ -29,6 +30,7 @@ function isBrowser() {
};
}
var runtime;
if (isBrowser()) {
runtime = {
print: console.log,
@@ -85,6 +87,14 @@ function toString(pointer) {
return string;
}
function toUTF16String(pointer, size) {
var string = '';
for (var i = pointer; i < pointer + size; i+=2) {
string += String.fromCharCode(heap[i] + heap[i+1]*256);
}
return string;
}
function int32ToHeap(value, pointer) {
heap[pointer] = value & 0xff;
heap[pointer+1] = (value & 0xff00) >>> 8;
@@ -166,48 +176,80 @@ var konan_dependencies = {
}
};
function invokeModule(buffer, args) {
function linkJavaScriptLibraries() {
konan.libraries.forEach ( function (library) {
for (var property in library) {
konan_dependencies.env[property] = library[property];
};
});
};
function invokeModule(inst, args) {
if (args.length < 1) print_usage();
global_arguments = args;
module = new WebAssembly.Module(new Uint8Array(buffer));
module.env = {};
module.env.memoryBase = 0;
module.env.tablebase = 0;
instance = inst;
instance = new WebAssembly.Instance(module, konan_dependencies);
memory = konan_dependencies.env.memory
heap = new Uint8Array(konan_dependencies.env.memory.buffer);
konanStackTop = stackTop();
var exit_status = 0;
try {
runGlobalInitializers(instance.exports);
exit_status = instance.exports.Konan_js_main(args.length);
if (isBrowser()) {
instance.memoryState = instance.exports.InitRuntime();
}
exit_status = instance.exports.Konan_js_main(args.length, isBrowser() ? 0 : 1);
// TODO: so when should we deinit runtime?
} catch (e) {
runtime.print("Exception executing Konan_js_main: " + e);
runtime.print("Exception executing entry point: " + e);
runtime.print(e.stack);
exit_status = 1;
}
runtime.flush();
return exit_status;
}
// Invoke from the browser.
function loadAndInvoke(filename) {
function listener () {
invokeModule(this.response, [filename]);
}
var request = new XMLHttpRequest();
request.addEventListener("load", listener);
request.open("GET", filename);
request.responseType = "arraybuffer";
request.send();
function setupModule(module) {
module.env = {};
module.env.memoryBase = 0;
module.env.tablebase = 0;
linkJavaScriptLibraries();
}
// Invoke from d8.
if (!isBrowser()) {
quit(invokeModule(readbuffer(arguments[0]), arguments));
// Instantiate module in Browser in a sequence of promises.
function instantiateAndRun(arraybuffer, args) {
WebAssembly.compile(arraybuffer)
.then(function(module) {
setupModule(module);
return WebAssembly.instantiate(module, konan_dependencies);
}).then(function(instance) {
return invokeModule(instance, args);
});
}
// Instantiate module in d8 synchronously.
function instantiateAndRunSync(arraybuffer, args) {
var module = WebAssembly.Module(arraybuffer)
setupModule(module);
var instance = WebAssembly.Instance(module, konan_dependencies);
return invokeModule(instance, args)
}
if (isBrowser()) {
var filename = document.currentScript.getAttribute("wasm");
fetch(filename).then( function(response) {
return response.arrayBuffer();
}).then(function(arraybuffer) {
instantiateAndRun(arraybuffer, [filename]);
});
} else {
// Invoke from d8.
var arrayBuffer = readbuffer(arguments[0]);
var exitStatus = instantiateAndRunSync(arrayBuffer, arguments);
quit(exitStatus);
}
+12
View File
@@ -1092,4 +1092,16 @@ OBJ_GETTER(Kotlin_String_subSequence, KString thiz, KInt startIndex, KInt endInd
RETURN_OBJ(result->obj());
}
const KChar* Kotlin_String_utf16pointer(KString message) {
RuntimeAssert(message->type_info() == theStringTypeInfo, "Must use a string");
const KChar* utf16 = CharArrayAddressOfElementAt(message, 0);
return utf16;
}
KInt Kotlin_String_utf16length(KString message) {
RuntimeAssert(message->type_info() == theStringTypeInfo, "Must use a string");
return message->count_ * sizeof(KChar);
}
} // extern "C"
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
DIR=$(cd "$(dirname "${BASH_SOURCE[0]}" )" && pwd )
PATH=$DIR/../../dist/bin:$DIR/../../bin:$PATH
if [ x$TARGET == x ]; then
case "$OSTYPE" in
darwin*) TARGET=macbook ;;
linux*) TARGET=linux ;;
*) echo "unknown: $OSTYPE" && exit 1;;
esac
fi
var=CFLAGS_${TARGET}
CFLAGS=${!var}
var=COMPILER_ARGS_${TARGET}
COMPILER_ARGS=${!var} # add -opt for an optimized build.
mkdir -p $DIR/build/bin
konanc $DIR/src/jsinterop/kotlin \
-includeBinary $DIR/src/jsinterop/js/jsinterop.js \
-p library -o $DIR/build/jsinterop -target wasm32 || exit 1
konanc $DIR/src/canvas/kotlin \
-includeBinary $DIR/src/canvas/js/canvas.js \
-l $DIR/build/jsinterop \
-p library -o $DIR/build/canvas -target wasm32 || exit 1
konanc $DIR/src/main/kotlin \
-l $DIR/build/jsinterop -l $DIR/build/canvas \
-o $DIR/build/bin/html5Canvas -target wasm32 || exit 1
echo "Artifact path is $DIR/build/bin/html5Canvas.wasm"
echo "Artifact path is $DIR/build/bin/html5Canvas.wasm.js"
echo "Check out $DIR/index.html"
echo "Serve $DIR/ by an http server for the demo"
# For example run: python -m SimpleHTTPServer 8080
+14
View File
@@ -0,0 +1,14 @@
<html>
<head>
<meta charset="utf-8">
<script src="build/bin/html5Canvas.wasm.js" wasm="build/bin//html5Canvas.wasm"> </script>
</head>
<body>
<canvas id="myCanvas" width="640" height="480" style="border:1px solid #000000;">
</canvas>
<p>Draw something using the mouse!</p>
</body>
</html>
@@ -0,0 +1,33 @@
konan.libraries.push ({ // This one will be auto generated.
knjs_addDocumentToArena: function(arena) {
// Should be resolved by jsinterop.
return konan_dependencies.env.Konan_js_addObjectToArena(arena, document);
},
knjs_setInterval: function (func, interval) {
setInterval(konan_dependencies.env.Konan_js_wrapLambda(func), interval);
},
knjs_getElementById: function (arena, obj, id, idLength) {
var name = toUTF16String(id, idLength);
var result = konan_dependencies.env.arenas[arena][obj].getElementById(name);
return konan_dependencies.env.Konan_js_addObjectToArena(arena, result);
},
knjs_getContext: function (arena, obj, context, contextLength) {
var name = toUTF16String(context, contextLength);
var result = konan_dependencies.env.arenas[arena][obj].getContext(name);
return konan_dependencies.env.Konan_js_addObjectToArena(arena, result);
},
knjs_getBoundingClientRect: function (arena, obj) {
var result = konan_dependencies.env.arenas[arena][obj].getBoundingClientRect();
return konan_dependencies.env.Konan_js_addObjectToArena(arena, result);
},
knjs_moveTo: function(arena, obj, x, y) {
konan_dependencies.env.arenas[arena][obj].moveTo(x, y);
},
knjs_lineTo: function(arena, obj, x, y) {
konan_dependencies.env.arenas[arena][obj].lineTo(x, y);
},
knjs_stroke: function(arena, obj) {
konan_dependencies.env.arenas[arena][obj].stroke();
},
});
@@ -0,0 +1,80 @@
package html5.minimal
// This file will be auto-generated.
import kotlinx.wasm.jsinterop.*
@SymbolName("knjs_addDocumentToArena")
external public fun addDocumentToArena(arena: Arena): Int
@SymbolName("knjs_getElementById")
external public fun knjs_getElementById(arena: Int, obj: Int, idPtr: Int, idLen: Int): Int;
@SymbolName("knjs_getContext")
external public fun knjs_getContext(arena: Int, obj: Int, idPtr: Int, idLen: Int): Int;
@SymbolName("knjs_getBoundingClientRect")
external public fun knjs_getBoundingClientRect(arena: Int, obj: Int): Int;
@SymbolName("knjs_stroke")
external public fun knjs_stroke(arena: Int, obj: Int);
@SymbolName("knjs_moveTo")
external public fun knjs_moveTo(arena: Int, obj: Int, x: Int, y: Int): Int;
@SymbolName("knjs_lineTo")
external public fun knjs_lineTo(arena: Int, obj: Int, x: Int, y: Int): Int;
@SymbolName("knjs_setInterval")
external public fun knjs_setInterval(functionIndex: Int, interval: Int);
open class Document(arena: Int, index: Int): JsValue(arena, index) {
fun getElementById(id: String): JsValue {
val resultIndex = knjs_getElementById(this.arena, this.index, stringPointer(id), stringLengthBytes(id))
return JsValue(this.arena, resultIndex)
}
}
open class DOMRect(arena: Int, index: Int): JsValue(arena, index) {
}
open class Canvas(arena: Int, index: Int): JsValue(arena, index) {
fun getContext(context: String): Context {
val resultIndex = knjs_getContext(this.arena, this.index, stringPointer(context), stringLengthBytes(context))
return Context(this.arena, resultIndex)
}
fun getBoundingClientRect(): DOMRect {
return DOMRect(this.arena, knjs_getBoundingClientRect(this.arena, this.index))
}
}
// TODO: this is awful.
val JsValue.asCanvas: Canvas
get() {
return Canvas(this.arena, this.index)
}
open class Context(arena: Int, index: Int): JsValue(arena, index) {
fun lineTo(x: Int, y: Int) {
knjs_lineTo(this.arena, this.index, x, y)
}
fun moveTo(x: Int, y: Int) {
knjs_moveTo(this.arena, this.index, x, y)
}
fun stroke() {
knjs_stroke(this.arena, this.index);
}
}
fun setInterval(lambda: (ArrayList<JsValue>)->Unit, interval: Int) {
knjs_setInterval(wrapFunction(lambda), interval);
}
// TODO: The arguments are flipped to have lambda within the last position.
fun setInterval(interval: Int, lambda: (ArrayList<JsValue>)->Unit)
= setInterval(lambda, interval)
class html5 {
val arena = allocateArena()
val document = Document(arena, addDocumentToArena(arena))
}
@@ -0,0 +1,41 @@
konan.libraries.push ({
arenas: [],
Konan_js_allocateArena: function (array) {
konan_dependencies.env.arenas.push(array || []);
return konan_dependencies.env.arenas.length - 1;
},
Konan_js_freeArena: function(arenaIndex) {
var arena = konan_dependencies.env.arenas[arenaIndex];
arena.forEach(function(element, index) {
arena[index] = null;
});
konan_dependencies.env.arenas[arenaIndex] = null;
},
Konan_js_addObjectToArena: function (arenaIndex, object) {
var arena = konan_dependencies.env.arenas[arenaIndex];
arena.push(object);
return arena.length - 1;
},
Konan_js_wrapLambda: function (index) {
return (function () {
// convert Arguments to an array
// to be provided by launcher.js
var arenaIndex = konan_dependencies.env.Konan_js_allocateArena(Array.prototype.slice.call(arguments));
// To be provided by Kotlin runtime.
instance.exports.Konan_js_runLambda(index, arenaIndex, arguments.length);
konan_dependencies.env.Konan_js_freeArena(arenaIndex);
});
},
Konan_js_getInt: function(arenaIndex, objIndex, propertyNamePtr, propertyNameLength) {
// TODO: The toUTF16String() is to be resolved by launcher.js runtime.
var property = toUTF16String(propertyNamePtr, propertyNameLength);
var value = konan_dependencies.env.arenas[arenaIndex][objIndex][property];
return value;
},
Konan_js_setFunction: function (arena, obj, propertyName, propertyNameLength, func) {
var name = toUTF16String(propertyName, propertyNameLength);
konan_dependencies.env.arenas[arena][obj][name] = konan_dependencies.env.Konan_js_wrapLambda(func);
},
});
@@ -0,0 +1,61 @@
package kotlinx.wasm.jsinterop
import konan.internal.ExportForCppRuntime
import kotlinx.cinterop.*
typealias Arena = Int
typealias Object = Int
typealias Pointer = Int
@SymbolName("Konan_js_allocateArena")
external public fun allocateArena(): Arena
@SymbolName("Konan_js_freeArena")
external public fun freeArena(arena: Arena)
@SymbolName("Kotlin_String_utf16pointer")
external public fun stringPointer(message: String): Pointer
@SymbolName("Kotlin_String_utf16length")
external public fun stringLengthBytes(message: String): Int
typealias KtFunction = (ArrayList<JsValue>)->Unit
fun wrapFunction(func: KtFunction): Int {
val ptr: Long = StableObjPtr.create(func).value.toLong()
return ptr.toInt() // TODO: LP64 unsafe.
}
@ExportForCppRuntime("Konan_js_runLambda")
fun runLambda(userData: Int, arena: Arena, arenaSize: Int) {
val arguments = arrayListOf<JsValue>()
for (i in 0..arenaSize-1) {
arguments.add(JsValue(arena, i));
}
// TODO: LP64 unsafe: wasm32 passes Int, not Long.
val func = StableObjPtr.fromValue(userData.toLong().toCPointer()!!).get() as KtFunction
func(arguments);
}
open class JsValue(val arena: Arena, val index: Object) {
fun getInt(property: String): Int {
return getInt(arena, index, stringPointer(property), stringLengthBytes(property))
}
}
@SymbolName("Konan_js_getInt")
external public fun getInt(arena: Arena, obj: Object, propertyPtr: Pointer, propertyLen: Int): Int;
@SymbolName("Konan_js_setFunction")
external public fun setFunction(arena: Arena, obj: Object, propertyName: Pointer, propertyLength: Int , function: Int)
fun setter(obj: JsValue, property: String, lambda: (arguments: ArrayList<JsValue>) -> Unit) {
val index = wrapFunction(lambda);
setFunction(obj.arena, obj.index, stringPointer(property), stringLengthBytes(property), index)
}
fun JsValue.setter(property: String, lambda: (ArrayList<JsValue>) -> Unit) {
setter(this, property, lambda)
}
@@ -0,0 +1,47 @@
import html5.minimal.*
import kotlinx.wasm.jsinterop.*
import konan.internal.ExportForCppRuntime
fun main(args: Array<String>) {
val html5 = html5()
val document = html5.document
val canvas = document.getElementById("myCanvas").asCanvas;
val ctx = canvas.getContext("2d");
val rect = canvas.getBoundingClientRect();
val rectLeft = rect.getInt("left")
val rectTop = rect.getInt("top")
var mouseX: Int = 0;
var mouseY: Int = 0;
var draw: Boolean = false;
document.setter("onmousemove") { args: ArrayList<JsValue> ->
val event = args[0];
mouseX = event.getInt("clientX") - rectLeft
mouseY = event.getInt("clientY") - rectTop
if (mouseX < 0) mouseX = 0;
if (mouseX > 639) mouseX = 639;
if (mouseY < 0) mouseY = 0;
if (mouseY > 479) mouseY = 479;
}
document.setter("onmousedown") {
draw = true;
}
document.setter("onmouseup") {
draw = false;
}
setInterval(10) {
if (draw) {
ctx.lineTo(mouseX, mouseY);
ctx.stroke();
} else {
ctx.moveTo(mouseX, mouseY);
ctx.stroke();
}
}
}