Remoto - VFS: Animated Clock
Remoto - VFS
Animated Clock

The Animated Clock plugin creates a custom pane in javascript that uses SVG animation to show the current system time. Because it wants to be displayed in an interface, it will need to use the Javascript Client and VFS_stdlib to load into a browser.

Additional files will be included as resources and served using the VFS_creator::code() mechanism.

The Javascript Client will need a user to authenticate with, so we will create a basic one that requires no password and has a minimal interface layout.

For this, we will need to:

To create a free standing example rather than just a compiled plugin, we will need to:

  • Include the node in an xml config file
  • Create a user
  • Create an index.html file with the connection settings

The Plugin Bundle

The following source files will be needed, arranged in this structure:

The clock.pro file includes the source files and the clock.qrc resource file, which bundles everything we will need for this plugin. It follows the same pattern laid out in The Skeleton Plugin.

The clockPlugin.h and clockPlugin.cpp files create the plugin, which in turn creates the clock node and allows it to serve code.

Attention
The complete clock plugin, including the brute force search algorithm, can be found in the 'server/src/plugins/clock/src/utils' folder. If you intend to build it, you should copy it to a work folder. Additional files will be needed to run the example, which are explained in the Freestanding Example section.

The Clock Node

The clock.h and clock.cpp files define the VFS_node. It provides an icon in case it appears in an application menu.

The read() function sets r->_success=true, and sets r->_data to the current time, which is unused. We need to subclass VFS_node::read() to suppress the default warning message and to provide some non-null data to prevent the pane manager from closing the pane immediately upon opening it.

The metadata() function defines r->_metadata["type"]="clock:clockPane", which means that when the node is read(), it will report itself as a "clock:clockPane", and therefore load the corresponding pane type from this plugin. The base implementation of VFS_node::metadata() will provide the VFS_node::icon() and set r->_success=true. The node also defines an "action", which tells the javascript listing() pane to "openLayout" with the provided clockLayout.json template when the button is clicked. The resulting pane is placed in the "nearest" tabs() pane that can receive new tabs.

The code() function is called by clock::code(). The code could be served directly from the plugin node itself instead of the VFS_node subclass, but it can be helpful to route code references directly to the VFS_node that use it rather than the plugin bundler itself. There are cases where a plugin may only bundle code and data without serving any nodes.

Note that the type name is "clock:clockPane", not "clock:clockPane.js", even though it is returning javascript. This is an important detail. If the library ends with .js, it will be minified before transfer in VFS_creator::code(). This will make the data much smaller, but also obfusticate it, making debugging much more difficult or impossible. Because we have not saved the type into the layout file, it is trivial to change the type in code() and metadata() later to include the .js for production use, where we may not want a user to be able to easily see the source code.

See also
VFS_creator::code()

The Clock Layout

The clocklayout.json template file gives the new tab a name and references the path of the node in the VFS.

The class "clock" is applied to the container <div> in the paneManager(), which will give the stylesheet a namespace to work with. Note that this is an optional design choice, as the clockPane can create its own wrapper <div>s to apply css to.

See also
pane::createHTML()

Note that "type" could be set to "clock:clockPane" here, which would bypass the need to set it in metadata(), however we would need to set "keepType:true" in the layout file, or any future layout change would cause it to revert back to type "pane". It it always better that a node positively identify its type, rather than force a type in the layout. However, many times we want to view the same data through different pane types in more complicated applications, which would require the layout to override the default.

See also
paneManager()

The Clock Stylesheet

The clock.css style sheet references the class "clock" found in the template. You will want to write your css using a namespace (in this case "clock") so that future name collisions don't happen.

Panes in the paneManager always provide an absolute positioning context, so any pane that wants to fill the given space can create a child <div> and use:

{
position: absolute;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
}

or use

{
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
}

or any other css absolute positioning method. This is especially useful if a pane wants to be subdivided.

See also
Themes For The Javascript Client

The Clock Pane

The clockPane.js javascript file will be dynamically loaded when the "clock:clockPane" type is returned from metadata().

The Javascript Libraries use requirejs as an AMD loader. The pane will require other libraries to function, and those are included as a array of requirements to the define() command. These other libraries will be loaded asynchronously, and may even depend on each other. The clock.css is also loaded this way. Notice that AMD modules will, for the most part, return something as the value of the module. The second argument to define() is a callback function that receives the value of each module. The exceptions for this are non-AMD things like .css files. Therefore, we place the css file last, and don't include a variable to record the value from the loader. The loader will detect the css extension and include it in a style block in the head of the html page.

Once all the dependencies have been satisfied, the callback function is executed. It needs to return the value of this module, which in this case will be the clockPane prototype used by the paneManager to instantiate the pane() subclass object, using the layout.

clockPane::createHTML() will then be called to create the gui elements.

clockPane::applySubscription() will then be called with any node data or metadata returned by calling the subclassed VFS_node::subscribe() method in the plugin's node. By default, applySubscription() will also call applyDiff(). In this example case this is not very interesting because the node has no metadata, data, or settings.

