CircuitPythonはAdafruitさんが支援しているシステムなので、グラフィックスは当然GFXだろうと思ったら、全然コンセプトの違う表示システムなので参った。CircuitPythonのPicoDVI用のグラフィックスライブラリとしては、displayioと呼ばれるライブラリが対応しているようです。

Adafruitさんのオリジナルの紹介ページは以下を参照してください。

displayioは、お絵かき用のグラフィックスライブラリというよりは、UI構築用のグラフィックスライブラリに見えますね。グラフィックス要素を描くというよりは、画面上に置いていく感じ。それと、オブジェクト指向の特徴もうまく利用して、Pythonのような対話的なプログラミング言語に合致した、面白いグラフィックスライブラリになってます。

初めてのdisplayio

RP2040-UNO-HDMIでモニタ画面に文字と簡単な図形を表示するプログラム例を以下に示します。

RP2040-UNO-HDMI
# 表示システムの初期化を行う
import displayio, picodvi, board, framebufferio

displayio.release_displays()

fb = picodvi.Framebuffer(
    width=320, height=240, color_depth=8,
    clk_dp=board.GP17, clk_dn=board.GP16, # 端子の配置はRP2040-UNO-HDMI用
    red_dp=board.GP19, red_dn=board.GP18,
    green_dp=board.GP21, green_dn=board.GP20,
    blue_dp=board.GP23, blue_dn=board.GP22)

display = framebufferio.FramebufferDisplay(fb)

group = displayio.Group()
display.root_group = group

# 文字列の表示を行う
from adafruit_display_text import label
import terminalio

group.append(label.Label(terminalio.FONT, text="MicroFan", x=0, y=10, scale=2, color=0xFFFFFF))
group.append(label.Label(terminalio.FONT, text="RP2040-UNO-HDMI:PicoDVI", x=0, y=30, scale=2, color=0xFFFFFF))

# 図形の表示を行う
from adafruit_display_shapes.line import Line
from adafruit_display_shapes.rect import Rect
from adafruit_display_shapes.circle import Circle
from adafruit_display_shapes.triangle import Triangle

group.append(Line(0, 80, 320, 80, 0xFF00FF))
group.append(Rect(0, 120, 100, 100, fill=0xFF0000, outline=0xFFFF00))
group.append(Circle(160, 170, 50, fill=0x00FF00, outline=0xFFFF00))
group.append(Triangle(260, 120, 210, 220, 310, 220, fill=0x0000FF, outline=0xFFFF00))

PICO-HDMI-PLUSの場合には、上記のプログラムの6-11行を以下の内容に差し替えてください。

fb = picodvi.Framebuffer(
    width=320, height=240, color_depth=8,
    clk_dp=board.GP14, clk_dn=board.GP15,
    red_dp=board.GP12, red_dn=board.GP13,
    green_dp=board.GP18, green_dn=board.GP19,
    blue_dp=board.GP16, blue_dn=board.GP17)

このプログラムを実行すると、画面の上の方に文字列が表示され、下の方に四角、丸三角の図形が表示されます。

こいつ動くぞ - 固まってない?(変更可能な)文字列や図形

一般的なグラフィックスライブラリの紹介だと、次のプログラムの例に行くところですが。。。

「こいつ動くぞ!」

ということで、話が続きます。

Pythonのプロンプトで、以下のように入力すると、何が起きるでしょうか?

group[3].y=50

面白いことにというか当然のこととして、最初のプログラムの実行が終了しても、pythonはインタプリタなので、そのプログラムの続きをプロンプトに対して引き続き入力して実行できます。

また、groupにappend()された描画要素は、0から順番にアクセス可能で、インデックスが3の要素は、Rectで四角形の図形です。プログラムでは、Rectにより四角形が画面に書かれたというよりは置かれただけなので、その位置を変更してやると、四角形の位置を動かすことができてしまいます。

また、以下のように入力すると、図形の色を変えることもできます。

group[3].fill=0xFFFFFF

さらに、groupの0番目の要素の文字列の内容を以下のように指定すると、表示される文字列も変えることができます。

