The Pyjsdl library in the Pyjsdl-ts package is a port of Pyjsdl to work with Transcrypt. Pyjsdl permit scripts coded in Python/Pygame to compile to JavaScript for online deployment. Pyjsdl is modelled on the Pygame commands that wraps JavaScript API providing the multimedia functionality. The post describes the process to convert the Draw Pad script to an app for online deployment.
The Draw Pad app was compiled to JavaScript with Transcrypt, the Python code of the script is in the textbox. The textbox was also coded in Python and converted to JavaScript with Transcrypt and uses the Prism JavaScript library syntax highlighter.
For development the following tools are required. Python 3 is required, while Pygame is optional for running on the desktop. The Pyjsdl-ts package must be downloaded and the pyjsdl folder placed in the script’s folder or on its path. Pyjsdl is coded in Python and will be converted to JavaScript during the compile process. The final requirement is Transcrypt installation. Transcrypt is a Python-to-JavaScript compiler/transpiler and can be installed with the command:
pip install transcrypt
It may be preferable to install in a virtual environment:
python3 -m venv env
source env/bin/activate
pip install --upgrade pip setuptools wheel
pip install transcrypt
When developing with Transcrypt in the virtual environment, use the following command in the folder the environment was set to initiate the environment:
source env/bin/activate
The script can be developed under Python/Pygame. Once the script is working properly, some code changes are necessary to function with Pyjsdl. Pyjsdl has a substantial portion of Pygame functionality using a similar API. If you prefer to continue with desktop execution, the code can be tested with PyJ2D that is the original library from which Pyjsdl was ported and has similar functionality. The Drawing Pad was initially developed with Python/Pygame and checked with Jython/PyJ2D.
Code changes are necessary due to functionality differences in either Pyjsdl, Transcrypt, or the web browser. Much of this content applies to conventional JavaScript applications, which is the goal even though we program with Python and Pyjsdl. To expose Pyjsdl API to the script, the library is imported:
import pyjsdl
The design intention of Pyjsdl was the replacement of Pygame functionality in the web browser so to permit Python/Pygame programs to be deployed online. Such an import would not permit the program to be developed and run under Pygame. Due to similarity of the API of Pygame and Pyjsdl, to allow the program to interoperate between the two environments the usual mechanism is to rebind the imports with a common name, thereby maintain functionality of the API statements. With this goal, the import statement that rebinds Pygame, for instance ‘pg’ that is usual for Pygame programs would be:
import pygame as pg
should be changed to import Pyjsdl to replace Pygame by rebinding to the common name:
import pyjsdl as pg
The Draw Pad script is coded to permit it to execute either on the desktop or in the web browser. To do so, it uses conditional import statements that will import the correct library under various environments. In this manner it not only opens up the possibility of online deployment but maintains its ability to be developed and executed on the desktop.
The other crucial change is the script’s main loop. Since the JavaScript environment of web browsers are single threaded, any loop of long duration will freeze the app. Typically, JavaScript programs use callback functions for iteration. Since Python code does not wait at a particular statement, code changes are necessary. The crucial concern is the main program loop that iterates the execution of the app frames, for instance:
while True:
run()
needs to be replaced with the Pyjsdl equivalence, using its ‘pyjsdl.setup’ function:
pyjsdl.setup(run)
The ‘pyjsdl.setup’ statement provides Pyjsdl with a callback, in this case the ‘run’ function, to execute repeatedly after each frame is displayed. The display is updated repeatedly by the web browser through a display refresh callback function, typically 60 fps dependant on processing load. The callback function provided by the ‘pyjsdl.setup’ statement hooks into this iteration. The ‘pyjsdl.setup’ function is unique to Pyjsdl API not present in Pygame, therefore to permit the program to operate under the different environment it requires to be under conditional execution. With Draw Pad, the conditional import of Pyjsdl or Pygame is registered in a variable ‘platform’, which can be used for conditional execution. By examining Draw Pad code, with conditional import and rebinding to ‘pg’, there is a conditional execution dependent on platform either the main ‘while’ loop or ‘pg.setup’. Registering a variable such as ‘platform’ is advisable if there are any other statements that requires conditional execution due to differences in the operating environment.
Another concern is image load at which the Python script expects the loading process to occur immediately before processing the next statement, while the web browser requires to retrieve the image file from a server and cannot wait as this would lock the app execution. To overcome this obstacle, the image download is performed asynchronously using an internal callback. This is achieved used a preloading of the images prior to the main callback being executed, using the statement:
pyjsdl.setup(run, ['./data/brush.png']
The statement’s second argument is a list of images to preload. With this accomplished, subsequent ‘image.load’ methods in the script will retrieve the image resident in memory. Pyjsdl requires the display to be initiated prior to preloading images, which probably should be revised. Therefore call ‘display.set_mode’ method prior to running the ‘pyjsdl.setup’ requiring image preloading. To pass variables to callback functions use global variables, for instance, the ‘display’ variable returned from ‘display.set_mode’ should be set as ‘global display’ in the function and there should be ‘display=None’ in the global scope.
Another option for the ‘pyjsdl.setup’ statement is that it can be chained and passed new callback functions. It may be appropriate to use the ‘pyjsdl.set_callback’ if just changing callback. Passing new callbacks will change the callback hooked into the web browser iteration. For instance:
def run():
prg.update()
def pre_run():
pyjsdl.setup(run)
pyjsdl.setup(pre_run)
An alternative option for ‘pyjsdl.setup’ to avoid use of global variables is to pass an object with a ‘run’ method as done with Draw Pad, the ‘run’ method will be used as the callback. For instance:
class App:
def __init__(self):
pass
def run(self):
prg.update()
app = App()
pyjsdl.setup(app)
This covers the crucial code changes. There may be a few other code changes necessary due to the nature of the web browser. For instance time.wait will not function as it would also lock the app execution, use a variable to check the pause state, change the main callback with ‘pyjsdl.setup’, or use time.set_timeout with a callback function for the web browser to schedule.
The final concern is to compile Python to JavaScript. Transcrypt compiles Python 3 syntax. It compiles Python code to clean, concise, and efficient JavaScript code. At this stage of development the web browser is used for testing. The web browser’s development console will provide messages, Transcrypt outputs print statements and exception errors to the console. To ensure code changes are reflected in the JavaScript code, development console settings have an option to disable cache when open.
Transcrypt translates with a close coupling of Python objects to equivalent JavaScript objects, for instance internally a Python list is translated to a JavaScript array with a thin layer to provide Python functionality. For efficiency, some of the JavaScript restrictions are apparent. Browse the Transcrypt documentation for detail information. For instance, you cannot iterate through a Python dict as:
for key in adict:
pass
This would requiring continuous checking and calling Python special methods at the expense of efficiency. Rather Transcrypt requires the code change to an explicit statement:
for key in adict.key():
pass
Another such difference are Python object truth value statements. For instance Python return False for an empty lists:
alist = []
if alist:
pass
For efficiency, Transcrypt requires an explicit statement in such cases:
alist = []
if len(alist) > 0:
pass
Transcrypt provides mechanisms to have these functionality at the expense of performance and size of the generated JavaScript code. Transcrypt compile program has command-line options to offer these functionality, but these compilation switches have impact on the performance of the whole program. For instance -i will permit implicit dictionary iteration. These command-line options are discouraged due to impact on performance. A preferable route is to use Transcrypt’s pragma mechanism to locally provide the functionality required:
# __pragma__ ('iconv')
for key in adict:
pass
# __pragma__ ('noiconv')
For development you can use a number of initial pragma statements at the start of the script and then eliminate with alternative code statements or restrict pragma to a limited number of statements that will not impact the performance of the entire program.
Draw Pad uses limited pragma statements, for instance rather than using a pragma the change to adict.keys in iteration statements is sufficient. Pragma statements have various forms, refer to Transcrypt documentation. In my opinion the preferable form is ‘# __pragma__’ that permits pragma statements that are recognized by Transcrypt but does not affect the script execution with Python as it is in syntax a comment statement. Draw Pad script uses a single pragma statement:
# __pragma__ ('skip')
<statements>
# __pragma__ ('noskip')
This pragma statement instructs Transcrypt to skip the intervening code. Referring to Draw Pad code, the import statements provide imports for the various environments: desktop, JVM, or JavaScript. Transcrypt process the import mechanism at compile time and the missing libraries will lead to compilation errors. The import of pyjsdl in the intervening skip pragma statements is for the former Pyjsdl library. Using this pragma statement, these import statements are ignored, and the variable ‘platform’ remains ‘None’, at which point Transcrypt will process the import of pyjsdl from the Pyjsdl-ts package.
This covers much of the intricacies with using Pyjsdl. The remaining step is Transcrypt compilation of the Python script to JavaScript:
transcrypt -n draw_pad.py
The -n option is to avoid JavaScript minification, which requires JVM installed and only necessary when online deployment in a production setting. If the compile step passes, the JavaScript code can be executed. By default, Transcrypt compiles the Python script to JavaScript in the __target__ folder. The folder contains the JavaScript files for the script along with the imports, including the pyjsdl library that is predominately coded in Python and therefore is compiled, and Transcrypt core. The __target__ folder along with media file folder is all that is required to deploy online, besides a main HTML file to retrieve the JavaScript app, for instance Drawing Pad has draw_pad.html in the root folder of the script:
<!doctype html>
<html>
<body style="background-color:black">
<div id="__panel__"></div>
<script type='module' src="__target__/draw_pad.js"></script>
</body>
</html>
The important statements are the ‘<script>’ with ‘src’ set to the location of the main app JavaScript file, in this case to ‘draw_pad.js’, and the ‘<div id=”__panel__”></div>’ in which Pyjsdl inserts the canvas display.
At this point the JavaScript app can run in the web browser. To run, it must be launched by the web browser from a web server. You can launch the app from a local server:
python3 -m http.server
From the web browser, browser to localhost:8000 or the local IP, then launch the app form the main HTML file. Use the local server for development, if changes are required, edit the Python script and rerun the transcrypt command to reflect changes in the JavaScript code.
After development, the app can be uploaded to a web server for online deployment. To embed the app into a web page, the following HTML statement is added at the desired placement, for instance with Draw Pad:
<div id="draw-pad" style="position:relative; left:50px;">
<iframe src="https://<server_path>/draw_pad.html"
marginwidth="0" marginheight="0" hspace="0" vspace="0"
scrolling="no" width="500" height="400" frameborder="0">
</iframe>
</div>
The app is now deployed online.