Now we have created the clockPane and applied any data from the layout, metadata, and node data retrieved from the VFS. The constructor has called startTimer(), which will begin the animation. Notice that we don't necessarily know that createHTML() has been called before the first timer event has fired, due to asynchronous network lag and potential load on the server. Therefore, the update function will only reference variables that are known to exist (nameley members of the _digits object). This requirement could be avoided by uncommenting the check at the head of the update function, however it is left commented to prove that the function is safe in the asynchronous loading context.

Many times you will want to include a pane::resize() function. In this case it just re-centers the clock widget using css.

The animation math here is left as an exercise for the reader, however the overarching concept is that a digit path is updated for each animation frame. The shape of that path is based on the vertical offset of the digit, which wants to wrap around the upper and lower pullies but also around the corners of the LCD digit. Then, a stroke-dashoffset css paramater is used to rotate the "string" to the correct place for the current digit. The rotations and offsets have been pre-calculated using a brute force algorithm. The brute force source code is in the utils folder of the clock plugin included with the server source code. It takes about an hour to run on a mac laptop... there may be a more efficient solution, and if you're up for it, take a look!

See also
paneManager()
paneLoader()
pane()

Running the Clock as a Free Standing Example

Attention
You will need a local webserver installed to run this example. This can be apache, nginx, node.js, or any other http server you like. It will need to be configured to support softlinked paths.

First, create a directory to work in and set some environment variables, then compile the plugin. For this example we will use ~/clockExample, and you will need to provide the actual path for VFS_DIR and VFS_PLUGINS_DIR.

cd ~
mkdir clockExample
cd clockExample
mkdir libs
export VFS_DIR=[path/to/vfs_server/server]
export VFS_PLUGINS_DIR=$PWD/libs
cp -rv $VFS_DIR/src/plugins/clock ./clock
cd clock
qmake clock.pro && make -j
cd ../

You should now be able to confirm that the clock dynamic library has been built in the libs folder. Depending on operating system, this will be called libclock.so or libclock.dylib.

Create an xml config file

We will include the clock as an application, which means it will appear as a button in a user's interface, and he or she can click on it to open the clock in a new tab. The tab can be closed and reopened.

Create the clock.xml file in your work directory:

<vfs logLevel='9'>
<env>
<portstart type='int' default='3560' />
</env>
<hd name='users' path='users/' />
<node name='services'>
<nopasswd name='nopasswd' nopasswordfile='nopasswd' />
<sessions name='sessions' />
<remotoserver name='remotoserver' port='@portstart@+0' auth='/services/nopasswd' sessions='services/sessions' />
<httpd_browser name='browser' port='@portstart@+1' path='/' />
</node>
<node name='applications'>
<clock name='clock' />
</node>
</vfs>

Notice that a portstart variable is set, and ports are counted based on this starting point. Any available port can be chosen. You don't have to use the @-syntax environment variable; it could just be the port number. Refer to XML Config File(s) for more information. We're using the @portstart@ variable here so the VFS_httpd_browser node can increment from it.

Create the user

The VFS_nopasswd authentication node needs the username listed in the nopasswd file.

Our user will need an entry:

echo "clockuser" >> nopasswd

The user will need a layout file and a preferences file:

mkdir -p users/clockuser
cd users/clockuser
echo "{}" > preferences.rfm
mkdir layouts
touch layouts/current.rui
cd ../../

Edit the empty users/clockuser/layouts/current.rui file, and paste this into it:

{
"type": "splitter",
"name": "Application Main",
"sizes": [ "64px", null ],
"stacking": "horizontal",
"fixedSize": true,
"children": [
{
"name": "Applications",
"path": "applications",
"type": "pane"
},
{
"children": [],
"name": "Tabswitcher",
"selected": 0,
"type": "tabswitcher"
}
]
}

For this example, the preferences file will be an empty json object and the layout will have our applications menu and a tabswitcher to place the clock. When the user clicks on the clock icon, it will open in a new tab.

Create an index.html file

If you haven't, you will need to clone The Javascript Client repository. We will need to create a client directory and an index.html file that will be served by a webserver and will tell the bootstrap.js and vfsClient what port and path to connect to. You will need to provide the actual path to the client repository.

export VFS_CLIENT_DIR=[path/to/vfs_client_javascript]
mkdir client
cd client
ln -s $VFS_CLIENT_DIR/images images
ln -s $VFS_CLIENT_DIR/js js
ln -s $VFS_CLIENT_DIR/style style
touch index.html
cd ..

Edit the client/index.html file and paste the following:

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:svg="http://www.w3.org/2000/svg">
<head>
<link rel="stylesheet" href="style/style.css" />
<script type='text/javascript'>
window.VFS_config = {
"ws": {
"applicationName": "Remoto Clock",
"host": "localhost",
"path": "",
"port": "3560",
"version": "v0.8"
}
};
</script>
<script data-main="js/main" src="js/require/require.js"></script>
</head>
<body>
</body>
</html>

If you are running this in a local development environment like a laptop or workstation, the host will be 'localhost', the port will be the one you chose, and the path will be an empty string. More advanced configurations are discussed in Nginx Proxy Configuration.

