├── Procfile ├── public ├── back.png ├── favicon.ico ├── og_image.jpg ├── rewind.png ├── spinner.gif ├── favicon-16x16.png ├── favicon-32x32.png ├── logo-beijing.png ├── logo-berlin.png ├── logo-gwangju.png ├── logo-london.png ├── logo-birmingham.png ├── logo-netherlands.png ├── logo-saintbrieuc.png ├── compiled │ ├── global.js │ ├── interface.js │ ├── clock.js │ ├── api.js │ ├── note.js │ ├── frontend.js │ ├── VideoPlayer.js │ └── drawingCanvas.js ├── browser.html ├── mobile.html ├── bowser.min.js ├── simplify.js └── index.html ├── .gitignore ├── typescript ├── global.ts ├── interface.ts ├── clock.ts ├── api.ts ├── Note.ts ├── frontend.ts ├── VideoPlayer.ts └── drawingCanvas.ts ├── app.json ├── typings ├── tsd.d.ts ├── google.analytics │ └── ga.d.ts ├── youtube │ └── youtube.d.ts ├── svgjs │ └── svgjs.d.ts └── moment │ └── moment.d.ts ├── uploader └── upload.sh ├── .nginx ├── boring.js ├── tsd.json ├── package.json ├── license.md ├── boring ├── regexes.json └── literals.json ├── index.js ├── api.js ├── sass └── style.scss └── readme.md /Procfile: -------------------------------------------------------------------------------- 1 | web: node index.js 2 | -------------------------------------------------------------------------------- /public/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylemcdonald/ExhaustingACrowd/HEAD/public/back.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylemcdonald/ExhaustingACrowd/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/og_image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylemcdonald/ExhaustingACrowd/HEAD/public/og_image.jpg -------------------------------------------------------------------------------- /public/rewind.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylemcdonald/ExhaustingACrowd/HEAD/public/rewind.png -------------------------------------------------------------------------------- /public/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylemcdonald/ExhaustingACrowd/HEAD/public/spinner.gif -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylemcdonald/ExhaustingACrowd/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylemcdonald/ExhaustingACrowd/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/logo-beijing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylemcdonald/ExhaustingACrowd/HEAD/public/logo-beijing.png -------------------------------------------------------------------------------- /public/logo-berlin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylemcdonald/ExhaustingACrowd/HEAD/public/logo-berlin.png -------------------------------------------------------------------------------- /public/logo-gwangju.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylemcdonald/ExhaustingACrowd/HEAD/public/logo-gwangju.png -------------------------------------------------------------------------------- /public/logo-london.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylemcdonald/ExhaustingACrowd/HEAD/public/logo-london.png -------------------------------------------------------------------------------- /public/logo-birmingham.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylemcdonald/ExhaustingACrowd/HEAD/public/logo-birmingham.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | public/style.css 4 | .DS_Store 5 | client_secret.json 6 | .sass-cache 7 | *.css.map -------------------------------------------------------------------------------- /public/logo-netherlands.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylemcdonald/ExhaustingACrowd/HEAD/public/logo-netherlands.png -------------------------------------------------------------------------------- /public/logo-saintbrieuc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylemcdonald/ExhaustingACrowd/HEAD/public/logo-saintbrieuc.png -------------------------------------------------------------------------------- /typescript/global.ts: -------------------------------------------------------------------------------- 1 | class GLOBAL { 2 | static MODE:string = "PLAYER"; 3 | 4 | static playerMode() { 5 | return GLOBAL.MODE == 'PLAYER'; 6 | } 7 | 8 | static editorMode() { 9 | return GLOBAL.MODE == 'EDITOR'; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Node.js Getting Started", 3 | "description": "A barebones Node.js app using Express 4", 4 | "repository": "https://github.com/heroku/node-js-getting-started", 5 | "logo": "http://node-js-sample.herokuapp.com/node.svg", 6 | "keywords": ["node", "express", "static"] 7 | } 8 | -------------------------------------------------------------------------------- /typings/tsd.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | /// 6 | /// 7 | -------------------------------------------------------------------------------- /uploader/upload.sh: -------------------------------------------------------------------------------- 1 | youtube-upload \ 2 | --client-secrets=client_secret.json \ 3 | --privacy=unlisted \ 4 | --description="http://www.exhaustingacrowd.com/" \ 5 | --title="Exhausting a Crowd" \ 6 | --title-template="{title} ({n}/{total})" \ 7 | --location="latitude=40.687730, longitude=-73.979779, altitude=0" \ 8 | /Volumes/LaCie/$1/*.MP4 -------------------------------------------------------------------------------- /.nginx: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name exhaustingacrowd.com www.exhaustingacrowd.com; 4 | 5 | location / { 6 | proxy_set_header X-Forwarded-For $remote_addr; 7 | proxy_set_header Host $http_host; 8 | proxy_pass "http://localhost:5000"; 9 | } 10 | } -------------------------------------------------------------------------------- /public/compiled/global.js: -------------------------------------------------------------------------------- 1 | var GLOBAL = (function () { 2 | function GLOBAL() { 3 | } 4 | GLOBAL.playerMode = function () { 5 | return GLOBAL.MODE == 'PLAYER'; 6 | }; 7 | GLOBAL.editorMode = function () { 8 | return GLOBAL.MODE == 'EDITOR'; 9 | }; 10 | GLOBAL.MODE = "PLAYER"; 11 | return GLOBAL; 12 | })(); 13 | -------------------------------------------------------------------------------- /boring.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var regexes = require('./boring/regexes.json'); 4 | var literals = require('./boring/literals.json').join('|'); 5 | var regexStr = regexes.join('|') + '|\\b(' + literals + ')\\b'; 6 | var psqlRegex = regexStr.replace(/'/g, "''").replace(/\\b/g, "\\y"); 7 | var regex = new RegExp(regexStr); 8 | 9 | module.exports = { 10 | check: function (text) { 11 | if(text) { 12 | var result = text.toLowerCase().match(regex); 13 | return result ? result[0] : null; 14 | } else { 15 | return null; 16 | } 17 | }, 18 | getRegexes: function() { return regexes; }, 19 | getRegex: function() { return regex.toString(); }, 20 | getPsqlRegex: function() { return psqlRegex; } 21 | } -------------------------------------------------------------------------------- /tsd.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "v4", 3 | "repo": "borisyankov/DefinitelyTyped", 4 | "ref": "master", 5 | "path": "typings", 6 | "bundle": "typings/tsd.d.ts", 7 | "installed": { 8 | "underscore/underscore.d.ts": { 9 | "commit": "61d066fef5a1ac9e2389b51b9cf88746b7bbfde9" 10 | }, 11 | "jquery/jquery.d.ts": { 12 | "commit": "61d066fef5a1ac9e2389b51b9cf88746b7bbfde9" 13 | }, 14 | "youtube/youtube.d.ts": { 15 | "commit": "61d066fef5a1ac9e2389b51b9cf88746b7bbfde9" 16 | }, 17 | "svgjs/svgjs.d.ts": { 18 | "commit": "00e7f3fede5f2df55b56c72f40a144824f0f0bfa" 19 | }, 20 | "moment/moment.d.ts": { 21 | "commit": "70737c2a2496f7a13c4da63eb00081cad94d19f1" 22 | }, 23 | "google.analytics/ga.d.ts": { 24 | "commit": "484544d14d400190b20f270341c97b16adc0f1ef" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "exhausting-a-crowd", 3 | "version": "0.2.0", 4 | "description": "Exhausting a crowd", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js" 8 | }, 9 | "dependencies": { 10 | "@types/google.analytics": "0.0.40", 11 | "@types/jquery": "^3.5.4", 12 | "@types/moment": "^2.13.0", 13 | "@types/underscore": "^1.10.24", 14 | "@types/youtube": "0.0.40", 15 | "basic-auth": "^1.0.1", 16 | "body-parser": "^1.12.2", 17 | "express": "~4.9.x", 18 | "mobile-detect": "^1.2.0", 19 | "node-sass-middleware": "^0.11.0", 20 | "pg": "^4.3.0", 21 | "pg-query": "^0.11.0", 22 | "raven": "^0.7.3", 23 | "request": "^2.55.0", 24 | "serve-favicon": "^2.2.1", 25 | "typescript-compiler": "^1.4.1-2", 26 | "typescript-middleware": "^0.5.3" 27 | }, 28 | "engines": { 29 | "node": "12.x", 30 | "npm": "6.x" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/kylemcdonald/exhaustingacrowd" 35 | }, 36 | "keywords": [ 37 | "node", 38 | "heroku", 39 | "express" 40 | ], 41 | "license": "MIT" 42 | } 43 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | The code in this repository is available under the [MIT License](https://secure.wikimedia.org/wikipedia/en/wiki/Mit_license). 2 | 3 | Copyright (c) 2015- Kyle McDonald & Jonas Jongejan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /public/browser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
EXHAUSTING A CROWD
16 |
17 | We are really sorry, but your browser is not supported. 18 |
Please use Chrome or Safari instead. 19 |
20 | 24 |
25 | 26 | 27 | 28 | 29 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /typescript/interface.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | interface IInterfaceCallbacks { 5 | onResize?: (() => void); 6 | } 7 | 8 | class Interface { 9 | 10 | private events:IInterfaceCallbacks; 11 | 12 | constructor(events:IInterfaceCallbacks){ 13 | this.events = events; 14 | 15 | $(window).resize(()=>{ 16 | if(this.events.onResize) this.events.onResize(); 17 | }); 18 | } 19 | 20 | hideLoadingScreen(){ 21 | $('#initial-spinner').animate({ 22 | opacity: '0' 23 | }, 250); 24 | $('#loading').animate({ 25 | opacity: "0" 26 | }, 1000, () =>{ 27 | $('#transition').hide(); 28 | $('#loading').hide(); 29 | $('#persistent').show(); 30 | } 31 | ); 32 | } 33 | 34 | 35 | hideVideo(cb?:()=>void){ 36 | var e = $('#persistent-spinner'); 37 | e.show(); 38 | e.animate({ opacity: "1" }, 250, ()=>{ 39 | $('#videocontainer').addClass('blur'); 40 | setTimeout(cb, 250); 41 | }) 42 | } 43 | 44 | showVideo(cb?:()=>void){ 45 | var e = $('#persistent-spinner'); 46 | e.animate({ opacity: "0" }, 250, ()=>{ 47 | e.hide(); 48 | $('#videocontainer').removeClass('blur'); 49 | setTimeout(cb, 250); 50 | }) 51 | } 52 | 53 | 54 | showCredits(){ 55 | $('#overlay').animate({opacity:"0"},250); 56 | $('#linedrawing').animate({opacity:"0"},250); 57 | $('#credits') 58 | .show() 59 | .animate({opacity:"1"}, 500); 60 | } 61 | 62 | hideCredits(){ 63 | var e = $('#credits'); 64 | e.animate({ opacity: "0" }, 500, ()=>{ 65 | $('#credits').hide(); 66 | }); 67 | $('#overlay').animate({opacity:"1"},500); 68 | $('#linedrawing').animate({opacity:"1"},500); 69 | 70 | } 71 | 72 | } -------------------------------------------------------------------------------- /public/compiled/interface.js: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | var Interface = (function () { 4 | function Interface(events) { 5 | var _this = this; 6 | this.events = events; 7 | $(window).resize(function () { 8 | if (_this.events.onResize) 9 | _this.events.onResize(); 10 | }); 11 | } 12 | Interface.prototype.hideLoadingScreen = function () { 13 | $('#initial-spinner').animate({ 14 | opacity: '0' 15 | }, 250); 16 | $('#loading').animate({ 17 | opacity: "0" 18 | }, 1000, function () { 19 | $('#transition').hide(); 20 | $('#loading').hide(); 21 | $('#persistent').show(); 22 | }); 23 | }; 24 | Interface.prototype.hideVideo = function (cb) { 25 | var e = $('#persistent-spinner'); 26 | e.show(); 27 | e.animate({ opacity: "1" }, 250, function () { 28 | $('#videocontainer').addClass('blur'); 29 | setTimeout(cb, 250); 30 | }); 31 | }; 32 | Interface.prototype.showVideo = function (cb) { 33 | var e = $('#persistent-spinner'); 34 | e.animate({ opacity: "0" }, 250, function () { 35 | e.hide(); 36 | $('#videocontainer').removeClass('blur'); 37 | setTimeout(cb, 250); 38 | }); 39 | }; 40 | Interface.prototype.showCredits = function () { 41 | $('#overlay').animate({ opacity: "0" }, 250); 42 | $('#linedrawing').animate({ opacity: "0" }, 250); 43 | $('#credits').show().animate({ opacity: "1" }, 500); 44 | }; 45 | Interface.prototype.hideCredits = function () { 46 | var e = $('#credits'); 47 | e.animate({ opacity: "0" }, 500, function () { 48 | $('#credits').hide(); 49 | }); 50 | $('#overlay').animate({ opacity: "1" }, 500); 51 | $('#linedrawing').animate({ opacity: "1" }, 500); 52 | }; 53 | return Interface; 54 | })(); 55 | -------------------------------------------------------------------------------- /public/compiled/clock.js: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | var Clock = (function () { 4 | function Clock() { 5 | var _this = this; 6 | this.clockTime = new Date(Clock.startTime); 7 | this.colon = $('#colon'); 8 | setInterval(function () { 9 | _this.updateClock(); 10 | }, 1000); 11 | } 12 | Clock.prototype.blink = function (elm) { 13 | if (elm.css('opacity') == '1') { 14 | elm.css('opacity', '0'); 15 | } 16 | else { 17 | elm.css('opacity', '1'); 18 | } 19 | }; 20 | Clock.prototype.updateClock = function () { 21 | var date = this.clockTime; 22 | date.setSeconds(date.getSeconds() + 1); 23 | var hours = date.getHours(); 24 | var minutes = date.getMinutes(); 25 | var language = (navigator.language || navigator.browserLanguage).split('-')[0]; 26 | var minutesString, hoursString; 27 | var dateString = date.toLocaleString(language); 28 | if (dateString.match(/am|pm/i) || date.toString().match(/am|pm/i)) { 29 | //12 hour clock 30 | var ampm = hours >= 12 ? 'PM' : 'AM'; 31 | minutesString = minutes < 10 ? '0' + String(minutes) : String(minutes); 32 | minutesString += ' ' + ampm; 33 | hours = hours % 12; 34 | hoursString = hours < 1 ? '12' : String(hours); 35 | } 36 | else { 37 | //24 hour clock 38 | minutesString = minutes < 10 ? '0' + String(minutes) : String(minutes); 39 | hoursString = String(hours); 40 | } 41 | this.blink(this.colon); 42 | $('#hour').html(hoursString.replace(/0/g, 'O')); 43 | $('#minute').html(minutesString.replace(/0/g, 'O')); 44 | }; 45 | Clock.prototype.frameUpdate = function (ytplayer) { 46 | this.clockTime = new Date(Clock.startTime); 47 | this.clockTime.setSeconds(this.clockTime.getSeconds() + ytplayer.currentTime / 1000.0); 48 | }; 49 | return Clock; 50 | })(); 51 | -------------------------------------------------------------------------------- /typescript/clock.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | 5 | class Clock { 6 | 7 | static startTime; 8 | private colon : JQuery; 9 | public clockTime : Date; 10 | 11 | constructor(){ 12 | this.clockTime = new Date(Clock.startTime); 13 | this.colon = $('#colon'); 14 | 15 | setInterval(()=>{this.updateClock()}, 1000); 16 | } 17 | 18 | blink(elm) { 19 | if (elm.css('opacity') == '1') { 20 | elm.css('opacity', '0'); 21 | } else { 22 | elm.css('opacity', '1'); 23 | } 24 | } 25 | 26 | updateClock(){ 27 | var date = this.clockTime; 28 | date.setSeconds(date.getSeconds() + 1); 29 | 30 | var hours = date.getHours(); 31 | var minutes = date.getMinutes(); 32 | 33 | var language = (navigator.language || navigator.browserLanguage).split('-')[0]; 34 | var minutesString, hoursString; 35 | var dateString = date.toLocaleString(language); 36 | 37 | if (dateString.match(/am|pm/i) || date.toString().match(/am|pm/i) ) 38 | { 39 | //12 hour clock 40 | var ampm = hours >= 12 ? 'PM' : 'AM'; 41 | minutesString = minutes < 10 ? '0' + String(minutes) : String(minutes); 42 | minutesString += ' ' + ampm; 43 | hours = hours % 12; 44 | hoursString = hours < 1 ? '12' : String(hours); 45 | } 46 | else 47 | { 48 | //24 hour clock 49 | minutesString = minutes < 10 ? '0' + String(minutes) : String(minutes); 50 | hoursString = String(hours); 51 | } 52 | 53 | this.blink(this.colon); 54 | $('#hour').html(hoursString.replace(/0/g, 'O')); 55 | $('#minute').html(minutesString.replace(/0/g, 'O') ); 56 | } 57 | 58 | 59 | frameUpdate(ytplayer : VideoPlayer) { 60 | this.clockTime = new Date(Clock.startTime); 61 | this.clockTime.setSeconds(this.clockTime.getSeconds()+ ytplayer.currentTime/1000.0) 62 | } 63 | 64 | 65 | } -------------------------------------------------------------------------------- /public/mobile.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | EXHAUSTING A CROWD 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 |
24 |
25 |

EXHAUSTING A CROWD

26 |

by KYLE MCDONALD

27 |

— UNAVAILABLE on MOBILE —

28 |
29 |
30 | 31 |
32 | 33 | 42 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /typescript/api.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | 5 | class NotesApi { 6 | public notes:Note[] = []; 7 | public currentTime:number = 0; 8 | 9 | fetchRate:number; 10 | fetchWindowSize:number; 11 | 12 | siteId:number; 13 | 14 | constructor(site:number) { 15 | this.siteId = site; 16 | } 17 | 18 | startFetching(fetchRate:number, fetchWindowSize:number){ 19 | this.fetchRate = fetchRate; 20 | this.fetchWindowSize = fetchWindowSize; 21 | 22 | setInterval(()=>{this.fetchNotes()}, 15000); 23 | 24 | this.fetchNotes(); 25 | } 26 | 27 | fetchNotes(_currentTime?:number){ 28 | if(_currentTime){ 29 | this.currentTime = _currentTime; 30 | } 31 | //console.log("Fetch",this.fetchWindowSize,this.currentTime); 32 | $.ajax({ 33 | dataType: "json", 34 | url: "/api/notes", 35 | data: { 36 | timeframeStart: this.currentTime-2000, 37 | timeframeEnd: this.currentTime+this.fetchWindowSize, 38 | site: this.siteId 39 | }, 40 | success: (data:any)=>{ 41 | for(var i=0;i{ 55 | //console.log("Submit ", note.path.points, note.text); 56 | 57 | ga('send', 'event', 'API', 'SubmitNote', 'submit'); 58 | $.ajax({ 59 | type: "POST", 60 | dataType: "json", 61 | url: "/api/notes", 62 | data: JSON.stringify({path: note.path.points, text: note.text, site:this.siteId}), 63 | contentType: "application/json; charset=utf-8", 64 | success: () => { 65 | setTimeout(()=>{ 66 | this.fetchNotes(); 67 | },300); 68 | 69 | } 70 | }); 71 | }, 5000); 72 | 73 | submitNote(note:Note){ 74 | this.submitNoteThrottle(note); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /boring/regexes.json: -------------------------------------------------------------------------------- 1 | [ 2 | "(asd|sdf|ghg|hgj|uyt|hdh|xz|cx|vc|vb|cv|zx|wq|qw|jh|hj|xv|bz|jj)", 3 | "(for|4)ever ?alo", 4 | "(q|sd|df|ds|as|fs|ff|gh|gf|fg|fa|fd|hg|sf|jg|da|ad|kj|jk|dg|af|iu|gs){2}", 5 | "(st|nd|rd) base", 6 | "4chan", 7 | "9gag", 8 | "\\b69", 9 | "===", 10 | "\\*\\*", 11 | "\\ball+a", 12 | "\\bass+\\b|a s s|a\\$|booty|butts|butt\\b", 13 | "\\bbj|skin ?flute", 14 | "\\bcrack\\b", 15 | "\\bcurve", 16 | "\\bdbag", 17 | "\\bisis", 18 | "\\brape|rapis", 19 | "\\bsex|s e x", 20 | "\\btest", 21 | "\\bthrob", 22 | "\\btroll\\b", 23 | "^[a-z0-9!#$\\'*,./<>?@^_`]$", 24 | "aids", 25 | "akbar", 26 | "aussie", 27 | "b.(ya)?tc|bi+she", 28 | "\\b(gang)?bang", 29 | "bastard", 30 | "bdsm", 31 | "bieb", 32 | "black (dude|guy|man)", 33 | "blah? ?bla", 34 | "boo+b|nipp", 35 | "bugger", 36 | "cancer", 37 | "chav", 38 | "cleav", 39 | "cocai|cok", 40 | "condom", 41 | "dealer", 42 | "dick|d i c k|dik|penis|c.ck\\b|c o c k", 43 | "doing (him|his|her)\\b", 44 | "douch", 45 | "dumbas", 46 | "dru+g", 47 | "drunk", 48 | "dyke", 49 | "ecst", 50 | "erect", 51 | "f(u|\\*)+c|fck|fuq|\\bf th| f'", 52 | "fapp", 53 | "fart", 54 | "fat\\b|fatso", 55 | "for a good time", 56 | "gay|homo|lesbia", 57 | "genit", 58 | "ginger", 59 | "grind", 60 | "hawt", 61 | "hell\\b", 62 | "heroin", 63 | "herp", 64 | "(so+|get|little) high\\b", 65 | "hitl", 66 | "hooker", 67 | "hottie|(she|chick|girl).+hot\\b|\\bhot.+(girl|chick|blond|mama)|^.{0,7}\\bhot.{0,3}$", 68 | "hussey", 69 | "http|www\\.|[^.]\\.(co|ne|or|uk)", 70 | "i'm hard", 71 | "jail ?bait", 72 | "jerk (it|off)", 73 | "jew[is]?\\b", 74 | "jiha", 75 | "[yj]ig+le", 76 | "junk(?! ?food)", 77 | "kanke", 78 | "kkk", 79 | "kyle", 80 | "legs", 81 | "lorem", 82 | "lol ?jk|jk ?lol", 83 | "mast[ue]rb|rubbing? one", 84 | "milf", 85 | "most ?wanted", 86 | "nice (bum|bod|bot|bac|rack|and)", 87 | "[nw][i1]gg", 88 | "oral", 89 | "pant[yi]", 90 | "\\bpee+\\b", 91 | "pedo(?! de)", 92 | "perv", 93 | "\\bpoon", 94 | "poo([oeph]|\\b)", 95 | "po+rn", 96 | "prick", 97 | "prost", 98 | "pube", 99 | "puss", 100 | "racial", 101 | "reach around", 102 | "rear", 103 | "reddit", 104 | "serial killer", 105 | "sh(i+|\\*)t", 106 | "shave", 107 | "skin ?head", 108 | "smack", 109 | "sperm", 110 | "stripper", 111 | "sucky|suck it", 112 | "terroris", 113 | "threesome", 114 | "thugs|a thug", 115 | "tongue", 116 | "total babes?|babes", 117 | "trans[veg]", 118 | "ugly", 119 | "up-?skirt", 120 | "unzip", 121 | "virgin", 122 | "wank", 123 | "(a|my|of) wee\\b", 124 | "wedg", 125 | "weed|dime bag|blunt|\\bgram|bong|joint|marij|\\bpot\\b", 126 | "xtc" 127 | ] -------------------------------------------------------------------------------- /public/compiled/api.js: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | var NotesApi = (function () { 4 | function NotesApi(site) { 5 | var _this = this; 6 | this.notes = []; 7 | this.currentTime = 0; 8 | this.submitNoteThrottle = _.throttle(function (note) { 9 | //console.log("Submit ", note.path.points, note.text); 10 | ga('send', 'event', 'API', 'SubmitNote', 'submit'); 11 | $.ajax({ 12 | type: "POST", 13 | dataType: "json", 14 | url: "/api/notes", 15 | data: JSON.stringify({ path: note.path.points, text: note.text, site: _this.siteId }), 16 | contentType: "application/json; charset=utf-8", 17 | success: function () { 18 | setTimeout(function () { 19 | _this.fetchNotes(); 20 | }, 300); 21 | } 22 | }); 23 | }, 5000); 24 | this.siteId = site; 25 | } 26 | NotesApi.prototype.startFetching = function (fetchRate, fetchWindowSize) { 27 | var _this = this; 28 | this.fetchRate = fetchRate; 29 | this.fetchWindowSize = fetchWindowSize; 30 | setInterval(function () { 31 | _this.fetchNotes(); 32 | }, 15000); 33 | this.fetchNotes(); 34 | }; 35 | NotesApi.prototype.fetchNotes = function (_currentTime) { 36 | var _this = this; 37 | if (_currentTime) { 38 | this.currentTime = _currentTime; 39 | } 40 | //console.log("Fetch",this.fetchWindowSize,this.currentTime); 41 | $.ajax({ 42 | dataType: "json", 43 | url: "/api/notes", 44 | data: { 45 | timeframeStart: this.currentTime - 2000, 46 | timeframeEnd: this.currentTime + this.fetchWindowSize, 47 | site: this.siteId 48 | }, 49 | success: function (data) { 50 | for (var i = 0; i < data.length; i++) { 51 | var existingNote = _.where(_this.notes, { id: data[i].id }); 52 | if (existingNote.length == 0) { 53 | //console.log("Add note"); 54 | var note = new Note(data[i]); 55 | _this.notes.push(note); 56 | } 57 | } 58 | } 59 | }); 60 | }; 61 | NotesApi.prototype.submitNote = function (note) { 62 | this.submitNoteThrottle(note); 63 | }; 64 | return NotesApi; 65 | })(); 66 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var favicon = require('serve-favicon'); 2 | var MobileDetect = require('mobile-detect'); 3 | var express = require('express'); 4 | var sassMiddleware = require('node-sass-middleware'); 5 | 6 | var path = require('path'); 7 | var raven = require('raven'); 8 | 9 | 10 | var client = new raven.Client('https://edf1ff6b26ca41b0a9bbb280902b8c4e:e709b93edcdf49aabf54f637c90bf6b0@app.getsentry.com/41348'); 11 | client.patchGlobal(); 12 | 13 | // Setup api app 14 | var api = require('./api'); 15 | api.setup(); 16 | 17 | // Setup express app 18 | var app = express(); 19 | 20 | // Put everything behind a username / password if PASSWORD is set 21 | if(process.env.PASSWORD) { 22 | var basicAuth = require('basic-auth'); 23 | var auth = function (req, res, next) { 24 | function unauthorized(res) { 25 | res.set('WWW-Authenticate', 'Basic realm=Authorization Required'); 26 | return res.sendStatus(401); 27 | }; 28 | var user = basicAuth(req); 29 | if (!user || !user.name || !user.pass) { 30 | return unauthorized(res); 31 | }; 32 | if (user.name === process.env.PASSWORD && user.pass === process.env.PASSWORD) { 33 | return next(); 34 | } else { 35 | return unauthorized(res); 36 | }; 37 | }; 38 | app.use('/', auth); 39 | } 40 | 41 | app.set('port', (process.env.PORT || 5000)); 42 | 43 | app.use(favicon(__dirname + '/public/favicon.ico')); 44 | 45 | // adding the sass middleware 46 | app.use(sassMiddleware({ 47 | src: path.join(__dirname,'sass'), 48 | dest: path.join(__dirname, 'public'), 49 | debug: false, 50 | outputStyle: 'compressed' 51 | })); 52 | 53 | app.use(raven.middleware.express('https://edf1ff6b26ca41b0a9bbb280902b8c4e:e709b93edcdf49aabf54f637c90bf6b0@app.getsentry.com/41348')); 54 | 55 | // Add new locations here. 56 | var sites = ['london', 'netherlands', 'birmingham', 'gwangju', 'beijing', 'saintbrieuc', 'berlin']; 57 | 58 | function randomChoice(array) { 59 | return array[Math.floor(Math.random()*array.length)]; 60 | } 61 | 62 | app.get('/', function(req, res) { 63 | // To redirect the main page during an exhibition, modify the next line. 64 | var location = 'berlin'; 65 | // var location = randomChoice(sites); 66 | res.redirect('/' + location); 67 | }); 68 | 69 | sites.forEach(function(site) { 70 | app.get('/' + site, function(req, res) { 71 | returnSite(req,res); 72 | }); 73 | }); 74 | 75 | app.use('/', express.static(__dirname + '/public')); 76 | 77 | // Host the api on /api 78 | app.use('/api', api.api); 79 | 80 | 81 | app.listen(app.get('port'), function() { 82 | console.log("Node app is running at localhost:" + app.get('port')); 83 | }); 84 | 85 | 86 | function returnSite(req,res){ 87 | md = new MobileDetect(req.headers['user-agent']); 88 | if(md.mobile()) { 89 | res.sendFile(__dirname + '/public/mobile.html'); 90 | } else { 91 | res.sendFile(__dirname + '/public/index.html'); 92 | } 93 | } -------------------------------------------------------------------------------- /typings/google.analytics/ga.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for Google Analytics (Classic and Universal) 2 | // Project: https://developers.google.com/analytics/devguides/collection/gajs/, https://developers.google.com/analytics/devguides/collection/analyticsjs/method-reference 3 | // Definitions by: Ronnie Haakon Hegelund , Pat Kujawa 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | 6 | declare class Tracker { 7 | _trackPageview(): void; 8 | _getName(): string; 9 | _getAccount(): string; 10 | _getVersion(): string; 11 | _getVisitorCustomVar(index: number): string; 12 | _setAccount(): string; 13 | _setCustomVar(index: number, name: string, value: string, opt_scope?: number): boolean; 14 | _setSampleRate(newRate: string): void; 15 | _setSessionCookieTimeout(cookieTimeoutMillis: number): void; 16 | _setSiteSpeedSampleRate(sampleRate: number): void; 17 | _setVisitorCookieTimeout(milliseconds: number): void; 18 | _trackPageLoadTime(): void; 19 | } 20 | 21 | interface GoogleAnalyticsCode { 22 | push(commandArray: string[]): void; 23 | push(func: Function): void; 24 | } 25 | 26 | interface GoogleAnalyticsTracker { 27 | _getTracker(account: string): Tracker; 28 | _createTracker(opt_account: string, opt_name?: string): Tracker; 29 | _getTrackerByName(opt_name?: string): Tracker; 30 | _anonymizeIp(): void; 31 | } 32 | 33 | interface GoogleAnalytics { 34 | type: string; 35 | src: string; 36 | async: boolean; 37 | } 38 | 39 | declare module UniversalAnalytics { 40 | // https://developers.google.com/analytics/devguides/collection/analyticsjs/method-reference 41 | 42 | interface ga { 43 | l: number; 44 | q: any[]; 45 | (command: string, poly: string, opt_poly?: {}): UniversalAnalytics.Tracker; 46 | (command: string, trackingId: string, auto: string, opt_configObject?: {}): UniversalAnalytics.Tracker; 47 | (command: string, hitDetails: {}): void; 48 | create(trackingId: string, opt_configObject?: {}): UniversalAnalytics.Tracker; 49 | create(trackingId: string, auto: string, opt_configObject?: {}): UniversalAnalytics.Tracker; 50 | getAll(): UniversalAnalytics.Tracker[]; 51 | getByName(name: string): UniversalAnalytics.Tracker; 52 | } 53 | 54 | interface Tracker { 55 | get(fieldName: string): T; 56 | send(hitType: string, opt_fieldObject?: {}): void; 57 | set(fieldName: string, value: string): void; 58 | set(fieldName: string, value: {}): void; 59 | set(fieldName: string, value: number): void; 60 | set(fieldName: string, value: boolean): void; 61 | } 62 | } 63 | 64 | declare var gaClassic: GoogleAnalytics; 65 | declare var ga: UniversalAnalytics.ga; 66 | declare var _gaq: GoogleAnalyticsCode; 67 | declare var _gat: GoogleAnalyticsTracker; 68 | -------------------------------------------------------------------------------- /public/compiled/note.js: -------------------------------------------------------------------------------- 1 | var PathPoint = (function () { 2 | function PathPoint(x, y, time) { 3 | this.x = x; 4 | this.y = y; 5 | this.time = time; 6 | // console.log(this); 7 | } 8 | return PathPoint; 9 | })(); 10 | var Path = (function () { 11 | function Path(json) { 12 | this.points = []; 13 | if (json && json.length > 0) { 14 | for (var i = 0; i < json.length; i++) { 15 | this.points.push(new PathPoint(json[i].x, json[i].y, json[i].time)); 16 | } 17 | // Check that the path is at least 3 sec long 18 | var diff = this.last().time - this.points[0].time; 19 | if (diff < 3000) { 20 | this.points[this.points.length - 1].time += 3000 - diff; 21 | } 22 | } 23 | } 24 | Path.prototype.push = function (point) { 25 | this.points.push(point); 26 | }; 27 | Path.prototype.last = function () { 28 | return _.last(this.points); 29 | }; 30 | Path.prototype.first = function () { 31 | return _.first(this.points); 32 | }; 33 | Path.prototype.getPosAtTime = function (time) { 34 | if (this.points.length == 0) { 35 | return; 36 | } 37 | if (time < this.points[0].time) { 38 | return undefined; 39 | } 40 | for (var i = 0; i < this.points.length; i++) { 41 | if (this.points[i].time >= time) { 42 | if (i == 0) { 43 | return this.points[i]; 44 | } 45 | var p = this.points[i - 1]; 46 | var n = this.points[i]; 47 | var pct = (time - p.time) / (n.time - p.time); 48 | return { 49 | x: p.x * (1 - pct) + n.x * pct, 50 | y: p.y * (1 - pct) + n.y * pct, 51 | time: time 52 | }; 53 | } 54 | } 55 | }; 56 | Path.prototype.simplify = function () { 57 | this.points = simplify(this.points, 0.002); 58 | }; 59 | return Path; 60 | })(); 61 | var Note = (function () { 62 | function Note(json) { 63 | if (json) { 64 | this.id = json.id; 65 | this.time_begin = json.time_begin; 66 | this.time_end = json.time_end; 67 | this.text = json.note; 68 | this.path = new Path(json.path); 69 | if (this.text) { 70 | var dur = this.time_end - this.time_begin; 71 | var minDur = this.text.length / 140 * 6000 + 3000; // 6 seconds to read 140 characters 72 | if (dur < minDur) { 73 | var newPoint = new PathPoint(this.path.last().x, this.path.last().y, this.path.first().time + minDur); 74 | this.path.push(newPoint); 75 | this.time_end = this.time_begin + minDur; 76 | } 77 | } 78 | } 79 | } 80 | return Note; 81 | })(); 82 | -------------------------------------------------------------------------------- /public/bowser.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bowser - a browser detector 3 | * https://github.com/ded/bowser 4 | * MIT License | (c) Dustin Diaz 2014 5 | */ 6 | !function(e,t){typeof module!="undefined"&&module.exports?module.exports.browser=t():typeof define=="function"&&define.amd?define(t):this[e]=t()}("bowser",function(){function t(t){function n(e){var n=t.match(e);return n&&n.length>1&&n[1]||""}var r=n(/(ipod|iphone|ipad)/i).toLowerCase(),i=/like android/i.test(t),s=!i&&/android/i.test(t),o=n(/version\/(\d+(\.\d+)?)/i),u=/tablet/i.test(t),a=!u&&/[^-]mobi/i.test(t),f;/opera|opr/i.test(t)?f={name:"Opera",opera:e,version:o||n(/(?:opera|opr)[\s\/](\d+(\.\d+)?)/i)}:/windows phone/i.test(t)?f={name:"Windows Phone",windowsphone:e,msie:e,version:n(/iemobile\/(\d+(\.\d+)?)/i)}:/msie|trident/i.test(t)?f={name:"Internet Explorer",msie:e,version:n(/(?:msie |rv:)(\d+(\.\d+)?)/i)}:/chrome|crios|crmo/i.test(t)?f={name:"Chrome",chrome:e,version:n(/(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i)}:r?(f={name:r=="iphone"?"iPhone":r=="ipad"?"iPad":"iPod"},o&&(f.version=o)):/sailfish/i.test(t)?f={name:"Sailfish",sailfish:e,version:n(/sailfish\s?browser\/(\d+(\.\d+)?)/i)}:/seamonkey\//i.test(t)?f={name:"SeaMonkey",seamonkey:e,version:n(/seamonkey\/(\d+(\.\d+)?)/i)}:/firefox|iceweasel/i.test(t)?(f={name:"Firefox",firefox:e,version:n(/(?:firefox|iceweasel)[ \/](\d+(\.\d+)?)/i)},/\((mobile|tablet);[^\)]*rv:[\d\.]+\)/i.test(t)&&(f.firefoxos=e)):/silk/i.test(t)?f={name:"Amazon Silk",silk:e,version:n(/silk\/(\d+(\.\d+)?)/i)}:s?f={name:"Android",version:o}:/phantom/i.test(t)?f={name:"PhantomJS",phantom:e,version:n(/phantomjs\/(\d+(\.\d+)?)/i)}:/blackberry|\bbb\d+/i.test(t)||/rim\stablet/i.test(t)?f={name:"BlackBerry",blackberry:e,version:o||n(/blackberry[\d]+\/(\d+(\.\d+)?)/i)}:/(web|hpw)os/i.test(t)?(f={name:"WebOS",webos:e,version:o||n(/w(?:eb)?osbrowser\/(\d+(\.\d+)?)/i)},/touchpad\//i.test(t)&&(f.touchpad=e)):/bada/i.test(t)?f={name:"Bada",bada:e,version:n(/dolfin\/(\d+(\.\d+)?)/i)}:/tizen/i.test(t)?f={name:"Tizen",tizen:e,version:n(/(?:tizen\s?)?browser\/(\d+(\.\d+)?)/i)||o}:/safari/i.test(t)?f={name:"Safari",safari:e,version:o}:f={},/(apple)?webkit/i.test(t)?(f.name=f.name||"Webkit",f.webkit=e,!f.version&&o&&(f.version=o)):!f.opera&&/gecko\//i.test(t)&&(f.name=f.name||"Gecko",f.gecko=e,f.version=f.version||n(/gecko\/(\d+(\.\d+)?)/i)),s||f.silk?f.android=e:r&&(f[r]=e,f.ios=e);var l="";r?(l=n(/os (\d+([_\s]\d+)*) like mac os x/i),l=l.replace(/[_\s]/g,".")):s?l=n(/android[ \/-](\d+(\.\d+)*)/i):f.windowsphone?l=n(/windows phone (?:os)?\s?(\d+(\.\d+)*)/i):f.webos?l=n(/(?:web|hpw)os\/(\d+(\.\d+)*)/i):f.blackberry?l=n(/rim\stablet\sos\s(\d+(\.\d+)*)/i):f.bada?l=n(/bada\/(\d+(\.\d+)*)/i):f.tizen&&(l=n(/tizen[\/\s](\d+(\.\d+)*)/i)),l&&(f.osversion=l);var c=l.split(".")[0];if(u||r=="ipad"||s&&(c==3||c==4&&!a)||f.silk)f.tablet=e;else if(a||r=="iphone"||r=="ipod"||s||f.blackberry||f.webos||f.bada)f.mobile=e;return f.msie&&f.version>=10||f.chrome&&f.version>=20||f.firefox&&f.version>=20||f.safari&&f.version>=6||f.opera&&f.version>=10||f.ios&&f.osversion&&f.osversion.split(".")[0]>=6||f.blackberry&&f.version>=10.1?f.a=e:f.msie&&f.version<10||f.chrome&&f.version<20||f.firefox&&f.version<20||f.safari&&f.version<6||f.opera&&f.version<10||f.ios&&f.osversion&&f.osversion.split(".")[0]<6?f.c=e:f.x=e,f}var e=!0,n=t(typeof navigator!="undefined"?navigator.userAgent:"");return n._detect=t,n}) -------------------------------------------------------------------------------- /typescript/Note.ts: -------------------------------------------------------------------------------- 1 | declare var simplify:any; // Magic 2 | 3 | class PathPoint { 4 | x:number; 5 | y:number; 6 | time:number; 7 | 8 | constructor(x:number, y:number, time:number){ 9 | this.x = x; 10 | this.y = y; 11 | this.time = time; 12 | 13 | // console.log(this); 14 | } 15 | } 16 | 17 | class Path { 18 | points:PathPoint[]; 19 | 20 | constructor(json:any[]){ 21 | this.points = []; 22 | 23 | 24 | if(json && json.length > 0) { 25 | for(var i=0;i= time ){ 60 | if(i == 0){ 61 | return this.points[i]; 62 | } 63 | 64 | var p = this.points[i-1]; 65 | var n = this.points[i]; 66 | 67 | var pct = (time - p.time) / (n.time - p.time); 68 | 69 | return { 70 | x: p.x * (1-pct) + n.x*pct, 71 | y: p.y * (1-pct) + n.y*pct, 72 | time: time 73 | }; 74 | } 75 | } 76 | } 77 | 78 | 79 | simplify(){ 80 | this.points = simplify(this.points, 0.002); 81 | } 82 | } 83 | 84 | class Note { 85 | id: number; 86 | time_begin:number; 87 | time_end:number; 88 | text:string; 89 | path:Path; 90 | 91 | elm: JQuery; 92 | line : any; 93 | curPos: any; 94 | 95 | constructor(json){ 96 | if(json) { 97 | this.id = json.id; 98 | this.time_begin = json.time_begin; 99 | this.time_end = json.time_end; 100 | this.text = json.note; 101 | this.path = new Path(json.path); 102 | 103 | 104 | if(this.text) { 105 | var dur = this.time_end - this.time_begin; 106 | var minDur = this.text.length / 140 * 6000 + 3000; // 6 seconds to read 140 characters 107 | if (dur < minDur) { 108 | var newPoint = new PathPoint( 109 | this.path.last().x, 110 | this.path.last().y, 111 | this.path.first().time + minDur 112 | ); 113 | this.path.push(newPoint); 114 | this.time_end = this.time_begin + minDur; 115 | } 116 | } 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /public/simplify.js: -------------------------------------------------------------------------------- 1 | /* 2 | (c) 2013, Vladimir Agafonkin 3 | Simplify.js, a high-performance JS polyline simplification library 4 | mourner.github.io/simplify-js 5 | */ 6 | 7 | (function () { 'use strict'; 8 | 9 | // to suit your point format, run search/replace for '.x' and '.y'; 10 | // for 3D version, see 3d branch (configurability would draw significant performance overhead) 11 | 12 | // square distance between 2 points 13 | function getSqDist(p1, p2) { 14 | 15 | var dx = p1.x - p2.x, 16 | dy = p1.y - p2.y; 17 | 18 | return dx * dx + dy * dy; 19 | } 20 | 21 | // square distance from a point to a segment 22 | function getSqSegDist(p, p1, p2) { 23 | 24 | var x = p1.x, 25 | y = p1.y, 26 | dx = p2.x - x, 27 | dy = p2.y - y; 28 | 29 | if (dx !== 0 || dy !== 0) { 30 | 31 | var t = ((p.x - x) * dx + (p.y - y) * dy) / (dx * dx + dy * dy); 32 | 33 | if (t > 1) { 34 | x = p2.x; 35 | y = p2.y; 36 | 37 | } else if (t > 0) { 38 | x += dx * t; 39 | y += dy * t; 40 | } 41 | } 42 | 43 | dx = p.x - x; 44 | dy = p.y - y; 45 | 46 | return dx * dx + dy * dy; 47 | } 48 | // rest of the code doesn't care about point format 49 | 50 | // basic distance-based simplification 51 | function simplifyRadialDist(points, sqTolerance) { 52 | 53 | var prevPoint = points[0], 54 | newPoints = [prevPoint], 55 | point; 56 | 57 | for (var i = 1, len = points.length; i < len; i++) { 58 | point = points[i]; 59 | 60 | if (getSqDist(point, prevPoint) > sqTolerance) { 61 | newPoints.push(point); 62 | prevPoint = point; 63 | } 64 | } 65 | 66 | if (prevPoint !== point) newPoints.push(point); 67 | 68 | return newPoints; 69 | } 70 | 71 | // simplification using optimized Douglas-Peucker algorithm with recursion elimination 72 | function simplifyDouglasPeucker(points, sqTolerance) { 73 | 74 | var len = points.length, 75 | MarkerArray = typeof Uint8Array !== 'undefined' ? Uint8Array : Array, 76 | markers = new MarkerArray(len), 77 | first = 0, 78 | last = len - 1, 79 | stack = [], 80 | newPoints = [], 81 | i, maxSqDist, sqDist, index; 82 | 83 | markers[first] = markers[last] = 1; 84 | 85 | while (last) { 86 | 87 | maxSqDist = 0; 88 | 89 | for (i = first + 1; i < last; i++) { 90 | sqDist = getSqSegDist(points[i], points[first], points[last]); 91 | 92 | if (sqDist > maxSqDist) { 93 | index = i; 94 | maxSqDist = sqDist; 95 | } 96 | } 97 | 98 | if (maxSqDist > sqTolerance) { 99 | markers[index] = 1; 100 | stack.push(first, index, index, last); 101 | } 102 | 103 | last = stack.pop(); 104 | first = stack.pop(); 105 | } 106 | 107 | for (i = 0; i < len; i++) { 108 | if (markers[i]) newPoints.push(points[i]); 109 | } 110 | 111 | return newPoints; 112 | } 113 | 114 | // both algorithms combined for awesome performance 115 | function simplify(points, tolerance, highestQuality) { 116 | 117 | if (points.length <= 1) return points; 118 | 119 | var sqTolerance = tolerance !== undefined ? tolerance * tolerance : 1; 120 | 121 | points = highestQuality ? points : simplifyRadialDist(points, sqTolerance); 122 | points = simplifyDouglasPeucker(points, sqTolerance); 123 | 124 | return points; 125 | } 126 | 127 | // export as AMD module / Node module / browser or worker variable 128 | if (typeof define === 'function' && define.amd) define(function() { return simplify; }); 129 | else if (typeof module !== 'undefined') module.exports = simplify; 130 | else if (typeof self !== 'undefined') self.simplify = simplify; 131 | else window.simplify = simplify; 132 | 133 | })(); 134 | -------------------------------------------------------------------------------- /typings/youtube/youtube.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for YouTube 2 | // Project: https://developers.google.com/youtube/ 3 | // Definitions by: Daz Wilkin , Ian Obermiller 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | 6 | declare module YT { 7 | interface EventArgs { 8 | target: Player; 9 | data: any; 10 | } 11 | 12 | interface EventHandler { 13 | (event: EventArgs): void; 14 | } 15 | 16 | export interface Events { 17 | onReady?: EventHandler; 18 | onPlayback?: EventHandler; 19 | onStateChange?: EventHandler; 20 | } 21 | 22 | export enum ListType { 23 | search, 24 | user_uploads, 25 | playlist, 26 | } 27 | 28 | export interface PlayerVars { 29 | autohide?: number; 30 | autoplay?: number; 31 | cc_load_policy?: any; 32 | color?: string; 33 | controls?: number; 34 | disablekb?: number; 35 | enablejsapi?: number; 36 | end?: number; 37 | fs?: number; 38 | iv_load_policy?: number; 39 | list?: string; 40 | listType?: ListType; 41 | loop?; 42 | modestbranding?: number; 43 | origin?; 44 | playerpiid?: string; 45 | playlist?; 46 | rel?: number; 47 | showinfo?: number; 48 | start?: number; 49 | theme?: string; 50 | } 51 | 52 | export interface PlayerOptions { 53 | width?: number; 54 | height?: number; 55 | videoId?: string; 56 | playerVars?: PlayerVars; 57 | events?: Events; 58 | } 59 | 60 | interface VideoByIdParams { 61 | videoId: string; 62 | startSeconds?: number; 63 | endSeconds?: number; 64 | suggestedQuality?: string; 65 | } 66 | 67 | interface VideoByUrlParams { 68 | mediaContentUrl: string; 69 | startSeconds?: number; 70 | endSeconds?: number; 71 | suggestedQuality?: string; 72 | } 73 | 74 | export interface VideoData 75 | { 76 | video_id: string; 77 | author: string; 78 | title: string; 79 | } 80 | 81 | export class Player { 82 | // Constructor 83 | constructor(id: string, playerOptions: PlayerOptions); 84 | 85 | // Queueing functions 86 | loadVideoById(videoId: string, startSeconds?: number, suggestedQuality?: string): void; 87 | loadVideoById(VideoByIdParams): void; 88 | cueVideoById(videoId: string, startSeconds?: number, suggestedQuality?: string): void; 89 | cueVideoById(VideoByIdParams): void; 90 | 91 | loadVideoByUrl(mediaContentUrl: string, startSeconds?: number, suggestedQuality?: string): void; 92 | loadVideoByUrl(VideoByUrlParams): void; 93 | cueVideoByUrl(mediaContentUrl: string, startSeconds?: number, suggestedQuality?: string): void; 94 | cueVideoByUrl(VideoByUrlParams): void; 95 | 96 | // Properties 97 | size; 98 | 99 | // Playing 100 | playVideo(): void; 101 | pauseVideo(): void; 102 | stopVideo(): void; 103 | seekTo(seconds:number, allowSeekAhead:boolean): void; 104 | clearVideo(): void; 105 | 106 | // Playlist 107 | nextVideo(): void; 108 | previousVideo(): void; 109 | playVideoAt(index: number): void; 110 | 111 | // Volume 112 | mute(): void; 113 | unMute(): void; 114 | isMuted(): boolean; 115 | setVolume(volume: number): void; 116 | getVolume(): number; 117 | 118 | // Sizing 119 | setSize(width: number, height: number): any; 120 | 121 | // Playback 122 | getPlaybackRate(): number; 123 | setPlaybackRate(suggestedRate:number): void; 124 | getAvailablePlaybackRates(): number[]; 125 | 126 | // Behavior 127 | setLoop(loopPlaylists: boolean): void; 128 | setShuffle(shufflePlaylist: boolean): void; 129 | 130 | // Status 131 | getVideoLoadedFraction(): number; 132 | getPlayerState(): number; 133 | getCurrentTime(): number; 134 | getVideoStartBytes(): number; 135 | getVideoBytesLoaded(): number; 136 | getVideoBytesTotal(): number; 137 | 138 | // Information 139 | getDuration(): number; 140 | getVideoUrl(): string; 141 | getVideoEmbedCode(): string; 142 | getVideoData(): VideoData; 143 | 144 | // Playlist 145 | getPlaylist(): any[]; 146 | getPlaylistIndex(): number; 147 | 148 | // Event Listener 149 | addEventListener(event: string, handler: EventHandler): void; 150 | } 151 | 152 | export enum PlayerState { 153 | BUFFERING, 154 | CUED, 155 | ENDED, 156 | PAUSED, 157 | PLAYING 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /boring/literals.json: -------------------------------------------------------------------------------- 1 | ["4r5e", 2 | "5h1t", 3 | "5hit", 4 | "a55", 5 | "anal", 6 | "anus", 7 | "ar5e", 8 | "arrse", 9 | "arse", 10 | "ass", 11 | "ass-fucker", 12 | "asses", 13 | "assfucker", 14 | "assfukka", 15 | "asshole", 16 | "assholes", 17 | "asswhole", 18 | "a_s_s", 19 | "b!tch", 20 | "b00bs", 21 | "b17ch", 22 | "b1tch", 23 | "ballbag", 24 | "balls", 25 | "ballsack", 26 | "bastard", 27 | "beastial", 28 | "beastiality", 29 | "bellend", 30 | "bestial", 31 | "bestiality", 32 | "bi\\+ch", 33 | "biatch", 34 | "bitch", 35 | "bitcher", 36 | "bitchers", 37 | "bitches", 38 | "bitchin", 39 | "bitching", 40 | "bloody", 41 | "blow job", 42 | "blowjob", 43 | "blowjobs", 44 | "boiolas", 45 | "bollock", 46 | "bollok", 47 | "boner", 48 | "boob", 49 | "boobs", 50 | "booobs", 51 | "boooobs", 52 | "booooobs", 53 | "booooooobs", 54 | "breasts", 55 | "buceta", 56 | "bugger", 57 | "bum", 58 | "bunny fucker", 59 | "butt", 60 | "butthole", 61 | "buttmuch", 62 | "buttplug", 63 | "c0ck", 64 | "c0cksucker", 65 | "carpet muncher", 66 | "cawk", 67 | "chink", 68 | "cipa", 69 | "cl1t", 70 | "clit", 71 | "clitoris", 72 | "clits", 73 | "cnut", 74 | "cock", 75 | "cock-sucker", 76 | "cockface", 77 | "cockhead", 78 | "cockmunch", 79 | "cockmuncher", 80 | "cocks", 81 | "cocksuck", 82 | "cocksucked", 83 | "cocksucker", 84 | "cocksucking", 85 | "cocksucks", 86 | "cocksuka", 87 | "cocksukka", 88 | "cok", 89 | "cokmuncher", 90 | "coksucka", 91 | "coon", 92 | "cox", 93 | "crap", 94 | "cum", 95 | "cummer", 96 | "cumming", 97 | "cums", 98 | "cumshot", 99 | "cunilingus", 100 | "cunillingus", 101 | "cunnilingus", 102 | "cunt", 103 | "cuntlick", 104 | "cuntlicker", 105 | "cuntlicking", 106 | "cunts", 107 | "cyalis", 108 | "cyberfuc", 109 | "cyberfuck", 110 | "cyberfucked", 111 | "cyberfucker", 112 | "cyberfuckers", 113 | "cyberfucking", 114 | "d1ck", 115 | "dick", 116 | "dickhead", 117 | "dildo", 118 | "dildos", 119 | "dink", 120 | "dinks", 121 | "dirsa", 122 | "dlck", 123 | "dog-fucker", 124 | "doggin", 125 | "dogging", 126 | "donkeyribber", 127 | "doosh", 128 | "duche", 129 | "dyke", 130 | "ejaculate", 131 | "ejaculated", 132 | "ejaculates", 133 | "ejaculating", 134 | "ejaculatings", 135 | "ejaculation", 136 | "ejakulate", 137 | "f u c k", 138 | "f u c k e r", 139 | "f4nny", 140 | "fag", 141 | "fagging", 142 | "faggitt", 143 | "faggot", 144 | "faggs", 145 | "fagot", 146 | "fagots", 147 | "fags", 148 | "fanny", 149 | "fannyflaps", 150 | "fannyfucker", 151 | "fanyy", 152 | "fatass", 153 | "fcuk", 154 | "fcuker", 155 | "fcuking", 156 | "feck", 157 | "fecker", 158 | "felching", 159 | "fellate", 160 | "fellatio", 161 | "fingerfuck", 162 | "fingerfucked", 163 | "fingerfucker", 164 | "fingerfuckers", 165 | "fingerfucking", 166 | "fingerfucks", 167 | "fistfuck", 168 | "fistfucked", 169 | "fistfucker", 170 | "fistfuckers", 171 | "fistfucking", 172 | "fistfuckings", 173 | "fistfucks", 174 | "flange", 175 | "fook", 176 | "fooker", 177 | "fuck", 178 | "fucka", 179 | "fucked", 180 | "fucker", 181 | "fuckers", 182 | "fuckhead", 183 | "fuckheads", 184 | "fuckin", 185 | "fucking", 186 | "fuckings", 187 | "fuckingshitmotherfucker", 188 | "fuckme", 189 | "fucks", 190 | "fuckwhit", 191 | "fuckwit", 192 | "fudge packer", 193 | "fudgepacker", 194 | "fuk", 195 | "fuker", 196 | "fukker", 197 | "fukkin", 198 | "fuks", 199 | "fukwhit", 200 | "fukwit", 201 | "fux", 202 | "fux0r", 203 | "f_u_c_k", 204 | "gangbang", 205 | "gangbanged", 206 | "gangbangs", 207 | "gaylord", 208 | "gaysex", 209 | "goatse", 210 | "god-dam", 211 | "god-damned", 212 | "goddamn", 213 | "goddamned", 214 | "hardcoresex", 215 | "hell", 216 | "heshe", 217 | "hoar", 218 | "hoare", 219 | "hoer", 220 | "homo", 221 | "hore", 222 | "horniest", 223 | "horny", 224 | "hotsex", 225 | "hustler", 226 | "jack-off", 227 | "jackoff", 228 | "jap", 229 | "jerk-off", 230 | "jism", 231 | "jiz", 232 | "jizm", 233 | "jizz", 234 | "kawk", 235 | "knob", 236 | "knobead", 237 | "knobed", 238 | "knobend", 239 | "knobhead", 240 | "knobjocky", 241 | "knobjokey", 242 | "kock", 243 | "kondum", 244 | "kondums", 245 | "kum", 246 | "kummer", 247 | "kumming", 248 | "kums", 249 | "kunilingus", 250 | "l3i\\+ch", 251 | "l3itch", 252 | "labia", 253 | "lmfao", 254 | "lust", 255 | "lusting", 256 | "m0f0", 257 | "m0fo", 258 | "m45terbate", 259 | "ma5terb8", 260 | "ma5terbate", 261 | "masochist", 262 | "master-bate", 263 | "masterb8", 264 | "masterbat", 265 | "masterbat3", 266 | "masterbate", 267 | "masterbation", 268 | "masterbations", 269 | "masturbate", 270 | "mo-fo", 271 | "mof0", 272 | "mofo", 273 | "mothafuck", 274 | "mothafucka", 275 | "mothafuckas", 276 | "mothafuckaz", 277 | "mothafucked", 278 | "mothafucker", 279 | "mothafuckers", 280 | "mothafuckin", 281 | "mothafucking", 282 | "mothafuckings", 283 | "mothafucks", 284 | "mother fucker", 285 | "motherfuck", 286 | "motherfucked", 287 | "motherfucker", 288 | "motherfuckers", 289 | "motherfuckin", 290 | "motherfucking", 291 | "motherfuckings", 292 | "motherfuckka", 293 | "motherfucks", 294 | "muff", 295 | "mutha", 296 | "muthafecker", 297 | "muthafuckker", 298 | "muther", 299 | "mutherfucker", 300 | "n1gga", 301 | "n1gger", 302 | "nazi", 303 | "nigg3r", 304 | "nigg4h", 305 | "nigga", 306 | "niggah", 307 | "niggas", 308 | "niggaz", 309 | "nigger", 310 | "niggers", 311 | "nob", 312 | "nob jokey", 313 | "nobhead", 314 | "nobjocky", 315 | "nobjokey", 316 | "numbnuts", 317 | "nutsack", 318 | "orgasim", 319 | "orgasims", 320 | "orgasm", 321 | "orgasms", 322 | "p0rn", 323 | "pawn", 324 | "pecker", 325 | "penis", 326 | "penisfucker", 327 | "phonesex", 328 | "phuck", 329 | "phuk", 330 | "phuked", 331 | "phuking", 332 | "phukked", 333 | "phukking", 334 | "phuks", 335 | "phuq", 336 | "pigfucker", 337 | "pimpis", 338 | "piss", 339 | "pissed", 340 | "pisser", 341 | "pissers", 342 | "pisses", 343 | "pissflaps", 344 | "pissin", 345 | "pissing", 346 | "pissoff", 347 | "poop", 348 | "porn", 349 | "porno", 350 | "pornography", 351 | "pornos", 352 | "prick", 353 | "pricks", 354 | "pron", 355 | "pube", 356 | "pusse", 357 | "pussi", 358 | "pussies", 359 | "pussy", 360 | "pussys", 361 | "rectum", 362 | "retard", 363 | "rimjaw", 364 | "rimming", 365 | "s hit", 366 | "s\\.o\\.b\\.", 367 | "sadist", 368 | "schlong", 369 | "screwing", 370 | "scroat", 371 | "scrote", 372 | "scrotum", 373 | "semen", 374 | "sex", 375 | "sh!\\+", 376 | "sh!t", 377 | "sh1t", 378 | "shag", 379 | "shagger", 380 | "shaggin", 381 | "shagging", 382 | "shemale", 383 | "shi\\+", 384 | "shit", 385 | "shitdick", 386 | "shite", 387 | "shited", 388 | "shitey", 389 | "shitfuck", 390 | "shitfull", 391 | "shithead", 392 | "shiting", 393 | "shitings", 394 | "shits", 395 | "shitted", 396 | "shitter", 397 | "shitters", 398 | "shitting", 399 | "shittings", 400 | "shitty", 401 | "skank", 402 | "slut", 403 | "sluts", 404 | "smegma", 405 | "smut", 406 | "snatch", 407 | "son-of-a-bitch", 408 | "spac", 409 | "spunk", 410 | "s_h_i_t", 411 | "t1tt1e5", 412 | "t1tties", 413 | "teets", 414 | "teez", 415 | "testical", 416 | "testicle", 417 | "tit", 418 | "titfuck", 419 | "tits", 420 | "titt", 421 | "tittie5", 422 | "tittiefucker", 423 | "titties", 424 | "tittyfuck", 425 | "tittywank", 426 | "titwank", 427 | "tosser", 428 | "turd", 429 | "tw4t", 430 | "twat", 431 | "twathead", 432 | "twatty", 433 | "twunt", 434 | "twunter", 435 | "v14gra", 436 | "v1gra", 437 | "vagina", 438 | "viagra", 439 | "vulva", 440 | "w00se", 441 | "wang", 442 | "wank", 443 | "wanker", 444 | "wanky", 445 | "whoar", 446 | "whore", 447 | "willies", 448 | "willy", 449 | "xrated", 450 | "xxx"] -------------------------------------------------------------------------------- /public/compiled/frontend.js: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | /// 6 | /// 7 | /// 8 | // Add new locations here. 9 | var sites = { 10 | london: { 11 | id: 0, 12 | playlist: 'PLscUku2aaZnFE-7wKovrbi76b26VKxIT-', 13 | videoDurations: [7650, 4941, 7424, 7264, 6835, 7128], 14 | startTime: "April 15, 2015 15:00:00", 15 | modulusHours: 12 16 | }, 17 | netherlands: { 18 | id: 1, 19 | playlist: 'PLscUku2aaZnEISIU_BFatpXUd91DFyRVA', 20 | videoDurations: [(1 * 60 * 60) + (2 * 60) + 15], 21 | startTime: "April 15, 2015 12:00:00", 22 | modulusHours: 1 23 | }, 24 | birmingham: { 25 | id: 2, 26 | playlist: 'PLscUku2aaZnGRbIOS1LGUt9GeDBX159yY', 27 | videoDurations: [(1 * 60 * 60) + (0 * 60) + 1], 28 | startTime: "April 15, 2015 12:00:00", 29 | modulusHours: 1 30 | }, 31 | gwangju: { 32 | id: 3, 33 | playlist: 'PLscUku2aaZnE7jj8SNk1nxWTTSXlOYLpP', 34 | videoDurations: [(1 * 60 * 60) + (0 * 60) + 1], 35 | startTime: "April 15, 2015 12:00:00", 36 | modulusHours: 1 37 | }, 38 | beijing: { 39 | id: 4, 40 | playlist: 'PLscUku2aaZnFTPxiEpcZHVCqhCFb_x4az', 41 | videoDurations: [(1 * 60 * 60) + (0 * 60) + 1], 42 | startTime: "April 15, 2015 12:00:00", 43 | modulusHours: 1 44 | }, 45 | saintbrieuc: { 46 | id: 5, 47 | playlist: 'PLscUku2aaZnHu_gyAtviCfndE_T8E7dk8', 48 | videoDurations: [(1 * 60 * 60)], 49 | startTime: "April 15, 2015 12:00:00", 50 | modulusHours: 1 51 | }, 52 | berlin: { 53 | id: 6, 54 | playlist: 'PLscUku2aaZnH3SMUCBxlaxPJnXyTEB1cM', 55 | videoDurations: [(1 * 60 * 60)], 56 | startTime: "April 15, 2015 12:00:00", 57 | modulusHours: 1 58 | } 59 | }; 60 | var recording = false; 61 | var site = location.pathname.replace("/", ''); 62 | // Fetch every 15sec, fetch 20sec of data 63 | var api = new NotesApi(sites[site].id); 64 | api.startFetching(15000, 20000); 65 | var timeUntilReload = 20 * 60 * 1000; // reload every 20 minutes 66 | function createMovementTimeout() { 67 | return setTimeout(function () { 68 | if (!recording) { 69 | location.reload(); 70 | } 71 | }, timeUntilReload); 72 | } 73 | var drawingCanvas; 74 | var ui; 75 | var video; 76 | var clock; 77 | Clock.startTime = sites[site].startTime; 78 | // Wait for a go from youtube api 79 | var onYouTubePlayerAPIReady = function () { 80 | video = new VideoPlayer(sites[site].playlist, sites[site].videoDurations, sites[site].modulusHours, { 81 | onLoadComplete: function () { 82 | video.setTime(moment(), function () { 83 | setTimeout(function () { 84 | ui.hideLoadingScreen(); 85 | }, 300); 86 | }); 87 | }, 88 | onNewFrame: function (player) { 89 | api.currentTime = player.currentTime; 90 | drawingCanvas.updateNotes(api.notes); 91 | drawingCanvas.updateAnimation(); 92 | clock.frameUpdate(video); 93 | updateVideoLoop(); 94 | } 95 | }); 96 | $(document).ready(function () { 97 | ui = new Interface({ 98 | onResize: function () { 99 | video.updatePlayerSize(); 100 | } 101 | }); 102 | drawingCanvas = new DrawingCanvas(); 103 | drawingCanvas.api = api; 104 | drawingCanvas.video = video; 105 | drawingCanvas.events.onDrawingComplete = gotoEditor; 106 | clock = new Clock(); 107 | window.onhashchange = function () { 108 | video.seek(parseInt(location.hash.substring(1))); 109 | var lastCharacter = location.hash.substring(location.hash.length - 1); 110 | if (lastCharacter == '-') { 111 | recording = true; 112 | $("img[src='rewind.png']").remove(); 113 | $("#locationHeader").remove(); 114 | $("#logoHeader").remove(); 115 | $('#clickArea').css('cursor', 'none'); 116 | } 117 | }; 118 | $('.' + site).show(); 119 | $('#back').click(function () { 120 | gotoVideo(video.currentTime - 1000); 121 | }); 122 | $('#logoHeader').click(ui.showCredits); 123 | $('#infoHeader').click(ui.showCredits); 124 | $('#credits').click(ui.hideCredits); 125 | // Rewind button 126 | $('#rewind').click(function () { 127 | ui.hideVideo(function () { 128 | video.seek(video.currentTime - (10 * 1000), function () { 129 | ui.showVideo(); 130 | }); 131 | }); 132 | }); 133 | // reload the page every so often if the visitor doesn't move their mouse 134 | var movementTimeout = createMovementTimeout(); 135 | document.onmousemove = function () { 136 | clearTimeout(movementTimeout); 137 | movementTimeout = createMovementTimeout(); 138 | }; 139 | }); 140 | }; 141 | function gotoEditor(path) { 142 | ui.hideVideo(function () { 143 | GLOBAL.MODE = "EDITOR"; 144 | video.zoom = 4; 145 | // Seek to the path start time 146 | video.seek(path.points[0].time, function () { 147 | ui.showVideo(); 148 | }); 149 | // Set the video zoom position 150 | video.zoomPos = { x: path.points[0].x, y: path.points[0].y }; 151 | video.updatePlayerSize(); 152 | // Update the interface 153 | $('#notes').hide(); 154 | $('#linedrawing').hide(); 155 | $('#logoHeader').hide(); 156 | $('#timeAndDate').hide(); 157 | $('#addNoteInterface').show(); 158 | $('#note-text').val('').focus(); 159 | $('#rewind').hide(); 160 | $('#back').show(); 161 | }); 162 | var trySubmit = function () { 163 | var text = $('#note-text').val(); 164 | if (text) { 165 | var note = new Note([]); 166 | note.path = path; 167 | note.text = text; 168 | // Submit the path to the API 169 | api.submitNote(note); 170 | // GOTO video mode again 171 | gotoVideo(path.points[0].time - 5000); 172 | } 173 | else { 174 | $('#note-text').attr('placeholder', 'Please write something').focus(); 175 | $('#submitButton').unbind('click').click(trySubmit); 176 | } 177 | }; 178 | $('#submitButton').unbind('click').click(trySubmit); 179 | var keypress = function (e) { 180 | if (e.which == 13) { 181 | trySubmit(); 182 | e.preventDefault(); 183 | e.stopPropagation(); 184 | return false; 185 | } 186 | var textElm = $('#note-text'); 187 | if (textElm.val().length >= 140) { 188 | //textElm.val(textElm.val().substring(0,140)); 189 | e.preventDefault(); 190 | e.stopPropagation(); 191 | return false; 192 | } 193 | }; 194 | $('#note-text').unbind('keypress').keypress(keypress); 195 | } 196 | function gotoVideo(seekTime) { 197 | ui.hideVideo(function () { 198 | GLOBAL.MODE = "PLAYER"; 199 | video.zoom = 1; 200 | video.updatePlayerSize(); 201 | drawingCanvas.clearMouseTrail(); 202 | // Seek to the path start time 203 | video.seek(seekTime, function () { 204 | ui.showVideo(); 205 | }); 206 | $('#notes').show(); 207 | $('#addNoteInterface').hide(); 208 | $('#linedrawing').show(); 209 | $('#timeAndDate').show(); 210 | $('#rewind').show(); 211 | $('#back').hide(); 212 | $('#logoHeader').show(); 213 | }); 214 | } 215 | function updateVideoLoop() { 216 | if (GLOBAL.MODE == "EDITOR") { 217 | var time = drawingCanvas.mousePath.last().time; 218 | var diff = time - drawingCanvas.mousePath.points[0].time; 219 | if (diff < 3000) { 220 | time += 3000 - diff; 221 | } 222 | if (video.currentTime > time) { 223 | this.video.seek(drawingCanvas.mousePath.points[0].time, undefined, true); 224 | } 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /typescript/frontend.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | /// 6 | /// 7 | /// 8 | 9 | // Add new locations here. 10 | var sites = { 11 | london: { 12 | id: 0, 13 | playlist: 'PLscUku2aaZnFE-7wKovrbi76b26VKxIT-', 14 | videoDurations: [7650, 4941, 7424, 7264, 6835, 7128], 15 | startTime: "April 15, 2015 15:00:00", 16 | modulusHours: 12 17 | }, 18 | netherlands: { 19 | id: 1, 20 | playlist: 'PLscUku2aaZnEISIU_BFatpXUd91DFyRVA', 21 | videoDurations: [(1*60*60)+(2*60)+15], 22 | startTime: "April 15, 2015 12:00:00", 23 | modulusHours: 1 24 | }, 25 | birmingham: { 26 | id: 2, 27 | playlist: 'PLscUku2aaZnGRbIOS1LGUt9GeDBX159yY', 28 | videoDurations: [(1*60*60)+(0*60)+1], 29 | startTime: "April 15, 2015 12:00:00", 30 | modulusHours: 1 31 | }, 32 | gwangju: { 33 | id: 3, 34 | playlist: 'PLscUku2aaZnE7jj8SNk1nxWTTSXlOYLpP', 35 | videoDurations: [(1*60*60)+(0*60)+1], 36 | startTime: "April 15, 2015 12:00:00", 37 | modulusHours: 1 38 | }, 39 | beijing: { 40 | id: 4, 41 | playlist: 'PLscUku2aaZnFTPxiEpcZHVCqhCFb_x4az', 42 | videoDurations: [(1*60*60)+(0*60)+1], 43 | startTime: "April 15, 2015 12:00:00", 44 | modulusHours: 1 45 | }, 46 | saintbrieuc: { 47 | id: 5, 48 | playlist: 'PLscUku2aaZnHu_gyAtviCfndE_T8E7dk8', 49 | videoDurations: [(1*60*60)], 50 | startTime: "April 15, 2015 12:00:00", 51 | modulusHours: 1 52 | }, 53 | berlin: { 54 | id: 6, 55 | playlist: 'PLscUku2aaZnH3SMUCBxlaxPJnXyTEB1cM', 56 | videoDurations: [(1*60*60)], 57 | startTime: "April 15, 2015 12:00:00", 58 | modulusHours: 1 59 | } 60 | }; 61 | 62 | var recording = false; 63 | var site = location.pathname.replace("/",''); 64 | 65 | // Fetch every 15sec, fetch 20sec of data 66 | var api = new NotesApi(sites[site].id); 67 | api.startFetching(15000, 20000); 68 | 69 | var timeUntilReload = 20 * 60 * 1000; // reload every 20 minutes 70 | function createMovementTimeout() { 71 | return setTimeout(function () { 72 | if (!recording) { 73 | location.reload(); 74 | } 75 | }, timeUntilReload); 76 | } 77 | 78 | var drawingCanvas : DrawingCanvas; 79 | var ui : Interface; 80 | var video : VideoPlayer; 81 | var clock : Clock; 82 | 83 | Clock.startTime = sites[site].startTime; 84 | 85 | // Wait for a go from youtube api 86 | var onYouTubePlayerAPIReady = () => { 87 | video = new VideoPlayer( 88 | sites[site].playlist, 89 | sites[site].videoDurations, 90 | sites[site].modulusHours, 91 | { 92 | onLoadComplete : () => { 93 | video.setTime(moment(), () => { 94 | setTimeout(()=>{ 95 | ui.hideLoadingScreen(); 96 | }, 300); 97 | }) 98 | }, 99 | onNewFrame:(player:VideoPlayer) => { 100 | api.currentTime = player.currentTime; 101 | 102 | drawingCanvas.updateNotes(api.notes); 103 | drawingCanvas.updateAnimation(); 104 | 105 | clock.frameUpdate(video); 106 | updateVideoLoop(); 107 | } 108 | }); 109 | 110 | $(document).ready(()=>{ 111 | 112 | ui = new Interface({ 113 | onResize: () => { 114 | video.updatePlayerSize(); 115 | } 116 | }); 117 | 118 | drawingCanvas = new DrawingCanvas(); 119 | drawingCanvas.api = api; 120 | drawingCanvas.video = video; 121 | 122 | drawingCanvas.events.onDrawingComplete = gotoEditor; 123 | 124 | clock = new Clock(); 125 | 126 | window.onhashchange = () => { 127 | video.seek(parseInt(location.hash.substring(1))); 128 | var lastCharacter = location.hash.substring(location.hash.length-1); 129 | if (lastCharacter == '-') { 130 | recording = true; 131 | $("img[src='rewind.png']").remove(); 132 | $("#locationHeader").remove(); 133 | $("#logoHeader").remove(); 134 | $('#clickArea').css('cursor', 'none'); 135 | } 136 | }; 137 | 138 | $('.' + site).show(); 139 | 140 | $('#back').click(function(){ 141 | gotoVideo(video.currentTime - 1000); 142 | }); 143 | 144 | $('#logoHeader').click(ui.showCredits); 145 | $('#infoHeader').click(ui.showCredits); 146 | $('#credits').click(ui.hideCredits); 147 | 148 | // Rewind button 149 | $('#rewind').click(()=>{ 150 | ui.hideVideo(() => { 151 | video.seek(video.currentTime - (10 * 1000), ()=>{ 152 | ui.showVideo(); 153 | }); 154 | }) 155 | }) 156 | 157 | // reload the page every so often if the visitor doesn't move their mouse 158 | var movementTimeout = createMovementTimeout(); 159 | document.onmousemove = function() { 160 | clearTimeout(movementTimeout); 161 | movementTimeout = createMovementTimeout(); 162 | } 163 | 164 | }); 165 | }; 166 | 167 | 168 | 169 | 170 | function gotoEditor(path: Path){ 171 | ui.hideVideo(() => { 172 | GLOBAL.MODE = "EDITOR"; 173 | video.zoom = 4; 174 | 175 | // Seek to the path start time 176 | video.seek(path.points[0].time, () => { 177 | ui.showVideo(); 178 | }); 179 | 180 | // Set the video zoom position 181 | video.zoomPos = { x: path.points[0].x, y: path.points[0].y }; 182 | video.updatePlayerSize(); 183 | 184 | // Update the interface 185 | $('#notes').hide(); 186 | $('#linedrawing').hide(); 187 | $('#logoHeader').hide(); 188 | $('#timeAndDate').hide(); 189 | $('#addNoteInterface').show(); 190 | 191 | $('#note-text').val('').focus(); 192 | 193 | $('#rewind').hide(); 194 | $('#back').show(); 195 | }); 196 | 197 | var trySubmit = ()=>{ 198 | var text = $('#note-text').val(); 199 | if(text) { 200 | var note = new Note([]); 201 | note.path = path; 202 | note.text = text; 203 | // Submit the path to the API 204 | api.submitNote(note); 205 | 206 | // GOTO video mode again 207 | gotoVideo(path.points[0].time-5000); 208 | } else { 209 | $('#note-text').attr('placeholder', 'Please write something').focus(); 210 | $('#submitButton').unbind('click').click(trySubmit) 211 | } 212 | }; 213 | 214 | $('#submitButton').unbind('click').click(trySubmit) 215 | 216 | 217 | var keypress = (e)=> { 218 | if (e.which == 13) { 219 | trySubmit(); 220 | e.preventDefault(); 221 | e.stopPropagation(); 222 | return false; 223 | } 224 | 225 | var textElm = $('#note-text'); 226 | if(textElm.val().length >= 140){ 227 | //textElm.val(textElm.val().substring(0,140)); 228 | e.preventDefault(); 229 | e.stopPropagation(); 230 | return false; 231 | } 232 | }; 233 | 234 | $('#note-text').unbind('keypress').keypress(keypress); 235 | } 236 | 237 | 238 | function gotoVideo(seekTime :number){ 239 | ui.hideVideo(()=>{ 240 | GLOBAL.MODE = "PLAYER"; 241 | video.zoom = 1; 242 | 243 | video.updatePlayerSize(); 244 | drawingCanvas.clearMouseTrail(); 245 | 246 | // Seek to the path start time 247 | video.seek(seekTime, () => { 248 | ui.showVideo(); 249 | }); 250 | 251 | $('#notes').show(); 252 | $('#addNoteInterface').hide(); 253 | $('#linedrawing').show(); 254 | $('#timeAndDate').show(); 255 | 256 | $('#rewind').show(); 257 | $('#back').hide(); 258 | $('#logoHeader').show(); 259 | }) 260 | } 261 | 262 | function updateVideoLoop(){ 263 | if(GLOBAL.MODE == "EDITOR"){ 264 | var time = drawingCanvas.mousePath.last().time; 265 | 266 | var diff = time- drawingCanvas.mousePath.points[0].time; 267 | if(diff < 3000){ 268 | time += 3000 - diff; 269 | } 270 | 271 | if(video.currentTime > time){ 272 | this.video.seek(drawingCanvas.mousePath.points[0].time, undefined, true); 273 | } 274 | } 275 | } 276 | 277 | -------------------------------------------------------------------------------- /public/compiled/VideoPlayer.js: -------------------------------------------------------------------------------- 1 | /// 2 | var VideoPlayer = (function () { 3 | function VideoPlayer(playlist, durations, modulusHours, events) { 4 | var _this = this; 5 | this.aspect = 16.0 / 9.0; 6 | this.zoom = 1.0; 7 | this.zoomPos = { x: 0, y: 0 }; 8 | this.loading = true; 9 | this.startTimes = []; 10 | this.durations = []; 11 | this.modulusHours = 1; 12 | this.totalDur = 0; 13 | /** Current time in millis **/ 14 | this.currentTime = 0; 15 | // onStateChange callback 16 | this.stateChangeCallback = function (state) { 17 | }; 18 | this.durations = durations; 19 | this.modulusHours = modulusHours; 20 | // Populate the startTimes array 21 | var _dur = 0; 22 | for (var i = 0; i < this.durations.length; i++) { 23 | this.startTimes.push(_dur); 24 | _dur += this.durations[i] * 1000; 25 | } 26 | this.startTimes.push(_dur); 27 | this.totalDur = _dur; 28 | this.events = events; 29 | this.ytplayer = new YT.Player('ytplayer', { 30 | height: 390, 31 | width: 640, 32 | // videoId: '', 33 | playerVars: { 34 | autoplay: 1, 35 | controls: 0, 36 | disablekb: 1, 37 | enablejsapi: 1, 38 | fs: 0, 39 | modestbranding: 1, 40 | origin: 'localhost', 41 | rel: 0, 42 | showinfo: 0, 43 | list: playlist, 44 | listType: 'playlist', 45 | start: 0, 46 | mute: 1 47 | }, 48 | events: { 49 | 'onReady': function () { 50 | _this.onPlayerReady(); 51 | }, 52 | 'onStateChange': function () { 53 | _this.onPlayerStateChange(); 54 | } 55 | } 56 | }); 57 | } 58 | VideoPlayer.prototype.updatePlayerSize = function () { 59 | var player = $('#videocontainer'); 60 | var size = this.calculatePlayerSize(); 61 | player.css({ 62 | left: size.left, 63 | top: size.top - 50, 64 | width: size.width, 65 | height: size.height + 100 66 | }); 67 | //updateMouseTrail(); 68 | }; 69 | VideoPlayer.prototype.clientToVideoCoord = function (clientX, clientY) { 70 | var playerSize = this.calculatePlayerSize(); 71 | var ret = { 72 | x: clientX, 73 | y: clientY 74 | }; 75 | ret.x -= playerSize.left; 76 | ret.y -= playerSize.top; 77 | ret.x /= playerSize.width; 78 | ret.y /= playerSize.height; 79 | return ret; 80 | }; 81 | VideoPlayer.prototype.videoToClientCoord = function (videoX, videoY) { 82 | var playerSize = this.calculatePlayerSize(); 83 | var ret = { 84 | x: videoX, 85 | y: videoY 86 | }; 87 | ret.x *= playerSize.width; 88 | ret.y *= playerSize.height; 89 | ret.x += playerSize.left; 90 | ret.y += playerSize.top; 91 | return ret; 92 | }; 93 | VideoPlayer.prototype.calculatePlayerSize = function () { 94 | var left = 0; 95 | var top = 0; 96 | if ($(window).width() / $(window).height() > this.aspect) { 97 | var width = $(window).width(); 98 | var height = $(window).width() / this.aspect; 99 | top = -(height - $(window).height()) / 2; 100 | } 101 | else { 102 | var width = $(window).height() * this.aspect; 103 | var height = $(window).height(); 104 | left = -(width - $(window).width()) / 2; 105 | } 106 | if (this.zoom != 1) { 107 | width *= this.zoom; 108 | height *= this.zoom; 109 | left = -this.zoomPos.x * width + $(window).width() * 0.25; 110 | top = -this.zoomPos.y * height + $(window).height() * 0.5; 111 | } 112 | return { left: left, top: top, width: width, height: height }; 113 | }; 114 | VideoPlayer.prototype.frameUpdate = function () { 115 | var time_update = this.ytplayer.getCurrentTime() * 1000; 116 | var playing = this.ytplayer.getPlayerState(); 117 | if (playing == 1) { 118 | if (this._last_time_update == time_update) { 119 | this.currentTime += 10; 120 | } 121 | if (this._last_time_update != time_update) { 122 | this.currentTime = time_update; 123 | //console.log(time_update); 124 | if (this.startTimes[this.ytplayer.getPlaylistIndex()]) { 125 | this.currentTime += this.startTimes[this.ytplayer.getPlaylistIndex()]; 126 | } 127 | } 128 | } 129 | this._last_time_update = time_update; 130 | if (this.events.onNewFrame) 131 | this.events.onNewFrame(this); 132 | /* updateAnimation(); 133 | updateNotes(); 134 | updateVideoLoop();*/ 135 | }; 136 | VideoPlayer.prototype.seek = function (ms, cb, dontFetchApi) { 137 | var _this = this; 138 | //console.log('seek: ' + ms); 139 | if (ms > this.totalDur) { 140 | ms %= this.totalDur; // loops back around to 3:00 - 3:27 141 | } 142 | var relativeMs; 143 | for (var i = 0; i < this.startTimes.length - 1; i++) { 144 | if (ms < this.startTimes[i + 1]) { 145 | if (this.ytplayer.getPlaylistIndex() != i) { 146 | this.ytplayer.playVideoAt(i); 147 | } 148 | relativeMs = ms - this.startTimes[i]; 149 | this.ytplayer.seekTo(relativeMs / 1000, true); 150 | break; 151 | } 152 | } 153 | this.currentTime = ms; 154 | // Start an interval and wait for the video to play again 155 | var interval = setInterval(function () { 156 | if (_this.ytplayer.getPlayerState() == 1) { 157 | clearInterval(interval); 158 | if (dontFetchApi != true) { 159 | api.fetchNotes(ms); 160 | } 161 | if (cb) 162 | cb(); 163 | } 164 | }, 100); 165 | }; 166 | VideoPlayer.prototype.onPlayerReady = function () { 167 | this.updatePlayerSize(); 168 | }; 169 | VideoPlayer.prototype.onPlayerStateChange = function () { 170 | var _this = this; 171 | if (this.stateChangeCallback) 172 | this.stateChangeCallback(this.ytplayer.getPlayerState()); 173 | this.ytplayer.mute(); 174 | if (this.ytplayer.getPlayerState() == 0) { 175 | this.seek(0); 176 | } 177 | if (this.loading && this.ytplayer.getPlayerState() == 1) { 178 | this.loading = false; 179 | if (this.events.onLoadComplete) { 180 | this.events.onLoadComplete(this); 181 | } 182 | setInterval(function () { 183 | _this.frameUpdate(); 184 | }, 10); 185 | } 186 | }; 187 | // use this from the frontend for testing 188 | VideoPlayer.prototype.setClock = function (time, cb) { 189 | this.setTime(moment(time, ['H:mm', 'HH:mm', 'HH:mm:ss', 'H:mm:ss'])); 190 | }; 191 | // use this from the backend to avoid time parsing problems 192 | VideoPlayer.prototype.setTime = function (time, cb) { 193 | // use the startTime data 194 | var target = moment(Clock.startTime); 195 | // use the time hours, minutes, seconds 196 | target.hour(time.hour()); 197 | target.minute(time.minute()); 198 | target.second(time.second()); 199 | // If the target is before the start clock of the video (its in the morning) 200 | if (target.isBefore(moment(Clock.startTime))) { 201 | target = target.add(24, 'hours'); 202 | } 203 | var hourMillis = 60 * 60 * 1000; 204 | var diff = target.diff(moment(Clock.startTime)); 205 | // modulus with the number of hours specified 206 | diff %= this.modulusHours * hourMillis; 207 | // Handle the case where the time is longer then the playlist, then pick a random hour 208 | if (diff > this.totalDur) { 209 | diff -= Math.floor(Math.random() * this.modulusHours) * hourMillis; 210 | } 211 | console.log(moment(Clock.startTime).add(diff, 'milliseconds').format()); 212 | video.seek(diff, cb); 213 | }; 214 | return VideoPlayer; 215 | })(); 216 | -------------------------------------------------------------------------------- /typings/svgjs/svgjs.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for svg.js 2 | // Project: http://www.svgjs.com/ 3 | // Definitions by: Sean Hess 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | // Reference: http://documentup.com/wout/svg.js 6 | 7 | // TODO sets 8 | // TODO gradients 9 | 10 | declare var SVG:svgjs.Library; 11 | 12 | declare module svgjs { 13 | 14 | export interface LinkedHTMLElement extends HTMLElement { 15 | instance: Element; 16 | } 17 | 18 | export interface Library { 19 | (selector:string):Doc; 20 | (domElement:HTMLElement):Doc; 21 | create(name:string):any; 22 | Element:ElementStatic; 23 | supported:boolean; 24 | get(id:string):Element; 25 | extend(parent:Object, obj:Object):void; 26 | } 27 | 28 | export interface Doc extends Element { 29 | svg(data:string):any; 30 | pattern(w:number, h:number, add:(e:Element)=>void):Element; 31 | 32 | defs():Defs; 33 | 34 | clear():void; 35 | 36 | mask():Mask; 37 | 38 | // TODO gradients 39 | } 40 | 41 | 42 | // https://github.com/wout/svg.filter.js 43 | export interface Filter { 44 | gaussianBlur(values:string):Filter; 45 | colorMatrix(name:string, value:number):Filter; 46 | colorMatrix(name:string, matrix:number[]):Filter; 47 | componentTransfer(components:{rgb?: FilterComponentTransfer; g?: FilterComponentTransfer;}):Filter; 48 | offset(x:number, y:number):Filter; 49 | blend():Filter; 50 | in(source:FilterSource):Filter; 51 | sourceAlpha:FilterSource; 52 | source:FilterSource; 53 | } 54 | 55 | export interface FilterSource { 56 | 57 | } 58 | 59 | 60 | export interface FilterComponentTransfer { 61 | type: string; 62 | tableValues?: string; 63 | slope?: number; 64 | intercept: number; 65 | amplitude: number; 66 | exponent: number; 67 | offset: number; 68 | } 69 | 70 | export interface Element extends Text, Parent { 71 | node:LinkedHTMLElement; 72 | 73 | nested():Doc; 74 | 75 | animate(duration?:number, ease?:string, delay?:number):Animation; 76 | animate(info:{ease?:string; duration?:number; delay?:number}):Animation; 77 | 78 | attr(name:string):any; 79 | attr(obj:Object):Element; 80 | attr(name:string, value:any, namespace?:string):Element; 81 | 82 | viewbox():Viewbox; 83 | viewbox(x:number, y:number, w:number, h:number):Element; 84 | viewbox(obj:Viewbox):Element; 85 | 86 | move(x:number, y:number, anchor?:boolean):Element; 87 | x(x:number, anchor?:boolean):Element; 88 | y(y:number, anchor?:boolean):Element; 89 | x(): number; 90 | y(): number; 91 | 92 | center(x:number, y:number, anchor?:boolean):Element; 93 | cx(x:number, anchor?:boolean):Element; 94 | cy(y:number, anchor?:boolean):Element; 95 | 96 | size(w:number, h:number, anchor?:boolean):Element; 97 | 98 | show():Element; 99 | hide():Element; 100 | visible():boolean; 101 | remove():void; 102 | 103 | each(iterator:(i?:number, children?:Element[])=>void, deep?:boolean):void; 104 | filter(adder:(filter:Filter)=>void):Element; 105 | 106 | transform(t:Transform):Element; 107 | transform(): Transform; 108 | 109 | style(name:string, value:string):Element; 110 | style(obj:Object):Element; 111 | style(name:string):string; 112 | style():string; 113 | bbox():BBox; 114 | rbox():RBox; 115 | doc():Doc; 116 | data(name:string):any; 117 | data(name:string, value:any):Element; 118 | remember(name:string, value:any):Element; 119 | remember(obj:Object):Element; 120 | remember(name:string):any; 121 | forget(...keys:string[]):Element; 122 | 123 | fill(fill:{color?:string; opacity?:number}):Element; 124 | fill(color:string):Element; 125 | fill(pattern:Element):Element; 126 | stroke(data:{color?:string; opacity?:number; width?: number}):Element; 127 | stroke(color:string):Element; 128 | opacity(o:number):Element; 129 | rotate(d:number, cx?:number, cy?:number):Element; 130 | skew(x:number, y:number):Element; 131 | scale(x:number, y:number):Element; 132 | translate(x:number, y:number):Element; 133 | 134 | maskWith(element:Element):Element; 135 | masker:Element; 136 | unmask():Element; 137 | 138 | clipWith(element:Element):Element; 139 | clipper:Element; 140 | unclip():Element; 141 | 142 | front():Element; 143 | back():Element; 144 | forward():Element; 145 | backward():Element; 146 | 147 | siblings():Element[]; 148 | position():number; 149 | next():Element; 150 | previous():Element; 151 | before(element:Element):Element; 152 | after(element:Element):Element; 153 | 154 | 155 | click(cb:Function):void; 156 | on(event:string, cb:Function):void; 157 | off(event:string, cb:Function):void; 158 | } 159 | 160 | export interface Mask extends Element { 161 | add(element:Element):Mask; 162 | } 163 | 164 | export interface Text { 165 | content:string; 166 | font(font:{family?:string; size?:number; anchor?:string; leading?:string}):Element; 167 | tspan(text:string):Element; 168 | path(data:string):Element; 169 | plot(data:string):Element; 170 | track:Element; 171 | } 172 | 173 | export interface ElementStatic extends Parent { 174 | new(node:any):Element; 175 | } 176 | 177 | export interface Defs extends Element {} 178 | 179 | 180 | export interface Animation { 181 | stop():Animation; 182 | 183 | attr(name:string, value:any, namespace?:string):Animation; 184 | attr(obj:Object):Animation; 185 | attr(name:string):any; 186 | 187 | viewbox(x:number, y:number, w:number, h:number):Animation; 188 | 189 | move(x:number, y:number, anchor?:boolean):Animation; 190 | x(x:number, anchor?:boolean):Animation; 191 | y(y:number, anchor?:boolean):Animation; 192 | 193 | center(x:number, y:number, anchor?:boolean):Animation; 194 | cx(x:number, anchor?:boolean):Animation; 195 | cy(y:number, anchor?:boolean):Animation; 196 | 197 | size(w:number, h:number, anchor?:boolean):Animation; 198 | during(cb:(pos:number)=>void):Animation; 199 | to(value:number):Animation; 200 | after(cb:()=>void):Animation; 201 | 202 | // TODO style, etc, bbox... 203 | } 204 | 205 | export interface Parent { 206 | put(element:Element, i?:number):Element; 207 | add(element:Element, i?:number):Element; 208 | children():Element[]; 209 | 210 | rect(w:number, h:number):Element; 211 | ellipse(w:number, h:number):Element; 212 | circle(diameter:number):Element; 213 | line(x1:number, y1:number, x2:number, y2:number):Element; 214 | polyline(data:string):Element; 215 | polyline(points:number[][]):Element; 216 | polygon(data:string):Element; 217 | polygon(points:number[][]):Element; 218 | path(data:string):Element; 219 | image(url:string, w?:number, h?:number):Element; 220 | text(text:string):Element; 221 | text(adder:(element:Element)=>void):Element; 222 | use(element:Element):Element; 223 | 224 | group():Element; 225 | } 226 | 227 | export interface BBox { 228 | height:number; 229 | width:number; 230 | y:number; 231 | x:number; 232 | cx:number; 233 | cy:number; 234 | merge(bbox:BBox):BBox; 235 | } 236 | 237 | export interface RBox extends BBox {} 238 | 239 | export interface Attributes { 240 | (name:string, value:any):void; 241 | (obj:Object):void; 242 | (name:string):any; 243 | } 244 | 245 | export interface Viewbox { 246 | x: number; 247 | y: number; 248 | width: number; 249 | height: number; 250 | zoom?: number; 251 | } 252 | 253 | export interface Transform { 254 | x?: number; 255 | y?: number; 256 | rotation?: number; 257 | cx?: number; 258 | cy?: number; 259 | scaleX?: number; 260 | scaleY?: number; 261 | skewX?: number; 262 | skewY?: number; 263 | matrix?: string; // 1,0,0,1,0,0 264 | a?: number; // direct digits of matrix 265 | b?: number; 266 | c?: number; 267 | d?: number; 268 | e?: number; 269 | f?: number; 270 | } 271 | } 272 | 273 | declare module "svg.js" { 274 | export = svgjs 275 | } 276 | -------------------------------------------------------------------------------- /typescript/VideoPlayer.ts: -------------------------------------------------------------------------------- 1 | /// 2 | declare var bowser:any; 3 | 4 | interface EventHandler { 5 | (player: VideoPlayer): void; 6 | } 7 | 8 | 9 | interface IVideoPlayerCallbacks { 10 | onLoadComplete?: EventHandler; 11 | onNewFrame?: EventHandler; 12 | } 13 | 14 | class VideoPlayer { 15 | ytplayer:YT.Player; 16 | 17 | aspect = 16.0/9.0; 18 | zoom = 1.0; 19 | zoomPos = {x:0, y:0}; 20 | loading = true; 21 | startTimes = []; 22 | durations = []; 23 | modulusHours = 1; 24 | public totalDur = 0; 25 | /** Current time in millis **/ 26 | currentTime: number = 0; 27 | 28 | // Events 29 | events:IVideoPlayerCallbacks; 30 | 31 | // onStateChange callback 32 | stateChangeCallback = (state)=>{}; 33 | 34 | constructor(playlist : string, 35 | durations: number[], 36 | modulusHours: number, 37 | events : IVideoPlayerCallbacks){ 38 | 39 | this.durations = durations; 40 | this.modulusHours = modulusHours; 41 | // Populate the startTimes array 42 | var _dur=0; 43 | for(var i=0;i {this.onPlayerReady()}, 73 | 'onStateChange': () => {this.onPlayerStateChange()}, 74 | //'onPlaybackQualityChange': onPlayerPlaybackQualityChange 75 | } 76 | }); 77 | 78 | } 79 | 80 | 81 | 82 | 83 | 84 | updatePlayerSize(){ 85 | var player = $('#videocontainer'); 86 | 87 | var size = this.calculatePlayerSize(); 88 | 89 | player.css({ 90 | left: size.left, 91 | top: size.top-50, 92 | width: size.width, 93 | height: size.height+100 94 | }); 95 | 96 | //updateMouseTrail(); 97 | } 98 | 99 | clientToVideoCoord(clientX:number, clientY:number){ 100 | var playerSize = this.calculatePlayerSize(); 101 | 102 | var ret = { 103 | x: clientX, 104 | y: clientY 105 | }; 106 | 107 | ret.x -= playerSize.left; 108 | ret.y -= playerSize.top; 109 | ret.x /= playerSize.width; 110 | ret.y /= playerSize.height; 111 | 112 | return ret; 113 | } 114 | 115 | videoToClientCoord(videoX:number, videoY:number){ 116 | var playerSize =this.calculatePlayerSize(); 117 | 118 | var ret = { 119 | x: videoX, 120 | y: videoY 121 | }; 122 | 123 | ret.x *= playerSize.width; 124 | ret.y *= playerSize.height; 125 | ret.x += playerSize.left; 126 | ret.y += playerSize.top; 127 | 128 | return ret; 129 | } 130 | 131 | calculatePlayerSize(){ 132 | var left = 0; 133 | var top = 0; 134 | 135 | if($(window).width() / $(window).height() > this.aspect){ 136 | var width = $(window).width() ; 137 | var height = $(window).width() / this.aspect; 138 | top = -(height - $(window).height())/2; 139 | 140 | } else { 141 | var width = $(window).height() * this.aspect; 142 | var height = $(window).height(); 143 | left = -(width - $(window).width())/2; 144 | } 145 | 146 | if(this.zoom != 1){ 147 | width *= this.zoom; 148 | height*= this.zoom; 149 | 150 | left = -this.zoomPos.x * width + $(window).width() * 0.25; 151 | top = -this.zoomPos.y * height + $(window).height() * 0.5; 152 | } 153 | 154 | return {left: left, top: top, width:width, height:height}; 155 | } 156 | 157 | 158 | private _last_time_update:number; 159 | frameUpdate(){ 160 | var time_update = this.ytplayer.getCurrentTime()*1000; 161 | var playing = this.ytplayer.getPlayerState(); 162 | 163 | if (playing==1) { 164 | 165 | if (this._last_time_update == time_update) { 166 | this.currentTime += 10; 167 | } 168 | 169 | if (this._last_time_update != time_update) { 170 | this.currentTime = time_update; 171 | //console.log(time_update); 172 | if(this.startTimes[this.ytplayer.getPlaylistIndex()]){ 173 | this.currentTime += this.startTimes[this.ytplayer.getPlaylistIndex()]; 174 | } 175 | 176 | //clockTime = new Date("April 15, 2015 11:13:00"); 177 | //clockTime.setSeconds(clockTime.getSeconds()+ time_update/1000) 178 | } 179 | 180 | } 181 | 182 | this._last_time_update = time_update; 183 | 184 | 185 | if(this.events.onNewFrame) this.events.onNewFrame(this); 186 | /* updateAnimation(); 187 | updateNotes(); 188 | updateVideoLoop();*/ 189 | 190 | 191 | } 192 | 193 | seek(ms: number, cb?: (() => void), dontFetchApi?: boolean) { 194 | //console.log('seek: ' + ms); 195 | if (ms > this.totalDur) { // this is possible between 2:27 - 3:00 196 | ms %= this.totalDur; // loops back around to 3:00 - 3:27 197 | } 198 | var relativeMs; 199 | for(var i=0;i{ 213 | if(this.ytplayer.getPlayerState() == 1){ 214 | clearInterval(interval); 215 | if(dontFetchApi != true) { 216 | api.fetchNotes(ms); 217 | } 218 | if(cb) cb(); 219 | } 220 | },100); 221 | } 222 | 223 | onPlayerReady(){ 224 | this.updatePlayerSize(); 225 | } 226 | 227 | onPlayerStateChange(){ 228 | 229 | if(this.stateChangeCallback) this.stateChangeCallback(this.ytplayer.getPlayerState()); 230 | 231 | this.ytplayer.mute(); 232 | 233 | if(this.ytplayer.getPlayerState() == 0){ 234 | this.seek(0); 235 | } 236 | 237 | if(this.loading && this.ytplayer.getPlayerState() == 1){ 238 | this.loading = false; 239 | 240 | if(this.events.onLoadComplete){ 241 | this.events.onLoadComplete(this); 242 | } 243 | setInterval(()=>{this.frameUpdate()}, 10); 244 | } 245 | } 246 | 247 | // use this from the frontend for testing 248 | setClock(time: string, cb?: (() => void)){ 249 | this.setTime(moment(time, ['H:mm', 'HH:mm', 'HH:mm:ss', 'H:mm:ss'])); 250 | } 251 | 252 | // use this from the backend to avoid time parsing problems 253 | setTime(time: any, cb?: (() => void)){ 254 | // use the startTime data 255 | var target = moment(Clock.startTime); 256 | // use the time hours, minutes, seconds 257 | target.hour(time.hour()); 258 | target.minute(time.minute()); 259 | target.second(time.second()); 260 | 261 | // If the target is before the start clock of the video (its in the morning) 262 | if(target.isBefore(moment(Clock.startTime))){ 263 | target = target.add(24, 'hours'); 264 | } 265 | 266 | var hourMillis = 60*60*1000; 267 | var diff = target.diff(moment(Clock.startTime)); 268 | 269 | // modulus with the number of hours specified 270 | diff %= this.modulusHours * hourMillis; 271 | 272 | // Handle the case where the time is longer then the playlist, then pick a random hour 273 | if(diff > this.totalDur){ 274 | diff -= Math.floor(Math.random() * this.modulusHours)*hourMillis; 275 | } 276 | console.log(moment(Clock.startTime).add(diff,'milliseconds').format()); 277 | 278 | video.seek(diff, cb); 279 | } 280 | } 281 | 282 | -------------------------------------------------------------------------------- /api.js: -------------------------------------------------------------------------------- 1 | var boring = require('./boring'); 2 | var query = require('pg-query'); 3 | var express = require('express'); 4 | var bodyParser = require("body-parser"); 5 | var request = require('request'); 6 | 7 | // use the heroku database 8 | // query.connectionParameters = process.env.HEROKU_POSTGRESQL_AQUA_URL; 9 | 10 | // use the local socket connection 11 | query.connectionParameters = { 12 | host: '/var/run/postgresql', 13 | database: 'exhausting' 14 | }; 15 | 16 | module.exports = { 17 | setup: function(){ 18 | this.api = express(); 19 | this.api.use(bodyParser.json()); 20 | this.api.enable('trust proxy') 21 | 22 | /*query("SELECT * FROM information_schema.tables where table_name = 'paths'", function(rows, ret){ 23 | if(ret.length == 0){ 24 | query('CREATE TABLE "public"."paths" (\ 25 | "id" serial,\ 26 | "coordinate" point,\ 27 | "time" int,\ 28 | "note_id" int,\ 29 | PRIMARY KEY ("id"));') 30 | } 31 | });*/ 32 | 33 | query("SELECT * FROM information_schema.tables where table_name = 'notes'", function(rows, ret){ 34 | if(ret.length == 0){ 35 | query('CREATE TABLE "public"."notes" (\ 36 | "id" serial,\ 37 | "time_begin" int,\ 38 | "time_end" int,\ 39 | "note" text,\ 40 | "ip" cidr,\ 41 | "timestamp" timestamp,\ 42 | "path" float[][],\ 43 | "hidden" boolean,\ 44 | "site" int NOT NULL DEFAULT 0, \ 45 | PRIMARY KEY ("id"));') 46 | } 47 | }); 48 | 49 | query("SELECT * FROM information_schema.tables where table_name = 'blacklist'", function(rows, ret){ 50 | if(ret.length == 0){ 51 | query('CREATE TABLE "public"."blacklist" (\ 52 | "ip" cidr,\ 53 | PRIMARY KEY ("ip"));') 54 | } 55 | }); 56 | 57 | this.api.get('/notes/count', function (req, res) { 58 | var site = req.query.site || 0; 59 | query('SELECT count(*) FROM notes ' + 60 | 'where site = $1', [site], function(err, ret){ 61 | if(err){ 62 | res.status(500).send('Could not select notes'); 63 | console.log(err); 64 | return; 65 | } 66 | if(!ret.length){ 67 | res.status(500).send('SELECT returned no results'); 68 | console.log(err); 69 | return; 70 | } 71 | res.send(ret[0]); 72 | }); 73 | }) 74 | 75 | this.api.get('/regex', function (req, res) { 76 | res.json({ 77 | all: boring.getRegex(), 78 | psql: boring.getPsqlRegex(), 79 | parts: boring.getRegexes() 80 | }); 81 | }) 82 | 83 | this.api.get('/clean', function (req, res) { 84 | console.log('Cleaning: marking all boring content in database hidden.'); 85 | var psqlRegex = boring.getPsqlRegex(); 86 | query('update notes set hidden = true where hidden is null and lower(note) ~ $1 returning note', 87 | [psqlRegex], function(err, ret) { 88 | if(err){ 89 | res.status(500).send('Could not select notes'); 90 | console.log(err); 91 | return; 92 | } 93 | res.send(ret.map(function(result) { 94 | console.log("Done cleaning"); 95 | return result.note; 96 | })); 97 | }) 98 | }) 99 | 100 | this.api.get('/notes/recent/hidden', function (req, res) { 101 | var limit = Math.min((req.query.limit || 250), 1000); 102 | var site = req.query.site || 0; 103 | query('select note from notes where hidden = true and site = $1 order by timestamp desc limit $2', 104 | [site, limit], function(err, ret) { 105 | if(err){ 106 | res.status(500).send('Could not select notes'); 107 | console.log(err); 108 | return; 109 | } 110 | res.send(ret.map(function(result) { 111 | return result.note; 112 | })); 113 | }) 114 | }) 115 | 116 | this.api.get('/notes/recent/visible', function (req, res) { 117 | var limit = Math.min((req.query.limit || 250), 1000); 118 | var site = req.query.site || 0; 119 | query('select note from notes where hidden is null and site = $1 order by timestamp desc limit $2', 120 | [site, limit], function(err, ret) { 121 | if(err){ 122 | res.status(500).send('Could not select notes'); 123 | console.log(err); 124 | return; 125 | } 126 | res.send(ret.map(function(result) { 127 | return result.note; 128 | })); 129 | }) 130 | }) 131 | 132 | this.api.get('/notes', function (req, res) { 133 | var startTime = Math.round(req.query.timeframeStart); 134 | var endTime = Math.round(req.query.timeframeEnd); 135 | var ip = req.query.ip || req.ip; 136 | var site = req.query.site || 0; 137 | query( 138 | 'select id, time_begin, time_end, note, path ' + 139 | 'from notes ' + 140 | 'where time_end >= $1 ' + 141 | 'and time_begin <= $2 ' + 142 | 'and site = $3 ' + 143 | 'and ( ' + 144 | 'ip = $4 ' + 145 | 'or not ( ' + 146 | 'hidden is true ' + 147 | 'or ( ' + 148 | 'hidden is null ' + 149 | 'and exists( ' + 150 | 'select 1 ' + 151 | 'from blacklist ' + 152 | 'where ip = notes.ip)))) ' + 153 | 'limit 250', 154 | [startTime, endTime, site, ip], function(err, ret){ 155 | if(err ){ 156 | res.status(500).send('Could not select notes'); 157 | console.log(err); 158 | return; 159 | } 160 | 161 | for(var u=0;u 140) { 201 | text = text.substr(0, 140); 202 | } 203 | 204 | // TODO value testing 205 | var q = []; 206 | for(var i=0;i 0){ 223 | if(ret.time_begin == Math.round(paths[0].time) && ret.time_end == Math.round(paths[paths.length-1].time)){ 224 | res.status(500).send('Duplicate entry'); 225 | return; 226 | } 227 | } 228 | 229 | query('SELECT count(*) as count ' + 230 | 'FROM "notes" ' + 231 | 'WHERE ip = $1 ' + 232 | 'AND timestamp > (now() - interval \'10 minute\');', 233 | [req.ip], function(err, ret){ 234 | if(err ){ 235 | res.status(500).send('Could not select notes'); 236 | console.log(err); 237 | return; 238 | } 239 | if(ret.length > 0 && ret[0].count > 15){ 240 | res.status(500).send('Note add rate limit'); 241 | return; 242 | } 243 | 244 | var hidden = boring.check(text); 245 | query('INSERT INTO "public"."notes" ("time_begin", "time_end", "note", "ip", "timestamp", "path", "hidden", "site") VALUES ($1, $2, $3, $4, now(), $5, $6, $7) RETURNING id', 246 | [ 247 | Math.round(paths[0].time), 248 | Math.round(paths[paths.length-1].time), 249 | text, 250 | req.ip, 251 | "{"+ q.join(",")+"}", 252 | hidden ? true : null, 253 | site 254 | ], function(err, ret) { 255 | 256 | if(err || ret.length == 0){ 257 | res.status(500).send('Could not submit note'); 258 | console.log(err); 259 | return; 260 | } 261 | 262 | var note_id = ret[0].id; 263 | res.send({id:note_id}); 264 | }) 265 | } 266 | ); 267 | }); 268 | }) 269 | 270 | } 271 | 272 | }; -------------------------------------------------------------------------------- /sass/style.scss: -------------------------------------------------------------------------------- 1 | @mixin filter($filter-type,$filter-amount) { 2 | -webkit-filter: $filter-type+unquote('(#{$filter-amount})'); 3 | -moz-filter: $filter-type+unquote('(#{$filter-amount})'); 4 | -ms-filter: $filter-type+unquote('(#{$filter-amount})'); 5 | -o-filter: $filter-type+unquote('(#{$filter-amount})'); 6 | filter: $filter-type+unquote('(#{$filter-amount})'); 7 | } 8 | 9 | @mixin no-select { 10 | -webkit-touch-callout: none; 11 | -webkit-user-select: none; 12 | -khtml-user-select: none; 13 | -moz-user-select: none; 14 | -ms-user-select: none; 15 | user-select: none; 16 | } 17 | 18 | @mixin font-smoothing($value: on) { 19 | @if $value == on { 20 | -webkit-font-smoothing: antialiased; 21 | -moz-osx-font-smoothing: grayscale; 22 | } 23 | @else { 24 | -webkit-font-smoothing: subpixel-antialiased; 25 | -moz-osx-font-smoothing: auto; 26 | } 27 | } 28 | 29 | $edgePad: 3vmin; 30 | $innerPad: 1vmin; 31 | 32 | $noteTextWidth: 165px; 33 | 34 | $transparentBlack: rgba(0,0,0,0.8); 35 | $transparentBlackHover: rgba(0,0,0,0.9); 36 | 37 | body { 38 | padding:0; 39 | margin:0; 40 | overflow:hidden; 41 | font-family: 'Dosis', sans-serif; 42 | @include font-smoothing(on); /* good for light text on dark bg */ 43 | font-weight: 400; 44 | color: white; 45 | background-color: black; 46 | } 47 | 48 | .local { 49 | display: none; 50 | } 51 | 52 | #videocontainer { 53 | position:absolute; 54 | top:0; 55 | width:100%; 56 | height:100%; 57 | overflow:hidden; 58 | 59 | -webkit-transition: -webkit-filter 250ms; 60 | 61 | &.blur { 62 | -webkit-filter: blur(50px); 63 | } 64 | 65 | #ytplayer { 66 | width:100%; 67 | height:100%; 68 | } 69 | 70 | #drawing , #linedrawing{ 71 | position:absolute; 72 | top:0; 73 | margin-bottom:50px; 74 | margin-top:50px; 75 | left:0; 76 | width:100%; 77 | //height:100%; 78 | bottom:0; 79 | 80 | } 81 | } 82 | 83 | #infoHeader { 84 | position: absolute; 85 | top: $edgePad; 86 | left: $edgePad; 87 | display: inline-block; 88 | padding: 0 $innerPad $innerPad; 89 | 90 | h1 { 91 | font-size: 5vmin; 92 | font-weight: 300; 93 | margin: auto; 94 | padding: 0; 95 | } 96 | 97 | p { 98 | font-size: 2.2vmin; 99 | font-weight: 400; 100 | margin: auto; 101 | padding: 0; 102 | } 103 | 104 | .light { 105 | font-weight: 300; 106 | } 107 | } 108 | 109 | #logoHeader { 110 | position:absolute; 111 | top: $edgePad; 112 | right: $edgePad; 113 | display: inline-block; 114 | padding: $innerPad; 115 | img { 116 | height: 4vmin; 117 | } 118 | } 119 | 120 | #locationHeader { 121 | position:absolute; 122 | top: $edgePad; 123 | left: $edgePad; 124 | display: inline-block; 125 | padding: $innerPad/2; 126 | font-size: 0.8em; 127 | cursor: crosshair; 128 | text-transform: uppercase; 129 | * { 130 | cursor: inherit; 131 | } 132 | a { 133 | cursor: pointer; 134 | color: white; 135 | text-decoration: none; 136 | } 137 | } 138 | 139 | #logoHeader, #infoHeader, #locationHeader { 140 | z-index: 100; 141 | @include no-select; 142 | background-color: $transparentBlack; 143 | } 144 | 145 | #logoHeader, #infoHeader { 146 | cursor: pointer; 147 | &:hover { 148 | background-color: $transparentBlackHover; 149 | } 150 | } 151 | 152 | #timeAndDate { 153 | background-color: $transparentBlack; 154 | position: absolute; 155 | right: $edgePad; 156 | bottom: $edgePad; 157 | padding: ($innerPad/2) $innerPad; 158 | margin: 0; 159 | display: inline-block; 160 | font-size: 4.5vmin; 161 | font-weight: 200; 162 | text-align: center; 163 | text-rendering: geometricPrecision; 164 | @include no-select; 165 | #colon { 166 | position: relative; 167 | top: -.5vmin; 168 | } 169 | } 170 | 171 | 172 | 173 | #clickArea { 174 | height:100%; 175 | width:100%; 176 | position:absolute; 177 | top:0; 178 | cursor: crosshair; 179 | z-index: 96; 180 | } 181 | 182 | #credits { 183 | opacity:0; 184 | display:none; 185 | background-color: $transparentBlack; 186 | //display:none; 187 | width:100%; 188 | height:100%; 189 | left:0; 190 | top:0; 191 | position: absolute; 192 | z-index: 100; 193 | 194 | text-align: center; 195 | line-height: 0.4; 196 | 197 | @include no-select; 198 | cursor: default; 199 | 200 | .credits-content { 201 | position: relative; 202 | top: 30vh; 203 | } 204 | 205 | .credits-divider { 206 | span { 207 | border-bottom: 1px solid white; 208 | position: relative; 209 | width: 100px; 210 | display: inline-block; 211 | top: -5px; 212 | margin: 0 8px; 213 | 214 | } 215 | } 216 | h1 { 217 | font-weight: 100; 218 | } 219 | p { 220 | font-weight: 400; 221 | } 222 | .for { 223 | font-weight: 100; 224 | margin: 0 4px; 225 | } 226 | a { 227 | color: inherit; 228 | text-decoration: inherit; 229 | } 230 | } 231 | 232 | #overlay { 233 | /*height:100%; 234 | width:100%; 235 | position:absolute; 236 | top:0; 237 | z-index: 99;*/ 238 | 239 | #rewind, #back { 240 | position: absolute; 241 | left: 1.5*$edgePad; 242 | bottom: 1.5*$edgePad; 243 | width: 10vmin; 244 | height: 10vmin; 245 | z-index: 100; 246 | opacity: .60; 247 | @include no-select; 248 | 249 | img { 250 | width:100%; 251 | height:100%; 252 | } 253 | 254 | .tooltip { 255 | position: absolute; 256 | top: 4vmin; 257 | width: 200%; 258 | left: 11vmin; 259 | vertical-align: middle; 260 | display:none; 261 | 262 | } 263 | } 264 | 265 | #rewind:hover, #back:hover { 266 | cursor: pointer; 267 | opacity: .80; 268 | .tooltip { 269 | display:inline; 270 | } 271 | } 272 | 273 | .note { 274 | z-index: 95; 275 | 276 | position:absolute; 277 | max-width: $noteTextWidth; 278 | word-wrap: break-word; 279 | @include no-select; 280 | cursor: crosshair; 281 | 282 | &:hover { 283 | .note-white { 284 | background-color: rgba(255, 255, 255, 0.1); 285 | } 286 | // border:1px solid rgba(255,255,255,0.4); 287 | } 288 | 289 | .note-text { 290 | background-color: $transparentBlack; 291 | padding: 0.2em 0.4em 0.4em; 292 | font-size: 1.0em; 293 | } 294 | 295 | } 296 | 297 | #addNoteInterface { 298 | z-index: 100; 299 | 300 | right:0; 301 | width: 50%; 302 | background-color: $transparentBlack; 303 | height:100%; 304 | position: absolute; 305 | 306 | #note-header { 307 | font-size:5.6vw; 308 | color: black; 309 | margin-left: 4vmin; 310 | margin-top: 2vmin; 311 | } 312 | 313 | #note-wrapper { 314 | margin-left: 4vmin; 315 | margin-right: 4vmin; 316 | height: auto; 317 | top: 4vmin; 318 | bottom: 18vmin; 319 | position: absolute; 320 | right: 0; 321 | left: 0; 322 | 323 | // border: 1px solid #D7D7D7; 324 | // background-color: rgba(255, 255, 255, 0.57); 325 | 326 | textarea { 327 | width:100%; 328 | height:100%; 329 | 330 | border: 0; 331 | color: white; 332 | background: none; 333 | resize: none; 334 | 335 | font-family: 'Dosis', sans-serif; 336 | font-size: 6vmin; 337 | outline: none !important; 338 | } 339 | } 340 | 341 | #submitButton { 342 | right: 0; 343 | left: 0; 344 | bottom: 0; 345 | background-color: $transparentBlack; 346 | color: white; 347 | position: absolute; 348 | margin: 4vmin; 349 | padding: 2vmin; 350 | font-size: 6vmin; 351 | @include no-select; 352 | cursor: pointer; 353 | text-align: center; 354 | 355 | &:hover { 356 | background-color: $transparentBlackHover; 357 | } 358 | } 359 | } 360 | 361 | } 362 | 363 | .spinner { 364 | width: 4vh; 365 | height: 4vh; 366 | opacity: .5; 367 | } 368 | 369 | #persistent-spinner { 370 | position: absolute; 371 | top: 50%; 372 | left: 50%; 373 | margin-left: -2vh; 374 | margin-top: -2vh; 375 | display: none; 376 | } 377 | 378 | 379 | #loading { 380 | width:100%; 381 | height:100%; 382 | left:0; 383 | top:0; 384 | position: absolute; 385 | z-index: 100; 386 | text-align: center; 387 | background-color:white; 388 | @include no-select; 389 | cursor: default; 390 | 391 | #loading-middle { 392 | position: relative; 393 | top: 35vh; 394 | color: gray; 395 | text-rendering: geometricPrecision; 396 | 397 | * { 398 | margin: 2vh; 399 | } 400 | p { 401 | font-size: 2vh; 402 | margin: 0; 403 | } 404 | .title { 405 | margin-bottom: -1vh; 406 | font-size: 5vh; 407 | } 408 | .location { 409 | font-size: 3vh; 410 | text-transform: uppercase; 411 | } 412 | } 413 | } 414 | 415 | #browser-error { 416 | color:black; 417 | text-align:center; 418 | top: 100px; 419 | position:relative; 420 | font-size:2em; 421 | 422 | #title { 423 | font-size:2em; 424 | line-height: 2em; 425 | } 426 | 427 | #error, #error-mobile { 428 | line-height: 1.6em; 429 | } 430 | 431 | b { 432 | font-weight: 400; 433 | } 434 | } 435 | 436 | @media only screen and (max-device-width: 480px) { 437 | #browser-error { 438 | top: 50px; 439 | font-size: 1em; 440 | } 441 | } -------------------------------------------------------------------------------- /public/compiled/drawingCanvas.js: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | var DrawingCanvas = (function () { 6 | function DrawingCanvas() { 7 | var _this = this; 8 | this.events = {}; 9 | this.isDragging = false; 10 | // Setup the drawing canvas 11 | this.drawing = SVG('drawing'); 12 | this.linedrawing = SVG('linedrawing'); 13 | $("#clickArea").mousedown(function (event) { 14 | if (GLOBAL.playerMode()) { 15 | // Reset the mousePath 16 | _this.mousePath = new Path([]); 17 | // Calculate the % position clicked 18 | var mousePos = _this.video.clientToVideoCoord(event.pageX, event.pageY); 19 | // Add the position to the mousePath 20 | _this.mousePath.push(new PathPoint(mousePos.x, mousePos.y, _this.video.currentTime)); 21 | _this.lastMouseTime = 0; 22 | // Listen for mouse drag 23 | $("#clickArea").mousemove(function (event) { 24 | if (event.which == 0) { 25 | // Stop dragging 26 | _this.isDragging = false; 27 | $("#clickArea").unbind("mousemove"); 28 | } 29 | else { 30 | // Add the mouse position to the path, if the time has changed 31 | _this.isDragging = true; 32 | var mousePos = _this.video.clientToVideoCoord(event.pageX, event.pageY); 33 | var mouseTime = _this.video.currentTime; 34 | if (_this.lastMouseTime != mouseTime) { 35 | _this.lastMouseTime = mouseTime; 36 | _this.mousePath.push(new PathPoint(mousePos.x, mousePos.y, mouseTime)); 37 | _this.updateMouseTrail(); 38 | } 39 | } 40 | }); 41 | } 42 | }).mouseup(function (event) { 43 | if (GLOBAL.playerMode()) { 44 | // Listen for mouseUp events 45 | var wasDragging = _this.isDragging; 46 | _this.isDragging = false; 47 | $("#clickArea").unbind("mousemove"); 48 | // if (wasDragging) { 49 | // Simplify the mouse trail 50 | _this.mousePath.simplify(); 51 | // Calculate the % position clicked 52 | var mousePos = _this.video.clientToVideoCoord(event.pageX, event.pageY); 53 | // Add the position to the mousePath 54 | _this.mousePath.push(new PathPoint(mousePos.x, mousePos.y, _this.video.currentTime)); 55 | // And go to the editor mode 56 | //gotoEditor(); 57 | if (_this.events.onDrawingComplete) 58 | _this.events.onDrawingComplete(_this.mousePath); 59 | } 60 | }); 61 | $(window).mouseleave(function (event) { 62 | if (GLOBAL.playerMode() && _this.isDragging) { 63 | // Listen for mouseUp events 64 | var wasDragging = _this.isDragging; 65 | _this.isDragging = false; 66 | $("#clickArea").unbind("mousemove"); 67 | // if (wasDragging) { 68 | // Simplify the mouse trail 69 | _this.mousePath.simplify(); 70 | // Calculate the % position clicked 71 | var mousePos = _this.video.clientToVideoCoord(event.pageX, event.pageY); 72 | // Add the position to the mousePath 73 | _this.mousePath.push(new PathPoint(mousePos.x, mousePos.y, _this.video.currentTime)); 74 | // And go to the editor mode 75 | //gotoEditor(); 76 | if (_this.events.onDrawingComplete) 77 | _this.events.onDrawingComplete(_this.mousePath); 78 | } 79 | }); 80 | } 81 | DrawingCanvas.prototype.updateMouseTrail = function () { 82 | if (!this.mousePolyline) { 83 | this.mousePolyline = this.drawing.polyline([]).fill('none').stroke({ width: 5, color: 'white', opacity: .5 }); 84 | } 85 | var c = $('#drawing'); 86 | var scaleX = c.width(); 87 | var scaleY = c.height(); 88 | var p = []; 89 | for (var i = 0; i < this.mousePath.points.length; i++) { 90 | p.push([this.mousePath.points[i].x * scaleX, this.mousePath.points[i].y * scaleY]); 91 | } 92 | var casted = this.mousePolyline; 93 | casted.plot(p); 94 | }; 95 | DrawingCanvas.prototype.updateAnimation = function () { 96 | if (GLOBAL.editorMode()) { 97 | var p = this.mousePath.getPosAtTime(this.video.currentTime); 98 | //'); 128 | var noteText = $('
'); 129 | noteText.text(note.text); 130 | note.elm.append(noteText); 131 | $('#notes').append(note.elm); 132 | note.elm.attr('id', note.id); 133 | note.line = this.linedrawing.polyline([]).fill('none').stroke({ width: 2, color: 'rgba(0,0,0,0.5)' }); 134 | } 135 | this.updateNoteElm(note, p); 136 | } 137 | else if (note.path.last().time + 100 < video.currentTime) { 138 | // Remove old notes 139 | if (note.elm) { 140 | this.removeNote(note); 141 | } 142 | notes.splice(i, 1); 143 | i--; 144 | } 145 | else if (note.elm && note.path.first().time > video.currentTime) { 146 | // Hide notes not visible yet 147 | this.removeNote(note); 148 | } 149 | } 150 | }; 151 | // Update a specific notes visual elements position 152 | DrawingCanvas.prototype.updateNoteElm = function (note, p) { 153 | if (p) { 154 | var pos = video.videoToClientCoord(p.x, p.y); 155 | // console.log(p,pos); 156 | if (pos != note.curPos) { 157 | if (!note.curPos) { 158 | note.curPos = pos; 159 | } 160 | var dirVec = { x: (pos.x - note.curPos.x), y: (pos.y - note.curPos.y) }; 161 | var length = Math.sqrt(dirVec.x * dirVec.x + dirVec.y * dirVec.y); 162 | if (length > 0) { 163 | var dirUnitVec = { 164 | x: dirVec.x / length, 165 | y: dirVec.y / length 166 | }; 167 | } 168 | else { 169 | var dirUnitVec = { 170 | x: 1.0, 171 | y: 0.0 172 | }; 173 | } 174 | var dist = 40; 175 | var goalDir = { 176 | x: dirVec.x - dirUnitVec.x * dist, 177 | y: dirVec.y - dirUnitVec.y * dist 178 | }; 179 | note.curPos.x += goalDir.x * 0.1; 180 | note.curPos.y += goalDir.y * 0.1; 181 | var offset = { x: -1, y: -1 }; 182 | if (dirUnitVec.x > 0.1) { 183 | offset.x = -note.elm.children(".note-text").outerWidth() + 1; 184 | } 185 | if (dirUnitVec.y > 0.5) { 186 | offset.y = -note.elm.children(".note-text").outerHeight() + 1; 187 | } 188 | var playerSize = video.calculatePlayerSize(); 189 | if (note.curPos.y + offset.y > playerSize.height) { 190 | note.curPos.y = playerSize.height - offset.y; 191 | } 192 | if (note.curPos.y + offset.y < 0) { 193 | note.curPos.y = 0 - offset.y; 194 | } 195 | if (note.curPos.x + offset.x > playerSize.width) { 196 | note.curPos.x = playerSize.width - offset.x; 197 | } 198 | if (note.curPos.x + offset.x < 0) { 199 | note.curPos.x = 0 - offset.x; 200 | } 201 | //console.log(note.curPos.y); 202 | //console.log(offset); 203 | note.elm.css({ 204 | top: note.curPos.y + offset.y, 205 | left: note.curPos.x + offset.x 206 | }); 207 | /* console.log({ 208 | top: note.curPos.y, 209 | left: note.curPos.x 210 | })*/ 211 | var c = $('#drawing'); 212 | var scaleX = c.width(); 213 | var scaleY = c.height(); 214 | var p2 = video.clientToVideoCoord(note.curPos.x, note.curPos.y); 215 | note.line.plot([[Math.floor(p2.x * scaleX), Math.floor(p2.y * scaleY)], [p.x * scaleX, p.y * scaleY]]); 216 | } 217 | } 218 | }; 219 | DrawingCanvas.prototype.removeNote = function (note) { 220 | //console.log("Remove ", note); 221 | note.elm.remove(); 222 | note.line.plot([]); 223 | delete note.line; 224 | delete note.elm; 225 | }; 226 | return DrawingCanvas; 227 | })(); 228 | -------------------------------------------------------------------------------- /typescript/drawingCanvas.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | 6 | 7 | 8 | interface IDrawingCallback { 9 | onDrawingComplete?: (path: Path) => void; 10 | } 11 | 12 | 13 | class DrawingCanvas { 14 | public api : NotesApi; 15 | public video : VideoPlayer; 16 | public events : IDrawingCallback = {}; 17 | 18 | public mousePath : Path; 19 | 20 | drawing : svgjs.Element; 21 | linedrawing : svgjs.Element; 22 | mousePolyline : svgjs.Element; 23 | circle: svgjs.Element; 24 | 25 | lastMouseTime : number; 26 | isDragging : boolean = false; 27 | 28 | 29 | 30 | 31 | constructor(){ 32 | // Setup the drawing canvas 33 | this.drawing = SVG('drawing'); 34 | this.linedrawing = SVG('linedrawing'); 35 | 36 | 37 | $("#clickArea") 38 | .mousedown((event)=>{ 39 | 40 | if(GLOBAL.playerMode()) { 41 | // Reset the mousePath 42 | this.mousePath = new Path([]); 43 | 44 | // Calculate the % position clicked 45 | var mousePos = this.video.clientToVideoCoord(event.pageX, event.pageY); 46 | 47 | // Add the position to the mousePath 48 | this.mousePath.push(new PathPoint(mousePos.x, mousePos.y, this.video.currentTime)); 49 | this.lastMouseTime = 0; 50 | 51 | // Listen for mouse drag 52 | $("#clickArea").mousemove((event)=>{ 53 | if (event.which == 0) { 54 | // Stop dragging 55 | this.isDragging = false; 56 | $("#clickArea").unbind("mousemove"); 57 | } else { 58 | 59 | // Add the mouse position to the path, if the time has changed 60 | this.isDragging = true; 61 | var mousePos = this.video.clientToVideoCoord(event.pageX, event.pageY); 62 | var mouseTime = this.video.currentTime; 63 | 64 | if (this.lastMouseTime != mouseTime) { 65 | this.lastMouseTime = mouseTime; 66 | this.mousePath.push(new PathPoint(mousePos.x, mousePos.y, mouseTime)); 67 | this.updateMouseTrail(); 68 | } 69 | } 70 | }); 71 | } 72 | }) 73 | .mouseup((event)=>{ 74 | 75 | if(GLOBAL.playerMode()) { 76 | // Listen for mouseUp events 77 | var wasDragging = this.isDragging; 78 | this.isDragging = false; 79 | $("#clickArea").unbind("mousemove"); 80 | // if (wasDragging) { 81 | // Simplify the mouse trail 82 | this.mousePath.simplify(); 83 | 84 | // Calculate the % position clicked 85 | var mousePos = this.video.clientToVideoCoord(event.pageX, event.pageY); 86 | 87 | // Add the position to the mousePath 88 | this.mousePath.push(new PathPoint(mousePos.x, mousePos.y, this.video.currentTime)); 89 | 90 | // And go to the editor mode 91 | //gotoEditor(); 92 | if(this.events.onDrawingComplete) this.events.onDrawingComplete(this.mousePath); 93 | } 94 | }) 95 | 96 | $(window).mouseleave((event)=>{ 97 | if(GLOBAL.playerMode() && this.isDragging) { 98 | // Listen for mouseUp events 99 | var wasDragging = this.isDragging; 100 | this.isDragging = false; 101 | $("#clickArea").unbind("mousemove"); 102 | // if (wasDragging) { 103 | // Simplify the mouse trail 104 | this.mousePath.simplify(); 105 | 106 | // Calculate the % position clicked 107 | var mousePos = this.video.clientToVideoCoord(event.pageX, event.pageY); 108 | 109 | // Add the position to the mousePath 110 | this.mousePath.push(new PathPoint(mousePos.x, mousePos.y, this.video.currentTime)); 111 | 112 | // And go to the editor mode 113 | //gotoEditor(); 114 | if(this.events.onDrawingComplete) this.events.onDrawingComplete(this.mousePath); 115 | } 116 | }) 117 | } 118 | 119 | updateMouseTrail(){ 120 | 121 | if(!this.mousePolyline){ 122 | this.mousePolyline = this.drawing.polyline([]).fill('none').stroke({ width: 5, color: 'white', opacity: .5 }) 123 | } 124 | 125 | var c = $('#drawing') 126 | 127 | var scaleX = c.width(); 128 | var scaleY = c.height() 129 | 130 | 131 | var p = []; 132 | for(var i=0;i'); 189 | var noteText = $('
'); 190 | noteText.text(note.text); 191 | note.elm.append(noteText); 192 | $('#notes').append(note.elm); 193 | note.elm.attr('id', note.id); 194 | 195 | note.line = this.linedrawing.polyline([]).fill('none').stroke({ width: 2, color: 'rgba(0,0,0,0.5)' }) 196 | 197 | } 198 | 199 | 200 | this.updateNoteElm(note, p); 201 | } else if(note.path.last().time+100 < video.currentTime){ 202 | // Remove old notes 203 | if(note.elm) { 204 | this.removeNote(note); 205 | } 206 | notes.splice(i,1); 207 | i--; 208 | } else if(note.elm && note.path.first().time > video.currentTime){ 209 | // Hide notes not visible yet 210 | this.removeNote(note); 211 | } 212 | } 213 | } 214 | 215 | 216 | 217 | 218 | // Update a specific notes visual elements position 219 | updateNoteElm(note :Note, p:any){ 220 | if(p) { 221 | 222 | var pos = video.videoToClientCoord(p.x, p.y); 223 | // console.log(p,pos); 224 | if(pos != note.curPos) { 225 | if(!note.curPos){ 226 | note.curPos = pos; 227 | } 228 | var dirVec = {x: (pos.x - note.curPos.x), 229 | y: (pos.y - note.curPos.y)}; 230 | 231 | var length = Math.sqrt(dirVec.x*dirVec.x + dirVec.y*dirVec.y); 232 | if(length > 0) { 233 | var dirUnitVec = { 234 | x: dirVec.x / length, 235 | y: dirVec.y / length 236 | }; 237 | } else { 238 | var dirUnitVec = { 239 | x: 1.0, 240 | y: 0.0 241 | }; 242 | 243 | } 244 | var dist = 40; 245 | 246 | var goalDir = { 247 | x: dirVec.x - dirUnitVec.x * dist, 248 | y: dirVec.y - dirUnitVec.y * dist 249 | }; 250 | 251 | note.curPos.x += goalDir.x * 0.1; 252 | note.curPos.y += goalDir.y * 0.1; 253 | 254 | 255 | 256 | var offset = {x:-1, y:-1}; 257 | if(dirUnitVec.x > 0.1){ 258 | offset.x = -note.elm.children(".note-text").outerWidth()+1; 259 | } 260 | 261 | if(dirUnitVec.y > 0.5){ 262 | offset.y = -note.elm.children(".note-text").outerHeight()+1; 263 | } 264 | 265 | var playerSize = video.calculatePlayerSize(); 266 | 267 | if(note.curPos.y + offset.y > playerSize.height){ 268 | note.curPos.y = playerSize.height - offset.y; 269 | } 270 | if(note.curPos.y + offset.y < 0){ 271 | note.curPos.y = 0-offset.y; 272 | } 273 | 274 | if(note.curPos.x + offset.x > playerSize.width){ 275 | note.curPos.x = playerSize.width - offset.x; 276 | } 277 | if(note.curPos.x + offset.x < 0){ 278 | note.curPos.x = 0-offset.x; 279 | } 280 | //console.log(note.curPos.y); 281 | 282 | //console.log(offset); 283 | note.elm.css({ 284 | top: note.curPos.y + offset.y, 285 | left: note.curPos.x + offset.x 286 | }); 287 | 288 | /* console.log({ 289 | top: note.curPos.y, 290 | left: note.curPos.x 291 | })*/ 292 | 293 | var c = $('#drawing'); 294 | var scaleX = c.width(); 295 | var scaleY = c.height(); 296 | 297 | var p2 = video.clientToVideoCoord(note.curPos.x, note.curPos.y); 298 | 299 | 300 | note.line.plot([[ Math.floor(p2.x*scaleX) , Math.floor(p2.y*scaleY)], 301 | [ p.x*scaleX , p.y*scaleY]]) 302 | } 303 | } 304 | } 305 | 306 | 307 | removeNote(note : Note){ 308 | //console.log("Remove ", note); 309 | note.elm.remove(); 310 | note.line.plot([]); 311 | delete note.line; 312 | delete note.elm; 313 | } 314 | 315 | 316 | 317 | 318 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Exhausting A Crowd 2 | 3 | Inspired by the classic 60-page piece of experimental literature from Georges Perec, "An Attempt at Exhausting a Place in Paris", written from a bench over three days in 1974. "Exhausting a Crowd" will automate the task of completely describing the events of 12 hours in a busy public space. This work speaks to the potential of a perfectly automated future of surveillance, enabled by a distributed combination of machine and human intelligence. A beautiful record of the energy present in shared space, as well as a disturbing look into the potential for control in a dystopian environment. Commissioned by the V&A for "[All of This Belongs to You](http://www.vam.ac.uk/content/exhibitions/all-of-this-belongs-to-you/)". 4 | 5 | This piece builds on my previous work including "[People Staring at Computers](https://vimeo.com/25958231)" and "[keytweeter](https://vimeo.com/9922212)" which address the boundaries between public and private spaces, and the way computers mediate those spaces. Most similar is "[Conversnitch](https://twitter.com/conversnitch)" where I worked with [Brian House](https://twitter.com/h0use) to create a lamp that tweets overheard conversations in realtime using distributed human transcriptions. The potential for the hive mind to aide or disable investigations was made clear in the aftermath of the Boston marathon bombings, where [Reddit took to vigilantism](http://www.nytimes.com/2013/04/29/business/media/bombings-trip-up-reddit-in-its-turn-in-spotlight.html), trawling through thousands of images of the incident in an attempt to find the criminal, eventually settling on the wrong suspect. 6 | 7 | In the work of others, I was inspired by David Rokeby's "[Sorting Daemon](www.davidrokeby.com/sorting.html)", Rafael Lozano-Hemmer's "[Subtitled Public](http://www.lozano-hemmer.com/subtitled_public.php)", Standish Lawder's "[Necrology](https://www.youtube.com/watch?v=Dadi7mw5gCs)". Late in the process of the developing the piece, someone shared "[The Girl Chewing Gum](https://www.youtube.com/watch?v=57hJn-nkKSA)" by John Smith which became more confirmation than inspiration. Another interesting reference a friend sent was ["An Attempt at Exhausting and Augmented Place in Paris." Georges Perec, observer-writer of urban life, as a mobile locative media user](http://www.i-3.fr/wp-content/uploads/2015/05/WP-i3-SES-15-07-Licoppe.pdf) by Christian Licoppe. 8 | 9 | The primary location inspiring this piece was 14th Street Union Square in NYC, as viewed from the south side of the park. At any moment, there may be anywhere from 10 people at midnight, to 100 people on a cold afternoon, to 500 people at lunch or thousands for a protest. People are engaged in a variety of activities from playing chess, to dancing, singing, chanting, panhandling, eating, kissing, walking through, or just waiting. 10 | 11 | ![](https://igcdn-photos-d-a.akamaihd.net/hphotos-ak-xaf1/t51.2885-15/11378623_774063319380827_678750027_o.jpg) 12 | 13 | The decision to go with Piccadilly Circus was at the request of the V&A to consider shooting in London. After exploring public spaces on street view and Wikipedia, I eventually found [this picture on Flickr](https://www.flickr.com/photos/mrandrewmurray/2765228320/) by Andrew Murray: 14 | 15 | ![](https://farm4.staticflickr.com/3280/2765228320_764394bc57_b.jpg) 16 | 17 | After a lot of discussion with different businesses around Piccadilly Circus we eventually found Lillywhites (Sports Direct) was willing to let us shoot the piece. 18 | 19 | Two big decisions were made throughout the project, one was about whether to present a live stream or a pre-recorded stream, and the other was about whether to use computer-assisted tags or even computer-assisted targets based on pedestrian detection. The pre-recorded stream was essential to get the effect of an abundance of notes at any moment, and we tried to create the feeling of it being "live" by removing almost all user interface elements that suggested otherwise. The computer-assisted tags were dropped because it felt more disturbing to know that all the notes left behind were left there by a real human clicking and typing. 20 | 21 | With 4k footage there was some concern about privacy. Legally, there are no privacy restrictions on filming and broadcasting people in public spaces in the UK (with the exception of a few places like [The Royal Square, Trafalgar Square, the London Underground](http://filmlondon.org.uk/get-permission-film)). But this piece is about the crowd, not any specific individual, I wanted to avoid making any persons face clearly recognizable. In practice, almost all individuals appear at enough of a distance, and most internet connections cannot support the full 4k video bandwidth required to make out faces in the foreground. 22 | 23 | ## Technical details 24 | 25 | All footage was recorded over 12 hours at 4k 30fps on a GoPro Hero 4, modified with a [12mm lens](http://peauproductions.com/store/index.php?main_page=product_info&products_id=690), and two [Lexar 64GB High-speed MicroSD cards](http://www.bhphotovideo.com/c/product/1031506-REG/lexar_lsdmi64gbsbna633r_64gb_micro_sdhc_card.html) that were swapped every two hours while the GoPro ran off USB power. The GoPro outputs a sequence of short videos that are then stripped of audio and concatenated with `ffmpeg`. Before being concatenated the videos are copied to a temporary folder on the internal SSD which changes the processing time from days to minutes. Finally, all six videos (approximately two hours each) are uploaded to YouTube, which will accept up to 128GB or 11 hour videos after verification. All the videos are added to a playlist, and YouTube handles the streaming and buffering. 26 | 27 | ## Adding a new location 28 | 29 | * Upload video and create a playlist for it. 30 | * Add new location name to `index.js` (line 61) 31 | * Toggle comments between line 64 and line 65 in `index.js` to redirect visitors to the new location. 32 | * Add new location metadata to `typescript/frontend.ts` (line 9). 33 | * Add new location to `public/index.html` in the `public_sites` variable. 34 | * Create a new logo for the top right corner called `logo-site.png` where `site` is the name. It should be a white logo with transparent background in a PNG no more than 187px wide. 35 | * Add credits to `public/index.html`. 36 | * Run `tsc --outDir public/compiled/ typescript/*` to update TypeScript definitions. It will print an error, `not assignable to parameter of type 'PlayerOptions'.`, but it can be ignored. 37 | * Edit the `res.redirect` for the root directory in `index.js`. 38 | 39 | ## Software details 40 | 41 | The frontend is written with TypeScript, and is getting definitions with `tsd`. Install these with `npm install -g typescript@1.4 tsd@next` 42 | 43 | Then, to run locally, you will need to modify the code to attach to the remote database. 44 | 45 | ```sh 46 | $ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash 47 | $ nvm install 12.18.3 48 | $ npm install -g typescript@1.4 tsd@0.31.1 49 | $ npm install 50 | $ tsc --outDir public/compiled/ typescript/* 51 | $ npm start 52 | ``` 53 | 54 | The app should now be running on [localhost:5000](http://localhost:5000/). 55 | 56 | To make TypeScript automatically recompile changes to the .ts definitions, run `tsc -w --outDir public/compiled/ typescript/*`. 57 | 58 | If your computer is connected to AirPlay, the pause function is delayed (in order to sync the audio). 59 | 60 | ### Setting up on a fresh machine 61 | 62 | Create an Ubuntu 20.04 machine and [create a non-root user and login as that user](https://www.digitalocean.com/community/tutorials/initial-server-setup-with-ubuntu-20-04) and enable ufw. 63 | 64 | [Install PostgreSQL](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-20-04): 65 | 66 | ``` 67 | sudo apt install postgresql postgresql-contrib 68 | sudo systemctl start postgresql@12-main 69 | sudo -i -u postgres 70 | createuser --interactive 71 | # "kyle" and "y" 72 | # ctrl-c 73 | createdb exhausting 74 | pg_dump "$DATABASE_URL" > dump.psql 75 | psql exhausting < dump.psql 76 | ``` 77 | 78 | Pull the repo: 79 | 80 | ``` 81 | sudo apt update && sudo apt install -y git 82 | git clone https://github.com/kylemcdonald/ExhaustingACrowd.git 83 | ``` 84 | 85 | Install and prepare Node: 86 | 87 | ``` 88 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash 89 | nvm install 12.18.3 90 | npm install -g typescript@1.4 tsd@next 91 | cd ExhaustingACrowd 92 | npm install 93 | tsc --outDir public/compiled/ typescript/* 94 | npm start # make sure it's working 95 | ``` 96 | 97 | Keep running with pm2, and [pm2 on startup](https://pm2.keymetrics.io/docs/usage/startup/): 98 | 99 | ``` 100 | npm install pm2 -g 101 | pm2 start index.js 102 | sudo env PATH=$PATH:/home/kyle/.nvm/versions/node/v12.18.3/bin /home/kyle/.nvm/versions/node/v12.18.3/lib/node_modules/pm2/bin/pm2 startup systemd -u kyle --hp /home/kyle 103 | pm2 save 104 | ``` 105 | 106 | Install [nginx](https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-ubuntu-20-04): 107 | 108 | ``` 109 | sudo apt update 110 | sudo apt install nginx 111 | sudo ufw allow 'Nginx Full' 112 | sudo cp .nginx /etc/nginx/sites-available/exhaustingacrowd.com 113 | sudo ln -s /etc/nginx/sites-available/exhaustingacrowd.com /etc/nginx/sites-enabled/ 114 | sudo systemctl reload nginx 115 | ``` 116 | 117 | Setup the DNS A record and install and configure [Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-20-04): 118 | 119 | ``` 120 | sudo apt install certbot python3-certbot-nginx 121 | sudo certbot --nginx -d exhaustingacrowd.com -d www.exhaustingacrowd.com 122 | ``` 123 | 124 | ### Problems Building 125 | 126 | * 2021-8-30 `tsd reinstall` yields `ReferenceError: primordials is not defined` in `fs.js:36`. Googling yields info about gulp and graceful-fs incompatibilities, but not using gulp here. Instead looked into `tsd` version as culprit, uninstalled `@next` and looked at [version history](https://www.npmjs.com/package/tsd). Picked version 0.17.0 instead, from 6/3/2021, 3 months ago, instead of `@next` which is 0.6.0-beta.5 from six years ago and seems very uncommonly used. Get the error `The type definition index.d.ts does not exist. Create one and try again.` and instead try 0.13.1 from 7/4/2020. Same issue. Finally realized I only need to use `tsc` not `tsd reinstall` and 0.13.1 worked fine. 127 | * 2021-4-10 Currently on Node v12.18.3. After running `npm start` I saw the message `Error: Node Sass does not yet support your current environment: OS X 64-bit with Unsupported runtime`. Tried `npm rebuild node-sass`. Then saw the error `No Xcode or CLT version detected!`. Ran Xcode and let it install "additional components". Then ran `sudo xcode-select --reset` and ran `npm rebuild node-sass` again. Found [this explanation](https://github.com/nodejs/node-gyp/issues/1763) and tried upgrading `npm install node-sass@4.12.0 --save`. Seems like it didn't actually try upgrading. [Node support for node-sass](https://github.com/sass/node-sass#node-version-support-policy) says Node 12+ requires node-sass 4.12+. Tried adding `--force` flag. Installed the correct version, but `npm start` still failed. Switched to new machine, same Node version. Pulled repo and ran `npm install` from empty. `npm start` works. The original reason to try fixing this is that the notes weren't showing up anymore. But it works fine locally, which means there is a distinction between the Heroku and local versions. Turns out this can be fixed by setting the config variable `PGSSLMODE` to `require`. The hint was in the message `heroku "error: no pg_hba.conf entry for host"` and the side note: `"SSL off"`. 128 | 129 | ## Credits 130 | 131 | These are the credits for the piece as it was initially created for London: 132 | 133 | ``` 134 | EXHAUSTING A CROWD (2015) 135 | by KYLE MCDONALD 136 | with JONAS JONGEJAN 137 | 138 | COLLABORATION & SITE DEVELOPMENT / JONAS JONGEJAN 139 | COMMISSIONED by VICTORIA AND ALBERT MUSEUM for ALL OF THIS BELONGS TO YOU 140 | NICO TURNER / VIDEO 141 | SPECIAL THANKS to CORINNA GARDNER, DAN JOYCE, HELLICAR & LEWIS 142 | ``` 143 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Exhausting a Crowd 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 31 | 32 | 33 |
34 |
35 |
36 |
37 |
38 | 39 |
40 | 41 |
42 | 58 |
59 | 60 |
61 | 66 |
67 | 68 |
69 | 01:O9 PM 70 |
71 | 72 |
73 | 74 |
75 | 76 |
77 | 78 |
REWIND 10SEC
79 |
80 | 81 | 84 | 85 | 86 | 92 | 93 | 94 | 95 |
96 |
97 |

EXHAUSTING A CROWD

98 |

99 | 104 |

105 |

by KYLE MCDONALD

106 | 107 |

— CLICK & FOLLOW EVERYONE —

108 |
109 |
110 | 111 |
112 | 113 |
114 |
115 |

EXHAUSTING A CROWD

116 |

LONDON

117 |
2O15
118 |

by KYLE MCDONALD for ALL OF THIS BELONGS TO YOU

119 |

with JONAS JONGEJAN / COLLABORATION & SITE DEVELOPMENT

120 |

COMMISSIONED by VICTORIA AND ALBERT MUSEUM

121 |

NICO TURNER / VIDEO

122 |

SPECIAL THANKS to CORINNA GARDNER, DAN JOYCE, HELLICAR & LEWIS

123 |
on location at
124 |

LILLYWHITES, PICCADILLY CIRCUS from 3PM–3AM, MAY 17–18

125 |

INSPIRED by GEORGES PEREC

126 |
127 |
128 |

EXHAUSTING A CROWD

129 |

NETHERLANDS

130 |
2O15
131 |

by KYLE MCDONALD

132 |

with JONAS JONGEJAN / COLLABORATION & SITE DEVELOPMENT

133 |

CO-SPONSORED by BRAKKE GROND, IDFA DOCLAB, V2_ INSTITUTE

134 |

FIRST COMMISSIONED by VICTORIA AND ALBERT MUSEUM for ALL OF THIS BELONGS TO YOU

135 |

DAAN KUYS / VIDEO

136 |

SPECIAL THANKS to LARA COOMANS, CASPAR SONNEN, JAN MISKER, MICHEL VAN DARTEL

137 |
on location at
138 |

AMSTERDAM, ROTTERDAM, UTRECHT on NOVEMBER 13

139 |

INSPIRED by GEORGES PEREC

140 |
141 |
142 |

EXHAUSTING A CROWD

143 |

BIRMINGHAM, UK

144 |
2O17
145 |

by KYLE MCDONALD

146 |

with JONAS JONGEJAN / COLLABORATION & SITE DEVELOPMENT

147 |

COMMISSIONED by BIRMINGHAM OPEN MEDIA

148 |

FIRST COMMISSIONED by VICTORIA AND ALBERT MUSEUM for ALL OF THIS BELONGS TO YOU

149 |

CARL DAVIES / VIDEO

150 |

SPECIAL THANKS to LOUISE LATTER

151 |
on location at
152 |

VICTORIA SQUARE, BIRMINGHAM on AUGUST 21

153 |

INSPIRED by GEORGES PEREC

154 |
155 |
156 |

EXHAUSTING A CROWD

157 |

GWANGJU

158 |
2O17
159 |

by KYLE MCDONALD

160 |

with JONAS JONGEJAN / COLLABORATION & SITE DEVELOPMENT

161 |

COMMISSIONED by ACC

162 |

FIRST COMMISSIONED by VICTORIA AND ALBERT MUSEUM for ALL OF THIS BELONGS TO YOU

163 |

LEE KYOUNGHO, HEO JI EUN / VIDEO

164 |

SPECIAL THANKS to 阿部一直, HIBIKI MIZUNO, 장미현

165 |
on location at
166 |

GWANGJU STATION, ACC SQUARE and GWANGJU FOLLY on OCTOBER 13

167 |

INSPIRED by GEORGES PEREC

168 |
169 |
170 |

EXHAUSTING A CROWD

171 |

BEIJING

172 |
2019
173 |

by KYLE MCDONALD

174 |

with JONAS JONGEJAN / COLLABORATION & SITE DEVELOPMENT

175 |

COMMISSIONED by HYUNDAI MOTORSTUDIO

176 |

FIRST COMMISSIONED by VICTORIA AND ALBERT MUSEUM for ALL OF THIS BELONGS TO YOU

177 |

XIAOMIN SHEN / VIDEO

178 |

SPECIAL THANKS to LONG XINRU

179 |
on location at
180 |

751 DISTRICT, and SOLANA

on JUNE 30 and JULY 6

181 |

INSPIRED by GEORGES PEREC

182 |
183 |
184 |

EXHAUSTING A CROWD

185 |

SAINT-BRIEUC

186 |
2021
187 |

by KYLE MCDONALD

188 |

with JONAS JONGEJAN / COLLABORATION & SITE DEVELOPMENT

189 |

COMMISSIONED by FESTIVAL ART ROCK

190 |

FIRST COMMISSIONED by VICTORIA AND ALBERT MUSEUM for ALL OF THIS BELONGS TO YOU

191 |

SIMON GUYOMARD, SPOON PRODUCTIONS / VIDEO

192 |

THANKS to LA PASSERELLE, AMICALE LAÏQUE, PATRICK LE PERSON, MUSEUM OF SAINT-BRIEUC

193 |

SPECIAL THANKS to CAROL MEYER, ALICE BOINET

194 |
on location at
195 |

PL. DE LA RÉSISTANCE, PL. DU GUESCLIN, GARE DE SAINT-BRIEUC

on JULY 2 and AUGUST 22

196 |

INSPIRED by GEORGES PEREC

197 |
198 |
199 |

EXHAUSTING A CROWD

200 |

BERLIN

201 |
2022
202 |

by KYLE MCDONALD

203 |

with JONAS JONGEJAN / COLLABORATION & SITE DEVELOPMENT

204 |

COMMISSIONED by STAATSBIBLIOTHEK ZU BERLIN for UNHEIMLICH FANTASTISCH

205 |

FIRST COMMISSIONED by VICTORIA AND ALBERT MUSEUM for ALL OF THIS BELONGS TO YOU

206 |

OTTO STOCKMEIR, REFRAME VIDEOS / VIDEO

207 |

THANKS to STIFTUNG BRANDENBURGER TOR IM MAX LIEBERMANN HAUS

208 |

THANKS to UNION DER DEUTSCHEN AKADEMIEN DER WISSENSCHAFTEN

209 |

SPECIAL THANKS to STUDIO THEGREENEYL, KEIRA CHANG

210 |
on location at
211 |

BRANDENBURG TOR, GENDARMENMARKT, HKW SPREE

on AUGUST 9

212 |

INSPIRED by GEORGES PEREC

213 |
214 |
215 | 216 | 217 | 218 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 245 | 254 | 255 | 256 | 257 | 258 | -------------------------------------------------------------------------------- /typings/moment/moment.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for Moment.js 2.8.0 2 | // Project: https://github.com/timrwood/moment 3 | // Definitions by: Michael Lakerveld , Aaron King , Hiroki Horiuchi , Dick van den Brink , Adi Dahiya 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | 6 | declare module moment { 7 | 8 | interface MomentInput { 9 | 10 | years?: number; 11 | y?: number; 12 | 13 | months?: number; 14 | M?: number; 15 | 16 | weeks?: number; 17 | w?: number; 18 | 19 | days?: number; 20 | d?: number; 21 | 22 | hours?: number; 23 | h?: number; 24 | 25 | minutes?: number; 26 | m?: number; 27 | 28 | seconds?: number; 29 | s?: number; 30 | 31 | milliseconds?: number; 32 | ms?: number; 33 | 34 | } 35 | 36 | interface Duration { 37 | 38 | humanize(withSuffix?: boolean): string; 39 | 40 | as(units: string): number; 41 | 42 | milliseconds(): number; 43 | asMilliseconds(): number; 44 | 45 | seconds(): number; 46 | asSeconds(): number; 47 | 48 | minutes(): number; 49 | asMinutes(): number; 50 | 51 | hours(): number; 52 | asHours(): number; 53 | 54 | days(): number; 55 | asDays(): number; 56 | 57 | months(): number; 58 | asMonths(): number; 59 | 60 | years(): number; 61 | asYears(): number; 62 | 63 | add(n: number, p: string): Duration; 64 | add(n: number): Duration; 65 | add(d: Duration): Duration; 66 | 67 | subtract(n: number, p: string): Duration; 68 | subtract(n: number): Duration; 69 | subtract(d: Duration): Duration; 70 | 71 | toISOString(): string; 72 | 73 | } 74 | 75 | interface Moment { 76 | 77 | format(format: string): string; 78 | format(): string; 79 | 80 | fromNow(withoutSuffix?: boolean): string; 81 | 82 | startOf(unitOfTime: string): Moment; 83 | endOf(unitOfTime: string): Moment; 84 | 85 | /** 86 | * Mutates the original moment by adding time. (deprecated in 2.8.0) 87 | * 88 | * @param unitOfTime the unit of time you want to add (eg "years" / "hours" etc) 89 | * @param amount the amount you want to add 90 | */ 91 | add(unitOfTime: string, amount: number): Moment; 92 | /** 93 | * Mutates the original moment by adding time. 94 | * 95 | * @param amount the amount you want to add 96 | * @param unitOfTime the unit of time you want to add (eg "years" / "hours" etc) 97 | */ 98 | add(amount: number, unitOfTime: string): Moment; 99 | /** 100 | * Mutates the original moment by adding time. Note that the order of arguments can be flipped. 101 | * 102 | * @param amount the amount you want to add 103 | * @param unitOfTime the unit of time you want to add (eg "years" / "hours" etc) 104 | */ 105 | add(amount: string, unitOfTime: string): Moment; 106 | /** 107 | * Mutates the original moment by adding time. 108 | * 109 | * @param objectLiteral an object literal that describes multiple time units {days:7,months:1} 110 | */ 111 | add(objectLiteral: MomentInput): Moment; 112 | /** 113 | * Mutates the original moment by adding time. 114 | * 115 | * @param duration a length of time 116 | */ 117 | add(duration: Duration): Moment; 118 | 119 | /** 120 | * Mutates the original moment by subtracting time. (deprecated in 2.8.0) 121 | * 122 | * @param unitOfTime the unit of time you want to subtract (eg "years" / "hours" etc) 123 | * @param amount the amount you want to subtract 124 | */ 125 | subtract(unitOfTime: string, amount: number): Moment; 126 | /** 127 | * Mutates the original moment by subtracting time. 128 | * 129 | * @param unitOfTime the unit of time you want to subtract (eg "years" / "hours" etc) 130 | * @param amount the amount you want to subtract 131 | */ 132 | subtract(amount: number, unitOfTime: string): Moment; 133 | /** 134 | * Mutates the original moment by subtracting time. Note that the order of arguments can be flipped. 135 | * 136 | * @param amount the amount you want to add 137 | * @param unitOfTime the unit of time you want to subtract (eg "years" / "hours" etc) 138 | */ 139 | subtract(amount: string, unitOfTime: string): Moment; 140 | /** 141 | * Mutates the original moment by subtracting time. 142 | * 143 | * @param objectLiteral an object literal that describes multiple time units {days:7,months:1} 144 | */ 145 | subtract(objectLiteral: MomentInput): Moment; 146 | /** 147 | * Mutates the original moment by subtracting time. 148 | * 149 | * @param duration a length of time 150 | */ 151 | subtract(duration: Duration): Moment; 152 | 153 | calendar(): string; 154 | calendar(start: Moment): string; 155 | 156 | clone(): Moment; 157 | 158 | /** 159 | * @return Unix timestamp, or milliseconds since the epoch. 160 | */ 161 | valueOf(): number; 162 | 163 | local(): Moment; // current date/time in local mode 164 | 165 | utc(): Moment; // current date/time in UTC mode 166 | 167 | isValid(): boolean; 168 | 169 | year(y: number): Moment; 170 | year(): number; 171 | quarter(): number; 172 | quarter(q: number): Moment; 173 | month(M: number): Moment; 174 | month(M: string): Moment; 175 | month(): number; 176 | day(d: number): Moment; 177 | day(d: string): Moment; 178 | day(): number; 179 | date(d: number): Moment; 180 | date(): number; 181 | hour(h: number): Moment; 182 | hour(): number; 183 | hours(h: number): Moment; 184 | hours(): number; 185 | minute(m: number): Moment; 186 | minute(): number; 187 | minutes(m: number): Moment; 188 | minutes(): number; 189 | second(s: number): Moment; 190 | second(): number; 191 | seconds(s: number): Moment; 192 | seconds(): number; 193 | millisecond(ms: number): Moment; 194 | millisecond(): number; 195 | milliseconds(ms: number): Moment; 196 | milliseconds(): number; 197 | weekday(): number; 198 | weekday(d: number): Moment; 199 | isoWeekday(): number; 200 | isoWeekday(d: number): Moment; 201 | weekYear(): number; 202 | weekYear(d: number): Moment; 203 | isoWeekYear(): number; 204 | isoWeekYear(d: number): Moment; 205 | week(): number; 206 | week(d: number): Moment; 207 | weeks(): number; 208 | weeks(d: number): Moment; 209 | isoWeek(): number; 210 | isoWeek(d: number): Moment; 211 | isoWeeks(): number; 212 | isoWeeks(d: number): Moment; 213 | weeksInYear(): number; 214 | isoWeeksInYear(): number; 215 | dayOfYear(): number; 216 | dayOfYear(d: number): Moment; 217 | 218 | from(f: Moment): string; 219 | from(f: Moment, suffix: boolean): string; 220 | from(d: Date): string; 221 | from(s: string): string; 222 | from(date: number[]): string; 223 | 224 | diff(b: Moment): number; 225 | diff(b: Moment, unitOfTime: string): number; 226 | diff(b: Moment, unitOfTime: string, round: boolean): number; 227 | 228 | toArray(): number[]; 229 | toDate(): Date; 230 | toISOString(): string; 231 | toJSON(): string; 232 | unix(): number; 233 | 234 | isLeapYear(): boolean; 235 | zone(): number; 236 | zone(b: number): Moment; 237 | zone(b: string): Moment; 238 | utcOffset(): number; 239 | utcOffset(b: number): Moment; 240 | utcOffset(b: string): Moment; 241 | daysInMonth(): number; 242 | isDST(): boolean; 243 | 244 | isBefore(): boolean; 245 | isBefore(b: Moment): boolean; 246 | isBefore(b: string): boolean; 247 | isBefore(b: Number): boolean; 248 | isBefore(b: Date): boolean; 249 | isBefore(b: number[]): boolean; 250 | isBefore(b: Moment, granularity: string): boolean; 251 | isBefore(b: String, granularity: string): boolean; 252 | isBefore(b: Number, granularity: string): boolean; 253 | isBefore(b: Date, granularity: string): boolean; 254 | isBefore(b: number[], granularity: string): boolean; 255 | 256 | isAfter(): boolean; 257 | isAfter(b: Moment): boolean; 258 | isAfter(b: string): boolean; 259 | isAfter(b: Number): boolean; 260 | isAfter(b: Date): boolean; 261 | isAfter(b: number[]): boolean; 262 | isAfter(b: Moment, granularity: string): boolean; 263 | isAfter(b: String, granularity: string): boolean; 264 | isAfter(b: Number, granularity: string): boolean; 265 | isAfter(b: Date, granularity: string): boolean; 266 | isAfter(b: number[], granularity: string): boolean; 267 | 268 | isSame(b: Moment): boolean; 269 | isSame(b: string): boolean; 270 | isSame(b: Number): boolean; 271 | isSame(b: Date): boolean; 272 | isSame(b: number[]): boolean; 273 | isSame(b: Moment, granularity: string): boolean; 274 | isSame(b: String, granularity: string): boolean; 275 | isSame(b: Number, granularity: string): boolean; 276 | isSame(b: Date, granularity: string): boolean; 277 | isSame(b: number[], granularity: string): boolean; 278 | 279 | // Deprecated as of 2.8.0. 280 | lang(language: string): Moment; 281 | lang(reset: boolean): Moment; 282 | lang(): MomentLanguage; 283 | 284 | locale(language: string): Moment; 285 | locale(reset: boolean): Moment; 286 | locale(): string; 287 | 288 | localeData(language: string): Moment; 289 | localeData(reset: boolean): Moment; 290 | localeData(): MomentLanguage; 291 | 292 | // Deprecated as of 2.7.0. 293 | max(date: Date): Moment; 294 | max(date: number): Moment; 295 | max(date: any[]): Moment; 296 | max(date: string): Moment; 297 | max(date: string, format: string): Moment; 298 | max(clone: Moment): Moment; 299 | 300 | // Deprecated as of 2.7.0. 301 | min(date: Date): Moment; 302 | min(date: number): Moment; 303 | min(date: any[]): Moment; 304 | min(date: string): Moment; 305 | min(date: string, format: string): Moment; 306 | min(clone: Moment): Moment; 307 | 308 | get(unit: string): number; 309 | set(unit: string, value: number): Moment; 310 | 311 | } 312 | 313 | interface MomentCalendar { 314 | 315 | lastDay: any; 316 | sameDay: any; 317 | nextDay: any; 318 | lastWeek: any; 319 | nextWeek: any; 320 | sameElse: any; 321 | 322 | } 323 | 324 | interface BaseMomentLanguage { 325 | months ?: any; 326 | monthsShort ?: any; 327 | weekdays ?: any; 328 | weekdaysShort ?: any; 329 | weekdaysMin ?: any; 330 | relativeTime ?: MomentRelativeTime; 331 | meridiem ?: (hour: number, minute: number, isLowercase: boolean) => string; 332 | calendar ?: MomentCalendar; 333 | ordinal ?: (num: number) => string; 334 | } 335 | 336 | interface MomentLanguage extends BaseMomentLanguage { 337 | longDateFormat?: MomentLongDateFormat; 338 | } 339 | 340 | interface MomentLanguageData extends BaseMomentLanguage { 341 | /** 342 | * @param formatType should be L, LL, LLL, LLLL. 343 | */ 344 | longDateFormat(formatType: string): string; 345 | } 346 | 347 | interface MomentLongDateFormat { 348 | 349 | L: string; 350 | LL: string; 351 | LLL: string; 352 | LLLL: string; 353 | LT: string; 354 | l?: string; 355 | ll?: string; 356 | lll?: string; 357 | llll?: string; 358 | lt?: string; 359 | 360 | } 361 | 362 | interface MomentRelativeTime { 363 | 364 | future: any; 365 | past: any; 366 | s: any; 367 | m: any; 368 | mm: any; 369 | h: any; 370 | hh: any; 371 | d: any; 372 | dd: any; 373 | M: any; 374 | MM: any; 375 | y: any; 376 | yy: any; 377 | 378 | } 379 | 380 | interface MomentStatic { 381 | 382 | version: string; 383 | 384 | (): Moment; 385 | (date: number): Moment; 386 | (date: number[]): Moment; 387 | (date: string, format?: string, strict?: boolean): Moment; 388 | (date: string, format?: string, language?: string, strict?: boolean): Moment; 389 | (date: string, formats: string[], strict?: boolean): Moment; 390 | (date: string, formats: string[], language?: string, strict?: boolean): Moment; 391 | (date: string, specialFormat: () => void, strict?: boolean): Moment; 392 | (date: string, specialFormat: () => void, language?: string, strict?: boolean): Moment; 393 | (date: string, formatsIncludingSpecial: any[], strict?: boolean): Moment; 394 | (date: string, formatsIncludingSpecial: any[], language?: string, strict?: boolean): Moment; 395 | (date: Date): Moment; 396 | (date: Moment): Moment; 397 | (date: Object): Moment; 398 | 399 | utc(): Moment; 400 | utc(date: number): Moment; 401 | utc(date: number[]): Moment; 402 | utc(date: string, format?: string, strict?: boolean): Moment; 403 | utc(date: string, format?: string, language?: string, strict?: boolean): Moment; 404 | utc(date: string, formats: string[], strict?: boolean): Moment; 405 | utc(date: string, formats: string[], language?: string, strict?: boolean): Moment; 406 | utc(date: Date): Moment; 407 | utc(date: Moment): Moment; 408 | utc(date: Object): Moment; 409 | 410 | unix(timestamp: number): Moment; 411 | 412 | invalid(parsingFlags?: Object): Moment; 413 | isMoment(): boolean; 414 | isMoment(m: any): boolean; 415 | isDuration(): boolean; 416 | isDuration(d: any): boolean; 417 | 418 | // Deprecated in 2.8.0. 419 | lang(language?: string): string; 420 | lang(language?: string, definition?: MomentLanguage): string; 421 | 422 | locale(language?: string): string; 423 | locale(language?: string[]): string; 424 | locale(language?: string, definition?: MomentLanguage): string; 425 | 426 | localeData(language?: string): MomentLanguageData; 427 | 428 | longDateFormat: any; 429 | relativeTime: any; 430 | meridiem: (hour: number, minute: number, isLowercase: boolean) => string; 431 | calendar: any; 432 | ordinal: (num: number) => string; 433 | 434 | duration(milliseconds: Number): Duration; 435 | duration(num: Number, unitOfTime: string): Duration; 436 | duration(input: MomentInput): Duration; 437 | duration(object: any): Duration; 438 | duration(): Duration; 439 | 440 | parseZone(date: string): Moment; 441 | 442 | months(): string[]; 443 | months(index: number): string; 444 | months(format: string): string[]; 445 | months(format: string, index: number): string; 446 | monthsShort(): string[]; 447 | monthsShort(index: number): string; 448 | monthsShort(format: string): string[]; 449 | monthsShort(format: string, index: number): string; 450 | 451 | weekdays(): string[]; 452 | weekdays(index: number): string; 453 | weekdays(format: string): string[]; 454 | weekdays(format: string, index: number): string; 455 | weekdaysShort(): string[]; 456 | weekdaysShort(index: number): string; 457 | weekdaysShort(format: string): string[]; 458 | weekdaysShort(format: string, index: number): string; 459 | weekdaysMin(): string[]; 460 | weekdaysMin(index: number): string; 461 | weekdaysMin(format: string): string[]; 462 | weekdaysMin(format: string, index: number): string; 463 | 464 | min(moments: Moment[]): Moment; 465 | max(moments: Moment[]): Moment; 466 | 467 | normalizeUnits(unit: string): string; 468 | relativeTimeThreshold(threshold: string, limit: number): void; 469 | 470 | /** 471 | * Constant used to enable explicit ISO_8601 format parsing. 472 | */ 473 | ISO_8601(): void; 474 | 475 | } 476 | 477 | } 478 | 479 | declare var moment: moment.MomentStatic; 480 | 481 | declare module 'moment' { 482 | export = moment; 483 | } 484 | --------------------------------------------------------------------------------