/**
* @module Player
*/
/**
* I am the type definition of an Overlay.
*
* An Overlay displays the content of any type of {{#crossLink "Resource"}}Resource{{/crossLink}}
* in a separate layer on top of the video.
*
* Overlays are managed by the {{#crossLink "OverlaysController"}}OverlaysController{{/crossLink}}.
*
* @class Overlay
* @category TypeDefinition
*/
FrameTrail.defineType(
'Overlay',
function(data){
this.data = data;
this.resourceItem = FrameTrail.newObject(
('Resource' + data.type.charAt(0).toUpperCase() + data.type.slice(1)),
data
)
if ( this.data.type === 'video'
&& this.data.attributes.autoPlay) {
// Note: Currently, the only synced media type is 'video', so we shortcut it
this.syncedMedia = true;
}
this.timelineElement = $('<div class="timelineElement"></div>');
this.overlayElement = $('<div class="overlayElement"></div>');
},
{
/**
* I hold the data object of an Overlay, which is stored in the {{#crossLink "Database"}}Database{{/crossLink}} and saved in the hypervideos's overlays.json file.
* @attribute data
* @type {}
*/
data: {},
/**
* I hold the Resource object of the overlay.
* @attribute resourceItem
* @type Resource
*/
resourceItem: {},
/**
* I signal wether the time-based content of myself should be played synchronized with the main video.
* I am set to true during construction, when my resource type is video and my data.attributes.autoPlay is also true.
* This can be changed later in the {{#crossLink "ResourceVideo/renderPropertiesControls:method"}}ResourceVideo/renderPropertiesControls{{/crossLink}}.
*
* Se also {{#crossLink "Overlay/setSyncedMedia:method"}}Overlay/setSyncedMedia(){{/crossLink}}
*
* @attribute syncedMedia
* @type Boolean
*/
syncedMedia: false,
/**
* I store my state, wether I am "active" (this is, when I am displayed and my timelineElement is highlighted) or not active (invisible).
* @attribute activeState
* @type Boolean
*/
activeState: false,
/**
* I store my state, wether I am "in focus" or not. See also:
* * {{#crossLink "Overlay/gotInFocus:method"}}Overlay/gotInFocus(){{/crossLink}}
* * {{#crossLink "Overlay/removedFromFocus:method"}}Overlay/removedFromFocus(){{/crossLink}}
* * {{#crossLink "OverlaysController/overlayInFocus:attribute"}}OverlaysController/overlayInFocus{{/crossLink}}
* @attribute permanentFocusState
* @type Boolean
*/
permanentFocusState: false,
/**
* I hold the timelineElement (a jquery-enabled HTMLElement), which indicates my start and end time.
* @attribute timelineElement
* @type HTMLElement
*/
timelineElement: null,
/**
* I hold the overlayElement (a jquery-enabled HTMLElement), which displays my content on top of the video.
* @attribute overlayElement
* @type {}
*/
overlayElement: null,
/**
* I render my DOM elements ({{#crossLink "Overlay/timelineElement:attribute"}}Overlay/timelineElement{{/crossLink}}
* and {{#crossLink "Overlay/overlayElement:attribute"}}Overlay/overlayElement{{/crossLink}}) into the DOM.
*
* I am called, when the Overlay is initialized. My counterpart ist {{#crossLink "Overlay/removeFromDOM:method"}}Overlay/removeFromDOM{{/crossLink}}.
*
* @method renderInDOM
*/
renderInDOM: function () {
var ViewVideo = FrameTrail.module('ViewVideo');
ViewVideo.OverlayTimeline.append(this.timelineElement);
ViewVideo.OverlayContainer.append(this.overlayElement);
this.overlayElement.append( this.resourceItem.renderContent() );
this.updateTimelineElement();
this.updateOverlayElement();
if (this.syncedMedia) {
this.setSyncedMedia(true);
}
this.timelineElement.hover(this.brushIn.bind(this), this.brushOut.bind(this));
this.overlayElement.hover(this.brushIn.bind(this), this.brushOut.bind(this));
},
/**
* I remove my DOM elements ({{#crossLink "Overlay/timelineElement:attribute"}}Overlay/timelineElement{{/crossLink}}
* and {{#crossLink "Overlay/overlayElement:attribute"}}Overlay/overlayElement{{/crossLink}}) from the DOM.
*
* I am called when the Overlay is to be deleted.
*
* @method removeFromDOM
*/
removeFromDOM: function () {
this.timelineElement.remove();
this.overlayElement.remove();
},
/**
* I update the CSS of the {{#crossLink "Overlay/timelineElement:attribute"}}timelineElement{{/crossLink}}
* to its correct position within the timeline.
*
* @method updateTimelineElement
*/
updateTimelineElement: function () {
var videoDuration = FrameTrail.module('HypervideoModel').duration,
positionLeft = 100 * (this.data.start / videoDuration),
width = 100 * ((this.data.end - this.data.start) / videoDuration);
this.timelineElement.css({
top: '',
left: positionLeft + '%',
right: '',
width: width + '%'
});
},
/**
* I update the CSS of the {{#crossLink "Overlay/overlayElement:attribute"}}overlayElement{{/crossLink}}
* to its correct position within the overlaysContainer.
*
* @method updateOverlayElement
*/
updateOverlayElement: function () {
this.overlayElement.css({
top: this.data.position.top + '%',
left: this.data.position.left + '%',
width: this.data.position.width + '%',
height: this.data.position.height + '%'
});
this.overlayElement.children('.resourceDetail').css({
opacity: (this.data.attributes.opacity || 1)
});
},
/**
* I update my behavior, wether my time-based content (video) should be synchronized with the main
* video or not.
*
* I control accordingly, wether the video controls should be shown or not.
*
* I append dynamically an attribute to myself (this.videoElement).
*
* Note: My attribute {{#crossLink "Overlay/syncedMedia:attribute"}}syncedMedia{{/crossLink}}
* is independent of this method and stores the current state for use in
* {{#crossLink "Overlays/setActive:method"}}this.setActive(){{/crossLink}} and
* {{#crossLink "Overlays/setInactive:method"}}this.setInactive(){{/crossLink}}.
*
* @method setSyncedMedia
* @param {Boolean} synced
*/
setSyncedMedia: function (synced) {
if (synced) {
this.videoElement = this.overlayElement.find('video.resourceDetail')[0];
this.videoElement.removeAttribute('controls');
} else {
this.videoElement.setAttribute('controls', 'controls');
delete this.videoElement;
}
},
/**
* When I am scheduled to be displayed, this is the method to be called.
* @method setActive
* @param {Boolean} onlyTimelineElement (optional)
*/
setActive: function (onlyTimelineElement) {
if (!onlyTimelineElement) {
this.overlayElement.addClass('active');
}
this.timelineElement.addClass('active');
if (this.syncedMedia) {
FrameTrail.module('OverlaysController').addSyncedMedia(this);
}
this.activeState = true;
},
/**
* When I am scheduled to disappear, this is the method to be called.
* @method setInactive
*/
setInactive: function () {
this.overlayElement.removeClass('active');
this.timelineElement.removeClass('active');
if (this.syncedMedia) {
FrameTrail.module('OverlaysController').removeSyncedMedia(this);
}
this.activeState = false;
},
/**
* When I "got into focus" (which happens, when I become the referenced object in the OverlaysController's
* {{#crossLink "OverlaysController/overlayInFocus:attribute"}}overlayInFocus attribute{{/crossLink}}),
* then this method will be called.
*
* @method gotInFocus
*/
gotInFocus: function () {
this.timelineElement.addClass('highlighted');
this.overlayElement.addClass('highlighted');
FrameTrail.module('OverlaysController').renderPropertiesControls(
this.resourceItem.renderPropertiesControls(this)
);
},
/**
* See also: {{#crossLink "Overlay/gotIntoFocus:method"}}this.gotIntoFocus(){{/crossLink}}
*
* When I was "removed from focus" (which happens, when the OverlaysController's
* {{#crossLink "OverlaysController/overlayInFocus:attribute"}}overlayInFocus attribute{{/crossLink}}),
* is set either to null or to an other overlay than myself),
* then this method will be called.
*
* @method removedFromFocus
*/
removedFromFocus: function () {
this.timelineElement.removeClass('highlighted');
this.overlayElement.removeClass('highlighted');
},
/**
* I am called when the mouse pointer is hovering over one of my two DOM elements
* @method brushIn
*/
brushIn: function () {
this.timelineElement.addClass('brushed');
this.overlayElement.addClass('brushed');
},
/**
* I am called when the mouse pointer is leaving the hovering area over my two DOM elements
* @method brushOut
*/
brushOut: function () {
this.timelineElement.removeClass('brushed');
this.overlayElement.removeClass('brushed');
},
/**
* I am called when the app switches to the editMode "overlays".
*
* I make sure
* * that my {{#crossLink "Overlay/timelineElement:attribute"}}timelineElement{{/crossLink}} is resizable and draggable
* * that my {{#crossLink "Overlay/overlayElement:attribute"}}overlayElement{{/crossLink}} is resizable and draggable
* * that my elements have click handlers for putting myself into focus.
*
* @method startEditing
*/
startEditing: function () {
var self = this,
OverlaysController = FrameTrail.module('OverlaysController');
this.makeTimelineElementDraggable();
this.makeTimelineElementResizeable();
this.makeOverlayElementDraggable();
this.makeOverlayElementResizeable();
this.timelineElement.on('click', putInFocus);
this.overlayElement.on('click', putInFocus);
function putInFocus() {
if (OverlaysController.overlayInFocus === self){
return OverlaysController.overlayInFocus = null;
}
self.permanentFocusState = true;
OverlaysController.overlayInFocus = self;
FrameTrail.module('HypervideoController').currentTime = self.data.start;
}
},
/**
* When the global editMode leaves the state "overlays", I am called to
* stop the editing features of the overlay.
*
* @method stopEditing
*/
stopEditing: function () {
this.timelineElement.draggable('destroy');
this.timelineElement.resizable('destroy');
this.overlayElement.draggable('destroy');
this.overlayElement.resizable('destroy');
this.timelineElement.unbind('click');
this.overlayElement.unbind('click');
},
/**
* I make my {{#crossLink "Overlay/timelineElement:attribute"}}timelineElement{{/crossLink}} draggable.
*
* The event handling changes my this.data.start and this.data.end attributes
* accordingly. Also it updates the control elements of my
* {{#crossLink "Resource/renderBasicPropertiesControls:method"}}properties control interface{{/crossLink}}.
*
* @method makeTimelineElementDraggable
*/
makeTimelineElementDraggable: function () {
var self = this;
this.timelineElement.draggable({
axis: 'x',
containment: 'parent',
snapTolerance: 10,
drag: function(event, ui) {
var closestGridline = FrameTrail.module('ViewVideo').closestToOffset($('.gridline'), {
left: ui.position.left,
top: ui.position.top
}),
snapTolerance = $(this).draggable('option', 'snapTolerance');
if (closestGridline) {
$('.gridline').css('background-color', '#ff9900');
if ( ui.position.left - snapTolerance < closestGridline.position().left &&
ui.position.left + snapTolerance > closestGridline.position().left ) {
ui.position.left = closestGridline.position().left;
closestGridline.css('background-color', '#00ff00');
}
}
var videoDuration = FrameTrail.module('HypervideoModel').duration,
leftPercent = 100 * (ui.helper.position().left / ui.helper.parent().width()),
widthPercent = 100 * (ui.helper.width() / ui.helper.parent().width()),
newStartValue = leftPercent * (videoDuration / 100),
newEndValue = (leftPercent + widthPercent) * (videoDuration / 100);
FrameTrail.module('HypervideoController').currentTime = newStartValue;
FrameTrail.module('OverlaysController').updateControlsStart(newStartValue);
FrameTrail.module('OverlaysController').updateControlsEnd( newEndValue );
},
start: function(event, ui) {
if (!self.permanentFocusState) {
FrameTrail.module('OverlaysController').overlayInFocus = self;
}
},
stop: function(event, ui) {
if (!self.permanentFocusState) {
FrameTrail.module('OverlaysController').overlayInFocus = null;
}
var videoDuration = FrameTrail.module('HypervideoModel').duration,
leftPercent = 100 * (ui.helper.position().left / ui.helper.parent().width()),
widthPercent = 100 * (ui.helper.width() / ui.helper.parent().width());
self.data.start = leftPercent * (videoDuration / 100);
self.data.end = (leftPercent + widthPercent) * (videoDuration / 100);
self.updateTimelineElement();
FrameTrail.module('OverlaysController').stackTimelineView();
FrameTrail.module('HypervideoModel').newUnsavedChange('overlays');
}
});
},
/**
* I make my {{#crossLink "Overlay/timelineElement:attribute"}}timelineElement{{/crossLink}} resizable.
*
* The event handling changes my this.data.start and this.data.end attributes
* accordingly. Also it updates the control elements of my
* {{#crossLink "Resource/renderBasicPropertiesControls:method"}}properties control interface{{/crossLink}}.
*
* @method makeTimelineElementResizeable
*/
makeTimelineElementResizeable: function () {
var self = this,
endHandleGrabbed;
this.timelineElement.resizable({
containment: 'parent',
handles: 'e, w',
resize: function(event, ui) {
var closestGridline = FrameTrail.module('ViewVideo').closestToOffset($('.gridline'), {
left: (endHandleGrabbed ? (ui.position.left + ui.helper.width()) : ui.position.left),
top: ui.position.top
}),
snapTolerance = $(this).draggable('option', 'snapTolerance');
if (closestGridline) {
$('.gridline').css('background-color', '#ff9900');
if ( !endHandleGrabbed &&
ui.position.left - snapTolerance < closestGridline.position().left &&
ui.position.left + snapTolerance > closestGridline.position().left ) {
ui.position.left = closestGridline.position().left;
ui.size.width = ( ui.helper.width() + ( ui.helper.position().left - ui.position.left ) );
closestGridline.css('background-color', '#00ff00');
} else if ( endHandleGrabbed &&
ui.position.left + ui.helper.width() - snapTolerance < closestGridline.position().left &&
ui.position.left + ui.helper.width() + snapTolerance > closestGridline.position().left ) {
ui.helper.width(closestGridline.position().left - ui.position.left);
closestGridline.css('background-color', '#00ff00');
}
}
var videoDuration = FrameTrail.module('HypervideoModel').duration,
leftPercent = 100 * (ui.position.left / ui.helper.parent().width()),
widthPercent = 100 * (ui.helper.width() / ui.helper.parent().width()),
newValue;
if ( endHandleGrabbed ) {
newValue = (leftPercent + widthPercent) * (videoDuration / 100);
FrameTrail.module('HypervideoController').currentTime = newValue;
FrameTrail.module('OverlaysController').updateControlsEnd( newValue );
} else {
newValue = leftPercent * (videoDuration / 100);
FrameTrail.module('HypervideoController').currentTime = newValue;
FrameTrail.module('OverlaysController').updateControlsStart(newValue);
}
},
start: function(event, ui) {
if (!self.permanentFocusState) {
FrameTrail.module('OverlaysController').overlayInFocus = self;
}
endHandleGrabbed = $(event.originalEvent.target).hasClass('ui-resizable-e')
},
stop: function(event, ui) {
if (!self.permanentFocusState) {
FrameTrail.module('OverlaysController').overlayInFocus = null;
}
var videoDuration = FrameTrail.module('HypervideoModel').duration,
leftPercent = 100 * (ui.helper.position().left / ui.helper.parent().width()),
widthPercent = 100 * (ui.helper.width() / ui.helper.parent().width());
self.data.start = leftPercent * (videoDuration / 100);
self.data.end = (leftPercent + widthPercent) * (videoDuration / 100);
FrameTrail.module('OverlaysController').stackTimelineView();
FrameTrail.module('HypervideoModel').newUnsavedChange('overlays');
}
});
},
/**
* I make my {{#crossLink "Overlay/overlayElement:attribute"}}overlayElement{{/crossLink}} draggable.
*
* The event handling changes my this.data.position.[top|left|width|height] attributes
* accordingly. Also it updates the control elements of my
* {{#crossLink "Resource/renderBasicPropertiesControls:method"}}properties control interface{{/crossLink}}.
*
* @method makeOverlayElementDraggable
*/
makeOverlayElementDraggable: function () {
var self = this;
self.overlayElement.draggable({
containment: 'parent',
drag: function(event, ui) {
var parent = ui.helper.parent();
FrameTrail.module('OverlaysController').updateControlsDimensions({
top: ui.helper.position().top/parent.height()*100,
left: ui.helper.position().left/parent.width()*100,
width: ui.helper.width()/parent.width()*100,
height: ui.helper.height()/parent.height()*100
});
},
start: function(event, ui) {
if (!self.permanentFocusState) {
FrameTrail.module('OverlaysController').overlayInFocus = self;
}
},
stop: function(event, ui) {
if (!self.permanentFocusState) {
FrameTrail.module('OverlaysController').overlayInFocus = null;
}
var parent = ui.helper.parent();
self.data.position.top = ui.helper.position().top/parent.height()*100;
self.data.position.left = ui.helper.position().left/parent.width()*100;
self.data.position.width = ui.helper.width()/parent.width()*100;
self.data.position.height = ui.helper.height()/parent.height()*100;
self.updateOverlayElement();
FrameTrail.module('HypervideoModel').newUnsavedChange('overlays');
}
});
},
/**
* I make my {{#crossLink "Overlay/overlayElement:attribute"}}overlayElement{{/crossLink}} resizable.
*
* The event handling changes my this.data.position.[top|left|width|height] attributes
* accordingly. Also it updates the control elements of my
* {{#crossLink "Resource/renderBasicPropertiesControls:method"}}properties control interface{{/crossLink}}.
*
* @method makeOverlayElementResizeable
*/
makeOverlayElementResizeable: function () {
var self = this;
self.overlayElement.resizable({
containment: 'parent',
handles: 'ne, se, sw, nw',
resize: function(event, ui) {
var parent = ui.helper.parent();
FrameTrail.module('OverlaysController').updateControlsDimensions({
top: ui.helper.position().top/parent.height()*100,
left: ui.helper.position().left/parent.width()*100,
width: ui.helper.width()/parent.width()*100,
height: ui.helper.height()/parent.height()*100
})
},
start: function(event, ui) {
if (!self.permanentFocusState) {
FrameTrail.module('OverlaysController').overlayInFocus = self;
}
},
stop: function(event, ui) {
if (!self.permanentFocusState) {
FrameTrail.module('OverlaysController').overlayInFocus = null;
}
var parent = ui.helper.parent();
self.data.position.top = ui.helper.position().top/parent.height()*100;
self.data.position.left = ui.helper.position().left/parent.width()*100;
self.data.position.width = ui.helper.width()/parent.width()*100;
self.data.position.height = ui.helper.height()/parent.height()*100;
self.updateOverlayElement();
FrameTrail.module('HypervideoModel').newUnsavedChange('overlays');
}
});
}
}
);