これは、「Processing Advent Calendar 2021」第一日目の記事です。
Processing は、その開発開始から今年で 20年目を迎えました。 20年、と一言でいっても「おぎゃー」といってた子がお酒を飲めるようになるまで。 長いです。
こんなに長い間開発を続けるってどういうことなのか? 何を思って何を目指して何のため? 楽しいときもつまらないときも、晴れの日も雨の日も風の日も、夏が去って冬が来て、それを20回繰り返す間開発を続けるって、一体どんな感じなのでしょう?
その感覚に少しでも触れることができないかと、Processing 20年の開発の記録を Processing を使ってビジュアライズしてみました。
👉 Read this article in English.
したようでしてない、ビジュアライズ
Processing の GitHub 上に記録されているコードの追加/削除の数値を、20年間の経過に合わせて変化させるアニメーションにしようと思いました。
データビジュアライゼーション作品と行きたかったのですが、データをもとに図を描くこと、音を出すことに精一杯で、「データからどういう意味を見出すか」という肝心な点を考えることが全く出来ませんでした。
そんなわけで、これは「データビジュアライゼーション作品」とは言えない、「データを基にただ動くだけのアニメーション」になりました。
できあがりのビデオ
出来上がった動画がこちらになります。音が出ますのでご注意ください。
表示されている Processing のバージョンとその期間は、そのバージョンに向けて開発している期間になっています。開発の記録ということで、このような表示にしました。
と、説明が必要なようじゃダメな気もします。 「意味」そっちのけで作ったらこうなるってことですね。
表示それぞれの部分が表しているものはこちらになります。
表示期間
バージョン
変更/追加/削除
GitHub 上の 変更(files changed)、追加(insertions)、削除(deletions)の値を表示しています。
お飾り
あとの2つは完全にお飾りです。
Processing 20年の過去、そして未来
約8ヶ月が 1秒に短縮されたこのアニメーション。 『1.0 リリースまでが長いなぁ』とか『2.0 から 3.0 の開発、活発で楽しそう!』とか思いながら繰り返し眺めていると、そこには確かに人々の手によってコツコツと積み上げられてきた大切なものがあると感じられます。
あらためて 20年の開発って大変なことだと思います。 普段自分の作品をつくるのに、どれだけ時間がかかっていますか? このアニメーションを作るのには結構時間がかかりましたが、それも 20年間に比べたらないに等しいようなものでした。 そもそもこのアニメーションを作れたのも、20年間の開発を経て作られた Processing という土台があったからです。
普段意識していませんでしたが、20年の開発成果の恩恵を受けながら作品を作れるのって、あらためて考えるととても幸せなことですね。 なんだかやる気が出てきましたよ!
これから先の 20年、きっといろんなことが起こるでしょう。 Processing 本体の開発は、細かい改善、新しいメディアや技術への対応など、これからも続いていくでしょうし、Processing のコミュニティももっともっと広がっていくでしょう。 Processing を使った新しい表現はもちろん、思ってもみなかったような使い方も出てくるかもしれません。
Processing を使ってこの先何ができるのか、今度は利用者である私達が次の 20年間を作っていく。 このアニメーションを作りながら、そんなことを考えていました。
どう作ったか?
データを基にした作り方のヒントや、応用の効きそうな部分に絞って、このアニメーションをどう作ったかを簡潔に説明します。
全体概略
- GitHub から git コマンドを使ってデータを取得し、テキストファイル(txt)に落とします。
- それを Perl のスクリプトを使ってマージしたり整形したりしてタブ区切りデータ(tsv)に書き出します。
- そのデータを Processing のコードで読み込んで図を描き、画像ファイル(png)に書き出します。
- 複数の画像ファイルを ffmpeg コマンドで動画(mp4)に変換します。
音は別撮りして動画編集ファイル(kdenlive)でアニメーションと同期させました。
GitHub からのデータのとり方
元データは Processing の GitHub から取得しました。 対象のデータはコミット履歴の中の変更/追加/削除の個数です。
取得は git コマンドで行いました。
git log --shortstat --reverse --date=short --no-merges
取得したデータの期間は 2001-07-26 から 2021-07-06 でした。
GitHub - processing/processing: Source code for the Processing Core and Development Environment (PDE)
https://github.com/processing/processing
Processing 4.0 からは下記リポジトリに移動しています。
https://github.com/processing/processing4/
Processing でのデータの持ち方
GitHub から取得したデータは、1日分 = 1レコードとしてファイルに書き出しました。
コードでは、1レコード上の項目全てを保持する 'Activity' という名のクラスを作り、レコード毎にそのクラスのインスタンスを生成、それを ArrayList に格納してゆくことで、全レコードを保持した ArrayList 'acts' を作りました。
この ArrayList 'acts' にアクセスすることで、全データを簡単に扱うことができます。
Processing での日付の扱い
日付は、表示用とは別に、Unix Time 値も持ちました。
Unix Time は 1970/01/01 00:00:00 からの経過秒数なので、期間の計算等が楽で扱いやすいです。
各パーツの描画
それぞれのパーツを背景透明の PGraphics 上に個別に描画し、それを 'image()' 関数を使ってキャンバス上に載せています。 こうするとパーツの位置調整や重なりの順番を変えたりがすごく楽になります。
Processing のコード
細かいところは説明するよりコードを見ていただいたほうが早いと思うので(私の書いたぐちゃなコードでもそう言えるだろうか…)、描画のコードを掲載しておきます。
GPL で公開します。GPL の条項に基づいて、どうぞご自由にお使いください。
/**
* The Processing 20 years in 30 seconds.
* data visualized animation of the Processing commit log data on GitHub.
*
* Processing 3.5.3
* @author @deconbatch
* @version 0.1
* @license GNU GPL v3
* created 0.1 2021.11.21
*/
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
/**
* there is only setup(), no draw().
*/
void setup() {
size(960, 540);
colorMode(HSB, 360.0, 100.0, 100.0, 100.0);
smooth();
noLoop();
imageMode(CENTER);
// constants
int frmRate = 30;
int secTotal = 27; // animation duration seconds
int frmMax = frmRate * secTotal;
int daysRange = 360 * 2;
int utPerDay = 60 * 60 * 24; // unix time length of one day
int cW = width;
int cH = height;
// colors
Colour cBlueDark = new BlueDark();
Colour cBlueBright = new BlueBright();
Colour cCyan = new Cyan();
Colour cRed = new Red();
Colour cVioletDark = new VioletDark();
Colour cVioletBright = new VioletBright();
Colour[] colours = {cCyan, cBlueBright, cBlueDark, cVioletDark, cVioletBright, cRed};
// read develop activities from the file
Development dev = new Development("./data/data.log", utPerDay);
int daysStep = floor((dev.daysDev + daysRange) / (frmMax - 1));
// read released version from the file
ArrayList<Version> vers = getVersions("./data/version.log");
// title decoration image
PImage theSun = modCircle(getActsRange(dev.acts, dev.dailyIndex, 0, dev.daysDev), dev.utTimeFrom, dev.daysDev * utPerDay, cW, floor(cH * 1.7));
// opening
noStroke();
textFont(createFont("Waree Bold",24,true));
textAlign(CENTER, CENTER);
textSize(cW * 0.03);
for (int frmCnt = 0; frmCnt < frmRate * 4; frmCnt++) {
background(0.0, 0.0, 90.0, 100.0);
// decoration
pushMatrix();
translate(cW * 0.5, cH * 0.5);
rotate(frmCnt * 0.01);
image(theSun, 0.0, 0.0);
popMatrix();
// mainTitle
image(
mainTitle(cW, cH, cBlueDark),
cW * 0.5,
cH * 0.5
);
// count down
fill(0.0, 0.0, 50.0, 100.0);
ellipse(cW * 0.5, cH * 0.85, cW * 0.08, cW * 0.08);
fill(0.0, 0.0, 90.0, 100.0);
text(3 - floor(frmCnt / frmRate), cW * 0.5, cH * 0.85);
saveFrame("frames/00." + String.format("%04d", frmCnt) + ".png");
}
// draw data visualization
// background image
PImage bd = drawBack(cW, cH);
// for the image of building-up data
PGraphics pgDecoGraph = null;
PGraphics pgDecoBubble = null;
for (int frmCnt = 0; frmCnt < frmMax; frmCnt++) {
float frmRatio = map(frmCnt, 0, frmMax, 0.0, 1.0);
int dayStart = frmCnt * daysStep - daysRange; // days count of the start
int utStart = dev.utTimeFrom + dayStart * utPerDay; // unix time of the start date
int utRange = daysRange * utPerDay; // days range in unix time to draw
List<Activity> actsDraw = getActsRange(dev.acts, dev.dailyIndex, dayStart, daysRange); // activities to draw
// background
background(0.0, 0.0, 90.0, 100.0);
image(bd, cW * 0.5, cH * 0.5);
// center column
image(
dateBanner(utStart, utStart + utRange, cW, floor(cH * 0.25), cBlueDark),
cW * 0.5,
cH * 0.1
);
image(
versionArc(vers, utStart, utRange, cW, cH, colours),
cW * 0.5,
cH * 0.5
);
image(
modCircle(actsDraw, utStart, utRange, cW, cH),
cW * 0.5,
cH * 0.5
);
image(
versionGraph(vers, dev.utTimeFrom, dev.utTimeTo, utStart, utRange, floor(cW * 0.8), floor(cH * 0.1), cBlueDark, cVioletBright),
cW * 0.5,
cH * 0.9
);
// left column
image(
insdelCircle(actsDraw, utStart, utRange, floor(cW * 0.5), floor(cH * 0.5), cBlueBright, cRed),
cW * 0.175,
cH * 0.385
);
image(
logBar(actsDraw, floor(cW * 0.25), floor(cH * 0.25), cVioletDark, cBlueBright, cRed),
cW * 0.225,
cH * 0.75
);
// right column
pgDecoGraph = decoGraph(pgDecoGraph, actsDraw, floor(cW * 0.20), floor(cH * 0.15), frmRatio, frmMax, cVioletDark);
image(
pgDecoGraph,
cW * 0.8,
cH * 0.265
);
pgDecoBubble = decoBubble(pgDecoBubble, actsDraw, floor(cW * 0.23), floor(cH * 0.5), frmRatio, cBlueBright);
image(
pgDecoBubble,
cW * 0.8,
cH * 0.56
);
saveFrame("frames/01." + String.format("%04d", frmCnt) + ".png");
}
// stop motion of the last frame
for (int frmCnt = frmMax; frmCnt < frmMax + frmRate; frmCnt++) {
saveFrame("frames/01." + String.format("%04d", frmCnt) + ".png");
}
// ending
noStroke();
textFont(createFont("Source Code Pro Bold",24,true));
textAlign(LEFT, CENTER);
for (int frmCnt = 0; frmCnt < frmRate * 5; frmCnt++) {
float circleRate = constrain(map(frmCnt, 0, frmRate * 2, 0.0, 1.0), 0.0, 1.0);
background(0.0, 0.0, 90.0, 100.0);
pushMatrix();
// moving
if (frmCnt < frmRate * 1) {
translate(cW * 0.5, cH * 0.5);
} else if (frmCnt < frmRate * 2) {
translate(cW * 0.5, cH * map(frmCnt, frmRate, frmRate * 2, 0.5, 0.33));
} else {
translate(cW * 0.5, cH * 0.325);
}
// decoration fade out
pushMatrix();
rotate(frmCnt * 0.01);
tint(0.0, 0.0, 100.0, sin(circleRate * PI) * 100.0);
image(theSun, 0.0, 0.0);
popMatrix();
// mainTitle fade in
tint(0.0, 0.0, 100.0, constrain(map(frmCnt, 0, frmRate * 1, -50.0, 100.0), 0.0, 100.0));
image(
mainTitle(cW, cH, cBlueDark),
0.0,
0.0
);
popMatrix();
// message fade in
pushMatrix();
translate(cW * 0.26, cH * 0.635);
fill(0.0, 0.0, 0.0, constrain(map(frmCnt, 0, frmRate * 3, -200.0, 100.0), 0.0, 100.0));
textSize(cW * 0.03);
text("I'll dedicate this video to", 0.0, 0.0);
text("Processing developers and", 0.0, cH * 0.08);
text("Processing lovers all over", 0.0, cH * 0.16);
text("the world.", 0.0, cH * 0.24);
textSize(cW * 0.02);
text("–deconbatch", cW * 0.35, cH * 0.25);
popMatrix();
saveFrame("frames/02." + String.format("%04d", frmCnt) + ".png");
}
exit();
}
/**
* mainTitle : draw main title image.
*/
public PGraphics mainTitle(int _w, int _h, Colour _c) {
int margin = 20;
float radius = (min(_w, _h) - margin) * 0.6;
float blindR = (min(_w, _h) - margin) * 0.5;
PGraphics g = createGraphics(_w, _h);
g.beginDraw();
g.colorMode(HSB, 360, 100, 100, 100);
g.smooth();
g.blendMode(REPLACE);
g.textFont(createFont("Waree Bold",240,true));
g.background(0.0, 0.0, 0.0, 0.0);
g.translate(_w * 0.5, _h * 0.5);
// plate
g.blendMode(BLEND);
g.noStroke();
g.rectMode(CENTER);
g.fill(0.0, 0.0, 90.0, 80.0);
g.rect(0.0, 0.0, _w, _h * 0.25);
g.fill(_c.h, _c.s, _c.b, 40.0);
g.rect(0.0, 0.0, _w, _h * 0.25);
g.blendMode(REPLACE);
g.fill(0.0, 0.0, 0.0, 0.0);
g.ellipse(0.0, 0.0, radius, radius);
g.stroke(0.0, 0.0, 0.0, 0.0);
g.strokeWeight(10.0);
g.noFill();
g.rect(0.0, 0.0, _w * 2.0, _h * 0.15);
g.noStroke();
g.fill(_c.h, _c.s, _c.b, 60.0);
g.ellipse(0.0, 0.0, blindR, blindR);
// title
g.textAlign(CENTER, CENTER);
g.noStroke();
g.fill(0.0, 0.0, 0.0, 100.0);
g.textSize(_w * 0.018);
g.text("The Processing", -_w * 0.33, 0.0);
g.text("in 30 seconds.", _w * 0.33, 0.0);
g.fill(0.0, 0.0, 100.0, 100.0);
g.textSize(_w * 0.065);
g.text("20", 0.0, -_h * 0.02);
g.textSize(_w * 0.026);
g.text("years", 0.0, _h * 0.1);
g.endDraw();
return g;
}
/**
* drawBack : draw background decoration image.
*/
public PGraphics drawBack(int _w, int _h) {
PGraphics g = createGraphics(_w, _h);
g.beginDraw();
g.colorMode(HSB, 360, 100, 100, 100);
g.smooth();
g.blendMode(REPLACE);
g.textFont(createFont("Waree Bold",240,true));
g.background(0.0, 0.0, 0.0, 0.0);
// g.translate(_w * 0.5, _h * 0.5);
// node garden
float div = min(_w, _h) * 0.01;
ArrayList<PVector> nodes = new ArrayList<PVector>();
for (float x = 0.0; x < _w; x += div) {
for (float y = 0.0; y < _w; y += div) {
if (random(1.0) < 0.3) {
nodes.add(new PVector(x, y));
}
}
}
g.strokeWeight(div * 0.08);
for (PVector n : nodes) {
for (PVector m : nodes) {
float d = dist(n.x, n.y, m.x, m.y);
if (d > div && d < div * 2.0) {
float v = noise(n.x, n.y);
g.stroke(0.0, 0.0, 90.0 + v * 10.0, 100.0);
g.line(n.x, n.y, m.x, m.y);
break;
}
}
}
g.noStroke();
for (PVector n : nodes) {
float v = noise(n.x, n.y);
float s = (0.5 + v) * div * 0.5;
g.fill(0.0, 0.0, 90.0 + v * 10.0, 100.0);
g.ellipse(n.x, n.y, s, s);
}
g.endDraw();
return g;
}
/**
* dateBanner : draw from/to date.
*/
public PGraphics dateBanner(int _start, int _end, int _w, int _h, Colour _c) {
PGraphics g = createGraphics(_w, _h);
g.beginDraw();
g.colorMode(HSB, 360, 100, 100, 100);
g.smooth();
g.blendMode(REPLACE);
g.background(0.0, 0.0, 0.0, 0.0);
g.textFont(createFont("Waree Bold",240,true));
g.textAlign(CENTER, CENTER);
g.textSize(_w * 0.03);
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String start = fmt.format(ZonedDateTime.ofInstant(Instant.ofEpochSecond(_start), ZoneId.systemDefault()));
String end = fmt.format(ZonedDateTime.ofInstant(Instant.ofEpochSecond(_end), ZoneId.systemDefault()));
// date string
g.translate(_w * 0.5, _h * 0.5);
g.noStroke();
g.fill(_c.h, _c.s, _c.b, _c.a);
g.text(start + "–" + end, 0.0, 0.0);
g.endDraw();
return g;
}
/**
* versionArc : draw arcs of version.
*/
public PGraphics versionArc(List<Version> _vers, int _start, int _range, int _w, int _h, Colour[] _cs) {
int margin = 20;
float divT = TWO_PI / _range;
float radius = (min(_w, _h) - margin) * 0.3 * 1.6;
PGraphics g = createGraphics(_w, _h);
g.beginDraw();
g.colorMode(HSB, 360, 100, 100, 100);
g.smooth();
g.blendMode(REPLACE);
g.background(0.0, 0.0, 0.0, 0.0);
g.textFont(createFont("URWGothic-Demi",240,true));
g.translate(_w * 0.5, _h * 0.5);
g.stroke(0.0, 0.0, 0.0, 100.0);
g.strokeWeight(2.0);
g.textSize(_w * 0.03);
g.textAlign(CENTER, CENTER);
Version pVer = _vers.get(0);
int sizeVers = _vers.size();
int sizeClrs = _cs.length;
for (int i = 0; i < sizeVers; i++) {
Colour clr = _cs[i % sizeClrs];
Version cVer = _vers.get(i);
if (cVer.unixTime < _start + _range) {
float thetaP = constrain((pVer.unixTime - _start) * divT, 0.0, TWO_PI);
float thetaC = constrain((cVer.unixTime - _start) * divT, 0.0, TWO_PI);
float drawS = TWO_PI * 1.75 - thetaP;
float drawE = TWO_PI * 1.75 - thetaC;
float drawT = TWO_PI * 1.75 - (thetaP + thetaC) * 0.5;
g.fill(clr.h, clr.s, clr.b, clr.a);
g.arc(0.0, 0.0, radius, radius, drawS, TWO_PI * 1.75, PIE);
g.fill(0.0, 0.0, 0.0, 100.0);
g.text(pVer.version, radius * 0.35 * cos(drawT), radius * 0.35 * sin(drawT));
if (cVer.unixTime < _start) {
break;
}
}
pVer = cVer;
}
g.endDraw();
return g;
}
/**
* modCircle : draw circles of bar graph of the files changed number.
*/
public PGraphics modCircle(List<Activity> _acts, int _start, int _range, int _w, int _h) {
int margin = 20;
float divT = TWO_PI / _range;
float radius = (min(_w, _h) - margin) * 0.3;
PGraphics g = createGraphics(_w, _h);
g.beginDraw();
g.colorMode(HSB, 360, 100, 100, 100);
g.smooth();
g.blendMode(REPLACE);
g.background(0.0, 0.0, 0.0, 0.0);
g.translate(_w * 0.5, _h * 0.5);
// radially lines
g.noFill();
g.strokeWeight(2.0);
for (Activity act : _acts) {
float theta = (act.unixTime - _start) * divT;
g.pushMatrix();
g.rotate(-theta);
g.stroke(0.0, 0.0, 0.0, 100.0 * constrain(theta, 0.0, PI) / TWO_PI);
g.translate(0.0, -radius);
g.line(0.0, 0.0, 0.0, -act.change * _w * 0.002);
g.popMatrix();
}
// decoration of broken line circle
g.fill(0.0, 0.0, 90.0, 30.0);
g.strokeWeight(5.0);
g.stroke(0.0, 0.0, 50.0, 100.0);
g.ellipse(0.0, 0.0, radius * 1.8, radius * 1.8);
for (int i = 0; i < 12; i++) {
float theta = i * TWO_PI / 12.0;
g.noStroke();
g.fill(0.0, 0.0, 90.0, 60.0);
g.ellipse(
radius * 0.9 * cos(theta),
radius * 0.9 * sin(theta),
radius * 0.05,
radius * 0.05
);
}
g.endDraw();
return g;
}
/**
* versionGraph : draw bar graph of the day x version.
*/
public PGraphics versionGraph(List<Version> _vers, int _from, int _to, int _start, int _range, int _w, int _h, Colour _cGrh, Colour _cTime) {
float step = _w * 1.0 / (_to - _from);
PGraphics g;
g = createGraphics(_w, _h);
g.beginDraw();
g.colorMode(HSB, 360, 100, 100, 100);
g.smooth();
g.blendMode(REPLACE);
g.background(0.0, 0.0, 0.0, 0.0);
// draw bar graph
g.fill(_cGrh.h, _cGrh.s, _cGrh.b, _cGrh.a);
g.stroke(_cGrh.h, _cGrh.s, _cGrh.b, _cGrh.a);
int utPrev = _from;
for (int i = _vers.size() - 2; i >= 0 ; i--) {
Version cVer = _vers.get(i);
float gX = (utPrev - _from) * step;
float gW = (cVer.unixTime - utPrev) * step;
float gH = map(Float.parseFloat(cVer.version), 1.0, 4.0, 0.01, 1.0) * _h;
g.rect(gX, _h - gH, gW, gH);
utPrev = cVer.unixTime;
}
// draw days range
float rW = _range * step;
float rX = (_start - _from) * step;
g.noFill();
g.stroke(_cTime.h, _cTime.s, _cTime.b, _cTime.a);
g.strokeWeight(3.0);
g.rect(rX, 0.0, rW, _h);
g.endDraw();
return g;
}
/**
* insdelCircle : draw circle graph of the insertions and deletions data.
*/
public PGraphics insdelCircle(List<Activity> _acts, int _start, int _range, int _w, int _h, Colour _cIns, Colour _cDel) {
float divT = TWO_PI / _range;
float baseRad = _h * 0.1;
PGraphics g = createGraphics(_w, _h);
g.beginDraw();
g.colorMode(HSB, 360, 100, 100, 100);
g.smooth();
g.blendMode(REPLACE);
g.background(0.0, 0.0, 0.0, 0.0);
// ins
g.pushMatrix();
g.translate(_w * 0.5, _h * 0.35);
g.stroke(_cIns.h, _cIns.s, _cIns.b, _cIns.a);
g.strokeWeight(2.0);
g.noFill();
g.beginShape();
g.vertex(
0.0 * cos(HALF_PI),
0.0 * sin(HALF_PI)
);
for (Activity act : _acts) {
float theta = (act.unixTime - _start) * divT;
float radius = baseRad + log(act.insert) * _w * 0.005;
g.vertex(
radius * cos(+theta + HALF_PI),
radius * sin(+theta + HALF_PI)
);
}
g.vertex(
0.0 * cos(HALF_PI),
0.0 * sin(HALF_PI)
);
g.endShape(CLOSE);
g.fill(0.0, 0.0, 90.0, 60.0);
g.stroke(_cIns.h, _cIns.s, _cIns.b, _cIns.a);
g.strokeWeight(6.0);
g.ellipse(0.0, 0.0, baseRad * 1.8, baseRad * 1.8);
g.popMatrix();
// del
g.pushMatrix();
g.translate(_w * 0.5, _h * 0.65);
g.stroke(_cDel.h, _cDel.s, _cDel.b, _cDel.a);
g.strokeWeight(2.0);
g.noFill();
g.beginShape();
g.vertex(
0.0 * cos(-HALF_PI),
0.0 * sin(-HALF_PI)
);
for (Activity act : _acts) {
float theta = (act.unixTime - _start) * divT;
float radius = baseRad + log(act.delete) * _w * 0.005;
g.vertex(
radius * cos(-theta - HALF_PI),
radius * sin(-theta - HALF_PI)
);
}
g.vertex(
0.0 * cos(-HALF_PI),
0.0 * sin(-HALF_PI)
);
g.endShape(CLOSE);
g.fill(0.0, 0.0, 90.0, 60.0);
g.stroke(_cDel.h, _cDel.s, _cDel.b, _cDel.a);
g.strokeWeight(6.0);
g.ellipse(0.0, 0.0, baseRad * 1.8, baseRad * 1.8);
g.popMatrix();
g.endDraw();
return g;
}
/**
* logBar : draw bar graph of the files changed, insertions and deletions data.
*/
public PGraphics logBar(List<Activity> _acts, int _w, int _h, Colour _cMod, Colour _cIns, Colour _cDel) {
float tSize = _w * 0.1;
int last = _acts.size() - 1;
PGraphics g = createGraphics(_w, _h);
g.beginDraw();
g.colorMode(HSB, 360, 100, 100, 100);
g.smooth();
g.blendMode(REPLACE);
g.background(0.0, 0.0, 0.0, 0.0);
g.rectMode(CORNER);
g.textFont(createFont("URWGothic-Demi",240,true));
g.textAlign(LEFT, CENTER);
g.textSize(tSize);
float rX = tSize * 3.0;
float rY = -tSize * 0.25;
float rH = tSize * 0.8;
// mod
g.pushMatrix();
g.translate(0.0, _h * 0.1);
g.noStroke();
g.fill(_cMod.h, _cMod.s, _cMod.b, _cMod.a);
g.text("FILE", 0.0, 0.0);
g.rect(rX, rY, log(_acts.get(last).change) * 15.0, rH);
g.popMatrix();
// ins
g.pushMatrix();
g.translate(0.0, _h * 0.1 + tSize * 1.1);
g.noStroke();
g.fill(_cIns.h, _cIns.s, _cIns.b, _cIns.a);
g.text("INS", 0.0, 0.0);
g.rect(rX, rY, log(_acts.get(last).insert) * 15.0, rH);
g.popMatrix();
// del
g.pushMatrix();
g.translate(0.0, _h * 0.1 + tSize * 2.2);
g.noStroke();
g.fill(_cDel.h, _cDel.s, _cDel.b, _cDel.a);
g.text("DEL", 0.0, 0.0);
g.rect(rX, rY, log(_acts.get(last).delete) * 15.0, rH);
g.popMatrix();
g.endDraw();
return g;
}
/**
* decoGraph : draw decoration image of the files changed.
*/
public PGraphics decoGraph(PGraphics _g, List<Activity> _acts, int _w, int _h, float _pathRatio, int _plotNum, Colour _c) {
int last = _acts.size() - 1;
PGraphics g;
if (_g == null) {
g = createGraphics(_w, _h);
g.beginDraw();
g.colorMode(HSB, 360, 100, 100, 100);
g.smooth();
g.blendMode(REPLACE);
g.background(0.0, 0.0, 0.0, 0.0);
} else {
g = _g;
g.beginDraw();
}
float rH = log(_acts.get(last).change) * _h * 0.1;
g.stroke(_c.h, _c.s, _c.b, _c.a);
g.fill(_c.h, _c.s, _c.b, _c.a);
g.translate(0, _h * 0.5);
g.rect(
_w * _pathRatio,
-rH * 0.5,
_w / _plotNum,
rH
);
g.endDraw();
return g;
}
/**
* decoBubble : draw decoration image of the files changed, insertions and deletions data.
*/
public PGraphics decoBubble(PGraphics _g, List<Activity> _acts, int _w, int _h, float _pathRatio, Colour _c) {
int last = _acts.size() - 1;
PGraphics g;
if (_g == null) {
g = createGraphics(_w, _h);
g.beginDraw();
g.colorMode(HSB, 360, 100, 100, 100);
g.smooth();
g.blendMode(REPLACE);
g.background(0.0, 0.0, 0.0, 0.0);
} else {
g = _g;
g.beginDraw();
}
float eSize = log(_acts.get(last).change) * _w * 0.01;
g.noFill();
g.stroke(_c.h, _c.s, _c.b, _c.a);
g.ellipse(
_w * (0.45 + (log(_acts.get(last).insert) - log(_acts.get(last).delete)) * 0.065),
_h * (0.1 + _pathRatio * 0.8),
eSize,
eSize
);
g.endDraw();
return g;
}
/**
* getActsRange : get acitivity data in days range.
*/
public List<Activity> getActsRange(ArrayList<Activity> _acts, int[] _dailyIndex, int _start, int _range) {
// get start index
int start = _start;
if (_start < 0) {
start = 0;
} else if (_start >= _dailyIndex.length){
start = _acts.size() - 1;
} else {
for (int i = _start; i <= _start + _range; i++) {
if (_dailyIndex[i] != -1) {
start = _dailyIndex[i];
break;
}
}
}
// get activities
int end = _start + _range;
if (_start + _range < 0) {
end = 0;
} else if (_start + _range >= _dailyIndex.length){
end = _acts.size() - 1;
} else {
for (int i = _start + _range; i >= _start; i--) {
if (_dailyIndex[i] != -1) {
end = _dailyIndex[i];
break;
}
}
}
return _acts.subList(start, end + 1);
}
/**
* getVersions : make day x version array from version data file.
*/
public ArrayList<Version> getVersions(String _fileName) {
ArrayList<Version> vers = new ArrayList<Version>();
// read version data file
String fileData = null;
ArrayList<ArrayList<String>> recs = new ArrayList<ArrayList<String>>();
try {
File f = new File(_fileName);
byte[] b = new byte[(int) f.length()];
FileInputStream fi = new FileInputStream(f);
fi.read(b);
fileData = new String(b);
} catch (Exception e) {
println("exception");
}
if (fileData == null) {
println("no data");
}
String[] rows = fileData.split("\n");
for (int i = 0; i < rows.length; i++) {
// println(rows[i]);
String[] cols = rows[i].split("\t");
vers.add(new Version(
cols[0],
Integer.parseInt(cols[1]),
cols[2]
));
}
return vers;
}
/**
* Version : hold version info.
*/
public class Version {
public String date;
public int unixTime;
public String version;
Version(String _s, int _u, String _v) {
date = _s;
unixTime = _u;
version = _v;
}
}
/**
* Development : make day x develop acitivities array from activity data file.
*/
public class Development {
public int utTimeFrom; // develop start unix time
public int utTimeTo; // develop end
public int daysDev; // during days of development
public ArrayList<Activity> acts; // development activity
public int dailyIndex[]; // holds activity index of every day of daysDev
Development(String _fileName, int _utPerDay) {
utTimeFrom = 0;
utTimeTo = 0;
// read activity file
String fileData = null;
ArrayList<ArrayList<String>> recs = new ArrayList<ArrayList<String>>();
try {
File f = new File(_fileName);
byte[] b = new byte[(int) f.length()];
FileInputStream fi = new FileInputStream(f);
fi.read(b);
fileData = new String(b);
} catch (Exception e) {
println("exception");
}
if (fileData == null) {
println("no data");
}
String[] rows = fileData.split("\n");
for (int i = 0; i < rows.length; i++) {
String[] cols = rows[i].split("\t");
ArrayList<String> colAry = new ArrayList<String>();
for (int j = 0; j < cols.length; j++) {
colAry.add(cols[j]);
}
recs.add(colAry);
if (i == 0) {
utTimeFrom = Integer.parseInt(cols[1]);
} else if (i == rows.length - 1) {
utTimeTo = Integer.parseInt(cols[1]);
}
}
// Activity data array set
// some day has no activity
// daysDev = 7286, recs.size() = 2798
daysDev = floor((utTimeTo - utTimeFrom) / _utPerDay) + 1; // days of development
acts = new ArrayList<Activity>(); // Activity data array
// initialize with -1
dailyIndex = new int[daysDev];
for (int i = 0; i < daysDev; i++) {
dailyIndex[i] = -1;
}
for (int i = 0; i < recs.size(); i++) {
ArrayList<String> rec = recs.get(i);
acts.add(new Activity(
rec.get(0),
Integer.parseInt(rec.get(1)),
Integer.parseInt(rec.get(2)),
Integer.parseInt(rec.get(3)),
Integer.parseInt(rec.get(4))
));
dailyIndex[floor((acts.get(i).unixTime - utTimeFrom) / _utPerDay)] = i;
}
}
}
/**
* Activity : hold activity info.
*/
public class Activity {
public String date; // date of the activity
public int unixTime; // unix time of the date
public int change; // changed files count
public int insert; // insert count
public int delete; // delete count
Activity(String _s, int _u, int _c, int _i, int _d) {
date = _s;
unixTime = _u;
change = _c;
insert = _i;
delete = _d;
}
}
/**
* Colour : define the theme color
* you know this theme ;-)
*/
abstract class Colour {
public int h, s, b, a; // hue, saturation, brightness, alpha
}
public class BlueDark extends Colour {
BlueDark() {
h = 232;
s = 83;
b = 35;
a = 100;
}
}
public class BlueBright extends Colour {
BlueBright() {
h = 231;
s = 82;
b = 67;
a = 100;
}
}
public class Cyan extends Colour {
Cyan() {
h = 218;
s = 49;
b = 100;
a = 100;
}
}
public class Red extends Colour {
Red() {
h = 343;
s = 86;
b = 93;
a = 100;
}
}
public class VioletDark extends Colour {
VioletDark() {
h = 271;
s = 99;
b = 64;
a = 100;
}
}
public class VioletBright extends Colour {
VioletBright() {
h = 267;
s = 71;
b = 100;
a = 100;
}
}
/*
Copyright (C) 2021- deconbatch
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
http://www.gnu.org/licenses/
データ形式
GitHub activity
表示用日付 Unix time Files change Insertion Deletion
例
2001-07-26 996073200 19 2376 114
2001-07-27 996159600 3 18 7
Version history
表示用日付 Unix time バージョン
例
2018-07-22 1532185200 3.3
2017-01-29 1485615600 3.2
Processing Community Catalog
Processing 誕生20周年を記念して発行される「20th Anniversary Community Catalog」にこの作品を使った静止画を応募しました。
Processing Community Catalog
https://processingfoundation.org/advocacy/community-catalog
採用されるかな?されるといいな。
これだけ作り込んで、力込めて説明記事まで書いておいて、カタログに載ってなかったりしたら、それはそれでズッコケで面白い!
Processing Advent Calendar 2021
今年で第11回目の開催となる「Processing Advent Calendar 2021」二日目の記事は、ZAWA WORKS(@Zawa_works)さんの「ZIG SIM × Processing|スマホをゲームコントローラにしよう!」です。 お楽しみください。