Search code examples
javascript2dcollision-detectiongame-physicscollision

JavaScript wall collision on convex shapes, getting stuck at corner


this is a follow up to this other question: How do I handle player collision with corners of a wall

In inspiration of the code given in its answer, I tried to write some new code.

Basically, in the original, the wall sliding works very well on the inside of the walls, but I wanted to make it work on the outside as well, so I made a new basic code engine, based on his technique:

var aD =[]
var r
function start() {
	r = new CanvasRenderer(can),
		my = new scene();
	window.my = my
	eventHandler();
	my.add(new mesh({
		verts: [
			0,   0,
			100, 15,
			115, 60,
			50, 100,
			20, 75,2,8
		],
		position: {
			x: 100,
			y:100
		},
		scale: {

			x:4,y:5
		},
		color:"orange",
		onupdate(me) {
		//	me.position.x++
		}
	}));
	var g = false
	my.add(new mesh({
		primitive:"rect",
		name: "player",
		scale: {
			x: 50,
			y:50
		},
		position: {
			x: 311,
			y:75
		},
		origin: {
			x:0.5,
			y:0.5
		},
		onupdate(me) {
			var upKey = keys[38],
				downKey = keys[40],
				rightKey = keys[39],
				leftKey = keys[37],
				drx  = 0,
				dx = 0,
				speed = 5,
				turningSpeed = 3
			
			drx = leftKey ? -1 : rightKey ? 1 : 0
			forward = upKey ? 1 : downKey ? -1 : 0

			me.rotation.x += (
				(drx * Math.PI / 180 * turningSpeed )
			)
			me.rotation.y = 1;

			var xDir = Math.cos(me.rotation.x)
			var yDir = Math.sin(me.rotation.x)
			
			me.position.x += xDir  * forward * speed
			me.position.y += yDir * forward * speed

			for(var i = 0; i < my.objects.length; i++) {
				let cur = my.objects[i];
				if(cur.name !== me.name) {
					cur.lineSegments.forEach(l => {
						var col = checkCollision(
							me.position.x,
							me.position.y,
							me.scale.x/2,
							l
						)
						
						if(col) {
		
							me.position.y=col.y
							me.position.x = col.x
						}
					 });
				}
			}


		
			
		}

	}));
	
	let i = setInterval(() => render(r, my), 16);
	r.on("resize", () => render(r, my));

}

function checkCollision(x1, y1, rad,l) {
		var dist = distance2(
							l.start[0],
							l.start[1],
							
							l.end[0],
							l.end[1]
						),
							vec1 = [
								x1 - l.start[0],
								y1 - l.start[1]
							],

							vec2 = [
								l.end[0] - l.start[0],
								l.end[1] - l.start[1]
							],

							percentOfWall = (
								Math.max(
									0,
									Math.min(
										1, 
										dot(
											vec1[0],
											vec1[1],

											vec2[0],
											vec2[1]
										) / dist
									)
								)
							),
							projection = [
								l.start[0] + percentOfWall * vec2[0],
								l.start[1] + percentOfWall * vec2[1],
							],
							acDist = Math.sqrt(distance2(
								x1, y1,
								projection[0], projection[1]
							))
aD.push( () => {
						r.ctx.beginPath()
						r.ctx.fillStyle="green"
						r.ctx.arc(projection[0], projection[1], 5, 0, Math.PI*2);
						r.ctx.fill()
						r.ctx.closePath();
						})

					
					if(acDist < rad) {
						var mag = Math.sqrt(dist),
							delt = [
							l.end[0] - l.start[0],
							l.end[1] - l.start[1]
						],
							normal = [
							delt[0] / mag,
							delt[1] / mag
						]
						
						return {
						
							x: projection[0] + 

							rad * (normal[1] ),
						
							 y:projection[1] + 
							rad* (-normal[0] ),
							projection,
							normal
						}
					}

					
}


function dot(x1, y1, x2, y2) {
	return (
		x1 * x2 + y1 * y2
	)
}

function distance2(x1, y1, x2, y2) {
	let dx = (x1 - x2), dy = (y1 - y2);
	return (
		dx * dx + dy * dy
	);
}

function render(r,s) {
//r.ctx.clearRect(0,0,r.ctx.canvas.width,r.ctx.canvas.height)
	s.update();
	r.render(s)
	aD.forEach(x=>x());
	aD = []
}

