klear 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -4,3 +4,4 @@
4
4
 
5
5
  ## PROJECT::SPECIFIC
6
6
  README.html
7
+ Gemfile.lock
data/Guardfile CHANGED
@@ -1,6 +1,6 @@
1
1
  # Guardfile info at: https://github.com/guard/guard#readme
2
2
 
3
- guard 'rspec', all_on_start: true, cli: '--format nested --color' do
3
+ guard 'rspec', all_on_start: true, cli: '--format nested --debug --color' do
4
4
  watch(%r{^spec/.+_spec\.rb$})
5
5
  watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
6
6
  watch('spec/spec_helper.rb') { "spec" }
data/README.md ADDED
@@ -0,0 +1,154 @@
1
+ # Kinetic Light Engine Archice - klear
2
+
3
+ Create and manage choreographies for motors and lights on the Manta Rhei. This
4
+ document describes the Kinetic Light Engine File Format which contains
5
+ Choreographies and all data needed for playing it.
6
+
7
+ To understand what this is all about please check the Manta Rhei project page:
8
+
9
+ - http://www.artcom.de/en/projects/project/detail/manta-rhei/
10
+
11
+ This gem now is the starting point for refactoring the Manta Rhei code. The klear repo will contain all code for handling the actual content. Running the stepper motors or driving the OLEDs or DMX is part of the kinetic-light-engine which goes online soon.
12
+
13
+ ## File info
14
+
15
+ $ klear info choreo.kle
16
+
17
+ will show what is in the file.
18
+
19
+ ## New File Generation
20
+
21
+ *Notice: file generation depends on jruby because of its usage of Java JAI*
22
+
23
+ Klear files are zipped directory structures which are generated from a set of images. The pixel values directly map to motor position and light intensity. On top of that, the klear file contains some additional meta info and cache date to speed up its loading at runtime. Generating a klear file from a images sequence in a directory goes like:
24
+
25
+ $ rvm jruby exec ./bin/klear.rb generate image_sequence_dir outfile.kle
26
+
27
+ and of course, more documentation needs to come (means must be converted from the internal wiki to here).
28
+
29
+ ## FileFormat
30
+
31
+ File of this format have the file extension "kle", e.g. "calm_05.kle"
32
+
33
+ ### Layout and container Structure
34
+
35
+ A .kle File contains multiple assets, incl. source PNGs, metatdata and a derived binary file containing frames in packed binary form. The Container Format is ZIP so a .kle file is just a zip-file containing files and folders.
36
+
37
+ *Directory Layout*
38
+
39
+ * Directory 'META-INF' (mandatory)
40
+ * 'kle.yml' containing meta-data describing aspects like framerate & more
41
+ * 'MANIFEST.MF' containing meta-data about the kle file itself (e.g. format version)
42
+ * Directory 'frames' (mandatory)
43
+ * Source PNGs which were used to generate the kle
44
+ * Directory 'cache' (optional)
45
+ * File 'frames.bin' containing binary data derived from the PNGs during creation
46
+ * further pre-processed data or application state for optimized restarts might be stored here.
47
+ * Directory 'icon'
48
+ * contains one file 'normal.png' for a normal sized icon (150 x 110 px)
49
+
50
+ ### Workflow
51
+
52
+ The initial source of a kle is a sequence of PNGs + metadata. Those are used to generate a .kle file incl. the file 'frames.bin'. The metadata is stored in 'META-INF/kle.yml' and describes aspects like fps.
53
+
54
+ If a kle file does not have frame.bin in its cache directory it can be regenerated. This is also useful for future format changes together with the manifest to detect if a frames.bin is deprecated and needs to be regenerated.
55
+
56
+ The source sequence of PNGs is stored in the KLE-file as well, which allows features like frames.bin regeneration in the first place.
57
+
58
+ ### File Format Details
59
+
60
+ #### PNGs
61
+
62
+ Each single PNG represents exactly one frame and is stored with 16-bit / channel. The size of the PNGs is determined by the number of columns and rows, where each tile is 10px x 10px in size. A Column represents the state of a blade at a frame (a certain point in time). The number of columns represents the number of blades. A Row represents one aspect across all blades, e.g. an outermost light or the state of the motor.
63
+
64
+ *Example*
65
+
66
+ !Waves_00129.png!
67
+
68
+ * We have 11 rows and 14 columns (blades)
69
+ * The lowest row describes the motor state
70
+ * The other rows describe the state of the lights from one direction to the other (TODO: Define the direction - what is a point of reference?)
71
+ * The Png then is 140x110px in size.
72
+
73
+ #### Sequence of PNGs
74
+
75
+ The order of the sequence is defined by the sorting delivered by the Posix command `sort -n`. So any natural alphabetical naming to order the sequence is allowed.
76
+
77
+ The number of columns and rows must be the same for all PNGs.
78
+
79
+ *Examples*
80
+
81
+ * `A.png, B.png, X.png` is valid sequence of 3 PNGs
82
+ * `Test_0001.png, Test_0002.png, Test__1000.png` is a valid sequence
83
+
84
+ A sequence does not need to be consecutive (it can have gaps e.g. `01.png,10.png` is valid and the existence of e.g. 05.png is not enforced).
85
+
86
+ #### kle.yml
87
+
88
+ Contains information about how to use the frames:
89
+
90
+ * Number of columns and rows (geometry)
91
+ * Frames per second
92
+ * recommended gamma value
93
+ * Potentially a free descriptor (name)
94
+
95
+ The geometry is determined automatically by reading the first png in the png sequence and dividing width and height by 10 respectively. This relies on one tile in the png being 10x10px in size.
96
+
97
+ *Example:*
98
+ <pre>
99
+ ---
100
+ description: calm_02
101
+ geometry:
102
+ rows: 11
103
+ columns: 14
104
+ fps: 25
105
+ </pre>
106
+
107
+ #### MANIFEST.MF
108
+
109
+ The manifest contains meta information about the file and file format itself:
110
+
111
+ * `Manifest-Version` Version of the manifest itself
112
+ * `Kle-Version` Version of the kle-file format (e.g. `1.0`)
113
+ * `Created-By` Tool which created this file.
114
+
115
+ *Example:*
116
+
117
+ <pre>
118
+ Manifest-Version: 1.0
119
+
120
+ Kle-Version: 1.0
121
+ Created-By: ruby-kle-generator 0.344b
122
+ </pre>
123
+
124
+ #### frames.bin
125
+
126
+ The frames.bin file contains the extracted 16-bit values sampled from each tile of the PNGs. It contains all frames and each frame contains all columns and rows.
127
+
128
+ Each 16-bit value is saved as unsigned 16 bit integer big endian (network byte order) and uses 2 bytes in frames.bin.
129
+
130
+ A PNG with 11 rows and 14 columns uses `14 x 11 x 2 bytes = 308 bytes`.
131
+
132
+ Each frame is encoded rows bottom to top and the columns from left to right.
133
+
134
+ Example of a PNG with 3 rows & cols:
135
+
136
+ col 1 col 2 col 3
137
+ |-------|-------|-------|
138
+ | 27009 | 38885 | 47331 | <- row 3
139
+ |-------|-------|-------|
140
+ | 51027 | 51233 | 49789 | <- row 2
141
+ |-------|-------|-------|
142
+ | 47645 | 45039 | 41857 | <- row 1
143
+ |-------|-------|-------|
144
+
145
+ is written as a sequence of values like:
146
+
147
+ 47645 45039 41857 51027 51233 49789 27009 38885 47331
148
+
149
+
150
+ which results in a binary big endian (network) byte order sequence like:
151
+
152
+ 0xBA1D 0xAFEF 0xA381 0xC753 0xC821 0xC27D 0x6981 0x97E5 0xB8E3
153
+
154
+ As a note: row 1 is the motor state.
@@ -50,11 +50,12 @@ Applix.main(ARGV, Defaults) do
50
50
  @app.regenerate(*args)
