エンジニアの頭の中

フリーランスエンジニアが書く技術系ブログです。

紙の書籍を自炊してKindleによる電子書籍生活に至るまで

現在では、本を読むときは電子書籍がメインとなっており、電子書籍端末として、Kindle Voyageを使用しています。最早、便利すぎてKindleが手放せない状態です。

そんな私も以前は紙の本で読書していたわけですが、紙の本から電子書籍への移行した時のこと、電子書籍生活になって感じたメリット、デメリットなどを記載していきます。

電子書籍へ移行するまでの流れ

紙の本が増えすぎた

まだ電子書籍があまり普及していなかった頃、紙の本を買って読んでいたわけですが、当時の私は月に1〜数冊程度の本を買っていました。
技術書が好きで、小説などはほとんど読みません。技術書は厚みがあり、重いものが多いので、所有する本が増えてきて、本棚が満杯になり、それでも強引に本を本棚へ収納していった結果、自宅の本棚が軋むようになってしまいました。

電子書籍への移行を決意する

このまま紙の本を増やしていったら、本棚には入りきらないし、だからといって本を処分したくなかったのです。
多分もう読まないであろう本はたくさんあるのですが、私はなぜか本だけは捨てたり、売ったりする気にはなれません。(基本的にモノをあまり持ちたくないのですが、本だけはなぜか別・・・)本が部屋の中で結構なスペースを取るようになっていました。

上記のような状態になり、電子書籍への移行を考えました。

持っていた紙の本を自炊する

自炊して電子書籍化するため、道具を揃えました。

スキャナ

一括で自炊して、電子書籍に完全移行するつもりであれば、購入するのはもったいないので、レンタルするのが良いでしょう。DMM.comなどで借りることができます。

ちなみに、自炊に使用するスキャナは、ある程度の量の紙をまとめてセットできるスキャナじゃないと効率が悪く、作業に時間がかかってしまいますので、気をつけてください。

私の場合、スキャナは仕事でも使用する機会があると思い、購入しました。私が購入したものは、Canonの以下のスキャナです。

 

Canon ドキュメントスキャナ imageFORMULA DR-C225W

当然ですが、WindowsでもMacでも使えます。スキャンしたデータをPDF化できる付属のソフトには、Evernote連携機能がついていて、スキャンした結果をEvernoteにダイレクトに保存することができましたが、その機能は一切使いませんでした(自炊には必要無かった)。

スキャナなら、自炊以外でも使用機会があると思って購入したのですが、自宅にある紙書類の電子化を行った後は、利用機会がほとんどなく、しばらくして、処分することになりました。

裁断機

裁断機は以下を使いました。 

プラス 断裁機 かんたん替刃交換 PK-513LN 裁断幅A4タテ 26-309

裁断機は、大きいものが圧倒的に効率が良いです。

小さな裁断機は、値段は安いですが、一冊を裁断するための労力は大きくなります。また、大きいものと比べて綺麗に裁断しにくいです。数十や数百の本を裁断する場合は、現実的ではないです。

さすがに裁断機は、自炊が完了した後は、使用する機会が無いだろうと思い、購入するのではなく、DMM.comでレンタルのサービスをやっているので、そこで借りました。

大きいものを一週間程度借りて、料金は数千円程度だったと思います。 

 

で、持っているほとんどの本(一部の本はは紙のまま所有しておきたかったので残した)を裁断&スキャンして、PDF化しました。スキャン後のバラバラになった本は、もちろん処分しました。

自炊の感想

自炊作業ですが、本が大量にあると結構大変で、二度とやりたくないです。(もう機会もないでしょうが)

本を裁断すること以上に、スキャンすることが大変でした。何度も紙詰まりを起こしましたし、自炊なので、PDFにした本にIndexも貼ろうとしましたが、大変すぎて途中でやめました。

自炊後は、新しく購入する本は、電子書籍が販売されていれば、そちらを買うようにしました。紙でしか販売されないものもありますが、最近はかなり減ったように感じます。

電子書籍を読むようになって感じたメリット、デメリット

3年以上の電子書籍中心の読書生活をしばらく送ってみて、感じてきた電子書籍のメリット、デメリットをまとめます。

この間、電子書籍リーダーは、Kindleを使用していますが、Kindle Paperwhiteを使用しており、後にKindle Voyageに変えています。

よって、電子書籍そのものというより、Kindleを組み合わせてのメリット、デメリットとなります。

本が部屋のスペースを取らない

当たり前ですが、本は電子データとなるため、場所を取りません。端末にデータとして入っている状態です。そうでなくてもクラウドで保管されているため、自分の書籍はいつでも手に入ります。

私はかつて、紙の書籍を本棚に詰め込みすぎて、重みで本棚の木の接合部が壊れたことがありますが、電子書籍に移行したことによって、大量の本が部屋から消え、本棚自体も廃棄しました。

これによって、部屋に空きスペースを確保することができました。

どこにいても全ての本(=知識)にアクセスできる

