teek 0.1.0 → 0.1.2
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 +99 -15
- data/Rakefile +201 -2
- data/ext/teek/extconf.rb +1 -1
- data/ext/teek/tcltkbridge.c +3 -110
- data/ext/teek/tcltkbridge.h +3 -0
- data/ext/teek/tkeventsource.c +195 -0
- data/ext/teek/tkphoto.c +169 -5
- data/ext/teek/tkwin.c +84 -0
- data/lib/teek/background_ractor4x.rb +35 -6
- data/lib/teek/debugger.rb +37 -32
- data/lib/teek/method_coverage_service.rb +265 -0
- data/lib/teek/photo.rb +232 -0
- data/lib/teek/ractor_support.rb +1 -1
- data/lib/teek/version.rb +1 -1
- data/lib/teek/widget.rb +104 -0
- data/lib/teek.rb +144 -1
- data/sample/calculator.rb +16 -21
- data/sample/debug_demo.rb +20 -22
- data/sample/optcarrot/vendor/optcarrot/apu.rb +856 -0
- data/sample/optcarrot/vendor/optcarrot/config.rb +257 -0
- data/sample/optcarrot/vendor/optcarrot/cpu.rb +1162 -0
- data/sample/optcarrot/vendor/optcarrot/driver.rb +144 -0
- data/sample/optcarrot/vendor/optcarrot/mapper/cnrom.rb +14 -0
- data/sample/optcarrot/vendor/optcarrot/mapper/mmc1.rb +105 -0
- data/sample/optcarrot/vendor/optcarrot/mapper/mmc3.rb +153 -0
- data/sample/optcarrot/vendor/optcarrot/mapper/uxrom.rb +14 -0
- data/sample/optcarrot/vendor/optcarrot/nes.rb +105 -0
- data/sample/optcarrot/vendor/optcarrot/opt.rb +168 -0
- data/sample/optcarrot/vendor/optcarrot/pad.rb +92 -0
- data/sample/optcarrot/vendor/optcarrot/palette.rb +65 -0
- data/sample/optcarrot/vendor/optcarrot/ppu.rb +1468 -0
- data/sample/optcarrot/vendor/optcarrot/rom.rb +143 -0
- data/sample/optcarrot/vendor/optcarrot.rb +14 -0
- data/sample/optcarrot.rb +354 -0
- 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 +255 -0
- data/sample/paint/layer_manager.rb +179 -0
- data/sample/paint/paint_demo.rb +837 -0
- data/sample/paint/sparse_pixel_buffer.rb +202 -0
- data/sample/sdl2_demo.rb +318 -0
- data/sample/threading_demo.rb +127 -132
- metadata +31 -1
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Sparse storage for pixel data - only stores non-default pixels.
|
|
4
|
+
# Memory efficient for layers with limited drawing.
|
|
5
|
+
#
|
|
6
|
+
# Stores pixels as 4-byte binary strings to avoid pack/unpack overhead
|
|
7
|
+
# when interfacing with TkPhotoImage#put_block.
|
|
8
|
+
#
|
|
9
|
+
class SparsePixelBuffer
|
|
10
|
+
# Default: fully transparent
|
|
11
|
+
DEFAULT_PIXEL = "\x00\x00\x00\x00".b.freeze
|
|
12
|
+
PIXEL_SIZE = 4
|
|
13
|
+
|
|
14
|
+
attr_reader :width, :height, :default_pixel
|
|
15
|
+
|
|
16
|
+
def initialize(width, height, default: DEFAULT_PIXEL)
|
|
17
|
+
@width = width
|
|
18
|
+
@height = height
|
|
19
|
+
@default_pixel = default.frozen? ? default : default.dup.freeze
|
|
20
|
+
@pixels = {} # {linear_index => 4-byte binary string}
|
|
21
|
+
@bbox = nil # [min_x, min_y, max_x, max_y] or nil if empty
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def get_pixel(x, y)
|
|
25
|
+
return nil if out_of_bounds?(x, y)
|
|
26
|
+
@pixels[y * @width + x] || @default_pixel
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def set_pixel(x, y, rgba_bytes)
|
|
30
|
+
return if out_of_bounds?(x, y)
|
|
31
|
+
|
|
32
|
+
key = y * @width + x
|
|
33
|
+
|
|
34
|
+
if rgba_bytes == @default_pixel
|
|
35
|
+
@pixels.delete(key)
|
|
36
|
+
recalculate_bbox if @pixels.empty? || bbox_edge?(x, y)
|
|
37
|
+
else
|
|
38
|
+
@pixels[key] = rgba_bytes.frozen? ? rgba_bytes : rgba_bytes.dup
|
|
39
|
+
expand_bbox(x, y)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Set pixel from RGBA integers (convenience method)
|
|
44
|
+
def set_rgba(x, y, r, g, b, a = 255)
|
|
45
|
+
set_pixel(x, y, [r, g, b, a].pack('CCCC'))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Get pixel as RGBA integers (convenience method)
|
|
49
|
+
def get_rgba(x, y)
|
|
50
|
+
pixel = get_pixel(x, y)
|
|
51
|
+
pixel&.unpack('CCCC')
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def empty?
|
|
55
|
+
@pixels.empty?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def pixel_count
|
|
59
|
+
@pixels.size
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def bbox
|
|
63
|
+
@bbox&.dup
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Returns [x, y, width, height] for the bounding box
|
|
67
|
+
def bbox_xywh
|
|
68
|
+
return nil unless @bbox
|
|
69
|
+
[@bbox[0], @bbox[1], @bbox[2] - @bbox[0] + 1, @bbox[3] - @bbox[1] + 1]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Materialize a region to a contiguous RGBA buffer for put_block
|
|
73
|
+
def materialize(x: 0, y: 0, width: @width, height: @height)
|
|
74
|
+
# Clamp to valid region
|
|
75
|
+
x = x.clamp(0, @width - 1)
|
|
76
|
+
y = y.clamp(0, @height - 1)
|
|
77
|
+
width = [width, @width - x].min
|
|
78
|
+
height = [height, @height - y].min
|
|
79
|
+
|
|
80
|
+
# Allocate buffer filled with default pixel
|
|
81
|
+
buffer = (@default_pixel * (width * height)).dup
|
|
82
|
+
|
|
83
|
+
# Patch in non-default pixels
|
|
84
|
+
@pixels.each do |key, rgba|
|
|
85
|
+
px = key % @width
|
|
86
|
+
py = key / @width
|
|
87
|
+
|
|
88
|
+
# Skip if outside requested region
|
|
89
|
+
next unless px >= x && px < x + width && py >= y && py < y + height
|
|
90
|
+
|
|
91
|
+
# Calculate offset in output buffer
|
|
92
|
+
offset = ((py - y) * width + (px - x)) * PIXEL_SIZE
|
|
93
|
+
buffer[offset, PIXEL_SIZE] = rgba
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
buffer
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Materialize only the bounding box (returns nil if empty)
|
|
100
|
+
def materialize_bbox
|
|
101
|
+
return nil if empty? || @bbox.nil?
|
|
102
|
+
bx, by, bw, bh = bbox_xywh
|
|
103
|
+
materialize(x: bx, y: by, width: bw, height: bh)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Resize the buffer, preserving pixels that fit within new bounds
|
|
107
|
+
def resize(new_width, new_height)
|
|
108
|
+
return if new_width == @width && new_height == @height
|
|
109
|
+
|
|
110
|
+
new_pixels = {}
|
|
111
|
+
@pixels.each do |key, rgba|
|
|
112
|
+
x = key % @width
|
|
113
|
+
y = key / @width
|
|
114
|
+
next if x >= new_width || y >= new_height
|
|
115
|
+
new_pixels[y * new_width + x] = rgba
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
@width = new_width
|
|
119
|
+
@height = new_height
|
|
120
|
+
@pixels = new_pixels
|
|
121
|
+
recalculate_bbox
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Clear all pixels
|
|
125
|
+
def clear
|
|
126
|
+
@pixels.clear
|
|
127
|
+
@bbox = nil
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Create a full copy
|
|
131
|
+
def dup
|
|
132
|
+
copy = SparsePixelBuffer.new(@width, @height, default: @default_pixel)
|
|
133
|
+
copy.instance_variable_set(:@pixels, @pixels.dup)
|
|
134
|
+
copy.instance_variable_set(:@bbox, @bbox&.dup)
|
|
135
|
+
copy
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Memory usage estimate in bytes
|
|
139
|
+
def memory_usage
|
|
140
|
+
# Hash overhead (~40 bytes) + per-entry (~50 bytes: key + value + hash bucket)
|
|
141
|
+
40 + (@pixels.size * 50)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Density as fraction of total pixels
|
|
145
|
+
def density
|
|
146
|
+
@pixels.size.to_f / (@width * @height)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Iterate over non-default pixels
|
|
150
|
+
def each_pixel
|
|
151
|
+
return enum_for(:each_pixel) unless block_given?
|
|
152
|
+
|
|
153
|
+
@pixels.each do |key, rgba|
|
|
154
|
+
x = key % @width
|
|
155
|
+
y = key / @width
|
|
156
|
+
yield x, y, rgba
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
def out_of_bounds?(x, y)
|
|
163
|
+
x < 0 || x >= @width || y < 0 || y >= @height
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def expand_bbox(x, y)
|
|
167
|
+
if @bbox.nil?
|
|
168
|
+
@bbox = [x, y, x, y]
|
|
169
|
+
else
|
|
170
|
+
@bbox[0] = x if x < @bbox[0]
|
|
171
|
+
@bbox[1] = y if y < @bbox[1]
|
|
172
|
+
@bbox[2] = x if x > @bbox[2]
|
|
173
|
+
@bbox[3] = y if y > @bbox[3]
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def bbox_edge?(x, y)
|
|
178
|
+
return false unless @bbox
|
|
179
|
+
x == @bbox[0] || x == @bbox[2] || y == @bbox[1] || y == @bbox[3]
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def recalculate_bbox
|
|
183
|
+
if @pixels.empty?
|
|
184
|
+
@bbox = nil
|
|
185
|
+
return
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
min_x = min_y = Float::INFINITY
|
|
189
|
+
max_x = max_y = -Float::INFINITY
|
|
190
|
+
|
|
191
|
+
@pixels.each_key do |key|
|
|
192
|
+
x = key % @width
|
|
193
|
+
y = key / @width
|
|
194
|
+
min_x = x if x < min_x
|
|
195
|
+
max_x = x if x > max_x
|
|
196
|
+
min_y = y if y < min_y
|
|
197
|
+
max_y = y if y > max_y
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
@bbox = [min_x, min_y, max_x, max_y]
|
|
201
|
+
end
|
|
202
|
+
end
|
data/sample/sdl2_demo.rb
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
# teek-record: title=SDL2 Demo
|
|
4
|
+
|
|
5
|
+
# SDL2 Demo - GPU-accelerated rendering embedded in a Tk frame
|
|
6
|
+
#
|
|
7
|
+
# Demonstrates:
|
|
8
|
+
# - Teek::SDL2::Viewport with animated rectangles
|
|
9
|
+
# - SDL2_image: loading a PNG sprite (ruby gem)
|
|
10
|
+
# - SDL2_ttf: text rendering
|
|
11
|
+
# - Keyboard/mouse input via Tk event bindings
|
|
12
|
+
# - Separate Tk event log window proving bidirectional event flow
|
|
13
|
+
#
|
|
14
|
+
# Ruby gem image: CC0 from https://purepng.com/photo/27996/clipart-ruby-gem
|
|
15
|
+
#
|
|
16
|
+
# Run: ruby -Ilib -Iteek-sdl2/lib sample/sdl2_demo.rb
|
|
17
|
+
|
|
18
|
+
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
|
19
|
+
$LOAD_PATH.unshift File.expand_path('../teek-sdl2/lib', __dir__)
|
|
20
|
+
require 'teek'
|
|
21
|
+
require 'teek/sdl2'
|
|
22
|
+
|
|
23
|
+
FONT_PATH = File.join(__dir__, '..', 'teek-sdl2', 'assets', 'JetBrainsMonoNL-Regular.ttf')
|
|
24
|
+
RUBY_GEM_PATH = File.join(__dir__, '..', 'teek-sdl2', 'assets', 'ruby_gem_64.png')
|
|
25
|
+
|
|
26
|
+
class SDL2Demo
|
|
27
|
+
attr_reader :app
|
|
28
|
+
|
|
29
|
+
COLORS = [
|
|
30
|
+
[255, 60, 60], # red
|
|
31
|
+
[60, 200, 60], # green
|
|
32
|
+
[60, 100, 255], # blue
|
|
33
|
+
[255, 200, 40], # yellow
|
|
34
|
+
[200, 60, 255], # purple
|
|
35
|
+
].freeze
|
|
36
|
+
|
|
37
|
+
MAX_KEYSTROKES = 8
|
|
38
|
+
MAX_PARTICLES = 60
|
|
39
|
+
|
|
40
|
+
def initialize
|
|
41
|
+
@app = Teek::App.new
|
|
42
|
+
@app.show
|
|
43
|
+
@app.set_window_title('SDL2 Demo')
|
|
44
|
+
@app.set_window_geometry('640x520')
|
|
45
|
+
|
|
46
|
+
@title = 'SDL2 Demo'
|
|
47
|
+
|
|
48
|
+
# SDL2 viewport
|
|
49
|
+
@viewport = Teek::SDL2::Viewport.new(@app, width: 640, height: 480)
|
|
50
|
+
@viewport.pack(fill: :both, expand: true)
|
|
51
|
+
|
|
52
|
+
# Load font for text rendering
|
|
53
|
+
@font = @viewport.renderer.load_font(FONT_PATH, 18)
|
|
54
|
+
@font_small = @viewport.renderer.load_font(FONT_PATH, 12)
|
|
55
|
+
|
|
56
|
+
# Load ruby gem sprite via SDL2_image
|
|
57
|
+
@gem_tex = @viewport.renderer.load_image(RUBY_GEM_PATH)
|
|
58
|
+
|
|
59
|
+
# Bouncing boxes (last one is the gem sprite)
|
|
60
|
+
@boxes = COLORS.each_with_index.map do |color, i|
|
|
61
|
+
{
|
|
62
|
+
x: 40 + i * 100, y: 40 + i * 60,
|
|
63
|
+
w: 60, h: 40,
|
|
64
|
+
dx: 2 + i, dy: 1 + i,
|
|
65
|
+
r: color[0], g: color[1], b: color[2]
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
@boxes << {
|
|
69
|
+
x: 300, y: 200,
|
|
70
|
+
w: @gem_tex.width, h: @gem_tex.height,
|
|
71
|
+
dx: 3, dy: 2,
|
|
72
|
+
sprite: @gem_tex
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# Input state
|
|
76
|
+
@recent_keys = [] # [{text:, age:}, ...]
|
|
77
|
+
@particles = [] # [{x:, y:, dx:, dy:, age:, r:, g:, b:}, ...]
|
|
78
|
+
@has_focus = false
|
|
79
|
+
|
|
80
|
+
# Wire up input
|
|
81
|
+
setup_input
|
|
82
|
+
setup_event_log
|
|
83
|
+
|
|
84
|
+
@frame_count = 0
|
|
85
|
+
@fps_frames = 0
|
|
86
|
+
@fps_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
87
|
+
@fps_text = '-- fps'
|
|
88
|
+
@running = true
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def setup_input
|
|
92
|
+
# Key events (viewport tracks state internally, we also show them)
|
|
93
|
+
@viewport.bind('KeyPress', :keysym) do |k|
|
|
94
|
+
@recent_keys.unshift({ text: k, age: 0 })
|
|
95
|
+
@recent_keys.pop if @recent_keys.size > MAX_KEYSTROKES
|
|
96
|
+
log_event("KEY DOWN: #{k}")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
@viewport.bind('KeyRelease', :keysym) do |k|
|
|
100
|
+
log_event("KEY UP: #{k}")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Mouse particles
|
|
104
|
+
@viewport.bind('Motion', :x, :y) do |x, y|
|
|
105
|
+
spawn_particle(x.to_i, y.to_i)
|
|
106
|
+
log_event("MOUSE: #{x},#{y}")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
@viewport.bind('ButtonPress-1', :x, :y) do |x, y|
|
|
110
|
+
5.times { spawn_particle(x.to_i, y.to_i) }
|
|
111
|
+
log_event("CLICK: #{x},#{y}")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Focus tracking
|
|
115
|
+
@viewport.bind('FocusIn') { @has_focus = true }
|
|
116
|
+
@viewport.bind('FocusOut') { @has_focus = false }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def setup_event_log
|
|
120
|
+
@log_path = '.evlog'
|
|
121
|
+
@app.command(:toplevel, @log_path)
|
|
122
|
+
@app.command(:wm, :title, @log_path, 'Event Log')
|
|
123
|
+
@app.command(:wm, :geometry, @log_path, '300x400+660+0')
|
|
124
|
+
|
|
125
|
+
@log_text = @app.create_widget(:text, @log_path + '.log',
|
|
126
|
+
width: 40, height: 25, font: '{TkFixedFont} 10',
|
|
127
|
+
state: :disabled, background: '#1e1e1e', foreground: '#cccccc')
|
|
128
|
+
@log_text.pack(fill: :both, expand: true)
|
|
129
|
+
|
|
130
|
+
@app.command(:wm, :protocol, @log_path, 'WM_DELETE_WINDOW',
|
|
131
|
+
proc { @app.command(:wm, :withdraw, @log_path) })
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def log_event(msg)
|
|
135
|
+
@app.command(@log_text, :configure, state: :normal)
|
|
136
|
+
@app.command(@log_text, :insert, 'end', msg + "\n")
|
|
137
|
+
@app.command(@log_text, :see, 'end')
|
|
138
|
+
@app.command(@log_text, :configure, state: :disabled)
|
|
139
|
+
|
|
140
|
+
# Keep last 200 lines
|
|
141
|
+
count = @app.command(@log_text, :count, '-lines', '1.0', 'end').to_i
|
|
142
|
+
if count > 200
|
|
143
|
+
@app.command(@log_text, :configure, state: :normal)
|
|
144
|
+
@app.command(@log_text, :delete, '1.0', "#{count - 200}.0")
|
|
145
|
+
@app.command(@log_text, :configure, state: :disabled)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def spawn_particle(x, y)
|
|
150
|
+
angle = rand * 2 * Math::PI
|
|
151
|
+
speed = 1 + rand * 3
|
|
152
|
+
color = COLORS.sample
|
|
153
|
+
@particles << {
|
|
154
|
+
x: x.to_f, y: y.to_f,
|
|
155
|
+
dx: Math.cos(angle) * speed, dy: Math.sin(angle) * speed,
|
|
156
|
+
age: 0, r: color[0], g: color[1], b: color[2]
|
|
157
|
+
}
|
|
158
|
+
@particles.shift if @particles.size > MAX_PARTICLES
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def tick
|
|
162
|
+
return unless @running
|
|
163
|
+
|
|
164
|
+
w, h = @viewport.renderer.output_size
|
|
165
|
+
|
|
166
|
+
# Move boxes
|
|
167
|
+
@boxes.each do |box|
|
|
168
|
+
box[:x] += box[:dx]
|
|
169
|
+
box[:y] += box[:dy]
|
|
170
|
+
|
|
171
|
+
if box[:x] <= 0 || box[:x] + box[:w] >= w
|
|
172
|
+
box[:dx] = -box[:dx]
|
|
173
|
+
box[:x] = box[:x].clamp(0, w - box[:w])
|
|
174
|
+
end
|
|
175
|
+
if box[:y] <= 0 || box[:y] + box[:h] >= h
|
|
176
|
+
box[:dy] = -box[:dy]
|
|
177
|
+
box[:y] = box[:y].clamp(0, h - box[:h])
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Age particles
|
|
182
|
+
@particles.each { |p| p[:age] += 1; p[:x] += p[:dx]; p[:y] += p[:dy] }
|
|
183
|
+
@particles.reject! { |p| p[:age] > 30 }
|
|
184
|
+
|
|
185
|
+
# Age keystrokes
|
|
186
|
+
@recent_keys.each { |k| k[:age] += 1 }
|
|
187
|
+
@recent_keys.reject! { |k| k[:age] > 120 }
|
|
188
|
+
|
|
189
|
+
# Draw
|
|
190
|
+
@viewport.render do |r|
|
|
191
|
+
r.clear(20, 20, 30)
|
|
192
|
+
|
|
193
|
+
# Bouncing boxes (+ gem sprite)
|
|
194
|
+
@boxes.each do |box|
|
|
195
|
+
if box[:sprite]
|
|
196
|
+
r.copy(box[:sprite], nil, [box[:x], box[:y], box[:w], box[:h]])
|
|
197
|
+
else
|
|
198
|
+
r.fill_rect(box[:x], box[:y], box[:w], box[:h],
|
|
199
|
+
box[:r], box[:g], box[:b])
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Particles
|
|
204
|
+
@particles.each do |p|
|
|
205
|
+
alpha = ((1.0 - p[:age] / 30.0) * 255).to_i
|
|
206
|
+
size = [4 - p[:age] / 10, 1].max
|
|
207
|
+
r.fill_rect(p[:x].to_i, p[:y].to_i, size, size,
|
|
208
|
+
p[:r], p[:g], p[:b], alpha)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Keystrokes in bottom-right
|
|
212
|
+
@recent_keys.each_with_index do |k, i|
|
|
213
|
+
alpha = ((1.0 - k[:age] / 120.0) * 255).to_i
|
|
214
|
+
r.draw_text(w - 150, h - 30 - i * 22, k[:text],
|
|
215
|
+
font: @font, r: 255, g: 255, b: 255, a: alpha)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# FPS top-left
|
|
219
|
+
r.draw_text(8, 8, @fps_text, font: @font_small, r: 180, g: 180, b: 180)
|
|
220
|
+
|
|
221
|
+
# Focus hint
|
|
222
|
+
unless @has_focus
|
|
223
|
+
r.draw_text(w / 2 - 60, h / 2, "click to focus",
|
|
224
|
+
font: @font_small, r: 100, g: 100, b: 100)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
@frame_count += 1
|
|
229
|
+
@fps_frames += 1
|
|
230
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
231
|
+
elapsed = now - @fps_time
|
|
232
|
+
if elapsed >= 0.5
|
|
233
|
+
@fps_text = "#{(@fps_frames / elapsed).round(1)} fps"
|
|
234
|
+
@fps_frames = 0
|
|
235
|
+
@fps_time = now
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def animate(interval: 16, &on_done)
|
|
240
|
+
tick
|
|
241
|
+
if @running
|
|
242
|
+
@app.after(interval) { animate(interval: interval, &on_done) }
|
|
243
|
+
else
|
|
244
|
+
on_done&.call
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def stop
|
|
249
|
+
@running = false
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def run
|
|
253
|
+
animate
|
|
254
|
+
@app.mainloop
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
demo = SDL2Demo.new
|
|
259
|
+
|
|
260
|
+
# Automated demo support (testing and recording)
|
|
261
|
+
require_relative '../lib/teek/demo_support'
|
|
262
|
+
TeekDemo.app = demo.app
|
|
263
|
+
|
|
264
|
+
if TeekDemo.recording?
|
|
265
|
+
demo.app.set_window_geometry('+0+0')
|
|
266
|
+
demo.app.tcl_eval('. configure -cursor none')
|
|
267
|
+
TeekDemo.signal_recording_ready
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
if TeekDemo.active?
|
|
271
|
+
vp = demo.instance_variable_get(:@viewport)
|
|
272
|
+
fp = vp.frame.path
|
|
273
|
+
|
|
274
|
+
TeekDemo.after_idle do
|
|
275
|
+
d = TeekDemo.method(:delay)
|
|
276
|
+
app = demo.app
|
|
277
|
+
gen = proc { |ev, **opts|
|
|
278
|
+
args = opts.map { |k, v| "-#{k} #{v}" }.join(' ')
|
|
279
|
+
app.tcl_eval("event generate #{fp} <#{ev}> #{args}")
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
demo.animate
|
|
283
|
+
|
|
284
|
+
# Give focus (-force needed on X11/xvfb for event generate to deliver key events)
|
|
285
|
+
app.after(d.call(test: 1, record: 300)) { app.tcl_eval("focus -force #{fp}") }
|
|
286
|
+
|
|
287
|
+
# Click around to show particles
|
|
288
|
+
clicks = [[200, 150], [400, 300], [100, 350], [500, 200], [300, 100]]
|
|
289
|
+
clicks.each_with_index do |(x, y), i|
|
|
290
|
+
t = d.call(test: 1, record: 600) * (i + 1) + d.call(test: 1, record: 500)
|
|
291
|
+
app.after(t) {
|
|
292
|
+
gen.call('ButtonPress-1', x: x, y: y)
|
|
293
|
+
app.after(50) { gen.call('ButtonRelease-1', x: x, y: y) }
|
|
294
|
+
}
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Type some keys (overlaps with clicks)
|
|
298
|
+
keys = %w[H e l l o space S D L 2]
|
|
299
|
+
base = d.call(test: 1, record: 2000)
|
|
300
|
+
keys.each_with_index do |k, i|
|
|
301
|
+
t = base + d.call(test: 1, record: 150) * (i + 1)
|
|
302
|
+
app.after(t) {
|
|
303
|
+
gen.call('KeyPress', keysym: k)
|
|
304
|
+
app.after(80) { gen.call('KeyRelease', keysym: k) }
|
|
305
|
+
}
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Finish after ~5s recording / immediately in test
|
|
309
|
+
app.after(d.call(test: 50, record: 5000)) {
|
|
310
|
+
demo.stop
|
|
311
|
+
TeekDemo.finish
|
|
312
|
+
}
|
|
313
|
+
end
|
|
314
|
+
else
|
|
315
|
+
demo.animate
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
demo.app.mainloop
|