├── 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 |
21 | We are really sorry, but mobile browsers are not supported.
22 |
Please use Chrome or Safari on a desktop computer instead.
23 |
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 | 
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 | 
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 |
38 |
39 |
40 |
41 |
59 |
60 |
67 |
68 |
69 | 01:O9 PM
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |

78 |
REWIND 10SEC
79 |
80 |
81 |
82 |

83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
TAKE NOTE
91 |
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 |
--------------------------------------------------------------------------------