この記事は、Pythonista3 Advent Calendar 2022 の06日目の記事です。
一方的な偏った目線で、Pythonista3 を紹介していきます。
ほぼ毎日iPhone(Pythonista3)で、コーディングをしている者です。よろしくお願いします。
以下、私の2022年12月時点の環境です。
--- SYSTEM INFORMATION ---
* Pythonista 3.3 (330025), Default interpreter 3.6.1
* iOS 16.0.2, model iPhone12,1, resolution (portrait) 828.0 x 1792.0 @ 2.0
他の環境(iPad や端末の種類、iOS のバージョン違い)では、意図としない挙動(エラーになる)なる場合もあります。ご了承ください。
ちなみに、model iPhone12,1
は、iPhone11 です。
ui
モジュールで絵を描こうぜ
前回(急足で)紹介ができていなかった機能たちがあります。
-
draw
メソッド ui.Path
他のui
モジュールとは使われ方や考え方が違っており、iOS API でいうところの
CAShapeLayer
UIBezierPath
のように、View ではなくLayer を用いた実装となります。
Pythonista3 には、canvas
モジュールも用意されていますが、ui
モジュールを使ってアプリ上に乗せると考えるとなかなかcanvas
モジュールの出番は少ないかもしれません。
canvas — Vector Graphics — Python 3.6.1 documentation
基礎的な呼び出し
とにかく、draw
メソッドやui.Path
を使ったコード書いて実行してみましょう。
import ui
class MainView(ui.View):
def __init__(self):
self.name = 'draw'
self.bg_color = 0.5 # todo: 灰色
def draw(self):
_, _, w, h = self.frame
rect_width = 100
rect_height = 100
x = w / 2 - rect_width / 2
y = h / 2 - rect_height / 2
rect = ui.Path.rect(x, y, rect_width, rect_height)
ui.set_color('red')
rect.fill()
if __name__ == '__main__':
main_view = MainView()
main_view.present(style='fullscreen', orientations=['portrait'])
前回と見た目が同じですか?そう見えますか?
敢えてです。わざわざ同じ見た目になるようにしました(詐欺師の手法みたい)。
前回はmain のView がadd_subview
で、sub のView を取り込んでいました。View の中にView を入れて表示されていたのです。
今回は、add_subview
が呼ばれてません。main のView の描画領域に赤色の矩形を描いています。
inspector を見るとsubviews
が空なのが確認できます。
色々描いてみる
(他にスマートな方法があるかもしれませんが)線や円を描画してみました。
import ui
class MainView(ui.View):
def __init__(self):
self.name = '色々描いてみる'
self.bg_color = 0.9 # todo: ほぼ白
def draw(self):
_, _, w, h = self.frame
xy_path = ui.Path() # 縦横の線を引くためのパスを準備
# todo: x line (横線を引く)
xy_path.line_width = 4 # 線の太さ
xy_path.move_to(0.0, h / 2) # 線の引き始めの位置設定(左端の、中央)
xy_path.line_to(w, h / 2) # 線の引き終わりの位置設定(右端の、中央)
# todo: y line (縦線を引く)
xy_path.move_to(w / 2, 0.0)
xy_path.line_to(w / 2, h)
ui.set_color('green')
xy_path.stroke() # 位置設定したパスで線を引く
# 円を書く設定
oval_width = 100
oval_height = 100
# 円の中心位置ではなく、起点が左上となるので
x = w / 2 - oval_width / 2 # (画面横幅/2) - (円横幅/2)
y = h / 2 - oval_height / 2 # (画面縦幅/2) - (円縦幅/2)
oval = ui.Path.oval(x, y, oval_width, oval_height) # 起点位置とサイズを指定した円を生成
ui.set_color((1.0, 0.0, 0.5, 0.5))
oval.fill() # 面(内側)を塗る
if __name__ == '__main__':
main_view = MainView()
main_view.present(style='fullscreen', orientations=['portrait'])
緑の十字線は、move_to
の起点とline_to
終点で「ここから、ここまで」をx, y 座標点として指定します。
横線は、左側から右側へ。縦線は、上から下へ。のイメージです。
stroke
が線を、fill
が面を塗る指定で、塗る直前のui.set_color
で指定した色が反映されます。
Path
オブジェクト(pathlib
のPath
ではない)の繋げ方で、曲線も表現できます。
ui.Path.add_quad_curve | ui — Native GUI for iOS — Python 3.6.1 documentation
アプリ(ui
モジュール) としての動き
前回の話に戻ると、ui
モジュールの基本的な設置(View)の方法について紹介しました。
実行ボタンを押し、アプリが立ち上がった後はdelegate
やAction
を使い、適宜処理を行う。実行後はユーザー側の指示よりアプリの内容が変化(更新)していきます。
それとは別の変化(更新)の処理方法として、実行後でもアプリ側が常に動き続けることで処理をしていく方法もあります。
update
メソッド
指定したタイミング毎に、View が更新されます。
更新させたい場合には、View.update_interval
にて更新間隔を指定します。
なお、更新間隔は秒単位ですので、1秒以下は1/n
で指定していきます。
import ui
class MainView(ui.View):
def __init__(self):
self.name = '1/n秒 カウントアップ'
self.bg_color = 0.5 # todo: 灰色
self.update_interval = 1 / 60 # todo: 1秒
self.num_count = 0
self.label = ui.Label()
self.label.text = f'{self.num_count}'
self.label.flex = 'TBLR'
self.label.bg_color = 1
self.add_subview(self.label)
def update(self):
self.num_count += 1
self.label.text = f'{self.num_count}'
if __name__ == '__main__':
main_view = MainView()
main_view.present(style='fullscreen', orientations=['portrait'])
1/1
秒(1秒)
self.update_interval = 1 / 1
1/60
秒(約0.016秒)
self.update_interval = 1 / 60
※ GIF ですと、速すぎて訳分からんですね!!
draw
とupdate
の組み合わせ
経験上update
は処理が重いと、指定した間隔で走らない場合があります。正確性よりも、更新して描画してくれる。程度の認識が良いかと思われます。
再度になりますが、ui
モジュールにはdelegate
やAction
といった適宜呼び出し可能な処理があるので、使い分けてお使いください。
「じゃあ、どんな時に使う😡」となりそうですが、draw
と組み合わせるのが面白いかと思います。
こんな感じのは如何でしょう?
レッツ!クリエイティブコーディング
サイン波って可愛いですよね。
「クリエイティブコーディングとは?」と、なると長くなってしまうので割愛しますが、コードを書いて絵を描こうぜ!という事です!!
draw
のみの実装で静止画だけでも楽しめますが、update
を使い時間経過とともに変化するものを描くのも面白いです。
私、数学は「嫌い寄りの苦手」な人生を送っていましたが、クリエイティブコーディングで三角関係から勉強し直し「おもしれー」ってなってしまいました。
(「勉強し直し」よりは、色々なコードを見て雰囲気で実装し「あー、数学的にはこうなるのか!」と雰囲気で再認識している感じです)
波の出し方一つ取っても、さまざまな視点からアプローチできます。
正解は一つではなく、結果的に絵が出れば勝ちです!
from math import sin, pi
import ui
class MainView(ui.View):
def __init__(self):
self.name = 'sine wave'
self.bg_color = 0.2
self.update_interval = 1 / 60
self.counter = 0 # update 毎に+1 していく
self.line_stroke_color = 0.8
self.segment = 16
def draw(self):
wire = ui.Path()
wire.line_width = 8
amp = self.height / 6
for i in range(self.segment):
x = i / (self.segment - 1) * self.width
radian = (i / self.segment * pi) + (self.counter / 32)
y = (amp * sin(radian)) + (self.height / 2)
if i: wire.line_to(x, y)
else: wire.move_to(x, y)
ui.set_color(self.line_stroke_color)
wire.stroke()
def update(self):
self.counter += 1
self.set_needs_display()
if __name__ == '__main__':
main_view = MainView()
main_view.present(style='fullscreen', orientations=['portrait'])
self.set_needs_display()
で再描画
draw
メソッドでupdate
を呼び出すには、View.set_needs_display()
を使います。
def update(self):
内で「再描画するんやでー」とself.set_needs_display()
を呼び出します。
コメントアウトし実行すると、描画が静止しているのが確認できます。
def update(self):
self.counter += 1
#self.set_needs_display()
ちょっとした実験ですが、self.counter
が一定数を越えたらdraw
の処理を「何もしない」としてみると、どうなるでしょうか?
実行する前にどのようになるか予想をし、実際の結果と差異があるか?
と、やってみると想定力がつくと思いますのでやってみてください💪
今回は、self.counter
が150
を越えたらif
分岐で「何もしない」としており、ui.Label
でself.counter
の数値を目視確認できるようにしています。
from math import sin, pi
import ui
class MainView(ui.View):
def __init__(self):
self.name = '一定数を越えたら、空描画'
self.bg_color = 0.2
self.update_interval = 1 / 60
self.counter = 0
self.label = ui.Label()
self.label.text = f'{self.counter}'
self.label.flex = 'TBLR'
self.label.bg_color = 1
self.add_subview(self.label)
self.line_stroke_color = 0.8
self.segment = 16
def draw(self):
if self.counter < 150:
wire = ui.Path()
wire.line_width = 8
amp = self.height / 6
for i in range(self.segment):
x = i / (self.segment - 1) * self.width
radian = (i / self.segment * pi) + (self.counter / 32)
y = (amp * sin(radian)) + (self.height / 2)
if i: wire.line_to(x, y)
else: wire.move_to(x, y)
ui.set_color(self.line_stroke_color)
wire.stroke()
def update(self):
self.counter += 1
self.set_needs_display()
self.label.text = f'{self.counter}'
if __name__ == '__main__':
main_view = MainView()
main_view.present(style='fullscreen', orientations=['portrait'])
パキッと消えてしまいましたね!
つまりは、View.set_needs_display()
をupdate
で呼び出す度に、draw
の内容を毎回描画してくれているのです。
こんな高速にiPhone ちゃんが頑張ってくれているんですね〜
再描画ではなく、前のフレームを継続させたい
「毎回リセットして描画されると、前の情報が消えてしまう😡」という声も一部あると思います。
ちょっと力技ですが、こんなのは如何でしょうか?
from math import sin, pi
import ui
wire = ui.Path()
wire.line_width = 0.5
class MainView(ui.View):
def __init__(self):
self.name = '描画状態を残す'
self.bg_color = 0.2
self.update_interval = 1 / 60
self.counter = 0
self.line_stroke_color = (0.8, 0.8, 0.8, 0.8)
self.segment = 4
def draw(self):
#wire = ui.Path()
#wire.line_width = 0.5
amp = self.height / 6
for i in range(self.segment):
x = i / (self.segment - 1) * self.width
radian = (i / self.segment * pi) + (self.counter / 32)
y = (amp * sin(radian)) + (self.height / 2)
if i: wire.line_to(x, y)
else: wire.move_to(x, y)
ui.set_color(self.line_stroke_color)
wire.stroke()
def update(self):
self.counter += 1
self.set_needs_display()
if __name__ == '__main__':
main_view = MainView()
main_view.present(style='fullscreen', orientations=['portrait'])
draw
内で生成していたwire = ui.Path()
を外に出してupdate
時に、line_to
とmove_to
を重ねがけしています。
描画への調整として以下数値を変更しています
- 線の太さ
8
→0.5
wire.line_width
- 線色に透過(
0.8
)self.line_stroke_color
- 線の分割(折れ曲がる部分)
16
→4
self.segment
(特に)self.segment
の数値を増やすと、もれなく更新が重くなります。
気軽に数式を書き換えてみよう
数値や数式をちょっと変更するだけで、結果がガラッと変わるところも、クリエイティブコーディングの面白さだと思います。
draw
内のy
を定義する部分sin
(三角関数のサイン)をtan
(三角関数のタンジェント)に変えてみました!
# import にtan を追加
from math import sin, pi, tan
# ここのsin を
y = (amp * sin(radian)) + (self.height / 2)
# tan に変更
y = (amp * tan(radian)) + (self.height / 2)
あぁ~!タンジェントの線~~!
from math import sin, pi, tan
import ui
wire = ui.Path()
wire.line_width = 0.5
class MainView(ui.View):
def __init__(self):
self.name = 'sin -> tan'
self.bg_color = 0.2
self.update_interval = 1 / 60
self.counter = 0
self.line_stroke_color = (0.8, 0.8, 0.8, 0.8)
self.segment = 8
def draw(self):
amp = self.height / 6
for i in range(self.segment):
x = i / (self.segment - 1) * self.width
radian = (i / self.segment * pi) + (self.counter / 32)
y = (amp * tan(radian)) + (self.height / 2)
if i: wire.line_to(x, y)
else: wire.move_to(x, y)
ui.set_color(self.line_stroke_color)
wire.stroke()
def update(self):
self.counter += 1
self.set_needs_display()
if __name__ == '__main__':
main_view = MainView()
main_view.present(style='fullscreen', orientations=['portrait'])
うーん、タンジェントみが出ていますねえ〜
とはいえ、時間が経過すればする程に重く(画面も白く)なっていくのは、いい実装とは言えませんね!
配列で管理し、一定量の線が出てきたら古いものから削除したり、毎描画で複数線を表示させるなど工夫の余地は二十分にあると思います。
最終的にどんな絵を出したいか?どんな表現をしたいか?により、実装方法は変わってきます。ぜひ思い描く表現を実装してみてはいかがでしょうか!?
クリエイティブコーディングの探し方
今回のサイン波の部分は、こちらを参考にさせて頂きました。
JavaScriptで取り組むクリエイティブコーディング - パーリンノイズを使いこなせ - ICS MEDIA
Pythonista3 でクリエイティブコーディングとなると、なかなか実例が無いのが現状です。
Processing や、p5.js などの例から、Python(Pythonista3)に書き換えるのが近道かと思われます。
もちろん、Python やPythonista3 のモジュールには「無い」関数を気軽に呼び出したりしているので、自前で実装するかPython で書かれているモジュールを探す必要があります。
パーリンノイズとかおすすめです(暗黒微笑)。
違う言語を読み替えてみるのは、コードを読み解くいい訓練になってるなぁと思っています。
次回は
ついに、本性を表してしまいました。。。
今回は、クリエイティブコーディングとして、ui
モジュールで絵を描いてみました。
実装方法によって、処理しきれない場面があり「こんなもんかー」と、落胆した方々には朗報です(!?)。
Pythonista3 には、2Dゲームやアニメーションに特化したscene
モジュールがあります。
scene — 2D Games and Animations — Python 3.6.1 documentation
つまり、ニッチなPythonista3 アプリという中で、ニッチな実装方法(ui
モジュールでの描画)を紹介してしまい申し訳ありません。。。
今回のui
モジュールでも、次回のscene
モジュールでも、「数値の微調整をしていたら、元の値がわからなくなった!」という場面が出てくるかもしれません。
そんな時は、以前に紹介した新規作成スクリプトを使うと「取り敢えず一旦退避」的なことができるので、ニーズに合致した際はぜひ試してみてください。
ここまで、読んでいただきありがとうございました。
せんでん
Discord
Pythonista3 の日本語コミュニティーがあります。みなさん優しくて、わからないところも親身に教えてくれるのでこの機会に覗いてみてください。
書籍
iPhone/iPad でプログラミングする最強の本。
その他
- サンプルコード
Pythonista3 Advent Calendar 2022 でのコードをまとめているリポジトリがあります。
コードのエラーや変なところや改善点など。ご指摘やPR お待ちしておりますー
なんしかガチャガチャしていますが、お気兼ねなくお声がけくださいませー
やれるか、やれないか。ではなく、やるんだけども、紹介説明することは尽きないと思うけど、締め切り守れるか?って話よ!(クズ)
— pome-ta (@pome_ta93) November 4, 2022
Pythonista3 Advent Calendar 2022 https://t.co/JKUxA525Pt #Qiita
- GitHub
基本的にGitHub にコードをあげているので、何にハマって何を実装しているのか観測できると思います。