processingにおけるペイントソフトのスケルトン

ある肌寒き日にここに記す。

int window_width = 500;
int window_height = 500;
void settings()
{
 size(window_width, window_height);
}

PVector pprev;
PVector prev;
PVector current;
PVector dv = new PVector(0,0);

ArrayList<ITool> Tools = new ArrayList<ITool>();
ITool CurrentTool;

ArrayList<IAObject> GUIs = new ArrayList<IAObject>();
IAObject CurrentGUI;

ArrayList<ILayer> Layers = new ArrayList<ILayer>();
ILayer CurrentLayer;
PGraphics BaseLayer;

void setup()
{
 BaseLayer = createGraphics(width,height);
 
 Layer layer = new Layer(width,height);  
 Layers.add(layer);
 CurrentLayer = layer;
 
 ITool tool = new StrokePen();
 //ITool tool = new LinerPen();
 //ITool tool = new PixelPen();
 Tools.add(tool);
 CurrentTool = tool;
 
}

void draw()
{
 //メインのバッファは常に白でクリア
 //GUIを動かした時など、これをせねば軌跡が全部残ってしまう
 background(255);

 //全てのレイヤーから一度BaseLayerに描き込む  
 //メインのバッファの系統、というかGUIの系統と分離することでクリアに巻き込まれないようにする
 for(ILayer layer : this.Layers)
 {
   //おそらく最終的に、レイヤーの非表示やレイヤーの移動機能を付けるなら、
   //BaseLayerも白透明(255,255,255,0)でクリアした方が良いと思われる。    
   //黒透明(0,0,0,0)のクリアは乗算ブレンドで常に0を返すのでやりにくい。
   if(layer instanceof IDrawablePG)
   {
     ((IDrawablePG)layer).draw(this.BaseLayer);
   }
 }
 //BaseLayerをprocessingのメインバッファに転写
 image(this.BaseLayer,0,0);

 //GUI
 //なんならGUI用にレイヤーを作っても良いと思われる
 for(IAObject gui : this.GUIs)
 {
   if(gui instanceof IUpdate)
   {
     ((IUpdate)gui).Update();
   }
   if(gui instanceof IDrawable)
   {
     ((IDrawable)gui).draw();
   }
 }
 
}

イベント



void mousePressed()
{
 pprev = new PVector(mouseX,mouseY);
 prev = new PVector(mouseX,mouseY);
 current = new PVector(mouseX,mouseY);

 for(int i = 0; i<this.Tools.size(); i++)
 {
   ITool tool = this.Tools.get(i);
   if(tool instanceof IClickable)
   {
     ((IClickable)tool).mousePressed();
   }
 }
 
 for(int i = 0; i<this.GUIs.size(); i++)
 {
   IAObject gui = this.GUIs.get(i);
   if(gui instanceof IHit)
   {
     if(((IHit)gui).Hit(mouseX,mouseY))
     {
         CurrentGUI = gui;      
     }
   }
   if(gui instanceof IClickable)
   {
     ((IClickable)gui).mousePressed();
   }
 }  
}

void mouseDragged()
{
 pprev = prev.copy();
 prev = current.copy();
 current.x = mouseX;
 current.y = mouseY;  
 dv = PVector.sub(current,prev);
   
 for(int i = 0; i<this.Tools.size(); i++)
 {
   ITool tool = this.Tools.get(i);
   if(tool instanceof IDraggable)
   {
     ((IDraggable)tool).mouseDragged();
   }
 }    
 
 if(CurrentGUI!=null)
 {
   if(CurrentGUI instanceof IDraggable)
   {
     ((IDraggable)CurrentGUI).mouseDragged();
   }
 }
}

void mouseReleased()
{
 pprev = null;
 prev = null;
 current = null;  
 
 for(int i = 0; i<this.Tools.size(); i++)
 {
   ITool tool = this.Tools.get(i);
   if(tool instanceof IClickable)
   {
     ((IClickable)tool).mouseReleased();
   }
 }
 
 for(int i = 0; i<this.GUIs.size(); i++)
 {
   IAObject gui = this.GUIs.get(i);
   if(gui instanceof IClickable)
   {
     ((IClickable)gui).mouseReleased();
   }
 }    
}