基本的に、本は端末にダウンロード済みの状態となっているか、そうでなくてもネットワークに繋がっていれば、クラウドからデータがダウンロード可能なので、所有するすべての本がいつでも手元にある状態となります。

つまり、自宅にいても外出していても、参照可能な本の数は変わりません。紙の本ならせいぜい1、2冊しか持ち歩けませんでした。

100冊だろうと1000冊だろうと、200グラム弱の端末に収まるのです。しかも、Kindeの場合、上着のポケットに収まるサイズなので、非常に持ち運びが楽です。

私は、技術書を購入することが多いのですが、リファレンス本などは、作業していて、ふと、あるページを見たいことが結構あります。

紙の本だと、外出時に見たい本が手元になくて困ることがありましたが、電子書籍の場合、この問題がありません。

私は、システムエンジニアなので、外出先でも本の内容を確認したいことは多く、この点は、大きなメリットとなっています。

暗いところでも本が読める

f:id:mitsu3204:20170326234735j:plain

端末にライトが付いているので、暗いところでも問題なく読書ができます。夜寝る前に部屋を暗くして本を読みながら眠くなったら寝るということをよくやってます。

Kindleの場合は、スマートフォンなどとは違い、暗いところでディスプレイを見ても、目に優しいライトになっているので、読みやすいです。ライトの明るさは調節可能です。

フォントの種類や大きさを変更することができる

f:id:mitsu3204:20170326234338j:plain

紙の本では変えようのないフォントの種類や大きさを好みに応じて変更することができます。私の場合、フォントの種類は特に変更することはありませんが、たまに文字の大きさは変更することがあります。

本の価格が安い

紙の書籍と比較すると、電子書籍の販売価格が若干安くなっている場合が多いです。

わからない英単語は英和辞書で調べる&翻訳できる

英語のお勉強のため、簡単な洋書を読むのですが、わからない単語が出てくる場合があります。でも、Kindleには英和辞書などが最初から入っているので、文字列を選択するだけで、単語の意味を調べることができます。

また、ネットワークさえ繋がっていれば、翻訳することもできます。これはかなり便利で助かってます。

電子書籍のデメリット

反対に電子書籍のデメリットと感じた点をあげていきます。

紙の本より読みにくい

やはり、紙の本と比較すると、電子機器の画面は読みやすさの点で劣っています。

電子書籍を読み始めた初期の頃は、Androidタブレット端末を使用して、電子書籍を読んでいましたが、スマートフォンのディスプレイを見ているのと変わらないため、長時間読書していると目が疲れるなどの問題がありました。

ただし、今では電子書籍リーダーを使用することで、ほぼ解決できていると感じます。

特定のページへアクセスしにくい場合がある

ページをパラパラとめくって、本の途中を開くということはできません。

電子書籍では、1ページずつ進むか、戻るか、インデックスにジャンプするか、ページ番号を指定してジャンプするかになります。

開きたいページが決まっていれば、開くのは電子書籍のほうが早いですが、なんとなくページ送りしてみるということはできません。

電池が切れる

電子機器なので、バッテリーが切れる場合があります。

ただ、KindleKoboのような、電子ペーパーを用した端末の場合、スマートフォンや通常のタブレット端末のように、電池消耗は激しくないため、充電が必要となる頻度は、決して高くないと思います。

私の場合、毎日の通勤時にKindleを使用していますが、充電は1ヶ月に1、2度程度です。充電の機会が少ないからこそ、忘れやすいです。

紙の本より発売日が遅い本がある

紙の本が先に発売されて、その1ヶ月後に電子書籍版が発売なんてことがよくあります。1ヶ月以上後から発売されることもあります。

ただ、以前と比べると、紙の本と電子書籍が同時に発売されるケースが増えてきていると感じられるようになりました。

まとめ

電子書籍推しです

メリット、デメリットまとめると、私個人的には電子書籍推しです。デメリットもあるものの、それを遥かに上回るメリットが得られると感じています。特に私のような技術書をたくさん買う人には良いんじゃないでしょうか。

ただ、一口に電子書籍といっても、利用する電子書籍リーダーによって、機能差、価格差があると思われるので、好みが変わってくるかもしれません。

また、本の種類によって、紙の本と電子書籍を使い分けるのも良いかもしれません。(まわりにそういう人はいる)

電子書籍リーダーはKindleで特に不満無し

現在は電子書籍リーダーはKindle Voyageを愛用しています。Kindle Voyageは電子書籍リーダーとしては2台目で、1台目はKindle Paperwhite(2013)を使用していました。

Kindle Paperwhiteにしても、Kindle Voyageにしても、Kindleの読書体験には、全く不満はありません。読みやすさはもちろん、端末の大きさや軽さ、バッテリーの持ちなど、どれも満足しています。

