Wednesday, May 30, 2012

Dead bitmaps could be beautiful... Part II

As you saw in my last post we now have the ability to create bitmaps in JavaFX by using the Canvas node that will be part of JavaFX 2.2
That's nice...BUT...would'nt it be even nicer to have the ability to fill a shape with a pattern like the good old TexturePaint in Swing ???
And you might guess right...it will be also part of JavaFX 2.2 and is called ImagePattern


YIPPPPPEEEEEAAAAAAA


Sorry but that was needed because I was waiting for this particular feature the whole time.
For those of you that have no idea what I'm talking about let me show you a little example from the Java Swing version of the SteelSeries lib (what else :) )




The carbon texture that I use to fill the background of the gauge is just a little image of 12x12 pixels that looks like this (zoomed to 128x128 pixels)




Now I use this pattern to fill the circular shape of the background by using the TexturePaint of Java Swing.


In former JavaFX releases there was nothing like TexturePaint which means you have to create this effect on your own (which was not really effective). So with the new ImagePattern class it's really easy to create the same effect in JavaFX.


The ImagePattern class definition that i use is as follows (at the moment)


ImagePattern(Image image, double x, double y, 
             double width, double height, boolean proportional);
  • image:            the image that will be used as pattern
  • x:                   the x origin of the anchor rectangle
  • y:                   the y origin of the anchor rectangle
  • width:             the width of the anchor rectangle
  • height:           the height of the anchor rectangle
  • proportional: indicates whether start and end locations are proportional

One way to realize this is to create the pattern image in a drawing program and add it to the resources of your project, load it from there in an image and use this image as fill pattern in ImagePattern.
But because I like to create the patterns by myself during runtime i make again use of the Canvas node that i explained in my last blogpost in combination with another new feature in JavaFX 2.2...the ability to create an image of a node and again...


YIPPPPPEEEEEAAAAAAA


With this feature you could take a snapshot of a scene or node and get an image back that you might want to use to print or you might guess it already use it as a pattern for an ImagePattern object. For my example I will use the following method from the Node class


public Image snapshot(SnapshotParameters params, Image image)
  • paramsthe snapshot parameters containing attributes that will control the rendering
  • image: the image where the snapshot will be rendered to (if null a new image will created)

So i will create the carbon texture using the Canvas node with the following method