onload = start;

function eventHandler() {
	window.keys = {};
	addEventListener("keyup" , e=> {
		keys[e.keyCode] = false;
			
	});

	addEventListener("keydown" , e=> {
		keys[e.keyCode] = true;
	});
}

function CanvasRenderer(dom) {
	if(!dom) dom = document.createElement("canvas");
	
	var events = {}, self = this;
	function rsz() {
		dom.width = dom.clientWidth;
		dom.height = dom.clientHeight;
		self.dispatchEvent("resize");
	}
	
	window.addEventListener("resize", rsz);	

	let ctx = dom.getContext("2d");

	function render(scene) {
		ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height);
		for(let i = 0; i < scene.objects.length; i++) {
			let o = scene.objects[i],
				verts = o.realVerts;

			
			ctx.beginPath();
			ctx.moveTo(
				verts[0] , 

				verts[1]
			);
			verts.forEach((v, i, ar) => {
				let y = i;
				


				ctx.lineTo(
					v[0] , 

					v[1]
				);
				
			});
			ctx.lineTo(
				verts[0],
				verts[1] 
			);
			ctx.fillStyle = o.color || "blue";
			ctx.lineWidth = 1;
			ctx.fill()
			ctx.stroke();
			ctx.closePath();
		}
	}

	Object.defineProperties(this, {
		domElement: {
			get: () => dom
		},	
		ctx: {
			get: () => ctx
		},
		render: {
			get: () => render
		},
		on: {
			get: () => (nm, cb) => {
				if(!events[nm]) {
					events[nm] = [];
				}
				events[nm].push(data => {
					if(typeof cb == "function") {
						cb(data);
					}
				});
			}		
		},
		dispatchEvent: {
			get: () => (name, data) => {
				if(events[name]) {
					events[name].forEach(x => {
						x(data);
					});
				}
			}
		}
	});
	
	rsz();

}

function scene() {
	let objects = [];
	Object.defineProperties(this, {
		add: {
			get: () => obj => {
				objects.push(obj);
			}
		},
		objects: {
			get: () => objects
		},
		update: {
			get: () => () => {				
				objects.forEach(x => {
					if(typeof x.update == "function") {
						x.update();
					}
				});
				
			}
		}
	});
}

