воскресенье, 22 января 2012 г.

Разбираемся в GWT JavaScript Native Interface (JSNI)

     Когда вы пишете код на Java (особенно, если это касается системного программирования), иногда вам нужно выполнить код вне JVM. Например, вам нужно получить доступ к библиотеке написанной на другом языке. Для этого в Java нужно объявить native метод и обеспечить реализацию этого метода на другом языке(например С). Этот механизм наз. Java Native Interface (JNI). Вы можете сделать тоже самое с GWT джава кодом на клиенте, только вместо кода на C, нативным языком для браузера будет JavaScript. Вот так Google придумала название JavaScript Native Interface. Иногда это очень полезно, смешать написанный вручную JavaScript в Java коде. Например, функциональность самого низкого уровня некоторых основных классов GWT написаны на JavaScript. 
  Написание и использование JSNI методов является мощной техникой, но следует использовать с осторожностью. JSNI код является менее кроссбраузерным, большая вероятность утечки памяти, меньше поддается Java-инструментам и является непростой задачей для оптимизации компилятором. 
     JSNI предоставляет такие возможности как: 
- выполнение Java методов непостредственно в JavaScript
- оборачивание безопасных Java методов вокруг существующих JavaScript
- вызов JavaScript в Java коде и наоборот
- чтение и запись Java полей из JavaScript
- использование debug режима как для Java так и для JavaScript(скриптовый отладчик)
  JSNI методы объявляются как native и содержат JavaScript код в спец. формате блока комментариев, который начинается с /*-{ и заканчивается */-} . JSNI методы называются также как и любые норм. методы Java. Синтаксис JSNI является директивой для Java к JavaScript компилятору для принятия любого текста между заявленным блоком комментариев как правильный JavaScript код и внедряет его в созданные GWT файлы. Во время компиляции GWT компилятор выполняет некоторую проверку синтаксиса JavaScript кода внутри метода, затем генерирует код интерфейса для преобразования аргументов метода и возвращаемых значений. 
     Простой пример JSNI метода, который выводит JavaScript диалоговое окно:
public static native void alert(String msg) /*-{
    $wnd.alert(msg);
}-*/;

* This source code was highlighted with Source Code Highlighter.
    Обратите внимание, что код не ссылается на объект JavaScript окна непосредственно внутри метода. При обращении к окну браузера и объектам документа из JSNI, вы должны ссылаться на них как $wnd и $doc соответственно. Этот скомпилированный скрипт работает во вложенном фрейме, $wnd и $doc автоматически инициализируются правильно ссылаясь на окно и документ хостовой страницы. 
     Посмотрим на другой пример, который возбуждает исключение:
public static native int exampleThrewException() /*-{
    return "not a number";
}-*/;
try {
    int value = exampleThrewException();
    GWT.log("We got a value " + value, null);
} catch(Exception ex) {
    GWT.log("JSNI method exampleThrewException() threw an exception", ex);
}

* This source code was highlighted with Source Code Highlighter.
     Этот пример компилируется как Java, а его синтаксис проверяется GWT компилятором как валидный JavaScript. Но если запустить пример в режиме разработки он выбросит след. исключение.
com.google.gwt.dev.shell.HostedModeException: Something other than an int was returned from JSNI method '@com.dmitrynikol.webstorage.gwt.client.WebStorageGwtApp::exampleThrewException()': JS value of type string, expected int
    at com.google.gwt.dev.shell.JsValueGlue.getIntRange(JsValueGlue.java:266)
    at com.google.gwt.dev.shell.JsValueGlue.get(JsValueGlue.java:144)
    at com.google.gwt.dev.shell.ModuleSpace.invokeNativeInt(ModuleSpace.java:247)
    at com.google.gwt.dev.shell.JavaScriptHost.invokeNativeInt(JavaScriptHost.java:75)
    …....

* This source code was highlighted with Source Code Highlighter.
  В этом случае, ни Java IDE, ни GWT компилятор не скажет вам, что существует несоответствие между типом кода внутри JSNI метода и Java декларацией. Сгенерированный код интерфейса на GWT поймал проблему в рантайме в режиме разработки. При запуске кода в рабочем режиме исключение увидеть не можно, т.к. динамическая типизация JavaScript скрывает проблемы такого рода. 
     Как получить доступ к Java-методам и полям из JavaScript?
     Бывает очень полезно манипулировать Java объектами в пределах JavaScript реализации JSNI метода. Но, поскольку JavaScript использует динамическую типизацию, а Java использует статическую типизацию, вы должны использовать специальный синтаксис.
     Вызов Java-методов из JavaScript
     Вызов Java-методов из JavaScript похожа на вызов Java-методов из C в JNI. JSNI заимствует JNI подход сигнатуры метода для различения перегруженных методов. Вызовы JavaScript в Java методах имеет след. вид:
     [instance-expression]@class-name::method-name(param-signature)(arguments) 