Run the VFS with this configuration

You can now point the VFS executable to the files that have been created:

$VFS_DIR/VFS ./clock.xml -plugins $VFS_PLUGINS_DIR

You should see the VFS build itself and log what ports have been opened.

Open it in a browser:

http://localhost/[path/to/index.html]

You will need to login with the user we created: clockuser. The password field will be left empty.

As a shortcut for direct linking:

http://localhost/[path/to/index.html]?username=clockuser

Clicking on the clock icon will open it in a new tab. By default only one tab per application can be opened. If you close the tab, it can be reopened. Changes to the layout are recorded. Reloading the page will retain the current state for the user. If multiple browser windows with the same user are simultaneously logged in, they will synchronize.

Also notice that we've opened a port for the VFS_httpd_browser, which provides some diagnostic information about the running VFS. This tool is for development use and can be very helpful when debugging connections and viewing the current state of things. It can be accessed here:

http://localhost:3561

You may need to change the port to the one you're using, as it is incremented from the @portstart@ variable. The VFS_httpd_browser shows the VFS_node::report() information of each node in the VFS. Click on the "expand reports" button to open the tree. When a user (clockuser) is logged in, you will see it as a child of the VFS_remotoserver node. When we start to use multithreading, the thread addresses can be useful to check. Subscriptions are also listed here. Notice that the clockuser is subscribed to both the clock node and the applications directory. If a new application is added by another user, it will appear here in realtime. Once the 'clock' button has been clicked, check the contents of the layout file to see what has changed.

Summary

We've visited a few things here:

  • how to define a plugin that hosts a single node, but could be many.
  • how to define a node that views a custom javascript pane.
  • how to define a fairly complex javascript pane for the node that uses SVG animation.
  • how to include the node in a config file, create a user, and run the VFS executable with this config file.
  • how to view some diagnostic information using the VFS_httpd_browser node.

The Screen Controller Example explains how to connect to real world devices, synchronize interfaces for multiple users, and more about subclasses.


Source Files

clock.pro

#clock.pro
# Read VFS_DIR from the environment
VFS_DIR=$$(VFS_DIR)
isEmpty(VFS_DIR) {
error(VFS_DIR must be defined to compile a plugin)
}
# The name of the resulting dynamic lbrary
TARGET = $$qtLibraryTarget(clock)
include($$VFS_DIR/VFS-plugin.pri)
#QT += network
SOURCES += \
src/clockPlugin.cpp \
src/clock.cpp
HEADERS += \
src/clockPlugin.h \
src/clock.h
RESOURCES += src/clock.qrc

src/clockPlugin.h

#ifndef CLOCK_PLUGIN_H
#define CLOCK_PLUGIN_H
class clockPlugin : public QObject, public VFS_node_interface
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "com.remoto.plugins.clock")
Q_INTERFACES(VFS_node_interface)
public:
virtual bool initialize();
virtual QString description(QString type);
virtual QString arguments(QString type);
virtual QStringList provides();
virtual VFS_node *create(QString type, QVariantMap env, QDomElement child);
virtual QString code(QString nodename, QString libname, QString &error);
};
#endif // CLOCK_PLUGIN_H
The interface class for dynamically loaded plugins.
virtual QString description(QString type)
Descriptive text about an individual node.
virtual bool initialize()
The plugin initializer.
virtual QString arguments(QString type)
Describe the arguments needed to create a node.
virtual QString code(QString nodename, QString libname, QString &error)
Request code or other resource from a plugin bundle.
virtual QStringList provides()
A string list of node names that this plugin is prepared to create.
virtual VFS_node * create(QString type, QVariantMap env, QDomElement child)
Create a new node instance.
VFS_node is the base class from which all other VFS_node classes derive.
Definition: VFS_node.h:143

src/clockPlugin.cpp

#include "VFS.h"
#include "VFS_acl.h"
#include "clockPlugin.h"
#include "clock.h"
bool clockPlugin::initialize()
{
return true;
}
QString clockPlugin::description(QString type)
{
if (type == "clock")
return "A an animated clock using javascript and SVG";
}
QString clockPlugin::arguments(QString type)
{
if (type == "clock")
return argumentString( rclock::staticMetaObject );
}
QStringList clockPlugin::provides()
{
QStringList p;
p << "clock"; //the node name, as written in the config file
return p;
}
VFS_node *clockPlugin::create(QString type, QVariantMap env, QDomElement child)
{
Q_UNUSED(env);
Q_UNUSED(child);
if (type=="clock")
return new rclock();
return 0;
}
QString clockPlugin::code(QString nodename, QString libname, QString &error)
{
if (nodename == "clock")
return rclock::code(nodename,libname,error);
return VFS_node_interface::code(nodename,libname,error);
}
setter type
a setter DOCME
setter error
Set the error value of this widget.

src/clock.qrc

<RCC>
<qresource prefix="/clock">
<file>html/js/clockPane.js</file>
<file>templates/clockLayout.json</file>
<file>html/style/clock.css</file>
</qresource>
</RCC>

src/clock.h

