/** *
Version 0.2, 2009-01-29
* *Idea started with * Hamster Gyro Music Video Generator * by Johan Larsby
*I saw Johan's video generator program and was interested to find out what made it * tick. I really should have written something like this last year when I had a need * for supplying lyrics for music at parties... I'd had a bit of an experiment in * another direction, using custom HLSL pixel shaders in the Neon v2 VJ software I use, * but that proved to be painful and not especially useful!
* *Anyhow, when I looked inside the hamstergyro, aside from the world of pain of * setting up those hundred or so timed lyrics entries, I saw that the method use * to calculate the animation was a bit confusing and awkward. My first thought was * to post a comment recommending Johan check out the lerp() and lerpColor() * functions for future projects, and started typing some example code in the comments * box to show how nice it might look in comparison.
* *I then realised the code probably would not be formatted properly in the * comment, and I also didn't want to leave a suggestion to use code that I'd not actually * tried out, sp downloaded the zip file and started hacking away to make sure it _would_ * work the way I was suggesting. I may just have a use for this myself, so it seemed * a worthwhile project to keep things tidy and make it easier to expend on in future! :o)
* *Of course, just testing out a couple of lerp() calls wasn't going far enough, and * what I've ended up with is something I reckon is pretty cool. A kind of video transport * system with a virtual playhead that can be controlled independently of Processing's * current frame number and/or frame rate. I also had this brilliant idea to try out * rendering in-between frames and blending them together for a nice motion blur effect. * That part in particular I'm sure can be improved upon, but it is a nice start. :o)
* *In all, I'm pretty pleased with the result so far, and I'd like to thank Johan for * the inspiration - seeing something done is sometimes all the motivation you need to * go out and try it yourself.
* *Cheers, * -spxl.
*/ // ----------------------------------------------------------------------------- // Options and state variables // ----------------------------------------------------------------------------- boolean saveToFile = false; // Set to false if you dont want your hdd full with images! boolean playAtStartup = true; // Start playing when program started? boolean displayStats = true; // Display time stats in the frame itself? float initialTargetFrameRate = 25; // In frames per second int renderQuality = 8; // Level from 0 to ...? String saveFileNameFormat = "frames/hamstergyro-#######.png"; // ----------------------------------------------------------------------------- // Time variables // ----------------------------------------------------------------------------- int clockMillis; // Value returned by millis() in tickTock() float clock; // The clock time in seconds Playhead playhead; // Virtual playhead with exact output video frame number and time float finishTime; // Need to know when to stop, especially if writing files! float t0 = 0; // Timing offset; used by addText for convenience // ----------------------------------------------------------------------------- // The last frame we produced // ----------------------------------------------------------------------------- PImage outputImage; int lastRenderedFrame = -1; // Frame number of frame on screen float lastRenderedRunningTime = -1; // Running time for last frame rendered String lastRenderedStats = "No frame rendered"; RenderResult outputRender; // ----------------------------------------------------------------------------- // Animation data // ----------------------------------------------------------------------------- ArrayList textEntries; // TimedText objects for the textEntries int numTextEntries; // A handy count of the number of text entries int firstTextEntry; // Don't need to start at entry 0 every time ArrayList backgrounds; // TimedImage objects for backgrounds int numBackgrounds; // A handy count of the number of background images ArrayList images; // Other TimedImage objects int numImages; // A handy count of these other images // ----------------------------------------------------------------------------- // Other variables // ----------------------------------------------------------------------------- // Font faces PFont f, f1, f2; // Background images PImage bg1; // The orange background PImage bg2; // The white background // Other images PImage cubicleImg; PImage clockImg; PImage coffeImg; PImage truckImg; PImage gyroImg; // ----------------------------------------------------------------------------- // Setup method // ----------------------------------------------------------------------------- void setup() { size(640, 480); frameRate(30); // Initialise the virtual playhead. Yeah. playhead = new Playhead(initialTargetFrameRate); bg1 = loadImage("bg.png"); bg2 = loadImage("bg2.png"); cubicleImg = loadImage("cubicle.png" ); clockImg = loadImage("clock.png" ); coffeImg = loadImage("coffe.png" ); truckImg = loadImage("truck.png" ); gyroImg = loadImage("gyro.png" ); // Initialise the arraylists to hold the animation data textEntries = new ArrayList(200); backgrounds = new ArrayList(); images = new ArrayList(); // And an image to hold the output (without the timestamp on it) outputImage = createImage(width, height, RGB); // Load fonts from files f = loadFont("Verdana-Italic-66.vlw"); f1 = loadFont("Cochin-Italic-66.vlw"); f2 = loadFont("MarkerFelt-Wide-66.vlw"); // Colours used for the text entries color blk = #000000; color wht = #ffffff; color red1 = #ff0000; color gray1 = #f0f0f0; color gray2 = #c0c0c0; // --------------------------------------------------------------------------- // Timed entries // All these times are taken from the aduio file, it was booring work // --------------------------------------------------------------------------- t0 = 0; addBackground(0, 96, bg1); // TIMES COLOURS SIZE PositionX PositionY, Font addText("Playhead", 2.0, 5, blk, blk, 66, 66, 110, 110, 140, 500, f2); addText("Playhead", 2.0, 10, red1, red1, 66, 66, 110, 110, 140, 140, f2); addText("demo", 3.0, 10, blk, blk, 66, 66, 110, 110, 140, 300, f); addText("by", 4.0, 10, blk, blk, 66, 66, 410, 410, 100, 100, f); addText("subpixel", 5.0, 10, wht, blk, 66, 66, 310, 310, 200, 200, f, -PI*20); addText("Playhead demo by subpixel", 0, 60, blk, blk, 44, 44, width, -width, height-40, height-22, f2); addText("Playhead demo by subpixel", 0, 60, wht, wht, 44, 44, width-2, -width-2, height-41, height-23, f2); addImage(10, 26, cubicleImg); addImage(17, 23, clockImg, PI); // TIMES COLOURS SIZE PositionX PositionY, Font addText("Late", 18.3, 23, blk, blk, 66, 66, 110, 110, 100, 100, f); addText("afternoon", 19.0, 23, blk, blk, 66, 66, 160, 160, 160, 160, f); addText("papercut", 21.4, 23, blk, red1, 66, 66, 220, 220, 220, 220, f2); addText("at", 23.9, 25.7, blk, blk, 66, 66, 100, 100, 300, 300, f); addText("the", 24.5, 25.7, blk, blk, 66, 66, 180, 180, 300, 300, f); addText("office.", 24.8, 25.7, blk, blk, 66, 66, 300, 300, 300, 300, f1); addText("another", 27.1, 32, blk, blk, 66, 66, 70, 70, 100, 100, f); addText("perfect", 28.3, 32, blk, blk, 66, 66, 350, 350, 100, 100, f); addText("day", 29.4, 32, blk, blk, 66, 66, 20, 20, 150, 150, f); addText("for", 30.0, 32, blk, blk, 66, 66, 140, 140, 150, 150, f); addText("the", 30.6, 32, blk, blk, 66, 66, 240, 240, 150, 150, f); addText("disgruntled", 31.7, 33, blk, blk, 66, 66, 20, 20, 300, 300, f); addText("employee.", 32.8, 34, blk, blk, 66, 66, 300, 300, 350, 350, f); addImage(40, 55, coffeImg); // TIMES COLOURS SIZE PositionX PositionY, Font addText("The", 40.8, 46.8, blk, blk, 66, 66, 100, 100, -10, 1500, f); addText("coffee", 41.1, 46.8, blk, blk, 66, 66, 100, 100, -10, 1500, f); addText("machine", 41.7, 46.8, blk, blk, 66, 66, 100, 100, -10, 1500, f); addText("is", 42.6, 46.8, blk, blk, 66, 66, 100, 100, -10, 1500, f); addText("broken", 43.1, 46.8, blk, blk, 66, 66, 100, 100, -10, 1500, f); addText("again", 44.0, 46.8, blk, blk, 66, 66, 100, 100, -10, 1500, f); addText("empty", 46.8, 54, blk, blk, 66, 66, 100, 100, -10, 1500, f); addText("staring", 47.5, 54, blk, blk, 66, 66, 100, 100, -10, 1500, f); addText("another", 50.0, 54, blk, blk, 66, 66, 100, 100, -10, 1500, f); addText("caffeine", 50.8, 54, blk, blk, 66, 66, 100, 100, -10, 1500, f); addText("casualty.", 52.3, 54.6, blk, blk, 66, 66, 100, 100, -10, 1500, f); addText("Disgruntled", 54.6, 58, blk, blk, 66, 66, 10, 10, 275, 250, f); addText("employee.", 55.7, 58, blk, blk, 66, 66, 300, 300, 275, 300, f); addImage(67, 80, gyroImg); // TIMES COLOURS SIZE PositionX PositionY, Font addText("Perfect", 68.5, 69, blk, blk, 66, 66, 300, 300, 300, 300, f); addText("spinning", 69.3, 69.8, blk, blk, 66, 66, 300, 300, 300, 300, f); addText("hamster", 70.6, 71.1, blk, blk, 66, 66, 200, 200, 300, 300, f); addText("gyro", 71.4, 71.9, blk, blk, 66, 66, 200, 200, 300, 300, f); addText("gyro", 71.4, 74, blk, blk, 66, 66, 100, 100, 200, 200, f, -8*PI); addText("rewards", 73.2, 73.7, blk, blk, 66, 66, 300, 300, 300, 300, f); addText("will", 74.2, 74.7, blk, blk, 66, 66, 300, 300, 300, 300, f); addText("be", 74.5, 75, blk, blk, 66, 66, 200, 200, 300, 300, f); addText("here", 75.1, 75.6, blk, blk, 66, 66, 200, 200, 300, 300, f); addText("tomorrow.", 75.7, 76.7, blk, blk, 66, 66, 250, 250, 300, 300, f); addText("Do", 77.7, 83, blk, blk, 66, 66, 10, 10, 100, 100, f); addText("your", 78.0, 83, blk, blk, 66, 66, 110, 110, 100, 100, f); addText("kids", 78.8, 83, blk, blk, 66, 66, 300, 300, 100, 100, f2); addText("know", 79.1, 83, blk, blk, 66, 66, 100, 200, 150, 150, f); addText("your", 80.8, 83, blk, blk, 66, 66, 10, 10, 200, 200, f); addText("middle", 81.1, 83, blk, blk, 66, 66, 180, 180, 200, 200, f); addText("name?", 82.0, 83, blk, blk, 66, 66, 410, 410, 200, 200, f); addImage(86, 96, truckImg); // TIMES COLOURS SIZE PositionX PositionY, Font addText("Posture", 86.8, 91.6, blk, blk, 66, 66, 10, 10, 100, 100, f); addText("of", 87.8, 91.6, blk, blk, 66, 66, 100, 100, 150, 150, f); addText("an", 88.3, 91.6, blk, blk, 66, 66, 200, 200, 150, 150, f); addText("old", 88.9, 91.6, blk, blk, 66, 66, 300, 300, 150, 150, f); addText("truck driver", 89.2, 91.6, blk, blk, 66, 66, 100, 100, 200, 200, f); addText("this", 91.6, 95.4, blk, blk, 66, 66, 30, 30, 250, 250, f); addText("life", 91.9, 95.4, blk, blk, 66, 66, 180, 180, 250, 250, f); addText("seems", 92.3, 95.4, blk, blk, 66, 66, 300, 300, 250, 250, f); addText("to", 92.8, 95.4, blk, blk, 66, 66, 10, 10, 300, 300, f); addText("have", 93.1, 95.4, blk, blk, 66, 66, 100, 100, 300, 300, f); addText("run", 93.4, 95.4, wht, wht, 66, 66, 292, 802, 301, 301, f2); addText("run", 93.4, 95.4, blk, blk, 66, 66, 290, 800, 300, 300, f2); addText("you", 93.9, 95.4, blk, blk, 66, 66, 210, 210, 350, 350, f); addText("over.", 94.6, 95.4, red1, red1, 66, 66, 350, 350, 350, 350, f); addBackground(96, 102, bg2); // TIMES COLOURS SIZE PositionX PositionY, Font addText("Did", 96.0, 101, gray1, gray2, 66, 66, 50, 50, 100, 100, f); addText("you", 96.4, 101, gray1, gray2, 66, 66, 200, 200, 100, 100, f); addText("notice", 97.1, 101, gray1, gray2, 66, 66, 300, 300, 150, 150, f); addText("every", 98.3, 101, gray1, gray2, 66, 66, 50, 50, 200, 200, f); addText("day's", 99.0, 101, gray1, gray2, 66, 66, 250, 250, 200, 200, f); addText("the", 99.9, 101, gray1, gray2, 66, 66, 150, 150, 250, 250, f); addText("same.", 100.1, 101, gray1, gray2, 66, 66, 300, 300, 250, 250, f); addBackground(102, 180, bg1); // TIMES COLOURS SIZE PositionX PositionY, Font addText("Managers", 123.4, 135, blk, blk, 66, 66, 10, 10, 50, 50, f); addText("disturbing", 124.8, 135, blk, blk, 66, 66, 100, 100, 100, 100, f); addText("me,", 126.4, 135, blk, blk, 66, 66, 340, 340, 50, 50, f); addText("there", 128.8, 135, blk, blk, 66, 66, 10, 10, 150, 150, f); addText("is", 129.3, 135, blk, blk, 66, 66, 10, 10, 200, 200, f); addText("an", 129.7, 135, blk, blk, 66, 66, 10, 10, 250, 250, f); addText("issue...", 130.0, 135, blk, red1, 66, 66, 10, 10, 300, 300, f); addText("of", 132.5, 135, blk, blk, 66, 66, 100, 100, 200, 200, f); addText("extreme", 132.9, 135, blk, blk, 66, 66, 100, 100, 250, 250, f); addText("importancy", 134.0, 135, blk, blk, 66, 66, 10, 10, 350, 350, f); addText("Disgruntled", 136.8, 140, blk, blk, 66, 66, 10, 10, 275, 250, f); addText("employee.", 137.9, 140, blk, blk, 66, 66, 300, 300, 275, 300, f); // --------------------------------------------------------------------------- // Let's repeat... // --------------------------------------------------------------------------- t0 = (2 * 60 + 25.5) - 68.5; addImage(60, 80, gyroImg); // TIMES COLOURS SIZE PositionX PositionY, Font addText("Perfect", 68.5, 69, blk, blk, 66, 66, 300, 300, 300, 300, f); addText("spinning", 69.3, 69.8, blk, blk, 66, 66, 300, 300, 300, 300, f); addText("hamster", 70.6, 71.1, blk, blk, 66, 66, 200, 200, 300, 300, f); addText("gyro", 71.4, 71.9, blk, blk, 66, 66, 200, 200, 300, 300, f); addText("rewards", 73.2, 73.7, blk, blk, 66, 66, 300, 300, 300, 300, f); addText("will", 74.2, 74.7, blk, blk, 66, 66, 300, 300, 300, 300, f); addText("be", 74.5, 75, blk, blk, 66, 66, 200, 200, 300, 300, f); addText("here", 75.1, 75.6, blk, blk, 66, 66, 200, 200, 300, 300, f); addText("tomorrow.", 75.7, 76.7, blk, blk, 66, 66, 250, 250, 300, 300, f); t0 = (2 * 60 + 36) - 77.7; // TIMES COLOURS SIZE PositionX PositionY, Font addText("Do", 77.7, 83, blk, blk, 66, 66, 10, 10, 100, 100, f); addText("your", 78.0, 83, blk, blk, 66, 66, 110, 110, 100, 100, f); addText("kids", 78.8, 83, blk, blk, 66, 66, 300, 300, 100, 100, f2); addText("know", 79.1, 83, blk, blk, 66, 66, 100, 200, 150, 150, f); addText("your", 80.8, 83, blk, blk, 66, 66, 10, 10, 200, 200, f); addText("middle", 81.1, 83, blk, blk, 66, 66, 180, 180, 200, 200, f); addText("name?", 82.0, 83, blk, blk, 66, 66, 410, 410, 200, 200, f); t0 = (2 * 60 + 44.5) - 86.8; addImage(86, 96, truckImg); // TIMES COLOURS SIZE PositionX PositionY, Font addText("Posture", 86.8, 91.6, blk, blk, 66, 66, 10, 10, 100, 100, f); addText("of", 87.8, 91.6, blk, blk, 66, 66, 100, 100, 150, 150, f); addText("an", 88.3, 91.6, blk, blk, 66, 66, 200, 200, 150, 150, f); addText("old", 88.9, 91.6, blk, blk, 66, 66, 300, 300, 150, 150, f); addText("truck driver", 89.2, 91.6, blk, blk, 66, 66, 100, 100, 200, 200, f); addText("this", 91.6, 95.4, blk, blk, 66, 66, 30, 30, 250, 250, f); addText("life", 91.9, 95.4, blk, blk, 66, 66, 180, 180, 250, 250, f); addText("seems", 92.3, 95.4, blk, blk, 66, 66, 300, 300, 250, 250, f); addText("to", 92.8, 95.4, blk, blk, 66, 66, 10, 10, 300, 300, f); addText("have", 93.1, 95.4, blk, blk, 66, 66, 100, 100, 300, 300, f); addText("run", 93.4, 95.4, blk, blk, 66, 66, 290, 800, 300, 300, f2); addText("you", 93.9, 95.4, blk, blk, 66, 66, 210, 210, 350, 350, f); addText("over.", 94.6, 95.4, red1, red1, 66, 66, 350, 350, 350, 350, f); t0 = (2 * 60 + 53.7) - 96.8; addBackground(96, 102, bg2); // TIMES COLOURS SIZE PositionX PositionY, Font addText("Did", 96.0, 101, gray1, gray2, 66, 66, 50, 50, 100, 100, f); addText("you", 96.4, 101, gray1, gray2, 66, 66, 200, 200, 100, 100, f); addText("notice", 97.1, 101, gray1, gray2, 66, 66, 300, 300, 150, 150, f); addText("every", 98.3, 101, gray1, gray2, 66, 66, 50, 50, 200, 200, f); addText("day's", 99.0, 101, gray1, gray2, 66, 66, 250, 250, 200, 200, f); addText("the", 99.9, 101, gray1, gray2, 66, 66, 150, 150, 250, 250, f); addText("same.", 100.1, 101, gray1, gray2, 66, 66, 300, 300, 250, 250, f); addBackground(102, 180, bg1); // --------------------------------------------------------------------------- // Count how many we have of everything // --------------------------------------------------------------------------- numTextEntries = textEntries.size(); numImages = backgrounds.size(); numBackgrounds = backgrounds.size(); // --------------------------------------------------------------------------- // Get ready to roll! // --------------------------------------------------------------------------- if (playAtStartup) playhead.play(); } // ----------------------------------------------------------------------------- // Helper methods to make adding a new entries to the ArrayLists easy. // Uses offset t0 to adjust the timing. // Also adjust finishTime if a later finish time is used. // For the moment a rotation is applied to every entry as well. // ----------------------------------------------------------------------------- void addText(String content, float t1, float t2, color c1, color c2, float s1, float s2, float x1, float x2, float y1, float y2, PFont font) { addText(content, t1, t2, c1, c2, s1, s2, x1, x2, y1, y2, font, 0); } void addText(String content, float t1, float t2, color c1, color c2, float s1, float s2, float x1, float x2, float y1, float y2, PFont font, float rotation) { float rot1 = -rotation/2; float rot2 = rotation/2; textEntries.add(new TimedText(content,t0+t1,t0+t2,c1,c2,s1,s2,x1,x2,y1,y2,rot1,rot2,font)); if (t2 > finishTime) finishTime = t2; } void addBackground(float t1, float t2, PImage img) { // Want the background images centred in the frame float x = width >> 1; float y = height >> 1; backgrounds.add(new TimedImage(t0 + t1, t0 + t2, img, x, x, y, y, 0, 0)); if (t2 > finishTime) finishTime = t2; } void addImage(float t1, float t2, PImage img) { addImage(t1, t2, img, 0); } void addImage(float t1, float t2, PImage img, float rotation) { float x = width - img.width * 0.5; float y = height * 0.5; float rot1 = -rotation/2; float rot2 = rotation/2; images.add(new TimedImage(t0 + t1, t0 + t2, img, x, x, y, y, rot1, rot2)); if (t2 > finishTime) finishTime = t2; } // ----------------------------------------------------------------------------- // Handle keyboard input // ----------------------------------------------------------------------------- void keyPressed() { if (key == CODED) switch(keyCode) { case LEFT: // Go back a couple of seconds playhead.cueTo(playhead.runningTime - 2); break; case RIGHT: // Advance a couple of seconds playhead.cueTo(playhead.runningTime + 2); break; } else switch(key) { case ' ': // Pause/play playhead.togglePaused(); break; case 'q': // Quit println("Exiting!"); exit(); break; case 'f': // Save frame if (outputRender != null) outputRender.playhead.recordFrame(saveFileNameFormat); else println("Nothing rendered yet to save!"); break; case 'p': // Play (normal play speed) playhead.playSpeed = 1; playhead.play(); break; case 'r': // Rewind to beginning playhead.rewindToStart(); break; case 'R': // Rewind to beginning (and pause) playhead.rewindToStart(); playhead.pause(); break; case 's': // Toggle saveing toggleSave(); break; case 't': // Timecode display on/off displayStats = !displayStats; break; case ',': // Frame back playhead.frameRewind(); break; case '.': // Frame forward playhead.frameAdvance(); break; case '[': // Reduce playspeed playhead.playSpeed -= 0.2; break; case ']': // Increase playspeed playhead.playSpeed += 0.2; break; case '_': // Reduce target frame rate (by a fraction) playhead.setFrameRate(playhead.targetFrameRate - 0.1); break; case '-': // Reduce target frame rate playhead.setFrameRate(playhead.targetFrameRate - 1); break; case '+': // Increase target frame rate (by a fraction) playhead.setFrameRate(playhead.targetFrameRate + 0.1); break; case '=': // Increase target frame rate playhead.setFrameRate(playhead.targetFrameRate + 1); break; case '0': setRenderQuality(0); break; case '1': setRenderQuality(1); break; case '2': setRenderQuality(2); break; case '3': setRenderQuality(3); break; case '4': setRenderQuality(4); break; case '5': setRenderQuality(5); break; case '6': setRenderQuality(6); break; case '7': setRenderQuality(7); break; case '8': setRenderQuality(8); break; case '9': setRenderQuality(9); break; } } public void setRenderQuality(int level) { renderQuality = level; println("renderQuality set to "+renderQuality); } public void toggleSave() { saveToFile = !saveToFile; println("saveToFile: " + saveToFile); playhead.pause(); } public void enableSave() { saveToFile = true; println("Saving of frames enabled. saveFileNameFormat: " + saveFileNameFormat); } public void disableSave() { saveToFile = false; println("Saving of frames disbled."); } // ----------------------------------------------------------------------------- // Observe real time passing // ----------------------------------------------------------------------------- void tickTock() { clockMillis = millis(); clock = clockMillis * 0.001; } // ----------------------------------------------------------------------------- // The main draw() method // ----------------------------------------------------------------------------- void draw() { float prevClock = clock; int maxSubFrames = renderQuality == 0 ? 1 : int(pow(2, renderQuality + 1)); // Limit the render quality when not recording, unless paused if (!saveToFile && !playhead.paused) maxSubFrames = min(maxSubFrames, 4); if(saveToFile) { boolean needRender = (outputRender == null) || (playhead.frame != outputRender.playhead.frame) || (playhead.targetFrameRate != outputRender.playhead.targetFrameRate) || (maxSubFrames != outputRender.maxSubFrames); // Advance a frame if the current frame is already rendered and saved if (!needRender && outputRender.playhead.frameSaved && !playhead.paused) { playhead.frameAdvance(); needRender = true; } if (playhead.frame >= 0 && playhead.time < finishTime) { if (needRender) outputRender = renderFrame(maxSubFrames); else print("s"); // Same as last frame, just refreshing the display image(outputRender.img, 0, 0); if (!outputRender.playhead.frameSaved) outputRender.playhead.recordFrame(saveFileNameFormat); } else { // Do something when the finish time is reached? println("*** End of video ***"); disableSave(); playhead.pause(); image(outputRender.img, 0, 0); } } else // Not saving to file; realtime running { tickTock(); // If not at the first frame, or we are at the first frame but it has // already been rendered (and therefore displayed), then continue on (note // that the playhead could be paused). The target framerate and number of // subFrames in the existing rendered output aren't important considerations // since we're trying to play in "real time". if ((playhead.frame != 0) || (outputRender != null && outputRender.playhead.frame == 0)) { float frameTimeAdvance = clock - prevClock; playhead.run(frameTimeAdvance); } if (playhead.frame >= 0 && playhead.time < finishTime) { // Need a new render if: we haven't done one yet, the frame number is // different, or the playhead is paused and the render quality (number of // subframes) is too high or the framerate has changed. if (outputRender == null || playhead.frame != outputRender.playhead.frame || playhead.targetFrameRate != outputRender.playhead.targetFrameRate || (playhead.paused && maxSubFrames != outputRender.maxSubFrames)) { outputRender = renderFrame(maxSubFrames); } else { print("."); // Same as last frame, just refreshing the display } } else { // Do something when the finish time is reached? println("*** End of video ***"); playhead.pause(); } image(outputRender.img, 0, 0); } // --------------------------------------------------------------------------- // Statistics (Timestamp, etc) for reference // --------------------------------------------------------------------------- if (displayStats) { pushStyle(); String txt = " " + outputRender.playhead.fmtRunningTimeInfo(); if (playhead.paused) txt += " PAUSED"; if (saveToFile) txt += " REC"; else if (playhead.playSpeed != 1) txt += " " + nf(playhead.playSpeed, 1, 1) + "x play"; else if (!playhead.paused) txt += " PLAY"; if (outputRender.playhead.frameSaved) txt += " SAVED"; noStroke(); fill(63); rect(0, 0, width, 22); textFont(f, 16); fill(220); text(txt, 10, 18); popStyle(); } } // ----------------------------------------------------------------------------- // The draw() method had to figure out some details about what to do; // This method does it (renders the frame). // Motion blur is produced by rendering a number of sub-frames and combining // the result, with later subframes weighted more than earlier sub-frames. // ----------------------------------------------------------------------------- RenderResult renderFrame(int maxSubFrames) { int startTime = millis(); // If the playhead is at an earlier time than for the last render, we // need to reset the firstTextEntry optimisation, as we don't know // how far back to go. if (outputRender == null || playhead.time < outputRender.playhead.time) { firstTextEntry = 0; } // The motion estimate is for one second; recalculate for current frame rate. float frameMotionEstimate = findMaxMotionEstimate() / playhead.targetFrameRate; // Don't use more subframes than the number of pixels the maximum motion // estimate gives, unless that is zero: always need at least one subframe! int numSubFrames = min( maxSubFrames, ceil(frameMotionEstimate)); numSubFrames = max( numSubFrames, 1); // Need at least one subframe! println("["+playhead.frame+"] frameMotionEstimate: "+frameMotionEstimate+" pixels -> numSubFrames: "+numSubFrames); if (numSubFrames < 2) // So only one subframe { renderSubFrame(playhead.time); return new RenderResult(get(), playhead, numSubFrames, maxSubFrames, millis() - startTime); } // Actually draw a number of sub-frames to better handle fast motion float subFrameDuration = playhead.frameDuration / numSubFrames; // Arrays to hold aggregated pixel data from the multiple sub-frames int[] calcR = new int[width * height]; int[] calcG = new int[width * height]; int[] calcB = new int[width * height]; int pixel; // A single sampled pixel // --------------------------------------------------------------------------- // Generate sub-frames and add them to the calculation arrays // --------------------------------------------------------------------------- for (int sf = 0; sf < numSubFrames; /* sf++ */ ) { float subFrameTime = playhead.time + sf * subFrameDuration; renderSubFrame(subFrameTime); loadPixels(); // Bit-mashing is faster than calling red(), green() and blue() // Also, we want to increment sf at the end of the loop, but since // we don't need "sf" anymore (and we do need "sf+1"), let's // just do the increment now. :o) sf++; for (int i = 0; i < pixels.length; i++) { pixel = pixels[i]; // Get the pixel value from the sub-frame calcR[i] += ((pixel >> 16) & 0xff) * sf; calcG[i] += ((pixel >> 8) & 0xff) * sf; calcB[i] += (pixel & 0xff) * sf; } } // Divide the calculation arrays by the sum of the weights of the sub-frames for // the final output int divisor = (numSubFrames * (numSubFrames + 1)) >> 1; // ie 1+2+3+...+numSubFields for (int i = 0; i < pixels.length; i++) { pixels[i] = ((calcR[i]/divisor) << 16) | ((calcG[i]/divisor) << 8) | (calcB[i]/divisor); } updatePixels(); return new RenderResult(get(), playhead, numSubFrames, maxSubFrames, millis() - startTime); } // ----------------------------------------------------------------------------- // Find out what the fastest moving item is in te current frame. // ----------------------------------------------------------------------------- private float findMaxMotionEstimate() { float maxMotionEstimate = 0; float frameEndTime = playhead.time + playhead.frameDuration; boolean foundCurrentFirst = false; for (int i = firstTextEntry; i < numTextEntries; i++) { TimedText entry = (TimedText) textEntries.get(i); // Not up to the next entry yet, so break out // Note: this depends on text entries appearing in order of start times if (frameEndTime < entry.t1) break; // Only update this shortcut if we haven't already found an active // entry in this loop. if (!foundCurrentFirst) firstTextEntry = i; // It seems this entry is over now, so go on to the next entry if (entry.t2 < playhead.time) continue; // We've found an active text entry. Let's check its speed. // We don't want to increase 'firstTextEntry' any more since // we know we need to render this item. foundCurrentFirst = true; if (entry.motionEstimate > maxMotionEstimate) maxMotionEstimate = entry.motionEstimate; } for (int i = 0; i < numImages; i++) { TimedImage overlay = (TimedImage) images.get(i); if (overlay.active(playhead.time, playhead.frameDuration)) { if (overlay.motionEstimate > maxMotionEstimate) maxMotionEstimate = overlay.motionEstimate; } } return maxMotionEstimate; } // ----------------------------------------------------------------------------- // Render a moment in time // ----------------------------------------------------------------------------- private void renderSubFrame(float subFrameTime) { // --------------------------------------------------------------------------- // The background needs to be set first // --------------------------------------------------------------------------- background(0); for (int i = 0; i < numBackgrounds; i++) { TimedImage bg = (TimedImage) backgrounds.get(i); bg.render(subFrameTime); } // --------------------------------------------------------------------------- // Then overlay images // --------------------------------------------------------------------------- for (int i = 0; i < numImages; i++) { TimedImage overlay = (TimedImage) images.get(i); overlay.render(subFrameTime); } // --------------------------------------------------------------------------- // And finally the text on top // --------------------------------------------------------------------------- smooth(); boolean foundCurrentFirst = false; for (int i = firstTextEntry; i < numTextEntries; i++) { TimedText entry = (TimedText) textEntries.get(i); // Not up to the next entry yet, so break out // Note: this depends on text entries appearing in order of start times if (subFrameTime < entry.t1) break; // Only update this shortcut if we haven't already found an active // entry in this loop. if (!foundCurrentFirst) firstTextEntry = i; // It seems this entry is over now, so go on to the next entry if (entry.t2 < subFrameTime) continue; // Okay, so now we have an active text entry. Let's render it. // We don't want to increase 'firstTextEntry' any more since // we might have to render this item next (sub-)frame. foundCurrentFirst = true; entry.render(subFrameTime); } }