ラベル ソフトウェア&プログラム の投稿を表示しています。 すべての投稿を表示
ラベル ソフトウェア&プログラム の投稿を表示しています。 すべての投稿を表示

2026年10月21日水曜日

生涯で最も実用的なソフトを作った件2

前回の1ボタン仕様。ただ、1クリック目はこのアプリをアクティブにする事に使われる為、実際はダブルクリックが必要。
この現象の回避方法もネットに出ているのですが、冗長になるのでパス。

ソースここから

from kivy.app import App
from kivy.core.window import Window
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button

import pyautogui

class LayMain(BoxLayout):
    def __init__(self,**kwargs):
        super(LayMain,self).__init__(**kwargs)
        
        self.clear_widgets()
        self.orientation='horizontal'

        self.b1 = Button(text='Shift del',size_hint_x=1)
        self.b1.bind(on_release=self.release_bt)
        
        self.add_widget(self.b1)

    def release_bt(self, instance):
        pyautogui.hotkey('alt', 'tab')
        pyautogui.hotkey('shift', 'del')
        pyautogui.hotkey('alt', 'tab')

class MainApp(App):
    def build(self):
        self.title = "mail kill"
        Window.size = (100,100)
        Window.always_on_top = True
        
        layout = LayMain()
        return layout

if __name__=='__main__':
    MainApp().run()

ソースここまで

見ての通り、ボタンを押すと Shift+Del を押してくれるボタン。
なんでこんなもの作ったかと言うと…
半年で積もったスパムメールを消す為。このボタンを使うと、左手はシフトキー、右手はマウスのみで一括削除を続けられる…。
本来1万件位のスパムメールを消すのに2時間以上かかっていたのですが1時間で終わりました…いや、スパム削除ソフト無いのかよ?と思うのですが、案外 Linux 用は良い物が無くてですね…とは言え、メインの Windows 機でメールを読まないのはウィルス感染を避けるためなのでこればっかりはどうにもなりませぬ。

2026年10月19日月曜日

生涯で最も実用的なソフトを作った件

…しかも4時間で出来た…なんか降りてくる時ってありますね。

昨年2月2つ前の会社を辞めた際、作ると言っていたソフトがありました。タッチパネルでカーソルキー入力をするツールです。
なんでこんなものが必要かというと、Linux の画像ビューアは大抵タッチパネルに対応しておらず、画面をタッチしてページ送りといった事が出来なかったからです。
同様に、動画や音声の早送り等もキーボードを前提としたタッチでは使いづらいソフトばかりとなっています。
一応、標準で搭載されている Onboard を使えばできるのですが、フルキーではなくカーソルキーだけのアプリが欲しかったのです。
私は Arrows Tab に Linux を入れて使っているのでこれはかなり優先度が高かったのですが…

で、なぜ今更そんなものを作っているのかというと、初手でテンキー繋げばよいじゃんとなったからですね。
ジャンク屋で100円くらいで売っています。風呂で使うならビニール袋をかぶせて輪ゴムで留めておけば良い。が…流石に面倒になりまして。

開発は割と大変
ガワは kivy で作る事がほぼ確定していましたが、じゃあキー入力部分は?と言えば、pyautogui を使う事は決めていました。
Python の教書の中でベストセラーになった「退屈なことはPythonにやらせよう」という本がありまして、一応少し読んではいたのですよ。
が、実際作ってみるとめちゃくちゃ難航…

なぜか print デバッグできない
私は開発に初期状態の Geany を使っています。初期状態ではラインデバッグが出来ない為 print デバッグするしかないのですが…どういうわけか kivy 系の表示をコンソールに吐いた後は print 処理の表示を吐かなかったのですよね…おかげでかなり難航しました。

非アクティブなアプリにキー入力を送るには?
本アプリを最前列設定で表示するとして、疑似キーボードである以上、他のアプリにキー入力情報を渡す必要がありますがいくらやっても自分自身にキー入力情報が渡ってしまいます。
AIですらこんな回答をしています。
調べたら起動しているアプリ一覧をOSから引き出して、特定アプリにアクセスする…等と出てきまして、難航するしOS毎に挙動が違うしで詰んだかな…と、思いましたが…

ソースここから

from kivy.app import App
from kivy.core.window import Window
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button

import pyautogui

class LaySub(BoxLayout):
    def __init__(self,**kwargs):
        super(LaySub,self).__init__(**kwargs)
        
        self.orientation='vertical'
        
        self.b3 = Button(text='^',size_hint_y=1)
        self.b3.bind(on_release=self.release_up)

        self.b4 = Button(text='v',size_hint_y=1)
        self.b4.bind(on_release=self.release_down)
        
        self.add_widget(self.b3)
        self.add_widget(self.b4)

    def release_up(self, instance):
        pyautogui.hotkey('alt', 'tab')
        pyautogui.press('up')
        pyautogui.hotkey('alt', 'tab')

    def release_down(self, instance):
        pyautogui.hotkey('alt', 'tab')
        pyautogui.press('down')
        pyautogui.hotkey('alt', 'tab')

class LayMain(BoxLayout):
    def __init__(self,**kwargs):
        super(LayMain,self).__init__(**kwargs)
        
        self.clear_widgets()
        self.orientation='horizontal'

        self.b1 = Button(text='<',size_hint_x=1)
        self.b1.bind(on_release=self.release_left)
        
        self.b2 = Button(text='>',size_hint_x=1)
        self.b2.bind(on_release=self.release_right)
        
        self.l1 = LaySub(size_hint_x=1)
        
        self.add_widget(self.b1)
        self.add_widget(self.l1)
        self.add_widget(self.b2)

    def release_left(self, instance):
        pyautogui.hotkey('alt', 'tab')
        pyautogui.press('left')
        pyautogui.hotkey('alt', 'tab')

    def release_right(self, instance):
        pyautogui.hotkey('alt', 'tab')
        pyautogui.press('right')
        pyautogui.hotkey('alt', 'tab')

class MainApp(App):
    def build(self):
        self.title = "mini key"
        Window.size = (300,100)
        Window.always_on_top = True
        
        layout = LayMain()
        return layout

if __name__=='__main__':
    MainApp().run()


ソースここまで

はい。
ボタンから手を離した状態を検知した所で

Alt+Tab(一つ前にアクティブだったアプリに切り替えるショートカット)
↓
カーソルキー入力
↓
Alt+Tab(本アプリに戻る)

…あまりにもバカバカしい方法ですが、そらそうよという内容。ちなみに挙動は Linux Windows で共通… Mac でどう動くは解りません、持ってないので。
ひょっとして、投稿日現在のAIを出し抜いたのでは…


