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