Hello all!
This post will be a variation from my first post Potential Energy, using color functions from the second one Burn It!
In original post, we calculate the field and store it directly to destination bitmap. In this one, we will use same technique that in fire effect, that will be the main one used always in this kind of effects: we will create a byte work array, and then dump it in a format 8bpp indexed bitmap. Then we will pass it to client side as a base64 encoded string. We will use a timer as well to rotate the palette and again, pass the new generated bitmap to client side. Remember that all these effects are generated to a web for show purposes, but it is not the best way to generate animations in an internet’s hanged app (the best one it’s, of course, all heavy work being realized at client side, so reducing to minimum post back calls to server). Anyway, you can use this code to get the way to reply same code in desktop forms apps, or as well in any local app). Another point it’s that we are using all the time static variables, don’t minding then specific user sessions.
Ok, let’s go. First of all, we add to original web template a Timer object (for palette cycling purposes), a Checkbox to activate/deactivate it, and a button to generate a new random palette.
Here complete PotentialEnergyColored.aspx
<%@ Page Language="C#" Inherits="OnlineRepository.PotentialEnergyColored" %>
<!DOCTYPE html>
<html>
<head runat="server">
<title>PotentialEnergyColored</title>
<style type="text/css">
p{text-align:center;font-variants: small-caps; font-name=Lucida Console; color:white;}
</style>
</head>
<body id="PageBody" bgcolor="#00000F" runat="server" >
<form id="form1" runat="server">
<asp:ScriptManager runat="server" id="ScriptManager1">
</asp:ScriptManager>
<p>Please enter values for width, height, and number of charges</p>
<table align="center" >
<tr>
<td><asp:Label runat="server" AssociatedControlID="tbWidth" Text="WIDTH" ForeColor="white" Font-size="small" /></td>
<td><asp:TextBox id="tbWidth" runat="server" Text="1000" Columns="6" Style="width:150pts;text-align: right" ToolTip="width" /></td>
</tr>
<tr>
<td><asp:Label runat="server" AssociatedControlID="tbHeight" Text="HEIGHT" ForeColor="white" Font-size="small" /></td>
<td><asp:TextBox id="tbHeight" runat="server" Text="300" Columns="6" Style="width:120pts;text-align: right" ToolTip="height" /></td>
</tr>
<tr>
<td><asp:Label runat="server" AssociatedControlID="tbCharges" Text="CHARGES" ForeColor="white" Font-size="small" /></td>
<td><asp:TextBox id="tbCharges" runat="server" Text="5" Columns="6" Style="width:120pts;text-align: right" ToolTip="charges" /></td>
</tr>
<tr>
<td><asp:Label runat="server" AssociatedControlID="tbK" Text="KONSTANT" ForeColor="white" Font-size="small" /></td>
<td><asp:TextBox id="tbK" runat="server" Text="3000" Columns="6" Style="width:120pts;text-align: right" ToolTip="charges" /></td>
</tr>
<tr>
<td><asp:Label runat="server" AssociatedControlID="tbMaxCharge" Text="MAXIMUM CHARGE" ForeColor="white" Font-size="small" /></td>
<td><asp:TextBox id="tbMaxCharge" runat="server" Text="3000" Columns="6" Style="width:120pts;text-align: right" ToolTip="charges" /></td>
</tr>
<tr>
<td><asp:Label runat="server" AssociatedControlID="cbPaletteCycling" Text="PALETTE CYCLING" ForeColor="white" Font-size="small" /></td>
<td><asp:CheckBox runat="server" id="cbPaletteCycling" Checked="false" ForeColor="White" Font-size="small" /></td>
</tr>
</table>
<asp:UpdatePanel runat="server" id="UpdatePanel1">
<ContentTemplate>
<p>
<asp:Timer runat="server" id="Timer" Interval="10" OnTick="Timer_Tick"></asp:Timer>
<asp:Button id="btGenerate" runat="server" Text="Generate field" OnClick="btGenerate_Click" Width="150" /><br/>
<asp:Button id="btRandomPalette" runat="server" Text="Random palette" OnClick="btRandomPalette_Click" Width="150" /><br/>
<asp:Label runat="server" Text="Any image yet generated" id="InfoLabel" font-size="small" font-name="verdana" ></asp:Label><br/>
<asp:Image runat="server" id="UImage" ImageAlign="AbsMiddle"/>
</p>
</ContentTemplate>
</asp:UpdatePanel>
</form>
</body>
</html>
As told, we’re going to give color to our potential field. We export our gradient and palette functions from Burn It! post. We will realize a minor modification to FillGradients, making final color same as first one, so this way getting a coherent cycled palette. As well, due we need exact index location, we will use float for the step variable.
// Generates gradient between Palette indexed pivot colors
protected void Gradient (int ao, int af)
{
int d = af - ao; // total distance between colors
float
ro = Palette [ao].R, // Initial r,g,b values
go = Palette [ao].G,
bo = Palette [ao].B,
dr = (Palette[af].R - ro)/d, // diferential of r,g,b
dg = (Palette[af].G - go)/d,
db = (Palette[af].B - bo)/d;
// lets fill each color in palette between range
for (int i=0;i<d+1;i++)
Palette [i + ao] =
Color.FromArgb (
(byte)(ro + i*dr),
(byte)(go + i*dg),
(byte)(bo + i*db)
);
}
// Fill gradients
protected void FillGradients(Color[] C)
{
// search distance between pivot colors in palette
float m = Palette.Length / (C.Length);
// let's tie palette, make it rounded
Palette [0] = C [0];
Palette [_MAXCOLORS-1] = C [0];
// Lets point each choosen color at position and do gradient between them
for (int i = 1; i < C.Length; i++)
{
Palette [(int)(i * m)] = C [i];
Gradient ((int)((i-1)* m),(int)(i * m));
}
// lets degrade last color index. This line is due float can be not exactly _MAXCOLORS-1
Gradient ((int)((C.Length-1)*m), _MAXCOLORS - 1);
}
Ok. Got it. For general performance, we will use static variables as well for the Field array and for Palette, PivotColors, and all data related to various different functions.
protected static float K; // Multiplicative factor
protected static int MAXQ; // Maximum value of a charge * 2
protected static byte[] Field; // byte array with calculated V values
protected static int width, height; // general width & height
protected static int _MAXCOLORS = 256; // maxcolors (remain in byte)
protected static Color[] Palette = new Color[_MAXCOLORS]; // working palette
protected static int _MAXPIVOTCOLORS = 6; // maxpivot colors (to degrade colors between)
protected static Color[] PivotColors = new Color[_MAXPIVOTCOLORS]; //
protected Random r = new Random(); // general random object
Ok. Let’s generate random palette click code. Will be about the same as original one, only changes the use of general static variable PivotColors and the SaveToUrl function, that we will use in some places to dump field array to bitmap, dump palette as well, and inform to reload UImage url:
// random palette : generates some pivot colors and fill palette about
public void btRandomPalette_Click (object sender, EventArgs args)
{
for (int i = 0; i < PivotColors.Length; i++)
PivotColors[i] = Color.FromArgb (r.Next () % 0xff, r.Next () % 0xff, r.Next () % 0xff);
FillGradients (PivotColors);
// invalidate page
SaveToUrl ();
}
SaveToUrl function will be used to dump Field array to a bitmap (remember bitmap will be 8bpp Format Indexed, so dump it’s no more than copy an array of bytes), dump Palette to bitmap’s palette, and to pass url to UImage object in client’s side:
// dump field to a 8bpp indexed bitmap
protected void DumpToBitmap(Bitmap B, byte[] A)
{
Rectangle R = new Rectangle (0, 0, B.Width, B.Height);
BitmapData b = B.LockBits(R,ImageLockMode.WriteOnly,System.Drawing.Imaging.PixelFormat.Format8bppIndexed);
IntPtr ptr = b.Scan0;
Marshal.Copy (A, 0, ptr, B.Width * B.Height);
B.UnlockBits (b);
}
// dump working palette to a bitmap palette
private void DumpPalette(Bitmap B,Color[] C)
{
ColorPalette P = B.Palette;
for (int i = 0; i < C.Length; i++)
P.Entries [i] = C[i];
B.Palette = P;
}
// generate a base64 string with the bitmap & palette and save it to image url
public void SaveToUrl()
{
// Counter
Stopwatch sw = Stopwatch.StartNew();
// Create bitmap, and fill it
Bitmap B = new Bitmap (width, height,System.Drawing.Imaging.PixelFormat.Format8bppIndexed);
DumpToBitmap (B, Field);
DumpPalette (B, Palette);
sw.Stop ();
InfoLabel.Text = "Bitmap generated on " + sw.ElapsedMilliseconds.ToString () + "ms";
// Lets copy it in a stream and make page to load it encoded as base64
sw.Start ();
using (MemoryStream m = new MemoryStream ())
{
B.Save (m, System.Drawing.Imaging.ImageFormat.Jpeg);
UImage.ImageUrl = "data:image/jpeg;base64," + Convert.ToBase64String (m.ToArray ());
InfoLabel.Text += "; size : " + Convert.ToBase64String (m.ToArray ()).Length + " bytes ";
}
B.Dispose();
sw.Stop ();
InfoLabel.Text += " / total ellapsed time : " + sw.ElapsedMilliseconds.ToString() + "ms";
}
Ok. We got a way to generate a palette, and to dump that palette and an array to a bitmap. We need yet to add info to that bitmap. This is did almost same way at original post: calculate value of V generated by some charges, and put calculated value in Field array:
// Charge definition protected class Charge { public int x; public int y; public float v; } // Potential of a specific point x,y derived from charges Q protected float V(int x, int y,Charge[] Q) { float v = 0; // So easy as run all charges and sum each q potential contribution foreach (Charge q in Q) { // Vq(x,y) = q/r2 try { v += K*q.v / ((q.x-x)*(q.x-x) + (q.y - y)*(q.y-y)); } catch(Exception) { // when r2 = 0, so position of same charge } } return (float)v; } // generates a new field public void generateField() { // get parameters width = Int32.Parse (tbWidth.Text); height = Int32.Parse (tbHeight.Text); K = Int32.Parse (tbK.Text); MAXQ = Int32.Parse (tbMaxCharge.Text); // Create charges and fill them. Values must be negatives & positives Charge[] Q = new Charge[Int32.Parse (tbCharges.Text)]; for (int i = 0; i < Q.Length; i++) { Q[i] = new Charge (); Q[i].x = r.Next () % width; Q[i].y = r.Next () % height; Q[i].v = (r.Next () % MAXQ) - MAXQ / 2; } // create array & fill it with rounded-to-byte values Field = new byte[width * height]; int x, y; for (x = 0; x < width; x++) for (y = 0; y < height; y++) Field [y * width + x] = (byte)V (x, y, Q); }
OK. Let’s fill Generate Field button event, and as well, prepare first page load.
// generate click will generate a new field and save it public void btGenerate_Click (object sender, EventArgs args) { generateField (); SaveToUrl (); } // main load event. Let's create a palette and a field public void Page_Load(object sender, EventArgs args) { // First time page load, lets generate a first field & a palette if (!Page.IsPostBack) { generateField (); btRandomPalette_Click (null, null); } }
Almost done! We got right now the potential field generated & colored. We add the timer tick event to realize a palette cycling effect
// timer event will cycle palette
public void Timer_Tick (object sender, EventArgs args)
{
if (cbPaletteCycling.Checked)
{
// no more than run palette
Color swap = Palette [0];
for (int i = 0; i < Palette.Length - 1; i++)
Palette [i] = Palette [i + 1];
Palette [_MAXCOLORS - 1] = swap;
// let's invalidate page
SaveToUrl ();
}
}
And that’s all! Here you can find a working implementation. Note that at each timer tick server it’s serving a 1000×300 bitmap, so, minimun 50.000 bytes in a jpeg-compressed b64-string, at each tick. As told before, this is not the fastest way, then, to realize this effect in web: all this code it’s for teaching & show purposes about cycling palette and work in general c# with 8bpp bitmaps, that you can translate without big effort to Android, iOS or any platform that use Xamarin or Visual Studio.
Here complete code :
PotentialEnergyColored.aspx
<%@ Page Language="C#" Inherits="OnlineRepository.PotentialEnergyColored" %>
<!DOCTYPE html>
<html>
<head runat="server">
<title>PotentialEnergyColored</title>
<style type="text/css">
p{text-align:center;font-variants: small-caps; font-name=Lucida Console; color:white;}
</style>
</head>
<body id="PageBody" bgcolor="#00000F" runat="server" >
<form id="form1" runat="server">
<asp:ScriptManager runat="server" id="ScriptManager1">
</asp:ScriptManager>
<p>Please enter values for width, height, and number of charges</p>
<table align="center" >
<tr>
<td><asp:Label runat="server" AssociatedControlID="tbWidth" Text="WIDTH" ForeColor="white" Font-size="small" /></td>
<td><asp:TextBox id="tbWidth" runat="server" Text="1000" Columns="6" Style="width:150pts;text-align: right" ToolTip="width" /></td>
</tr>
<tr>
<td><asp:Label runat="server" AssociatedControlID="tbHeight" Text="HEIGHT" ForeColor="white" Font-size="small" /></td>
<td><asp:TextBox id="tbHeight" runat="server" Text="300" Columns="6" Style="width:120pts;text-align: right" ToolTip="height" /></td>
</tr>
<tr>
<td><asp:Label runat="server" AssociatedControlID="tbCharges" Text="CHARGES" ForeColor="white" Font-size="small" /></td>
<td><asp:TextBox id="tbCharges" runat="server" Text="5" Columns="6" Style="width:120pts;text-align: right" ToolTip="charges" /></td>
</tr>
<tr>
<td><asp:Label runat="server" AssociatedControlID="tbK" Text="KONSTANT" ForeColor="white" Font-size="small" /></td>
<td><asp:TextBox id="tbK" runat="server" Text="3000" Columns="6" Style="width:120pts;text-align: right" ToolTip="charges" /></td>
</tr>
<tr>
<td><asp:Label runat="server" AssociatedControlID="tbMaxCharge" Text="MAXIMUM CHARGE" ForeColor="white" Font-size="small" /></td>
<td><asp:TextBox id="tbMaxCharge" runat="server" Text="3000" Columns="6" Style="width:120pts;text-align: right" ToolTip="charges" /></td>
</tr>
<tr>
<td><asp:Label runat="server" AssociatedControlID="cbPaletteCycling" Text="PALETTE CYCLING" ForeColor="white" Font-size="small" /></td>
<td><asp:CheckBox runat="server" id="cbPaletteCycling" Checked="false" ForeColor="White" Font-size="small" /></td>
</tr>
</table>
<asp:UpdatePanel runat="server" id="UpdatePanel1">
<ContentTemplate>
<p>
<asp:Timer runat="server" id="Timer" Interval="10" OnTick="Timer_Tick"></asp:Timer>
<asp:Button id="btGenerate" runat="server" Text="Generate field" OnClick="btGenerate_Click" Width="150" /><br/>
<asp:Button id="btRandomPalette" runat="server" Text="Random palette" OnClick="btRandomPalette_Click" Width="150" /><br/>
<asp:Label runat="server" Text="Any image yet generated" id="InfoLabel" font-size="small" font-name="verdana" ></asp:Label><br/>
<asp:Image runat="server" id="UImage" ImageAlign="AbsMiddle"/>
</p>
</ContentTemplate>
</asp:UpdatePanel>
</form>
</body>
</html>
PotentialEnergyColored.aspx.cs
using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace OnlineRepository
{
public partial class PotentialEnergyColored : System.Web.UI.Page
{
protected static float K; // Multiplicative factor
protected static int MAXQ; // Maximum value of a charge * 2
protected static byte[] Field; // byte array with calculated V values
protected static int width, height; // general width & height
protected static int _MAXCOLORS = 256; // maxcolors (remain in byte)
protected static Color[] Palette = new Color[_MAXCOLORS]; // working palette
protected static int _MAXPIVOTCOLORS = 6; // maxpivot colors (to degrade colors between)
protected static Color[] PivotColors = new Color[_MAXPIVOTCOLORS]; //
protected Random r = new Random(); // general random object
// Charge definition
protected class Charge
{
public int x;
public int y;
public float v;
}
// main load event. Let's create a palette and a field
public void Page_Load(object sender, EventArgs args)
{
// First time page load, lets generate a first field & a palette
if (!Page.IsPostBack)
{
generateField ();
btRandomPalette_Click (null, null);
}
}
// Potential of a specific point x,y derived from charges Q
protected float V(int x, int y,Charge[] Q)
{
float v = 0;
// So easy as run all charges and sum each q potential contribution
foreach (Charge q in Q)
{
// Vq(x,y) = q/r2
try
{
v += K*q.v / ((q.x-x)*(q.x-x) + (q.y - y)*(q.y-y));
}
catch(Exception)
{
// when r2 = 0, so position of same charge
}
}
return (float)v;
}
// generates a new field
public void generateField()
{
// get parameters
width = Int32.Parse (tbWidth.Text);
height = Int32.Parse (tbHeight.Text);
K = Int32.Parse (tbK.Text);
MAXQ = Int32.Parse (tbMaxCharge.Text);
// Create charges and fill them. Values must be negatives & positives
Charge[] Q = new Charge[Int32.Parse (tbCharges.Text)];
for (int i = 0; i < Q.Length; i++)
{
Q[i] = new Charge ();
Q[i].x = r.Next () % width;
Q[i].y = r.Next () % height;
Q[i].v = (r.Next () % MAXQ) - MAXQ / 2;
}
// create array & fill it with rounded-to-byte values
Field = new byte[width * height];
int x, y;
for (x = 0; x < width; x++)
for (y = 0; y < height; y++)
Field [y * width + x] = (byte)V (x, y, Q);
}
// Generates gradient between Palette indexed pivot colors
protected void Gradient (int ao, int af)
{
int d = af - ao; // total distance between colors
float
ro = Palette [ao].R, // Initial r,g,b values
go = Palette [ao].G,
bo = Palette [ao].B,
dr = (Palette[af].R - ro)/d, // diferential of r,g,b
dg = (Palette[af].G - go)/d,
db = (Palette[af].B - bo)/d;
// lets fill each color in palette between range
for (int i=0;i<d+1;i++)
Palette [i + ao] =
Color.FromArgb (
(byte)(ro + i*dr),
(byte)(go + i*dg),
(byte)(bo + i*db)
);
}
// Fill gradients
protected void FillGradients(Color[] C)
{
// search distance between pivot colors in palette
float m = Palette.Length / (C.Length);
// let's tie palette, make it rounded
Palette [0] = C [0];
Palette [_MAXCOLORS-1] = C [0];
// Lets point each choosen color at position and do gradient between them
for (int i = 1; i < C.Length; i++)
{
Palette [(int)(i * m)] = C [i];
Gradient ((int)((i-1)* m),(int)(i * m));
}
// lets degrade last color index. This line is due float can be not exactly _MAXCOLORS-1
Gradient ((int)((C.Length-1)*m), _MAXCOLORS - 1);
}
// dump field to a 8bpp indexed bitmap
protected void DumpToBitmap(Bitmap B, byte[] A)
{
Rectangle R = new Rectangle (0, 0, B.Width, B.Height);
BitmapData b = B.LockBits(R,ImageLockMode.WriteOnly,System.Drawing.Imaging.PixelFormat.Format8bppIndexed);
IntPtr ptr = b.Scan0;
Marshal.Copy (A, 0, ptr, B.Width * B.Height);
B.UnlockBits (b);
}
// dump working palette to a bitmap palette
private void DumpPalette(Bitmap B,Color[] C)
{
ColorPalette P = B.Palette;
for (int i = 0; i < C.Length; i++)
P.Entries [i] = C[i];
B.Palette = P;
}
// generate a base64 string with the bitmap & palette and save it to image url
public void SaveToUrl()
{
// Counter
Stopwatch sw = Stopwatch.StartNew();
// Create bitmap, and fill it
Bitmap B = new Bitmap (width, height,System.Drawing.Imaging.PixelFormat.Format8bppIndexed);
DumpToBitmap (B, Field);
DumpPalette (B, Palette);
sw.Stop ();
InfoLabel.Text = "Bitmap generated on " + sw.ElapsedMilliseconds.ToString () + "ms";
// Lets copy it in a stream and make page to load it encoded as base64
sw.Start ();
using (MemoryStream m = new MemoryStream ())
{
B.Save (m, System.Drawing.Imaging.ImageFormat.Jpeg);
UImage.ImageUrl = "data:image/jpeg;base64," + Convert.ToBase64String (m.ToArray ());
InfoLabel.Text += "; size : " + Convert.ToBase64String (m.ToArray ()).Length + " bytes ";
}
B.Dispose();
sw.Stop ();
InfoLabel.Text += " / total ellapsed time : " + sw.ElapsedMilliseconds.ToString() + "ms";
}
// generate click will generate a new field and save it
public void btGenerate_Click (object sender, EventArgs args)
{
generateField ();
SaveToUrl ();
}
// random palette : generates some pivot colors and fill palette about
public void btRandomPalette_Click (object sender, EventArgs args)
{
for (int i = 0; i < PivotColors.Length; i++)
PivotColors[i] = Color.FromArgb (r.Next () % 0xff, r.Next () % 0xff, r.Next () % 0xff);
FillGradients (PivotColors);
// invalidate page
SaveToUrl ();
}
// timer event will cycle palette
public void Timer_Tick (object sender, EventArgs args)
{
if (cbPaletteCycling.Checked)
{
// no more than run palette
Color swap = Palette [0];
for (int i = 0; i < Palette.Length - 1; i++)
Palette [i] = Palette [i + 1];
Palette [_MAXCOLORS - 1] = swap;
// let's invalidate page
SaveToUrl ();
}
}
}
}
Hope you enjoy it!
Best,