LEDマトリクス用csvをドット絵を描いて作れるプログラム by Processing

背景

  Twitter(現X 以下Twitter)で見かけたLEDマトリクスを光らせるためにExcelで値を計算しているツイート。これを見て、Processingでドット絵を描いてそれを自動でLEDマトリクス用のcsvの形式に変換して保存できるプログラムを書きたくなった。

概要

 マウス操作でドット絵を描くことが出来る。描いたドット絵を、LEDマトリクスで表示するための形式のcsvファイルとして保存できる。

実際のプログラムの様子

 下の動画は第一弾で、操作ボタンは未設置。

 内容:描画→csvで保存→Excelで整える


ドット絵描画機能

 描画エリアをマウスでなぞって描画できる。ペンの色を白(点灯)と黒(消灯)で切り替える事ができる。リセットボタンで画面をリセット(全消灯)できる。

リアルタイム数値化

 見かけたTwitterで使っていたLEDマトリクスのライブラリでは、8ピクセル分の消灯/点灯データを1つの値として扱うようだ。そこで、描画中にリアルタイムでその数値変換の結果を表示すし、マトリクスのライブラリについての理解を深めてもらうことも狙った。(そんな真面目な話をしなくともリアルタイムで変化する数字を見ていて楽しいから良いのだ)


csvで保存

 点灯ピクセルを1、消灯ピクセルを0とした表にするだけでなく、前述の8ピクセル数値化も載っているcsvフィアルに自動で変換される。また、この8ピクセル数値化のセルは、数値化した値を載せるのではなく計算式を載せることで、数値のブラックボックス化を防ぎ理解に繋げる狙いがある。

 csvファイルはProcessingにて自動生成されるコードが入っているフォルダに保存されるが、ソースコードのファイル名にパスを追加して記述すれば任意のフォルダに保存できるはず。


操作説明

操作方法はマウス操作とキー操作。

動画は画面のボタンで操作する様子。

描画関連操作

・描画
描画エリア内のピクセルにマウスポインタを当ててクリックすると描画できる。マウスのボタンを押したまま動かせば線を描ける。
・ペンの色の変更
白(点灯)と黒(消灯)を選べる。
白:画面のペンアイコンの下の白ボタンをマウスで押すか、Wキーを押す。
黒:画面のペンアイコンの下の黒ボタンをマウスで押すか、Bキーを押す。
・リセット
描画エリアをリセット(全て消灯に)できる。
画面のリセットボタンをマウスで押すか、Rキーを押す。
・アンドゥ(取り消し)、リドゥ(やり直し)
アンドゥは操作を取り消して1つ前の状態に戻る、リドゥはアンドゥをキャンセルして1つ次の状態に進む。
アンドゥ:Zキーを押す。
リドゥ:Yキーを押す。

csvで保存

・csvで保存
画面上のCSVで保存ボタンをマウスで押すか、Shift+Sキーを押す。
コンソールに保存日時が表示される。

おまけ:Excel操作

保存したcsvをExcelで開き、いくつか操作をすると見やすくなる。

・セルの幅を整える
セルの幅を整えるとセルが正方形に近づき、実際のLEDマトリクスの様子に近い状態で見ることができる。
⌘+A(contlor+A)で全選択→列名と列名の間をダブルクリック
・LEDマトリクスエリアの色を変える
0や1といった数字が入ったセルを見ても分かりにくいので、0と1で色分けをして見やすくする。
LEDマトリクスの範囲を選択→上部ホームタブ>「条件付き書式」>カラースケール>適当なやつを選ぶ


ソースコード

 これをProcessingにコピペすればよい。

 ちなみに、このシンタックスハイライトはVScodeによるもので、Processingのエディタではこのようなハイライトが当たるとは限らない。(VScodeからコピペするとハイライトがコピペ先に反映されるのでVScodeから持ってきた)

/*
ピクセルアートからLEDマトリクスに
作成:2024/08/25
更新:2024/09/05
*/
PrintWriter file;
float x, y;
String csvFile;

int MWN=32;//マトリクスのピクセル数 幅
int MHN=16;//マトリクスのピクセル数 高さ
int[][] MS=new int[MHN][MWN];//ピクセルs

int rireki=10;//履歴件数
int now=0;//現在の履歴ナンバー
int[][][] MSP=new int[rireki][MHN][MWN];//過去ピクセルs

int PW=20;// 1ピクセルの幅
int PH=20;// 1ピクセルの高さ
int MPAX, MPAY, MPAW, MPAH;//マトリクスプリントエリア座標とサイズ

