RandWeav: How Does It Work?

Under ipl/gprogs/ is a program randweav.icn. This program presents a small GUI, allowing the user to generate various kinds of weaving patterns. It is a typical example of a small GUI program with some vidgets for inputting data / providing information and a main panel with a drawn display.

Here, I have rewritten the program to construct the GUI using hand-coded vidgets in place of vsetup, and tried to explain how I understand the important graphical/GUI elements.

The original display of randweav is:

Constructing the GUI

The basic interface consists of some input text and toggle buttons at the top of the screen, and buttons to render, save or quit around the central area. The central area is a pane onto which is drawn the pattern - drawing the pattern is discussed in the next section.

Setting up the interface is straightforward: for each input vidget, we save the vidget into a global variable, for later access.

  win := Window ! put(["size=380,492", "bg=pale gray", "label=weaver"], args) # open window
  root := Vroot_frame(win)
  VSetFont(win)

  # top-left are three input values
  VInsert(root,
    colours_text := Vtext(win, "Colours: \\=6", , "colours", 3), # default value of 6
    10, 9, 87, 19)
  VInsert(root,
    vcyclic_toggle := Vtoggle(win, "cycle warp", , "vcyclic", "checkno", 97, 17),
    5, 36)
  VInsert(root,
    hcyclic_toggle := Vtoggle(win, "cycle weft", , "hcyclic", "checkno", 97, 20),
    5, 56)

  # top-centre is a button to redraw the display
  VInsert(root,
    Vbutton(win, "RENDER", render, "render", "regular", 72, 36),
    159, 24)

  # top-right are three input values
  VInsert(root,
    side_text := Vtext(win, "Side:   \\=90", , "side", 3),
    285, 8, 87, 19)
  VInsert(root,
    bias_text := Vtext(win, "Bias:   \\=60", , "bias", 3),
    285, 37, 87, 19)
  VInsert(root,
    perfect_toggle := Vtoggle(win, "perfect", , "perfect", "checkno", 76, 20),
    281, 57)

  # two buttons at the base of the display
  VInsert(root,
    Vbutton(win, "save   @S", save_image, "save", "regular", 78, 20),
    8, 462)
  VInsert(root,
    Vbutton(win, "quit   @Q", quit, "quit", "regular", 78, 20),
    293, 462)

  # draw an outline around the RENDER button
  VInsert(root,
    Vpane(win, , "outline", "sunken"),
    153, 18, 84, 48)
  # drawn an outline for the drawing region
  VInsert(root,
    region := Vpane(win, , "region", "grooved"),
    8, 84, 364, 364)

  VResize(root)

Some notes:

The design of the interface can be seen in the following screenshot, which does not draw the pattern.

Events are handled in the all procedure which captures some key events, and by buttons directly calling the render/save_image/quit procedures. It's worth also noting the save_image procedure, where a file dialog is opened and an image saved.

OpenDialog TODO

Handling the Graphics

Hidden Window

Drawing the Image

Showing the Image on main Window

Saving the Image to a file

Revised Program

link random
link vsetup

global root                           # root vidget
global hcyclic_toggle
global vcyclic_toggle
global perfect_toggle
global colours_text, side_text, bias_text
global region                         # pattern region

global hidwin                         # hidden window for saving to file

global allcolours                     # string of all palette colours

global maxsiz                         # maximum pattern size
global patsize                        # pattern size selected

$define PALETTE "c1"                  # colour palette
$define PREFCOLOURS "06NBCDFsHIJM?!"  # preferred colours

procedure main(args)
  local win

  randomize()
  allcolours := PREFCOLOURS || (PaletteChars(PALETTE) -- PREFCOLOURS)

  win := Window ! put(["size=380,492", "bg=pale gray", "label=weaver"], args) # open window
  root := Vroot_frame(win)
  VSetFont(win)

  VInsert(root,
    colours_text := Vtext(win, "Colours: \\=6", , "colours", 3),
    10, 9, 87, 19)
  VInsert(root,
    vcyclic_toggle := Vtoggle(win, "cycle warp", , "vcyclic", "checkno", 97, 17),
    5, 36)
  VInsert(root,
    hcyclic_toggle := Vtoggle(win, "cycle weft", , "hcyclic", "checkno", 97, 20),
    5, 56)

  VInsert(root,
    Vbutton(win, "RENDER", render, "render", "regular", 72, 36),
    159, 24)

  VInsert(root,
    side_text := Vtext(win, "Side:   \\=90", , "side", 3),
    285, 8, 87, 19)
  VInsert(root,
    bias_text := Vtext(win, "Bias:   \\=60", , "bias", 3),
    285, 37, 87, 19)
  VInsert(root,
    perfect_toggle := Vtoggle(win, "perfect", , "perfect", "checkno", 76, 20),
    281, 57)

  VInsert(root,
    Vbutton(win, "save   @S", save_image, "save", "regular", 78, 20),
    8, 462)
  VInsert(root,
    Vbutton(win, "quit   @Q", quit, "quit", "regular", 78, 20),
    293, 462)

  VInsert(root,
    Vpane(win, , "outline", "sunken"),
    153, 18, 84, 48)
  VInsert(root,
    region := Vpane(win, , "region", "grooved"),
    8, 84, 364, 364)

  VResize(root)

  VSetState(vcyclic_toggle, 1)  # default "cycle warp" on
  VSetState(hcyclic_toggle, 1)  # default "cycle weft" on

  hidwin := WOpen("canvas=hidden",  # open hidden window
     "width=" || region.uw, "height=" || region.uh)

  maxsiz := region.uw           # set maximum size
  maxsiz >:= region.uh

  render()                      # draw once without prompting
  GetEvents(root, , all)        # then wait for events
