Casio Python - グラフィックス出力関数の追加

Casioグラフ関数電卓の Python を使ってみる
- グラフィックス出力関数の追加:line() とユーザーモジュールの作成
<目次>
初版:2020/07/11
修正:2020/07/17
追記修正:2020/11/08
▲ 前の記事 - 5. 関数の作成と活用 | ▼ 次の記事 - 7. テキスト出力関数の追加修正:2020/07/17
追記修正:2020/11/08
<fx-CG50 OS3.40 以降、fx-9750GIII, fx-9860GIII OS3.40以降>
6. グラフィックス出力関数の追加:line() とユーザーモジュール の作成
前回は、グラフィックス描画関数で使うための 色指定関数 grp_color() 関数を作り、それを使って 円描画関数 circle() を拡張しました。その上で、モンテカルロ法シミュレーションをカラー化した monteca2.py を作りました。
Casio Python の現在の言語仕様では、Casio Basic で作った実用プログラムを完全に移植できないレベルの貧弱な仕様で、発展途上と言えます。Casio Basic で実用プログラムが作れる最大の要因は、望む位置に数値や文字列を出力できる Locate コマンドと電卓のほぼ全てのキー入力を取得可能な Getkey コマンドの存在です。一方 Casio Python には Locate と Getkey に相当するものがありません。
従って、プログラム=スクリプトを実行する際、シェル画面において 唯一の入力関数 input() を用いて必要な入力を済ませ、その後グラフィックス画面(描画画面)で出力を行うといった構造にするのが、現状では実用的なスクリプトを作成する最善の方法だと考えています。
Getkey 相当の関数については、カシオによる今後のOSアップデートに期待するしかありません。Locate に相当する locate() 関数は自作できるので、これについては次回紹介する予定です。
ところで、既に circle() 関数を作っていますので、今回は線分描画の line() 関数を作って、さらにユーザーモジュールに追加します。
6.1 line() 関数の作成 [2020/07/12 大幅修正]
[2020/07/12 ロジック見直し] 読者のK様からのご指摘により、垂直に近い傾きの大きな線分は 反復回数が少なくなるため飛び飛びの点描画になり、きちんと線分が描画されないことが判明しました。そこで、K様のご提案に従って、線分の傾きが常に45度を超えない処理2つを組み合わせることで、十分な反復回数を確保し、確実に線分を描画できるように修正しました。K様、ありがとうございます。