void keyPressed()
{
 for(int i = 0; i<this.GUIs.size(); i++)
 {
   IAObject gui = this.GUIs.get(i);
   if(gui instanceof IKeyInputtable)
   {
     ((IKeyInputtable)gui).keyPressed();
   }
 }   
}

void keyReleased()
{
 for(int i = 0; i<this.GUIs.size(); i++)
 {
   IAObject gui = this.GUIs.get(i);
   if(gui instanceof IKeyInputtable)
   {
     ((IKeyInputtable)gui).keyReleased();
   }
 }     
}

GUI

ここでは割愛
詳しくは以下

https://note.com/alchan/n/nf1096417fdfd




//GUIと描画用ベクトル群を抽象化する。
//すなわちGUIを特別視しない。ただの入力に反応する絵とみなす。
interface IAObject 
{  

}

//主にGUI用途
interface IGObject extends IAObject, IDrawable, IHit, ITranslate, IClickable, IDraggable
{
 
}

interface IDrawable
{
 void draw();
}

interface IDrawableXY
{
 void draw(float x, float y);
}

interface IDrawablePG
{
 void draw(PGraphics pg);
}

interface IHit
{
 boolean Hit(float x, float y);
}

interface IMove
{
 void Move(float x, float  y);
}

interface ITranslate
{
 void Translate(PVector dv);
}

interface HasOrigin
{
 PVector GetOrigin();
}

interface HasCoordinates
{
 ArrayList<PVector> GetCoordinates();
}

interface HasParent
{
 IGObject GetParent();
}

interface IUpdate
{
 void Update();
}

interface IClickable
{
 void mousePressed();
 void mouseReleased(); 
}

interface IDraggable
{  
 void mouseDragged();  
}

interface IKeyInputtable
{
 void keyPressed();
 void keyReleased();
}

レイヤー



interface ILayer
{
 PGraphics GetGraphics();
}

class Layer implements ILayer, IDrawablePG
{
 PVector pos = new PVector(0,0);
 PGraphics Graphics;
 
 PGraphics GetGraphics()
 {
   return this.Graphics;
 }
 
 void draw(PGraphics base)
 {
   base.beginDraw();
   base.image(this.Graphics,pos.x,pos.y);
   base.endDraw();
 }
 
 //コンストラクタ
 Layer(int x, int y, int w, int h)
 {
   pos.x=x;
   pos.y=y;
   Graphics = createGraphics(w,h);
   
   //初期値(0,0,0,0:黒の透明)だと乗算ペンや乗算レイヤー(blendMode(MULTIPLY)が機能しないので
   //白の透明にしておく
   Graphics.beginDraw();
   Graphics.background(255);
   Graphics.alpha(0);
   Graphics.endDraw();        
 }  
 Layer(int w, int h)
 {
   Graphics = createGraphics(w,h);
   
   //初期値(0,0,0,0:黒の透明)だと乗算ペンや乗算レイヤ(blendMode(MULTIPLY)が機能しないので
   //白の透明にしておく
   Graphics.beginDraw();
   Graphics.background(255);
   Graphics.alpha(0);
   Graphics.endDraw();    
 }  
}

ツール

今回のメイン。

interface ITool
{
 
}

class ToolBase implements ITool, IUpdate, IClickable, IDraggable, IKeyInputtable
{
 void Update(){}
 void mousePressed(){}
 void mouseReleased(){}   
 void mouseDragged(){}  
 void keyPressed(){}
 void keyReleased(){}  
}

PixelPen

画像1

おそらく最も単純かつ原始的なアイディアによるペン。
マウスが今ある座標のピクセルを塗る。というだけのペン。
しかしやってみれば分かるが、座標は飛び飛びとなる。ゆえにピクセルは飛び飛びにしかプロットすることができない。
これを原理的に修正しようとすると、我々はOSにそのAPI、CPUにビデオカード、ディスプレイからその他の周辺機器に至るまで、あらゆるものに変更を加えなければならない上に、得られる利益はほとんどない。
ゆえに我々はまず、飛び飛びの値をしゃーなしと受け入れて、それらを補間することを考えるのである。最下部に参考となるものをいくらか列挙。

class PixelPen extends ToolBase
{  
 boolean pressed = false;
 @Override
 void mousePressed()
 {
   this.pressed = true;
 }
 