また、Kindleストア以外で購入した電子書籍Kindleで読めます。私は、オライリー社から発売されている技術書をよく購入しますが、本によって、複数のフォーマットが用意されており、Kindleで読むためのmobiファイルが提供されているものも結構あります。mobiファイルが配布されていない本の場合は、PDF形式で読んでいます。

Kindleについて

Kindleには、いくつかのモデルがあります。

さらに各モデルは「キャンペーン情報のあり/なし」と、「Wi-Fi/無料3G」の違いによって、価格が変わります。

「キャンペーン情報あり」の端末は、広告が表示される分、「キャンペーン情報無し」の端末と比較して、2000円ほど端末価格が安くなっています。また、「無料3G付き」のモデルは、Wi-Fiに接続できない環境でも、無料の3G回線を使って、ストアを閲覧したり、購入した電子書籍をダウンロードする事ができます。「無料3G付き」を選択できるモデルは、限られています。

モデル イメージ 価格帯
Kindle (Newモデル) Wi-Fi ¥8,980 - ¥10,980
Kindle Paperwhite ¥14,280 - ¥21,480
Kindle Paperwhite 32GB、マンガモデル、Wi-Fi ¥16,280 - ¥18,280
Kindle Voyage ¥23,980 - ¥31,180
Kindle Oasis バッテリー内蔵レザーカバー付属 ¥35,980 - ¥43,190

 ちなみに、私は、Kindle Voyageのキャンペーン情報無し&Wi-Fiのみ(無料3G無し)のタイプを使用しています。

Goでライフゲーム(Conway's Game of Life)を書いてみた

先日、プログラミング言語を学ぶ際に、よくライフゲームを書いてみるという記事を書きましたが、最近Goに興味を持ち出して、Goでライフゲームを書いてみたくなりました。

以下は、以前の記事です。この時は、JavaScriptとHTMLで作ったライフゲームを記事に載せました。

mi2.hatenablog.com

Goで書いてみた

で、早速Goでライフゲームを書いてみました。こちらが、今回Goで作ってみたライフゲームの画面です。

f:id:mitsu3204:20170325165916p:plain

ターミナル上で稼働します。以下の通り、キー操作が可能です。

  • スペース:セルの進化の停止、再開を切り替えます。
  • Control + R:セルをランダムに再生成します。
  • Control + U:セルの進化の速度を上げます。
  • Control + D:セルの進化の速度を下げます。
  • Escape:ライフゲームを終了します。

ターミナルへのセルの出力方法

termbox-goというGoで作られたターミナル出力を補助するためのライブラリを使用してみました。

github.com

termbox-goは初めて使用しましたが、termbox-goを使用して書かれているgotetrisというテトリスゲームのソースコードを参考にしたら、簡単に使用することができました。

github.com

ちなみに、gotetrisはgoで書いたテトリス(ゲームの)で、ターミナル上でテトリスをプレイすることができます。

termbox-goの使い方は簡単なので、以下の二つのソースを見れば大体わかると思います。

ソースコード

以下がソースコードです。1ファイルのみで書いています。

package main

import (
	"math/rand"
	"time"

	"github.com/nsf/termbox-go"
)

const (
	backgroundColor = termbox.ColorBlack
	cellColor       = termbox.ColorGreen
	textColor       = termbox.ColorWhite
	cellWidth       = 2
	size            = 32
	marginTop       = 2
	marginLeft      = 4
)

var (
	cells        = [size][size]bool{}
	pause        = true
	drawInterval = 100 * time.Millisecond
)

func reset() {
	for row := 0; row < len(cells); row++ {
		for col := 0; col < len(cells[row]); col++ {
			cells[row][col] = rand.Intn(10) < 4
		}
	}
}

func next() {
	newCells := [size][size]bool{}
	for row := 0; row < size; row++ {
		for col := 0; col < size; col++ {
			c := countIfAlive(row, col)
			if c == 3 {
				newCells[row][col] = true
			} else if c == 2 {
				newCells[row][col] = cells[row][col]
			} else {
				newCells[row][col] = false
			}
		}
	}
	cells = newCells
}

func countIfAlive(row, col int) (count int) {
	if row > 0 {
		if col > 0 && cells[row-1][col-1] {
			count++
		}
		if cells[row-1][col] {
			count++
		}
		if col < size-1 && cells[row-1][col+1] {
			count++
		}
	}
	if col > 0 && cells[row][col-1] {
		count++
	}
	if col < size-1 && cells[row][col+1] {
		count++
	}
	if row < size-1 {
		if col > 0 && cells[row+1][col-1] {
			count++
		}
		if cells[row+1][col] {
			count++
		}
		if col < size-1 && cells[row+1][col+1] {
			count++
		}
	}
	return
}

func draw() {
	for row := 0; row < len(cells); row++ {
		for col := 0; col < len(cells[row]); col++ {
			color := backgroundColor
			if cells[row][col] {
				color = cellColor
			}
			for i := 0; i < cellWidth; i++ {
				termbox.SetCell(marginLeft+col*cellWidth+i, marginTop+row, ' ', color, color)
			}
		}
	}
}

func drawText(x, y int, text string) {
	for _, c := range text {
		termbox.SetCell(size+4+x, y, c, textColor, backgroundColor)
		x++
	}
}

func drawKeyOperationsText() {
	y := marginTop
	drawText(size+8, y, "+----------+----------------+")
	y++
	drawText(size+8, y, "| Key      | Function       |")
	y++
	drawText(size+8, y, "+----------+----------------+")
	y++
	drawText(size+8, y, "| Space    | Run or Pause   |")
	y++
	drawText(size+8, y, "| Ctrl + R | Reset cells    |")
	y++
	drawText(size+8, y, "| Ctrl + U | Speed up       |")
	y++
	drawText(size+8, y, "| Ctrl + D | Speed down     |")
	y++
	drawText(size+8, y, "| Escape   | Exit           |")
	y++
	drawText(size+8, y, "+----------+----------------+")
	y++
}

func main() {

	err := termbox.Init()
	if err != nil {
		panic(err)
	}
	defer termbox.Close()

	eventQueue := make(chan termbox.Event)
	go func() {
		for {
			eventQueue <- termbox="" pollevent="" clear="" backgroundcolor="" reset="" draw="" drawkeyoperationstext="" flush="" loop:="" for="" select="" case="" event="" :="<-eventQueue:" if="" type="=" eventkey="" key="=" keyesc="" break="" loop="loop" else="" keyspace="" pause="true" keyctrlr="" keyctrld="" drawinterval="" 50="" time="" millisecond="" keyctrlu=""> 50*time.Millisecond {
						drawInterval -= 50 * time.Millisecond
					}
				}
			}
		default:
			if !pause {
				termbox.Clear(backgroundColor, backgroundColor)
				next()
				draw()
				drawKeyOperationsText()
			}
		}
		termbox.Flush()
		time.Sleep(drawInterval)
	}
}

