テキストでプレゼンテーションツールkoxela

読みは"こけら"で、お願いします。

で、例のごとくPartty!.orgで録画しようとしたのですが、Partty!.orgのターミナルエミュレータは256色エスケープシーケンス対応じゃないというアレなんで、Partty!.org付属のcapttyを使います。


実はPartty!.orgはURLの"session"のところを"pty"に書き換えると、capttyで再生できる形式のファイルが落とせるのです。
と、いうことで、まず256色対応のターミナルを用意します。
256対応かどうかはここサイトでチェックできます。
で、Partty!.orgからcapttyデータを落としてきます。
http://www.partty.org/pty/goth/2008/05/03/01/28/36
あとはcapttyで再生するだけ

$ captty play 36

ファイル名は適当に。


一応ガジェットも張っておきますが、最後の最後で盛大にバグります。

はやく256色対応しないかなぁ〜 >> id:viver


今回の録画したターミナルがバカでかいので、フォントサイズを下げるなりしてpartty-scaleがちゃんと表示されるようにしてください。
録画データの先頭の方でpartty-scaleを何度か叩いてますが、合わせられなかったらgを押してもう一度先頭にrewindして何度も調整してやってください。
サイズは214x62です。



と、いうことで、ここからkoxelaの説明。
koxelaは完全テキストベースのプレゼンテーションツールです。
プレゼンテーションデータはYAML形式で書かれてます。
vimさえあれば簡単にプレゼンが作れます。
自前でパース、も考えたのですが、まぁ、YAMLでいいでしょう。