function mesh(data={}) {
	let verts = [],
		self = this,
		holder = {
			position:{},
			scale: {
				
			},
			rotation: {},
			origin:{}
		},
		actual = {
	
		},
		position = {},
		scale = {},
		rotation = {},
		origin = {},
		color,
		name,
		primitive,
		eventNames = "update",
		events = {},
		drawPrimitive = {
			circle(ctx) {
				ctx.beginPath();
				ctx.arc(
					self.position.x,
					self.position.y,
					5, 
					0,
					360 * Math.PI / 180
				);
				ctx.closePath();
			},
			rect(ctx) {
				ctx.strokeRect(
					self.position.x,
					self.position.y,
					30, 30
				);
			}
		},
		width = 1,
		height = 1,
		primitiveToVerts = {
			rect: () =>  [
					0, 0,
					width , 0,
					width, height,
					0, height
			]
		},
		realVerts = verts,
		lineSegments = [],
		o = this;
	
	function updateRealVerts() {
			
			let  actualVerts = [],
				originedVerts = [],
				adjustedVerts = [],
				rotatedVerts = [],
				stepSize = o.step || 2,
				curVerts = [];
			
			o.verts.forEach((v, i) => {
				curVerts.push(v);
				if(
					(i - 1) % stepSize === 0 &&
					i !== 0
				) {
					actualVerts.push(curVerts);
					curVerts = [];
				}
			});
			actualVerts = actualVerts.filter(x => x.length == stepSize);
			
			originedVerts = actualVerts.map(v => [
				v[0] - o.origin.x,
				v[1] - o.origin.y,
				v[2] - o.origin.z
			]);
	
			rotatedVerts = originedVerts.map(v => 
				[

					v[0] * Math.cos(o.rotation.x) - 
					v[1] * Math.sin(o.rotation.x),

					v[0] * Math.sin(o.rotation.x) + 
					v[1] *Math.cos(o.rotation.x),
v[2]
				]
			);

			adjustedVerts = rotatedVerts.map(v => 
				[
					v[0] * 
					o.scale.x + 
					o.position.x,
	
					v[1] * 
					o.scale.y + 
					o.position.y,

					v[2] * 
					o.scale.z + 
					o.position.z,
				]
			);

			realVerts = adjustedVerts;
			updateLineSegments();
	}	

	function updateLineSegments() {
				let lines = [];
				for(let i = 0, a = realVerts; i < a.length;i++) {
					let start = [], end = []
					if(i < a.length - 1) {
						start = a[i];
						end = a[i + 1];
					} else {
						start = a[i];
						end = a[0];
					}

					lines.push({
						start, end
					})
				}
				lineSegments = lines;
	}
	Object.defineProperties(position, {
		x: {
			get: () => holder.position.x || 0,
			set: v => holder.position.x = v
		},
		y: {
			get: () => holder.position.y || 0,
			set: v => holder.position.y = v
		},
		z: {
			get: () => holder.position.z || 0,
			set: v => holder.position.z = v
		}
	});

	Object.defineProperties(scale, {
		x: {
			get: () => holder.scale.x || 1,
			set: v => holder.scale.x = v
		},
		y: {
			get: () => holder.scale.y || 1,
			set: v => holder.scale.y = v
		},
		z: {
			get: () => holder.scale.z || 1,
			set: v => holder.scale.z = v
		}
	});

	Object.defineProperties(rotation, {
		x: {
			get: () => holder.rotation.x || 0,
			set: v => holder.rotation.x = v
		},
		y: {
			get: () => holder.rotation.y || 0,
			set: v => holder.rotation.y = v
		},
		z: {
			get: () => holder.rotation.z || 0,
			set: v => holder.rotation.z = v
		}
	});

	Object.defineProperties(origin, {
		x: {
			get: () => holder.origin.x || 0,
			set: v => holder.origin.x = v
		},
		y: {
			get: () => holder.origin.y || 0,
			set: v => holder.origin.y = v
		},
		z: {
			get: () => holder.origin.z || 0,
			set: v => holder.origin.z = v
		}
	});
	

	Object.defineProperties(this, {
		verts: {
			get: ()=>verts,
			set(v) {
				verts = v
			}
		},
		name: {
			get: ()=>name,
			set(v) {
				name = v
			}
		},
		primitive: {
			get: ()=>primitive,
			set(v) {
				primitive = v;
				let newVerts = primitiveToVerts[v];
				if(newVerts) {
					this.verts = newVerts();
				}
			}
		},
		width: {
			get: ()=>width,
			set(v) {
				width = v
			}
		},
		height: {
			get: ()=>height,
			set(v) {
				height = v
			}
		},
		position: {
			get: () => position,
			set: v => {
				position.x = v.x || 0;
				position.y = v.y || 0;
				position.z = v.z || 0;
			}
		},
		scale: {
			get: () => scale,
			set: v => {
				scale.x = v.x || v.x === 0 ? v.x : 1;
				scale.y = v.y  || v.y === 0 ? v.y : 1;
				scale.z = v.z  || v.z === 0 ? v.z : 1;
			}
		},
		rotation: {
			get: () => rotation,
			set: v => {
				rotation.x = v.x || 0;
				rotation.y = v.y || 0;
				rotation.z = v.z || 0;
			}
		},
		origin: {
			get: () => origin,
			set: v => {
				origin.x = v.x || 0;
				origin.y = v.y || 0;
				origin.z = v.z || 0;
			}
		},
		color: {
			get: () => color,
			set: v => {
				color = v;
			}
		},
		realVerts: {
			get: () => realVerts
		},
		lineSegments: {
			get: () => lineSegments
		},
		update: {
			get: () => () => {
				if(events["update"]) {
					events.update.forEach(x => {
						updateRealVerts();
						x(this);
					});
				}
			}
		},
		on: {
			get: () => (nm, fnc) => {
				if(!events[nm]) events[nm] = [];
				events[nm].push(stuff => {
					if(typeof fnc == "function") {
						fnc(stuff);
					}
				});
			}
		}
	});

	eventNames.split(" ").forEach(x => {
		var name = "on" + x;
		if(!this.hasOwnProperty(name)) {
			Object.defineProperty(this, name, {
				get: () => events[name],
				set(v) {
					events[x] = [
						data => {
							typeof v == "function" && v(data)
						}
					];
				}
			});
		}
	});

	for(let k in data) {
		this[k] = data[k]
	}

	updateRealVerts();

}
canvas{
	width:100%;
	height:100%;
	position:absolute;
	top:0;	
	left:0px
}