public final ImagePattern createCarbonPattern() {
    final double WIDTH        = 12;
    final double HEIGHT       = 12;
    final Canvas CANVAS       = new Canvas(WIDTH, HEIGHT);
    final GraphicsContext CTX = CANVAS.getGraphicsContext2D();

    double offsetY = 0;

    CTX.beginPath();
    CTX.rect(0, 0, WIDTH * 0.5, HEIGHT * 0.5);
    CTX.closePath();

    CTX.setFill(new LinearGradient(0, offsetY * HEIGHT,
                                   0, 0.5 * HEIGHT + offsetY * HEIGHT,
                                   false, CycleMethod.NO_CYCLE,
                                   new Stop(0, Color.rgb(35, 35, 35)),
                                   new Stop(1, Color.rgb(23, 23, 23))));
    CTX.fill();

    CTX.beginPath();
    CTX.rect(WIDTH * 0.083333, 0, WIDTH * 0.333333, HEIGHT * 0.416666);
    CTX.closePath();
    offsetY = 0;
    CTX.setFill(new LinearGradient(0, offsetY * HEIGHT,
                                   0, 0.416666 * HEIGHT + offsetY * HEIGHT,
                                   false, CycleMethod.NO_CYCLE,
                                   new Stop(0, Color.rgb(38, 38, 38)),
                                   new Stop(1, Color.rgb(30, 30, 30))));
    CTX.fill();

    CTX.beginPath();
    CTX.rect(WIDTH * 0.5, HEIGHT * 0.5, WIDTH * 0.5, HEIGHT * 0.5);
    CTX.closePath();
    offsetY = 0.5;
    CTX.setFill(new LinearGradient(0, offsetY * HEIGHT,
                                   0, 0.5 * HEIGHT + offsetY * HEIGHT,
                                   false, CycleMethod.NO_CYCLE,
                                   new Stop(0, Color.rgb(35, 35, 35)),
                                   new Stop(1, Color.rgb(23, 23, 23))));
    CTX.fill();

    CTX.beginPath();
    CTX.rect(WIDTH * 0.583333, HEIGHT * 0.5, WIDTH * 0.333333, HEIGHT * 0.416666);
    CTX.closePath();
    offsetY = 0.5;
    CTX.setFill(new LinearGradient(0, offsetY * HEIGHT,
                                   0, 0.416666 * HEIGHT + offsetY * HEIGHT,
                                   false, CycleMethod.NO_CYCLE,
                                   new Stop(0, Color.rgb(38, 38, 38)),
                                   new Stop(1, Color.rgb(30, 30, 30))));
    CTX.fill();

    CTX.beginPath();
    CTX.rect(WIDTH * 0.5, 0, WIDTH * 0.5, HEIGHT * 0.5);
    CTX.closePath();
    offsetY = 0;
    CTX.setFill(new LinearGradient(0, offsetY * HEIGHT,
                                   0, 0.5 * HEIGHT + offsetY * HEIGHT,
                                   false, CycleMethod.NO_CYCLE,
                                   new Stop(0, Color.rgb(48, 48, 48)),
                                   new Stop(1, Color.rgb(40, 40, 40))));
    CTX.fill();

    CTX.beginPath();
    CTX.rect(WIDTH * 0.583333, HEIGHT * 0.083333, WIDTH * 0.333333, HEIGHT * 0.416666);
    CTX.closePath();
    offsetY = 0.083333;
    CTX.setFill(new LinearGradient(0, offsetY * HEIGHT,
                                   0, 0.416666 * HEIGHT + offsetY * HEIGHT,
                                   false, CycleMethod.NO_CYCLE,
                                   new Stop(0, Color.rgb(53, 53, 53)),
                                   new Stop(1, Color.rgb(45, 45, 45))));
    CTX.fill();

    CTX.beginPath();
    CTX.rect(0, HEIGHT * 0.5, WIDTH * 0.5, HEIGHT * 0.5);
    CTX.closePath();
    offsetY = 0.5;
    CTX.setFill(new LinearGradient(0, offsetY * HEIGHT,
                                   0, 0.5 * HEIGHT + offsetY * HEIGHT,
                                   false, CycleMethod.NO_CYCLE,
                                   new Stop(0, Color.rgb(48, 48, 48)),
                                   new Stop(1, Color.rgb(40, 40, 40))));
    CTX.fill();

    CTX.beginPath();
    CTX.rect(WIDTH * 0.083333, HEIGHT * 0.583333, WIDTH * 0.333333, HEIGHT * 0.416666);
    CTX.closePath();
    offsetY = 0.583333;
    CTX.setFill(new LinearGradient(0, offsetY * HEIGHT,
                                   0, 0.416666 * HEIGHT + offsetY * HEIGHT,
                                   false, CycleMethod.NO_CYCLE,
                                   new Stop(0, Color.rgb(53, 53, 53)),
                                   new Stop(1, Color.rgb(45, 45, 45))));
    CTX.fill();

    final Image PATTERN_IMAGE = CANVAS.snapshot(new SnapshotParameters(), null);
    final ImagePattern PATTERN = new ImagePattern(PATTERN_IMAGE, 0, 0, WIDTH, HEIGHT, false);

    return PATTERN;
}


Most of the above code is drawing code to create the carbon look but the really interesting part are the last rows of that code.
There I create the snapshot of the CANVAS node and store it in an image named PATTERN_IMAGE. Then i take this image as image for the ImagePattern that will be returned by the method.


To fill a shape with the created ImagePattern i simply have to write the following code


Shape background = new Circle(200, 200, 160);
background.setFill(createCarbonPattern());


And the result will look similar to the following image (which is a little bit polished)




As you can see this works like a charm. I just have to say 


T H A N K   Y O U   S O   M U C H


to the JavaFX team, you really do a great job !!!


I hope you get an idea on how to use these new features in the upcoming JavaFX 2.2 release.


So keep coding...

Friday, May 25, 2012

Dead bitmaps could be beautiful... Part I

In the SteelSeries Swing library i use this conical gradient to visualize the frames of the gauges like this