- instance-expression - должен присутствовать при вызове метода экземпляра и должен отсутствовать при вызове статического метода
- class-name - полное имя класса в котором объявляется метод
- param-signature - внутренняя сигнатура Java метода определенная в JNI Type Signatures, но без задней сигнатуры возвращаемого типа метода, т.к. в нем нет необходимости при выборе перезагрузки
- arguments - список аргументов для передачи в вызываемый метод
     Вызов Java конструктора из JavaScript
Сравним на примере Java выражения против JSNI выражений:
class Main {
    public Main() {/** */}
    public Main(int i) {/** */}

    static class StaticInner {
        public StaticInner() {/** */}
    }

    class TestInner {
        public TestInner(int i) {/** */}
    }
}

* This source code was highlighted with Source Code Highlighter.
- new Main() equals
     @com.dmitrynikol.webstorage.gwt.client.Main::new()()
- newStaticInner() equals
     @com.dmitrynikol.webstorage.gwt.client.Main.StaticInner::new()()
- mainInstance.new TestInner(135) equals
     @com.dmitrynikol.webstorage.gwt.client.Main.TestInner: :new(Lcom/dmitrynikol/webstorage/gwt/client/Main;I)(mainInstance,135)
     Доступ к Java полям из JavaScript 
     Рассмотрим на примере доступ к статическим полям и полям экземпляра класса из JSNI:
class JSNI {
    String instanceField = "test";
    static double staticField = 12345;

    public void instanceMethod(String value) {
        /** just use 'value' */ Window.alert("execute instanceMethod");
    }

    public static void staticMethod(String value) {
        /** just use 'value' */ Window.alert("execute staticMethod");
    }

    /**
     * different situation of accessing Java fields from JavaScript by JSNI
     */
    public native void execute(JSNI jsni, String value) /*-{
        // call instance method runInstanceMethod() on this
        this.@com.dmitrynikol.webstorage.gwt.client.JSNI::instanceMethod(Ljava/lang/String;)(value);
        
        // call instance method runInstanceMethod() on jsni
        jsni.@com.dmitrynikol.webstorage.gwt.client.JSNI::instanceMethod(Ljava/lang/String;)(value);
        
        // call static method staticMethod()
        @com.dmitrynikol.webstorage.gwt.client.JSNI::staticMethod(Ljava/lang/String;)(value);
        
        // read instance field on this
        var valueFromInstanceField = this.@com.dmitrynikol.webstorage.gwt.client.JSNI::instanceField;
        $wnd.alert(valueFromInstanceField);
        
        // write instance field on jsni
        jsni.@com.dmitrynikol.webstorage.gwt.client.JSNI::instanceField = value + " value";
        $wnd.alert(jsni.@com.dmitrynikol.webstorage.gwt.client.JSNI::instanceField);
        
        // access to static field (without qualifier as you can see)
        $wnd.alert(@com.dmitrynikol.webstorage.gwt.client.JSNI::staticField + 1);
    }-*/;
}

* This source code was highlighted with Source Code Highlighter.
     Вызов Java метода из JavaScript
   Иногда необходимо получить доступ к методу или конструктору определенному в GWT во внешнем JavaScript коде. Этот код может быть написанный вручную и включен в другой js файл или это может быть частью сторонней библиотеки. В этом случае, GWT компилятор не сможет создать интерфейс между пользовательским JavaScript кодом и сгенерированным JavaScript через GWT напрямую. Эту взаимосвязь можно выполнить назначив метод через JSNI как внешний, глобально видимое JavaScript имя, которое может ссылаться на написанный вручную JavaScript.
.....
public static int recalculate(int period, float rate, int overtime) {
    /** ... */
}

public static native void exportStaticMethod() /*-{
    $wnd.count = $entry(@com.dmitrynikol.webstorage.gwt.client.JSNI::recalculate(IFI));
}-*/;
.....