グライダーガンとか、エデンの園などの固有のパターンも表示できるように改良していきたいです。

新しいプログラミング言語の学習でライフゲームを作る(Conway’s Game of Life)

ライフゲーム(Conway’s Game Of Life)とは

ライフゲームをご存知でしょうか? ライフゲームは、ボード上にセル(細胞)を表示して、それぞれのセルがあるルールに従い、誕生、淘汰、生存を繰り返していく様子を眺めるシミュレーションゲームのことです。

https://upload.wikimedia.org/wikipedia/commons/thumb/7/7f/Game_of_life_boat.svg/82px-Game_of_life_boat.svg.png

知らない人は、これだけじゃ何のことかさっぱりわからないと思います。詳細な説明はWikipediaにお任せします。

ライフゲーム - Wikipedia

※ちなみに日本ではライフゲームと呼びますが、英語では「Conway’s Game of Life」と言います。頭にConway’s (コンウェイの)と付いているのは、人生ゲーム(The Game of Life )と区別するために、先頭にライフゲームの発案者である John Horton Conwayの名前を付けているようです。(ってWikipediaにも書いてあります)

新しいプログラミング言語を覚える時にライフゲームを書く

私はライフゲームが好きです。他にも似たようなものだと群れをシミュレーションするBoidsなども好きです。何と無く眺めているのが面白いのです。 そんなライフゲームですが、私は新しいプログラミング言語に触れてみる際の入門的なプログラムとして、よくライフゲームを書いています。

必要なコードは基本的な条件分岐や、繰り返し処理、配列に加え、ちょっとした乱数、スリープの扱いなどがあるくらいなので、軽くコード書いてみるには、丁度良いのです。ただし、セル(細胞)の描画処理は、標準出力だと寂しいので、GUI付きで書く場合もあります。(ここが若干面倒な場合がありますが)

ライフゲームソースコードJavaScript

セルの描画処理が簡単に書けるので、JavaScriptとHTMLを使用して、WEBブラウザ上でライフゲームを描画するコードを載せておきます。

このコードを起動すると以下のような画面が表示されます。 f:id:mitsu3204:20170320000950p:plain

画面の下部に描画のコントローラがあります。各役割は以下のとおりです。

  • Start ・・・クリックするとセルの描画を開始します。一定時間ごとにセルの世代交代が発生します。
  • Reset ・・・クリックするとセルの状態をリセットします。リセットすると、セルの位置をランダムに再描画します。
  • Speed ・・・横にスライドするとセルの世代交代の速度を変更します。

index.html ライフゲームを動かすためのCanvasを表示するhtmlファイルです。

<html>
<head>
<title>Conway's Game of Life</title>
<script src="./cell.js"></script>
<style>
.controll {
    background-color: #c0c0c0;
    width: 600px;
    border-top: 1px solid #c0c0c0;
}
.controll .label {
    font-size: 14px;
    font-weight: bold;
}
</style>
</head>
<body>

    <!-- セルの描画部分 -->
    <canvas id="cell" width="600" height="500"></canvas>

    <!-- 描画の開始、停止や、速度のコントローラ -->
    <div class="controll">
        <label for="speed" class="label">Speed</label>
        <input id="speed" type="range" min="1" max="100" value="50" onchange="changeSpeed();"/>
        <button id="random" onclick="resetCells();" class="label">Reset</button>
        <button id="start" onclick="start();" class="label">Start</button>
    <div>
