Grooscript で Canvas アニメーション

G* Advent Calendar 2015 16日目です!(更新前に来てくださった方すいません…)


Groovy を altJS にしてしまった驚異の Grooscript を紹介したいと思います。Groovy をかくとソースコードが JavaScript に変換され、ブラウザや node.js 上で実行できてしまいます。

論よりrunということで、Try online がありますので試してみると良いと思います。

Try groovy to javascript conversion with grooscript

http://grooscript.org/conversions.html

ここでは、ブラウザを Grooscript の実行環境とし、Groovy で Canvas を使ったアニメーションのプログラミングをしてみます。

Grooscript を始めるには Grooscript の Gradle plugin を使うと簡単です。初期コードの作成からファイルウォッチからの変換まで可能です。

build.gradle

plugins {
    id "org.grooscript.conversion" version "1.2.2"
}

これで、

gradle initStaticWeb

とすれば、初期コードを生成してくれます。

grooscript-01

この状態で次のようにすれば、src/main/groovy 配下の *.groovy ファイルの更新を監視して、src/main/webapp/js/app の下に変換した *.js を入れてくれます。

gradle --daemon convertThread

監視を止める場合は gradle のデーモンを停止させればOKです。(Windows, Linux, OS X とも動作するはずです)

gradle --stop

あとは Groovy をかけば完成です。 🙂

ここでは Canvas アニメーションを実装するために、src/main/webapp/index.html を次のようにしました。

<html>
<head>
<title>groovy on browser!</title>
<link rel="stylesheet" href="css/style.css" />
<script type="text/javascript" src="js/lib/grooscript.min.js"></script>
<script type="text/javascript" src="js/app/Presenter.js"></script>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
<canvas></canvas>
</body>

src/main/groovy/Presenter.groovy で Canvas に対するアニメーション処理を Groovy でかきました。

ブラウザの window や document などのオブジェクトはそのまま記述できます。

import org.grooscript.asts.GsNative

class Animation {

    static ANIMATION_FPS = 60;

    def canvas
    def context
    def width
    def height

    def actor
    def line

    Animation() {
        window.addEventListener('load', init, false)
        window.addEventListener('resize', init, false)
        window.addEventListener('orientationchange', init, false)
        window.setInterval(update, 1000 / ANIMATION_FPS, false)
        render()
    }

    def init() {
        actor = []
        canvas = document.getElementsByTagName('canvas')[0]
        context = canvas.getContext('2d')
        width = canvas.clientWidth
        height = canvas.clientHeight
        canvas.setAttribute("width" ,width);
        canvas.setAttribute("height" ,height);
        for(def degree = 0 ; degree <= 360; degree += 360/86) {
            this.actor << new Ham("images/ham.png"
                , this.width
                , this.height
                , degree
            )
        }
    }

    def update() {
        actor.each {
            it.update()
        }
    }

    def render() {
        window.requestAnimationFrame(render)
        if(!context) return
        context.fillStyle = "#000"
        context.fillRect(0, 0, width, height)
        actor.each {
            it.draw(context)
        }
    }
}

class Ham extends Actor {

    def swidth
    def sheight
    def degree
    def sx
    def sy
    def sv

    Ham(def url, def w, def h, def d) {
        super(url)
        swidth = w
        sheight = h
        sx = swidth / 2
        sy = sheight / 2
        sv = 1
        degree = d
        width = 48
        height = 48
    }

    @GsNative
    def update() {/*
        var radian = Math.PI / 180 * this.degree
        this.x = Math.cos(radian) * (this.sx / 2 - 24) + this.swidth / 2 - 24
        this.y = Math.sin(radian) * (this.sy / 2 - 24) + this.sheight / 2 - 24
        this.degree += 0.9
        if(this.degree > 360) {
            this.degree = 0
        }
        this.sx += this.sv * 8
        this.sy += this.sv * 8
        if(this.sx >= this.swidth
            || this.sy >= this.sheight
            || this.sx <= (this.swidth * -1) + 48 + 24
            || this.sy <= (this.sheight * -1) + 48 + 24
            ) {
            this.sv = this.sv * -1
        }
        this.width = 48 + (this.sx / 48)
        this.height = 48 + (this.sy / 48)
    */}
}