.wow{
	float:right;
	z-index:1298737198
}
<meta charset="utf-8">
<button onclick="start()" class=wow>ok</button>
<canvas id=can>

</canvas>

See line 71 for the collision detection implementation call (and the return value of the function there).

The problem is, as you can hopefully see (just fullscreen it and use arrow keys to move, try colliding with the orange mesh at the corners) that it slides fine, but when it gets to the corners, it gets stuck at them.

Any ideas how to fix this -- without using any kind of external libraries etc. (only what's available in the snippet)?


Solution

  • I would say just check that you are not in the corner - discard cases when percentOfWall is either exactly 0 or exactly 1

    EDIT: to address the corners mentioned in the comments I have to explain why your implementation got stuck. It calculated penetration with all the walls and decreased position change by this penetration amount. In the corner, the object collided with both edges at once, and being repulsed from both at once stopped moving.

    As you rightly noticed, in the corners your square gets inside the obstacle for one frame and then is pushed out on the consequent frame.

    Alternative solutions, however, are more complicated and harder to debug, but here is a sketch for a couple of options:

    • Limit the collisions to repulse from only one wall at a time and early exit your forEach loop. This is a simple solution which will work in this particular example but won't work in the general case, for example in the corner when you need to collide with 2 walls preventing you to go in both directions.
    • Add a tiny circle in each of the corners and collide with it to avoid going inside. The normal is along the line between the center of the circle and the point of contact. That smooths the normal discontinuity you have in the corners and there is always a single normal along which the object is repulsed, continuously changing from one segment to the other.

    Directing the "push" to outside each segment of the obstacle (which is what you are asking) wouldn't prevent stopping as on the corner (which is exactly the point you are concerned with), both walls will collide with your object and "outside" will be in the opposite directions. So it'll get stuck the same way as before and for exact same reason - normals will be not continuous.

    I hope that helps

    var aD =[]
    var r
    function start() {
    	r = new CanvasRenderer(can),
    		my = new scene();
    	window.my = my
    	eventHandler();
    	my.add(new mesh({
    		verts: [
    			0,   0,
    			100, 15,
    			115, 60,
    			50, 100,
    			20, 75,2,8
    		],
    		position: {
    			x: 100,
    			y:100
    		},
    		scale: {
    
    			x:4,y:5
    		},
    		color:"orange",
    		onupdate(me) {
    		//	me.position.x++
    		}
    	}));
    	var g = false
    	my.add(new mesh({
    		primitive:"rect",
    		name: "player",
    		scale: {
    			x: 50,
    			y:50
    		},
    		position: {
    			x: 311,
    			y:75
    		},
    		origin: {
    			x:0.5,
    			y:0.5
    		},
    		onupdate(me) {
    			var upKey = keys[38],
    				downKey = keys[40],
    				rightKey = keys[39],
    				leftKey = keys[37],
    				drx  = 0,
    				dx = 0,
    				speed = 5,
    				turningSpeed = 3
    			
    			drx = leftKey ? -1 : rightKey ? 1 : 0
    			forward = upKey ? 1 : downKey ? -1 : 0
    
    			me.rotation.x += (
    				(drx * Math.PI / 180 * turningSpeed )
    			)
    			me.rotation.y = 1;
    
    			var xDir = Math.cos(me.rotation.x)
    			var yDir = Math.sin(me.rotation.x)
    			
    			me.position.x += xDir  * forward * speed
    			me.position.y += yDir * forward * speed
    
    			for(var i = 0; i < my.objects.length; i++) {
    				let cur = my.objects[i];
    				if(cur.name !== me.name) {
    					cur.lineSegments.forEach(l => {
    						var col = checkCollision(
    							me.position.x,
    							me.position.y,
    							me.scale.x/2,
    							l
    						)
    						
    						if(col) {
    							me.position.y=col.y
    							me.position.x = col.x
    						}
    					 });
    				}
    			}
    
    
    		
    			
    		}
    
    	}));
    	
    	let i = setInterval(() => render(r, my), 16);
    	r.on("resize", () => render(r, my));
    
    }
    
    function checkCollision(x1, y1, rad,l) {
    		var dist = distance2(
    							l.start[0],
    							l.start[1],
    							
    							l.end[0],
    							l.end[1]
    						),
    							vec1 = [
    								x1 - l.start[0],
    								y1 - l.start[1]
    							],
    
    							vec2 = [
    								l.end[0] - l.start[0],
    								l.end[1] - l.start[1]
    							],
    
    							percentOfWall = (
    								Math.max(
    									0,
    									Math.min(
    										1, 
    										dot(
    											vec1[0],
    											vec1[1],
    
    											vec2[0],
    											vec2[1]
    										) / dist
    									)
    								)
    							),
    							projection = [
    								l.start[0] + percentOfWall * vec2[0],
    								l.start[1] + percentOfWall * vec2[1],
    							],
    							acDist = Math.sqrt(distance2(
    								x1, y1,
    								projection[0], projection[1]
    							))
    aD.push( () => {
    						r.ctx.beginPath()
    						r.ctx.fillStyle="green"
    						r.ctx.arc(projection[0], projection[1], 5, 0, Math.PI*2);
    						r.ctx.fill()
    						r.ctx.closePath();
    						})
    
    					
    					if(acDist < rad && percentOfWall > 0 && percentOfWall < 1) {
    						var mag = Math.sqrt(dist),
    							delt = [
    							l.end[0] - l.start[0],
    							l.end[1] - l.start[1]
    						],
    							normal = [
    							delt[0] / mag,
    							delt[1] / mag
    						]
    						
    						return {
    						
    							x: projection[0] + 
    
    							rad * (normal[1] ),
    						
    							 y:projection[1] + 
    							rad* (-normal[0] ),
    							projection,
    							normal
    						}
    					}
    
    					
    }
    
    
    function dot(x1, y1, x2, y2) {
    	return (
    		x1 * x2 + y1 * y2
    	)
    }
    
    function distance2(x1, y1, x2, y2) {
    	let dx = (x1 - x2), dy = (y1 - y2);
    	return (
    		dx * dx + dy * dy
    	);
    }
    
    function render(r,s) {
    //r.ctx.clearRect(0,0,r.ctx.canvas.width,r.ctx.canvas.height)
    	s.update();
    	r.render(s)
    	aD.forEach(x=>x());
    	aD = []
    }
    
    onload = start;
    
    function eventHandler() {
    	window.keys = {};
    	addEventListener("keyup" , e=> {
    		keys[e.keyCode] = false;
    			
    	});
    
    	addEventListener("keydown" , e=> {
    		keys[e.keyCode] = true;
    	});
    }
    
    function CanvasRenderer(dom) {
    	if(!dom) dom = document.createElement("canvas");
    	
    	var events = {}, self = this;
    	function rsz() {
    		dom.width = dom.clientWidth;
    		dom.height = dom.clientHeight;
    		self.dispatchEvent("resize");
    	}
    	
    	window.addEventListener("resize", rsz);	
    
    	let ctx = dom.getContext("2d");
    
    	function render(scene) {
    		ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height);
    		for(let i = 0; i < scene.objects.length; i++) {
    			let o = scene.objects[i],
    				verts = o.realVerts;
    
    			
    			ctx.beginPath();
    			ctx.moveTo(
    				verts[0] , 
    
    				verts[1]
    			);
    			verts.forEach((v, i, ar) => {
    				let y = i;
    				
    
    
    				ctx.lineTo(
    					v[0] , 
    
    					v[1]
    				);
    				
    			});
    			ctx.lineTo(
    				verts[0],
    				verts[1] 
    			);
    			ctx.fillStyle = o.color || "blue";
    			ctx.lineWidth = 1;
    			ctx.fill()
    			ctx.stroke();
    			ctx.closePath();
    		}
    	}
    
    	Object.defineProperties(this, {
    		domElement: {
    			get: () => dom
    		},	
    		ctx: {
    			get: () => ctx
    		},
    		render: {
    			get: () => render
    		},
    		on: {
    			get: () => (nm, cb) => {
    				if(!events[nm]) {
    					events[nm] = [];
    				}
    				events[nm].push(data => {
    					if(typeof cb == "function") {
    						cb(data);
    					}
    				});
    			}		
    		},
    		dispatchEvent: {
    			get: () => (name, data) => {
    				if(events[name]) {
    					events[name].forEach(x => {
    						x(data);
    					});
    				}
    			}
    		}
    	});
    	
    	rsz();
    
    }
    
    function scene() {
    	let objects = [];
    	Object.defineProperties(this, {
    		add: {
    			get: () => obj => {
    				objects.push(obj);
    			}
    		},
    		objects: {
    			get: () => objects
    		},
    		update: {
    			get: () => () => {				
    				objects.forEach(x => {
    					if(typeof x.update == "function") {
    						x.update();
    					}
    				});
    				
    			}
    		}
    	});
    }
    
    function mesh(data={}) {
    	let verts = [],
    		self = this,
    		holder = {
    			position:{},
    			scale: {
    				
    			},
    			rotation: {},
    			origin:{}
    		},
    		actual = {
    	
    		},
    		position = {},
    		scale = {},
    		rotation = {},
    		origin = {},
    		color,
    		name,
    		primitive,
    		eventNames = "update",
    		events = {},
    		drawPrimitive = {
    			circle(ctx) {
    				ctx.beginPath();
    				ctx.arc(
    					self.position.x,
    					self.position.y,
    					5, 
    					0,
    					360 * Math.PI / 180
    				);
    				ctx.closePath();
    			},
    			rect(ctx) {
    				ctx.strokeRect(
    					self.position.x,
    					self.position.y,
    					30, 30
    				);
    			}
    		},
    		width = 1,
    		height = 1,
    		primitiveToVerts = {
    			rect: () =>  [
    					0, 0,
    					width , 0,
    					width, height,
    					0, height
    			]
    		},
    		realVerts = verts,
    		lineSegments = [],
    		o = this;
    	
    	function updateRealVerts() {
    			
    			let  actualVerts = [],
    				originedVerts = [],
    				adjustedVerts = [],
    				rotatedVerts = [],
    				stepSize = o.step || 2,
    				curVerts = [];
    			
    			o.verts.forEach((v, i) => {
    				curVerts.push(v);
    				if(
    					(i - 1) % stepSize === 0 &&
    					i !== 0
    				) {
    					actualVerts.push(curVerts);
    					curVerts = [];
    				}
    			});
    			actualVerts = actualVerts.filter(x => x.length == stepSize);
    			
    			originedVerts = actualVerts.map(v => [
    				v[0] - o.origin.x,
    				v[1] - o.origin.y,
    				v[2] - o.origin.z
    			]);
    	
    			rotatedVerts = originedVerts.map(v => 
    				[
    
    					v[0] * Math.cos(o.rotation.x) - 
    					v[1] * Math.sin(o.rotation.x),
    
    					v[0] * Math.sin(o.rotation.x) + 
    					v[1] *Math.cos(o.rotation.x),
    v[2]
    				]
    			);
    
    			adjustedVerts = rotatedVerts.map(v => 
    				[
    					v[0] * 
    					o.scale.x + 
    					o.position.x,
    	
    					v[1] * 
    					o.scale.y + 
    					o.position.y,
    
    					v[2] * 
    					o.scale.z + 
    					o.position.z,
    				]
    			);
    
    			realVerts = adjustedVerts;
    			updateLineSegments();
    	}	
    
    	function updateLineSegments() {
    				let lines = [];
    				for(let i = 0, a = realVerts; i < a.length;i++) {
    					let start = [], end = []
    					if(i < a.length - 1) {
    						start = a[i];
    						end = a[i + 1];
    					} else {
    						start = a[i];
    						end = a[0];
    					}
    
    					lines.push({
    						start, end
    					})
    				}
    				lineSegments = lines;
    	}
    	Object.defineProperties(position, {
    		x: {
    			get: () => holder.position.x || 0,
    			set: v => holder.position.x = v
    		},
    		y: {
    			get: () => holder.position.y || 0,
    			set: v => holder.position.y = v
    		},
    		z: {
    			get: () => holder.position.z || 0,
    			set: v => holder.position.z = v
    		}
    	});
    
    	Object.defineProperties(scale, {
    		x: {
    			get: () => holder.scale.x || 1,
    			set: v => holder.scale.x = v
    		},
    		y: {
    			get: () => holder.scale.y || 1,
    			set: v => holder.scale.y = v
    		},
    		z: {
    			get: () => holder.scale.z || 1,
    			set: v => holder.scale.z = v
    		}
    	});
    
    	Object.defineProperties(rotation, {
    		x: {
    			get: () => holder.rotation.x || 0,
    			set: v => holder.rotation.x = v
    		},
    		y: {
    			get: () => holder.rotation.y || 0,
    			set: v => holder.rotation.y = v
    		},
    		z: {
    			get: () => holder.rotation.z || 0,
    			set: v => holder.rotation.z = v
    		}
    	});
    
    	Object.defineProperties(origin, {
    		x: {
    			get: () => holder.origin.x || 0,
    			set: v => holder.origin.x = v
    		},
    		y: {
    			get: () => holder.origin.y || 0,
    			set: v => holder.origin.y = v
    		},
    		z: {
    			get: () => holder.origin.z || 0,
    			set: v => holder.origin.z = v
    		}
    	});
    	
    
    	Object.defineProperties(this, {
    		verts: {
    			get: ()=>verts,
    			set(v) {
    				verts = v
    			}
    		},
    		name: {
    			get: ()=>name,
    			set(v) {
    				name = v
    			}
    		},
    		primitive: {
    			get: ()=>primitive,
    			set(v) {
    				primitive = v;
    				let newVerts = primitiveToVerts[v];
    				if(newVerts) {
    					this.verts = newVerts();
    				}
    			}
    		},
    		width: {
    			get: ()=>width,
    			set(v) {
    				width = v
    			}
    		},
    		height: {
    			get: ()=>height,
    			set(v) {
    				height = v
    			}
    		},
    		position: {
    			get: () => position,
    			set: v => {
    				position.x = v.x || 0;
    				position.y = v.y || 0;
    				position.z = v.z || 0;
    			}
    		},
    		scale: {
    			get: () => scale,
    			set: v => {
    				scale.x = v.x || v.x === 0 ? v.x : 1;
    				scale.y = v.y  || v.y === 0 ? v.y : 1;
    				scale.z = v.z  || v.z === 0 ? v.z : 1;
    			}
    		},
    		rotation: {
    			get: () => rotation,
    			set: v => {
    				rotation.x = v.x || 0;
    				rotation.y = v.y || 0;
    				rotation.z = v.z || 0;
    			}
    		},
    		origin: {
    			get: () => origin,
    			set: v => {
    				origin.x = v.x || 0;
    				origin.y = v.y || 0;
    				origin.z = v.z || 0;
    			}
    		},
    		color: {
    			get: () => color,
    			set: v => {
    				color = v;
    			}
    		},
    		realVerts: {
    			get: () => realVerts
    		},
    		lineSegments: {
    			get: () => lineSegments
    		},
    		update: {
    			get: () => () => {
    				if(events["update"]) {
    					events.update.forEach(x => {
    						updateRealVerts();
    						x(this);
    					});
    				}
    			}
    		},
    		on: {
    			get: () => (nm, fnc) => {
    				if(!events[nm]) events[nm] = [];
    				events[nm].push(stuff => {
    					if(typeof fnc == "function") {
    						fnc(stuff);
    					}
    				});
    			}
    		}
    	});
    
    	eventNames.split(" ").forEach(x => {
    		var name = "on" + x;
    		if(!this.hasOwnProperty(name)) {
    			Object.defineProperty(this, name, {
    				get: () => events[name],
    				set(v) {
    					events[x] = [
    						data => {
    							typeof v == "function" && v(data)
    						}
    					];
    				}
    			});
    		}
    	});
    
    	for(let k in data) {
    		this[k] = data[k]
    	}
    
    	updateRealVerts();
    
    }
    canvas{
    	width:100%;
    	height:100%;
    	position:absolute;
    	top:0;	
    	left:0px
    }
    
    .wow{
    	float:right;
    	z-index:1298737198
    }
    <meta charset="utf-8">
    <button onclick="start()" class=wow>ok</button>
    <canvas id=can>
    
    </canvas>