├── favicon.ico ├── params.json ├── LICENSE.txt ├── assets ├── style.css ├── setup.js ├── control.js └── main.js ├── index.html └── README.md /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jomo/isopaint/HEAD/favicon.ico -------------------------------------------------------------------------------- /params.json: -------------------------------------------------------------------------------- 1 | {"name":"jomo.github.io","tagline":"","body":"","google":"","note":"Don't delete this file! It's used internally to help with page regeneration."} -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. -------------------------------------------------------------------------------- /assets/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | font-size: 0; 5 | font-family: "Helvetica Neue", Arial, sans-serif; 6 | background: #49f; 7 | overflow: hidden; 8 | } 9 | 10 | a.forkme { 11 | top: 0; 12 | right: 0; 13 | color: #fff; 14 | border: 2px solid #006400; 15 | z-index: 10; 16 | padding: 3px 40px; 17 | display: inline-block; 18 | position: fixed; 19 | font-size: 14px; 20 | background: #008000; 21 | box-shadow: 0 0 5px #000; 22 | font-weight: bold; 23 | text-decoration: none; 24 | -webkit-transform: rotate(45deg) translate(65px); 25 | transform: rotate(45deg) translate(65px); 26 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | jomo – Isometric Painting Tool 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Fork me on GitHub 14 | Please enable JavaScript :) 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Isometric Painting Tool 2 | 3 | Allows you to navigate and paint on canvas using a cube in an isometric world. 4 | 5 | This was a quick weekend hack and it's more of a demo than an actual thing, but it can be fun to use anyway. 6 | 7 | # Share your stuff 8 | 9 | Made something cool? :sunglasses: 10 | Add the link to [the wiki](https://github.com/jomo/isopaint/wiki)! 11 | 12 | 13 | # Demo 14 | 15 | [![screenshot](https://i.imgur.com/lB5fa2B.png)
Click here to open](https://jomo.tv/isopaint/#,,-1,,,-2,-1,,,-2,,,,-1,,,-2,,1,,,2,,,,1,,,2,,,,1,,,2,,-5,,,-6,,,-7,,1,-7,,2,-7,,,-7,-1,,-7,-2,-5,,,-6,,,-7,,,-7,,1,-7,,2,-7,1,,-7,2,,,5,,,6,,,7,,-1,7,,-2,7,,,7,1,,7,2,,,-5,,,-6,,,-7,,-1,-7,,-2,-7,1,,-7,2,,-7,,,5,,,6,,,7,-1,,7,-2,,7,,1,7,,2,7,5,,,6,,,7,,,7,-1,,7,-2,,7,,-1,7,,-2,,,) 16 | 17 | As you can see, you can create [isometric illusions](https://en.wikipedia.org/wiki/Impossible_object) with ease, and you can share creations via link as well. 18 | 19 | # Usage 20 | 21 | 22 | - (Shift) Arrow Keys → navigate 23 | - Scroll → Scale 24 | - Space → toggle painting 25 | - f → flip view 26 | - d → delete block 27 | - r → reset -------------------------------------------------------------------------------- /assets/setup.js: -------------------------------------------------------------------------------- 1 | var dp = window.devicePixelRatio || 1; 2 | 3 | var canvas = document.querySelector("canvas"); 4 | 5 | (window.updateSize = function() { 6 | canvas.width = dp * innerWidth; 7 | canvas.height = dp * innerHeight; 8 | canvas.style.width = innerWidth + "px"; 9 | canvas.style.height = innerHeight + "px"; 10 | })(); 11 | 12 | // setup 13 | var steps = []; 14 | var draw = []; 15 | var painting = true; 16 | var flip = 1; 17 | 18 | // skew_a / skew_b = 0.5 19 | // don't change for true isometric view 20 | var skew_a = 0.577777777; 21 | var skew_b = 1.155555555; 22 | 23 | // 1 coord = 20px 24 | var scale = 20; 25 | 26 | // shift canvas (used to center 0,0,0) 27 | var shiftx = canvas.width / 2; 28 | var shifty = canvas.height / 2; 29 | 30 | // cube position 31 | var x = 0; 32 | var y = 0; 33 | var z = 0; 34 | 35 | var ctx = canvas.getContext("2d"); 36 | ctx.font = dp * 15 + "px 'Helvetica Neue', Arial, sans-serif"; 37 | 38 | window.onresize = function() { 39 | updateSize(); 40 | shiftx = canvas.width / 2; 41 | shifty = canvas.height / 2; 42 | ctx.font = dp * 15 + "px 'Helvetica Neue', Arial, sans-serif"; 43 | render(); 44 | }; 45 | -------------------------------------------------------------------------------- /assets/control.js: -------------------------------------------------------------------------------- 1 | document.onwheel = function(event) { 2 | var newscale = Math.round(scale - event.deltaY); 3 | if (newscale < 1) { 4 | scale = 1; 5 | } else if (newscale > 150) { 6 | scale = 150; 7 | } else { 8 | scale = newscale; 9 | } 10 | render(); 11 | }; 12 | 13 | // Listen to keys 14 | document.onkeydown = function(event) { 15 | // push current position to steps 16 | var push = true; 17 | 18 | if (!steps.length && painting) { 19 | // first step 20 | steps.push([x, y, z]); 21 | } 22 | 23 | switch (event.keyCode) { 24 | case 32: 25 | painting = !painting; 26 | break; 27 | case 38: 28 | if (event.shiftKey) { 29 | z--; 30 | } else { 31 | y++; 32 | } 33 | break; 34 | case 40: 35 | if (event.shiftKey) { 36 | z++; 37 | } else { 38 | y--; 39 | } 40 | break; 41 | case 37: 42 | x--; 43 | break; 44 | case 39: 45 | x++; 46 | break; 47 | case 68: 48 | push = false; 49 | del(); 50 | break; 51 | case 70: 52 | flip *= -1; 53 | break; 54 | case 82: 55 | push = false; 56 | steps = []; 57 | painting = true; 58 | scale = 20; 59 | x = z = y = 0; 60 | break; 61 | default: 62 | return; 63 | } 64 | 65 | if (painting && push) { 66 | steps.push([x, y, z]); 67 | } 68 | 69 | render(); 70 | history.replaceState({}, document.title, location.protocol + "//" + location.host + location.pathname + "#" + gethash()); 71 | }; -------------------------------------------------------------------------------- /assets/main.js: -------------------------------------------------------------------------------- 1 | // get list of steps to be placed in location.hash 2 | function gethash() { 3 | var hash = []; 4 | for (var i = 0; i < steps.length; i++) { 5 | hash.push(steps[i].join(",")); 6 | } 7 | // remove 0s 8 | return hash.join(",").replace(/(^0)?,0/g, ","); 9 | } 10 | 11 | // parse location.hash into a list of steps 12 | function parsehash() { 13 | var hash = location.hash.split(","); 14 | if (hash.length % 3 !== 0) { 15 | console.warn("Invalid hash"); 16 | } else { 17 | var list = []; 18 | 19 | for (var i = 0; i < hash.length; i += 3) { 20 | var posx = flip * Number(hash[i + 0]) || 0; 21 | var posy = flip * Number(hash[i + 1]) || 0; 22 | var posz = Number(hash[i + 2]) || 0; 23 | list.push([posx, posy, posz]); 24 | 25 | if (i === hash.length - 3) { 26 | x = posx; 27 | y = posy; 28 | z = posz; 29 | } 30 | } 31 | 32 | return list; 33 | } 34 | } 35 | 36 | function processSteps() { 37 | // unique 38 | var unique = {}; 39 | steps = steps.filter(function(item) { 40 | return unique.hasOwnProperty(item) ? false : unique[item] = true; 41 | }); 42 | 43 | // clone steps 44 | draw = steps.slice(0); 45 | // we draw all steps + current position 46 | draw.push([x, y, z]); 47 | // render by order: y, x, z 48 | // avoids falsely overlapping 49 | draw = draw.sort(function(a, b) { 50 | if (flip === 1) { 51 | return b[1] - a[1] || a[0] - b[0] || b[2] - a[2]; 52 | } else { 53 | return a[1] - b[1] || b[0] - a[0] || b[2] - a[2]; 54 | } 55 | }); 56 | } 57 | 58 | function printInfo() { 59 | ctx.setTransform(1, 0, 0, 1, 0, 0); 60 | 61 | ctx.fillStyle = "#fff"; 62 | ctx.fillText("x: " + x + " y: " + y + " z: " + -z, dp * 10, dp * 20); 63 | ctx.fillText("painting: " + painting, dp * 10, dp * 35); 64 | ctx.fillText("flipped: " + (flip === -1), dp * 10, dp * 50); 65 | ctx.fillText("scale: " + scale, dp * 10, dp * 65); 66 | 67 | ctx.fillText("(Shift) Arrow Keys → navigate", dp * 10, canvas.height - dp * 85); 68 | ctx.fillText("Scroll → Scale", dp * 10, canvas.height - dp * 70); 69 | ctx.fillText("Space → toggle painting", dp * 10, canvas.height - dp * 55); 70 | ctx.fillText("f → flip view", dp * 10, canvas.height - dp * 40); 71 | ctx.fillText("d → delete block", dp * 10, canvas.height - dp * 25); 72 | ctx.fillText("r → reset", dp * 10, canvas.height - dp * 10); 73 | } 74 | 75 | // deletes the block at current position 76 | function del() { 77 | steps = steps.filter(function(value) { 78 | return value.toString() !== [x, y, z].toString(); 79 | }); 80 | } 81 | 82 | function render() { 83 | processSteps(); 84 | 85 | // reset canvas 86 | ctx.setTransform(1, 0, 0, 1, 0, 0); 87 | ctx.clearRect(0, 0, canvas.width, canvas.height); 88 | 89 | var renderScale = dp * scale; 90 | 91 | for (var i = 0; i < draw.length; i++) { 92 | var pos = draw[i]; 93 | var posx = pos[0] * renderScale * flip; 94 | var posy = pos[1] * renderScale * flip; 95 | var posz = pos[2] * renderScale; 96 | 97 | var flipped = flip === -1; 98 | 99 | var last = x * renderScale === flip * posx && y * renderScale === flip * posy && z * renderScale === posz; 100 | 101 | var colorTop = "#c44"; 102 | if (last) { 103 | colorTop = painting ? "rgba(204, 34, 34, 0.5)" : "rgba(30, 30, 30, 0.5)"; 104 | } 105 | var colorLeft = "#f44"; 106 | if (last) { 107 | colorLeft = painting ? "rgba(221, 34, 34, 0.5)" : "rgba(50, 50, 50, 0.5)"; 108 | } 109 | var colorRight = "#a44"; 110 | if (last) { 111 | colorRight = painting ? "rgba(136, 34, 34, 0.5)" : "rgba(00, 00, 00, 0.5)"; 112 | } 113 | 114 | // render top 115 | // + 0.5 is used to avoid the "problem of adjacent edges" with anti-aliasing 116 | ctx.setTransform(1, -skew_a, 1, skew_a, shiftx - renderScale, shifty - renderScale / 2 + 0.5); 117 | ctx.fillStyle = colorTop; 118 | ctx.fillRect(posy - posz, posx + posz, renderScale, renderScale); 119 | 120 | // render left side 121 | ctx.setTransform(1, skew_a, 0, skew_b, shiftx - renderScale, shifty - renderScale / 2 - 0.5); 122 | ctx.fillStyle = flipped ? colorRight : colorLeft; 123 | ctx.fillRect(posx + posy, posz - posy, renderScale, renderScale); 124 | 125 | // render right side 126 | ctx.setTransform(1, -skew_a, 0, skew_b, shiftx - renderScale + renderScale, shifty - renderScale / 2 + renderScale * skew_a); 127 | ctx.fillStyle = flipped ? colorLeft : colorRight; 128 | ctx.fillRect(posy + posx, posx + posz, renderScale, renderScale); 129 | } 130 | 131 | printInfo(); 132 | } 133 | 134 | if (location.hash) { 135 | steps = parsehash() || []; 136 | } 137 | render(); --------------------------------------------------------------------------------