17
7

More than 1 year has passed since last update.

SVGベースのPythonフロントエンドライブラリを作っているという話

Last updated at Posted at 2021-12-05

TL;DR

今年からPythonで書けるapyscという名前のSVGベースのフロントエンドライブラリを作り始めたのでその1年のまとめです。

どんなライブラリなのか、何故作っていっているのか、現在どこまでできるようになったのか、その他使い方の詳細や今後どの辺を実装していこうと考えているのかなどについて触れていきます。

ライブラリ基本情報

GitHub:

ドキュメント:

PyPIに登録済みのため以下のようにpipでさくっとインストールすることができます(Python 3.6以降をサポートしています)。

$ pip install apysc

※本記事では執筆時点で最新の0.55.9のバージョンを使っていきます。

$ pip install apysc==0.55.9

どんなライブラリなのか(現在までにできるようになっていること)

様々なSVGの図形の描写機能

設定に応じた四角・丸・線と様々なSVGの図形を描写できるようになっています。

image.png

例えば四角でいうと以下のような書き方で描写できるようになっています。

rectangle: ap.Rectangle = sprite.graphics.draw_rect(
    x=50, y=50, width=50, height=50)

また、各図形はコンテナ用のインスタンスを挟んでグループ的に扱うことなどができるようになっています。

マウスイベント関係の操作

配置した要素に対してクリック・マウスダウン / アップ・マウスオーバー / アウト・ムーブなどのマウスイベントを登録できるようになっています。

mouse_move.gif

クリックイベントであれば以下のような感じで設定できるようになっています。

def on_click(e: ap.MouseEvent, options: dict) -> None:
    ap.trace('Clicked!')


...
rectangle.click(on_click)

各属性の設定

生成したSVG要素は生成してから各属性の値を更新することもできます。例えば塗りの色をマゼンタにして線幅や線の色を後から更新するには以下のような指定になります。

rectangle.fill_color = ap.String('#f0a')
rectangle.line_thickness = ap.Int(3)
rectangle.line_color = ap.String('#fff')

image.png

(Tween)アニメーション設定

各属性などはイージング設定も含めてアニメーションを設定することができるようになっています。

animation_interfaces_abstract.gif

インターフェイスはanimation_というプレフィックスで統一してあり、例えばX座標のアニメーション設定であれば以下のように設定します。

rectangle.animation_x(
    x=100, duration=1000, easing=ap.Easing.EASE_OUT_QUINT).start()

HTMLの保存やJupyter上での表示

Pythonで書いたコードはHTMLで保存してブラウザ上で表示することができます。

もしくはJupyter notebookもしくはJupyterLab、Colaboratory上で直接使うこともできます。

jupyterlab_interface.png

ただしVS Code上のJupyterは現在サポートしていません(以前対応しようとしたところストレートにいかず一旦後回しに・・・ただし私自身はJupyterはVS Code上で使うことが多くなってきているので後日サポートをリベンジしたいところです)。

型を恩恵を受けられる

apyscの内部実装はコードは基本的にmypyのLintをCIとして通しており、且つエディタ上ではPylance(Pyright)で型チェックしながら作業しているため、基本的に全コードで型アノテーションがされており型の恩恵を受けることができます。

世の中がTypeScript移行がどんどん進んでいる点や他の言語でのゲーム開発などでも基本的に型付きでやるケースが大半だと思いますし、Pythonでもやはり型の恩恵を受けながら開発できるのは快適です。

また、Pylanceなどを使う場合コンパイルやビルドなどをしなくても型のミスや文法ミスをリアルタイムに確認できて作業が効率的というのもあります。

この辺りは以前記事にしたので必要に応じてそちらをご確認ください。

docstringの恩恵を受けられる

apyscの内部実装はNumPyスタイルのdocstringの設定は必須となるようにCI部分でLintを設定しているため、各インターフェイスでdocstringを参照しながら開発することができます。

NumPyスタイルのdocstringやそのチェック用のLintなどについては以前記事にしているのでそちらをご参照ください。

また、docstring内には各インターフェイスのドキュメントのURLなどもReferencesセクションに記載しているのでコードを動かしたらどんな感じになるのか?といった内容なども確認しやすくなっています。

image.png

何故作っているのか

趣味が目的の9割5分くらいを占めます。作っていてとても楽しいです。

私のHTMLやjsの勉強目的も含みます。

お仕事でweb上でのインタラクティブ性を持ったデータビジュアライゼーション関係を扱うことが結構あります。一般的なものであれば様々な選択肢があります。例えばC3.jsであったりPlotlyのweb版であったり等様々です。

一方で様々なカスタマイズしたり自前で色々やりたくなってきた場合、選択肢がD3.jsなどになってきます。D3.js自体はとても面白いですし資料も多く、GitHub上でスター数がこの記事を書いている時点で10万くらいになっており非常に支持もされていると思います。

一方でこの辺のお仕事は社内のデータエンジニアが担当することが多くなっています。技術スタック的にwebフロントエンドには世の中のフロントエンジニアさん達と比べると全然詳しくないですし、D3.jsやらTypeScriptやら・・・となってくるとキャッチアップも大変です。仕事のプロジェクト自体はDjangoでバックエンドがPythonになっているのでフロントエンドのその辺の複雑な可視化が必要になってきた時にPythonで書けると学習コストが減って楽ができそうです。

※フロントエンドもバックエンドもTypeScriptなどで統一する・・・というのも両方担当する場合は楽なのですが、データを色々と扱うお仕事なためバックエンドはPythonだと色々楽だったりエコシステムが非常に充実しているためバックエンドはPythonはこのまま使いたいと考えているためフロント側がPythonで書けるようになったら良いな・・・と思っています。全体の置換は無理でも複雑なデータビジュアライゼーション部分などで一部分でも段々と将来使っていけたらな・・・と考えています。

また、バックエンドとフロントエンドで言語を統一することで学習コストだけでなく言語のスイッチングコストも抑えたりができるかなと期待しています。例えば普段はPythonばっかり仕事で書いていますが、たまにjsなどを書くとうっかり()や{}の括弧を書き忘れていたりprint()といった記述をしたりといったケースがあります(Pythonと書き方がごっちゃになったりと・・・)。すぐに気づくのでほぼ実害は無いのですがちょっと気になります。

また、CI的なところも整備もPythonで統一ができるので楽ができます(現状バックエンドでmypy・Pylance・flake8・autopep8・autoflake・numdoclintのLintを使っていますがフロントエンドのPythonコードでも同じLintの仕組みやテストの仕組みを反映できます)。

今後どういった機能を実装していこうと考えているか

  • とりあえず今までの実装分ではSVGテキストすら扱えないものの、この辺は実装しないと話にならないので近いうちに対応を進めていこうと考えています(正直他の所の基底部分などの実装だけでも結構大変で時間がかかりましたが段々とこの辺も着手していきたいところです・・・)。
  • 元々Django上で使うことを想定しているのでDjangoのテンプレート上でのパラメーターやテンプレートタグと絡めた利用を想定した実装やインターフェイスを色々追加していきたいと考えています。
  • 基本的な各種クラスがもっと充実してきたらそれらを使ったコンポーネントなども追加していきたいと思っています。基本的なチャートライブラリもこのライブラリ上で扱えるようにして、このライブラリでも完結できることを多くしつつも自前で色々カスタマイズできるようにしていきたいなと思っています。
  • MITライセンスなどで公開されているSVGアイコンなども使わせていただいて、それらを同じ属性や二メーションのインターフェイスで使えるようにしたいなと思っています。
  • 結構先の話になりますが、SVG以外の領域のものも勉強して組み込んでいけたら・・・と思っています(Canvas関係やProcessing周り、3Dなど)。

使い方の詳細

以降の節では現状までの実装分の使い方について詳しく触れていきます。長くなりますのでご注意ください。

apyscのプロジェクトの作り方

とりあえず現状ではコマンドなどは挟んでおらず、任意のPythonスクリプトを実行する形で動くようになっています。

大体のケースではmain.pyといったエントリーポイントを記述したファイルを設けて、if __name__ == '__main__':という分岐を入れつつ以下のような感じで書き始める形を想定しています。

def main() -> None:
    print('Hello apysc!')


if __name__ == '__main__':
    main()

importの慣習