</body>
</html>

cell.js index.htmlから読み込んでいるJavaScriptファイルです。

var COLOR_CELL_LIFE = '#00ff00'; // 生きているセルの色
var COLOR_CELL_DEAD = '#000000'; // 死んでいるセルの色
var CELL_SIZE = 14; // セルの大きさ
var interval = 500; // セル描画処理の一時停止止時間(ミリ秒)
var pause = true; // 描画を停止中かどうかを表すフラグ
var cells; // セルの配列
var canvas;
var context;
var timer;

onload = function() {
    init();
};

// 初期化
function init() {
    canvas = document.getElementById('cell');
    context = canvas.getContext('2d');
    cells = new Array(Math.ceil(canvas.height / CELL_SIZE));
    resetCells();
    canvas.onmousedown = mouseMoveListner;
    canvas.onmousemove = mouseMoveListner;
}

// セル描画を初期化する
function resetCells() {

    clearTimeout(timer);

    // ボタンを初期表示に戻す
    document.getElementById('start').innerHTML = 'Start';

    // セルの位置を初期化(ランダムに配置する)
    for (i = 0; i < cells.length; i++) {
        cells[i] = new Array(Math.ceil(canvas.width / CELL_SIZE));
        for (j = 0; j < cells[i].length; j++) {
            if (Math.random() < 0.4) {
                cells[i][j] = true;
            } else {
                cells[i][j] = false;
            }
        }
    }
    
    // セルをcanvasへ描画する
    drawCells();
}

// グリッドをクリックした場合にセルを描画する
function mouseMoveListner(e) {
    if (e.which == 1) {
        var rect = e.target.getBoundingClientRect();
        var mouseX = e.clientX - rect.left;
        var mouseY = e.clientY - rect.top;
        var rowIndex = Math.round(mouseY / CELL_SIZE);
        var colIndex = Math.round(mouseX / CELL_SIZE);
        cells[rowIndex][colIndex] = true;
        if (pause) {
            drawCells();
        }
    }
}

// セルの世代交代を再開する
function start() {
    var startButton = document.getElementById('start');
    if (startButton.innerHTML == 'Start') {
        pause = true;
        startButton.innerHTML = 'Pause';
        draw();
    } else {
        pause = false;
        startButton.innerHTML = 'Start';
        clearTimeout(timer);
    }
}

// セル描画のインターバルを短くして、世代交代の速度を変更する
function changeSpeed() {
    var speed = document.getElementById('speed').value;
    interval = 1000 - speed * 10;
}

var draw = function() {
    var tmpCells = new Array(cells.length);

    for (i = 0; i < cells.length; i++) {
        tmpCells[i] = new Array(cells[i].length);

        for (j = 0; j < cells[i].length; j++) {
            cnt = countRoundCells(i, j);
            if (cnt == 3) {
                tmpCells[i][j] = true;
            } else if (cnt ==2) {
                tmpCells[i][j] = cells[i][j];
            } else {
                tmpCells[i][j] = false;
            }
        }
    }
    cells = tmpCells;
    drawCells();

    clearTimeout(timer);
    timer = setTimeout(draw, interval);
}

// 周囲のセルの数を数えて返す
function countRoundCells(rowIndex, colIndex) {
    topIndex = rowIndex - 1;
    btm = rowIndex + 1;
    lft = colIndex - 1;
    rgt = colIndex + 1;
    count = 0;
    if (rowIndex != 0) {
        if (colIndex != 0 && cells[topIndex][lft]) {
            count++;
        }
        if (cells[topIndex][colIndex]) {
            count++;
        }
        if (colIndex != cells[0].length - 1 && cells[topIndex][rgt]) {
            count++;
        }
    }
    if (colIndex != 0 && cells[rowIndex][lft]) {
        count++;
    }
    if (colIndex != cells[0].length - 1 && cells[rowIndex][rgt]) {
        count++
    }
    if (rowIndex != cells.length - 1) {
        if (colIndex != 0 && cells[btm][lft]) {
            count++;
        }
        if (cells[btm][colIndex]) {
            count++;
        }
        if (colIndex != cells[0].length - 1 && cells[btm][rgt]) {
            count++;
        }
    }
    return count;
}

