UNPKG

opentok-annotation

Version:
1,428 lines (1,205 loc) 91.4 kB
/* global OT OTSolution OTKAnalytics ScreenSharingAccPack define */ (function () { /** Include external dependencies */ var _; var $; var OTKAnalytics; if (typeof module === 'object' && typeof module.exports === 'object') { /* eslint-disable import/no-unresolved */ _ = require('underscore'); $ = require('jquery'); OTKAnalytics = require('opentok-solutions-logging'); /* eslint-enable import/no-unresolved */ } else { _ = this._; $ = this.$; OTKAnalytics = this.OTKAnalytics; } /** Private variables */ var _this; var _accPack; var _session; var _canvas; var _subscribingToMobileScreen; var _elements = {}; /** Analytics */ var _otkanalytics; // vars for the analytics logs. Internal use var _logEventData = { clientVersion: 'js-vsol-2.0.59', // x.y.z filled by npm build script componentId: 'annotationsAccPack', name: 'guidAnnotationsKit', actionInitialize: 'Init', actionStart: 'Start', actionEnd: 'End', actionFreeHand: 'FreeHand', actionPickerColor: 'PickerColor', actionText: 'Text', actionScreenCapture: 'ScreenCapture', actionErase: 'Erase', actionUseToolbar: 'UseToolbar', variationAttempt: 'Attempt', variationError: 'Failure', variationSuccess: 'Success', }; var _logAnalytics = function () { // init the analytics logs var _source = window.location.href; var otkanalyticsData = { clientVersion: _logEventData.clientVersion, source: _source, componentId: _logEventData.componentId, name: _logEventData.name }; _otkanalytics = new OTKAnalytics(otkanalyticsData); var sessionInfo = { sessionId: _session.id, connectionId: _session.connection.connectionId, partnerId: _session.apiKey }; _otkanalytics.addSessionInfo(sessionInfo); }; var _log = function (action, variation) { var data = { action: action, variation: variation }; _otkanalytics.logEvent(data); }; /** End Analytics */ // Check for DOM element or string. Return element. var _getElem = function (el) { return typeof el === 'string' ? document.querySelector(el) : el; }; // Trigger event via common layer API var _triggerEvent = function (event, data) { if (_accPack) { _accPack.triggerEvent(event, data); } }; var _registerEvents = function () { if (_accPack) { var events = [ 'startAnnotation', 'linkAnnotation', 'resizeCanvas', 'annotationWindowClosed', 'endAnnotation' ]; _accPack.registerEvents(events); } }; var _setupUI = function () { var toolbar = [ '<div id="annotationToolbarContainer" class="ots-annotation-toolbar-container">', '<div id="toolbar"></div>', '</div>' ].join('\n'); $('body').append(toolbar); _log(_logEventData.actionUseToolbar, _logEventData.variationSuccess); }; var _palette = [ '#1abc9c', '#2ecc71', '#3498db', '#9b59b6', '#8e44ad', '#f1c40f', '#e67e22', '#e74c3c', '#ded5d5' ]; var _aspectRatio = (10 / 6); /** Private methods */ var _refreshCanvas = _.throttle(function () { _canvas.onResize(); }, 1000); /** Resize the canvas to match the size of its container */ var _resizeCanvas = _.throttle(function () { var width; var height; var cobrowsing = !!_elements.cobrowsingImage; if (cobrowsing) { // Cobrowsing images are currently fixed size, so resize isn't needed return; } if (_elements.cobrowsingImage === null) { var el = _elements.absoluteParent || _elements.canvasContainer; width = el.clientWidth; height = el.clientHeight; } try { var videoDimensions = _canvas.videoFeed.stream.videoDimensions; } catch (e) { console.log('OT Annotation: Annotation video stream no longer exists'); return; } // Override dimensions when subscribing to a mobile screen if (_subscribingToMobileScreen) { videoDimensions.width = width; videoDimensions.height = height; } var origRatio = videoDimensions.width / videoDimensions.height; var destRatio = width / height; var calcDimensions = { top: 0, left: 0, height: height, width: width }; if (!_elements.externalWindow) { if (origRatio < destRatio) { // height is the limiting prop, we'll get vertical bars calcDimensions.width = calcDimensions.height * origRatio; calcDimensions.left = (width - calcDimensions.width) / 2; } else { calcDimensions.height = calcDimensions.width / origRatio; calcDimensions.top = (height - calcDimensions.height) / 2; } } $(_elements.canvasContainer).find('canvas').css(calcDimensions); $(_elements.canvasContainer).find('canvas').attr(calcDimensions); _refreshCanvas(); _triggerEvent('resizeCanvas'); }, 500, {trailing: false}); var _changeColorByIndex = function (colorIndex) { _canvas.changeColorByIndex(colorIndex); }; var _takeScreenShot = function () { _canvas.captureScreenshot(true); }; var _listenForResize = function () { $(_elements.resizeSubject).on('resize', _resizeCanvas); }; var _createToolbar = function (session, options, externalWindow) { _setupUI(); var toolbarId = _.property('toolbarId')(options) || 'toolbar'; var items = _.property('toolbarItems')(options) || []; var shapes = _.property('toolbarShapes')(options) || []; var colors = _.property('colors')(options) || _palette; var imageAssets = _.property('imageAssets')(options) || null; var backgroundColor = _.property('backgroundColor')(options) || null; var container = function () { var w = !!externalWindow ? externalWindow : window; return w.document.getElementById(toolbarId); }; /* eslint-disable no-native-reassign */ toolbar = new OTSolution.Annotations.Toolbar({ session: session, container: container(), colors: colors, items: items.length ? items : ['*'], shapes: shapes.length ? shapes : ['rectangle', 'oval', 'star', 'arrow', 'line'], externalWindow: externalWindow || null, imageAssets: imageAssets, backgroundColor: backgroundColor, OTKAnalytics: OTKAnalytics }); toolbar.itemClicked(function (id) { var actions = { OT_pen: _logEventData.actionFreeHand, OT_colors: _logEventData.actionPickerColor, OT_text: _logEventData.actionText, OT_clear: _logEventData.actionErase }; var action = actions[id]; if (!!action) { _log(action, _logEventData.variationSuccess); } }); /* eslint-enable no-native-reassign */ }; // Create external screen sharing window var _createExternalWindow = function () { var deferred = $.Deferred(); var width = screen.width * 0.80 | 0; var height = width / (_aspectRatio); var externalWindowHTML = '<!DOCTYPE html><html lang="en"><head><meta http-equiv="Content-type" content="text/html; charset=utf-8"><title>OpenTok Screen Sharing Solution Annotation</title><link rel="stylesheet" href="https://assets.tokbox.com/solutions/css/style.css"><style type="text/css" media="screen"> body{margin:0;background-color:rgba(0,153,203,.7);box-sizing:border-box;height:100vh}canvas{top:0;z-index:1000}.hidden{display:none}.ots-hidden{display:none !important}.main-wrap{width:100%;height:100%;-ms-box-orient:horizontal;display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-moz-flex;display:-webkit-flex;display:flex;-webkit-justify-content:center;justify-content:center;-webkit-align-items:center;align-items:center}.inner-wrap{position:relative;border-radius:8px;overflow:hidden}.publisherContainer{display:block;background-color:#000;position:absolute}.publisher-wrap{height:100%;width:100%}.subscriberContainer{position:absolute;display:flex;top:20px;left:20px;width:200px;height:120px;background-color:#000;border:2px solid #fff;border-radius:6px}.subscriberContainer .OT_video-poster{width:100%;height:100%;opacity:.25;background-repeat:no-repeat;background-image:url(https://static.opentok.com/webrtc/v2.8.2/images/rtc/audioonly-silhouette.svg);background-size:50%;background-position:center}.OT_video-element{height:100%;width:100%}.OT_edge-bar-item{display:none}</style></head><body> <div class="main-wrap"> <div id="annotationContainer" class="inner-wrap"></div></div><div id="toolbarContainer" class="ots-annotation-toolbar-container"> <div id="toolbar" class="toolbar-wrap"></div></div><div id="subscriberVideo" class="subscriberContainer hidden"></div><script type="text/javascript" charset="utf-8"> /** Must use double-quotes since everything must be converted to a string */ var opener; var canvas; if (!toolbar){alert("Something went wrong: You must pass an OpenTok annotation toolbar object into the window.")}else{opener=window.opener; window.onbeforeunload=window.triggerCloseEvent;}var localScreenProperties={insertMode: "append", width: "100%", height: "100%", videoSource: "window", showControls: false, style:{buttonDisplayMode: "off"}, subscribeToVideo: "true", subscribeToAudio: "false", fitMode: "contain"}; var createContainerElements=function(){var parentDiv=document.getElementById("annotationContainer"); var publisherContainer=document.createElement("div"); publisherContainer.setAttribute("id", "screenshare_publisher"); publisherContainer.classList.add("publisher-wrap"); parentDiv.appendChild(publisherContainer); return{annotation: parentDiv, publisher: publisherContainer};}; var addSubscriberVideo=function(stream){var container=document.getElementById("subscriberVideo"); var subscriber=session.subscribe(stream, container, localScreenProperties, function(error){if (error){console.log("Failed to add subscriber video", error);}container.classList.remove("hidden");});}; if (navigator.userAgent.indexOf("Firefox") !==-1){var ghost=window.open("about:blank"); ghost.focus(); ghost.close();}</script></body></html>'; /* eslint-disable max-len */ var windowFeatures = [ 'toolbar=no', 'location=no', 'directories=no', 'status=no', 'menubar=no', 'scrollbars=no', 'resizable=no', 'copyhistory=no', ['width=', width].join(''), ['height=', height].join(''), ['left=', ((screen.width / 2) - (width / 2))].join(''), ['top=', ((screen.height / 2) - (height / 2))].join('') ].join(','); /* eslint-enable max-len */ var annotationWindow = window.open('about:blank', '', windowFeatures); annotationWindow.document.write(externalWindowHTML); window.onbeforeunload = function () { annotationWindow.close(); }; // External window needs access to certain globals annotationWindow.toolbar = toolbar; annotationWindow.OT = OT; annotationWindow.session = _session; annotationWindow.$ = $; annotationWindow.triggerCloseEvent = function () { _triggerEvent('annotationWindowClosed'); }; annotationWindow.onbeforeunload = function () { _triggerEvent('annotationWindowClosed'); }; // TODO Find something better. var windowReady = function () { if (!!annotationWindow.createContainerElements) { $(annotationWindow.document).ready(function () { deferred.resolve(annotationWindow); }); } else { setTimeout(windowReady, 100); } }; windowReady(); return deferred.promise(); }; // Remove the toolbar and cancel event listeners var _removeToolbar = function () { _canvas.onClearAnnotation(); $(_elements.resizeSubject).off('resize', _resizeCanvas); toolbar.remove(); $('#annotationToolbarContainer').remove(); }; // Determine whether or not the subscriber stream is from a mobile device var _requestPlatformData = function (pubSub, mobileInitiator) { if (!!pubSub.stream) { var isPublisher = Object.prototype.hasOwnProperty.call(pubSub, 'accessAllowed'); _session.signal({ type: 'otAnnotation_requestPlatform', to: pubSub.stream.connection, }); _session.on('signal:otAnnotation_mobileScreenShare', function (event) { var platform = event.data ? JSON.parse(event.data).platform : null; var isMobile = (platform == 'ios' || platform === 'android'); _subscribingToMobileScreen = isMobile; _canvas.onMobileScreenShare(isMobile, isPublisher); }); } if (mobileInitiator) { _subscribingToMobileScreen = true; _canvas.onMobileScreenShare(true); } }; /** * Creates an external window (if required) and links the annotation toolbar * to the session * @param {object} session * @param {object} [options] * @param {boolean} [options.screensharing] - Using an external window * @param {string} [options.toolbarId] - If the container has an id other than 'toolbar' * @param {array} [options.items] - Custom set of tools * @param {array} [options.colors] - Custom color palette * @returns {promise} < Resolve: undefined | {object} Reference to external annotation window > */ var start = function (session, options) { var deferred = $.Deferred(); if (_.property('screensharing')(options)) { _createExternalWindow() .then(function (externalWindow) { _createToolbar(session, options, externalWindow); toolbar.createPanel(externalWindow); _triggerEvent('startAnnotation', externalWindow); _log(_logEventData.actionStart, _logEventData.variationSuccess); deferred.resolve(externalWindow); }); } else { _createToolbar(session, options); _triggerEvent('startAnnotation'); _log(_logEventData.actionStart, _logEventData.variationSuccess); deferred.resolve(); } return deferred.promise(); }; /** * @param {object} pubSub - Either the publisher(sharing) or subscriber(viewing) * @ param {object} container - The parent container for the canvas element * @ param {object} options * @param {object} options.canvasContainer - The id of the parent for the annotation canvas * @param {object | string} [options.externalWindow] - Reference to the annotation window (or query selector) if publishing * @param {array | string} [options.absoluteParent] - Reference to element (or query selector) for resize if other than container * * @param {Boolean} [options.mobileInitiator] - Is cobrowsing being initiated by a mobile device */ var linkCanvas = function (pubSub, container, options) { /** * jQuery only allows listening for a resize event on the window or a * jQuery resizable element, like #wmsFeedWrap. windowRefernce is a * reference to the popup window created for annotation. If this doesn't * exist, we are watching the canvas belonging to the party viewing the * shared screen */ _elements.resizeSubject = _getElem(_.property('externalWindow')(options) || window); _elements.externalWindow = _getElem(_.property('externalWindow')(options) || null); _elements.absoluteParent = _getElem(_.property('absoluteParent')(options) || null); _elements.cobrowsingImage = _getElem(_.property('cobrowsingImage')(options) || null); _elements.canvasContainer = _getElem(container); // The canvas object _canvas = new OTSolution.Annotations({ feed: pubSub, container: _elements.canvasContainer, externalWindow: _elements.externalWindow }); toolbar.addCanvas(_canvas, _elements.externalWindow); var onScreenCapture = _this.options.onScreenCapture ? _this.options.onScreenCapture : function (dataUrl) { var win = window.open(dataUrl, '_blank'); win.focus(); }; _canvas.onScreenCapture(onScreenCapture); _requestPlatformData(pubSub, options && options.mobileInitiator); var context = _elements.externalWindow ? _elements.externalWindow : window; // The canvas DOM element _elements.canvas = $(_.first(context.document.getElementsByTagName('canvas'))); _listenForResize(); _resizeCanvas(); _triggerEvent('linkAnnotation'); }; /** * Manually update the size of the canvas to match it's container, or the * absolute parent, if defined. */ var resizeCanvas = function () { _resizeCanvas(); }; /** * Change the annotation color of the toolbar passing the colorIndex * @param {Integer} colorIndex - The color index number */ var changeColorByIndex = function (colorIndex) { _changeColorByIndex(colorIndex); }; var takeScreenShot = function () { _takeScreenShot(); }; /** * Adds a subscriber's video the external annotation window * @param {Object} stream - The subscriber stream object */ var addSubscriberToExternalWindow = function (stream) { if (!_elements.externalWindow) { console.log('OT Annotation: External window does not exist. Cannot add subscriber video.'); } else { _elements.externalWindow.addSubscriberVideo(stream); } }; /** * Stop annotation and clean up components * @param {Boolean} publisher Are we the publisher? */ var end = function () { _removeToolbar(); _elements.canvas = null; if (!!_elements.externalWindow) { _elements.externalWindow.close(); _elements.externalWindow = null; _elements.resizeSubject = null; } _triggerEvent('endAnnotation'); _log(_logEventData.actionEnd, _logEventData.variationSuccess); }; var hideToolbar = function () { $(toolbar.parent).hide(); }; var showToolbar = function () { $(toolbar.parent).show(); }; /** * @constructor * Represents an annotation component, used for annotation over video or a shared screen * @param {object} options * @param {object} options.session - An OpenTok session * @param {object} options.canvasContainer - The id of the parent for the annotation canvas * @param {object} options.watchForResize - The DOM element to watch for resize * @param {object} options.onScreenCapture- A callback function to be invoked on screen capture */ var AnnotationAccPack = function (options) { _this = this; _this.options = _.omit(options, 'accPack', 'session'); _accPack = _.property('accPack')(options); _session = _.property('session')(options); if (!_session) { throw new Error('OpenTok Annotation Accelerator Pack requires an OpenTok session'); } _registerEvents(); // init analytics logs _logAnalytics(); _log(_logEventData.actionInitialize, _logEventData.variationSuccess); }; AnnotationAccPack.prototype = { constructor: AnnotationAccPack, start: start, linkCanvas: linkCanvas, resizeCanvas: resizeCanvas, addSubscriberToExternalWindow: addSubscriberToExternalWindow, end: end, hideToolbar: hideToolbar, showToolbar: showToolbar, changeColorByIndex: changeColorByIndex, takeScreenShot: takeScreenShot }; if (typeof exports === 'object') { module.exports = AnnotationAccPack; } else if (typeof define === 'function' && define.amd) { define(function () { return AnnotationAccPack; }); } else { this.AnnotationAccPack = AnnotationAccPack; } }.call(this)); /*! * Annotation Plugin for OpenTok * * @Author: Trevor Boyer * @Copyright (c) 2015 TokBox, Inc **/ /* eslint-disable */ /** Analytics */ (function () { var _OTKAnalytics; var _otkanalytics; var _session; // vars for the analytics logs. Internal use var _logEventData = { clientVersion: 'js-vsol-2.0.59', // x.y.z filled by npm build script componentId: 'annotationsAccPack', name: 'guidAnnotationsKit', actionStartDrawing: 'StartDrawing', actionEndDrawing: 'EndDrawing', variationSuccess: 'Success', }; var _logAnalytics = function () { // init the analytics logs var _source = window.location.href; var otkanalyticsData = { clientVersion: _logEventData.clientVersion, source: _source, componentId: _logEventData.componentId, name: _logEventData.name }; _otkanalytics = new _OTKAnalytics(otkanalyticsData); var sessionInfo = { sessionId: _session.id, connectionId: _session.connection.connectionId, partnerId: _session.apiKey }; _otkanalytics.addSessionInfo(sessionInfo); }; var _log = function (action, variation) { var data = { action: action, variation: variation }; _otkanalytics.logEvent(data); }; /** End Analytics */ //-------------------------------------- // OPENTOK ANNOTATION CANVAS/VIEW //-------------------------------------- var DEFAULT_ASSET_URL = 'https://assets.tokbox.com/solutions/images/'; OTSolution = this.OTSolution || {}; OTSolution.Annotations = function (options) { options = options || {}; this.widgetVersion = 'js-1.0.0-beta'; this.parent = options.container; this.videoFeed = options.feed; this.imageAssets = options.imageAssets || DEFAULT_ASSET_URL; _OTKAnalytics = _OTKAnalytics || options.OTKAnalytics; if (!_otkanalytics) { _logAnalytics(); } if (typeof module === 'object' && typeof module.exports === 'object') { $ = require('jquery'); } var context = options.externalWindow ? options.externalWindow.document : window.document; var self = this; if (this.parent) { var canvas = document.createElement('canvas'); canvas.setAttribute('id', 'opentok_canvas'); // session.connection.id? canvas.style.position = 'absolute'; this.parent.appendChild(canvas); canvas.setAttribute('width', this.parent.clientWidth + 'px'); canvas.style.width = window.getComputedStyle(this.parent).width; canvas.setAttribute('height', this.parent.clientHeight + 'px'); canvas.style.height = window.getComputedStyle(this.parent).height; } function VideoRelativeCoordinateSet(update) { var returnedObj = {}; var scale = { get X() { if (publishingScreenToMobileDevice || (cobrowsing && subscribingToMobileScreen)) { return update.canvasWidth / canvas.width; } var width = cobrowsing ? canvas.width : self.videoFeed.stream.videoDimensions.width; return width / canvas.width; }, get Y() { if (publishingScreenToMobileDevice || (cobrowsing && subscribingToMobileScreen)) { return update.canvasHeight / canvas.height; } var height = cobrowsing ? canvas.height : self.videoFeed.stream.videoDimensions.height; return height / canvas.height; } }; Object.keys(update).forEach(function (attr) { returnedObj[attr] = update[attr]; }); ['X', 'Y'].forEach(function (coord) { ['to', 'from', 'last', 'm', 'start', 'point'].forEach(function (verb) { var attr = verb + coord; returnedObj['_' + attr] = returnedObj[attr]; Object.defineProperty(returnedObj, attr, { get: function () { return returnedObj['_' + attr] / scale[coord]; }, set: function (newVal) { returnedObj['_' + attr] = newVal; // * scale[coord]; } }); }); }); return returnedObj; } var self = this; var ctx; var cbs = []; var isPublisher; var mirrored; var scaledToFill; var batchUpdates = []; var drawHistory = []; var drawHistoryReceivedFrom = []; var updateHistory = []; var eventHistory = []; var isStartPoint = false; var isVideo = self.videoFeed && self.videoFeed.element ? true : false; var cobrowsing = !self.videoFeed.stream; var subscribingToMobileScreen = false; var publishingScreenToMobileDevice = false; var client = new VideoRelativeCoordinateSet({ dragging: false }); // INFO Mirrored feeds contain the OT_mirrored class if (isVideo) { isPublisher = (' ' + self.videoFeed.element.className + ' ').indexOf(' ' + 'OT_publisher' + ' ') > -1; mirrored = isPublisher ? (' ' + self.videoFeed.element.className + ' ').indexOf(' ' + 'OT_mirrored' + ' ') > -1 : false; scaledToFill = (' ' + self.videoFeed.element.className + ' ').indexOf(' ' + 'OT_fit-mode-cover' + ' ') > -1; } else { mirrored = false; scaledToFill = false; } this.canvas = function () { return canvas; }; /** * Links an OpenTok session to the annotation canvas. Typically, this is automatically linked * when using {@link Toolbar#addCanvas}. * @param session The OpenTok session. */ this.link = function (session) { this.session = session; }; /** * Changes the active annotation color for the canvas. * @param color The hex string representation of the color (#rrggbb). */ this.changeColor = function (color) { self.userColor = color; if (!self.lineWidth) { self.lineWidth = 2; // TODO Default to first option in list of line widths } }; /** * Changes the active annotation color for the canvas. * @param colorIndex - the index regarding the colors array */ this.changeColorByIndex = function (colorIndex) { //set the user color self.userColor = this.colors[colorIndex]; //activate the change on the toolbar var colorChoices = context.querySelectorAll('.color-choice'); colorChoices[colorIndex].classList.add('active'); var button = context.getElementById('OT_colors'); button.setAttribute('class', 'OT_color annotation-btn colors'); button.style.borderRadius = '50%'; button.style.backgroundColor = this.colors[colorIndex]; }; /** * Changes the line/stroke width of the active annotation for the canvas. * @param size The size in pixels. */ this.changeLineWidth = function (size) { this.lineWidth = size; }; /** * Sets the selected menu item from the toolbar. This is typically handled * automatically by the toolbar, but can be used to programmatically select an item. * @param item The menu item to set as selected. */ this.selectItem = function (item) { if (self.overlay) { self.parent.removeChild(self.overlay); self.overlay = null; } /** * Update classes for toolbar items */ var updateSelected = function () { // Remove the 'selected' class from the currently selected item (or parent) var current = context.getElementById(self.selectedItem.id); var shapesBtn = context.getElementById('OT_shapes'); var currentIsShape = shapesBtn.classList.contains('selected'); currentIsShape ? shapesBtn.classList.remove('selected') : current.classList.remove('selected'); // If the newly selected item is a shape, update the shapes subpanel button var newlySelected = context.getElementById(item.id); if (newlySelected.parentElement.classList.contains('shapes')) { shapesBtn.classList.add('selected'); } else { newlySelected.classList.add('selected'); } } if (item && item.id === 'OT_capture') { self.captureScreenshot(); } else if (item && item.id.indexOf('OT_line_width') !== -1) { if (item.size) { self.changeLineWidth(item.size); } // 'undo' and 'clear' are actions, not items that can be selected } else if (item.id !== 'OT_undo' && item.id !== 'OT_clear') { updateSelected(); self.selectedItem = item; } }; /** * Sets the color palette for the color picker * @param colors The array of hex color strings (#rrggbb). */ this.colors = function (colors) { this.colors = colors; this.changeColor(colors[0]); }; /** * Clears the canvas for the active user. Only annotations added by the active OpenTok user will * be removed, leaving the history of all other annotations. */ this.clear = function () { clearCanvas(false, self.session.connection.connectionId); if (self.session) { self.session.signal({ type: 'otAnnotation_clear' }); } }; this.undo = function () { undoLast(false, self.session.connection.connectionId); } // TODO Allow the user to choose the image type? (jpg, png) Also allow size? /** * Captures a screenshot of the annotations displayed on top of the active video feed. */ this.captureScreenshot = function (isAnnotationEnd) { var canvasCopy = document.createElement('canvas'); canvasCopy.width = canvas.width; canvasCopy.height = canvas.height; var width = isVideo ? self.videoFeed.videoWidth() : canvas.width; var height = isVideo ? self.videoFeed.videoHeight() : canvas.height; var scale = 1; var offsetX = 0; var offsetY = 0; if (scaledToFill) { if (width < height) { scale = canvas.width / width; width = canvas.width; height = height * scale; } else { scale = canvas.height / height; height = canvas.height; width = width * scale; } // If stretched to fill, we need an offset to center the image offsetX = (width - canvas.width) / 2; offsetY = (height - canvas.height) / 2; } else { if (width > height) { scale = canvas.width / width; width = canvas.width; height = height * scale; offsetX = 0; offsetY = (canvas.height - height) / 2; } else { scale = canvas.height / height; height = canvas.height; width = width * scale; offsetX = (canvas.width - width) / 2; offsetY = 0; } } // Combine the video and annotation images var image = new Image(); image.onload = function () { var ctxCopy = canvasCopy.getContext('2d'); if (mirrored) { ctxCopy.translate(width, 0); ctxCopy.scale(-1, 1); } ctxCopy.drawImage(image, offsetX, offsetY, width, height); // We want to make sure we draw the annotations the same way, so we need to flip back if (mirrored) { ctxCopy.translate(width, 0); ctxCopy.scale(-1, 1); } ctxCopy.drawImage(canvas, 0, 0); cbs.forEach(function (cb) { var data = { src: canvasCopy.toDataURL(), isAnnotationEnd: isAnnotationEnd }; cb.call(self, data); }); // Clear and destroy the canvas copy canvasCopy = null; }; if (isVideo) { imgData = 'data:image/png;base64,' + self.videoFeed.getImgData(); image.src = imgData; } else { var currentWindow = options.externalWindow ? options.externalWindow : window; image.src = currentWindow.getComputedStyle(self.parent)['background-image'].replace(/url\("|"\)/g, ''); } }; this.onScreenCapture = function (cb) { cbs.push(cb); }; /** * Set flags for sharing with mobile devices * @param {Boolean} mobile - Is the other party using a mobile device * @param {Boolean} publishing - Are we publishing our screen? */ this.onMobileScreenShare = function (mobile, publishing) { if (publishing) { publishingScreenToMobileDevice = mobile; } else { subscribingToMobileScreen = mobile; } }; this.onResize = function () { drawUpdates(updateHistory, true); draw(null, true); }; this.onClearAnnotation = function () { dismissTextAnnotation(); }; /** Canvas Handling **/ function addEventListeners(el, s, fn) { var evts = s.split(' '); for (var i = 0, iLen = evts.length; i < iLen; i++) { el.addEventListener(evts[i], fn, true); } } function updateCanvas(event, resizeEvent) { // Ensure that our canvas has been properly sized if (canvas.width === 0) { canvas.width = self.parent.getBoundingClientRect().width; } if (canvas.height === 0) { canvas.height = self.parent.getBoundingClientRect().height; } var offsetLeft = !!resizeEvent ? event.canvas.offsetLeft : canvas.offsetLeft; var offsetTop = !!resizeEvent ? event.canvas.offsetTop : canvas.offsetTop; if (cobrowsing) { var baseWidth = !!resizeEvent ? event.canvas.width : self.parent.clientWidth; var baseHeight = !!resizeEvent ? event.canvas.height : self.parent.clientHeight; var scaleX = canvas.width / baseWidth; var scaleY = canvas.height / baseHeight; var offsetX = event.offsetX || event.pageX - offsetLeft || (event.changedTouches && event.changedTouches[0].pageX - offsetLeft); var offsetY = event.offsetY || event.pageY - offsetTop || (event.changedTouches && event.changedTouches[0].pageY - offsetTop); var x = offsetX * scaleX; var y = offsetY * scaleY; } else { var videoDimensions = self.videoFeed.stream.videoDimensions; var scaleX = videoDimensions.width / canvas.width; var scaleY = videoDimensions.height / canvas.height; if (subscribingToMobileScreen) { scaleX = 1; scaleY = 1; } var offsetX = event.offsetX || event.pageX - offsetLeft || (event.changedTouches && event.changedTouches[0].pageX - offsetLeft); var offsetY = event.offsetY || event.pageY - offsetTop || (event.changedTouches && event.changedTouches[0].pageY - offsetTop); var x = offsetX * scaleX; var y = offsetY * scaleY; } var update; var selectedItem = resizeEvent ? event.selectedItem : self.selectedItem; if (selectedItem) { if (selectedItem.id === 'OT_pen') { switch (event.type) { case 'mousedown': case 'touchstart': client.dragging = true; client.lastX = x; client.lastY = y; self.isStartPoint = true; !resizeEvent && _log(_logEventData.actionStartDrawing, _logEventData.variationSuccess); break; case 'mousemove': case 'touchmove': if (client.dragging) { update = { id: isVideo ? self.videoFeed.stream.connection.connectionId : self.session.connection.connectionId, fromId: self.session.connection.connectionId, fromX: client._lastX, fromY: client._lastY, toX: x, toY: y, color: resizeEvent ? event.userColor : self.userColor, lineWidth: self.lineWidth, videoWidth: isVideo ? self.videoFeed.videoElement().clientWidth : canvas.width, videoHeight: isVideo ? self.videoFeed.videoElement().clientHeight : canvas.height, canvasWidth: canvas.width, canvasHeight: canvas.height, mirrored: mirrored, startPoint: self.isStartPoint, // Each segment is treated as a new set of points endPoint: false, selectedItem: selectedItem, platform: 'web', guid: event.guid }; draw(new VideoRelativeCoordinateSet(update), true); client.lastX = x; client.lastY = y; !resizeEvent && sendUpdate(update); self.isStartPoint = false; } break; case 'mouseup': case 'touchend': client.dragging = false; update = { id: isVideo ? self.videoFeed.stream.connection.connectionId : self.session.connection.connectionId, fromId: self.session.connection.connectionId, fromX: client._lastX, fromY: client._lastY, toX: x, toY: y, color: resizeEvent ? event.userColor : self.userColor, lineWidth: self.lineWidth, videoWidth: isVideo ? self.videoFeed.videoElement().clientWidth : canvas.width, videoHeight: isVideo ? self.videoFeed.videoElement().clientHeight : canvas.height, canvasWidth: canvas.width, canvasHeight: canvas.height, mirrored: mirrored, startPoint: self.isStartPoint, // Each segment is treated as a new set of points endPoint: true, selectedItem: selectedItem, platform: 'web', guid: event.guid }; draw(new VideoRelativeCoordinateSet(update), true); client.lastX = x; client.lastY = y; !resizeEvent && sendUpdate(update); self.isStartPoint = false; !resizeEvent && _log(_logEventData.actionEndDrawing, _logEventData.variationSuccess); break; case 'mouseout': client.dragging = false; } } else if (selectedItem.id === 'OT_text') { update = { id: isVideo ? self.videoFeed.stream.connection.connectionId : self.session.connection.connectionId, fromId: self.session.connection.connectionId, fromX: x, fromY: y + event.inputHeight, // Account for the height of the text input color: event.userColor, font: event.font, text: event.text, videoWidth: isVideo ? self.videoFeed.videoElement().clientWidth : canvas.width, videoHeight: isVideo ? self.videoFeed.videoElement().clientHeight : canvas.height, canvasWidth: canvas.width, canvasHeight: canvas.height, mirrored: mirrored, selectedItem: selectedItem, platform: 'web', guid: event.guid }; draw(new VideoRelativeCoordinateSet(update)); !resizeEvent && sendUpdate(update); } else { // We have a shape or custom object // We are currently using a constant default width for shapes var shapeLineWidth = 2; if (selectedItem && selectedItem.points) { client.mX = x; client.mY = y; switch (event.type) { case 'mousedown': case 'touchstart': client.isDrawing = true; client.dragging = true; client.startX = x; client.startY = y; break; case 'mousemove': case 'touchmove': if (client.dragging) { update = { color: resizeEvent ? event.userColor : self.userColor, lineWidth: resizeEvent ? event.lineWidth : shapeLineWidth, selectedItem: selectedItem // INFO The points for scaling will get added when drawing is complete }; draw(new VideoRelativeCoordinateSet(update), true); } break; case 'mouseup': case 'touchend': client.isDrawing = false; var points = selectedItem.points; if (points.length === 2) { update = { id: isVideo ? self.videoFeed.stream.connection.connectionId : self.session.connection.connectionId, fromId: self.session.connection.connectionId, fromX: client._startX, fromY: client._startY, toX: client._mX, toY: client._mY, color: resizeEvent ? event.userColor : self.userColor, lineWidth: resizeEvent ? event.lineWidth : shapeLineWidth, videoWidth: isVideo ? self.videoFeed.videoElement().clientWidth : canvas.width, videoHeight: isVideo ? self.videoFeed.videoElement().clientHeight : canvas.height, canvasWidth: canvas.width, canvasHeight: canvas.height, mirrored: mirrored, smoothed: false, startPoint: true, selectedItem: selectedItem, platform: 'web', guid: event.guid }; drawHistory.push(new VideoRelativeCoordinateSet(update)); !resizeEvent && sendUpdate(update); } else { var scale = scaleForPoints(points); for (var i = 0; i < points.length; i++) { var firstPoint = false; var endPoint = false; // Scale the points according to the difference between the start and end points var pointX = client._startX + (scale.x * points[i][0]); var pointY = client._startY + (scale.y * points[i][1]); if (i === 0) { client.lastX = pointX; client.lastY = pointY; firstPoint = true; } else if (i === points.length - 1) { endPoint = true; } update = { id: isVideo ? self.videoFeed.stream.connection.connectionId : self.session.connection.connectionId, fromId: self.session.connection.connectionId, fromX: client._lastX, fromY: client._lastY, toX: pointX, toY: pointY, color: resizeEvent ? event.userColor : self.userColor, lineWidth: resizeEvent ? event.lineWidth : shapeLineWidth, videoWidth: isVideo ? self.videoFeed.videoElement().clientWidth : canvas.width, videoHeight: isVideo ? self.videoFeed.videoElement().clientHeight : canvas.height, canvasWidth: canvas.width, canvasHeight: canvas.height, mirrored: mirrored, smoothed: selectedItem.enableSmoothing, startPoint: firstPoint, endPoint: endPoint, selectedItem: selectedItem, platform: 'web', guid: event.guid }; drawHistory.push(new VideoRelativeCoordinateSet(update)); !resizeEvent && sendUpdate(update); client.lastX = pointX; // SCALE BACK! client.lastY = pointY; } draw(null); } client.dragging = false; } } } } } function guid() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } addEventListeners(canvas, 'mousedown mousemove mouseup mouseout touchstart touchmove touchend', function (event) { // Handle text annotation separately and ignore mouse movements if we're not dragging. var istextEvent = self.selectedItem && self.selectedItem.id === 'OT_text'; var notDragging = event.type === 'mousemove' && !client.dragging; if (istextEvent || notDragging) { return; } event.preventDefault(); // Save raw events to reprocess on canvas resize event.selectedItem = self.selectedItem; if (event.selectedItem) { event.canvas = { width: canvas.width, height: canvas.height, offsetLeft: canvas.offsetLeft, offsetTop: canvas.offsetTop }; event.userColor = self.userColor; event.lineWidth = self.lineWidth; event.guid = guid(); eventHistory.push(event); } updateCanvas(event); }); /** * We need intermediate event handling for text annotation since the user is adding * text to an input element before it is actually added to the canvas. The original * click event is assigned to textEvent, which is then updated before being passed * to updateCanvas. */ /** Listen for a click on the canvas. When it occurs, append a text input * that the user can edit and listen for keydown on the enter key. When enter is * pressed, processTextEvent is called, the input element is removed, and the text * is appended to the canvas. */ var textEvent; var textInputId = 'textAnnotation'; var commitPopId = 'commitTextPop'; var commitPopClickId = 'confirm-btn'; var dismissPopId = 'dismiss-btn'; var ignoreClicks = false; var handleClick = function (event) { event.preventDefault(); if (!self.selectedItem || self.selectedItem.id !== 'OT_text' || ignoreClicks) { return; } ignoreClicks = true; // Save raw events to reprocess on canvas resize event.selectedItem = self.selectedItem; createTextInput(event); }; /** * Get the value of the text input and use it to create an "event". */ var processTextEvent = function () { var textInput = context.getElementById(textInputId); var inputheight = textInput.clientHeight; if (!textInput.value) { textEvent = null; return; } textEvent.text = textInput.value; textEvent.font = '16px Arial'; textEvent.userColor = self.userColor; textEvent.canvas = { width: canvas.width, height: canvas.height, offsetLeft: canvas.offsetLeft, offsetTop: canvas.offsetTop } eventHistory.push(textEvent); updateCanvas(textEvent); dismissTextAnnotation(); ignoreClicks = false; }; var createTextInput = function (event) { var textInput = context.createElement('input'); textInput.setAttribute('type', 'text'); textInput.style.position = 'fixed'; textInput.style.top = event.clientY + 'px'; textInput.style.left = event.clientX + 'px'; textInput.style.background = 'rgba(255,255,255, .5)'; textInput.style.width = '100px'; textInput.style.maxWidth = '200px'; textInput.style.border = '1px dashed red'; textInput.style.fontSize = '16px'; textInput.style.color = self.userColor; textInput.style.fontFamily = 'Arial'; textInput.style.zIndex = '1001'; textInput.setAttribute('data-canvas-origin', JSON.stringify({ x: event.offsetX, y: event.offsetY })); textInput.setAttribute('data-top', event.clientY - 50) textInput.id = textInputId; context.body.appendChild(textInput); textInput.focus(); textInput.addEventListener('keydown', function (event) { //If its Enter if (event.which === 13) { creteCommitPop(textInput) } }) textInput.addEventListener('blur', function () { creteCommitPop(textInput) }) textEvent = event; textEvent.inputHeight = textInput.clientHeight; ignoreClicks = true; }; var creteCommitPop = function (textInput) { if (context.getElementById(commitPopId)) return; var commitPop = context.createElement('div'); commitPop.style.position = 'fixed'; commitPop.style.top = textInput.dataset.top + 'px'; commitPop.style.left = textInput.style.left; commitPop.style.width = '200px'; commitPop.style.fontSize = '16px'; commitPop.style.fontFamily = 'Arial'; commitPop.style.zIndex = '2000'; commitPop.style.border = '1px solid grey'; commitPop.style.height = '40px'; commitPop.className = 'ots-annotation-prompt'; commitPop.id = commitPopId; var commitPopText = context.createElement('span'); var text = context.createTextNode('Commit type?'); commitPopText.appendChild(text); commitPop.append(commitPopText) var commitPopClick = context.createElement('div'); commitPopClick.id = commitPopClickId; commitPopClick.className = commitPopClickId; var dismissDiv = context.createElement('div'); dismissDiv.id = dismissPopId; dismissDiv.className = dismissPopId; commitPop.appendChild(commitPopClick); commitPop.appendChild(dismissDiv); context.body.appendChild(commitPop); dismissDiv.addEventListener('click', dismissTextAnnotation); commitPopClick.addEventListener('click', function () { processTextEvent(); }); }; var dismissTextAnnotation = function () { if (context.getElementById(textInputId)) context.getElementById(textInputId).remove(); if (context.getElementById(commitPopId)) context.getElementById(commitPopId).remove(); textEvent = null; ignoreClicks = false; } addEventListeners(canvas, 'click', handleClick); /** * End Handle text markup */ var draw = function (update, resizeEvent) { if (!ctx) { ctx = canvas.getContext('2d'); ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.fillStyle = 'solid'; } // Clear the canvas ctx.clearRect(0, 0, canvas.width, canvas.height); // Repopulate the canvas with items from drawHistory drawHistory.forEach(function (history) { ctx.strokeStyle = history.color; ctx.lineWidth = history.lineWidth; // IN