#ifndef CLOCK_H
#define CLOCK_H
#include "VFS_node.h"
class rclock : public VFS_node
{
Q_OBJECT
public:
Q_INVOKABLE explicit rclock();
virtual ~rclock();
virtual bool isContainer();
static QString code(QString nodename, QString libname, QString &error);
protected:
virtual QByteArray icon();
private:
// virtual void ls(VFS_request *r);
virtual void read(VFS_request *r);
// virtual void write(VFS_request *r);
virtual void metadata(VFS_request *r);
// virtual void report(VFS_request *r);
// virtual void submit(VFS_request *r);
// virtual void rm(VFS_request *r);
virtual void aclDefaults(VFS_request *r);
};
#endif // CLOCK_H
virtual void read(VFS_request *r)
Return the data contents of this node, or if it's a container call ls()
Definition: VFS_node.cpp:724
virtual void metadata(VFS_request *r)
Fetch the metadata of this node.
Definition: VFS_node.cpp:797
virtual QByteArray icon()
Fetch the icon for a node.
Definition: VFS_node.cpp:655
static QString code(QString nodename, QString libname, QString &error)
Fetch code or any other resource from a node.
Definition: VFS_node.cpp:1038
virtual bool isContainer()
A VFS_node may have children.
Definition: VFS_node.cpp:2025
virtual void aclDefaults(VFS_request *r)
Return default values and features associated wth this node.
Definition: VFS_node.cpp:918
The base class for all requests between nodes.
Definition: VFS_node.h:54

src/clock.cpp

#include "VFS.h"
#include "VFS_icons.h"
#include "VFS_acl.h"
#include "clock.h"
rclock::rclock()
{
//VFS::LOG( QString("Created rclock instance") );
}
rclock::~rclock()
{
//VFS::LOG( "rclock destructor" );
}
QByteArray rclock::icon()
{
//return VFS_icons::get("clock");
return (char *) "data:image/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 32 32\"><path fill=\"white\" d=\"M1.83 10.67h28.33v12.21H1.83z\"/><path d=\"M29.67 11.17v11.21H2.33V11.17h27.34m1-1H1.33v13.21h29.34V10.17z\"/><path d=\"M4.13 16.27v-2.65l1 1V16l-.6.6zm1 1.31V19l-1 .95v-2.61l.36-.34zm2.1-1.25l.48.47-.48.48h-2l-.48-.48.48-.47zm.06-.3v-1.45l.95-1v2.65l-.35.36zm.95 1.31V20l-.99-1v-1.42l.6-.59zM9.8 16.27v-2.65l.95 1V16l-.6.6zm.95 1.31V19l-.95 1v-2.66l.35-.35zm2.11-1.25l.48.47-.48.48h-2l-.48-.48.48-.47zm.05-.3v-1.45l1-1v2.65l-.36.36zm1 1.31V20l-1-.95v-1.47l.6-.59zM15.57 15.63v-.94h1v.94zm0 4.36V19h1v1zM18.22 16.27V13.8l1 .93V16l-.6.6zm1 1.31V19l-1 .95v-2.61l.36-.35zm2.94-4l-1 1h-1.8l-.95-1zm-2.31 1.64l.49-.47.47.47v1.87l-.47.48-.49-.48zm1.52.77v-1.24l1-.93v2.45l-.36.36zm1 1.31V20l-1-.95v-1.47l.6-.59zM23.89 16.27V13.8l.95.93V16l-.59.6zm.95 1.31V19l-.95.95v-2.61l.36-.35zm2.95-4l-1 1H25l-1-1zm-2.31 1.64l.48-.47.47.47v1.87l-.47.48-.48-.48zM27 16v-1.25l1-.93v2.45l-.36.36zm1 1.31V20l-1-1v-1.42l.6-.59z\"/></svg>";
}
bool rclock::isContainer()
{
return false;
}
void rclock::read(VFS_request *r)
{
QJsonObject o;
bool blink = VFS_acl::checkAllowAccess(r,"blink");
o["now"] = QDateTime::currentMSecsSinceEpoch();
o["blink"] = blink;
r->_data.setObject(o);
r->_success = true;
}
void rclock::metadata(VFS_request *r)
{
r->_metadata["type"] = "clock:clockPane";
if (r->_success)
{
QJsonObject a;
a["type"] = "openLayout";
r->_metadata["action"] = a;
QJsonObject l = rutils::jsonResource(":/clock/templates/clockLayout.json");
l["icon"] = "@icon@";
QJsonObject t;
t["openLayout"] = l;
r->_metadata["template"] = t;
r->_metadata["where"] = "nearest";
}
}
QString rclock::code(QString nodename, QString libname, QString &error)
{
if (libname == "clockPane")
return rutils::resourceContents(":/clock/html/js/clockPane.js");
if (libname == "clock.css")
return rutils::resourceContents(":/clock/html/style/clock.css");
return VFS_node::code(nodename,libname,error);
}
void rclock::aclDefaults(VFS_request *r)
{
QJsonObject acl;
addACLDefault( acl, true ); // allow by default
//addACLGroup( acl, "developer", true ); // add developer group to default to true
//addACLGroup( acl, "admin", true ); // add admin group to default to true
//addACLUser( acl, "test3", false );
//addACLUser( acl, "root", true ); // and add root as true
addACLFeature( acl, "blink", true, "Blink the dots between digit groups." ); // add feature "blink", default to true
//addACLFeatureGroup( acl, "blink", "developer", true );
//addACLFeatureGroup( acl, "blink", "admin", false );
//addACLFeatureUser( acl, "blink", "test2", false );
//addACLFeature( acl, "fade", true );
r->_data.setObject(acl);
r->_success = true;
}
static bool checkAllowAccess(VFS_session *s, QString path, QString feature="")
Check if a session has access to a resource.
Definition: VFS_acl.cpp:407
bool _success
if the request was successfully completed
Definition: VFS_node.h:107
QJsonDocument _data
the request payload
Definition: VFS_node.h:102
QJsonObject _metadata
the request payload
Definition: VFS_node.h:101
QJsonObject jsonResource(QString resource, bool *ok=nullptr)
Fetch the contents of a Qt resource as a QJsonObject.
Definition: rutils.cpp:90
QByteArray resourceContents(QString resource, bool *ok=nullptr, bool squashHash=false)
Fetch the contents of a Qt resource file.
Definition: rutils.cpp:53