NumPyをnp、Pandasをpd、Tkinterをtk・・・といった名前を付けてモジュールをインポートすることが慣習的に多い感じですが、似たような感じでapyscもapという名前で使う形でドキュメントなどは扱っています。

また、基本的に必要なクラスや関数などはルートのパッケージに入れてありそれだけimportすれば全体的に機能が使えるようになっています。

import apysc as ap


def main() -> None:
    ...


if __name__ == '__main__':
    main()

以下のように必要なものがルートのパッケージに色々入っています。

image.png

描画領域の作成

全体の描画領域となるインスタンスはStageというクラスを使って作れるようになっています。全体のコンテナー的にお考えください。これは一番最初に作る必要があります。

引数でサイズや背景色・出力されるHTML要素のID設定などができるようになっています。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')


if __name__ == '__main__':
    main()

HTMLの保存

HTMLの保存はsave_overall_htmlという関数で行えます。HTMLの出力先のディレクトリパスの指定は必須です。基本的に記述の最後に呼び出します。引数設定でminify設定を無効化したり、HTMLと同じディレクトリに出力される利用しているjsライブラリを1つのHTMLファイルとしてまとめたりといったオプションが設けてあります。

※将来的にはDJangoなどでの利用等も加味してjsのスクリプト部分のみでの保存などもサポートしようと思っています。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

上記コードではStageをインスタンス化しただけなので、HTMLを表示してみても150 x 150のグレーの背景が表示されるだけになっています。

image.png

コンテナ要素の作成

各SVG要素などを格納したりするためのクラスはSpriteという名前を付けています。引数には紐づける対象のStageのインスタンスを指定します(インスタンス化した時点でそのStageへ追加されるようになっています)。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

また、このクラスはgraphicsという属性を持っており塗りや線の設定を行ったりSVGの図形を描写したりすることができます。描写した図形などはこのSprite内に格納される形になります(つまり複数の図形を単一のSpriteで描写してそのSpriteを動かすと一通りの図形が移動します)。

例えばシアンの色を設定して50 x 50の四角形を描画したい場合は以下のようになります(この辺は後々の節で詳しく触れます)。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')
    rectangle: ap.Rectangle = sprite.graphics.draw_rect(
        x=50, y=50, width=50, height=50)
    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

Jupyter notebookもしくはJupyterLabやColaboratory上で使う場合

HTMLファイルとして出力して使うのではなくJupyter上などで使いたい場合はdisplay_on_jupyter関数で表示することができます。処理の過程で一度HTMLを保存しているのでそのパスなどが引数で必要になります。

import apysc as ap


stage: ap.Stage = ap.Stage(
    stage_width=150, stage_height=150, background_color='#333',
    stage_elem_id='sample_stage')
sprite: ap.Sprite = ap.Sprite(stage=stage)
sprite.graphics.begin_fill(color='#0af')
rectangle: ap.Rectangle = sprite.graphics.draw_rect(
    x=50, y=50, width=50, height=50)
ap.display_on_jupyter(stage=stage, html_file_name='output.html')

image.png

※Jupyter notebookとJupyterLab問わずこの関数で扱えますが、VS Code上のJupyterは前述の通りまだサポートできていません。

Colaboraty上で表示したい場合にはdisplay_on_colaboratory関数で対応ができます。

image.png

他のモジュールや関数などの呼び出し

ほぼほぼのケースで関数1つで終わることはなく、大体他のモジュールとかに記述を分割したり関数を分割したり・・・となってくると思いますが、勿論他の関数などを呼び出しても出力結果には反映されます。大体のケースでStageのインスタンスは渡す必要は出てくると思います。

また、保存処理(save_overall_html関数など)は最後に来る形は維持する必要があります。

以下のコードでは別の関数でマゼンタ色の別の四角を描写しています。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=250, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')
    sprite.graphics.draw_rect(x=50, y=50, width=50, height=50)
    _draw_another_rectangle(stage=stage)
    ap.save_overall_html(dest_dir_path='./output/')


def _draw_another_rectangle(stage: ap.Stage) -> None:
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#f0a')
    sprite.graphics.draw_rect(x=150, y=50, width=50, height=50)


if __name__ == '__main__':
    main()

image.png

塗りと線の設定

塗りの設定

塗りの設定はgraphics属性のbegin_fillメソッドで扱います。フォトショやイラレなどのAdobeのツールの塗り設定とお考えください。この設定以降に描画した図形などはこの別の設定を行うまで塗り設定が保持されます(同じ塗りが反映されます)。

※後々の節で触れますが、作成後の図形は後で個別に塗り設定などを変更することは可能です。

begin_fillメソッドは塗りのカラーコードと透明度(省略した場合は不透明)の設定を行うことができます。カラーコードは16進数の記述で#cccccc#0af#0といったweb界隈で一般的な記述が指定可能です。0afといったように#の記号を省略しても受け付けることができます。

以下のサンプルでは幅100の2つの四角を半分重なる形で、且つ透明度を50%で描写しています。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=250, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af', alpha=0.5)
    sprite.graphics.draw_rect(x=50, y=50, width=100, height=50)
    sprite.graphics.begin_fill(color='#ff00aa', alpha=0.5)
    sprite.graphics.draw_rect(x=100, y=50, width=100, height=50)
    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

線の設定

線の設定はline_styleメソッドで行います。各挙動は基本的に塗りの設定と同じで、設定を再度行わない限り複数の図形で設定が引き継がれます(生成後の個別の変更も同様に効きます)。

基本的な設定として線色(color)、線幅(thickness)、透明度(alpha・省略時は不透明)の引数を持ちます。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.line_style(color='#fff', thickness=5, alpha=0.7)
    sprite.graphics.draw_rect(x=50, y=50, width=50, height=50)
    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

線の端の設定も行うことができます。LineCapsというenumを定義してあるのでそちらをcap引数に指定することで設定することができます。enumは以下の3つを用意してあります。

  • BUTT: デフォルトの挙動であり、線の端に設定は追加されません。
  • ROUND: 線の端に円形が追加されます。
  • SQUARE: 線の端が四角が追加されます。BUTTと似ていますが、線幅などに応じて追加される分長さが大きくなります。

※以下のコードでは線描写のインターフェイス(draw_lineやmove_to, line_toなど)を使っていますがこの辺は後々の節で触れます。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=200, stage_height=180, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.line_style(
        color='#0af', thickness=20, cap=ap.LineCaps.BUTT)
    sprite.graphics.draw_line(x_start=50, x_end=150, y_start=50, y_end=50)

    sprite.graphics.line_style(
        color='#0af', thickness=20, cap=ap.LineCaps.ROUND)
    sprite.graphics.draw_line(x_start=50, x_end=150, y_start=90, y_end=90)

    sprite.graphics.line_style(
        color='#0af', thickness=20, cap=ap.LineCaps.SQUARE)
    sprite.graphics.draw_line(x_start=50, x_end=150, y_start=130, y_end=130)

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

線の連結部分(折れ線などでの折れている部分)のスタイル設定もjoints引数で行えます。enumでLineJointsというクラスを定義してあるのでそちらを指定します。以下の定義が存在します。

  • MITER: デフォルトの設定であり、尖った感じの連結部分の表示になります。
  • ROUND: 丸い連結部分の表示になります。
  • BEVEL: Adobeのツールとかだと良く出てくるベベルエッジ(もしくはsloping verticesとか射角・面取りなどと呼ばれる)表示になります。
import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=350, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)

    sprite.graphics.line_style(
        color='#0af', thickness=10, joints=ap.LineJoints.MITER)
    sprite.graphics.move_to(x=50, y=100)
    sprite.graphics.line_to(x=75, y=50)
    sprite.graphics.line_to(x=100, y=100)

    sprite.graphics.line_style(
        color='#0af', thickness=10, joints=ap.LineJoints.ROUND)
    sprite.graphics.move_to(x=150, y=100)
    sprite.graphics.line_to(x=175, y=50)
    sprite.graphics.line_to(x=200, y=100)

    sprite.graphics.line_style(
        color='#0af', thickness=10, joints=ap.LineJoints.BEVEL)
    sprite.graphics.move_to(x=250, y=100)
    sprite.graphics.line_to(x=275, y=50)
    sprite.graphics.line_to(x=300, y=100)

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

