0.Prologue Easing / Tweening
この記事では、Pyxelでイージングまたはトゥイーンを扱う方法を説明します。まずは、イージングとトゥイーンとは何ぞやというところから始めますね。
Easing (イージング)とTweening (トゥイーン)は、主にアニメーションやモーショングラフィックス分野の用語で密接に関連しています。
- Tweening (トゥイーン):アニメーションの中間フレームを自動生成する技術(補間)を指します。
- Easing (イージング):そのトゥイーン(中間補間)の速度変化(緩急)を定義する機能を指します。
- 両者とも「動きを滑らかに見せる」という意味では同義です。
今後は、私が使い慣れているイージングで話を進めたいと思います。
主要なイージング30種類
https://easings.net/ja
(各イージングの上にマウスカーソルを置くと、そのイージングが確認できます)

1.PyTweenig
PyTweeningは、Pythonで実装されたイージング関数の集まりで、主要なイージング30種類に他の3種類を加えた計33種類の関数群になっています。(等速処理の Linear を入れると34種類になります)
- PyTweening のホームページ
https://github.com/asweigart/pytweening
- PyTweeningイージング33種イッキ見
Web版Pyxelデモ:https://malo21st.github.io/Pyxel_Pytweening/demo/
(アプリ起動後、クリックしてお楽しみ下さい)
また、PyTweeningは2つの方法でイージングを提供しています。
-
ease系関数
0.0から始まり1.0で終わる時間経過を入力すると、線分の過程を0.0~1.0の値で出力します。
import pytweening as tween
# 0.0~1.0 の範囲で easeInQuad を適用
for time_progress in [0.0, 0.25, 0.5, 0.75, 1.0]:
line_progress = tween.easeInQuad(time_progress)
print(f"IN: {time_progress:.2f} OUT: {line_progress}")
IN: 0.00 OUT: 0.0
IN: 0.25 OUT: 0.0625
IN: 0.50 OUT: 0.25
IN: 0.75 OUT: 0.5625
IN: 1.00 OUT: 1.0
-
iterEase系関数
始点(startX, startY)と終点(endX, endY)、そして、間隔サイズ(intervalSize) [ 0.0~1.0 ] を入力すると、間隔サイズ毎のXY座標のジェネレータを出力します。
import pytweening as tween
# (0, 0)から(1, 100)へ4ステップで easeInQuad の値を生成
startX, startY = 0, 0
endX, endY = 1, 100
intervalSize = 0.25 # 1/4 [ 0.0~1.0 ]
iter_point_progress = tween.iterEaseInQuad(startX, startY, endX, endY, intervalSize)
print(f"{type(iter_point_progress)=}")
for point in iter_point_progress:
print(point)
type(iter_point_progress)=<class 'generator'>
(0.0, 0.0)
(0.0625, 6.25)
(0.25, 25.0)
(0.5625, 56.25)
(1.0, 100.0)
- この2種類の関数に共通して言えることは、始まりと終わり、そして時間の経過具合や間隔を指定するだけで、その間の中間フレームを生成してくれるところです。
2.Pyxel × PyTweening
(1) 簡単な例から
PyxelでPyTweeningを使うなら、Pyxelが2次元座標を取り扱うことが多いので、iterEase系関数がおススメです。
例えば、30FPSで、始点(10, 50)から終点(210, 50)まで2秒で移動する円のイージングを考えてみましょう。間隔サイズは、60 frame (= 30 frame/sec * 2 sec) の逆数になります。
import pytweening as tween
# (10, 50)から(210, 50)へ60ステップで easeInQuad の値を生成
startX, startY = 10, 50
endX, endY = 210, 50
intervalSize = 1 / 60
iter_point_progress = tween.iterEaseInQuad(startX, startY, endX, endY, intervalSize)
print(f"{len(list(iter_point_progress))=}")
len(list(iter_point_progress))=61
生成される座標の数(フレーム数)は61で、始点の座標に各移動後の座標60個を加えた計61個が出力されます。
あとは、Pyxelで各フレーム毎にPyTweeningで求めた座標を参照して描画するだけになります。
import pyxel
import pytweening as tween
class App:
def __init__(self):
pyxel.init(220, 100, title="EaseInQuad Bullet Demo", fps=30)
self.reset()
pyxel.run(self.update, self.draw)
def reset(self):
startX, startY = 10, 50
endX, endY = 210, 50
intervalSize = 1 / 60 # 1 / (30FPS * 2sec)
# iterEaseInQuad で座標列を生成
self.points = list(tween.iterEaseInQuad(startX, startY, endX, endY, intervalSize))
self.frame = 0
def update(self):
# スペースキーでリセット
if pyxel.btnp(pyxel.KEY_SPACE):
self.reset()
# フレーム進行
if self.frame < len(self.points) - 1:
self.frame += 1
def draw(self):
pyxel.cls(0)
x, y = self.points[self.frame]
pyxel.circ(int(x), int(y), 4, 11)
# UI表示
pyxel.text(5, 5, "SPACE: Reset", 7)
pyxel.text(5, 15, f"Frame: {self.frame}", 7)
App()
(2) 複数のオブジェクトにイージング
ゲームで使用するなら、複数のオブジェクトにイージング処理を行い、フレーム毎に表示できないと意味がありません。
例えば、スペースキーを押すと弾丸が発射されるコードを考えてみましょう。各弾丸にイージングを行って、複数の弾丸飛び交う場面が期待できます。実行結果がこちらです。
- Web版Pyxelデモ:https://malo21st.github.io/Pyxel_Pytweening/demo_shot/
(スペース長押しで、連射モード)
いい感じに弾幕ができたのではないでしょうか。因みに、画面右下の数字は弾丸の数で最大58個の弾丸を表示していました。
スペースキーを押すと飛行体の左右から弾丸を発射しますが、左側は等速で、右側は減速系(初速は等速の弾より早く、だんだん減速していく)のイージング処理を行っています。
import pytweening as tween
# 弾丸のイージング
EASE_FUNC_L = tween.iterLinear # 等速
EASE_FUNC_R = tween.iterEaseOutQuad # 減速系
class Bullet:
def __init__(self, ease_func, start_pos, end_pos, frames=FRAMES):
self.start_x, self.start_y = start_pos
self.end_x, self.end_y = end_pos
self.frames = frames
interval_size = 1.0 / max(1, frames)
self.iterator = ease_func(self.start_x, self.start_y, self.end_x, self.end_y, interval_size)
self.current = next(self.iterator, None)
class App:
def __init__(self):
self.objects = []
def update(self):
if pyxel.btnp(pyxel.KEY_SPACE, 30, 2): # 30フレーム押し続けたら連射モード
start_pos_L = (self.x - 7, self.y)
end_pos_L = (self.x - 7, -1)
self.objects.append(
Bullet(EASE_FUNC_L, start_pos_L, end_pos_L)
)
start_pos_R = (self.x + 7, self.y)
end_pos_R = (self.x + 7, -1)
self.objects.append(
Bullet(EASE_FUNC_R, start_pos_R, end_pos_R)
)
スペースキーが押される度に弾丸オブジェクト( Bullet )を生成し、生成のタイミングでイージング処理された座標群が弾丸オブジェクト( Bullet )のiteratorプロパティ( self.itetator )に格納されます。この生成した弾丸オブジェクト( Bullet )は、Appオブジェクトのリスト( self.objects )に追加されます。
次に、上記と同じ箇所で、弾丸オブジェクトのアップデートを行います。具体的には、弾丸オブジェクトの座標を提供するジェネレータを 組込み関数 next で至近の座標( self.current )を出力します。
class Bullet:
def update(self):
self.current = next(self.iterator, None)
def is_finished(self):
return self.current[1] < 0 # Y座標マイナスならTrue(表示終了)
class App:
def update(self):
## 弾丸オブジェクトのアップデートと枠外弾丸は削除(メモリ開放)
for obj in self.objects[:]:
obj.update()
if obj.is_finished():
self.objects.remove(obj)
また、アップデートの際、弾丸が画面より外なら、当該オブジェクトは弾丸のリスト( self.objects )から削除し、メモリから解放されます。
あとは、App と Bullet の drawメソッド で弾丸を描画するだけになります。
class Bullet:
def draw(self):
if self.current is None:
return
pyxel.pset(self.current[0], self.current[1], 10)
class App:
def draw(self):
pyxel.cls(0)
# 弾丸
for obj in self.objects:
obj.draw()
3.Epilogue
始点と終点と使用するフレーム数が決まれば、その間の座標の計算はイージング関数がやってくれます。
移動だけでなく、画像の大きさ(面積)や、Pyxelではできませんが画像の透過度などをスムーズに変化させたい時にもイージングは使えます。
ぜひ、イージングで動きを演出してみませんか。
Appendix
コードの公開
- 紹介したコードは、こちらで公開しています
https://github.com/malo21st/Pyxel_Pytweening
自作したイージング作品の紹介
- Pyxelではありませんが、今年の1月(2025年1月)に参加したgenuary( https://genuary.art/ )から私がイージングを使って作成した作品を紹介します。
イージングは自作ライブラリ、データ処理にPandas、画像処理にPILを使用しています。