teek 0.1.3 → 0.1.4
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.
- checksums.yaml +4 -4
- data/README.md +21 -0
- data/Rakefile +120 -22
- data/ext/teek/extconf.rb +19 -1
- data/ext/teek/tcltkbridge.c +38 -2
- data/ext/teek/tcltkbridge.h +3 -0
- data/ext/teek/tkdrop.c +66 -0
- data/ext/teek/tkdrop.h +26 -0
- data/ext/teek/tkdrop_macos.m +141 -0
- data/ext/teek/tkdrop_win.c +232 -0
- data/ext/teek/tkdrop_x11.c +337 -0
- data/ext/teek/tkwin.c +42 -0
- data/lib/teek/platform.rb +29 -0
- data/lib/teek/version.rb +1 -1
- data/lib/teek.rb +49 -3
- data/teek.gemspec +3 -2
- metadata +7 -53
- data/sample/calculator.rb +0 -255
- data/sample/debug_demo.rb +0 -43
- data/sample/gamepad_viewer/assets/controller.png +0 -0
- data/sample/gamepad_viewer/gamepad_viewer.rb +0 -554
- data/sample/goldberg.rb +0 -1803
- data/sample/goldberg_helpers.rb +0 -170
- data/sample/optcarrot/thwaite.nes +0 -0
- data/sample/optcarrot/vendor/optcarrot/apu.rb +0 -856
- data/sample/optcarrot/vendor/optcarrot/config.rb +0 -257
- data/sample/optcarrot/vendor/optcarrot/cpu.rb +0 -1162
- data/sample/optcarrot/vendor/optcarrot/driver.rb +0 -144
- data/sample/optcarrot/vendor/optcarrot/mapper/cnrom.rb +0 -14
- data/sample/optcarrot/vendor/optcarrot/mapper/mmc1.rb +0 -105
- data/sample/optcarrot/vendor/optcarrot/mapper/mmc3.rb +0 -153
- data/sample/optcarrot/vendor/optcarrot/mapper/uxrom.rb +0 -14
- data/sample/optcarrot/vendor/optcarrot/nes.rb +0 -105
- data/sample/optcarrot/vendor/optcarrot/opt.rb +0 -168
- data/sample/optcarrot/vendor/optcarrot/pad.rb +0 -92
- data/sample/optcarrot/vendor/optcarrot/palette.rb +0 -65
- data/sample/optcarrot/vendor/optcarrot/ppu.rb +0 -1468
- data/sample/optcarrot/vendor/optcarrot/rom.rb +0 -143
- data/sample/optcarrot/vendor/optcarrot.rb +0 -14
- data/sample/optcarrot.rb +0 -354
- data/sample/paint/assets/bucket.png +0 -0
- data/sample/paint/assets/cursor.png +0 -0
- data/sample/paint/assets/eraser.png +0 -0
- data/sample/paint/assets/pencil.png +0 -0
- data/sample/paint/assets/spray.png +0 -0
- data/sample/paint/layer.rb +0 -255
- data/sample/paint/layer_manager.rb +0 -179
- data/sample/paint/paint_demo.rb +0 -837
- data/sample/paint/sparse_pixel_buffer.rb +0 -202
- data/sample/sdl2_demo.rb +0 -318
- data/sample/threading_demo.rb +0 -494
- data/sample/yam/assets/MINESWEEPER_0.png +0 -0
- data/sample/yam/assets/MINESWEEPER_1.png +0 -0
- data/sample/yam/assets/MINESWEEPER_2.png +0 -0
- data/sample/yam/assets/MINESWEEPER_3.png +0 -0
- data/sample/yam/assets/MINESWEEPER_4.png +0 -0
- data/sample/yam/assets/MINESWEEPER_5.png +0 -0
- data/sample/yam/assets/MINESWEEPER_6.png +0 -0
- data/sample/yam/assets/MINESWEEPER_7.png +0 -0
- data/sample/yam/assets/MINESWEEPER_8.png +0 -0
- data/sample/yam/assets/MINESWEEPER_F.png +0 -0
- data/sample/yam/assets/MINESWEEPER_M.png +0 -0
- data/sample/yam/assets/MINESWEEPER_X.png +0 -0
- data/sample/yam/assets/click.wav +0 -0
- data/sample/yam/assets/explosion.wav +0 -0
- data/sample/yam/assets/flag.wav +0 -0
- data/sample/yam/assets/music.mp3 +0 -0
- data/sample/yam/assets/sweep.wav +0 -0
- data/sample/yam/yam.rb +0 -587
data/sample/paint/paint_demo.rb
DELETED
|
@@ -1,837 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# teek-record: title=Paint Demo
|
|
3
|
-
# frozen_string_literal: true
|
|
4
|
-
|
|
5
|
-
# Paint Demo - Simple MS Paint-style drawing application
|
|
6
|
-
#
|
|
7
|
-
# Demonstrates what's possible with Teek beyond "hello world":
|
|
8
|
-
# - Teek::Photo for fast CPU-side pixel manipulation (flood fill, spray paint)
|
|
9
|
-
# - Canvas for vector drawing (strokes, shapes)
|
|
10
|
-
# - Layers with sparse pixel storage and photo image backing
|
|
11
|
-
# - Multi-window UI (tools palette, color palette, main canvas)
|
|
12
|
-
# - Undo/redo system
|
|
13
|
-
# - Menu bars and keyboard shortcuts
|
|
14
|
-
#
|
|
15
|
-
# This is NOT meant to be a production paint app -- it's a showcase of
|
|
16
|
-
# Teek's capabilities for anyone wondering "what can I actually build?"
|
|
17
|
-
#
|
|
18
|
-
# Tool icons in assets/ from Lucide (https://lucide.dev, MIT license)
|
|
19
|
-
# and Iconoir (https://iconoir.com, MIT license).
|
|
20
|
-
|
|
21
|
-
require_relative '../../lib/teek'
|
|
22
|
-
require_relative 'layer_manager'
|
|
23
|
-
|
|
24
|
-
class PaintDemo
|
|
25
|
-
# Classic 16-color palette (Windows/VGA style)
|
|
26
|
-
COLORS = [
|
|
27
|
-
'#000000', '#808080', '#800000', '#808000',
|
|
28
|
-
'#008000', '#008080', '#000080', '#800080',
|
|
29
|
-
'#FFFFFF', '#C0C0C0', '#FF0000', '#FFFF00',
|
|
30
|
-
'#00FF00', '#00FFFF', '#0000FF', '#FF00FF'
|
|
31
|
-
].freeze
|
|
32
|
-
|
|
33
|
-
MAX_UNDO = 10
|
|
34
|
-
|
|
35
|
-
PHOTO_WIDTH = 800
|
|
36
|
-
PHOTO_HEIGHT = 600
|
|
37
|
-
ASSETS_DIR = File.join(__dir__, 'assets').freeze
|
|
38
|
-
|
|
39
|
-
def initialize(app)
|
|
40
|
-
@app = app
|
|
41
|
-
@brush_color = '#000000'
|
|
42
|
-
@bg_color_hex = '#FFFFFF'
|
|
43
|
-
@brush_size = 1
|
|
44
|
-
@spray_density = 3
|
|
45
|
-
@canvas_width = PHOTO_WIDTH
|
|
46
|
-
@canvas_height = PHOTO_HEIGHT
|
|
47
|
-
@last_x = nil
|
|
48
|
-
@last_y = nil
|
|
49
|
-
|
|
50
|
-
# Undo/redo stacks
|
|
51
|
-
@undo_stack = []
|
|
52
|
-
@redo_stack = []
|
|
53
|
-
@current_stroke_items = []
|
|
54
|
-
|
|
55
|
-
# Layer manager (created after canvas in setup_main_window)
|
|
56
|
-
@layers = nil
|
|
57
|
-
|
|
58
|
-
setup_main_window
|
|
59
|
-
setup_tools_window
|
|
60
|
-
setup_palette_window
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def setup_main_window
|
|
64
|
-
@app.set_window_title('Paint')
|
|
65
|
-
@app.set_window_geometry("#{PHOTO_WIDTH}x#{PHOTO_HEIGHT + 40}")
|
|
66
|
-
|
|
67
|
-
# Menu bar
|
|
68
|
-
@app.command(:menu, '.menubar')
|
|
69
|
-
@app.command('.', :configure, menu: '.menubar')
|
|
70
|
-
create_edit_menu('.menubar')
|
|
71
|
-
create_layer_menu('.menubar')
|
|
72
|
-
create_window_menu('.menubar')
|
|
73
|
-
|
|
74
|
-
# Status bar (packed first so canvas gets remaining space)
|
|
75
|
-
status_frame = @app.create_widget('ttk::frame')
|
|
76
|
-
status_frame.pack(side: :bottom, fill: :x)
|
|
77
|
-
|
|
78
|
-
# Canvas fills the rest of the window
|
|
79
|
-
@canvas = @app.create_widget(:canvas, background: :gray, cursor: :crosshair)
|
|
80
|
-
@canvas.pack(fill: :both, expand: true)
|
|
81
|
-
|
|
82
|
-
# Layer manager handles photo images and pixel buffers
|
|
83
|
-
@layers = LayerManager.new(@app, @canvas, PHOTO_WIDTH, PHOTO_HEIGHT)
|
|
84
|
-
@layers.active_layer.ensure_photo!
|
|
85
|
-
@layers.active_layer.refresh_display
|
|
86
|
-
|
|
87
|
-
# Drawing bindings
|
|
88
|
-
@canvas.bind('ButtonPress-1', :x, :y) { |x, y| start_stroke(x.to_i, y.to_i) }
|
|
89
|
-
@canvas.bind('B1-Motion', :x, :y) { |x, y| continue_stroke(x.to_i, y.to_i) }
|
|
90
|
-
@canvas.bind('ButtonRelease-1') { end_stroke }
|
|
91
|
-
|
|
92
|
-
# Keyboard shortcuts
|
|
93
|
-
@app.bind('.', 'c') { clear_active_layer }
|
|
94
|
-
@app.bind('.', 'Escape') { @app.destroy('.') }
|
|
95
|
-
@app.bind('.', 'Control-z') { undo }
|
|
96
|
-
@app.bind('.', 'Control-Z') { redo_action }
|
|
97
|
-
@app.bind('.', 'Control-y') { redo_action }
|
|
98
|
-
|
|
99
|
-
# Tool shortcuts
|
|
100
|
-
@app.bind('.', 'b') { select_tool(:brush) }
|
|
101
|
-
@app.bind('.', 'e') { select_tool(:eraser) }
|
|
102
|
-
@app.bind('.', 'g') { select_tool(:bucket) }
|
|
103
|
-
@app.bind('.', 's') { select_tool(:spray) }
|
|
104
|
-
|
|
105
|
-
# Layer shortcuts
|
|
106
|
-
@app.bind('.', 'Control-N') { add_layer }
|
|
107
|
-
@app.bind('.', 'Control-period') { toggle_layer_visibility }
|
|
108
|
-
(1..9).each do |n|
|
|
109
|
-
@app.bind('.', "Key-#{n}") { select_layer_by_number(n - 1) }
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
@color_indicator = @app.create_widget(:canvas, parent: status_frame,
|
|
113
|
-
width: 20, height: 20, highlightthickness: 1)
|
|
114
|
-
@color_indicator.pack(side: :left, padx: 5, pady: 3)
|
|
115
|
-
update_color_indicator
|
|
116
|
-
|
|
117
|
-
@layer_var = 'paint_layer_info'
|
|
118
|
-
@app.set_variable(@layer_var, '[0] Background')
|
|
119
|
-
@app.create_widget('ttk::label', parent: status_frame,
|
|
120
|
-
textvariable: @layer_var, width: 20).pack(side: :left, padx: 5)
|
|
121
|
-
|
|
122
|
-
# Brush size control
|
|
123
|
-
@app.create_widget('ttk::label', parent: status_frame,
|
|
124
|
-
text: 'Size:').pack(side: :left, padx: 5)
|
|
125
|
-
@brush_size_var = 'paint_brush_size'
|
|
126
|
-
@app.set_variable(@brush_size_var, @brush_size.to_s)
|
|
127
|
-
size_spinbox = @app.create_widget('ttk::spinbox', parent: status_frame,
|
|
128
|
-
from: 1, to: 10, width: 3,
|
|
129
|
-
textvariable: @brush_size_var,
|
|
130
|
-
command: proc { update_brush_size })
|
|
131
|
-
size_spinbox.pack(side: :left)
|
|
132
|
-
size_spinbox.bind('KeyRelease') { update_brush_size }
|
|
133
|
-
|
|
134
|
-
# Spray density control (only visible when spray tool selected)
|
|
135
|
-
@density_label = @app.create_widget('ttk::label', parent: status_frame,
|
|
136
|
-
text: 'Density:')
|
|
137
|
-
@spray_density_var = 'paint_spray_density'
|
|
138
|
-
@app.set_variable(@spray_density_var, @spray_density.to_s)
|
|
139
|
-
@density_spinbox = @app.create_widget('ttk::spinbox', parent: status_frame,
|
|
140
|
-
from: 1, to: 20, width: 3,
|
|
141
|
-
textvariable: @spray_density_var,
|
|
142
|
-
command: proc { update_spray_density })
|
|
143
|
-
@density_spinbox.bind('KeyRelease') { update_spray_density }
|
|
144
|
-
# Hidden by default (shown when spray tool is selected)
|
|
145
|
-
|
|
146
|
-
@coords_var = 'paint_coords'
|
|
147
|
-
@app.set_variable(@coords_var, '0, 0')
|
|
148
|
-
@app.create_widget('ttk::label', parent: status_frame,
|
|
149
|
-
textvariable: @coords_var, width: 12).pack(side: :left, padx: 10)
|
|
150
|
-
|
|
151
|
-
@app.create_widget('ttk::label', parent: status_frame,
|
|
152
|
-
text: "Ruby #{RUBY_VERSION}").pack(side: :right, padx: 10)
|
|
153
|
-
|
|
154
|
-
# Track mouse position
|
|
155
|
-
@last_coords_update = 0
|
|
156
|
-
@canvas.bind('Motion', :x, :y) do |x, y|
|
|
157
|
-
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
158
|
-
if (now - @last_coords_update) >= 0.005
|
|
159
|
-
@app.set_variable(@coords_var, "#{x}, #{y}")
|
|
160
|
-
@last_coords_update = now
|
|
161
|
-
end
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
# Resize layers when canvas resizes
|
|
165
|
-
@canvas.bind('Configure', :width, :height) do |w, h|
|
|
166
|
-
new_w = w.to_i
|
|
167
|
-
new_h = h.to_i
|
|
168
|
-
if new_w > 0 && new_h > 0 && (new_w != @canvas_width || new_h != @canvas_height)
|
|
169
|
-
@canvas_width = new_w
|
|
170
|
-
@canvas_height = new_h
|
|
171
|
-
@layers.resize(new_w, new_h)
|
|
172
|
-
end
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
update_title
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
def setup_tools_window
|
|
179
|
-
@tools_path = '.tools'
|
|
180
|
-
@app.command(:toplevel, @tools_path)
|
|
181
|
-
@app.command(:wm, :title, @tools_path, 'Tools')
|
|
182
|
-
@app.command(:wm, :geometry, @tools_path, '50x200+910+300')
|
|
183
|
-
@app.command(:wm, :resizable, @tools_path, 0, 0)
|
|
184
|
-
|
|
185
|
-
@current_tool = :brush
|
|
186
|
-
|
|
187
|
-
# Load PNG icons from assets
|
|
188
|
-
@tool_icons = {}
|
|
189
|
-
{ brush: 'pencil', eraser: 'eraser', bucket: 'bucket', spray: 'spray' }.each do |tool, file|
|
|
190
|
-
path = File.join(ASSETS_DIR, "#{file}.png")
|
|
191
|
-
@tool_icons[tool] = Teek::Photo.new(@app, file: path)
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
tool_defs = [
|
|
195
|
-
[:brush, 'Brush (B)'],
|
|
196
|
-
[:eraser, 'Eraser (E)'],
|
|
197
|
-
[:bucket, 'Fill (G)'],
|
|
198
|
-
[:spray, 'Spray (S)']
|
|
199
|
-
]
|
|
200
|
-
|
|
201
|
-
@tool_buttons = {}
|
|
202
|
-
tool_defs.each do |tool, tip|
|
|
203
|
-
btn = @app.create_widget(:canvas, "#{@tools_path}.#{tool}",
|
|
204
|
-
width: 36, height: 36, background: :white,
|
|
205
|
-
highlightthickness: 2, highlightbackground: :gray)
|
|
206
|
-
btn.pack(padx: 4, pady: 4)
|
|
207
|
-
@app.command(btn, :create, :image, 18, 18,
|
|
208
|
-
image: @tool_icons[tool].name, anchor: :center)
|
|
209
|
-
btn.bind('ButtonPress-1') { select_tool(tool) }
|
|
210
|
-
add_tooltip(btn, tip)
|
|
211
|
-
@tool_buttons[tool] = btn
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
select_tool(:brush)
|
|
215
|
-
|
|
216
|
-
@app.command(:wm, :protocol, @tools_path, 'WM_DELETE_WINDOW',
|
|
217
|
-
proc { @app.command(:wm, :withdraw, @tools_path) })
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
def setup_palette_window
|
|
221
|
-
@palette_path = '.palette'
|
|
222
|
-
@app.command(:toplevel, @palette_path)
|
|
223
|
-
@app.command(:wm, :title, @palette_path, 'Colors')
|
|
224
|
-
@app.command(:wm, :geometry, @palette_path, '170x160+910+100')
|
|
225
|
-
@app.command(:wm, :resizable, @palette_path, 0, 0)
|
|
226
|
-
|
|
227
|
-
# Grid of color buttons (4x4)
|
|
228
|
-
COLORS.each_with_index do |color, i|
|
|
229
|
-
row = i / 4
|
|
230
|
-
col = i % 4
|
|
231
|
-
|
|
232
|
-
btn = @app.create_widget(:canvas, parent: @palette_path,
|
|
233
|
-
width: 32, height: 32, background: color,
|
|
234
|
-
highlightthickness: 2, highlightbackground: :gray)
|
|
235
|
-
btn.grid(row: row, column: col, padx: 2, pady: 2)
|
|
236
|
-
btn.bind('ButtonPress-1') { select_color(color) }
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
@app.command(:wm, :protocol, @palette_path, 'WM_DELETE_WINDOW',
|
|
240
|
-
proc { @app.command(:wm, :withdraw, @palette_path) })
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
def create_edit_menu(menubar)
|
|
244
|
-
@app.command(:menu, "#{menubar}.edit", tearoff: 0)
|
|
245
|
-
@app.command(menubar, :add, :cascade, label: 'Edit', menu: "#{menubar}.edit")
|
|
246
|
-
@app.command("#{menubar}.edit", :add, :command,
|
|
247
|
-
label: 'Undo', accelerator: 'Ctrl+Z', command: proc { undo })
|
|
248
|
-
@app.command("#{menubar}.edit", :add, :command,
|
|
249
|
-
label: 'Redo', accelerator: 'Ctrl+Shift+Z', command: proc { redo_action })
|
|
250
|
-
@app.command("#{menubar}.edit", :add, :separator)
|
|
251
|
-
@app.command("#{menubar}.edit", :add, :command,
|
|
252
|
-
label: 'Clear Layer', command: proc { clear_active_layer })
|
|
253
|
-
@app.command("#{menubar}.edit", :add, :command,
|
|
254
|
-
label: 'Clear All Layers', command: proc { clear_canvas })
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
def create_layer_menu(menubar)
|
|
258
|
-
@app.command(:menu, "#{menubar}.layer", tearoff: 0)
|
|
259
|
-
@app.command(menubar, :add, :cascade, label: 'Layer', menu: "#{menubar}.layer")
|
|
260
|
-
@app.command("#{menubar}.layer", :add, :command,
|
|
261
|
-
label: 'Add Layer', command: proc { add_layer })
|
|
262
|
-
@app.command("#{menubar}.layer", :add, :command,
|
|
263
|
-
label: 'Delete Layer', command: proc { delete_layer })
|
|
264
|
-
@app.command("#{menubar}.layer", :add, :separator)
|
|
265
|
-
@app.command("#{menubar}.layer", :add, :command,
|
|
266
|
-
label: 'Toggle Visibility', command: proc { toggle_layer_visibility })
|
|
267
|
-
@app.command("#{menubar}.layer", :add, :separator)
|
|
268
|
-
@app.command("#{menubar}.layer", :add, :command,
|
|
269
|
-
label: 'Flatten All', command: proc { flatten_layers })
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
def create_window_menu(menubar)
|
|
273
|
-
@app.command(:menu, "#{menubar}.window", tearoff: 0)
|
|
274
|
-
@app.command(menubar, :add, :cascade, label: 'Window', menu: "#{menubar}.window")
|
|
275
|
-
@app.command("#{menubar}.window", :add, :command,
|
|
276
|
-
label: 'Show Tools', command: proc { @app.command(:wm, :deiconify, @tools_path) })
|
|
277
|
-
@app.command("#{menubar}.window", :add, :command,
|
|
278
|
-
label: 'Show Colors', command: proc { @app.command(:wm, :deiconify, @palette_path) })
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
def add_tooltip(widget, text)
|
|
282
|
-
widget.bind('Enter') do
|
|
283
|
-
@app.tcl_eval('catch {destroy .tooltip}')
|
|
284
|
-
@app.command(:toplevel, '.tooltip', background: '#FFFFE0')
|
|
285
|
-
@app.command(:wm, :overrideredirect, '.tooltip', 1)
|
|
286
|
-
@app.tcl_eval('catch {wm attributes .tooltip -type tooltip}')
|
|
287
|
-
@app.tcl_eval('catch {wm attributes .tooltip -transparent true}')
|
|
288
|
-
x = @app.tcl_eval('winfo pointerx .').to_i + 15
|
|
289
|
-
y = @app.tcl_eval('winfo pointery .').to_i + 10
|
|
290
|
-
@app.command(:wm, :geometry, '.tooltip', "+#{x}+#{y}")
|
|
291
|
-
@app.create_widget(:frame, '.tooltip.f',
|
|
292
|
-
background: '#FFFFE0', relief: :solid,
|
|
293
|
-
borderwidth: 1).pack(fill: :both, expand: true)
|
|
294
|
-
@app.create_widget(:label, '.tooltip.f.l', text: text,
|
|
295
|
-
background: '#FFFFE0', foreground: '#000000',
|
|
296
|
-
padx: 4, pady: 2).pack
|
|
297
|
-
end
|
|
298
|
-
widget.bind('Leave') do
|
|
299
|
-
@app.tcl_eval('catch {destroy .tooltip}')
|
|
300
|
-
end
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
def select_tool(tool)
|
|
304
|
-
@current_tool = tool
|
|
305
|
-
@tool_buttons.each do |_name, btn|
|
|
306
|
-
btn.command(:configure, background: :white, highlightbackground: :gray, highlightthickness: 2)
|
|
307
|
-
end
|
|
308
|
-
@tool_buttons[tool]&.command(:configure, background: '#ADD8E6',
|
|
309
|
-
highlightbackground: :black, highlightthickness: 3)
|
|
310
|
-
|
|
311
|
-
cursor = case tool
|
|
312
|
-
when :brush then :crosshair
|
|
313
|
-
when :eraser then :dotbox
|
|
314
|
-
when :bucket then :target
|
|
315
|
-
when :spray then :spraycan
|
|
316
|
-
else :crosshair
|
|
317
|
-
end
|
|
318
|
-
@canvas.command(:configure, cursor: cursor)
|
|
319
|
-
|
|
320
|
-
# Show/hide spray density control
|
|
321
|
-
if tool == :spray
|
|
322
|
-
@density_label.pack(side: :left, padx: 5)
|
|
323
|
-
@density_spinbox.pack(side: :left)
|
|
324
|
-
else
|
|
325
|
-
@app.command(:pack, :forget, @density_label) rescue nil
|
|
326
|
-
@app.command(:pack, :forget, @density_spinbox) rescue nil
|
|
327
|
-
end
|
|
328
|
-
end
|
|
329
|
-
|
|
330
|
-
def select_color(color)
|
|
331
|
-
@brush_color = color
|
|
332
|
-
update_color_indicator
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
def update_color_indicator
|
|
336
|
-
@color_indicator.command(:configure, background: @brush_color)
|
|
337
|
-
end
|
|
338
|
-
|
|
339
|
-
def update_brush_size
|
|
340
|
-
size = @app.get_variable(@brush_size_var).to_i
|
|
341
|
-
size = 1 if size < 1
|
|
342
|
-
size = 10 if size > 10
|
|
343
|
-
@brush_size = size
|
|
344
|
-
end
|
|
345
|
-
|
|
346
|
-
def update_spray_density
|
|
347
|
-
d = @app.get_variable(@spray_density_var).to_i
|
|
348
|
-
d = 1 if d < 1
|
|
349
|
-
d = 20 if d > 20
|
|
350
|
-
@spray_density = d
|
|
351
|
-
end
|
|
352
|
-
|
|
353
|
-
# -- Drawing operations ---------------------------------------------------
|
|
354
|
-
|
|
355
|
-
def start_stroke(x, y)
|
|
356
|
-
if @current_tool == :bucket
|
|
357
|
-
flood_fill(x, y)
|
|
358
|
-
return
|
|
359
|
-
end
|
|
360
|
-
|
|
361
|
-
if @current_tool == :spray
|
|
362
|
-
layer = @layers.active_layer
|
|
363
|
-
@spray_old_pixels = layer.snapshot_pixels
|
|
364
|
-
spray_paint(x, y)
|
|
365
|
-
return
|
|
366
|
-
end
|
|
367
|
-
|
|
368
|
-
return unless @current_tool == :brush || @current_tool == :eraser
|
|
369
|
-
@current_stroke_items = []
|
|
370
|
-
@last_x = x
|
|
371
|
-
@last_y = y
|
|
372
|
-
draw_point(x, y)
|
|
373
|
-
end
|
|
374
|
-
|
|
375
|
-
def continue_stroke(x, y)
|
|
376
|
-
if @current_tool == :spray
|
|
377
|
-
spray_paint(x, y)
|
|
378
|
-
return
|
|
379
|
-
end
|
|
380
|
-
|
|
381
|
-
return unless @current_tool == :brush || @current_tool == :eraser
|
|
382
|
-
return unless @last_x && @last_y
|
|
383
|
-
|
|
384
|
-
color = @current_tool == :eraser ? @bg_color_hex : @brush_color
|
|
385
|
-
size = @current_tool == :eraser ? @brush_size * 3 : @brush_size
|
|
386
|
-
|
|
387
|
-
item = @app.command(@canvas, :create, :line, @last_x, @last_y, x, y,
|
|
388
|
-
fill: color, width: size, capstyle: :round, joinstyle: :round)
|
|
389
|
-
@current_stroke_items << item
|
|
390
|
-
|
|
391
|
-
@last_x = x
|
|
392
|
-
@last_y = y
|
|
393
|
-
end
|
|
394
|
-
|
|
395
|
-
def end_stroke
|
|
396
|
-
if @current_tool == :spray
|
|
397
|
-
layer = @layers.active_layer
|
|
398
|
-
if @spray_old_pixels
|
|
399
|
-
push_undo(LayerPixelsCommand.new(layer, @spray_old_pixels, layer.snapshot_pixels))
|
|
400
|
-
end
|
|
401
|
-
@spray_old_pixels = nil
|
|
402
|
-
return
|
|
403
|
-
end
|
|
404
|
-
|
|
405
|
-
if @current_stroke_items && @current_stroke_items.any?
|
|
406
|
-
push_undo(StrokeCommand.new(@app, @canvas, @current_stroke_items.dup))
|
|
407
|
-
end
|
|
408
|
-
@current_stroke_items = []
|
|
409
|
-
@last_x = nil
|
|
410
|
-
@last_y = nil
|
|
411
|
-
end
|
|
412
|
-
|
|
413
|
-
def draw_point(x, y)
|
|
414
|
-
color = @current_tool == :eraser ? @bg_color_hex : @brush_color
|
|
415
|
-
size = @current_tool == :eraser ? @brush_size * 3 : @brush_size
|
|
416
|
-
r = size / 2.0
|
|
417
|
-
item = @app.command(@canvas, :create, :oval, x - r, y - r, x + r, y + r,
|
|
418
|
-
fill: color, outline: color)
|
|
419
|
-
@current_stroke_items << item if @current_stroke_items
|
|
420
|
-
end
|
|
421
|
-
|
|
422
|
-
# -- Layer operations -----------------------------------------------------
|
|
423
|
-
|
|
424
|
-
def clear_canvas
|
|
425
|
-
@layers.clear_all
|
|
426
|
-
@layers.refresh_all
|
|
427
|
-
end
|
|
428
|
-
|
|
429
|
-
def clear_active_layer
|
|
430
|
-
layer = @layers.active_layer
|
|
431
|
-
return unless layer
|
|
432
|
-
layer.clear
|
|
433
|
-
layer.refresh_display
|
|
434
|
-
end
|
|
435
|
-
|
|
436
|
-
def add_layer
|
|
437
|
-
@layers.add_layer
|
|
438
|
-
update_title
|
|
439
|
-
end
|
|
440
|
-
|
|
441
|
-
def delete_layer
|
|
442
|
-
return if @layers.layers.size <= 1
|
|
443
|
-
@layers.remove_layer(@layers.active_index)
|
|
444
|
-
@layers.refresh_all
|
|
445
|
-
update_title
|
|
446
|
-
end
|
|
447
|
-
|
|
448
|
-
def toggle_layer_visibility
|
|
449
|
-
layer = @layers.active_layer
|
|
450
|
-
return unless layer
|
|
451
|
-
layer.toggle_visibility
|
|
452
|
-
end
|
|
453
|
-
|
|
454
|
-
def flatten_layers
|
|
455
|
-
@layers.flatten
|
|
456
|
-
update_title
|
|
457
|
-
end
|
|
458
|
-
|
|
459
|
-
def update_title
|
|
460
|
-
layer = @layers.active_layer
|
|
461
|
-
layer_info = layer ? "[#{@layers.active_index}] #{layer.name}" : ""
|
|
462
|
-
@app.set_window_title("Paint - #{layer_info}")
|
|
463
|
-
@app.set_variable(@layer_var, layer_info) if @layer_var
|
|
464
|
-
end
|
|
465
|
-
|
|
466
|
-
def select_layer_by_number(index)
|
|
467
|
-
return unless index >= 0 && index < @layers.layers.size
|
|
468
|
-
@layers.active_index = index
|
|
469
|
-
update_title
|
|
470
|
-
end
|
|
471
|
-
|
|
472
|
-
# -- Pixel operations -----------------------------------------------------
|
|
473
|
-
|
|
474
|
-
def get_pixel(x, y)
|
|
475
|
-
@layers.active_layer&.get_rgba(x, y)
|
|
476
|
-
end
|
|
477
|
-
|
|
478
|
-
def set_pixel(x, y, rgba)
|
|
479
|
-
layer = @layers.active_layer
|
|
480
|
-
return unless layer
|
|
481
|
-
layer.set_rgba(x, y, *rgba)
|
|
482
|
-
end
|
|
483
|
-
|
|
484
|
-
def parse_hex_color(hex)
|
|
485
|
-
hex = hex.delete('#')
|
|
486
|
-
r = hex[0, 2].to_i(16)
|
|
487
|
-
g = hex[2, 2].to_i(16)
|
|
488
|
-
b = hex[4, 2].to_i(16)
|
|
489
|
-
[r, g, b, 255]
|
|
490
|
-
end
|
|
491
|
-
|
|
492
|
-
def colors_match?(c1, c2, tolerance = 0)
|
|
493
|
-
return false unless c1 && c2
|
|
494
|
-
(c1[0] - c2[0]).abs <= tolerance &&
|
|
495
|
-
(c1[1] - c2[1]).abs <= tolerance &&
|
|
496
|
-
(c1[2] - c2[2]).abs <= tolerance
|
|
497
|
-
end
|
|
498
|
-
|
|
499
|
-
# -- Flood fill -----------------------------------------------------------
|
|
500
|
-
|
|
501
|
-
def flood_fill(x, y)
|
|
502
|
-
x = x.to_i
|
|
503
|
-
y = y.to_i
|
|
504
|
-
layer = @layers.active_layer
|
|
505
|
-
return unless layer
|
|
506
|
-
return if x < 0 || x >= @canvas_width || y < 0 || y >= @canvas_height
|
|
507
|
-
|
|
508
|
-
target_color = get_pixel(x, y)
|
|
509
|
-
fill_color = parse_hex_color(@brush_color)
|
|
510
|
-
|
|
511
|
-
return if colors_match?(target_color, fill_color)
|
|
512
|
-
|
|
513
|
-
old_pixels = layer.snapshot_pixels
|
|
514
|
-
scanline_fill(x, y, target_color, fill_color)
|
|
515
|
-
layer.refresh_display
|
|
516
|
-
push_undo(LayerPixelsCommand.new(layer, old_pixels, layer.snapshot_pixels))
|
|
517
|
-
end
|
|
518
|
-
|
|
519
|
-
def scanline_fill(start_x, start_y, target_color, fill_color)
|
|
520
|
-
stack = [[start_x, start_y]]
|
|
521
|
-
|
|
522
|
-
while !stack.empty?
|
|
523
|
-
x, y = stack.pop
|
|
524
|
-
next if y < 0 || y >= @canvas_height
|
|
525
|
-
|
|
526
|
-
lx = x
|
|
527
|
-
while lx > 0 && colors_match?(get_pixel(lx - 1, y), target_color)
|
|
528
|
-
lx -= 1
|
|
529
|
-
end
|
|
530
|
-
|
|
531
|
-
span_above = false
|
|
532
|
-
span_below = false
|
|
533
|
-
|
|
534
|
-
while lx < @canvas_width && colors_match?(get_pixel(lx, y), target_color)
|
|
535
|
-
set_pixel(lx, y, fill_color)
|
|
536
|
-
|
|
537
|
-
if y > 0
|
|
538
|
-
above_matches = colors_match?(get_pixel(lx, y - 1), target_color)
|
|
539
|
-
if !span_above && above_matches
|
|
540
|
-
stack.push([lx, y - 1])
|
|
541
|
-
span_above = true
|
|
542
|
-
elsif span_above && !above_matches
|
|
543
|
-
span_above = false
|
|
544
|
-
end
|
|
545
|
-
end
|
|
546
|
-
|
|
547
|
-
if y < @canvas_height - 1
|
|
548
|
-
below_matches = colors_match?(get_pixel(lx, y + 1), target_color)
|
|
549
|
-
if !span_below && below_matches
|
|
550
|
-
stack.push([lx, y + 1])
|
|
551
|
-
span_below = true
|
|
552
|
-
elsif span_below && !below_matches
|
|
553
|
-
span_below = false
|
|
554
|
-
end
|
|
555
|
-
end
|
|
556
|
-
|
|
557
|
-
lx += 1
|
|
558
|
-
end
|
|
559
|
-
end
|
|
560
|
-
end
|
|
561
|
-
|
|
562
|
-
# -- Spray paint ----------------------------------------------------------
|
|
563
|
-
|
|
564
|
-
def spray_paint(x, y)
|
|
565
|
-
x = x.to_i
|
|
566
|
-
y = y.to_i
|
|
567
|
-
layer = @layers.active_layer
|
|
568
|
-
return unless layer
|
|
569
|
-
|
|
570
|
-
fill_color = parse_hex_color(@brush_color)
|
|
571
|
-
radius = @brush_size * 5
|
|
572
|
-
pixels_per_spray = @brush_size * @spray_density
|
|
573
|
-
|
|
574
|
-
pixels_per_spray.times do
|
|
575
|
-
angle = rand * 2 * Math::PI
|
|
576
|
-
r = rand * radius
|
|
577
|
-
px = x + (r * Math.cos(angle)).to_i
|
|
578
|
-
py = y + (r * Math.sin(angle)).to_i
|
|
579
|
-
set_pixel(px, py, fill_color)
|
|
580
|
-
end
|
|
581
|
-
|
|
582
|
-
layer.refresh_display
|
|
583
|
-
end
|
|
584
|
-
|
|
585
|
-
# -- Undo/Redo ------------------------------------------------------------
|
|
586
|
-
|
|
587
|
-
def push_undo(command)
|
|
588
|
-
@undo_stack << command
|
|
589
|
-
@undo_stack.shift if @undo_stack.size > MAX_UNDO
|
|
590
|
-
@redo_stack.clear
|
|
591
|
-
end
|
|
592
|
-
|
|
593
|
-
def undo
|
|
594
|
-
return if @undo_stack.empty?
|
|
595
|
-
command = @undo_stack.pop
|
|
596
|
-
command.undo
|
|
597
|
-
@redo_stack << command
|
|
598
|
-
end
|
|
599
|
-
|
|
600
|
-
def redo_action
|
|
601
|
-
return if @redo_stack.empty?
|
|
602
|
-
command = @redo_stack.pop
|
|
603
|
-
command.redo
|
|
604
|
-
@undo_stack << command
|
|
605
|
-
end
|
|
606
|
-
|
|
607
|
-
# Command classes for undo/redo
|
|
608
|
-
class StrokeCommand
|
|
609
|
-
def initialize(app, canvas, items)
|
|
610
|
-
@app = app
|
|
611
|
-
@canvas = canvas
|
|
612
|
-
@items = items
|
|
613
|
-
@configs = items.map do |item|
|
|
614
|
-
type = @app.command(@canvas, :type, item)
|
|
615
|
-
coords = @app.split_list(@app.command(@canvas, :coords, item))
|
|
616
|
-
{
|
|
617
|
-
type: type,
|
|
618
|
-
coords: coords,
|
|
619
|
-
fill: (@app.command(@canvas, :itemcget, item, '-fill') rescue nil),
|
|
620
|
-
width: (@app.command(@canvas, :itemcget, item, '-width') rescue nil),
|
|
621
|
-
outline: (@app.command(@canvas, :itemcget, item, '-outline') rescue nil),
|
|
622
|
-
capstyle: (@app.command(@canvas, :itemcget, item, '-capstyle') rescue nil),
|
|
623
|
-
joinstyle: (@app.command(@canvas, :itemcget, item, '-joinstyle') rescue nil)
|
|
624
|
-
}
|
|
625
|
-
end
|
|
626
|
-
end
|
|
627
|
-
|
|
628
|
-
def undo
|
|
629
|
-
@items.each { |item| @app.command(@canvas, :delete, item) }
|
|
630
|
-
end
|
|
631
|
-
|
|
632
|
-
def redo
|
|
633
|
-
@items = @configs.map do |cfg|
|
|
634
|
-
case cfg[:type]
|
|
635
|
-
when 'line'
|
|
636
|
-
opts = { fill: cfg[:fill], width: cfg[:width] }
|
|
637
|
-
opts[:capstyle] = cfg[:capstyle] if cfg[:capstyle] && cfg[:capstyle] != ''
|
|
638
|
-
opts[:joinstyle] = cfg[:joinstyle] if cfg[:joinstyle] && cfg[:joinstyle] != ''
|
|
639
|
-
@app.command(@canvas, :create, :line, *cfg[:coords], **opts)
|
|
640
|
-
when 'oval'
|
|
641
|
-
@app.command(@canvas, :create, :oval, *cfg[:coords],
|
|
642
|
-
fill: cfg[:fill], outline: cfg[:outline] || cfg[:fill])
|
|
643
|
-
when 'rectangle'
|
|
644
|
-
@app.command(@canvas, :create, :rectangle, *cfg[:coords],
|
|
645
|
-
outline: cfg[:outline], width: cfg[:width])
|
|
646
|
-
end
|
|
647
|
-
end
|
|
648
|
-
end
|
|
649
|
-
end
|
|
650
|
-
|
|
651
|
-
class LayerPixelsCommand
|
|
652
|
-
def initialize(layer, old_pixels, new_pixels)
|
|
653
|
-
@layer = layer
|
|
654
|
-
@old_pixels = old_pixels
|
|
655
|
-
@new_pixels = new_pixels
|
|
656
|
-
end
|
|
657
|
-
|
|
658
|
-
def undo
|
|
659
|
-
@layer.restore_pixels(@old_pixels)
|
|
660
|
-
end
|
|
661
|
-
|
|
662
|
-
def redo
|
|
663
|
-
@layer.restore_pixels(@new_pixels)
|
|
664
|
-
end
|
|
665
|
-
end
|
|
666
|
-
|
|
667
|
-
# -- Auto-paint demo (for TeekDemo) --------------------------------------
|
|
668
|
-
# Simulates real user interaction via virtual mouse events.
|
|
669
|
-
# Actions are chained sequentially so the event loop can process display
|
|
670
|
-
# updates between each one.
|
|
671
|
-
|
|
672
|
-
def run_auto_demo
|
|
673
|
-
# Move tools window onto the canvas so it's visible in the recording
|
|
674
|
-
@app.command(:wm, :geometry, @tools_path, '+10+80')
|
|
675
|
-
@app.command(:wm, :deiconify, @tools_path)
|
|
676
|
-
|
|
677
|
-
@demo_queue = []
|
|
678
|
-
@demo_canvas_path = @canvas.path
|
|
679
|
-
@demo_interval = TeekDemo.delay(test: 1, record: 15)
|
|
680
|
-
@demo_action_num = 0
|
|
681
|
-
|
|
682
|
-
# Helper: queue a virtual mouse event
|
|
683
|
-
q_mouse = proc do |event, x, y|
|
|
684
|
-
@demo_queue << proc {
|
|
685
|
-
@app.tcl_eval("event generate #{@demo_canvas_path} <#{event}> -x #{x} -y #{y}")
|
|
686
|
-
}
|
|
687
|
-
end
|
|
688
|
-
|
|
689
|
-
# Helper: queue a UI action
|
|
690
|
-
q_act = proc do |&block|
|
|
691
|
-
@demo_queue << block
|
|
692
|
-
end
|
|
693
|
-
|
|
694
|
-
# -- Fill background sky blue with bucket tool --
|
|
695
|
-
q_act.call { select_color('#87CEEB') }
|
|
696
|
-
q_act.call { select_tool(:bucket) }
|
|
697
|
-
q_mouse.call('ButtonPress-1', 400, 300)
|
|
698
|
-
q_mouse.call('ButtonRelease-1', 400, 300)
|
|
699
|
-
|
|
700
|
-
# -- Spray green ground --
|
|
701
|
-
q_act.call { select_color('#228B22') }
|
|
702
|
-
q_act.call { select_tool(:spray) }
|
|
703
|
-
q_act.call do
|
|
704
|
-
@brush_size = 10
|
|
705
|
-
@app.set_variable(@brush_size_var, '10')
|
|
706
|
-
@spray_density = 20
|
|
707
|
-
@app.set_variable(@spray_density_var, '20')
|
|
708
|
-
end
|
|
709
|
-
q_mouse.call('ButtonPress-1', 50, 500)
|
|
710
|
-
(100..750).step(40) do |x|
|
|
711
|
-
q_mouse.call('B1-Motion', x, 480 + rand(40))
|
|
712
|
-
end
|
|
713
|
-
q_mouse.call('ButtonRelease-1', 750, 510)
|
|
714
|
-
q_mouse.call('ButtonPress-1', 750, 550)
|
|
715
|
-
(710..50).step(-40) do |x|
|
|
716
|
-
q_mouse.call('B1-Motion', x, 530 + rand(40))
|
|
717
|
-
end
|
|
718
|
-
q_mouse.call('ButtonRelease-1', 50, 560)
|
|
719
|
-
|
|
720
|
-
# -- Spray white clouds --
|
|
721
|
-
q_act.call { select_color('#FFFFFF') }
|
|
722
|
-
q_act.call do
|
|
723
|
-
@brush_size = 8
|
|
724
|
-
@app.set_variable(@brush_size_var, '8')
|
|
725
|
-
@spray_density = 8
|
|
726
|
-
@app.set_variable(@spray_density_var, '8')
|
|
727
|
-
end
|
|
728
|
-
q_mouse.call('ButtonPress-1', 180, 100)
|
|
729
|
-
[[195, 90], [210, 85], [225, 90], [240, 100]].each { |x, y| q_mouse.call('B1-Motion', x, y) }
|
|
730
|
-
q_mouse.call('ButtonRelease-1', 240, 100)
|
|
731
|
-
q_mouse.call('ButtonPress-1', 520, 110)
|
|
732
|
-
[[540, 100], [560, 95], [580, 100], [595, 110]].each { |x, y| q_mouse.call('B1-Motion', x, y) }
|
|
733
|
-
q_mouse.call('ButtonRelease-1', 595, 110)
|
|
734
|
-
|
|
735
|
-
# -- Spray golden sun --
|
|
736
|
-
q_act.call { select_color('#FFD700') }
|
|
737
|
-
q_act.call do
|
|
738
|
-
@brush_size = 10
|
|
739
|
-
@app.set_variable(@brush_size_var, '10')
|
|
740
|
-
@spray_density = 10
|
|
741
|
-
@app.set_variable(@spray_density_var, '10')
|
|
742
|
-
end
|
|
743
|
-
q_mouse.call('ButtonPress-1', 660, 80)
|
|
744
|
-
[[670, 70], [680, 85], [665, 90], [675, 75]].each { |x, y| q_mouse.call('B1-Motion', x, y) }
|
|
745
|
-
q_mouse.call('ButtonRelease-1', 670, 80)
|
|
746
|
-
|
|
747
|
-
# -- Brush strokes: winding path --
|
|
748
|
-
q_act.call { select_color('#8B6914') }
|
|
749
|
-
q_act.call { select_tool(:brush) }
|
|
750
|
-
q_act.call do
|
|
751
|
-
@brush_size = 5
|
|
752
|
-
@app.set_variable(@brush_size_var, '5')
|
|
753
|
-
end
|
|
754
|
-
path = [[100, 550], [200, 520], [320, 530], [450, 510], [550, 520], [680, 500], [780, 510]]
|
|
755
|
-
q_mouse.call('ButtonPress-1', *path.first)
|
|
756
|
-
path[1..].each { |x, y| q_mouse.call('B1-Motion', x, y) }
|
|
757
|
-
q_mouse.call('ButtonRelease-1', *path.last)
|
|
758
|
-
|
|
759
|
-
# -- Brush strokes: tree trunks and canopy --
|
|
760
|
-
[[160, 440], [620, 430]].each do |tx, ty|
|
|
761
|
-
q_act.call { select_color('#8B4513') }
|
|
762
|
-
q_mouse.call('ButtonPress-1', tx, ty)
|
|
763
|
-
q_mouse.call('B1-Motion', tx, ty + 70)
|
|
764
|
-
q_mouse.call('ButtonRelease-1', tx, ty + 70)
|
|
765
|
-
q_act.call do
|
|
766
|
-
select_color('#006400')
|
|
767
|
-
@brush_size = 8
|
|
768
|
-
@app.set_variable(@brush_size_var, '8')
|
|
769
|
-
end
|
|
770
|
-
[[-20, -10], [0, -25], [20, -10], [-10, -18], [10, -18]].each do |dx, dy|
|
|
771
|
-
q_mouse.call('ButtonPress-1', tx + dx, ty + dy)
|
|
772
|
-
q_mouse.call('ButtonRelease-1', tx + dx, ty + dy)
|
|
773
|
-
end
|
|
774
|
-
end
|
|
775
|
-
|
|
776
|
-
# -- Eraser demo: zigzag sweep so it's clearly erasing --
|
|
777
|
-
q_act.call { select_tool(:eraser) }
|
|
778
|
-
q_act.call do
|
|
779
|
-
@brush_size = 6
|
|
780
|
-
@app.set_variable(@brush_size_var, '6')
|
|
781
|
-
end
|
|
782
|
-
q_mouse.call('ButtonPress-1', 300, 280)
|
|
783
|
-
[[330, 320], [360, 270], [390, 320], [420, 270],
|
|
784
|
-
[450, 320], [480, 270], [510, 320]].each do |x, y|
|
|
785
|
-
q_mouse.call('B1-Motion', x, y)
|
|
786
|
-
end
|
|
787
|
-
q_mouse.call('ButtonRelease-1', 510, 320)
|
|
788
|
-
|
|
789
|
-
# -- Reset and finish --
|
|
790
|
-
q_act.call do
|
|
791
|
-
select_tool(:brush)
|
|
792
|
-
@brush_size = 1
|
|
793
|
-
@app.set_variable(@brush_size_var, '1')
|
|
794
|
-
end
|
|
795
|
-
|
|
796
|
-
$stdout.puts "[paint-demo] queued #{@demo_queue.size} actions"
|
|
797
|
-
$stdout.flush
|
|
798
|
-
run_next_demo_action
|
|
799
|
-
end
|
|
800
|
-
|
|
801
|
-
def run_next_demo_action
|
|
802
|
-
if @demo_queue.empty?
|
|
803
|
-
$stdout.puts "[paint-demo] all actions complete"
|
|
804
|
-
$stdout.flush
|
|
805
|
-
@app.after(@demo_interval) { TeekDemo.finish } if defined?(TeekDemo) && TeekDemo.active?
|
|
806
|
-
return
|
|
807
|
-
end
|
|
808
|
-
|
|
809
|
-
action = @demo_queue.shift
|
|
810
|
-
@demo_action_num += 1
|
|
811
|
-
if (@demo_action_num % 20).zero?
|
|
812
|
-
$stdout.puts "[paint-demo] action #{@demo_action_num}..."
|
|
813
|
-
$stdout.flush
|
|
814
|
-
end
|
|
815
|
-
action.call
|
|
816
|
-
@app.after(@demo_interval) { run_next_demo_action }
|
|
817
|
-
end
|
|
818
|
-
end
|
|
819
|
-
|
|
820
|
-
# -- Main ------------------------------------------------------------------
|
|
821
|
-
|
|
822
|
-
app = Teek::App.new(track_widgets: false)
|
|
823
|
-
app.show
|
|
824
|
-
|
|
825
|
-
paint = PaintDemo.new(app)
|
|
826
|
-
|
|
827
|
-
# Automated demo support
|
|
828
|
-
require_relative '../../lib/teek/demo_support'
|
|
829
|
-
TeekDemo.app = app
|
|
830
|
-
|
|
831
|
-
if TeekDemo.active?
|
|
832
|
-
TeekDemo.on_visible do
|
|
833
|
-
app.after(200) { paint.run_auto_demo }
|
|
834
|
-
end
|
|
835
|
-
end
|
|
836
|
-
|
|
837
|
-
app.mainloop
|