さておき、挙動は良好で画像ビューアではページ送りができますし、動画再生では巻き戻しも出来ます。ボタンを放した瞬間をとらえている都合上「押しっぱなし」操作はできませんがかなりQOLが上がります。久々に良い物が作れて満足です。

2026年10月4日日曜日

師匠の難題10

新ネタが尽きる事が無い奥の深い難題なのですが…ふと、第5回目に「横で読み込んで斜めに描画する」処理から「斜めに読み込んで横に描画する」方向に転換しましたが、
「横で読み込んで斜めに描画する方向性」でそのまま進んだらどうなったのか?が気になりまして…

まず、第4回で回転できる所まで作っているので、そちらを確認して頂きたいのですが…

「横で読み込んで斜めに描画する方向性」の良い点:きれいに描画できる
従来型だと斜め45度に近づくと画像がギザギザに崩れていきます。
これはこの付近が斜め読み込みした際に最も誤差が出るためであり、横で読み込む場合は画像データの並びと読み込みの方向が一致している為すべての読み込みが均質になり、ズレが起きないので綺麗に出力できます。

「横で読み込んで斜めに描画する方向性」の悪い点:モワレが発生する。
描画時に誤差が出た場合に画像に穴が開く現象です。
で、この穴ですが、よく見ると1ドットずつしか空いていません。
※赤い部分が穴
斜めに描画する方法ではブレゼンハムアルゴリズムで作成した斜線を束ねてて面にしています。この為、段差が出来た所で上図のような隙間ができると思われます。

…で、あれば、予め穴の空きそうなドットに付近の画像を転送しておけばよくね?

というわけで、穴の空きそうなドットですが…
こんな感じ。
斜線を引いて段差が出来た時に赤で示したドットの部分を埋めていく方針。
最初は360度法で45度ずつ区切らないといけないかと思いましたが、最終的にはこのようなコードになりました。
※相変わらず割り算も実数も使っていません

ソースここから

import pyxel

SCREEN_SIZE_W = 160
SCREEN_SIZE_H = 120
CENTER_X = 80
CENTER_Y = 60
EMPTY = -32767

ALPHA_NON       = 0
ALPHA_HALF_1    = 1
ALPHA_HALF_2    = 2
ALPHA_QUARTER_1 = 3
ALPHA_QUARTER_2 = 4
ALPHA_QUARTER_3 = 5
ALPHA_QUARTER_4 = 6

my_table = (
       0,   6,  13,  19,  25,  31,  37,  43,  49,  55,  60,  66,  71,  76,  81,  86,
      91,  95,  99, 103, 106, 110, 113, 116, 118, 121, 122, 124, 126, 127, 127, 128,
     128, 128, 127, 127, 126, 124, 122, 121, 118, 116, 113, 110, 106, 103,  99,  95,
      91,  86,  81,  76,  71,  66,  60,  55,  49,  43,  37,  31,  25,  19,  13,   6,
       0,  -6, -13, -19, -25, -31, -37, -43, -49, -55, -60, -66, -71, -76, -81, -86,
     -91, -95, -99,-103,-106,-110,-113,-116,-118,-121,-122,-124,-126,-127,-127,-128,
    -128,-128,-127,-127,-126,-124,-122,-121,-118,-116,-113,-110,-106,-103,- 99, -95,
     -91, -86, -81, -76, -71, -66, -60, -55, -49, -43, -37, -31, -25, -19, -13,  -6,
       0,   6,  13,  19,  25,  31,  37,  43,  49,  55,  60,  66,  71,  76,  81,  86,
      91,  95,  99, 103, 106, 110, 113, 116, 118, 121, 122, 124, 126, 127, 127, 128
   )

def my_sin(angle,r):
    angle &= 127
    return r * my_table[angle]>>7
    
def my_cos(angle,r):
    angle &= 127
    angle += 32
    return r * my_table[angle]>>7

class VBresenham:
    
    def __init__(self):
        self.l_side = 0
        self.s_side = 0
        
        # horizonatial side is longer
        self.b_long_hs = True

        # Direction (1 or -1)
        self.dct_x = 1
        self.dct_y = 1

        self.counter = 0
        self.adder_x = 0
        self.adder_y = 0
        
        self.start_x = 0
        self.start_y = 0

        self.bLaneChange = False

    # start x,y terminus x,y
    def setup(self,s_x,s_y,t_x,t_y):
        
        self.b_long_hs = True
        self.dct_x = 1
        self.dct_y = 1
        self.counter = 0
        self.adder_x = 0
        self.adder_y = 0
        self.bLaneChange = False

        self.start_x = s_x
        self.start_y = s_y
        
        if (s_x > t_x):
            self.l_side = s_x - t_x
            self.dct_x = -1
        else:
            self.l_side = t_x - s_x

        if (s_y > t_y):
            self.s_side = s_y - t_y
            self.dct_y = -1
        else:
            self.s_side = t_y - s_y
            
        if (self.s_side > self.l_side):
            # swap
            self.s_side,self.l_side = self.l_side,self.s_side
            self.b_long_hs = False

        self.l_side += 1
        self.s_side += 1
        return self.l_side

    def step(self):
        self.counter += self.s_side
        i = self.counter - self.l_side
        
        ret_x  = self.start_x+self.adder_x
        ret_y  = self.start_y+self.adder_y
        ret_LC = self.bLaneChange

        self.bLaneChange = False

        if self.b_long_hs:
            self.adder_x += self.dct_x
            if i >= 0:
                self.counter = i
                self.adder_y += self.dct_y
                self.bLaneChange = True
        else:
            self.adder_y += self.dct_y
            if i >= 0:
                self.counter = i
                self.adder_x += self.dct_x
                self.bLaneChange = True
                
        return ret_x,ret_y,ret_LC

class Bresenham:
    
    def __init__(self):
        self.l_side = 0
        self.s_side = 0
        self.counter = 0
        self.adder = 0
        
        # Src side is longer
        self.b_long_ss = True

    def setup(self,Dest_side,Src_side):
        self.counter = 0
        self.adder = 0
        
        if Dest_side > Src_side:
            self.b_long_ss = False
            self.l_side = Dest_side+1
            self.s_side = Src_side+1
            return self.l_side
            
        self.b_long_ss = True
        self.l_side = Src_side+1
        self.s_side = Dest_side+1
        return self.s_side
        
    def step(self):
        ret = self.adder
        
        if self.b_long_ss:
            self.counter += self.s_side
            i = self.counter - self.l_side
            if i >= 0:
                self.counter = i
                self.adder += 1
        else:
            while self.counter <= self.l_side:
                self.adder += 1
                self.counter += self.s_side
                
            self.counter -= self.l_side

        return ret