group[0].text="Python"

対話的にグラフィックスを操作できるって面白いですよね。

いろいろな図形要素を対象に、位置や色を変える操作を行ってみてください。

ただし、なんでもオッケーではなく、図形の形が変わる(図形の描画表現を作成するために確保したビットマップバッファの構成を変える?)ことはできないようです。

位置を変えるこれはできても、

group[3].x=100

形を変えるこれはできないようです。

group[3].width=100

四角形の幅や高さ、円の直径、三角形の頂点の位置(したがって三角形は動かせない)などは変えられないようです。

それでも、図形や文字列の位置や色を変えられるだけでも、ゲームの作成などで色々楽しめそうですね。


ボールを描こう

円を表すCircleを使ってボールを表示します。

import displayio, picodvi, board, framebufferio

displayio.release_displays()

fb = picodvi.Framebuffer(
    width=320, height=240, color_depth=8,
    clk_dp=board.GP17, clk_dn=board.GP16, # 端子の配置はRP2040-UNO-HDMI用
    red_dp=board.GP19, red_dn=board.GP18,
    green_dp=board.GP21, green_dn=board.GP20,
    blue_dp=board.GP23, blue_dn=board.GP22)

display = framebufferio.FramebufferDisplay(fb)

group = displayio.Group()
display.root_group = group
# ここまではグラフィックス利用の初期化で今後も共通

from adafruit_display_shapes.circle import Circle

ball = Circle(100, 100, 10, fill=0x00FF00, outline=0xFFFF00)
group.append(ball)

すでに確認済の様に、プロンプトに以下のように入力すると、ボールを動かすことができますよね。

group[0].x=200

ここで、x, y 座標は図形の左上の座標を示しています。円だと、中心座標を操作したくなりますが、その場合には、x0, y0を使用します。

実際に使ってみると、円が動くのでx, x0の違いが判りますね。

group[0].x0=200

ボールを動かそう

ボールの位置を保持する x0, y0 を操作してボールを画面内で動かします。

横方向と縦方向の移動速度(0.02秒当たりの移動ピクセル量)を指定し、それをx0, y0に追加してボールを動かします。

ボールの位置が画面を逸脱する前に移動速度を反転させて跳ね返らせます。

import displayio, picodvi, board, framebufferio

displayio.release_displays()

fb = picodvi.Framebuffer(
    width=320, height=240, color_depth=8,
    clk_dp=board.GP17, clk_dn=board.GP16, # 端子の配置はRP2040-UNO-HDMI用
    red_dp=board.GP19, red_dn=board.GP18,
    green_dp=board.GP21, green_dn=board.GP20,
    blue_dp=board.GP23, blue_dn=board.GP22)

display = framebufferio.FramebufferDisplay(fb)

group = displayio.Group()
display.root_group = group
# ここまではグラフィックス利用の初期化で今後も共通

from adafruit_display_shapes.circle import Circle

ball = Circle(100, 100, 10, fill=0x00FF00, outline=0xFFFF00)
group.append(ball)

import time

vx = 2
vy = 5
while True:
    ball.x0 += vx
    ball.y0 += vy
    if ball.x0 <= ball.r or ball.x0 >= (display.width-1)-ball.r:
        vx = -vx
    if ball.y0 <= ball.r or ball.y0 >= (display.height-1)-ball.r:
        vy = -vy
    time.sleep(0.02)

ボールを増やそう

この方式でボールを増やすと、そのボールを動かす処理がボールの数だけ増える

具体例は後で書きます。

ボール自身に動いてもらおう

ボールを増やしやすいように、ボール自身に動いてもらおう

ボールをオブジェクトにして動く能力を付与し、勝手に動いてもらう。

import displayio, picodvi, board, framebufferio

displayio.release_displays()

fb = picodvi.Framebuffer(
    width=320, height=240, color_depth=8,
    clk_dp=board.GP17, clk_dn=board.GP16, # 端子の配置はRP2040-UNO-HDMI用
    red_dp=board.GP19, red_dn=board.GP18,
    green_dp=board.GP21, green_dn=board.GP20,
    blue_dp=board.GP23, blue_dn=board.GP22)