* This source code was highlighted with Source Code Highlighter.
     Следует обратить внимание на то, что ссылка на экспортируемый метод была завернута в вызов метода $entry. Эта ф-ция гарантирует, что Java-производный метод выполняется с неперехваченным установленным обработчиком исключений.
     При инициализации приложения просто нужно вызвать ClassName.exportStaticMethod() из стартовой GWT точки. Это позволит присвоит ф-цию переменной в окне объекта наз. count.
     Параметры и возвращаемые типы JSNI методов объявлены как Java типы. Посмотрим на картинке конкретные правила того, как значения проходящие в и из JSNI кода должны быть обработаны.

     Исключения и JSNI
     Исключения могут быть брошены во время выполнения любого нормального Java кода или JavaScript кода в JSNI методе. Когда исключение генерируется в JSNI методе оно распространяется вверх по стеку вызова и улавливается в Java catch блоке. Выброшенное JavaScript исключение заворачивается в объект JavaScriptException в то время, когда оно было поймано. Этот объект обертки содержит только имя класса и описание исключения JavaScript, которое произошло. Рекомендуется обрабатывать JS исключения в JS коде, а Java исключения в Java коде. Java исключения могут безопасно сохранять идентичность распространяющихся через JSNI методы.
Например:
    1. Java метод firstExecute() вызывает JSNI метод nativeMethod()
    2. nativeMethod() внутри вызывает Java метод secondExecute()
    3. а secondExecute() бросает исключение
     Исключение из secondExecute() будет распространятся через nativeMethod() и может быть поймано в firstExecute() методе. Исключение будет сохранять свой тип и идентификацию.

суббота, 7 января 2012 г.

GWT and HTML5 client-side storage

     В GWT 2.3 появилась возможность хранения данных на клиентской стороне(веб-хранилище). Хранилище позволяет веб приложению получить преимущества хранения при работе с HTML5 совместимым браузером. 
   В HTML5 спецификация хранения является стандартизированным способом предоставления большого количества хранения данных на клиентской стороне и адекватное разделение между хранением в сессии и в локальном постоянном хранилище. Также она обеспечивает хранение сгенерированных событий и обработку заинтересованными слушателями. 
    Без возможности HTML5 на клиентской стороне хранилище ограничено хранением предоставляемым через куки (cookie) (4Kb для куки, 20 куки для домена). Если используются куки, то они обеспечивают хранения как для сессии так и локально, при этом одновременно доступны во всех окнах и вкладках браузера. Куки отправляются с каждым запросом на этот домен, что влияет на пропускную способность. Также механизм обработки куки является громоздким. 
     HTML5 предоставляет локальное хранилище размером в 5MB на домен, неограниченное хранилище сессии и успешное разделение между хранением в сессии и локально. Хранение в сессии доступно только от его происходящих вкладок или окон, нет распределение между всеми окнами браузера и вкладками. Процесс доступа к сессии и локальному хранилищу является простым, состоящий из простого чтения или записи ключей-значений в строковом виде. 
     Преимущества, которые мы получаем при использовании локального хранилища
- уменьшение трафика сети 
- ускорение отображения 
- кэширования данных через RPC вызовы 
- загрузка кэшированных данных при старте 
- сохранение временного состояния 
- восстановление состояния при перевхождении 
- предотвращение потери работы при разрыве сети 
  HTML5 веб хранилище определяет два типа хранения ключ-значение: хранение в сессии(SessionStorage) и локально(LocalStorage). Основное отличие состоит в том, как долго хранятся данные и как используются совместно. 
     LocalStorage позволяет хранить 5Mb для приложения в браузере, пока пользователь не почистит кеш. Данные доступны между каждым окном и вкладками одного браузера.  
     SessionStorage позволяет хранить без ограничений, сохраняется только в оригинальном окне или вкладке. Доступ можно получить только в открытом окне или вкладке. 
Локальное хранилище создается для каждого браузера и доступно в любом его окне или вкладке. 
     Как хранятся события при работе с хранилищем? 
Когда данные добавляются, модифицируются или удаляются из хранилища(LocalStorage или SessionStorage) StorageEvent возбуждаются внутри текущей вкладки или окна браузера. Это событие содержит объект хранилища, в котором происходит событие URL документа, к которому относится это хранилище, старое и новое значение ключа, которое было изменено. Любой слушатель зарегистрированный на это событие может обработать его. 
     Рассмотрим работу с хранилищем на примере. 
