commit 68b0e81a95695635b5428b4e0f29e7aace2d7610 Author: leduc Date: Thu Dec 13 12:05:32 2018 +0100 First commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..227a8b0 --- /dev/null +++ b/README.md @@ -0,0 +1,459 @@ +LJ v0.7.0 + +By Sam Neurohack, Loloster, Cocoa + +LICENCE : CC BY + + + +![LJ](http://www.teamlaser.fr/thsf/images/fulls/THSF9-33.jpg) + +A software server with gui for up to 4 lasers live actions. Think creative like Laser "battles", planetarium,... + +No .ild file here, you run your client that generate/send point lists to LJ. Any redis capable programming langage is fine. + +Needs at least : an etherdream DAC connected to an ILDA laser, RJ 45 IP network (gigabits only !! no wifi, 100 mpbs doesn't work well with several lasers) + +GUIs : WebUI, TouchOSC, Pure Data patch. You can build your own GUI and send/get commands to/from LJ through OSC. Attention : the Pure Data patch works with PD-extended 0.43. Any contribution for whatever "better" Pure Data version are welcome. + +Devices supported : Launchpad mini, LP8, enttec DMX PRO, bhoreal, nozoids, gamepad, smartphone & tablet (with OSC like gyroscopes) and any MIDI controller that is recognised by your OS. + +Nozosc : Semi modular synthetizers from Nozoids can send a lot of their inner sound curves and be displayed in many ways, i.e VCO 1 on X axis and LFO 2 on Y axis. + +You can also send OSC commands to a video, music,... external software to trigger what you want. + + +The server approach is based on redis. One process per etherdream is spawn to : retrieve the given point list from redis, warp, resample and manage the given etherdream DAC dialog. + + +# +# Features among many others. +# + +(Doc in progress) + +- Automatically hook to Midi devices IN & OUT seen by OS. Very cool : LJ can script or be scripted by a midi device : Triggering different musics at given moments,... or in opposite, you can make a midi file with an external midi sequencer to script/trigger laser effects. +- Automatic USB enttec PRO DMX interface detection. See mydmx.py +- OSC server. Very cool : LJ can also script or be scripted with an OSC sequencer like Vezer or score. +- OSC to midi bridge (see /note and /cc/number) +- OSC to DMX bridge (see /cc/number) +- Bhoreal and Launchpad device start animation +- Control all leds of Bhoreal and Launchpad Mini through midi. Notes on and off, velocity is color. +- Interactive (mouse style) warp correction for each laser. +- Interactive (mouse style) any shape correction. Imagine you project on a building and want to use the windows like in a pinball. You need to define rectangle corner points and align them to the window, that's a shape you can use. The shape point list must be defined in the given laser "screen". See configuration file mainy.conf example. +- Using python for client, you can use all bhorosc functions like control Resolume Arena video software through OSC : +import bhorosc +bhorosc.sendresol("/layer1/clip1/connect",1) + +- Web ui : In your browser open webui/index.html. Javascript is needed. +- Status every 0.5 seconds : every etherdream DAC state, number of buffer points sent,... +- "Optimisation" points automatically added, can be changed live for glitch art. See + + +# +# External devices +# + +(Doc in Progress) + +- LPD8 : A config file is included. +- enttec USB pro +- LaunchPad mini +- Bhoreal +- Joypads : Joypads are detected and read by pygame. You need to adapt the button mapping to your specific gamepad in the code. Search "joypad" in setexample.py + + + + +# +# Introduction +# + + +You need to update mainy.conf to your network/etherdreams IPs and Be sure to check command arguments : python mainyservers.py --help + +LJ is meant for Live, so a lot of parameters can be changed via OSC/midi, webUI,... + + +Program your own "Client" : +------------------------- + +- Read the Introduction part in this readme. +- Carefully read all comments in clients examples. +- Generate at least one point list array (say a square). +- Feed your point list string to redis server + + +If you need to receive data externally : + +use /nozoid/osc/number value : Get the new value in gstt.osc[number] (number : 0-255) +or program your own OSC commands in bhorosc.py + + + +Joypads : +--------- + +You need to decide what to do with joypads axis, hat, buttons. See joypads() in setexample.py. To adapt pygame button numbers to your gamepad use : + +python joys.py + + + + +"Shapes" : +---------- + +"Shapes" are mouse editable areas i.e you make a flipper on a building and want something happen with the building windows. "Shapes" are the list of points you see at the beginning of conf file. "Shapes" are grouped in "Screens" that will be displayed by a given laser. See curve 0 in setexample.py +Again "Shapes" are only mousely editable list of points : you can display them or not. + + + +# +# Install +# + +In terminal type : + +./install.sh + +Check the bind line in /etc/redis/redis.conf : + +- If client and laser servers computers are the same, use 127.0.0.1 +- Client and laser server are different, use the laser server computer IP. + +In webui/index.html change the ws ip adress to the server IP or 127.0.0.1 if client computer = laser server computer. + +Using the same idea check all ip address in mainy.conf. + +For network Gurus : bind to all network interface scheme is not working yet. + + + +# +# To run +# + +Always start the laser server first. + +Case 1 : the laser server computer is the same that the computer running a client : + +python mainyservers.py + +Open/reload in browser webui/index.html. (javascript must be enabled) + +Check in your client if the server IP is the good one + +Run your client + +to monitor redis server : + +redis-cli monitor + + +Case 2 : Server and Client computers are different : + + +Say the laser server computer (running LJ) IP is 192.138.1.13, the client computer is 192.168.1.52 + +On the server computer : +edit /etc/redis/redis.conf +python mainyservers.py -r 192.168.1.13 + +on the client computer for all features : + +to just generate and send list points +node testredis.js + +to monitor redis server : + + +redis-cli -h monitor + + + +# +# Todo +# + +(Doc in Progress) + +- Find 3D rotations matrices and 2 projections, test speed / normal algo with algotest.py +- Smaller cpu footprint (compute only when something has changed,...) +- kpps live modification for glitch art. +- Improve Bhoreal & LaunchPad inputs +- Tags for automatic laser load/ balancing +- Texts : multilasers support, more fonts. +- Improve WebUI with simulator. +- tomidi should not disable other targets. +- Warp corrections should not used warpdestinations default values in conf file. + + + +# +# LJ OSC commands : +# + +# General + +/noteon number velocity + : Note on sent to laser (see Midi below for notes effects). Noteon can also be send to midi targets if gstt.tomidi is True, but this disable all other targets for the moment. Todo. + +/noteoff number : Note off is sent only to midi targets. + + +/accxyz x y z : TouchOSC gyroscope x assigned to cc 1 and y assigned to cc 2. See Midi below for cc effects. + +/gyrosc/gyro x y z : Change 3D rotation angles with gyroscope float values. i.e for GyrOSC iOS app. At this time Z is ignored and Z rotation set to 0 + +/point x y z : Set point coordinates for "slave" curve. Need to be changed change to collections deque as in llstr.py + +/stop/rotation : Set all 3D rotations speed and 3D rotation angles to 0 + +/cc/number value : Change the cc with given value. Effect will depend on flags set to True : gstt.todmx (value is forwarded to dmx channel) , gstt.tomidi, gstt.tolaser (center align or curve mode). See cc effects below + +/number value : switch current displayed curve to value. + +/quit : Do nothing yet + + +# Laser Control + + +/display number : Select what point list (PL) is displayed by simulator + + +/swap/X/lasernumber value (0 or 1) + : switch on and off general X inversion on given laser + +/swap/Y/lasernumber value (0 or 1) + : switch on and off general Y inversion on given laser + + +/loffset/X/lasernumber value + : Move X center on given laser of value pixels + +/loffset/Y/lasernumber value + : Move Y center on given laser of value pixels + + +/scale/X/lasernumber value + : Stretch laser display of given laser of value + +/scale/Y/lasernumber value + : Stretch laser display of given laser of value + + +/ip/lasernumber IP + : Assign a new etherdream (by its IP adress) for given laser number + +/angle/lasernumber value + : Change geometric angle correction for given laser number by computing a new homgraphy + +/intens/lasernumber value + : Assign a new beam intensity for given laser (todo : if etherdream can actually change it) + +/grid/lasernumber value (0 or 1) + : Toggle a grid display for given laser + +/mouse/lasernumber value (0 or 1) + : Toggle the mapping function for given laser + + +# Colors + +For currently selected laser and in RGB Color mode (see below MIDI notes effects to switch Color mode and "current" laser selection) + +/red 0 : Switch off blue laser. + +/red 255 (or >0) Switch on blue laser + + +/green 0 : Switch off blue laser + +/green 255 (or >0) Switch on blue laser + + +/blue 0 : Switch off blue laser + +/blue 255 (or >0) Switch on blue laser + + + +# Bhoreal and Launchpad devices + +![Bhoreal](http://levfestival.com/13/wp-content/uploads/Bhoreal_2.jpg) + +/led led number color : Switch on given led with given color. + +/led/xy x y color Switch on led with x y position to given color. + +/xy x y + +/allcolorbhor : Switch all Bhoreal Leds with given colour (0-127) + +/clsbhor : Switch off all bhoreal leds + +/padmode : Code not available yet in LJay. Different modes available for Bhoreal and Launchpad. "Prompt" = 10 ; "Myxo" = 2 ; "Midifile" = 3 + + + + +# Nozoids synthetizers + +![Nozoid synthetizer](http://nozoid.com/wp-content/uploads/2017/05/OCS_previus-600x330.png) + + + +Functions originated by nozosc.py and executed in llstr.py (See Nozosc readme for complete OSC implementation and how to control Nozosc). A new firmware by loloster is mandatory for OCS 2 (https://github.com/loloster/ocs-2) and MMO3 (https://github.com/loloster/mmo-3). "curve" means on of the 4 curves managed by nozosc. setllstr.py as differents Set/Curve generator called by LJay that displays these 4 "curves"; + + +/nozoid/osc/number value : Store a new value for given oscillator/LFO/VCO + +/nozoid/X value curve : Use given oscillator/LFO/VCO number as X axis for given curve . See llstr.py + +/nozoid/Y value curve : use given oscillator/LFO/VCO number for Y axis for given curve. See llstr.py + +/nozoid/color r g b curve : set current laser color for given curve + +/nozoid/knob/number value : Not used yet + +/nozoid/mix/number value : Not used yet + +/nozoid/vco/number value : Not used yet + +/nozoid/lfo/number value : Not used yet + + + +# GUI + +![Advanced Gui](http://www.teamlaser.fr/mcontroller.png) + +/on : Accept a GUI with status widget. Automatically get its IP, send status,... + +/off : Disconnect the GUI + +/status text Display some text on status widget GUI + +TouchOSC GUI button matrix + +/clear : Clear status widget text. + +/enter : should validate previous chosen number + +/control/matrix/Y/X 0 or 1 + First screen ("Control") buttons toggle state : on or off + +/pad/rights/note 0 or 1 + "Pad" screen (launchpad mini simulator screen), right column : Send note on and note off + +/pad/tops/cc 0 or 1 + "Pad" screen top raw : Send CC 0/127 + + + +# +# Midi commands +# + +Midi Note : built in midi notes assignation. More : if you hook a midi led matrix like bhoreal, led are updated. See Noteon_Update() in bhorosc.py + +0-7 Curve choice. Note on 0 to set Curve O, Note on 1 for Curve 1,... + +8-15 Set choice. All happening Live, so as the new Set may not have the same Curve number, changing Set autoselect the builtin "black" curve (-1) as a fallback, so you can safely choose a new Curve in the new Set. +Example : to switch to Set 0, use note on 8. For Set 1 use note on 9,.... + +16-23 Laser choice. "Current laser" choice +Example : to switch to Laser 0, use note on 16. For Laser 1 use note on 17,.... + + +24-31 SimuPL choice. Example : to display PL 0 on simulator it's note on 24. To display PL 1 on simulator it's note on 25.... + +57 Color mode : Rainbow + +58 Color mode : RGB + + +Midi CC channel effects (0-127) built in assignation, *only* if you use built in 3D rotation and 2D projection in your code. You can assign any CC to any function you code. You can get current value in gstt.cc[ccnumber]. See setexample.py + +1 X position + +2 Y position + +5 X select for Lissa curves (set curve ) + +6 Y select for Lissa curves (set curve ) + + +21 3D projection : FOV + +22 3D projection : Distance + + +29 3D Rotation speed X + +30 3D Rotation speed Y + +31 3D Rotation speed Z + + +# +# Resolume Arena commands +# + +A dedicated OSC client is built in. To send OSC commands to resolume use something like + +bhorosc.sendresol("/layer1/clip1/connect",1) + +Remember to specify Resolume IP and port in the beginning of bhorosc.py + + + + +# +# Ether dream configuration +# + +![Etherdream Laser DAC](https://www.ether-dream.com/ed2-external.jpg) + +This program suppose that the ether dream is configured in a certain way especially for its IP address. For ether dream 1 : write an autoplay.txt file inside an SD Card within the ether dream DAC, with the following lines you can adjust i.e for pps or fps. Yes, there is a builtin DHCP client in the ether dream DAC but if you run multiple lasers, having a fixed dedicated network makes you focus on laser stuff. + +/net/ipaddr 192.168.1.3 + +/net/netmask 255.255.255.0 + +/net/gateway 192.168.1.1 + +/ilda/pps 25000 + +/ilda/fps 25 + +About hardware setup, especially if you have several lasers : ILDA cables are insanely expensive. You may consider the Power Over Ethernet 'POE' option. Buy a very small ILDA cable, a POE splitter and connect everything to the ether dream fixed near your laser. You can have then a simple and very long network cable and use a Power Over Ethernet injector or switch closed to the driving computer. Beware some vendors use 24V POE Injector : POE injectors and splitters must match. + + +# +# Coordinates if you use the proj() function +# + +3D points (x,y,z) has *0,0,0 in the middle* +A square centered around origin and size 200 (z =0 is added automatically) : +([-200, -200, 0], [200, -200, 0], [200, 200, 0], [-200, 200, 0], [-200, -200, 0]) + +Pygame screen points are 2D. *0,0 is top left* +with no 3D rotations + 3D -> 2D Projection + translation to top left: +[(300.0, 400.0), (500.0, 400.0), (500.0, 200.0), (300.0, 200.0), (300.0, 400.0)] + + +Pygame points with color is fed to laser renderer +[(300.0, 400.0, 0), (500.0, 400.0, 16776960), (500.0, 200.0, 16776960), (300.0, 200.0, 16776960), (300.0, 400.0, 16776960)] + + +Laser points traced + +Because of blanking many points are automatically added and converted in etherdream coordinates system -32765 to +32765 in x and y axis. + +16 (-1500.0, 1500.0, 65280, 65280, 0), (-1500.0, 1500.0, 65280, 65280, 0), (-1500.0, 1500.0, 65280, 65280, 0), (-1500.0, 1500.0, 65280, 65280, 0), (-1500.0, 1500.0, 65280, 65280, 0), (-1500.0, 1500.0, 65280, 65280, 0), (-1500.0, 1500.0, 65280, 65280, 0), (-1500.0, 1500.0, 65280, 65280, 0), (-1500.0, 1500.0, 0, 0, 0), (-1500.0, 1500.0, 0, 0, 0), (-1500.0, 1500.0, 0, 0, 0), (-1500.0, 1500.0, 0, 0, 0), (-1500.0, 1500.0, 0, 0, 0), (-1500.0, 1500.0, 0, 0, 0), (-1500.0, 1500.0, 0, 0, 0), (-1500.0, 1500.0, 0, 0, 0) +8 (1500.0, 1500.0, 65280, 65280, 0), (1500.0, 1500.0, 65280, 65280, 0), (1500.0, 1500.0, 65280, 65280, 0), (1500.0, 1500.0, 65280, 65280, 0), (1500.0, 1500.0, 65280, 65280, 0), (1500.0, 1500.0, 65280, 65280, 0), (1500.0, 1500.0, 65280, 65280, 0), (1500.0, 1500.0, 65280, 65280, 0) +8 (1500.0, -1500.0, 65280, 65280, 0), (1500.0, -1500.0, 65280, 65280, 0), (1500.0, -1500.0, 65280, 65280, 0), (1500.0, -1500.0, 65280, 65280, 0), (1500.0, -1500.0, 65280, 65280, 0), (1500.0, -1500.0, 65280, 65280, 0), (1500.0, -1500.0, 65280, 65280, 0), (1500.0, -1500.0, 65280, 65280, 0) +8 (-1500.0, -1500.0, 65280, 65280, 0), (-1500.0, -1500.0, 65280, 65280, 0), (-1500.0, -1500.0, 65280, 65280, 0), (-1500.0, -1500.0, 65280, 65280, 0), (-1500.0, -1500.0, 65280, 65280, 0), (-1500.0, -1500.0, 65280, 65280, 0), (-1500.0, -1500.0, 65280, 65280, 0), (-1500.0, -1500.0, 65280, 65280, 0) diff --git a/cli.py b/cli.py new file mode 100644 index 0000000..744e25c --- /dev/null +++ b/cli.py @@ -0,0 +1,163 @@ +# coding=UTF-8 +""" +LJay/LJ + +v0.8 + +Command line arguments parser + +by Sam Neurohack +from /team/laser + +""" + + +import gstt +import argparse + + +def handle(): + + if gstt.debug > 2: + print "" + print "Arguments parsing if needed..." + #have to be done before importing bhorosc.py to get correct port assignment + argsparser = argparse.ArgumentParser(description="LJay") + argsparser.add_argument("-r","--redisIP",help="Redis computer IP address (gstt.LjayServerIP by default)",type=str) + argsparser.add_argument("-i","--iport",help="OSC port number to listen to (8001 by default)",type=int) + argsparser.add_argument("-o","--oport",help="OSC port number to send to (8002 by default)",type=int) + argsparser.add_argument("-x","--invx",help="Invert laser 0 X axis again",action="store_true") + argsparser.add_argument("-y","--invy",help="Invert laser 0 Y axis again",action="store_true") + argsparser.add_argument("-s","--set",help="Only for VJ version. Specify wich generator set to use (default is in gstt.py)",type=int) + argsparser.add_argument("-c","--curve",help="Only for VJ version. Specify with generator curve to use (default is in gstt.py)",type=int) + argsparser.add_argument("-a","--align",help="Reset laser 0 alignement values",action="store_true") + argsparser.add_argument("-d","--display",help="Point List number displayed in pygame simulator",type=int) + argsparser.add_argument("-v","--verbose",help="Debug mode 0,1 or 2.",type=int) + argsparser.add_argument("-L","--Lasers",help="Number of lasers connected.",type=int) + argsparser.add_argument("-b","--bhoroscIP",help="Computer IP running bhorosc ('127.0.0.1' by default)",type=str) + argsparser.add_argument("-n","--nozoscIP",help="Computer IP running Nozosc ('127.0.0.1' by default)",type=str) + + + + # Keep it ! if new features of cli.py is used in a monolaser program + # argsparser.add_argument("-l","--laser",help="Last digit of etherdream ip address 192.168.1.0/24 (4 by default). Localhost if digit provided is 0.",type=int) + + + args = argsparser.parse_args() + + + # Ports arguments + if args.iport: + iport = args.iport + gstt.iport = iport + else: + iport = gstt.iport + + if args.oport: + oport = args.oport + gstt.oport = oport + else: + oport = gstt.oport + + if gstt.debug > 0: + print "gstt.oport:",gstt.oport + print "gstt.iport:",gstt.iport + + + # X Y inversion arguments + if args.invx == True: + + gstt.swapX[0] = -1 * gstt.swapX[0] + gstt.centerx[0] = 0 + gstt.centery[0] = 0 + #WriteSettings() + print("laser 0 X new invertion Asked") + if gstt.swapX[0] == 1: + print ("X not Inverted") + else: + print ("X Inverted") + + if args.invy == True: + + gstt.swapY[0] = -1 * gstt.swapY[0] + gstt.centerx[0] = 0 + gstt.centery[0] = 0 + #WriteSettings() + print("laser 0 Y new invertion Asked") + if gstt.swapY[0] == 1: + print ("Y not Inverted") + else: + print("Y inverted") + + # Redis Computer IP + if args.redisIP != None: + gstt.LjayServerIP = args.redisIP + + + # Set / Curves arguments + if args.set != None: + gstt.Set = args.set + print "Set : " + str(gstt.Set) + + if args.curve != None: + gstt.Curve = args.curve + print "Curve : " + str(gstt.Curve) + + + # Point list number used by simulator + if args.display != None: + gstt.simuPL = args.display + print "Display : " + str(gstt.simuPL) + + + + # Verbose = debug + if args.verbose != None: + gstt.debug = args.verbose + + + # Lasers = number of laser connected + if args.Lasers != None: + gstt.LaserNumber = args.Lasers + + + if args.bhoroscIP != None: + gstt.oscIPin = args.bhoroscIP + else: + gstt.oscIPin = '127.0.0.1' + + if args.nozoscIP != None: + gstt.nozoscIP = args.nozoscIP + else: + gstt.nozoscIP = '127.0.0.1' + + # Etherdream target for mono laser program + ''' + if args.laser != None: + lstdgtlaser = args.laser + + if lstdgtlaser == 0: + etherIP = "127.0.0.1" + else: + etherIP = "192.168.1."+str(lstdgtlaser) + + else: + etherIP = "192.168.1.4" + + #print ("Laser 1 etherIP:",etherIP) + ''' + + # Reset alignment values + if args.align == True: + + gstt.centerx[0] = 0 + gstt.centery[0] = 0 + gstt.zoomx[0] = 15 + gstt.zoomy[0] = 15 + gstt.sizex[0] = 32000 + gstt.sizey[0] = 32000 + gstt.finangle[0] = 0.0 + gstt.swapx[0] = 1 + gstt.swapy[0] = 1 + #Settings.Write() + diff --git a/clients/framy.py b/clients/framy.py new file mode 100644 index 0000000..844ce78 --- /dev/null +++ b/clients/framy.py @@ -0,0 +1,114 @@ +# coding=UTF-8 + +''' +LJay v0.8.0 + + +LICENCE : CC +pclf, Sam Neurohack + +''' + +import math +import redis + +redisIP = '192.168.1.13' +r = redis.StrictRedis(host=redisIP, port=6379, db=0) + +point_list = [] +pl = [[],[],[],[]] + +def LineTo(xy, c, PL): + + pl[PL].append((xy + (c,))) + +def Line(xy1, xy2, c, PL): + LineTo(xy1, 0, PL) + LineTo(xy2, c , PL) + + +def PolyLineOneColor(xy_list, c, PL , closed ): + #print "--" + #print "c",c + #print "xy_list",xy_list + #print "--" + xy0 = None + for xy in xy_list: + if xy0 is None: + xy0 = xy + #print "xy0:",xy0 + LineTo(xy0,0, PL) + else: + #print "xy:",xy + LineTo(xy,c, PL) + if closed: + LineTo(xy0,c, PL) + + +# Computing points coordinates for rPolyline function from 3D and around 0,0 to pygame coordinates +def Pointransf(xy, xpos = 0, ypos =0, resize =1, rotx =0, roty =0 , rotz=0): + + x = xy[0] * resize + y = xy[1] * resize + z = 0 + + rad = rotx * math.pi / 180 + cosaX = math.cos(rad) + sinaX = math.sin(rad) + + y2 = y + y = y2 * cosaX - z * sinaX + z = y2 * sinaX + z * cosaX + + rad = roty * math.pi / 180 + cosaY = math.cos(rad) + sinaY = math.sin(rad) + + z2 = z + z = z2 * cosaY - x * sinaY + x = z2 * sinaY + x * cosaY + + rad = rotz * math.pi / 180 + cosZ = math.cos(rad) + sinZ = math.sin(rad) + + x2 = x + x = x2 * cosZ - y * sinZ + y = x2 * sinZ + y * cosZ + + #print xy, (x + xpos,y+ ypos) + return (x + xpos,y+ ypos) + ''' + to understand why it get negative Y + + # 3D to 2D projection + factor = 4 * gstt.cc[22] / ((gstt.cc[21] * 8) + z) + print xy, (x * factor + xpos, - y * factor + ypos ) + return (x * factor + xpos, - y * factor + ypos ) + ''' + +# Send 2D point list around 0,0 with 3D rotation resizing and reposition around xpos ypos +#def rPolyLineOneColor(self, xy_list, c, PL , closed, xpos = 0, ypos =0, resize =1, rotx =0, roty =0 , rotz=0): +def rPolyLineOneColor(xy_list, c, PL , closed, xpos = 0, ypos =0, resize =0.7, rotx =0, roty =0 , rotz=0): + xy0 = None + for xy in xy_list: + if xy0 is None: + xy0 = xy + LineTo(Pointransf(xy0, xpos, ypos, resize, rotx, roty, rotz),0, PL) + else: + LineTo(Pointransf(xy, xpos, ypos, resize, rotx, roty, rotz),c, PL) + if closed: + LineTo(Pointransf(xy0, xpos, ypos, resize, rotx, roty, rotz),c, PL) + + +# set all points for given laser. special behavior depends on GridDisplay flag +# 0: point list / 1: Grid +def LinesPL(PL): + + if r.set('/pl/'+str(PL), str(pl[PL])) == True: + return True + else: + return False + +def ResetPL(self, PL): + pl[PL] = [] diff --git a/clients/nodeclient.js b/clients/nodeclient.js new file mode 100644 index 0000000..cc40f91 --- /dev/null +++ b/clients/nodeclient.js @@ -0,0 +1,91 @@ +// Send points lists to redis server +// In compatible LJay string format (pythonic lists) + +var redis = require("redis"), +client = redis.createClient(6379,'192.168.1.13'); + + + +function rgb2int(r,g,b) { + // Generate color from r g b components + var color = hex(r) + hex(g) + hex(b); + return parseInt(color, 16) + } + +function hex(v) { + // Get hexadecimal numbers. + var hex = v.toString(16); + if (v < 16) { + hex = "0" + hex; + } + return hex; +} + +// add one dot to Laser 0 point list +function adddot0(dotdata){ + var dotstring = "(" + dotdata + "),"; + pl0 += dotstring; + } + +// add one dot to Laser 1 point list +function adddot1(dotdata){ + var dotstring = "(" + dotdata + "),"; + pl1 += dotstring; + } + +// Generate same square to laser 0 and laser 1 +function GenPoints() + { + var pt = {}; + + // direct colors, i.e red + pt.r = 255; + pt.g = 0; + pt.b = 0; + + // named colors + var white = rgb2int(255, 255, 255); + + pt.x = 100; + pt.y = 200; + adddot0([pt.x, pt.y, rgb2int(pt.r, pt.g, pt.b)]); + adddot1([pt.x, pt.y, rgb2int(pt.r, pt.g, pt.b)]); + + pt.x = 100; + pt.y = 300; + adddot0([pt.x, pt.y, white]); + adddot1([pt.x, pt.y, white]); + + pt.x = 200; + pt.y = 300; + adddot0([pt.x, pt.y, white]); + adddot1([pt.x, pt.y, white]); + + pt.x = 200; + pt.y = 200; + adddot0([pt.x, pt.y, white]); + adddot1([pt.x, pt.y, white]); + + pt.x = 100; + pt.y = 200; + adddot0([pt.x, pt.y, white]); + adddot1([pt.x, pt.y, white]); + } + +// Point lists strings +var pl0 = "["; +var pl1 = "["; +GenPoints(); +pl0 = pl0.slice(0,-1) + "]" +pl1 = pl1.slice(0,-1) + "]" + +console.log(pl0); +console.log(pl1); + +// Send points lists to redis server +client.set("/pl/0",pl0); +client.set("/pl/1",pl1); + +// Quit +client.quit() + diff --git a/clients/pyclient.py b/clients/pyclient.py new file mode 100644 index 0000000..b43eba6 --- /dev/null +++ b/clients/pyclient.py @@ -0,0 +1,56 @@ +# coding=UTF-8 + +''' +Multi Laser client example + +LICENCE : CC +''' + +import redis + +# IP defined in /etd/redis/redis.conf +redisIP = '127.0.0.1' + +r = redis.StrictRedis(host=redisIP, port=6379, db=0) + +# (x,y,color in integer) 65280 is color #00FF00 +# Green rectangular shape : +pl0 = [(100,300,65280),(200,300,65280),(200,200,65280),(100,200,65280)] + + +# If you want to use rgb for color : +def rgb2int(r,g,b): + return int('0x%02x%02x%02x' % (r,g,b),0) + +# White rectangular shape +pl1 = [(100,300,rgb2int(255,255,255)),(200,300,rgb2int(255,255,255)),(200,200,rgb2int(255,255,255)),(100,200,rgb2int(255,255,255))] + +# Send to laser 0 (see mainy.conf) +r.set('/pl/0', str(pl0)) + +# Send to laser 1 (see mainy.conf) +r.set('/pl/1', str(pl1)) + +r.set('/pl/2', str(pl1)) + +''' +You can also use PolyLineOneColor or rPolylineOneColor to stack n point lists to build a "frame" + +import framy + +# for laser0 : + +pl0 = [(100,300),(200,300),(200,200),(100,200)] +framy.PolyLineOneColor(pl0, rgb2int(255,255,255), 0 , closed = False) +# You can add as much polylineOneColor as you want = construct a "frame" +# Then send it to the laser server : +print "All one color lines sent to laser 0 :",framy.LinesPL(0) # Will be True is sent correctly + +# instead of PolyLineOneColor you can use rPolylineOneColor to send 2D point list around 0,0 with 3D rotation,resizing and repositioning at xpos ypos +# rPolylineOneColor is very useful to add different polylines to different position. Imagine different game elements. +# rPolyLineOneColor(xy_list, c, PL , closed, xpos = 0, ypos =0, resize =1, rotx =0, roty =0 , rotz=0): +# Send the pl0 to laser 1 + +framy.rPolyLineOneColor((pl0, c = rgb2int(255,255,255), PL = 1, closed = False, xpos = 200, ypos = 250, resize = 1, rotx =0, roty =0 , rotz=0) +print "All one color lines sent to laser 1 :",framy.LinesPL(1) # Will be True is sent correctly +''' \ No newline at end of file diff --git a/clients/rebclient.r b/clients/rebclient.r new file mode 100644 index 0000000..881e137 --- /dev/null +++ b/clients/rebclient.r @@ -0,0 +1,39 @@ +REBOL [] + +outport: open/lines tcp://localhost:13857 +on: 1 +off: 0 + +pl0: "[(100,200, 0), (100,300, 65280), (200,300, 65280), (200,200, 65280), (100,200, 65280)]" + + +oscommand: to-string reduce ["pl/0 " pl0] +insert outport oscommand + +for counter 1 2 1 [ + ;; print counter + oscommand: to-string reduce ["/40h/clear " on] + insert outport oscommand + wait 0.3 + +] + +for counter 1 2 1 [ + for raw 0 7 1 [ + oscommand: to-string reduce ["/40h/led_row " raw " " on] + insert outport oscommand + wait 0.001 + ] + +] + +for counter 1 2 1 [ + ;; print counter + oscommand: to-string reduce ["/40h/frame 0 126 126 126 126 126 126 0"] + insert outport oscommand + + wait 0.3 + +] + +close outport \ No newline at end of file diff --git a/clients/rebserver.py b/clients/rebserver.py new file mode 100644 index 0000000..bf92910 --- /dev/null +++ b/clients/rebserver.py @@ -0,0 +1,47 @@ + +#!/usr/bin/env python +# coding=UTF-8 +""" + +TCP server for rebol links like from Amiga +Forward /pl/lasernumber pointslist to redis server + + +by Sam Neurohack +from /team/laser + +""" + +import socket, time,random, redis + + +r = redis.StrictRedis(host=gstt.LjayServerIP, port=6379, db=0) + + + +# TCP listener + +TCP_IP = '127.0.0.1' +TCP_PORT = 13857 +BUFFER_SIZE = 1024 # Normally 1024, but we want fast response + +s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +s.bind((TCP_IP, TCP_PORT)) +s.listen(1) +conn, addr = s.accept() +print 'Connection address:', addr + + +while 1: + data = conn.recv(BUFFER_SIZE) + if not data: break + #print "received data:", data + commands = data.split() + nb_oscargs = len(commands) + print commands + + #r.set('/pl/'+str(PL), str(something to code with commands, nb_oscargs)) + #conn.send(data) # echo + + +conn.close() \ No newline at end of file diff --git a/clients/redclient.red b/clients/redclient.red new file mode 100644 index 0000000..40056cf --- /dev/null +++ b/clients/redclient.red @@ -0,0 +1,8 @@ +Red [] + + +#https://github.com/red/red/wiki/[DOC]-Guru-Meditations#how-to-make-http-requests + +pl0: "[(100,200, 0), (100,300, 65280), (200,300, 65280), (200,200, 65280), (100,200, 65280)]" + +read http://127.0.0.1:13857/path?name="jones" \ No newline at end of file diff --git a/clients/redserver.py b/clients/redserver.py new file mode 100644 index 0000000..8bc226e --- /dev/null +++ b/clients/redserver.py @@ -0,0 +1,57 @@ + +#!/usr/bin/env python +# coding=UTF-8 +""" + +Http server for red 0.6.4 +Forward /pl/lasernumber pointslist to redis server + +by Sam Neurohack +from /team/laser + +""" + + +import redis + +r = redis.StrictRedis(host=gstt.LjayServerIP, port=6379, db=0) + +from BaseHTTPServer import BaseHTTPRequestHandler,HTTPServer + +PORT_NUMBER = 8080 + +#This class will handles any incoming request from +#the browser +class myHandler(BaseHTTPRequestHandler): + + #Handler for the GET requests + def do_GET(self): + self.send_response(200) + self.send_header('Content-type','text/html') + self.end_headers() + + # Send the html message + self.wfile.write("Hello World !") + + # r.set('/pl/'+str(PL), str(self.grid_points)) + + return + +try: + #Create a web server and define the handler to manage the + #incoming request + server = HTTPServer(('', PORT_NUMBER), myHandler) + print 'Started httpserver on port ' , PORT_NUMBER + + #Wait forever for incoming htto requests + server.serve_forever() + +except KeyboardInterrupt: + print '^C received, shutting down the web server' +server.socket.close() + + + + + + diff --git a/gstt.py b/gstt.py new file mode 100644 index 0000000..561ff49 --- /dev/null +++ b/gstt.py @@ -0,0 +1,230 @@ +# coding=UTF-8 +''' +Etat global (anciennement singleton de la classe GameState + autres VARIABLES nécessaires partout)" +''' + +#from globalVars import * + +#ConfigName = "setexample.conf" +ConfigName = "mainy.conf" + +debug = 0 + +anims= [[],[],[],[]] + +# How many lasers are connected. Different that "currentlaser" used by bhorosc +LaserNumber = 2 +screen_size = [800,600] +xy_center = [screen_size[0]/2,screen_size[1]/2] + +# Will be overriden by mainy.conf file data +LjayServerIP = '192.168.1.13' +oscIPin = '192.168.1.15' +nozoscip = '192.168.1.15' + +# gstt.Laser select to what laser modifcation will occur. +# Can be changed with /noteon 16-23 +Laser = 2 + +# gstt.simuPL select what point list number to display in pygame simulator +# Can be changed with /noteon 24-31 +simuPL = 1 + +# gstt.laserIPS. Will be overridden by the ConfigName (see below) file values +lasersIPS = ['192.168.1.5','192.168.1.6','192.168.1.3','192.168.1.4'] + + +# gstt.laserPLS : What point list is sent to what laser. +# ** Will be overridden by the ConfigName (see below) file values ** +lasersPLS = [0,1,2,0] + + +# gstt.kpps stores kpps for each laser. +# ** Will be overridden by the ConfigName (see below) file values ** +kpps = [25000,25000,25000,25000] + +# gstt.GridDisplay : if = 1 Curve points actually sent to PL are replaced by a grid +GridDisplay = [0,0,0,0] + +# with 4 laser available, 4 PL only are necessary +PL = [[],[],[],[]] + + +# Transformation Matrix for each laser +EDH = [[], [], [], []] + +# Laser states +# ipconn is initial newdac to its etherdream +lstt_ipconn = [[-1], [-1], [-1], [-1]] +# dacstt is dac light engine state +lstt_dacstt = [[-1], [-1], [-1], [-1]] +# store last dac answers ACK, not ACK +lstt_dacanswers = [[-1], [-1], [-1], [-1]] +# store last number of points sent to etherdreams buffer +lstt_points = [[0], [0], [0], [0]] + +swapX = [1,1,1,-1] +swapY = [1,1,1,-1] + +maxCurvesByLaser = 4 + + +# For glitch art : change position and decrease number of points added by newdac.py +# shortline for lines shorter than 4000 (in etherdream coordinates) +# i.e (0.25,3) means add 3 points at 25% on the line. +stepshortline = [ (1.0, 8)] +stepslongline = [ (0.25, 3), (0.75, 3), (1.0, 10)] + + +#curveColor = [255,0,0] * maxCurvesByLaser +#curveColor = [[0 for _ in range(3)] for _ in range(maxCurvesByLaser)] +curveColor = [[255 for _ in range(3)] for _ in range(maxCurvesByLaser)] +colorX = [[255 for _ in range(3)] for _ in range(maxCurvesByLaser)] +colorY = [[255 for _ in range(3)] for _ in range(maxCurvesByLaser)] +offsetX = [0] * maxCurvesByLaser +offsetY = [0] * maxCurvesByLaser +curveNumber = 0 +Curve = curveNumber +XTimeAxe=30000 +YTimeAxe=30000 + +#curveX = [255,255,255] * maxCurvesByLaser +#curveY = [255,255,255] * maxCurvesByLaser + +Mode = 5 + +point = [0,0,0] + +# gstt.colormode select what to display. Can be changed with /noteon 57-64 +colormode = 0 +color = [255,255,255] +newcolor = 0 + +surpriseoff = 10 +surpriseon = 50 +surprisey = -10 +surprisex = -10 + + +cc = [0] * 256 +lfo = [0] * 10 +osc = [0] * 255 +oscInUse = [0] * 255 +knob = [0] * 33 + +stars0=[] +stars1=[] +stars2=[] +#stars3=[] +# Viewer distance (cc 21) +cc[21]=60 +viewer_distance = cc[21] * 8 + +# fov (cc 22) +cc[22]= 60 +fov = 4 * cc[22] + + + +''' +Also vailable with args : -v Value + +if debug = 1 you get : + + +if debug = 2 you get : +- dac errors + +''' + + +JumpFlag =0 + + +# nice X (cc 5) Y (cc 6) curve at first +cc[5] = cc[6] = 60 + +# Dot mode start at middle screen +cc[1] = cc[2] = 63 + +note = 0 +velocity = 0 + +WingHere = -1 +BhorealHere = -1 +LaunchHere = -1 +BhorLeds = [0] * 64 + +oscx = 0 +oscy = 0 +oscz = 0 + + +# Ai Parameters + +aivelocity = 0.5 +aiexpressivity = 0.5 +aisensibility = 0.5 +aibeauty = 0.5 + + +# OSC ports +#temporaray fix hack : iport=nozoport +iport = 8001 #LJay (bhorosc) input port +oport = 8002 #LJay (bhorosc) output port +noziport=8003 #nozosc.py receiving commands port +nozoport=8001 #nozosc.py sending port to LJay (main.py) +nozuport=0 #linux serial usb port connecting nozoid devices ACM0 by default + + +X = [0] * maxCurvesByLaser +Y = [0] * maxCurvesByLaser + +# No rotation X (cc 29) Y (cc 30) Z (cc 31) at first +cc[29] = cc[30] = cc[31] = prev_cc29 = 0 +prev_cc29 = prev_cc30 = prev_cc31 = -1 + +angleX = 0 +angleY = 0 +angleZ = 0 + +tomidi = False # currently tomidi bypass all other directions +todmx = False +toled = False +tolaser = True +tosynth = False + +sernozoid = "" +nozoid = "" +serdmx = "" +newnumber = "" +oldnumber = "" + +''' +# will be overrided but settings.conf values. +# legacy one laser only values +centerx = LASER_CENTER_X +centery = LASER_CENTER_Y +zoomx = LASER_ZOOM_X +zoomy = LASER_ZOOM_Y +sizex = LASER_SIZE_X +sizey = LASER_SIZE_Y +finangle = LASER_ANGLE +''' + +# multilasers arrays +# will be overrided but settings.conf values. +centerX = [0,0,0,0] +centerY = [0,0,0,0] +zoomX = [0,0,0,0] +zoomY = [0,0,0,0] +sizeX = [0,0,0,0] +sizeY = [0,0,0,0] +finANGLE = [0,0,0,0] + +warpdest = [[[ 1. , 0. , 0.],[ 0. , 1. , 0.],[ 0. , 0. , 1.]], +[[ 1. , 0. , 0.],[ 0. , 1. , 0.],[ 0. , 0. , 1.]], +[[ 1. , 0. , 0.],[ 0. , 1. , 0.],[ 0. , 0. , 1.]], +[[ 1. , 0. , 0.],[ 0. , 1. , 0.],[ 0. , 0. , 1.]] +] + diff --git a/homographyp.py b/homographyp.py new file mode 100755 index 0000000..2270432 --- /dev/null +++ b/homographyp.py @@ -0,0 +1,246 @@ + +#!/usr/bin/python2.7 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + +''' + +LJay/LJ +v0.7.0 + +LICENCE : CC +Sam Neurohack + +Homographies for align + swap corrections and warp corrections + +Align + swap homography if found with 4 original points and corrected coordinates +Warp correction is disabled for the moment. Should be computed at warp edition : set 1 curve 1 + +Use the : + +######################################################################## +# Module to compute homographies # +# # +# Author : Alexis Mignon # +# email : alexis.mignon@info.unicaen.fr # +# date : 10/03/2010 # +######################################################################## + + Module to compute homographies between two sets of 2D points + + implemented functions : + - find_homography(points1,points2) : finds the homography between + two sets of 2D points + - find_affine_homography(points1,points2) : finds the affine + homography between two sets of 2D points + - apply_homography(H,points) : applies homography H to the set of + 2D points 'points' + + example : + >>> from homography import * + >>> + >>> points1 = np.array([[ 0., 0. ], + >>> [ 1., 0. ], + >>> [ 0., 1. ], + >>> [ 1., 1. ]]) + >>> + >>> points2 = np.array([[ 0. , 0. ], + >>> [ 1. , 0. ], + >>> [ 0.25, 1. ], + >>> [ 0.75, 1. ]]) + >>> + >>> points3 = np.array([[-1., 0.], + >>> [ 0.,-1.], + >>> [ 0., 1.], + >>> [ 1., 0.]]) + >>> + >>> H1 = find_homography(points1,points2) + >>> print H1 + >>> print apply_homography(H1,points1) + >>> H2 = find_affine_homography(points1,points3) + >>> print H2 + >>> print apply_homography(H2,points1) +''' + + +import numpy as np +import math +from scipy.linalg import svd,lstsq +import ast +import gstt +#from globalVars import xy_center +import redis + + +r = redis.StrictRedis(host=gstt.LjayServerIP, port=6379, db=0) + +def find(points1,points2): + if points1.shape[0] != points2.shape[0] : raise ValueError("The number of input and output points mismatches") + if points1.shape[1] == 2 : + p1 = np.ones((len(points1),3),'float64') + p1[:,:2] = points1 + elif points1.shape[1] == 3 : p1 = points1 + else : raise ValueError("Bad shape for input points") + + if points2.shape[1] == 2 : + p2 = np.ones((len(points2),3),'float64') + p2[:,:2] = points2 + elif points2.shape[1] == 3 : p2 = points2 + else : raise ValueError("Bad shape for output points") + + npoints = len(points1) + + A = np.zeros((3*npoints,9),'float64') + + for i in xrange(npoints): + p1i = p1[i] + x2i,y2i,w2i = p2[i] + xpi = x2i*p1i + ypi = y2i*p1i + wpi = w2i*p1i + + A[i*3 ,3:6] = -wpi + A[i*3 ,6:9] = ypi + A[i*3+1,0:3] = wpi + A[i*3+1,6:9] = -xpi + A[i*3+2,0:3] = -ypi + A[i*3+2,3:6] = xpi + + U,s,Vt = svd(A,full_matrices = False, overwrite_a = True) + del U,s + h = Vt[-1] + H = h.reshape(3,3) + return H + +def find_affine(points1,points2): + if points1.shape[0] != points2.shape[0] : raise ValueError("The number of input and output points mismatches") + if points1.shape[1] == 2 : + p1 = np.ones((len(points1),3),'float64') + p1[:,:2] = points1 + elif points1.shape[1] == 3 : p1 = points1 + else : raise ValueError("Bad shape for input points") + + if points2.shape[1] == 2 : + p2 = np.ones((len(points2),3),'float64') + p2[:,:2] = points2 + elif points2.shape[1] == 3 : p2 = points2 + else : raise ValueError("Bad shape for output points") + + npoints = len(points1) + + A = np.zeros((3*npoints,6),'float64') + b = np.zeros((3*npoints,1),'float64') + for i in xrange(npoints): + p1i = p1[i] + x2i,y2i,w2i = p2[i] + xpi = x2i*p1i + ypi = y2i*p1i + wpi = w2i*p1i + + A[i*3 ,3:6] = -wpi + A[i*3+1,0:3] = wpi + A[i*3+2,0:3] = -ypi + A[i*3+2,3:6] = xpi + + b[i*3 ] = -y2i*p1i[2] + b[i*3+1] = x2i*p1i[2] + + h = lstsq(A,b,overwrite_a = True, overwrite_b = True)[0] + H = np.zeros( (3,3) , 'float64' ) + H[:2,:] = h.reshape(2,3) + H[2,2] = 1 + return H + +def apply(H,points): + + p = np.ones((len(points),3),'float64') + p[:,:2] = points + pp = np.dot(p,H.T) + pp[:,:2]/=pp[:,2].reshape(len(p),1) + return pp[:,:2] + +# Align and axis swap corrections +# Reference points +pointsref = np.array([(300.0, 400.0), (500.0, 400.0), (500.0, 200.0), (300.0, 200.0)]) + +def EDpoint(mylaser,(pygamex,pygamey)): + + #print "current point : ", pygamex, pygamey + XX = pygamex - gstt.xy_center[0] + YY = pygamey - gstt.xy_center[1] + CosANGLE = math.cos(gstt.finANGLE[mylaser]) + SinANGLE = math.sin(gstt.finANGLE[mylaser]) + # Multilaser style + x = (gstt.xy_center[0] + ((XX * CosANGLE) - (YY * SinANGLE)) - gstt.xy_center[0]) * gstt.zoomX[mylaser] + gstt.centerX[mylaser] + y = (gstt.xy_center[1] + ((XX * SinANGLE) + (YY * CosANGLE)) - gstt.xy_center[1]) * gstt.zoomY[mylaser] + gstt.centerY[mylaser] + + if gstt.debug >0: + + #print "global center :", xy_center + + print "Laser :", mylaser, "center at : ", gstt.centerX[mylaser], gstt.centerY[mylaser] + ''' + print "swaps : ", (gstt.swapX[mylaser]), str(gstt.swapY[mylaser]) + print "zooms : ", gstt.zoomX[mylaser], gstt.zoomY[mylaser] + print "angles : ", gstt.finANGLE[mylaser] + ''' + print "result : ", x * gstt.swapX[mylaser] , y * gstt.swapY[mylaser] + return [x * gstt.swapX[mylaser] , y * gstt.swapY[mylaser]] + +''' +def EDpoint((pygamex,pygamey)): + + XX = pygamex - xy_center[0] + YY = pygamey - xy_center[1] + CosANGLE = math.cos(finangle) + SinANGLE = math.sin(finangle) + # Multilaser style + x = (xy_center[0] + ((XX * CosANGLE) - (YY * SinANGLE)) - xy_center[0]) * zoomx + centerx + y = (xy_center[1] + ((XX * SinANGLE) + (YY * CosANGLE)) - xy_center[1]) * zoomy + centery + + return [x*1, y*1] +''' + + +# New total homography from always the same reference points : ED (= align + swap) transform + warp transform. +# WARP IS DISABLED. Some bug tracking is needed ! +def newEDH(mylaser): + + EDpoints = [] + for point in xrange(4): + EDpoints.append(EDpoint(mylaser,pointsref[point])) + + # H matrix tansform pygame points in Etherdream system with align and swap correction, + H = find(pointsref, np.array(EDpoints)) + + # Computer Hwarp matrix with previously reference warped points in configuration file. + Hwarp = find(pointsref, gstt.warpdest[mylaser]) + #Hwarp = np.identity(3, dtype = float) + # EDH matrix + gstt.EDH[mylaser] = H + + # EDH matrix is H x Hwarp + #gstt.EDH[mylaser] = np.dot(H,Hwarp) + print "Laser",mylaser,"NEW EDH computed, sending to redis..." + r.set('/EDH/'+str(mylaser), np.array2string(gstt.EDH[mylaser], separator=',')) + + # Laser bit 0 = 0 and bit 1 = 1 : New EDH + order = r.get('/order') + print order + neworder = order & ~(1<< mylaser*2) + neworder = neworder | (1<< 1+mylaser*2) + r.set('/order', str(neworder)) + + if gstt.debug >1: + print "" + print "laser ", mylaser + print "reference points", pointsref + print "laser EDpoints :", EDpoints + print "-> Computed H :",H + #print "warped points coordinates ", gstt.warpdest[mylaser] + #print "-> Computed Hwarp", Hwarp + #print "laser ", mylaser, "warpd ",ast.literal_eval(gstt.warpdest[gstt.Laser]) + #print "laser ", mylaser, "Hwarp ", Hwarp + #print "" + print "-> new EDH :", gstt.EDH[mylaser] + diff --git a/las.py b/las.py new file mode 100644 index 0000000..f2a4e6f --- /dev/null +++ b/las.py @@ -0,0 +1,230 @@ +# coding=UTF-8 +""" + +LJ OSC handler +v0.7.0 + + +LICENCE : CC +by Sam Neurohack, Loloster, +from /team/laser + + +""" + +import types, time +import gstt + +#import colorify +import homographyp +import settings +#import alignp +import redis + + +r = redis.StrictRedis(host=gstt.LjayServerIP , port=6379, db=0) + +def GridOn(laser): + + print "Grid for laser ", laser + # Grid PL is Laser bit 0 = 1 and bit 1 = 1 + order = r.get('/order') + neworder = order | (1<0: + print "" + print "default handler" + print "Bhorosc said for laser",laser,": ", path, oscpath, args + + + # /grid/lasernumber value (0 or 1) + if oscpath[1] == "grid": + + if args[0] == "1": + print "Grid requested for laser ", laser + GridOn(laser) + else: + print "No grid for laser ", laser + UserOn(laser) + + + # /black/lasernumber value (0 or 1) + if oscpath[1] == "black": + + if args[0] == "1": + print "Grid requested for laser ", laser + BlackOn(laser) + else: + print "No grid for laser ", laser + UserOn(laser) + + + + # /ip/lasernumber value + if oscpath[1] == "ip": + print "New IP for laser ", laser + gstt.lasersIPS[laser]= args[0] + NewEDH(laser) + + # /kpps/lasernumber value + # Live change of kpps is not implemented in newdac.py. Change will effect next startup. + if oscpath[1] == "kpps": + print "New kpps for laser ", laser, " next startup", args[0] + gstt.kpps[laser]= int(args[0]) + NewEDH(laser) + + # /angle/lasernumber value + if oscpath[1] == "angle": + print "New Angle modification for laser ", oscpath[2], ":", args[0] + gstt.finANGLE[laser] += int(args[0]) + homographyp.newEDH(laser) + NewEDH(laser) + + + + # /intens/lasernumber value + if oscpath[1] == "intens": + print "New intensity requested for laser ", oscpath[2], ":", args[0] + print "Change not implemented yet" + + + + # /mouse/lasernumber value (0 or 1) + if oscpath[1] == "mouse": + + if args[0] == "1": + print "Mouse requested for laser ", oscpath[2] + gstt.Laser = oscpath[2] + else: + print "No mouse for laser ", oscpath[2] + + + # /swap/X/lasernumber value (0 or 1) + if oscpath[1] == "swap" and oscpath[2] == "X": + + if args[0] == "0": + print "swap X : -1 for laser ", laser + gstt.swapX[laser]= -1 + homographyp.newEDH(laser) + NewEDH(laser) + + else: + print "swap X : 1 for laser ", laser + gstt.swapX[laser]= 1 + homographyp.newEDH(laser) + NewEDH(laser) + + # /swap/Y/lasernumber value (0 or 1) + if oscpath[1] == "swap" and oscpath[2] == "Y": + if args[0] == "0": + print "swap Y : -1 for laser ", laser + gstt.swapY[laser]= -1 + homographyp.newEDH(laser) + NewEDH(laser) + else: + print "swap Y : 1 for laser ", laser + gstt.swapY[laser]= 1 + homographyp.newEDH(laser) + NewEDH(laser) + + # /loffset/X/lasernumber value + if oscpath[1] == "loffset" and oscpath[2] == "X": + print "offset/X laser ", laser, "modified : ", args[0] + gstt.centerX[laser] -= int(args[0]) + homographyp.newEDH(laser) + NewEDH(laser) + + # /loffset/Y/lasernumber value + if oscpath[1] == "loffset" and oscpath[2] == "Y": + print "offset/Y laser ", laser, "modified : ", args[0] + gstt.centerY[laser] -= int(args[0]) + homographyp.newEDH(laser) + NewEDH(laser) + + + # /scale/X/lasernumber value + if oscpath[1] == "scale" and oscpath[2] == "X": + print "scale/X laser ", laser , "modified : ", args[0] + gstt.zoomX[laser] += int(args[0]) + homographyp.newEDH(laser) + NewEDH(laser) + + # /scale/Y/lasernumber value + if oscpath[1] == "scale" and oscpath[2] == "Y": + print "scale/Y laser ", laser, "modified : ", args[0] + gstt.zoomY[laser] += int(args[0]) + homographyp.newEDH(laser) + NewEDH(laser) + +''' +For reference values of EDH modifier if assign to keyboard keys (was alignp) + + gstt.centerY[gstt.Laser] -= 20 + + gstt.centerY[gstt.Laser] += 20 + + gstt.zoomX[gstt.Laser]-= 0.1 + + gstt.zoomX[gstt.Laser] += 0.1 + gstt.zoomY[gstt.Laser] -= 0.1 + + gstt.zoomY[gstt.Laser] += 0.1 + + gstt.sizeX[gstt.Laser] -= 50 + + gstt.sizeX[gstt.Laser] += 50 + + gstt.sizeY[gstt.Laser] -= 50 + + gstt.sizeY[gstt.Laser] += 50 + + gstt.finANGLE[gstt.Laser] -= 0.001 + + gstt.finANGLE[gstt.Laser] += 0.001 +''' diff --git a/mainy.conf b/mainy.conf new file mode 100644 index 0000000..e0a7cdf --- /dev/null +++ b/mainy.conf @@ -0,0 +1,84 @@ +[General] +set = 5 +curve = 0 +lasernumber = 3 +ljayserverip = 127.0.0.1 +nozoscip = 127.0.0.1 +bhoroscip = 127.0.0.1 + +[laser0] +pl = 0 +color = -1 +ip = 192.168.1.4 +kpps = 25000 +centerx = 0 +centery = 0 +zoomx = 49.2 +zoomy = 49.0 +sizex = 31450 +sizey = 32000 +finangle = 0.0 +swapx = 1 +swapy = -1 +warpdest = [[-1500., 1500.], + [ 1500., 1500.], + [ 1500.,-1500.], + [-1500.,-1500.]] + +[laser1] +pl = 1 +color = -1 +ip = 192.168.1.5 +kpps = 25000 +centerx = 0 +centery = 0 +zoomx = 48.5 +zoomy = 50.1 +sizex = 32000 +sizey = 32000 +finangle = 0.0 +swapx = 1 +swapy = 1 +warpdest = [[-1500., 1500.], + [ 1500., 1500.], + [ 1500.,-1500.], + [-1500.,-1500.]] + +[laser2] +pl = 2 +color = -1 +ip = 192.168.1.6 +kpps = 25000 +centerx = 0 +centery = 0 +zoomx = 47.8 +zoomy = 39.3 +sizex = 30600 +sizey = 32000 +finangle = -0.008 +swapx = 1 +swapy = 1 +warpdest = [[-1500., 1500.], + [ 1500., 1500.], + [ 1500.,-1500.], + [-1500.,-1500.]] + +[laser3] +pl = 3 +color = -1 +ip = 192.168.1.3 +kpps = 25000 +centerx = 0 +centery = 0 +zoomx = 38.0 +zoomy = 26.0 +sizex = 32000 +sizey = 32000 +finangle = 0.0 +swapx = -1 +swapy = -1 +warpdest = [[-1500., 1500.], + [ 1500., 1500.], + [ 1500.,-1500.], + [-1500.,-1500.]] + diff --git a/mainyservers.py b/mainyservers.py new file mode 100644 index 0000000..57b2223 --- /dev/null +++ b/mainyservers.py @@ -0,0 +1,534 @@ +''' +LJ Servers v0.8 + +Laser server + webUI servers (ws + OSC) + +- get point list to draw : /pl/lasernumber +- for report /lstt/lasernumber /lack/lasernumber /cap/lasernumber + +todo : + +r.set('/resampler/0', '[ (1.0, 8),(0.25, 3), (0.75, 3), (1.0, 10)]') +r.set('/resampler/1', '[ (1.0, 8),(0.25, 3), (0.75, 3), (1.0, 10)]') +r.set('/resampler/2', '[ (1.0, 8),(0.25, 3), (0.75, 3), (1.0, 10)]') +r.set('/resampler/3', '[ (1.0, 8),(0.25, 3), (0.75, 3), (1.0, 10)]') + +''' +from __future__ import absolute_import +import time +import gstt +import redis + + +print "" +print "" +print "LJ Laser Servers" +print "v0.8.0" +print "" + +import settings +settings.Read() + +from multiprocessing import Process, Queue, TimeoutError +import random, ast + +import newdacp +import homographyp +import las + + +from OSC import OSCServer, OSCClient, OSCMessage +from websocket_server import WebsocketServer +#import socket +import types, thread, time + +r = redis.StrictRedis(host=gstt.LjayServerIP , port=6379, db=0) + +def dac_process(number, pl): + while True: + try: + d = newdacp.DAC(number,pl) + d.play_stream() + except Exception as e: + + import sys, traceback + if gstt.debug == 2: + print '\n---------------------' + print 'Exception: %s' % e + print '- - - - - - - - - - -' + traceback.print_tb(sys.exc_info()[2]) + print "\n" + pass + + except KeyboardInterrupt: + sys.exit(0) + + + +''' +def Laserver(): + + + + + #for laserid in range(0,4): + # r.set('/lack/'+str(laserid),0) + # r.set('/lstt/'+str(laserid),0) + + + + # Some random lists for all lasers at launch. + print "" + print "Creating startup point lists..." + + random_points = [(300.0+random.randint(-100, 100), 200.0+random.randint(-100, 100), 0), (500.0+random.randint(-100, 100), 200.0+random.randint(-100, 100), 65280), (500.0+random.randint(-100, 100), 400.0+random.randint(-100, 100), 65280), (300.0+random.randint(-100, 100), 400.0+random.randint(-100, 100), 65280), (300.0+random.randint(-100, 100), 200.0+random.randint(-100, 100), 65280)] + if r.set('/pl/0', str(random_points)) == True: + print "/pl/0 ", ast.literal_eval(r.get('/pl/0')) + + random_points = [(300.0+random.randint(-100, 100), 200.0+random.randint(-100, 100), 0), (500.0+random.randint(-100, 100), 200.0+random.randint(-100, 100), 65280), (500.0+random.randint(-100, 100), 400.0+random.randint(-100, 100), 65280), (300.0+random.randint(-100, 100), 400.0+random.randint(-100, 100), 65280), (300.0+random.randint(-100, 100), 200.0+random.randint(-100, 100), 65280)] + if r.set('/pl/1', str(random_points)) == True: + print "/pl/1 ", ast.literal_eval(r.get('/pl/1')) + + random_points = [(300.0+random.randint(-100, 100), 200.0+random.randint(-100, 100), 0), (500.0+random.randint(-100, 100), 200.0+random.randint(-100, 100), 65280), (500.0+random.randint(-100, 100), 400.0+random.randint(-100, 100), 65280), (300.0+random.randint(-100, 100), 400.0+random.randint(-100, 100), 65280), (300.0+random.randint(-100, 100), 200.0+random.randint(-100, 100), 65280)] + if r.set('/pl/2', str(random_points)) == True: + print "/pl/2 ", ast.literal_eval(r.get('/pl/2')) + + random_points = [(300.0+random.randint(-100, 100), 200.0+random.randint(-100, 100), 0), (500.0+random.randint(-100, 100), 200.0+random.randint(-100, 100), 65280), (500.0+random.randint(-100, 100), 400.0+random.randint(-100, 100), 65280), (300.0+random.randint(-100, 100), 400.0+random.randint(-100, 100), 65280), (300.0+random.randint(-100, 100), 200.0+random.randint(-100, 100), 65280)] + if r.set('/pl/3', str(random_points)) == True: + print "/pl/3 ", ast.literal_eval(r.get('/pl/3')) + + + # Order all lasers to show these random shapes at startup -> tell all 4 laser process to USER PLs + r.set('/order', "0") + + + # Launch one process (a newdacp instance) by etherdream + + print "" + dac_worker0= Process(target=dac_process,args=(0,0)) + print "Launching Laser 0 Process..." + dac_worker0.start() + + if lasernumber >0: + dac_worker1= Process(target=dac_process,args=(1,0)) + print "Launching Laser 1 Process..." + dac_worker1.start() + + if lasernumber >1: + dac_worker2= Process(target=dac_process,args=(2,0)) + print "Launching Laser 2 Process..." + dac_worker2.start() + + if lasernumber >2: + dac_worker3= Process(target=dac_process,args=(3,0)) + print "Launching Laser 3 Process..." + dac_worker3.start() + + # Main loop do nothing. Maybe do the webui server ? + try: + #while True: + + # Websocket startup + + server = WebsocketServer(wsPORT,host=serverIP) + # Launch OSC thread listening to Bhorosc + print "" + print "Launching webUI OSC Handler..." + thread.start_new_thread(osc_thread, ()) + # Default OSC handler for all incoming message from Bhorosc + oscserver.addMsgHandler("default", handler) + + #print server + print "" + print "Launching webUI Websocket server..." + print "at :", serverIP, "port :",wsPORT + server.set_fn_new_client(new_client) + server.set_fn_client_left(client_left) + server.set_fn_message_received(message_received) + server.run_forever() + print "" + print "Running..." + + except KeyboardInterrupt: + pass + + # Gently stop on CTRL C + + finally: + + dac_worker0.join() + if lasernumber >0: + dac_worker1.join() + if lasernumber >1: + dac_worker2.join() + if lasernumber >2: + dac_worker3.join() + + + for laserid in range(0,lasernumber+1): + print "reset redis values for laser",laserid + r.set('/lack/'+str(laserid),64) + r.set('/lstt/'+str(laserid),64) + r.set('/cap/'+str(laserid),0) + + print "Fin des haricots" +''' + + +# +# webUI server +# + + +serverIP = gstt.LjayServerIP +print "Redis IP :", serverIP + +bhoroscIP = gstt.oscIPin +print "Bhorosc IP :", bhoroscIP + +nozoscIP = gstt.nozoscip +print "Nozosc IP :", nozoscIP + +debug = gstt.debug +print "Debug :", debug + +lasernumber = gstt.LaserNumber -1 +print "Lasers requested :", gstt.LaserNumber + + +# Websocket listening port +wsPORT = 9001 + +# With Bhorosc +# OSC Server : relay OSC message from Bhorosc outport 8002 to UI +#oscIPin = "192.168.1.10" +bhoroscIPin = serverIP +bhoroscPORTin = 8002 + +# OSC Client : relay message from UI to Bhorosc inport 8001 +bhoroscIPout = bhoroscIP +bhoroscPORTout = 8001 + + +# With Nozosc +# OSC Client : relay message from UI to Nozosc inport 8003 +NozoscIPout = nozoscIP +NozoscPORTout = 8003 + + +# +# OSC part +# + +print "" +print "Launching Bhorosc commands receiver..." +print "at", bhoroscIPin, "port",str(bhoroscPORTin) +oscserver = OSCServer( (bhoroscIPin, bhoroscPORTin) ) +oscserver.timeout = 0 +OSCRunning = True + + +def handle_timeout(self): + self.timed_out = True + +oscserver.handle_timeout = types.MethodType(handle_timeout, oscserver) + +osclientbhorosc = OSCClient() +oscmsg = OSCMessage() +osclientbhorosc.connect((bhoroscIPout, bhoroscPORTout)) + +# send UI string as OSC message to Bhorosc 8001 +# sendbhorosc(oscaddress, [arg1, arg2,...]) + +def sendbhorosc(oscaddress,oscargs=''): + + oscmsg = OSCMessage() + oscmsg.setAddress(oscaddress) + oscmsg.append(oscargs) + + #print ("sending to bhorosc : ",oscmsg) + try: + osclientbhorosc.sendto(oscmsg, (bhoroscIPout, bhoroscPORTout)) + oscmsg.clearData() + except: + print ('Connection to bhorosc refused : died ?') + sendWSall("/on 0") + sendWSall("/status NoLJay") + pass + #time.sleep(0.001) + + +# send UI string as OSC message to Nozosc 8003 +# sendnozosc(oscaddress, [arg1, arg2,...]) + +def sendnozosc(oscaddress,oscargs=''): + + oscmsg = OSCMessage() + oscmsg.setAddress(oscaddress) + oscmsg.append(oscargs) + + #print ("sending to nozosc : ",oscmsg) + try: + osclientnozosc.sendto(oscmsg, (NozoscIPout, NozoscPORTout)) + oscmsg.clearData() + except: + print ('Connection to nozosc refused : died ?') + sendWSall("/on 0") + sendWSall("/status No Nozosc ") + pass + #time.sleep(0.001) + +# NOT USED see las.py +# OSC default path handler : send OSC message from Bhorosc 8002 to UI via websocket 9001 +def handler(path, tags, args, source): + + oscpath = path.split("/") + pathlength = len(oscpath) + if debug >0: + print "" + print "default handler" + print "Bhorosc said : ", path, oscpath, args + sendWSall(path + " " + str(args[0])) + + ''' + # /lstt/number value + if oscpath[1] == "lstt": + sendWSall(path + " " + str(args[0])) + # /status string + if oscpath[1] == "status": + sendWSall(path + " " + str(args[0])) + ''' + + +# 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() + + + +# OSC Thread. Bhorosc handler and Automated status sender to UI. +def osc_thread(): + + print "Launching Automatic Dac status and bhorosc forwarder." + print "Will use Redis server IP ", serverIP + + ''' + r = redis.StrictRedis(host=serverIP, port=6379, db=0) + print "Connection to redis server.." + print "Running..." + ''' + + while True: + try: + while True: + + time.sleep(1) + osc_frame() + + + for laserid in range(0,lasernumber): # Laser not used -> led is not lit + + lstt = r.get('/lstt/'+ str(laserid)) + #print "laserid", laserid,"lstt",lstt + if lstt == "0": # Dac IDLE state(0) -> led is blue (3) + sendWSall("/lstt/" + str(laserid) + " 3") + if lstt == "1": # Dac PREPARE state (1) -> led is cyan (2) + sendWSall("/lstt/" + str(laserid) + " 2") + if lstt == "2": # Dac PLAYING (2) -> led is green (1) + sendWSall("/lstt/" + str(laserid) + " 1") + + # This is used not working : lack never change. Todo : retest. + lack= r.get('/lack/'+str(laserid)) + #print "laserid", laserid,"lack",lack + if lack == 'a': # Dac sent ACK ("a") -> led is green (1) + sendWSall("/lack/" + str(laserid) +" 1") + if lack == 'F': # Dac sent FULL ("F") -> led is orange (5) + sendWSall("/lack/" + str(laserid) +" 5") + if lack == 'I': # Dac sent INVALID ("I") -> led is yellow (4) + sendWSall("/lack/" + str(laserid)+" 4") + #print lack + + if lack == "64" or lack =="35": # no connection to dac -> leds are red (6) + sendWSall("/lack/" + str(laserid) + " 0") + sendWSall("/lstt/" + str(laserid) + " 0") + #sendWSall("/lstt/" + str(laserid) + " 0") + sendWSall("/points/" + str(laserid) + " 0") + + else: + # last number of points sent to etherdream buffer + sendWSall("/points/" + str(laserid) + " " + str(r.get('/cap/'+str(laserid)))) + + #sendWSall("/plframe/" + str(laserid) ) # + " " + str(r.get('/pl/'+str(laserid)))) + + # WIP Too much packets -> flood webUI : Draw all PL point lists in JS canvas in WebUI + + ''' + for pl in range(0,1): + bhorosc.sendosc("/plframe/" + str(pl),"") + for plpoint in range(0,len(gstt.PL[pl])): + bhorosc.sendosc("/plpoint/" + str(pl),"") + ''' + + + + except Exception as e: + import sys, traceback + print '\n---------------------' + print 'Exception: %s' % e + print '- - - - - - - - - - -' + traceback.print_tb(sys.exc_info()[2]) + print "\n" + + + +# +# Websocket part +# + +# Called for every WS client connecting (after handshake) +def new_client(client, server): + print("New WS client connected and was given id %d" % client['id']) + sendWSall("/status Hello %d" % client['id']) + +# Called for every WS client disconnecting +def client_left(client, server): + print("WS Client(%d) disconnected" % client['id']) + + +# Called when a WS client sends a message +def message_received(client, server, message): + if len(message) > 200: + message = message[:200]+'..' + if debug >0: + print("WS Client(%d) said: %s" % (client['id'], message)) + oscpath = message.split(" ") + + # current UI has no dedicated off button so /on 0 trigs /off to bhorosc + if oscpath[0] == "/on": + if oscpath[1] == "1": + sendbhorosc("/on") + else: + sendbhorosc("/off") + else: + print "sending to bhorosc",oscpath[0],oscpath[1] + sendbhorosc(oscpath[0],oscpath[1]) + + # if needed a loop back : WS Client -> server -> WS Client + #sendWSall("ws"+message) + + +def handle_timeout(self): + self.timed_out = True + + +def sendWSall(message): + if debug >0: + print("WS sending %s" % (message)) + server.send_message_to_all(message) + + + +# Some random lists for all lasers at launch. +print "" +print "Creating startup point lists..." + +random_points = [(300.0+random.randint(-100, 100), 200.0+random.randint(-100, 100), 0), (500.0+random.randint(-100, 100), 200.0+random.randint(-100, 100), 65280), (500.0+random.randint(-100, 100), 400.0+random.randint(-100, 100), 65280), (300.0+random.randint(-100, 100), 400.0+random.randint(-100, 100), 65280), (300.0+random.randint(-100, 100), 200.0+random.randint(-100, 100), 65280)] +if r.set('/pl/0', str(random_points)) == True: + print "/pl/0 ", ast.literal_eval(r.get('/pl/0')) + +random_points = [(300.0+random.randint(-100, 100), 200.0+random.randint(-100, 100), 0), (500.0+random.randint(-100, 100), 200.0+random.randint(-100, 100), 65280), (500.0+random.randint(-100, 100), 400.0+random.randint(-100, 100), 65280), (300.0+random.randint(-100, 100), 400.0+random.randint(-100, 100), 65280), (300.0+random.randint(-100, 100), 200.0+random.randint(-100, 100), 65280)] +if r.set('/pl/1', str(random_points)) == True: + print "/pl/1 ", ast.literal_eval(r.get('/pl/1')) + +random_points = [(300.0+random.randint(-100, 100), 200.0+random.randint(-100, 100), 0), (500.0+random.randint(-100, 100), 200.0+random.randint(-100, 100), 65280), (500.0+random.randint(-100, 100), 400.0+random.randint(-100, 100), 65280), (300.0+random.randint(-100, 100), 400.0+random.randint(-100, 100), 65280), (300.0+random.randint(-100, 100), 200.0+random.randint(-100, 100), 65280)] +if r.set('/pl/2', str(random_points)) == True: + print "/pl/2 ", ast.literal_eval(r.get('/pl/2')) + +random_points = [(300.0+random.randint(-100, 100), 200.0+random.randint(-100, 100), 0), (500.0+random.randint(-100, 100), 200.0+random.randint(-100, 100), 65280), (500.0+random.randint(-100, 100), 400.0+random.randint(-100, 100), 65280), (300.0+random.randint(-100, 100), 400.0+random.randint(-100, 100), 65280), (300.0+random.randint(-100, 100), 200.0+random.randint(-100, 100), 65280)] +if r.set('/pl/3', str(random_points)) == True: + print "/pl/3 ", ast.literal_eval(r.get('/pl/3')) + + +# Order all lasers to show these random shapes at startup -> tell all 4 laser process to USER PLs +r.set('/order', "0") + + +# Launch one process (a newdacp instance) by etherdream + +print "" +dac_worker0= Process(target=dac_process,args=(0,0)) +print "Launching Laser 0 Process..." +dac_worker0.start() + +if lasernumber >0: + dac_worker1= Process(target=dac_process,args=(1,0)) + print "Launching Laser 1 Process..." + dac_worker1.start() + +if lasernumber >1: + dac_worker2= Process(target=dac_process,args=(2,0)) + print "Launching Laser 2 Process..." + dac_worker2.start() + +if lasernumber >2: + dac_worker3= Process(target=dac_process,args=(3,0)) + print "Launching Laser 3 Process..." + dac_worker3.start() + +# Main loop do nothing. Maybe do the webui server ? +try: + #while True: + + # Websocket startup + + server = WebsocketServer(wsPORT,host=serverIP) + # Launch OSC thread listening to Bhorosc + print "" + print "Launching webUI OSC Handler..." + thread.start_new_thread(osc_thread, ()) + # Default OSC handler for all incoming message from Bhorosc + oscserver.addMsgHandler("default", las.handler) + + #print server + print "" + print "Launching webUI Websocket server..." + print "at :", serverIP, "port :",wsPORT + server.set_fn_new_client(new_client) + server.set_fn_client_left(client_left) + server.set_fn_message_received(message_received) + server.run_forever() + print "" + print "Running..." + +except KeyboardInterrupt: + pass + +# Gently stop on CTRL C + +finally: + + dac_worker0.join() + if lasernumber >0: + dac_worker1.join() + if lasernumber >1: + dac_worker2.join() + if lasernumber >2: + dac_worker3.join() + + + for laserid in range(0,lasernumber+1): + print "Redis Etherdream",laserid,"feedback reset." + r.set('/lack/'+str(laserid),64) + r.set('/lstt/'+str(laserid),64) + r.set('/cap/'+str(laserid),0) + +print "Fin des haricots" + + + + + diff --git a/newdacp.py b/newdacp.py new file mode 100644 index 0000000..676ccbc --- /dev/null +++ b/newdacp.py @@ -0,0 +1,444 @@ +#!/usr/bin/python2.7 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + +''' +LJay v0.8.0 + +newdacp.py +Unhanced version (redis and process style) of the etherdream python library from j4cDAC. + +LICENCE : CC +Sam Neurohack, pclf + +Conversion in etherdream coordinates, geometric corrections,... +Init call with a laser number and which point list to draw. Etherdream IP is found in conf file for given laser number + +Uses redis keys value for live inputs/outputs +These redis keys are read and set at each main loop. + +Live inputs : +/pl/lasernumber [(x,y,color),(x1,y1,color),...] A string of list of pygame points list. +/resampler/lasernumber [(1.0,8), (0.25,3),(0.75,3),(1.0,10)] : a string for resampling rules. + the first tuple (1.0,8) is for short line < 4000 in etherdream space + (0.25,3),(0.75,3),(1.0,10) for long line > 4000 + i.e (0.25,3) means go at 25% position on the line, send 3 times this position to etherdream + +Live ouputs : +/lstt/lasernumber value etherdream last_status.playback_state (0: idle 1: prepare 2: playing) +/cap/lasernumber number of empty points sent to fill etherdream buffer (up to 1799) +/lack/lasernumber value "a": ACK "F": Full "I": invalid. 64 or 35 for no connection. +Geometric corrections : + + + + +''' + +import socket +import time +import struct +from gstt import debug, PL +import gstt +import math +from itertools import cycle +#from globalVars import * +import pdb +import ast +import redis + +import homographyp +import numpy as np + +black_points = [(278.0,225.0,0),(562.0,279.0,0),(401.0,375.0,0),(296.0,454.0,0),(298.0,165.0,0)] +grid_points = [(300.0,200.0,0),(500.0,00.0,65280),(500.0,400.0,65280),(300.0,400.0,65280),(300.0,200.0,65280),(200.0,100.0,0),(600.0,100.0,65280),(600.0,500.0,65280),(200.0,500.0,65280),(200.0,100.0,65280)] + +r = redis.StrictRedis(host=gstt.LjayServerIP, port=6379, db=0) + + +def pack_point(x, y, r, g, b, i = -1, u1 = 0, u2 = 0, flags = 0): + """Pack some color values into a struct dac_point. + + Values must be specified for x, y, r, g, and b. If a value is not + passed in for the other fields, i will default to max(r, g, b); the + rest default to zero. + """ + + if i < 0: + i = max(r, g, b) + + return struct.pack("> 16) & 0xFF) << 8, ((c >> 8) & 0xFF) << 8, (c & 0xFF) << 8 + #print '' + return (position[0][0], position[0][1], ((c >> 16) & 0xFF) << 8, ((c >> 8) & 0xFF) << 8, (c & 0xFF) << 8) + + + def read(self, l): + """Read exactly length bytes from the connection.""" + while l > len(self.buf): + self.buf += self.conn.recv(4096) + + obuf = self.buf + self.buf = obuf[l:] + return obuf[:l] + + def readresp(self, cmd): + """Read a response from the DAC.""" + data = self.read(22) + response = data[0] + #print "laser response", self.mylaser, response + gstt.lstt_dacanswers[self.mylaser] = response + cmdR = data[1] + status = Status(data[2:]) + r.set('/lack/'+str(self.mylaser), response) + + if cmdR != cmd: + raise ProtocolError("expected resp for %r, got %r" + % (cmd, cmdR)) + + if response != "a": + raise ProtocolError("expected ACK, got %r" + % (response, )) + + self.last_status = status + return status + + def __init__(self, mylaser, PL, port = 7765): + """Connect to the DAC over TCP.""" + socket.setdefaulttimeout(2) + + #print "init" + self.mylaser = mylaser + #print "DAC", self.mylaser, "Handler process, connecting to", gstt.lasersIPS[mylaser] + self.conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.connstatus = self.conn.connect_ex((gstt.lasersIPS[mylaser], port)) + #print "Connection status for", self.mylaser,":", self.connstatus + #print 'debug', debug, gstt.debug + # ipconn state is -1 at startup (see gstt) and modified here + r.set('/lack/'+str(self.mylaser), self.connstatus) + gstt.lstt_ipconn[self.mylaser] = self.connstatus + + self.buf = "" + # Upper case PL is the Point List number + self.PL = PL + + # Lower case pl is the actual point list coordinates + self.pl = ast.literal_eval(r.get('/pl/'+str(self.mylaser))) + #if self.mylaser ==0: + print "DAC Init Laser", self.mylaser + #print "pl :", self.pl + #print "EDH/"+str(self.mylaser),r.get('/EDH/'+str(self.mylaser)) + if r.get('/EDH/'+str(self.mylaser)) == None: + print "Laser",self.mylaser,"NO EDH !! Computing one..." + homographyp.newEDH(self.mylaser) + else: + gstt.EDH[self.mylaser] = np.array(ast.literal_eval(r.get('/EDH/'+str(self.mylaser)))) + print "Laser",self.mylaser,"found its EDH in redis" + #print gstt.EDH[self.mylaser] + + ''' + d =homographyp.apply(gstt.EDH[self.mylaser],np.array([(300,400)])) + print '' + print "d",d + print "d0",d[0] + #print "d1",len(d[1]) + print " " + ''' + + self.xyrgb = self.xyrgb_prev = (0,0,0,0,0) + self.newstream = self.OnePoint() + + print "Connection status for", self.mylaser,":", self.connstatus + #print 'debug', debug + if self.connstatus != 0: + print "" + print "Connection ERROR",self.connstatus,"with laser", str(mylaser),":",str(gstt.lasersIPS[mylaser]) + #print "first 10 points in PL",self.PL, self.GetPoints(10) + + # Reference points + # Read the "hello" message + first_status = self.readresp("?") + first_status.dump() + position = [] + + + def begin(self, lwm, rate): + cmd = struct.pack("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/webui/index.html b/webui/index.html new file mode 100644 index 0000000..f2ca6be --- /dev/null +++ b/webui/index.html @@ -0,0 +1,800 @@ + + + + + + LJay + + + + + + + + + + + + + + + + +
+ + +
+ + +
+
+ + +
+
/on
+
+
+ + +
+ +
+
+
S
+
C
+ +
0
+
+
+
+ +
1
+
+
+
+ +
2
+
+
+
+ + +
3
+
+
+
+
+ +
+
Laser
+
+
+
Set
+
+
Curve
+
+
Simu
+
+
+
+
+
+ + +
+
+ + + + + +
+
+ +
+
+ + +
+ + +
+ +
+
+ +
+
+
+ + + + + + + + +
+ +
+ +
+
+
kPPS
+
Points
+
+
+
+
+
+
+
Offset X
+
Offset Y
+
+
+
+
+
+
+
Scale X
+
Scale Y
+
+
+
+
+
Angle
+
Intens.
+
+
+ + +
+ +
+
+ +
+
+
+ + + + + + + + +
+ +
+
+
+
kPPS
+
Points
+
+
+
+
+
+
+
Offset X
+
Offset Y
+
+
+
+
+
+
+
Scale X
+
Scale Y
+
+
+
+
+
Angle
+
Intens.
+
+
+ + +
+ +
+
+ +
+
+
+ + + + + + + + +
+ +
+
+
+
kPPS
+
Points
+
+
+
+
+
+
+
Offset X
+
Offset Y
+
+
+
+
+
+
+
Scale X
+
Scale Y
+
+
+
+
+
Angle
+
Intens.
+
+
+ + +
+ +
+
+ +
+
+
+ + + + + + + + +
+ +
+
+
+
kPPS
+
Points
+
+
+
+
+
+
+
Offset X
+
Offset Y
+
+
+
+
+
+
+
Scale X
+
Scale Y
+
+
+
+
+
Angle
+
Intens.
+
+
+
+ + +
+ + + +
+
With AI +
+
+
+
+
+
+
Velocity
+
Express.
+
+
+
+
+
+
+
Sens.
+
Beauty
+
+
+
+
+
+
+
CC 1
+
CC 2
+
+
+ + + +
+
LISSA +
+
+ +
+
+
+
+
Select X
+
Select Y
+
+
+
+
+
+
+
FOV
+
Dist
+
+
+
+
+
+
+
Offset X
+
Offset Y
+
+
+ + +
+
+
3D ROT
+
+
+
+
+
+
+
+
+
+
+
X
+
Y
+
Z
+
+ + +
+ + +
+
+ +
+
+
+ + +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + diff --git a/webui/knobs/Prophetic10.png b/webui/knobs/Prophetic10.png new file mode 100644 index 0000000..0d3c897 Binary files /dev/null and b/webui/knobs/Prophetic10.png differ diff --git a/webui/knobs/Prophetic5.png b/webui/knobs/Prophetic5.png new file mode 100644 index 0000000..900d880 Binary files /dev/null and b/webui/knobs/Prophetic5.png differ diff --git a/webui/knobs/bigbluetoggle.png b/webui/knobs/bigbluetoggle.png new file mode 100644 index 0000000..e9131a6 Binary files /dev/null and b/webui/knobs/bigbluetoggle.png differ diff --git a/webui/knobs/blackout.png b/webui/knobs/blackout.png new file mode 100644 index 0000000..9c66b38 Binary files /dev/null and b/webui/knobs/blackout.png differ diff --git a/webui/knobs/grid.png b/webui/knobs/grid.png new file mode 100644 index 0000000..da415a2 Binary files /dev/null and b/webui/knobs/grid.png differ diff --git a/webui/knobs/lasergrid0.png b/webui/knobs/lasergrid0.png new file mode 100644 index 0000000..a393790 Binary files /dev/null and b/webui/knobs/lasergrid0.png differ diff --git a/webui/knobs/lasergrid1.png b/webui/knobs/lasergrid1.png new file mode 100644 index 0000000..288d464 Binary files /dev/null and b/webui/knobs/lasergrid1.png differ diff --git a/webui/knobs/lasergrid2.png b/webui/knobs/lasergrid2.png new file mode 100644 index 0000000..dea8ab1 Binary files /dev/null and b/webui/knobs/lasergrid2.png differ diff --git a/webui/knobs/lasergrid3.png b/webui/knobs/lasergrid3.png new file mode 100644 index 0000000..12ada9c Binary files /dev/null and b/webui/knobs/lasergrid3.png differ diff --git a/webui/knobs/leds.png b/webui/knobs/leds.png new file mode 100644 index 0000000..6bc3530 Binary files /dev/null and b/webui/knobs/leds.png differ diff --git a/webui/knobs/ljaylogo.png b/webui/knobs/ljaylogo.png new file mode 100644 index 0000000..233cb62 Binary files /dev/null and b/webui/knobs/ljaylogo.png differ diff --git a/webui/knobs/mouse.png b/webui/knobs/mouse.png new file mode 100644 index 0000000..5000744 Binary files /dev/null and b/webui/knobs/mouse.png differ diff --git a/webui/knobs/simplegray.png b/webui/knobs/simplegray.png new file mode 100755 index 0000000..9c57010 Binary files /dev/null and b/webui/knobs/simplegray.png differ diff --git a/webui/knobs/swapx.png b/webui/knobs/swapx.png new file mode 100644 index 0000000..970c998 Binary files /dev/null and b/webui/knobs/swapx.png differ diff --git a/webui/knobs/swapy.png b/webui/knobs/swapy.png new file mode 100644 index 0000000..71620cb Binary files /dev/null and b/webui/knobs/swapy.png differ diff --git a/webui/knobs/switch_toggle.png b/webui/knobs/switch_toggle.png new file mode 100755 index 0000000..c8c5c65 Binary files /dev/null and b/webui/knobs/switch_toggle.png differ diff --git a/webui/webaudio-controls.js b/webui/webaudio-controls.js new file mode 100755 index 0000000..d9fdb43 --- /dev/null +++ b/webui/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/webui/webcomponents-lite.js b/webui/webcomponents-lite.js new file mode 100755 index 0000000..d3e7beb --- /dev/null +++ b/webui/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