diff --git a/ShadowEditor.Web/src/editor/animation/Code.js b/ShadowEditor.Web/src/editor/animation/Code.js new file mode 100644 index 00000000..2cb35549 --- /dev/null +++ b/ShadowEditor.Web/src/editor/animation/Code.js @@ -0,0 +1,217 @@ +/** + * @author mrdoob / http://mrdoob.com/ + */ + +var Code = function ( editor ) { + + var signals = editor.signals; + + var container = new UI.Panel(); + container.setId( 'effect' ); + container.setPosition( 'absolute' ); + container.setBackgroundColor( '#272822' ); + container.setDisplay( 'none' ); + + var header = new UI.Panel(); + header.setPadding( '10px' ); + container.add( header ); + + var title = new UI.Text().setColor( '#fff' ); + header.add( title ); + + var buttonSVG = ( function () { + var svg = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' ); + svg.setAttribute( 'width', 32 ); + svg.setAttribute( 'height', 32 ); + var path = document.createElementNS( 'http://www.w3.org/2000/svg', 'path' ); + path.setAttribute( 'd', 'M 12,12 L 22,22 M 22,12 12,22' ); + path.setAttribute( 'stroke', '#fff' ); + svg.appendChild( path ); + return svg; + } )(); + + var close = new UI.Element( buttonSVG ); + close.setPosition( 'absolute' ); + close.setTop( '3px' ); + close.setRight( '1px' ); + close.setCursor( 'pointer' ); + close.onClick( function () { + + container.setDisplay( 'none' ); + + } ); + header.add( close ); + + var delay; + var errorLine = null; + + var currentEffect = null; + var currentInclude = null; + + var codemirror = CodeMirror( container.dom, { + value: '', + lineNumbers: true, + lineWrapping: true, + matchBrackets: true, + indentWithTabs: true, + tabSize: 4, + indentUnit: 4, + mode: 'javascript' + } ); + codemirror.setOption( 'theme', 'monokai' ); + codemirror.on( 'change', function () { + + if ( codemirror.state.focused === false ) return; + + clearTimeout( delay ); + delay = setTimeout( function () { + + if ( errorLine ) { + + codemirror.removeLineClass( errorLine, 'CodeMirror-errorLine' ); + errorLine = null; + + } + + if ( currentInclude !== null ) { + + currentInclude.source = codemirror.getValue(); + + editor.signals.includeChanged.dispatch(); + + } else if ( currentEffect !== null ) { + + var error; + var currentSource = currentEffect.source; + + editor.timeline.reset(); + + try { + + currentEffect.source = codemirror.getValue(); + editor.compileEffect( currentEffect ); + + } catch ( e ) { + + error = e.name + ' : ' + e.message; // e.stack, e.columnNumber, e.lineNumber + + if ( /Chrome/i.test( navigator.userAgent ) ) { + + var result = /:([0-9]+):([0-9+])/g.exec( e.stack ); + if ( result !== null ) errorLine = parseInt( result[ 1 ] ) - 3; + + } else if ( /Firefox/i.test( navigator.userAgent ) ) { + + var result = /Function:([0-9]+):([0-9+])/g.exec( e.stack ); + if ( result !== null ) errorLine = parseInt( result[ 1 ] ) - 1; + + } + + if ( errorLine !== null ) { + + codemirror.addLineClass( errorLine, 'errorLine', 'CodeMirror-errorLine' ); + + } + + } + + editor.timeline.update( editor.player.currentTime ); + + if ( error !== undefined ) { + + errorDiv.setDisplay( '' ); + errorText.setValue( '⌦ ' + error ); + + currentEffect.source = currentSource; + + } else { + + errorDiv.setDisplay( 'none' ); + + } + + } + + }, 1000 ); + + } ); + + var wrapper = codemirror.getWrapperElement(); + wrapper.addEventListener( 'keydown', function ( event ) { + + event.stopPropagation(); + + } ); + + // + + var errorDiv = new UI.Div(); + errorDiv.setPosition( 'absolute' ); + errorDiv.setDisplay( 'none' ); + errorDiv.setTop( '8px' ); + errorDiv.setWidth( '100%' ); + errorDiv.setTextAlign( 'center' ); + errorDiv.setZIndex( '3' ); + container.add( errorDiv ); + + var errorText = new UI.Text(); + errorText.setBackgroundColor( '#f00' ); + errorText.setColor( '#fff' ); + errorText.setPadding( '4px' ); + errorDiv.add( errorText ); + + // + + signals.editorCleared.add( function () { + + container.setDisplay( 'none' ); + + } ); + + signals.effectSelected.add( function ( effect ) { + + container.setDisplay( '' ); + + title.setValue( effect.name ); + + codemirror.setValue( effect.source ); + codemirror.clearHistory(); + + currentEffect = effect; + currentInclude = null; + + } ); + + signals.includeSelected.add( function ( include ) { + + container.setDisplay( '' ); + + title.setValue( include.name ); + + codemirror.setValue( include.source ); + codemirror.clearHistory(); + + currentEffect = null; + currentInclude = include; + + } ); + + editor.signals.animationSelected.add( function ( animation ) { + + if ( animation === null ) return; + + var effect = animation.effect; + + title.setValue( effect.name ); + + codemirror.setValue( effect.source ); + codemirror.clearHistory(); + + currentEffect = effect; + currentInclude = null; + + } ); + + return container; + +}; diff --git a/ShadowEditor.Web/src/editor/animation/Config.js b/ShadowEditor.Web/src/editor/animation/Config.js new file mode 100644 index 00000000..65896eda --- /dev/null +++ b/ShadowEditor.Web/src/editor/animation/Config.js @@ -0,0 +1,53 @@ +/** + * @author mrdoob / http://mrdoob.com/ + */ + +var Config = function () { + + var name = 'framejs-editor'; + + var storage = {}; + + if ( window.localStorage[ name ] !== undefined ) { + + var data = JSON.parse( window.localStorage[ name ] ); + + for ( var key in data ) { + + storage[ key ] = data[ key ]; + + } + + } + + return { + + getKey: function ( key ) { + + return storage[ key ]; + + }, + + setKey: function () { // key, value, key, value ... + + for ( var i = 0, l = arguments.length; i < l; i += 2 ) { + + storage[ arguments[ i ] ] = arguments[ i + 1 ]; + + } + + window.localStorage[ name ] = JSON.stringify( storage ); + + console.log( '[' + /\d\d\:\d\d\:\d\d/.exec( new Date() )[ 0 ] + ']', 'Saved config to LocalStorage.' ); + + }, + + clear: function () { + + delete window.localStorage[ name ]; + + } + + }; + +}; diff --git a/ShadowEditor.Web/src/editor/animation/Controls.js b/ShadowEditor.Web/src/editor/animation/Controls.js new file mode 100644 index 00000000..511f0c29 --- /dev/null +++ b/ShadowEditor.Web/src/editor/animation/Controls.js @@ -0,0 +1,133 @@ +/** + * @author mrdoob / http://mrdoob.com/ + */ + +var Controls = function ( editor ) { + + var signals = editor.signals; + + var container = new UI.Panel(); + container.setId( 'controls' ); + + var row = new UI.Row(); + row.setPadding( '6px' ); + container.add( row ); + + var prevButton = new UI.Button(); + prevButton.setBackground( 'url(files/prev.svg)' ); + prevButton.setWidth( '20px' ); + prevButton.setHeight( '20px' ); + prevButton.setMarginRight( '4px' ); + prevButton.setVerticalAlign( 'middle' ); + prevButton.onClick( function () { + + editor.setTime( editor.player.currentTime - 1 ); + + } ); + row.add( prevButton ); + + var playButton = new UI.Button(); + playButton.setBackground( 'url(files/play.svg)' ); + playButton.setWidth( '20px' ); + playButton.setHeight( '20px' ); + playButton.setMarginRight( '4px' ); + playButton.setVerticalAlign( 'middle' ); + playButton.onClick( function () { + + editor.player.isPlaying ? editor.stop() : editor.play(); + + } ); + row.add( playButton ); + + var nextButton = new UI.Button(); + nextButton.setBackground( 'url(files/next.svg)' ); + nextButton.setWidth( '20px' ); + nextButton.setHeight( '20px' ); + nextButton.setMarginRight( '4px' ); + nextButton.setVerticalAlign( 'middle' ); + nextButton.onClick( function () { + + editor.setTime( editor.player.currentTime + 1 ); + + } ); + row.add( nextButton ); + + function ignoreKeys( event ) { + + switch ( event.keyCode ) { + + case 13: case 32: event.preventDefault(); + + } + + }; + + prevButton.onKeyDown( ignoreKeys ); + playButton.onKeyDown( ignoreKeys ); + nextButton.onKeyDown( ignoreKeys ); + + var timeText = new UI.Text(); + timeText.setColor( '#bbb' ); + timeText.setWidth( '60px' ); + timeText.setMarginLeft( '10px' ); + timeText.setValue( '0:00.00' ); + row.add( timeText ); + + function updateTimeText( value ) { + + var minutes = Math.floor( value / 60 ); + var seconds = value % 60; + var padding = seconds < 10 ? '0' : ''; + + timeText.setValue( minutes + ':' + padding + seconds.toFixed( 2 ) ); + + } + + var playbackRateText = new UI.Text(); + playbackRateText.setColor( '#999' ); + playbackRateText.setMarginLeft( '8px' ); + playbackRateText.setValue( '1.0x' ); + row.add( playbackRateText ); + + function updatePlaybackRateText( value ) { + + playbackRateText.setValue( value.toFixed( 1 ) + 'x' ); + + } + + var fullscreenButton = new UI.Button(); + fullscreenButton.setBackground( 'url(files/fullscreen.svg)' ); + fullscreenButton.setWidth( '20px' ); + fullscreenButton.setHeight( '20px' ); + fullscreenButton.setFloat( 'right' ); + fullscreenButton.setVerticalAlign( 'middle' ); + fullscreenButton.onClick( function () { + + editor.signals.fullscreen.dispatch(); + + } ); + row.add( fullscreenButton ); + + // + + signals.playingChanged.add( function ( isPlaying ) { + + playButton.setBackground( isPlaying ? 'url(files/pause.svg)' : 'url(files/play.svg)' ) + + } ); + + signals.playbackRateChanged.add( function ( value ) { + + updatePlaybackRateText( value ); + + } ); + + signals.timeChanged.add( function ( value ) { + + updateTimeText( value ); + + } ); + + return container; + +}; diff --git a/ShadowEditor.Web/src/editor/animation/Editor.js b/ShadowEditor.Web/src/editor/animation/Editor.js new file mode 100644 index 00000000..2d30cbdb --- /dev/null +++ b/ShadowEditor.Web/src/editor/animation/Editor.js @@ -0,0 +1,583 @@ +/** + * @author mrdoob / http://mrdoob.com/ + */ + +var Editor = function () { + + var Signal = signals.Signal; + + this.signals = { + + editorCleared: new Signal(), + + // libraries + + libraryAdded: new Signal(), + + // includes + + includeAdded: new Signal(), + includeSelected: new Signal(), + includeChanged: new Signal(), + includeRemoved: new Signal(), + includesCleared: new Signal(), + + // effects + + effectAdded: new Signal(), + effectRenamed: new Signal(), + effectRemoved: new Signal(), + effectSelected: new Signal(), + effectCompiled: new Signal(), + + // actions + + fullscreen: new Signal(), + exportState: new Signal(), + + // animations + + animationRenamed: new Signal(), + animationAdded: new Signal(), + animationModified: new Signal(), + animationRemoved: new Signal(), + animationSelected: new Signal(), + + // curves + + curveAdded: new Signal(), + + // events + + playingChanged: new Signal(), + playbackRateChanged: new Signal(), + timeChanged: new Signal(), + timelineScaled: new Signal(), + + windowResized: new Signal() + + }; + + this.config = new Config(); + + this.player = new FRAME.Player(); + this.resources = new FRAME.Resources(); + + this.duration = 500; + + this.libraries = []; + this.includes = []; + this.effects = []; + this.timeline = new FRAME.Timeline(); + + this.selected = null; + + // signals + + var scope = this; + + this.signals.animationModified.add( function () { + + scope.timeline.reset(); + scope.timeline.sort(); + + try { + + scope.timeline.update( scope.player.currentTime ); + + } catch ( e ) { + + console.error( e ); + + } + + } ); + + this.signals.effectCompiled.add( function () { + + try { + + scope.timeline.update( scope.player.currentTime ); + + } catch ( e ) { + + console.error( e ); + + } + + } ); + + this.signals.timeChanged.add( function () { + + try { + + scope.timeline.update( scope.player.currentTime ); + + } catch ( e ) { + + console.error( e ); + + } + + } ); + + // Animate + + var prevTime = 0; + + function animate( time ) { + + scope.player.tick( time - prevTime ); + + if ( scope.player.isPlaying ) { + + scope.signals.timeChanged.dispatch( scope.player.currentTime ); + + } + + prevTime = time; + + requestAnimationFrame( animate ); + + } + + requestAnimationFrame( animate ); + +}; + +Editor.prototype = { + + play: function () { + + this.player.play(); + this.signals.playingChanged.dispatch( true ); + + }, + + stop: function () { + + this.player.pause(); + this.signals.playingChanged.dispatch( false ); + + }, + + speedUp: function () { + + this.player.playbackRate += 0.1; + this.signals.playbackRateChanged.dispatch( this.player.playbackRate ); + + }, + + speedDown: function () { + + this.player.playbackRate -= 0.1; + this.signals.playbackRateChanged.dispatch( this.player.playbackRate ); + + }, + + setTime: function ( time ) { + + // location.hash = time; + + this.player.currentTime = Math.max( 0, time ); + this.signals.timeChanged.dispatch( this.player.currentTime ); + + }, + + // libraries + + addLibrary: function ( url, content ) { + + var script = document.createElement( 'script' ); + script.id = 'library-' + this.libraries.length; + script.textContent = content; + document.head.appendChild( script ); + + this.libraries.push( url ); + this.signals.libraryAdded.dispatch(); + + }, + + // includes + + addInclude: function ( name, source ) { + + try { + new Function( 'resources', source )( this.resources ); + } catch ( e ) { + console.error( e ); + } + + this.includes.push( { name: name, source: source } ); + this.signals.includeAdded.dispatch(); + + }, + + removeInclude: function ( include ) { + + var index = this.includes.indexOf( include ); + + this.includes.splice( index, 1 ); + this.signals.includeRemoved.dispatch(); + + }, + + selectInclude: function ( include ) { + + this.signals.includeSelected.dispatch( include ); + + }, + + reloadIncludes: function () { + + var includes = this.includes; + + this.signals.includesCleared.dispatch(); + + for ( var i = 0; i < includes.length; i ++ ) { + + var include = includes[ i ]; + + try { + new Function( 'resources', include.source )( this.resources ); + } catch ( e ) { + console.error( e ); + } + + } + + }, + + // effects + + addEffect: function ( effect ) { + + this.effects.push( effect ); + this.signals.effectAdded.dispatch( effect ); + + }, + + selectEffect: function ( effect ) { + + this.signals.effectSelected.dispatch( effect ); + + }, + + removeEffect: function ( effect ) { + + var index = this.effects.indexOf( effect ); + + if ( index >= 0 ) { + + this.effects.splice( index, 1 ); + this.signals.effectRemoved.dispatch( effect ); + + } + + }, + + compileEffect: function ( effect ) { + + try { + + effect.compile( this.resources, this.player ); + + } catch ( e ) { + + console.error( e ); + + } + + this.signals.effectCompiled.dispatch( effect ); + + }, + + // Remove any effects that are not bound to any animations. + + cleanEffects: function () { + + var scope = this; + var effects = this.effects.slice( 0 ); + var animations = this.timeline.animations; + + effects.forEach( function ( effect, i ) { + + var bound = false; + + for ( var j = 0; j < animations.length; j++ ) { + + var animation = animations[ j ]; + + if ( animation.effect === effect ) { + + bound = true; + break; + + } + + } + + if ( bound === false ) { + + scope.removeEffect( effect ); + + } + + } ); + + }, + + // animations + + addAnimation: function ( animation ) { + + var effect = animation.effect; + + if ( effect.program === null ) { + + this.compileEffect( effect ); + + } + + this.timeline.add( animation ); + this.signals.animationAdded.dispatch( animation ); + + }, + + selectAnimation: function ( animation ) { + + if ( this.selected === animation ) return; + + this.selected = animation; + this.signals.animationSelected.dispatch( animation ); + + }, + + removeAnimation: function ( animation ) { + + this.timeline.remove( animation ); + this.signals.animationRemoved.dispatch( animation ); + + }, + + addCurve: function ( curve ) { + + this.timeline.curves.push( curve ); + this.signals.curveAdded.dispatch( curve ); + + }, + + clear: function () { + + this.libraries = []; + this.includes = []; + this.effects = []; + + while ( this.timeline.animations.length > 0 ) { + + this.removeAnimation( this.timeline.animations[ 0 ] ); + + } + + this.signals.editorCleared.dispatch(); + + }, + + fromJSON: function ( json ) { + + function loadFile( url, onLoad ) { + + var request = new XMLHttpRequest(); + request.open( 'GET', url, true ); + request.addEventListener( 'load', function ( event ) { + + onLoad( event.target.response ); + + } ); + request.send( null ); + + } + + function loadLibraries( libraries, onLoad ) { + + var count = 0; + + function loadNext() { + + if ( count === libraries.length ) { + + onLoad(); + return; + + } + + var url = libraries[ count ++ ]; + + loadFile( url, function ( content ) { + + scope.addLibrary( url, content ); + loadNext(); + + } ); + + } + + loadNext(); + + } + + var scope = this; + + var libraries = json.libraries || []; + + loadLibraries( libraries, function () { + + var includes = json.includes; + + for ( var i = 0, l = includes.length; i < l; i ++ ) { + + var data = includes[ i ]; + + var name = data[ 0 ]; + var source = data[ 1 ]; + + if ( Array.isArray( source ) ) source = source.join( '\n' ); + + scope.addInclude( name, source ); + + } + + var effects = json.effects; + + for ( var i = 0, l = effects.length; i < l; i ++ ) { + + var data = effects[ i ]; + + var name = data[ 0 ]; + var source = data[ 1 ]; + + if ( Array.isArray( source ) ) source = source.join( '\n' ); + + scope.addEffect( new FRAME.Effect( name, source ) ); + + } + + var animations = json.animations; + + for ( var i = 0, l = animations.length; i < l; i ++ ) { + + var data = animations[ i ]; + + var animation = new FRAME.Animation( + data[ 0 ], + data[ 1 ], + data[ 2 ], + data[ 3 ], + scope.effects[ data[ 4 ] ], + data[ 5 ] + ); + + scope.addAnimation( animation ); + + } + + scope.setTime( 0 ); + + } ); + + }, + + toJSON: function () { + + var json = { + "config": {}, + "libraries": this.libraries.slice(), + "includes": [], + "effects": [], + // "curves": [], + "animations": [] + }; + + /* + // curves + + var curves = this.timeline.curves; + + for ( var i = 0, l = curves.length; i < l; i ++ ) { + + var curve = curves[ i ]; + + if ( curve instanceof FRAME.Curves.Linear ) { + + json.curves.push( [ 'linear', curve.points ] ); + + } + + } + */ + + // includes + + var includes = this.includes; + + for ( var i = 0, l = includes.length; i < l; i ++ ) { + + var include = includes[ i ]; + + var name = include.name; + var source = include.source; + + json.includes.push( [ name, source.split( '\n' ) ] ); + + } + + // effects + + var effects = this.effects; + + for ( var i = 0, l = effects.length; i < l; i ++ ) { + + var effect = effects[ i ]; + + var name = effect.name; + var source = effect.source; + + json.effects.push( [ name, source.split( '\n' ) ] ); + + } + + // animations + + var animations = this.timeline.animations; + + for ( var i = 0, l = animations.length; i < l; i ++ ) { + + var animation = animations[ i ]; + var effect = animation.effect; + + /* + var parameters = {}; + + for ( var key in module.parameters ) { + + parameters[ key ] = module.parameters[ key ].value; + + } + */ + + json.animations.push( [ + animation.name, + animation.start, + animation.end, + animation.layer, + this.effects.indexOf( animation.effect ), + animation.enabled + ] ); + + } + + return json; + + } + +}; diff --git a/ShadowEditor.Web/src/editor/animation/Menubar.Edit.js b/ShadowEditor.Web/src/editor/animation/Menubar.Edit.js new file mode 100644 index 00000000..f69d8e9d --- /dev/null +++ b/ShadowEditor.Web/src/editor/animation/Menubar.Edit.js @@ -0,0 +1,65 @@ +/** + * @author mrdoob / http://mrdoob.com/ + */ + +Menubar.Edit = function ( editor ) { + + var container = new UI.Panel(); + container.setClass( 'menu' ); + + var title = new UI.Panel(); + title.setClass( 'title' ); + title.setTextContent( 'Edit' ); + container.add( title ); + + // + + var options = new UI.Panel(); + options.setClass( 'options' ); + container.add( options ); + + // duplicate + + var option = new UI.Panel(); + option.setClass( 'option' ); + option.setTextContent( 'Duplicate' ); + option.onClick( function () { + + if ( editor.selected === null ) return; + + var selected = editor.selected; + + var offset = selected.end - selected.start; + + var animation = new FRAME.Animation( + selected.name, + selected.start + offset, + selected.end + offset, + selected.layer, + selected.effect + ); + + editor.addAnimation( animation ); + editor.selectAnimation( animation ); + + } ); + options.add( option ); + + // remove + + var option = new UI.Panel(); + option.setClass( 'option' ); + option.setTextContent( 'Remove' ); + option.onClick( function () { + + if ( editor.selected === null ) return; + + editor.removeAnimation( editor.selected ); + editor.selectAnimation( null ); + + } ); + options.add( option ); + + return container; + +}; diff --git a/ShadowEditor.Web/src/editor/animation/Menubar.Examples.js b/ShadowEditor.Web/src/editor/animation/Menubar.Examples.js new file mode 100644 index 00000000..03663d03 --- /dev/null +++ b/ShadowEditor.Web/src/editor/animation/Menubar.Examples.js @@ -0,0 +1,62 @@ +/** + * @author mrdoob / http://mrdoob.com/ + */ + +Menubar.Examples = function ( editor ) { + + var container = new UI.Panel(); + container.setClass( 'menu' ); + + var title = new UI.Panel(); + title.setClass( 'title' ); + title.setTextContent( 'Examples' ); + container.add( title ); + + var options = new UI.Panel(); + options.setClass( 'options' ); + container.add( options ); + + // Examples + + var items = [ + { title: 'HTML Colors', file: 'html_colors.json' }, + { title: 'HTML Loop', file: 'html_loop.json' }, + { title: 'Three.js Cube', file: 'threejs_cube.json' }, + { title: 'Three.js Shaders', file: 'threejs_shaders.json' } + ]; + + for ( var i = 0; i < items.length; i ++ ) { + + ( function ( i ) { + + var item = items[ i ]; + + var option = new UI.Row(); + option.setClass( 'option' ); + option.setTextContent( item.title ); + option.onClick( function () { + + if ( confirm( 'Any unsaved data will be lost. Are you sure?' ) ) { + + var request = new XMLHttpRequest(); + request.open( 'GET', '../examples/' + item.file, true ); + request.addEventListener( 'load', function ( event ) { + + editor.clear(); + editor.fromJSON( JSON.parse( event.target.responseText ) ); + + }, false ); + request.send( null ); + + } + + } ); + options.add( option ); + + } )( i ) + + } + + return container; + +}; diff --git a/ShadowEditor.Web/src/editor/animation/Menubar.File.js b/ShadowEditor.Web/src/editor/animation/Menubar.File.js new file mode 100644 index 00000000..dc46627a --- /dev/null +++ b/ShadowEditor.Web/src/editor/animation/Menubar.File.js @@ -0,0 +1,92 @@ +/** + * @author mrdoob / http://mrdoob.com/ + */ + +Menubar.File = function ( editor ) { + + var signals = editor.signals; + + var container = new UI.Panel(); + container.setClass( 'menu' ); + + var title = new UI.Panel(); + title.setClass( 'title' ); + title.setTextContent( 'File' ); + container.add( title ); + + var options = new UI.Panel(); + options.setClass( 'options' ); + container.add( options ); + + // New + + var option = new UI.Row(); + option.setClass( 'option' ); + option.setTextContent( 'New' ); + option.onClick( function () { + + if ( confirm( 'Any unsaved data will be lost. Are you sure?' ) ) { + + editor.clear(); + + } + + } ); + options.add( option ); + + // import + + var option = new UI.Panel(); + option.setClass( 'option' ); + option.setTextContent( 'Import' ); + option.onClick( Import ); + options.add( option ); + + var fileInput = document.createElement( 'input' ); + fileInput.type = 'file'; + fileInput.addEventListener( 'change', function ( event ) { + + var reader = new FileReader(); + reader.addEventListener( 'load', function ( event ) { + + editor.clear(); + editor.fromJSON( JSON.parse( event.target.result ) ); + + }, false ); + + reader.readAsText( fileInput.files[ 0 ] ); + + } ); + + function Import () { + + if ( confirm( 'Any unsaved data will be lost. Are you sure?' ) ) + fileInput.click(); + + } + + // export + + var option = new UI.Panel(); + option.setClass( 'option' ); + option.setTextContent( 'Export' ); + option.onClick( Export ); + options.add( option ); + + signals.exportState.add( Export ); + + function Export () { + + var output = JSON.stringify( editor.toJSON(), null, '\t' ); + + var blob = new Blob( [ output ], { type: 'text/plain' } ); + var objectURL = URL.createObjectURL( blob ); + + window.open( objectURL, '_blank' ); + window.focus(); + + } + + return container; + +}; diff --git a/ShadowEditor.Web/src/editor/animation/Menubar.Help.js b/ShadowEditor.Web/src/editor/animation/Menubar.Help.js new file mode 100644 index 00000000..e420f7eb --- /dev/null +++ b/ShadowEditor.Web/src/editor/animation/Menubar.Help.js @@ -0,0 +1,41 @@ +/** + * @author mrdoob / http://mrdoob.com/ + */ + +Menubar.Help = function ( editor ) { + + var signals = editor.signals; + + var container = new UI.Panel(); + container.setClass( 'menu' ); + + var title = new UI.Panel(); + title.setClass( 'title' ); + title.setTextContent( 'Help' ); + container.add( title ); + + // + + var options = new UI.Panel(); + options.setClass( 'options' ); + container.add( options ); + + // source code + + var option = new UI.Panel(); + option.setClass( 'option' ); + option.setTextContent( 'Source code' ); + option.onClick( function () { window.open( 'https://github.com/mrdoob/frame.js/tree/master/editor', '_blank' ) } ); + options.add( option ); + + // about + + var option = new UI.Panel(); + option.setClass( 'option' ); + option.setTextContent( 'About' ); + option.onClick( function () { window.open( 'http://github.com/mrdoob/frame.js/', '_blank' ) } ); + options.add( option ); + + return container; + +} diff --git a/ShadowEditor.Web/src/editor/animation/Menubar.View.js b/ShadowEditor.Web/src/editor/animation/Menubar.View.js new file mode 100644 index 00000000..4b10f438 --- /dev/null +++ b/ShadowEditor.Web/src/editor/animation/Menubar.View.js @@ -0,0 +1,35 @@ +/** + * @author mrdoob / http://mrdoob.com/ + */ + +Menubar.View = function ( editor ) { + + var container = new UI.Panel(); + container.setClass( 'menu' ); + + var title = new UI.Panel(); + title.setClass( 'title' ); + title.setTextContent( 'View' ); + container.add( title ); + + // + + var options = new UI.Panel(); + options.setClass( 'options' ); + container.add( options ); + + // remove + + var option = new UI.Panel(); + option.setClass( 'option' ); + option.setTextContent( 'Fullscreen' ); + option.onClick( function () { + + editor.signals.fullscreen.dispatch(); + + } ); + options.add( option ); + + return container; + +} diff --git a/ShadowEditor.Web/src/editor/animation/Menubar.js b/ShadowEditor.Web/src/editor/animation/Menubar.js new file mode 100644 index 00000000..f29718bb --- /dev/null +++ b/ShadowEditor.Web/src/editor/animation/Menubar.js @@ -0,0 +1,17 @@ +/** + * @author mrdoob / http://mrdoob.com/ + */ + +var Menubar = function ( editor ) { + + var container = new UI.Panel(); + container.setId( 'menubar' ); + + container.add( new Menubar.File( editor ) ); + container.add( new Menubar.Edit( editor ) ); + container.add( new Menubar.Examples( editor ) ); + container.add( new Menubar.Help( editor ) ); + + return container; + +} diff --git a/ShadowEditor.Web/src/editor/animation/Sidebar.Animation.js b/ShadowEditor.Web/src/editor/animation/Sidebar.Animation.js new file mode 100644 index 00000000..42625242 --- /dev/null +++ b/ShadowEditor.Web/src/editor/animation/Sidebar.Animation.js @@ -0,0 +1,349 @@ +/** + * @author mrdoob / http://mrdoob.com/ + */ + +Sidebar.Animation = function ( editor ) { + + var signals = editor.signals; + + var container = new UI.Panel(); + container.setId( 'animation' ); + + // + + var selected = null; + var values; + + function createParameterRow( key, parameter ) { + + if ( parameter === null ) return; + + var parameterRow = new UI.Row(); + parameterRow.add( new UI.Text( parameter.name ).setWidth( '90px' ) ); + + if ( parameter instanceof FRAME.Parameters.Boolean ) { + + var parameterValue = new UI.Checkbox() + .setValue( parameter.value ) + .onChange( function () { + + parameter.value = this.getValue(); + signals.animationModified.dispatch( selected ); + + } ); + + parameterRow.add( parameterValue ); + + values[ key ] = parameterValue; + + } else if ( parameter instanceof FRAME.Parameters.Integer ) { + + var parameterValue = new UI.Integer() + .setRange( parameter.min, parameter.max ) + .setValue( parameter.value ) + .setWidth( '150px' ) + .onChange( function () { + + parameter.value = this.getValue(); + signals.animationModified.dispatch( selected ); + + } ); + + parameterRow.add( parameterValue ); + + values[ key ] = parameterValue; + + } else if ( parameter instanceof FRAME.Parameters.Float ) { + + var parameterValue = new UI.Number() + .setRange( parameter.min, parameter.max ) + .setValue( parameter.value ) + .setWidth( '150px' ) + .onChange( function () { + + parameter.value = this.getValue(); + signals.animationModified.dispatch( selected ); + + } ); + + parameterRow.add( parameterValue ); + + values[ key ] = parameterValue; + + } else if ( parameter instanceof FRAME.Parameters.Vector2 ) { + + var vectorX = new UI.Number() + .setValue( parameter.value[ 0 ] ) + .setWidth( '50px' ) + .onChange( function () { + + parameter.value[ 0 ] = this.getValue(); + signals.animationModified.dispatch( selected ); + + } ); + + var vectorY = new UI.Number() + .setValue( parameter.value[ 1 ] ) + .setWidth( '50px' ) + .onChange( function () { + + parameter.value[ 1 ] = this.getValue(); + signals.animationModified.dispatch( selected ); + + } ); + + parameterRow.add( vectorX ); + parameterRow.add( vectorY ); + + } else if ( parameter instanceof FRAME.Parameters.Vector3 ) { + + var vectorX = new UI.Number() + .setValue( parameter.value[ 0 ] ) + .setWidth( '50px' ) + .onChange( function () { + + parameter.value[ 0 ] = this.getValue(); + signals.animationModified.dispatch( selected ); + + } ); + + var vectorY = new UI.Number() + .setValue( parameter.value[ 1 ] ) + .setWidth( '50px' ) + .onChange( function () { + + parameter.value[ 1 ] = this.getValue(); + signals.animationModified.dispatch( selected ); + + } ); + + var vectorZ = new UI.Number() + .setValue( parameter.value[ 2 ] ) + .setWidth( '50px' ) + .onChange( function () { + + parameter.value[ 2 ] = this.getValue(); + signals.animationModified.dispatch( selected ); + + } ); + + parameterRow.add( vectorX ); + parameterRow.add( vectorY ); + parameterRow.add( vectorZ ); + + } else if ( parameter instanceof FRAME.Parameters.String ) { + + var parameterValue = new UI.Input() + .setValue( parameter.value ) + .setWidth( '150px' ) + .onKeyUp( function () { + + parameter.value = this.getValue(); + signals.animationModified.dispatch( selected ); + + } ); + + parameterRow.add( parameterValue ); + + } else if ( parameter instanceof FRAME.Parameters.Color ) { + + var parameterValue = new UI.Color() + .setHexValue( parameter.value ) + .setWidth( '150px' ) + .onChange( function () { + + parameter.value = this.getHexValue(); + signals.animationModified.dispatch( selected ); + + } ); + + parameterRow.add( parameterValue ); + + } + + return parameterRow; + + } + + function build() { + + container.clear(); + + if ( selected === null ) return; + + values = {}; + + // Name + + var row = new UI.Row(); + row.add( new UI.Text( 'Name' ).setWidth( '90px' ) ); + container.add( row ); + + var animationName = new UI.Input( selected.name ) + animationName.onChange( function () { + + selected.name = this.getValue(); + signals.animationRenamed.dispatch( selected ); + + } ); + row.add( animationName ); + + // Time + + var row = new UI.Row(); + row.add( new UI.Text( 'Time' ).setWidth( '90px' ) ); + container.add( row ); + + var animationStart = new UI.Number( selected.start ).setWidth( '80px' ); + animationStart.onChange( function () { + + selected.start = this.getValue(); + signals.animationModified.dispatch( selected ); + + } ); + row.add( animationStart ); + + var animationEnd = new UI.Number( selected.end ).setWidth( '80px' ); + animationEnd.onChange( function () { + + selected.end = this.getValue(); + signals.animationModified.dispatch( selected ); + + } ); + row.add( animationEnd ); + + // Layer + + var row = new UI.Row(); + row.add( new UI.Text( 'Layer' ).setWidth( '90px' ) ); + container.add( row ); + + var animationLayer = new UI.Integer( selected.layer ).setWidth( '80px' ); + animationLayer.onChange( function () { + + selected.layer = this.getValue(); + signals.animationModified.dispatch( selected ); + + } ); + row.add( animationLayer ); + + // Enabled + + var row = new UI.Row(); + row.add( new UI.Text( 'Enabled' ).setWidth( '90px' ) ); + container.add( row ); + + var animationEnabled = new UI.Checkbox( selected.enabled ) + animationEnabled.onChange( function () { + + selected.enabled = this.getValue(); + signals.animationModified.dispatch( selected ); + + } ); + row.add( animationEnabled ); + + // + + container.add( new UI.HorizontalRule().setMargin( '20px 0px' ) ); + + // + + var row = new UI.Row(); + row.add( new UI.Text( 'Effect' ).setWidth( '90px' ) ); + container.add( row ); + + var effects = editor.effects; + var options = {}; + + for ( var i = 0; i < effects.length; i ++ ) { + + options[ i ] = effects[ i ].name; + + } + + var effectsSelect = new UI.Select().setWidth( '130px' ); + effectsSelect.setOptions( options ).setValue( effects.indexOf( selected.effect ) ); + effectsSelect.onChange( function () { + + editor.timeline.reset(); + selected.effect = editor.effects[ this.getValue() ]; + + signals.animationModified.dispatch( selected ); + + build(); + + } ); + row.add( effectsSelect ); + + var edit = new UI.Button( 'EDIT' ).setMarginLeft( '8px' ); + edit.onClick( function () { + + editor.selectEffect( selected.effect ); + + } ); + row.add( edit ); + + + var row = new UI.Row(); + row.add( new UI.Text( 'Name' ).setWidth( '90px' ) ); + container.add( row ); + + var effectName = new UI.Input( selected.effect.name ); + effectName.onChange( function () { + + selected.effect.name = this.getValue(); + signals.effectRenamed.dispatch( selected.effect ); + + } ); + row.add( effectName ); + + // + + var parameters = selected.effect.program.parameters; + + for ( var key in parameters ) { + + container.add( createParameterRow( key, parameters[ key ] ) ); + + } + + + } + + // + + signals.editorCleared.add( function () { + + selected = null; + build(); + + } ); + + signals.animationSelected.add( function ( animation ) { + + selected = animation; + build(); + + } ); + + signals.effectCompiled.add( build ); + + /* + signals.timeChanged.add( function () { + + if ( selected !== null ) { + + for ( var key in values ) { + + values[ key ].setValue( selected.module.parameters[ key ].value ); + + } + + } + + } ); + */ + + return container; + +}; diff --git a/ShadowEditor.Web/src/editor/animation/Sidebar.Project.js b/ShadowEditor.Web/src/editor/animation/Sidebar.Project.js new file mode 100644 index 00000000..ced645f2 --- /dev/null +++ b/ShadowEditor.Web/src/editor/animation/Sidebar.Project.js @@ -0,0 +1,183 @@ +/** + * @author mrdoob / http://mrdoob.com/ + */ + +Sidebar.Project = function ( editor ) { + + var signals = editor.signals; + + var container = new UI.Panel(); + container.setId( 'project' ); + + // Libraries + + container.add( new UI.Text( 'Libraries' ).setTextTransform( 'uppercase' ) ); + container.add( new UI.Break(), new UI.Break() ); + + var libraries = new UI.Select().setMultiple( true ).setWidth( '280px' ); + container.add( libraries ); + + container.add( new UI.Break(), new UI.Break() ); + + // Effects + + container.add( new UI.Text( 'Effects' ).setTextTransform( 'uppercase' ) ); + container.add( new UI.Break(), new UI.Break() ); + + var effects = new UI.Select().setMultiple( true ).setWidth( '280px' ).setMarginBottom( '8px' ); + container.add( effects ); + + var cleanEffects = new UI.Button( 'Clean Effects' ); + cleanEffects.onClick( function () { + + editor.cleanEffects(); + + } ); + container.add( cleanEffects ); + + container.add( new UI.Break(), new UI.Break() ); + + // Scripts + + container.add( new UI.Text( 'Scripts' ).setTextTransform( 'uppercase' ) ); + container.add( new UI.Break(), new UI.Break() ); + + var includesContainer = new UI.Row(); + container.add( includesContainer ); + + var newInclude = new UI.Button( 'New' ); + newInclude.onClick( function () { + + editor.addInclude( 'Name', '' ); + + update(); + + } ); + container.add( newInclude ); + + var reload = new UI.Button( 'Reload Scripts' ); + reload.onClick( function () { + + editor.reloadIncludes(); + + var effects = editor.effects; + + for ( var j = 0; j < effects.length; j++ ) { + + var effect = effects[ j ]; + editor.compileEffect( effect ); + + } + + editor.timeline.reset(); + editor.timeline.update( editor.player.currentTime ); + + } ); + reload.setMarginLeft( '4px' ); + container.add( reload ); + + container.add( new UI.Break(), new UI.Break() ); + + // + + function buildInclude( id ) { + + var include = editor.includes[ id ]; + + var span = new UI.Span(); + + var name = new UI.Input( include.name ).setWidth( '130px' ).setFontSize( '12px' ); + name.onChange( function () { + + include.name = this.getValue(); + + } ); + span.add( name ); + + var edit = new UI.Button( 'Edit' ); + edit.setMarginLeft( '4px' ); + edit.onClick( function () { + + editor.selectInclude( include ); + + } ); + span.add( edit ); + + var remove = new UI.Button( 'Remove' ); + remove.setMarginLeft( '4px' ); + remove.onClick( function () { + + if ( confirm( 'Are you sure?' ) ) { + + editor.removeInclude( include ); + + } + + } ); + span.add( remove ); + + return span; + + } + + // + + function update() { + + updateLibraries(); + updateEffects(); + updateScripts(); + + } + + function updateLibraries() { + + libraries.setOptions( editor.libraries ); + libraries.dom.size = editor.libraries.length; + + } + + function updateEffects() { + + var names = []; + + for ( var i = 0; i < editor.effects.length; i ++ ) { + + names.push( editor.effects[ i ].name ); + + } + + effects.setOptions( names ); + effects.dom.size = editor.effects.length; + + } + + function updateScripts() { + + includesContainer.clear(); + + var includes = editor.includes; + + for ( var i = 0; i < includes.length; i ++ ) { + + includesContainer.add( buildInclude( i ) ); + + } + + } + + // signals + + signals.editorCleared.add( update ); + + signals.libraryAdded.add( updateLibraries ); + + signals.effectAdded.add( updateEffects ); + signals.effectRemoved.add( updateEffects ); + + signals.includeAdded.add( updateScripts ); + signals.includeRemoved.add( updateScripts ); + + return container; + +}; diff --git a/ShadowEditor.Web/src/editor/animation/Sidebar.Settings.js b/ShadowEditor.Web/src/editor/animation/Sidebar.Settings.js new file mode 100644 index 00000000..1946f9f9 --- /dev/null +++ b/ShadowEditor.Web/src/editor/animation/Sidebar.Settings.js @@ -0,0 +1,12 @@ +/** + * @author mrdoob / http://mrdoob.com/ + */ + +Sidebar.Settings = function ( editor ) { + + var container = new UI.Panel(); + container.setId( 'settings' ); + + return container; + +}; diff --git a/ShadowEditor.Web/src/editor/animation/Sidebar.js b/ShadowEditor.Web/src/editor/animation/Sidebar.js new file mode 100644 index 00000000..bc4e514b --- /dev/null +++ b/ShadowEditor.Web/src/editor/animation/Sidebar.js @@ -0,0 +1,80 @@ +/** + * @author mrdoob / http://mrdoob.com/ + */ + +var Sidebar = function ( editor ) { + + var container = new UI.Panel(); + container.setId( 'sidebar' ); + + // + + var animationTab = new UI.Text( 'ANIMATION' ).onClick( onClick ); + var projectTab = new UI.Text( 'PROJECT' ).onClick( onClick ); + // var settingsTab = new UI.Text( 'SETTINGS' ).onClick( onClick ); + + var tabs = new UI.Div(); + tabs.setId( 'tabs' ); + tabs.add( animationTab, projectTab/*, settingsTab*/ ); + container.add( tabs ); + + function onClick( event ) { + + select( event.target.textContent ); + + } + + // + + var animation = new UI.Span().add( + new Sidebar.Animation( editor ) + ); + container.add( animation ); + + var project = new UI.Span().add( + new Sidebar.Project( editor ) + ); + container.add( project ); + /* + var settings = new UI.Span().add( + new Sidebar.Settings( editor ) + ); + container.add( settings ); + */ + + // + + function select( section ) { + + animationTab.setClass( '' ); + projectTab.setClass( '' ); + // settingsTab.setClass( '' ); + + animation.setDisplay( 'none' ); + project.setDisplay( 'none' ); + // settings.setDisplay( 'none' ); + + switch ( section ) { + case 'ANIMATION': + animationTab.setClass( 'selected' ); + animation.setDisplay( '' ); + break; + case 'PROJECT': + projectTab.setClass( 'selected' ); + project.setDisplay( '' ); + break; + /* + case 'SETTINGS': + settingsTab.setClass( 'selected' ); + settings.setDisplay( '' ); + break; + */ + } + + } + + select( 'ANIMATION' ); + + return container; + +}; diff --git a/ShadowEditor.Web/src/editor/animation/Timeline.Animations.js b/ShadowEditor.Web/src/editor/animation/Timeline.Animations.js new file mode 100644 index 00000000..f3a3677d --- /dev/null +++ b/ShadowEditor.Web/src/editor/animation/Timeline.Animations.js @@ -0,0 +1,305 @@ +/** + * @author mrdoob / http://mrdoob.com/ + */ + +Timeline.Animations = function ( editor ) { + + var signals = editor.signals; + + var container = new UI.Panel(); + container.setHeight( '100%' ); + container.setBackground( 'linear-gradient(#444 1px, transparent 1px) 0% 0% / 32px 32px repeat' ); + + var scale = 32; + + var Block = ( function ( animation ) { + + var scope = this; + + var dom = document.createElement( 'div' ); + dom.className = 'block'; + dom.style.position = 'absolute'; + dom.style.height = '30px'; + dom.addEventListener( 'click', function ( event ) { + + editor.selectAnimation( animation ); + + } ); + dom.addEventListener( 'mousedown', function ( event ) { + + var movementX = 0; + var movementY = 0; + + function onMouseMove( event ) { + + movementX = event.movementX | event.webkitMovementX | event.mozMovementX | 0; + + animation.start += movementX / scale; + animation.end += movementX / scale; + + if ( animation.start < 0 ) { + + var offset = - animation.start; + + animation.start += offset; + animation.end += offset; + + } + + movementY += event.movementY | event.webkitMovementY | event.mozMovementY | 0; + + if ( movementY >= 30 ) { + + animation.layer = animation.layer + 1; + movementY = 0; + + } + + if ( movementY <= -30 ) { + + animation.layer = Math.max( 0, animation.layer - 1 ); + movementY = 0; + + } + + signals.animationModified.dispatch( animation ); + + } + + function onMouseUp( event ) { + + document.removeEventListener( 'mousemove', onMouseMove ); + document.removeEventListener( 'mouseup', onMouseUp ); + + } + + document.addEventListener( 'mousemove', onMouseMove, false ); + document.addEventListener( 'mouseup', onMouseUp, false ); + + }, false ); + + var resizeLeft = document.createElement( 'div' ); + resizeLeft.style.position = 'absolute'; + resizeLeft.style.width = '6px'; + resizeLeft.style.height = '30px'; + resizeLeft.style.cursor = 'w-resize'; + resizeLeft.addEventListener( 'mousedown', function ( event ) { + + event.stopPropagation(); + + var movementX = 0; + + function onMouseMove( event ) { + + movementX = event.movementX | event.webkitMovementX | event.mozMovementX | 0; + + animation.start += movementX / scale; + + signals.animationModified.dispatch( animation ); + + } + + function onMouseUp( event ) { + + if ( Math.abs( movementX ) < 2 ) { + + editor.selectAnimation( animation ); + + } + + document.removeEventListener( 'mousemove', onMouseMove ); + document.removeEventListener( 'mouseup', onMouseUp ); + + } + + document.addEventListener( 'mousemove', onMouseMove, false ); + document.addEventListener( 'mouseup', onMouseUp, false ); + + }, false ); + dom.appendChild( resizeLeft ); + + var name = document.createElement( 'div' ); + name.className = 'name'; + dom.appendChild( name ); + + var resizeRight = document.createElement( 'div' ); + resizeRight.style.position = 'absolute'; + resizeRight.style.right = '0px'; + resizeRight.style.top = '0px'; + resizeRight.style.width = '6px'; + resizeRight.style.height = '30px'; + resizeRight.style.cursor = 'e-resize'; + resizeRight.addEventListener( 'mousedown', function ( event ) { + + event.stopPropagation(); + + var movementX = 0; + + function onMouseMove( event ) { + + movementX = event.movementX | event.webkitMovementX | event.mozMovementX | 0; + + animation.end += movementX / scale; + + signals.animationModified.dispatch( animation ); + + } + + function onMouseUp( event ) { + + if ( Math.abs( movementX ) < 2 ) { + + editor.selectAnimation( animation ); + + } + + document.removeEventListener( 'mousemove', onMouseMove ); + document.removeEventListener( 'mouseup', onMouseUp ); + + } + + document.addEventListener( 'mousemove', onMouseMove, false ); + document.addEventListener( 'mouseup', onMouseUp, false ); + + }, false ); + dom.appendChild( resizeRight ); + + // + + function getAnimation() { + + return animation; + + } + + function select() { + + dom.classList.add( 'selected' ); + + } + + function deselect() { + + dom.classList.remove( 'selected' ); + + } + + function update() { + + animation.enabled === false ? dom.classList.add( 'disabled' ) : dom.classList.remove( 'disabled' ); + + dom.style.left = ( animation.start * scale ) + 'px'; + dom.style.top = ( animation.layer * 32 ) + 'px'; + dom.style.width = ( ( animation.end - animation.start ) * scale - 2 ) + 'px'; + + name.innerHTML = animation.name + ' ' + animation.effect.name + ''; + + } + + update(); + + return { + dom: dom, + getAnimation: getAnimation, + select: select, + deselect: deselect, + update: update + }; + + } ); + + container.dom.addEventListener( 'dblclick', function ( event ) { + + var start = event.offsetX / scale; + var end = start + 2; + var layer = Math.floor( event.offsetY / 32 ); + + var effect = new FRAME.Effect( 'Effect' ); + editor.addEffect( effect ); + + var animation = new FRAME.Animation( 'Animation', start, end, layer, effect ); + editor.addAnimation( animation ); + + } ); + + // signals + + var blocks = {}; + var selected = null; + + signals.animationAdded.add( function ( animation ) { + + var block = new Block( animation ); + container.dom.appendChild( block.dom ); + + blocks[ animation.id ] = block; + + } ); + + signals.animationModified.add( function ( animation ) { + + blocks[ animation.id ].update(); + + } ); + + signals.animationSelected.add( function ( animation ) { + + if ( blocks[ selected ] !== undefined ) { + + blocks[ selected ].deselect(); + + } + + if ( animation === null ) return; + + selected = animation.id; + blocks[ selected ].select(); + + } ); + + signals.animationRemoved.add( function ( animation ) { + + var block = blocks[ animation.id ]; + container.dom.removeChild( block.dom ); + + delete blocks[ animation.id ]; + + } ); + + signals.timelineScaled.add( function ( value ) { + + scale = value; + + for ( var key in blocks ) { + + blocks[ key ].update(); + + } + + } ); + + signals.animationRenamed.add( function ( animation ) { + + blocks[ animation.id ].update(); + + } ); + + signals.effectRenamed.add( function ( effect ) { + + for ( var key in blocks ) { + + var block = blocks[ key ]; + + if ( block.getAnimation().effect === effect ) { + + block.update(); + + } + + } + + } ); + + return container; + +}; diff --git a/ShadowEditor.Web/src/editor/animation/Timeline.Curves.js b/ShadowEditor.Web/src/editor/animation/Timeline.Curves.js new file mode 100644 index 00000000..0332b750 --- /dev/null +++ b/ShadowEditor.Web/src/editor/animation/Timeline.Curves.js @@ -0,0 +1,72 @@ +/** + * @author mrdoob / http://mrdoob.com/ + */ + +Timeline.Curves = function ( editor ) { + + var signals = editor.signals; + + var container = new UI.Panel(); + + var selected = null; + var scale = 32; + + var svg = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' ); + svg.style.position = 'fixed'; + svg.setAttribute( 'width', 2048 ); + svg.setAttribute( 'height', 128 ); + container.dom.appendChild( svg ); + + var path = document.createElementNS( 'http://www.w3.org/2000/svg', 'path' ); + path.setAttribute( 'style', 'stroke: #444444; stroke-width: 1px; fill: none;' ); + path.setAttribute( 'd', 'M 0 64 2048 65'); + svg.appendChild( path ); + + var path = document.createElementNS( 'http://www.w3.org/2000/svg', 'path' ); + path.setAttribute( 'style', 'stroke: #00ff00; stroke-width: 1px; fill: none;' ); + svg.appendChild( path ); + + function drawCurve() { + + /* + var curve = selected; + var drawing = ''; + + for ( var i = 0; i <= 2048; i ++ ) { + + curve.update( i / scale ); + + drawing += ( i === 0 ? 'M' : 'L' ) + i + ' ' + ( ( 1 - curve.value ) * 64 ) + ' '; + + } + + path.setAttribute( 'd', drawing ); + */ + + } + + // signals + + signals.curveAdded.add( function ( curve ) { + + if ( curve instanceof FRAME.Curves.Saw ) { + + selected = curve; + + drawCurve(); + + } + + } ); + + signals.timelineScaled.add( function ( value ) { + + scale = value; + + drawCurve(); + + } ); + + return container; + +}; diff --git a/ShadowEditor.Web/src/editor/animation/Timeline.js b/ShadowEditor.Web/src/editor/animation/Timeline.js new file mode 100644 index 00000000..f84eba7d --- /dev/null +++ b/ShadowEditor.Web/src/editor/animation/Timeline.js @@ -0,0 +1,263 @@ +/** + * @author mrdoob / http://mrdoob.com/ + */ + +var Timeline = function ( editor ) { + + var signals = editor.signals; + var player = editor.player; + + var container = new UI.Panel(); + container.setId( 'timeline' ); + + // controls + + /* + var buttons = new UI.Div(); + buttons.setPosition( 'absolute' ); + buttons.setTop( '5px' ); + buttons.setRight( '5px' ); + controls.add( buttons ); + + var button = new UI.Button(); + button.setLabel( 'ANIMATIONS' ); + button.onClick( function () { + + elements.setDisplay( '' ); + curves.setDisplay( 'none' ); + + } ); + buttons.add( button ); + + var button = new UI.Button(); + button.setLabel( 'CURVES' ); + button.setMarginLeft( '4px' ); + button.onClick( function () { + + scroller.style.background = ''; + + elements.setDisplay( 'none' ); + curves.setDisplay( '' ); + + } ); + buttons.add( button ); + */ + + // timeline + + var keysDown = {}; + document.addEventListener( 'keydown', function ( event ) { keysDown[ event.keyCode ] = true; } ); + document.addEventListener( 'keyup', function ( event ) { keysDown[ event.keyCode ] = false; } ); + + var scale = 32; + var prevScale = scale; + + var timeline = new UI.Panel(); + timeline.setPosition( 'absolute' ); + timeline.setTop( '0px' ); + timeline.setBottom( '0px' ); + timeline.setWidth( '100%' ); + timeline.setOverflow( 'hidden' ); + timeline.dom.addEventListener( 'wheel', function ( event ) { + + if ( event.altKey === true ) { + + event.preventDefault(); + + scale = Math.max( 2, scale + ( event.deltaY / 10 ) ); + + signals.timelineScaled.dispatch( scale ); + + } + + } ); + container.add( timeline ); + + var canvas = document.createElement( 'canvas' ); + canvas.height = 32; + canvas.style.position = 'absolute'; + canvas.addEventListener( 'mousedown', function ( event ) { + + event.preventDefault(); + + function onMouseMove( event ) { + + editor.setTime( ( event.offsetX + scroller.scrollLeft ) / scale ); + + } + + function onMouseUp( event ) { + + onMouseMove( event ); + + document.removeEventListener( 'mousemove', onMouseMove ); + document.removeEventListener( 'mouseup', onMouseUp ); + + } + + document.addEventListener( 'mousemove', onMouseMove, false ); + document.addEventListener( 'mouseup', onMouseUp, false ); + + }, false ); + timeline.dom.appendChild( canvas ); + + function updateMarks() { + + canvas.width = scroller.clientWidth; + + var context = canvas.getContext( '2d', { alpha: false } ); + + context.fillStyle = '#555'; + context.fillRect( 0, 0, canvas.width, canvas.height ); + + context.strokeStyle = '#888'; + context.beginPath(); + + context.translate( - scroller.scrollLeft, 0 ); + + var duration = editor.duration; + var width = duration * scale; + var scale4 = scale / 4; + + for ( var i = 0.5; i <= width; i += scale ) { + + context.moveTo( i + ( scale4 * 0 ), 18 ); context.lineTo( i + ( scale4 * 0 ), 26 ); + + if ( scale > 16 ) context.moveTo( i + ( scale4 * 1 ), 22 ), context.lineTo( i + ( scale4 * 1 ), 26 ); + if ( scale > 8 ) context.moveTo( i + ( scale4 * 2 ), 22 ), context.lineTo( i + ( scale4 * 2 ), 26 ); + if ( scale > 16 ) context.moveTo( i + ( scale4 * 3 ), 22 ), context.lineTo( i + ( scale4 * 3 ), 26 ); + + } + + context.stroke(); + + context.font = '10px Arial'; + context.fillStyle = '#888' + context.textAlign = 'center'; + + var step = Math.max( 1, Math.floor( 64 / scale ) ); + + for ( var i = 0; i < duration; i += step ) { + + var minute = Math.floor( i / 60 ); + var second = Math.floor( i % 60 ); + + var text = ( minute > 0 ? minute + ':' : '' ) + ( '0' + second ).slice( - 2 ); + + context.fillText( text, i * scale, 13 ); + + } + + } + + var scroller = document.createElement( 'div' ); + scroller.style.position = 'absolute'; + scroller.style.top = '32px'; + scroller.style.bottom = '0px'; + scroller.style.width = '100%'; + scroller.style.overflow = 'auto'; + scroller.addEventListener( 'scroll', function ( event ) { + + updateMarks(); + updateTimeMark(); + + }, false ); + timeline.dom.appendChild( scroller ); + + var elements = new Timeline.Animations( editor ); + scroller.appendChild( elements.dom ); + + /* + var curves = new Timeline.Curves( editor ); + curves.setDisplay( 'none' ); + scroller.appendChild( curves.dom ); + */ + + function updateContainers() { + + var width = editor.duration * scale; + + elements.setWidth( width + 'px' ); + // curves.setWidth( width + 'px' ); + + } + + // + + var loopMark = document.createElement( 'div' ); + loopMark.style.position = 'absolute'; + loopMark.style.top = 0; + loopMark.style.height = 100 + '%'; + loopMark.style.width = 0; + loopMark.style.background = 'rgba( 255, 255, 255, 0.1 )'; + loopMark.style.pointerEvents = 'none'; + loopMark.style.display = 'none'; + timeline.dom.appendChild( loopMark ); + + var timeMark = document.createElement( 'div' ); + timeMark.style.position = 'absolute'; + timeMark.style.top = '0px'; + timeMark.style.left = '-8px'; + timeMark.style.width = '16px'; + timeMark.style.height = '100%'; + timeMark.style.background = 'linear-gradient(90deg, transparent 8px, #f00 8px, #f00 9px, transparent 9px) 0% 0% / 16px 16px repeat-y'; + timeMark.style.pointerEvents = 'none'; + timeline.dom.appendChild( timeMark ); + + function updateTimeMark() { + + timeMark.style.left = ( player.currentTime * scale ) - scroller.scrollLeft - 8 + 'px'; + + // TODO Optimise this + + var loop = player.getLoop(); + + if ( Array.isArray( loop ) ) { + + var loopStart = loop[ 0 ] * scale; + var loopEnd = loop[ 1 ] * scale; + + loopMark.style.display = ''; + loopMark.style.left = ( loopStart - scroller.scrollLeft ) + 'px'; + loopMark.style.width = ( loopEnd - loopStart ) + 'px'; + + } else { + + loopMark.style.display = 'none'; + + } + + } + + // signals + + signals.timeChanged.add( function () { + + updateTimeMark(); + + } ); + + signals.timelineScaled.add( function ( value ) { + + scale = value; + + scroller.scrollLeft = ( scroller.scrollLeft * value ) / prevScale; + + updateMarks(); + updateTimeMark(); + updateContainers(); + + prevScale = value; + + } ); + + signals.windowResized.add( function () { + + updateMarks(); + updateContainers(); + + } ); + + return container; + +}; diff --git a/ShadowEditor.Web/src/editor/animation/Viewport.js b/ShadowEditor.Web/src/editor/animation/Viewport.js new file mode 100644 index 00000000..03ad1303 --- /dev/null +++ b/ShadowEditor.Web/src/editor/animation/Viewport.js @@ -0,0 +1,43 @@ +/** + * @author mrdoob / http://mrdoob.com/ + */ + +var Viewport = function ( editor ) { + + var scope = this; + var signals = editor.signals; + + var container = this.container = new UI.Panel(); + container.setId( 'viewport' ); + + editor.resources.set( 'dom', container.dom ); + + editor.signals.fullscreen.add( function () { + + var element = container.dom.firstChild; + + if ( element.requestFullscreen ) element.requestFullscreen(); + if ( element.msRequestFullscreen ) element.msRequestFullscreen(); + if ( element.mozRequestFullScreen ) element.mozRequestFullScreen(); + if ( element.webkitRequestFullscreen ) element.webkitRequestFullscreen(); + + } ); + + function clear () { + + var dom = container.dom; + + while ( dom.children.length ) { + + dom.removeChild( dom.lastChild ); + + } + + } + + signals.editorCleared.add( clear ); + signals.includesCleared.add( clear ); + + return container; + +};