Для получение объекта хранилища используются методы: Storage.getLocalStorageIfSupported() or Storage.getSessionStorageIfSupported() в зависимости от типа хранения, которое вы хотите получить. 
В примере используется след. операции при работе с хранилищем: 
- проверка поддержки браузером хранилища 
- получение объекта хранилища для браузера 
- запись, чтение, удаление данных хранилища 
- обработка событий хранилища 
Класс StorageActionHandler отвечает за возможность записи, чтения и удаления значений из хранилища. Класс ManageEventHandlers занимается обработкой событий. И класс StorageEventHandler выводит подробную информацию о событии в текстовую область. 
  Для вывода информации из хранилища используется виджет BooksWidget с таблицей, в которую вставляются данные ключ-значение.
/**
* @author Nikolaenko Dmitry
*/
public class BooksWidget<T extends Book> extends DialogBox {
    public static BooksWidgetUiBinder uiBinder = GWT.create(BooksWidgetUiBinder.class);

    public interface BooksWidgetUiBinder extends UiBinder<Widget, BooksWidget<?>> {}

    @UiField CellTable<T> bookTable;
    @UiField Anchor close;

    public BooksWidget() {
        setWidget(uiBinder.createAndBindUi(this));
        setText("Detective books");
    }

    public void setData(List<T> books) {
        bookTable.addColumn(new TextColumn<T>() {
            @Override
            public String getValue(T object) {
                return object.getId();
            }
        }, "Id");
        bookTable.addColumn(new TextColumn<T>() {
            @Override
            public String getValue(T object) {
                return object.getTitle();
            }
        }, "Title");

        bookTable.setRowCount(books.size(), true);
        bookTable.setRowData(0, books);
    }

    @UiHandler("close")
    void onCloseClicked(ClickEvent event) {
        hide();
    }
}


* This source code was highlighted with Source Code Highlighter.
     Главный класс WebStorageGwtApp, где и происходят все основные операции, он же является стартовой точкой GWT приложения:
/**
* @author Dmitry Nikolaenko
*/
public class WebStorageGwtApp implements EntryPoint {
 
    public enum WebStorage {
        LOCAL,
        SESSION;
    }
    public enum StorageAction {
        WRITE,
        READ,
        DELETE;
    }
    public enum StorageEvents {
        ADD,
        DELETE;
    }
 
    private static List<Book> books = new LinkedList<Book>();
 
    static {
        books.add(new Book("132529", "The Hound of the Baskervilles"));
        books.add(new Book("745353", "The Man in the Brown Suit"));
        books.add(new Book("248941", "Liberty Bar"));
        books.add(new Book("478134", "The Case of the Turning Tide"));
        books.add(new Book("328473", "The Dogs of War"));
    }
 
    private List<StorageEvent.Handler> handlers = new ArrayList<StorageEvent.Handler>();
    private TextArea eventArea;
    private Label handlersLabel;
 
    private Storage local;
    private Storage session;

    public void onModuleLoad() {
        FlowPanel main = new FlowPanel();
        main.getElement().getStyle().setPosition(Position.ABSOLUTE);

        eventArea = new TextArea();
        eventArea.setStyleName("eventArea");
        eventArea.setText("StorageEvent info: ");
        handlersLabel = new Label("#Handlers: 0");
     
        main.add(eventArea);
        main.add(handlersLabel);

        if (!Storage.isLocalStorageSupported() && !Storage.isSessionStorageSupported()) {
            Window.alert("Web Storage NOT supported in this browser!");
            return;
        }

        local = Storage.getLocalStorageIfSupported();
        session = Storage.getSessionStorageIfSupported();
     
        /** handling storage events */
        main.add(new Button("Add a Handler", new ManageEventHandlers(StorageEvents.ADD)));
        main.add(new Button("Delete a Handler", new ManageEventHandlers(StorageEvents.DELETE)));
     
        /** read action */
        main.add(new Button("Read data from Local Storage",
                new StorageActionHandler(WebStorage.LOCAL, StorageAction.READ)));
        main.add(new Button("Read data from Session Storage",
                new StorageActionHandler(WebStorage.SESSION, StorageAction.READ)));
     
        /** write action */
        main.add(new Button("Write data to Local Storage",
                new StorageActionHandler(WebStorage.LOCAL, StorageAction.WRITE)));
        main.add(new Button("Write data to Session Storage",
                new StorageActionHandler(WebStorage.SESSION, StorageAction.WRITE)));
     
        /** delete action */
        main.add(new Button("Delete data from Local Storage",
                new StorageActionHandler(WebStorage.LOCAL, StorageAction.DELETE)));
        main.add(new Button("Delete data from Session Storage",
                new StorageActionHandler(WebStorage.SESSION, StorageAction.DELETE)));
     
        RootPanel.get().add(main);
    }
 