display = framebufferio.FramebufferDisplay(fb)

group = displayio.Group()
display.root_group = group
# ここまではグラフィックス利用の初期化で今後も共通

from adafruit_display_shapes.circle import Circle

class Ball(Circle):
    # 初期座標、縦横速度、色を与える
    def __init__(self, x0, y0, r, vx, vy, color):
        super().__init__(x0, y0, r, fill=color, outline=0xFFFF00)
        self.vx = vx
        self.vy = vy
    def tick(self):
        # 縦横速度分移動して
        self.x0 += self.vx
        self.y0 += self.vy
        # 画面の端に来たら速度を反転させて跳ね返る
        if self.x0 <= self.r or self.x0 >= (display.width-1)-self.r:
            self.vx = -self.vx
        if self.y0 <= self.r or self.y0 >= (display.height-1)-self.r:
            self.vy = -self.vy

ball = Ball(100, 100, 10, 2, 5, 0x00FF00)
group.append(ball)

import time

while True:
    group[0].tick()
    time.sleep(0.02)

ボールがいっぱい

import displayio, picodvi, board, framebufferio, time

displayio.release_displays()

fb = picodvi.Framebuffer(
    width=320, height=240, color_depth=8,
    clk_dp=board.GP17, clk_dn=board.GP16, # 端子の配置はRP2040-UNO-HDMI用
    red_dp=board.GP19, red_dn=board.GP18,
    green_dp=board.GP21, green_dn=board.GP20,
    blue_dp=board.GP23, blue_dn=board.GP22)

display = framebufferio.FramebufferDisplay(fb)

group = displayio.Group()
display.root_group = group
# ここまではグラフィックス利用の初期化で今後も共通

from adafruit_display_shapes.circle import Circle

class Ball(Circle):
    # 初期座標、縦横速度、色を与える
    def __init__(self, x0, y0, r, vx, vy, color):
        super().__init__(x0, y0, r, fill=color, outline=0xFFFF00)
        self.vx = vx
        self.vy = vy
    def tick(self):
        # 縦横速度分移動して
        self.x0 += self.vx
        self.y0 += self.vy
        # 画面の端に来たら速度を反転させて跳ね返る
        if self.x0 <= self.r or self.x0 >= (display.width-1)-self.r:
            self.vx = -self.vx
        if self.y0 <= self.r or self.y0 >= (display.height-1)-self.r:
            self.vy = -self.vy

color = [
    0xFF0000, 0xFFFF00, 0x00FF00, 0x00FFFF, 0x0000FF, 0xFF00FF, 0xFFFFFF,
    ]

import random

# ボールを20個作ろう
for c in range(0, 20):
    # 座標と速度は乱数で設定、色は作成順に順番に
    ball = Ball(random.randint(10,200), random.randint(10,200), 10,
                random.randint(1,7), random.randint(1,7), color[c%7])
    group.append(ball)

import time

# 作成したすべてのボールに、繰り返し自分の(単位時間当たりの)処理を行うように依頼する
while True:
    for ball in group: # すべてのボールに
        ball.tick() # 自分の状態に合わせて動けと指示
    time.sleep(0.02)

ボールを操作しよう

沢山のボールが勝手に動くようになりましたが、ここでちょっと話題を変えて、1つのボールをスイッチで操作するプログラムを作ってみましょう。

PICO-HDMI-PLUSの使用例

ここでは、操作用のスイッチを利用できるPICO-HDMI-PLUSを使用したプログラム例を示します。

import displayio, picodvi, digitalio, board, framebufferio

displayio.release_displays()

fb = picodvi.Framebuffer(
    width=320, height=240, color_depth=8,
    clk_dp=board.GP14, clk_dn=board.GP15, # 端子の配置はPICO-HDMI-PLUS用
    red_dp=board.GP12, red_dn=board.GP13,
    green_dp=board.GP18, green_dn=board.GP19,
    blue_dp=board.GP16, blue_dn=board.GP17)

