Find Point along Bezier Curve in JavaScript
I adapted this from an excellent C++ example yesterday and wanted to share. This is for an HTML5 game (like) project I’m working on where interpolating along vector art created in Adobe Illustrator is needed (more on that in a future post hopefully!)
| Javascript | | copy code | | ? |
| 01 | |
| 02 | /////////////////////////////////////////////////////////////// |
| 03 | // JavaScript Implementation of the DeCasteljau Algorithm |
| 04 | // Based on C++ Implementation from |
| 05 | // http://cubic.org/docs/bezier.htm |
| 06 | // which credits Nils Pipenbrinck aka Submissive/Cubic & $eeN |
| 07 | // this particular implementation written by Mike Randrup Aug 30 2012 |
| 08 | /////////////////////////////////////////////////////////////// |
| 09 | |
| 10 | /* Usage: |
| 11 | |
| 12 | // A & D are the start and end points of the line |
| 13 | // B & C are the handle (influence) points |
| 14 | |
| 15 | myPointA = bezier.point(40,100); |
| 16 | myPointB = bezier.point(80,20); |
| 17 | myPointC = bezier.point(150,180); |
| 18 | myPointD = bezier.point(260,100); |
| 19 | |
| 20 | time = 0.5; // halfway through curve |
| 21 | |
| 22 | resultPoint = bezier.calc( |
| 23 | myPointA, myPointB, |
| 24 | myPointC, myPointD, |
| 25 | time |
| 26 | ); |
| 27 | |
| 28 | */ |
| 29 | |
| 30 | |
| 31 | var bezier = (function(){ |
| 32 | |
| 33 | var Point = function (x, y) { |
| 34 | this.x = x || 0; |
| 35 | this.y = y || 0; |
| 36 | return this; |
| 37 | } |
| 38 | |
| 39 | var interpolateLinear = function (pointA, pointB, time) { |
| 40 | return { |
| 41 | x: pointA.x + (pointB.x - pointA.x) * time, |
| 42 | y: pointA.y + (pointB.y - pointA.y) * time, |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | var bezObj = { |
| 47 | |
| 48 | point: function(x, y) { |
| 49 | return new Point(x, y); |
| 50 | }, |
| 51 | |
| 52 | calc: function(p1, p2, p3, p4, time) { |
| 53 | var ab = interpolateLinear(p1, p2, time), |
| 54 | bc = interpolateLinear(p2, p3, time), |
| 55 | cd = interpolateLinear(p3, p4, time), |
| 56 | abbc = interpolateLinear(ab, bc, time), |
| 57 | bccd = interpolateLinear(bc, cd, time); |
| 58 | |
| 59 | return interpolateLinear(abbc, bccd, time); |
| 60 | }, |
| 61 | |
| 62 | } |
| 63 | |
| 64 | return bezObj; |
| 65 | |
| 66 | }()); |
| 67 | |
| 68 | console.log("bezier loaded", bezier); |
| 69 | |
| 70 | /////////////////////////////////////////////////////////////// |
| 71 | // Also check out http://13thparallel.com/archive/bezier-curves/ |
| 72 | // for an alternate bezier option in JavaScript |
| 73 | /////////////////////////////////////////////////////////////// |
| 74 |
Plotting and animating the resulting curve in a canvas is a good way to see it working. Here is what that might look like.
| HTML | | copy code | | ? |
| 01 | |
| 02 | <html lang="us-en"> |
| 03 | |
| 04 | <head> |
| 05 | <title>Bezier curve test by Mike Randrup</title> |
| 06 | </head> |
| 07 | |
| 08 | <body> |
| 09 | |
| 10 | <canvas id="output" width="600" height="400"></canvas> |
| 11 | |
| 12 | <script type="text/javascript" src="bezier.js"></script> |
| 13 | <script type="text/javascript"> |
| 14 | // the below javascript snippet goes inline here |
| 15 | </script></body></html> |
This snippet goes into the HTML5 markup above. (I needed to separate to fix a problem with my new WordPress code formatting plugin)
| Javascript | | copy code | | ? |
| 01 | |
| 02 | window.onload = function() { |
| 03 | |
| 04 | console.log("main running"); |
| 05 | |
| 06 | var i, |
| 07 | t, |
| 08 | steps = 200, |
| 09 | resultPoint, |
| 10 | |
| 11 | myPointA = bezier.point(40,100), |
| 12 | myPointB = bezier.point(80,20), |
| 13 | myPointC = bezier.point(700,180), |
| 14 | myPointD = bezier.point(550,100), |
| 15 | |
| 16 | canvasEl = document.getElementById("output"), |
| 17 | canvasContext = canvasEl.getContext("2d"); |
| 18 | |
| 19 | var nextFrame = function() { |
| 20 | moveControlPoints(); |
| 21 | drawCurve(); |
| 22 | }; |
| 23 | |
| 24 | var moveControlPoints = function() { |
| 25 | if (myPointA.x>0 && myPointA.x<600) myPointA.x++; |
| 26 | if (myPointB.x>0 && myPointB.x<600) myPointB.x++; |
| 27 | |
| 28 | if (myPointC.y>0 && myPointC.y<400) myPointC.y--; |
| 29 | if (myPointD.x>0 && myPointD.x<600) myPointD.x--; |
| 30 | |
| 31 | }; |
| 32 | |
| 33 | var drawCurve = function () { |
| 34 | canvasContext.fillStyle = "#FFF"; |
| 35 | canvasContext.fillRect(0,0,600,400); |
| 36 | |
| 37 | canvasContext.fillStyle = "#F00"; |
| 38 | |
| 39 | for (i=0; i<steps ; i++) { |
| 40 | t = i / (steps-1); |
| 41 | |
| 42 | resultPoint = bezier.calc( |
| 43 | myPointA, myPointB, |
| 44 | myPointC, myPointD, |
| 45 | t |
| 46 | ); |
| 47 | |
| 48 | canvasContext.fillRect(resultPoint.x, resultPoint.y, 2, 2); |
| 49 | } |
| 50 | }; |
| 51 | |
| 52 | setInterval(nextFrame, 50); // I don't like this here. |
| 53 | |
| 54 | |
| 55 | } |
| 56 |
Realtime 3D version of Water Flow Simulation written for Microsoft XNA Framework
Though this is admittedly from earlier this year, hopefully it’s still interesting. This is the realtime 3D experiment written on the Microsoft XNA (Xbox, Windows, Windows Phone) Gaming Framework in C#. This is some of my earliest work in the C# language, so much refactoring could occur.
Here is the slide deck from a talk I gave on this (contains video clips of results):
The diff analysis on the code between tutorial stage and current stage illustrates my contributions and technique for the water calculation:
Starting point is tutorial from Riemer Grootjans here: http://www.riemers.net/eng/Tutorials/XNA/Csharp/series4.php Each additional commit shows my changes to achieve water effects. THANK YOU to Riemer!
The core technique is worth discussing, as is the experience of working in XNA, as well as observations on how realtime 3D rendering is handled in this framework. Hopefully a future blog post will follow.
3D Land & Water Coding Experiment
Have you ever played the video game called From Dust? It contains intriguing land, water, and lava simulation. Since first playing it, I have been fascinated with the results of Ubisoft’s very interesting simulation system. I developed an inspired guess as to how the basic algorithm and technique might work. During some holiday downtime, I put code to the concept and came up with the following:
The algorithm steps through a pair of 2 Dimensional arrays containing the height of land and water at each coordinate in the grid. During each step, it determines if and where the water would run downhill, then adjusts the old and new values accordingly. The resolution of the grid was limited by the performance of Flash, which I could no doubt have improved dramatically by using more advanced stage drawing techniques. After running and adjusting the simulations in Flash, I switched the movie to export a JPG image of each frame. That sequence was then run through After Effects (frame blend) to smooth the adjustments. The final sequences were then applied as separate Y-axis displacement maps on “land” and “water” objects in Lightwave 3D.
In the first of the two animations (mountain springs running downhill into a valley), a trio of simple “water emitters” put out a finite amount of water at the tops of the hills, and the algorithm runs it downhill to pool at the bottom. The land was generated with high values at the edges, and low values at the center, with a touch of randomization of the terrain (a “valley”).
In the second (sea mountain rising), a flat land mass with water sitting on top (an “ocean”) is affected by a strong “land emitter” which pushes a mountain out of the sea floor. The water is reacting by being displaced downhill from the land. The circular wave of water was the natural product of the algorithm I created, which seems to indicate being on the right track for how the video game works internally.
Why generate the frames in Flash AS3? Frankly, that choice was due to my own limitations as a programmer. Also, my algorithm fails to ever resolve the water to a natural state of a flat surface.
I now have an even greater sense of awe for what the Ubisoft team achieved with their amazing video game. For example, the game contains dynamic water foam textures which give realistic cues to how the water runs downhill and around obstacles.
The Flash AS3 source code is below. I realize my coding techniques are horrible, and not even object oriented. Had this been for anything other than a proof-of-concept fun project, many loose ends would be wrapped up. There was nothing else in the Flash movie, as it creates all needed movieclips dynamically. If you want to try it, just create a new Flash (ActionScript v3) document and paste the below into the frame 1 actions. It reflects the coding for the “mountain rising out of the sea” effect.
| ActionScript 3 | | copy code | | ? |
| 001 | |
| 002 | import flash.display.MovieClip; |
| 003 | import flash.events.MouseEvent; |
| 004 | import flash.events.KeyboardEvent; |
| 005 | import flash.geom.Matrix; |
| 006 | //import com.adobe.images.JPGEncoder; // external library used for saving frames |
| 007 | |
| 008 | stop(); |
| 009 | var frameNum:int = 99; |
| 010 | |
| 011 | var resX:int = 40; |
| 012 | var resY:int = resX; |
| 013 | |
| 014 | var paintStrength:Number = 1; |
| 015 | |
| 016 | var tileSpacing:int = 0; |
| 017 | var tileWidth:int = stage.stageWidth/(resX+(resX*tileSpacing)); |
| 018 | var tileHeight:int = stage.stageHeight/(resY+(resY*tileSpacing)); |
| 019 | |
| 020 | var alreadyEmitted:Number = 0; |
| 021 | var maxEmitted:Number = 0; |
| 022 | |
| 023 | var edgeValue:Number = 0; |
| 024 | var maxBoundary:int = resX-1; |
| 025 | var drawValue:Number = paintStrength; |
| 026 | var editMode:String = "water"; |
| 027 | var hideLand:Boolean = false; |
| 028 | var hideWater:Boolean = false; |
| 029 | |
| 030 | var waterInit:Number = 0.1; |
| 031 | var landInit:Number = 0.05; |
| 032 | |
| 033 | var updateTimer:Timer = new Timer(250); |
| 034 | updateTimer.addEventListener(TimerEvent.TIMER, advanceFrame); |
| 035 | updateTimer.start(); |
| 036 | |
| 037 | // used to capture frames in non-realtime |
| 038 | //stage.addEventListener(KeyboardEvent.KEY_DOWN, advanceFrame); |
| 039 | |
| 040 | function advanceFrame(e:TimerEvent):void{ |
| 041 | updateMap(null)//commands |
| 042 | } |
| 043 | |
| 044 | var parent_mc:MovieClip = new MovieClip(); |
| 045 | stage.addChild(parent_mc); |
| 046 | |
| 047 | var landMap:Array = new Array(); |
| 048 | var landHeight:Number = 0; |
| 049 | landMap.push(new Array()); |
| 050 | for (var yInit:int=0; yInit<resy ; yInit++) { |
| 051 | var newRow:Array = new Array(); |
| 052 | //newRow.splice(0,newRow.length); |
| 053 | newRow[0] = new Array(); |
| 054 | for (var xInit:int=0; xInit<resX; xInit++) { |
| 055 | landHeight = landInit;//+(Math.random()*.1); |
| 056 | newRow[0].push(landHeight); |
| 057 | } |
| 058 | landMap[0].push(newRow); |
| 059 | } |
| 060 | |
| 061 | var waterMap:Array = new Array(); |
| 062 | waterMap.push(new Array()); |
| 063 | for (yInit=0; yInit<resY; yInit++) { |
| 064 | var waterRow:Array = new Array(); |
| 065 | //newRow.splice(0,newRow.length); |
| 066 | waterRow[0] = new Array(); |
| 067 | for (xInit=0; xInit<resX; xInit++) { |
| 068 | waterRow[0].push(waterInit); |
| 069 | } |
| 070 | waterMap[0].push(waterRow); |
| 071 | } |
| 072 | |
| 073 | function calculate_new(x:int, y:int):void { |
| 074 | var newVal:Number = 0; |
| 075 | var newDelta:Number = 0; |
| 076 | var proposedX:int = 0; |
| 077 | var proposedY:int = 0; |
| 078 | |
| 079 | var minLevel:Number = 100; |
| 080 | var minX:int = 0; |
| 081 | var minY:int = 0; |
| 082 | |
| 083 | var curLand:Number; |
| 084 | var curWater:Number; |
| 085 | var checkLand:Number; |
| 086 | var checkWater:Number; |
| 087 | |
| 088 | curLand = landMap[0][y][0][x]; |
| 089 | curWater = waterMap[0][y][0][x]; |
| 090 | for ( var neighborX:int=-1; neighborX<=1; neighborX++) { |
| 091 | for ( var neighborY:int=-1; neighborY<=1; neighborY++) { |
| 092 | |
| 093 | proposedX = x+neighborX; |
| 094 | proposedY = y+neighborY; |
| 095 | if ( |
| 096 | (proposedX<0) || (proposedY<0) || |
| 097 | (proposedX>maxBoundary) || (proposedY>maxBoundary) |
| 098 | ) { |
| 099 | //trace('skipping edge'); |
| 100 | } |
| 101 | else { |
| 102 | checkLand = landMap[0][proposedY][0][proposedX]; |
| 103 | checkWater = waterMap[0][proposedY][0][proposedX]; |
| 104 | if (neighborX==0 && neighborY==0) { |
| 105 | //trace('skipping self'); |
| 106 | } |
| 107 | else { |
| 108 | if ((checkLand+checkWater)<minlevel ) { |
| 109 | minLevel = checkLand+checkWater; |
| 110 | minX = proposedX; |
| 111 | minY = proposedY; |
| 112 | //trace('new tallness found: ' + minLevel); |
| 113 | } |
| 114 | } |
| 115 | } |
| 116 | } |
| 117 | } |
| 118 | compare_cells(x, y, minX, minY); |
| 119 | } |
| 120 | |
| 121 | function compare_cells(curX:int, curY:int, checkX:int, checkY:int):void { |
| 122 | var delta:Number; |
| 123 | var curLand:Number = landMap[0][curY][0][curX]; |
| 124 | var curWater:Number = waterMap[0][curY][0][curX]; |
| 125 | var checkLand:Number = landMap[0][checkY][0][checkX]; |
| 126 | var checkWater:Number = waterMap[0][checkY][0][checkX]; |
| 127 | var idealLevel:Number = 0; |
| 128 | |
| 129 | // if there is water here |
| 130 | if (curWater > 0) { |
| 131 | //if this water should fall |
| 132 | if ((curLand+curLand) > (checkLand+checkWater) ) { |
| 133 | |
| 134 | // get the goal average height between the two including water |
| 135 | idealLevel = (curLand + curWater + checkLand + checkWater)/2; |
| 136 | if (idealLevel > curLand) { idealLevel = curLand}; |
| 137 | |
| 138 | delta = curLand + curWater - idealLevel; |
| 139 | delta *= 0.3; |
| 140 | waterMap[0][curY][0][curX] -= delta; |
| 141 | waterMap[0][checkY][0][checkX] += delta; |
| 142 | } |
| 143 | } |
| 144 | } |
| 145 | |
| 146 | function updateMap(eventObject:MouseEvent):void { |
| 147 | frameNum++; |
| 148 | |
| 149 | // clean up stage from last update |
| 150 | if(parent_mc.numChildren!=0){ |
| 151 | var cond:int = parent_mc.numChildren; |
| 152 | while( cond -- ) { |
| 153 | parent_mc.removeChildAt( cond ); |
| 154 | } |
| 155 | } |
| 156 | |
| 157 | var shading:Number = 0; |
| 158 | for (var k in waterMap[0]) { |
| 159 | for (var i in waterMap[0][k][0]) { |
| 160 | var square:Shape = new Shape(); |
| 161 | var squareMC:MovieClip = new MovieClip(); |
| 162 | calculate_new(i,k); |
| 163 | |
| 164 | if (waterMap[0][k][0][i]>0.001 ) { // water is on this spot |
| 165 | shading = get_water_color(k,i); |
| 166 | } |
| 167 | else { // land is showing on this spot |
| 168 | shading = get_land_color(k,i); |
| 169 | } |
| 170 | |
| 171 | square.graphics.beginFill(shading); |
| 172 | square.graphics.drawRect(0, 0, tileWidth, tileHeight); |
| 173 | square.graphics.endFill(); |
| 174 | |
| 175 | squareMC.addChild(square); |
| 176 | squareMC.x = (i * (tileWidth + tileSpacing + tileSpacing)) + tileSpacing; |
| 177 | squareMC.y = (k * (tileHeight + tileSpacing + tileSpacing)) + tileSpacing; |
| 178 | |
| 179 | parent_mc.addChild(squareMC); |
| 180 | } |
| 181 | } |
| 182 | |
| 183 | //save_screenshot(); // used to capture frames in non-realtime |
| 184 | |
| 185 | // water emmitters |
| 186 | emit_water(); |
| 187 | progress_land(); |
| 188 | trace(frameNum); |
| 189 | } |
| 190 | |
| 191 | function get_land_color(y:int, x:int):Number { |
| 192 | var color:Number = (int)(landMap[0][y][0][x] * 0xFF); |
| 193 | if (color < 0) { color = 0 } |
| 194 | if (color > 255) { color = 255 } |
| 195 | color = (color < < 16) | (color << 8) | color; // maps to grey of RGB |
| 196 | return(color); |
| 197 | } |
| 198 | |
| 199 | function get_water_color(y:int, x:int):Number { |
| 200 | var color:Number = 0; |
| 201 | if (waterMap[0][y][0][x] >0) |
| 202 | color = (waterMap[0][y][0][x] + landMap[0][y][0][x]) * 0xFF; |
| 203 | |
| 204 | if (color < 0) { color = 0 } |
| 205 | if (color > 255) { color = 255 } |
| 206 | |
| 207 | // the below line draws the water in greyscale, otherwise shades of blue |
| 208 | // as in an RGB value, the blue is in the least significant digits |
| 209 | //color = (color < < 16) | (color << 8) | color; // maps to grey of RGB |
| 210 | return(color); |
| 211 | } |
| 212 | |
| 213 | /* |
| 214 | function save_screenshot():void { |
| 215 | var jpgSource:BitmapData = new BitmapData (stage.stageWidth, stage.stageHeight); |
| 216 | jpgSource.draw(stage); |
| 217 | |
| 218 | var jpgEncoder:JPGEncoder = new JPGEncoder(100); |
| 219 | var jpgStream:ByteArray = jpgEncoder.encode(jpgSource); |
| 220 | var file:FileReference = new FileReference(); |
| 221 | |
| 222 | file.save(jpgStream, 'riseLand-'+frameNum+'.jpg'); |
| 223 | } |
| 224 | */ |
| 225 | |
| 226 | function emit_water():void { |
| 227 | if (alreadyEmitted < maxEmitted) { |
| 228 | if (waterMap[0][int(resY*.30)][0][int(resX*0.95)]<.3) { |
| 229 | waterMap[0][int(resY*.30)][0][int(resX*0.95)] += .2; |
| 230 | alreadyEmitted += .2; |
| 231 | } |
| 232 | if (waterMap[0][int(resY*.10)][0][int(resX*0.45)]<.3) { |
| 233 | waterMap[0][int(resY*.10)][0][int(resX*0.45)] += .1; |
| 234 | alreadyEmitted += .1; |
| 235 | } |
| 236 | |
| 237 | if (waterMap[0][int(resY*.85)][0][int(resX*0.15)]<.3) { |
| 238 | waterMap[0][int(resY*.85)][0][int(resX*0.15)]+= .05; |
| 239 | alreadyEmitted += .05; |
| 240 | } |
| 241 | } |
| 242 | } |
| 243 | |
| 244 | var landAdded:Number = 0; |
| 245 | var landMax:Number = 200; |
| 246 | var cur_landshift:Number = 0; |
| 247 | var max_landshift:Number = 100; |
| 248 | |
| 249 | function progress_land():void { |
| 250 | var proposedX:int; |
| 251 | var proposedY:int; |
| 252 | |
| 253 | // these are the land emitters in the center. |
| 254 | if (landAdded < landMax) { |
| 255 | landMap[0][int(resY*.5)][0][int(resX*0.5)]+= 2; |
| 256 | landAdded += 2; |
| 257 | } |
| 258 | |
| 259 | cur_landshift++; |
| 260 | if (cur_landshift > max_landshift) { return; } |
| 261 | |
| 262 | |
| 263 | for (var k in landMap[0]) { |
| 264 | for (var i in landMap[0][k][0]) { |
| 265 | for ( var neighborX:int=-1; neighborX< =1; neighborX++) { |
| 266 | for ( var neighborY:int=-1; neighborY<=1; neighborY++) { |
| 267 | proposedX = i+neighborX; |
| 268 | proposedY = k+neighborY; |
| 269 | if ( |
| 270 | (proposedX<0) || (proposedY<0) || |
| 271 | (proposedX>resX-1) || (proposedY>resY-1) |
| 272 | ) { |
| 273 | //trace('skipping land edge'); |
| 274 | } |
| 275 | else { |
| 276 | shift_land(i, k, proposedX, proposedY); |
| 277 | } |
| 278 | } |
| 279 | } |
| 280 | } |
| 281 | } |
| 282 | } |
| 283 | |
| 284 | function shift_land(curX:int, curY:int, checkX:int, checkY:int):void { |
| 285 | var delta:Number = landMap[0][curY][0][curX] - landMap[0][checkY][0][checkX]; |
| 286 | delta *= .5; |
| 287 | landMap[0][curY][0][curX] -= delta; |
| 288 | landMap[0][checkY][0][checkX] += delta; |
| 289 | } |
| 290 | |
| 291 | </minlevel></resy> |
Work Stuff: Motion Graphics for Splash Coupons
These two videos were created to help the Splash Coupons sales force. The videos were available online, and also on iPads the sales team took to appointments. After Effects was my tool of choice. It was my first project getting to use After Effects after completing training. I’m looking forward to creating better and better results in future projects.
One feature in these videos is the “traveling line effect” in the background of both videos (most visible in 2nd video). The lines were created in Illustrator using an “expanded” “blend”. This was then brought into After Effects, where a 3D camera was attached to a variation of its own path. After Effects did an amazing job of rendering the vector artwork transformed in 3D space. The voiceover was edited in Adobe Soundbooth CS5. (I’ve now upgraded to Adobe Audition as part of Adobe CS5.5).
Credits: Shana Rose (voice performance), Various iStockPhoto.com Photographers, Mike Randrup (all other roles)
This was the full product introduction video. It contains a call-to-action, directing users to an online calendar that was on the original page (but is not on this blog page).
http://www.youtube.com/watch?v=Lsa3y5g-9eU
Work Stuff: Big Brands Poster Design
This print project was for www.BargainBanners.com. Over the years, they have printed banners promoting quite a few national brands. The idea for a poster to show the diverse group of companies seemed natural.
The first task was gathering logo files. Most of them were possible to take from the original banner artwork, but a few needed to be picked up from www.BrandsOfTheWorld.com as vector files. Each logo is superimposed on a small “virtual banner”, which I created in Photoshop back in 2008 as part of the BargainBanners.com logo. The background contains a lovely US Map and “light steaks” courtesy of VectorStock.com. Everything was assembled in InDesign, and output to a PDF for the poster printing.
The finished piece is 4 feet wide by about 3 feet tall, and is now hanging in their banner workshop.
Work Stuff: Loading Animation for native iPhone App

Earlier this year, I worked on the graphics used in a Native iPhone App (now live on the App Store) for www.SplashCoupons.com. The app lists offers for home improvement savings for people in the North Dallas, Texas area. When the App first launches, it downloads the latest set of coupons from a data server via XML. During this process, which can take a couple of seconds, I wanted to display a visually interesting animation. So out came the storyboard, and a new After Effects project was born.
Conceptually, the animation was designed to show a user that “coupons” are going “into their iPhone”. The After Effects project was relatively simple. The coupon image assets I created were animated along paths from off screen into the pulsing iPhone in the middle. The screen of the iPhone has an AJAX-style busy cursor superimposed on it. Two layers of After Effects “Particle Playground” effects were blending with a glow effect. Since the particle filter naturally emits from the center outward, I time reversed the particle composition so the particles would flow into the iPhone. There is a colored glow in the center, and concentric circles pulsing inward. The last element, “droplet” pieces of the Splash Coupons logo, fly outward from the phone. The color decisions were based on the style guide for the Splash Coupons logo and brand.
The end deliverable for the animation was an 18 frame looping sequence. It didn’t take up much memory or room in the IPA file, and loops to run as long as needed. I also provided several other button graphics for the user interface.
Hopefully it makes for a nicer wait as the XML downloads in the background. At any rate, it was fun to create.
Lightwave 3D v10: Yay! I’ve got new graphic software to create work in…
I have such a back log of finished work this year to post on this blog, and yet haven’t been making it happen. Just added to the category of work will be 3D rendering using Lightwave. They just came out with version 10, and I did a lot of work with it way back in the day starting at version 3.5. So naturally I am blown away by the many new features.
I figure over the next year I can learn and develop some basic 3D skills again. I have a big monster project in mind. On a more practical level, this stuff has already come in handy for my work. We just received back from the printers a brochure with a big 3D illustration of a product name on it.
After installing, I rendered a couple of the example scenes:

That's a wolf, with the kind of artificial fur that Pixar made famous with the giant blue monster in Monsters, Inc. Fun stuff!
Work Stuff: Sales Folder (Print Project)
Fun Factoid: Everything in the photo is at half scale, even the tiny 1 inch tall business card.
This was a recent project I completed for our sales team at work. It will be a nicely printed folder filled with sales materials (brochures, price sheets, things like that).
Tiny Mockup of Dark Version:
Tiny Mockup of Light Version:
In the end, we went with the version over white. This better matches the other series of collateral items I produced earlier this year (envelope, sales team business cards, a promotional coupon, and notesheet sized letterhead). Can’t wait to get them back from the printer next week!
We are glad to have great suppliers
Our raw materials supplier, which also has a service and technical support division, happened to request and offer to come by today.
The meeting was a great conversation, and we very much enjoyed their insight. We figured out pretty quickly that our sales rep was a great one. And our tech (when we need him for advanced service needs) is also very, very good.
What a great day. We definitely realized that we chose the right major supplier.
Can’t post our supplier name here unfortunately, but please private message me if you are in the banner printing business and need a suggestion for who to align with. They provide sign industry inputs (inks, materials, and other such items).












