Merge branch 'meteor-rebuild-server' of https://github.com/antobinary/bigbluebutton into prototype-metor-client

This commit is contained in:
perroned 2014-07-31 13:22:45 -07:00
commit b3430cc09b
25 changed files with 1968 additions and 9 deletions

View File

@ -14,3 +14,4 @@ iron-router
bootstrap
underscore
amplify
raphaeljs-package

View File

@ -0,0 +1,102 @@
/*
* ScaleRaphael 0.8 by Zevan Rosser 2010
* For use with Raphael library : www.raphaeljs.com
* Licensed under the MIT license.
*
* www.shapevent.com/scaleraphael/
*/
(function(){
window.ScaleRaphael = function(container, width, height){
var wrapper = document.getElementById(container);
if (!wrapper.style.position) wrapper.style.position = "relative";
wrapper.style.width = width + "px";
wrapper.style.height = height + "px";
wrapper.style.overflow = "hidden";
var nestedWrapper;
if (Raphael.type == "VML"){
wrapper.innerHTML = "<rvml:group style='position : absolute; width: 1000px; height: 1000px; top: 0px; left: 0px' coordsize='1000,1000' class='rvml' id='vmlgroup'><\/rvml:group>";
nestedWrapper = document.getElementById("vmlgroup");
}else{
wrapper.innerHTML = "<div id='svggroup'><\/div>";
nestedWrapper = document.getElementById("svggroup");
}
var paper = new Raphael(nestedWrapper, width, height);
var vmlDiv;
if (Raphael.type == "SVG"){
paper.canvas.setAttribute("viewBox", "0 0 "+width+" "+height);
}else{
vmlDiv = wrapper.getElementsByTagName("div")[0];
}
paper.changeSize = function(w, h, center, clipping){
clipping = !clipping;
var ratioW = w / width;
var ratioH = h / height;
var scale = ratioW < ratioH ? ratioW : ratioH;
var newHeight = parseInt(height * scale);
var newWidth = parseInt(width * scale);
if (Raphael.type == "VML"){
// scale the textpaths
var txt = document.getElementsByTagName("textpath");
for (var i in txt){
var curr = txt[i];
if (curr.style){
if(!curr._fontSize){
var mod = curr.style.font.split("px");
curr._fontSize = parseInt(mod[0]);
curr._font = mod[1];
}
curr.style.font = curr._fontSize * scale + "px" + curr._font;
}
}
var newSize;
if (newWidth < newHeight){
newSize = newWidth * 1000 / width;
}else{
newSize = newHeight * 1000 / height;
}
newSize = parseInt(newSize);
nestedWrapper.style.width = newSize + "px";
nestedWrapper.style.height = newSize + "px";
if (clipping){
nestedWrapper.style.left = parseInt((w - newWidth) / 2) + "px";
nestedWrapper.style.top = parseInt((h - newHeight) / 2) + "px";
}
vmlDiv.style.overflow = "visible";
}
if (clipping){
newWidth = w;
newHeight = h;
}
wrapper.style.width = newWidth + "px";
wrapper.style.height = newHeight + "px";
paper.setSize(newWidth, newHeight);
if (center){
wrapper.style.position = "absolute";
wrapper.style.left = parseInt((w - newWidth) / 2) + "px";
wrapper.style.top = parseInt((h - newHeight) / 2) + "px";
}
}
paper.scaleAll = function(amount){
paper.changeSize(width * amount, height * amount);
}
paper.changeSize(width, height);
paper.w = width;
paper.h = height;
return paper;
}
})();

View File

@ -28,6 +28,8 @@ Meteor.startup ->
{isActive:false, name:"Options", class: "optionsChatTab"}
]
@whiteboardPaperModel = new WhiteboardPaperModel('whiteboard-paper')
Template.header.events
"click .usersListIcon": (event) ->
toggleUsersList()

View File

@ -0,0 +1,43 @@
Template.whiteboard.png = ->
currentPresentation = Meteor.Presentations.findOne({"presentation.current": true})
presentationId = currentPresentation?.presentation?.id
currentSlide = Meteor.Slides.findOne({"presentationId": presentationId, "slide.current": true})
console.log "the current slide is:" + currentSlide?.slide?.id
if currentSlide?.slide?.png_uri?
Template.whiteboard.displaySlide((slide) ->
console.log "THIS SLIDE IS DISPLAYED:"# + JSON.stringify slide
Template.whiteboard.displayShapeOnSlide();
)
Template.whiteboard.helpers
displayShapeOnSlide: ->
currentPresentation = Meteor.Presentations.findOne({"presentation.current": true})
presentationId = currentPresentation?.presentation?.id
currentSlide = Meteor.Slides.findOne({"presentationId": presentationId, "slide.current": true})
console.log "(shapesOnSlide)the current slide is:" + currentSlide?.slide?.id
console.log "(shapesOnSlide) and there are #{Meteor.Shapes.find({whiteboardId: currentSlide?.slide?.id}).count()} shapes"
for shape in Meteor.Shapes.find({whiteboardId: currentSlide?.slide?.id}).fetch()
console.log "displaying shape on slide for a " + JSON.stringify shape.shape
shapeType = shape.shape?.shape?.type
data = shape.shape?.shape # TODO change some of these!!
data.points[0] = data.points[0] / 100
data.points[1] = data.points[1] / 100
data.points[2] = data.points[2] / 100
data.points[3] = data.points[3] / 100
console.log "shapeType=" + shapeType
console.log "data=" + JSON.stringify data
whiteboardPaperModel.makeShape(shapeType, data)
whiteboardPaperModel.updateShape(shapeType, data)
displaySlide: (callback) ->
currentPresentation = Meteor.Presentations.findOne({"presentation.current": true})
presentationId = currentPresentation?.presentation?.id
currentSlide = Meteor.Slides.findOne({"presentationId": presentationId, "slide.current": true})
whiteboardPaperModel.create() #TODO maybe move this to main.coffee
whiteboardPaperModel._displayPage(currentSlide?.slide?.png_uri)
callback(currentSlide?.slide)

View File