51
51
  end
52
52
 
53
- # handle(:info) do |*args, opts|
54
- # (0 < args.size) or usage
55
- # choreograhpy = Klear::Choreography.load args[0]
56
- # puts choreograhpy.info
57
- # end
53
+ handle(:info) do |*args, opts|
54
+ (0 < args.size) or usage
55
+ path = args.shift
56
+ choreo = Klear::Choreography.load(path)
57
+ puts choreo.info
58
+ end
58
59
 
59
60
  handle(:any) {|_, _| usage}
60
61
  end
data/klear.gemspec CHANGED
@@ -9,10 +9,11 @@ Gem::Specification.new do |s|
9
9
  s.authors = ['art+com/dirk luesebrink']
10
10
  s.email = ['dirk.luesebrink@artcom.de']
11
11
  s.homepage = 'http://www.artcom.de/en/projects/project/detail/manta-rhei/'
12
- s.summary = 'create, manage and play choreographies and atrificial light patterns on the Manta Rhei'
12
+ s.summary = 'create and manage choreographies for motors and lights on the Manta Rhei'
13
13
  s.description = %q{
14
- kinetic light engine by art+com. Create, manage and play choreographies and
15
- artificial light patterns on the Manta Rhei
14
+ create and manage choreographies for motors and lights on the Manta Rhei.
15
+ klear is the kinetic-light-engine archive format and is used by the
16
+ kinetic-light-engine runtime to drive the Manta Rhei installation.
16
17
  }