// セルを描画する
function drawCells() {

    // 現在の描画を消去
    context.clearRect(0, 0, canvas.width, canvas.height);

    // 枠描画
    context.rect(0, 0, canvas.width, canvas.height);
    context.stroke();

    // グリッドを描画
    drawLine();

    for (i = 0; i < cells.length; i++) {
        for (j = 0; j < cells[i].length; j++) {
            x = j * CELL_SIZE + 0.1
            y = i * CELL_SIZE + 0.1
            w = CELL_SIZE - 0.5
            h = CELL_SIZE - 0.5
            if (cells[i][j]) {
                context.fillStyle = COLOR_CELL_LIFE
                context.fillRect(x, y, w, h);
            } else {
                context.fillStyle = COLOR_CELL_DEAD
                context.fillRect(x, y, w, h);
            }
        }
    }
}

// グリッドを描画する
function drawLine() {
    context.linesize = 0.1;
    context.beginPath();

    // 横線
    for (i = 0; i <= cells.length; i++) {
        context.moveTo(0, i * CELL_SIZE);
        context.lineTo(canvas.width, i * CELL_SIZE);
    }

    // 縦線
    for (i = 0; i <= cells[0].length; i++) {
        context.moveTo(i * CELL_SIZE, 0);
        context.lineTo(i * CELL_SIZE, canvas.height);
    }
    context.closePath();
    context.stroke();
}

index.htmlとcell.jsを同じ階層に配置して、index.htmlをWEBブラウザで開くと、ライフゲームが起動します。

Karabinerの代わりにHammerspoonを使用してmacOS SierraでVim風のキーバインドを実現する

f:id:mitsu3204:20170403231525j:plain

macOS Sierraへアップデートするため、macキーバインドツールであるKarabinerの使用をやめて、Hammerspoonに移行した話です。

Karabinerが未対応のためmacOS Sierraに移行できずにいたが思わぬところできっかけが訪れる

macOS Sierra では、キーのカスタマイズソフトであるKarabinerが使用できないため(2017年3月3日時点)、macOS Sierraには移行せず、OS X El Captainを使い続けていました。

Karabiner - OS X用のソフトウェア

macOS SierraではAirPodsが使えない

https://store.storeimages.cdn-apple.com/8561/as-images.apple.com/is/image/AppleInc/aos/published/images/M/ME/MMEF2/MMEF2?wid=572&hei=572&fmt=jpeg&qlt=95&op_sharpen=0&resMode=bicub&op_usm=0.5,0.5,0,0&iccEmbed=0&layer=comp&.v=1473705678057

AirPodsを購入して以来、AirPodsをすっかり気に入っていまいた。

MacでもAirPodsを使用したくなったのですが、OSがmacOS Sierraでないと、AirPodsを使用できないということを知りました。

AirPodsを理由にmacOS Sierraへの移行を決意する

AirPodsが使いたかったので、Karabinerの代わりとなるキーバインドを変更できるソフトを探して、macOS Sierraへと移行することを決意しました。(Sierraの他の機能には興味無し!)

ということで、Karabinerの代わりに使用したソフトウェアと、その設定内容について以下に記載します。

Karabinerで実現していたキーバインド

私が、Karabinerで実現していたキーのカスタマイズは以下の3点のみです。

  • functionキー+hjklキーでカーソル移動(Vim風の移動キーを実現)
  • 左commandキーで英語入力へ切り替え
  • 右commandキーで日本語入力へ切り替え

ちなみに、キーのカスタマイズは、上記以外にも、caps lockキーをcontrolキーとして使用していますが、これはOSの機能で実現可能なため、そちらで設定しています。 

 

Karabinerの代わりにHammerspoonを使用

Karabinerと類似の機能を持つソフトをいくつか調べてみた結果、一番良さそうだったのが、Hammerspoonでした。

github.com

Hammerspoonは、単にキーバインドの変更ツールではなく、様々な自動化を行うためのアプリで、Lua言語で設定を書くのが特徴です。

 

 

ただ、設定をいじり始めて気付いたのが、Hammerspoonでは、hjklキーによる移動をfunctionキーと組み合わせて設定することができないということでした。仕方がないので、諦めてcontrolキーと組み合わせることにしました。

操作が変わってしまう点は慣れれば問題ないでしょう。 

Hammerspoonのインストール

brew cask install するのみです。

brew cask install hammerspoon

GithubのReleasesからzipをダウンロードすることもできます。

Hammerspoonの設定内容

最終的な設定内容は以下の通りです。

  • controlキー+hjklキーでカーソル移動(Vim風の移動キーを実現)
  • 左commandキーで英語入力へ切り替え
  • 右commandキーで日本語入力へ切り替え

設定ファイルの内容は以下の通りです。 

~/.hammerspoon/init.lua

local function keyCode(key, modifiers)
   modifiers = modifiers or {}
   return function()
      hs.eventtap.event.newKeyEvent(modifiers, string.lower(key), true):post()
      hs.timer.usleep(1000)
      hs.eventtap.event.newKeyEvent(modifiers, string.lower(key), false):post()      
   end
end

local function remapKey(modifiers, key, keyCode)
   hs.hotkey.bind(modifiers, key, keyCode, nil, keyCode)
end

local function enableAllHotkeys()
   for k, v in pairs(hs.hotkey.getHotkeys()) do
      v['_hk']:enable()
   end
