Zoomable TkCanvas?

Hello,

It looks like TkCanvas has no methods for zooming in and out. For Perl, I
have found the Tk::Worldcanvas and Tk::Abstractcanvas modules. Anybody knows
about something like that for ruby?

Josef Wolf wrote:

It looks like TkCanvas has no methods for zooming in and out. For Perl, I
have found the Tk::Worldcanvas and Tk::Abstractcanvas modules. Anybody knows
about something like that for ruby?

Those perl modules sound interesting--maybe it would be useful to have them in ruby if someone hasn't done that yet.

I've used Tk's Canvas from tcl and from ruby, and I've always had to implement zooming myself in wrapper classes. One way to do it is tag all canvas objects (or just the ones that zoom), and use the Canvas#scale method on that tag. You have to keep track of the current zoom level (Tk doesn't), and use that to calculate the arguments to #scale. You also have to adjust the scroll bars (using 'configure :scrollregion => ...'). Then use xview/yview to keep the current view position in sync with the zoom level.

I think that covers it, but if you're interested, take a look at canvas.rb in my tkar project. Tkar is a process, rather than a library, but it abstracts out details like zooming and provides a basic user interface for controlling zoom, pan, etc.

http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/343516

http://rubyforge.org/projects/tkar

···

--
       vjoel : Joel VanderWerf : path berkeley edu : 510 665 3407

Josef Wolf wrote:

It looks like TkCanvas has no methods for zooming in and out. For Perl,
I have found the Tk::Worldcanvas and Tk::Abstractcanvas modules. Anybody
knows about something like that for ruby?

Those perl modules sound interesting--maybe it would be useful to have
them in ruby if someone hasn't done that yet.

Yeah, they make life a whole lot easier. In addition to zooming, they:
- maintain original coordinates at all zoom factors. Thus even after
   zoom operations, events as well as querying/moving/adding items are
   done as if the canvas is at zoom factor 1.0. So zoom is handled
   completely transparent to the user of the module..
- turn around the y-axis (origin is at left bottom)
- handle scroll bars + panning

I've used Tk's Canvas from tcl and from ruby, and I've always had to
implement zooming myself in wrapper classes.

Strange, that such a powerful widget is missing such a basic functionality.