17
18
  s.add_dependency 'applix'
18
19
  s.add_dependency 'rubyzip'
@@ -27,7 +28,7 @@ Gem::Specification.new do |s|
27
28
  s.add_development_dependency 'rb-fsevent', '~>0.9.1'
28
29
 
29
30
  if RUBY_PLATFORM.match /java/i
30
- #s.add_development_dependency 'ruby-debug'
31
+ s.add_development_dependency 'ruby-debug'
31
32
  else
32
33
  s.add_development_dependency 'debugger'
33
34
  end
@@ -0,0 +1,150 @@
1
+ # TODO Add separate accessors for motors and lights?
2
+ # TODO Add row/blade accessors to Frames so a time-slice can be retrieved?
3
+
4
+ # Note: Row 0 is the motor setting / this is equivalent to the first value of a blade
5
+ class Klear::Choreography
6
+
7
+ #class Frames < BinData::Record
8
+ # array :numbers, :type => :uint16be, :read_until => :eof
9
+ #end
10
+
11
+ attr_reader :archive, :geometry, :gamma, :fps
12
+ attr_accessor :location
13
+
14
+ def self.load path
15
+ #choreo = Zip::ZipFile.open(path) { |klear| new klear }
16
+ self.new(Klear::File.new(path))
17
+ end
18
+
19
+ def initialize archive
20
+ @archive = archive
21
+ @location = @archive.path
22
+ #info = YAML.load(@archive.read("META-INF/kle.yml"))
23
+ info = @archive.info
24
+ @geometry = info['geometry']
25
+ @fps = info['fps'] || 25
26
+ @gamma = info['gamma'] || 1.0
27
+ @frames = @archive.frames
28
+ end
29
+
30
+ def info
31
+ puts <<-INFO
32
+ Klear file at: #{@location}
33
+
34
+ Settings:
35
+ ----------------
36
+ fps : #{@fps}
37
+ gamma : #{@gamma}
38
+ geometry:
39
+ columns: #{@geometry[:columns]}
40
+ rows : #{@geometry[:rows]}
41
+ ----------------
42
+
43
+
44
+ Frames:
45
+ ----------------
46
+ framesize : #{framesize}
47
+ framecount: #{framecount}
48
+ ----------------
49
+
50
+
51
+ MANIFEST:
52
+ ----------------
53
+ #{@archive.manifest}
54
+ ----------------
55
+ INFO
56
+ end
57
+
58
+ def id
59
+ File.basename(self.location, ".kle")
60
+ end
61
+
62
+ def framesize
63
+ @geometry[:columns] * @geometry[:rows]
64
+ end
65
+
66
+ def framecount
67
+ @frames.count
68
+ end
69
+
70
+ def frame no
71
+ @frames.get(no)
72
+ end
73
+
74
+ def each_frame &blk #block_given?
75
+ (0...framecount).each do |n|
76
+ myFrame = frame(n)
77
+ blk.call(myFrame, n+1)
78
+ end
79
+ end
80
+ end
81
+
82
+ __END__
83
+ class Klear::Choreography::Frame
84
+
85
+ attr_reader :data
86
+
87
+ def initialize data, geometry
88
+ @data = data
89
+ @geometry = geometry
90
+ @num_cols = @geometry[:columns] # local copy of columns to avoid per pixel symbol lookups
91
+ @num_rows = @geometry[:rows] # local copy of columns to avoid per pixel symbol lookups
92
+ end
93
+
94
+ def size
95
+ [@geometry[:columns], @geometry[:rows]]
96
+ end
97
+
98
+ def cell column, row
99
+ @data[@num_cols * row + column]
100
+ end
101
+
102
+ def row(no)
103
+ @data.slice(no * @geometry[:columns], @geometry[:columns])
104
+ end
105
+
106
+ # XXX returning an array would be better than hashed by row number...
107
+ def rows &blk
108
+ myRows = {}
109
+ (0...@geometry[:rows]).each do |n|
110
+ current = myRows[n] = row(n)
111
+ blk.call(current, n) if block_given?
112
+ end
113
+ myRows
114
+ end
115
+
116
+ def column theColumnNumber
117
+ col = []
118
+ (0...@geometry[:rows]).each { |curRow|
119
+ col << @data[theColumnNumber + curRow * @geometry[:columns]]
120
+ }
121
+ col
122
+ end
123
+
124
+ def columns &blk
125
+ myColumns = {}
126
+ (0...@geometry[:columns]).each do |n|
127
+ current = myColumns[n] = column(n)
128
+ blk.call(current, n) if block_given?
129
+ end
130
+ myColumns
131
+ end
132
+
133
+ #def each_column &blk
134
+ # (0...@geometry[:columns]).each do |n|
135
+ # myColumn = column(n)
136
+ # blk.call(myColumn, n)
137
+ # end
138
+ #end
139
+
140
+ def dump
141
+ myStr = ""
142
+ rows { |row, idx|
143
+ myStr << "#{idx} || " << row.collect{|x| x.to_s}.join(" | ") << "\n"
144
+ }
145
+ myStr
146
+ end
147
+
148
+ alias :blade :column
149
+ alias :blades :columns
150
+ end
data/lib/klear/file.rb ADDED
@@ -0,0 +1,89 @@
1
+ require 'klear/frames'
2
+ require 'klear/motors'
3
+
4
+ class Klear::File
5
+ attr_accessor :path
6
+
7
+ def initialize path = nil
8
+ @path = path
9
+ end
10
+
11
+ def info
12
+ @info ||= YAML.load(zipfile.read('META-INF/kle.yml'))
13
+ end
14
+
15
+ def manifest
16
+ @manifest ||= YAML.load(zipfile.read('META-INF/MANIFEST.MF'))
17
+ end
18
+
19
+ def dimensions
20
+ @dimensions ||= info['geometry']
21
+ end
22
+
23
+ def frame_count
24
+ @frame_count ||= (zipfile.dir.entries("/frames").size)
25
+ end
26
+
27
+ def frames
28
+ @frames ||= (
29
+ data = zipfile.read("cache/frames.bin")
30
+ Klear::Frames.new(dimensions[:columns], dimensions[:rows], data)
31
+ )
32
+ end
33
+
34
+ def motors
35
+ @motors ||= (
36
+ #begin
37
+ # data = zipfile.read("cache/motors.bin")
38
+ # Klear::Motors.new(dimensions[:columns], data)
39
+ ##rescue Errno::ENOENT => e
40
+ # puts " ~~ empty motors cache (#{e})"
41
+ # Klear::Motors.new(dimensions[:columns])
42
+ #end
43
+
44
+ motors = frames.all.map do |frame|
45
+ (frame.row 0) # convention: row zero is the motors
46
+ end
47
+ Klear::Motors.new(dimensions[:columns], motors)
48
+
49
+ #animations = {}
50
+ #frames.each do |frame, no|
51
+ # puts "frame: ##{no}"
52
+ # row = (frame.row 0) # convention: row zero is the motors
53
+ #
54
+ # # scale input onto manta geometry
55
+ # low, high = @config[:low], @config[:high]
56
+ # d = (high - low).to_f
57
+ #
58
+ # row.each_with_index do |v_in, x|
59
+ # v_out = (low + d * (v_in.value.to_f / 0xffff)).to_i
60
+ # (animations[x+1] ||= []) << v_out
61
+ # end
62
+ # end
63
+ )
64
+ end
65
+
66
+ private
67
+ def zipfile
68
+ @zipfile ||= (
69
+ @path or (raise 'path not specified')
70
+ Zip::ZipFile.new(@path)
71
+ )
72
+ end
73
+ end
74
+
75
+ __END__
76
+
77
+ def self.load theKleFile
78
+ myChoreography = nil
79
+ Zip::ZipFile.open(theKleFile) { |kle|
80
+ myChoreography = new kle
81
+ }
82
+ myChoreography
83
+ end
84
+
85
+ def initialize theKleFile
86
+ @kle_file = theKleFile
87
+ @geometry = YAML.load(@kle_file.read("META-INF/kle.yml"))['geometry']
88
+ @frames = Kleac::Choreography::Frames.read @kle_file.read("cache/frames.bin")
89
+ end
@@ -0,0 +1,19 @@
1
+ module Klear::Filters
2
+ # scale input onto manta geometry
3
+ def project values, low, high
4
+ #low, high = @config[:low], @config[:high]
5
+ d = (high - low).to_f
6
+
7
+ values.map do |val|
8
+ (low + d * (val.to_f / 0xffff)).to_i
9
+ end
10
+ end
11
+
12
+ # hack: sample(F14 by JJ) 600 down to 2 times 20 => 40 frame positions
13
+ def f14jj values
14
+ no = -1
15
+ values.select { (no += 1) % 30 == 0 }
16
+ end
17
+
18
+ extend(self)
19
+ end
@@ -0,0 +1,71 @@
1
+ class Klear::Frame
2
+
3
+ attr_reader :data, :row_count, :column_count, :size
4
+
5
+ def initialize column_count, row_count, data = nil
6
+ @row_count, @column_count = row_count, column_count
7
+ @size = @row_count * @column_count
8
+ @data = data
9
+ end
10
+
11
+ def cell column, row
12
+ # TODO implement me
13
+ end
14
+
15
+ def row no
16
+ @data.slice(no * @column_count, @column_count)
17
+ end
18
+
19
+ #def rows *args, &blk
20
+ # if block_given?
21
+ # each_row(&blk)
22
+ # else
23
+ # myRows = {}
24
+ # (0...@row_count).each do |n|
25
+ # myRows[n] = row(n)
26
+ # end
27
+ # myRows
28
+ # end
29
+ #end
30
+
31
+ # returns array of rows
32
+ def each_row &blk
33
+ @rows ||= (0...@row_count).map {|n| row(n)}
34
+
35
+ if block_given?
36
+ @rows.each_with_index {|row, idx| blk.call(row, idx)}
37
+ end
38
+
39
+ @rows
40
+ end
41
+ alias_method :rows, :each_row
42
+
43
+ def column no
44
+ col = []
45
+ (0...@row_count).each do |curRow|
46
+ col << @data[no + curRow * @column_count]
47
+ end
48
+ col
49
+ end
50
+ alias_method :blade, :column
51
+
52
+ def each_column &blk
53
+ @columns ||= (0...@column_count).map {|n| column(n)}
54
+
55
+ if block_given?
56
+ @columns.each_with_index {|column, idx| blk.call(column, idx)}
57
+ end
58
+
59
+ @columns
60
+ end
61
+ alias_method :each_blade, :each_column
62
+ alias_method :columns, :each_column
63
+
64
+ #def dump
65
+ # myStr = ""
66
+ # rows do |row, idx|
67
+ # myStr << "#{idx} || " << row.collect{|x| x.to_s}.join(" | ") << "\n"
68
+ # end
69
+ # myStr
70
+ #end
71
+ end
@@ -0,0 +1,43 @@
1
+ require 'klear/frame'
2
+
3
+ class Klear::Frames
4
+ class Data < BinData::Record
5
+ array :numbers, :type => :uint16be, :read_until => :eof
6
+ end
7
+
8
+ attr_reader :framesize, :rows, :columns
9
+
10
+ def initialize columns, rows, binary_string = nil
11
+ @rows, @columns = rows, columns
12
+ @framesize = @rows * @columns
13
+ @data = nil
14
+ load(binary_string) unless binary_string.nil?
15
+ end
16
+
17
+ def load binary_string
18
+ @data = Data.read(binary_string).numbers
19
+ self
20
+ end
21
+
22
+ def count
23
+ @data.size / framesize
24
+ end
25
+
26
+ def each &blk
27
+ 0.upto(count-1) do |frame_no|
28
+ yield(get(frame_no), frame_no)
29
+ end
30
+ end
31
+
32
+ def get frame_no
33
+ (0 <= frame_no) or (raise "invalid negative frame no: #{frame_no}")
34
+ (frame_no < count) or (raise "frame no out of bound: #{frame_no}")
35
+ Klear::Frame.new(
36
+ @columns, @rows, @data.slice(frame_no * @framesize, @framesize)
37
+ )
38
+ end
39
+
40
+ def all
41
+ (0...count).map { |frame_no| get(frame_no) }
42
+ end
43
+ end
@@ -0,0 +1,53 @@
1
+ class Klear::Motors
2
+ #class Data < BinData::Record
3
+ # array :numbers, :type => :uint16be, :read_until => :eof
4
+ #end
5
+
6
+ attr_reader :count, :frame_count
7
+ alias :blade_count :count
8
+
9
+ def initialize blade_count, frames = nil
10
+ @count = blade_count
11
+ @frames = frames || [[0] * @count] # init with one frame
12
+ @frame_count ||= @frames.size
13
+ end
14
+
15
+ #def frame_count
16
+ #end
17
+
18
+ def each &blk
19
+ #0.upto(@frame_count - 1) do |frame_no|
20
+ (1..@count).each do |motor_no|
21
+ yield(get(motor_no), motor_no)
22
+ end
23
+ end
24
+
25
+ # motor_no is one-based!!!
26
+ def get motor_no
27
+ motor_no = Integer(motor_no)
28
+ (0 < motor_no) or (raise "invalid motor no: #{motor_no}")
29
+ (motor_no <= @count) or (raise "motor no out of bounds: #{motor_no}")
30
+ @frames.map {|frame| frame[motor_no-1]}
31
+ end
32
+
33
+ # frame_no is zero based!!!
34
+ def frame frame_no
35
+ (0 <= frame_no) or (raise "invalid negative frame no: #{frame_no}")
36
+ (frame_no < @frame_count) or (raise "frame no out of bound: #{frame_no}")
37
+ @frames[frame_no]
38
+ end
39
+
40
+ def render motor_no, options = {}
41
+ low = options[:low] || 0
42
+ high = options[:high] || 20000
43
+ values = get(motor_no)
44
+ values = f14jj(values)
45
+ values = project(values, low, high)
46
+ end
47
+
48
+ def render_all options = {}
49
+ low = options[:low] || 0
50
+ high = options[:high] || 20000
51
+ (1..@count).map {|i| render(i, low, high)}
52
+ end
53
+ end
data/lib/klear/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module Klear
2
- VERSION = '0.0.1'
3
- DATE = '2013-06-20'
2
+ VERSION = '0.1.0'
3
+ DATE = '2013-06-23'
4
4
  end
data/lib/klear.rb CHANGED
@@ -4,4 +4,7 @@ require 'yaml'
4
4
 
5
5
  module Klear; end
6
6
  require 'klear/version'
7
+ require 'klear/file'
8
+ require 'klear/filters'
9
+ require 'klear/choreography'
7
10
  require 'klear/file_generator'