/* Spacedeck Whiteboard Directive This module registers a custom Vue directive that handles Whiteboard sections. */ function setup_whiteboard_directives() { if ('ontouchstart' in window) { var edown = "touchstart"; var emove = "touchmove"; var eup = "touchend"; } else { var edown = "mousedown"; var emove = "mousemove"; var eup = "mouseup"; } Vue.directive('sd-whiteboard', { bind: function () { var el = this.el; $(el).on(edown, ".artifact", this.handle_mouse_down_artifact.bind(this)); $(el).on("dblclick", ".artifact", this.handle_double_click_artifact.bind(this)); $(el).on("keyup", ".artifact", this.handle_key_up_artifact.bind(this)); $(el).on("keydown", ".artifact", this.handle_key_down_artifact.bind(this)); $(el).bind(edown, this.handle_mouse_down_space.bind(this)); $(el).bind(emove, this.handle_mouse_move.bind(this)); $(el).bind(eup, this.handle_mouse_up_space.bind(this)); $(el).bind("wheel", this.handle_wheel_space.bind(this)); $(document.body).bind("mouseleave", this.handle_mouse_leave.bind(this)); $(el).find(".handle.resize-nw").bind(edown, function(e){this.handle_transform_mouse_down(e,1,1)}.bind(this)); $(el).find(".handle.resize-n").bind(edown, function(e){this.handle_transform_mouse_down(e,0.5,1)}.bind(this)); $(el).find(".handle.resize-ne").bind(edown, function(e){this.handle_transform_mouse_down(e,0,1)}.bind(this)); $(el).find(".handle.resize-e").bind(edown, function(e){this.handle_transform_mouse_down(e,0,0.5)}.bind(this)); $(el).find(".handle.resize-se").bind(edown, function(e){this.handle_transform_mouse_down(e,0,0)}.bind(this)); $(el).find(".handle.resize-s").bind(edown, function(e){this.handle_transform_mouse_down(e,0.5,0)}.bind(this)); $(el).find(".handle.resize-sw").bind(edown, function(e){this.handle_transform_mouse_down(e,1,0)}.bind(this)); $(el).find(".handle.resize-w").bind(edown, function(e){this.handle_transform_mouse_down(e,1,0.5)}.bind(this)); $(el).find(".edge-handle.resize-n").bind(edown, function(e){this.handle_transform_mouse_down(e,0.5,1)}.bind(this)); $(el).find(".edge-handle.resize-s").bind(edown, function(e){this.handle_transform_mouse_down(e,0.5,0)}.bind(this)); $(el).find(".edge-handle.resize-e").bind(edown, function(e){this.handle_transform_mouse_down(e,0,0.5)}.bind(this)); $(el).find(".edge-handle.resize-w").bind(edown, function(e){this.handle_transform_mouse_down(e,1,0.5)}.bind(this)); $(el).on(edown, ".vector-handle", function(e){this.handle_vector_transform_mouse_down(e)}.bind(this)); var $scope = this.vm.$root; this.space_zoom = 1; this.artifacts_before_transaction = []; $scope.active_tool = "pointer"; }, update: function () { }, unbind: function () { // do clean up work // e.g. remove event listeners added in bind() var el = this.el; $(el).off(edown+" "+emove+" "+eup+" "+"keyup keydown mouseleave"); $(document.body).unbind("mouseleave"); }, handle_key_down_artifact: function(evt) { var $scope = this.vm.$root; }, handle_key_up_artifact: function(evt) { var $scope = this.vm.$root; }, handle_mouse_down_artifact: function(evt) { var $scope = this.vm.$root; if (!$scope.editing_artifact_id) { evt.preventDefault(); evt.stopPropagation(); } if ($scope.active_tool == "zoom") return; if (evt.which == 2) { // middle mouse button this.handle_mouse_down_space(evt); return; } var a = $scope.find_artifact_by_id(evt.currentTarget.id.replace("artifact-","")); if ($scope.active_tool == "eyedrop") { var arts = $scope.selected_artifacts(); if (!$scope.is_selected(a) && arts.length > 0) { // copy style from clicked artifact to selected artifacts $scope.begin_transaction(); $scope.update_selected_artifacts(function(selected_artifact) { selected_artifact.style = _.clone(a.style); }); $scope.active_tool = "pointer"; return; } } if ($scope.active_tool == "pan") { this.start_pan(evt); return; } if ($scope.active_tool == "pointer") { if (!$scope.is_selected(a) || evt.shiftKey) { this.select(evt,a); } // copy via alt+move if (evt.altKey) { a = $scope.clone_artifact(a); this.select(evt,a); } } $scope.begin_transaction(); var cursor = this.cursor_point_to_space(evt); $scope.mouse_ox = cursor.x; $scope.mouse_oy = cursor.y; $scope.mouse_moved = false; this.mouse_state = "move"; evt.stopPropagation(); }, handle_double_click_artifact: function(evt) { var $scope = this.vm.$root; var a = $scope.find_artifact_by_id(evt.currentTarget.id.replace("artifact-","")); if (!a) return; if (a.payload_uri) { $scope.download_selected_artifacts(); } $scope.toggle_selected_artifact_editing(true); }, handle_transform_mouse_down: function(evt,origin_x,origin_y) { evt.stopPropagation(); evt.preventDefault(); var $scope = this.vm.$root; $scope.begin_transaction(); var cursor = this.cursor_point_to_space(evt); this.mouse_state = "transform"; $scope.mouse_ox = cursor.x; $scope.mouse_oy = cursor.y; $scope.transform_ox = origin_x; $scope.transform_oy = origin_y; }, handle_vector_transform_mouse_down: function(evt) { evt.stopPropagation(); evt.preventDefault(); var $scope = this.vm.$root; var idx = parseInt($(evt.currentTarget).attr("data-idx")); $scope.selected_control_point_idx = idx; $scope.begin_transaction(); var cursor = this.cursor_point_to_space(evt); this.mouse_state = "vector_transform"; $scope.mouse_ox = cursor.x; $scope.mouse_oy = cursor.y; //$scope.transform_ox = origin_x; //$scope.transform_oy = origin_y; }, handle_wheel_space: function(evt) { var $scope = this.vm.$root; if (!evt.ctrlKey && !evt.shiftKey) return; evt.preventDefault(); evt.stopPropagation(); var amount = 1; var dy = evt.originalEvent.deltaY; if (dy>0) { amount=1.2; if ($scope.viewport_zoom<=0.05) return false; } else if (dy<0) { amount=0.9; if ($scope.viewport_zoom>=2) return false; } else { return false; } $scope.zoom_to_cursor(evt,amount); }, handle_mouse_down_space: function(evt) { if (evt.which != 2) { if (evt.target != evt.currentTarget && !_.include(["wrapper"],evt.target.className)) return; } var $scope = this.vm.$root; $scope.opened_dialog="none"; var cursor = this.cursor_point_to_space(evt); $scope.mouse_ox = cursor.x; $scope.mouse_oy = cursor.y; if (evt.which == 2 || evt.buttons == 4) { $scope.active_tool = "pan"; } if ($scope.active_tool=="note") { this.deselect(); this.mouse_state = "transform"; $scope.mouse_state = this.mouse_state; this.start_drawing_note(evt); return; } else if ($scope.active_tool=="arrow") { this.deselect(); this.mouse_state = "vector_transform"; $scope.mouse_state = this.mouse_state; this.start_drawing_arrow(evt); return; } else if ($scope.active_tool=="line") { this.deselect(); this.mouse_state = "vector_transform"; $scope.mouse_state = this.mouse_state; this.start_drawing_line(evt); return; } else if ($scope.active_tool=="scribble") { this.deselect(); this.mouse_state = "scribble"; $scope.mouse_state = this.mouse_state; this.start_drawing_scribble(evt); return; } else if ($scope.active_tool=="zoom") { if (evt.altKey) { $scope.zoom_out(); } else { $scope.zoom_in(); } return; } else if ($scope.active_tool=="pointer") { this.mouse_state = "lasso"; this.start_lasso(evt); } else if ($scope.active_tool=="zone") { this.deselect(); this.mouse_state = "transform"; $scope.start_adding_zone(evt); return; } else if ($scope.active_tool=="image") { this.deselect(); this.mouse_state = "transform"; $scope.start_adding_placeholder(evt); return; } else if ($scope.active_tool=="pan") { this.start_pan(evt); return; } if ($scope.selection_metrics.count>0) { this._no_artifact_toolbar_this_round = true; } this.deselect(); }, start_pan: function(evt) { var $scope = this.vm.$root; el = $("#space")[0]; if (el) { this.mouse_state = "pan"; this.old_panx = el.scrollLeft; this.old_pany = el.scrollTop; } var cursor = this.cursor_point_to_space(evt); $scope.mouse_ox = cursor.x; $scope.mouse_oy = cursor.y; $scope.mouse_moved = false; }, deselect: function() { var $scope = this.vm.$root; $scope.deselect(); }, select: function(evt, a) { var $scope = this.vm.$root; $scope.select(evt, a); }, multi_select: function(arts) { var $scope = this.vm.$root; $scope.multi_select(arts); }, start_lasso: function(evt) { var point = this.cursor_point_to_space(evt); this.lasso = { x: point.x, y: point.y, w: 0, h: 0 } }, rects_intersecting: function(r1,r2) { if (!r1 || !r2) return false; if ( (r1.x+r1.w < r2.x) || (r1.x > r2.x+r2.w) || (r1.y+r1.h < r2.y) || (r1.y > r2.y+r2.h) ) return false; return true; }, artifacts_in_rect: function(rect) { if (!rect) return []; var $scope = this.vm.$root; return _.filter($scope.active_space_artifacts, function(a) { return this.rects_intersecting(a, rect); }.bind(this)); }, abs_rect: function(rect) { var res = { x: rect.x, y: rect.y, w: Math.abs(rect.w), h: Math.abs(rect.h) } if (rect.w<0) res.x+=rect.w; if (rect.h<0) res.y+=rect.h; return res; }, lasso_style: function() { var $scope = this.vm.$root; if (!this.lasso) return ""; var lasso_scaled = { x:this.lasso.x, y:this.lasso.y, w:this.lasso.w*$scope.viewport_zoom, h:this.lasso.h*$scope.viewport_zoom } lasso_scaled = this.abs_rect(lasso_scaled); lasso_scaled.x += $scope.bounds_margin_horiz; lasso_scaled.y += $scope.bounds_margin_vert; var s = "left:" +lasso_scaled.x+"px;"; s += "top:" +lasso_scaled.y+"px;"; s += "width:" +lasso_scaled.w+"px;"; s += "height:"+lasso_scaled.h+"px;"; s += "opacity: 1;"; return s; }, render_lasso: function() { if (!this.lasso) { $("#lasso").hide(); return; } $("#lasso").attr("style", this.lasso_style()); $("#lasso").show(); }, cursor_point_to_space: function(evt) { var $scope = this.vm.$root; var offset = {left: 0, top: 0}; evt = fixup_touches(evt); return { x: (parseInt(evt.pageX) - parseInt(offset.left) - $scope.bounds_margin_horiz) / this.space_zoom, y: (parseInt(evt.pageY) - parseInt(offset.top) - $scope.bounds_margin_vert) / this.space_zoom }; }, rect_to_points: function(rect) { return [ {x:rect.x,y:rect.y}, {x:rect.x+rect.w,y:rect.y}, {x:rect.x,y:rect.y+rect.h}, {x:rect.x+rect.w,y:rect.y+rect.h} ]; }, old_selection_rect: function() { var $scope = this.vm.$root; var selected = $scope.selected_artifacts().map(function(a){ return $scope.find_artifact_before_transaction(a); }.bind(this)); return $scope.enclosing_rect(selected); }, snap_point: function(x,y,snap_middle) { var $scope = this.vm.$root; var TOL = 8; var dists = []; if (snap_middle) { dists.push([[x-window.innerWidth/2,Math.abs(y-window.innerHeight/2)],[x-window.innerWidth/2,Math.abs(y-window.innerHeight/2)]]); } if ($scope.grid_active) { // snap to grid var gw = $scope.grid.spacing/$scope.grid.subdivisions; var gh = $scope.grid.spacing/$scope.grid.subdivisions; var sx1 = parseInt(x/gw)*gw; var sy1 = parseInt(y/gh)*gh; var sx2 = (parseInt(x/gw)+1)*gw; var sy2 = (parseInt(y/gh)+1)*gh; dists = [[[Math.abs(sx1-x),sx1], [Math.abs(sy1-y),sy1]], [[Math.abs(sx2-x),sx2], [Math.abs(sy2-y),sy2]]]; } else { // snap to other artifacts dists = $scope.unselected_artifacts().map(function(a){ var r = this.rect_to_points(a); var xd1 = Math.abs(r[0].x-x); var xd2 = Math.abs(r[1].x-x); var xd3 = Math.abs(r[0].x+a.w/2 - x); var yd1 = Math.abs(r[0].y-y); var yd2 = Math.abs(r[2].y-y); var yd3 = Math.abs(r[0].y+a.h/2 - y); if (!snap_middle) { if (xd2TOL) { results.snapx = [0,x]; // distance, coordinate } else { // FIXME snap rulers are broken //$scope.snap_ruler_x = xdists[0][1]; } if (!ydists[0] || ydists[0][0]>TOL) { results.snapy = [0,y]; } else { //$scope.snap_ruler_y = ydists[0][1]; } return results; }, offset_point_in_wrapper: function(point) { var $scope = this.vm.$root; var section_el = $(this.el)[0]; var z = $scope.viewport_zoom; var pt = parseInt($("#space").css("padding-top")); point.y=(point.y+section_el.scrollTop-pt)/z; point.x=(point.x+section_el.scrollLeft)/z; return point; }, start_drawing_note: function(evt) { evt.preventDefault(); evt.stopPropagation(); var $scope = this.vm.$root; var point = this.cursor_point_to_space(evt); this.offset_point_in_wrapper(point); var z = $scope.highest_z()+1; var a = { space_id: $scope.active_space._id, mime: "text/html", description: "

Text

", x: point.x, y: point.y, z: z, w: 64, h: 64, align: "center", valign: "middle", stroke_color: "#000000", fill_color: "rgb(241, 196, 15)", stroke: 0 }; $scope.save_artifact(a, function(saved_a) { $scope.update_board_artifact_viewmodel(saved_a); $scope.active_space_artifacts.push(saved_a); $scope.select(evt,a); $scope.transform_ox = 0; $scope.transform_oy = 0; $scope.begin_transaction(); }.bind(this)); }, start_drawing_scribble: function(evt) { evt.preventDefault(); evt.stopPropagation(); var $scope = this.vm.$root; var point = this.offset_point_in_wrapper(this.cursor_point_to_space(evt)); var z = $scope.highest_z()+1; $scope.deselect(); var a = { space_id: $scope.active_space._id, mime: "x-spacedeck/vector", description: "", control_points: [{dx:0,dy:0}], x: point.x, y: point.y, z: z, w: 64, h: 64, stroke_color: "#000000", stroke: 2, shape: "scribble" }; $scope.save_artifact(a, function(saved_a) { $scope.update_board_artifact_viewmodel(saved_a); $scope.active_space_artifacts.push(saved_a); this.select(evt,saved_a); //$scope.tool_artifact = a; $scope.transform_ox = 0; $scope.transform_oy = 0; $scope.begin_transaction(); }.bind(this)); }, start_drawing_arrow: function(evt) { evt.preventDefault(); evt.stopPropagation(); var $scope = this.vm.$root; var point = this.cursor_point_to_space(evt); this.offset_point_in_wrapper(point); var z = $scope.highest_z()+1; var a = { space_id: $scope.active_space._id, mime: "x-spacedeck/vector", description: "", control_points: [{dx:0,dy:0},{dx:0,dy:0},{dx:0,dy:0}], x: point.x, y: point.y, z: z, w: 64, h: 64, stroke_color: "#000000", stroke: 2, shape: "arrow" }; $scope.save_artifact(a, function(saved_a) { $scope.update_board_artifact_viewmodel(saved_a); $scope.active_space_artifacts.push(saved_a); $scope.select(evt,a); $scope.selected_control_point_idx = 1; $scope.transform_ox = 0; $scope.transform_oy = 0; $scope.begin_transaction(); }.bind(this)); }, // FIXME: consolidate with arrow drawing? start_drawing_line: function(evt) { evt.preventDefault(); evt.stopPropagation(); var $scope = this.vm.$root; var point = this.cursor_point_to_space(evt); this.offset_point_in_wrapper(point); var z = $scope.highest_z()+1; var a = { space_id: $scope.active_space._id, mime: "x-spacedeck/vector", description: "", control_points: [{dx:0,dy:0},{dx:0,dy:0}], x: point.x, y: point.y, z: z, w: 64, h: 64, stroke_color: "#000000", stroke: 2, shape: "line" }; $scope.save_artifact(a, function(saved_a) { $scope.update_board_artifact_viewmodel(saved_a); $scope.active_space_artifacts.push(saved_a); $scope.select(evt,a); $scope.selected_control_point_idx = 1; $scope.transform_ox = 0; $scope.transform_oy = 0; $scope.begin_transaction(); }.bind(this)); }, snap_point_simple: function(point) { var snapped = this.snap_point(point.x, point.y); return { x: snapped.snapx[1], y: snapped.snapy[1] } }, handle_mouse_up_space: function(evt) { var $scope = this.vm.$root; evt.preventDefault(); if (this.mouse_state == "lasso") { var lasso_rect = this.abs_rect(this.offset_point_in_wrapper(this.lasso)); // convert to space coordinates if (lasso_rect.w>0 && lasso_rect.h>0) { var arts = this.artifacts_in_rect(lasso_rect); this.multi_select(arts); } else { if (this._no_artifact_toolbar_this_round) { this._no_artifact_toolbar_this_round = false; } else { $scope.start_adding_artifact(evt); } } this.lasso = null; this.render_lasso(); } else if (_.include(["transform","move","vector_transform","scribble"],this.mouse_state)) { var ars = $scope.selected_artifacts(); for (var i=0; i10 || dy>10 || dt>100) { var name = "anonymous"; if ($scope.logged_in) { name = $scope.user.nickname || $scope.user.email; } else { name = $scope.guest_nickname || "anonymous"; } var cursor_msg = { action: "cursor", x: cursor.x/zoom, y: cursor.y/zoom, name: name, id: $scope.user._id||name }; $scope.websocket_send(cursor_msg); } // side effects ftw! $scope.snap_ruler_x = -1000; $scope.snap_ruler_y = -1000; $scope.mouse_moved = true; $scope.transform_lock = evt.shiftKey; if ($scope.transform_lock) { if (this.mouse_state == "transform") { // lock aspect is done in transform } else { // lock axis if (Math.abs(dy)>Math.abs(dx)) { dx = 0; } else { dy = 0; } } } if (this.mouse_state == "move") { $scope.hide_toolbar_props(); var snap_dx = 0; var snap_dy = 0; var selected = $scope.selected_artifacts(); var snap_edges = this.old_selection_rect(); if (selected.length && selected[0]._id==$scope.editing_artifact_id) { // bail out of moving editable artifact return; } if (snap_edges) { var mx = snap_edges.x1 + (snap_edges.x2-snap_edges.x1)/2; var my = snap_edges.y1 + (snap_edges.y2-snap_edges.y1)/2; var snapped1 = this.snap_point(snap_edges.x1 + dx, snap_edges.y1 + dy, false); var snapped2 = this.snap_point(snap_edges.x2 + dx, snap_edges.y2 + dy, false); var snapped3 = this.snap_point(mx + dx, my + dy, true); if (snapped3.snapx[0]>0) { snap_dx = mx + dx - snapped3.snapx[1]; } else if (snapped2.snapx[0]>0) { snap_dx = snap_edges.x2 + dx - snapped2.snapx[1]; } else { snap_dx = snap_edges.x1 + dx - snapped1.snapx[1]; } if (snapped3.snapy[0]>0) { snap_dy = my + dy - snapped3.snapy[1]; } else if (snapped2.snapy[0]>0) { snap_dy = snap_edges.y2 + dy - snapped2.snapy[1]; } else { snap_dy = snap_edges.y1 + dy - snapped1.snapy[1]; } } $scope.update_selected_artifacts(function(a) { var old_a = $scope.find_artifact_before_transaction(a); if (old_a) { return { x: old_a.x + dx - snap_dx, y: old_a.y + dy - snap_dy }; } else { // deleted? return {}; } }.bind(this)); } else if (this.mouse_state == "transform") { var selected = $scope.selected_artifacts(); var edges = this.old_selection_rect(); if (!edges) { this.mouse_state = "idle"; return; } $scope.hide_toolbar_props(); var ew = (edges.x2-edges.x1); var eh = (edges.y2-edges.y1); var origin_x = edges.x1 + ew * $scope.transform_ox; var origin_y = edges.y1 + eh * $scope.transform_oy; // "leading point" var lead_x = edges.x1 + ew * (1-$scope.transform_ox) - origin_x; var lead_y = edges.y1 + eh * (1-$scope.transform_oy) - origin_y; var lead_snapped = this.snap_point(origin_x + lead_x + dx, origin_y + lead_y + dy); var moved_x = (lead_snapped.snapx[1] - origin_x); var moved_y = (lead_snapped.snapy[1] - origin_y); var scale_x = lead_x ? (moved_x)/lead_x : 1; var scale_y = lead_y ? (moved_y)/lead_y : 1; if ($scope.transform_lock) scale_y = scale_x; $scope.update_selected_artifacts(function(a) { var old_a = $scope.find_artifact_before_transaction(a); var x1 = origin_x + ((old_a.x - origin_x) * scale_x); var y1 = origin_y + ((old_a.y - origin_y) * scale_y); var x2 = origin_x + (((old_a.x + old_a.w) - origin_x) * scale_x); var y2 = origin_y + (((old_a.y + old_a.h) - origin_y) * scale_y); if (x1>x2) { var t = x1; x1 = x2; x2 = t; } if (y1>y2) { var t = y1; y1 = y2; y2 = t; } return { x: x1, y: y1, w: x2 - x1, h: y2 - y1 }; }.bind(this)); } else if (this.mouse_state == "lasso") { this.lasso.w = dx; this.lasso.h = dy; this.render_lasso(); } else if (this.mouse_state == "vector_transform") { $scope.hide_toolbar_props(); var _this = this; $scope.update_selected_artifacts(function(a) { var old_a = $scope.find_artifact_before_transaction(a); var control_points = _.cloneDeep(old_a.control_points); var cp = control_points[$scope.selected_control_point_idx]; var snapped = _this.snap_point(old_a.x+cp.dx+dx, old_a.y+cp.dy+dy); dx = snapped.snapx[1]-(old_a.x+cp.dx); dy = snapped.snapy[1]-(old_a.y+cp.dy); cp.dx += dx; cp.dy += dy; // special case for arrow's 3rd point if (a.shape == "arrow" && $scope.selected_control_point_idx!=2) { /*control_points[2].dx += dx/2; control_points[2].dy += dy/2; */ control_points[2].dx = (control_points[0].dx+control_points[1].dx)/2; control_points[2].dy = (control_points[0].dy+control_points[1].dy)/2; } return _this.normalize_control_points(control_points, old_a); }); } else if (this.mouse_state == "scribble") { $scope.update_selected_artifacts(function(a) { var old_a = a; var control_points = _.cloneDeep(old_a.control_points); var offset = this.offset_point_in_wrapper({x:cursor.x,y:cursor.y}); control_points.push({ dx: offset.x-old_a.x, dy: offset.y-old_a.y }); return this.normalize_control_points(simplify_scribble_points(control_points), old_a); }.bind(this)); var arts = $scope.selected_artifacts(); if (arts.length) { $scope.update_board_artifact_viewmodel(arts[0]); } } else if (this.mouse_state == "pan") { if (!$("#space").length) return; el = $("#space")[0]; el.scrollLeft = this.old_panx - dx*$scope.viewport_zoom; el.scrollTop = this.old_pany - dy*$scope.viewport_zoom; $scope.handle_scroll(); } }, normalize_control_points: function(control_points, artifact) { var x1 = _.min(control_points,"dx").dx; var y1 = _.min(control_points,"dy").dy; var x2 = _.max(control_points,"dx").dx; var y2 = _.max(control_points,"dy").dy; var shiftx = -x1; var shifty = -y1; var shifted_cps = control_points.map(function(cp) { return { dx: cp.dx + shiftx, dy: cp.dy + shifty }; }); var w = Math.abs(x2 - x1); var h = Math.abs(y2 - y1); var bshiftx = 0; var bshifty = 0; if (artifact.w < 0) bshiftx = -artifact.w; if (artifact.h < 0) bshifty = -artifact.h; return { x: artifact.x + bshiftx - shiftx, y: artifact.y + bshifty - shifty, w: w, h: h, z: artifact.z, control_points: shifted_cps }; } }); }