0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Pythonista3Advent Calendar 2022

Day 6

Pythonista3 のui モジュールを使って絵を描く 〜 クリエイティブコーディングする 〜

Last updated at Posted at 2022-12-05

この記事は、Pythonista3 Advent Calendar 2022 の06日目の記事です。

一方的な偏った目線で、Pythonista3 を紹介していきます。

ほぼ毎日iPhone(Pythonista3)で、コーディングをしている者です。よろしくお願いします。

以下、私の2022年12月時点の環境です。

sysInfo.log
--- 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'])

img221122_084419.png

前回と見た目が同じですか?そう見えますか?

敢えてです。わざわざ同じ見た目になるようにしました(詐欺師の手法みたい)。

前回はmain のView がadd_subview で、sub のView を取り込んでいました。View の中にView を入れて表示されていたのです。

今回は、add_subview が呼ばれてません。main のView の描画領域に赤色の矩形を描いています。

img221122_091910.png

inspector を見るとsubviews が空なのが確認できます。

img221122_090034.png

色々描いてみる

(他にスマートな方法があるかもしれませんが)線や円を描画してみました。

img221122_122135.png

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 オブジェクト(pathlibPathではない)の繋げ方で、曲線も表現できます。

ui.Path.add_quad_curve | ui — Native GUI for iOS — Python 3.6.1 documentation

アプリ(ui モジュール) としての動き

前回の話に戻ると、ui モジュールの基本的な設置(View)の方法について紹介しました。

実行ボタンを押し、アプリが立ち上がった後はdelegateAction を使い、適宜処理を行う。実行後はユーザー側の指示よりアプリの内容が変化(更新)していきます。

それとは別の変化(更新)の処理方法として、実行後でもアプリ側が常に動き続けることで処理をしていく方法もあります。

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秒)

.py
self.update_interval = 1 / 1

img221122_164327.gif

1/60 秒(約0.016秒)

.py
self.update_interval = 1 / 60

img221122_164645.gif

※ GIF ですと、速すぎて訳分からんですね!!

drawupdate の組み合わせ

経験上update は処理が重いと、指定した間隔で走らない場合があります。正確性よりも、更新して描画してくれる。程度の認識が良いかと思われます。

再度になりますが、ui モジュールにはdelegateAction といった適宜呼び出し可能な処理があるので、使い分けてお使いください。

「じゃあ、どんな時に使う😡」となりそうですが、draw と組み合わせるのが面白いかと思います。

こんな感じのは如何でしょう?

img221122_181629.gif

レッツ!クリエイティブコーディング

サイン波って可愛いですよね。

「クリエイティブコーディングとは?」と、なると長くなってしまうので割愛しますが、コードを書いて絵を描こうぜ!という事です!!

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.counter150 を越えたらif 分岐で「何もしない」としており、ui.Labelself.counter の数値を目視確認できるようにしています。

.py
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'])

img221122_190250.gif

パキッと消えてしまいましたね!

つまりは、View.set_needs_display()update で呼び出す度に、draw の内容を毎回描画してくれているのです。

こんな高速にiPhone ちゃんが頑張ってくれているんですね〜

再描画ではなく、前のフレームを継続させたい

「毎回リセットして描画されると、前の情報が消えてしまう😡」という声も一部あると思います。

ちょっと力技ですが、こんなのは如何でしょうか?

img221122_194749.gif

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_tomove_to を重ねがけしています。

描画への調整として以下数値を変更しています

  • 線の太さ80.5
    • wire.line_width
  • 線色に透過(0.8
    • self.line_stroke_color
  • 線の分割(折れ曲がる部分)164
    • 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)

img221122_195006.gif

あぁ~!タンジェントの線~~!

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 お待ちしておりますー

  • Twitter

なんしかガチャガチャしていますが、お気兼ねなくお声がけくださいませー

  • GitHub

基本的にGitHub にコードをあげているので、何にハマって何を実装しているのか観測できると思います。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?