class RollBlt:
    
    def __init__(self):
        self.vbre_h = VBresenham()
        self.bre_h  = Bresenham()
        self.vbre_w = VBresenham()
        self.bre_w  = Bresenham()

        self.tile = {
            ALPHA_NON       :self._non,
            ALPHA_HALF_1    :self._half1,
            ALPHA_HALF_2    :self._half2,
            ALPHA_QUARTER_1 :self._quarter1,
            ALPHA_QUARTER_2 :self._quarter2,
            ALPHA_QUARTER_3 :self._quarter3,
            ALPHA_QUARTER_4 :self._quarter4,
        }

    def setup(self):
        pass

    # タイル転送用内部関数群
    def _non(self,x,y,u,v):
        pyxel.blt(x, y, 0, u, v, 1, 1)

    def _half1(self,x,y,u,v):
        if x&1 == y&1:
            pyxel.blt(x, y, 0, u, v, 1, 1)

    def _half2(self,x,y,u,v):
        if x&1 != y&1:
            pyxel.blt(x, y, 0, u, v, 1, 1)

    def _quarter1(self,x,y,u,v):
        if (x&1) + (y&1)==0:
            pyxel.blt(x, y, 0, u, v, 1, 1)

    def _quarter2(self,x,y,u,v):
        if ((x+1)&1) + (y&1)==0:
            pyxel.blt(x, y, 0, u, v, 1, 1)

    def _quarter3(self,x,y,u,v):
        if (x&1) + ((y+1)&1)==0:
            pyxel.blt(x, y, 0, u, v, 1, 1)

    def _quarter4(self,x,y,u,v):
        if (x&1) + (y&1)==2:
            pyxel.blt(x, y, 0, u, v, 1, 1)
    
    # 原点座標中央位置補正コピー    
    def blt_c(self,dest_x, dest_y, bank, src_x, src_y, src_w, src_h, angle, scale_w=128, scale_h=128,alpha=ALPHA_NON):
        h = src_h * scale_h >> 8
        w = src_w * scale_w >> 8
        
        x = dest_x - my_sin(angle,h) - my_sin(angle+32,w)
        y = dest_y - my_cos(angle,h) - my_cos(angle+32,w)
        self.blt(x, y, bank, src_x, src_y, src_w, src_h, angle, scale_w, scale_h, alpha)
        
    def blt(self,dest_x, dest_y, bank, src_x, src_y, src_w, src_h, angle, scale_w=128, scale_h=128, alpha=ALPHA_NON):

        # 角度のまるめ
        angle &= 127

        # 原点から見た縦方向の端の座標
        h = src_h * scale_h >> 7
        tv_x = my_sin(angle,h-1)+dest_x
        tv_y = my_cos(angle,h-1)+dest_y

        # 原点から見た横方向の端の座標
        w = src_w * scale_w >> 7
        th_x = my_sin(angle+32,w-1)+dest_x
        th_y = my_cos(angle+32,w-1)+dest_y

        # 画面内に収まっているかの判定
        top = dest_y
        if tv_y < top:
            top = tv_y
        if th_y < top:
            top = th_y
        if (tv_y + th_y - dest_y) < top:
            top = tv_y + th_y - dest_y

        bottom = dest_y
        if tv_y > bottom:
            bottom = tv_y
        if th_y > bottom:
            bottom = th_y
        if (tv_y + th_y - dest_y) > bottom:
            bottom = tv_y + th_y - dest_y

        # 画面外
        if top < 0 and bottom < 0:
            return
        if top >= SCREEN_SIZE_H and bottom >= SCREEN_SIZE_H:
            return
        
        left = dest_x
        if tv_x < left:
            left = tv_x
        if th_x < left:
            left = th_x
        if (tv_x + th_x - dest_x) < left:
            left = tv_x + th_x - dest_x

        right = dest_x
        if tv_x > right:
            right = tv_x
        if th_x > right:
            right = th_x
        if (tv_x + th_x - dest_x) > right:
            right = tv_x + th_x - dest_x

        # 画面外
        if left < 0 and right < 0:
            return
        if left >= SCREEN_SIZE_W and right >= SCREEN_SIZE_W:
            return

        # 一旦横方向は原点を引いてベクトルとしないといけない、意味が変わるので注意
        th_x -= dest_x
        th_y -= dest_y

        base_reg = self.vbre_h.setup(dest_x,dest_y,tv_x,tv_y)
        self.bre_h.setup(src_h-1,base_reg-1)
        
        match angle:
            case a if a < 32:

                for h in range (base_reg):
                    base_x,base_y,bDummy = self.vbre_h.step()
                    add_src_y = self.bre_h.step()

                    reg = self.vbre_w.setup(base_x,base_y,base_x+th_x,base_y+th_y)
                    self.bre_w.setup(src_w-1,reg-1)

                    old_x = base_x
                    old_y = base_y
                    
                    for w in range (reg):
                        x,y,bLC = self.vbre_w.step()
                        add_src_x = self.bre_w.step()

                        if x < 0 or y >= SCREEN_SIZE_H:
                            continue
                        
                        if x >= SCREEN_SIZE_W:
                            break

                        if bLC:
                            self.tile[alpha](x,old_y,src_x+add_src_x,src_y+add_src_y)

                        if y < 0:
                            break

                        self.tile[alpha](x,y,src_x+add_src_x,src_y+add_src_y)
                            
                        old_x = x
                        old_y = y
                        
            case a if a < 64:

                for h in range (base_reg):
                    base_x,base_y,bDummy = self.vbre_h.step()
                    add_src_y = self.bre_h.step()

                    reg = self.vbre_w.setup(base_x,base_y,base_x+th_x,base_y+th_y)
                    self.bre_w.setup(src_w-1,reg-1)

                    old_x = base_x
                    old_y = base_y
                    
                    for w in range (reg):
                        x,y,bLC = self.vbre_w.step()
                        add_src_x = self.bre_w.step()

                        if y >= SCREEN_SIZE_H or x >= SCREEN_SIZE_W:
                            continue

                        if y < 0:
                            break

                        if bLC:
                            self.tile[alpha](old_x,y,src_x+add_src_x,src_y+add_src_y)

                        if x < 0:
                            break
                            
                        self.tile[alpha](x,y,src_x+add_src_x,src_y+add_src_y)
                            
                        old_x = x
                        old_y = y

            case a if a < 96:

                for h in range (base_reg):
                    base_x,base_y,bDummy = self.vbre_h.step()
                    add_src_y = self.bre_h.step()

                    reg = self.vbre_w.setup(base_x,base_y,base_x+th_x,base_y+th_y)
                    self.bre_w.setup(src_w-1,reg-1)

                    old_x = base_x
                    old_y = base_y
                    
                    for w in range (reg):
                        x,y,bLC = self.vbre_w.step()
                        add_src_x = self.bre_w.step()

                        if x >= SCREEN_SIZE_W or y < 0:
                            continue

                        if x < 0:
                            break

                        if bLC:
                            self.tile[alpha](x,old_y,src_x+add_src_x,src_y+add_src_y)

                        if y >= SCREEN_SIZE_H:
                            break

                        self.tile[alpha](x,y,src_x+add_src_x,src_y+add_src_y)
                            
                        old_x = x
                        old_y = y
                        
            case _:

                for h in range (base_reg):
                    base_x,base_y,bDummy = self.vbre_h.step()
                    add_src_y = self.bre_h.step()

                    reg = self.vbre_w.setup(base_x,base_y,base_x+th_x,base_y+th_y)
                    self.bre_w.setup(src_w-1,reg-1)

                    old_x = base_x
                    old_y = base_y
                    
                    for w in range (reg):
                        x,y,bLC = self.vbre_w.step()
                        add_src_x = self.bre_w.step()

                        if y < 0 or x < 0:
                            continue

                        if y >= SCREEN_SIZE_H:
                            break

                        if bLC:
                            self.tile[alpha](old_x,y,src_x+add_src_x,src_y+add_src_y)

                        if x >= SCREEN_SIZE_W:
                            break
                            
                        self.tile[alpha](x,y,src_x+add_src_x,src_y+add_src_y)

                        old_x = x
                        old_y = y