    /**
     * handler to manage the storage
     */
    private class StorageActionHandler implements ClickHandler {
     
        private WebStorage webStorage;
        private StorageAction action;
        private Storage storage;
     
        private StorageActionHandler(WebStorage storage, StorageAction action) {
            this.webStorage = storage;
            this.action = action;
        }

        @Override
        public void onClick(ClickEvent event) {
            storage = webStorage.equals(WebStorage.LOCAL) ? local : session;
         
            if (action.equals(StorageAction.WRITE)) {
                for (Book key : books) {
                    storage.setItem(key.getId(), key.getTitle());
                }
            } else if (action.equals(StorageAction.READ)) {
                BooksWidget<Book> booksWidget = new BooksWidget<Book>();
             
                List<Book> storageBooks = new LinkedList<Book>();
                for (int i = 0; i < storage.getLength(); i++) {
                    storageBooks.add(new Book(storage.key(i), storage.getItem(storage.key(i))));
                }
                booksWidget.setData(storageBooks);
                booksWidget.show();
            } else {
                storage.clear();
            }
        }
    }
 
    /**
     * class that responsible for manage event handlers
     */
    private class ManageEventHandlers implements ClickHandler {
     
        private StorageEvents storageEvents;
     
        private ManageEventHandlers(StorageEvents event) {
            this.storageEvents = event;
        }

        @Override
        public void onClick(ClickEvent event) {
            if (storageEvents.equals(StorageEvents.ADD)) {
                StorageEvent.Handler handler = new StorageEventHandler(handlers.size() + 1);
                handlers.add(handler);
                Storage.addStorageEventHandler(handler);
            } else {
                if (handlers.size() > 0) {
                    StorageEvent.Handler handler = handlers.remove(handlers.size() - 1);
                    Storage.removeStorageEventHandler(handler);
                }
            }
            handlersLabel.setText("#Handlers: " + handlers.size());
        }
    }
 
    /**
     * class that responsible for update information in {@link eventArea} element
     */
    private class StorageEventHandler implements StorageEvent.Handler {
        private int number;

        private StorageEventHandler(int number) {
            this.number = number;
        }

        public void onStorageChange(StorageEvent event) {
            StringBuilder sb = new StringBuilder();
            sb.append(eventArea.getText());
            sb.append("\nStorageEvent: Handler=").append(number);
            sb.append(", key=").append(event.getKey());
            sb.append(", oldValue=").append(event.getOldValue());
            sb.append(", newValue=").append(event.getNewValue());
            sb.append(", url=").append(event.getUrl());
            sb.append(", timestamp=").append(new Date());
         
            eventArea.setText(sb.toString());
        }
    }
}


* This source code was highlighted with Source Code Highlighter.
Вот так выглядит работающие приложение:
Eclipse проект можно скачать по следующей ссылке.

пятница, 6 января 2012 г.

Использование GWT Canvas

     Спецификация HTML 5 включает множество новых ф-ций, одной из которых является тег canvas. Он позволяет выводить графику и рисовать с использованием JavaScript. В GWT 2.2 была добавлена поддержка HTML5 Canvas елемента. 
     Canvas предоставляет такие инструменты как: 
- рисование: прямоугольников, дуг, линий и др. 
- эффекты: тени, прозрачность... 
- преобразования: масштабирование, вращение, трансформирование... 
     В пакете com.google.gwt.canvas содержится множество полезных классов для манипулирования полотном. Но перед тем как будет загружен модуль мы должны инициализировать canvas, если он поддерживается браузером.
canvas = Canvas.createIfSupported();
if (canvas == null) {
    RootPanel.get().add(new Label("Sorry, your browser doesn't support the HTML5 Canvas element"));
    return;
}


* This source code was highlighted with Source Code Highlighter.
     Если браузер не имеет поддержки HTML 5, то выводим метку с сообщением. 
