commit ab868905b34658bc17971f895bafdfb7655f9db9 Author: Sam Date: Sun Oct 25 20:18:56 2020 +0100 Post sonoptik commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/clitools/README.md b/clitools/README.md new file mode 100644 index 0000000..b69e4a5 --- /dev/null +++ b/clitools/README.md @@ -0,0 +1,70 @@ +# Chaining Lasers In Submission Tools for LJ + +Alright everybody, ready for some fun? Here comes The Piping And Plumbing Your Way To The Top Show! + +You're going to push so many points to this laser it will hog and cry... + +BOOM | WIIIIIZ :: PHHHHHRACKRACKRACK ~~ WOOP ~~###~~ WIIT + + +## The basic loop +``` +python3 generators/dummy.py -f 2 | python3 filters/kaleidoscope.py | python3 exports/toRedis.py -v + ------------------------------ --------------------- ------------------- + \/ \/ \/ + Generator Filter Export +``` + +### 1. The Generator + +Use it to produce some points in any manner, orderly or total chaos. + +Don't be that boring Sinusoids bugger! Flash Maps of Dooms, Disbitnic sprites, Dismorphic HexaFonts all over the walls! + +### 2. The Filter(s) + +These babies will modify data on the wire by passing around the points and modifying them in sequence. + +Want your Double Heavy Laser Cannons to Bounce Together Like They Been Drinking Jagerbombs For Two Hours? That's the place. + +### 3. The Exporter + +Now, this IS the most boring part. Send your points to whatever output system. Yawn. Are we there yet? + +## Hacking around + +Say what!? Why, this is exactly the place for that! + +Take a seat and copy paste the "dummy.py" files, they present the basic structure you need to play around. + +Just be cautious to use the `debug` method if you're the kind of miss that debugs by outputing data structures (who does not, yah know, sometimes?). Or you'll break the chain. + +### Generators + +They must send list of points to standard out. Don't forget the "flush" argument, or the piping will be breaking, ain't no Mario lazering. + +* dummy.py : sends always the same list of points. The Monomaniac. +* @todo : read texts from redis and others + +### Filters + +These do listen and read on STDIN and do the same print'n'flush on STDOUT. + +* kaleidoscope.py : mirrors the points based on a pivot +* @todo : fourier analysis and other realtime reaction + +### Export + +Read from STDIN and send to redis mostly + +* toRedis.py : provide a key, server IP, etc. + +### Common parameters + +Every command can be called with a `-h/--help` flag to get some help + +Every command has a `-v/--verbose` flag to send debug info to STDERR. + +Generators have a `-f/--fps` param for FPS, to be fast but not so furious on your machine + +Filters and Exports are their own beasts diff --git a/clitools/__pycache__/runner_lib.cpython-38.pyc b/clitools/__pycache__/runner_lib.cpython-38.pyc new file mode 100644 index 0000000..aa536fd Binary files /dev/null and b/clitools/__pycache__/runner_lib.cpython-38.pyc differ diff --git a/clitools/_run.sh b/clitools/_run.sh new file mode 100755 index 0000000..96014a0 --- /dev/null +++ b/clitools/_run.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +killexit(){ + pkill -9 -s $$ + } + +trap killexit SIGTERM SIGINT SIGKILL SIGSTOP + +bash -c "$@" + +killbill(){ pkill -9 -s $(awk '{print $6}' /proc/$1/stat) } diff --git a/clitools/exports/.DS_Store b/clitools/exports/.DS_Store new file mode 100644 index 0000000..397ae06 Binary files /dev/null and b/clitools/exports/.DS_Store differ diff --git a/clitools/exports/__pycache__/websocket_server.cpython-38.pyc b/clitools/exports/__pycache__/websocket_server.cpython-38.pyc new file mode 100644 index 0000000..614f7e9 Binary files /dev/null and b/clitools/exports/__pycache__/websocket_server.cpython-38.pyc differ diff --git a/clitools/exports/__pycache__/websocket_server.cpython-38.sync-conflict-20201018-144618-NJKFP4Q.pyc b/clitools/exports/__pycache__/websocket_server.cpython-38.sync-conflict-20201018-144618-NJKFP4Q.pyc new file mode 100644 index 0000000..5a8b6a7 Binary files /dev/null and b/clitools/exports/__pycache__/websocket_server.cpython-38.sync-conflict-20201018-144618-NJKFP4Q.pyc differ diff --git a/clitools/exports/toNull.py b/clitools/exports/toNull.py new file mode 100755 index 0000000..08b4dfc --- /dev/null +++ b/clitools/exports/toNull.py @@ -0,0 +1,46 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + + +''' + +The exporter that drops all traffic ! +v0.1.0 + +A basic exporter + +LICENCE : CC + +by cocoa + + +''' +from __future__ import print_function +import sys +import os +import argparse +import redis +import time + +argsparser = argparse.ArgumentParser(description="Null exporter LJ") +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose") +args = argsparser.parse_args() + +verbose=args.verbose + +name = "exports::toNull" +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + +try: + while True: + line = sys.stdin.readline() + if line == "": + time.sleep(0.01) + debug(name,"dumping: "+line) +except EOFError: + debug("break")# no more information + diff --git a/clitools/exports/toRedis.py b/clitools/exports/toRedis.py new file mode 100644 index 0000000..36381b8 --- /dev/null +++ b/clitools/exports/toRedis.py @@ -0,0 +1,63 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + + +''' + +redis exporter +v0.1.0 + +A basic exporter + +LICENCE : CC + +by cocoa + + +''' +from __future__ import print_function +import sys +import os +import argparse +import redis +import time + +argsparser = argparse.ArgumentParser(description="Redis exporter LJ") +argsparser.add_argument("-i","--ip",help="IP address of the Redis server ",default="127.0.0.1",type=str) +argsparser.add_argument("-p","--port",help="Port of the Redis server ",default="6379",type=str) +argsparser.add_argument("-k","--key",help="Redis key to update",default="0",type=str) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose") +args = argsparser.parse_args() + +ip = args.ip +port = args.port +key = args.key +verbose=args.verbose + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + +r=redis.StrictRedis(host=ip, port=port, db=0) + +try: + while True: + line = sys.stdin.readline() + if line == "": + time.sleep(0.01) + continue + line = line.rstrip('\n') + line=line[1:-1] + line = line.replace("[",'(') + line = line.replace("]",')') + line = "[{}]".format(line) + if line == "[]": + line="[(400.0,400.0,0),(400.0,400.0,0),(400.0,400.0,0),(400.0,400.0,0)]" + continue + if r.set(key,line)==True: + debug("exports::redis set("+str(key)+") to "+line) +except EOFError: + debug("break")# no more information + diff --git a/clitools/exports/toUDP.py b/clitools/exports/toUDP.py new file mode 100644 index 0000000..64787db --- /dev/null +++ b/clitools/exports/toUDP.py @@ -0,0 +1,84 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + + +''' + +toUDP +v0.1.0 + +A basic exporter + +LICENCE : CC + +by cocoa + + +''' +from __future__ import print_function +import sys +import os +import argparse +import time +import socket +import ast + +argsparser = argparse.ArgumentParser(description="toUDP v0.1b help mode") +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output") +argsparser.add_argument("-i","--ip",help="IP to bind to (0.0.0.0 by default)",default="0.0.0.0",type=str) +argsparser.add_argument("-p","--port",help="UDP port to bind to (9000 by default)",default="9003",type=str) +args = argsparser.parse_args() + +verbose = args.verbose +ip = args.ip +port = int(args.port) +verbose = args.verbose + + +name = "exports::toUDP" + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + +def ClientStart(ip, port): + global sockclient + + sockclient = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM) + +def ClientSend(msgFromClient): + + bytesToSend = str.encode(str(msgFromClient)) + serverAddressPort = (ip, port) + bufferSize = 1024 + + # Send to server using created UDP socket + sockclient.sendto(bytesToSend, serverAddressPort) + + ''' + # If reply : + msgFromServer = sockclient.recvfrom(bufferSize) + + msg = "Message from Server {}".format(msgFromServer[0]) + print(msg) + ''' + +try: + + ClientStart(ip, port) + while True: + + line = sys.stdin.readline() + if line == "": + time.sleep(0.01) + line = line.rstrip('\n') + #pointsList = ast.literal_eval(line) + debug(name,": "+line) + ClientSend(line) + + +except EOFError: + debug("break")# no more information + diff --git a/clitools/exports/toWS.py b/clitools/exports/toWS.py new file mode 100644 index 0000000..1af33ed --- /dev/null +++ b/clitools/exports/toWS.py @@ -0,0 +1,115 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + +''' +toWS +exporter to LJ via WebSocket +v0.1b + +''' +from __future__ import print_function +import websocket +import time +import argparse +import traceback +import sys + +try: + import thread +except ImportError: + import _thread as thread + + +print("") +print("toWS v0.1b") +print ("Arguments parsing if needed...") +argsparser = argparse.ArgumentParser(description="toWS v0.1b help mode") +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output") +argsparser.add_argument("-s","--server",help="WS server IP (127.0.0.1 by default)", type=str) +argsparser.add_argument("-p","--port",help="WS port to bind to (9001 by default)", type=str) +argsparser.add_argument("-k","--key",help="Redis key to update",default="0",type=str) +args = argsparser.parse_args() + +key = args.key +verbose=args.verbose + +name = "exports::toWS" + + + +# Server name +if args.server: + serverIP = args.server +else: + serverIP = "127.0.0.1" + +# ORCA destination device +if args.port: + wsPORT = args.port +else: + wsPORT = 9001 + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + + +def GetTime(): + return time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()) + + +def on_message(ws, message): + debug(message) + +def on_error(ws, error): + debug(error) + +def on_close(ws): + debug("### closed ###") + +def on_open(ws): + + def run(*args): + + try: + while True: + line = sys.stdin.readline() + if line == "": + time.sleep(0.01) + line = line.rstrip('\n') + line=line[1:-1] + line = line.replace("[",'(') + line = line.replace("]",')') + #debug(line) + line = "[{}]".format(line) + ws.send(str(key)+' "'+line+'"') + debug("exports::ws "+str(key)+" "+line) + + + except EOFError: + debug("break")# no more information + + finally: + ws.close() + print("sendWS terminating...") + + + thread.start_new_thread(run, ()) + + +if __name__ == "__main__": + websocket.enableTrace(True) + ws = websocket.WebSocketApp("ws://"+str(serverIP)+":"+str(wsPORT), + on_message = on_message, + on_error = on_error, + on_close = on_close) + ws.on_open = on_open + ws.run_forever() + + + + + + diff --git a/clitools/exports/tonano.py b/clitools/exports/tonano.py new file mode 100644 index 0000000..bea3924 --- /dev/null +++ b/clitools/exports/tonano.py @@ -0,0 +1,157 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + +''' +tonano +exporter to LJ nano +v0.1b + +''' +from __future__ import print_function +import websocket +import time +import argparse +import traceback +import sys +import random +from websocket_server import WebsocketServer +from socket import * + +try: + import thread +except ImportError: + import _thread as thread + +name = "exports::tonano" + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + + +argsparser = argparse.ArgumentParser(description="tonano v0.1b help mode") +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output") +argsparser.add_argument("-s","--server",help="WS server IP (127.0.0.1 by default)", type=str) +argsparser.add_argument("-p","--port",help="WS port to bind to (9001 by default)", type=str) +argsparser.add_argument("-k","--key",help="Redis key to update",default="0",type=str) +args = argsparser.parse_args() + +key = args.key + +if args.verbose: + verbose = True +else: + verbose = False + +if args.server: + serverIP = args.server +else: + serverIP = "127.0.0.1" + +if args.port: + wsPORT = args.port +else: + wsPORT = 9001 + +debug("") +debug("tonano v0.1b") + +points0 = "[(150.0, 230.0, 65280), (170.0, 170.0, 65280), (230.0, 170.0, 65280), (210.0, 230.0, 65280), (150.0, 230.0, 65280)]" +points1 = "[(180.0, 230.0, 65280), (200.0, 170.0, 65280), (180.0, 230.0, 65280)]" +points2 = "[(170.0, 190.0, 65280), (200.0, 170.0, 65280), (230.0, 190.0, 65280), (230.0, 200.0, 65280), (170.0, 230.0, 65280), (230.0, 230.0, 65280)]" +points3 = "[(170.0, 170.0, 65280), (200.0, 170.0, 65280), (230.0, 190.0, 65280), (200.0, 200.0, 65280), (230.0, 210.0, 65280), (200.0, 230.0, 65280), (170.0, 230.0, 65280)]" +points = [points0, points1, points2, points3] + +LaserNumber = 1 +SceneNumber = 0 +Laser = 0 + +def sendbroadcast(): + + debug("Sending broadcast") + cs = socket(AF_INET, SOCK_DGRAM) + cs.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) + cs.setsockopt(SOL_SOCKET, SO_BROADCAST, 1) + cs.sendto("LJ tonano 0.1".encode(), ("255.255.255.255", 54545)) + + +# +# CLI websocket client -> WS server (nanoLJ) -> webpage +# + +def on_message(ws, message): + if random.randint(0,100)>95: + sendbroadcast() + #debug("CLI WS client received and dropped "+message) + +def on_error(ws, error): + debug("CLI WS client got error :"+error) + +def on_close(ws): + debug("### CLI WS client WS closed ###") + +def on_open(ws): + + def run(*args): + + try: + while True: + line = sys.stdin.readline() + if line == "": + time.sleep(0.005) + + #debug("CLI string", line) + line = line.rstrip('\n') + line=line[1:-1] + line = line.replace("[",'(') + line = line.replace("]",')') + #debug(line) + line = "[{}]".format(line) + debug("CLI proccess sending : /simul" +" "+ line) + #sendWSall("/simul" +" "+ str(points[laserid].decode('ascii'))) + ws.send("/simul "+line) + #debug("exports::tosimuCLIent "+str(key)+" "+line) + + except EOFError: + debug("tonano break")# no more information + + finally: + ws.close() + debug("tonano WS terminating...") + + + thread.start_new_thread(run, ()) + +def handle_timeout(self): + self.timed_out = True + + +# +# Launch WS CLI client +# + +if __name__ == "__main__": + + try: + + # CLI Websocket client + debug("Launching tosimu CLI websocket client...") + #websocket.enableTrace(True) + websocket.enableTrace(False) + ws = websocket.WebSocketApp("ws://"+str(serverIP)+":"+str(wsPORT), + on_message = on_message, + on_error = on_error, + on_close = on_close) + ws.on_open = on_open + ws.run_forever() + + except Exception: + debug("tonano Exception") + traceback.print_exc() + + + + + diff --git a/clitools/exports/websocket_server.py b/clitools/exports/websocket_server.py new file mode 100644 index 0000000..50844a0 --- /dev/null +++ b/clitools/exports/websocket_server.py @@ -0,0 +1,388 @@ +# Author: Johan Hanssen Seferidis +# License: MIT + +''' +Custom version +with clients_list() +For 2 clients : +[{'id': 1, 'handler': , 'address': ('127.0.0.1', 62718)}, {'id': 2, 'handler': , 'address': ('127.0.0.1', 62720)}] + + +def client_list(): + + clients = wserver.clients() + for client in clients: + print(client['id']) +''' + +import sys +import struct +from base64 import b64encode +from hashlib import sha1 +import logging +from socket import error as SocketError +import errno + +if sys.version_info[0] < 3: + from SocketServer import ThreadingMixIn, TCPServer, StreamRequestHandler +else: + from socketserver import ThreadingMixIn, TCPServer, StreamRequestHandler + +logger = logging.getLogger(__name__) +logging.basicConfig() + +''' ++-+-+-+-+-------+-+-------------+-------------------------------+ + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ++-+-+-+-+-------+-+-------------+-------------------------------+ +|F|R|R|R| opcode|M| Payload len | Extended payload length | +|I|S|S|S| (4) |A| (7) | (16/64) | +|N|V|V|V| |S| | (if payload len==126/127) | +| |1|2|3| |K| | | ++-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + +| Extended payload length continued, if payload len == 127 | ++ - - - - - - - - - - - - - - - +-------------------------------+ +| Payload Data continued ... | ++---------------------------------------------------------------+ +''' + +FIN = 0x80 +OPCODE = 0x0f +MASKED = 0x80 +PAYLOAD_LEN = 0x7f +PAYLOAD_LEN_EXT16 = 0x7e +PAYLOAD_LEN_EXT64 = 0x7f + +OPCODE_CONTINUATION = 0x0 +OPCODE_TEXT = 0x1 +OPCODE_BINARY = 0x2 +OPCODE_CLOSE_CONN = 0x8 +OPCODE_PING = 0x9 +OPCODE_PONG = 0xA + + +# -------------------------------- API --------------------------------- + +class API(): + + def run_forever(self): + try: + logger.info("Listening on port %d for clients.." % self.port) + self.serve_forever() + except KeyboardInterrupt: + self.server_close() + logger.info("Server terminated.") + except Exception as e: + logger.error(str(e), exc_info=True) + exit(1) + + def new_client(self, client, server): + pass + + def client_left(self, client, server): + pass + + def message_received(self, client, server, message): + pass + + def set_fn_new_client(self, fn): + self.new_client = fn + + def set_fn_client_left(self, fn): + self.client_left = fn + + def set_fn_message_received(self, fn): + self.message_received = fn + + def send_message(self, client, msg): + self._unicast_(client, msg) + + def send_message_to_all(self, msg): + self._multicast_(msg) + + def clients_list(self): + return self.clients + + +# ------------------------- Implementation ----------------------------- + +class WebsocketServer(ThreadingMixIn, TCPServer, API): + """ + A websocket server waiting for clients to connect. + + Args: + port(int): Port to bind to + host(str): Hostname or IP to listen for connections. By default 127.0.0.1 + is being used. To accept connections from any client, you should use + 0.0.0.0. + loglevel: Logging level from logging module to use for logging. By default + warnings and errors are being logged. + + Properties: + clients(list): A list of connected clients. A client is a dictionary + like below. + { + 'id' : id, + 'handler' : handler, + 'address' : (addr, port) + } + """ + + allow_reuse_address = True + daemon_threads = True # comment to keep threads alive until finished + + clients = [] + id_counter = 0 + + def __init__(self, port, host='127.0.0.1', loglevel=logging.WARNING): + logger.setLevel(loglevel) + TCPServer.__init__(self, (host, port), WebSocketHandler) + self.port = self.socket.getsockname()[1] + + def _message_received_(self, handler, msg): + self.message_received(self.handler_to_client(handler), self, msg) + + def _ping_received_(self, handler, msg): + handler.send_pong(msg) + + def _pong_received_(self, handler, msg): + pass + + def _new_client_(self, handler): + self.id_counter += 1 + client = { + 'id': self.id_counter, + 'handler': handler, + 'address': handler.client_address + } + self.clients.append(client) + self.new_client(client, self) + + def _client_left_(self, handler): + client = self.handler_to_client(handler) + self.client_left(client, self) + if client in self.clients: + self.clients.remove(client) + + def _unicast_(self, to_client, msg): + to_client['handler'].send_message(msg) + + def _multicast_(self, msg): + for client in self.clients: + self._unicast_(client, msg) + + def handler_to_client(self, handler): + for client in self.clients: + if client['handler'] == handler: + return client + + +class WebSocketHandler(StreamRequestHandler): + + def __init__(self, socket, addr, server): + self.server = server + StreamRequestHandler.__init__(self, socket, addr, server) + + def setup(self): + StreamRequestHandler.setup(self) + self.keep_alive = True + self.handshake_done = False + self.valid_client = False + + def handle(self): + while self.keep_alive: + if not self.handshake_done: + self.handshake() + elif self.valid_client: + self.read_next_message() + + def read_bytes(self, num): + # python3 gives ordinal of byte directly + bytes = self.rfile.read(num) + if sys.version_info[0] < 3: + return map(ord, bytes) + else: + return bytes + + def read_next_message(self): + try: + b1, b2 = self.read_bytes(2) + except SocketError as e: # to be replaced with ConnectionResetError for py3 + if e.errno == errno.ECONNRESET: + logger.info("Client closed connection.") + print("Error: {}".format(e)) + self.keep_alive = 0 + return + b1, b2 = 0, 0 + except ValueError as e: + b1, b2 = 0, 0 + + fin = b1 & FIN + opcode = b1 & OPCODE + masked = b2 & MASKED + payload_length = b2 & PAYLOAD_LEN + + if opcode == OPCODE_CLOSE_CONN: + logger.info("Client asked to close connection.") + self.keep_alive = 0 + return + if not masked: + logger.warn("Client must always be masked.") + self.keep_alive = 0 + return + if opcode == OPCODE_CONTINUATION: + logger.warn("Continuation frames are not supported.") + return + elif opcode == OPCODE_BINARY: + logger.warn("Binary frames are not supported.") + return + elif opcode == OPCODE_TEXT: + opcode_handler = self.server._message_received_ + elif opcode == OPCODE_PING: + opcode_handler = self.server._ping_received_ + elif opcode == OPCODE_PONG: + opcode_handler = self.server._pong_received_ + else: + logger.warn("Unknown opcode %#x." % opcode) + self.keep_alive = 0 + return + + if payload_length == 126: + payload_length = struct.unpack(">H", self.rfile.read(2))[0] + elif payload_length == 127: + payload_length = struct.unpack(">Q", self.rfile.read(8))[0] + + masks = self.read_bytes(4) + message_bytes = bytearray() + for message_byte in self.read_bytes(payload_length): + message_byte ^= masks[len(message_bytes) % 4] + message_bytes.append(message_byte) + opcode_handler(self, message_bytes.decode('utf8')) + + def send_message(self, message): + self.send_text(message) + + def send_pong(self, message): + self.send_text(message, OPCODE_PONG) + + def send_text(self, message, opcode=OPCODE_TEXT): + """ + Important: Fragmented(=continuation) messages are not supported since + their usage cases are limited - when we don't know the payload length. + """ + + # Validate message + if isinstance(message, bytes): + message = try_decode_UTF8(message) # this is slower but ensures we have UTF-8 + if not message: + logger.warning("Can\'t send message, message is not valid UTF-8") + return False + elif sys.version_info < (3,0) and (isinstance(message, str) or isinstance(message, unicode)): + pass + elif isinstance(message, str): + pass + else: + logger.warning('Can\'t send message, message has to be a string or bytes. Given type is %s' % type(message)) + return False + + header = bytearray() + payload = encode_to_UTF8(message) + payload_length = len(payload) + + # Normal payload + if payload_length <= 125: + header.append(FIN | opcode) + header.append(payload_length) + + # Extended payload + elif payload_length >= 126 and payload_length <= 65535: + header.append(FIN | opcode) + header.append(PAYLOAD_LEN_EXT16) + header.extend(struct.pack(">H", payload_length)) + + # Huge extended payload + elif payload_length < 18446744073709551616: + header.append(FIN | opcode) + header.append(PAYLOAD_LEN_EXT64) + header.extend(struct.pack(">Q", payload_length)) + + else: + raise Exception("Message is too big. Consider breaking it into chunks.") + return + + self.request.send(header + payload) + + def read_http_headers(self): + headers = {} + # first line should be HTTP GET + http_get = self.rfile.readline().decode().strip() + assert http_get.upper().startswith('GET') + # remaining should be headers + while True: + header = self.rfile.readline().decode().strip() + if not header: + break + head, value = header.split(':', 1) + headers[head.lower().strip()] = value.strip() + return headers + + def handshake(self): + headers = self.read_http_headers() + + try: + assert headers['upgrade'].lower() == 'websocket' + except AssertionError: + self.keep_alive = False + return + + try: + key = headers['sec-websocket-key'] + except KeyError: + logger.warning("Client tried to connect but was missing a key") + self.keep_alive = False + return + + response = self.make_handshake_response(key) + self.handshake_done = self.request.send(response.encode()) + self.valid_client = True + self.server._new_client_(self) + + @classmethod + def make_handshake_response(cls, key): + return \ + 'HTTP/1.1 101 Switching Protocols\r\n'\ + 'Upgrade: websocket\r\n' \ + 'Connection: Upgrade\r\n' \ + 'Sec-WebSocket-Accept: %s\r\n' \ + '\r\n' % cls.calculate_response_key(key) + + @classmethod + def calculate_response_key(cls, key): + GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' + hash = sha1(key.encode() + GUID.encode()) + response_key = b64encode(hash.digest()).strip() + return response_key.decode('ASCII') + + def finish(self): + self.server._client_left_(self) + + +def encode_to_UTF8(data): + try: + return data.encode('UTF-8') + except UnicodeEncodeError as e: + logger.error("Could not encode data to UTF-8 -- %s" % e) + return False + except Exception as e: + raise(e) + return False + + +def try_decode_UTF8(data): + try: + return data.decode('utf-8') + except UnicodeDecodeError: + return False + except Exception as e: + raise(e) diff --git a/clitools/filters/.DS_Store b/clitools/filters/.DS_Store new file mode 100644 index 0000000..d9b12aa Binary files /dev/null and b/clitools/filters/.DS_Store differ diff --git a/clitools/filters/anaglyph.py b/clitools/filters/anaglyph.py new file mode 100755 index 0000000..12d2203 --- /dev/null +++ b/clitools/filters/anaglyph.py @@ -0,0 +1,200 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + + +''' + +anaglyph +v0.1.0 + +Attempts to create a valid 3D-glasses structure + +LICENCE : CC + +by cocoa + + +''' +from __future__ import print_function +import argparse +import ast +import math +import os +import random +import sys +import time +name = "filters::cycle" + +maxDist = 300 + +argsparser = argparse.ArgumentParser(description="Redis exporter LJ") +argsparser.add_argument("-x","--centerX",help="geometrical center X position",default=400,type=int) +argsparser.add_argument("-y","--centerY",help="geometrical center Y position",default=400,type=int) +argsparser.add_argument("-m","--min",help="Minimal displacement (default:2) ",default=1,type=int) +argsparser.add_argument("-M","--max",help="Maximal displacement (default:20) ",default=5,type=int) +argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose") + +args = argsparser.parse_args() +fps = args.fps +minVal = args.min +maxVal = args.max +centerX = args.centerX +centerY = args.centerY +verbose = args.verbose + +optimal_looptime = 1 / fps +name = "filters::anaglyph" + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + +def rgb2int(rgb): + #debug(name,"::rgb2int rbg:{}".format(rgb)) + return int('0x%02x%02x%02x' % tuple(rgb),0) + +def isValidColor( color, intensityColThreshold ): + if color[0] + color[1] + color[2] > intensityColThreshold: + return True + return False + +# These are paper colors +red = (41,24,24) +white = (95,95,95) +blue = (0,41,64) + +red = (127,0,0) +blue = (0,128,128) +white = (128,128,128) +def anaglyph( pl ): + + debug(name,'--------------- new loop ------------------') + # We will send one list after the other to optimize color change + blueList = list() + redList = list() + whiteList = list() + out = [] + out1 = [] + out2 = [] + out3 = [] + + # The anaglyphic effect will be optained by : + # * having close objects appear as white + # * having distant objects appear as blue + red + # * having in between objects appear as distanceDecreased(white) + blue + red + for i, point in enumerate(pl): + ref_x = point[0]-centerX + ref_y = point[1]-centerY + ref_color = point[2] + angle = math.atan2( ref_x , ref_y ) + dist = ref_y / math.cos(angle) + white_rvb = (0,0,0) + blue_rvb = (0,0,0) + red_rvb = (0,0,0) + + # Calculate the point's spread factor (0.0 to 1.0) + # The spread is high if the point is close to center + """ + dist = 0 : spread = 1.0 + dist = maxDist spread = 0.0 + """ + if dist == 0: + spread = 1.0 + else : + spread =( maxDist - dist ) / maxDist + if spread < 0.0: + spread = 0.0 + + #debug(name,"dist:{} spread:{}".format(dist,spread)) + + # White color is high if spread is low, i.e. point away from center + """ + spread = 1.0 : white_c = 0.0 + spread = 0.0 : whice_c = 1.0 + """ + if point[2] == 0: + white_color = 0 + else: + white_factor = 1.0 - math.pow(spread,0.5) + white_rvb = tuple(map( lambda a: int(white_factor* a), white)) + white_color = rgb2int( white_rvb) + #debug(name,"spread:{}\t white_rvb:{}\t white_color:{}".format(spread, white_rvb, white_color)) + + # Blue and Red colors are high if spread is high, i.e. close to center + """ + spread = 1.0 : red_c = 1.0 + spread = 0.0 : red_c = 0.0 + """ + color_factor = math.pow(spread,1) + if point[2] == 0: + blue_color = 0 + red_color = 0 + else: + blue_rvb = tuple(map( lambda a: int(color_factor * a), blue)) + blue_color = rgb2int( blue_rvb) + red_rvb = tuple(map( lambda a: int(color_factor * a), red)) + red_color = rgb2int( red_rvb) + + #debug(name,"color_factor:{}\t\t blue_color:{}\t\t red_color:{}".format(color_factor,blue_color,red_color)) + + # Blue-to-Red spatial spread is high when spread is high, i.e. point close to center + """ + spread = 1.0 : spatial_spread = maxVal + spread = 0.0 : spatial_spread = minVal + """ + spatial_spread = minVal + spread * (maxVal - minVal) + #debug(name,"spatial_spread:{}".format(spatial_spread)) + red_x = int(point[0] + spatial_spread) + blue_x = int(point[0] - spatial_spread ) + red_y = int(point[1] ) + blue_y = int(point[1]) + + white_point = [point[0], point[1], white_color] + blue_point = [blue_x,blue_y,blue_color] + red_point = [red_x,red_y,red_color] + + #debug(name,"white[x,y,c]:{}".format(white_point)) + #debug(name,"blue[x,y,c]:{}".format(blue_point)) + #debug(name,"red[x,y,c]:{}".format(red_point)) + # Do not append "black lines" i.e. a color where each composent is below X +# if isValidColor(white_rvb, 150): +# out1.append(white_point) +# if isValidColor(blue_rvb, 50): +# out2.append(blue_point) +# if isValidColor(red_rvb, 30): +# out3.append(red_point) + out1.append(white_point) + out2.append(blue_point) + out3.append(red_point) + + #debug(name,"source pl:{}".format(pl)) + debug(name,"whiteList:{}".format(out1)) + debug(name,"blueList:{}".format(out2)) + debug(name,"redList:{}".format(out3)) + return out1 + out3 + out2 + #return out1 + out2 + out3 + + + +try: + while True: + start = time.time() + line = sys.stdin.readline() + if line == "": + time.sleep(0.01) + line = line.rstrip('\n') + pointsList = ast.literal_eval(line) + # Do the filter + result = anaglyph( pointsList ) + print( result, flush=True ) + looptime = time.time() - start + # debug(name+" looptime:"+str(looptime)) + if( looptime < optimal_looptime ): + time.sleep( optimal_looptime - looptime) + # debug(name+" micro sleep:"+str( optimal_looptime - looptime)) +except EOFError: + debug(name+" break")# no more information + diff --git a/clitools/filters/colorcycle.py b/clitools/filters/colorcycle.py new file mode 100755 index 0000000..e293336 --- /dev/null +++ b/clitools/filters/colorcycle.py @@ -0,0 +1,108 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + + +''' + +colorcycle +v0.1.0 + +A simple effect : cycle colors + +LICENCE : CC + +by cocoa + + +''' +from __future__ import print_function +import sys +import ast +import os +import argparse +import random +import time +name = "filters::cycle" + +argsparser = argparse.ArgumentParser(description="Redis exporter LJ") +argsparser.add_argument("-x","--centerX",help="geometrical center X position",default=300,type=int) +argsparser.add_argument("-y","--centerY",help="geometrical center Y position",default=300,type=int) +argsparser.add_argument("-m","--min",help="Lowest value in the range 0-255",default=10,type=int) +argsparser.add_argument("-M","--max",help="Highest value in the range 0-255",default=255,type=int) +argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose") + +args = argsparser.parse_args() +fps = args.fps +minVal = args.min +maxVal = args.max +centerX = args.centerX +centerY = args.centerY +verbose = args.verbose + +optimal_looptime = 1 / fps + +UP = 5 +DOWN = -5 +currentColor = [0,0,0] +composant = 0 +currentDirection = UP + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + +def rgb2int(rgb): + return int('0x%02x%02x%02x' % tuple(rgb),0) + +def cycleColor( pl ): + + global composant + global currentDirection + # debug(name,"pl:{}".format(pl)) + value = currentColor[composant] + if currentDirection == UP: + target = maxVal + else: + target = minVal + value += currentDirection + currentColor[composant] = value + + debug(name,"currentColor:{}".format(currentColor)) + for i in range( 0, len(pl)): + if pl[i][2] != 0: + pl[i][2] = rgb2int( currentColor) + + # change the composant if target reached + if value <= target and currentDirection == DOWN or value >= target and currentDirection == UP : + composant = random.randint( 0,2) + value = currentColor[composant] + if value == 0 : + currentDirection = UP + else: + currentDirection = DOWN + #debug( "pl:{}".format(pl)) + return pl + + +try: + while True: + start = time.time() + line = sys.stdin.readline() + if line == "": + time.sleep(0.01) + line = line.rstrip('\n') + pointsList = ast.literal_eval(line) + # Do the filter + result = cycleColor( pointsList ) + print( result, flush=True ) + looptime = time.time() - start + # debug(name+" looptime:"+str(looptime)) + if( looptime < optimal_looptime ): + time.sleep( optimal_looptime - looptime) + # debug(name+" micro sleep:"+str( optimal_looptime - looptime)) +except EOFError: + debug(name+" break")# no more information + diff --git a/clitools/filters/kaleidoscope.py b/clitools/filters/kaleidoscope.py new file mode 100755 index 0000000..5ca4396 --- /dev/null +++ b/clitools/filters/kaleidoscope.py @@ -0,0 +1,174 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + + +''' + +kaleidoscop +v0.1.0 + +A simple effect : mirror a quadrant of the input + +LICENCE : CC + +by Sam Neurohack + + +''' +from __future__ import print_function +import sys +import ast +import os +import argparse +ljpath = r'%s' % os.getcwd().replace('\\','/') +sys.path.append(ljpath +'/../libs/') +sys.path.append(ljpath +'/libs/') + +import time +name = "filters::kaleidoscope" + +argsparser = argparse.ArgumentParser(description="Redis exporter LJ") +argsparser.add_argument("-x","--centerX",help="geometrical center X position",default=400,type=int) +argsparser.add_argument("-y","--centerY",help="geometrical center Y position",default=400,type=int) +argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose") + +args = argsparser.parse_args() +fps = args.fps +centerX = args.centerX +centerY = args.centerY +verbose = args.verbose + +optimal_looptime = 1 / fps + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + +def kaleidoscope( pl ): + + # Stage 1: Crop points in single quadrant + quad1 = [] + # Iterate trough the segments + for i in range( 0, len(pl) ): + + #debug(name+" point #", i) + currentpoint = cp = pl[i] + cx,cy,cc = [cp[0],cp[1],cp[2]] + + # Exception: escape early if last point + if i == len(pl) - 1: + if cx >= centerX and cy >= centerY : + quad1.append( currentpoint ) + break + + # Search for the couple of points + nextpoint = pl[i+1] + nx,ny,nc = [nextpoint[0],nextpoint[1],nextpoint[2]] + rect=[[cx,cy],[cx,ny],[nx,ny],[nx,cy]] + + right = wrong = 0 + #debug(name+" rect: ", rect,"curr",currentpoint,"next",nextpoint ) + + # Enumerate the points in rectangle to see + # how many right / wrong there are + # either to add or skip early + for iterator, p in enumerate(rect): + if p[0] >= centerX and p[1] >= centerY: + right += 1 + else: + #if p[0] <= centerX and p[1] <= centerY: + wrong += 1 + # If all rectangle points are in the right quadrant, Add and Skip + if right == 4: + quad1.append(pl[i]) + #debug(name+" found valid point", pl[i]) + continue + # If all rectangle points in wrong quadrant, Skip + if wrong == 4: + #debug(name+" found bad point", pl[i]) + continue + + # Find the (x,y) intersections + # + #debug(name+" Looking for crossing point between ("+str(cx)+","+str(cy)+") and ("+str(nx)+","+str(ny)+")") + delta=[ nx - cx, ny - cy ] + #debug(name+" delta:",delta) + crossX = None + crossY = None + absnewX = 0 + absnewY = 0 + # If one point has negative x, search y axis crossing + if cx < centerX or nx < centerX: + if delta[0] == 0 : + delta[0] = 0.0000001 + v=[ delta[0]/abs(delta[0]), delta[1]/abs(delta[0]) ] + absnewX = abs( centerX - cx ) + #print("on y axis, v=",str(v)," and absnewX=",str(absnewX)) + crossX = [( absnewX*v[0] + cx ),( absnewX*v[1]+cy ), nc] + # If one point has negative y, search x axis crossing + if cy < centerY or ny < centerY: + if delta[1] == 0 : + delta[1] = 0.0000001 + v=[ delta[0]/abs(delta[1]), delta[1]/abs(delta[1])] + absnewY = abs( centerY - cy ) + #print("on x axis, v=",str(v)," and absnewY=",str(absnewY)) + crossY = [( absnewY*v[0] + cy ),( absnewY*v[1]+cy ), nc] + # Inject in order + # If current point is the quadrant, add it + if cx >= centerX and cy >= centerY : + quad1.append( currentpoint ) + # If absnewX smaller, it is closest to currentPoint + if absnewX < absnewY: + if None != crossX : quad1.append( crossX ) + if None != crossY : quad1.append( crossY ) + else : + if None != crossY : quad1.append( crossY ) + if None != crossX : quad1.append( crossX ) + # Add a black point at the end + #lastQuad1Point = quad1[-1] + #quad1.append( [lastQuad1Point[0],lastQuad1Point[1],0] ) + + ## Stage 2 : Mirror points + # + quad2 = [] + # quad2 = vertical symetric of quad1 + for iterator in range( len(quad1) -1 , -1, -1): + point = quad1[iterator] + quad2.append([ point[0], 2*centerY - point[1], point[2] ]) + # quad3 is the merge of 1 and 2 + quad3 = quad1 + quad2 + # quad4 is the horizontal symetric of quad3 + quad4 = [] + for iterator in range( len(quad3) -1, -1, -1): + point = quad3[iterator] + quad4.append([ 2*centerX - point[0], point[1], point[2] ]) + + #debug(name+" quad1:",quad1) + #debug(name+" quad2:", quad2 ) + #debug(name+" quad3:", quad3 ) + #debug(name+" quad4:", quad4 ) + return quad3+quad4 + +try: + while True: + start = time.time() + line = sys.stdin.readline() + if line == "": + time.sleep(0.01) + line = line.rstrip('\n') + pointsList = ast.literal_eval(line) + # Do the filter + result = kaleidoscope( pointsList ) + print( result, flush=True ) + + looptime = time.time() - start + # debug(name+" looptime:"+str(looptime)) + if( looptime < optimal_looptime ): + time.sleep( optimal_looptime - looptime) + # debug(name+" micro sleep:"+str( optimal_looptime - looptime)) +except EOFError: + debug(name+" break")# no more information + diff --git a/clitools/filters/redilysis.py b/clitools/filters/redilysis.py new file mode 100755 index 0000000..00cd61a --- /dev/null +++ b/clitools/filters/redilysis.py @@ -0,0 +1,300 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + + +''' + +redilysis +v0.1.0 + +A complex effect that depends on redis keys for audio analysis + +see https://git.interhacker.space/teamlase/redilysis for more informations +about the redilysis project + +LICENCE : CC + +by cocoa + + +''' +from __future__ import print_function +import argparse +import ast +import os +import math +import random +import redis +import sys +import time +name = "filters::redilysis" + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) +def msNow(): + return time.time() + +# The list of available modes => redis keys each requires to run +oModeList = { + "rms_noise": ["rms"], + "rms_size": ["rms"], + "bpm_size": ["bpm"], + "bpm_detect_size": ["bpm","bpm_delay","bpm_sample_interval","beats"] + } +CHAOS = 1 +REDISLATENCY = 30 +REDIS_FREQ = 300 + +# General Args +argsparser = argparse.ArgumentParser(description="Redilysis filter") +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose") +# Redis Args +argsparser.add_argument("-i","--ip",help="IP address of the Redis server ",default="127.0.0.1",type=str) +argsparser.add_argument("-p","--port",help="Port of the Redis server ",default="6379",type=str) +argsparser.add_argument("-s","--redis-freq",help="Query Redis every x (in milliseconds). Default:{}".format(REDIS_FREQ),default=REDIS_FREQ,type=int) +# General args +argsparser.add_argument("-x","--centerX",help="geometrical center X position",default=400,type=int) +argsparser.add_argument("-y","--centerY",help="geometrical center Y position",default=400,type=int) +argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int) +# Modes And Common Modes Parameters +argsparser.add_argument("-l","--redisLatency",help="Latency in ms to substract. Default:{}".format(REDISLATENCY),default=REDISLATENCY,type=float) +argsparser.add_argument("-m","--modelist",required=True,help="Comma separated list of modes to use from: {}".format("i, ".join(oModeList.keys())),type=str) +argsparser.add_argument("--chaos",help="How much disorder to bring. High value = More chaos. Default {}".format(CHAOS), default=CHAOS, type=str) + +args = argsparser.parse_args() +ip = args.ip +port = args.port +redisFreq = args.redis_freq / 1000 +verbose = args.verbose +fps = args.fps +centerX = args.centerX +centerY = args.centerY +redisLatency = args.redisLatency +chaos = float(args.chaos) +optimal_looptime = 1 / fps + +modeList = args.modelist.split(",") +redisKeys = [] +for mode in modeList: + if not mode in oModeList: + print("Mode '{}' is invalid. Exiting.".format(mode)) + sys.exit(2) + redisKeys += oModeList[mode] +redisKeys = list(set(redisKeys)) +debug(name,"Redis Keys:{}".format(redisKeys)) +redisData = {} +redisLastHit = msNow() - 99999 +r = redis.Redis( + host=ip, + port=port) + +# Records the last bpm +tsLastBeat = time.time() + +def gauss(x, mu, sigma): + return( math.exp(-math.pow((x-mu),2)/(2*math.pow(sigma,2))/math.sqrt(2*math.pi*math.pow(sigma,2)))) + +previousPTTL = 0 +tsNextBeatsList = [] +def bpmDetect( ): + """ + An helper to compute the next beat time in milliseconds + Returns True if the cache was updated + """ + global tsNextBeatsList + global previousPTTL + global redisLastHit + global redisLatency + + # Get the redis PTTL value for bpm + PTTL = redisData["bpm_pttl"] + + # Skip early if PTTL < 0 + if PTTL < 0 : + debug(name,"bpmDetect skip detection : PTTL expired for 'bpm' key") + return False + + # Skip early if the record hasn't been rewritten + if PTTL <= previousPTTL : + previousPTTL = PTTL + #debug(name,"bpmDetect skip detection : {} <= {}".format(PTTL, previousPTTL)) + return False + debug(name,"bpmDetect running detection : {} > {}".format(PTTL, previousPTTL)) + previousPTTL = PTTL + + # Skip early if beat list is empty + beatsList = ast.literal_eval(redisData["beats"]) + tsNextBeatsList = [] + if( len(beatsList) == 0 ): + return True + + # Read from redis + bpm = float(redisData["bpm"]) + msBpmDelay = float(redisData["bpm_delay"]) + samplingInterval = float(redisData["bpm_sample_interval"]) + + # Calculate some interpolations + lastBeatTiming = float(beatsList[len(beatsList) - 1]) + msPTTLDelta = 2 * samplingInterval - float(PTTL) + sPerBeat = 60 / bpm + lastBeatDelay = msBpmDelay - lastBeatTiming*1000 + msPTTLDelta + countBeatsPast = math.floor( (lastBeatDelay / 1000) / sPerBeat) + #debug(name,"bpmDetect lastBeatTiming:{}\tmsPTTLDelta:{}\tsPerBeat:{}".format(lastBeatTiming,msPTTLDelta,sPerBeat)) + #debug(name,"lastBeatDelay:{}\t countBeatsPast:{}".format(lastBeatDelay, countBeatsPast)) + for i in range( countBeatsPast, 1000): + beatTime = i * sPerBeat - lastBeatTiming + if beatTime < 0: + continue + if beatTime * 1000 > 2 * samplingInterval : + break + #debug(name, "bpmDetect beat add beatTime:{} redisLastHit:{}".format(beatTime, redisLastHit)) + tsNextBeatsList.append( redisLastHit + beatTime - redisLatency/1000) + debug(name, "bpmDetect new tsNextBeatsList:{}".format(tsNextBeatsList)) + + return True + +def bpm_detect_size( pl ): + bpmDetect() + + # Find the next beat in the list + tsNextBeat = 0 + + now = time.time() + msNearestBeat = None + msRelativeNextBTList = list(map( lambda a: abs(now - a) * 1000, tsNextBeatsList)) + msToBeat = min( msRelativeNextBTList) + + #debug(name,"bpm_detect_size msRelativeNextBTList:{} msToBeat:{}".format(msRelativeNextBTList,msToBeat)) + # Calculate the intensity based on bpm coming/leaving + # The curb is a gaussian + mu = 15 + intensity = gauss( msToBeat, 0 , mu) + #debug(name,"bpm_size","mu:{}\t msToBeat:{}\tintensity:{}".format(mu, msToBeat, intensity)) + if msToBeat < 20: + debug(name,"bpm_detect_size kick:{}".format(msToBeat)) + pass + for i, point in enumerate(pl): + ref_x = point[0]-centerX + ref_y = point[1]-centerY + #debug(name,"In new ref x:{} y:{}".format(point[0]-centerX,point[1]-centerY)) + angle=math.atan2( point[0] - centerX , point[1] - centerY ) + l = ref_y / math.cos(angle) + new_l = l * intensity + #debug(name,"bpm_size","angle:{} l:{} new_l:{}".format(angle,l,new_l)) + new_x = math.sin(angle) * new_l + centerX + new_y = math.cos(angle) * new_l + centerY + #debug(name,"x,y:({},{}) x',y':({},{})".format(point[0],point[1],new_x,new_y)) + pl[i][0] = new_x + pl[i][1] = new_y + #debug( name,"bpm_detect_size output:{}".format(pl)) + return( pl ); + +def bpm_size( pl ): + global tsLastBeat + bpm = float(redisData["bpm"]) + # msseconds ber beat + msPerBeat = int(60 / bpm * 1000) + # Calculate the intensity based on bpm coming/leaving + # The curb is a gaussian + mu = math.sqrt(msPerBeat) + msTimeToLastBeat = (time.time() - tsLastBeat) * 1000 + msTimeToNextBeat = (msPerBeat - msTimeToLastBeat) + intensity = gauss( msTimeToNextBeat, 0 , mu) + debug(name,"bpm_size","msPerBeat:{}\tmu:{}".format(msPerBeat, mu)) + debug(name,"bpm_size","msTimeToLastBeat:{}\tmsTimeToNextBeat:{}\tintensity:{}".format(msTimeToLastBeat, msTimeToNextBeat, intensity)) + if msTimeToNextBeat <= 0 : + tsLastBeat = time.time() + for i, point in enumerate(pl): + ref_x = point[0]-centerX + ref_y = point[1]-centerY + #debug(name,"In new ref x:{} y:{}".format(point[0]-centerX,point[1]-centerY)) + angle=math.atan2( point[0] - centerX , point[1] - centerY ) + l = ref_y / math.cos(angle) + new_l = l * intensity + #debug(name,"bpm_size","angle:{} l:{} new_l:{}".format(angle,l,new_l)) + new_x = math.sin(angle) * new_l + centerX + new_y = math.cos(angle) * new_l + centerY + #debug(name,"x,y:({},{}) x',y':({},{})".format(point[0],point[1],new_x,new_y)) + pl[i][0] = new_x + pl[i][1] = new_y + #debug( name,"bpm_noise output:{}".format(pl)) + return pl + +def rms_size( pl ): + rms = float(redisData["rms"]) + for i, point in enumerate(pl): + + ref_x = point[0]-centerX + ref_y = point[1]-centerY + debug(name,"In new ref x:{} y:{}".format(point[0]-centerX,point[1]-centerY)) + angle=math.atan2( point[0] - centerX , point[1] - centerY ) + l = ref_y / math.cos(angle) + debug(name,"angle:{} l:{}".format(angle,l)) + new_l = l + rms * chaos + new_x = math.sin(angle) * new_l + centerX + new_y = math.cos(angle) * new_l + centerY + debug(name,"x,y:({},{}) x',y':({},{})".format(point[0],point[1],new_x,new_y)) + pl[i][0] = new_x + pl[i][1] = new_y + #debug( name,"rms_noise output:{}".format(pl)) + return pl + +def rms_noise( pl ): + rms = float(redisData["rms"]) + debug(name, "pl:{}".format(pl)) + for i, point in enumerate(pl): + #debug(name,"rms_noise chaos:{} rms:{}".format(chaos, rms)) + xRandom = random.uniform(-1,1) * rms * chaos + yRandom = random.uniform(-1,1) * rms * chaos + #debug(name,"rms_noise xRandom:{} yRandom:{}".format(xRandom, yRandom)) + pl[i][0] += xRandom + pl[i][1] += yRandom + #debug( name,"rms_noise output:{}".format(pl)) + return pl + + +def refreshRedis(): + global redisLastHit + global redisData + # Skip if cache is sufficent + diff = msNow() - redisLastHit + if diff < redisFreq : + #debug(name, "refreshRedis not updating redis, {} < {}".format(diff, redisFreq)) + pass + else: + #debug(name, "refreshRedis updating redis, {} > {}".format(diff, redisFreq)) + redisLastHit = msNow() + for key in redisKeys: + redisData[key] = r.get(key).decode('ascii') + #debug(name,"refreshRedis key:{} value:{}".format(key,redisData[key])) + # Only update the TTLs + if 'bpm' in redisKeys: + redisData['bpm_pttl'] = r.pttl('bpm') + #debug(name,"refreshRedis key:bpm_ttl value:{}".format(redisData["bpm_pttl"])) + #debug(name,"redisData:{}".format(redisData)) + return True + +try: + while True: + refreshRedis() + start = time.time() + line = sys.stdin.readline() + if line == "": + time.sleep(0.01) + line = line.rstrip('\n') + pointsList = ast.literal_eval(line) + # Do the filter + for mode in modeList: + pointsList = locals()[mode](pointsList) + print( pointsList, flush=True ) + looptime = time.time() - start + # debug(name+" looptime:"+str(looptime)) + if( looptime < optimal_looptime ): + time.sleep( optimal_looptime - looptime) + # debug(name+" micro sleep:"+str( optimal_looptime - looptime)) +except EOFError: + debug(name+" break")# no more information + diff --git a/clitools/filters/redilysis_colors.py b/clitools/filters/redilysis_colors.py new file mode 100644 index 0000000..230fa08 --- /dev/null +++ b/clitools/filters/redilysis_colors.py @@ -0,0 +1,186 @@ + +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + + +''' + +redilysis colors +v0.1.0 + +A complex effect that depends on redis keys for audio analysis + +see https://git.interhacker.space/teamlase/redilysis for more informations +about the redilysis project + +LICENCE : CC + +by cocoa + + +''' +from __future__ import print_function +import argparse +import ast +import os +import math +import random +import redis +import sys +import time +name = "filters::redilysis_colors" + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) +def msNow(): + return time.time() + +# The list of available modes => redis keys each requires to run +oModeList = { + } + +def rgb2int(rgb): + return int('0x%02x%02x%02x' % tuple(rgb),0) + +def int2rgb(intcode): + #hexcode = hex(intcode)[2:] + hexcode = '{0:06X}'.format(intcode) + return tuple(int(hexcode[i:i+2], 16) for i in (0, 2, 4)) + #return tuple(map(ord,hexcode[1:].decode('hex'))) + + + +CHAOS = 1 +REDIS_FREQ = 100 + +# General Args +argsparser = argparse.ArgumentParser(description="Redilysis filter") +argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose") +# Redis Args +argsparser.add_argument("-i","--ip",help="IP address of the Redis server ",default="127.0.0.1",type=str) +argsparser.add_argument("-p","--port",help="Port of the Redis server ",default="6379",type=str) +argsparser.add_argument("-s","--redis-freq",help="Query Redis every x (in milliseconds). Default:{}".format(REDIS_FREQ),default=REDIS_FREQ,type=int) +# Modes And Common Modes Parameters +#argsparser.add_argument("-m","--modelist",required=False,help="Comma separated list of modes to use from: {}".format("i, ".join(oModeList.keys())),type=str) +argsparser.add_argument("-c","--chaos",help="How much disorder to bring. High value = More chaos. Default {}".format(CHAOS), default=CHAOS, type=float) + +args = argsparser.parse_args() +fps = args.fps +ip = args.ip +port = args.port +redisFreq = args.redis_freq / 1000 +verbose = args.verbose +chaos = float(args.chaos) +optimal_looptime = 1 / fps + +max_width = 800 +max_height = 800 + +redisKeys = ["rms","spectrum_10","spectrum_120"] + +debug(name,"Redis Keys:{}".format(redisKeys)) +redisData = {} +redisLastHit = msNow() - 99999 +r = redis.Redis( + host=ip, + port=port) + + +def refreshRedis(): + global redisData + for key in redisKeys: + try: + redisData[key] = ast.literal_eval(r.get(key).decode('ascii')) + except : + debug("Error when reading redis key '{}".format(key)) + +def gauss(x, mu, sigma): + return( math.exp(-math.pow((x-mu),2)/(2*math.pow(sigma,2))/math.sqrt(2*math.pi*math.pow(sigma,2)))) + + +spect10Correct = [ + + 6.0, + 1.5, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.8, + 0.6, + 0.5, + +] + +def default( pl ): + global redisData + spect = redisData["spectrum_10"] + debug(name, "spect:{}".format(spect)) + new_list = [] + + # We want to color points that are on left and right when high is strong + # i.e. the farther the distance from spectrum, the higher notes have influence + # power = 0-1 + # x = 800 spec[2]= 6.0 spec[7]=0.0 power=0.0 + # x = 0 spec[2]= 6.0 spec[7]=0.0 power=0.0 + # x = 0 spec[2]= 1.0 spec[7]=0.5 power=1.0 + + # dist 0 = 1 + # 400 - 400 : maxW/2 -x + # 399 = -1 : x - 400 + # 401 = 1 + # x = 400 spec[2]= 6.0 spec[7]=0.0 power=1.0 + # x = 400 spec[2]= 1.0 spec[7]=0.5 power=0.0 + + for i, point in enumerate(pl): + ocolor = pl[i][2] + if ocolor == 0 : + new_list.append(point) + continue + colorTuple = int2rgb(ocolor) + x = point[0] + dist = abs(x - max_width/2) + key = int(2* dist / max_width * 8) + power = spect[key] / spect10Correct[key] * chaos + color = [] + for i in colorTuple: + new_color = int(i * power) + if new_color > 255 : + new_color = 255 + if new_color < 0 : + new_color = 0 + color.append( new_color ) + color = rgb2int(tuple(color)) + + point[2] = color + new_list.append(point) + #debug(name,"x:{}\t dist:{}\t key:{}\t power:{}\t ocolor:{}\t color:{}".format(point[0], dist, key,power, ocolor, pl[i][2])) + debug( name,"rms_noise output:{}".format(new_list)) + return new_list + + +try: + while True: + refreshRedis() + start = time.time() + line = sys.stdin.readline() + if line == "": + time.sleep(0.01) + line = line.rstrip('\n') + pointsList = ast.literal_eval(line) + # Do the filter + pointsList = default(pointsList) + print( pointsList, flush=True ) + looptime = time.time() - start + # debug(name+" looptime:"+str(looptime)) + if( looptime < optimal_looptime ): + time.sleep( optimal_looptime - looptime) + # debug(name+" micro sleep:"+str( optimal_looptime - looptime)) +except EOFError: + debug(name+" break")# no more information + diff --git a/clitools/generators/.DS_Store b/clitools/generators/.DS_Store new file mode 100644 index 0000000..58a8651 Binary files /dev/null and b/clitools/generators/.DS_Store differ diff --git a/clitools/generators/159.gml b/clitools/generators/159.gml new file mode 100755 index 0000000..c121f21 --- /dev/null +++ b/clitools/generators/159.gml @@ -0,0 +1,2407 @@ + + +
+ + katsu + +
+ + + 117.000000 + -77.000000 + 0.810000 + + + 0.000000 + 0.000000 + 0.000000 + + + + + + 0.237305 + 0.406250 + 0.000000 + + + + 0.238266 + 0.403367 + 1.400000 + + + + 0.239250 + 0.399071 + 3.300000 + + + + 0.238950 + 0.393930 + 4.800000 + + + + 0.237823 + 0.387477 + 6.700000 + + + + 0.236545 + 0.380665 + 8.100000 + + + + 0.234818 + 0.372415 + 10.000000 + + + + 0.232324 + 0.363344 + 11.400000 + + + + 0.229294 + 0.353159 + 13.400000 + + + + 0.227202 + 0.345460 + 14.800000 + + + + 0.225002 + 0.338646 + 16.600000 + + + + 0.221592 + 0.334011 + 18.100000 + + + + 0.217400 + 0.330838 + 20.100000 + + + + 0.214784 + 0.331250 + 21.400000 + + + + 0.213434 + 0.333180 + 23.299999 + + + + 0.214631 + 0.333387 + 24.700001 + + + + 0.217572 + 0.333613 + 26.600000 + + + + 0.221165 + 0.337621 + 28.000000 + + + + 0.226135 + 0.344309 + 29.900000 + + + + 0.233033 + 0.350578 + 31.400000 + + + + 0.242032 + 0.357017 + 33.299999 + + + + 0.250983 + 0.362042 + 34.700001 + + + + 0.260938 + 0.366166 + 36.599998 + + + + 0.269995 + 0.367181 + 38.099998 + + + + 0.279334 + 0.366333 + 39.900002 + + + + 0.286610 + 0.364380 + 41.400002 + + + + 0.292839 + 0.361992 + 43.200001 + + + + 0.295499 + 0.360920 + 44.700001 + + + + 0.296439 + 0.361083 + 46.599998 + + + + 0.296743 + 0.363428 + 48.000000 + + + + 0.296226 + 0.366788 + 49.900002 + + + + 0.293507 + 0.368321 + 51.299999 + + + + 0.289128 + 0.370038 + 53.299999 + + + + 0.284667 + 0.375439 + 54.700001 + + + + 0.279292 + 0.383830 + 56.599998 + + + + 0.272978 + 0.392327 + 58.000000 + + + + 0.264724 + 0.402094 + 59.900002 + + + + 0.254888 + 0.412061 + 61.299999 + + + + 0.242979 + 0.421866 + 63.200001 + + + + 0.232024 + 0.425413 + 64.599998 + + + + 0.220943 + 0.425049 + 66.500000 + + + + 0.213157 + 0.421471 + 68.000000 + + + + 0.206600 + 0.415554 + 69.900002 + + + + 0.202100 + 0.408913 + 71.300003 + + + + 0.198550 + 0.402056 + 73.199997 + + + + 0.196690 + 0.396906 + 74.599998 + + + + 0.194517 + 0.386215 + 76.500000 + + + + 0.192082 + 0.372647 + 78.000000 + + + + 0.191367 + 0.363967 + 79.900002 + + + + 0.192626 + 0.350222 + 81.300003 + + + + 0.195742 + 0.331501 + 83.199997 + + + + 0.201127 + 0.314455 + 84.599998 + + + + 0.208935 + 0.298130 + 86.500000 + + + + 0.218118 + 0.289324 + 87.900002 + + + + 0.227836 + 0.284232 + 89.800003 + + + + 0.233098 + 0.282664 + 91.199997 + + + + 0.239028 + 0.283907 + 93.199997 + + + + 0.246120 + 0.286885 + 94.599998 + + + + 0.251493 + 0.290493 + 96.500000 + + + + 0.255933 + 0.294309 + 97.900002 + + + + 0.257817 + 0.296784 + 99.800003 + + + + 0.258481 + 0.300957 + 101.199997 + + + + 0.258513 + 0.306063 + 103.099998 + + + + 0.257754 + 0.308355 + 104.599998 + + + + 0.256641 + 0.308205 + 106.400002 + + + + 0.256132 + 0.304081 + 107.900002 + + + + 0.255947 + 0.298122 + 109.800003 + + + + 0.255520 + 0.294058 + 111.199997 + + + + 0.253853 + 0.287026 + 112.900002 + + + + 0.251582 + 0.276624 + 114.800003 + + + + 0.250550 + 0.264782 + 116.199997 + + + + 0.250176 + 0.252431 + 117.900002 + + + + 0.251030 + 0.243700 + 119.800003 + + + + 0.255387 + 0.226922 + 121.300003 + + + + 0.263554 + 0.202966 + 123.099998 + + + + 0.275911 + 0.180318 + 124.599998 + + + + 0.292870 + 0.157185 + 126.400002 + + + + 0.311563 + 0.140687 + 127.900002 + + + + 0.330837 + 0.127536 + 129.600006 + + + + 0.342601 + 0.121198 + 131.399994 + + + + 0.360462 + 0.115677 + 132.899994 + + + + 0.385381 + 0.110322 + 134.699997 + + + + 0.410884 + 0.108722 + 136.199997 + + + + 0.436238 + 0.108989 + 137.899994 + + + + 0.452571 + 0.111970 + 139.699997 + + + + 0.480148 + 0.124429 + 141.199997 + + + + 0.517819 + 0.145000 + 143.100006 + + + + 0.550556 + 0.168666 + 144.500000 + + + + 0.581781 + 0.196913 + 146.399994 + + + + 0.601696 + 0.223718 + 147.800003 + + + + 0.616049 + 0.249516 + 149.600006 + + + + 0.622164 + 0.264828 + 151.399994 + + + + 0.625057 + 0.287230 + 152.899994 + + + + 0.625694 + 0.316760 + 154.699997 + + + + 0.621193 + 0.341844 + 156.100006 + + + + 0.614264 + 0.364259 + 157.800003 + + + + 0.609128 + 0.376585 + 159.699997 + + + + 0.599180 + 0.392089 + 161.100006 + + + + 0.585311 + 0.411216 + 163.000000 + + + + 0.573383 + 0.425426 + 164.500000 + + + + 0.563464 + 0.436341 + 166.399994 + + + + 0.562424 + 0.438469 + 167.800003 + + + + 0.565697 + 0.436409 + 169.600006 + + + + 0.568010 + 0.434087 + 171.300003 + + + + 0.571403 + 0.428415 + 172.800003 + + + + 0.575579 + 0.419926 + 174.699997 + + + + 0.578187 + 0.411397 + 176.100006 + + + + 0.579963 + 0.403025 + 177.699997 + + + + 0.579101 + 0.397602 + 179.699997 + + + + 0.572211 + 0.388333 + 181.100006 + + + + 0.560337 + 0.376165 + 183.000000 + + + + 0.546285 + 0.367486 + 184.399994 + + + + 0.529320 + 0.361160 + 186.300003 + + + + 0.513171 + 0.360249 + 187.800003 + + + + 0.497612 + 0.361897 + 189.600006 + + + + 0.489072 + 0.362779 + 191.300003 + + + + 0.478694 + 0.363116 + 192.800003 + + + + 0.466592 + 0.365000 + 194.600006 + + + + 0.459726 + 0.373001 + 196.100006 + + + + 0.455583 + 0.383964 + 197.699997 + + + + 0.455467 + 0.389494 + 199.600006 + + + + 0.461772 + 0.393773 + 201.099991 + + + + 0.472622 + 0.397396 + 202.899994 + + + + 0.484001 + 0.397476 + 204.399994 + + + + 0.495359 + 0.395824 + 206.300003 + + + + 0.500694 + 0.396154 + 207.699997 + + + + 0.504116 + 0.401060 + 209.500000 + + + + 0.507142 + 0.407704 + 211.300003 + + + + 0.509173 + 0.409010 + 212.800003 + + + + 0.510549 + 0.408760 + 214.600006 + + + + 0.510377 + 0.413436 + 216.000000 + + + + 0.509656 + 0.423175 + 217.699997 + + + + 0.510039 + 0.437761 + 219.600006 + + + + 0.511007 + 0.454457 + 221.000000 + + + + 0.512445 + 0.465432 + 223.000000 + + + + 0.517009 + 0.484037 + 224.399994 + + + + 0.523108 + 0.506813 + 226.199997 + + + + 0.526299 + 0.519175 + 227.699997 + + + + 0.529222 + 0.532477 + 229.500000 + + + + 0.532194 + 0.546901 + 231.199997 + + + + 0.533497 + 0.553280 + 232.699997 + + + + 0.532559 + 0.554776 + 234.500000 + + + + 0.526393 + 0.551951 + 236.000000 + + + + 0.515800 + 0.545929 + 237.600006 + + + + 0.503077 + 0.537723 + 239.500000 + + + + 0.489564 + 0.528625 + 241.000000 + + + + 0.480308 + 0.523430 + 242.899994 + + + + 0.463340 + 0.516762 + 244.300003 + + + + 0.442252 + 0.509113 + 246.100006 + + + + 0.429044 + 0.505096 + 247.800003 + + + + 0.408866 + 0.501124 + 249.399994 + + + + 0.382391 + 0.497741 + 251.199997 + + + + 0.361137 + 0.499471 + 252.800003 + + + + 0.341117 + 0.505065 + 254.500000 + + + + 0.325356 + 0.513125 + 256.000000 + + + + 0.310059 + 0.523336 + 257.799988 + + + + 0.297048 + 0.532576 + 259.200012 + + + + 0.285151 + 0.541197 + 260.899994 + + + + 0.279039 + 0.544930 + 262.799988 + + + + 0.272821 + 0.546257 + 264.299988 + + + + 0.266187 + 0.546688 + 266.000000 + + + + 0.262526 + 0.545294 + 267.799988 + + + + 0.258181 + 0.538467 + 269.399994 + + + + 0.252331 + 0.527450 + 271.100006 + + + + 0.245771 + 0.516021 + 272.700012 + + + + 0.237181 + 0.502877 + 274.399994 + + + + 0.226215 + 0.489939 + 276.000000 + + + + 0.212237 + 0.476052 + 277.799988 + + + + 0.198120 + 0.465278 + 279.200012 + + + + 0.183146 + 0.455572 + 280.899994 + + + + 0.172635 + 0.449194 + 282.799988 + + + + 0.164410 + 0.444415 + 284.200012 + + + + 0.160954 + 0.442424 + 286.000000 + + + + 0.159557 + 0.441727 + 287.799988 + + + + 0.158342 + 0.441503 + 289.299988 + + + + 0.157638 + 0.441801 + 291.100006 + + + + 0.159560 + 0.443429 + 292.700012 + + + + 0.164166 + 0.445872 + 294.399994 + + + + 0.171964 + 0.447725 + 295.899994 + + + + 0.183510 + 0.449047 + 297.799988 + + + + 0.198072 + 0.448860 + 299.200012 + + + + 0.216656 + 0.446182 + 300.799988 + + + + 0.236024 + 0.437773 + 302.700012 + + + + 0.255533 + 0.426657 + 304.200012 + + + + 0.264077 + 0.421624 + 305.899994 + + + + 0.270242 + 0.416507 + 307.700012 + + + + 0.285242 + 0.401079 + 309.299988 + + + + 0.306944 + 0.377572 + 311.000000 + + + + 0.325403 + 0.355063 + 312.700012 + + + + 0.342980 + 0.330997 + 314.299988 + + + + 0.355254 + 0.310116 + 315.899994 + + + + 0.365175 + 0.288973 + 317.700012 + + + + 0.370345 + 0.271894 + 319.200012 + + + + 0.372310 + 0.256070 + 321.000000 + + + + 0.369063 + 0.246051 + 322.399994 + + + + 0.362007 + 0.238857 + 324.200012 + + + + 0.353067 + 0.235886 + 325.899994 + + + + 0.342262 + 0.235644 + 327.700012 + + + + 0.332780 + 0.238882 + 329.200012 + + + + 0.323099 + 0.245278 + 331.000000 + + + + 0.314945 + 0.254620 + 332.600006 + + + + 0.307750 + 0.266582 + 334.299988 + + + + 0.305395 + 0.277350 + 335.899994 + + + + 0.306559 + 0.288579 + 337.700012 + + + + 0.311927 + 0.298402 + 339.200012 + + + + 0.321557 + 0.307862 + 341.000000 + + + + 0.335558 + 0.313442 + 342.399994 + + + + 0.353981 + 0.316924 + 344.100006 + + + + 0.372326 + 0.317582 + 345.799988 + + + + 0.392738 + 0.316352 + 347.700012 + + + + 0.411319 + 0.313254 + 349.200012 + + + + 0.430198 + 0.308730 + 351.000000 + + + + 0.443847 + 0.304220 + 352.500000 + + + + 0.455290 + 0.300051 + 354.299988 + + + + 0.461868 + 0.299224 + 355.899994 + + + + 0.465540 + 0.300232 + 357.600006 + + + + 0.464523 + 0.301520 + 359.200012 + + + + 0.460185 + 0.303992 + 361.000000 + + + + 0.453440 + 0.309787 + 362.399994 + + + + 0.444113 + 0.319305 + 364.299988 + + + + 0.433524 + 0.332246 + 365.700012 + + + + 0.421915 + 0.346667 + 367.600006 + + + + 0.414575 + 0.353069 + 369.100006 + + + + 0.409007 + 0.357931 + 371.000000 + + + + 0.404972 + 0.370217 + 372.500000 + + + + 0.402204 + 0.385333 + 374.200012 + + + + 0.403513 + 0.388217 + 375.899994 + + + + 0.406782 + 0.383831 + 377.600006 + + + + 0.408335 + 0.377552 + 379.100006 + + + + 0.408907 + 0.369803 + 380.899994 + + + + 0.409096 + 0.362416 + 382.299988 + + + + 0.408545 + 0.355666 + 384.200012 + + + + 0.405816 + 0.354217 + 385.700012 + + + + 0.401068 + 0.355807 + 387.600006 + + + + 0.394958 + 0.358402 + 389.100006 + + + + 0.386951 + 0.361858 + 390.899994 + + + + 0.377958 + 0.365621 + 392.500000 + + + + 0.367797 + 0.369911 + 394.200012 + + + + 0.360106 + 0.373530 + 395.799988 + + + + 0.353720 + 0.376353 + 397.500000 + + + + 0.351009 + 0.375830 + 399.100006 + + + + 0.350846 + 0.372523 + 400.899963 + + + + 0.354108 + 0.366263 + 402.400024 + + + + 0.359899 + 0.357724 + 404.199982 + + + + 0.366497 + 0.349940 + 405.600037 + + + + 0.374134 + 0.342065 + 407.499969 + + + + 0.380713 + 0.336173 + 409.100006 + + + + 0.386883 + 0.331278 + 410.899994 + + + + 0.390289 + 0.329198 + 412.500000 + + + + 0.391362 + 0.328892 + 414.200012 + + + + 0.387780 + 0.330577 + 415.799988 + + + + 0.379452 + 0.335101 + 417.500000 + + + + 0.365258 + 0.345071 + 419.200012 + + + + 0.345925 + 0.359539 + 420.799988 + + + + 0.327136 + 0.373961 + 422.299988 + + + + 0.305583 + 0.391195 + 424.200012 + + + + 0.282553 + 0.411361 + 425.600006 + + + + 0.257187 + 0.434770 + 427.500000 + + + + 0.238864 + 0.453028 + 429.000000 + + + + 0.223230 + 0.469666 + 430.799988 + + + + 0.212653 + 0.481482 + 432.399994 + + + + 0.204680 + 0.490439 + 434.100006 + + + + 0.202330 + 0.491983 + 435.700012 + + + + 0.203973 + 0.488410 + 437.500000 + + + + 0.210313 + 0.480180 + 439.100006 + + + + 0.220521 + 0.468781 + 440.799988 + + + + 0.232309 + 0.458956 + 442.200012 + + + + 0.245988 + 0.449142 + 444.100006 + + + + 0.257461 + 0.440942 + 445.600006 + + + + 0.268508 + 0.433367 + 447.399994 + + + + 0.276488 + 0.429358 + 448.899994 + + + + 0.282595 + 0.427889 + 450.799988 + + + + 0.283685 + 0.430579 + 452.299988 + + + + 0.281773 + 0.436715 + 454.100006 + + + + 0.278308 + 0.445719 + 455.700012 + + + + 0.273276 + 0.458077 + 457.500000 + + + + 0.267071 + 0.472238 + 458.899994 + + + + 0.259583 + 0.488495 + 460.700012 + + + + 0.253053 + 0.501354 + 462.200012 + + + + 0.246716 + 0.512565 + 464.100006 + + + + 0.242491 + 0.518100 + 465.600006 + + + + 0.240103 + 0.519739 + 467.399994 + + + + 0.242347 + 0.515574 + 468.799988 + + + + 0.247951 + 0.507164 + 470.700012 + + + + 0.255252 + 0.496871 + 472.299988 + + + + 0.265144 + 0.483949 + 474.100006 + + + + 0.277446 + 0.470320 + 475.700012 + + + + 0.292135 + 0.455340 + 477.399994 + + + + 0.304081 + 0.444064 + 478.799988 + + + + 0.315483 + 0.434723 + 480.700012 + + + + 0.324345 + 0.430757 + 482.100006 + + + + 0.331603 + 0.429969 + 484.000000 + + + + 0.333213 + 0.432263 + 485.500000 + + + + 0.331672 + 0.437245 + 487.399994 + + + + 0.329092 + 0.444963 + 488.799988 + + + + 0.324664 + 0.456201 + 490.700012 + + + + 0.316508 + 0.470615 + 492.200012 + + + + 0.305199 + 0.488167 + 494.000000 + + + + 0.294439 + 0.503123 + 495.600006 + + + + 0.283174 + 0.517742 + 497.399994 + + + + 0.274307 + 0.528757 + 498.799988 + + + + 0.266672 + 0.537988 + 500.700012 + + + + 0.263409 + 0.541916 + 502.100006 + + + + 0.262681 + 0.543120 + 504.000000 + + + + 0.264231 + 0.542744 + 505.399994 + + + + 0.268529 + 0.539429 + 507.399994 + + + + 0.277662 + 0.528517 + 508.700012 + + + + 0.290994 + 0.510818 + 510.600006 + + + + 0.304901 + 0.489955 + 512.099976 + + + + 0.319566 + 0.467018 + 514.000000 + + + + 0.328959 + 0.453192 + 515.599976 + + + + 0.336279 + 0.444050 + 517.299988 + + + + 0.341794 + 0.440344 + 518.799988 + + + + 0.347328 + 0.439062 + 520.599976 + + + + 0.352925 + 0.438652 + 522.099976 + + + + 0.358917 + 0.438528 + 523.900024 + + + + 0.363028 + 0.438491 + 525.400024 + + + + 0.366784 + 0.438481 + 527.400024 + + + + 0.370847 + 0.438478 + 528.900024 + + + + 0.375878 + 0.439026 + 530.599976 + + + + 0.381314 + 0.441498 + 532.000000 + + + + 0.386464 + 0.444897 + 533.900024 + + + + 0.386990 + 0.446442 + 535.500000 + + + + 0.384857 + 0.447978 + 537.299988 + + + + 0.382292 + 0.452556 + 538.900024 + + + + 0.378701 + 0.460060 + 540.500000 + + + + 0.373160 + 0.469140 + 542.099976 + + + + 0.365731 + 0.479986 + 543.900024 + + + + 0.358456 + 0.489480 + 545.299988 + + + + 0.351377 + 0.498188 + 547.200012 + + + + 0.348293 + 0.501949 + 548.700012 + + + + 0.347986 + 0.501881 + 550.599976 + + + + 0.351199 + 0.495997 + 552.000000 + + + + 0.357159 + 0.485918 + 553.900024 + + + + 0.364576 + 0.475143 + 555.400024 + + + + 0.373101 + 0.463488 + 557.200012 + + + + 0.379096 + 0.455161 + 558.700012 + + + + 0.384186 + 0.448551 + 560.599976 + + + + 0.388065 + 0.445767 + 562.000000 + + + + 0.391945 + 0.444605 + 563.900024 + + + + 0.395839 + 0.443465 + 565.299988 + + + + 0.400169 + 0.442419 + 567.200012 + + + + 0.403799 + 0.442699 + 568.599976 + + + + 0.407846 + 0.444004 + 570.599976 + + + + 0.412816 + 0.446104 + 572.000000 + + + + 0.419199 + 0.449147 + 573.900024 + + + + 0.425986 + 0.452979 + 575.299988 + + + + 0.432459 + 0.457329 + 577.200012 + + + + 0.433563 + 0.459997 + 578.599976 + + + + 0.431635 + 0.462402 + 580.500000 + + + + 0.429137 + 0.465879 + 582.000000 + + + + 0.425567 + 0.471068 + 583.799988 + + + + 0.420032 + 0.478084 + 585.299988 + + + + 0.412971 + 0.487249 + 587.200012 + + + + 0.407346 + 0.496765 + 588.599976 + + + + 0.402533 + 0.506320 + 590.500000 + + + + 0.400478 + 0.510503 + 591.900024 + + + + 0.400361 + 0.511448 + 593.799988 + + + + 0.402873 + 0.509462 + 595.200012 + + + + 0.407921 + 0.504809 + 597.099976 + + + + 0.415658 + 0.496991 + 598.599976 + + + + 0.425742 + 0.486598 + 600.500000 + + + + 0.434939 + 0.477252 + 601.900024 + + + + 0.444523 + 0.468223 + 603.799988 + + + + 0.452646 + 0.462828 + 605.299988 + + + + 0.459832 + 0.459413 + 607.099976 + + + + 0.462184 + 0.458776 + 608.599976 + + + + 0.461997 + 0.460562 + 610.400024 + + + + 0.461023 + 0.466134 + 611.900024 + + + + 0.459022 + 0.476376 + 613.900024 + + + + 0.454908 + 0.492869 + 615.200012 + + + + 0.449254 + 0.515733 + 617.099976 + + + + 0.444486 + 0.540295 + 618.599976 + + + + 0.440625 + 0.567259 + 620.400024 + + + + 0.440457 + 0.587020 + 621.900024 + + + + 0.443666 + 0.604370 + 623.799988 + + + + 0.451628 + 0.617181 + 625.200012 + + + + 0.463893 + 0.627507 + 627.099976 + + + + 0.478073 + 0.630404 + 628.599976 + + + + 0.495688 + 0.626996 + 630.599976 + + + + 0.514596 + 0.614125 + 631.799988 + + + + 0.536437 + 0.592826 + 633.799988 + + + + 0.556186 + 0.566624 + 635.200012 + + + + 0.574705 + 0.538497 + 637.099976 + + + + 0.584683 + 0.521737 + 638.500000 + + + + 0.596327 + 0.497782 + 640.400024 + + + + 0.609338 + 0.469517 + 641.900024 + + + + 0.616886 + 0.450656 + 643.700012 + + + + 0.626927 + 0.418090 + 645.200012 + + + + 0.638739 + 0.374543 + 647.099976 + + + + 0.644749 + 0.340645 + 648.500000 + + + + 0.647749 + 0.312258 + 650.400024 + + + + 0.648873 + 0.300183 + 651.799988 + + + + 0.649248 + 0.295919 + 653.700012 + + + + 0.649365 + 0.294543 + 655.099976 + + + + 0.649400 + 0.294121 + 657.000000 + + + + 0.649410 + 0.293995 + 658.500000 + + + + +
+
diff --git a/clitools/generators/160.gml b/clitools/generators/160.gml new file mode 100755 index 0000000..ec75b4d --- /dev/null +++ b/clitools/generators/160.gml @@ -0,0 +1,2791 @@ + + +
+ + katsu-2f + +
+ + + -390.000000 + -32.000000 + 0.419999 + + + 27.000000 + 1.000000 + 0.000000 + + + + + + 0.147498 + 0.219538 + 0.000000 + + + + 0.147101 + 0.212942 + 4.800000 + + + + 0.146946 + 0.207503 + 8.100000 + + + + 0.146896 + 0.203511 + 11.700000 + + + + 0.146724 + 0.200267 + 14.900000 + + + + 0.145868 + 0.198226 + 21.400000 + + + + 0.144555 + 0.198876 + 37.400002 + + + + 0.144007 + 0.204583 + 40.700001 + + + + 0.143825 + 0.218993 + 43.799999 + + + + 0.144083 + 0.244724 + 46.799999 + + + + 0.145928 + 0.283239 + 50.000000 + + + + 0.149557 + 0.334128 + 55.200001 + + + + 0.153129 + 0.393071 + 58.299999 + + + + 0.157055 + 0.452832 + 61.500000 + + + + 0.160675 + 0.504902 + 64.699997 + + + + 0.162888 + 0.543471 + 68.000000 + + + + 0.164641 + 0.566973 + 70.800003 + + + + 0.165319 + 0.577339 + 74.099998 + + + + 0.165380 + 0.577548 + 77.500000 + + + + 0.164436 + 0.569779 + 80.699997 + + + + 0.162290 + 0.551430 + 83.800003 + + + + 0.160746 + 0.519498 + 86.900002 + + + + 0.162346 + 0.474171 + 90.099998 + + + + 0.168769 + 0.419902 + 95.099998 + + + + 0.179235 + 0.363529 + 96.599998 + + + + 0.191592 + 0.310718 + 101.600006 + + + + 0.203349 + 0.266803 + 104.599998 + + + + 0.212709 + 0.234213 + 107.800003 + + + + 0.221958 + 0.211743 + 111.099998 + + + + 0.230964 + 0.197793 + 114.500000 + + + + 0.238148 + 0.190139 + 117.699997 + + + + 0.241712 + 0.187392 + 121.199997 + + + + 0.241863 + 0.188725 + 127.599998 + + + + 0.238471 + 0.193682 + 131.100006 + + + + 0.230219 + 0.204596 + 134.399994 + + + + 0.215112 + 0.223009 + 137.600006 + + + + 0.190758 + 0.248874 + 140.899994 + + + + 0.160085 + 0.277562 + 144.000000 + + + + 0.137657 + 0.299022 + 147.199997 + + + + 0.128785 + 0.309744 + 150.600006 + + + + 0.133414 + 0.311778 + 154.100006 + + + + 0.155941 + 0.309188 + 157.199997 + + + + 0.193346 + 0.305108 + 160.500000 + + + + 0.228740 + 0.299964 + 163.800003 + + + + 0.253422 + 0.295978 + 167.199997 + + + + 0.264634 + 0.294442 + 170.399994 + + + + 0.266414 + 0.294263 + 175.100006 + + + + 0.263061 + 0.296915 + 178.300003 + + + + 0.252713 + 0.307180 + 188.000000 + + + + 0.232599 + 0.328229 + 191.399994 + + + + 0.204065 + 0.355782 + 194.500000 + + + + 0.172543 + 0.382441 + 197.800003 + + + + 0.149473 + 0.403684 + 201.000000 + + + + 0.140979 + 0.415348 + 204.199997 + + + + 0.147685 + 0.417817 + 207.199997 + + + + 0.170972 + 0.415219 + 210.300003 + + + + 0.207678 + 0.410333 + 213.600006 + + + + 0.241156 + 0.403733 + 218.399994 + + + + 0.264855 + 0.398507 + 220.199997 + + + + 0.276658 + 0.396329 + 224.699997 + + + + 0.278753 + 0.396704 + 227.899994 + + + + 0.275512 + 0.399013 + 231.399994 + + + + 0.265043 + 0.409096 + 240.699997 + + + + 0.244400 + 0.431799 + 244.000000 + + + + 0.215947 + 0.461355 + 247.199997 + + + + 0.184644 + 0.490551 + 250.399994 + + + + 0.158855 + 0.515649 + 255.300003 + + + + 0.147252 + 0.530952 + 258.600006 + + + + 0.150665 + 0.537521 + 260.200012 + + + + 0.170124 + 0.539555 + 265.299988 + + + + 0.203710 + 0.539361 + 268.500000 + + + + 0.237539 + 0.538235 + 270.200012 + + + + 0.263426 + 0.537737 + 275.000000 + + + + 0.276508 + 0.537570 + 278.100006 + + + + 0.280789 + 0.537051 + 281.399994 + + + + 0.282061 + 0.533388 + 291.299988 + + + + 0.280388 + 0.521766 + 294.600006 + + + + 0.276759 + 0.498281 + 297.799988 + + + + 0.271772 + 0.458763 + 301.299988 + + + + 0.267678 + 0.405435 + 304.100006 + + + + 0.265460 + 0.349976 + 307.399994 + + + + 0.264354 + 0.299561 + 310.500000 + + + + 0.263968 + 0.259491 + 313.700012 + + + + 0.261974 + 0.226793 + 317.000000 + + + + 0.259377 + 0.203145 + 321.799988 + + + + 0.257215 + 0.188513 + 323.500000 + + + + 0.255590 + 0.179361 + 328.299988 + + + + 0.253874 + 0.172849 + 331.600006 + + + + 0.252245 + 0.170845 + 334.700012 + + + + 0.250191 + 0.177144 + 344.500000 + + + + 0.251849 + 0.191016 + 347.899994 + + + + 0.264729 + 0.207100 + 350.799988 + + + + 0.286535 + 0.215832 + 354.100006 + + + + 0.305704 + 0.211457 + 357.700012 + + + + 0.322146 + 0.198270 + 360.600006 + + + + 0.332962 + 0.184742 + 363.899994 + + + + 0.337163 + 0.177908 + 367.000000 + + + + 0.338044 + 0.180978 + 371.899994 + + + + 0.341200 + 0.195514 + 375.299988 + + + + 0.348981 + 0.221432 + 378.700012 + + + + 0.358888 + 0.249712 + 381.799988 + + + + 0.368269 + 0.268955 + 383.500000 + + + + 0.375277 + 0.277451 + 388.299988 + + + + 0.376711 + 0.274843 + 391.600006 + + + + 0.370444 + 0.264650 + 394.700012 + + + + 0.354739 + 0.251913 + 397.899994 + + + + 0.330954 + 0.239503 + 401.100037 + + + + 0.305098 + 0.234869 + 404.400024 + + + + 0.278715 + 0.242935 + 407.799988 + + + + 0.257355 + 0.258671 + 411.299988 + + + + 0.247030 + 0.276599 + 414.799988 + + + + 0.249702 + 0.292942 + 418.100006 + + + + 0.269077 + 0.306275 + 421.299988 + + + + 0.298912 + 0.313020 + 424.500000 + + + + 0.324806 + 0.311014 + 427.700012 + + + + 0.347155 + 0.301181 + 431.200012 + + + + 0.364667 + 0.289043 + 434.299988 + + + + 0.371274 + 0.284698 + 437.600006 + + + + 0.369309 + 0.291631 + 440.600006 + + + + 0.366688 + 0.310593 + 443.799988 + + + + 0.369982 + 0.336748 + 447.000000 + + + + 0.377064 + 0.359132 + 451.799988 + + + + 0.382586 + 0.371178 + 453.399994 + + + + 0.384248 + 0.372355 + 458.200012 + + + + 0.379720 + 0.364313 + 461.500000 + + + + 0.368068 + 0.349230 + 464.700012 + + + + 0.350863 + 0.332844 + 467.899994 + + + + 0.330266 + 0.323100 + 471.000000 + + + + 0.306545 + 0.322503 + 474.200012 + + + + 0.279713 + 0.334284 + 477.500000 + + + + 0.258044 + 0.354927 + 480.799988 + + + + 0.251509 + 0.375972 + 485.399994 + + + + 0.259964 + 0.394879 + 487.000000 + + + + 0.283940 + 0.408116 + 491.899994 + + + + 0.315556 + 0.415470 + 495.200012 + + + + 0.343272 + 0.413118 + 498.600006 + + + + 0.364788 + 0.402943 + 502.000000 + + + + 0.378398 + 0.391449 + 505.000000 + + + + 0.383290 + 0.386474 + 508.299988 + + + + 0.382467 + 0.390747 + 511.399994 + + + + 0.381383 + 0.405106 + 514.599976 + + + + 0.385468 + 0.429323 + 518.000000 + + + + 0.393922 + 0.458409 + 521.099976 + + + + 0.402835 + 0.479974 + 524.400024 + + + + 0.409548 + 0.489318 + 527.599976 + + + + 0.409624 + 0.485882 + 530.799988 + + + + 0.399947 + 0.472618 + 534.000000 + + + + 0.382151 + 0.456040 + 537.299988 + + + + 0.359939 + 0.441743 + 540.500000 + + + + 0.335052 + 0.437076 + 545.400024 + + + + 0.308402 + 0.446628 + 548.500000 + + + + 0.282966 + 0.466629 + 551.799988 + + + + 0.268008 + 0.490933 + 554.900024 + + + + 0.264737 + 0.518682 + 558.000000 + + + + 0.277294 + 0.545423 + 561.200012 + + + + 0.304358 + 0.563725 + 564.599976 + + + + 0.336221 + 0.565624 + 567.900024 + + + + 0.365144 + 0.554470 + 571.099976 + + + + 0.385629 + 0.537324 + 574.299988 + + + + 0.395120 + 0.520635 + 577.400024 + + + + 0.397741 + 0.509295 + 580.700012 + + + + 0.398446 + 0.504870 + 585.500000 + + + + 0.400507 + 0.501883 + 595.200012 + + + + 0.403275 + 0.494354 + 598.299988 + + + + 0.405980 + 0.476584 + 601.599976 + + + + 0.407215 + 0.447245 + 604.700012 + + + + 0.406229 + 0.409150 + 608.000000 + + + + 0.402911 + 0.365193 + 611.099976 + + + + 0.395999 + 0.320598 + 614.400024 + + + + 0.387898 + 0.283510 + 617.599976 + + + + 0.378035 + 0.255143 + 620.799988 + + + + 0.364471 + 0.234389 + 625.500000 + + + + 0.348860 + 0.220218 + 627.200012 + + + + 0.333607 + 0.213004 + 630.900024 + + + + 0.323149 + 0.210466 + 635.500000 + + + + 0.319674 + 0.209528 + 637.200012 + + + + 0.324884 + 0.208452 + 641.000000 + + + + 0.344589 + 0.207077 + 644.299988 + + + + 0.381633 + 0.206356 + 647.400024 + + + + 0.428256 + 0.205475 + 650.599976 + + + + 0.474102 + 0.204958 + 655.599976 + + + + 0.505527 + 0.205708 + 658.799988 + + + + 0.516439 + 0.205758 + 661.799988 + + + + 0.508215 + 0.203712 + 665.000000 + + + + 0.485687 + 0.198101 + 668.500000 + + + + 0.458773 + 0.190210 + 671.700012 + + + + 0.437007 + 0.182408 + 674.900024 + + + + 0.424183 + 0.176636 + 678.299988 + + + + 0.419345 + 0.173647 + 681.599976 + + + + 0.418746 + 0.173684 + 684.799988 + + + + 0.418562 + 0.176975 + 694.599976 + + + + 0.417728 + 0.184705 + 697.799988 + + + + 0.416737 + 0.200831 + 701.099976 + + + + 0.418364 + 0.231608 + 704.200012 + + + + 0.423521 + 0.276369 + 707.400024 + + + + 0.429823 + 0.327786 + 710.599976 + + + + 0.435994 + 0.385716 + 715.400024 + + + + 0.443300 + 0.443433 + 718.599976 + + + + 0.450654 + 0.494634 + 721.799988 + + + + 0.457348 + 0.534476 + 725.000000 + + + + 0.461711 + 0.558751 + 728.200012 + + + + 0.464278 + 0.568464 + 731.599976 + + + + 0.465058 + 0.568496 + 738.000000 + + + + 0.464499 + 0.562244 + 741.500000 + + + + 0.463116 + 0.550366 + 744.900024 + + + + 0.461115 + 0.531740 + 748.299988 + + + + 0.455752 + 0.504265 + 751.299988 + + + + 0.446720 + 0.470332 + 754.500000 + + + + 0.436830 + 0.434405 + 758.000000 + + + + 0.427988 + 0.400716 + 761.400024 + + + + 0.421847 + 0.373602 + 764.700012 + + + + 0.416401 + 0.354023 + 768.000000 + + + + 0.410507 + 0.340942 + 771.299988 + + + + 0.402502 + 0.332598 + 774.500000 + + + + 0.393569 + 0.327622 + 778.000000 + + + + 0.384325 + 0.325644 + 781.200012 + + + + 0.375299 + 0.324378 + 784.700012 + + + + 0.368262 + 0.323594 + 787.700012 + + + + 0.366172 + 0.323617 + 790.900024 + + + + 0.372145 + 0.323510 + 799.000000 + + + + 0.389782 + 0.324242 + 800.599976 + + + + 0.417888 + 0.323640 + 805.400024 + + + + 0.452310 + 0.321457 + 808.700012 + + + + 0.488739 + 0.318786 + 812.199951 + + + + 0.514817 + 0.316756 + 815.499939 + + + + 0.525668 + 0.316132 + 818.700073 + + + + 0.525497 + 0.316734 + 822.000000 + + + + 0.520815 + 0.318286 + 825.299988 + + + + 0.512779 + 0.321455 + 828.700012 + + + + 0.499554 + 0.330459 + 831.799988 + + + + 0.480254 + 0.346696 + 835.099976 + + + + 0.457230 + 0.366834 + 838.500000 + + + + 0.435334 + 0.385397 + 841.599976 + + + + 0.417258 + 0.398212 + 844.799988 + + + + 0.404593 + 0.407738 + 848.000000 + + + + 0.394486 + 0.414978 + 851.200012 + + + + 0.387133 + 0.420416 + 854.799988 + + + + 0.384329 + 0.423438 + 858.099976 + + + + 0.386247 + 0.424527 + 861.299988 + + + + 0.395220 + 0.424865 + 867.799988 + + + + 0.415724 + 0.424963 + 870.900024 + + + + 0.446690 + 0.424990 + 874.099976 + + + + 0.483037 + 0.425154 + 878.900024 + + + + 0.513626 + 0.426007 + 882.200012 + + + + 0.530573 + 0.427008 + 885.500000 + + + + 0.536514 + 0.425072 + 891.900024 + + + + 0.539590 + 0.417120 + 898.299988 + + + + 0.542084 + 0.400982 + 901.900024 + + + + 0.544145 + 0.375992 + 905.200012 + + + + 0.544324 + 0.343366 + 908.700012 + + + + 0.546176 + 0.307746 + 911.900024 + + + + 0.550750 + 0.274911 + 915.099976 + + + + 0.556467 + 0.247385 + 918.299988 + + + + 0.562560 + 0.226640 + 921.900024 + + + + 0.568610 + 0.213369 + 925.099976 + + + + 0.573840 + 0.206559 + 928.299988 + + + + 0.577922 + 0.204314 + 932.000000 + + + + 0.581998 + 0.204738 + 935.200012 + + + + 0.585659 + 0.207542 + 938.400024 + + + + 0.587884 + 0.210501 + 941.599976 + + + + 0.589484 + 0.210442 + 944.900024 + + + + 0.589155 + 0.206517 + 952.200012 + + + + 0.586739 + 0.198973 + 955.500000 + + + + 0.581450 + 0.189525 + 958.799988 + + + + 0.570753 + 0.180337 + 962.000000 + + + + 0.554798 + 0.175269 + 965.299988 + + + + 0.536278 + 0.176573 + 968.400024 + + + + 0.517960 + 0.185382 + 971.700012 + + + + 0.504393 + 0.202094 + 974.900024 + + + + 0.497798 + 0.226673 + 978.700012 + + + + 0.498790 + 0.251586 + 982.000000 + + + + 0.508159 + 0.266724 + 985.099976 + + + + 0.524045 + 0.272603 + 988.700012 + + + + 0.540881 + 0.268866 + 992.000000 + + + + 0.554694 + 0.257203 + 995.099976 + + + + 0.568662 + 0.241204 + 998.400024 + + + + 0.586445 + 0.224275 + 1001.599976 + + + + 0.607029 + 0.214020 + 1004.799988 + + + + 0.628998 + 0.214627 + 1008.099976 + + + + 0.649572 + 0.225100 + 1011.299988 + + + + 0.663201 + 0.244939 + 1014.599976 + + + + 0.665337 + 0.270374 + 1017.799988 + + + + 0.654835 + 0.294422 + 1021.000000 + + + + 0.636253 + 0.310065 + 1025.800049 + + + + 0.613572 + 0.316058 + 1029.000000 + + + + 0.592576 + 0.311410 + 1032.300049 + + + + 0.576137 + 0.297925 + 1035.599976 + + + + 0.566508 + 0.282871 + 1038.699951 + + + + 0.564403 + 0.274740 + 1041.900024 + + + + 0.570343 + 0.280583 + 1048.400024 + + + + 0.581247 + 0.298306 + 1051.599976 + + + + 0.591481 + 0.320678 + 1054.800049 + + + + 0.599135 + 0.340327 + 1058.000000 + + + + 0.604412 + 0.351957 + 1061.199951 + + + + 0.605622 + 0.351208 + 1064.400024 + + + + 0.602778 + 0.339352 + 1067.699951 + + + + 0.597219 + 0.322804 + 1070.900024 + + + + 0.586266 + 0.306888 + 1075.699951 + + + + 0.569584 + 0.293991 + 1077.500000 + + + + 0.550593 + 0.289608 + 1082.300049 + + + + 0.532725 + 0.297187 + 1085.500000 + + + + 0.517822 + 0.312066 + 1088.699951 + + + + 0.508728 + 0.331508 + 1091.900024 + + + + 0.508349 + 0.357325 + 1095.199951 + + + + 0.515664 + 0.380783 + 1098.300049 + + + + 0.528819 + 0.394893 + 1101.599976 + + + + 0.544529 + 0.398818 + 1104.800049 + + + + 0.557503 + 0.392380 + 1107.900024 + + + + 0.568650 + 0.379443 + 1111.099976 + + + + 0.581767 + 0.362380 + 1114.500000 + + + + 0.599613 + 0.344568 + 1117.699951 + + + + 0.618550 + 0.333499 + 1122.500000 + + + + 0.635398 + 0.332690 + 1126.099976 + + + + 0.651876 + 0.343348 + 1128.199951 + + + + 0.664543 + 0.362404 + 1132.000000 + + + + 0.669732 + 0.385758 + 1136.000000 + + + + 0.665965 + 0.407815 + 1138.000000 + + + + 0.655260 + 0.421885 + 1142.000000 + + + + 0.641100 + 0.426645 + 1145.900024 + + + + 0.623389 + 0.423083 + 1147.900024 + + + + 0.605345 + 0.412180 + 1151.900024 + + + + 0.591861 + 0.398389 + 1155.900024 + + + + 0.584977 + 0.387573 + 1159.900024 + + + + 0.583646 + 0.382926 + 1161.900024 + + + + 0.586361 + 0.386844 + 1165.900024 + + + + 0.592024 + 0.400871 + 1170.000000 + + + + 0.598187 + 0.422768 + 1172.099976 + + + + 0.602552 + 0.444890 + 1176.099976 + + + + 0.605770 + 0.459687 + 1180.000000 + + + + 0.607000 + 0.461333 + 1182.000000 + + + + 0.605205 + 0.452585 + 1186.000000 + + + + 0.600118 + 0.437472 + 1190.000000 + + + + 0.589793 + 0.419546 + 1192.000000 + + + + 0.576026 + 0.405203 + 1196.000000 + + + + 0.562378 + 0.399333 + 1200.099976 + + + + 0.546925 + 0.403189 + 1202.199951 + + + + 0.530029 + 0.416156 + 1206.199951 + + + + 0.517886 + 0.436955 + 1208.300049 + + + + 0.513735 + 0.464572 + 1212.400024 + + + + 0.517474 + 0.491428 + 1216.400024 + + + + 0.528895 + 0.509789 + 1218.500000 + + + + 0.545723 + 0.517334 + 1222.500000 + + + + 0.563818 + 0.513873 + 1226.500000 + + + + 0.580026 + 0.502408 + 1228.599976 + + + + 0.596883 + 0.485546 + 1232.800049 + + + + 0.617096 + 0.468063 + 1234.900024 + + + + 0.636912 + 0.456976 + 1239.099976 + + + + 0.653880 + 0.457392 + 1241.199951 + + + + 0.669432 + 0.470322 + 1245.300049 + + + + 0.679957 + 0.493150 + 1249.300049 + + + + 0.683290 + 0.520121 + 1251.500000 + + + + 0.678327 + 0.546741 + 1255.599976 + + + + 0.664051 + 0.567840 + 1259.800049 + + + + 0.641157 + 0.580379 + 1261.900024 + + + + 0.612037 + 0.582622 + 1266.099976 + + + + 0.583202 + 0.576130 + 1268.300049 + + + + 0.561058 + 0.562159 + 1272.500000 + + + + 0.550816 + 0.545103 + 1274.599976 + + + + 0.549971 + 0.530313 + 1278.900024 + + + + 0.552603 + 0.521073 + 1283.199951 + + + + 0.554454 + 0.518592 + 1285.400024 + + + + 0.557187 + 0.515750 + 1289.699951 + + + + 0.565511 + 0.504439 + 1295.599976 + + + + 0.581152 + 0.483085 + 1300.099976 + + + + 0.606212 + 0.451072 + 1302.599976 + + + + 0.633582 + 0.414297 + 1304.900024 + + + + 0.653239 + 0.379192 + 1309.199951 + + + + 0.665091 + 0.347578 + 1311.500000 + + + + 0.667723 + 0.320888 + 1315.800049 + + + + 0.665502 + 0.296552 + 1320.099976 + + + + 0.662496 + 0.273029 + 1322.300049 + + + + 0.659536 + 0.251629 + 1326.599976 + + + + 0.657260 + 0.233176 + 1328.800049 + + + + 0.655603 + 0.218285 + 1333.300049 + + + + 0.654033 + 0.205944 + 1335.500000 + + + + 0.653254 + 0.193948 + 1339.900024 + + + + 0.652200 + 0.184011 + 1342.099976 + + + + 0.650984 + 0.179514 + 1346.500000 + + + + 0.651583 + 0.182362 + 1348.800049 + + + + 0.654725 + 0.194181 + 1353.199951 + + + + 0.659547 + 0.217951 + 1355.500000 + + + + 0.663591 + 0.250556 + 1359.900024 + + + + 0.665951 + 0.280603 + 1362.099976 + + + + 0.667748 + 0.301399 + 1366.400024 + + + + 0.668439 + 0.313631 + 1368.599976 + + + + 0.669129 + 0.318251 + 1373.000000 + + + + 0.672842 + 0.318625 + 1375.199951 + + + + 0.684166 + 0.317937 + 1386.199951 + + + + 0.704545 + 0.317297 + 1388.500000 + + + + 0.732368 + 0.317199 + 1392.900024 + + + + 0.762776 + 0.316406 + 1395.099976 + + + + 0.786474 + 0.316835 + 1399.599976 + + + + 0.798431 + 0.317537 + 1401.800049 + + + + 0.800595 + 0.314393 + 1408.400024 + + + + 0.798315 + 0.303256 + 1412.900024 + + + + 0.794056 + 0.283246 + 1415.199951 + + + + 0.788816 + 0.257298 + 1419.599976 + + + + 0.782921 + 0.230216 + 1421.900024 + + + + 0.778655 + 0.207764 + 1426.400024 + + + + 0.775311 + 0.193858 + 1428.699951 + + + + 0.773238 + 0.190285 + 1433.199951 + + + + 0.773410 + 0.197498 + 1435.699951 + + + + 0.775310 + 0.216704 + 1439.300049 + + + + 0.777407 + 0.248267 + 1442.599976 + + + + 0.780755 + 0.289166 + 1445.900024 + + + + 0.789189 + 0.333961 + 1449.199951 + + + + 0.800941 + 0.377074 + 1452.500000 + + + + 0.809557 + 0.410329 + 1455.800049 + + + + 0.814410 + 0.432982 + 1459.099976 + + + + 0.816319 + 0.444169 + 1462.500000 + + + + 0.816154 + 0.447289 + 1465.900024 + + + + 0.816609 + 0.447197 + 1469.199951 + + + + 0.816510 + 0.446843 + 1475.800049 + + + + 0.812068 + 0.445907 + 1479.099976 + + + + 0.800386 + 0.444410 + 1482.400024 + + + + 0.780039 + 0.443002 + 1485.699951 + + + + 0.753186 + 0.441818 + 1489.099976 + + + + 0.724619 + 0.443230 + 1492.400024 + + + + 0.700763 + 0.447363 + 1495.800049 + + + + 0.685206 + 0.451027 + 1499.099976 + + + + 0.679398 + 0.452469 + 1502.500000 + + + + 0.678969 + 0.452466 + 1505.800049 + + + + 0.678285 + 0.449268 + 1509.100098 + + + + 0.675342 + 0.439860 + 1512.599976 + + + + 0.673114 + 0.423442 + 1515.899902 + + + + 0.671947 + 0.400587 + 1519.300049 + + + + 0.669814 + 0.374874 + 1522.699951 + + + + 0.666257 + 0.352257 + 1526.100098 + + + + 0.663825 + 0.339772 + 1529.499878 + + + + 0.663065 + 0.339035 + 1532.900024 + + + + 0.663938 + 0.349327 + 1536.300049 + + + + 0.667181 + 0.372746 + 1538.000000 + + + + 0.672035 + 0.406363 + 1542.999878 + + + + 0.676244 + 0.448289 + 1546.499878 + + + + 0.679614 + 0.494068 + 1548.199951 + + + + 0.683577 + 0.531947 + 1551.699951 + + + + 0.686129 + 0.560923 + 1555.099976 + + + + 0.687079 + 0.580349 + 1558.500000 + + + + 0.687379 + 0.590142 + 1561.900024 + + + + 0.687467 + 0.594358 + 1565.400024 + + + + 0.687493 + 0.597168 + 1579.099854 + + + + 0.687655 + 0.599945 + 1582.599854 + + + + 0.688820 + 0.601836 + 1586.000000 + + + + 0.693243 + 0.601767 + 1598.000000 + + + + 0.705331 + 0.600703 + 1603.099854 + + + + 0.727112 + 0.600228 + 1606.600220 + + + + 0.754746 + 0.600224 + 1608.400024 + + + + 0.786093 + 0.601027 + 1611.899902 + + + + 0.813192 + 0.602482 + 1615.499878 + + + + 0.830850 + 0.603878 + 1618.199951 + + + + 0.838535 + 0.605215 + 1622.900024 + + + + 0.840017 + 0.604340 + 1625.400146 + + + + 0.839206 + 0.598724 + 1637.400146 + + + + 0.835954 + 0.585459 + 1640.000244 + + + + 0.830625 + 0.564145 + 1642.500000 + + + + 0.823858 + 0.538030 + 1647.400024 + + + + 0.816393 + 0.510937 + 1649.900024 + + + + 0.809934 + 0.485051 + 1652.399780 + + + + 0.805526 + 0.467726 + 1657.300171 + + + + 0.803709 + 0.462162 + 1659.800049 + + + + 0.802018 + 0.463961 + 1667.199951 + + + + +
+
diff --git a/clitools/generators/OSC3.py b/clitools/generators/OSC3.py new file mode 100644 index 0000000..f0df277 --- /dev/null +++ b/clitools/generators/OSC3.py @@ -0,0 +1,2873 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +""" +March 2015: + Python 3 version tested in Blender and simpleOSC with twisted + +This module contains an OpenSoundControl implementation (in Pure Python), based +(somewhat) on the good old 'SimpleOSC' implementation by Daniel Holth & Clinton +McChesney. + +This implementation is intended to still be 'simple' to the user, but much more +complete (with OSCServer & OSCClient classes) and much more powerful (the +OSCMultiClient supports subscriptions & message-filtering, OSCMessage & +OSCBundle are now proper container-types) + +=============================================================================== +OpenSoundControl +=============================================================================== + +OpenSoundControl is a network-protocol for sending (small) packets of addressed +data over network sockets. This OSC-implementation supports the classical +UDP/IP protocol for sending and receiving packets but provides as well support +for TCP/IP streaming, whereas the message size is prepended as int32 (big +endian) before each message/packet. + +OSC-packets come in two kinds: + + - OSC-messages consist of an 'address'-string (not to be confused with a + (host:port) network-address!), followed by a string of 'typetags' + associated with the message's arguments (ie. 'payload'), and finally the + arguments themselves, encoded in an OSC-specific way. The OSCMessage class + makes it easy to create & manipulate OSC-messages of this kind in a + 'pythonesque' way (that is, OSCMessage-objects behave a lot like lists) + + - OSC-bundles are a special type of OSC-message containing only + OSC-messages as 'payload'. Recursively. (meaning; an OSC-bundle could + contain other OSC-bundles, containing OSC-bundles etc.) + +OSC-bundles start with the special keyword '#bundle' and do not have an +OSC-address (but the OSC-messages a bundle contains will have OSC-addresses!). +Also, an OSC-bundle can have a timetag, essentially telling the receiving +server to 'hold' the bundle until the specified time. The OSCBundle class +allows easy cration & manipulation of OSC-bundles. + +For further information see also http://opensoundcontrol.org/spec-1_0 + +------------------------------------------------------------------------------- + +To send OSC-messages, you need an OSCClient, and to receive OSC-messages you +need an OSCServer. + +The OSCClient uses an 'AF_INET / SOCK_DGRAM' type socket (see the 'socket' +module) to send binary representations of OSC-messages to a remote host:port +address. + +The OSCServer listens on an 'AF_INET / SOCK_DGRAM' type socket bound to a local +port, and handles incoming requests. Either one-after-the-other (OSCServer) or +in a multi-threaded / multi-process fashion (ThreadingOSCServer/ +ForkingOSCServer). If the Server has a callback-function (a.k.a. handler) +registered to 'deal with' (i.e. handle) the received message's OSC-address, +that function is called, passing it the (decoded) message. + +The different OSCServers implemented here all support the (recursive) un- +bundling of OSC-bundles, and OSC-bundle timetags. + +In fact, this implementation supports: + + - OSC-messages with 'i' (int32), 'f' (float32), 'd' (double), 's' (string) and + 'b' (blob / binary data) types + - OSC-bundles, including timetag-support + - OSC-address patterns including '*', '?', '{,}' and '[]' wildcards. + +(please *do* read the OSC-spec! http://opensoundcontrol.org/spec-1_0 it +explains what these things mean.) + +In addition, the OSCMultiClient supports: + - Sending a specific OSC-message to multiple remote servers + - Remote server subscription / unsubscription (through OSC-messages, of course) + - Message-address filtering. + +------------------------------------------------------------------------------- +SimpleOSC: + Copyright (c) Daniel Holth & Clinton McChesney. +pyOSC: + Copyright (c) 2008-2010, Artem Baguinski et al., Stock, V2_Lab, Rotterdam, Netherlands. +Streaming support (OSC over TCP): + Copyright (c) 2010 Uli Franke , Weiss Engineering, Uster, Switzerland. + +------------------------------------------------------------------------------- +Changelog: +------------------------------------------------------------------------------- +v0.3.0 - 27 Dec. 2007 + Started out to extend the 'SimpleOSC' implementation (v0.2.3) by Daniel Holth & Clinton McChesney. + Rewrote OSCMessage + Added OSCBundle + +v0.3.1 - 3 Jan. 2008 + Added OSClient + Added OSCRequestHandler, loosely based on the original CallbackManager + Added OSCServer + Removed original CallbackManager + Adapted testing-script (the 'if __name__ == "__main__":' block at the end) to use new Server & Client + +v0.3.2 - 5 Jan. 2008 + Added 'container-type emulation' methods (getitem(), setitem(), __iter__() & friends) to OSCMessage + Added ThreadingOSCServer & ForkingOSCServer + - 6 Jan. 2008 + Added OSCMultiClient + Added command-line options to testing-script (try 'python OSC.py --help') + +v0.3.3 - 9 Jan. 2008 + Added OSC-timetag support to OSCBundle & OSCRequestHandler + Added ThreadingOSCRequestHandler + +v0.3.4 - 13 Jan. 2008 + Added message-filtering to OSCMultiClient + Added subscription-handler to OSCServer + Added support fon numpy/scipy int & float types. (these get converted to 'standard' 32-bit OSC ints / floats!) + Cleaned-up and added more Docstrings + +v0.3.5 - 14 aug. 2008 + Added OSCServer.reportErr(...) method + +v0.3.6 - 19 April 2010 + Added Streaming support (OSC over TCP) + Updated documentation + Moved pattern matching stuff into separate class (OSCAddressSpace) to + facilitate implementation of different server and client architectures. + Callbacks feature now a context (object oriented) but dynamic function + inspection keeps the code backward compatible + Moved testing code into separate testbench (testbench.py) + +----------------- +Original Comments +----------------- +> Open SoundControl for Python +> Copyright (C) 2002 Daniel Holth, Clinton McChesney +> +> This library is free software; you can redistribute it and/or modify it under +> the terms of the GNU Lesser General Public License as published by the Free +> Software Foundation; either version 2.1 of the License, or (at your option) any +> later version. +> +> This library is distributed in the hope that it will be useful, but WITHOUT ANY +> WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +> PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +> details. +> +> You should have received a copy of the GNU Lesser General Public License along +> with this library; if not, write to the Free Software Foundation, Inc., 59 +> Temple Place, Suite 330, Boston, MA 02111-1307 USA +> +> For questions regarding this module contact Daniel Holth +> or visit http://www.stetson.edu/~ProctoLogic/ +> +> Changelog: +> 15 Nov. 2001: +> Removed dependency on Python 2.0 features. +> - dwh +> 13 Feb. 2002: +> Added a generic callback handler. +> - dwh +""" + +import math, re, socket, select, string, struct, sys, threading, time, types, array, errno, inspect +from socketserver import UDPServer, DatagramRequestHandler, ForkingMixIn, ThreadingMixIn, StreamRequestHandler, TCPServer +from contextlib import closing + +global version +version = ("0.3","6", "$Rev: 6382 $"[6:-2]) + +global FloatTypes +FloatTypes = [float] + +global IntTypes +IntTypes = [int] + +global NTP_epoch +from calendar import timegm +NTP_epoch = timegm((1900,1,1,0,0,0)) # NTP time started in 1 Jan 1900 +del timegm + +global NTP_units_per_second +NTP_units_per_second = 0x100000000 # about 232 picoseconds + +## +# numpy/scipy support: +## + +try: + from numpy import typeDict + + for ftype in ['float32', 'float64', 'float128']: + try: + FloatTypes.append(typeDict[ftype]) + except KeyError: + pass + + for itype in ['int8', 'int16', 'int32', 'int64']: + try: + IntTypes.append(typeDict[itype]) + IntTypes.append(typeDict['u' + itype]) + except KeyError: + pass + + # thanks for those... + del typeDict, ftype, itype + +except ImportError: + pass + +###### +# +# OSCMessage classes +# +###### + +class OSCMessage(object): + """ Builds typetagged OSC messages. + + OSCMessage objects are container objects for building OSC-messages. + On the 'front' end, they behave much like list-objects, and on the 'back' end + they generate a binary representation of the message, which can be sent over a network socket. + OSC-messages consist of an 'address'-string (not to be confused with a (host, port) IP-address!), + followed by a string of 'typetags' associated with the message's arguments (ie. 'payload'), + and finally the arguments themselves, encoded in an OSC-specific way. + + On the Python end, OSCMessage are lists of arguments, prepended by the message's address. + The message contents can be manipulated much like a list: + >>> msg = OSCMessage("/my/osc/address") + >>> msg.append('something') + >>> msg.insert(0, 'something else') + >>> msg[1] = 'entirely' + >>> msg.extend([1,2,3.]) + >>> msg += [4, 5, 6.] + >>> del msg[3:6] + >>> msg.pop(-2) + 5 + >>> print msg + /my/osc/address ['something else', 'entirely', 1, 6.0] + + OSCMessages can be concatenated with the + operator. In this case, the resulting OSCMessage + inherits its address from the left-hand operand. The right-hand operand's address is ignored. + To construct an 'OSC-bundle' from multiple OSCMessage, see OSCBundle! + + Additional methods exist for retreiving typetags or manipulating items as (typetag, value) tuples. + """ + def __init__(self, address=""): + """Instantiate a new OSCMessage. + The OSC-address can be specified with the 'address' argument + """ + self.clear(address) + + def setAddress(self, address): + """Set or change the OSC-address + """ + self.address = address + + def clear(self, address=""): + """Clear (or set a new) OSC-address and clear any arguments appended so far + """ + self.address = address + self.clearData() + + def clearData(self): + """Clear any arguments appended so far + """ + self.typetags = "," + self.message = b"" + + def append(self, argument, typehint=None): + """Appends data to the message, updating the typetags based on + the argument's type. If the argument is a blob (counted + string) pass in 'b' as typehint. + 'argument' may also be a list or tuple, in which case its elements + will get appended one-by-one, all using the provided typehint + """ + if isinstance(argument,dict): + argument = list(argument.items()) + elif isinstance(argument, OSCMessage): + raise TypeError("Can only append 'OSCMessage' to 'OSCBundle'") + + if hasattr(argument, '__iter__') and not type(argument) in (str,bytes): + for arg in argument: + self.append(arg, typehint) + + return + + if typehint == 'b': + binary = OSCBlob(argument) + tag = 'b' + elif typehint == 't': + binary = OSCTimeTag(argument) + tag = 't' + else: + tag, binary = OSCArgument(argument, typehint) + + self.typetags += tag + self.message += binary + + def getBinary(self): + """Returns the binary representation of the message + """ + binary = OSCString(self.address) + binary += OSCString(self.typetags) + binary += self.message + + return binary + + def __repr__(self): + """Returns a string containing the decode Message + """ + return str(decodeOSC(self.getBinary())) + + def __str__(self): + """Returns the Message's address and contents as a string. + """ + return "%s %s" % (self.address, str(list(self.values()))) + + def __len__(self): + """Returns the number of arguments appended so far + """ + return (len(self.typetags) - 1) + + def __eq__(self, other): + """Return True if two OSCMessages have the same address & content + """ + if not isinstance(other, self.__class__): + return False + + return (self.address == other.address) and (self.typetags == other.typetags) and (self.message == other.message) + + def __ne__(self, other): + """Return (not self.__eq__(other)) + """ + return not self.__eq__(other) + + def __add__(self, values): + """Returns a copy of self, with the contents of 'values' appended + (see the 'extend()' method, below) + """ + msg = self.copy() + msg.extend(values) + return msg + + def __iadd__(self, values): + """Appends the contents of 'values' + (equivalent to 'extend()', below) + Returns self + """ + self.extend(values) + return self + + def __radd__(self, values): + """Appends the contents of this OSCMessage to 'values' + Returns the extended 'values' (list or tuple) + """ + out = list(values) + out.extend(list(self.values())) + + if isinstance(values,tuple): + return tuple(out) + + return out + + def _reencode(self, items): + """Erase & rebuild the OSCMessage contents from the given + list of (typehint, value) tuples""" + self.clearData() + for item in items: + self.append(item[1], item[0]) + + def values(self): + """Returns a list of the arguments appended so far + """ + return decodeOSC(self.getBinary())[2:] + + def tags(self): + """Returns a list of typetags of the appended arguments + """ + return list(self.typetags.lstrip(',')) + + def items(self): + """Returns a list of (typetag, value) tuples for + the arguments appended so far + """ + out = [] + values = list(self.values()) + typetags = self.tags() + for i in range(len(values)): + out.append((typetags[i], values[i])) + + return out + + def __contains__(self, val): + """Test if the given value appears in the OSCMessage's arguments + """ + return (val in list(self.values())) + + def __getitem__(self, i): + """Returns the indicated argument (or slice) + """ + return list(self.values())[i] + + def __delitem__(self, i): + """Removes the indicated argument (or slice) + """ + items = list(self.items()) + del items[i] + + self._reencode(items) + + def _buildItemList(self, values, typehint=None): + if isinstance(values, OSCMessage): + items = list(values.items()) + elif isinstance(values,list): + items = [] + for val in values: + if isinstance(val,tuple): + items.append(val[:2]) + else: + items.append((typehint, val)) + elif isinstance(values,tuple): + items = [values[:2]] + else: + items = [(typehint, values)] + + return items + + def __setitem__(self, i, val): + """Set indicatated argument (or slice) to a new value. + 'val' can be a single int/float/string, or a (typehint, value) tuple. + Or, if 'i' is a slice, a list of these or another OSCMessage. + """ + items = list(self.items()) + + new_items = self._buildItemList(val) + + if not isinstance(i,slice): + if len(new_items) != 1: + raise TypeError("single-item assignment expects a single value or a (typetag, value) tuple") + + new_items = new_items[0] + + # finally... + items[i] = new_items + + self._reencode(items) + + def setItem(self, i, val, typehint=None): + """Set indicated argument to a new value (with typehint) + """ + items = list(self.items()) + + items[i] = (typehint, val) + + self._reencode(items) + + def copy(self): + """Returns a deep copy of this OSCMessage + """ + msg = self.__class__(self.address) + msg.typetags = self.typetags + msg.message = self.message + return msg + + def count(self, val): + """Returns the number of times the given value occurs in the OSCMessage's arguments + """ + return list(self.values()).count(val) + + def index(self, val): + """Returns the index of the first occurence of the given value in the OSCMessage's arguments. + Raises ValueError if val isn't found + """ + return list(self.values()).index(val) + + def extend(self, values): + """Append the contents of 'values' to this OSCMessage. + 'values' can be another OSCMessage, or a list/tuple of ints/floats/strings + """ + items = list(self.items()) + self._buildItemList(values) + + self._reencode(items) + + def insert(self, i, val, typehint = None): + """Insert given value (with optional typehint) into the OSCMessage + at the given index. + """ + items = list(self.items()) + + for item in reversed(self._buildItemList(val)): + items.insert(i, item) + + self._reencode(items) + + def popitem(self, i): + """Delete the indicated argument from the OSCMessage, and return it + as a (typetag, value) tuple. + """ + items = list(self.items()) + + item = items.pop(i) + + self._reencode(items) + + return item + + def pop(self, i): + """Delete the indicated argument from the OSCMessage, and return it. + """ + return self.popitem(i)[1] + + def reverse(self): + """Reverses the arguments of the OSCMessage (in place) + """ + items = list(self.items()) + + items.reverse() + + self._reencode(items) + + def remove(self, val): + """Removes the first argument with the given value from the OSCMessage. + Raises ValueError if val isn't found. + """ + items = list(self.items()) + + # this is not very efficient... + i = 0 + for (t, v) in items: + if (v == val): + break + i += 1 + else: + raise ValueError("'%s' not in OSCMessage" % str(m)) + # but more efficient than first calling self.values().index(val), + # then calling self.items(), which would in turn call self.values() again... + + del items[i] + + self._reencode(items) + + def __iter__(self): + """Returns an iterator of the OSCMessage's arguments + """ + return iter(list(self.values())) + + def __reversed__(self): + """Returns a reverse iterator of the OSCMessage's arguments + """ + return reversed(list(self.values())) + + def itervalues(self): + """Returns an iterator of the OSCMessage's arguments + """ + return iter(list(self.values())) + + def iteritems(self): + """Returns an iterator of the OSCMessage's arguments as + (typetag, value) tuples + """ + return iter(list(self.items())) + + def itertags(self): + """Returns an iterator of the OSCMessage's arguments' typetags + """ + return iter(self.tags()) + +class OSCBundle(OSCMessage): + """Builds a 'bundle' of OSC messages. + + OSCBundle objects are container objects for building OSC-bundles of OSC-messages. + An OSC-bundle is a special kind of OSC-message which contains a list of OSC-messages + (And yes, OSC-bundles may contain other OSC-bundles...) + + OSCBundle objects behave much the same as OSCMessage objects, with these exceptions: + - if an item or items to be appended or inserted are not OSCMessage objects, + OSCMessage objectss are created to encapsulate the item(s) + - an OSC-bundle does not have an address of its own, only the contained OSC-messages do. + The OSCBundle's 'address' is inherited by any OSCMessage the OSCBundle object creates. + - OSC-bundles have a timetag to tell the receiver when the bundle should be processed. + The default timetag value (0) means 'immediately' + """ + def __init__(self, address="", time=0): + """Instantiate a new OSCBundle. + The default OSC-address for newly created OSCMessages + can be specified with the 'address' argument + The bundle's timetag can be set with the 'time' argument + """ + super(OSCBundle, self).__init__(address) + self.timetag = time + + def __str__(self): + """Returns the Bundle's contents (and timetag, if nonzero) as a string. + """ + if (self.timetag > 0.): + out = "#bundle (%s) [" % self.getTimeTagStr() + else: + out = "#bundle [" + + if self.__len__(): + for val in list(self.values()): + out += "%s, " % str(val) + out = out[:-2] # strip trailing space and comma + + return out + "]" + + def setTimeTag(self, time): + """Set or change the OSCBundle's TimeTag + In 'Python Time', that's floating seconds since the Epoch + """ + if time >= 0: + self.timetag = time + + def getTimeTagStr(self): + """Return the TimeTag as a human-readable string + """ + fract, secs = math.modf(self.timetag) + out = time.ctime(secs)[11:19] + out += ("%.3f" % fract)[1:] + + return out + + def append(self, argument, typehint = None): + """Appends data to the bundle, creating an OSCMessage to encapsulate + the provided argument unless this is already an OSCMessage. + Any newly created OSCMessage inherits the OSCBundle's address at the time of creation. + If 'argument' is an iterable, its elements will be encapsuated by a single OSCMessage. + Finally, 'argument' can be (or contain) a dict, which will be 'converted' to an OSCMessage; + - if 'addr' appears in the dict, its value overrides the OSCBundle's address + - if 'args' appears in the dict, its value(s) become the OSCMessage's arguments + """ + if isinstance(argument, OSCMessage): + binary = OSCBlob(argument.getBinary()) + else: + msg = OSCMessage(self.address) + if isinstance(argument,dict): + if 'addr' in argument: + msg.setAddress(argument['addr']) + if 'args' in argument: + msg.append(argument['args'], typehint) + else: + msg.append(argument, typehint) + + binary = OSCBlob(msg.getBinary()) + + self.message += binary + self.typetags += 'b' + + def getBinary(self): + """Returns the binary representation of the message + """ + binary = OSCString("#bundle") + binary += OSCTimeTag(self.timetag) + binary += self.message + + return binary + + def _reencapsulate(self, decoded): + if decoded[0] == "#bundle": + msg = OSCBundle() + msg.setTimeTag(decoded[1]) + for submsg in decoded[2:]: + msg.append(self._reencapsulate(submsg)) + + else: + msg = OSCMessage(decoded[0]) + tags = decoded[1].lstrip(',') + for i in range(len(tags)): + msg.append(decoded[2+i], tags[i]) + + return msg + + def values(self): + """Returns a list of the OSCMessages appended so far + """ + out = [] + for decoded in decodeOSC(self.getBinary())[2:]: + out.append(self._reencapsulate(decoded)) + + return out + + def __eq__(self, other): + """Return True if two OSCBundles have the same timetag & content + """ + if not isinstance(other, self.__class__): + return False + + return (self.timetag == other.timetag) and (self.typetags == other.typetags) and (self.message == other.message) + + def copy(self): + """Returns a deep copy of this OSCBundle + """ + copy = super(OSCBundle, self).copy() + copy.timetag = self.timetag + return copy + +###### +# +# OSCMessage encoding functions +# +###### + +def OSCString(next): + """Convert a string into a zero-padded OSC String. + The length of the resulting string is always a multiple of 4 bytes. + The string ends with 1 to 4 zero-bytes ('\x00') + """ + + OSCstringLength = math.ceil((len(next)+1) / 4.0) * 4 + return struct.pack(">%ds" % (OSCstringLength), str(next).encode('latin1')) + +def OSCBlob(next): + """Convert a string into an OSC Blob. + An OSC-Blob is a binary encoded block of data, prepended by a 'size' (int32). + The size is always a mutiple of 4 bytes. + The blob ends with 0 to 3 zero-bytes ('\x00') + """ + + if isinstance(next,str): + next = next.encode('latin1') + if isinstance(next,bytes): + OSCblobLength = math.ceil((len(next)) / 4.0) * 4 + binary = struct.pack(">i%ds" % (OSCblobLength), OSCblobLength, next) + else: + binary = b'' + + return binary + +def OSCArgument(next, typehint=None): + """ Convert some Python types to their + OSC binary representations, returning a + (typetag, data) tuple. + """ + if not typehint: + if type(next) in FloatTypes: + binary = struct.pack(">f", float(next)) + tag = 'f' + elif type(next) in IntTypes: + binary = struct.pack(">i", int(next)) + tag = 'i' + else: + binary = OSCString(next) + tag = 's' + + elif typehint == 'd': + try: + binary = struct.pack(">d", float(next)) + tag = 'd' + except ValueError: + binary = OSCString(next) + tag = 's' + + elif typehint == 'f': + try: + binary = struct.pack(">f", float(next)) + tag = 'f' + except ValueError: + binary = OSCString(next) + tag = 's' + elif typehint == 'i': + try: + binary = struct.pack(">i", int(next)) + tag = 'i' + except ValueError: + binary = OSCString(next) + tag = 's' + else: + binary = OSCString(next) + tag = 's' + + return (tag, binary) + +def OSCTimeTag(time): + """Convert a time in floating seconds to its + OSC binary representation + """ + if time > 0: + fract, secs = math.modf(time) + secs = secs - NTP_epoch + binary = struct.pack('>LL', int(secs), int(fract * NTP_units_per_second)) + else: + binary = struct.pack('>LL', 0, 1) + + return binary + +###### +# +# OSCMessage decoding functions +# +###### + +def _readString(data): + """Reads the next (null-terminated) block of data + """ + length = data.find(b'\0') + nextData = int(math.ceil((length+1) / 4.0) * 4) + return (data[0:length].decode('latin1'), data[nextData:]) + +def _readBlob(data): + """Reads the next (numbered) block of data + """ + + length = struct.unpack(">i", data[0:4])[0] + nextData = int(math.ceil((length) / 4.0) * 4) + 4 + return (data[4:length+4], data[nextData:]) + +def _readInt(data): + """Tries to interpret the next 4 bytes of the data + as a 32-bit integer. """ + + if(len(data)<4): + print(("Error: too few bytes for int", data, len(data))) + rest = data + integer = 0 + else: + integer = struct.unpack(">i", data[0:4])[0] + rest = data[4:] + + return (integer, rest) + +def _readLong(data): + """Tries to interpret the next 8 bytes of the data + as a 64-bit signed integer. + """ + + high, low = struct.unpack(">ll", data[0:8]) + big = (int(high) << 32) + low + rest = data[8:] + return (big, rest) + +def _readTimeTag(data): + """Tries to interpret the next 8 bytes of the data + as a TimeTag. + """ + high, low = struct.unpack(">LL", data[0:8]) + if (high == 0) and (low <= 1): + time = 0.0 + else: + time = int(NTP_epoch + high) + float(low / NTP_units_per_second) + rest = data[8:] + return (time, rest) + +def _readFloat(data): + """Tries to interpret the next 4 bytes of the data + as a 32-bit float. + """ + + if(len(data)<4): + print(("Error: too few bytes for float", data, len(data))) + rest = data + float = 0 + else: + float = struct.unpack(">f", data[0:4])[0] + rest = data[4:] + + return (float, rest) + +def _readDouble(data): + """Tries to interpret the next 8 bytes of the data + as a 64-bit float. + """ + + if(len(data)<8): + print(("Error: too few bytes for double", data, len(data))) + rest = data + float = 0 + else: + float = struct.unpack(">d", data[0:8])[0] + rest = data[8:] + + return (float, rest) + +def decodeOSC(data): + """Converts a binary OSC message to a Python list. + """ + table = {"i":_readInt, "f":_readFloat, "s":_readString, "b":_readBlob, "d":_readDouble, "t":_readTimeTag} + decoded = [] + address, rest = _readString(data) + if address.startswith(","): + typetags = address + address = "" + else: + typetags = "" + + if address == "#bundle": + time, rest = _readTimeTag(rest) + decoded.append(address) + decoded.append(time) + while len(rest)>0: + length, rest = _readInt(rest) + decoded.append(decodeOSC(rest[:length])) + rest = rest[length:] + + elif len(rest)>0: + if not len(typetags): + typetags, rest = _readString(rest) + decoded.append(address) + decoded.append(typetags) + if typetags.startswith(","): + for tag in typetags[1:]: + value, rest = table[tag](rest) + decoded.append(value) + else: + raise OSCError("OSCMessage's typetag-string lacks the magic ','") + + return decoded + +###### +# +# Utility functions +# +###### + +def hexDump(bytes): + """ Useful utility; prints the string in hexadecimal. + """ + print("byte 0 1 2 3 4 5 6 7 8 9 A B C D E F") + + if isinstance(bytes,str): + bytes = bytes.encode('latin1') + num = len(bytes) + for i in range(num): + if (i) % 16 == 0: + line = "%02X0 : " % (i/16) + line += "%02X " % bytes[i] + if (i+1) % 16 == 0: + print(("%s: %s" % (line, repr(bytes[i-15:i+1])))) + line = "" + + bytes_left = num % 16 + if bytes_left: + print(("%s: %s" % (line.ljust(54), repr(bytes[-bytes_left:])))) + +def getUrlStr(*args): + """Convert provided arguments to a string in 'host:port/prefix' format + Args can be: + - (host, port) + - (host, port), prefix + - host, port + - host, port, prefix + """ + if not len(args): + return "" + + if type(args[0]) == tuple: + host = args[0][0] + port = args[0][1] + args = args[1:] + else: + host = args[0] + port = args[1] + args = args[2:] + + if len(args): + prefix = args[0] + else: + prefix = "" + + if len(host) and (host != '0.0.0.0'): + try: + (host, _, _) = socket.gethostbyaddr(host) + except socket.error: + pass + else: + host = 'localhost' + + if isinstance(port,int): + return "%s:%d%s" % (host, port, prefix) + else: + return host + prefix + +def parseUrlStr(url): + """Convert provided string in 'host:port/prefix' format to it's components + Returns ((host, port), prefix) + """ + if not (isinstance(url,str) and len(url)): + return (None, '') + + i = url.find("://") + if i > -1: + url = url[i+3:] + + i = url.find(':') + if i > -1: + host = url[:i].strip() + tail = url[i+1:].strip() + else: + host = '' + tail = url + + for i in range(len(tail)): + if not tail[i].isdigit(): + break + else: + i += 1 + + portstr = tail[:i].strip() + tail = tail[i:].strip() + + found = len(tail) + for c in ('/', '+', '-', '*'): + i = tail.find(c) + if (i > -1) and (i < found): + found = i + + head = tail[:found].strip() + prefix = tail[found:].strip() + + prefix = prefix.strip('/') + if len(prefix) and prefix[0] not in ('+', '-', '*'): + prefix = '/' + prefix + + if len(head) and not len(host): + host = head + + if len(host): + try: + host = socket.gethostbyname(host) + except socket.error: + pass + + try: + port = int(portstr) + except ValueError: + port = None + + return ((host, port), prefix) + +###### +# +# OSCClient class +# +###### + +class OSCClient(object): + """Simple OSC Client. Handles the sending of OSC-Packets (OSCMessage or OSCBundle) via a UDP-socket + """ + # set outgoing socket buffer size + sndbuf_size = 4096 * 8 + + def __init__(self, server=None): + """Construct an OSC Client. + When the 'address' argument is given this client is connected to a specific remote server. + - address ((host, port) tuple): the address of the remote server to send all messages to + Otherwise it acts as a generic client: + If address == 'None', the client doesn't connect to a specific remote server, + and the remote address must be supplied when calling sendto() + - server: Local OSCServer-instance this client will use the socket of for transmissions. + If none is supplied, a socket will be created. + """ + self.socket = None + + if server == None: + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, self.sndbuf_size) + self._fd = self.socket.fileno() + + self.server = None + else: + self.setServer(server) + + self.client_address = None + + def setServer(self, server): + """Associate this Client with given server. + The Client will send from the Server's socket. + The Server will use this Client instance to send replies. + """ + if not isinstance(server, OSCServer): + raise ValueError("'server' argument is not a valid OSCServer object") + + if self.socket != None: + self.close() + + self.socket = server.socket.dup() + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, self.sndbuf_size) + self._fd = self.socket.fileno() + + self.server = server + + if self.server.client != None: + self.server.client.close() + + self.server.client = self + + def close(self): + """Disconnect & close the Client's socket + """ + if self.socket != None: + self.socket.close() + self.socket = None + + def __str__(self): + """Returns a string containing this Client's Class-name, software-version + and the remote-address it is connected to (if any) + """ + out = self.__class__.__name__ + out += " v%s.%s-%s" % version + addr = self.address() + if addr: + out += " connected to osc://%s" % getUrlStr(addr) + else: + out += " (unconnected)" + + return out + + def __eq__(self, other): + """Compare function. + """ + if not isinstance(other, self.__class__): + return False + + isequal = cmp(self.socket._sock, other.socket._sock) + if isequal and self.server and other.server: + return cmp(self.server, other.server) + + return isequal + + def __ne__(self, other): + """Compare function. + """ + return not self.__eq__(other) + + def address(self): + """Returns a (host,port) tuple of the remote server this client is + connected to or None if not connected to any server. + """ + try: + return self.socket.getpeername() + except socket.error: + return None + + def connect(self, address): + """Bind to a specific OSC server: + the 'address' argument is a (host, port) tuple + - host: hostname of the remote OSC server, + - port: UDP-port the remote OSC server listens to. + """ + try: + self.socket.connect(address) + self.client_address = address + except socket.error as e: + self.client_address = None + raise OSCClientError("SocketError: %s" % str(e)) + + if self.server != None: + self.server.return_port = address[1] + + def sendto(self, msg, address, timeout=None): + """Send the given OSCMessage to the specified address. + - msg: OSCMessage (or OSCBundle) to be sent + - address: (host, port) tuple specifing remote server to send the message to + - timeout: A timeout value for attempting to send. If timeout == None, + this call blocks until socket is available for writing. + Raises OSCClientError when timing out while waiting for the socket. + """ + if not isinstance(msg, OSCMessage): + raise TypeError("'msg' argument is not an OSCMessage or OSCBundle object") + + ret = select.select([],[self._fd], [], timeout) + try: + ret[1].index(self._fd) + except: + # for the very rare case this might happen + raise OSCClientError("Timed out waiting for file descriptor") + + try: + self.socket.connect(address) + self.socket.sendall(msg.getBinary()) + + if self.client_address: + self.socket.connect(self.client_address) + + except socket.error as e: + if e[0] in (7, 65): # 7 = 'no address associated with nodename', 65 = 'no route to host' + raise e + else: + raise OSCClientError("while sending to %s: %s" % (str(address), str(e))) + + def send(self, msg, timeout=None): + """Send the given OSCMessage. + The Client must be already connected. + - msg: OSCMessage (or OSCBundle) to be sent + - timeout: A timeout value for attempting to send. If timeout == None, + this call blocks until socket is available for writing. + Raises OSCClientError when timing out while waiting for the socket, + or when the Client isn't connected to a remote server. + """ + if not isinstance(msg, OSCMessage): + raise TypeError("'msg' argument is not an OSCMessage or OSCBundle object") + + ret = select.select([],[self._fd], [], timeout) + try: + ret[1].index(self._fd) + except: + # for the very rare case this might happen + raise OSCClientError("Timed out waiting for file descriptor") + + try: + self.socket.sendall(msg.getBinary()) + except socket.error as e: + if e[0] in (7, 65): # 7 = 'no address associated with nodename', 65 = 'no route to host' + raise e + else: + raise OSCClientError("while sending: %s" % str(e)) + +###### +# +# FilterString Utility functions +# +###### + +def parseFilterStr(args): + """Convert Message-Filter settings in '+ - ...' format to a dict of the form + { '':True, '':False, ... } + Returns a list: ['', filters] + """ + out = {} + + if isinstance(args,str): + args = [args] + + prefix = None + for arg in args: + head = None + for plus in arg.split('+'): + minus = plus.split('-') + plusfs = minus.pop(0).strip() + if len(plusfs): + plusfs = '/' + plusfs.strip('/') + + if (head == None) and (plusfs != "/*"): + head = plusfs + elif len(plusfs): + if plusfs == '/*': + out = { '/*':True } # reset all previous filters + else: + out[plusfs] = True + + for minusfs in minus: + minusfs = minusfs.strip() + if len(minusfs): + minusfs = '/' + minusfs.strip('/') + if minusfs == '/*': + out = { '/*':False } # reset all previous filters + else: + out[minusfs] = False + + if prefix == None: + prefix = head + + return [prefix, out] + +def getFilterStr(filters): + """Return the given 'filters' dict as a list of + '+' | '-' filter-strings + """ + if not len(filters): + return [] + + if '/*' in list(filters.keys()): + if filters['/*']: + out = ["+/*"] + else: + out = ["-/*"] + else: + if False in list(filters.values()): + out = ["+/*"] + else: + out = ["-/*"] + + for (addr, bool) in list(filters.items()): + if addr == '/*': + continue + + if bool: + out.append("+%s" % addr) + else: + out.append("-%s" % addr) + + return out + +# A translation-table for mapping OSC-address expressions to Python 're' expressions +OSCtrans = str.maketrans("{,}?","(|).") + +def getRegEx(pattern): + """Compiles and returns a 'regular expression' object for the given address-pattern. + """ + # Translate OSC-address syntax to python 're' syntax + pattern = pattern.replace(".", r"\.") # first, escape all '.'s in the pattern. + pattern = pattern.replace("(", r"\(") # escape all '('s. + pattern = pattern.replace(")", r"\)") # escape all ')'s. + pattern = pattern.replace("*", r".*") # replace a '*' by '.*' (match 0 or more characters) + pattern = pattern.translate(OSCtrans) # change '?' to '.' and '{,}' to '(|)' + + return re.compile(pattern) + +###### +# +# OSCMultiClient class +# +###### + +class OSCMultiClient(OSCClient): + """'Multiple-Unicast' OSC Client. Handles the sending of OSC-Packets (OSCMessage or OSCBundle) via a UDP-socket + This client keeps a dict of 'OSCTargets'. and sends each OSCMessage to each OSCTarget + The OSCTargets are simply (host, port) tuples, and may be associated with an OSC-address prefix. + the OSCTarget's prefix gets prepended to each OSCMessage sent to that target. + """ + def __init__(self, server=None): + """Construct a "Multi" OSC Client. + - server: Local OSCServer-instance this client will use the socket of for transmissions. + If none is supplied, a socket will be created. + """ + super(OSCMultiClient, self).__init__(server) + + self.targets = {} + + def _searchHostAddr(self, host): + """Search the subscribed OSCTargets for (the first occurence of) given host. + Returns a (host, port) tuple + """ + try: + host = socket.gethostbyname(host) + except socket.error: + pass + + for addr in list(self.targets.keys()): + if host == addr[0]: + return addr + + raise NotSubscribedError((host, None)) + + def _updateFilters(self, dst, src): + """Update a 'filters' dict with values form another 'filters' dict: + - src[a] == True and dst[a] == False: del dst[a] + - src[a] == False and dst[a] == True: del dst[a] + - a not in dst: dst[a] == src[a] + """ + if '/*' in list(src.keys()): # reset filters + dst.clear() # 'match everything' == no filters + if not src.pop('/*'): + dst['/*'] = False # 'match nothing' + + for (addr, bool) in list(src.items()): + if (addr in list(dst.keys())) and (dst[addr] != bool): + del dst[addr] + else: + dst[addr] = bool + + def _setTarget(self, address, prefix=None, filters=None): + """Add (i.e. subscribe) a new OSCTarget, or change the prefix for an existing OSCTarget. + - address ((host, port) tuple): IP-address & UDP-port + - prefix (string): The OSC-address prefix prepended to the address of each OSCMessage + sent to this OSCTarget (optional) + """ + if address not in list(self.targets.keys()): + self.targets[address] = ["",{}] + + if prefix != None: + if len(prefix): + # make sure prefix starts with ONE '/', and does not end with '/' + prefix = '/' + prefix.strip('/') + + self.targets[address][0] = prefix + + if filters != None: + if isinstance(filters,str): + (_, filters) = parseFilterStr(filters) + elif not isinstance(filters,dict): + raise TypeError("'filters' argument must be a dict with {addr:bool} entries") + + self._updateFilters(self.targets[address][1], filters) + + def setOSCTarget(self, address, prefix=None, filters=None): + """Add (i.e. subscribe) a new OSCTarget, or change the prefix for an existing OSCTarget. + the 'address' argument can be a ((host, port) tuple) : The target server address & UDP-port + or a 'host' (string) : The host will be looked-up + - prefix (string): The OSC-address prefix prepended to the address of each OSCMessage + sent to this OSCTarget (optional) + """ + if isinstance(address,str): + address = self._searchHostAddr(address) + + elif (isinstance(address,tuple)): + (host, port) = address[:2] + try: + host = socket.gethostbyname(host) + except: + pass + + address = (host, port) + else: + raise TypeError("'address' argument must be a (host, port) tuple or a 'host' string") + + self._setTarget(address, prefix, filters) + + def setOSCTargetFromStr(self, url): + """Adds or modifies a subscribed OSCTarget from the given string, which should be in the + ':[/] [+/]|[-/] ...' format. + """ + (addr, tail) = parseUrlStr(url) + (prefix, filters) = parseFilterStr(tail) + self._setTarget(addr, prefix, filters) + + def _delTarget(self, address, prefix=None): + """Delete the specified OSCTarget from the Client's dict. + the 'address' argument must be a (host, port) tuple. + If the 'prefix' argument is given, the Target is only deleted if the address and prefix match. + """ + try: + if prefix == None: + del self.targets[address] + elif prefix == self.targets[address][0]: + del self.targets[address] + except KeyError: + raise NotSubscribedError(address, prefix) + + def delOSCTarget(self, address, prefix=None): + """Delete the specified OSCTarget from the Client's dict. + the 'address' argument can be a ((host, port) tuple), or a hostname. + If the 'prefix' argument is given, the Target is only deleted if the address and prefix match. + """ + if isinstance(address,str): + address = self._searchHostAddr(address) + + if isinstance(address,tuple): + (host, port) = address[:2] + try: + host = socket.gethostbyname(host) + except socket.error: + pass + address = (host, port) + + self._delTarget(address, prefix) + + def hasOSCTarget(self, address, prefix=None): + """Return True if the given OSCTarget exists in the Client's dict. + the 'address' argument can be a ((host, port) tuple), or a hostname. + If the 'prefix' argument is given, the return-value is only True if the address and prefix match. + """ + if isinstance(address,str): + address = self._searchHostAddr(address) + + if isinstance(address,tuple): + (host, port) = address[:2] + try: + host = socket.gethostbyname(host) + except socket.error: + pass + address = (host, port) + + if address in list(self.targets.keys()): + if prefix == None: + return True + elif prefix == self.targets[address][0]: + return True + + return False + + def getOSCTargets(self): + """Returns the dict of OSCTargets: {addr:[prefix, filters], ...} + """ + out = {} + for ((host, port), pf) in list(self.targets.items()): + try: + (host, _, _) = socket.gethostbyaddr(host) + except socket.error: + pass + + out[(host, port)] = pf + + return out + + def getOSCTarget(self, address): + """Returns the OSCTarget matching the given address as a ((host, port), [prefix, filters]) tuple. + 'address' can be a (host, port) tuple, or a 'host' (string), in which case the first matching OSCTarget is returned + Returns (None, ['',{}]) if address not found. + """ + if isinstance(address,str): + address = self._searchHostAddr(address) + + if (isinstance(address,tuple)): + (host, port) = address[:2] + try: + host = socket.gethostbyname(host) + except socket.error: + pass + address = (host, port) + + if (address in list(self.targets.keys())): + try: + (host, _, _) = socket.gethostbyaddr(host) + except socket.error: + pass + + return ((host, port), self.targets[address]) + + return (None, ['',{}]) + + def clearOSCTargets(self): + """Erases all OSCTargets from the Client's dict + """ + self.targets = {} + + def updateOSCTargets(self, dict): + """Update the Client's OSCTargets dict with the contents of 'dict' + The given dict's items MUST be of the form + { (host, port):[prefix, filters], ... } + """ + for ((host, port), (prefix, filters)) in list(dict.items()): + val = [prefix, {}] + self._updateFilters(val[1], filters) + + try: + host = socket.gethostbyname(host) + except socket.error: + pass + + self.targets[(host, port)] = val + + def getOSCTargetStr(self, address): + """Returns the OSCTarget matching the given address as a ('osc://:[]', ['', ...])' tuple. + 'address' can be a (host, port) tuple, or a 'host' (string), in which case the first matching OSCTarget is returned + Returns (None, []) if address not found. + """ + (addr, (prefix, filters)) = self.getOSCTarget(address) + if addr == None: + return (None, []) + + return ("osc://%s" % getUrlStr(addr, prefix), getFilterStr(filters)) + + def getOSCTargetStrings(self): + """Returns a list of all OSCTargets as ('osc://:[]', ['', ...])' tuples. + """ + out = [] + for (addr, (prefix, filters)) in list(self.targets.items()): + out.append(("osc://%s" % getUrlStr(addr, prefix), getFilterStr(filters))) + + return out + + def connect(self, address): + """The OSCMultiClient isn't allowed to connect to any specific + address. + """ + return NotImplemented + + def sendto(self, msg, address, timeout=None): + """Send the given OSCMessage. + The specified address is ignored. Instead this method calls send() to + send the message to all subscribed clients. + - msg: OSCMessage (or OSCBundle) to be sent + - address: (host, port) tuple specifing remote server to send the message to + - timeout: A timeout value for attempting to send. If timeout == None, + this call blocks until socket is available for writing. + Raises OSCClientError when timing out while waiting for the socket. + """ + self.send(msg, timeout) + + def _filterMessage(self, filters, msg): + """Checks the given OSCMessge against the given filters. + 'filters' is a dict containing OSC-address:bool pairs. + If 'msg' is an OSCBundle, recursively filters its constituents. + Returns None if the message is to be filtered, else returns the message. + or + Returns a copy of the OSCBundle with the filtered messages removed. + """ + if isinstance(msg, OSCBundle): + out = msg.copy() + msgs = list(out.values()) + out.clearData() + for m in msgs: + m = self._filterMessage(filters, m) + if m: # this catches 'None' and empty bundles. + out.append(m) + + elif isinstance(msg, OSCMessage): + if '/*' in list(filters.keys()): + if filters['/*']: + out = msg + else: + out = None + + elif False in list(filters.values()): + out = msg + else: + out = None + + else: + raise TypeError("'msg' argument is not an OSCMessage or OSCBundle object") + + expr = getRegEx(msg.address) + + for addr in list(filters.keys()): + if addr == '/*': + continue + + match = expr.match(addr) + if match and (match.end() == len(addr)): + if filters[addr]: + out = msg + else: + out = None + break + + return out + + def _prefixAddress(self, prefix, msg): + """Makes a copy of the given OSCMessage, then prepends the given prefix to + The message's OSC-address. + If 'msg' is an OSCBundle, recursively prepends the prefix to its constituents. + """ + out = msg.copy() + + if isinstance(msg, OSCBundle): + msgs = list(out.values()) + out.clearData() + for m in msgs: + out.append(self._prefixAddress(prefix, m)) + + elif isinstance(msg, OSCMessage): + out.setAddress(prefix + out.address) + + else: + raise TypeError("'msg' argument is not an OSCMessage or OSCBundle object") + + return out + + def send(self, msg, timeout=None): + """Send the given OSCMessage to all subscribed OSCTargets + - msg: OSCMessage (or OSCBundle) to be sent + - timeout: A timeout value for attempting to send. If timeout == None, + this call blocks until socket is available for writing. + Raises OSCClientError when timing out while waiting for the socket. + """ + for (address, (prefix, filters)) in list(self.targets.items()): + if len(filters): + out = self._filterMessage(filters, msg) + if not out: # this catches 'None' and empty bundles. + continue + else: + out = msg + + if len(prefix): + out = self._prefixAddress(prefix, msg) + + binary = out.getBinary() + + ret = select.select([],[self._fd], [], timeout) + try: + ret[1].index(self._fd) + except: + # for the very rare case this might happen + raise OSCClientError("Timed out waiting for file descriptor") + + try: + while len(binary): + sent = self.socket.sendto(binary, address) + binary = binary[sent:] + + except socket.error as e: + if e[0] in (7, 65): # 7 = 'no address associated with nodename', 65 = 'no route to host' + raise e + else: + raise OSCClientError("while sending to %s: %s" % (str(address), str(e))) + +class OSCAddressSpace: + def __init__(self): + self.callbacks = {} + def addMsgHandler(self, address, callback): + """Register a handler for an OSC-address + - 'address' is the OSC address-string. + the address-string should start with '/' and may not contain '*' + - 'callback' is the function called for incoming OSCMessages that match 'address'. + The callback-function will be called with the same arguments as the 'msgPrinter_handler' below + """ + for chk in '*?,[]{}# ': + if chk in address: + raise OSCServerError("OSC-address string may not contain any characters in '*?,[]{}# '") + + if type(callback) not in (types.FunctionType, types.MethodType): + raise OSCServerError("Message callback '%s' is not callable" % repr(callback)) + + if address != 'default': + address = '/' + address.strip('/') + + self.callbacks[address] = callback + + def delMsgHandler(self, address): + """Remove the registered handler for the given OSC-address + """ + del self.callbacks[address] + + def getOSCAddressSpace(self): + """Returns a list containing all OSC-addresses registerd with this Server. + """ + return list(self.callbacks.keys()) + + def dispatchMessage(self, pattern, tags, data, client_address): + """Attmept to match the given OSC-address pattern, which may contain '*', + against all callbacks registered with the OSCServer. + Calls the matching callback and returns whatever it returns. + If no match is found, and a 'default' callback is registered, it calls that one, + or raises NoCallbackError if a 'default' callback is not registered. + + - pattern (string): The OSC-address of the receied message + - tags (string): The OSC-typetags of the receied message's arguments, without ',' + - data (list): The message arguments + """ + if len(tags) != len(data): + raise OSCServerError("Malformed OSC-message; got %d typetags [%s] vs. %d values" % (len(tags), tags, len(data))) + + expr = getRegEx(pattern) + + replies = [] + matched = 0 + for addr in list(self.callbacks.keys()): + match = expr.match(addr) + if match and (match.end() == len(addr)): + reply = self.callbacks[addr](pattern, tags, data, client_address) + matched += 1 + if isinstance(reply, OSCMessage): + replies.append(reply) + elif reply != None: + raise TypeError("Message-callback %s did not return OSCMessage or None: %s" % (self.server.callbacks[addr], type(reply))) + + if matched == 0: + if 'default' in self.callbacks: + reply = self.callbacks['default'](pattern, tags, data, client_address) + if isinstance(reply, OSCMessage): + replies.append(reply) + elif reply != None: + raise TypeError("Message-callback %s did not return OSCMessage or None: %s" % (self.server.callbacks['default'], type(reply))) + else: + raise NoCallbackError(pattern) + + return replies + +###### +# +# OSCRequestHandler classes +# +###### +class OSCRequestHandler(DatagramRequestHandler): + """RequestHandler class for the OSCServer + """ + def setup(self): + """Prepare RequestHandler. + Unpacks request as (packet, source socket address) + Creates an empty list for replies. + """ + (self.packet, self.socket) = self.request + self.replies = [] + + def _unbundle(self, decoded): + """Recursive bundle-unpacking function""" + if decoded[0] != "#bundle": + self.replies += self.server.dispatchMessage(decoded[0], decoded[1][1:], decoded[2:], self.client_address) + return + + now = time.time() + timetag = decoded[1] + if (timetag > 0.) and (timetag > now): + time.sleep(timetag - now) + + for msg in decoded[2:]: + self._unbundle(msg) + + def handle(self): + """Handle incoming OSCMessage + """ + decoded = decodeOSC(self.packet) + if not len(decoded): + return + + self._unbundle(decoded) + + def finish(self): + """Finish handling OSCMessage. + Send any reply returned by the callback(s) back to the originating client + as an OSCMessage or OSCBundle + """ + if self.server.return_port: + self.client_address = (self.client_address[0], self.server.return_port) + + if len(self.replies) > 1: + msg = OSCBundle() + for reply in self.replies: + msg.append(reply) + elif len(self.replies) == 1: + msg = self.replies[0] + else: + return + + self.server.client.sendto(msg, self.client_address) + +class ThreadingOSCRequestHandler(OSCRequestHandler): + """Multi-threaded OSCRequestHandler; + Starts a new RequestHandler thread for each unbundled OSCMessage + """ + def _unbundle(self, decoded): + """Recursive bundle-unpacking function + This version starts a new thread for each sub-Bundle found in the Bundle, + then waits for all its children to finish. + """ + if decoded[0] != "#bundle": + self.replies += self.server.dispatchMessage(decoded[0], decoded[1][1:], decoded[2:], self.client_address) + return + + now = time.time() + timetag = decoded[1] + if (timetag > 0.) and (timetag > now): + time.sleep(timetag - now) + now = time.time() + + children = [] + + for msg in decoded[2:]: + t = threading.Thread(target = self._unbundle, args = (msg,)) + t.start() + children.append(t) + + # wait for all children to terminate + for t in children: + t.join() + +###### +# +# OSCServer classes +# +###### +class OSCServer(UDPServer, OSCAddressSpace): + """A Synchronous OSCServer + Serves one request at-a-time, until the OSCServer is closed. + The OSC address-pattern is matched against a set of OSC-adresses + that have been registered to the server with a callback-function. + If the adress-pattern of the message machtes the registered address of a callback, + that function is called. + """ + + # set the RequestHandlerClass, will be overridden by ForkingOSCServer & ThreadingOSCServer + RequestHandlerClass = OSCRequestHandler + + # define a socket timeout, so the serve_forever loop can actually exit. + socket_timeout = 1 + + # DEBUG: print error-tracebacks (to stderr)? + print_tracebacks = False + + def __init__(self, server_address, client=None, return_port=0): + """Instantiate an OSCServer. + - server_address ((host, port) tuple): the local host & UDP-port + the server listens on + - client (OSCClient instance): The OSCClient used to send replies from this server. + If none is supplied (default) an OSCClient will be created. + - return_port (int): if supplied, sets the default UDP destination-port + for replies coming from this server. + """ + UDPServer.__init__(self, server_address, self.RequestHandlerClass) + OSCAddressSpace.__init__(self) + + self.setReturnPort(return_port) + self.error_prefix = "" + self.info_prefix = "/info" + + self.socket.settimeout(self.socket_timeout) + + self.running = False + self.client = None + + if client == None: + self.client = OSCClient(server=self) + else: + self.setClient(client) + + def setClient(self, client): + """Associate this Server with a new local Client instance, closing the Client this Server is currently using. + """ + if not isinstance(client, OSCClient): + raise ValueError("'client' argument is not a valid OSCClient object") + + if client.server != None: + raise OSCServerError("Provided OSCClient already has an OSCServer-instance: %s" % str(client.server)) + + # Server socket is already listening at this point, so we can't use the client's socket. + # we'll have to force our socket on the client... + client_address = client.address() # client may be already connected + client.close() # shut-down that socket + + # force our socket upon the client + client.socket = self.socket.dup() + client.socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, client.sndbuf_size) + client._fd = client.socket.fileno() + client.server = self + + if client_address: + client.connect(client_address) + if not self.return_port: + self.return_port = client_address[1] + + if self.client != None: + self.client.close() + + self.client = client + + def serve_forever(self): + """Handle one request at a time until server is closed.""" + self.running = True + while self.running: + self.handle_request() # this times-out when no data arrives. + + def close(self): + """Stops serving requests, closes server (socket), closes used client + """ + self.running = False + self.client.close() + self.server_close() + + def __str__(self): + """Returns a string containing this Server's Class-name, software-version and local bound address (if any) + """ + out = self.__class__.__name__ + out += " v%s.%s-%s" % version + addr = self.address() + if addr: + out += " listening on osc://%s" % getUrlStr(addr) + else: + out += " (unbound)" + + return out + + def __eq__(self, other): + """Compare function. + """ + if not isinstance(other, self.__class__): + return False + + return cmp(self.socket._sock, other.socket._sock) + + def __ne__(self, other): + """Compare function. + """ + return not self.__eq__(other) + + def address(self): + """Returns a (host,port) tuple of the local address this server is bound to, + or None if not bound to any address. + """ + try: + return self.socket.getsockname() + except socket.error: + return None + + def setReturnPort(self, port): + """Set the destination UDP-port for replies returning from this server to the remote client + """ + if (port > 1024) and (port < 65536): + self.return_port = port + else: + self.return_port = None + + + def setSrvInfoPrefix(self, pattern): + """Set the first part of OSC-address (pattern) this server will use to reply to server-info requests. + """ + if len(pattern): + pattern = '/' + pattern.strip('/') + + self.info_prefix = pattern + + def setSrvErrorPrefix(self, pattern=""): + """Set the OSC-address (pattern) this server will use to report errors occuring during + received message handling to the remote client. + + If pattern is empty (default), server-errors are not reported back to the client. + """ + if len(pattern): + pattern = '/' + pattern.strip('/') + + self.error_prefix = pattern + + def addDefaultHandlers(self, prefix="", info_prefix="/info", error_prefix="/error"): + """Register a default set of OSC-address handlers with this Server: + - 'default' -> noCallback_handler + the given prefix is prepended to all other callbacks registered by this method: + - ' serverInfo_handler + - ' -> msgPrinter_handler + - '/print' -> msgPrinter_handler + and, if the used Client supports it; + - '/subscribe' -> subscription_handler + - '/unsubscribe' -> subscription_handler + + Note: the given 'error_prefix' argument is also set as default 'error_prefix' for error-messages + *sent from* this server. This is ok, because error-messages generally do not elicit a reply from the receiver. + + To do this with the serverInfo-prefixes would be a bad idea, because if a request received on '/info' (for example) + would send replies to '/info', this could potentially cause a never-ending loop of messages! + Do *not* set the 'info_prefix' here (for incoming serverinfo requests) to the same value as given to + the setSrvInfoPrefix() method (for *replies* to incoming serverinfo requests). + For example, use '/info' for incoming requests, and '/inforeply' or '/serverinfo' or even just '/print' as the + info-reply prefix. + """ + self.error_prefix = error_prefix + self.addMsgHandler('default', self.noCallback_handler) + self.addMsgHandler(prefix + info_prefix, self.serverInfo_handler) + self.addMsgHandler(prefix + error_prefix, self.msgPrinter_handler) + self.addMsgHandler(prefix + '/print', self.msgPrinter_handler) + + if isinstance(self.client, OSCMultiClient): + self.addMsgHandler(prefix + '/subscribe', self.subscription_handler) + self.addMsgHandler(prefix + '/unsubscribe', self.subscription_handler) + + def printErr(self, txt): + """Writes 'OSCServer: txt' to sys.stderr + """ + sys.stderr.write("OSCServer: %s\n" % txt) + + def sendOSCerror(self, txt, client_address): + """Sends 'txt', encapsulated in an OSCMessage to the default 'error_prefix' OSC-addres. + Message is sent to the given client_address, with the default 'return_port' overriding + the client_address' port, if defined. + """ + lines = txt.split('\n') + if len(lines) == 1: + msg = OSCMessage(self.error_prefix) + msg.append(lines[0]) + elif len(lines) > 1: + msg = OSCBundle(self.error_prefix) + for line in lines: + msg.append(line) + else: + return + + if self.return_port: + client_address = (client_address[0], self.return_port) + + self.client.sendto(msg, client_address) + + def reportErr(self, txt, client_address): + """Writes 'OSCServer: txt' to sys.stderr + If self.error_prefix is defined, sends 'txt' as an OSC error-message to the client(s) + (see printErr() and sendOSCerror()) + """ + self.printErr(txt) + + if len(self.error_prefix): + self.sendOSCerror(txt, client_address) + + def sendOSCinfo(self, txt, client_address): + """Sends 'txt', encapsulated in an OSCMessage to the default 'info_prefix' OSC-addres. + Message is sent to the given client_address, with the default 'return_port' overriding + the client_address' port, if defined. + """ + lines = txt.split('\n') + if len(lines) == 1: + msg = OSCMessage(self.info_prefix) + msg.append(lines[0]) + elif len(lines) > 1: + msg = OSCBundle(self.info_prefix) + for line in lines: + msg.append(line) + else: + return + + if self.return_port: + client_address = (client_address[0], self.return_port) + + self.client.sendto(msg, client_address) + + ### + # Message-Handler callback functions + ### + + def handle_error(self, request, client_address): + """Handle an exception in the Server's callbacks gracefully. + Writes the error to sys.stderr and, if the error_prefix (see setSrvErrorPrefix()) is set, + sends the error-message as reply to the client + """ + (e_type, e) = sys.exc_info()[:2] + self.printErr("%s on request from %s: %s" % (e_type.__name__, getUrlStr(client_address), str(e))) + + if self.print_tracebacks: + import traceback + traceback.print_exc() # XXX But this goes to stderr! + + if len(self.error_prefix): + self.sendOSCerror("%s: %s" % (e_type.__name__, str(e)), client_address) + + def noCallback_handler(self, addr, tags, data, client_address): + """Example handler for OSCMessages. + All registerd handlers must accept these three arguments: + - addr (string): The OSC-address pattern of the received Message + (the 'addr' string has already been matched against the handler's registerd OSC-address, + but may contain '*'s & such) + - tags (string): The OSC-typetags of the received message's arguments. (without the preceding comma) + - data (list): The OSCMessage's arguments + Note that len(tags) == len(data) + - client_address ((host, port) tuple): the host & port this message originated from. + + a Message-handler function may return None, but it could also return an OSCMessage (or OSCBundle), + which then gets sent back to the client. + + This handler prints a "No callback registered to handle ..." message. + Returns None + """ + self.reportErr("No callback registered to handle OSC-address '%s'" % addr, client_address) + + def msgPrinter_handler(self, addr, tags, data, client_address): + """Example handler for OSCMessages. + All registerd handlers must accept these three arguments: + - addr (string): The OSC-address pattern of the received Message + (the 'addr' string has already been matched against the handler's registerd OSC-address, + but may contain '*'s & such) + - tags (string): The OSC-typetags of the received message's arguments. (without the preceding comma) + - data (list): The OSCMessage's arguments + Note that len(tags) == len(data) + - client_address ((host, port) tuple): the host & port this message originated from. + + a Message-handler function may return None, but it could also return an OSCMessage (or OSCBundle), + which then gets sent back to the client. + + This handler prints the received message. + Returns None + """ + txt = "OSCMessage '%s' from %s: " % (addr, getUrlStr(client_address)) + txt += str(data) + + self.printErr(txt) # strip trailing comma & space + + def serverInfo_handler(self, addr, tags, data, client_address): + """Example handler for OSCMessages. + All registerd handlers must accept these three arguments: + - addr (string): The OSC-address pattern of the received Message + (the 'addr' string has already been matched against the handler's registerd OSC-address, + but may contain '*'s & such) + - tags (string): The OSC-typetags of the received message's arguments. (without the preceding comma) + - data (list): The OSCMessage's arguments + Note that len(tags) == len(data) + - client_address ((host, port) tuple): the host & port this message originated from. + + a Message-handler function may return None, but it could also return an OSCMessage (or OSCBundle), + which then gets sent back to the client. + + This handler returns a reply to the client, which can contain various bits of information + about this server, depending on the first argument of the received OSC-message: + - 'help' | 'info' : Reply contains server type & version info, plus a list of + available 'commands' understood by this handler + - 'list' | 'ls' : Reply is a bundle of 'address ' messages, listing the server's + OSC address-space. + - 'clients' | 'targets' : Reply is a bundle of 'target osc://:[] [] [...]' + messages, listing the local Client-instance's subscribed remote clients. + """ + if len(data) == 0: + return None + + cmd = data.pop(0) + + reply = None + if cmd in ('help', 'info'): + reply = OSCBundle(self.info_prefix) + reply.append(('server', str(self))) + reply.append(('info_command', "ls | list : list OSC address-space")) + reply.append(('info_command', "clients | targets : list subscribed clients")) + elif cmd in ('ls', 'list'): + reply = OSCBundle(self.info_prefix) + for addr in list(self.callbacks.keys()): + reply.append(('address', addr)) + elif cmd in ('clients', 'targets'): + if hasattr(self.client, 'getOSCTargetStrings'): + reply = OSCBundle(self.info_prefix) + for trg in self.client.getOSCTargetStrings(): + reply.append(('target',) + trg) + else: + cli_addr = self.client.address() + if cli_addr: + reply = OSCMessage(self.info_prefix) + reply.append(('target', "osc://%s/" % getUrlStr(cli_addr))) + else: + self.reportErr("unrecognized command '%s' in /info request from osc://%s. Try 'help'" % (cmd, getUrlStr(client_address)), client_address) + + return reply + + def _subscribe(self, data, client_address): + """Handle the actual subscription. the provided 'data' is concatenated together to form a + ':[] [] [...]' string, which is then passed to + parseUrlStr() & parseFilterStr() to actually retreive , , etc. + + This 'long way 'round' approach (almost) guarantees that the subscription works, + regardless of how the bits of the are encoded in 'data'. + """ + url = "" + have_port = False + for item in data: + if (isinstance(item,int)) and not have_port: + url += ":%d" % item + have_port = True + elif isinstance(item,str): + url += item + + (addr, tail) = parseUrlStr(url) + (prefix, filters) = parseFilterStr(tail) + + if addr != None: + (host, port) = addr + if not host: + host = client_address[0] + if not port: + port = client_address[1] + addr = (host, port) + else: + addr = client_address + + self.client._setTarget(addr, prefix, filters) + + trg = self.client.getOSCTargetStr(addr) + if trg[0] != None: + reply = OSCMessage(self.info_prefix) + reply.append(('target',) + trg) + return reply + + def _unsubscribe(self, data, client_address): + """Handle the actual unsubscription. the provided 'data' is concatenated together to form a + ':[]' string, which is then passed to + parseUrlStr() to actually retreive , & . + + This 'long way 'round' approach (almost) guarantees that the unsubscription works, + regardless of how the bits of the are encoded in 'data'. + """ + url = "" + have_port = False + for item in data: + if (isinstance(item,int)) and not have_port: + url += ":%d" % item + have_port = True + elif isinstance(item,str): + url += item + + (addr, _) = parseUrlStr(url) + + if addr == None: + addr = client_address + else: + (host, port) = addr + if not host: + host = client_address[0] + if not port: + try: + (host, port) = self.client._searchHostAddr(host) + except NotSubscribedError: + port = client_address[1] + + addr = (host, port) + + try: + self.client._delTarget(addr) + except NotSubscribedError as e: + txt = "%s: %s" % (e.__class__.__name__, str(e)) + self.printErr(txt) + + reply = OSCMessage(self.error_prefix) + reply.append(txt) + return reply + + def subscription_handler(self, addr, tags, data, client_address): + """Handle 'subscribe' / 'unsubscribe' requests from remote hosts, + if the local Client supports this (i.e. OSCMultiClient). + + Supported commands: + - 'help' | 'info' : Reply contains server type & version info, plus a list of + available 'commands' understood by this handler + - 'list' | 'ls' : Reply is a bundle of 'target osc://:[] [] [...]' + messages, listing the local Client-instance's subscribed remote clients. + - '[subscribe | listen | sendto | target] [ ...] : Subscribe remote client/server at , + and/or set message-filters for messages being sent to the subscribed host, with the optional + arguments. Filters are given as OSC-addresses (or '*') prefixed by a '+' (send matching messages) or + a '-' (don't send matching messages). The wildcard '*', '+*' or '+/*' means 'send all' / 'filter none', + and '-*' or '-/*' means 'send none' / 'filter all' (which is not the same as unsubscribing!) + Reply is an OSCMessage with the (new) subscription; 'target osc://:[] [] [...]' + - '[unsubscribe | silence | nosend | deltarget] : Unsubscribe remote client/server at + If the given isn't subscribed, a NotSubscribedError-message is printed (and possibly sent) + + The given to the subscribe/unsubscribe handler should be of the form: + '[osc://][][:][]', where any or all components can be omitted. + + If is not specified, the IP-address of the message's source is used. + If is not specified, the is first looked up in the list of subscribed hosts, and if found, + the associated port is used. + If is not specified and is not yet subscribed, the message's source-port is used. + If is specified on subscription, is prepended to the OSC-address of all messages + sent to the subscribed host. + If is specified on unsubscription, the subscribed host is only unsubscribed if the host, + port and prefix all match the subscription. + If is not specified on unsubscription, the subscribed host is unsubscribed if the host and port + match the subscription. + """ + if not isinstance(self.client, OSCMultiClient): + raise OSCServerError("Local %s does not support subsctiptions or message-filtering" % self.client.__class__.__name__) + + addr_cmd = addr.split('/')[-1] + + if len(data): + if data[0] in ('help', 'info'): + reply = OSCBundle(self.info_prefix) + reply.append(('server', str(self))) + reply.append(('subscribe_command', "ls | list : list subscribed targets")) + reply.append(('subscribe_command', "[subscribe | listen | sendto | target] [ ...] : subscribe to messages, set filters")) + reply.append(('subscribe_command', "[unsubscribe | silence | nosend | deltarget] : unsubscribe from messages")) + return reply + + if data[0] in ('ls', 'list'): + reply = OSCBundle(self.info_prefix) + for trg in self.client.getOSCTargetStrings(): + reply.append(('target',) + trg) + return reply + + if data[0] in ('subscribe', 'listen', 'sendto', 'target'): + return self._subscribe(data[1:], client_address) + + if data[0] in ('unsubscribe', 'silence', 'nosend', 'deltarget'): + return self._unsubscribe(data[1:], client_address) + + if addr_cmd in ('subscribe', 'listen', 'sendto', 'target'): + return self._subscribe(data, client_address) + + if addr_cmd in ('unsubscribe', 'silence', 'nosend', 'deltarget'): + return self._unsubscribe(data, client_address) + +class ForkingOSCServer(ForkingMixIn, OSCServer): + """An Asynchronous OSCServer. + This server forks a new process to handle each incoming request. + """ + # set the RequestHandlerClass, will be overridden by ForkingOSCServer & ThreadingOSCServer + RequestHandlerClass = ThreadingOSCRequestHandler + +class ThreadingOSCServer(ThreadingMixIn, OSCServer): + """An Asynchronous OSCServer. + This server starts a new thread to handle each incoming request. + """ + # set the RequestHandlerClass, will be overridden by ForkingOSCServer & ThreadingOSCServer + RequestHandlerClass = ThreadingOSCRequestHandler + +###### +# +# OSCError classes +# +###### + +class OSCError(Exception): + """Base Class for all OSC-related errors + """ + def __init__(self, message): + self.message = message + + def __str__(self): + return self.message + +class OSCClientError(OSCError): + """Class for all OSCClient errors + """ + pass + +class OSCServerError(OSCError): + """Class for all OSCServer errors + """ + pass + +class NoCallbackError(OSCServerError): + """This error is raised (by an OSCServer) when an OSCMessage with an 'unmatched' address-pattern + is received, and no 'default' handler is registered. + """ + def __init__(self, pattern): + """The specified 'pattern' should be the OSC-address of the 'unmatched' message causing the error to be raised. + """ + self.message = "No callback registered to handle OSC-address '%s'" % pattern + +class NotSubscribedError(OSCClientError): + """This error is raised (by an OSCMultiClient) when an attempt is made to unsubscribe a host + that isn't subscribed. + """ + def __init__(self, addr, prefix=None): + if prefix: + url = getUrlStr(addr, prefix) + else: + url = getUrlStr(addr, '') + + self.message = "Target osc://%s is not subscribed" % url + +###### +# +# OSC over streaming transport layers (usually TCP) +# +# Note from the OSC 1.0 specifications about streaming protocols: +# +# The underlying network that delivers an OSC packet is responsible for +# delivering both the contents and the size to the OSC application. An OSC +# packet can be naturally represented by a datagram by a network protocol such +# as UDP. In a stream-based protocol such as TCP, the stream should begin with +# an int32 giving the size of the first packet, followed by the contents of the +# first packet, followed by the size of the second packet, etc. +# +# The contents of an OSC packet must be either an OSC Message or an OSC Bundle. +# The first byte of the packet's contents unambiguously distinguishes between +# these two alternatives. +# +###### + +class OSCStreamRequestHandler(StreamRequestHandler, OSCAddressSpace): + """ This is the central class of a streaming OSC server. If a client + connects to the server, the server instantiates a OSCStreamRequestHandler + for each new connection. This is fundamentally different to a packet + oriented server which has a single address space for all connections. + This connection based (streaming) OSC server maintains an address space + for each single connection, because usually tcp server spawn a new thread + or process for each new connection. This would generate severe + multithreading synchronization problems when each thread would operate on + the same address space object. Therefore: To implement a streaming/TCP OSC + server a custom handler must be implemented which implements the + setupAddressSpace member in which it creates its own address space for this + very connection. This has been done within the testbench and can serve as + inspiration. + """ + def __init__(self, request, client_address, server): + """ Initialize all base classes. The address space must be initialized + before the stream request handler because the initialization function + of the stream request handler calls the setup member which again + requires an already initialized address space. + """ + self._txMutex = threading.Lock() + OSCAddressSpace.__init__(self) + StreamRequestHandler.__init__(self, request, client_address, server) + + def _unbundle(self, decoded): + """Recursive bundle-unpacking function""" + if decoded[0] != "#bundle": + self.replies += self.dispatchMessage(decoded[0], decoded[1][1:], decoded[2:], self.client_address) + return + + now = time.time() + timetag = decoded[1] + if (timetag > 0.) and (timetag > now): + time.sleep(timetag - now) + + for msg in decoded[2:]: + self._unbundle(msg) + + def setup(self): + StreamRequestHandler.setup(self) + print("SERVER: New client connection.") + self.setupAddressSpace() + self.server._clientRegister(self) + + def setupAddressSpace(self): + """ Override this function to customize your address space. """ + pass + + def finish(self): + StreamRequestHandler.finish(self) + self.server._clientUnregister(self) + print("SERVER: Client connection handled.") + def _transmit(self, data): + sent = 0 + while sent < len(data): + tmp = self.connection.send(data[sent:]) + if tmp == 0: + return False + sent += tmp + return True + def _transmitMsg(self, msg): + """Send an OSC message over a streaming socket. Raises exception if it + should fail. If everything is transmitted properly, True is returned. If + socket has been closed, False. + """ + if not isinstance(msg, OSCMessage): + raise TypeError("'msg' argument is not an OSCMessage or OSCBundle object") + + try: + binary = msg.getBinary() + length = len(binary) + # prepend length of packet before the actual message (big endian) + len_big_endian = array.array('c', '\0' * 4) + struct.pack_into(">L", len_big_endian, 0, length) + len_big_endian = len_big_endian.tostring() + if self._transmit(len_big_endian) and self._transmit(binary): + return True + return False + except socket.error as e: + if e[0] == errno.EPIPE: # broken pipe + return False + raise e + + def _receive(self, count): + """ Receive a certain amount of data from the socket and return it. If the + remote end should be closed in the meanwhile None is returned. + """ + chunk = self.connection.recv(count) + if not chunk or len(chunk) == 0: + return None + while len(chunk) < count: + tmp = self.connection.recv(count - len(chunk)) + if not tmp or len(tmp) == 0: + return None + chunk = chunk + tmp + return chunk + + def _receiveMsg(self): + """ Receive OSC message from a socket and decode. + If an error occurs, None is returned, else the message. + """ + # get OSC packet size from stream which is prepended each transmission + chunk = self._receive(4) + if chunk == None: + print("SERVER: Socket has been closed.") + return None + # extract message length from big endian unsigned long (32 bit) + slen = struct.unpack(">L", chunk)[0] + # receive the actual message + chunk = self._receive(slen) + if chunk == None: + print("SERVER: Socket has been closed.") + return None + # decode OSC data and dispatch + msg = decodeOSC(chunk) + if msg == None: + raise OSCError("SERVER: Message decoding failed.") + return msg + + def handle(self): + """ + Handle a connection. + """ + # set socket blocking to avoid "resource currently not available" + # exceptions, because the connection socket inherits the settings + # from the listening socket and this times out from time to time + # in order to provide a way to shut the server down. But we want + # clean and blocking behaviour here + self.connection.settimeout(None) + + print("SERVER: Entered server loop") + try: + while True: + decoded = self._receiveMsg() + if decoded == None: + return + elif len(decoded) <= 0: + # if message decoding fails we try to stay in sync but print a message + print("OSC stream server: Spurious message received.") + continue + + self.replies = [] + self._unbundle(decoded) + + if len(self.replies) > 1: + msg = OSCBundle() + for reply in self.replies: + msg.append(reply) + elif len(self.replies) == 1: + msg = self.replies[0] + else: + # no replies, continue receiving + continue + self._txMutex.acquire() + txOk = self._transmitMsg(msg) + self._txMutex.release() + if not txOk: + break + + except socket.error as e: + if e[0] == errno.ECONNRESET: + # if connection has been reset by client, we do not care much + # about it, we just assume our duty fullfilled + print("SERVER: Connection has been reset by peer.") + else: + raise e + + def sendOSC(self, oscData): + """ This member can be used to transmit OSC messages or OSC bundles + over the client/server connection. It is thread save. + """ + self._txMutex.acquire() + result = self._transmitMsg(oscData) + self._txMutex.release() + return result + +""" TODO Note on threaded unbundling for streaming (connection oriented) +transport: + +Threaded unbundling as implemented in ThreadingOSCServer must be implemented in +a different way for the streaming variant, because contrary to the datagram +version the streaming handler is instantiated only once per connection. This +leads to the problem (if threaded unbundling is implemented as in OSCServer) +that all further message reception is blocked until all (previously received) +pending messages are processed. + +Each StreamRequestHandler should provide a so called processing queue in which +all pending messages or subbundles are inserted to be processed in the future). +When a subbundle or message gets queued, a mechanism must be provided that +those messages get invoked when time asks for them. There are the following +opportunities: + - a timer is started which checks at regular intervals for messages in the + queue (polling - requires CPU resources) + - a dedicated timer is started for each message (requires timer resources) +""" + +class OSCStreamingServer(TCPServer): + """ A connection oriented (TCP/IP) OSC server. + """ + + # define a socket timeout, so the serve_forever loop can actually exit. + # with 2.6 and server.shutdown this wouldn't be necessary + socket_timeout = 1 + + # this is the class which handles a new connection. Override this for a + # useful customized server. See the testbench for an example + RequestHandlerClass = OSCStreamRequestHandler + + def __init__(self, address): + """Instantiate an OSCStreamingServer. + - server_address ((host, port) tuple): the local host & UDP-port + the server listens for new connections. + """ + self._clientList = [] + self._clientListMutex = threading.Lock() + TCPServer.__init__(self, address, self.RequestHandlerClass) + self.socket.settimeout(self.socket_timeout) + + def serve_forever(self): + """Handle one request at a time until server is closed. + Had to add this since 2.5 does not support server.shutdown() + """ + self.running = True + while self.running: + self.handle_request() # this times-out when no data arrives. + + def start(self): + """ Start the server thread. """ + self._server_thread = threading.Thread(target=self.serve_forever) + self._server_thread.setDaemon(True) + self._server_thread.start() + + def stop(self): + """ Stop the server thread and close the socket. """ + self.running = False + self._server_thread.join() + self.server_close() + # 2.6 only + #self.shutdown() + + def _clientRegister(self, client): + """ Gets called by each request/connection handler when connection is + established to add itself to the client list + """ + self._clientListMutex.acquire() + self._clientList.append(client) + self._clientListMutex.release() + + def _clientUnregister(self, client): + """ Gets called by each request/connection handler when connection is + lost to remove itself from the client list + """ + self._clientListMutex.acquire() + self._clientList.remove(client) + self._clientListMutex.release() + + def broadcastToClients(self, oscData): + """ Send OSC message or bundle to all connected clients. """ + result = True + for client in self._clientList: + result = result and client.sendOSC(oscData) + return result + +class OSCStreamingServerThreading(ThreadingMixIn, OSCStreamingServer): + pass + """ Implements a server which spawns a separate thread for each incoming + connection. Care must be taken since the OSC address space is for all + the same. + """ + +class OSCStreamingClient(OSCAddressSpace): + """ OSC streaming client. + A streaming client establishes a connection to a streaming server but must + be able to handle replies by the server as well. To accomplish this the + receiving takes place in a secondary thread, because no one knows if we + have to expect a reply or not, i.e. synchronous architecture doesn't make + much sense. + Replies will be matched against the local address space. If message + handlers access code of the main thread (where the client messages are sent + to the server) care must be taken e.g. by installing sychronization + mechanisms or by using an event dispatcher which can handle events + originating from other threads. + """ + # set outgoing socket buffer size + sndbuf_size = 4096 * 8 + rcvbuf_size = 4096 * 8 + + def __init__(self): + self._txMutex = threading.Lock() + OSCAddressSpace.__init__(self) + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, self.sndbuf_size) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, self.rcvbuf_size) + self.socket.settimeout(1.0) + self._running = False + + def _receiveWithTimeout(self, count): + chunk = str() + while len(chunk) < count: + try: + tmp = self.socket.recv(count - len(chunk)) + except socket.timeout: + if not self._running: + print("CLIENT: Socket timed out and termination requested.") + return None + else: + continue + except socket.error as e: + if e[0] == errno.ECONNRESET: + print("CLIENT: Connection reset by peer.") + return None + else: + raise e + if not tmp or len(tmp) == 0: + print("CLIENT: Socket has been closed.") + return None + chunk = chunk + tmp + return chunk + def _receiveMsgWithTimeout(self): + """ Receive OSC message from a socket and decode. + If an error occurs, None is returned, else the message. + """ + # get OSC packet size from stream which is prepended each transmission + chunk = self._receiveWithTimeout(4) + if not chunk: + return None + # extract message length from big endian unsigned long (32 bit) + slen = struct.unpack(">L", chunk)[0] + # receive the actual message + chunk = self._receiveWithTimeout(slen) + if not chunk: + return None + # decode OSC content + msg = decodeOSC(chunk) + if msg == None: + raise OSCError("CLIENT: Message decoding failed.") + return msg + + def _receiving_thread_entry(self): + print("CLIENT: Entered receiving thread.") + self._running = True + while self._running: + decoded = self._receiveMsgWithTimeout() + if not decoded: + break + elif len(decoded) <= 0: + continue + + self.replies = [] + self._unbundle(decoded) + if len(self.replies) > 1: + msg = OSCBundle() + for reply in self.replies: + msg.append(reply) + elif len(self.replies) == 1: + msg = self.replies[0] + else: + continue + self._txMutex.acquire() + txOk = self._transmitMsgWithTimeout(msg) + self._txMutex.release() + if not txOk: + break + print("CLIENT: Receiving thread terminated.") + + def _unbundle(self, decoded): + if decoded[0] != "#bundle": + self.replies += self.dispatchMessage(decoded[0], decoded[1][1:], decoded[2:], self.socket.getpeername()) + return + + now = time.time() + timetag = decoded[1] + if (timetag > 0.) and (timetag > now): + time.sleep(timetag - now) + + for msg in decoded[2:]: + self._unbundle(msg) + + def connect(self, address): + self.socket.connect(address) + self.receiving_thread = threading.Thread(target=self._receiving_thread_entry) + self.receiving_thread.start() + + def close(self): + # let socket time out + self._running = False + self.receiving_thread.join() + self.socket.close() + + def _transmitWithTimeout(self, data): + sent = 0 + while sent < len(data): + try: + tmp = self.socket.send(data[sent:]) + except socket.timeout: + if not self._running: + print("CLIENT: Socket timed out and termination requested.") + return False + else: + continue + except socket.error as e: + if e[0] == errno.ECONNRESET: + print("CLIENT: Connection reset by peer.") + return False + else: + raise e + if tmp == 0: + return False + sent += tmp + return True + + def _transmitMsgWithTimeout(self, msg): + if not isinstance(msg, OSCMessage): + raise TypeError("'msg' argument is not an OSCMessage or OSCBundle object") + binary = msg.getBinary() + length = len(binary) + # prepend length of packet before the actual message (big endian) + len_big_endian = array.array('c', '\0' * 4) + struct.pack_into(">L", len_big_endian, 0, length) + len_big_endian = len_big_endian.tostring() + if self._transmitWithTimeout(len_big_endian) and self._transmitWithTimeout(binary): + return True + else: + return False + + def sendOSC(self, msg): + """Send an OSC message or bundle to the server. Returns True on success. + """ + self._txMutex.acquire() + txOk = self._transmitMsgWithTimeout(msg) + self._txMutex.release() + return txOk + + def __str__(self): + """Returns a string containing this Client's Class-name, software-version + and the remote-address it is connected to (if any) + """ + out = self.__class__.__name__ + out += " v%s.%s-%s" % version + addr = self.socket.getpeername() + if addr: + out += " connected to osc://%s" % getUrlStr(addr) + else: + out += " (unconnected)" + + return out + + def __eq__(self, other): + """Compare function. + """ + if not isinstance(other, self.__class__): + return False + + isequal = cmp(self.socket._sock, other.socket._sock) + if isequal and self.server and other.server: + return cmp(self.server, other.server) + + return isequal + + def __ne__(self, other): + """Compare function. + """ + return not self.__eq__(other) diff --git a/clitools/generators/__pycache__/OSC3.cpython-38.pyc b/clitools/generators/__pycache__/OSC3.cpython-38.pyc new file mode 100644 index 0000000..852f67e Binary files /dev/null and b/clitools/generators/__pycache__/OSC3.cpython-38.pyc differ diff --git a/clitools/generators/__pycache__/turtle.cpython-38.pyc b/clitools/generators/__pycache__/turtle.cpython-38.pyc new file mode 100644 index 0000000..5b818f5 Binary files /dev/null and b/clitools/generators/__pycache__/turtle.cpython-38.pyc differ diff --git a/clitools/generators/blank.py b/clitools/generators/blank.py new file mode 100755 index 0000000..657bc5a --- /dev/null +++ b/clitools/generators/blank.py @@ -0,0 +1,53 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + + +''' + +Send only black points +v0.1.0 + +Use it to test your filters and outputs + +LICENCE : CC + +by cocoa + +''' + +from __future__ import print_function +import time +import argparse +import sys +name="generator::dummy" + + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + +argsparser = argparse.ArgumentParser(description="dummy generator") +argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output") +args = argsparser.parse_args() + +fps=args.fps +verbose=args.verbose +optimal_looptime = 1 / fps +debug(name+" optimal looptime "+str(optimal_looptime)) + + +shape = [[400,400,0],[400,400,64],[400,400,0]] + + +while True: + start = time.time() + print(shape, flush=True); + looptime = time.time() - start + if( looptime < optimal_looptime ): + time.sleep( optimal_looptime - looptime) + debug(name+" micro sleep:"+str( optimal_looptime - looptime)) + + diff --git a/clitools/generators/book2.ild b/clitools/generators/book2.ild new file mode 100755 index 0000000..608803e Binary files /dev/null and b/clitools/generators/book2.ild differ diff --git a/clitools/generators/brmlab1.svg b/clitools/generators/brmlab1.svg new file mode 100755 index 0000000..05bf423 --- /dev/null +++ b/clitools/generators/brmlab1.svg @@ -0,0 +1,54 @@ + +image/svg+xml + + \ No newline at end of file diff --git a/clitools/generators/brmlab2.svg b/clitools/generators/brmlab2.svg new file mode 100755 index 0000000..5c3a447 --- /dev/null +++ b/clitools/generators/brmlab2.svg @@ -0,0 +1,53 @@ + +image/svg+xml + \ No newline at end of file diff --git a/clitools/generators/dummy.py b/clitools/generators/dummy.py new file mode 100755 index 0000000..8f7eb2c --- /dev/null +++ b/clitools/generators/dummy.py @@ -0,0 +1,85 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + + +''' + +This is the most basic generator you can imagine: straight up static! +v0.1.0 + +Use it to test your filters and outputs + +LICENCE : CC + +by cocoa + +''' + +from __future__ import print_function +import time +import argparse +import sys +name="generator::dummy" + + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + +argsparser = argparse.ArgumentParser(description="dummy generator") +argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int) +argsparser.add_argument("-s","--speed",help="point per frame progress",default=3,type=int) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output") +args = argsparser.parse_args() + +fps=args.fps +verbose=args.verbose +optimal_looptime = 1 / fps +debug(name+" optimal looptime "+str(optimal_looptime)) +color = 16777215 +square = [[100.0, 100.0, color], [100.0, 500.0, color], [500.0, 500.0, color], [500.0, 100.0, color], [100.0, 100.0, color]] +line =[] +for i in range(00,800,int(800/120)): + line.append([i, 400, color]) +square = [[100.0, 100.0, color], [100.0, 500.0, color], [500.0, 500.0, color], [500.0, 100.0, color], [100.0, 100.0, color]] +mire = [ + [600,600,0], + [600,600,color], + [700,600,color], + [700,700,color], + [600,700,color], + [600,600,color], + [100,100,0], + [100,100,color], + [200,100,color], + [200,200,color], + [100,200,color], + [100,100,color], + [0,0,0], + [0,0,color], + [800,0,color], + [800,800,color], + [0,800,color], + [0,0,color], + [350,400,0], + [350,400,color], + [450,400,color], + [400,350,0], + [400,350,color], + [400,450,color], +] + +shape = mire + + +while True: + start = time.time() + print(shape, flush=True); + looptime = time.time() - start + if( looptime < optimal_looptime ): + time.sleep( optimal_looptime - looptime) + debug(name+" micro sleep:"+str( optimal_looptime - looptime)) + + diff --git a/clitools/generators/example.py b/clitools/generators/example.py new file mode 100755 index 0000000..b1ad01c --- /dev/null +++ b/clitools/generators/example.py @@ -0,0 +1,182 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + + +''' + +example, based on custom +v0.1.0 + +A copy of square.py you can modify to code your plugin. +custom1 has necessary hooks in LJ.conf, webui and so on. + + +LICENCE : CC + +by Sam Neurohack + + +''' +import sys +import os +ljpath = r'%s' % os.getcwd().replace('\\','/') + +# import from shell +sys.path.append(ljpath +'/../../libs/') + +#import from LJ +sys.path.append(ljpath +'/libs/') +print(ljpath+'/../libs/') + +import lj23layers as lj + +sys.path.append('../libs') +import math +import time +import argparse + + +print ("") +print ("Arguments parsing if needed...") +argsparser = argparse.ArgumentParser(description="Custom1 example for LJ") +argsparser.add_argument("-v","--verbose",help="Verbosity level (0 by default)",default=0,type=int) +args = argsparser.parse_args() + +# Useful variables init. +white = lj.rgb2int(255,255,255) +red = lj.rgb2int(255,0,0) +blue = lj.rgb2int(0,0,255) +green = lj.rgb2int(0,255,0) + +width = 800 +height = 600 +centerX = width / 2 +centerY = height / 2 + +# 3D to 2D projection parameters +fov = 256 +viewer_distance = 2.2 + +# Anaglyph computation parameters for right and left eyes. +# algorythm come from anaglyph geo maps +eye_spacing = 100 +nadir = 0.5 +observer_altitude = 30000 +map_layerane_altitude = 0.0 + +# square coordinates : vertices that compose each of the square. +vertices = [ + (- 1.0, 1.0,- 1.0), + ( 1.0, 1.0,- 1.0), + ( 1.0,- 1.0,- 1.0), + (- 1.0,- 1.0,- 1.0) + ] + +face = [0,1,2,3] + +# +# LJ inits +# + +layer = 0 + +# Define properties for each drawn "element" : name, intensity, active, xy, color, red, green, blue, layer , closed +Leftsquare = lj.FixedObject('Leftsquare', True, 255, [], red, 255, 0, 0, layer , True) +Rightsquare = lj.FixedObject('Rightsquare', True, 255, [], green, 0, 255, 0, layer , True) + +# 'Destination' for given layer : name, number, active, layer , scene, laser +Dest0 = lj.DestObject('0', 0, True, 0 , 0, 0) # Dest0 will send layer 0 points to scene 0, laser 0 + + +# +# Anaglyph computation : different X coordinate for each eye +# + +def LeftShift(elevation): + + diff = elevation - map_layerane_altitude + return nadir * eye_spacing * diff / (observer_altitude - elevation) + +def RightShift(elevation): + + diff = map_layerane_altitude - elevation + return (1 - nadir) * eye_spacing * diff / (observer_altitude - elevation) + + +def Proj(x,y,z,angleX,angleY,angleZ): + + rad = angleX * math.pi / 180 + cosa = math.cos(rad) + sina = math.sin(rad) + y2 = y + y = y2 * cosa - z * sina + z = y2 * sina + z * cosa + + rad = angleY * math.pi / 180 + cosa = math.cos(rad) + sina = math.sin(rad) + z2 = z + z = z2 * cosa - x * sina + x = z2 * sina + x * cosa + + rad = angleZ * math.pi / 180 + cosa = math.cos(rad) + sina = math.sin(rad) + x2 = x + x = x2 * cosa - y * sina + y = x2 * sina + y * cosa + + + """ Transforms this 3D point to 2D using a perspective projection. """ + factor = fov / (viewer_distance + z) + x = x * factor + centerX + y = - y * factor + centerY + return (x,y) + + +# +# Main +# + +def Run(): + Left = [] + Right = [] + counter =0 + try: + while True: + Left = [] + Right = [] + x = vertices[0][0] + y = vertices[0][1] + z = vertices[0][2] + + # lj tracers will "move" the laser to this first point in black, then move to the next with second point color. + # for more accuracy in dac emulator, repeat this first point. + + # generate all points in square. + for point in face: + x = vertices[point][0] + y = vertices[point][1] + z = vertices[point][2] + left.append(proj(x+leftshift(z*25),y,z,0,counter,0)) + right.append(proj(x+rightshift(z*25),y,z,0,counter,0)) + + + lj.polylineonecolor(left, c = leftsquare.color , layer = leftsquare.layer, closed = leftsquare.closed) + lj.polylineonecolor(right, c = rightsquare.color , layer = rightsquare.layer, closed = rightsquare.closed) + lj.drawdests() + time.sleep(0.1) + counter += 1 + if counter > 360: + counter = 0 + + except KeyboardInterrupt: + pass + + # Gently stop on CTRL C + finally: + lj.ClosePlugin() + + +Run() diff --git a/clitools/generators/fromGML.py b/clitools/generators/fromGML.py new file mode 100644 index 0000000..7576b8f --- /dev/null +++ b/clitools/generators/fromGML.py @@ -0,0 +1,406 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + +''' + +fromGML +v0.1.0 + +Display a GML file + +See GML specs at the end. +Support the gml spec="1.0 (minimum)" +and header/client/name +and maybe one day drawing/brush/color + +LICENCE : CC +by cocoa and Sam Neurohack + +Heavy use of : https://github.com/kgn/pygml +''' +from __future__ import print_function +import time +import struct +import argparse +import sys +import xml.etree.ElementTree as etree +#import urllib +from datetime import datetime +import math, random +import ast + +name="generator::fromgml" + + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + +argsparser = argparse.ArgumentParser(description="GML file frame generator") +argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output") +argsparser.add_argument("-g","--gml",help=".gml file",default="147.gml",type=str) +argsparser.add_argument("-t","--total",help="Total time",default=32,type=int) +argsparser.add_argument("-m","--mode",help="once or anim mode",default="anim",type=str) +argsparser.add_argument("-s","--skip",help="% of points to skip",default="0.4",type=float) +argsparser.add_argument("-r","--rot",help="(angleX, angleY, angleZ) in degree",default="(0,0,270)",type=str) +args = argsparser.parse_args() + +fps=args.fps +verbose=args.verbose +mode = args.mode +optimal_looptime = 1 / fps +angles = ast.literal_eval(args.rot) +debug(name+" optimal frame time "+str(optimal_looptime)) + + +TOTAL_TIME=float(args.total) +TIME_STRETCH = 1 +ZOOM=1.0 +DELTA = 7 +width = 500 +height = 500 +centerX = width / 2 +centerY = height / 2 +# 3D to 2D projection parameters +fov = 200 +viewer_distance = 2.2 + +skip = args.skip +#skip is the percentage of points that we ignore in order to render +# faster in the laser display. Unfortunately we are not able to render too +# complex content in our display without resulting in a lot of blinking. + +# return a list with all points +def readGML(filename): + + outputData = [] + tree = etree.parse(filename) + root = tree.getroot() + ''' + if (root.tag.lower() != "gml"): + print("Not a GML file.") + return + ''' + #~ + tag = root.find("tag") + header = tag.find("header") + if header != None: + client = header.find("client") + if client != None: + debug("Graffiti name :", client.find("name").text) + drawing = tag.find("drawing") + environment = header.find("environment") + if not environment: + environment = tag.find("environment") + #screenBounds = environment.find("screenBounds") + #globalScale = (1.0,1.0,1.0) + #dim = (float(screenBounds.find("x").text) * globalScale[0], float(screenBounds.find("y").text) * globalScale[1], float(screenBounds.find("z").text) * globalScale[2]) + #dim = (40.0,40.0,40.0) + #~ + strokes = drawing.findall("stroke") + for stroke in strokes: + pointsEl = stroke.findall("pt") + for pointEl in pointsEl: + + x = float(pointEl.find("x").text) - 0.5 + y = float(pointEl.find("y").text) - 0.5 + z = float(pointEl.find("z").text) - 0.5 + transpoint = Rot(x,y,z,angles[0],angles[1],angles[2]) + x = (transpoint[0]*ZOOM*width/2) + (width/2) + y = (transpoint[1]*ZOOM*height/2) + (height/2) + z = transpoint[2] + # WIDTH/2 + ZOOM*point[0]*WIDTH/2, HEIGHT/2 + ZOOM*point[1]*HEIGHT/2 + time = float(pointEl.find("time").text) + outputData.append([x,y,z,time]) + #print(outputData) + return outputData + +def Rot(x, y, z, angleX, angleY, angleZ): + + rad = angleX * math.pi / 180 + cosa = math.cos(rad) + sina = math.sin(rad) + y2 = y + y = y2 * cosa - z * sina + z = y2 * sina + z * cosa + + rad = angleY * math.pi / 180 + cosa = math.cos(rad) + sina = math.sin(rad) + z2 = z + z = z2 * cosa - x * sina + x = z2 * sina + x * cosa + + rad = angleZ * math.pi / 180 + cosa = math.cos(rad) + sina = math.sin(rad) + x2 = x + x = x2 * cosa - y * sina + y = x2 * sina + y * cosa + + return (x,y,z) + + +#[x,y,z,time] +def iterPoints(): + + for point in gml: + yield point + + +# Play once during total time arg +def Once(): + + debug(name,"play once mode") + shape = [] + for point in gml: + shape.append([point[0],point[1], 65535]) + debug(name + str(shape)) + + t0=datetime.now() + deltat=0 + + while deltat TOTAL_TIME: + t0=datetime.now() + + first=True + shape = [] + for point in iterPoints(): + if point[3] <= deltat and deltat <= point[3]+DELTA and random.random()<(1-skip): + if first: + first=False + else: + #LD.draw_point(WIDTH/2 + ZOOM*point.x*WIDTH/2, HEIGHT/2 + ZOOM*point.y*HEIGHT/2) + shape.append([point[0], point[1], 65535]) + print(shape, flush=True); + + +debug(name + " Reading : "+args.gml+" in "+mode+" mode.") +gml = readGML(args.gml) + +debug(name + " total points : "+ str(len(gml))) + +if mode =="once": + Once() +else: + Anim() + +debug(name + " ends.") +exit() +''' + + + + + + + + 0.0 + 0.0 + + + + + + + + + + + + + +
+ + + Laser Tag + 2.0 + MyUserName + http://000000book.com/data/156/ + katsu,paris,2010 + 28sks922ks992 + 192.168.1.1 + + + -39.392922 + 53.29292 + + + + + + + + + + 0.0 + 0.0 + 0.0 + + + 0.0 + 0.0 + 0.0 + + + 0.0 + -1.0 + 0.0 + + + 1024 + 768 + 0 + + + 0 + 0 + 0 + + + 1000 + 600 + 0 + cm + + + yourimage.jpg + + +
+ + + + + + + + 0.0 + 0.0 + 0.0 + 0.013 + + + + + + + + + + 0 + LaserTagArrowLetters + + http://aurltodescribethebrushspec.com/someSpec.xml + 10 + 1.5 + 1.0 + 1.0 + 0 + + 255 + 255 + 255 + 255 + + + 0 + 1 + 0 + + + + + 0.0 + 0.0 + 0.0 + 0.013 + + + + 0.0 + 0.0 + 0.0 + 0.023 + + + + + + + + + + true + + + + 255 + 255 + 0 + + + -1 + + + + + 0.0 + 0.0 + + + + 0.0 + 0.0 + + + + + + + 0.5 + 0.5 + + + + + + + + + +
+ +
+''' \ No newline at end of file diff --git a/clitools/generators/fromOSC.py b/clitools/generators/fromOSC.py new file mode 100644 index 0000000..d8005c5 --- /dev/null +++ b/clitools/generators/fromOSC.py @@ -0,0 +1,126 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + +''' +Forward /pl pointlist to cli + +input OSC in END points format : (x,y,color) +output CLI in CLI points format : [x,y,color] + +/pl "[(150.0, 230.0, 255), (170.0, 170.0, 255), (230.0, 170.0, 255), (210.0, 230.0, 255), (150.0, 230.0, 255)]" + +v0.1.0 + +LICENCE : CC + +by Cocoa, Sam Neurohack + +''' +from __future__ import print_function +from OSC3 import OSCServer, OSCClient, OSCMessage +import sys +from time import sleep +import argparse +import ast + +argsparser = argparse.ArgumentParser(description="fromOSC generator") +argsparser.add_argument("-i","--ip",help="IP to bind to (0.0.0.0 by default)",default="0.0.0.0",type=str) +argsparser.add_argument("-p","--port",help="OSC port to bind to (9002 by default)",default=9002,type=str) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output") +args = argsparser.parse_args() + +verbose = args.verbose +ip = args.ip +port = int(args.port) + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + +oscserver = OSCServer( (ip, port) ) +oscserver.timeout = 0 +run = True + +# this method of reporting timeouts only works by convention +# that before calling handle_request() field .timed_out is +# set to False +def handle_timeout(self): + self.timed_out = True + +# funny python's way to add a method to an instance of a class +import types +oscserver.handle_timeout = types.MethodType(handle_timeout, oscserver) + +# RAW OSC Frame available ? +def OSC_frame(): + # clear timed_out flag + oscserver.timed_out = False + # handle all pending requests then return + while not oscserver.timed_out: + oscserver.handle_request() + + +# default handler +def OSChandler(oscpath, tags, args, source): + + oscaddress = ''.join(oscpath.split("/")) + debug("fromOSC Default OSC Handler got oscpath", oscpath, "from" + str(source[0]), ":", args) + #print("OSC address", path) + #print("find.. /bhoreal ?", path.find('/bhoreal')) + + if oscpath == "/pl" and len(args)==1: + + debug("correct OSC type :'/pl") + + if validate(args[0]) == True: + + debug("new pl : ", args[0]) + line = args[0].replace("(",'[') + line = line.replace(")",']') + line = "[{}]".format(line) + print(line, flush=True); + + else: + debug("Bad pointlist -> msg trapped.") + + else: + debug("BAD OSC Message : " + oscpath +" " +args[0]) + + +oscserver.addMsgHandler( "default", OSChandler ) + + +def validate(pointlist): + + state = True + + if len(pointlist)<9: + debug("Not enough characters :", pointlist) + state = False + + if pointlist.find("(") == -1: + debug("Bad format : use () not [] for points", pointlist) + state = False + + try: + pl = bytes(pointlist, 'ascii') + check = ast.literal_eval(pl.decode('ascii')) + + except: + debug("BAD POINTLIST :", pointlist) + state = False + + return state + + +# simulate a "game engine" +while run: + # do the game stuff: + sleep(0.01) + # call user script + OSC_frame() + +oscserver.close() + diff --git a/clitools/generators/fromRedis.py b/clitools/generators/fromRedis.py new file mode 100755 index 0000000..8c65951 --- /dev/null +++ b/clitools/generators/fromRedis.py @@ -0,0 +1,72 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + + +''' + +This generator reads a frame from redis +v0.1.0 + +Use it to create feedback loops by writing to the same frame +or to copy the frame from someone else + +LICENCE : CC + +by cocoa + +''' + +from __future__ import print_function +import ast +import argparse +import json +import redis +import sys +import time +name="generator::fromRedis" + + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + + +argsparser = argparse.ArgumentParser(description="Dummy generator") +argsparser.add_argument("-k","--key",required=True,help="Redis key to look after",default=30,type=str) +argsparser.add_argument("-i","--ip",help="IP address of the Redis server ",default="127.0.0.1",type=str) +argsparser.add_argument("-p","--port",help="Port of the Redis server ",default="6379",type=str) +argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output") +args = argsparser.parse_args() + +fps = args.fps +verbose = args.verbose +key = args.key +ip = args.ip +port = args.port +optimal_looptime = 1 / fps +debug(name+" optimal looptime "+str(optimal_looptime)) + +r = redis.Redis( + host=ip, + port=port) + +while True: + start = time.time() + # Read from Redis + line = r.get(key) + # Decode as list of tuples + pointsList = ast.literal_eval(line.decode('ascii')) + # convert to list of lists + pointsList = [list(elem) for elem in pointsList] + # Convert to JSON string + line = json.dumps( pointsList ) + debug(name,"Key:{} line:{}".format(key,line)) + print(line, flush=True); + looptime = time.time() - start + if( looptime < optimal_looptime ): + time.sleep( optimal_looptime - looptime) + debug(name+" micro sleep:"+str( optimal_looptime - looptime)) + diff --git a/clitools/generators/fromUDP.py b/clitools/generators/fromUDP.py new file mode 100644 index 0000000..5f6e5e9 --- /dev/null +++ b/clitools/generators/fromUDP.py @@ -0,0 +1,84 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + +''' + +fromUDP + +Udp server to cli +v0.1b + +''' +from __future__ import print_function +import traceback, time +import argparse +import socket +import _thread +import sys + +name="generator::fromUDP" + + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + + +argsparser = argparse.ArgumentParser(description="fromUDP v0.1b help mode") +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output") +argsparser.add_argument("-i","--ip",help="IP to bind to (0.0.0.0 by default)",default="0.0.0.0",type=str) +argsparser.add_argument("-p","--port",help="UDP port to bind to (9000 by default)",default=9000,type=str) +args = argsparser.parse_args() + +verbose = args.verbose +ip = args.ip +port = int(args.port) +verbose = args.verbose + + + +def udp_thread(): + + while True: + + payload, client_address = sock.recvfrom(1024) + udpath = payload.decode('utf_8') + debug(udpath[0:]) + print(udpath[0:], flush=True); + + ''' + # Reply to client + bytesToSend = str.encode("ACK :"+str(payload)) + serverAddressPort = (client_address, port) + bufferSize = 1024 + #sock.sendto(bytesToSend, serverAddressPort) + sock.sendto(bytesToSend, client_address) + ''' + +def StartUDP(serverIP, UDPORT): + global sock + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + server = ( serverIP,UDPORT) + sock.bind(server) + _thread.start_new_thread(udp_thread, ()) + + +StartUDP(ip, port) + + +# Do something else +try: + + while True: + time.sleep(0.005) + +except Exception: + traceback.print_exc() + + + + + diff --git a/clitools/generators/fromilda.py b/clitools/generators/fromilda.py new file mode 100644 index 0000000..e6547e5 --- /dev/null +++ b/clitools/generators/fromilda.py @@ -0,0 +1,319 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + +''' + +fromild +v0.1.0 + +Read/display once an .ild animation file and quit ?? +LICENCE : CC + +by cocoa and Sam Neurohack + +Heavy u-se of : + +ILDA.py + +Python module for dealing with the ILDA Image Data Transfer Format, +an interchange format for laser image frames. + +Copyright (c) 2008 Micah Dowty + + 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: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + 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. + +''' + +from __future__ import print_function +import time +import struct +import argparse +import sys + +name="generator::fromild" + + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + +argsparser = argparse.ArgumentParser(description=".ild file frame generator") +argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output") +argsparser.add_argument("-i","--ild",help=".ild file",default="book2.ild",type=str) +args = argsparser.parse_args() + +fps=args.fps +verbose=args.verbose + +optimal_looptime = 1 / fps +debug(name+" optimal looptime "+str(optimal_looptime)) + +# Format codes +FORMAT_3D = 0 +FORMAT_2D = 1 +FORMAT_COLOR_TABLE = 2 + +# Mapping from FORMAT_* codes to struct format strings +formatTable = ( + '>hhhH', + '>hhH', + '>BBB', + ) + +# Header values +HEADER_MAGIC = b"ILDA\0\0\0" +HEADER_RESERVED = 0 +HEADER_FORMAT = ">7sB16sHHHBB" +HEADER_LEN = struct.calcsize(HEADER_FORMAT) + +# 64 default colors table : use rgb2int(colors64[ildacolor]) +colors64 = [[255, 0, 0], [255, 17, 0], [255, 34, 0], [255, 51, 0], [255, 68, 0], [255, 85, 0], [255, 102, 0], [255, 119, 0], [255, 136, 0], [255, 153, 0], [255, 170, 0], [255, 187, 0], [255, 204, 0], [255, 221, 0], [255, 238, 0], [255, 255, 0], [255, 255, 0], [238, 255, 0], [204, 255, 0], [170, 255, 0], [136, 255, 0], [102, 255, 0], [68, 255, 0], [34, 255, 0], [0, 255, 0], [0, 255, 34], [0, 255, 68], [0, 255, 102], [0, 255, 136], [0, 255, 170], [0, 255, 204], [0, 255, 238], [0, 136, 255], [0, 119, 255], [0, 102, 255], [0, 102, 255], [0, 85, 255], [0, 68, 255], [0, 68, 255], [0, 34, 255], [0, 0, 255], [34, 0, 255], [68, 0, 255], [102, 0, 255], [136, 0, 255], [170, 0, 255], [204, 0, 255], [238, 0, 255], [255, 0, 255], [255, 34, 255], [255, 68, 255], [255, 102, 255], [255, 136, 255], [255, 170, 255], [255, 204, 255], [255, 238, 255], [255, 255, 255], [255, 238, 238], [255, 204, 204], [255, 170, 170], [255, 136, 136], [255, 102, 102], [255, 68, 68], [0, 34, 34]] + + +# 256 default colors table +colors256 = [[0, 0, 0], [255, 255, 255], [255, 0, 0], [255, 255, 0], [0, 255, 0], [0, 255, 255], [0, 0, 255], [255, 0, 255], [255, 128, 128], [255, 140, 128], [255, 151, 128], [255, 163, 128], [255, 174, 128], [255, 186, 128], [255, 197, 128], [255, 209, 128], [255, 220, 128], [255, 232, 128], [255, 243, 128], [255, 255, 128], [243, 255, 128], [232, 255, 128], [220, 255, 128], [209, 255, 128], [197, 255, 128], [186, 255, 128], [174, 255, 128], [163, 255, 128], [151, 255, 128], [140, 255, 128], [128, 255, 128], [128, 255, 140], [128, 255, 151], [128, 255, 163], [128, 255, 174], [128, 255, 186], [128, 255, 197], [128, 255, 209], [128, 255, 220], [128, 255, 232], [128, 255, 243], [128, 255, 255], [128, 243, 255], [128, 232, 255], [128, 220, 255], [128, 209, 255], [128, 197, 255], [128, 186, 255], [128, 174, 255], [128, 163, 255], [128, 151, 255], [128, 140, 255], [128, 128, 255], [140, 128, 255], [151, 128, 255], [163, 128, 255], [174, 128, 255], [186, 128, 255], [197, 128, 255], [209, 128, 255], [220, 128, 255], [232, 128, 255], [243, 128, 255], [255, 128, 255], [255, 128, 243], [255, 128, 232], [255, 128, 220], [255, 128, 209], [255, 128, 197], [255, 128, 186], [255, 128, 174], [255, 128, 163], [255, 128, 151], [255, 128, 140], [255, 0, 0], [255, 23, 0], [255, 46, 0], [255, 70, 0], [255, 93, 0], [255, 116, 0], [255, 139, 0], [255, 162, 0], [255, 185, 0], [255, 209, 0], [255, 232, 0], [255, 255, 0], [232, 255, 0], [209, 255, 0], [185, 255, 0], [162, 255, 0], [139, 255, 0], [116, 255, 0], [93, 255, 0], [70, 255, 0], [46, 255, 0], [23, 255, 0], [0, 255, 0], [0, 255, 23], [0, 255, 46], [0, 255, 70], [0, 255, 93], [0, 255, 116], [0, 255, 139], [0, 255, 162], [0, 255, 185], [0, 255, 209], [0, 255, 232], [0, 255, 255], [0, 232, 255], [0, 209, 255], [0, 185, 255], [0, 162, 255], [0, 139, 255], [0, 116, 255], [0, 93, 255], [0, 70, 255], [0, 46, 255], [0, 23, 255], [0, 0, 255], [23, 0, 255], [46, 0, 255], [70, 0, 255], [93, 0, 255], [116, 0, 255], [139, 0, 255], [162, 0, 255], [185, 0, 255], [209, 0, 255], [232, 0, 255], [255, 0, 255], [255, 0, 232], [255, 0, 209], [255, 0, 185], [255, 0, 162], [255, 0, 139], [255, 0, 116], [255, 0, 93], [255, 0, 70], [255, 0, 46], [255, 0, 23], [128, 0, 0], [128, 12, 0], [128, 23, 0], [128, 35, 0], [128, 47, 0], [128, 58, 0], [128, 70, 0], [128, 81, 0], [128, 93, 0], [128, 105, 0], [128, 116, 0], [128, 128, 0], [116, 128, 0], [105, 128, 0], [93, 128, 0], [81, 128, 0], [70, 128, 0], [58, 128, 0], [47, 128, 0], [35, 128, 0], [23, 128, 0], [12, 128, 0], [0, 128, 0], [0, 128, 12], [0, 128, 23], [0, 128, 35], [0, 128, 47], [0, 128, 58], [0, 128, 70], [0, 128, 81], [0, 128, 93], [0, 128, 105], [0, 128, 116], [0, 128, 128], [0, 116, 128], [0, 105, 128], [0, 93, 128], [0, 81, 128], [0, 70, 128], [0, 58, 128], [0, 47, 128], [0, 35, 128], [0, 23, 128], [0, 12, 128], [0, 0, 128], [12, 0, 128], [23, 0, 128], [35, 0, 128], [47, 0, 128], [58, 0, 128], [70, 0, 128], [81, 0, 128], [93, 0, 128], [105, 0, 128], [116, 0, 128], [128, 0, 128], [128, 0, 116], [128, 0, 105], [128, 0, 93], [128, 0, 81], [128, 0, 70], [128, 0, 58], [128, 0, 47], [128, 0, 35], [128, 0, 23], [128, 0, 12], [255, 192, 192], [255, 64, 64], [192, 0, 0], [64, 0, 0], [255, 255, 192], [255, 255, 64], [192, 192, 0], [64, 64, 0], [192, 255, 192], [64, 255, 64], [0, 192, 0], [0, 64, 0], [192, 255, 255], [64, 255, 255], [0, 192, 192], [0, 64, 64], [192, 192, 255], [64, 64, 255], [0, 0, 192], [0, 0, 64], [255, 192, 255], [255, 64, 255], [192, 0, 192], [64, 0, 64], [255, 96, 96], [255, 255, 255], [245, 245, 245], [235, 235, 235], [224, 224, 224], [213, 213, 213], [203, 203, 203], [192, 192, 192], [181, 181, 181], [171, 171, 171], [160, 160, 160], [149, 149, 149], [139, 139, 139], [128, 128, 128], [117, 117, 117], [107, 107, 107], [96, 96, 96], [85, 85, 85], [75, 75, 75], [64, 64, 64], [53, 53, 53], [43, 43, 43], [32, 32, 32], [21, 21, 21], [11, 11, 11], [0, 0, 0]] + +def rgb2int(rgb): + return int('0x%02x%02x%02x' % tuple(rgb),0) + +class Table(object): + """Container object for one ILDA table: either a frame (table of points) + or a palette (table of colors). + + The 'items' list contains the data within this table. Each item + is a tuple, corresponding to the raw values within that row of the + table. + + 2D frame: (x, y, status) + 3D frame: (x, y, z, status) + Color: (r, g, b) + + """ + def __init__(self, format=FORMAT_2D, name="", + length=0, number=0, total=0, scanHead=0): + self.__dict__.update(locals()) + self.items = [] + self.itemsproducer = None + + def __repr__(self): + return ("" % + (self.format, self.name, self.length, self.number, + self.total, self.scanHead)) + + def unpackHeader(self, data): + magic, self.format, self.name, self.length, \ + self.number, self.total, self.scanHead, \ + reserved = struct.unpack(HEADER_FORMAT, data) + print(magic, HEADER_MAGIC) + if magic != HEADER_MAGIC: + raise ValueError("Bad ILDA header magic. Not an ILDA file?") + if reserved != HEADER_RESERVED: + raise ValueError("Reserved ILDA field is not zero.") + + def packHeader(self): + return struct.pack(HEADER_FORMAT, HEADER_MAGIC, self.format, + self.name, self.length, self.number, + self.total, self.scanHead, HEADER_RESERVED) + + def readHeader(self, stream): + self.unpackHeader(stream.read(HEADER_LEN)) + + def writeHeader(self, stream): + stream.write(self.packHeader()) + + def _getItemFormat(self): + try: + return formatTable[self.format] + except IndexError: + raise ValueError("Unsupported format code") + + def read_stream(self, stream): + """Read the header, then read all items in this table.""" + self.readHeader(stream) + if self.length: + fmt = self._getItemFormat() + itemSize = struct.calcsize(fmt) + self.items = [struct.unpack(fmt, stream.read(itemSize)) + for i in range(self.length)] + self.itemsproducer = self.produce() + + def write(self, stream): + """Write the header, then write all items in this table.""" + self.writeHeader(stream) + if self.length: + fmt = self._getItemFormat() + itemSize = struct.calcsize(fmt) + stream.write(''.join([struct.pack(fmt, *item) + for item in self.items])) + + def iterPoints(self): + """Iterate over Point instances for each item in this table. + Only makes sense if this is a 2D or 3D point table. + """ + for item in self.items: + p = Point() + p.decode(item) + yield p + + + def produce(self): + """Iterate over Point instances for each item in this table. + Only makes sense if this is a 2D or 3D point table. + """ + while True: + for item in self.items: + p = Point() + p.decode(item) + yield p.encode() + #yield (p.x, p.y, p.z, p.color, p.blanking) + + def read(self, cap): + """yields what dac.play_stream() needs (x, y, z, ?, ?) + """ + return [next(self.itemsproducer) for i in range(cap)] + + +class Point: + """Abstraction for one vector point. The Table object, for + completeness and efficiency, stores raw tuples for each + point. This is a higher level interface that decodes the status + bits and represents coordinates in floating point. + """ + def __init__(self, x=0.0, y=0.0, z=0.0, color=0, blanking=False): + self.__dict__.update(locals()) + + def __repr__(self): + + return "%s, %s, %s, %s, %s" % ( + self.x, self.y, self.z, self.color, self.blanking) + #return "" % ( + # self.x, self.y, self.z, self.color, self.blanking) + + def encode(self): + status = self.color & 0xFF + if self.blanking: + status |= 1 << 14 + + return ( + int( min(0x7FFF, max(-0x7FFF, self.x * 0x7FFF)) ), + int( min(0x7FFF, max(-0x7FFF, self.y * 0x7FFF)) ), + int( min(0x7FFF, max(-0x7FFF, self.z * 0x7FFF)) ), + int( min(0x7FFF, max(-0x7FFF, self.color * 0x7FFF)) ), + int( min(0x7FFF, max(-0x7FFF, self.blanking * 0x7FFF)) ) + ) + + def decode(self, t): + #print "~~ Decoding, t of len "+ str(len(t)) +" is: " + str(t) + self.x = t[0] / 0x7FFF + self.y = t[1] / 0x7FFF + if len(t) > 3: + self.z = t[2] / 0x7FFF + # self.color = t[3] & 0xFF + # self.blanking = (t[3] & (1 << 14)) != 0 + else: + self.z = 0.0 + + self.color = t[-1] & 0xFF + self.blanking = (t[-1] & (1 << 14)) != 0 + + +def read(stream): + """Read ILDA data from a stream until we hit the + end-of-stream marker. Yields a sequence of Table objects. + """ + while True: + t = Table() + t.read_stream(stream) + if not t.length: + # End-of-stream + break + yield t + + +def write(stream, tables): + """Write a sequence of tables in ILDA format, + terminated by an end-of-stream marker. + """ + for t in tables: + t.write(stream) + Table().write(stream) + + +def readFrames(stream): + """Read ILDA data from a stream, and ignore + all non-frame tables. Yields only 2D or 3D + point tables. + """ + for t in read(stream): + if t.format in (FORMAT_2D, FORMAT_3D): + yield t + + +def readFirstFrame(stream): + """Read only a single frame from an ILDA stream.""" + for frame in readFrames(stream): + return frame + + +# +f = open(args.ild, 'rb') +myframe = readFirstFrame(f) + +while myframe.number +1< myframe.total: + + start = time.time() + shape =[] + + if myframe is None: + f.close() + break + + debug(name,"Frame", myframe.number, "/",myframe.total, "length", myframe.length) + + for p in myframe.iterPoints(): + p2 = str(p) + point = p2.split(',') + + x = float(point[0]) + y = float(point[1]) + z = float(point[2]) + color = int(point[3]) + blanking = point[4][1:] + + if blanking == "True": + shape.append([300+(x*300),300+(-y*300),0]) + else: + shape.append([300+(x*300),300+(-y*300),rgb2int(colors64[color])]) + + print(shape, flush=True); + myframe = readFirstFrame(f) + + looptime = time.time() - start + if( looptime < optimal_looptime ): + time.sleep( optimal_looptime - looptime) + debug(name+" micro sleep:"+str( optimal_looptime - looptime)) + +f.close() +debug(name + " end of .ild animation") diff --git a/clitools/generators/osc2redis.py b/clitools/generators/osc2redis.py new file mode 100644 index 0000000..af8a73e --- /dev/null +++ b/clitools/generators/osc2redis.py @@ -0,0 +1,131 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + +''' + +Forward pointlist to redis key + +END POINT Format : (x,y,color) + +/pl/0/0 "[(150.0, 230.0, 255), (170.0, 170.0, 255), (230.0, 170.0, 255), (210.0, 230.0, 255), (150.0, 230.0, 255)]" + +v0.1.0 + +LICENCE : CC + +by Cocoa, Sam Neurohack + +''' + +from OSC3 import OSCServer, OSCClient, OSCMessage +import sys +from time import sleep +import argparse +import ast +import redis + +argsparser = argparse.ArgumentParser(description="osc2redis generator") +argsparser.add_argument("-i","--ip",help="IP to bind to (0.0.0.0 by default)",default="0.0.0.0",type=str) +argsparser.add_argument("-p","--port",help="OSC port to bind to (9002 by default)",default=9002,type=str) + +argsparser.add_argument("-r","--rip",help="Redis server IP (127.0.0.1 by default)",default="127.0.0.1",type=str) +argsparser.add_argument("-o","--rout",help="Redis port (6379 by default)",default=6379,type=str) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output") +args = argsparser.parse_args() + +verbose = args.verbose +ip = args.ip +port = int(args.port) +rip = args.rip +rport = int(args.rout) + + +r = redis.StrictRedis(host=rip, port=rport, db=0) + + +def debug(msg): + if( verbose == False ): + return + print(msg) + + +oscserver = OSCServer( (ip, port) ) +oscserver.timeout = 0 +run = True + +# this method of reporting timeouts only works by convention +# that before calling handle_request() field .timed_out is +# set to False +def handle_timeout(self): + self.timed_out = True + +# funny python's way to add a method to an instance of a class +import types +oscserver.handle_timeout = types.MethodType(handle_timeout, oscserver) + + +def validate(pointlist): + + state = True + + if len(pointlist)<9: + state = False + + try: + pl = bytes(pointlist, 'ascii') + check = ast.literal_eval(pl.decode('ascii')) + + except: + state = False + + return state + + +# RAW OSC Frame available ? +def OSC_frame(): + # clear timed_out flag + oscserver.timed_out = False + # handle all pending requests then return + while not oscserver.timed_out: + oscserver.handle_request() + + +# default handler +def OSChandler(oscpath, tags, args, source): + + oscaddress = ''.join(oscpath.split("/")) + print("fromOSC Default OSC Handler got oscpath :", oscpath, "from :" + str(source[0]), "args :", args) + print(oscpath.find("/pl/"), len(oscpath)) + + if oscpath.find("/pl/") ==0 and len(args)==1: + + print("correct OSC type :'/pl/") + + if validate(args[0]) == True and len(oscpath) == 7: + print("new pl for key ", oscpath, ":", args[0]) + + if r.set(oscpath,args[0])==True: + debug("exports::redis set("+str(oscpath)+") to "+args[0]) + + else: + print("Bad pointlist -> msg trapped.") + + + else: + print("BAD OSC Message :", oscpath) + +oscserver.addMsgHandler( "default", OSChandler ) + + + + +# simulate a "game engine" +while run: + # do the game stuff: + sleep(0.01) + # call user script + OSC_frame() + +oscserver.close() + diff --git a/clitools/generators/redilysis_lines.py b/clitools/generators/redilysis_lines.py new file mode 100755 index 0000000..123f487 --- /dev/null +++ b/clitools/generators/redilysis_lines.py @@ -0,0 +1,174 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + + +''' + +redilysis_lines +v0.1.0 + +Add a line on every frame and scroll + +see https://git.interhacker.space/teamlaser/redilysis for more informations +about the redilysis project + +LICENCE : CC + +by cocoa + + +''' +from __future__ import print_function +import argparse +import ast +import os +import math +import random +import redis +import sys +import time +name = "generator::redilysis_lines" + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) +def msNow(): + return time.time() + +CHAOS = 1 +REDIS_FREQ = 33 + +# General Args +argsparser = argparse.ArgumentParser(description="Redilysis filter") +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose") +# Redis Args +argsparser.add_argument("-i","--ip",help="IP address of the Redis server ",default="127.0.0.1",type=str) +argsparser.add_argument("-p","--port",help="Port of the Redis server ",default="6379",type=str) +argsparser.add_argument("-F","--redis-freq",help="Query Redis every x (in milliseconds). Default:{}".format(REDIS_FREQ),default=REDIS_FREQ,type=int) +# General args +argsparser.add_argument("-n","--nlines",help="number of lines on screen",default=60,type=int) +argsparser.add_argument("-x","--centerX",help="geometrical center X position",default=400,type=int) +argsparser.add_argument("-y","--centerY",help="geometrical center Y position",default=400,type=int) +argsparser.add_argument("-W","--max-width",help="geometrical max width",default=800,type=int) +argsparser.add_argument("-H","--max-height",help="geometrical max height",default=800,type=int) +argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int) + +args = argsparser.parse_args() +verbose = args.verbose +ip = args.ip +port = args.port +fps = args.fps +centerX = args.centerX +centerY = args.centerY +redisFreq = args.redis_freq / 1000 +maxWidth = args.max_width +maxHeight = args.max_height +nlines = args.nlines +optimal_looptime = 1 / fps + +redisKeys = ["spectrum_120","spectrum_10"] +debug(name,"Redis Keys:{}".format(redisKeys)) +redisData = {} +redisLastHit = msNow() - 99999 +r = redis.Redis( + host=ip, + port=port) + +white = 16777215 +lineList = [] + +scroll_speed = int(maxHeight / nlines ) +line_length = int(maxWidth / 10) +line_pattern = [] + +def rgb2int(rgb): + #debug(name,"::rgb2int rbg:{}".format(rgb)) + return int('0x%02x%02x%02x' % tuple(rgb),0) + +def spectrum_10( ): + delList = [] + spectrum = ast.literal_eval(redisData["spectrum_10"]) + debug( name, "spectrum:{}".format(spectrum)) + # scroll lines + for i,line in enumerate(lineList): + skip_line = False + new_y = int(line[0][1] + scroll_speed) + if( new_y >= maxHeight ): + debug(name,"{} > {}".format(new_y,maxHeight)) + debug(name,"delete:{}".format(i)) + delList.append(i) + continue + + for j,point in enumerate(line): + line[j][1] = new_y + lineList[i] = line + + for i in delList: + del lineList[i] + + # new line + currentLine = [] + for i in range(0,10): + x = int(i * line_length) + y = 0 + # get frequency level + level = spectrum[i] + # get color + comp = int(255*level) + color = rgb2int( (comp,comp,comp)) + # new point + currentLine.append( [x,y,color] ) + + # add line to list + lineList.append( currentLine) + + +def refreshRedis(): + global redisLastHit + global redisData + # Skip if cache is sufficent + diff = msNow() - redisLastHit + if diff < redisFreq : + #debug(name, "refreshRedis not updating redis, {} < {}".format(diff, redisFreq)) + pass + else: + #debug(name, "refreshRedis updating redis, {} > {}".format(diff, redisFreq)) + redisLastHit = msNow() + for key in redisKeys: + redisData[key] = r.get(key).decode('ascii') + #debug(name,"refreshRedis key:{} value:{}".format(key,redisData[key])) + # Only update the TTLs + if 'bpm' in redisKeys: + redisData['bpm_pttl'] = r.pttl('bpm') + #debug(name,"refreshRedis key:bpm_ttl value:{}".format(redisData["bpm_pttl"])) + #debug(name,"redisData:{}".format(redisData)) + return True + +def linelistToPoints( lineList ): + pl = [] + for i,line in enumerate(lineList): + # add a blank point + pl.append([ line[0][0], line[0][1], 0 ]) + # append all the points of the line + pl += line + #debug(name,"pl:{}".format(pl)) + debug(name,"pl length:{}".format(len(pl))) + return pl + +try: + while True: + refreshRedis() + start = time.time() + # Do the thing + pointsList = spectrum_10() + print( linelistToPoints(lineList), flush=True ) + looptime = time.time() - start + # debug(name+" looptime:"+str(looptime)) + if( looptime < optimal_looptime ): + time.sleep( optimal_looptime - looptime) + # debug(name+" micro sleep:"+str( optimal_looptime - looptime)) +except EOFError: + debug(name+" break")# no more information + diff --git a/clitools/generators/redilysis_particles.py b/clitools/generators/redilysis_particles.py new file mode 100755 index 0000000..59e775a --- /dev/null +++ b/clitools/generators/redilysis_particles.py @@ -0,0 +1,288 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + + +''' + +v0.1.0 + + +LICENCE : CC + +by cocoa + +''' +from __future__ import print_function +import math +import random +import sys +import os +import time +import redis +import ast +import argparse + + +MAX_PARTICLES = 50 +MAX_TIME = 500 + +argsparser = argparse.ArgumentParser(description="Dummy generator") +argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output") +# Redis Args +argsparser.add_argument("-i","--ip",help="IP address of the Redis server ",default="127.0.0.1",type=str) +argsparser.add_argument("-p","--port",help="Port of the Redis server ",default="6379",type=str) +# +argsparser.add_argument("-M","--max-particles",help="Max Particles. Default:{}".format(MAX_PARTICLES),default=MAX_PARTICLES,type=int) +argsparser.add_argument("-m","--max-time",help="Max Particles. Default:{}".format(MAX_TIME),default=MAX_TIME,type=int) + + +args = argsparser.parse_args() +verbose = args.verbose +ip = args.ip +port = args.port +max_particles = args.max_particles +max_time = args.max_time + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + + +def rgb2int(rgb): + #debug(name,"::rgb2int rbg:{}".format(rgb)) + return int('0x%02x%02x%02x' % tuple(rgb),0) + +def spectrum_120( ): + return ast.literal_eval(redisData["spectrum_10"]) + + +def rgb2int(rgb): + #debug(name,"::rgb2int rbg:{}".format(rgb)) + return int('0x%02x%02x%02x' % tuple(rgb),0) + +def msNow(): + return time.time() + +def refreshRedis(): + global redisData + for key in redisKeys: + redisData[key] = ast.literal_eval(r.get(key).decode('ascii')) + +name="generator::redilisys_particles" + +class UnpreparedParticle(Exception): + pass + +class Particle(object): + def __init__(self, x, y, m): + self.x = x + self.y = y + self.m = m + self.dx = 0 + self.dy = 0 + self.connectedTo = [] + + self.decay = random.randint(10,max_time) + self.color = (random.randint(128,256) - int(12.8 * self.m), + random.randint(128,256) - int(12.8 * self.m), + random.randint(128,256) - int(12.8 * self.m)) + self.color = (255,255,255) + #debug( self.color ) + + def interact(self, bodies): + self.connectedTo = [] + spec = redisData["spectrum_10"] + power = int(sum(spec[4:6])) + for other in bodies: + if other is self: + continue + dx = other.x - self.x + dy = other.y - self.y + dist = math.sqrt(dx*dx + dy*dy) + if dist == 0: + dist = 1 + if dist < 100 and random.randint(0,power) > 0.5 : + self.connectedTo.append(other) + self.decay += 2 + factor = other.m / dist**2 + high_power = sum(spec[8:9]) if sum(spec[8:9]) != 0 else 0.01 + self.dx += (dx * factor * self.m) + self.dy += (dy * factor * self.m) + #print "factor %f" % (factor,) + + def move(self): + spec = redisData["spectrum_10"] + x_friction = (2.2-(1+spec[7]/2)) + y_friction = (2.2-(1+spec[7]/2)) + #x_friction = 1.02 + #y_friction = 1.04 + self.dx /= x_friction if x_friction != 0 else 0.01 + self.dy /= y_friction if y_friction != 0 else 0.01 + self.x += self.dx + self.y += self.dy + if self.x > max_width: + self.dx = - self.dx /8 + self.x = max_width + if self.x < 1: + self.dx = - self.dx /8 + self.x = 1 + if self.y > max_height: + self.dy = - self.dy /4 + self.y = max_height + if self.y < 1: + self.dy = - self.dy /4 + self.y = 1 + #print "(%.2f,%.2f) -> (%.2f,%.2f)" % (ox, oy, self.x, self.y) + + def attractor(self,attractor): + spec = redisData["spectrum_10"] + power = sum(spec[0:4])/3 + # If we're going in the direction of center, reverse + next_x = self.x + self.dx + next_y = self.y + self.dy + next_dx = attractor["x"] - self.x + next_dy = attractor["y"] - self.y + next_dist = math.sqrt(next_dx*next_dx + next_dy*next_dy) + + dx = attractor["x"] - self.x + dy = attractor["y"] - self.y + dist = math.sqrt(dx*dx + dy*dy) + if dist == 0: + dist = 1 + factor = power/ dist**2 + x_acceleration = (dx * factor * power * power) + y_acceleration = (dx * factor * power * power) + + + if next_dist > dist: + self.dx -= x_acceleration * power + self.dy -= y_acceleration * power + else: + self.dx += x_acceleration + self.dy += y_acceleration + + +class Attractor(Particle): + def move(self): + pass + + +class ParticleViewer(object): + def __init__(self, particles, size=(800,800)): + (self.width, self.height) = size + self.size = size + self.particles = particles + self.xoff = 0 + self.yoff = 0 + self.scalefactor = 1 + + def redraw(self): + + pl = [] + drawnVectors = [] + for p in self.particles: + x = int(self.scalefactor * p.x) - self.xoff + y = int(self.scalefactor * p.y) - self.yoff + if x > max_width: + x = max_width + if x < 1: + x = 1 + if y > max_height: + y = max_height + if y < 1: + y = 1 + + color = rgb2int(p.color) + pl.append([x+1,y+1,0]) + pl.append([x+1,y+1,color]) + pl.append([x,y,color]) + + for other in p.connectedTo: + + if [other,self] in drawnVectors: + continue + drawnVectors.append([other,self]) + pl.append([x,y,0]) + pl.append([x,y,color]) + pl.append([other.x,other.y,color]) + + print(pl,flush = True) + + def decayParticles(self): + for i,p in enumerate(self.particles): + # Handle positional decay + if p.decay == 0: + del self.particles[i] + continue + p.decay = p.decay - 1 + # Handle color decay + n = int(255 * (p.decay / max_time )) + p.color = (n,n,n) + + + def emitParticles(self): + spec = redisData["spectrum_10"] + power = sum(spec[6:]) + if len(self.particles ) > math.sqrt(max_particles): + if len(self.particles) > max_particles: + return + if random.random() > power: + return + # x is either left or right + d = 600 + rx = 100 if random.randint(0,1) else 700 + #rx = random.randint(1,max_width) + ry = random.randint(1,max_height) + spec = redisData["spectrum_10"] + m = random.randint(1,1+int(10*spec[7])) + particles.append(Particle(rx, ry, m)) + + + def tick(self): + self.decayParticles() + self.emitParticles() + for p in self.particles: + p.interact(self.particles) + p.attractor({ + "x":max_width/2, + "y":max_height/2 + }) + for p in particles: + p.move() + self.redraw() + + def scale(self, factor): + self.scalefactor += factor + + + + +max_width = 800 +max_height = 800 +redisKeys = ["spectrum_120","spectrum_10"] +redisData = {} +redisLastHit = msNow() - 99999 +r = redis.Redis( + host=ip, + port=port) + +white = 16777215 + +refreshRedis() +if __name__ == "__main__": + particles = [] +# particles.append(Attractor(320, 200, 10)) +# particles.append(Attractor(100, 100, 10)) + + win = ParticleViewer(particles) + try: + while True: + win.tick() + refreshRedis() + time.sleep(.03) + + except KeyboardInterrupt: + pass diff --git a/clitools/generators/text.py b/clitools/generators/text.py new file mode 100644 index 0000000..c1c5c39 --- /dev/null +++ b/clitools/generators/text.py @@ -0,0 +1,93 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + +''' + +A text generators using Hershey fonts +https://pypi.org/project/Hershey-Fonts/ + +pip3 install Hershey-Fonts + +v0.1.0 + +Font list : +'futural', 'astrology', 'cursive', 'cyrilc_1', 'cyrillic', 'futuram', 'gothgbt', 'gothgrt', +'gothiceng', 'gothicger', 'gothicita', 'gothitt', 'greek', 'greekc', 'greeks', 'japanese', +'markers', 'mathlow', 'mathupp', 'meteorology', 'music', 'rowmand', 'rowmans', 'rowmant', +'scriptc', 'scripts', 'symbolic', 'timesg', 'timesi', 'timesib', 'timesr', 'timesrb' + +LICENCE : CC + +by cocoa and Sam Neurohack + +''' + +from __future__ import print_function +import time +import argparse +import sys +from HersheyFonts import HersheyFonts + +name="generator::text" + + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + + +argsparser = argparse.ArgumentParser(description="Text generator") +argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output") +argsparser.add_argument("-t","--text",help="Text to display",default="hello",type=str) +argsparser.add_argument("-p","--police",help="Herschey font to use",default="futural",type=str) +args = argsparser.parse_args() + +fps=args.fps +verbose=args.verbose + +text = args.text +fontname = args.police + +optimal_looptime = 1 / fps +debug(name+" optimal looptime "+str(optimal_looptime)) + +def rgb2int(rgb): + return int('0x%02x%02x%02x' % tuple(rgb),0) + +# Useful variables init. +white = rgb2int((255,255,255)) +red = rgb2int((255,0,0)) +blue = rgb2int((0,0,255)) +green = rgb2int((0,255,0)) + +color = 65280 + +shape =[] + +Allfonts = ['futural', 'astrology', 'cursive', 'cyrilc_1', 'cyrillic', 'futuram', 'gothgbt', 'gothgrt', 'gothiceng', 'gothicger', 'gothicita', 'gothitt', 'greek', 'greekc', 'greeks', 'japanese', 'markers', 'mathlow', 'mathupp', 'meteorology', 'music', 'rowmand', 'rowmans', 'rowmant', 'scriptc', 'scripts', 'symbolic', 'timesg', 'timesi', 'timesib', 'timesr', 'timesrb'] + +thefont = HersheyFonts() +#thefont.load_default_font() +thefont.load_default_font(fontname) +thefont.normalize_rendering(120) + +for (x1, y1), (x2, y2) in thefont.lines_for_text(text): + shape.append([x1, -y1+400, color]) + shape.append([x2 ,-y2+400, color]) + +while True: + + start = time.time() + print(shape, flush=True); + + looptime = time.time() - start + if( looptime < optimal_looptime ): + time.sleep( optimal_looptime - looptime) + debug(name+" micro sleep:"+str( optimal_looptime - looptime)) + + +#[[14.285714285714286, 100.0, 14.285714285714286, 25.0, 65280], [64.28571428571429, 100.0, 64.28571428571429, 25.0, 65280], [14.285714285714286, 64.28571428571429, 64.28571428571429, 64.28571428571429, 65280], [89.28571428571428, 53.57142857142858, 132.14285714285714, 53.57142857142858, 65280], [132.14285714285714, 53.57142857142858, 132.14285714285714, 60.714285714285715, 65280], [132.14285714285714, 60.714285714285715, 128.57142857142856, 67.85714285714286, 65280], [128.57142857142856, 67.85714285714286, 125.0, 71.42857142857143, 65280], [125.0, 71.42857142857143, 117.85714285714286, 75.0, 65280], [117.85714285714286, 75.0, 107.14285714285714, 75.0, 65280], [107.14285714285714, 75.0, 100.0, 71.42857142857143, 65280], [100.0, 71.42857142857143, 92.85714285714286, 64.28571428571429, 65280], [92.85714285714286, 64.28571428571429, 89.28571428571428, 53.57142857142858, 65280], [89.28571428571428, 53.57142857142858, 89.28571428571428, 46.42857142857143, 65280], [89.28571428571428, 46.42857142857143, 92.85714285714286, 35.714285714285715, 65280], [92.85714285714286, 35.714285714285715, 100.0, 28.571428571428573, 65280], [100.0, 28.571428571428573, 107.14285714285714, 25.0, 65280], [107.14285714285714, 25.0, 117.85714285714286, 25.0, 65280], [117.85714285714286, 25.0, 125.0, 28.571428571428573, 65280], [125.0, 28.571428571428573, 132.14285714285714, 35.714285714285715, 65280]] + diff --git a/clitools/generators/trckr.py b/clitools/generators/trckr.py new file mode 100644 index 0000000..3c9bdec --- /dev/null +++ b/clitools/generators/trckr.py @@ -0,0 +1,175 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + +''' + +A Face tracker +v0.1.0 + +Get all points fom redis /trckr/frame/WSclientID points + +LICENCE : CC + +by cocoa and Sam Neurohack + +''' + +from __future__ import print_function +import time +import argparse +import sys +import redis +import ast + +name="generator::trckr" + + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + + +argsparser = argparse.ArgumentParser(description="Face tracking generator") +argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output") +argsparser.add_argument("-i","--id",help="Trckr client ID",default="0",type=str) +argsparser.add_argument("-s","--server",help="redis server IP (127.0.0.1 by default)", type=str) +args = argsparser.parse_args() + +fps=args.fps +verbose=args.verbose +idclient = args.id + +if args.server: + redisIP = args.server +else: + redisIP = "127.0.0.1" + + + +optimal_looptime = 1 / fps +debug(name+" optimal looptime "+str(optimal_looptime)) + +color = 65280 + + +def rgb2int(rgb): + return int('0x%02x%02x%02x' % tuple(rgb),0) + +# Useful variables init. +white = rgb2int((255,255,255)) +red = rgb2int((255,0,0)) +blue = rgb2int((0,0,255)) +green = rgb2int((0,255,0)) + +# +# Redis functions +# + +r = redis.StrictRedis(host=redisIP , port=6379, db=0) + +# read from redis key +def fromKey(keyname): + + return r.get(keyname) + +# Write to redis key +def toKey(keyname,keyvalue): + return r.set(keyname,keyvalue) + +# +# Trckr faces +# + +TrckrPts = [[159.39, 137.68], [155.12, 159.31], [155.56, 180.13], [159.81, 201.6], [170.48, 220.51], [187.46, 234.81], [208.4, 244.68], [229.46, 248.21], [246.44, 244.91], [259.69, 234.83], [270.95, 221.51], [278.54, 204.66], [283.53, 185.63], [286.27, 165.79], [284.72, 144.84], [280.06, 125.01], [274.35, 118.7], [260.71, 117.23], [249.52, 118.86], [182.04, 121.5], [193.63, 114.79], [210.24, 114.77], [222.35, 117.57], [190.6, 137.49], [203.59, 132.42], [214.75, 137.58], [203.04, 140.46], [203.32, 136.53], [272.45, 141.57], [263.33, 135.42], [250.31, 138.89], [262.15, 143.27], [261.99, 139.37], [235.82, 131.74], [221.87, 156.09], [213.66, 165.88], [219.28, 173.53], [236.3, 175.25], [249.02, 174.4], [254.22, 167.81], [248.83, 157.39], [237.94, 147.51], [227.01, 168.39], [245.68, 170.02], [204.94, 197.32], [217.56, 192.77], [228.27, 190.55], [234.66, 192.19], [240.47, 191.09], [247.96, 193.87], [254.52, 199.19], [249.35, 204.25], [242.74, 207.16], [233.2, 207.87], [222.13, 206.52], [212.44, 203.09], [220.34, 198.74], [233.31, 200.04], [244.0, 199.6], [244.27, 197.8], [233.81, 197.44], [220.88, 196.99], [239.57, 162.69], [196.52, 133.86], [210.2, 133.98], [209.43, 139.41], [196.59, 139.47], [268.99, 137.59], [256.36, 136.02], [255.95, 141.5], [267.9, 142.85]] +toKey('/trckr/frame/0',str(TrckrPts)) + +# get absolute face position points +def getPART(TrckrPts, pose_points): + + dots = [] + #debug(pose_points) + #debug(TrckrPts) + for dot in pose_points: + dots.append((TrckrPts[dot][0], TrckrPts[dot][1],0)) + #debug(dots) + return dots + + +# Face keypoints +def face(TrckrPts): + pose_points = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14] + return getPART(TrckrPts, pose_points) + +def browL(TrckrPts): + pose_points = [15,16,17,18] + return getPART(TrckrPts, pose_points) + +def browR(TrckrPts): + pose_points = [22,21,20,19] + return getPART(TrckrPts, pose_points) + +def eyeR(TrckrPts): + pose_points = [25,64,24,63,23,66,26,65,25] + return getPART(TrckrPts, pose_points) + +def eyeL(TrckrPts): + pose_points = [28,67,29,68,30,69,31,28] + return getPART(TrckrPts, pose_points) + +def pupR(TrckrPts): + pose_points = [27] + return getPART(TrckrPts, pose_points) + +def pupL(TrckrPts): + pose_points = [32] + return getPART(TrckrPts, pose_points) + + +def nose1(TrckrPts): + pose_points = [62,41,33] + return getPART(TrckrPts, pose_points) + +def nose2(TrckrPts): + pose_points = [40,39,38,43,37,42,36,35,34] + return getPART(TrckrPts, pose_points) + +def mouth(TrckrPts): + pose_points = [50,49,48,47,46,45,44,55,54,53,52,51,50] + return getPART(TrckrPts, pose_points) + +def mouthfull(TrckrPts): + pose_points = [50,49,48,47,46,45,44,55,54,53,52,51,50,59,60,61,44,56,57,58,50] + return getPART(TrckrPts, pose_points) + + +while True: + + start = time.time() + shape =[] + points = ast.literal_eval(fromKey('/trckr/frame/'+idclient).decode('ascii')) + shape.append(browL(points)) + shape.append(eyeL(points)) + shape.append(browR(points)) + shape.append(eyeR(points)) + shape.append(pupL(points)) + shape.append(pupR(points)) + shape.append(nose1(points)) + shape.append(nose2(points)) + shape.append(mouthfull(points)) + line = str(shape) + line = line.replace("(",'[') + line = line.replace(")",']') + line = "[{}]".format(line) + print(line, flush=True); + #debug(shape) + #print(shape, flush=True); + + looptime = time.time() - start + if( looptime < optimal_looptime ): + time.sleep( optimal_looptime - looptime) + debug(name+" micro sleep:"+str( optimal_looptime - looptime)) + + diff --git a/clitools/generators/tunnel.py b/clitools/generators/tunnel.py new file mode 100755 index 0000000..437cb12 --- /dev/null +++ b/clitools/generators/tunnel.py @@ -0,0 +1,194 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + + +''' + +Woooh! I'm progressing in a tunnel ! +v0.1.0 + +Use it to test your filters and outputs + +LICENCE : CC + +by cocoa + +''' + +from __future__ import print_function +import argparse +import math +import random +import sys +import time +name="generator::tunnel" + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + + +argsparser = argparse.ArgumentParser(description="tunnel generator") +argsparser.add_argument("-c","--color",help="Color",default=65280,type=int) +argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int) +argsparser.add_argument("-i","--interval",help="point per shape interval",default=30,type=int) +argsparser.add_argument("-m","--max-size",help="maximum size for objects",default=400,type=int) +argsparser.add_argument("-r","--randomize",help="center randomization",default=5,type=int) +argsparser.add_argument("-s","--speed",help="point per frame progress",default=3,type=int) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output") +argsparser.add_argument("-x","--centerX",help="geometrical center X position",default=400,type=int) +argsparser.add_argument("-y","--centerY",help="geometrical center Y position",default=400,type=int) + +args = argsparser.parse_args() +centerX = args.centerX +centerY = args.centerY +color = args.color +fps = args.fps +interval = args.interval +max_size = args.max_size +randomize = args.randomize +speed = args.speed +verbose = args.verbose + +origSpeed = speed +optimal_looptime = 1 / fps +square = [ + [-1,1], + [1,1], + [1,-1], + [-1,-1], + [-1,1] + ] + +circle = [[1,0], +[0.9238795325112867,0.3826834323650898], +[0.7071067811865476,0.7071067811865475], +[0.38268343236508984,0.9238795325112867], +[0,1.0], +[-0.3826834323650897,0.9238795325112867], +[-0.7071067811865475,0.7071067811865476], +[-0.9238795325112867,0.3826834323650899], +[-1.0,0], +[-0.9238795325112868,-0.38268343236508967], +[-0.7071067811865477,-0.7071067811865475], +[-0.38268343236509034,-0.9238795325112865], +[0,-1.0], +[0.38268343236509,-0.9238795325112866], +[0.707106781186548,-0.707106781186547], +[0.9238795325112872,-0.3826834323650887], +[1,0]] + +shape = circle +currentCenter = [centerX, centerY] +centerVector= [0,0] +# tweak random basis +if randomize % 2 == 1: + randomize += 1 +debug(name,"randomize:{}".format(randomize)) +centerRand = int(math.sqrt(randomize) / 4 ) + 1 +debug( name, "centerRand:{}".format(centerRand ) ) +class polylineGenerator( object ): + + def __init__( self ): + self.polylineList = [[0,[currentCenter[0],currentCenter[1]]]] + self.buf = [] + + def init(self): + finished = False + while not finished: + finished = self.increment() + debug(name,"init done:{}".format(self.polylineList)) + def draw( self ): + self.buf = [] + for it_pl, infoList in enumerate(self.polylineList): + size = infoList[0] + center = infoList[1] + for it_sqr, point in enumerate(shape): + x = int( center[0] + point[0]*size ) + y = int( center[1] + point[1]*size ) + # Add an invisible point in first location + if 0 == it_sqr: + self.buf.append([x,y,0]) + self.buf.append([x,y,color]) + #debug( name, "buf size:", str(len(self.buf)) ) + return self.buf + + def increment(self): + global speed + self.buffer = [] + min_size = 9999 + delList = [] + if randomize : + # Change the vector + centerVector[0] += random.randrange( -centerRand,centerRand ) + centerVector[1] += random.randrange( -centerRand,centerRand ) + # Modify the vector if it is over the limit + if currentCenter[0] + centerVector[0] >= centerX + randomize or currentCenter[0] + centerVector[0] <= centerX - randomize: + centerVector[0] = 0 + if currentCenter[1] + centerVector[1] >= centerY + randomize or currentCenter[1] +centerVector[1] <= centerY - randomize: + centerVector[1] = 0 + currentCenter[0] += centerVector[0] + currentCenter[1] += centerVector[1] + # Change speed + speed += int( random.randrange( int(-origSpeed),origSpeed ) ) + if speed < origSpeed : + speed = origSpeed + elif speed > (origSpeed + randomize / 2) : + speed = origSpeed + randomize / 2 + #debug(name, "currentCenter:{} speed:{}".format(currentCenter,speed)) + + for i, shapeInfo in enumerate(self.polylineList): + size = shapeInfo[0] + # Augment speed with size + """ + size = 0 : += sqrt(speed) + size = half max size : +=speed + + """ + if size < max_size / 4: + size += math.pow(speed, 0.1) + elif size < max_size / 3: + size += math.pow(speed, 0.25) + elif size < max_size / 2: + size += math.pow(speed, 0.5) + else: + size += math.pow(speed, 1.25) + if size < min_size : min_size = size + if size > max_size : delList.append(i) + self.polylineList[i][0] = size + for i in delList: + del self.polylineList[i] + #debug(name, "polyline:",self.polylineList) + if min_size >= interval: + debug(name, "new shape") + self.polylineList.append([0,[currentCenter[0],currentCenter[1]]]) + + # Return True if we delete a shape + + if len(delList): + return True + return False + + +pgen = polylineGenerator() +pgen.init() + +while True: + start = time.time() + + # Generate + pgen.increment() + + # send + pl = pgen.draw() + print(pl, flush=True) + #debug(name,"output:{}".format(pl)) + + looptime = time.time() - start + if( looptime < optimal_looptime ): + time.sleep( optimal_looptime - looptime) + #debug(name+" micro sleep:"+str( optimal_looptime - looptime)) + + diff --git a/clitools/runner.py b/clitools/runner.py new file mode 100755 index 0000000..ef27070 --- /dev/null +++ b/clitools/runner.py @@ -0,0 +1,126 @@ +#!/usr/bin/python3 + +import sys +import os +import signal +import subprocess +import time +import tty,termios +import re +import json +from pathlib import Path +import runner_lib as runner + + + +def action_help(): + global bindings + print("\nKey\tAction\n--------------------------------------") + for i in bindings: + print(" {}\t{}".format(bindings[i],i)) + print("--------------------------------------\n") + + + +bindings={ + "Show playlist" : "l", + "Launch [0-x] cmd" : "0-x", + "Previous command" : "p", + "Next command" : "o", + "New command" : "a", + "Edit command" : "e", + "Delete command" : "d", + "Load playlist" : "L", + "Save playlist" : "S", + "Save as new" : "A", + "New playlist" : "N", + "Command help" : "H", + "Kill process Id" : "K", + "Edit Laser Id" : "i", + "Edit Laser Scene" : "s", + "Information" : "I", + "Help" : "h", + "Quit" : "q", + +} + + +## Init user contact + + + +# Main Loop +runner.action_info() +action_help() +print("\n\nLoad a playlist? [Y/n]: ") +if "y" == runner.inkey() : + runner.action_loadPlaylist() + +while True: + # Fuck zombies + runner._killBill() + runner._ok("> Next Action?") + k = runner.inkey() + + if bindings["Next command"] == k: + runner.action_changeCommand( 1 ) + runner.action_runCommand() + elif bindings["Previous command"] == k: + runner.action_changeCommand( -1 ) + runner.action_runCommand() + elif re.match( r'^\d+$',k): + runner.action_match(k) + runner.action_runCommand() + elif bindings["New command"] == k: + runner.action_newCommand() + continue + elif bindings["Show playlist"] == k: + runner.action_listAll() + continue + elif bindings["Delete command"] == k: + runner.action_deleteCommand() + continue + elif bindings["Edit command"] == k: + runner.action_listAll() + runner.action_edit() + continue + elif bindings["Load playlist"] == k: + if runner.action_loadPlaylist(): + runner.action_listAll() + continue + elif bindings["Save playlist"] == k: + runner.action_savePlaylist() + continue + elif bindings["Save as new"] == k: + runner.action_savePlaylist() + continue + elif bindings["New playlist"] == k: + runner.action_newPlaylist() + continue + elif bindings["Command help"] == k: + runner.action_commandHelp() + continue + elif bindings["Edit Laser Id"] == k: + runner.action_laserId() + continue + elif bindings["Edit Laser Scene"] == k: + runner.action_laserScene() + continue + elif bindings["Kill process Id"] == k: + runner.action_killPid() + continue + elif bindings["Help"] == k: + action_help() + continue + elif bindings["Information"] == k: + runner.action_info() + continue + elif bindings["Quit"] == k: + runner.action_quit() + else: + runner._err("Unexpected key:{}".format(k)) + continue + + + + diff --git a/clitools/runner_lib.py b/clitools/runner_lib.py new file mode 100644 index 0000000..85ddbe3 --- /dev/null +++ b/clitools/runner_lib.py @@ -0,0 +1,379 @@ + +import sys +import os +import signal +import subprocess +import time +import tty,termios +import re +import json +from pathlib import Path +import redis + + +environ = { +# "REDIS_IP" : "127.0.0.1", + "REDIS_IP" : "192.168.2.44", + "REDIS_PORT" : "6379", + "REDIS_KEY" : "/pl/0/0", + "REDIS_SCENE" : "0", + "REDIS_LASER" : "0" +} + +class bcolors: + HL = '\033[31m' + OKBLUE = '\033[94m' + OKGREEN = '\033[92m' + ENDC = '\033[0m' + BOLD = '\033[1m' + UNDERLINE = '\033[4m' + +class _Getch: + def __call__(self): + fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(fd) + try: + tty.setraw(sys.stdin.fileno()) + ch = sys.stdin.read(1) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + return ch +inkey = _Getch() + +def intkey(): + try: + i = int( inkey() ) + return(i) + except ValueError: + print("Error.") + +current_id = 0 +current_cmd = "" +process = None +current_filename = "" +currentPlayList = [] +playlistsDir = Path("./playlists") +if not playlistsDir.is_dir() : playlistsDir.mkdir() + +def ask(q): + print(q) + return inkey() + +def _ok(msg): + print( bcolors.BOLD+bcolors.OKBLUE+ msg + bcolors.ENDC) + +def _err(msg): + print( bcolors.HL + msg + bcolors.ENDC) + +def _kill(process): + if process : + try: + pid = os.getpgid(process.pid) + os.killpg(pid, signal.SIGTERM) + os.killpg(pid, signal.SIGKILL) + os.kill(pid, signal.SIGTERM) + os.kill(pid, signal.SIGKILL) + process.terminate() + process.kill() + except Exception as e: + print("woops:{}".format(e)) + + + +def _killBill(): + subprocess.run("ps --ppid 1 -fo pid,sess,ppid,cmd | grep 'toRedis.py' | while read pid sid other; do pkill -9 -s $sid; done", shell=True,executable='/bin/bash') + + + +def action_info(): + print(""" +Welcome to LJ playlist manager + +Currently running on + +IP : {} +Port : {} +Key : {} +Scene : {} +Laser : {} +""".format( + environ["REDIS_IP"], + environ["REDIS_PORT"], + environ["REDIS_KEY"], + environ["REDIS_SCENE"], + environ["REDIS_LASER"] +)) + + +def action_changeCommand( inc ): + global currentPlayList + global current_id + if 0 == len(currentPlayList): + _err("Empty playlist") + return False + current_id = (current_id + 1) % len(currentPlayList) + return True + +def action_match( k ): + global current_id, currentPlayList + if int(k) > (len(currentPlayList) - 1): + print( bcolors.HL + "This key does not exist" + bcolors.ENDC ) + return False + else : + _ok("Changed action id to {}.".format(k)) + current_id = int(k) + +def action_runCommand(): + global currentPlayList + global current_id + global process + + # Get new command + try: + current_cmd = currentPlayList[current_id] + except IndexError as e: + _err("woops:{}".format(e)) + return False + + print("\n[!]New command:'{}'\n".format(current_cmd)) + + # Start subprocess + try : + _kill(process) + process = subprocess.Popen("./_run.sh '"+current_cmd+" | exports/toRedis.py -i $REDIS_IP -k $REDIS_KEY'", shell=True, executable='/bin/bash', stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=environ, preexec_fn=os.setsid) + + except Exception as e: + print("woops:{}".format(e)) + + + +def action_newCommand(): + global currentPlayList + print("Enter new command or e(x)it.") + k = input() + # Exit early + if "x" == k: + return(False) + currentPlayList.append(k) + print(bcolors.OKBLUE + "Command added" + bcolors.ENDC) + return True + + + + +def action_deleteCommand(): + global currentPlayList + print("Select sequence to delete or e(x)it.") + action_listAll() + key = int(input()) + # Exit early + if "x" == key: + return(False) + del currentPlayList[key] + return True + + + +def action_listAll(): + global currentPlayList, current_cmd, current_id + print("\n--------------------------------------") + for i,seq in enumerate(currentPlayList): + pre="" + suf="" + if current_cmd == seq : + pre = bcolors.HL + suf = bcolors.ENDC + print( pre + "{}\t{}".format(i,seq) + suf ) + print("--------------------------------------\n") + + + + + + +def action_edit(): + print("Enter the command number to edit, or 'x' to abort.") + k = intkey() + if 'x' == k: + return + print("Enter the next value, or 'x' to abort.") + value = input() + if 'x' == value: + return + currentPlayList[k] = value + + + + + + + + +def action_loadPlaylist(): + + global playlistsDir + global currentPlayList + global current_playlist_name + # list files + i=0 + file_list = [x for x in playlistsDir.glob("*json")] + if 0 == len(file_list ): + print( bcolors.HL + "Error. No file in path '{}'\n".format(playlistsDir.name)) + return False + + print("\n Id\tName") + for k,name in enumerate(file_list) : + print(" {}\t{}".format(k,name),flush=True) + + # ask file + print("\nChoose a file or e(x)it:") + k = intkey() + if '' == k: + print("Invalid choice: '{}'".format(k)) + return + + # Exit early + if "x" == k: return(False) + + # todo : helper for detecting invalid keys + try: + if k > (len(file_list) - 1): + print( bcolors.HL + "This key '{}' does not exist".format(k) + bcolors.ENDC ) + return False + except TypeError: + print( bcolors.HL + "This key '{}' is not valid".format(k) + bcolors.ENDC ) + return False + + # @todo replace with _loadPlaylist + playlistFile = Path("./playlists/"+file_list[k].name) + currentPlayList = json.loads(playlistFile.read_text()) + current_playlist_name = file_list[k].name + current_id = 0 + print( bcolors.OKBLUE + "Playlist loaded: {}\n".format(current_playlist_name)+ bcolors.ENDC) + return True + + + +def _loadPlaylist( filename ): + + global currentPlayList, current_playlist_name, current_id + try: + playlistFile = Path(filename) + currentPlayList = json.loads(playlistFile.read_text()) + current_playlist_name = filename + current_id = 0 + _ok("Playlist loaded: {}\n".format(current_playlist_name)) + return True + except Exception as e: + _err("_loadPlaylist error when loading '{}':{}".format(filename,e)) + + + + + + +def action_newPlaylist(): + global playlistsDir + global currentPlayList + # ask for name + print("Enter new playlist name (without.json) or e(x)it question?") + k = input() + + # Exit early + if "x" == k: + return(False) + + # save file + currentPlayList = [] + _savePlaylist( k+".json" ) + currentPlayList = [] + + pass + + + + +def _savePlaylist( playlistname ): + global currentPlayList + filepath = Path("playlists/{}".format(playlistname)) + with filepath.open("w", encoding ="utf-8") as f: + f.write(json.dumps(currentPlayList, indent=4, sort_keys=True)) + return(True) + + + + + +def action_savePlaylist( name=False ): + global current_playlist_name + playlist_name = name if name else current_playlist_name + if not playlist_name : + _err("No name found.") + return False + try: + _savePlaylist(playlist_name) + print( bcolors.OKBLUE + "\nSaved as '{}'.\n".format(playlist_name) + bcolors.ENDC) + except Exception as e: + print("woops:{}".format(e)) + return False + + + + + +def action_commandHelp(): + global playlistsDir + + # iterate through files + file_list=[] + for folder in ["generators","filters","exports"]: + p = Path("./"+folder) + for plFile in Path("./"+folder).iterdir() : + if re.match("^.*py$",plFile.name): + file_list.append(os.path.join(folder,plFile.name)) + print("\n Id\tFile") + for k,filename in enumerate(file_list): + print(" {}\t{}".format(k,filename)) + print("\nChoose a file:") + k = int(input()) + print("\n-----------------------------------------------\n") + subprocess.run("python3 "+file_list[k]+" -h", shell=True, executable='/bin/bash') + print("\n-----------------------------------------------\n") + + + + +def _setKey( laser=0, scene=0 ): + global environ + laser = laser if laser else environ["REDIS_LASER"] + scene = scene if scene else environ["REDIS_SCENE"] + new_key = "/pl/{}/{}".format(scene,laser) + environ["REDIS_KEY"] = new_key + print("Sending new key '{}'".format(new_key)) + + +def action_laserId(): + k = int(ask("Enter the LJ Laser id [0-3]")) + _setKey( laser = k ) + + + +def action_laserScene(): + k = int(ask("Enter the LJ Scene id [0-3]")) + _setKey( scene = k ) + + + +def action_killPid(): + print("Enter pid to kill") + kill_pid = input() + subprocess.run("pkill -9 -s $(awk '{print $6}' /proc/$kill_pid/stat)", shell=True,executable='/bin/bash', env={"kill_pid":kill_pid}) + + +def action_quit(): + print("Quit [Y/n]?") + global process + quit = inkey() + if quit != "n": + _kill(process) + sys.exit(1) \ No newline at end of file diff --git a/clitools/runner_midi.py b/clitools/runner_midi.py new file mode 100755 index 0000000..979137b --- /dev/null +++ b/clitools/runner_midi.py @@ -0,0 +1,87 @@ +#!/usr/bin/python3 + +import argparse +import re +import redis +import runner_lib as runner +import time + +novationRows = [ + [ 0, 1, 2, 3, 4, 5, 6, 7 ], + [ *range(16,24)], + [ *range(32,40)], + [ *range(48,56)] +] + +argsparser = argparse.ArgumentParser(description="Playlist midi") +argsparser.add_argument("playlist",help="JSON playlist file ",type=str) +argsparser.add_argument("-i","--ip",help="IP address of the Redis server ",default="127.0.0.1",type=str) +argsparser.add_argument("-r","--row",help="Row of Novation pad. Default:1 ",default=1,type=str) +argsparser.add_argument("-k","--key",help="Redis key to update",default="0",type=str) +argsparser.add_argument("-l","--laser",help="Laser number. Default:0 ",default=0,type=int) +argsparser.add_argument("-p","--port",help="Port of the Redis server ",default="6379",type=str) +argsparser.add_argument("-s","--scene",help="Laser scene. Default:0 ",default=0,type=int) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose") +args = argsparser.parse_args() + +ip = args.ip +port = args.port +key = args.key +verbose=args.verbose +laser = args.laser +scene = args.scene +playlist = args.playlist +row = args.row - 1 +rowKeys = novationRows[row] + + + +# Subscriber + +r = redis.StrictRedis(host=ip, port=port, db=0) +p = r.pubsub() +p.subscribe('/midi/last_event') +runner._killBill() + +# Set Laser and scene +runner._setKey( laser = laser, scene = scene) + +# Load playlist +runner._loadPlaylist( playlist ) + +print("Loaded playlist : {}".format(runner.currentPlayList)) + + +runner.action_info() +runner.current_id = -1 +while True: + runner._killBill() + + message = p.get_message() + if message: + #runner._ok ("Subscriber: %s" % message['data']) + + + # b'/midi/noteon/0/19/127' + match = re.match(".*/([0-9]+)/[0-9]+",str(message['data'])) + if not match: + continue + key = int(match.group(1)) + + # Check if the event is for us + if key not in rowKeys: + print("key {} not in {} ".format(key,rowKeys)) + continue + + try: + command_id = rowKeys.index(key) + cmd = runner.currentPlayList[command_id] + + if command_id != runner.current_id : + runner._ok("Launching command #{}\n Previous was {}\n Cmd:{}".format(command_id,runner.current_id,cmd)) + runner.action_match(command_id) + runner.action_runCommand() + else : + runner._err("Not running {} : already running.".format(command_id)) + except Exception as e : + print("Woops.",e) diff --git a/nano.py b/nano.py new file mode 100644 index 0000000..c144e1f --- /dev/null +++ b/nano.py @@ -0,0 +1,273 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + +''' +LJnano + +v0.1b + +Open localhost websocket 9001 + +Write Redis keys : + +- '/trckr/frame/WSclientid' : TrckrPts +- 'WSclients' : [{'id': 1, 'handler': , 'address': ('127.0.0.1', 62718)}, {'id': 2, 'handler': , 'address': ('127.0.0.1', 62720)}] + +''' +from __future__ import print_function +import websocket +import time +import argparse +import traceback +import sys +import random +from websocket_server import WebsocketServer +import redis +from socket import * + +try: + import thread +except ImportError: + import _thread as thread + +name = "exports::ljnano" + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + +argsparser = argparse.ArgumentParser(description="ljnano v0.1b help mode") +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output") +argsparser.add_argument("-s","--server",help="WS server IP (127.0.0.1 by default)", type=str) +argsparser.add_argument("-p","--port",help="WS port to bind to (9001 by default)", type=str) +argsparser.add_argument("-k","--key",help="Redis key to update",default="0",type=str) +args = argsparser.parse_args() + +key = args.key + +if args.verbose: + verbose = True +else: + verbose = False + +if args.server: + serverIP = args.server +else: + serverIP = "127.0.0.1" + +if args.port: + wsPORT = args.port +else: + wsPORT = 9001 + +debug("") +debug("LJnano v0.1b") + +points0 = "[(150.0, 230.0, 65280), (170.0, 170.0, 65280), (230.0, 170.0, 65280), (210.0, 230.0, 65280), (150.0, 230.0, 65280)]" +points1 = "[(180.0, 230.0, 65280), (200.0, 170.0, 65280), (180.0, 230.0, 65280)]" +points2 = "[(170.0, 190.0, 65280), (200.0, 170.0, 65280), (230.0, 190.0, 65280), (230.0, 200.0, 65280), (170.0, 230.0, 65280), (230.0, 230.0, 65280)]" +points3 = "[(170.0, 170.0, 65280), (200.0, 170.0, 65280), (230.0, 190.0, 65280), (200.0, 200.0, 65280), (230.0, 210.0, 65280), (200.0, 230.0, 65280), (170.0, 230.0, 65280)]" +points = [points0, points1, points2, points3] + +LaserNumber = 1 +SceneNumber = 0 +Laser = 0 + + +# +# Redis +# + +r = redis.StrictRedis(host=serverIP , port=6379, db=0) + +# read from redis key +def fromKey(keyname): + + return r.get(keyname) + +# Write to redis key +def toKey(keyname,keyvalue): + + #print(keyname,keyvalue) + # Store encoded data in Redis + return r.set(keyname,keyvalue) + +# event key can be like "/midi/last_event" +# Send en event to eventkey (publish) +def toKeyevent(eventkey,eventname): + + print("redis midi event key :", eventname) + r.publish(eventkey, eventname) + +# examples +#if toKey("/clientkey","/pl/"+str(gstt.SceneNumber)+"/")==True: +# print("sent clientkey : /pl/"+str(gstt.SceneNumber)+"/") + +# toKeyevent("/midi/noteon/"+str(MidiChannel)+"/"+str(MidiNote)+"/"+str(MidiVel)) + + +def custom_handler(message): + # do_something with the message + print(message) +''' +p.psubscribe(**{'hello*':custom_handler}) +thread = p.run_in_thread(sleep_time=0.001) +''' + + + +# Change current laser. +def NoteOn(note): + debug("NoteOn", note) + + # Change laser client + if note < 8: + + Laser = note + debug("Current Laser switched to", Laser) + sendWSall("/status Laser " + str(Laser)) + sendWSall("/simul" +" "+ points[Laser]) + + # Change PL displayed on webui + if note > 23 and note < 32: + if note - 24 > LaserNumber -1: + debug("Only",LaserNumber,"lasers asked, you dum ass !") + sendWSall("/redstatus No Laser"+str(note-24)) + sendWSall("/laser "+str(LaserNumber-1)) + sendWSall("/simul" +" "+ points[LaserNumber-1]) + else: + Laser = note -24 + debug("Current Laser switched to", Laser) + sendWSall("/status Laser " + str(Laser)) + sendWSall("/simul" +" "+ points[Laser]) + + +def sendbroadcast(): + + debug("Sending broadcast") + cs = socket(AF_INET, SOCK_DGRAM) + cs.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) + cs.setsockopt(SOL_SOCKET, SO_BROADCAST, 1) + cs.sendto("LJ nano 0.1".encode(), ("255.255.255.255", 54545)) + + +# +# Websocket server +# + + +def client_list(): + + clients = [] + for client in wserver.clients_list(): + clients.append(client['id']) + #print(clients) + return str(clients) + +# Called for every WS client connecting (after handshake) +def new_client(client, wserver): + + debug("WS server got new client connected and was given id %d" % client['id']) + toKey('WSclients', client_list()) + sendWSall("/status Hello " + str(client['id'])) + sendWSall("/laser "+str(0)) + sendWSall("/lack/" + str(0) + " 3") + sendWSall("/lstt/" + str(0) + " 3") + sendWSall("/simul" +" "+ points[0]) + + +# Called for every WS client disconnecting +def client_left(client, wserver): + + try: + debug("WS server had client(%d) disconnected" % client['id']) + + except: + debug("Something weird if coming from",client,"on the wire...") + pass + + + +# Called for each WS received message. +def message_received(client, wserver, message): + + #if len(message) > 200: + # message = message[:200]+'..' + + oscpath = message.split(" ") + #print("WS Client", client['id'], "said :", message, "splitted in an oscpath :", oscpath) + #if( verbose == True ): + # debug("WS server's client", client['id'], "said :", message) + + if message[0:5] == "/simu": + debug("WS server sending to simu page :",message) + sendWSall(message) + + if message[0:7] == "/noteon": + oscpath = message.split(" ") + debug("WS server got new laser", int(oscpath[1])) + NoteOn(int(oscpath[1])) + + + # Get WS /aurora/trckr/frame layernumber framenumber points + # Set redis /trckr/frame/WSclientID points + if message.find('trckr/frame') > 0: + + args = message.split(" ") + TrckrPts = [] + for dot in range(3,len(args)-1,2): + TrckrPts.append([float(args[dot]), float(args[dot+1])]) + + debug('/trckr/frame/'+str(client['id'])+" "+str(TrckrPts)) + toKey('/trckr/frame/'+str(client['id']),str(TrckrPts)) + +def sendWSall(message): + if ( verbose == True ): + debug("WS server is sending to all : %s" % (message)) + wserver.send_message_to_all(message) + + +def LaunchServer(*args): + global wserver + + try: + + # Websocket server + wserver = WebsocketServer(wsPORT,host=serverIP) + + debug("Launching Websocket server...") + debug("at", serverIP, "port",wsPORT) + wserver.set_fn_new_client(new_client) + wserver.set_fn_client_left(client_left) + wserver.set_fn_message_received(message_received) + debug("LJ local server running...") + debug("") + + # websocket server loop + wserver.run_forever() + + + except EOFError: + debug("break")# no more information + + finally: + + debug("sendWS terminating...") + + +# +# Launch WS server +# + +if __name__ == "__main__": + + try: + + LaunchServer() + + except Exception: + debug("nano Exception") + traceback.print_exc() + \ No newline at end of file diff --git a/websocket_server.py b/websocket_server.py new file mode 100644 index 0000000..9a3b7fa --- /dev/null +++ b/websocket_server.py @@ -0,0 +1,374 @@ +# Author: Johan Hanssen Seferidis +# License: MIT + +import sys +import struct +from base64 import b64encode +from hashlib import sha1 +import logging +from socket import error as SocketError +import errno + +if sys.version_info[0] < 3: + from SocketServer import ThreadingMixIn, TCPServer, StreamRequestHandler +else: + from socketserver import ThreadingMixIn, TCPServer, StreamRequestHandler + +logger = logging.getLogger(__name__) +logging.basicConfig() + +''' ++-+-+-+-+-------+-+-------------+-------------------------------+ + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ++-+-+-+-+-------+-+-------------+-------------------------------+ +|F|R|R|R| opcode|M| Payload len | Extended payload length | +|I|S|S|S| (4) |A| (7) | (16/64) | +|N|V|V|V| |S| | (if payload len==126/127) | +| |1|2|3| |K| | | ++-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + +| Extended payload length continued, if payload len == 127 | ++ - - - - - - - - - - - - - - - +-------------------------------+ +| Payload Data continued ... | ++---------------------------------------------------------------+ +''' + +FIN = 0x80 +OPCODE = 0x0f +MASKED = 0x80 +PAYLOAD_LEN = 0x7f +PAYLOAD_LEN_EXT16 = 0x7e +PAYLOAD_LEN_EXT64 = 0x7f + +OPCODE_CONTINUATION = 0x0 +OPCODE_TEXT = 0x1 +OPCODE_BINARY = 0x2 +OPCODE_CLOSE_CONN = 0x8 +OPCODE_PING = 0x9 +OPCODE_PONG = 0xA + + +# -------------------------------- API --------------------------------- + +class API(): + + def run_forever(self): + try: + logger.info("Listening on port %d for clients.." % self.port) + self.serve_forever() + except KeyboardInterrupt: + self.server_close() + logger.info("Server terminated.") + except Exception as e: + logger.error(str(e), exc_info=True) + exit(1) + + def new_client(self, client, server): + pass + + def client_left(self, client, server): + pass + + def message_received(self, client, server, message): + pass + + def set_fn_new_client(self, fn): + self.new_client = fn + + def set_fn_client_left(self, fn): + self.client_left = fn + + def set_fn_message_received(self, fn): + self.message_received = fn + + def send_message(self, client, msg): + self._unicast_(client, msg) + + def send_message_to_all(self, msg): + self._multicast_(msg) + + def clients_list(self): + return self.clients + + +# ------------------------- Implementation ----------------------------- + +class WebsocketServer(ThreadingMixIn, TCPServer, API): + """ + A websocket server waiting for clients to connect. + + Args: + port(int): Port to bind to + host(str): Hostname or IP to listen for connections. By default 127.0.0.1 + is being used. To accept connections from any client, you should use + 0.0.0.0. + loglevel: Logging level from logging module to use for logging. By default + warnings and errors are being logged. + + Properties: + clients(list): A list of connected clients. A client is a dictionary + like below. + { + 'id' : id, + 'handler' : handler, + 'address' : (addr, port) + } + """ + + allow_reuse_address = True + daemon_threads = True # comment to keep threads alive until finished + + clients = [] + id_counter = 0 + + def __init__(self, port, host='127.0.0.1', loglevel=logging.WARNING): + logger.setLevel(loglevel) + TCPServer.__init__(self, (host, port), WebSocketHandler) + self.port = self.socket.getsockname()[1] + + def _message_received_(self, handler, msg): + self.message_received(self.handler_to_client(handler), self, msg) + + def _ping_received_(self, handler, msg): + handler.send_pong(msg) + + def _pong_received_(self, handler, msg): + pass + + def _new_client_(self, handler): + self.id_counter += 1 + client = { + 'id': self.id_counter, + 'handler': handler, + 'address': handler.client_address + } + self.clients.append(client) + self.new_client(client, self) + + def _client_left_(self, handler): + client = self.handler_to_client(handler) + self.client_left(client, self) + if client in self.clients: + self.clients.remove(client) + + def _unicast_(self, to_client, msg): + to_client['handler'].send_message(msg) + + def _multicast_(self, msg): + for client in self.clients: + self._unicast_(client, msg) + + def handler_to_client(self, handler): + for client in self.clients: + if client['handler'] == handler: + return client + + +class WebSocketHandler(StreamRequestHandler): + + def __init__(self, socket, addr, server): + self.server = server + StreamRequestHandler.__init__(self, socket, addr, server) + + def setup(self): + StreamRequestHandler.setup(self) + self.keep_alive = True + self.handshake_done = False + self.valid_client = False + + def handle(self): + while self.keep_alive: + if not self.handshake_done: + self.handshake() + elif self.valid_client: + self.read_next_message() + + def read_bytes(self, num): + # python3 gives ordinal of byte directly + bytes = self.rfile.read(num) + if sys.version_info[0] < 3: + return map(ord, bytes) + else: + return bytes + + def read_next_message(self): + try: + b1, b2 = self.read_bytes(2) + except SocketError as e: # to be replaced with ConnectionResetError for py3 + if e.errno == errno.ECONNRESET: + logger.info("Client closed connection.") + print("Error: {}".format(e)) + self.keep_alive = 0 + return + b1, b2 = 0, 0 + except ValueError as e: + b1, b2 = 0, 0 + + fin = b1 & FIN + opcode = b1 & OPCODE + masked = b2 & MASKED + payload_length = b2 & PAYLOAD_LEN + + if opcode == OPCODE_CLOSE_CONN: + logger.info("Client asked to close connection.") + self.keep_alive = 0 + return + if not masked: + logger.warn("Client must always be masked.") + self.keep_alive = 0 + return + if opcode == OPCODE_CONTINUATION: + logger.warn("Continuation frames are not supported.") + return + elif opcode == OPCODE_BINARY: + logger.warn("Binary frames are not supported.") + return + elif opcode == OPCODE_TEXT: + opcode_handler = self.server._message_received_ + elif opcode == OPCODE_PING: + opcode_handler = self.server._ping_received_ + elif opcode == OPCODE_PONG: + opcode_handler = self.server._pong_received_ + else: + logger.warn("Unknown opcode %#x." % opcode) + self.keep_alive = 0 + return + + if payload_length == 126: + payload_length = struct.unpack(">H", self.rfile.read(2))[0] + elif payload_length == 127: + payload_length = struct.unpack(">Q", self.rfile.read(8))[0] + + masks = self.read_bytes(4) + message_bytes = bytearray() + for message_byte in self.read_bytes(payload_length): + message_byte ^= masks[len(message_bytes) % 4] + message_bytes.append(message_byte) + opcode_handler(self, message_bytes.decode('utf8')) + + def send_message(self, message): + self.send_text(message) + + def send_pong(self, message): + self.send_text(message, OPCODE_PONG) + + def send_text(self, message, opcode=OPCODE_TEXT): + """ + Important: Fragmented(=continuation) messages are not supported since + their usage cases are limited - when we don't know the payload length. + """ + + # Validate message + if isinstance(message, bytes): + message = try_decode_UTF8(message) # this is slower but ensures we have UTF-8 + if not message: + logger.warning("Can\'t send message, message is not valid UTF-8") + return False + elif sys.version_info < (3,0) and (isinstance(message, str) or isinstance(message, unicode)): + pass + elif isinstance(message, str): + pass + else: + logger.warning('Can\'t send message, message has to be a string or bytes. Given type is %s' % type(message)) + return False + + header = bytearray() + payload = encode_to_UTF8(message) + payload_length = len(payload) + + # Normal payload + if payload_length <= 125: + header.append(FIN | opcode) + header.append(payload_length) + + # Extended payload + elif payload_length >= 126 and payload_length <= 65535: + header.append(FIN | opcode) + header.append(PAYLOAD_LEN_EXT16) + header.extend(struct.pack(">H", payload_length)) + + # Huge extended payload + elif payload_length < 18446744073709551616: + header.append(FIN | opcode) + header.append(PAYLOAD_LEN_EXT64) + header.extend(struct.pack(">Q", payload_length)) + + else: + raise Exception("Message is too big. Consider breaking it into chunks.") + return + + self.request.send(header + payload) + + def read_http_headers(self): + headers = {} + # first line should be HTTP GET + http_get = self.rfile.readline().decode().strip() + assert http_get.upper().startswith('GET') + # remaining should be headers + while True: + header = self.rfile.readline().decode().strip() + if not header: + break + head, value = header.split(':', 1) + headers[head.lower().strip()] = value.strip() + return headers + + def handshake(self): + headers = self.read_http_headers() + + try: + assert headers['upgrade'].lower() == 'websocket' + except AssertionError: + self.keep_alive = False + return + + try: + key = headers['sec-websocket-key'] + except KeyError: + logger.warning("Client tried to connect but was missing a key") + self.keep_alive = False + return + + response = self.make_handshake_response(key) + self.handshake_done = self.request.send(response.encode()) + self.valid_client = True + self.server._new_client_(self) + + @classmethod + def make_handshake_response(cls, key): + return \ + 'HTTP/1.1 101 Switching Protocols\r\n'\ + 'Upgrade: websocket\r\n' \ + 'Connection: Upgrade\r\n' \ + 'Sec-WebSocket-Accept: %s\r\n' \ + '\r\n' % cls.calculate_response_key(key) + + @classmethod + def calculate_response_key(cls, key): + GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' + hash = sha1(key.encode() + GUID.encode()) + response_key = b64encode(hash.digest()).strip() + return response_key.decode('ASCII') + + def finish(self): + self.server._client_left_(self) + + +def encode_to_UTF8(data): + try: + return data.encode('UTF-8') + except UnicodeEncodeError as e: + logger.error("Could not encode data to UTF-8 -- %s" % e) + return False + except Exception as e: + raise(e) + return False + + +def try_decode_UTF8(data): + try: + return data.decode('utf-8') + except UnicodeDecodeError: + return False + except Exception as e: + raise(e) diff --git a/www/LJ.js b/www/LJ.js new file mode 100644 index 0000000..743e289 --- /dev/null +++ b/www/LJ.js @@ -0,0 +1,316 @@ +// +// LJ.js v0.7.1 +// + +// LJ websocket address. IP will be updated at LJ startup according to LJ.conf wwwIP + + var LJ = websocket_uri + + +// +// Central horizontal menu +// + + + function noMenu() { + var x = document.getElementById("align"); + x.value = 0 ; + var x = document.getElementById("run"); + x.value = 0 ; + var x = document.getElementById("simu"); + x.value = 0 ; + var x = document.getElementById("live"); + x.value = 0 ; + + // Hide all possible main central grids. + var x = document.getElementById("mgalign"); + x.style.display = "none"; + var x = document.getElementById("mgsimu"); + x.style.display = "none"; + var x = document.getElementById("mgrun"); + x.style.display = "none"; + var x = document.getElementById("mglive"); + x.style.display = "none"; + + } + + function showAlign() { + noMenu(); + var x = document.getElementById("mgalign"); + x.style.display = "grid"; + var x = document.getElementById("align"); + x.value = 1 ; + } + + function showRun() { + noMenu(); + var x = document.getElementById("mgrun"); + x.style.display = "grid"; + var x = document.getElementById("run"); + x.value = 1 ; + } + + function showCanvas() { + noMenu(); + var x = document.getElementById("mgsimu"); + x.style.display = "grid"; + var x = document.getElementById("cnvbuttons"); + x.style.display = "grid"; + var x = document.getElementById("simu"); + x.value = 1 ; + } + + function showLive() { + noMenu(); + var x = document.getElementById("mglive"); + x.style.display = "grid"; + var x = document.getElementById("live"); + x.value = 1 ; + } + + +// +// SimuUIs +// + + function nosimuUI() { + // Hide all possible main central grids. + var x = document.getElementById("planetUI"); + x.style.display = "none"; + var x = document.getElementById("nozoidUI"); + x.style.display = "none"; + var x = document.getElementById("aiUI"); + x.style.display = "none"; + var x = document.getElementById("lissaUI"); + x.style.display = "none"; + var x = document.getElementById("vjUI"); + x.style.display = "none"; + var x = document.getElementById("poseUI"); + x.style.display = "none"; + var x = document.getElementById("wordsUI"); + x.style.display = "none"; + var x = document.getElementById("pluginsUI"); + x.style.display = "none"; + } + + function showplanetUI() { + nosimuUI(); + var x = document.getElementById("planetUI"); + x.style.display = "grid"; + _WS.send("/planet/ping"); + } + + function shownozoidUI() { + nosimuUI(); + var x = document.getElementById("nozoidUI"); + x.style.display = "grid"; + _WS.send("/nozoid/ping"); + } + + function showaiUI() { + nosimuUI(); + var x = document.getElementById("aiUI"); + x.style.display = "grid"; + } + + function showlissaUI() { + nosimuUI(); + var x = document.getElementById("lissaUI"); + x.style.display = "grid"; + } + + function showvjUI() { + nosimuUI(); + var x = document.getElementById("vjUI"); + x.style.display = "grid"; + _WS.send("/bank0/ping"); + } + + + function showposeUI() { + nosimuUI(); + var x = document.getElementById("poseUI"); + x.style.display = "grid"; + _WS.send("/pose/ping"); + } + + + function showwordsUI() { + nosimuUI(); + var x = document.getElementById("wordsUI"); + x.style.display = "grid"; + _WS.send("/words/ping"); + } + + function showpluginsUI() { + nosimuUI(); + var x = document.getElementById("pluginsUI"); + x.style.display = "grid"; + //_WS.send("/words/ping"); + } + +// +// Button clicked +// + + function buttonClicked(clicked_id) { + + _WS.send("/" + clicked_id); + + // update Canvas right part of maingrid + if (clicked_id === "planet/planetUI") { + showplanetUI(); + } + if (clicked_id === "nozoid/nozoidUI") { + shownozoidUI(); + } + if (clicked_id === "ai/aiUI") { + showaiUI(); + } + if (clicked_id === "lissa/lissaUI") { + showlissaUI(); + } + if (clicked_id === "bank0/vjUI") { + showvjUI(); + } + if (clicked_id === "pose/poseUI") { + showposeUI(); + } + if (clicked_id === "words/wordsUI") { + showwordsUI(); + } + if (clicked_id === "lj/pluginsUI") { + showpluginsUI(); + } + if (clicked_id === "nozoid/down 50") { + var x = document.getElementById("nozoid/down 50"); + x.value = 0 ; + } + } + +// +// Matrix selection +// + + + function MatrixSelect(clicked_value) { + + console.log("/" + clicked_value); + _WS.send("/" + clicked_value); + } + +// +// Forms submits +// + + function onSubmit(clicked_id) { + var input = document.getElementById(clicked_id); + console.log("/" + clicked_id + " " + input.value); + _WS.send("/" + clicked_id + " " + input.value); + _WS.showout("/" + clicked_id + " " + input.value); + } + + +// +// Websocket handler +// + + var pl = ""; + var pl2 = new Array(); + var _WS = { + uri: LJ, + ws: null, + + init : function (e) { + _WS.s = new WebSocket(_WS.uri); + _WS.s.onopen = function (e) { _WS.onOpen(e); }; + _WS.s.onclose = function (e) { _WS.onClose(e); }; + _WS.s.onmessage = function (e) { _WS.onMessage(e); }; + _WS.s.onerror = function (e) { _WS.onError(e); }; + }, + + onOpen: function () { + _WS.showout(_WS.uri); + _WS.showout('CONNECTED'); + document.getElementById("on").value = 1; + }, + + onClose: function () { + _WS.showout('DISCONNECTED'); + document.getElementById("on").value = 0; + document.getElementById("lstt/0").value = 0; + document.getElementById("lstt/1").value = 0; + document.getElementById("lstt/2").value = 0; + document.getElementById("lstt/3").value = 0; + document.getElementById("lack/0").value = 0; + document.getElementById("lack/1").value = 0; + document.getElementById("lack/2").value = 0; + document.getElementById("lack/3").value = 0; + }, + + onMessage: function (e) { + var res = e.data.split(" "); + //console.log(e.data) + //console.log(res[0].substring(0,6)) + //console.log(res) + //console.log(res[0].slice(1)) + + switch (res[0].substring(0,6)) { + + case "/statu": + _WS.showstatus(e.data.slice(8)); + break; + case "/simul": + pl = e.data.slice(7); + //console.log(pl) + pl2 = eval(pl.replace(/[()]/g, '')); + break; + case "/plpoi": + //console.log("plpoint"); + break; + default: + //console.log(e); + document.getElementById(res[0].slice(1)).value = res[1]; + _WS.showin(e.data); + } + }, + + onError: function (e) { + _WS.showin('ERROR: ' + e.data); + }, + + showin: function (message) { + var divtext = document.getElementById('showin'); + divtext.innerHTML=""; + divtext.innerHTML= message.toString(); + }, + + showout: function (message) { + var divtext = document.getElementById('showout'); + divtext.innerHTML=""; + divtext.innerHTML= message.toString(); + }, + + showstatus: function (message) { + var divtext = document.getElementById('showstatus'); + divtext.innerHTML=""; + divtext.innerHTML= message.toString(); + }, + + send: function (message) { + if (!message.length) { + alert('Empty message not allowed !'); + } else { + _WS.showout(message); + _WS.s.send(message); + } + }, + + close: function () { + _WS.showout('GOODBYE !'); + _WS.s.close(); + } + }; + + window.addEventListener('load', _WS.init, false); + diff --git a/www/android-chrome-192x192.png b/www/android-chrome-192x192.png new file mode 100755 index 0000000..93252cb Binary files /dev/null and b/www/android-chrome-192x192.png differ diff --git a/www/android-chrome-512x512.png b/www/android-chrome-512x512.png new file mode 100755 index 0000000..c52c30f Binary files /dev/null and b/www/android-chrome-512x512.png differ diff --git a/www/apple-touch-icon-80x80.png b/www/apple-touch-icon-80x80.png new file mode 100644 index 0000000..bb05f11 Binary files /dev/null and b/www/apple-touch-icon-80x80.png differ diff --git a/www/apple-touch-icon.png b/www/apple-touch-icon.png new file mode 100755 index 0000000..d79efc3 Binary files /dev/null and b/www/apple-touch-icon.png differ diff --git a/www/config.js b/www/config.js new file mode 100644 index 0000000..d9c1042 --- /dev/null +++ b/www/config.js @@ -0,0 +1 @@ +websocket_uri = "ws://192.168.2.43:9001/" diff --git a/www/css/7f37946c45abf5482c243bf326f82628.eot b/www/css/7f37946c45abf5482c243bf326f82628.eot new file mode 100755 index 0000000..2b3667e Binary files /dev/null and b/www/css/7f37946c45abf5482c243bf326f82628.eot differ diff --git a/www/css/7f37946c45abf5482c243bf326f82628.svg b/www/css/7f37946c45abf5482c243bf326f82628.svg new file mode 100755 index 0000000..c7f2311 --- /dev/null +++ b/www/css/7f37946c45abf5482c243bf326f82628.svg @@ -0,0 +1,828 @@ + + + + +Created by FontForge 20120731 at Sat Jun 6 06:35:01 2020 + By www +This work is licensed under a Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 License (click below for details). +The font was created by Michal Sulik using Fony, bdfresize, potrace, autotrace and Fontforge. +If you use this font in any public work, please be so nice and leave a link in comments on http://msulik.deviantart.com/. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/www/css/7f37946c45abf5482c243bf326f82628.ttf b/www/css/7f37946c45abf5482c243bf326f82628.ttf new file mode 100755 index 0000000..0e38d7b Binary files /dev/null and b/www/css/7f37946c45abf5482c243bf326f82628.ttf differ diff --git a/www/css/7f37946c45abf5482c243bf326f82628.woff b/www/css/7f37946c45abf5482c243bf326f82628.woff new file mode 100755 index 0000000..5dc5b8a Binary files /dev/null and b/www/css/7f37946c45abf5482c243bf326f82628.woff differ diff --git a/www/css/7f37946c45abf5482c243bf326f82628.woff2 b/www/css/7f37946c45abf5482c243bf326f82628.woff2 new file mode 100755 index 0000000..bf0e347 Binary files /dev/null and b/www/css/7f37946c45abf5482c243bf326f82628.woff2 differ diff --git a/www/css/LED.eot b/www/css/LED.eot new file mode 100644 index 0000000..423ff5d Binary files /dev/null and b/www/css/LED.eot differ diff --git a/www/css/LED.svg b/www/css/LED.svg new file mode 100644 index 0000000..5266b79 --- /dev/null +++ b/www/css/LED.svg @@ -0,0 +1,519 @@ + + + + +Created by FontForge 20110222 at Mon Mar 7 11:46:57 2011 + By www-data + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/www/css/LED.ttf b/www/css/LED.ttf new file mode 100644 index 0000000..ab6b07d Binary files /dev/null and b/www/css/LED.ttf differ diff --git a/www/css/LED.woff b/www/css/LED.woff new file mode 100644 index 0000000..f0e1058 Binary files /dev/null and b/www/css/LED.woff differ diff --git a/www/css/common.css b/www/css/common.css new file mode 100644 index 0000000..d8df076 --- /dev/null +++ b/www/css/common.css @@ -0,0 +1,377 @@ + .grid-container { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 0.8fr 47px 0.7fr 2.8fr; + grid-template-areas: ". . ." ". . ." ". . ." ". . ."; + } + .buttons-container { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 1fr; + grid-template-areas: ". . ." ". . ." ". . ." ". . ."; + } + .buttons-6container { + display: grid; + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr; + grid-template-rows: 1fr; + gap: 1px 1px; + grid-template-areas: ". . . . . ."; + } + .buttons-7container { + display: grid; + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr; + grid-template-rows: 1fr; + gap: 1px 1px; + grid-template-areas: ". . . . . . ."; + } + #title{ + font-family: "Lucida Grande", Verdana, Arial, sans-serif; + text-align: center; + color: #ddd; + font-size: 4ex; + } + #text{ + font-family: "Lucida Grande", Verdana, Arial, sans-serif; + text-align: center; + color: #ccc; + font-size: 2ex; + } + .encoders{ + margin: 0 auto; + } + .mainGrid { + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; + grid-template-rows: 1fr; + gap: 1px 1px; + grid-template-areas: ". . . . . . . ."; + } + .buttonsGrid { + display: grid; + grid-template-columns: 70px 70px 70px 70px 70px 70px 70px 70px 70px 70px 70px 70px; + grid-template-rows: 1fr; + gap: 1px 1px; + grid-template-areas: ". . . . ." ". . . . ." ". . . . ."; + } + .FxGrid { + display: grid; + grid-template-columns: 70px 70px 70px 70px 70px 70px 70px; + grid-template-rows: 1fr; + gap: 1px 1px; + align-items: stretch; + background-color: #020202; + grid-template-areas: ". . . . . . ,"; + } + .RGYGrid { + display: grid; + grid-template-columns: 70px 70px 70px; + grid-template-rows: 1fr; + background-color: #020202; + gap: 1px 1px; + grid-template-areas: ". . ."; + } + .RGBGrid { + display: grid; + grid-template-columns: 70px 70px 70px 70px 70px 70px; + grid-template-rows: 1fr; + gap: 1px 1px; + background-color: #020202; + grid-template-areas: ". . . . . ."; + } + .TopRackGrid { + display: grid; + grid-template-columns: 120px 600px 30px 30px; + grid-template-rows: 1fr; + gap: 1px 1px; + grid-template-areas: ". . . ."; + } + .TextGrid { + display: grid; + grid-template-columns: 250px 370px; + grid-template-rows: 1fr; + gap: 1px 1px; + grid-template-areas: ". ."; + } + .Rackgrid { + display: grid; + grid-template-columns: 170px 70px 70px 70px 70px 70px 70px 70px 70px 70px; + grid-template-rows: 1fr; + gap: 1px 1px; + grid-template-areas: ". . . . . . . . . ."; + } + .Settingrid { + display: grid; + grid-template-columns: 110px 80px 80px 100px 80px 80px 140px 40px; + grid-template-rows: 1fr; + gap: 1px 1px; + grid-template-areas: ". . . . . . . . ."; + } + .ColorRackGrid { + display: grid; + grid-template-columns: 170px 70px 70px 70px 70px 70px 70px 70px 70px 70px; + grid-template-rows: 1fr; + gap: 1px 1px; + grid-template-areas: ". . . . . . . . . ."; + } + .grid3 { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 1fr; + gap: 1px 1px; + grid-template-areas: ". . ."; + } + .encodersGrid { + display: grid; + grid-template-columns: 1fr; + grid-template-rows: 1fr 1fr 1fr 1fr 1fr; + gap: 1px 1px; + grid-template-areas: "." "." "." "." "."; + } + .webaudiobut{ + border-radius: 4px; + border : #002020 1px solid; + -webkit-box-shadow: 2px 4px 8px -1px rgba(0,0,0,0.72); + -moz-box-shadow: 2px 4px 8px -1px rgba(0,0,0,0.72); + box-shadow: 2px 4px 8px -1px rgba(0,0,0,0.72); + background-image: linear-gradient(174deg, #111,#030303); + } + .content{ + width : 800px; + padding : 10px 20px; + margin : 5px auto; + border-radius: 10px; + border : #212121 2px solid; + background-color: #000000; + background-image: linear-gradient(174deg, #222,#111); + -webkit-box-shadow: 4px 6px 10px -1px rgba(0,0,0,0.72); + -moz-box-shadow: 4px 6px 10px -1px rgba(0,0,0,0.72); + box-shadow: 4px 6px 10px -1px rgba(0,0,0,0.72); + } + .Rackcontent{ + width : 845px; + border-radius: 3px; + margin : 1px auto; + border : #111 2px solid; + background-color: #111; + -webkit-box-shadow: 4px 6px 44px -1px rgba(0,0,0,0.72); + -moz-box-shadow: 4px 6px 44px -1px rgba(0,0,0,0.72); + box-shadow: 4px 6px 44px -1px rgba(0,0,0,0.72); + } + .content-title { + color : #ddd; + border : none; + margin-top : 15px; + padding-bottom : 0; + margin-bottom : 0; + background-color : inherit; + } + + .content-names { + padding : 10px; + padding-top : 0; + border : none; + box-shadow : none; + background-color : inherit; + } + .etherled { + color: #666; + font-family: 'Lucida Grande', Verdana, Arial, sans-serif; + font-size: 1.5ex; + text-align: middle; + margin-left: 20px + margin-top: 10px + } + + a { text-decoration: none; + border: none; + outline: none; + } + a:hover, a:focus { + border: none; + outline: none; + color: #fff; + } + + h2 { + position: relative; + line-height: 3ex; + color: #666; + text-align: middle; + font-family: 'Lucida Grande', Verdana, Arial, sans-serif; + font-size: 1.7ex; + margin-top : 1px; + text-shadow: 2px 3px #000; + } + @font-face { + + font-family: "Bus Led Display Small"; + src: url("7f37946c45abf5482c243bf326f82628.eot"); /* IE9*/ + src: url("7f37946c45abf5482c243bf326f82628.eot?#iefix") format("embedded-opentype"), /* IE6-IE8 */ + url("7f37946c45abf5482c243bf326f82628.woff2") format("woff2"), /* chrome、firefox */ + url("7f37946c45abf5482c243bf326f82628.woff") format("woff"), /* chrome、firefox */ + url("7f37946c45abf5482c243bf326f82628.ttf") format("truetype"), /* chrome、firefox、opera、Safari, Android, iOS 4.2+*/ + url("7f37946c45abf5482c243bf326f82628.svg#Bus Led Display Small") format("svg"); /* iOS 4.1- */ + } + + .busled{ + color: #ccc; + background: #000; + font-family:"Bus Led Display Small" !important; + font-size:2em; + font-style:normal; + -webkit-font-smoothing: antialiased; + -webkit-text-stroke-width: 0.7px; + -moz-osx-font-smoothing: grayscale; + } + .navled{ + color: #ccc; + background: #000; + font-family:"Bus Led Display Small" !important; + font-size:1.5em; + margin-left: 2px; + font-style:normal; + -webkit-font-smoothing: antialiased; + -webkit-text-stroke-width: 0.7px; + -moz-osx-font-smoothing: grayscale; + } + .submit { + background: #000; + color: #f0f0f0; + width: 90px; + height: 20px; + text-align: center; + margin-left: 15px; + vertical-align: middle; + font-family:"Bus Led Display Small" !important; + font-size:1.1em; + font-style:normal; + border: 1px solid #445; + -webkit-box-shadow: 4px 6px 10px -1px rgba(0,0,0,0.72); + -moz-box-shadow: 4px 6px 10px -1px rgba(0,0,0,0.72); + box-shadow: 4px 6px 10px -1px rgba(0,0,0,0.72); + } + .submite { + background: #000; + color: #f0f0f0; + width: 120px; + height: 25px; + text-align: center; + margin-left: 15px; + vertical-align: middle; + font-family:"Bus Led Display Small" !important; + font-size:1.3em; + font-style:normal; + border: 1px solid #033; + -webkit-box-shadow: 4px 6px 10px -1px rgba(0,0,0,0.72); + -moz-box-shadow: 4px 6px 10px -1px rgba(0,0,0,0.72); + box-shadow: 4px 6px 10px -1px rgba(0,0,0,0.72); + } + .submitalign { + background: #000; + color: #f0f0f0; + width: 60px; + height: 30px; + text-align: center; + margin-left: 15px; + vertical-align: middle; + font-family:"Bus Led Display Small" !important; + font-size:1.4em; + font-style:normal; + border: 1px solid #001515; + -webkit-box-shadow: 4px 6px 10px -1px rgba(0,0,0,0.72); + -moz-box-shadow: 4px 6px 10px -1px rgba(0,0,0,0.72); + box-shadow: 4px 6px 10px -1px rgba(0,0,0,0.72); + } + .submites { + background: #000; + color: #f0f0f0; + width: 30px; + height: 35px; + text-align: center; + margin-left: 15px; + vertical-align: middle; + font-family:"Bus Led Display Small" !important; + font-size:1.3em; + font-style:normal; + border: 1px solid #445; + -webkit-box-shadow: 4px 6px 10px -1px rgba(0,0,0,0.72); + -moz-box-shadow: 4px 6px 10px -1px rgba(0,0,0,0.72); + box-shadow: 4px 6px 10px -1px rgba(0,0,0,0.72); + } + .submitext{ + font-family: 'Bus Led Display Small'; + color: #fff; + background: #000; + font-size: 5ex;" + border-radius: 5px; + border : #222222 2px solid; + -webkit-box-shadow: 4px 6px 10px -1px rgba(0,0,0,0.72); + -moz-box-shadow: 4px 6px 10px -1px rgba(0,0,0,0.72); + box-shadow: 4px 6px 10px -1px rgba(0,0,0,0.72); + + } + #overlay { + position: absolute; + top: 200px; + left: 650px; + -o-transform : scaleX(-1); + -webkit-transform : scaleX(-1); + transform : scaleX(-1); + -ms-filter : fliph; /*IE*/ + filter : fliph; /*IE*/ + } + + #videoel { + -o-transform : scaleX(-1); + -webkit-transform : scaleX(-1); + transform : scaleX(-1); + -ms-filter : fliph; /*IE*/ + filter : fliph; /*IE*/ + } + .lasertext { + font-size: small; + font-family: Helvetica, Verdana, Arial, sans-serif; + color: #bbb; + } + .mgalign { + color: #aaa; + text-align: middle; + font-family: 'Lucida Grande', Verdana, Arial, sans-serif; + display: grid; + height: 400px; + grid-template-columns: 200px 200px 200px 200px; + grid-template-rows: 1Fr; + + border-width: 1px solid #445; + gap: 1px 1px; + grid-template-areas: ". . . . . . . . . ."; + } + .lasergrid { + display: grid; + height: 323px; + width: 200px; + grid-template-columns: 62px 62px 62px; + grid-template-rows: 30px 19px 10px 55px 19px 20px 16px 55px 19px 25px 16px 55px 19px; + line-height: 1; + justify-items: center; + align-items: center; + color:#88c; + } + .lissabox { + display: grid; + height: 353px; + width: 126px; + grid-template-columns: 124px; + grid-template-rows: 15px 338px; + background-color: #000000; + background-image: linear-gradient(147deg, #000000 0%, #434343 374%); + line-height: 1; + padding: 6px; + justify-items: center; + align-items: center; + border-color: #334; + border-style: groove; + border-width: 1px; + } diff --git a/www/css/font-awesome.min.css b/www/css/font-awesome.min.css new file mode 100644 index 0000000..06a13c5 --- /dev/null +++ b/www/css/font-awesome.min.css @@ -0,0 +1,5 @@ +/*! + * Font Awesome Free 5.13.0 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +.fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adobe:before{content:"\f778"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asterisk:before{content:"\f069"}.fa-asymmetrik:before{content:"\f372"}.fa-at:before{content:"\f1fa"}.fa-atlas:before{content:"\f558"}.fa-atlassian:before{content:"\f77b"}.fa-atom:before{content:"\f5d2"}.fa-audible:before{content:"\f373"}.fa-audio-description:before{content:"\f29e"}.fa-autoprefixer:before{content:"\f41c"}.fa-avianex:before{content:"\f374"}.fa-aviato:before{content:"\f421"}.fa-award:before{content:"\f559"}.fa-aws:before{content:"\f375"}.fa-baby:before{content:"\f77c"}.fa-baby-carriage:before{content:"\f77d"}.fa-backspace:before{content:"\f55a"}.fa-backward:before{content:"\f04a"}.fa-bacon:before{content:"\f7e5"}.fa-bahai:before{content:"\f666"}.fa-balance-scale:before{content:"\f24e"}.fa-balance-scale-left:before{content:"\f515"}.fa-balance-scale-right:before{content:"\f516"}.fa-ban:before{content:"\f05e"}.fa-band-aid:before{content:"\f462"}.fa-bandcamp:before{content:"\f2d5"}.fa-barcode:before{content:"\f02a"}.fa-bars:before{content:"\f0c9"}.fa-baseball-ball:before{content:"\f433"}.fa-basketball-ball:before{content:"\f434"}.fa-bath:before{content:"\f2cd"}.fa-battery-empty:before{content:"\f244"}.fa-battery-full:before{content:"\f240"}.fa-battery-half:before{content:"\f242"}.fa-battery-quarter:before{content:"\f243"}.fa-battery-three-quarters:before{content:"\f241"}.fa-battle-net:before{content:"\f835"}.fa-bed:before{content:"\f236"}.fa-beer:before{content:"\f0fc"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-bell:before{content:"\f0f3"}.fa-bell-slash:before{content:"\f1f6"}.fa-bezier-curve:before{content:"\f55b"}.fa-bible:before{content:"\f647"}.fa-bicycle:before{content:"\f206"}.fa-biking:before{content:"\f84a"}.fa-bimobject:before{content:"\f378"}.fa-binoculars:before{content:"\f1e5"}.fa-biohazard:before{content:"\f780"}.fa-birthday-cake:before{content:"\f1fd"}.fa-bitbucket:before{content:"\f171"}.fa-bitcoin:before{content:"\f379"}.fa-bity:before{content:"\f37a"}.fa-black-tie:before{content:"\f27e"}.fa-blackberry:before{content:"\f37b"}.fa-blender:before{content:"\f517"}.fa-blender-phone:before{content:"\f6b6"}.fa-blind:before{content:"\f29d"}.fa-blog:before{content:"\f781"}.fa-blogger:before{content:"\f37c"}.fa-blogger-b:before{content:"\f37d"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-bold:before{content:"\f032"}.fa-bolt:before{content:"\f0e7"}.fa-bomb:before{content:"\f1e2"}.fa-bone:before{content:"\f5d7"}.fa-bong:before{content:"\f55c"}.fa-book:before{content:"\f02d"}.fa-book-dead:before{content:"\f6b7"}.fa-book-medical:before{content:"\f7e6"}.fa-book-open:before{content:"\f518"}.fa-book-reader:before{content:"\f5da"}.fa-bookmark:before{content:"\f02e"}.fa-bootstrap:before{content:"\f836"}.fa-border-all:before{content:"\f84c"}.fa-border-none:before{content:"\f850"}.fa-border-style:before{content:"\f853"}.fa-bowling-ball:before{content:"\f436"}.fa-box:before{content:"\f466"}.fa-box-open:before{content:"\f49e"}.fa-box-tissue:before{content:"\f95b"}.fa-boxes:before{content:"\f468"}.fa-braille:before{content:"\f2a1"}.fa-brain:before{content:"\f5dc"}.fa-bread-slice:before{content:"\f7ec"}.fa-briefcase:before{content:"\f0b1"}.fa-briefcase-medical:before{content:"\f469"}.fa-broadcast-tower:before{content:"\f519"}.fa-broom:before{content:"\f51a"}.fa-brush:before{content:"\f55d"}.fa-btc:before{content:"\f15a"}.fa-buffer:before{content:"\f837"}.fa-bug:before{content:"\f188"}.fa-building:before{content:"\f1ad"}.fa-bullhorn:before{content:"\f0a1"}.fa-bullseye:before{content:"\f140"}.fa-burn:before{content:"\f46a"}.fa-buromobelexperte:before{content:"\f37f"}.fa-bus:before{content:"\f207"}.fa-bus-alt:before{content:"\f55e"}.fa-business-time:before{content:"\f64a"}.fa-buy-n-large:before{content:"\f8a6"}.fa-buysellads:before{content:"\f20d"}.fa-calculator:before{content:"\f1ec"}.fa-calendar:before{content:"\f133"}.fa-calendar-alt:before{content:"\f073"}.fa-calendar-check:before{content:"\f274"}.fa-calendar-day:before{content:"\f783"}.fa-calendar-minus:before{content:"\f272"}.fa-calendar-plus:before{content:"\f271"}.fa-calendar-times:before{content:"\f273"}.fa-calendar-week:before{content:"\f784"}.fa-camera:before{content:"\f030"}.fa-camera-retro:before{content:"\f083"}.fa-campground:before{content:"\f6bb"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-candy-cane:before{content:"\f786"}.fa-cannabis:before{content:"\f55f"}.fa-capsules:before{content:"\f46b"}.fa-car:before{content:"\f1b9"}.fa-car-alt:before{content:"\f5de"}.fa-car-battery:before{content:"\f5df"}.fa-car-crash:before{content:"\f5e1"}.fa-car-side:before{content:"\f5e4"}.fa-caravan:before{content:"\f8ff"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-caret-square-down:before{content:"\f150"}.fa-caret-square-left:before{content:"\f191"}.fa-caret-square-right:before{content:"\f152"}.fa-caret-square-up:before{content:"\f151"}.fa-caret-up:before{content:"\f0d8"}.fa-carrot:before{content:"\f787"}.fa-cart-arrow-down:before{content:"\f218"}.fa-cart-plus:before{content:"\f217"}.fa-cash-register:before{content:"\f788"}.fa-cat:before{content:"\f6be"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-apple-pay:before{content:"\f416"}.fa-cc-diners-club:before{content:"\f24c"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-cc-visa:before{content:"\f1f0"}.fa-centercode:before{content:"\f380"}.fa-centos:before{content:"\f789"}.fa-certificate:before{content:"\f0a3"}.fa-chair:before{content:"\f6c0"}.fa-chalkboard:before{content:"\f51b"}.fa-chalkboard-teacher:before{content:"\f51c"}.fa-charging-station:before{content:"\f5e7"}.fa-chart-area:before{content:"\f1fe"}.fa-chart-bar:before{content:"\f080"}.fa-chart-line:before{content:"\f201"}.fa-chart-pie:before{content:"\f200"}.fa-check:before{content:"\f00c"}.fa-check-circle:before{content:"\f058"}.fa-check-double:before{content:"\f560"}.fa-check-square:before{content:"\f14a"}.fa-cheese:before{content:"\f7ef"}.fa-chess:before{content:"\f439"}.fa-chess-bishop:before{content:"\f43a"}.fa-chess-board:before{content:"\f43c"}.fa-chess-king:before{content:"\f43f"}.fa-chess-knight:before{content:"\f441"}.fa-chess-pawn:before{content:"\f443"}.fa-chess-queen:before{content:"\f445"}.fa-chess-rook:before{content:"\f447"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-down:before{content:"\f078"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-chevron-up:before{content:"\f077"}.fa-child:before{content:"\f1ae"}.fa-chrome:before{content:"\f268"}.fa-chromecast:before{content:"\f838"}.fa-church:before{content:"\f51d"}.fa-circle:before{content:"\f111"}.fa-circle-notch:before{content:"\f1ce"}.fa-city:before{content:"\f64f"}.fa-clinic-medical:before{content:"\f7f2"}.fa-clipboard:before{content:"\f328"}.fa-clipboard-check:before{content:"\f46c"}.fa-clipboard-list:before{content:"\f46d"}.fa-clock:before{content:"\f017"}.fa-clone:before{content:"\f24d"}.fa-closed-captioning:before{content:"\f20a"}.fa-cloud:before{content:"\f0c2"}.fa-cloud-download-alt:before{content:"\f381"}.fa-cloud-meatball:before{content:"\f73b"}.fa-cloud-moon:before{content:"\f6c3"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-cloud-rain:before{content:"\f73d"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-cloud-sun:before{content:"\f6c4"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-cloud-upload-alt:before{content:"\f382"}.fa-cloudscale:before{content:"\f383"}.fa-cloudsmith:before{content:"\f384"}.fa-cloudversify:before{content:"\f385"}.fa-cocktail:before{content:"\f561"}.fa-code:before{content:"\f121"}.fa-code-branch:before{content:"\f126"}.fa-codepen:before{content:"\f1cb"}.fa-codiepie:before{content:"\f284"}.fa-coffee:before{content:"\f0f4"}.fa-cog:before{content:"\f013"}.fa-cogs:before{content:"\f085"}.fa-coins:before{content:"\f51e"}.fa-columns:before{content:"\f0db"}.fa-comment:before{content:"\f075"}.fa-comment-alt:before{content:"\f27a"}.fa-comment-dollar:before{content:"\f651"}.fa-comment-dots:before{content:"\f4ad"}.fa-comment-medical:before{content:"\f7f5"}.fa-comment-slash:before{content:"\f4b3"}.fa-comments:before{content:"\f086"}.fa-comments-dollar:before{content:"\f653"}.fa-compact-disc:before{content:"\f51f"}.fa-compass:before{content:"\f14e"}.fa-compress:before{content:"\f066"}.fa-compress-alt:before{content:"\f422"}.fa-compress-arrows-alt:before{content:"\f78c"}.fa-concierge-bell:before{content:"\f562"}.fa-confluence:before{content:"\f78d"}.fa-connectdevelop:before{content:"\f20e"}.fa-contao:before{content:"\f26d"}.fa-cookie:before{content:"\f563"}.fa-cookie-bite:before{content:"\f564"}.fa-copy:before{content:"\f0c5"}.fa-copyright:before{content:"\f1f9"}.fa-cotton-bureau:before{content:"\f89e"}.fa-couch:before{content:"\f4b8"}.fa-cpanel:before{content:"\f388"}.fa-creative-commons:before{content:"\f25e"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-credit-card:before{content:"\f09d"}.fa-critical-role:before{content:"\f6c9"}.fa-crop:before{content:"\f125"}.fa-crop-alt:before{content:"\f565"}.fa-cross:before{content:"\f654"}.fa-crosshairs:before{content:"\f05b"}.fa-crow:before{content:"\f520"}.fa-crown:before{content:"\f521"}.fa-crutch:before{content:"\f7f7"}.fa-css3:before{content:"\f13c"}.fa-css3-alt:before{content:"\f38b"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-cut:before{content:"\f0c4"}.fa-cuttlefish:before{content:"\f38c"}.fa-d-and-d:before{content:"\f38d"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-dailymotion:before{content:"\f952"}.fa-dashcube:before{content:"\f210"}.fa-database:before{content:"\f1c0"}.fa-deaf:before{content:"\f2a4"}.fa-delicious:before{content:"\f1a5"}.fa-democrat:before{content:"\f747"}.fa-deploydog:before{content:"\f38e"}.fa-deskpro:before{content:"\f38f"}.fa-desktop:before{content:"\f108"}.fa-dev:before{content:"\f6cc"}.fa-deviantart:before{content:"\f1bd"}.fa-dharmachakra:before{content:"\f655"}.fa-dhl:before{content:"\f790"}.fa-diagnoses:before{content:"\f470"}.fa-diaspora:before{content:"\f791"}.fa-dice:before{content:"\f522"}.fa-dice-d20:before{content:"\f6cf"}.fa-dice-d6:before{content:"\f6d1"}.fa-dice-five:before{content:"\f523"}.fa-dice-four:before{content:"\f524"}.fa-dice-one:before{content:"\f525"}.fa-dice-six:before{content:"\f526"}.fa-dice-three:before{content:"\f527"}.fa-dice-two:before{content:"\f528"}.fa-digg:before{content:"\f1a6"}.fa-digital-ocean:before{content:"\f391"}.fa-digital-tachograph:before{content:"\f566"}.fa-directions:before{content:"\f5eb"}.fa-discord:before{content:"\f392"}.fa-discourse:before{content:"\f393"}.fa-disease:before{content:"\f7fa"}.fa-divide:before{content:"\f529"}.fa-dizzy:before{content:"\f567"}.fa-dna:before{content:"\f471"}.fa-dochub:before{content:"\f394"}.fa-docker:before{content:"\f395"}.fa-dog:before{content:"\f6d3"}.fa-dollar-sign:before{content:"\f155"}.fa-dolly:before{content:"\f472"}.fa-dolly-flatbed:before{content:"\f474"}.fa-donate:before{content:"\f4b9"}.fa-door-closed:before{content:"\f52a"}.fa-door-open:before{content:"\f52b"}.fa-dot-circle:before{content:"\f192"}.fa-dove:before{content:"\f4ba"}.fa-download:before{content:"\f019"}.fa-draft2digital:before{content:"\f396"}.fa-drafting-compass:before{content:"\f568"}.fa-dragon:before{content:"\f6d5"}.fa-draw-polygon:before{content:"\f5ee"}.fa-dribbble:before{content:"\f17d"}.fa-dribbble-square:before{content:"\f397"}.fa-dropbox:before{content:"\f16b"}.fa-drum:before{content:"\f569"}.fa-drum-steelpan:before{content:"\f56a"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-drupal:before{content:"\f1a9"}.fa-dumbbell:before{content:"\f44b"}.fa-dumpster:before{content:"\f793"}.fa-dumpster-fire:before{content:"\f794"}.fa-dungeon:before{content:"\f6d9"}.fa-dyalog:before{content:"\f399"}.fa-earlybirds:before{content:"\f39a"}.fa-ebay:before{content:"\f4f4"}.fa-edge:before{content:"\f282"}.fa-edit:before{content:"\f044"}.fa-egg:before{content:"\f7fb"}.fa-eject:before{content:"\f052"}.fa-elementor:before{content:"\f430"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-ello:before{content:"\f5f1"}.fa-ember:before{content:"\f423"}.fa-empire:before{content:"\f1d1"}.fa-envelope:before{content:"\f0e0"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-text:before{content:"\f658"}.fa-envelope-square:before{content:"\f199"}.fa-envira:before{content:"\f299"}.fa-equals:before{content:"\f52c"}.fa-eraser:before{content:"\f12d"}.fa-erlang:before{content:"\f39d"}.fa-ethereum:before{content:"\f42e"}.fa-ethernet:before{content:"\f796"}.fa-etsy:before{content:"\f2d7"}.fa-euro-sign:before{content:"\f153"}.fa-evernote:before{content:"\f839"}.fa-exchange-alt:before{content:"\f362"}.fa-exclamation:before{content:"\f12a"}.fa-exclamation-circle:before{content:"\f06a"}.fa-exclamation-triangle:before{content:"\f071"}.fa-expand:before{content:"\f065"}.fa-expand-alt:before{content:"\f424"}.fa-expand-arrows-alt:before{content:"\f31e"}.fa-expeditedssl:before{content:"\f23e"}.fa-external-link-alt:before{content:"\f35d"}.fa-external-link-square-alt:before{content:"\f360"}.fa-eye:before{content:"\f06e"}.fa-eye-dropper:before{content:"\f1fb"}.fa-eye-slash:before{content:"\f070"}.fa-facebook:before{content:"\f09a"}.fa-facebook-f:before{content:"\f39e"}.fa-facebook-messenger:before{content:"\f39f"}.fa-facebook-square:before{content:"\f082"}.fa-fan:before{content:"\f863"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-fast-backward:before{content:"\f049"}.fa-fast-forward:before{content:"\f050"}.fa-faucet:before{content:"\f905"}.fa-fax:before{content:"\f1ac"}.fa-feather:before{content:"\f52d"}.fa-feather-alt:before{content:"\f56b"}.fa-fedex:before{content:"\f797"}.fa-fedora:before{content:"\f798"}.fa-female:before{content:"\f182"}.fa-fighter-jet:before{content:"\f0fb"}.fa-figma:before{content:"\f799"}.fa-file:before{content:"\f15b"}.fa-file-alt:before{content:"\f15c"}.fa-file-archive:before{content:"\f1c6"}.fa-file-audio:before{content:"\f1c7"}.fa-file-code:before{content:"\f1c9"}.fa-file-contract:before{content:"\f56c"}.fa-file-csv:before{content:"\f6dd"}.fa-file-download:before{content:"\f56d"}.fa-file-excel:before{content:"\f1c3"}.fa-file-export:before{content:"\f56e"}.fa-file-image:before{content:"\f1c5"}.fa-file-import:before{content:"\f56f"}.fa-file-invoice:before{content:"\f570"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-file-medical:before{content:"\f477"}.fa-file-medical-alt:before{content:"\f478"}.fa-file-pdf:before{content:"\f1c1"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-file-prescription:before{content:"\f572"}.fa-file-signature:before{content:"\f573"}.fa-file-upload:before{content:"\f574"}.fa-file-video:before{content:"\f1c8"}.fa-file-word:before{content:"\f1c2"}.fa-fill:before{content:"\f575"}.fa-fill-drip:before{content:"\f576"}.fa-film:before{content:"\f008"}.fa-filter:before{content:"\f0b0"}.fa-fingerprint:before{content:"\f577"}.fa-fire:before{content:"\f06d"}.fa-fire-alt:before{content:"\f7e4"}.fa-fire-extinguisher:before{content:"\f134"}.fa-firefox:before{content:"\f269"}.fa-firefox-browser:before{content:"\f907"}.fa-first-aid:before{content:"\f479"}.fa-first-order:before{content:"\f2b0"}.fa-first-order-alt:before{content:"\f50a"}.fa-firstdraft:before{content:"\f3a1"}.fa-fish:before{content:"\f578"}.fa-fist-raised:before{content:"\f6de"}.fa-flag:before{content:"\f024"}.fa-flag-checkered:before{content:"\f11e"}.fa-flag-usa:before{content:"\f74d"}.fa-flask:before{content:"\f0c3"}.fa-flickr:before{content:"\f16e"}.fa-flipboard:before{content:"\f44d"}.fa-flushed:before{content:"\f579"}.fa-fly:before{content:"\f417"}.fa-folder:before{content:"\f07b"}.fa-folder-minus:before{content:"\f65d"}.fa-folder-open:before{content:"\f07c"}.fa-folder-plus:before{content:"\f65e"}.fa-font:before{content:"\f031"}.fa-font-awesome:before{content:"\f2b4"}.fa-font-awesome-alt:before{content:"\f35c"}.fa-font-awesome-flag:before{content:"\f425"}.fa-font-awesome-logo-full:before{content:"\f4e6"}.fa-fonticons:before{content:"\f280"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-football-ball:before{content:"\f44e"}.fa-fort-awesome:before{content:"\f286"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-forumbee:before{content:"\f211"}.fa-forward:before{content:"\f04e"}.fa-foursquare:before{content:"\f180"}.fa-free-code-camp:before{content:"\f2c5"}.fa-freebsd:before{content:"\f3a4"}.fa-frog:before{content:"\f52e"}.fa-frown:before{content:"\f119"}.fa-frown-open:before{content:"\f57a"}.fa-fulcrum:before{content:"\f50b"}.fa-funnel-dollar:before{content:"\f662"}.fa-futbol:before{content:"\f1e3"}.fa-galactic-republic:before{content:"\f50c"}.fa-galactic-senate:before{content:"\f50d"}.fa-gamepad:before{content:"\f11b"}.fa-gas-pump:before{content:"\f52f"}.fa-gavel:before{content:"\f0e3"}.fa-gem:before{content:"\f3a5"}.fa-genderless:before{content:"\f22d"}.fa-get-pocket:before{content:"\f265"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-ghost:before{content:"\f6e2"}.fa-gift:before{content:"\f06b"}.fa-gifts:before{content:"\f79c"}.fa-git:before{content:"\f1d3"}.fa-git-alt:before{content:"\f841"}.fa-git-square:before{content:"\f1d2"}.fa-github:before{content:"\f09b"}.fa-github-alt:before{content:"\f113"}.fa-github-square:before{content:"\f092"}.fa-gitkraken:before{content:"\f3a6"}.fa-gitlab:before{content:"\f296"}.fa-gitter:before{content:"\f426"}.fa-glass-cheers:before{content:"\f79f"}.fa-glass-martini:before{content:"\f000"}.fa-glass-martini-alt:before{content:"\f57b"}.fa-glass-whiskey:before{content:"\f7a0"}.fa-glasses:before{content:"\f530"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-globe:before{content:"\f0ac"}.fa-globe-africa:before{content:"\f57c"}.fa-globe-americas:before{content:"\f57d"}.fa-globe-asia:before{content:"\f57e"}.fa-globe-europe:before{content:"\f7a2"}.fa-gofore:before{content:"\f3a7"}.fa-golf-ball:before{content:"\f450"}.fa-goodreads:before{content:"\f3a8"}.fa-goodreads-g:before{content:"\f3a9"}.fa-google:before{content:"\f1a0"}.fa-google-drive:before{content:"\f3aa"}.fa-google-play:before{content:"\f3ab"}.fa-google-plus:before{content:"\f2b3"}.fa-google-plus-g:before{content:"\f0d5"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-wallet:before{content:"\f1ee"}.fa-gopuram:before{content:"\f664"}.fa-graduation-cap:before{content:"\f19d"}.fa-gratipay:before{content:"\f184"}.fa-grav:before{content:"\f2d6"}.fa-greater-than:before{content:"\f531"}.fa-greater-than-equal:before{content:"\f532"}.fa-grimace:before{content:"\f57f"}.fa-grin:before{content:"\f580"}.fa-grin-alt:before{content:"\f581"}.fa-grin-beam:before{content:"\f582"}.fa-grin-beam-sweat:before{content:"\f583"}.fa-grin-hearts:before{content:"\f584"}.fa-grin-squint:before{content:"\f585"}.fa-grin-squint-tears:before{content:"\f586"}.fa-grin-stars:before{content:"\f587"}.fa-grin-tears:before{content:"\f588"}.fa-grin-tongue:before{content:"\f589"}.fa-grin-tongue-squint:before{content:"\f58a"}.fa-grin-tongue-wink:before{content:"\f58b"}.fa-grin-wink:before{content:"\f58c"}.fa-grip-horizontal:before{content:"\f58d"}.fa-grip-lines:before{content:"\f7a4"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-grip-vertical:before{content:"\f58e"}.fa-gripfire:before{content:"\f3ac"}.fa-grunt:before{content:"\f3ad"}.fa-guitar:before{content:"\f7a6"}.fa-gulp:before{content:"\f3ae"}.fa-h-square:before{content:"\f0fd"}.fa-hacker-news:before{content:"\f1d4"}.fa-hacker-news-square:before{content:"\f3af"}.fa-hackerrank:before{content:"\f5f7"}.fa-hamburger:before{content:"\f805"}.fa-hammer:before{content:"\f6e3"}.fa-hamsa:before{content:"\f665"}.fa-hand-holding:before{content:"\f4bd"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-hand-holding-medical:before{content:"\f95c"}.fa-hand-holding-usd:before{content:"\f4c0"}.fa-hand-holding-water:before{content:"\f4c1"}.fa-hand-lizard:before{content:"\f258"}.fa-hand-middle-finger:before{content:"\f806"}.fa-hand-paper:before{content:"\f256"}.fa-hand-peace:before{content:"\f25b"}.fa-hand-point-down:before{content:"\f0a7"}.fa-hand-point-left:before{content:"\f0a5"}.fa-hand-point-right:before{content:"\f0a4"}.fa-hand-point-up:before{content:"\f0a6"}.fa-hand-pointer:before{content:"\f25a"}.fa-hand-rock:before{content:"\f255"}.fa-hand-scissors:before{content:"\f257"}.fa-hand-sparkles:before{content:"\f95d"}.fa-hand-spock:before{content:"\f259"}.fa-hands:before{content:"\f4c2"}.fa-hands-helping:before{content:"\f4c4"}.fa-hands-wash:before{content:"\f95e"}.fa-handshake:before{content:"\f2b5"}.fa-handshake-alt-slash:before{content:"\f95f"}.fa-handshake-slash:before{content:"\f960"}.fa-hanukiah:before{content:"\f6e6"}.fa-hard-hat:before{content:"\f807"}.fa-hashtag:before{content:"\f292"}.fa-hat-cowboy:before{content:"\f8c0"}.fa-hat-cowboy-side:before{content:"\f8c1"}.fa-hat-wizard:before{content:"\f6e8"}.fa-hdd:before{content:"\f0a0"}.fa-head-side-cough:before{content:"\f961"}.fa-head-side-cough-slash:before{content:"\f962"}.fa-head-side-mask:before{content:"\f963"}.fa-head-side-virus:before{content:"\f964"}.fa-heading:before{content:"\f1dc"}.fa-headphones:before{content:"\f025"}.fa-headphones-alt:before{content:"\f58f"}.fa-headset:before{content:"\f590"}.fa-heart:before{content:"\f004"}.fa-heart-broken:before{content:"\f7a9"}.fa-heartbeat:before{content:"\f21e"}.fa-helicopter:before{content:"\f533"}.fa-highlighter:before{content:"\f591"}.fa-hiking:before{content:"\f6ec"}.fa-hippo:before{content:"\f6ed"}.fa-hips:before{content:"\f452"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-history:before{content:"\f1da"}.fa-hockey-puck:before{content:"\f453"}.fa-holly-berry:before{content:"\f7aa"}.fa-home:before{content:"\f015"}.fa-hooli:before{content:"\f427"}.fa-hornbill:before{content:"\f592"}.fa-horse:before{content:"\f6f0"}.fa-horse-head:before{content:"\f7ab"}.fa-hospital:before{content:"\f0f8"}.fa-hospital-alt:before{content:"\f47d"}.fa-hospital-symbol:before{content:"\f47e"}.fa-hospital-user:before{content:"\f80d"}.fa-hot-tub:before{content:"\f593"}.fa-hotdog:before{content:"\f80f"}.fa-hotel:before{content:"\f594"}.fa-hotjar:before{content:"\f3b1"}.fa-hourglass:before{content:"\f254"}.fa-hourglass-end:before{content:"\f253"}.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-start:before{content:"\f251"}.fa-house-damage:before{content:"\f6f1"}.fa-house-user:before{content:"\f965"}.fa-houzz:before{content:"\f27c"}.fa-hryvnia:before{content:"\f6f2"}.fa-html5:before{content:"\f13b"}.fa-hubspot:before{content:"\f3b2"}.fa-i-cursor:before{content:"\f246"}.fa-ice-cream:before{content:"\f810"}.fa-icicles:before{content:"\f7ad"}.fa-icons:before{content:"\f86d"}.fa-id-badge:before{content:"\f2c1"}.fa-id-card:before{content:"\f2c2"}.fa-id-card-alt:before{content:"\f47f"}.fa-ideal:before{content:"\f913"}.fa-igloo:before{content:"\f7ae"}.fa-image:before{content:"\f03e"}.fa-images:before{content:"\f302"}.fa-imdb:before{content:"\f2d8"}.fa-inbox:before{content:"\f01c"}.fa-indent:before{content:"\f03c"}.fa-industry:before{content:"\f275"}.fa-infinity:before{content:"\f534"}.fa-info:before{content:"\f129"}.fa-info-circle:before{content:"\f05a"}.fa-instagram:before{content:"\f16d"}.fa-instagram-square:before{content:"\f955"}.fa-intercom:before{content:"\f7af"}.fa-internet-explorer:before{content:"\f26b"}.fa-invision:before{content:"\f7b0"}.fa-ioxhost:before{content:"\f208"}.fa-italic:before{content:"\f033"}.fa-itch-io:before{content:"\f83a"}.fa-itunes:before{content:"\f3b4"}.fa-itunes-note:before{content:"\f3b5"}.fa-java:before{content:"\f4e4"}.fa-jedi:before{content:"\f669"}.fa-jedi-order:before{content:"\f50e"}.fa-jenkins:before{content:"\f3b6"}.fa-jira:before{content:"\f7b1"}.fa-joget:before{content:"\f3b7"}.fa-joint:before{content:"\f595"}.fa-joomla:before{content:"\f1aa"}.fa-journal-whills:before{content:"\f66a"}.fa-js:before{content:"\f3b8"}.fa-js-square:before{content:"\f3b9"}.fa-jsfiddle:before{content:"\f1cc"}.fa-kaaba:before{content:"\f66b"}.fa-kaggle:before{content:"\f5fa"}.fa-key:before{content:"\f084"}.fa-keybase:before{content:"\f4f5"}.fa-keyboard:before{content:"\f11c"}.fa-keycdn:before{content:"\f3ba"}.fa-khanda:before{content:"\f66d"}.fa-kickstarter:before{content:"\f3bb"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-kiss:before{content:"\f596"}.fa-kiss-beam:before{content:"\f597"}.fa-kiss-wink-heart:before{content:"\f598"}.fa-kiwi-bird:before{content:"\f535"}.fa-korvue:before{content:"\f42f"}.fa-landmark:before{content:"\f66f"}.fa-language:before{content:"\f1ab"}.fa-laptop:before{content:"\f109"}.fa-laptop-code:before{content:"\f5fc"}.fa-laptop-house:before{content:"\f966"}.fa-laptop-medical:before{content:"\f812"}.fa-laravel:before{content:"\f3bd"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-laugh:before{content:"\f599"}.fa-laugh-beam:before{content:"\f59a"}.fa-laugh-squint:before{content:"\f59b"}.fa-laugh-wink:before{content:"\f59c"}.fa-layer-group:before{content:"\f5fd"}.fa-leaf:before{content:"\f06c"}.fa-leanpub:before{content:"\f212"}.fa-lemon:before{content:"\f094"}.fa-less:before{content:"\f41d"}.fa-less-than:before{content:"\f536"}.fa-less-than-equal:before{content:"\f537"}.fa-level-down-alt:before{content:"\f3be"}.fa-level-up-alt:before{content:"\f3bf"}.fa-life-ring:before{content:"\f1cd"}.fa-lightbulb:before{content:"\f0eb"}.fa-line:before{content:"\f3c0"}.fa-link:before{content:"\f0c1"}.fa-linkedin:before{content:"\f08c"}.fa-linkedin-in:before{content:"\f0e1"}.fa-linode:before{content:"\f2b8"}.fa-linux:before{content:"\f17c"}.fa-lira-sign:before{content:"\f195"}.fa-list:before{content:"\f03a"}.fa-list-alt:before{content:"\f022"}.fa-list-ol:before{content:"\f0cb"}.fa-list-ul:before{content:"\f0ca"}.fa-location-arrow:before{content:"\f124"}.fa-lock:before{content:"\f023"}.fa-lock-open:before{content:"\f3c1"}.fa-long-arrow-alt-down:before{content:"\f309"}.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-long-arrow-alt-right:before{content:"\f30b"}.fa-long-arrow-alt-up:before{content:"\f30c"}.fa-low-vision:before{content:"\f2a8"}.fa-luggage-cart:before{content:"\f59d"}.fa-lungs:before{content:"\f604"}.fa-lungs-virus:before{content:"\f967"}.fa-lyft:before{content:"\f3c3"}.fa-magento:before{content:"\f3c4"}.fa-magic:before{content:"\f0d0"}.fa-magnet:before{content:"\f076"}.fa-mail-bulk:before{content:"\f674"}.fa-mailchimp:before{content:"\f59e"}.fa-male:before{content:"\f183"}.fa-mandalorian:before{content:"\f50f"}.fa-map:before{content:"\f279"}.fa-map-marked:before{content:"\f59f"}.fa-map-marked-alt:before{content:"\f5a0"}.fa-map-marker:before{content:"\f041"}.fa-map-marker-alt:before{content:"\f3c5"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-markdown:before{content:"\f60f"}.fa-marker:before{content:"\f5a1"}.fa-mars:before{content:"\f222"}.fa-mars-double:before{content:"\f227"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mask:before{content:"\f6fa"}.fa-mastodon:before{content:"\f4f6"}.fa-maxcdn:before{content:"\f136"}.fa-mdb:before{content:"\f8ca"}.fa-medal:before{content:"\f5a2"}.fa-medapps:before{content:"\f3c6"}.fa-medium:before{content:"\f23a"}.fa-medium-m:before{content:"\f3c7"}.fa-medkit:before{content:"\f0fa"}.fa-medrt:before{content:"\f3c8"}.fa-meetup:before{content:"\f2e0"}.fa-megaport:before{content:"\f5a3"}.fa-meh:before{content:"\f11a"}.fa-meh-blank:before{content:"\f5a4"}.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-memory:before{content:"\f538"}.fa-mendeley:before{content:"\f7b3"}.fa-menorah:before{content:"\f676"}.fa-mercury:before{content:"\f223"}.fa-meteor:before{content:"\f753"}.fa-microblog:before{content:"\f91a"}.fa-microchip:before{content:"\f2db"}.fa-microphone:before{content:"\f130"}.fa-microphone-alt:before{content:"\f3c9"}.fa-microphone-alt-slash:before{content:"\f539"}.fa-microphone-slash:before{content:"\f131"}.fa-microscope:before{content:"\f610"}.fa-microsoft:before{content:"\f3ca"}.fa-minus:before{content:"\f068"}.fa-minus-circle:before{content:"\f056"}.fa-minus-square:before{content:"\f146"}.fa-mitten:before{content:"\f7b5"}.fa-mix:before{content:"\f3cb"}.fa-mixcloud:before{content:"\f289"}.fa-mixer:before{content:"\f956"}.fa-mizuni:before{content:"\f3cc"}.fa-mobile:before{content:"\f10b"}.fa-mobile-alt:before{content:"\f3cd"}.fa-modx:before{content:"\f285"}.fa-monero:before{content:"\f3d0"}.fa-money-bill:before{content:"\f0d6"}.fa-money-bill-alt:before{content:"\f3d1"}.fa-money-bill-wave:before{content:"\f53a"}.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-money-check:before{content:"\f53c"}.fa-money-check-alt:before{content:"\f53d"}.fa-monument:before{content:"\f5a6"}.fa-moon:before{content:"\f186"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-mosque:before{content:"\f678"}.fa-motorcycle:before{content:"\f21c"}.fa-mountain:before{content:"\f6fc"}.fa-mouse:before{content:"\f8cc"}.fa-mouse-pointer:before{content:"\f245"}.fa-mug-hot:before{content:"\f7b6"}.fa-music:before{content:"\f001"}.fa-napster:before{content:"\f3d2"}.fa-neos:before{content:"\f612"}.fa-network-wired:before{content:"\f6ff"}.fa-neuter:before{content:"\f22c"}.fa-newspaper:before{content:"\f1ea"}.fa-nimblr:before{content:"\f5a8"}.fa-node:before{content:"\f419"}.fa-node-js:before{content:"\f3d3"}.fa-not-equal:before{content:"\f53e"}.fa-notes-medical:before{content:"\f481"}.fa-npm:before{content:"\f3d4"}.fa-ns8:before{content:"\f3d5"}.fa-nutritionix:before{content:"\f3d6"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-oil-can:before{content:"\f613"}.fa-old-republic:before{content:"\f510"}.fa-om:before{content:"\f679"}.fa-opencart:before{content:"\f23d"}.fa-openid:before{content:"\f19b"}.fa-opera:before{content:"\f26a"}.fa-optin-monster:before{content:"\f23c"}.fa-orcid:before{content:"\f8d2"}.fa-osi:before{content:"\f41a"}.fa-otter:before{content:"\f700"}.fa-outdent:before{content:"\f03b"}.fa-page4:before{content:"\f3d7"}.fa-pagelines:before{content:"\f18c"}.fa-pager:before{content:"\f815"}.fa-paint-brush:before{content:"\f1fc"}.fa-paint-roller:before{content:"\f5aa"}.fa-palette:before{content:"\f53f"}.fa-palfed:before{content:"\f3d8"}.fa-pallet:before{content:"\f482"}.fa-paper-plane:before{content:"\f1d8"}.fa-paperclip:before{content:"\f0c6"}.fa-parachute-box:before{content:"\f4cd"}.fa-paragraph:before{content:"\f1dd"}.fa-parking:before{content:"\f540"}.fa-passport:before{content:"\f5ab"}.fa-pastafarianism:before{content:"\f67b"}.fa-paste:before{content:"\f0ea"}.fa-patreon:before{content:"\f3d9"}.fa-pause:before{content:"\f04c"}.fa-pause-circle:before{content:"\f28b"}.fa-paw:before{content:"\f1b0"}.fa-paypal:before{content:"\f1ed"}.fa-peace:before{content:"\f67c"}.fa-pen:before{content:"\f304"}.fa-pen-alt:before{content:"\f305"}.fa-pen-fancy:before{content:"\f5ac"}.fa-pen-nib:before{content:"\f5ad"}.fa-pen-square:before{content:"\f14b"}.fa-pencil-alt:before{content:"\f303"}.fa-pencil-ruler:before{content:"\f5ae"}.fa-penny-arcade:before{content:"\f704"}.fa-people-arrows:before{content:"\f968"}.fa-people-carry:before{content:"\f4ce"}.fa-pepper-hot:before{content:"\f816"}.fa-percent:before{content:"\f295"}.fa-percentage:before{content:"\f541"}.fa-periscope:before{content:"\f3da"}.fa-person-booth:before{content:"\f756"}.fa-phabricator:before{content:"\f3db"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-phoenix-squadron:before{content:"\f511"}.fa-phone:before{content:"\f095"}.fa-phone-alt:before{content:"\f879"}.fa-phone-slash:before{content:"\f3dd"}.fa-phone-square:before{content:"\f098"}.fa-phone-square-alt:before{content:"\f87b"}.fa-phone-volume:before{content:"\f2a0"}.fa-photo-video:before{content:"\f87c"}.fa-php:before{content:"\f457"}.fa-pied-piper:before{content:"\f2ae"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-square:before{content:"\f91e"}.fa-piggy-bank:before{content:"\f4d3"}.fa-pills:before{content:"\f484"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-p:before{content:"\f231"}.fa-pinterest-square:before{content:"\f0d3"}.fa-pizza-slice:before{content:"\f818"}.fa-place-of-worship:before{content:"\f67f"}.fa-plane:before{content:"\f072"}.fa-plane-arrival:before{content:"\f5af"}.fa-plane-departure:before{content:"\f5b0"}.fa-plane-slash:before{content:"\f969"}.fa-play:before{content:"\f04b"}.fa-play-circle:before{content:"\f144"}.fa-playstation:before{content:"\f3df"}.fa-plug:before{content:"\f1e6"}.fa-plus:before{content:"\f067"}.fa-plus-circle:before{content:"\f055"}.fa-plus-square:before{content:"\f0fe"}.fa-podcast:before{content:"\f2ce"}.fa-poll:before{content:"\f681"}.fa-poll-h:before{content:"\f682"}.fa-poo:before{content:"\f2fe"}.fa-poo-storm:before{content:"\f75a"}.fa-poop:before{content:"\f619"}.fa-portrait:before{content:"\f3e0"}.fa-pound-sign:before{content:"\f154"}.fa-power-off:before{content:"\f011"}.fa-pray:before{content:"\f683"}.fa-praying-hands:before{content:"\f684"}.fa-prescription:before{content:"\f5b1"}.fa-prescription-bottle:before{content:"\f485"}.fa-prescription-bottle-alt:before{content:"\f486"}.fa-print:before{content:"\f02f"}.fa-procedures:before{content:"\f487"}.fa-product-hunt:before{content:"\f288"}.fa-project-diagram:before{content:"\f542"}.fa-pump-medical:before{content:"\f96a"}.fa-pump-soap:before{content:"\f96b"}.fa-pushed:before{content:"\f3e1"}.fa-puzzle-piece:before{content:"\f12e"}.fa-python:before{content:"\f3e2"}.fa-qq:before{content:"\f1d6"}.fa-qrcode:before{content:"\f029"}.fa-question:before{content:"\f128"}.fa-question-circle:before{content:"\f059"}.fa-quidditch:before{content:"\f458"}.fa-quinscape:before{content:"\f459"}.fa-quora:before{content:"\f2c4"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-quran:before{content:"\f687"}.fa-r-project:before{content:"\f4f7"}.fa-radiation:before{content:"\f7b9"}.fa-radiation-alt:before{content:"\f7ba"}.fa-rainbow:before{content:"\f75b"}.fa-random:before{content:"\f074"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-ravelry:before{content:"\f2d9"}.fa-react:before{content:"\f41b"}.fa-reacteurope:before{content:"\f75d"}.fa-readme:before{content:"\f4d5"}.fa-rebel:before{content:"\f1d0"}.fa-receipt:before{content:"\f543"}.fa-record-vinyl:before{content:"\f8d9"}.fa-recycle:before{content:"\f1b8"}.fa-red-river:before{content:"\f3e3"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-alien:before{content:"\f281"}.fa-reddit-square:before{content:"\f1a2"}.fa-redhat:before{content:"\f7bc"}.fa-redo:before{content:"\f01e"}.fa-redo-alt:before{content:"\f2f9"}.fa-registered:before{content:"\f25d"}.fa-remove-format:before{content:"\f87d"}.fa-renren:before{content:"\f18b"}.fa-reply:before{content:"\f3e5"}.fa-reply-all:before{content:"\f122"}.fa-replyd:before{content:"\f3e6"}.fa-republican:before{content:"\f75e"}.fa-researchgate:before{content:"\f4f8"}.fa-resolving:before{content:"\f3e7"}.fa-restroom:before{content:"\f7bd"}.fa-retweet:before{content:"\f079"}.fa-rev:before{content:"\f5b2"}.fa-ribbon:before{content:"\f4d6"}.fa-ring:before{content:"\f70b"}.fa-road:before{content:"\f018"}.fa-robot:before{content:"\f544"}.fa-rocket:before{content:"\f135"}.fa-rocketchat:before{content:"\f3e8"}.fa-rockrms:before{content:"\f3e9"}.fa-route:before{content:"\f4d7"}.fa-rss:before{content:"\f09e"}.fa-rss-square:before{content:"\f143"}.fa-ruble-sign:before{content:"\f158"}.fa-ruler:before{content:"\f545"}.fa-ruler-combined:before{content:"\f546"}.fa-ruler-horizontal:before{content:"\f547"}.fa-ruler-vertical:before{content:"\f548"}.fa-running:before{content:"\f70c"}.fa-rupee-sign:before{content:"\f156"}.fa-sad-cry:before{content:"\f5b3"}.fa-sad-tear:before{content:"\f5b4"}.fa-safari:before{content:"\f267"}.fa-salesforce:before{content:"\f83b"}.fa-sass:before{content:"\f41e"}.fa-satellite:before{content:"\f7bf"}.fa-satellite-dish:before{content:"\f7c0"}.fa-save:before{content:"\f0c7"}.fa-schlix:before{content:"\f3ea"}.fa-school:before{content:"\f549"}.fa-screwdriver:before{content:"\f54a"}.fa-scribd:before{content:"\f28a"}.fa-scroll:before{content:"\f70e"}.fa-sd-card:before{content:"\f7c2"}.fa-search:before{content:"\f002"}.fa-search-dollar:before{content:"\f688"}.fa-search-location:before{content:"\f689"}.fa-search-minus:before{content:"\f010"}.fa-search-plus:before{content:"\f00e"}.fa-searchengin:before{content:"\f3eb"}.fa-seedling:before{content:"\f4d8"}.fa-sellcast:before{content:"\f2da"}.fa-sellsy:before{content:"\f213"}.fa-server:before{content:"\f233"}.fa-servicestack:before{content:"\f3ec"}.fa-shapes:before{content:"\f61f"}.fa-share:before{content:"\f064"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-share-square:before{content:"\f14d"}.fa-shekel-sign:before{content:"\f20b"}.fa-shield-alt:before{content:"\f3ed"}.fa-shield-virus:before{content:"\f96c"}.fa-ship:before{content:"\f21a"}.fa-shipping-fast:before{content:"\f48b"}.fa-shirtsinbulk:before{content:"\f214"}.fa-shoe-prints:before{content:"\f54b"}.fa-shopify:before{content:"\f957"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-shopping-cart:before{content:"\f07a"}.fa-shopware:before{content:"\f5b5"}.fa-shower:before{content:"\f2cc"}.fa-shuttle-van:before{content:"\f5b6"}.fa-sign:before{content:"\f4d9"}.fa-sign-in-alt:before{content:"\f2f6"}.fa-sign-language:before{content:"\f2a7"}.fa-sign-out-alt:before{content:"\f2f5"}.fa-signal:before{content:"\f012"}.fa-signature:before{content:"\f5b7"}.fa-sim-card:before{content:"\f7c4"}.fa-simplybuilt:before{content:"\f215"}.fa-sistrix:before{content:"\f3ee"}.fa-sitemap:before{content:"\f0e8"}.fa-sith:before{content:"\f512"}.fa-skating:before{content:"\f7c5"}.fa-sketch:before{content:"\f7c6"}.fa-skiing:before{content:"\f7c9"}.fa-skiing-nordic:before{content:"\f7ca"}.fa-skull:before{content:"\f54c"}.fa-skull-crossbones:before{content:"\f714"}.fa-skyatlas:before{content:"\f216"}.fa-skype:before{content:"\f17e"}.fa-slack:before{content:"\f198"}.fa-slack-hash:before{content:"\f3ef"}.fa-slash:before{content:"\f715"}.fa-sleigh:before{content:"\f7cc"}.fa-sliders-h:before{content:"\f1de"}.fa-slideshare:before{content:"\f1e7"}.fa-smile:before{content:"\f118"}.fa-smile-beam:before{content:"\f5b8"}.fa-smile-wink:before{content:"\f4da"}.fa-smog:before{content:"\f75f"}.fa-smoking:before{content:"\f48d"}.fa-smoking-ban:before{content:"\f54d"}.fa-sms:before{content:"\f7cd"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-snowboarding:before{content:"\f7ce"}.fa-snowflake:before{content:"\f2dc"}.fa-snowman:before{content:"\f7d0"}.fa-snowplow:before{content:"\f7d2"}.fa-soap:before{content:"\f96e"}.fa-socks:before{content:"\f696"}.fa-solar-panel:before{content:"\f5ba"}.fa-sort:before{content:"\f0dc"}.fa-sort-alpha-down:before{content:"\f15d"}.fa-sort-alpha-down-alt:before{content:"\f881"}.fa-sort-alpha-up:before{content:"\f15e"}.fa-sort-alpha-up-alt:before{content:"\f882"}.fa-sort-amount-down:before{content:"\f160"}.fa-sort-amount-down-alt:before{content:"\f884"}.fa-sort-amount-up:before{content:"\f161"}.fa-sort-amount-up-alt:before{content:"\f885"}.fa-sort-down:before{content:"\f0dd"}.fa-sort-numeric-down:before{content:"\f162"}.fa-sort-numeric-down-alt:before{content:"\f886"}.fa-sort-numeric-up:before{content:"\f163"}.fa-sort-numeric-up-alt:before{content:"\f887"}.fa-sort-up:before{content:"\f0de"}.fa-soundcloud:before{content:"\f1be"}.fa-sourcetree:before{content:"\f7d3"}.fa-spa:before{content:"\f5bb"}.fa-space-shuttle:before{content:"\f197"}.fa-speakap:before{content:"\f3f3"}.fa-speaker-deck:before{content:"\f83c"}.fa-spell-check:before{content:"\f891"}.fa-spider:before{content:"\f717"}.fa-spinner:before{content:"\f110"}.fa-splotch:before{content:"\f5bc"}.fa-spotify:before{content:"\f1bc"}.fa-spray-can:before{content:"\f5bd"}.fa-square:before{content:"\f0c8"}.fa-square-full:before{content:"\f45c"}.fa-square-root-alt:before{content:"\f698"}.fa-squarespace:before{content:"\f5be"}.fa-stack-exchange:before{content:"\f18d"}.fa-stack-overflow:before{content:"\f16c"}.fa-stackpath:before{content:"\f842"}.fa-stamp:before{content:"\f5bf"}.fa-star:before{content:"\f005"}.fa-star-and-crescent:before{content:"\f699"}.fa-star-half:before{content:"\f089"}.fa-star-half-alt:before{content:"\f5c0"}.fa-star-of-david:before{content:"\f69a"}.fa-star-of-life:before{content:"\f621"}.fa-staylinked:before{content:"\f3f5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-steam-symbol:before{content:"\f3f6"}.fa-step-backward:before{content:"\f048"}.fa-step-forward:before{content:"\f051"}.fa-stethoscope:before{content:"\f0f1"}.fa-sticker-mule:before{content:"\f3f7"}.fa-sticky-note:before{content:"\f249"}.fa-stop:before{content:"\f04d"}.fa-stop-circle:before{content:"\f28d"}.fa-stopwatch:before{content:"\f2f2"}.fa-stopwatch-20:before{content:"\f96f"}.fa-store:before{content:"\f54e"}.fa-store-alt:before{content:"\f54f"}.fa-store-alt-slash:before{content:"\f970"}.fa-store-slash:before{content:"\f971"}.fa-strava:before{content:"\f428"}.fa-stream:before{content:"\f550"}.fa-street-view:before{content:"\f21d"}.fa-strikethrough:before{content:"\f0cc"}.fa-stripe:before{content:"\f429"}.fa-stripe-s:before{content:"\f42a"}.fa-stroopwafel:before{content:"\f551"}.fa-studiovinari:before{content:"\f3f8"}.fa-stumbleupon:before{content:"\f1a4"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-subscript:before{content:"\f12c"}.fa-subway:before{content:"\f239"}.fa-suitcase:before{content:"\f0f2"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-sun:before{content:"\f185"}.fa-superpowers:before{content:"\f2dd"}.fa-superscript:before{content:"\f12b"}.fa-supple:before{content:"\f3f9"}.fa-surprise:before{content:"\f5c2"}.fa-suse:before{content:"\f7d6"}.fa-swatchbook:before{content:"\f5c3"}.fa-swift:before{content:"\f8e1"}.fa-swimmer:before{content:"\f5c4"}.fa-swimming-pool:before{content:"\f5c5"}.fa-symfony:before{content:"\f83d"}.fa-synagogue:before{content:"\f69b"}.fa-sync:before{content:"\f021"}.fa-sync-alt:before{content:"\f2f1"}.fa-syringe:before{content:"\f48e"}.fa-table:before{content:"\f0ce"}.fa-table-tennis:before{content:"\f45d"}.fa-tablet:before{content:"\f10a"}.fa-tablet-alt:before{content:"\f3fa"}.fa-tablets:before{content:"\f490"}.fa-tachometer-alt:before{content:"\f3fd"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-tape:before{content:"\f4db"}.fa-tasks:before{content:"\f0ae"}.fa-taxi:before{content:"\f1ba"}.fa-teamspeak:before{content:"\f4f9"}.fa-teeth:before{content:"\f62e"}.fa-teeth-open:before{content:"\f62f"}.fa-telegram:before{content:"\f2c6"}.fa-telegram-plane:before{content:"\f3fe"}.fa-temperature-high:before{content:"\f769"}.fa-temperature-low:before{content:"\f76b"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-tenge:before{content:"\f7d7"}.fa-terminal:before{content:"\f120"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-th:before{content:"\f00a"}.fa-th-large:before{content:"\f009"}.fa-th-list:before{content:"\f00b"}.fa-the-red-yeti:before{content:"\f69d"}.fa-theater-masks:before{content:"\f630"}.fa-themeco:before{content:"\f5c6"}.fa-themeisle:before{content:"\f2b2"}.fa-thermometer:before{content:"\f491"}.fa-thermometer-empty:before{content:"\f2cb"}.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-think-peaks:before{content:"\f731"}.fa-thumbs-down:before{content:"\f165"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbtack:before{content:"\f08d"}.fa-ticket-alt:before{content:"\f3ff"}.fa-times:before{content:"\f00d"}.fa-times-circle:before{content:"\f057"}.fa-tint:before{content:"\f043"}.fa-tint-slash:before{content:"\f5c7"}.fa-tired:before{content:"\f5c8"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-toilet:before{content:"\f7d8"}.fa-toilet-paper:before{content:"\f71e"}.fa-toilet-paper-slash:before{content:"\f972"}.fa-toolbox:before{content:"\f552"}.fa-tools:before{content:"\f7d9"}.fa-tooth:before{content:"\f5c9"}.fa-torah:before{content:"\f6a0"}.fa-torii-gate:before{content:"\f6a1"}.fa-tractor:before{content:"\f722"}.fa-trade-federation:before{content:"\f513"}.fa-trademark:before{content:"\f25c"}.fa-traffic-light:before{content:"\f637"}.fa-trailer:before{content:"\f941"}.fa-train:before{content:"\f238"}.fa-tram:before{content:"\f7da"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-trash:before{content:"\f1f8"}.fa-trash-alt:before{content:"\f2ed"}.fa-trash-restore:before{content:"\f829"}.fa-trash-restore-alt:before{content:"\f82a"}.fa-tree:before{content:"\f1bb"}.fa-trello:before{content:"\f181"}.fa-tripadvisor:before{content:"\f262"}.fa-trophy:before{content:"\f091"}.fa-truck:before{content:"\f0d1"}.fa-truck-loading:before{content:"\f4de"}.fa-truck-monster:before{content:"\f63b"}.fa-truck-moving:before{content:"\f4df"}.fa-truck-pickup:before{content:"\f63c"}.fa-tshirt:before{content:"\f553"}.fa-tty:before{content:"\f1e4"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-tv:before{content:"\f26c"}.fa-twitch:before{content:"\f1e8"}.fa-twitter:before{content:"\f099"}.fa-twitter-square:before{content:"\f081"}.fa-typo3:before{content:"\f42b"}.fa-uber:before{content:"\f402"}.fa-ubuntu:before{content:"\f7df"}.fa-uikit:before{content:"\f403"}.fa-umbraco:before{content:"\f8e8"}.fa-umbrella:before{content:"\f0e9"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-underline:before{content:"\f0cd"}.fa-undo:before{content:"\f0e2"}.fa-undo-alt:before{content:"\f2ea"}.fa-uniregistry:before{content:"\f404"}.fa-unity:before{content:"\f949"}.fa-universal-access:before{content:"\f29a"}.fa-university:before{content:"\f19c"}.fa-unlink:before{content:"\f127"}.fa-unlock:before{content:"\f09c"}.fa-unlock-alt:before{content:"\f13e"}.fa-untappd:before{content:"\f405"}.fa-upload:before{content:"\f093"}.fa-ups:before{content:"\f7e0"}.fa-usb:before{content:"\f287"}.fa-user:before{content:"\f007"}.fa-user-alt:before{content:"\f406"}.fa-user-alt-slash:before{content:"\f4fa"}.fa-user-astronaut:before{content:"\f4fb"}.fa-user-check:before{content:"\f4fc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-clock:before{content:"\f4fd"}.fa-user-cog:before{content:"\f4fe"}.fa-user-edit:before{content:"\f4ff"}.fa-user-friends:before{content:"\f500"}.fa-user-graduate:before{content:"\f501"}.fa-user-injured:before{content:"\f728"}.fa-user-lock:before{content:"\f502"}.fa-user-md:before{content:"\f0f0"}.fa-user-minus:before{content:"\f503"}.fa-user-ninja:before{content:"\f504"}.fa-user-nurse:before{content:"\f82f"}.fa-user-plus:before{content:"\f234"}.fa-user-secret:before{content:"\f21b"}.fa-user-shield:before{content:"\f505"}.fa-user-slash:before{content:"\f506"}.fa-user-tag:before{content:"\f507"}.fa-user-tie:before{content:"\f508"}.fa-user-times:before{content:"\f235"}.fa-users:before{content:"\f0c0"}.fa-users-cog:before{content:"\f509"}.fa-usps:before{content:"\f7e1"}.fa-ussunnah:before{content:"\f407"}.fa-utensil-spoon:before{content:"\f2e5"}.fa-utensils:before{content:"\f2e7"}.fa-vaadin:before{content:"\f408"}.fa-vector-square:before{content:"\f5cb"}.fa-venus:before{content:"\f221"}.fa-venus-double:before{content:"\f226"}.fa-venus-mars:before{content:"\f228"}.fa-viacoin:before{content:"\f237"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-vial:before{content:"\f492"}.fa-vials:before{content:"\f493"}.fa-viber:before{content:"\f409"}.fa-video:before{content:"\f03d"}.fa-video-slash:before{content:"\f4e2"}.fa-vihara:before{content:"\f6a7"}.fa-vimeo:before{content:"\f40a"}.fa-vimeo-square:before{content:"\f194"}.fa-vimeo-v:before{content:"\f27d"}.fa-vine:before{content:"\f1ca"}.fa-virus:before{content:"\f974"}.fa-virus-slash:before{content:"\f975"}.fa-viruses:before{content:"\f976"}.fa-vk:before{content:"\f189"}.fa-vnv:before{content:"\f40b"}.fa-voicemail:before{content:"\f897"}.fa-volleyball-ball:before{content:"\f45f"}.fa-volume-down:before{content:"\f027"}.fa-volume-mute:before{content:"\f6a9"}.fa-volume-off:before{content:"\f026"}.fa-volume-up:before{content:"\f028"}.fa-vote-yea:before{content:"\f772"}.fa-vr-cardboard:before{content:"\f729"}.fa-vuejs:before{content:"\f41f"}.fa-walking:before{content:"\f554"}.fa-wallet:before{content:"\f555"}.fa-warehouse:before{content:"\f494"}.fa-water:before{content:"\f773"}.fa-wave-square:before{content:"\f83e"}.fa-waze:before{content:"\f83f"}.fa-weebly:before{content:"\f5cc"}.fa-weibo:before{content:"\f18a"}.fa-weight:before{content:"\f496"}.fa-weight-hanging:before{content:"\f5cd"}.fa-weixin:before{content:"\f1d7"}.fa-whatsapp:before{content:"\f232"}.fa-whatsapp-square:before{content:"\f40c"}.fa-wheelchair:before{content:"\f193"}.fa-whmcs:before{content:"\f40d"}.fa-wifi:before{content:"\f1eb"}.fa-wikipedia-w:before{content:"\f266"}.fa-wind:before{content:"\f72e"}.fa-window-close:before{content:"\f410"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-windows:before{content:"\f17a"}.fa-wine-bottle:before{content:"\f72f"}.fa-wine-glass:before{content:"\f4e3"}.fa-wine-glass-alt:before{content:"\f5ce"}.fa-wix:before{content:"\f5cf"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-won-sign:before{content:"\f159"}.fa-wordpress:before{content:"\f19a"}.fa-wordpress-simple:before{content:"\f411"}.fa-wpbeginner:before{content:"\f297"}.fa-wpexplorer:before{content:"\f2de"}.fa-wpforms:before{content:"\f298"}.fa-wpressr:before{content:"\f3e4"}.fa-wrench:before{content:"\f0ad"}.fa-x-ray:before{content:"\f497"}.fa-xbox:before{content:"\f412"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-y-combinator:before{content:"\f23b"}.fa-yahoo:before{content:"\f19e"}.fa-yammer:before{content:"\f840"}.fa-yandex:before{content:"\f413"}.fa-yandex-international:before{content:"\f414"}.fa-yarn:before{content:"\f7e3"}.fa-yelp:before{content:"\f1e9"}.fa-yen-sign:before{content:"\f157"}.fa-yin-yang:before{content:"\f6ad"}.fa-yoast:before{content:"\f2b1"}.fa-youtube:before{content:"\f167"}.fa-youtube-square:before{content:"\f431"}.fa-zhihu:before{content:"\f63f"}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto} \ No newline at end of file diff --git a/www/css/fontawesome.css b/www/css/fontawesome.css new file mode 100644 index 0000000..c73d7c0 --- /dev/null +++ b/www/css/fontawesome.css @@ -0,0 +1,4522 @@ +/*! + * Font Awesome Free 5.13.0 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +.fa, +.fas, +.far, +.fal, +.fad, +.fab { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + display: inline-block; + font-style: normal; + font-variant: normal; + text-rendering: auto; + line-height: 1; } + +.fa-lg { + font-size: 1.33333em; + line-height: 0.75em; + vertical-align: -.0667em; } + +.fa-xs { + font-size: .75em; } + +.fa-sm { + font-size: .875em; } + +.fa-1x { + font-size: 1em; } + +.fa-2x { + font-size: 2em; } + +.fa-3x { + font-size: 3em; } + +.fa-4x { + font-size: 4em; } + +.fa-5x { + font-size: 5em; } + +.fa-6x { + font-size: 6em; } + +.fa-7x { + font-size: 7em; } + +.fa-8x { + font-size: 8em; } + +.fa-9x { + font-size: 9em; } + +.fa-10x { + font-size: 10em; } + +.fa-fw { + text-align: center; + width: 1.25em; } + +.fa-ul { + list-style-type: none; + margin-left: 2.5em; + padding-left: 0; } + .fa-ul > li { + position: relative; } + +.fa-li { + left: -2em; + position: absolute; + text-align: center; + width: 2em; + line-height: inherit; } + +.fa-border { + border: solid 0.08em #eee; + border-radius: .1em; + padding: .2em .25em .15em; } + +.fa-pull-left { + float: left; } + +.fa-pull-right { + float: right; } + +.fa.fa-pull-left, +.fas.fa-pull-left, +.far.fa-pull-left, +.fal.fa-pull-left, +.fab.fa-pull-left { + margin-right: .3em; } + +.fa.fa-pull-right, +.fas.fa-pull-right, +.far.fa-pull-right, +.fal.fa-pull-right, +.fab.fa-pull-right { + margin-left: .3em; } + +.fa-spin { + -webkit-animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear; } + +.fa-pulse { + -webkit-animation: fa-spin 1s infinite steps(8); + animation: fa-spin 1s infinite steps(8); } + +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); } } + +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); } } + +.fa-rotate-90 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)"; + -webkit-transform: rotate(90deg); + transform: rotate(90deg); } + +.fa-rotate-180 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)"; + -webkit-transform: rotate(180deg); + transform: rotate(180deg); } + +.fa-rotate-270 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)"; + -webkit-transform: rotate(270deg); + transform: rotate(270deg); } + +.fa-flip-horizontal { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)"; + -webkit-transform: scale(-1, 1); + transform: scale(-1, 1); } + +.fa-flip-vertical { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; + -webkit-transform: scale(1, -1); + transform: scale(1, -1); } + +.fa-flip-both, .fa-flip-horizontal.fa-flip-vertical { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; + -webkit-transform: scale(-1, -1); + transform: scale(-1, -1); } + +:root .fa-rotate-90, +:root .fa-rotate-180, +:root .fa-rotate-270, +:root .fa-flip-horizontal, +:root .fa-flip-vertical, +:root .fa-flip-both { + -webkit-filter: none; + filter: none; } + +.fa-stack { + display: inline-block; + height: 2em; + line-height: 2em; + position: relative; + vertical-align: middle; + width: 2.5em; } + +.fa-stack-1x, +.fa-stack-2x { + left: 0; + position: absolute; + text-align: center; + width: 100%; } + +.fa-stack-1x { + line-height: inherit; } + +.fa-stack-2x { + font-size: 2em; } + +.fa-inverse { + color: #fff; } + +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen +readers do not read off random characters that represent icons */ +.fa-500px:before { + content: "\f26e"; } + +.fa-accessible-icon:before { + content: "\f368"; } + +.fa-accusoft:before { + content: "\f369"; } + +.fa-acquisitions-incorporated:before { + content: "\f6af"; } + +.fa-ad:before { + content: "\f641"; } + +.fa-address-book:before { + content: "\f2b9"; } + +.fa-address-card:before { + content: "\f2bb"; } + +.fa-adjust:before { + content: "\f042"; } + +.fa-adn:before { + content: "\f170"; } + +.fa-adobe:before { + content: "\f778"; } + +.fa-adversal:before { + content: "\f36a"; } + +.fa-affiliatetheme:before { + content: "\f36b"; } + +.fa-air-freshener:before { + content: "\f5d0"; } + +.fa-airbnb:before { + content: "\f834"; } + +.fa-algolia:before { + content: "\f36c"; } + +.fa-align-center:before { + content: "\f037"; } + +.fa-align-justify:before { + content: "\f039"; } + +.fa-align-left:before { + content: "\f036"; } + +.fa-align-right:before { + content: "\f038"; } + +.fa-alipay:before { + content: "\f642"; } + +.fa-allergies:before { + content: "\f461"; } + +.fa-amazon:before { + content: "\f270"; } + +.fa-amazon-pay:before { + content: "\f42c"; } + +.fa-ambulance:before { + content: "\f0f9"; } + +.fa-american-sign-language-interpreting:before { + content: "\f2a3"; } + +.fa-amilia:before { + content: "\f36d"; } + +.fa-anchor:before { + content: "\f13d"; } + +.fa-android:before { + content: "\f17b"; } + +.fa-angellist:before { + content: "\f209"; } + +.fa-angle-double-down:before { + content: "\f103"; } + +.fa-angle-double-left:before { + content: "\f100"; } + +.fa-angle-double-right:before { + content: "\f101"; } + +.fa-angle-double-up:before { + content: "\f102"; } + +.fa-angle-down:before { + content: "\f107"; } + +.fa-angle-left:before { + content: "\f104"; } + +.fa-angle-right:before { + content: "\f105"; } + +.fa-angle-up:before { + content: "\f106"; } + +.fa-angry:before { + content: "\f556"; } + +.fa-angrycreative:before { + content: "\f36e"; } + +.fa-angular:before { + content: "\f420"; } + +.fa-ankh:before { + content: "\f644"; } + +.fa-app-store:before { + content: "\f36f"; } + +.fa-app-store-ios:before { + content: "\f370"; } + +.fa-apper:before { + content: "\f371"; } + +.fa-apple:before { + content: "\f179"; } + +.fa-apple-alt:before { + content: "\f5d1"; } + +.fa-apple-pay:before { + content: "\f415"; } + +.fa-archive:before { + content: "\f187"; } + +.fa-archway:before { + content: "\f557"; } + +.fa-arrow-alt-circle-down:before { + content: "\f358"; } + +.fa-arrow-alt-circle-left:before { + content: "\f359"; } + +.fa-arrow-alt-circle-right:before { + content: "\f35a"; } + +.fa-arrow-alt-circle-up:before { + content: "\f35b"; } + +.fa-arrow-circle-down:before { + content: "\f0ab"; } + +.fa-arrow-circle-left:before { + content: "\f0a8"; } + +.fa-arrow-circle-right:before { + content: "\f0a9"; } + +.fa-arrow-circle-up:before { + content: "\f0aa"; } + +.fa-arrow-down:before { + content: "\f063"; } + +.fa-arrow-left:before { + content: "\f060"; } + +.fa-arrow-right:before { + content: "\f061"; } + +.fa-arrow-up:before { + content: "\f062"; } + +.fa-arrows-alt:before { + content: "\f0b2"; } + +.fa-arrows-alt-h:before { + content: "\f337"; } + +.fa-arrows-alt-v:before { + content: "\f338"; } + +.fa-artstation:before { + content: "\f77a"; } + +.fa-assistive-listening-systems:before { + content: "\f2a2"; } + +.fa-asterisk:before { + content: "\f069"; } + +.fa-asymmetrik:before { + content: "\f372"; } + +.fa-at:before { + content: "\f1fa"; } + +.fa-atlas:before { + content: "\f558"; } + +.fa-atlassian:before { + content: "\f77b"; } + +.fa-atom:before { + content: "\f5d2"; } + +.fa-audible:before { + content: "\f373"; } + +.fa-audio-description:before { + content: "\f29e"; } + +.fa-autoprefixer:before { + content: "\f41c"; } + +.fa-avianex:before { + content: "\f374"; } + +.fa-aviato:before { + content: "\f421"; } + +.fa-award:before { + content: "\f559"; } + +.fa-aws:before { + content: "\f375"; } + +.fa-baby:before { + content: "\f77c"; } + +.fa-baby-carriage:before { + content: "\f77d"; } + +.fa-backspace:before { + content: "\f55a"; } + +.fa-backward:before { + content: "\f04a"; } + +.fa-bacon:before { + content: "\f7e5"; } + +.fa-bahai:before { + content: "\f666"; } + +.fa-balance-scale:before { + content: "\f24e"; } + +.fa-balance-scale-left:before { + content: "\f515"; } + +.fa-balance-scale-right:before { + content: "\f516"; } + +.fa-ban:before { + content: "\f05e"; } + +.fa-band-aid:before { + content: "\f462"; } + +.fa-bandcamp:before { + content: "\f2d5"; } + +.fa-barcode:before { + content: "\f02a"; } + +.fa-bars:before { + content: "\f0c9"; } + +.fa-baseball-ball:before { + content: "\f433"; } + +.fa-basketball-ball:before { + content: "\f434"; } + +.fa-bath:before { + content: "\f2cd"; } + +.fa-battery-empty:before { + content: "\f244"; } + +.fa-battery-full:before { + content: "\f240"; } + +.fa-battery-half:before { + content: "\f242"; } + +.fa-battery-quarter:before { + content: "\f243"; } + +.fa-battery-three-quarters:before { + content: "\f241"; } + +.fa-battle-net:before { + content: "\f835"; } + +.fa-bed:before { + content: "\f236"; } + +.fa-beer:before { + content: "\f0fc"; } + +.fa-behance:before { + content: "\f1b4"; } + +.fa-behance-square:before { + content: "\f1b5"; } + +.fa-bell:before { + content: "\f0f3"; } + +.fa-bell-slash:before { + content: "\f1f6"; } + +.fa-bezier-curve:before { + content: "\f55b"; } + +.fa-bible:before { + content: "\f647"; } + +.fa-bicycle:before { + content: "\f206"; } + +.fa-biking:before { + content: "\f84a"; } + +.fa-bimobject:before { + content: "\f378"; } + +.fa-binoculars:before { + content: "\f1e5"; } + +.fa-biohazard:before { + content: "\f780"; } + +.fa-birthday-cake:before { + content: "\f1fd"; } + +.fa-bitbucket:before { + content: "\f171"; } + +.fa-bitcoin:before { + content: "\f379"; } + +.fa-bity:before { + content: "\f37a"; } + +.fa-black-tie:before { + content: "\f27e"; } + +.fa-blackberry:before { + content: "\f37b"; } + +.fa-blender:before { + content: "\f517"; } + +.fa-blender-phone:before { + content: "\f6b6"; } + +.fa-blind:before { + content: "\f29d"; } + +.fa-blog:before { + content: "\f781"; } + +.fa-blogger:before { + content: "\f37c"; } + +.fa-blogger-b:before { + content: "\f37d"; } + +.fa-bluetooth:before { + content: "\f293"; } + +.fa-bluetooth-b:before { + content: "\f294"; } + +.fa-bold:before { + content: "\f032"; } + +.fa-bolt:before { + content: "\f0e7"; } + +.fa-bomb:before { + content: "\f1e2"; } + +.fa-bone:before { + content: "\f5d7"; } + +.fa-bong:before { + content: "\f55c"; } + +.fa-book:before { + content: "\f02d"; } + +.fa-book-dead:before { + content: "\f6b7"; } + +.fa-book-medical:before { + content: "\f7e6"; } + +.fa-book-open:before { + content: "\f518"; } + +.fa-book-reader:before { + content: "\f5da"; } + +.fa-bookmark:before { + content: "\f02e"; } + +.fa-bootstrap:before { + content: "\f836"; } + +.fa-border-all:before { + content: "\f84c"; } + +.fa-border-none:before { + content: "\f850"; } + +.fa-border-style:before { + content: "\f853"; } + +.fa-bowling-ball:before { + content: "\f436"; } + +.fa-box:before { + content: "\f466"; } + +.fa-box-open:before { + content: "\f49e"; } + +.fa-box-tissue:before { + content: "\f95b"; } + +.fa-boxes:before { + content: "\f468"; } + +.fa-braille:before { + content: "\f2a1"; } + +.fa-brain:before { + content: "\f5dc"; } + +.fa-bread-slice:before { + content: "\f7ec"; } + +.fa-briefcase:before { + content: "\f0b1"; } + +.fa-briefcase-medical:before { + content: "\f469"; } + +.fa-broadcast-tower:before { + content: "\f519"; } + +.fa-broom:before { + content: "\f51a"; } + +.fa-brush:before { + content: "\f55d"; } + +.fa-btc:before { + content: "\f15a"; } + +.fa-buffer:before { + content: "\f837"; } + +.fa-bug:before { + content: "\f188"; } + +.fa-building:before { + content: "\f1ad"; } + +.fa-bullhorn:before { + content: "\f0a1"; } + +.fa-bullseye:before { + content: "\f140"; } + +.fa-burn:before { + content: "\f46a"; } + +.fa-buromobelexperte:before { + content: "\f37f"; } + +.fa-bus:before { + content: "\f207"; } + +.fa-bus-alt:before { + content: "\f55e"; } + +.fa-business-time:before { + content: "\f64a"; } + +.fa-buy-n-large:before { + content: "\f8a6"; } + +.fa-buysellads:before { + content: "\f20d"; } + +.fa-calculator:before { + content: "\f1ec"; } + +.fa-calendar:before { + content: "\f133"; } + +.fa-calendar-alt:before { + content: "\f073"; } + +.fa-calendar-check:before { + content: "\f274"; } + +.fa-calendar-day:before { + content: "\f783"; } + +.fa-calendar-minus:before { + content: "\f272"; } + +.fa-calendar-plus:before { + content: "\f271"; } + +.fa-calendar-times:before { + content: "\f273"; } + +.fa-calendar-week:before { + content: "\f784"; } + +.fa-camera:before { + content: "\f030"; } + +.fa-camera-retro:before { + content: "\f083"; } + +.fa-campground:before { + content: "\f6bb"; } + +.fa-canadian-maple-leaf:before { + content: "\f785"; } + +.fa-candy-cane:before { + content: "\f786"; } + +.fa-cannabis:before { + content: "\f55f"; } + +.fa-capsules:before { + content: "\f46b"; } + +.fa-car:before { + content: "\f1b9"; } + +.fa-car-alt:before { + content: "\f5de"; } + +.fa-car-battery:before { + content: "\f5df"; } + +.fa-car-crash:before { + content: "\f5e1"; } + +.fa-car-side:before { + content: "\f5e4"; } + +.fa-caravan:before { + content: "\f8ff"; } + +.fa-caret-down:before { + content: "\f0d7"; } + +.fa-caret-left:before { + content: "\f0d9"; } + +.fa-caret-right:before { + content: "\f0da"; } + +.fa-caret-square-down:before { + content: "\f150"; } + +.fa-caret-square-left:before { + content: "\f191"; } + +.fa-caret-square-right:before { + content: "\f152"; } + +.fa-caret-square-up:before { + content: "\f151"; } + +.fa-caret-up:before { + content: "\f0d8"; } + +.fa-carrot:before { + content: "\f787"; } + +.fa-cart-arrow-down:before { + content: "\f218"; } + +.fa-cart-plus:before { + content: "\f217"; } + +.fa-cash-register:before { + content: "\f788"; } + +.fa-cat:before { + content: "\f6be"; } + +.fa-cc-amazon-pay:before { + content: "\f42d"; } + +.fa-cc-amex:before { + content: "\f1f3"; } + +.fa-cc-apple-pay:before { + content: "\f416"; } + +.fa-cc-diners-club:before { + content: "\f24c"; } + +.fa-cc-discover:before { + content: "\f1f2"; } + +.fa-cc-jcb:before { + content: "\f24b"; } + +.fa-cc-mastercard:before { + content: "\f1f1"; } + +.fa-cc-paypal:before { + content: "\f1f4"; } + +.fa-cc-stripe:before { + content: "\f1f5"; } + +.fa-cc-visa:before { + content: "\f1f0"; } + +.fa-centercode:before { + content: "\f380"; } + +.fa-centos:before { + content: "\f789"; } + +.fa-certificate:before { + content: "\f0a3"; } + +.fa-chair:before { + content: "\f6c0"; } + +.fa-chalkboard:before { + content: "\f51b"; } + +.fa-chalkboard-teacher:before { + content: "\f51c"; } + +.fa-charging-station:before { + content: "\f5e7"; } + +.fa-chart-area:before { + content: "\f1fe"; } + +.fa-chart-bar:before { + content: "\f080"; } + +.fa-chart-line:before { + content: "\f201"; } + +.fa-chart-pie:before { + content: "\f200"; } + +.fa-check:before { + content: "\f00c"; } + +.fa-check-circle:before { + content: "\f058"; } + +.fa-check-double:before { + content: "\f560"; } + +.fa-check-square:before { + content: "\f14a"; } + +.fa-cheese:before { + content: "\f7ef"; } + +.fa-chess:before { + content: "\f439"; } + +.fa-chess-bishop:before { + content: "\f43a"; } + +.fa-chess-board:before { + content: "\f43c"; } + +.fa-chess-king:before { + content: "\f43f"; } + +.fa-chess-knight:before { + content: "\f441"; } + +.fa-chess-pawn:before { + content: "\f443"; } + +.fa-chess-queen:before { + content: "\f445"; } + +.fa-chess-rook:before { + content: "\f447"; } + +.fa-chevron-circle-down:before { + content: "\f13a"; } + +.fa-chevron-circle-left:before { + content: "\f137"; } + +.fa-chevron-circle-right:before { + content: "\f138"; } + +.fa-chevron-circle-up:before { + content: "\f139"; } + +.fa-chevron-down:before { + content: "\f078"; } + +.fa-chevron-left:before { + content: "\f053"; } + +.fa-chevron-right:before { + content: "\f054"; } + +.fa-chevron-up:before { + content: "\f077"; } + +.fa-child:before { + content: "\f1ae"; } + +.fa-chrome:before { + content: "\f268"; } + +.fa-chromecast:before { + content: "\f838"; } + +.fa-church:before { + content: "\f51d"; } + +.fa-circle:before { + content: "\f111"; } + +.fa-circle-notch:before { + content: "\f1ce"; } + +.fa-city:before { + content: "\f64f"; } + +.fa-clinic-medical:before { + content: "\f7f2"; } + +.fa-clipboard:before { + content: "\f328"; } + +.fa-clipboard-check:before { + content: "\f46c"; } + +.fa-clipboard-list:before { + content: "\f46d"; } + +.fa-clock:before { + content: "\f017"; } + +.fa-clone:before { + content: "\f24d"; } + +.fa-closed-captioning:before { + content: "\f20a"; } + +.fa-cloud:before { + content: "\f0c2"; } + +.fa-cloud-download-alt:before { + content: "\f381"; } + +.fa-cloud-meatball:before { + content: "\f73b"; } + +.fa-cloud-moon:before { + content: "\f6c3"; } + +.fa-cloud-moon-rain:before { + content: "\f73c"; } + +.fa-cloud-rain:before { + content: "\f73d"; } + +.fa-cloud-showers-heavy:before { + content: "\f740"; } + +.fa-cloud-sun:before { + content: "\f6c4"; } + +.fa-cloud-sun-rain:before { + content: "\f743"; } + +.fa-cloud-upload-alt:before { + content: "\f382"; } + +.fa-cloudscale:before { + content: "\f383"; } + +.fa-cloudsmith:before { + content: "\f384"; } + +.fa-cloudversify:before { + content: "\f385"; } + +.fa-cocktail:before { + content: "\f561"; } + +.fa-code:before { + content: "\f121"; } + +.fa-code-branch:before { + content: "\f126"; } + +.fa-codepen:before { + content: "\f1cb"; } + +.fa-codiepie:before { + content: "\f284"; } + +.fa-coffee:before { + content: "\f0f4"; } + +.fa-cog:before { + content: "\f013"; } + +.fa-cogs:before { + content: "\f085"; } + +.fa-coins:before { + content: "\f51e"; } + +.fa-columns:before { + content: "\f0db"; } + +.fa-comment:before { + content: "\f075"; } + +.fa-comment-alt:before { + content: "\f27a"; } + +.fa-comment-dollar:before { + content: "\f651"; } + +.fa-comment-dots:before { + content: "\f4ad"; } + +.fa-comment-medical:before { + content: "\f7f5"; } + +.fa-comment-slash:before { + content: "\f4b3"; } + +.fa-comments:before { + content: "\f086"; } + +.fa-comments-dollar:before { + content: "\f653"; } + +.fa-compact-disc:before { + content: "\f51f"; } + +.fa-compass:before { + content: "\f14e"; } + +.fa-compress:before { + content: "\f066"; } + +.fa-compress-alt:before { + content: "\f422"; } + +.fa-compress-arrows-alt:before { + content: "\f78c"; } + +.fa-concierge-bell:before { + content: "\f562"; } + +.fa-confluence:before { + content: "\f78d"; } + +.fa-connectdevelop:before { + content: "\f20e"; } + +.fa-contao:before { + content: "\f26d"; } + +.fa-cookie:before { + content: "\f563"; } + +.fa-cookie-bite:before { + content: "\f564"; } + +.fa-copy:before { + content: "\f0c5"; } + +.fa-copyright:before { + content: "\f1f9"; } + +.fa-cotton-bureau:before { + content: "\f89e"; } + +.fa-couch:before { + content: "\f4b8"; } + +.fa-cpanel:before { + content: "\f388"; } + +.fa-creative-commons:before { + content: "\f25e"; } + +.fa-creative-commons-by:before { + content: "\f4e7"; } + +.fa-creative-commons-nc:before { + content: "\f4e8"; } + +.fa-creative-commons-nc-eu:before { + content: "\f4e9"; } + +.fa-creative-commons-nc-jp:before { + content: "\f4ea"; } + +.fa-creative-commons-nd:before { + content: "\f4eb"; } + +.fa-creative-commons-pd:before { + content: "\f4ec"; } + +.fa-creative-commons-pd-alt:before { + content: "\f4ed"; } + +.fa-creative-commons-remix:before { + content: "\f4ee"; } + +.fa-creative-commons-sa:before { + content: "\f4ef"; } + +.fa-creative-commons-sampling:before { + content: "\f4f0"; } + +.fa-creative-commons-sampling-plus:before { + content: "\f4f1"; } + +.fa-creative-commons-share:before { + content: "\f4f2"; } + +.fa-creative-commons-zero:before { + content: "\f4f3"; } + +.fa-credit-card:before { + content: "\f09d"; } + +.fa-critical-role:before { + content: "\f6c9"; } + +.fa-crop:before { + content: "\f125"; } + +.fa-crop-alt:before { + content: "\f565"; } + +.fa-cross:before { + content: "\f654"; } + +.fa-crosshairs:before { + content: "\f05b"; } + +.fa-crow:before { + content: "\f520"; } + +.fa-crown:before { + content: "\f521"; } + +.fa-crutch:before { + content: "\f7f7"; } + +.fa-css3:before { + content: "\f13c"; } + +.fa-css3-alt:before { + content: "\f38b"; } + +.fa-cube:before { + content: "\f1b2"; } + +.fa-cubes:before { + content: "\f1b3"; } + +.fa-cut:before { + content: "\f0c4"; } + +.fa-cuttlefish:before { + content: "\f38c"; } + +.fa-d-and-d:before { + content: "\f38d"; } + +.fa-d-and-d-beyond:before { + content: "\f6ca"; } + +.fa-dailymotion:before { + content: "\f952"; } + +.fa-dashcube:before { + content: "\f210"; } + +.fa-database:before { + content: "\f1c0"; } + +.fa-deaf:before { + content: "\f2a4"; } + +.fa-delicious:before { + content: "\f1a5"; } + +.fa-democrat:before { + content: "\f747"; } + +.fa-deploydog:before { + content: "\f38e"; } + +.fa-deskpro:before { + content: "\f38f"; } + +.fa-desktop:before { + content: "\f108"; } + +.fa-dev:before { + content: "\f6cc"; } + +.fa-deviantart:before { + content: "\f1bd"; } + +.fa-dharmachakra:before { + content: "\f655"; } + +.fa-dhl:before { + content: "\f790"; } + +.fa-diagnoses:before { + content: "\f470"; } + +.fa-diaspora:before { + content: "\f791"; } + +.fa-dice:before { + content: "\f522"; } + +.fa-dice-d20:before { + content: "\f6cf"; } + +.fa-dice-d6:before { + content: "\f6d1"; } + +.fa-dice-five:before { + content: "\f523"; } + +.fa-dice-four:before { + content: "\f524"; } + +.fa-dice-one:before { + content: "\f525"; } + +.fa-dice-six:before { + content: "\f526"; } + +.fa-dice-three:before { + content: "\f527"; } + +.fa-dice-two:before { + content: "\f528"; } + +.fa-digg:before { + content: "\f1a6"; } + +.fa-digital-ocean:before { + content: "\f391"; } + +.fa-digital-tachograph:before { + content: "\f566"; } + +.fa-directions:before { + content: "\f5eb"; } + +.fa-discord:before { + content: "\f392"; } + +.fa-discourse:before { + content: "\f393"; } + +.fa-disease:before { + content: "\f7fa"; } + +.fa-divide:before { + content: "\f529"; } + +.fa-dizzy:before { + content: "\f567"; } + +.fa-dna:before { + content: "\f471"; } + +.fa-dochub:before { + content: "\f394"; } + +.fa-docker:before { + content: "\f395"; } + +.fa-dog:before { + content: "\f6d3"; } + +.fa-dollar-sign:before { + content: "\f155"; } + +.fa-dolly:before { + content: "\f472"; } + +.fa-dolly-flatbed:before { + content: "\f474"; } + +.fa-donate:before { + content: "\f4b9"; } + +.fa-door-closed:before { + content: "\f52a"; } + +.fa-door-open:before { + content: "\f52b"; } + +.fa-dot-circle:before { + content: "\f192"; } + +.fa-dove:before { + content: "\f4ba"; } + +.fa-download:before { + content: "\f019"; } + +.fa-draft2digital:before { + content: "\f396"; } + +.fa-drafting-compass:before { + content: "\f568"; } + +.fa-dragon:before { + content: "\f6d5"; } + +.fa-draw-polygon:before { + content: "\f5ee"; } + +.fa-dribbble:before { + content: "\f17d"; } + +.fa-dribbble-square:before { + content: "\f397"; } + +.fa-dropbox:before { + content: "\f16b"; } + +.fa-drum:before { + content: "\f569"; } + +.fa-drum-steelpan:before { + content: "\f56a"; } + +.fa-drumstick-bite:before { + content: "\f6d7"; } + +.fa-drupal:before { + content: "\f1a9"; } + +.fa-dumbbell:before { + content: "\f44b"; } + +.fa-dumpster:before { + content: "\f793"; } + +.fa-dumpster-fire:before { + content: "\f794"; } + +.fa-dungeon:before { + content: "\f6d9"; } + +.fa-dyalog:before { + content: "\f399"; } + +.fa-earlybirds:before { + content: "\f39a"; } + +.fa-ebay:before { + content: "\f4f4"; } + +.fa-edge:before { + content: "\f282"; } + +.fa-edit:before { + content: "\f044"; } + +.fa-egg:before { + content: "\f7fb"; } + +.fa-eject:before { + content: "\f052"; } + +.fa-elementor:before { + content: "\f430"; } + +.fa-ellipsis-h:before { + content: "\f141"; } + +.fa-ellipsis-v:before { + content: "\f142"; } + +.fa-ello:before { + content: "\f5f1"; } + +.fa-ember:before { + content: "\f423"; } + +.fa-empire:before { + content: "\f1d1"; } + +.fa-envelope:before { + content: "\f0e0"; } + +.fa-envelope-open:before { + content: "\f2b6"; } + +.fa-envelope-open-text:before { + content: "\f658"; } + +.fa-envelope-square:before { + content: "\f199"; } + +.fa-envira:before { + content: "\f299"; } + +.fa-equals:before { + content: "\f52c"; } + +.fa-eraser:before { + content: "\f12d"; } + +.fa-erlang:before { + content: "\f39d"; } + +.fa-ethereum:before { + content: "\f42e"; } + +.fa-ethernet:before { + content: "\f796"; } + +.fa-etsy:before { + content: "\f2d7"; } + +.fa-euro-sign:before { + content: "\f153"; } + +.fa-evernote:before { + content: "\f839"; } + +.fa-exchange-alt:before { + content: "\f362"; } + +.fa-exclamation:before { + content: "\f12a"; } + +.fa-exclamation-circle:before { + content: "\f06a"; } + +.fa-exclamation-triangle:before { + content: "\f071"; } + +.fa-expand:before { + content: "\f065"; } + +.fa-expand-alt:before { + content: "\f424"; } + +.fa-expand-arrows-alt:before { + content: "\f31e"; } + +.fa-expeditedssl:before { + content: "\f23e"; } + +.fa-external-link-alt:before { + content: "\f35d"; } + +.fa-external-link-square-alt:before { + content: "\f360"; } + +.fa-eye:before { + content: "\f06e"; } + +.fa-eye-dropper:before { + content: "\f1fb"; } + +.fa-eye-slash:before { + content: "\f070"; } + +.fa-facebook:before { + content: "\f09a"; } + +.fa-facebook-f:before { + content: "\f39e"; } + +.fa-facebook-messenger:before { + content: "\f39f"; } + +.fa-facebook-square:before { + content: "\f082"; } + +.fa-fan:before { + content: "\f863"; } + +.fa-fantasy-flight-games:before { + content: "\f6dc"; } + +.fa-fast-backward:before { + content: "\f049"; } + +.fa-fast-forward:before { + content: "\f050"; } + +.fa-faucet:before { + content: "\f905"; } + +.fa-fax:before { + content: "\f1ac"; } + +.fa-feather:before { + content: "\f52d"; } + +.fa-feather-alt:before { + content: "\f56b"; } + +.fa-fedex:before { + content: "\f797"; } + +.fa-fedora:before { + content: "\f798"; } + +.fa-female:before { + content: "\f182"; } + +.fa-fighter-jet:before { + content: "\f0fb"; } + +.fa-figma:before { + content: "\f799"; } + +.fa-file:before { + content: "\f15b"; } + +.fa-file-alt:before { + content: "\f15c"; } + +.fa-file-archive:before { + content: "\f1c6"; } + +.fa-file-audio:before { + content: "\f1c7"; } + +.fa-file-code:before { + content: "\f1c9"; } + +.fa-file-contract:before { + content: "\f56c"; } + +.fa-file-csv:before { + content: "\f6dd"; } + +.fa-file-download:before { + content: "\f56d"; } + +.fa-file-excel:before { + content: "\f1c3"; } + +.fa-file-export:before { + content: "\f56e"; } + +.fa-file-image:before { + content: "\f1c5"; } + +.fa-file-import:before { + content: "\f56f"; } + +.fa-file-invoice:before { + content: "\f570"; } + +.fa-file-invoice-dollar:before { + content: "\f571"; } + +.fa-file-medical:before { + content: "\f477"; } + +.fa-file-medical-alt:before { + content: "\f478"; } + +.fa-file-pdf:before { + content: "\f1c1"; } + +.fa-file-powerpoint:before { + content: "\f1c4"; } + +.fa-file-prescription:before { + content: "\f572"; } + +.fa-file-signature:before { + content: "\f573"; } + +.fa-file-upload:before { + content: "\f574"; } + +.fa-file-video:before { + content: "\f1c8"; } + +.fa-file-word:before { + content: "\f1c2"; } + +.fa-fill:before { + content: "\f575"; } + +.fa-fill-drip:before { + content: "\f576"; } + +.fa-film:before { + content: "\f008"; } + +.fa-filter:before { + content: "\f0b0"; } + +.fa-fingerprint:before { + content: "\f577"; } + +.fa-fire:before { + content: "\f06d"; } + +.fa-fire-alt:before { + content: "\f7e4"; } + +.fa-fire-extinguisher:before { + content: "\f134"; } + +.fa-firefox:before { + content: "\f269"; } + +.fa-firefox-browser:before { + content: "\f907"; } + +.fa-first-aid:before { + content: "\f479"; } + +.fa-first-order:before { + content: "\f2b0"; } + +.fa-first-order-alt:before { + content: "\f50a"; } + +.fa-firstdraft:before { + content: "\f3a1"; } + +.fa-fish:before { + content: "\f578"; } + +.fa-fist-raised:before { + content: "\f6de"; } + +.fa-flag:before { + content: "\f024"; } + +.fa-flag-checkered:before { + content: "\f11e"; } + +.fa-flag-usa:before { + content: "\f74d"; } + +.fa-flask:before { + content: "\f0c3"; } + +.fa-flickr:before { + content: "\f16e"; } + +.fa-flipboard:before { + content: "\f44d"; } + +.fa-flushed:before { + content: "\f579"; } + +.fa-fly:before { + content: "\f417"; } + +.fa-folder:before { + content: "\f07b"; } + +.fa-folder-minus:before { + content: "\f65d"; } + +.fa-folder-open:before { + content: "\f07c"; } + +.fa-folder-plus:before { + content: "\f65e"; } + +.fa-font:before { + content: "\f031"; } + +.fa-font-awesome:before { + content: "\f2b4"; } + +.fa-font-awesome-alt:before { + content: "\f35c"; } + +.fa-font-awesome-flag:before { + content: "\f425"; } + +.fa-font-awesome-logo-full:before { + content: "\f4e6"; } + +.fa-fonticons:before { + content: "\f280"; } + +.fa-fonticons-fi:before { + content: "\f3a2"; } + +.fa-football-ball:before { + content: "\f44e"; } + +.fa-fort-awesome:before { + content: "\f286"; } + +.fa-fort-awesome-alt:before { + content: "\f3a3"; } + +.fa-forumbee:before { + content: "\f211"; } + +.fa-forward:before { + content: "\f04e"; } + +.fa-foursquare:before { + content: "\f180"; } + +.fa-free-code-camp:before { + content: "\f2c5"; } + +.fa-freebsd:before { + content: "\f3a4"; } + +.fa-frog:before { + content: "\f52e"; } + +.fa-frown:before { + content: "\f119"; } + +.fa-frown-open:before { + content: "\f57a"; } + +.fa-fulcrum:before { + content: "\f50b"; } + +.fa-funnel-dollar:before { + content: "\f662"; } + +.fa-futbol:before { + content: "\f1e3"; } + +.fa-galactic-republic:before { + content: "\f50c"; } + +.fa-galactic-senate:before { + content: "\f50d"; } + +.fa-gamepad:before { + content: "\f11b"; } + +.fa-gas-pump:before { + content: "\f52f"; } + +.fa-gavel:before { + content: "\f0e3"; } + +.fa-gem:before { + content: "\f3a5"; } + +.fa-genderless:before { + content: "\f22d"; } + +.fa-get-pocket:before { + content: "\f265"; } + +.fa-gg:before { + content: "\f260"; } + +.fa-gg-circle:before { + content: "\f261"; } + +.fa-ghost:before { + content: "\f6e2"; } + +.fa-gift:before { + content: "\f06b"; } + +.fa-gifts:before { + content: "\f79c"; } + +.fa-git:before { + content: "\f1d3"; } + +.fa-git-alt:before { + content: "\f841"; } + +.fa-git-square:before { + content: "\f1d2"; } + +.fa-github:before { + content: "\f09b"; } + +.fa-github-alt:before { + content: "\f113"; } + +.fa-github-square:before { + content: "\f092"; } + +.fa-gitkraken:before { + content: "\f3a6"; } + +.fa-gitlab:before { + content: "\f296"; } + +.fa-gitter:before { + content: "\f426"; } + +.fa-glass-cheers:before { + content: "\f79f"; } + +.fa-glass-martini:before { + content: "\f000"; } + +.fa-glass-martini-alt:before { + content: "\f57b"; } + +.fa-glass-whiskey:before { + content: "\f7a0"; } + +.fa-glasses:before { + content: "\f530"; } + +.fa-glide:before { + content: "\f2a5"; } + +.fa-glide-g:before { + content: "\f2a6"; } + +.fa-globe:before { + content: "\f0ac"; } + +.fa-globe-africa:before { + content: "\f57c"; } + +.fa-globe-americas:before { + content: "\f57d"; } + +.fa-globe-asia:before { + content: "\f57e"; } + +.fa-globe-europe:before { + content: "\f7a2"; } + +.fa-gofore:before { + content: "\f3a7"; } + +.fa-golf-ball:before { + content: "\f450"; } + +.fa-goodreads:before { + content: "\f3a8"; } + +.fa-goodreads-g:before { + content: "\f3a9"; } + +.fa-google:before { + content: "\f1a0"; } + +.fa-google-drive:before { + content: "\f3aa"; } + +.fa-google-play:before { + content: "\f3ab"; } + +.fa-google-plus:before { + content: "\f2b3"; } + +.fa-google-plus-g:before { + content: "\f0d5"; } + +.fa-google-plus-square:before { + content: "\f0d4"; } + +.fa-google-wallet:before { + content: "\f1ee"; } + +.fa-gopuram:before { + content: "\f664"; } + +.fa-graduation-cap:before { + content: "\f19d"; } + +.fa-gratipay:before { + content: "\f184"; } + +.fa-grav:before { + content: "\f2d6"; } + +.fa-greater-than:before { + content: "\f531"; } + +.fa-greater-than-equal:before { + content: "\f532"; } + +.fa-grimace:before { + content: "\f57f"; } + +.fa-grin:before { + content: "\f580"; } + +.fa-grin-alt:before { + content: "\f581"; } + +.fa-grin-beam:before { + content: "\f582"; } + +.fa-grin-beam-sweat:before { + content: "\f583"; } + +.fa-grin-hearts:before { + content: "\f584"; } + +.fa-grin-squint:before { + content: "\f585"; } + +.fa-grin-squint-tears:before { + content: "\f586"; } + +.fa-grin-stars:before { + content: "\f587"; } + +.fa-grin-tears:before { + content: "\f588"; } + +.fa-grin-tongue:before { + content: "\f589"; } + +.fa-grin-tongue-squint:before { + content: "\f58a"; } + +.fa-grin-tongue-wink:before { + content: "\f58b"; } + +.fa-grin-wink:before { + content: "\f58c"; } + +.fa-grip-horizontal:before { + content: "\f58d"; } + +.fa-grip-lines:before { + content: "\f7a4"; } + +.fa-grip-lines-vertical:before { + content: "\f7a5"; } + +.fa-grip-vertical:before { + content: "\f58e"; } + +.fa-gripfire:before { + content: "\f3ac"; } + +.fa-grunt:before { + content: "\f3ad"; } + +.fa-guitar:before { + content: "\f7a6"; } + +.fa-gulp:before { + content: "\f3ae"; } + +.fa-h-square:before { + content: "\f0fd"; } + +.fa-hacker-news:before { + content: "\f1d4"; } + +.fa-hacker-news-square:before { + content: "\f3af"; } + +.fa-hackerrank:before { + content: "\f5f7"; } + +.fa-hamburger:before { + content: "\f805"; } + +.fa-hammer:before { + content: "\f6e3"; } + +.fa-hamsa:before { + content: "\f665"; } + +.fa-hand-holding:before { + content: "\f4bd"; } + +.fa-hand-holding-heart:before { + content: "\f4be"; } + +.fa-hand-holding-medical:before { + content: "\f95c"; } + +.fa-hand-holding-usd:before { + content: "\f4c0"; } + +.fa-hand-holding-water:before { + content: "\f4c1"; } + +.fa-hand-lizard:before { + content: "\f258"; } + +.fa-hand-middle-finger:before { + content: "\f806"; } + +.fa-hand-paper:before { + content: "\f256"; } + +.fa-hand-peace:before { + content: "\f25b"; } + +.fa-hand-point-down:before { + content: "\f0a7"; } + +.fa-hand-point-left:before { + content: "\f0a5"; } + +.fa-hand-point-right:before { + content: "\f0a4"; } + +.fa-hand-point-up:before { + content: "\f0a6"; } + +.fa-hand-pointer:before { + content: "\f25a"; } + +.fa-hand-rock:before { + content: "\f255"; } + +.fa-hand-scissors:before { + content: "\f257"; } + +.fa-hand-sparkles:before { + content: "\f95d"; } + +.fa-hand-spock:before { + content: "\f259"; } + +.fa-hands:before { + content: "\f4c2"; } + +.fa-hands-helping:before { + content: "\f4c4"; } + +.fa-hands-wash:before { + content: "\f95e"; } + +.fa-handshake:before { + content: "\f2b5"; } + +.fa-handshake-alt-slash:before { + content: "\f95f"; } + +.fa-handshake-slash:before { + content: "\f960"; } + +.fa-hanukiah:before { + content: "\f6e6"; } + +.fa-hard-hat:before { + content: "\f807"; } + +.fa-hashtag:before { + content: "\f292"; } + +.fa-hat-cowboy:before { + content: "\f8c0"; } + +.fa-hat-cowboy-side:before { + content: "\f8c1"; } + +.fa-hat-wizard:before { + content: "\f6e8"; } + +.fa-hdd:before { + content: "\f0a0"; } + +.fa-head-side-cough:before { + content: "\f961"; } + +.fa-head-side-cough-slash:before { + content: "\f962"; } + +.fa-head-side-mask:before { + content: "\f963"; } + +.fa-head-side-virus:before { + content: "\f964"; } + +.fa-heading:before { + content: "\f1dc"; } + +.fa-headphones:before { + content: "\f025"; } + +.fa-headphones-alt:before { + content: "\f58f"; } + +.fa-headset:before { + content: "\f590"; } + +.fa-heart:before { + content: "\f004"; } + +.fa-heart-broken:before { + content: "\f7a9"; } + +.fa-heartbeat:before { + content: "\f21e"; } + +.fa-helicopter:before { + content: "\f533"; } + +.fa-highlighter:before { + content: "\f591"; } + +.fa-hiking:before { + content: "\f6ec"; } + +.fa-hippo:before { + content: "\f6ed"; } + +.fa-hips:before { + content: "\f452"; } + +.fa-hire-a-helper:before { + content: "\f3b0"; } + +.fa-history:before { + content: "\f1da"; } + +.fa-hockey-puck:before { + content: "\f453"; } + +.fa-holly-berry:before { + content: "\f7aa"; } + +.fa-home:before { + content: "\f015"; } + +.fa-hooli:before { + content: "\f427"; } + +.fa-hornbill:before { + content: "\f592"; } + +.fa-horse:before { + content: "\f6f0"; } + +.fa-horse-head:before { + content: "\f7ab"; } + +.fa-hospital:before { + content: "\f0f8"; } + +.fa-hospital-alt:before { + content: "\f47d"; } + +.fa-hospital-symbol:before { + content: "\f47e"; } + +.fa-hospital-user:before { + content: "\f80d"; } + +.fa-hot-tub:before { + content: "\f593"; } + +.fa-hotdog:before { + content: "\f80f"; } + +.fa-hotel:before { + content: "\f594"; } + +.fa-hotjar:before { + content: "\f3b1"; } + +.fa-hourglass:before { + content: "\f254"; } + +.fa-hourglass-end:before { + content: "\f253"; } + +.fa-hourglass-half:before { + content: "\f252"; } + +.fa-hourglass-start:before { + content: "\f251"; } + +.fa-house-damage:before { + content: "\f6f1"; } + +.fa-house-user:before { + content: "\f965"; } + +.fa-houzz:before { + content: "\f27c"; } + +.fa-hryvnia:before { + content: "\f6f2"; } + +.fa-html5:before { + content: "\f13b"; } + +.fa-hubspot:before { + content: "\f3b2"; } + +.fa-i-cursor:before { + content: "\f246"; } + +.fa-ice-cream:before { + content: "\f810"; } + +.fa-icicles:before { + content: "\f7ad"; } + +.fa-icons:before { + content: "\f86d"; } + +.fa-id-badge:before { + content: "\f2c1"; } + +.fa-id-card:before { + content: "\f2c2"; } + +.fa-id-card-alt:before { + content: "\f47f"; } + +.fa-ideal:before { + content: "\f913"; } + +.fa-igloo:before { + content: "\f7ae"; } + +.fa-image:before { + content: "\f03e"; } + +.fa-images:before { + content: "\f302"; } + +.fa-imdb:before { + content: "\f2d8"; } + +.fa-inbox:before { + content: "\f01c"; } + +.fa-indent:before { + content: "\f03c"; } + +.fa-industry:before { + content: "\f275"; } + +.fa-infinity:before { + content: "\f534"; } + +.fa-info:before { + content: "\f129"; } + +.fa-info-circle:before { + content: "\f05a"; } + +.fa-instagram:before { + content: "\f16d"; } + +.fa-instagram-square:before { + content: "\f955"; } + +.fa-intercom:before { + content: "\f7af"; } + +.fa-internet-explorer:before { + content: "\f26b"; } + +.fa-invision:before { + content: "\f7b0"; } + +.fa-ioxhost:before { + content: "\f208"; } + +.fa-italic:before { + content: "\f033"; } + +.fa-itch-io:before { + content: "\f83a"; } + +.fa-itunes:before { + content: "\f3b4"; } + +.fa-itunes-note:before { + content: "\f3b5"; } + +.fa-java:before { + content: "\f4e4"; } + +.fa-jedi:before { + content: "\f669"; } + +.fa-jedi-order:before { + content: "\f50e"; } + +.fa-jenkins:before { + content: "\f3b6"; } + +.fa-jira:before { + content: "\f7b1"; } + +.fa-joget:before { + content: "\f3b7"; } + +.fa-joint:before { + content: "\f595"; } + +.fa-joomla:before { + content: "\f1aa"; } + +.fa-journal-whills:before { + content: "\f66a"; } + +.fa-js:before { + content: "\f3b8"; } + +.fa-js-square:before { + content: "\f3b9"; } + +.fa-jsfiddle:before { + content: "\f1cc"; } + +.fa-kaaba:before { + content: "\f66b"; } + +.fa-kaggle:before { + content: "\f5fa"; } + +.fa-key:before { + content: "\f084"; } + +.fa-keybase:before { + content: "\f4f5"; } + +.fa-keyboard:before { + content: "\f11c"; } + +.fa-keycdn:before { + content: "\f3ba"; } + +.fa-khanda:before { + content: "\f66d"; } + +.fa-kickstarter:before { + content: "\f3bb"; } + +.fa-kickstarter-k:before { + content: "\f3bc"; } + +.fa-kiss:before { + content: "\f596"; } + +.fa-kiss-beam:before { + content: "\f597"; } + +.fa-kiss-wink-heart:before { + content: "\f598"; } + +.fa-kiwi-bird:before { + content: "\f535"; } + +.fa-korvue:before { + content: "\f42f"; } + +.fa-landmark:before { + content: "\f66f"; } + +.fa-language:before { + content: "\f1ab"; } + +.fa-laptop:before { + content: "\f109"; } + +.fa-laptop-code:before { + content: "\f5fc"; } + +.fa-laptop-house:before { + content: "\f966"; } + +.fa-laptop-medical:before { + content: "\f812"; } + +.fa-laravel:before { + content: "\f3bd"; } + +.fa-lastfm:before { + content: "\f202"; } + +.fa-lastfm-square:before { + content: "\f203"; } + +.fa-laugh:before { + content: "\f599"; } + +.fa-laugh-beam:before { + content: "\f59a"; } + +.fa-laugh-squint:before { + content: "\f59b"; } + +.fa-laugh-wink:before { + content: "\f59c"; } + +.fa-layer-group:before { + content: "\f5fd"; } + +.fa-leaf:before { + content: "\f06c"; } + +.fa-leanpub:before { + content: "\f212"; } + +.fa-lemon:before { + content: "\f094"; } + +.fa-less:before { + content: "\f41d"; } + +.fa-less-than:before { + content: "\f536"; } + +.fa-less-than-equal:before { + content: "\f537"; } + +.fa-level-down-alt:before { + content: "\f3be"; } + +.fa-level-up-alt:before { + content: "\f3bf"; } + +.fa-life-ring:before { + content: "\f1cd"; } + +.fa-lightbulb:before { + content: "\f0eb"; } + +.fa-line:before { + content: "\f3c0"; } + +.fa-link:before { + content: "\f0c1"; } + +.fa-linkedin:before { + content: "\f08c"; } + +.fa-linkedin-in:before { + content: "\f0e1"; } + +.fa-linode:before { + content: "\f2b8"; } + +.fa-linux:before { + content: "\f17c"; } + +.fa-lira-sign:before { + content: "\f195"; } + +.fa-list:before { + content: "\f03a"; } + +.fa-list-alt:before { + content: "\f022"; } + +.fa-list-ol:before { + content: "\f0cb"; } + +.fa-list-ul:before { + content: "\f0ca"; } + +.fa-location-arrow:before { + content: "\f124"; } + +.fa-lock:before { + content: "\f023"; } + +.fa-lock-open:before { + content: "\f3c1"; } + +.fa-long-arrow-alt-down:before { + content: "\f309"; } + +.fa-long-arrow-alt-left:before { + content: "\f30a"; } + +.fa-long-arrow-alt-right:before { + content: "\f30b"; } + +.fa-long-arrow-alt-up:before { + content: "\f30c"; } + +.fa-low-vision:before { + content: "\f2a8"; } + +.fa-luggage-cart:before { + content: "\f59d"; } + +.fa-lungs:before { + content: "\f604"; } + +.fa-lungs-virus:before { + content: "\f967"; } + +.fa-lyft:before { + content: "\f3c3"; } + +.fa-magento:before { + content: "\f3c4"; } + +.fa-magic:before { + content: "\f0d0"; } + +.fa-magnet:before { + content: "\f076"; } + +.fa-mail-bulk:before { + content: "\f674"; } + +.fa-mailchimp:before { + content: "\f59e"; } + +.fa-male:before { + content: "\f183"; } + +.fa-mandalorian:before { + content: "\f50f"; } + +.fa-map:before { + content: "\f279"; } + +.fa-map-marked:before { + content: "\f59f"; } + +.fa-map-marked-alt:before { + content: "\f5a0"; } + +.fa-map-marker:before { + content: "\f041"; } + +.fa-map-marker-alt:before { + content: "\f3c5"; } + +.fa-map-pin:before { + content: "\f276"; } + +.fa-map-signs:before { + content: "\f277"; } + +.fa-markdown:before { + content: "\f60f"; } + +.fa-marker:before { + content: "\f5a1"; } + +.fa-mars:before { + content: "\f222"; } + +.fa-mars-double:before { + content: "\f227"; } + +.fa-mars-stroke:before { + content: "\f229"; } + +.fa-mars-stroke-h:before { + content: "\f22b"; } + +.fa-mars-stroke-v:before { + content: "\f22a"; } + +.fa-mask:before { + content: "\f6fa"; } + +.fa-mastodon:before { + content: "\f4f6"; } + +.fa-maxcdn:before { + content: "\f136"; } + +.fa-mdb:before { + content: "\f8ca"; } + +.fa-medal:before { + content: "\f5a2"; } + +.fa-medapps:before { + content: "\f3c6"; } + +.fa-medium:before { + content: "\f23a"; } + +.fa-medium-m:before { + content: "\f3c7"; } + +.fa-medkit:before { + content: "\f0fa"; } + +.fa-medrt:before { + content: "\f3c8"; } + +.fa-meetup:before { + content: "\f2e0"; } + +.fa-megaport:before { + content: "\f5a3"; } + +.fa-meh:before { + content: "\f11a"; } + +.fa-meh-blank:before { + content: "\f5a4"; } + +.fa-meh-rolling-eyes:before { + content: "\f5a5"; } + +.fa-memory:before { + content: "\f538"; } + +.fa-mendeley:before { + content: "\f7b3"; } + +.fa-menorah:before { + content: "\f676"; } + +.fa-mercury:before { + content: "\f223"; } + +.fa-meteor:before { + content: "\f753"; } + +.fa-microblog:before { + content: "\f91a"; } + +.fa-microchip:before { + content: "\f2db"; } + +.fa-microphone:before { + content: "\f130"; } + +.fa-microphone-alt:before { + content: "\f3c9"; } + +.fa-microphone-alt-slash:before { + content: "\f539"; } + +.fa-microphone-slash:before { + content: "\f131"; } + +.fa-microscope:before { + content: "\f610"; } + +.fa-microsoft:before { + content: "\f3ca"; } + +.fa-minus:before { + content: "\f068"; } + +.fa-minus-circle:before { + content: "\f056"; } + +.fa-minus-square:before { + content: "\f146"; } + +.fa-mitten:before { + content: "\f7b5"; } + +.fa-mix:before { + content: "\f3cb"; } + +.fa-mixcloud:before { + content: "\f289"; } + +.fa-mixer:before { + content: "\f956"; } + +.fa-mizuni:before { + content: "\f3cc"; } + +.fa-mobile:before { + content: "\f10b"; } + +.fa-mobile-alt:before { + content: "\f3cd"; } + +.fa-modx:before { + content: "\f285"; } + +.fa-monero:before { + content: "\f3d0"; } + +.fa-money-bill:before { + content: "\f0d6"; } + +.fa-money-bill-alt:before { + content: "\f3d1"; } + +.fa-money-bill-wave:before { + content: "\f53a"; } + +.fa-money-bill-wave-alt:before { + content: "\f53b"; } + +.fa-money-check:before { + content: "\f53c"; } + +.fa-money-check-alt:before { + content: "\f53d"; } + +.fa-monument:before { + content: "\f5a6"; } + +.fa-moon:before { + content: "\f186"; } + +.fa-mortar-pestle:before { + content: "\f5a7"; } + +.fa-mosque:before { + content: "\f678"; } + +.fa-motorcycle:before { + content: "\f21c"; } + +.fa-mountain:before { + content: "\f6fc"; } + +.fa-mouse:before { + content: "\f8cc"; } + +.fa-mouse-pointer:before { + content: "\f245"; } + +.fa-mug-hot:before { + content: "\f7b6"; } + +.fa-music:before { + content: "\f001"; } + +.fa-napster:before { + content: "\f3d2"; } + +.fa-neos:before { + content: "\f612"; } + +.fa-network-wired:before { + content: "\f6ff"; } + +.fa-neuter:before { + content: "\f22c"; } + +.fa-newspaper:before { + content: "\f1ea"; } + +.fa-nimblr:before { + content: "\f5a8"; } + +.fa-node:before { + content: "\f419"; } + +.fa-node-js:before { + content: "\f3d3"; } + +.fa-not-equal:before { + content: "\f53e"; } + +.fa-notes-medical:before { + content: "\f481"; } + +.fa-npm:before { + content: "\f3d4"; } + +.fa-ns8:before { + content: "\f3d5"; } + +.fa-nutritionix:before { + content: "\f3d6"; } + +.fa-object-group:before { + content: "\f247"; } + +.fa-object-ungroup:before { + content: "\f248"; } + +.fa-odnoklassniki:before { + content: "\f263"; } + +.fa-odnoklassniki-square:before { + content: "\f264"; } + +.fa-oil-can:before { + content: "\f613"; } + +.fa-old-republic:before { + content: "\f510"; } + +.fa-om:before { + content: "\f679"; } + +.fa-opencart:before { + content: "\f23d"; } + +.fa-openid:before { + content: "\f19b"; } + +.fa-opera:before { + content: "\f26a"; } + +.fa-optin-monster:before { + content: "\f23c"; } + +.fa-orcid:before { + content: "\f8d2"; } + +.fa-osi:before { + content: "\f41a"; } + +.fa-otter:before { + content: "\f700"; } + +.fa-outdent:before { + content: "\f03b"; } + +.fa-page4:before { + content: "\f3d7"; } + +.fa-pagelines:before { + content: "\f18c"; } + +.fa-pager:before { + content: "\f815"; } + +.fa-paint-brush:before { + content: "\f1fc"; } + +.fa-paint-roller:before { + content: "\f5aa"; } + +.fa-palette:before { + content: "\f53f"; } + +.fa-palfed:before { + content: "\f3d8"; } + +.fa-pallet:before { + content: "\f482"; } + +.fa-paper-plane:before { + content: "\f1d8"; } + +.fa-paperclip:before { + content: "\f0c6"; } + +.fa-parachute-box:before { + content: "\f4cd"; } + +.fa-paragraph:before { + content: "\f1dd"; } + +.fa-parking:before { + content: "\f540"; } + +.fa-passport:before { + content: "\f5ab"; } + +.fa-pastafarianism:before { + content: "\f67b"; } + +.fa-paste:before { + content: "\f0ea"; } + +.fa-patreon:before { + content: "\f3d9"; } + +.fa-pause:before { + content: "\f04c"; } + +.fa-pause-circle:before { + content: "\f28b"; } + +.fa-paw:before { + content: "\f1b0"; } + +.fa-paypal:before { + content: "\f1ed"; } + +.fa-peace:before { + content: "\f67c"; } + +.fa-pen:before { + content: "\f304"; } + +.fa-pen-alt:before { + content: "\f305"; } + +.fa-pen-fancy:before { + content: "\f5ac"; } + +.fa-pen-nib:before { + content: "\f5ad"; } + +.fa-pen-square:before { + content: "\f14b"; } + +.fa-pencil-alt:before { + content: "\f303"; } + +.fa-pencil-ruler:before { + content: "\f5ae"; } + +.fa-penny-arcade:before { + content: "\f704"; } + +.fa-people-arrows:before { + content: "\f968"; } + +.fa-people-carry:before { + content: "\f4ce"; } + +.fa-pepper-hot:before { + content: "\f816"; } + +.fa-percent:before { + content: "\f295"; } + +.fa-percentage:before { + content: "\f541"; } + +.fa-periscope:before { + content: "\f3da"; } + +.fa-person-booth:before { + content: "\f756"; } + +.fa-phabricator:before { + content: "\f3db"; } + +.fa-phoenix-framework:before { + content: "\f3dc"; } + +.fa-phoenix-squadron:before { + content: "\f511"; } + +.fa-phone:before { + content: "\f095"; } + +.fa-phone-alt:before { + content: "\f879"; } + +.fa-phone-slash:before { + content: "\f3dd"; } + +.fa-phone-square:before { + content: "\f098"; } + +.fa-phone-square-alt:before { + content: "\f87b"; } + +.fa-phone-volume:before { + content: "\f2a0"; } + +.fa-photo-video:before { + content: "\f87c"; } + +.fa-php:before { + content: "\f457"; } + +.fa-pied-piper:before { + content: "\f2ae"; } + +.fa-pied-piper-alt:before { + content: "\f1a8"; } + +.fa-pied-piper-hat:before { + content: "\f4e5"; } + +.fa-pied-piper-pp:before { + content: "\f1a7"; } + +.fa-pied-piper-square:before { + content: "\f91e"; } + +.fa-piggy-bank:before { + content: "\f4d3"; } + +.fa-pills:before { + content: "\f484"; } + +.fa-pinterest:before { + content: "\f0d2"; } + +.fa-pinterest-p:before { + content: "\f231"; } + +.fa-pinterest-square:before { + content: "\f0d3"; } + +.fa-pizza-slice:before { + content: "\f818"; } + +.fa-place-of-worship:before { + content: "\f67f"; } + +.fa-plane:before { + content: "\f072"; } + +.fa-plane-arrival:before { + content: "\f5af"; } + +.fa-plane-departure:before { + content: "\f5b0"; } + +.fa-plane-slash:before { + content: "\f969"; } + +.fa-play:before { + content: "\f04b"; } + +.fa-play-circle:before { + content: "\f144"; } + +.fa-playstation:before { + content: "\f3df"; } + +.fa-plug:before { + content: "\f1e6"; } + +.fa-plus:before { + content: "\f067"; } + +.fa-plus-circle:before { + content: "\f055"; } + +.fa-plus-square:before { + content: "\f0fe"; } + +.fa-podcast:before { + content: "\f2ce"; } + +.fa-poll:before { + content: "\f681"; } + +.fa-poll-h:before { + content: "\f682"; } + +.fa-poo:before { + content: "\f2fe"; } + +.fa-poo-storm:before { + content: "\f75a"; } + +.fa-poop:before { + content: "\f619"; } + +.fa-portrait:before { + content: "\f3e0"; } + +.fa-pound-sign:before { + content: "\f154"; } + +.fa-power-off:before { + content: "\f011"; } + +.fa-pray:before { + content: "\f683"; } + +.fa-praying-hands:before { + content: "\f684"; } + +.fa-prescription:before { + content: "\f5b1"; } + +.fa-prescription-bottle:before { + content: "\f485"; } + +.fa-prescription-bottle-alt:before { + content: "\f486"; } + +.fa-print:before { + content: "\f02f"; } + +.fa-procedures:before { + content: "\f487"; } + +.fa-product-hunt:before { + content: "\f288"; } + +.fa-project-diagram:before { + content: "\f542"; } + +.fa-pump-medical:before { + content: "\f96a"; } + +.fa-pump-soap:before { + content: "\f96b"; } + +.fa-pushed:before { + content: "\f3e1"; } + +.fa-puzzle-piece:before { + content: "\f12e"; } + +.fa-python:before { + content: "\f3e2"; } + +.fa-qq:before { + content: "\f1d6"; } + +.fa-qrcode:before { + content: "\f029"; } + +.fa-question:before { + content: "\f128"; } + +.fa-question-circle:before { + content: "\f059"; } + +.fa-quidditch:before { + content: "\f458"; } + +.fa-quinscape:before { + content: "\f459"; } + +.fa-quora:before { + content: "\f2c4"; } + +.fa-quote-left:before { + content: "\f10d"; } + +.fa-quote-right:before { + content: "\f10e"; } + +.fa-quran:before { + content: "\f687"; } + +.fa-r-project:before { + content: "\f4f7"; } + +.fa-radiation:before { + content: "\f7b9"; } + +.fa-radiation-alt:before { + content: "\f7ba"; } + +.fa-rainbow:before { + content: "\f75b"; } + +.fa-random:before { + content: "\f074"; } + +.fa-raspberry-pi:before { + content: "\f7bb"; } + +.fa-ravelry:before { + content: "\f2d9"; } + +.fa-react:before { + content: "\f41b"; } + +.fa-reacteurope:before { + content: "\f75d"; } + +.fa-readme:before { + content: "\f4d5"; } + +.fa-rebel:before { + content: "\f1d0"; } + +.fa-receipt:before { + content: "\f543"; } + +.fa-record-vinyl:before { + content: "\f8d9"; } + +.fa-recycle:before { + content: "\f1b8"; } + +.fa-red-river:before { + content: "\f3e3"; } + +.fa-reddit:before { + content: "\f1a1"; } + +.fa-reddit-alien:before { + content: "\f281"; } + +.fa-reddit-square:before { + content: "\f1a2"; } + +.fa-redhat:before { + content: "\f7bc"; } + +.fa-redo:before { + content: "\f01e"; } + +.fa-redo-alt:before { + content: "\f2f9"; } + +.fa-registered:before { + content: "\f25d"; } + +.fa-remove-format:before { + content: "\f87d"; } + +.fa-renren:before { + content: "\f18b"; } + +.fa-reply:before { + content: "\f3e5"; } + +.fa-reply-all:before { + content: "\f122"; } + +.fa-replyd:before { + content: "\f3e6"; } + +.fa-republican:before { + content: "\f75e"; } + +.fa-researchgate:before { + content: "\f4f8"; } + +.fa-resolving:before { + content: "\f3e7"; } + +.fa-restroom:before { + content: "\f7bd"; } + +.fa-retweet:before { + content: "\f079"; } + +.fa-rev:before { + content: "\f5b2"; } + +.fa-ribbon:before { + content: "\f4d6"; } + +.fa-ring:before { + content: "\f70b"; } + +.fa-road:before { + content: "\f018"; } + +.fa-robot:before { + content: "\f544"; } + +.fa-rocket:before { + content: "\f135"; } + +.fa-rocketchat:before { + content: "\f3e8"; } + +.fa-rockrms:before { + content: "\f3e9"; } + +.fa-route:before { + content: "\f4d7"; } + +.fa-rss:before { + content: "\f09e"; } + +.fa-rss-square:before { + content: "\f143"; } + +.fa-ruble-sign:before { + content: "\f158"; } + +.fa-ruler:before { + content: "\f545"; } + +.fa-ruler-combined:before { + content: "\f546"; } + +.fa-ruler-horizontal:before { + content: "\f547"; } + +.fa-ruler-vertical:before { + content: "\f548"; } + +.fa-running:before { + content: "\f70c"; } + +.fa-rupee-sign:before { + content: "\f156"; } + +.fa-sad-cry:before { + content: "\f5b3"; } + +.fa-sad-tear:before { + content: "\f5b4"; } + +.fa-safari:before { + content: "\f267"; } + +.fa-salesforce:before { + content: "\f83b"; } + +.fa-sass:before { + content: "\f41e"; } + +.fa-satellite:before { + content: "\f7bf"; } + +.fa-satellite-dish:before { + content: "\f7c0"; } + +.fa-save:before { + content: "\f0c7"; } + +.fa-schlix:before { + content: "\f3ea"; } + +.fa-school:before { + content: "\f549"; } + +.fa-screwdriver:before { + content: "\f54a"; } + +.fa-scribd:before { + content: "\f28a"; } + +.fa-scroll:before { + content: "\f70e"; } + +.fa-sd-card:before { + content: "\f7c2"; } + +.fa-search:before { + content: "\f002"; } + +.fa-search-dollar:before { + content: "\f688"; } + +.fa-search-location:before { + content: "\f689"; } + +.fa-search-minus:before { + content: "\f010"; } + +.fa-search-plus:before { + content: "\f00e"; } + +.fa-searchengin:before { + content: "\f3eb"; } + +.fa-seedling:before { + content: "\f4d8"; } + +.fa-sellcast:before { + content: "\f2da"; } + +.fa-sellsy:before { + content: "\f213"; } + +.fa-server:before { + content: "\f233"; } + +.fa-servicestack:before { + content: "\f3ec"; } + +.fa-shapes:before { + content: "\f61f"; } + +.fa-share:before { + content: "\f064"; } + +.fa-share-alt:before { + content: "\f1e0"; } + +.fa-share-alt-square:before { + content: "\f1e1"; } + +.fa-share-square:before { + content: "\f14d"; } + +.fa-shekel-sign:before { + content: "\f20b"; } + +.fa-shield-alt:before { + content: "\f3ed"; } + +.fa-shield-virus:before { + content: "\f96c"; } + +.fa-ship:before { + content: "\f21a"; } + +.fa-shipping-fast:before { + content: "\f48b"; } + +.fa-shirtsinbulk:before { + content: "\f214"; } + +.fa-shoe-prints:before { + content: "\f54b"; } + +.fa-shopify:before { + content: "\f957"; } + +.fa-shopping-bag:before { + content: "\f290"; } + +.fa-shopping-basket:before { + content: "\f291"; } + +.fa-shopping-cart:before { + content: "\f07a"; } + +.fa-shopware:before { + content: "\f5b5"; } + +.fa-shower:before { + content: "\f2cc"; } + +.fa-shuttle-van:before { + content: "\f5b6"; } + +.fa-sign:before { + content: "\f4d9"; } + +.fa-sign-in-alt:before { + content: "\f2f6"; } + +.fa-sign-language:before { + content: "\f2a7"; } + +.fa-sign-out-alt:before { + content: "\f2f5"; } + +.fa-signal:before { + content: "\f012"; } + +.fa-signature:before { + content: "\f5b7"; } + +.fa-sim-card:before { + content: "\f7c4"; } + +.fa-simplybuilt:before { + content: "\f215"; } + +.fa-sistrix:before { + content: "\f3ee"; } + +.fa-sitemap:before { + content: "\f0e8"; } + +.fa-sith:before { + content: "\f512"; } + +.fa-skating:before { + content: "\f7c5"; } + +.fa-sketch:before { + content: "\f7c6"; } + +.fa-skiing:before { + content: "\f7c9"; } + +.fa-skiing-nordic:before { + content: "\f7ca"; } + +.fa-skull:before { + content: "\f54c"; } + +.fa-skull-crossbones:before { + content: "\f714"; } + +.fa-skyatlas:before { + content: "\f216"; } + +.fa-skype:before { + content: "\f17e"; } + +.fa-slack:before { + content: "\f198"; } + +.fa-slack-hash:before { + content: "\f3ef"; } + +.fa-slash:before { + content: "\f715"; } + +.fa-sleigh:before { + content: "\f7cc"; } + +.fa-sliders-h:before { + content: "\f1de"; } + +.fa-slideshare:before { + content: "\f1e7"; } + +.fa-smile:before { + content: "\f118"; } + +.fa-smile-beam:before { + content: "\f5b8"; } + +.fa-smile-wink:before { + content: "\f4da"; } + +.fa-smog:before { + content: "\f75f"; } + +.fa-smoking:before { + content: "\f48d"; } + +.fa-smoking-ban:before { + content: "\f54d"; } + +.fa-sms:before { + content: "\f7cd"; } + +.fa-snapchat:before { + content: "\f2ab"; } + +.fa-snapchat-ghost:before { + content: "\f2ac"; } + +.fa-snapchat-square:before { + content: "\f2ad"; } + +.fa-snowboarding:before { + content: "\f7ce"; } + +.fa-snowflake:before { + content: "\f2dc"; } + +.fa-snowman:before { + content: "\f7d0"; } + +.fa-snowplow:before { + content: "\f7d2"; } + +.fa-soap:before { + content: "\f96e"; } + +.fa-socks:before { + content: "\f696"; } + +.fa-solar-panel:before { + content: "\f5ba"; } + +.fa-sort:before { + content: "\f0dc"; } + +.fa-sort-alpha-down:before { + content: "\f15d"; } + +.fa-sort-alpha-down-alt:before { + content: "\f881"; } + +.fa-sort-alpha-up:before { + content: "\f15e"; } + +.fa-sort-alpha-up-alt:before { + content: "\f882"; } + +.fa-sort-amount-down:before { + content: "\f160"; } + +.fa-sort-amount-down-alt:before { + content: "\f884"; } + +.fa-sort-amount-up:before { + content: "\f161"; } + +.fa-sort-amount-up-alt:before { + content: "\f885"; } + +.fa-sort-down:before { + content: "\f0dd"; } + +.fa-sort-numeric-down:before { + content: "\f162"; } + +.fa-sort-numeric-down-alt:before { + content: "\f886"; } + +.fa-sort-numeric-up:before { + content: "\f163"; } + +.fa-sort-numeric-up-alt:before { + content: "\f887"; } + +.fa-sort-up:before { + content: "\f0de"; } + +.fa-soundcloud:before { + content: "\f1be"; } + +.fa-sourcetree:before { + content: "\f7d3"; } + +.fa-spa:before { + content: "\f5bb"; } + +.fa-space-shuttle:before { + content: "\f197"; } + +.fa-speakap:before { + content: "\f3f3"; } + +.fa-speaker-deck:before { + content: "\f83c"; } + +.fa-spell-check:before { + content: "\f891"; } + +.fa-spider:before { + content: "\f717"; } + +.fa-spinner:before { + content: "\f110"; } + +.fa-splotch:before { + content: "\f5bc"; } + +.fa-spotify:before { + content: "\f1bc"; } + +.fa-spray-can:before { + content: "\f5bd"; } + +.fa-square:before { + content: "\f0c8"; } + +.fa-square-full:before { + content: "\f45c"; } + +.fa-square-root-alt:before { + content: "\f698"; } + +.fa-squarespace:before { + content: "\f5be"; } + +.fa-stack-exchange:before { + content: "\f18d"; } + +.fa-stack-overflow:before { + content: "\f16c"; } + +.fa-stackpath:before { + content: "\f842"; } + +.fa-stamp:before { + content: "\f5bf"; } + +.fa-star:before { + content: "\f005"; } + +.fa-star-and-crescent:before { + content: "\f699"; } + +.fa-star-half:before { + content: "\f089"; } + +.fa-star-half-alt:before { + content: "\f5c0"; } + +.fa-star-of-david:before { + content: "\f69a"; } + +.fa-star-of-life:before { + content: "\f621"; } + +.fa-staylinked:before { + content: "\f3f5"; } + +.fa-steam:before { + content: "\f1b6"; } + +.fa-steam-square:before { + content: "\f1b7"; } + +.fa-steam-symbol:before { + content: "\f3f6"; } + +.fa-step-backward:before { + content: "\f048"; } + +.fa-step-forward:before { + content: "\f051"; } + +.fa-stethoscope:before { + content: "\f0f1"; } + +.fa-sticker-mule:before { + content: "\f3f7"; } + +.fa-sticky-note:before { + content: "\f249"; } + +.fa-stop:before { + content: "\f04d"; } + +.fa-stop-circle:before { + content: "\f28d"; } + +.fa-stopwatch:before { + content: "\f2f2"; } + +.fa-stopwatch-20:before { + content: "\f96f"; } + +.fa-store:before { + content: "\f54e"; } + +.fa-store-alt:before { + content: "\f54f"; } + +.fa-store-alt-slash:before { + content: "\f970"; } + +.fa-store-slash:before { + content: "\f971"; } + +.fa-strava:before { + content: "\f428"; } + +.fa-stream:before { + content: "\f550"; } + +.fa-street-view:before { + content: "\f21d"; } + +.fa-strikethrough:before { + content: "\f0cc"; } + +.fa-stripe:before { + content: "\f429"; } + +.fa-stripe-s:before { + content: "\f42a"; } + +.fa-stroopwafel:before { + content: "\f551"; } + +.fa-studiovinari:before { + content: "\f3f8"; } + +.fa-stumbleupon:before { + content: "\f1a4"; } + +.fa-stumbleupon-circle:before { + content: "\f1a3"; } + +.fa-subscript:before { + content: "\f12c"; } + +.fa-subway:before { + content: "\f239"; } + +.fa-suitcase:before { + content: "\f0f2"; } + +.fa-suitcase-rolling:before { + content: "\f5c1"; } + +.fa-sun:before { + content: "\f185"; } + +.fa-superpowers:before { + content: "\f2dd"; } + +.fa-superscript:before { + content: "\f12b"; } + +.fa-supple:before { + content: "\f3f9"; } + +.fa-surprise:before { + content: "\f5c2"; } + +.fa-suse:before { + content: "\f7d6"; } + +.fa-swatchbook:before { + content: "\f5c3"; } + +.fa-swift:before { + content: "\f8e1"; } + +.fa-swimmer:before { + content: "\f5c4"; } + +.fa-swimming-pool:before { + content: "\f5c5"; } + +.fa-symfony:before { + content: "\f83d"; } + +.fa-synagogue:before { + content: "\f69b"; } + +.fa-sync:before { + content: "\f021"; } + +.fa-sync-alt:before { + content: "\f2f1"; } + +.fa-syringe:before { + content: "\f48e"; } + +.fa-table:before { + content: "\f0ce"; } + +.fa-table-tennis:before { + content: "\f45d"; } + +.fa-tablet:before { + content: "\f10a"; } + +.fa-tablet-alt:before { + content: "\f3fa"; } + +.fa-tablets:before { + content: "\f490"; } + +.fa-tachometer-alt:before { + content: "\f3fd"; } + +.fa-tag:before { + content: "\f02b"; } + +.fa-tags:before { + content: "\f02c"; } + +.fa-tape:before { + content: "\f4db"; } + +.fa-tasks:before { + content: "\f0ae"; } + +.fa-taxi:before { + content: "\f1ba"; } + +.fa-teamspeak:before { + content: "\f4f9"; } + +.fa-teeth:before { + content: "\f62e"; } + +.fa-teeth-open:before { + content: "\f62f"; } + +.fa-telegram:before { + content: "\f2c6"; } + +.fa-telegram-plane:before { + content: "\f3fe"; } + +.fa-temperature-high:before { + content: "\f769"; } + +.fa-temperature-low:before { + content: "\f76b"; } + +.fa-tencent-weibo:before { + content: "\f1d5"; } + +.fa-tenge:before { + content: "\f7d7"; } + +.fa-terminal:before { + content: "\f120"; } + +.fa-text-height:before { + content: "\f034"; } + +.fa-text-width:before { + content: "\f035"; } + +.fa-th:before { + content: "\f00a"; } + +.fa-th-large:before { + content: "\f009"; } + +.fa-th-list:before { + content: "\f00b"; } + +.fa-the-red-yeti:before { + content: "\f69d"; } + +.fa-theater-masks:before { + content: "\f630"; } + +.fa-themeco:before { + content: "\f5c6"; } + +.fa-themeisle:before { + content: "\f2b2"; } + +.fa-thermometer:before { + content: "\f491"; } + +.fa-thermometer-empty:before { + content: "\f2cb"; } + +.fa-thermometer-full:before { + content: "\f2c7"; } + +.fa-thermometer-half:before { + content: "\f2c9"; } + +.fa-thermometer-quarter:before { + content: "\f2ca"; } + +.fa-thermometer-three-quarters:before { + content: "\f2c8"; } + +.fa-think-peaks:before { + content: "\f731"; } + +.fa-thumbs-down:before { + content: "\f165"; } + +.fa-thumbs-up:before { + content: "\f164"; } + +.fa-thumbtack:before { + content: "\f08d"; } + +.fa-ticket-alt:before { + content: "\f3ff"; } + +.fa-times:before { + content: "\f00d"; } + +.fa-times-circle:before { + content: "\f057"; } + +.fa-tint:before { + content: "\f043"; } + +.fa-tint-slash:before { + content: "\f5c7"; } + +.fa-tired:before { + content: "\f5c8"; } + +.fa-toggle-off:before { + content: "\f204"; } + +.fa-toggle-on:before { + content: "\f205"; } + +.fa-toilet:before { + content: "\f7d8"; } + +.fa-toilet-paper:before { + content: "\f71e"; } + +.fa-toilet-paper-slash:before { + content: "\f972"; } + +.fa-toolbox:before { + content: "\f552"; } + +.fa-tools:before { + content: "\f7d9"; } + +.fa-tooth:before { + content: "\f5c9"; } + +.fa-torah:before { + content: "\f6a0"; } + +.fa-torii-gate:before { + content: "\f6a1"; } + +.fa-tractor:before { + content: "\f722"; } + +.fa-trade-federation:before { + content: "\f513"; } + +.fa-trademark:before { + content: "\f25c"; } + +.fa-traffic-light:before { + content: "\f637"; } + +.fa-trailer:before { + content: "\f941"; } + +.fa-train:before { + content: "\f238"; } + +.fa-tram:before { + content: "\f7da"; } + +.fa-transgender:before { + content: "\f224"; } + +.fa-transgender-alt:before { + content: "\f225"; } + +.fa-trash:before { + content: "\f1f8"; } + +.fa-trash-alt:before { + content: "\f2ed"; } + +.fa-trash-restore:before { + content: "\f829"; } + +.fa-trash-restore-alt:before { + content: "\f82a"; } + +.fa-tree:before { + content: "\f1bb"; } + +.fa-trello:before { + content: "\f181"; } + +.fa-tripadvisor:before { + content: "\f262"; } + +.fa-trophy:before { + content: "\f091"; } + +.fa-truck:before { + content: "\f0d1"; } + +.fa-truck-loading:before { + content: "\f4de"; } + +.fa-truck-monster:before { + content: "\f63b"; } + +.fa-truck-moving:before { + content: "\f4df"; } + +.fa-truck-pickup:before { + content: "\f63c"; } + +.fa-tshirt:before { + content: "\f553"; } + +.fa-tty:before { + content: "\f1e4"; } + +.fa-tumblr:before { + content: "\f173"; } + +.fa-tumblr-square:before { + content: "\f174"; } + +.fa-tv:before { + content: "\f26c"; } + +.fa-twitch:before { + content: "\f1e8"; } + +.fa-twitter:before { + content: "\f099"; } + +.fa-twitter-square:before { + content: "\f081"; } + +.fa-typo3:before { + content: "\f42b"; } + +.fa-uber:before { + content: "\f402"; } + +.fa-ubuntu:before { + content: "\f7df"; } + +.fa-uikit:before { + content: "\f403"; } + +.fa-umbraco:before { + content: "\f8e8"; } + +.fa-umbrella:before { + content: "\f0e9"; } + +.fa-umbrella-beach:before { + content: "\f5ca"; } + +.fa-underline:before { + content: "\f0cd"; } + +.fa-undo:before { + content: "\f0e2"; } + +.fa-undo-alt:before { + content: "\f2ea"; } + +.fa-uniregistry:before { + content: "\f404"; } + +.fa-unity:before { + content: "\f949"; } + +.fa-universal-access:before { + content: "\f29a"; } + +.fa-university:before { + content: "\f19c"; } + +.fa-unlink:before { + content: "\f127"; } + +.fa-unlock:before { + content: "\f09c"; } + +.fa-unlock-alt:before { + content: "\f13e"; } + +.fa-untappd:before { + content: "\f405"; } + +.fa-upload:before { + content: "\f093"; } + +.fa-ups:before { + content: "\f7e0"; } + +.fa-usb:before { + content: "\f287"; } + +.fa-user:before { + content: "\f007"; } + +.fa-user-alt:before { + content: "\f406"; } + +.fa-user-alt-slash:before { + content: "\f4fa"; } + +.fa-user-astronaut:before { + content: "\f4fb"; } + +.fa-user-check:before { + content: "\f4fc"; } + +.fa-user-circle:before { + content: "\f2bd"; } + +.fa-user-clock:before { + content: "\f4fd"; } + +.fa-user-cog:before { + content: "\f4fe"; } + +.fa-user-edit:before { + content: "\f4ff"; } + +.fa-user-friends:before { + content: "\f500"; } + +.fa-user-graduate:before { + content: "\f501"; } + +.fa-user-injured:before { + content: "\f728"; } + +.fa-user-lock:before { + content: "\f502"; } + +.fa-user-md:before { + content: "\f0f0"; } + +.fa-user-minus:before { + content: "\f503"; } + +.fa-user-ninja:before { + content: "\f504"; } + +.fa-user-nurse:before { + content: "\f82f"; } + +.fa-user-plus:before { + content: "\f234"; } + +.fa-user-secret:before { + content: "\f21b"; } + +.fa-user-shield:before { + content: "\f505"; } + +.fa-user-slash:before { + content: "\f506"; } + +.fa-user-tag:before { + content: "\f507"; } + +.fa-user-tie:before { + content: "\f508"; } + +.fa-user-times:before { + content: "\f235"; } + +.fa-users:before { + content: "\f0c0"; } + +.fa-users-cog:before { + content: "\f509"; } + +.fa-usps:before { + content: "\f7e1"; } + +.fa-ussunnah:before { + content: "\f407"; } + +.fa-utensil-spoon:before { + content: "\f2e5"; } + +.fa-utensils:before { + content: "\f2e7"; } + +.fa-vaadin:before { + content: "\f408"; } + +.fa-vector-square:before { + content: "\f5cb"; } + +.fa-venus:before { + content: "\f221"; } + +.fa-venus-double:before { + content: "\f226"; } + +.fa-venus-mars:before { + content: "\f228"; } + +.fa-viacoin:before { + content: "\f237"; } + +.fa-viadeo:before { + content: "\f2a9"; } + +.fa-viadeo-square:before { + content: "\f2aa"; } + +.fa-vial:before { + content: "\f492"; } + +.fa-vials:before { + content: "\f493"; } + +.fa-viber:before { + content: "\f409"; } + +.fa-video:before { + content: "\f03d"; } + +.fa-video-slash:before { + content: "\f4e2"; } + +.fa-vihara:before { + content: "\f6a7"; } + +.fa-vimeo:before { + content: "\f40a"; } + +.fa-vimeo-square:before { + content: "\f194"; } + +.fa-vimeo-v:before { + content: "\f27d"; } + +.fa-vine:before { + content: "\f1ca"; } + +.fa-virus:before { + content: "\f974"; } + +.fa-virus-slash:before { + content: "\f975"; } + +.fa-viruses:before { + content: "\f976"; } + +.fa-vk:before { + content: "\f189"; } + +.fa-vnv:before { + content: "\f40b"; } + +.fa-voicemail:before { + content: "\f897"; } + +.fa-volleyball-ball:before { + content: "\f45f"; } + +.fa-volume-down:before { + content: "\f027"; } + +.fa-volume-mute:before { + content: "\f6a9"; } + +.fa-volume-off:before { + content: "\f026"; } + +.fa-volume-up:before { + content: "\f028"; } + +.fa-vote-yea:before { + content: "\f772"; } + +.fa-vr-cardboard:before { + content: "\f729"; } + +.fa-vuejs:before { + content: "\f41f"; } + +.fa-walking:before { + content: "\f554"; } + +.fa-wallet:before { + content: "\f555"; } + +.fa-warehouse:before { + content: "\f494"; } + +.fa-water:before { + content: "\f773"; } + +.fa-wave-square:before { + content: "\f83e"; } + +.fa-waze:before { + content: "\f83f"; } + +.fa-weebly:before { + content: "\f5cc"; } + +.fa-weibo:before { + content: "\f18a"; } + +.fa-weight:before { + content: "\f496"; } + +.fa-weight-hanging:before { + content: "\f5cd"; } + +.fa-weixin:before { + content: "\f1d7"; } + +.fa-whatsapp:before { + content: "\f232"; } + +.fa-whatsapp-square:before { + content: "\f40c"; } + +.fa-wheelchair:before { + content: "\f193"; } + +.fa-whmcs:before { + content: "\f40d"; } + +.fa-wifi:before { + content: "\f1eb"; } + +.fa-wikipedia-w:before { + content: "\f266"; } + +.fa-wind:before { + content: "\f72e"; } + +.fa-window-close:before { + content: "\f410"; } + +.fa-window-maximize:before { + content: "\f2d0"; } + +.fa-window-minimize:before { + content: "\f2d1"; } + +.fa-window-restore:before { + content: "\f2d2"; } + +.fa-windows:before { + content: "\f17a"; } + +.fa-wine-bottle:before { + content: "\f72f"; } + +.fa-wine-glass:before { + content: "\f4e3"; } + +.fa-wine-glass-alt:before { + content: "\f5ce"; } + +.fa-wix:before { + content: "\f5cf"; } + +.fa-wizards-of-the-coast:before { + content: "\f730"; } + +.fa-wolf-pack-battalion:before { + content: "\f514"; } + +.fa-won-sign:before { + content: "\f159"; } + +.fa-wordpress:before { + content: "\f19a"; } + +.fa-wordpress-simple:before { + content: "\f411"; } + +.fa-wpbeginner:before { + content: "\f297"; } + +.fa-wpexplorer:before { + content: "\f2de"; } + +.fa-wpforms:before { + content: "\f298"; } + +.fa-wpressr:before { + content: "\f3e4"; } + +.fa-wrench:before { + content: "\f0ad"; } + +.fa-x-ray:before { + content: "\f497"; } + +.fa-xbox:before { + content: "\f412"; } + +.fa-xing:before { + content: "\f168"; } + +.fa-xing-square:before { + content: "\f169"; } + +.fa-y-combinator:before { + content: "\f23b"; } + +.fa-yahoo:before { + content: "\f19e"; } + +.fa-yammer:before { + content: "\f840"; } + +.fa-yandex:before { + content: "\f413"; } + +.fa-yandex-international:before { + content: "\f414"; } + +.fa-yarn:before { + content: "\f7e3"; } + +.fa-yelp:before { + content: "\f1e9"; } + +.fa-yen-sign:before { + content: "\f157"; } + +.fa-yin-yang:before { + content: "\f6ad"; } + +.fa-yoast:before { + content: "\f2b1"; } + +.fa-youtube:before { + content: "\f167"; } + +.fa-youtube-square:before { + content: "\f431"; } + +.fa-zhihu:before { + content: "\f63f"; } + +.sr-only { + border: 0; + clip: rect(0, 0, 0, 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; } + +.sr-only-focusable:active, .sr-only-focusable:focus { + clip: auto; + height: auto; + margin: 0; + overflow: visible; + position: static; + width: auto; } diff --git a/www/css/style.css b/www/css/style.css new file mode 100644 index 0000000..613dcde --- /dev/null +++ b/www/css/style.css @@ -0,0 +1 @@ +* { box-sizing: border-box; } body {margin: 0;}.row{display:table;padding:10px;width:100%;}.cell{width:8%;display:table-cell;height:75px;}body, html{background-color:#1d1c25;margin:0;padding:0;}.range{-webkit-appearance:none;-moz-appearance:none;position:absolute;left:50%;top:50%;width:200px;margin-top:10px;transform:translate(-50%, -50%);}input[type=range]::-webkit-slider-runnable-track{-webkit-appearance:none;background:linear-gradient(45deg, rgba(59,173,227,1) 0%, rgba(87,111,230,1) 25%, rgba(152,68,183,1) 51%, rgba(255,53,127,1) 100%);filter:progid:DXImageTransform.Microsoft.gradient( startColorstr='#3bade3 ', endColorstr='#ff357f ', GradientType=1 );height:2px;}input[type=range]:focus{outline:none;}input[type=range]::-moz-range-track{-moz-appearance:none;background:linear-gradient(45deg, rgba(59,173,227,1) 0%, rgba(87,111,230,1) 25%, rgba(152,68,183,1) 51%, rgba(255,53,127,1) 100%);filter:progid:DXImageTransform.Microsoft.gradient( startColorstr='#3bade3 ', endColorstr='#ff357f ', GradientType=1 );height:2px;}input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;border:2px solid;border-radius:50%;height:25px;width:25px;max-width:80px;position:relative;bottom:11px;background-color:#1d1c25;cursor:-webkit-grab;-webkit-transition:border 1000ms ease;transition:border 1000ms ease;}input[type=range]::-moz-range-thumb{-moz-appearance:none;border:2px solid;border-radius:50%;height:25px;width:25px;max-width:80px;position:relative;bottom:11px;background-color:#1d1c25;cursor:-moz-grab;-moz-transition:border 1000ms ease;transition:border 1000ms ease;}.range.blue::-webkit-slider-thumb{border-color:rgb(59,173,227);}.range.ltpurple::-webkit-slider-thumb{border-color:rgb(87,111,230);}.range.purple::-webkit-slider-thumb{border-color:rgb(152,68,183);}.range.pink::-webkit-slider-thumb{border-color:rgb(255,53,127);}.range.blue::-moz-range-thumb{border-color:rgb(59,173,227);}.range.ltpurple::-moz-range-thumb{border-color:rgb(87,111,230);}.range.purple::-moz-range-thumb{border-color:rgb(152,68,183);}.range.pink::-moz-range-thumb{border-color:rgb(255,53,127);}input[type=range]::-webkit-slider-thumb:active{cursor:-webkit-grabbing;}input[type=range]::-moz-range-thumb:active{cursor:-moz-grabbing;}.range.blue{position:relative;background-color:rgba(31,27,27,0.09);width:75%;top:37.5px;}*{box-sizing:border-box;}body{margin:0;}#iy3nk{height:68px;}#il4ah{text-align:center;color:#ef4079;}#i7ox{font-family:Verdana, Geneva, sans-serif;color:#40acef;text-align:left;}#io5q{color:#40acef;}#iy2eh{text-align:center;}#i8a73{text-align:center;color:#4540ef;}#i3fzo{text-align:center;color:#7740ef;}#ikbjg{text-align:center;color:#b140ef;}@media (max-width: 768px){.cell{width:100%;display:block;}} \ No newline at end of file diff --git a/www/css/style2.css b/www/css/style2.css new file mode 100644 index 0000000..115c94b --- /dev/null +++ b/www/css/style2.css @@ -0,0 +1 @@ +input[type=range]::-webkit-slider-runnable-track{-webkit-appearance:none;background:linear-gradient(45deg, rgba(59,173,227,1) 0%, rgba(87,111,230,1) 25%, rgba(152,68,183,1) 51%, rgba(255,53,127,1) 100%);filter:progid:DXImageTransform.Microsoft.gradient( startColorstr='#3bade3 ', endColorstr='#ff357f ', GradientType=1 );height:2px;}input[type=range]:focus{outline:none;}input[type=range]::-moz-range-track{-moz-appearance:none;background:linear-gradient(45deg, rgba(59,173,227,1) 0%, rgba(87,111,230,1) 25%, rgba(152,68,183,1) 51%, rgba(255,53,127,1) 100%);filter:progid:DXImageTransform.Microsoft.gradient( startColorstr='#3bade3 ', endColorstr='#ff357f ', GradientType=1 );height:2px;}input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;border:2px solid;border-radius:50%;height:25px;width:25px;max-width:80px;position:relative;bottom:11px;background-color:#1d1c25;cursor:-webkit-grab;-webkit-transition:border 1000ms ease;transition:border 1000ms ease;}input[type=range]::-moz-range-thumb{-moz-appearance:none;border:2px solid;border-radius:50%;height:25px;width:25px;max-width:80px;position:relative;bottom:11px;background-color:#1d1c25;cursor:-moz-grab;-moz-transition:border 1000ms ease;transition:border 1000ms ease;}.range.blue::-webkit-slider-thumb{border-color:rgb(59,173,227);}.range.ltpurple::-webkit-slider-thumb{border-color:rgb(87,111,230);}.range.purple::-webkit-slider-thumb{border-color:rgb(152,68,183);}.range.pink::-webkit-slider-thumb{border-color:rgb(255,53,127);}.range.blue::-moz-range-thumb{border-color:rgb(59,173,227);}.range.ltpurple::-moz-range-thumb{border-color:rgb(87,111,230);}.range.purple::-moz-range-thumb{border-color:rgb(152,68,183);}.range.pink::-moz-range-thumb{border-color:rgb(255,53,127);}input[type=range]::-webkit-slider-thumb:active{cursor:-webkit-grabbing;}input[type=range]::-moz-range-thumb:active{cursor:-moz-grabbing;}.range.blue{position:relative;background-color:rgba(31,27,27,0.09);width:75%;top:37.5px;}body{margin:0;}#iy3nk{font-family: "Lucida Grande", Verdana, Arial, sans-serif; font-size: 10ex; height:68px;}#il4ah{text-align:center;color:#ef4079;}#i7ox{font-family:Verdana, Geneva, sans-serif;color:#40acef;text-align:left;}#io5q{color:#40acef;}#iy2eh{text-align:center;}#i8a73{text-align:center;color:#4540ef;}#i3fzo{text-align:center;color:#7740ef;}#ikbjg{text-align:center;color:#b140ef;}@media (max-width: 768px){} \ No newline at end of file diff --git a/www/favicon-16x16.png b/www/favicon-16x16.png new file mode 100755 index 0000000..fd247b7 Binary files /dev/null and b/www/favicon-16x16.png differ diff --git a/www/favicon-32x32.png b/www/favicon-32x32.png new file mode 100755 index 0000000..a86ae5b Binary files /dev/null and b/www/favicon-32x32.png differ diff --git a/www/favicon.ico b/www/favicon.ico new file mode 100755 index 0000000..806f2ca Binary files /dev/null and b/www/favicon.ico differ diff --git a/www/knobs/.DS_Store b/www/knobs/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/www/knobs/.DS_Store differ diff --git a/www/knobs/big0.png b/www/knobs/big0.png new file mode 100644 index 0000000..6b20ff5 Binary files /dev/null and b/www/knobs/big0.png differ diff --git a/www/knobs/big1.png b/www/knobs/big1.png new file mode 100644 index 0000000..11198ac Binary files /dev/null and b/www/knobs/big1.png differ diff --git a/www/knobs/big2.png b/www/knobs/big2.png new file mode 100644 index 0000000..7a9f557 Binary files /dev/null and b/www/knobs/big2.png differ diff --git a/www/knobs/big3.png b/www/knobs/big3.png new file mode 100644 index 0000000..ac11298 Binary files /dev/null and b/www/knobs/big3.png differ diff --git a/www/knobs/blue.png b/www/knobs/blue.png new file mode 100644 index 0000000..dee6dce Binary files /dev/null and b/www/knobs/blue.png differ diff --git a/www/knobs/blue.psd b/www/knobs/blue.psd new file mode 100644 index 0000000..812ddba Binary files /dev/null and b/www/knobs/blue.psd differ diff --git a/www/knobs/blue2.png b/www/knobs/blue2.png new file mode 100644 index 0000000..4301fc7 Binary files /dev/null and b/www/knobs/blue2.png differ diff --git a/www/knobs/close.svg b/www/knobs/close.svg new file mode 100644 index 0000000..749b17d --- /dev/null +++ b/www/knobs/close.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/www/knobs/cyan.png b/www/knobs/cyan.png new file mode 100644 index 0000000..ad80b3a Binary files /dev/null and b/www/knobs/cyan.png differ diff --git a/www/knobs/green.png b/www/knobs/green.png new file mode 100644 index 0000000..272f968 Binary files /dev/null and b/www/knobs/green.png differ diff --git a/www/knobs/green.sync-conflict-20200612-000836.png b/www/knobs/green.sync-conflict-20200612-000836.png new file mode 100644 index 0000000..603f60f Binary files /dev/null and b/www/knobs/green.sync-conflict-20200612-000836.png differ diff --git a/www/knobs/indexs.png b/www/knobs/indexs.png new file mode 100644 index 0000000..97f632d Binary files /dev/null and b/www/knobs/indexs.png differ diff --git a/www/knobs/lasergrid0.png b/www/knobs/lasergrid0.png new file mode 100644 index 0000000..e6fbbc6 Binary files /dev/null and b/www/knobs/lasergrid0.png differ diff --git a/www/knobs/lasergrid1.png b/www/knobs/lasergrid1.png new file mode 100644 index 0000000..054cc3a Binary files /dev/null and b/www/knobs/lasergrid1.png differ diff --git a/www/knobs/lasergrid2.png b/www/knobs/lasergrid2.png new file mode 100644 index 0000000..b31d50e Binary files /dev/null and b/www/knobs/lasergrid2.png differ diff --git a/www/knobs/lasergrid3.png b/www/knobs/lasergrid3.png new file mode 100644 index 0000000..e31d5b3 Binary files /dev/null and b/www/knobs/lasergrid3.png differ diff --git a/www/knobs/leds.png b/www/knobs/leds.png new file mode 100644 index 0000000..6bc3530 Binary files /dev/null and b/www/knobs/leds.png differ diff --git a/www/knobs/pink.png b/www/knobs/pink.png new file mode 100644 index 0000000..90fca21 Binary files /dev/null and b/www/knobs/pink.png differ diff --git a/www/knobs/power.png b/www/knobs/power.png new file mode 100644 index 0000000..5c98bb8 Binary files /dev/null and b/www/knobs/power.png differ diff --git a/www/knobs/red.png b/www/knobs/red.png new file mode 100644 index 0000000..f3a841a Binary files /dev/null and b/www/knobs/red.png differ diff --git a/www/knobs/simplegray.png b/www/knobs/simplegray.png new file mode 100644 index 0000000..9c57010 Binary files /dev/null and b/www/knobs/simplegray.png differ diff --git a/www/knobs/white.png b/www/knobs/white.png new file mode 100644 index 0000000..c85b04f Binary files /dev/null and b/www/knobs/white.png differ diff --git a/www/knobs/yellow.png b/www/knobs/yellow.png new file mode 100644 index 0000000..6255946 Binary files /dev/null and b/www/knobs/yellow.png differ diff --git a/www/launch.png b/www/launch.png new file mode 100644 index 0000000..e912ac5 Binary files /dev/null and b/www/launch.png differ diff --git a/www/repository/calig.png b/www/repository/calig.png new file mode 100644 index 0000000..b4887e4 Binary files /dev/null and b/www/repository/calig.png differ diff --git a/www/repository/display.png b/www/repository/display.png new file mode 100644 index 0000000..7ea4aa5 Binary files /dev/null and b/www/repository/display.png differ diff --git a/www/repository/layer.png b/www/repository/layer.png new file mode 100644 index 0000000..b4bbff3 Binary files /dev/null and b/www/repository/layer.png differ diff --git a/www/repository/lj2.png b/www/repository/lj2.png new file mode 100644 index 0000000..36ef71c Binary files /dev/null and b/www/repository/lj2.png differ diff --git a/www/repository/scenes.png b/www/repository/scenes.png new file mode 100644 index 0000000..5e96e68 Binary files /dev/null and b/www/repository/scenes.png differ diff --git a/www/selector.min.css b/www/selector.min.css new file mode 100644 index 0000000..061a532 --- /dev/null +++ b/www/selector.min.css @@ -0,0 +1 @@ +div.selector-element{font-size: 0.75em; display:inline-block;position:relative;width:100%;max-width: 85px;height: 24px;font-family:sans-serif;color: #fbfff5;background-color: #111111}div.selector-element div.selector-selected{display:inline-block;width:100%;height:100%;position:relative;cursor:pointer;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;line-height:30px;padding:0 12px;transition:.15s ease-in-out;border:1px solid gray}div.selector-element div.selector-selected:hover{background-color: #060606}div.selector-element div.selector-selected:after{content:'';position:absolute;top:50%;right:22px;transform:translateY(-50%);height:0;width:0;border-top:5px solid silver;border-left:5px solid transparent;border-right:5px solid transparent}div.selector-element div.selector-selected p.selected-text{display:inline-block;margin:0;width:100%;height:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding-right:30px;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}div.selector-element div.selector-options{color: #dbdbdb; display:none;position:absolute;background-color: #111111;top:100%;width:100%;height:auto;max-height:156px;overflow:auto;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;border:1px solid grey;border-top:0;box-shadow:0 2px 3px rgba(0,0,0,0.2);z-index:1;transition:.2s ease-in-out}div.selector-element div.selector-options.options-search{max-height:220px}div.selector-element.open div.selector-options{display:block}div.selector-element div.selector-options div.selector-option{display:block;position:relative;width:100%;height:32px;border-bottom:1px solid silver;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0 12px;line-height:31px;cursor:pointer;overflow:hidden;transition:.15s ease-in-out}div.selector-element div.selector-options div.selector-option.hide{height:0;border-bottom:0;transition:.1s ease-in-out}div.selector-element div.selector-options div.selector-option.option-selected{background-color: #111111}div.selector-element div.selector-options div.selector-option:hover{color: #fbfff5; background-color: #111111}div.selector-element div.selector-options div.selector-option:last-of-type{border-bottom:0}div.selector-element div.selector-options div.selector-option.option-disabled{opacity:.6}div.selector-element div.selector-options div.option-search{display:block;height:32px;line-height:33px;position:relative;width:100%;border-bottom:1px solid grey}div.selector-element div.selector-options div.option-search span{position:absolute;top:50%;transform:translateY(-48%) !important;height:14px;width:14px;cursor:pointer;z-index:1;background:url("../knobs/close.svg") center no-repeat;background-size:11px;right:12px}div.selector-element div.selector-options div.option-search input{display:inline-block;width:100%;height:100%;padding:0 36px 0 36px;font-family:sans-serif;font-size:1em;color: white;outline:0;border:0;background:url("../knobs/search.svg") left 13px center no-repeat;background-size:13px} \ No newline at end of file diff --git a/www/selector.min.js b/www/selector.min.js new file mode 100644 index 0000000..263e77d --- /dev/null +++ b/www/selector.min.js @@ -0,0 +1 @@ +function Selector(a){this.elements={};this.currentElement={};this.currentInstance={};this.config={selector:"select.selector-instance"};this.parameters=a;this.applyParameters();this.core()}Selector.prototype.applyParameters=function(){let allowedParameters=["selector"];for(let i in this.parameters){if(allowedParameters.indexOf(i)===-1){continue}this.config[i]=this.parameters[i]}};Selector.prototype.core=function(){this.getElements();for(let i=0;i + + + Simu Rack + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+
+
+

+ LJ nano +   +

+ + +
+
+
+ Simu Rack +
+
+ /team/laser +
+
+
+ Stt + + +
+
+ Ack + + +
+ +
+
+ + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + diff --git a/www/touch-icon-ipad-retina.png b/www/touch-icon-ipad-retina.png new file mode 100644 index 0000000..d492e49 Binary files /dev/null and b/www/touch-icon-ipad-retina.png differ diff --git a/www/touch-icon-ipad.png b/www/touch-icon-ipad.png new file mode 100644 index 0000000..e557f78 Binary files /dev/null and b/www/touch-icon-ipad.png differ diff --git a/www/touch-icon-iphone-retina.png b/www/touch-icon-iphone-retina.png new file mode 100644 index 0000000..83e4b30 Binary files /dev/null and b/www/touch-icon-iphone-retina.png differ diff --git a/www/webaudio-controls.js b/www/webaudio-controls.js new file mode 100644 index 0000000..d9fdb43 --- /dev/null +++ b/www/webaudio-controls.js @@ -0,0 +1,1875 @@ +/* * + * + * WebAudio-Controls is based on + * webaudio-knob by Eiji Kitamura http://google.com/+agektmr + * webaudio-slider by RYoya Kawai https://plus.google.com/108242669191458983485/posts + * webaudio-switch by Keisuke Ai http://d.hatena.ne.jp/aike/ + * Integrated and enhanced by g200kg http://www.g200kg.com/ + * + * Copyright 2013 Eiji Kitamura / Ryoya KAWAI / Keisuke Ai / g200kg(Tatsuya Shinyagaito) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * */ +if(window.customElements){ + let styles=document.createElement("style"); + styles.innerHTML= +`#webaudioctrl-context-menu { + display: none; + position: absolute; + z-index: 10; + padding: 0; + width: 100px; + color:#eee; + background-color: #268; + border: solid 1px #888; + box-shadow: 1px 1px 2px #888; + font-family: sans-serif; + font-size: 11px; + line-height:1.7em; + text-align:center; + cursor:pointer; + color:#fff; + list-style: none; +} +#webaudioctrl-context-menu.active { + display: block; +} +.webaudioctrl-context-menu__item { + display: block; + margin: 0; + padding: 0; + color: #000; + background-color:#eee; + text-decoration: none; +} +.webaudioctrl-context-menu__title{ + font-weight:bold; +} +.webaudioctrl-context-menu__item:last-child { + margin-bottom: 0; +} +.webaudioctrl-context-menu__item:hover { + background-color: #b8b8b8; +} +`; + document.head.appendChild(styles); + let midimenu=document.createElement("ul"); + midimenu.id="webaudioctrl-context-menu"; + midimenu.innerHTML= +`
  • MIDI Learn
  • +
  • Learn
  • +
  • Clear
  • +
  • Close
  • +`; + let opt={ + useMidi:0, + midilearn:0, + mididump:0, + outline:0, + knobSrc:null, + knobSprites:0, + knobWidth:0, + knobHeight:0, + knobDiameter:64, + knobColors:"#e00;#000;#000", + sliderSrc:null, + sliderKnobsrc:null, + sliderWidth:0, + sliderHeight:0, + sliderKnobwidth:0, + sliderKnobheight:0, + sliderDitchlength:0, + sliderColors:"#e00;#000;#fcc", + switchWidth:0, + switchHeight:0, + switchDiameter:24, + switchColors:"#e00;#000;#fcc", + paramWidth:32, + paramHeight:16, + paramColors:"#fff;#000", + xypadColors:"#e00;#000;#fcc", + }; + if(window.WebAudioControlsOptions) + Object.assign(opt,window.WebAudioControlsOptions); + class WebAudioControlsWidget extends HTMLElement{ + constructor(){ + super(); + this.addEventListener("keydown",this.keydown); + this.addEventListener("mousedown",this.pointerdown,{passive:false}); + this.addEventListener("touchstart",this.pointerdown,{passive:false}); + this.addEventListener("wheel",this.wheel); + this.addEventListener("mouseover",this.pointerover); + this.addEventListener("mouseout",this.pointerout); + this.addEventListener("contextmenu",this.contextMenu); + this.hover=this.drag=0; + document.body.appendChild(midimenu); + this.basestyle=` +.webaudioctrl-tooltip{ + display:inline-block; + position:absolute; + margin:0 -1000px; + z-index: 999; + background:#eee; + color:#000; + border:1px solid #666; + border-radius:4px; + padding:5px 10px; + text-align:center; + left:0; top:0; + font-size:11px; + opacity:0; + visibility:hidden; +} +.webaudioctrl-tooltip:before{ + content: ""; + position: absolute; + top: 100%; + left: 50%; + margin-left: -8px; + border: 8px solid transparent; + border-top: 8px solid #666; +} +.webaudioctrl-tooltip:after{ + content: ""; + position: absolute; + top: 100%; + left: 50%; + margin-left: -6px; + border: 6px solid transparent; + border-top: 6px solid #eee; +} +`; + } + sendEvent(ev){ + let event; + event=document.createEvent("HTMLEvents"); + event.initEvent(ev,false,true); + this.dispatchEvent(event); + } + getAttr(n,def){ + let v=this.getAttribute(n); + if(v==""||v==null) return def; + switch(typeof(def)){ + case "number": + if(v=="true") return 1; + v=+v; + if(isNaN(v)) return 0; + return v; + } + return v; + } + showtip(d){ + function valstr(x,c,type){ + switch(type){ + case "x": return (x|0).toString(16); + case "X": return (x|0).toString(16).toUpperCase(); + case "d": return (x|0).toString(); + case "f": return x.toFixed(c); + case "s": return x.toString(); + } + return ""; + } + function numformat(s,x){ + if(typeof(x)=="undefined") + return; + let i=s.indexOf("%"); + let c=[0,0],type=0,m=0,r="",j=i+1; + for(;j=0){ + type=s[j]; + break; + } + if(s[j]==".") + m=1; + else + c[m]=c[m]*10+parseInt(s[j]); + } + if(typeof(x)=="number") + r=valstr(x,c[1],type); + else + r=valstr(x.x,c[1],type)+","+valstr(x.y,c[1],type); + if(c[0]>0) + r=(" "+r).slice(-c[0]); + r=s.replace(/%.*[xXdfs]/,r); + return r; + } + let s=this.tooltip; + if(this.drag||this.hover){ + if(this.valuetip){ + if(s==null) + s=`%.${this.digits}f`; + else if(s.indexOf("%")<0) + s+=` : %.${this.digits}f`; + } + if(s){ + this.ttframe.innerHTML=numformat(s,this.convValue); + this.ttframe.style.display="inline-block"; + this.ttframe.style.width="auto"; + this.ttframe.style.height="auto"; + this.ttframe.style.transition="opacity 0.5s "+d+"s,visibility 0.5s "+d+"s"; + this.ttframe.style.opacity=0.9; + this.ttframe.style.visibility="visible"; + let rc=this.getBoundingClientRect(),rc2=this.ttframe.getBoundingClientRect(),rc3=document.documentElement.getBoundingClientRect(); + this.ttframe.style.left=((rc.width-rc2.width)*0.5+1000)+"px"; + this.ttframe.style.top=(-rc2.height-8)+"px"; + return; + } + } + this.ttframe.style.transition="opacity 0.1s "+d+"s,visibility 0.1s "+d+"s"; + this.ttframe.style.opacity=0; + this.ttframe.style.visibility="hidden"; + } + pointerover(e) { + this.hover=1; + this.showtip(0.6); + } + pointerout(e) { + this.hover=0; + this.showtip(0); + } + contextMenu(e){ + if(window.webAudioControlsMidiManager && this.midilearn) + webAudioControlsMidiManager.contextMenuOpen(e,this); + e.preventDefault(); + e.stopPropagation(); + } + setMidiController(channel, cc) { + if (this.listeningToThisMidiController(channel, cc)) return; + this.midiController={ 'channel': channel, 'cc': cc}; + console.log("Added mapping for channel=" + channel + " cc=" + cc + " tooltip=" + this.tooltip); + } + listeningToThisMidiController(channel, cc) { + const c = this.midiController; + if((c.channel === channel || c.channel < 0) && c.cc === cc) + return true; + return false; + } + processMidiEvent(event){ + const channel = event.data[0] & 0xf; + const controlNumber = event.data[1]; + if(this.midiMode == 'learn') { + this.setMidiController(channel, controlNumber); + webAudioControlsMidiManager.contextMenuClose(); + this.midiMode = 'normal'; + } + if(this.listeningToThisMidiController(channel, controlNumber)) { + if(this.tagName=="WEBAUDIO-SWITCH"){ + switch(this.type){ + case "toggle": + if(event.data[2]>=64) + this.setValue(1-this.value,true); + break; + case "kick": + this.setValue(event.data[2]>=64?1:0); + break; + case "radio": + let els=document.querySelectorAll("webaudio-switch[type='radio'][group='"+this.group+"']"); + for(let i=0;i +${this.basestyle} +webaudio-knob{ + display:inline-block; + position:relative; + margin:0; + padding:0; + cursor:pointer; + font-family: sans-serif; + font-size: 11px; +} +.webaudio-knob-body{ + display:inline-block; + position:relative; + margin:0; + padding:0; + vertical-align:bottom; +} + +
    +`; + this.elem=root.childNodes[2]; + this.ttframe=root.childNodes[3]; + this.enable=this.getAttr("enable",1); + this._src=this.getAttr("src",opt.knobSrc); Object.defineProperty(this,"src",{get:()=>{return this._src},set:(v)=>{this._src=v;this.setupImage()}}); + this._value=this.getAttr("value",0); Object.defineProperty(this,"value",{get:()=>{return this._value},set:(v)=>{this._value=v;this.redraw()}}); + this.defvalue=this.getAttr("defvalue",0); + this._min=this.getAttr("min",0); Object.defineProperty(this,"min",{get:()=>{return this._min},set:(v)=>{this._min=+v;this.redraw()}}); + this._max=this.getAttr("max",100); Object.defineProperty(this,"max",{get:()=>{return this._max},set:(v)=>{this._max=+v;this.redraw()}}); + this._step=this.getAttr("step",1); Object.defineProperty(this,"step",{get:()=>{return this._step},set:(v)=>{this._step=+v;this.redraw()}}); + this._sprites=this.getAttr("sprites",opt.knobSprites); Object.defineProperty(this,"sprites",{get:()=>{return this._sprites},set:(v)=>{this._sprites=v;this.setupImage()}}); + this._width=this.getAttr("width",opt.knobWidth); Object.defineProperty(this,"width",{get:()=>{return this._width},set:(v)=>{this._width=v;this.setupImage()}}); + this._height=this.getAttr("height",opt.knobHeight); Object.defineProperty(this,"height",{get:()=>{return this._height},set:(v)=>{this._height=v;this.setupImage()}}); + this._diameter=this.getAttr("diameter",opt.knobDiameter); Object.defineProperty(this,"diameter",{get:()=>{return this._diameter},set:(v)=>{this._diameter=v;this.setupImage()}}); + this._colors=this.getAttr("colors",opt.knobColors); Object.defineProperty(this,"colors",{get:()=>{return this._colors},set:(v)=>{this._colors=v;this.setupImage()}}); + this.outline=this.getAttr("outline",opt.outline); + this.sensitivity=this.getAttr("sensitivity",1); + this.valuetip=this.getAttr("valuetip",1); + this.tooltip=this.getAttr("tooltip",null); + this.conv=this.getAttr("conv",null); + if(this.conv) + this.convValue=eval(this.conv)(this._value); + else + this.convValue=this._value; + this.midilearn=this.getAttr("midilearn",opt.midilearn); + this.midicc=this.getAttr("midicc",null); + + this.midiController={}; + this.midiMode="normal"; + if(this.midicc) { + let ch = parseInt(this.midicc.substring(0, this.midicc.lastIndexOf("."))) - 1; + let cc = parseInt(this.midicc.substring(this.midicc.lastIndexOf(".") + 1)); + this.setMidiController(ch, cc); + } + this.setupImage(); + this.digits=0; + this.coltab=["#e00","#000","#000"]; + if(window.webAudioControlsMidiManager) +// window.webAudioControlsMidiManager.updateWidgets(); + window.webAudioControlsMidiManager.addWidget(this); + } + disconnectedCallback(){} + setupImage(){ + this.kw=this.width||this.diameter; + this.kh=this.height||this.diameter; + if(!this.src){ + if(this.colors) + this.coltab = this.colors.split(";"); + if(!this.coltab) + this.coltab=["#e00","#000","#000"]; + let svg= +` + + +`; + for(let i=0;i<101;++i){ + svg += ``; + svg += ``; + } + svg += ""; + this.elem.style.backgroundImage = "url(data:image/svg+xml;base64,"+btoa(svg)+")"; +// this.elem.style.backgroundSize = "100% 10100%"; + this.elem.style.backgroundSize = `${this.kw}px ${this.kh*101}px`; + } + else{ + this.elem.style.backgroundImage = "url("+(this.src)+")"; + if(!this.sprites) + this.elem.style.backgroundSize = "100% 100%"; + else{ +// this.elem.style.backgroundSize = `100% ${(this.sprites+1)*100}%`; + this.elem.style.backgroundSize = `${this.kw}px ${this.kh*(this.sprites+1)}px`; + } + } + this.elem.style.outline=this.outline?"":"none"; + this.elem.style.width=this.kw+"px"; + this.elem.style.height=this.kh+"px"; + this.style.height=this.kh+"px"; + this.redraw(); + } + redraw() { + this.digits=0; + if(this.step && this.step < 1) { + for(let n = this.step ; n < 1; n *= 10) + ++this.digits; + } + if(this.valuethis.max){ + this.value=this.max; + return; + } + let range = this.max - this.min; + let style = this.elem.style; + let sp = this.src?this.sprites:100; + if(sp>=1){ + let offset = ((sp * (this.value - this.min) / range) | 0); + style.backgroundPosition = "0px " + (-offset*this.kh) + "px"; + style.transform = 'rotate(0deg)'; + } else { + let deg = 270 * ((this.value - this.min) / range - 0.5); + style.backgroundPosition="0px 0px"; + style.transform = 'rotate(' + deg + 'deg)'; + } + } + _setValue(v){ + if(this.step) + v=(Math.round((v-this.min)/this.step))*this.step+this.min; + this._value=Math.min(this.max,Math.max(this.min,v)); + if(this._value!=this.oldvalue){ + this.oldvalue=this._value; + if(this.conv) + this.convValue=eval(this.conv)(this._value); + else + this.convValue=this._value; + this.redraw(); + this.showtip(0); + return 1; + } + return 0; + } + setValue(v,f){ + if(this._setValue(v) && f) + this.sendEvent("input"),this.sendEvent("change"); + } + wheel(e) { + let delta=(this.max-this.min)*0.01; + delta=e.deltaY>0?-delta:delta; + if(!e.shiftKey) + delta*=5; + if(Math.abs(delta) < this.step) + delta = (delta > 0) ? +this.step : -this.step; + this.setValue(+this.value+delta,true); + e.preventDefault(); + e.stopPropagation(); + } + pointerdown(ev){ + if(!this.enable) + return; + let e=ev; + if(ev.touches){ + e = ev.changedTouches[0]; + this.identifier=e.identifier; + } + else { + if(e.buttons!=1 && e.button!=0) + return; + } + this.elem.focus(); + this.drag=1; + this.showtip(0); + let pointermove=(ev)=>{ + let e=ev; + if(ev.touches){ + for(let i=0;i{ + let e=ev; + if(ev.touches){ + for(let i=0;;){ + if(ev.changedTouches[i].identifier==this.identifier){ + break; + } + if(++i>=ev.changedTouches.length) + return; + } + } + this.drag=0; + this.showtip(0); + this.startPosX = this.startPosY = null; + window.removeEventListener('mousemove', pointermove); + window.removeEventListener('touchmove', pointermove, {passive:false}); + window.removeEventListener('mouseup', pointerup); + window.removeEventListener('touchend', pointerup); + window.removeEventListener('touchcancel', pointerup); + document.body.removeEventListener('touchstart', preventScroll,{passive:false}); + this.sendEvent("change"); + } + let preventScroll=(e)=>{ + e.preventDefault(); + } + if(e.ctrlKey || e.metaKey) + this.setValue(this.defvalue,true); + else { + this.startPosX = e.pageX; + this.startPosY = e.pageY; + this.startVal = this.value; + window.addEventListener('mousemove', pointermove); + window.addEventListener('touchmove', pointermove, {passive:false}); + } + window.addEventListener('mouseup', pointerup); + window.addEventListener('touchend', pointerup); + window.addEventListener('touchcancel', pointerup); + document.body.addEventListener('touchstart', preventScroll,{passive:false}); + ev.preventDefault(); + ev.stopPropagation(); + return false; + } + }); +} catch(error){ + console.log("webaudio-knob already defined"); +} + +try{ + customElements.define("webaudio-slider", class WebAudioSlider extends WebAudioControlsWidget { + constructor(){ + super(); + } + connectedCallback(){ + let root; +// if(this.attachShadow) +// root=this.attachShadow({mode: 'open'}); +// else + root=this; + root.innerHTML= +` +
    +`; + this.elem=root.childNodes[2]; + this.knob=this.elem.childNodes[0]; + this.ttframe=root.childNodes[3]; + this.enable=this.getAttr("enable",1); + this._src=this.getAttr("src",opt.sliderSrc); Object.defineProperty(this,"src",{get:()=>{return this._src},set:(v)=>{this._src=v;this.setupImage()}}); + this._knobsrc=this.getAttr("knobsrc",opt.sliderKnobsrc); Object.defineProperty(this,"knobsrc",{get:()=>{return this._knobsrc},set:(v)=>{this._knobsrc=v;this.setupImage()}}); + this._value=this.getAttr("value",0); Object.defineProperty(this,"value",{get:()=>{return this._value},set:(v)=>{this._value=v;this.redraw()}}); + this.defvalue=this.getAttr("defvalue",0); + this._min=this.getAttr("min",0); Object.defineProperty(this,"min",{get:()=>{return this._min},set:(v)=>{this._min=v;this.redraw()}}); + this._max=this.getAttr("max",100); Object.defineProperty(this,"max",{get:()=>{return this._max},set:(v)=>{this._max=v;this.redraw()}}); + this._step=this.getAttr("step",1); Object.defineProperty(this,"step",{get:()=>{return this._step},set:(v)=>{this._step=v;this.redraw()}}); + this._sprites=this.getAttr("sprites",0); Object.defineProperty(this,"sprites",{get:()=>{return this._sprites},set:(v)=>{this._sprites=v;this.setupImage()}}); + this._direction=this.getAttr("direction",null); Object.defineProperty(this,"direction",{get:()=>{return this._direction},set:(v)=>{this._direction=v;this.setupImage()}}); + this._width=this.getAttr("width",opt.sliderWidth); Object.defineProperty(this,"width",{get:()=>{return this._width},set:(v)=>{this._width=v;this.setupImage()}}); + this._height=this.getAttr("height",opt.sliderHeight); Object.defineProperty(this,"height",{get:()=>{return this._height},set:(v)=>{this._height=v;this.setupImage()}}); + if(this._direction=="horz"){ + if(this._width==0) this._width=128; + if(this._height==0) this._height=24; + } + else{ + if(this._width==0) this._width=24; + if(this._height==0) this._height=128; + } + this._knobwidth=this.getAttr("knobwidth",opt.sliderKnobwidth); Object.defineProperty(this,"knobwidth",{get:()=>{return this._knobwidth},set:(v)=>{this._knobwidth=v;this.setupImage()}}); + this._knobheight=this.getAttr("knbheight",opt.sliderKnobheight); Object.defineProperty(this,"knobheight",{get:()=>{return this._knobheight},set:(v)=>{this._knobheight=v;this.setupImage()}}); + this._ditchlength=this.getAttr("ditchlength",opt.sliderDitchlength); Object.defineProperty(this,"ditchlength",{get:()=>{return this._ditchlength},set:(v)=>{this._ditchlength=v;this.setupImage()}}); + this._colors=this.getAttr("colors",opt.sliderColors); Object.defineProperty(this,"colors",{get:()=>{return this._colors},set:(v)=>{this._colors=v;this.setupImage()}}); + this.outline=this.getAttr("outline",opt.outline); + this.sensitivity=this.getAttr("sensitivity",1); + this.valuetip=this.getAttr("valuetip",1); + this.tooltip=this.getAttr("tooltip",null); + this.conv=this.getAttr("conv",null); + if(this.conv) + this.convValue=eval(this.conv)(this._value); + else + this.convValue=this._value; + this.midilearn=this.getAttr("midilearn",opt.midilearn); + this.midicc=this.getAttr("midicc",null); + this.midiController={}; + this.midiMode="normal"; + if(this.midicc) { + let ch = parseInt(this.midicc.substring(0, this.midicc.lastIndexOf("."))) - 1; + let cc = parseInt(this.midicc.substring(this.midicc.lastIndexOf(".") + 1)); + this.setMidiController(ch, cc); + } + this.setupImage(); + this.digits=0; + if(window.webAudioControlsMidiManager) +// window.webAudioControlsMidiManager.updateWidgets(); + window.webAudioControlsMidiManager.addWidget(this); + this.elem.onclick=(e)=>{e.stopPropagation()}; + } + disconnectedCallback(){} + setupImage(){ + this.coltab = this.colors.split(";"); + this.dr=this.direction; + this.dlen=this.ditchlength; + if(!this.width){ + if(this.dr=="horz") + this.width=128; + else + this.width=24; + } + if(!this.height){ + if(this.dr=="horz") + this.height=24; + else + this.height=128; + } + if(!this.dr) + this.dr=(this.width<=this.height)?"vert":"horz"; + if(this.dr=="vert"){ + if(!this.dlen) + this.dlen=this.height-this.width; + } + else{ + if(!this.dlen) + this.dlen=this.width-this.height; + } + this.knob.style.backgroundSize = "100% 100%"; + this.elem.style.backgroundSize = "100% 100%"; + this.elem.style.width=this.width+"px"; + this.elem.style.height=this.height+"px"; + this.style.height=this.height+"px"; + this.kwidth=this.knobwidth||(this.dr=="horz"?this.height:this.width); + this.kheight=this.knobheight||(this.dr=="horz"?this.height:this.width); + this.knob.style.width = this.kwidth+"px"; + this.knob.style.height = this.kheight+"px"; + if(!this.src){ + let r=Math.min(this.width,this.height)*0.5; + let svgbody= +` +`; + this.elem.style.backgroundImage = "url(data:image/svg+xml;base64,"+btoa(svgbody)+")"; + } + else{ + this.elem.style.backgroundImage = "url("+(this.src)+")"; + } + if(!this.knobsrc){ + let svgthumb= +` + +`; + this.knob.style.backgroundImage = "url(data:image/svg+xml;base64,"+btoa(svgthumb)+")"; + } + else{ + this.knob.style.backgroundImage = "url("+(this.knobsrc)+")"; + } + this.elem.style.outline=this.outline?"":"none"; + this.redraw(); + } + redraw() { + this.digits=0; + if(this.step && this.step < 1) { + for(let n = this.step ; n < 1; n *= 10) + ++this.digits; + } + if(this.valuethis.max){ + this.value=this.max; + return; + } + let range = this.max - this.min; + let style = this.knob.style; + if(this.dr=="vert"){ + style.left=(this.width-this.kwidth)*0.5+"px"; + style.top=(1-(this.value-this.min)/range)*this.dlen+"px"; + this.sensex=0; this.sensey=1; + } + else{ + style.top=(this.height-this.kheight)*0.5+"px"; + style.left=(this.value-this.min)/range*this.dlen+"px"; + this.sensex=1; this.sensey=0; + } + } + _setValue(v){ + v=(Math.round((v-this.min)/this.step))*this.step+this.min; + this._value=Math.min(this.max,Math.max(this.min,v)); + if(this._value!=this.oldvalue){ + this.oldvalue=this._value; + if(this.conv) + this.convValue=eval(this.conv)(this._value); + else + this.convValue=this._value; + this.redraw(); + this.showtip(0); + return 1; + } + return 0; + } + setValue(v,f){ + if(this._setValue(v)&&f) + this.sendEvent("input"),this.sendEvent("change"); + } + wheel(e) { + let delta=(this.max-this.min)*0.01; + delta=e.deltaY>0?-delta:delta; + if(!e.shiftKey) + delta*=5; + if(Math.abs(delta) < this.step) + delta = (delta > 0) ? +this.step : -this.step; + this.setValue(+this.value+delta,true); + e.preventDefault(); + e.stopPropagation(); + this.redraw(); + } + pointerdown(ev){ + if(!this.enable) + return; + let e=ev; + if(ev.touches){ + e = ev.changedTouches[0]; + this.identifier=e.identifier; + } + else { + if(e.buttons!=1 && e.button!=0) + return; + } + this.elem.focus(); + this.drag=1; + this.showtip(0); + let pointermove=(ev)=>{ + let e=ev; + if(ev.touches){ + for(let i=0;i{ + let e=ev; + if(ev.touches){ + for(let i=0;;){ + if(ev.changedTouches[i].identifier==this.identifier){ + break; + } + if(++i>=ev.changedTouches.length) + return; + } + } + this.drag=0; + this.showtip(0); + this.startPosX = this.startPosY = null; + window.removeEventListener('mousemove', pointermove); + window.removeEventListener('touchmove', pointermove, {passive:false}); + window.removeEventListener('mouseup', pointerup); + window.removeEventListener('touchend', pointerup); + window.removeEventListener('touchcancel', pointerup); + document.body.removeEventListener('touchstart', preventScroll,{passive:false}); + this.sendEvent("change"); + } + let preventScroll=(e)=>{ + e.preventDefault(); + } + if(e.touches) + e = e.touches[0]; + if(e.ctrlKey || e.metaKey) + this.setValue(this.defvalue,true); + else { + this.startPosX = e.pageX; + this.startPosY = e.pageY; + this.startVal = this.value; + window.addEventListener('mousemove', pointermove); + window.addEventListener('touchmove', pointermove, {passive:false}); + } + window.addEventListener('mouseup', pointerup); + window.addEventListener('touchend', pointerup); + window.addEventListener('touchcancel', pointerup); + document.body.addEventListener('touchstart', preventScroll,{passive:false}); + e.preventDefault(); + e.stopPropagation(); + return false; + } + }); +} catch(error){ + console.log("webaudio-slider already defined"); +} + +try{ + customElements.define("webaudio-switch", class WebAudioSwitch extends WebAudioControlsWidget { + constructor(){ + super(); + } + connectedCallback(){ + let root; +// if(this.attachShadow) +// root=this.attachShadow({mode: 'open'}); +// else + root=this; + root.innerHTML= +` +
    +`; + this.elem=root.childNodes[2]; + this.ttframe=this.elem.childNodes[0]; + + this.enable=this.getAttr("enable",1); + this._src=this.getAttr("src",null); Object.defineProperty(this,"src",{get:()=>{return this._src},set:(v)=>{this._src=v;this.setupImage()}}); + this._value=this.getAttr("value",0); Object.defineProperty(this,"value",{get:()=>{return this._value},set:(v)=>{this._value=v;this.redraw()}}); + this.defvalue=this.getAttr("defvalue",0); + this.type=this.getAttr("type","toggle"); + this.group=this.getAttr("group",""); + this._width=this.getAttr("width",0); Object.defineProperty(this,"width",{get:()=>{return this._width},set:(v)=>{this._width=v;this.setupImage()}}); + this._height=this.getAttr("height",0); Object.defineProperty(this,"height",{get:()=>{return this._height},set:(v)=>{this._height=v;this.setupImage()}}); + this._diameter=this.getAttr("diameter",0); Object.defineProperty(this,"diameter",{get:()=>{return this._diameter},set:(v)=>{this._diameter=v;this.setupImage()}}); + this.invert=this.getAttr("invert",0); + this._colors=this.getAttr("colors",opt.switchColors); Object.defineProperty(this,"colors",{get:()=>{return this._colors},set:(v)=>{this._colors=v;this.setupImage()}}); + this.outline=this.getAttr("outline",opt.outline); + this.valuetip=0; + this.tooltip=this.getAttr("tooltip",null); + this.midilearn=this.getAttr("midilearn",opt.midilearn); + this.midicc=this.getAttr("midicc",null); + this.midiController={}; + this.midiMode="normal"; + if(this.midicc) { + let ch = parseInt(this.midicc.substring(0, this.midicc.lastIndexOf("."))) - 1; + let cc = parseInt(this.midicc.substring(this.midicc.lastIndexOf(".") + 1)); + this.setMidiController(ch, cc); + } + this.setupImage(); + this.digits=0; + if(window.webAudioControlsMidiManager) +// window.webAudioControlsMidiManager.updateWidgets(); + window.webAudioControlsMidiManager.addWidget(this); + this.elem.onclick=(e)=>{e.stopPropagation()}; + } + disconnectedCallback(){} + setupImage(){ + let w=this.width||this.diameter||opt.switchWidth||opt.switchDiameter; + let h=this.height||this.diameter||opt.switchHeight||opt.switchDiameter; + if(!this.src){ + this.coltab = this.colors.split(";"); + let mm=Math.min(w,h); + let svg= +` + + + + +`; + this.elem.style.backgroundImage = "url(data:image/svg+xml;base64,"+btoa(svg)+")"; + this.elem.style.backgroundSize = "100% 200%"; + } + else{ + this.elem.style.backgroundImage = "url("+(this.src)+")"; + if(!this.sprites) + this.elem.style.backgroundSize = "100% 200%"; + else + this.elem.style.backgroundSize = `100% ${(this.sprites+1)*100}%`; + } + this.elem.style.width=w+"px"; + this.elem.style.height=h+"px"; + this.style.height=h+"px"; + this.elem.style.outline=this.outline?"":"none"; + this.redraw(); + } + redraw() { + let style = this.elem.style; + if(this.value^this.invert) + style.backgroundPosition = "0px -100%"; + else + style.backgroundPosition = "0px 0px"; + } + setValue(v,f){ + this.value=v; + this.checked=(!!v); + if(this.value!=this.oldvalue){ + this.redraw(); + this.showtip(0); + if(f){ + this.sendEvent("input"); + this.sendEvent("change"); + } + this.oldvalue=this.value; + } + } + pointerdown(ev){ + if(!this.enable) + return; + let e=ev; + if(ev.touches){ + e = ev.changedTouches[0]; + this.identifier=e.identifier; + } + else { + if(e.buttons!=1 && e.button!=0) + return; + } + this.elem.focus(); + this.drag=1; + this.showtip(0); + let pointermove=(e)=>{ + e.preventDefault(); + e.stopPropagation(); + return false; + } + let pointerup=(e)=>{ + this.drag=0; + this.showtip(0); + window.removeEventListener('mousemove', pointermove); + window.removeEventListener('touchmove', pointermove, {passive:false}); + window.removeEventListener('mouseup', pointerup); + window.removeEventListener('touchend', pointerup); + window.removeEventListener('touchcancel', pointerup); + document.body.removeEventListener('touchstart', preventScroll,{passive:false}); + if(this.type=="kick"){ + this.value=0; + this.checked=false; + this.redraw(); + this.sendEvent("change"); + } + this.sendEvent("click"); + e.preventDefault(); + e.stopPropagation(); + } + let preventScroll=(e)=>{ + e.preventDefault(); + } + switch(this.type){ + case "kick": + this.setValue(1); + this.sendEvent("change"); + break; + case "toggle": + if(e.ctrlKey || e.metaKey) + this.value=defvalue; + else + this.value=1-this.value; + this.checked=!!this.value; + this.sendEvent("change"); + break; + case "radio": + let els=document.querySelectorAll("webaudio-switch[type='radio'][group='"+this.group+"']"); + for(let i=0;i +${this.basestyle} +webaudio-param{ + display:inline-block; + user-select:none; + margin:0; + padding:0; + font-family: sans-serif; + font-size: 8px; + cursor:pointer; + position:relative; + vertical-align:baseline; +} +.webaudio-param-body{ + display:inline-block; + position:relative; + text-align:center; + border:1px solid #888; + background:none; + border-radius:4px; + margin:0; + padding:0; + font-family:sans-serif; + font-size:11px; + vertical-align:bottom; +} + +
    +`; + this.elem=root.childNodes[2]; + this.ttframe=root.childNodes[3]; + this.enable=this.getAttr("enable",1); + this._value=this.getAttr("value",0); Object.defineProperty(this,"value",{get:()=>{return this._value},set:(v)=>{this._value=v;this.redraw()}}); + this.defvalue=this.getAttr("defvalue",0); + this._fontsize=this.getAttr("fontsize",9); Object.defineProperty(this,"fontsize",{get:()=>{return this._fontsize},set:(v)=>{this._fontsize=v;this.setupImage()}}); + this._src=this.getAttr("src",null); Object.defineProperty(this,"src",{get:()=>{return this._src},set:(v)=>{this._src=v;this.setupImage()}}); + this.link=this.getAttr("link",""); + this._width=this.getAttr("width",32); Object.defineProperty(this,"width",{get:()=>{return this._width},set:(v)=>{this._width=v;this.setupImage()}}); + this._height=this.getAttr("height",20); Object.defineProperty(this,"height",{get:()=>{return this._height},set:(v)=>{this._height=v;this.setupImage()}}); + this._colors=this.getAttr("colors","#fff;#000"); Object.defineProperty(this,"colors",{get:()=>{return this._colors},set:(v)=>{this._colors=v;this.setupImage()}}); + this.outline=this.getAttr("outline",opt.outline); + this.midiController={}; + this.midiMode="normal"; + if(this.midicc) { + let ch = parseInt(this.midicc.substring(0, this.midicc.lastIndexOf("."))) - 1; + let cc = parseInt(this.midicc.substring(this.midicc.lastIndexOf(".") + 1)); + this.setMidiController(ch, cc); + } + this.setupImage(); + if(window.webAudioControlsMidiManager) +// window.webAudioControlsMidiManager.updateWidgets(); + window.webAudioControlsMidiManager.addWidget(this); + this.fromLink=((e)=>{ + this.setValue(e.target.convValue.toFixed(e.target.digits)); + }).bind(this); + this.elem.onchange=()=>{ + this.value=this.elem.value; + let le=document.getElementById(this.link); + if(le) + le.setValue(+this.elem.value); + } + } + disconnectedCallback(){} + setupImage(){ + this.coltab = this.colors.split(";"); + this.elem.style.color=this.coltab[0]; + if(!this.src){ + this.elem.style.backgroundColor=this.coltab[1]; + } + else{ + this.elem.style.backgroundImage = "url("+(this.src)+")"; + this.elem.style.backgroundSize = "100% 100%"; + } + this.elem.style.width=this.width+"px"; + this.elem.style.height=this.height+"px"; + this.elem.style.fontSize=this.fontsize+"px"; + this.elem.style.outline=this.outline?"":"none"; + let l=document.getElementById(this.link); + if(l&&typeof(l.value)!="undefined"){ + this.setValue(l.value.toFixed(l.digits)); + l.addEventListener("input",(e)=>{this.setValue(l.value.toFixed(l.digits))}); + } + this.redraw(); + } + redraw() { + this.elem.value=this.value; + } + setValue(v,f){ + this.value=v; + if(this.value!=this.oldvalue){ + this.redraw(); + this.showtip(0); + if(f){ + let event=document.createEvent("HTMLEvents"); + event.initEvent("change",false,true); + this.dispatchEvent(event); + } + this.oldvalue=this.value; + } + } + pointerdown(ev){ + if(!this.enable) + return; + let e=ev; + if(ev.touches) + e = ev.touches[0]; + else { + if(e.buttons!=1 && e.button!=0) + return; + } + this.elem.focus(); + this.redraw(); + } + }); +} catch(error){ + console.log("webaudio-param already defined"); +} + +try{ + customElements.define("webaudio-keyboard", class WebAudioKeyboard extends WebAudioControlsWidget { + constructor(){ + super(); + } + connectedCallback(){ + let root; +// if(this.attachShadow) +// root=this.attachShadow({mode: 'open'}); +// else + root=this; + root.innerHTML= +` +
    +`; + this.cv=root.childNodes[2]; + this.ttframe=root.childNodes[3]; + this.ctx=this.cv.getContext("2d"); + this._values=[]; + this.enable=this.getAttr("enable",1); + this._width=this.getAttr("width",480); Object.defineProperty(this,"width",{get:()=>{return this._width},set:(v)=>{this._width=v;this.setupImage()}}); + this._height=this.getAttr("height",128); Object.defineProperty(this,"height",{get:()=>{return this._height},set:(v)=>{this._height=v;this.setupImage()}}); + this._min=this.getAttr("min",0); Object.defineProperty(this,"min",{get:()=>{return this._min},set:(v)=>{this._min=+v;this.redraw()}}); + this._keys=this.getAttr("keys",25); Object.defineProperty(this,"keys",{get:()=>{return this._keys},set:(v)=>{this._keys=+v;this.setupImage()}}); + this._colors=this.getAttr("colors","#222;#eee;#ccc;#333;#000;#e88;#c44;#c33;#800"); Object.defineProperty(this,"colors",{get:()=>{return this._colors},set:(v)=>{this._colors=v;this.setupImage()}}); + this.outline=this.getAttr("outline",opt.outline); + this.midilearn=this.getAttr("midilearn",0); + this.midicc=this.getAttr("midicc",null); + this.press=0; + this.keycodes1=[90,83,88,68,67,86,71,66,72,78,74,77,188,76,190,187,191,226]; + this.keycodes2=[81,50,87,51,69,82,53,84,54,89,55,85,73,57,79,48,80,192,222,219]; + this.addEventListener("keyup",this.keyup); + this.midiController={}; + this.midiMode="normal"; + if(this.midicc) { + let ch = parseInt(this.midicc.substring(0, this.midicc.lastIndexOf("."))) - 1; + let cc = parseInt(this.midicc.substring(this.midicc.lastIndexOf(".") + 1)); + this.setMidiController(ch, cc); + } + this.setupImage(); + this.digits=0; + if(window.webAudioControlsMidiManager) + window.webAudioControlsMidiManager.addWidget(this); + } + disconnectedCallback(){} + setupImage(){ + this.cv.style.width=this.width+"px"; + this.cv.style.height=this.height+"px"; + this.bheight = this.height * 0.55; + this.kp=[0,7/12,1,3*7/12,2,3,6*7/12,4,8*7/12,5,10*7/12,6]; + this.kf=[0,1,0,1,0,0,1,0,1,0,1,0]; + this.ko=[0,0,(7*2)/12-1,0,(7*4)/12-2,(7*5)/12-3,0,(7*7)/12-4,0,(7*9)/12-5,0,(7*11)/12-6]; + this.kn=[0,2,4,5,7,9,11]; + this.coltab=this.colors.split(";"); + this.cv.width = this.width; + this.cv.height = this.height; + this.cv.style.width = this.width+'px'; + this.cv.style.height = this.height+'px'; + this.style.height = this.height+'px'; + this.cv.style.outline=this.outline?"":"none"; + this.bheight = this.height * 0.55; + this.max=this.min+this.keys-1; + this.dispvalues=[]; + this.valuesold=[]; + if(this.kf[this.min%12]) + --this.min; + if(this.kf[this.max%12]) + ++this.max; + this.redraw(); + } + redraw(){ + function rrect(ctx, x, y, w, h, r, c1, c2) { + if(c2) { + let g=ctx.createLinearGradient(x,y,x+w,y); + g.addColorStop(0,c1); + g.addColorStop(1,c2); + ctx.fillStyle=g; + } + else + ctx.fillStyle=c1; + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(x+w, y); + ctx.lineTo(x+w, y+h-r); + ctx.quadraticCurveTo(x+w, y+h, x+w-r, y+h); + ctx.lineTo(x+r, y+h); + ctx.quadraticCurveTo(x, y+h, x, y+h-r); + ctx.lineTo(x, y); + ctx.fill(); + } + this.ctx.fillStyle = this.coltab[0]; + this.ctx.fillRect(0,0,this.width,this.height); + let x0=7*((this.min/12)|0)+this.kp[this.min%12]; + let x1=7*((this.max/12)|0)+this.kp[this.max%12]; + let n=x1-x0; + this.wwidth=(this.width-1)/(n+1); + this.bwidth=this.wwidth*7/12; + let h2=this.bheight; + let r=Math.min(8,this.wwidth*0.2); + for(let i=this.min,j=0;i<=this.max;++i) { + if(this.kf[i%12]==0) { + let x=this.wwidth*(j++)+1; + if(this.dispvalues.indexOf(i)>=0) + rrect(this.ctx,x,1,this.wwidth-1,this.height-2,r,this.coltab[5],this.coltab[6]); + else + rrect(this.ctx,x,1,this.wwidth-1,this.height-2,r,this.coltab[1],this.coltab[2]); + } + } + r=Math.min(8,this.bwidth*0.3); + for(let i=this.min;i=0) + rrect(this.ctx,x,1,this.bwidth,h2,r,this.coltab[7],this.coltab[8]); + else + rrect(this.ctx,x,1,this.bwidth,h2,r,this.coltab[3],this.coltab[4]); + this.ctx.strokeStyle=this.coltab[0]; + this.ctx.stroke(); + } + } + } + _setValue(v){ + if(this.step) + v=(Math.round((v-this.min)/this.step))*this.step+this.min; + this._value=Math.min(this.max,Math.max(this.min,v)); + if(this._value!=this.oldvalue){ + this.oldvalue=this._value; + this.redraw(); + this.showtip(0); + return 1; + } + return 0; + } + setValue(v,f){ + if(this._setValue(v) && f) + this.sendEvent("input"),this.sendEvent("change"); + } + wheel(e){} + keydown(e){ + let m=Math.floor((this.min+11)/12)*12; + let k=this.keycodes1.indexOf(e.keyCode); + if(k<0) { + k=this.keycodes2.indexOf(e.keyCode); + if(k>=0) k+=12; + } + if(k>=0){ + k+=m; + if(this.currentKey!=k){ + this.currentKey=k; + this.sendEventFromKey(1,k); + this.setNote(1,k); + } + } + } + keyup(e){ + let m=Math.floor((this.min+11)/12)*12; + let k=this.keycodes1.indexOf(e.keyCode); + if(k<0) { + k=this.keycodes2.indexOf(e.keyCode); + if(k>=0) k+=12; + } + if(k>=0){ + k+=m; + this.currentKey=-1; + this.sendEventFromKey(0,k); + this.setNote(0,k); + } + } + pointerdown(ev){ + this.cv.focus(); + if(this.enable) { + ++this.press; + } + let pointermove=(ev)=>{ + if(!this.enable) + return; + let r=this.getBoundingClientRect(); + let v=[],p; + if(ev.touches) + p=ev.targetTouches; + else if(this.press) + p=[ev]; + else + p=[]; + if(p.length>0) + this.drag=1; + for(let i=0;i=0&&py=this.min&&k<=this.max) + v.push(k); + } + } + v.sort(); + this.values=v; + this.sendevent(); + this.redraw(); + } + + let pointerup=(ev)=>{ + if(this.enable) { + if(ev.touches) + this.press=ev.touches.length; + else + this.press=0; + pointermove(ev); + this.sendevent(); + if(this.press==0){ + window.removeEventListener('mousemove', pointermove); + window.removeEventListener('touchmove', pointermove, {passive:false}); + window.removeEventListener('mouseup', pointerup); + window.removeEventListener('touchend', pointerup); + window.removeEventListener('touchcancel', pointerup); + document.body.removeEventListener('touchstart', preventScroll,{passive:false}); + } + this.redraw(); + } + this.drag=0; + ev.preventDefault(); + } + let preventScroll=(ev)=>{ + ev.preventDefault(); + } + window.addEventListener('mousemove', pointermove); + window.addEventListener('touchmove', pointermove, {passive:false}); + window.addEventListener('mouseup', pointerup); + window.addEventListener('touchend', pointerup); + window.addEventListener('touchcancel', pointerup); + document.body.addEventListener('touchstart', preventScroll,{passive:false}); + pointermove(ev); + ev.preventDefault(); + ev.stopPropagation(); + } + sendEventFromKey(s,k){ + let ev=document.createEvent('HTMLEvents'); + ev.initEvent('change',true,true); + ev.note=[s,k]; + this.dispatchEvent(ev); + } + sendevent(){ + let notes=[]; + for(let i=0,j=this.valuesold.length;i=0) this.dispvalues.splice(n,1); + } + } + setNote(state,note) { + this.setdispvalues(state,note); + this.redraw(); + } + }); +} catch(error){ + console.log("webaudio-keyboard already defined"); +} + +try{ + customElements.define("webaudio-xypad", class WebAudioXYPad extends WebAudioControlsWidget { + constructor(){ + super(); + } + connectedCallback(){ + let root; +// if(this.attachShadow) +// root=this.attachShadow({mode: 'open'}); +// else + root=this; + root.innerHTML= +` +
    +`; + this.elem=root.childNodes[2]; + this.knob=this.elem.childNodes[0]; + this.ttframe=root.childNodes[3]; + + this.enable=this.getAttr("enable",1); + this._src=this.getAttr("src",opt.sliderSrc); Object.defineProperty(this,"src",{get:()=>{return this._src},set:(v)=>{this._src=v;this.setupImage()}}); + this._knobsrc=this.getAttr("knobsrc",opt.sliderKnobsrc); Object.defineProperty(this,"knobsrc",{get:()=>{return this._knobsrc},set:(v)=>{this._knobsrc=v;this.setupImage()}}); + this._x=this.getAttr("x",50); Object.defineProperty(this,"x",{get:()=>{return this._x},set:(v)=>{this._x=v;this.redraw()}}); + this._y=this.getAttr("y",50); Object.defineProperty(this,"y",{get:()=>{return this._y},set:(v)=>{this._y=v;this.redraw()}}); + this.defx=this.getAttr("defx",50); + this.defy=this.getAttr("defy",50); + this._min=this.getAttr("min",0); Object.defineProperty(this,"min",{get:()=>{return this._min},set:(v)=>{this._min=v;this.redraw()}}); + this._max=this.getAttr("max",100); Object.defineProperty(this,"max",{get:()=>{return this._max},set:(v)=>{this._max=v;this.redraw()}}); + this._step=this.getAttr("step",1); Object.defineProperty(this,"step",{get:()=>{return this._step},set:(v)=>{this._step=v;this.redraw()}}); + this._sprites=this.getAttr("sprites",0); Object.defineProperty(this,"sprites",{get:()=>{return this._sprites},set:(v)=>{this._sprites=v;this.setupImage()}}); + this._width=this.getAttr("width",128); Object.defineProperty(this,"width",{get:()=>{return this._width},set:(v)=>{this._width=v;this.setupImage()}}); + this._height=this.getAttr("height",128); Object.defineProperty(this,"height",{get:()=>{return this._height},set:(v)=>{this._height=v;this.setupImage()}}); + this._knobwidth=this.getAttr("knobwidth",28); Object.defineProperty(this,"knobwidth",{get:()=>{return this._knobwidth},set:(v)=>{this._knobwidth=v;this.setupImage()}}); + this._knobheight=this.getAttr("knbheight",28); Object.defineProperty(this,"knobheight",{get:()=>{return this._knobheight},set:(v)=>{this._knobheight=v;this.setupImage()}}); + this._colors=this.getAttr("colors",opt.sliderColors); Object.defineProperty(this,"colors",{get:()=>{return this._colors},set:(v)=>{this._colors=v;this.setupImage()}}); + this.outline=this.getAttr("outline",opt.outline); + this.valuetip=this.getAttr("valuetip",1); + this.tooltip=this.getAttr("tooltip",null); + this.conv=this.getAttr("conv",null); + if(this.conv){ + this.convValue={x:eval(this.conv)(this._x),y:eval(this.conv)(this._y)}; + } + else + this.convValue={x:this._x,y:this._y}; + this.midilearn=this.getAttr("midilearn",opt.midilearn); + this.midicc=this.getAttr("midicc",null); + this.midiController={}; + this.midiMode="normal"; + if(this.midicc) { + let ch = parseInt(this.midicc.substring(0, this.midicc.lastIndexOf("."))) - 1; + let cc = parseInt(this.midicc.substring(this.midicc.lastIndexOf(".") + 1)); + this.setMidiController(ch, cc); + } + this.setupImage(); + this.digits=0; + if(window.webAudioControlsMidiManager) +// window.webAudioControlsMidiManager.updateWidgets(); + window.webAudioControlsMidiManager.addWidget(this); + this.elem.onclick=(e)=>{e.stopPropagation()}; + } + disconnectedCallback(){} + setupImage(){ + this.coltab = this.colors.split(";"); + this.dr=this.direction; + this.dlen=this.ditchlength; + if(!this.width) + this.width=256; + if(!this.height) + this.height=256; + this.knob.style.backgroundSize = "100% 100%"; + this.elem.style.backgroundSize = "100% 100%"; + this.elem.style.width=this.width+"px"; + this.elem.style.height=this.height+"px"; + this.kwidth=this.knobwidth||(this.width*0.15|0); + this.kheight=this.knobheight||(this.height*0.15|0); + this.knob.style.width = this.kwidth+"px"; + this.knob.style.height = this.kheight+"px"; + if(!this.src){ + let r=Math.min(this.width,this.height)*0.02; + let svgbody= +` +`; + this.elem.style.backgroundImage = "url(data:image/svg+xml;base64,"+btoa(svgbody)+")"; + } + else{ + this.elem.style.backgroundImage = "url("+(this.src)+")"; + } + if(!this.knobsrc){ + let svgthumb= +` + +`; + this.knob.style.backgroundImage = "url(data:image/svg+xml;base64,"+btoa(svgthumb)+")"; + } + else{ + this.knob.style.backgroundImage = "url("+(this.knobsrc)+")"; + } + this.elem.style.outline=this.outline?"":"none"; + this.redraw(); + } + redraw() { + this.digits=0; + if(this.step && this.step < 1) { + for(let n = this.step ; n < 1; n *= 10) + ++this.digits; + } + if(this.valuethis.max){ + this.value=this.max; + return; + } + let range = this.max - this.min; + let style = this.knob.style; + style.left=(this.width-this.kwidth)*(this._x-this.min)/(this.max-this.min)+"px"; style.top=(this.height-this.kheight)*(1-(this._y-this.min)/(this.max-this.min))+"px"; + this.sensex=0; this.sensey=1; + } + _setX(v){ + v=(Math.round((v-this.min)/this.step))*this.step+this.min; + this._x=Math.min(this.max,Math.max(this.min,v)); + if(this._x!=this.oldx){ + this.oldx=this._x; + if(this.conv){ + this.convValue={x:eval(this.conv)(this._x),y:eval(this.conv)(this._y)}; + } + else + this.convValue={x:this._x,y:this._y}; + this.redraw(); + this.showtip(0); + return 1; + } + return 0; + } + _setY(v){ + v=(Math.round((v-this.min)/this.step))*this.step+this.min; + this._y=Math.min(this.max,Math.max(this.min,v)); + if(this._y!=this.oldy){ + this.oldy=this._y; + if(this.conv){ + this.convValue={x:eval(this.conv)(this._x),y:eval(this.conv)(this._y)}; + } + else + this.convValue={x:this._x,y:this._y}; + this.redraw(); + this.showtip(0); + return 1; + } + return 0; + } + setX(v,f){ + if(this._setX(v)&&f) + this.sendEvent("input"),this.sendEvent("change"); + } + setY(v,f){ + if(this._setY(v)&&f) + this.sendEvent("input"),this.sendEvent("change"); + } + wheel(e) { + let delta=(this.max-this.min)*0.01; + delta=e.deltaY>0?-delta:delta; + if(!e.shiftKey) + delta*=5; + if(Math.abs(delta) < this.step) + delta = (delta > 0) ? +this.step : -this.step; + this.setValue(+this.value+delta,true); + e.preventDefault(); + e.stopPropagation(); + this.redraw(); + } + pointerdown(ev){ + if(!this.enable) + return; + let e=ev; + if(ev.touches){ + e = ev.changedTouches[0]; + this.identifier=e.identifier; + } + else { + if(e.buttons!=1 && e.button!=0) + return; + } + this.elem.focus(); + this.drag=1; + this.showtip(0); + let pointermove=(ev)=>{ + let e=ev; + if(ev.touches){ + for(let i=0;i{ + let e=ev; + if(ev.touches){ + for(let i=0;;){ + if(ev.changedTouches[i].identifier==this.identifier){ + break; + } + if(++i>=ev.changedTouches.length) + return; + } + } + this.drag=0; + this.showtip(0); + this.startPosX = this.startPosY = null; + window.removeEventListener('mousemove', pointermove); + window.removeEventListener('touchmove', pointermove, {passive:false}); + window.removeEventListener('mouseup', pointerup); + window.removeEventListener('touchend', pointerup); + window.removeEventListener('touchcancel', pointerup); + document.body.removeEventListener('touchstart', preventScroll,{passive:false}); + this.sendEvent("change"); + } + pointermove(ev); + let preventScroll=(e)=>{ + e.preventDefault(); + } + if(e.touches) + e = e.touches[0]; + if(e.ctrlKey || e.metaKey) + this.setValue(this.defvalue,true); + else { + this.startPosX = e.pageX; + this.startPosY = e.pageY; + this.startVal = this.value; + window.addEventListener('mousemove', pointermove); + window.addEventListener('touchmove', pointermove, {passive:false}); + } + window.addEventListener('mouseup', pointerup); + window.addEventListener('touchend', pointerup); + window.addEventListener('touchcancel', pointerup); + document.body.addEventListener('touchstart', preventScroll,{passive:false}); + e.preventDefault(); + e.stopPropagation(); + return false; + } + }); +} catch(error){ + console.log("webaudio-xypad already defined"); +} + + + + // FOR MIDI LEARN + class WebAudioControlsMidiManager { + constructor(){ + this.midiAccess = null; + this.listOfWidgets = []; + this.listOfExternalMidiListeners = []; + this.updateWidgets(); + this.initWebAudioControls(); + } + addWidget(w){ + this.listOfWidgets.push(w); + } + updateWidgets(){ +// this.listOfWidgets = document.querySelectorAll("webaudio-knob,webaudio-slider,webaudio-switch"); + } + initWebAudioControls() { + if(navigator.requestMIDIAccess) { + navigator.requestMIDIAccess().then( + (ma)=>{this.midiAccess = ma,this.enableInputs()}, + (err)=>{ console.log("MIDI not initialized - error encountered:" + err.code)} + ); + } + } + enableInputs() { + let inputs = this.midiAccess.inputs.values(); + console.log("Found " + this.midiAccess.inputs.size + " MIDI input(s)"); + for(let input = inputs.next(); input && !input.done; input = inputs.next()) { + console.log("Connected input: " + input.value.name); + input.value.onmidimessage = this.handleMIDIMessage.bind(this); + } + } + midiConnectionStateChange(e) { + console.log("connection: " + e.port.name + " " + e.port.connection + " " + e.port.state); + enableInputs(); + } + + onMIDIStarted(midi) { + this.midiAccess = midi; + midi.onstatechange = this.midiConnectionStateChange; + enableInputs(midi); + } + // Add hooks for external midi listeners support + addMidiListener(callback) { + this.listOfExternalMidiListeners.push(callback); + } + getCurrentConfigAsJSON() { + return currentConfig.stringify(); + } + handleMIDIMessage(event) { + this.listOfExternalMidiListeners.forEach(function (externalListener) { + externalListener(event); + }); + if(((event.data[0] & 0xf0) == 0xf0) || ((event.data[0] & 0xf0) == 0xb0 && event.data[1] >= 120)) + return; + for(let w of this.listOfWidgets) { + if(w.processMidiEvent) + w.processMidiEvent(event); + } + if(opt.mididump) + console.log(event.data); + } + contextMenuOpen(e,knob){ + if(!this.midiAccess) + return; + let menu=document.getElementById("webaudioctrl-context-menu"); + menu.style.left=e.pageX+"px"; + menu.style.top=e.pageY+"px"; + menu.knob=knob; + menu.classList.add("active"); + menu.knob.focus(); +// document.activeElement.onblur=this.contextMenuClose; + menu.knob.addEventListener("keydown",this.contextMenuCloseByKey.bind(this)); + } + contextMenuCloseByKey(e){ + if(e.keyCode==27) + this.contextMenuClose(); + } + contextMenuClose(){ + let menu=document.getElementById("webaudioctrl-context-menu"); + menu.knob.removeEventListener("keydown",this.contextMenuCloseByKey); + menu.classList.remove("active"); + let menuItemLearn=document.getElementById("webaudioctrl-context-menu-learn"); + menuItemLearn.innerHTML = 'Learn'; + menu.knob.midiMode = 'normal'; + } + contextMenuLearn(){ + let menu=document.getElementById("webaudioctrl-context-menu"); + let menuItemLearn=document.getElementById("webaudioctrl-context-menu-learn"); + menuItemLearn.innerHTML = 'Listening...'; + menu.knob.midiMode = 'learn'; + } + contextMenuClear(e){ + let menu=document.getElementById("webaudioctrl-context-menu"); + menu.knob.midiController={}; + this.contextMenuClose(); + } + } + if(window.UseWebAudioControlsMidi||opt.useMidi) + window.webAudioControlsMidiManager = new WebAudioControlsMidiManager(); +} diff --git a/www/webcomponents-lite.js b/www/webcomponents-lite.js new file mode 100644 index 0000000..d3e7beb --- /dev/null +++ b/www/webcomponents-lite.js @@ -0,0 +1,197 @@ +(function(){/* + + Copyright (c) 2016 The Polymer Project Authors. All rights reserved. + This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + Code distributed by Google as part of the polymer project is also + subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt +*/ +'use strict';var p,q="undefined"!=typeof window&&window===this?this:"undefined"!=typeof global&&null!=global?global:this,ba="function"==typeof Object.defineProperties?Object.defineProperty:function(a,b,c){a!=Array.prototype&&a!=Object.prototype&&(a[b]=c.value)};function ca(){ca=function(){};q.Symbol||(q.Symbol=da)}var da=function(){var a=0;return function(b){return"jscomp_symbol_"+(b||"")+a++}}(); +function ea(){ca();var a=q.Symbol.iterator;a||(a=q.Symbol.iterator=q.Symbol("iterator"));"function"!=typeof Array.prototype[a]&&ba(Array.prototype,a,{configurable:!0,writable:!0,value:function(){return fa(this)}});ea=function(){}}function fa(a){var b=0;return ha(function(){return b"+this.innerHTML+""},set:function(a){if(this.parentNode){J.body.innerHTML=a;for(a=this.ownerDocument.createDocumentFragment();J.body.firstChild;)l.call(a, +J.body.firstChild);m.call(this.parentNode,a,this)}else throw Error("Failed to set the 'outerHTML' property on 'Element': This element has no parent node.");},configurable:!0})};na(a.prototype);aa(a.prototype);a.J=function(c){c=b(c,"template");for(var d=0,e=c.length,f;d]/g, +U=function(a){switch(a){case "&":return"&";case "<":return"<";case ">":return">";case "\u00a0":return" "}}}if(c||eb){a.ca=function(a,b){var c=f.call(a,!1);this.D&&this.D(c);b&&(l.call(c.content,f.call(a.content,!0)),fb(c.content,a.content));return c};var fb=function(c,d){if(d.querySelectorAll&&(d=b(d,"template"),0!==d.length)){c=b(c,"template");for(var e=0,f=c.length,h,g;e]*)(rel=['|"]?stylesheet['|"]?[^>]*>)/g,x={La:function(a,b){a.href&&a.setAttribute("href",x.Y(a.getAttribute("href"),b));a.src&&a.setAttribute("src",x.Y(a.getAttribute("src"),b));if("style"===a.localName){var c=x.ta(a.textContent,b,Ca);a.textContent=x.ta(c,b,Da)}},ta:function(a,b,c){return a.replace(c, +function(a,c,d,e){a=d.replace(/["']/g,"");b&&(a=x.Y(a,b));return c+"'"+a+"'"+e})},Y:function(a,b){if(void 0===x.ba){x.ba=!1;try{var c=new URL("b","http://a");c.pathname="c%20d";x.ba="http://a/c%20d"===c.href}catch(Lg){}}if(x.ba)return(new URL(a,b)).href;c=x.Ba;c||(c=document.implementation.createHTMLDocument("temp"),x.Ba=c,c.ma=c.createElement("base"),c.head.appendChild(c.ma),c.la=c.createElement("a"));c.ma.href=b;c.la.href=a;return c.la.href||a}},na={async:!0,load:function(a,b,c){if(a)if(a.match(/^data:/)){a= +a.split(",");var d=a[1];d=-1e.status?b(d,a):c(d)};e.send()}else c("error: href must be specified")}},aa=/Trident/.test(navigator.userAgent)||/Edge\/\d./i.test(navigator.userAgent); +k.prototype.loadImports=function(a){var b=this;a=m(a,"link[rel=import]");n(a,function(a){return b.s(a)})};k.prototype.s=function(a){var b=this,c=a.href;if(void 0!==this.a[c]){var d=this.a[c];d&&d.__loaded&&(a.__import=d,this.h(a))}else this.b++,this.a[c]="pending",na.load(c,function(a,d){a=b.Sa(a,d||c);b.a[c]=a;b.b--;b.loadImports(a);b.L()},function(){b.a[c]=null;b.b--;b.L()})};k.prototype.Sa=function(a,b){if(!a)return document.createDocumentFragment();aa&&(a=a.replace(Ea,function(a,b,c){return-1=== +a.indexOf("type=")?b+" type=import-disable "+c:a}));var c=document.createElement("template");c.innerHTML=a;if(c.content)a=c.content,l(a);else for(a=document.createDocumentFragment();c.firstChild;)a.appendChild(c.firstChild);if(c=a.querySelector("base"))b=x.Y(c.getAttribute("href"),b),c.removeAttribute("href");c=m(a,'link[rel=import],link[rel=stylesheet][href][type=import-disable],style:not([type]),link[rel=stylesheet][href]:not([type]),script:not([type]),script[type="application/javascript"],script[type="text/javascript"]'); +var d=0;n(c,function(a){h(a);x.La(a,b);a.setAttribute("import-dependency","");"script"===a.localName&&!a.src&&a.textContent&&(a.setAttribute("src","data:text/javascript;charset=utf-8,"+encodeURIComponent(a.textContent+("\n//# sourceURL="+b+(d?"-"+d:"")+".js\n"))),a.textContent="",d++)});return a};k.prototype.L=function(){var a=this;if(!this.b){this.c.disconnect();this.flatten(document);var b=!1,c=!1,d=function(){c&&b&&(a.loadImports(document),a.b||(a.c.observe(document.head,{childList:!0,subtree:!0}), +a.Pa()))};this.Ua(function(){c=!0;d()});this.Ta(function(){b=!0;d()})}};k.prototype.flatten=function(a){var b=this;a=m(a,"link[rel=import]");n(a,function(a){var c=b.a[a.href];(a.__import=c)&&c.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&(b.a[a.href]=a,a.readyState="loading",a.__import=a,b.flatten(c),a.appendChild(c))})};k.prototype.Ta=function(a){function b(e){if(e]/g;function hc(a){switch(a){case "&":return"&";case "<":return"<";case ">":return">";case '"':return""";case "\u00a0":return" "}}function ic(a){for(var b={},c=0;c";break a;case Node.TEXT_NODE:g=g.data;g=k&&kc[k.localName]?g:g.replace(gc,hc);break a;case Node.COMMENT_NODE:g="\x3c!--"+g.data+"--\x3e";break a;default:throw window.console.error(g), +Error("not implemented");}}c+=g}return c};var B={},D=document.createTreeWalker(document,NodeFilter.SHOW_ALL,null,!1),E=document.createTreeWalker(document,NodeFilter.SHOW_ELEMENT,null,!1);function mc(a){var b=[];D.currentNode=a;for(a=D.firstChild();a;)b.push(a),a=D.nextSibling();return b}B.parentNode=function(a){D.currentNode=a;return D.parentNode()};B.firstChild=function(a){D.currentNode=a;return D.firstChild()};B.lastChild=function(a){D.currentNode=a;return D.lastChild()};B.previousSibling=function(a){D.currentNode=a;return D.previousSibling()}; +B.nextSibling=function(a){D.currentNode=a;return D.nextSibling()};B.childNodes=mc;B.parentElement=function(a){E.currentNode=a;return E.parentNode()};B.firstElementChild=function(a){E.currentNode=a;return E.firstChild()};B.lastElementChild=function(a){E.currentNode=a;return E.lastChild()};B.previousElementSibling=function(a){E.currentNode=a;return E.previousSibling()};B.nextElementSibling=function(a){E.currentNode=a;return E.nextSibling()}; +B.children=function(a){var b=[];E.currentNode=a;for(a=E.firstChild();a;)b.push(a),a=E.nextSibling();return b};B.innerHTML=function(a){return lc(a,function(a){return mc(a)})};B.textContent=function(a){switch(a.nodeType){case Node.ELEMENT_NODE:case Node.DOCUMENT_FRAGMENT_NODE:a=document.createTreeWalker(a,NodeFilter.SHOW_TEXT,null,!1);for(var b="",c;c=a.nextNode();)b+=c.nodeValue;return b;default:return a.nodeValue}};var nc=Object.getOwnPropertyDescriptor(Element.prototype,"innerHTML")||Object.getOwnPropertyDescriptor(HTMLElement.prototype,"innerHTML"),oc=document.implementation.createHTMLDocument("inert"),pc=Object.getOwnPropertyDescriptor(Document.prototype,"activeElement"),qc={parentElement:{get:function(){var a=this.__shady&&this.__shady.parentNode;a&&a.nodeType!==Node.ELEMENT_NODE&&(a=null);return void 0!==a?a:B.parentElement(this)},configurable:!0},parentNode:{get:function(){var a=this.__shady&&this.__shady.parentNode; +return void 0!==a?a:B.parentNode(this)},configurable:!0},nextSibling:{get:function(){var a=this.__shady&&this.__shady.nextSibling;return void 0!==a?a:B.nextSibling(this)},configurable:!0},previousSibling:{get:function(){var a=this.__shady&&this.__shady.previousSibling;return void 0!==a?a:B.previousSibling(this)},configurable:!0},className:{get:function(){return this.getAttribute("class")||""},set:function(a){this.setAttribute("class",a)},configurable:!0},nextElementSibling:{get:function(){if(this.__shady&& +void 0!==this.__shady.nextSibling){for(var a=this.nextSibling;a&&a.nodeType!==Node.ELEMENT_NODE;)a=a.nextSibling;return a}return B.nextElementSibling(this)},configurable:!0},previousElementSibling:{get:function(){if(this.__shady&&void 0!==this.__shady.previousSibling){for(var a=this.previousSibling;a&&a.nodeType!==Node.ELEMENT_NODE;)a=a.previousSibling;return a}return B.previousElementSibling(this)},configurable:!0}},rc={childNodes:{get:function(){if(vb(this)){if(!this.__shady.childNodes){this.__shady.childNodes= +[];for(var a=this.firstChild;a;a=a.nextSibling)this.__shady.childNodes.push(a)}var b=this.__shady.childNodes}else b=B.childNodes(this);b.item=function(a){return b[a]};return b},configurable:!0},childElementCount:{get:function(){return this.children.length},configurable:!0},firstChild:{get:function(){var a=this.__shady&&this.__shady.firstChild;return void 0!==a?a:B.firstChild(this)},configurable:!0},lastChild:{get:function(){var a=this.__shady&&this.__shady.lastChild;return void 0!==a?a:B.lastChild(this)}, +configurable:!0},textContent:{get:function(){if(vb(this)){for(var a=[],b=0,c=this.childNodes,d;d=c[b];b++)d.nodeType!==Node.COMMENT_NODE&&a.push(d.textContent);return a.join("")}return B.textContent(this)},set:function(a){if("undefined"===typeof a||null===a)a="";switch(this.nodeType){case Node.ELEMENT_NODE:case Node.DOCUMENT_FRAGMENT_NODE:for(;this.firstChild;)this.removeChild(this.firstChild);(0b.__shady.assignedNodes.length&&(b.__shady.ia=!0)}b.__shady.ia&&(b.__shady.ia=!1,od(this,b))}a=this.o;b=[];for(c=0;cb.indexOf(d))||b.push(d);for(a=0;a "+b}))}a=a.replace(Df,function(a,b,c){return'[dir="'+c+'"] '+b+", "+b+'[dir="'+c+'"]'});return{value:a,Ka:b,stop:f}}function Bf(a,b){a=a.split(Ef);a[0]+=b;return a.join(Ef)} +function Af(a,b){var c=a.match(Ff);return(c=c&&c[2].trim()||"")?c[0].match(Gf)?a.replace(Ff,function(a,c,f){return b+f}):c.split(Gf)[0]===b?c:Hf:a.replace(wf,b)}function If(a){a.selector===Jf&&(a.selector="html")}hf.prototype.c=function(a){return a.match(zf)?this.b(a,Kf):Bf(a.trim(),Kf)};q.Object.defineProperties(hf.prototype,{a:{configurable:!0,enumerable:!0,get:function(){return"style-scope"}}}); +var uf=/:(nth[-\w]+)\(([^)]+)\)/,Kf=":not(.style-scope)",sf=",",xf=/(^|[\s>+~]+)((?:\[.+?\]|[^\s>+~=[])+)/g,Gf=/[[.:#*]/,wf=":host",Jf=":root",zf="::slotted",vf=new RegExp("^("+zf+")"),Ff=/(:host)(?:\(((?:\([^)(]*\)|[^)(]*)+?)\))/,Cf=/(?:::slotted)(?:\(((?:\([^)(]*\)|[^)(]*)+?)\))/,Df=/(.*):dir\((?:(ltr|rtl))\)/,qf=".",Ef=":",mf="class",Hf="should_not_match",W=new hf;function Lf(a,b,c,d){this.w=a||null;this.b=b||null;this.ja=c||[];this.G=null;this.P=d||"";this.a=this.u=this.B=null}function X(a){return a?a.__styleInfo:null}function Mf(a,b){return a.__styleInfo=b}Lf.prototype.c=function(){return this.w};Lf.prototype._getStyleRules=Lf.prototype.c;var Nf,Of=window.Element.prototype;Nf=Of.matches||Of.matchesSelector||Of.mozMatchesSelector||Of.msMatchesSelector||Of.oMatchesSelector||Of.webkitMatchesSelector;var Pf=navigator.userAgent.match("Trident");function Qf(){}function Rf(a){var b={},c=[],d=0;af(a,function(a){Sf(a);a.index=d++;a=a.i.cssText;for(var c;c=Ve.exec(a);){var e=c[1];":"!==c[2]&&(b[e]=!0)}},function(a){c.push(a)});a.b=c;a=[];for(var e in b)a.push(e);return a} +function Sf(a){if(!a.i){var b={},c={};Tf(a,c)&&(b.v=c,a.rules=null);b.cssText=a.parsedCssText.replace(Ye,"").replace(Te,"");a.i=b}}function Tf(a,b){var c=a.i;if(c){if(c.v)return Object.assign(b,c.v),!0}else{c=a.parsedCssText;for(var d;a=Te.exec(c);){d=(a[2]||a[3]).trim();if("inherit"!==d||"unset"!==d)b[a[1].trim()]=d;d=!0}return d}} +function Uf(a,b,c){b&&(b=0<=b.indexOf(";")?Vf(a,b,c):ff(b,function(b,e,f,h){if(!e)return b+h;(e=Uf(a,c[e],c))&&"initial"!==e?"apply-shim-inherit"===e&&(e="inherit"):e=Uf(a,c[f]||f,c)||f;return b+(e||"")+h}));return b&&b.trim()||""} +function Vf(a,b,c){b=b.split(";");for(var d=0,e,f;d *"===f||"html"===f,g=0===f.indexOf(":host")&&!h;"shady"===c&&(h=f===e+" > *."+e||-1!==f.indexOf("html"),g=!h&&0===f.indexOf(e));"shadow"===c&&(h=":host > *"===f||"html"===f,g=g&&!h);if(h||g)c=e,g&&(R&&!b.m&&(b.m=rf(W,b,W.b,a?qf+a:"",e)),c=b.m||e),d({Xa:c,Qa:g,hb:h})}} +function Yf(a,b){var c={},d={},e=b&&b.__cssBuild;af(b,function(b){Xf(a,b,e,function(e){Nf.call(a.b||a,e.Xa)&&(e.Qa?Tf(b,c):Tf(b,d))})},null,!0);return{Wa:d,Oa:c}} +function Zf(a,b,c,d){var e=V(b),f=pf(e.is,e.P),h=new RegExp("(?:^|[^.#[:])"+(b.extends?"\\"+f.slice(0,-1)+"\\]":f)+"($|[.:[\\s>+~])");e=X(b).w;var g=$f(e,d);return nf(b,e,function(b){var e="";b.i||Sf(b);b.i.cssText&&(e=Vf(a,b.i.cssText,c));b.cssText=e;if(!R&&!cf(b)&&b.cssText){var k=e=b.cssText;null==b.ra&&(b.ra=We.test(e));if(b.ra)if(null==b.W){b.W=[];for(var n in g)k=g[n],k=k(e),e!==k&&(e=k,b.W.push(n))}else{for(n=0;n=l._useCount&&l.parentNode&&l.parentNode.removeChild(l));R?f.a?(f.a.textContent=e,d=f.a):e&&(d=df(e,g,a.shadowRoot,f.b)):d?d.parentNode|| +(Pf&&-1.\n var capturedCloneNode = Node.prototype.cloneNode;\n var capturedCreateElement = Document.prototype.createElement;\n var capturedImportNode = Document.prototype.importNode;\n var capturedRemoveChild = Node.prototype.removeChild;\n var capturedAppendChild = Node.prototype.appendChild;\n var capturedReplaceChild = Node.prototype.replaceChild;\n var capturedParseFromString = DOMParser.prototype.parseFromString;\n var capturedHTMLElementInnerHTML = Object.getOwnPropertyDescriptor(window.HTMLElement.prototype, 'innerHTML');\n var capturedChildNodes = Object.getOwnPropertyDescriptor(window.Node.prototype, 'childNodes');\n\n var elementQuerySelectorAll = Element.prototype.querySelectorAll;\n var docQuerySelectorAll = Document.prototype.querySelectorAll;\n var fragQuerySelectorAll = DocumentFragment.prototype.querySelectorAll;\n\n var scriptSelector = 'script:not([type]),script[type=\"application/javascript\"],script[type=\"text/javascript\"]';\n\n function QSA(node, selector) {\n // IE 11 throws a SyntaxError with `scriptSelector` if the node has no children due to the `:not([type])` syntax\n if (!node.childNodes.length) {\n return [];\n }\n switch (node.nodeType) {\n case Node.DOCUMENT_NODE:\n return docQuerySelectorAll.call(node, selector);\n case Node.DOCUMENT_FRAGMENT_NODE:\n return fragQuerySelectorAll.call(node, selector);\n default:\n return elementQuerySelectorAll.call(node, selector);\n }\n }\n\n // returns true if nested templates cannot be cloned (they cannot be on\n // some impl's like Safari 8 and Edge)\n // OR if cloning a document fragment does not result in a document fragment\n var needsCloning = (function() {\n if (!needsTemplate) {\n var t = document.createElement('template');\n var t2 = document.createElement('template');\n t2.content.appendChild(document.createElement('div'));\n t.content.appendChild(t2);\n var clone = t.cloneNode(true);\n return (clone.content.childNodes.length === 0 || clone.content.firstChild.content.childNodes.length === 0\n || brokenDocFragment);\n }\n })();\n\n var TEMPLATE_TAG = 'template';\n var PolyfilledHTMLTemplateElement = function() {};\n\n if (needsTemplate) {\n\n var contentDoc = document.implementation.createHTMLDocument('template');\n var canDecorate = true;\n\n var templateStyle = document.createElement('style');\n templateStyle.textContent = TEMPLATE_TAG + '{display:none;}';\n\n var head = document.head;\n head.insertBefore(templateStyle, head.firstElementChild);\n\n /**\n Provides a minimal shim for the