src/templates/clockLayout.json

{
"name": "Clock",
"type": "pane",
"path": "@sourcepath@/@sourcefile@",
"class": "clock"
}

src/html/style/clock.css

@charset "utf-8";
.clock
{
background-color: var(--BG0);
}
.clock .clockCanvas
{
/*
border: var(--Border1);
background: var(--BG3);
box-sizing:content-box;
*/
padding:0px;
}
.clock .clockCanvas:focus
{
/*
border: var(--InputBorderFocus);
*/
}
.clock .clockCanvasSVG
{
position:absolute;
left:0px;
top:0px;
width:100%;
height:100%;
}

src/html/js/clockPane.js

define( [
'remoto!stdlib:js/panes/pane.js',
'remoto!stdlib:js/include/utils.js',
'remoto!clock:clock.css'
],
function(pane,utils)
{
'use strict';
/*
based on this shape:
/---\
| • |
| |
...
| | 0 G
\ • / | 1 |
X +---+ +---+
/ \ | F | | |
| • | E| |2 | |
\ / | 3 | | |
X +---+ = +---+ = 8 = 7 cell LCD
/ \ | D | | |
| • | 4| |C | |
\ / | B | | |
X +---+ +---+
/ • \ | 5 |
| | A 6
...
| |
| • |
\---/
SOLUTION: 34 slots, 8761410992 iterations
1 1 2 2 3
0 5 0 5 0 5 0
=======================================================================
1 0 0 0 0 0 1 0 1 0 0 0 1 1 1 0 0 0 1 0 0 0 0 1 0 1 1 0 1 1 0 0 0 0
0) 0 0 1 0 1 0 0 0 1 1 0 1 1 0
1) 1 0 0 0 0 0 0 1 0 0 0 0 0 0
2) 0 0 0 1 0 1 0 0 0 1 0 1 1 0
3) 0 1 1 1 0 0 0 0 1 1 0 0 0 0
4) 1 0 0 0 0 0 0 1 1 1 0 0 0 0
5) 0 0 0 1 0 1 0 0 0 1 0 1 1 0
6) 0 0 0 1 1 1 0 0 0 1 0 1 1 0
7) 0 0 1 0 0 0 0 0 1 1 0 0 0 0
8) 0 0 1 1 1 0 0 0 1 1 0 1 1 0
9) 0 0 1 1 1 0 0 0 1 1 0 0 0 0
*/
var sequence = [ 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0 ];
var sequenceLength = sequence.length;
//Some digits can be made multiple ways. I picked the ones that I liked best.
var digits = {
"0": [4,-1.5], //== == 24,1.5 4,-1.5^
"1": [16,.5], //==
"2": [5,-1], //== == 3,-2 5,-1^
"3": [11,0.5], //== ==
"4": [10,-2.5], //==
"5": [21,.5], //== == 21,.5 24,2 24,1^
"6": [21,-2.5], //== == 21,-2.5^ 24,-1
"7": [16,3.0], //==
"8": [10,1.5], //== == 24,-1.5 10,1.5^
"9": [10,0] //== == 24,-2 10,0^
};
var scale = 40;
var margin = scale * .4;
var animationTime = 800;
var svgns = "http://www.w3.org/2000/svg";
var xlinkns = "http://www.w3.org/1999/xlink";
function spad(s)
{
s = "00"+s;
s = s.substr(s.length-2,2);
return s;
}
// //////////////
// Clock Pane //
// //////////////
clockPane.prototype = new pane;
function clockPane(layout)
{
pane.call(this,layout);
this._type = "clock:clockPane";
this._svg = null;
this._dots = [];
this._root = null;
this._digits = {
"S0": ( new digit( 1.5*scale+2.5*margin, -.25*scale) ),
"S1": ( new digit( 2.5*scale+3.5*margin, -.25*scale) ),
"M0": ( new digit(-scale/2-margin/2, -.25*scale) ),
"M1": ( new digit( scale/2+margin/2, -.25*scale) ),
"H0": ( new digit(-2.5*scale-3.5*margin, -.25*scale) ),
"H1": ( new digit(-1.5*scale-2.5*margin, -.25*scale) ),
};
this._timerID = null;
this._blink = false;
this.startTimer();
}
clockPane.prototype.createHTML = function()
{
if (this._html) return this._html;
pane.prototype.createHTML.call(this);
var h = this._content;
h.addClass('clockCanvas');
h.attr("tabindex","-1"); //for keybaord to work, if the library is included
var s = this._svg = $( document.createElementNS(svgns,"svg") ).appendTo(h).attr("class","clockCanvasSVG");
s.attr( { "version":"1.1", "xmlns":svgns, "xmlns:xlink":xlinkns, "shape-rendering":"auto", } );
var r = this._root = $( document.createElementNS(svgns,"g") ).appendTo(s);
//dots separating hours:minutes:seconds
var c0 = $( document.createElementNS(svgns,"circle") ).appendTo(r);
c0.attr( { "cx": scale+1.5*margin, "cy": scale/2 - .25*scale, "r": scale/8, "fill": "var(--FG0)" } );
var c1 = $( document.createElementNS(svgns,"circle") ).appendTo(r);
c1.attr( { "cx": scale+1.5*margin, "cy": -scale/2 - .25*scale, "r": scale/8, "fill": "var(--FG0)" } );
var c2 = $( document.createElementNS(svgns,"circle") ).appendTo(r);
c2.attr( { "cx": -scale-1.5*margin, "cy": scale/2 - .25*scale, "r": scale/8, "fill": "var(--FG0)" } );
var c3 = $( document.createElementNS(svgns,"circle") ).appendTo(r);
c3.attr( { "cx": -scale-1.5*margin, "cy": -scale/2 - .25*scale, "r": scale/8, "fill": "var(--FG0)" } );
this._dots.push(c0,c1,c2,c3);
//build the digit graphics
for (var d in this._digits)
r.append( this._digits[d].createSVG() );
//for keyboard commands...
h.bind("mouseenter mousemove", function() { h.focus(); } );
return this._html;
}
clockPane.prototype.applySettings = function(diff)
{
//console.log("clock:");
//console.log(diff);
}
clockPane.prototype.applyDiff = function(diff,user)
{
//console.log("clock:",diff);
if ("blink" in diff)
this._blink = diff["blink"];
}
clockPane.prototype.resize = function()
{
var x = this._html.outerWidth() / 2;
var y = this._html.outerHeight() / 2;
var t = "translate("+x+","+y+")";
this._root.attr( { "transform":t } );
}
clockPane.prototype.startTimer = function()
{
if (this._timerID) return;
//calculate when the next second will start, minus the animationTime, and actually start then
var d = new Date();
var w = 2000 - d.getMilliseconds() - animationTime/2;
while (w > 1000) w -= 1000;
//update now
this.update();
//start the timer when the second begins
var THIS = this;
setTimeout( function() {
THIS._timerID = setInterval( THIS.update.bind(THIS), 1000 );
}, w );
}
clockPane.prototype.update = function()
{
//if (!this._html) return;
var d = new Date();
d.setTime( d.getTime() + 1000 ); //animate to the time it will be when the animation completes
var s = spad(d.getSeconds());
var m = spad(d.getMinutes());
var h = spad(d.getHours());
this._digits.S0.value = s[0];
this._digits.S1.value = s[1];
this._digits.M0.value = m[0];
this._digits.M1.value = m[1];
this._digits.H0.value = h[0];
this._digits.H1.value = h[1];
if (this._blink)
this._dots.forEach(function(e){
e.toggle();
});
}
// ////////
// Digit //
// ////////
function digit(x,y)
{
this._x = x;
this._y = y;
this._value = 0;
this._rotation = 0;
this._vertical = 0;
this._root = null;
this._pulleys = null;
this._pulleyArray = [];
this._upperPulley = null;
this._lowerPulley = null;
this._path = null;
this._centerOffset = {
x: -.5,
y: 2
};
this.createSVG();
this.value = 0;
}
digit.prototype = {
createSVG: function()
{
if (this._root) return this._root;
//the chunk of extra line needed to get a wrap around a unit circle to have a length of 2
var chunk = (2 - Math.PI/2) / 2;
var r = this._root = $( document.createElementNS(svgns,"g") );
r.attr( {
"transform": "translate("+this._x+","+this._y+") scale("+scale+")",
} );
//create the background shapes
$( document.createElementNS(svgns,"path") ).appendTo(r).attr( {
d: "M 0 "+(-8.5-chunk)+" L 0 "+(9+chunk),
stroke: "rgba(255,255,255,.15)",
"stroke-width": 1.3,
"stroke-linecap": "round"
} );
$( document.createElementNS(svgns,"path") ).appendTo(r).attr( {
d: "M 0 "+(-8.5-chunk)+" L 0 "+(-3-chunk),
stroke: "rgba(0,0,0,.25)",
"stroke-width": .3,
"stroke-linecap": "round"
} );
$( document.createElementNS(svgns,"path") ).appendTo(r).attr( {
d: "M 0 "+(3.5+chunk)+" L 0 "+(9+chunk),
stroke: "rgba(0,0,0,.25)",
//stroke: "var(--FG0)",
"stroke-width": .3,
"stroke-linecap": "round"
} );
//create the pulleys
var y = this._pulleys = $( document.createElementNS(svgns,"g") ).appendTo(r);
var up = this._upperPulley = $( document.createElementNS(svgns,"g") ).attr({ transform:"translate(.5,"+(-8-chunk)+") rotate(0)" }).appendTo(y);
up.transform = { x:.5, y:(-8-chunk), r:0 };
var lp = this._lowerPulley = $( document.createElementNS(svgns,"g") ).attr({ transform:"translate(.5,"+( 4+chunk)+") rotate(0)" }).appendTo(y)
lp.transform = { x:.5, y:( 4+chunk), r:0 };
var ps = this._pulleyArray = [ up,lp ];
//add the detailed pulley path
for (var i=0;i<ps.length;i++)
{ var p2 = $(document.createElementNS(svgns,"path")).appendTo(ps[i]);
p2.attr( {
d: "M21.6 0C9.648 0 0 9.648 0 21.6s9.648 21.6 21.6 21.6 21.6-9.647 21.6-21.6S33.552 0 21.6 0zm0 41.808c-11.184 0-20.208-9.072-20.208-20.208S10.464 1.392 21.6 1.392c11.137 0 20.208 9.072 20.208 20.208S32.737 41.808 21.6 41.808zm0-39.552c-10.704 0-19.344 8.688-19.344 19.344 0 10.704 8.688 19.344 19.344 19.344 10.704 0 19.344-8.688 19.344-19.344 0-10.704-8.64-19.344-19.344-19.344zm1.584 10.608l.048-5.088s0-.672.384-1.152c.24-.288.48-1.2.625-1.968.144-.72.239-1.392.239-1.392l.433 1.488c5.088.96 9.359 4.176 11.76 8.544l1.535-.048s-.575.288-1.248.672c-.719.384-1.487.864-1.68 1.2-.336.528-.959.72-.959.72l-4.801 1.632-1.393.48c-2.399.384-4.607-1.248-4.991-3.648v-1.44h.048zm-4.272 11.472c.576 0 1.056.479 1.056 1.056s-.48 1.056-1.056 1.056-1.056-.479-1.056-1.056.48-1.056 1.056-1.056zm-1.68-3.12c-.576 0-1.056-.48-1.056-1.056 0-.576.48-1.056 1.056-1.056.576 0 1.056.48 1.056 1.056 0 .576-.48 1.056-1.056 1.056zm7.152 3.072c.575 0 1.056.48 1.056 1.057s-.48 1.056-1.056 1.056c-.576 0-1.056-.479-1.056-1.056s.48-1.057 1.056-1.057zm.624-4.176c0-.576.48-1.056 1.056-1.056.576 0 1.056.48 1.056 1.056 0 .576-.479 1.056-1.056 1.056-.575 0-1.056-.48-1.056-1.056zm-3.36-4.272c.576 0 1.056.479 1.056 1.056s-.479 1.056-1.056 1.056c-.576 0-1.056-.48-1.056-1.056s.48-1.056 1.056-1.056zM6.576 13.2c2.448-4.368 6.72-7.584 11.76-8.544l.432-1.488s.096.624.24 1.392c.144.768.384 1.68.624 1.968.384.48.384 1.152.384 1.152l.048 5.088v1.488c-.384 2.4-2.592 3.984-4.992 3.648l-1.392-.48-4.8-1.632s-.624-.192-.96-.72c-.192-.336-1.008-.816-1.68-1.2-.624-.384-1.248-.672-1.248-.672l1.536.048.048-.048zm4.608 19.343c-.384.097-1.056.673-1.68 1.248a32.57 32.57 0 0 0-1.008.961l.528-1.44c-2.88-3.071-4.608-7.2-4.608-11.76 0-.72.048-1.392.144-2.112l-1.296-.864s.624.096 1.392.192c.816.096 1.728.144 2.064 0 .576-.24 1.2 0 1.2 0l4.848 1.488 1.392.432c2.16 1.104 3.024 3.744 1.92 5.855l-.864 1.152-3.024 4.08s-.384.528-1.008.721v.047zm17.76 4.657c-2.207 1.008-4.656 1.633-7.296 1.633s-5.088-.576-7.296-1.633l-1.2.961.624-1.248c.336-.721.672-1.584.672-1.969-.048-.624.384-1.152.384-1.152l2.928-4.127.864-1.2c1.68-1.681 4.464-1.681 6.192 0l.864 1.2 2.928 4.176s.385.527.385 1.152c0 .383.336 1.248.672 1.967.287.673.623 1.248.623 1.248l-1.199-.959-.145-.049zm5.856-2.448s-.433-.48-1.009-.961c-.575-.527-1.295-1.104-1.68-1.248-.576-.144-1.008-.719-1.008-.719l-3.023-4.08-.864-1.201c-1.104-2.159-.24-4.751 1.92-5.855l1.392-.432 4.848-1.488s.625-.192 1.201 0c.383.144 1.295.096 2.063 0 .72-.096 1.392-.192 1.392-.192l-1.295.864a15.1 15.1 0 0 1 .143 2.112c0 4.56-1.775 8.688-4.607 11.76l.527 1.44zM21.648 23.375c-1.104 0-1.968-.863-1.968-1.967s.864-1.968 1.968-1.968 1.968.864 1.968 1.968-.864 1.967-1.968 1.967z",
transform: "scale(0.021) translate(-21.5,-21.5)",
//fill: "#333333",
fill: "var(--FG0)",
} );
}
var p = this._path = $(document.createElementNS(svgns,"path")).appendTo(r);
p.attr( {
//stroke: "black",
stroke: "var(--FG0)",
fill: "none",
"stroke-width": "5",
"stroke-linecap": "round",
"stroke-linejoin": "round",
"vector-effect": "non-scaling-stroke",
"stroke-dasharray": this.makeDashArray(),
"stroke-dashoffset": 0,
d: this.makePath(),
"transform": "translate("+this._centerOffset.x+","+this._centerOffset.y+")",
} );
return this._root;
},
get value() { return this._value; },
set value(d) {
var v = digits[d];
var rot = v[0];
var vert = v[1];
//sometimes we want to go backwards
if ( rot - this._rotation > sequenceLength/2 )
rot -= sequenceLength;
if ( this._rotation - rot > sequenceLength/2 )
rot += sequenceLength;
//console.log(rot+" "+vert);
var THIS = this;
$(this).stop().animate(
{
rotation: [rot,"easeOutExpo"],
vertical: [vert,"easeOutBounce"],
},
animationTime,
function()
{
THIS.rotation = (THIS.rotation + sequenceLength) % sequenceLength;
}
);
},
get rotation() { return this._rotation; },
set rotation(r) {
//console.log("ROT: "+r);
this._rotation = r;
this._path.attr( {
"stroke-dashoffset": r*scale,
} );
var p = (this._rotation * 2) - this.vertical*2;
this.pullyRotation = p;
},
get vertical() { return this._vertical; },
set vertical(v) {
this._vertical = utils.bound(-3.5,v,3.5);
this._path.attr( {
d: this.makePath()
} );
this._pulleys.attr( {
"transform": "translate("+this._centerOffset.x+","+(this._vertical+this._centerOffset.y)+")",
} );
var p = (this._rotation * 2) - this.vertical*2;
this.pullyRotation = p;
},
get pullyRotation() { return this._rotation + this._vertical*2; },
set pullyRotation(r) {
var d = r * (180/Math.PI);
var pa = this._pulleyArray;
for (var i=0;i<pa.length;i++)
{
var p = pa[i];
var t = p.transform;
t.r = r;
p.attr( {
"transform": "translate("+t.x+","+t.y+") rotate("+(d*(i>0?-1:1))+")"
} );
}
},
makePath: function()
{
//starting at upper left corner of digit, at the margin
var L,B,R,T;
var bs;
var ts;
//the chunk of extra line needed to get a wrap around a unit circle to have a length of 2
var chunk = (2 - Math.PI/2) / 2;
var v = this._vertical;
L = [ "M 0 0 ", //left half start
"L 0 -1 ",
"L 1 -1 ",
"L 1 -2 ",
"L 0 -2 ",
"L 0 -3 ",
"L 1 -3 ",
"L 1 -4 ", //left half end
];
bs = -4 + v + -4 - chunk;
B = [ "L 1 " + bs, //bottom loop start
"A .5 .5 0 0 0 0 "+bs,
"L 0 -4 ", //bottom loop end
];
R = [ "L 0 -3 ", //right half start
"L 1 -3 ",
"L 1 -2 ",
"L 0 -2 ",
"L 0 -1 ",
"L 1 -1 ",
"L 1 0 ", //right half end
];
ts = 0 + v + 4 + chunk
T = [ "L 1 " + ts, //top loop start
"A .5 .5 0 0 1 0 "+ts,
"L 0 0 ", //top loop end
];
return L.join('') + B.join('') + R.join('') + T.join('');
},
makeDashArray: function()
{
var c = sequence[0];
var m = 0;
var d = "";
for (var i=0; i<sequence.length; i++)
{
while (sequence[i] === c && i<sequence.length)
{ m++;
i++;
}
d += m*scale+" ";
c = sequence[i];
m = 1;
}
//console.log(d);
return d;
}
};
return clockPane;
}
);
setter value
a setter DOCME
setter user
a setter DOCME
setter scale
Set a scale value for rendering.
transform(x, y, z)
getter x
a getter DOCME
getter y
a getter DOCME
Create a pane which will be mounted into a paneManager layout.
createHTML()
pane(layout, object)
update(dirty)
Process the _queue and apply the settings.
Utility functions for javascript clients.
bound(min, n, max)
Return a number where n is bounded by [min,max].