├── .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 | 
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 | 
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 |
--------------------------------------------------------------------------------
/src/assets/svg/icon-cross.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/svg/icon-dots-one.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/svg/icon-dots-two.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/svg/icon-facebook.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/svg/icon-play-circle.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/svg/icon-play.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/svg/icon-replay.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/svg/icon-skip.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/svg/icon-soundcloud.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/svg/icon-spinner.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/svg/icon-tick.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/svg/icon-twitter.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/svg/icon-webcam.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/svg/icon-youtube.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/svg/svg.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
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
--------------------------------------------------------------------------------