その他にもdot_setting引数でドット線、dash_setting引数でダッシュ線、round_dot_setting引数で丸ドット線、dash_dot_setting引数で一点鎖線などに変更することもできます。それぞれLineDotSetting、LineDashSetting、LineRoundDotSetting、LineDashDotSettingといった設定値用のクラスがあるのでそれらを指定します。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=300, stage_height=100, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.line_style(
        color='#0af', thickness=5, dot_setting=ap.LineDotSetting(dot_size=2))
    sprite.graphics.move_to(x=50, y=50)
    sprite.graphics.line_to(x=250, y=50)

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

各図形の描画

以降の節では各図形描画のインターフェイスについて触れていきます。共通してgraphics属性を使います。

四角の描画

前述までの節でも何度も使ってきましたがdraw_rectメソッドを使います。引数にはX座標、Y座標、幅、高さが必要になります。Rectangleクラスのインスタンスが返却されます。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')
    rectangle: ap.Rectangle = sprite.graphics.draw_rect(
        x=50, y=50, width=50, height=50)

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

角丸の四角の描画

draw_round_rectメソッドが角丸の四角を描画できます。draw_rectメソッドと比べて角丸のサイズとしてのellipse_widthとellipse_heightの引数が増えています。返却値はdraw_rectと同様Rectangleクラスのインスタンスとなります。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')
    rectangle: ap.Rectangle = sprite.graphics.draw_round_rect(
        x=50, y=50, width=50, height=50,
        ellipse_width=10, ellipse_height=20)

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

円の描画

draw_circleメソッドで円を描画することができます。円の中心のX座標、Y座標、半径の引数が必要です。返却値はCircleクラスのインスタンスになります。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')
    circle: ap.Circle = sprite.graphics.draw_circle(
        x=75, y=75, radius=25)

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

楕円の描画

draw_ellipseメソッドで楕円を描画することができます。円の中心のX座標、Y座標、楕円の幅と高さの引数が必要です。Ellipseクラスのインスタンスが返却されます。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')
    ellipse: ap.Ellipse = sprite.graphics.draw_ellipse(
        x=75, y=75, width=100, height=50)

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

折れ線の描画

折れ線に関してはmove_toメソッドで線の開始位置を指定し、line_toメソッドを複数回呼び出すことで描画できます。line_toを1回以上呼んだ後にmove_toを再度呼び出すと新しい折れ線のインスタンスが生成されます。move_to及びline_toそれぞれでPolylineクラスのインスタンスが返ります(line_to側はmove_to呼び出し時と同じインスタンスが返ります)。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.line_style(color='#fff', thickness=3)
    polyline: ap.Polyline = sprite.graphics.move_to(x=50, y=50)
    sprite.graphics.line_to(x=100, y=50)
    sprite.graphics.line_to(x=50, y=100)
    sprite.graphics.line_to(x=100, y=100)

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

線の描画

シンタックスシュガー的なものになりますが、折れ線ではなく単純な直線であればdraw_lineメソッドでも描画することができます。線の開始位置の終了位置のそれぞれX座標とY座標の引数が必要になります。返却値はLineクラスのインスタンスとなります。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=100, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.line_style(color='#fff', thickness=5)
    line: ap.Line = sprite.graphics.draw_line(
        x_start=50, y_start=50, x_end=100, y_end=50)

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

※以降のドットの直線なども同様に記述をシンプルにするためのインターフェイスが設けてあります。
※それらの直線のインターフェイスも含めて、line_styleのドット設定などは無視されます(各インターフェイスで直接引数で設定ができるようになっています)。

ドット線の描画

draw_dotted_lineメソッドでドット線の直線を描画することができます。ドットサイズの引数が追加で必要になっています。返却値は変わらずLineクラスのインスタンスとなります。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=100, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.line_style(color='#fff', thickness=5)
    line: ap.Line = sprite.graphics.draw_dotted_line(
        x_start=50, y_start=50, x_end=100, y_end=50, dot_size=3)

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

ダッシュ線の描画

draw_dashed_lineメソッドでダッシュ線の直線を描画することができます。ダッシュのサイズとスペース(空白部分)のサイズの引数が追加になっています。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=100, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.line_style(color='#fff', thickness=3)
    line: ap.Line = sprite.graphics.draw_dashed_line(
        x_start=50, y_start=50, x_end=100, y_end=50,
        dash_size=6, space_size=3)

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

丸ドット線の描画

draw_round_dotted_lineメソッドで丸ドットの直線が描画できます。円のサイズと円の間のスペースの引数の指定が必要になります。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=250, stage_height=100, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.line_style(color='#fff', thickness=3)
    line: ap.Line = sprite.graphics.draw_round_dotted_line(
        x_start=50, y_start=50, x_end=200, y_end=50,
        round_size=6, space_size=3)

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

一点鎖線の描画

draw_dash_dotted_lineメソッドで一点鎖線を描画することができます。ドットサイズ、ダッシュサイズ、スペースのサイズの3つを指定することができます。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=250, stage_height=100, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.line_style(color='#fff', thickness=3)
    line: ap.Line = sprite.graphics.draw_dash_dotted_line(
        x_start=50, y_start=50, x_end=200, y_end=50,
        dot_size=2, dash_size=6, space_size=3)

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

多角形の描画

draw_polygonメソッドで多角形を描画することができます。引数のpointsには各頂点をリストで指定する必要があります。各頂点の座標にはPoint2DというXとY座標の値を保持するクラスの値を指定します。返却値はPolygonクラスのインスタンスとなります。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')
    polygon: ap.Polygon = sprite.graphics.draw_polygon(
        points=[
            ap.Point2D(x=50, y=50),
            ap.Point2D(x=100, y=75),
            ap.Point2D(x=50, y=100)
        ])

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

apysc上での基本的なデータクラスについて

apyscではPythonビルトインの基本的なデータクラス(intやlistなど)に加えて独自のデータクラスも使用しています。

これは、jsなどに変換する際に内部の実装的に変動しない値であればビルトインのもので問題が無いものの、特定のイベントなどで変動する値などはビルトインだと追うのが難しいため若干煩雑ではありますがこのような仕様に現在しています。例えばマウスイベントの度に値が変動する変数・・・といった場合にラッパー的に値のクラスを設けていないと実装が難しい・・・といった具合です。

そのため、

  • 固定値で扱えばOKなもの(四角の固定のサイズだったりアニメーションの最終位置など) -> Pythonビルトインの値で問題はない
  • イベントなどで途中で変動する前提の値など -> apysc独自のデータの型

を使うことが基本的に必要になってきます。各クラスはPythonに近い感じで書けたりjsなどにある程度寄せたり・・・といった具合に組んであります。

整数の型

apyscの整数の型はIntクラスとして定義してあります。

import apysc as ap

int_val: ap.Int = ap.Int(10)
print(int_val)
10

基本的な演算に関してはPythonのビルトインのintに近い形で行えます(一部できない計算が出来ない条件もあります)。

import apysc as ap

int_val: ap.Int = ap.Int(10)
added_int: ap.Int = int_val + 20
print(added_int)
30
import apysc as ap

added_int: ap.Int = ap.Int(30) + ap.Int(20)
print(added_int)
50
import apysc as ap

int_val: ap.Int = ap.Int(10)
int_val += 50
print(int_val)
60

比較演算もPythonビルトインに近い感じにしてあります。ただし返却値はビルトインのbool型ではなく後述するBoolean型の値などに都合が良いためなっています。

import apysc as ap

int_val: ap.Int = ap.Int(10)
print(int_val == 10)
Boolean(True)
import apysc as ap

int_val: ap.Int = ap.Int(10)
print(int_val == ap.Int(10))
Boolean(True)
import apysc as ap

int_val: ap.Int = ap.Int(10)
print(int_val > ap.Int(9))
Boolean(True)

浮動小数点数の型

浮動小数点数のクラスにはNumberというクラスを設けてあります。扱う値が浮動小数点数になっている以外の各挙動は大体Intクラスと同じです。

import apysc as ap

number: ap.Number = ap.Number(10.5)
print(number)
10.5
import apysc as ap

number: ap.Number = ap.Number(10.5)
number /= 3
print(number)
3.5

文字列の型

文字列に関してはStringクラスを設けてあります。

import apysc as ap

string: ap.String = ap.String('Hello')
print(string)
Hello
import apysc as ap

string: ap.String = ap.String('Hello')
string += ' apysc!'
print(string)
Hello apysc!

