Jump to content

Lua Screen Units API and Instructions (Updated: 19/07/2021)


NQ-Deckard

Recommended Posts

Introduction

Updated: 19/07/2021

 

Lua screen units are a new technology aimed at solving the performance issues associated with our current implementation of HTML on screens, while bringing a new feature allowing complex animations with thousands of graphical elements.

 

Alongside of setting HTML on screen units, it is now possible to set a render script on screen units instead.

Render scripts are Lua scripts residing inside the screen units themselves that provide rendering instructions for screen units. 


Each screen also has a maximum render cost to limit the impact a single screen can have on users that are able to see it, this is important as the impact of a screen is not limited to only the creator of the screen's render script or content but also other nearby players.

 

Inside the render script the content is built up using layers which are rendered on top of each other in order of creation at the end of the render script.

 

After creating a layer, it is then possible to optionally specify a few parameters for the next shape you intend to add to that layer. Such as stroke color, fill color, stroke width and rotation depending on what's available for that shape. Once those parameters are defined we then use "add" functions that draw shapes on those layers using those parameters.
 

If the screen is intended to be a static display there is no further requirement, if you wish for the screen to be animated we can then add:

requestAnimationFrame(<frames>)

Using this at the end of the script which will aim to refresh the screen after <frames> amount of frames.
This should only be used when required for animated content. For performance reasons we recommend not calling this function if no animations are intended to be run.

 

In future we will further expand on this feature to provide more functionality and synergy with control units allowing for far more efficient and responsive screens in the game.

 

Getting Started

To start creating renderscripts we start by right clicking the screen and using the context menu option Edit Content (Ctrl+L) to start editing the screen.

image.png

 

This will bring up a new Lua & HTML editor window which will allow you to switch between Lua (Render Script) and HTML display modes.

image.png

 

In Lua mode it will provide error reporting for the render script in the lower portion of the screen, and to aid in debugging processes there is a check box which will allow you to write to the Lua Channel using logMessage("message") inside the renderscript. This is logging method is local only to you if you have the check box enabled and will be automatically be disabled when the screen is unloaded.

 

Drawing screen contents with render scripts consists of creating one or more layers, on which shapes will be drawn, and then adding shapes to these layers to populate the screen contents. The API currently supports relatively basic shapes: rectangles, circles, lines, triangles, quadrilaterals along with images and text. In the future, we will consider adding more complex shapes as we receive your feedback. 
 

Take a look at this basic ‘hello world’ render script: 

local layer = createLayer()                        -- Creates a new layer
local rx, ry = getResolution()                     -- Gets the resolution of the screen

local font = loadFont("Play", 20)                  -- Loads the "Play" font at size 20

setNextFillColor(layer, 1, 0, 0, 1)                -- Prepares the fill color (Red,Green,Blue,Alpha) for the next shape to be drawn on the layer
addBox(layer, rx/4, ry/4, rx/2, ry/2)              -- Adds a box shape to layer, starting a 1/4th into the screen, with half the screens width and height
addText(layer, font, "Hello world!", rx/3, ry/2)   -- Adds a text shape without any properties to layer, using the font, one third from the left, and half way down.

 

This script renders a red rectangle that is half the size of the screen, in the center of the screen, and "Hello World!" on top. Simple! 

helloWorld.png

 

image.png

image.png

Link to comment
Share on other sites

Render Script Lua API (PTS v1.1)

  • Shapes

Squares

addBox(layer, x, y, width, height) 

Add a rectangle to the given layer with top-left corner (x, y) and dimensions width x height
Supported properties:
fillColor, rotation, shadowstrokeColor, strokeWidth

 

 Rounded Squares (New!)

addBoxRounded(layer, x, y, width, height, radius) 

Add a rectangle to the given layer with top-left corner (x, y) and dimensions width x height with each corner rounded to radius
Supported properties:
fillColor, rotation, shadowstrokeColor, strokeWidth

 

 Circles

addCircle(layer, x, y, radius) 

Add a circle to the given layer with center (x, y) and radius radius
Supported properties:
fillColor, shadowstrokeColor, strokeWidth

 

 Images

addImage(layer, image, x, y, width, height)

Add image reference to layer as a rectangle with top-left corner (x, y) and dimensions width x height
Supported properties:
fillColor, rotation 

 

 Lines

addLine(layer, x1, y1, x2, y2) 

Add a line to layer from (x1, y1) to (x2, y2). 
Supported properties:
rotation, shadowstrokeColor, strokeWidth

 

 Quadrilaterals

addQuad(layer, x1, y1, x2, y2, x3, y3, x4, y4) 

Add a quadrilateral to the given layer with vertices (x1, y1), (x2, y2), (x3, y3), (x4, y4). 
Supported properties:
fillColor, rotation, shadowstrokeColor, strokeWidth 

 

 Text

addText(layer, font, text, x, y)

Add text to layer using font reference, with top-left baseline starting at (x, y).
Note that each glyph in text counts as one shape toward the total rendered shape limit. 
Supported properties:
fillColor 

 

 Triangles

addTriangle(layer, x1, y1, x2, y2, x3, y3) 

Add a triangle to the given layer with vertices (x1, y1), (x2, y2), (x3, y3). 
Supported properties:
fillColor, rotation, shadowstrokeColor, strokeWidth 

 

  • Layers

Creating a new layer

createLayer() -> int

Create a new layer and return a handle to it that can be used by subsequent calls to the above add shapes. 
Layers are rendered in the order in which they are created by the script, such that all shapes on layer N+1 will appear on top of layer N. 
This results in the first created layer being the in the background and the last created layer will be in the foreground.

Layers now have a fixed render cost and are no longer limited to 8.

 

 Set Screen Background (New!)

setBackgroundColor(r, g, b)

Set the background color of the screen as red (r), green (g), blue (b) in the range [0, 1]

 

  • Screen State Functions

Get Cursor Coordinates

getCursor() -> int, int

Return the screen location that is currently raycasted by the player in screen pixel coordinates as a (x, y) tuple
Returns
(-1, -1) if the current raycasted location is not inside the screen. 

 

Get Cursor Down State (New!)

getCursorDown() -> boolean

Return true if the mouse cursor is currently pressed down on the screen, false otherwise. 

Retains its state if dragged out of the screen.

 

Get Cursor Pressed (New!)

getCursorPressed() -> boolean

Return true if the mouse button changed from being released to being pressed at any point since the last update.

Note that it is possible for both getCursorPressed() and getCursorReleased() to return true in the same script execution, if the mouse button was both pressed and released since the last execution. 

 

Get Cursor Released (New!)

getCursorReleased() -> boolean

Return true if the mouse button changed from being pressed to being released at any point since the last update.

Note that it is possible for both getCursorPressed() and getCursorReleased() to return true in the same script execution, if the mouse button was both pressed and released since the last execution. 

 

Get Delta Time

getDeltaTime() -> float 

Return the time, in seconds, since the screen was last updated. Useful for timing-based animations, since screens are not guaranteed to be updated at any specific time interval, it is more reliable to update animations based on this timer than based on a frame counter.

 

 Get Render Cost

getRenderCost() -> int

Return the current render cost of the script thus far, used to profile the performance of a screen.

