気を散らすノート

色々と散った気をまとめておくところ.論文読んだり自分で遊んだりする.たぶん.

コードでお絵描きの件

とてもシンプルな話ですぐできるという話を口頭で伝えきれなかったので補足.技術的に有意義な情報はありません.

普段扱う2次元画像は,普段扱う画像なら各ピクセルが (r,g,b) の値を持つ二次元配列と捉えることができる. 逆に,横x, 縦y の位置にあるピクセルに対して,その持つべき色 (r,g,b) を与えることができれば,それを画像化することができる.こういうケースは多いので,手で PNG ファイルとかを書き込んでいくのは大変でも,多くの言語ではライブラリとかでかんたんに使えるようになっている.

ほか,コードからお絵描きするときのもう一つの手として,多くのライブラリには「円を書く」「四角を塗りつぶす」とかそういう命令を持っていて,多くの場合はこちらが使いやすい.

命令を重ねて絵を書いていく

たとえば javascript なら canvas を使うのがシンプルかつ触りやすい:

<!DOCTYPE html>
<html lang="ja">
<head> <script src="./main.js"></script>
</head>
<body>
    <canvas id="example" width="500" height="500" style="border:2px solid black; width:500px; height:500px;"></canvas>
</body>
</html>
// main.js
function main(){
    const c = document.getElementById("example");
    const ctx = c.getContext('2d');
    for (let i=0; i<50; i++){ // 10px ずつずらして50個
        for (let j=0; j<50; j++){
            ctx.fillStyle="#193"; // 緑
            ctx.fillRect(i*10, j*10, 6+Math.random(), 6+Math.random());
            ctx.fillStyle="#9af"; // 小さい青
            ctx.fillRect(i*10+3+Math.random(), j*10+3+Math.random(), 3, 3);
        }
    }
}

window.addEventListener('load', main);

いろんな錯視が見えそうになる

例えば2018年の年賀状の柄も同じように書いている.

ピクセルの値を与える

ピクセル (x,y) に対してそこでの色 (r,g,b) を与えて,そこから画像を作ることを考える. 概念的には二次元配列 [[ c(x,y) ]] を用意して(各 c(x,y) は (r,g,b) の組),それを画像に変換することになる.

javascript でいうと,canvas 関連で ImageDataといのがあって,めちゃくちゃ平たくは [ rxy, gxy, bxy, αxy ] の順でできた1次元配列に近い形式になっている. これをCanvasRenderingContext2D.putImageData() を使って画像に戻すことになる.パッと見ややこしいが:

// main.js
const WIDTH=500;
const HEIGHT=500;
function main(){
    const c = document.getElementById("example");
    const ctx = c.getContext('2d');
    let imageData =  ctx.createImageData(WIDTH,HEIGHT);
    for(let y=0; y<HEIGHT; y++) {
        for (let x=0; x<WIDTH; x++) {
            // 各ピクセルの色を決めていく!
            let loc = (y*WIDTH + x)*4; // (r,g,b,a) for each pixel
            imageData.data[loc] = 0; // r = 0
            imageData.data[loc+1] = y/HEIGHT * 255; // g ~ y
            imageData.data[loc+2] = x/WIDTH * 255; // b ~  x
            imageData.data[loc+3] = 255; // a
        }
    }
    ctx.putImageData(imageData,0,0);
}

window.addEventListener('load', main);

ピクセルごとに値を決めるというやり口はシェーダと近いのだが,こちらは並列計算のための制約があるわけではないので,他のピクセルに依存したりするの (cellular automata とか) も書ける.

この前話題になったの

この前のは, (x_0, y_0) = (0,0) から初めて, x_{n+1} = y_n + a*x_n, y_{n+1} = (x_n)^2 +b で決まる (x_n, y_n) (ただし a= 0.18525845, b=-1.1336243)の列がどういう点を動くかを画像にしたもの(点が通ったところに青の点を打っていっている). それぞれの点 (x,y) を,ここではちょっと大きめの 1500*1000 px のキャンバスに移して,そのピクセルの色だけ書き換えれば良い. だいたい -1.8 - 1.8 くらいの座標に収まるから,そのように変形して

<!DOCTYPE html>
<html lang="ja">
<head>
   <script src="./main.js"></script>
</head>
<body>
    <canvas id="example" width="1500" height="1000" style="border:2px solid black; width:1500px; height:1000px;"></canvas>
</body>
</html>
// main.js
const WIDTH=1500;
const HEIGHT=1000;
const N = 200000; // number of iteration

const A= 0.18525845;
const B=-1.1336243;

const DW = 1.8;
const DH = DW/1.5;

function main(){
    const c = document.getElementById("example");
    const ctx = c.getContext('2d');
    let imageData =  ctx.createImageData(WIDTH,HEIGHT);
    let x = 0; let y = 0;
    // まずかっこいいので背景黒くする: aだけ 255 に
    for (let i=3; i<imageData.data.length; i+=4){ imageData.data[i]=255; } 
    // その上に点列おいていく
    for (let n=0; n<N; n++){
        let x_next = y+ A*x; let y_next = x*x + B;
        // 点 (x,y) は,キャンバス上の点でいうとここ
        let grid_x = Math.floor((x_next * WIDTH) / (2*DW) + WIDTH/2);
        let grid_y = Math.floor((y_next * HEIGHT) / (2*DH) + HEIGHT/2);
        // imageData.data は各ピクセルにつき (r,g,b,a) を並べた1次元配列
        let loc = (grid_y*WIDTH+ grid_x)*4;
        imageData.data[loc] = 60; // r
        imageData.data[loc+1] = 100; // g
        imageData.data[loc+2] = 200; // b
        imageData.data[loc+3] = 255; // a
        x = x_next; y = y_next;
    }
    console.log(imageData.data);
    ctx.putImageData(imageData,0,0);
}

window.addEventListener('load', main);

こんな感じになる:

ちょっとかっこいいアレ

Python なら Pillow, Rust なら image などを使えば同じことはほぼ同じ手間でできるはず.