//=================================================================================== // // Video Streamer // // a look at motion from an odd angle // // or // // an attempt at resolving these three: // // space = view(time); // // time = change(space); // // view = space + time; // // // ---> interesting patterns emerge when looking at space and time at once // //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // eddie elliott // www.lightmoves.net // eddie@lightmoves.net //=================================================================================== // // the parts: // // StreamerBlock - stacks images and plays through them // // VideoClip - produces video frames from filmstrip-like image files // // StartupClip - extends VideoClip to produce frames from code // // ImageButton - renders buttons using images // // ButtonSet - manages all the buttons in this sketch // //=================================================================================== StreamerBlock block; // a stack of images ButtonSet buttonset; // clip buttons, help button and panel, etc. String[] videoStrips = { // images containing hundreds of video frames ... "wand.jpg", "swirls.jpg", "birds.jpg", "light.jpg", "sand.jpg", "vortex.jpg", "orangewhite.jpg", "startup" // ... except this one (just a placeholder string) }; VideoClip[] videoClips; // at most, two loaded at once VideoClip loadingClip; // the clip currently loading, if any int loadingClipIndex = -1; // its destination in videoClips int streamingClipIndex = -1; // block's current clip VideoClip startupClip; // fast initializing clip to show something immediately VideoClip loadingProgress; // indicates loading in progress static final int HORIZONTAL_SPACING = 0; // space between frames in image strips static final int VERTICAL_SPACING = 0; static final int FRAME_W = 128; // default frame dimensions static final int FRAME_H = 96; color backgroundColor; color rolloverTextBoxStrokeColor; color rolloverTextBoxColor; color rolloverTextColor; int rolloverTextSize; int rolloverTimeout; BFont rolloverFont; int frameCounter; int clipsLoadedCount; void setup() { size (500, 400); framerate(12); colorMode(HSB, 1.0); backgroundColor = color(0.1, 0.1, 0.39); background(backgroundColor); rolloverTextBoxColor = color(0.15, 0.1, 0.9); rolloverTextColor = color(0.15, 0.2, 0.2); rolloverTextBoxStrokeColor = color(0.0, 0.0, 0.3); rolloverTextSize = 16; rolloverFont = loadFont("Meta-Bold.vlw.gz"); textFont(rolloverFont, rolloverTextSize); // a small animation to indicate that a clip is loading (image file being read from server) loadingProgress = new VideoClip("spinningArrows.gif", 18, 18, 0, 0); videoClips = new VideoClip[videoStrips.length]; buttonset = new ButtonSet(); buttonset.updateImages(); block = new StreamerBlock(200, FRAME_W, FRAME_H); // give the block a copy of StartupClip to show while first image clip loads startupClip = new StartupClip(FRAME_W, FRAME_H, 300); block.setVideoClip(startupClip); streamingClipIndex = 7; // begin loading first image clip loadClip(0); } void loop() { frameCounter++; //------------------------------------------ // check status of loading clip, if any //------------------------------------------ if (loadingClip != null) { if (loadingClip.failed) { println("failed to load clip: " + videoStrips[loadingClipIndex]); loadingClipIndex = -1; loadingClip = null; startupClip = null; buttonset.updateImages(); } else if (loadingClip.isReady()) { clipsLoadedCount++; block.setVideoClip(loadingClip); streamingClipIndex = loadingClipIndex; videoClips[loadingClipIndex] = loadingClip; loadingClipIndex = -1; loadingClip = null; startupClip = null; buttonset.updateImages(); } } int buttonAtMouse = buttonset.buttonAtPoint(mouseX, mouseY); //------------------------------------------ // update things //------------------------------------------ block.step(); buttonset.update(); int c = ARROW; if (buttonAtMouse >= 0) { if (buttonset.buttons[buttonAtMouse].enabled) { c = HAND; } } else if (block.mouseOverPlayer || block.mouseOverBlock) { c = HAND; } cursor(c); //------------------------------------------ // draw things //------------------------------------------ background(backgroundColor); block.draw(); // show progress animation if clip still loading, but only after first clip if ((loadingClip != null) && (!loadingClip.isReady()) && (clipsLoadedCount > 0)) { if (loadingProgress.isReady()) { int frame = (millis() / 250) % 4; image(loadingProgress.getFrame(frame), width - 60, 18 + (loadingClipIndex * 35)); } } buttonset.draw(); // display rollover "tooltip", if any if (millis() < rolloverTimeout) { if (buttonAtMouse >= 0) { // only show rollover for clip buttons and when clip not loaded if ((buttonAtMouse <= buttonset.STARTUP_CLIP) && (videoClips[buttonAtMouse] == null)) { ImageButton b = buttonset.buttons[buttonAtMouse]; if (b.enabled && b.over && (b.rolloverText != null)) { drawRollover(b.rolloverText , mouseX, mouseY); } } } } } void drawRollover(String string, int x, int y) { int textW = int(rolloverFont.width(string) + 0.5); int boxW = textW + 4; int boxLeft = min(x, width - boxW); int boxTop = min(y + 22, height - rolloverTextSize - 2); fill(rolloverTextBoxColor); stroke(rolloverTextBoxStrokeColor); rect(boxLeft, boxTop, boxW, rolloverTextSize + 2); textSize(rolloverTextSize); fill(rolloverTextColor); text(string, boxLeft + 2, boxTop + rolloverTextSize - 2); } public void stop() { // stop any downloads in progress if (loadingClip != null) { loadingClip.stop(); loadingClip = null; } // It appears loop() sometimes continues to be called after the browser // leaves the page, at least with IE. Setting the "finished" flag true // seems to help end the applet. finished = true; } void loadClip(int index) { if (loadingClip != null) { // not done loading current clip ==> ignore request return; } if (index == loadingClipIndex) { // already loading it ==> do nothing return; } VideoClip existingClip = videoClips[index]; if (existingClip != null) { // already loaded ==> just give it to the block streamingClipIndex = index; block.setVideoClip(existingClip); return; } loadingClipIndex = index; // clear old clip(s), if any, to hopefully release memory for (int i = 0; i < videoClips.length; i++) { if (i != streamingClipIndex) { videoClips[i] = null; } } if (index == ButtonSet.STARTUP_CLIP) { loadingClip = new StartupClip(FRAME_W, FRAME_H, 300); } else { loadingClip = new VideoClip(videoStrips[index], FRAME_W, FRAME_H, HORIZONTAL_SPACING, VERTICAL_SPACING); } } //----------------------------------------------------------------------------------- // key and mouse handlers //----------------------------------------------------------------------------------- void keyPressed() { if (key == 0) return; if (key == 32) { // spacebar block.streaming = !block.streaming; block.playerSpeed = 1; } else { buttonset.setHelpPanelVisible(false); } } void mousePressed() { if (buttonset.handleMousePress(mouseX, mouseY)) return; if (block.handleMousePress(mouseX, mouseY)) return; // anywhwere else toggles streaming and resets the play speed block.streaming = !block.streaming; block.playerSpeed = 1; } void mouseReleased() { buttonset.handleMouseRelease(mouseX, mouseY); } void mouseMoved() { rolloverTimeout = millis() + 3000; } //=================================================================================== //=================================================================================== // StreamerBlock - holds an array of references to frames in a VideoClip //=================================================================================== class StreamerBlock { boolean streaming = true; //----- guts of the block ----------------- int length; // number of frames in the block int len; // len = length - 1 int frameCount; // actual number of frames currently loaded (0 <= frameCount <= length) VideoClip clip; // source of video frames int[] frameRefs; // maps from block frames to clip frames //----- rendering dimensions -------------- int blockTop = 2; int blockLeft = 2;; int headTop; int headLeft; int frameW; int frameH; static final int WING_FRAMES = 7; static final float RELATIVE_SCALE = 0.55; int[] wingFrameWidths; //----- player stuff ---------------------- int playFrame = -1; color playFrameHighlightColor; color wingHighlightColor; Rectangle playFrameRect; Rectangle widePlayerRect; boolean playerExpanded = false; float playerSpeed = 1; boolean playFrameIsOffset = false; // abandoned experiment int playOffsetX = -50; int playOffsetY = -10; //----- mouse status ---------------------- boolean mouseOverBlock = false; boolean mouseOverPlayer = false; int lastTouchedFrame = -1; // frame last touched by mouse, -1 when mouse away // Tried fading out frames at far end of block. However, I haven't yet figured out how to // get replicate() and tint(gray, alpha) to work togehter, so fadeEnabled set false for now. boolean fadeEnabled = false; BImage workImage; // handy for copying pixels around StreamerBlock(int blockLen, int _frameW, int _frameH) { length = blockLen; frameW = _frameW; frameH = _frameH; len = length - 1; workImage = new BImage(new int[frameW * frameH], frameW, frameH, RGB); playFrameHighlightColor = color(0.0, 0.6, 0.8, 0.7); wingHighlightColor = color(0.0, 0.4, 0.8, 0.7); headTop = blockTop + len; headLeft = blockLeft + len; playFrameRect = new Rectangle(headLeft, headTop + 2 + frameH, frameW, frameH); wingFrameWidths = new int[WING_FRAMES]; float width = RELATIVE_SCALE; int wingW = 0; for (int i = 0; i < WING_FRAMES; i++) { wingFrameWidths[i] = (int) (width * frameW); wingW += wingFrameWidths[i] + 1; width *= RELATIVE_SCALE; } widePlayerRect = new Rectangle(headLeft - wingW, headTop + 2 + frameH, frameW + 2 * wingW, frameH); frameRefs = new int[length]; for (int i = 0; i < length; i++) { frameRefs[i] = -1; // -1 indicates no frame yet } } void setVideoClip(VideoClip _clip) { clip = _clip; frameW = clip.frameW; frameH = clip.frameH; for (int i = 0; i < frameRefs.length; i++) { frameRefs[i] = -1; // -1 indicates no frame yet } playFrame = -1; // automatically start streaming, if paused streaming = true; } boolean handleMousePress(int mouseX, int mouseY) { if (mouseOverPlayer) { playerExpanded = !playerExpanded; return true; } return false; } void step() { if (streaming) { // slide everything down a frame along the t-axis // "time keeps on slipping, slipping, into the past" for (int i = len; i > 0; i--) { frameRefs[i] = frameRefs[i-1]; } // assign new head frame if (clip != null) { frameRefs[0] = (frameRefs[0] + 1) % clip.frameCount; } // count number of frames actually loaded in the block frameCount = 0; for (int i = 0; i < length; i++) { if (frameRefs[i] >= 0) { frameCount++; } } // the play frame drifts towards the far end of the block, though // the mouse can still reset it below if (playFrame >= 0) { playFrame++; if (playFrame > len) { playFrame = -1; } } } else { // paused for playback // ease play speed towards 1.0 if ((-1.0 < playerSpeed) && (playerSpeed <= 1.1)) { playerSpeed = 1; } else { playerSpeed = 1 + 0.95 * (playerSpeed - 1); } } // handle mouse if (playerExpanded) { mouseOverPlayer = (playFrame >= 0) && widePlayerRect.contains(mouseX, mouseY); } else { mouseOverPlayer = (playFrame >= 0) && playFrameRect.contains(mouseX, mouseY); } if (mouseOverPlayer) { mouseOverBlock = false; } else { int frameNum = frameNumAtPoint(mouseX, mouseY); mouseOverBlock = (frameNum >= 0); // adjust playSpeed if mouse over block two steps in a row if (mouseOverBlock && (lastTouchedFrame >= 0)) { playerSpeed = lastTouchedFrame - frameNum; if (playerSpeed == 0) { playerSpeed = 1; } } lastTouchedFrame = frameNum; if (mouseOverBlock) { playFrame = frameNum; } } if ((!streaming) && (!mouseOverBlock)) { //playFrame--; playFrame = int(playFrame - playerSpeed); if (playFrame < 0) { playFrame = frameCount - 1; } if (playFrame >= frameCount) { playFrame = 0; } } } void draw() { drawBlock(); drawPlayer(); } void drawBlock() { BImage frame; for (int i = len; i > 0; i--) { if (fadeEnabled) { int fadeFrames = 10; if (i > (len-fadeFrames)) { float opacity = 1 - ((len-i) / float(fadeFrames)); tint(255, opacity * 255); } else { noTint(); } } int frameIndex = frameRefs[i]; if (frameIndex >= 0) { int z = len - i; if (clip != null) { //clip.drawFrame(frameIndex, blockLeft+z, blockTop+z); frame = clip.getFrame(frameIndex, workImage); if (playFrameIsOffset && (i == playFrame)) { int dx1 = blockLeft + z + playOffsetX; int dy1 = blockTop + z + playOffsetY; replicate(frame, 0, 0, frameW, frameH, dx1, dy1, dx1 + frameW, dy1 + frameH); } else { int dx1 = blockLeft + z; int dy1 = blockTop + z; replicate(frame, 0, 0, frameW, 1, dx1, dy1, dx1 + frameW, dy1 + 1); replicate(frame, 0, 0, 1, frameH, dx1, dy1, dx1 + 1, dy1 + frameH); } } } } // head frame // Note: Seems replicate() works better than image() for the rendered clip // frames (i.e. in StartupClip, not the image frames in VideoClip). //image(clip.getFrame(frameRefs[0]), blockLeft+len, blockTop+len); frame = clip.getFrame(frameRefs[0], workImage); replicate(frame, 0, 0, FRAME_W, FRAME_H, headLeft, headTop, headLeft + FRAME_W, headTop + FRAME_H); // edge lines colorMode(HSB, 1.0); stroke(0.0, 0.0, 0.5); line(headLeft, headTop, headLeft - (frameCount - 1), headTop - (frameCount - 1)); line(headLeft, headTop, headLeft + frameW - 1, headTop); line(headLeft, headTop, headLeft, headTop + frameH - 1); } void drawPlayer() { if (clip == null) return; if ((playFrame < 0) || (frameRefs[playFrame] < 0)) return; if (playerExpanded) { // left wing int x = playFrameRect.x; for (int i = 0; i < WING_FRAMES; i++) { int frameIndex = playFrame + 1 + i; if (frameIndex <= len) { int frameRef = frameRefs[frameIndex]; if ((frameRef >= 0)) { int w = wingFrameWidths[i]; x -= w + 1; BImage frame = clip.getFrame(frameRef, workImage); replicate(frame, 0, 0, frameW, frameH, x, playFrameRect.y, x+w, playFrameRect.y+playFrameRect.height); } } } // right wing x = playFrameRect.x + playFrameRect.width + 1; for (int i = 0; i < WING_FRAMES; i++) { int frameIndex = playFrame - 1 - i; if (frameIndex >= 0) { int frameRef = frameRefs[frameIndex]; if ((frameRef >= 0)) { int w = wingFrameWidths[i]; BImage frame = clip.getFrame(frameRef, workImage); replicate(frame, 0, 0, frameW-1, frameH-1, x, playFrameRect.y, x+w, playFrameRect.y+playFrameRect.height); x += w + 1; } } } } int frameRef = frameRefs[playFrame]; if (frameRef >= 0) { // draw image // Note: Seems replicate() works better than image() for the rendered clip // frames (i.e. in StartupClip, not the image frames in VideoClip). //image(clip.getFrame(frameRef), playFrameRect.x, playFrameRect.y); BImage frame = clip.getFrame(frameRef, workImage); replicate(frame, 0, 0, FRAME_W, FRAME_H, playFrameRect.x, playFrameRect.y, playFrameRect.x + FRAME_W, playFrameRect.y + FRAME_H); if (playerExpanded) { // highlight extents of player "wings" int earliestFrame = constrain(playFrame + WING_FRAMES, 0, frameCount-1); int latestFrame = constrain(playFrame - WING_FRAMES, 0, frameCount-1); stroke(wingHighlightColor); for (int i = earliestFrame; i >= latestFrame; i--) { int x = blockLeft + len - i; int y = blockTop + len - i; line(x, y, x + frameW - 1, y); line(x, y, x, y + frameH - 1); } } // highlight play frame int x = blockLeft + len - playFrame; int y = blockTop + len - playFrame; stroke(playFrameHighlightColor); line(x, y, x + frameW - 1, y); line(x, y, x, y + frameH - 1); } } // returns index of frame at given point, or -1 if point not over the block int frameNumAtPoint(int px, int py) { int x = px - blockLeft; int y = py - blockTop; if ((x < 0) || (y < 0)) { return -1; } if ((x >= len) && (y >= len)) { boolean toRight = x >= (len + frameW); boolean toBottom = y >= (len + frameH); if (toRight || toBottom) { return -1; } else { return 0; } } if (x > y) { if (x >= (y + frameW)) { return -1; } return len - y; } else { if (y >= (x + frameH)) { return -1; } return len - x; } } } // end StreamerBlock //=================================================================================== //=================================================================================== // VideoClip - a sequence of images, either in an array or in a single image strip. //=================================================================================== class VideoClip { int frameW = FRAME_W; int frameH = FRAME_H; int frameCount = 0; BImage strip; // single image, holding a series of frames int columnW ; // width of frame and horizontal spacing int rowH; // height of frame and vertical spacing int hSpacing; int columns = 1; // number of columns in strip BImage workImage; // holds temporary copies of pixels as a BImage BImage[] frames; // collection of individual frame images Thread thread; boolean failed = false; // this empty default constructor keeps the compiler happy with the StartupClip constructor VideoClip() { } // Constructs a VideoClip from a single image file containing a series of image frames. // Uses a thread to load the image file (likely relatively large) in the background. // Call VideoClip.isReady() to determine when the image is done loading. VideoClip(final String stripFileName, final int _frameW, final int _frameH, final int _hSpacing, final int vSpacing) { thread = new Thread(new Runnable() { public void run() { // Note: calling loadImage with the flag false to enable use of cache, in case this // image was downloaded previously. See Ben's note at the bottom of // http://processing.org/discourse/yabb/YaBB.cgi?board=Programs;action=display;num=1078795681 try { BImage img = loadImage(stripFileName, false); frameW = _frameW; frameH = _frameH; hSpacing = _hSpacing; columnW = frameW + hSpacing; if (hSpacing == 0) { columns = img.width / frameW; } else { columns = (img.width / columnW) + 1; } rowH = frameH + vSpacing; frameCount = columns * img.height / rowH; workImage = new BImage(new int[frameW * frameH], frameW, frameH, RGB); strip = img; // assign to strip, signalling ready } catch (java.lang.OutOfMemoryError e) { failed = true; return; } } }, "thread-" + stripFileName); // Not sure if this is necessary. The intent is to make sure that the JVM doesn't get // caught on a long download if you close your browser before the download completes. thread.setDaemon(true); thread.start(); } // constructs a VideoClip from an array of images VideoClip(BImage[] _frames) { frameCount = _frames.length; frameW = _frames[0].width; frameH = _frames[0].height; frames = _frames; // assign to frames, signalling ready } // constructs a VideoClip by loading a series of image files VideoClip(String filename, String ext, int _frameCount) { frameCount = _frameCount; BImage[] imageArray = new BImage[frameCount]; for(int i = 0; i < frameCount; i++) { int j = i + 1; String imageName = filename + ((j < 10) ? "00" : (j < 100) ? "0" : "") + j + ext; // Note: loadImage's flag set false here to enable use of cache imageArray[i] = loadImage(imageName, false); } frameW = imageArray[0].width; frameH = imageArray[0].height; frames = imageArray; // assign to frames, signalling ready } // returns true when image loading and initialization are complete boolean isReady() { return ((strip != null) || (frames != null)); } // tries to kill the image loading thread, if any void stop() { if ((thread != null) && thread.isAlive()) { // Note: not sure this works thread.interrupt(); } } BImage getFrame(int frameNum, BImage destImage) { frameNum = constrain(frameNum, 0, frameCount-1); if (frames == null) { if (destImage == null) { // return the image via workImage destImage = workImage; } // copy source pixels from strip to destination pixels in destImage if (columns == 1) { System.arraycopy(strip.pixels, frameNum * rowH * frameW, destImage.pixels, 0, frameW * frameH); } else { int rowWidth = strip.width; int row = frameNum / columns; int col = frameNum % columns; int srcLineStart = (row * rowWidth * rowH) + (col * columnW); int dstLineStart = 0; for (int r = 0; r < frameH; r++) { System.arraycopy(strip.pixels, srcLineStart, destImage.pixels, dstLineStart, frameW); srcLineStart += rowWidth; dstLineStart += frameW; } } return destImage; } else { return frames[frameNum]; } } BImage getFrame(int frameNum) { return getFrame(frameNum, workImage); } } // end VideoClip //=================================================================================== //=================================================================================== // StartupClip - doesn't require downloading a large image file. Intended for // showing something immediately on startup, while first image-based clip downloads. //=================================================================================== class StartupClip extends VideoClip { StartupClip(int w, int h, int count) { frameW = w; frameH = h; frameCount = count; BImage[] imageArray = new BImage[count]; int centerX = 20; int centerY = 20; int dx = 3; int dy = 4; int rays = 7; float phase = 0; float dp = -5 * PI / frameCount; float rayInc = TWO_PI / rays; float rayArc = rayInc / 2; float bh0 = 0.1; // background blend colors float bs0 = 0.1; float bb0 = 0.4; float bh1 = 0.1; float bs1 = 0.2; float bb1 = 0.9; float h0 = 0.6; // ray blend colors float s0 = 0.1; float b0 = 0.4; float h1 = 0.5; float s1 = 0.7; float b1 = 0.7; float radius = 200; for (int i = 0; i < count; i++) { float f = i / float(count); BGraphics bg = new BGraphics(w, h); // BGraphics extends BImage bg.colorMode(HSB, 1.0); bg.noStroke(); bg.fill(bh0 + f * (bh1 - bh0), bs0 + f * (bs1 - bs0), bb0 + f * (bb1 - bb0)); bg.rect(0, 0, w, h); bg.fill(h0 + f * (h1 - h0), s0 + f * (s1 - s0), b0 + f * (b1 - b0)); for (int j = 0; j < rays; j++) { float angle1 = phase + j * rayInc; float angle2 = angle1 + rayArc; float p1x = centerX + radius * cos(angle1); float p1y = centerY + radius * sin(angle1); float p2x = centerX + radius * cos(angle2); float p2y = centerY + radius * sin(angle2); bg.triangle(centerX, centerY, p1x, p1y, p2x, p2y); } imageArray[i] = bg; centerX += dx; centerY += dy; if ((centerX < -20) ||(centerX > frameW + 20)) { dx = -dx; } if ((centerY < -20) ||(centerY > frameH + 20)) { dy = -dy; } phase += dp; } frameW = w; frameH = h; frames = imageArray; // assign to frames, signalling ready } } // end StartupClip //=================================================================================== //=================================================================================== // ImageButton - renders a button's three states with images // Mostly just handles display. The real button action is below in ButtonSet. //=================================================================================== class ImageButton { Rectangle bounds; boolean enabled = true; boolean over = false; boolean pressed = false; BImage base; BImage roll; BImage down; boolean visible = true; float opacity = 1; String rolloverText; ImageButton(int x, int y, int w, int h, BImage b, BImage r, BImage d) { bounds = new Rectangle(x, y, w, h); base = b; roll = r; down = d; } void update() { over = bounds.contains(mouseX, mouseY); pressed = over && mousePressed; } void draw() { if (!visible) return; if (opacity < 1.0) { colorMode(RGB, 255); tint(255, opacity * 255); } if (pressed) { image(down, bounds.x, bounds.y); } else if (over) { image(roll, bounds.x, bounds.y); } else { image(base, bounds.x, bounds.y); } noTint(); } } // end ImageButton //=================================================================================== //=================================================================================== // ButtonSet - manages the clip buttons, as well as the help button and panel //=================================================================================== class ButtonSet { static final int STARTUP_CLIP = 7; static final int HELP_BUTTON = 8; static final int HELP_PANEL = 9; static final int MORE_BUTTON = 10; static final int CLIP_BUTTON_COUNT = STARTUP_CLIP + 1; ImageButton[] buttons; BImage[][] clipButtonImages; int activeButton = -1; int fadeStart = 40; // lag from applet start to when buttons begin appearing int fadeDuration = 24; // time it takes for all buttons to fade in int fadeEnd = fadeStart + fadeDuration; int buttonFadeFrames = 8; // duration of each individual button's fade in ButtonSet() { // Note: buttons are drawn following their order in the buttons array. // So, the first button in the array is the "farthest" and the last in // the array is the "nearest". buttons = new ImageButton[11]; clipButtonImages = new BImage[9][3]; createClipButton(0, "~ 30 sec @ 56k", "wandBase.gif", "wandOver.gif", "wandDown.gif"); createClipButton(1, "~ 60 sec @ 56k", "swirlsBase.gif", "swirlsOver.gif", "swirlsDown.gif"); createClipButton(2, "~ 40 sec @ 56k", "birdsBase.gif", "birdsOver.gif", "birdsDown.gif"); createClipButton(3, "~ 40 sec @ 56k", "lightBase.gif", "lightOver.gif", "lightDown.gif"); createClipButton(4, "~ 60 sec @ 56k", "sandBase.gif", "sandOver.gif", "sandDown.gif"); createClipButton(5, "~ 35 sec @ 56k", "vortexBase.gif", "vortexOver.gif", "vortexDown.gif"); createClipButton(6, "~ 60 sec @ 56k", "orangewhiteBase.gif", "orangewhiteOver.gif", "orangewhiteDown.gif"); createClipButton(7, "< 1 sec", "startupBase.gif", "startupOver.gif", "startupDown.gif"); BImage b; BImage r; BImage d; int x; int y; int w; int h; Rectangle bounds; // help button b = loadImage("helpBase.gif"); r = loadImage("helpOver.gif"); d = loadImage("helpDown.gif"); x = 410; y = 0; w = b.width; h = b.height; buttons[HELP_BUTTON] = new ImageButton(x, y, w, h, b, r, d); buttons[HELP_BUTTON].opacity = 0; // help panel BImage helpPanel = loadImage("helpPanel.gif"); BImage helpMask = loadImage("helpPanelMask.gif"); helpPanel.alpha(helpMask.pixels); b = helpPanel; r = helpPanel; d = helpPanel; x = x + w - 12 - b.width; y = y + h; w = b.width; h = b.height; buttons[HELP_PANEL] = new ImageButton(x, y, w, h, b, r, d); buttons[HELP_PANEL].visible = false; buttons[HELP_PANEL].opacity = 1.0; // more button BImage more = loadImage("more.gif"); BImage moreOver = loadImage("moreOver.gif"); BImage moreMask = loadImage("moreMask.gif"); more.alpha(moreMask.pixels); moreOver.alpha(moreMask.pixels); b = more; r = moreOver; d = moreOver; bounds = buttons[HELP_PANEL].bounds; x = bounds.x + bounds.width - 60; y = bounds.y + bounds.height - b.height - 2; w = b.width; h = b.height; buttons[MORE_BUTTON] = new ImageButton(x, y, w, h, b, r, d); buttons[MORE_BUTTON].visible = false; } ImageButton createClipButton(int buttonNum, String rolloverText, String base, String over, String down) { BImage b = loadImage(base); BImage r = loadImage(over); BImage d = loadImage(down); clipButtonImages[buttonNum][0] = b; clipButtonImages[buttonNum][1] = r; clipButtonImages[buttonNum][2] = d; int x = width - 2 - b.width; int y = 10 + buttonNum * 35; ImageButton newButton = new ImageButton(x, y, b.width, b.height, b, r, d); newButton.rolloverText = rolloverText; newButton.enabled = false; // untill fully faded in newButton.opacity = 0; buttons[buttonNum] = newButton; return newButton; } void update() { if (frameCounter <= fadeEnd) { // haven't reached fade out point if (frameCounter >= fadeStart) { // but we are past fade in point int fadeStartSpan = fadeDuration - buttonFadeFrames; for (int i = 0; i < buttons.length; i++) { ImageButton b = buttons[i]; if (b.opacity < 1.0) { // button not yet fully faded in int buttonFadeStart = fadeStart + ((i * fadeStartSpan) / buttons.length); float opacity = (frameCounter - buttonFadeStart) / float(buttonFadeFrames); buttons[i].opacity = constrain(opacity, 0.0, 1.0); if (opacity > 0.9) { // enable fully visible buttons buttons[i].enabled = true; } } } } } if (activeButton >= 0) { // effectively disable the other buttons by only updating the active button buttons[activeButton].update(); } else { // update mouse status for each button for (int i = 0; i < buttons.length; i++) { buttons[i].update(); } } } void updateImages() { for (int i = 0; i <= STARTUP_CLIP; i++) { ImageButton b = buttons[i]; if (videoClips[i] == null) { // clip not loaded ==> give each state a different image b.base = clipButtonImages[i][0]; b.roll = clipButtonImages[i][1]; b.down = clipButtonImages[i][2]; } else { // clip loaded ==> all states show the blocky "down" image b.base = clipButtonImages[i][2]; b.roll = clipButtonImages[i][2]; b.down = clipButtonImages[i][2]; } } } // draws all the buttons in the set void draw() { for (int i = 0; i < buttons.length; i++) { buttons[i].draw(); } } boolean handleMousePress(int x, int y) { activeButton = buttonset.buttonAtPoint(mouseX, mouseY); return activeButton >= 0; } boolean handleMouseRelease(int x, int y) { int mouseDownButton = activeButton; activeButton = -1; int b = buttonAtPoint(x, y); if (b < 0) return false; if (b != mouseDownButton) return false; if (!buttonset.buttons[b].enabled) return true; switch (b) { case HELP_BUTTON: setHelpPanelVisible(!buttons[HELP_PANEL].visible); break; case HELP_PANEL: setHelpPanelVisible(false); break; case MORE_BUTTON: link("http://lightmoves.net/videostreamer/", "_new"); break; default: loadClip(b); } return true; } // Note: we walk the list of buttons from last to first so that // the "nearest" button gets first chance for a hit int buttonAtPoint(int x, int y) { for (int i = buttons.length - 1; i >= 0; i--) { if (buttons[i].visible && buttons[i].bounds.contains(x, y)) { return i; } } return -1; } void setHelpPanelVisible(boolean b) { buttonset.buttons[ButtonSet.HELP_PANEL].visible = b; buttonset.buttons[ButtonSet.MORE_BUTTON].visible = b; } } // end ButtonSet //===================================================================================