When I started porting the gauges to JavaFX I stumbled upon the described problem and that's the reason why you won't find these frame designs in the current version of the gauges in the JFXtras project.

The much i like the scenegraph of JavaFX it has it's drawbacks too. E.g. if you would like to draw a cone like on the following image as a background image


The problem is that in a scenegraph every node is "alive" where in an image every pixel is more or less "dead". The advantage of death bitmaps is that you have to draw them once and after that you simply use the whole image where in a scenegraph every "living" node will be accessible as long as the scenegraph is alive. That means all the Line nodes that would be needed to create such a cone are always on the scenegraph which would slow it down.
So it absolutely makes sense to have an image node on which you could draw something once and after that simply use this image node to visualize something like this cone. With this solution you would end up with only one node instead of thousands of nodes for the same effect.
With JavaFX 2.2 there will be the Canvas node which represents exactly this image node I was talking about. The name canvas might remind you on the HTML5 canvas everybody is talking about and you are absolutely right. The Canvas node 2D API in JavaFX will be very similar to the 2D api that is used in HTML5 canvas. This means if you are familiar with the HTML5 canvas node it should be easy to work with the JavaFX Canvas node.
Well to be honest in my dreams the image node would not been using the Canvas api but the JavaFX api to be more consistent but...ok now we have to live with the Canvas api which is fine too.

So what I would like to do is to create a Shape object and apply a conical gradient to it...hmm...there are NO custom paints in JavaFX right now...
To be able to use the conical gradient in an easy way i created a little helper method named createConicalGradient() which takes the following arguments
  • a Shape object
  • an array of Stop objects
  • the rotation offset
and here it is...

private Canvas createConicalGradient(final Shape SHAPE, final Stop[] STOPS, final double ROTATION_OFFSET) {
    final Canvas CANVAS = new Canvas(SHAPE.getLayoutBounds().getWidth(), SHAPE.getLayoutBounds().getHeight());
    createConicalGradient(CANVAS, SHAPE, STOPS, ROTATION_OFFSET);
    return CANVAS;
}

private void createConicalGradient(final Canvas CANVAS, final Shape SHAPE, final Stop[] STOPS, final double ROTATION_OFFSET) {
    // adjust size of canvas to size of shape if needed
    if (CANVAS.getLayoutBounds().getWidth() < SHAPE.getLayoutBounds().getWidth()) {
        CANVAS.setWidth(SHAPE.getLayoutBounds().getWidth());
    }
    if (CANVAS.getLayoutBounds().getHeight() < SHAPE.getLayoutBounds().getHeight()) {
        CANVAS.setHeight(SHAPE.getLayoutBounds().getHeight());
    }
    // create clip shape
    final Shape CLIP = SHAPE;
    CLIP.setTranslateX(-SHAPE.getLayoutBounds().getMinX());
    CLIP.setTranslateY(-SHAPE.getLayoutBounds().getMinY());
    // adjust position of canvas in relation to shape
    CANVAS.setLayoutX(SHAPE.getLayoutBounds().getMinX());
    CANVAS.setLayoutY(SHAPE.getLayoutBounds().getMinY());
    CANVAS.setClip(CLIP);
    // create the gradient with the given stops
    final GraphicsContext CTX         = CANVAS.getGraphicsContext2D();
    final Bounds BOUNDS               = SHAPE.getLayoutBounds();
    final Point2D CENTER              = new Point2D(BOUNDS.getWidth() / 2, BOUNDS.getHeight() / 2);
    final double RADIUS               = Math.sqrt(BOUNDS.getWidth() * BOUNDS.getWidth() + BOUNDS.getHeight() * BOUNDS.getHeight()) / 2;
    final double ANGLE_STEP           = 0.1;
    final GradientLookup COLOR_LOOKUP = new GradientLookup(STOPS);
    CTX.translate(CENTER.getX(), CENTER.getY());
    CTX.rotate(-90 + ROTATION_OFFSET);
    CTX.translate(-CENTER.getX(), -CENTER.getY());
    for (int i = 0, size = STOPS.length - 1; i < size; i++) {
        for (double angle = STOPS[i].getOffset() * 360; Double.compare(angle,STOPS[i + 1].getOffset() * 360) <= 0; angle += 0.1) {
            CTX.beginPath();
            CTX.moveTo(CENTER.getX() - RADIUS, CENTER.getY() - RADIUS);
            CTX.setFill(COLOR_LOOKUP.getColorAt(angle / 360));
            if (RADIUS > 0) {
                CTX.fillArc(CENTER.getX() - RADIUS, CENTER.getY() - RADIUS, 2 * RADIUS, 2 * RADIUS, angle, ANGLE_STEP, ArcType.ROUND);
            } else {
                CTX.moveTo(CENTER.getX() - RADIUS, CENTER.getY() - RADIUS);
            }
            CTX.fill();
        }
    }
}

