lib/scratchpad.rb in scratchpad-app-0.1.1 vs lib/scratchpad.rb in scratchpad-app-0.1.2
- old
+ new
@@ -2,10 +2,28 @@
require "scratchpad/version"
require 'toml'
require 'gtk2'
+class Array
+ def x
+ self[0]
+ end
+
+ def x=(val)
+ self[0]=val
+ end
+
+ def y
+ self[1]
+ end
+
+ def y=(val)
+ self[1]=val
+ end
+end
+
module Scratchpad
# 便利関数
def distance(p1, p2)
x1, y1 = p1
@@ -33,25 +51,48 @@
raise 'wrong dimension' unless bytes.size.between?(3, 4)
return bytes.map { |b| b / 255.0 }
end
+def plus(v1, v2)
+ [v1.x + v2.x, v1.y + v2.y]
+end
+
+def vector(p1, p2)
+ [p2.x - p1.x, p2.y - p1.y]
+end
+
+def dot_product(v1, v2)
+ return v1.x * v2.x + v1.y * v2.y
+end
+
+def scalar_times(factor, v)
+ v.map { |x| factor * x }
+end
+
+def vec_lerp(alpha, v1, v2)
+ return plus(scalar_times(alpha, v1), scalar_times(1.0-alpha, v2))
+end
+
# /便利関数
class Pen
include Math
extend Scratchpad
attr_reader :x, :y
+ attr_accessor :color
- FAVORITE_ORANGE = color_bytes_to_floats [255, 109, 50]
- FAVORITE_BLUE = color_bytes_to_floats [0, 3, 126]
+ BLUE = color_bytes_to_floats [0, 3, 126]
+ ORANGE = color_bytes_to_floats [255, 109, 50]
+ GREEN = color_bytes_to_floats [0, 124, 126]
def initialize
@is_down = false
@interpolator = Interpolator.new
@path = []
+ @color = BLUE
end
# [[x, y, radius]*]
def path
f = proc { |x|
@@ -80,29 +121,27 @@
end
def radius
2.0
end
-
- def color
- FAVORITE_BLUE
- end
end
class SheetModel < GLib::Object
type_register
signal_new('changed', GLib::Signal::ACTION, nil, nil)
include Math
include Cairo
- attr_reader :surface, :pen, :debug_surface
+ attr_reader :surface, :pen, :debug_surface, :outline_surface
- def initialize
+ def initialize(width, height)
super()
- @surface = ImageSurface.new(Cairo::FORMAT_ARGB32, 1200, 900)
- @debug_surface = ImageSurface.new(Cairo::FORMAT_ARGB32, 1200, 900)
+ @surface = ImageSurface.new(Cairo::FORMAT_ARGB32, width, height)
+ @debug_surface = ImageSurface.new(Cairo::FORMAT_ARGB32, width, height)
+ @outline_surface = ImageSurface.new(Cairo::FORMAT_ARGB32, width, height)
+
cr = Context.new(@surface)
cr.set_operator(Cairo::OPERATOR_OVER)
cr.line_cap = cr.line_join = :round
@context = cr
@@ -114,21 +153,18 @@
def cr
@context
end
def clear
- cr.save do
- cr.set_operator(Cairo::OPERATOR_SOURCE)
- cr.set_source_rgba(0, 0, 0, 0)
- cr.paint
- end
- clear_debug
+ clear_surface(@surface)
+ clear_surface(@outline_surface)
+ clear_surface(@debug_surface)
signal_emit('changed')
end
- def clear_debug
- c = Context.new(@debug_surface)
+ def clear_surface(surface)
+ c = Context.new(surface)
c.save do
c.set_operator(Cairo::OPERATOR_SOURCE)
c.set_source_rgba(0, 0, 0, 0)
c.paint
end
@@ -153,158 +189,253 @@
def update
if @pen.down?
case @portion
when :all
path = @pen.path
+
+ stroke_outline(path)
+
cr.set_source_rgba(@pen.color)
cr.line_width = @pen.radius
- path.each.with_index do |(x, y, radius), i|
- if i == 0
- cr.move_to(x, y)
- else
- cr.line_to(x, y)
- end
- end
- cr.stroke
+ stroke_path(cr, path)
when :latter_half
path = @pen.path[5..10] || []
+
+ stroke_outline(path)
+
cr.set_source_rgba(@pen.color)
cr.line_width = @pen.radius
- path.each.with_index do |(x, y, radius), i|
- if i == 0
- cr.move_to(x, y)
- else
- # cr.arc(x, y, (i/4.0)*@pen.radius, 0, 2*PI)
- cr.line_to(x, y)
- end
- end
- cr.stroke
+ stroke_path(cr, path)
end
@portion = :all
+ signal_emit('changed')
else
if @portion == :first_half
path = @pen.path[0..4] || []
+
+ stroke_outline(path)
+
cr.set_source_rgba(@pen.color)
cr.line_width = @pen.radius
- path.each.with_index do |(x, y, radius), i|
- if i == 0
- cr.move_to(x, y)
- else
- cr.line_to(x, y)
- end
- end
- cr.stroke
+ stroke_path(cr, path)
+
@portion = :all
+ signal_emit('changed')
end
end
- signal_emit('changed')
end
+ def stroke_outline(path)
+ cr = Cairo::Context.new(@outline_surface)
+ cr.line_cap = cr.line_join = :round
+ cr.set_source_rgba([1, 1, 1, 1.0])
+ cr.line_width = @pen.radius * 3
+ stroke_path(cr, path)
+ end
+
+ def stroke_path(cr, path)
+ path.each.with_index do |(x, y, radius), i|
+ if i == 0
+ cr.move_to(x, y)
+ else
+ cr.line_to(x, y)
+ end
+ end
+ cr.stroke
+ end
+
def pen_move(ev)
@pen.move(ev)
update
-
- signal_emit('changed')
end
end
class SheetView < Gtk::DrawingArea
include Scratchpad
def initialize()
super()
self.app_paintable = true
+ @dirty = true
+
set_size_request(800, 600)
signal_connect('expose-event') do
draw
true
end
- last_point = nil
- signal_connect('motion-notify-event') do |self_, ev|
- ev.x = ev.x + 0.5
- ev.y = ev.y + 0.5
- c = Cairo::Context.new(@model.debug_surface)
- c.set_source_rgba([0, 1, 0])
- c.rectangle(ev.x - 1, ev.y - 1, 2, 2)
- # c.set_source_rgba([0, 0, 1])
- # c.rectangle(ev.x - 2, ev.y - 2, 4, 4)
- c.fill
- c.destroy
-
- if last_point == nil
- last_point = [ev.x, ev.y]
- @model.pen_move(ev)
- else
- if distance(last_point, [ev.x, ev.y]) < (1.0/0.0)
- last_point = midpoint(last_point, [ev.x, ev.y])
- ev.x, ev.y = last_point
- else
- last_point = [ev.x, ev.y]
- end
- @model.pen_move(ev)
- end
+ # モーションノティファイ。
+ @last_point = nil
+ signal_connect('motion-notify-event') do |self_, ev|
+ handle_motion_notify_event(ev)
end
+
+ # ボタンプレス。このイベントは同じ座標へのモーションノティファイの
+ # 後に来る。
signal_connect('button-press-event') do |self_, ev|
+ # デバッグ描画。ボタン押下座標に赤の四角を描く。
c = Cairo::Context.new(@model.debug_surface)
c.set_source_rgba([1, 0, 0])
c.rectangle(ev.x - 2, ev.y - 2, 4, 4)
c.fill
c.destroy
-
if ev.button == 1
@model.pen_down(ev)
+ elsif ev.button == 2
+ # 押し間違いを防ぐ。
+ unless @model.pen.down?
+ @model.clear
+ end
elsif ev.button == 3
- @model.clear
+ # 押し間違いを防ぐ。
+ unless @model.pen.down?
+ menu_popup(ev)
+ end
end
end
+
+ # ボタンリリース。このイベントは同じ座標へのモーションノティファイ
+ # の後に来る。
signal_connect('button-release-event') do |self_, ev|
+ # デバッグ描画。ボタンリリース座標に赤の四角を描く。
c = Cairo::Context.new(@model.debug_surface)
c.set_source_rgba([1, 0, 0])
c.rectangle(ev.x - 2, ev.y - 2, 4, 4)
c.fill
c.destroy
- if ev.button == 1
+ if ev.button == 1 && @model.pen.down?
@model.pen_move(ev)
@model.pen_up(ev)
end
end
- @model = SheetModel.new
+ @model = SheetModel.new(screen.width, screen.height)
@model.signal_connect('changed') do
- # invalidate
+ @dirty = true
end
+ # POINTER_MOTION_HINT_MASKを付けるとこちらの反応が遅い場合にポイン
+ # ター座標を省略する。
self.events |= Gdk::Event::BUTTON_PRESS_MASK |
Gdk::Event::BUTTON_RELEASE_MASK |
# Gdk::Event::POINTER_MOTION_HINT_MASK |
Gdk::Event::POINTER_MOTION_MASK
end
+ def tick
+ return if @last_motion_notify_event.nil?
+
+ t = Time.now
+ if (t - @last_motion_notify_event_time) > 0.033
+ handle_motion_notify_event(@last_motion_notify_event)
+ @last_motion_notify_event_time = t
+ end
+ end
+
+ def handle_motion_notify_event(ev)
+ @last_motion_notify_event = ev.dup
+ @last_motion_notify_event_time = Time.now
+
+ ev.x = ev.x + 0.5
+ ev.y = ev.y + 0.5
+
+ # デバッグ描画。ポインター座標に緑の四角を描く。
+ c = Cairo::Context.new(@model.debug_surface)
+ c.set_source_rgba([0, 1, 0])
+ c.rectangle(ev.x - 1, ev.y - 1, 2, 2)
+ c.fill
+ c.destroy
+
+ if @last_point == nil
+ @last_point = [ev.x, ev.y]
+ @model.pen_move(ev)
+ else
+ # 1サンプル時間あたり距離30以上移動した場合は完全にポインター
+ # 座標にならう。
+ d = distance(@last_point, [ev.x, ev.y])
+ alpha = if d <= 5.0 then 0.30 else 0.60 end
+ p [d, alpha] if $DEBUG
+ @last_point = vec_lerp(alpha, [ev.x, ev.y], @last_point)
+ ev.x, ev.y = @last_point
+ @model.pen_move(ev)
+ end
+ end
+
+ def dirty?
+ @dirty
+ end
+
+ def create_context_menu
+ menu = Gtk::Menu.new
+
+ item1 = Gtk::MenuItem.new('クリア')
+ item1.signal_connect('activate') do
+ @model.clear
+ end
+ menu.append(item1)
+
+ menu.append(Gtk::MenuItem.new)
+
+ item2 = Gtk::MenuItem.new('青')
+ item2.signal_connect('activate') do
+ @model.pen.color = Pen::BLUE
+ end
+ menu.append(item2)
+
+ item3 = Gtk::MenuItem.new('オレンジ')
+ item3.signal_connect('activate') do
+ @model.pen.color = Pen::ORANGE
+ end
+ menu.append(item3)
+
+ item4 = Gtk::MenuItem.new('緑')
+ item4.signal_connect('activate') do
+ @model.pen.color = Pen::GREEN
+ end
+ menu.append(item4)
+
+ menu.show_all
+
+ return menu
+ end
+
+ def menu_popup(button_event)
+ menu = create_context_menu
+ menu.popup(nil, nil, button_event.button, button_event.time)
+ end
+
def draw
cr = window.create_cairo_context
cr.set_operator(Cairo::OPERATOR_SOURCE)
cr.set_source_rgba(1.0, 1.0, 1.0, 0.5)
cr.paint
cr.set_operator(Cairo::OPERATOR_OVER)
+
+ cr.set_source(@model.outline_surface)
+ cr.paint
+
cr.set_source(@model.surface)
cr.paint
- # cr.set_source(@model.debug_surface)
- # cr.paint
+ if $DEBUG
+ cr.set_source(@model.debug_surface)
+ cr.paint
+ end
+
cr.destroy
end
def invalidate
window.invalidate(window.clip_region, true)
window.process_updates(true)
+ @dirty = false
end
end
class MainWindow < Gtk::Window
def initialize
@@ -325,40 +456,38 @@
class Program
include Gtk
def initialize
- @quit_requested = false
-
Signal.trap(:INT) {
STDERR.puts("Interrupted")
- Gtk.main_quit
- @quit_requested = true
+ quit
}
end
def quit
- @quit_requested = true
+ Gtk.main_quit
end
def run
win = MainWindow.new
win.signal_connect('delete-event') do
- Gtk.main_quit
+ quit
end
win.maximize
sheet = SheetView.new
win.add sheet
win.show_all
Gtk.timeout_add(33) do
- sheet.invalidate
+ sheet.tick
+ if sheet.dirty?
+ sheet.invalidate
+ end
+ true
end
- # until @quit_requested
- # Gtk.main_iteration while Gtk.events_pending?
- # end
Gtk.main
end
end
end