display = framebufferio.FramebufferDisplay(fb)

group = displayio.Group()
display.root_group = group
# ここまではグラフィックス利用の初期化で今後も共通

# スイッチの初期化
pullup = digitalio.DigitalInOut(board.GP9)
pullup.direction = digitalio.Direction.OUTPUT # プルアップ電源にするので出力設定
pullup.value = True # 出力値は HIGH, 1, 理想的には3.3V

sw1 = digitalio.DigitalInOut(board.GP10) # 初期状態は入力用
sw2 = digitalio.DigitalInOut(board.GP11)
sw3 = digitalio.DigitalInOut(board.GP20)
sw4 = digitalio.DigitalInOut(board.GP21)

# 動かすボールの処理
from adafruit_display_shapes.circle import Circle

ball = Circle(10, 10, 10, fill=0x00FF00, outline=0xFFFF00)
group.append(ball)

v = 5
import time
while True:
    if sw1.value == 0: # 左
        if ball.x0 <= ball.r:
            ball.x0 = ball.r
        else:
            ball.x0 -= v
    if sw2.value == 0: # 右
        if ball.x0 >= (display.width-1)-ball.r:
            ball.x0 = (display.width-1)-ball.r
        else:
            ball.x0 += v
    if sw3.value == 0: # 上
        if ball.y0 <= ball.r:
            ball.y0 = ball.r
        else:
            ball.y0 -= v
    if sw4.value == 0: # 下
        if ball.y0 >= (display.height-1)-ball.r:
            ball.y0 = (display.height-1)-ball.r
        else:
            ball.y0 += v
    time.sleep(0.02)

# スイッチを押さないとボールは動かないよ!

ボールの反射に音を付けてみよう

ボールが画面の端に行きついて跳ね返る際に音を出すようにしましょう。

音を出すための圧電スピーカーが必要なので、このプログラムもPICO-HDMI-PLUS用に作成します。

音を出す機能は、simpleioのtone()を使用します。

プログラムは、ボールが自律的に動くようにオブジェクト化したものをベースに修正を加えます。

まず、5-10行までを置き換えて、PICO-HDMI-PLUS用に変更します。次に、18,33,36に音を出すための記述を加えます。音の周波数は、縦と横の壁で変えてみました。

import displayio, picodvi, board, framebufferio

displayio.release_displays()

fb = picodvi.Framebuffer(
    width=320, height=240, color_depth=8,
    clk_dp=board.GP14, clk_dn=board.GP15, # 端子の配置はPICO-HDMI-PLUS用
    red_dp=board.GP12, red_dn=board.GP13,
    green_dp=board.GP18, green_dn=board.GP19,
    blue_dp=board.GP16, blue_dn=board.GP17)

display = framebufferio.FramebufferDisplay(fb)

group = displayio.Group()
display.root_group = group
# ここまではグラフィックス利用の初期化で今後も共通

import simpleio
from adafruit_display_shapes.circle import Circle

class Ball(Circle):
    # 初期座標、縦横速度、色を与える
    def __init__(self, x0, y0, r, vx, vy, color):
        super().__init__(x0, y0, r, fill=color, outline=0xFFFF00)
        self.vx = vx
        self.vy = vy
    def tick(self):
        # 縦横速度分移動して
        self.x0 += self.vx
        self.y0 += self.vy
        # 画面の端に来たら速度を反転させて跳ね返る
        if self.x0 <= self.r or self.x0 >= (display.width-1)-self.r:
            simpleio.tone(board.GP8, 1000, 0.01)
            self.vx = -self.vx
        if self.y0 <= self.r or self.y0 >= (display.height-1)-self.r:
            simpleio.tone(board.GP8, 500, 0.01)
            self.vy = -self.vy

ball = Ball(100, 100, 10, 2, 5, 0x00FF00)
group.append(ball)

import time

while True:
    group[0].tick()
    time.sleep(0.02)

音がすると、それだけでゲームみたいな感じ。印象がかなり変わりますね。音がモニターから聞けないのが残念かな。