This method either uses an existing Canvas object or creates a new one, then it uses the given Shape object as the clip and draws the conical gradient by using the given Stop objects. To make sure that the conical gradient will fill the whole area I took the diagonal of the given shape to calculate the radius for the gradient. In this example i use a class named GradientLookup that is not part of JavaFX but could be found in the JFXtras project. This class makes it possible to interpolate colors in a given array of Stop objects. To create a radial frame that looks like the one in the first picture you just have to write the following code:

Pane pane = new Pane();

Stop[] stops = {
    new Stop(0.0, Color.rgb(254, 254, 254)),
    new Stop(0.125, Color.BLACK),
    new Stop(0.35, Color.rgb(153, 153, 153)),
    new Stop(0.5, Color.BLACK),
    new Stop(0.65, Color.rgb(153, 153, 153)),
    new Stop(0.875, Color.BLACK),
    new Stop(1.0, Color.rgb(254, 254, 254))
};
        
Shape shape = Shape.subtract(new Circle(200, 200, 200), new Circle(200, 200, 168));

Canvas canvas = createConicalGradient(shape, stops, 0);

pane.getChildren().addAll(canvas);

Scene scene = new Scene(pane, 800, 800, Color.rgb(68, 68, 68));

The result of this code looks like this




It's close to the original but not the same quality. This is because of the technique I use to create the gradient. In principle I create a lot of arc objects and vary the fill color of these arcs. So you could imagine that the stepsize that is used to calculate the angles for these Arc objects is very important for the quality of the resulting image. Another example of the conical gradient could be a stainless steel effect like this...






That's it for Part I, so stay tuned and keep coding...

Wednesday, May 16, 2012

Yes...we can...

A long long time ago i wrote the last blog post but now i'm back...
Some of you might already know that i've changed my job (again) and now am working for canoo engineering in Basel (Switzerland).
To make it short...i really love it :)


Yesterday i was playing around with the new release of Adobe Fireworks CS6 to learn about the new features of the new version. Because i'm working on JavaFX 2.x now a lot i had a special interest in the new CSS capabilities of Fireworks CS6 to check if they might be of use for my JavaFX development. 
As an very small example i will take the following image (you might know it from the iPhone):




If you take a look at the design in Adobe Fireworks it's made out of three shapes:

  • background
  • gloss
  • arrow



If i would like to convert this image to JavaFX i could do this by using my own FXG Converter but that would mean i have to use the FXG file format which is only available on Adobe products. So to make this blogpost more usefull i decided to stick to a more widely used format...SVG.
To be able to export SVG from Fireworks one has to install a little extension created by Aaron Beall which is named Export and could be found here. (This extension will also work on Fireworks CS5)
Now with this extension in place i exported the SVG file of the image and got the following file:



