I am attempting to control a 240 long line of RGB pixels (ws2812b) using an artnet to dmx controller and need to generate colour gradients down the length of the line of pixels.
I had the idea of using the C# built in graphics libraries to generate the colour gradients and then extract the individual pixel values and send these to the dmx controller.
Is it possible to extract individual interpolated values from a LinearGradientBrush or a LinearGradientBrush applied to a shape (line/rectangle etc)?
What you could do is let the brush draw a line on a bitmap and extract the pixels from that, but I believe that would be unnecessarily expensive and complicated. What would be better is simply lerping between the colours you want.
This can be achieved by writing a lerp method like so:
float Lerp(float from, float to, float amount)
{
return from + amount * (to - from);
}
and using this for the R G and B values of the colors you want to lerp between. For example:
Color Lerp(Color from, Color to, float amount)
{
return Color.FromArgb(
(int)Lerp(from.R, to.R, amount),
(int)Lerp(from.G, to.G, amount),
(int)Lerp(from.B, to.B, amount));
}
I hope this helps.
~Luca
Here is a function that takes a list of stop colors and returns a list of evenly interpolated colors:
List<Color> interpolateColors(List<Color> stopColors, int count)
{
List<Color> ColorList = new List<Color>();
using (Bitmap bmp = new Bitmap(count, 1))
using (Graphics G = Graphics.FromImage(bmp))
{
Rectangle bmpCRect = new Rectangle(Point.Empty, bmp.Size);
LinearGradientBrush br = new LinearGradientBrush
(bmpCRect, Color.Empty, Color.Empty, 0, false);
ColorBlend cb = new ColorBlend();
cb.Colors = stopColors.ToArray();
float[] Positions = new float[stopColors.Count];
for (int i = 0; i < stopColors.Count; i++)
Positions [i] = 1f * i / (stopColors.Count-1);
cb.Positions = Positions;
br.InterpolationColors = cb;
G.FillRectangle(br, bmpCRect);
for (int i = 0; i < count; i++) ColorList.Add(bmp.GetPixel(i, 0));
br.Dispose();
}
return ColorList;
}
You could call it as:
List<Color> ColorList = interpolateColors(
new List<Color>{Color.Red, Color.Blue, Color.Yellow}, 240);
240 and 740 colors. To get all distinct colors make sure they are not too many and not too close, as the maximum number of RGB hues between two colors is 256, so the second example may hit that limit..
Related
I am currently creating sort of a game with C# and am trying to create outfits for the players. I would like to make cloth design and let players chose the colors.
I took pictures from gamefiles of TibiaME (tibiame.com), which does pretty much what i want to to.
How can I Fill this form with color? When I try to replace a certain color, it does not work, since it's not the same everyhwere. The shadows look pretty cool :P
The simplest (and fastest) way to color (tint) an image is to use a ColorMatrix.
Here is the result of using nine colors to tint the original:
Note that I have photoshopped the posted image bo be transparent around the center part; using just the original looks like this..:
((The glitch in the lower right is in the original..))
Here is a function the returns a list of tinted version of an image, one for each color in a list..:
List<Bitmap> TintImages(Bitmap bmp0, List<Color> colors )
{
List<Bitmap> tinted = new List<Bitmap>();
Size sz = bmp0.Size;
float f = 256f;
for (int i = 0; i < colors.Count; i++)
{
float r = colors[i].R / f;
float g = colors[i].G / f;
float b = colors[i].B / f;
float[][] colorMatrixElements = {
new float[] {r, 0, 0, 0, 0}, // red scaling factor of
new float[] {0, g, 0, 0, 0}, // green scaling factor
new float[] {0, 0, b, 0, 0}, // blue scaling factor
new float[] {0, 0, 0, 1, 0}, // alpha scaling factor
new float[] {0, 0, 0, 0, 1}}; // no further translations
ImageAttributes imageAttributes = new ImageAttributes();
ColorMatrix colorMatrix = new ColorMatrix(colorMatrixElements);
imageAttributes.SetColorMatrix(
colorMatrix,
ColorMatrixFlag.Default,
ColorAdjustType.Bitmap);
Bitmap bmp = new Bitmap(sz.Width, sz.Height);
using (Graphics gr = Graphics.FromImage(bmp))
{
gr.DrawImage(bmp0, new Rectangle(0, 0, sz.Width, sz.Height),
0, 0, sz.Width, sz.Height, GraphicsUnit.Pixel, imageAttributes);
tinted.Add(bmp);
}
}
return tinted;
}
You could iterate over each pixel of the bitmap and make a color shift in the direction you want. When I say colorshoft I mean a you have to adapt the RGB values of each pixel.
A simple shift to red could look like this:
for (int Xcount = 0; Xcount < myBitmap.Width; Xcount++)
{
for (int Ycount = 0; Ycount < myBitmap.Height; Ycount++)
{
//get color of the pixel
Color pixelColor = myBitmap.GetPixel(Xcount, Ycount);
byte red = pixelColor.R;
byte green = pixelColor.G;
byte blue = pixelColor.B;
//make shift and prevent overflow
if (red < 205)
red += 50;
else
red = 255;
//set color of the pixel
myBitmap.SetPixel(Xcount, Ycount, Color.FromRgb(red, green, blue));
}
}
Keep in mind this is just a simple example and may not result in what you expected.
You can read more about the RGB colorspace here: RGB color model and here you find a RGB Color Codes Chart
I am wondering why would this piece of code NOT generate a checkerboard pattern?
pbImage.Image = new Bitmap(8, 8);
Bitmap bmp = ((Bitmap)pbImage.Image);
byte[] bArr = new byte[64];
int currentX = 0;
int currentY = 0;
Color color = Color.Black;
do
{
currentY = 0;
do
{
bmp.SetPixel(currentX, currentY, color);
if (color == Color.Black) color = Color.White; else color = Color.Black;
currentY++;
} while (currentY < bitmapHeight);
currentX++;
} while (currentX < bitmapWidth);
pbImage.Refresh();
Edit: I realized that i need to expand Bitmaps ctor with
new Bitmap(bitmapWidth, bitmapHeight, PixelFormat.Format8bppIndexed)
and it seems SetPixel does not support Indexed Images and expects a Color.
My point is i want to create raw(pure byte array) grayscale images and show it on a picture box, while keeping it as simple as possible, without using any external libraries.
Your calculation fails, because, if you switch at every pixel, then even lines that start with colour 0 will end on the colour 1, meaning the next line will once again start with colour 0.
0101010101010101
0101010101010101
0101010101010101
0101010101010101
etc...
But since, in X and Y coordinates, any horizontal and vertical movement by 1 pixel across the pattern will change the colour, the actual calculation of whether you want a filled or non-filled pixel can be simplified to (x + y) % 2 == 0.
The checkerboard generating function I put below takes an array of colours as colour palette, and allows you to specify which specific indices from that palette to use as the two colours to use on the pattern. If you just want an image with nothing but a 2-colour palette containing black and white, you can just call it like this:
Bitmap check = GenerateCheckerboardImage(8, 8, new Color[]{Color.Black, Color.White}, 0,1);
The generating function:
public static Bitmap GenerateCheckerboardImage(Int32 width, Int32 height, Color[] colors, Byte color1, Byte color2)
{
Byte[] patternArray = new Byte[width * height];
for (Int32 y = 0; y < height; y++)
{
for (Int32 x = 0; x < width; x++)
{
Int32 offset = x + y * height;
patternArray[offset] = (((x + y) % 2 == 0) ? color1 : color2);
}
}
return BuildImage(patternArray, width, height, width, PixelFormat.Format8bppIndexed, colors, Color.Black);
}
The BuildImage function I used is a general-purpose function I made to convert a byte array to an image. You can find it in this answer.
As explained in the rest of that question and the answers on it, the stride argument is the amount of bytes on each line of the image data. For the constructed 8-bit array we got here, that's simply identical to the width, but when loading it's generally rounded to a multiple of 4, and can contain unused padding bytes. (The function takes care of all that, so the input byte array has no such requirements.)
I have a image that contains a layout for a level, and I want to load the level in the game by reading each pixels color from the image, and drawing the corresponding block. I am using this code:
public void readLevel(string path, GraphicsDevice graphics)
{
//GET AN ARRAY OF COLORS
Texture2D level = Content.Load<Texture2D>(path);
Color[] colors = new Color[level.Width * level.Height];
level.GetData(colors);
//READ EACH PIXEL AND DRAW LEVEL
Vector3 brickRGB = new Vector3(128, 128, 128);
int placeX = 0;
int placeY = 0;
foreach (Color pixel in colors)
{
SpriteBatch spriteBatch = new SpriteBatch(graphics);
spriteBatch.Begin();
if (pixel == new Color(brickRGB))
{
Texture2D brick = Content.Load<Texture2D>("blocks/brick");
spriteBatch.Draw(brick, new Rectangle(placeX, placeY, 40, 40), Color.White);
}
if(placeX == 22)
{
placeX = 0;
placeY++;
}
else
spriteBatch.End();
}
}
But it just shows a blank screen. Help would be appreciated!
EDIT: PROBLEM FIXED! (Read htmlcoderexe's answer below) Also, there was another problem with this code, read here.
Your code seems to draw each sprite at one pixel offset from the previous, but your other parameter suggests they are 40 pixel wide. placeX and placeY will need to be multiplied by the stride of your tiles (40).
Also, in the bit where you compare colours, you might be having a problem with floating point colour values (0.0f-1.0f) and byte colours being used together.
new Color(brickRGB)
This translates to:
new Color(new Vector3(128f,128f,128f))
So it tries constructing a colour from the 0.0f-1.0f range, clips it down to 1f (the allowed maximum for float input for Color), and you end up with a white colour (255,255,255), which is not equal to your target colour (128,128,128).
To get around this, try changing
Vector3 brickRGB = new Vector3(128, 128, 128);
to
Color brickRGB = new Color(128, 128, 128);
and this part
if (pixel == new Color(brickRGB))
to just
if (pixel == brickRGB)
You will also need to create your drawing rectangle with placeX and placeY multiplied by 40, but do not write to the variable - just use placeX*40 for now and replace it with a constant later.
I have this constructor that takes in an input of an array of Color objects and some misc strings (like file location and size of list), and stores this heatmap into a bitmap instance called _image_. Everything works out fine, but my main issue is I have no way to render text onto this heatmap. I would like to overlay title text and x and y-axis labels onto this bitmap.
public HeatMap(IEnumerable<Color> colors, int width, int height, string file, int U, int V) {
if (colors == null)
throw new ArgumentNullException("colors");
if (width <= 0)
throw new ArgumentException("width must be at least 1");
if (height <= 0)
throw new ArgumentException("height must be at least 1");
_width = width;
_height = height;
_file = file;
_image = new Bitmap(U, V, PixelFormat.Format32bppArgb);
Graphics graphics = Graphics.FromImage(_image);
graphics.Clear(Color.White);
graphics.Dispose();
int x = 0;
int y = 0;
foreach (Color color in colors) {
_image.SetPixel(x, y, color); // This can be speeded up by using GH_MemoryBitmap if you want.
y++;
if (y >= V) {
y = 0;
x++;
}
if (x >= U)
break;
}
}
Below, I also have a method that resizes the image (without too much distortion), and I figured I might as well make use of this since I have a graphics object I can use, like so:
private Bitmap ResizeBitmap(Bitmap sourceBMP, int width, int height) {
Bitmap result = new Bitmap(width, height);
using (Graphics g = Graphics.FromImage(result)) {
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
g.DrawImage(sourceBMP, 0, 0, width, height);
g.DrawString("A heatmap.", SystemFonts.DefaultFont, Brushes.Black, 10, 10);
}
return result;
}
Using the above, I was able to overlay text over it, as a starting point. My question is, how do I add a white boarder over the above image (so my text doesn't overlap my graph), and then overlay x and y-axis text onto it?
I guess my specific question really is - how do I render text onto, say, every four columns of my heatmap? In most instances, there are 100 x-axis objects, and like 24 y-axis objects. The y-axis would have the time of the day, while the x-axis has the day of the year. My familiarity with using graphics in C# is very low, so any pointers is very appreciated.
You've to use the DrawString before the graphics.Dispose() and after the Clear().
Graphics graphics = Graphics.FromImage(_image);
graphics.Clear(Color.WhiteSmoke);
graphics.DrawString("your text", SystemFonts.DefaultFont, Brushes.Black, new PointF(0, 0));
graphics.Dispose();
Where the PointF(0, 0) represents the up-left corner of your text placeholder, you've to calculate the text position based on the positions of your heatmap.
UPDATE (alignment)
You can align your text in relation with a virtual rectangle, doing something like:
Graphics graphics = Graphics.FromImage(_image);
graphics.Clear(Color.WhiteSmoke);
StringFormat stringFormat = new StringFormat() { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center };
graphics.DrawString("your text", SystemFonts.DefaultFont, Brushes.Black, new RectangleF(0, 0, 100, 100), stringFormat);
graphics.Dispose();
In this case I've created a virtual box of 100x100 positioned on the top-left corner, and the text is h/v centered relatively to this box.
I am trying to use the Clipper library to modify a graphics path.
I have list of widths that represent outlines / strokes. I want to start with the largest first and work my way down to the smallest.
For this example, we will add 2 strokes with widths of 20 and 10.
I want to take take my graphics path, and expand / offset it by 20 pixels into a new graphics path. I do not want to alter the original path. Then I want to fill the new graphics path with a solid color.
Next, I want to take my original graphics path, and expand / offset it by 10 pixels into a new graphics path. I want to fill this new path with a different color.
Then I want to fill my original path with a different color.
What is the proper way to do this. I have the following method that I created to try and do this, but it is not working properly.
private void createImage(Graphics g, GraphicsPath gp, List<int> strokeWidths)
{
ClipperOffset pathConverter = new ClipperOffset();
Clipper c = new Clipper();
gp.Flatten();
foreach(int strokeSize in strokeWidths)
{
g.clear();
ClipperPolygons polyList = new ClipperPolygons();
GraphicsPath gpTest = (GraphicsPath)gp.Clone();
PathToPolygon(gpTest, polyList, 100);
gpTest.Reset();
c.Execute(ClipType.ctUnion, polyList, PolyFillType.pftPositive, PolyFillType.pftEvenOdd);
pathConverter.AddPaths(polyList, JoinType.jtMiter, EndType.etClosedPolygon);
pathConverter.Execute(ref polyList, strokeSize * 100);
for (int i = 0; i < polyList.Count; i++)
{
// reverses scaling
PointF[] pts2 = PolygonToPointFArray(polyList[i], 100);
gpTest.AddPolygon(pts2);
}
g.FillPath(new SolidBrush(Color.Red), gpTest);
}
}
private void PathToPolygon(GraphicsPath path, ClipperPolygons polys, Single scale)
{
GraphicsPathIterator pathIterator = new GraphicsPathIterator(path);
pathIterator.Rewind();
polys.Clear();
PointF[] points = new PointF[pathIterator.Count];
byte[] types = new byte[pathIterator.Count];
pathIterator.Enumerate(ref points, ref types);
int i = 0;
while (i < pathIterator.Count)
{
ClipperPolygon pg = new ClipperPolygon();
polys.Add(pg);
do
{
IntPoint pt = new IntPoint((int)(points[i].X * scale), (int)(points[i].Y * scale));
pg.Add(pt);
i++;
}
while (i < pathIterator.Count && types[i] != 0);
}
}
private PointF[] PolygonToPointFArray(ClipperPolygon pg, float scale)
{
PointF[] result = new PointF[pg.Count];
for (int i = 0; i < pg.Count; ++i)
{
result[i].X = (float)pg[i].X / scale;
result[i].Y = (float)pg[i].Y / scale;
}
return result;
}
While you've made a pretty reasonable start, you seem to be getting muddled in your createImage() function. You mention wanting different colors with the different offsets and so you're missing a colors array to match your strokeWidths array. Also, it's unclear to me what you're doing with the clipping (union) stuff, but it's probably unnecessary.
So in pseudo-code I suggest something like the following ....
static bool CreateImage(Graphics g, GraphicsPath gp,
List<int> offsets, List<Color> colors)
{
const scale = 100;
if (colors.Count < offsets.Count) return false;
//convert GraphicsPath path to Clipper paths ...
Clipper.Paths cpaths = GPathToCPaths(gp.Flatten(), scale);
//setup the ClipperOffset object ...
ClipperOffset co = new ClipperOffsets();
co.AddPaths(cpaths, JoinType.jtMiter, EndType.etClosedPolygon);
//now loop through each offset ...
foreach(offset in offsets, color in colors)
{
Clipper.Paths csolution = new Clipper.Paths();
co.Execute(csolution, offset);
if (csolution.IsEmpty) break; //useful for negative offsets
//now convert back to floating point coordinate array ...
PointF[] solution = CPathToPointFArray(csolution, scale);
DrawMyPaths(Graphics g, solution, color);
}
}
And something to watch for if you were to use increasingly larger offsets, each polygon drawn in the 'foreach' loop would hide previously drawn polygons.