scratchpad-app 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE +674 -0
- data/README.md +19 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/scratchpad +5 -0
- data/lib/scratchpad.rb +365 -0
- data/lib/scratchpad/interpolator.rb +132 -0
- data/lib/scratchpad/version.rb +3 -0
- data/scratchpad.gemspec +27 -0
- metadata +128 -0
data/README.md
ADDED
@@ -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
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -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
|
data/bin/setup
ADDED
data/exe/scratchpad
ADDED
data/lib/scratchpad.rb
ADDED
@@ -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
|