PHP Hacks: Tips & Tools For Creating Dynamic Websites
Hack 29. Simplify Your Graphics with Objects
Use the object-oriented features of PHP to simplify your graphics using layering, object-oriented drawing, and viewport scaling. PHP's support for graphics is great. But when you try to build a complex visualization, PHP becomes difficult to use for several reasons. First, the drawing order is really important. Things that you draw first will be covered by the stuff that you draw later (and there's no good way to get around that limitation). This means that you have to sequence your code based on the drawing order, even when it's difficult to do so programmatically. Another problem is scaling. To draw into an image, you have to know how big the image is and how to scale your drawing. That means passing around a lot of information about the drawing context. Add that to the layering issues, and PHP image code becomes a real mess. Lucky for all of us hackers who aren't graphics pros, all of these problems have been solved via graphics libraries like PHP's GD library. In this hack, I build a simple object API for graphics that manages drawing order through z buffering, and handles scaling by creating a viewport. 4.4.1. The Code
Save the code in Example 4-4 as layers.php. Example 4-4. Defining several classes used to layer graphics
<?php class GraphicSpace { var $image; var $colors; var $xoffset; var $yoffset; var $xscale; var $yscale; function GraphicSpace() { $this->colors = array(); } function get_image() { return $this->image; } function set_image( $im ) { $this->image = $im; } function get_color( $id ) { return $this->colors[ $id ]; } function set_color( $id, $color ) { $this->colors[ $id ] = $color; } function set_viewport( $left, $top, $right, $bottom ) { $this->xoffset = $left; $this->yoffset = $top; $this->xscale = imagesx( $this->image ) / ( $right - $left ); $this->yscale = imagesy( $this->image ) / ( $bottom - $top ); } function transform_x( $x ) { return ( $x - $this->xoffset ) * $this->xscale; } function transform_y( $y ) { return ( $y - $this->yoffset ) * $this->yscale; } function scale_x( $x ) { return $x * $this->xscale; } function scale_y( $y ) { return $y * $this->yscale; } } class RenderItem { var $left; var $right; var $top; var $bottom; var $color; var $z; function RenderItem( $left, $top, $right, $bottom, $color, $z ) { $this->left = $left; $this->right = $right; $this->top = $top; $this->bottom = $bottom; $this->color = $color; $this->z = $z; } function get_left() { return $this->left; } function get_right() { return $this->right; } function get_top() { return $this->top; } function get_bottom() { return $this->bottom; } function get_z() { return $this->z; } function render( $gs ) { } function transform( $x, $y ) { } } class Line extends RenderItem { var $sx; var $sy; var $ex; var $ey; var $thickness; function Line( $sx, $sy, $ex, $ey, $color, $z, $thickness ) { $this->RenderItem( min( $sx, $ex ), min( $sy, $ey ), max( $sx, $ex ), max( $sy, $ey ), $color, $z ); $this->sx = $sx; $this->sy = $sy; $this->ex = $ex; $this->ey = $ey; $this->thickness = $thickness; } function render( $gs ) { if ( $this->thickness > 1 ) imagesetthickness( $gs->get_image(), $this->thickness ); $this->drawline( $gs->get_image(), $gs->transform_x( $this->sx ), $gs->transform_y( $this->sy ), $gs->transform_x( $this->ex ), $gs->transform_y( $this->ey ), $gs->get_color( $this->color ) ); if ( $this->thickness > 1 ) imagesetthickness( $gs->get_image(), 1 ); } function drawline( $im, $sx, $sy, $ex, $ey, $color ) { imageline( $im, $sx, $sy, $ex, $ey, $color ); } } class DashedLine extends Line { function drawline( $im, $sx, $sy, $ex, $ey, $color ) { imagedashedline( $im, $sx, $sy, $ex, $ey, $color ); } } class Ball extends RenderItem { var $text; function Ball( $x, $y, $size, $color, $text, $z ) { $width = $size / 2; if ( $text ) $width += 20; $this->RenderItem( $x, $y, $x + $width, $y + ( $size / 2 ), $color, $z ); $this->text = $text; $this->size = $size; } function render( $gs ) { imagefilledellipse( $gs->get_image(), $gs->transform_x( $this->left ), $gs->transform_y( $this->top ), $gs->scale_x( $this->size ), $gs->scale_x( $this->size ), $gs->get_color( $this->color ) ); if ( strlen( $this->text ) ) imagestring($gs->get_image(), 0, $gs->transform_x( $this->left ) + 7, $gs->transform_y( $this->top )-5, $this->text, $gs->get_color( $this->color ) ); } } function zsort( $a, $b ) { if ( $a->get_z() == $b->get_z() ) return 0; return ( $a->get_z() > $b->get_z() ) ? 1 : -1; } class RenderQueue { var $items; function RenderQueue() { $this->items = array(); } function add( $item ) { $this->items [] = $item; } function render( $gs ) { usort( &$this->items, "zsort" ); foreach( $this->items as $item ) {$item->render( $gs );} } function get_size() { $minx = 1000; $maxx = -1000; $miny = 1000; $maxy = -1000; foreach( $this->items as $item ) { if ( $item->get_left() < $minx ) $minx = $item->get_left(); if ( $item->get_right() > $maxx ) $maxx = $item->get_right(); if ( $item->get_top() < $miny ) $miny = $item->get_top(); if ( $item->get_bottom() > $maxy ) $maxy = $item->get_bottom(); } return array( left => $minx, top => $miny, right => $maxx, bottom => $maxy ); } } $width = 400; $height = 400; function calcpoint( $d, $r ) { $x = cos( deg2rad( $d ) ) * $r; $y = sin( deg2rad( $d ) ) * $r; return array( $x, $y ); } $render_queue = new RenderQueue(); $ox = null; $oy = null; for( $d = 0; $d < 380; $d += 10 ) { list( $x, $y ) = calcpoint( $d, 10 ); $render_queue->add( new Ball( $x, $y, 1, "line", "", 10 ) ); $render_queue->add( new Line( 0, 0, $x, $y, "red", 1, 1 ) ); if ( $ox != null && $oy != null ) { $render_queue->add( new Line( $ox, $oy, $x, $y, "red", 1, 1 ) ); } $ox = $x; $oy = $y; } $gsize = $render_queue->get_size(); $fudgex = ( $gsize['right'] - $gsize['left'] ) * 0.1; $gsize['left'] -= $fudgex; $gsize['right'] += $fudgex; $fudgey = ( $gsize['bottom'] - $gsize['top'] ) * 0.1; $gsize['top'] -= $fudgey; $gsize['bottom'] += $fudgey; print_r( $gsize ); $im = imagecreatetruecolor( $width, $height ); imageantialias( $im, true ); $bg = imagecolorallocate($im, 255, 255, 255); imagefilledrectangle( $im, 0, 0, $width, $height, $bg ); $gs = new graphicspace(); $gs->set_image( $im ); $gs->set_color( 'back', $bg ); $gs->set_color( 'line', imagecolorallocate($im, 96, 96, 96) ); $gs->set_color( 'red', imagecolorallocate($im, 255, 0, 0) ); $gs->set_viewport( $gsize['left'], $gsize['top'], $gsize['right'], $gsize['bottom'] ); $render_queue->render( $gs ); imagepng( $im, "test.png" ); imagedestroy( $im ); ?>
Figure 4-4 shows the layout of the classes in this script. RenderQueue refers to two objects: an array of RenderItems, and a GraphicSpace that holds the image and the transformation information. The derived classes of RenderItem create the different types of shapes: lines, balls, and dashed lines. To add more items, just add more child classes of RenderItem. Each RenderItem has a z level associated with it. Items that have a lower z level (or z value) will be rendered behind items that have a larger z value. This allows you to create objects in any order you like, assign them z values, and know that they will be sorted and rendered in the proper order. For the coordinate system, I used a mechanism called a viewport. The viewport is a virtual graphics space. The coordinates can be anything you like, ranging from 0 to 1 or from 0 to 1 billion. The system automatically scales the graphics to the size of the image. Figure 4-4. The UML of the graphics objects
Starting with this simple object API, you can create much more complex graphics far more easily than if you used the PHP graphics API directly. The object code is also far easier to understand and maintain. 4.4.2. Running the Hack
Use the PHP command-line interpreter to run the layers.php script: % php layers.php Array ( [left] => -12.05 [top] => -12.05 [right] => 12.55 [bottom] => 12.55 )
Then use your browser to look at the resulting test.png file, shown in Figure 4-5. Figure 4-5. A circle built with graphics objects
There are three sets of objects in this graph: the lines that go from the center to the balls, the gray balls, and the lines that go between the balls along the perimeter. The lines radiating out from the center are at a z level of 1. The balls are at a z level of 10. And the lines that run along the perimeter are at a z level of 20. To change the z order of the perimeter lines, you can just change the z value from, for example, 20 to 1: if ( $ox != null && $oy != null ) { $render_queue->add( new Line( $ox, $oy, $x, $y, "red", 1, 1 ) ); } Now if you rerun the script and look at the results in the browser, you'll see that the balls are on top of the lines (see Figure 4-6), whereas before they were underneath them. Figure 4-6. Dropping the connectors behind the gray balls
Now the perimeter lines have dropped behind the gray balls because of the lower z order. 4.4.3. See Also
|