<?xml version="1.0" standalone="no"?>
<!-- Generator: Adobe Fireworks CS6, Export SVG Extension by Aaron Beall (http://fireworks.abeall.com) . Version: 0.6.0  -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg id="" viewBox="0 0 128 128" style="background-color:#ffffff00" version="1.1"
     xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" 
     xml:space="preserve" x="0px" y="0px" width="128px" height="128px">
  <defs>
    <filter id="filter1" x="-100%" y="-100%" width="300%" height="300%">
      <!-- Drop Shadow -->
      <feOffset result="out" in="SourceGraphic" dx="-1.2941" dy="-4.8296"/>
      <feColorMatrix result="out" in="out" type="matrix" 
                     values="0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 0.451 0"/>
      <feGaussianBlur result="out" in="out" stdDeviation="3"/>
      <feBlend in="SourceGraphic" in2="out" mode="normal" result="Drop_Shadow1"/>
    </filter>
  </defs>
  <path id="background" 
        d="M 62.6381 6.8085 C 31.0529 6.8085 5.447 32.4156 5.447 64 
           C 5.447 66.482 5.6577 68.9089 5.9636 71.3072 C 9.5577 99.4342 33.5351 
           121.1915 62.6381 121.1915 C 91.7415 121.1915 115.7188 99.4342 119.313 
           71.3072 C 119.6185 68.9089 119.83 66.482 119.83 64 C 119.83 32.4156 
           94.2232 6.8085 62.6381 6.8085 Z" 
        stroke="#ffffff" 
        stroke-width="11" 
        fill="#6096e3"/>
  <path id="gloss" 
        d="M 62.6381 114.383 C 88.9706 114.383 110.6626 94.6968 113.9152 69.2487 C 
           100.6936 60.109 82.6032 54.4681 62.6381 54.4681 C 42.673 54.4681 24.5821 
           60.109 11.3617 69.2487 C 14.6139 94.6968 36.3059 114.383 62.6381 114.383 
           Z" 
        fill="#236ed8"/>
  <path id="arrow" 
        filter="url(#filter1)" 
        d="M 44.7016 41.0951 L 69.3251 65.7207 L 44.5005 90.2008 L 55.7281 101.4289 
           L 92.0866 65.0698 L 56.5768 29.5638 L 44.7016 41.0951 Z" 
        fill="#ffffff"/>
</svg>



So you might ask yourself why i choosed svg for the conversion, well there's a really simple reason named SVGPath. With this Object you will be able to simple copy the path definition from the svg file and paste it as a string into your JavaFX code. The arrow of the example would look like this:



String arrow = "M 44.7016 41.0951 L 69.3251 65.7207 L 44.5005 90.2008 L 55.7281 
                101.4289 L 92.0866 65.0698 L 56.5768 29.5638 L 44.7016 41.0951 Z";        
SVGPath arrowShape = SVGPathBuilder.create()
                                   .content(arrow)                                        
                                   .build();

That's easy isn't it? But now we also need to style these shapes with css and that's unfortunately not as easy as the shape "conversion". Well it's still easy enough but you can't simply copy paste some code. This is because the css definition in JavaFX has it's own syntax (which is nothing special if you take a look at all the different browser vendors which do exactly the same).


In Adobe Fireworks CS6 you will find support for CSS in a way that makes working with stylesheets a charm. For the background in our example it will look like this:




You could see that i've selected the background circle and the CSS Properties window shows the css style definition for the selected shape. But if you know the JavaFX css definitions you will also see that these are different and that you have to modify them manually which will lead to this:


-fx-fill        : rgb(96, 150, 227);
-fx-stroke      : rgb(255, 255, 255);
-fx-stroke-width: 11px;


This information is also in the svg file and could be taken from there too which means it's up to you where you take it from.
The advantage of the css support in Adobe Fireworks CS6 is more that you could modify the drawing and directly get the css style information like colors and gradients.


In the end the conversion of the drawing led to the following JavaFX code:



Pane pane = new Pane();


String background       = "M 62.6381 6.8085 C 31.0529 6.8085 5.447 32.4156 5.447 64 
                           C 5.447 66.482 5.6577 68.9089 5.9636 71.3072 C 9.5577 
                           99.4342 33.5351 121.1915 62.6381 121.1915 C 91.7415 
                           121.1915 115.7188 99.4342 119.313 71.3072 C 119.6185 
                           68.9089 119.83 66.482 119.83 64 C 119.83 32.4156 94.2232 
                           6.8085 62.6381 6.8085 Z";
String backgroundStyle  = "-fx-fill        : rgb(96,150,227);" +
                          "-fx-stroke      : rgb(255,255,255);" +
                          "-fx-stroke-width: 11px;";
SVGPath backgroundShape = SVGPathBuilder.create()
                                        .content(background)
                                        .style(backgroundStyle)
                                        .build();


String gloss       = "M 62.6381 114.383 C 88.9706 114.383 110.6626 94.6968 113.9152 
                      69.2487 C 100.6936 60.109 82.6032 54.4681 62.6381 54.4681 C 
                      42.673 54.4681 24.5821 60.109 11.3617 69.2487 C 14.6139 
                      94.6968 36.3059 114.383 62.6381 114.383 Z";
String glossStyle  = "-fx-fill  : rgb(35,110,216);" +
                     "-fx-stroke: transparent;";
SVGPath glossShape = SVGPathBuilder.create()
                                   .content(gloss)
                                   .style(glossStyle)
                                   .build();


String arrow       = "M 44.7016 41.0951 L 69.3251 65.7207 L 44.5005 90.2008 L 
                      55.7281 101.4289 L 92.0866 65.0698 L 56.5768 29.5638 L 
                      44.7016 41.0951 Z";
String arrowStyle  = "-fx-fill  : rgb(255,255,255);" +
                     "-fx-stroke: transparent;" +
                     "-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.35), 4, 0.1, 
                                             -2px, -4px);";
