├── .gitignore ├── LICENSE ├── README.md ├── docs ├── index.html ├── wasm.js ├── wasm.wasm └── wasm_exec.js ├── server.go └── wasm.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018-2019 TinyGo Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of the copyright holder nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is just simple play around code, to learn what TinyGo 2 | can do with the html5 canvas at present. :smile: 3 | 4 | The running version of this is here: 5 | 6 |     https://justinclift.github.io/tinygo_canvas2/ 7 | 8 | To compile the WebAssembly file: 9 | 10 | $ tinygo build -target wasm -no-debug -o docs/wasm.wasm wasm.go 11 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | TinyGo WebAssembly Canvas example 7 | 8 | 9 | 10 | 21 | 22 | 23 | Your browser doesn't appear to support the canvas tag. 24 | 25 | 26 | -------------------------------------------------------------------------------- /docs/wasm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const WASM_URL = 'wasm.wasm'; 4 | 5 | var wasm; 6 | 7 | // Apply the matrix transformations 8 | function applyTransformation() { 9 | wasm.exports.applyTransformation(); 10 | } 11 | 12 | // Pass mouse clicks through to the wasm handler 13 | function clickHandler(evt) { 14 | wasm.exports.clickHandler(evt.clientX, evt.clientY); 15 | } 16 | 17 | // Pass key presses through to the wasm handler 18 | function keyPressHandler(evt) { 19 | let key = 0; 20 | switch(evt.key) { 21 | // Move keys 22 | case "d": 23 | case "D": 24 | key = 1; 25 | break; 26 | case "a": 27 | case "A": 28 | key = 2; 29 | break; 30 | case "w": 31 | case "W": 32 | key = 3; 33 | break; 34 | case "s": 35 | case "S": 36 | key = 4; 37 | break; 38 | 39 | // Rotate keys 40 | case "ArrowLeft": 41 | case "4": 42 | key = 5; 43 | break; 44 | case "ArrowRight": 45 | case "6": 46 | key = 6; 47 | break; 48 | case "ArrowUp": 49 | case "8": 50 | key = 7; 51 | break; 52 | case "ArrowDown": 53 | case "2": 54 | key = 8; 55 | break; 56 | case "PageUp": 57 | case "9": 58 | key = 9; 59 | break; 60 | case "PageDown": 61 | case "3": 62 | key = 10; 63 | break; 64 | case "Home": 65 | case "7": 66 | key = 11; 67 | break; 68 | case "End": 69 | case "1": 70 | key = 12; 71 | break; 72 | 73 | // Change step size keys 74 | case "-": 75 | key = 13; 76 | break; 77 | case "+": 78 | key = 14; 79 | break; 80 | 81 | // Unknown key press, don't pass it through 82 | default: 83 | return; 84 | } 85 | 86 | // console.log("JS: Key pressed = " + key); 87 | wasm.exports.keyPressHandler(key); 88 | } 89 | 90 | // Pass mouse movement events through to its wasm handler 91 | function moveHandler(evt) { 92 | // console.log(evt); 93 | wasm.exports.moveHandler(evt.clientX, evt.clientY); 94 | } 95 | 96 | // Render one frame of the animation 97 | function renderFrame() { 98 | wasm.exports.renderFrame(); 99 | } 100 | 101 | // Pass mouse wheel events through to its wasm handler 102 | function wheelHandler(evt) { 103 | wasm.exports.wheelHandler(evt.deltaY); 104 | } 105 | 106 | 107 | function init() { 108 | const go = new Go(); 109 | if ('instantiateStreaming' in WebAssembly) { 110 | WebAssembly.instantiateStreaming(fetch(WASM_URL), go.importObject).then(function (obj) { 111 | wasm = obj.instance; 112 | go.run(wasm); 113 | 114 | // Set up wasm event handlers 115 | document.addEventListener("keydown", keyPressHandler); 116 | document.getElementById("mycanvas").addEventListener("mousedown", clickHandler); 117 | document.getElementById("mycanvas").addEventListener("mousemove", moveHandler); 118 | document.getElementById("mycanvas").addEventListener("wheel", wheelHandler); 119 | 120 | // Set up basic render loop 121 | setInterval(function() { 122 | applyTransformation(); 123 | },25); 124 | }) 125 | } else { 126 | fetch(WASM_URL).then(resp => 127 | resp.arrayBuffer() 128 | ).then(bytes => 129 | WebAssembly.instantiate(bytes, go.importObject).then(function (obj) { 130 | wasm = obj.instance; 131 | go.run(wasm); 132 | 133 | // Set up wasm event handlers 134 | document.getElementById("mycanvas").addEventListener("mousedown", clickHandler); 135 | document.getElementById("mycanvas").addEventListener("keydown", keyPressHandler); 136 | document.getElementById("mycanvas").addEventListener("mousemove", moveHandler); 137 | document.getElementById("mycanvas").addEventListener("wheel", wheelHandler); 138 | 139 | // Set up basic render loop 140 | setInterval(function() { 141 | applyTransformation(); 142 | },25); 143 | }) 144 | ) 145 | } 146 | } 147 | 148 | init(); 149 | -------------------------------------------------------------------------------- /docs/wasm.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justinclift/tinygo_canvas2/8872c737b8ae18420fbc9ed0d8beb62e9b1f89b0/docs/wasm.wasm -------------------------------------------------------------------------------- /docs/wasm_exec.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | // 5 | // This file has been modified for use by the TinyGo compiler. 6 | 7 | (() => { 8 | // Map web browser API and Node.js API to a single common API (preferring web standards over Node.js API). 9 | const isNodeJS = typeof process !== "undefined"; 10 | if (isNodeJS) { 11 | global.require = require; 12 | global.fs = require("fs"); 13 | 14 | const nodeCrypto = require("crypto"); 15 | global.crypto = { 16 | getRandomValues(b) { 17 | nodeCrypto.randomFillSync(b); 18 | }, 19 | }; 20 | 21 | global.performance = { 22 | now() { 23 | const [sec, nsec] = process.hrtime(); 24 | return sec * 1000 + nsec / 1000000; 25 | }, 26 | }; 27 | 28 | const util = require("util"); 29 | global.TextEncoder = util.TextEncoder; 30 | global.TextDecoder = util.TextDecoder; 31 | } else { 32 | if (typeof window !== "undefined") { 33 | window.global = window; 34 | } else if (typeof self !== "undefined") { 35 | self.global = self; 36 | } else { 37 | throw new Error("cannot export Go (neither window nor self is defined)"); 38 | } 39 | 40 | let outputBuf = ""; 41 | global.fs = { 42 | constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused 43 | writeSync(fd, buf) { 44 | outputBuf += decoder.decode(buf); 45 | const nl = outputBuf.lastIndexOf("\n"); 46 | if (nl != -1) { 47 | console.log(outputBuf.substr(0, nl)); 48 | outputBuf = outputBuf.substr(nl + 1); 49 | } 50 | return buf.length; 51 | }, 52 | write(fd, buf, offset, length, position, callback) { 53 | if (offset !== 0 || length !== buf.length || position !== null) { 54 | throw new Error("not implemented"); 55 | } 56 | const n = this.writeSync(fd, buf); 57 | callback(null, n); 58 | }, 59 | open(path, flags, mode, callback) { 60 | const err = new Error("not implemented"); 61 | err.code = "ENOSYS"; 62 | callback(err); 63 | }, 64 | fsync(fd, callback) { 65 | callback(null); 66 | }, 67 | }; 68 | } 69 | 70 | const encoder = new TextEncoder("utf-8"); 71 | const decoder = new TextDecoder("utf-8"); 72 | var logLine = []; 73 | 74 | global.Go = class { 75 | constructor() { 76 | this._callbackTimeouts = new Map(); 77 | this._nextCallbackTimeoutID = 1; 78 | 79 | const mem = () => { 80 | // The buffer may change when requesting more memory. 81 | return new DataView(this._inst.exports.memory.buffer); 82 | } 83 | 84 | const setInt64 = (addr, v) => { 85 | mem().setUint32(addr + 0, v, true); 86 | mem().setUint32(addr + 4, Math.floor(v / 4294967296), true); 87 | } 88 | 89 | const getInt64 = (addr) => { 90 | const low = mem().getUint32(addr + 0, true); 91 | const high = mem().getInt32(addr + 4, true); 92 | return low + high * 4294967296; 93 | } 94 | 95 | const loadValue = (addr) => { 96 | const f = mem().getFloat64(addr, true); 97 | if (f === 0) { 98 | return undefined; 99 | } 100 | if (!isNaN(f)) { 101 | return f; 102 | } 103 | 104 | const id = mem().getUint32(addr, true); 105 | return this._values[id]; 106 | } 107 | 108 | const storeValue = (addr, v) => { 109 | const nanHead = 0x7FF80000; 110 | 111 | if (typeof v === "number") { 112 | if (isNaN(v)) { 113 | mem().setUint32(addr + 4, nanHead, true); 114 | mem().setUint32(addr, 0, true); 115 | return; 116 | } 117 | if (v === 0) { 118 | mem().setUint32(addr + 4, nanHead, true); 119 | mem().setUint32(addr, 1, true); 120 | return; 121 | } 122 | mem().setFloat64(addr, v, true); 123 | return; 124 | } 125 | 126 | switch (v) { 127 | case undefined: 128 | mem().setFloat64(addr, 0, true); 129 | return; 130 | case null: 131 | mem().setUint32(addr + 4, nanHead, true); 132 | mem().setUint32(addr, 2, true); 133 | return; 134 | case true: 135 | mem().setUint32(addr + 4, nanHead, true); 136 | mem().setUint32(addr, 3, true); 137 | return; 138 | case false: 139 | mem().setUint32(addr + 4, nanHead, true); 140 | mem().setUint32(addr, 4, true); 141 | return; 142 | } 143 | 144 | let ref = this._refs.get(v); 145 | if (ref === undefined) { 146 | ref = this._values.length; 147 | this._values.push(v); 148 | this._refs.set(v, ref); 149 | } 150 | let typeFlag = 0; 151 | switch (typeof v) { 152 | case "string": 153 | typeFlag = 1; 154 | break; 155 | case "symbol": 156 | typeFlag = 2; 157 | break; 158 | case "function": 159 | typeFlag = 3; 160 | break; 161 | } 162 | mem().setUint32(addr + 4, nanHead | typeFlag, true); 163 | mem().setUint32(addr, ref, true); 164 | } 165 | 166 | const loadSlice = (array, len, cap) => { 167 | return new Uint8Array(this._inst.exports.memory.buffer, array, len); 168 | } 169 | 170 | const loadSliceOfValues = (array, len, cap) => { 171 | const a = new Array(len); 172 | for (let i = 0; i < len; i++) { 173 | a[i] = loadValue(array + i * 8); 174 | } 175 | return a; 176 | } 177 | 178 | const loadString = (ptr, len) => { 179 | return decoder.decode(new DataView(this._inst.exports.memory.buffer, ptr, len)); 180 | } 181 | 182 | const timeOrigin = Date.now() - performance.now(); 183 | this.importObject = { 184 | wasi_unstable: { 185 | // https://github.com/bytecodealliance/wasmtime/blob/master/docs/WASI-api.md#__wasi_fd_write 186 | fd_write: function(fd, iovs_ptr, iovs_len, nwritten_ptr) { 187 | let nwritten = 0; 188 | if (fd == 1) { 189 | for (let iovs_i=0; iovs_i { 217 | return timeOrigin + performance.now(); 218 | }, 219 | 220 | // func sleepTicks(timeout float64) 221 | "runtime.sleepTicks": (timeout) => { 222 | // Do not sleep, only reactivate scheduler after the given timeout. 223 | setTimeout(this._inst.exports.go_scheduler, timeout); 224 | }, 225 | 226 | // func stringVal(value string) ref 227 | "syscall/js.stringVal": (ret_ptr, value_ptr, value_len) => { 228 | const s = loadString(value_ptr, value_len); 229 | storeValue(ret_ptr, s); 230 | }, 231 | 232 | // func valueGet(v ref, p string) ref 233 | "syscall/js.valueGet": (retval, v_addr, p_ptr, p_len) => { 234 | let prop = loadString(p_ptr, p_len); 235 | let value = loadValue(v_addr); 236 | let result = Reflect.get(value, prop); 237 | storeValue(retval, result); 238 | }, 239 | 240 | // func valueSet(v ref, p string, x ref) 241 | "syscall/js.valueSet": (v_addr, p_ptr, p_len, x_addr) => { 242 | const v = loadValue(v_addr); 243 | const p = loadString(p_ptr, p_len); 244 | const x = loadValue(x_addr); 245 | Reflect.set(v, p, x); 246 | }, 247 | 248 | // func valueIndex(v ref, i int) ref 249 | "syscall/js.valueIndex": (ret_addr, v_addr, i) => { 250 | storeValue(ret_addr, Reflect.get(loadValue(v_addr), i)); 251 | }, 252 | 253 | // valueSetIndex(v ref, i int, x ref) 254 | "syscall/js.valueSetIndex": (v_addr, i, x_addr) => { 255 | Reflect.set(loadValue(v_addr), i, loadValue(x_addr)); 256 | }, 257 | 258 | // func valueCall(v ref, m string, args []ref) (ref, bool) 259 | "syscall/js.valueCall": (ret_addr, v_addr, m_ptr, m_len, args_ptr, args_len, args_cap) => { 260 | const v = loadValue(v_addr); 261 | const name = loadString(m_ptr, m_len); 262 | const args = loadSliceOfValues(args_ptr, args_len, args_cap); 263 | try { 264 | const m = Reflect.get(v, name); 265 | storeValue(ret_addr, Reflect.apply(m, v, args)); 266 | mem().setUint8(ret_addr + 8, 1); 267 | } catch (err) { 268 | storeValue(ret_addr, err); 269 | mem().setUint8(ret_addr + 8, 0); 270 | } 271 | }, 272 | 273 | // func valueInvoke(v ref, args []ref) (ref, bool) 274 | "syscall/js.valueInvoke": (ret_addr, v_addr, args_ptr, args_len, args_cap) => { 275 | try { 276 | const v = loadValue(v_addr); 277 | const args = loadSliceOfValues(args_ptr, args_len, args_cap); 278 | storeValue(ret_addr, Reflect.apply(v, undefined, args)); 279 | mem().setUint8(ret_addr + 8, 1); 280 | } catch (err) { 281 | storeValue(ret_addr, err); 282 | mem().setUint8(ret_addr + 8, 0); 283 | } 284 | }, 285 | 286 | // func valueNew(v ref, args []ref) (ref, bool) 287 | "syscall/js.valueNew": (ret_addr, v_addr, args_ptr, args_len, args_cap) => { 288 | const v = loadValue(v_addr); 289 | const args = loadSliceOfValues(args_ptr, args_len, args_cap); 290 | try { 291 | storeValue(ret_addr, Reflect.construct(v, args)); 292 | mem().setUint8(ret_addr + 8, 1); 293 | } catch (err) { 294 | storeValue(ret_addr, err); 295 | mem().setUint8(ret_addr+ 8, 0); 296 | } 297 | }, 298 | 299 | // func valueLength(v ref) int 300 | "syscall/js.valueLength": (v_addr) => { 301 | return loadValue(v_addr).length; 302 | }, 303 | 304 | // valuePrepareString(v ref) (ref, int) 305 | "syscall/js.valuePrepareString": (ret_addr, v_addr) => { 306 | const s = String(loadValue(v_addr)); 307 | const str = encoder.encode(s); 308 | storeValue(ret_addr, str); 309 | setInt64(ret_addr + 8, str.length); 310 | }, 311 | 312 | // valueLoadString(v ref, b []byte) 313 | "syscall/js.valueLoadString": (v_addr, slice_ptr, slice_len, slice_cap) => { 314 | const str = loadValue(v_addr); 315 | loadSlice(slice_ptr, slice_len, slice_cap).set(str); 316 | }, 317 | 318 | // func valueInstanceOf(v ref, t ref) bool 319 | //"syscall/js.valueInstanceOf": (sp) => { 320 | // mem().setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16)); 321 | //}, 322 | } 323 | }; 324 | } 325 | 326 | async run(instance) { 327 | this._inst = instance; 328 | this._values = [ // TODO: garbage collection 329 | NaN, 330 | 0, 331 | null, 332 | true, 333 | false, 334 | global, 335 | this, 336 | ]; 337 | this._refs = new Map(); 338 | this._callbackShutdown = false; 339 | this.exited = false; 340 | 341 | const mem = new DataView(this._inst.exports.memory.buffer) 342 | 343 | while (true) { 344 | const callbackPromise = new Promise((resolve) => { 345 | this._resolveCallbackPromise = () => { 346 | if (this.exited) { 347 | throw new Error("bad callback: Go program has already exited"); 348 | } 349 | setTimeout(resolve, 0); // make sure it is asynchronous 350 | }; 351 | }); 352 | this._inst.exports._start(); 353 | if (this.exited) { 354 | break; 355 | } 356 | await callbackPromise; 357 | } 358 | } 359 | 360 | _resume() { 361 | if (this.exited) { 362 | throw new Error("Go program has already exited"); 363 | } 364 | this._inst.exports.resume(); 365 | if (this.exited) { 366 | this._resolveExitPromise(); 367 | } 368 | } 369 | 370 | _makeFuncWrapper(id) { 371 | const go = this; 372 | return function () { 373 | const event = { id: id, this: this, args: arguments }; 374 | go._pendingEvent = event; 375 | go._resume(); 376 | return event.result; 377 | }; 378 | } 379 | } 380 | 381 | if (isNodeJS) { 382 | if (process.argv.length != 3) { 383 | process.stderr.write("usage: go_js_wasm_exec [wasm binary] [arguments]\n"); 384 | process.exit(1); 385 | } 386 | 387 | const go = new Go(); 388 | WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => { 389 | process.on("exit", (code) => { // Node.js exits if no callback is pending 390 | if (code === 0 && !go.exited) { 391 | // deadlock, make Go print error and stack traces 392 | go._callbackShutdown = true; 393 | } 394 | }); 395 | return go.run(result.instance); 396 | }).catch((err) => { 397 | throw err; 398 | }); 399 | } 400 | })(); 401 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | const dir = "./docs" 10 | 11 | func main() { 12 | fs := http.FileServer(http.Dir(dir)) 13 | log.Print("Serving " + dir + " on http://localhost:8080") 14 | http.ListenAndServe(":8080", http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { 15 | resp.Header().Add("Cache-Control", "no-cache") 16 | if strings.HasSuffix(req.URL.Path, ".wasm") { 17 | resp.Header().Set("content-type", "application/wasm") 18 | } 19 | fs.ServeHTTP(resp, req) 20 | })) 21 | } 22 | -------------------------------------------------------------------------------- /wasm.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math" 5 | "sort" 6 | "strconv" 7 | "syscall/js" 8 | ) 9 | 10 | type matrix []float64 11 | 12 | type Point struct { 13 | Num int 14 | X float64 15 | Y float64 16 | Z float64 17 | } 18 | 19 | type Edge []int 20 | type Surface []int 21 | 22 | type Object struct { 23 | C string // Colour of the object 24 | P []Point 25 | E []Edge // List of points to connect by edges 26 | S []Surface // List of points to connect in order, to create a surface 27 | Mid Point // The mid point of the object. Used for calculating object draw order in a very simple way 28 | } 29 | 30 | const ( 31 | KEY_NONE int = iota 32 | KEY_MOVE_LEFT 33 | KEY_MOVE_RIGHT 34 | KEY_MOVE_UP 35 | KEY_MOVE_DOWN 36 | KEY_ROTATE_LEFT 37 | KEY_ROTATE_RIGHT 38 | KEY_ROTATE_UP 39 | KEY_ROTATE_DOWN 40 | KEY_PAGE_UP 41 | KEY_PAGE_DOWN 42 | KEY_HOME 43 | KEY_END 44 | KEY_MINUS 45 | KEY_PLUS 46 | ) 47 | 48 | type OperationType int 49 | 50 | const ( 51 | NOTHING OperationType = iota 52 | ROTATE 53 | SCALE 54 | TRANSLATE 55 | ) 56 | 57 | type paintOrder struct { 58 | midZ float64 // Z depth of an object's mid point 59 | name string 60 | } 61 | 62 | type paintOrderSlice []paintOrder 63 | 64 | func (p paintOrder) String() string { 65 | return "Name: " + p.name + ", Mid point: " + strconv.FormatFloat(p.midZ, 'f', 1, 64) 66 | } 67 | 68 | func (p paintOrderSlice) Len() int { 69 | return len(p) 70 | } 71 | 72 | func (p paintOrderSlice) Swap(i, j int) { 73 | p[i], p[j] = p[j], p[i] 74 | } 75 | 76 | func (p paintOrderSlice) Less(i, j int) bool { 77 | return p[i].midZ < p[j].midZ 78 | } 79 | 80 | const sourceURL = "https://github.com/justinclift/tinygo_canvas2" 81 | 82 | var ( 83 | // The empty world space 84 | worldSpace map[string]Object 85 | pointCounter = 1 86 | 87 | // The point objects 88 | object1 = Object{ 89 | C: "lightblue", 90 | P: []Point{ 91 | {X: 0, Y: 1.75, Z: 1.0}, // Point 0 for this object 92 | {X: 1.5, Y: -1.75, Z: 1.0}, // Point 1 for this object 93 | {X: -1.5, Y: -1.75, Z: 1.0}, 94 | {X: 0, Y: 0, Z: 1.75}, 95 | }, 96 | E: []Edge{ 97 | {0, 1}, // Connect point 0 to point 1 98 | {0, 2}, // Connect point 0 to point 2 99 | {1, 2}, // Connect point 1 to point 2 100 | {0, 3}, // etc 101 | {1, 3}, 102 | {2, 3}, 103 | }, 104 | S: []Surface{ 105 | {0, 1, 3}, 106 | {0, 2, 3}, 107 | {0, 1, 2}, 108 | {1, 2, 3}, 109 | }, 110 | } 111 | object2 = Object{ 112 | C: "lightgreen", 113 | P: []Point{ 114 | {X: 1.5, Y: 1.5, Z: -1.0}, // Point 0 for this object 115 | {X: 1.5, Y: -1.5, Z: -1.0}, // Point 1 for this object 116 | {X: -1.5, Y: -1.5, Z: -1.0}, 117 | }, 118 | E: []Edge{ 119 | {0, 1}, // Connect point 0 to point 1 120 | {1, 2}, // Connect point 1 to point 2 121 | {2, 0}, // etc 122 | }, 123 | S: []Surface{ 124 | {0, 1, 2}, 125 | }, 126 | } 127 | object3 = Object{ 128 | C: "indianred", 129 | P: []Point{ 130 | {X: 2, Y: -2, Z: 1.0}, 131 | {X: 2, Y: -4, Z: 1.0}, 132 | {X: -2, Y: -4, Z: 1.0}, 133 | {X: -2, Y: -2, Z: 1.0}, 134 | {X: 0, Y: -3, Z: 2.5}, 135 | }, 136 | E: []Edge{ 137 | {0, 1}, 138 | {1, 2}, 139 | {2, 3}, 140 | {3, 0}, 141 | {0, 4}, 142 | {1, 4}, 143 | {2, 4}, 144 | {3, 4}, 145 | }, 146 | S: []Surface{ 147 | {0, 1, 4}, 148 | {1, 2, 4}, 149 | {2, 3, 4}, 150 | {3, 0, 4}, 151 | {0, 1, 2, 3}, 152 | }, 153 | } 154 | 155 | // The 4x4 identity matrix 156 | identityMatrix = matrix{ 157 | 1, 0, 0, 0, 158 | 0, 1, 0, 0, 159 | 0, 0, 1, 0, 160 | 0, 0, 0, 1, 161 | } 162 | 163 | // Initialise the transform matrix with the identity matrix 164 | transformMatrix = identityMatrix 165 | 166 | canvasEl, ctx, doc js.Value 167 | graphWidth float64 168 | graphHeight float64 169 | width, height float64 170 | opText string 171 | highLightSource bool 172 | stepSize = float64(15) 173 | 174 | // Queue operations 175 | prevKey int 176 | queueOp OperationType 177 | queueParts int32 178 | 179 | debug = false 180 | ) 181 | 182 | func main() { 183 | width := js.Global().Get("innerWidth").Int() 184 | height := js.Global().Get("innerHeight").Int() 185 | doc = js.Global().Get("document") 186 | canvasEl = doc.Call("getElementById", "mycanvas") 187 | canvasEl.Call("setAttribute", "width", width) 188 | canvasEl.Call("setAttribute", "height", height) 189 | canvasEl.Set("tabIndex", 0) // Not sure if this is needed 190 | ctx = canvasEl.Call("getContext", "2d") 191 | 192 | // Add some objects to the world space 193 | worldSpace = make(map[string]Object, 1) 194 | worldSpace["ob1"] = importObject(object1, 5.0, 3.0, 0.0) 195 | worldSpace["ob1 copy"] = importObject(object1, -1.0, 3.0, 0.0) 196 | worldSpace["ob2"] = importObject(object2, 5.0, -3.0, 1.0) 197 | worldSpace["ob3"] = importObject(object3, -1.0, 0.0, -1.0) 198 | 199 | // Scale them up a bit 200 | queueOp = SCALE 201 | queueParts = 1 202 | transformMatrix = scale(transformMatrix, 2.0, 2.0, 2.0) 203 | applyTransformation() 204 | 205 | // Start a rotation going 206 | setUpOperation(ROTATE, 50, 12, stepSize, stepSize, stepSize) 207 | 208 | // Start the frame renderer 209 | js.Global().Call("requestAnimationFrame", js.Global().Get("renderFrame")) 210 | } 211 | 212 | // Apply each transformation, one small part at a time (this gives the animation effect) 213 | //go:export applyTransformation 214 | func applyTransformation() { 215 | if (queueParts < 1 && queueOp == SCALE) || queueOp == NOTHING { 216 | opText = "Complete." 217 | return 218 | } 219 | 220 | for j, o := range worldSpace { 221 | var newPoints []Point 222 | // Transform each point in the object 223 | for _, j := range o.P { 224 | newPoints = append(newPoints, transform(transformMatrix, j)) 225 | } 226 | o.P = newPoints 227 | 228 | // Transform the mid point of the object. In theory, this should mean the mid point can always be used 229 | // for a simple (not-cpu-intensive) way to sort the objects in Z depth order 230 | o.Mid = transform(transformMatrix, o.Mid) 231 | 232 | // Update the object in world space 233 | worldSpace[j] = o 234 | } 235 | 236 | queueParts-- 237 | } 238 | 239 | // Simple mouse handler watching for people clicking on the source code link 240 | //go:export clickHandler 241 | func clickHandler(cx int, cy int) { 242 | clientX := float64(cx) 243 | clientY := float64(cy) 244 | if debug { 245 | println("ClientX: " + strconv.FormatFloat(clientX, 'f', 0, 64) + " clientY: " + strconv.FormatFloat(clientY, 'f', 0, 64)) 246 | if clientX > graphWidth && clientY > (float64(height)-40) { 247 | println("URL hit!") 248 | } 249 | } 250 | 251 | // If the user clicks the source code URL area, open the URL 252 | if clientX > graphWidth && clientY > (float64(height)-40) { 253 | w := js.Global().Call("open", sourceURL) 254 | if w == js.Null() { 255 | // Couldn't open a new window, so try loading directly in the existing one instead 256 | doc.Set("location", sourceURL) 257 | } 258 | } 259 | } 260 | 261 | // Simple keyboard handler for catching the arrow, WASD, and numpad keys 262 | // Key value info can be found here: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values 263 | //go:export keyPressHandler 264 | func keyPressHandler(keyVal int) { 265 | if debug { 266 | println("Key is: " + strconv.Itoa(keyVal)) 267 | } 268 | 269 | // If a key is pressed for a 2nd time in a row, then stop the animated movement 270 | if keyVal == prevKey && queueOp != NOTHING { 271 | queueOp = NOTHING 272 | return 273 | } 274 | 275 | // The the plus or minus keys were pressed, increase the step size then cause the current operation to be recalculated 276 | switch keyVal { 277 | case KEY_MINUS: 278 | stepSize -= 5.0 279 | keyVal = prevKey 280 | case KEY_PLUS: 281 | stepSize += 5.0 282 | keyVal = prevKey 283 | } 284 | 285 | // Set up translate and rotate operations 286 | switch keyVal { 287 | case KEY_MOVE_LEFT: 288 | setUpOperation(TRANSLATE, 50, 12, stepSize/2, 0, 0) 289 | case KEY_MOVE_RIGHT: 290 | setUpOperation(TRANSLATE, 50, 12, -stepSize/2, 0, 0) 291 | case KEY_MOVE_UP: 292 | setUpOperation(TRANSLATE, 50, 12, 0, stepSize/2, 0) 293 | case KEY_MOVE_DOWN: 294 | setUpOperation(TRANSLATE, 50, 12, 0, -stepSize/2, 0) 295 | case KEY_ROTATE_LEFT: 296 | setUpOperation(ROTATE, 50, 12, 0, -stepSize, 0) 297 | case KEY_ROTATE_RIGHT: 298 | setUpOperation(ROTATE, 50, 12, 0, stepSize, 0) 299 | case KEY_ROTATE_UP: 300 | setUpOperation(ROTATE, 50, 12, -stepSize, 0, 0) 301 | case KEY_ROTATE_DOWN: 302 | setUpOperation(ROTATE, 50, 12, stepSize, 0, 0) 303 | case KEY_PAGE_UP: 304 | setUpOperation(ROTATE, 50, 12, -stepSize, stepSize, 0) 305 | case KEY_PAGE_DOWN: 306 | setUpOperation(ROTATE, 50, 12, stepSize, stepSize, 0) 307 | case KEY_HOME: 308 | setUpOperation(ROTATE, 50, 12, -stepSize, -stepSize, 0) 309 | case KEY_END: 310 | setUpOperation(ROTATE, 50, 12, stepSize, -stepSize, 0) 311 | } 312 | prevKey = keyVal 313 | } 314 | 315 | // Simple mouse handler watching for people moving the mouse over the source code link 316 | //go:export moveHandler 317 | func moveHandler(cx int, cy int) { 318 | clientX := float64(cx) 319 | clientY := float64(cy) 320 | if debug { 321 | println("ClientX: " + strconv.FormatFloat(clientX, 'f', 0, 64) + " clientY: " + strconv.FormatFloat(clientY, 'f', 0, 64)) 322 | } 323 | 324 | // If the mouse is over the source code link, let the frame renderer know to draw the url in bold 325 | if clientX > graphWidth && clientY > (float64(height)-40) { 326 | highLightSource = true 327 | } else { 328 | highLightSource = false 329 | } 330 | } 331 | 332 | // Renders one frame of the animation 333 | //go:export renderFrame 334 | func renderFrame() { 335 | // Handle window resizing 336 | curBodyW := js.Global().Get("innerWidth").Float() 337 | curBodyH := js.Global().Get("innerHeight").Float() 338 | if curBodyW != width || curBodyH != height { 339 | width, height = curBodyW, curBodyH 340 | canvasEl.Set("width", width) 341 | canvasEl.Set("height", height) 342 | } 343 | 344 | // Setup useful variables 345 | border := float64(2) 346 | gap := float64(3) 347 | left := border + gap 348 | top := border + gap 349 | graphWidth = float64(width) * 0.75 350 | graphHeight = float64(height) - 1 351 | centerX := graphWidth / 2 352 | centerY := graphHeight / 2 353 | 354 | // Clear the background 355 | ctx.Set("fillStyle", "white") 356 | ctx.Call("fillRect", 0, 0, width, height) 357 | 358 | // Save the current graphics state - no clip region currently defined - as the default 359 | ctx.Call("save") 360 | 361 | // Set the clip region so drawing only occurs in the display area 362 | ctx.Call("beginPath") 363 | ctx.Call("moveTo", 0, 0) 364 | ctx.Call("lineTo", graphWidth, 0) 365 | ctx.Call("lineTo", graphWidth, height) 366 | ctx.Call("lineTo", 0, height) 367 | ctx.Call("clip") 368 | 369 | // Draw grid lines 370 | step := math.Min(float64(width), float64(height)) / float64(30) 371 | ctx.Set("strokeStyle", "rgb(220, 220, 220)") 372 | for i := left; i < graphWidth-step; i += step { 373 | // Vertical dashed lines 374 | ctx.Call("beginPath") 375 | ctx.Call("moveTo", i+step, top) 376 | ctx.Call("lineTo", i+step, graphHeight) 377 | ctx.Call("stroke") 378 | } 379 | for i := top; i < graphHeight-step; i += step { 380 | // Horizontal dashed lines 381 | ctx.Call("beginPath") 382 | ctx.Call("moveTo", left, i+step) 383 | ctx.Call("lineTo", graphWidth-border, i+step) 384 | ctx.Call("stroke") 385 | } 386 | 387 | // Sort the objects by mid point Z depth order 388 | var order paintOrderSlice 389 | for i, j := range worldSpace { 390 | order = append(order, paintOrder{name: i, midZ: j.Mid.Z}) 391 | } 392 | sort.Sort(paintOrderSlice(order)) 393 | 394 | // Draw the objects, in Z depth order 395 | var pointX, pointY float64 396 | numWld := len(worldSpace) 397 | for i := 0; i < numWld; i++ { 398 | o := worldSpace[order[i].name] 399 | 400 | // Draw the surfaces 401 | ctx.Set("fillStyle", o.C) 402 | for _, l := range o.S { 403 | for m, n := range l { 404 | pointX = o.P[n].X 405 | pointY = o.P[n].Y 406 | if m == 0 { 407 | ctx.Call("beginPath") 408 | ctx.Call("moveTo", centerX+(pointX*step), centerY+((pointY*step)*-1)) 409 | } else { 410 | ctx.Call("lineTo", centerX+(pointX*step), centerY+((pointY*step)*-1)) 411 | } 412 | } 413 | ctx.Call("closePath") 414 | ctx.Call("fill") 415 | } 416 | 417 | // Draw the edges 418 | ctx.Set("strokeStyle", "black") 419 | ctx.Set("fillStyle", "black") 420 | ctx.Set("lineWidth", "1") 421 | var point1X, point1Y, point2X, point2Y float64 422 | for _, l := range o.E { 423 | point1X = o.P[l[0]].X 424 | point1Y = o.P[l[0]].Y 425 | point2X = o.P[l[1]].X 426 | point2Y = o.P[l[1]].Y 427 | ctx.Call("beginPath") 428 | ctx.Call("moveTo", centerX+(point1X*step), centerY+((point1Y*step)*-1)) 429 | ctx.Call("lineTo", centerX+(point2X*step), centerY+((point2Y*step)*-1)) 430 | ctx.Call("stroke") 431 | } 432 | 433 | // Draw the points on the graph 434 | var px, py float64 435 | for _, l := range o.P { 436 | px = centerX + (l.X * step) 437 | py = centerY + ((l.Y * step) * -1) 438 | ctx.Call("beginPath") 439 | ctx.Call("arc", px, py, 1, 0, 2*math.Pi) 440 | ctx.Call("fill") 441 | } 442 | } 443 | 444 | // Set the clip region so drawing only occurs in the display area 445 | ctx.Call("restore") 446 | ctx.Call("save") 447 | ctx.Call("beginPath") 448 | ctx.Call("moveTo", graphWidth, 0) 449 | ctx.Call("lineTo", width, 0) 450 | ctx.Call("lineTo", width, height) 451 | ctx.Call("lineTo", graphWidth, height) 452 | ctx.Call("clip") 453 | 454 | // Draw the text describing the current operation 455 | textY := top + 20 456 | ctx.Set("fillStyle", "black") 457 | ctx.Set("font", "bold 14px serif") 458 | ctx.Call("fillText", "Operation:", graphWidth+20, textY) 459 | textY += 20 460 | ctx.Set("font", "14px sans-serif") 461 | ctx.Call("fillText", opText, graphWidth+20, textY) 462 | textY += 30 463 | 464 | // Add the help text about control keys and mouse zoom 465 | ctx.Set("fillStyle", "blue") 466 | ctx.Set("font", "14px sans-serif") 467 | ctx.Call("fillText", "Use wasd to move, numpad keys", graphWidth+20, textY) 468 | textY += 20 469 | ctx.Call("fillText", "to rotate, mouse wheel to zoom.", graphWidth+20, textY) 470 | textY += 30 471 | ctx.Call("fillText", "+ and - keys to change speed.", graphWidth+20, textY) 472 | textY += 30 473 | ctx.Call("fillText", "Press a key a 2nd time to", graphWidth+20, textY) 474 | textY += 20 475 | ctx.Call("fillText", "stop the current change.", graphWidth+20, textY) 476 | textY += 40 477 | 478 | // Clear the source code link area 479 | ctx.Set("fillStyle", "white") 480 | ctx.Call("fillRect", graphWidth+1, graphHeight-55, width, height) 481 | 482 | // Add the URL to the source code 483 | ctx.Set("fillStyle", "black") 484 | ctx.Set("font", "bold 14px serif") 485 | ctx.Call("fillText", "Source code:", graphWidth+20, graphHeight-35) 486 | ctx.Set("fillStyle", "blue") 487 | if highLightSource == true { 488 | ctx.Set("font", "bold 12px sans-serif") 489 | } else { 490 | ctx.Set("font", "12px sans-serif") 491 | } 492 | ctx.Call("fillText", sourceURL, graphWidth+20, graphHeight-15) 493 | 494 | // Draw a border around the graph area 495 | ctx.Set("lineWidth", "2") 496 | ctx.Set("strokeStyle", "white") 497 | ctx.Call("beginPath") 498 | ctx.Call("moveTo", 0, 0) 499 | ctx.Call("lineTo", width, 0) 500 | ctx.Call("lineTo", width, height) 501 | ctx.Call("lineTo", 0, height) 502 | ctx.Call("closePath") 503 | ctx.Call("stroke") 504 | ctx.Set("lineWidth", "2") 505 | ctx.Set("strokeStyle", "black") 506 | ctx.Call("beginPath") 507 | ctx.Call("moveTo", border, border) 508 | ctx.Call("lineTo", graphWidth, border) 509 | ctx.Call("lineTo", graphWidth, graphHeight) 510 | ctx.Call("lineTo", border, graphHeight) 511 | ctx.Call("closePath") 512 | ctx.Call("stroke") 513 | 514 | // Restore the default graphics state (eg no clip region) 515 | ctx.Call("restore") 516 | 517 | // Keep the frame rendering going 518 | js.Global().Call("requestAnimationFrame", js.Global().Get("renderFrame")) 519 | } 520 | 521 | // Simple mouse handler watching for mouse wheel events 522 | // Reference info can be found here: https://developer.mozilla.org/en-US/docs/Web/Events/wheel 523 | //go:export wheelHandler 524 | func wheelHandler(val int32) { 525 | wheelDelta := int64(val) 526 | scaleSize := 1 + (float64(wheelDelta) / 5) 527 | if debug { 528 | println("Wheel delta: " + strconv.FormatInt(wheelDelta, 10) + " scaleSize: " + strconv.FormatFloat(scaleSize, 'f', 1, 64) + "\n") 529 | } 530 | setUpOperation(SCALE, 50, 12, scaleSize, scaleSize, scaleSize) 531 | prevKey = KEY_NONE 532 | } 533 | 534 | // Returns an object whose points have been transformed into 3D world space XYZ co-ordinates. Also assigns a number 535 | // to each point 536 | func importObject(ob Object, x float64, y float64, z float64) (translatedObject Object) { 537 | // X and Y translation matrix. Translates the objects into the world space at the given X and Y co-ordinates 538 | translateMatrix := matrix{ 539 | 1, 0, 0, x, 540 | 0, 1, 0, y, 541 | 0, 0, 1, z, 542 | 0, 0, 0, 1, 543 | } 544 | 545 | // Translate the points 546 | var midX, midY, midZ float64 547 | var pt Point 548 | for _, j := range ob.P { 549 | pt = Point{ 550 | Num: pointCounter, 551 | X: (translateMatrix[0] * j.X) + (translateMatrix[1] * j.Y) + (translateMatrix[2] * j.Z) + (translateMatrix[3] * 1), // 1st col, top 552 | Y: (translateMatrix[4] * j.X) + (translateMatrix[5] * j.Y) + (translateMatrix[6] * j.Z) + (translateMatrix[7] * 1), // 1st col, upper middle 553 | Z: (translateMatrix[8] * j.X) + (translateMatrix[9] * j.Y) + (translateMatrix[10] * j.Z) + (translateMatrix[11] * 1), // 1st col, lower middle 554 | } 555 | translatedObject.P = append(translatedObject.P, pt) 556 | midX += pt.X 557 | midY += pt.Y 558 | midZ += pt.Z 559 | pointCounter++ 560 | } 561 | 562 | // Determine the mid point for the object 563 | numPts := float64(len(ob.P)) 564 | translatedObject.Mid.X = midX / numPts 565 | translatedObject.Mid.Y = midY / numPts 566 | translatedObject.Mid.Z = midZ / numPts 567 | 568 | // Copy the colour, edge, and surface definitions across 569 | translatedObject.C = ob.C 570 | for _, j := range ob.E { 571 | translatedObject.E = append(translatedObject.E, j) 572 | } 573 | for _, j := range ob.S { 574 | translatedObject.S = append(translatedObject.S, j) 575 | } 576 | 577 | return translatedObject 578 | } 579 | 580 | // Multiplies one matrix by another 581 | func matrixMult(opMatrix matrix, m matrix) (resultMatrix matrix) { 582 | top0 := m[0] 583 | top1 := m[1] 584 | top2 := m[2] 585 | top3 := m[3] 586 | upperMid0 := m[4] 587 | upperMid1 := m[5] 588 | upperMid2 := m[6] 589 | upperMid3 := m[7] 590 | lowerMid0 := m[8] 591 | lowerMid1 := m[9] 592 | lowerMid2 := m[10] 593 | lowerMid3 := m[11] 594 | bot0 := m[12] 595 | bot1 := m[13] 596 | bot2 := m[14] 597 | bot3 := m[15] 598 | 599 | resultMatrix = matrix{ 600 | (opMatrix[0] * top0) + (opMatrix[1] * upperMid0) + (opMatrix[2] * lowerMid0) + (opMatrix[3] * bot0), // 1st col, top 601 | (opMatrix[0] * top1) + (opMatrix[1] * upperMid1) + (opMatrix[2] * lowerMid1) + (opMatrix[3] * bot1), // 2nd col, top 602 | (opMatrix[0] * top2) + (opMatrix[1] * upperMid2) + (opMatrix[2] * lowerMid2) + (opMatrix[3] * bot2), // 3rd col, top 603 | (opMatrix[0] * top3) + (opMatrix[1] * upperMid3) + (opMatrix[2] * lowerMid3) + (opMatrix[3] * bot3), // 4th col, top 604 | 605 | (opMatrix[4] * top0) + (opMatrix[5] * upperMid0) + (opMatrix[6] * lowerMid0) + (opMatrix[7] * bot0), // 1st col, upper middle 606 | (opMatrix[4] * top1) + (opMatrix[5] * upperMid1) + (opMatrix[6] * lowerMid1) + (opMatrix[7] * bot1), // 2nd col, upper middle 607 | (opMatrix[4] * top2) + (opMatrix[5] * upperMid2) + (opMatrix[6] * lowerMid2) + (opMatrix[7] * bot2), // 3rd col, upper middle 608 | (opMatrix[4] * top3) + (opMatrix[5] * upperMid3) + (opMatrix[6] * lowerMid3) + (opMatrix[7] * bot3), // 4th col, upper middle 609 | 610 | (opMatrix[8] * top0) + (opMatrix[9] * upperMid0) + (opMatrix[10] * lowerMid0) + (opMatrix[11] * bot0), // 1st col, lower middle 611 | (opMatrix[8] * top1) + (opMatrix[9] * upperMid1) + (opMatrix[10] * lowerMid1) + (opMatrix[11] * bot1), // 2nd col, lower middle 612 | (opMatrix[8] * top2) + (opMatrix[9] * upperMid2) + (opMatrix[10] * lowerMid2) + (opMatrix[11] * bot2), // 3rd col, lower middle 613 | (opMatrix[8] * top3) + (opMatrix[9] * upperMid3) + (opMatrix[10] * lowerMid3) + (opMatrix[11] * bot3), // 4th col, lower middle 614 | 615 | (opMatrix[12] * top0) + (opMatrix[13] * upperMid0) + (opMatrix[14] * lowerMid0) + (opMatrix[15] * bot0), // 1st col, bottom 616 | (opMatrix[12] * top1) + (opMatrix[13] * upperMid1) + (opMatrix[14] * lowerMid1) + (opMatrix[15] * bot1), // 2nd col, bottom 617 | (opMatrix[12] * top2) + (opMatrix[13] * upperMid2) + (opMatrix[14] * lowerMid2) + (opMatrix[15] * bot2), // 3rd col, bottom 618 | (opMatrix[12] * top3) + (opMatrix[13] * upperMid3) + (opMatrix[14] * lowerMid3) + (opMatrix[15] * bot3), // 4th col, bottom 619 | } 620 | return resultMatrix 621 | } 622 | 623 | // Rotates a transformation matrix around the X axis by the given degrees 624 | func rotateAroundX(m matrix, degrees float64) matrix { 625 | rad := (math.Pi / 180) * degrees // The Go math functions use radians, so we convert degrees to radians 626 | rotateXMatrix := matrix{ 627 | 1, 0, 0, 0, 628 | 0, math.Cos(rad), -math.Sin(rad), 0, 629 | 0, math.Sin(rad), math.Cos(rad), 0, 630 | 0, 0, 0, 1, 631 | } 632 | return matrixMult(rotateXMatrix, m) 633 | } 634 | 635 | // Rotates a transformation matrix around the Y axis by the given degrees 636 | func rotateAroundY(m matrix, degrees float64) matrix { 637 | rad := (math.Pi / 180) * degrees // The Go math functions use radians, so we convert degrees to radians 638 | rotateYMatrix := matrix{ 639 | math.Cos(rad), 0, math.Sin(rad), 0, 640 | 0, 1, 0, 0, 641 | -math.Sin(rad), 0, math.Cos(rad), 0, 642 | 0, 0, 0, 1, 643 | } 644 | return matrixMult(rotateYMatrix, m) 645 | } 646 | 647 | // Rotates a transformation matrix around the Z axis by the given degrees 648 | func rotateAroundZ(m matrix, degrees float64) matrix { 649 | rad := (math.Pi / 180) * degrees // The Go math functions use radians, so we convert degrees to radians 650 | rotateZMatrix := matrix{ 651 | math.Cos(rad), -math.Sin(rad), 0, 0, 652 | math.Sin(rad), math.Cos(rad), 0, 0, 653 | 0, 0, 1, 0, 654 | 0, 0, 0, 1, 655 | } 656 | return matrixMult(rotateZMatrix, m) 657 | } 658 | 659 | // Scales a transformation matrix by the given X, Y, and Z values 660 | func scale(m matrix, x float64, y float64, z float64) matrix { 661 | scaleMatrix := matrix{ 662 | x, 0, 0, 0, 663 | 0, y, 0, 0, 664 | 0, 0, z, 0, 665 | 0, 0, 0, 1, 666 | } 667 | return matrixMult(scaleMatrix, m) 668 | } 669 | 670 | // Set up the details for the transformation operation 671 | func setUpOperation(op OperationType, t int32, f int32, X float64, Y float64, Z float64) { 672 | queueParts = f // Number of parts to break each transformation into 673 | transformMatrix = identityMatrix // Reset the transform matrix 674 | switch op { 675 | case ROTATE: // Rotate the objects in world space 676 | // Divide the desired angle into a small number of parts 677 | if X != 0 { 678 | transformMatrix = rotateAroundX(transformMatrix, X/float64(queueParts)) 679 | } 680 | if Y != 0 { 681 | transformMatrix = rotateAroundY(transformMatrix, Y/float64(queueParts)) 682 | } 683 | if Z != 0 { 684 | transformMatrix = rotateAroundZ(transformMatrix, Z/float64(queueParts)) 685 | } 686 | opText = "Rotation. X: " + strconv.FormatFloat(X, 'f', 0, 64) + " Y: " + strconv.FormatFloat(Y, 'f', 0, 64) + " Z: " + strconv.FormatFloat(Z, 'f', 0, 64) 687 | 688 | case SCALE: 689 | // Scale the objects in world space 690 | var xPart, yPart, zPart float64 691 | if X != 1 { 692 | xPart = ((X - 1) / float64(queueParts)) + 1 693 | } 694 | if Y != 1 { 695 | yPart = ((Y - 1) / float64(queueParts)) + 1 696 | } 697 | if Z != 1 { 698 | zPart = ((Z - 1) / float64(queueParts)) + 1 699 | } 700 | transformMatrix = scale(transformMatrix, xPart, yPart, zPart) 701 | opText = "Scale. X: " + strconv.FormatFloat(X, 'f', 0, 64) + " Y: " + strconv.FormatFloat(Y, 'f', 0, 64) + " Z: " + strconv.FormatFloat(Z, 'f', 0, 64) 702 | 703 | case TRANSLATE: 704 | // Translate (move) the objects in world space 705 | transformMatrix = translate(transformMatrix, X/float64(queueParts), Y/float64(queueParts), Z/float64(queueParts)) 706 | opText = "Translate. X: " + strconv.FormatFloat(X, 'f', 0, 64) + " Y: " + strconv.FormatFloat(Y, 'f', 0, 64) + " Z: " + strconv.FormatFloat(Z, 'f', 0, 64) 707 | } 708 | queueOp = op 709 | } 710 | 711 | // Transform the XYZ co-ordinates using the values from the transformation matrix 712 | func transform(m matrix, p Point) (t Point) { 713 | top0 := m[0] 714 | top1 := m[1] 715 | top2 := m[2] 716 | top3 := m[3] 717 | upperMid0 := m[4] 718 | upperMid1 := m[5] 719 | upperMid2 := m[6] 720 | upperMid3 := m[7] 721 | lowerMid0 := m[8] 722 | lowerMid1 := m[9] 723 | lowerMid2 := m[10] 724 | lowerMid3 := m[11] 725 | //bot0 := m[12] // The fourth row values can be ignored for 3D matrices 726 | //bot1 := m[13] 727 | //bot2 := m[14] 728 | //bot3 := m[15] 729 | 730 | t.Num = p.Num 731 | t.X = (top0 * p.X) + (top1 * p.Y) + (top2 * p.Z) + top3 732 | t.Y = (upperMid0 * p.X) + (upperMid1 * p.Y) + (upperMid2 * p.Z) + upperMid3 733 | t.Z = (lowerMid0 * p.X) + (lowerMid1 * p.Y) + (lowerMid2 * p.Z) + lowerMid3 734 | return 735 | } 736 | 737 | // Translates (moves) a transformation matrix by the given X, Y and Z values 738 | func translate(m matrix, translateX float64, translateY float64, translateZ float64) matrix { 739 | translateMatrix := matrix{ 740 | 1, 0, 0, translateX, 741 | 0, 1, 0, translateY, 742 | 0, 0, 1, translateZ, 743 | 0, 0, 0, 1, 744 | } 745 | return matrixMult(translateMatrix, m) 746 | } 747 | --------------------------------------------------------------------------------