end

remapKey({'ctrl'}, 'h', keyCode('left'))
remapKey({'ctrl'}, 'j', keyCode('down'))
remapKey({'ctrl'}, 'k', keyCode('up'))
remapKey({'ctrl'}, 'l', keyCode('right'))
remapKey({'ctrl', 'shift'}, 'h', keyCode('left', {'shift'}))
remapKey({'ctrl', 'shift'}, 'j', keyCode('down', {'shift'}))
remapKey({'ctrl', 'shift'}, 'k', keyCode('up', {'shift'}))
remapKey({'ctrl', 'shift'}, 'l', keyCode('right', {'shift'}))
remapKey({'ctrl', 'cmd'}, 'h', keyCode('left', {'cmd'}))
remapKey({'ctrl', 'cmd'}, 'j', keyCode('down', {'cmd'}))
remapKey({'ctrl', 'cmd'}, 'k', keyCode('up', {'cmd'}))
remapKey({'ctrl', 'cmd'}, 'l', keyCode('right', {'cmd'}))
remapKey({'ctrl', 'shift', 'cmd'}, 'h', keyCode('left', {'shift', 'cmd'}))
remapKey({'ctrl', 'shift', 'cmd'}, 'j', keyCode('down', {'shift', 'cmd'}))
remapKey({'ctrl', 'shift', 'cmd'}, 'k', keyCode('up', {'shift', 'cmd'}))
remapKey({'ctrl', 'shift', 'cmd'}, 'l', keyCode('right', {'shift', 'cmd'}))

local prevKeyCode
local escape = 0x35
local leftCommand = 0x37
local rightCommand = 0x36
local eisuu = 0x66
local kana = 0x68

local function keyStroke(modifiers, character)
    hs.eventtap.keyStroke(modifiers, character)
end

local function handleEvent(e)
    local keyCode = e:getKeyCode()
    local isCmdKeyUp = not(e:getFlags()['cmd']) and e:getType() == hs.eventtap.event.types.flagsChanged
    if isCmdKeyUp and prevKeyCode == leftCommand then
        keyStroke({}, eisuu)
    elseif isCmdKeyUp and prevKeyCode == rightCommand then
        keyStroke({}, kana)
    end
    prevKeyCode = keyCode
end

eventtap = hs.eventtap.new({hs.eventtap.event.types.flagsChanged, hs.eventtap.event.types.keyDown, hs.eventtap.event.types.keyUp}, handleEvent)
eventtap:start()

設定を保存したら、hammerspoonのメニューから「Reload Config」を選択すれば、保存した設定内容が反映されます。

 

commandキーの英かな変更は、以下のページを参考にさせて頂きました。

macOS SierraにアップデートしてHammerspoonでCommandキーにかなと英数を割り当てた | mizoguche.info

 

以上

MacBookにギークなステッカーを!unixstickersでのステッカー購入の流れ

http://www.unixstickers.com/image/cache/data/stickers/golang/xgolang.sh-180x180.png.pagespeed.ic.dMPMru1rcp.webp

ギークなステッカーを購入して、MacBookに貼りたくなったので、unixstickersでステッカーを購入してみました。

 

発注から、受け取りまでは、以下のような流れでした。

  • 2月22日 発注
  • 2月22日 発送
  • 3月4日 ステッカー到着(購入から10日後)

発注

Webサイトから発注します。

www.unixstickers.com

 

私は、以下の4点を購入しました。

f:id:mitsu3204:20170304232909p:plain

送料が、$3.99で、合計$14.75でした。決済は、PayPalで行いました。

 

発送

発注してからすぐ、荷物を発送した旨を伝えるメールが送られてきました。

f:id:mitsu3204:20170304233505j:plain

発送が早かったのは、たまたまタイミングが良かっただけかもしれません。

ステッカー到着

10日後、無事に荷物が到着しました。メール便なのでポストに入ってました。

f:id:mitsu3204:20170304232015j:plain

驚いたのが、注文したステッカー以外に、色々なステッカー(一部はマグネット)が同梱されていました。

以下が、受け取ったステッカーの全てなのですが、私が注文したステッカーは、上段の4つのみです。

f:id:mitsu3204:20170304232320j:plain

何なのかよくわからないステッカーも混じってますが、オマケをつけてくれたんですかね・・・

貼ることは無さそうw 

フリーランスエンジニアの年収

f:id:mitsu3204:20160521142522j:plain

今から数年前の、27歳の頃に約4年勤めたIT系の会社を辞めて、辞めた翌日からフリーランスのエンジニアとなりました。
その際の収入面の変化について記載します。

なぜ会社を辞めてフリーランスのエンジニアになったのか?

会社を辞めてフリーランスになった理由としては、当時いろいろと考える事はあったものの、ザックリ書き出すと以下のような感じです。

理由その1.収入で満足していなかった

