Mercurial > repos > dereeper > sniploid2
diff GD/Polyline.pm @ 0:e94de0ea3351 draft default tip
Uploaded
author | dereeper |
---|---|
date | Wed, 11 Sep 2013 09:08:15 -0400 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/GD/Polyline.pm Wed Sep 11 09:08:15 2013 -0400 @@ -0,0 +1,792 @@ +############################################################################ +# +# Polyline.pm +# +# Author: Dan Harasty +# Email: harasty@cpan.org +# Version: 0.2 +# Date: 2002/08/06 +# +# For usage documentation: see POD at end of file +# +# For changes: see "Changes" file included with distribution +# + +use strict; + +package GD::Polyline; + +############################################################################ +# +# GD::Polyline +# +############################################################################ +# +# What's this? A class with nothing but a $VERSION and and @ISA? +# Below, this module overrides and adds several modules to +# the parent class, GD::Polygon. Those updated/new methods +# act on polygons and polylines, and sometimes those behaviours +# vary slightly based on whether the object is a polygon or polyine. +# + +use vars qw($VERSION @ISA); +$VERSION = "0.2"; +@ISA = qw(GD::Polygon); + + +package GD::Polygon; + +############################################################################ +# +# new methods on GD::Polygon +# +############################################################################ + +use GD; +use Carp 'croak','carp'; + +use vars qw($bezSegs $csr); +$bezSegs = 20; # number of bezier segs -- number of segments in each portion of the spline produces by toSpline() +$csr = 1/3; # control seg ratio -- the one possibly user-tunable parameter in the addControlPoints() algorithm + + +sub rotate { + my ($self, $angle, $cx, $cy) = @_; + $self->offset(-$cx,-$cy) if $cx or $cy; + $self->transform(cos($angle),sin($angle),-sin($angle),cos($angle),$cx,$cy); +} + +sub centroid { + my ($self, $scale) = @_; + my ($cx,$cy); + $scale = 1 unless defined $scale; + + map {$cx += $_->[0]; $cy += $_->[1]} $self->vertices(); + + $cx *= $scale / $self->length(); + $cy *= $scale / $self->length(); + + return ($cx, $cy); +} + + +sub segLength { + my $self = shift; + my @points = $self->vertices(); + + my ($p1, $p2, @segLengths); + + $p1 = shift @points; + + # put the first vertex on the end to "close" a polygon, but not a polyline + push @points, $p1 unless $self->isa('GD::Polyline'); + + while ($p2 = shift @points) { + push @segLengths, _len($p1, $p2); + $p1 = $p2; + } + + return @segLengths if wantarray; + + my $sum; + map {$sum += $_} @segLengths; + return $sum; +} + +sub segAngle { + my $self = shift; + my @points = $self->vertices(); + + my ($p1, $p2, @segAngles); + + $p1 = shift @points; + + # put the first vertex on the end to "close" a polygon, but not a polyline + push @points, $p1 unless $self->isa('GD::Polyline'); + + while ($p2 = shift @points) { + push @segAngles, _angle_reduce2(_angle($p1, $p2)); + $p1 = $p2; + } + + return @segAngles; +} + +sub vertexAngle { + my $self = shift; + my @points = $self->vertices(); + + my ($p1, $p2, $p3, @vertexAngle); + + $p1 = $points[$#points]; # last vertex + $p2 = shift @points; # current point -- the first vertex + + # put the first vertex on the end to "close" a polygon, but not a polyline + push @points, $p2 unless $self->isa('GD::Polyline'); + + while ($p3 = shift @points) { + push @vertexAngle, _angle_reduce2(_angle($p1, $p2, $p3)); + ($p1, $p2) = ($p2, $p3); + } + + $vertexAngle[0] = undef if defined $vertexAngle[0] and $self->isa("GD::Polyline"); + + return @vertexAngle if wantarray; + +} + + + +sub toSpline { + my $self = shift; + my @points = $self->vertices(); + + # put the first vertex on the end to "close" a polygon, but not a polyline + push @points, [$self->getPt(0)] unless $self->isa('GD::Polyline'); + + unless (@points > 1 and @points % 3 == 1) { + carp "Attempt to call toSpline() with invalid set of control points"; + return undef; + } + + my ($ap1, $dp1, $dp2, $ap2); # ap = anchor point, dp = director point + $ap1 = shift @points; + + my $bez = new ref($self); + + $bez->addPt(@$ap1); + + while (@points) { + ($dp1, $dp2, $ap2) = splice(@points, 0, 3); + + for (1..$bezSegs) { + my ($t0, $t1, $c1, $c2, $c3, $c4, $x, $y); + + $t1 = $_/$bezSegs; + $t0 = (1 - $t1); + + # possible optimization: + # these coefficient could be calculated just once and + # cached in an array for a given value of $bezSegs + + $c1 = $t0 * $t0 * $t0; + $c2 = 3 * $t0 * $t0 * $t1; + $c3 = 3 * $t0 * $t1 * $t1; + $c4 = $t1 * $t1 * $t1; + + $x = $c1 * $ap1->[0] + $c2 * $dp1->[0] + $c3 * $dp2->[0] + $c4 * $ap2->[0]; + $y = $c1 * $ap1->[1] + $c2 * $dp1->[1] + $c3 * $dp2->[1] + $c4 * $ap2->[1]; + + $bez->addPt($x, $y); + } + + $ap1 = $ap2; + } + + # remove the last anchor point if this is a polygon -- since it will autoclose without it + $bez->deletePt($bez->length()-1) unless $self->isa('GD::Polyline'); + + return $bez; +} + +sub addControlPoints { + my $self = shift; + my @points = $self->vertices(); + + unless (@points > 1) { + carp "Attempt to call addControlPoints() with too few vertices in polyline"; + return undef; + } + + my $points = scalar(@points); + my @segAngles = $self->segAngle(); + my @segLengths = $self->segLength(); + + my ($prevLen, $nextLen, $prevAngle, $thisAngle, $nextAngle); + my ($controlSeg, $pt, $ptX, $ptY, @controlSegs); + + # this loop goes about creating polylines -- here called control segments -- + # that hold the control points for the final set of control points + + # each control segment has three points, and these are colinear + + # the first and last will ultimately be "director points", and + # the middle point will ultimately be an "anchor point" + + for my $i (0..$#points) { + + $controlSeg = new GD::Polyline; + + $pt = $points[$i]; + ($ptX, $ptY) = @$pt; + + if ($self->isa('GD::Polyline') and ($i == 0 or $i == $#points)) { + $controlSeg->addPt($ptX, $ptY); # director point + $controlSeg->addPt($ptX, $ptY); # anchor point + $controlSeg->addPt($ptX, $ptY); # director point + next; + } + + $prevLen = $segLengths[$i-1]; + $nextLen = $segLengths[$i]; + $prevAngle = $segAngles[$i-1]; + $nextAngle = $segAngles[$i]; + + # make a control segment with control points (director points) + # before and after the point from the polyline (anchor point) + + $controlSeg->addPt($ptX - $csr * $prevLen, $ptY); # director point + $controlSeg->addPt($ptX , $ptY); # anchor point + $controlSeg->addPt($ptX + $csr * $nextLen, $ptY); # director point + + # note that: + # - the line is parallel to the x-axis, as the points have a common $ptY + # - the points are thus clearly colinear + # - the director point is a distance away from the anchor point in proportion to the length of the segment it faces + + # now, we must come up with a reasonable angle for the control seg + # first, "unwrap" $nextAngle w.r.t. $prevAngle + $nextAngle -= 2*pi() until $nextAngle < $prevAngle + pi(); + $nextAngle += 2*pi() until $nextAngle > $prevAngle - pi(); + # next, use seg lengths as an inverse weighted average + # to "tip" the control segment toward the *shorter* segment + $thisAngle = ($nextAngle * $prevLen + $prevAngle * $nextLen) / ($prevLen + $nextLen); + + # rotate the control segment to $thisAngle about it's anchor point + $controlSeg->rotate($thisAngle, $ptX, $ptY); + + } continue { + # save the control segment for later + push @controlSegs, $controlSeg; + + } + + # post process + + my $controlPoly = new ref($self); + + # collect all the control segments' points in to a single control poly + + foreach my $cs (@controlSegs) { + foreach my $pt ($cs->vertices()) { + $controlPoly->addPt(@$pt); + } + } + + # final clean up based on poly type + + if ($controlPoly->isa('GD::Polyline')) { + # remove the first and last control point + # since they are director points ... + $controlPoly->deletePt(0); + $controlPoly->deletePt($controlPoly->length()-1); + } else { + # move the first control point to the last control point + # since it is supposed to end with two director points ... + $controlPoly->addPt($controlPoly->getPt(0)); + $controlPoly->deletePt(0); + } + + return $controlPoly; +} + + +# The following helper functions are for internal +# use of this module. Input arguments of "points" +# refer to an array ref of two numbers, [$x, $y] +# as is used internally in the GD::Polygon +# +# _len() +# Find the length of a segment, passing in two points. +# Internal function; NOT a class or object method. +# +sub _len { +# my ($p1, $p2) = @_; +# return sqrt(($p2->[0]-$p1->[0])**2 + ($p2->[1]-$p1->[1])**2); + my $pt = _subtract(@_); + return sqrt($pt->[0] ** 2 + $pt->[1] **2); +} + +use Math::Trig; + +# _angle() +# Find the angle of... well, depends on the number of arguments: +# - one point: the angle from x-axis to the point (origin is the center) +# - two points: the angle of the vector defined from point1 to point2 +# - three points: +# Internal function; NOT a class or object method. +# +sub _angle { + my ($p1, $p2, $p3) = @_; + my $angle = undef; + if (@_ == 1) { + return atan2($p1->[1], $p1->[0]); + } + if (@_ == 2) { + return _angle(_subtract($p1, $p2)); + } + if (@_ == 3) { + return _angle(_subtract($p2, $p3)) - _angle(_subtract($p2, $p1)); + } +} + +# _subtract() +# Find the difference of two points; returns a point. +# Internal function; NOT a class or object method. +# +sub _subtract { + my ($p1, $p2) = @_; +# print(_print_point($p2), "-", _print_point($p1), "\n"); + return [$p2->[0]-$p1->[0], $p2->[1]-$p1->[1]]; +} + +# _print_point() +# Returns a string suitable for displaying the value of a point. +# Internal function; NOT a class or object method. +# +sub _print_point { + my ($p1) = @_; + return "[" . join(", ", @$p1) . "]"; +} + +# _angle_reduce1() +# "unwraps" angle to interval -pi < angle <= +pi +# Internal function; NOT a class or object method. +# +sub _angle_reduce1 { + my ($angle) = @_; + $angle += 2 * pi() while $angle <= -pi(); + $angle -= 2 * pi() while $angle > pi(); + return $angle; +} + +# _angle_reduce2() +# "unwraps" angle to interval 0 <= angle < 2 * pi +# Internal function; NOT a class or object method. +# +sub _angle_reduce2 { + my ($angle) = @_; + $angle += 2 * pi() while $angle < 0; + $angle -= 2 * pi() while $angle >= 2 * pi(); + return $angle; +} + +############################################################################ +# +# new methods on GD::Image +# +############################################################################ + +sub GD::Image::polyline { + my $self = shift; # the GD::Image + my $p = shift; # the GD::Polyline (or GD::Polygon) + my $c = shift; # the color + + my @points = $p->vertices(); + my $p1 = shift @points; + my $p2; + while ($p2 = shift @points) { + $self->line(@$p1, @$p2, $c); + $p1 = $p2; + } +} + +sub GD::Image::polydraw { + my $self = shift; # the GD::Image + my $p = shift; # the GD::Polyline or GD::Polygon + my $c = shift; # the color + + return $self->polyline($p, $c) if $p->isa('GD::Polyline'); + return $self->polygon($p, $c); +} + + +1; +__END__ + +=pod + +=head1 NAME + +GD::Polyline - Polyline object and Polygon utilities (including splines) for use with GD + +=head1 SYNOPSIS + + use GD; + use GD::Polyline; + + # create an image + $image = new GD::Image (500,300); + $white = $image->colorAllocate(255,255,255); + $black = $image->colorAllocate( 0, 0, 0); + $red = $image->colorAllocate(255, 0, 0); + + # create a new polyline + $polyline = new GD::Polyline; + + # add some points + $polyline->addPt( 0, 0); + $polyline->addPt( 0,100); + $polyline->addPt( 50,125); + $polyline->addPt(100, 0); + + # polylines can use polygon methods (and vice versa) + $polyline->offset(200,100); + + # rotate 60 degrees, about the centroid + $polyline->rotate(3.14159/3, $polyline->centroid()); + + # scale about the centroid + $polyline->scale(1.5, 2, $polyline->centroid()); + + # draw the polyline + $image->polydraw($polyline,$black); + + # create a spline, which is also a polyine + $spline = $polyline->addControlPoints->toSpline; + $image->polydraw($spline,$red); + + # output the png + binmode STDOUT; + print $image->png; + +=head1 DESCRIPTION + +B<Polyline.pm> extends the GD module by allowing you to create polylines. Think +of a polyline as "an open polygon", that is, the last vertex is not connected +to the first vertex (unless you expressly add the same value as both points). + +For the remainder of this doc, "polyline" will refer to a GD::Polyline, +"polygon" will refer to a GD::Polygon that is not a polyline, and +"polything" and "$poly" may be either. + +The big feature added to GD by this module is the means +to create splines, which are approximations to curves. + +=head1 The Polyline Object + +GD::Polyline defines the following class: + +=over 5 + +=item C<GD::Polyline> + +A polyline object, used for storing lists of vertices prior to +rendering a polyline into an image. + +=item C<new> + +C<GD::Polyline-E<gt>new> I<class method> + +Create an empty polyline with no vertices. + + $polyline = new GD::Polyline; + + $polyline->addPt( 0, 0); + $polyline->addPt( 0,100); + $polyline->addPt( 50,100); + $polyline->addPt(100, 0); + + $image->polydraw($polyline,$black); + +In fact GD::Polyline is a subclass of GD::Polygon, +so all polygon methods (such as B<offset> and B<transform>) +may be used on polylines. +Some new methods have thus been added to GD::Polygon (such as B<rotate>) +and a few updated/modified/enhanced (such as B<scale>) I<in this module>. +See section "New or Updated GD::Polygon Methods" for more info. + +=back + +Note that this module is very "young" and should be +considered subject to change in future releases, and/or +possibly folded in to the existing polygon object and/or GD module. + +=head1 Updated Polygon Methods + +The following methods (defined in GD.pm) are OVERRIDDEN if you use this module. + +All effort has been made to provide 100% backward compatibility, but if you +can confirm that has not been achieved, please consider that a bug and let the +the author of Polyline.pm know. + +=over 5 + +=item C<scale> + +C<$poly-E<gt>scale($sx, $sy, $cx, $cy)> I<object method -- UPDATE to GD::Polygon::scale> + +Scale a polything in along x-axis by $sx and along the y-axis by $sy, +about centery point ($cx, $cy). + +Center point ($cx, $cy) is optional -- if these are omitted, the function +will scale about the origin. + +To flip a polything, use a scale factor of -1. For example, to +flip the polything top to bottom about line y = 100, use: + + $poly->scale(1, -1, 0, 100); + +=back + +=head1 New Polygon Methods + +The following methods are added to GD::Polygon, and thus can be used +by polygons and polylines. + +Don't forget: a polyline is a GD::Polygon, so GD::Polygon methods +like offset() can be used, and they can be used in +GD::Image methods like filledPolygon(). + +=over 5 + +=item C<rotate> + +C<$poly-E<gt>rotate($angle, $cx, $cy)> I<object method> + +Rotate a polything through $angle (clockwise, in radians) about center point ($cx, $cy). + +Center point ($cx, $cy) is optional -- if these are omitted, the function +will rotate about the origin + +In this function and other angle-oriented functions in GD::Polyline, +positive $angle corrensponds to clockwise rotation. This is opposite +of the usual Cartesian sense, but that is because the raster is opposite +of the usual Cartesian sense in that the y-axis goes "down". + +=item C<centroid> + +C<($cx, $cy) = $poly-E<gt>centroid($scale)> I<object method> + +Calculate and return ($cx, $cy), the centroid of the vertices of the polything. +For example, to rotate something 180 degrees about it's centroid: + + $poly->rotate(3.14159, $poly->centroid()); + +$scale is optional; if supplied, $cx and $cy are multiplied by $scale +before returning. The main use of this is to shift an polything to the +origin like this: + + $poly->offset($poly->centroid(-1)); + +=item C<segLength> + +C<@segLengths = $poly-E<gt>segLength()> I<object method> + +In array context, returns an array the lengths of the segments in the polything. +Segment n is the segment from vertex n to vertex n+1. +Polygons have as many segments as vertices; polylines have one fewer. + +In a scalar context, returns the sum of the array that would have been returned +in the array context. + +=item C<segAngle> + +C<@segAngles = $poly-E<gt>segAngle()> I<object method> + +Returns an array the angles of each segment from the x-axis. +Segment n is the segment from vertex n to vertex n+1. +Polygons have as many segments as vertices; polylines have one fewer. + +Returned angles will be on the interval 0 <= $angle < 2 * pi and +angles increase in a clockwise direction. + +=item C<vertexAngle> + +C<@vertexAngles = $poly-E<gt>vertexAngle()> I<object method> + +Returns an array of the angles between the segment into and out of each vertex. +For polylines, the vertex angle at vertex 0 and the last vertex are not defined; +however $vertexAngle[0] will be undef so that $vertexAngle[1] will correspond to +vertex 1. + +Returned angles will be on the interval 0 <= $angle < 2 * pi and +angles increase in a clockwise direction. + +Note that this calculation does not attempt to figure out the "interior" angle +with respect to "inside" or "outside" the polygon, but rather, +just the angle between the adjacent segments +in a clockwise sense. Thus a polygon with all right angles will have vertex +angles of either pi/2 or 3*pi/2, depending on the way the polygon was "wound". + +=item C<toSpline> + +C<$poly-E<gt>toSpline()> I<object method & factory method> + +Create a new polything which is a reasonably smooth curve +using cubic spline algorithms, often referred to as Bezier +curves. The "source" polything is called the "control polything". +If it is a polyline, the control polyline must +have 4, 7, 10, or some number of vertices of equal to 3n+1. +If it is a polygon, the control polygon must +have 3, 6, 9, or some number of vertices of equal to 3n. + + $spline = $poly->toSpline(); + $image->polydraw($spline,$red); + +In brief, groups of four points from the control polyline +are considered "control +points" for a given portion of the spline: the first and +fourth are "anchor points", and the spline passes through +them; the second and third are "director points". The +spline does not pass through director points, however the +spline is tangent to the line segment from anchor point to +adjacent director point. + +The next portion of the spline reuses the previous portion's +last anchor point. The spline will have a cusp +(non-continuous slope) at an anchor point, unless the anchor +points and its adjacent director point are colinear. + +In the current implementation, toSpline() return a fixed +number of segments in the returned polyline per set-of-four +control points. In the future, this and other parameters of +the algorithm may be configurable. + +=item C<addControlPoints> + +C<$polyline-E<gt>addControlPoints()> I<object method & factory method> + +So you say: "OK. Splines sound cool. But how can I +get my anchor points and its adjacent director point to be +colinear so that I have a nice smooth curves from my +polyline?" Relax! For The Lazy: addControlPoints() to the +rescue. + +addControlPoints() returns a polyline that can serve +as the control polyline for toSpline(), which returns +another polyline which is the spline. Is your head spinning +yet? Think of it this way: + +=over 5 + +=item + + +If you have a polyline, and you have already put your +control points where you want them, call toSpline() directly. +Remember, only every third vertex will be "on" the spline. + +You get something that looks like the spline "inscribed" +inside the control polyline. + +=item + + +If you have a polyline, and you want all of its vertices on +the resulting spline, call addControlPoints() and then +toSpline(): + + $control = $polyline->addControlPoints(); + $spline = $control->toSpline(); + $image->polyline($spline,$red); + +You get something that looks like the control polyline "inscribed" +inside the spline. + +=back + +Adding "good" control points is subjective; this particular +algorithm reveals its author's tastes. +In the future, you may be able to alter the taste slightly +via parameters to the algorithm. For The Hubristic: please +build a better one! + +And for The Impatient: note that addControlPoints() returns a +polyline, so you can pile up the the call like this, +if you'd like: + + $image->polyline($polyline->addControlPoints()->toSpline(),$mauve); + +=back + +=head1 New GD::Image Methods + +=over 5 + +=item C<polyline> + +C<$image-E<gt>polyline(polyline,color)> I<object method> + + $image->polyline($polyline,$black) + +This draws a polyline with the specified color. +Both real color indexes and the special +colors gdBrushed, gdStyled and gdStyledBrushed can be specified. + +Neither the polyline() method or the polygon() method are very +picky: you can call either method with either a GD::Polygon or a GD::Polyline. +The I<method> determines if the shape is "closed" or "open" as drawn, I<not> +the object type. + +=item C<polydraw> + +C<$image-E<gt>polydraw(polything,color)> I<object method> + + $image->polydraw($poly,$black) + +This method draws the polything as expected (polygons are closed, +polylines are open) by simply checking the object type and calling +either $image->polygon() or $image->polyline(). + +=back + +=head1 Examples + +Please see file "polyline-examples.pl" that is included with the distribution. + +=head1 See Also + +For more info on Bezier splines, see http://www.webreference.com/dlab/9902/bezier.html. + +=head1 Future Features + +On the drawing board are additional features such as: + + - polygon winding algorithms (to determine if a point is "inside" or "outside" the polygon) + + - new polygon from bounding box + + - find bounding polygon (tightest fitting simple convex polygon for a given set of vertices) + + - addPts() method to add many points at once + + - clone() method for polygon + + - functions to interwork GD with SVG + +Please provide input on other possible features you'd like to see. + +=head1 Author + +This module has been written by Daniel J. Harasty. +Please send questions, comments, complaints, and kudos to him +at harasty@cpan.org. + +Thanks to Lincoln Stein for input and patience with me and this, +my first CPAN contribution. + +=head1 Copyright Information + +The Polyline.pm module is copyright 2002, Daniel J. Harasty. It is +distributed under the same terms as Perl itself. See the "Artistic +License" in the Perl source code distribution for licensing terms. + +The latest version of Polyline.pm is available at +your favorite CPAN repository and/or +along with GD.pm by Lincoln D. Stein at http://stein.cshl.org/WWW/software/GD. + +=cut + +# future: +# addPts +# boundingPolygon +# addControlPoints('method' => 'fitToSegments', 'numSegs' => 10) +# toSpline('csr' => 1/4); + +# GD::Color +# colorMap('x11' | 'svg' | <filename> ) +# colorByName($image, 'orange'); +# setImage($image); +# cbn('orange'); +# +# +#