タートルグラフィックスで『星』を描いてみた

# tl;dr;

![hosi.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81851/51c61b98-c6c8-cca1-b36c-c27670de55d6.gif)

[タートルグラフィックス](https://docs.python.org/ja/3.10/library/turtle.html)を使用して、文字通り『星』をたくさん描きます。
※コードは最後の方にあります。

# 動機(読み飛ばし推奨)

もういくつ寝るとコミケですね。ごきげんよう。
年末ということで、もうすぐ非リア充たちが恐れているあの日がやってきますね。

そうです。あの日です。
…
……
………
この素敵なアドベントカレンダー期間の最終日が迫ってきています！！

私は今年もアドカレで知見の雨を浴びて、私のスキルはメキメキのニョッキニョキのビビデバビデブーに伸びたかというと**そんなことはなく**、急な寒さに耐えながら日常を過ごしておりました。(最後までこのノリで行きます。**耐えてください**)

でもそんな日常でいいのか？
1つくらい他人の役に立ったり学習意欲を伸ばしたりする記事をアドカレに上げて、残念な年末を回避するべきじゃないか！
せめてこう…完全に腐ったまま今年を終えるんじゃなくて、**ほんのりなまぐさく傷んだ**程度に終えた方がいいんじゃないのか！！

そんな気分でアドカレに参加しようと思い立ちました。
がんばるぞ。

しかし技術力も発想力も大してない私は、脳みそ腐敗スメル(加齢スメルではない！)を出しながら某技術系QA掲示板で回答をしたりネタ探しをしたりしたのです。

そこでざっくり次のような意図の質問を見つけました。
「Pythonで亀を使って星を描きたいです。最低でも3つは描けって学校の先生に言われました」

ふむ。
悩める若人のためにひと肌ぬぎますか！
3ヶ月前の質問だから**提出期限過ぎてる**かもしれないけど！！
※掲示板には別途回答しました。

# タートルグラフィックスを使ってみた

Pythonには標準でタートルグラフィックスというプログラミング学習用ライブラリがついてきます。
ロボット亀を動かしてお絵描きする教材で、ロボット亀にリモコンよろしく「前に進め」「右に曲がれ」「円を描け」のような命令を出すと簡単に動かすことができます。

例えば下記のコードを実行してみましょう。

```py
import turtle # タートルグラフィックスのライブラリを使用可能にする

turtle.penup() # 直線は書かない
for _ in range(2):
    turtle.dot(100)
    turtle.forward(100)
    turtle.right(120)
turtle.dot(150)
turtle.mainloop() # 自動終了しないおまじない
```

どこかで見たような黒い円を3つ描けます。**ハハッ！**

五芒星を1つ描く方法は、下記の回答が模範解答例でしょう。

```py
import turtle

for _ in range(5):
    turtle.forward(100)
    turtle.right(144)
```

![basic.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81851/9dc7f628-4270-3163-0b04-081cfd746314.gif)

上のコードの外側でさらにループして3個の五芒星を描くことができます。

```py
import turtle

for i in range(3):
    turtle.penup() # ペンを持ち上げて座標移動開始
    turtle.setx(turtle.xcor() + 110 * (i // 2)) # x軸を適当に移動
    turtle.sety(turtle.ycor() + 110 * (i % 2))  # y軸を適当に移動
    turtle.pendown() # ペンをおろして描画開始
    for _ in range(5):
        turtle.forward(100)
        turtle.right(144)

# おまけ
turtle.penup()
turtle.setpos(50, 100)
for _ in range(2):
    turtle.dot(100)
    turtle.forward(100)
    turtle.right(120)
turtle.dot(150)

turtle.mainloop()
```

前半は**普通すぎ**てテンションが下がりますね！
なのでおまけとして星の上に黒い円を3つ描いて、筆者が少なくともあと数日間はドキドキする構図を追加しました。**ハハッ！**

# リアルな星を描いてみた

## ピクセルアート

さて前章の回答では題意を満たさない、**普通じゃ満足できないケース**を考えてみます。

もしかしたら出題者は簡略化された図形を許容せず、リアルな満天の星でないと「こんな変十角形は星と認めん！」と突き返してくるかもしれません。
その時は星の写った写真を書き写して写実的に描写しましょう。

満天の星の画像をダウンロードしても良いですし、自分で油絵を描いてスキャンしても良いです。
今回は適当にAIで生成した画像を加工して利用します。

![star.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81851/e4021d43-1fdd-0074-9e0e-8eeb509db7bc.jpeg)

実際のコードは以下です。
あらかじめ`pip install pillow`していい感じに画像を読み取れるようにしてください。
そして`star.jpg`を実行フォルダに配置してください。
最後に、**アウラ実行しろ**。

```py
import turtle as t
from PIL import Image
import itertools

# 画像パス設定
img_path = r"star.jpg"

# 超高速で描画するためのおまじない
t.delay(0)
#t.tracer(50000, 0) # 5万倍速

# 画像を点描で描画する
def draw_image(img_path):
    img  = Image.open(img_path)
    t.colormode(255)
    for y, x in itertools.product(range(img.height), range(img.width)):
        c = img.getpixel((x, y))
        if c == (255,255,255):
            continue
        t.penup()
        t.goto(x, y*-1)
        t.pendown()
        t.pencolor(c)
        t.dot(size=2)

draw_image(img_path)
t.hideturtle()
t.mainloop()
```

やってることは画像を読み取り、[`getpixel`](https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.getpixel)で1ピクセルずつ色を読取り、ロボット亀に点描してもらってます。
1ピクセル書き写したら1つ右のピクセルに移動し、一番右のピクセルに移動したら次の行に移動します。
何千何万ピクセルもある画像からピクセルを読み取っては描き、読み取っては描きを繰り返す、**賽の河原積み**のような所業を強要しても文句ひとつ言わずに実行してくれるところがロボット亀の良いところです。
特に「アウラ実行しろ」とかありえないネタをこの私が唐突にねじ込んでも文句を言わないのが素晴らしいです。

ちなみにこの操作を実行してもらうと激烈に遅いです。
[`delay`](https://docs.python.org/ja/3/library/turtle.html#turtle.delay)を0にして最高速で作業してもらっても描画速度に限界があります。
※次の画像は3000倍速で録画しています。

![star.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81851/6b667814-abe8-bc09-9570-bfff6542ce78.gif)

**ISDNや3G時代**を思い出した人はインターネットおじいちゃんです。
**テレホ時代**を思い出した人はCYBORGじいちゃんですか？
**ブラウン管の原理**を思い出したコンピューターおばあちゃんはイェイイェイぼくは大好きです。

ナウなヤングの読者はこの遅さと**前時代的なノリ**に耐えられないと思いますので、先ほどのコード10行目あたりの[`tracer`](https://docs.python.org/ja/3/library/turtle.html#turtle.tracer)の先頭コメントを消して有効化してから実行しなおしてください。
描画が省略されて劇的に速度向上するため、バカウケするのは当たり前田の**もうやめます**。

## 背景画像

ちなみにタートルグラフィックスは[`bgpic`](https://docs.python.org/ja/3/library/turtle.html#turtle.bgpic)で背景にgif画像を指定することができるので、gif画像があれば下記のコードで画像を表示できます。
※実行時のフォルダに`star.gif`をご用意ください。

```py
import turtle

screen = turtle.Screen()
screen.bgpic('star.gif')
turtle.hideturtle()

turtle.mainloop()
```

背景画像なので一瞬で描画してくれます。
先ほどは**徒労の多い点描**おつかれさまでした。

# 『星』をたくさん描いてみた

さて、まだこの回答で題意を満たしていない可能性もあります。
出題者から「んー？『星』を書けと言ったのに、イガイガの線や写真のトレースを書いてきてどうする。最近のこわっぱどもは『星』の漢字書き取りでこれを書くのか？この**KUSO☆TAWAKE**め！」とか言われちゃうかもしれません。
本当にそう言われたら**次に会うのは法廷**かもしれませんが、そうなる前に対策くらい知っておいても良いでしょう。

先ほどのコードの応用で、『星』を画像化→画像を転写するステップを踏めば漢字を転写することもできます。
下記のコードを実行すると冒頭のような画像を出力できます。
コードの`各種設定`を書き換えることで文字や個数などを変更可能です。
※フォントがメイリオなのでWindowsじゃないと動きません。

```py
import turtle as t
from PIL import Image, ImageDraw, ImageFont, ImageOps
from random import randrange

# 各種設定
fontname = r"C:\Windows\Fonts\meiryob.ttc" 
font_range = [8, 80]
star_text = "星"
x_range = [-500, 500]
y_range = [-500, 500]
star_count = 256
bg_color = (0,0,0)

# 超高速で描画するためのおまじない
t.delay(0)
t.tracer(65535, 0)

# 星の画像を作る
def create_image(size, color) -> Image:
    #ImageDrawのtextbboxを使ってテキストの描画サイズを測る
    img  = Image.new("RGB", (1,1), color=bg_color)
    font = ImageFont.truetype(fontname, size)
    draw = ImageDraw.Draw(img)
    box = draw.textbbox((0,0), star_text, font=font)
    image_size = (box[2] + 1, box[3]) #draw_line関数で左端ピクセルを白くするため幅+1
    #画像サイズを文字に合わせて拡張
    img = img.resize(image_size)
    draw = ImageDraw.Draw(img)
    draw.text((0,0), star_text, fill=color, font=font)
    img = ImageOps.flip(img)  #turtle描画時に上下反転するため
    #img.save("test.png")
    return img

# 1行分描画する
def draw_line(img, y, left, bottom):
    buffer_color = bg_color
    t.penup()
    for x in range(img.width):
        # x,y座標の色を読み込んで処理する
        color = img.getpixel((x, y))
        if color == buffer_color:
            #同色の場合は何もせずに次のピクセルへ移動
            continue
        #色の変わり目では左隣のピクセルまで移動(pendownされていれば線が引かれる)
        t.goto(x - 1 + bottom, y + left)
        if color == bg_color:
            #cが白の場合はペンを上げる
            t.penup()
        else:
            #cが有色の場合は色を変えてペンを置く
            t.pencolor(color)
            t.pendown()
        buffer_color = color

# 星を1つ描画する
def draw_star(size, color, left, bottom):
    img = create_image(size, color)
    t.colormode(255)
    # 行描画バージョン(中速)
    for y in range(img.height):
        draw_line(img, y, left, bottom)
    """ 点描バージョン(低速)
    for y, x in itertools.product(range(img.height), range(img.width)):
        c = img.getpixel((x, y))
        if c == (255,255,255):
            continue
        t.penup()
        t.goto(x+left, y+bottom)
        t.pendown()
        t.pencolor(c)
        t.dot(size=2)
    """ 

# ループして星を描画
t.bgcolor(bg_color)
for i in range(star_count):
    fontsize = randrange(font_range[0], font_range[1])
    fontcolor = (randrange(255), randrange(255), randrange(255))
    left = randrange(x_range[0], x_range[1])
    bottom = randrange(y_range[0], y_range[1])
    print("{:>3}/{} size:{}, RGB:{}, left:{}, bottom:{}".format(i+1, star_count, fontsize, fontcolor, left, bottom)) #描画中の星
    draw_star(fontsize, fontcolor, left, bottom)
t.hideturtle()
t.mainloop()
```

コードの見どころは特にありませんが、左右の余白は飛ばしたり、同一色が横に連続するピクセルは点描ではなく線で描くなどの工夫をして若干高速化しました。
`star_text = "星" `の右辺を"☆"に変えれば五芒星を描画することもできますし、"恋人"に変えれば**漆黒のキャンバスに色とりどりの恋人**が浮き上がります。
ね？どうですか？実用的かつロマンティックでしょう？

はい、**んなわきゃない**というツッコミありがとうございます。
かわいい**手書きフォント**じゃないとロマンティックじゃないですもんね。
ぜひご自身のお好みに合わせ(たフォントを使っ)て**恋人を機械的に量産**してください。

# まとめ

プログラミングスキルに秀でた読者諸賢にとっては凡庸で退屈な記事だと承知しておりますが、にぎやかしということで石や星を投げないで許してください。なんで**もうやめます**から。
~~課題に悩まされる若者はこんな大人になっちゃダメだゾ★彡~~

最後に、ここまで読んでくださったあなたの**聖人ニコラウスのような忍耐力**へ惜しみない拍手を送るため筆をおかせていただきます。
それでは良いPythonライフを！