// Canvas.Hs, control javascript canvas with Haskell
// Copyright (C) 2013, Lennart Buit, Joost van Doorn, Pim Jager, Martijn Roo,
// Thijs Scheepers
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
// USA
// Global variables
var topLayerIdx = 0;
var layerList = new Array();
var stage = undefined;
var connection = new WebSocket('ws://localhost:8080');
// Keep track of scrollable shapes.
var scrollShapes = new Array();
var canvasWindowWidth = 900;
var canvasWindowHeight = 600;
// Event handlers
var canvas = undefined;
var generadedShapeIdIdx = 0;
var debugOn = true;
var debugAnnimatingShapes = [];
var debugCanClose = true;
var mouseDragId = undefined;
var mouseDragEndHandler = undefined;
var mouseDragHandler = undefined;
var dragEventRateLimiter = undefined;
var mouseDragFound = true;
var enableDragHandler = true;
var prevMousePosX = 0;
var prevMousePosY = 0;
var mouseMoveRateLimit = 90; // The mousemove interval limit
var animated = false;
/**
* Handles data received from the websocket connection.
* @param {String} event The event received from the server side.
* @returns {undefined}
*/
function connectionDataReceived(event) {
// Reset mousedrag
mouseDragFound = false;
var dataObject = jQuery.parseJSON(event.data);
// handle the shape data
if(hasProperty(dataObject,"shape") && dataObject.shape != undefined) {
// Reset tracking of scrollable shapes
scrollShapes = new Array();
var shape = parseShapeData(dataObject.shape);
// Clear screen
layerList[topLayerIdx].destroyChildren();
// Disable mousedrag if event is no longer attached to the shape
if(!mouseDragFound && mouseDragId!=undefined) {
mouseDragEndEventHandler(mouseDragId,undefined);
}
// Draw on current layer
if(shape != undefined) {
layerList[topLayerIdx].add(shape);
}
layerList[topLayerIdx].batchDraw();
}
else {
printDebugMessage("No shape data recieved",1);
}
if(hasProperty(dataObject,"actions") && dataObject.actions != undefined &&
Object.prototype.toString.call( dataObject.actions ) === '[object Array]' ) {
var actions = dataObject.actions;
for (var i = 0; i < actions.length; i++) {
parseActionData(actions[i]);
}
}
else {
printDebugMessage("No actions received",0);
}
}
/**
* Prints a message to the console when a connection error occurs.
* @param {String} error The error message.
* @returns {undefined}
*/
function connectionError(error) {
printDebugMessage("WebSocket Error " + error,2);
}
/**
* Display a connection lost window.
* @param {String} error An error message for the console.
* @returns {undefined}
*/
function connectionClosed(error) {
printDebugMessage("Connection closed " + error,0);
openControlWindow("Connection lost");
}
/**
* Opens a control element with a title and a message
* @param {String} title The title of the message (required)
* @param {String} message The contents of the message (optional)
* @returns {undefined}
*/
function openControlWindow(title, message) {
// Opens the control interface element
message = message == undefined ? "" : message;
$("#control-wrapper").addClass('display');
$("#control-window").addClass('display');
$("#control-window").html("<div class=\"control-content\"><p><strong>"+title+"</strong><br />"+message+"</p></div>");
}
/**
* Closes the control element
* @returns {undefined}
*/
function closeControlWindow() {
// Closes the control interface element
$("#control-wrapper").removeClass('display');
$("#control-window").removeClass('display');
}
/**
* Type is an enumeration where 0 is FizedSize 1 is FullWindow and 2 is FullScreen.
* Width and height are required with FixedSize and are ignored with the other types.
* @param {String} displayType
* @param {Number} attempt
* @returns {undefined}
*/
function setWindowDisplayType(displayType, attempt)
{
attempt = attempt != undefined ? attempt : 1;
switch (displayType) {
case 0: // FixedSize
window.fullScreenApi.cancelFullScreen(document.getElementById('wrapper'));
$("body").removeClass('fullscreen');
$("body").removeClass('fullwindow');
// Animate the kinetic container
$("#canvas div").animate({
width: canvasWindowWidth+'px',
height: canvasWindowHeight+'px'},(animated ? 300 : 0));
setFixedProportions($("#canvas"), canvasWindowWidth, canvasWindowHeight);
resizeCanvas(); // Resizes the canvas
break;
case 1: // FullWindow
window.fullScreenApi.cancelFullScreen(document.getElementById('wrapper'));
$("body").addClass('fullwindow');
$("body").removeClass('fullscreen');
closeControlWindow(); // Maybe we need to check if we really want to do this?
setFluidProportions($(canvas).add(canvas).parent().parent());
$(window).resize(resizeCanvas);
resizeCanvas(); // Resizes the canvas
break;
case 2: // FullScreen
console.log(canvas);
window.fullScreenApi.requestFullScreen(canvas.parent().parent().get(0));
$("body").addClass('fullscreen');
$("body").removeClass('fullwindow');
closeControlWindow(); // Maybe we need to check if we really want to do this?
setFluidProportions($(canvas).add(canvas).parent().parent());
$(window).resize(resizeCanvas);
resizeCanvas(); // Resizes the canvas
// If this did not result in a full screen window then request it from the user.
if(window.fullScreenApi.isFullScreen() == false) {
if(attempt > 2) {
// Show a message if it was not possible to switch to full screen
openControlWindow("Failed to switch to fullscreen");
setTimeout(closeControlWindow, (animated ? 2400 : 0));
}
else {
setTimeout(requestFullscreen.bind(undefined, attempt+1), (animated ? 100*attempt : 0));
}
}
break;
default:
printDebugMessage("Window display type not supported ("+displayType+")",0);
};
}
/**
* Asks the user to go fullscreen.
* @param {Number} attempt
* @returns {undefined}
*/
function requestFullscreen(attempt) {
if(window.fullScreenApi.isFullScreen() == false) {
openControlWindow("Switch to fullscreen?","<a href=\"#\" id=\"switchToFullscreen\">Yes</a> - <a href=\"#\" id=\"switchToFullwindow\">No</a>");
$("#switchToFullscreen").off('click');
$("#switchToFullwindow").off('click');
$("#switchToFullscreen").click(setWindowDisplayType.bind(undefined, 2, attempt));
$("#switchToFullwindow").click(setWindowDisplayType.bind(undefined, 1, attempt));
}
}
/**
* Opens a prompt to ask if a file should be uploaded. When clicked on "Yes" a file selection browser will be opened.
* @returns {undefined}
*/
function requestUpload() {
openControlWindow("Upload a file?","<a href=\"#\" id=\"acceptPrompt\">Yes</a> - <a href=\"#\" id=\"closePrompt\">No</a>");
$("#acceptPrompt").off('click');
$("#closePrompt").off('click');
$("#acceptPrompt").click(promptFileBrowser.bind(undefined));
$("#closePrompt").click(closeControlWindow.bind(undefined));
}
/**
* Opens a file browser in which you can select a file. This function should be called directly through user input
* and not through a websocket for example. Browsers have built in protection to prevent this, the prompt will not show.
* @returns {undefined}
*/
function promptFileBrowser() {
$('#fileUpload').trigger('click');
$('#fileUpload').change(function() {
if ( this.files && this.files[0] ) {
var FR= new FileReader();
FR.onload = function(e) {
printDebugMessage("Uploading file: "+e.target.result,0);
//e.target.result contains the base64 file contents, but with a mimetype prepended (which we don't want)
parts = e.target.result.split("base64,");
mimetype = parts[0]; //not used
contents = parts[1];
connection.send(JSON.stringify({
"event":"upload",
"data":{
"filecontents": contents
}
}));
};
FR.readAsDataURL( this.files[0] );
}
});
closeControlWindow();
}
/**
* Sets fluid proportions for the container.
* @param {Element} container
* @returns {undefined}
*/
function setFluidProportions(container) {
// Animate to fluid width and height
container.animate({
top: "0",
left: "0",
width: '100%',
height: '100%',
marginTop: "0px",
marginLeft: "0px"
},{duration: (animated ? 300 : 0),step:resizeCanvasWithoutEvent, done:resizeCanvas});
}
/**
* Sets fixed proportions for the container.
* @param {Element} container
* @param {Number} width The new fixed width of the container.
* @param {Number} height The new fixed height of the container.
* @returns {undefined}
*/
function setFixedProportions(container,width,height) {
// Animate to fixed width and height
container.animate({
top: "50%",
left: "50%",
width: width+'px',
height: height+'px',
marginTop: "-"+height/2+"px",
marginLeft: "-"+width/2+"px"
},{duration: (animated ? 300 : 0),step:resizeCanvas});
}
/**
* Resizes the canvas.
* @param {Event} event
* @returns {undefined}
*/
function resizeCanvas(event) {
$("#canvas canvas").css( "width", $("#canvas").width()+"px" );
$("#canvas canvas").css( "height", $("#canvas").height()+"px" );
if(stage != undefined) {
stage.setSize($("#canvas").width(),$("#canvas").height());
stage.batchDraw(); // Redraw Canvas
}
sendWindowResizeEvent($("#canvas").width(),$("#canvas").height());
}
/**
* Resizes the canvas without sending an event, so that the connection doesn't get filled with resize messages
*/
function resizeCanvasWithoutEvent(event) {
$("#canvas canvas").css( "width", $("#canvas").width()+"px" );
$("#canvas canvas").css( "height", $("#canvas").height()+"px" );
if(stage != undefined) {
stage.setSize($("#canvas").width(),$("#canvas").height());
stage.batchDraw(); // Redraw Canvas
}
}
/**
* Parses shape data and returns an object kinetic accepts. Also coupling to event handlers is done.
* @param {JSON} shape data
* @returns {Kinetic.Group|shapeFromData.shape|Kinetic.Circle|Kinetic.Line|Kinetic.Polygon|Kinetic.Text|Kinetic.Rect}
*/
function parseShapeData(data) {
var shape = shapeFromData(data);
enableEventHandlers(shape, data);
return shape;
}
/**
* Parses action data and execute the actions.
* @param {JSON} data
* @returns {undefined}
*/
function parseActionData(data) {
if(hasProperty(data,"action") && data.action != undefined &&
hasProperty(data,"data") && data.data != undefined) {
// Parse certain properties depending on type of action
var actionProperties = data.data;
switch (data.action) {
case "windowdisplaytype": // To change the display type
if(hasProperty(actionProperties,"type") && actionProperties.type != undefined) {
// First set the global width and height var's if action contains them
if(hasProperty(actionProperties,"width") && actionProperties.width != undefined)
canvasWindowWidth = actionProperties.width;
if(hasProperty(actionProperties,"height") && actionProperties.height != undefined)
canvasWindowHeight = actionProperties.height;
setWindowDisplayType(actionProperties.type);
}
else {
printDebugMessage("Window Display Type action received without type",2);
}
break;
case "debugger": // To enable or disable the debugger
if(hasProperty(actionProperties,"enabled") && actionProperties.enabled != undefined) {
if(actionProperties.enabled)
initDebug();
else
debugOff();
}
else {
printDebugMessage("Debugger action received without enabled",2);
}
break;
case "requestupload":
if(hasProperty(actionProperties,"multiple") && actionProperties.multiple != undefined) {
if(actionProperties.multiple)
$('#fileUpload').prop('multiple', true);
else
$('#fileUpload').removeProp('multiple');
requestUpload();
}
else {
printDebugMessage("Request upload action received without multiple attribute",2);
}
break;
case "download":
if(hasProperty(actionProperties,"filecontents") && actionProperties.filecontents != undefined
&& hasProperty(actionProperties, "filename") && actionProperties.filename != undefined) {
var blob = new Blob([atob(actionProperties.filecontents)], {type: 'text/other'});
var url = window.URL || window.webkitURL;
var link = document.createElementNS("http://www.w3.org/1999/xhtml", "a");
link.href = url.createObjectURL(blob);
link.download = actionProperties.filename;
var event = document.createEvent("MouseEvents");
event.initEvent("click", true, false);
link.dispatchEvent(event);
} else {
printDebugMessage("Download action recieved without file data/file name",2);
}
break;
case "prompt":
if(hasProperty(actionProperties,"message") && actionProperties.message != undefined) {
var result;
if(hasProperty(actionProperties,"placeholder"))
result = prompt(actionProperties.message,actionProperties.placeholder);
else
result = prompt(actionProperties.message,"");
if(result != undefined)
{
connection.send(JSON.stringify({
"event":"prompt",
"data":{
"value": result
}
}));
}
}
else {
printDebugMessage("Prompt action received without file data",2);
}
break;
case "acceptfiledragndrop":
break;
default:
printDebugMessage("Unknown action type: "+data.action,1);
}
}
else {
printDebugMessage("Error parsing action data",2);
}
}
/**
* Adds event handlers for events the server is interested in.
* @param {JSON} shape The shape on which the event handler is set.
* @param {String} message The message from the server.
* @returns {undefined}
*/
function enableEventHandlers(shape, message) {
if(message.eventData != undefined && message.eventData != null) {
if(message.eventData.listen.indexOf("mouseclick") != -1) {
shape.on('click', clickEventHandler.bind(undefined, message.eventData.eventId));
}
if(message.eventData.listen.indexOf("mousedoubleclick") != -1) {
shape.on('dblclick', doubleClickEventHandler.bind(undefined, message.eventData.eventId));
}
if(message.eventData.listen.indexOf("mousedown") != -1) {
shape.on('mousedown', mouseDownEventHandler.bind(undefined, message.eventData.eventId));
}
if(message.eventData.listen.indexOf("mouseup") != -1) {
shape.on('mouseup', mouseUpEventHandler.bind(undefined, message.eventData.eventId));
}
if(message.eventData.listen.indexOf("mouseover") != -1) {
shape.on('mouseover', mouseOverEventHandler.bind(undefined, message.eventData.eventId));
}
if(message.eventData.listen.indexOf("mouseout") != -1) {
shape.on('mouseout', mouseOutEventHandler.bind(undefined, message.eventData.eventId));
}
if(message.eventData.listen.indexOf("mousedrag") != -1) {
mouseDragFound = mouseDragFound || message.eventData["eventId"]==mouseDragId; // Used to disable mousedrag when no longer this event is requested
shape.on('mousedown', mouseDragStartEventHandler.bind(undefined, message.eventData.eventId));
}
if(message.eventData.listen.indexOf("scroll") != -1) {
// Start listening for scroll events using mousewheel.js
shape["id"] = message.eventData.eventId;
scrollShapes.push(shape);
}
}
}
function clickEventHandler(id, event) { mouseEvent("mouseclick", id, event); }
function doubleClickEventHandler(id, event) { mouseEvent("mousedoubleclick", id, event); }
function mouseDownEventHandler(id, event) { mouseEvent("mousedown", id, event); }
function mouseUpEventHandler(id, event) { mouseEvent("mouseup", id, event); }
function mouseOverEventHandler(id, event) { mouseEvent("mouseover", id, event); }
function mouseOutEventHandler(id, event) {
// Needed, otherwise redraw fires mouseOut event
if(event.targetNode.getParent() != undefined) {
mouseEvent("mouseout", id, event);
}
}
function scrollEventHandler(event) {
// Find out which of the scrollable images is scrolled on, if any.
var shape = null;
for (i = 0; i < scrollShapes.length; i++) {
if(scrollShapes[i].intersects(realX(event.pageX),realY(event.pageY))) {
shape = scrollShapes[i];
break;
}
}
if(shape != null) {
sendScrollEvent(shape.id, event.deltaX, event.deltaY);
}
event.preventDefault();
}
/**
* Starts the mouse drag event handler.
* @param {Number} id The id of the shape the event handler will listen on.
* @param {Event} event
* @returns {undefined}
*/
function mouseDragStartEventHandler(id, event) {
mouseDragEndHandler = mouseDragEndEventHandler.bind(undefined, id);
mouseDragHandler = mouseDragEventHandler.bind(undefined, id);
mouseDragId = id; // Used to disable mousedrag when mousedrag is no longer attached to the shape
// Compensate for the position of the canvas
prevMousePosX = realX(event.pageX);
prevMousePosY = realY(event.pageY);
canvas.on('mouseout', mouseDragEndHandler);
canvas.on('mouseup', mouseDragEndHandler);
canvas.on('mousemove', mouseDragHandler);
// Limit mousemoves from firing by a specified interval
dragEventRateLimiter = true;
dragEventRateLimiter = window.setInterval(function(){
enableDragHandler = true;
}, mouseMoveRateLimit);
// Cancel event bubbling
event.cancelBubble = true;
}
/**
* Handles mouse drag events.
* @param {Number} id The id of the shape the event handler listens on.
* @param {Event} event
* @returns {undefined}
*/
function mouseDragEndEventHandler(id, event) {
canvas.off('mouseout', mouseDragEndHandler);
canvas.off('mouseup', mouseDragEndHandler);
canvas.off('mousemove', mouseDragHandler);
mouseDragId = undefined;
mouseDragEndHandler = undefined;
mouseDragHandler = undefined;
if(dragEventRateLimiter != undefined) {
clearInterval(dragEventRateLimiter);
dragEventRateLimiter = undefined;
}
// Event is undefined if it is called directly
if(event != undefined) {
// Cancel event bubbling
event.cancelBubble = true;
}
}
/**
* Sends information belonging to a mouse drag event.
* @param {Number} id The id of the shape the drag occurs on.
* @param {Event} event
* @returns {undefined}
*/
function mouseDragEventHandler(id, event) {
if (enableDragHandler) {
// Compensate for the position of the canvas
var x1 = prevMousePosX; // Fix me
var x2 = realX(event.pageX);
prevMousePosX = x2;
var y1 = prevMousePosY; // Fix me
var y2 = realY(event.pageY);
prevMousePosY = y2;
printDebugMessage("Event mousedrag on <a data-sid=\"" + id + "\" class=\"debugSelector\">" + id + "</a> from (x1:"+x1+" y1:"+y1+") to (x2:"+x2+" y2:"+y2+")" ,0);
connection.send(JSON.stringify({
"event":"mousedrag",
"data":{
"id": id,
"x1": x1,
"y1": y1,
"x2": x2,
"y2": y2
}
}));
// Wait for the next interval before firing again
enableDragHandler = false;
}
// Cancel event bubbling
event.cancelBubble = true;
}
/**
* Sends information about mouse events.
* @param {String} eventName The name of the event as send to the server.
* @param {Number} id The id of the shape as send to the server.
* @param {Event} event
* @returns {undefined}
*/
function mouseEvent(eventName, id, event) {
// Compensate for the position of the canvas
x = realX(event.pageX);
y = realY(event.pageY);
printDebugMessage("Event "+eventName+" on <a data-sid=\"" + id + "\" class=\"debugSelector\">" + id + "</a> (x:"+x+" y:"+y+")",0);
connection.send(JSON.stringify({
"event":eventName,
"data":{
"id": id,
"x": x,
"y": y
}
}));
// Cancel event bubbling
event.cancelBubble = true;
}
/**
* Sends information about key events to the server.
* @param {String} eventName The name of the key event as send to the server.
* @param {Event} event
* @returns {undefined}
*/
function sendKeyEvent(eventName, event) {
console.log(eventName);
console.log(event);
var key = normalizeKeyCode(event);
var ctrl = event.ctrlKey || event.metaKey;
var alt = event.altKey;
var shift = event.shiftKey;
printDebugMessage("KeyEvent "+key+" "+eventName+" (ctrl:"+ctrl+" alt:"+alt+" shift:"+shift+")",0);
event.preventDefault(); // Prevent browser default behavior to block ctrl-c for example
connection.send(JSON.stringify({
"event":eventName,
"data":{
"key": key,
"control": ctrl,
"alt": alt,
"shift": shift
}
}));
}
function realX(x) {
var canvasPos = $(canvas).position();
return x-canvasPos.left-parseInt($("#canvas").css("margin-left"));
}
function realY(y) {
var canvasPos = $(canvas).position();
return y-canvasPos.top-parseInt($("#canvas").css("margin-top"));
}
/**
* Sends a scroll event to the server.
* @param {Number} deltaX How far is scrolled in horizontal direction.
* @param {Number} deltaY How far is scrolled in vertical direction.
* @returns {undefined}
*/
function sendScrollEvent(id,deltaX,deltaY) {
printDebugMessage("ScrollEvent (id:"+id+" deltaX:"+deltaX+" deltaY:"+deltaY+")",0);
connection.send(JSON.stringify({
"event":"scroll",
"data":{
"id": id,
"xdelta": deltaX,
"ydelta": deltaY
}
}));
}
/**
* Sends a window resize event to the server.
* @param {Number} width How wide the window has become.
* @param {Number} height How high the window has become.
* @returns {undefined}
*/
function sendWindowResizeEvent(width,height) {
printDebugMessage("Window Resize (width:"+width+" height:"+height+")",0);
if(connection.readyState === 1) {
connection.send(JSON.stringify({
"event":"resizewindow",
"data":{
"width": width,
"height": height
}
}));
}
}
/**
* Draws shapes from a JSON message.
* @param {JSON} message The message out of which a shape is constructed.
* @returns {Kinetic.Group|shapeFromData.shape|Kinetic.Circle|Kinetic.Line|Kinetic.Polygon|Kinetic.Text|Kinetic.Rect}
*/
function shapeFromData(message) {
var shape = null;
var debugMessage = "";
var data = message.data;
// both fill and stroke are in the form {"r": 255, "g": 255, "b": 255, ("a": 1.0)?}
if(data["fill"]){
data["fill"] = rgbaDictToColor(data["fill"]);
}
if(data["stroke"]){
data["stroke"] = rgbaDictToColor(data["stroke"]);
}
if (!data["fill"] && !data["stroke"]) {
data["stroke"] = "rgba(0,0,0,1.0)";
}
// Debug message
if(!data["id"] && debugOn) {
data["id"] = "sid" + generadedShapeIdIdx;
generadedShapeIdIdx++;
}
// Hackfix so that kinetic rotates counterclockwise
if(data["rotationDeg"]){
data["rotationDeg"] *= -1;
}
debugMessage = "Drawing <a data-sid=\"" + data["id"] + "\" class=\"debugSelector\">" + message.type + " (" + data["id"] + ")</a> ";
// Init shape based on type
switch (message.type) {
case "line":
if(data.points != undefined && data.points.length != 0) {
shape = new Kinetic.Line(data);
}
debugMessage += "with points: " + data.points;
break;
case "polygon":
shape = new Kinetic.Polygon(data);
break;
case "circle":
shape = new Kinetic.Circle(data);
debugMessage += "width x:"+data.x+" y:"+data.y+" and radius:"+data.radius;
break;
case "rect":
shape = new Kinetic.Rect(data);
debugMessage += "width x:"+data.x+" y:"+data.y+" width:"+data.width+" height:"+data.height;
break;
case "arc":
shape = new CanvasHs.Arc(data);
debugMessage += "x:"+data.x+" y:"+data.y;
break;
case "text":
var fontStyle = new String();
if(data.bold) {
fontStyle = "bold";
delete data.bold;
}
if(data.italic) {
fontStyle += " italic";
delete data.italic;
}
data.fontStyle = fontStyle;
shape = new Kinetic.Text(data);
// As Haskell has no idea about text sizes this code will fix align
// it will make sure that the offset is set at the middle/end of the
// text.
// It does keep align set, that way kinetic knows what to do with
// multiline strings.
var align = data["align"];
if(align != undefined){
var offsetX = shape.getOffsetX();
var width = shape.getWidth();
if(align == 'center'){
offsetX = offsetX + (width / 2);
} else if(align == 'right') {
offsetX = offsetX + width;
}
shape.setOffsetX(offsetX);
}
debugMessage += "width x:"+data.x+" y:"+data.y+" text:"+data.text;
break;
case "container":
data.clip = [0, 0, data.width, data.height];
shape = new Kinetic.Group(data);
message.children.forEach(function(child) {
shapeAdd = parseShapeData(child);
if(shapeAdd != undefined) {
shape.add(shapeAdd);
}
});
break;
default:
debugMessage = null;
printDebugMessage("Unrecognized JSON message received from server.",2);
}
if(debugMessage) {
printDebugMessage(debugMessage,0);
}
return shape;
}
/**
* Converts rgba values to colors.
* @param {Object} dict
* @returns {String}
*/
function rgbaDictToColor(dict){
var res = "";
if(dict["a"] != undefined){
res = "rgba({0},{1},{2},{3})".format(dict["r"], dict["g"], dict["b"], dict["a"]);
} else {
res = "rgb({0},{1},{2})".format(dict["r"], dict["g"], dict["b"]);
}
return res;
}
/*
* Visible debugger
*
* Type 0 = message
* Type 1 = warning
* Type 2 = error
*/
/**
* Prints debug messages to the console if debug is enabled.
* @param {String} message The message that is printed to the console.
* @param {Number} type The severity of the debug message.
* @returns {undefined}
*/
function printDebugMessage(message, type) {
if(type == 1) {
console.warn(message);
}
else if(type == 2) {
console.error(message);
}
else if(debugOn) {
console.log(message);
}
if(debugOn) {
var now = new Date(),
now = now.getHours()+':'+now.getMinutes()+':'+now.getSeconds();
$("#debug").prepend("<p><strong>["+now+"]</strong> "+message+"</p>");
}
}
/**
* Hides or unhides the debug console.
* @returns {undefined}
*/
var hideDebugConsole = function(){
if(debugCanClose) {
if($(this).width() == 20)
{
$(this).animate({
width: 350
},(animated ? 300 : 0));
}
else
{
$(this).animate({
width: 20
},(animated ? 300 : 0));
}
}
};
/**
* Flashes a shape when it is selected in the debugger.
* @returns {undefined}
*/
var debugSelector = function () {
debugCanClose = false;
var sid = $(this).data("sid"); // The shape id in an extended data attribute
var shape = stage.find('#' + sid)[0];
var alreadyAnnimatingIdx = $.inArray(shape, debugAnnimatingShapes);
if(alreadyAnnimatingIdx == -1)
{
debugAnnimatingShapes.push(shape);
var fill = shape.getFill();
shape.setFill("red");
layerList[topLayerIdx].draw();
var interval = setInterval(function(){
shape.setFill(fill);
layerList[topLayerIdx].draw();
clearInterval(interval);
debugAnnimatingShapes.splice( alreadyAnnimatingIdx ,1 );
debugCanClose = true;
},250);
}
};
/**
* Initializes debug capabilities.
* @returns {undefined}
*/
function initDebug() {
// Enable debug
debugOn = true;
// Debug selector for flashing shapes
$("#debug").delegate( ".debugSelector", "click", debugSelector);
// For hiding the debug console
$("#debug").click(hideDebugConsole);
$("#debug").show();
}
/**
* Disables debug capabilities.
* @returns {undefined}
*/
function debugOff() {
debugOn = false;
// Remove debug selector
$("#debug").undelegate( ".debugSelector", "click", debugSelector);
// Remove click event for hiding the console
$("#debug").off('click',hideDebugConsole);
// Clear debug screen and hide
$("#debug").hide().html("");
}
/**
* Initializes the canvas.
* @param {Element} container
* @param {Number} width The width of the canvas.
* @param {Number} height The height of the canvas.
* @returns {undefined}
*/
function initCanvas(container, width, height) {
// Only init canvas when there is a container
// Container is provided for testing purposes and extensibility
if(container.exists()) {
setFixedProportions(container,width,height);
stage = new Kinetic.Stage({
container: container.attr('id'),
width: width, // Default width and height
height: height // Can be changed with the resize canvas function
});
// Create new layer to draw on
newDefaultLayer();
// Assign the canvas global variable for mousedrag
canvas = container.find("canvas");
// Start listening for scroll events using mousewheel.js
canvas.mousewheel(scrollEventHandler);
}
}
/**
* Makes a new layer to draw on.
* @returns {undefined}
*/
function newDefaultLayer() {
topLayerIdx++;
layerList[topLayerIdx] = new Kinetic.Layer();
stage.add(layerList[topLayerIdx]);
}