class App:

    def __init__(self):
        # 画面サイズ 160x120 で初期化
        pyxel.init(SCREEN_SIZE_W, SCREEN_SIZE_H, title="RollBlt")
        self.add_x = 0
        pyxel.load("yajirushi.pyxres")

        self.rb = RollBlt()
        
        self.angle  = 0
        self.angle2 = 0

        pyxel.run(self.update, self.draw)

    def update(self):
        self.angle += 1
        self.angle &= 127
        
        self.angle2 -= 1
        self.angle2 &= 127

    def draw(self):
        # 画面を黒(色番号0)でクリア
        pyxel.cls(0)
        
        #self.rb.blt(CENTER_X, CENTER_Y, 0, 0, 0, 32, 16, 16,512,512)
        #self.rb.blt_c(CENTER_X, CENTER_Y, 0, 0, 0, 32, 16, self.angle,256,256)
        #self.rb.blt_c(CENTER_X, CENTER_Y, 0, 0, 0, 32, 16, self.angle,512,512)

        #self.rb.blt_c(CENTER_X, CENTER_Y, 0, 0, 0, 32, 16, self.angle,scale_w=64,scale_h=64)
        #self.rb.blt_c(CENTER_X, CENTER_Y, 0, 0, 0, 32, 16, self.angle,scale_w=256,scale_h=256)
        """
        self.rb.blt_c(CENTER_X, CENTER_Y, 0, 0, 0, 32, 16, self.angle,scale_w=512,scale_h=512, alpha=ALPHA_HALF_1)
        self.rb.blt_c(CENTER_X, CENTER_Y, 0, 0, 0, 32, 16, self.angle2,scale_w=512,scale_h=512, alpha=ALPHA_HALF_2)
        """
        #"""
        self.rb.blt_c(CENTER_X-30, CENTER_Y-20, 0, 0, 0, 32, 16, self.angle,scale_w=512,scale_h=512, alpha=ALPHA_QUARTER_1)
        self.rb.blt_c(CENTER_X-30, CENTER_Y+20, 0, 0, 0, 32, 16, self.angle2,scale_w=512,scale_h=512, alpha=ALPHA_QUARTER_4)
        self.rb.blt_c(CENTER_X+30, CENTER_Y-20, 0, 0, 0, 32, 16, self.angle,scale_w=512,scale_h=512, alpha=ALPHA_QUARTER_2)
        self.rb.blt_c(CENTER_X+30, CENTER_Y+20, 0, 0, 0, 32, 16, self.angle2,scale_w=512,scale_h=512, alpha=ALPHA_QUARTER_3)
        #"""

App()

ソースここまで

これまでのソースでは斜め線を求めるブレゼンハムアルゴリズムのクラス VBresenham は1ステップ進むたびに座標だけを返していましたが、段差ができたかのフラグも追加で返す事にしました。

で、出力ですが…


絵の出力は完璧。ソースコードも短くなりました。
ただ、斜めに読み込み横に書き込む方法は、最初に外枠とその間をどう補完するのかをあらかじめ求めてラインバッファに保管し、その後に一気に描画する方法で描画や判定処理が少なく抑えられています。
対して今回の方法はバッファに一時保管するといった処理を行いませんが、そもそもモワレが発生しない浅い角度の時は予防として穴埋めしている処理が全て無駄になる(上書きされる)事になりますし、1ドットを転送する為にかかる処理が大変重かったりします。
セレロン2コアのパソコンでデバッグしている分には体感できるレベルで処理落ちしており、実測では2~3割ほど処理が重いようです。

普通画像転送といえば見栄えを重視しますからこちらの方が正解なのですが、エフェクト等のブレても良いものなら以前のものの方が良いのかもしれません。

2026年10月1日木曜日

師匠の難題9

以前疑似アルファのタイル転送を作った後、いろいろ弄っていて気付いた点を二つほど。

関数テーブルもどき
うーん? Python が内部でC言語が走っているってのはなんとなく聞いていたのですが…なら関数テーブルを作れないかな?と、思ったらクラスのメンバー関数を辞書登録する方法が見つかりまして。ただ、流石に1ドット打つごとに文字列をキーとした検索を行っていたら重すぎると思っていたら、数字でもできると判り試してみました…

ソースここから

import pyxel

SCREEN_SIZE_W = 160
SCREEN_SIZE_H = 120
CENTER_X = 80
CENTER_Y = 60
EMPTY = -32767

ALPHA_NON       = 0
ALPHA_HALF_1    = 1
ALPHA_HALF_2    = 2
ALPHA_QUARTER_1 = 3
ALPHA_QUARTER_2 = 4
ALPHA_QUARTER_3 = 5
ALPHA_QUARTER_4 = 6

