bigbluebutton-Github/labs/bbb-html5-client/public/js/models/whiteboard_paper.coffee
Leonardo Crauss Daronco d2f09e916d Redraw shapes when resizing the HTML5 client
Only redrawing shapes that were loaded when the HTML5 started. Shapes
drawn before are not being saved in the arrays yet and so are not being
redraw when resizing (they disappear).
2013-04-27 11:58:20 -04:00

937 lines
37 KiB
CoffeeScript
Executable File

define [
'jquery',
'underscore',
'backbone',
'raphael',
'scale.raphael',
'globals',
'cs!utils'
], ($, _, Backbone, Raphael, ScaleRaphael, globals, Utils) ->
# TODO: text, ellipse, line, rect and cursor could be models
# TODO: temporary solution
PRESENTATION_SERVER = window.location.protocol + "//" + window.location.host
PRESENTATION_SERVER = PRESENTATION_SERVER.replace(/:\d+/, "/") # remove :port
MAX_PATHS_IN_SEQUENCE = 30
# "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, @textbox) ->
@gw = "100%"
@gh = "100%"
@panX = null
@panY = null
@lineX = null
@lineY = null
@ellipseX = null
@ellipseY = null
@textX = null
@textY = null
# TODO: could be local variables or defined with better names
@cx2 = null
@cy2 = null
@cursor = null
@cursorRadius = 4
@slides = null
@currentSlide = null
@fitToPage = true
@currentShapes = null
@currentShapesDefinitions = []
@currentLine = null
@currentRect = null
@currentEllipse = 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, @gw, @gh)
@raphaelObj.canvas.setAttribute "preserveAspectRatio", "xMinYMin slice"
@cursor = @raphaelObj.circle(0, 0, @cursorRadius)
@cursor.attr "fill", "red"
$(@cursor.node).on "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: ->
@_setCurrentSlide(null)
for url of @slides
if @slides.hasOwnProperty(url)
@addImageToPaper url, @slides[url].w, @slides[url].h
# 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 = @_getCurrentSlide()
@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 = PRESENTATION_SERVER + url unless url.match(/http[s]?:/)
sw = width / max
sh = height / max
cx = (@containerWidth / 2) - (width / 2)
cy = (@containerHeight / 2) - (height / 2)
img = @raphaelObj.image(url, cx, cy, @gw = width, @gh = height)
else
# fit to width
console.log "no fit"
# assume it will fit width ways
sw = width / wr
sh = height / wr
wr = width / @containerWidth
img = @raphaelObj.image(url, cx = 0, cy = 0, width / wr, height / wr)
@gw = sw
@gh = sh
@slides[url] =
id: img.id
w: sw # sw slide width as percentage of original width of paper
h: sh # sh slide height as a percentage of original height of paper
img: img
url: url
cx: cx # x-offset from top left corner as percentage of original width of paper
cy: cy # y-offset from top left corner as percentage of original height of paper
unless @_getCurrentSlide()?
img.toBack()
@_setCurrentSlide(@slides[url])
else if @_getCurrentSlide()?.url is url
img.toBack()
else
img.hide()
$(@container).on "mousemove", _.bind(@_onCursorMove, @)
$(@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].id).remove()
@trigger('paper:image:removed', @slides[url].id)
@slides = {}
@_setCurrentSlide(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) ->
url = PRESENTATION_SERVER + url unless url.match(/http[s]?:/)
if @_getCurrentSlide()?.url isnt url and @slides[url]?
# TODO: temporary solution
@_hideImageFromPaper @_getCurrentSlide()?.url
next = @_getImageFromPaper(url)
if next
next.show()
next.toFront()
@currentShapes.forEach (element) ->
element.toFront()
@_bringCursorToFront()
@_setCurrentSlide(@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!)
if sw_ and sh_
@raphaelObj.setViewBox cx_ * @gw, cy_ * @gh, sw_ * @gw, sh_ * @gh
sw = @gw / sw_
sh = @gh / sh_
# just panning, so use old slide size values
else
[sw, sh] = @_currentSlideDimensions()
@raphaelObj.setViewBox cx_ * @gw, cy_ * @gh, @raphaelObj._viewBox[2], @raphaelObj._viewBox[3]
# update corners
cx = cx_ * sw
cy = cy_ * sh
# update position of svg object in the window
sx = (@containerWidth - @gw) / 2
sy = (@containerHeight - @gh) / 2
sy = 0 if sy < 0
@raphaelObj.canvas.style.left = sx + "px"
@raphaelObj.canvas.style.top = sy + "px"
@raphaelObj.setSize @gw - 2, @gh - 2
# update zoom level and cursor position
z = @raphaelObj._viewBox[2] / @gw
@cursor.attr r: dcr * z
@zoomLevel = 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 "line"
@cursor.undrag()
@cursor.drag _.bind(@_lineDragging, @),
_.bind(@_lineDragStart, @), _.bind(@_lineDragStop, @)
when "rect"
@cursor.undrag()
@cursor.drag _.bind(@_rectDragging, @),
_.bind(@_rectDragStart, @), _.bind(@_rectDragStop, @)
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
# The server has said the text is finished,
# so set it to null for the next text object
textDone: ->
if @currentText?
@currentText = null
@currentRect.hide() if @currentRect?
# 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 = JSON.parse(shape.data)
switch shape.shape
when "path"
@_drawLine.apply @, data
when "rect"
@_drawRect.apply @, data
when "ellipse"
@_drawEllipse.apply @, data
when "text"
@_drawText.apply @, data
# make sure the cursor is still on top
@_bringCursorToFront()
# Clear all shapes from this paper.
clearShapes: ->
console.log "clearing shapes", @currentShapes
if @currentShapes?
@currentShapes.forEach (element) ->
element.remove()
@currentShapes = null
@currentShapesDefinitions = []
# Updated a shape `shape` with the data in `data`.
updateShape: (shape, data) ->
switch shape
when "line"
@_updateLine.apply @, data
when "rect"
@_updateRect.apply @, data
when "ellipse"
@_updateEllipse.apply @, data
when "text"
@_updateText.apply @, data
else
console.log "shape not recognized at updateShape", shape
# Make a shape `shape` with the data in `data`.
makeShape: (shape, data) ->
switch shape
when "line"
@_makeLine.apply @, data
when "rect"
@_makeRect.apply @, data
when "ellipse"
@_makeEllipse.apply @, data
else
console.log "shape not recognized at makeShape", shape
# 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()
@cursor.attr
cx: x * @gw + cx
cy: y * @gh + 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].id
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?
# Puts the cursor on top so it doesn't get hidden behind any objects.
_bringCursorToFront: ->
@cursor.toFront()
# Drawing a line from the list o
# @param {string} path height of the shape as a percentage of the original height
# @param {string} colour the colour of the shape to be drawn
# @param {number} thickness the thickness of the line to be drawn
_drawLine: (path, colour, thickness) ->
[cx, cy] = @_currentSlideOffsets()
line = @raphaelObj.path(Utils.stringToScaledPath(path, @gw, @gh, cx, cy))
line.attr @_strokeAndThickness(colour, thickness)
@currentShapes.push line
# Draw a rectangle on the paper
# @param {number} x the x value of the object as a percentage of the original width
# @param {number} y the y value of the object as a percentage of the original height
# @param {number} w width of the shape as a percentage of the original width
# @param {number} h height of the shape as a percentage of the original height
# @param {string} colour the colour of the object
# @param {number} thickness the thickness of the object's line(s)
_drawRect: (x, y, w, h, colour, thickness) ->
[cx, cy] = @_currentSlideOffsets()
x = x * @gw
y = y * @gh
r = @raphaelObj.rect(x + cx, y + cy, (w * @gw) - x, (h * @gh) - y)
r.attr @_strokeAndThickness(colour, thickness)
@currentShapes.push r
# Draw an ellipse on the whiteboard
# @param {[type]} cx the x value of the center as a percentage of the original width
# @param {[type]} cy the y value of the center as a percentage of the original height
# @param {[type]} rx the radius-x of the ellipse as a percentage of the original width
# @param {[type]} ry the radius-y of the ellipse as a percentage of the original height
# @param {string} colour the colour of the object
# @param {number} thickness the thickness of the object's line(s)
# TODO: not tested yet
_drawEllipse: (cx, cy, rx, ry, colour, thickness) ->
elip = @raphaelObj.ellipse(cx * @gw, cy * @gh, rx * @gw, ry * @gh)
elip.attr @_strokeAndThickness(colour, thickness)
@currentShapes.push elip
# Drawing the text on the whiteboard from object
# @param {string} t the text of the text object
# @param {number} x the x value of the object as a percentage of the original width
# @param {number} y the y value of the object as a percentage of the original height
# @param {number} w the width of the text box as a percentage of the original width
# @param {number} spacing the spacing between the letters
# @param {string} colour the colour of the text
# @param {string} font the font family of the text
# @param {number} fontsize the size of the font (in PIXELS)
# TODO: not tested yet
_drawText: (t, x, y, w, spacing, colour, font, fontsize) ->
x = x * @gw
y = y * @gh
txt = @raphaelObj.text(x, y, "").attr(
fill: colour
"font-family": font
"font-size": fontsize
)
txt.node.style["text-anchor"] = "start" # force left align
txt.node.style["textAnchor"] = "start" # for firefox, 'cause they like to be different
dy = textFlow(t, txt.node, w, x, spacing, false)
@currentShapes.push txt
# Make a line on the whiteboard that could be updated shortly after
# @param {number} x the x value of the line start point as a percentage of the original width
# @param {number} y the y value of the line start point as a percentage of the original height
# @param {string} colour the colour of the shape to be drawn
# @param {number} thickness the thickness of the line to be drawn
_makeLine: (x, y, colour, thickness) ->
[cx, cy] = @_currentSlideOffsets()
x = x * @gw + cx
y = y * @gh + cy
@currentLine = @raphaelObj.path("M" + x + " " + y + " L" + x + " " + y)
@currentLine.attr @_strokeAndThickness(colour, thickness)
@currentShapes.push @currentLine
# Socket response - Make rectangle on canvas
# @param {number} x the x value of the object as a percentage of the original width
# @param {number} y the y value of the object as a percentage of the original height
# @param {string} colour the colour of the object
# @param {number} thickness the thickness of the object's line(s)
_makeRect: (x, y, colour, thickness) ->
[cx, cy] = @_currentSlideOffsets()
@currentRect = @raphaelObj.rect(x * @gw + cx, y * @gh + cy, 0, 0)
@currentRect.attr @_strokeAndThickness(colour, thickness)
@currentShapes.push @currentRect
# Make an ellipse on the whiteboard
# @param {[type]} cx the x value of the center as a percentage of the original width
# @param {[type]} cy the y value of the center as a percentage of the original height
# @param {string} colour the colour of the object
# @param {number} thickness the thickness of the object's line(s)
# TODO: not tested yet
_makeEllipse: (cx, cy, colour, thickness) ->
@currentEllipse = @raphaelObj.ellipse(cx * @gw, cy * @gh, 0, 0)
@currentEllipse.attr @_strokeAndThickness(colour, thickness)
@currentShapes.push @currentEllipse
# Updating drawing the line
# @param {number} x2 the next x point to be added to the line as a percentage of the original width
# @param {number} y2 the next y point to be added to the line as a percentage of the original height
# @param {boolean} add true if the line should be added to the current line, false if it should replace the last point
_updateLine: (x2, y2, add) ->
if @currentLine?
[cx, cy] = @_currentSlideOffsets()
x2 = x2 * @gw + cx
y2 = y2 * @gh + cy
# if adding to the line
if add
@currentLine.attr path: (@currentLine.attrs.path + "L" + x2 + " " + y2)
# if simply updating the last portion (for drawing a straight line)
else
@currentLine.attrs.path.pop()
path = @currentLine.attrs.path.join(" ")
@currentLine.attr path: (path + "L" + x2 + " " + y2)
# Socket response - Update rectangle drawn
# @param {number} x1 the x value of the object as a percentage of the original width
# @param {number} y1 the y value of the object as a percentage of the original height
# @param {number} w width of the shape as a percentage of the original width
# @param {number} h height of the shape as a percentage of the original height
_updateRect: (x1, y1, w, h) ->
if @currentRect?
[cx, cy] = @_currentSlideOffsets()
x = x1 * @gw + cx
y = y1 * @gh + cy
@currentRect.attr
x: x
y: y
width: (w * @gw + cx) - x
height: (h * @gh + cy) - y
# Socket response - Update rectangle drawn
# @param {number} x the x value of the object as a percentage of the original width
# @param {number} y the y value of the object as a percentage of the original height
# @param {number} w width of the shape as a percentage of the original width
# @param {number} h height of the shape as a percentage of the original height
# TODO: not tested yet
_updateEllipse: (x, y, w, h) ->
if @currentEllipse?
@currentEllipse.attr
cx: x * @gw
cy: y * @gh
rx: w * @gw
ry: h * @gh
# Updating the text from the messages on the socket
# @param {string} t the text of the text object
# @param {number} x the x value of the object as a percentage of the original width
# @param {number} y the y value of the object as a percentage of the original height
# @param {number} w the width of the text box as a percentage of the original width
# @param {number} spacing the spacing between the letters
# @param {string} colour the colour of the text
# @param {string} font the font family of the text
# @param {number} fontsize the size of the font (in PIXELS)
_updateText: (t, x, y, w, spacing, colour, font, fontsize) ->
x = x * @gw
y = y * @gh
unless @currentText?
# TODO: does almost the same as calling @_drawText()
@currentText = @raphaelObj.text(x, y, "").attr(
fill: colour
"font-family": font
"font-size": fontsize
)
@currentText.node.style["text-anchor"] = "start" # force left align
@currentText.node.style["textAnchor"] = "start" # for firefox, 'cause they like to be different
@currentShapes.push text
else
@currentText.attr fill: colour
cell = @currentText.node
cell.removeChild cell.firstChild while cell.hasChildNodes()
dy = textFlow(t, cell, w, x, spacing, false)
@cursor.toFront()
# 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
_onCursorMove: (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) ->
sx = (@containerWidth - @gw) / 2
sy = (@containerHeight - @gh) / 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 = @gw + x
else
x2 = @containerWidth + x
# cannot pan past the width
x = (if x2 > sw then sw - (@containerWidth - sx * 2) else x)
if @fitToPage
y2 = @gh + 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()
# When dragging for drawing lines starts
# @param {number} x the x value of the cursor
# @param {number} y the y value of the cursor
_lineDragStart: (x, y) ->
# find the x and y values in relation to the whiteboard
sx = (@containerWidth - @gw) / 2
sy = (@containerHeight - @gh) / 2
[sw, sh] = @_currentSlideDimensions()
[cx, cy] = @_currentSlideOffsets()
@lineX = x - @containerOffsetLeft - sx + cx
@lineY = y - @containerOffsetTop - sy + cy
values = [ @lineX / sw, @lineY / sh, @currentColour, @currentThickness ]
globals.connection.emitMakeShape "line", values
# As line drawing drag continues
# @param {number} dx the difference between the x value from _lineDragStart and now
# @param {number} dy the difference between the y value from _lineDragStart and now
# @param {number} x the x value of the cursor
# @param {number} y the y value of the cursor
_lineDragging: (dx, dy, x, y) ->
sx = (@containerWidth - @gw) / 2
sy = (@containerHeight - @gh) / 2
[sw, sh] = @_currentSlideDimensions()
[cx, cy] = @_currentSlideOffsets()
# find the x and y values in relation to the whiteboard
@cx2 = x - @containerOffsetLeft - sx + cx
@cy2 = y - @containerOffsetTop - sy + cy
if @shiftPressed
globals.connection.emitUpdateShape "line", [ @cx2 / sw, @cy2 / sh, false ]
else
@currentPathCount++
if @currentPathCount < MAX_PATHS_IN_SEQUENCE
globals.connection.emitUpdateShape "line", [ @cx2 / sw, @cy2 / sh, true ]
else if @currentLine?
@currentPathCount = 0
# save the last path of the line
@currentLine.attrs.path.pop()
path = @currentLine.attrs.path.join(" ")
@currentLine.attr path: (path + "L" + @lineX + " " + @lineY)
# scale the path appropriately before sending
pathStr = @currentLine.attrs.path.join(",")
globals.connection.emitPublishShape "path",
[ Utils.stringToScaledPath(pathStr, 1 / @gw, 1 / @gh),
@currentColour, @currentThickness ]
globals.connection.emitMakeShape "line",
[ @lineX / sw, @lineY / sh, @currentColour, @currentThickness ]
@lineX = @cx2
@lineY = @cy2
# Drawing line has ended
# @param {Event} e the mouse event
_lineDragStop: (e) ->
if @currentLine?
path = @currentLine.attrs.path
@currentLine = null # any late updates will be blocked by this
# scale the path appropriately before sending
globals.connection.emitPublishShape "path",
[ Utils.stringToScaledPath(path.join(","), 1 / @gw, 1 / @gh),
@currentColour, @currentThickness ]
# Creating a rectangle has started
# @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
_rectDragStart: (x, y) ->
sx = (@containerWidth - @gw) / 2
sy = (@containerHeight - @gh) / 2
[sw, sh] = @_currentSlideDimensions()
[cx, cy] = @_currentSlideOffsets()
# find the x and y values in relation to the whiteboard
@cx2 = (x - @containerOffsetLeft - sx + cx) / sw
@cy2 = (y - @containerOffsetTop - sy + cy) / sh
globals.connection.emitMakeShape "rect",
[ @cx2, @cy2, @currentColour, @currentThickness ]
# Adjusting rectangle continues
# @param {number} dx the difference in the x value at the start as opposed to the x value now
# @param {number} dy the difference in the y value at the start as opposed to the y value now
# @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
# @param {Event} e the mouse event
_rectDragging: (dx, dy, x, y, e) ->
[sw, sh] = @_currentSlideDimensions()
# if shift is pressed, make it a square
dy = dx if @shiftPressed
dx = dx / sw
dy = dy / sh
# adjust for negative values as well
if dx >= 0
x1 = @cx2
else
x1 = @cx2 + dx
dx = -dx
if dy >= 0
y1 = @cy2
else
y1 = @cy2 + dy
dy = -dy
globals.connection.emitUpdateShape "rect", [ x1, y1, dx, dy ]
# When rectangle finished being drawn
# @param {Event} e the mouse event
_rectDragStop: (e) ->
if @currentRect?
attrs = undefined
attrs = @currentRect.attrs if @currentRect
if attrs?
globals.connection.emitPublishShape "rect",
[ attrs.x / @gw, attrs.y / @gh, attrs.width / @gw, attrs.height / @gh,
@currentColour, @currentThickness ]
@currentRect = null
# When first starting drawing the ellipse
# @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
_ellipseDragStart: (x, y) ->
sx = (@containerWidth - @gw) / 2
sy = (@containerHeight - @gh) / 2
[sw, sh] = @_currentSlideDimensions()
[cx, cy] = @_currentSlideOffsets()
# find the x and y values in relation to the whiteboard
@ellipseX = (x - @containerOffsetLeft - sx + cx)
@ellipseY = (y - @containerOffsetTop - sy + cy)
globals.connection.emitMakeShape "ellipse",
[ @ellipseX / sw, @ellipseY / sh, @currentColour, @currentThickness ]
# When first starting to draw an ellipse
# @param {number} dx the difference in the x value at the start as opposed to the x value now
# @param {number} dy the difference in the y value at the start as opposed to the y value now
# @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
# @param {Event} e the mouse event
_ellipseDragging: (dx, dy, x, y, e) ->
[sw, sh] = @_currentSlideDimensions()
# if shift is pressed, draw a circle instead of ellipse
dy = dx if @shiftPressed
dx = dx / 2
dy = dy / 2
# adjust for negative values as well
x = @ellipseX + dx
y = @ellipseY + dy
dx = (if dx < 0 then -dx else dx)
dy = (if dy < 0 then -dy else dy)
globals.connection.emitUpdateShape "ellipse",
[ x / sw, y / sh, dx / sw, dy / sh ]
# When releasing the mouse after drawing the ellipse
# @param {Event} e the mouse event
_ellipseDragStop: (e) ->
attrs = undefined
attrs = @currentEllipse.attrs if @currentEllipse?
if attrs?
globals.connection.emitPublishShape "ellipse",
[ attrs.cx / @gw, attrs.cy / @gh, attrs.rx / @gw, attrs.ry / @gh,
@currentColour, @currentThickness ]
@currentEllipse = null # late updates will be blocked by this
# When first dragging the mouse to create the textbox size
# @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
_textStart: (x, y) ->
[sw, sh] = @_currentSlideDimensions()
[cx, cy] = @_currentSlideOffsets()
if @currentText?
globals.connection.emitPublishShape "text",
[ @textbox.value, @currentText.attrs.x / @gw, @currentText.attrs.y / @gh,
@textbox.clientWidth, 16, @currentColour, "Arial", 14 ]
globals.connection.emitTextDone()
@textbox.value = ""
@textbox.style.visibility = "hidden"
@textX = x
@textY = y
sx = (@containerWidth - @gw) / 2
sy = (@containerHeight - @gh) / 2
@cx2 = (x - @containerOffsetLeft - sx + cx) / sw
@cy2 = (y - @containerOffsetTop - sy + cy) / sh
@_makeRect @cx2, @cy2, "#000", 1
globals.connection.emitMakeShape "rect", [ @cx2, @cy2, "#000", 1 ]
# Finished drawing the rectangle that the text will fit into
# @param {Event} e the mouse event
_textStop: (e) ->
@currentRect.hide() if @currentRect?
[sw, sh] = @_currentSlideDimensions()
[cx, cy] = @_currentSlideOffsets()
tboxw = (e.pageX - @textX)
tboxh = (e.pageY - @textY)
if tboxw >= 14 or tboxh >= 14 # restrict size
@textbox.style.width = tboxw * (@gw / sw) + "px"
@textbox.style.visibility = "visible"
@textbox.style["font-size"] = 14 + "px"
@textbox.style["fontSize"] = 14 + "px" # firefox
@textbox.style.color = @currentColour
@textbox.value = ""
sx = (@containerWidth - @gw) / 2
sy = (@containerHeight - @gh) / 2
x = @textX - @containerOffsetLeft - sx + cx + 1 # 1px random padding
y = @textY - @containerOffsetTop - sy + cy
@textbox.focus()
# if you click outside, it will automatically sumbit
@textbox.onblur = (e) =>
if @currentText
globals.connection.emitPublishShape "text",
[ @value, @currentText.attrs.x / @gw, @currentText.attrs.y / @gh,
@textbox.clientWidth, 16, @currentColour, "Arial", 14 ]
globals.connection.emitTextDone()
@textbox.value = ""
@textbox.style.visibility = "hidden"
# if user presses enter key, then automatically submit
@textbox.onkeypress = (e) ->
if e.keyCode is "13"
e.preventDefault()
e.stopPropagation()
@onblur()
# update everyone with the new text at every change
_paper = @
@textbox.onkeyup = (e) ->
@style.color = _paper.currentColour
@value = @value.replace(/\n{1,}/g, " ").replace(/\s{2,}/g, " ")
globals.connection.emitUpdateShape "text",
[ @value, x / _paper.sw, (y + (14 * (_paper.sh / _paper.gh))) / _paper.sh,
tboxw * (_paper.gw / _paper.sw), 16, _paper.currentColour, "Arial", 14 ]
# 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
_setCurrentSlide: (value) ->
@currentSlide = value
_getCurrentSlide: ->
@currentSlide
_currentSlideDimensions: ->
if @currentSlide?
[ @currentSlide.w or 0,
@currentSlide.h or 0 ]
else
[0, 0]
_currentSlideOffsets: ->
if @currentSlide?
[ @currentSlide.cx or 0,
@currentSlide.cy or 0 ]
else
[0, 0]
# @param {string,int} stroke stroke color, can be a number (a hex converted to int) or a
# string (e.g. "#ffff00")
# @param {string,ing} thickness thickness as a number or string (e.g. "2" or "2px")
_strokeAndThickness: (stroke, thickness) ->
stroke = "0" unless stroke?
thickness = "1" unless thickness? and thickness
r =
stroke: if stroke.toString().match(/\#.*/) then stroke else @_colourToHex(stroke)
"stroke-width": if thickness.toString().match(/.*px$/) then thickness else "#{thickness}px"
r
_colourToHex: (value) ->
hex = value.toString(16)
hex = "0" + hex while hex.length < 6
"##{hex}"
WhiteboardPaperModel