UNPKG

smartdown-gallery

Version:

Example Smartdown documents and associated resources that demonstrate various Smartdown features and serve as raw material for other Smartdown demos.

268 lines (194 loc) 9.2 kB
### Brython in Smartdown Smartdown is designed as a way to explain and share prose, media and active programming fragments called *playables*. Javascript-based playables have been well-supported and integrated, including the `javascript` playable and its more specialized `p5js` playable. Smartdown also supports other languages that can run in the browser, either via transpilation to Javascript (as in the `GopherJS` integration) or via compilation to WebAssembly (e.g., `GraphViz`). Eventually, Smartdown will support the optional use of *Remotely Executing* playables, which is the Jupyter notebook model, and the use of *Remotely Dependent* variables, but that's a subject for a different document. I recently explored the possibility of using [Brython](https://www.brython.info) as a way to run Python code as a playable within the browser, while adhering to Smartdown's serverless principles. Brython is very cool, well-documented, and it was pretty easy (a weekend) to build the following prototype. #### Hello World This simple example effectively uses the browser's `window.alert()` function, but does so via Brython and the [browser](https://www.brython.info/static_doc/en/browser.html) module. ```brython/playable/debug """A very simple Python3 program""" import browser browser.alert("Hello World") ``` #### Smartdown Reactivity Let's see if we can make a Brython playable *observe* and *react* to a Smartdown variable. First, play the following playable, which will *wait* until it's dependent variable `NAME` is changed. ```brython/playable """React to changes in NAME by adjusting the DOM""" import browser sd = __BRYTHON__.smartdown def nameChanged(): sd.this.div.innerHTML = "<h4>Hello, %s</h4>" % sd.env.NAME sd.smartdown.setVariable('NAME', '') sd.this.dependOn = ['NAME'] sd.this.depend = nameChanged ``` In the Smartdown cell below, enter your name (or whatever), which will trigger the dependent function, which will change the content above to the new value of the Smartdown variable `NAME`. [What is your name?](:?NAME) #### Prettier Reactivity with SVG Adapting the examples in [browser.svg](https://www.brython.info/static_doc/en/svg.html) by dynamically creating an SVG wrapper, we can get a more interesting reactivity. In this case, we'll draw the latest version of `NAME` and a star diagram below it. ```brython/playable from browser import document, svg sd = __BRYTHON__.smartdown def nameChanged(): title = svg.text(sd.env.NAME, x=70, y=25, font_size=22, text_anchor="middle") star = svg.polygon(fill="red", stroke="blue", stroke_width="10", points=""" 75,38 90,80 135,80 98,107 111,150 75,125 38,150 51,107 15,80 60,80""") sdDiv = sd.div gId = sdDiv.id + "_g" svgWrapper = """\ <svg xmlns="https://www.w3.org/2000/svg" xmlns:xlink="https://www.w3.org/1999/xlink" width="200" height="200" style="border-style:solid;border-width:1;border-color:#000;"> <g id="%s"></g> </svg>""" % gId sdDiv.innerHTML = svgWrapper gElement = document[gId] gElement <= title gElement <= star sd.this.dependOn = ['NAME'] sd.this.depend = nameChanged ``` #### Analog Clock The following example is based upon the Brython example [Analog Clock](https://brython.info/gallery/clock.html). The original example specified the `<canvas>` tag in HTML; I've adapted it to use the Smartdown per-playable `<div>` as a parent, and added Brython code to create the `<canvas>` dynamically. ```brython/playable """Code for the clock""" import time import math import datetime import sys from browser import document as doc from browser import window as win import browser.timer sin, cos = math.sin, math.cos width, height = 250, 250 # canvas dimensions ray = 100 # clock ray background = "#111" digits = "#fff" border = "#333" timer = None def needle(angle, r1, r2, color="#000000"): """Draw a needle at specified angle in specified color. r1 and r2 are percentages of clock ray. """ x1 = width / 2 - ray * cos(angle) * r1 y1 = height / 2 - ray * sin(angle) * r1 x2 = width / 2 + ray * cos(angle) * r2 y2 = height / 2 + ray * sin(angle) * r2 ctx.beginPath() ctx.strokeStyle = "#fff" ctx.moveTo(x1, y1) ctx.lineTo(x2, y2) ctx.stroke() def set_clock(): # erase clock ctx.beginPath() ctx.fillStyle = background ctx.arc(width / 2, height / 2, ray * 0.89, 0, 2 * math.pi) ctx.fill() # redraw hours show_hours() # print day now = datetime.datetime.now() day = now.day ctx.font = "bold 14px Arial" ctx.textAlign = "center" ctx.textBaseline = "middle" ctx.fillStyle="#000" ctx.fillText(day, width * 0.7, height * 0.5) # draw needles for hour, minute, seconds ctx.lineWidth = 2 hour = now.hour % 12 + now.minute / 60 angle = hour * 2 * math.pi / 12 - math.pi / 2 needle(angle, 0.05, 0.5) minute = now.minute angle = minute * 2 *math.pi / 60 - math.pi / 2 needle(angle, 0.05, 0.85) ctx.lineWidth = 1 second = now.second + now.microsecond / 1000000 angle = second * 2 * math.pi / 60 - math.pi / 2 needle(angle, 0.05, 0.85, "#FF0000") # in red def show_hours(): ctx.beginPath() ctx.arc(width / 2, height / 2, ray * 0.05, 0, 2 * math.pi) ctx.fillStyle = digits ctx.fill() for i in range(1, 13): angle = i * math.pi / 6 - math.pi / 2 x3 = width / 2 + ray * cos(angle) * 0.75 y3 = height / 2 + ray * sin(angle) * 0.75 ctx.font = "18px Arial" ctx.textAlign = "center" ctx.textBaseline = "middle" ctx.fillText(i, x3, y3) # cell for day ctx.fillStyle = "#fff" ctx.fillRect(width * 0.65, height * 0.47, width * 0.1, height * 0.06) sd = __BRYTHON__.smartdown canvasId = sd.divId + "_canvas" canvas = doc.createElement("canvas") canvas.attrs['id'] = canvasId; canvas.attrs['width'] = '250'; canvas.attrs['height'] = '250'; canvas.attrs['id'] = canvasId; sd.div.appendChild(canvas); # draw clock border if hasattr(canvas, 'getContext'): ctx = canvas.getContext("2d") ctx.beginPath() ctx.arc(width / 2, height / 2, ray, 0, 2 * math.pi) ctx.fillStyle = background ctx.fill() ctx.beginPath() ctx.lineWidth = 6 ctx.arc(width / 2,height / 2, ray + 3, 0, 2 * math.pi) ctx.strokeStyle = border ctx.stroke() for i in range(60): ctx.lineWidth = 1 if i%5 == 0: ctx.lineWidth = 3 angle = i * 2 * math.pi / 60 - math.pi / 3 x1 = width / 2 + ray * cos(angle) y1 = height / 2 + ray * sin(angle) x2 = width / 2 + ray * cos(angle) * 0.9 y2 = height / 2 + ray * sin(angle) * 0.9 ctx.beginPath() ctx.strokeStyle = digits ctx.moveTo(x1, y1) ctx.lineTo(x2, y2) ctx.stroke() timer = browser.timer.set_interval(set_clock, 100) show_hours() else: doc['navig_zone'].html = "On Internet Explorer 9 or more, use a Standard rendering engine" def atExit(): print("atExit in Brython") browser.timer.clear_timeout(timer) sd.this.atExit(atExit) ``` #### How it works Brython's default API is the `brython()` function which obtains Python3 source code from a `<script type="text/python3">` tag. Smartdown detects `brython` playables and generates a corresponding `<script ...>` tag. When the playable is *played*, the `Brython` compiler is invoked upon the target Python3 script and the translated Python is `eval`-ed in the context of a Smartdown-generated wrapper script that ensures that Smartdown's context is passed. #### Inter-language Communication Because Smartdown's variables are available to all playables, whether Javascript or Brython, we can *compose* a document using multiple languages, depending on which language is appropriate and best expresses a concept. For example, we can write a Javascript playable that reacts to the same variable `NAME` as above. In this simple example, we'll just update the playable's DOM similar to how we did this in Brython above. For convenience, we have another input field below which will *reflect* and *affect* the value of the `NAME` variable. [What is your Name (again)?](:?NAME) ```javascript/playable const myDiv = this.div; this.dependOn = ['NAME'] this.depend = function() { const name = env.NAME; myDiv.innerHTML = `<h4>Hello, ${name}</h4>` }; if (!env.NAME) { smartdown.setVariable('NAME', ''); } ``` #### What's next The above examples work pretty well, after a lot of trial and error. Some of the next steps: - The *augmented code* is unnecessarily large and contains dead code left over from prior experiments. - If there is a syntax error in the Python code, it is difficult for an ordinary user to debug. The Brython stack trace needs to be interpreted in an author-friendly way. This problem is shared with other Smartdown playables. - I'm still unclear on the variable scoping and what effects one `brython` playable might have on another. This needs to be explored and explained. --- [Back to Home](:@Home)