var cam = {} /* presentation can be one of letterbox overscan stretch .. or simply 'null' for no presentation */ function rect_contains_pt(rect, x, y) { return x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height } var basecam = { pos: {x:0,y:0}, ortho:true, width: 1, height: 1, fov:50, near_z:0, far_z:1000, anchor: {x:0.5,y:0.5}, rotation:[0,0,0,1], presentation: "letterbox", background: {r:1,g:1,b:1,a:0}, viewport: {x:0,y:0,width:1,height:1}, aspect_ratio() { return this.y/this.x; }, screen_rect() { var src = { width: this.width, height: this.height } var dst = { x: 0, y: 0, width: gameres.width, height: gameres.height } return place_rect(src, dst, this.presentation, this.align_x, this.align_y, false) }, rect() { return {x:0, y:0, width: this.width, height: this.height} }, sensor() { return { width: gameres.width, height: gameres.height, } }, // The rectangle (in window pixels) where this camera’s target will be drawn screen_rect() { var src = { width: this.width, height: this.height } var dst = { x: 0, y: 0, width: gameres.width, height: gameres.height } return place_rect(src, dst, this.presentation, this.align_x, this.align_y, false) }, // --- Space converters --------------------------------------------------- // World -> View UV [0..1] independent of pixels/letterbox world_to_view_uv(wx, wy) { var ax = this.anchor[0], ay = this.anchor[1] var u = (wx - this.pos[0]) / this.width + ax var v = (wy - this.pos[1]) / this.height + ay // apply viewport (maps camera-local UV into sub-rect) var vp = this.viewport u = vp.x + u * vp.width v = vp.y + v * vp.height return { x: u, y: v } }, // View UV -> World view_uv_to_world(u, v) { var vp = this.viewport var uu = (u - vp.x) / vp.width var vv = (v - vp.y) / vp.height var ax = this.anchor[0], ay = this.anchor[1] var wx = this.pos[0] + (uu - ax) * this.width var wy = this.pos[1] + (vv - ay) * this.height return { x: wx, y: wy } }, // World -> Window pixels (what you want for hit-tests) world_to_window(wx, wy) { var uv = this.world_to_view_uv(wx, wy) var sr = this.screen_rect() var sx = sr.x + uv.x * sr.width var sy = sr.y + uv.y * sr.height return { x: sx, y: sy } }, // Window pixels -> World (mouse picking) window_to_world(sx, sy) { var sr = this.screen_rect() if (!rect_contains_pt(sr, sx, sy)) { // outside letterbox bars; clamp or return null // return null } var u = (sx - sr.x) / sr.width var v = (sy - sr.y) / sr.height return this.view_uv_to_world(u, v) }, // World -> normalized window [0..1] world_to_screen_norm(wx, wy) { var p = this.world_to_window(wx, wy) return { x: p.x / gameres.width, y: p.y / gameres.height } }, // Normalized window [0..1] -> World screen_norm_to_world(nx, ny) { var sx = nx * gameres.width var sy = ny * gameres.height return this.window_to_world(sx, sy) }, screen_to_world: function(sx, sy) { // sx, sy are normalized screen coordinates (0-1), bottom left [0,0], top right [1,1] var screen_rect = this.screen_rect() var pixel_x = sx * gameres.width var pixel_y = sy * gameres.height var rel_x = (pixel_x - screen_rect.x) / screen_rect.width var rel_y = (pixel_y - screen_rect.y) / screen_rect.height var ax = this.anchor[0] var ay = this.anchor[1] var world_x = this.pos[0] + (rel_x - ax) * this.width var world_y = this.pos[1] + (rel_y - ay) * this.height return {x: world_x, y: world_y} }, } cam.make = function() { return meme(basecam) } return cam