Напишем пример аналоговых часов на GWT используя Canvas.
/**
* @author Dmitry Nikolaenko
*/
public class CanvasGwtApp implements EntryPoint {

    /**
     * update canvas based on interval
     */
    private static final int REFRESH_RATE = 1000;

    private static final int CANVAS_HEIGHT = 300;
    private static final int CANVAS_WIDTH = 300;

    private Canvas canvas;
    private Context2d context;

    public void onModuleLoad() {
    canvas = Canvas.createIfSupported();
    if (canvas == null) {
        RootPanel.get().add(new Label("Sorry, your browser doesn't support the HTML5 Canvas element"));
        return;
    }

        canvas.setWidth(CANVAS_WIDTH + Unit.PX.getType());
        canvas.setCoordinateSpaceWidth(CANVAS_WIDTH);
        canvas.setHeight(CANVAS_HEIGHT + Unit.PX.getType());
        canvas.setCoordinateSpaceHeight(CANVAS_HEIGHT);

        context = canvas.getContext2d();
        RootPanel.get().add(canvas);

        final Timer timer = new Timer() {
            @Override
            public void run() {
                updateClock(JsDate.create());
            }
        };
        timer.scheduleRepeating(REFRESH_RATE);
    }

    private void updateClock(JsDate date) {
        context.save();
        context.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
        context.translate(CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2);
        context.scale(0.9, 0.9);
        context.rotate(-Math.PI / 2);
        context.setStrokeStyle("black");
        context.setLineWidth(4);
        context.setLineCap("round");

        // draw hour marks
        context.save();
        for (int i = 0; i < 12; i++) {
            context.rotate(Math.PI / 6);
            drawClockLine(120, 130);
        }
        context.restore();

        // draw minute marks
        context.save();
        context.setLineWidth(2);
        for (int i = 0; i < 60; i++) {
            if (i % 5 != 0) {
                drawClockLine(127, 130);
            }
            context.rotate(Math.PI / 30);
        }
        context.restore();

        // define current time
        long sec = date.getSeconds();
        long min = date.getMinutes();
        long hr = date.getHours();
        hr = hr >= 12 ? hr - 12 : hr;

        // write hours, save the context's state
        context.save();
        context.rotate(hr * (Math.PI / 6) + (Math.PI / 360) * min
                + (Math.PI / 21600) * sec);

        // draw the hour hands
        context.setLineWidth(6);
        drawClockLine(-20, 90);
        context.restore();

        // write minutes
        context.save();
        context.rotate((Math.PI / 30) * min + (Math.PI / 1800) * sec);

        // draw the minute hands
        context.setLineWidth(5);
        drawClockLine(-28, 112);
        context.restore();

        // write seconds
        context.save();
        context.rotate(sec * Math.PI / 30);

        context.setFillStyle(getRandomColor());
        context.setLineWidth(2);
        drawClockLine(-30, 105);

        // draw the circle at the end of seconds-hand
        context.beginPath();
        context.arc(0, 0, 10, 0, Math.PI * 2, true);
        context.fill();
        context.beginPath();
        context.arc(90, 0, 6, 0, Math.PI * 2, true);
        context.fill();
        context.stroke();
        context.closePath();
        context.setFillStyle(getRandomColor());
        context.beginPath();
        context.arc(0, 0, 3, 0, Math.PI * 2, true);
        context.fill();
        context.restore();

        // draw the main arc of the clock
        context.beginPath();
        context.setLineWidth(4);
        context.setStrokeStyle("#A25A94");
        context.arc(0, 0, 142, 0, Math.PI * 2, true);
        context.stroke();
        context.restore();
    }

    /**
     * draw the line - clock hands
     */
    private void drawClockLine(int from, int to) {
        context.beginPath();
        context.moveTo(from, 0);
        context.lineTo(to, 0);
        context.stroke();
    }

    /**
     * generate random color
     */
    private CssColor getRandomColor() {
        int redColor = Random.nextInt(255);
        int greenColor = Random.nextInt(255);
        int blueColor = Random.nextInt(255);

        // with alpha transparency
        // CssColor randomColor = CssColor.make("rgba(" + redColor + ", " +
        //        greenColor + "," + blueColor + ", " + Random.nextDouble() + ")");

        return CssColor.make(redColor, greenColor, blueColor);
    }
}


* This source code was highlighted with Source Code Highlighter.
Вот так выглядит запущенный пример: