scratchpad-app 0.1.1

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,19 @@
1
+ # Scratchpad
2
+
3
+ メモ書き用の簡易ペイントソフトです。半透明の背景で最大化されて起動しま
4
+ す。
5
+
6
+ ## インストール
7
+
8
+ $ gem install scratchpad-app
9
+
10
+ ## 使い方
11
+
12
+ 左マウスボタンで線が書けて、右クリックで全消去します。
13
+
14
+ 画像の保存機能や消しゴム機能はありません。
15
+
16
+ ## 貢献
17
+
18
+ バグ報告などは https://github.com/plonk/scratchpad まで。
19
+
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "scratchpad"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'scratchpad'
4
+
5
+ Scratchpad::Program.new.run
@@ -0,0 +1,365 @@
1
+ require "scratchpad/interpolator"
2
+ require "scratchpad/version"
3
+
4
+ require 'toml'
5
+ require 'gtk2'
6
+
7
+ module Scratchpad
8
+
9
+ # 便利関数
10
+ def distance(p1, p2)
11
+ x1, y1 = p1
12
+ x2, y2 = p2
13
+
14
+ return Math.sqrt((x1 - x2)**2 + (y1 - y2)**2)
15
+ end
16
+
17
+ def chess_distance(p1, p2)
18
+ x1, y1 = p1
19
+ x2, y2 = p2
20
+
21
+ return [(x1 - x2).abs, (y1 - y2).abs].max
22
+ end
23
+
24
+ def midpoint(p1, p2)
25
+ x1, y1 = p1
26
+ x2, y2 = p2
27
+
28
+ return [(x1 + x2) * 0.5, (y1 + y2) * 0.5]
29
+ end
30
+
31
+ def color_bytes_to_floats(bytes)
32
+ raise 'wrong range' unless bytes.all? { |x| x >= 0 && x <= 255 }
33
+ raise 'wrong dimension' unless bytes.size.between?(3, 4)
34
+
35
+ return bytes.map { |b| b / 255.0 }
36
+ end
37
+
38
+ # /便利関数
39
+
40
+ class Pen
41
+ include Math
42
+ extend Scratchpad
43
+
44
+ attr_reader :x, :y
45
+
46
+ FAVORITE_ORANGE = color_bytes_to_floats [255, 109, 50]
47
+ FAVORITE_BLUE = color_bytes_to_floats [0, 3, 126]
48
+
49
+ def initialize
50
+ @is_down = false
51
+ @interpolator = Interpolator.new
52
+ @path = []
53
+ end
54
+
55
+ # [[x, y, radius]*]
56
+ def path
57
+ f = proc { |x|
58
+ [-5.0/10.0 * x + 8.0, 0.3].max
59
+ #x
60
+ }
61
+ @path.map { |x, y, velocity|
62
+ [x, y, sqrt(f.(velocity))]
63
+ }
64
+ end
65
+
66
+ def down(ev)
67
+ @is_down = true
68
+ end
69
+
70
+ def up(ev)
71
+ @is_down = false
72
+ end
73
+
74
+ def move(ev)
75
+ @path = @interpolator.feed(ev.x, ev.y, @is_down)
76
+ end
77
+
78
+ def down?
79
+ @is_down
80
+ end
81
+
82
+ def radius
83
+ 2.0
84
+ end
85
+
86
+ def color
87
+ FAVORITE_BLUE
88
+ end
89
+ end
90
+
91
+ class SheetModel < GLib::Object
92
+ type_register
93
+ signal_new('changed', GLib::Signal::ACTION, nil, nil)
94
+
95
+ include Math
96
+ include Cairo
97
+
98
+ attr_reader :surface, :pen, :debug_surface
99
+
100
+ def initialize
101
+ super()
102
+ @surface = ImageSurface.new(Cairo::FORMAT_ARGB32, 1200, 900)
103
+ @debug_surface = ImageSurface.new(Cairo::FORMAT_ARGB32, 1200, 900)
104
+ cr = Context.new(@surface)
105
+ cr.set_operator(Cairo::OPERATOR_OVER)
106
+ cr.line_cap = cr.line_join = :round
107
+ @context = cr
108
+
109
+ @pen = Pen.new
110
+
111
+ @portion = :all
112
+ end
113
+
114
+ def cr
115
+ @context
116
+ end
117
+
118
+ def clear
119
+ cr.save do
120
+ cr.set_operator(Cairo::OPERATOR_SOURCE)
121
+ cr.set_source_rgba(0, 0, 0, 0)
122
+ cr.paint
123
+ end
124
+ clear_debug
125
+ signal_emit('changed')
126
+ end
127
+
128
+ def clear_debug
129
+ c = Context.new(@debug_surface)
130
+ c.save do
131
+ c.set_operator(Cairo::OPERATOR_SOURCE)
132
+ c.set_source_rgba(0, 0, 0, 0)
133
+ c.paint
134
+ end
135
+ c.destroy
136
+ end
137
+
138
+ def pen_down(ev)
139
+ @pen.down(ev)
140
+ @portion = :latter_half
141
+ end
142
+
143
+ def pen_up(ev)
144
+ @pen.up(ev)
145
+ @portion = :first_half
146
+ end
147
+
148
+ def normalize(ary)
149
+ sum = ary.max
150
+ ary.map { |x| x / sum }
151
+ end
152
+
153
+ def update
154
+ if @pen.down?
155
+ case @portion
156
+ when :all
157
+ path = @pen.path
158
+ cr.set_source_rgba(@pen.color)
159
+ cr.line_width = @pen.radius
160
+ path.each.with_index do |(x, y, radius), i|
161
+ if i == 0
162
+ cr.move_to(x, y)
163
+ else
164
+ cr.line_to(x, y)
165
+ end
166
+ end
167
+ cr.stroke
168
+ when :latter_half
169
+ path = @pen.path[5..10] || []
170
+ cr.set_source_rgba(@pen.color)
171
+ cr.line_width = @pen.radius
172
+ path.each.with_index do |(x, y, radius), i|
173
+ if i == 0
174
+ cr.move_to(x, y)
175
+ else
176
+ # cr.arc(x, y, (i/4.0)*@pen.radius, 0, 2*PI)
177
+ cr.line_to(x, y)
178
+ end
179
+ end
180
+ cr.stroke
181
+ end
182
+ @portion = :all
183
+ else
184
+ if @portion == :first_half
185
+ path = @pen.path[0..4] || []
186
+ cr.set_source_rgba(@pen.color)
187
+ cr.line_width = @pen.radius
188
+ path.each.with_index do |(x, y, radius), i|
189
+ if i == 0
190
+ cr.move_to(x, y)
191
+ else
192
+ cr.line_to(x, y)
193
+ end
194
+ end
195
+ cr.stroke
196
+ @portion = :all
197
+ end
198
+ end
199
+ signal_emit('changed')
200
+ end
201
+
202
+ def pen_move(ev)
203
+ @pen.move(ev)
204
+ update
205
+
206
+ signal_emit('changed')
207
+ end
208
+ end
209
+
210
+ class SheetView < Gtk::DrawingArea
211
+ include Scratchpad
212
+
213
+ def initialize()
214
+ super()
215
+ self.app_paintable = true
216
+
217
+ set_size_request(800, 600)
218
+ signal_connect('expose-event') do
219
+ draw
220
+ true
221
+ end
222
+ last_point = nil
223
+ signal_connect('motion-notify-event') do |self_, ev|
224
+ ev.x = ev.x + 0.5
225
+ ev.y = ev.y + 0.5
226
+
227
+ c = Cairo::Context.new(@model.debug_surface)
228
+ c.set_source_rgba([0, 1, 0])
229
+ c.rectangle(ev.x - 1, ev.y - 1, 2, 2)
230
+ # c.set_source_rgba([0, 0, 1])
231
+ # c.rectangle(ev.x - 2, ev.y - 2, 4, 4)
232
+ c.fill
233
+ c.destroy
234
+
235
+ if last_point == nil
236
+ last_point = [ev.x, ev.y]
237
+ @model.pen_move(ev)
238
+ else
239
+ if distance(last_point, [ev.x, ev.y]) < (1.0/0.0)
240
+ last_point = midpoint(last_point, [ev.x, ev.y])
241
+ ev.x, ev.y = last_point
242
+ else
243
+ last_point = [ev.x, ev.y]
244
+ end
245
+ @model.pen_move(ev)
246
+ end
247
+ end
248
+ signal_connect('button-press-event') do |self_, ev|
249
+ c = Cairo::Context.new(@model.debug_surface)
250
+ c.set_source_rgba([1, 0, 0])
251
+ c.rectangle(ev.x - 2, ev.y - 2, 4, 4)
252
+ c.fill
253
+ c.destroy
254
+
255
+
256
+ if ev.button == 1
257
+ @model.pen_down(ev)
258
+ elsif ev.button == 3
259
+ @model.clear
260
+ end
261
+ end
262
+ signal_connect('button-release-event') do |self_, ev|
263
+ c = Cairo::Context.new(@model.debug_surface)
264
+ c.set_source_rgba([1, 0, 0])
265
+ c.rectangle(ev.x - 2, ev.y - 2, 4, 4)
266
+ c.fill
267
+ c.destroy
268
+
269
+ if ev.button == 1
270
+ @model.pen_move(ev)
271
+ @model.pen_up(ev)
272
+ end
273
+ end
274
+
275
+ @model = SheetModel.new
276
+ @model.signal_connect('changed') do
277
+ # invalidate
278
+ end
279
+
280
+ self.events |= Gdk::Event::BUTTON_PRESS_MASK |
281
+ Gdk::Event::BUTTON_RELEASE_MASK |
282
+ # Gdk::Event::POINTER_MOTION_HINT_MASK |
283
+ Gdk::Event::POINTER_MOTION_MASK
284
+
285
+ end
286
+
287
+ def draw
288
+ cr = window.create_cairo_context
289
+
290
+ cr.set_operator(Cairo::OPERATOR_SOURCE)
291
+ cr.set_source_rgba(1.0, 1.0, 1.0, 0.5)
292
+ cr.paint
293
+
294
+ cr.set_operator(Cairo::OPERATOR_OVER)
295
+ cr.set_source(@model.surface)
296
+ cr.paint
297
+ # cr.set_source(@model.debug_surface)
298
+ # cr.paint
299
+
300
+ cr.destroy
301
+ end
302
+
303
+ def invalidate
304
+ window.invalidate(window.clip_region, true)
305
+ window.process_updates(true)
306
+ end
307
+ end
308
+
309
+ class MainWindow < Gtk::Window
310
+ def initialize
311
+ super()
312
+
313
+ self.title = "Scratchpad"
314
+
315
+ set_rgba_colormap
316
+ signal_connect('screen-changed') do
317
+ set_rgba_colormap
318
+ end
319
+ end
320
+
321
+ def set_rgba_colormap
322
+ self.colormap = screen.rgba_colormap || screen.rgb_colormap
323
+ end
324
+ end
325
+
326
+ class Program
327
+ include Gtk
328
+
329
+ def initialize
330
+ @quit_requested = false
331
+
332
+ Signal.trap(:INT) {
333
+ STDERR.puts("Interrupted")
334
+ Gtk.main_quit
335
+ @quit_requested = true
336
+ }
337
+ end
338
+
339
+ def quit
340
+ @quit_requested = true
341
+ end
342
+
343
+ def run
344
+ win = MainWindow.new
345
+ win.signal_connect('delete-event') do
346
+ Gtk.main_quit
347
+ end
348
+
349
+ win.maximize
350
+ sheet = SheetView.new
351
+ win.add sheet
352
+ win.show_all
353
+
354
+ Gtk.timeout_add(33) do
355
+ sheet.invalidate
356
+ end
357
+
358
+ # until @quit_requested
359
+ # Gtk.main_iteration while Gtk.events_pending?
360
+ # end
361
+ Gtk.main
362
+ end
363
+ end
364
+
365
+ end
@@ -0,0 +1,132 @@
1
+ module Scratchpad
2
+
3
+ class Interpolator
4
+ include Math
5
+
6
+ class DataPoint < Struct.new(:v, :pos)
7
+ end
8
+
9
+ def initialize
10
+ @x_v = nil
11
+ @y_v = nil
12
+ @x = nil
13
+ @y = nil
14
+ @ring = [nil, nil, nil]
15
+ end
16
+
17
+ X = 0
18
+ Y = 1
19
+
20
+ # (x, y) -> [[Float, Float, Float]]
21
+ def feed(x, y, is_down)
22
+ raise unless @ring.size == 3
23
+
24
+ @ring.shift
25
+ @ring << [DataPoint.new(nil, x), DataPoint.new(nil, y), is_down]
26
+
27
+ if @ring[1] == nil
28
+ # cannot calculate yet
29
+ return []
30
+ else
31
+ # calculate @ring[1]'s velocity
32
+ if @ring[0] == nil
33
+ @ring[1][X].v = (@ring[2][X].pos - @ring[1][X].pos)
34
+ @ring[1][Y].v = (@ring[2][Y].pos - @ring[1][Y].pos)
35
+ return []
36
+ else
37
+ @ring[1][X].v = ((@ring[1][X].pos - @ring[0][X].pos) + (@ring[2][X].pos - @ring[1][X].pos)) * 0.5
38
+ @ring[1][Y].v = ((@ring[1][Y].pos - @ring[0][Y].pos) + (@ring[2][Y].pos - @ring[1][Y].pos)) * 0.5
39
+ # unless @ring[1][2] # is_down
40
+ # return []
41
+ # else
42
+ return interpolate
43
+ # end
44
+ end
45
+ end
46
+ end
47
+
48
+ # interpolate using @ring[0] and @ring[1]
49
+ def interpolate
50
+ a, b = find_accelerations(@ring[0][X].pos,
51
+ @ring[0][X].v,
52
+ @ring[1][X].pos,
53
+ @ring[1][X].v)
54
+ s = @ring[0][X].pos
55
+ velocity = @ring[0][X].v
56
+ xpoints = (0..10).map { |n| n*0.1 }.map do |t|
57
+ [s, velocity].tap do
58
+ s += 0.1 * velocity
59
+ if t < 0.5
60
+ velocity += a * 0.1
61
+ else
62
+ velocity += b * 0.1
63
+ end
64
+ end
65
+ end
66
+
67
+ a, b = find_accelerations(@ring[0][Y].pos,
68
+ @ring[0][Y].v,
69
+ @ring[1][Y].pos,
70
+ @ring[1][Y].v)
71
+ s = @ring[0][Y].pos
72
+ velocity = @ring[0][Y].v
73
+ ypoints = (0..10).map { |n| n*0.1 }.map do |t|
74
+ [s, velocity].tap do
75
+ s += 0.1 * velocity
76
+ if t < 0.5
77
+ velocity += a * 0.1
78
+ else
79
+ velocity += b * 0.1
80
+ end
81
+ end
82
+ end
83
+
84
+ return xpoints.zip(ypoints).map { |x_data, y_data|
85
+ [x_data[0], y_data[0], sqrt(x_data[1] ** 2 + y_data[1] ** 2)]
86
+ }
87
+ end
88
+
89
+ def find_accelerations(s, velocity, target_position, target_velocity)
90
+ a = 0.0
91
+ b = 0.0
92
+
93
+ s_ = s
94
+ velocity_ = velocity
95
+
96
+ loop do
97
+ s = s_
98
+ velocity = velocity_
99
+
100
+ (0..9).map { |n| n*0.1 }.map do |t|
101
+ [t, s].tap do
102
+ s += 0.1 * velocity
103
+ if t < 0.5
104
+ velocity += a * 0.1
105
+ else
106
+ velocity += b * 0.1
107
+ end
108
+ end
109
+ end
110
+
111
+ if (s - target_position).abs >= 0.001
112
+ a += -(s - target_position) * 1.0
113
+ elsif (velocity - target_velocity).abs >= 0.001
114
+ b += -(velocity - target_velocity) * 1.0
115
+ else
116
+ return a, b
117
+ end
118
+ end
119
+ end
120
+
121
+ end
122
+
123
+ if __FILE__ == $0
124
+ obj = Interpolator.new
125
+ p obj.feed(0.0, 0.0)
126
+ p obj.feed(1.0, 1.0)
127
+ p obj.feed(2.0, 2.0)
128
+ p obj.feed(5.0, 5.0)
129
+ p obj.feed(0.0, 0.0)
130
+ end
131
+
132
+ end