(function(){ var log = function(){ if(typeof console !== "undefined"){ console.log.apply(console, arguments); } }; var localStorageAvailable = false; try { localStorage._available = true; localStorageAvailable = localStorage._available; delete localStorage._available; // eslint-disable-next-line no-empty } catch (e) {} // @TODO: keep other data in addition to the image data // such as the file_name and other state // (maybe even whether it's considered saved? idk about that) // I could have the image in one storage slot and the state in another var LocalSession = function(session_id){ var lsid = "image#" + session_id; log("local storage id: " + lsid); // save image to storage var save_image_to_storage = function(){ storage.set(lsid, canvas.toDataURL("image/png"), function(err){ if(err){ if(err.quotaExceeded){ storage_quota_exceeded(); }else{ // e.g. localStorage is disabled // (or there's some other error?) // TODO: show warning with "Don't tell me again" type option } } }); } storage.get(lsid, function(err, uri){ if(err){ if (localStorageAvailable) { show_error_message("Failed to retrieve image from local storage:", err); } else { // TODO: DRY with storage manager message show_error_message("Please enable local storage in your browser's settings for local backup. It's may be called Cookies, Storage, or Site Data."); } }else if(uri){ open_from_URI(uri, function(err){ if(err){ return show_error_message("Failed to open image from local storage:", err); } saved = false; // it may be safe, sure, but you haven't "Saved" it }); } else { // no uri so lets save the blank canvas save_image_to_storage(); } }); $canvas.on("change.session-hook", save_image_to_storage); }; LocalSession.prototype.end = function(){ // Remove session-related hooks $app.find("*").off(".session-hook"); $G.off(".session-hook"); }; // The user id is not persistent // A person can enter a session multiple times, // and is always given a new user id var user_id; // @TODO: I could make the color persistent, though. // You could still have multiple cursors and they would just be the same color. // There could also be an option to change your color // The data in this object is stored in the server when you enter a session // It is (supposed to be) removed when you leave var user = { // Cursor status cursor: { // cursor position in canvas coordinates x: 0, y: 0, // whether the user is elsewhere, such as in another tab away: true, }, // Currently selected tool (@TODO) tool: "Pencil", // Color components hue: ~~(Math.random() * 360), saturation: ~~(Math.random() * 50) + 50, lightness: ~~(Math.random() * 40) + 50, }; // The main cursor color user.color = "hsla(" + user.hue + ", " + user.saturation + "%, " + user.lightness + "%, 1)"; // Unused user.color_transparent = "hsla(" + user.hue + ", " + user.saturation + "%, " + user.lightness + "%, 0.5)"; // (@TODO) The color used in the toolbar indicating to other users it is selected by this user user.color_desaturated = "hsla(" + user.hue + ", " + ~~(user.saturation*0.4) + "%, " + user.lightness + "%, 0.8)"; // The image used for other people's cursors var cursor_image = new Image(); cursor_image.src = "images/cursors/default.png"; var FireSession = function(session_id){ var session = this; session.id = session_id; file_name = "[Loading "+session.id+"]"; update_title(); var on_firebase_loaded = function(){ file_name = "["+session.id+"]"; update_title(); session.start(); }; if(!FireSession.fb_root){ $.getScript("lib/firebase.js") .done(function(){ var config = { apiKey: "AIzaSyBgau8Vu9ZE8u_j0rp-Lc044gYTX5O3X9k", authDomain: "jspaint.firebaseapp.com", databaseURL: "https://jspaint.firebaseio.com", projectId: "firebase-jspaint", storageBucket: "", messagingSenderId: "63395010995" }; firebase.initializeApp(config); FireSession.fb_root = firebase.database().ref("/"); on_firebase_loaded(); }) .fail(function(){ show_error_message("Failed to load Firebase; the document will not load, and changes will not be saved."); file_name = "[Failed to load "+session.id+"]"; update_title(); }); }else{ on_firebase_loaded(); } }; FireSession.prototype.start = function(){ var session = this; // TODO: how do you actually detect if it's failing??? var $w = $FormWindow().title("Warning").addClass("dialogue-window"); $w.$main.html( "
The document may not load. Changes may not save.
" + "Multiuser sessions are public. There is no security.
" // "The document may not load. Changes may not save. If it does save, it's public. There is no security.
"// + // "I haven't found a way to detect Firebase quota limits being exceeded, " + // "so for now I'm showing this message regardless of whether it's working.
" + // "If you're interested in using multiuser mode, please thumbs-up " + // "this issue to show interest, and/or subscribe for updates.
" ); $w.$main.css({maxWidth: "500px"}); $w.$Button("OK", function(){ $w.close(); }); $w.center(); // Wrap the Firebase API because they don't // provide a great way to clean up event listeners session._fb_listeners = []; var _fb_on = function(fb, event_type, callback, error_callback){ session._fb_listeners.push({fb, event_type, callback, error_callback}); fb.on(event_type, callback, error_callback); }; // Get Firebase references session.fb = FireSession.fb_root.child(session.id); session.fb_data = session.fb.child("data"); session.fb_users = session.fb.child("users"); if(user_id){ session.fb_user = session.fb_users.child(user_id); }else{ session.fb_user = session.fb_users.push(); user_id = session.fb_user.key; } // Remove the user from the session when they disconnect session.fb_user.onDisconnect().remove(); // Make the user present in the session session.fb_user.set(user); // @TODO: Execute the above two lines when .info/connected // For each existing and new user _fb_on(session.fb_users, "child_added", function(snap){ // Is this you? if(snap.key === user_id){ // You already have a cursor. return; } // Get the Firebase reference for this user var fb_other_user = snap.ref; // Get the user object stored on the server var other_user = snap.val(); // @TODO: display other cursor types? // @TODO: display pointer button state? // @TODO: display selections var cursor_canvas = new Canvas(32, 32); // Make the cursor element var $cursor = $(cursor_canvas).addClass("user-cursor").appendTo($app); $cursor.css({ display: "none", position: "absolute", left: 0, top: 0, opacity: 0, zIndex: 500, // arbitrary; maybe too high pointerEvents: "none", transition: "opacity 0.5s", }); // When the cursor data changes _fb_on(fb_other_user, "value", function(snap){ other_user = snap.val(); // If the user has left if(other_user == null){ // Remove the cursor element $cursor.remove(); }else{ // Draw the cursor var draw_cursor = function(){ cursor_canvas.width = cursor_image.width; cursor_canvas.height = cursor_image.height; var cctx = cursor_canvas.ctx; cctx.fillStyle = other_user.color; cctx.fillRect(0, 0, cursor_canvas.width, cursor_canvas.height); cctx.globalCompositeOperation = "darker"; cctx.drawImage(cursor_image, 0, 0); cctx.globalCompositeOperation = "destination-atop"; cctx.drawImage(cursor_image, 0, 0); }; if(cursor_image.complete){ draw_cursor(); }else{ $(cursor_image).one("load", draw_cursor); } // Update the cursor element var canvas_rect = canvas.getBoundingClientRect(); $cursor.css({ display: "block", position: "absolute", left: canvas_rect.left + magnification * other_user.cursor.x, top: canvas_rect.top + magnification * other_user.cursor.y, opacity: 1 - other_user.cursor.away, }); } }); }); var previous_uri; var pointer_operations = []; var sync = function(){ // Sync the data from this client to the server (one-way) var uri = canvas.toDataURL(); if(previous_uri !== uri){ log("clear pointer operations to set data", pointer_operations); pointer_operations = []; log("set data"); session.fb_data.set(uri); previous_uri = uri; }else{ log("don't set data; it hasn't changed"); } }; $canvas.on("change.session-hook", sync); // Any time we change or recieve the image data _fb_on(session.fb_data, "value", function(snap){ log("data update"); var uri = snap.val(); if(uri == null){ // If there's no value at the data location, this is a new session // Sync the current data to it sync(); }else{ previous_uri = uri; saved = true; // hopefully // Load the new image data var img = new Image(); img.onload = function(){ // Cancel any in-progress pointer operations if(pointer_operations.length){ $G.triggerHandler("pointerup", "cancel"); } // Write the image data to the canvas ctx.copy(img); $canvas_area.trigger("resize"); // (detect_transparency() here would not be ideal // Perhaps a better way of syncing transparency // and other options will be established) // Playback recorded in-progress pointer operations window.console && console.log("playback", pointer_operations); for(var i=0; i