d2f09e916d
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).
937 lines
37 KiB
CoffeeScript
Executable File
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
|