my_table = (
       0,   6,  13,  19,  25,  31,  37,  43,  49,  55,  60,  66,  71,  76,  81,  86,
      91,  95,  99, 103, 106, 110, 113, 116, 118, 121, 122, 124, 126, 127, 127, 128,
     128, 128, 127, 127, 126, 124, 122, 121, 118, 116, 113, 110, 106, 103,  99,  95,
      91,  86,  81,  76,  71,  66,  60,  55,  49,  43,  37,  31,  25,  19,  13,   6,
       0,  -6, -13, -19, -25, -31, -37, -43, -49, -55, -60, -66, -71, -76, -81, -86,
     -91, -95, -99,-103,-106,-110,-113,-116,-118,-121,-122,-124,-126,-127,-127,-128,
    -128,-128,-127,-127,-126,-124,-122,-121,-118,-116,-113,-110,-106,-103,- 99, -95,
     -91, -86, -81, -76, -71, -66, -60, -55, -49, -43, -37, -31, -25, -19, -13,  -6,
       0,   6,  13,  19,  25,  31,  37,  43,  49,  55,  60,  66,  71,  76,  81,  86,
      91,  95,  99, 103, 106, 110, 113, 116, 118, 121, 122, 124, 126, 127, 127, 128
   )

def my_sin(angle,r):
    angle &= 127
    return r * my_table[angle]>>7
    
def my_cos(angle,r):
    angle &= 127
    angle += 32
    return r * my_table[angle]>>7

class VBresenham:
    
    def __init__(self):
        self.l_side = 0
        self.s_side = 0
        
        # horizonatial side is longer
        self.b_long_hs = True

        # Direction (1 or -1)
        self.dct_x = 1
        self.dct_y = 1

        self.counter = 0
        self.adder_x = 0
        self.adder_y = 0
        
        self.start_x = 0
        self.start_y = 0

    # start x,y terminus x,y
    def setup(self,s_x,s_y,t_x,t_y):
        
        self.b_long_hs = True
        self.dct_x = 1
        self.dct_y = 1
        self.counter = 0
        self.adder_x = 0
        self.adder_y = 0

        self.start_x = s_x
        self.start_y = s_y
        
        if (s_x > t_x):
            self.l_side = s_x - t_x
            self.dct_x = -1
        else:
            self.l_side = t_x - s_x

        if (s_y > t_y):
            self.s_side = s_y - t_y
            self.dct_y = -1
        else:
            self.s_side = t_y - s_y
            
        if (self.s_side > self.l_side):
            # swap
            self.s_side,self.l_side = self.l_side,self.s_side
            self.b_long_hs = False

        self.l_side += 1
        self.s_side += 1
        return self.l_side

    def step(self):
        self.counter += self.s_side
        i = self.counter - self.l_side
        
        ret_x = self.start_x+self.adder_x
        ret_y = self.start_y+self.adder_y

        if self.b_long_hs:
            self.adder_x += self.dct_x
            if i >= 0:
                self.counter = i
                self.adder_y += self.dct_y
        else:
            self.adder_y += self.dct_y
            if i >= 0:
                self.counter = i
                self.adder_x += self.dct_x
                
        return ret_x,ret_y

class Bresenham:
    
    def __init__(self):
        self.l_side = 0
        self.s_side = 0
        self.counter = 0
        self.adder = 0
        
        # Src side is longer
        self.b_long_ss = True

    def setup(self,Dest_side,Src_side):
        self.counter = 0
        self.adder = 0
        
        if Dest_side > Src_side:
            self.b_long_ss = False
            self.l_side = Dest_side+1
            self.s_side = Src_side+1
            return self.l_side
            
        self.b_long_ss = True
        self.l_side = Src_side+1
        self.s_side = Dest_side+1
        return self.s_side
        
    def step(self):
        ret = self.adder
        
        if self.b_long_ss:
            self.counter += self.s_side
            i = self.counter - self.l_side
            if i >= 0:
                self.counter = i
                self.adder += 1
        else:
            while self.counter <= self.l_side:
                self.adder += 1
                self.counter += self.s_side
                
            self.counter -= self.l_side

        return ret

class LineBuffer:
    def __init__(self):
        self.l = 0 # left
        self.r = 0 # right

        # 始点
        self.src_sx = 0
        self.src_sy = 0
        
        # 終点
        self.src_tx = 0
        self.src_ty = 0