abstract class Actor {

    def image
    def x = 0
    def y = 0
    def width
    def height

    Actor(def url) {
        image = Resource.getInstance().getImage(url)
    }

    abstract update()

    @GsNative
    def draw(context) {/*
        if(!this.image['loaded']) return
        context.drawImage(this.image['image'], this.x, this.y, this.width, this.height)
    */}
}

class Resource {

    def static resource = new Resource()
    def images = [:]

    private Resource() { }
    
    def static getInstance() {
        return resource
    }

    def getImage(url) {
        if(!images.containsKey(url)) {
            images[url] = [:]
            def image = document.createElement('img')
            image.src = url
            image.addEventListener('load', {
                images[url]['image'] = image
                images[url]['loaded'] = true
                images[url]['width'] = image.naturalWidth
                images[url]['height'] = image.naturalHeight
            }, false)
        }
        return images[url]
    }
}

new Animation()

本当に Groovy です。 😀

一部 @GsNative というアノテーションがついたコメント実装されているメソッドは JavaScript のネイティブコードです。 速度を稼ぎたい部分は、このように(Groovy で変換された周りのコードを考えつつ) JS をそのままかくこともできます。

以下をクリックすると、実際に動いている様子が見ることができます。

ham

変換された JavaScript のコードを見ると面白いと思います。

Grooscript はここ1年で node.js、require.js や React.js の対応が進むなど、開発を見ているのも楽しいです。

ブラウザとのつなぎも簡単ですので、Groovy な方はぜひ試してみてください 🙂

Keep on Groovy-ing!

Groovy + Spring Boot + SWT でクライアントアプリケーションをつくる

G* Advent Calendar 2015 9日目です!

昨日は saba1024 さん「[Groovy] MongoDBを簡単に扱えるイケてる言語Groovy -Groovyの応用編-」でした!


 

職場などで業務改善的なツールをつくりたくなる場合がありますが、案外みんなの PC にスクリプト言語を動かす環境がなかったりします。そんな時は Groovy ! Java の現場であればそのままつくった jar を渡せますし、そうでなくても launch4j などで JRE ごと渡すことができます。

今回は「Groovy + Spring Boot + SWT」という組み合わせで、手軽に高速に GUI Groovy アプリケーションをつくる骨格を紹介してみたいと思います。

珍しい組み合わせかと思いますが、Spring Boot のオートコンフィグレーションと、後述する spring-loaded によるホットデプロイ(GUI 再起動無しで処理を変更できる)と Groovy によるプログラミングの組み合わせは、かなり高速に開発を進めることができると思います。

プロジェクトの構成は以下のような感じになります。

groovy-swt-02

まずは、build.gradle で Spring Boot と SWT を定義してあげます。

build.gradle

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.0.RELEASE'
    }
}

apply plugin: 'groovy'
apply plugin: 'spring-boot'

repositories {
    mavenCentral()
    maven { url 'http://maven-eclipse.github.io/maven' }
}

dependencies {
    compile 'org.codehaus.groovy:groovy-all:2.4.5'
    compile 'org.springframework.boot:spring-boot-starter'
    compile "org.eclipse.swt:org.eclipse.swt.win32.win32.x86:4.5.1"
}

springBoot {
    mainClass = 'sample.Application'
}

SWT は Windows x86 のものを選択していますが、他の環境の定義は次の通りです。

    // The Standard Widget Toolkit(SWT)
    // System.getProperty('os.name').toLowerCase().split()[0]
    // System.getProperty("os.arch")
    // 
    // Windows x86
    // compile "org.eclipse.swt:org.eclipse.swt.win32.win32.x86:4.5.1"
    // Windows x64
    // compile 'org.eclipse.swt:org.eclipse.swt.win32.win32.x86_64:4.5.1'
    // Linux GTK+ x86
    // compile 'org.eclipse.swt:org.eclipse.swt.gtk.linux.x86:4.5.1'
    // Linux GTK+ x64
    // compile 'org.eclipse.swt:org.eclipse.swt.gtk.linux.x86_64:4.5.1'
    // OS X Cocoa x64
    // compile 'org.eclipse.swt:org.eclipse.swt.cocoa.macosx.x86_64:4.5.1'