int pen=1;//ペンの状態

int saveStatus=0; // 保存状態 0:Non, 1:Saving, 2:Saved

//初期化
void setup() {
csvFile="Book2.csv";//保存ファイルパス
file = createWriter(csvFile);
x = 0;
y = 0;

MPAW=PW*MWN;
MPAH=PH*MHN;
MPAX=110;
MPAY=10;

surface.setSize(MPAW+360, MPAH+20);//画面サイズ

matrixReset();
matrixPrint2();
botton();
}

//フレームごとの処理
void draw() {
if (mousePressed)
matrixDrow();

matrixPrint2();
pixelToIntegers();
botton();
}

//キー押下時
void keyPressed() {
botton();
switch(key) {
case 'S':
//ピクセル画をCSVに保存
saveStatus=1;
//csvEx3();
break;
case 'r':
//画面リセット
matrixReset();
break;
case 'z':
//りどうあんどう
matrixHistoryBack(1);
break;
case 'y':
//りどうあんどう
matrixHistoryBack(-1);
break;
case 'w':
//白ペン
pen=1;
break;
case 'b':
//黒ペン
pen=0;
break;
}
botton();
}

//マウス押下時
void mousePressed() {
botton(true);
}

//マウスボタンを離した時
void mouseReleased() {
matrixHistoryWrite();//履歴を更新
botton();
}

//ピクセル画をCSVに保存
//8ピクセル数値化計算付き
void csvEx() {
//8ピクセル数値化のための値
int[] eightPixelsElm={1, 2, 4, 8, 16, 32, 64, 128};
//8ピクセル数値化した値 ceil(MWN/8)はピクセル数を8で割った数の小数点以下切り上げ
int[] eightPixelsSum=new int[ceil(MWN/8)];

for (int y=0; y<MHN; y++) {
for (int x=0; x<MWN; x++) {
if (x%8==0) eightPixelsSum[x/8]=0;
file.print(MS[y][x]);
file.print(",");
eightPixelsSum[x/8]+=eightPixelsElm[x%8]*MS[y][x];
}
//8ピクセル数値化を表示して改行
for (int i=0; i<eightPixelsSum.length; i++) {
file.print(","+eightPixelsSum[i]);
}
file.println();
}
file.flush();
file.close();

String [] ymdhms={str(year()), str(month()), str(day()), str(hour()), nf(minute(), 2), nf(second(), 2)};
String ym_ = join(ymdhms, "_");

println("save "+ym_);
saveStatus=2;
//exit();
}

//ピクセル画をCSVに保存
//8ピクセル数値化計算式付き
void csvEx3() {
//8ピクセル数値化のための値
int[] eightPixelsElm={1, 2, 4, 8, 16, 32, 64, 128};
//8ピクセル数値化した値を求める計算式の準備
String[] eightPixelsSum=new String[ceil(MWN/8)];

for (int y=0; y<MHN; y++) {
for (int x=0; x<MWN; x++) {
if (x%8==0) eightPixelsSum[x/8]="=";

file.print(MS[y][x]);
file.print(",");

eightPixelsSum[x/8]+=numToCol(x)+str(y+1)+"*"+str(eightPixelsElm[x%8]);
if (x<MWN-1 && x%8<7) eightPixelsSum[x/8]+="+";
}
//8ピクセル数値化を表示して改行
for (int i=0; i<eightPixelsSum.length; i++) {
file.print(","+eightPixelsSum[i]);
}
file.println();
}
file.flush();
file.close();

String [] ymdhms={str(year()), str(month()), str(day()), str(hour()), nf(minute(), 2), nf(second(), 2)};
String ym_ = join(ymdhms, "_");

println("save "+ym_);
saveStatus=2;
//exit();
}


//ピクセル画をCSVに保存
void csvEx1() {
for (int y=0; y<MHN; y++) {
for (int x=0; x<MWN; x++) {
//n=y*10+x;
file.print(MS[y][x]);
if (x<MWN-1) file.print(",");
else file.println();
}
}
file.flush();
file.close();

String [] ymdhms={str(year()), str(month()), str(day()), str(hour()), nf(minute(), 2), nf(second(), 2)};
String ym_ = join(ymdhms, "_");

println("save "+ym_);
saveStatus=2;
//exit();
}

//値をカラムに変換
String numToCol(int num) {
String col="";

//col+=char('A'+25)+"_";

//num;

while (true) {
col=char('A'+num%26)+col;
if (num<26)
break;
else
num=floor(num/26)-1;
}

return col;
}

