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