真偽値の型

真偽値の型はBooleanという型を設けてあります。

import apysc as ap

boolean: ap.Boolean = ap.Boolean(True)
print(boolean)
Boolean(True)

配列の型

配列の型にはArrayというクラスを設けてあります。

import apysc as ap

arr: ap.Array = ap.Array([1, 2, 3])
print(arr)
[1, 2, 3]

単一のインデックスへの参照などはPythonに近い感じで行えます(ただし添え字によるスライスなどには現在対応していません。sliceメソッドなどは別途用意してあり、そちらで配列範囲などは絞れるようにはなっています)。

import apysc as ap

arr: ap.Array = ap.Array([1, 2, 3])
print(arr[1])
2

辞書の型

辞書にはDictionaryというクラスを設けてあります。

import apysc as ap

dictionary: ap.Dictionary = ap.Dictionary({'a': 10, 'b': 20})
print(dictionary)
{'a': 10, 'b': 20}

for文の制御

固定のループであれば通常通りのループで対応ができます。例えば以下のように3個の固定の四角を描写する・・・といった場合にはビルトインのfor文で対応ができます。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=350, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')
    for i in range(3):
        sprite.graphics.draw_rect(
            x=i * 100 + 50, y=50, width=50, height=50)

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

一方でアニメーションやマウスイベントでの動的な実装、例えばクリックの度に四角が増え続けて且つそれらの四角に対してループで制御を行いたい・・・といった制御が必要な場合にはapyscのForクラスの利用が必要になります。

現在withステートメントと一緒に使う形で実装してあります。as部分には引数にArrayを指定した場合にはインデックスの値、辞書を指定した場合はキーの値が返るようにしてあります。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=350, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')
    arr: ap.Array = ap.Array(range(3))
    with ap.For(arr) as i:
        sprite.graphics.draw_rect(
            x=i * 100 + 50, y=50, width=50, height=50)

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

こちらのコードを動かした場合はビルトインのfor文で回したときと結果は同じになっていますが、一方で出力結果のHTML側でもfor文や変数参照などがされるようになっており、動的な実装が必要な時でも対応ができるようになっています。

image.png

if文の制御

if文もfor文と同様に固定の制御であればPython上の制御で大丈夫な一方で動的な実装(マウスイベントなどで参照する変数内容が変わるなど)の場合にはapyscのIfクラスなどを利用する必要があります。こちらもwithステートメントを利用する形にしてあります。Ifクラスなどの引数にはBoolean型の値が必要になります。

以下のコードではIfクラスでの分岐を加えています。固定でFalseの値を設定しているので必ず処理が通らない形となり、四角が描画されていません。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')

    condition: ap.Boolean = ap.Boolean(False)
    with ap.If(condition):
        sprite.graphics.draw_rect(x=50, y=50, width=50, height=50)

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

Elif、Elseのクラスも存在します。それぞれIfもくしはElifクラスのwithステートメント直後じゃないとエラーになるようにしてあります。

また、注意点として現在の実装ではElifの引数内でBooleanクラスなどを生成したりするとエラーになるようになっています(Booleanインスタンス生成タイミングでjsのコードが追加されてしまうため生成されるjsコードでelseの前に処理が入ってしまうため・・・)。Elifのコンストラクタなどの外でBooleanの値を変数などに持って使う必要があります(この辺は少々微妙なので将来うまいことできれば修正するかもしれません・・・)。

以下のコードではIfによるwithステートメントのシアン色の四角ではなくElseのwithステートメント内のマゼンタの四角の方が描画されています。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)

    condition: ap.Boolean = ap.Boolean(False)
    with ap.If(condition):
        sprite.graphics.begin_fill(color='#0af')
        sprite.graphics.draw_rect(x=50, y=50, width=50, height=50)
    with ap.Else():
        sprite.graphics.begin_fill(color='#f0a')
        sprite.graphics.draw_rect(x=50, y=50, width=50, height=50)

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

図形の各属性やメソッドのインターフェイス

以降の節では図形などの表示系のクラスが持つ各属性(一部メソッド)のインターフェイスについて触れていきます。

各属性の設定の共通仕様

以降の節で触れる、メソッドではない各属性値が持っている共通仕様について触れておきます。

まずはsetter/getterの通常利用ですが、基本的に型はapyscのクラス(IntやStringなど)が必要になります。Pythonのビルトインのものは型アノテーション的に弾かれるようになっています。整数の属性であればsetterはビルトインのintもしくはapyscのInt、getterはapyscのInt・・・としたかったのですが、mypyやPylanceなどでの型チェックの挙動を考えるとgetterとsetterの型を統一しないとストレートに行かなそうだったためapysc側の型で統一してあります。

例えばX座標の属性であれば以下のようにapyscのIntで指定することで型チェックに通ります。

rectangle.x = ap.Int(50)

各属性は+=や-=などのオペレーターでも計算することができます。これらはタイマーなどでアニメーションさせたりする際にも便利です。また、こちらは(getter側の型統一などを考えなくて良いので)Pythonビルトインの整数などのデータも使うことができます。

例えば以下のコードではタイマーで短い周期でX座標を1ずつ加算してアニメーションさせています(タイマー関係などについては後々の節で詳しく触れます)。

from typing_extensions import TypedDict

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=300, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')
    rectangle: ap.Rectangle = sprite.graphics.draw_rect(
        x=0, y=50, width=50, height=50)
    options: RectOption = {'rectangle': rectangle}
    ap.Timer(on_timer, delay=ap.FPS.FPS_60, options=options).start()

    ap.save_overall_html(dest_dir_path='./output/')


class RectOption(TypedDict):
    rectangle: ap.Rectangle


def on_timer(e: ap.TimerEvent, options: RectOption) -> None:
    options['rectangle'].x += 1


if __name__ == '__main__':
    main()

attribute_animation.gif

XとY座標

XとY座標はxとy属性で設定・取得することができます。型はIntとなります。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')
    rectangle: ap.Rectangle = sprite.graphics.draw_rect(
        x=0, y=0, width=50, height=50)
    rectangle.x = ap.Int(50)
    rectangle.y = ap.Int(50)

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

表示・非表示

表示・非表示の設定はvisible属性で設定・取得が可能です。型はBooleanとなります。

以下のコードでは非表示(visible = False)としているため四角は画面に表示されなくなります。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')
    rectangle: ap.Rectangle = sprite.graphics.draw_rect(
        x=50, y=50, width=50, height=50)
    rectangle.visible = ap.Boolean(False)

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

中心座標での回転

rotation_around_center属性で中央座標を基準とした回転量を設定することができます。型はIntとなります。

以下のコードでは半透明な四角を3つ描画し、それぞれ30度ずつずらす形で回転量を設定しています。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af', alpha=0.3)
    for i in range(3):
        rectangle: ap.Rectangle = sprite.graphics.draw_rect(
            x=50, y=50, width=50, height=50)
        rectangle.rotation_around_center = ap.Int(i * 30)

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()
import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af', alpha=0.3)
    for i in range(3):
        rectangle: ap.Rectangle = sprite.graphics.draw_rect(
            x=50, y=50, width=50, height=50)
        rectangle.rotation_around_center = ap.Int(i * 30)

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

任意の座標での回転

任意の座標での回転量の取得と設定を行うにはget_rotation_around_pointメソッドとset_rotation_around_pointを使います。X座標とY座標の引数が必要なため属性ではなくメソッドになっています。

以下のコードサンプルでは四角の右下を基準に回転量を設定しています(四角の座標が50px、幅や高さも50pxなので100pxの位置で右下基準となります)。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af', alpha=0.3)
    for i in range(3):
        rectangle: ap.Rectangle = sprite.graphics.draw_rect(
            x=50, y=50, width=50, height=50)
        rectangle.set_rotation_around_point(
            rotation=ap.Int(i * 30), x=ap.Int(100), y=ap.Int(100))

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

中心座標での拡大・縮小

中心座標での拡大縮小の取得や設定をしたい場合にはscale_x_from_center(X方向の拡大縮小)とscale_y_from_center(Y方向の拡大縮小)属性を使います。

以下のサンプルコードではX座標はそのまま、ループ内で各四角を縮小しています(分かりやすいようにY座標はずらしています)。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=350, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')
    for i in range(3):
        rectangle: ap.Rectangle = sprite.graphics.draw_rect(
            x=50, y=100 * i + 50, width=50, height=50)
        rectangle.scale_x_from_center -= i * 0.3

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