void matrixReset() {
saveStatus=0;
for (int y=0; y<MHN; y++) {
for (int x=0; x<MWN; x++) {
MS[y][x]=0;
}
}
}

void matrixPrint2() {

//各ピクセルの色を表示
strokeWeight(1);
stroke(128, 128);
for (int y=0; y<MHN; y++) {
for (int x=0; x<MWN; x++) {
if (MS[y][x]==0)
fill(0);
else
fill(255);
rect(MPAX+PW*x, MPAY+PH*y, PW, PH);
}
}
//8ピクセルごとの太枠を表示
strokeWeight(3);
fill(0, 0);
for (int y=0; y<MHN; y+=8) {
for (int x=0; x<MWN; x+=8) {
int w, h;
w=h=8;
rect(MPAX+PW*x, MPAY+PH*y, PW*w, PH*h);
}
}
strokeWeight(1);
noStroke();
}

////8ピクセル数値化計算
void pixelToIntegers() {
//8ピクセル数値化のための値
int[] eightPixelsElm={1, 2, 4, 8, 16, 32, 64, 128};
//8ピクセル数値化した値 ceil(MWN/8)はピクセル数を8で割った数の小数点以下切り上げ
int[] eightPixelsSum=new int[ceil(MWN/8)];

int tx, ty, tw, th;//テキスト表示座標とサイズ

textSize(PH*0.8);
textAlign(RIGHT);
for (int y=0; y<MHN; y++) {
for (int x=0; x<MWN; x++) {
if (x%8==0) eightPixelsSum[x/8]=0;
eightPixelsSum[x/8]+=eightPixelsElm[x%8]*MS[y][x];
}

stroke(128);
for (int i=0; i<eightPixelsSum.length; i++) {
fill(0);
tw=PW*2;
th=PH;
tx=MPAX+MPAW+PW+tw*i;
ty=MPAY+y*PH;
rect(tx, ty, tw, th);
fill(255);
text(eightPixelsSum[i]+" ", tx, ty, tw, th);
}
}
}

//履歴を記録する
void matrixHistoryWrite() {
for (int r=rireki-1; r>=0; r--) {
for (int y=0; y<MHN; y++) {
for (int x=0; x<MWN; x++) {
if (r>0)
MSP[r][y][x]=MSP[r-1][y][x];
else
MSP[r][y][x]=MS[y][x];
}
}
}
}

//戻る zキー押下時
void matrixHistoryBack(int pm) {
now=constrain(now+pm, 0, rireki-1);

if (now<rireki) {
for (int y=0; y<MHN; y++) {
for (int x=0; x<MWN; x++) {
MS[y][x]=MSP[now][y][x];
}
}
}
}

void matrixDrow() {
int x=constrain((mouseX-MPAX)/PW, 0, MWN-1);
int y=constrain((mouseY-MPAY)/PH, 0, MHN-1);

//マウス座標が範囲外なら処理せず終了
if ((mouseX-MPAX)/PW-x!=0) return;
if ((mouseY-MPAY)/PH-y!=0) return;

MS[y][x]=pen;
saveStatus=0;
}

//ボタン関連関数
//渡す値がない
void botton() {
botton(false);//オーバーロード
}

