Graphics
Line Stretch  Lab Report
"Stretch" Lines Without Using XOR

Shown above is the version modifed by Juliano Zabeo Pessini from São Paulo, Brasil

Purpose

  1. To explore whether a line can be stretched without using the XOR "rubber band" technique.

  2. To demonstrate how to "lock" the cursor in the drawing area.

  3. To demonstrate how to select and move a line.

Materials and Equipment

Software Requirements
Windows 95/98
Delphi 3/4/5 (to recompile)
LineStretch.EXE

Hardware Requirements
VGA display

Procedure

  1. Double click on the LineStretch.EXE icon to start the program. (In Juliano's version, select a line by clicking on one.)
  2. Move the arrow cursor near the red line or the green endpoint "handles."   (Juliano's version has yellow and green endpoints.) Note that the cursor changes to the "hand" when near the endpoints or the line itself.
  3. When near one of the endpoint handles, press and drag the handle to another location. Note the cursor changes to the "dragging cursor" and that motion is limited to the drawing area. That is, while moving the endpoint handle, the cursor cannot be moved outside of the drawing area.
  4. When near the line but away from one of the handles, press and drag the whole line to a new location. Note that the cursor cannot be moved outside of the drawing area while dragging the whole line. This ensures that the line will always be in the drawing area and cannot be moved outside the area and "lost."

Discussion
Every time any change is made to the drawing, the background is cleared to white and all the black lines are first drawn. Next the red "selected" line is drawing. Lastly, the "handles" on the selected line are drawn.

The rubber banding technique is often used for drawing lines by using one exclusive or (XOR) to draw a line and a second XOR to remove the line.. See Rubber banding example on pp. 7-22 - 7-27 of the Delphi 4 Developer's Guide. The sample code explained in the example is on the CD in the directory EXAMPLES\DOC\GRAPHEX. Also, see Robert Vivrette's How to Draw a Rubber-Banding Line at www.undu.com/DN960901/00000007.htm.

One of the drawbacks of rubber banding with XOR is little control of the color of the line, which can sometimes result in poor contrast between the line being drawn and the background color. Users of drawing programs don't "ask" for XOR for rubber banding lines; users would normally prefer a line to be a specific color.

During the OnMouseDown, the event handler determines whether the "down" point (X, Y) is in one of the handles, or near the line. Determining whether a point is within a rectangle is straightforward. Determining whether a point is near a line takes a little more work.

A formula exists that tells how far a point is from a line, but this is at best awkward and may be slow to use since the computation involves a square root. For lines with a slope < 1, the slope-intercept line equation is useful: y = mx * b, where m is the slope computed as m = (y2 - y1) / (x2 - x1). For a given point (X,Y), the value Y' = mX * b can be quickly computed. If |Y - Y' | is fairly small, perhaps just a few pixels, then the line can be considered "selected." See the "NearLine" routine. For slopes > 1, the X and Y values are reversed. The line equation becomes X = mY * b, and if |X - X'| is sufficiently small, the line is "selected." This approach avoids vertical lines with infinite slopes.

The ClipCursor Windows API call is used to restrict cursor motion.  The RestrictCursorToDrawingArea limits drawing within an area defined by a TImage.  This restriction is imposed as part of the MouseDown method:

PROCEDURE RestrictCursorToDrawingArea (CONST Image: TImage);
  VAR
    CursorClipArea: TRect;
BEGIN
  CursorClipArea := Bounds(Image.ClientOrigin.X, Image.ClientOrigin.Y,
                                          Image.Width, Image.Height);
  Windows.ClipCursor(@CursorClipArea)
END {RestrictCursorToDrawingArea};

The restriction is removed via the RemoveCursorRestrictions procedure as part of the MouseUp method:

PROCEDURE RemoveCursorRestrictions;
BEGIN
  Windows.ClipCursor(NIL)
  END {RemoveCursorRestrictions};

The LineLibrary.PAS unit contains the low-level routines CalcLineParameters, NearLine and other routines so this functionality can be used with other programs.

CalcLineParameters Procedure

// Determine whether a line is ltHorizonal or ltVertical, along with the
// appropriate slope and intercept FOR point-slope line equations. These
// parameters are used to determine if a line is selected.
PROCEDURE CalcLineParameters (CONST PointA, PointB : TPoint;
    VAR Slope, Intercept: DOUBLE;
    VAR LineOrientation : TLineOrientation);
  VAR
    Delta: TPoint;
BEGIN
  Delta := SubtractPoints(PointB, PointA);

  IF       (Delta.X = 0) AND (Delta.Y = 0)
  THEN BEGIN
    // This special CASE should never happen if iMinPixels > 0
    LineOrientation := loPoint;
    Slope := 0.0;
    Intercept := 0.0
  END
  ELSE BEGIN

    IF       ABS(Delta.X) >= ABS(Delta.Y)
    THEN BEGIN
      // The line is more horizontal than vertical. Determine values FOR
      // equation: Y = slope*X + intercept
      LineOrientation := loHorizontal;
      TRY
        Slope := Delta.Y / Delta.X        {conventional slope in geometry}
      EXCEPT
        Slope := 0.0
      END;
      Intercept := PointA.Y - PointA.X*Slope
    END
    ELSE BEGIN
      // The line is more vertical than horizontal. Determine values for
      // equation: X = slope*Y + intercept
      LineOrientation := loVertical;
      TRY
        Slope := Delta.X / Delta.Y        {reciprocal of conventional slope}
      EXCEPT
        Slope := 0.0
      END;
      Intercept := PointA.X - PointA.Y*Slope;
    END

  END
END {CalcLineParameters};

 

NearLine Function

// Determine if Target1 is "near" line segment between Point1 and Point2
FUNCTION NearLine(CONST Target, Point1, Point2: TPoint): BOOLEAN;
  CONST
    LineSelectFuzz = 4; // Pixel "fuzz" used in line selection
  VAR
    Intercept : DOUBLE;
    LineOrientation: TLineOrientation;
    maxX : INTEGER;
    maxY : INTEGER;
    minX : INTEGER;
    minY : INTEGER;
    Slope : DOUBLE;
    xCalculated : INTEGER;
   yCalculated : INTEGER;
BEGIN
  RESULT := FALSE;

  // If an Endpoint is not selected, was part of line selected?
  CalcLineParameters (Point1, Point2, Slope, Intercept, LineOrientation);

  CASE LineOrientation OF
    loHorizontal:
      BEGIN
        minX := MinIntValue([Point1.X, Point2.X]);
        maxX := MaxIntValue([Point1.X, Point2.X]);
        // first check if selection within horizontal range of line
        IF        (Target.X >= minX) and (Target.X <= maxX)
        THEN BEGIN
          // Since X is within range of line, now see if Y value is close
          // enough to the calculated Y value FOR the line to be selected.
          yCalculated := ROUND( Slope*Target.X + Intercept );
          IF        ABS(yCalculated - Target.Y) <= LineSelectFuzz
          THEN RESULT := TRUE
        END
      END;

    loVertical:
      BEGIN
        minY := MinIntValue([Point1.Y, Point2.Y]);
        maxY := MaxIntValue([Point1.Y, Point2.Y]);
        // first check if selection within vertical range of line
        IF        (Target.Y >= minY) AND (Target.Y <= maxY)
        THEN BEGIN
          // Since Y is within range of line, now see if X value is close
          // enough to the calculated X value FOR the line to be selected.
          xCalculated := ROUND( Slope*Target.Y + Intercept );
          IF        ABS(xCalculated - Target.X) <= LineSelectFuzz
          THEN RESULT := TRUE
        END
      END;

    loPoint:
      // Do nothing -- should not occur
  END
END {NearLine};

In the original version of the program, the extra lines on the screen were intended just to be "noise" over which the single line was moved.  Juliano Zabeo Pessini's clever modification allows  interaction with any of the lines on the form.

Also see David Lively's UseNet Post about controlling XOR color if background is not too complex

Conclusions
In certain applications, lines can be stretched with a specified color and without the use of XOR rubber banding. (Flickering of a non-white background was not addressed in this experiment.) This technique is not quite fast enough when the background is a bitmap (at least on my 166 MHz Pentium).

Locking the cursor inside the drawing area is a useful technique to keep graphics objects from being "lost" by a user.


Keywords
OnMouseDown, OnMouseMove, OnMouseUp, Bounds, ClipCursor, PtInRect, Rectangle, Screen.Cursor, crHandPoint, crDrag, crDefault, AddPoints, SubtractPoints, NearLine, Canvas.MoveTo, Canvas.LineTo

Download

Original Version Manipulate single line (for beginners).
Delphi 3/4/5 Source and EXE (114 KB):
LineStretch.ZIP
Modified Version Modifications by Juliano Zabeo Pessini to interact with all lines on screen.  Delphi 3/4/5 Source (7 KB):
JulianoZabeoPessini_LineStretch.ZIP

LineCursors examples (to be explained here some day):  LineCursors.ZIP


Updated
26 Feb 2005


since 1 Nov 1998