@ -2,4 +2,9 @@
{{#if getInSession "display_whiteboard"}}
<p>The whiteboard</p>
{{/if}}
<div id="whiteboard-paper" style="position: fixed; top: 10%; left: 30%;">
<p>
{{png}}
</p>
</div>
</template>

View File

@ -0,0 +1,26 @@
# A base class for whiteboard tools
class @WhiteboardToolModel
initialize: (@paper) ->
console.log "paper:" + @paper
@gh = 0
@gw = 0
@obj = 0
# the defintion of this shape, kept so we can redraw the shape whenever needed
@definition = []
#set the size of the paper
# @param {number} @gh gh parameter
# @param {number} @gw gw parameter
setPaperSize: (@gh, @gw) ->
setOffsets: (@xOffset, @yOffset) ->
setPaperDimensions: (@paperWidth, @paperHeight) ->
# TODO: can't we simply take the width and the height from `@paper`?
getDefinition: () ->
@definition
hide: () ->
@obj.hide() if @obj?

View File

@ -0,0 +1,42 @@
# General utility methods
Meteor.methods
# POST request using javascript
# @param {string} path path of submission
# @param {string} params parameters to submit
# @param {string} method method of submission ("post" is default)
# @return {undefined}
postToUrl: (path, params, method) ->
method = method or "post"
form = $("<form></form>")
form.attr
"method" : method,
"action" : path
for key of params
if params.hasOwnProperty(key)
$hiddenField = $ "input"
$hiddenField.attr
"type" : "hidden",
"name" : key,
"value" : params[key]
form.append $hiddenField
$('body').append form
form.submit()
# @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"
thickness ?= "1"
r =
stroke: if stroke.toString().match(/\#.*/) then stroke else colourToHex(stroke)
"stroke-width": if thickness.toString().match(/.*px$/) then thickness else "#{thickness}px"
r
# Convert a color `value` as integer to a hex color (e.g. 255 to #0000ff)
colourToHex = (value) ->
hex = value.toString(16)
hex = "0" + hex while hex.length < 6
"##{hex}"

View File

@ -0,0 +1,40 @@
# The cursor/pointer in the whiteboard
class @WhiteboardCursorModel
constructor: (@paper, @radius=null, @color=null) ->
@radius ?= 3
@color ?= "#ff6666" # a pinkish red
@cursor = null
@paper
draw: () =>
@cursor = @paper.circle(0, 0, @radius)
@cursor.attr
"fill": @color
"stroke": @color
"opacity": "0.8"
$(@cursor.node).on "mousewheel", _.bind(@_onMouseWheel, @)
toFront: () ->
@cursor.toFront() if @cursor?
setRadius: (value) ->
if @cursor?
@cursor.attr r: value
setPosition: (x, y) ->
if @cursor?
@cursor.attr
cx: x
cy: y
undrag: () ->
@cursor.undrag() if @cursor?
drag: (onMove, onStart, onEnd, target=null) ->
if @cursor?
target or= @
@cursor.drag _.bind(onMove, target), _.bind(onStart, target), _.bind(onEnd, target)
_onMouseWheel: (e, delta) ->
@trigger("cursor:mousewheel", e, delta)

View File

@ -0,0 +1,155 @@
# An ellipse in the whiteboard
class @WhiteboardEllipseModel extends WhiteboardToolModel
constructor: (@paper) ->
super @paper
# the defintion of this shape, kept so we can redraw the shape whenever needed
# format: top left x, top left y, bottom right x, bottom right y, stroke color, thickness
@definition = [0, 0, 0, 0, "#000", "0px"]
# @ellipseX = null
# @ellipseY = null
# Make an ellipse on the whiteboard
# @param {[type]} x the x value of the top left corner
# @param {[type]} y the y value of the top left corner
# @param {string} colour the colour of the object
# @param {number} thickness the thickness of the object's line(s)
make: (info) ->
x = info.payload.data.coordinate.first_x
y = info.payload.data.coordinate.first_y
color = info.payload.data.line.color
thickness = info.payload.data.line.weight
@obj = @paper.ellipse(x * @gw + @xOffset, y * @gh + @yOffset, 0, 0)
@obj.attr Utils.strokeAndThickness(color, thickness)
@definition =
shape: "ellipse"
data: [x, y, y, x, @obj.attrs["stroke"], @obj.attrs["stroke-width"]]
@obj
# Update ellipse drawn
# @param {number} x1 the x value of the top left corner in percent of current slide size
# @param {number} y1 the y value of the top left corner in percent of current slide size
# @param {number} x2 the x value of the bottom right corner in percent of current slide size
# @param {number} y2 the y value of the bottom right corner in percent of current slide size
# @param {boolean} square (draw a circle or not
update: (info) ->
x1 = info.payload.data.coordinate.first_x
y1 = info.payload.data.coordinate.first_y
x2 = info.payload.data.coordinate.last_x
y2 = info.payload.data.coordinate.last_y
circle = info.payload.data.square
if @obj?
[x1, x2] = [x2, x1] if x2 < x1
if y2 < y1
[y1, y2] = [y2, y1]
reversed = true
if circle
if reversed #if reveresed, the y1 coordinate gets updated, not the y2 coordinate
y1 = y2 - (x2 - x1) * @gw / @gh
else
y2 = y1 + (x2 - x1) * @gw / @gh
#if the control key is pressed then the width and height of the ellipse are equal (a circle)
#we calculate this by making the y2 coord equal to the y1 coord plus the width of x2-x1 and corrected for the slide size
coords =
x1: x1
x2: x2
y1: y1
y2: y2
console.log(coords)
rx = (x2 - x1) / 2
ry = (y2 - y1) / 2
r=
rx: rx * @gw
ry: ry * @gh
cx: (rx + x1) * @gw + @xOffset
cy: (ry + y1) * @gh + @yOffset
console.log "------------>", r
@obj.attr(r)
console.log( "@gw: " + @gw + "\n@gh: " + @gh + "\n@xOffset: " + @xOffset + "\n@yOffset: " + @yOffset );
# we need to update all these values, specially for when shapes are drawn backwards
@definition.data[0] = x1
@definition.data[1] = y1
@definition.data[2] = x2
@definition.data[3] = y2
# Draw an ellipse on the whiteboard
# @param {number} x1 the x value of the top left corner
# @param {number} y1 the y value of the top left corner
# @param {number} x2 the x value of the bottom right corner
# @param {number} y2 the y value of the bottom right corner
# @param {string} colour the colour of the object
# @param {number} thickness the thickness of the object's line(s)
draw: (x1, y1, x2, y2, colour, thickness) ->
[x1, x2] = [x2, x1] if x2 < x1
[y1, y2] = [y2, y1] if y2 < y1
rx = (x2 - x1) / 2
ry = (y2 - y1) / 2
x = (rx + x1) * @gw + @xOffset
y = (ry + y1) * @gh + @yOffset
elip = @paper.ellipse(x, y, rx * @gw, ry * @gh)
elip.attr Utils.strokeAndThickness(colour, thickness)
elip
# 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
# TODO: moved here but not finished
dragOnStart: (x, y) ->
# sx = (@paperWidth - @gw) / 2
# sy = (@paperHeight - @gh) / 2
# # find the x and y values in relation to the whiteboard
# @ellipseX = (x - @containerOffsetLeft - sx + @xOffset)
# @ellipseY = (y - @containerOffsetTop - sy + @yOffset)
# globals.connection.emitMakeShape "ellipse",
# [ @ellipseX / @paperWidth, @ellipseY / @paperHeight, @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
# TODO: moved here but not finished
dragOnMove: (dx, dy, x, y, e) ->
# # 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 / @paperWidth, y / @paperHeight, dx / @paperWidth, dy / @paperHeight ]
# When releasing the mouse after drawing the ellipse
# @param {Event} e the mouse event
# TODO: moved here but not finished
dragOnStop: (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

View File

@ -0,0 +1,208 @@
MAX_PATHS_IN_SEQUENCE = 30
# A line in the whiteboard
# Note: is used to draw lines from the pencil tool and from the line tool, this is why some
# methods can receive different set of parameters.
# TODO: Maybe this should be split in WhiteboardPathModel for the pencil and
# WhiteboardLineModel for the line tool
class @WhiteboardLineModel extends WhiteboardToolModel
constructor: (@paper) ->
super @paper
# the defintion of this shape, kept so we can redraw the shape whenever needed
# format: svg path, stroke color, thickness
@definition = ["", "#000", "0px"]
# @lineX = null
# @lineY = null
# Creates a line in the paper
# @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
make: (info) ->
console.log "in line MAKE(info): " + info
x = info.payload.data.coordinate.first_x
y = info.payload.data.coordinate.first_y
color = info.payload.data.line.color
thickness = info.payload.data.line.weight
x1 = x * @gw + @xOffset
y1 = y * @gh + @yOffset
path = "M" + x1 + " " + y1 + " L" + x1 + " " + y1
pathPercent = "M" + x + " " + y + " L" + x + " " + y
@obj = @paper.path(path)
@obj.attr Utils.strokeAndThickness(color, thickness)
@obj.attr({"stroke-linejoin": "round"})
@definition =
shape: "path"
data: [pathPercent, @obj.attrs["stroke"], @obj.attrs["stroke-width"]]
@obj
# Update the line dimensions
# @param {number} x1 1) the x of the first point
# 2) the next x point to be added to the line
# @param {number} y1 1) the y of the first point
# 2) the next y point to be added to the line
# @param {number,boolean} x2 1) the x of the second point
# 2) true if the line should be added to the current line,
# false if it should replace the last point
# @param {number} y2 1) the y of the second point
# 2) undefined
update: (info) ->
console.log "in line-UPDATE(info): " + info
x1 = info.payload.data.coordinate.first_x
y1 = info.payload.data.coordinate.first_y
x2 = info.payload.data.coordinate.last_x
y2 = info.payload.data.coordinate.last_y
if @obj?
# if adding points from the pencil
if _.isBoolean(info.adding)
add = info.adding
pathPercent = "L" + x1 + " " + y1
@definition.data[0] += pathPercent
x1 = x1 * @gw + @xOffset
y1 = y1 * @gh + @yOffset
# if adding to the line
if add
path = @obj.attrs.path + "L" + x1 + " " + y1
@obj.attr path: path
# if simply updating the last portion (for drawing a straight line)
else
@obj.attrs.path.pop()
path = @obj.attrs.path.join(" ")
path = path + "L" + x1 + " " + y1
@obj.attr path: path
# adding lines from the line tool
else
path = @_buildPath(x1, y1, x2, y2)
@definition.data[0] = path
path = @_scaleLinePath(path, @gw, @gh, @xOffset, @yOffset)
@obj.attr path: path
# Draw a line on the paper
# @param {number,string} x1 1) the x value of the first point
# 2) the string path
# @param {number,string} y1 1) the y value of the first point
# 2) the colour
# @param {number} x2 1) the x value of the second point
# 2) the thickness
# @param {number} y2 1) the y value of the second point
# 2) undefined
# @param {string} colour 1) the colour of the shape to be drawn
# 2) undefined
# @param {number} thickness 1) the thickness of the line to be drawn
# 2) undefined
draw: (x1, y1, x2, y2, colour, thickness) ->
# if the drawing is from the pencil tool, it comes as a path first
if _.isString(x1)
colour = y1
thickness = x2
path = x1
# if the drawing is from the line tool, it comes with two points
else
path = @_buildPath(x1, y1, x2, y2)
line = @paper.path(@_scaleLinePath(path, @gw, @gh, @xOffset, @yOffset))
line.attr Utils.strokeAndThickness(colour, thickness)
line.attr({"stroke-linejoin": "round"})
line
# When dragging for drawing lines starts
# @param {number} x the x value of the cursor
# @param {number} y the y value of the cursor
# TODO: moved here but not finished
dragOnStart: (x, y) ->
# # find the x and y values in relation to the whiteboard
# sx = (@paperWidth - @gw) / 2
# sy = (@paperHeight - @gh) / 2
# @lineX = x - @containerOffsetLeft - sx + @xOffset
# @lineY = y - @containerOffsetTop - sy + @yOffset
# values = [ @lineX / @paperWidth, @lineY / @paperHeight, @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
# TODO: moved here but not finished
dragOnMove: (dx, dy, x, y) ->
# sx = (@paperWidth - @gw) / 2
# sy = (@paperHeight - @gh) / 2
# [cx, cy] = @_currentSlideOffsets()
# # find the x and y values in relation to the whiteboard
# @cx2 = x - @containerOffsetLeft - sx + @xOffset
# @cy2 = y - @containerOffsetTop - sy + @yOffset
# if @shiftPressed
# globals.connection.emitUpdateShape "line", [ @cx2 / @paperWidth, @cy2 / @paperHeight, false ]
# else
# @currentPathCount++
# if @currentPathCount < MAX_PATHS_IN_SEQUENCE
# globals.connection.emitUpdateShape "line", [ @cx2 / @paperHeight, @cy2 / @paperHeight, true ]
# else if @obj?
# @currentPathCount = 0
# # save the last path of the line
# @obj.attrs.path.pop()
# path = @obj.attrs.path.join(" ")
# @obj.attr path: (path + "L" + @lineX + " " + @lineY)
# # scale the path appropriately before sending
# pathStr = @obj.attrs.path.join(",")
# globals.connection.emitPublishShape "path",
# [ @_scaleLinePath(pathStr, 1 / @gw, 1 / @gh),
# @currentColour, @currentThickness ]
# globals.connection.emitMakeShape "line",
# [ @lineX / @paperWidth, @lineY / @paperHeight, @currentColour, @currentThickness ]
# @lineX = @cx2
# @lineY = @cy2
# Drawing line has ended
# @param {Event} e the mouse event
# TODO: moved here but not finished
dragOnEnd: (e) ->
# if @obj?
# path = @obj.attrs.path
# @obj = null # any late updates will be blocked by this
# # scale the path appropriately before sending
# globals.connection.emitPublishShape "path",
# [ @_scaleLinePath(path.join(","), 1 / @gw, 1 / @gh),
# @currentColour, @currentThickness ]
_buildPath: (x1, y1, x2, y2) ->
"M#{x1} #{y1}L#{x2} #{y2}"
# Scales a path string to fit within a width and height of the new paper size
# @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
# @return {string} the path string after being manipulated to new paper size
_scaleLinePath: (string, w, h, xOffset=0, yOffset=0) ->
path = null
points = string.match(/(\d+[.]?\d*)/g)
len = points.length
j = 0
# go through each point and multiply it by the new height and width
while j < len
if j isnt 0
path += "L" + (points[j] * w + xOffset) + "," + (points[j + 1] * h + yOffset)
else
path = "M" + (points[j] * w + xOffset) + "," + (points[j + 1] * h + yOffset)
j += 2
path

View File

@ -0,0 +1,827 @@
# "Paper" which is the Raphael term for the entire SVG object on the webpage.
# This class deals with this SVG component only.
class @WhiteboardPaperModel
# Container must be a DOM element
constructor: (@container) ->
console.log "paper in WhiteboardPaperModel =" + @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, @)
# Bind to the event triggered when the client connects to the server
# if globals.connection.isConnected()
# @_registerEvents()
# else
# globals.events.on "connection:connected", =>
# @_registerEvents()
# 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.
console.log ("@container=" + @container)
@raphaelObj ?= ScaleRaphael(@container, "500", "500")
@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()
# initializing border around slide to cover up areas which shouldnt show
@borders = {}
for border in ['left', 'right', 'top', 'bottom']
@borders[border] = @raphaelObj.rect(0, 0, 0, 0)
@borders[border].attr("fill", "#ababab")
@borders[border].attr( {stroke:"#ababab"} )
@raphaelObj
# 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()
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
alert "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()) # TODO do we need this?
@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 working as it should
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
# dcr = 1
# @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()
#Changes the currently displayed presentation (if any) with this one
#@param {object} containing the "presentation" object -id,name,pages[]
sharePresentation: (data) ->
#globals.events.trigger("connection:all_slides", data.payload)
# Clear all shapes from this paper.
clearShapes: ->
if @currentShapes?
@currentShapes.forEach (element) ->
element.remove()
@currentShapes = []
@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) ->
alert "updating a " + shape
switch shape
when "line"
@currentLine.update(data)
when "rectangle"
@currentRect.update(data)
when "ellipse"
@currentEllipse.update(data)
when "triangle"
@currentTriangle.update(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) ->
alert "making a " + shape
tool = null
switch shape
when "path", "line"
@currentLine = @_createTool(shape)
toolModel = @currentLine
tool = @currentLine.make(data)
when "rectangle"
@currentRect = @_createTool(shape)
toolModel = @currentRect
tool = @currentRect.make(data)
when "ellipse"
@currentEllipse = @_createTool(shape)
toolModel = @currentEllipse
tool = @currentEllipse.make(data)
when "triangle"
@currentTriangle = @_createTool(shape)
toolModel = @currentTriangle
tool = @currentTriangle.make(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 ?= @raphaelObj.set()
console.log "currentShapes:" + @currentShapes
@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)
#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 )
# Update the slide to move and zoom
# @param {number} xOffset the x value of offset
# @param {number} yOffset the y value of offset
# @param {number} widthRatio the ratio of the previous width
# @param {number} heightRatio the ratio of the previous height
moveAndZoom: (xOffset, yOffset, widthRatio, heightRatio) ->
@globalxOffset = xOffset
@globalyOffset = yOffset
@globalwidthRatio = widthRatio
@globalheightRatio = heightRatio
[slideWidth, slideHeight] = @_currentSlideOriginalDimensions()
#console.log("xOffset: " + xOffset + ", yOffset: " + yOffset);
#console.log("@containerWidth: " + @containerWidth + " @containerHeight: " + @containerHeight);
#console.log("slideWidth: " + slideWidth + " slideHeight: " + slideHeight);
baseWidth = (@containerWidth - slideWidth) / 2
baseHeight = (@containerHeight - slideHeight) / 2
#get the actual size of the slide, depending on the limiting factor (container width or container height)
actualWidth = @currentSlide.displayWidth
actualHeight = @currentSlide.displayHeight
#console.log("actualWidth:" + actualWidth + " actualHeight: " + actualHeight)
#calculate parameters to pass
newXPos = baseWidth - 2* xOffset * actualWidth / 100
newyPos = baseHeight - 2* yOffset * actualHeight / 100
newWidth = actualWidth / 100 * widthRatio
newHeight = actualHeight / 100 * heightRatio
@viewBoxXpos = newXPos
@viewBoxYPos = newyPos
@viewBoxWidth = newWidth
@viewBoxHeight = newHeight
#console.log("newXPos: " + newXPos + " newyPos: " + newyPos + " newWidth: " + newWidth + " newHeight: " + newHeight)
#set parameters to raphael viewbox
@raphaelObj.setViewBox(newXPos , newyPos, newWidth , newHeight , true)
# update the rectangle elements which create the border when page is zoomed
@borders.left.attr( {width:newXPos, height: @containerHeight} )
@borders.right.attr(
x: newXPos + newWidth
y: 0
width:newXPos
height:@containerHeight
)
@borders.top.attr(
width: @containerWidth
height: newyPos
)
@borders.bottom.attr(
y: newyPos + newHeight
width: @containerWidth
height: @containerHeight
)
# borders should appear infront of every other element (i.e. shapes)
for _, border of @borders
border.toFront()
#update cursor to appear the same size even when page is zoomed in
@cursor.setRadius( 3 * widthRatio / 100 )
# Registers listeners for events in the gloval event bus
_registerEvents: ->
# globals.events.on "connection:all_slides", (data) =>
# @removeAllImagesFromPaper()
# ###
# urls = data.slides
# for url in urls
# @addImageToPaper(url[0], url[1], url[2])
# #alert "registerEvents url[0]=" + url[0]
# ###
# urls = data.presentation.pages
# for url in urls
# @addImageToPaper(url.png , 200, 200)
# # alert "registerEvents url[0]=" + url[0]
# globals.events.trigger("whiteboard:paper:all_slides", urls)
# globals.events.on "connection:clrPaper", =>
# @clearShapes()
# globals.events.on "connection:allShapes", (allShapesEventObject) =>
# # TODO: a hackish trick for making compatible the shapes from redis with the node.js
# shapes = allShapesEventObject.shapes
# for shape in shapes
# properties = JSON.parse(shape.data)
# if shape.shape is "path"
# points = properties[0]
# strPoints = ""
# for i in [0..points.length] by 2
# letter = ""
# pA = points[i]
# pB = points[i+1]
# if i == 0
# letter = "M"
# else
# letter = "L"
# strPoints += letter + (pA/100) + "," + (pB/100)
# properties[0] = strPoints
# shape.data = JSON.stringify properties #TODO
# @clearShapes()
# @drawListOfShapes shapes
# globals.events.on "connection:updShape", (shape, data) =>
# @updateShape shape, data
# globals.events.on "connection:whiteboard_draw_event", (shape, data) =>
# @makeShape shape, data
# globals.events.on "connection:share_presentation_event", (data) =>
# @sharePresentation data
# globals.events.on "connection:whiteboardDrawPen", (startingData) =>
# type = startingData.payload.shape_type
# color = startingData.payload.data.line.color
# thickness = startingData.payload.data.line.weight
# points = startingData.shape.points
# if type is "line"
# for i in [0..points.length - 1]
# if i is 0
# #make these compatible with a line
# console.log "points[i]: " + points[i]
# lineObject = {
# shape: {
# type: "line",
# coordinate: {
# firstX : points[i].x/100,
# firstY : points[i].y/100
# },
# color: startingData.payload.data.line.color,
# thickness : startingData.payload.data.line.weight
# }
# adding : false #tell the line object that we are NOT adding points but creating a new line
# }
# console.log "lineObject: " + lineObject
# @makeShape type, lineObject
# else
# console.log "points[i]: "+ points[i]
# lineObject = {
# shape: {
# type: "line",
# coordinate: {
# firstX : points[i].x/100,
# firstY : points[i].y/100
# },
# color: startingData.payload.data.line.color,
# thickness : startingData.payload.data.line.weight
# }
# adding : true #tell the line object that we ARE adding points and NOT creating a new line
# }
# console.log "lineObject: " + lineObject
# @updateShape type, lineObject
# globals.events.on "connection:mvCur", (x, y) =>
# @moveCursor(x, y)
# #console.log "x: " + x + " y: " + y
# globals.events.on "connection:move_and_zoom", (xOffset, yOffset, widthRatio, heightRatio) =>
# @moveAndZoom(xOffset, yOffset, widthRatio, heightRatio)
# globals.events.on "connection:changeslide", (url) =>
# @showImageFromPaper(url)
# globals.events.on "connection:viewBox", (xperc, yperc, wperc, hperc) =>
# xperc = parseFloat(xperc, 10)
# yperc = parseFloat(yperc, 10)
# wperc = parseFloat(wperc, 10)
# hperc = parseFloat(hperc, 10)
# @updatePaperFromServer(xperc, yperc, wperc, hperc)
# globals.events.on "connection:fitToPage", (value) =>
# @setFitToPage(value)
# globals.events.on "connection:zoom", (delta) =>
# @setZoom(delta)
# globals.events.on "connection:paper", (cx, cy, sw, sh) =>
# @updatePaperFromServer(cx, cy, sw, sh)
# globals.events.on "connection:panStop", =>
# @stopPanning()
# globals.events.on "connection:toolChanged", (tool) =>
# @setCurrentTool(tool)
# globals.events.on "connection:textDone", =>
# @textDone()
# globals.events.on "connection:uploadStatus", (message, fade) =>
# globals.events.trigger("whiteboard:paper:uploadStatus", message, fade)
# 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()
console.log "_onWindowResize"
#TODO: temporary hacked away fix so that the buttons resize correctly when the window resizes
$("#users-btn").click();
$("#users-btn").click();
#TODO: maybe find solution besides these global values..no conflicts however
[slideWidth, slideHeight] = @_currentSlideOriginalDimensions()
#console.log("xOffset: " + xOffset + ", yOffset: " + yOffset);
#console.log("@containerWidth: " + @containerWidth + " @containerHeight: " + @containerHeight);
#console.log("slideWidth: " + slideWidth + " slideHeight: " + slideHeight);
baseWidth = (@containerWidth - slideWidth) / 2
baseHeight = (@containerHeight - slideHeight) / 2
#get the actual size of the slide, depending on the limiting factor (container width or container height)
if(@containerWidth - slideWidth < @containerHeight - slideHeight)
actualHeight = @containerWidth * slideHeight / slideWidth
actualWidth = @containerWidth
else
actualWidth = @containerHeight * slideWidth / slideHeight
actualHeight = @containerHeight
#console.log("actualWidth:" + actualWidth + " actualHeight: " + actualHeight)
#calculate parameters to pass
newXPos = baseWidth
newyPos = baseHeight
newWidth = actualWidth
newHeight = actualHeight
#now the zooming will still be correct when the window is resized
#and hopefully when rotated on a mobile device
if @globalxOffset? && @globalyOffset? && @globalwidthRatio? && @globalheightRatio?
console.log "has zoomed in"
@moveAndZoom(@globalxOffset, @globalyOffset, @globalwidthRatio, @globalheightRatio)
else
obj =
globalxOffset : @globalxOffset
globalyOffset : @globalyOffset
globalwidthRatio : @globalwidthRatio
globalheightRatio : @globalheightRatio
console.log obj
console.log "not zoomed"
@raphaelObj.setViewBox(newXPos, newyPos, newWidth, newHeight,true)
# 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 "rectangle"
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
console.log "the url did not match the expected format"
#globals.presentationServer + url
#Changes the currently displayed page/slide (if any) with this one
#@param {data} message object containing the "presentation" object
_displayPage: (data) ->
@removeAllImagesFromPaper()
#page = data?.payload?.currentPage
#pngSlide = "http://www.tux.org/pub/sites/ftp.gnome.org/GNOME/teams/art.gnome.org/backgrounds/ABSTRACT-BlueRidge_1280x1024.png"
#@addImageToPaper(page.png_uri, 400, 400) # TODO the dimensions should be modified
@addImageToPaper(data, 400, 400) # TODO the dimensions should be modified

View File

@ -0,0 +1,150 @@
# A rectangle in the whiteboard
class @WhiteboardRectModel extends WhiteboardToolModel
constructor: (@paper) ->
super @paper
console.log "@paper in WhiteboardRectModel=" + @paper
# the defintion of this shape, kept so we can redraw the shape whenever needed
# format: x1, y1, x2, y2, stroke color, thickness
@definition = [0, 0, 0, 0, "#000", "0px"]
@paper
# Creates a rectangle in the paper
# @param {number} x the x value of the top left corner
# @param {number} y the y value of the top left corner
# @param {string} colour the colour of the object
# @param {number} thickness the thickness of the object's line(s)
make: (startingData) =>
console.log "MAKING RECT"
console.log "@paper in make in WhiteboardRectModel=" + @paper
console.log "make startingData"+ JSON.stringify startingData
x = startingData.points[0]
y = startingData.points[1]
color = startingData.color
thickness = startingData.thickness
@obj = @paper.rect(x * @gw + @xOffset, y * @gh + @yOffset, 0, 0, 1)
@obj.attr Meteor.call("strokeAndThickness",color, thickness)
@definition =
shape: "rect"
data: [x, y, 0, 0, @obj.attrs["stroke"], @obj.attrs["stroke-width"]]
@obj
# Update the rectangle dimensions
# @param {number} x1 the x value of the top left corner
# @param {number} y1 the y value of the top left corner
# @param {number} x2 the x value of the bottom right corner
# @param {number} y2 the y value of the bottom right corner
# @param {boolean} square (draw a square or not)
update: (startingData) ->
console.log "UPDATING RECT"
x1 = startingData.points[0]
y1 = startingData.points[1]
x2 = startingData.points[2]
y2 = startingData.points[3]
console.log "updating rect points:" + x1, x2, y1, y2
square = startingData.square
if @obj?
[x1, x2] = [x2, x1] if x2 < x1
[x1, x2] = [x2, x1] if x2 < x1
if y2 < y1
[y1, y2] = [y2, y1]
reversed = true
if square
if reversed #if reveresed, the y1 coordinate gets updated, not the y2 coordinate
y1 = y2 - (x2 - x1) * @gw / @gh
else
y2 = y1 + (x2 - x1) * @gw / @gh
x = x1 * @gw + @xOffset
y = y1 * @gh + @yOffset
width = (x2 * @gw + @xOffset) - x
height = (y2 * @gh + @yOffset) - y
#if !square
@obj.attr
x: x
y: y
width: width
height: height
###else
@obj.attr
x: x
y: y
width: width
height: width###
# we need to update all these values, specially for when shapes are drawn backwards
@definition.data[0] = x1
@definition.data[1] = y1
@definition.data[2] = x2
@definition.data[3] = y2
# Draw a rectangle on the paper
# @param {number} x1 the x value of the top left corner
# @param {number} y1 the y value of the top left corner
# @param {number} x2 the x value of the bottom right corner
# @param {number} y2 the y value of the bottom right corner
# @param {string} colour the colour of the object
# @param {number} thickness the thickness of the object's line(s)
draw: (x1, y1, x2, y2, colour, thickness) ->
[x1, x2] = [x2, x1] if x2 < x1
[y1, y2] = [y2, y1] if y2 < y1
x = x1 * @gw
y = y1 * @gh
r = @paper.rect(x + @xOffset, y + @yOffset, (x2 * @gw) - x, (y2 * @gh) - y, 1)
r.attr Meteor.call("strokeAndThickness", colour, thickness)
r
# 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
# TODO: moved here but not finished
dragOnStart: (x, y) ->
# sx = (@paperWidth - @gw) / 2
# sy = (@paperHeight - @gh) / 2
# # find the x and y values in relation to the whiteboard
# @cx2 = (x - @containerOffsetLeft - sx + @xOffset) / @paperWidth
# @cy2 = (y - @containerOffsetTop - sy + @yOffset) / @paperHeight
# 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
# TODO: moved here but not finished
dragOnMove: (dx, dy, x, y, e) ->
# # if shift is pressed, make it a square
# dy = dx if @shiftPressed
# dx = dx / @paperWidth
# dy = dy / @paperHeight
# # 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
# TODO: moved here but not finished
dragOnEnd: (e) ->
# if @obj?
# attrs = @obj.attrs
# if attrs?
# globals.connection.emitPublishShape "rect",
# [ attrs.x / @gw, attrs.y / @gh, attrs.width / @gw, attrs.height / @gh,
# @currentColour, @currentThickness ]
# @obj = null

View File

@ -0,0 +1,21 @@
# A slide in the whiteboard
class @WhiteboardSlideModel
# TODO: check if we really need original and display width and heights separate or if they can be the same
constructor: (@id, @url, @img, @originalWidth, @originalHeight, @displayWidth, @displayHeight, @xOffset=0, @yOffset=0) ->
getWidth: -> @displayWidth
getHeight: -> @displayHeight
getOriginalWidth: -> @originalWidth
getOriginalHeight: -> @originalHeight
getId: -> @id
getDimensions: -> [@getWidth(), @getHeight()]
getOriginalDimensions: -> [@getOriginalWidth(), @getOriginalHeight()]
getOffsets: -> [@xOffset, @yOffset]

View File

@ -0,0 +1,155 @@
# A text in the whiteboard
class @WhiteboardTextModel extends WhiteboardToolModel
constructor: (@paper) ->
super @paper
# the defintion of this shape, kept so we can redraw the shape whenever needed
# format: x, y, width, height, colour, fontSize, calcFontSize, text
@definition = [0, 0, 0, 0, "#000", 0, 0, ""]
# @textX = null
# @textY = null
# Make a text on the whiteboard
make: (x, y, width, height, colour, fontSize, calcFontSize, text) ->
@definition =
shape: "text"
data: [x, y, width, height, colour, fontSize, calcFontSize, text]
calcFontSize = (calcFontSize/100 * @gh)
x = (x * @gw) + @xOffset
y = (y * @gh) + @yOffset + calcFontSize
width = width/100 * @gw
colour = Utils.strokeAndThickness(colour)["stroke"]
@obj = @paper.text(x, y, "")
@obj.attr
fill: colour
"font-family": "Arial" # TODO: make dynamic
"font-size": calcFontSize
@obj.node.style["text-anchor"] = "start" # force left align
@obj.node.style["textAnchor"] = "start" # for firefox, 'cause they like to be different
@obj
# Update triangle drawn
# @param {number} x1 the x value of the top left corner
# @param {number} y1 the y value of the top left corner
# @param {number} x2 the x value of the bottom right corner
# @param {number} y2 the y value of the bottom right corner
update: (x, y, width, height, colour, fontSize, calcFontSize, text) ->
if @obj?
@definition.data = [x, y, width, height, colour, fontSize, calcFontSize, text]
calcFontSize = (calcFontSize/100 * @gh)
x = (x * @gw) + @xOffset
width = width/100 * @gw
colour = Utils.strokeAndThickness(colour)["stroke"]
@obj.attr
fill: colour
"font-size": calcFontSize
cell = @obj.node
while cell? and cell.hasChildNodes()
cell.removeChild(cell.firstChild)
textFlow(text, cell, width, x, calcFontSize, false)
# Draw a text on the whiteboard
# @param {string} colour the colour of the object
# @param {number} thickness the thickness of the object's line(s)
draw: (x, y, width, height, colour, fontSize, calcFontSize, text) ->
calcFontSize = (calcFontSize/100 * @gh)
x = x * @gw + @xOffset
y = (y * @gh) + @yOffset + calcFontSize
width = width/100 * @gw
colour = Utils.strokeAndThickness(colour)["stroke"]
el = @paper.text(x, y, "")
el.attr
fill: colour
"font-family": "Arial" # TODO: make dynamic
"font-size": calcFontSize
el.node.style["text-anchor"] = "start" # force left align
el.node.style["textAnchor"] = "start" # for firefox, 'cause they like to be different
textFlow(text, el.node, width, x, calcFontSize, false)
el
# 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
# TODO: moved here but not finished nor tested
# _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
# TODO: moved here but not finished nor tested
# _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 ]
# The server has said the text is finished,
# so set it to null for the next text object
# TODO: moved here but not finished nor tested
# textDone: ->
# if @currentText?
# @currentText = null
# @currentRect.hide() if @currentRect?

View File

@ -0,0 +1,105 @@
# A triangle in the whiteboard
class @WhiteboardTriangleModel extends WhiteboardToolModel
constructor: (@paper) ->
super @paper
# the defintion of this shape, kept so we can redraw the shape whenever needed
# format: x1, y1, x2, y2, stroke color, thickness
@definition = [0, 0, 0, 0, "#000", "0px"]
# Make a triangle on the whiteboard
# @param {[type]} x the x value of the top left corner
# @param {[type]} y the y value of the top left corner
# @param {string} colour the colour of the object
# @param {number} thickness the thickness of the object's line(s)
make: (info) ->
x = info.payload.data.coordinate.first_x
y = info.payload.data.coordinate.first_y
color = info.payload.data.line.color
thickness = info.payload.data.line.weight
path = @_buildPath(x, y, x, y, x, y)
@obj = @paper.path(path)
@obj.attr Utils.strokeAndThickness(color, thickness)
@obj.attr({"stroke-linejoin": "round"})
@definition =
shape: "triangle"
data: [x, y, x, y, @obj.attrs["stroke"], @obj.attrs["stroke-width"]]
@obj
# Update triangle drawn
# @param {number} x1 the x value of the top left corner
# @param {number} y1 the y value of the top left corner
# @param {number} x2 the x value of the bottom right corner
# @param {number} y2 the y value of the bottom right corner
update: (info) ->
x1 = info.payload.data.coordinate.first_x
y1 = info.payload.data.coordinate.first_y
x2 = info.payload.data.coordinate.last_x
y2 = info.payload.data.coordinate.last_y
if @obj?
[xTop, yTop, xBottomLeft, yBottomLeft, xBottomRight, yBottomRight] = @_getCornersFromPoints(x1, y1, x2, y2)
path = @_buildPath(xTop * @gw + @xOffset, yTop * @gh + @yOffset,
xBottomLeft * @gw + @xOffset, yBottomLeft * @gh + @yOffset,
xBottomRight * @gw + @xOffset, yBottomRight * @gh + @yOffset)
@obj.attr path: path
@definition.data[0] = x1
@definition.data[1] = y1
@definition.data[2] = x2
@definition.data[3] = y2
# Draw a triangle on the whiteboard
# @param {number} x1 the x value of the top left corner
# @param {number} y1 the y value of the top left corner
# @param {number} x2 the x value of the bottom right corner
# @param {number} y2 the y value of the bottom right corner
# @param {string} colour the colour of the object
# @param {number} thickness the thickness of the object's line(s)
draw: (x1, y1, x2, y2, colour, thickness) ->
[xTop, yTop, xBottomLeft, yBottomLeft, xBottomRight, yBottomRight] = @_getCornersFromPoints(x1, y1, x2, y2)
path = @_buildPath(xTop, yTop, xBottomLeft, yBottomLeft, xBottomRight, yBottomRight)
path = @_scaleTrianglePath(path, @gw, @gh, @xOffset, @yOffset)
triangle = @paper.path(path)
triangle.attr Utils.strokeAndThickness(colour, thickness)
triangle.attr({"stroke-linejoin": "round"})
triangle
_getCornersFromPoints: (x1, y1, x2, y2) ->
xTop = (((x2 - x1) / 2) + x1)
yTop = y1
xBottomLeft = x1
yBottomLeft = y2
xBottomRight = x2
yBottomRight = y2
[xTop, yTop, xBottomLeft, yBottomLeft, xBottomRight, yBottomRight]
_buildPath: (xTop, yTop, xBottomLeft, yBottomLeft, xBottomRight, yBottomRight) ->
"M#{xTop},#{yTop},#{xBottomLeft},#{yBottomLeft},#{xBottomRight},#{yBottomRight}z"
# Scales a triangle path string to fit within a width and height of the new paper size
# @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
# @return {string} the path string after being manipulated to new paper size
_scaleTrianglePath: (string, w, h, xOffset=0, yOffset=0) ->
path = null
points = string.match(/(\d+[.]?\d*)/g)
len = points.length
j = 0
# go through each point and multiply it by the new height and width
path = "M"
while j < len
path += "," unless j is 0
path += "" + (points[j] * w + xOffset) + "," + (points[j + 1] * h + yOffset)
j += 2
path + "z"
WhiteboardTriangleModel

View File

@ -1,5 +1,6 @@
Meteor.Users = new Meteor.Collection("bbb_users")
Meteor.Chat = new Meteor.Collection("bbb_chat")
Meteor.Meetings = new Meteor.Collection("meetings")
Meteor.Presentations = new Meteor.Collection("presentations")
Meteor.Shapes = new Meteor.Collection("shapes")
Meteor.Slides = new Meteor.Collection("slides")

View File

@ -0,0 +1,21 @@
Meteor.methods
addPresentationToCollection: (meetingId, presentationObject) ->
#check if the presentation is already in the collection
unless Meteor.Presentations.findOne({meetingId: meetingId, 'presentation.id': presentationObject.id})?
entry =
meetingId: meetingId
presentation:
id: presentationObject.id
name: presentationObject.name
current: presentationObject.current
id = Meteor.Presentations.insert(entry)
console.log "added presentation id =[#{id}]:#{presentationObject.id} in #{meetingId}. Presentations.size is now
#{Meteor.Presentations.find({meetingId: meetingId}).count()}"
removePresentationFromCollection: (meetingId, presentationId) ->
if meetingId? and presentationId? and Meteor.Presentations.findOne({meetingId: meetingId, "presentation.id": presentationId})?
id = Meteor.Presentations.findOne({meetingId: meetingId, "presentation.id": presentationId})
if id?
Meteor.Presentations.remove(id._id)
console.log "----removed presentation[" + presentationId + "] from " + meetingId

View File

@ -22,3 +22,4 @@ Meteor.methods
id = Meteor.Shapes.insert(entry)
console.log "added shape id =[#{id}]:#{shapeObject.id} in #{meetingId}"

View File

@ -1,6 +1,6 @@
Meteor.methods
addSlideToCollection: (meetingId, presentationId, slideObject) ->
unless Meteor.Slides.findOne({meetingId: meetingId, presentationId: presentationId})?
unless Meteor.Slides.findOne({meetingId: meetingId, "slide.id": slideObject.id})?
entry =
meetingId: meetingId
presentationId: presentationId
@ -20,3 +20,21 @@ Meteor.methods
id = Meteor.Slides.insert(entry)
console.log "added slide id =[#{id}]:#{slideObject.id} in #{meetingId}. Now there are
#{Meteor.Slides.find({meetingId: meetingId}).count()} slides in the meeting"
removeSlideFromCollection: (meetingId, slideId) ->
if meetingId? and slideId? and Meteor.Slides.findOne({meetingId: meetingId, "slide.id": slideId})?
id = Meteor.Slides.findOne({meetingId: meetingId, "slide.id": slideId})
if id?
Meteor.Slides.remove(id._id)
console.log "----removed slide[" + slideId + "] from " + meetingId
displayThisSlide: (meetingId, newSlideId, slideObject) ->
presentationId = newSlideId.split("/")[0] # grab the presentationId part of the slideId
# change current to false for the old slide
Meteor.Slides.update({presentationId: presentationId, "slide.current": true}, {$set: {"slide.current": false}})
# for the new slide: remove the version which came with presentation_shared_message from the Collection
# to avoid using old data (this message contains everything we need for the new slide)
Meteor.call("removeSlideFromCollection", meetingId, newSlideId)
# add the new slide to the collection
Meteor.call("addSlideToCollection", meetingId, presentationId, slideObject)

View File

@ -13,7 +13,8 @@
Meteor.subscribe 'shapes', getInSession('meetingId'), ->
Meteor.subscribe 'slides', getInSession('meetingId'), ->
Meteor.subscribe 'meetings', getInSession('meetingId'), ->
self.redirect('/')
Meteor.subscribe 'presentations', getInSession('meetingId'), ->
self.redirect('/')
onBeforeAction: ()->
url = location.href
@ -49,7 +50,8 @@
Meteor.subscribe 'chat', getInSession('meetingId'), ->
Meteor.subscribe 'shapes', getInSession('meetingId'), ->
Meteor.subscribe 'slides', getInSession('meetingId'), ->
Meteor.subscribe 'meetings', getInSession('meetingId')
Meteor.subscribe 'meetings', getInSession('meetingId'), ->
Meteor.subscribe 'presentations', getInSession('meetingId')
@route "logout",
path: "logout"

View File

@ -2,3 +2,4 @@
/iron-router
/blaze-layout
/npm
/raphaeljs-package

View File

@ -15,6 +15,9 @@ Meteor.publish 'slides', (meetingId) ->
Meteor.publish 'meetings', (meetingId) ->
Meteor.Meetings.find({meetingId: meetingId})
Meteor.publish 'presentations', (meetingId) ->
Meteor.Presentations.find({meetingId: meetingId})
# Clear all data in subcriptions
@clearCollections = ->
Meteor.Users.remove({})
@ -27,3 +30,5 @@ Meteor.publish 'meetings', (meetingId) ->
console.log "cleared Shapes Collection!"
Meteor.Slides.remove({})
console.log "cleared Slides Collection!"
Meteor.Presentations.remove({})
console.log "cleared Presentations Collection!"

View File

@ -153,7 +153,7 @@ class Meteor.RedisPubSub
Meteor.call("removeUserFromCollection", meetingId, userId)
if message.header?.name is "get_chat_history_reply" and message.payload?.requester_id is "nodeJSapp"
unless Meteor.Meetings.findOne({MeetingId: message.payload?.meeting_id})?
unless Meteor.Meetings.findOne({MeetingId: message.payload?.meeting_id})? # TODO check if MeetingId or meetingId!!
for chatMessage in message.payload?.chat_history
Meteor.call("addChatToCollection", meetingId, chatMessage)
@ -168,16 +168,28 @@ class Meteor.RedisPubSub
@invokeGetAllMeetingsRequest()
if message.header?.name is "presentation_shared_message"
presentationId = message.payload?.presentation?.id
# change the currently displayed presentation to presentation.current = false
Meteor.Presentations.update({"presentation.current": true},{$set: {"presentation.current": false}})
#update(if already present) entirely the presentation with the fresh data
Meteor.call("removePresentationFromCollection", meetingId, presentationId)
Meteor.call("addPresentationToCollection", meetingId, message.payload?.presentation)
for slide in message.payload?.presentation?.pages
Meteor.call("addSlideToCollection", meetingId, slide.id, slide)
Meteor.call("addSlideToCollection", meetingId, message.payload?.presentation?.id, slide)
if slide.current
Meteor.call("displayThisSlide", meetingId, slide.id, slide)
if message.header?.name is "get_presentation_info_reply" and message.payload?.requester_id is "nodeJSapp"
# to do: grab the whiteboard shapes using the whiteboard_id we have here
# todo: grab the whiteboard shapes using the whiteboard_id we have here
for presentation in message.payload?.presentations
Meteor.call("addPresentationToCollection", meetingId, presentation)
for page in presentation.pages
#add the slide to the collection
Meteor.call("addSlideToCollection", meetingId, page.id, page)
Meteor.call("addSlideToCollection", meetingId, presentation.id, page)
#request for shapes
whiteboardId = "#{presentation.id}/#{page.num}" # d2d9a672040fbde2a47a10bf6c37b6a4b5ae187f-1404411622872/1
@ -198,6 +210,10 @@ class Meteor.RedisPubSub
else
console.log "did not have enough information to send a user_leaving_request"
if message.header?.name is "presentation_page_changed_message"
newSlide = message.payload?.page
Meteor.call("displayThisSlide", meetingId, newSlide?.id, newSlide)
if message.header?.name is "get_whiteboard_shapes_reply" and message.payload?.requester_id is "nodeJSapp"
for shape in message.payload.shapes
whiteboardId = shape.wb_id

View File

@ -3,6 +3,9 @@
"redis": {},
"npm": {},
"bootstrap-3": {},
"iron-router": {}
"iron-router": {},
"raphaeljs-package": {
"git": "https://github.com/tomconnors/raphaeljs-package.git"
}
}
}

View File

@ -5,7 +5,11 @@
"redis": {},
"npm": {},
"bootstrap-3": {},
"iron-router": {}
"iron-router": {},
"raphaeljs-package": {
"git": "https://github.com/tomconnors/raphaeljs-package.git",
"branch": "master"
}
},
"packages": {
"redis": {
@ -28,6 +32,11 @@
"tag": "v0.7.1",
"commit": "d1ffb3f06ea4c112132b030f2eb1a70b81675ecb"
},
"raphaeljs-package": {
"git": "https://github.com/tomconnors/raphaeljs-package.git",
"branch": "master",
"commit": "85eaef3664ec063e4bcb81be1226d126d4d20539"
},
"blaze-layout": {
"git": "https://github.com/EventedMind/blaze-layout.git",
"tag": "v0.2.4",