次に Spring Boot の規約に従って(必要なら)ロガー(logback-spring.xml)と Application.yml を定義します。

src/main/resources/logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/base.xml"/>
    <logger name="sample" level="DEBUG"/>
</configuration>

src/main/resources/Application.yml

setting:
    defaultPath: C:\Users

Application.yml は Spring Boot の設定も記述できますが、作成するアプリケーションで外だししたい設定なども書けます。これを読むための、ApplicationSetting は次のようになります。

src/main/groovy/ApplicationSetting.groovy

package sample;

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.stereotype.Component

@Component
@ConfigurationProperties(prefix = "setting")
public class ApplicationSetting {
    String defaultPath
}

Java でかくと Setter/Getter が必要ですが、Groovy ならこれだけです。.yml のキーと変数名を合わせれば勝手に Spring Boot がバインドしてくれます。

プログラムの起動点となる Application は次のようになります。

src/main/groovy/Application.groovy

package sample

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.SpringApplication
import sample.gui.Controller

@SpringBootApplication
class Application {

    @Autowired
    Controller controller

    static void main(args) {
        SpringApplication.run(Application.class, args).withCloseable {
            it.getBean(Application.class).controller.start()
        }
    }
}

通常の Java アプリケーションと同様に main を起動してあげると、Spring Boot が main があるパッケージ配下のコンポーネントを自動でスキャンしてクラスロードしてくれます。 @Autowired で GUI の Controller をインジェクションして main から呼び出しました。

呼び出される gui.Controller は次のようなものです。

src/main/groovy/gui/Controller.groovy

package sample.gui

import org.slf4j.*
import org.eclipse.swt.*
import org.eclipse.swt.events.SelectionListener
import org.eclipse.swt.graphics.*
import org.eclipse.swt.layout.*
import org.eclipse.swt.widgets.*
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
import sample.ApplicationSetting
import sample.service.Convert

@Component
class Controller {

    static final logger = LoggerFactory.getLogger(Controller.class);

    @Autowired
    Convert convert

    @Autowired
    ApplicationSetting setting

    Display display
    Shell shell
    ToolItem open
    Table list

    def private init() {
        // Window Setting
        display = new Display()
        shell = new Shell(
            display, SWT.TITLE | SWT.RESIZE | SWT.MIN | SWT.MAX | SWT.CLOSE)
        shell.setSize(640, 480)
        shell.setText("Sample Application")
        shell.setLayout(new GridLayout())
        // Application Icon
        shell.setImage(new Image(display, this.getClass().getResourceAsStream(
            "/images/icon/calculator.32x32.png")))

        // Toolbar
        def toolbar = new ToolBar(shell, SWT.FLAT | SWT.RIGHT)
        // Open
        open = new ToolItem(toolbar, SWT.PUSH)
        open.setText('Open')
        open.setImage(new Image(display, this.getClass().getResourceAsStream(
            "/images/icon/start.32x32.png")))
        // Toolbar Size
        toolbar.pack()

        // List Table
        list = new Table(shell, SWT.FULL_SELECTION | SWT.BORDER)
        list.setHeaderVisible(true)
        list.setLayoutData(new GridData(GridData.FILL_BOTH))
        list.setFocus()

        // Event Listener
        open.addSelectionListener([
            widgetSelected : {  e ->
                def f = new FileDialog(shell, SWT.OPEN)
                f.setFilterPath(setting.defaultPath);
                f.setFilterExtensions(["*.csv"] as String[])
                def login = f.open()
                def message = convert.input(login);
                def box = new MessageBox(shell, SWT.OK | SWT.OK)
                box.setMessage(message)
                box.open()
            }
        ] as SelectionListener)
 
        shell.open()
    }

    public void loop() {
        while(!shell.isDisposed()) {
            if(!display.readAndDispatch()) {
                display.sleep();
            }
        }
        display.dispose()
    }

