I have a Java application where I need to draw text on top of an image. The text, the font, and the image are all determined at run time. The text needs to look nice, yet be readable (sufficiently contrastive) on top of the image.
To meet these requirements, I create a drop shadow. This is done by drawing the text in opaque black, on a blank/transparent BufferedImage, then applying a Gaussian blur filter. I then draw the text again, in opaque white, on top of the drop shadow. So I have opaque white text, with a black blurred shadow around it that quickly fades to full transparency. I can then draw this image on top of the background image.
The problem I'm trying to solve is that the drop shadow seems too transparent. So against bright, busy backgrounds, it doesn't give the white text enough separation.
So how to increase the opacity of the shadow? I've tried increasing the radius of the gaussian blur, and that makes the shadow wider, but doesn't make it more opaque.
The code I'm using is based on this DropShadowDemo by Romain Guy. I use his createDropShadow() and gaussianBlurFilter(). But instead of painting the drop shadow and the text separately during paintComponent(), I draw them both onto a BufferedImage in advance; and I draw this single BufferedImage on top of the background during paintComponent(). Maybe that's my problem? But I don't see how that would decrease the opacity of the shadow. I'm not using g2.setComposite() during paintComponent().
I've looked at adjusting the opacity of the drop shadow using some kind of BufferedImageOp, such as a LookupOp. But it seems like a lot of work for a simple adjustment (creating four arrays of numbers, I guess). I don't think a RescaleOp would work, since I want the result alpha to fall in the same range (0 to 1) as the source alpha. If I could specify a BufferedImageOp that sets new alpha = sqrt(old alpha), or something like that, that would be ideal. But I don't know an easy way to do that.
Details of the code can be seen here:
ShadowedText.java (creates the text-with-drop-shadow image)
SetBeforeMe.java (implements paintComponent() that draws the image)
I would include relevant code blocks here, but it seems like the relevant amount of code is too big (wall of code)... might as well just give links to the source files.
Update
It looks like Change the alpha value of a BufferedImage? would be a way to change the opacity of the drop shadow... basically recalculating the alpha value of each pixel, one by one. TBD: whether it's portable (to 64-bit machines, e.g.), and whether it's fast enough. If I do a = sqrt(a) or a = sin(a * pi * 0.5) on every pixel (thinking of a in the range 0 to 1), will that be slow? I would be happy to know if there's a simpler way that takes advantage of available optimizations, like the built-in BufferedImageOps presumably do. Maybe the answer is building arrays for LookupOp after all. Anybody know of some example code for that?
Final update
Solved using a LookupOp; see code below.
Below is the code I ended up with to make a BufferedImage more opaque. I decided to go ahead and use a LookupOp, rather than a potentially unportable and slow loop over getRGB / setRGB on each pixel. The work to set up Lookup arrays wasn't so bad.
/* Make alpha channel more opaque.
* Modify the alpha (opacity) channel so that values are higher, but still
* continuous and monotonically increasing.
*/
private static void adjustOpacity(BufferedImage shadowImage) {
// Use a lookup table with four arrays;
// the three for RGB are identity arrays (no change).
byte identityArray[] = new byte[256];
for (int i=0; i < 256; i++)
identityArray[i] = (byte)i;
byte alphaArray[] = new byte[256];
// map the range (0..256] to (0..pi/2]
double mapTo90Deg = Math.PI / 2.0 / 256.0;
for (int i=0; i < 256; i++) {
alphaArray[i] = (byte)(Math.sin(i * mapTo90Deg) * 256);
}
byte[][] tables = {
identityArray,
identityArray,
identityArray,
alphaArray
};
ByteLookupTable blut = new ByteLookupTable(0, tables);
LookupOp op = new LookupOp(blut, null);
// With LookupOp, it's ok for src and dest to be the same image.
op.filter(shadowImage, shadowImage);
}
It seems to work (though I haven't taken before-and-after screenshots for comparison).
Related
I am making a platformer game using Java. I have a character in the form of a JLable with an Image Icon that can be moved around using the arrow keys. I am using the setBounds() method to set the position of the JLabel, but this method only takes int values as x and y arguments, and I would like more precise control. Using a null layout, is it possible to use more precise values such as double to position a JLabel?
Thank you for your time, and have a great day.
Well if you are using JLabels then you are using Swing, and Swing has non-floating point coordinates system, so no you cannot.
Looking at the JavaDocs for JLabel...
setLocation takes ints as parameters. There doesn't seem to be a way to use decimal coordinates.
JavaFX can do. However in Swing the coordinates are integer pixels.
You could draw scaled 2x2 in memory (Image getGraphics) and then rescale to achieve half int steps of 0.5; but that would blur (interpolated pixels) or flicker (best nearby match).
For zooming scalable vector graphics, .vg, using the batik library would be feasible.
For text subpixel hinting can on some LED screens position black text on thirds of a pixel, depending on the layout of the Red, Green and Blue subpixels.
In general it is not worth the effort to attempt the above. Except that JavaFX has nice animation support, CSS styling and animation effects.
As others are saying, UI components are designed to position only with exact pixel coordinates.
You could store a location in double and then round it to int when you actually set the location. Something like this:
double velX = ..., velY = ...;
double x = ..., y = ...;
void example() {
int t = 1000 / 30;
new Timer(t, (ActionEvent e) -> {
x += t * velX;
y += t * velY;
label.setLocation((int) Math.round(x),
(int) Math.round(y));
}).start();
}
That way you can move in smaller increments. It just won't be visible.
There is a really great example of animating an image here in a way very similar to what you're describing which you should take a look at. Trying to position components is really more trouble than it's worth, in my opinion, especially if you're trying to make something like a game. Performing custom painting is more flexible. (The tutorial is here.)
first of all I have scoured Google and SO for this answer, finding only how to change the actual pixels to be of a certain alpha value, which would be incredibly slow or actually making a part of the BufferedImage completely transparent via the use of lwg2.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR)). This is the exact functionality I need, however, I need to have the value to be less than 1f, which you cannot do with this specific instance of AlphaComposite.CLEAR.
What I want this implementation for is to make a wall inside my 2.5d game become transparent when the player goes behind it, like so:
The logic behind my game is that the terrain is one BufferedImage which is only updated when called, and then having the rest of the walls, etc, being drawn onto another BufferedImage where entities are also drawn, so the opacity transformation would only affect the trees (or walls).
This is the code I am using atm, but as I said I don't want the circle that I am drawing to make a part of the image completely transparent, but only slightly (about 50%):
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, 0.5f));
g2.fillOval(x - (int) (TILE_WIDTH * 1), y - (int) (TILE_HEIGHT * 1.5), TILE_WIDTH * 2, TILE_HEIGHT * 3);
(The 0.5f in the AlphaComposite constructor does nothing).
The reason I need this to be efficient is because I am updating this image 30 times a second, so efficiency > quality.
So, I ended up solving the issue by not manipulating the image directly via making a part of the image translucent, but manipulating the opacity of the images I am drawing with. As #user343760 and #NESPowerGlove mentioned, I could just make the assets I am using translucent when the player is behind it. Since I am using a underlying grid array to back my game, I could do this by working out if the tile.x - 1 == (int) player.x and tile.y - 1== (int) player.y. In isometry, this meant that the player was on the tile directly above it in our perspective. Then I had to solve the issue if the wall.z is bigger than 0 or 1, hence having the problem where a tile 5 blocks "bellow" the player could obstruct him if the walls extended z = 5 above the tile. For this problem, I implemented the following solution:
for(int i = 0; i < wall.getAsset(1f).getHeight()/TILE_HEIGHT; i++) {
if((tile.x - i - wall.z == (int) world.player.getX() && tile.y - i -wall.z == (int) world.player.getY())) {
lwg2.drawImage(wall.getAsset(0.5f), x, y, this);
}
}
This also ensures that the image is transparent even if the player is "above" the tile "above" the tile where the wall is situated, in terms of the image extending above that limit. I have fixed this via using the for loop which looks above for i number of times, depending on the image.height/tile_height, which is an universal constant.
If you require to make a part of the image transparent, I have not found solutions which would work fault free, except for manipulating the pixels in the low-levels of BufferedImage. If you also want to erase a part of an image directly, use the code g2.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR)); and draw as you would normally. Remember to switch back to a normal composite via g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));.
You could also draw with a certain opacity in the first place using the Composite g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacity));, where opacity is a float with values from 0f to 1f, 0f being completely transparent and 1f being completely opaque.
I hope this helped anyone out there. If you find a better way of doing this, please leave a comment for future readers.
This is what my solution looks like :):
I am making turn based strategy based on a tiled map. My map contains two layers - first one is visible all the time and is used as a background, the second one is used for highlighting the tiles. Highlighted tiles shows possible movement for the player. For now the progress looks like this:
My border boxes pic
To make a highlighting possible I use this for loop:
for(int s = 0;s<7;){
for (int i = 0;i<7;i++) {
if ((inField(clickx + i,clicky + s)) && !(tileWithPlayer(clickx + i,clicky + s)))
{
highlight_layer.getCell(clickx + i, clicky+s).setTile(mark);
}
}
s++;
}
In this I use one tile from second layer of the map to make a border for a specific tiles. For now this tile is only 2px border of 32x32 transparent tile. To make highlighting disappear, I use similar for loop, where I set the tile to null. Selecting/deselecting tiles works great, but it is probably not the most efficient way to do this. I would like to exchange the blue border boxes into colored transparent boxes, something like in Advance wars during the movement, or as it can be seen below:
Desired look pic
I tried to make a semi transparent tile on the second layer in tiled, but without luck. It could be nice if there is some possibility to apply only semi-transparent color on the specific tiles, without using the second layer tile. So, my question is - how can I exchange those ugly borders, into fully colored, (semi)transparent tiles, to achieve similar result as in above picture?
Well...okay. I'm not an expert with Tiled. I've never really worked with it much but doesn't it support PNG tilemap imports? As in...make a tile texture that is semi transparent and then use that in your map on the second layer and only activate the ones that you really need?
Alternatively you could do it programmatically. Where for every tile that you need to highlight you just create a new TextureRegion with the coortinates of your tile:
float x = tileWidth * col;
float y = tileHight * row;
final TextureRegion region = Assets.getRegion(...);
This way you load a texture region to be displayed at the right position whenever you need it to. You could even (if the range is never going to change) store them in a collection and then with every move shift their coordinates by one in whatever direciton.
This would at least be my approach, not really knowing anything about Tiled.
I have used a HexagonTile map before, however I had to write my own implementation (based on LibGDX). Here is the code, in case it helps you to further understand the idea: https://github.com/2RSoftworks/interstellarempires/blob/master/IEMaster/IEClient/src/main/de/r2soft/empires/client/maps/hex/R2HexMapRenderer.java#L167
Hope this helped.
I have used the LinearGradientPaint class to draw a rectangle dynamically filled with user-defined colors. This happens by overriding the paintComponent method.
Here is how it looks like:
.
You can see the small thumbs/ handle on top. The user can move these around, delete them, and add new ones. They can also change the color. As a result the user can completely customize how the gradient looks like. This works just fine so far, no issue.
What I need now, and I tried searching for this info, is to get RGB-values anywhere on this gradient.
I only know the x-amount of colors that LinearGradientPaint uses to generate the gradient. I know at what point (fraction) these colors are located (the number below the gradient box, corresponding with the 'thumbs' on top.
Is there anyway to get the colors in between the RGB-values which are used to generate the gradient? In my example above I mean the darkened red or green areas.
If this really is a linear gradient between new Color(r1,g1,b1) and new Color(r2,g2,b2), then the colour at x, where 0 <= x <= 1 is
new Color((int)(r1*(1-x)+r2*x),(int)(g1*(1-x)+g2*x),(int)(b1*(1-x)+b2*x));
Of course, I have no idea whether this is the formula that is actually used inside LinearGradientPaint - but it ought to be something equivalent to this.
A way you might be able to do this, is to create a 1 pixel high BufferedImage of the same width of your component, render the gradient to it and simple use something like BufferedImage#getRGB.
This will return a int packed color value, which you can then use Color(int) to return a Color object, which makes it easier to extract the color components of the pixel.
Of course, this would all be easier if you used the BufferedImage as your primary output as well, then you would only have to create it once and because you'd be updating the image so it could be rendered to the screen, it would also be up-to-date
I'm using Java Graphics2D to generate this map with some sort of tinted red overlay over it. As you can see, the overlay gets cut off along the image boundary on the left side:-
After demo'ing this to my project stakeholders, what they want is for this overlay to clip along the map boundary with some consistent padding around it. The simple reason for this is to give users the idea that the overlay extends outside the map.
So, my initial thought was to perform a "zoom and shift", by creating another larger map that serves as a "cookie cutter", here's my simplified code:-
// polygon of the map
Polygon minnesotaPolygon = ...;
// convert polygon to area
Area minnesotaArea = new Area();
minnesotaArea.add(new Area(minnesotaPolygon));
// this represents the whole image
Area wholeImageArea = new Area(new Rectangle(mapWidth, mapHeight));
// zoom in by 8%
double zoom = 1.08;
// performing "zoom and shift"
Rectangle bound = minnesotaArea.getBounds();
AffineTransform affineTransform = new AffineTransform(g.getTransform());
affineTransform.translate(-((bound.getWidth() * zoom) - bound.getWidth()) / 2,
-((bound.getHeight() * zoom) - bound.getHeight()) / 2);
affineTransform.scale(zoom, zoom);
minnesotaArea.transform(affineTransform);
// using it as a cookie cutter
wholeImageArea.subtract(minnesotaArea);
g.setColor(Color.GREEN);
g.fill(wholeImageArea);
The reason I'm filling the outside part with green is to allow me to see if the cookie cutter is implemented properly. Here's the result:-
As you can see, "zoom and shift" doesn't work in this case. There is absolutely no padding at the bottom right. Then, I realized that this technique will not work for irregular shape, like the map... and it only works on simpler shapes like square, circle, etc.
What I want is to create consistent padding/margin around the map before clipping the rest off. To make sure you understand what I'm saying here, I photoshopped this image below (albeit, poorly done) to explain what I'm trying to accomplish here:-
I'm not sure how to proceed from here, and I hope you guys can give me some guidance on this.
Thanks.
I'll just explain the logic, as I don't have time to write the code myself. The short answer is that you should step through each pixel of the map image and if any pixels in the surrounding area (i.e. a certain distance away) are considered "land" then you register the current pixel as part of the padding area.
For the long answer, here are 9 steps to achieve your goal.
1. Decide on the size of the padding. Let's say 6 pixels.
2. Create an image of the map in monochrome (black is "water", white is "land"). Leave a margin of at least 6 pixels around the edge. This is the input image: (it isn't to scale)
3. Create an image of a circle which is 11 pixels in diameter (11 = 6*2-1). Again, black is empty/transparent, white is solid. This is the hit-area image:
4. Create a third picture which is all black (to start with). Make it the same size as the input image. It will be used as the output image.
5. Iterate each pixel of the input image.
6. At that pixel overlay the hit-area image (only do this virtually, via calculation), so that the center of the hit-area (the white circle) is over the current input image pixel.
7. Now iterate each pixel of the hit-area image.
8. If the any white pixel of the hit-area image intersects a white pixel of the input image then draw a white pixel (where the center of the circle is) into the output image.
9. Go to step 5.
Admittedly, from step 6 onward it isn't so simple, but it should be fairly easy to implement. Hopefully you understand the logic. If my explanation is too confusing (sorry) then I could spend some time and write the full solution (in Javascript, C# or Haskell).