diff options
Diffstat (limited to 'public/assets/javascripts')
134 files changed, 10223 insertions, 1690 deletions
diff --git a/public/assets/javascripts/app.js b/public/assets/javascripts/app.js index 1dd8a5e..3cafeca 100644 --- a/public/assets/javascripts/app.js +++ b/public/assets/javascripts/app.js @@ -1,69 +1,34 @@ -var is_iphone = (navigator.userAgent.match(/iPhone/i)) || (navigator.userAgent.match(/iPod/i)); -var is_ipad = (navigator.userAgent.match(/iPad/i)); -var is_android = (navigator.userAgent.match(/Android/i)) -var is_mobile = is_iphone || is_ipad || is_android; - if (is_mobile) { - // window.location.href = "mobile.html" - $("html").addClass("mobile"); -} -else if ($.browser.msie || ! has3d()) { - // window.location.href = "error.html" + $("html").addClass("mobile") } else { - $("html").addClass("desktop"); + $("html").addClass("desktop") } + var scene, cam, map; var app = new function(){} app.mode = { editor: false, builder: false } - + app.init = function () { app.tube = new Tube () app.router = new SiteRouter () } app.launch = function () { - var movements - - scene = new MX.Scene().addTo('#scene') - scene.width = window.innerWidth - scene.height = window.innerHeight - scene.perspective = window.innerHeight - - window.onresize = function () { - scene.width = window.innerWidth - scene.height = window.innerHeight - scene.perspective = window.innerHeight - scene.update() - } - - cam = scene.camera - cam.y = viewHeight - - if (MX.Map) map = app.map = new MX.Map() - - if (is_mobile) { - app.movements = new MX.MobileMovements(cam, viewHeight) - } - else { - app.movements = new MX.Movements(cam, viewHeight) - } - app.movements.init() - + if ($.browser.msie || ! has3d()) { return app.fallback() } + + var last_t = 0 function animate (t) { + var dt = t - last_t + last_t = t requestAnimationFrame(animate) - environment.update(t) - window.path && path.update(t) - app.movements.update() - scene.update() + environment.update(t, dt) } - window.inAnimation = true - var loader = new Loader(function(){ $("#loader").hide() window.environment && window.environment.init() @@ -74,6 +39,20 @@ app.launch = function () { // loader.preloadImages([]) loader.ready() + + window.scrollTo(0,0) +} + +app.fallback = function(){ + app.unsupported = true + var msg = "Sorry, your browser is not supported.<br><br>" + + "Please use <a href='http://chrome.com/'>Chrome</a> or <a href='https://www.apple.com/safari/'>Safari</a> or <a href='http://getfirefox.com/'>Firefox</a>." + var $fallback = $("<div>") + $fallback.attr('id', 'fallback') + $fallback.html(msg) + $('body').append($fallback) + $("#keyhint").hide() + $("#editorView").hide() } app.on = function(){ diff --git a/public/assets/javascripts/defaults.js b/public/assets/javascripts/defaults.js index 038a882..1e1ac5b 100644 --- a/public/assets/javascripts/defaults.js +++ b/public/assets/javascripts/defaults.js @@ -1,9 +1,11 @@ +app = window.app || {} app.defaults = { viewHeight: window.viewHeight = 186, + wallHeight: 397, units: app.units = "ft", footResolution: 36, meterResolution: 100, - wallOpacity: 0.95, + wallOpacity: 0.98, outlineWidth: 2, colors: { wall: [255,255,255], @@ -12,3 +14,8 @@ app.defaults = { ceiling: [255,255,255], }, } + +window.marked && marked.setOptions({ + sanitize: true, + smartypants: true, +}) diff --git a/public/assets/javascripts/mx/extensions/mx.movements.js b/public/assets/javascripts/mx/extensions/mx.movements.js index 669a7f4..3e34c1b 100644 --- a/public/assets/javascripts/mx/extensions/mx.movements.js +++ b/public/assets/javascripts/mx/extensions/mx.movements.js @@ -16,193 +16,281 @@ MX.Movements = function (cam) { creeping = false, locked = false, gravity = false, - rotationX_min = PI/-2, - rotationX_max = PI/2 + rotationX_min = PI/-4, + rotationX_max = PI/6 var v = 12, - vr = Math.PI * 0.012, + vr = Math.PI * 0.018, jumpV = 23, vx = vy = vz = 0, creepFactor = 0.3 + var mouseX, mouseY, dx, dy, rotX, rotY, dragging = false + + var trackpad + var DEFAULT_SCALE = 1.0, scale = DEFAULT_SCALE var pos = { x: 0, y: 0, z: 0, rotationX: 0, rotationY: 0 } - return { + $(document).one("keydown", function(){ + $("#keyhint").fadeOut(250); + $('.reader #minimap').addClass('active'); + }) - init: function () { + function clampRotation( vr ) { + if (window.Rooms && Rooms.mover.noclip) { + return clamp(vr, PI/-2, PI/2 ) + } + else { + return clamp(vr, PI/-4, PI/6 ) + } + } - document.addEventListener('keydown', function (e) { - // console.log(e.keyCode) - if (locked || e.altKey || e.metaKey || e.ctrlKey) { - return - } - switch ( e.keyCode ) { - - case 16: // shift - creeping = true - break + var exports = { + + init: function () { + + trackpad = new wheel ({ + el: scene.el, + update: exports.mousewheel, + }) + + document.addEventListener('keydown', exports.keydown) + document.addEventListener('keyup', exports.keyup) + document.addEventListener('mousedown', exports.mousedown) + document.addEventListener('mousemove', exports.mousemove) + document.addEventListener('mouseup', exports.mouseup) + window.addEventListener('blur', exports.reset) + window.addEventListener('focus', exports.reset) + }, + + keydown: function (e) { + // console.log(e.keyCode) + if (locked || e.altKey || e.metaKey || e.ctrlKey) { + return + } + switch ( e.keyCode ) { + + case 16: // shift + creeping = true + break - case 38: // up - case 87: // w - moveForward = true - break + case 38: // up + case 87: // w + moveForward = true + break - case 37: // left - case 65: // a - moveLeft = true - break + case 65: // a + moveLeft = true + break - case 40: // down - case 83: // s - moveBackward = true - break + case 40: // down + case 83: // s + moveBackward = true + break - case 39: // right - case 68: // d - moveRight = true - break + case 68: // d + moveRight = true + break - case 81: // q - turnLeft = true - break - - case 69: // e - turnRight = true - break - - case 82: // r - turnUp = true - break + case 37: // left + case 81: // q + turnLeft = true + break + + case 39: // right + case 69: // e + turnRight = true + break + + case 82: // r + turnUp = true + break - case 70: // f - turnDown = true - break + case 70: // f + turnDown = true + break - case 32: // space - if (gravity) { - jumping = true - vy = abs(vy) + jumpV * scale - if (e.shiftKey) { - vy *= -1 - } + case 32: // space + if (gravity) { + jumping = true + vy = abs(vy) + jumpV * scale + if (e.shiftKey) { + vy *= -1 + } + } + else { + if (e.shiftKey) { + moveDown = true } else { - if (e.shiftKey) { - moveDown = true - } - else { - moveUp = true - } + moveUp = true } - break + } + break - case 27: // esc - map.toggle() - break - } - }) + case 27: // esc + if (Scenery.nextMedia) { + Scenery.nextMedia = null + app.tube('cancel-scenery') + } + else if (Scenery.nextWallpaper) { + Scenery.nextWallpaper = null + app.tube('cancel-wallpaper') + } + else if (app.controller.mediaViewer && app.controller.mediaViewer.$el.hasClass("active")) { + app.controller.mediaViewer.hide() + $(".inuse").removeClass("inuse") + } + else if (app.controller.colorControl && app.controller.colorControl.$el.hasClass('active')) { + app.controller.colorControl.hide() + $(".inuse").removeClass("inuse") + } + else if (app.controller.wallpaperPicker && app.controller.wallpaperPicker.$el.hasClass('active')) { + app.controller.wallpaperPicker.hide() + $(".inuse").removeClass("inuse") + } + else if (app.controller.presets && app.controller.presets.$el.hasClass('active')) { + app.controller.presets.hide() + $(".inuse").removeClass("inuse") + } + else if (Rooms.shapesMode) { + // don't show map in editor for now.. + } + else { + app.controller.toolbar.toggleMap() + } + break + + case 8: // backspace + e.preventDefault() + if (app.controller.sculptureEditor && app.controller.sculptureEditor.sculpture) { + app.controller.sculptureEditor.sculpture.remove() + } + else if (app.controller.mediaEditor && app.controller.mediaEditor.scenery) { + app.controller.mediaEditor.scenery.remove() + } + else if (app.controller.textEditor && app.controller.textEditor.scenery) { + app.controller.textEditor.scenery.remove() + } + } + }, - document.addEventListener('keyup', function (e) { - if (locked) return; - switch ( e.keyCode ) { - - case 16: // shift - creeping = false - break + keyup: function (e) { + if (locked) return; + switch ( e.keyCode ) { + + case 16: // shift + creeping = false + break - case 38: // up - case 87: // w - moveForward = false - break + case 38: // up + case 87: // w + moveForward = false + break - case 37: // left - case 65: // a - moveLeft = false - break + case 65: // a + moveLeft = false + break - case 40: // down - case 83: // s - moveBackward = false - break + case 40: // down + case 83: // s + moveBackward = false + break - case 39: // right - case 68: // d - moveRight = false - break + case 68: // d + moveRight = false + break - case 81: // q - turnLeft = false - break - - case 69: // e - turnRight = false - break + case 37: // left + case 81: // q + turnLeft = false + break + + case 39: // right + case 69: // e + turnRight = false + break - case 82: // r - turnUp = false - break + case 82: // r + turnUp = false + break - case 70: // f - turnDown = false - break + case 70: // f + turnDown = false + break - case 32: // space - moveUp = moveDown = false - break + case 32: // space + moveUp = moveDown = false + break /* - case 48: // 0 - cam.rotationX = 0 - cam.rotationY = 0 - cam.x = 0 - cam.y = viewHeight - cam.z = 0 - break + case 48: // 0 + movements.center() + break */ - } - }) - - var mouseX, mouseY, dx, dy, rotX, rotY, dragging = false - document.addEventListener('mousedown', function (e) { - if (locked) return; - mouseX = e.pageX - mouseY = e.pageY - rotX = cam.rotationX - rotY = cam.rotationY - dragging = true - }) - - document.addEventListener('mousemove', function (e) { - if (locked || ! dragging) return - var dx = (e.pageX - mouseX) / window.innerWidth * Math.PI/3 - var dy = (e.pageY - mouseY) / window.innerHeight * Math.PI/3 - cam.rotationY = rotY + dx - cam.rotationX = clamp( rotX - dy, rotationX_min, rotationX_max ) - }) - - document.addEventListener('mouseup', function (e) { - dragging = false - }) - - window.addEventListener('blur', reset) - window.addEventListener('focus', reset) - function reset(){ - moveForward = moveLeft = moveBackward = moveRight = moveUp = moveDown = turnLeft = turnRight = jumping = dragging = creeping = false } + }, + + center: function(){ + cam.rotationX = 0 + cam.rotationY = 0 + cam.x = 0 + cam.y = viewHeight + cam.z = 0 + }, + mousedown: function (e) { + if (locked) return; + mouseX = e.pageX + mouseY = e.pageY + rotX = cam.rotationX + rotY = cam.rotationY + dragging = true + }, + + mousemove: function (e) { + if (locked || ! dragging) return + var dx = (e.pageX - mouseX) / window.innerWidth * Math.PI/3 + var dy = (e.pageY - mouseY) / window.innerHeight * Math.PI/3 + cam.rotationY = rotY + dx + cam.rotationX = clampRotation( rotX - dy ) + }, + + mouseup: function (e) { + dragging = false + }, + + reset: function(){ + moveForward = moveLeft = moveBackward = moveRight = moveUp = moveDown = turnLeft = turnRight = jumping = dragging = creeping = false + }, + + mousewheel: function (e, deltaY, deltaX) { + if (e.shiftKey) { + cam.rotationY -= deltaY / 150 + } + else { + pos.x += deltaY * Math.cos(cam.rotationY + Math.PI / 2) * 10 + pos.z += deltaY * Math.sin(cam.rotationY + Math.PI / 2) * 10 + app.tube("move", pos) + } }, - update: function () { + update: function (dt) { - if (locked) return; + if (locked) { return } var ry = cam.rotationY var s = creeping ? scale * creepFactor : scale - var vrrrr = creeping ? vr * creepFactor * 5 : vr + var vrrrr = creeping ? vr * creepFactor * 5 : vr * 0.5 var moving = moveForward || moveBackward || moveRight || moveLeft || moveUp || moveDown || turnLeft || turnRight || turnUp || turnDown vx = vz = 0 + + var vv = v +// vv *= dt / 100 * 8 +// s *= dt / 100 * 8 +// console.log(dt / 100 * 8) pos.x = cam.x pos.z = cam.z @@ -210,33 +298,33 @@ MX.Movements = function (cam) { if (moving) { if (moveForward) { - vx += v * Math.cos(ry + Math.PI / 2) * s - vz += v * Math.sin(ry + Math.PI / 2) * s + vx += vv * Math.cos(ry + Math.PI / 2) * s + vz += vv * Math.sin(ry + Math.PI / 2) * s } if (moveBackward) { - vx -= v * Math.cos(ry + Math.PI / 2) * s - vz -= v * Math.sin(ry + Math.PI / 2) * s + vx -= vv * Math.cos(ry + Math.PI / 2) * s + vz -= vv * Math.sin(ry + Math.PI / 2) * s } if (moveLeft) { - vx -= v * Math.cos(ry) * s - vz -= v * Math.sin(ry) * s + vx -= vv * Math.cos(ry) * s + vz -= vv * Math.sin(ry) * s } if (moveRight) { - vx += v * Math.cos(ry) * s - vz += v * Math.sin(ry) * s + vx += vv * Math.cos(ry) * s + vz += vv * Math.sin(ry) * s } if (moveUp) { - pos.y += v * scale + pos.y += vv * scale } if (moveDown) { - pos.y -= v * scale + pos.y -= vv * scale } if (turnUp) { - cam.rotationX = clamp( cam.rotationX - vrrrr*s, rotationX_min, rotationX_max) + cam.rotationX = clampRotation( cam.rotationX - vrrrr*s ) } if (turnDown) { - cam.rotationX = clamp( cam.rotationX + vrrrr*s, rotationX_min, rotationX_max) + cam.rotationX = clampRotation( cam.rotationX + vrrrr*s ) } if (turnLeft) { cam.rotationY += vrrrr*s @@ -261,15 +349,13 @@ MX.Movements = function (cam) { pos.y = viewHeight vy = 0 jumping = false - vz = vz || 1 } - var ceiling = (Rooms.mover.room ? Rooms.mover.room.height : 5000) + var ceiling = ((window.Rooms && Rooms.mover.room) ? Rooms.mover.room.height : 5000) - if (pos.y >= ceiling) { + if (pos.y >= ceiling-5) { vy = 0 - pos.y = ceiling - vz = vz || 1 + pos.y = ceiling-5 } } @@ -286,4 +372,6 @@ MX.Movements = function (cam) { velocity: function(n){ v = clamp(n, 1, 50) }, jumpVelocity: function(n){ jumpV = clamp(n, 1, 50) }, } + + return exports } diff --git a/public/assets/javascripts/mx/extensions/mx.movementsMobile.js b/public/assets/javascripts/mx/extensions/mx.movementsMobile.js index 994c8d7..95b61d1 100644 --- a/public/assets/javascripts/mx/extensions/mx.movementsMobile.js +++ b/public/assets/javascripts/mx/extensions/mx.movementsMobile.js @@ -14,56 +14,169 @@ MX.MobileMovements = function (cam) { var pos = { x: 0, y: viewHeight, z: 0, rotationX: 0, rotationY: 0 } var pointX, pointY, deltaX, deltaY, distX = 0, distY = 0, absDistX = 0, absDistY = 0, startTime - - return { + + var rotationX = 0, rotationY = 0, destRotationX = 0, destRotationY = 0 + var rotationSum = 0 + var rotationMedian = 0 + var orientationMax = 0 + var samples = 0 + var sampleThreshold = 120 + var lastAlpha + + var is_portrait + + var exports = { init: function () { - document.addEventListener("touchstart", function(e){ - if (e.touches.length == 1) { - touching = true - - startTime = Date.now() - - var point = event.touches[0] - pointX = point.pageX - pointY = point.pageY - distX = distY = 0 - pos.x = cam.x - pos.z = cam.z - pos.rotationY = cam.rotationY - } - }) - document.addEventListener("touchmove", function(e){ - e.preventDefault() - if (e.touches.length == 1) { + exports.orientationchange() + + document.addEventListener("touchstart", exports.touchstart) + document.addEventListener("touchmove", exports.touchmove) + document.addEventListener("touchend", exports.touchend) + window.addEventListener('orientationchange', exports.orientationchange) + window.addEventListener("devicemotion", exports.devicemotion) + window.addEventListener("deviceorientation", exports.deviceorientation) + }, + touchstart: function(e){ + if (e.touches.length == 1) { + touching = true + + startTime = Date.now() + + var point = event.touches[0] + pointX = point.pageX + pointY = point.pageY + distX = distY = 0 + pos.x = cam.x + pos.z = cam.z + pos.rotationY = cam.rotationY + } + }, + touchmove: function(e){ + e.preventDefault() + if (e.touches.length == 1) { + + var timestamp = Date.now() + var point = event.touches[0] + deltaX = point.pageX - pointX + deltaY = point.pageY - pointY + + pointX = point.pageX + pointY = point.pageY + + distX += deltaX + distY += deltaY + absDistX = abs(distX) + absDistY = abs(distY) + } + }, + touchend: function(e){ + e.preventDefault() + if (e.touches.length == 0) { + touching = directionLocked = false + var timestamp = Date.now() + var duration = startTime - timestamp + if (duration < 300) { + } + } + }, + orientationchange: function(e){ + is_portrait = window.innerWidth < window.innerHeight + if (is_portrait) { + lastAlpha = 0 + } + }, + devicemotion: function(e) { + if (! is_portrait) return; + var rotationBeta = e.rotationRate.alpha; // weird! + rotationSum += rotationBeta; + samples += 1; + }, + deviceorientation: function (e) { + if (! lastAlpha) { lastAlpha = e.alpha } + is_portrait ? exports.portraitorientation(e) : exports.landscapeorientation(e) + }, + portraitorientation: function(e) { + // compass gives most accurate orientation in portrait mode + var alpha, dx = 0, dy = 0 + + if (e.webkitCompassHeading) { + alpha = 180 - e.webkitCompassHeading; + } + else { + alpha = 180 - e.alpha; + } + + // android rotates in reverse + if (is_android) { + alpha = 360 - alpha + } + + // use rotationRate to gauge if we've tilted the screen past vertical + // for looking at ceiling + if (e.beta > orientationMax) { + orientationMax = e.beta + rotationMedian = rotationSum + } + + // this number was only going to 83 max.. not 90.. weird + var beta = e.beta + 7; + + // if we've got enough motion data, we should be able to determine + // if we've tilted backwards. otherwise, lock to the horizon. + if (! is_android && samples > sampleThreshold) { + dx = rotationSum > rotationMedian ? e.beta - 90 : 90 - e.beta + } + else { + dx = 0 + } + + // avoid jumping around in a circle + if (Math.abs(alpha - lastAlpha) < 100 || Math.abs(alpha - lastAlpha) > 300) { + dy = alpha - lastAlpha + lastAlpha = alpha + } + + // avoid jumping around in a circle #2 + if (dy > 300) { + dy -= 360 + } else if (dy < -300) { + dy += 360 + } + + destRotationX = MX.toRad(dx) + destRotationY += MX.toRad(dy) + }, + + landscapeorientation: function (e) { + var dx, dy + + dx = e.gamma > 0 ? 90 - e.gamma : 90 + e.gamma + dy = e.alpha - lastAlpha + lastAlpha = e.alpha - var timestamp = Date.now() - var point = event.touches[0] - deltaX = point.pageX - pointX - deltaY = point.pageY - pointY - - pointX = point.pageX - pointY = point.pageY + // avoid the sudden jump from 0 to -360 + if (dy > 300) { + dy -= 360 + } + else if (dy < -300) { + dy += 360 + } - distX += deltaX - distY += deltaY - absDistX = abs(distX) - absDistY = abs(distY) - } - }) - document.addEventListener("touchend", function(e){ - e.preventDefault() - if (e.touches.length == 0) { - touching = directionLocked = false - var timestamp = Date.now() - var duration = startTime - timestamp - if (duration < 300) { - } - } - }) + destRotationX = dx > 45 ? 0 : MX.toRad(dx) + destRotationY += MX.toRad(dy) }, update: function () { + var drx, dry + + dry = (destRotationY - rotationY) / 6 + drx = (destRotationX - rotationX) / 6 + rotationY += dry + rotationX += drx + cam.rotationY = pos.rotationY += dry + cam.rotationX = pos.rotationX += drx + if (distX || distY) { var oldDistY = absDistY, oldDistX = absDistX absDistY = avg(absDistY, 0, 5) @@ -77,8 +190,7 @@ MX.MobileMovements = function (cam) { pos.x -= dy * Math.cos(pos.rotationY + Math.PI / 2) pos.z -= dy * Math.sin(pos.rotationY + Math.PI / 2) - pos.rotationY += dx / (window.innerWidth) * Math.PI / 2 - cam.rotationY = pos.rotationY + cam.rotationY = pos.rotationY += dx / (window.innerWidth) * Math.PI / 2 app.tube("move", pos) } @@ -93,6 +205,7 @@ MX.MobileMovements = function (cam) { jumpVelocity: function(n){ jumpV = clamp(n, 1, 50) }, } + return exports } diff --git a/public/assets/javascripts/mx/extensions/mx.orbitCamera.js b/public/assets/javascripts/mx/extensions/mx.orbitCamera.js new file mode 100644 index 0000000..a936cef --- /dev/null +++ b/public/assets/javascripts/mx/extensions/mx.orbitCamera.js @@ -0,0 +1,97 @@ +MX.OrbitCamera = function(opt){ + var exports = {}, bound = false + exports.opt = opt = defaults(opt, { + el: window, // object to bind events on + camera: scene.camera, // camera object we'll be moving + radius: 100, + radiusRange: [ 10, 1000 ], + rotationX: PI/2, + rotationY: 0, + center: { x: 0, y: 0, z: 0 }, + sensitivity: 10, // moving 1 pixel is like moving N radians + wheelSensitivity: 10, + ease: 10, + }) + var rx, ry, radius, px, py, epsilon = 1e-10, dragging = false + exports.init = function(){ + ry = opt.rotationY + rx = opt.rotationX + radius = opt.radius + exports.wheel = new wheel({ + el: opt.el, + update: function(e, delta){ + opt.radius = clamp( opt.radius + delta * opt.wheelSensitivity, opt.radiusRange[0], opt.radiusRange[1] ) + }, + }) + exports.bind() + } + exports.toggle = function(state){ + if (state) exports.bind() + else exports.unbind() + } + exports.bind = function(){ + if (bound) return; + bound = true + opt.el.addEventListener("mousedown", down) + window.addEventListener("mousemove", move) + window.addEventListener("mouseup", up) + exports.wheel.unlock() + } + exports.unbind = function(){ + if (! bound) return; + bound = false + opt.el.removeEventListener("mousedown", down) + window.removeEventListener("mousemove", move) + window.removeEventListener("mouseup", up) + exports.wheel.lock() + } + function down (e) { + px = e.pageX + py = e.pageY + dragging = true + } + function move (e) { + if (! dragging) return + exports.delta(px- e.pageX, py - e.pageY) + px = e.pageX + py = e.pageY + } + function up (e) { + dragging = false + } + exports.delta = function(x,y){ + opt.rotationY += x/window.innerWidth * opt.sensitivity + opt.rotationX = clamp( opt.rotationX + y/window.innerHeight * opt.sensitivity, 0, PI) + } + exports.move = function(y, x){ + opt.rotationY = y + if (typeof x == "number") { opt.rotationX = x } + } + exports.update = function(){ + if (! bound) return + if (abs(ry - opt.rotationY) > epsilon) { + ry = avg(ry, opt.rotationY, opt.ease) + } + else { + ry = opt.rotationY + } + if (abs(rx - opt.rotationX) > epsilon) { + rx = avg(rx, opt.rotationX, opt.ease) + } + else { + rx = opt.rotationX + } + if (abs(radius - opt.radius) > epsilon) { + radius = avg(radius, opt.radius, opt.ease) + } + else { + radius = opt.radius + } + opt.camera.x = opt.center.x + radius * sin(rx) * cos(ry) + opt.camera.y = opt.center.y + radius * cos(rx) + opt.camera.z = opt.center.z + radius * sin(rx) * sin(ry) + opt.camera.rotationX = PI/2 - rx + opt.camera.rotationY = ry + PI/2 + } + return exports +} diff --git a/public/assets/javascripts/mx/mx.js b/public/assets/javascripts/mx/mx.js index 40a5f2e..60651eb 100644 --- a/public/assets/javascripts/mx/mx.js +++ b/public/assets/javascripts/mx/mx.js @@ -25,7 +25,7 @@ var MX = MX || (function (undefined) { var MX = { version: '0.1.0', prefix: undefined, - rotationUnit: 'rad' + rotationUnit: 'rad', } var floatPrecision = 5 @@ -162,24 +162,24 @@ var MX = MX || (function (undefined) { Object.defineProperty(this, 'width', { get: function () { return width - || parseInt(self.el.style.width, 10) + || parseInt(self.el.style.width, 10) * app_devicePixelRatio || 0 }, set: function (val) { width = val - this.el.style.width = width + 'px' + this.el.style.width = (width/app_devicePixelRatio) + 'px' } }) Object.defineProperty(this, 'height', { get: function () { return height - || parseInt(self.el.style.height, 10) + || parseInt(self.el.style.height, 10) * app_devicePixelRatio || 0 }, set: function (val) { height = val - this.el.style.height = height + 'px' + this.el.style.height = (height/app_devicePixelRatio) + 'px' } }) } @@ -302,9 +302,9 @@ var MX = MX || (function (undefined) { + (-this.y).toFixed(floatPrecision) + 'px,' + (-this.z).toFixed(floatPrecision) + 'px) ' + 'scale3d(' - + this.scaleX.toFixed(floatPrecision) + ',' - + this.scaleY.toFixed(floatPrecision) + ',' - + this.scaleZ.toFixed(floatPrecision) + ') ' + + (app_devicePixelRatio * this.scaleX).toFixed(floatPrecision) + ',' + + (app_devicePixelRatio * this.scaleY).toFixed(floatPrecision) + ',' + + (app_devicePixelRatio * this.scaleZ).toFixed(floatPrecision) + ') ' if (rotationTranslation) { transformString += rotationTranslation.before diff --git a/public/assets/javascripts/mx/primitives/mx.grid.js b/public/assets/javascripts/mx/primitives/mx.grid.js new file mode 100644 index 0000000..a765c89 --- /dev/null +++ b/public/assets/javascripts/mx/primitives/mx.grid.js @@ -0,0 +1,70 @@ +MX.Grid = MX.Object3D.extend({ + init: function (ops) { + + this.type = "Grid" + + var ops = this.ops = defaults(ops, { + x: 0, + y: 0, + z: 0, + space: 20, + cells: 20, + }) + + ops.side = ops.space * ops.cells + + var ctx, canvas = document.createElement("canvas") + + this.el = canvas + this.width = this.height = canvas.width = canvas.height = ops.side + 4 + + ctx = canvas.getContext('2d') + + this.x = ops.x || 0 + this.y = ops.y || 0 + this.z = ops.z || 0 + this.rotationX = PI/2 + this.scale = ops.scale || 1 + this.backface = ops.backface || false + + ops.className && this.el.classList.add(ops.className) + this.backface && this.el.classList.add("backface-visible") + this.el.classList.add("mx-grid") + + this.draw(ctx) + }, + + draw: function(ctx, recenter){ + ctx = ctx || this.ctx + + if (recenter) { + ctx.save() + ctx.translate( -grid.width/2, -grid.height/2 ) + } + + var cells = this.ops.cells, + space = this.ops.space, + side = this.ops.side + + ctx.strokeStyle = "#444" + ctx.lineWidth = 1 + ctx.beginPath() + ctx.translate(1,1) + for (var i = 0; i <= cells; i++) { + ctx.moveTo(i*space, 0) + ctx.lineTo(i*space, side) + ctx.moveTo(0, i*space) + ctx.lineTo(side, i*space) + } + ctx.closePath() + ctx.stroke() + + if (recenter) { + ctx.restore() + } + + }, + +}) + + diff --git a/public/assets/javascripts/mx/primitives/mx.image.js b/public/assets/javascripts/mx/primitives/mx.image.js index 575e9c0..ce99592 100644 --- a/public/assets/javascripts/mx/primitives/mx.image.js +++ b/public/assets/javascripts/mx/primitives/mx.image.js @@ -1,6 +1,7 @@ MX.Image = MX.Object3D.extend({ init: function (ops) { - + ops = ops || {} + this.type = "Image" this.media = ops.media this.width = 0 @@ -12,13 +13,13 @@ MX.Image = MX.Object3D.extend({ this.backface = ops.backface || false ops.className && this.el.classList.add(ops.className) - this.backface && this.el.classList.add("backface-visible") + this.backface && this.el.classList.add("backface-visible") this.el.classList.add("image") this.el.classList.add("mx-scenery") this.el.style.backgroundRepeat = 'no-repeat' - this.load(ops) + ops.src && this.load(ops) }, load: function(ops){ @@ -41,8 +42,41 @@ MX.Image = MX.Object3D.extend({ layer.el.classList.add('image') layer.dirty = true layer.update() + layer.ops.onload + + if (ops.keepImage) { + layer.image = image + } + } + + if (ops.src) { + image.src = ops.src + } + else if (ops.media) { + image.src = ops.media.url + } + else if (ops.url) { + image.src = ops.url + } + }, + + draw: function(ctx, recenter){ + if (! this.image) { return } + + if (recenter) { + ctx.save() + ctx.scale(-1, 1) + ctx.translate( -this.width/2 * this.scale, -this.height/2 * this.scale ) + } + + ctx.drawImage(this.image, + 0, 0, this.image.naturalWidth, this.image.naturalHeight, + 0, 0, this.image.width * this.scale * devicePixelRatio, this.image.height * this.scale * devicePixelRatio + ) + + if (recenter) { + ctx.restore() } - image.src = ops.src; }, }) diff --git a/public/assets/javascripts/mx/primitives/mx.point.js b/public/assets/javascripts/mx/primitives/mx.point.js new file mode 100644 index 0000000..41a7732 --- /dev/null +++ b/public/assets/javascripts/mx/primitives/mx.point.js @@ -0,0 +1,17 @@ +MX.Point = MX.Object3D.extend({ + init: function(p){ + this.updateChildren = false + this.move({ + x: p.a, + y: 11, + z: p.b, + width: 20, + height: 20, + rotationX: PI/2, + }) + this.el.style.backgroundColor = 'rgb(' + [abs(floor(p.a*30)), 0, abs(floor(p.b*30))] + ')' + this.el.style.backfaceVisibility = "visible" + this.el.style.borderRadius = "50%" + scene.add(this) + } +}) diff --git a/public/assets/javascripts/mx/primitives/mx.polyline.js b/public/assets/javascripts/mx/primitives/mx.polyline.js new file mode 100644 index 0000000..e549150 --- /dev/null +++ b/public/assets/javascripts/mx/primitives/mx.polyline.js @@ -0,0 +1,57 @@ +MX.Polyline = MX.Object3D.extend({ + init: function(polyline){ + this.faces = [] + this.points = polyline.points + for (var i = 1; i < this.points.length; i++) { + var mx = new MX.Object3D() + var head = this.points[i-1] + var tail = this.points[i] + this.move_face(mx, head, tail) + this.faces.push(mx) + scene.add(mx) + } + }, + + rebuild: function(){ + for (var i = 1; i < this.points.length; i++) { + var mx = this.faces[i-1] + var head = this.points[i-1] + var tail = this.points[i] + this.move_face(mx, head, tail) + } + }, + + move_face: function (mx, head, tail){ + var mid_x = (head.a + tail.a) + var mid_z = (head.b + tail.b) + var len = head.distanceTo( tail ) + var angle = atan2( head.b - tail.b, head.a - tail.a ) + mx.move({ + x: mid_x / 2, + y: wallHeight/2 + 1, + z: mid_z / 2, + width: ceil(len), + height: wallHeight, + rotationY: angle + }) + // var hue = abs(round( angle / PI * 90 + 300)) + // mx.el.style.backgroundColor = 'hsl(' + [hue, "100%", "50%"] + ')' + var lum = abs(round( angle / PI * 20 + 70)) + mx.el.style.backgroundColor = 'hsla(' + ["0", "0%", lum + "%", "0.99"] + ')' + }, + + set_height: function(height){ + for (var i = 0; i < this.faces.length; i++) { + this.faces[i].height = height + this.faces[i].y = height / 2 + 1 + } + }, + + destroy: function(){ + this.faces.forEach(function(mx){ + scene.remove(mx) + }) + this.faces = null + this.points = null + }, +}) diff --git a/public/assets/javascripts/mx/primitives/mx.soundcloud.js b/public/assets/javascripts/mx/primitives/mx.soundcloud.js new file mode 100644 index 0000000..fecb2f4 --- /dev/null +++ b/public/assets/javascripts/mx/primitives/mx.soundcloud.js @@ -0,0 +1,130 @@ +MX.Soundcloud = MX.Object3D.extend({ + init: function (ops) { + + this.type = "Soundcloud" + this.media = ops.media + this.width = 0 + this.height = 0 + this.x = ops.x || 0 + this.y = ops.y || 0 + this.z = ops.z || 0 + this.scale = ops.scale || 1 + this.backface = ops.backface || false + + ops.className && this.el.classList.add(ops.className) + this.backface && this.el.classList.add("backface-visible") + this.el.classList.add("audio") + this.el.classList.add("mx-scenery") + + this.el.style.backgroundRepeat = 'no-repeat' + this.paused = true + + this.ops = ops + }, + + load: function(ops){ + if (ops) { + ops = this.ops = defaults(ops, this.ops) + } + else { + ops = this.ops + } + + this.width = ops.media.width + this.height = ops.media.height + + var tag = Parser.lookup.soundcloud.tag(ops.media) + var $iframe = $(tag) + var iframe = $iframe[0] + $iframe.css('z-index', '-1') + this.el.appendChild( iframe ) + + var overlay = this.overlay = document.createElement("div") + overlay.style.width = "100%" + overlay.style.height = "100%" + overlay.style.position = "absolute" + overlay.style.top = "0" + overlay.style.left = "0" + overlay.style.zIndex = "2" + overlay.className = "overlay" + this.el.appendChild(overlay) + + this.player = SC.Widget( iframe ) + this.player.setVolume(80) + + this.duration = 0 + + this.player.bind(SC.Widget.Events.READY, this.ready.bind(this)) +// this.player.bind(SC.Widget.Events.LOAD_PROGRESS, this.loadProgress.bind(this)) +// this.player.bind(SC.Widget.Events.PLAY_PROGRESS, this.playProgress.bind(this)) + this.player.bind(SC.Widget.Events.PLAY, this.didPlay.bind(this)) + this.player.bind(SC.Widget.Events.PAUSE, this.didPause.bind(this)) + this.player.bind(SC.Widget.Events.FINISH, this.finished.bind(this)) + }, + + ready: function(){ + this.seek( this.media.keyframe || 0 ) + + if (this.media.autoplay) { + this.play() + } + + this.player.getDuration(function(duration){ + this.duration = duration + }.bind(this)) + }, + + play: function(){ + this.player.play() + }, + + pause: function(){ + this.player.pause() + }, + + toggle: function(state){ + if (typeof state === "boolean") { + if (state) this.play() + else this.pause() + } + else { + this.player.toggle() + } + }, + + seek: function(n){ + if (n < 1) { + n = n * this.duration + } + this.player.seekTo(n) + }, + + setLoop: function(state){ + this.media.loop = state + }, + + setVolume: function(n){ + if (this.muted || ! this.player) return + this.player.setVolume(floor( n * 100 )) + }, + + didPlay: function(){ + this.paused = false + }, + + didPause: function(){ + this.paused = true + }, + + finished: function(){ + console.log("soundcloud finished") + if (this.media.loop) { + this.seek(0) + this.play() + } + else if (this.bound) { + $(".playButton").removeClass('playing') + } + }, + +}) diff --git a/public/assets/javascripts/mx/primitives/mx.text.js b/public/assets/javascripts/mx/primitives/mx.text.js index 9c7af5c..3095b67 100644 --- a/public/assets/javascripts/mx/primitives/mx.text.js +++ b/public/assets/javascripts/mx/primitives/mx.text.js @@ -3,34 +3,57 @@ MX.Text = MX.Object3D.extend({ init: function (ops) { this.type = "Text" - - var layer = new MX.Object3D('text') - layer.width = ops.width || 100 - layer.height = ops.height || 50 - layer.x = ops.x || 0 - layer.y = ops.y || 0 - layer.z = ops.z || 0 - layer.scale = ops.scale || 1 - layer.el.innerHTML = ops.value || "" - if (ops.id) layer.el.id = ops.id; - if (ops.background) layer.el.style.background = ops.background; - if (ops.color) layer.el.style.color = ops.color; - if (ops.fontSize) layer.el.style.fontSize = ops.fontSize + "px"; - this.add(layer) + this.type = "Image" + this.media = ops.media + this.width = 0 + this.height = 0 + this.x = ops.x || 0 + this.y = ops.y || 0 + this.z = ops.z || 0 + this.scale = ops.scale || 1 + this.backface = ops.backface || false + + this.scale = ops.scale || 1 + this.width = ops.media.width + this.height = ops.media.height + this.x = ops.x || 0 + this.y = ops.y || 0 + this.z = ops.z || 0 + this.rotationX = ops.rotationX || 0 + this.rotationY = ops.rotationY || 0 + this.rotationZ = ops.rotationZ || 0 + + ops.className && this.el.classList.add(ops.className) + this.backface && this.el.classList.add("backface-visible") + this.el.classList.add("mx-text") + this.el.classList.add("mx-scenery") + + this.inner = document.createElement("div") + this.inner.classList.add("inner") + this.el.appendChild(this.inner) + + this.load(ops) + }, + + load: function(ops){ + var media = ops.media + if (media.font) this.setFont(media.font) - this.children.forEach(function (c, i) { - if (ops.classname) { - c.el.classList.add(ops.classname) - } - else { - } - c.el.style.backgroundRepeat = 'no-repeat' - }) + this.setText( media.description ) + }, + + setFont: function(font){ + if (! font.color || font.color[0] == "#") { font.color = [0,0,0] } - this.dirty = true - this.updateChildren = true - this.update() - } + this.inner.style.fontFamily = "'" + font.family + "',sans-serif" + this.el.style.fontSize = (font.size / devicePixelRatio) + "pt" + this.el.style.textAlign = font.align + this.el.style.color = rgb_string(font.color) + }, + + setText: function(text){ + this.inner.innerHTML = marked( text || "" ) + }, }) diff --git a/public/assets/javascripts/mx/primitives/mx.video.js b/public/assets/javascripts/mx/primitives/mx.video.js index 12d3dcb..53ccf2e 100644 --- a/public/assets/javascripts/mx/primitives/mx.video.js +++ b/public/assets/javascripts/mx/primitives/mx.video.js @@ -21,6 +21,7 @@ MX.Video = MX.Object3D.extend({ this.el.classList.add("video") this.el.classList.add("mx-scenery") this.paused = !! this.media.autoplay + this.playing = false this.muted = app.muted || !! this.media.mute }, @@ -31,8 +32,8 @@ MX.Video = MX.Object3D.extend({ this.player.addEventListener("loadedmetadata", this.ready.bind(this)) this.player.addEventListener("error", this.error.bind(this)) this.player.addEventListener("ended", this.finished.bind(this)) - this.player.width = this.width - this.player.height = this.height + this.player.width = "100%" + this.player.height = "100%" this.player.src = this.media.url this.player.load() @@ -45,6 +46,9 @@ MX.Video = MX.Object3D.extend({ if (this.media.mute) { this.mute() } + else { + this.unmute() + } if (this.media.autoplay) { this.play() @@ -57,11 +61,13 @@ MX.Video = MX.Object3D.extend({ play: function(){ this.paused = false + this.playing = true this.player.play() }, pause: function(){ this.paused = true + this.playing = false this.player.pause() }, @@ -74,14 +80,21 @@ MX.Video = MX.Object3D.extend({ mute: function(){ this.player.muted = true + this.player.volume = 0 this.muted = true }, unmute: function(){ this.player.muted = false + this.player.volume = 0.8 this.muted = false }, + setVolume: function(n){ + if (this.muted || ! this.player) return + this.player.volume = n + }, + setLoop: function(state){ this.media.loop = state }, diff --git a/public/assets/javascripts/mx/primitives/mx.vimeo.js b/public/assets/javascripts/mx/primitives/mx.vimeo.js index 4922519..af138ae 100644 --- a/public/assets/javascripts/mx/primitives/mx.vimeo.js +++ b/public/assets/javascripts/mx/primitives/mx.vimeo.js @@ -20,6 +20,7 @@ MX.Vimeo = MX.Object3D.extend({ this.backface && this.el.classList.add("backface-visible") this.el.classList.add("video") this.el.classList.add("mx-scenery") + this.playing = false this.paused = !! this.media.autoplay this.muted = app.muted || !! this.media.mute this.started = false @@ -32,8 +33,8 @@ MX.Vimeo = MX.Object3D.extend({ preload.id = uid preload.setAttribute("src", "//player.vimeo.com/video/" + this.media.token + "?api=1&badge=0&controls=0branding=0&byline=0&portrait=0&title=0&" + loop + "&player_id=" + uid) preload.style.backgroundImage = "url(" + this.media.thumbnail + ")" - preload.style.width = this.media.width + "px" - preload.style.height = this.media.height + "px" + preload.style.width = "100%" + preload.style.height = "100%" preload.style.border = "0" preload.style.pointerEvents = "none" preload.className = "preload" @@ -62,6 +63,9 @@ MX.Vimeo = MX.Object3D.extend({ if (this.media.mute) { this.mute() } + else { + this.unmute() + } this.seek( this.media.keyframe || 0 ) @@ -96,7 +100,7 @@ MX.Vimeo = MX.Object3D.extend({ return } - if (! this.started || n === 0) { + if (! this.started) { return } @@ -127,6 +131,11 @@ MX.Vimeo = MX.Object3D.extend({ this.player.api('setVolume', 0.8) this.muted = false }, + + setVolume: function(n){ + if (this.muted || ! this.player) return + this.player.api('setVolume', n) + }, setLoop: function(state){ this.media.loop = state @@ -137,19 +146,26 @@ MX.Vimeo = MX.Object3D.extend({ if (this.paused) { this.pause() } + else { + this.playing = true + } }, onPause: function(){ if (! this.paused) { this.play() } + else { + this.playing = false + } }, finished: function(){ -// if (this.media.loop) { -// this.seek(0) -// this.play() -// } + console.log("vimeo finished") + if (this.media.loop) { + this.seek(0) + this.play() + } // else if (this.bound) { if (! this.media.loop && this.bound) { $(".playButton").removeClass('playing') diff --git a/public/assets/javascripts/mx/primitives/mx.youtube.js b/public/assets/javascripts/mx/primitives/mx.youtube.js index 873348f..8cd9f59 100644 --- a/public/assets/javascripts/mx/primitives/mx.youtube.js +++ b/public/assets/javascripts/mx/primitives/mx.youtube.js @@ -21,6 +21,7 @@ MX.Youtube = MX.Object3D.extend({ this.el.classList.add("video") this.el.classList.add("mx-scenery") this.paused = !! this.media.autoplay + this.playing = false this.muted = app.muted || !! this.media.mute }, @@ -31,12 +32,25 @@ MX.Youtube = MX.Object3D.extend({ preload.id = uid preload.style.backgroundImage = "url(" + this.media.thumbnail + ")" preload.style.backgroundSize = "cover" - preload.style.width = this.media.width + "px" - preload.style.height = this.media.height + "px" + preload.style.width = "100%" + preload.style.height = "100%" preload.style.pointerEvents = "none" + preload.style.position = "absolute" + preload.style.top = "0" + preload.style.left = "0" + preload.style.zIndex = "1" preload.className = "preload" this.el.appendChild(preload) + var overlay = this.overlay = document.createElement("div") + overlay.style.width = "100%" + overlay.style.height = "100%" + overlay.style.position = "absolute" + overlay.style.top = "0" + overlay.style.left = "0" + overlay.style.zIndex = "2" + overlay.className = "overlay" + this.el.appendChild(overlay) this.defer(uid) }, @@ -96,6 +110,9 @@ MX.Youtube = MX.Object3D.extend({ if (this.media.mute) { this.mute() } + else { + this.unmute() + } this.seek( this.media.keyframe || 0 ) }, @@ -127,11 +144,13 @@ MX.Youtube = MX.Object3D.extend({ play: function(){ this.paused = false + this.playing = true this.player.playVideo() }, pause: function(){ this.paused = true + this.playing = false this.player.pauseVideo() }, @@ -157,6 +176,11 @@ MX.Youtube = MX.Object3D.extend({ this.player.setVolume(80) this.muted = false }, + + setVolume: function(n){ + if (this.muted || ! this.player || ! this.player.setVolume) return + this.player.setVolume( floor(n * 100) ) + }, setLoop: function(state){ this.media.loop = state diff --git a/public/assets/javascripts/rectangles/_env.js b/public/assets/javascripts/rectangles/_env.js index 4b14a21..b3c7d66 100644 --- a/public/assets/javascripts/rectangles/_env.js +++ b/public/assets/javascripts/rectangles/_env.js @@ -1,6 +1,22 @@ var environment = new function(){} environment.init = function(){ + scene = new MX.Scene().addTo('#scene') + scene.width = window.innerWidth + scene.height = window.innerHeight + scene.perspective = window.innerHeight + + cam = scene.camera + cam.y = viewHeight + + if (is_mobile) { + app.movements = new MX.MobileMovements(cam, viewHeight) + } + else { + app.movements = new MX.Movements(cam, viewHeight) + } + app.movements.init() + map = new Map () if (window.scene) { @@ -10,21 +26,30 @@ environment.init = function(){ "z": 0, "rotationX": 0, // PI/2, "rotationY": PI/2, // PI -// "rotationX": 0, -// "rotationY": PI + // "rotationX": 0, + // "rotationY": PI }) scene.camera.radius = 20 } - - app.movements.gravity(true) - + + window.onresize = function () { + scene.width = window.innerWidth + scene.height = window.innerHeight + scene.perspective = window.innerHeight + scene.update() + } + Rooms.init() + Walls.init() Scenery.init() + Sculpture.init() scene.update() environment.update() + app.movements.gravity(true) + var minimap_el = document.querySelector("#minimap .el") if (minimap_el) { window.minimap = new Map ({ @@ -50,8 +75,13 @@ environment.init = function(){ } }) } -environment.update = function(t){ +environment.minimal = function(){ + environment.update = function(t){} +} +environment.update = function(t, dt){ + app.movements.update(dt || 0) + scene.update() map.update() - window.minimap && window.minimap.update && minimap.update() + window.minimap && minimap.update && minimap.update() z = false } diff --git a/public/assets/javascripts/rectangles/engine/map/_map.js b/public/assets/javascripts/rectangles/engine/map/_map.js index 99ede82..ba3ec92 100644 --- a/public/assets/javascripts/rectangles/engine/map/_map.js +++ b/public/assets/javascripts/rectangles/engine/map/_map.js @@ -15,6 +15,8 @@ var Map = function(opt){ var base = this base.el = opt.el + base.$el = $(base.el) + base.opt = opt if (! base.el) return @@ -32,7 +34,6 @@ var Map = function(opt){ scene.camera.x + sides.a, scene.camera.z + sides.b ) } - base.set_zoom = function (n) { n = clamp(n, opt.zoom_min, opt.zoom_max) base.zoom_exponent = n @@ -40,15 +41,40 @@ var Map = function(opt){ } base.set_zoom(opt.zoom) + base.resize = function(w, h){ + if (w && h) { + canvas.width = base.dimensions.a = w + canvas.height = base.dimensions.b = h + } + else { + // resize here - esp if 2d-hires + canvas.width = base.dimensions.a = base.el.parentNode.offsetWidth + canvas.height = base.dimensions.b = base.el.parentNode.offsetHeight + } + } + + base.toggle = function(state){ + return $(base.el).toggle(state).is(':visible') + } + var canvas = base.canvas = document.createElement("canvas") canvas.width = base.dimensions.a canvas.height = base.dimensions.b - + base.el.appendChild(canvas) - + switch (opt.type) { + case "ortho": + base.draw = new Map.Draw (base, { ortho: true }) + base.draw.grid_size = MAP_GRID_SIZE + base.ui = new Map.UI.Ortho (base) + base.sides = base.sides_for_center + $(window).resize(base.resize) + break + case "editor": base.draw = new Map.Draw (base) + base.draw.grid_size = MAP_GRID_SIZE base.ui = new Map.UI.Editor (base) base.sides = base.sides_for_center $(window).resize(base.resize) @@ -56,20 +82,11 @@ var Map = function(opt){ case "minimap": base.draw = new Map.Draw (base, { center: scene.camera, minimap: true }) + base.draw.grid_size = MAP_GRID_SIZE * 10 base.ui = new Map.UI.Minimap (base) base.sides = base.sides_for_camera break } - - base.resize = function(){ - canvas.width = base.dimensions.a = window.innerWidth - canvas.height = base.dimensions.b = window.innerHeight - } - - base.toggle = function(){ - $(base.el).toggle() - } - } Map.prototype.update = function(){ diff --git a/public/assets/javascripts/rectangles/engine/map/draw.js b/public/assets/javascripts/rectangles/engine/map/draw.js index 3e185d2..4f4759f 100644 --- a/public/assets/javascripts/rectangles/engine/map/draw.js +++ b/public/assets/javascripts/rectangles/engine/map/draw.js @@ -5,79 +5,129 @@ Map.Draw = function(map, opt){ var draw = this - var ctx = map.canvas.getContext("2d") + var ctx = draw.ctx = map.canvas.getContext("2d") draw.animate = function(){ ctx.save() draw.clear() - // draw.ruler() + draw.fill("rgba(255,255,255,0.98)") - if (opt.minimap) { + if (opt.ortho) { + } + else if (opt.minimap) { ctx.translate( map.dimensions.a * 1/2, map.dimensions.b * 1/2) ctx.scale( map.zoom, map.zoom ) + if (map.opt.pivot) { ctx.rotate(scene.camera.rotationY + PI) } ctx.translate( opt.center.x, - opt.center.z ) ctx.scale( -1, 1 ) draw.coords() - draw.regions(Rooms.regions, [ "#fff" ]) + if (Rooms.shapesMode) { + shapes.draw(draw.ctx, "#fff", null) + shapes.draw(draw.ctx, null, "#000") + } + else { + draw.regions(Rooms.regions, [ "#fff" ], "#000") + } draw.camera(scene.camera) } - else { ctx.translate( map.dimensions.a * 1/2, map.dimensions.b * 1/2) ctx.scale( map.zoom, map.zoom ) ctx.translate( map.center.a, map.center.b ) ctx.scale( -1, 1 ) - draw.regions(Rooms.regions, [ "#f8f8f8" ]) + draw.regions(Rooms.regions, [ "#f8f8f8" ], "#000") draw.mouse(map.ui.mouse.cursor) + draw.mouse_dimensions(map.ui.mouse.cursor) draw.coords() scene && draw.camera(scene.camera) } ctx.restore() } + draw.translate = function(){ + ctx.translate( map.dimensions.a * 1/2, map.dimensions.b * 1/2) + ctx.scale( map.zoom, map.zoom ) + ctx.translate( map.center.a, map.center.b ) + ctx.scale( -1, 1 ) + } + // changes the ctx temporarily draw.render = function(){ - ctx.save() + var thumbnail_width = 1000 + var thumbnail_height = 750 + + var extent = Rooms.extent() + var center = extent.center() + var extent_width = extent.width() + var extent_height = extent.height() + var zoom + if (extent_width > extent_height) { + zoom = thumbnail_width / extent.width() * 0.9 + } + else { + zoom = thumbnail_height / extent.height() * 0.9 + } + + var canvas = document.createElement("canvas") + canvas.width = thumbnail_width + canvas.height = thumbnail_height + ctx = canvas.getContext('2d') + draw.clear() - ctx.translate( map.dimensions.a * 1/2, map.dimensions.b * 1/2) - ctx.scale( map.zoom, map.zoom ) - ctx.translate( map.center.a, map.center.b ) + ctx.translate( thumbnail_width * 1/2, thumbnail_height * 1/2) + ctx.scale( zoom, zoom ) + ctx.translate( center.a, -center.b ) ctx.scale( -1, 1 ) - draw.regions(Rooms.regions, ["#fff"]) - draw.mouse(map.ui.mouse.cursor) - scene && draw.camera(scene.camera) - + draw.regions(Rooms.regions, ["#fff"], null) + ctx.restore() + + // invert opacity + var pixelData = ctx.getImageData(0, 0, canvas.width, canvas.height) + var pixels = pixelData.data + for (var i = 0, k, _len = pixels.length; i < _len; i++) { + k = i*4 + if (pixels[k+3] == 0) { + pixels[k] = pixels[k+1] = pixels[k+2] = pixels[k+3] = 255 + } + else { + pixels[k] = pixels[k+1] = pixels[k+2] = 255 + pixels[k+3] = 0 + } + } + ctx.putImageData(pixelData, 0, 0) + + // reset the ctx + ctx = draw.ctx + + return canvas } draw.clear = function(){ - ctx.fillStyle = "rgba(255,255,255,0.98)" ctx.clearRect(0, 0, map.dimensions.a, map.dimensions.b) - ctx.fillRect(0, 0, map.dimensions.a, map.dimensions.b) } - draw.ruler = function (){ - ctx.strokeStyle = "rgba(80,80,80,0.5)" - ctx.lineWidth = 1 - var len = 5 - for (var i = 0.5; i < map.dimensions.a; i += 10) { - line(i, 0, i, len) - line(0, i, len, i) - } + draw.fill = function(fillStyle){ + ctx.fillStyle = fillStyle + ctx.fillRect(0, 0, map.dimensions.a, map.dimensions.b) } - draw.regions = function(regions, colors){ + draw.regions = function(regions, colors, stroke){ + if (stroke) { + ctx.strokeStyle = "#000" + ctx.lineWidth = (1 / map.zoom) + } for (var i = 0; i < regions.length; i++) { if (regions[i].dupe) continue ctx.fillStyle = colors[i % colors.length] - ctx.strokeStyle = "#000" - ctx.lineWidth = (1 / map.zoom) fill_region(regions[i]) - stroke_sides(regions[i]) + if (stroke) { + stroke_sides(regions[i]) + } } } @@ -88,7 +138,8 @@ Map.Draw = function(map, opt){ ctx.beginPath(); ctx.arc(mouse.x.b, mouse.y.b, radius, 0, 2*Math.PI, false); ctx.fill(); - + } + draw.mouse_dimensions = function(mouse){ if (mouse.width() != 0 && mouse.height() != 0) { if (map.ui.dragging) { stroke_rect(mouse) @@ -124,36 +175,39 @@ Map.Draw = function(map, opt){ } draw.coords = function(){ - /* - ctx.fillStyle = "#888"; - dot_at(0,0) - ctx.fillStyle = "#bbb"; - dot_at(100,0) - dot_at(0,100) - ctx.fillStyle = "#ddd"; - dot_at(200,0) - dot_at(0,200) - ctx.fillStyle = "#eee"; - dot_at(300,0) - dot_at(0,300) - */ - ctx.strokeStyle = "rgba(0,0,0,0.1)" ctx.lineWidth = 1/map.zoom var sides = map.sides() - var quant = sides.clone().quantize(200) - for (var x = quant.x.a - 200; x <= quant.x.b; x += 200) { - line(x, sides.y.a, x, sides.y.b) - } - for (var y = quant.y.a - 200; y <= quant.y.b; y += 200) { - line(sides.x.a, y, sides.x.b, y) + var quant = sides.clone().quantize(draw.grid_size) + for (var x = quant.x.a - draw.grid_size; x <= quant.x.b; x += draw.grid_size) { + if (Math.round(x) % 360 == 0) { + ctx.strokeStyle = "rgba(0,0,0,0.3)" + ctx.lineWidth = 1/map.zoom + } + else { + ctx.strokeStyle = "rgba(0,0,0,0.05)" + ctx.lineWidth = 1/map.zoom + } + line(x, sides.y.a, x, sides.y.b) } + + for (var y = quant.y.a - draw.grid_size; y <= quant.y.b; y += draw.grid_size) { + if (Math.round(y) % 360 == 0) { + ctx.strokeStyle = "rgba(0,0,0,0.3)" + ctx.lineWidth = 1/map.zoom + } + else { + ctx.strokeStyle = "rgba(0,0,0,0.05)" + ctx.lineWidth = 1/map.zoom + } + line(sides.x.a, y, sides.x.b, y) + } } // - function line (x,y,a,b,translation){ + var line = draw.line = function (x,y,a,b,translation){ if (translation) { x += translation.a a += translation.a @@ -184,11 +238,11 @@ Map.Draw = function(map, opt){ line(r.x.a, r.y.a, r.x.b, r.y.b, r.translation) } - function dot_at (x,z){ + draw.dot_at = function dot_at (x, z, radius){ ctx.save() ctx.translate(x,z) - var radius = 2 / map.zoom + radius = (radius || 2) / map.zoom ctx.beginPath(); ctx.arc(0, 0, radius, 0, 2*Math.PI, false); @@ -196,4 +250,29 @@ Map.Draw = function(map, opt){ ctx.restore() } + + draw.x_at = function x_at (x, z, length){ + ctx.save() + if (x && 'x' in x) { + length = z + ctx.translate(x.x,x.z) + } + else { + ctx.translate(x,z) + } + + var len = (length/2 || 4) / map.zoom + + ctx.lineCap = "square" + ctx.lineWidth = 2/map.zoom + ctx.beginPath() + ctx.moveTo( -len, -len); + ctx.lineTo( len, len); + ctx.moveTo( -len, len); + ctx.lineTo( len, -len); + ctx.stroke(); + + ctx.restore() + } + }
\ No newline at end of file diff --git a/public/assets/javascripts/rectangles/engine/map/tools/_base.js b/public/assets/javascripts/rectangles/engine/map/tools/_base.js new file mode 100644 index 0000000..17b247d --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/map/tools/_base.js @@ -0,0 +1,11 @@ +var MapTool = Fiber.extend(function(base){ + var exports = { + recenterCursor: true, + down: function(e, cursor){}, + move: function(e, cursor){}, + drag: function(e, cursor){}, + up: function(e, cursor, new_cursor){}, + cancel: function(){}, + } + return exports +})
\ No newline at end of file diff --git a/public/assets/javascripts/rectangles/engine/map/tools/arrow.js b/public/assets/javascripts/rectangles/engine/map/tools/arrow.js new file mode 100644 index 0000000..0b0557e --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/map/tools/arrow.js @@ -0,0 +1,83 @@ +// Tool used to move corners of polylines + +var ArrowTool = MapTool.extend(function(base){ + var exports = {} + + var selected_point = null, selected_segment = null, original_point = null, selected_shape = null + var src_points, dest_points + + exports.down = function(e, cursor){ + last_point.a = cursor.x.a + last_point.b = cursor.y.a + var p = shapes.findClosestPoint(last_point) + if (p && p.shape.type() !== "ortho") { + selected_shape = p.shape + selected_point = p.point + original_point = selected_point.clone() + return + } + var segment = shapes.findClosestSegment(last_point) + if (segment) { + document.body.style.cursor = "pointer" + + selected_segment = segment + console.log(segment.head, segment.tail) + selected_shape = segment.shape + src_points = segment.shape.cloneSegment( segment ) + dest_points = segment.shape.getSegment( segment ) + + last_point.a = segment.x + last_point.b = segment.y + cursor.x.a = cursor.x.b = last_point.a + cursor.y.a = cursor.y.b = last_point.b + } + else { + map.ui.set_drag_tool("position") + } + } + + exports.move = function(e, cursor){ + last_point.a = cursor.x.a + last_point.b = cursor.y.a + var p = shapes.findClosestPoint(last_point) + if (p) { + document.body.style.cursor = "pointer" + last_point.assign(p.point) + cursor.x.a = cursor.x.b = last_point.a + cursor.y.a = cursor.y.b = last_point.b + return + } + var segment = shapes.findClosestSegment(last_point) + if (segment) { + document.body.style.cursor = "pointer" + last_point.a = segment.x + last_point.b = segment.y + cursor.x.a = cursor.x.b = last_point.a + cursor.y.a = cursor.y.b = last_point.b + } + else { + document.body.style.cursor = "crosshair" + } + } + + exports.drag = function(e, cursor){ + if (selected_point) { + selected_point.a = original_point.a + cursor.x.magnitude() + selected_point.b = original_point.b + cursor.y.magnitude() + selected_shape.rebuild() + } + else if (selected_segment) { + selected_shape.translateSegment( + src_points, dest_points, + cursor.x.magnitude(), cursor.y.magnitude() + ) + selected_shape.rebuild() + } + } + + exports.up = function(e, cursor){ + selected_point = selected_shape = selected_segment = original_point = null + } + + return exports +})
\ No newline at end of file diff --git a/public/assets/javascripts/rectangles/engine/map/tools/eraser.js b/public/assets/javascripts/rectangles/engine/map/tools/eraser.js new file mode 100644 index 0000000..8fc3687 --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/map/tools/eraser.js @@ -0,0 +1,29 @@ +// Tool used to delete lines + +var EraserTool = MapTool.extend(function(base){ + var exports = {} + exports.down = function(e, cursor){ + last_point.a = cursor.x.a + last_point.b = cursor.y.a + var segment = shapes.findClosestSegment(last_point) + if (segment) { + shapes.removeSegment(segment) + } + } + exports.move = function(e, cursor){ + last_point.a = cursor.x.a + last_point.b = cursor.y.a + var segment = shapes.findClosestSegment(last_point) + if (segment) { + document.body.style.cursor = "pointer" + last_point.a = segment.x + last_point.b = segment.y + cursor.x.a = cursor.x.b = last_point.a + cursor.y.a = cursor.y.b = last_point.b + } + else { + document.body.style.cursor = "crosshair" + } + } + return exports +}) diff --git a/public/assets/javascripts/rectangles/engine/map/tools/line.js b/public/assets/javascripts/rectangles/engine/map/tools/line.js new file mode 100644 index 0000000..8175d66 --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/map/tools/line.js @@ -0,0 +1,63 @@ +// Tool is used to define a very simple line between two points. +// It is used by the BlueprintScaler to specify a sample distance to scale. + +var LineTool = MapTool.extend(function(base){ + var exports = {} + + var selected_point = null + + var line = exports.line = [] + + var can_drag, dragging + + exports.down = function(e, cursor){ + + // rightclick? + if (e.ctrlKey || e.which === 3) { + cursor.quantize(1/map.zoom) + app.router.blueprintView.map.center.a = cursor.x.a + app.router.blueprintView.map.center.b = -cursor.y.a + cursor.x.b = cursor.x.a + cursor.y.b = cursor.y.a + return + } + + this.cursor = cursor + switch (line.length) { + case 0: + line[0] = cursor.x_component() + can_drag = true + break + case 1: + line[1] = cursor.x_component() + can_drag = false + break + case 2: + line[0] = cursor.x_component() + line.pop() + can_drag = true + break + } + } + + exports.move = function(e, cursor){ + this.cursor = cursor + } + + exports.drag = function(e, cursor){ + if (dragging) { + line[1].a = cursor.x.b + line[1].b = cursor.y.b + } + else if (can_drag && cursor.magnitude() > 10/map.zoom) { + line[1] = cursor.y_component() + dragging = true + } + } + + exports.up = function(e, cursor){ + can_drag = dragging = false + } + + return exports +})
\ No newline at end of file diff --git a/public/assets/javascripts/rectangles/engine/map/tools/ortho.js b/public/assets/javascripts/rectangles/engine/map/tools/ortho.js new file mode 100644 index 0000000..374c822 --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/map/tools/ortho.js @@ -0,0 +1,118 @@ +// Tool to make a polyline where all walls are orthogonal + +var OrthoPolylineTool = MapTool.extend(function (base) { + + var prev_point, horizontal = false, first_edge_is_horizontal = false + + var exports = {} + exports.down = function(e, cursor){ + // rightclick? + if (e.ctrlKey || e.which === 3) { + e.preventDefault() + e.stopPropagation() + if (map.ui.placing) { + // close polyline or cancel + map.ui.placing = false + if (shapes.workline.points.length > 2) { + shapes.workline.build() + shapes.add(shapes.workline) + } + else { + shapes.workline.reset() + } + return + } + else { + map.ui.tools.position.rightclick(e, cursor) + } + return + } + + // compare to initial point + var p = last_point.clone() + if (map.ui.placing) { + if (shapes.workline.lastPoint().eq(p)) { + return + } + else if (shapes.workline.canCloseWith(p)) { + shapes.workline.close() + shapes.workline.build() + shapes.add(shapes.workline) + map.ui.placing = false + } + else { + shapes.workline.add(p) + prev_point = p + horizontal = ! horizontal + } + } + else { + map.ui.placing = true + shapes.workline = new OrthoPolyline () + shapes.workline.add(p) + first_point = prev_point = p + horizontal = false + } + } + exports.move = function(e, cursor){ + last_point.a = cursor.x.a + last_point.b = cursor.y.a + if (map.ui.placing) { + if (shapes.workline.points.length == 1) { + var x = abs(prev_point.a - last_point.a) + var y = abs(prev_point.b - last_point.b) + if (x > y) { + last_point.b = prev_point.b + first_edge_is_horizontal = horizontal = true + } + else { + last_point.a = prev_point.a + first_edge_is_horizontal = horizontal = false + } + } + else { + if (horizontal) { + last_point.b = prev_point.b + } + else { + last_point.a = prev_point.a + } + if (horizontal == first_edge_is_horizontal) { + // check if this point is within N pixels of the normal + // and lock it into place if so + if (horizontal && abs( first_point.a - last_point.a ) < 10/map.zoom) { + last_point.a = first_point.a + } + else if (! horizontal && abs( first_point.b - last_point.b ) < 10/map.zoom) { + last_point.b = first_point.b + } + } + } + + if (shapes.workline.canCloseWith(last_point)) { + document.body.style.cursor = "pointer" + last_point.assign(first_point) + cursor.x.a = cursor.x.b = last_point.a + cursor.y.a = cursor.y.b = last_point.b + } + return + } + var end_point = shapes.findClosestEndPoint(last_point) + if (end_point) { + document.body.style.cursor = "pointer" + last_point.assign(end_point.point) + cursor.x.a = cursor.x.b = last_point.a + cursor.y.a = cursor.y.b = last_point.b + return + } + else { + document.body.style.cursor = "crosshair" + } + } + exports.cancel = function(){ + if (map.ui.placing) { shapes.workline.reset() } + first_point = null + map.ui.placing = false + } + return exports +})
\ No newline at end of file diff --git a/public/assets/javascripts/rectangles/engine/map/tools/polyline.js b/public/assets/javascripts/rectangles/engine/map/tools/polyline.js new file mode 100644 index 0000000..445ae26 --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/map/tools/polyline.js @@ -0,0 +1,73 @@ +// Tool used to draw polylines with arbitrary angles + +var PolylineTool = MapTool.extend(function (base) { + var exports = {} + exports.down = function(e, cursor){ + + // rightclick? + if (e.ctrlKey || e.which === 3) { + e.preventDefault() + e.stopPropagation() + if (map.ui.placing) { + // close polyline or cancel + map.ui.placing = false + if (shapes.workline.points.length > 2) { + shapes.workline.build() + shapes.add(shapes.workline) + } + else { + shapes.workline.reset() + } + return + } + map.ui.tools.position.rightclick(e, cursor) + return + } + + // compare to initial point + var p = last_point.clone() + if (map.ui.placing) { + if (shapes.workline.canCloseWith(p)) { + shapes.workline.close() + shapes.workline.build() + shapes.add(shapes.workline) + map.ui.placing = false + } + else { + shapes.workline.add(p) + } + } + else { + map.ui.placing = true + shapes.workline = new Polyline () + shapes.workline.add(p) + } + } + exports.move = function(e, cursor){ + last_point.a = cursor.x.a + last_point.b = cursor.y.a + if (map.ui.placing && shapes.workline.canCloseWith(last_point)) { + document.body.style.cursor = "pointer" + last_point.assign(shapes.workline.points[0]) + cursor.x.a = cursor.x.b = last_point.a + cursor.y.a = cursor.y.b = last_point.b + return + } + var end_point = shapes.findClosestEndPoint(last_point) + if (end_point) { + document.body.style.cursor = "pointer" + last_point.assign(end_point.point) + cursor.x.a = cursor.x.b = last_point.a + cursor.y.a = cursor.y.b = last_point.b + return + } + else { + document.body.style.cursor = "crosshair" + } + } + exports.cancel = function(){ + if (map.ui.placing) { shapes.workline.reset() } + map.ui.placing = false + } + return exports +})
\ No newline at end of file diff --git a/public/assets/javascripts/rectangles/engine/map/tools/position.js b/public/assets/javascripts/rectangles/engine/map/tools/position.js new file mode 100644 index 0000000..f8365bc --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/map/tools/position.js @@ -0,0 +1,19 @@ +// Tool used to set position on the map and let you change the view by dragging. + +var PositionTool = MapTool.extend(function(base){ + var exports = { + recenterCursor: false, + drag: function(e, cursor){ + map.center.a = -cursor.x.magnitude() + map.center.b = cursor.y.magnitude() + }, + rightclick: function(e, cursor){ + cursor.quantize(1/map.zoom) + map.center.a = cursor.x.a + map.center.b = -cursor.y.a + cursor.x.b = cursor.x.a + cursor.y.b = cursor.y.a + } + } + return exports +})
\ No newline at end of file diff --git a/public/assets/javascripts/rectangles/engine/map/tools/start.js b/public/assets/javascripts/rectangles/engine/map/tools/start.js new file mode 100644 index 0000000..203a85f --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/map/tools/start.js @@ -0,0 +1,29 @@ +// Tool is used to set the start position on the map. + +var StartPositionTool = MapTool.extend(function(base){ + var exports = {} + + var selected_point = null + + var line = exports.line = [] + + var can_drag, dragging + + exports.down = function(e, cursor){ + // rightclick? + if (e.ctrlKey || e.which === 3) { + cursor.quantize(1/map.zoom) + app.router.blueprintView.map.center.a = cursor.x.a + app.router.blueprintView.map.center.b = -cursor.y.a + cursor.x.b = cursor.x.a + cursor.y.b = cursor.y.a + return + } + + cam.x = app.controller.startPosition.x = cursor.x.a + cam.z = app.controller.startPosition.z = cursor.y.a + } + + return exports + +}) diff --git a/public/assets/javascripts/rectangles/engine/map/ui_editor.js b/public/assets/javascripts/rectangles/engine/map/ui/editor.js index f9334e6..699597a 100644 --- a/public/assets/javascripts/rectangles/engine/map/ui_editor.js +++ b/public/assets/javascripts/rectangles/engine/map/ui/editor.js @@ -26,6 +26,13 @@ Map.UI.Editor = function(map){ resize: true, destroy: false, }) + + base.blur = function(){ + Rooms.forEach(function(r){ + return r.focused = false + }) + app.tube("builder-pick-nothing") + } // @@ -35,6 +42,7 @@ Map.UI.Editor = function(map){ cursor.y.div(map.dimensions.b).sub(0.5).mul(map.dimensions.b / map.zoom).sub(map.center.b) if (e.ctrlKey || e.which === 3) { + if (Rooms.regions.length == 0) return cursor.quantize(1/map.zoom) map.center.a = cursor.x.a map.center.b = -cursor.y.a @@ -50,7 +58,7 @@ Map.UI.Editor = function(map){ return r.focused = r.rect.contains(cursor.x.a, cursor.y.a) }) - if (intersects.length && base.permissions.destroy) { + if (intersects.length && (base.permissions.destroy || e.altKey)) { base.mouse.down = false room = intersects[0] @@ -63,6 +71,9 @@ Map.UI.Editor = function(map){ Rooms.remove(room) app.tube("builder-destroy-room", room) + + // TODO: watch individual scenery object here + Minotaur.watch( (app.router.builderView || app.router.editorView).settings ) return } else if (intersects.length) { @@ -161,18 +172,22 @@ Map.UI.Editor = function(map){ cursor.y.abs().quantize(1) var room = Rooms.add_with_rect( cursor ) + Rooms.rebuild() + UndoStack.push({ type: "create-room", undo: { id: room.id }, redo: room.copy() }) + Rooms.rebuild() app.tube("builder-pick-room", room) + + // TODO: watch individual scenery object here + Minotaur.watch( app.controller.settings ) } } - if (base.resizing || base.dragging) { - var oldState = base.dragging.copy() if (base.resizing) { @@ -187,24 +202,61 @@ Map.UI.Editor = function(map){ undo: oldState, redo: base.dragging.copy() }) + Rooms.rebuild() + + // TODO: watch individual scenery object here + Minotaur.watch( (app.router.builderView || app.router.editorView).settings ) } + + var intersects = Rooms.filter(function(r){ + return r.focused = r.rect.contains(cursor.x.a, cursor.y.a) + }) + if (! base.dragging && ! intersects.length) { + app.tube("builder-pick-nothing") + } base.creating = base.dragging = base.resizing = false } var wheelState, wheelTimeout - function mousewheel (e, val, delta){ + function mousewheel (e, deltaY, deltaX){ var cursor = base.mouse.cursor var intersects = Rooms.filter(function(r){ - return r.focused = r.rect.contains(cursor.x.a, cursor.y.a) + return r.focused // = r.rect.contains(cursor.x.a, cursor.y.a) }) - if (intersects.length) { + if (intersects.length && window.heightIsGlobal) { + var rooms = Rooms.values() + wheelState = wheelState || rooms[0].height + var height = clamp( ~~(rooms[0].height + deltaY * 2), height_min, height_max ) + rooms.forEach(function(room){ + room.height = height + }) + + app.tube("builder-pick-room", intersects[0]) + + clearTimeout(wheelTimeout) + wheelTimeout = setTimeout(function(){ + UndoStack.push({ + type: "update-rooms-height", + undo: wheelState, + redo: height + }) + Rooms.rebuild() + + // TODO: watch individual scenery object here + Minotaur.watch( (app.router.builderView || app.router.editorView).settings ) + + wheelState = null + }, 250) + } + else if (intersects.length) { wheelState = wheelState || intersects[0].copy() - intersects[0].height = clamp( ~~(intersects[0].height - delta), height_min, height_max ) + intersects[0].height = clamp( ~~(intersects[0].height + deltaY * 2), height_min, height_max ) + app.tube("builder-pick-room", intersects[0]) clearTimeout(wheelTimeout) wheelTimeout = setTimeout(function(){ @@ -213,15 +265,20 @@ Map.UI.Editor = function(map){ undo: wheelState, redo: intersects[0].copy() }) - Rooms.clipper.update() + Rooms.rebuild() + + // TODO: watch individual scenery object here + Minotaur.watch( app.router.editorView.settings ) + wheelState = null - }, 500) + }, 250) } else { - map.set_zoom(map.zoom_exponent - delta/20) + map.set_zoom(map.zoom_exponent + deltaY/20) } } function rightclick (e){ } + } diff --git a/public/assets/javascripts/rectangles/engine/map/ui_minimap.js b/public/assets/javascripts/rectangles/engine/map/ui/minimap.js index fabbdb9..0fdd336 100644 --- a/public/assets/javascripts/rectangles/engine/map/ui_minimap.js +++ b/public/assets/javascripts/rectangles/engine/map/ui/minimap.js @@ -72,8 +72,8 @@ Map.UI.Minimap = function(map){ base.dragging = false } - function mousewheel (e, val, delta){ - map.set_zoom(map.zoom_exponent - delta/20) + function mousewheel (e, deltaY, deltaX){ + map.set_zoom(map.zoom_exponent - deltaY/20) } function rightclick (e){ diff --git a/public/assets/javascripts/rectangles/engine/map/ui/ortho.js b/public/assets/javascripts/rectangles/engine/map/ui/ortho.js new file mode 100644 index 0000000..5be7446 --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/map/ui/ortho.js @@ -0,0 +1,107 @@ +Map.UI = Map.UI || {} +Map.UI.Ortho = function(map){ + + var base = this + var last_event = null + + base.creating = base.dragging = base.resizing = false + + base.mouse = new mouse({ + el: map.el, + down: function(e, cursor){ + last_event = e + cursor.x.div(map.dimensions.a).add(0.5).mul(map.dimensions.a / map.zoom) + cursor.y.div(map.dimensions.b).sub(0.5).mul(map.dimensions.b / map.zoom) + if (tool.recenterCursor) { + cursor.x.add(map.center.a) + cursor.y.sub(map.center.b) + base.tools[currentTool].down(e, cursor) + } + else { + base.tools[currentTool].down(e, cursor) + cursor.x.add(map.center.a) + cursor.y.sub(map.center.b) + } + }, + move: function(e, cursor){ + last_event = e + cursor.x.div(map.dimensions.a).add(0.5).mul(map.dimensions.a / map.zoom) + cursor.y.div(map.dimensions.b).sub(0.5).mul(map.dimensions.b / map.zoom) + if (tool.recenterCursor) { + cursor.x.add(map.center.a) + cursor.y.sub(map.center.b) + base.tools[currentTool].move(e, cursor) + } + else { + base.tools[currentTool].move(e, cursor) + cursor.x.add(map.center.a) + cursor.y.sub(map.center.b) + } + }, + drag: function(e, cursor){ + last_event = e + cursor.x.b = ((cursor.x.b/map.dimensions.a)+0.5) * map.dimensions.a / map.zoom + cursor.y.b = ((cursor.y.b/map.dimensions.b)-0.5) * map.dimensions.b / map.zoom + if (tool.recenterCursor) { + cursor.x.b += map.center.a + cursor.y.b -= map.center.b + base.tools[currentTool].drag(e, cursor) + } + else { + base.tools[currentTool].drag(e, cursor) + cursor.x.b += map.center.a + cursor.y.b -= map.center.b + } + }, + up: function(e, cursor, new_cursor){ + last_event = e + new_cursor.x.div(map.dimensions.a).add(0.5).mul(map.dimensions.a / map.zoom) + new_cursor.y.div(map.dimensions.b).sub(0.5).mul(map.dimensions.b / map.zoom) + if (tool.recenterCursor) { + new_cursor.x.add(map.center.a) + new_cursor.y.sub(map.center.b) + base.tools[currentTool].up(e, cursor, new_cursor) + } + else { + base.tools[currentTool].up(e, cursor, new_cursor) + new_cursor.x.add(map.center.a) + new_cursor.y.sub(map.center.b) + } + if (nextTool) { + console.log('found nextTool') + base.set_tool(nextTool) + nextTool = null + } + } + }) + + var currentTool = "polyline", nextTool, tool + base.add_tool = function(name, tool){ + base.tools[name] = tool + } + base.set_tool = function(s){ + console.log("set tool to", s) + if (base.tools[currentTool]) { + base.tools[currentTool].cancel() + } + currentTool = s + tool = base.tools[currentTool] + } + base.set_drag_tool = function(s){ + console.log('set drag tool to', s) + nextTool = currentTool + currentTool = s + tool = base.tools[currentTool] + base.tools[currentTool].down(last_event, base.mouse.cursor) + } + base.tools = {} + + base.wheel = new wheel({ + el: map.el, + update: mousewheel, + }) + + function mousewheel (e, deltaY, deltaX){ + map.set_zoom(map.zoom_exponent - deltaY/20) + } +} diff --git a/public/assets/javascripts/rectangles/engine/rooms/_rooms.js b/public/assets/javascripts/rectangles/engine/rooms/_rooms.js index 6f96275..9aff33f 100644 --- a/public/assets/javascripts/rectangles/engine/rooms/_rooms.js +++ b/public/assets/javascripts/rectangles/engine/rooms/_rooms.js @@ -13,6 +13,7 @@ vec2 = require('../../models/vec2') Rect = require('../../models/rect') Room = require('../../models/room') + Walls = require('./_walls') UidGenerator = require('../../util/uid') sort = require('../../util/sort') _ = require('lodash') @@ -36,6 +37,7 @@ base.list = {} base.regions = [] + base.shapesMode = false base.uid = new UidGenerator(base.list) @@ -64,13 +66,13 @@ base.remove = function(room){ delete base.list[room.id] - Rooms.clipper.update() + Rooms.rebuild() } base.removeAll = function(){ base.list = {} base.regions = [] - Rooms.clipper.update() + Rooms.rebuild() } base.count = function(){ @@ -80,6 +82,10 @@ base.forEach = function(f){ return base.values().forEach(f) } + + base.some = function(f){ + return base.values().some(f) + } base.map = function(f){ return base.values().map(f) @@ -88,6 +94,17 @@ base.values = function(){ return _.values(base.list) } + + base.rebuild = function(walls_data){ + if (base.shapesMode) return + walls_data = walls_data || Walls.serialize() + Rooms.clipper.update() + Rooms.builder.rebuild() + Rooms.grouper.build() + Walls.paint() + Walls.deserialize(walls_data) + app.tube("rooms-built") + } base.serialize = function(){ var rooms = base.map(function(room){ @@ -96,7 +113,7 @@ return rooms } - base.deserialize = function(rooms_data){ + base.deserialize = function(rooms_data, walls_data){ rooms_data.forEach(function(data){ if (! data || ! data.rect) return var rect = new Rect(data.rect.x[0], data.rect.y[0], data.rect.x[1], data.rect.y[1]) @@ -107,7 +124,46 @@ }) base.add(room) }) - Rooms.clipper.update() + Rooms.rebuild(walls_data) + } + + base.deserializeFromShapes = function(data, walls_data) { + walls_data = walls_data || Walls.serialize() + base.shapesMode = true + window.viewHeight = data.viewHeight || app.defaults.viewHeight + window.wallHeight = data.wallHeight || app.defaults.wallHeight + $(".units").val( data.units ) + + Rooms.builder.clear() + + shapes.deserialize( data.shapes ) + // shapes.build() + var regions = RegionList.build() + + regions.forEach(function(region){ + var room = new Room({ + rect: region, + regions: [region], + height: wallHeight, + }) + + room.sides = region.sides + region.id = Rooms.uid("room_") + Rooms.list[ region.id ] = room + var mx_walls = Rooms.builder.build_walls(region) + room.mx_floor = Rooms.builder.make_floor(room, region) + room.mx_ceiling = Rooms.builder.make_ceiling(room, region) + + mx_walls.forEach(function(mx){ scene.add(mx) }) + scene.add(room.mx_floor) + scene.add(room.mx_ceiling) + }) + + Rooms.grouper.build() + + Walls.paint() + Walls.deserialize(walls_data) + app.tube("rooms-built") } base.report = function(){ @@ -118,6 +174,14 @@ return data } + base.extent = function(){ + var extent = new Rect ( new vec2(Infinity, -Infinity), new vec2(Infinity, -Infinity) ) + base.forEach(function(room){ + extent.expand(room.rect) + }) + return extent + } + base.sorted_by_position = function(){ return sort.rooms_by_position( base.values() ) } diff --git a/public/assets/javascripts/rectangles/engine/rooms/_walls.js b/public/assets/javascripts/rectangles/engine/rooms/_walls.js index 82ccb87..38dac84 100644 --- a/public/assets/javascripts/rectangles/engine/rooms/_walls.js +++ b/public/assets/javascripts/rectangles/engine/rooms/_walls.js @@ -35,8 +35,23 @@ var base = this base.list = [] + base.floors = [] base.lookup = {} base.colors = {} + + base.init = function(){ + base.colors = base.copyColors( app.defaults.colors ) + } + + base.copyColors = function(colors){ + var copy = { + wall: colors.wall.slice(), + outline: colors.outline.slice(), + floor: colors.floor.slice(), + ceiling: colors.ceiling.slice(), + } + return copy + } base.first = function(){ for (var i in base.list) { @@ -46,6 +61,10 @@ } } + base.find = function(id){ + return base.lookup[id] + } + base.assign = function(list){ base.list = list base.lookup = {} @@ -53,11 +72,21 @@ base.lookup[wall.id] = wall }) } + + base.assignFloors = function(floors){ + base.floors = floors + floors.forEach(function(floor){ + base.lookup[floor.id] = floor + }) + } base.bind = function(){ base.list.forEach(function(wall){ wall.bind() }) + base.floors.forEach(function(floor){ + floor.bind() + }) } base.count = function(){ @@ -65,16 +94,20 @@ } base.forEach = function(f){ - return base.list.forEach(f) + return base.values().forEach(f) } base.map = function(f){ - return base.list.map(f) + return base.values().map(f) } - + + base.values = function(){ + return _.values(base.lookup) + } + base.serialize = function(){ var data = [] - base.list.forEach(function(wall){ + base.forEach(function(wall){ data.push(wall.serialize()) }) return data @@ -84,6 +117,9 @@ walls_data.forEach(function(wall_data){ if (! wall_data) { return } var wall = base.lookup[ wall_data.id ] + if (! wall) { + return + } wall.deserialize( wall_data ) }) }, @@ -93,21 +129,91 @@ var outlineColor = rgb_string(Walls.colors.outline) var floorColor = rgb_string(Walls.colors.floor) var ceilingColor = rgb_string(Walls.colors.ceiling) - Walls.forEach(function(wall){ + Walls.list.forEach(function(wall){ wall.outline(wallColor, outlineColor) }) - Rooms.forEach(function(room){ - room.setFloorColor(floorColor) - room.setCeilingColor(ceilingColor) + Walls.floors.forEach(function(floor){ + if (floor.ceiling) floor.color(ceilingColor) + else floor.color(floorColor) }) } + base.luminance = function(rgb){ + rgb = rgb || Walls.colors.ceiling + var rgb_max = Math.max.apply(0, rgb) + var rgb_min = Math.min.apply(255, rgb) + return (rgb_max + rgb_min ) / 2 + } + + base.setBodyColor = function(){ + $("#header").toggleClass("black", Walls.luminance() < 100) + $("body").css("background-color", rgb_string( Walls.colors.wall )) + } + base.clearBodyColor = function(){ + $("#header").removeClass("black") + $("body").css("background-color", "transparent") + } + + base.setWallpaper = { + wall: function(background){ + var img = new Image () + img.onload = function(){ + Walls.list.forEach(function(wall){ + wall.wallpaper(background, img) + }) + }.bind(this) + img.src = background.src.replace(/url\(\"?\'?/,"").replace(/\"?\'?\)/,"") + img.complete && img.onload() + }, + floor: function(background){ + Walls.floors.forEach(function(floor){ + if (floor.ceiling) return + floor.wallpaper(background) + }) + }, + ceiling: function(background){ + Walls.floors.forEach(function(floor){ + if (! floor.ceiling) return + floor.wallpaper(background) + }) + }, + } + + base.clearWallpaper = { + wall: function(){ + Walls.list.forEach(function(wall){ + wall.wallpaper("none") + }) + }, + outline: function(){ + }, + floor: function(){ + Walls.floors.forEach(function(floor){ + if (floor.ceiling) return + floor.wallpaper("none") + }) + }, + ceiling: function(){ + Walls.floors.forEach(function(floor){ + if (! floor.ceiling) return + floor.wallpaper("none") + }) + }, + } + base.setColor = { wall: function(rgb){ var rgbaColor = rgba_string(rgb, app.defaults.wallOpacity) + var rgbColor = rgb_string(rgb) + + if (Rooms.mover.room) { + $("#header").toggleClass("black", base.luminance() < 100) + $("body").css("background-color", rgbColor) + } + Walls.colors.wall = rgb - Walls.forEach(function(wall){ + Walls.list.forEach(function(wall){ wall.outline(rgbaColor, null) }) }, @@ -115,7 +221,7 @@ outline: function(rgb){ var rgbColor = rgb_string(rgb) Walls.colors.outline = rgb - Walls.forEach(function(wall){ + Walls.list.forEach(function(wall){ wall.outline(null, rgbColor) }) }, @@ -123,19 +229,20 @@ floor: function(rgb){ var rgbColor = rgb_string(rgb) Walls.colors.floor = rgb - Rooms.forEach(function(room){ - room.setFloorColor(rgbColor) + Walls.floors.forEach(function(floor){ + if (floor.ceiling) return + floor.color(rgbColor) }) }, ceiling: function(rgb){ var rgbColor = rgb_string(rgb) Walls.colors.ceiling = rgb - Rooms.forEach(function(room){ - room.setCeilingColor(rgbColor) + Walls.floors.forEach(function(floor){ + if (! floor.ceiling) return + floor.color(rgbColor) }) }, - } } diff --git a/public/assets/javascripts/rectangles/engine/rooms/builder.js b/public/assets/javascripts/rectangles/engine/rooms/builder.js index 6a89158..f78fb91 100644 --- a/public/assets/javascripts/rectangles/engine/rooms/builder.js +++ b/public/assets/javascripts/rectangles/engine/rooms/builder.js @@ -14,6 +14,7 @@ PI = Math.PI HALF_PI = PI/2 TOP = CEILING, BOTTOM = FLOOR + $ = { browser: { mozilla: false } } function sidesToString(sides){ var s = "" if (sides & FRONT) s += "front " @@ -32,19 +33,16 @@ var els = [] base.init = function(){ - base.bind() + // base.bind() } base.bind = function(){ - app.on("clip", base.rebuild.bind(base)) } base.rebuild = function(){ if (window.scene) { base.clear() base.build() - Rooms.grouper.build() - app.tube("rooms-built") } } @@ -264,6 +262,9 @@ el.rotationX = PI/2 el.rect = region el.side = FLOOR + if ($.browser.mozilla) { + el.el.style.display = "none" + } return el } this.make_ceiling = function (room, region) { @@ -280,10 +281,13 @@ el.rotationX = -PI/2 el.rect = region el.side = CEILING + if ($.browser.mozilla) { + el.el.style.display = "none" + } return el } this.make_wall = function (klass) { - // klass += ".backface-hidden" + klass += ".backface-hidden" var el = new MX.Object3D(".face" + (klass || "")) el.width = el.height = el.scaleX = el.scaleY = el.scaleZ = 1 el.z = el.y = el.x = 0 @@ -296,7 +300,7 @@ this.el = this.rect = this.face = null } - // possible if walls are opaque + // preferable if walls are opaque // el.el.classList.add("backface-hidden") return el diff --git a/public/assets/javascripts/rectangles/engine/rooms/clipper.js b/public/assets/javascripts/rectangles/engine/rooms/clipper.js index 33e3a84..df83cc8 100644 --- a/public/assets/javascripts/rectangles/engine/rooms/clipper.js +++ b/public/assets/javascripts/rectangles/engine/rooms/clipper.js @@ -38,7 +38,6 @@ base.update = function(){ base.solve_rects() - app.tube("clip") } var regions @@ -49,7 +48,7 @@ Rooms.regions = regions = [] return } - + base.reset_rects() base.clip_rects() var culled = base.cull_rects_iterative() diff --git a/public/assets/javascripts/rectangles/engine/rooms/grouper.js b/public/assets/javascripts/rectangles/engine/rooms/grouper.js index 663d29d..890a476 100644 --- a/public/assets/javascripts/rectangles/engine/rooms/grouper.js +++ b/public/assets/javascripts/rectangles/engine/rooms/grouper.js @@ -48,13 +48,16 @@ base.build = function (){ var walls = [] + var floors = [] var collections = base.collect() base.cull(collections) base.group(walls, collections, FRONT) base.group(walls, collections, BACK) base.group(walls, collections, LEFT) base.group(walls, collections, RIGHT) + base.groupFloors(floors, collections) Walls.assign( walls ) + Walls.assignFloors( floors ) Walls.bind() } base.collect = function(){ @@ -63,12 +66,16 @@ collections[BACK] = [] collections[LEFT] = [] collections[RIGHT] = [] + collections[FLOOR] = [] + collections[CEILING] = [] Rooms.forEach(function(room){ room.mx_walls.forEach(function(mx){ var side = mx.side || mx.half_side collections[side].push(mx) }) + collections[FLOOR] = collections[FLOOR].concat( room.mx_floor ) + collections[CEILING] = collections[CEILING].concat( room.mx_ceiling ) }) base.cull(collections) @@ -161,6 +168,20 @@ return walls } + base.groupFloors = function(floors, collections){ + var floor = new Floor ({ + id: 'floor', + side: FLOOR, + mx: collections[FLOOR] + }) + var ceiling = new Floor ({ + id: 'ceiling', + side: CEILING, + mx: collections[CEILING] + }) + floors.push(floor) + floors.push(ceiling) + } } diff --git a/public/assets/javascripts/rectangles/engine/rooms/mover.js b/public/assets/javascripts/rectangles/engine/rooms/mover.js index 7195fcc..ac27aba 100644 --- a/public/assets/javascripts/rectangles/engine/rooms/mover.js +++ b/public/assets/javascripts/rectangles/engine/rooms/mover.js @@ -4,6 +4,15 @@ Rooms.mover = new function(){ base.room = null base.noclip = false + var cursor = base.cursor = new Rect (0,0,0,0) + var cursor_copy = new Rect (0,0,0,0) + var wall_vec = new Rect (0,0,0,0) + var intersect = new vec2(0,0) + + var origins = new Rect() + origins.x = cursor.x + origins.y = wall_vec.x + base.init = function(){ base.bind() base.update(scene.camera) @@ -11,7 +20,8 @@ Rooms.mover = new function(){ base.bind = function(){ app.on("move", base.update) - keys.on("backslash", function(){ + keys.on("backslash", function(e){ + if ( ! (e.ctrlKey || e.metaKey) ) return base.noclip = ! base.noclip base.room = null app.movements.gravity( ! base.noclip ) @@ -19,15 +29,9 @@ Rooms.mover = new function(){ } base.update = function(pos){ + var intersect, collision, distance var radius = scene.camera.radius - if (base.noclip) { - cam.x = pos.x - cam.y = pos.y - cam.z = pos.z - return - } - cam.y = pos.y // if we were in a room already.. @@ -39,17 +43,41 @@ Rooms.mover = new function(){ return } - // check if we've breached one of the walls.. clamp position if so - var collision = base.room.collidesDisc(pos.x, pos.z, radius) - - if (collision) { - cam.x = (collision & LEFT_RIGHT) ? base.room.rect.x.clampDisc(pos.x, radius) : pos.x - cam.z = (collision & FRONT_BACK) ? base.room.rect.y.clampDisc(pos.z, radius) : pos.z - return + distance = sqrt(pow(cam.x-pos.x, 2), pow(cam.z-pos.z, 2)) + + // check if we've breached one of the walls.. clamp position if so. + // there are two algorithms for this now, ridiculously.. + // - the first one is smoother, best for keyboard use + // but if the distance travelled is greater than the min. distance from the wall, + // then there is a possibility of ejection from the room. so is bad for phone/mousewheel. + + // - the second one determines fortuitously if we have breached any of the walls + // however it can get jumpy if you run into a wall.. thus it is best for devices like phone or mousewheel + // the benefit is you will never leave the room. + if (base.noclip) { + // in no-clip mode we walk through walls. } + else if (distance < radius) { + collision = base.room.collidesDisc(cam, pos, radius) + + if (collision) { + cam.x = (collision & LEFT_RIGHT) ? base.room.rect.x.clampDisc(pos.x, radius) : pos.x + cam.z = (collision & FRONT_BACK) ? base.room.rect.y.clampDisc(pos.z, radius) : pos.z + return + } + } + else { + intersect = base.intersect(pos) + if (intersect) { + cam.x = intersect.a + cam.z = intersect.b + return + } + } // in this case, we appear to have left the room.. - $(".face.active").removeClass("active") + // $(".face.active").removeClass("active") + Walls.clearBodyColor() base.room = null } @@ -65,11 +93,162 @@ Rooms.mover = new function(){ // did we actually enter a room? if (intersects.length) { base.room = intersects[0] - base.room.mx_floor.forEach(function(w){ $(w.el).addClass("active") }) - base.room.mx_ceiling.forEach(function(w){ $(w.el).addClass("active") }) - base.room.mx_walls.forEach(function(w){ $(w.el).addClass("active") }) + if (! base.noclip) { + Walls.setBodyColor() + } + app.tube("change-room", { room: base.room }) } - } + base.intersect = function(pos){ + var closest_intersect, closest_wall, new_t, wall_t, t, min_t = 1 + + cursor_copy.x.a = cursor.x.a = cam.x + cursor_copy.x.b = cursor.x.b = cam.z + cursor_copy.y.a = cursor.y.a = pos.x + cursor_copy.y.b = cursor.y.b = pos.z + + cursor_copy.extend_ends(scene.camera.radius) + + origins.x = cursor_copy.x + origins.y = wall_vec.x + + Walls.list.forEach(function(wall, i){ + var actually_intersects = false, intersecting_face + + wall.get_points(wall_vec) + + t = perp(origins, wall_vec) / ( perp(cursor_copy, wall_vec) || 0.0000001 ) + + if ( min_t < t || t < 0 || 1 < t ) return + + intersect.a = cursor_copy.x.a + ( cursor_copy.y.a - cursor_copy.x.a ) * t + intersect.b = cursor_copy.x.b + ( cursor_copy.y.b - cursor_copy.x.b ) * t + + if ( ! is_collinear( intersect, wall_vec ) ) return + + if (wall.side & LEFT_RIGHT) { + intersecting_face = wall.surface.face_for_x(intersect.b) + } + else { + intersecting_face = wall.surface.face_for_x(intersect.a) + } + + actually_intersects = !! (intersecting_face && intersecting_face.y.a == 0) + if (actually_intersects) { + closest_intersect = intersect.clone() + closest_wall = wall_vec.clone() + min_t = t + } + }) + + if (closest_intersect) { + + var aw, len, dd + aw = angle(closest_wall) + wall_vec.assign(closest_wall) + wall_vec.x.a -= scene.camera.radius * sin(aw) + wall_vec.x.b += scene.camera.radius * cos(aw) + wall_vec.y.a -= scene.camera.radius * sin(aw) + wall_vec.y.b += scene.camera.radius * cos(aw) + + origins.x = cursor.x + origins.y = wall_vec.x + + new_t = perp(origins, wall_vec) / ( perp(cursor, wall_vec) || 0.0000001 ) + wall_t = perp(origins, cursor) / ( perp(wall_vec, cursor) || 0.0000001 ) + + intersect.a = cursor.x.a + ( cursor.y.a - cursor.x.a ) * new_t + intersect.b = cursor.x.b + ( cursor.y.b - cursor.x.b ) * new_t + + // here compare len to the length of the wall in the direction we are travelling + dd = dot2(diff(closest_wall), diff(cursor)) + len = sqrt(dot(wall_vec, wall_vec)) + if (dd > 0) { + len *= 1-abs(wall_t) + } + else { + len *= abs(wall_t) + aw += PI + } + + len = clamp(len, 0, (1-min_t) * sqrt(dot(cursor, cursor))) + + intersect.a += len * cos(aw) + intersect.b += len * sin(aw) + + wall_vec.normalize() + intersect.a = clamp(intersect.a, wall_vec.x.a, wall_vec.y.a) + intersect.b = clamp(intersect.b, wall_vec.x.b, wall_vec.y.b) + + return intersect + } + else { + return cursor.y + } + } + function diff (v) { + return new vec2(v.y.a - v.x.a, v.y.b - v.x.b) + } + function angle (va) { + return atan2(va.y.b - va.x.b, va.y.a - va.x.a) + } + function angle2 (pa, pb) { + return atan2(pb.b - pa.b, pb.a - pa.a) + } + function normal (va) { + return atan2(va.x.a - va.y.a, va.y.b - va.x.b) + } + function dot (va, vb) { + return (va.y.a - va.x.a) * (vb.y.a - vb.x.a) + (va.y.b - va.x.b) * (vb.y.b - vb.x.b) + } + function dot2 (pa, pb) { + return pa.a * pb.a + pa.b * pb.b + } + function perp (va, vb) { + return (va.y.a - va.x.a) * (vb.y.b - vb.x.b) - (va.y.b - va.x.b) * (vb.y.a - vb.x.a) + } + function is_collinear (p, vec) { + var on_x, on_y + var pa = round(p.a), pb = round(p.b) + + if (vec.x.a < vec.y.a) { + on_x = vec.x.a <= pa && pa <= vec.y.a + } + else { + on_x = vec.x.a >= pa && pa >= vec.y.a + } + + if (vec.x.b < vec.y.b) { + on_y = vec.x.b <= pb && pb <= vec.y.b + } + else { + on_y = vec.x.b >= pb && pb >= vec.y.b + } + + return !! (on_x && on_y) + } + cursor_copy.extend_ends = function(n){ + var a = angle(this) + this.x.a -= n*cos(a) + this.x.b -= n*sin(a) + this.y.a += n*cos(a) + this.y.b += n*sin(a) + return this + } + wall_vec.normalize = function(){ + var carry + if (this.x.a > this.y.a) { +// console.log("SWAP X") + carry = this.x.a + this.x.a = this.y.a + this.y.a = carry + } + if (this.x.b > this.y.b) { +// console.log("SWAP Y") + carry = this.x.b + this.x.b = this.y.b + this.y.b = carry + } + } } diff --git a/public/assets/javascripts/rectangles/engine/rooms/projector.js b/public/assets/javascripts/rectangles/engine/rooms/projector.js deleted file mode 100644 index 2eac314..0000000 --- a/public/assets/javascripts/rectangles/engine/rooms/projector.js +++ /dev/null @@ -1,30 +0,0 @@ - -rooms.projector = new function(){ - - projector = new THREE.Projector(); - vector = new THREE.Vector3( mouse.x, mouse.y, 0.5 ); - projector.unprojectVector( vector, camera ); - - raycaster = new THREE.Raycaster( camera.position, vector.sub( camera.position ).normalize() ); - intersects = raycaster.intersectObjects( scene.children, true ); - -} - - - -THREE.Projector = function () { - - _viewProjectionMatrix = new THREE.Matrix4(), - - this.unprojectVector = function ( vector, camera ) { - camera.projectionMatrixInverse.getInverse( camera.projectionMatrix ); - - _viewProjectionMatrix.multiplyMatrices( - camera.matrixWorld, - camera.projectionMatrixInverse - ); - - return vector.applyProjection( _viewProjectionMatrix ); - }; - -} diff --git a/public/assets/javascripts/rectangles/engine/scenery/_scenery.js b/public/assets/javascripts/rectangles/engine/scenery/_scenery.js index d34e299..6203c20 100644 --- a/public/assets/javascripts/rectangles/engine/scenery/_scenery.js +++ b/public/assets/javascripts/rectangles/engine/scenery/_scenery.js @@ -5,11 +5,13 @@ var Scenery = new function(){ base.list = {} base.nextMedia = null + base.nextWallpaper = null - base.mouse = new mouse ({ use_offset: false }) + base.mouse = new mouse ({ use_offset: false, mousedownUsesCapture: true }) base.init = function(){ base.resize.init() + base.sound.init() } base.add = function(opt){ @@ -22,20 +24,35 @@ var Scenery = new function(){ case 'video': case 'youtube': case 'vimeo': + if (is_mobile) return scene_media = new Scenery.types.video (opt) break + + case 'soundcloud': + if (is_mobile) return + scene_media = new Scenery.types.audio (opt) + break + + case 'text': + scene_media = new Scenery.types.text (opt) + scene_media.focused = !! opt.newMedia + break } base.list[scene_media.id] = scene_media return scene_media } base.addNextToWall = function(opt){ + opt.newMedia = true opt.media = base.nextMedia opt.index = opt.index || 0 var scene_media = base.add(opt) // test if scenery was placed here - if (! scene_media.bounds) { + if (! scene_media) { + return null + } + else if (! scene_media.bounds) { base.remove( scene_media.id ) return null } @@ -48,7 +65,7 @@ var Scenery = new function(){ base.find = function(id){ return base.list[id] || null } - + base.remove = function(id){ var scene_media = base.list[id] delete base.list[id] @@ -83,6 +100,7 @@ var Scenery = new function(){ } base.deserialize = function(scenery_data){ + var added = [] scenery_data.forEach(function(data){ var wall = Walls.lookup[data.wall_id] || Walls.first() var scene_media = base.add({ @@ -91,9 +109,40 @@ var Scenery = new function(){ media: data.media, id: data.id }) + added.push(scene_media) }) + return added } + base.rewindAll = function(){ + base.forEach(function(scenery){ + if (scenery.type == "video") scenery.seek(0) + }) + } + base.playAll = function(){ + base.forEach(function(scenery){ + if (scenery.type == "video") { + scenery.unmute() + scenery.play() + } + }) + } + base.pauseAll = function(){ + base.forEach(function(scenery){ + if (scenery.type == "video") scenery.pause() + }) + } + base.muteAll = function(){ + base.forEach(function(scenery){ + if (scenery.type == "video") scenery.mx.mute() + }) + } + base.unmuteAll = function(){ + base.forEach(function(scenery){ + if (scenery.type == "video") scenery.mx.unmute() + }) + } + return base } diff --git a/public/assets/javascripts/rectangles/engine/scenery/move.js b/public/assets/javascripts/rectangles/engine/scenery/move.js index 93bccb0..94c6281 100644 --- a/public/assets/javascripts/rectangles/engine/scenery/move.js +++ b/public/assets/javascripts/rectangles/engine/scenery/move.js @@ -2,7 +2,7 @@ Scenery.move = function(base){ var x, y, z, position, dimension, bounds - var dragging = false + var dragging = false, moved = false var oldState this.bind = function(){ @@ -14,6 +14,7 @@ Scenery.move = function(base){ } this.unbind = function(){ + base.focused = false Scenery.mouse.unbind_el(base.mx.el) Scenery.mouse.off("down", down) Scenery.mouse.off("enter", switch_wall) @@ -22,29 +23,26 @@ Scenery.move = function(base){ } function down (e, cursor){ - if (e.target != base.mx.el) return; + if (e.target != base.mx.el && (e.target != base.mx.overlay)) return; if (editor.permissions.destroy) { - UndoStack.push({ - type: 'destroy-scenery', - undo: base.serialize(), - redo: { id: base.id }, - }) - - // TODO: watch individual scenery object here - Minotaur.watch( app.router.editorView.settings ) - - Scenery.remove(base.id) + base.remove() return } - + // load the modal app.controller.pick(base) + if (! base.focused) { + e.clickAccepted = false + base.focused = true + return + } if (! (editor.permissions.move || editor.permissions.resize) ) { e.clickAccepted = false return } dragging = true + moved = false x = base.mx.x y = base.mx.y z = base.mx.z @@ -59,6 +57,8 @@ Scenery.move = function(base){ function drag (e, cursor){ if (! dragging) return + + moved = true var flipX = base.wall.side & (FRONT | RIGHT) @@ -75,13 +75,14 @@ Scenery.move = function(base){ switch (base.wall.side) { case FRONT: case BACK: - base.mx.x = position.a + delta.a * cos(wall_rotation[base.wall.side]) + dimension.a / 2 + base.mx.x = position.a + delta.a * cos(base.wall.rotationY) + dimension.a / 2 break case LEFT: case RIGHT: - base.mx.z = position.a + delta.a * sin(wall_rotation[base.wall.side]) + dimension.a / 2 + base.mx.z = position.a + delta.a * sin(base.wall.rotationY) + dimension.a / 2 break } + if (editor.permissions.resize) { Scenery.resize.move_dots() } @@ -89,21 +90,22 @@ Scenery.move = function(base){ function up (e, cursor){ if (! dragging || ! oldState) return - - dragging = false - document.body.classList.remove("dragging") - - UndoStack.push({ - type: 'update-scenery', - undo: oldState, - redo: base.serialize(), - }) + + if (moved) { + UndoStack.push({ + type: 'update-scenery', + undo: oldState, + redo: base.serialize(), + }) - // TODO: watch individual scenery object here - Minotaur.watch( app.router.editorView.settings ) + // TODO: watch individual scenery object here + Minotaur.watch( app.router.editorView.settings ) + } + dragging = moved = false oldState = null - } + document.body.classList.remove("dragging") + } function switch_wall (e, target, cursor){ if (! dragging) return @@ -142,7 +144,7 @@ Scenery.move = function(base){ if (editor.permissions.resize) { Scenery.resize.rotate_dots() -// Scenery.resize.move_dots() + Scenery.resize.move_dots() } cursor.x.a = cursor.x.b diff --git a/public/assets/javascripts/rectangles/engine/scenery/randomize.js b/public/assets/javascripts/rectangles/engine/scenery/randomize.js new file mode 100644 index 0000000..6581f38 --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/scenery/randomize.js @@ -0,0 +1,115 @@ +/* + // get the list of media we want to place + var media_objs = $(".mediaContainer").toArray().map(function(el){ + return $(el).data("media") + }) + Scenery.randomize.add( media_objs ) +*/ + +Scenery.randomize = {}; + +// Given a list of media objects, generate their ideal dimensions +Scenery.randomize.get_dimensions = function (media_objs){ + var media_list = media_objs.map(function(media){ + var width, height + if (media.width > media.height) { + width = Math.min(DEFAULT_PICTURE_WIDTH, media.width) + height = media.height/media.width * width + } + else { + height = Math.min(DEFAULT_PICTURE_WIDTH, media.height) + width = media.width/media.height * height + } + return { + dimensions: new vec2( width, height ), + media: media, + } + }) + return media_list +} + +// Build the lookup of empty walls. +// Takes a list of walls to use, or undefined to use all empty walls. +// Returns a lookup of walls to use, keyed by wall ID. +Scenery.randomize.get_empty_walls = function(wall_list){ + // get a list of all walls + var walls = {}, removed = []; + + (wall_list || Walls.list).forEach(function(wall){ + walls[wall.id] = wall + }) + + // remove the walls that already have stuff on them + if (! wall_list) { + Scenery.forEach(function(scenery){ + if (scenery.was_randomly_placed) { + // remove it and reuse this wall? + removed.push( scenery.serialize() ) + Scenery.remove( scenery.id ) + } + else { + delete walls[scenery.wall.id] + } + }) + } + + return { walls: walls, removed: removed } +} + +// Randomly place a set of media objects on empty walls. +// Only one object per wall will be added. +// Optionally takes a list of walls to use. +Scenery.randomize.add = function (media_objs, wall_list) { + var media_list = Scenery.randomize.get_dimensions(media_objs) + var empty_data = Scenery.randomize.get_empty_walls(wall_list) + var walls = empty_data.walls + var removed = empty_data.removed + var added = [] + + var wall_ids = _.keys(walls) + if (! wall_ids.length) { return } + + // randomize walls + shuffle(wall_ids) + shuffle(media_list) + + // assign each of the media to the walls, until we run out of either + media_list.some(function(media){ + // bail if we're out of walls + if (wall_ids.length == 0) { return true } + + var i, fits = -1 + + for (i = 0; i < wall_ids.length; i++) { + if (walls[wall_ids[i]].surface.fits(media.dimensions)) { + fits = i + break + } + } + + if (fits != -1) { + var wall = walls[wall_ids[fits]] + wall_ids.splice(fits, 1) + + var scenery = Scenery.add({ + media: media.media, + wall: wall, + index: 0, + }) + scenery.was_randomly_placed = true + added.push(scenery.serialize()) + } + else { + // artwork won't fit anywhere?? + } + + return false + }) + + UndoStack.push({ + type: "randomize-scenery", + undo: { added: added, removed: removed }, + redo: { added: added, removed: removed }, + }) + +}
\ No newline at end of file diff --git a/public/assets/javascripts/rectangles/engine/scenery/resize.js b/public/assets/javascripts/rectangles/engine/scenery/resize.js index e26c0a7..252af74 100644 --- a/public/assets/javascripts/rectangles/engine/scenery/resize.js +++ b/public/assets/javascripts/rectangles/engine/scenery/resize.js @@ -6,8 +6,9 @@ Scenery.resize = new function(){ var obj var x, y, z, bounds var dragging = false - var naturalDimension, dimension, position, scale + var naturalDimension, naturalDimensionCopy, dimension, position, scale var oldState + var rotationY var dots = [], dot, selected_dot @@ -31,7 +32,8 @@ Scenery.resize = new function(){ // generate a dot element base.build_dot = function(side) { var dot = new MX.Object3D('.dot') - dot.width = dot.height = dot_side + dot.width = dot.height = dot_side * 2 + dot.scale = 0.5 dot.side = side $(dot.el).on({ mouseenter: function(){ base.hovering = true }, @@ -48,7 +50,8 @@ Scenery.resize = new function(){ // rotate the dots as appropriate base.rotate_dots = function(){ - rotationY = wall_rotation[obj.wall.side] + // console.trace() + rotationY = obj.wall.rotationY dots.forEach(function(dot){ dot.rotationY = rotationY }) @@ -56,45 +59,43 @@ Scenery.resize = new function(){ // move all the dots to the object's current position base.move_dots = function(){ - x = obj.mx.x + sin(rotationY) * dot_distance_from_picture - y = obj.mx.y - z = obj.mx.z - cos(rotationY) * dot_distance_from_picture + var x = obj.mx.x + sin(rotationY) * dot_distance_from_picture + var y = obj.mx.y + var z = obj.mx.z - cos(rotationY) * dot_distance_from_picture dots.forEach(function(dot){ - base.move_dot(dot) + base.move_dot(dot, { x: x, y: y, z: z }) }) } // move a dot .. to the initial position of the image - base.move_dot = function(dot){ - dot.x = x - dot.y = y - dot.z = z - + base.move_dot = function(dot, pos){ if (dot.side & TOP) { - dot.y += obj.mx.height * obj.mx.scale / 2 + pos.y += obj.dimensions.b / 2 } if (dot.side & BOTTOM) { - dot.y -= obj.mx.height * obj.mx.scale / 2 + pos.y -= obj.dimensions.b / 2 } if (dot.side & LEFT) { - dot.x -= cos(rotationY) * (obj.mx.width * obj.mx.scale) / 2 - dot.z -= sin(rotationY) * (obj.mx.width * obj.mx.scale) / 2 + pos.x -= cos(rotationY) * (obj.dimensions.a) / 2 + pos.z -= sin(rotationY) * (obj.dimensions.a) / 2 } if (dot.side & RIGHT) { - dot.x += cos(rotationY) * (obj.mx.width * obj.mx.scale) / 2 - dot.z += sin(rotationY) * (obj.mx.width * obj.mx.scale) / 2 + pos.x += cos(rotationY) * (obj.dimensions.a) / 2 + pos.z += sin(rotationY) * (obj.dimensions.a) / 2 } + dot.move(pos) } // pick a new object to focus on and show the dots base.show = function(new_object) { // if (obj === new_object) return + if (! new_object) return obj = new_object - base.add_dots() base.rotate_dots() base.move_dots() + Sculpture.resize.hide() } // dismiss the dots on blur @@ -119,22 +120,13 @@ Scenery.resize = new function(){ base.bind = function(){ dots.forEach(function(dot){ Scenery.mouse.bind_el(dot.el) - $(dot.el).bind({ - mouseenter: function(e){ - Scenery.resize.hovering = true - }, - mouseleave: function(e){ - Scenery.resize.hovering = false - base.defer_hide() - } - }) }) Scenery.mouse.on("down", down) Scenery.mouse.on("drag", drag) Scenery.mouse.on("up", up) } - this.unbind = function(){ + base.unbind = function(){ dots.forEach(function(dot){ Scenery.mouse.unbind_el(dot.el) }) @@ -146,15 +138,19 @@ Scenery.resize = new function(){ function down (e, cursor){ var selection = dots.filter(function(dot){return e.target == dot.el}) if (! selection.length) return - + selected_dot = selection[0] dragging = true naturalDimension = obj.naturalDimensions dimension = obj.dimensions position = new vec3(obj.mx.x, obj.mx.y, obj.mx.z) - scale = obj.mx.scale oldState = obj.serialize() + + if (obj.type == "text") { + naturalDimensionCopy = naturalDimension.clone() + positionCopy = position.clone() + } document.body.classList.add("dragging") } @@ -167,7 +163,6 @@ Scenery.resize = new function(){ var width = cursor.x.magnitude() var height = cursor.y.magnitude() var mag = cursor.magnitude() - var old_width = dimension.a * scale if (abs(width) >Â abs(height)) { mag = x_sign * mag * sign(width) @@ -176,18 +171,23 @@ Scenery.resize = new function(){ mag = y_sign * mag * sign(height) } - obj.set_scale( ( dimension.a + mag ) / naturalDimension.a ) - // dimension.a // scale * (old_width + mag) / old_width + if (obj.type == "text") { + obj.mx.width = obj.media.width = naturalDimension.a = naturalDimensionCopy.a + (mag * 2) + obj.mx.height = obj.media.height = naturalDimension.b = naturalDimensionCopy.b + (mag * 2) + dimension.a = naturalDimension.a * obj.scale + dimension.b = naturalDimension.b * obj.scale + } + else { + obj.set_scale( ( dimension.a + mag ) / naturalDimension.a ) + } -// console.log(scale, obj.mx.scale, dimension.a + mag, naturalDimension.a) - - if (selected_dot.side & LEFT_RIGHT) { - obj.mx.x = position.a + cos(rotationY) * mag/2 * (x_sign) - obj.mx.z = position.c + sin(rotationY) * mag/2 * (x_sign) - } - if (selected_dot.side & TOP_BOTTOM) { - obj.mx.y = position.b - mag/2 * y_sign - } + if (selected_dot.side & LEFT_RIGHT) { + obj.mx.x = position.a + cos(rotationY) * mag/2 * (x_sign) + obj.mx.z = position.c + sin(rotationY) * mag/2 * (x_sign) + } + if (selected_dot.side & TOP_BOTTOM) { + obj.mx.y = position.b - mag/2 * y_sign + } base.move_dots() @@ -195,12 +195,28 @@ Scenery.resize = new function(){ } function up (e, cursor){ + if (! dragging) return dragging = false - selected_dot = null if (! editor.permissions.resize) { return } - obj.scale = obj.mx.ops.scale = obj.mx.scale - obj.dimensions.assign(obj.naturalDimensions).mul(obj.scale) + if (obj.type == "text") { + var newHeight = $(obj.mx.inner).height() + if (selected_dot.side & BOTTOM) { + obj.mx.y = position.b + (naturalDimensionCopy.b - newHeight) / 2 * obj.scale + } + else { + obj.mx.y = dots[0].y - newHeight/2*obj.scale + } + obj.mx.height = obj.media.height = naturalDimension.b = newHeight + dimension.a = naturalDimension.a * obj.scale + dimension.b = naturalDimension.b * obj.scale + base.move_dots() + } + else { + obj.scale = obj.mx.ops.scale = obj.mx.scale + obj.dimensions.assign(obj.naturalDimensions).mul(obj.scale) + } + UndoStack.push({ type: 'update-scenery', undo: oldState, @@ -211,6 +227,7 @@ Scenery.resize = new function(){ Minotaur.watch( app.router.editorView.settings ) document.body.classList.remove("dragging") + selected_dot = null } } diff --git a/public/assets/javascripts/rectangles/engine/scenery/sound.js b/public/assets/javascripts/rectangles/engine/scenery/sound.js new file mode 100644 index 0000000..5a783d6 --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/scenery/sound.js @@ -0,0 +1,16 @@ + +Scenery.sound = {} +Scenery.sound.max_distance = 1500 +Scenery.sound.init = function(){ + app.tube.on("move", Scenery.sound.move) +} +Scenery.sound.move = function(){ + Scenery.forEach(function(scenery){ + if ((scenery.type == "video" || scenery.type == "audio")) { // && ! scenery.muted()) { + var distance = dist(cam.x, cam.z, + scenery.mx.x, scenery.mx.z) + var volume = 1 - (clamp( distance, 0, Scenery.sound.max_distance ) / Scenery.sound.max_distance) + scenery.setVolume(volume) + } + }) +} diff --git a/public/assets/javascripts/rectangles/engine/scenery/types/_object.js b/public/assets/javascripts/rectangles/engine/scenery/types/_object.js index 2dbae48..e3b9b4d 100644 --- a/public/assets/javascripts/rectangles/engine/scenery/types/_object.js +++ b/public/assets/javascripts/rectangles/engine/scenery/types/_object.js @@ -28,32 +28,119 @@ Scenery.types.base = Fiber.extend(function(base){ this.dimensions = this.naturalDimensions.clone().mul(this.scale) }, + place: function(opt){ + if (opt.data) { + if (opt.wall) { + var position = opt.wall.mxToPosition(opt.data.position) + opt.index = opt.wall.surface.index_for_x( position.a, 0 ) + } + this.set_wall(opt) + this.deserialize(opt.data) + } + else { + this.set_wall(opt) + if (this.wall && ! this.bounds) { + this.find_minimum_scale(opt) + if (! this.bounds) return + } + + this.recenter() + if (opt.position) { + this.translateTo(opt.position) + } + var mx_position = this.wall.positionToMx( this.position, this.dimensions ) + this.mx.move(mx_position) + } + }, + + find_minimum_scale: function(opt){ + var bounds = this.wall.surface.bounds_at_index_with_dimensions(opt.index || 0, new vec2(50, 50)) + var scale = 1 + if (! bounds || bounds.width() < 50 || bounds.height < 50) return + if (this.naturalDimensions.a > bounds.width()) { + scale = bounds.width() / (this.naturalDimensions.a + 10) + } + if (this.naturalDimensions.b > bounds.height()) { + scale = Math.min(scale, bounds.height() / (this.naturalDimensions.b + 10)) + } + this.set_scale(scale) + this.set_wall(opt) + }, + recenter: function () { if (! this.bounds) return var center = this.bounds.center() center.a -= this.dimensions.a / 2 center.b -= this.dimensions.b / 2 - var mx_position = this.wall.positionToMx( center, this.dimensions ) - this.mx.move(mx_position) this.position.assign(center) }, + translateTo: function (position){ + var flipX = this.wall.side & (FRONT | RIGHT) + var delta = position.clone().subVec(this.position) + delta.a -= this.dimensions.a/2 + delta.b -= this.dimensions.b/2 + + if (flipX) { delta.a *= -1 } + + var new_bounds = this.wall.surface.translate( this.bounds, this.dimensions, this.position, delta ) + + this.position.b += delta.b + + switch (this.wall.side) { + case FRONT: + case BACK: + this.position.a += delta.a * cos(this.wall.rotationY) + break + case LEFT: + case RIGHT: + this.position.a += delta.a * sin(this.wall.rotationY) + break + } + }, + bind: function(){ this.move.bind() - $(this.mx.el).bind({ - mouseenter: this.enter, - mouseleave: this.leave, - }) +// $(this.mx.el).bind({ +// mouseenter: this.enter, +// mouseleave: this.leave, +// }) }, unbind: function(){ this.move.unbind() - $(this.mx.el).unbind({ - mouseenter: this.enter, - mouseleave: this.leave, - }) +// $(this.mx.el).unbind({ +// mouseenter: this.enter, +// mouseleave: this.leave, +// }) }, + remove: function(){ + if (this.removed) return + this.removed = true + + UndoStack.push({ + type: 'destroy-scenery', + undo: this.serialize(), + redo: { id: this.id }, + }) + + // TODO: watch individual scenery object here + Minotaur.watch( app.router.editorView.settings ) + + Scenery.remove(this.id) + + Scenery.resize.hide() + if (app.controller.mediaEditor) { + app.controller.mediaEditor.tainted = false + app.controller.mediaEditor.hide() + } + if (app.controller.textEditor) { + app.controller.textEditor.tainted = false + app.controller.textEditor.hide() + } + }, + destroy: function(){ this.unbind() scene.remove(this.mx) @@ -70,17 +157,17 @@ Scenery.types.base = Fiber.extend(function(base){ }, enter: function(e){ - if (editor.permissions.resize) { - Scenery.resize.show(this) - Scenery.hovering = true - } +// if (editor.permissions.resize) { +// Scenery.resize.show(this) +// Scenery.hovering = true +// } }, leave: function(e){ - if (editor.permissions.resize) { - Scenery.resize.defer_hide(this) - Scenery.hovering = false - } +// if (editor.permissions.resize) { +// Scenery.resize.defer_hide(this) +// Scenery.hovering = false +// } }, serialize: function(){ diff --git a/public/assets/javascripts/rectangles/engine/scenery/types/audio.js b/public/assets/javascripts/rectangles/engine/scenery/types/audio.js new file mode 100644 index 0000000..fdd221d --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/scenery/types/audio.js @@ -0,0 +1,77 @@ + +Scenery.types.audio = Scenery.types.base.extend(function(base){ + + var exports = { + + type: 'audio', + + init: function(opt){ + + opt.scale = 1.0 + + base.init.call(this, opt) + + this.build() + this.bind() + this.place(opt) + }, + + build: function(){ + this.mx = new MX.Soundcloud({ + scale: this.scale, + media: this.media, + y: this.scale * this.media.height/2, + backface: false, + }) + scene.add( this.mx ) + this.mx.load() + }, + + serialize: function(){ + var data = base.serialize.call(this) + return data + }, + + deserialize: function(data){ + this.mx.move(data.position) + this.mx.ops.width = data.dimensions.a + this.mx.ops.height = data.dimensions.b + this.dimensions.deserialize(data.dimensions) + }, + + setVolume: function(n){ + this.mx.setVolume(n) + }, + + play: function(){ + this.mx.play() + }, + + pause: function(){ + this.mx.pause() + }, + + toggle: function(){ + this.mx.toggle() + }, + + paused: function(){ + return this.mx.paused + }, + + muted: function(){ + return this.mx.muted + }, + + seek: function(n){ + this.mx.seek(n) + }, + + setLoop: function(shouldLoop){ + this.mx.setLoop(shouldLoop) + }, + + } + + return exports +}) diff --git a/public/assets/javascripts/rectangles/engine/scenery/types/image.js b/public/assets/javascripts/rectangles/engine/scenery/types/image.js index d2fa3ab..0e5e77c 100644 --- a/public/assets/javascripts/rectangles/engine/scenery/types/image.js +++ b/public/assets/javascripts/rectangles/engine/scenery/types/image.js @@ -3,27 +3,17 @@ Scenery.types.image = Scenery.types.base.extend(function(base){ var exports = { + type: 'image', + init: function(opt){ - opt.scale = opt.scale || (opt.data && opt.data.scale) || 300 / max(300, opt.media.width) + opt.scale = opt.scale || (opt.data && opt.data.scale) || DEFAULT_PICTURE_WIDTH / max(DEFAULT_PICTURE_WIDTH, opt.media.width) base.init.call(this, opt) this.build() this.bind() - - if (opt.data) { - if (opt.wall) { - var position = opt.wall.mxToPosition(opt.data.position) - opt.index = opt.wall.surface.index_for_x( position.a, 0 ) - } - this.set_wall(opt) - this.deserialize(opt.data) - } - else { - this.set_wall(opt) - this.bounds && this.recenter() - } + this.place(opt) }, build: function(){ @@ -46,6 +36,7 @@ Scenery.types.image = Scenery.types.base.extend(function(base){ this.mx.move(data.position) this.mx.ops.width = data.dimensions.a this.mx.ops.height = data.dimensions.b + this.dimensions.deserialize(data.dimensions) }, } diff --git a/public/assets/javascripts/rectangles/engine/scenery/types/text.js b/public/assets/javascripts/rectangles/engine/scenery/types/text.js new file mode 100644 index 0000000..6e11da2 --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/scenery/types/text.js @@ -0,0 +1,58 @@ + +Scenery.types.text = Scenery.types.base.extend(function(base){ + + var exports = { + + type: 'text', + + init: function(opt){ + + opt.scale = 0.5 + + base.init.call(this, opt) + + this.build() + this.bind() + this.place(opt) + }, + + build: function(){ + this.mx = new MX.Text({ + scale: this.scale, + media: this.media, + y: this.scale * this.media.height/2, + backface: false, + }) + scene.add( this.mx ) + }, + + setText: function(text){ + this.media.description = text + this.mx.setText( text ) + }, + + setFont: function(style){ + for (var i in style) { + if (style.hasOwnProperty(i)) { + this.media.font[i] = style[i] + } + } + this.mx.setFont(this.media.font) + }, + + serialize: function(){ + var data = base.serialize.call(this) + return data + }, + + deserialize: function(data){ + this.mx.move(data.position) + this.mx.ops.width = data.dimensions.a + this.mx.ops.height = data.dimensions.b + this.dimensions.deserialize(data.dimensions) + }, + + } + + return exports +}) diff --git a/public/assets/javascripts/rectangles/engine/scenery/types/video.js b/public/assets/javascripts/rectangles/engine/scenery/types/video.js index ef25d8d..163e19e 100644 --- a/public/assets/javascripts/rectangles/engine/scenery/types/video.js +++ b/public/assets/javascripts/rectangles/engine/scenery/types/video.js @@ -2,27 +2,18 @@ Scenery.types.video = Scenery.types.base.extend(function(base){ var exports = { + + type: 'video', init: function(opt){ - opt.scale = opt.scale || 300 / max(300, opt.media.width) - + + opt.scale = opt.scale || (opt.data && opt.data.scale) || DEFAULT_PICTURE_WIDTH / max(DEFAULT_PICTURE_WIDTH, opt.media.width) + base.init.call(this, opt) this.build() this.bind() - - if (opt.data) { - if (opt.wall) { - var position = opt.wall.mxToPosition(opt.data.position) - opt.index = opt.wall.surface.index_for_x( position.a, 0 ) - } - this.set_wall(opt) - this.deserialize(opt.data) - } - else { - this.set_wall(opt) - this.bounds && this.recenter() - } + this.place(opt) }, build: function(){ @@ -47,7 +38,8 @@ Scenery.types.video = Scenery.types.base.extend(function(base){ backface: false, }) scene.add(this.mx) - this.mx.load() + + this.mx.load() }, play: function(){ @@ -99,6 +91,14 @@ Scenery.types.video = Scenery.types.base.extend(function(base){ } }, + unmute: function(){ + this.mx.unmute() + }, + + setVolume: function(n){ + this.mx.setVolume(n) + }, + serialize: function(){ var data = base.serialize.call(this) return data @@ -106,6 +106,9 @@ Scenery.types.video = Scenery.types.base.extend(function(base){ deserialize: function(data){ this.mx.move(data.position) + this.mx.ops.width = data.dimensions.a + this.mx.ops.height = data.dimensions.b + this.dimensions.deserialize(data.dimensions) }, } diff --git a/public/assets/javascripts/rectangles/engine/scenery/undo.js b/public/assets/javascripts/rectangles/engine/scenery/undo.js index e5624a0..b976ea2 100644 --- a/public/assets/javascripts/rectangles/engine/scenery/undo.js +++ b/public/assets/javascripts/rectangles/engine/scenery/undo.js @@ -4,12 +4,14 @@ type: "create-scenery", undo: function(state){ Scenery.remove(state.id) + Scenery.resize.hide() // TODO: watch individual scenery object here Minotaur.watch( app.router.editorView.settings ) }, redo: function(state){ - Scenery.deserialize([ state ]) + var scenery = Scenery.deserialize([ state ]) + Scenery.resize.show( scenery ) // TODO: watch individual scenery object here Minotaur.watch( app.router.editorView.settings ) @@ -19,8 +21,10 @@ type: "update-scenery", undo: function(state){ var scenery = Scenery.find(state.id) + var wall = Walls.find( state.wall_id ) + scenery.deserialize(state) - scenery.set_wall(Walls.find( state.wall_id )) + scenery.set_wall({ wall: wall }) if (editor.permissions.resize) { Scenery.resize.show(scenery) @@ -31,13 +35,15 @@ }, redo: function(state){ var scenery = Scenery.find(state.id) + var wall = Walls.find( state.wall_id ) + scenery.deserialize(state) - scenery.set_wall(Walls.find( state.wall_id )) + scenery.set_wall({ wall: wall }) if (editor.permissions.resize) { Scenery.resize.show(scenery) - Scenery.resize.rotate_dots() Scenery.resize.move_dots() + Scenery.resize.rotate_dots() } // TODO: watch individual scenery object here @@ -47,12 +53,14 @@ { type: "destroy-scenery", undo: function(state){ - Scenery.deserialize([ state ]) + var scenery = Scenery.deserialize([ state ]) + Scenery.resize.show( scenery ) // TODO: watch individual scenery object here Minotaur.watch( app.router.editorView.settings ) }, redo: function(state){ + Scenery.resize.hide() Scenery.remove(state.id) // TODO: watch individual scenery object here @@ -66,11 +74,11 @@ type: "create-room", undo: function(room){ Rooms.remove(room) - Rooms.clipper.update() + Rooms.rebuild() }, redo: function(room){ Rooms.add(new Room(room)) - Rooms.clipper.update() + Rooms.rebuild() app.tube("builder-pick-room", room) }, }, @@ -80,14 +88,14 @@ var room = Rooms.list[state.id] room.rect.assign( state.rect ) room.height = state.height - Rooms.clipper.update() + Rooms.rebuild() app.tube("builder-pick-room", room) }, redo: function(state){ var room = Rooms.list[state.id] room.rect.assign( state.rect ) room.height = state.height - Rooms.clipper.update() + Rooms.rebuild() app.tube("builder-pick-room", room) }, }, @@ -95,12 +103,22 @@ type: "destroy-room", undo: function(room){ Rooms.add(new Room(room)) - Rooms.clipper.update() + Rooms.rebuild() app.tube("builder-pick-room", room) }, redo: function(room){ Rooms.remove(room) - Rooms.clipper.update() + Rooms.rebuild() + }, + }, + + { + type: "update-rooms-height", + undo: function(state){ + var rooms = Rooms.values() + rooms.forEach(function(room){ + room.height = state + }) }, }, @@ -111,7 +129,32 @@ undo: function(state){ var wall = Walls.lookup[state.id] wall.deserialize(state) - + Minotaur.watch( app.router.editorView.settings ) + }, + }, + { + type: "update-all-wallpaper", + undo: function(state){ + Walls.deserialize(state) + Minotaur.watch( app.router.editorView.settings ) + }, + }, + { + type: "choose-preset", + undo: function(state){ + app.controller.colorControl.load(state.colors) + Walls.deserialize(state.walls) + Minotaur.watch( app.router.editorView.settings ) + }, + redo: function(state){ + app.controller.presets.loadByName(state) + Minotaur.watch( app.router.editorView.settings ) + }, + }, + { + type: "choose-another-preset", + undo: function(state){ + app.controller.presets.loadByName(state) Minotaur.watch( app.router.editorView.settings ) }, }, @@ -119,11 +162,103 @@ type: "update-colors", undo: function(state){ Walls.setColor[ state.mode ]( state.rgb ) - app.router.editorView.lightControl.setSwatchColor( state.mode, state.rgb ) + app.router.editorView.colorControl.setSwatchColor( state.mode, state.rgb ) + Minotaur.watch( app.router.editorView.settings ) + }, + }, + { + type: "randomize-scenery", + undo: function(state){ + state.added.forEach(function(_scenery){ + Scenery.remove(_scenery.id) + }) + var scenery_list = Scenery.deserialize(state.removed) + scenery_list.forEach(function(scenery){ + scenery.was_randomly_placed = true + }) + Scenery.resize.hide() + // TODO: watch individual scenery object here + Minotaur.watch( app.router.editorView.settings ) + }, + redo: function(state){ + state.removed.forEach(function(_scenery){ + Scenery.remove(_scenery.id) + }) + var scenery_list = Scenery.deserialize(state.added) + scenery_list.forEach(function(scenery){ + scenery.was_randomly_placed = true + }) + + // TODO: watch individual scenery object here Minotaur.watch( app.router.editorView.settings ) }, }, + // + { + type: "create-sculpture", + undo: function(state){ + Sculpture.remove(state.id) + Sculpture.resize.hide() + + // TODO: watch individual scenery object here + Minotaur.watch( app.router.editorView.settings ) + }, + redo: function(state){ + var scenery = Sculpture.deserialize([ state ]) + Sculpture.resize.show( sculpture ) + + // TODO: watch individual scenery object here + Minotaur.watch( app.router.editorView.settings ) + }, + }, + { + type: "update-sculpture", + undo: function(state){ + var sculpture = Sculpture.find(state.id) + + sculpture.deserialize(state) + + if (editor.permissions.resize) { + Sculpture.resize.show(sculpture) + } + + // TODO: watch individual scenery object here + Minotaur.watch( app.router.editorView.settings ) + }, + redo: function(state){ + var sculpture = Sculpture.find(state.id) + + sculpture.deserialize(state) + + if (editor.permissions.resize) { + Sculpture.resize.show(sculpture) + Sculpture.resize.move_dots() + Sculpture.resize.rotate_dots() + } + + // TODO: watch individual scenery object here + Minotaur.watch( app.router.editorView.settings ) + }, + }, + { + type: "destroy-sculpture", + undo: function(state){ + var sculpture = Sculpture.deserialize([ state ]) + Sculpture.resize.show( sculpture ) + + // TODO: watch individual scenery object here + Minotaur.watch( app.router.editorView.settings ) + }, + redo: function(state){ + Sculpture.resize.hide() + Sculpture.remove(state.id) + + // TODO: watch individual scenery object here + Minotaur.watch( app.router.editorView.settings ) + }, + }, + ]) })() diff --git a/public/assets/javascripts/rectangles/engine/sculpture/_sculpture.js b/public/assets/javascripts/rectangles/engine/sculpture/_sculpture.js new file mode 100644 index 0000000..888b925 --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/sculpture/_sculpture.js @@ -0,0 +1,105 @@ + +var Sculpture = new function(){ + + var base = this; + + base.list = {} + + base.mouse = new mouse ({ use_offset: false, mousedownUsesCapture: true }) + + base.init = function(){ + app.on("move", base.updateBillboards) + base.resize.init() + } + + base.updateBillboards = function(){ + base.forEach(function(sculpture){ + if (sculpture.billboard) { + sculpture.mx.rotationY = cam.rotationY + } + }) + if (Sculpture.resize.obj && Sculpture.resize.obj.billboard) { + Sculpture.resize.move_dots() + } + } + + base.add = function(opt){ + var sculpture + switch (opt.media.type) { + case 'image': + sculpture = new Sculpture.types.image (opt) + break + } + base.list[sculpture.id] = sculpture + return sculpture + } + + base.addNext = function(opt){ + opt.newMedia = true + opt.media = Scenery.nextMedia + var sculpture = base.add(opt) + + // test if sculpture was placed here + if (! sculpture) { + return null + } + else { + Scenery.nextMedia = null + return sculpture + } + } + + base.find = function(id){ + return base.list[id] || null + } + + base.remove = function(id){ + var scene_media = base.list[id] + delete base.list[id] + scene_media && scene_media.destroy() + } + + base.removeAll = function(){ + base.forEach(function(scene_media){ + base.remove(scene_media.id) + }) + } + + base.uid = new UidGenerator(base.list) + + base.forEach = function(f){ + return base.values().forEach(f) + } + + base.map = function(f){ + return base.values().map(f) + } + + base.values = function(){ + return _.values(base.list) + } + + base.serialize = function(){ + var sculptures = base.map(function(sculpture){ + return sculpture.serialize() + }) + return sculptures + } + + base.deserialize = function(sculpture_data){ + var added = [] + sculpture_data.forEach(function(data){ + var scene_media = base.add({ + id: data.id, + data: data, + media: data.media, + }) + added.push(scene_media) + }) + return added + } + + return base +} + +Sculpture.types = {} diff --git a/public/assets/javascripts/rectangles/engine/sculpture/move.js b/public/assets/javascripts/rectangles/engine/sculpture/move.js new file mode 100644 index 0000000..0cbeccd --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/sculpture/move.js @@ -0,0 +1,111 @@ + +Sculpture.move = function(base){ + + var x, y, z, position, dimension, bounds + var rotationY + var dragging = false, altPressed = false, shiftPressed = false, moved = false + var oldState + var height, width + + this.bind = function(){ + Sculpture.mouse.bind_el(base.mx.el) + Sculpture.mouse.on("down", down) + Sculpture.mouse.on("drag", drag) + Sculpture.mouse.on("up", up) + } + + this.unbind = function(){ + base.focused = false + Sculpture.mouse.unbind_el(base.mx.el) + Sculpture.mouse.off("down", down) + Sculpture.mouse.off("drag", drag) + Sculpture.mouse.off("up", up) + } + + function down (e, cursor){ + if (e.target != base.mx.el && (e.target != base.mx.overlay)) return; + if (editor.permissions.destroy) { + base.remove() + return + } + + // load the modal + app.controller.pick(base) + + if (! base.focused) { + e.clickAccepted = false + base.focused = true + return + } + if (! (editor.permissions.move || editor.permissions.resize) ) { + e.clickAccepted = false + return + } + altPressed = (e.altKey && ! base.billboard) + shiftPressed = e.shiftKey + dragging = true + moved = false + x = base.mx.x + y = base.mx.y + z = base.mx.z + rotationY = base.mx.rotationY + height = base.mx.height * base.mx.scale + + bounds = base.bounds + dimension = base.dimensions + + oldState = base.serialize() + document.body.classList.add("dragging") + } + + function drag (e, cursor){ + if (! dragging) return + + moved = true + + var delta = cursor.delta() + delta.mul( cursor_amp ) // TODO: this should be proportional to your distance from the wall + delta.b *= -1 + + // here we need to move the element based on the XY coords, as + // base.mx.y = position.b + delta.b + dimension.b / 2 + if (shiftPressed) { + base.mx.y = clamp( y + delta.b, height/2 + sculpture_distance_from_floor, Infinity ) + } + else if (altPressed) { + base.mx.rotationY = mod( rotationY + delta.a / (window.innerWidth / 2) , TWO_PI ) + Sculpture.resize.move_dots() + } + else { + base.mx.x = x + delta.a * cos(cam.rotationY) - delta.b * sin(cam.rotationY) + base.mx.z = z + delta.a * sin(cam.rotationY) + delta.b * cos(cam.rotationY) + } + + if (editor.permissions.resize) { + Sculpture.resize.move_dots() + base.updateOutline() + } + } + + function up (e, cursor){ + if (! dragging || ! oldState) return + + if (moved) { + UndoStack.push({ + type: 'update-sculpture', + undo: oldState, + redo: base.serialize(), + }) + + // TODO: watch individual sculpture object here + Minotaur.watch( app.router.editorView.settings ) + } + + dragging = moved = false + oldState = null + document.body.classList.remove("dragging") + } + + return this + +}
\ No newline at end of file diff --git a/public/assets/javascripts/rectangles/engine/sculpture/resize.js b/public/assets/javascripts/rectangles/engine/sculpture/resize.js new file mode 100644 index 0000000..5f21d66 --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/sculpture/resize.js @@ -0,0 +1,208 @@ +Sculpture.resize = new function(){ + + var base = this + + var obj + var x, y, z, bounds + var dragging = false + var naturalDimension, naturalDimensionCopy, dimension, position, scale + var oldState + var rotationY + + var dots = [], dot, selected_dot + + base.init = function(){ + base.build() + base.bind() + } + + // create 9 dots at the corners of the div + base.build = function(){ + [ TOP, + TOP_RIGHT, + RIGHT, + BOTTOM_RIGHT, + BOTTOM, + BOTTOM_LEFT, + LEFT, + TOP_LEFT ].forEach(base.build_dot) + } + + // generate a dot element + base.build_dot = function(side) { + var dot = new MX.Object3D('.dot') + dot.width = dot.height = dot_side * 2 + dot.scale = 0.5 + dot.side = side + $(dot.el).on({ + mouseenter: function(){ base.hovering = true }, + mouseleave: function(){ base.hovering = false }, + }) + dots.push(dot) + } + + base.add_dots = function(){ + dots.forEach(function(dot){ + scene.add(dot) + }) + } + + // move all the dots to the object's current position + base.move_dots = function(){ + rotationY = obj.mx.rotationY + + var x = obj.mx.x + sin(rotationY) * dot_distance_from_picture + var y = obj.mx.y + var z = obj.mx.z - cos(rotationY) * dot_distance_from_picture + + dots.forEach(function(dot){ + base.move_dot(dot, { x: x, y: y, z: z, rotationY: rotationY }) + }) + } + + // move a dot .. to the initial position of the image + base.move_dot = function(dot, pos){ + if (dot.side & TOP) { + pos.y += obj.dimensions.b / 2 + } + if (dot.side & BOTTOM) { + pos.y -= obj.dimensions.b / 2 + } + if (dot.side & LEFT) { + pos.x -= cos(rotationY) * (obj.dimensions.a) / 2 + pos.z -= sin(rotationY) * (obj.dimensions.a) / 2 + } + if (dot.side & RIGHT) { + pos.x += cos(rotationY) * (obj.dimensions.a) / 2 + pos.z += sin(rotationY) * (obj.dimensions.a) / 2 + } + dot.move(pos) + } + + // pick a new object to focus on and show the dots + base.show = function(new_object) { + // if (obj === new_object) return + if (! new_object) return + base.obj = obj = new_object + base.add_dots() + base.move_dots() + } + + // dismiss the dots on blur + var dotsHideTimeout; + base.defer_hide = function(){ + clearTimeout(dotsHideTimeout) + + dotsHideTimeout = setTimeout(function(){ + if (Scenery.hovering || Scenery.resize.hovering || Scenery.mouse.down) return + Scenery.resize.hide() + }, dot_hide_delay) + } + + base.hide = function () { + if (! obj) return + base.obj = obj = null + dots.forEach(function(dot){ + scene.remove(dot) + }) + } + + base.bind = function(){ + dots.forEach(function(dot){ + Sculpture.mouse.bind_el(dot.el) + }) + Sculpture.mouse.on("down", down) + Sculpture.mouse.on("drag", drag) + Sculpture.mouse.on("up", up) + } + + base.unbind = function(){ + dots.forEach(function(dot){ + Sculpture.mouse.unbind_el(dot.el) + }) + Sculpture.mouse.off("down", down) + Sculpture.mouse.off("drag", drag) + Sculpture.mouse.off("up", up) + } + + function down (e, cursor){ + var selection = dots.filter(function(dot){return e.target == dot.el}) + if (! selection.length) return + + selected_dot = selection[0] + dragging = true + + naturalDimension = obj.naturalDimensions + dimension = obj.dimensions + position = new vec3(obj.mx.x, obj.mx.y, obj.mx.z) + oldState = obj.serialize() + + if (obj.type == "text") { + naturalDimensionCopy = naturalDimension.clone() + positionCopy = position.clone() + } + + document.body.classList.add("dragging") + } + + function drag (e, cursor){ + if (! dragging) return + + var x_sign = selected_dot.side & LEFT ? -1 : selected_dot.side & RIGHT ? 1 : 0 + var y_sign = selected_dot.side & TOP ? -1 : selected_dot.side & BOTTOM ? 1 : 0 + var width = cursor.x.magnitude() + var height = cursor.y.magnitude() + var mag = cursor.magnitude() + + if (abs(width) >Â abs(height)) { + mag = x_sign * mag * sign(width) + } + else { + mag = y_sign * mag * sign(height) + } + + if (obj.type == "text") { + obj.mx.width = obj.media.width = naturalDimension.a = naturalDimensionCopy.a + (mag * 2) + obj.mx.height = obj.media.height = naturalDimension.b = naturalDimensionCopy.b + (mag * 2) + dimension.a = naturalDimension.a * obj.scale + dimension.b = naturalDimension.b * obj.scale + } + else { + obj.set_scale( ( dimension.a + mag ) / naturalDimension.a ) + } + + if (selected_dot.side & LEFT_RIGHT) { + obj.mx.x = position.a + cos(rotationY) * mag/2 * (x_sign) + obj.mx.z = position.c + sin(rotationY) * mag/2 * (x_sign) + } + if (selected_dot.side & TOP_BOTTOM) { + obj.mx.y = position.b - mag/2 * y_sign + } + + base.move_dots() + obj.updateOutline() + + app.controller.sculptureEditor.setDimensions() + } + + function up (e, cursor){ + if (! dragging) return + dragging = false + if (! editor.permissions.resize) { return } + + obj.scale = obj.mx.ops.scale = obj.mx.scale + obj.dimensions.assign(obj.naturalDimensions).mul(obj.scale) + + UndoStack.push({ + type: 'update-sculpture', + undo: oldState, + redo: obj.serialize(), + }) + + // TODO: watch individual scenery object here + Minotaur.watch( app.router.editorView.settings ) + + document.body.classList.remove("dragging") + selected_dot = null + } +}
\ No newline at end of file diff --git a/public/assets/javascripts/rectangles/engine/sculpture/types/_object.js b/public/assets/javascripts/rectangles/engine/sculpture/types/_object.js new file mode 100644 index 0000000..2f593e8 --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/sculpture/types/_object.js @@ -0,0 +1,176 @@ +Sculpture.types.base = Fiber.extend(function(base){ + + var exports = { + + init: function(opt){ + this.id = opt.id || Sculpture.uid("sculpture") + // this.move = new Sculpture.move (this) + this.media = opt.media + this.naturalDimensions = new vec3(this.media.width, this.media.height, this.media.width) + this.dimensions = new vec3(this.media.width, this.media.height, this.media.width) + + this.move = new Sculpture.move (this) + this.billboard = true + this.backface = true + + this.set_scale( opt.scale || this.media.scale || 1.0 ) + this.position = new vec2(0,0) + + this.isSculpture = true + }, + + set_scale: function(scale){ + this.scale = scale || 1.0 + if (this.mx) { + this.mx.scale = this.mx.ops.scale = this.scale + } + this.dimensions = this.naturalDimensions.clone().mul(this.scale) + }, + set_depth: function(depth){ + console.log(this.dimensions.c, this.naturalDimensions.c, depth) + this.dimensions.c = depth + this.naturalDimensions.c = depth / this.scale + }, + + place: function(opt){ + if (opt.data) { + this.deserialize(opt.data) + } + else { + this.mx.move(opt.position) + } + }, + + bind: function(){ + this.move.bind() + }, + + unbind: function(){ + this.move.unbind() + }, + + setOutline: function(val){ + this.outline = val + if (val && ! this.outlineEl) { + this.buildOutline() + } + this.outlineEl.el.style.display = val ? "block" : "none" + }, + setOutlineColor: function(val){ + this.outlineColor = val + if (this.outlineEl) { + this.outlineColor = this.outlineEl.el.style.borderColor = val + } + }, + setBillboard: function(val){ + this.billboard = val + if (this.billboard) { + this.mx.el.classList.add("backface-hidden") + this.mx.el.classList.remove("backface-visible") + this.mx.rotationY = cam.rotationY + } + else { + this.mx.el.classList.add("backface-visible") + this.mx.el.classList.remove("backface-hidden") + this.mx.rotationY = quantize(cam.rotationY, PI/2) + } + Sculpture.resize.move_dots() + }, + + buildOutline: function(){ + this.outlineEl = new MX.Object3D(".mx-outline") + this.outlineEl.width = this.naturalDimensions.a + this.outlineEl.height = this.naturalDimensions.c + this.outlineEl.scale = this.mx.scale + this.outlineEl.rotationX = -PI/2 + this.outlineEl.x = this.mx.x + this.outlineEl.y = sculpture_distance_from_floor + this.outlineEl.z = this.mx.z + this.outlineEl.el.style.borderColor = this.outlineColor || "#000000" + scene.add(this.outlineEl) + }, + updateOutline: function(){ + if (! this.outline) { return } + if (! this.outlineEl) { + this.buildOutline() + } + this.outlineEl.x = this.mx.x + this.outlineEl.y = sculpture_distance_from_floor + this.outlineEl.z = this.mx.z + this.outlineEl.width = this.naturalDimensions.a + this.outlineEl.height = this.naturalDimensions.c + + this.outlineEl.scale = this.mx.scale + }, + + remove: function(){ + if (this.removed) return + this.removed = true + + UndoStack.push({ + type: 'destroy-sculpture', + undo: this.serialize(), + redo: { id: this.id }, + }) + + // TODO: watch individual scenery object here + Minotaur.watch( app.router.editorView.settings ) + + Sculpture.remove(this.id) + + Sculpture.resize.hide() + if (app.controller.sculptureEditor) { + app.controller.sculptureEditor.tainted = false + app.controller.sculptureEditor.hide() + } + }, + + destroy: function(){ + this.unbind() + scene.remove(this.mx) + this.mx.media = null + this.mx.ops = null + this.mx = null + this.move = null + this.media = null + this.dimensions = null + this.naturalDimensions = null + this.wall = null + this.bounds = null + this.center = null + }, + + serialize: function(){ + var data = { + id: this.id, + dimensions: this.dimensions.serialize(), + position: app.position(this.mx), + scale: this.scale, + media: this.media, + billboard: this.billboard, + outline: this.outline, + outlineColor: this.outlineColor || "#000000", + backface: this.backface, + } + return data + }, + + deserialize: function(data){ + this.mx.move(data.position) + this.mx.ops.width = data.dimensions.a + this.mx.ops.height = data.dimensions.b + this.billboard = data.billboard + this.outline = data.outline + this.outlineColor = data.outlineColor || "#000000" + this.backface = data.backface + if (! this.backface) this.mx.el.classList.remove("backface-visible") + this.dimensions.deserialize(data.dimensions) + if (this.outline) { + this.buildOutline() + } + }, + } + + return exports + +}) diff --git a/public/assets/javascripts/rectangles/engine/sculpture/types/image.js b/public/assets/javascripts/rectangles/engine/sculpture/types/image.js new file mode 100644 index 0000000..1a53f5b --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/sculpture/types/image.js @@ -0,0 +1,38 @@ + +Sculpture.types.image = Sculpture.types.base.extend(function(base){ + + var exports = { + + type: 'image', + + init: function(opt){ + + opt.scale = opt.scale || (opt.data && opt.data.scale) || DEFAULT_PICTURE_WIDTH / max(DEFAULT_PICTURE_WIDTH, opt.media.width) + + base.init.call(this, opt) + + this.build(opt) + this.bind() + this.place(opt) + }, + + build: function(opt){ + this.mx = new MX.Image({ + src: this.media.url, + scale: this.scale, + media: this.media, + backface: true, + }) + + if (opt.position) { + opt.position.y = opt.position.y || this.scale * this.media.height/2 + sculpture_distance_from_floor + opt.position.rotationY = opt.position.rotationY || scene.camera.rotationY + } + + scene.add( this.mx ) + }, + + } + + return exports +}) diff --git a/public/assets/javascripts/rectangles/engine/shapes/ortho.js b/public/assets/javascripts/rectangles/engine/shapes/ortho.js new file mode 100644 index 0000000..163f646 --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/shapes/ortho.js @@ -0,0 +1,66 @@ +// An OrthoPolyline is a Polyline where all angles are 90 degrees. + +if (! ('window' in this) ) { + var Polyline = require("./polyline.js") +} + +var OrthoPolyline = Polyline.extend(function(base){ + var exports = {} + exports.type = function(){ + return "ortho" + } + exports.instantiate = function(){ + return new OrthoPolyline + } + exports.canCloseWith = function(p){ + return (this.points.length > 2 && this.points[0].distanceTo( p ) < 10/map.zoom) + } + exports.draw = function(ctx, fillStyle, strokeStyle){ + var points = this.points + if (! points.length) return + if (points.length == 1) { + ctx.fillStyle = "#f80" + map.draw.dot_at(this.points[0].a, points[0].b, 5) + } + if (points.length > 1) { + ctx.fillStyle = fillStyle + ctx.strokeStyle = strokeStyle + ctx.lineWidth = 2 / map.zoom + ctx.beginPath() + ctx.moveTo(points[0].a, points[0].b) + points.forEach(function(point, i){ + i && ctx.lineTo(point.a, point.b) + }) + strokeStyle && ctx.stroke() + if (! map.ui.placing || this.closed) { + fillStyle && ctx.fill() + } + } + } + exports.translateSegment = function(src, dest, dx, dy) { + if (src[0].a == src[1].a) { + dest[0].a = src[0].a + dx + dest[1].a = src[1].a + dx + if (src.length == 3) { + dest[2].a = src[2].a + dx + } + } + else { + dest[0].b = src[0].b + dy + dest[1].b = src[1].b + dy + if (src.length == 3) { + dest[2].b = src[2].b + dy + } + } + } + exports.close = function(){ + this.points[this.points.length] = this.points[0] + this.closed = true + } + return exports +}) + + +if (! ('window' in this) ) { + module.exports = OrthoPolyline +} diff --git a/public/assets/javascripts/rectangles/engine/shapes/polyline.js b/public/assets/javascripts/rectangles/engine/shapes/polyline.js new file mode 100644 index 0000000..b2cd92f --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/shapes/polyline.js @@ -0,0 +1,212 @@ +// A Polyline is a set of points inputted by the user using the V2 editor to trace a blueprint. +// Additionally, it manages a set of MX objects which correspond to the walls in 3D. +// In this way, it attempts to bridge the 2D (canvas, imperative) and 3D (css, declarative) views. + +if (! ('window' in this) ) { + var Fiber = require("../../../vendor/bower_components/fiber/src/fiber.js") + var vec2 = require("../../models/vec2") +} + +var Polyline = Fiber.extend(function(base){ + var exports = {} + exports.init = function(){ + this.points = [] + this.mx_points = [] + this.closed = false + } + exports.type = function(){ + return "polyline" + } + exports.instantiate = function(){ + return new Polyline + } + exports.add = function(p){ + this.points.push( p ) + this.mx_points.push( new MX.Point(p) ) + } + exports.firstPoint = function(){ + return this.points[0] + } + exports.lastPoint = function(){ + return this.points[this.points.length-1] + } + exports.canCloseWith = function(p){ + return (this.points.length > 2 && this.points[0].distanceTo( p ) < 10/map.zoom) + } + exports.getHeadAtIndex = function(index){ + if (index == 0) { return null } + if (index == this.points.length-1) { return this.clone() } + var head = this.instantiate() + head.points = this.points.slice(0, index+1) + return head + } + exports.getTailAtIndex = function(index){ + if (index == this.points.length-1) { return null } + if (index == 0) { return this.clone() } + var tail = this.instantiate() + tail.points = this.points.slice(index, this.points.length) + return tail + } + exports.clone = function(){ + var clone = this.instantiate() + clone.points = this.points.concat() + } + exports.getSegment = function(segment){ + var seg = [ + this.points[segment.head], + this.points[segment.tail], + ] + if (segment.head == 0) { + seg.push( this.lastPoint() ) + } + else if (segment.tail == this.points.length-1) { + seg.push( this.firstPoint() ) + } + return seg + } + exports.cloneSegment = function(segment){ + return this.getSegment(segment).map(function(point){ return point.clone() }) + } + exports.translateSegment = function(src, dest, dx, dy){ + dest[0].a = src[0].a + dx + dest[0].b = src[0].b + dy + dest[1].a = src[1].a + dx + dest[1].b = src[1].b + dy + if (src.length == 3) { + dest[2].a = src[2].a + dx + dest[2].b = src[2].b + dy + } + } + exports.hasPointNear = function(p){ + var point + for (var i = 0; i <Â this.points.length; i++){ + point = this.points[i] + if (point.distanceTo( p ) < 10/map.zoom) { + return point + } + } + return null + } + exports.hasEndPointNear = function(p){ + if (this.closed || ! this.points.length) return null + if (this.firstPoint().distanceTo( p ) < 10/map.zoom) { + return this.firstPoint() + } + if (this.lastPoint().distanceTo( p ) < 10/map.zoom) { + return this.lastPoint() + } + return null + } + exports.hasSegmentNear = function(p, min_dist){ + var p1, p2, d1, d2, sum, rat + var dx, dy, new_x, new_y, x, y, closest_distance = min_dist || Infinity + var closest_i = -1 + var points = this.points + var p1, p2 = points[0] + for (var i = 1; i < points.length; i++) { + p1 = p2 + p2 = points[i] + d1 = p2.a - p1.a + d2 = p2.b - p1.b + sum = d1*d1 + d2*d2 + rat = ((p.a - p1.a) * d1 + (p.b - p1.b) * d2) / sum + rat = rat < 0 ? 0 : rat < 1 ? rat : 1 + new_x = p1.a + rat * d1 + new_y = p1.b + rat * d2 + dx = new_x - p.a + dy = new_y - p.b + sum2 = sqrt(dx*dx+dy*dy) + if (sum2 < closest_distance) { + x = new_x + y = new_y + closest_distance = sum2 + closest_i = i + } + } + if (closest_i == -1) return null + return { + x: x, + y: y, + distance: closest_distance, + head: closest_i-1, + tail: closest_i, + } + } + exports.draw = function(ctx, fillStyle, strokeStyle){ + var points = this.points + if (! points.length) return + if (points.length == 1) { + ctx.fillStyle = "#f80" + map.draw.dot_at(this.points[0].a, points[0].b, 5) + } + if (points.length > 1) { + ctx.fillStyle = fillStyle + ctx.strokeStyle = strokeStyle + ctx.lineWidth = 2 / map.zoom + ctx.beginPath() + ctx.moveTo(points[0].a, points[0].b) + points.forEach(function(point, i){ + i && ctx.lineTo(point.a, point.b) + }) + strokeStyle && ctx.stroke() + if (! map.ui.placing || this.closed) { + fillStyle && ctx.fill() + } + } + } + exports.draw_line = function (ctx, p){ + var last = this.points[this.points.length-1] + ctx.strokeStyle = "#f80" + ctx.lineWidth = 2 / map.zoom + ctx.beginPath() + ctx.moveTo(last.a, last.b) + ctx.lineTo(p.a, p.b) + ctx.stroke() + } + exports.close = function(){ + this.points[this.points.length] = this.points[0] + this.closed = true + } + exports.build = function(){ + this.mx_points && this.mx_points.forEach(function(mx){ scene.remove(mx) }) + this.mx = new MX.Polyline(this) + } + exports.rebuild = function(){ + this.mx.rebuild() + } + exports.getSegments = function(){ + if (this.points.length == 1) { + return [] + } + var segments = [] + for (var i = 1; i < this.points.length; i++) { + segments.push( [ this.points[i-1], this.points[i] ] ) + } + return segments + } + exports.serialize = function(){ + return { + type: this.type(), + closed: this.closed, + points: this.points.map(function(point){ return [point.a, point.b] }), + } + } + exports.deserialize = function(data){ + this.closed = data.closed || false + this.points = (data.points || data).map(function(point){ return new vec2(point[0], point[1]) }) + } + exports.reset = function(){ + this.mx_points.forEach(function(mx){ scene.remove(mx) }) + this.mx_points.length = 0 + this.points.length = 0 + } + exports.destroy = function(){ + this.reset() + this.mx && this.mx.destroy() + } + return exports +}) + +if (! ('window' in this) ) { + module.exports = Polyline +} diff --git a/public/assets/javascripts/rectangles/engine/shapes/regionlist.js b/public/assets/javascripts/rectangles/engine/shapes/regionlist.js new file mode 100644 index 0000000..8c9e732 --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/shapes/regionlist.js @@ -0,0 +1,240 @@ +// This algorithm takes the polylines from ShapeList as input and produces +// a set of Rooms which can be used by the existing V1 mover and editor. + +// The algorithm assumes that +// 1) all angles are orthogonal +// 2) all polylines are closed + +if (! ('window' in this) ) { + var Fiber = require("../../../vendor/bower_components/fiber/src/fiber.js") + var vec2 = require("../../models/vec2") + var Rect = require("../../models/rect") + var sort = require("../../util/sort") +} + +var RegionList = (function(){ + + var RegionList = {} + var regions = RegionList.regions + + // Build a list of regions from the existing shapes. + // Operates on all the shapes at once. + RegionList.build = function(){ + var segments = RegionList.getSortedSegments() + return RegionList.buildRoomsFromSegments(segments) + } + + // Build a list of regions from the individual shapes. + // Same, but operates on the shapes individually + RegionList.buildByShape = function(){ + var shapes = RegionList.getSortedSegmentsByShape() + var region_lists = shapes.map(function(shape){ + return RegionList.buildRoomsFromSegments(shape.segments) + }) + var regions = [] + return regions.concat.apply(regions, region_lists); + } + + RegionList.buildRoomsFromSegments = function(segments){ + + var rooms = [] + var seen_rooms = {} + var open_segments = [] + + var segment, open_segment, x_segment, y_segments, overlapped, seen_segments + + // first pass: generate rooms from the vertical segments only + for (var i = 0; i < segments.length; i++) { + segment = segments[i] + if (! segment.isVertical()) { + continue + } + + // check all the "open segments" we know about, i.e. rooms where we've only found + // the right wall. + overlapped = false + for (var j = 0; j < open_segments.length; j++) { + open_segment = open_segments[j] + + // if these two segments overlap each other, then there is a room between them. + if (segment.y.overlaps(open_segment.y)) { + overlapped = true + open_segments.splice(j--, 1) + + // the X part of the room will be the span between these two vertical segments' + // X components. the Y part of the room is the "split" or subdivision between + // the two horizontal vectors. + + // the split function is non-commutative, + // so we need to call A split B and B split A, + // then dedupe the segments we got back.. + x_segment = new vec2( open_segment.x.a, segment.x.b ) + y_segments = open_segment.y.split(segment.y, 0, 0) + + seen_segments = {} + + // check each of the splits.. if the two segments overlap, then we definitely + // have a room here. + y_segments.forEach(function(seg){ + seen_segments[ seg[0]+"" ] = true + var room = new Rect( x_segment, seg[0] ) + + if (seen_rooms[ room+"" ]) return + seen_rooms[ room+"" ] = true + + room.sides = 0 + + rooms.push(room) + var new_seg = new Rect( segment.x, seg[0] ) + open_segments.unshift(new_seg) + j++ + }) + + // splitting the other way.. + y_segments = segment.y.split(open_segment.y, 0, 0) + y_segments.forEach(function(seg){ + if (seen_segments[ seg[0]+"" ]) return; + var new_seg = new Rect( segment.x, seg[0] ) + open_segments.unshift(new_seg) + j++ + }) + } + } + + // if we have overlap, then re-sort the open segments Y-wise + // and (again) dedupe.. + if (overlapped) { + open_segments = open_segments.sort(function(a,b){ + if (a.y.a < b.y.a) { return -1 } + if (a.y.a == b.y.a) { return 0 } + if (a.y.a > b.y.a) { return 1 } + }) + + if (open_segments.length < 2) { continue } + + for (var k = 1; k < open_segments.length; k++) { + if (open_segments[k-1].y.containsVec(open_segments[k].y)) { + open_segments.splice(k--, 1) + } + else if (open_segments[k-1].y.overlaps(open_segments[k].y)) { + open_segments[k].y = open_segments[k].y.clone() + open_segments[k].y.a = open_segments[k-1].y.b + } + } + } + // if we don't have any overlap, then this is a new open segment. + else { + open_segments.push(segment) + } + } + + var splits, splitter + + // second pass: now that we have a bunch of rooms, assign sides to all of them. + // sides are used in the "mover" script to do bounds checking. + for (var i = 0; i < segments.length; i++) { + segment = segments[i] + var horizontal = segment.isHorizontal(), vertical = segment.isVertical() + for (var r = 0; r < rooms.length; r++){ + room = rooms[r] + + // vertical segments determine the left and right edges of the room, fairly simply. + if (vertical) { + if (segment.y.containsVec(room.y)) { + if (segment.x.a == room.x.a) { + room.sides |= LEFT + } + if (segment.x.a == room.x.b) { + room.sides |= RIGHT + } + } + } + + // horizontal segments determine the front and back edges of the room. + if (horizontal) { + if (segment.x.containsVec(room.x)) { + if (segment.y.a == room.y.a) { + room.sides |= FRONT + } + if (segment.y.a == room.y.b) { + room.sides |= BACK + } + } + + // however, since we did not split on horizontal segments, our rooms may + // only have partial overlap with these segments, in which case we need to + // split the rooms apart. + else if ((segment.y.a == room.y.a || segment.y.a == room.y.b) && room.x.overlaps(segment.x)) { + + // split the room across the segment. preserve whether or not we know the + // room borders a segment on the left or right. + splits = room.x.split(segment.x, room.sides & LEFT, room.sides & RIGHT) + rooms.splice(r--, 1) + for (var k = 0; k < splits.length; k++) { + splitter = splits[k] + var new_room = new Rect( splitter[0], room.y ) + new_room.sides = splitter[1] | (Â room.sides & FRONT_BACK ) + if (segment.x.overlaps( splitter[0] )) { + if (segment.y.a == new_room.y.a) { + new_room.sides |= FRONT + } + if (segment.y.a == new_room.y.b) { + new_room.sides |= BACK + } + } + rooms.unshift(new_room) + r++ + } + } + + } + } + } + + return rooms + } + + // Gets a list of polylines from the ShapeList and sorts the segments. + RegionList.getSortedSegments = function(){ + // get a list of all segments from these polylines + var segments = shapes.getAllSegments() + + segments = segments.map(RegionList.segmentsToRects) + + return sort.rects_by_position(segments) + } + + RegionList.getSortedSegmentsByShape = function(){ + return shapes.getAllShapeSegments().map(function(shape){ + var segments = shape.map(RegionList.segmentsToRects) + return { + shape: shape, + segments: sort.rects_by_position(segments) + } + }) + } + + // re-orient a segment so it's either facing up or right and make it into a rect + RegionList.segmentsToRects = function(segment){ + // vertical + if (segment[0].a == segment[1].a) { + if (segment[0].b > segment[1].b) { + segment.push(segment.shift()) + } + } + // horizontal + else if (segment[0].b == segment[1].b) { + if (segment[0].a > segment[1].a) { + segment.push(segment.shift()) + } + } + return new Rect( segment[0].a, segment[0].b, segment[1].a, segment[1].b ) + } + + if (! ('window' in this) ) { + module.exports = RegionList + } + + return RegionList + +})()
\ No newline at end of file diff --git a/public/assets/javascripts/rectangles/engine/shapes/shapelist.js b/public/assets/javascripts/rectangles/engine/shapes/shapelist.js new file mode 100644 index 0000000..21beb76 --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/shapes/shapelist.js @@ -0,0 +1,124 @@ +// The ShapeList manages the list of polylines which form a V2 layout. + +if (! ('window' in this) ) { + var Fiber = require("../../../vendor/bower_components/fiber/src/fiber.js") + var Polyline = require("./polyline.js") + var OrthoPolyline = require("./ortho.js") +} + +var ShapeList = Fiber.extend(function(base){ + var exports = {} + exports.init = function(){ + this.shapes = [] + this.workline = null + } + exports.add = function(shape){ + this.shapes.push(shape) + } + exports.remove = function(shape){ + var index = this.shapes.indexOf(shape) + if (index !== -1) { + this.shapes.splice(index, 1) + } + } + exports.destroy = function(){ + this.shapes.forEach(function(shape){ + shape.destroy() + }) + this.shapes = [] + } + exports.count = function(){ + return this.shapes.length + } + exports.removeSegment = function (segment){ + var shape = segment.shape + var head = shape.getHeadAtIndex(segment.head) + var tail = shape.getTailAtIndex(segment.tail) + this.remove(shape) + shape.destroy() + if (head) { + this.add(head) + head.build() + } + if (tail) { + this.add(tail) + tail.build() + } + } + exports.findClosestPoint = function (p){ + var point + for (var i = 0; i < this.shapes.length; i++) { + point = this.shapes[i].hasPointNear(p) + if (point) return { point: point, shape: this.shapes[i] } + } + return null + } + exports.findClosestEndPoint = function (p){ + var point + for (var i = 0; i < this.shapes.length; i++) { + point = this.shapes[i].hasEndPointNear(p) + if (point) return { point: point, shape: this.shapes[i] } + } + return null + } + exports.findClosestSegment = function (p){ + var segment = null, closest_segment = null + for (var i = 0; i < this.shapes.length; i++) { + segment = this.shapes[i].hasSegmentNear(p, 10) + if (segment && (! closest_segment || segment.distance < closest_segment.distance)) { + closest_segment = segment + closest_segment.shape = this.shapes[i] + } + } + return closest_segment + } + exports.forEach = function(fn){ + this.shapes.forEach(fn) + } + exports.getAllShapeSegments = function(){ + return this.shapes.map(function(shape){ return shape.getSegments() }) + } + exports.getAllSegments = function(){ + var segments = [] + this.shapes.forEach(function(shape){ + segments = segments.concat( shape.getSegments() ) + }) + return segments + } + exports.draw = function(ctx, fillStyle, strokeStyle) { + this.shapes.forEach(function(shape){ + shape.draw(ctx, fillStyle, strokeStyle) + }) + } + exports.serialize = function(){ + return this.shapes.map(function(shape){ + return shape.serialize() + }) + } + exports.deserialize = function(data){ + data && data.forEach(function(shape_data){ + var line + switch (shape_data.type) { + case 'ortho': + line = new OrthoPolyline() + break + default: + line = new Polyline() + break + } + line.deserialize(shape_data) + shapes.add(line) + }.bind(this)) + } + exports.build = function(){ + this.shapes.forEach(function(shape){ + shape.build() + }) + } + return exports +}) + +if (! ('window' in this) ) { + shapes = new ShapeList + module.exports = shapes +} diff --git a/public/assets/javascripts/rectangles/models/floor.js b/public/assets/javascripts/rectangles/models/floor.js new file mode 100644 index 0000000..63eebcc --- /dev/null +++ b/public/assets/javascripts/rectangles/models/floor.js @@ -0,0 +1,218 @@ +(function(){ + + var vec2, Rect, sort + if ('window' in this) { + vec2 = window.vec2 + Rect = window.Rect + sort = window.sort + } + else { + vec2 = require('./vec2') + Rect = require('./rect') + UidGenerator = require('../util/uid') + } + + var Floor = function(opt){ + this.id = opt.id + this.side = opt.side + this.ceiling = opt.side & CEILING + this.mx = opt.mx + } + + Floor.prototype.serialize = function(){ + return { + id: this.id, + background: this.background, + } + } + + Floor.prototype.deserialize = function(data){ + this.wallpaper( data.background ) + } + + Floor.prototype.bind = function(){ + var base = this + base.$els = $( this.mx.map(function(mx){ return mx.el }) ) + + this.mx.forEach(function(mx, index){ + $(mx.el).bind({ + contextmenu: function(e){ + if (! (e.ctrlKey || e.metaKey || e.shiftKey) ) { + e.preventDefault() + } + if (Scenery.nextMedia) { + e.preventDefault() + Scenery.nextMedia = null + app.tube('cancel-scenery') + } + else if (Scenery.nextWallpaper) { + e.preventDefault() + Scenery.nextWallpaper = null + app.tube('cancel-wallpaper') + } + }, + + mousedown: function(e){ + + // right-click + if (e.which == 3) { + if (Scenery.nextMedia) { + e.preventDefault() + Scenery.nextMedia = null + app.tube('cancel-scenery') + } + else if (Scenery.nextWallpaper) { + e.preventDefault() + Scenery.nextWallpaper = null + app.tube('cancel-wallpaper') + } + return + } + + + var offset = offsetFromPoint(e, mx.el) + if (! offset) { return } + + var x = mx.x + mx.width * (offset.left-0.5) + var z = mx.z + mx.height * (0.5-offset.top) + + if (Scenery.nextMedia) { + e.preventDefault() + + var sculpture = Sculpture.addNext({ + position: { x: x, y: 0, z: z }, + }) + + // scenery was not placed + if (! sculpture) { + e.stopPropagation() + return + } + + app.controller.toolbar.resetPermissions() + Sculpture.resize.show(sculpture) + Sculpture.hovering = true + + // app.controller.pick(sculpture) + + UndoStack.push({ + type: 'create-sculpture', + undo: { id: sculpture.id }, + redo: sculpture.serialize(), + }) + + // TODO: watch individual sculpture object here + Minotaur.watch( app.router.editorView.settings ) + } + else if (Scenery.nextWallpaper) { + var oldState = base.serialize() + base.wallpaper(Scenery.nextWallpaper) + // Scenery.nextWallpaper = null + + UndoStack.push({ + type: 'update-wallpaper', + undo: oldState, + redo: base.serialize(), + }) + + // TODO: watch individual scenery object here + Minotaur.watch( app.router.editorView.settings ) + + app.controller.pickWall(base, null) + } + else { + app.controller.pickWall(base, null) + } + } + }) + }) + + // flip the mx order + var shouldFlip = this.side & (CEILING) + if (! shouldFlip) { + this.mx.reverse() + } + } + + Floor.prototype.color = function(color){ + this.$els.css("background-color", color) + } + + Floor.prototype.wallpaper = function(background){ + if (! background) { + background = { src: "none" } + } + else if (typeof background == "string") { + background = { src: background } + } + else if (! background.src) { + background = { src: "none" } + } + background.x = background.x || 0 + background.y = background.y || 0 + background.scale = background.scale || 1 + + this.background = background + this.background.src = this.background.src.replace(/url\(\"?\'?/,"").replace(/\"?\'?\)/,"") + + if (this.background.src == "none") { + this.wallpaperLoad(this.background.src) + return + } + + var img = new Image () + img.onload = function(){ + this.backgroundImage = img + this.wallpaperLoad(this.background.src) + this.wallpaperPosition(background) + }.bind(this) + img.src = this.background.src + img.complete && img.onload() + } + + Floor.prototype.wallpaperLoad = function(url){ + if (url !== "none") { + url = "url(" + url + ")" + } + this.mx.forEach(function(mx){ + mx.el.style.backgroundImage = url + }) + } + + Floor.prototype.wallpaperPosition = function(background){ + if (this.background.src == "none") { return } + + this.background.x = background.x || this.background.x || 0 + this.background.y = background.y || this.background.y || 0 + this.background.scale = background.scale || this.background.scale || 1 + + var mx, dx, dy + var w = Math.round( this.backgroundImage.naturalWidth * this.background.scale ) + var h = Math.round( this.backgroundImage.naturalHeight * this.background.scale ) + + this.mx.forEach(function(mx, i){ + + var region = mx.rect + + if (this.ceiling) { + dx = Math.round( this.background.x - region.x.a ) + dy = Math.round( this.background.y - region.y.a ) + } + else { + dx = Math.round( this.background.x - region.x.a ) + dy = Math.round( this.background.y + region.y.b ) + } + + mx.el.style.backgroundPosition = dx + 'px ' + dy + 'px' + mx.el.style.backgroundSize = w + 'px ' + h + 'px' + }.bind(this)) + bbb = this + } + + if ('window' in this) { + window.Floor = Floor + } + else { + module.exports = Floor + } +})() diff --git a/public/assets/javascripts/rectangles/models/mat4.js b/public/assets/javascripts/rectangles/models/mat4.js deleted file mode 100644 index b061199..0000000 --- a/public/assets/javascripts/rectangles/models/mat4.js +++ /dev/null @@ -1,78 +0,0 @@ -function mat4(e){ - this.elements = [ 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1 ] - return this -} -mat4.prototype.set = function (a) { - var els = this.elements - a.forEach(function(n,i){ els[i] = n }) - return this -} -mat4.prototype.clone = function(){ - return (new mat4).set(this.els) -} -mat4.prototype.identity = function () { - this.set([ - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1 - ]); - return this; -} -mat4.prototype.getInverse = function (m, throwOnInvertible) { - - // based on http://www.euclideanspace.com/maths/algebra/matrix/functions/inverse/fourD/index.htm - var te = this.elements; - var me = m.elements; - - var n11 = me[0], n12 = me[4], n13 = me[8], n14 = me[12]; - var n21 = me[1], n22 = me[5], n23 = me[9], n24 = me[13]; - var n31 = me[2], n32 = me[6], n33 = me[10], n34 = me[14]; - var n41 = me[3], n42 = me[7], n43 = me[11], n44 = me[15]; - - te[0] = n23*n34*n42 - n24*n33*n42 + n24*n32*n43 - n22*n34*n43 - n23*n32*n44 + n22*n33*n44; - te[4] = n14*n33*n42 - n13*n34*n42 - n14*n32*n43 + n12*n34*n43 + n13*n32*n44 - n12*n33*n44; - te[8] = n13*n24*n42 - n14*n23*n42 + n14*n22*n43 - n12*n24*n43 - n13*n22*n44 + n12*n23*n44; - te[12] = n14*n23*n32 - n13*n24*n32 - n14*n22*n33 + n12*n24*n33 + n13*n22*n34 - n12*n23*n34; - te[1] = n24*n33*n41 - n23*n34*n41 - n24*n31*n43 + n21*n34*n43 + n23*n31*n44 - n21*n33*n44; - te[5] = n13*n34*n41 - n14*n33*n41 + n14*n31*n43 - n11*n34*n43 - n13*n31*n44 + n11*n33*n44; - te[9] = n14*n23*n41 - n13*n24*n41 - n14*n21*n43 + n11*n24*n43 + n13*n21*n44 - n11*n23*n44; - te[13] = n13*n24*n31 - n14*n23*n31 + n14*n21*n33 - n11*n24*n33 - n13*n21*n34 + n11*n23*n34; - te[2] = n22*n34*n41 - n24*n32*n41 + n24*n31*n42 - n21*n34*n42 - n22*n31*n44 + n21*n32*n44; - te[6] = n14*n32*n41 - n12*n34*n41 - n14*n31*n42 + n11*n34*n42 + n12*n31*n44 - n11*n32*n44; - te[10] = n12*n24*n41 - n14*n22*n41 + n14*n21*n42 - n11*n24*n42 - n12*n21*n44 + n11*n22*n44; - te[14] = n14*n22*n31 - n12*n24*n31 - n14*n21*n32 + n11*n24*n32 + n12*n21*n34 - n11*n22*n34; - te[3] = n23*n32*n41 - n22*n33*n41 - n23*n31*n42 + n21*n33*n42 + n22*n31*n43 - n21*n32*n43; - te[7] = n12*n33*n41 - n13*n32*n41 + n13*n31*n42 - n11*n33*n42 - n12*n31*n43 + n11*n32*n43; - te[11] = n13*n22*n41 - n12*n23*n41 - n13*n21*n42 + n11*n23*n42 + n12*n21*n43 - n11*n22*n43; - te[15] = n12*n23*n31 - n13*n22*n31 + n13*n21*n32 - n11*n23*n32 - n12*n21*n33 + n11*n22*n33; - - var det = n11 * te[ 0 ] + n21 * te[ 4 ] + n31 * te[ 8 ] + n41 * te[ 12 ]; - - if ( det == 0 ) { - var msg = "Matrix4.getInverse(): can't invert matrix, determinant is 0"; - - if ( throwOnInvertible || false ) { - throw new Error( msg ) - } - else { - console.warn( msg ) - } - this.identity(); - return this - } - this.multiplyScalar( 1 / det ); - return this -} -mat4.prototype.multiplyScalar = function (n) { - var els = this.elements - els[0] *= n; els[4] *= n; els[8] *= n; els[12] *= n - els[1] *= n; els[5] *= n; els[9] *= n; els[13] *= n - els[2] *= n; els[6] *= n; els[10] *= n; els[14] *= n - els[3] *= n; els[7] *= n; els[11] *= n; els[15] *= n - return this -} - diff --git a/public/assets/javascripts/rectangles/models/rect.js b/public/assets/javascripts/rectangles/models/rect.js index a08176a..4f73bec 100644 --- a/public/assets/javascripts/rectangles/models/rect.js +++ b/public/assets/javascripts/rectangles/models/rect.js @@ -1,4 +1,3 @@ - (function(){ var vec2 if ('window' in this) { @@ -8,17 +7,17 @@ vec2 = require('./vec2') FRONT = 0x1, BACK = 0x2, LEFT = 0x4, RIGHT = 0x8, FLOOR = 0x10, CEILING = 0x20 TOP = CEILING, BOTTOM = FLOOR - function sidesToString(sides){ - var s = "" - if (sides & FRONT) s += "front " - if (sides & BACK) s += "back " - if (sides & LEFT) s += "left " - if (sides & RIGHT) s += "right " - if (sides & TOP) s += "top " - if (sides & BOTTOM) s += "bottom " - return s - } } + function sidesToString(sides){ + var s = "" + if (sides & FRONT) s += "front " + if (sides & BACK) s += "back " + if (sides & LEFT) s += "left " + if (sides & RIGHT) s += "right " + if (sides & TOP) s += "top " + if (sides & BOTTOM) s += "bottom " + return s + } var Rect = function (x0,y0,x1,y1){ if (x0 instanceof vec2) { @@ -39,6 +38,12 @@ Rect.prototype.clone = function(){ return new Rect( this.x.clone(), this.y.clone() ) } + Rect.prototype.x_component = function(){ + return new vec2( this.x.a, this.y.a ) + } + Rect.prototype.y_component = function(){ + return new vec2( this.x.b, this.y.b ) + } Rect.prototype.assign = function(r) { this.x.assign(r.x) this.y.assign(r.y) @@ -56,6 +61,12 @@ Rect.prototype.maxDimension = function(){ return abs(this.width) > abs(this.height) ? this.width : this.height } + Rect.prototype.isVertical = function(){ + return this.x.isPoint() + } + Rect.prototype.isHorizontal = function(){ + return this.y.isPoint() + } Rect.prototype.mul = function(n){ this.x.mul(n) @@ -141,11 +152,40 @@ Rect.prototype.width = function(){ return this.x.length() } Rect.prototype.height = function(){ return this.y.length() } Rect.prototype.delta = function(){ return new vec2( this.x.magnitude(), this.y.magnitude() ) } + Rect.prototype.expand = function(rect){ + this.x.a = Math.min( this.x.a, rect.x.a ) + this.x.b = Math.max( this.x.b, rect.x.b ) + this.y.a = Math.min( this.y.a, rect.y.a ) + this.y.b = Math.max( this.y.b, rect.y.b ) + return this + } + Rect.prototype.square = function(){ + var width = this.x.length() + var height = this.y.length() + var diff + if (width < height) { + diff = (height - width) / 2 + this.x.a -= diff + this.x.b += diff + } + else { + diff = (width - height) / 2 + this.y.a -= diff + this.y.b += diff + } + return this + } Rect.prototype.toString = function(){ var sides = sidesToString(this.sides) var s = "[" + this.x.toString() + " " + this.y.toString() + "] " + sides return s } + Rect.prototype.exactString = function(){ + var sides = sidesToString(this.sides) + var s = "[" + this.x.exactString() + " " + this.y.exactString() + "] " + sides + return s + } + Rect.prototype.serialize = function(){ return { x: this.x.serialize(), y: this.y.serialize() } } diff --git a/public/assets/javascripts/rectangles/models/room.js b/public/assets/javascripts/rectangles/models/room.js index 0f09325..1abe2ba 100644 --- a/public/assets/javascripts/rectangles/models/room.js +++ b/public/assets/javascripts/rectangles/models/room.js @@ -31,10 +31,14 @@ var Room = function(opt){ this.id = opt.id || Rooms.uid("room_") this.rect = opt.rect - this.regions = [] + this.regions = opt.regions || [] this.height = opt.height || 200 this.focused = false + + this.mx_walls = [] + this.mx_floor = [] + this.mx_ceiling = [] } Room.prototype.copy = function(){ @@ -122,8 +126,10 @@ return collision } - Room.prototype.collidesDisc = function(x,y,radius){ + Room.prototype.collidesDisc = function(src, dest, radius){ + var x = dest.x, y = dest.z var collision = 0, wall_collision, contains_x, contains_y + this.regions.forEach(function(r){ if (! r.sides) return @@ -152,25 +158,10 @@ if (contains_y) { collision |= wall_collision & LEFT_RIGHT } -// if (bitcount(wall_collision) > 1) { -// collision |= wall_collision -// } }) return collision } - - Room.prototype.setFloorColor = function(rgbColor) { - this.mx_floor.map(function(mx){ - mx.el.style.backgroundColor = rgbColor - }) - } - - Room.prototype.setCeilingColor = function(rgbColor) { - this.mx_ceiling.map(function(mx){ - mx.el.style.backgroundColor = rgbColor - }) - } - + if ('window' in this) { window.Room = Room } diff --git a/public/assets/javascripts/rectangles/models/surface.js b/public/assets/javascripts/rectangles/models/surface.js index 53977c8..f031665 100644 --- a/public/assets/javascripts/rectangles/models/surface.js +++ b/public/assets/javascripts/rectangles/models/surface.js @@ -12,6 +12,7 @@ var Surface = function (face){ this.bounds = new Rect (new vec2(0, 0), new vec2(0, 0)) + this.vec = new Rect (new vec2(0, 0), new vec2(0, 0)) this.faces = [] if (face) { this.add(face) @@ -36,7 +37,7 @@ Surface.prototype.fits = function(v){ var faces = this.faces var scratch - if (this.bounds.x.b < v.a || this.bounds.y.b < v.b) { + if (this.bounds.width() < v.a || this.bounds.height() < v.b) { return null } for (var i = 0; i < faces.length; i++) { @@ -46,7 +47,7 @@ } scratch = new Rect (0,0,0,0) for (var i = 0; i < faces.length; i++) { - if (faces[i].y.length() < v.b) { + if (faces[i].height() < v.b) { continue } scratch.x.a = faces[i].x.a @@ -108,7 +109,7 @@ this.clamp_delta( this.bounds, dimension, position, delta ) var new_delta = delta.clone() - if (this.clamp_delta(old_bounds, dimension, position, new_delta).eq(delta)) { + if (old_bounds && this.clamp_delta(old_bounds, dimension, position, new_delta).eq(delta)) { return old_bounds } @@ -154,6 +155,21 @@ } return -1 } + Surface.prototype.face_for_x = function(x, min_i){ + min_i = min_i || 0 + for (var i = min_i; i < this.faces.length; i++) { + if (this.faces[i].x.contains(x)) { + return this.faces[i] + } + } + if (x < this.faces[0].x.a) { + return this.faces[0] + } + else { + return this.faces[this.faces.length-1] + } + return null + } Surface.prototype.bounds_at_index_with_dimensions = function(index, dimensions){ var faces = this.faces diff --git a/public/assets/javascripts/rectangles/models/vec2.js b/public/assets/javascripts/rectangles/models/vec2.js index 0040435..8942d92 100644 --- a/public/assets/javascripts/rectangles/models/vec2.js +++ b/public/assets/javascripts/rectangles/models/vec2.js @@ -37,9 +37,15 @@ vec2.prototype.midpoint = function(){ return lerp(0.5, this.a, this.b) } + vec2.prototype.lerp = function(n){ + return lerp(n, this.a, this.b) + } vec2.prototype.eq = function(v){ return this.a == v.a && this.b == v.b } + vec2.prototype.isPoint = function(){ + return this.a == this.b + } vec2.prototype.add = function(n){ this.a += n this.b += n @@ -60,9 +66,28 @@ this.b /= n return this } + vec2.prototype.addVec = function(v){ + this.a += v.a + this.b += v.b + return this + } + vec2.prototype.subVec = function(v){ + this.a -= v.a + this.b -= v.b + return this + } vec2.prototype.zero = function(){ this.a = this.b = 0 } + vec2.prototype.round = function(){ + this.a = Math.round(this.a) + this.b = Math.round(this.b) + } + vec2.prototype.distanceTo = function(v){ + var va = (this.a - v.a) + var vb = (this.b - v.b) + return Math.sqrt( va*va + vb*vb ) + } vec2.prototype.setPosition = function(n){ var len = this.length() this.a = n @@ -86,6 +111,12 @@ vec2.prototype.containsDisc = function(n,r){ return this.a <= n-r && n+r <= this.b } + vec2.prototype.containsVec = function(v){ + return this.a <= v.a && v.b <= this.b + } + vec2.prototype.containsCenterVec = function(v){ + return this.a < v.a && v.b < this.b + } vec2.prototype.clamp = function(n){ return clamp(n, this.a, this.b) } @@ -183,10 +214,17 @@ } vec2.prototype.toString = function(){ - return "[" + ~~this.a + " " + ~~this.b + "]" + return "[" + Math.round(this.a) + " " + Math.round(this.b) + "]" + } + vec2.prototype.exactString = function(){ + return "[" + this.a + " " + this.b + "]" } vec2.prototype.serialize = function(){ - return [ ~~this.a, ~~this.b ] + return [ Math.round(this.a), Math.round(this.b) ] + } + vec2.prototype.deserialize = function(data){ + this.a = data[0] + this.b = data[1] } vec2.prototype.quantize = function(n){ n = n || 10 diff --git a/public/assets/javascripts/rectangles/models/vec3.js b/public/assets/javascripts/rectangles/models/vec3.js index 4e00b0c..b3825a9 100644 --- a/public/assets/javascripts/rectangles/models/vec3.js +++ b/public/assets/javascripts/rectangles/models/vec3.js @@ -15,6 +15,9 @@ vec3.prototype.sub = function(v){ this.c -= v.c return this } +vec3.prototype.clone = function(){ + return new vec3(this.a, this.b, this.c) +} // input: mat4 projection matrix vec3.prototype.apply_projection = function (m) { @@ -29,3 +32,28 @@ vec3.prototype.apply_projection = function (m) { return this; } + +vec3.prototype.serialize = function(){ + return [ round(this.a), round(this.b), round(this.c) ] +} +vec3.prototype.deserialize = function(data){ + this.a = data[0] + this.b = data[1] + this.c = data[2] || data[0] + return this +} +vec3.prototype.clone = function(){ + return new vec3(this.a, this.b, this.c) +} +vec3.prototype.assign = function(v){ + this.a = v.a + this.b = v.b + this.c = v.c + return this +} +vec3.prototype.mul = function(n) { + this.a *= n + this.b *= n + this.c *= n + return this +}
\ No newline at end of file diff --git a/public/assets/javascripts/rectangles/models/wall.js b/public/assets/javascripts/rectangles/models/wall.js index 1a3ef7c..cf3cea8 100644 --- a/public/assets/javascripts/rectangles/models/wall.js +++ b/public/assets/javascripts/rectangles/models/wall.js @@ -10,21 +10,55 @@ vec2 = require('./vec2') Rect = require('./rect') UidGenerator = require('../util/uid') + wall_rotation = {} + wall_rotation[FRONT] = PI + wall_rotation[BACK] = 0 + wall_rotation[LEFT] = HALF_PI + wall_rotation[RIGHT] = -HALF_PI } var Wall = function(opt){ - this.id = [ opt.side, opt.edge, opt.vec.a ].join("_") + this.id = [ opt.side|0, opt.edge|0, opt.vec.a|0 ].join("_") this.vec = opt.vec this.edge = opt.edge this.side = opt.side this.surface = opt.surface + this.rotationY = ('rotationY' in opt) ? opt.rotationY : wall_rotation[opt.side] this.mx = opt.mx - this.background = "" + this.background = { src: "none" } } Wall.prototype.toString = function(){ return this.vec.toString() } + Wall.prototype.get_points = function(wall_vec){ + wall_vec = wall_vec || new Rect () + if (this.side & LEFT) { + wall_vec.x.a = this.edge + wall_vec.x.b = this.vec.b + wall_vec.y.a = this.edge + wall_vec.y.b = this.vec.a + } + else if (this.side & RIGHT) { + wall_vec.x.a = this.edge + wall_vec.x.b = this.vec.a + wall_vec.y.a = this.edge + wall_vec.y.b = this.vec.b + } + else if (this.side & FRONT) { + wall_vec.x.a = this.vec.a + wall_vec.x.b = this.edge + wall_vec.y.a = this.vec.b + wall_vec.y.b = this.edge + } + else if (this.side & BACK) { + wall_vec.x.a = this.vec.b + wall_vec.x.b = this.edge + wall_vec.y.a = this.vec.a + wall_vec.y.b = this.edge + } + return wall_vec + } Wall.prototype.reset = function(){ } @@ -50,13 +84,66 @@ index: index, }) }, +/* mousemove: function(e){ + var offset = offsetFromPoint(e, mx.el) + if (offset) { + var pos = base.mxOffsetToPosition( offset, index ) + + var mx_pos = base.positionToMx(pos, new vec2(5,5)) + var mx_dot = new MX.Object3D + mx_dot.move(mx_pos) + mx_dot.width = 5 + mx_dot.height = 5 + mx_dot.rotationY = base.rotationY + mx_dot.el.style.backgroundColor = "red" + scene.add(mx_dot) + } }, +*/ + contextmenu: function(e){ + if (! (e.ctrlKey || e.metaKey || e.shiftKey) ) { + e.preventDefault() + } + if (Scenery.nextMedia) { + e.preventDefault() + Scenery.nextMedia = null + app.tube('cancel-scenery') + } + else if (Scenery.nextWallpaper) { + e.preventDefault() + Scenery.nextWallpaper = null + app.tube('cancel-wallpaper') + } + }, + mousedown: function(e){ + + // right-click + if (e.which == 3) { + if (Scenery.nextMedia) { + e.preventDefault() + Scenery.nextMedia = null + app.tube('cancel-scenery') + } + else if (Scenery.nextWallpaper) { + e.preventDefault() + Scenery.nextWallpaper = null + app.tube('cancel-wallpaper') + } + return + } + + var offset = offsetFromPoint(e, mx.el) + if (! offset) { return } + + var pos = base.mxOffsetToPosition( offset, index ) + if (Scenery.nextMedia) { var scenery = Scenery.addNextToWall({ wall: base, - index: index + index: index, + position: pos, }) // scenery was not placed @@ -64,20 +151,26 @@ e.stopPropagation() return } - + + app.controller.toolbar.resetPermissions() + Scenery.resize.show(scenery) + Scenery.hovering = true + + app.controller.pick(scenery) + UndoStack.push({ type: 'create-scenery', undo: { id: scenery.id }, redo: scenery.serialize(), }) - + // TODO: watch individual scenery object here Minotaur.watch( app.router.editorView.settings ) } else if (Scenery.nextWallpaper) { var oldState = base.serialize() base.wallpaper(Scenery.nextWallpaper) - Scenery.nextWallpaper = null + // Scenery.nextWallpaper = null UndoStack.push({ type: 'update-wallpaper', @@ -87,11 +180,13 @@ // TODO: watch individual scenery object here Minotaur.watch( app.router.editorView.settings ) + + app.controller.pickWall(base, pos) } else { - app.controller.hideExtras() + app.controller.pickWall(base, pos) } - } + }, }) }) @@ -100,12 +195,10 @@ if (! shouldFlip) { this.mx.reverse() } - - // this.outline(wallColor, outlineColor) } Wall.prototype.serialize = function(){ - return { + return { id: this.id, background: this.background, } @@ -149,7 +242,7 @@ x: x, y: position.b + dimension.b / 2, z: z, - rotationY: wall_rotation[ this.side ], + rotationY: this.rotationY, } } Wall.prototype.mxToPosition = function(mx, dimension) { @@ -172,30 +265,104 @@ } return position } - + Wall.prototype.mxOffsetToPosition = function( offset, index ) { + var face = this.surface.faces[index] + var shouldFlip = this.side & (RIGHT | FRONT) + var position = new vec2(0,0) + position.a = face.x.lerp(shouldFlip ? 1-offset.left : offset.left) + position.b = face.y.lerp(1-offset.top) + position.round() + return position + } + Wall.prototype.color = function(color){ this.$walls.css("background-color", color) } - Wall.prototype.wallpaper = function(background){ - var useX = this.side & FRONT_BACK - var shouldFlip = this.side & (LEFT | BACK) - - this.background = background || "none" + Wall.prototype.wallpaper = function(background, img){ + if (! background) { + background = { src: "none" } + } + else if (typeof background == "string") { + background = { src: background } + } + else if (! background.src) { + background = { src: "none" } + } + background.x = background.x || 0 + background.y = background.y || 0 + background.scale = background.scale || 1 + + this.background = background + this.background.src = this.background.src.replace(/url\(\"?\'?/,"").replace(/\"?\'?\)/,"") + + if (this.background.src == "none") { + this.wallpaperLoad(this.background.src) + return + } + img = img || new Image () + img.onload = function(){ + this.backgroundImage = img + this.wallpaperLoad(this.background.src) + this.wallpaperPosition(background) + }.bind(this) + img.src = this.background.src + img.complete && img.onload() + } + + Wall.prototype.wallpaperLoad = function(url){ + if (url !== "none") { + url = "url(" + url + ")" + } this.mx.forEach(function(mx){ - var partitionOffset = useX ? mx.x : mx.z - if (shouldFlip) partitionOffset *= -1 - partitionOffset += mx.width/2 - var floorOffset = mx.y + mx.height/2 - - mx.el.style.backgroundImage = background - mx.el.style.backgroundPosition = (~~partitionOffset) + "px " + (~~floorOffset) + "px" + if (mx.el) mx.el.style.backgroundImage = url }) } + + Wall.prototype.wallpaperPosition = function(background){ + if (this.background.src == "none") { return } + + this.background.x = background.x || this.background.x || 0 + this.background.y = background.y || this.background.y || 0 + this.background.scale = background.scale || this.background.scale || 1 + + var mx, dx, dy + var w = Math.round( this.backgroundImage.naturalWidth * this.background.scale ) + var h = Math.round( this.backgroundImage.naturalHeight * this.background.scale ) + + this.surface.faces.forEach(function(face, i){ + // this.mx[i].el.innerHTML = sidesToString(this.side) + + switch (this.side) { + case LEFT: + mx = this.mx[this.mx.length-1-i] + dx = Math.round( this.background.x + face.x.a ) + dy = Math.round( this.background.y + face.y.b ) + break + case RIGHT: + mx = this.mx[this.mx.length-1-i] + dx = Math.round( this.background.x + face.x.b ) + dy = Math.round( this.background.y + face.y.b ) + break + case FRONT: + mx = this.mx[this.mx.length-1-i] + dx = Math.round( this.background.x + face.x.b ) + dy = Math.round( this.background.y + face.y.b ) + break + case BACK: + mx = this.mx[i] + dx = Math.round( this.background.x - face.x.a ) + dy = Math.round( this.background.y + face.y.b ) + break + } + + mx.el.style.backgroundPosition = dx + 'px ' + dy + 'px' + mx.el.style.backgroundSize = w + 'px ' + h + 'px' + }.bind(this)) + } Wall.prototype.outline = function(wallColor, outlineColor){ - var useX = this.side & FRONT_BACK var mx = this.mx var len = this.mx.length diff --git a/public/assets/javascripts/rectangles/util/constants.js b/public/assets/javascripts/rectangles/util/constants.js index 4c6b3cc..522689b 100644 --- a/public/assets/javascripts/rectangles/util/constants.js +++ b/public/assets/javascripts/rectangles/util/constants.js @@ -1,5 +1,6 @@ var FRONT = 0x1, BACK = 0x2, LEFT = 0x4, RIGHT = 0x8, FLOOR = 0x10, CEILING = 0x20 - FRONT_BACK = FRONT | BACK, LEFT_RIGHT = LEFT | RIGHT, FLOOR_CEILING = FLOOR | CEILING +var FRONT_BACK = FRONT | BACK, LEFT_RIGHT = LEFT | RIGHT, FLOOR_CEILING = FLOOR | CEILING +var WALL_SIDES = FRONT | BACK | LEFT | RIGHT var TOP = CEILING, BOTTOM = FLOOR, TOP_LEFT = TOP | LEFT, @@ -19,10 +20,13 @@ var height_min = 200, side_min = 10, side_max = 5000, resize_margin = 8, - cursor_amp = 1.5 + cursor_amp = 1.5, + DEFAULT_PICTURE_WIDTH = 350, + MAP_GRID_SIZE = 36 // 10 feet var painting_distance_from_wall = 10, - dot_distance_from_picture = 3 + dot_distance_from_picture = 3, + sculpture_distance_from_floor = 3 var dot_hide_delay = 50, // ms dot_side = 20 diff --git a/public/assets/javascripts/rectangles/util/coords.js b/public/assets/javascripts/rectangles/util/coords.js new file mode 100644 index 0000000..ff56199 --- /dev/null +++ b/public/assets/javascripts/rectangles/util/coords.js @@ -0,0 +1,33 @@ +function offsetFromPoint(event, element) { + function a(width) { + var l = 0, r = 200; + while (r - l > 0.0001) { + var mid = (r + l) / 2; + var a = document.createElement('div'); + a.style.cssText = 'position: absolute;left:0;top:0;background: red;z-index: 1000;'; + a.style[width ? 'width' : 'height'] = mid.toFixed(3) + '%'; + a.style[width ? 'height' : 'width'] = '100%'; + element.appendChild(a); + var x = document.elementFromPoint(event.clientX, event.clientY); + element.removeChild(a); + if (x === a) { + r = mid; + } else { + if (r === 200) { + return null; + } + l = mid; + } + } + return mid; + } + var l = a(1), + t = a(0); + return l && t ? { + left: l / 100, + top: t / 100, + toString: function () { + return 'left: ' + l + '%, top: ' + t + '%'; + } + } : null; +}
\ No newline at end of file diff --git a/public/assets/javascripts/rectangles/util/measurement.js b/public/assets/javascripts/rectangles/util/measurement.js index d6a0b35..6346eac 100644 --- a/public/assets/javascripts/rectangles/util/measurement.js +++ b/public/assets/javascripts/rectangles/util/measurement.js @@ -28,6 +28,10 @@ function measurementToString( n ) { case 'ft': ft = floor(n / 36) inch = abs(round((n % 36) / 3)) + if (inch == 12) { + inch = 0 + ft += 1 + } s = ft + "'" if (inch > 0) { s += " " + inch + '"' diff --git a/public/assets/javascripts/rectangles/util/minotaur.js b/public/assets/javascripts/rectangles/util/minotaur.js index 0fcc766..8b1abfe 100644 --- a/public/assets/javascripts/rectangles/util/minotaur.js +++ b/public/assets/javascripts/rectangles/util/minotaur.js @@ -4,7 +4,7 @@ var base = this base.$el = $("#minotaur") base.timeout = null - base.delay = 1000 + base.delay = 2500 base.objects = {} base.init = function () { @@ -38,7 +38,7 @@ for (var id in base.objects[type]) { var obj = base.objects[type][id] if (obj) { - obj.save(null, function(){ base.hide() }, function(){}) + obj.save(null, function(){ base.hide() }, function(){ base.hide() }) } delete base.objects[type][id] saving = true @@ -53,9 +53,7 @@ } base.hide = function () { - setTimeout(function(){ - base.$el.removeClass() - }, 500) + base.$el.removeClass('saving') } base.init(); diff --git a/public/assets/javascripts/rectangles/util/mouse.js b/public/assets/javascripts/rectangles/util/mouse.js index cb36038..6d9862c 100644 --- a/public/assets/javascripts/rectangles/util/mouse.js +++ b/public/assets/javascripts/rectangles/util/mouse.js @@ -54,10 +54,20 @@ function mouse (opt) { opt.up && base.tube.on("up", opt.up) opt.rightclick && base.tube.on("rightclick", opt.rightclick) - var offset = (opt.use_offset && opt.el) ? opt.el.getBoundingClientRect() : null + var offset; base.init = function (){ base.bind() + base.set_offset() + } + + base.set_offset = function(){ + if (opt.use_offset && opt.el) { + offset = opt.el.getBoundingClientRect() + } + else { + offset = null + } } base.on = function(){ @@ -73,9 +83,20 @@ function mouse (opt) { opt.el.addEventListener("mousedown", base.mousedown) opt.el.addEventListener("contextmenu", base.contextmenu) } + if (opt.use_offset) { + window.addEventListener("resize", base.set_offset) + } window.addEventListener("mousemove", base.mousemove) window.addEventListener("mouseup", base.mouseup) } + base.unbind = function(){ + if (opt.el) { + opt.el.removeEventListener("mousedown", base.mousedown) + opt.el.removeEventListener("contextmenu", base.contextmenu) + } + window.removeEventListener("mousemove", base.mousemove) + window.removeEventListener("mouseup", base.mouseup) + } base.bind_el = function(el){ el.addEventListener("mousedown", base.mousedown) @@ -126,7 +147,7 @@ function mouse (opt) { } var x = pos.a, y = pos.b - + if (base.down) { base.cursor.x.b = x base.cursor.y.b = y diff --git a/public/assets/javascripts/rectangles/util/permissions.js b/public/assets/javascripts/rectangles/util/permissions.js index 1b5a1b5..9e3ef4d 100644 --- a/public/assets/javascripts/rectangles/util/permissions.js +++ b/public/assets/javascripts/rectangles/util/permissions.js @@ -31,7 +31,7 @@ Permissions.prototype.add = function (key) { Permissions.prototype.remove = function (key) { var base = this - base[key] = true + base[key] = false } Permissions.prototype.clear = function () { diff --git a/public/assets/javascripts/rectangles/util/undostack.js b/public/assets/javascripts/rectangles/util/undostack.js index 959e3d1..040a4eb 100644 --- a/public/assets/javascripts/rectangles/util/undostack.js +++ b/public/assets/javascripts/rectangles/util/undostack.js @@ -10,6 +10,7 @@ this.pointer++ this.stack[this.pointer] = action this.purge() + this.debug && console.log("push", action.type) } UndoStack.prototype.purge = function(){ if (this.stack.length-1 == this.pointer) return diff --git a/public/assets/javascripts/rectangles/util/wheel.js b/public/assets/javascripts/rectangles/util/wheel.js index 6836772..4155a70 100644 --- a/public/assets/javascripts/rectangles/util/wheel.js +++ b/public/assets/javascripts/rectangles/util/wheel.js @@ -3,8 +3,8 @@ base.wheel = new wheel({ el: document.querySelector("#map"), - update: function(e, val, delta){ - // do something with val + update: function(e, delta){ + // do something with delta }, }) @@ -13,7 +13,7 @@ function wheel (opt) { opt = defaults(opt, { el: document, - fn: function(e, val, delta){}, + update: function(e, delta){}, propagate: false, locked: false, reversible: true, @@ -21,37 +21,46 @@ function wheel (opt) { val: 0, }) - opt.el.addEventListener('mousewheel', onMouseWheel, false); + opt.el.addEventListener('wheel', onMouseWheel, false); + // opt.el.addEventListener('mousewheel', onMouseWheel, false); opt.el.addEventListener('DOMMouseScroll', onMouseWheel, false); function onMouseWheel (e) { if (opt.locked) { return } + if (! opt.propagate) { e.stopPropagation() e.preventDefault() } - var delta = 0; + var deltaX = 0, deltaY = 0; // WebKit - if ( event.wheelDeltaY ) { - delta -= event.wheelDeltaY * opt.ratio + if ( event.deltaY ) { + deltaY -= event.deltaY * opt.ratio + deltaX -= event.deltaX * opt.ratio + } + else if ( event.wheelDeltaY ) { + deltaY -= event.wheelDeltaY * opt.ratio + deltaX -= event.wheelDeltaX * opt.ratio } // Opera / Explorer 9 else if ( event.wheelDelta ) { - delta -= event.wheelDelta * opt.ratio + deltaY -= event.wheelDelta * opt.ratio } // Firefox else if ( event.detail ) { - delta += event.detail * 2 + deltaY += event.detail * 2 } - if (! opt.reversible && delta < 0) return; + if (! opt.reversible && (deltaY < 0 && deltaX < 0)) return; - opt.val = clamp(opt.val + delta, opt.min, opt.max) + // opt.val = clamp(opt.val + delta, opt.min, opt.max) - opt.update(e, opt.val, delta) + // deltaX is also passed, but these values tend to be unusable + // try http://vvalls.com/assets/test/wheel.html with a trackpad + opt.update(e, deltaY, deltaX) } opt.lock = function(){ opt.locked = true } diff --git a/public/assets/javascripts/ui/_router.js b/public/assets/javascripts/ui/_router.js index 794079e..61b1d1b 100644 --- a/public/assets/javascripts/ui/_router.js +++ b/public/assets/javascripts/ui/_router.js @@ -9,6 +9,7 @@ var SiteRouter = Router.extend({ "click [data-role='new-project-modal']": 'newProject', "click [data-role='edit-project-modal']": 'editProject', "click [data-role='edit-profile-modal']": 'editProfile', + "click [data-role='edit-subscription-modal']": 'editSubscription', "click [data-role='new-document-modal']": 'newDocument', "click [data-role='edit-document-modal']": 'editDocument', "click [data-role='destroy-document-modal']": 'destroyDocument', @@ -17,36 +18,55 @@ var SiteRouter = Router.extend({ }, routes: { + "/": 'home', + "/home": 'home', + "/login": 'signin', + "/signin": 'signin', + "/signup": 'signup', + + "/auth/usernameTaken": 'usernameTaken', + "/auth/password": 'passwordReset', + "/auth/forgotPassword": 'passwordForgot', + + "/profile": 'profile', + "/profile/edit": 'editProfile', + "/profile/billing": 'editSubscription', + "/profile/:name": 'profile', + "/about/:name/edit": 'editDocument', + "/about/new": 'newDocument', + + "/layout": 'layoutPicker', + "/layout/:name": 'layoutEditor', + + "/blueprint": 'blueprintEditor', + "/blueprint/:name": 'blueprintEditor', + + "/project": 'projectPicker', + "/project/new": 'newProject', + "/project/blueprint/:blueprint": 'projectNewWithBlueprint', + "/project/new/:layout": 'projectNewWithLayout', + "/project/:name": 'projectViewer', + "/project/:name/edit": 'projectEditor', + "/project/:name/view": 'projectViewer', + + "/test/blueprint": 'blueprintEditor', + }, + + mobileRoutes: { "/": 'home', + "/home": 'home', "/login": 'signin', + "/signin": 'signin', "/signup": 'signup', - "/auth/usernameTaken": 'usernameTaken', "/auth/password": 'passwordReset', "/auth/forgotPassword": 'passwordForgot', "/profile": 'profile', "/profile/edit": 'editProfile', + "/profile/billing": 'editSubscription', "/profile/:name": 'profile', - "/about/:name/edit": 'editDocument', - "/about/new": 'newDocument', - - "/layout": 'layoutPicker', - "/layout/:name": 'layoutEditor', - "/project": 'projectPicker', - "/project/new": 'newProject', - "/project/new/:layout": 'projectNewWithLayout', - "/project/:name": 'projectViewer', - "/project/:name/edit": 'projectEditor', - "/project/:name/view": 'projectViewer', - - "/test/wallpaper": 'testWallpaper', - }, - - mobileRoutes: { - "/": 'home', - "/profile": 'profile', "/project/:name": 'projectViewer', }, @@ -58,6 +78,7 @@ var SiteRouter = Router.extend({ this.newProjectModal = new NewProjectModal() this.editProjectModal = new EditProjectModal() this.editProfileModal = new EditProfileModal() + this.editSubscriptionModal = new EditSubscriptionModal() this.passwordForgotModal = new PasswordForgot() this.documentModal = new DocumentModal() this.profileView = new ProfileView() @@ -65,16 +86,22 @@ var SiteRouter = Router.extend({ this.route() if (is_mobile) { - $(".topLinks").hide() - $(".share").hide() + // $(".topLinks").hide() + // $(".share").hide() + $('.projectItem').each(function(){ + this.href = this.href.replace(/\/edit$/, "") + }) } - $("body").removeClass("loading") + setTimeout(function(){ + $("body").removeClass("loading") + }, 200) }, layoutEditor: function(e, name){ app.mode.builder = true app.launch() + if (app.unsupported) return this.builderView = app.controller = new BuilderView() this.builderView.load(name) @@ -97,12 +124,29 @@ var SiteRouter = Router.extend({ window.history.pushState(null, document.title, "/project/new") this.newProjectModal.load() }, + + projectNewWithBlueprint: function(e, blueprint){ + e && e.preventDefault() + + Rooms.shapesMode = true + + app.mode.editor = true + app.launch() + if (app.unsupported) return + + blueprint = slugify(blueprint) + + window.history.pushState(null, document.title, "/project/blueprint/" + blueprint) + this.editorView = app.controller = new EditorView() + this.editorView.loadBlueprint(blueprint) + }, projectNewWithLayout: function(e, layout){ e && e.preventDefault() app.mode.editor = true app.launch() + if (app.unsupported) return layout = slugify(layout) @@ -123,6 +167,7 @@ var SiteRouter = Router.extend({ projectEditor: function(e, name){ app.mode.editor = true app.launch() + if (app.unsupported) return this.editorView = app.controller = new EditorView() this.editorView.load(name) @@ -131,10 +176,20 @@ var SiteRouter = Router.extend({ projectViewer: function(e, name){ app.mode.editor = true app.launch() + if (app.unsupported) return this.readerView = app.controller = new ReaderView() this.readerView.load(name) }, + + blueprintEditor: function(e, name){ + environment.init = environment.minimal + app.launch() + if (app.unsupported) return + + this.blueprintView = app.controller = new BlueprintView () + this.blueprintView.load(name) + }, signup: function(e){ e && e.preventDefault() @@ -177,7 +232,12 @@ var SiteRouter = Router.extend({ this.editProfileModal.load() }, + editSubscription: function(e){ + e && e.preventDefault() + window.history.pushState(null, document.title, "/profile/billing") + this.editSubscriptionModal.load() + }, newDocument: function(e){ e && e.preventDefault() @@ -212,30 +272,5 @@ var SiteRouter = Router.extend({ // this.documentModal.destroy(name) }, - - testWallpaper: function(e){ - var content = document.getElementById("content") - content.style.width = "680px" - content.style.margin = "0 auto" - var wm = new WallpaperManager() - app.on('wallpaper-ready', function(){ - var black = [0,0,0,0] - var white = [255,255,255,1.0] - var swatches = wm.buildSwatches(black, white, 4) - document.body.style.backgroundColor = "#eee" - swatches.forEach(function(swatch){ - swatch.style.margin = "4px" - swatch.style.border = "1px solid lime" - swatch.style.backgroundColor = "#888" - content.appendChild(swatch) - swatch.onclick = function(){ - dataUrl = swatch.toDataURL() - document.body.style.backgroundImage = "url(" + dataUrl + ")" - } - }) - }) - wm.init() - }, - }) diff --git a/public/assets/javascripts/ui/blueprint/BlueprintEditor.js b/public/assets/javascripts/ui/blueprint/BlueprintEditor.js new file mode 100644 index 0000000..7704689 --- /dev/null +++ b/public/assets/javascripts/ui/blueprint/BlueprintEditor.js @@ -0,0 +1,129 @@ + +var wallHeight = 180 +var shapes = new ShapeList +var last_point = new vec2 (0,0) + +var BlueprintEditor = View.extend(AnimatedView.prototype).extend({ + + regions: [], + + initialize: function(opt){ + this.parent = opt.parent + + $(window).resize(this.resize.bind(this)) + + scene = new MX.Scene().addTo("#perspective") + scene.camera.radius = 20 + cam = scene.camera + + scene.width = window.innerWidth/2 + scene.height = window.innerHeight + scene.perspective = window.innerHeight + + movements = new MX.Movements(cam, viewHeight) + movements.init() + movements.lock() + + app.on("move", function(pos){ + cam.x = pos.x + cam.y = pos.y + cam.z = pos.z + }) + + var floorplan = this.floorplan = new MX.Image({ + backface: true, + }) + scene.add(this.floorplan) + + // recenter perspective view by rightclicking map + this.floorplan.el.addEventListener("contextmenu", function(e){ + e.preventDefault() + var offset = offsetFromPoint(e, this) + var x = (offset.left - 0.5) * floorplan.width * floorplan.scale + var z = (offset.top - 0.5) * floorplan.height * floorplan.scale + controls.opt.center.x = -x + controls.opt.center.y = 0 + controls.opt.center.z = z + }, true) + + scene.update() + + controls = new MX.OrbitCamera({ + el: scene.el, + radius: 3000, + radiusRange: [ 10, 10000 ], + rotationX: PI/4, + rotationY: PI/2, + }) + controls.init() + }, + + resize: function(){ + if (this.parent.orbiting) { + scene.width = window.innerWidth/2 + scene.height = window.innerHeight + this.parent.map.resize( window.innerWidth/2, window.innerHeight ) + this.parent.map.canvas.style.display = "block" + } + else { + scene.width = window.innerWidth + scene.height = window.innerHeight + this.parent.map.canvas.style.display = "none" + } + }, + + loadFloorplan: function(media){ + // console.log(media) + this.floorplan.load({ + media: media, + keepImage: true, + rotationX: -PI/2, + rotationY: PI, + scale: media.scale, + }) + this.startAnimating() + this.regions = RegionList.buildByShape() + }, + + animate: function(t, dt){ + map.update(t) + + movements.update(dt) + controls.update() + scene.update() + + map.draw.ctx.save() + map.draw.translate() + + this.floorplan.draw(map.draw.ctx, true) + + map.draw.coords() + + if (shapes.workline) { + shapes.workline.draw(map.draw.ctx, "rgba(255,255,0,0.1)", "#f80") + if (map.ui.placing && last_point) { + shapes.workline.draw_line( map.draw.ctx, last_point ) + } + } + + shapes.draw(map.draw.ctx, "rgba(255,255,0,0.1)", "#f80") + + map.draw.ctx.strokeStyle = "#f00"; + map.draw.x_at( this.parent.startPosition ) + map.draw.mouse(map.ui.mouse.cursor) + map.draw.camera(scene.camera) + +// var colors = ["rgba(0,0,0,0.1)"] +// var colors = ["rgba(255,255,255,1)"] + +// map.draw.regions(this.regions, colors, "#000") + +// this.regions.forEach(function(room,i){ +// map.draw.ctx.fillStyle = colors[i % colors.length] +// map.draw.ctx.fillRect( room.x.a, room.y.a, room.width(), room.height() ) +// }) + + map.draw.ctx.restore() + }, + +}) diff --git a/public/assets/javascripts/ui/blueprint/BlueprintInfo.js b/public/assets/javascripts/ui/blueprint/BlueprintInfo.js new file mode 100644 index 0000000..51b310e --- /dev/null +++ b/public/assets/javascripts/ui/blueprint/BlueprintInfo.js @@ -0,0 +1,92 @@ + +var BlueprintInfo = View.extend({ + el: "#blueprintInfo", + + events: { + "mousedown": "stopPropagation", + "keydown": 'stopPropagation', + "change [name=height]": 'changeHeight', + "keydown [name=height]": 'enterHeight', + "change [name=units]": 'changeUnits', + "keydown [name=viewHeight]": 'enterViewHeight', + "change [name=viewHeight]": 'changeViewHeight', + "click .openScaler": 'openScaler', + }, + + initialize: function(opt){ + this.parent = opt.parent + this.$height = this.$("[name=height]") + this.$units = this.$("[name=units]") + this.$viewHeight = this.$("[name=viewHeight]") + this.$unitName = this.$(".unitName") + this.$blueprintScaleDisplay = this.$("#blueprintScaleDisplay") + }, + + load: function(data){ + this.$viewHeight.unitVal( window.viewHeight = data.viewHeight || app.defaults.viewHeight ) + this.$height.unitVal( window.wallHeight = data.wallHeight || app.defaults.wallHeight ) + this.$units.val( data.units ) + this.$('span.units').html( data.units ) + this.$unitName.html( data.units ) + + var resolution + switch (data.units) { + case 'ft': + resolution = app.defaults.footResolution + break + case 'm': + resolution = app.defaults.meterResolution + break + case 'px': + default: + resolution = 1 + break + } + this.$blueprintScaleDisplay.html( ((1/data.scale) * resolution).toFixed(1) ) + this.show() + }, + + toggle: function(state){ + this.$el.toggleClass("active", state) + this.$viewHeight.unitVal( window.viewHeight ) + }, + + openScaler: function(){ + this.parent.scaler.pick( this.parent.data, true ) + this.parent.scaler.show() + }, + + show: function(){ + this.toggle(true) + }, + + hide: function(){ + this.toggle(false) + }, + + deselect: function(){ + this.toggle(true) + }, + + enterHeight: function(e){ + if (e.keyCode == 13) this.changeHeight(e) + }, + changeHeight: function(e){ + e.stopPropagation() + window.wallHeight = this.$height.unitVal() + shapes.forEach(function(line){ + line.mx.set_height( window.wallHeight ) + }) + }, + changeUnits: function(){ + app.units = this.$units.val() + this.$('.units').resetUnitVal() + }, + enterViewHeight: function(e){ + if (e.keyCode == 13) this.changeViewHeight(e) + }, + changeViewHeight: function(){ + window.viewHeight = this.$viewHeight.unitVal() + } + +}) diff --git a/public/assets/javascripts/ui/blueprint/BlueprintNotice.js b/public/assets/javascripts/ui/blueprint/BlueprintNotice.js new file mode 100644 index 0000000..4b799a6 --- /dev/null +++ b/public/assets/javascripts/ui/blueprint/BlueprintNotice.js @@ -0,0 +1,62 @@ +var BlueprintNotice = View.extend(ToggleableView.prototype).extend({ + + el: "#blueprintNotice", + + events: { + "click .next": "next", + }, + + initialize: function(opt){ + this.parent = opt.parent + this.$notice = this.$(".notice") + this.$next = this.$(".next") + }, + + notice: function(msg){ + this.$notice.html(msg) + }, + + showCreateProjectNotice: function(){ + this.notice("<a href='/project/blueprint/" + this.parent.data.slug + + "'>Start a new project</a> with this blueprint.") + this.$next.hide() + this.nextFn = null + this.show() + }, + + showStartPositionNotice: function(){ + this.parent.settings.hide() + this.notice("First, click the map to set the starting location.") + this.$next.show().html("Next") + this.show() + this.nextFn = this.showStartAngleNotice.bind(this) + }, + + showStartAngleNotice: function(){ + this.parent.settings.hide() + this.notice("Next, rotate the camera to the desired orientation.") + this.$next.show().html("Done") + this.show() + this.nextFn = this.doneSettingPosition.bind(this) + this.parent.toolbar.toggleOrbitMode(false) + }, + + doneSettingPosition: function(){ + this.nextFn = null + this.$next.hide() + this.hide() + this.parent.settings.show() + this.parent.startPosition.rotationX = cam.rotationX + this.parent.startPosition.rotationY = cam.rotationY + this.parent.toolbar.toggleOrbitMode(true) + this.parent.toolbar.orthoPolylineMode() + }, + + nextFn: null, + next: function(){ + if (this.nextFn) { + this.nextFn() + } + }, + +})
\ No newline at end of file diff --git a/public/assets/javascripts/ui/blueprint/BlueprintScaler.js b/public/assets/javascripts/ui/blueprint/BlueprintScaler.js new file mode 100644 index 0000000..cd370ef --- /dev/null +++ b/public/assets/javascripts/ui/blueprint/BlueprintScaler.js @@ -0,0 +1,158 @@ + +var BlueprintScaler = ModalFormView.extend(AnimatedView.prototype).extend({ + el: ".blueprintScaler", + + fixedClose: true, + + action: "/api/blueprint/scale", + + events: { + "change [name=blueprint-dimensions]": "changeDimensions", + "change [name=blueprint-units]": "changeUnits", + "click .uploadNewBlueprint": "showUploader", + }, + + initialize: function(opt){ + this.parent = opt.parent + + this.$blueprintMap = this.$("#blueprintMap") + this.$blueprintDimensionsRapper = this.$("#blueprintDimensions") + this.$dimensions = this.$("[name=blueprint-dimensions]") + this.$pixels = this.$("[name=blueprint-pixels]") + this.$units = this.$("[name=blueprint-units]") + this.$save = this.$("#saveBlueprint") + + this.map = new Map ({ + type: "ortho", + el: this.$blueprintMap.get(0), + width: window.innerWidth, + height: window.innerHeight, + zoom: -2, + zoom_min: -7.0, + zoom_max: 2, + }) + this.lineTool = new LineTool + this.map.ui.add_tool("line", this.lineTool) + this.map.ui.set_tool("line") + + scene = scene || { camera: { x: 0, y: 0, z: 0 } } + + this.floorplan = new MX.Image () + }, + + showUploader: function(){ + this.parent.uploader.show() + }, + + pick: function(media, shouldEdit){ + this.media = media + + this.floorplan.load({ media: media, scale: 1, keepImage: true }) + + if (!! media.units && ! shouldEdit) { + this.parent.ready(media) + this.hide() + this.stopAnimating() + return + } + + if (media.units && media.line && media.scale) { + var points = media.line.split(",") + this.lineTool.line[0] = new vec2( +points[0], +points[1] ) + this.lineTool.line[1] = new vec2( +points[2], +points[3] ) + + app.units = media.units + this.$units.val( media.units ) + this.$dimensions.unitVal( media.scale * this.lineLength() ) + } + + this.startAnimating() + }, + + animate: function(t, dt){ + this.map.update(t) + + this.map.draw.ctx.save() + this.map.draw.translate() + + this.floorplan.draw(this.map.draw.ctx, true) + + this.map.draw.ctx.save() + this.map.draw.ctx.strokeStyle = "#f00" + this.map.draw.ctx.lineWidth = 1/map.zoom + switch (this.lineTool.line.length) { + case 1: + this.map.draw.line( + this.lineTool.line[0].a, + this.lineTool.line[0].b, + this.lineTool.cursor.x.a, + this.lineTool.cursor.y.a + ) + break + case 2: + this.map.draw.line( + this.lineTool.line[0].a, + this.lineTool.line[0].b, + this.lineTool.line[1].a, + this.lineTool.line[1].b + ) + break + } + this.map.draw.ctx.restore() + + this.map.draw.coords() + + this.map.draw.mouse(this.map.ui.mouse.cursor) + + this.map.draw.ctx.restore() + }, + + changeDimensions: function(){ + app.units = this.$units.val() + this.$dimensions.unitVal() + }, + changeUnits: function(){ + app.units = this.$units.val() + this.$dimensions.resetUnitVal() + }, + lineLength: function(){ + if (this.lineTool.line.length !== 2) return 0 + var line = this.lineTool.line + return dist( line[0].a, line[0].b, line[1].a, line[1].b ) + }, + + validate: function(){ + var val = this.$dimensions.unitVal() + var errors = [] + if (! this.lineLength()) { + errors.push("no line") + this.$dimensions.val("") + alert("Please click two corners of a wall and then specify how long it is in feet or meters.") + } + else if (val == 0) { + errors.push("no measurement") + alert("Please tell us how long the wall is in feet or meters.") + } + return errors + }, + + showErrors: function(){}, + + serialize: function(){ + var fd = new FormData(), line = this.lineTool.line + fd.append( "_id", this.media._id) + fd.append( "units", this.$units.val() ) + fd.append( "scale", this.$dimensions.unitVal() / this.lineLength() ) + fd.append( "line", [line[0].a,line[0].b,line[1].a,line[1].b].join(",") ) + fd.append( "_csrf", $("[name=_csrf]").val()) + return fd + }, + + success: function(){ + this.media.scale = this.$dimensions.unitVal() / this.lineLength() + this.stopAnimating() + this.parent.ready(this.media) + this.hide() + }, + +}) diff --git a/public/assets/javascripts/ui/blueprint/BlueprintSettings.js b/public/assets/javascripts/ui/blueprint/BlueprintSettings.js new file mode 100644 index 0000000..8addb9c --- /dev/null +++ b/public/assets/javascripts/ui/blueprint/BlueprintSettings.js @@ -0,0 +1,123 @@ + +var BlueprintSettings = FormView.extend(ToggleableView.prototype).extend({ + el: "#blueprintSettings", + + action: "/api/blueprint/edit", + destroyAction: "/api/blueprint/destroy", + + events: { + "mousedown": "stopPropagation", + "keydown": 'stopPropagation', + "keydown [name=name]": 'enterSubmit', + "click [data-role='save-layout']": 'clickSave', + "click [data-role='clear-layout']": 'clear', + "click [data-role='destroy-layout']": 'destroy', + }, + + initialize: function(opt){ + this.parent = opt.parent + this.__super__.initialize.call(this) + + this.$id = this.$("[name=_id]") + this.$csrf = this.$("[name=_csrf]") + this.$name = this.$("[name=name]") + }, + + load: function(data){ + this.$id.val(data._id) + if (data.name) { + this.$name.val(data.name) + this.hide() + } + else { + this.$name.val("") + } + if (data.shapes) { + shapes.destroy() + shapes.deserialize( data.shapes ) + shapes.build() + } + }, + + clear: function(){ + shapes.destroy() + }, + + destroy: function(){ + var msg = "Are you sure you want to delete the blueprint " + sanitize(this.$name.val()) + "?" + ConfirmModal.confirm(msg, function(){ + $.ajax({ + url: this.destroyAction, + type: "delete", + data: { _id: this.$id.val(), _csrf: this.$csrf.val() }, + success: function(data){ + window.location.href = "/layout" + } + }) + }.bind(this)) + }, + + enterSubmit: function (e) { + e.stopPropagation() + var base = this + if (e.keyCode == 13) { + setTimeout(function(){ base.save(e) }, 100) + } + }, + + validate: function(){ + var errors = [] + var name = this.$name.val() + if (! name || ! name.length) { + errors.push("Blueprint needs a name.") + } + if (shapes.count() == 0) { + errors.push("Please add some walls.") + } + return errors + }, + + showErrors: function(errors){ + var $errors = $("<span>") + errors.forEach(function(err){ + var $row = $("<div>") + $row.html(err) + $errors.append( $row ) + }) + ErrorModal.alert($errors) + }, + + serialize: function(){ + var fd = new FormData() + fd.append( "_csrf", this.$csrf.val() ) + fd.append( "_id", this.$id.val() ) + fd.append( "name", this.$name.val() ) + fd.append( "shapes", JSON.stringify( shapes.serialize() ) ) + fd.append( "startPosition", JSON.stringify( this.parent.quantizeStartPosition() ) ) + fd.append( "wallHeight", this.parent.info.$height.unitVal() ) + fd.append( "units", this.parent.info.$units.val() ) + return fd + }, + + clickSave: function(){ + this.toggle(false) + this.save() + }, + + success: function(data){ + this.parent.data = data + + this.$id.val(data._id) + this.$name.val(data.name) + this.action = this.updateAction + + this.hide() + this.parent.notice.showCreateProjectNotice() + + Minotaur.unwatch(this) + Minotaur.hide() + + window.history.pushState(null, document.title, "/blueprint/" + data.slug) + }, + +}) diff --git a/public/assets/javascripts/ui/blueprint/BlueprintToolbar.js b/public/assets/javascripts/ui/blueprint/BlueprintToolbar.js new file mode 100644 index 0000000..458357d --- /dev/null +++ b/public/assets/javascripts/ui/blueprint/BlueprintToolbar.js @@ -0,0 +1,97 @@ +var BlueprintToolbar = View.extend({ + + el: "#blueprintToolbar", + + events: { + "click [data-role=upload-floorplan]": 'showUploader', + "click [data-role=toggle-orbit-mode]": 'toggleOrbitMode', + "click [data-role=arrow-mode]": 'arrowMode', + "click [data-role=polyline-mode]": 'polylineMode', + "click [data-role=ortho-polyline-mode]": 'orthoPolylineMode', + "click [data-role=eraser-mode]": 'eraserMode', + "click [data-role=start-position-mode]": 'startPositionMode', + "click [data-role=toggle-layout-settings]": 'toggleSettings', + }, + + initialize: function(opt){ + this.parent = opt.parent + + this.$modes = this.$('.mode') + this.$toggleOrbitMode = this.$('[data-role=toggle-orbit-mode]') + this.$arrowMode = this.$('[data-role=arrow-mode]') + this.$polylineMode = this.$('[data-role=polyline-mode]') + this.$orthoPolylineMode = this.$('[data-role=ortho-polyline-mode]') + this.$eraserMode = this.$('[data-role=eraser-mode]') + this.$startPositionMode = this.$('[data-role=start-position-mode]') + + keys.on('escape', function(){ + app.controller.toolbar.toggleOrbitMode() + }) + + this.arrowMode() + }, + + showUploader: function(){ + this.parent.scaler.show() + this.parent.uploader.show() + }, + + toggleOrbitMode: function(state){ + this.parent.orbiting = typeof state == "boolean" ? state : ! this.parent.orbiting + this.$toggleOrbitMode.toggleClass("inuse", ! this.parent.orbiting) + this.parent.editor.resize() + if (this.parent.orbiting) { + controls.toggle(true) + movements.lock() + } + else { + controls.toggle(false) + movements.unlock() + movements.gravity(true) + var pos = this.parent.quantizeStartPosition() + cam.rotationX = pos.rotationX + cam.rotationY = pos.rotationY + cam.x = pos.x + cam.y = viewHeight + cam.z = pos.z + } + }, + + toggleSettings: function(){ + this.parent.settings.toggle() + this.parent.notice.toggle( ! this.parent.data.isNew && ! this.parent.settings.visible() ) + }, + + setActiveMode: function( $el ) { + this.$modes.removeClass('active') + $el.addClass('active') + }, + + arrowMode: function(){ + this.setActiveMode( this.$arrowMode ) + this.parent.map.ui.set_tool("arrow") + }, + + polylineMode: function(){ + this.setActiveMode( this.$polylineMode ) + this.parent.map.ui.set_tool("polyline") + }, + + orthoPolylineMode: function(){ + this.setActiveMode( this.$orthoPolylineMode ) + this.parent.map.ui.set_tool("ortho-polyline") + }, + + eraserMode: function(){ + this.setActiveMode( this.$eraserMode ) + this.parent.map.ui.set_tool("eraser") + }, + + startPositionMode: function(){ + this.setActiveMode( this.$startPositionMode ) + this.parent.map.ui.set_tool("start-position") + this.parent.settings.hide() + this.parent.notice.showStartPositionNotice() + }, + +})
\ No newline at end of file diff --git a/public/assets/javascripts/ui/blueprint/BlueprintUploader.js b/public/assets/javascripts/ui/blueprint/BlueprintUploader.js new file mode 100644 index 0000000..aa62a4c --- /dev/null +++ b/public/assets/javascripts/ui/blueprint/BlueprintUploader.js @@ -0,0 +1,147 @@ + +var BlueprintUploader = UploadView.extend({ + el: ".blueprintUploader", + + mediaTag: "blueprint", + createAction: "/api/blueprint/new", + uploadAction: "/api/blueprint/upload", + listAction: "/api/blueprint/user", + destroyAction: "/api/blueprint/destroy", + + events: { + "mousedown": 'stopPropagation', + "change .url": "enterUrl", + "keydown .url": "enterSetUrl", + + "click .blueprint": "pick", + "click .remove": "destroy", + }, + + initialize: function(opt){ + this.parent = opt.parent + this.__super__.initialize.call(this) + + this.$url = this.$(".url") + this.$blueprints = this.$(".blueprints") + }, + + loaded: false, + nameToShow: null, + load: function(name){ + this.nameToShow = name || "" + $.get(this.listAction, { tag: this.mediaTag }, this.populate.bind(this)) + }, + + populate: function(data){ + this.loaded = true + if (data && data.length) { + this.$blueprints.show() + data.forEach(this.append.bind(this)) + if (this.nameToShow === "new") { + // don't pick anything.. + this.show() + } + else if (! this.nameToShow) { + this.hide() + data.some(function(el){ + if (el.slug == this.nameToShow) { + this.parent.scaler.pick(el) + return true + } + }.bind(this)) + } + else { + this.hide() + this.parent.scaler.pick(data[0]) + } + } + else { + this.parent.scaler.hideClose() + this.show() + } + }, + + pick: function(e){ + var $el = $(e.currentTarget) + var media = $el.data("media") + this.hide() + this.parent.scaler.pick(media) + if (media.slug) { + window.history.pushState(null, document.title, "/blueprint/" + media.slug) + } + }, + + destroy: function(e){ + e.stopPropagation() + var $el = $(e.currentTarget) + var _id = $el.closest(".blueprint").data("id") + $el.remove() + $.ajax({ + type: "delete", + url: this.destroyAction, + data: { _id: _id, _csrf: $("[name=_csrf]").val() } + }).complete(function(){ + }) + }, + + show: function(){ + this.toggle(true) + }, + hide: function(){ + this.toggle(false) + }, + toggle: function (state) { + this.$el.toggleClass("active", state) + }, + + addUrl: function (url){ + Parser.loadImage(url, function(media){ + if (! media) return + media._csrf = $("[name=_csrf]").val() + media.tag = this.mediaTag + + var request = $.ajax({ + type: "post", + url: this.createAction, + data: media, + }) + request.done(this.add.bind(this)) + + }.bind(this)) + }, + enterUrl: function(){ + var url = this.$url.sanitize() + this.addUrl(url) + this.$url.val("") + }, + enterSetUrl: function (e) { + e.stopPropagation() + if (e.keyCode == 13) { + setTimeout(this.enterUrl.bind(this), 100) + } + }, + + add: function(media){ + this.$blueprints.show() + this.append(media) + this.hide() + this.parent.scaler.pick(media, true) + }, + + append: function(media){ + var $el = $("<span>") + var img = new Image () + img.src = media.url + var remove = document.createElement("span") + remove.className = "remove" + remove.innerHTML = "<span>x</span>" + + $el.data("id", media._id) + $el.data("media", media) + $el.append(img) + $el.append(remove) + $el.addClass("blueprint") + this.$blueprints.append($el) + }, + +}) diff --git a/public/assets/javascripts/ui/blueprint/BlueprintView.js b/public/assets/javascripts/ui/blueprint/BlueprintView.js new file mode 100644 index 0000000..1858c3d --- /dev/null +++ b/public/assets/javascripts/ui/blueprint/BlueprintView.js @@ -0,0 +1,106 @@ + +var BlueprintView = View.extend({ + el: "#blueprintView", + + action: "/api/blueprint/show/", + + events: { + }, + + initialize: function(){ +// this.colorControl = new ColorControl ({ parent: this }) +// this.cursor = new HelpCursor({ parent: this }) + this.map = this.buildMap() + this.editor = new BlueprintEditor ({ parent: this }) + this.toolbar = new BlueprintToolbar ({ parent: this }) + this.uploader = new BlueprintUploader ({ parent: this }) + this.scaler = new BlueprintScaler ({ parent: this }) + this.info = new BlueprintInfo ({ parent: this }) + this.settings = new BlueprintSettings ({ parent: this }) + this.notice = new BlueprintNotice ({ parent: this }) + Rooms.shapesMode = true + }, + + load: function(name){ + name = sanitize(name) || "new" + this.uploader.load(name) +// name = sanitize(name) +// $.get(this.action + name, this.ready.bind(this)) + }, + + orbiting: true, + startPosition: {}, + quantizeStartPosition: function(){ + // + var regions = RegionList.build() + var pos = this.startPosition + var startPositionIsInARoom = regions.some(function(region){ + return region.contains(pos.x, pos.z) + }) + if (startPositionIsInARoom) { + return this.startPosition + } + else if (! regions.length) { + return { + x: 0, + y: viewHeight, + z: 0, + rotationX: 0, + rotationY: Math.PI/2, + } + } + else { + var center = regions[0].center() + return { + x: center.a, + y: viewHeight, + z: center.b, + rotationX: 0, + rotationY: Math.PI/2, + } + } + }, + + buildMap: function(){ + // i forget if this has to be global + map = new Map ({ + type: "ortho", + el: document.querySelector("#orthographic"), + width: window.innerWidth/2, + height: window.innerHeight, + zoom: -2, + zoom_min: -6.2, + zoom_max: 1, + }) + map.ui.add_tool("arrow", new ArrowTool) + map.ui.add_tool("polyline", new PolylineTool) + map.ui.add_tool("ortho-polyline", new OrthoPolylineTool) + map.ui.add_tool("eraser", new EraserTool) + map.ui.add_tool("position", new PositionTool) + map.ui.add_tool("start-position", new StartPositionTool) + map.ui.placing = false + return map + }, + + ready: function(data){ + this.data = data + this.info.load(data) + this.settings.load(data) + this.editor.loadFloorplan(data) + if (! data.shapes || data.shapes.length == 0) { + this.startPosition = { x: 0, y: 0, z: 0, rotationX: 0, rotationY: Math.PI/2 } + } + else { + this.startPosition = data.startPosition + } + this.notice.hide() + this.settings.show() + }, + + hideExtras: function(){ + }, + + pickWall: function(wall, pos){ + }, + +}) diff --git a/public/assets/javascripts/ui/builder/BuilderInfo.js b/public/assets/javascripts/ui/builder/BuilderInfo.js index e6b7e97..aa58d6e 100644 --- a/public/assets/javascripts/ui/builder/BuilderInfo.js +++ b/public/assets/javascripts/ui/builder/BuilderInfo.js @@ -3,15 +3,21 @@ var BuilderInfo = View.extend({ el: "#builderInfo", events: { + "mousedown": "stopPropagation", "keydown": 'stopPropagation', "change [name=x]": 'changeX', "change [name=y]": 'changeY', "change [name=width]": 'changeWidth', "change [name=depth]": 'changeDepth', "change [name=height]": 'changeHeight', + "keydown [name=width]": 'enterWidth', + "keydown [name=depth]": 'enterDepth', + "keydown [name=height]": 'enterHeight', "change [name=units]": 'changeUnits', - "change [name=resolution]": 'changeResolution', + "keydown [name=viewHeight]": 'enterViewHeight', "change [name=viewHeight]": 'changeViewHeight', + "change [name=heightGlobal]": 'changeHeightGlobal', + "click [data-role=destroy-room]": 'destroy', }, initialize: function(opt){ @@ -24,25 +30,51 @@ var BuilderInfo = View.extend({ this.$units = this.$("[name=units]") this.$viewHeight = this.$("[name=viewHeight]") this.$unitName = this.$(".unitName") + this.$noSelection = this.$(".no-selection") + this.$settings = this.$(".setting") + this.$heightGlobalCheckbox = this.$("[name=heightGlobal]") app.on("builder-pick-room", this.pick.bind(this)) - app.on("builder-destroy-room", this.destroy.bind(this)) + app.on("builder-destroy-room", this.hide.bind(this)) + app.on("builder-pick-nothing", this.deselect.bind(this)) }, load: function(data){ - this.$viewHeight.unitVal( data.viewHeight || app.defaults.viewHeight ) - this.$units.val( "ft" ) - this.$unitName.html( "ft" ) + this.$viewHeight.unitVal( window.viewHeight = data.viewHeight || app.defaults.viewHeight ) + this.$units.val( data.units || "ft" ) + this.$unitName.html( data.units || "ft" ) + + if (Rooms.regions.length == 0) { + this.changeHeightGlobal(true) + } + else { + var rooms = Rooms.values() + var height = rooms[0].height + var differentHeights = Rooms.some(function(room){ + return room.height != height + }) + this.changeHeightGlobal( ! differentHeights ) + } }, toggle: function(state){ + this.$settings.toggle( !! this.room ) + this.$noSelection.toggle( ! this.room ) this.$el.toggleClass("active", state) + if (state) { + this.$viewHeight.unitVal( window.viewHeight ) + this.parent.cursor.message("builder") + } + else { + this.parent.cursor.message("start") + } }, show: function(){ this.toggle(true) }, - hide: function(){ + hide: function(){ + this.room = null this.toggle(false) }, @@ -57,43 +89,84 @@ var BuilderInfo = View.extend({ this.$y.unitVal( room.rect.y.a ) this.show() }, + + deselect: function(){ + this.room = null + this.toggle(true) + }, + + destroy: function(){ + UndoStack.push({ + type: "destroy-room", + undo: this.room.copy(), + redo: { id: this.room.id }, + }) + + Rooms.remove(this.room) + app.tube("builder-destroy-room", this.room) - destroy: function(room){ this.room = null this.hide() }, + enterWidth: function(e){ + if (e.keyCode == 13) this.changeWidth(e) + }, changeWidth: function(e){ e.stopPropagation() this.room.rect.x.setLength( this.$width.unitVal() ) - Rooms.clipper.update() + Rooms.rebuild() + }, + + enterDepth: function(e){ + if (e.keyCode == 13) this.changeDepth(e) }, changeDepth: function(e){ e.stopPropagation() this.room.rect.y.setLength( this.$depth.unitVal() ) - Rooms.clipper.update() + Rooms.rebuild() + }, + + enterHeight: function(e){ + if (e.keyCode == 13) this.changeHeight(e) }, changeHeight: function(e){ e.stopPropagation() - this.room.height = this.$height.unitVal() - Rooms.clipper.update() + var height = this.room.height = this.$height.unitVal() + if (window.heightIsGlobal) { + Rooms.forEach(function(room){ + room.height = height + }) + } + Rooms.rebuild() }, changeX: function(e){ e.stopPropagation() this.room.rect.x.setPosition( this.$x.unitVal() ) - Rooms.clipper.update() + Rooms.rebuild() }, changeY: function(e){ e.stopPropagation() this.room.rect.y.setPosition( this.$y.unitVal() ) - Rooms.clipper.update() + Rooms.rebuild() }, changeUnits: function(){ app.units = this.$units.val() this.$('.units').resetUnitVal() }, - changeViewHeight: function(){ + enterViewHeight: function(e){ + if (e.keyCode == 13) this.changeViewHeight(e) + }, + changeViewHeight: function(){ window.viewHeight = this.$viewHeight.unitVal( ) }, + changeHeightGlobal: function(state){ + if (typeof state == "boolean") { + this.$heightGlobalCheckbox.prop("checked", state) + window.heightIsGlobal = state + return + } + window.heightIsGlobal = this.$heightGlobalCheckbox.prop("checked") + }, }) diff --git a/public/assets/javascripts/ui/builder/BuilderSettings.js b/public/assets/javascripts/ui/builder/BuilderSettings.js index 94eed29..256bffe 100644 --- a/public/assets/javascripts/ui/builder/BuilderSettings.js +++ b/public/assets/javascripts/ui/builder/BuilderSettings.js @@ -7,6 +7,7 @@ var BuilderSettings = FormView.extend({ destroyAction: "/api/layout/destroy", events: { + "mousedown": "stopPropagation", "keydown": 'stopPropagation', "keydown [name=name]": 'enterSubmit', "click [data-role='save-layout']": 'clickSave', @@ -29,7 +30,7 @@ var BuilderSettings = FormView.extend({ this.$id.val(data._id) this.$name.val(data.name) - this.parent.lightControl.loadDefaults() + this.parent.colorControl.loadDefaults() data.rooms && Rooms.deserialize(data.rooms) data.startPosition && scene.camera.move(data.startPosition) @@ -51,7 +52,7 @@ var BuilderSettings = FormView.extend({ this.$name.val( names.join(" ") ) this.action = this.createAction - window.history.pushState(null, document.title, "/builder/new") + window.history.pushState(null, document.title, "/layout/new") }, clear: function(){ @@ -107,7 +108,7 @@ var BuilderSettings = FormView.extend({ }, serialize: function(){ - map.draw.render() + var thumbnail = map.draw.render() var fd = new FormData() fd.append( "_csrf", this.$csrf.val() ) fd.append( "_id", this.$id.val() ) @@ -115,7 +116,7 @@ var BuilderSettings = FormView.extend({ fd.append( "privacy", this.$privacy.filter(":checked").val() == "private" ) fd.append( "rooms", JSON.stringify( Rooms.serialize() ) ) fd.append( "startPosition", JSON.stringify( app.position(scene.camera) ) ) - fd.append( "thumbnail", dataUriToBlob(map.canvas.toDataURL()) ) + fd.append( "thumbnail", dataUriToBlob(thumbnail.toDataURL()) ) return fd }, diff --git a/public/assets/javascripts/ui/builder/BuilderToolbar.js b/public/assets/javascripts/ui/builder/BuilderToolbar.js index 2eb7590..e9dcce3 100644 --- a/public/assets/javascripts/ui/builder/BuilderToolbar.js +++ b/public/assets/javascripts/ui/builder/BuilderToolbar.js @@ -3,6 +3,7 @@ var BuilderToolbar = View.extend({ el: "#builderToolbar", events: { + "mousedown": "stopPropagation", "click [data-role='toggle-map-view']": 'toggleMap', "click [data-role='toggle-layout-settings']": 'toggleSettings', "click [data-role='undo']": 'undo', @@ -59,6 +60,9 @@ var BuilderToolbar = View.extend({ var state = map.ui.permissions.toggle("destroy") $(".inuse").removeClass("inuse") $(e.currentTarget).toggleClass("inuse", state) + if (! state) { + this.resetPermissions() + } }, }) diff --git a/public/assets/javascripts/ui/builder/BuilderView.js b/public/assets/javascripts/ui/builder/BuilderView.js index 81dce52..b975499 100644 --- a/public/assets/javascripts/ui/builder/BuilderView.js +++ b/public/assets/javascripts/ui/builder/BuilderView.js @@ -11,12 +11,8 @@ var BuilderView = View.extend({ this.info = new BuilderInfo ({ parent: this }) this.toolbar = new BuilderToolbar ({ parent: this }) this.settings = new BuilderSettings ({ parent: this }) - this.lightControl = new LightControl ({ parent: this }) - - app.on("rooms-built", function(){ - Walls.paint() - }) - + this.colorControl = new ColorControl ({ parent: this }) + this.cursor = new HelpCursor({ parent: this }) }, load: function(name){ @@ -36,6 +32,9 @@ var BuilderView = View.extend({ }, hideExtras: function(){ - } + }, + + pickWall: function(wall, pos){ + }, }) diff --git a/public/assets/javascripts/ui/editor/Collaborators.js b/public/assets/javascripts/ui/editor/Collaborators.js index 452ad15..b57510d 100644 --- a/public/assets/javascripts/ui/editor/Collaborators.js +++ b/public/assets/javascripts/ui/editor/Collaborators.js @@ -9,6 +9,7 @@ var Collaborators = ModalFormView.extend({ destroyAction: function(){ return "/api/collaborator/" + this.parent.data.slug + "/destroy" }, events: { + "mousedown": "stopPropagation", "keydown [name=email]": "enterSubmit", "click [data-role=destroy-collaborator]": "destroy", }, diff --git a/public/assets/javascripts/ui/editor/ColorControl.js b/public/assets/javascripts/ui/editor/ColorControl.js new file mode 100644 index 0000000..54a6a2e --- /dev/null +++ b/public/assets/javascripts/ui/editor/ColorControl.js @@ -0,0 +1,153 @@ + +var ColorControl = View.extend({ + el: ".colorcontrol", + + events: { + "mousedown": "stopPropagation", + "click .color-swatches span": "setSurface", + "click .colors span": "setHue", + }, + + colors: [ + [255,94,58], + [255,149,0], + [255,219,76], + [76,217,100], + [52,170,220], + [29,98,240], + [198,68,252], + [0,0,0], + [74,74,74], + [125,126,127], + [209,211,212], + [235,235,235], + [255,255,255], + ], + + initialize: function(opt){ + this.parent = opt.parent + + this.colorPicker = new LabColorPicker(this, 155, 155) + this.$(".color-picker").append( this.colorPicker.canvas ) + this.$(".color-picker").append( this.colorPicker.cursor ) + this.$(".slider").append( this.colorPicker.brightness ) + + this.$swatches = this.$(".swatch") + this.$labels = this.$(".swatch + label") + this.$swatch = { + wall: this.$("#wall-color"), + outline: this.$("#outline-color"), + floor: this.$("#floor-color"), + ceiling: this.$("#ceiling-color"), + } + + this.$colors = this.$(".colors") + this.colors.forEach(function(color){ + var $swatch = $("<span>") + $swatch.css("background-color","rgb(" + color + ")") + $swatch.data('color', color) + this.$colors.append($swatch) + }.bind(this)) + + if ($.browser.mozilla) { + $("#floor-color").parent().hide() + $("#ceiling-color").parent().hide() + } + }, + + modes: [ "wall", "outline", "floor", "ceiling" ], + + load: function(data){ + this.modes.forEach(function(mode){ + Walls.setColor[mode](data[mode]) + this.$swatch[ mode ].css("background-color", rgb_string(data[mode])) + }.bind(this)) + this.setMode("wall") + }, + + loadDefaults: function(){ + var colors = { + wall: app.defaults.colors.wall.slice(), + outline: app.defaults.colors.outline.slice(), + floor: app.defaults.colors.floor.slice(), + ceiling: app.defaults.colors.ceiling.slice(), + } + this.load(colors) + }, + + toggle: function(state){ + if (state) { + this.parent.cursor.message("colors") + } + this.$el.toggleClass("active", state); + }, + + show: function(){ + this.toggle(true) + }, + + hide: function(){ + this.toggle(false) + }, + + pickColor: function(rgb, Lab){ + this.labColor = Lab + this.setSwatchColor(this.mode, rgb) + // console.log(rgb) + Walls.setColor[ this.mode ](rgb) + this.parent.presets.modified = true + }, + + setSwatchColor: function(mode, rgb) { + this.$swatch[ mode ].css("background-color", rgb_string(rgb)) + }, + + initialState: null, + + begin: function(){ + this.initialState = this.serialize() + }, + + serialize: function(){ + return { + mode: this.mode, + rgb: Walls.colors[ this.mode ] + } + }, + + finalize: function(){ + if (! this.initialState) { return } + UndoStack.push({ + type: 'update-colors', + undo: this.initialState, + redo: this.serialize(), + }) + + this.initialState = null + + // TODO: watch individual wall object here + Minotaur.watch( app.router.editorView.settings ) + }, + + setMode: function (mode) { + var color, brightness + this.mode = mode + this.$(".active").removeClass("active") + this.$swatch[ mode ].parent().addClass("active") + color = Walls.colors[ mode ] + + this.labColor = this.colorPicker.load(color) + }, + + setSurface: function(e){ + var mode = $('.swatch', e.currentTarget).data('mode') + this.setMode(mode) + }, + + setHue: function(e){ + var color = $(e.currentTarget).data('color') + this.labColor = this.colorPicker.load(color) + this.pickColor(color, this.labColor) + } + +}) diff --git a/public/assets/javascripts/ui/editor/EditorSettings.js b/public/assets/javascripts/ui/editor/EditorSettings.js index 8a88f7a..5aa88e9 100644 --- a/public/assets/javascripts/ui/editor/EditorSettings.js +++ b/public/assets/javascripts/ui/editor/EditorSettings.js @@ -5,8 +5,11 @@ var EditorSettings = FormView.extend({ createAction: "/api/project/new", updateAction: "/api/project/edit", destroyAction: "/api/project/destroy", + + useMinotaur: true, events: { + "mousedown": "stopPropagation", "keydown": 'stopPropagation', "keydown [name=name]": 'enterSubmit', "click [data-role='show-collaborators']": 'showCollaborators', @@ -14,6 +17,12 @@ var EditorSettings = FormView.extend({ "click [data-role='clone-project']": 'clone', "click [data-role='clear-project']": 'clear', "click [data-role='destroy-project']": 'destroy', + "click [data-role='toggle-map']": 'toggleMap', + "click [data-role='view-project']": 'viewProject', + "click #startText": "setStartPosition", + "click #moveText": "confirmStartPosition", + "click #confirmText": "setStartPosition", + "click #goText": "goToStartPosition", }, initialize: function(opt){ @@ -25,21 +34,30 @@ var EditorSettings = FormView.extend({ this.$name = this.$("[name=name]") this.$description = this.$("[name=description]") this.$privacy = this.$("[name=privacy]") + this.$startPoint = this.$("#startpoint") }, load: function(data){ this.action = data.isNew ? this.createAction : this.updateAction this.parent.data = data - data.rooms && Rooms.deserialize(data.rooms) - data.walls && Walls.deserialize(data.walls) - data.startPosition && scene.camera.move(data.startPosition) + if (data.shapes && data.shapes.length) { + Rooms.deserializeFromShapes(data, data.walls) + } + else if (data.rooms) { + Rooms.deserialize(data.rooms, data.walls) + } + if (data.startPosition) { + scene.camera.move(data.startPosition) + this.startPosition = data.startPosition + this.$startPoint.addClass("confirmed") + } if (data.colors && data.colors.wall) { - this.parent.lightControl.load(data.colors) + this.parent.colorControl.load(data.colors) } else { - this.parent.lightControl.loadDefaults() + this.parent.colorControl.loadDefaults() } if (data.walls) { @@ -53,7 +71,7 @@ var EditorSettings = FormView.extend({ } if (data.isNew) { - this.$name.val( "Untitled Room" ) + this.$name.val( "Untitled" ) } else { this.thumbnailIsStale() @@ -64,6 +82,7 @@ var EditorSettings = FormView.extend({ data.privacy && this.$privacy.find("[value=" + data.privacy + "]").prop("checked", "checked") data.media && Scenery.deserialize(data.media) + data.sculpture && Sculpture.deserialize(data.sculpture) } }, @@ -72,7 +91,14 @@ var EditorSettings = FormView.extend({ this.parent.collaborators.show() }, - clone: function(){ + toggleMap: function(e){ + e.preventDefault() + app.controller.toolbar.toggleMap() + }, + + clone: function(e){ + e.preventDefault() + var names = this.$name.val().split(" ") if ( ! isNaN(Number( names[names.length-1] )) ) { names[names.length-1] = Number( names[names.length-1] ) + 1 @@ -84,12 +110,15 @@ var EditorSettings = FormView.extend({ this.$id.val('new') this.$name.val( names.join(" ") ) this.action = this.createAction + this.thumbnailState = null - window.history.pushState(null, document.title, "/builder/new") + window.history.pushState(null, document.title, "/project/new") }, - clear: function(){ - Rooms.removeAll() + clear: function(e){ + e.preventDefault() + Scenery.removeAll() + Sculpture.removeAll() }, destroy: function(){ @@ -100,7 +129,7 @@ var EditorSettings = FormView.extend({ type: "delete", data: { _id: this.$id.val(), _csrf: this.$csrf.val() }, success: function(data){ - window.location.href = "/project" + window.location.href = "/profile" } }) }.bind(this)) @@ -109,8 +138,12 @@ var EditorSettings = FormView.extend({ toggle: function(state){ var state = typeof state == 'boolean' ? state : ! this.$el.hasClass("active") this.$el.toggleClass("active", state) - + + $(".inuse").removeClass("inuse") $("[data-role='toggle-project-settings']").toggleClass("inuse", state) + if (state) { + this.parent.cursor.message("settings") + } }, enterSubmit: function (e) { @@ -140,22 +173,43 @@ var EditorSettings = FormView.extend({ ErrorModal.alert($errors) }, + startPosition: null, + setStartPosition: function(){ + this.$startPoint.addClass("active").removeClass("confirmed") + }, + confirmStartPosition: function(){ + this.$startPoint.removeClass("active").addClass("confirmed") + this.startPosition = app.position(scene.camera) + }, + goToStartPosition: function(){ + if (! this.startPosition) return + scene.camera.move(this.startPosition) + }, + serialize: function(){ - map.draw.render() var fd = new FormData() fd.append( "_csrf", this.$csrf.val() ) fd.append( "_id", this.$id.val() ) fd.append( "name", this.$name.val() ) fd.append( "description", this.$description.val() ) fd.append( "privacy", this.$privacy.filter(":checked").val() == "private" ) - fd.append( "rooms", JSON.stringify( Rooms.serialize() ) ) + fd.append( "viewHeight", window.viewHeight ) + if (Rooms.shapesMode) { + fd.append( "shapes", JSON.stringify( shapes.serialize() ) ) + } + else { + fd.append( "rooms", JSON.stringify( Rooms.serialize() ) ) + } fd.append( "walls", JSON.stringify( Walls.serialize() ) ) fd.append( "colors", JSON.stringify( Walls.colors ) ) fd.append( "media", JSON.stringify( Scenery.serialize() ) ) - fd.append( "startPosition", JSON.stringify( app.position(scene.camera) ) ) + fd.append( "sculpture", JSON.stringify( Sculpture.serialize() ) ) + fd.append( "startPosition", JSON.stringify( this.startPosition || false ) ) + fd.append( "lastPosition", JSON.stringify( app.position(scene.camera) ) ) if (this.thumbnailIsStale()) { - fd.append( "thumbnail", dataUriToBlob(map.canvas.toDataURL()) ) + var thumbnail = map.draw.render() + fd.append( "thumbnail", dataUriToBlob(thumbnail.toDataURL()) ) } return fd }, @@ -174,6 +228,16 @@ var EditorSettings = FormView.extend({ clickSave: function(){ this.toggle(false) this.save() + this.isVisible = true + }, + + viewAfterSave: false, + viewProject: function(e){ + e.preventDefault() + Minotaur.unwatch(this) + Minotaur.hide() + this.viewAfterSave = true + this.save() }, success: function(data){ @@ -181,11 +245,22 @@ var EditorSettings = FormView.extend({ this.$name.val(data.name) this.action = this.updateAction + if (this.viewAfterSave) { + window.location.pathname = "/project/" + data.slug + return + } + Minotaur.unwatch(this) Minotaur.hide() window.history.pushState(null, document.title, "/project/" + data.slug + "/edit") - + + this.parent.share.setLink( "http://vvalls.com/project/" + data.slug ) + if (this.isVisible) { + this.isVisible = false + this.parent.share.show() + } + this.parent.data = data }, diff --git a/public/assets/javascripts/ui/editor/EditorToolbar.js b/public/assets/javascripts/ui/editor/EditorToolbar.js index e91da0f..a5ad2dd 100644 --- a/public/assets/javascripts/ui/editor/EditorToolbar.js +++ b/public/assets/javascripts/ui/editor/EditorToolbar.js @@ -3,86 +3,86 @@ var EditorToolbar = View.extend({ el: "#editorToolbar", events: { + "mousedown": 'stopPropagation', + "mouseenter": 'mouseenter', + "mouseleave": 'mouseleave', + "click [data-role='undo']": 'undo', "click [data-role='toggle-map-view']": 'toggleMap', "click [data-role='toggle-project-settings']": 'toggleSettings', "click [data-role='open-media-viewer']": 'openMediaViewer', - "click [data-role='resize-media']": 'resizeMedia', - "click [data-role='destroy-media']": 'destroyMedia', + "click [data-role='toggle-presets']": 'togglePresets', "click [data-role='toggle-wallpaper-panel']": 'toggleWallpaper', - "click [data-role='toggle-light-control']": 'toggleLightControl', - "click [data-role='edit-wall-text']": 'editWallText', + "click [data-role='toggle-color-control']": 'toggleColorControl', + "click [data-role='toggle-text-editor']": 'toggleTextEditor', }, initialize: function(opt){ this.parent = opt.parent }, - toggleMap: function(){ - map.toggle() - // $("#minimap").toggleClass("hide"); + undo: function(e){ + if (e.shiftKey) { + var canRedo = UndoStack.redo() + console.log("can redo", canRedo) + } + else { + var canUndo = UndoStack.undo() + console.log("can undo", canUndo) + } + }, + + toggleMap: function(state){ +// if (typeof state != "boolean") { +// state = ! $("[data-role='toggle-map-view']").hasClass("inuse") +// this.resetControls() +// } +// $("[data-role='toggle-map-view']").toggleClass("inuse", state) + var state = map.toggle(state) + if (state) { map.ui.blur() } + $("#minimap").toggleClass("hide", state) + this.parent.info.toggle(state) }, toggleSettings: function(){ - this.resetMode() - $(".inuse").removeClass("inuse") - this.parent.lightControl.hide() +// this.resetMode() + this.toggleMap(false) + this.parent.textEditor.hide() + this.parent.presets.hide() + this.parent.colorControl.hide() this.parent.wallpaperPicker.hide() this.parent.mediaEditor.hide() this.parent.settings.toggle() }, openMediaViewer: function(){ - this.parent.mediaViewer.show() - this.parent.mediaUpload.show() this.resetMode() this.resetControls() + this.toggleMap(false) + this.parent.mediaViewer.show() }, - + resetMode: function(){ - this.resizeMedia(false) - this.destroyMedia(false) + $(".inuse").removeClass("inuse") + $("body").removeClass("addText") + this.parent.hideExtras() + this.resetPermissions() }, resetControls: function(){ + $(".inuse").removeClass("inuse") + this.toggleMap(false) + this.parent.textEditor.hide() this.parent.wallpaperPicker.hide() - this.parent.lightControl.hide() - }, - - resizeMedia: function(e, state){ - this.resetControls() - if (! state && typeof e == "boolean") { - state = e - editor.permissions.assign("resize", state) - } - else { - state = editor.permissions.toggle("resize") - } - ! state && editor.permissions.assign("move", true) - $(".inuse").removeClass("inuse") - $("[data-role='resize-media']").toggleClass("inuse", state) - if (state) { - if (this.parent.mediaEditor.scenery) { - Scenery.resize.show( this.parent.mediaEditor.scenery ) - } - } - else { - Scenery.resize.hide() - } + this.parent.presets.hide() + this.parent.colorControl.hide() + this.parent.settings.hide() }, - destroyMedia: function(e, state){ - this.resetControls() - if (! state && typeof e == "boolean") { - state = e - editor.permissions.assign("destroy", state) - } - else { - state = editor.permissions.toggle("destroy") - } - ! state && editor.permissions.assign("move", true) - $(".inuse").removeClass("inuse") - $("[data-role='destroy-media']").toggleClass("inuse", state) - $("body").toggleClass("destroyActive", state) + resetPermissions: function(){ + editor.permissions.add("pick") + editor.permissions.add("move") + editor.permissions.add("resize") + editor.permissions.remove("destroy") }, toggleWallpaper: function(){ @@ -90,31 +90,67 @@ var EditorToolbar = View.extend({ this.resetMode() $("[data-role='toggle-wallpaper-panel']").toggleClass("inuse", state) this.parent.mediaEditor.hide() - this.parent.lightControl.hide() + this.parent.colorControl.hide() + this.parent.textEditor.hide() this.parent.settings.hide() + this.parent.presets.hide() + this.toggleMap(false) this.parent.wallpaperPicker.toggle(state) }, - toggleLightControl: function(){ - var state = ! $("[data-role='toggle-light-control']").hasClass("inuse") + toggleColorControl: function(){ + var state = ! $("[data-role='toggle-color-control']").hasClass("inuse") this.resetMode() - $("[data-role='toggle-light-control']").toggleClass("inuse", state) + $("[data-role='toggle-color-control']").toggleClass("inuse", state) this.parent.mediaEditor.hide() this.parent.wallpaperPicker.hide() + this.parent.textEditor.hide() this.parent.settings.hide() - this.parent.lightControl.toggle(state) + this.parent.presets.hide() + this.toggleMap(false) + this.parent.colorControl.toggle(state) }, - editWallText: function(){ - }, + toggleTextEditor: function(){ + var state = ! $("[data-role='toggle-text-editor']").hasClass("inuse") + this.resetMode() + $("[data-role='toggle-text-editor']").toggleClass("inuse", state) + this.parent.mediaEditor.hide() + this.parent.wallpaperPicker.hide() + this.parent.colorControl.hide() + this.parent.settings.hide() + this.parent.presets.hide() + this.toggleMap(false) + this.parent.textEditor.toggle(state) + }, + togglePresets: function(){ + var state = ! $("[data-role='toggle-presets']").hasClass("inuse") + this.resetMode() + $("[data-role='toggle-presets']").toggleClass("inuse", state) + this.parent.mediaEditor.hide() + this.parent.wallpaperPicker.hide() + this.parent.textEditor.hide() + this.parent.settings.hide() + this.parent.colorControl.hide() + this.toggleMap(false) + this.parent.presets.toggle(state) + }, + + mouseenter: function(){ + this.parent.cursor.hide() + }, + + mouseleave: function(){ + this.parent.cursor.show() + }, }) var editor = new function(){ this.permissions = new Permissions({ 'pick': true, 'move': true, - 'resize': false, + 'resize': true, 'destroy': false, }) }
\ No newline at end of file diff --git a/public/assets/javascripts/ui/editor/EditorView.js b/public/assets/javascripts/ui/editor/EditorView.js index e11f189..c05b373 100644 --- a/public/assets/javascripts/ui/editor/EditorView.js +++ b/public/assets/javascripts/ui/editor/EditorView.js @@ -2,6 +2,7 @@ var EditorView = View.extend({ el: "#editorView", + blueprintAction: "/api/blueprint/user/", projectAction: "/api/project/", layoutAction: "/api/layout/", @@ -9,14 +10,21 @@ var EditorView = View.extend({ }, initialize: function(){ + this.cursor = new HelpCursor ({ parent: this }) this.toolbar = new EditorToolbar ({ parent: this }) this.settings = new EditorSettings ({ parent: this }) + this.info = new BuilderInfo ({ parent: this }) this.mediaViewer = new MediaViewer ({ parent: this }) this.mediaUpload = new MediaUpload ({ parent: this }) + this.mediaTumblr = new MediaTumblr ({ parent: this }) this.mediaEditor = new MediaEditor ({ parent: this }) + this.sculptureEditor = new SculptureEditor ({ parent: this }) this.wallpaperPicker = new WallpaperPicker ({ parent: this }) - this.lightControl = new LightControl ({ parent: this }) + this.colorControl = new ColorControl ({ parent: this }) + this.textEditor = new TextEditor ({ parent: this }) this.collaborators = new Collaborators ({ parent: this }) + this.presets = new Presets ({ parent: this }) + this.share = new ShareView ({ parent: this }) }, load: function(name){ @@ -25,27 +33,65 @@ var EditorView = View.extend({ }, loadLayout: function(layout){ + if (layout == "empty") { + this.readyLayout({}) + this.toolbar.toggleMap() + return + } layout = sanitize(layout) $.get(this.layoutAction + layout, this.readyLayout.bind(this)) }, + loadBlueprint: function(blueprint){ + $.get(this.blueprintAction + blueprint, this.readyLayout.bind(this)) + }, + ready: function(data){ $("#map").hide() - + + this.data = data + this.settings.load(data) + this.info.load(data) }, readyLayout: function(data){ data.isNew = true + // $('#help-button').trigger("click") this.ready(data) }, pick: function(scenery){ - this.mediaEditor.pick(scenery) + if (scenery.isSculpture) { + this.mediaEditor.hide() + this.textEditor.hide() + this.sculptureEditor.pick(scenery) + } + else if (scenery.type == "text") { + this.mediaEditor.hide() + this.sculptureEditor.hide() + this.textEditor.pick(scenery) + } + else { + this.textEditor.hide() + this.sculptureEditor.hide() + this.mediaEditor.pick(scenery) + } + }, + + pickWall: function(wall, pos){ + this.hideExtras() + this.wallpaperPicker.pickWall(wall) }, hideExtras: function(){ + this.sculptureEditor.hide() this.mediaEditor.hide() - } + this.textEditor.hide() + this.share.hide() + Sculpture.resize.hide() + Scenery.resize.hide() + Scenery.hovering = false + } }) diff --git a/public/assets/javascripts/ui/editor/HelpCursor.js b/public/assets/javascripts/ui/editor/HelpCursor.js new file mode 100644 index 0000000..4c8ff0c --- /dev/null +++ b/public/assets/javascripts/ui/editor/HelpCursor.js @@ -0,0 +1,79 @@ + +var HelpCursor = View.extend({ + el: "#helpCursor", + + active: false, + + messages: { + start: "Welcome to VValls! Click one of the tools at right to learn about it.", + media: "This is where you pick media to go on the walls. You can upload media and paste links.", + addmedia: "Great, now click a wall to place this image.", + resize: "Drag the image to position it, or use the dots to resize.", + presets: "These are some basic presets to get you started. Click em! :-)", + wallpaper: "Click the wallpaper you want then apply it to the walls. Feel free to upload your own too!", + colors: "Use these colors to change the color of the walls, floor, and ceiling.", + settings: "This is where you publish your project. Give it a name, hit save, and you'll have a URL you can share with your friends.", + builder: "This is a map of your rooms. Draw new boxes, or move and resize the ones that are there. Hit ESCAPE to toggle the map.", + }, + + initialize: function(){ + this.helpButton = $('#help-button') + + this.helpButton.click(this.toggle.bind(this)) + this.$el.html(this.messages['start']) + }, + + toggle: function(){ + this.active ? this.stop() : this.start() + }, + + start: function(){ + if (this.active) return + this.active = true + this.helpButton.addClass('active') + this.$el.show() + this.move({ pageX: -1000, pageY: -10000 }) + this.moveFn = this.move.bind(this) + document.addEventListener("mousemove", this.moveFn) + }, + + stop: function(){ + this.active = false + this.$el.hide() + this.helpButton.removeClass('active') + document.removeEventListener("mousemove", this.moveFn) + }, + + offset: 100, + lastPosition: { pageX: 0, pageY: 0 }, + move: function(e){ + this.el.style.right = clamp(window.innerWidth - e.pageX, this.offset, window.innerWidth) + "px" + this.el.style.top = e.pageY + "px" + this.lastPosition = e + }, + + show: function(name){ + if (this.active) { + this.$el.show() + } + }, + hide: function(){ + this.$el.hide() + }, + + message: function(name){ + if (! this.active) return + if (name == "start" || name == "media" || name == "settings") { + this.offset = 100 + } + else if (name == "colors") { + this.offset = 270 + } + else { + this.offset = 290 + } + this.move(this.lastPosition) + this.$el.html(this.messages[name]) + }, + +}) diff --git a/public/assets/javascripts/ui/editor/MediaEditor.js b/public/assets/javascripts/ui/editor/MediaEditor.js index 9b20a43..6068c48 100644 --- a/public/assets/javascripts/ui/editor/MediaEditor.js +++ b/public/assets/javascripts/ui/editor/MediaEditor.js @@ -3,7 +3,7 @@ var MediaEditor = FormView.extend({ el: "#mediaEditor", events: { - "keydown": 'stopPropagation', + "keydown": 'taint', "focus [name]": "clearMinotaur", "click [data-role=play-media]": "togglePaused", "mousedown [name=keyframe]": "stopPropagation", @@ -51,40 +51,56 @@ var MediaEditor = FormView.extend({ }, pick: function(scenery) { - if (this.scenery) { + if (this.scenery && scenery !== this.scenery) { this.unbind() } this.bind(scenery) this.$el.addClass("active") +// app.controller.toolbar.resetMode() + app.controller.toolbar.resetControls() + Scenery.resize.show(scenery) + Scenery.hovering = true + var media = scenery.media - this.$name.val(media.title) + // console.log(media) + + this.$name.val(media.title) // || filenameFromUrl(media.url) ) this.$description.val(media.description) this.setDimensions() this.$units.val( "ft" ) switch (media.type) { case "image": - this.$(".image").show() this.$(".video").hide() - + this.$(".audio").hide() + this.$(".image").show() break case "youtube": case "vimeo": case "video": - this.$(".video").show() this.$(".image").hide() + this.$(".audio").hide() + this.$(".video").show() this.$playButton.toggleClass("paused", ! this.scenery.paused()) this.$autoplay.prop('checked', !! media.autoplay) this.$loop.prop('checked', !! media.loop) this.$mute.prop('checked', !! media.mute) this.$keyframe.val( Number(media.keyframe || 0) ) - break + + case "soundcloud": + this.$(".image").hide() + this.$(".video").hide() + this.$(".audio").show() + this.$playButton.toggleClass("paused", ! this.scenery.paused()) + this.$autoplay.prop('checked', !! media.autoplay) + this.$loop.prop('checked', !! media.loop) + break } }, @@ -98,12 +114,14 @@ var MediaEditor = FormView.extend({ seek: function(){ var n = parseFloat( this.$keyframe.val() ) this.scenery.seek(n) + this.tainted = true this.scenery.media.keyframe = n }, setAutoplay: function(){ var checked = this.$autoplay.prop('checked') this.scenery.media.autoplay = checked + this.tainted = true if (checked && this.scenery.paused()) { this.togglePaused() } @@ -111,17 +129,20 @@ var MediaEditor = FormView.extend({ setLoop: function(){ var checked = this.$loop.prop('checked') this.scenery.setLoop(checked) + this.tainted = true }, setMute: function(){ var checked = this.$mute.prop('checked') this.scenery.media.mute = checked this.scenery.mute(checked) + this.tainted = true }, setDimensions: function(){ if (! this.scenery) return this.$width.unitVal( Number(this.scenery.naturalDimensions.a * this.scenery.scale) || "" ) this.$height.unitVal( Number(this.scenery.naturalDimensions.b * this.scenery.scale) || "" ) + this.tainted = true }, changeWidth: function(e){ e.stopPropagation() @@ -137,6 +158,11 @@ var MediaEditor = FormView.extend({ app.units = this.$units.val() this.$('.units').resetUnitVal() }, + + taint: function(e){ + e.stopPropagation() + this.tainted = true + }, bind: function(scenery){ this.scenery = scenery @@ -146,24 +172,29 @@ var MediaEditor = FormView.extend({ unbind: function(){ if (this.scenery) { - this.scenery.media.title = this.$name.val() - this.scenery.media.description = this.$description.val() - Minotaur.watch( app.router.editorView.settings ) - + this.scenery.focused = false + if (this.tainted && this.scenery.media) { + this.scenery.media.title = this.$name.val() + this.scenery.media.description = this.$description.val() + Minotaur.watch( app.router.editorView.settings ) + } if (this.scenery.mx) { this.scenery.mx.bound = false this.scenery.mx.el.classList.remove("picked") } } + this.tainted = false this.scenery = null }, destroy: function(){ - ConfirmModal.confirm("Are you sure you want delete to this media?", function(){ - var scenery = this.scenery - this.hide() - Scenery.remove(scenery.id) - }.bind(this)) + var scenery = this.scenery + this.hide() + + scenery.remove() + + this.tainted = false + this.scenery = null }, }) diff --git a/public/assets/javascripts/ui/editor/MediaTumblr.js b/public/assets/javascripts/ui/editor/MediaTumblr.js new file mode 100644 index 0000000..47419ae --- /dev/null +++ b/public/assets/javascripts/ui/editor/MediaTumblr.js @@ -0,0 +1,53 @@ + +var MediaTumblr = ModalView.extend({ + el: "#tumblrUpload", + + events: { + 'mousedown': "stopPropagation", + "keydown .url": "enterSubmit", + "click .exampleTumblr": "loadExample", + }, + + initialize: function(opt){ + this.__super__.initialize.call(this) + this.parent = opt.parent + this.$url = this.$(".url") + }, + + show: function(){ + this.$el.addClass("active") + this.$url.val("") + }, + + hide: function(){ + this.$el.removeClass("active") + }, + + enterSubmit: function(e){ + e.stopPropagation() + if (e.keyCode == 13) { + e.preventDefault() + var url = this.$tumblrUrl.val() + this.loadTumblr(url) + } + }, + + loadTumblr: function(url){ + Parser.tumblr(url, function(media_list){ + console.log(media_list) + this.parent.mediaViewer.$foundMediaContainer.empty() + media_list.reverse().forEach(function(media){ + this.parent.mediaViewer.add(media, this.parent.mediaViewer.$foundMediaContainer) + }.bind(this)) + }.bind(this)) + }, + + loadExample: function(e){ + e.preventDefault() + var name = $(e.currentTarget).html() + var url = "http://" + name + ".tumblr.com/" + this.$url.val(url) + this.loadTumblr(url) + }, + +})
\ No newline at end of file diff --git a/public/assets/javascripts/ui/editor/MediaUpload.js b/public/assets/javascripts/ui/editor/MediaUpload.js index 92cf2bd..d09e38c 100644 --- a/public/assets/javascripts/ui/editor/MediaUpload.js +++ b/public/assets/javascripts/ui/editor/MediaUpload.js @@ -1,11 +1,12 @@ var MediaUpload = UploadView.extend({ - el: ".fileUpload", + el: "#fileUpload", createAction: "/api/media/new", uploadAction: "/api/media/upload", events: { + 'mousedown': "stopPropagation", "keydown .url": "enterSubmit", }, @@ -25,6 +26,7 @@ var MediaUpload = UploadView.extend({ }, enterSubmit: function(e){ + e.stopPropagation() if (e.keyCode == 13) { e.preventDefault() this.parse() @@ -34,7 +36,10 @@ var MediaUpload = UploadView.extend({ parse: function(){ var url = this.$url.val() this.$url.val("") - + this.parseUrl(url) + }, + + parseUrl: function(url){ Parser.parse(url, function(media){ if (! media) { alert("Not a valid image/video link") @@ -42,7 +47,6 @@ var MediaUpload = UploadView.extend({ } media._csrf = $("[name=_csrf]").val() - console.log(media) var request = $.ajax({ type: "post", @@ -52,10 +56,15 @@ var MediaUpload = UploadView.extend({ request.done(this.add.bind(this)) }.bind(this)) }, - + add: function(media){ console.log(media) - this.parent.mediaViewer.add(media) + this.parent.mediaViewer.addUploadedMedia(media) + }, + + error: function(error){ + console.log(error) + alert(error.errors.media.message) }, beforeUpload: function(){ diff --git a/public/assets/javascripts/ui/editor/MediaViewer.js b/public/assets/javascripts/ui/editor/MediaViewer.js index 436c0cb..2ae5104 100644 --- a/public/assets/javascripts/ui/editor/MediaViewer.js +++ b/public/assets/javascripts/ui/editor/MediaViewer.js @@ -3,44 +3,82 @@ var MediaViewer = ModalView.extend({ el: ".mediaDrawer.mediaViewer", destroyAction: "/api/media/destroy", usesFileUpload: true, - + loaded: false, + perPage: 12, + offset: 0, + fixedClose: true, + events: { + 'mousedown': "stopPropagation", 'click .foundToggle': "foundToggle", 'click .userToggle': "userToggle", 'click #deleteMedia': "deleteArmed", + 'click #randomize': "randomize", 'click .mediaContainer': "pick", + 'click .viewMore': "load", + 'keydown #tumblr-url': 'enterTumblrUrl', }, initialize: function(opt){ this.__super__.initialize.call(this) this.parent = opt.parent + + this.$myMedia = this.$(".myMedia").addClass('active') + this.$myMediaContainer = this.$(".myMedia > .container") + this.$userToggle = this.$(".userToggle") + this.$foundMedia = this.$(".foundMedia") - this.$myMedia = this.$(".myMedia") + this.$foundMediaContainer = this.$(".foundMedia > .container") this.$foundToggle = this.$(".foundToggle") - this.$userToggle = this.$(".userToggle") + + this.$wallpaperMedia = this.$(".wallpaperMedia") + this.$wallpaperMediaContainer = this.$(".wallpaperMedia > .container") + this.$wallpaperToggle = this.$(".wallpaperToggle") + this.$deleteMedia = this.$("#deleteMedia") - }, + this.$viewMore = this.$(".viewMore") + this.$noMedia = this.$(".noMedia") + }, + mode: "user", + wallpaperToggle: function(){ + this.mode = "wallpaper" + this.$wallpaperMedia.addClass("active") + this.$foundMedia.addClass("inactive") + this.$myMedia.addClass("inactive").removeClass('active') + this.$("a").removeClass("active") + this.$foundToggle.addClass("active") + }, + foundToggle: function(){ - this.foundMedia.addClass("active"); - this.myMedia.addClass("inactive"); - this.$("a").removeClass("active"); - this.foundToggle.addClass("active"); + this.mode = "found" + this.$wallpaperMedia.removeClass("active") + this.$foundMedia.addClass("active") + this.$myMedia.addClass("inactive").removeClass('active') + this.$("a").removeClass("active") + this.$foundToggle.addClass("active") + this.parent.mediaUpload.hide() + this.parent.mediaTumblr.show() }, userToggle: function(){ - this.foundMedia.removeClass("active"); - this.myMedia.removeClass("inactive"); - this.$("a").removeClass("active"); - this.userToggle.addClass("active"); + this.mode = "user" + this.$wallpaperMedia.removeClass("active") + this.$foundMedia.removeClass("active") + this.$myMedia.removeClass("inactive").addClass('active') + this.$("a").removeClass("active") + this.$userToggle.addClass("active") + this.parent.mediaUpload.show() + this.parent.mediaTumblr.hide() }, show: function(){ if (! this.loaded) { this.load() + // this.loadTrending() } else { - this.__super__.show.call(this) + this.reallyShow() } }, @@ -48,24 +86,110 @@ var MediaViewer = ModalView.extend({ this.__super__.hide.call(this) this.deleteArmed(false) this.parent.mediaUpload.hide() + this.parent.mediaTumblr.hide() + this.parent.cursor.message('start') + }, + + reallyShow: function(){ + this.__super__.show.call(this) + if (this.mode == "user") { + this.userToggle() + } + else { + this.foundToggle() + } + this.parent.cursor.message("media") }, load: function(){ - $.get("/api/media/user", this.populate.bind(this)) + $.get("/api/media/user", { offset: this.offset, limit: this.perPage }, this.populate.bind(this)) + }, + +/* + loadTrending: function(){ + var trending_imagery = [ + 'https://d1ycxz9plii3tb.cloudfront.net/post_images/52ec0e20c9dc24f1d8000067/large.jpg', + 'https://d1ycxz9plii3tb.cloudfront.net/additional_images/4e6bf67bc23f490001004579/1/tall.jpg', + 'https://d1ycxz9plii3tb.cloudfront.net/additional_images/52dcca28139b2135030002a8/tall.jpg', + 'https://d1ycxz9plii3tb.cloudfront.net/additional_images/52927bb2b202a3669d000704/larger.jpg', + 'https://d1ycxz9plii3tb.cloudfront.net/additional_images/4f9f3a3ce262e60001000fb3/large.jpg', + 'http://2.bp.blogspot.com/-GD6IxUvsdOo/UdrcMFLVYNI/AAAAAAAAF2E/kbRfxMxiUlQ/s1600/okeeffe.jpg', + 'http://www.bobkessel.com/wordpress/wp-content/uploads/2009/10/moma-bob-kessel-410.jpg', + 'http://static1.artsy.net/partner_show_images/52f28f348b3b81f2fc000364/large.jpg', + 'http://static3.artsy.net/partner_show_images/52e83674c9dc24397f0000d8/large.jpg', + 'http://static0.artsy.net/partner_show_images/52d96d484b84801ef0000273/large.jpg', + 'http://static1.artsy.net/partner_show_images/52778616275b24f95c00011d/1/large.jpg', + 'http://static1.artsy.net/partner_show_images/52dc65311a1e86be6b000205/large.jpg', + ] + trending_imagery.forEach(function(url){ + var loaded = false + var img = new Image () + img.onload = function(){ + if (loaded) return + loaded = true + var media = { + type: 'image', + url: url, + width: img.naturalWidth, + height: img.naturalHeight, + } + this.add(media, this.$foundMediaContainer) + }.bind(this) + img.src = url + if (img.complete && ! loaded) { img.onload() } + }.bind(this)) }, +*/ + + randomize: function(){ + var $divs = this.$(".active .container").find(".mediaContainer").toArray() + if ($divs.length < 3) { + $divs = $divs.concat( this.$foundMediaContainer.find(".mediaContainer").toArray() ) + } + var media_objs = $divs.map(function(el){ + return $(el).data("media") + }) + Scenery.randomize.add( media_objs ) + this.hide() + }, populate: function(data){ - this.loaded = true + var scrollTop = this.loaded ? $('.myMedia .container').height() : 0 if (data && data.length) { - data.forEach(this.add.bind(this)) + if (data.length < this.perPage) { + this.$viewMore.hide() + } + data.forEach(function(media){ + this.add(media, this.$myMediaContainer) + this.offset += 1 + }.bind(this)) + this.$noMedia.hide() + this.$deleteMedia.show() } else { + this.$viewMore.hide() + this.$noMedia.show() this.$deleteMedia.hide() } - this.__super__.show.call(this) + if (this.loaded) { + this.$el.delay(300).animate({ scrollTop: scrollTop }, 200) + } + else { + this.loaded = true + this.reallyShow() + } + }, + + addUploadedMedia: function(media){ + this.$deleteMedia.show() + this.$noMedia.hide() + this.add(media, this.$myMedia, true) // prepend + this.userToggle() + this.$el.scrollTop(0) + this.offset += 1 }, - add: function(media){ + add: function(media, $container, prepend){ var image = new Image () var $span = $("<span>") $span.addClass("mediaContainer") @@ -87,13 +211,17 @@ var MediaViewer = ModalView.extend({ image.src = media.url image.load() break + + case 'soundcloud': + image.src = media.thumbnail + break } $span.data("media", media) $span.append(image) - this.$(".myMedia").prepend($span) - this.$deleteMedia.show() + if (prepend) $container.prepend($span) + else $container.append($span) }, deleteIsArmed: false, @@ -131,35 +259,23 @@ var MediaViewer = ModalView.extend({ if ($(".myMedia .mediaContainer").length == 0) { this.$deleteMedia.hide() + this.$noMedia.show() this.deleteArmed(false) } return } -// else { -// this.picked = {} -// this.picked.media = media -// this.picked.image = image -// } -// }, -// -// drag: function(e){ -// if (! this.pickedMedia) return -// var media = this.picked.media -// var image = this.picked.image -// this.picked = null - this.hide() - + this.parent.cursor.message('addmedia') + var $ants = $('.ants'); var $floatingImg = $('.floatingImg'); Scenery.nextMedia = media -// console.log(media.type) - switch (media.type) { case "video": + case "soundcloud": $floatingImg.attr('src', '/assets/img/playbutton.png') break @@ -185,12 +301,15 @@ var MediaViewer = ModalView.extend({ $floatingImg.attr('src', '') $(window).off('mousemove', _followCursor) $(window).off('mousedown', _hideCursor) + app.off('cancel-scenery', _hideCursor) $floatingImg.parent().removeClass('edit') + app.controller.cursor.message('resize') } $(window).on('mousemove', _followCursor) $(window).on('mousedown', _hideCursor) + app.on('cancel-scenery', _hideCursor) $ants.addClass('edit') _followCursor(e) }, - + }) diff --git a/public/assets/javascripts/ui/editor/Presets.js b/public/assets/javascripts/ui/editor/Presets.js new file mode 100644 index 0000000..5f5ac35 --- /dev/null +++ b/public/assets/javascripts/ui/editor/Presets.js @@ -0,0 +1,122 @@ +var Presets = View.extend({ + el: "#presets", + + events: { + "mousedown": "stopPropagation", + "click .presets span": "selectPreset", + }, + + presets: { + wireframe: { + wall: [255,255,255], + outline: [0,0,0], + floor: [246,246,246], + ceiling: [255,255,255], + }, + shaded: { + wall: [205,205,204], + outline: [0,0,0], + floor: [109,116,106], + ceiling: [159,163,157], + background: [109,116,106], + }, + "P.Funk": { + wall: [255,63,78], + outline: [255,246,0], + floor: [255,255,0], + ceiling: [225,118,252], + }, + inverse: { + wall: [0,0,0], + outline: [255,255,255], + floor: [0,0,0], + ceiling: [0,0,0], + }, + matrix: { + wall: { src: "http://dumpfm.s3.amazonaws.com/images/20130225/1361818675427-dumpfm-melipone-matrixremixtransfast.gif", scale: 4.0, color: [0,0,0] }, + outline: [0,0,0], + floor: [10,15,10], + ceiling: [0,0,0], + }, + }, + + initialize: function(opt){ + this.parent = opt.parent + + this.$presets = this.$(".presets") + _.keys(this.presets).forEach(function(name){ + var $swatch = $("<span>") + $swatch.html(capitalize(name)) + $swatch.data('preset', name) + this.$presets.append($swatch) + }.bind(this)) + }, + + modified: true, + lastPreset: "wireframe", + + toggle: function(state){ + this.$el.toggleClass("active", state) + this.parent.cursor.message(state ? "presets" : "start") + if (this.modified) { + this.$(".active").removeClass('active') + } + }, + + show: function(){ + this.toggle(true) + }, + + hide: function(){ + this.toggle(false) + }, + + selectPreset: function(e){ + var preset = $(e.currentTarget).data('preset') + if (! this.presets[preset]) return + this.$(".active").removeClass('active') + $(e.currentTarget).addClass('active') + if (this.modified) { + UndoStack.push({ + type: "choose-preset", + undo: { walls: Walls.serialize(), colors: Walls.copyColors(Walls.colors) }, + redo: preset, + }) + Minotaur.watch( app.router.editorView.settings ) + } + else { + UndoStack.push({ + type: "choose-another-preset", + undo: this.lastPreset, + redo: preset, + }) + Minotaur.watch( app.router.editorView.settings ) + } + this.lastPreset = preset + this.load(this.presets[preset]) + this.modified = false + }, + + loadByName: function(name){ + var preset = this.presets[name] + this.load(preset) + }, + load: function(preset){ + this.parent.colorControl.modes.forEach(function(mode){ + var color + if (! preset[mode].length) { + Walls.setWallpaper[mode](preset[mode]) + color = preset[mode].color + } + else { + Walls.clearWallpaper[mode]() + color = preset[mode] + } + Walls.setColor[mode](color) + this.parent.colorControl.$swatch[ mode ].css("background-color", rgb_string(color)) + }.bind(this)) + this.parent.colorControl.setMode(preset.wall.color ? "wall" : "floor") + Walls.setBodyColor() + }, + +})
\ No newline at end of file diff --git a/public/assets/javascripts/ui/editor/SculptureEditor.js b/public/assets/javascripts/ui/editor/SculptureEditor.js new file mode 100644 index 0000000..953260c --- /dev/null +++ b/public/assets/javascripts/ui/editor/SculptureEditor.js @@ -0,0 +1,237 @@ + +var SculptureEditor = FormView.extend({ + el: "#sculptureEditor", + + events: { + "keydown": 'taint', + "focus [name]": "clearMinotaur", + "click [data-role=play-media]": "togglePaused", + "mousedown [name=keyframe]": "stopPropagation", + "mousedown": "stopPropagation", + "change [name=keyframe]": "seek", + "change [name=autoplay]": "setAutoplay", + "change [name=billboard]": "setBillboard", + "change [name=outline]": "setOutline", + "change [name=outlineColor]": "setOutlineColor", + "change [name=loop]": "setLoop", + "change [name=mute]": "setMute", + "change [name=width]": 'changeWidth', + "change [name=height]": 'changeHeight', + "change [name=depth]": 'changeDepth', + "change [name=units]": 'changeUnits', + "click [data-role=destroy-sculpture]": "destroy", + }, + + initialize: function(opt){ + this.parent = opt.parent + this.__super__.initialize.call(this) + + this.$name = this.$("[name=name]") + this.$description = this.$("[name=description]") + + this.$billboard = this.$("[name=billboard]") + this.$outline = this.$("[name=outline]") + this.$outlineColor = this.$("[name=outlineColor]") + + // image fields + this.$width = this.$("[name=width]") + this.$height = this.$("[name=height]") + this.$depth = this.$("[name=depth]") + this.$units = this.$("[name=units]") + + // video fields + this.$playButton = this.$("[data-role=play-media]") + this.$autoplay = this.$("[name=autoplay]") + this.$loop = this.$("[name=loop]") + this.$mute = this.$("[name=mute]") + this.$keyframe = this.$("[name=keyframe]") + }, + + toggle: function(state) { + if (state) { + this.parent.settings.toggle() + } + this.$el.toggleClass("active", state); + }, + + togglePaused: function(state){ + var state = this.sculpture.toggle(state) + this.$playButton.toggleClass("paused", ! state) + }, + + pick: function(sculpture) { + if (this.sculpture && sculpture !== this.sculpture) { + this.unbind() + } + + this.bind(sculpture) + this.$el.addClass("active") + +// app.controller.toolbar.resetMode() + app.controller.toolbar.resetControls() + Sculpture.resize.show(sculpture) + Sculpture.hovering = true + + var media = sculpture.media + + // console.log(media) + this.$name.val(media.title || "") // || filenameFromUrl(media.url) ) + this.$description.val(media.description || "") + this.setDimensions() + this.$units.val( "ft" ) + + this.$outline.prop( 'checked', !! sculpture.outline ) + this.$outlineColor.val( sculpture.outlineColor || "#000000" ) + this.$billboard.prop( 'checked', !! sculpture.billboard ) + + switch (media.type) { + case "image": + this.$(".video").hide() + this.$(".audio").hide() + this.$(".image").show() + break + + case "youtube": + case "vimeo": + case "video": + this.$(".image").hide() + this.$(".audio").hide() + this.$(".video").show() + + this.$playButton.toggleClass("paused", ! this.sculpture.paused()) + this.$autoplay.prop('checked', !! media.autoplay) + this.$loop.prop('checked', !! media.loop) + this.$mute.prop('checked', !! media.mute) + this.$keyframe.val( Number(media.keyframe || 0) ) + break + + case "soundcloud": + this.$(".image").hide() + this.$(".video").hide() + this.$(".audio").show() + this.$playButton.toggleClass("paused", ! this.sculpture.paused()) + this.$autoplay.prop('checked', !! media.autoplay) + this.$loop.prop('checked', !! media.loop) + break + } + }, + + hide: function(sculpture){ + if (this.sculpture) { + this.unbind() + } + this.toggle(false) + }, + + seek: function(){ + var n = parseFloat( this.$keyframe.val() ) + this.sculpture.seek(n) + this.tainted = true + + this.sculpture.media.keyframe = n + }, + setAutoplay: function(){ + var checked = this.$autoplay.prop('checked') + this.sculpture.media.autoplay = checked + this.tainted = true + if (checked && this.sculpture.paused()) { + this.togglePaused() + } + }, + setLoop: function(){ + var checked = this.$loop.prop('checked') + this.sculpture.setLoop(checked) + this.tainted = true + }, + setMute: function(){ + var checked = this.$mute.prop('checked') + this.sculpture.media.mute = checked + this.sculpture.mute(checked) + this.tainted = true + }, + + setBillboard: function(){ + var checked = this.$billboard.prop('checked') + this.sculpture.setBillboard(checked) + this.tainted = true + }, + setOutline: function(){ + var checked = this.$outline.prop('checked') + this.sculpture.setOutline(checked) + this.tainted = true + }, + setOutlineColor: function(){ + var color = this.$outlineColor.val() + this.sculpture.setOutlineColor(color) + this.tainted = true + }, + + setDimensions: function(){ + if (! this.sculpture) return + this.$width.unitVal( Number(this.sculpture.naturalDimensions.a * this.sculpture.scale) || "" ) + this.$height.unitVal( Number(this.sculpture.naturalDimensions.b * this.sculpture.scale) || "" ) + this.$depth.unitVal( Number(this.sculpture.naturalDimensions.c * this.sculpture.scale) || "" ) + this.tainted = true + }, + changeWidth: function(e){ + e.stopPropagation() + this.sculpture.set_scale( this.$width.unitVal() / this.sculpture.naturalDimensions.a ) + this.setDimensions() + this.sculpture.updateOutline() + }, + changeHeight: function(e){ + e.stopPropagation() + this.sculpture.set_scale( this.$height.unitVal() / this.sculpture.naturalDimensions.b ) + this.setDimensions() + this.sculpture.updateOutline() + }, + changeDepth: function(e){ + e.stopPropagation() + this.sculpture.set_depth( this.$depth.unitVal() ) + this.$depth.unitVal( Number(this.sculpture.naturalDimensions.c * this.sculpture.scale) || "" ) + this.sculpture.updateOutline() + }, + changeUnits: function(){ + app.units = this.$units.val() + this.$('.units').resetUnitVal() + }, + + taint: function(e){ + e.stopPropagation() + this.tainted = true + }, + + bind: function(sculpture){ + this.sculpture = sculpture + this.sculpture.mx.bound = true + this.sculpture.mx.el.classList.add("picked") + }, + + unbind: function(){ + if (this.sculpture) { + this.sculpture.focused = false + if (this.tainted && this.sculpture.media) { + this.sculpture.media.title = this.$name.val() + this.sculpture.media.description = this.$description.val() + Minotaur.watch( app.router.editorView.settings ) + } + if (this.sculpture.mx) { + this.sculpture.mx.bound = false + this.sculpture.mx.el.classList.remove("picked") + } + } + this.tainted = false + this.sculpture = null + }, + + destroy: function(){ + var sculpture = this.sculpture + this.hide() + + sculpture.remove() + + this.tainted = false + this.sculpture = null + }, + +}) diff --git a/public/assets/javascripts/ui/editor/TextEditor.js b/public/assets/javascripts/ui/editor/TextEditor.js new file mode 100644 index 0000000..53d5b9f --- /dev/null +++ b/public/assets/javascripts/ui/editor/TextEditor.js @@ -0,0 +1,238 @@ + +var TextEditor = FormView.extend({ + el: "#textEditor", + tainted: false, + scenery: null, + + events: { + "keydown": 'taint', + "focus [name]": "clearMinotaur", + "mousedown": "stopPropagation", + "change [name=font-family]": 'changeFontFamily', + "change [name=font-size]": 'changeFontSize', + "change [name=text-align]": 'changeTextAlign', + "click .swatch": 'showColorPicker', + "click [data-role=hide-color-picker]": 'hideColorPicker', + "click [data-role=hide-text-editor]": 'hide', + "input [name=text-body]": 'changeText', + "click [data-role=destroy-text]": "destroy", + "click .colors span": "setHue", + }, + + initialize: function(opt){ + this.parent = opt.parent + this.__super__.initialize.call(this) + + this.$textSettings = this.$(".text-setting") + this.$colorSettings = this.$(".color-setting") + this.$noTextMessage = this.$(".no-text") + this.$fontFamily = this.$("[name=font-family]") + this.$fontSize = this.$("[name=font-size]") + this.$textBody = this.$("[name=text-body]") + this.$textAlign = this.$("[name=text-align]") + this.$swatch = this.$(".swatch") + + this.colorPicker = new LabColorPicker(this, 155, 155) + this.$(".color-picker").append( this.colorPicker.canvas ) + this.$(".color-picker").append( this.colorPicker.cursor ) + this.$(".slider").append( this.colorPicker.brightness ) + + this.$colors = this.$(".colors") + this.parent.colorControl.colors.forEach(function(color){ + var $swatch = $("<span>") + $swatch.css("background-color","rgb(" + color + ")") + $swatch.data('color', color) + this.$colors.append($swatch) + }.bind(this)) + + this.$(".setting").hide() + + app.on("cancel-scenery", function(){ + this.createMode(true) + $("body").toggleClass("addText", false) + }.bind(this)) + + }, + + toggle: function(state){ + this.$el.toggleClass("active", state) + if (state) { + $("#keyhint").fadeOut(200) + Scenery.nextMedia = { + type: 'text', + width: 600, + height: 450, + scale: 0.5, + font: { family: 'Lato', size: 24, align: 'left', color: "#000" }, + } + this.createMode(true) + } + else { + $("[data-role='toggle-text-editor']").removeClass("inuse") + } + }, + + hide: function(scenery){ + Scenery.nextMedia = null + if (this.scenery) { + this.unbind() + } + Scenery.resize.hide() + this.toggle(false) + }, + + taint: function(e){ + e.stopPropagation() + this.tainted = true + }, + + bind: function(scenery){ + this.tainted = false + this.scenery = scenery + this.scenery.mx.bound = true + this.scenery.mx.el.classList.add("picked") + this.scenery.media.font.color = this.scenery.media.font.color || [0,0,0] + }, + + unbind: function(){ + if (this.scenery) { + this.scenery.focused = false + if (this.tainted) { + Minotaur.watch( app.router.editorView.settings ) + } + if (this.scenery.mx) { + this.scenery.mx.bound = false + this.scenery.mx.el.classList.remove("picked") + } + if (! this.scenery.media || ! this.scenery.media.description || this.scenery.media.description == "") { + this.scenery.remove() + } + } + this.tainted = false + this.scenery = null + }, + + createMode: function(state){ + this.hideColorPicker() + this.$textSettings.toggle(! state) + this.$noTextMessage.toggle(!! state) + $("body").toggleClass("addText", !! state) + }, + + pick: function(scenery){ + if (this.scenery && scenery !== this.scenery) { + this.unbind() + } + + this.parent.settings.hide() + Scenery.resize.show(scenery) + Scenery.hovering = true + + this.bind(scenery) + this.$el.toggleClass("active", true) + this.$textBody.val( this.scenery.media.description ) + + this.$fontFamily.val( this.scenery.media.font.family ) + this.$fontSize.val( this.scenery.media.font.size ) + this.$textAlign.val( this.scenery.media.font.align ) + this.setSwatchColor( this.scenery.media.font.color ) + + this.createMode(false) + + if (! this.scenery.media.description) { + setTimeout(function(){ + this.$textBody.focus() + }.bind(this), 100) + } + }, + + taint: function(e){ + e.stopPropagation() + }, + + changeFontFamily: function(){ + this.scenery.setFont({ family: this.$fontFamily.val() }) + }, + + changeTextAlign: function(){ + this.scenery.setFont({ align: this.$textAlign.val() }) + }, + + changeFontSize: function(){ + var size = parseInt( this.$fontSize.val() ) + size && this.scenery.setFont({ size: size }) + }, + + changeText: function(e){ + e.stopPropagation() + var text = this.$textBody.val() + this.scenery.setText(text) + }, + + destroy: function(){ + this.tainted = false + this.scenery.remove() + this.hide() + }, + + setSwatchColor: function(rgb){ + this.$swatch.css("background-color", rgb_string(rgb)) + }, + showColorPicker: function(){ + this.$textSettings.hide() + this.$colorSettings.show() + + var color = this.scenery.media.font.color + this.labColor = this.colorPicker.load(color) + this.pickColor(color, this.labColor) + + this.$el.addClass("color-mode") + }, + + hideColorPicker: function(e){ + e && e.preventDefault() + this.$textSettings.show() + this.$colorSettings.hide() + this.$el.removeClass("color-mode") + }, + + pickColor: function(rgb, Lab){ + this.labColor = Lab + this.setSwatchColor(rgb) + this.scenery.setFont({ color: rgb }) + this.tainted = true + }, + + setHue: function(e){ + var color = $(e.currentTarget).data('color') + this.labColor = this.colorPicker.load(color) + this.pickColor(color, this.labColor) + }, + + initialState: null, + + begin: function(){ + // this.initialState = this.serialize() + }, + + serialize: function(){ + return { + rgb: Walls.colors[ this.mode ] + } + }, + + finalize: function(){ + if (! this.initialState) { return } + UndoStack.push({ + type: 'update-colors', + undo: this.initialState, + redo: this.serialize(), + }) + + this.initialState = null + + // TODO: watch individual wall object here + Minotaur.watch( app.router.editorView.settings ) + }, + +}) diff --git a/public/assets/javascripts/ui/editor/WallpaperPicker.js b/public/assets/javascripts/ui/editor/WallpaperPicker.js index 0dd2921..3640d6d 100644 --- a/public/assets/javascripts/ui/editor/WallpaperPicker.js +++ b/public/assets/javascripts/ui/editor/WallpaperPicker.js @@ -3,34 +3,57 @@ var WallpaperPicker = UploadView.extend({ el: ".wallpaper", mediaTag: "wallpaper", + createAction: "/api/media/new", uploadAction: "/api/media/upload", + destroyAction: "/api/media/destroy", events: { + "contextmenu": 'contextmenu', + "mousedown": 'stopPropagation', "click .swatch": 'pick', "click .wallpaperRemove": 'remove', + "input [data-role='wallpaper-scale']": 'updateScale', + "change .url": "enterUrl", + "keydown .url": "enterSetUrl", }, - initialize: function(){ + initialize: function(opt){ + this.parent = opt.parent this.__super__.initialize.call(this) this.$swatches = this.$(".swatches") this.$remove = this.$(".wallpaperRemove") - this.$remove.hide() + + this.$url = this.$(".url") + + this.$position = this.$("[data-role='wallpaper-position']") + this.$scale = this.$("[data-role='wallpaper-scale']") + + this.$wallpaperResizeControls = this.$(".wallpaperResizeControls") + this.$wallpaperResizeControls.addClass('disabled') + + this.initializePositionCursor() }, loaded: false, show: function(){ - if (! this.loaded) { - this.load() - } - else { - this.toggle(true) - } + this.toggle(true) }, - hide: function(){ - this.__super__.hide.call(this) + this.toggle(false) }, - + + toggle: function (state) { + Scenery.nextWallpaper = null + app.tube('cancel-wallpaper') + this.$el.toggleClass("active", state) + if (state) { + this.parent.cursor.message("wallpaper") + if (! this.loaded) { + this.load() + } + } + }, + load: function(){ $.get("/api/media/user", { tag: this.mediaTag }, this.populate.bind(this)) }, @@ -39,49 +62,98 @@ var WallpaperPicker = UploadView.extend({ this.loaded = true if (data && data.length) { data.forEach(this.add.bind(this)) + this.$(".txt").hide() + } + else { + this.$(".txt").show() } this.toggle(true) }, + seenWallpapers: {}, add: function (media) { if (media.type !== "image") { return } + if (this.seenWallpapers[ media.url ]) { return } var swatch = document.createElement("div") swatch.className = "swatch" swatch.style.backgroundImage = "url(" + media.url + ")" + swatch.setAttribute("data-id", media._id) this.$swatches.append(swatch) this.$swatches.show() - }, - - toggle: function (state) { - if (state && ! this.loaded) { - this.show() - } - else { - this.$el.toggleClass("active", state) - } - // toggle the class that makes the cursor a paintbucket - // $("body").removeClass("pastePaper") + this.$(".txt").hide() + this.seenWallpapers[ media.url ] = true }, - hide: function(){ - this.toggle(false) + addUrl: function (url){ + Parser.loadImage(url, function(media){ + if (! media) return + media._csrf = $("[name=_csrf]").val() + media.tag = this.mediaTag + + var request = $.ajax({ + type: "post", + url: this.createAction, + data: media, + }) + request.done(this.add.bind(this)) + + }.bind(this)) }, - + beforeUpload: function(){ }, pick: function(e){ var $swatch = $(e.currentTarget) - this.follow( e, $swatch.css('background-image') ) - this.$remove.show() + if (Scenery.nextWallpaper == "none") { + var _id = $swatch[0].getAttribute("data-id") + $swatch.remove() + this.destroy(_id, function(){}) + } + else { + app.tube('cancel-wallpaper') + this.follow( e, $swatch.css('background-image') ) + this.parent.presets.modified = true + } }, remove: function(e){ - this.follow( e, "none" ) - $(".floatingSwatch").addClass("scissors") + if (Scenery.nextWallpaper) { + // remove red class to the wallpaper + Scenery.nextWallpaper = null + app.tube('cancel-wallpaper') + } + else { + // add red class to the wallpaper + this.follow( e, "none" ) + $(".floatingSwatch").addClass("scissors") + this.$el.addClass("deleteArmed") + } + }, + + destroy: function(_id, cb){ + $.ajax({ + type: "delete", + url: this.destroyAction, + data: { _id: _id, _csrf: $("[name=_csrf]").val() } + }).complete(cb || function(){}) + }, + + contextmenu: function(e){ + if (Scenery.nextWallpaper) { + e.preventDefault() + this.cancel() + } + }, + cancel: function(){ + if (Scenery.nextWallpaper) { + Scenery.nextWallpaper = null + app.tube('cancel-wallpaper') + } }, follow: function(e, wallpaper, icon){ + var base = this icon = icon || wallpaper var $floatingSwatch = $(".floatingSwatch") @@ -90,6 +162,8 @@ var WallpaperPicker = UploadView.extend({ Scenery.nextWallpaper = wallpaper + $(".floodMessage").show() + setTimeout(function(){ function _followCursor(e) { $floatingSwatch.css({ @@ -97,14 +171,105 @@ var WallpaperPicker = UploadView.extend({ left: (e.pageX + 10) + 'px', }); } - $(window).on('mousemove', _followCursor) - $(window).one('click', function () { + function _hideCursor (e) { + $(window).off('keydown', _floodRoom) $(window).off('mousemove', _followCursor) + app.off('cancel-wallpaper', _hideCursor) $floatingSwatch.removeClass("scissors").hide() - }); + $(".floodMessage").hide() + base.$el.removeClass("deleteArmed") + } + function _floodRoom (e) { + if (e.keyCode == 13) { + base.flood() + } + } + $(window).on('keydown', _floodRoom) + $(window).on('mousemove', _followCursor) + // $(window).one('click', _hideCursor); + app.on('cancel-wallpaper', _hideCursor) $floatingSwatch.show() _followCursor(e); }) }, + wall: null, + pickWall: function(wall){ + if (! wall.background || wall.background.src == "none") { + this.$wallpaperResizeControls.addClass('disabled') + this.$scale.val( 0.0 ) + return; + } + this.$wallpaperResizeControls.removeClass('disabled') + this.wall = wall + this.$scale.val( Math.log( this.wall.background.scale ) ) + }, + + scaleTimeout: null, + updateScale: function(){ + if (! this.wall) return; + var scale = Math.exp( parseFloat(this.$scale.val()) ) + this.wall.wallpaperPosition({ scale: scale }) + + clearTimeout(this.scaleTimeout) + this.scaleTimeout = setTimeout(function(){ + + // TODO: watch individual scenery object here + Minotaur.watch( app.router.editorView.settings ) + + }.bind(this), 500) + }, + + enterUrl: function(){ + var url = this.$url.sanitize() + this.addUrl(url) + this.$url.val("") + }, + enterSetUrl: function (e) { + e.stopPropagation() + if (e.keyCode == 13) { + setTimeout(this.enterUrl.bind(this), 100) + } + }, + + flood: function(url){ + url = url || Scenery.nextWallpaper + if (! url) return + Walls.setWallpaper.wall({ src: url }) + Walls.setWallpaper.floor({ src: url }) + Walls.setWallpaper.ceiling({ src: url }) + this.cancel() + }, + + initializePositionCursor: function(){ + var base = this + var dx = 0, dy = 0, dragging = false, delta + var x = 0, y = 0, s = 1 + var mymouse = new mouse({ + el: this.$position[0], + down: function(e, cursor){ + if (! base.wall) return + dragging = true + // s = parseFloat( base.$scale.val() ) + x = base.wall.background.x + y = base.wall.background.y + }, + drag: function(e, cursor){ + if (! dragging) return + delta = cursor.delta() + delta.a = - delta.a + dx = delta.a*s + dy = delta.b*s + base.wall.wallpaperPosition({ + // scale: s, + x: x+dx, + y: y+dy, + }) + }, + up: function(e, cursor, new_cursor){ + dragging = false + }, + }) + }, + }) diff --git a/public/assets/javascripts/ui/lib/AlertModal.js b/public/assets/javascripts/ui/lib/AlertModal.js index 1b0f40f..a4bf241 100644 --- a/public/assets/javascripts/ui/lib/AlertModal.js +++ b/public/assets/javascripts/ui/lib/AlertModal.js @@ -1,25 +1,25 @@ var AlertModal = new( ModalFormView.extend({ - el: ".mediaDrawer.alert", + el: ".mediaDrawer.alert", - events: { - "click .ok": "advance", - "click .close": "advance", - }, - - alert: function(message, callback){ - this.$(".message").empty().append(message) - this.callback = callback - this.show() - this.$(".ok").focus() - }, - - advance: function(e){ - e && e.preventDefault() - this.hide() - this.callback && this.callback() - this.callback = null - } + events: { + "click .ok": "advance", + "click .close": "advance", + }, + + alert: function(message, callback){ + this.$(".message").empty().append(message) + this.callback = callback + this.show() + this.$(".ok").focus() + }, + + advance: function(e){ + e && e.preventDefault() + this.hide() + this.callback && this.callback() + this.callback = null + } })) diff --git a/public/assets/javascripts/ui/lib/AnimatedView.js b/public/assets/javascripts/ui/lib/AnimatedView.js new file mode 100644 index 0000000..3c50b0a --- /dev/null +++ b/public/assets/javascripts/ui/lib/AnimatedView.js @@ -0,0 +1,31 @@ +var AnimatedView = View.extend({ + + _animating: false, + last_t: 0, + + startAnimating: function(){ + if (this._animating) return + this._animating = true + this._animate() + }, + + stopAnimating: function(){ + this._animating = false + }, + + _animate: function(t){ + if (! this._animating) return + + requestAnimationFrame(this._animate.bind(this)) + + var dt = t - this.last_t + this.last_t = t + + if (! t) return + + this.animate(t, dt) + }, + + animate: function(t, dt){}, + +})
\ No newline at end of file diff --git a/public/assets/javascripts/ui/lib/ConfirmModal.js b/public/assets/javascripts/ui/lib/ConfirmModal.js index 01720bb..7d9da67 100644 --- a/public/assets/javascripts/ui/lib/ConfirmModal.js +++ b/public/assets/javascripts/ui/lib/ConfirmModal.js @@ -1,24 +1,34 @@ var ConfirmModal = new( ModalFormView.extend({ - el: ".mediaDrawer.confirm", + el: ".mediaDrawer.confirm", - events: { - "click .yes": "advance", - "click .no": "hide", - }, - - confirm: function(question, callback){ - this.$(".question").empty().append(question) - this.callback = callback - this.show() - }, - - advance: function(e){ - e && e.preventDefault() - this.hide() - this.callback && this.callback() - this.callback = null - } + events: { + "click .yes": "agree", + "click .no": "cancel", + }, + + confirm: function(question, agreeCallback, cancelCallback){ + this.$(".question").empty().append(question) + this.agreeCallback = agreeCallback + this.cancelCallback = cancelCallback + this.show() + }, + + agree: function(e){ + e && e.preventDefault() + this.hide() + this.agreeCallback && this.agreeCallback() + this.agreeCallback = null + this.cancelCallback = null + }, + + cancel: function(e){ + e && e.preventDefault() + this.hide() + this.cancelCallback && this.cancelCallback() + this.agreeCallback = null + this.cancelCallback = null + } }) )
\ No newline at end of file diff --git a/public/assets/javascripts/ui/lib/ErrorModal.js b/public/assets/javascripts/ui/lib/ErrorModal.js index 8b01077..cfc2e6d 100644 --- a/public/assets/javascripts/ui/lib/ErrorModal.js +++ b/public/assets/javascripts/ui/lib/ErrorModal.js @@ -1,26 +1,26 @@ var ErrorModal = new( ModalFormView.extend({ - el: ".mediaDrawer.error", + el: ".mediaDrawer.error", - events: { - "click .ok": "advance", - "click .close": "advance", - }, - - alert: function(message, callback){ - this.$(".errorList").empty().append(message) - this.callback = callback - this.show() - this.$(".ok").focus() - }, - - advance: function(e){ - e && e.preventDefault() - this.hide() - this.callback && this.callback() - this.callback = null - } + events: { + "click .ok": "advance", + "click .close": "advance", + }, + + alert: function(message, callback){ + this.$(".errorList").empty().append(message) + this.callback = callback + this.show() + this.$(".ok").focus() + }, + + advance: function(e){ + e && e.preventDefault() + this.hide() + this.callback && this.callback() + this.callback = null + } })) diff --git a/public/assets/javascripts/ui/lib/FormView.js b/public/assets/javascripts/ui/lib/FormView.js index 17b748a..a952ecb 100644 --- a/public/assets/javascripts/ui/lib/FormView.js +++ b/public/assets/javascripts/ui/lib/FormView.js @@ -1,127 +1,138 @@ var FormView = View.extend({ - method: "post", + method: "post", + useMinotaur: false, - events: { - "submit form": "save" - }, + events: { + "submit form": "save" + }, - initialize: function(opt){ - if (opt && opt.parent) { - this.parent = opt.parent - } - this.$form = this.$("form") - this.$errors = this.$(".errors") - this.$errorList = this.$(".errorList") - }, + initialize: function(opt){ + if (opt && opt.parent) { + this.parent = opt.parent + } + this.$form = this.$("form") + this.$errors = this.$(".errors") + this.$errorList = this.$(".errorList") + }, - reset: function(){ - this.$("input,textarea").not("[type='submit']").not("[type='hidden']").val("") - }, - - showErrors: function(errors){ - if (errors && errors.length) { - this.$errorList.empty(); - for (var i in errors) { - this.$errorList.append('<div>' + errors[i] + '</div>'); - } - this.$errors.css("opacity", 1.0); - setTimeout(function(){ - this.$errors.show().css("opacity", 1.0); - }.bind(this), 200) - } - }, - - serialize: function(){ - var fd = new FormData(), hasCSRF = false - - this.$("input[name], select[name], textarea[name]").each( function(){ - if (this.type == "file") { - if (this.files.length > 0) { - fd.append(this.name, this.files[0]); - } - } - else if (this.type == "password") { - if (this.value.length > 0) { - fd.append(this.name, SHA1.hex('lol$' + this.value + '$vvalls')) - } - } - else { - fd.append(this.name, this.value); - hasCSRF = hasCSRF || this.name == "_csrf" - } - }); - - if (! hasCSRF) { + reset: function(){ + this.$("input,textarea").not("[type='submit']").not("[type='hidden']").val("") + }, + + showErrors: function(errors){ + if (errors && errors.length) { + this.$errorList.empty(); + for (var i in errors) { + this.$errorList.append('<div>' + errors[i] + '</div>'); + } + this.$errors.css("opacity", 1.0); + setTimeout(function(){ + this.$errors.show().css("opacity", 1.0); + }.bind(this), 200) + } + }, + + serialize: function(){ + var fd = new FormData(), hasCSRF = false + + this.$("input[name], select[name], textarea[name]").each( function(){ + if (this.type == "file") { + if (this.files.length > 0) { + fd.append(this.name, this.files[0]); + } + } + else if (this.type == "password") { + if (this.value.length > 0) { + fd.append(this.name, SHA1.hex('lol$' + this.value + '$vvalls')) + } + } + else { + fd.append(this.name, this.value); + hasCSRF = hasCSRF || this.name == "_csrf" + } + }); + + if (! hasCSRF) { fd.append("_csrf", $("[name=_csrf]").val()) } - - return fd - }, - - save: function(e, successCallback, errorCallback){ - e && e.preventDefault() + + return fd + }, + + save: function(e, successCallback, errorCallback){ + e && e.preventDefault() - this.$errors.hide().css("opacity", 0.0); - - if (this.validate) { - var errors = this.validate() - if (errors && errors.length) { - if (errorCallback) { - errorCallback(errors) - } - else { + this.$errors && this.$errors.hide().css("opacity", 0.0); + + if (this.validate) { + var errors = this.validate() + if (errors && errors.length) { + if (errorCallback) { + setTimeout(function(){ + errorCallback(errors) + }) + } + else { this.showErrors(errors) - } - return - } - } - - var action = typeof this.action == "function" ? this.action() : this.action - if (! action) return + } + return + } + } + var action = typeof this.action == "function" ? this.action() : this.action + if (! action) return - var request = $.ajax({ - url: action, - type: this.method, - data: this.serialize(), - dataType: "json", - processData: false, - contentType: false, - }) - - request.done($.proxy(function (response) { - if (response.error) { - var errors = [] - for (var key in response.error.errors) { - errors.push(response.error.errors[key].message); - } - if (errorCallback) { - errorCallback(errors) - } - else { + var request = $.ajax({ + url: action, + type: this.method, + data: this.serialize(), + dataType: "json", + processData: false, + contentType: false, + }) + + if (this.useMinotaur) { + Minotaur.show() + } + + request.done($.proxy(function (response) { + if (this.useMinotaur) { + Minotaur.hide() + } + if (response.error) { + var errors = [] + for (var key in response.error.errors) { + errors.push(response.error.errors[key].message); + } + if (errorCallback) { + errorCallback(errors) + } + else { this.showErrors(errors) - } - return - } - else { - if (successCallback) { - successCallback(response) - } - if (this.success) { - this.success(response) - } - } - }, this)); - } + } + return + } + else { + console.log("ok") + if (successCallback) { + console.log("use cb") + successCallback(response) + } + if (this.success) { + this.success(response) + } + } + }, this)); + } }) var ModalFormView = ModalView.extend(FormView.prototype).extend({ - load: function(){ - this.reset() - this.show() - } + load: function(){ + this.reset() + this.show() + } }) diff --git a/public/assets/javascripts/ui/editor/LightControl.js b/public/assets/javascripts/ui/lib/LabColorPicker.js index 3eb2861..2c8fb90 100644 --- a/public/assets/javascripts/ui/editor/LightControl.js +++ b/public/assets/javascripts/ui/lib/LabColorPicker.js @@ -1,145 +1,22 @@ - -var LightControl = View.extend({ - el: ".lightcontrol", - - events: { - "mousedown": "stopPropagation", - "click .color-swatches span": "select", - "input #shadow-control": "updateShadow", - "mousedown #brightness-control": "beginBrightness", - "input #brightness-control": "updateBrightness", - "input #outline-hue": "updateShadow", - "input #wall-hue": "updateShadow", - }, - - initialize: function(){ - - this.colorPicker = new LabColorPicker(this, 180, 180) - this.$("#color-picker").append( this.colorPicker.canvas ) - this.$("#color-picker").append( this.colorPicker.cursor ) - - this.$swatches = this.$(".swatch") - this.$labels = this.$(".swatch + label") - this.$swatch = { - wall: this.$("#wall-color"), - outline: this.$("#outline-color"), - floor: this.$("#floor-color"), - ceiling: this.$("#ceiling-color"), - } - this.$brightnessControl = this.$("#brightness-control") - }, - - modes: [ "wall", "outline", "floor", "ceiling" ], - - load: function(data){ - this.modes.forEach(function(mode){ - Walls.setColor[mode](data[mode]) - this.$swatch[ mode ].css("background-color", rgb_string(data[mode])) - }.bind(this)) - this.setMode("wall") - }, - - loadDefaults: function(){ - var colors = { - wall: app.defaults.colors.wall.slice(), - outline: app.defaults.colors.outline.slice(), - floor: app.defaults.colors.floor.slice(), - ceiling: app.defaults.colors.ceiling.slice(), - } - this.load(colors) - }, - - toggle: function(state){ - this.$el.toggleClass("active", state); - }, - - show: function(){ - this.toggle(true) - }, - - hide: function(){ - this.toggle(false) - }, - - pick: function(rgb, Lab){ - this.labColor = Lab - this.setSwatchColor(this.mode, rgb) - Walls.setColor[ this.mode ](rgb) - }, - - setSwatchColor: function(mode, rgb) { - this.$swatch[ mode ].css("background-color", rgb_string(rgb)) - }, - - initialState: null, - - begin: function(){ - this.initialState = this.serialize() - }, - - serialize: function(){ - return { - mode: this.mode, - rgb: Walls.colors[ this.mode ] - } - }, - - finalize: function(){ - if (! this.initialState) { return } - UndoStack.push({ - type: 'update-colors', - undo: this.initialState, - redo: this.serialize(), - }) - - this.initialState = null - - // TODO: watch individual wall object here - Minotaur.watch( app.router.editorView.settings ) - }, - - setMode: function (mode) { - var color, brightness - this.mode = mode - this.$(".active").removeClass("active") - this.$swatch[ mode ].parent().addClass("active") - color = Walls.colors[ mode ] - - this.labColor = this.colorPicker.load(color) - this.$brightnessControl.val( this.labColor[0] ) - }, - - select: function(e){ - var mode = $('.swatch', e.currentTarget).data('mode') - this.setMode(mode) - }, - - beginBrightness: function(){ - this.begin() - $(window).one("mouseup", this.finalize.bind(this)) - }, - - updateBrightness: function(){ - this.labColor[0] = parseFloat( this.$brightnessControl.val() ) - var rgb = this.colorPicker.setLab( this.labColor ) - this.pick(rgb, this.labColor) - }, - -}) - var LabColorPicker = function (parent, w, h) { var base = this var canvas = this.canvas = document.createElement('canvas') - var ctx = this.ctx = canvas.getContext('2d') - var imageData = ctx.createImageData(w,h) - var data = imageData.data + canvas.width = w + canvas.height = h + var ctx = this.ctx = canvas.getContext('2d-lodpi') +// canvas.className = "colorPicker" +// var imageData = ctx.createImageData(w, h) +// var data = imageData.data var cursor = this.cursor = document.createElement("div") cursor.className = "colorPickerCursor" - - canvas.width = w - canvas.height = h - canvas.className = "colorPicker" + + var brightnessControl = this.brightness = document.createElement("input") + var $brightnessControl = $(brightnessControl) + brightnessControl.setAttribute("type", "range") + brightnessControl.setAttribute("min", "0") + brightnessControl.setAttribute("max", "110") + brightnessControl.setAttribute("value", "0") var ww = w-1 var hh = h-1 @@ -168,6 +45,11 @@ var LabColorPicker = function (parent, w, h) { } }) + $brightnessControl.on({ + "mousedown": function(){ base.beginBrightness() }, + "input": function(){ base.updateBrightness() }, + }) + this.setLab = function(Lab) { val = Lab[0] this.paint() @@ -181,7 +63,7 @@ var LabColorPicker = function (parent, w, h) { var y = mix( j/hh, b_range[0], b_range[1] ) var rgb = xyz2rgb(hunterlab2xyz(val, x, y)).map(Math.round) this.moveCursor(i, j) - parent.pick( rgb, [val,x,y] ) + parent.pickColor( rgb, [val,x,y] ) } this.load = function(rgba){ var Lab = xyz2hunterlab(rgb2xyz(rgba)) @@ -191,6 +73,8 @@ var LabColorPicker = function (parent, w, h) { this.moveCursor(x,y) this.setLab(Lab) + this.setBrightness(Lab) + return Lab } this.moveCursor = function(x,y){ @@ -199,11 +83,14 @@ var LabColorPicker = function (parent, w, h) { } this.paint = function() { val = clamp(val, L_range[0], L_range[1]) - var x, y, t - for (var i = 0; i < w; i++) { - for (var j = 0; j < h; j++) { - x = mix( i/ww, a_range[0], a_range[1] ) - y = mix( j/hh, b_range[0], b_range[1] ) + var imageData = ctx.createImageData(canvas.width, canvas.height) + var data = imageData.data + var x, y, t, cw = imageData.width, ch = imageData.height + var cww = cw-1, chh = ch-1 + for (var i = 0; i < cw; i++) { + for (var j = 0; j < ch; j++) { + x = mix( i/cww, a_range[0], a_range[1] ) + y = mix( j/chh, b_range[0], b_range[1] ) t = (j*w + i) * 4 rgb = xyz2rgb(hunterlab2xyz(val, x, y)) data[t] = Math.round( rgb[0] ) @@ -214,6 +101,19 @@ var LabColorPicker = function (parent, w, h) { } ctx.putImageData(imageData,0,0) } + + this.beginBrightness = function(){ + parent.begin() + $(window).one("mouseup", parent.finalize.bind(parent)) + } + this.updateBrightness = function(){ + parent.labColor[0] = parseFloat( $brightnessControl.val() ) + var rgb = base.setLab( parent.labColor ) + parent.pickColor(rgb, parent.labColor) + } + this.setBrightness = function(Lab){ + $brightnessControl.val(Lab[0]) + } function hunterlab2xyz (L,a,b) { var_Y = L / 10 diff --git a/public/assets/javascripts/ui/lib/ModalView.js b/public/assets/javascripts/ui/lib/ModalView.js index d9b518a..e0070ce 100644 --- a/public/assets/javascripts/ui/lib/ModalView.js +++ b/public/assets/javascripts/ui/lib/ModalView.js @@ -1,42 +1,53 @@ var ModalView = View.extend({ - events: { - "click .close": 'close', - }, - - initialize: function(opt){ - if (opt && opt.parent) { - this.parent = opt.parent - } - }, - - usesFileUpload: false, - - show: function(){ - $(".mediaDrawer").removeClass("active") - - if (! this.usesFileUpload) { - $(".fileUpload").removeClass("active") - } - - this.$el.addClass("active") + events: { + "click .close": 'close', + }, + + initialize: function(opt){ + if (opt && opt.parent) { + this.parent = opt.parent + } + }, + + usesFileUpload: false, + + show: function(){ + $(".mediaDrawer").removeClass("active") + $(".fileUpload").removeClass("active") + + if (this.fixedClose) { + $("#fixed_close").addClass("active") + $("#fixed_close").bind("click", this.hide.bind(this)) + } + + this.$el.addClass("active") $("body").addClass("noOverflow") - }, + }, - hide: function(){ + hide: function(){ // $(".mediaDrawer, .room1").removeClass("active editing"); - this.$el.removeClass("active"); + if (this.fixedClose) { + $("#fixed_close").removeClass("active") + $("#fixed_close").unbind("click", this.hide.bind(this)) + } + this.$el.removeClass("active"); $("body").removeClass("noOverflow"); - }, - - close: function(){ - if (window.isModalView) { - window.location.pathname = "/" - } - else { - history.pushState(null, document.title, app.router.originalPath) - this.hide() - } - } + }, + + hideClose: function(){ + $("#fixed_close").removeClass("active") + $("#fixed_close").unbind("click", this.hide.bind(this)) + }, + + close: function(){ + if (window.isModalView) { + window.location.pathname = "/" + } + else { + history.pushState(null, document.title, app.router.originalPath) + this.hide() + } + } }) diff --git a/public/assets/javascripts/ui/lib/Parser.js b/public/assets/javascripts/ui/lib/Parser.js index 52c96e6..411f425 100644 --- a/public/assets/javascripts/ui/lib/Parser.js +++ b/public/assets/javascripts/ui/lib/Parser.js @@ -5,9 +5,12 @@ var Parser = { fetch: function(url, done) { var img = new Image () img.onload = function(){ + if (!img) return var width = img.naturalWidth, height = img.naturalHeight img = null done({ + url: url, + type: "image", token: "", thumbnail: "", title: "", @@ -32,6 +35,8 @@ var Parser = { var width = video.videoWidth, height = video.videoHeight video = null done({ + url: url, + type: "video", token: "", thumbnail: "", title: "", @@ -63,6 +68,8 @@ var Parser = { success: function(result){ var res = result.items[0] done({ + url: url, + type: "youtube", token: id, thumbnail: thumb, title: res.snippet.title, @@ -92,6 +99,8 @@ var Parser = { return } done({ + url: url, + type: "vimeo", token: id, thumbnail: res.thumbnail_large, title: res.title, @@ -103,10 +112,9 @@ var Parser = { }, tag: function (media) { // return '<img class="video" type="vimeo" vid="'+media.token+'" src="'+media.thumbnail+'"><span class="playvid">▶</span>'; - return '<div class="video" style="width: ' + media.width + 'px; height: ' + media.height + 'px; overflow: hidden; position: relative;"><iframe frameborder="0" scrolling="no" seamless="seamless" webkitallowfullscreen="webkitAllowFullScreen" mozallowfullscreen="mozallowfullscreen" allowfullscreen="allowfullscreen" id="okplayer" src="http://player.vimeo.com/video/' + media.token + '?api=1&js_api=1&title=0&byline=0&portrait=0&playbar=0&player_id=okplayer&loop=0&autoplay=0" width="' + media.width + '" height="' + media.height + '" style="position: absolute; top: 0px; left: 0px; width: ' + media.width + 'px; height: ' + media.height + 'px;"></iframe></div>' + return '<div class="video" style="width: ' + media.width + 'px; height: ' + media.height + 'px; overflow: hidden; position: relative;"><iframe frameborder="0" scrolling="no" seamless="seamless" webkitallowfullscreen="webkitAllowFullScreen" mozallowfullscreen="mozallowfullscreen" allowfullscreen="allowfullscreen" id="okplayer" src="http://player.vimeo.com/video/' + media.token + '?api=1&title=0&byline=0&portrait=0&playbar=0&player_id=okplayer&loop=0&autoplay=0" width="' + media.width + '" height="' + media.height + '" style="position: absolute; top: 0px; left: 0px; width: ' + media.width + 'px; height: ' + media.height + 'px;"></iframe></div>' } }, - /* { type: 'soundcloud', regex: /soundcloud.com\/[-a-zA-Z0-9]+\/[-a-zA-Z0-9]+\/?$/i, @@ -118,26 +126,33 @@ var Parser = { + '&client_id=' + '0673fbe6fc794a7750f680747e863b10', success: function(result) { + // console.log(result) done({ + url: url, + type: "soundcloud", token: result.id, - thumbnail: "", - title: "", - width: 100, - height: 100, + thumbnail: result.artwork_url || result.user.avatar_url, + title: result.user.username + " - " + result.title, + width: 166, + height: 166, }) } }); }, tag: function (media) { - return '<iframe width="400" height="166" scrolling="no" frameborder="no"' + + return '<iframe width="166" height="166" scrolling="no" frameborder="no"' + 'src="https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/' + media.token + '&color=ff6600&auto_play=false&show_artwork=true"></iframe>' } - }, { + }, + /* + { type: 'link', regex: /^http.+/i, fetch: function(url, done) { done({ + url: url, + type: "link", token: "", thumbnail: "", title: "", @@ -152,12 +167,86 @@ var Parser = { */ ], + tumblr: function(url, cb){ + var domain = url.replace(/^https?:\/\//,"").split("/")[0] + if (domain.indexOf(".") == -1) { + domain += ".tumblr.com" + } + $.ajax({ + type: 'GET', + url: "http://" + domain + "/api/read", + dataType: "jsonp", + data: { + format: "json", + }, + success: function(data){ + var media_list = [] + var blog = data.tumblelog + + data.posts.forEach(parse) + cb(media_list) + + function parse(post){ + var media, caption, url + switch (post.type) { + case 'photo': + caption = stripHTML(post['photo-caption']) + if (post.photos.length) { + post.photos.forEach(function(photo){ + var media = { + url: photo['photo-url-1280'], + type: "image", + token: "", + thumbnail: photo['photo-url-500'], + description: caption, + width: parseInt(photo.width), + height: parseInt(photo.height), + } + media_list.push(media) + }) + } + else { + media = { + url: post['photo-url-1280'], + type: "image", + token: "", + thumbnail: post['photo-url-500'], + description: caption, + width: parseInt(post.width), + height: parseInt(post.height), + } + media_list.push(media) + } + break + case 'video': + url = post['video-source'] + if (url.indexOf("http") !== 0) { break } + if (Parser.lookup.youtube.regex.test(url)) { + var id = (url.match(/v=([-_a-zA-Z0-9]{11})/i) || url.match(/youtu.be\/([-_a-zA-Z0-9]{11})/i) || url.match(/embed\/([-_a-zA-Z0-9]{11})/i))[1].split('&')[0]; + var thumb = "http://i.ytimg.com/vi/" + id + "/hqdefault.jpg" + media = { + url: post['video-source'], + type: "youtube", + token: id, + thumbnail: thumb, + title: stripHTML(post['video-caption']), + width: 640, + height: 360, + } + media_list.push(media) + } + break + } + } +// console.log(post) + } + }) + }, + parse: function (url, cb) { var matched = Parser.integrations.some(function(integration){ if (integration.regex.test(url)) { integration.fetch(url, function(res){ - res.url = url - res.type = integration.type cb(res) }) return true @@ -176,6 +265,15 @@ var Parser = { return "" }, + loadImage: function(url, cb, error){ + if (Parser.lookup.image.regex.test(url)) { + Parser.lookup.image.fetch(url, function(media){ + cb(media) + }) + } + else error && error() + }, + thumbnail: function (media) { return '<img src="' + (media.thumbnail || media.url) + '" class="thumb">'; }, diff --git a/public/assets/javascripts/ui/lib/Router.js b/public/assets/javascripts/ui/lib/Router.js index 0b6385c..28905b2 100644 --- a/public/assets/javascripts/ui/lib/Router.js +++ b/public/assets/javascripts/ui/lib/Router.js @@ -16,6 +16,7 @@ var Router = View.extend({ if (pathname in routes) { this[this.routes[pathname]](null) + return } if (path[path.length-1] == null) { diff --git a/public/assets/javascripts/ui/lib/ToggleableView.js b/public/assets/javascripts/ui/lib/ToggleableView.js new file mode 100644 index 0000000..371629f --- /dev/null +++ b/public/assets/javascripts/ui/lib/ToggleableView.js @@ -0,0 +1,19 @@ +var ToggleableView = View.extend({ + + toggle: function(state){ + this.$el.toggleClass("active", state) + }, + + show: function(){ + this.toggle(true) + }, + + hide: function(){ + this.toggle(false) + }, + + visible: function(){ + return this.$el.hasClass("active") + } + +})
\ No newline at end of file diff --git a/public/assets/javascripts/ui/lib/Toolbar.js b/public/assets/javascripts/ui/lib/Toolbar.js new file mode 100644 index 0000000..a9ce51c --- /dev/null +++ b/public/assets/javascripts/ui/lib/Toolbar.js @@ -0,0 +1,22 @@ +var Toolbar = Fiber.extend(function(base){ + var exports = {} + exports.init = function(rapper){ + this.rapper = (typeof rapper == "string") ? $(rapper)[0] : rapper + this.tools = {} + this.els = {} + } + exports.add = function(role, fn){ + var self = this + this.tools[role] = fn + this.els[role] = $("[data-role=" + role + "]", self.rapper) + this.els[role].click(function(){ + $(".active", self.rapper).removeClass('active') + $(this).addClass('active') + fn() + }) + } + exports.pick = function(role){ + this.els[role].trigger("click") + } + return exports +})
\ No newline at end of file diff --git a/public/assets/javascripts/ui/lib/UploadView.js b/public/assets/javascripts/ui/lib/UploadView.js index efaa8c9..d1e2b73 100644 --- a/public/assets/javascripts/ui/lib/UploadView.js +++ b/public/assets/javascripts/ui/lib/UploadView.js @@ -3,88 +3,94 @@ var UploadView = View.extend({ // define uploadAction - events: { - "change .file": "handleFileSelect", - "submit form": "preventDefault", - }, - - initialize: function(){ - this.$file = this.$(".file") - this.$upload = this.$(".upload-icon") + events: { + "change [type=file]": "handleFileSelect", + "submit form": "preventDefault", + }, + + initialize: function(){ + this.$file = this.$("[type=file]") + this.$upload = this.$(".upload-icon") }, beforeUpload: function(){ }, - + handleFileSelect: function(e) { - e.stopPropagation(); - e.preventDefault(); - - this.beforeUpload() + e.stopPropagation(); + e.preventDefault(); + + this.beforeUpload() - var files = e.dataTransfer ? e.dataTransfer.files : e.target.files; + var files = e.dataTransfer ? e.dataTransfer.files : e.target.files; - for (var i = 0, f; f = files[i]; i++) { - if ( ! f.type.match('image.*')) { - continue; - } - - this.getImageDimensions(f) - } - }, - - getImageDimensions: function(f){ - var base = this - - this.$upload.addClass('uploading') + for (var i = 0, f; f = files[i]; i++) { + if ( ! f.type.match('image.*')) { + continue; + } + + this.getImageDimensions(f) + } + }, + + getImageDimensions: function(f){ + var base = this + + this.$upload.addClass('uploading') - var reader = new FileReader(); + var reader = new FileReader(); - reader.onload = function(e) { - var image = new Image() - image.onload = function(){ - var width = image.naturalWidth, - height = image.naturalHeight - base.upload(f, width, height) - } - image.src = e.target.result - } - - reader.readAsDataURL(f); - }, - - upload: function(f, width, height){ - var fd = new FormData() - fd.append('image', f) - fd.append('width', width) - fd.append('height', height) - fd.append('_csrf', $("[name=_csrf]").val()) - - if (this.mediaTag) { - fd.append('tag', this.mediaTag) - } + reader.onload = function(e) { + var image = new Image() + image.onload = function(){ + var width = image.naturalWidth, + height = image.naturalHeight + base.upload(f, width, height) + } + image.src = e.target.result + } + + reader.readAsDataURL(f); + }, + + upload: function(f, width, height){ + var fd = new FormData() + fd.append('image', f) + fd.append('width', width) + fd.append('height', height) + fd.append('_csrf', $("[name=_csrf]").val()) + + if (this.mediaTag) { + fd.append('tag', this.mediaTag) + } - var request = $.ajax({ - url: this.uploadAction, - type: "post", - data: fd, - dataType: "json", - processData: false, - contentType: false, - }) - request.done(this.success.bind(this)) - }, - - success: function(media){ - if (media.error) { - return - } - this.$upload.removeClass('uploading') - this.add(media) - }, - - add: function(media){ - console.log(media) - }, + var request = $.ajax({ + url: this.uploadAction, + type: "post", + data: fd, + dataType: "json", + processData: false, + contentType: false, + }) + request.done(this.success.bind(this)) + }, + + success: function(media){ + if (media.error) { + // console.log(media.error) + this.$upload.removeClass('uploading') + this.error(media.error) + return + } + this.$upload.removeClass('uploading') + this.add(media) + }, + + add: function(media){ + console.log(media) + }, + + error: function(error){ + }, }) diff --git a/public/assets/javascripts/ui/lib/View.js b/public/assets/javascripts/ui/lib/View.js index d94e6db..9a8ab5b 100644 --- a/public/assets/javascripts/ui/lib/View.js +++ b/public/assets/javascripts/ui/lib/View.js @@ -1,139 +1,142 @@ var View = (function($, _){ - var View = function(options) { - this._id = _.uniqueId('view') - this.type = "view" - options || (options = {}); - _.extend(this, _.pick(options, viewOptions)) - this._ensureElement() - this.initialize.apply(this, arguments) - this.delegateEvents() - } + var View = function(options) { + this._id = _.uniqueId('view') + this.type = "view" + options || (options = {}); + _.extend(this, _.pick(options, viewOptions)) + this._ensureElement() + this.initialize.apply(this, arguments) + this.delegateEvents() + } - var delegateEventSplitter = /^(\S+)\s*(.*)$/; + var delegateEventSplitter = /^(\S+)\s*(.*)$/; - var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; + var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; - _.extend(View.prototype, { + _.extend(View.prototype, { - // The default `tagName` of a View's element is `"div"`. - tagName: 'div', + // The default `tagName` of a View's element is `"div"`. + tagName: 'div', - $: function(selector) { - return this.$el.find(selector); - }, + $: function(selector) { + return this.$el.find(selector); + }, - initialize: function(){}, + initialize: function(){}, - setElement: function(element, delegate) { - if (this.$el) this.undelegateEvents(); - this.$el = element instanceof $ ? element : $(element); - this.el = this.$el[0]; - if (delegate !== false) this.delegateEvents(); - return this; - }, + setElement: function(element, delegate) { + if (this.$el) this.undelegateEvents(); + this.$el = element instanceof $ ? element : $(element); + this.el = this.$el[0]; + if (delegate !== false) this.delegateEvents(); + return this; + }, - // Set callbacks, where `this.events` is a hash of - // - // *{"event selector": "callback"}* - // - // { - // 'mousedown .title': 'edit', - // 'click .button': 'save', - // 'click .open': function(e) { ... } - // } - // - // pairs. Callbacks will be bound to the view, with `this` set properly. - // Uses event delegation for efficiency. - // Omitting the selector binds the event to `this.el`. - // This only works for delegate-able events: not `focus`, `blur`, and - // not `change`, `submit`, and `reset` in Internet Explorer. - delegateEvents: function(events) { - if (!(events || (events = _.result(this, 'events')))) return this; - this.undelegateEvents(); - for (var key in events) { - var method = events[key]; - if (!_.isFunction(method)) method = this[events[key]]; - if (!method) continue; + // Set callbacks, where `this.events` is a hash of + // + // *{"event selector": "callback"}* + // + // { + // 'mousedown .title': 'edit', + // 'click .button': 'save', + // 'click .open': function(e) { ... } + // } + // + // pairs. Callbacks will be bound to the view, with `this` set properly. + // Uses event delegation for efficiency. + // Omitting the selector binds the event to `this.el`. + // This only works for delegate-able events: not `focus`, `blur`, and + // not `change`, `submit`, and `reset` in Internet Explorer. + delegateEvents: function(events) { + if (!(events || (events = _.result(this, 'events')))) return this; + this.undelegateEvents(); + for (var key in events) { + var method = events[key]; + if (!_.isFunction(method)) method = this[events[key]]; + if (!method) continue; - var match = key.match(delegateEventSplitter); - var eventName = match[1], selector = match[2]; - method = _.bind(method, this); - eventName += '.delegateEvents' + this._id; - if (selector === '') { - this.$el.on(eventName, method); - } else { - this.$el.on(eventName, selector, method); - } - } - return this; - }, + var match = key.match(delegateEventSplitter); + var eventName = match[1], selector = match[2]; + method = _.bind(method, this); + eventName += '.delegateEvents' + this._id; + if (is_mobile && (selector === 'mouseenter' || selector === 'mouseleave')) { + continue + } + else if (selector === '') { + this.$el.on(eventName, method); + } else { + this.$el.on(eventName, selector, method); + } + } + return this; + }, - // Clears all callbacks previously bound to the view with `delegateEvents`. - undelegateEvents: function() { - this.$el.off('.delegateEvents' + this._id); - return this; - }, + // Clears all callbacks previously bound to the view with `delegateEvents`. + undelegateEvents: function() { + this.$el.off('.delegateEvents' + this._id); + return this; + }, - // Ensure that the View has a DOM element to render into. - // If `this.el` is a string, pass it through `$()`, take the first - // matching element, and re-assign it to `el`. Otherwise, create - // an element from the `id`, `className` and `tagName` properties. - _ensureElement: function() { - this.setElement(_.result(this, 'el'), false); - }, - - preventDefault: function(e){ - e && e.preventDefault() - }, - - stopPropagation: function(e){ - e && e.stopPropagation() - }, + // Ensure that the View has a DOM element to render into. + // If `this.el` is a string, pass it through `$()`, take the first + // matching element, and re-assign it to `el`. Otherwise, create + // an element from the `id`, `className` and `tagName` properties. + _ensureElement: function() { + this.setElement(_.result(this, 'el'), false); + }, + + preventDefault: function(e){ + e && e.preventDefault() + }, + + stopPropagation: function(e){ + e && e.stopPropagation() + }, - }); + }); - var extend = function(protoProps, staticProps) { - var staticProps = staticProps || {} - var parent = this; - var child; - var childEvents = {}; + var extend = function(protoProps, staticProps) { + var staticProps = staticProps || {} + var parent = this; + var child; + var childEvents = {}; - // The constructor function for the new subclass is either defined by you - // (the "constructor" property in your `extend` definition), or defaulted - // by us to simply call the parent's constructor. - if (protoProps && _.has(protoProps, 'constructor')) { - child = protoProps.constructor; - } else { - child = function(){ return parent.apply(this, arguments); }; - } + // The constructor function for the new subclass is either defined by you + // (the "constructor" property in your `extend` definition), or defaulted + // by us to simply call the parent's constructor. + if (protoProps && _.has(protoProps, 'constructor')) { + child = protoProps.constructor; + } else { + child = function(){ return parent.apply(this, arguments); }; + } - // Extend events so we can subclass views - _.extend(childEvents, parent.prototype.events, protoProps.events) + // Extend events so we can subclass views + _.extend(childEvents, parent.prototype.events, protoProps.events) - // Add static properties to the constructor function, if supplied. - _.extend(child, parent, staticProps); + // Add static properties to the constructor function, if supplied. + _.extend(child, parent, staticProps); - // Set the prototype chain to inherit from `parent`, without calling - // `parent`'s constructor function. - var Surrogate = function(){ this.constructor = child; }; - Surrogate.prototype = parent.prototype; - child.prototype = new Surrogate; + // Set the prototype chain to inherit from `parent`, without calling + // `parent`'s constructor function. + var Surrogate = function(){ this.constructor = child; }; + Surrogate.prototype = parent.prototype; + child.prototype = new Surrogate; - // Add prototype properties (instance properties) to the subclass, - // if supplied. - if (protoProps) _.extend(child.prototype, protoProps); + // Add prototype properties (instance properties) to the subclass, + // if supplied. + if (protoProps) _.extend(child.prototype, protoProps); - // Set a convenience property in case the parent's prototype is needed - // later. - child.prototype.__super__ = parent.prototype; - child.prototype.events = childEvents + // Set a convenience property in case the parent's prototype is needed + // later. + child.prototype.__super__ = parent.prototype; + child.prototype.events = childEvents - return child; - }; + return child; + }; - View.extend = extend; - - return View; + View.extend = extend; + + return View; })(jQuery, _) diff --git a/public/assets/javascripts/ui/reader/EmbedView.js b/public/assets/javascripts/ui/reader/EmbedView.js new file mode 100644 index 0000000..7a75e00 --- /dev/null +++ b/public/assets/javascripts/ui/reader/EmbedView.js @@ -0,0 +1,76 @@ +var EmbedView = ModalView.extend({ + el: ".embedView", + + events: { + "keydown": "stopPropagation", + "input [name=width]": "build", + "input [name=height]": "build", + "click [name=mute]": "build", + "click [name=interactive]": "build", + "click textarea": "selectAll", + "click #testEmbed": "test", + }, + + defaultWidth: 600, + defaultHeight: 450, + + initialize: function(opt){ + this.parent = opt.parent + this.$embedCode = this.$("#embedCode") + this.$width = this.$("[name=width]") + this.$height = this.$("[name=height]") + this.$mute = this.$("[name=mute]") + this.$interactive = this.$("[name=interactive]") + + this.$width.val(this.defaultWidth) + this.$height.val(this.defaultHeight) + }, + + show: function(){ + this.build() + this.__super__.show.call(this) + }, + + build: function(){ + var kode = this.getEmbedCode() + this.$embedCode.val( kode ) + }, + + getEmbedCode: function(){ + var mute = this.$mute.prop('checked') ? 1 : 0 + var interactive = this.$interactive.prop('checked') ? 1 : 0 + var width = clamp( this.$width.int(), 0, 2000) || this.defaultWidth + var height = clamp( this.$height.int(), 0, 2000) || this.defaultHeight + var link = this.parent.getLink() + var embed_link = link + embed_link += "?mute=" + mute + embed_link += "&embed=1" + if (interactive) { + embed_link += "&interactive=1" + } + + var kode = "<iframe src='" + encodeURI(embed_link) + "' width='" + width + "' height='" + height + "'" + kode += " seamless scrolling='no' style='border: 0'" + kode += " webkitallowfullscreen mozallowfullscreen allowfullscreen" + if (! interactive) { + kode += " style='pointer-events:none;'" + } + kode += "></iframe>" + + if (! interactive) { + kode = "<div style='position:relative'>" + kode + "<a href='" + encodeURI(link) + "' style='display:block;position:absolute;top:0;left:0;width:" + width + "px;height:" + height + "px;'></a></div>" + } + + return kode + }, + + test: function(){ + var kode = this.getEmbedCode() + window.open("data:text/html," + kode, "_blank") + }, + + selectAll: function(){ + this.$embedCode[0].select() + }, + +}) diff --git a/public/assets/javascripts/ui/reader/MediaPlayer.js b/public/assets/javascripts/ui/reader/MediaPlayer.js index df2d075..8e65976 100644 --- a/public/assets/javascripts/ui/reader/MediaPlayer.js +++ b/public/assets/javascripts/ui/reader/MediaPlayer.js @@ -1,5 +1,5 @@ -var MediaPlayer = FormView.extend({ +var MediaPlayer = View.extend({ el: "#mediaPlayer", events: { @@ -44,22 +44,26 @@ var MediaPlayer = FormView.extend({ this.unbind() } if (media.type == "image") { - if ((! media.title || ! media.title.length) && (! media.description || ! media.description.length)) { + if ( ! media.description && (! media.title || media.title == filenameFromUrl(media.url)) ) { this.hide() - return + return false } } + else if (media.type == "text") { + return false + } this.bind(scenery) this.$el.addClass("active") - this.$name.html(media.title) - this.$description.html(media.description) + this.$name.html( sanitize(media.title) ) + this.$description.html( marked(media.description) ) switch (media.type) { case "image": - this.$(".image").show() this.$(".video").hide() + this.$(".audio").hide() + this.$(".image").show() // this.$widthDimension.html( Number(media.widthDimension) || "" ) // this.$heightDimension.html( Number(media.heightDimension) || "" ) @@ -70,14 +74,24 @@ var MediaPlayer = FormView.extend({ case "youtube": case "vimeo": case "video": - this.$(".video").show() this.$(".image").hide() + this.$(".audio").hide() + this.$(".video").show() this.$playButton.toggleClass("paused", ! this.scenery.paused()) this.$muteButton.toggleClass("muted", this.scenery.muted()) break + + case "soundcloud": + this.$(".image").hide() + this.$(".video").hide() + this.$(".audio").show() + + this.$playButton.toggleClass("paused", ! this.scenery.paused()) + break } + return true }, hide: function(scenery){ diff --git a/public/assets/javascripts/ui/reader/ReaderView.js b/public/assets/javascripts/ui/reader/ReaderView.js index d80f225..43e81d8 100644 --- a/public/assets/javascripts/ui/reader/ReaderView.js +++ b/public/assets/javascripts/ui/reader/ReaderView.js @@ -9,36 +9,98 @@ var ReaderView = View.extend({ initialize: function(){ this.mediaPlayer = new MediaPlayer ({ parent: this }) + this.shareView = new ShareView ({ parent: this }) }, load: function(name){ - if (window.location.search.indexOf("noui") !== -1) { - $(".logo,.topLinks,#editorView").hide() + var opt = this.getQS() + var mode = "default" + var name = sanitize(name) + + if (opt.noui) { + $(".logo, .topLinks, #editorView, #keyhint").hide() + mode = "noui" + } + if (opt.embed) { + $(".topLinks, .share, #edit-room-link, #keyhint").hide() + mode = "embed" } - if (window.location.search.indexOf("mute") !== -1) { + if (opt.mute) { app.muted = true } - name = sanitize(name) - $.get(this.projectAction + name, this.ready.bind(this)) + + this.tracker = new Tracker ({ mode: mode }) + + if ('vvalls_data' in window) { + this.ready(window.vvalls_data) + } + else { + $.get(this.projectAction + name, this.ready.bind(this)) + } + }, + + getQS: function(){ + var qs = {} + window.location.search.replace(/^\?/,"").split("&").forEach(function(s){ + var pair = s.split("=") + if (pair.length < 2) { + qs[pair[0]] = true + } + else { + qs[pair[0]] = pair[1] + } + }) + return qs }, ready: function(data){ $("#map").hide() + this.data = data + var is_landscape = window.innerWidth > window.innerHeight - data.rooms && Rooms.deserialize(data.rooms) + if (is_desktop || (is_mobile && is_landscape)) { + this.build(data) + return + } + + // don't build anything until we're in landscape mode, otherwise ios might crash!! + var orientationFn = orientationchange.bind(this) + window.addEventListener('orientationchange', orientationFn) + function orientationchange (e) { + var is_landscape = window.innerWidth > window.innerHeight + if (is_landscape) { + window.removeEventListener('orientationchange', orientationFn) + this.build(data) + } + } + }, + + build: function(data){ + if (data.shapes.length) { + Rooms.deserializeFromShapes(data) + } + else { + Rooms.deserialize(data.rooms) + } data.walls && Walls.deserialize(data.walls) data.media && Scenery.deserialize(data.media) + data.sculpture && Sculpture.deserialize(data.sculpture) data.startPosition && scene.camera.move(data.startPosition) + cam.y = window.viewHeight = data.viewHeight || app.defaults.viewHeight + var colors = data.colors || app.defaults.colors var modes = [ "wall", "outline", "floor", "ceiling" ] modes.forEach(function(mode){ Walls.setColor[mode](colors[mode]) }) - editor.permissions.clear() + window.editor && editor.permissions.clear() - this.listen() + // disable until we start using spinning again + // this.listen() + + app.tube("site-ready") }, listen: function(){ @@ -71,11 +133,19 @@ var ReaderView = View.extend({ }, pick: function(scenery){ - this.mediaPlayer.pick(scenery) + var has_info = this.mediaPlayer.pick(scenery) + $("#minimap").toggleClass('active', ! has_info) + app.tube("pick-scenery", { scenery: scenery }) + }, + + pickWall: function(wall, pos){ + this.hideExtras() }, hideExtras: function(){ + $("#minimap").addClass('active') this.mediaPlayer.hide() + app.tube("close-scenery") } }) diff --git a/public/assets/javascripts/ui/reader/ShareView.js b/public/assets/javascripts/ui/reader/ShareView.js new file mode 100644 index 0000000..dbe6f64 --- /dev/null +++ b/public/assets/javascripts/ui/reader/ShareView.js @@ -0,0 +1,62 @@ +var ShareView = View.extend({ + el: ".share", + + events: { + "keydown": "stopPropagation", + "click #share_facebook": "facebook", + "click #share_twitter": "twitter", + "click #share_embed": "embed", + }, + + initialize: function(opt){ + this.parent = opt.parent + this.embedView = new EmbedView ({ parent: this }) + this.$link = this.$("#share_link") + }, + + toggle: function(state){ + if (typeof state == "boolean") { + this.$el.toggleClass("active", state) + } + else { + this.$el.toggleClass("active") + } + }, + show: function(){ + this.toggle(true) + }, + hide: function(){ + this.toggle(false) + }, + + setLink: function(url){ + this.$link.val( url ) + }, + getLink: function(){ + var link = window.location.origin + window.location.pathname + link = link.replace(/\/edit\/?$/, "") + return link + }, + + facebook: function (e) { + e.preventDefault() + var link = this.getLink() + var msg = app.controller.data.name + " on VValls" + var url = "https://www.facebook.com/share.php?u=" + encodeURIComponent(link) + "&t=" + encodeURIComponent(msg) + window.open(url, "_blank") + }, + + twitter: function (e) { + e.preventDefault() + var link = this.getLink() + var msg = app.controller.data.name + " on VValls" + var url = "https://twitter.com/home?status=" + encodeURIComponent(link + " " + msg) + window.open(url, "_blank") + }, + + embed: function (e) { + e.preventDefault() + this.embedView.show() + }, + +}) diff --git a/public/assets/javascripts/ui/reader/Tracker.js b/public/assets/javascripts/ui/reader/Tracker.js new file mode 100644 index 0000000..d2dec39 --- /dev/null +++ b/public/assets/javascripts/ui/reader/Tracker.js @@ -0,0 +1,133 @@ +if (window.location.host.indexOf("lvh.me") === -1) { + (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ + (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), + m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) + })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); +} +else { + ga = function(){} +} +ga('create', 'UA-56883705-1', 'auto'); +ga('send', 'pageview'); + +var Tracker = Fiber.extend(function(base){ + + var exports = { + init: function(opt){ + this.wall_id = null + this.scenery_id = null + this.clicks = 0 + + this.wallTimer = new Timer () + this.roomTimer = new Timer () + this.sceneryTimer = new Timer () + + this.bind() + // this.trackPageview(opt) + }, + + bind: function () { + window.addEventListener("click", this.trackClick.bind(this), true) + app.on("change-wall", this.changeWall.bind(this)) + app.on("pick-scenery", this.pickScenery.bind(this)) + app.on("close-scenery", this.trackScenery.bind(this)) + app.on("change-room", this.changeRoom.bind(this)) + }, + + pushEvent: function(event){ + // this.events.push(event) + event.unshift("send") + ga.apply( ga, event ) + }, + + trackPageview: function(opt){ + // this.pushEvent([ "view", opt.mode ]) + }, + + // + // how long they spend in front of each wall + + changeWall: function(opt){ + var duration = this.wallTimer.currentTime() + if (this.wall_id && duration > 5000) { + this.pushEvent([ "wall", this.wall_id, duration ]) + } + this.wall_id = opt.wall.id + this.wallTimer.start() + }, + + // + // how long the user spends on each item they click + + pickScenery: function(opt){ + if (this.scenery_id && opt.scenery.id !== this.scenery_id) { + this.trackScenery() + } + else { + this.sceneryTimer.start() + } + this.scenery_id = opt.scenery.id + }, + + trackScenery: function(){ + var duration = this.sceneryTimer.currentTime() + if (this.scenery_id && duration > 1000) { + this.pushEvent([ "scenery", this.scenery_id, duration ]) + } + this.scenery_id = null + this.sceneryTimer.reset() + }, + + // + // how long they spend in the room + + changeRoom: function(opt){ + var duration = this.roomTimer.currentTime() + if (this.room_id !== opt.room.id) { + if (this.room_id && duration > 5000) { + this.pushEvent([ "room", this.room_id, duration ]) + } + this.roomTimer.start() + this.room_id = opt.room.id + } + }, + + // + // how many clicks per room + + trackClick: function(opt){ + this.clicks += 1 + }, + + save: function () { + // possibly just push to google analytics + }, + + } + + return exports +}) + + +var Timer = Fiber.extend(function(base){ + var exports = { + + init: function(opt){ + this.time = 0 + }, + + reset: function(){ + this.time = 0 + }, + + start: function(){ + this.time = Date.now() + }, + + currentTime: function(){ + return this.time ? Date.now() - this.time : 0 + }, + } + + return exports +}) diff --git a/public/assets/javascripts/ui/reader/_router.js b/public/assets/javascripts/ui/reader/_router.js new file mode 100644 index 0000000..f3b121b --- /dev/null +++ b/public/assets/javascripts/ui/reader/_router.js @@ -0,0 +1,24 @@ + +var SiteRouter = Router.extend({ + el: "body", + + initialize: function(){ + app.launch() + if (app.unsupported) return + + this.readerView = app.controller = new ReaderView() + this.readerView.load() + + $("body").removeClass("loading") + } + +}) + +var editor = { + permissions: new Permissions({ + 'pick': true, + 'move': true, + 'resize': true, + 'destroy': false, + }) +} diff --git a/public/assets/javascripts/ui/site/EditProfileModal.js b/public/assets/javascripts/ui/site/EditProfileModal.js index b023923..d0e5d05 100644 --- a/public/assets/javascripts/ui/site/EditProfileModal.js +++ b/public/assets/javascripts/ui/site/EditProfileModal.js @@ -3,11 +3,14 @@ var EditProfileModal = ModalFormView.extend({ el: ".mediaDrawer.editProfile", action: "/api/profile", method: "put", + + events: { + "click [data-role='changePasswordToggle']": 'togglePasswordFields' + }, load: function(){ this.reset() $.get("/api/profile", function(data){ - console.log(data) for (var i in data) { this.$("[name='" + i + "']").val(data[i]) @@ -25,6 +28,10 @@ var EditProfileModal = ModalFormView.extend({ this.show() }.bind(this)) }, + + togglePasswordFields: function(){ + this.$("[data-role='changePasswordFields']").toggleClass("hidden") + }, validate: function(){ var errors = [] diff --git a/public/assets/javascripts/ui/site/EditSubscriptionModal.js b/public/assets/javascripts/ui/site/EditSubscriptionModal.js new file mode 100644 index 0000000..c1dc9f8 --- /dev/null +++ b/public/assets/javascripts/ui/site/EditSubscriptionModal.js @@ -0,0 +1,290 @@ + +var EditSubscriptionModal = ModalView.extend({ + el: ".mediaDrawer.editSubscription", + action: "/api/subscription", + syncAction: "/api/subscription/sync", + updateAction: "/api/subscription", + destroyAction: "/api/subscription/destroy", + + fixedClose: true, + editing: false, + subscriber: null, + tempSubscriber: null, + + events: { + "click [data-role='addLayouts']": 'addLayouts', + "click [data-role='changePlan']": 'changePlan', + "click [data-role='cancelSubscription']": 'destroy', + "click .gear": 'sync', + "click .planList button": 'followLink', + + "click [data-role=showEditMenu]": "editMode", + + "click [data-role=closeMenu]": "resetMode", + + "input [data-role=basicLayoutInput]": "updateQuantity", + "input [data-role=proLayoutInput]": "updateQuantity", + "click [data-role=saveChanges]": "saveChanges", + + "change [name=planRadio]": "updatePlan", + "click [data-role=savePlan]": "savePlan", + + "submit form": "preventDefault", + }, + + initialize: function(){ + // this.parent = opt.parent + this.__super__.initialize.call(this) + + // two sections + this.$freePlan = this.$(".freePlan") + this.$paidPlan = this.$(".paidPlan") + + // subscription table + this.$planInfo = this.$(".planInfo") + this.$planRow = this.$(".planRow") + this.$basicLayoutRow = this.$(".basicLayoutRow") + this.$proLayoutRow = this.$(".proLayoutRow") + this.$totalRow = this.$(".totalRow") + this.$planList = this.$(".planList") + + this.$billingInterval = this.$("[data-role=billingInterval]") + + // plan stuff + this.$planName = this.$("[data-role=planName]") + this.$planCost = this.$("[data-role=planCost]") + this.$planTotal = this.$("[data-role=planTotal]") + + this.$basicPlanName = this.$("[data-role=basicPlanName]") + this.$basicPlanCost = this.$("[data-role=basicPlanCost]") + + this.$proPlanName = this.$("[data-role=proPlanName]") + this.$proPlanCost = this.$("[data-role=proPlanCost]") + + // basic + pro layout stuff + this.$basicLayoutCost = this.$("[data-role=basicLayoutCost]") + this.$basicLayoutQuantity = this.$("[data-role=basicLayoutQuantity]") + this.$basicLayoutTotal = this.$("[data-role=basicLayoutTotal]") + + this.$proLayoutCost = this.$("[data-role=proLayoutCost]") + this.$proLayoutQuantity = this.$("[data-role=proLayoutQuantity]") + this.$proLayoutTotal = this.$("[data-role=proLayoutTotal]") + + // menus.. main menu + this.$showEditMenu = this.$("[data-role=showEditMenu]") + this.$cancelSubscription = this.$("[data-role=cancelSubscription]") + + // three submenus + this.$editMenu = this.$("[data-role=editMenu]") + this.$planMenu = this.$("[data-role=planMenu]") + + this.$buyLayouts = this.$("[data-role=buyLayouts]") + this.$closeMenu = this.$("[data-role=closeMenu]") + this.$changePlan = this.$("[data-role=changePlan]") + + // input fields + this.$basicLayoutInput = this.$("[data-role=basicLayoutInput]") + this.$proLayoutInput = this.$("[data-role=proLayoutInput]") + this.$planRadio = this.$("[name=planRadio]") + this.$basicPlanInput = this.$("[data-role=basicPlanInput]") + this.$proPlanInput = this.$("[data-role=proPlanInput]") + + this.$gear = this.$(".gear") + }, + + plan_levels: { + free: 0, + basic: 1, + pro: 2, + custom: 3, + artist: 4, + }, + + loaded: false, + load: function(){ + if (this.loaded) { return this.show() } + $.get(this.action, this.didLoad.bind(this)) + }, + didLoad: function(data){ + this.loaded = true + this.plans = data.plans + if (data.subscription) { + this.subscriber = data.subscription + } + else if (data.error) { + // ...no subscription found + this.subscriber = null + } + return this.show() + }, + followLink: function(e){ + e.preventDefault(); + window.location.href = $(e.target).closest("a").attr("href") + }, + + show: function(){ + this.$gear.removeClass("turning") + if (! this.subscriber) { + this.$freePlan.show() + this.$paidPlan.hide() + this.$planList.load("/partials/plans", function(){ + this.$(".free_plan_info").remove() + this.__super__.show.call(this) + }.bind(this)) + return + } + + this.$freePlan.hide() + this.$paidPlan.show() + + this.resetMode() + + this.__super__.show.call(this) + }, + reset: function(){ + var subscriber = this.subscriber + var plan = this.getPlan(subscriber.plan_type) + this.displayTotals(subscriber, plan) + }, + getPlan: function(plan_type){ + return this.plans[ this.plan_levels[ plan_type ] ] + }, + calculateTotals: function(subscriber, plan){ + var t = {} + t.is_pro = subscriber.plan_type == "pro" + t.is_monthly = subscriber.plan_period == "monthly" + t.plan_price = t.is_monthly ? plan.monthly_price : plan.yearly_price + t.basic_layout_price = t.is_monthly ? plan.basic_layout_monthly_price : plan.basic_layout_yearly_price + t.basic_layout_total = subscriber.basic_layouts * t.basic_layout_price + t.pro_layout_price = t.is_monthly ? plan.pro_layout_monthly_price : plan.pro_layout_yearly_price + t.pro_layout_total = t.is_pro ? subscriber.pro_layouts * t.pro_layout_price : 0 + t.plan_total = t.plan_price + t.basic_layout_total + t.pro_layout_total + return t + }, + displayTotals: function(subscriber, plan){ + var totals = this.calculateTotals(subscriber, plan) + + this.$basicPlanName.html ( this.plans[1].name ) + this.$proPlanName.html ( this.plans[2].name ) + this.$basicPlanCost.toDollars ( totals.is_monthly ? this.plans[1].monthly_price : this.plans[2].yearly_price) + this.$proPlanCost.toDollars ( totals.is_monthly ? this.plans[2].monthly_price : this.plans[2].yearly_price) + + this.$planName.html ( plan.name ) + this.$planCost.toDollars ( totals.plan_price ) + + this.$billingInterval.html ( totals.is_monthly ? "mo." : "yr." ) + this.$basicLayoutRow.toggle ( subscriber.basic_layouts > 0 ) + this.$proLayoutRow.toggle ( totals.is_pro && subscriber.pro_layouts > 0) + + this.$basicLayoutCost.toDollars ( totals.basic_layout_price ) + this.$basicLayoutQuantity.html ( subscriber.basic_layouts ) + this.$basicLayoutTotal.toDollars ( totals.basic_layout_total ) + + this.$proLayoutCost.toDollars ( totals.pro_layout_price ) + this.$proLayoutQuantity.html ( subscriber.pro_layouts ) + this.$proLayoutTotal.toDollars ( totals.pro_layout_total ) + + this.$planTotal.toDollars ( totals.plan_total ) + }, + + editMode: function(e){ + e && e.preventDefault() + + this.editing = true + this.$el.addClass("editing") + this.tempSubscriber = defaults({}, this.subscriber) + this.$basicLayoutInput.val( this.subscriber.basic_layouts ) + this.$proLayoutInput.val( this.subscriber.pro_layouts ) + this.$basicLayoutRow.show() + this.$proLayoutRow.toggle(this.subscriber.plan_type == "pro") + switch (this.subscriber.plan_type) { + case 'basic': this.$basicPlanInput.prop('checked', true); break; + case 'pro': this.$proPlanInput.prop('checked', true); break; + } + }, + resetMode: function(e){ + e && e.preventDefault() + this.editing = false + this.$el.removeClass("editing") + this.reset() + }, + + updateQuantity: function(e){ + e && e.preventDefault() + var plan = this.getPlan( this.tempSubscriber.plan_type ) + this.tempSubscriber.basic_layouts = clamp( this.$basicLayoutInput.int() || 0, 0, 100) + this.tempSubscriber.pro_layouts = clamp( this.$proLayoutInput.int() || 0, 0, 100) + + this.$basicLayoutInput.val(this.tempSubscriber.basic_layouts) + this.$proLayoutInput.val(this.tempSubscriber.pro_layouts) + this.displayTotals(this.tempSubscriber, plan) + this.$basicLayoutRow.show() + this.$proLayoutRow.toggle(this.tempSubscriber.plan_type == "pro") + }, + saveChanges: function(e){ + e && e.preventDefault() + var is_changed = false + var diff = {} + "plan_type basic_layouts pro_layouts".split(" ").forEach(function(field){ + diff[field] = this.tempSubscriber[field] + if (this.tempSubscriber[field] != this.subscriber[field]) { + is_changed = true + } + }.bind(this)) + + if (is_changed) { + this.update(diff) + } + this.subscriber = this.tempSubscriber + this.resetMode() + }, + + updatePlan: function(e){ + e && e.preventDefault() + this.tempSubscriber.plan_type = this.$("[name=planRadio]:checked").val() + this.updateQuantity() + }, + + sync: function(){ + this.$gear.addClass("turning") + $.ajax({ + url: this.syncAction, + type: "put", + data: { _csrf: $("[name=_csrf]").val() }, + success: this.didLoad.bind(this) + }) + }, + + update: function(data){ + data['_csrf'] = $("[name=_csrf]").val() + this.$gear.addClass("turning") + $.ajax({ + url: this.updateAction, + type: "put", + data: data, + success: function(data){ + this.$gear.removeClass("turning") + }.bind(this) + }) + }, + + destroy: function(e){ + e.preventDefault() + var msg = "Are you sure you want to cancel your subscription?" + ConfirmModal.confirm(msg, function(){ + $.ajax({ + url: this.destroyAction, + type: "delete", + data: { _csrf: $("[name=_csrf]").val() }, + success: function(data){ + this.subscriber = null + this.didLoad(data) + }.bind(this) + }) + }.bind(this), + function(){ + this.show() + }.bind(this)) + }, + +}) diff --git a/public/assets/javascripts/ui/site/HomeView.js b/public/assets/javascripts/ui/site/HomeView.js index 02f9ab9..20452bd 100644 --- a/public/assets/javascripts/ui/site/HomeView.js +++ b/public/assets/javascripts/ui/site/HomeView.js @@ -6,6 +6,32 @@ var HomeView = View.extend({ load: function() { this.projectList = new ProjectList () - } + + var iframe = $("#okplayer")[0] + var player = $f( iframe ) + player.addEvent('ready', function(){ + player.addEvent('play', function(){ + player.api('setVolume', 1.0) + }) + player.addEvent('finish', function(){ + hide() + }) + }) + $('.hero .circle').click( function(){ + $('.videoModal').css("display","table").addClass('active'); + player.api('play') + }) + + $('.videoModal .ion-ios-close-empty').click( function(){ + player.api('pause') + hide() + }) + + function hide() { + $('.videoModal').fadeOut(300, function(){ + $('.videoModal').removeClass('active') + }) + } + }, }) diff --git a/public/assets/javascripts/ui/site/LayoutsIndex.js b/public/assets/javascripts/ui/site/LayoutsIndex.js new file mode 100644 index 0000000..f7272bb --- /dev/null +++ b/public/assets/javascripts/ui/site/LayoutsIndex.js @@ -0,0 +1,85 @@ + +var LayoutsIndex = View.extend({ + + initialize: function(){ + this.$templates = this.$(".templates") + this.$templatesList = this.$(".templates-list") + this.$noTemplates = this.$(".no-templates") + this.$form = this.$("form") + + this.$userTemplatesList = this.$(".userTemplatesList") + this.$blueprintsList = this.$(".blueprintsList") + this.$newBlueprintButton = this.$("[data-role='create-new-blueprint']") + }, + + load: function(type){ + this.$templates.children("span").remove() + + $.get(this.action, this.populate.bind(this)) + }, + + populate: function(data){ + if (! data.layouts.length) { + this.$templates.hide() + this.$form.hide() + this.$noTemplates.show() + } + this.$templatesList.empty() + data.layouts.forEach(function(room){ + var $span = $("<span>") + $span.data("slug", room.slug) + + var $label = $("<label>") + $label.html( room.name ) + + var $image = $("<span>") + $image.addClass("image").css("background-image", "url(" + room.photo + ")") + + $span.append( $image ) + $span.append( $label ) + + this.$templatesList.append($span) + }.bind(this)) + this.show() + } + +}) + + + +var ProjectsModal = ModalView.extend(LayoutsIndex.prototype).extend({ + el: ".mediaDrawer.projects", + + action: "/api/project", + + events: { + "click .templates span": 'toggleActive', + "submit form": 'newProject', + }, + + populate: function(data){ + if (! data.length) { + app.router.newProject() + } + else { + this.__super__.populate.call(this, data) + } + }, + + toggleActive: function(e){ + e.preventDefault() + this.$(".templates .active").removeClass("active") + var $layout = $(e.currentTarget) + $layout.addClass("active") + + // actually do + window.location.pathname = "/project/" + $layout.data("slug") + "/edit" + }, + + newProject: function(e){ + e && e.preventDefault() + window.location.pathname = "/project/new" + } + +}) + diff --git a/public/assets/javascripts/ui/site/LayoutsModal.js b/public/assets/javascripts/ui/site/LayoutsModal.js index 2449465..0222077 100644 --- a/public/assets/javascripts/ui/site/LayoutsModal.js +++ b/public/assets/javascripts/ui/site/LayoutsModal.js @@ -1,127 +1,106 @@ +var LayoutsModal = ModalView.extend(LayoutsIndex.prototype).extend({ + el: ".mediaDrawer.layouts", -var LayoutsIndex = View.extend({ + action: "/api/layout", - initialize: function(){ - this.$templates = this.$(".templates") - this.$noTemplates = this.$(".no-templates") - this.$form = this.$("form") + events: { + "click [data-role='create-new-layout']": 'createNewLayout', + "click [data-role='create-new-blueprint']": 'createNewBlueprint', + "click .templates span": 'pick', }, + + pick: function(e){ + e.preventDefault() + var layout = $(e.currentTarget).data("slug") + var isBlueprint = $(e.currentTarget).data("blueprint") - load: function(type){ - this.$templates.children("span").remove() + if (! layout || ! layout.length) return - $.get(this.action, this.populate.bind(this)) + if (isBlueprint) { + window.location.pathname = "/blueprint/" + layout + } + else { + window.location.pathname = "/layout/" + layout + } + }, + + createNewLayout: function(e){ + e.preventDefault() + window.location.pathname = "/layout/new" + }, + + createNewBlueprint: function(e){ + e.preventDefault() + window.location.pathname = "/blueprint/new" }, populate: function(data){ - if (! data.length) { +/* + if (data.user.plan_level < 1 && data.projectCount == 1) { + // show lockout message + } +*/ + if (! data.layouts.length) { this.$templates.hide() this.$form.hide() this.$noTemplates.show() } - this.$templates.empty() - data.forEach(function(room){ + this.$templatesList.empty() + data.layouts.forEach(function(room){ var $span = $("<span>") - // $span.html(JSON.stringify(room)) $span.data("slug", room.slug) - $span.css("background-image", "url(" + room.photo + ")") - $span.attr("data-name", room.name) - this.$templates.append($span) + var $label = $("<label>") + $label.html( room.name ) + + var $image = $("<span>") + $image.addClass("image").css("background-image", "url(" + room.photo + ")") + + $span.append( $image ) + $span.append( $label ) + + this.$templatesList.append($span) }.bind(this)) - console.log(this.$templates.html()) - this.show() - } - -}) - -var ProjectsModal = ModalView.extend(LayoutsIndex.prototype).extend({ - el: ".mediaDrawer.projects", - - action: "/api/project", - - events: { - "click .templates span": 'toggleActive', - "submit form": 'newProject', - }, - - populate: function(data){ - if (! data.length) { - app.router.newProject() - } - else { - this.__super__.populate.call(this, data) - } - }, - - toggleActive: function(e){ - e.preventDefault() - this.$(".templates .active").removeClass("active") - var $layout = $(e.currentTarget) - $layout.addClass("active") - - // actually do - window.location.pathname = "/project/" + $layout.data("slug") + "/edit" - }, - - newProject: function(e){ - e && e.preventDefault() - window.location.pathname = "/project/new" - } - -}) - - -var LayoutsModal = ModalView.extend(LayoutsIndex.prototype).extend({ - el: ".mediaDrawer.layouts", - - action: "/api/layout", - events: { - "click .templates span": 'toggleActive', - "submit form": 'newLayout', - }, - - toggleActive: function(e){ - e.preventDefault() - this.$(".templates .active").removeClass("active") - var $layout = $(e.currentTarget) - $layout.addClass("active") + data.user_layouts.forEach(function(room){ + var $span = $("<span>") + $span.data("slug", room.slug) + + var $label = $("<label>") + $label.html( room.name ) + + var $image = $("<span>") + $image.addClass("image").css("background-image", "url(" + room.photo + ")") + + $span.append( $image ) + $span.append( $label ) + + this.$templatesList.append($span) + }.bind(this)) + + data.blueprints.forEach(function(blueprint){ + var $span = $("<span>") + $span.data("slug", blueprint.slug) + $span.data("blueprint", true) + + var $label = $("<label>") + $label.html( blueprint.name ) + + var $image = $("<span>") + $image.addClass("image").css("background-image", "url(" + blueprint.url + ")") + + $span.append( $image ) + $span.append( $label ) + + this.$templatesList.append($span) + }.bind(this)) - // actually do - window.location.pathname = "/layout/" + $layout.data("slug") + this.show() }, - + newLayout: function(e){ e && e.preventDefault() window.location.pathname = "/layout/new" } }) - - -var NewProjectModal = ModalView.extend(LayoutsIndex.prototype).extend({ - el: ".mediaDrawer.newProject", - - action: "/api/layout", - - events: { - "click .templates span": 'choose', - "submit form": 'choose', - }, - - toggleActive: function(e){ - e.preventDefault() - this.$(".templates .active").removeClass("active") - $(e.currentTarget).addClass("active") - }, - - choose: function(e){ - e && e.preventDefault() -// var layout = this.$(".templates .active").data("slug") - var layout = $(e.currentTarget).data("slug") - if (! layout || ! layout.length) return - window.location.pathname = "/project/new/" + layout - } - -}) diff --git a/public/assets/javascripts/ui/site/NewProjectModal.js b/public/assets/javascripts/ui/site/NewProjectModal.js new file mode 100644 index 0000000..31675ba --- /dev/null +++ b/public/assets/javascripts/ui/site/NewProjectModal.js @@ -0,0 +1,118 @@ +var NewProjectModal = ModalView.extend(LayoutsIndex.prototype).extend({ + el: ".mediaDrawer.newProject", + + action: "/api/layout", + + events: { + "click [data-role='create-new-layout']": 'createNewLayout', + "click [data-role='create-new-blueprint']": 'createNewBlueprint', + "click .templates span": 'pick', + }, + + toggleActive: function(e){ + e.preventDefault() + this.$(".templates .active").removeClass("active") + $(e.currentTarget).addClass("active") + }, + + pick: function(e){ + e && e.preventDefault() +// var layout = this.$(".templates .active").data("slug") + var layout = $(e.currentTarget).data("slug") + var isBlueprint = $(e.currentTarget).data("blueprint") + if (! layout || ! layout.length) return + + if (isBlueprint) { + window.location.pathname = "/project/blueprint/" + layout + } + else { + window.location.pathname = "/project/new/" + layout + } + }, + + createNewLayout: function(e){ + e.preventDefault() + window.location.pathname = "/project/new/empty" + }, + + createNewBlueprint: function(e){ + e.preventDefault() + window.location.pathname = "/blueprint/new" + }, + + populate: function(data){ +/* + if (data.user.plan_level < 1 && data.projectCount == 1) { + // show lockout message + this.$newBlueprintButton.hide() + } +*/ + if (! data.layouts.length) { + this.$templates.hide() + this.$form.hide() + this.$noTemplates.show() + } + if (! data.blueprints.length) { + this.$blueprintsList.parent().hide() + } + if (! data.user_layouts.length) { + this.$userTemplatesList.parent().hide() + } + + this.$templatesList.empty() + data.layouts.forEach(function(room){ + var $span = $("<span>") + $span.data("slug", room.slug) + + var $label = $("<label>") + $label.html( room.name ) + + var $image = $("<span>") + $image.addClass("image").css("background-image", "url(" + room.photo + ")") + + $span.append( $image ) + $span.append( $label ) + + this.$templatesList.append($span) + }.bind(this)) + + data.user_layouts.forEach(function(room){ + var $span = $("<span>") + $span.data("slug", room.slug) + + var $label = $("<label>") + $label.html( room.name ) + + var $image = $("<span>") + $image.addClass("image").css("background-image", "url(" + room.photo + ")") + + $span.append( $image ) + $span.append( $label ) + + this.$templatesList.append($span) + }.bind(this)) + + data.blueprints.forEach(function(blueprint){ + if (! blueprint.slug) { return } + + var $span = $("<span>") + $span.data("blueprint", true) + $span.data("slug", blueprint.slug) + + var $label = $("<label>") + $label.html( blueprint.name ) + + var $image = $("<span>") + $image.addClass("image").css("background-image", "url(" + blueprint.url + ")") + + $span.append( $image ) + $span.append( $label ) + + this.$templatesList.append($span) + }.bind(this)) + + this.show() + }, + + +})
\ No newline at end of file diff --git a/public/assets/javascripts/ui/site/ProfileView.js b/public/assets/javascripts/ui/site/ProfileView.js index f3b35d9..8471abc 100644 --- a/public/assets/javascripts/ui/site/ProfileView.js +++ b/public/assets/javascripts/ui/site/ProfileView.js @@ -1,6 +1,12 @@ var ProfileView = View.extend({ + el: ".profilepage", + + events: { + "change #profile_avatar": "uploadAvatar", + }, + initialize: function() { }, @@ -12,6 +18,28 @@ var ProfileView = View.extend({ $(".bio").addClass(choice(classes)); this.projectList = new ProjectList () + }, + + uploadAvatar: function(){ + var fd = new FormData(), hasCSRF = false + var files = this.$("#profile_avatar")[0].files + if (! files.length) return + + fd.append("avatar", files[0]); + fd.append("_csrf", $("[name=_csrf]").val()) + + var request = $.ajax({ + url: "/api/profile", + type: "put", + data: fd, + dataType: "json", + processData: false, + contentType: false, + }) + + request.done($.proxy(function (response) { + window.location.href = "/profile" + }, this)); } }) diff --git a/public/assets/javascripts/ui/site/ProjectList.js b/public/assets/javascripts/ui/site/ProjectList.js index 993d805..076a674 100644 --- a/public/assets/javascripts/ui/site/ProjectList.js +++ b/public/assets/javascripts/ui/site/ProjectList.js @@ -1,16 +1,75 @@ +var projectListTimeout = null var ProjectList = View.extend({ el: ".projectList", events: { - "mouseenter .room": 'spinOn', - "mouseleave .room": 'spinOff', + "click .viewMore": 'viewMore', + "mouseenter .room": 'enter', + "mouseleave .room": 'leave', }, initialize: function(){ + this.$viewMore = this.$(".viewMore") + this.$(".images").each(function(){ + $divs = $(this).children("div") + $divs.hide() + $divs.first().show() + $(this).data("index", 0) + }) }, + timeout: null, + enter: function(e){ + clearTimeout(projectListTimeout) + this.advance(e.currentTarget) + }, + + leave: function(e){ + clearTimeout(projectListTimeout) + var $divs = $(e.currentTarget).find(".images div") + $divs.hide() + $divs.eq(0).show() + }, + + advance: function(el){ + projectListTimeout = setTimeout(function(){ + this.advance(el) + }.bind(this), 500) + var $images = $(el).find(".images") + var $divs = $images.children("div") + var index = $images.data("index") + var nextIndex = (index + 1) % $divs.length + $divs.eq(index).hide() + $divs.eq(nextIndex).show() + $images.data("index", nextIndex) + }, + + viewMore: function(e){ + e.preventDefault() + var criteria = {} + criteria.offset = this.$(".projectItem").length + if (window.location.pathname == "/" || window.location.pathname.match("/home")) { + criteria.home = 1 + } + else { + criteria.user_id = this.$(".projectItem").first().data("userid") + } + + $.get("/api/project/paginate", criteria, function(data){ + var offset = this.$viewMore.offset() + var $data = $(data) + var $els = $data.find(".projectItem") + $els.insertBefore( this.$viewMore ) + if (! $data.find(".viewMore").length) { + this.$viewMore.hide() + } + $("body,html").animate({ scrollTop: offset.top - 80 }, 300) + }.bind(this)) + }, + +/* spinOn: function(e){ var iframe = $(e.currentTarget).find("iframe").get('0') if (! iframe) return @@ -22,5 +81,6 @@ var ProjectList = View.extend({ if (! iframe) return iframe.contentWindow.postMessage("spin-off", window.location.origin) } +*/ }) diff --git a/public/assets/javascripts/ui/site/StaffView.js b/public/assets/javascripts/ui/site/StaffView.js index fdf39d2..97f86c2 100644 --- a/public/assets/javascripts/ui/site/StaffView.js +++ b/public/assets/javascripts/ui/site/StaffView.js @@ -3,14 +3,29 @@ var StaffView = View.extend({ events: { "click #toggle-staff": "toggleStaff", + "click #toggle-featured": "toggleFeatured", + "click #toggle-stock": "toggleStock", + "click #toggle-artist": "toggleArtist", }, initialize: function() { this.$toggleStaff = $("#toggle-staff") + this.$toggleFeatured = $("#toggle-featured") + this.$toggleStock = $("#toggle-stock") + this.$toggleArtist = $("#toggle-artist") this.$mediaEmbed = $("#media-embed") if (this.$toggleStaff.length && this.$toggleStaff.data().isstaff) { this.$toggleStaff.html("Is Staff") } + if (this.$toggleFeatured.length && this.$toggleFeatured.data().featured) { + this.$toggleFeatured.html("Featured Project") + } + if (this.$toggleStock.length && this.$toggleStock.data().stock) { + this.$toggleStock.html("Layout is Stock") + } + if (this.$toggleArtist.length && this.$toggleArtist.data().isartist) { + this.$toggleArtist.html("Is Artist") + } if (this.$mediaEmbed.length) { var media = this.$mediaEmbed.data() this.$mediaEmbed.html( Parser.tag( media ) ) @@ -44,6 +59,59 @@ var StaffView = View.extend({ }.bind(this) }) }.bind(this)) - }, + }, + toggleFeatured: function(){ + var state = ! this.$toggleFeatured.data().featured + $.ajax({ + type: "put", + dataType: "json", + url: window.location.href + "/feature", + data: { + state: state, + _csrf: $("#_csrf").val(), + }, + success: function(data){ + this.$toggleFeatured.data("featured", data.state) + this.$toggleFeatured.html(data.state ? "Featured Project" : "Feature this project") + $("#isFeaturedProject").html(data.state ? "yes" : "no") + }.bind(this) + }) + }, + + toggleStock: function(){ + var state = ! this.$toggleStock.data().stock + $.ajax({ + type: "put", + dataType: "json", + url: window.location.href + "/stock", + data: { + state: state, + _csrf: $("#_csrf").val(), + }, + success: function(data){ + this.$toggleStock.data("stock", data.state) + this.$toggleStock.html(data.state ? "Stock Layout" : "Make this layout Stock") + $("#isStockLayout").html(data.state ? "yes" : "no") + }.bind(this) + }) + }, + + toggleArtist: function(){ + var state = ! this.$toggleArtist.data().isartist + $.ajax({ + type: "put", + dataType: "json", + url: window.location.href + "/artist", + data: { + state: state, + _csrf: $("#_csrf").val(), + }, + success: function(data){ + this.$toggleArtist.data("stock", data.state) + this.$toggleArtist.html(data.state ? "Is Artist" : "Make Artist") + $("#isArtist").html(data.state ? "yes" : "no") + }.bind(this) + }) + }, }) diff --git a/public/assets/javascripts/ui/z_share.js b/public/assets/javascripts/ui/z_share.js deleted file mode 100644 index d31aa89..0000000 --- a/public/assets/javascripts/ui/z_share.js +++ /dev/null @@ -1,25 +0,0 @@ -var share = { - init: function(){ - share.bind() - }, - bind: function(){ - $("#facebook").click(share.facebook) - $("#twitter").click(share.twitter) - }, - url: "http://vvalls.com/", - facebook_msg: "", - twitter_msg: "", - openLink: function (url) { - window.open(url, "_blank"); - }, - facebook: function () { - var url = "https://www.facebook.com/share.php?u=" + encodeURIComponent(share.url) + "&t=" + encodeURIComponent(share.facebook_msg); - share.openLink(url); - return false; - }, - twitter: function () { - var url = "https://twitter.com/home?status=" + encodeURIComponent(share.url + " " + share.twitter_msg); - share.openLink(url); - return false; - } -} diff --git a/public/assets/javascripts/util.js b/public/assets/javascripts/util.js index 7812a4d..0f5c6ed 100644 --- a/public/assets/javascripts/util.js +++ b/public/assets/javascripts/util.js @@ -4,11 +4,14 @@ if (window.$) { $.fn.string = function() { return trim($(this).val()) } $.fn.enable = function() { return $(this).attr("disabled",null) } $.fn.disable = function() { return $(this).attr("disabled","disabled") } + $.fn.sanitize = function(s) { return trim(sanitize($(this).val())) } $.fn.htmlSafe = function(s) { return $(this).html(sanitize(s)) } + $.fn.toDollars = function(i) { return $(this).html((i/100).toFixed(2)) } } -function trim(s){ return s.replace(/^\s+/,"").replace(/\s+$/,"") } +function trim (s){ return s.replace(/^\s+/,"").replace(/\s+$/,"") } function sanitize (s){ return (s || "").replace(new RegExp("[<>&]", 'g'), "") } +function stripHTML (s){ return (s || "").replace(/<[^>]+>/g, "") } function capitalize (s){ return s.split(" ").map(capitalizeWord).join(" ") } function capitalizeWord (s){ return s.charAt(0).toUpperCase() + s.slice(1) } function slugify (s){ return (s || "").toLowerCase().replace(/\s/g,"-").replace(/[^-_a-zA-Z0-9]/g, '-').replace(/-+/g,"-") } @@ -92,9 +95,10 @@ function smoothstep(min,max,n){ } function shuffle(a){ + var r, swap for (var i = a.length; i > 0; i--){ - var r = randint(i) - var swap = a[i-1] + r = randint(i) + swap = a[i-1] a[i-1] = a[r] a[r] = swap } @@ -173,9 +177,80 @@ function invert_hash (h) { for (var i in h) { if (h.hasOwnProperty(i)) k[h[i]] = i } return k } +function filenameFromUrl (url) { + var partz = url.split( "/" ) + return partz[partz.length-1].split(".")[0] +} function bitcount(v) { v = v - ((v >>> 1) & 0x55555555); v = (v & 0x33333333) + ((v >>> 2) & 0x33333333); return ((v + (v >>> 4) & 0xF0F0F0F) * 0x1010101) >>> 24; } + +// Function.bind polyfill +if (!Function.prototype.bind) { + Function.prototype.bind = function(oThis) { + if (typeof this !== 'function') { + // closest thing possible to the ECMAScript 5 + // internal IsCallable function + throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable'); + } + + var aArgs = Array.prototype.slice.call(arguments, 1), + fToBind = this, + fNOP = function() {}, + fBound = function() { + return fToBind.apply(this instanceof fNOP && oThis + ? this + : oThis, + aArgs.concat(Array.prototype.slice.call(arguments))); + }; + + fNOP.prototype = this.prototype; + fBound.prototype = new fNOP(); + + return fBound; + }; +} + +// rAF polyfill +(function() { + var lastTime = 0; + var vendors = ['ms', 'moz', 'webkit', 'o']; + for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { + window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; + window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] + || window[vendors[x]+'CancelRequestAnimationFrame']; + } + + if (!window.requestAnimationFrame) + window.requestAnimationFrame = function(callback, element) { + var currTime = new Date().getTime(); + var timeToCall = Math.max(0, 16 - (currTime - lastTime)); + var id = window.setTimeout(function() { callback(currTime + timeToCall); }, + timeToCall); + lastTime = currTime + timeToCall; + return id; + }; + + if (!window.cancelAnimationFrame) + window.cancelAnimationFrame = function(id) { + clearTimeout(id); + }; +}()); + + +function selectElementContents(el) { + if (window.getSelection && document.createRange) { + var sel = window.getSelection(); + var range = document.createRange(); + range.selectNodeContents(el); + sel.removeAllRanges(); + sel.addRange(range); + } else if (document.selection && document.body.createTextRange) { + var textRange = document.body.createTextRange(); + textRange.moveToElementText(el); + textRange.select(); + } +} diff --git a/public/assets/javascripts/vendor/canvasutilities.js b/public/assets/javascripts/vendor/canvasutilities.js new file mode 100644 index 0000000..011ebb0 --- /dev/null +++ b/public/assets/javascripts/vendor/canvasutilities.js @@ -0,0 +1,145 @@ +var drawArrow = function(ctx, x1, y1, x2, y2, style, which, angle, d) { + 'use strict'; + // Ceason pointed to a problem when x1 or y1 were a string, and concatenation + // would happen instead of addition + if (typeof(x1) == 'string') x1 = parseInt(x1); + if (typeof(y1) == 'string') y1 = parseInt(y1); + if (typeof(x2) == 'string') x2 = parseInt(x2); + if (typeof(y2) == 'string') y2 = parseInt(y2); + style = typeof(style) != 'undefined' ? style : 3; + which = typeof(which) != 'undefined' ? which : 1; // end point gets arrow + angle = typeof(angle) != 'undefined' ? angle : Math.PI / 8; + d = typeof(d) != 'undefined' ? d : 10; + // default to using drawHead to draw the head, but if the style + // argument is a function, use it instead + var toDrawHead = typeof(style) != 'function' ? drawHead : style; + + // For ends with arrow we actually want to stop before we get to the arrow + // so that wide lines won't put a flat end on the arrow. + // + var dist = Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)); + var ratio = (dist - d / 3) / dist; + var tox, toy, fromx, fromy; + if (which & 1) { + tox = Math.round(x1 + (x2 - x1) * ratio); + toy = Math.round(y1 + (y2 - y1) * ratio); + } else { + tox = x2; + toy = y2; + } + if (which & 2) { + fromx = x1 + (x2 - x1) * (1 - ratio); + fromy = y1 + (y2 - y1) * (1 - ratio); + } else { + fromx = x1; + fromy = y1; + } + + // Draw the shaft of the arrow + ctx.beginPath(); + ctx.moveTo(fromx, fromy); + ctx.lineTo(tox, toy); + ctx.stroke(); + + // calculate the angle of the line + var lineangle = Math.atan2(y2 - y1, x2 - x1); + // h is the line length of a side of the arrow head + var h = Math.abs(d / Math.cos(angle)); + + if (which & 1) { // handle far end arrow head + var angle1 = lineangle + Math.PI + angle; + var topx = x2 + Math.cos(angle1) * h; + var topy = y2 + Math.sin(angle1) * h; + var angle2 = lineangle + Math.PI - angle; + var botx = x2 + Math.cos(angle2) * h; + var boty = y2 + Math.sin(angle2) * h; + toDrawHead(ctx, topx, topy, x2, y2, botx, boty, style); + } + if (which & 2) { // handle near end arrow head + var angle1 = lineangle + angle; + var topx = x1 + Math.cos(angle1) * h; + var topy = y1 + Math.sin(angle1) * h; + var angle2 = lineangle - angle; + var botx = x1 + Math.cos(angle2) * h; + var boty = y1 + Math.sin(angle2) * h; + toDrawHead(ctx, topx, topy, x1, y1, botx, boty, style); + } +} + +var drawHead = function(ctx, x0, y0, x1, y1, x2, y2, style) { + 'use strict'; + if (typeof(x0) == 'string') x0 = parseInt(x0); + if (typeof(y0) == 'string') y0 = parseInt(y0); + if (typeof(x1) == 'string') x1 = parseInt(x1); + if (typeof(y1) == 'string') y1 = parseInt(y1); + if (typeof(x2) == 'string') x2 = parseInt(x2); + if (typeof(y2) == 'string') y2 = parseInt(y2); + var radius = 3; + var twoPI = 2 * Math.PI; + + // all cases do this. + ctx.save(); + ctx.beginPath(); + ctx.moveTo(x0, y0); + ctx.lineTo(x1, y1); + ctx.lineTo(x2, y2); + switch (style) { + case 0: + // curved filled, add the bottom as an arcTo curve and fill + var backdist = Math.sqrt(((x2 - x0) * (x2 - x0)) + ((y2 - y0) * (y2 - y0))); + ctx.arcTo(x1, y1, x0, y0, .55 * backdist); + ctx.fill(); + break; + case 1: + // straight filled, add the bottom as a line and fill. + ctx.beginPath(); + ctx.moveTo(x0, y0); + ctx.lineTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.lineTo(x0, y0); + ctx.fill(); + break; + case 2: + // unfilled head, just stroke. + ctx.stroke(); + break; + case 3: + //filled head, add the bottom as a quadraticCurveTo curve and fill + var cpx = (x0 + x1 + x2) / 3; + var cpy = (y0 + y1 + y2) / 3; + ctx.quadraticCurveTo(cpx, cpy, x0, y0); + ctx.fill(); + break; + case 4: + //filled head, add the bottom as a bezierCurveTo curve and fill + var cp1x, cp1y, cp2x, cp2y, backdist; + var shiftamt = 5; + if (x2 == x0) { + // Avoid a divide by zero if x2==x0 + backdist = y2 - y0; + cp1x = (x1 + x0) / 2; + cp2x = (x1 + x0) / 2; + cp1y = y1 + backdist / shiftamt; + cp2y = y1 - backdist / shiftamt; + } else { + backdist = Math.sqrt(((x2 - x0) * (x2 - x0)) + ((y2 - y0) * (y2 - y0))); + var xback = (x0 + x2) / 2; + var yback = (y0 + y2) / 2; + var xmid = (xback + x1) / 2; + var ymid = (yback + y1) / 2; + + var m = (y2 - y0) / (x2 - x0); + var dx = (backdist / (2 * Math.sqrt(m * m + 1))) / shiftamt; + var dy = m * dx; + cp1x = xmid - dx; + cp1y = ymid - dy; + cp2x = xmid + dx; + cp2y = ymid + dy; + } + + ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x0, y0); + ctx.fill(); + break; + } + ctx.restore(); +} diff --git a/public/assets/javascripts/vendor/chardinjs.min.js b/public/assets/javascripts/vendor/chardinjs.min.js new file mode 100755 index 0000000..56cd7b7 --- /dev/null +++ b/public/assets/javascripts/vendor/chardinjs.min.js @@ -0,0 +1,2 @@ +// Generated by CoffeeScript 1.6.2 +(function(){var e=[].slice;(function(t,n){var r;return r=function(){function e(e){var r=this;this.$el=t(e),t(n).resize(function(){return r.refresh()})}return e.prototype.start=function(){var e,t,n,r;if(this._overlay_visible())return!1;this._add_overlay_layer(),r=this.$el.find("*[data-intro]:visible");for(t=0,n=r.length;t<n;t++)e=r[t],this._show_element(e);return this.$el.trigger("chardinJs:start")},e.prototype.toggle=function(){return this._overlay_visible()?this.stop():this.start()},e.prototype.refresh=function(){var e,t,n,r,i;if(this._overlay_visible()){r=this.$el.find("*[data-intro]:visible"),i=[];for(t=0,n=r.length;t<n;t++)e=r[t],i.push(this._position_helper_layer(e));return i}return this},e.prototype.stop=function(){return this.$el.find(".chardinjs-overlay").fadeOut(function(){return t(this).remove()}),this.$el.find(".chardinjs-helper-layer").remove(),this.$el.find(".chardinjs-show-element").removeClass("chardinjs-show-element"),this.$el.find(".chardinjs-relative-position").removeClass("chardinjs-relative-position"),n.removeEventListener?n.removeEventListener("keydown",this._onKeyDown,!0):document.detachEvent&&document.detachEvent("onkeydown",this._onKeyDown),this.$el.trigger("chardinJs:stop")},e.prototype._overlay_visible=function(){return this.$el.find(".chardinjs-overlay").length!==0},e.prototype._add_overlay_layer=function(){var e,t,n,r=this;return this._overlay_visible()?!1:(t=document.createElement("div"),n="",t.className="chardinjs-overlay",this.$el.prop("tagName")==="BODY"?(n+="top: 0;bottom: 0; left: 0;right: 0;position: fixed;",t.setAttribute("style",n)):(e=this._get_offset(this.$el.get()[0]),e&&(n+="width: "+e.width+"px; height:"+e.height+"px; top:"+e.top+"px;left: "+e.left+"px;",t.setAttribute("style",n))),this.$el.get()[0].appendChild(t),t.onclick=function(){return r.stop()},setTimeout(function(){return n+="opacity: .8;opacity: .8;-ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=80)';filter: alpha(opacity=80);",t.setAttribute("style",n)},10))},e.prototype._get_position=function(e){return e.getAttribute("data-position")||"bottom"},e.prototype._place_tooltip=function(e){var n,r,i,s,o,u,a;u=t(e).data("tooltip_layer"),a=this._get_offset(u),u.style.top=null,u.style.right=null,u.style.bottom=null,u.style.left=null;switch(this._get_position(e)){case"top":case"bottom":i=this._get_offset(e),o=i.width,r=t(u).width(),u.style.left=""+(o/2-a.width/2)+"px";break;case"left":case"right":i=this._get_offset(e),s=i.height,n=t(u).height(),u.style.top=""+(s/2-a.height/2)+"px"}switch(this._get_position(e)){case"left":return u.style.left="-"+(a.width-34)+"px";case"right":return u.style.right="-"+(a.width-34)+"px";case"bottom":return u.style.bottom="-"+a.height+"px";case"top":return u.style.top="-"+a.height+"px"}},e.prototype._position_helper_layer=function(e){var n,r;return r=t(e).data("helper_layer"),n=this._get_offset(e),r.setAttribute("style","width: "+n.width+"px; height:"+n.height+"px; top:"+n.top+"px; left: "+n.left+"px;")},e.prototype._show_element=function(e){var n,r,i,s;r=this._get_offset(e),i=document.createElement("div"),s=document.createElement("div"),t(e).data("helper_layer",i).data("tooltip_layer",s),e.id&&i.setAttribute("data-id",e.id),i.className="chardinjs-helper-layer chardinjs-"+this._get_position(e),this._position_helper_layer(e),this.$el.get()[0].appendChild(i),s.className="chardinjs-tooltip chardinjs-"+this._get_position(e),s.innerHTML="<div class='chardinjs-tooltiptext'>"+e.getAttribute("data-intro")+"</div>",i.appendChild(s),this._place_tooltip(e),e.className+=" chardinjs-show-element",n="",e.currentStyle?n=e.currentStyle.position:document.defaultView&&document.defaultView.getComputedStyle&&(n=document.defaultView.getComputedStyle(e,null).getPropertyValue("position")),n=n.toLowerCase();if(n!=="absolute"&&n!=="relative")return e.className+=" chardinjs-relative-position"},e.prototype._get_offset=function(e){var t,n,r;t={width:e.offsetWidth,height:e.offsetHeight},n=0,r=0;while(e&&!isNaN(e.offsetLeft)&&!isNaN(e.offsetTop))n+=e.offsetLeft,r+=e.offsetTop,e=e.offsetParent;return t.top=r,t.left=n,t},e}(),t.fn.extend({chardinJs:function(){var n,i,s,o;return o=arguments[0],i=2<=arguments.length?e.call(arguments,1):[],n=t(this[0]),s=n.data("chardinJs"),s||n.data("chardinJs",s=new r(this,o)),typeof o=="string"&&s[o].apply(s,i),s}})})(window.jQuery,window)}).call(this);
\ No newline at end of file diff --git a/public/assets/javascripts/vendor/froogaloop.js b/public/assets/javascripts/vendor/froogaloop.js new file mode 100644 index 0000000..c9330e6 --- /dev/null +++ b/public/assets/javascripts/vendor/froogaloop.js @@ -0,0 +1,287 @@ +// Init style shamelessly stolen from jQuery http://jquery.com +var Froogaloop = (function(){ + // Define a local copy of Froogaloop + function Froogaloop(iframe) { + // The Froogaloop object is actually just the init constructor + return new Froogaloop.fn.init(iframe); + } + + var eventCallbacks = {}, + hasWindowEvent = false, + isReady = false, + slice = Array.prototype.slice, + playerDomain = ''; + + Froogaloop.fn = Froogaloop.prototype = { + element: null, + + init: function(iframe) { + if (typeof iframe === "string") { + iframe = document.getElementById(iframe); + } + + this.element = iframe; + + // Register message event listeners + playerDomain = getDomainFromUrl(this.element.getAttribute('src')); + + return this; + }, + + /* + * Calls a function to act upon the player. + * + * @param {string} method The name of the Javascript API method to call. Eg: 'play'. + * @param {Array|Function} valueOrCallback params Array of parameters to pass when calling an API method + * or callback function when the method returns a value. + */ + api: function(method, valueOrCallback) { + if (!this.element || !method) { + return false; + } + + var self = this, + element = self.element, + target_id = element.id !== '' ? element.id : null, + params = !isFunction(valueOrCallback) ? valueOrCallback : null, + callback = isFunction(valueOrCallback) ? valueOrCallback : null; + + // Store the callback for get functions + if (callback) { + storeCallback(method, callback, target_id); + } + + postMessage(method, params, element); + return self; + }, + + /* + * Registers an event listener and a callback function that gets called when the event fires. + * + * @param eventName (String): Name of the event to listen for. + * @param callback (Function): Function that should be called when the event fires. + */ + addEvent: function(eventName, callback) { + if (!this.element) { + return false; + } + + var self = this, + element = self.element, + target_id = element.id !== '' ? element.id : null; + + storeCallback(eventName, callback, target_id); + + // The ready event is not registered via postMessage. It fires regardless. + if (eventName != 'ready') { + postMessage('addEventListener', eventName, element); + } + else if (eventName == 'ready' && isReady) { + callback.call(null, target_id); + } + + return self; + }, + + /* + * Unregisters an event listener that gets called when the event fires. + * + * @param eventName (String): Name of the event to stop listening for. + */ + removeEvent: function(eventName) { + if (!this.element) { + return false; + } + + var self = this, + element = self.element, + target_id = element.id !== '' ? element.id : null, + removed = removeCallback(eventName, target_id); + + // The ready event is not registered + if (eventName != 'ready' && removed) { + postMessage('removeEventListener', eventName, element); + } + } + }; + + /** + * Handles posting a message to the parent window. + * + * @param method (String): name of the method to call inside the player. For api calls + * this is the name of the api method (api_play or api_pause) while for events this method + * is api_addEventListener. + * @param params (Object or Array): List of parameters to submit to the method. Can be either + * a single param or an array list of parameters. + * @param target (HTMLElement): Target iframe to post the message to. + */ + function postMessage(method, params, target) { + if (!target.contentWindow.postMessage) { + return false; + } + + var url = target.getAttribute('src').split('?')[0], + data = JSON.stringify({ + method: method, + value: params + }); + + if (url.substr(0, 2) === '//') { + url = window.location.protocol + url; + } + + target.contentWindow.postMessage(data, url); + } + + /** + * Event that fires whenever the window receives a message from its parent + * via window.postMessage. + */ + function onMessageReceived(event) { + var data, method; + + try { + data = JSON.parse(event.data); + method = data.event || data.method; + } + catch(e) { + //fail silently... like a ninja! + } + + if (method == 'ready' && !isReady) { + isReady = true; + } + + // Handles messages from moogaloop only + if (event.origin != playerDomain) { + return false; + } + + var value = data.value, + eventData = data.data, + target_id = target_id === '' ? null : data.player_id, + + callback = getCallback(method, target_id), + params = []; + + if (!callback) { + return false; + } + + if (value !== undefined) { + params.push(value); + } + + if (eventData) { + params.push(eventData); + } + + if (target_id) { + params.push(target_id); + } + + return params.length > 0 ? callback.apply(null, params) : callback.call(); + } + + + /** + * Stores submitted callbacks for each iframe being tracked and each + * event for that iframe. + * + * @param eventName (String): Name of the event. Eg. api_onPlay + * @param callback (Function): Function that should get executed when the + * event is fired. + * @param target_id (String) [Optional]: If handling more than one iframe then + * it stores the different callbacks for different iframes based on the iframe's + * id. + */ + function storeCallback(eventName, callback, target_id) { + if (target_id) { + if (!eventCallbacks[target_id]) { + eventCallbacks[target_id] = {}; + } + eventCallbacks[target_id][eventName] = callback; + } + else { + eventCallbacks[eventName] = callback; + } + } + + /** + * Retrieves stored callbacks. + */ + function getCallback(eventName, target_id) { + if (target_id) { + return eventCallbacks[target_id][eventName]; + } + else { + return eventCallbacks[eventName]; + } + } + + function removeCallback(eventName, target_id) { + if (target_id && eventCallbacks[target_id]) { + if (!eventCallbacks[target_id][eventName]) { + return false; + } + eventCallbacks[target_id][eventName] = null; + } + else { + if (!eventCallbacks[eventName]) { + return false; + } + eventCallbacks[eventName] = null; + } + + return true; + } + + /** + * Returns a domain's root domain. + * Eg. returns http://vimeo.com when http://vimeo.com/channels is sbumitted + * + * @param url (String): Url to test against. + * @return url (String): Root domain of submitted url + */ + function getDomainFromUrl(url) { + if (url.substr(0, 2) === '//') { + url = window.location.protocol + url; + } + + var url_pieces = url.split('/'), + domain_str = ''; + + for(var i = 0, length = url_pieces.length; i < length; i++) { + if(i<3) {domain_str += url_pieces[i];} + else {break;} + if(i<2) {domain_str += '/';} + } + + return domain_str; + } + + function isFunction(obj) { + return !!(obj && obj.constructor && obj.call && obj.apply); + } + + function isArray(obj) { + return toString.call(obj) === '[object Array]'; + } + + // Give the init function the Froogaloop prototype for later instantiation + Froogaloop.fn.init.prototype = Froogaloop.fn; + + // Listens for the message event. + // W3C + if (window.addEventListener) { + window.addEventListener('message', onMessageReceived, false); + } + // IE + else { + window.attachEvent('onmessage', onMessageReceived); + } + + // Expose froogaloop to the global object + return (window.Froogaloop = window.$f = Froogaloop); + +})();
\ No newline at end of file diff --git a/public/assets/javascripts/vendor/polyfill.js b/public/assets/javascripts/vendor/polyfill.js index f97e438..2139618 100644 --- a/public/assets/javascripts/vendor/polyfill.js +++ b/public/assets/javascripts/vendor/polyfill.js @@ -48,10 +48,18 @@ function has3d(){ } else if ( browser.webkit ) { browser.safari = true; } - $.browser = browser; + if (window.$) $.browser = browser; return browser; })( navigator.userAgent ); +// Naive useragent detection pattern +var is_iphone = (navigator.userAgent.match(/iPhone/i)) || (navigator.userAgent.match(/iPod/i)) +var is_ipad = (navigator.userAgent.match(/iPad/i)) +var is_android = (navigator.userAgent.match(/Android/i)) +var is_mobile = is_iphone || is_ipad || is_android +var is_desktop = ! is_mobile; +var app_devicePixelRatio = is_mobile ? devicePixelRatio : 1; + // rAF shim (function() { diff --git a/public/assets/javascripts/vendor/wow.js b/public/assets/javascripts/vendor/wow.js new file mode 100644 index 0000000..83ca71b --- /dev/null +++ b/public/assets/javascripts/vendor/wow.js @@ -0,0 +1,452 @@ +(function() { + var MutationObserver, Util, WeakMap, getComputedStyle, getComputedStyleRX, + __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, + __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; + + Util = (function() { + function Util() {} + + Util.prototype.extend = function(custom, defaults) { + var key, value; + for (key in defaults) { + value = defaults[key]; + if (custom[key] == null) { + custom[key] = value; + } + } + return custom; + }; + + Util.prototype.isMobile = function(agent) { + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(agent); + }; + + Util.prototype.addEvent = function(elem, event, fn) { + if (elem.addEventListener != null) { + return elem.addEventListener(event, fn, false); + } else if (elem.attachEvent != null) { + return elem.attachEvent("on" + event, fn); + } else { + return elem[event] = fn; + } + }; + + Util.prototype.removeEvent = function(elem, event, fn) { + if (elem.removeEventListener != null) { + return elem.removeEventListener(event, fn, false); + } else if (elem.detachEvent != null) { + return elem.detachEvent("on" + event, fn); + } else { + return delete elem[event]; + } + }; + + Util.prototype.innerHeight = function() { + if ('innerHeight' in window) { + return window.innerHeight; + } else { + return document.documentElement.clientHeight; + } + }; + + return Util; + + })(); + + WeakMap = this.WeakMap || this.MozWeakMap || (WeakMap = (function() { + function WeakMap() { + this.keys = []; + this.values = []; + } + + WeakMap.prototype.get = function(key) { + var i, item, _i, _len, _ref; + _ref = this.keys; + for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) { + item = _ref[i]; + if (item === key) { + return this.values[i]; + } + } + }; + + WeakMap.prototype.set = function(key, value) { + var i, item, _i, _len, _ref; + _ref = this.keys; + for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) { + item = _ref[i]; + if (item === key) { + this.values[i] = value; + return; + } + } + this.keys.push(key); + return this.values.push(value); + }; + + return WeakMap; + + })()); + + MutationObserver = this.MutationObserver || this.WebkitMutationObserver || this.MozMutationObserver || (MutationObserver = (function() { + function MutationObserver() { + if (typeof console !== "undefined" && console !== null) { + console.warn('MutationObserver is not supported by your browser.'); + } + if (typeof console !== "undefined" && console !== null) { + console.warn('WOW.js cannot detect dom mutations, please call .sync() after loading new content.'); + } + } + + MutationObserver.notSupported = true; + + MutationObserver.prototype.observe = function() {}; + + return MutationObserver; + + })()); + + getComputedStyle = this.getComputedStyle || function(el, pseudo) { + this.getPropertyValue = function(prop) { + var _ref; + if (prop === 'float') { + prop = 'styleFloat'; + } + if (getComputedStyleRX.test(prop)) { + prop.replace(getComputedStyleRX, function(_, char) { + return char.toUpperCase(); + }); + } + return ((_ref = el.currentStyle) != null ? _ref[prop] : void 0) || null; + }; + return this; + }; + + getComputedStyleRX = /(\-([a-z]){1})/g; + + this.WOW = (function() { + WOW.prototype.defaults = { + boxClass: 'wow', + animateClass: 'animated', + offset: 0, + mobile: true, + live: true + }; + + function WOW(options) { + if (options == null) { + options = {}; + } + this.scrollCallback = __bind(this.scrollCallback, this); + this.scrollHandler = __bind(this.scrollHandler, this); + this.start = __bind(this.start, this); + this.scrolled = true; + this.config = this.util().extend(options, this.defaults); + this.animationNameCache = new WeakMap(); + } + + WOW.prototype.init = function() { + var _ref; + this.element = window.document.documentElement; + if ((_ref = document.readyState) === "interactive" || _ref === "complete") { + this.start(); + } else { + this.util().addEvent(document, 'DOMContentLoaded', this.start); + } + return this.finished = []; + }; + + WOW.prototype.start = function() { + var box, _i, _len, _ref; + this.stopped = false; + this.boxes = (function() { + var _i, _len, _ref, _results; + _ref = this.element.querySelectorAll("." + this.config.boxClass); + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + box = _ref[_i]; + _results.push(box); + } + return _results; + }).call(this); + this.all = (function() { + var _i, _len, _ref, _results; + _ref = this.boxes; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + box = _ref[_i]; + _results.push(box); + } + return _results; + }).call(this); + if (this.boxes.length) { + if (this.disabled()) { + this.resetStyle(); + } else { + _ref = this.boxes; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + box = _ref[_i]; + this.applyStyle(box, true); + } + this.util().addEvent(window, 'scroll', this.scrollHandler); + this.util().addEvent(window, 'resize', this.scrollHandler); + this.interval = setInterval(this.scrollCallback, 50); + } + } + if (this.config.live) { + return new MutationObserver((function(_this) { + return function(records) { + var node, record, _j, _len1, _results; + _results = []; + for (_j = 0, _len1 = records.length; _j < _len1; _j++) { + record = records[_j]; + _results.push((function() { + var _k, _len2, _ref1, _results1; + _ref1 = record.addedNodes || []; + _results1 = []; + for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) { + node = _ref1[_k]; + _results1.push(this.doSync(node)); + } + return _results1; + }).call(_this)); + } + return _results; + }; + })(this)).observe(document.body, { + childList: true, + subtree: true + }); + } + }; + + WOW.prototype.stop = function() { + this.stopped = true; + this.util().removeEvent(window, 'scroll', this.scrollHandler); + this.util().removeEvent(window, 'resize', this.scrollHandler); + if (this.interval != null) { + return clearInterval(this.interval); + } + }; + + WOW.prototype.sync = function(element) { + if (MutationObserver.notSupported) { + return this.doSync(this.element); + } + }; + + WOW.prototype.doSync = function(element) { + var box, _i, _len, _ref, _results; + if (!this.stopped) { + if (element == null) { + element = this.element; + } + if (element.nodeType !== 1) { + return; + } + element = element.parentNode || element; + _ref = element.querySelectorAll("." + this.config.boxClass); + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + box = _ref[_i]; + if (__indexOf.call(this.all, box) < 0) { + this.applyStyle(box, true); + this.boxes.push(box); + this.all.push(box); + _results.push(this.scrolled = true); + } else { + _results.push(void 0); + } + } + return _results; + } + }; + + WOW.prototype.show = function(box) { + this.applyStyle(box); + return box.className = "" + box.className + " " + this.config.animateClass; + }; + + WOW.prototype.applyStyle = function(box, hidden) { + var delay, duration, iteration; + duration = box.getAttribute('data-wow-duration'); + delay = box.getAttribute('data-wow-delay'); + iteration = box.getAttribute('data-wow-iteration'); + return this.animate((function(_this) { + return function() { + return _this.customStyle(box, hidden, duration, delay, iteration); + }; + })(this)); + }; + + WOW.prototype.animate = (function() { + if ('requestAnimationFrame' in window) { + return function(callback) { + return window.requestAnimationFrame(callback); + }; + } else { + return function(callback) { + return callback(); + }; + } + })(); + + WOW.prototype.resetStyle = function() { + var box, _i, _len, _ref, _results; + _ref = this.boxes; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + box = _ref[_i]; + _results.push(box.setAttribute('style', 'visibility: visible;')); + } + return _results; + }; + + WOW.prototype.customStyle = function(box, hidden, duration, delay, iteration) { + if (hidden) { + this.cacheAnimationName(box); + } + box.style.visibility = hidden ? 'hidden' : 'visible'; + if (duration) { + this.vendorSet(box.style, { + animationDuration: duration + }); + } + if (delay) { + this.vendorSet(box.style, { + animationDelay: delay + }); + } + if (iteration) { + this.vendorSet(box.style, { + animationIterationCount: iteration + }); + } + this.vendorSet(box.style, { + animationName: hidden ? 'none' : this.cachedAnimationName(box) + }); + return box; + }; + + WOW.prototype.vendors = ["moz", "webkit"]; + + WOW.prototype.vendorSet = function(elem, properties) { + var name, value, vendor, _results; + _results = []; + for (name in properties) { + value = properties[name]; + elem["" + name] = value; + _results.push((function() { + var _i, _len, _ref, _results1; + _ref = this.vendors; + _results1 = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + vendor = _ref[_i]; + _results1.push(elem["" + vendor + (name.charAt(0).toUpperCase()) + (name.substr(1))] = value); + } + return _results1; + }).call(this)); + } + return _results; + }; + + WOW.prototype.vendorCSS = function(elem, property) { + var result, style, vendor, _i, _len, _ref; + style = getComputedStyle(elem); + result = style.getPropertyCSSValue(property); + _ref = this.vendors; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + vendor = _ref[_i]; + result = result || style.getPropertyCSSValue("-" + vendor + "-" + property); + } + return result; + }; + + WOW.prototype.animationName = function(box) { + var animationName; + try { + animationName = this.vendorCSS(box, 'animation-name').cssText; + } catch (_error) { + animationName = getComputedStyle(box).getPropertyValue('animation-name'); + } + if (animationName === 'none') { + return ''; + } else { + return animationName; + } + }; + + WOW.prototype.cacheAnimationName = function(box) { + return this.animationNameCache.set(box, this.animationName(box)); + }; + + WOW.prototype.cachedAnimationName = function(box) { + return this.animationNameCache.get(box); + }; + + WOW.prototype.scrollHandler = function() { + return this.scrolled = true; + }; + + WOW.prototype.scrollCallback = function() { + var box; + if (this.scrolled) { + this.scrolled = false; + this.boxes = (function() { + var _i, _len, _ref, _results; + _ref = this.boxes; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + box = _ref[_i]; + if (!(box)) { + continue; + } + if (this.isVisible(box)) { + this.show(box); + continue; + } + _results.push(box); + } + return _results; + }).call(this); + if (!(this.boxes.length || this.config.live)) { + return this.stop(); + } + } + }; + + WOW.prototype.offsetTop = function(element) { + var top; + while (element.offsetTop === void 0) { + element = element.parentNode; + } + top = element.offsetTop; + while (element = element.offsetParent) { + top += element.offsetTop; + } + return top; + }; + + WOW.prototype.isVisible = function(box) { + var bottom, offset, top, viewBottom, viewTop; + offset = box.getAttribute('data-wow-offset') || this.config.offset; + viewTop = window.pageYOffset; + viewBottom = viewTop + Math.min(this.element.clientHeight, this.util().innerHeight()) - offset; + top = this.offsetTop(box); + bottom = top + box.clientHeight; + return top <= viewBottom && bottom >= viewTop; + }; + + WOW.prototype.util = function() { + return this._util != null ? this._util : this._util = new Util(); + }; + + WOW.prototype.disabled = function() { + return !this.config.mobile && this.util().isMobile(navigator.userAgent); + }; + + return WOW; + + })(); + +}).call(this); |