class RollBlt:
    
    def __init__(self):
        self.buff = []
        for i in range(SCREEN_SIZE_H):
            self.buff.append(LineBuffer())

        self.top = 0
        self.bottom = 0

        self.vbre = VBresenham()
        self.bre  = Bresenham()

        self.once  = True

        self.tile = {
            ALPHA_NON       :self._non,
            ALPHA_HALF_1    :self._half1,
            ALPHA_HALF_2    :self._half2,
            ALPHA_QUARTER_1 :self._quarter1,
            ALPHA_QUARTER_2 :self._quarter2,
            ALPHA_QUARTER_3 :self._quarter3,
            ALPHA_QUARTER_4 :self._quarter4,
        }

    def setup(self):
        pass

    # タイル転送用内部関数群
    def _non(self,x,y,u,v):
        pyxel.blt(x, y, 0, u, v, 1, 1)

    def _half1(self,x,y,u,v):
        if x&1 == y&1:
            pyxel.blt(x, y, 0, u, v, 1, 1)

    def _half2(self,x,y,u,v):
        if x&1 != y&1:
            pyxel.blt(x, y, 0, u, v, 1, 1)

    def _quarter1(self,x,y,u,v):
        if (x&1) + (y&1)==0:
            pyxel.blt(x, y, 0, u, v, 1, 1)

    def _quarter2(self,x,y,u,v):
        if ((x+1)&1) + (y&1)==0:
            pyxel.blt(x, y, 0, u, v, 1, 1)

    def _quarter3(self,x,y,u,v):
        if (x&1) + ((y+1)&1)==0:
            pyxel.blt(x, y, 0, u, v, 1, 1)

    def _quarter4(self,x,y,u,v):
        if (x&1) + (y&1)==2:
            pyxel.blt(x, y, 0, u, v, 1, 1)
    
    # 原点座標中央位置補正コピー    
    def blt_c(self,dest_x, dest_y, bank, src_x, src_y, src_w, src_h, angle, scale_w=128, scale_h=128,alpha=ALPHA_NON):
        h = src_h * scale_h >> 8
        w = src_w * scale_w >> 8
        
        x = dest_x - my_sin(angle,h) - my_sin(angle+32,w)
        y = dest_y - my_cos(angle,h) - my_cos(angle+32,w)
        self.blt(x, y, bank, src_x, src_y, src_w, src_h, angle, scale_w, scale_h, alpha)
        
    def blt(self,dest_x, dest_y, bank, src_x, src_y, src_w, src_h, angle, scale_w=128, scale_h=128, alpha=ALPHA_NON):

        # 角度のまるめ
        angle &= 127

        # 原点から見た縦方向の端の座標
        h = src_h * scale_h >> 7
        tv_x = my_sin(angle,h-1)+dest_x
        tv_y = my_cos(angle,h-1)+dest_y

        # 原点から見た横方向の端の座標
        w = src_w * scale_w >> 7
        th_x = my_sin(angle+32,w-1)+dest_x
        th_y = my_cos(angle+32,w-1)+dest_y
        
        # 4頂点と比較し上下の座標を求める
        self.top = dest_y
        if tv_y < self.top:
            self.top = tv_y
        if th_y < self.top:
            self.top = th_y
        if (tv_y + th_y - dest_y) < self.top:
            self.top = tv_y + th_y - dest_y
        if self.top < 0:
            self.top = 0

        self.bottom = dest_y
        if tv_y > self.bottom:
            self.bottom = tv_y
        if th_y > self.bottom:
            self.bottom = th_y
        if (tv_y + th_y - dest_y) > self.bottom:
            self.bottom = tv_y + th_y - dest_y
        if self.bottom > (SCREEN_SIZE_H-1):
            self.bottom = SCREEN_SIZE_H-1

        # テーブル初期化
        for y in range (self.top, self.bottom+1):
            self.buff[y].l = EMPTY
            self.buff[y].r = EMPTY

        # 画像縦方向処理
        reg = self.vbre.setup(dest_x,dest_y,tv_x,tv_y)
        self.bre.setup(src_h-1,reg-1)

        match angle:
            # 垂直にコピーする時は縦方向の処理を行わない
            case a if a == 32 or a == 96:
                pass
                
            case a if a < 32:
                
                for i in range (reg):
                    x,y = self.vbre.step()
                    add_src_y = self.bre.step()

                    # MainSide
                    if y >= 0 and y < SCREEN_SIZE_H:
                        
                        if self.buff[y].l == EMPTY:
                            self.buff[y].l = x
                        #pyxel.pset(x,y,6)

                        self.buff[y].src_sx = src_x
                        self.buff[y].src_sy = src_y + add_src_y
                    
                    x = x - dest_x + th_x
                    y = y - dest_y + th_y

                    # SubSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        self.buff[y].r = x
                        #pyxel.pset(x,y,6)
                        
                        self.buff[y].src_tx = src_x + src_w-1
                        self.buff[y].src_ty = src_y + add_src_y

            case a if a < 64:

                for i in range (reg):
                    x,y = self.vbre.step()
                    add_src_y = self.bre.step()
                    
                    # MainSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        self.buff[y].r = x
                        #pyxel.pset(x,y,6)

                        self.buff[y].src_tx = src_x
                        self.buff[y].src_ty = src_y + add_src_y
                        
                    x = x - dest_x + th_x
                    y = y - dest_y + th_y
                    
                    # SubSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        if self.buff[y].l == EMPTY:
                            self.buff[y].l = x
                        #pyxel.pset(x,y,6)

                        self.buff[y].src_sx = src_x + src_w-1
                        self.buff[y].src_sy = src_y + add_src_y
            
            case a if a < 96:

                for i in range (reg):
                    x,y = self.vbre.step()
                    add_src_y = self.bre.step()
                    
                    # MainSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        if self.buff[y].r == EMPTY:
                            self.buff[y].r = x
                        #pyxel.pset(x,y,6)

                        self.buff[y].src_tx = src_x
                        self.buff[y].src_ty = src_y + add_src_y
                    
                    x = x - dest_x + th_x
                    y = y - dest_y + th_y
                    
                    # SubSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        self.buff[y].l = x
                        #pyxel.pset(x,y,6)

                        self.buff[y].src_sx = src_x + src_w-1
                        self.buff[y].src_sy = src_y + add_src_y

            case _:

                for i in range (reg):
                    x,y = self.vbre.step()
                    add_src_y = self.bre.step()
                
                    # MainSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        self.buff[y].l = x
                        #pyxel.pset(x,y,6)

                        self.buff[y].src_sx = src_x
                        self.buff[y].src_sy = src_y + add_src_y

                    x = x - dest_x + th_x
                    y = y - dest_y + th_y
                    
                    # SubSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        if self.buff[y].r == EMPTY:
                            self.buff[y].r = x
                        #pyxel.pset(x,y,6)

                        self.buff[y].src_tx = src_x + src_w-1
                        self.buff[y].src_ty = src_y + add_src_y

        # 画像横方向処理
        reg = self.vbre.setup(dest_x,dest_y,th_x,th_y)
        self.bre.setup(src_w-1,reg-1)

        match angle:
            # 水平にコピーする時は横方向の処理を行わない
            case a if angle == 0 or angle == 64:
                pass
                
            case a if a < 32:
                
                for i in range (reg):
                    x,y = self.vbre.step()
                    add_src_x = self.bre.step()
                    
                    # MainSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        if self.buff[y].l == EMPTY:
                            self.buff[y].l = x
                        #pyxel.pset(x,y,6)

                        self.buff[y].src_sx = src_x + add_src_x
                        self.buff[y].src_sy = src_y
                    
                    x = x - dest_x + tv_x
                    y = y - dest_y + tv_y

                    # SubSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        self.buff[y].r = x
                        #pyxel.pset(x,y,6)

                        self.buff[y].src_tx = src_x + add_src_x
                        self.buff[y].src_ty = src_y + src_h-1
                
            case a if a < 64:

                for i in range (reg):
                    x,y = self.vbre.step()
                    add_src_x = self.bre.step()
                
                    # MainSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        self.buff[y].l = x
                        #pyxel.pset(x,y,6)

                        self.buff[y].src_sx = src_x + add_src_x
                        self.buff[y].src_sy = src_y
                    
                    x = x - dest_x + tv_x
                    y = y - dest_y + tv_y

                    # SubSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        if self.buff[y].r == EMPTY:
                            self.buff[y].r = x
                        #pyxel.pset(x,y,6)

                        self.buff[y].src_tx = src_x + add_src_x
                        self.buff[y].src_ty = src_y + src_h-1

            case a if a < 96:

                for i in range (reg):
                    x,y = self.vbre.step()
                    add_src_x = self.bre.step()

                    # MainSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        if self.buff[y].r == EMPTY:
                            self.buff[y].r = x
                        #pyxel.pset(x,y,6)

                        self.buff[y].src_tx = src_x + add_src_x
                        self.buff[y].src_ty = src_y
                    
                    x = x - dest_x + tv_x
                    y = y - dest_y + tv_y

                    # SubSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        self.buff[y].l = x
                        #pyxel.pset(x,y,6)

                        self.buff[y].src_sx = src_x + add_src_x
                        self.buff[y].src_sy = src_y + src_h-1

            case _:
                
                for i in range (reg):
                    x,y = self.vbre.step()
                    add_src_x = self.bre.step()
                    
                    # MainSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        self.buff[y].r = x
                        #pyxel.pset(x,y,6)

                        self.buff[y].src_tx = src_x + add_src_x
                        self.buff[y].src_ty = src_y
                    
                    x = x - dest_x + tv_x
                    y = y - dest_y + tv_y

                    # SubSide
                    if y >= 0 and y < SCREEN_SIZE_H:

                        if self.buff[y].l == EMPTY:
                            self.buff[y].l = x
                        #pyxel.pset(x,y,6)

                        self.buff[y].src_sx = src_x + add_src_x
                        self.buff[y].src_sy = src_y + src_h-1

        # バッファを基に転送を行う

        for y in range (self.top, self.bottom+1):
            reg = self.vbre.setup(self.buff[y].src_sx,self.buff[y].src_sy,
                                  self.buff[y].src_tx,self.buff[y].src_ty)
            self.bre.setup(reg-1,self.buff[y].r - self.buff[y].l)
            
            u,v = self.vbre.step()
            new_src = self.bre.step()               
            old_src = new_src

            for x in range (self.buff[y].l,self.buff[y].r+1):
                new_src = self.bre.step()
                for step_src in range (old_src, new_src):
                    u,v = self.vbre.step()
                old_src = new_src
                
                # 画面左右端処理
                if x < 0:
                    continue
                if x >= SCREEN_SIZE_W:
                    break
                
                self.tile[alpha](x,y,u,v)
                #pyxel.pset(x,y,pyxel.images[0].pget(sx,sy))

