├── .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 |
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 |
--------------------------------------------------------------------------------