end

#  all(a, x, y) -- process all events, checking for keyboard shortcuts
procedure all(a, x, y)
   if a === !" \n\r" then render() # draw new pattern for SPACE, CR, LF
   else if &meta then case a of {
      !"qQ":  exit()   # exit for @Q
      !"sS":  save_image()   # save image for @S
      }
   return
end

#  render() -- draw a new pattern according to current parameters
procedure render()
   local ncolours, bias
   local s, x, y, w, h, z, k
   static prevsize

   ncolours := txtval(colours_text, 1, *allcolours) # retrieve "Colours" setting
   patsize := txtval(side_text, 1, maxsiz)  # retrieve "Side" setting
   bias := txtval(bias_text, 0, 100)  # retrieve "Bias" setting

   k := (shuffle(PREFCOLOURS) | allcolours)[1+:ncolours]   # pick a colour set
   s := genpatt(patsize, k, bias / 100.0)     # generate a pattern
   DrawImage(hidwin, 0, 0, s)      # draw on hidden win

   z := maxsiz / patsize    # calculate scaling
   x := region.ux + (region.uw - z * patsize) / 2
   y := region.uy + (region.uh - z * patsize) / 2

   #  copy to main window with enlargement
   if prevsize ~===:= patsize then
      EraseArea(region.ux, region.uy, region.uw, region.uh)  # erase old pattern
   Zoom(hidwin, &window, 0, 0, patsize, patsize, x, y, z * patsize, z * patsize)

   return
end

#  genpatt(size, colours, bias) -- generate a new pattern as DrawImage() string
procedure genpatt(size, colours, bias)
   local warp, weft, perfect, s, x, y, w

   #  choose thread colours
   warp := genthreads(size, colours, VGetState(vcyclic_toggle))
   weft := genthreads(size, colours, VGetState(hcyclic_toggle))

   #  initialize output string (including first row)
   s := size || "," || PALETTE || "," || warp

   perfect := VGetState(perfect_toggle)

   #  fill in remaining rows
   every y := 2 to size do {
      w := ?weft[y]   # get weft colour
      s ||:= w    # put in first column
      if \perfect then
         every x := 2 to size do # fill the rest (perfect case)
            s ||:= if ((x + y) % 2) = 0 then w else warp[x]
      else
         every x := 2 to size do # fill the rest (random case)
            s ||:= if ?0 > bias then w else warp[x]
      }

   return s
end

#  genthreads(n, colours, cyclic) -- generate a set of warp or weft threads
procedure genthreads(n, colours, cyclic)
   local s

   if \cyclic then
      return repl(shuffle(colours), 1 + n / *colours)[1+:n]

   s := ""
   every 1 to n do s ||:= ?colours
   return s
end

#  txtval(s, min, max) -- get numeric value from named vidget and clamp to range
procedure txtval(v, min, max)
   local n

   VEvent(v, "\r", v.ax, v.ay)  # set RETURN event to update state
   n := integer(VGetState(v)) | min # retrieve int value, else use minimum
   n <:= min    # limit value by min and max
   n >:= max
   VSetState(v, n)   # update vidget with validated value
   return n    # return value
end

#  save_image() -- present dialog box and save pattern as image file
procedure save_image()
   local g

   g := WAttrib("gamma") # save old gamma value
   WAttrib("gamma=1.0")  # don't gamma-correct on write
   repeat case OpenDialog("Save pattern as:") of {
      "Cancel": {
         WAttrib("gamma=" || g)
         fail
         }
      "Okay": {
         if WriteImage(hidwin, dialog_value, 0, 0, patsize, patsize) then
            break
         else
            Notice("cannot write file:", dialog_value)
         }
      }
   WAttrib("gamma=" || g) # restore gamma value
   return
end

procedure quit()
   exit()
end

Page from Peter's Scrapbook, output from a VimWiki on 2024-01-21.