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
To explore whether a line can be stretched without using the XOR "rubber band" technique.
To demonstrate how to "lock" the cursor in the drawing area.
To demonstrate how to select and move a line.
Materials and Equipment
Software Requirements
Windows 95/98
Delphi 3/4/5 (to recompile)
LineStretch.EXEHardware Requirements
VGA display
Procedure
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 = (y_{2} - y_{1}) / (x_{2} - x_{1}). 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