任意の座標での拡大・縮小

任意の座標での拡大縮小をしたい場合にはget_scale_x_from_point(X方向の取得)、set_scale_x_from_point(X方向の設定)、get_scale_y_from_point(Y方向の取得)、set_scale_y_from_point(Y方向の設定)の各メソッドを使います。拡大縮小の基準位置となるXもしくはY座標が引数に必要になります。

以下のサンプルコードでは各四角に対して四角の左端の位置を基準として段々大きくなるように拡大設定をしています。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=200, stage_height=350, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')
    for i in range(3):
        rectangle: ap.Rectangle = sprite.graphics.draw_rect(
            x=50, y=100 * i + 50, width=50, height=50)
        rectangle.set_scale_x_from_point(
            scale_x=ap.Number(1 + i * 0.5), x=ap.Int(50))

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

反転

反転させる、もしくは現在の判定状況を取得するにはflip_xとflip_y属性を使います。型はBooleanとなります。反転は要素の中央位置を基準に実行されます。

以下のコードサンプルでは2つの斜めの線を同じ座標設定で描画しています。2つ目の線のみflip_x属性で反転しているため結果的には×の表示となります。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.line_style(color='#fff', thickness=3)
    line_1: ap.Line = sprite.graphics.draw_line(
        x_start=50, y_start=50, x_end=100, y_end=100)
    line_2: ap.Line = sprite.graphics.draw_line(
        x_start=50, y_start=50, x_end=100, y_end=100)
    line_2.flip_x = ap.Boolean(True)

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

歪み

skew_xとskew_y属性で歪みの値の取得と更新を行うことができます。

以下のコードサンプルでは3つの四角を描画しており、左の四角が歪み無し、中央の四角がX方向への歪みを設定、右の四角にはY方向への歪みを設定しています。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=350, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')

    rectangle_1: ap.Rectangle = sprite.graphics.draw_rect(
        x=50, y=50, width=50, height=50)

    rectangle_2: ap.Rectangle = sprite.graphics.draw_rect(
        x=150, y=50, width=50, height=50)
    rectangle_2.skew_x = ap.Int(30)

    rectangle_3: ap.Rectangle = sprite.graphics.draw_rect(
        x=250, y=50, width=50, height=50)
    rectangle_3.skew_y = ap.Int(30)

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

塗りの色

要素作成前のbegin_fillメソッドでの塗り設定の他にも、要素作成後にfill_color属性で塗りの色を取得したり設定したりすることができます。型はStringとなります。

以下のサンプルコードでは四角の作成時はシアン色で作成し、作成後にfill_color属性を使ってマゼンタ色に変更しています。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')

    rectangle: ap.Rectangle = sprite.graphics.draw_rect(
        x=50, y=50, width=50, height=50)
    rectangle.fill_color = ap.String('#f0a')

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

塗りの透明度

塗りの色と同様に塗りの色に関してもfill_alpha属性で要素生成後に取得や変更を行うことができます。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')

    rectangle: ap.Rectangle = sprite.graphics.draw_rect(
        x=50, y=50, width=50, height=50)
    rectangle.fill_alpha = ap.Number(0.3)

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

線の色

line_color属性で途中で線の色を取得したり更新したりすることができます。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=250, stage_height=100, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.line_style(color='#fff', thickness=10)

    line: ap.Line = sprite.graphics.draw_line(
        x_start=50, y_start=50, x_end=200, y_end=50)
    line.line_color = ap.String('#0af')

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

線の透明度

line_alpha属性で途中で線の透明度を取得したり更新することができます。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=250, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.line_style(color='#fff', thickness=10)

    line_1: ap.Line = sprite.graphics.draw_line(
        x_start=50, y_start=50, x_end=200, y_end=100)
    line_1.line_alpha = ap.Number(0.3)

    line_2: ap.Line = sprite.graphics.draw_line(
        x_start=50, y_start=100, x_end=200, y_end=50)
    line_2.line_alpha = ap.Number(0.3)

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

線幅

線幅はline_thickness属性で途中で取得や変更ができます。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=250, stage_height=100, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.line_style(color='#fff', thickness=1)

    line: ap.Line = sprite.graphics.draw_line(
        x_start=50, y_start=50, x_end=200, y_end=50)
    line.line_thickness = ap.Int(15)

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

線の種類の調整

線の種類(ドット線など)の設定の途中での取得や設定はline_dot_setting、line_dash_setting、line_round_dot_setting、line_dash_dot_setting属性で行えます。

以下のコードサンプルではline_dot_settingを途中で設定してドット線に変更しています。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=250, stage_height=100, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.line_style(color='#fff', thickness=5)

    line: ap.Line = sprite.graphics.draw_line(
        x_start=50, y_start=50, x_end=200, y_end=50)
    line.line_dot_setting = ap.LineDotSetting(dot_size=5)

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

CSS設定

基本的には直接のCSSが不要になるようにじわりじわりとインターフェイスを増やしていこう・・・とは考えていますが、一方でサポートができていないものも山ほどありますのでCSS関係のインターフェイスも設けてあります。

取得にはget_css、設定にはset_cssメソッドを使います。name引数にはCSS名、value引数にはCSSに設定する値を指定します。

以下のコードサンプルではCSSのdisplayにnoneを指定しているため四角が表示されなくなっています(visible属性と同じような挙動をします)。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')
    rectangle: ap.Rectangle = sprite.graphics.draw_rect(
        x=50, y=50, width=50, height=50)
    rectangle.set_css(name='display', value='none')

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

image.png

イベントハンドラ

様々なインターフェイスでイベントハンドラ登録用のものが用意してあります。例えばクリックやマウスオーバー、毎タイマー、タイマー終了時、アニメーション終了時などです。

以降の節ではイベントハンドラ関係について触れていきます。

イベントハンドラの基本

ハンドラの関数(もしくはメソッド)では共通して第一引数(メソッドの場合にはselfは除く)はイベントのインスタンス(大体引数名はeとしています。型はMouseEventだったりTimerEventだったりとインターフェイスによって様々です)、第二引数にはオプションとなる各データを格納する辞書(省略可)となります。

以下のコードではクリックイベントを登録しており、クリックする度に四角の色が変わるようにしてあります(一部まだ触れていないジェネリック関係などを設定していますがその辺は後々触れます)。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=200, stage_height=200, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')
    rectangle: ap.Rectangle = sprite.graphics.draw_rect(
        x=50, y=50, width=100, height=100)
    rectangle.click(on_click)

    ap.save_overall_html(dest_dir_path='./output/')


def on_click(e: ap.MouseEvent[ap.Rectangle], options: dict) -> None:
    condition: ap.Boolean = e.this.fill_color == '#00aaff'
    with ap.If(condition):
        e.this.fill_color = ap.String('#f0a')
    with ap.Else():
        e.this.fill_color = ap.String('#0af')


if __name__ == '__main__':
    main()

rect_click.gif

イベントインスタンスのthis属性

ハンドラの第一引数に指定されるイベントインスタンスのthis属性にはイベントを設定したインスタンスが設定されます。

例えば以下のように四角に設定したクリックのイベントでのthis属性をprintで出力してみると四角関係の情報が表示されることを確認できます。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')
    rectangle: ap.Rectangle = sprite.graphics.draw_rect(
        x=50, y=50, width=50, height=50)
    rectangle.click(on_click)

    ap.save_overall_html(dest_dir_path='./output/')


def on_click(e: ap.MouseEvent, options: dict) -> None:
    print(e.this)


if __name__ == '__main__':
    main()
Rectangle('rect_1')
...

また、this属性はそのままだとAnyの型が設定されますが、ジェネリックなどで対象とする型を明示することができるイベントクラスも存在します。もし登録先のインスタンスの型が決まっているようであればジェネリックの型指定もすると入力補完や型チェック面でより一層開発体験が良くなります(詳細は各イベントインスタンスの節で補足していきます)。

マウスイベントも該当し、例えば四角のインスタンスに登録されるのであればap.MouseEvent[ap.Rectangle]といった指定ができます。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')
    rectangle: ap.Rectangle = sprite.graphics.draw_rect(
        x=50, y=50, width=50, height=50)
    rectangle.click(on_click)

    ap.save_overall_html(dest_dir_path='./output/')


