├── .gitignore ├── README.md ├── gulpfile.js ├── package.json ├── project.json ├── project.sublime-project └── src ├── assets └── .gitkeep ├── gulp ├── config.js ├── tasks │ ├── assets.js │ ├── build.js │ ├── clean.js │ ├── html.js │ ├── scripts.js │ ├── server.js │ ├── styles.js │ └── watch.js └── utils │ └── on-error.js ├── index.html ├── scripts ├── .gitkeep └── app.js ├── styles ├── .gitkeep └── styles.scss └── vendors └── TrackballControls.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .sass-cache/ 3 | /build/ 4 | node_modules/ 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Playground-starter 2 | 3 | A quick starter for lab projects. 4 | Including 5 | * [Gulp](http://gulpjs.com/) 6 | * [Sass](http://sass-lang.com/) 7 | * [GSAP](http://greensock.com/gsap) 8 | * [Browserify](http://browserify.org/) 9 | * [Browsersync](https://www.browsersync.io/) 10 | * [Babel - ES6](https://babeljs.io/) 11 | 12 | ## Before everything 13 | - You'll need [Node](https://nodejs.org/) (which includes NPM). 14 | - Install Gulp using `npm install -g gulp`. This installs Gulp globally and is needed later. 15 | - Clone this repo to your local computer 16 | - Edit project.json with your datas 17 | - Install the nodes modules 18 | ```shell 19 | $ npm install 20 | ``` 21 | ## Run the project 22 | 23 | There is three kind of environments available : `dev`, `staging`, `live`. To change the config of each environement you need to edit the __project.json__ file. 24 | 25 | - Build and watch the app with __development configs__ 26 | ```shell 27 | $ npm start 28 | ``` 29 | * Build app with __staging config__ 30 | ```shell 31 | $ npm run staging 32 | ``` 33 | * Build app with __live configs__ 34 | ```shell 35 | $ npm run live 36 | ``` 37 | 38 | ### License 39 | 40 | MIT 41 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var project = require('./project.json'); 4 | var path = require('path'); 5 | var fs = require('fs'); 6 | 7 | // Dynamically start all tasks 8 | fs.readdirSync('./' + project.foldersName.entry + '/gulp/tasks/') 9 | .forEach(function(task) { 10 | if (path.extname(task) !== '.js') return; 11 | require('./' + project.foldersName.entry + '/gulp/tasks/' + task); 12 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground-starter", 3 | "version": "1.0.0", 4 | "description": "simple project starter", 5 | "main": "gulpfile.js", 6 | "author": "Corentin Fardeau - fardeaucorentin@gmail.com", 7 | "license": "MIT", 8 | "scripts": { 9 | "start": "npm run server", 10 | "server": "gulp server --watch", 11 | "staging": "gulp server --staging", 12 | "live": "gulp server --live" 13 | }, 14 | "dependencies": { 15 | "events": "^1.1.1", 16 | "fastclick": "^1.0.6", 17 | "geo-arc": "^1.1.2", 18 | "glsl-noise": "0.0.0", 19 | "gsap": "^1.18.5", 20 | "jquery": "^2.1.4", 21 | "nk-css-reset": "^1.0.1", 22 | "nk-css-utils": "^2.0.0", 23 | "size": "watsondg/size", 24 | "sniffer": "watsondg/sniffer", 25 | "three": "^0.80.1", 26 | "underscore": "^1.8.3" 27 | }, 28 | "devDependencies": { 29 | "babel-preset-es2015": "^6.9.0", 30 | "babelify": "^7.3.0", 31 | "browser-sync": "2.6.5", 32 | "browserify": "^11.0.1", 33 | "browserify-shim": "^3.8.10", 34 | "colors": "^1.1.2", 35 | "del": "^2.0.0", 36 | "gulp": "^3.9.1", 37 | "gulp-autoprefixer": "3.1.0", 38 | "gulp-clean-css": "2.0.11", 39 | "gulp-concat": "2.5.2", 40 | "gulp-filter": "^3.0.1", 41 | "gulp-imagemin": "^2.3.0", 42 | "gulp-notify": "^2.2.0", 43 | "gulp-plumber": "1.0.0", 44 | "gulp-rename": "1.2.2", 45 | "gulp-sass": "2.3.2", 46 | "gulp-sourcemaps": "^1.6.0", 47 | "gulp-template": "^4.0.0", 48 | "gulp-uglify": "^1.5.4", 49 | "gulp-util": "^3.0.6", 50 | "lodash.assign": "^3.2.0", 51 | "minimist": "^1.1.2", 52 | "rimraf": "^2.4.2", 53 | "vinyl-buffer": "^1.0.0", 54 | "vinyl-source-stream": "^1.1.0", 55 | "watchify": "^3.3.1" 56 | }, 57 | "browserify": { 58 | "transform": [ 59 | "browserify-shim" 60 | ] 61 | }, 62 | "browserify-shim": { 63 | "timelinemax": "global:TimelineMax", 64 | "tweenmax": "global:TweenMax", 65 | "jquery": "$", 66 | "underscore": "_" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /project.json: -------------------------------------------------------------------------------- 1 | { 2 | "foldersName": { 3 | "entry" : "src", 4 | "build" : "build" 5 | }, 6 | "env": { 7 | "dev": { 8 | "siteURL": "", 9 | "baseURL": "/", 10 | "scriptURL": "/", 11 | "assetsURL": "/assets/", 12 | "env" : "dev" 13 | }, 14 | "staging": { 15 | "siteURL": "", 16 | "baseURL": "/", 17 | "scriptURL": "/", 18 | "assetsURL": "/assets/", 19 | "env" : "staging" 20 | }, 21 | "live": { 22 | "siteURL": "", 23 | "baseURL": "/three-js-audio/", 24 | "scriptURL": "/three-js-flag/", 25 | "assetsURL": "/three-js-flag/assets/", 26 | "env" : "live" 27 | } 28 | }, 29 | "port": 3000 30 | } 31 | -------------------------------------------------------------------------------- /project.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "path": "." 6 | } 7 | ], 8 | "settings": 9 | { 10 | "tab_size": 4 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Corentinfardeau/three-js-flag/90ddf7649fa342d031ee47c98a8f807c29bd82d1/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/gulp/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var project = require('../../project.json'), 4 | flags = require('minimist')(process.argv.slice(2)); 5 | 6 | var envOpts = {}; 7 | 8 | if (flags.watch) { 9 | envOpts.watch = true; 10 | envOpts.debug = true; 11 | envOpts.minify = false; 12 | envOpts.env = 'dev'; 13 | } 14 | 15 | if (flags.staging) { 16 | envOpts.watch = false; 17 | envOpts.debug = false; 18 | envOpts.minify = true; 19 | envOpts.env = 'staging'; 20 | } 21 | 22 | if (flags.live) { 23 | envOpts.watch = false; 24 | envOpts.debug = false; 25 | envOpts.minify = true; 26 | envOpts.env = 'live'; 27 | } 28 | 29 | var options = { 30 | 31 | cssEntry : project.foldersName.entry + '/styles/', 32 | cssDest : project.foldersName.build + '/styles/', 33 | jsEntry : project.foldersName.entry + '/scripts/', 34 | jsDest : project.foldersName.build + '/scripts/', 35 | jsVendorsEntry : project.foldersName.entry + '/vendors/', 36 | mainJS : 'app.js', 37 | imageEntry : project.foldersName.entry + '/assets/images/', 38 | imageDest : project.foldersName.build + '/assets/images/', 39 | copyBase : project.foldersName.entry + '/assets/', 40 | copyDest : project.foldersName.build + '/assets/', 41 | jsVendorsDest : project.foldersName.build + '/vendors/', 42 | envOpts : envOpts 43 | 44 | } 45 | 46 | module.exports = options; -------------------------------------------------------------------------------- /src/gulp/tasks/assets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'), 4 | browserSync = require('browser-sync'), 5 | config = require('../config'); 6 | 7 | gulp.task('assets', function(){ 8 | return gulp.src(config.copyBase + '**/*') 9 | .pipe(gulp.dest(config.copyDest)) 10 | .pipe(browserSync.reload({stream:true})); 11 | }); 12 | 13 | gulp.task('vendors', function(){ 14 | return gulp.src(config.jsVendorsEntry + '**/*') 15 | .pipe(gulp.dest(config.jsVendorsDest)) 16 | .pipe(browserSync.reload({stream:true})); 17 | }); 18 | -------------------------------------------------------------------------------- /src/gulp/tasks/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'), 4 | uglify = require('gulp-uglify'), 5 | cleanCSS = require('gulp-clean-css'), 6 | sourcemaps = require('gulp-sourcemaps'), 7 | config = require('../config'); 8 | 9 | gulp.task('build', ['clean', 'assets', 'vendors', 'html', 'scripts', 'styles'], function(cb) { 10 | 11 | if (config.envOpts.minify) { 12 | 13 | gulp.src(config.jsDest + '/' + config.mainJS) 14 | .pipe(uglify({ 15 | sequences: true, 16 | dead_code: true, 17 | conditionals: true, 18 | booleans: true, 19 | unused: true, 20 | if_return: true, 21 | join_vars: true 22 | })) 23 | .pipe(gulp.dest(config.jsDest)); 24 | 25 | gulp.src(config.cssDest + 'styles.css') 26 | .pipe(sourcemaps.init()) 27 | .pipe(cleanCSS({ 28 | rebase: false, 29 | advanced: false, 30 | aggressiveMerging: false, 31 | mediaMerging: true, // pack media queries 32 | processImport: true, // automatically embed @import rules 33 | })) 34 | .pipe(sourcemaps.write('./')) 35 | .pipe(gulp.dest(config.cssDest + 'styles.css')); 36 | 37 | } 38 | 39 | cb(); 40 | 41 | }); 42 | -------------------------------------------------------------------------------- /src/gulp/tasks/clean.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var rm = require('rimraf'), 4 | project = require('../../../project.json'); 5 | 6 | require('gulp').task('clean', function(cb) { 7 | rm.sync(project.foldersName.build); 8 | cb(); 9 | }); -------------------------------------------------------------------------------- /src/gulp/tasks/html.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'), 4 | browserSync = require('browser-sync'), 5 | template = require('gulp-template'), 6 | project = require('../../../project.json'), 7 | config = require('../config'); 8 | 9 | var paths = project.env[config.envOpts.env]; 10 | 11 | gulp.task('html', function(){ 12 | 13 | gulp.src(project.foldersName.entry + '/**/*.html') 14 | .pipe(template(paths)) 15 | .pipe(gulp.dest('./' + project.foldersName.build + '/')) 16 | .pipe(browserSync.reload({stream:true})); 17 | }); 18 | -------------------------------------------------------------------------------- /src/gulp/tasks/scripts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'), 4 | buffer = require('vinyl-buffer'), 5 | uglify = require('gulp-uglify'), 6 | watchify = require('watchify'), 7 | source = require('vinyl-source-stream'), 8 | sourcemaps = require('gulp-sourcemaps'), 9 | assign = require('lodash.assign'), 10 | browserify = require('browserify'), 11 | rename = require('gulp-rename'), 12 | browserSync = require('browser-sync'), 13 | babel = require('babelify'), 14 | config = require('../config.js'), 15 | onError = require('../utils/on-error'); 16 | 17 | var customOpts = { 18 | entries: [config.jsEntry + '/' + config.mainJS], 19 | debug: config.envOpts.debug 20 | }; 21 | 22 | var opts = assign({}, watchify.args, customOpts); 23 | var b = watchify(browserify(opts).transform("babelify", {presets: ["es2015"]})); 24 | 25 | gulp.task('scripts', bundle); 26 | b.on('update', bundle); 27 | 28 | function bundle() { 29 | return b.bundle() 30 | .on('error', onError) 31 | .pipe(source(config.mainJS)) 32 | .pipe(buffer()) 33 | .pipe(gulp.dest(config.jsDest)) 34 | .pipe(sourcemaps.init({loadMaps: true})) 35 | .pipe(sourcemaps.write('./')) 36 | .pipe(gulp.dest(config.jsDest)) 37 | .pipe(browserSync.reload({stream:true})) 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/gulp/tasks/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'), 4 | browserSync = require('browser-sync'), 5 | project = require('../../../project.json'), 6 | config = require('../config'), 7 | colors = require('colors'), 8 | notify = require('gulp-notify'); 9 | 10 | var serverConfig = { 11 | server: "./" + project.foldersName.build, 12 | port: project.port || 3000, 13 | browser: ['google chrome'], 14 | notify: false, 15 | minify: false, 16 | ghostMode: false, 17 | } 18 | 19 | gulp.task('server', ['build'], function(cb) { 20 | 21 | if(config.envOpts.watch){ 22 | browserSync.init(serverConfig); 23 | gulp.start('watch'); 24 | }else{ 25 | cb() 26 | } 27 | 28 | switch(config.envOpts.env){ 29 | case 'dev' : 30 | console.log('PROJECT BUILT IN DEV ENVIRONEMENT'.green); 31 | break; 32 | case 'staging' : 33 | console.log('PROJECT BUILT IN STAGING ENVIRONEMENT'.green); 34 | process.exit(); 35 | break; 36 | case 'live' : 37 | console.log('PROJECT BUILT IN LIVE ENVIRONEMENT'.green); 38 | process.exit(); 39 | break; 40 | } 41 | 42 | 43 | 44 | }); 45 | -------------------------------------------------------------------------------- /src/gulp/tasks/styles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'), 4 | browserSync = require('browser-sync'), 5 | autoprefixer = require('gulp-autoprefixer'), 6 | sass = require('gulp-sass'), 7 | config = require('../config'); 8 | 9 | gulp.task('styles', function(){ 10 | gulp.src([config.cssEntry + '**/*.scss']) 11 | .pipe(sass()) 12 | .pipe(autoprefixer('last 2 versions')) 13 | .pipe(gulp.dest(config.cssDest)) 14 | .pipe(browserSync.reload({stream:true})) 15 | }); -------------------------------------------------------------------------------- /src/gulp/tasks/watch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'), 4 | watch = require('watchify'), 5 | project = require('../../../project.json'), 6 | config = require('../config'); 7 | 8 | gulp.task('watch', function() { 9 | 10 | gulp.watch(config.cssEntry +"**/*.scss", ['styles']); 11 | gulp.watch(project.foldersName.entry + "/**/*.html", ['html']); 12 | gulp.watch(project.foldersName.entry + "/assets/**/*", ['assets']); 13 | 14 | }); -------------------------------------------------------------------------------- /src/gulp/utils/on-error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var notify = require('gulp-notify'); 4 | 5 | module.exports = function() { 6 | var args = Array.prototype.slice.call(arguments); 7 | 8 | // Send error to notification center with gulp-notify 9 | notify.onError({ 10 | title: 'Compile Error', 11 | message: '<%= error.message %>' 12 | }).apply(this, args); 13 | 14 | // Keep gulp from hanging on this task 15 | this.emit('end'); 16 | }; 17 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Three.js 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 |
35 | ← Go back to lab 36 | 76 | 77 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /src/scripts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Corentinfardeau/three-js-flag/90ddf7649fa342d031ee47c98a8f807c29bd82d1/src/scripts/.gitkeep -------------------------------------------------------------------------------- /src/scripts/app.js: -------------------------------------------------------------------------------- 1 | /********************************************************************** 2 | 3 | APP 4 | 5 | **********************************************************************/ 6 | 7 | window.THREE = require('three'); 8 | 9 | require('../vendors/TrackballControls.js'); 10 | require('gsap'); 11 | const _ = require('underscore'); 12 | 13 | class App { 14 | 15 | constructor(){ 16 | 17 | _.bindAll(this, 'animate', 'onKeyDown', 'onKeyUp', 'restart', 'onResize'); 18 | 19 | this.isKeydown = false; 20 | 21 | this.time = 0; 22 | 23 | this.params = { 24 | text : '9', 25 | speed: 4.0, 26 | offset: 20.0, 27 | fontSize: 1000 28 | }; 29 | 30 | let width = window.innerWidth; 31 | let height = window.innerHeight; 32 | 33 | this.cameraOpts = { 34 | near : 0.1, 35 | aspect: width/height, 36 | far : 10000, 37 | z: 100 38 | } 39 | 40 | this.planeHeight = 400; 41 | this.ratio = width/height; 42 | this.planeWidth = this.planeHeight*this.ratio; 43 | 44 | let fov = 2 * Math.atan( this.planeHeight / ( 2 * this.cameraOpts.z ) ) * ( 180 / Math.PI ); 45 | let container = document.getElementsByClassName('container')[0]; 46 | 47 | this.clock = new THREE.Clock(); 48 | this.renderer = new THREE.WebGLRenderer({ antialias: true}); 49 | 50 | this.camera = new THREE.PerspectiveCamera(fov, this.cameraOpts.aspect, this.cameraOpts.near, this.cameraOpts.far); 51 | 52 | 53 | this.scene = new THREE.Scene(); 54 | this.scene.add(this.camera); 55 | 56 | this.camera.position.z = this.cameraOpts.z; 57 | //this.camera.position.y = 10; 58 | 59 | this.renderer.setSize(width, height); 60 | 61 | container.appendChild(this.renderer.domElement); 62 | this.renderer.render(this.scene, this.camera); 63 | this.renderer.setClearColor( 0x000000, 1); 64 | 65 | this.cameraControls = new THREE.TrackballControls(this.camera, this.renderer.domElement); 66 | this.cameraControls.target.set(0, 0, 0); 67 | 68 | this.gui = new dat.GUI(); 69 | this.initDat(); 70 | 71 | this.start(); 72 | 73 | } 74 | 75 | restart(){ 76 | let texture = new THREE.Texture(this.createText(this.params.text)); 77 | texture.needsUpdate = true; 78 | this.uniforms.texture1.value = texture; 79 | this.uniforms.speed.value = this.params.speed; 80 | this.uniforms.offset.value = this.params.offset; 81 | } 82 | 83 | start(){ 84 | 85 | let texture = new THREE.Texture(this.createText(this.params.text)); 86 | texture.needsUpdate = true; 87 | 88 | var vertShader = document.getElementById('vertexShader').innerHTML; 89 | var fragShader = document.getElementById('fragmentShader').innerHTML; 90 | 91 | let planeGeometry = new THREE.PlaneGeometry(this.planeWidth, this.planeHeight, 200, 200); 92 | 93 | this.uniforms = { 94 | texture1: { 95 | type: 't', 96 | value: texture 97 | }, 98 | time: { 99 | type: "f", 100 | value: this.time 101 | }, 102 | speed: { 103 | type: "f", 104 | value: this.params.speed 105 | }, 106 | offset: { 107 | type: "f", 108 | value: this.params.offset 109 | } 110 | } 111 | 112 | let planeMaterial = new THREE.ShaderMaterial({ 113 | uniforms: this.uniforms, 114 | vertexShader: vertShader, 115 | fragmentShader: fragShader, 116 | wireframe : false, 117 | wireframeLinewidth : 2, 118 | transparent : true 119 | }); 120 | 121 | this.plane = new THREE.Mesh(planeGeometry, planeMaterial); 122 | this.scene.add(this.plane); 123 | 124 | TweenMax.ticker.addEventListener('tick', this.animate); 125 | document.addEventListener('keydown', this.onKeyDown); 126 | document.addEventListener('keyup', this.onKeyUp); 127 | window.addEventListener('resize', this.onResize); 128 | } 129 | 130 | initDat(){ 131 | this.gui.add(this.params, 'text').onChange(this.restart); 132 | this.gui.add(this.params, 'fontSize').min(100).max(1000).step(10).onChange(this.restart); 133 | this.gui.add(this.params, 'speed').min(0).max(10.0).step(1).onChange(this.restart); 134 | this.gui.add(this.params, 'offset').min(0.0).max(100.0).step(1).onChange(this.restart); 135 | } 136 | 137 | onKeyDown(e){ 138 | if(e.keyCode == 32){ 139 | this.isKeydown = true; 140 | } 141 | } 142 | 143 | onKeyUp(e){ 144 | if(e.keyCode == 32){ 145 | this.isKeydown = false; 146 | } 147 | } 148 | 149 | onResize(e){ 150 | this.camera.aspect = window.innerWidth / window.innerHeight; 151 | this.camera.updateProjectionMatrix(); 152 | this.renderer.setSize(window.innerWidth, window.innerHeight); 153 | this.restart(); 154 | } 155 | 156 | createText(text){ 157 | this.canvas = document.createElement( 'canvas' ); 158 | 159 | this.canvas.height = 2048; 160 | this.canvas.width = this.canvas.height*this.ratio; 161 | let context = this.canvas.getContext( '2d' ); 162 | 163 | context.font = 'Bold '+ this.params.fontSize +'px Helvetica'; 164 | context.fillStyle = 'white'; 165 | let width = context.measureText(text).width; 166 | context.fillText(text, this.canvas.width/2 - width/2, this.canvas.height/2 + this.params.fontSize/(2*1.5)); 167 | context.fill(); 168 | 169 | return this.canvas; 170 | } 171 | 172 | animate(){ 173 | let delta = this.clock.getDelta(); 174 | this.cameraControls.update(delta); 175 | if(!this.isKeydown){ 176 | this.uniforms.time.value += delta; 177 | } 178 | this.renderer.render(this.scene, this.camera); 179 | } 180 | 181 | } 182 | 183 | new App(); -------------------------------------------------------------------------------- /src/styles/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Corentinfardeau/three-js-flag/90ddf7649fa342d031ee47c98a8f807c29bd82d1/src/styles/.gitkeep -------------------------------------------------------------------------------- /src/styles/styles.scss: -------------------------------------------------------------------------------- 1 | /* ------------------------------------------------------------------ */ 2 | /* SCSS IMPORT 3 | /* ------------------------------------------------------------------ */ 4 | 5 | *{ 6 | margin: 0; 7 | padding: 0; 8 | 9 | font-family: sans-serif; 10 | font-size: 12px; 11 | } 12 | 13 | .back{ 14 | color: white; 15 | position: absolute; 16 | bottom : 20px; 17 | left : 20px; 18 | } -------------------------------------------------------------------------------- /src/vendors/TrackballControls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Eberhard Graether / http://egraether.com/ 3 | * @author Mark Lundin / http://mark-lundin.com 4 | * @author Simone Manini / http://daron1337.github.io 5 | * @author Luca Antiga / http://lantiga.github.io 6 | */ 7 | 8 | THREE.TrackballControls = function ( object, domElement ) { 9 | 10 | var _this = this; 11 | var STATE = { NONE: - 1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 }; 12 | 13 | this.object = object; 14 | this.domElement = ( domElement !== undefined ) ? domElement : document; 15 | 16 | // API 17 | 18 | this.enabled = true; 19 | 20 | this.screen = { left: 0, top: 0, width: 0, height: 0 }; 21 | 22 | this.rotateSpeed = 1.0; 23 | this.zoomSpeed = 1.2; 24 | this.panSpeed = 0.3; 25 | 26 | this.noRotate = false; 27 | this.noZoom = false; 28 | this.noPan = false; 29 | 30 | this.staticMoving = false; 31 | this.dynamicDampingFactor = 0.2; 32 | 33 | this.minDistance = 0; 34 | this.maxDistance = Infinity; 35 | 36 | this.keys = [ 65 /*A*/, 83 /*S*/, 68 /*D*/ ]; 37 | 38 | // internals 39 | 40 | this.target = new THREE.Vector3(); 41 | 42 | var EPS = 0.000001; 43 | 44 | var lastPosition = new THREE.Vector3(); 45 | 46 | var _state = STATE.NONE, 47 | _prevState = STATE.NONE, 48 | 49 | _eye = new THREE.Vector3(), 50 | 51 | _movePrev = new THREE.Vector2(), 52 | _moveCurr = new THREE.Vector2(), 53 | 54 | _lastAxis = new THREE.Vector3(), 55 | _lastAngle = 0, 56 | 57 | _zoomStart = new THREE.Vector2(), 58 | _zoomEnd = new THREE.Vector2(), 59 | 60 | _touchZoomDistanceStart = 0, 61 | _touchZoomDistanceEnd = 0, 62 | 63 | _panStart = new THREE.Vector2(), 64 | _panEnd = new THREE.Vector2(); 65 | 66 | // for reset 67 | 68 | this.target0 = this.target.clone(); 69 | this.position0 = this.object.position.clone(); 70 | this.up0 = this.object.up.clone(); 71 | 72 | // events 73 | 74 | var changeEvent = { type: 'change' }; 75 | var startEvent = { type: 'start' }; 76 | var endEvent = { type: 'end' }; 77 | 78 | 79 | // methods 80 | 81 | this.handleResize = function () { 82 | 83 | if ( this.domElement === document ) { 84 | 85 | this.screen.left = 0; 86 | this.screen.top = 0; 87 | this.screen.width = window.innerWidth; 88 | this.screen.height = window.innerHeight; 89 | 90 | } else { 91 | 92 | var box = this.domElement.getBoundingClientRect(); 93 | // adjustments come from similar code in the jquery offset() function 94 | var d = this.domElement.ownerDocument.documentElement; 95 | this.screen.left = box.left + window.pageXOffset - d.clientLeft; 96 | this.screen.top = box.top + window.pageYOffset - d.clientTop; 97 | this.screen.width = box.width; 98 | this.screen.height = box.height; 99 | 100 | } 101 | 102 | }; 103 | 104 | this.handleEvent = function ( event ) { 105 | 106 | if ( typeof this[ event.type ] == 'function' ) { 107 | 108 | this[ event.type ]( event ); 109 | 110 | } 111 | 112 | }; 113 | 114 | var getMouseOnScreen = ( function () { 115 | 116 | var vector = new THREE.Vector2(); 117 | 118 | return function getMouseOnScreen( pageX, pageY ) { 119 | 120 | vector.set( 121 | ( pageX - _this.screen.left ) / _this.screen.width, 122 | ( pageY - _this.screen.top ) / _this.screen.height 123 | ); 124 | 125 | return vector; 126 | 127 | }; 128 | 129 | }() ); 130 | 131 | var getMouseOnCircle = ( function () { 132 | 133 | var vector = new THREE.Vector2(); 134 | 135 | return function getMouseOnCircle( pageX, pageY ) { 136 | 137 | vector.set( 138 | ( ( pageX - _this.screen.width * 0.5 - _this.screen.left ) / ( _this.screen.width * 0.5 ) ), 139 | ( ( _this.screen.height + 2 * ( _this.screen.top - pageY ) ) / _this.screen.width ) // screen.width intentional 140 | ); 141 | 142 | return vector; 143 | 144 | }; 145 | 146 | }() ); 147 | 148 | this.rotateCamera = ( function() { 149 | 150 | var axis = new THREE.Vector3(), 151 | quaternion = new THREE.Quaternion(), 152 | eyeDirection = new THREE.Vector3(), 153 | objectUpDirection = new THREE.Vector3(), 154 | objectSidewaysDirection = new THREE.Vector3(), 155 | moveDirection = new THREE.Vector3(), 156 | angle; 157 | 158 | return function rotateCamera() { 159 | 160 | moveDirection.set( _moveCurr.x - _movePrev.x, _moveCurr.y - _movePrev.y, 0 ); 161 | angle = moveDirection.length(); 162 | 163 | if ( angle ) { 164 | 165 | _eye.copy( _this.object.position ).sub( _this.target ); 166 | 167 | eyeDirection.copy( _eye ).normalize(); 168 | objectUpDirection.copy( _this.object.up ).normalize(); 169 | objectSidewaysDirection.crossVectors( objectUpDirection, eyeDirection ).normalize(); 170 | 171 | objectUpDirection.setLength( _moveCurr.y - _movePrev.y ); 172 | objectSidewaysDirection.setLength( _moveCurr.x - _movePrev.x ); 173 | 174 | moveDirection.copy( objectUpDirection.add( objectSidewaysDirection ) ); 175 | 176 | axis.crossVectors( moveDirection, _eye ).normalize(); 177 | 178 | angle *= _this.rotateSpeed; 179 | quaternion.setFromAxisAngle( axis, angle ); 180 | 181 | _eye.applyQuaternion( quaternion ); 182 | _this.object.up.applyQuaternion( quaternion ); 183 | 184 | _lastAxis.copy( axis ); 185 | _lastAngle = angle; 186 | 187 | } else if ( ! _this.staticMoving && _lastAngle ) { 188 | 189 | _lastAngle *= Math.sqrt( 1.0 - _this.dynamicDampingFactor ); 190 | _eye.copy( _this.object.position ).sub( _this.target ); 191 | quaternion.setFromAxisAngle( _lastAxis, _lastAngle ); 192 | _eye.applyQuaternion( quaternion ); 193 | _this.object.up.applyQuaternion( quaternion ); 194 | 195 | } 196 | 197 | _movePrev.copy( _moveCurr ); 198 | 199 | }; 200 | 201 | }() ); 202 | 203 | 204 | this.zoomCamera = function () { 205 | 206 | var factor; 207 | 208 | if ( _state === STATE.TOUCH_ZOOM_PAN ) { 209 | 210 | factor = _touchZoomDistanceStart / _touchZoomDistanceEnd; 211 | _touchZoomDistanceStart = _touchZoomDistanceEnd; 212 | _eye.multiplyScalar( factor ); 213 | 214 | } else { 215 | 216 | factor = 1.0 + ( _zoomEnd.y - _zoomStart.y ) * _this.zoomSpeed; 217 | 218 | if ( factor !== 1.0 && factor > 0.0 ) { 219 | 220 | _eye.multiplyScalar( factor ); 221 | 222 | if ( _this.staticMoving ) { 223 | 224 | _zoomStart.copy( _zoomEnd ); 225 | 226 | } else { 227 | 228 | _zoomStart.y += ( _zoomEnd.y - _zoomStart.y ) * this.dynamicDampingFactor; 229 | 230 | } 231 | 232 | } 233 | 234 | } 235 | 236 | }; 237 | 238 | this.panCamera = ( function() { 239 | 240 | var mouseChange = new THREE.Vector2(), 241 | objectUp = new THREE.Vector3(), 242 | pan = new THREE.Vector3(); 243 | 244 | return function panCamera() { 245 | 246 | mouseChange.copy( _panEnd ).sub( _panStart ); 247 | 248 | if ( mouseChange.lengthSq() ) { 249 | 250 | mouseChange.multiplyScalar( _eye.length() * _this.panSpeed ); 251 | 252 | pan.copy( _eye ).cross( _this.object.up ).setLength( mouseChange.x ); 253 | pan.add( objectUp.copy( _this.object.up ).setLength( mouseChange.y ) ); 254 | 255 | _this.object.position.add( pan ); 256 | _this.target.add( pan ); 257 | 258 | if ( _this.staticMoving ) { 259 | 260 | _panStart.copy( _panEnd ); 261 | 262 | } else { 263 | 264 | _panStart.add( mouseChange.subVectors( _panEnd, _panStart ).multiplyScalar( _this.dynamicDampingFactor ) ); 265 | 266 | } 267 | 268 | } 269 | 270 | }; 271 | 272 | }() ); 273 | 274 | this.checkDistances = function () { 275 | 276 | if ( ! _this.noZoom || ! _this.noPan ) { 277 | 278 | if ( _eye.lengthSq() > _this.maxDistance * _this.maxDistance ) { 279 | 280 | _this.object.position.addVectors( _this.target, _eye.setLength( _this.maxDistance ) ); 281 | _zoomStart.copy( _zoomEnd ); 282 | 283 | } 284 | 285 | if ( _eye.lengthSq() < _this.minDistance * _this.minDistance ) { 286 | 287 | _this.object.position.addVectors( _this.target, _eye.setLength( _this.minDistance ) ); 288 | _zoomStart.copy( _zoomEnd ); 289 | 290 | } 291 | 292 | } 293 | 294 | }; 295 | 296 | this.update = function () { 297 | 298 | _eye.subVectors( _this.object.position, _this.target ); 299 | 300 | if ( ! _this.noRotate ) { 301 | 302 | _this.rotateCamera(); 303 | 304 | } 305 | 306 | if ( ! _this.noZoom ) { 307 | 308 | _this.zoomCamera(); 309 | 310 | } 311 | 312 | if ( ! _this.noPan ) { 313 | 314 | _this.panCamera(); 315 | 316 | } 317 | 318 | _this.object.position.addVectors( _this.target, _eye ); 319 | 320 | _this.checkDistances(); 321 | 322 | _this.object.lookAt( _this.target ); 323 | 324 | if ( lastPosition.distanceToSquared( _this.object.position ) > EPS ) { 325 | 326 | _this.dispatchEvent( changeEvent ); 327 | 328 | lastPosition.copy( _this.object.position ); 329 | 330 | } 331 | 332 | }; 333 | 334 | this.reset = function () { 335 | 336 | _state = STATE.NONE; 337 | _prevState = STATE.NONE; 338 | 339 | _this.target.copy( _this.target0 ); 340 | _this.object.position.copy( _this.position0 ); 341 | _this.object.up.copy( _this.up0 ); 342 | 343 | _eye.subVectors( _this.object.position, _this.target ); 344 | 345 | _this.object.lookAt( _this.target ); 346 | 347 | _this.dispatchEvent( changeEvent ); 348 | 349 | lastPosition.copy( _this.object.position ); 350 | 351 | }; 352 | 353 | // listeners 354 | 355 | function keydown( event ) { 356 | 357 | if ( _this.enabled === false ) return; 358 | 359 | window.removeEventListener( 'keydown', keydown ); 360 | 361 | _prevState = _state; 362 | 363 | if ( _state !== STATE.NONE ) { 364 | 365 | return; 366 | 367 | } else if ( event.keyCode === _this.keys[ STATE.ROTATE ] && ! _this.noRotate ) { 368 | 369 | _state = STATE.ROTATE; 370 | 371 | } else if ( event.keyCode === _this.keys[ STATE.ZOOM ] && ! _this.noZoom ) { 372 | 373 | _state = STATE.ZOOM; 374 | 375 | } else if ( event.keyCode === _this.keys[ STATE.PAN ] && ! _this.noPan ) { 376 | 377 | _state = STATE.PAN; 378 | 379 | } 380 | 381 | } 382 | 383 | function keyup( event ) { 384 | 385 | if ( _this.enabled === false ) return; 386 | 387 | _state = _prevState; 388 | 389 | window.addEventListener( 'keydown', keydown, false ); 390 | 391 | } 392 | 393 | function mousedown( event ) { 394 | 395 | if ( _this.enabled === false ) return; 396 | 397 | event.preventDefault(); 398 | event.stopPropagation(); 399 | 400 | if ( _state === STATE.NONE ) { 401 | 402 | _state = event.button; 403 | 404 | } 405 | 406 | if ( _state === STATE.ROTATE && ! _this.noRotate ) { 407 | 408 | _moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) ); 409 | _movePrev.copy( _moveCurr ); 410 | 411 | } else if ( _state === STATE.ZOOM && ! _this.noZoom ) { 412 | 413 | _zoomStart.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 414 | _zoomEnd.copy( _zoomStart ); 415 | 416 | } else if ( _state === STATE.PAN && ! _this.noPan ) { 417 | 418 | _panStart.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 419 | _panEnd.copy( _panStart ); 420 | 421 | } 422 | 423 | document.addEventListener( 'mousemove', mousemove, false ); 424 | document.addEventListener( 'mouseup', mouseup, false ); 425 | 426 | _this.dispatchEvent( startEvent ); 427 | 428 | } 429 | 430 | function mousemove( event ) { 431 | 432 | if ( _this.enabled === false ) return; 433 | 434 | event.preventDefault(); 435 | event.stopPropagation(); 436 | 437 | if ( _state === STATE.ROTATE && ! _this.noRotate ) { 438 | 439 | _movePrev.copy( _moveCurr ); 440 | _moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) ); 441 | 442 | } else if ( _state === STATE.ZOOM && ! _this.noZoom ) { 443 | 444 | _zoomEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 445 | 446 | } else if ( _state === STATE.PAN && ! _this.noPan ) { 447 | 448 | _panEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 449 | 450 | } 451 | 452 | } 453 | 454 | function mouseup( event ) { 455 | 456 | if ( _this.enabled === false ) return; 457 | 458 | event.preventDefault(); 459 | event.stopPropagation(); 460 | 461 | _state = STATE.NONE; 462 | 463 | document.removeEventListener( 'mousemove', mousemove ); 464 | document.removeEventListener( 'mouseup', mouseup ); 465 | _this.dispatchEvent( endEvent ); 466 | 467 | } 468 | 469 | function mousewheel( event ) { 470 | 471 | if ( _this.enabled === false ) return; 472 | 473 | event.preventDefault(); 474 | event.stopPropagation(); 475 | 476 | var delta = 0; 477 | 478 | if ( event.wheelDelta ) { 479 | 480 | // WebKit / Opera / Explorer 9 481 | 482 | delta = event.wheelDelta / 40; 483 | 484 | } else if ( event.detail ) { 485 | 486 | // Firefox 487 | 488 | delta = - event.detail / 3; 489 | 490 | } 491 | 492 | _zoomStart.y += delta * 0.01; 493 | _this.dispatchEvent( startEvent ); 494 | _this.dispatchEvent( endEvent ); 495 | 496 | } 497 | 498 | function touchstart( event ) { 499 | 500 | if ( _this.enabled === false ) return; 501 | 502 | switch ( event.touches.length ) { 503 | 504 | case 1: 505 | _state = STATE.TOUCH_ROTATE; 506 | _moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) ); 507 | _movePrev.copy( _moveCurr ); 508 | break; 509 | 510 | case 2: 511 | _state = STATE.TOUCH_ZOOM_PAN; 512 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 513 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 514 | _touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy ); 515 | 516 | var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2; 517 | var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2; 518 | _panStart.copy( getMouseOnScreen( x, y ) ); 519 | _panEnd.copy( _panStart ); 520 | break; 521 | 522 | default: 523 | _state = STATE.NONE; 524 | 525 | } 526 | _this.dispatchEvent( startEvent ); 527 | 528 | 529 | } 530 | 531 | function touchmove( event ) { 532 | 533 | if ( _this.enabled === false ) return; 534 | 535 | event.preventDefault(); 536 | event.stopPropagation(); 537 | 538 | switch ( event.touches.length ) { 539 | 540 | case 1: 541 | _movePrev.copy( _moveCurr ); 542 | _moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) ); 543 | break; 544 | 545 | case 2: 546 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 547 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 548 | _touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy ); 549 | 550 | var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2; 551 | var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2; 552 | _panEnd.copy( getMouseOnScreen( x, y ) ); 553 | break; 554 | 555 | default: 556 | _state = STATE.NONE; 557 | 558 | } 559 | 560 | } 561 | 562 | function touchend( event ) { 563 | 564 | if ( _this.enabled === false ) return; 565 | 566 | switch ( event.touches.length ) { 567 | 568 | case 1: 569 | _movePrev.copy( _moveCurr ); 570 | _moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) ); 571 | break; 572 | 573 | case 2: 574 | _touchZoomDistanceStart = _touchZoomDistanceEnd = 0; 575 | 576 | var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2; 577 | var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2; 578 | _panEnd.copy( getMouseOnScreen( x, y ) ); 579 | _panStart.copy( _panEnd ); 580 | break; 581 | 582 | } 583 | 584 | _state = STATE.NONE; 585 | _this.dispatchEvent( endEvent ); 586 | 587 | } 588 | 589 | function contextmenu( event ) { 590 | 591 | event.preventDefault(); 592 | 593 | } 594 | 595 | this.dispose = function() { 596 | 597 | this.domElement.removeEventListener( 'contextmenu', contextmenu, false ); 598 | this.domElement.removeEventListener( 'mousedown', mousedown, false ); 599 | this.domElement.removeEventListener( 'mousewheel', mousewheel, false ); 600 | this.domElement.removeEventListener( 'MozMousePixelScroll', mousewheel, false ); // firefox 601 | 602 | this.domElement.removeEventListener( 'touchstart', touchstart, false ); 603 | this.domElement.removeEventListener( 'touchend', touchend, false ); 604 | this.domElement.removeEventListener( 'touchmove', touchmove, false ); 605 | 606 | document.removeEventListener( 'mousemove', mousemove, false ); 607 | document.removeEventListener( 'mouseup', mouseup, false ); 608 | 609 | window.removeEventListener( 'keydown', keydown, false ); 610 | window.removeEventListener( 'keyup', keyup, false ); 611 | 612 | } 613 | 614 | this.domElement.addEventListener( 'contextmenu', contextmenu, false ); 615 | this.domElement.addEventListener( 'mousedown', mousedown, false ); 616 | this.domElement.addEventListener( 'mousewheel', mousewheel, false ); 617 | this.domElement.addEventListener( 'MozMousePixelScroll', mousewheel, false ); // firefox 618 | 619 | this.domElement.addEventListener( 'touchstart', touchstart, false ); 620 | this.domElement.addEventListener( 'touchend', touchend, false ); 621 | this.domElement.addEventListener( 'touchmove', touchmove, false ); 622 | 623 | window.addEventListener( 'keydown', keydown, false ); 624 | window.addEventListener( 'keyup', keyup, false ); 625 | 626 | this.handleResize(); 627 | 628 | // force an update at start 629 | this.update(); 630 | 631 | }; 632 | 633 | THREE.TrackballControls.prototype = Object.create( THREE.EventDispatcher.prototype ); 634 | THREE.TrackballControls.prototype.constructor = THREE.TrackballControls; --------------------------------------------------------------------------------