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.
- 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
|