├── .gitattributes ├── README.md ├── my_shapes.js ├── index.html ├── my_shapes.css ├── editor.css └── editor.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 3D-level-editor 2 | 3 | to use your own 3D shapes and 2D sprites, edit my_shapes.js and my_shapes.css -------------------------------------------------------------------------------- /my_shapes.js: -------------------------------------------------------------------------------- 1 | shapes = { 2 | cube: { 3 | type: "shape_3d", 4 | template: 5 | `
6 |
7 |
8 |
9 |
10 |
` 11 | }, 12 | 13 | pyramid: { 14 | type: "shape_3d", 15 | template: 16 | `
17 |
18 |
19 |
20 |
` 21 | }, 22 | 23 | snail: { 24 | type: "shape_sprite", 25 | template: `🐌` 26 | }, 27 | 28 | ball: { 29 | type: "shape_sprite", 30 | template: `⚽` 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 3D level editor 4 | 5 | 6 | 7 | 8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | 32 | 33 | 34 |
35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /my_shapes.css: -------------------------------------------------------------------------------- 1 | .cube div { width: 500px; height: 500px; position: absolute; transform-origin: center center; border: 5px solid #000; } 2 | .cube .up { transform: translateZ(250px); background: blue; } 3 | .cube .down { transform: translateZ(-250px); background: blue; } 4 | .cube .left { transform: translateX(-250px) rotateY(90deg); background: red; } 5 | .cube .right { transform: translateX(250px) rotateY(90deg); background: red; } 6 | .cube .front { transform: translateY(250px) rotateX(90deg); background: yellow; } 7 | .cube .back { transform: translateY(-250px) rotateX(90deg); background: yellow; } 8 | 9 | .pyramid div { width: 500px; height: 500px; position: absolute; transform-origin: center center; width: 0; height: 0; background: none; border-left: 250px solid transparent; border-right: 250px solid transparent; } 10 | .pyramid .up { display: none; } 11 | .pyramid .down { display: none; } 12 | .pyramid .front { transform: translateZ(-30px) translateY(125px) rotateX(-60deg) rotateZ(0deg); border-bottom: 500px solid red; } 13 | .pyramid .back { transform: translateZ(-30px) translateY(-125px) rotateX(60deg) rotateZ(180deg); border-bottom: 500px solid yellow; } 14 | .pyramid .left { transform: translateZ(-30px) translateX(-125px) rotateY(-60deg) rotateZ(90deg); border-bottom: 500px solid blue; } 15 | .pyramid .right { transform: translateZ(-30px) translateX(125px) rotateY(60deg) rotateZ(-90deg); border-bottom: 500px solid white; } 16 | 17 | .shape_sprite { font-size: 200px; transform: rotateX(-90deg); } -------------------------------------------------------------------------------- /editor.css: -------------------------------------------------------------------------------- 1 | * { box-sizing: border-box; margin: 0; padding: 0; } 2 | body * { transition: .5s; } 3 | body { background: #abf; } 4 | button { z-index: 2; padding: 5px; font-size: 18px; transform-style: preserve-3d; } 5 | #viewport { width: 100vw; height: 100vh; position: fixed; top: 0; left; 0; overflow: hidden; perspective: 900px; transition: .5s; } 6 | #center { width: 0; height: 0; position: relative; left: 50%; top: 50%; transition:.5s; border: 25px solid red; transform: translateZ(-3000px); transform-style: preserve-3d; } 7 | #scene_perspective { width: 0; height: 0; transform-origin: 50% 50%; transform: rotateX(60deg); transform-style: preserve-3d; } 8 | #scene { width: 0; height: 0; transition: .5s; transform-style: preserve-3d; } 9 | #things { transform-style: preserve-3d; } 10 | #grid { transition: .5s; transform-style: preserve-3d; } 11 | .tile { position: absolute; box-shadow: 0 0 0 10px black; width: 500px; height: 500px; transform-style: preserve-3d; } 12 | body.iso #viewport { perspective: none; } 13 | body.iso #center { transform: scale(.1); } 14 | body.iso #scene_perspective { transform: rotateX(90deg); } 15 | body.fps #center { transform: translateZ(900px) translateY(100px); } 16 | body.fps #scene_perspective { transform: rotateX(90deg); } 17 | .shape { position:fixed; width: 500px; height: 500px; transform-style: preserve-3d; transform: translateZ(250px); position: absolute; top: 0; left: 0; pointer-events: none; transform-origin: 250px 250px; } 18 | .shape.temp div { opacity: .5; } 19 | .shape.shape_sprite.temp { opacity: .5; } 20 | #ground { border-radius: 50%; width: 9000px; height: 9000px; background: rgba(0,128,0,0.9); transform: translateX(-3000px) translateY(-3000px) translateZ(-100px); position: fixed; } 21 | .shape_sprite { font-size: 250px; transform: rotateX(-90deg); } -------------------------------------------------------------------------------- /editor.js: -------------------------------------------------------------------------------- 1 | onload=function(){ 2 | 3 | // Add one button for each shape 4 | for(i in shapes){ 5 | b.innerHTML += ``; 6 | } 7 | 8 | // Build the grid (6x6) 9 | for(i=0;i<6;i++){ 10 | for(j=0;j<6;j++){ 11 | grid.innerHTML+=`
`; 12 | } 13 | } 14 | 15 | // Switch between 2D iso, 3D, and 3D FPS views by applying a class to the body 16 | b_3D.onclick = function(){b.className = ""}; 17 | b_2D.onclick = function(){b.className = "iso"}; 18 | b_fps.onclick = function(){b.className = "fps"}; 19 | 20 | // Move/rotate the scene relative to the center (the red square) 21 | 22 | // Position 23 | x = -1500; 24 | y = -1500; 25 | z = 0; 26 | 27 | // Rotation 28 | rot = 0; 29 | 30 | // Apply new position/rotation. 31 | move_scene = function(){ 32 | 33 | // Update scene's transformOrogin/transform 34 | scene.style.transformOrigin = (-x) + "px " + (-y) + "px"; 35 | scene.style.transform = "translateX(" + x + "px) translateY(" + y + "px) translateZ(" + z + "px) rotateZ(" + rot + "rad)"; 36 | 37 | // The grid moves up and down to follow the center 38 | grid.style.transform = "translateZ(" + (-z) + "px)"; 39 | } 40 | move_scene(); 41 | 42 | // Moving left/right/front/back requires to take the current angle into account. 43 | // That's what these functions do for x and y offsets relative to the current angle. 44 | xoffset = function(o){ 45 | x += o * Math.cos(rot); 46 | y += o * Math.sin(rot); 47 | } 48 | yoffset = function(o){ 49 | y += o * Math.cos(rot); 50 | x += o * Math.sin(rot); 51 | } 52 | 53 | // All the move/rotate buttons 54 | b_rl.onclick = function(){rot -= Math.PI/4; move_scene()}; 55 | b_rr.onclick = function(){rot += Math.PI/4; move_scene()}; 56 | b_mr.onclick = function(){xoffset(-500); move_scene()}; 57 | b_ml.onclick = function(){xoffset(500); move_scene()}; 58 | b_md.onclick = function(){if(z<0) z += 500; move_scene()}; 59 | b_mu.onclick = function(){z -= 500; move_scene()}; 60 | b_mf.onclick = function(){yoffset(-500); move_scene()}; 61 | b_mb.onclick = function(){yoffset(500); move_scene()}; 62 | 63 | // Rotate last 3D shape 64 | last_shape_angle = 0; 65 | b_rot.onclick = function(){ 66 | last_shape_angle += Math.PI/2; 67 | if(top["shape"+(shapes_count-1)]){ 68 | top["shape"+(shapes_count-1)].style.transform = top["shape"+(shapes_count-1)].style.transform.replace(/$|rotateZ\(.*?\)$/, "rotateZ(" + last_shape_angle + "rad)"); 69 | data[data.length-1].angle = (parseFloat((last_shape_angle/(Math.PI*2)).toFixed(2))%1); 70 | } 71 | }; 72 | 73 | // Delete last shape 74 | last_shape = null; 75 | b_del.onclick = function(){ 76 | last_shape.remove(); 77 | data.pop(); 78 | }; 79 | 80 | // Export 81 | b_export.onclick = function(){console.log(JSON.stringify(data))}; 82 | 83 | // Current shape 84 | shape = "cube"; 85 | 86 | // Buttons change the current shape 87 | for(i in shapes){ 88 | (function(i){ 89 | top["b_" + i].onclick = function(){ shape = i; }; 90 | })(i); 91 | } 92 | 93 | // Preview current shape when hovering the grid 94 | onmousemove = e => { 95 | if(e.target.className == "tile"){ 96 | if(shapes[shape].type == "shape_3d"){ 97 | e.target.innerHTML = `
${shapes[shape].template}
`; 98 | } 99 | else if(shapes[shape].type == "shape_sprite"){ 100 | e.target.innerHTML = `
${shapes[shape].template}
`; 101 | } 102 | } 103 | } 104 | 105 | onmouseout = e => { 106 | if(e.target.className == "tile"){ 107 | e.target.innerHTML = ""; 108 | } 109 | } 110 | 111 | // Click to place a shape with an angle set to 0 and save it in the map's data 112 | data = []; 113 | shapes_count = 0; 114 | sprites_count = 0; 115 | sprites_styles = []; 116 | onclick = e => { 117 | if(e.which == 1){ 118 | if(e.target.className == "tile"){ 119 | data.push({type: shape, x: parseInt(e.target.style.left)/500, y: parseInt(e.target.style.top)/500, z: -z / 500, angle: 0}); 120 | if(shapes[shape].type == "shape_3d"){ 121 | things.innerHTML += `
${shapes[shape].template}
`; 122 | last_shape=top["shape"+shapes_count]; 123 | shapes_count++; 124 | last_shape_angle = 0; 125 | } 126 | else if(shapes[shape].type == "shape_sprite"){ 127 | sprites_styles[sprites_count] = `translateX(0) translateY(0) translateZ(${-z}px) rotateX(-90deg)`; 128 | things.innerHTML += `
${shapes[shape].template}
`; 129 | last_shape=top["sprite"+sprites_count]; 130 | sprites_count++; 131 | } 132 | } 133 | } 134 | } 135 | 136 | // Sprites (snail, ball...) always face the camera. 137 | // This is executed at each frame. 138 | setInterval(function(){ 139 | for(i = 0; i < sprites_count; i++){ 140 | if(top["sprite" + i]){ 141 | top["sprite" + i].style.transform = sprites_styles[i] + " rotateY(" + (rot) + "rad)"; 142 | } 143 | } 144 | }, 33); 145 | 146 | } --------------------------------------------------------------------------------