私は年収で400万円くらいでした。サラリーマンの平均年収が400万円程度であることと、当時の自分の年齢(辞めようと決断した時は26歳)を考えれば、特別年収が低いということは無いかとは思いますが、私としては満足していませんでした。

理由その2.この会社で今後も働いてきたいとは思えなかった

決定的に悪い箇所があったというわけでは無いですが、逆に大きな魅力を感じるところがなかったということです。

社員の仲間とは、仲良くしていたし、嫌いではなかったのですが、エンジニアとして魅力を感じる人はあまりいませんでした。

また、自社で展開しているサービスや、経営陣の面々にも、魅力を感じるところは少なかったです。

フリーランスエンジニアとしての最初の仕事の見つけ方

開発案件を紹介してくれる企業を利用する

開発案件を紹介してくれるエージェント企業がいるので、そちらのお世話になりました。そのため、フリーランスとしての最初の仕事は、会社員の間に見つけることができました。

ちなみに私がお世話になったのは、首都圏コンピュータ株式会社というちょっと変わった名前の会社です。現在は、社名をPE-BANKと変えていますが、この会社は業界では長くやっている方で、エンジニアの中では有名ですね。

他にも複数のエージェント企業から開発案件を紹介してもらっていたのですが、条件の良くない開発案件が多かったので、それらの仕事は受けませんでした。

契約までの流れ

流れとしては、エージェント企業にお客さんを紹介してもらい、お客さんと面談して、お互いに条件に問題がなければ、仕事が決まり、晴れて契約といった流れです。(この業界では普通の流れですね。フリーだろうが、会社員だろうが変わりません)

注意点

上記のような仕事を紹介してくれる企業に対しては、手数料として月の契約単価の1割前後(会社や契約回数などでも変わる)を払う必要があります。

毎月1割前後も支払い続けるのはもったいないです。最初に仕事を取ってきたこと以外に彼らに価値はありません。

よって、私の場合は、仕事に慣れてからは、エージェント企業を介さずに、お客さんと直接契約するようにしました。そのほうが、契約条件の交渉もエージェント企業の営業を介さずに自分できるのでオススメします。

フリーランスエンジニアになってからの収入の変化

肝心の収入ですが、フリーランスになってどれくらい年収が変わったか、具体的な数値を以下に示します。 

年間の売上

平均月収

2010

455万円 ※4月からのため9ヶ月分

50.5万円

2011

714万円

59.5万円

2012

761万円

63.4万円

2013

854万円 

71.1万円

2014

904万円

75.3万円

2015

1056万円

88.0万円

2016

1093万円

91.0万円

 

会社員時代の400万円という収入から比べると、大分変わりましたね。直近では売り上げが1000万円を超えていますが、私が特別というわけではありません。私の知り合いのフリーランスのエンジニア達には、もっと稼いでいる人もいます。

基本的に客先のオフィスに常駐するスタイルでやっています。代金は「一月いくら」というスタイルです。

売り上げが年々増えてるのは、1ヶ月当たりの代金(単価)が上がっているからです。同じプロジェクトに携わっている間にも、単価交渉をして単価を上げさせて頂いたことが何度かあります。

上記の売り上げから、各種経費を引いた分が所得ということになります。経費は、通勤の交通費や、携帯電話などの通信費、また、自宅を事務所という扱いにしているのであれば、自宅兼事務所ということで自宅の家賃や、水道光熱費なども、按分して何割かを経費にすることができます。

収入以外の変化

会社員時代の雑務がなくなる

これは勤めている会社によって異なりますが、私の場合は、自社の煩わしい雑務が一切なくなりました。会社勤めの頃は、社外のプロジェクトに常駐していても、たまに自社で雑務をこなしたり、自宅からメールのやり取りをする事があったのですが、これらがなくなった事により、時間が確保できるようになりました。

会社に勤めるということに対する意識が変わる

フリーランスになることを意識している会社員の人もいるとは思いますが、その中には会社員でなくなるという事に不安を感じている人もたくさんいると思いますが、会社を辞める事を怖がる必要は無いと思います。

私がフリーランスになってしばらくして思ったのは、会社員だろうが、フリーランスだろうが、開発者としてやるべき事は何も変わらないということです。私は、フリーランスになる前は「仕事をする」ということは「会社に勤める」ということと同等であると、頭の何処かで認識してしまっていたのだと思います。

実際には働き方なんてどうでも良い、もっと自由で良かったのです。

フリーランスエンジニアになるべきか?

結論から言うと、働き方は人それぞれの事情、好みなどがあるので、「こうあるべき」ということは無いと思います。

私が言えるのは、現状の収入に不満があったり、勤めている会社で上を目指していきたいわけでもなければ、フリーランスになった方が良いと思います。

会社を辞めて失敗するんじゃないかと心配している人もいるでしょうが、心配しないでください。世の中にエンジニアの仕事は溢れています。よっぽど能力の低いエンジニアでない限りはうまくやれるでしょう。

むしろ、燻ったまま行動しないでいることこそ、一番の失敗だと私は思います。