This can be used to abort further render instructions when close to the render maximum preventing the screen from shutting down.

 

 Get Maximum Render Cost

getRenderCostMax() -> int

Return the maximum render cost limit. When a script exceeds this render cost in one execution, an error will be thrown and the contents will fail  to render. 

 

 Get Resolution

getResolution() -> int, int

Return the current viewport resolution as a (width, height) tuple.

 

 Log Message to Lua Chat (New!)

logMessage(message)

Write message to the Lua chat if the output checkbox is checked.

 

  • Loading References

Loading Images

loadImage(path) -> int?

Return an image handle that can be used with addImage. If the image is not yet loaded, a sentinel value will be returned that will cause addImage to fail silently, so that the rendered image will not appear until it is loaded. Only images that have gone through image validation are available.

     

    Loading Fonts

    loadFont(name, size) -> int?

    Return a font handle that can be used with addText. The font size is size vertical pixels. If the font is not yet loaded, a sentinel value will be returned that will cause addText to fail silently, so that the rendered text will not appear until the font is loaded.  A maximum of 8 fonts can be loaded for each render.

    Name must be one of the following currently-available fonts:

    FiraMono
    FiraMono-Bold
    Montserrat 
    Montserrat-Light 
    Montserrat-Bold 
    Play 
    Play-Bold 
    RefrigeratorDeluxe 
    RefrigeratorDeluxe-Light 
    RobotoCondensed
    RobotoMono
    RobotoMono-Bold

    New: We added monospaced fonts

     

     Is Image Loaded (New!)

    isImageLoaded(imageHandle) -> bool

    Returns true if the given imageHandle is loaded.

     

     Get Text Bounds (New!)

    getTextBounds(font,text) -> float, float

    Compute and return the bounding box width and height of the given text in the given font as a (width, height) tuple.

     

     Get Font Metrics (New!)

    getFontMetrics(font) -> float, float

    Compute and return the ascender and descender height of given font.

     

    • Animation

     Request refresh in amount of frames

    requestAnimationFrame(frames)

    Notify the screen manager that this screen should be redrawn in frames frames.

    A screen that requires highly-fluid animations should thus call requestAnimationFrame(1) before it returns. 
    Usage of this function has an obvious and significant performance impact on the screen unit system.

    Scripts should try to request updates as infrequently as possible for their application. 
    A screen with unchanging (static) contents should not call this function at all. 
     

    • Properties Defaults (New!)

    Shape Types (New!)

    For shapeType defaults the following constants are valid:

    Shape_Box
    Shape_BoxRounded
    Shape_Circle
    Shape_Image
    Shape_Line
    Shape_Polygon
    Shape_Text

     

     Set Default Fill Color (New!)

    setDefaultFillColor(layer, shapeType, r, g, b, a) 

    Set the default fill color for all shapeType on layer
    Red (
    r), green (g), blue (b), and alpha (a) components are specified, respectively, in the range [0, 1].  
    Has no effect on shapes that don't support the
    fillColor property.

    For users who are used to using colors in the range [0, 255] they can simply divide their color by 255 to achieve a [0,1] range.

    For example: setDefaultFillColor(layer,shapeType, 50/255, 150/255 , 200/255, 255/255)

     

     Set Default Rotation in Radians (New!)

    setDefaultRotation(layer, shapeType, radians)

    Set the default rotation for all shapeType on layer
    Rotation is specified in CCW
    radians radians. 
    Has no effect on shapes that don't support the
    rotation property. 

     

     Set Default Shadow (New!)

    setDefaultShadow(layer, shapeType, radius, r, g, b, a)

    Set the default shadow for all shapeType on layer with size radius.
    Red (r), green (g), blue (b), and alpha (a) components are specified, respectively, in the range [0, 1].  
    Has no effect on shapes that don't support the
    shadow property. 

     

    For users who are used to using colors in the range [0, 255] they can simply divide their color by 255 to achieve a [0,1] range.

    For example: setDefaultFillColor(layer,shapeType, 50/255, 150/255 , 200/255, 255/255)

     

     Set Default Stroke Color (New!)

    setDefaultStrokeColor(layer, shapeType, r, g, b, a) 

    Set the default stroke color for all shapeType on layer
    Red (
    r), green (g), blue (b), and alpha (a) components are specified, respectively, in the range [0, 1].  
    Has no effect on shapes that don't support the
    strokeColor property. 

     

    For users who are used to using colors in the range [0, 255] they can simply divide their color by 255 to achieve a [0,1] range.

    For example: setDefaultFillColor(layer,shapeType, 50/255, 150/255 , 200/255, 255/255)

     

     Set Default Stroke Width (New!)

    setDefaultStrokeWidth(layer, shapeType, width) 

    Set the default stroke width for all shapeType on layer. Width is specified in pixels. 
    Positive values produce an outer stroke, while negative values produce an inner stroke. 
    Has no effect on shapes that don't support the
    strokeWidth property. 

     

    • Properties

    Set Fill Color for the next shape

    setNextFillColor(layer, r, g, b, a) 

    Set the fill color of the next rendered shape on layer
    Red (
    r), green (g), blue (b), and alpha (a) components are specified, respectively, in the range [0, 1].  
    Has no effect on shapes that don't support the
    fillColor property.

     

    For users who are used to using colors in the range [0, 255] they can simply divide their color by 255 to achieve a [0,1] range.

    For example: setDefaultFillColor(layer,shapeType, 50/255, 150/255 , 200/255, 255/255)

     

     Set Rotation for the next shape in Radians

    setNextRotation(layer, radians)

    Set the rotation of the next rendered shape on layer
    Rotation is specified in CCW
    radians radians. 
    Has no effect on shapes that don't support the
    rotation property. 

     

     Set Rotation for the next shape in Degrees

    setNextRotationDegrees(layer, degrees)

    Set the rotation of the next rendered shape on layer
    Rotation is specified in CCW
    degrees degrees. 
    Has no effect on shapes that don't support the
    rotation property. 

     

     Set Shadow for the next shape (New!)

    setNextShadow(layer, radius, r, g, b, a)

    Set the shadow of the next rendered shape on layer with size radius.
    Red (r), green (g), blue (b), and alpha (a) components are specified, respectively, in the range [0, 1].  
    Has no effect on shapes that don't support the
    shadow property. 

     

    For users who are used to using colors in the range [0, 255] they can simply divide their color by 255 to achieve a [0,1] range.

    For example: setDefaultFillColor(layer,shapeType, 50/255, 150/255 , 200/255, 255/255)

     

     Set Stroke Color for the next shape

    setNextStrokeColor(layer, r, g, b, a) 

    Set the stroke color of the next rendered shape on layer
    Red (
    r), green (g), blue (b), and alpha (a) components are specified, respectively, in the range [0, 1].  
    Has no effect on shapes that don't support the
    strokeColor property. 

     

    For users who are used to using colors in the range [0, 255] they can simply divide their color by 255 to achieve a [0,1] range.

    For example: setDefaultFillColor(layer,shapeType, 50/255, 150/255 , 200/255, 255/255)

     

     Set Stroke Width for the next shape

    setNextStrokeWidth(layer, width) 

    Set the stroke width of the next rendered shape on layer. Width is specified in pixels. 
    Positive values produce an outer stroke, while negative values produce an inner stroke. 
    Has no effect on shapes that don't support the
    strokeWidth property. 

     

     Set Text Alignment for the next shape (New!)

    setNextTextAlign(layer, alignH, alignV)
    

    Set the next text alignment for the next rendered shape on layer.

    alignH controls the horizontal alignment of a text shape relative to the draw coordinates.

    alignV controls the vertical alignment of a text shape relative to the draw coordinates.

     

    alignH must be one of the following built-in constants:

    AlignH_Left
    AlignH_Center
    AlignH_Right

     

    alignV must be one of the following built-in constants:

    AlignV_Ascender
    AlignV_Baseline
    AlignV_Top
    AlignV_Middle
    AlignV_Bottom
    AlignV_Descender

     

    Note that there is a subtle difference between AlignV_Ascender/AlignV_Descender and AlignV_Top/AlignV_Bottom: the ascender and descender alignment modes anchor a text string to a global top/bottom position of the font, while the top and bottom alignment modes anchor a text string relative to its own bounding box. Thus, while top/bottom are useful for aligning individual text strings with high precision, they depend on the contents of the text string that is rendered. On the other hand, ascender/descender align text in such a way that the alignment will not change depending on the text string. The correct choice will depend on your specific use case and needs.

     

    • Control Unit Interaction

    Get Input (New!)

    getInput() -> string 

    Return a string of input data (or an empty string, if no input has been set) that can be set via a control unit with the screen unit API function screen.setScriptInput(inputString).

     

     

    Set Output (New!)

    setOutput(outputString)

    Set the script’s output string to outputString, which can be retrieved via a control unit with the screen unit API function screen.getScriptOutput().

     

    Link to comment
    Share on other sites

    Technical Details

    • How to use Properties

    Different shapes support different properties that can be set to alter various aspects of the rendering of the shape. The current properties are: 
    fillColor - the interior color of the shape 
    rotation - the angular orientation of the shape about its center 
    shadow - the shadow of the shape 
    strokeColor - the color the border around the shape 
    strokeWidth - the size, in pixels of the border around the shape; negative values produce an interior border, while positive values produce an exterior border 

    The current API design requires that these properties are set before adding the shape, so that, for example, rendering a red rectangle looks as follows: 

    local layer = createLayer() 
    setNextFillColor(layer, 1, 0, 0, 1) -- red with 100% alpha 
    addBox(layer, 0, 0, 16, 16)

     

    When you set a property, it does not persist. Thus, to render many red rectangles, you must call setNextFillColor each time. 

    Note that every shape rendering command consumes all properties for that layer even if the shape does not support them. 
    This is for safety and to ensure that the global state does not impact your script in confusing ways. 
    For example, although images do not currently support a stroke color or width, if you call setNextStrokeWidth before addImage, the stroke width will be reset to the default after the call to addImage. 
    Consult the above API specifications to learn which properties are supported for which shapes. 

     

    • Shape Render Order

    When you need explicit control over the top-to-bottom ordering of rendered elements, you should use layers.

    As stated in the createLayer() API documentation, each layer that is created within a script will be rendered on top of each previous layer, such that the first layer created appears at the bottom, while the last layer created appears at the top.

     

    Shapes that live on the same layer do not offer as much control. Among the same type of shape, instances rendered later will appear on top of those rendered before. So if you add two boxes to a layer, the last box added will appear on top.

     

    However, the situation is more complex when mixing different shapes. For efficiency, rendering happens in batches, so that all instances of a given shape on the same layer are drawn at the same time.

     

    This means, in general, that all instances of one shape will appear below or above all instances of other shapes, regardless of the relative order in which they were added to the layer.

     

    Currently, the ordering is as follows, from top to bottom:

    addText() -- Top
    addQuad()
    addTriangle()
    addLine()
    addCircle()
    addBoxRounded()
    addBox()
    addImage() -- Bottom

     

    Thus, all boxes will always render below all circles on the same layer, and text on the same layer will appear on top of both.

    It is not possible to control this behavior, nor is it a good idea to rely on it, as it is subject to change in the future.
    If you need to rely on something appearing in front of something else, you should probably use multiple layers. (There is currently a maximum of 8 layers)

     

    • Render Cost Limitations

    Since render script is intended to solve screen unit performance problems, we impose relatively harsh restrictions on content compared to HTML.  

    This does not mean you won’t be able to create amazing, detailed, high-framerate screen contents – it simply means that you’ll need to be aware of the budgeting mechanism. 


    Any render script call that draws a shape (box, circle, line, text..) adds to a hidden ‘cost’ metric, that consumes some of the screen’s total rendering budget.

    The details of how this cost is computed are beyond the scope of this document, and it is likely that they will undergo changes in the future as we learn how to best optimize this technology. Roughly-speaking, however, the render cost incurred by any shape is proportional to the screen-space area of the shape, plus a constant factor.

     

    This means that a box of dimension 16 x 16 consumes roughly four times as much render cost as a box of 8 x 8 . This is fairly intuitive when you realize that the number of pixels filled by the larger box is,  indeed, four times that of the smaller box. Note that four times is not exact, as we neglected to consider the constant factor, but it is close enough. 


    For most render scripts, it is unlikely that the maximum cost will ever be exceeded, so most users probably don’t need to worry too much about this mechanism. However, drawing lots of large text or lots of large, overlapping images, may cause you to exceed the budget. 


    If you wish to dig deeply into this mechanism, you can do so using the built-in API calls getRenderCost() and getRenderCostMax().

    getRenderCost() can be called at any point during a render script to see how much all the contents added so far costs.

    You can use this function to profile the specific cost of specific shapes, if you wish. 

     

    It is easy enough to draw a render cost profiler on top of your screen to help you understand this mechanism. You can add this snippet at the end of your script to do so (you may need to adjust the text’s fill color if your background is a light colour):

    -- render cost profiler 
    if true then 
     local layer = createLayer() 
     local font = loadFont('Play-Bold', 14) 
     setNextFillColor(layer, 1, 1, 1, 1) 
     addText(layer, font, string.format('render cost : %d / %d',  getRenderCost(), getRenderCostMax()), 8, 16) 
    end

     

    • Coordinate Space

    All render script coordinates are in screen pixels, ranging from (0, 0) at the top-left of the screen, to (width, height) at the bottom-right.

    The width and height of the screen in pixels can be retrieved by calling getResolution().

    For maximal robustness, and the ability to fit different aspect ratios, scripts should be written so as to adapt to the resolution using getResolution().

     

    • Control Units and Synchronization

    It is important to know that screen unit Lua runs locally to the player viewing it, this means that dynamic content seen for one player will likely not match what is seen by another player as the two will often be in differing states. For example the bouncy ball example below will not display the ball in the same location for both players viewing the same screen. To achieve similar results for multiple players you can route communication to a screens getInput() which can be interacted with from the control unit side using screen.setScriptInput(). By setting the screen.setScreenInput() from a programming board, we set a string variable (getInput()) for all viewers of the screen unit that will synchronize about once per second. (This is already a considerable amount faster than HTML does currently.)

     

    Similarly a screen unit can set an output string variable using setOutput() which can then be read by the programming board using screen.getScriptOutput() to receive data back from the screen. It is important to note that a screens output field is local only, this is a technical limitation for synchronization reasons. 

     

    The limits of these fields are currently 1024 characters at any given time.

     

    Using JSON strings it is possible to send back and forth packets of data to and from the screens for the player running both the control unit and the screen.

    All other players are limited to the input field only, this allows for one player to "control" the interaction and data exchange between the programming board and screen unit. And all others to use and operate the screen using the screen Lua.

    An example of how we see this working, would be that the pilot of a ship controls it from the control seat with a screen in front of them which allows them to see various readouts from their ship. Using a mouse they are able to click various interface parts to use the screen to control aspects of their ship. These are relayed back to their control unit using setOutput(). In the meantime fellow crew members are still able to see those readouts animated and moving in real time from their perspective, even if the data they are receiving is only synchronized once per second.

     

    Link to comment
    Share on other sites

    Sample Scripts (Basic)

    The following scripts are intended to minimally demonstrate basic usage of render script.

     

    • Checkerboard

    Creates a simple checkerboard pattern by covering the screen with rectangles of alternating color. 

    local resX, resY = getResolution()
    local tilesX, tilesY = 32, 16
    
    local tileW, tileH = resX / tilesX, resY / tilesY
    local layer = createLayer()
    
    for y = 0, tilesY - 1 do
        local color = y % 2
        for x = 0, tilesX - 1 do
            if color == 0 then
                setNextFillColor(layer, 1, 1, 1, 1)
            else
                setNextFillColor(layer, 0, 0, 0, 1)
            end
            addBox(layer, x * tileW, y * tileH, tileW, tileH)
            color = (color + 1) % 2
        end
    end

     

    6f1790936ae2a0fdc1f7b0566670c86a.jpg

     

    • Simple Text

    Draws some lines of text. 

    local lines = {
        "Lua screen units are great!",
        "They can render lots of contents...",
        "even really smooth, complex animations!",
        "They're very fast.",
        "They're not as easy to use as HTML,",
        "...but once you get the hang of them...",
        "you'll be unstoppable.",
    }
    local fontSize = 32
    local font = loadFont('Play', fontSize)
    
    local layer = createLayer()
    local y = fontSize + 16
    
    for i, line in ipairs(lines) do
        addText(layer, font, line, 16, y)
        y = y + fontSize + 2
    end

     

    1fec1db36e08399c98499c01dc9ce7c6.jpg

     

    • Bouncy Ball

    Demonstrates usage of both one-time initialization for local persistent data, as well as requestAnimationFrame(), to draw a moving ball that bounces off the edges of the screen.

    local resX, resY = getResolution()
    local dt = 100 * getDeltaTime()
    
    -- initialize ball position once
    if not init then
        init = true
        ball = {}
        ball.x = resX * math.random()
        ball.y = resY * math.random()
        ball.vx = 2 * math.random() - 1
        ball.vy = 2 * math.random() - 1
        ball.radius = math.min(resX, resY) / 8
    end
    
    -- animate ball position
    ball.x = ball.x + ball.vx * dt
    ball.y = ball.y + ball.vy * dt
    if ball.x < ball.radius or ball.x > (resX-ball.radius) then
        ball.x = ball.x - ball.vx * dt
        ball.vx = -ball.vx
    end
    if ball.y < ball.radius or ball.y > (resY-ball.radius) then
        ball.y = ball.y - ball.vy * dt
        ball.vy = -ball.vy
    end
    
    -- render
    local layer = createLayer()
    addCircle(layer, ball.x, ball.y, ball.radius)
    requestAnimationFrame(1)

     

     

     

    Link to comment
    Share on other sites

    Sample Scripts (Advanced)

    These more advanced scripts demonstrate what can ultimately be achieved with this API. Perhaps they will inspire you a bit!

     

    • Entropy

    Like the bouncy ball example, but fancier!

    local ballCount = 5000
    local speed = 20
    
    local rx, ry = getResolution()
    
    --[[ init ]]--------------------------------------------------------------------
    
    if not init then
        init = true
    
        function randF (a, b)
            return a + (b - a) * math.random()
        end
            
        function randExp ()
            return -math.log(1.0 - math.random())
        end
    
        balls = {}
        for i = 1, ballCount do
            local e = {}
            e.x = randF(0, rx)
            e.y = randF(0, ry)
            e.r = 1 + 1.0 * math.log(1.0 - math.random()) ^ 2.0
            e.vx = randF(-1, 1) * randExp()
            e.vy = randF(-1, 1) * randExp()
            e.cx = 1.0 * math.random()
            e.cy = 0.1 * math.random()
            e.cz = 0.4 * math.random()
            e.ca = math.random()
            table.insert(balls, e)
        end
    end
    
    --[[ simulation ]]--------------------------------------------------------------
    
    local dt = speed * getDeltaTime()
    
    for _, v in ipairs(balls) do
        v.x = v.x + dt * v.vx
        v.y = v.y + dt * v.vy
        if v.x < 0 or v.x > rx then
            v.x = v.x - dt * v.vx
            v.vx = -v.vx
        end
        if v.y < 0 or v.y > ry then
            v.y = v.y - dt * v.vy
            v.vy = -v.vy
        end
    end
    
    --[[ rendering ]]---------------------------------------------------------------
    
    local l = createLayer()
    
    -- render balls
    for _, e in ipairs(balls) do
        setNextFillColor(l, e.cx, e.cy, e.cz, e.ca)
        addCircle(l, e.x, e.y, e.r)
    end
    
    -- render title
    local font = loadFont('Play-Bold', 64)
    addText(l, font, '{{ E N T R O P Y }}', 32, ry - 32)
    
    requestAnimationFrame(1)
    
    --------------------------------------------------------------------------------

     

     

    • Radial Menu

    Practical example of how responsive UI elements could be designed, creates 3 radial menu's using the same function.
    Left: Example with transparency on the circle layer to demonstrate layering and the use of math to define the circle stroke.
    Top right: Example without transparency, drawn to a different scale.
    Bottom right: Example of a collapsible version saving screen real-estate and reducing rending costs while collapsed. (Only a dot is drawn while collapsed)

    -- Get screen resolution and center coordinates
    local resolutionX,resolutionY = getResolution()
    local centerX,centerY = resolutionX/2, resolutionY/2
    
    -- Draw radial menu layers
    local triLayer = createLayer()
    local circleLayer = createLayer()
    local textLayer = createLayer()
    
    -- Define some helpers to convert degrees to radians
    local d2r = math.pi/180
    
    -- Helper function to get distance of cursor from coordinates
    function getCursorDistance(x,y)
        local curX,curY = getCursor()
        curX = curX - x
        curY = curY - y
        return math.sqrt(curX^2+curY^2)
    end
    
    -- Helper function to check if cursor is inside a radial menu section
    function isCursorInside(radialCenterX,radialCenterY,radiusMin,radiusMax,degMin,degMax)
        local curX,curY = getCursor()
        curX = curX - radialCenterX
        curY = curY - radialCenterY
        local magnitude = getCursorDistance(radialCenterX,radialCenterY)
        local deg = math.atan(curY/curX)/d2r
        if curX < 0 then deg = deg + 180 end
        if curX > 0 and curY < 0 then deg = deg + 360 end
        if magnitude <= radiusMax and magnitude >= radiusMin and deg > degMin and deg < degMax then return true end
    end
    
    -- Define function that can draw a radial menu on demand
    function drawRadialMenu(posX,posY,radius,steps,hide,strokeAndCenterAlpha)
        -- Define default variables
        local posX = posX or centerX
        local posY = posY or centerY
        local radius = radius or 200
        local steps = steps or 15
        if hide ~= nil then hide = hide else hide = true end
        local strokeAndCenterAlpha = strokeAndCenterAlpha or 1
        
        -- minimum of 4 segments
        steps = math.max(steps,4)
        
        -- work out the depth of the triangular facets along the circumferance and use it to determine the outer circle stroke width
        local aX,aY = radius*math.cos(0) , radius*math.sin(0) 
        local bX,bY = radius*math.cos(((360/steps)*1)*d2r) , radius*math.sin(((360/steps)*1)*d2r)
        local stroke = math.min(-(radius-math.sqrt(((aX+bX)/2)^2+((aY+bY)/2)^2)),-3)
        
        if not hide or (hide and getCursorDistance(posX,posY) < (radius)) then
                
            -- Draw triangular segments
            local alpha = 0.5
            for step=1,steps,1 do
            
                -- calculate the corners of the triangles on the radius
                local aX,aY = posX + (radius*math.cos(((360/steps)*(step-1))*d2r)) , posY + (radius*math.sin(((360/steps)*(step-1))*d2r)) 
                local bX,bY = posX + (radius*math.cos(((360/steps)*step)*d2r)) , posY + (radius*math.sin(((360/steps)*step)*d2r))
                
                -- call helper function to determine if cursor is inside the segment and change color if true
                if isCursorInside(posX,posY,radius/3,radius,((360/steps)*(step-1)),(360/steps)*step) then
                    setNextFillColor(triLayer,1,1,1,1)
                    
                    -- create center number while the cursor is over a selection
                    local font = loadFont('Montserrat', math.floor(radius/4))
                    setNextFillColor(textLayer, 1, 1, 1, 1)
                    addText(textLayer, font,step, posX-((radius/12)*#tostring(step)), posY+math.floor(radius/12))
                else
                    setNextFillColor(triLayer,1,0,0,alpha + 0.5)
                end
                
                -- Draw the triangle based on pre-calculated coordinates and with the color now preset
                addTriangle(triLayer,posX,posY,aX,aY,bX,bY)
                
                -- flip the alpha between 1 and 0 for the next segment
                alpha = 0.5 - alpha
            end
            
            -- Create the outer stroke which should be a negative value so it overlaps the triangle facets 
            setNextStrokeColor(circleLayer,0.5,0.5,1,strokeAndCenterAlpha)
            setNextFillColor(circleLayer,0,0,0,0)
            setNextStrokeWidth(circleLayer,stroke)
            addCircle(circleLayer,posX,posY,radius)
            
            -- Create the inner stroke and fill to create the "hole" in the center at 1/4th of the radius.
            setNextStrokeColor(circleLayer,0.5,0.5,1,strokeAndCenterAlpha)
            setNextFillColor(circleLayer,0,0,0,strokeAndCenterAlpha)
            setNextStrokeWidth(circleLayer,stroke)
            addCircle(circleLayer,posX,posY,radius/4)
        
        else
        
            -- Create a transparent dot to indicate its position while its hidden.
            setNextFillColor(circleLayer,0.5,0.5,1,0.2)
            addCircle(circleLayer,posX,posY,radius/4)
            
        end
    end
    
    -- Draw three examples
    
    -- Example 1 (large one on left) with transparency to demonstrate how the circular shape was achieved with the triangles.
    drawRadialMenu(resolutionX/3,centerY,200,6,false,0.5)
    
    -- Example 2 (Top right) Does not hide, shows the result without transparency
    drawRadialMenu((resolutionX/3)*2,resolutionY/3,100,8,false)
    
    -- Example 3 (Bottom right) Remains hidden and un-rendered until the cursor enters its area, reducing render cost by ~120000 while hidden.
    drawRadialMenu((resolutionX/3)*2,(resolutionY/3)*2,80,36)
    
    -- render cost profiler
    if true then
        local layer = createLayer()
        local font = loadFont('Play-Bold', 14)
        setNextFillColor(layer, 1, 1, 1, 1)
        addText(layer, font, string.format('render cost : %d / %d', getRenderCost(), getRenderCostMax()), 8, 16)
    end
    
    -- Request a refresh
    requestAnimationFrame(1)

     

     

    • Text positioning

    Demonstration example of the various alignment points for text using Font Metrics and Text Align.

    local fontName = 'Play'
    local fontSize = 32
    local font = loadFont(fontName, fontSize)
    local fontDebug = loadFont('FiraMono', 12)
    local back = createLayer()
    local layer = createLayer()
    local rx, ry = getResolution()
    local ascender, descender = getFontMetrics(font)
    
    local text = "??"
    frame = (frame or 0) + 1
    
    setDefaultFillColor(layer, Shape_Text, 1, 1, 1, 1)
    setDefaultFillColor(layer, Shape_Circle, 1, 0, 0, 1)
    
    function drawTextBox (x, y, alignH, alignV)
        local w, h, ascent, descent = getTextBounds(font, text)
    
        if alignH == AlignH_Left then
            x = x
        elseif alignH == AlignH_Center then
            x = x - w/2
        elseif alignH == AlignH_Right then
            x = x - w
        end
    
        if alignV == AlignV_Ascender then
            y = y
            h = ascender - descender
        elseif alignV == AlignV_Top then
            y = y
        elseif alignV == AlignV_Middle then
            h = ascender - descender
            y = y - h/2
        elseif alignV == AlignV_Baseline then
            y = y - ascent
        elseif alignV == AlignV_Bottom then
            y = y - h
        elseif alignV == AlignV_Descender then
            h = ascender - descender
            y = y - h
        end
    
        setNextFillColor(back, 0.1, 0.5, 1.0, 1.0)
        setNextStrokeWidth(back, 1)
        addBox(back, x, y, w, h)
    end
    
    function main ()
        if frame % 2 == 0 then
            text = "acemnor "
        else
            text = "aCjÅkö[]|!r "
        end
    
        -- display debug info about font
        if true then
            local infoLines = {
                string.format("     font : %s", fontName),
                string.format("     size : %d", fontSize),
                string.format(" ascender : %d", ascender),
                string.format("descender : %d", descender),
            }
    
            for i, line in ipairs(infoLines) do
                setNextTextAlign(layer, AlignH_Left, AlignV_Top)
                addText(layer, fontDebug, line, 16, 16 + (i - 1) * 12)
            end
        end
    
        local alignHs = {
            'AlignH_Left',
            'AlignH_Center',
            'AlignH_Right',
        }
    
        local alignVs = {
            'AlignV_Ascender',
            'AlignV_Top',
            'AlignV_Middle',
            'AlignV_Baseline',
            'AlignV_Bottom',
            'AlignV_Descender',
        }
    
        -- draw text boxes with various alignments
        local sx = 300
        local sy = 80
        local x0 = rx/2 - sx * (#alignHs-1)/2
        local y0 = ry/2 - sy * (#alignVs-1)/2
        for y, alignVName in ipairs(alignVs) do
            for x, alignHName in ipairs(alignHs) do
                local px = x0 + sx * (x - 1)
                local py = y0 + sy * (y - 1)
                local alignV = _ENV[alignVName]
                local alignH = _ENV[alignHName]
                drawTextBox(px, py, alignH, alignV)
                setNextTextAlign(layer, alignH, alignV)
                addText(layer, font, text, px, py)
                addCircle(layer, px, py, 3)
            end
        end
    
        -- draw AlignH labels
        for x, alignH in ipairs(alignHs) do
            setNextTextAlign(layer, _ENV[alignH], AlignV_Descender)
            setNextFillColor(layer, 0.5, 0.5, 0.5, 1)
            addText(layer, fontDebug, alignH, x0 + (x - 1)*sx, y0 - 16)
        end
    
        -- draw AlignV labels
        for y, alignV in ipairs(alignVs) do
            setNextTextAlign(layer, AlignH_Right, _ENV[alignV])
            setNextFillColor(layer, 0.5, 0.5, 0.5, 1)
            addText(layer, fontDebug, alignV, x0 - 16, y0 + (y - 1)*sy)
        end    
    end
    
    if not isFontLoaded(font) then
        requestAnimationFrame(10)
    else
        main()
        requestAnimationFrame(120)
    end

     

    textAlign.png

    Link to comment
    Share on other sites

    • NQ-Deckard changed the title to Lua Screen Units API and Instructions (Updated: 19/07/2021)

    Sample Scripts (Widget Objects)

    These widget scripts show some examples of interactive UI elements created using the new Lua Screen Units.

     

    • Sliders

    This allows you to create and use sliders for number values, the example handles change the color properties of background square.

    ------------------------------------
    -- Slider widget example
    -- Usage:
    --     Creates a slider style widget with a minimum and maximum value.
    -- 
    -- Init:
    --     widget = Slider(x,y,width,length,minValue,maxValue,defaultValue,label)
    -- Return fractional value of the slider position:
    --     widget:getFraction()
    -- Force set the value of the slider:
    --     widget:setValue(value)
    -- Draw the slider:
    --     widget:draw(layer)
    -- Return the value of slider:
    --     widget.value
    ------------------------------------
    
    if not Slider then
        -- Object definition: Lets start by creating a meta table with some properties
        local slider = {}
        slider.__index = slider
        function Slider(x, y, width, length, min, max, defaultValue, label)
            self = {}
            self.x = x or 0
            self.y = y or 0
            self.l = length or 100
            self.w = width or 20
            self.min = min or 0
            self.max = max or 1
            self.value = math.max(self.min, math.min(self.max, defaultValue)) or self.min
            self.label = label or ""
            self.doDrag = false
            return setmetatable(self, slider)
        end
        
        -- example helper method that returns the position of the slider between a value of 0 and 1
        function slider:getFraction()
            return (self.value - self.min) / (self.max - self.min)
        end
        
        -- example set method that allows us to set the value of the slider
        function slider:setValue(val)
            self.value = math.max(self.min, math.min(self.max, val))
        end
        
        -- example draw method that draws the slider on the screen using the given layer and stored properties
        function slider:draw(layer)
            -- Collect some cursor data
            local cx, cy = getCursor()
            local cPressed = getCursorPressed()
            local cReleased = getCursorReleased()
            
            -- Determine if the handle for this slider was just clicked or released
            if cPressed and cx >= self.x and cx <= (self.x + self.w) and cy >= self.y + self.l - (self:getFraction() * self.l) and cy <= self.y + (self.w / 5) + self.l - (self:getFraction() * self.l) + (2 * self.w / 5) then
                self.doDrag = true
            elseif cReleased == true or cx == -1 then
                self.doDrag = false
            end
        
            -- If we are currently dragging the handle we should update the position and value of the slider.
            if self.doDrag then
                local vy = 1 - ((cy - self.y - self.w / 5) / self.l)
                self.value = math.max(self.min, math.min(self.max, self.min + ((self.max - self.min) * vy)))
            end
            
            -- Finaly lets draw the entire slider on the screen
            
            -- Draw the background vertical bar (You could change the color below this line, or make a property for it)
            setNextFillColor(layer, 0.5, 0.5, 1, 1)
            addBoxRounded(layer, self.x + (self.w / 2.5), self.y + self.w / 5, self.w / 5, self.l, math.min(self.w / 10, self.l / 10))
            
            -- Draw the handle (You could change the color below this line, or make a property for it)
            setNextFillColor(layer, 0.7, 0.7, 1, 1)
            addBoxRounded(layer, self.x, self.y + self.l - (self:getFraction() * self.l), self.w, self.w / 2.5, self.w / 10)
            
            -- Lets load a font for this slider, if you want lots of sliders, you may want to load the font outside this object to avoid to many fonts.
            local font = loadFont("Montserrat", self.w / 5)
            
            -- Fetch the dimensions of the label and value we are about to draw
            local lw,lh = getTextBounds(font, self.label)
            local vw,vh = getTextBounds(font, math.floor(self.value * 100) * 0.01)
            
            -- draw the label above the slider
            addText(layer, font, self.label, self.x + (self.w / 2) - (lw / 2), self.y - (lh * 0.5))
            
            -- draw the value on the handle
            setNextFillColor(layer, 0.1, 0.1, 0.1, 1)
            addText(layer, font, math.floor(self.value * 100) * 0.01, self.x + (self.w / 2) - (vw / 2), self.y + self.l - (self:getFraction() * self.l) + (self.w / 5) + (vh / 2))
        end
    end
    
    ------------------------------------
    -- Display
    ------------------------------------
    
    -- Collect the screen resolution for positioning
    local rx, ry = getResolution()
    
    -- Create two layers, one for the background square, the other for the sliders
    local backgroundLayer = createLayer()
    local sliderLayer = createLayer()
    
    -- Run this portion only once on screen startup, this creates the slider objects
    if not init then
        init = true
        sliderRed   = Slider(rx/5-25,   ry/8,50, ry/8*6, 0, 1, 0.2, "Red")
        sliderGreen = Slider(rx/5*2-25, ry/8,50, ry/8*6, 0, 1, 0.4, "Green")
        sliderBlue  = Slider(rx/5*3-25, ry/8,50, ry/8*6, 0, 1, 0.6, "Blue")
        sliderAlpha = Slider(rx/5*4-25, ry/8,50, ry/8*6, 0, 1, 0.8, "Alpha")
    end
    
    -- every frame, draw the 4 slider
    sliderRed:draw(sliderLayer)
    sliderGreen:draw(sliderLayer)
    sliderBlue:draw(sliderLayer)
    sliderAlpha:draw(sliderLayer)
    
    -- every frame, draw the background square using the slider values as colour properties for the fill
    setNextFillColor(backgroundLayer, sliderRed.value, sliderGreen.value, sliderBlue.value, sliderAlpha.value)
    addBoxRounded(backgroundLayer,25,25,rx-50,ry-50,20)
    
    -- render cost profiler
    if true then
        local layer = createLayer()
        local font = loadFont('Play-Bold', 14)
        setNextFillColor(layer, 1, 1, 1, 1)
        addText(layer, font, string.format('render cost : %d / %d', getRenderCost(), getRenderCostMax()), 8, 16)
    end
    
    -- refresh the screen at a high rate to keep the slider elements responsive
    requestAnimationFrame(1)

     

    image.png

     

     

    • Toggle buttons

    This allows you to create and use toggle buttons for boolean values, the examples flip not only their own state, but also the horizontally neighbouring toggle.

    ------------------------------------
    -- Toggle widget example
    -- Usage:
    --     Creates a toggle button style widget with a true or false state.
    -- 
    -- Init:
    --     widget = Toggle(x, y, width, height, defaultState, label, font, onClick) -- onClick can be a function that is executed when the button is clicked.
    -- Draw the slider:
    --     widget:draw(layer)
    -- Return the value of slider:
    --     widget:getState()
    --   or
    --     widget.state
    -- Set the state of the toggle:
    --     widget:setState(boolean)
    -- Switch the state of the toggle:
    --     widget:switch() -- Also returns the new state of the widget
    ------------------------------------
    
    if not Toggle then
        -- Object definition: Lets start by creating a meta table with some properties
        local toggle = {}
        toggle.__index = toggle
        function Toggle(x, y, width, height, defaultState, label, font, onClick)
            self = {}
            self.x = x or 0
            self.y = y or 0
            self.h = height or 20
            self.w = width or 20
            self.state = defaultState or false
            self.label = label or ""
            self.onClick = onClick
            self.font = font or function() return loadFont("Montserrat",self.h * 5.5) end
            return setmetatable(self, toggle)
        end
        
        -- create a function that creates a new font if required, otherwise returns the font identifier from the properties
        local function getFont()
            local font = self.font
            if type(font) == "function" then
                font = self.font()
            end
            return font
        end
        
        -- draw method that draws the toggle on the screen unit
        function toggle:draw(layer)
            -- Collect cursor coordinates
            local cx, cy = getCursor()
            
            -- check if the mouse is over the button
            if cx >= self.x and cx <= self.x + self.w and cy >= self.y and cy <= self.y + self.h then
            
                -- handle the click action
                if getCursorReleased() then
                    self.state = not self.state
                    if self.onClick ~= nil then
                        self.onClick()
                    end
                end
                
                -- set the mouseover colors
                if self.state and getCursorDown() then
                    setNextFillColor(layer, 1, 0.6, 0.8, 1)
                elseif getCursorDown() then
                    setNextFillColor(layer, 0.6, 1, 0.8, 1)
                elseif self.state then
                    setNextFillColor(layer, 1, 0.2, 0.2, 1)
                else
                    setNextFillColor(layer, 0.2, 0.8, 0.2, 1)
                end
                
            elseif self.state then
                -- set color if toggle is true
                setNextFillColor(layer, 0.2, 0.2, 0.6, 1)
            else
                -- set color if toggle is false
                setNextFillColor(layer, 0.2, 0.2, 0.2, 1)
            end
            -- draw the button toggle
            local w, h = self:getBounds()
            addBox(layer, self.x, self.y + (h / 2 - self.h / 2), self.w, self.h)
            setNextTextAlign(layer, AlignH_Left, AlignV_Middle)
            addText(layer, getFont(), self.label, self.x + self.w + (self.w / 5), self.y + (h / 2))
        end
        
        -- simple example helper method that returns the current state of the toggle
        function toggle:getState()
            return self.state
        end
        
        -- simple example helper method that sets the state of the toggle
        function toggle:setState(newState)
            if type(newState) == "boolean" then self.state = newState end 
        end
        
        -- simple example helper method that changes and returns the new state of the toggle
        function toggle:switch()
            self.state = not self.state
            return self.state
        end
        
        -- helper method that returns the width and height of toggle
        function toggle:getBounds()
            local tx, ty = getTextBounds(getFont(),self.label)
            return self.x + self.w + (self.w / 5) + tx, math.max(self.h, ty)
        end
    end
        
    
    ----------------------------------------------------
    -- Display
    ----------------------------------------------------
    
    -- Collect the screen resolution for positioning
    local rx,ry = getResolution()
    
    -- Create a layer to draw the toggles
    local exampleLayer = createLayer()
    
    -- Create a font for all the toggles
    local font = loadFont("Montserrat", 20)
    
    -- Run this portion only once on screen startup, this creates the toggle objects
    if not init then
        init = true
        example1 = Toggle(rx / 7, ry / 5 - rx / 40, rx / 20, rx / 20, true, "Example Toggle 1 (Linked)", font)
        example2 = Toggle(rx / 7, ry / 5 * 2 - rx / 40, rx / 20, rx / 20, false, "Example Toggle 2 (Linked)", font)
        example3 = Toggle(rx / 7, ry / 5 * 3 - rx / 40, rx / 20, rx / 20, true, "Example Toggle 3", font)
        example4 = Toggle(rx / 7, ry / 5 * 4 - rx / 40, rx / 20, rx / 20, false, "Example Toggle 4", font)
        exampleA = Toggle(rx / 7 * 4, ry / 5 - rx / 40, rx / 20, rx / 20, false, "Example Toggle A (Linked)", font)
        exampleB = Toggle(rx / 7 * 4, ry / 5 * 2 - rx / 40, rx / 20, rx / 20, true, "Example Toggle B (Linked)", font)
        exampleC = Toggle(rx / 7 * 4, ry / 5 * 3 - rx / 40, rx / 20, rx / 20, false, "Example Toggle C", font)
        exampleD = Toggle(rx / 7 * 4, ry / 5 * 4 - rx / 40, rx / 20, rx / 20, true, "Example Toggle D", font)
    end
    
    -- for toggle, make the toggle next to it always the opposite and then draw the toggle
    example1:setState(not exampleA:getState())
    example1:draw(exampleLayer)
    example2:setState(not exampleB:getState())
    example2:draw(exampleLayer)
    example3:draw(exampleLayer)
    example4:draw(exampleLayer)
    
    exampleA:setState(not example1:getState())
    exampleA:draw(exampleLayer)
    exampleB:setState(not example2:getState())
    exampleB:draw(exampleLayer)
    exampleC:draw(exampleLayer)
    exampleD:draw(exampleLayer)
    
    -- render cost profiler
    if true then
        local layer = createLayer()
        local font = loadFont('Play-Bold', 14)
        setNextFillColor(layer, 1, 1, 1, 1)
        addText(layer, font, string.format('render cost : %d / %d', getRenderCost(), getRenderCostMax()), 8, 16)
    end
    
    -- Refresh the screen
    requestAnimationFrame(1)

     

    image.png

    Link to comment
    Share on other sites

    Sample Scripts (Control Unit Interaction)

    These scripts show some simple examples of Control Units interacting with screen units.

     

    • Light Control (Screen to Programming Board)

    This example demonstrates clickable buttons on a screen unit, that set the screens output to a value, the programming board reads that value and then clears it and toggles lights based on that data. (See the top section for the programming board code)

    --[[----------------------------------------------------------------------------
    -- Add the following section to a programming boards System.Update()
    -- Link the screen unit and three lights to the programming board
    -- Name the slots in the programming board respectively: screen, light0, light1, light2
    
    local handlers = {}
    handlers['Light 0'] = function() light0.toggle() end
    handlers['Light 1'] = function() light1.toggle() end
    handlers['Light 2'] = function() light2.toggle() end
    
    local output = screen.getScriptOutput()
    if #output > 0 then
        screen.clearScriptOutput()
        if handlers[output] then handlers[output]() end
    end
    
    --]]----------------------------------------------------------------------------
    -- Screen render script below
    --------------------------------------------------------------------------------
    
    font = loadFont('Play-Bold', 32)
    rx, ry = getResolution()
    cx, cy = getCursor()
    layer = createLayer()
    click = getCursorPressed()
    
    --------------------------------------------------------------------------------
    
    if not Button then
        local mt = {}
        mt.__index = mt
        function Button (text, x, y)
            return setmetatable({
                text = text,
                x = x,
                y = y,
            }, mt)
        end
    
        function mt:draw ()
            local sx, sy = self:getSize()
            local x0 = self.x - sx/2
            local y0 = self.y - sy/2
            local x1 = x0 + sx
            local y1 = y0 + sy
            
            local r, g, b = 0.3, 0.7, 1.0
            if cx >= x0 and cx <= x1 and cy >= y0 and cy <= y1 then
                r, g, b = 1.0, 0.0, 0.4
                if click then setOutput(self.text) end
            end
            
            setNextShadow(layer, 64, r, g, b, 0.3)
            setNextFillColor(layer, 0.1, 0.1, 0.1, 1)
            setNextStrokeColor(layer, r, g, b, 1)
            setNextStrokeWidth(layer, 2)
            addBoxRounded(layer, self.x - sx/2, self.y - sy/2, sx, sy, 4)
            setNextFillColor(layer, 1, 1, 1, 1)
            setNextTextAlign(layer, AlignH_Center, AlignV_Middle)
            addText(layer, font, self.text, self.x, self.y)
        end
    
        function mt:getSize ()
            local sx, sy = getTextBounds(font, self.text)
            return sx + 32, sy + 16
        end
    
        function mt:setPos (x, y)
            self.x, self.y = x, y
        end
    end
    
    function drawFree (elems)
        for i, v in ipairs(elems) do v:draw() end
    end
    
    function drawListV (elems, x, y)
        for i, v in ipairs(elems) do
            local sx, sy = v:getSize()
            v:setPos(x, y)
            v:draw()
            y = y + sy + 4
        end
    end
    
    function drawUsage ()
        local font = loadFont('FiraMono', 16)
        setNextTextAlign(layer, AlignH_Center, AlignV_Top)
        addText(layer, font, "Activate board to enable buttons!", rx/2, ry - 32)
    end
    
    function drawCursor ()
        if cx < 0 then return end
        addLine(layer, cx - 12, cy - 12, cx + 12, cy + 12)
        addLine(layer, cx + 12, cy - 12, cx - 12, cy + 12)
    end
    
    --------------------------------------------------------------------------------
    
    local buttons = {
        Button('Light 0', 128, 90),
        Button('Light 1', 128, 290),
        Button('Light 2', 128, 490),
    }
    
    drawFree(buttons)
    
    drawUsage()
    drawCursor()
    
    requestAnimationFrame(5)

     

    image.png

     

     

    • Light Control (Screen to Programming Board)

    This allows you to create and use toggle buttons for boolean values, the examples flip not only their own state, but also the horizontally neighbouring toggle.

    --[[----------------------------------------------------------------------------
    -- Add to programming board's unit.start() and connect it to this screen.
    
    local messages = {
        "Hi, I am a message!",
        "Yes hello this is Lua Screen.",
        "We love Pops the Hamster!",
        "WARNING: No warnings found.",
        "I am a screen unit.",
        "Are you enjoying this?",
        "Pending Screen Operations",
        "Lua Screen Units o/",
        "It is NOT NQ-Node\'s fault.",
        "Knock knock...",
        "Who's there?",
        "Ran out of Knock knock jokes.",
        "It is all NQ-Deckard\'s fault."
    }
    
    local message = messages[math.random(1, #messages)]
    
    local params = {
        message = message,
        testNumber = 1.337,
        testStr = 'hello I am a string',
        testTable = {x = 1, y = 0, k = {1, 2, 3, 4}},
    }
    
    screen.setScriptInput(json.encode(params))
    unit.exit()
    
    --]]----------------------------------------------------------------------------
    -- Screen render script below
    --------------------------------------------------------------------------------
    
    local json = require('dkjson')
    local params = json.decode(getInput()) or {}
    
    message = params.message or '[ no message ]'
    fontSize = params.fontSize or 64
    color = params.color or {r=1.0,g=0,b=0.3}
    
    --------------------------------------------------------------------------------
    
    local font = loadFont('Play-Bold', fontSize)
    local rx, ry = getResolution()
    local layer = createLayer()
    local cx, cy = getCursor()
    
    local sx, sy = getTextBounds(font, message)
    setDefaultStrokeColor(layer, Shape_Line, 1, 1, 1, 0.5)
    setNextShadow(layer, 64, color.r, color.g, color.b, 0.4)
    setNextFillColor(layer, color.r, color.g, color.b, 1.0)
    addBoxRounded(layer, (rx-sx-16)/2, (ry-sy-16)/2, sx+16, sy+16, 8)
    setNextFillColor(layer, 0, 0, 0, 1)
    setNextTextAlign(layer, AlignH_Center, AlignV_Middle)
    addText(layer, font, message, rx/2,ry/2)
    
    --------------------------------------------------------------------------------
    
    local fontCache = {}
    function getFont (font, size)
        local k = font .. '_' .. tostring(size)
        if not fontCache[k] then fontCache[k] = loadFont(font, size) end
        return fontCache[k]
    end
    
    function drawUsage ()
        local font = getFont('FiraMono', 16)
        setNextTextAlign(layer, AlignH_Center, AlignV_Top)
        addText(layer, font, "Activate for an exciting new message!", rx/2, ry - 32)
    end
    
    function drawCursor ()
        if cx < 0 then return end
        if getCursorDown() then
            setDefaultShadow(layer, Shape_Line, 32, color.r, color.g, color.b, 0.5)
        end
        addLine(layer, cx - 12, cy - 12, cx + 12, cy + 12)
        addLine(layer, cx + 12, cy - 12, cx - 12, cy + 12)
    end
    
    function prettyStr (x)
        if type(x) == 'table' then
            local elems = {}
            for k, v in pairs(x) do
                table.insert(elems, string.format('%s = %s', prettyStr(k), prettyStr(v)))
            end
            return string.format('{%s}', table.concat(elems, ', '))
        else
            return tostring(x)
        end
    end
    
    function drawParams ()
        local font = getFont('RobotoMono', 11)
        setNextTextAlign(layer, AlignH_Left, AlignV_Bottom)
        addText(layer, font, "Script Parameters", 16, 16)
        local y = 32
        for k, v in pairs(params) do
            setNextTextAlign(layer, AlignH_Left, AlignV_Bottom)
            addText(layer, font, k .. ' = ' .. prettyStr(v), 24, y)
            y = y + 12
        end
    end
    
    --------------------------------------------------------------------------------
    
    drawUsage()
    drawCursor()
    drawParams()
    
    requestAnimationFrame(1)

     

    image.png

    Link to comment
    Share on other sites

    Guest
    This topic is now closed to further replies.
    ×
    ×
    • Create New...