def on_click(e: ap.MouseEvent[ap.Rectangle], options: dict) -> None:
    print(e.this)


if __name__ == '__main__':
    main()

VS Code上などでthis属性にマウスオーバーしてみるとちゃんとRectangle型として認識してくれています。

image.png

イベントハンドラでの辞書の引数パラメーターに対するTypedDictについて

ハンドラの第二引数に渡されるオプションとしての辞書のパラメーターにはTypedDictを指定することもできます。辞書のキーと型を明示できるためmypyやPylance(Pyright)などの型チェッカーを利用している場合型のミスなどを事前検知しやすくなります。

以下のコードサンプルではmessageという文字列のキーを持つTypedDictをオプションの引数に指定しています。TypedDictを使うことで例えばうっかりキー名をmessageではなくmsgとしてしまって渡していた・・・といったことやハンドラ内でのキーの参照のミスなどを検知しやすくなります。

from typing_extensions import TypedDict

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')
    rectangle: ap.Rectangle = sprite.graphics.draw_rect(
        x=50, y=50, width=50, height=50)
    options: MessageOptions = {'message': 'Hello apysc!'}
    rectangle.click(on_click, options=options)

    ap.save_overall_html(dest_dir_path='./output/')


class MessageOptions(TypedDict):
    message: str


def on_click(e: ap.MouseEvent[ap.Rectangle], options: MessageOptions) -> None:
    ap.trace(options['message'])


if __name__ == '__main__':
    main()

ハンドラ内でのキーのミスを気づいたり補完面などのメリットもそうですが、ハンドラ登録時のオプションの辞書の指定でもミスに気づきやすくなります。例えば以下のように型が合っていない辞書を指定するとPylanceなどで型チェックを有効化していればエラーが可視化されます。

    ...
    rectangle.click(
        on_click, options={'msg': 'Hello apysc!'})
    ...

image.png

※補足: Python 3.8以降のバージョンであればtyping_extensionsパッケージではなくビルトインのtypingパッケージからTypedDictをimportすることができます。apyscインストール時にバックポート用のtyping_extensionsも一緒にインストールされるようになっていますが、3.6や3.7などのPythonバージョンでも使う場合にはtyping_extensions側からimportする必要があります。

マウスイベント

以降の節ではクリックやマウスオーバーなどのイベントについて詳しく触れていきます。現在対応しているのはクリック・ダブルクリック・マウスダウン・マウスアップ・マウスオーバー・マウスアウト・マウスムーブです。

マウスホイールに関しては一応現時点でも画面全体に対するものは対応しているのですが、個別の要素などに対してはまだ対応できていません。恐らく将来その辺に手を加えると思われます。

マウスイベントにおけるカーソル位置の取得

ハンドラに渡されるイベントインスタンス(MouseEvent)は以下のようなカーソル位置の座標を取得するための属性を持っています。

  • local_x: その要素内でのX座標
  • local_y: その要素内でのY座標
  • stage_x: Stage基準でのX座標
  • stage_y: Stage基準でのY座標

例えばX=100の座標に配置された四角をクリックした際に、そのクリック位置が四角内で10pxのX位置にあったとするとlocal_xは10、stage_xは110となります。

以下のコードサンプルではクリック時に各座標をコンソールに出力しています。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')
    rectangle: ap.Rectangle = sprite.graphics.draw_rect(
        x=50, y=50, width=50, height=50)
    rectangle.click(on_click)

    ap.save_overall_html(dest_dir_path='./output/')


def on_click(e: ap.MouseEvent[ap.Rectangle], options: dict) -> None:
    ap.trace('local_x:', e.local_x)
    ap.trace('local_y:', e.local_y)
    ap.trace('stage_x:', e.stage_x)
    ap.trace('stage_y:', e.stage_y)


if __name__ == '__main__':
    main()

ChromeのDeveloper Toolsなどで見てみるとクリックした際に以下のような内容が出力されます(四角は50の位置に配置されているので、50 + localの値がstage基準の座標になっています)。

image.png

ハンドラ登録の各インターフェイス

マウスイベントのハンドラ登録用の各メソッドのインターフェイスは以下のようなものがあります。jQueryとかでお馴染みの名前にしてあります。

  • click -> クリック時
  • dblclick -> ダブルクリック時
  • mousedown -> マウスダウン(左クリックを押した)時
  • mouseup -> マウスアップ(左クリックで押した状態を離した)時
  • mouseover -> マウスオーバー(要素上にカーソルが乗った)時
  • mouseout -> マウスアウト(要素上からカーソルが離れた)時
  • mousemove -> マウスムーブ(要素上でカーソルを動かした)時

各引数は統一して第一引数にハンドラとしての関数(もしくはメソッド)、第二引数にハンドラに渡したい辞書のパラメーター(省略可)となっています。

例えばクリックイベントを四角に対して登録したい場合には以下のようになります。四角をクリックした際にChromeなどのDeveloper Toolsのコンソール上にClicked!と表示されるようにしています。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')
    rectangle: ap.Rectangle = sprite.graphics.draw_rect(
        x=50, y=50, width=50, height=50)
    rectangle.click(on_click)

    ap.save_overall_html(dest_dir_path='./output/')


def on_click(e: ap.MouseEvent[ap.Rectangle], options: dict) -> None:
    ap.trace('Clicked!')


if __name__ == '__main__':
    main()

マウスイベントの伝搬やデフォルトの挙動の停止

イベントインスタンスはイベントの伝搬を止めるためのstop_propagationメソッドとデフォルトの挙動を止めるprevent_defaultメソッドを持っています。

例えば以下のように四角とその親となるSprite両方にクリックイベントを登録した際に、四角(子)の方でstop_propagationメソッドが呼ばれているとイベントが伝搬しなくなりSprite(親)側のイベントのハンドラの関数は呼ばれなくなります。stop_propagationの呼び出しがなければ1回のクリックで四角とSpriteのハンドラの関数の両方が呼び出されます。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')
    rectangle: ap.Rectangle = sprite.graphics.draw_rect(
        x=50, y=50, width=50, height=50)
    rectangle.click(on_rectangle_click)
    sprite.click(on_sprite_click)

    ap.save_overall_html(dest_dir_path='./output/')


def on_rectangle_click(e: ap.MouseEvent[ap.Rectangle], options: dict) -> None:
    ap.trace('Rectangle is clicked!')
    e.stop_propagation()


def on_sprite_click(e: ap.MouseEvent[ap.Sprite], options: dict) -> None:
    ap.trace('Sprite is clicked!')


if __name__ == '__main__':
    main()

マウスイベントインスタンスへのジェネリックの型アノテーション

マウスイベントのインスタンスへはe: ap.MouseEvent[ap.Rectangle]といったようにジェネリックの型アノテーションを行うことができます。指定された型はthisなどの属性に反映されます。特定の型の要素にのみに設定するハンドラ・・・といった時に便利です。

ハンドラ単体の登録の解除

unbind_ というプレフィックスを持つインターフェイスでマウスイベントのハンドラ単体の登録を解除することができます。たとえばクリックイベントであればunbind_click、マウスダウンであればunbind_mousedownといったような名前のメソッドが定義されています。

以下のコードサンプルでは四角に2つのクリックイベントのハンドラを登録していますが、1つ目のハンドラはunbind_clickメソッドで解除しているため四角をクリックすると2つ目のハンドラのみ呼び出されます(Developer Toolsのコンソール上にメッセージが表示されます)。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')
    rectangle: ap.Rectangle = sprite.graphics.draw_rect(
        x=50, y=50, width=50, height=50)
    rectangle.click(on_click_1)
    rectangle.click(on_click_2)

    rectangle.unbind_click(on_click_1)

    ap.save_overall_html(dest_dir_path='./output/')


def on_click_1(e: ap.MouseEvent[ap.Rectangle], options: dict) -> None:
    ap.trace('The first click event handler is called!')


def on_click_2(e: ap.MouseEvent[ap.Rectangle], options: dict) -> None:
    ap.trace('The second click event handler is called!')


if __name__ == '__main__':
    main()

ハンドラ全体の登録の解除

unbind_ というプレフィックスと _all というサフィックスを持つ各メソッドで一括してマウスイベントの登録を解除することができます。例えばunbind_click_allであれば全てのクリックイベントが解除され、unbind_mousedown_allであれば全てのマウスダウンのイベントが解除されるといった具合です。

