Psychedelic Trip! Colored potential Energy field with Palette Cycling! (Potential Energy 2)

P1 p2 p3 p4 p5

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,

Leave a comment