    def start() {
        try {
            init()
            loop()
        } catch(Exception e) {
            e.printStackTrace()
            def box = new MessageBox(shell, SWT.OK | SWT.ABORT)
            box.setMessage("例外が発生しました。\n" + e.message)
            box.open()
        }
    }
}

SWT の GUI 作成とメインイベントループがあります。

ソース先頭で、サービスクラスにあたる service.Convert と先ほど Application.yml を読むためにつくった ApplicationSetting を DI しています。

    @Autowired
    Convert convert

    @Autowired
    ApplicationSetting setting

ボタンを押したときのイベントでこれらを利用しています。

        // Event Listener
        open.addSelectionListener([
            widgetSelected : {  e ->
                def f = new FileDialog(shell, SWT.OPEN)
                f.setFilterPath(setting.defaultPath);
                f.setFilterExtensions(["*.csv"] as String[])
                def login = f.open()
                def message = convert.input(login);
                def box = new MessageBox(shell, SWT.OK | SWT.OK)
                box.setMessage(message)
                box.open()
            }
        ] as SelectionListener)

サービスクラスはとりあえず。

src/main/groovy/service/Convert.groovy

package sample.service;

import java.io.File;

import org.springframework.stereotype.Component;

@Component
public class Convert {
    def input(file) {
        return "converted!" 
    }
}

というわけで、Application.groovy を IDE から実行するか、./gradlew bootRun するとアプリケーションが起動します。

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.3.0.RELEASE)

2015-12-09 00:25:31.353  INFO 6560 --- [           main] sample.Application 

groovy-swt-01

さて、ここで大技です。 spring-loaded を実行時に javaagent で読み込むことで、実行中に修正したクラスをリロードできるようになります。 プロジェクトサイトから jar をダウンロードし、アプリケーション実行時の JVM 引数に以下を加えます。

-javaagent:${project_loc:template-springboot-swt}/lib/springloaded-1.2.5.RELEASE.jar -noverify

“${project_loc:template-springboot-swt}” は Eclipse 的なプロジェクトホームの指定ですので、適宜変更してください。

groovy-swt-04

なお、Eclipse で実行する場合は Application.groovy から Run してください。./gradle bootRun だとリロードされません。これは、Eclipse の場合、自動コンパイルの .class ファイルを置く先が bin/ ディレクトリ配下となり、bootRun した場合は build/ 配下の class ファイルで実行され、ファイルの更新がされないためです。

というわけで、spring-loaded ですが、やってみると異常に快適です。 GUI をつくる場合、対象の処理にたどり着くまでの操作が長くかかることがありますが、spring-loaded を入れておくとリトライが簡単で、なんだか世界が変わります。 🙂

その他、Spring Boot 上であると、H2 の組み込み DB や、JPA や GORM などの O/R マッパーも、gradle の定義だけでオートコンフィグレーションされてすぐ使えるようになりますので、非常に便利です。

今回のサンプルでは、サービスクラスで CSV を H2 に読んで GORM で抽出をかけたかったのですが、出張先にて機材トラブルにより間に合いませんでした。ごめんなさい。。

以上、あまり手間をかけず、ゆるいフレームワーク規約で自由気ままにツールなどの GUI をつくりたいなんて時に、良ければお試しください。

最後に、Apache Groovy おめでとうございます! 今年も Groovy にずいぶん助けてもらいました。 🙂

Keep on Groovy-ing!

統合環境で baserCMS のテンプレートで使える関数を補完する

baserCMS のテーマをつくっていると、関数の補完機能がほしくなってきます。

というわけで、Eclispe や PHPStorm などの統合環境を使っていれば、簡単な定義をするだけで補完が効くようになりますのでできますので紹介したいと思います。(NetBeans には NetBeans baserCMS support があります!)

補完というのは、オートコンプリート、インテリセンス、いわゆるこれです。

autocomp

使える関数(メソッド)が自動ででてくるあれですね。 🙂

何もせずとも補完ができると良いのですが、PHP などの動的型付け言語は、プログラムの形が実行時まで決定しないため統合環境がうまくソースコードを解析できず補完できないパターンがあり、baserCMS のテーマ・テンプレートもこの条件に当てはまります。