以下のサンプルコードでは2つのクリックイベントのハンドラを登録していますがその後にunbind_click_allを呼び出しているため四角をクリックしても反応しなくなっています。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')
    rectangle: ap.Rectangle = sprite.graphics.draw_rect(
        x=50, y=50, width=50, height=50)
    rectangle.click(on_click_1)
    rectangle.click(on_click_2)

    rectangle.unbind_click_all()

    ap.save_overall_html(dest_dir_path='./output/')


def on_click_1(e: ap.MouseEvent[ap.Rectangle], options: dict) -> None:
    ap.trace('The first click event handler is called!')


def on_click_2(e: ap.MouseEvent[ap.Rectangle], options: dict) -> None:
    ap.trace('The second click event handler is called!')


if __name__ == '__main__':
    main()

タイマー

一定時間ごとに任意の関数を呼び出したり、指定秒後に1回だけ処理を呼び出したり・・・といった制御のためにTimerというクラスが実装してあります。以降の節ではこのTimerクラスについて色々触れていきます。

基本の使い方

Timerクラスは第一引数に登録するハンドラ、第二引数のdelay引数には実行間隔をミリ秒単位で指定します。例えばdelayに1000を指定すれば1秒ごとに第一引数に指定したハンドラの関数が呼び出されます。options引数で任意のパラメーターが辞書で渡せるのはマウスイベントなどと同様です。タイマーをスタートさせるにはstartメソッド、止める時にはstopメソッドを呼び出します。

以下のサンプルコードでは16ミリ秒ごと(約60FPS)にハンドラを呼び出すようにし、そのハンドラ内で四角のX座標をインクリメントしています。結果的にX座標のアニメーションになっています。

from typing_extensions import TypedDict

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=300, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')
    rectangle: ap.Rectangle = sprite.graphics.draw_rect(
        x=50, y=50, width=50, height=50)

    options: RectOptions = {'rectangle': rectangle}
    timer: ap.Timer = ap.Timer(on_timer, delay=16, options=options)
    timer.start()

    ap.save_overall_html(dest_dir_path='./output/')


class RectOptions(TypedDict):
    rectangle: ap.Rectangle


def on_timer(e: ap.TimerEvent, options: RectOptions) -> None:
    options['rectangle'].x += 1


if __name__ == '__main__':
    main()

timer_basic.gif

FPSのenum

タイマーのdelay引数にはFPSという用意されているenumを指定することができます。FPS_60やFPS_30といった定数が含まれています。

これらを指定した場合各FPSに相当するミリ秒に展開されます。例えばFPS_60を指定すると約16ミリ秒ごとにタイマーが実行(1秒ごとに約60回実行)されるようになります。FPS_30と指定すれば約33ミリ秒ごとにタイマーが実行されるようになります。

...
    options: RectOptions = {'rectangle': rectangle}
    timer: ap.Timer = ap.Timer(
        on_timer, delay=ap.FPS.FPS_60, options=options)
    timer.start()
...

呼び出し回数の制御

タイマーのrepeat_count引数を指定すると、ここに指定された回数タイマーが実行されたらタイマーが停止します。例えば1回だけ実行してほしい場合には1、5回実行したら停止して欲しければ5を指定します。

以下のコードサンプルでは3秒後に1回だけタイマーを実行し、四角の色を変えています。

from typing_extensions import TypedDict

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=150, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')
    rectangle: ap.Rectangle = sprite.graphics.draw_rect(
        x=50, y=50, width=50, height=50)

    options: RectOptions = {'rectangle': rectangle}
    timer: ap.Timer = ap.Timer(
        on_timer, delay=3000, repeat_count=1,
        options=options)
    timer.start()

    ap.save_overall_html(dest_dir_path='./output/')


class RectOptions(TypedDict):
    rectangle: ap.Rectangle


def on_timer(e: ap.TimerEvent, options: RectOptions) -> None:
    options['rectangle'].fill_color = ap.String('#f0a')


if __name__ == '__main__':
    main()

timer_repeat_count.gif

タイマー終了時のイベント

repeat_count引数を指定した時に使うものとなりますが、タイマーが指定回数終わったタイミングのイベントハンドラをtimer_completeメソッドで設定できます。

引数はマウスイベントのインターフェイスなどと似たような感じで第一引数は対象のハンドラの関数(もしくはメソッド)、第二引数はオプションとなる任意の値を格納した辞書となります。

以下のコードサンプルでは50回タイマーが実行された後に設定したイベントが実行され、コンソール上にTimer completed!と表示されるようにしてあります。

from typing_extensions import TypedDict

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=200, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')
    rectangle: ap.Rectangle = sprite.graphics.draw_rect(
        x=50, y=50, width=50, height=50)

    options: RectOptions = {'rectangle': rectangle}
    timer: ap.Timer = ap.Timer(
        on_timer, delay=ap.FPS.FPS_60, repeat_count=50,
        options=options)
    timer.timer_complete(on_timer_complete)
    timer.start()

    ap.save_overall_html(dest_dir_path='./output/')


class RectOptions(TypedDict):
    rectangle: ap.Rectangle


def on_timer_complete(e: ap.TimerEvent, options: dict) -> None:
    ap.trace('Timer completed!')


def on_timer(e: ap.TimerEvent, options: RectOptions) -> None:
    options['rectangle'].x += 1


if __name__ == '__main__':
    main()

アニメーション(Tween)

タイマーなどを使ってもアニメーションをすることはできますが、最終値と設定する時間が決まっている場合やイージングなどが必要な場合にはアニメーション用のインターフェイスを設けてあるのでそちらを使うと楽です。

以降の節では各アニメーションのインターフェイスについて触れていきます。

基本の使い方

各アニメーションのインターフェイスはanimation_というプレフィックスを共通して持っています。例えばX座標のアニメーションを設定したければanimation_xというメソッドで設定することができます。

各アニメーションのインターフェイスでは共通する基底クラスを継承した、該当するアニメーションのクラスのインスタンスが返却されます。例えばanimation_xメソッドであればAnimationXというクラスのインスタンスが返却されます。これらのインスタンスはstartメソッドなどを持っています(startメソッドを呼ぶとアニメーションがスタートします)。

アニメーションに設定する時間はduration引数で指定します。タイマーと同様にミリ秒で指定します。

以下のコードサンプルでは1秒かけてX座標を100の位置にアニメーションさせています(イージング設定に関しては後の節で触れます)。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=200, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')
    rectangle: ap.Rectangle = sprite.graphics.draw_rect(
        x=50, y=50, width=50, height=50)
    animation_x: ap.AnimationX = rectangle.animation_x(
        x=100, duration=1000, easing=ap.Easing.EASE_OUT_QUINT,
    )
    animation_x.start()

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

animation_x.gif

使用できるアニメーションのインターフェイス

現在実装している各属性は大体のもののインターフェイスが存在します(一部不安定なものなどはここでは省きます)。

animation_interfaces_abstract.gif

リストにすると以下のようなものが存在します。

  • animation_x -> X座標のアニメーション
  • animation_y -> Y座標のアニメーション
  • animation_move -> XとY座標両方のアニメーション
  • animation_width -> 幅のアニメーション
  • animation_height -> 高さのアニメーション
  • animation_fill_color -> 塗りのアニメーション
  • animation_fill_alpha -> 塗りの透明度のアニメーション
  • animation_line_color -> 線の色のアニメーション
  • animation_line_alpha -> 線の透明度のアニメーション
  • animation_line_thickness -> 線幅のアニメーション
  • animation_radius -> 半径のアニメーション(円などのクラスのみ持ちます)
  • animation_rotation_around_center -> 中心座標基準での回転のアニメーション
  • animation_rotation_around_point -> 任意の座標基準での回転のアニメーション
  • animation_scale_x_from_center -> 中心座標基準でのX方向の拡縮のアニメーション
  • animation_scale_y_from_center -> 中心座標基準でのY方向の拡縮のアニメーション
  • animation_scale_x_from_point -> 任意の座標基準でのX方向の拡縮のアニメーション
  • animation_scale_y_from_point -> 任意の座標基準でのY方向の拡縮のアニメーション

※コードや実際のHTML上の表示などを確認されたい場合にはAnimation interfaces abstractのドキュメントをご確認ください。