//ボタン関連関数
//マウスを押し始めかどうかを渡す 押したままマウスがボタンに触れても押した判定はしない
void botton(boolean pressBottonErera) {
//ボタンの座標、幅
float bx, by;
float bw;
float byt;//各ボタン暫定座標
bx=10;
bw=90;

//ペンのアイコンと色選択
//ボタンの座標とサイズ botton pen
float bpx, bpy;
float bpw, bph;
bpx=bpy=bx;
bpw=bph=bw;
byt=bpy;
//ペンの背景
fill(128);
stroke(0);
rect(bpx, bpy, bpw, bph);
//ペン(色付き部分)
if (pen==1) fill(255);
else fill(0);
beginShape();
vertex(bpx+bpw*1/8, bpy+bph*7/8);
vertex(bpx+bpw*4/8, bpy+bph*6/8);
vertex(bpx+bpw*7/8, bpy+bph*3/8);
vertex(bpx+bpw*5/8, bpy+bph*1/8);
vertex(bpx+bpw*2/8, bpy+bph*4/8);
vertex(bpx+bpw*1/8, bpy+bph*7/8);
endShape(CLOSE);
//ペン(木の部分)
fill(255, 255, 128);
beginShape();
vertex(bpx+bpw*4/8, bpy+bph*6/8);
vertex(bpx+bpw*2.5/8, bpy+bph*6.5/8);
vertex(bpx+bpw*1.5/8, bpy+bph*5.5/8);
vertex(bpx+bpw*2/8, bpy+bph*4/8);
vertex(bpx+bpw*4/8, bpy+bph*6/8);
endShape(CLOSE);
//色選択
fill(255);
rect(bpx, bpy+bph, bpw/2, bph/2);
fill(0);
rect(bpx+bpw/2, bpy+bph, bpw/2, bph/2);
stroke(0, 255, 0);
strokeWeight(3);
noFill();
//選択色
if (pen==1) rect(bpx+bpw/20, bpy+bph*1.05, bpw*0.8/2, bph*0.8/2);
else rect(bpx+bpw/2+bpw/20, bpy+bph*1.05, bpw*0.8/2, bph*0.8/2);
strokeWeight(1);
textSize(bph/5);
textAlign(CENTER, CENTER);
fill(0);
text("W", bpx, bpy+bph, bpw/2, bph/2);
fill(255);
text("B", bpx+bpw/2, bpy+bph, bpw/2, bph/2);
//当たり判定
noFill();
stroke(128, 192);
strokeWeight(3);
if (bpy+bph<=mouseY && mouseY<bpy+bph*1.5) {
if (bpx<=mouseX && mouseX<bpx+bpw/2) {
if (pen!=1) rect(bpx+bpw/20, bpy+bph*1.05, bpw*0.8/2, bph*0.8/2);
if (mousePressed && pressBottonErera) pen=1;
}
if (bpx+bpw/2<=mouseX && mouseX<bpx+bpw) {
if (pen!=0) rect(bpx+bpw/2+bpw/20, bpy+bph*1.05, bpw*0.8/2, bph*0.8/2);
if (mousePressed && pressBottonErera) pen=0;
}
}
strokeWeight(1);

byt+=bph*1.5;

//リセット
float brx, bry;
float brw, brh;
byt+=10;
brx=bpx;
bry=byt;
brw=bw;
brh=brw/2;
fill(64);
stroke(0);
rect(brx, bry, brw, brh);
fill(255);
textSize(brh*0.4);
text("RESET:R", brx, bry, brw, brh);
//当たり判定
boolean dr=false;
if (brx<=mouseX && mouseX<brx+brw && bry<=mouseY && mouseY<bry+brh) {
noFill();
stroke(192, 192);
strokeWeight(3);
rect(brx+brh/10, bry+brh/10, brw-brh/5, brh-brh/5);
strokeWeight(1);
if (mousePressed && pressBottonErera) {
dr=true;
//画面リセット
matrixReset();
}
}
if (keyPressed && key=='r') dr=true;
if (dr) {
noFill();
stroke(0, 255, 0);
strokeWeight(3);
rect(brx+brh/10, bry+brh/10, brw-brh/5, brh-brh/5);
strokeWeight(1);
}


byt+=brh;

//Save to CSV
float bsx, bsy;
float bsw, bsh;
bsx=bx;
bsy=byt+10;
bsw=bw;
bsh=bsw;
fill(64);
stroke(0);
rect(bsx, bsy, bsw, bsh);
fill(255);
text("Save to CSV:Shift+S", bsx, bsy, bsw, bsh);
//当たり判定
//boolean dr=false;
if (bsx<=mouseX && mouseX<bsx+bsw && bsy<=mouseY && mouseY<bsy+bsh) {
noFill();
stroke(192, 192);
strokeWeight(3);
rect(bsx+bsh/20, bsy+bsh/20, bsw-bsh/10, bsh-bsh/10);
strokeWeight(1);
if (mousePressed && pressBottonErera) {
if (saveStatus==0) saveStatus=1;
}
}
if (keyPressed && key=='S' && saveStatus==0) saveStatus=1;
if (saveStatus==1) {
noFill();
stroke(0, 255, 0);
strokeWeight(3);
rect(bsx+bsh/20, bsy+bsh/20, bsw-bsh/10, bsh-bsh/10);
strokeWeight(1);
csvEx3();
}
if (saveStatus==2) {
noFill();
stroke(0, 255, 0);
strokeWeight(3);
rect(bsx+bsh/20, bsy+bsh/20, bsw-bsh/10, bsh-bsh/10);
strokeWeight(1);
}
}


コメント