cryptopunks-gui 0.0.10 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,187 @@
1
+ class CryptopunksGui
2
+ module Model
3
+ class Image
4
+ PALETTES = ['Standard'] + (Palette8bit.constants).map(&:to_s).map {|palette| palette.split('_').map(&:capitalize).join(' ')}.reject { |palette| palette.include?(' ') }.sort
5
+ STYLES = ['Normal', 'Led', 'Sketch']
6
+ OUTPUT_LOCATION_DEFAULT = File.join(Dir.home, 'cryptopunks')
7
+ CONFIG_FILE = File.join(OUTPUT_LOCATION_DEFAULT, 'cryptopunks.yml')
8
+ COLLECTIONS_YAML_PATH = File.join(OUTPUT_LOCATION_DEFAULT, 'cryptopunks-collections.yml')
9
+ COLLECTIONS_YAML_URL = 'https://raw.githubusercontent.com/cryptopunksnotdead/cryptopunks-gui/master/cryptopunks-collections.yml'
10
+ COLLECTIONS_YAML_REPO_PATH = File.expand_path('../../cryptopunks-collections.yml', __dir__)
11
+
12
+ attr_accessor :collection, :image_index, :zoom, :palette, :style, :led_spacing, :led_round_corner, :sketch_line, :flip, :mirror,
13
+ :collection_size, :collections_map, :images, :image_location, :output_location, :config
14
+
15
+ def initialize
16
+ initialize_output_location
17
+ initialize_collections_map
18
+ initialize_collection
19
+ load_config
20
+ initialize_defaults
21
+ observe_image_attribute_changes
22
+ end
23
+
24
+ def collection_options
25
+ @collections_map.keys.select {|collection_name| @collections_map[collection_name][:enabled]}
26
+ end
27
+
28
+ def palette_options
29
+ PALETTES
30
+ end
31
+
32
+ def style_options
33
+ STYLES
34
+ end
35
+
36
+ def initialize_output_location
37
+ @output_location = OUTPUT_LOCATION_DEFAULT
38
+ FileUtils.mkdir_p(@output_location)
39
+ end
40
+
41
+ def initialize_collections_map(reset: false)
42
+ FileUtils.touch(COLLECTIONS_YAML_PATH)
43
+ @collections_map = reset ? {} : (YAML.load(File.read(COLLECTIONS_YAML_PATH)) || {})
44
+ new_collections_map = {}
45
+ begin
46
+ http_response = Net::HTTP.get_response(URI(COLLECTIONS_YAML_URL))
47
+ if http_response.is_a?(Net::HTTPSuccess)
48
+ new_collections_map = YAML.load(http_response.body)
49
+ else
50
+ raise "code: #{http_response.code} message: #{http_response.message}"
51
+ end
52
+ rescue StandardError, SocketError => e
53
+ puts "Failed to utilize collection YAML from: #{COLLECTIONS_YAML_URL}"
54
+ puts e.full_message
55
+ puts "Utilizing local collection YAML instead: #{COLLECTIONS_YAML_REPO_PATH}"
56
+ new_collections_map = YAML.load(File.read(COLLECTIONS_YAML_REPO_PATH)) rescue {}
57
+ end
58
+ @collections_map_observers ||= {}
59
+ new_collections_map.each do |collection_name, collection_options|
60
+ @collections_map[collection_name] ||= {}
61
+ original_collections_map_for_collection = @collections_map[collection_name]
62
+ @collections_map[collection_name] = @collections_map[collection_name]
63
+ @collections_map[collection_name].reverse_merge!(collection_options)
64
+ @collections_map[collection_name][:enabled] = true unless @collections_map[collection_name].has_key?(:enabled)
65
+ @collections_map_observers[collection_name] ||= Glimmer::DataBinding::Observer.proc { |value, key|
66
+ if key == :enabled
67
+ self.collection = @collections_map.find { |name, options| options[:enabled] }.first if key == :enabled && value == false && @collection == collection_name
68
+ notify_observers(:collection_options)
69
+ end
70
+ save_collections_map
71
+ }.tap {|o| o.observe(original_collections_map_for_collection)}
72
+ end
73
+ @collections_map_observer ||= Glimmer::DataBinding::Observer.proc { |collection_options, collection_name|
74
+ if collection_options.nil?
75
+ self.collection = @collections_map.find { |name, options| options[:enabled] }.first if @collection == collection_name
76
+ self.collection = collection_name if @collections_map.select { |name, options| options[:enabled] }.count == 0
77
+ end
78
+ notify_observers(:collection_options)
79
+ save_collections_map
80
+ }.tap {|o| o.observe(@collections_map)}
81
+ save_collections_map
82
+ end
83
+
84
+ def save_collections_map
85
+ normalized_collections_map = Hash[@collections_map.map {|k,v| [k, v.to_h]}]
86
+ File.write(COLLECTIONS_YAML_PATH, YAML.dump(normalized_collections_map))
87
+ end
88
+
89
+ def initialize_collection
90
+ return if @collection && @collection == @last_collection
91
+ @collection ||= @collections_map.keys.first
92
+ url = @collections_map[@collection][:url]
93
+ width = @collections_map[@collection][:width]
94
+ height = @collections_map[@collection][:height]
95
+ @image_file = File.join(OUTPUT_LOCATION_DEFAULT, File.basename(url))
96
+ File.binwrite(@image_file, Net::HTTP.get(URI(url))) unless File.exist?(@image_file)
97
+ @images ||= {}
98
+ @images[@collection] ||= Punks::Image::Composite.read(@image_file, width: width, height: height)
99
+ @last_collection = @collection
100
+ self.image_index = 0
101
+ end
102
+
103
+ def load_config
104
+ FileUtils.touch(CONFIG_FILE)
105
+ @config = YAML.load(File.read(CONFIG_FILE)) || {}
106
+ @output_location = @config[:output_location]
107
+ @config[:output_location] = @output_location = OUTPUT_LOCATION_DEFAULT if @output_location.nil?
108
+ end
109
+
110
+ def save_config
111
+ File.write(CONFIG_FILE, YAML.dump(@config))
112
+ end
113
+
114
+ def initialize_defaults
115
+ @collection = @collections_map.keys.first
116
+ @zoom = 12
117
+ @palette = PALETTES.first
118
+ @style = STYLES.first
119
+ @led_spacing = 2
120
+ @led_round_corner = false
121
+ @sketch_line = 1
122
+ @mirror = false
123
+ @flip = false
124
+ end
125
+
126
+ def observe_image_attribute_changes
127
+ observer = Glimmer::DataBinding::Observer.proc { generate_image }
128
+ observer.observe(self, :collection)
129
+ observer.observe(self, :image_index)
130
+ observer.observe(self, :zoom)
131
+ observer.observe(self, :palette)
132
+ observer.observe(self, :style)
133
+ observer.observe(self, :led_spacing)
134
+ observer.observe(self, :led_round_corner)
135
+ observer.observe(self, :sketch_line)
136
+ observer.observe(self, :mirror)
137
+ observer.observe(self, :flip)
138
+ end
139
+
140
+ def generate_image
141
+ initialize_collection
142
+ return if @image_index.to_i >= @images[@collection].size
143
+ new_image_location = File.join(@output_location, "#{@collection.gsub(' ', '').downcase}-#{@image_index}#{"x#{@zoom}" if @zoom.to_i > 1}#{"-#{@palette.underscore}" if @palette != PALETTES.first}#{"-#{@style.underscore}" if @style != STYLES.first}#{"-spacing#{@led_spacing.to_i}" if @style == 'Led'}#{'-round-corner' if @style == 'Led' && @led_round_corner}#{"-line#{@sketch_line.to_i}" if @style == 'Sketch'}#{'-mirror' if @mirror}#{'-flip' if @flip}.png")
144
+ puts "Writing punk image to #{new_image_location}"
145
+ selected_punk = @images[@collection][@image_index.to_i]
146
+ selected_punk = selected_punk.change_palette8bit(Palette8bit.const_get(@palette.gsub(' ', '_').upcase.to_sym)) if @palette != PALETTES.first
147
+ @original_zoom = @zoom
148
+ if @previous_collection && @collection != @previous_collection && @collections_map[@collection][:width] != @collections_map[@previous_collection][:width]
149
+ @zoom = @collections_map[@collection][:default_zoom]
150
+ end
151
+ if @style != STYLES.first
152
+ style_options = {}
153
+ if @style == 'Led'
154
+ style_options[:spacing] = @led_spacing.to_i
155
+ style_options[:round_corner] = @led_round_corner
156
+ end
157
+ if @style == 'Sketch'
158
+ style_options[:line] = @sketch_line.to_i
159
+ end
160
+ selected_punk = selected_punk.send(@style.underscore, @zoom.to_i, **style_options)
161
+ end
162
+ selected_punk = selected_punk.mirror if @mirror
163
+ selected_punk = selected_punk.flip if @flip
164
+ selected_punk = selected_punk.zoom(@zoom.to_i) if @style == STYLES.first
165
+ selected_punk.save(new_image_location)
166
+ self.image_location = new_image_location
167
+ notify_observers(:zoom) if @zoom != @original_zoom
168
+ @previous_style = @style
169
+ @previous_collection = @collection
170
+ end
171
+
172
+ def change_output_location(new_output_location)
173
+ @output_location = new_output_location
174
+ @config[:output_location] = @output_location
175
+ save_config
176
+ generate_image
177
+ end
178
+
179
+ def reset_output_location
180
+ @output_location = OUTPUT_LOCATION_DEFAULT
181
+ @config[:output_location] = @output_location
182
+ save_config
183
+ generate_image
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,31 @@
1
+ class CryptopunksGui
2
+ module View
3
+ module HelpDialog
4
+ def help_dialog(root: )
5
+ toplevel(root) {
6
+ title 'CryptoPunks GUI - README.md'
7
+ width 800
8
+ height 600
9
+ escapable true
10
+
11
+ help_dialog = text {
12
+ grid row: 0, column: 0, row_weight: 1, column_weight: 1
13
+ value help
14
+ }
15
+
16
+ help_dialog_yscrollbar = scrollbar {
17
+ grid row: 0, column: 1
18
+ orient 'vertical'
19
+ }
20
+ help_dialog.yscrollbar help_dialog_yscrollbar.tk
21
+
22
+ help_dialog_xscrollbar = scrollbar {
23
+ grid row: 1, column: 0, column_span: 2
24
+ orient 'horizontal'
25
+ }
26
+ help_dialog.xscrollbar help_dialog_xscrollbar.tk
27
+ }
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,145 @@
1
+ class CryptopunksGui
2
+ module View
3
+ module ImageFrame
4
+ def image_frame(root: , image: )
5
+ image_label = output_location_entry = image_index_spinbox = style_options_container_frame = nil
6
+
7
+ frame {
8
+ label {
9
+ text 'Collection:'
10
+ }
11
+ combobox {
12
+ readonly true
13
+ values <= [image, :collection_options]
14
+ text <=> [image, :collection]
15
+ }
16
+
17
+ label {
18
+ text 'Image Index:'
19
+ }
20
+ image_index_spinbox = spinbox {
21
+ from 0
22
+ to image.images[image.collection].size - 1
23
+ text <=> [image, :image_index]
24
+ }
25
+
26
+ label {
27
+ text 'Zoom:'
28
+ }
29
+ spinbox {
30
+ from 1
31
+ to 72
32
+ text <=> [image, :zoom]
33
+ }
34
+
35
+ label {
36
+ text 'Palette:'
37
+ }
38
+ combobox {
39
+ readonly true
40
+ text <=> [image, :palette]
41
+ }
42
+
43
+ label {
44
+ text 'Style:'
45
+ }
46
+ combobox {
47
+ readonly true
48
+ text <=> [image, :style, after_write: ->(val) {add_style_options(style_options_container_frame: style_options_container_frame, image: image)}]
49
+ }
50
+
51
+ style_options_container_frame = frame {
52
+ padding 0
53
+ }
54
+
55
+ frame {
56
+ padding 0
57
+
58
+ checkbutton {
59
+ grid row: 0, column: 0, column_weight: 0
60
+ variable <=> [image, :mirror]
61
+ }
62
+ label {
63
+ grid row: 0, column: 1
64
+ text 'Mirror'
65
+
66
+ on('Button-1') do
67
+ image.mirror = !image.mirror
68
+ end
69
+ }
70
+
71
+ checkbutton {
72
+ grid row: 0, column: 2
73
+ variable <=> [image, :flip]
74
+ }
75
+ label {
76
+ grid row: 0, column: 3
77
+ text 'Flip'
78
+
79
+ on('Button-1') do
80
+ image.flip = !image.flip
81
+ end
82
+ }
83
+ }
84
+
85
+ label {
86
+ text 'Output Location:'
87
+ }
88
+ frame {
89
+ padding 0
90
+
91
+ output_location_entry = entry {
92
+ grid row: 0, column: 0
93
+ readonly true
94
+ width 47
95
+ }
96
+ button {
97
+ grid row: 0, column: 1
98
+ text '...'
99
+ width 1.1
100
+
101
+ on('command') do
102
+ change_output_location(root: root, image: image)
103
+ end
104
+ }
105
+ }
106
+
107
+ image_label = label {
108
+ grid row_weight: 1
109
+ }
110
+ }
111
+
112
+ register_image_frame_observers(image_label: image_label, output_location_entry: output_location_entry, image_index_spinbox: image_index_spinbox, image: image)
113
+ end
114
+
115
+ def register_image_frame_observers(image_label: , output_location_entry: , image_index_spinbox: , image: )
116
+ Glimmer::DataBinding::Observer.proc do
117
+ image_label.image = output_location_entry.text = image.image_location
118
+ end.observe(image, :image_location)
119
+
120
+ Glimmer::DataBinding::Observer.proc do
121
+ image_index_spinbox.to = image.images[image.collection].size - 1
122
+ end.observe(image, :collection)
123
+ end
124
+
125
+ def add_style_options(style_options_container_frame: , image: )
126
+ style_options_container_frame.children.each(&:destroy)
127
+ style_options_container_frame.content {
128
+ case image.style
129
+ when 'Led'
130
+ led_style_options_frame(image: image)
131
+ when 'Sketch'
132
+ sketch_style_options_frame(image: image)
133
+ else
134
+ no_style_options_frame
135
+ end
136
+ }
137
+ end
138
+
139
+ def change_output_location(root: , image: )
140
+ new_output_location = choose_directory(parent: root)
141
+ image.change_output_location(new_output_location) unless new_output_location.to_s.empty?
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,88 @@
1
+ # require_relative 'preferences_dialog'
2
+ require_relative 'help_dialog'
3
+
4
+ class CryptopunksGui
5
+ module View
6
+ module MenuBar
7
+ # include View::PreferencesDialog
8
+ include View::HelpDialog
9
+
10
+ LICENSE = File.expand_path('../../LICENSE.txt', __dir__)
11
+ HELP = File.expand_path('../../README.md', __dir__)
12
+
13
+ def cryptopunks_gui_menu_bar(root: , image: )
14
+ menu {
15
+ if OS.mac?
16
+ menu(:application) {
17
+ menu_item(:about, label: 'About') {
18
+ accelerator OS.mac? ? 'Command+Shift+A' : 'Control+Alt+A'
19
+
20
+ on('command') do
21
+ show_about_dialog(root: root)
22
+ end
23
+ }
24
+
25
+ # menu_item(:preferences) {
26
+ # on('command') do
27
+ # preferences_dialog(root: root, image: image)
28
+ # end
29
+ # }
30
+ }
31
+ end
32
+
33
+ menu(label: 'File', underline: 0) {
34
+ menu_item(label: 'Change Output Location...', underline: 7) {
35
+ accelerator OS.mac? ? 'Command+O' : 'Control+O'
36
+
37
+ on('command') do
38
+ change_output_location(root: root, image: image) # assumes View::ImageFrame is mixed in
39
+ end
40
+ }
41
+
42
+ menu_item(label: 'Reset Output Location', underline: 0) {
43
+ on('command') do
44
+ image.reset_output_location
45
+ end
46
+ }
47
+
48
+ menu_item(:separator)
49
+
50
+ # menu_item(label: 'Preferences...', underline: 0) {
51
+ # on('command') do
52
+ # preferences_dialog(root: root, image: image)
53
+ # end
54
+ # }
55
+
56
+ menu_item(:separator)
57
+
58
+ menu_item(label: 'Exit', underline: 1) {
59
+ on('command') do
60
+ exit(0)
61
+ end
62
+ }
63
+ }
64
+
65
+ menu(label: 'Help') {
66
+ menu_item(:help) {
67
+ on('command') do
68
+ help_dialog(root: root)
69
+ end
70
+ }
71
+ }
72
+ }
73
+ end
74
+
75
+ def show_about_dialog(root: )
76
+ message_box(parent: root, title: 'CryptoPunks GUI', message: "CryptoPunks GUI\n\n#{license}")
77
+ end
78
+
79
+ def license
80
+ @license ||= File.read(LICENSE)
81
+ end
82
+
83
+ def help
84
+ @help ||= File.read(HELP)
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,174 @@
1
+ class CryptopunksGui
2
+ module View
3
+ module PreferencesDialog
4
+ def preferences_dialog(root: , image: )
5
+ toplevel(root) {
6
+ title 'Preferences'
7
+ width 1280
8
+ height 700
9
+ escapable true
10
+
11
+ scrollbar_frame {
12
+ label {
13
+ grid row: 0, column: 0
14
+ text ''
15
+ }
16
+
17
+ label {
18
+ grid row: 0, column: 1
19
+ text 'Collection Name'
20
+ }
21
+
22
+ label {
23
+ grid row: 0, column: 2
24
+ text 'URL'
25
+ width 82
26
+ }
27
+
28
+ label {
29
+ grid row: 0, column: 3
30
+ text 'Width'
31
+ width 3
32
+ }
33
+
34
+ label {
35
+ grid row: 0, column: 4
36
+ text 'Height'
37
+ width 3
38
+ }
39
+
40
+ label {
41
+ grid row: 0, column: 5
42
+ text "Default\nZoom"
43
+ width 3
44
+ }
45
+
46
+ label {
47
+ grid row: 0, column: 6, column_span: 3
48
+ text 'Actions'
49
+ }
50
+
51
+ image.collections_map.each_with_index do |pair, index|
52
+ collection_name, collection_options = pair
53
+ row = index + 1
54
+
55
+ checkbutton { |cb|
56
+ grid row: row, column: 0
57
+ # TODO consider using hash_proxy object to bind more effectively
58
+ variable <= [image.collections_map[collection_name], :enabled]
59
+
60
+ on('command') do
61
+ collection_options[:enabled] = cb.variable
62
+ end
63
+ }
64
+
65
+ entry {
66
+ grid row: row, column: 1
67
+ text collection_name
68
+ }
69
+
70
+ entry {
71
+ grid row: row, column: 2
72
+ text collection_options[:url]
73
+ width 82
74
+ }
75
+
76
+ spinbox {
77
+ grid row: row, column: 3
78
+ from 1
79
+ to 512
80
+ text collection_options[:width]
81
+ width 3
82
+ }
83
+
84
+ spinbox {
85
+ grid row: row, column: 4
86
+ from 1
87
+ to 512
88
+ text collection_options[:height]
89
+ width 3
90
+ }
91
+
92
+ spinbox {
93
+ grid row: row, column: 5
94
+ from 1
95
+ to 72
96
+ text collection_options[:default_zoom]
97
+ width 3
98
+ }
99
+
100
+ button {
101
+ grid row: row, column: 6
102
+ text 'X'
103
+ width 1
104
+ }
105
+ button {
106
+ grid row: row, column: 7
107
+ text '^'
108
+ width 1
109
+ }
110
+ button {
111
+ grid row: row, column: 8
112
+ text 'v'
113
+ width 1
114
+ }
115
+ end
116
+
117
+ row = image.collections_map.keys.size + 1
118
+
119
+ checkbutton {
120
+ grid row: row, column: 0
121
+
122
+ }
123
+
124
+ entry {
125
+ grid row: row, column: 1
126
+ text ''
127
+ }
128
+
129
+ entry {
130
+ grid row: row, column: 2
131
+ text ''
132
+ width 82
133
+ }
134
+
135
+ spinbox {
136
+ grid row: row, column: 3
137
+ from 1
138
+ to 512
139
+ text '24'
140
+ width 3
141
+ }
142
+
143
+ spinbox {
144
+ grid row: row, column: 4
145
+ from 1
146
+ to 512
147
+ text '24'
148
+ width 3
149
+ }
150
+
151
+ spinbox {
152
+ grid row: row, column: 5
153
+ from 1
154
+ to 72
155
+ text '12'
156
+ width 3
157
+ }
158
+ }
159
+
160
+ frame {
161
+ button {
162
+ text 'Reset'
163
+
164
+ on('command') do
165
+ image.initialize_collections_map(reset: true)
166
+ # TODO cause an update to all collections
167
+ end
168
+ }
169
+ }
170
+ }
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,60 @@
1
+ class CryptopunksGui
2
+ module View
3
+ module StyleOptionsFrame
4
+ def led_style_options_frame(image: )
5
+ frame {
6
+ padding 0
7
+
8
+ label {
9
+ grid row: 0, column: 0, column_weight: 0
10
+ text 'Spacing:'
11
+ }
12
+ spinbox {
13
+ grid row: 0, column: 1
14
+ from 1
15
+ to 72
16
+ text <=> [image, :led_spacing]
17
+ }
18
+
19
+ checkbutton {
20
+ grid row: 0, column: 2
21
+ variable <=> [image, :led_round_corner]
22
+ }
23
+ label {
24
+ grid row: 0, column: 3
25
+ text 'Round Corner'
26
+
27
+ on('Button-1') do
28
+ image.led_round_corner = !image.led_round_corner
29
+ end
30
+ }
31
+ }
32
+ end
33
+
34
+ def sketch_style_options_frame(image: )
35
+ frame {
36
+ padding 0
37
+
38
+ label {
39
+ grid row: 0, column: 0, column_weight: 0
40
+ text 'Line:'
41
+ }
42
+ spinbox {
43
+ grid row: 0, column: 1
44
+ from 1
45
+ to 72
46
+ text <=> [image, :sketch_line]
47
+ }
48
+ }
49
+ end
50
+
51
+ def no_style_options_frame
52
+ frame { # filler
53
+ padding 0
54
+ height 0
55
+ width 0
56
+ }
57
+ end
58
+ end
59
+ end
60
+ end
data/bin/cryptopunks-gui CHANGED
File without changes