define [ 'jquery', 'underscore', 'backbone', 'raphael', 'scale.raphael', 'globals', 'cs!utils', 'cs!models/whiteboard_cursor', 'cs!models/whiteboard_slide', 'cs!models/whiteboard_rect', 'cs!models/whiteboard_line', 'cs!models/whiteboard_ellipse', 'cs!models/whiteboard_triangle', 'cs!models/whiteboard_text' ], ($, _, Backbone, Raphael, ScaleRaphael, globals, Utils, WhiteboardCursorModel, WhiteboardSlideModel, WhiteboardRectModel, WhiteboardLineModel, WhiteboardEllipseModel, WhiteboardTriangleModel, WhiteboardTextModel) -> # "Paper" which is the Raphael term for the entire SVG object on the webpage. # This class deals with this SVG component only. WhiteboardPaperModel = Backbone.Model.extend # Container must be a DOM element initialize: (@container) -> # a WhiteboardCursorModel @cursor = null # all slides in the presentation indexed by url @slides = {} # the slide being shown @currentSlide = null @fitToPage = true @panX = null @panY = null # a raphaeljs set with all the shapes in the current slide @currentShapes = null # a list of shapes as passed to this client when it receives `all_slides` # (se we are able to redraw the shapes whenever needed) @currentShapesDefinitions = [] # pointers to the current shapes being drawn @currentLine = null @currentRect = null @currentEllipse = null @currentTriangle = null @currentText = null @zoomLevel = 1 @shiftPressed = false @currentPathCount = 0 $(window).on "resize.whiteboard_paper", _.bind(@_onWindowResize, @) $(document).on "keydown.whiteboard_paper", _.bind(@_onKeyDown, @) $(document).on "keyup.whiteboard_paper", _.bind(@_onKeyUp, @) @_updateContainerDimensions() # Override the close() to unbind events. unbindEvents: -> $(window).off "resize.whiteboard_paper" $(document).off "keydown.whiteboard_paper" $(document).off "keyup.whiteboard_paper" # TODO: other events are being used in the code and should be off() here # Initializes the paper in the page. # Can't do these things in initialize() because by then some elements # are not yet created in the page. create: -> # paper is embedded within the div#slide of the page. @raphaelObj ?= ScaleRaphael(@container, "100%", "100%") @raphaelObj.canvas.setAttribute "preserveAspectRatio", "xMinYMin slice" @cursor = new WhiteboardCursorModel(@raphaelObj) @cursor.draw() @cursor.on "cursor:mousewheel", _.bind(@_zoomSlide, @) if @slides @rebuild() else @slides = {} # if previously loaded unless navigator.userAgent.indexOf("Firefox") is -1 @raphaelObj.renderfix() # Re-add the images to the paper that are found # in the slides array (an object of urls and dimensions). rebuild: -> @currentSlide = null for url of @slides if @slides.hasOwnProperty(url) @addImageToPaper url, @slides[url].getWidth(), @slides[url].getHeight() # A wrapper around ScaleRaphael's `changeSize()` method, more details at: # http://www.shapevent.com/scaleraphael/ # Also makes sure that the images are redraw in the canvas so they are actually resized. changeSize: (windowWidth, windowHeight, center=true, clipping=false) -> if @raphaelObj? @raphaelObj.changeSize(windowWidth, windowHeight, center, clipping) # TODO: we can scale the slides and drawings instead of re-adding them, but the logic # will change quite a bit # slides slidesTmp = _.clone(@slides) urlTmp = @currentSlide @removeAllImagesFromPaper() @slides = slidesTmp @rebuild() @showImageFromPaper(urlTmp.url) # drawings tmp = _.clone(@currentShapesDefinitions) @clearShapes() @drawListOfShapes(tmp) # Add an image to the paper. # @param {string} url the URL of the image to add to the paper # @param {number} width the width of the image (in pixels) # @param {number} height the height of the image (in pixels) # @return {Raphael.image} the image object added to the whiteboard addImageToPaper: (url, width, height) -> @_updateContainerDimensions() console.log "adding image to paper", url, width, height if @fitToPage # solve for the ratio of what length is going to fit more than the other max = Math.max(width / @containerWidth, height / @containerHeight) # fit it all in appropriately # TODO: temporary solution url = @_slideUrl(url) sw = width / max sh = height / max cx = (@containerWidth / 2) - (width / 2) cy = (@containerHeight / 2) - (height / 2) img = @raphaelObj.image(url, cx, cy, width, height) originalWidth = width originalHeight = height else # fit to width console.log "no fit" # assume it will fit width ways sw = width / wr sh = height / wr wr = width / @containerWidth originalWidth = sw originalHeight = sh sw = width / wr sh = height / wr img = @raphaelObj.image(url, cx = 0, cy = 0, sw, sh) # sw slide width as percentage of original width of paper # sh slide height as a percentage of original height of paper # x-offset from top left corner as percentage of original width of paper # y-offset from top left corner as percentage of original height of paper @slides[url] = new WhiteboardSlideModel(img.id, url, img, originalWidth, originalHeight, sw, sh, cx, cy) unless @currentSlide? img.toBack() @currentSlide = @slides[url] else if @currentSlide.url is url img.toBack() else img.hide() $(@container).on "mousemove", _.bind(@_onMouseMove, @) $(@container).on "mousewheel", _.bind(@_zoomSlide, @) # TODO $(img.node).bind "mousewheel", zoomSlide @trigger('paper:image:added', img) # TODO: other places might also required an update in these dimensions @_updateContainerDimensions() img # Removes all the images from the Raphael paper. removeAllImagesFromPaper: -> for url of @slides if @slides.hasOwnProperty(url) @raphaelObj.getById(@slides[url].getId()).remove() @trigger('paper:image:removed', @slides[url].getId()) @slides = {} @currentSlide = null # Shows an image from the paper. # The url must be in the slides array. # @param {string} url the url of the image (must be in slides array) showImageFromPaper: (url) -> # TODO: temporary solution url = @_slideUrl(url) if not @currentSlide? or (@slides[url]? and @currentSlide.url isnt url) @_hideImageFromPaper(@currentSlide.url) if @currentSlide? next = @_getImageFromPaper(url) if next next.show() next.toFront() @currentShapes.forEach (element) -> element.toFront() @cursor.toFront() @currentSlide = @slides[url] # Updates the paper from the server values. # @param {number} cx_ the x-offset value as a percentage of the original width # @param {number} cy_ the y-offset value as a percentage of the original height # @param {number} sw_ the slide width value as a percentage of the original width # @param {number} sh_ the slide height value as a percentage of the original height # TODO: not tested yet updatePaperFromServer: (cx_, cy_, sw_, sh_) -> # if updating the slide size (zooming!) [slideWidth, slideHeight] = @_currentSlideOriginalDimensions() if sw_ and sh_ @raphaelObj.setViewBox cx_ * slideWidth, cy_ * slideHeight, sw_ * slideWidth, sh_ * slideHeight, sw = slideWidth / sw_ sh = slideHeight / sh_ # just panning, so use old slide size values else [sw, sh] = @_currentSlideDimensions() @raphaelObj.setViewBox cx_ * slideWidth, cy_ * slideHeight, @raphaelObj._viewBox[2], @raphaelObj._viewBox[3] # update corners cx = cx_ * sw cy = cy_ * sh # update position of svg object in the window sx = (@containerWidth - slideWidth) / 2 sy = (@containerHeight - slideHeight) / 2 sy = 0 if sy < 0 @raphaelObj.canvas.style.left = sx + "px" @raphaelObj.canvas.style.top = sy + "px" @raphaelObj.setSize slideWidth - 2, slideHeight - 2 # update zoom level and cursor position z = @raphaelObj._viewBox[2] / slideWidth @zoomLevel = z @cursor.setRadius(dcr * z) # force the slice attribute despite Raphael changing it @raphaelObj.canvas.setAttribute "preserveAspectRatio", "xMinYMin slice" # Switches the tool and thus the functions that get # called when certain events are fired from Raphael. # @param {string} tool the tool to turn on # @return {undefined} setCurrentTool: (tool) -> @currentTool = tool console.log "setting current tool to", tool switch tool when "path", "line" @cursor.undrag() @currentLine = @_createTool(tool) @cursor.drag(@currentLine.dragOnMove, @currentLine.dragOnStart, @currentLine.dragOnEnd) when "rect" @cursor.undrag() @currentRect = @_createTool(tool) @cursor.drag(@currentRect.dragOnMove, @currentRect.dragOnStart, @currentRect.dragOnEnd) # TODO: the shapes below are still in the old format # when "panzoom" # @cursor.undrag() # @cursor.drag _.bind(@_panDragging, @), # _.bind(@_panGo, @), _.bind(@_panStop, @) # when "ellipse" # @cursor.undrag() # @cursor.drag _.bind(@_ellipseDragging, @), # _.bind(@_ellipseDragStart, @), _.bind(@_ellipseDragStop, @) # when "text" # @cursor.undrag() # @cursor.drag _.bind(@_rectDragging, @), # _.bind(@_textStart, @), _.bind(@_textStop, @) else console.log "ERROR: Cannot set invalid tool:", tool # Sets the fit to page. # @param {boolean} value If true fit to page. If false fit to width. # TODO: not really working as it should be setFitToPage: (value) -> @fitToPage = value # TODO: we can scale the slides and drawings instead of re-adding them, but the logic # will change quite a bit temp = @slides @removeAllImagesFromPaper() @slides = temp # re-add all the images as they should fit differently @rebuild() # set to default zoom level globals.connection.emitPaperUpdate 0, 0, 1, 1 # get the shapes to reprocess globals.connection.emitAllShapes() # Socket response - Update zoom variables and viewbox # @param {number} d the delta value from the scroll event # @return {undefined} setZoom: (d) -> step = 0.05 # step size if d < 0 @zoomLevel += step # zooming out else @zoomLevel -= step # zooming in [sw, sh] = @_currentSlideDimensions() [cx, cy] = @_currentSlideOffsets() x = cx / sw y = cy / sh # cannot zoom out further than 100% z = (if @zoomLevel > 1 then 1 else @zoomLevel) # cannot zoom in further than 400% (1/4) z = (if z < 0.25 then 0.25 else z) # cannot zoom to make corner less than (x,y) = (0,0) x = (if x < 0 then 0 else x) y = (if y < 0 then 0 else y) # cannot view more than the bottom corners zz = 1 - z x = (if x > zz then zz else x) y = (if y > zz then zz else y) globals.connection.emitPaperUpdate x, y, z, z # send update to all clients stopPanning: -> # nothing to do # Draws an array of shapes to the paper. # @param {array} shapes the array of shapes to draw drawListOfShapes: (shapes) -> @currentShapesDefinitions = shapes @currentShapes = @raphaelObj.set() for shape in shapes data = if _.isString(shape.data) then JSON.parse(shape.data) else shape.data tool = @_createTool(shape.shape) if tool? @currentShapes.push tool.draw.apply(tool, data) else console.log "shape not recognized at drawListOfShapes", shape # make sure the cursor is still on top @cursor.toFront() # Clear all shapes from this paper. clearShapes: -> console.log "clearing shapes" if @currentShapes? @currentShapes.forEach (element) -> element.remove() @currentShapes = null @currentShapesDefinitions = [] # Updated a shape `shape` with the data in `data`. # TODO: check if the objects exist before calling update, if they don't they should be created updateShape: (shape, data) -> switch shape when "line" @currentLine.update.apply(@currentLine, data) when "rect" @currentRect.update.apply(@currentRect, data) when "ellipse" @currentEllipse.update.apply(@currentEllipse, data) when "triangle" @currentTriangle.update.apply(@currentTriangle, data) when "text" @currentText.update.apply(@currentText, data) else console.log "shape not recognized at updateShape", shape # Make a shape `shape` with the data in `data`. makeShape: (shape, data) -> tool = null switch shape when "path", "line" @currentLine = @_createTool(shape) toolModel = @currentLine tool = @currentLine.make.apply(@currentLine, data) when "rect" @currentRect = @_createTool(shape) toolModel = @currentRect tool = @currentRect.make.apply(@currentRect, data) when "ellipse" @currentEllipse = @_createTool(shape) toolModel = @currentEllipse tool = @currentEllipse.make.apply(@currentEllipse, data) when "triangle" @currentTriangle = @_createTool(shape) toolModel = @currentTriangle tool = @currentTriangle.make.apply(@currentTriangle, data) when "text" @currentText = @_createTool(shape) toolModel = @currentText tool = @currentText.make.apply(@currentText, data) else console.log "shape not recognized at makeShape", shape if tool? @currentShapes.push(tool) @currentShapesDefinitions.push(toolModel.getDefinition()) # Update the cursor position on screen # @param {number} x the x value of the cursor as a percentage of the width # @param {number} y the y value of the cursor as a percentage of the height moveCursor: (x, y) -> [cx, cy] = @_currentSlideOffsets() [slideWidth, slideHeight] = @_currentSlideOriginalDimensions() @cursor.setPosition(x * slideWidth + cx, y * slideHeight + cy) # Update the dimensions of the container. _updateContainerDimensions: -> $container = $(@container) @containerWidth = $container.innerWidth() @containerHeight = $container.innerHeight() @containerOffsetLeft = $container.offset().left @containerOffsetTop = $container.offset().top # Retrieves an image element from the paper. # The url must be in the slides array. # @param {string} url the url of the image (must be in slides array) # @return {Raphael.image} return the image or null if not found _getImageFromPaper: (url) -> if @slides[url] id = @slides[url].getId() return @raphaelObj.getById(id) if id? null # Hides an image from the paper given the URL. # The url must be in the slides array. # @param {string} url the url of the image (must be in slides array) _hideImageFromPaper: (url) -> img = @_getImageFromPaper(url) img.hide() if img? # Update zoom variables on all clients # @param {Event} e the event that occurs when scrolling # @param {number} delta the speed/direction at which the scroll occurred _zoomSlide: (e, delta) -> globals.connection.emitZoom delta # Called when the cursor is moved over the presentation. # Sends cursor moving event to server. # @param {Event} e the mouse event # @param {number} x the x value of cursor at the time in relation to the left side of the browser # @param {number} y the y value of cursor at the time in relation to the top of the browser # TODO: this should only be done if the user is the presenter _onMouseMove: (e, x, y) -> [sw, sh] = @_currentSlideDimensions() xLocal = (e.pageX - @containerOffsetLeft) / sw yLocal = (e.pageY - @containerOffsetTop) / sh globals.connection.emitMoveCursor xLocal, yLocal # When the user is dragging the cursor (click + move) # @param {number} dx the difference between the x value from panGo and now # @param {number} dy the difference between the y value from panGo and now _panDragging: (dx, dy) -> [slideWidth, slideHeight] = @_currentSlideOriginalDimensions() sx = (@containerWidth - slideWidth) / 2 sy = (@containerHeight - slideHeight) / 2 [sw, sh] = @_currentSlideDimensions() # ensuring that we cannot pan outside of the boundaries x = (@panX - dx) # cannot pan past the left edge of the page x = (if x < 0 then 0 else x) y = (@panY - dy) # cannot pan past the top of the page y = (if y < 0 then 0 else y) if @fitToPage x2 = slideWidth + x else x2 = @containerWidth + x # cannot pan past the width x = (if x2 > sw then sw - (@containerWidth - sx * 2) else x) if @fitToPage y2 = slideHeight + y else # height of image could be greater (or less) than the box it fits in y2 = @containerHeight + y # cannot pan below the height y = (if y2 > sh then sh - (@containerHeight - sy * 2) else y) globals.connection.emitPaperUpdate x / sw, y / sh, null, null # When panning starts # @param {number} x the x value of the cursor # @param {number} y the y value of the cursor _panGo: (x, y) -> [cx, cy] = @_currentSlideOffsets() @panX = cx @panY = cy # When panning finishes # @param {Event} e the mouse event _panStop: (e) -> @stopPanning() # Called when the application window is resized. _onWindowResize: -> @_updateContainerDimensions() # when pressing down on a key at anytime _onKeyDown: (event) -> unless event keyCode = window.event.keyCode else keyCode = event.keyCode switch keyCode when 16 # shift key @shiftPressed = true # when releasing any key at any time _onKeyUp: -> unless event keyCode = window.event.keyCode else keyCode = event.keyCode switch keyCode when 16 # shift key @shiftPressed = false _currentSlideDimensions: -> if @currentSlide? then @currentSlide.getDimensions() else [0, 0] _currentSlideOriginalDimensions: -> if @currentSlide? then @currentSlide.getOriginalDimensions() else [0, 0] _currentSlideOffsets: -> if @currentSlide? then @currentSlide.getOffsets() else [0, 0] # Wrapper method to create a tool for the whiteboard _createTool: (type) -> switch type when "path", "line" model = WhiteboardLineModel when "rect" model = WhiteboardRectModel when "ellipse" model = WhiteboardEllipseModel when "triangle" model = WhiteboardTriangleModel when "text" model = WhiteboardTextModel if model? [slideWidth, slideHeight] = @_currentSlideOriginalDimensions() [xOffset, yOffset] = @_currentSlideOffsets() [width, height] = @_currentSlideDimensions() tool = new model(@raphaelObj) # TODO: why are the parameters inverted and it works? tool.setPaperSize(slideHeight, slideWidth) tool.setOffsets(xOffset, yOffset) tool.setPaperDimensions(width,height) tool else null # Adds the base url (the protocol+server part) to `url` if needed. _slideUrl: (url) -> if url.match(/http[s]?:/) url else globals.presentationServer + url WhiteboardPaperModel