├── .gitignore ├── README.md ├── config.js ├── credentials-imgur.js ├── gulpfile.js ├── package.json └── src ├── apple-touch-icon.png ├── assets ├── images │ ├── error-background.jpg │ └── loading-spinner.gif ├── sass │ ├── _animations.scss │ ├── _base.scss │ ├── _icons.scss │ ├── _mixins.scss │ ├── _modules.scss │ ├── _typography.scss │ ├── _variables.scss │ ├── main.scss │ └── modules │ │ ├── _block-text.scss │ │ ├── _footer.scss │ │ ├── _heading-block.scss │ │ ├── _loading.scss │ │ ├── _options-box.scss │ │ ├── _options-list.scss │ │ ├── _playback.scss │ │ ├── _player.scss │ │ ├── _selector.scss │ │ ├── _snapshots.scss │ │ ├── _stage.scss │ │ └── _view.scss └── svg │ ├── icon-bandcamp.svg │ ├── icon-cross.svg │ ├── icon-dots-one.svg │ ├── icon-dots-two.svg │ ├── icon-facebook.svg │ ├── icon-play-circle.svg │ ├── icon-play.svg │ ├── icon-replay.svg │ ├── icon-skip.svg │ ├── icon-soundcloud.svg │ ├── icon-spinner.svg │ ├── icon-tick.svg │ ├── icon-twitter.svg │ ├── icon-webcam.svg │ ├── icon-youtube.svg │ └── svg.svg ├── favicon.ico ├── index.html ├── lib ├── app.js ├── components │ ├── Images.js │ ├── ImgurCanvas.js │ ├── ImgurUpload.js │ └── Timeline.js ├── constants │ ├── Timings.js │ ├── constants.js │ └── urls.js ├── events │ ├── Dispatcher.js │ └── actions.js ├── main.js ├── vendor │ └── modernizr.js ├── views │ ├── Approval.js │ ├── Links.js │ ├── Playback.js │ ├── UnsupportedError.js │ ├── VideoPlayer.js │ ├── WebcamError.js │ ├── WebcamInitialisation.js │ ├── intro.js │ └── outro.js └── webgl │ ├── Scene.js │ ├── fragment.glsl │ └── vertex.glsl └── media └── texture.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | jspm_packages 2 | node_modules 3 | dist 4 | src/media/**/*.mp4 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![I Will Never Let You Go](https://user-images.githubusercontent.com/449385/218269561-df28ea0b-da36-4e25-88f3-a3e389c040de.svg) 2 | 3 | # I Will Never Let You Go 4 | 5 | > 🚨 **Note:** It's been a long time since this project was built, and the underlying technologies have changed. A lot. With the goal of keeping the spirit of this project alive, I've created a modern version, mostly by patching and noodling my way through already minified code. As a result, [I Will Never Let You Go](https://iwillneverletyougo.wearebrightly.com/) works on modern devices, like mobile, but this repo is no longer reflective of the underlying code. You can find the Frankenstein's monster [that is currently deployed here](https://github.com/superhighfives/iwnlyg). 6 | 7 | An interactive WebGL music video. 8 | 9 | You can [watch it here](https://iwillneverletyougo.wearebrightly.com/). 10 | 11 | You can read about [how it was made here](https://medium.com/@superhighfives/making-a-music-video-f60757ceb4cf). 12 | 13 | You can [check out my other music here](https://wearebrightly.com). 14 | 15 | ![Screenshot from the video](https://user-images.githubusercontent.com/449385/218269633-a827eaf0-febc-463f-b351-95b73b96ed89.jpeg) 16 | 17 | ## Getting started 18 | 19 | First up, install the dependencies and [JSPM](http://jspm.io/): 20 | 21 | ```` 22 | npm install 23 | ./node_modules/.bin/jspm install 24 | ```` 25 | 26 | ## Imgur support 27 | 28 | You'll need to add your own Imgur credentials, which you can add to `/credentials-imgur.js`. You can get the id and secret from the [Imgur API](https://api.imgur.com/) by [registering an application](https://api.imgur.com/oauth2/addclient). 29 | 30 | The album ID and hash is returned when created, and you can do so by running `gulp imgur:create`. You'll need to have added your ID and secret to `/credentials-imgur.js` from the Imgur API first though. 31 | 32 | ``` 33 | module.exports = { 34 | clientId: "", 35 | clientSecret: "", 36 | album: { 37 | id: "", 38 | hash: "" 39 | } 40 | } 41 | ``` 42 | 43 | ## Assets 44 | 45 | You'll need the following media files: 46 | 47 | ``` 48 | /media/loop.mp4 49 | /media/loop-square.mp4 50 | /media/video.mp4 51 | /media/video-square.mp4 52 | ``` 53 | 54 | You can curl them from here: 55 | ``` 56 | curl -o src/media/loop.mp4 http://iwillneverletyougo.wearebrightly.com.s3.amazonaws.com/media/loop.mp4 57 | curl -o src/media/loop-square.mp4 http://iwillneverletyougo.wearebrightly.com.s3.amazonaws.com/media/loop-square.mp4 58 | curl -o src/media/video.mp4 http://iwillneverletyougo.wearebrightly.com.s3.amazonaws.com/media/video.mp4 59 | curl -o src/media/video-square.mp4 http://iwillneverletyougo.wearebrightly.com.s3.amazonaws.com/media/video-square.mp4 60 | ``` 61 | 62 | ## Fire it up 63 | 64 | Fire up the development server with `npm run dev` and check it out on [http://localhost:8888/](http://localhost:8888/) 65 | 66 | ## Build 67 | 68 | Output to `/dist` with `npm run build`. Host it wherever! 69 | 70 | ## Thanks 71 | 72 | Thanks to [Glen Maddern](https://github.com/geelen/) for the advice, motivation and endless ideas. Superhero. 73 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | System.config({ 2 | baseURL: "/", 3 | defaultJSExtensions: true, 4 | transpiler: "babel", 5 | babelOptions: { 6 | "optional": [ 7 | "runtime" 8 | ], 9 | "blacklist": [] 10 | }, 11 | paths: { 12 | "github:*": "jspm_packages/github/*", 13 | "npm:*": "jspm_packages/npm/*" 14 | }, 15 | 16 | map: { 17 | "babel": "npm:babel-core@5.8.22", 18 | "babel-runtime": "npm:babel-runtime@5.8.20", 19 | "classnames": "npm:classnames@2.1.3", 20 | "core-js": "npm:core-js@0.9.18", 21 | "flux": "npm:flux@2.0.3", 22 | "keymirror": "npm:keymirror@0.1.1", 23 | "react": "npm:react@0.14.0-beta3", 24 | "react-addons-css-transition-group": "npm:react-addons-css-transition-group@0.14.0-beta3", 25 | "react-dom": "npm:react-dom@0.14.0-beta3", 26 | "text": "github:systemjs/plugin-text@0.0.2", 27 | "three": "npm:three@0.70.1", 28 | "twbs/bootstrap-sass": "github:twbs/bootstrap-sass@3.3.4", 29 | "whatwg-fetch": "npm:whatwg-fetch@0.9.0", 30 | "github:jspm/nodelibs-assert@0.1.0": { 31 | "assert": "npm:assert@1.3.0" 32 | }, 33 | "github:jspm/nodelibs-buffer@0.1.0": { 34 | "buffer": "npm:buffer@3.4.3" 35 | }, 36 | "github:jspm/nodelibs-domain@0.1.0": { 37 | "domain-browser": "npm:domain-browser@1.1.4" 38 | }, 39 | "github:jspm/nodelibs-events@0.1.1": { 40 | "events": "npm:events@1.0.2" 41 | }, 42 | "github:jspm/nodelibs-path@0.1.0": { 43 | "path-browserify": "npm:path-browserify@0.0.0" 44 | }, 45 | "github:jspm/nodelibs-process@0.1.1": { 46 | "process": "npm:process@0.10.1" 47 | }, 48 | "github:jspm/nodelibs-stream@0.1.0": { 49 | "stream-browserify": "npm:stream-browserify@1.0.0" 50 | }, 51 | "github:jspm/nodelibs-util@0.1.0": { 52 | "util": "npm:util@0.10.3" 53 | }, 54 | "npm:amdefine@1.0.0": { 55 | "fs": "github:jspm/nodelibs-fs@0.1.2", 56 | "module": "github:jspm/nodelibs-module@0.1.0", 57 | "path": "github:jspm/nodelibs-path@0.1.0", 58 | "process": "github:jspm/nodelibs-process@0.1.1" 59 | }, 60 | "npm:asap@2.0.3": { 61 | "domain": "github:jspm/nodelibs-domain@0.1.0", 62 | "process": "github:jspm/nodelibs-process@0.1.1" 63 | }, 64 | "npm:assert@1.3.0": { 65 | "util": "npm:util@0.10.3" 66 | }, 67 | "npm:babel-runtime@5.8.20": { 68 | "process": "github:jspm/nodelibs-process@0.1.1" 69 | }, 70 | "npm:buffer@3.4.3": { 71 | "base64-js": "npm:base64-js@0.0.8", 72 | "ieee754": "npm:ieee754@1.1.6", 73 | "is-array": "npm:is-array@1.0.1" 74 | }, 75 | "npm:classnames@2.1.3": { 76 | "assert": "github:jspm/nodelibs-assert@0.1.0", 77 | "process": "github:jspm/nodelibs-process@0.1.1", 78 | "systemjs-json": "github:systemjs/plugin-json@0.1.0" 79 | }, 80 | "npm:core-js@0.9.18": { 81 | "fs": "github:jspm/nodelibs-fs@0.1.2", 82 | "process": "github:jspm/nodelibs-process@0.1.1", 83 | "systemjs-json": "github:systemjs/plugin-json@0.1.0" 84 | }, 85 | "npm:core-js@1.1.1": { 86 | "fs": "github:jspm/nodelibs-fs@0.1.2", 87 | "process": "github:jspm/nodelibs-process@0.1.1", 88 | "systemjs-json": "github:systemjs/plugin-json@0.1.0" 89 | }, 90 | "npm:core-util-is@1.0.1": { 91 | "buffer": "github:jspm/nodelibs-buffer@0.1.0" 92 | }, 93 | "npm:domain-browser@1.1.4": { 94 | "events": "github:jspm/nodelibs-events@0.1.1" 95 | }, 96 | "npm:envify@3.4.0": { 97 | "jstransform": "npm:jstransform@10.1.0", 98 | "process": "github:jspm/nodelibs-process@0.1.1", 99 | "through": "npm:through@2.3.8" 100 | }, 101 | "npm:esprima-fb@13001.1001.0-dev-harmony-fb": { 102 | "fs": "github:jspm/nodelibs-fs@0.1.2", 103 | "process": "github:jspm/nodelibs-process@0.1.1" 104 | }, 105 | "npm:fbjs@0.1.0-alpha.4": { 106 | "core-js": "npm:core-js@1.1.1", 107 | "fs": "github:jspm/nodelibs-fs@0.1.2", 108 | "path": "github:jspm/nodelibs-path@0.1.0", 109 | "process": "github:jspm/nodelibs-process@0.1.1", 110 | "promise": "npm:promise@7.0.4", 111 | "whatwg-fetch": "npm:whatwg-fetch@0.9.0" 112 | }, 113 | "npm:inherits@2.0.1": { 114 | "util": "github:jspm/nodelibs-util@0.1.0" 115 | }, 116 | "npm:jstransform@10.1.0": { 117 | "base62": "npm:base62@0.1.1", 118 | "buffer": "github:jspm/nodelibs-buffer@0.1.0", 119 | "esprima-fb": "npm:esprima-fb@13001.1001.0-dev-harmony-fb", 120 | "fs": "github:jspm/nodelibs-fs@0.1.2", 121 | "process": "github:jspm/nodelibs-process@0.1.1", 122 | "source-map": "npm:source-map@0.1.31" 123 | }, 124 | "npm:path-browserify@0.0.0": { 125 | "process": "github:jspm/nodelibs-process@0.1.1" 126 | }, 127 | "npm:promise@7.0.4": { 128 | "asap": "npm:asap@2.0.3", 129 | "fs": "github:jspm/nodelibs-fs@0.1.2" 130 | }, 131 | "npm:react-addons-css-transition-group@0.14.0-beta3": { 132 | "react": "npm:react@0.14.0-beta3" 133 | }, 134 | "npm:react-dom@0.14.0-beta3": { 135 | "fbjs": "npm:fbjs@0.1.0-alpha.4", 136 | "react": "npm:react@0.14.0-beta3" 137 | }, 138 | "npm:react@0.14.0-beta3": { 139 | "buffer": "github:jspm/nodelibs-buffer@0.1.0", 140 | "envify": "npm:envify@3.4.0", 141 | "fbjs": "npm:fbjs@0.1.0-alpha.4", 142 | "process": "github:jspm/nodelibs-process@0.1.1" 143 | }, 144 | "npm:readable-stream@1.1.13": { 145 | "buffer": "github:jspm/nodelibs-buffer@0.1.0", 146 | "core-util-is": "npm:core-util-is@1.0.1", 147 | "events": "github:jspm/nodelibs-events@0.1.1", 148 | "inherits": "npm:inherits@2.0.1", 149 | "isarray": "npm:isarray@0.0.1", 150 | "process": "github:jspm/nodelibs-process@0.1.1", 151 | "stream-browserify": "npm:stream-browserify@1.0.0", 152 | "string_decoder": "npm:string_decoder@0.10.31" 153 | }, 154 | "npm:source-map@0.1.31": { 155 | "amdefine": "npm:amdefine@1.0.0", 156 | "fs": "github:jspm/nodelibs-fs@0.1.2", 157 | "path": "github:jspm/nodelibs-path@0.1.0", 158 | "process": "github:jspm/nodelibs-process@0.1.1" 159 | }, 160 | "npm:stream-browserify@1.0.0": { 161 | "events": "github:jspm/nodelibs-events@0.1.1", 162 | "inherits": "npm:inherits@2.0.1", 163 | "readable-stream": "npm:readable-stream@1.1.13" 164 | }, 165 | "npm:string_decoder@0.10.31": { 166 | "buffer": "github:jspm/nodelibs-buffer@0.1.0" 167 | }, 168 | "npm:three@0.70.1": { 169 | "buffer": "github:jspm/nodelibs-buffer@0.1.0", 170 | "process": "github:jspm/nodelibs-process@0.1.1" 171 | }, 172 | "npm:through@2.3.8": { 173 | "process": "github:jspm/nodelibs-process@0.1.1", 174 | "stream": "github:jspm/nodelibs-stream@0.1.0" 175 | }, 176 | "npm:util@0.10.3": { 177 | "inherits": "npm:inherits@2.0.1", 178 | "process": "github:jspm/nodelibs-process@0.1.1" 179 | } 180 | } 181 | }); 182 | -------------------------------------------------------------------------------- /credentials-imgur.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clientId: "", 3 | clientSecret: "", 4 | album: { 5 | id: "", 6 | hash: "" 7 | } 8 | } -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | browserSync = require('browser-sync').create(), 3 | sass = require('gulp-sass'), 4 | autoprefixer = require('gulp-autoprefixer'), 5 | minify = require('gulp-minify-css'), 6 | argv = require('yargs').argv, 7 | url = require('url'), 8 | proxy = require('proxy-middleware'), 9 | preprocess = require('gulp-preprocess'), 10 | strip = require('gulp-strip-debug'), 11 | shell = require('gulp-shell'), 12 | svgstore = require('gulp-svgstore'), 13 | cheerio = require('gulp-cheerio'), 14 | svgmin = require('gulp-svgmin'), 15 | path = require('path'), 16 | fs = require('fs') 17 | 18 | // Static server with proxy 19 | gulp.task('default', ['build', 'sass:watch', 'preprocess:watch'], function () { 20 | browserSync.init({ 21 | port: 8888, 22 | files: ["*.html", "src/lib/**"], 23 | server: { 24 | baseDir: ["./dist", "./"] 25 | }, 26 | open: false, 27 | notify: false, 28 | inject: true 29 | }) 30 | }) 31 | 32 | gulp.task('sass', function () { 33 | return gulp.src('./src/assets/sass/**/*.scss') 34 | .pipe(sass().on('error', sass.logError)) 35 | .pipe(autoprefixer({ browsers: ['last 2 versions'] })) 36 | .pipe(minify()) 37 | .pipe(gulp.dest('./dist/assets/css')) 38 | .pipe(browserSync.stream()) 39 | }); 40 | 41 | gulp.task('sass:watch', ['sass'], function () { 42 | gulp.watch('./src/assets/sass/**/*.scss', ['sass']) 43 | }) 44 | 45 | gulp.task('preprocess', ['svg'], function() { 46 | return gulp.src('./src/*.html') 47 | .pipe(preprocess({context: { NODE_ENV: argv.production ? 'production' : 'development'}})) 48 | .pipe(gulp.dest('./dist/')) 49 | }) 50 | 51 | gulp.task('preprocess:watch', ['preprocess'], function() { 52 | gulp.watch('./src/*.html', ['preprocess']) 53 | }) 54 | 55 | gulp.task('js', function() { 56 | if(argv.production) { 57 | return gulp.src('') 58 | .pipe(shell([ 59 | 'jspm bundle-sfx ./src/lib/main ./dist/assets/js/main.js --minify --skip-source-maps' 60 | ])) 61 | } 62 | }) 63 | 64 | gulp.task('debug', ['js'], function() { 65 | return gulp.src('./dist/assets/js/*.js') 66 | .pipe(strip()) 67 | .pipe(gulp.dest('./dist/assets/js/')) 68 | }) 69 | 70 | gulp.task('svg', function () { 71 | return gulp.src('./src/assets/svg/*.svg') 72 | .pipe(svgmin(function (file) { 73 | var prefix = path.basename(file.relative, path.extname(file.relative)) 74 | return { 75 | plugins: [{ 76 | cleanupIDs: { 77 | prefix: prefix + '-', 78 | minify: true 79 | } 80 | }] 81 | } 82 | })) 83 | .pipe(cheerio({ 84 | run: function ($) { 85 | $('[fill]').removeAttr('fill'); 86 | }, 87 | parserOptions: { xmlMode: true } 88 | })) 89 | .pipe(svgstore({inlineSvg: true})) 90 | .pipe(gulp.dest('./src/assets/svg')) 91 | }) 92 | 93 | gulp.task('copy', function() { 94 | var files = [ 95 | './src/media/*', 96 | './src/assets/images/*', 97 | './src/favicon.ico', 98 | './src/apple-touch-icon.png' 99 | ] 100 | if(!argv.production) { 101 | files.push('src/lib/**/*') 102 | } 103 | return gulp.src(files, { base: './src' }) 104 | .pipe(gulp.dest('./dist/')) 105 | }) 106 | 107 | gulp.task('build', ['sass', 'js', 'svg', 'copy', 'preprocess']) 108 | 109 | // Imgur tasks 110 | var request = require('request'), 111 | imgurCredentials = require('./credentials-imgur.js') 112 | 113 | gulp.task('imgur:create', function () { 114 | return request({ 115 | url: "https://api.imgur.com/3/album/", 116 | method: 'post', 117 | headers: { 118 | 'Authorization': 'Client-ID ' + imgurCredentials.clientId 119 | } 120 | }, function(error, response, body) { 121 | if (!error && response.statusCode == 200) { 122 | var data = JSON.parse(body) 123 | console.log("Album ID: " + data.data.id) 124 | console.log("Album deletehash: " + data.data.deletehash) 125 | } 126 | }) 127 | }) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dev": "./node_modules/.bin/de dist && gulp", 4 | "build-dev": "./node_modules/.bin/de dist && gulp build", 5 | "build": "./node_modules/.bin/de dist && gulp build --production", 6 | "deploy": "./node_modules/.bin/de dist && gulp build --production && gulp deploy" 7 | }, 8 | "jspm": { 9 | "directories": { 10 | "lib": "." 11 | }, 12 | "dependencies": { 13 | "classnames": "npm:classnames@^2.1.3", 14 | "flux": "npm:flux@^2.0.3", 15 | "react": "npm:react@^0.14.0-beta3", 16 | "react-addons-css-transition-group": "npm:react-addons-css-transition-group@^0.14.0-beta3", 17 | "react-dom": "npm:react-dom@^0.14.0-beta3", 18 | "text": "github:systemjs/plugin-text@^0.0.2", 19 | "three": "npm:three@^0.70.0", 20 | "twbs/bootstrap-sass": "github:twbs/bootstrap-sass@^3.3.4", 21 | "whatwg-fetch": "npm:whatwg-fetch@^0.9.0" 22 | }, 23 | "devDependencies": { 24 | "babel": "npm:babel-core@^5.1.13", 25 | "babel-runtime": "npm:babel-runtime@^5.1.13", 26 | "core-js": "npm:core-js@^0.9.4" 27 | } 28 | }, 29 | "dependencies": { 30 | "jspm": "^0.16.1" 31 | }, 32 | "devDependencies": { 33 | "browser-sync": "^2.7.6", 34 | "del": "^1.2.0", 35 | "gulp": "~3.9", 36 | "gulp-autoprefixer": "^2.3.1", 37 | "gulp-awspublish": "^2.0.2", 38 | "gulp-cheerio": "^0.6.2", 39 | "gulp-cloudfront": "0.0.14", 40 | "gulp-minify-css": "^1.1.6", 41 | "gulp-preprocess": "^1.2.0", 42 | "gulp-rev-all": "^0.8.21", 43 | "gulp-sass": "^2.0.1", 44 | "gulp-shell": "^0.4.2", 45 | "gulp-strip-debug": "^1.0.2", 46 | "gulp-svgfallback": "^3.0.2", 47 | "gulp-svgmin": "^1.2.0", 48 | "gulp-svgstore": "^5.0.3", 49 | "merge-stream": "^1.0.0", 50 | "proxy-middleware": "~0.11.0", 51 | "request": "^2.61.0", 52 | "yargs": "^3.10.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superhighfives/i-will-never-let-you-go-archive/6fe20eebe3aad315cd8507550521c29f5e3acffd/src/apple-touch-icon.png -------------------------------------------------------------------------------- /src/assets/images/error-background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superhighfives/i-will-never-let-you-go-archive/6fe20eebe3aad315cd8507550521c29f5e3acffd/src/assets/images/error-background.jpg -------------------------------------------------------------------------------- /src/assets/images/loading-spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superhighfives/i-will-never-let-you-go-archive/6fe20eebe3aad315cd8507550521c29f5e3acffd/src/assets/images/loading-spinner.gif -------------------------------------------------------------------------------- /src/assets/sass/_animations.scss: -------------------------------------------------------------------------------- 1 | @keyframes fade-in { 2 | 0% { 3 | visibility: visible; 4 | opacity: 0; 5 | } 6 | 100% { 7 | opacity: 1; 8 | } 9 | } 10 | 11 | @keyframes fade-out { 12 | 0% { 13 | opacity: 1; 14 | } 15 | 100% { 16 | opacity: 0; 17 | } 18 | } 19 | 20 | @keyframes fade-in-out { 21 | 0% { 22 | opacity: 0; 23 | } 24 | 10% { 25 | opacity: 1; 26 | } 27 | 90% { 28 | opacity: 1; 29 | } 30 | 100% { 31 | opacity: 0; 32 | } 33 | } 34 | 35 | @keyframes spin { 36 | from { 37 | transform: rotate(0deg); 38 | } 39 | to { 40 | transform: rotate(360deg); 41 | } 42 | } -------------------------------------------------------------------------------- /src/assets/sass/_base.scss: -------------------------------------------------------------------------------- 1 | html, body { 2 | overflow: hidden; 3 | } 4 | 5 | body { 6 | background: $background-color; 7 | color: $text-color; 8 | @extend %type; 9 | min-width: 320px; 10 | font-size: 16px; 11 | } 12 | 13 | [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak { 14 | display: none !important; 15 | } 16 | 17 | h1, h2, h3, h4, h5, h6 { 18 | font-weight: normal; 19 | font-family: inherit; 20 | color: inherit; 21 | padding: 0; 22 | } 23 | 24 | h1, h2, h3, h4, h5, h6, hgroup, 25 | ul, ol, dl, 26 | blockquote, p, address, 27 | table, fieldset, 28 | figure, pre { 29 | margin: 0; 30 | margin-bottom: 24px; 31 | } 32 | 33 | hr { 34 | margin: 24px 0; 35 | border-top: 2px solid $color-off-white; 36 | } 37 | 38 | p { 39 | &.subtle { 40 | color: $color-mid-grey; 41 | } 42 | &.last { 43 | margin-bottom: 0; 44 | } 45 | } 46 | 47 | a { 48 | transition: color 0.3s, border-color 0.3s, background-color 0.3s; 49 | &:hover, &:active, &:focus { 50 | text-decoration: none; 51 | } 52 | &.link { 53 | text-decoration: underline; 54 | &:hover, &:active, &:focus { 55 | text-decoration: underline; 56 | } 57 | } 58 | } 59 | 60 | // hide flash of unstyled text 61 | html { 62 | body { 63 | opacity: 1; 64 | } 65 | &.wf-loading { 66 | body { 67 | opacity: 0; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/assets/sass/_icons.scss: -------------------------------------------------------------------------------- 1 | %icon { 2 | display: inline-block; 3 | width: 2em; 4 | height: 2em; 5 | vertical-align: top; 6 | fill: currentColor; 7 | } 8 | 9 | .icon { 10 | @extend %icon; 11 | } 12 | 13 | .icon--sm { 14 | @extend %icon; 15 | width: 1.5em; 16 | height: 1.5em; 17 | } 18 | 19 | .icon--lg { 20 | @extend %icon; 21 | width: 3em; 22 | height: 3em; 23 | } 24 | 25 | .icon--spinning { 26 | fill: $color-light-grey; 27 | animation: spin 2s infinite linear; 28 | } -------------------------------------------------------------------------------- /src/assets/sass/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin clearfix { 2 | &:before, 3 | &:after { 4 | content: " "; 5 | display: table; 6 | } 7 | &:after { 8 | clear: both; 9 | } 10 | } 11 | 12 | @mixin type($size) { 13 | font-size: #{$size / 16}em; 14 | } 15 | 16 | @mixin line-height($size) { 17 | line-height: #{$size / 16}em; 18 | } 19 | -------------------------------------------------------------------------------- /src/assets/sass/_modules.scss: -------------------------------------------------------------------------------- 1 | @import "modules/loading"; 2 | @import "modules/block-text"; 3 | @import "modules/options-box"; 4 | @import "modules/options-list"; 5 | @import "modules/heading-block"; 6 | @import "modules/playback"; 7 | @import "modules/selector"; 8 | @import "modules/snapshots"; 9 | @import "modules/view"; 10 | @import "modules/player"; 11 | @import "modules/stage"; 12 | @import "modules/footer"; -------------------------------------------------------------------------------- /src/assets/sass/_typography.scss: -------------------------------------------------------------------------------- 1 | %type-regular { 2 | font-weight: 400; 3 | } 4 | 5 | %type-semibold { 6 | font-weight: 600; 7 | } 8 | 9 | %type-bold { 10 | font-weight: 700; 11 | } 12 | 13 | %type-upcase { 14 | text-transform: uppercase; 15 | } 16 | 17 | %type-downcase { 18 | text-transform: lowercase; 19 | } 20 | 21 | %heading-type { 22 | font-family: $heading-stack; 23 | } 24 | 25 | %type { 26 | font-family: $font-stack; 27 | font-size: 16px; 28 | } -------------------------------------------------------------------------------- /src/assets/sass/_variables.scss: -------------------------------------------------------------------------------- 1 | $color-white : #fff; 2 | $color-black : #000; 3 | $color-dark-grey : #222; 4 | $color-mid-grey : #333; 5 | $color-light-grey : #999; 6 | $color-off-white : #dedede; 7 | $color-red : #f02233; 8 | 9 | // Overriding Bootstrap 10 | $brand-primary : $color-red; 11 | $link-hover-color : $color-black; 12 | 13 | $heading-stack : "Playfair Display", sans-serif; 14 | $font-stack : "Open Sans", sans-serif; 15 | 16 | $background-color : $color-white; 17 | $text-color : $color-black; -------------------------------------------------------------------------------- /src/assets/sass/main.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | @import "jspm_packages/github/twbs/bootstrap-sass@3.3.4/assets/stylesheets/bootstrap"; 3 | 4 | @import "animations"; 5 | @import "mixins"; 6 | @import "typography"; 7 | @import "icons"; 8 | @import "base"; 9 | @import "modules"; -------------------------------------------------------------------------------- /src/assets/sass/modules/_block-text.scss: -------------------------------------------------------------------------------- 1 | %block-text { 2 | @extend %heading-type; 3 | @extend %type-bold; 4 | text-transform: uppercase; 5 | letter-spacing: 0.25em; 6 | line-height: 1.25em; 7 | } 8 | 9 | .block-text { 10 | @extend %block-text; 11 | @include type(16) 12 | } 13 | 14 | .block-text--lg { 15 | @extend %block-text; 16 | @include type(18) 17 | } 18 | 19 | .block-text--sm { 20 | @extend %block-text; 21 | @include type(14) 22 | } -------------------------------------------------------------------------------- /src/assets/sass/modules/_footer.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | position: fixed; 3 | left: 0; 4 | bottom: 0; 5 | padding: 1em; 6 | .footer__link { 7 | @include type(13); 8 | display: inline-block; 9 | margin-right: 1em; 10 | } 11 | .footer__highlight { 12 | @extend .block-text; 13 | text-decoration: underline; 14 | display: block; 15 | } 16 | @media (max-width: $screen-xs-max) { 17 | display: none; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/assets/sass/modules/_heading-block.scss: -------------------------------------------------------------------------------- 1 | .heading-block { 2 | text-align: center; 3 | max-width: 50%; 4 | @media (max-width: $screen-xs-max) { 5 | max-width: 80%; 6 | } 7 | .heading-block__title, 8 | .heading-block__subtitle, 9 | .heading-block__note { 10 | margin-bottom: 0; 11 | } 12 | .heading-block__title { 13 | @extend .block-text; 14 | color: $brand-primary; 15 | padding-bottom: 0.5em; 16 | } 17 | .heading-block__subtitle { 18 | @extend %heading-type; 19 | @include type(24); 20 | font-style: italic; 21 | } 22 | .heading-block__note { 23 | @extend .block-text--sm; 24 | color: $color-light-grey; 25 | padding-top: 2em; 26 | } 27 | } -------------------------------------------------------------------------------- /src/assets/sass/modules/_loading.scss: -------------------------------------------------------------------------------- 1 | %loading { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | background: url(../images/loading-spinner.gif) no-repeat center center; 8 | background-size: 30px; 9 | %loading__fallback { 10 | position: absolute; 11 | top: 0; 12 | left: -99999em; 13 | max-width: 320px; 14 | padding: 1em; 15 | line-height: 1em; 16 | > * + * { 17 | margin-top: 0.5em; 18 | } 19 | } 20 | %loading__title, 21 | %loading__subtitle { 22 | @include type(16); 23 | margin-bottom: 0; 24 | } 25 | %loading__link { 26 | display: block; 27 | } 28 | } 29 | 30 | .loading { 31 | @extend %loading; 32 | } 33 | 34 | .loading__fallback { 35 | @extend %loading__fallback; 36 | } 37 | 38 | .loading__title { 39 | @extend %loading__title; 40 | } 41 | 42 | .loading__subtitle { 43 | @extend %loading__subtitle; 44 | } 45 | 46 | .loading__link { 47 | display: block; 48 | } -------------------------------------------------------------------------------- /src/assets/sass/modules/_options-box.scss: -------------------------------------------------------------------------------- 1 | .options-box { 2 | display: block; 3 | padding: 0.5em; 4 | margin: 0.5em; 5 | text-align: center; 6 | float: left; 7 | text-align: center; 8 | @include type(13); 9 | color: rgba($color-red, 0.6); 10 | .options-box__icon { 11 | transition: color 0.3s, background-color 0.3s; 12 | display: block; 13 | margin: 0 auto; 14 | color: $color-red; 15 | border: 1px solid rgba($color-red, 0.25); 16 | width: 8em; 17 | height: 8em; 18 | display: flex; 19 | align-items: center; 20 | justify-content: center; 21 | margin-bottom: 0.5em; 22 | } 23 | &:hover, &:active, &:focus { 24 | color: $color-red; 25 | .options-box__icon { 26 | color: $color-white; 27 | background-color: rgba($color-red, 0.9); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/assets/sass/modules/_options-list.scss: -------------------------------------------------------------------------------- 1 | .options-list { 2 | .options-list__item { 3 | border: 1px solid rgba($color-red, 0.25); 4 | display: block; 5 | padding: 0.5em 3em; 6 | margin: 0.5em; 7 | text-align: center; 8 | @extend %type-semibold; 9 | @include type(14); 10 | &:hover { 11 | border-color: $color-black; 12 | } 13 | } 14 | .options-list__item--reversed { 15 | @extend .options-list__item; 16 | background: $brand-primary; 17 | color: $color-white; 18 | &:hover { 19 | background: $color-black; 20 | color: $color-white; 21 | } 22 | } 23 | .options-list__icon { 24 | @extend .icon--sm; 25 | margin-right: 0.5em; 26 | } 27 | } -------------------------------------------------------------------------------- /src/assets/sass/modules/_playback.scss: -------------------------------------------------------------------------------- 1 | %playback { 2 | animation: fade-in-out 3.5s both 0.5s; 3 | %playback__item { 4 | font-family: $heading-stack; 5 | font-style: italic; 6 | } 7 | &%playback__item--unsupported { 8 | text-decoration: line-through; 9 | color: $color-light-grey; 10 | strong { 11 | font-weight: normal; 12 | color: $color-light-grey; 13 | } 14 | } 15 | } 16 | 17 | .playback { 18 | @extend %playback; 19 | } 20 | 21 | .playback__item { 22 | @extend %playback__item; 23 | strong { 24 | color: $brand-primary; 25 | } 26 | } 27 | 28 | .playback__item--unsupported { 29 | @extend %playback__item--unsupported; 30 | } -------------------------------------------------------------------------------- /src/assets/sass/modules/_player.scss: -------------------------------------------------------------------------------- 1 | .player { 2 | transition: opacity 0.3s ease-in-out 0.3s; 3 | canvas { 4 | margin: 0; 5 | padding: 0; 6 | } 7 | .player__video, 8 | .player__webcam { 9 | display: none; 10 | } 11 | &.player--hidden { 12 | opacity: 0; 13 | } 14 | } 15 | 16 | .buffering { 17 | position: absolute; 18 | top: 2em; 19 | left: 2em; 20 | line-height: 2em; 21 | .buffering__status { 22 | @extend .block-text--sm; 23 | color: $color-light-grey; 24 | padding-left: 0.5em; 25 | } 26 | .icon--lg { 27 | width: 2em; 28 | height: 2em; 29 | } 30 | } -------------------------------------------------------------------------------- /src/assets/sass/modules/_selector.scss: -------------------------------------------------------------------------------- 1 | .selector { 2 | z-index: 10; 3 | position: fixed; 4 | bottom: 1em; 5 | right: 1em; 6 | display: none; 7 | opacity: 0; 8 | transition: opacity 0.3s; 9 | &.selector--visible { 10 | display: block; 11 | opacity: 1; 12 | } 13 | .selector__title { 14 | @extend .block-text--sm; 15 | color: $color-light-grey; 16 | margin-bottom: 0.5em; 17 | } 18 | .selector__frame { 19 | transition: background 0.3s, color 0.3s; 20 | @extend .block-text--sm; 21 | display: block; 22 | float: left; 23 | padding: 0.5em 1em 0; 24 | border: 2px solid $brand-primary; 25 | color: $brand-primary; 26 | cursor: pointer; 27 | min-height: 3em; 28 | line-height: 0; 29 | + .selector__frame { 30 | margin-left: 0.25em; 31 | } 32 | &:hover, &.selector__frame--active { 33 | color: $color-white; 34 | background: $brand-primary; 35 | } 36 | &.selector__frame--hidden { 37 | display: none; 38 | } 39 | .icon { 40 | width: 1.5em; 41 | height: 1.5em; 42 | } 43 | } 44 | .selector__frame--reset { 45 | @extend .selector__frame; 46 | } 47 | } -------------------------------------------------------------------------------- /src/assets/sass/modules/_snapshots.scss: -------------------------------------------------------------------------------- 1 | .snapshot { 2 | position: relative; 3 | width: 560px; 4 | height: 105px; 5 | background-size: cover; 6 | box-shadow: 0 0 10px rgba($color-black, 0.5); 7 | background-position: 0 1px; 8 | border: 2px solid rgba($color-white, 0.9); 9 | &:before, 10 | &:after { 11 | content: ""; 12 | position: absolute; 13 | width: 3px; 14 | height: 100%; 15 | background-color: rgba($color-white, 0.75); 16 | box-shadow: inset 0 0 5px rgba($color-black, 0.5); 17 | top: 0; 18 | } 19 | &:before { 20 | left: 33%; 21 | } 22 | &:after { 23 | right: 33%; 24 | } 25 | } 26 | 27 | .snapshot-approval{ 28 | margin-top: 24px; 29 | display: flex; 30 | align-items: center; 31 | justify-content: center; 32 | > * + * { 33 | margin-left: 1em; 34 | } 35 | .snapshot-approval__option { 36 | display: block; 37 | transition: color 0.3s, background-color 0.3s, border-color 0.3s; 38 | box-shadow: 0 0 5px rgba($color-black, 0.25); 39 | color: $color-white; 40 | background-color: rgba($color-black, 0.25); 41 | border: 1px solid $color-white; 42 | width: 4em; 43 | height: 4em; 44 | display: flex; 45 | align-items: center; 46 | justify-content: center; 47 | &:hover, &:active, &:focus { 48 | color: $color-white; 49 | background-color: rgba($color-red, 0.9); 50 | border-color: $color-red; 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/assets/sass/modules/_stage.scss: -------------------------------------------------------------------------------- 1 | .stage-appear, 2 | .stage-enter { 3 | transition: opacity 0.3s linear 0.3s; 4 | } 5 | .stage-leave { 6 | transition: opacity 0.1s linear; 7 | } 8 | 9 | .stage-appear, 10 | .stage-enter { 11 | opacity: 0.01; 12 | &.stage-appear-active, 13 | &.stage-enter-active { 14 | opacity: 1; 15 | } 16 | } 17 | 18 | .stage-leave { 19 | opacity: 1; 20 | &.stage-leave-active { 21 | opacity: 0.01; 22 | } 23 | } 24 | 25 | .stage-wrapper { 26 | transition: filter 0.3s; 27 | &.is-buffering { 28 | filter: grayscale(100%); 29 | } 30 | } -------------------------------------------------------------------------------- /src/assets/sass/modules/_view.scss: -------------------------------------------------------------------------------- 1 | .view { 2 | position: fixed; 3 | width: 100%; 4 | height: 100%; 5 | left: 0; 6 | top: 0; 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | flex-direction: column; 11 | &.error--unsupported { 12 | background: url(../images/error-background.jpg) no-repeat center center; 13 | background-size: cover; 14 | } 15 | } -------------------------------------------------------------------------------- /src/assets/svg/icon-bandcamp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | bandcamp 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/svg/icon-cross.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | cross 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/svg/icon-dots-one.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | dots-one 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/svg/icon-dots-two.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | dots-two 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/assets/svg/icon-facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | facebook 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/svg/icon-play-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | play-circle 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/svg/icon-play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | play 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/svg/icon-replay.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | replay 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/svg/icon-skip.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | skip 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/svg/icon-soundcloud.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | soundcloud 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/svg/icon-spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | spinner 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/svg/icon-tick.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tick 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/svg/icon-twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | twitter 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/svg/icon-webcam.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | webcam 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/svg/icon-youtube.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | youtube 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/svg/svg.svg: -------------------------------------------------------------------------------- 1 | bandcampcrossdots-onedots-twofacebookplay-circleplayreplayskipsoundcloudspinnerticktwitterwebcamyoutube -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superhighfives/i-will-never-let-you-go-archive/6fe20eebe3aad315cd8507550521c29f5e3acffd/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | I Will Never Let You Go - A Song By Brightly 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 |
24 | 25 |
26 |
27 |
28 |

I Will Never Let You Go

29 |

Thank You For Listening

30 | Watch on YouTube 31 | Buy on Bandcamp 32 | Listen on Soundcloud 33 | Follow via Twitter 34 | Like via Facebook 35 |
36 |
37 |
38 | 39 | 43 | 44 | 45 | 59 | 60 | 61 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /src/lib/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactAddonsCSSTransitionGroup from 'react-addons-css-transition-group' 3 | import {Modernizr} from './vendor/modernizr' 4 | import Dispatcher from './events/Dispatcher' 5 | import Constants from './constants/Constants' 6 | import Urls from './constants/Urls' 7 | import Actions from './events/Actions' 8 | import ImgurUpload from './components/ImgurUpload' 9 | import Images from './components/Images' 10 | import VideoPlayer from './views/VideoPlayer' 11 | import ClassNames from 'classnames' 12 | import 'whatwg-fetch' 13 | 14 | export default class App extends React.Component { 15 | constructor() { 16 | super() 17 | let isIphone = navigator.userAgent.match(/(iPhone|iPod)/) 18 | this.supported = Modernizr.webgl && Modernizr.video && Modernizr.flexbox && !isIphone 19 | this.state = {view: this.supported ? Constants.VIEW_INTRO : Constants.VIEW_UNSUPPORTED_ERROR, buffering: false, approvalRequired: false, webcam: false} 20 | Modernizr.on('videoautoplay', (result) => { this.setState({singleTrack: !result}) }) 21 | this.updateView(this.state.view) 22 | if(isIphone) { 23 | document.ontouchmove = function(event) { 24 | event.preventDefault() 25 | } 26 | } 27 | } 28 | componentDidMount() { 29 | // console.log("Mounted: App") 30 | 31 | window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; 32 | 33 | if(this.supported) { 34 | /* PRELOAD SCHOOL OF WITCHCRAFT AND WIZARDRY */ 35 | this.getImages() 36 | this.preloader = document.createElement("video") 37 | this.preloader.volume = 0 38 | this.preloader.src = Urls.mainVideoUrl 39 | this.preloader.play() 40 | // console.log("PRELOADING") 41 | let startedAt = window.performance.now() 42 | this.preloader.addEventListener('canplaythrough', _ => { 43 | console.log(`PRELOADED VIDEO IN ${(performance.now() - startedAt) / 1000} seconds`) 44 | // this.setState({readyToStart: true}) 45 | Actions.canPlayThrough() 46 | }) 47 | } 48 | 49 | Dispatcher.register((action) => { 50 | switch(action.actionType) { 51 | case Constants.ACTION_INITIATE_UNSUPPORTED: 52 | // console.log("Dispatching: Unsupported Error") 53 | this.setState({view: Constants.VIEW_UNSUPPORTED_ERROR}) 54 | break 55 | case Constants.ACTION_PROMPT_WEBCAM: 56 | // console.log("Dispatching: Webcam Initialisation") 57 | this.setState({view: Constants.VIEW_WEBCAM_PROMPT}) 58 | break 59 | case Constants.ACTION_INITIATE_WEBCAM_SUCCESS: 60 | console.log("ACTION_INITIATE_WEBCAM_SUCCESS") 61 | this.setState({webcam: true}) 62 | break 63 | case Constants.ACTION_INITIATE_WEBCAM_FAILURE: 64 | // console.log("Dispatching: Webcam Error") 65 | this.setState({view: Constants.VIEW_WEBCAM_ERROR, message: action.message}) 66 | break 67 | case Constants.ACTION_START: 68 | // console.log("Dispatching: Playing") 69 | if(this.preloader) { 70 | this.preloader.pause() 71 | this.preloader.src = "" 72 | this.preloader = null 73 | } 74 | this.setState({view: Constants.VIEW_VIDEO, message: this.state.webcam}) 75 | break 76 | case Constants.ACTION_BUFFERING_VIDEO: 77 | // console.log("Dispatching: Buffering " + action.message) 78 | this.setState({buffering: action.message}) 79 | break 80 | case Constants.ACTION_IMAGE_CAPTURED: 81 | // console.log("Dispatching: Captured image") 82 | this.setState({capturedImage: action.message, approvalRequired: true}) 83 | break 84 | case Constants.ACTION_END: 85 | // console.log("Dispatching: Outro") 86 | if(this.state.approvalRequired) { 87 | this.setState({view: Constants.VIEW_APPROVAL}) 88 | } else { 89 | this.setState({view: Constants.VIEW_OUTRO}) 90 | } 91 | break 92 | case Constants.ACTION_APPROVED: 93 | // console.log("Dispatching: Approved") 94 | this.setState({view: Constants.VIEW_OUTRO, approvalRequired: false}) 95 | break 96 | case Constants.ACTION_UPLOAD_IMAGE: 97 | // console.log("Dispatching: Upload image") 98 | this.uploadImage() 99 | this.setState({view: Constants.VIEW_OUTRO, approvalRequired: false}) 100 | break 101 | } 102 | }) 103 | } 104 | componentWillUpdate(props, state) { 105 | if(state.view) { 106 | this.updateView(state.view) 107 | } 108 | if(state.singleTrack) { 109 | Actions.setupSingleTrack() 110 | } 111 | } 112 | uploadImage() { 113 | ImgurUpload.uploadSingleImage(this.state.capturedImage) 114 | .then((response) => { 115 | if(response.success) { 116 | // console.log(response.data) 117 | var metadata = { 118 | id: response.data.id, 119 | deletehash: response.data.deletehash, 120 | width: response.data.width, 121 | height: response.data.height 122 | } 123 | console.log(metadata) 124 | } 125 | }) 126 | } 127 | getImages() { 128 | ImgurUpload.getAlbum() 129 | .then((response) => { 130 | if(response.success) { 131 | Images.loadAll(response.data) 132 | .then((response) => { 133 | Actions.loadedImages(response) 134 | }) 135 | } 136 | }) 137 | } 138 | updateView(view) { 139 | // console.log("App state changed", view) 140 | this.view = view 141 | } 142 | render() { 143 | let ReactCSSTransitionGroup = ReactAddonsCSSTransitionGroup 144 | // console.log("Rendering stage") 145 | let stageClasses = ClassNames({ 146 | 'stage-wrapper': true, 147 | 'is-buffering': this.state.buffering 148 | }) 149 | let iconSpinner = '' 150 | let VideoPlayerElement = this.supported ? : null 151 | let View = this.view 152 | return
153 | { this.state.buffering 154 | ?
155 | 156 | Buffering... 157 |
158 | : null 159 | } 160 | {VideoPlayerElement} 161 | 162 | 163 | 164 |
165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/lib/components/Images.js: -------------------------------------------------------------------------------- 1 | export default { 2 | loadAll(data) { 3 | var totalImages = 10 4 | return new Promise((resolve, reject) => { 5 | let promises = [] 6 | let images = data.images.slice(-totalImages) 7 | images.forEach((imageData) => { 8 | var promise = new Promise((resolve, reject) => { 9 | var image = new Image() 10 | image.onload = (() => resolve(image)) 11 | image.src = imageData.link.replace(/^http:\/\//,'https://') 12 | }) 13 | promises.push(promise) 14 | }) 15 | 16 | Promise.all(promises).then((response) => { 17 | resolve(response) 18 | }) 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/components/ImgurCanvas.js: -------------------------------------------------------------------------------- 1 | import Actions from '../events/Actions' 2 | 3 | export default class ImgurCanvas { 4 | constructor(webcam) { 5 | this.webcam = webcam 6 | this.createCanvas() 7 | this.setupCanvas() 8 | this.attachEvents() 9 | } 10 | 11 | createCanvas() { 12 | this.webcamCanvas = document.createElement('canvas') 13 | this.webcamCanvas.id = "webcamCanvas" 14 | this.webcamContext = this.webcamCanvas.getContext('2d') 15 | } 16 | 17 | attachEvents() { 18 | this.webcam.addEventListener('canplay', this.setupCanvas.bind(this)) 19 | } 20 | 21 | setupCanvas() { 22 | if (!this.webcamStreaming) { 23 | this.webcamCanvas.width = this.webcam.videoWidth * 3 24 | this.webcamCanvas.height = this.webcam.videoHeight 25 | this.webcamStreaming = true 26 | this.captureImage() 27 | .then(function(response) { 28 | Actions.imageCaptured(response) 29 | }) 30 | } 31 | } 32 | 33 | desaturateImage(imageData) { 34 | var data = imageData.data 35 | for (var i = 0; i < data.length; i += 4) { 36 | var bright = 0.34 * data[i] + 0.5 * data[i + 1] + 0.16 * data[i + 2] 37 | data[i] = bright 38 | data[i + 1] = bright 39 | data[i + 2] = bright 40 | } 41 | return imageData 42 | } 43 | 44 | captureImage() { 45 | return new Promise((resolve, reject) => { 46 | if (this.webcam.paused || this.webcam.ended) return 47 | let totalSnapshots = 3 48 | let promises = [] 49 | for (let i = 0; i < totalSnapshots; i++) { 50 | var promise = new Promise((resolve, reject) => { 51 | setTimeout(() => { 52 | let xPosition = i * this.webcam.videoWidth 53 | this.webcamContext.drawImage(this.webcam, xPosition, 0, this.webcam.videoWidth, this.webcamCanvas.height) 54 | resolve() 55 | }, 10000 * (i + 1)) 56 | }) 57 | promises.push(promise) 58 | } 59 | 60 | Promise.all(promises).then(() => { 61 | var imageData = this.webcamContext.getImageData(0, 0, this.webcamCanvas.width, this.webcamCanvas.height) 62 | this.webcamContext.putImageData(this.desaturateImage(imageData), 0, 0) 63 | var image = this.webcamCanvas.toDataURL('image/jpeg', 0.8).split(',')[1] 64 | resolve(image) 65 | }) 66 | }) 67 | } 68 | } -------------------------------------------------------------------------------- /src/lib/components/ImgurUpload.js: -------------------------------------------------------------------------------- 1 | import 'whatwg-fetch' 2 | import ImgurCredentials from '../../../credentials-imgur' 3 | 4 | export default { 5 | getAlbum() { 6 | return fetch('https://api.imgur.com/3/album/' + ImgurCredentials.album.id, { 7 | method: 'get', 8 | headers: { 9 | "Authorization": "Client-ID " + ImgurCredentials.clientId 10 | } 11 | }).then(response => response.json()) 12 | }, 13 | addToAlbum(id) { 14 | return fetch('https://api.imgur.com/3/album/' + ImgurCredentials.album.hash + '/add', { 15 | method: 'post', 16 | body: "ids:" + id, 17 | headers: { 18 | "Authorization": "Client-ID " + + ImgurCredentials.clientId 19 | } 20 | }).then(response => response.json()) 21 | }, 22 | uploadSingleImage(image) { 23 | let data = new FormData() 24 | data.append("image", image) 25 | data.append("type", "base64") 26 | data.append("album", ImgurCredentials.album.hash) 27 | return fetch('https://api.imgur.com/3/image', { 28 | method: 'post', 29 | body: data, 30 | headers: { 31 | "Authorization": "Client-ID " + ImgurCredentials.clientId 32 | } 33 | }).then(response => response.json()) 34 | }, 35 | createAlbum() { 36 | let data = new FormData() 37 | data.append("title", "I Will Never Let You Go") 38 | return fetch('https://api.imgur.com/3/album', { 39 | method: 'post', 40 | body: data, 41 | headers: { 42 | "Authorization": "Client-ID " + ImgurCredentials.clientId 43 | } 44 | }).then(response => response.json()) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/lib/components/Timeline.js: -------------------------------------------------------------------------------- 1 | import Constants from "../constants/Constants" 2 | 3 | export default class Timeline { 4 | constructor() { 5 | this.commands = {} 6 | } 7 | add(time, code) { 8 | this.commands[time] = code 9 | } 10 | get(time) { 11 | let instructions = []; 12 | let code = this.commands[time] 13 | if(code) { 14 | let commands = code.split(" ") 15 | commands.forEach((command) => { 16 | let splitCommands = command.split("|") 17 | let action = splitCommands[0] 18 | let duration = parseInt(splitCommands[1]) * 10 19 | instructions.push({ 20 | action: action, 21 | duration: duration, 22 | }) 23 | }) 24 | return(instructions) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/constants/Timings.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | {time: 3, code: "y|1"}, 3 | {time: 4, code: "x|1"}, 4 | {time: 8, code: "w|8"}, 5 | {time: 12, code: "g|30"}, 6 | {time: 13, code: "g|8"}, 7 | {time: 15, code: "g|1 g|16"}, 8 | {time: 16, code: "y|1"}, 9 | {time: 17, code: "x|1"}, 10 | {time: 23, code: "g|80"}, 11 | {time: 24, code: "x|1"}, 12 | {time: 26, code: "g|1"}, 13 | {time: 28, code: "y|1"}, 14 | {time: 30, code: "x|1"}, 15 | {time: 32, code: "g|4"}, 16 | {time: 33, code: "w|8 x|8 w|8 x|8 w|8 g|16 x|8"}, 17 | {time: 40, code: "y|100"}, 18 | {time: 41, code: "g|20"}, 19 | {time: 43, code: "g|20"}, 20 | {time: 45, code: "w|4"}, 21 | {time: 47, code: "x|1"}, 22 | {time: 51, code: "y|10"}, 23 | {time: 54, code: "w|10"}, 24 | {time: 57, code: "y|4"}, 25 | {time: 60, code: "x|4 g|8"}, 26 | {time: 61, code: "y|4"}, 27 | {time: 63, code: "x|4 g|2"}, 28 | {time: 67, code: "w|8 g|4"}, 29 | {time: 69, code: "g|40"}, 30 | {time: 70, code: "g|20"}, 31 | {time: 71, code: "g|40"}, 32 | {time: 72, code: "y|10"}, 33 | {time: 73, code: "x|10"}, 34 | {time: 75, code: "g|40"}, 35 | {time: 76, code: "y|4 g|5"}, 36 | {time: 78, code: "x|4 g|8"}, 37 | {time: 81, code: "y|4"}, 38 | {time: 83, code: "x|4 g|2 g|10 g|20"}, 39 | {time: 85, code: "y|8"}, 40 | {time: 87, code: "w|8 g|4"}, 41 | {time: 89, code: "g|20"}, 42 | {time: 91, code: "x|4 g|2"}, 43 | {time: 93, code: "g|8"}, 44 | {time: 95, code: "w|8 g|4"}, 45 | {time: 97, code: "w|20"}, 46 | {time: 100, code: "g|20"}, 47 | {time: 104, code: "y|8"}, 48 | {time: 106, code: "x|4"}, 49 | {time: 110, code: "w|4"}, 50 | {time: 111, code: "g|4 g|20 g|4 g|20"}, 51 | {time: 112, code: "w|4"}, 52 | {time: 113, code: "g|40"}, 53 | {time: 114, code: "w|4"}, 54 | {time: 115, code: "g|40"}, 55 | {time: 116, code: "w|4"}, 56 | {time: 117, code: "g|40"}, 57 | {time: 118, code: "y|4"}, 58 | {time: 122, code: "g|20 w|8 g|20"}, 59 | {time: 123, code: "y|4"}, 60 | {time: 125, code: "g|4 w|8"}, 61 | {time: 127, code: "x|8"}, 62 | {time: 131, code: "w|4"}, 63 | {time: 134, code: "g|12"}, 64 | {time: 136, code: "x|4"}, 65 | {time: 139, code: "y|8"}, 66 | {time: 140, code: "x|10"}, 67 | {time: 142, code: "w|4 g|20 g|20"}, 68 | {time: 143, code: "x|8"}, 69 | {time: 146, code: "g|4"}, 70 | {time: 147, code: "x|5 g|10"}, 71 | {time: 150, code: "y|4"}, 72 | {time: 151, code: "x|4"}, 73 | {time: 153, code: "g|20 g|20 g|100"}, 74 | {time: 155, code: "x|4"}, 75 | {time: 157, code: "w|4"}, 76 | {time: 157, code: "x|4"}, 77 | {time: 161, code: "y|10 x|1"}, 78 | {time: 162, code: "g|10 w|5 g|20"}, 79 | {time: 165, code: "y|4"}, 80 | {time: 166, code: "x|4"}, 81 | {time: 168, code: "g|40"}, 82 | {time: 173, code: "y|20 x|4"}, 83 | {time: 178, code: "g|40 x|1"}, 84 | {time: 184, code: "y|4"}, 85 | {time: 186, code: "w10 g|20 g|40 g|80"}, 86 | {time: 188, code: "g|40"}, 87 | {time: 190, code: "x|4"}, 88 | {time: 193, code: "w|4 y|10 x|10"}, 89 | {time: 199, code: "g|30"}, 90 | {time: 200, code: "x|4 g|80"}, 91 | {time: 205, code: "g|40 x|10"}, 92 | {time: 208, code: "g|80 x|1"} 93 | ] 94 | -------------------------------------------------------------------------------- /src/lib/constants/constants.js: -------------------------------------------------------------------------------- 1 | import Approval from '../views/Approval' 2 | import Intro from '../views/Intro' 3 | import Outro from '../views/Outro' 4 | import Playback from '../views/Playback' 5 | import UnsupportedError from '../views/UnsupportedError' 6 | import WebcamInitialisation from '../views/WebcamInitialisation' 7 | import WebcamError from '../views/WebcamError' 8 | 9 | export default { 10 | VIEW_INTRO: Intro, 11 | VIEW_UNSUPPORTED_ERROR: UnsupportedError, 12 | VIEW_WEBCAM_PROMPT: WebcamInitialisation, 13 | VIEW_WEBCAM_ERROR: WebcamError, 14 | VIEW_VIDEO: Playback, 15 | VIEW_OUTRO: Outro, 16 | VIEW_APPROVAL: Approval, 17 | ACTION_START: "ACTION_START", 18 | ACTION_PROMPT_WEBCAM: "ACTION_PROMPT_WEBCAM", 19 | ACTION_INITIATE_WEBCAM: "ACTION_INITIATE_WEBCAM", 20 | ACTION_INITIATE_WEBCAM_SUCCESS: "ACTION_INITIATE_WEBCAM_SUCCESS", 21 | ACTION_INITIATE_WEBCAM_FAILURE: "ACTION_INITIATE_WEBCAM_FAILURE", 22 | ACTION_INITIATE_UNSUPPORTED: "ACTION_INITIATE_UNSUPPORTED", 23 | ACTION_BUFFERING_VIDEO: "ACTION_BUFFERING_VIDEO", 24 | ACTION_SELECTED_FIRST_FRAME: "ACTION_SELECTED_FIRST_FRAME", 25 | ACTION_SELECTED_SECOND_FRAME: "ACTION_SELECTED_SECOND_FRAME", 26 | ACTION_SELECTED_WEBCAM_FRAME: "ACTION_SELECTED_WEBCAM_FRAME", 27 | ACTION_SELECTED_RESET: "ACTION_SELECTED_RESET", 28 | ACTION_CAN_PLAY_THROUGH: "ACTION_CAN_PLAY_THROUGH", 29 | ACTION_END: "ACTION_END", 30 | ACTION_IMAGE_CAPTURED: "ACTION_IMAGE_CAPTURED", 31 | ACTION_APPROVED: "ACTION_APPROVED", 32 | ACTION_UPLOAD_IMAGE: "ACTION_UPLOAD_IMAGE", 33 | ACTION_LOADED_IMAGES: "ACTION_LOADED_IMAGES", 34 | ACTION_SINGLE_TRACK: "ACTION_SINGLE_TRACK", 35 | VIDEO_LOOP: "VIDEO_LOOP", 36 | VIDEO_MAIN: "VIDEO_MAIN", 37 | TIMELINE_FIRST_VIDEO: "x", 38 | TIMELINE_SECOND_VIDEO: "y", 39 | TIMELINE_WEBCAM: "w", 40 | TIMELINE_GHOST: "g", 41 | TIMELINE_RESET: "v" 42 | } 43 | 44 | -------------------------------------------------------------------------------- /src/lib/constants/urls.js: -------------------------------------------------------------------------------- 1 | let loopVideoUrl = '/media/loop.mp4'; 2 | let mainVideoUrl = '/media/video.mp4'; 3 | let squareLoopVideoUrl = '/media/loop-square.mp4'; 4 | let squareMainVideoUrl = '/media/video-square.mp4'; 5 | 6 | export default { 7 | loopVideoUrl, 8 | mainVideoUrl, 9 | squareLoopVideoUrl, 10 | squareMainVideoUrl 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/events/Dispatcher.js: -------------------------------------------------------------------------------- 1 | import Flux from 'flux' 2 | 3 | export default new Flux.Dispatcher() -------------------------------------------------------------------------------- /src/lib/events/actions.js: -------------------------------------------------------------------------------- 1 | import Dispatcher from './Dispatcher' 2 | import Constants from '../constants/Constants' 3 | 4 | export default class Actions { 5 | static start() { 6 | // console.log("Action: Start") 7 | Dispatcher.dispatch({ 8 | actionType: Constants.ACTION_START 9 | }) 10 | } 11 | static promptWebcam() { 12 | // console.log("Action: Prompt Webcam") 13 | Dispatcher.dispatch({ 14 | actionType: Constants.ACTION_PROMPT_WEBCAM 15 | }) 16 | } 17 | static initiateWebcam() { 18 | // console.log("Action: Initiate Webcam") 19 | Dispatcher.dispatch({ 20 | actionType: Constants.ACTION_INITIATE_WEBCAM 21 | }) 22 | } 23 | static webcamInitiated() { 24 | // console.log("Action: Initiate Webcam") 25 | Dispatcher.dispatch({ 26 | actionType: Constants.ACTION_INITIATE_WEBCAM_SUCCESS 27 | }) 28 | } 29 | static bufferingVideo(buffering) { 30 | // console.log("Action: Buffering Video " + buffering) 31 | Dispatcher.dispatch({ 32 | actionType: Constants.ACTION_BUFFERING_VIDEO, 33 | message: buffering 34 | }) 35 | } 36 | static initiateWebcamFailure(err) { 37 | // console.log("Action: Initiate Webcam Failure") 38 | let errorMessages = { 39 | PermissionDismissedError: "We couldn't access it.", 40 | PermissionDeniedError: "We couldn't access it.", 41 | NotFoundError: "We couldn't find one.", 42 | MandatoryUnsatisfiedError: "It looks like it's not compatible." 43 | } 44 | Dispatcher.dispatch({ 45 | actionType: Constants.ACTION_INITIATE_WEBCAM_FAILURE, 46 | message: errorMessages[err.name] || "Something weird happened." 47 | }) 48 | } 49 | static imageCaptured(image) { 50 | // console.log("Action: Can capture image") 51 | Dispatcher.dispatch({ 52 | actionType: Constants.ACTION_IMAGE_CAPTURED, 53 | message: image 54 | }) 55 | } 56 | static canPlayThrough() { 57 | // console.log("Action: Can play through") 58 | Dispatcher.dispatch({ 59 | actionType: Constants.ACTION_CAN_PLAY_THROUGH 60 | }) 61 | } 62 | static ended() { 63 | // console.log("Action: Ended") 64 | Dispatcher.dispatch({ 65 | actionType: Constants.ACTION_END 66 | }) 67 | } 68 | static approved() { 69 | // console.log("Action: Approved") 70 | Dispatcher.dispatch({ 71 | actionType: Constants.ACTION_APPROVED 72 | }) 73 | } 74 | static uploadImage() { 75 | // console.log("Action: Upload image") 76 | Dispatcher.dispatch({ 77 | actionType: Constants.ACTION_UPLOAD_IMAGE 78 | }) 79 | } 80 | static loadedImages(images) { 81 | // console.log("Action: Loaded images") 82 | Dispatcher.dispatch({ 83 | actionType: Constants.ACTION_LOADED_IMAGES, 84 | message: images 85 | }) 86 | } 87 | static setupSingleTrack() { 88 | Dispatcher.dispatch({ 89 | actionType: Constants.ACTION_SINGLE_TRACK 90 | }) 91 | } 92 | } -------------------------------------------------------------------------------- /src/lib/main.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './app' 4 | 5 | ReactDOM.render(, document.querySelector('main')) -------------------------------------------------------------------------------- /src/lib/views/Approval.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Actions from '../events/Actions' 3 | import Dispatcher from '../events/Dispatcher' 4 | import Constants from '../constants/Constants' 5 | 6 | export default class Approval extends React.Component { 7 | constructor() { 8 | super() 9 | this.state = {image: false} 10 | } 11 | componentDidMount() { 12 | // console.log("Mounted: Approval") 13 | } 14 | handleUpload(event) { 15 | Actions.uploadImage() 16 | } 17 | handleCancel(event) { 18 | Actions.approved() 19 | } 20 | render() { 21 | let iconTick = '' 22 | let iconCross = '' 23 | let imageStyles = {backgroundImage: "url(data:image/jpeg;base64," + this.props.capturedImage + ")"} 24 | return
25 |
26 |

Join the video

27 |

Can we use your photos?

28 |

(These frames will be randomly injected into other videos)

29 |
30 |
31 | 39 |
40 | } 41 | } -------------------------------------------------------------------------------- /src/lib/views/Links.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default class Links extends React.Component { 4 | constructor() { 5 | super() 6 | } 7 | componentDidMount() { 8 | // console.log("Mounted: Links") 9 | } 10 | render() { 11 | let Extra = this.props.extra ? this.props.extra : null 12 | let iconYoutube = '' 13 | let iconBandcamp = '' 14 | let iconSoundcloud = '' 15 | let iconTwitter = '' 16 | let iconFacebook = '' 17 | return 40 | } 41 | } -------------------------------------------------------------------------------- /src/lib/views/Playback.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ClassNames from 'classnames' 3 | 4 | export default class Playback extends React.Component { 5 | constructor() { 6 | super() 7 | } 8 | componentDidMount() { 9 | // console.log("Mounted: Playback") 10 | } 11 | render() { 12 | let playbackItemClasses = ClassNames({ 13 | 'playback__item': true, 14 | 'playback__item--unsupported': !this.props.message 15 | }) 16 | return
17 |

This is an interactive video.

18 |

You are a part of it.

19 |

So are other people.

20 |
21 | } 22 | } -------------------------------------------------------------------------------- /src/lib/views/UnsupportedError.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Links from '../views/Links' 3 | import Actions from '../events/Actions' 4 | 5 | export default class UnsupportedError extends React.Component { 6 | constructor() { 7 | super() 8 | } 9 | componentDidMount() { 10 | // console.log("Mounted: Error") 11 | } 12 | render() { 13 | return
14 |
15 |

Okay...

16 |

So, it looks like your browser cannot play this video.

17 |

(But we have options)

18 |
19 | 20 |
21 | } 22 | } -------------------------------------------------------------------------------- /src/lib/views/VideoPlayer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import Scene from '../webgl/Scene' 4 | import Dispatcher from '../events/Dispatcher' 5 | import Constants from '../constants/Constants' 6 | import Actions from '../events/Actions' 7 | import Urls from '../constants/Urls' 8 | import ClassNames from 'classnames' 9 | import ImgurCanvas from '../components/ImgurCanvas' 10 | import Timeline from '../components/Timeline' 11 | import Timings from '../constants/Timings' 12 | 13 | export default class VideoPlayer extends React.Component { 14 | constructor() { 15 | super() 16 | this.state = {looping: true, url: this.getUrl(Constants.VIDEO_LOOP), webcam: false, selector: false, displaying: true, playbackRate: "0.8"} 17 | this.animation = null 18 | this.selectedFrame = 1 19 | this.currentTime = 0 20 | this.chosenVideoTimestamp = 0 21 | } 22 | 23 | getUrl(video) { 24 | switch(video) { 25 | case Constants.VIDEO_LOOP: 26 | if(this.state && this.state.singleTrack) { 27 | return Urls.squareLoopVideoUrl 28 | } else { 29 | return Urls.loopVideoUrl 30 | } 31 | break 32 | case Constants.VIDEO_MAIN: 33 | if(this.state && this.state.singleTrack) { 34 | return Urls.squareMainVideoUrl 35 | } else { 36 | return Urls.mainVideoUrl 37 | } 38 | break 39 | } 40 | } 41 | 42 | componentDidMount() { 43 | // console.log("Mounted: Video Player") 44 | 45 | let domNode = ReactDOM.findDOMNode(this) 46 | Scene.start(domNode) 47 | 48 | Dispatcher.register((action) => { 49 | switch (action.actionType) { 50 | case Constants.ACTION_START: 51 | // console.log("Playing: Video") 52 | this.setState({displaying: false}) 53 | setTimeout(() => { 54 | this.setState({displaying: true, looping: false, url: this.getUrl(Constants.VIDEO_MAIN), selector: true, playbackRate: "1"}) 55 | }, 5000) 56 | break 57 | case Constants.ACTION_INITIATE_WEBCAM: 58 | // console.log("Initiating: Webcam") 59 | this.setState({webcam: true}) 60 | break 61 | case Constants.ACTION_END: 62 | // console.log("Playing: Loop") 63 | this.setState({looping: true, url: this.getUrl(Constants.VIDEO_LOOP), selector: false, ended: true}) 64 | break 65 | case Constants.ACTION_LOADED_IMAGES: 66 | // console.log("Initiating: Ghosts") 67 | this.setState({ghosts: action.message}) 68 | break 69 | case Constants.ACTION_SINGLE_TRACK: 70 | // console.log("Initiating: Single Track") 71 | this.setState({singleTrack: true}) 72 | break 73 | } 74 | }) 75 | 76 | this.video = domNode.querySelector(".player__video") 77 | this.webcam = domNode.querySelector(".player__webcam") 78 | this.selector = domNode.querySelector(".player__selector") 79 | this.setupVideo(this.video) 80 | this.setupTimeline() 81 | 82 | // this.setupDebug() 83 | } 84 | 85 | // setupDebug() { 86 | // document.onkeypress = (e) => { 87 | // let charcode = (typeof e.which == "number") ? e.which : e.keyCode 88 | // let key = ""; 89 | // switch(charcode) { 90 | // case 120: 91 | // key = "x" 92 | // break 93 | // case 121: 94 | // key = "y" 95 | // break 96 | // case 119: 97 | // key = "w" 98 | // break 99 | // case 103: 100 | // key = "g" 101 | // break 102 | // } 103 | // console.log(`{time: ${this.video.currentTime.toFixed(2)}, code: "${key}|"}`) 104 | // } 105 | // } 106 | 107 | componentWillUpdate(props, state) { 108 | // console.log("Video Player state changed") 109 | 110 | if(state.webcam && !this.webcamInitiated) { 111 | this.setupWebcam(this.webcam) 112 | } 113 | 114 | if(state.ghosts && !this.ghostsInitiated) { 115 | this.setupGhosts(state.ghosts) 116 | } 117 | 118 | if(state.selectedFrame) { 119 | this.selectedFrame = state.selectedFrame 120 | } 121 | 122 | if(state.playbackRate) { 123 | this.video.playbackRate = state.playbackRate 124 | } 125 | 126 | if(state.disableSwitching) { 127 | this.disableSwitching = true; 128 | setTimeout(function () { 129 | this.setState({disableSwitching: false}) 130 | this.disableSwitching = false; 131 | }.bind(this), 4000) 132 | } 133 | } 134 | 135 | componentDidUpdate(props, state) { 136 | if(state.url) { 137 | this.video.play() 138 | } 139 | } 140 | 141 | setupWebcam(webcam) { 142 | this.webcamInitiated = true 143 | let videoSettings = { 144 | video: { 145 | mandatory: { 146 | minWidth: 720, 147 | minHeight: 480 148 | } 149 | } 150 | } 151 | navigator.getUserMedia = (navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia) 152 | if (navigator.getUserMedia) { 153 | navigator.getUserMedia(videoSettings, function (stream) { 154 | webcam.src = window.URL.createObjectURL(stream) 155 | webcam.onloadedmetadata = function (e) { 156 | // console.log("Webcam media loaded") 157 | Scene.instance.setupWebcam() 158 | new ImgurCanvas(webcam) 159 | Actions.webcamInitiated() 160 | Actions.start() 161 | } 162 | }, function (err) { 163 | // console.log("Webcam media failed", err) 164 | Actions.initiateWebcamFailure(err) 165 | }) 166 | } else { 167 | // console.log("GetUserMedia not supported") 168 | Actions.initiateWebcamFailure({}) 169 | } 170 | } 171 | 172 | setupGhosts(ghosts) { 173 | this.ghostsInitiated = true 174 | Scene.instance.setupGhosts(ghosts) 175 | } 176 | 177 | setupVideo(video) { 178 | // Switch things up for dev 179 | // video.volume = 0 180 | 181 | // Fire up animation for beats 182 | this.animation = requestAnimationFrame(this.animate.bind(this)) 183 | 184 | this.lastPlayPosition = 0 185 | this.currentPlayPosition = 0 186 | this.bufferingDetected = false 187 | this.checkInterval = 50 188 | this.bufferInterval = setInterval(this.checkBuffering.bind(this), this.checkInterval) 189 | 190 | video.onended = (e) => { 191 | // console.log("Video Player: Ended") 192 | Scene.instance.resetImages() 193 | Actions.ended() 194 | } 195 | } 196 | 197 | checkBuffering(video) { 198 | if (this.state.url == Urls.mainVideoUrl) { 199 | this.currentPlayPosition = this.video.currentTime 200 | 201 | // checking offset, e.g. 1 / 50ms = 0.02 202 | var offset = 1 / this.checkInterval 203 | 204 | // if no buffering is currently detected, 205 | // and the position does not seem to increase 206 | // and the player isn't manually paused... 207 | if ((!this.bufferingDetected 208 | && this.currentPlayPosition <= (this.lastPlayPosition + offset) 209 | && !this.video.paused) 210 | // || (!this.bufferingDetected 211 | // && this.currentPlayPosition == 0) 212 | ) { 213 | // console.log("buffering") 214 | this.bufferingDetected = true 215 | Actions.bufferingVideo(this.bufferingDetected) 216 | } 217 | 218 | // if we were buffering but the player has advanced, 219 | // then there is no buffering 220 | if (this.bufferingDetected 221 | && this.currentPlayPosition > (this.lastPlayPosition + offset) 222 | && !this.video.paused 223 | ) { 224 | // console.log("not buffering anymore") 225 | this.bufferingDetected = false 226 | Actions.bufferingVideo(this.bufferingDetected) 227 | } 228 | this.lastPlayPosition = this.currentPlayPosition 229 | } else if (this.bufferingDetected) { 230 | this.bufferingDetected = false 231 | } 232 | } 233 | 234 | setupTimeline() { 235 | this.timeline = new Timeline() 236 | Timings.forEach((data) => { 237 | this.timeline.add(data.time, data.code) 238 | }) 239 | } 240 | 241 | cueAnimations(timeline) { 242 | var chain = Promise.resolve() 243 | // console.log("Time to queue up some videoActions!") 244 | timeline.forEach((block) => { 245 | console.log(`Adding ${block.action} for ${block.duration}ms`) 246 | chain = chain.then(() => { 247 | return this.videoAction(block.action, block.duration) 248 | }) 249 | }) 250 | chain.then(() => { 251 | // console.log("YAY DONE") 252 | this.videoAction(Constants.TIMELINE_RESET) 253 | }) 254 | } 255 | 256 | videoAction(action, duration) { 257 | switch (action) { 258 | case Constants.TIMELINE_FIRST_VIDEO: 259 | case Constants.TIMELINE_SECOND_VIDEO: 260 | this.cueVideo(action) 261 | break 262 | case Constants.TIMELINE_WEBCAM: 263 | this.cueWebcam() 264 | break 265 | case Constants.TIMELINE_GHOST: 266 | this.cueGhost() 267 | break 268 | case Constants.TIMELINE_RESET: 269 | this.cueVideo() 270 | break 271 | } 272 | 273 | if (duration) { 274 | return new Promise(resolve => { 275 | setTimeout(resolve, duration) 276 | }) 277 | } else { 278 | return Promise.resolve() 279 | } 280 | } 281 | 282 | cueVideo(video) { 283 | if (video) { 284 | // console.log("PLAY VIDEO") 285 | let frame = video == Constants.TIMELINE_SECOND_VIDEO ? 2 : 1 286 | let selectedFrame = Scene.instance.showVideo(frame) 287 | this.setState({selectedFrame: selectedFrame}) 288 | } else { 289 | // console.log("RESETTING VIDEO") 290 | let selectedFrame = Scene.instance.switchToVideo() 291 | this.setState({selectedFrame: selectedFrame}) 292 | } 293 | } 294 | 295 | cueWebcam() { 296 | // console.log("PLAY WEBCAM") 297 | if(this.webcamInitiated) { 298 | let selectedFrame = Scene.instance.showWebcam() 299 | this.setState({selectedFrame: selectedFrame}) 300 | } else { 301 | Scene.instance.showGhost() 302 | } 303 | } 304 | 305 | cueGhost() { 306 | // console.log("PLAY GHOST") 307 | Scene.instance.showGhost() 308 | } 309 | 310 | animate(time) { 311 | if (this.state.url == Urls.mainVideoUrl) { 312 | let videoTime = Math.ceil(this.video.currentTime) 313 | if (this.currentTime != videoTime) { 314 | // console.log(videoTime) 315 | let timeline = this.timeline.get(this.currentTime) 316 | let doGlitch = this.chosenVideoTimestamp + 2000 < window.performance.now() 317 | if (timeline && doGlitch) { 318 | this.cueAnimations(timeline) 319 | } 320 | this.currentTime = videoTime 321 | } 322 | } 323 | 324 | this.animation = requestAnimationFrame(this.animate.bind(this)) 325 | } 326 | 327 | handleFirst() { 328 | // console.log("Video: Displaying first frame") 329 | let selectedFrame = 1; 330 | Scene.instance.showVideo(selectedFrame) 331 | this.chosenVideoTimestamp = window.performance.now() 332 | this.setState({selectedFrame: selectedFrame, disableSwitching: true}) 333 | } 334 | 335 | handleSecond() { 336 | // console.log("Video: Displaying second frame") 337 | let selectedFrame = 2; 338 | Scene.instance.showVideo(selectedFrame) 339 | this.chosenVideoTimestamp = window.performance.now() 340 | this.setState({selectedFrame: selectedFrame, disableSwitching: true}) 341 | } 342 | 343 | handleWebcam() { 344 | // console.log("Video: Displaying webcam") 345 | let selectedFrame = 3; 346 | Scene.instance.showWebcam(true) 347 | this.chosenVideoTimestamp = window.performance.now() 348 | this.setState({selectedFrame: selectedFrame, disableSwitching: true}) 349 | } 350 | 351 | render() { 352 | // console.log("Current state for player: ", this.state) 353 | let selectorClasses = ClassNames({ 354 | 'player__selector': true, 355 | 'selector': true, 356 | 'selector--visible': this.state.selector 357 | }) 358 | let firstFrameClasses = ClassNames({ 359 | 'selector__frame': true, 360 | 'selector__frame--active': this.selectedFrame == 1 361 | }) 362 | let secondFrameClasses = ClassNames({ 363 | 'selector__frame': true, 364 | 'selector__frame--active': this.selectedFrame == 2 365 | }) 366 | let webcamFrameClasses = ClassNames({ 367 | 'selector__frame': true, 368 | 'selector__frame--hidden': !this.webcamInitiated, 369 | 'selector__frame--active': this.selectedFrame == 3 370 | }) 371 | let playerClasses = ClassNames({ 372 | 'player': true, 373 | 'player--hidden': !this.state.displaying 374 | }) 375 | // console.log("Currently selected frame is: " + this.selectedFrame); 376 | let iconWebcam = '' 377 | let iconDotsOne = '' 378 | let iconDotsTwo = '' 379 | return
380 |
381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 |
391 |
394 | } 395 | } 396 | -------------------------------------------------------------------------------- /src/lib/views/WebcamError.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Actions from '../events/Actions' 3 | 4 | export default class WebcamError extends React.Component { 5 | constructor() { 6 | super() 7 | } 8 | componentDidMount() { 9 | // console.log("Mounted: Error") 10 | } 11 | handleStart(event) { 12 | Actions.start() 13 | } 14 | render() { 15 | let iconSkip = '' 16 | return
17 |
18 |

Hmmmm...

19 |

Looks like we had an issue initialising your webcam.

20 |

(Psst... {this.props.message})

21 |
22 | 30 |
31 | } 32 | } -------------------------------------------------------------------------------- /src/lib/views/WebcamInitialisation.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Actions from '../events/Actions' 3 | 4 | export default class WebcamInitialisation extends React.Component { 5 | constructor() { 6 | super() 7 | } 8 | componentDidMount() { 9 | // console.log("Mounted: Webcam Initialisation") 10 | } 11 | handleStart(event) { 12 | Actions.start() 13 | } 14 | handleWebcam(event) { 15 | Actions.initiateWebcam() 16 | } 17 | render() { 18 | let iconWebcam = '' 19 | let iconSkip = '' 20 | return
21 |
22 |

Before we start

23 |

Would you like to be a part of it?

24 |
25 | 39 |
40 | } 41 | } -------------------------------------------------------------------------------- /src/lib/views/intro.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Actions from '../events/Actions' 3 | import Dispatcher from '../events/Dispatcher' 4 | import Constants from '../constants/Constants' 5 | 6 | export default class Intro extends React.Component { 7 | constructor() { 8 | super() 9 | this.state = {} 10 | } 11 | componentDidMount() { 12 | // console.log("Mounted: Intro") 13 | 14 | Dispatcher.register((action) => { 15 | switch(action.actionType) { 16 | case Constants.ACTION_CAN_PLAY_THROUGH: 17 | this.setState({readyToStart: true}) 18 | break 19 | case Constants.ACTION_SINGLE_TRACK: 20 | this.setState({readyToStart: true, skipSetup: true}) 21 | break 22 | } 23 | }) 24 | } 25 | handleClick(event) { 26 | if(!this.handled) { 27 | if(this.state.skipSetup) { 28 | Actions.start() 29 | } else { 30 | Actions.promptWebcam() 31 | } 32 | this.handled = true 33 | } 34 | } 35 | render() { 36 | let iconPlay = '' 37 | let iconSpinner = '' 38 | return
39 |
40 |

Brightly

41 |

I Will Never Let You Go

42 |

(A real-time WebGL-powered music video)

43 |
44 | { this.state.readyToStart 45 | ? 46 | 47 | 48 | : 49 | } 50 |
51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/lib/views/outro.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Links from '../views/Links' 3 | import Actions from '../events/Actions' 4 | 5 | export default class Outro extends React.Component { 6 | constructor() { 7 | super() 8 | } 9 | componentDidMount() { 10 | // console.log("Mounted: Outro") 11 | } 12 | handleClick(event) { 13 | Actions.start() 14 | } 15 | render() { 16 | let iconReplay = '' 17 | var replay = 18 | 19 | Replay 20 | 21 | return
22 |
23 |

I Will Never Let You Go

24 |

Thank You For Listening

25 |
26 | 27 |
28 | } 29 | } -------------------------------------------------------------------------------- /src/lib/webgl/Scene.js: -------------------------------------------------------------------------------- 1 | import THREE from 'three' 2 | import vertexShader from './vertex.glsl!text' 3 | import fragmentShader from './fragment.glsl!text' 4 | 5 | export default class Scene { 6 | constructor(container) { 7 | this.container = container 8 | this.scene = new THREE.Scene() 9 | this.setupRenderer() 10 | this.setupCamera() 11 | this.setupVideoTexture() 12 | this.setupWebcamTexture() 13 | this.setupVariables() 14 | this.setupMaterial() 15 | this.setupGeometry() 16 | this.animate() 17 | window.addEventListener('resize', this.onWindowResize.bind(this), false) 18 | } 19 | 20 | setupRenderer() { 21 | this.renderer = new THREE.WebGLRenderer({ alpha: true }) 22 | this.container.appendChild(this.renderer.domElement) 23 | this.renderer.setSize(window.innerWidth, window.innerHeight) 24 | } 25 | 26 | getSizes() { 27 | let aspect = 9/16 28 | let width = window.innerWidth 29 | let height = window.innerHeight 30 | return { 31 | left: aspect * width / -2, 32 | right: aspect * width / 2, 33 | top: height / 2, 34 | bottom: height / -2 35 | } 36 | } 37 | 38 | setupCamera() { 39 | let sizes = this.getSizes() 40 | this.camera = new THREE.OrthographicCamera(sizes.left, sizes.right, sizes.top, sizes.bottom, 0, 10) 41 | this.camera.position.x = 0 42 | this.camera.position.y = 0 43 | this.camera.position.z = 1 44 | } 45 | 46 | setupGhosts(ghosts) { 47 | this.ghostsInitialised = true 48 | this.ghosts = ghosts 49 | this.totalGhosts = this.ghosts.length 50 | this.setupGhostTextures() 51 | } 52 | 53 | setupWebcam() { 54 | this.webcamInitialised = true 55 | } 56 | 57 | setupVideoTexture() { 58 | this.video = this.container.querySelector('.player__video') 59 | this.videoTexture = new THREE.Texture(this.video) 60 | this.videoTexture.minFilter = THREE.LinearFilter 61 | this.videoTexture.magFilter = THREE.LinearFilter 62 | } 63 | 64 | setupWebcamTexture() { 65 | this.webcam = this.container.querySelector('.player__webcam') 66 | this.webcamTexture = new THREE.Texture(this.webcam) 67 | this.webcamTexture.minFilter = THREE.LinearFilter 68 | this.webcamTexture.magFilter = THREE.LinearFilter 69 | } 70 | 71 | setupGhostTextures() { 72 | this.currentGhostTexture = 0 73 | this.ghostTextures = [] 74 | THREE.ImageUtils.crossOrigin = '' 75 | this.ghosts.forEach((ghost) => { 76 | ghost.crossOrigin = '' 77 | let ghostTexture = THREE.ImageUtils.loadTexture(ghost.src) 78 | ghostTexture.minFilter = THREE.LinearFilter 79 | ghostTexture.magFilter = THREE.LinearFilter 80 | ghostTexture.onload = () => { 81 | ghostTexture.needsUpdate = true 82 | } 83 | this.ghostTextures.push(ghostTexture) 84 | setInterval(() => { 85 | this.ghostNr = (this.ghostNr + 1) % 3 86 | this.uniforms.ghostNr.value = this.ghostNr 87 | }, 50) 88 | }) 89 | } 90 | 91 | setupVariables() { 92 | this.videoNr = 0 93 | this.ghostNr = 0 94 | this.ghostVisible = 0 95 | this.webcamVisible = 0 96 | this.webcamStreaming = 0 97 | } 98 | 99 | showVideo(frame) { 100 | this.switchToVideo() 101 | this.uniforms.videoNr.value = frame - 1 102 | return this.uniforms.videoNr.value + 1 103 | } 104 | 105 | switchToVideo() { 106 | this.webcamVisible = false 107 | this.uniforms.webcamVisible.value = this.webcamVisible 108 | this.ghostVisible = false 109 | this.uniforms.ghostVisible.value = this.ghostVisible 110 | return this.uniforms.videoNr.value + 1 111 | } 112 | 113 | showWebcam() { 114 | if(this.webcamInitialised) { 115 | this.webcamVisible = true 116 | this.uniforms.webcamVisible.value = this.webcamVisible 117 | if(this.webcamVisible) { 118 | return 3; 119 | } else { 120 | return this.uniforms.videoNr.value + 1; 121 | } 122 | } 123 | } 124 | 125 | showGhost() { 126 | if(this.ghostsInitialised) { 127 | this.uniforms.ghost.value = this.ghostTextures[(this.currentGhostTexture++) % this.totalGhosts] 128 | this.ghostVisible = true 129 | this.uniforms.ghostVisible.value = Number(this.ghostVisible) 130 | } 131 | } 132 | 133 | getCurrentVideoFrame() { 134 | return this.uniforms.videoNr.value 135 | } 136 | 137 | resetImages() { 138 | if(this.webcamInitialised) { 139 | this.showWebcam() 140 | } else { 141 | this.showVideo(1) 142 | } 143 | } 144 | 145 | setupMaterial() { 146 | this.uniforms = { 147 | video: { 148 | type: 't', 149 | value: this.videoTexture 150 | }, 151 | videoNr: { 152 | type: 'i', 153 | value: this.videoNr 154 | }, 155 | webcam: { 156 | type: 't', 157 | value: this.webcamTexture 158 | }, 159 | webcamVisible: { 160 | type: 'i', 161 | value: this.webcamVisible 162 | }, 163 | ghost: { 164 | type: 't', 165 | value: null 166 | }, 167 | ghostNr: { 168 | type: 'i', 169 | value: this.ghostNr 170 | }, 171 | ghostVisible: { 172 | type: 'i', 173 | value: this.ghostVisible 174 | }, 175 | underlay: { 176 | type: 't', 177 | value: THREE.ImageUtils.loadTexture("/media/texture.jpg") 178 | } 179 | } 180 | this.material = new THREE.ShaderMaterial({vertexShader, fragmentShader, uniforms: this.uniforms}) 181 | } 182 | 183 | getGeometrySize() { 184 | let width = window.innerWidth 185 | let height = window.innerHeight 186 | if(height >= width) { 187 | return Math.max(width, height) 188 | } else { 189 | return Math.min(width, height) 190 | } 191 | } 192 | 193 | setupGeometry() { 194 | let size = this.getGeometrySize() 195 | this.geometry = new THREE.PlaneGeometry(size, size, 1, 1) 196 | this.mesh = new THREE.Mesh(this.geometry, this.material) 197 | this.scene.add(this.mesh) 198 | } 199 | 200 | updateGeometry() { 201 | this.scene.remove(this.mesh) 202 | let size = this.getGeometrySize() 203 | this.geometry = new THREE.PlaneGeometry(size, size, 1, 1) 204 | this.mesh = new THREE.Mesh(this.geometry, this.material) 205 | this.scene.add(this.mesh) 206 | } 207 | 208 | onWindowResize() { 209 | this.updateGeometry() 210 | this.renderer.setSize(window.innerWidth, window.innerHeight) 211 | 212 | let sizes = this.getSizes() 213 | this.camera.left = sizes.left 214 | this.camera.right = sizes.right 215 | this.camera.top = sizes.top 216 | this.camera.bottom = sizes.bottom 217 | this.camera.updateProjectionMatrix() 218 | } 219 | 220 | animate() { 221 | if (this.video.readyState === this.video.HAVE_ENOUGH_DATA) { 222 | if (this.videoTexture) this.videoTexture.needsUpdate = true 223 | } 224 | if (this.webcam && this.webcam.readyState === this.webcam.HAVE_ENOUGH_DATA) { 225 | if (this.webcamTexture) this.webcamTexture.needsUpdate = true 226 | } 227 | 228 | this.renderer.render(this.scene, this.camera) 229 | requestAnimationFrame(this.animate.bind(this)) 230 | } 231 | 232 | static start(view) { 233 | Scene.instance = new Scene(view) 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/lib/webgl/fragment.glsl: -------------------------------------------------------------------------------- 1 | #ifdef GL_ES 2 | precision highp float; 3 | #endif 4 | uniform sampler2D video; 5 | uniform sampler2D ghost; 6 | uniform sampler2D webcam; 7 | uniform sampler2D underlay; 8 | uniform int videoNr; 9 | uniform int ghostNr; 10 | uniform int ghostVisible; 11 | uniform int webcamVisible; 12 | varying vec2 vUv; 13 | 14 | void main(void) 15 | { 16 | if(webcamVisible == 1) { 17 | 18 | vec4 colourblend = texture2D(webcam, vec2(vUv.x, vUv.y)); 19 | vec4 scaledColor = colourblend * vec4(0.3, 0.59, 0.11, 1.0); 20 | float luminance = scaledColor.r + scaledColor.g + scaledColor.b; 21 | vec4 blend = vec4(vec3(luminance, luminance, luminance), colourblend.a); 22 | 23 | vec4 base = texture2D(underlay, vUv); 24 | vec4 screen = (1.0 - ((1.0 - base) * (1.0 - blend))); 25 | // vec4 overlay = (length(base) < 0.5 ? (2.0 * base * blend) : (1.0 - 2.0 * (1.0 - base) * (1.0 - blend))); 26 | 27 | vec4 mask = texture2D(video, vec2(2.0 / 3.0 + vUv.x / 3.0, vUv.y)); 28 | 29 | gl_FragColor = 1.0 - mask * (1.0 - screen); 30 | 31 | } else { 32 | 33 | if(ghostVisible == 0) { 34 | 35 | float offset = 1.0 * float(videoNr) / 3.0; 36 | vec4 blend = texture2D(video, vec2(offset + vUv.x / 3.0, vUv.y)); 37 | vec4 base = texture2D(underlay, vUv); 38 | vec4 screen = (1.0 - ((1.0 - base) * (1.0 - blend))); 39 | // vec4 overlay = (length(base) < 0.5 ? (2.0 * base * blend) : (1.0 - 2.0 * (1.0 - base) * (1.0 - blend))); 40 | 41 | vec4 mask = texture2D(video, vec2(2.0 / 3.0 + vUv.x / 3.0, vUv.y)); 42 | 43 | gl_FragColor = 1.0 - mask * (1.0 - screen); 44 | 45 | } else { 46 | 47 | float offset = 1.0 * float(ghostNr) / 3.0; 48 | vec4 blend = texture2D(ghost, vec2(offset + vUv.x / 3.0, vUv.y)); 49 | vec4 base = texture2D(underlay, vUv); 50 | vec4 screen = (1.0 - ((1.0 - base) * (1.0 - blend))); 51 | // vec4 overlay = (length(base) < 0.5 ? (2.0 * base * blend) : (1.0 - 2.0 * (1.0 - base) * (1.0 - blend))); 52 | 53 | vec4 mask = texture2D(video, vec2(2.0 / 3.0 + vUv.x / 3.0, vUv.y)); 54 | 55 | gl_FragColor = 1.0 - mask * (1.0 - screen); 56 | // gl_FragColor = blend; 57 | } 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/lib/webgl/vertex.glsl: -------------------------------------------------------------------------------- 1 | #ifdef GL_ES 2 | precision highp float; 3 | #endif 4 | 5 | varying vec2 vUv; 6 | 7 | void main() 8 | { 9 | vUv = uv; 10 | vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 ); 11 | gl_Position = projectionMatrix * mvPosition; 12 | } 13 | -------------------------------------------------------------------------------- /src/media/texture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superhighfives/i-will-never-let-you-go-archive/6fe20eebe3aad315cd8507550521c29f5e3acffd/src/media/texture.jpg --------------------------------------------------------------------------------