点描画を反復して線分を描く際、x 座標 を1だけ増減させた時 y 座標は 傾きの分を増減させます。
最初に与えられる (x1, y1) と (x2, y2) の2つの座標から、
dx = x2 - x1
dy = y2 - y1
とすると、傾き slope = dy/dx となります。
x 座標が x だけ変化したとき、y 座標は x*slope だけ変化します。(x1, y1) 座標を基準にして、x 座標 が x だけ変化したときの座標は、(x1+x, y1+x*slope) になります。この座標への点の描画を反復して線分を描くのですが、その反復回数を range(dx) で与えます。
線分の傾きが大きいと dx は小さな値になり、range(dx) で十分な反復回数が得られません。画面の左端から右端にわたる線分を描画する場合の反復回数は 191回未満になると、理屈の上では線分がまばらになります。傾きが垂直に近いと range(dx) は数回以下になってしまい、適切に線分を描けなくなります。
反復回数の一番厳しい条件として 最低でも 191 回確保するためには、傾きの絶対値が45度未満が条件で、別の表現をすれば、|dx| > |dy| が条件になります。| | は絶対値です。Python での表現では、適切な線分描画の条件は、abs(dx) > abs(dy) となります。
では、傾きの絶対値が 45 度以上になる場合は、x 座標と y 座標を入れ替えると、傾きの絶対値が 45 度未満になります。直交座標で2つの座標を入れ替えても数学的に等価であることは保証されているので、スクリプトも x 座標と y 座標を入れ替えて、数学的に等価になるようにすれば良いことになります。このときの傾きは、x と y を入れ替えて sllope = dx/dy となります。y 座標を y だけ変化させるとき、x 座標は y*slope だけ変化します。この変化は、(x1, y1) を基準にすると、座標 (x1+y*slope, y1+y) になります。
この問題は、以下の簡単なスクリプトで確認できます。
from u import *
for x in range(383):
line(0, 0, x, 191)
三角形の領域が塗りつぶされるべきところ、そうなっていなかったことから、K様のご指摘を検証できました。今回の修正の結果、三角形の領域が正しく塗りつぶされることが確認できました。
なお、show が1の場合にVRAMから画面にデータ転送を行う処理は、関数定義の一番最後に一括して記述します。
----------
(x1, y1) と (y1, y2) の間に線分を描きます。返値はなしです。すると関数定義の1行目は次のようになります。
def line(x1, y1, x2, y2, color=1, show=1):
ここで、第5引数の color はデフォルトで 1、前回作った grp_color() の引数仕様をそのまま引き継ぎます。
第6引数の show は、VRAMから画面への転送を行うかどうかを指定し、デフォルトで 1、つまり転送します。
但しここで注意しなければならないのは、dx = 0 の時、つまり垂直な線分を描画しようとするとき、0で除算できないのでエラーになります。これについては、とりあえずスクリプトを書いた後に検討します。
▍abs(dx) > abs(dy)、つまり線分の傾きが45度未満の場合
def line(x1, y1, x2, y2, color=1, show=1):
rgb = grp_color(color)
dx = x2 - x1
dy = y2 - y1
if abs(dx)>abs(dy):
if dx: #when dx is not 0
slope = dy/dx
for x in range(dx):
set_pixel(x1+x, y1+x*slope, rgb)
else:
(slope≧1 の時の処理)
if show:
show_screen()
最初は、line() の第5引数 color から、タプル型の rgb 値を得るために、grp_color() を使っています。というのも set_pixel() の第3引数にタプル型の rgb 値を使って色指定するのが仕様になっているからです。
さて、点の描画を反復させるために for 文を使い、range(dx) により、x を 0 から dx-1 まで1づつ増やしながら set_pixel() で点を描画させます。
この反復処理では、最初の x = 0 のときに (x1, y1) に点を描画します。次の描画、つまり隣の点がどうなるかと言えば、x が 1 増え、点描画の x 座標は x1 + x なので実際は x1 + 1 となります。x 座標の増分が x なので、そのときの y座標の増分は、x に傾き slope を掛けた値になります。 つまり y1 + x*slope です。
但し、ピクセル単位で点を描画するので、set_pixel() の引数は整数でなければなりません。 しかし slope は整数とは限りませんので、x*slope は小数点を切り捨てて整数にしてやる必要があります。
追加した部分は赤文字にしています。
def line(x1, y1, x2, y2, color=1, show=1):
rgb = grp_color()
dx = x2 - x1
dy = y2 - y1
if abs(dx)>abs(dy):
if dx: #when dx is not 0
slope = dy/dx
for x in range(dx):
set_pixel(x1+x, y1+int(x*slope))
else:
(slope≧1 の時の処理)
0 での除算エラーは dx=0 で発生します。このとき、abs(dx)=0 なので、if の条件 abs(dx)>abs(dy) は 0 > abs(dy) となります。絶対値は常に 0 以上なので、この条件は False (偽) となります。つまり除算エラーが 発生する dx=0 の時は、この if 文以下は実行されず、else 節にジャンプします。除算エラーについては、else 節を書く時に改めて検討します。
実は、range() の使い方で、考えなければならない問題があります。
for x in range(dx): の動作を細かくみてゆきます。
仮に dx = 10 だとすると、range(dx) はリスト [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] を作り、左から順に x に適用しながら反復処理を行います。実は、dx が正か負かで動作が変わってしまう問題があります。
dx が正の時は、問題ありません。 しかし dx = x2 - x1 なので、dx は常に正である保証はありません。
dx が負の場合、例えば -10 のときは、点描画の最初の座標 (x1, y1) は (x2, y2) よりも右にあり、点描画は右から左へ進んでゆきます。つまり、range(dx) は、リスト [0, -1, -2, -3, -4, -5, -6, -7, -8, -9] を作り、左から順に x に適用しながら反復処理を行う必要があります。
従って、range(dx) は、dx が正の場合は range(0, dx, 1) で、dx が負の場合は range(0, dx, -1) でないといけないことが分かります。
ちなみに、range(stop) は、range(start, stop, step) と書き換えることができ、start のデフォルトは 0 、step のデフォルトは 1 です。
⇒ range() のリファレンス参照
そこで、dx が正でも負での同じ動作にするためには、range(dx) と記述する代わりに range(0, dx, k) とし、dxが正の場合は k=1、dxが負の場合は k=-1 となるように k を決めれば、問題を解消できます。
k の決め方は色々な方法があると思いますが、ここでは dx を dxの絶対値で割り算すると 1 か -1 かになるので、この方法で k を決定します。但し range() の仕様上 k は整数でなければならないので、int() 関数で小数点以下を切り捨て整数にします。
k = int(dx/abs(dx))
abs() 関数は引数の絶対値を返す関数なので、これを使いました。
追加した部分は赤文字にしています。
def line(x1, y1, x2, y2, color=1, show=1):
rgb = grp_color()
dx = x2 - x1
dy = y2 - y1
if abs(dx)>abs(dy):
if dx: #when dx is not 0
k = int(dx/abs(dx))
slope = dy/dx
for x in range(0, dx, k):
set_pixel(x1+x, y1+int(x*slope))
else:
(slope≧1 の時の処理)
if show:
show_screen()
これで、abs(dx)>abs(dy) の時の処理は完成です。
from casioplot import*
def line(x1, y1, x2, y2, color=1, show=1):
rgb = grp_color()
dx = x2 - x1
dy = y2 - y1
if abs(x)>abs(y):
if dx: #when dx is not 0
k = int(dx/abs(dx))
slope = dy/dx
for x in range(0, dx, k):
set_pixel(x1+x, y1+int(x*slope))
else:
#abs(dx)>abs(dy) でない時の処理
if show: #data transfer to screen
show_screen()
▍abs(dx)>abs(dy) でない時、つまり線分の傾きが 45度以上の場合
[2020/07/12 追記]
直交座標系では、2つの座標を入れ替えても数学的に等価であることは保証されています。そして、x 座標と y 座標を入れ替えると、傾きは 45度未満になって、適切に線分を描けます。
そこで、x と y を入れ替え、dx と dy を入れ替えて作ったスクリプトを書きます。但し、set_pixel() の引数については、上で検討したように、単なる文字の入れ替えではなく、数学的な意味で等価になるように変更します。
from casioplot import*
def line(x1, y1, x2, y2, color=1, show=1):
rgb = grp_color()
dx = x2 - x1
dy = y2 - y1
if abs(dx)>abs(dy):
if dx: #when dx is not 0
k = int(dx/abs(dx))
slope = dy/dx
for x in range(0, dx, k):
set_pixel(x1+x, y1+int(x*slope))
else:
if dy: #when dy is not 0
k = int(dy/abs(dy))
slope = dx/dy
for y in range(0, dy, k):
set_pixel(x1+int(y*slope), y1+y)
if show: #data transfer to screen
show_screen()
今追加した else 節について、0 除算のエラーについて検討します。上で検討したように、dx=0 の場合は else 節の処理にジャンプします。dx=0 の時は slope=0 となるので、問題ないことが分かります。
では、dy=0 になる場合を検討します。最初の if の条件 abs(dx)>abs(dy) は、abs(dx)>0 となるので、これは常に True (真) です。つまり dy=0 の場合は、最初の if 文で処理され、slope=0 となり問題ありません。
除算エラーになる最後のケースは、dx=0 かつ dy=0 の場合です。この条件が成り立つ時は、x1 = x2 かつ y1 = y2 です。このとき描画するのは線分でなくて、点になることが分かります。この条件が成り立つ時は、if 文の条件には合わないので else 節へジャンプし、そこで slope の計算で除算エラーが発生します。
そこで、if 文の上に以下の処理を追加して、dx と dy がともに 0 の時、座標 (x1, y1) に点を描画した後、return で終了し、それ以下を実行しないようにします。
from casioplot import*
def line(x1, y1, x2, y2, color=1, show=1):
rgb = grp_color()
dx = x2 - x1
dy = y2 - y1
if dx==0 and dy==0: # avoid division by 0 error
set_pixel(x1, y1, rgb)
return
if abs(dx)>abs(dy):
if dx: #when dx is not 0
k = int(dx/abs(dx))
slope = dy/dx
for x in range(0, dx, k):
set_pixel(x1+x, y1+int(x*slope))
else:
if dy: #when dy is not 0
k = int(dy/abs(dy))
slope = dx/dy
for y in range(0, dy, k):
set_pixel(x1+int(y*slope), y1+y)
if show: #data transfer to screen
show_screen()
6.2 グラフィックスユーザーモジュールの作成
前回作成した grp_color()、circle() そして今回作った line() は、グラフィックス画面に出力する汎用関数なので、いちいち関数定義をコピーして使うのは煩わしいです。そこで、calioplot や math, random モジュールのように呼び出して簡単に使うようにします。
そのためには、自分で作ったユーザー関数の定義を1つのスクリプトファイルに収め、そのスクリプトをモジュールとして呼びだして使います。今回は、このスクリプトファイル名を u.py とします。このモジュール(ユーザーモジュール)を使うには、スクリプトの冒頭に
from u import *
と書くだけです。
⇒ u.py (ユーザーモジュール ver 1.5) のダウンロード [2020/11/08 修正]
このモジュールには、grp_color()、circle()、line() が含まれます。これらは、モノクロ液晶モデル (FXモデル) にも対応しています。但し、line() 関数は FXモデル対応のために変更する必要がなく、そのままです。
これまで作った関数のスクリプトは、電卓内に保存されている筈です。電卓をPCとリンクし、エディタを使ってPC上でカット&ペーストするのが、間違いなく、簡単に u.py を作れます。
エディタについては、Casio Python - はじめに:電卓で作る初めてのスクリプト の 1.3 スクリプと作成と編集の2つの方法 を参照してください。
6.3 line() 関数の動作確認 - ckLine.py [fx-CG50 OS3.4以降専用]
line() 関数を実際に使ってみます。
⇒ ckLine.py のダウンロード - このスクリプトはCGモデル専用です。
from u import *
line(0,0,383,0,'black')
line(383,0,383,191,'blue')
line(383,191,0,191,'red')
line(0,191,0,0,'magenta')
line(10,10,373,10,'green')
line(373,10,373,181,'cyan')
line(373,181,10,181,'yellow')
line(10,181,10,10,1)
line(0,0,383,191,2)
line(0,191,383,0,3)
実行結果は以下のようになります;