実際のプレゼンテーションデータ(の一部)です。(拡張子はなんとなく.kxl

# filename: demo.kxl
# vim: ft=yaml:sw=2:ts=2:sts=2:et
common:
  width: 600
  height: 400
  background: [0, 0, 0]
  foreground: [1, 1, 1]
  chars: " .,~=+?I7Z08DNM#"
  font:
    name: nil
    size: 200
  line:
    width: 10
pages:
  - title: Top
    objects:
      - type: "string"
        string: "自己紹介"
        pos: [300, 200]
        size: 150
  - title: Intro0
# 略

インデントが意味を持つので注意。あとインデントは半角スペースのみで、Tabは不可です。
あと"#"から始まる行はコメントです。
まぁ、その辺はYAMLの仕様で。


で。最初のcommon。
width/heightはkxlファイルの中で使う仮想的な座標系の大きさを表します。
実際にはターミナルのサイズにスケールされます。
と、いうのも、ターミナルサイズはいつも固定ワケではないので、kxlファイルの中で"X文字目のY列目"とか指定してしまうと、ターミナルサイズ変更で困ることに...
もっと大きな値を与えてもいいのでしょうけど、まぁ、どうせターミナルサイズにスケールされるので...。

background/foregroundはプレゼンの背景色とオブジェクトのデフォの描画色です。
後術するobjectにcolorを指定しない場合foregroundが適応されます。
個人的に黒背景のターミナルが基本なんで、背景は黒、描画色が白になってます。

charsはアスキーアート化するときに使う文字。
右に行くほど線密度が濃くなるようにするとよさげ。" #"で二階調になります。

fontはstring objectのデフォのフォント。
nameをnilにすると適当に並んでくれるそうです(Cairoに丸投げなもので、その辺はRubyCairoの仕様です...。

lineのwidthはデフォの線の太さ。
この辺はまだ適当に与えてるのでガラッと仕様変えるかも...(д


pagesのところから実際のプレゼンデータが始まります。

  - title: Top
    objects:
      - type: "string"
        string: "自己紹介"
        pos: [300, 200]
        size: 150

titleはページのタイトル...なんだけど何にも使ってないというね。
ターミナルのタイトルとか変えようかなとか画策ちぅ。
typeは"string"の他"line","rect","circle"を実装済み。

stringは文字を描画。
必須なプロパティは、表示する文字を指定する"string"と、表示する座標"pos"
オプショナルで"font", "size", "color", "width"(線の太さ), "method"("fill"で塗りつぶし、"stroke"で輪郭だけ)

lineは線を描画。必須は、始点/終点座標"pos1"、"pos2"
オプショナルで"color", "width"

rectは四角を描画。必須は、角の2点の座標"rect"([x1, y1, x2, y2])
オプショナルで"color", "width"

circleは円。必須は中心座標"pos"と半径"radius"
オプショナルで"color", "width"


と、言った感じ。
細かい修正(エラーチェックとか仕様の策定とか)したら正式にリリースしたいと思います〜。(とか言って忘れるんだよな...。atode "koxelaをリリースする"


とりあえず現段階のソースコード

#!/usr/bin/env ruby
# lisence = とりあえずGPLv2

require "cairo"
require "yaml"

TIOCGWINSZ = 0x00005413
TIOCSWINSZ = 0x00005414

str = ""
$stdin.ioctl( TIOCGWINSZ, str )
(row, col) = str.unpack("S!4")

if ARGF.filename == "-" then
	exit
end
yaml = YAML.load( ARGF.read )
width = yaml["common"]["width"]			|| 600
height = yaml["common"]["height"]		|| 400
bg = yaml["common"]["background"]		|| [0, 0, 0]
fg = yaml["common"]["foreground"]		|| [1, 1, 1]
chars = yaml["common"]["chars"]			|| " .,~=+?I7Z08DNM#"
fontName = yaml["common"]["font"]["name"]	|| ""
fontSize = yaml["common"]["font"]["size"]	|| 400
lineWidth = yaml["common"]["line"]["width"]	|| 10
pages = yaml["pages"]				|| []

surface = Cairo::ImageSurface.new( col, row )
context = Cairo::Context.new( surface )

context.scale( col / width.to_f, row / height.to_f )

basename = File.basename ARGF.filename, ".*"
File.open( "#{basename}.sh", "w" ){ |fp|
	fp.puts "#!/bin/sh"
	fp.puts "# col:#{col} row:#{row}"
	pages.each_with_index{ |page,i|
		context.set_source_rgb( bg ).rectangle( 0, 0, width, height ).fill
		context.save
		page["objects"].each{ |obj|
			case obj["type"]
			when "line"
				context.set_source_rgb( obj["color"] || fg )
				context.move_to( obj["pos1"][0], obj["pos1"][1] )
				context.line_to( obj["pos2"][0], obj["pos2"][1] )
				context.set_line_width( obj["width"] || lineWidth )
				case obj["method"] 
				when "fill"
					context.fill
				when "stroke"
					context.stroke
				else
					context.stroke
				end
			when "circle"
				context.set_source_rgb( obj["color"] || fg )
				context.circle( obj["pos"][0], obj["pos"][1], obj["radius"] )
				context.set_line_width( obj["width"] || lineWidth )
				case obj["method"] 
				when "fill"
					context.fill
				when "stroke"
					context.stroke
				else
					context.fill
				end
			when "rect"
				context.set_source_rgb( obj["color"] || fg )
				context.rectangle( *obj["rect"] )
				context.set_line_width( obj["width"] || lineWidth )
				case obj["method"] 
				when "fill"
					context.fill
				when "stroke"
					context.stroke
				else
					context.stroke
				end
			when "string"
				context.select_font_face( obj["font"] || fontName, 0, 0 )
				context.set_font_size( obj["size"] || fontSize )
				context.set_source_rgb( obj["color"] || fg )
				context.set_line_width( obj["width"] || lineWidth )

				ext = context.text_extents( obj["string"] )
				extWidth = ext.x_advance - ext.x_bearing
				extHeight = ext.y_advance - ext.y_bearing

				context.move_to( obj["pos"][0] - extWidth / 2, obj["pos"][1] + extHeight / 2)
				context.text_path( obj["string"] )
				case obj["method"] 
				when "fill"
					context.fill
				when "stroke"
					context.stroke
				else
					context.fill
				end
			else
			end
		}

		preColorCode = 0
		jabanner = ""
		row.times { |y|
			col.times { |x|
				rr = surface.data[4*(x + y * col) + 2]
				gg = surface.data[4*(x + y * col) + 1]
				bb = surface.data[4*(x + y * col) + 0]

				begin
					colorCode =
						(rr * 6 / 256) * 36 +
						(gg * 6 / 256) * 6 +
						(bb * 6 / 256) +
						16
					if colorCode != preColorCode then
						jabanner += "\033[38;5;%dm" % colorCode
						preColorCode = colorCode
					end
				end

				yy = (rr * 0.299 + gg * 0.587 +  bb * 0.114)
				jabanner += chars[chars.length * yy / 256].chr
			}
		}
		fp.puts "echo\ \"#{jabanner.gsub( "\"", "\\\"" )}\""
		fp.puts "read"
		context.restore
	}

	surface.finish
	fp.puts "reset"
}
`chmod u+x #{basename}.sh`

きたねぇソース...
激氣

オープンソースなんで適当にいじっちゃってくださいな。
難しいことは何一つしてないので、多分人によっては10分コースぐらいかと。