Webdev workflow
This commit is contained in:
parent
1ea85dd490
commit
c5227d7f8c
24 changed files with 7315 additions and 180 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -3,4 +3,6 @@ venv
|
|||
.project
|
||||
.pydevproject
|
||||
venv3
|
||||
node_modules
|
||||
.sass-cache
|
||||
|
||||
|
|
18
README.md
Normal file
18
README.md
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Hugvey / Pillow Talk
|
||||
|
||||
## Server
|
||||
|
||||
Run the server: `python hugvey_server.py --config server_config.yml`
|
||||
|
||||
### Panopticon
|
||||
|
||||
The server also integrates the _panopticon_, the monitoring & administration interface to Hugvey.
|
||||
|
||||
|
||||
## Client
|
||||
|
||||
To run it: `python hugvey_client.py -c client_config.yml`
|
||||
|
||||
## Development
|
||||
|
||||
The Panopticon uses gulp to compile SASS into CSS, and to set up browser-sync for css & js. For now, no js user facing dependencies are managed trough node/npm.
|
|
@ -18,6 +18,7 @@ from hugvey.voice.google import GoogleVoiceClient
|
|||
from hugvey.voice.player import Player
|
||||
from hugvey.voice.streamer import AudioStreamer
|
||||
import queue
|
||||
import os
|
||||
|
||||
|
||||
logger = logging.getLogger("command")
|
||||
|
@ -67,7 +68,8 @@ class CentralCommand(object):
|
|||
self.languages = {}
|
||||
|
||||
for lang in self.config['languages']:
|
||||
with open(lang['file'], 'r') as fp:
|
||||
lang_filename = os.path.join(self.config['web']['files_dir'], lang['file'])
|
||||
with open(lang_filename, 'r') as fp:
|
||||
self.languages[lang['code']] = yaml.load(fp)
|
||||
|
||||
self.panopticon = Panopticon(self, self.config)
|
||||
|
|
|
@ -12,6 +12,7 @@ import os
|
|||
from pytz.reference import Central
|
||||
import asyncio
|
||||
import json
|
||||
from urllib.parse import urlparse
|
||||
|
||||
logger = logging.getLogger("panopticon")
|
||||
|
||||
|
@ -21,8 +22,15 @@ print(web_dir)
|
|||
|
||||
def getWebSocketHandler(central_command):
|
||||
class WebSocketHandler(tornado.websocket.WebSocketHandler):
|
||||
CORS_ORIGINS = ['localhost']
|
||||
connections = set()
|
||||
|
||||
def check_origin(self, origin):
|
||||
parsed_origin = urlparse(origin)
|
||||
# parsed_origin.netloc.lower() gives localhost:3333
|
||||
valid = parsed_origin.hostname in self.CORS_ORIGINS
|
||||
return valid
|
||||
|
||||
# the client connected
|
||||
def open(self):
|
||||
self.connections.add(self)
|
||||
|
@ -30,6 +38,7 @@ def getWebSocketHandler(central_command):
|
|||
|
||||
# the client sent the message
|
||||
def on_message(self, message):
|
||||
logger.debug(f"recieve: {message}")
|
||||
try:
|
||||
msg = json.loads(message)
|
||||
if msg['action'] == 'init':
|
||||
|
@ -48,6 +57,7 @@ def getWebSocketHandler(central_command):
|
|||
|
||||
def send(self, message):
|
||||
j = json.dumps(message)
|
||||
print(self.connections)
|
||||
[con.write_message(j) for con in self.connections]
|
||||
|
||||
# client disconnected
|
||||
|
@ -86,7 +96,7 @@ class Panopticon(object):
|
|||
self.config = config
|
||||
self.application = tornado.web.Application([
|
||||
(r"/ws", getWebSocketHandler(self.command)),
|
||||
(r"/uploads/(.*)", tornado.web.StaticFileHandler,
|
||||
(r"/local/(.*)", tornado.web.StaticFileHandler,
|
||||
{"path": config['web']['files_dir']}),
|
||||
(r"/(.*)", tornado.web.StaticFileHandler,
|
||||
{"path": web_dir, "default_filename": 'index.html'}),
|
||||
|
@ -100,7 +110,7 @@ class Panopticon(object):
|
|||
asyncio.set_event_loop(evt_loop)
|
||||
|
||||
self.loop = tornado.ioloop.IOLoop.current()
|
||||
logger.info(f"Start Panopticon on port {self.config['web']['port']}")
|
||||
logger.info(f"Start Panopticon on http://localhost:{self.config['web']['port']}")
|
||||
self.loop.start()
|
||||
|
||||
def stop(self):
|
||||
|
|
|
@ -421,7 +421,6 @@ class Story(object):
|
|||
async def start(self):
|
||||
logger.info("Starting story")
|
||||
self.timer.reset()
|
||||
# self.startTime = time.time()
|
||||
self.isRunning = True
|
||||
self.setCurrentMessage(self.startMessage)
|
||||
await self._renderer()
|
||||
|
|
|
@ -6,7 +6,7 @@ voice:
|
|||
out_rate: 44100
|
||||
port: 4444
|
||||
chunk: 2972
|
||||
google_credentials: "/home/ruben/Documents/Projecten/2018/Hugvey/test_googlespeech/My First Project-0c7833e0d5fa.json"
|
||||
google_credentials: "../test_googlespeech/My First Project-0c7833e0d5fa.json"
|
||||
hugveys: 25
|
||||
languages:
|
||||
- code: en-GB
|
||||
|
@ -17,4 +17,4 @@ languages:
|
|||
file: story_fr.json
|
||||
web:
|
||||
port: 8888
|
||||
files_dir: "/home/ruben/Documents/Projecten/2018/Hugvey/hugvey/local/"
|
||||
files_dir: "local/"
|
58
www/css/styles.css
Normal file
58
www/css/styles.css
Normal file
|
@ -0,0 +1,58 @@
|
|||
body {
|
||||
font-family: sans-serif;
|
||||
margin: 0; }
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
background: #333;
|
||||
padding: 5px;
|
||||
color: white;
|
||||
border-radius: 5px; }
|
||||
|
||||
#interface {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100vh;
|
||||
width: 100vw; }
|
||||
|
||||
#status {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
width: 430px;
|
||||
height: 100%;
|
||||
overflow-y: scroll; }
|
||||
#status > div {
|
||||
width: 33.3333333%;
|
||||
height: 150px;
|
||||
border: solid 1px;
|
||||
box-sizing: border-box;
|
||||
position: relative; }
|
||||
#status .hugvey {
|
||||
background-image: linear-gradient(to top, #587457, #35a589);
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center; }
|
||||
#status .hugvey h1 {
|
||||
text-align: center;
|
||||
margin: 0; }
|
||||
#status .hugvey.hugvey--on h1 {
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: 5px; }
|
||||
#status .hugvey.hugvey--off {
|
||||
background-image: linear-gradient(to top, #575d74, #3572a5); }
|
||||
#status .hugvey.hugvey--off h1::after {
|
||||
content: '[off]'; }
|
||||
|
||||
#story {
|
||||
position: relative; }
|
||||
#story #controls {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 5px; }
|
||||
#story svg#graph {
|
||||
width: 100%;
|
||||
height: 100%; }
|
1
www/css/styles.css.map
Normal file
1
www/css/styles.css.map
Normal file
|
@ -0,0 +1 @@
|
|||
{"version":3,"sourceRoot":"","sources":["styles.scss"],"names":[],"mappings":"AAAA;EACC;EACA;;;AAGD;EACC;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;;;AAED;EACC;EACG;EACA;EACA;EACA;;;AAGJ;EACC;EACA;EACA;EACA;EACA;;AAEA;EACC;;;AAKF;EACC;EACA;;;AAED;EACC;;;AAED;EACI;EACA;EACA","file":"styles.css"}
|
82
www/gulpfile.js
Normal file
82
www/gulpfile.js
Normal file
|
@ -0,0 +1,82 @@
|
|||
var gulp = require('gulp');
|
||||
var sass = require('gulp-sass');
|
||||
//var babel = require('gulp-babel');
|
||||
//var concat = require('gulp-concat');
|
||||
//var rename = require('gulp-rename');
|
||||
//var uglify = require('gulp-uglify');
|
||||
//var rollup = require('rollup-stream');
|
||||
//var sourcemaps = require('gulp-sourcemaps');
|
||||
var browserSync = require('browser-sync');
|
||||
|
||||
|
||||
var through = require('through2')
|
||||
// todo: rollup for d3 & possibly jsonld
|
||||
|
||||
var paths = {
|
||||
"styles": {
|
||||
"src": "./scss/*.scss",
|
||||
"dest": "./css/"
|
||||
}
|
||||
};
|
||||
|
||||
gulp.task('styles', function() {
|
||||
gulp.src(paths.styles.src, { sourcemaps: true })
|
||||
.pipe(sass().on('error', sass.logError))
|
||||
.pipe(gulp.dest(paths.styles.dest))
|
||||
.pipe(browserSync.reload({ stream: true }));
|
||||
});
|
||||
|
||||
|
||||
/*gulp.task('scripts', function() {
|
||||
return gulp.src(paths.scripts.src)
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(babel({
|
||||
ignore: [
|
||||
'./src/d3.v5.js',
|
||||
'./src/vue.js',
|
||||
]
|
||||
}))
|
||||
.pipe(concat('portfolio.js'))
|
||||
.pipe(gulp.dest(paths.scripts.dest)) // save .js
|
||||
.pipe(uglify())
|
||||
.pipe(rename({ extname: '.min.js' }))
|
||||
// .pipe(sourcemaps.write('maps'))
|
||||
.pipe(gulp.dest(paths.scripts.d3destDir)) // save .min.js
|
||||
});
|
||||
|
||||
gulp.task('d3', function() {
|
||||
return rollup( 'rollup.config.js' )
|
||||
.pipe(source('d3.bundle.js'))
|
||||
.pipe(gulp.dest(paths.scripts.d3destDir)) // save .js
|
||||
.pipe(buffer())
|
||||
.pipe(uglify())
|
||||
.pipe(rename({ extname: '.min.js' }))
|
||||
.pipe(sourcemaps.write('maps'))
|
||||
.pipe(gulp.dest(paths.scripts.d3destDir)) // save .min.js
|
||||
});
|
||||
*/
|
||||
|
||||
var watchStylesAndScripts = function() {
|
||||
gulp.watch(paths.styles.src,['styles']);
|
||||
// gulp.watch(paths.scripts.src,['scripts', browserSync.reload]);
|
||||
// gulp.watch(paths.scripts.d3src,['d3', browserSync.reload]);
|
||||
}
|
||||
|
||||
gulp.task('watch', watchStylesAndScripts);
|
||||
|
||||
// watch files for changes and reload
|
||||
gulp.task('serve', function() {
|
||||
browserSync.init({
|
||||
proxy: {
|
||||
target: "localhost:8888",
|
||||
ws: true
|
||||
},
|
||||
port: 3000
|
||||
});
|
||||
|
||||
gulp.watch(['index.html', 'js/hugvey_console.js'], browserSync.reload);
|
||||
watchStylesAndScripts();
|
||||
});
|
||||
|
||||
|
||||
gulp.task('default', ['serve']);
|
|
@ -1,100 +0,0 @@
|
|||
var panopticon;
|
||||
|
||||
class Panopticon {
|
||||
constructor() {
|
||||
console.log( "Init panopticon" );
|
||||
this.hugveys = new Vue( {
|
||||
el: "#status",
|
||||
data: {
|
||||
uptime: 0,
|
||||
languages: [],
|
||||
hugveys: []
|
||||
},
|
||||
methods: {
|
||||
time_passed: function (hugvey, property) {
|
||||
console.log("property!", Date(hugvey[property] * 1000));
|
||||
return moment(Date(hugvey[property] * 1000)).fromNow();
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
||||
|
||||
this.socket = new ReconnectingWebSocket( "ws://localhost:8888/ws", null, { debug: true, reconnectInterval: 3000 } );
|
||||
|
||||
|
||||
this.socket.addEventListener( 'open', ( e ) => {
|
||||
this.send( { action: 'init' } );
|
||||
} );
|
||||
|
||||
this.socket.addEventListener( 'close', function( e ) {
|
||||
console.log( 'Closed connection' );
|
||||
} );
|
||||
this.socket.addEventListener( 'message', ( e ) => {
|
||||
let msg = JSON.parse( e.data );
|
||||
if ( typeof msg['alert'] !== 'undefined' ) {
|
||||
alert(msg['alert']);
|
||||
}
|
||||
|
||||
if ( typeof msg['action'] === 'undefined' ) {
|
||||
console.error( "not a valid message: " + e.data );
|
||||
return;
|
||||
}
|
||||
|
||||
switch ( msg['action'] ) {
|
||||
|
||||
case 'status':
|
||||
this.hugveys.uptime = this.stringToHHMMSS(msg['uptime']);
|
||||
this.hugveys.languages = msg['languages'];
|
||||
this.hugveys.hugveys = msg['hugveys'];
|
||||
break;
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
send( msg ) {
|
||||
this.socket.send( JSON.stringify( msg ) );
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
// console.log('get status', this, panopticon);
|
||||
panopticon.send( { action: 'get_status' } );
|
||||
}
|
||||
|
||||
init() {
|
||||
setInterval( this.getStatus, 3000 );
|
||||
}
|
||||
|
||||
stringToHHMMSS (string) {
|
||||
var sec_num = parseInt(string, 10); // don't forget the second param
|
||||
var hours = Math.floor(sec_num / 3600);
|
||||
var minutes = Math.floor((sec_num - (hours * 3600)) / 60);
|
||||
var seconds = sec_num - (hours * 3600) - (minutes * 60);
|
||||
|
||||
if (hours < 10) {hours = "0"+hours;}
|
||||
if (minutes < 10) {minutes = "0"+minutes;}
|
||||
if (seconds < 10) {seconds = "0"+seconds;}
|
||||
return hours+':'+minutes+':'+seconds;
|
||||
}
|
||||
|
||||
|
||||
loadNarrative(code, file) {
|
||||
|
||||
}
|
||||
|
||||
resume(hv_id) {
|
||||
this.send({ action: 'resume', hugvey: hv_id })
|
||||
}
|
||||
pause(hv_id) {
|
||||
this.send({ action: 'play', hugvey: hv_id })
|
||||
}
|
||||
restart(hv_id) {
|
||||
this.send({ action: 'restart', hugvey: hv_id })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
window.addEventListener( 'load', function() {
|
||||
panopticon = new Panopticon();
|
||||
panopticon.init();
|
||||
})
|
|
@ -2,13 +2,16 @@
|
|||
<head>
|
||||
<title>Pillow Talk Control Interface</title>
|
||||
<!-- development version, includes helpful console warnings -->
|
||||
<script src="/vue.js"></script>
|
||||
<script src="/reconnecting-websocket.js"></script>
|
||||
<script src="/moment.min.js"></script>
|
||||
<link rel="stylesheet" href="styles.css"></link>
|
||||
<script src="/js/vue.js"></script>
|
||||
<script src="/js/reconnecting-websocket.js"></script>
|
||||
<script src="/js/moment.min.js"></script>
|
||||
<script src="/js/d3.v5.min.js"></script>
|
||||
<script src="/js/crel.min.js"></script>
|
||||
<link rel="stylesheet" href="/css/styles.css"></link>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div id="interface">
|
||||
<div id='status'>
|
||||
<div id='overview'>
|
||||
<dl>
|
||||
|
@ -16,7 +19,7 @@
|
|||
<dd>{{uptime}}</dd>
|
||||
<dt>Languages</dt>
|
||||
<dd v-for="lang in languages" :title="lang.file"
|
||||
@click="panopticon.loadNarrative(lang.code, lang.file)">{{lang.code}}</dd>
|
||||
class="btn lang--btn" @click="loadNarrative(lang.code, lang.file)">{{lang.code}}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class='hugvey' v-for="hv in hugveys"
|
||||
|
@ -27,17 +30,35 @@
|
|||
</h1>
|
||||
<div v-if="hv.status != 'off'">
|
||||
{{ hv.language }} / {{ hv.msg }}
|
||||
<div v-if="hv.finished != false">
|
||||
Finished: {{time_passed(hv, 'finished')}}
|
||||
<div v-if="hv.finished != false">Finished: {{time_passed(hv,
|
||||
'finished')}}</div>
|
||||
<div v-for="c, key in hv.counts">
|
||||
<dt>{{key}}</dt>
|
||||
<dd>{{c}}</dd>
|
||||
</div>
|
||||
<div v-for="c, key in hv.counts"><dt>{{key}}</dt><dd>{{c}}</dd></div>
|
||||
<div v-if="hv.status != 'running'" @click="panopticon.pause(hv.id)">Pause</div>
|
||||
<div v-if="hv.status != 'paused'" @click="panopticon.resume(hv.id)">Resume</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id='story'></div>
|
||||
<div id='hugvey'></div>
|
||||
<script type='application/javascript' src="/hugvey_console.js"></script>
|
||||
<div id='story'>
|
||||
<div id="controls">
|
||||
<div id="btn-save" class="btn">Save</div>
|
||||
<div id="btn-addMsg" class="btn">Create message</div>
|
||||
</div>
|
||||
<svg id='graph' viewbox="0 0 1280 1024"
|
||||
preserveAspectRatio="xMidYMid">
|
||||
<defs>
|
||||
<marker markerHeight="8" markerWidth="8" refY="0"
|
||||
refX="12" viewBox="0 -6 16 12" preserveAspectRatio="none"
|
||||
orient="auto" id="arrowHead">
|
||||
<path d="M0,-6L16,0L0,6" fill="black"></path></marker>
|
||||
</defs>
|
||||
<g id='container'>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<script type='application/javascript' src="/js/hugvey_console.js"></script>
|
||||
</body>
|
||||
</html>
|
7
www/js/crel.min.js
vendored
Normal file
7
www/js/crel.min.js
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
!function(n,e){"object"==typeof exports?module.exports=e():"function"==typeof define&&define.amd?define(e):n.crel=e()}(this,function(){function n(a){var d,s=arguments,p=s[1],y=2,m=s.length,x=n[o];if(a=n[u](a)?a:c.createElement(a),m>1){if((!e(p,r)||n[f](p)||Array.isArray(p))&&(--y,p=null),m-y==1&&e(s[y],"string"))a.textContent=s[y];else for(;y<m;++y)null!==(d=s[y])&&l(a,d);for(var v in p)if(x[v]){var A=x[v];e(A,t)?A(a,p[v]):a[i](A,p[v])}else e(p[v],t)?a[v]=p[v]:a[i](v,p[v])}return a}var e=function(n,e){return typeof n===e},t="function",r="object",i="setAttribute",o="attrMap",f="isNode",u="isElement",c=document,a=function(n){return n instanceof Node},d=function(n){return n instanceof Element},l=function(e,t){if(Array.isArray(t))return void t.map(function(n){l(e,n)});n[f](t)||(t=c.createTextNode(t)),e.appendChild(t)};return n[o]={},n[u]=d,n[f]=a,e(Proxy,"undefined")||(n.proxy=new Proxy(n,{get:function(e,t){return!(t in n)&&(n[t]=n.bind(null,t)),n[t]}})),n});
|
||||
|
||||
crel.attrMap['on'] = function(element, value) {
|
||||
for (var eventName in value) {
|
||||
element.addEventListener(eventName, value[eventName]);
|
||||
}
|
||||
};
|
2
www/js/d3.v5.min.js
vendored
Normal file
2
www/js/d3.v5.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
745
www/js/hugvey_console.js
Normal file
745
www/js/hugvey_console.js
Normal file
|
@ -0,0 +1,745 @@
|
|||
var panopticon;
|
||||
|
||||
class Panopticon {
|
||||
constructor() {
|
||||
console.log( "Init panopticon" );
|
||||
this.hugveys = new Vue( {
|
||||
el: "#status",
|
||||
data: {
|
||||
uptime: 0,
|
||||
languages: [],
|
||||
hugveys: []
|
||||
},
|
||||
methods: {
|
||||
time_passed: function (hugvey, property) {
|
||||
console.log("property!", Date(hugvey[property] * 1000));
|
||||
return moment(Date(hugvey[property] * 1000)).fromNow();
|
||||
},
|
||||
loadNarrative: function(code, file) {
|
||||
return panopticon.loadNarrative(code, file);
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
||||
|
||||
|
||||
this.socket = new ReconnectingWebSocket( "ws://localhost:8888/ws", null, { debug: true, reconnectInterval: 3000 } );
|
||||
this.graph = new Graph();
|
||||
|
||||
|
||||
this.socket.addEventListener( 'open', ( e ) => {
|
||||
this.send( { action: 'init' } );
|
||||
} );
|
||||
|
||||
this.socket.addEventListener( 'close', function( e ) {
|
||||
console.log( 'Closed connection' );
|
||||
} );
|
||||
this.socket.addEventListener( 'message', ( e ) => {
|
||||
let msg = JSON.parse( e.data );
|
||||
if ( typeof msg['alert'] !== 'undefined' ) {
|
||||
alert(msg['alert']);
|
||||
}
|
||||
|
||||
if ( typeof msg['action'] === 'undefined' ) {
|
||||
console.error( "not a valid message: " + e.data );
|
||||
return;
|
||||
}
|
||||
|
||||
switch ( msg['action'] ) {
|
||||
|
||||
case 'status':
|
||||
this.hugveys.uptime = this.stringToHHMMSS(msg['uptime']);
|
||||
this.hugveys.languages = msg['languages'];
|
||||
this.hugveys.hugveys = msg['hugveys'];
|
||||
break;
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
send( msg ) {
|
||||
if(this.socket.readyState == WebSocket.OPEN) {
|
||||
this.socket.send( JSON.stringify( msg ) );
|
||||
} else {
|
||||
console.error("Socket not open: ", this.socket.readyState);
|
||||
}
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
// console.log('get status', this, panopticon);
|
||||
panopticon.send( { action: 'get_status' } );
|
||||
}
|
||||
|
||||
init() {
|
||||
setInterval( this.getStatus, 3000 );
|
||||
}
|
||||
|
||||
stringToHHMMSS (string) {
|
||||
var sec_num = parseInt(string, 10); // don't forget the second param
|
||||
var hours = Math.floor(sec_num / 3600);
|
||||
var minutes = Math.floor((sec_num - (hours * 3600)) / 60);
|
||||
var seconds = sec_num - (hours * 3600) - (minutes * 60);
|
||||
|
||||
if (hours < 10) {hours = "0"+hours;}
|
||||
if (minutes < 10) {minutes = "0"+minutes;}
|
||||
if (seconds < 10) {seconds = "0"+seconds;}
|
||||
return hours+':'+minutes+':'+seconds;
|
||||
}
|
||||
|
||||
|
||||
loadNarrative(code, file) {
|
||||
let req = new XMLHttpRequest();
|
||||
let graph = this.graph;
|
||||
req.addEventListener("load", function(e){
|
||||
console.log('TEST',this);
|
||||
graph.loadData(JSON.parse(this.response));
|
||||
// console.log(, e);
|
||||
});
|
||||
req.open("GET", "/local/" + file);
|
||||
req.send();
|
||||
}
|
||||
|
||||
resume(hv_id) {
|
||||
this.send({ action: 'resume', hugvey: hv_id })
|
||||
}
|
||||
pause(hv_id) {
|
||||
this.send({ action: 'play', hugvey: hv_id })
|
||||
}
|
||||
restart(hv_id) {
|
||||
this.send({ action: 'restart', hugvey: hv_id })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
window.addEventListener( 'load', function() {
|
||||
panopticon = new Panopticon();
|
||||
panopticon.init();
|
||||
});
|
||||
|
||||
class Graph{
|
||||
constructor() {
|
||||
this.width = 1280;
|
||||
this.height = 1024;
|
||||
this.nodeSize = 80;
|
||||
this.maxChars = 16;
|
||||
this.svg = d3.select('#graph');
|
||||
this.container = d3.select('#container');
|
||||
this.selectedMsg = null;
|
||||
this.messages = []; // initialise empty array. For the simulation, make sure we keep the same array object
|
||||
this.directions = []; // initialise empty array. For the simulation, make sure we keep the same array object
|
||||
this.conditions = []; // initialise empty array. For the simulation, make sure we keep the same array object
|
||||
this.interruptions = []; // initialise empty array. For the simulation, make sure we keep the same array object
|
||||
|
||||
let graph = this;
|
||||
this.controlDown = false;
|
||||
document.addEventListener('keydown', function(e){
|
||||
console.log(e);
|
||||
if(e.which == "17") {
|
||||
graph.controlDown = true;
|
||||
document.body.classList.add('controlDown');
|
||||
}
|
||||
});
|
||||
document.addEventListener('keyup', function(e){
|
||||
console.log(e);
|
||||
if(e.which == "17") {
|
||||
graph.controlDown = false;
|
||||
document.body.classList.remove('controlDown');
|
||||
}
|
||||
});
|
||||
|
||||
let c = this.container;
|
||||
let zoomed = function(){
|
||||
c.attr("transform", d3.event.transform);
|
||||
}
|
||||
this.svg.call(d3.zoom()
|
||||
.scaleExtent([1 / 2, 8])
|
||||
.on("zoom", zoomed));
|
||||
|
||||
this.nodesG = this.container.append("g")
|
||||
.attr("id", "nodes")
|
||||
|
||||
this.linkG = this.container.append("g")
|
||||
.attr("id", "links");
|
||||
|
||||
document.getElementById('btn-save').addEventListener('click', function(e){ graph.saveJson(); });
|
||||
document.getElementById('btn-addMsg').addEventListener('click', function(e){ graph.createMsg(); });
|
||||
}
|
||||
|
||||
clickMsg(msg) {
|
||||
// event when a message is clicked.
|
||||
console.log(msg);
|
||||
|
||||
if(this.controlDown) {
|
||||
this.secondarySelectMsg(msg);
|
||||
} else {
|
||||
this.selectMsg(msg);
|
||||
}
|
||||
}
|
||||
|
||||
secondarySelectMsg(msg) {
|
||||
if(this.selectedMsg !== null) {
|
||||
this.addDirection(this.selectedMsg, msg);
|
||||
} else {
|
||||
console.error('No message selected as Source');
|
||||
}
|
||||
}
|
||||
|
||||
selectMsg(msg) {
|
||||
let selectedEls = document.getElementsByClassName('selectedMsg');
|
||||
while(selectedEls.length > 0){
|
||||
selectedEls[0].classList.remove('selectedMsg');
|
||||
}
|
||||
document.getElementById(msg['@id']).classList.add('selectedMsg');
|
||||
|
||||
this.selectedMsg = msg;
|
||||
|
||||
this.showMsg(msg);
|
||||
}
|
||||
|
||||
updateMsg() {
|
||||
// used eg. after a condition creation.
|
||||
this.showMsg(this.selectedMsg);
|
||||
}
|
||||
|
||||
showMsg(msg) {
|
||||
let msgEl = document.getElementById('msg');
|
||||
msgEl.innerHTML = "";
|
||||
let startAttributes = {
|
||||
'name': msg['@id'] + '-start',
|
||||
'disabled': true,
|
||||
'type': 'checkbox',
|
||||
'on': {
|
||||
'change': this.getEditEventListener()
|
||||
}
|
||||
}
|
||||
if(msg['start'] == true) {
|
||||
startAttributes['checked'] = 'checked';
|
||||
}
|
||||
let msgInfoEl = crel('div', {'class': 'msg__info'},
|
||||
crel('h1', {'class':'msg__id'}, msg['@id']),
|
||||
crel('label',
|
||||
crel('span', 'Text'),
|
||||
crel('input', {
|
||||
'name': msg['@id'] + '-text',
|
||||
'value': msg['text'],
|
||||
'on': {
|
||||
'change': this.getEditEventListener()
|
||||
}
|
||||
} )
|
||||
),
|
||||
crel('label',
|
||||
crel('span', 'Start'),
|
||||
crel('input', startAttributes)
|
||||
)
|
||||
);
|
||||
msgEl.appendChild(msgInfoEl);
|
||||
|
||||
// let directionHEl = document.createElement('h2');
|
||||
// directionHEl.innerHTML = "Directions";
|
||||
|
||||
let fromDirections =[] , toDirections = [];
|
||||
|
||||
for(let direction of this.getDirectionsTo(msg)) {
|
||||
toDirections.push(this.getDirectionEl(direction, msg));
|
||||
}
|
||||
|
||||
for(let direction of this.getDirectionsFrom(msg)) {
|
||||
fromDirections.push(this.getDirectionEl(direction, msg));
|
||||
}
|
||||
let directionsEl = crel('div', {'class': 'directions'},
|
||||
crel('h2', 'Directions'),
|
||||
...toDirections, ...fromDirections
|
||||
);
|
||||
msgEl.appendChild(directionsEl);
|
||||
|
||||
}
|
||||
|
||||
getDirectionEl(direction, msg) {
|
||||
let directionEl = document.createElement('div');
|
||||
if(direction['source'] == msg) {
|
||||
directionEl.innerHTML = `<h3>To ${direction['target']['@id']}</h3>`;
|
||||
} else {
|
||||
directionEl.innerHTML = `<h3>From ${direction['source']['@id']}</h3>`;
|
||||
}
|
||||
let del = document.createElement('div');
|
||||
del.innerHTML = "delete";
|
||||
del.classList.add("deleteBtn");
|
||||
let g = this;
|
||||
del.addEventListener('click', (e) => g.rmDirection(direction));
|
||||
directionEl.appendChild(del);
|
||||
|
||||
// TODO; conditions
|
||||
|
||||
for(let conditionId of direction['conditions']) {
|
||||
let condition = this.getNodeById(conditionId);
|
||||
directionEl.appendChild(this.getEditConditionFormEl(condition, direction));
|
||||
}
|
||||
|
||||
directionEl.appendChild(this.getAddConditionFormEl(direction));
|
||||
|
||||
return directionEl;
|
||||
}
|
||||
|
||||
getEditConditionFormEl(condition, direction) {
|
||||
let conditionEl = crel('div', {'class': 'condition condition--edit'},
|
||||
crel('h4', {'title': condition['@id']}, condition['type'])
|
||||
)
|
||||
let labelLabel = document.createElement('label');
|
||||
labelLabel.innerHTML = "Description";
|
||||
let labelInput = crel('input',{
|
||||
'name': `${condition['@id']}-label`,
|
||||
'value': typeof condition['label'] == 'undefined' ? "" : condition['label'],
|
||||
'on': {
|
||||
'change': this.getEditEventListener()
|
||||
}
|
||||
});
|
||||
labelLabel.appendChild(labelInput);
|
||||
conditionEl.appendChild(labelLabel);
|
||||
|
||||
for(let v in condition['vars']) {
|
||||
let varLabel = document.createElement('label');
|
||||
varLabel.innerHTML = v;
|
||||
let varInput = document.createElement('input');
|
||||
if(v == 'seconds') {
|
||||
varInput.type = 'number';
|
||||
}
|
||||
varInput.name = `${condition['@id']}-vars.${v}`;
|
||||
varInput.value = condition['vars'][v];
|
||||
varInput.addEventListener('change', this.getEditEventListener());
|
||||
varLabel.appendChild(varInput);
|
||||
conditionEl.appendChild(varLabel);
|
||||
}
|
||||
return conditionEl;
|
||||
}
|
||||
|
||||
getConditionTypes() {
|
||||
if(typeof this.conditionTypes === 'undefined') {
|
||||
// type: vars: attribtes for crel()
|
||||
this.conditionTypes = {
|
||||
'timeout': {
|
||||
'seconds': {'type': 'number', 'value': 10, 'min':0, 'step': 0.1}
|
||||
},
|
||||
'replyContains': {
|
||||
'regex': {'value': '.+'}
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.conditionTypes;
|
||||
}
|
||||
|
||||
fillConditionFormForType(conditionForm, type) {
|
||||
conditionForm.innerHTML = "";
|
||||
let vars = this.getConditionTypes()[type];
|
||||
for(let v in vars){
|
||||
let attr = vars[v];
|
||||
attr['name'] = v;
|
||||
conditionForm.appendChild(
|
||||
crel('label',
|
||||
crel('span', v),
|
||||
crel('input', attr)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getAddConditionFormEl(direction) {
|
||||
|
||||
let optionEls = [];
|
||||
let types = this.getConditionTypes();
|
||||
for(let type in types) {
|
||||
optionEls.push(crel('option', type));
|
||||
}
|
||||
|
||||
let conditionForm = crel('div', {'class': 'condition--vars'});
|
||||
let g = this;
|
||||
let addConditionEl = crel('div', {'class': 'condition condition--add'},
|
||||
crel('form', {
|
||||
'on': {
|
||||
'submit': function(e) {
|
||||
e.preventDefault();
|
||||
let form = new FormData(e.target);
|
||||
console.log('submit', form);
|
||||
let type = form.get('type');
|
||||
form.delete('type');
|
||||
let label = form.get('label');
|
||||
form.delete('label');
|
||||
let vars = {};
|
||||
for(var pair of form.entries()) {
|
||||
vars[pair[0]] = pair[1];
|
||||
}
|
||||
g.addConditionForDirection(type, label, vars, direction);
|
||||
}
|
||||
}
|
||||
},
|
||||
crel("h4", "Create New Condition"),
|
||||
crel("label",
|
||||
crel('span', "Type"),
|
||||
crel('select', {
|
||||
'name': 'type',
|
||||
'on': {
|
||||
'change': function(e){
|
||||
g.fillConditionFormForType(conditionForm, e.target.value);
|
||||
}
|
||||
}}, optionEls),
|
||||
),
|
||||
crel("label",
|
||||
crel('span', "Description"),
|
||||
crel('input', {'name': 'label'})
|
||||
),
|
||||
conditionForm,
|
||||
crel('input', {
|
||||
'type':'submit',
|
||||
'value':'create'
|
||||
})
|
||||
)
|
||||
);
|
||||
this.fillConditionFormForType(conditionForm, optionEls[0].value);
|
||||
|
||||
return addConditionEl;
|
||||
}
|
||||
|
||||
rmConditionFromDirection(condition, direction) {
|
||||
let id = condition['@id'];
|
||||
// TODO
|
||||
if(typeof direction != 'undefined') {
|
||||
|
||||
}
|
||||
this._rmNode(id);
|
||||
}
|
||||
|
||||
getConditionEl(condition) {
|
||||
let conditionEl = document.createElement('div');
|
||||
|
||||
return conditionEl;
|
||||
}
|
||||
|
||||
getDirectionsFrom(msg) {
|
||||
return this.directions.filter(d => d['source'] == msg);
|
||||
}
|
||||
|
||||
getDirectionsTo(msg) {
|
||||
return this.directions.filter(d => d['target'] == msg);
|
||||
}
|
||||
|
||||
|
||||
addMsg() {
|
||||
let msg = {
|
||||
"@id": "n" + Date.now().toString(36),
|
||||
"@type": "Msg",
|
||||
"text": "New",
|
||||
"start": false
|
||||
}
|
||||
this.data.push(msg);
|
||||
this.updateFromData();
|
||||
this.build();
|
||||
return msg;
|
||||
}
|
||||
|
||||
rmMsg(msg) {
|
||||
let invalidatedDirections = this.directions.filter(d => d['source'] == msg || d['target'] == msg);
|
||||
console.log('invalidated', invalidatedDirections);
|
||||
for(let dir of invalidatedDirections) {
|
||||
let i = this.data.indexOf(dir);
|
||||
this.data.splice(i, 1);
|
||||
}
|
||||
this._rmNode(msg);
|
||||
}
|
||||
|
||||
_rmNode(node) {
|
||||
// remove msg/direction/condition/etc
|
||||
let i = this.data.indexOf(node);
|
||||
this.data.splice(i, 1);
|
||||
this.updateFromData();
|
||||
this.build();
|
||||
return this.data;
|
||||
}
|
||||
|
||||
addConditionForDirection(type, label, vars, direction) {
|
||||
let con = this.addCondition(type, label, vars, true);
|
||||
direction['conditions'].push(con['@id']);
|
||||
this.updateFromData();
|
||||
this.build();
|
||||
|
||||
this.updateMsg();
|
||||
}
|
||||
|
||||
addCondition(type, label, vars, skip) {
|
||||
let con = {
|
||||
"@id": "c" + Date.now().toString(36),
|
||||
"@type": "Condition",
|
||||
"type": type,
|
||||
"label": label,
|
||||
"vars": vars
|
||||
}
|
||||
this.data.push(con);
|
||||
if(skip !== true) {
|
||||
this.updateFromData();
|
||||
this.build();
|
||||
}
|
||||
return con;
|
||||
}
|
||||
|
||||
addDirection(source, target) {
|
||||
let dir = {
|
||||
"@id": "d" + Date.now().toString(36),
|
||||
"@type": "Direction",
|
||||
"source": source,
|
||||
"target": target,
|
||||
"conditions": []
|
||||
}
|
||||
this.data.push(dir);
|
||||
this.updateFromData();
|
||||
this.build();
|
||||
return dir;
|
||||
}
|
||||
|
||||
rmDirection(dir) {
|
||||
this._rmNode(dir);
|
||||
}
|
||||
|
||||
createMsg() {
|
||||
this.addMsg();
|
||||
this.build();
|
||||
}
|
||||
|
||||
getNodeById(id) {
|
||||
return this.data.filter(node => node['@id'] == id)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Use wrapper method, because for event handlers 'this' will refer to
|
||||
* the input object
|
||||
*/
|
||||
getEditEventListener(){
|
||||
let graph = this;
|
||||
let el = function(e){
|
||||
let parts = e.srcElement.name.split('-');
|
||||
let id = parts[0], field = parts[1];
|
||||
console.log(this, graph);
|
||||
let node = graph.getNodeById(id);
|
||||
let path = field.split('.'); // use vars.test to set ['vars']['test'] = value
|
||||
var res=node;
|
||||
for (var i=0;i<path.length;i++){
|
||||
if(i == (path.length -1)) {
|
||||
console.log('last', path[i]);
|
||||
res[path[i]] = e.srcElement.value;
|
||||
} else {
|
||||
res=res[path[i]];
|
||||
}
|
||||
}
|
||||
|
||||
// node[field] = e.srcElement.value;
|
||||
|
||||
graph.build();
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
getJsonString() {
|
||||
// recreate array to have the right order of items.
|
||||
this.data = [...this.messages, ...this.conditions,
|
||||
...this.directions, ...this.interruptions]
|
||||
let d = [];
|
||||
let toRemove = ['sourceX', 'sourceY', 'targetX', 'targetY', 'x','y', 'vx','vy']
|
||||
for(let node of this.data) {
|
||||
let n = {};
|
||||
console.log(node['source']);
|
||||
for (let e in node) {
|
||||
if (node.hasOwnProperty(e) && toRemove.indexOf(e) == -1 ) {
|
||||
if(this.data.indexOf(node[e]) != -1) {
|
||||
n[e] = node[e]['@id'];
|
||||
} else {
|
||||
n[e] = node[e];
|
||||
}
|
||||
}
|
||||
}
|
||||
d.push(n);
|
||||
}
|
||||
return JSON.stringify(d);
|
||||
}
|
||||
|
||||
saveJson() {
|
||||
var blob = new Blob([this.getJsonString()], {type: 'application/json'});
|
||||
if(window.navigator.msSaveOrOpenBlob) {
|
||||
window.navigator.msSaveBlob(blob, "pillow_talk.json");
|
||||
}
|
||||
else{
|
||||
var elem = window.document.createElement('a');
|
||||
elem.href = window.URL.createObjectURL(blob);
|
||||
elem.download = "pillow_talk.json";
|
||||
document.body.appendChild(elem);
|
||||
elem.click();
|
||||
document.body.removeChild(elem);
|
||||
}
|
||||
}
|
||||
|
||||
loadData(data) {
|
||||
this.data = data;
|
||||
this.updateFromData();
|
||||
this.build(true);
|
||||
}
|
||||
|
||||
updateFromData() {
|
||||
this.messages = this.data.filter((node) => node['@type'] == 'Msg');
|
||||
this.directions = this.data.filter((node) => node['@type'] == 'Direction');
|
||||
this.conditions = this.data.filter((node) => node['@type'] == 'Condition');
|
||||
this.interruptions = this.data.filter((node) => node['@type'] == 'Interruption');
|
||||
|
||||
// save state;
|
||||
this.saveState();
|
||||
}
|
||||
|
||||
saveState() {
|
||||
window.localStorage.setItem("lastState", this.getJsonString());
|
||||
}
|
||||
|
||||
hasSavedState() {
|
||||
return window.localStorage.getItem("lastState") !== null;
|
||||
}
|
||||
|
||||
loadFromState() {
|
||||
this.loadData(JSON.parse(window.localStorage.getItem("lastState")));
|
||||
}
|
||||
|
||||
build(isInit) {
|
||||
this.simulation = d3.forceSimulation(this.messages)
|
||||
.force("link", d3.forceLink(this.directions).id(d => d['@id']))
|
||||
.force("charge", d3.forceManyBody().strength(-1000))
|
||||
.force("center", d3.forceCenter(this.width / 2, this.height / 2))
|
||||
.force("collide", d3.forceCollide(this.nodeSize*2))
|
||||
;
|
||||
|
||||
// Update existing nodes
|
||||
let node = this.nodesG
|
||||
.selectAll("g")
|
||||
.data(this.messages, n => n['@id'])
|
||||
;
|
||||
|
||||
// Update existing nodes
|
||||
let newNode = node.enter();
|
||||
|
||||
let newNodeG = newNode.append("g")
|
||||
.attr('id', d => d['@id'])
|
||||
.call(d3.drag(this.simulation))
|
||||
.on('click', function(d){
|
||||
this.clickMsg(d);
|
||||
}.bind(this))
|
||||
;
|
||||
console.log('a');
|
||||
let circle = newNodeG.append("circle")
|
||||
.attr('r', this.nodeSize)
|
||||
// .text(d => d.id)
|
||||
;
|
||||
let text = newNodeG.append("text")
|
||||
;
|
||||
|
||||
// remove
|
||||
node.exit().remove();
|
||||
node = node.merge(newNodeG);
|
||||
|
||||
// for all existing nodes:
|
||||
node.attr('class', msg => {
|
||||
let classes = [];
|
||||
if( this.selectedMsg == msg) classes.push('selectedMsg');
|
||||
if( msg['start'] == true ) classes.push('startMsg');
|
||||
if(this.getDirectionsFrom(msg).length < 1) {
|
||||
classes.push('endMsg');
|
||||
if(this.getDirectionsTo(msg).length < 1) classes.push('orphanedMsg');
|
||||
}
|
||||
|
||||
return classes.join(' ');
|
||||
})
|
||||
|
||||
let link = this.linkG
|
||||
.selectAll("line")
|
||||
.data(this.directions)
|
||||
;
|
||||
let newLink = link.enter()
|
||||
.append("line")
|
||||
;
|
||||
//remove
|
||||
link.exit().remove();
|
||||
link = link.merge(newLink);
|
||||
|
||||
link.attr('class', l => { return `link ` + (l['conditions'].length == 0 ? "link--noconditions" : "link--withconditions"); });
|
||||
|
||||
// console.log('c');
|
||||
let formatText = (t) => {
|
||||
if(t.length > this.maxChars) {
|
||||
return t.substr(0, this.maxChars - 3) + '...';
|
||||
} else {
|
||||
return t;
|
||||
}
|
||||
};
|
||||
|
||||
node.selectAll("text").text(d => formatText(`(${d['@id']}) ${d['text']}`));
|
||||
// console.log('q');
|
||||
// // TODO: update text
|
||||
// let text = newNodeG.append("text")
|
||||
// // .attr('stroke', "black")
|
||||
// .text(d => formatText(`(${d['@id']}) ${d['text']}`))
|
||||
// // .attr('title', d => d.label)
|
||||
// ;
|
||||
|
||||
let n = this.nodesG;
|
||||
this.simulation.on("tick", () => {
|
||||
link
|
||||
.each(function(d){
|
||||
let sourceX, targetX, midX, dx, dy, angle;
|
||||
// This mess makes the arrows exactly perfect.
|
||||
// thanks to http://bl.ocks.org/curran/9b73eb564c1c8a3d8f3ab207de364bf4
|
||||
if( d.source.x < d.target.x ){
|
||||
sourceX = d.source.x;
|
||||
targetX = d.target.x;
|
||||
} else if( d.target.x < d.source.x ){
|
||||
targetX = d.target.x;
|
||||
sourceX = d.source.x;
|
||||
} else if (d.target.isCircle) {
|
||||
targetX = sourceX = d.target.x;
|
||||
} else if (d.source.isCircle) {
|
||||
targetX = sourceX = d.source.x;
|
||||
} else {
|
||||
midX = (d.source.x + d.target.x) / 2;
|
||||
if(midX > d.target.x){
|
||||
midX = d.target.x;
|
||||
} else if(midX > d.source.x){
|
||||
midX = d.source.x;
|
||||
} else if(midX < d.target.x){
|
||||
midX = d.target.x;
|
||||
} else if(midX < d.source.x){
|
||||
midX = d.source.x;
|
||||
}
|
||||
targetX = sourceX = midX;
|
||||
}
|
||||
|
||||
dx = targetX - sourceX;
|
||||
dy = d.target.y - d.source.y;
|
||||
angle = Math.atan2(dx, dy);
|
||||
|
||||
// Compute the line endpoint such that the arrow
|
||||
// is touching the edge of the node rectangle perfectly.
|
||||
d.sourceX = sourceX + Math.sin(angle) * this.nodeSize;
|
||||
d.targetX = targetX - Math.sin(angle) * this.nodeSize;
|
||||
d.sourceY = d.source.y + Math.cos(angle) * this.nodeSize;
|
||||
d.targetY = d.target.y - Math.cos(angle) * this.nodeSize;
|
||||
}.bind(this))
|
||||
.attr("x1", function(d) { return d.sourceX; })
|
||||
.attr("y1", function(d) { return d.sourceY; })
|
||||
.attr("x2", function(d) { return d.targetX; })
|
||||
.attr("y2", function(d) { return d.targetY; });
|
||||
|
||||
node.attr("transform", d => `translate(${d.x},${d.y})`);
|
||||
// .attr("cy", d => d.y);
|
||||
});
|
||||
|
||||
// this.simulation.alpha(1);
|
||||
// this.simulation.restart();
|
||||
if(typeof isInit != 'undefined' && isInit) {
|
||||
for (let i = 0, n = Math.ceil(Math.log(this.simulation.alphaMin()) / Math.log(1 - this.simulation.alphaDecay())); i < n; ++i) {
|
||||
this.simulation.tick();
|
||||
}
|
||||
}
|
||||
return this.svg.node();
|
||||
}
|
||||
}
|
||||
|
0
www/moment.min.js → www/js/moment.min.js
vendored
0
www/moment.min.js → www/js/moment.min.js
vendored
6230
www/package-lock.json
generated
Normal file
6230
www/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
22
www/package.json
Normal file
22
www/package.json
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "Hugvey_Panopticon",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.html",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "Ruben van de Ven",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"browser-sync": "^2.24.6",
|
||||
"gulp": "^3.9.1",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-rename": "^1.4.0",
|
||||
"gulp-sass": "^4.0.1",
|
||||
"gulp-sourcemaps": "^2.6.4",
|
||||
"gulp-uglify": "^3.0.1",
|
||||
"rollup-stream": "^1.24.1"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
80
www/scss/styles.scss
Normal file
80
www/scss/styles.scss
Normal file
|
@ -0,0 +1,80 @@
|
|||
body{
|
||||
font-family: sans-serif;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.btn{
|
||||
display:inline-block;
|
||||
cursor: pointer;
|
||||
background: #333;
|
||||
padding: 5px;
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#interface{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
#status{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
width: 430px;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
|
||||
& > div{
|
||||
width: 33.3333333%;
|
||||
height: 150px;
|
||||
border: solid 1px;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hugvey{
|
||||
background-image: linear-gradient(to top, #587457, #35a589);
|
||||
color:white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
h1{
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&.hugvey--on{
|
||||
h1 {
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&.hugvey--off{
|
||||
background-image: linear-gradient(to top, #575d74, #3572a5);
|
||||
h1::after{
|
||||
content: '[off]'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#story{
|
||||
position: relative;
|
||||
|
||||
#controls{
|
||||
position:absolute;
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
}
|
||||
svg#graph{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
body{
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
#status{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
width: 430px;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
#status > div{
|
||||
width: 33.3333333%;
|
||||
height: 150px;
|
||||
border: solid 1px;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hugvey{
|
||||
background-image: linear-gradient(to top, #587457, #35a589);
|
||||
color:white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
.hugvey.hugvey--off{
|
||||
background-image: linear-gradient(to top, #575d74, #3572a5);
|
||||
}
|
||||
|
||||
.hugvey h1{
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
.hugvey.hugvey--off h1::after{
|
||||
content: '[off]'
|
||||
}
|
||||
.hugvey.hugvey--on h1 {
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: 5px;
|
||||
|
||||
}
|
Loading…
Reference in a new issue