From 58086265a511f4a94732efe65ce7a3172718157f Mon Sep 17 00:00:00 2001 From: Wensheng Yan Date: Sun, 9 Jul 2017 03:19:42 -0400 Subject: [PATCH] add animation support. --- index.html | 55 +++++-- package.json | 4 +- src/scripts/Components/Animation.js | 169 ++++++++++++++++++++++ src/scripts/Components/BaseCanvas.js | 47 +++--- src/scripts/Components/Marker.js | 6 + src/scripts/Components/MarkerContainer.js | 10 +- src/scripts/Panorama.js | 37 ++++- src/scripts/types/Settings.js | 29 +++- src/scripts/utils/animation.js | 31 +++- src/scripts/utils/warning.js | 1 + src/styles/plugin.scss | 2 +- 11 files changed, 353 insertions(+), 38 deletions(-) create mode 100644 src/scripts/Components/Animation.js diff --git a/index.html b/index.html index 5e64d55..42b0f93 100644 --- a/index.html +++ b/index.html @@ -28,12 +28,17 @@ top: 0; left: 0; } + .vjs-marker{ + white-space: normal; + width: 100px; + font-size: 2em; + }
-
@@ -50,11 +55,7 @@ return check; } (function(window, videojs) { - var player = window.player = videojs('videojs-panorama-player', { - poster: "assets/poster-360.jpg", - plugins: { - } - }); + var player = window.player = videojs('videojs-panorama-player', {}); var panorama = player.panorama({ PanoramaThumbnail: true, @@ -72,9 +73,7 @@ lon: 180 }, radius: 500, - element: "Marker 1", - keyPoint: 5000, - duration: 10000 + element: "This is text 1 with long text" }, { location: { @@ -82,9 +81,43 @@ lon: 160 }, radius: 500, - element: "Marker 2", + element: "This is text 2 with long text", + onShow: function(){ + console.log("text 2 is shown"); + }, + onHide: function(){ + console.log("text 2 is hidden"); + } + } + ], + Animation: [ + { + keyPoint: 0, + from: { + lon: 180, + }, + to:{ + lon: 540, + }, + duration: 8000, + ease: "linear", + onComplete: function () { + console.log("animation 1 is completed"); + } + }, + { + keyPoint: 0, + from: { + fov: 75, + }, + to:{ + fov: 90, + }, + duration: 5000, + ease: "linear", } - ] + ], + VRGapDegree: 0 }); window.player = player; diff --git a/package.json b/package.json index 8856711..42df6e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "videojs-panorama", - "version": "0.1.6", + "version": "1.0.0", "description": "a plugin for videojs run a full 360 degree panorama video. ", "keywords": [ "videojs", @@ -69,4 +69,4 @@ "video.js": "global:videojs", "three": "global:THREE" } -} +} \ No newline at end of file diff --git a/src/scripts/Components/Animation.js b/src/scripts/Components/Animation.js new file mode 100644 index 0000000..6d3be7a --- /dev/null +++ b/src/scripts/Components/Animation.js @@ -0,0 +1,169 @@ +// @flow + +import type { Player, AnimationSettings } from '../types'; +import BaseCanvas from './BaseCanvas'; +import { mergeOptions, easeFunctions } from '../utils'; + +type Timeline = { + active: boolean; + initialized: boolean; + completed: boolean; + startValue: any; + byValue: any; + endValue: any; + ease?: Function; + onComplete?: Function; + keyPoint: number; + duration: number; + beginTime: number; + endTime: number; + from?: any; + to: any; +} + +class Animation { + _player: Player; + _options: { + animation: AnimationSettings[]; + canvas: BaseCanvas + }; + _canvas: BaseCanvas; + _timeline: Timeline[]; + _active: boolean; + + constructor(player: Player, options: {animation: AnimationSettings[], canvas: BaseCanvas}){ + this._player = player; + this._options = mergeOptions({}, this._options); + this._options = mergeOptions(this._options, options); + + this._canvas = this._options.canvas; + this._timeline = []; + + this._options.animation.forEach((obj: AnimationSettings) =>{ + this.addTimeline(obj); + }); + } + + addTimeline(opt: AnimationSettings){ + let timeline: Timeline = { + active: false, + initialized: false, + completed: false, + startValue: {}, + byValue: {}, + endValue: {}, + keyPoint: opt.keyPoint, + duration: opt.duration, + beginTime: Infinity, + endTime: Infinity, + onComplete: opt.onComplete, + from: opt.from, + to: opt.to + }; + + if(typeof opt.ease === "string"){ + timeline.ease = easeFunctions[opt.ease]; + } + if(typeof opt.ease === "undefined"){ + timeline.ease = easeFunctions.linear; + } + + this._timeline.push(timeline); + this.attachEvents(); + } + + initialTimeline(timeline: Timeline){ + for(let key in timeline.to){ + if(timeline.to.hasOwnProperty(key)){ + let from = timeline.from? (typeof timeline.from[key] !== "undefined"? timeline.from[key] : this._canvas[`_${key}`]) : this._canvas[`_${key}`]; + timeline.startValue[key] = from; + timeline.endValue[key] = timeline.to[key]; + timeline.byValue[key] = timeline.to[key] - from; + } + } + } + + processTimeline(timeline: Timeline, animationTime: number){ + for (let key in timeline.to){ + if (timeline.to.hasOwnProperty(key)) { + let newVal = timeline.ease && timeline.ease(animationTime, timeline.startValue[key], timeline.byValue[key], timeline.duration); + if(key === "fov"){ + this._canvas._camera.fov = newVal; + this._canvas._camera.updateProjectionMatrix(); + }else{ + this._canvas[`_${key}`] = newVal; + } + } + } + } + + attachEvents(){ + this._active = true; + this._canvas.addListener("beforeRender", this.renderAnimation.bind(this)); + this._player.on("seeked", this.handleVideoSeek.bind(this)); + } + + detachEvents(){ + this._active = false; + this._canvas.controlable = true; + this._canvas.removeListener("beforeRender", this.renderAnimation.bind(this)); + } + + handleVideoSeek(){ + let currentTime = this._player.getVideoEl().currentTime * 1000; + let resetTimeline = 0; + this._timeline.forEach((timeline: Timeline)=>{ + let res = timeline.keyPoint >= currentTime || (timeline.keyPoint <= currentTime && (timeline.keyPoint + timeline.duration) >= currentTime); + if(res){ + resetTimeline++; + timeline.completed = false; + timeline.initialized = false; + } + }); + + if(resetTimeline > 0 && !this._active){ + this.attachEvents(); + } + } + + renderAnimation(){ + let currentTime = this._player.getVideoEl().currentTime * 1000; + let completeTimeline = 0; + let inActiveTimeline = 0; + this._timeline.filter((timeline: Timeline)=>{ + if(timeline.completed) { + completeTimeline++; + return false; + } + let res = timeline.keyPoint <= currentTime && (timeline.keyPoint + timeline.duration) > currentTime; + timeline.active = res; + if(timeline.active === false) inActiveTimeline++; + + if(res && !timeline.initialized){ + timeline.initialized = true; + timeline.beginTime = timeline.keyPoint; + timeline.endTime = timeline.beginTime + timeline.duration; + this.initialTimeline(timeline); + } + if(timeline.endTime <= currentTime){ + timeline.completed = true; + this.processTimeline(timeline, timeline.duration); + if(timeline.onComplete){ + timeline.onComplete.call(this); + } + } + return res; + }).forEach((timeline: Timeline)=>{ + let animationTime = currentTime - timeline.beginTime; + this.processTimeline(timeline, animationTime); + }); + + this._canvas.controlable = inActiveTimeline === this._timeline.length; + + if(completeTimeline === this._timeline.length){ + this.detachEvents(); + } + } +} + +export default Animation; \ No newline at end of file diff --git a/src/scripts/Components/BaseCanvas.js b/src/scripts/Components/BaseCanvas.js index 17cee34..0a33cb1 100644 --- a/src/scripts/Components/BaseCanvas.js +++ b/src/scripts/Components/BaseCanvas.js @@ -34,6 +34,7 @@ class BaseCanvas extends Component{ /** * Interaction */ + _controlable: boolean; _VRMode: boolean; _mouseDown: boolean; _mouseDownPointer: Point; @@ -68,6 +69,7 @@ class BaseCanvas extends Component{ this._isUserInteracting = false; this._runOnMobile = mobileAndTabletcheck(); this._VRMode = false; + this._controlable = true; this._mouseDownPointer = { x: 0, @@ -363,24 +365,27 @@ class BaseCanvas extends Component{ } render(){ - if(!this._isUserInteracting){ - let symbolLat = (this._lat > this.options.initLat)? -1 : 1; - let symbolLon = (this._lon > this.options.initLon)? -1 : 1; - if(this.options.backToInitLat){ - this._lat = ( - this._lat > (this.options.initLat - Math.abs(this.options.returnLatSpeed)) && - this._lat < (this.options.initLat + Math.abs(this.options.returnLatSpeed)) - )? this.options.initLat : this._lat + this.options.returnLatSpeed * symbolLat; - } - if(this.options.backToInitLon){ - this._lon = ( - this._lon > (this.options.initLon - Math.abs(this.options.returnLonSpeed)) && - this._lon < (this.options.initLon + Math.abs(this.options.returnLonSpeed)) - )? this.options.initLon : this._lon + this.options.returnLonSpeed * symbolLon; + this.trigger("beforeRender"); + if(this._controlable){ + if(!this._isUserInteracting){ + let symbolLat = (this._lat > this.options.initLat)? -1 : 1; + let symbolLon = (this._lon > this.options.initLon)? -1 : 1; + if(this.options.backToInitLat){ + this._lat = ( + this._lat > (this.options.initLat - Math.abs(this.options.returnLatSpeed)) && + this._lat < (this.options.initLat + Math.abs(this.options.returnLatSpeed)) + )? this.options.initLat : this._lat + this.options.returnLatSpeed * symbolLat; + } + if(this.options.backToInitLon){ + this._lon = ( + this._lon > (this.options.initLon - Math.abs(this.options.returnLonSpeed)) && + this._lon < (this.options.initLon + Math.abs(this.options.returnLonSpeed)) + )? this.options.initLon : this._lon + this.options.returnLonSpeed * symbolLon; + } + }else if(this._accelector.x !== 0 && this._accelector.y !== 0){ + this._lat += this._accelector.y; + this._lon += this._accelector.x; } - }else if(this._accelector.x !== 0 && this._accelector.y !== 0){ - this._lat += this._accelector.y; - this._lon += this._accelector.x; } if(this._options.minLon === 0 && this._options.maxLon === 360){ @@ -406,6 +411,14 @@ class BaseCanvas extends Component{ get VRMode(): boolean{ return this._VRMode; } + + get controlable(): boolean{ + return this._controlable; + } + + set controlable(val: boolean): void{ + this._controlable = val; + } } export default BaseCanvas; \ No newline at end of file diff --git a/src/scripts/Components/Marker.js b/src/scripts/Components/Marker.js index a756334..b53aba1 100644 --- a/src/scripts/Components/Marker.js +++ b/src/scripts/Components/Marker.js @@ -50,11 +50,17 @@ class Marker extends Component{ enableMarker(){ this._enable = true; this.addClass("vjs-marker--enable"); + if(this.options.onShow){ + this.options.onShow.call(null); + } } disableMarker(){ this._enable = false; this.removeClass("vjs-marker--enable"); + if(this.options.onHide){ + this.options.onHide.call(null); + } } render(canvas: BaseCanvas, camera: THREE.PerspectiveCamera){ diff --git a/src/scripts/Components/MarkerContainer.js b/src/scripts/Components/MarkerContainer.js index bdf4a1d..3d9e87f 100644 --- a/src/scripts/Components/MarkerContainer.js +++ b/src/scripts/Components/MarkerContainer.js @@ -3,6 +3,7 @@ import BaseCanvas from './BaseCanvas'; import Component from './Component'; import MarkerGroup from './MarkerGroup'; +import { mergeOptions } from '../utils'; import type { Player, MarkerSettings } from '../types'; class MarkerContainer extends Component{ @@ -24,10 +25,17 @@ class MarkerContainer extends Component{ markers: this.options.markers, camera: this._canvas._camera }); + + let markersSettings = this.options.markers.map((marker: MarkerSettings)=>{ + let newMarker = mergeOptions({}, marker); + newMarker.onShow = undefined; + newMarker.onHide = undefined; + return newMarker; + }); let rightMarkerGroup = new MarkerGroup(this.player, { id: "right_group", canvas: this._canvas, - markers: this.options.markers, + markers: markersSettings, camera: this._canvas._camera }); this.addChild("leftMarkerGroup", leftMarkerGroup); diff --git a/src/scripts/Panorama.js b/src/scripts/Panorama.js index db6e76f..0712f80 100644 --- a/src/scripts/Panorama.js +++ b/src/scripts/Panorama.js @@ -1,7 +1,7 @@ // @flow import makeVideoPlayableInline from 'iphone-inline-video'; -import type {Settings, Player, VideoTypes, Coordinates} from './types/index'; +import type {Settings, Player, VideoTypes, Coordinates, AnimationSettings} from './types/index'; import type BaseCanvas from './Components/BaseCanvas'; import EventEmitter from 'wolfy87-eventemitter'; import Equirectangular from './Components/Equirectangular'; @@ -13,6 +13,7 @@ import Notification from './Components/Notification'; import Thumbnail from './Components/Thumbnail'; import VRButton from './Components/VRButton'; import MarkerContainer from './Components/MarkerContainer'; +import Animation from './Components/Animation'; import { Detector, webGLErrorMessage, crossDomainWarning, transitionEvent, mergeOptions, mobileAndTabletcheck, isIos, isRealIphone, warning } from './utils'; const runOnMobile = mobileAndTabletcheck(); @@ -97,7 +98,9 @@ export const defaults: Settings = { HideTime: 3000, }, - Markers: false + Markers: false, + + Animations: false }; export const VR180Defaults: any = { @@ -122,6 +125,7 @@ class Panorama extends EventEmitter{ _player: Player; _videoCanvas: BaseCanvas; _thumbnailCanvas: BaseCanvas | null; + _animation: Animation; /** * check legacy option settings and produce warning message if user use legacy options, automatically set it to new options. @@ -349,17 +353,34 @@ class Panorama extends EventEmitter{ this.player.addComponent("markerContainer", markerContainer); } + //initial animations + if(this.options.Animation && Array.isArray(this.options.Animation)){ + this._animation = new Animation(this.player, { + animation: this.options.Animation, + canvas: this.videoCanvas + }); + } + //detect black screen if(window.console && window.console.error){ let originalErrorFunction = window.console.error; + let originalWarnFunction = window.console.warn; window.console.error = (error)=>{ if(error.message.indexOf("insecure") !== -1){ this.popupNotification(crossDomainWarning()); this.dispose(); } }; + window.console.warn = (warn) =>{ + if(warn.indexOf("gl.getShaderInfoLog") !== -1){ + this.popupNotification(crossDomainWarning()); + this.dispose(); + window.console.warn = originalWarnFunction; + } + }; setTimeout(()=>{ window.console.error = originalErrorFunction; + window.console.warn = originalWarnFunction; }, 500); } }; @@ -405,6 +426,18 @@ class Panorama extends EventEmitter{ } } + addTimeline(animation: AnimationSettings) : void{ + this._animation.addTimeline(animation); + } + + enableAnimation(){ + this._animation.attachEvents(); + } + + disableAnimation(){ + this._animation.detachEvents(); + } + getCoordinates(): Coordinates{ let canvas = this.thumbnailCanvas || this.videoCanvas; return { diff --git a/src/scripts/types/Settings.js b/src/scripts/types/Settings.js index c98d229..1d49712 100644 --- a/src/scripts/types/Settings.js +++ b/src/scripts/types/Settings.js @@ -26,9 +26,30 @@ export type MarkerSettings = { duration?: number; /** - * callback function when marker is disappear + * callback function when marker is shown */ - complete?: Function; + onShow?: Function; + /** + * callback function when marker is hidden + */ + onHide?: Function; +} + +export type AnimationSettings = { + keyPoint: number; + from?: { + lon?: number; + lat?: number; + fov?: number; + }; + to: { + lon?: number; + lat?: number; + fov?: number; + }; + duration: number; + ease?: Function | string; + onComplete?: Function; } /** @@ -136,7 +157,9 @@ export type Settings = { HideTime?: number; }; - Markers?: MarkerSettings[] | boolean, + Markers?: MarkerSettings[] | boolean; + + Animation?: AnimationSettings[] | boolean; ready?: Function; diff --git a/src/scripts/utils/animation.js b/src/scripts/utils/animation.js index c36a272..cda7c68 100644 --- a/src/scripts/utils/animation.js +++ b/src/scripts/utils/animation.js @@ -17,4 +17,33 @@ function whichTransitionEvent(){ } } -export const transitionEvent = whichTransitionEvent(); \ No newline at end of file +export const transitionEvent = whichTransitionEvent(); + +//adopt from http://gizma.com/easing/ +function linear(t: number, b: number, c: number, d: number): number{ + return c*t/d + b; +} + +function easeInQuad(t: number, b: number, c: number, d: number): number { + t /= d; + return c*t*t + b; +} + +function easeOutQuad(t: number, b: number, c: number, d: number): number { + t /= d; + return -c * t*(t-2) + b; +} + +function easeInOutQuad(t: number, b: number, c: number, d: number): number { + t /= d / 2; + if (t < 1) return c / 2 * t * t + b; + t--; + return -c / 2 * (t * (t - 2) - 1) + b; +} + +export const easeFunctions = { + linear: linear, + easeInQuad: easeInQuad, + easeOutQuad: easeOutQuad, + easeInOutQuad: easeInOutQuad +}; \ No newline at end of file diff --git a/src/scripts/utils/warning.js b/src/scripts/utils/warning.js index 244e565..64623d3 100644 --- a/src/scripts/utils/warning.js +++ b/src/scripts/utils/warning.js @@ -23,6 +23,7 @@ export const warning = (message: string): void => { export const crossDomainWarning = (): HTMLElement => { let element = document.createElement( 'div' ); + element.className = "vjs-cross-domain-unsupport"; element.innerHTML = "Sorry, Your browser don't support cross domain."; return element; }; \ No newline at end of file diff --git a/src/styles/plugin.scss b/src/styles/plugin.scss index d217c20..d2d1d9f 100644 --- a/src/styles/plugin.scss +++ b/src/styles/plugin.scss @@ -77,7 +77,7 @@ } } - #webgl-error-message{ + #webgl-error-message, .vjs-cross-domain-unsupport{ position: relative; font-family: monospace; font-size: 13px;