class App:

    def __init__(self):
        # 画面サイズ 160x120 で初期化
        pyxel.init(SCREEN_SIZE_W, SCREEN_SIZE_H, title="RollBlt")
        self.add_x = 0
        pyxel.load("yajirushi.pyxres")

        self.rb = RollBlt()
        
        self.angle  = 0
        self.angle2 = 0

        pyxel.run(self.update, self.draw)

    def update(self):
        self.angle += 1
        self.angle &= 127
        
        self.angle2 -= 1
        self.angle2 &= 127

    def draw(self):
        # 画面を黒(色番号0)でクリア
        pyxel.cls(0)
        
        #self.rb.blt(CENTER_X, CENTER_Y, 0, 0, 0, 32, 16, 16,512,512)
        self.rb.blt_c(CENTER_X, CENTER_Y, 0, 0, 0, 32, 16, self.angle,512,512)

        #self.rb.blt_c(CENTER_X, CENTER_Y, 0, 0, 0, 32, 16, self.angle,scale_w=64,scale_h=64)
        #self.rb.blt_c(CENTER_X, CENTER_Y, 0, 0, 0, 32, 16, self.angle,scale_w=256,scale_h=256)
        """
        self.rb.blt_c(CENTER_X, CENTER_Y, 0, 0, 0, 32, 16, self.angle,scale_w=512,scale_h=512, alpha=ALPHA_HALF_1)
        self.rb.blt_c(CENTER_X, CENTER_Y, 0, 0, 0, 32, 16, self.angle2,scale_w=512,scale_h=512, alpha=ALPHA_HALF_2)
        """
        """
        self.rb.blt_c(CENTER_X-30, CENTER_Y-20, 0, 0, 0, 32, 16, self.angle,scale_w=512,scale_h=512, alpha=ALPHA_QUARTER_1)
        self.rb.blt_c(CENTER_X-30, CENTER_Y+20, 0, 0, 0, 32, 16, self.angle2,scale_w=512,scale_h=512, alpha=ALPHA_QUARTER_4)
        self.rb.blt_c(CENTER_X+30, CENTER_Y-20, 0, 0, 0, 32, 16, self.angle,scale_w=512,scale_h=512, alpha=ALPHA_QUARTER_2)
        self.rb.blt_c(CENTER_X+30, CENTER_Y+20, 0, 0, 0, 32, 16, self.angle2,scale_w=512,scale_h=512, alpha=ALPHA_QUARTER_3)
        """

App()


ソースここまで

微妙に三角関数テーブルを修正していたり、描画部を変えていたりしますが、基本的には前回と出力結果は変わりません。
ただ、実際に1ドットずつ画像を転送する部分を関数テーブルに分けた事でソースコードは大分短くなっています。
気になる実行速度の鈍化ですが、思ったよりも重くない様子…
いや、1ドット打つごとに遅くなるので数万回のオーバーヘッドがかかっており、確実に遅くはなっているハズなのですがそれでも影響がないと言う事は機材が進歩したって事ですかね。


点描に変えてみたら
ふと、1ドット毎に Blt 関数使っているなら1ドット毎に元画像のパレット取得して、転送先に1ドットずつ描画した方が早くない?と、思ってやってみたのですが…

たとえばココを…

    def _non(self,x,y,u,v):
        pyxel.blt(x, y, 0, u, v, 1, 1)


こうする

    def _non(self,x,y,u,v):
        col = pyxel.images[0].pget(u, v)
        pyxel.pset(x,y,col)


めちゃくちゃ遅い
どういうわけだか Pyxel に関して言えば1ドット毎に blt 命令を繰り返す事に対して1ドット毎に pget-pset を繰り返す方が断然遅い。
自動的に画面が拡大される仕様上、Blt の方は内部的にハードウェアを使った拡大を行っている可能性は高いのだが…ここまであからさまに処理落ちが起きるとは思っていなかったので意外でした。これだと実際にゲームで使用する際に多用されるパレット0の時は透過させるといった処理を組むととても重いのでは…と思いました。

2026年9月17日木曜日

円と三角のコリジョン

以前師匠の難題7の際にちょっと触れたカリング判定式…検索したらCGソフト系の解説で単に3D処理を軽減する判定…と解説されている記事が見つかりまして。そら、CGツールの利用から考えれば理解する所はそこだけで良いのだけれども…と思って簡単なソースを書いてみました。