メソッドチェーンによる設定

各アニメーションのインターフェイスは自身のインスタンスを返却するようにしているためD3.jsなどのようにメソッドチェーンで繋いでシンプルに記述することができます。例えば後の節で触れますがアニメーション終了時のイベント設定とstartメソッドを繋いで以下のように書くことができます。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=200, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')
    rectangle: ap.Rectangle = sprite.graphics.draw_rect(
        x=50, y=50, width=50, height=50)
    rectangle.animation_x(
        x=100, duration=1000, easing=ap.Easing.EASE_OUT_QUINT,
    ).animation_complete(on_animation_complete).start()

    ap.save_overall_html(dest_dir_path='./output/')


def on_animation_complete(e: ap.AnimationEvent, options: dict) -> None:
    ap.trace('Animation completed!')


if __name__ == '__main__':
    main()

イージング設定

各アニメーションのメソッドのeasing引数にEasingのenumを指定するとイージングの設定を行うことができます。イーズイン(開始時は遅く段々速くなるアニメーション)、イーズアウト(開始時は速く段々遅くなるアニメーション)、イーズインアウト(開始と終了時両方が遅く中間時点が一番速くなるアニメーション)でそれぞれ主要な定義を揃えています(EASE_IN_CUBICやEASE_OUT_QUINT、EASE_IN_OUT_BOUNCEなど)。

以下のサンプルコードでは3つの四角を生成し、それぞれイーズイン・イーズアウト・イーズインアウトのイージング設定をそれぞれ行っています。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=200, stage_height=350, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')

    DURATION: int = 1000
    X: int = 100

    rectangle_1: ap.Rectangle = sprite.graphics.draw_rect(
        x=50, y=50, width=50, height=50)
    rectangle_1.animation_x(
        x=X, duration=DURATION, easing=ap.Easing.EASE_IN_QUINT,
    ).start()

    rectangle_2: ap.Rectangle = sprite.graphics.draw_rect(
        x=50, y=150, width=50, height=50)
    rectangle_2.animation_x(
        x=X, duration=DURATION, easing=ap.Easing.EASE_OUT_QUINT,
    ).start()

    rectangle_3: ap.Rectangle = sprite.graphics.draw_rect(
        x=50, y=250, width=50, height=50)
    rectangle_3.animation_x(
        x=X, duration=DURATION, easing=ap.Easing.EASE_IN_OUT_QUINT,
    ).start()

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

easing.gif

遅延設定

各アニメーションのインターフェイスにはdelayという引数があり、こちらにミリ秒を指定するとその秒数分遅れてからアニメーションが開始します。

以下のコードサンプルでは3つの四角に対して同じX座標のアニメーションを設定していますが、2つ目の四角は1秒遅延、3つ目の四角は2秒遅延させてからアニメーションを開始しています。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=200, stage_height=350, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')

    DURATION: int = 1000
    X: int = 100
    EASING: ap.Easing = ap.Easing.EASE_IN_QUINT

    rectangle_1: ap.Rectangle = sprite.graphics.draw_rect(
        x=50, y=50, width=50, height=50)
    rectangle_1.animation_x(
        x=X, duration=DURATION, easing=EASING,
    ).start()

    rectangle_2: ap.Rectangle = sprite.graphics.draw_rect(
        x=50, y=150, width=50, height=50)
    rectangle_2.animation_x(
        x=X, duration=DURATION, delay=1000, easing=EASING,
    ).start()

    rectangle_3: ap.Rectangle = sprite.graphics.draw_rect(
        x=50, y=250, width=50, height=50)
    rectangle_3.animation_x(
        x=X, duration=DURATION, delay=2000, easing=EASING,
    ).start()

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

animation_delay.gif

アニメーション終了時のイベント

animation_completeメソッドでアニメーション終了時のハンドラを登録することができます。使い方はハンドラの第一引数がAnimationEventに変わる以外は他のイベントと大体同じです。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=200, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')

    rectangle: ap.Rectangle = sprite.graphics.draw_rect(
        x=50, y=50, width=50, height=50)
    rectangle.animation_x(
        x=100, duration=1000, easing=ap.Easing.EASE_OUT_QUINT,
    ).animation_complete(on_animation_complete).start()

    ap.save_overall_html(dest_dir_path='./output/')


def on_animation_complete(e: ap.AnimationEvent, options: dict) -> None:
    ap.trace('Animation is completed!')


if __name__ == '__main__':
    main()

アニメーションのイベントインスタンスにおけるthisとジェネリックの型アノテーション

AnimationEventのインスタンスのthisはイベントを設定したアニメーションのインスタンス(AnimationBaseのサブクラスのインスタンス)となります(例 : AnimationXクラスなど)。

AnimationBaseクラスはtarget属性を持っており、アニメーションが設定されているインスタンス(四角のインスタンスなど)が設定されます。アニメーション設定先の型が固定されている場合にはAnimationEventクラスにジェネリックの型アノテーションをしておくとtargetの値の型が決定され補完などが効くようになります(例 : ap.AnimationEvent[ap.Rectangle])。

以下のようにジェネリック関係の型アノテーションをしているとマウスオーバー時などに型をVS Codeなどで認識できていることを確認できます。

def on_animation_complete(
        e: ap.AnimationEvent[ap.Rectangle], options: dict) -> None:
    e.this.target

image.png

複数の連続したアニメーションを設定する

1つのアニメーションが終わったら次のアニメーションへ・・・と設定したい場合にはシンプルに同じインスタンスで連続してアニメーションのインターフェイスを呼び出すことで実装ができます。

以下のコードサンプルでは右に進むアニメーション、下に進むアニメーション、左に進むアニメーション、上に進むアニメーションを順番に実行しています。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=200, stage_height=200, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')

    DURATION: int = 500
    EASING: ap.Easing = ap.Easing.EASE_IN_QUINT

    rectangle: ap.Rectangle = sprite.graphics.draw_rect(
        x=50, y=50, width=50, height=50)
    rectangle.animation_x(x=100, duration=DURATION, easing=EASING).start()
    rectangle.animation_y(y=100, duration=DURATION, easing=EASING).start()
    rectangle.animation_x(x=50, duration=DURATION, easing=EASING).start()
    rectangle.animation_y(y=50, duration=DURATION, easing=EASING).start()

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

animation_sequence.gif

複数のアニメーションを同時に走らせる

複数のアニメーション設定を同時に走らせたくなるケースも多々あります。例えばX座標と透明度のアニメーションを同時に走らせる・・・といったケースです。

そういった場合にはanimation_parallelメソッドを使うと対応ができます。animations引数にリストで複数のアニメーションを設定することでそれらのアニメーションが同時にスタートします。

以下のコードサンプルでX座標・塗りの透明度・塗りの色を同時にアニメーションさせています。

import apysc as ap


def main() -> None:
    stage: ap.Stage = ap.Stage(
        stage_width=200, stage_height=150, background_color='#333',
        stage_elem_id='sample_stage')
    sprite: ap.Sprite = ap.Sprite(stage=stage)
    sprite.graphics.begin_fill(color='#0af')

    rectangle: ap.Rectangle = sprite.graphics.draw_rect(
        x=50, y=50, width=50, height=50)
    rectangle.animation_parallel(
        animations=[
            rectangle.animation_x(x=100),
            rectangle.animation_fill_alpha(alpha=0.5),
            rectangle.animation_fill_color(fill_color='#f0a'),
        ],
        duration=1000, delay=500, easing=ap.Easing.EASE_OUT_QUINT,
    ).start()

    ap.save_overall_html(dest_dir_path='./output/')


if __name__ == '__main__':
    main()

animation_parallel.gif

蛇足

細かい機能は他にも色々あるのですがあまりに長くなりそうだったので今回は割愛しましたが、それでも1年弱ほどちまちまと作っていただけあって大分ボリュームが多くなりました・・・。

今年の分は基礎的な部分ばかりになりましたが、来年からは実用面でもっと役に立つ&お手軽な機能なども拡充していきたいところです・・・!(コンポーネント等々)

まだまだ開発中な感じが強いですが、もしも「将来に期待できる」「今後のアップデートに期待」「将来使ってみたい」と思われた方がごく僅かでもGitHubのリポジトリへのスターや本記事へLGTMをいただけましたらモチベが当社比3割増しくらいになるかもしれません・・・!(?)

17
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
7