以下は、baserCMS の Blog/index.php テンプレートですが、ここででてくる $this がどこの this であるかわからないため、統合環境が適切に補完を出すことができません。

<?php
/**
 * ブログトップ
 */
$this->BcBaser->css('admin/colorbox/colorbox', array('inline' => false));
$this->BcBaser->js('admin/jquery.colorbox-min-1.4.5', false);
$this->BcBaser->setDescription($this->Blog->getDescription());

というわけで、これを解決してあげるために統合環境に this を教えてあげます。

ファイル先頭などに @var コメントアノテーション行を追加し $this が AppView であることを明示します。(テーマ・テンプレートファイル全てに入れればよいです)

<?php /* @var $this AppView */ ?>
<?php
/**
 * ブログトップ
 */
$this->BcBaser->css('admin/colorbox/colorbox', array('inline' => false));
$this->BcBaser->js('admin/jquery.colorbox-min-1.4.5', false);
$this->BcBaser->setDescription($this->Blog->getDescription());
?>

これで $this が何者であるか統合環境が理解できるようになったのですが、こんどは $this->BcBaser の BcBaser が AppView のソースコード中に存在しないため、まだ setDescription などの関数の補完ができません。これは Helper オブジェクトが実行時に動的にインジェクションされ、ソースコードだけではプログラムの形がわからないためです。

というわけで、AppView にコメントをいれてあげます。幸い、baserCMS は lib/AppView.php のオーバーライドが app/View で可能ですので、app/View/AppView.php にコピーして以下のコメントを追加してあげます。(使わないものも含めてありったけ入れてしまいましたが…)

/**
 * View 拡張クラス
 *
 * @package			Baser.View
 * @property BcAdminHelper $BcAdmin
 * @property BcAppHelper $BcApp
 * @property BcArrayHelper $BcArray
 * @property BcBaserHelper $BcBaser
 * @property BcCacheHelper $BcCache
 * @property BcCkeditorHelper $BcCkeditor
 * @property BcCsvHelper $BcCsv
 * @property BcFormHelper $BcForm
 * @property BcFreezeHelper $BcFreeze
 * @property BcGooglemapsHelper $BcGooglemaps
 * @property BcHtmlHelper $BcHtml
 * @property BcMobileHelper $BcMobile
 * @property BcPageHelper $BcPage
 * @property BcSmartphoneHelper $BcSmartphone
 * @property BcTextHelper $BcText
 * @property BcTimeHelper $BcTime
 * @property BcUploadHelper $BcUpload
 * @property BcXmlHelper $BcXml
 */
class AppView extends BcAppView {
	
}

これでテーマ・テンプレートから補完ができるようになりました。 😀

Eclipse PHP for Developer の例。

baser-eclipse

PHPStorm の例。

baser-phpstome

両 IDE ともに AppView が複数存在していても、コメントをマージしてくれるようですので、ヘルパーが存在するプラグインでは (PluginDir)/View/AppView.php にコメントかいておけば補完を出すことができます。

同じ要領で、プラグインの Controler でも、基底クラスなどにコメントをかいておけば、インジェクションされてくるモデルも補完できます。(baserCart テーマでやってますのでご参考まで)

<?php
App::uses('BcPluginAppController', 'Controller');

/**
 * CartAppController
 * 
 * @property CartConfig $CartConfig
 * @property CartItem $CartItem
 * @property CartItemTag $CartItemTag
 * @property CartOrder $CartOrder
 * @property CartOrderDetail $CartOrderDetail
 * @property BcAuth $BcAuth
 * @property Session $Session
 * @author hiromasa
 */
class CartAppController extends BcPluginAppController {

ちなみに、WordPress はテンプレートタグ(関数)がグローバル空間に存在しますので、Eclipse、PHPStorm ともに何もしなくても補完がでます。

加えて、functions.php などでたとえば global $wpdb なんてグローバル変数経由のインスタンスを使う場合は、@var を使って以下のようにすると補完できます。

/* @var $wpdb wpdb */

wordpress-eclipse

てなわけで補完がきくときかないのでは、ずいぶん開発効率が違うと思いますので、良ければお試しください。 🙂