359 lines
13 KiB
CoffeeScript
Executable File
359 lines
13 KiB
CoffeeScript
Executable File
# "Paper" which is the Raphael term for the entire SVG object on the webpage.
|
|
# This class deals with this SVG component only.
|
|
class Meteor.WhiteboardPaperModel
|
|
|
|
# Container must be a DOM element
|
|
constructor: (@container) ->
|
|
# a WhiteboardCursorModel
|
|
@cursor = null
|
|
|
|
# all slides in the presentation indexed by url
|
|
@slides = {}
|
|
|
|
@panX = null
|
|
@panY = null
|
|
|
|
@current = {}
|
|
|
|
# the slide being shown
|
|
@current.slide = null
|
|
|
|
# a raphaeljs set with all the shapes in the current slide
|
|
@current.shapes = 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)
|
|
@current.shapeDefinitions = []
|
|
|
|
@zoomLevel = 1
|
|
@shiftPressed = false
|
|
@currentPathCount = 0
|
|
|
|
@_updateContainerDimensions()
|
|
|
|
@zoomObserver = null
|
|
|
|
@adjustedWidth = 0
|
|
@adjustedHeight = 0
|
|
|
|
@widthRatio = 100
|
|
@heightRatio = 100
|
|
|
|
# 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, "900", "500")
|
|
|
|
h = $("#"+@container).height()
|
|
w = $("#"+@container).width()
|
|
|
|
@raphaelObj ?= ScaleRaphael(@container, w, h)
|
|
@raphaelObj ?= ScaleRaphael(@container, $container.innerHeight(), $container.innerWidth())
|
|
|
|
@raphaelObj.canvas.setAttribute "preserveAspectRatio", "xMinYMin slice"
|
|
|
|
@createCursor()
|
|
|
|
if @slides
|
|
@rebuild()
|
|
else
|
|
@slides = {} # if previously loaded
|
|
unless navigator.userAgent.indexOf("Firefox") is -1
|
|
@raphaelObj.renderfix()
|
|
|
|
@raphaelObj
|
|
|
|
# Re-add the images to the paper that are found
|
|
# in the slides array (an object of urls and dimensions).
|
|
rebuild: ->
|
|
@current.slide = null
|
|
for url of @slides
|
|
if @slides.hasOwnProperty(url)
|
|
@addImageToPaper url, @slides[url].getWidth(), @slides[url].getHeight()
|
|
|
|
scale: (width, height) ->
|
|
@raphaelObj?.changeSize(width, height)
|
|
|
|
# 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()
|
|
|
|
# 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
|
|
url = @_slideUrl(url)
|
|
sw = width / max
|
|
sh = height / max
|
|
#cx = (@containerWidth / 2) - (width / 2)
|
|
#cy = (@containerHeight / 2) - (height / 2)
|
|
img = @raphaelObj.image(url, cx = 0, cy = 0, width, height)
|
|
|
|
# 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, width, height, sw, sh, cx, cy)
|
|
|
|
unless @current.slide?
|
|
img.toBack()
|
|
@current.slide = @slides[url]
|
|
else if @current.slide.url is url
|
|
img.toBack()
|
|
else
|
|
img.hide()
|
|
|
|
# TODO: other places might also required an update in these dimensions
|
|
@_updateContainerDimensions()
|
|
|
|
@_updateZoomRatios()
|
|
if @raphaelObj.w is 100 # on first load: Raphael object is initially tiny
|
|
@cursor.setRadius(0.65 * @widthRatio / 100)
|
|
else
|
|
@cursor.setRadius(6 * @widthRatio / 100)
|
|
|
|
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()) # Removes the previous image preventing images from being redrawn over each other repeatedly
|
|
@slides = {}
|
|
@current.slide = null
|
|
|
|
|
|
# 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 "line"
|
|
@cursor.undrag()
|
|
@current.line = @_createTool(tool)
|
|
@cursor.drag(@current.line.dragOnMove, @current.line.dragOnStart, @current.line.dragOnEnd)
|
|
when "rectangle"
|
|
@cursor.undrag()
|
|
@current.rectangle = @_createTool(tool)
|
|
@cursor.drag(@current.rectangle.dragOnMove, @current.rectangle.dragOnStart, @current.rectangle.dragOnEnd)
|
|
else
|
|
console.log "ERROR: Cannot set invalid tool:", tool
|
|
|
|
# Clear all shapes from this paper.
|
|
clearShapes: ->
|
|
if @current.shapes?
|
|
@current.shapes.forEach (element) ->
|
|
element.remove()
|
|
@current.shapeDefinitions = []
|
|
@current.shapes.clear()
|
|
@clearCursor()
|
|
@createCursor()
|
|
|
|
clearCursor: ->
|
|
@cursor?.remove()
|
|
|
|
createCursor: ->
|
|
if @raphaelObj.w is 100 # on first load: Raphael object is initially tiny
|
|
@cursor = new WhiteboardCursorModel(@raphaelObj, 0.65)
|
|
@cursor.setRadius(0.65 * @widthRatio / 100)
|
|
else
|
|
@cursor = new WhiteboardCursorModel(@raphaelObj)
|
|
@cursor.setRadius(6 * @widthRatio / 100)
|
|
@cursor.draw()
|
|
|
|
# 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) ->
|
|
@current[shape].update(data)
|
|
|
|
# Make a shape `shape` with the data in `data`.
|
|
makeShape: (shape, data) ->
|
|
data.thickness *= @adjustedWidth / 1000
|
|
|
|
tool = null
|
|
#TODO pay attention to this array, data in this array slows down the whiteboard
|
|
#console.log @current
|
|
#console.log @
|
|
@current[shape] = @_createTool(shape)
|
|
toolModel = @current[shape]
|
|
tool = @current[shape].make(data)
|
|
|
|
if tool? and shape isnt "poll_result"
|
|
@current.shapes ?= @raphaelObj.set()
|
|
@current.shapes.push(tool)
|
|
@current.shapeDefinitions.push(toolModel.getDefinition())
|
|
|
|
#We have a separate case for Poll as it returns an array instead of just one object
|
|
if tool? and shape is "poll_result"
|
|
@current.shapes ?= @raphaelObj.set()
|
|
for obj in tool
|
|
@current.shapes.push(obj)
|
|
@current.shapeDefinitions.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)
|
|
|
|
#if the slide is zoomed in then move the cursor based on where the viewBox is looking
|
|
if @viewBoxXpos? && @viewBoxYPos? && @viewBoxWidth? && @viewBoxHeight?
|
|
@cursor.setPosition( @viewBoxXpos + x * @viewBoxWidth, @viewBoxYPos + y * @viewBoxHeight )
|
|
|
|
zoomAndPan: (widthRatio, heightRatio, xOffset, yOffset) ->
|
|
# console.log "zoomAndPan #{widthRatio} #{heightRatio} #{xOffset} #{yOffset}"
|
|
newX = - xOffset * 2 * @adjustedWidth / 100
|
|
newY = - yOffset * 2 * @adjustedHeight / 100
|
|
newWidth = @adjustedWidth * widthRatio / 100
|
|
newHeight = @adjustedHeight * heightRatio / 100
|
|
@raphaelObj.setViewBox(newX, newY, newWidth, newHeight) # zooms and pans
|
|
|
|
setAdjustedDimensions: (width, height) ->
|
|
@adjustedWidth = width
|
|
@adjustedHeight = height
|
|
|
|
# Update the dimensions of the container.
|
|
_updateContainerDimensions: ->
|
|
#console.log "update Container Dimensions"
|
|
|
|
$container = $('#whiteboard-paper')
|
|
@containerWidth = $container.innerWidth()
|
|
@containerHeight = $container.innerHeight()
|
|
|
|
@containerOffsetLeft = $container.offset()?.left
|
|
@containerOffsetTop = $container.offset()?.top
|
|
|
|
_updateZoomRatios: ->
|
|
currentSlideDoc = getCurrentSlideDoc()
|
|
@widthRatio = currentSlideDoc.slide.width_ratio
|
|
@heightRatio = currentSlideDoc.slide.height_ratio
|
|
|
|
# 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
|
|
|
|
_currentSlideDimensions: ->
|
|
if @current.slide? then @current.slide.getDimensions() else [0, 0]
|
|
|
|
_currentSlideOriginalDimensions: ->
|
|
if @current.slide? then @current.slide.getOriginalDimensions() else [0, 0]
|
|
|
|
_currentSlideOffsets: ->
|
|
if @current.slide? then @current.slide.getOffsets() else [0, 0]
|
|
|
|
# Wrapper method to create a tool for the whiteboard
|
|
_createTool: (type) ->
|
|
switch type
|
|
when "pencil"
|
|
model = WhiteboardLineModel
|
|
when "path", "line"
|
|
model = WhiteboardLineModel
|
|
when "rectangle"
|
|
model = WhiteboardRectModel
|
|
when "ellipse"
|
|
model = WhiteboardEllipseModel
|
|
when "triangle"
|
|
model = WhiteboardTriangleModel
|
|
when "text"
|
|
model = WhiteboardTextModel
|
|
when "poll_result"
|
|
model = WhiteboardPollModel
|
|
|
|
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
|
|
console.log "The url '#{url}'' did not match the expected format of: http/s"
|
|
#globals.presentationServer + url
|
|
|
|
#Changes the currently displayed page/slide (if any) with this one
|
|
#@param {data} message object containing the "presentation" object
|
|
_displayPage: (data, originalWidth, originalHeight) ->
|
|
@removeAllImagesFromPaper()
|
|
|
|
@_updateContainerDimensions()
|
|
boardWidth = @containerWidth
|
|
boardHeight = @containerHeight
|
|
|
|
currentSlide = getCurrentSlideDoc()
|
|
|
|
# TODO currentSlide undefined in some cases - will check later why
|
|
imageWidth = boardWidth * (currentSlide?.slide.width_ratio/100) or boardWidth
|
|
imageHeight = boardHeight * (currentSlide?.slide.height_ratio/100) or boardHeight
|
|
|
|
currentPresentation = Meteor.Presentations.findOne({"presentation.current": true})
|
|
presentationId = currentPresentation?.presentation?.id
|
|
currentSlideCursor = Meteor.Slides.find({"presentationId": presentationId, "slide.current": true})
|
|
|
|
if @zoomObserver isnt null
|
|
@zoomObserver.stop()
|
|
_this = this
|
|
@zoomObserver = currentSlideCursor.observe # watching the current slide changes
|
|
changed: (newDoc, oldDoc) ->
|
|
if originalWidth <= originalHeight
|
|
@adjustedWidth = boardHeight * originalWidth / originalHeight
|
|
@adjustedHeight = boardHeight
|
|
else
|
|
@adjustedHeight = boardWidth * originalHeight / originalWidth
|
|
@adjustedWidth = boardWidth
|
|
|
|
_this.zoomAndPan(newDoc.slide.width_ratio, newDoc.slide.height_ratio,
|
|
newDoc.slide.x_offset, newDoc.slide.y_offset)
|
|
|
|
oldRatio = (oldDoc.slide.width_ratio + oldDoc.slide.height_ratio) / 2
|
|
newRatio = (newDoc.slide.width_ratio + newDoc.slide.height_ratio) / 2
|
|
|
|
_this?.current?.shapes?.forEach (shape) ->
|
|
shape.attr "stroke-width", shape.attr('stroke-width') * oldRatio / newRatio
|
|
|
|
if _this.raphaelObj is 100 # on first load: Raphael object is initially tiny
|
|
_this.cursor.setRadius(0.65 * newDoc.slide.width_ratio / 100)
|
|
else
|
|
_this.cursor.setRadius(6 * newDoc.slide.width_ratio / 100)
|
|
|
|
if originalWidth <= originalHeight
|
|
# square => boardHeight is the shortest side
|
|
@adjustedWidth = boardHeight * originalWidth / originalHeight
|
|
$('#whiteboard-paper').width(@adjustedWidth)
|
|
@addImageToPaper(data, @adjustedWidth, boardHeight)
|
|
@adjustedHeight = boardHeight
|
|
else
|
|
@adjustedHeight = boardWidth * originalHeight / originalWidth
|
|
$('#whiteboard-paper').height(@adjustedHeight)
|
|
@addImageToPaper(data, boardWidth, @adjustedHeight)
|
|
@adjustedWidth = boardWidth
|
|
|
|
@zoomAndPan(currentSlide.slide.width_ratio, currentSlide.slide.height_ratio,
|
|
currentSlide.slide.x_offset, currentSlide.slide.y_offset)
|