var backend = use('sdl_gpu') 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 } function place_rect(src, dst, mode) { if (mode == 'stretch') return {x: 0, y: 0, width: dst.width, height: dst.height} var sx = 0, sy = 0, s = 0, w = 0, h = 0, scale = 0 if (mode == 'integer_scale') { sx = floor(dst.width / src.width) sy = floor(dst.height / src.height) s = max(1, min(sx, sy)) w = src.width * s h = src.height * s return {x: (dst.width - w) / 2, y: (dst.height - h) / 2, width: w, height: h} } // letterbox (default) scale = min(dst.width / src.width, dst.height / src.height) w = src.width * scale h = src.height * scale return {x: (dst.width - w) / 2, y: (dst.height - h) / 2, width: w, height: h} } 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; }, rect() { return {x:0, y:0, width: this.width, height: this.height} }, sensor() { var ws = backend.get_window_size() return { width: ws.width, height: ws.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 ws = backend.get_window_size() var dst = { x: 0, y: 0, width: ws.width, height: ws.height } return place_rect(src, dst, this.presentation) }, // --- Space converters --------------------------------------------------- // World -> View UV [0..1] independent of pixels/letterbox world_to_view_uv(wx, wy) { var ax = this.anchor.x, ay = this.anchor.y var u = (wx - this.pos.x) / this.width + ax var v = (wy - this.pos.y) / 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.x, ay = this.anchor.y var wx = this.pos.x + (uu - ax) * this.width var wy = this.pos.y + (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() var u = (sx - sr.x) / sr.width var v = 1 - (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) var ws = backend.get_window_size() return { x: p.x / ws.width, y: p.y / ws.height } }, // Normalized window [0..1] -> World screen_norm_to_world(nx, ny) { var ws = backend.get_window_size() var sx = nx * ws.width var sy = ny * ws.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 ws = backend.get_window_size() var pixel_x = sx * ws.width var pixel_y = sy * ws.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.x var ay = this.anchor.y var world_x = this.pos.x + (rel_x - ax) * this.width var world_y = this.pos.y + (rel_y - ay) * this.height return {x: world_x, y: world_y} }, } cam.make = function(config) { return meme(basecam, config || {}) } return cam