One way to do it is tag all
canvas objects (or just the ones that zoom), and use the Canvas#scale
method on that tag. You have to keep track of the current zoom level (Tk
doesn't), and use that to calculate the arguments to #scale. You also have
to adjust the scroll bars (using 'configure :scrollregion => ...'). Then
use xview/yview to keep the current view position in sync with the zoom
level.

Sounds easy enough. Is it really that easy? I think this would work only
if you do not add or move any items after you have made zoom operations.
Looks like above mentioned modules do a whole lot more of work. They
override all of the item creation and modification methods to fix movement
or addition of new items to the current zoom factor.

I think that covers it, but if you're interested, take a look at canvas.rb
in my tkar project. Tkar is a process, rather than a library, but it
abstracts out details like zooming and provides a basic user interface for
controlling zoom, pan, etc.

http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/343516

http://rubyforge.org/projects/tkar

Sounds very interesting.

Thanks, I'll check that out!

···

On Tue, Sep 08, 2009 at 04:26:32AM +0900, Joel VanderWerf wrote:

Josef Wolf wrote:

- maintain original coordinates at all zoom factors. Thus even after
   zoom operations, events as well as querying/moving/adding items are
   done as if the canvas is at zoom factor 1.0. So zoom is handled
   completely transparent to the user of the module..

Yep, tkar does that too--object coordinates are independent of zoom level. TkCanvas coordinates are hidden in the abstraction.

- turn around the y-axis (origin is at left bottom)

Tkar has an option to flip the y-axis (and also an option to use radians instead of degrees for rotation commands).

- handle scroll bars + panning

Check.

One way to do it is tag all canvas objects (or just the ones that zoom), and use the Canvas#scale method on that tag. You have to keep track of the current zoom level (Tk doesn't), and use that to calculate the arguments to #scale. You also have to adjust the scroll bars (using 'configure :scrollregion => ...'). Then use xview/yview to keep the current view position in sync with the zoom level.

Sounds easy enough. Is it really that easy? I think this would work only
if you do not add or move any items after you have made zoom operations.

You are right, that's just the basic idea... tkar does coordinate transforms for all operations (move, rotate, add).

···

--
       vjoel : Joel VanderWerf : path berkeley edu : 510 665 3407

Josef Wolf wrote:

- maintain original coordinates at all zoom factors. Thus even after
   zoom operations, events as well as querying/moving/adding items are
   done as if the canvas is at zoom factor 1.0. So zoom is handled
   completely transparent to the user of the module..

Yep, tkar does that too--object coordinates are independent of zoom level.
TkCanvas coordinates are hidden in the abstraction.

Hmm, somehow I fail to see how this is supposed to work.

I see zoom_by does the scaling, adjusts the scrollregion and updates the
view. IMHO, to keep coordinates independent from zoom level, you would need
to intercept query/movement/creation of the items. Without that

TkcRectangle.new(canvas, [100,100], [300, 200])
canvas.zoom_by(2.0)
TkcRectangle.new(canvas, [100,100], [300, 200])

would result in two rectangles with different sizes.

- turn around the y-axis (origin is at left bottom)

Tkar has an option to flip the y-axis (and also an option to use radians
instead of degrees for rotation commands).

- handle scroll bars + panning

Check.

This is done with the help of the Window class, AFAICS. So it is not as
transparent as the Tk::AbstractCanvas module.

···

On Tue, Sep 08, 2009 at 07:35:31AM +0900, Joel VanderWerf wrote:

Josef Wolf wrote:

Josef Wolf wrote:

- maintain original coordinates at all zoom factors. Thus even after
   zoom operations, events as well as querying/moving/adding items are
   done as if the canvas is at zoom factor 1.0. So zoom is handled
   completely transparent to the user of the module..

Yep, tkar does that too--object coordinates are independent of zoom level. TkCanvas coordinates are hidden in the abstraction.

Hmm, somehow I fail to see how this is supposed to work.

I see zoom_by does the scaling, adjusts the scrollregion and updates the
view. IMHO, to keep coordinates independent from zoom level, you would need
to intercept query/movement/creation of the items. Without that

TkcRectangle.new(canvas, [100,100], [300, 200])
canvas.zoom_by(2.0)
TkcRectangle.new(canvas, [100,100], [300, 200])

would result in two rectangles with different sizes.

Tkar is intended to be used as a _process_ not as a library. Another process (doesn't have to be ruby, doesn't have to be a Tk gui) sends commands to tkar over a pipe or socket. Those commands use the abstract coordinate system.

If you use the _Tk_ methods such as TkcRectangle.new, they will use Tk's native coordinates.

The corresponding methods in Tkar are in the primitives.rb file. For example the #rect method. This method understands scaling. It also understands rotation, which Tk primitives do not. These methods aren't designed to be used as a library, though.

This is done with the help of the Window class, AFAICS. So it is not as
transparent as the Tk::AbstractCanvas module.

Different kind of abstraction here--tkar implements a little language to drive animations over IO, it's not a library API.

I think a ruby port of the perl Tk::AbstractCanvas would be useful, but in a different way from tkar. I wrote tkar primarily so that I could do 2D animations in simulink--a ruby library isn't much use for that, but a socket interface is fine (and has the advantage of distributing workload). Also, with a little munging, you can pipe the output of real-time log files and get useful animations. See ps.rb for an example--it filters the output of ps to show a graphical representation of the cpu usage of running processes.

···

On Tue, Sep 08, 2009 at 07:35:31AM +0900, Joel VanderWerf wrote:

--
       vjoel : Joel VanderWerf : path berkeley edu : 510 665 3407

Josef Wolf wrote:

Josef Wolf wrote:

- maintain original coordinates at all zoom factors. Thus even after
   zoom operations, events as well as querying/moving/adding items are
   done as if the canvas is at zoom factor 1.0. So zoom is handled
   completely transparent to the user of the module..

Yep, tkar does that too--object coordinates are independent of zoom level. TkCanvas coordinates are hidden in the abstraction.

Hmm, somehow I fail to see how this is supposed to work.

I see zoom_by does the scaling, adjusts the scrollregion and updates the
view. IMHO, to keep coordinates independent from zoom level, you would need
to intercept query/movement/creation of the items. Without that

TkcRectangle.new(canvas, [100,100], [300, 200])
canvas.zoom_by(2.0)
TkcRectangle.new(canvas, [100,100], [300, 200])

would result in two rectangles with different sizes.

Tkar is intended to be used as a _process_ not as a library. Another process (doesn't have to be ruby, doesn't have to be a Tk gui) sends commands to tkar over a pipe or socket. Those commands use the abstract coordinate system.

If you use the _Tk_ methods such as TkcRectangle.new, they will use Tk's native coordinates.

The corresponding methods in Tkar are in the primitives.rb file. For example the #rect method. This method understands scaling. It also understands rotation, which Tk primitives do not. These methods aren't designed to be used as a library, though.

This is done with the help of the Window class, AFAICS. So it is not as
transparent as the Tk::AbstractCanvas module.

Different kind of abstraction here--tkar implements a little language to drive animations over IO, it's not a library API.

I think a ruby port of the perl Tk::AbstractCanvas would be useful, but in a different way from tkar. I wrote tkar primarily so that I could do 2D animations in simulink--a ruby library isn't much use for that, but a socket interface is fine (and has the advantage of distributing workload). Also, with a little munging, you can pipe the output of real-time log files and get useful animations. See ps.rb for an example--it filters the output of ps to show a graphical representation of the cpu usage of running processes.

···

On Tue, Sep 08, 2009 at 07:35:31AM +0900, Joel VanderWerf wrote:

--
       vjoel : Joel VanderWerf : path berkeley edu : 510 665 3407

So OK. I thought, although I'm a complete newbie to ruby, I'd try to roll
my own. At least, that would result in a good exercise. So I started by
stealing the basics for a scrolled canvas from

http://blade.nagaokaut.ac.jp/cgi-bin/vframe.rb/ruby/ruby-talk/122597?122482-123428

and applying this patch:

--- lib/scrolledcanvas.rb.orig
+++ lib/scrolledcanvas.rb
@@ -6,6 +6,8 @@
   include TkComposite

   def initialize_composite(keys={})
+ @zoom = 1.0 # need this for the zoom_by method

···

On Wed, Sep 09, 2009 at 02:05:34AM +0900, Joel VanderWerf wrote:

Josef Wolf wrote:

On Tue, Sep 08, 2009 at 07:35:31AM +0900, Joel VanderWerf wrote:

Josef Wolf wrote:

- maintain original coordinates at all zoom factors. Thus even after
   zoom operations, events as well as querying/moving/adding items are
   done as if the canvas is at zoom factor 1.0. So zoom is handled
   completely transparent to the user of the module..

Yep, tkar does that too--object coordinates are independent of zoom
level. TkCanvas coordinates are hidden in the abstraction.

I see zoom_by does the scaling, adjusts the scrollregion and updates the
view. IMHO, to keep coordinates independent from zoom level, you would need
to intercept query/movement/creation of the items. Without that

TkcRectangle.new(canvas, [100,100], [300, 200])
canvas.zoom_by(2.0)
TkcRectangle.new(canvas, [100,100], [300, 200])

would result in two rectangles with different sizes.

[ ... ]
I think a ruby port of the perl Tk::AbstractCanvas would be useful, but in
a different way from tkar. [ ... ]

+
     @h_scr = TkScrollbar.new(@frame)
     @v_scr = TkScrollbar.new(@frame)

@@ -23,7 +25,7 @@
     @v_scr.grid(:row=>0, :column=>1, :sticky=>'ns')

     delegate('DEFAULT', @canvas)
- delegate('background', @text, @h_scr, @v_scr)
+ delegate('background', @frame, @h_scr, @v_scr) # looked like a typo
     delegate('activeforeground', @h_scr, @v_scr)
     delegate('troughcolor', @h_scr, @v_scr)
     delegate('repeatdelay', @h_scr, @v_scr)

Then, I copied the zoon_by, xview and yview methods from your tkar package
and commented the call to adjust_scrollregion to avoid access to the
uninitialized @bounds array.

So at this stage, I have a canvas that can be scrolled and zoomed. Fine.

But how do I override the methods to create the items? In Perl/Tk, that
would be easy, since item creation is done via canvas methods. But in
Ruby/Tk, items are created via their own classes (e.g. TkcLine.new(args)
or something). There don't seem to exist methods in the Canvas class to
create items, which could easily be overridden.

Any hints?

PS: here's the current state of affairs:

#!/usr/bin/env ruby

require 'tk'

class TkScrolledCanvas < TkCanvas
  include TkComposite

  def initialize_composite(keys={})
    @zoom = 1.0

    @h_scr = TkScrollbar.new(@frame)
    @v_scr = TkScrollbar.new(@frame)

    @canvas = TkCanvas.new(@frame)
    @path = @canvas.path

    @canvas.xscrollbar(@h_scr)
    @canvas.yscrollbar(@v_scr)

    TkGrid.rowconfigure(@frame, 0, :weight=>1, :minsize=>0)
    TkGrid.columnconfigure(@frame, 0, :weight=>1, :minsize=>0)

    @canvas.grid(:row=>0, :column=>0, :sticky=>'news')
    @h_scr.grid(:row=>1, :column=>0, :sticky=>'ew')
    @v_scr.grid(:row=>0, :column=>1, :sticky=>'ns')

    delegate('DEFAULT', @canvas)
    delegate('background', @text, @h_scr, @v_scr)
    delegate('activeforeground', @h_scr, @v_scr)
    delegate('troughcolor', @h_scr, @v_scr)
    delegate('repeatdelay', @h_scr, @v_scr)
    delegate('repeatinterval', @h_scr, @v_scr)
    delegate('borderwidth', @frame)
    delegate('relief', @frame)

    delegate_alias('canvasborderwidth', 'borderwidth', @canvas)
    delegate_alias('canvasrelief', 'relief', @canvas)

    delegate_alias('scrollbarborderwidth', 'borderwidth', @h_scr, @v_scr)
    delegate_alias('scrollbarrelief', 'relief', @h_scr, @v_scr)

    configure(keys) unless keys.empty?
  end

  def zoom_by zf
    zf = Float(zf)
    @zoom *= zf
         
    vf = (1 - 1/zf) / 2
         
    x0, x1 = xview ; xf = x0 + vf * (x1-x0)
    y0, y1 = yview ; yf = y0 + vf * (y1-y0)
   
    scale 'all', 0, 0, zf, zf
    adjust_scrollregion
   
    xview "moveto", xf
    yview "moveto", yf
  end

  def adjust_scrollregion
# configure :scrollregion => @bounds.map {|u|u*@zoom}
    ## if all of canvas can be shown, hide the scroll bars
  end

  def xview(mode=nil, *args)
    if mode and mode == "scroll" and @follow_xdelta
      number, what = args
      x_pre, = xview
      r = super(mode, *args)
      x_post, = xview
      x0,y0,x1,y1 = @bounds
      @follow_xdelta += (x_post - x_pre) * (x1-x0)
      r
    elsif not mode
      super()
    else
      super(mode, *args)
    end
  end
  
  def yview(mode=nil, *args)
    if mode and mode == "scroll" and @follow_ydelta
      number, what = args
      y_pre, = yview
      r = super(mode, *args)
      y_post, = yview
      x0,y0,x1,y1 = @bounds
      @follow_ydelta += (y_post - y_pre) * (y1-y0)
      r
    elsif not mode
      super()
    else
      super(mode, *args)
    end
  end
end

root = TkRoot.new { title "zoomcanvas" }

c = TkScrolledCanvas.new(:scrollregion=>[0,0,500,400],
                         :relief=>"sunken").pack(:expand=>1,:fill=>"both")
TkcRectangle.new(c, [100,100], [300, 200])
c.bind("1", proc{|e| TkcRectangle.new(c, [100,100], [300, 200]) })
root.bind("z") { c.zoom_by(1.5) }
root.bind("Z") { c.zoom_by(1/1.5) }

Tk.mainloop

Josef Wolf wrote:

But how do I override the methods to create the items? In Perl/Tk, that
would be easy, since item creation is done via canvas methods. But in
Ruby/Tk, items are created via their own classes (e.g. TkcLine.new(args)
or something). There don't seem to exist methods in the Canvas class to
create items, which could easily be overridden.

Maybe subclass (or delegate to) the Tk classes:

class MyRectangle < TkcRectangle
   def initialize(x,y,w,h)
     super(...) # adjust args depending on zoom level etc.
   end
end

···

--
       vjoel : Joel VanderWerf : path berkeley edu : 510 665 3407

Thanks for your patience with me, Joel!

Josef Wolf wrote:

But how do I override the methods to create the items? In Perl/Tk, that
would be easy, since item creation is done via canvas methods. But in
Ruby/Tk, items are created via their own classes (e.g. TkcLine.new(args)
or something). There don't seem to exist methods in the Canvas class to
create items, which could easily be overridden.

Maybe subclass (or delegate to) the Tk classes:

class MyRectangle < TkcRectangle
  def initialize(x,y,w,h)
    super(...) # adjust args depending on zoom level etc.
  end
end

That would result in:
- lots of new subclasses, polluting namespaces
- lots of code duplication
- it would not be a drop-in replacement: one would have to change the
   class names of the items when switching from standard canvas to the
   improved canvas

Maybe extending TkcItem would be a better solution. Something like:

   class TkcItem
     alias orig_initialize initialize
     def initialize(parent, *args)
       # do whatever we need
       orig_initialize parent, args
     end
   end

Opinions?

···

On Wed, Sep 09, 2009 at 05:29:47AM +0900, Joel VanderWerf wrote:

Josef Wolf wrote:

Thanks for your patience with me, Joel!

Josef Wolf wrote:

But how do I override the methods to create the items? In Perl/Tk, that
would be easy, since item creation is done via canvas methods. But in
Ruby/Tk, items are created via their own classes (e.g. TkcLine.new(args)
or something). There don't seem to exist methods in the Canvas class to
create items, which could easily be overridden.

Maybe subclass (or delegate to) the Tk classes:

class MyRectangle < TkcRectangle
  def initialize(x,y,w,h)
    super(...) # adjust args depending on zoom level etc.
  end
end

That would result in:
- lots of new subclasses, polluting namespaces
- lots of code duplication
- it would not be a drop-in replacement: one would have to change the
   class names of the items when switching from standard canvas to the
   improved canvas

It's a matter of taste, I suppose. Preserving the existing Tk classes has an advantage: if you want to place objects on the canvas that are not affected by zoooming (foreground, OSD-type display, controls, etc), you can still use the base classes.

Keep namespaces clean by putting everything in a module:

module MyTk
   class TkcRectangle < ::TkcRectangle
   ...

That way, you have both:

MyTk::TkcRectangle.new # new kind
TkcRectangle # old kind
::TkcRectangle # old kind even when in MyTk scope

I don't see a problem with new subclasses or code duplication.

···

On Wed, Sep 09, 2009 at 05:29:47AM +0900, Joel VanderWerf wrote:

--
       vjoel : Joel VanderWerf : path berkeley edu : 510 665 3407