そもカリングとは
はい、巻くを意味するカールの動名詞です。それの判定式ですから…
3頂点が右に巻いているか、左に巻いているか、あるいはどちらでも無い(全て同一点もしくは直線状に並んでいる)かの判定に用いられます。
どちらでもなければ0、右/左なら正もしくは負の値を返すのですが…
どちらが右回りでどちらが左回りかは環境によるのでなんとも言えないです、DirectX と OprnGL でも違いましたし、ゲーム機とCGツールで齟齬が出た事もありました…すり合わせで地獄を見た記憶が…

カリング判定の計算式
(X1-Px)(Y2-Py)-(X2-Px)(Y1-Py)

えらく簡単です。やっている事は3次元の面法線を外積から求める計算から、必要な部分だけを抽出した計算式…らしいのですが、正直外積の理解がイマイチなので綺麗に説明できません。
ちなみに面法線とは Blender では水色に見えるコレ(面に垂直に立つ面の向いている方向を示すベクトル)の事で、コレが手前に向いているか奥に向いているかを抽出しています。

表示処理の軽減:裏面省略
サイコロのような立方体を考えた際、これを3角ポリゴンで表現するとして…

これらの表面に例えば右巻きのマークを描きます。
で、このサイコロを回してみたらどうなるか?を考えてみましょう。
裏面…というか手前側から観えない面は左巻きになりますよね。
ならば、逆巻になったポリゴンは見えないのだから描画処理を省略すれば良い…というのが裏面省略でした。
…が、最近は光の反射計算等を行っている場合もあるので単純に省略すれば良いと言うものでも無くなってきています。

又、応用として、右巻き/左巻きの面の境界になる辺を線で描画すると輪郭線ができるため、アニメ調のアウトライン処理を行う際にも用いられています。


表示処理の軽減:視界によるオブジェクト省略
この図を上からの俯瞰図ととらえ、三角の下の頂点を視点、三角を視野角の範囲とすると青く変色した×マークが視野角の範囲に入った物で、それ以外は見えていない物になります。
ならば見えていないオブジェクトを省略すれば処理を大きく省略できます。もちろん光の反射等もあるので(以下略

どう判定するのかと言えば…
視野の範囲の三頂点がどちら周りかを調べ…
判定する点と各辺の頂点がどちら周りになるのかを調べます。
これらが全て同じ方向に回転している場合、判定する頂点は三角の中にあると判ります。


視界の範囲…を三角で示しましたが「視界って距離依存だから三角ではなくないか?」という当然の疑問が出ます。はい、正確には円の判定を組み合わせて距離の判定も行います…


円の範囲判定

(Px-Ox)(Px-Ox)+(Py-Oy)(Py-Oy)

これを円の半径の二乗と比較します。式自体はピタゴラスの定理って奴ですね。
直角三角形の底辺の二乗と高さの二乗の値が斜辺の二乗の値に等しいという奴です。
この斜辺の二乗と半径の二乗を比較して半径の二乗の方が大きければ円の範囲内に入っている事になります。

より正確には斜辺の二乗の平方根が斜辺の長さになり(あたり前)、この値と半径の長さを比較すれば良いのだけど、平方根の計算は非常に重い処理である事と、二乗の場合必ず正の値になる為比較が用意である事、各値の絶対値の大小関係と二乗の大小関係が同じである事から二乗の値で比較を行います。


でもって二つの処理を重ねた物がこんな感じ

ソースここから

import pyxel
import random

SCREEN_SIZE_W = 320
SCREEN_SIZE_H = 240

OBJ_MAX = 100
RADIUS = 50

def JudgCircle(px,py,circle_x,circle_y,r):
    
    x = px - circle_x
    y = py - circle_y
    
    if r*r >= x*x+y*y:
        return True
    return False

def JudgCulling(px,py,ax,ay,bx,by):
    return (ax-px)*(by-py)-(bx-px)*(ay-py)

def JudgTriangle(px,py,ax,ay,bx,by,cx,cy):
    f = JudgCulling(ax,ay,bx,by,cx,cy)
    f1 = JudgCulling(px,py,ax,ay,bx,by)
    f2 = JudgCulling(px,py,bx,by,cx,cy)
    f3 = JudgCulling(px,py,cx,cy,ax,ay)
    
    if f == 0:
        return False

    if f < 0 and f1 < 0 and f2 < 0 and f3 < 0:
        return True

    if f > 0 and f1 > 0 and f2 > 0 and f3 > 0:
        return True

    return False

class Obj:
    def __init__(self):
        x = 0
        y = 0
        color = 2
    
class App:

    def __init__(self):
        # 画面サイズ 320x240 で初期化
        pyxel.init(SCREEN_SIZE_W, SCREEN_SIZE_H, title="judg")
        
        self.object = []
        for i in range(OBJ_MAX):
            self.object.append(Obj())
            self.object[i].x = random.randint(0,SCREEN_SIZE_W)
            self.object[i].y = random.randint(0,SCREEN_SIZE_H)
            self.object[i].color = 2
        
        pyxel.run(self.update, self.draw)

    def update(self):
        pass

    def draw(self):
        # 画面を黒(色番号0)でクリア
        pyxel.cls(0)
        
        mx = pyxel.mouse_x
        my = pyxel.mouse_y
        
        t1x = mx + int(RADIUS/2)
        t1y = my - RADIUS
        t2x = mx - int(RADIUS/2)
        t2y = my - RADIUS

        pyxel.circb(mx,my,RADIUS,15)
        
        pyxel.line(mx,my,t1x,t1y,15)
        pyxel.line(mx,my,t2x,t2y,15)
        pyxel.line(t1x,t1y,t2x,t2y,15)
        
        for i in range(OBJ_MAX):
            self.object[i].color = 2
            
            if JudgCircle(self.object[i].x,self.object[i].y,mx,my,RADIUS):
                if JudgTriangle(self.object[i].x,self.object[i].y,mx,my,t1x,t1y,t2x,t2y):
                    self.object[i].color = 3
            
            pyxel.line(self.object[i].x-5,self.object[i].y-5,
                       self.object[i].x+5,self.object[i].y+5,
                       self.object[i].color)
            pyxel.line(self.object[i].x+5,self.object[i].y-5,
                       self.object[i].x-5,self.object[i].y+5,
                       self.object[i].color)
App()

ソースここまで

…三角だけの時とそこまで変わらなくない?という気がしないでもないですが、オブジェクト表示そのものを省略できるならかなりの軽量化になりますし、
円の判定計算は観ての通り非常に軽いのでやっておいて損は無いと思います。


はい、そんなわけで円と三角のコリジョン判定式でした
ぶっちゃけた話、2Dアクションゲームの当たり判定のほとんどはこの二つの式で片付きます。昔はよく取り扱ったなあと思い、書いてみました。