※ FXモデル (fx-9750GIII, fx-9860GIII OS3.40以降) では、各座標値を3で割った整数部に置き換える必要があります。
例えば、383 を 383//3 に変更します。[2020/11/08 追記]
6.4 line() 関数の動作確認 - ckLine2.py [fx-CG50 OS3.40 以降専用]
line() 関数を使った別のスクリプトです。
左上の座標 (0, 0) を基点にして、扇状に線分を -90度から0度まで反復描画してみます。反復する際にカラーコードを 1 ~ 7 まで繰り返し変更してみると、なんとも不思議な模様が得られました。
⇒ ckLine2.py のダウンロード - このスクリプトはCGモデル専用です
from u import *
for x in range(383):
line(0, 0, x, 191, x%7)
for y in range(191, 0, -1):
line(0, 0, 383, y, y%7)
Note: 除算の余りを求める - %
整数 x を 7 で除算した時の余りは x%7 で得られます。この計算により 0 ~ 7 の整数を算出できます。



※ FXモデル (fx-9750GIII, fx-9860GIII OS3.40以降) では、各座標値を3で割った整数部に置き換える必要があります。
例えば 383 を 383//3 に変更します。[2020/11/08 追記]
▶ 目 次
▲ 前の記事 - 5. 関数の作成と活用
▼ 次の記事 - 7. テキスト出力関数の追加
応援クリックをお願いします。励みになるので...