SVGPath arrowShape = SVGPathBuilder.create()
                                   .content(arrow)
                                   .style(arrowStyle)
                                   .build();


pane.getChildren().addAll(backgroundShape, glossShape, arrowShape);



And here you will see the original (left) and the converted version (right):




So if you have to convert a shape from your drawing to code you might want to use SVGPath which makes this task a piece of cake.


The nice thing about this approach is that once the path is a SVGPath you could do everything with it that works on javafx.scene.shape.Shape like scaling, adding effects etc.


And there's one more thing that might be interesting for you...one could use the SVG paths directly in css. This will allow you to separate the visualization completely from the code. In this case you would only define a StackPane in the code and apply a style to the stackpane object. The appearance of this stackpane object is then completely taken from the given css definition.
The code for the example above would than look like this:



Pane pane = new Pane();


StackPane backgroundPane   = new StackPane();
String backgroundPaneStyle = 
    "-fx-background-color  : transparent, rgb(255,255,255), rgb(96,150,227);" +
    "-fx-shape             : \"M 62.6381 6.8085 C 31.0529 6.8085 5.447 32.4156 
                               5.447 64 C 5.447 66.482 5.6577 68.9089 5.9636 
                               71.3072 C 9.5577 99.4342 33.5351 121.1915 62.6381 
                               121.1915 C 91.7415 121.1915 115.7188 99.4342 119.313 
                               71.3072 C 119.6185 68.9089 119.83 66.482 119.83 64 C 
                               119.83 32.4156 94.2232 6.8085 62.6381 6.8085 Z\";" +
    "-fx-background-insets : 0, 0, 11;" +
    "-fx-padding           : 0 128 128 0;";
backgroundPane.setStyle(backgroundPaneStyle);


StackPane glossPane   = new StackPane();
String glossPaneStyle = 
    "-fx-background-color: transparent, transparent, rgb(35,110,216);" +
    "-fx-shape           : \"M 62.6381 114.383 C 88.9706 114.383 110.6626 94.6968 
                             113.9152 69.2487 C 100.6936 60.109 82.6032 54.4681 
                             62.6381 54.4681 C 42.673 54.4681 24.5821 60.109  
                             11.3617 69.2487 C 14.6139 94.6968 36.3059 114.383 
                             62.6381 114.383 Z\";" +
    "-fx-padding         : 0 103 60 0;";
glossPane.setStyle(glossPaneStyle);
glossPane.relocate(13, 56);


StackPane arrowPane = new StackPane();
String arrowPaneStyle =
    "-fx-background-color: transparent, transparent, white;" +
    "-fx-shape  : \"M 44.7016 41.0951 L 69.3251 65.7207 L 44.5005 90.2008 L 55.7281 
                    101.4289 L 92.0866 65.0698 L 56.5768 29.5638 L 44.7016 41.0951 
                    Z\";" +
    "-fx-padding: 0 48 73 0;" +       
    "-fx-effect : dropshadow(gaussian, rgba(0,0,0,0.35), 4, 0.1, -2px, -4px);";
arrowPane.setStyle(arrowPaneStyle);
arrowPane.relocate(45, 30);


pane.getChildren().addAll(backgroundPane, glossPane, arrowPane);



And to be complete again a comparison between the original and the style based approach:








Keep in mind that you would usually store the style information in a separate css file instead of using it inline.


That's it so far...i hope this post will be usefull for one or the other...




Keep coding...