 @Override
 void mouseDragged()
 {
   PGraphics pg = CurrentLayer.GetGraphics();  
   pg.beginDraw();
   pg.loadPixels();
   
   int x = mouseX;
   int y = mouseY;
   
   pg.pixels[y*width+x]=color(0,0,0); 
   
   pg.updatePixels();
   pg.endDraw();
   
 }
 @Override
 void mouseReleased()
 {
   this.pressed = false;
 }  
}

LinerPen

画像2

離散点を線で繋ぐもの。これはおそらく世に出回っているほとんどすべてのビデオカード類がハード的に実装しているであろうブレセンハムのアルゴリズムでもって実現されるであろう。
processingのline関数はおそらくOSに『おたくのAPI、線引ける関数ある?』と問いただしているだけであり、OSは『バカにすなよぉ! あるわそんなもん!』とポッケに入れているビデオカードを渡すのみである。最終的な実装を知っているのはビデオカードの設計者のみであり、とりわけ末端のプログラマなどはprocessing商店で売ってる安価な定規を買うだけである。


class LinerPen extends ToolBase
{  
 boolean pressed = false;
 @Override
 void mousePressed()
 {
   this.pressed = true;
 }
 
 @Override
 void mouseDragged()
 {
   PGraphics pg = CurrentLayer.GetGraphics();   
   pg.beginDraw();    
   pg.line(prev.x,prev.y,current.x,current.y);
   pg.endDraw();
 }
 
 @Override
 void mouseReleased()
 {
   this.pressed = false;
 }  
}

StrokePen

画像3

ドットを打ったり関数を発動するでなく、連続的に画像を張り付けることでブラシ的なものの表現を試みるペン。はりつけの際に、離散点の間を埋めるような、わずかばかりの補間を試みている。

class StrokePen extends ToolBase
{  
 PGraphics PGBrush;
 
 void MakeStandardGradationBrush()
 {
   //ブラシとなる画像を生成する
   PGBrush = createGraphics(Radius*2, Radius*2);
   PGBrush.beginDraw();
   for(int r = Radius; 0<r; r--)
   {
     float ratio = (float)r/(float)Radius;
     float val = 255*ratio;
     color c = color(val);
     PGBrush.stroke(c);
     PGBrush.fill(c);
     PGBrush.circle(Radius,Radius,r*2);
   }     
   PGBrush.endDraw();    
 }
 
 StrokePen()
 {
   MakeStandardGradationBrush();
 }
 
 int Radius = 5;
 
 boolean pressed = false;
 @Override
 void mousePressed()
 {
   this.pressed = true;   
 }
 
 //prev-currentの長さに応じて補間する
 void RelationalStrokeInterpolation(PGraphics pg)
 {
   PVector lv = PVector.sub(current,prev);
   float accuracy = 1/(float)lv.mag();
   for(float i = 0; i<1; i+= accuracy)
   {
     PVector p = PVector.add(prev, lv.copy().mult(i));
     pg.image(PGBrush,p.x-Radius,p.y-Radius);      
   }    
 }
 
 //入力パラメータに応じて補間する
 void AbsStrokeIntrerpolation(PGraphics pg, int interpolate_count)
 {
   PVector lv = PVector.sub(current,prev);
   float accuracy = 1/(float)interpolate_count;
   for(float i = 0; i<1; i+= accuracy)
   {
     PVector p = PVector.add(prev, lv.mult(i));
     pg.image(PGBrush,p.x-Radius,p.y-Radius);
   }       
 }
 
 @Override
 void mouseDragged()
 {    
   PGraphics pg = CurrentLayer.GetGraphics();
   pg.beginDraw();
   pg.blendMode(MULTIPLY);  
   pg.tint(125,20);    
   
   //補間無しのプロットの場合
   //pg.image(brush,x-radius,y-radius);
      
   //補間あり
   RelationalStrokeInterpolation(pg); 
   
   pg.endDraw();
 }//mouseDragged()
 
 @Override
 void mouseReleased()
 {
   this.pressed = false;
 }  
}


参考。

ブレンドモードやフィルター処理
https://note.com/alchan/n/n63464a6f1d3c

補間
https://note.com/alchan/n/n2b42708ca6d1

ブレセンハム

ベジェ

スプライン
https://note.com/alchan/n/ncdd980a6f9b0

この記事が気に入ったらサポートをしてみませんか?