Clone wiki

SAGE2 / SAGE2 Application API

SAGE2 Application Methods:

Method Parameters
init(data) data is an object with initialization parameters, such as x, y, width, height, and date
load(date) called when state is updated (mainly used for remote sync, date is the date
draw(date) date is the date (used to calculate t and dt for animations). Within your application, call 'refresh' if you need a redraw. If it's interactive application, you can enable 'animation' in the file "instruction.json". the default frame rate is 60fps. Maximum frame rate can be controlled by the variable maxFPS (this.maxFPS = 20.0 for instance)
startResize(date) date is the date
resize(date) date is the date. Application can trigger a resize if needed by calling this.sendResize (newWidth, newHeight)
startMove(date) date is the date
move(date) date is the date
isMaster() returns true if Display Client is marked as the master, otherwise false
broadcast(functionName, data) calls function in non-master Display clients, functionName is the function's name and data is the parameter
event(type, position, user, data, date) type is the type of event, position contains the x and y positions of the event, user contains data about the user who triggered the event, data is an object containing extra data about the event, date is the date. See event description below.
quit() called when an application is closed

Boiler-plate for a canvas application

var myApp = SAGE2_App.extend( {
    init: function(data) {
        // data: contains initialization parameters, such as `x`, `y`, `width`, `height`, and `date`
        this.SAGE2Init(<html-tag container for your app (eg. 'img', 'canvas')>, data);

        this.resizeEvents = "continuous";//see below for other options

        // initialize your variables
        this.myvalue = 5.0;
    },

    //load function allows application to begin with a particular state.  Needed for remote site collaboration.
    load: function(date) {
        //your load code here- update app based on this.state
    },

    draw: function(date) {
        // application specific 'draw'
        this.log("Draw");
    },

    resize: function(date) {
        // to do:  may be a super class resize
        // or your resize code here
        this.refresh(date); //redraw after resize
    },

    event: function(type, position, user, data, date) {
        // see event handling description below

        // may need to update state here

        // may need to redraw
        this.refresh(date);
    },

    move: function(date) {
        // this.sage2_x, this.sage2_y give x,y position of upper left corner of app in global wall coordinates
                // this.sage2_width, this.sage2_height give width and height of app in global wall coordinates
                // date: when it happened
        this.refresh(date);
       },

    quit: function() {
        // It's the end
        this.log("Done");
    }
});

Function Descriptions

Init

Draw

Resize

Load

Event

event: function(type, position, user, data, date) {

}

type can be any one of the following:

  • pointerPress (button down) - data.button will indicate whether it is 'left' or 'right' button that caused the event

  • pointerRelease (button up) - data.button will indicate whether it is 'left' or 'right' button that caused the event

  • pointerMove

  • pointerDoubleClick (left button double click)

  • pointerScroll (moving the wheel) - data.wheelDelta will contain the amount scrolled. If looking at a page of text, positive value would make the text go up. While negative would bring the text down.

  • keyboard (printable key events from the keyboard) - data.code is the character code, data.character is the printable character's string

  • specialKey (key events for all keys on the keyboard including Backspace, Delete, Shift, etc...) - data.code is the character code, data.state is 'up' or 'down'

  • widgetEvent (button click, slider lock, slider drag, slider release, text input enter) - data.identifier is the id of the UI element that generated the event. data.action describes the kind of event that was generated. data.text contains text in case of text input enter event.

position has x and y to give the position of the event.

user has id, label, and color to describe the user who generated the event

date object may be used for synchronizing.

Quit

External Libraries

How to write an app

instructions.json

A new application can be created by running npm run newapp in the base directory of SAGE2.

All properties are optional and are set with default values if not specified.

Property name Type Values Notes
main_script string <path> The path to the file containing the application definition
resize string 'proportional', 'free' The resize mode of the window
width int The width of the window in pixels
height int The height of the window in pixels
animation boolean unknown
sticky boolean unknown
dependencies [string] <path>, <uri> A list of dependencies of the application
load object An object passed into the load function at startup
icon string <path> The file containing an application icon
title string
version string The current version of the application
description string A short description of the purpose of your application
author string
license string The name of the license of the application
keywords [string] A list of keywords associated with the application
filetypes [string] A list of filetypes that the application can open
directory string unknown

load into app library

Widgets

Three different types of widgets are available for custom applications. Namely:

  • Buttons

  • Sliders and

  • Text Input (Single line)

To add a widget element:

init: function(id, width, height, resrc, date) {
        ...
        this.controls.<addWidget>(paramObject);

        this.controls.finishedAddingControls();
}

Button

To add a button to the widget bar, the application may call addButton function on the controls. AddButton function takes one object with three properties, namely type, sequenceNo, and id. Type specifies the type of the button (appearance and animation of the button). sequenceNo specifies the position of the button around the radial dial of the UI. id is used to associate the button-press event to action code in the app.event handler.

The type property can take any value out of a list of predefined values such as - play-pause, play-stop, rewind, fastforward, prev, next, and so on. A custom defined Type can also be added to the list of Types available to any app by calling addButtonType function on the controls object. This function takes a string value for the name of the type being defined and a type object as it parameters. The example below illustrates the addition of a new type, and the way the id field of a button description is used to link the button to action code in the app.event handler.

var plusButton = {
    "state": 0,
    "from":"m 0 -6 l 0 12 m 6 -6 l -12 0", //svg paths
    "to":"m 6 0 l -12 0 m 6 6 l 0 -12",
    "width":12,
    "height":12,
    "fill":"none",
    "strokeWidth": 1,
    "delay": 600,
    "textual":false,
    "animation":true
};
this.controls.addButtonType("plus", plusButton);

this.controls.addButton({type:"plus", sequenceNo:1, id:"PlusButton"});
this.controls.addButton({type:"rewind", seqeunceNo:7, id:"RewindButton"});
this.controls.finishedAddingControls();

//In the event handler with in the custom app code:
event: function(eventType, position, userId, data, date) {
    if (eventType === "widgetEvent"){
        switch(data.identifier){
            case "PlusButton":
                // Code to be executed when plus button is clicked
                break;
            case "RewindButton":
                // Code to be executed when rewind button is clicked
                break;
            // Other controls follow
        }

    }
}

Slider

To add a slider to the widget bar, the application may call addSlider function on the controls. addSlider function takes an object with the following properties:

  • appObj: The instance of the application that is bound to this slider. Usually 'this' reference is set as the value of appObj.

  • property: A string representing the property of the application that is tied to the slider. (A change in the value of appObj.property will be reflected in the slider and vice versa)

  • begin: starting (minimum) value of the property associated with the slider.

  • end: last (maximum) value of the property associated with the slider.

  • increments: The step value change that occurs in the property when the slider is moved one unit. (alternatively, parts can be specified in place of increments, where parts represents the number of increments from begin to end)

  • id: String to associate slider events to action code in the app.event handler.

  • caption(optional): caption takes a small(length < 6) string and sets a visible label in front of the slider control using this string.

this.controls.addSlider({
                            begin:  ,
                            end:  ,
                            increments:  ,
                            appObj:  ,
                            property: ,
                            id:  "exampleSlider",
                        });

//In the event handler with in the custom app code:
event: function(eventType, position, userId, data, date) {
    if (eventType === "widgetEvent"){
        switch(data.identifier){
            case "exampleSlider":
                switch (data.action){
                    case "sliderLock":
                        // Code to be executed when slider is pressed upon by the user
                        break;
                    case "sliderUpdate":
                        // Code to be executed when slider is dragged by the user
                        break;
                    case "sliderRelease":
                        // Code to be executed when slider is released by the user
                        break;
                }
                break;

            // Other controls follow
        }

    }
}

Text Input

To add a text input to the widget bar, the application may call addTextInput function on the controls. addTextInput function takes an object with id as a mandatory property and two optional properties namely defualtText and caption. id is a String to associate action code in the app.event handler to the "Enter"(or "Return") key event, which marks the end of the input. defualtText can be set to any string value and the widget will be created by placing this string in the text area. caption takes a small(length < 6) string and sets a visible label in front of the text input control using this string.

this.controls.addTextInput({defaultText: "", caption:"Addr", id:"Address"});

//In the event handler with in the custom app code:
event: function(eventType, position, userId, data, date) {
    if (eventType === "widgetEvent"){
        switch(data.identifier){
            case "Address":
                // Code to be executed when Enter key is hit by the user
                break;

            // Other controls follow
        }
    }
}

Future: Inter-application communication

Example Application: Clock

var clock = SAGE2_App.extend( {
    init: function(data) {
        this.SAGE2Init("canvas", data);

        this.resizeEvents = "continuous";

        // application specific 'init'
        this.ctx = this.element.getContext("2d");
        this.minDim = Math.min(this.element.width, this.element.height);
        this.timer = 0.0;
        this.redraw = true;
        this.log("Clock created");
    },

    load: function(date) {

    },

    draw: function(date) {
        // application specific 'draw'
        // only redraw if more than 1 sec has passed
        this.timer = this.timer + this.dt;
        if(this.timer >= 1.0) {
            this.timer = 0.0;
            this.redraw = true;
        }

        if(this.redraw) {
            // clear canvas
            this.ctx.clearRect(0,0, this.element.width, this.element.height);

            this.ctx.fillStyle = "rgba(255, 255, 255, 1.0)"
            this.ctx.fillRect(0,0, this.element.width, this.element.height)

            var radius = 0.95 * this.minDim / 2;
            var centerX = this.element.width / 2;
            var centerY = this.element.height / 2;

            // outside of clock
            this.ctx.lineWidth = (3.0/100.0) * this.minDim;
            this.ctx.strokeStyle = "rgba(85, 100, 120, 1.0)";
            this.ctx.beginPath();
            this.ctx.arc(centerX, centerY, radius, 0, Math.PI*2);
            this.ctx.closePath();
            this.ctx.stroke();

            // tick marks
            var theta = 0;
            var distance = radius * 0.90; // 90% from the center
            var x = 0;
            var y = 0;

            // second dots
            this.ctx.lineWidth = (0.5/100.0) * this.minDim;
            this.ctx.strokeStyle = "rgba(20, 50, 120, 1.0)";

            for(var i=0; i<60; i++){
                // calculate theta
                theta = theta + (6 * Math.PI/180);
                // calculate x,y
                x = centerX + distance * Math.cos(theta);
                y = centerY + distance * Math.sin(theta);

                this.ctx.beginPath();
                this.ctx.arc(x, y, (1.0/100.0) * this.minDim, 0, Math.PI*2);
                this.ctx.closePath();
                this.ctx.stroke();
            }

            // hour dots
            this.ctx.lineWidth = (2.5/100.0) * this.minDim;
            this.ctx.strokeStyle = "rgba(20, 50, 120, 1.0)";

            for(var i=0; i<12; i++){
                // calculate theta
                theta = theta + (30 * Math.PI/180);
                // calculate x,y
                x = centerX + distance * Math.cos(theta);
                y = centerY + distance * Math.sin(theta);

                this.ctx.beginPath();
                this.ctx.arc(x, y, (1.0/100.0) * this.minDim, 0, Math.PI*2, true);
                this.ctx.closePath();
                this.ctx.stroke();
            }

            // second hand
            var handSize = radius * 0.80; // 80% of the radius
            var sec = date.getSeconds();

            theta = (6 * Math.PI / 180);
            x = centerX + handSize * Math.cos(sec*theta - Math.PI/2);
            y = centerY + handSize * Math.sin(sec*theta - Math.PI/2);

            this.ctx.lineWidth = (1.0/100.0) * this.minDim;
            this.ctx.strokeStyle = "rgba(70, 35, 50, 1.0)";
            this.ctx.lineCap = "round";

            this.ctx.beginPath();
            this.ctx.moveTo(x, y);
            this.ctx.lineTo(centerX, centerY);
            this.ctx.moveTo(x, y);
            this.ctx.closePath();
            this.ctx.stroke();

            // minute hand
            handSize = radius * 0.60; // 60% of the radius
            var min = date.getMinutes() + sec/60;

            theta = (6 * Math.PI / 180);
            x = centerX + handSize * Math.cos(min*theta - Math.PI/2);
            y = centerY + handSize * Math.sin(min*theta - Math.PI/2);

            this.ctx.lineWidth = (1.5/100.0) * this.minDim;
            this.ctx.strokeStyle = "rgba(70, 35, 50, 1.0)";
            this.ctx.lineCap = "round";

            this.ctx.beginPath();
            this.ctx.moveTo(x, y);
            this.ctx.lineTo(centerX, centerY);
            this.ctx.moveTo(x, y);
            this.ctx.closePath();
            this.ctx.stroke();

            // hour hand
            handSize = radius * 0.40; // 40% of the radius
            var hour = date.getHours() + min/60;

            theta = (30 * Math.PI / 180);
            x = centerX + handSize * Math.cos(hour * theta - Math.PI/2);
            y = centerY + handSize * Math.sin(hour * theta - Math.PI/2);

            this.ctx.lineWidth = (2.0/100.0) * this.minDim;
            this.ctx.strokeStyle = "rgba(70, 35, 50, 1.0)";
            this.ctx.lineCap = "round";

            this.ctx.beginPath();
            this.ctx.moveTo(x, y);
            this.ctx.lineTo(centerX, centerY);
            this.ctx.moveTo(x, y);
            this.ctx.closePath();
            this.ctx.stroke();

            this.redraw = false;
        }
    },

    resize: function(date) {
        this.minDim = Math.min(this.element.width, this.element.height);
        this.redraw = true;

        this.refresh(date);
    },

    event: function(type, position, user, data, date) {
        // this.refresh(date);
    },

    quit: function() {
        // done
    }
});

Updated