Sampled Dynamic Visual Content
An Introduction with Examples in Java |
Prof. David Bernstein |
Computer Science Department |
bernstdh@jmu.edu |
An Illustration
SimpleContent
objects over time
SimpleContent
objects
SimpleContent
objectsVisualization
object
to manage the collection of SimpleContent
objects
Visualization
and
VisualizationView
classes
Visualization
and
VisualizationView
classes
Visualization
and
VisualizationView
classes
SimpleContent
object's render()
method
Controlling the Timing of the Rendering Process
Outline of a Screen
Class
package visual.dynamic.sampled; import java.awt.*; import java.awt.event.*; import javax.swing.*; import java.util.*; import collectionframework.*; import event.*; import visual.*; import visual.statik.SimpleContent; /** * A component that renders a sequence of Content objects * (i.e., static visual content) * * @author Prof. David Bernstein, James Madison University * @version 1.0 */ public class Screen extends Visualization implements MetronomeListener { private boolean repeating; private int frameNumber, height, width; private Iterator<SimpleContent> frames; protected Metronome metronome; protected SimpleContent currentFrame; public static final int FRAME_DELAY = 42; /** * Default Constructor */ public Screen() { super(); metronome = new Metronome(FRAME_DELAY); metronome.addListener(this); setRepeating(false); } /** * Create the default view associated with this Visualization * * Note: This method should only be called by constructors. * It should be overridden by derived classes that * need to use a specialized view */ protected VisualizationView createDefaultView() { ScreenRenderer renderer; renderer = new ScreenRenderer(new PlainVisualizationRenderer()); return new VisualizationView(this, renderer); } /** * Get the numebr of the current frame * * @return The number of the current frame */ public int getFrameNumber() { return frameNumber; } /** * Set the frame rate (i.e., the number of frames per second) * * @param frameRate */ public void setFrameRate(double frameRate) { int delay; delay = (int)(1000.0 / frameRate); metronome.setDelay(delay); } /** * Set whether this presentation should repeat/loop * * @param repeating true to repeat/loop; false otherwise */ public void setRepeating(boolean repeating) { this.repeating = repeating; } /** * Start the presentation */ public void start() { reset(); if (frames != null) metronome.start(); } /** * Stop the presentation * */ public void stop() { metronome.stop(); } }
Handling Metronome
Ticks
/** * Handle tick events (required by MetronomeListener) * * Specifically, make the current frame the "current" Content * and call repaint() to start the rendering process. * * @param evt The ActionEvent */ public void handleTick(int time) { if (frames != null) { // See if we're done if (frameNumber < 0) { if (repeating) reset(); else stop(); } // Start the rendering process (i.e., request that the // paint() method be called) repaint(); // Advance the frame advanceFrame(); } }
/** * Advance to the next frame */ private void advanceFrame() { if ((frames != null) && (frames.hasNext())) { currentFrame = frames.next(); frameNumber++; } else { currentFrame = null; frameNumber = -1; } }
Modifying Behavior in the Parent Class
protected NullIterator<SimpleContent> currentFrameIterator; currentFrameIterator = new NullIterator<SimpleContent>(); /** * Get an Iterator that contains the current SimpleContent * object * * @return The Iterator (containing 0 or 1 elements) */ public Iterator<SimpleContent> iterator() { currentFrameIterator.setElement(currentFrame); if (frameNumber < 0) currentFrameIterator.clear(); return currentFrameIterator; } /** * Get an Iterator that contains either all of the SimpleContent * objects or the current SimpleContent Object * * @param all true to get all frames; false to get the current frame * @return The SimpleContent objects */ public Iterator<SimpleContent> iterator(boolean all) { Iterator<SimpleContent> result; if (all) result = super.iterator(); else result = iterator(); return result; }
The Rendering Process
// Attributes used for double-buffering protected boolean useDoubleBuffering; protected Graphics2D bg; protected Image offscreenImage; protected int height, width;
/** * Create the off-screen buffer if necessary * (i.e., if the size has changed) * * @return The rendering engine for the off-screen buffer */ private Graphics2D createOffscreenBuffer() { Dimension d; d = getSize(); if ((d.height != height) || (d.width != width)) { height = d.height; width = d.width; offscreenImage = createImage(width, height); bg = (Graphics2D)offscreenImage.getGraphics(); bg.setClip(0,0,width,height); } return bg; }
/** * Paint (i.e., render) this JComponent * * @param g The rendering engine to use */ public void paint(Graphics g) { Graphics2D bg; if (useDoubleBuffering) { bg = createOffscreenBuffer(); } else { bg = (Graphics2D)g; } if (bg != null) { // Perform necessary operations before rendering preRendering(bg); // Render the visual content render(bg); // Perform necessary operations after rendering postRendering(bg); if (useDoubleBuffering) { // Put the offscreen image on the screen g.drawImage(offscreenImage, 0, 0, null); // Reset the clipping area bg.setClip(0,0,width,height); } } }
Fade
Class/** * Set the destination (in the Porter-Duff sense) pixels * to be used in alpha blending * * @param g The rendering engine */ protected void setDestinationPixels(Graphics g) { Graphics2D g2; Rectangle r; g2 = (Graphics2D)g; r = g2.getClipBounds(); g2.setComposite(AlphaComposite.Src); g2.setColor(g2.getBackground()); g2.fill(r); g2.draw(r); }
Pre-Rendering Phase
g2 = (Graphics2D)g; originalComposite = g2.getComposite(); alpha = ((float)(frame - first + 1))/(float)duration; if (direction == FADE_OUT) alpha = 1.0f - alpha; if (alpha > 1.0f) alpha = 1.0f; else if (alpha < 0.0f) alpha = 0.0f; setDestinationPixels(g2); ac = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha); g2.setComposite(ac);
Post-Rendering Phase
g2 = (Graphics2D)g; if (originalComposite != null) g2.setComposite(originalComposite);
Disolve
Classpackage visual.dynamic.sampled; import java.awt.*; /** * A transition that fades-out the previous frame and * fades-in the new one * * @author Prof. David Bernstein, James Madison University * @version 1.0 */ public class Dissolve extends Fade { /** * Explicit Value Constructor * * @param first The first frame * @param duration The duration (in frames) */ public Dissolve(int first, int duration) { super(FADE_IN, first, duration); } /** * Set the destination (in the Porter-Duff sense) pixels * to be used in alpha blending * * @param g The rendering engine */ protected void setDestinationPixels(Graphics g) { // Use the last frame } }
RectangleWipe
Class/** * Calculate the size of the clip rectangle * * @param width The width of the image * @param height The height of the image * @param int The frame of the wipe */ protected Rectangle2D calculateClip(float width, float height, int frame) { float h, w, x, y; Rectangle2D clip; w = scale*frame*width; h = scale*frame*height; x = width/2.0f - w/2.0f; y = height/2.0f - h/2.0f; clip = new Rectangle2D.Float(x, y, w, h); return clip; }
Pre-Rendering Phase
g2 = (Graphics2D)g; originalClip = g2.getClip(); bounds = g2.getClipBounds(); height = (float)(bounds.getHeight()); width = (float)(bounds.getWidth()); clip = calculateClip(width, height, frame-first+1); g2.setClip(clip);
Post-Rendering Phase
g2 = (Graphics2D)g; if (originalClip != null) g2.setClip(originalClip);
LineWipe
Class/** * Calculate the size of the clip rectangle * * @param width The width of the image * @param height The height of the image * @param frame The frame of the wipe */ protected Rectangle2D calculateClip(float width, float height, int frame) { float h, w, x, y; Rectangle2D clip; w = width; h = height; x = 0.0f; y = 0.0f; if (direction == RIGHT) { w = scale*frame*width; h = height; x = 0.0f; y = 0.0f; } else if (direction == LEFT) { w = scale*frame*width; h = height; x = width - w; y = 0.0f; } else if (direction == UP) { w = width; h = scale*frame*height; x = 0.0f; y = height - h; } else { w = width; h = scale*frame*height; x = 0.0f; y = 0.0f; } clip = new Rectangle2D.Float(x, y, w, h); return clip; }
AbstractSuperimposition
Classpackage visual.dynamic.sampled; import java.awt.*; import java.awt.geom.*; import javax.swing.*; /** * An abstract implementation of the Superimposition interface. * * @author Prof. David Bernstein, James Madison University * @version 1.0 */ public abstract class AbstractSuperimposition extends AbstractFrameOp implements Superimposition { private int position; /** * Explicit Value Constructor * * @param first The first frame * @param duration The duration (in frames) * @param position The position */ public AbstractSuperimposition(int first, int duration, int position) { super(first, duration); this.position = SwingConstants.SOUTH_EAST; if ((position == SwingConstants.NORTH) || (position == SwingConstants.NORTH_EAST) || (position == SwingConstants.EAST) || (position == SwingConstants.SOUTH_EAST) || (position == SwingConstants.SOUTH) || (position == SwingConstants.SOUTH_WEST) || (position == SwingConstants.WEST) || (position == SwingConstants.NORTH_WEST) || (position == SwingConstants.CENTER ) ) { this.position = position; } } /** * Calculate the registration point for the superimposition * based on its size, the frame size, and the desired position * * @param frameWidth The width of the frame * @param frameHeight The height of the frame * @param siWidth The width of the superimposition * @param siHeight The height of the superimposition */ protected Point2D calculateRegistrationPoint(double frameWidth, double frameHeight, double siWidth, double siHeight) { double left, top; top = 0.0; left = 0.0; if (position == SwingConstants.NORTH) { top = siHeight; left = frameWidth/2.0 - siWidth/2.0; } else if (position == SwingConstants.NORTH_EAST) { top = siHeight; left = frameWidth - siWidth - 1; } else if (position == SwingConstants.EAST) { top = frameHeight/2.0 - siHeight/2.0; left = frameWidth - siWidth - 1; } else if (position == SwingConstants.SOUTH_EAST) { top = frameHeight - siHeight - 1; left = frameWidth - siWidth - 1; } else if (position == SwingConstants.SOUTH) { top = frameHeight - siHeight - 1; left = frameWidth/2.0 - siWidth/2.0; } else if (position == SwingConstants.SOUTH_WEST) { top = frameHeight - siHeight - 1; left = 0.0; } else if (position == SwingConstants.WEST) { top = frameHeight/2.0 - siHeight/2.0; left = 0.0; } else if (position == SwingConstants.NORTH_WEST) { top = siHeight; left = 0.0; } else if (position == SwingConstants.CENTER) { top = frameHeight/2.0 - siHeight/2.0; left = frameWidth/2.0 - siWidth/2.0; } return new Point2D.Double(left, top); } /** * Get the position * * Possible values: javax.SwingConstants.NORTH, NORTH_EAST, * EAST, SOUTH_EAST, SOUTH, SOUTH_WEST, WEST, NORTH_WEST, or CENTER) * * @return The position */ public int getPosition() { return position; } /** * Apply the post-rendering portion of * this Superimposition * * @param g The rendering engine * @param frame The current frame number */ public abstract void postRendering(Graphics g, int frame); /** * Apply the pre-rendering portion of * this Superimposition * * @param g The rendering engine * @param frame The current frame number */ public abstract void preRendering(Graphics g, int frame); }
TransformableContentSuperimposition
Class/** * Explicit Value Constructor * * @param content The visual content to use * @param first The first frame * @param duration The duration (in frames) * @param position The position (SwingConstants.NORTH, ...) */ public TransformableContentSuperimposition( TransformableContent content, int first, int duration, int position) { super(first, duration, position); this.content = content; }
g2 = (Graphics2D)g; // Transform the TransformableContent so that // it is positioned properly frameBounds = g2.getClipBounds(); frameWidth = (double)frameBounds.width; frameHeight = (double)frameBounds.height; contentBounds = content.getBounds2D(false); contentWidth = contentBounds.getWidth(); contentHeight = contentBounds.getHeight(); rp = calculateRegistrationPoint(frameWidth, frameHeight, contentWidth, contentHeight); content.setLocation(rp.getX(), rp.getY()); content.render(g);
package visual.dynamic.sampled; import java.awt.Graphics; /** * The requirements of all operations on frames of sampled dyanmic * content (individual and multiple-frame operations) * * @author Prof. David Bernstein, James Madison University * @version 1.0 */ public interface FrameOp { /** * Get the index of the first frame of this FrameOp * * @return The index of the first frame */ public abstract int getFirstFrame(); /** * Get the index of the last frame of this FrameOp * * @return The index of the last frame */ public abstract int getLastFrame(); /** * Apply the post-rendering portion of * this Transition * * @param g The rendering engine * @param frame The current frame number */ public abstract void postRendering(Graphics g, int frame); /** * Apply the pre-rendering portion of * this Transition * * @param g The rendering engine * @param frame The current frame number */ public abstract void preRendering(Graphics g, int frame); }
package visual.dynamic.sampled; import java.awt.Graphics; /* * An abstract implementation of the FrameOp interface. * * @author Prof. David Bernstein, James Madison University * @version 1.0 */ public abstract class AbstractFrameOp implements FrameOp { protected int duration, first; /** * Explicit Value Constructor * * @param first The first frame * @param duration The duration (in frames) */ public AbstractFrameOp(int first, int duration) { this.first = first; this.duration = 0; if (duration > 0) this.duration = duration; } /** * Get the index of the first frame of this FrameOp * * @return The index of the first frame */ public int getFirstFrame() { return first; } /** * Get the index of the last frame of this FrameOp * * @return The index of the last frame */ public int getLastFrame() { return first+duration; } /** * Has this FrameOp finished at the given frame? * * @return true if finished; false otherwise */ protected boolean hasFinishedAt(int frame) { return (frame >= (first+duration-1)); } /** * Apply the post-rendering portion of * this FrameOp * * @param g The rendering engine * @param frame The current frame number */ public abstract void postRendering(Graphics g, int frame); /** * Apply the pre-rendering portion of * this FrameOp * * @param g The rendering engine * @param frame The current frame number */ public abstract void preRendering(Graphics g, int frame); /** * Should this FrameOp be applied at the given frame * * @return true if yes; false otherwise */ protected boolean shouldApplyAt(int frame) { return ((frame >= first) && (frame <= (first+duration-1))); } }
Screen
Class// Attributes used for transitions private IntervalIndexedCollection<Transition> transitions;
// Attributes used for superimpositions private IntervalIndexedCollection<Superimposition> superimpositions;
/** * Add a Transition * * Note: This method does not ensure that * the order is correct * * @param t The Transition to add */ public void addTransition(Transition t) { transitions.add(t, t.getFirstFrame(), t.getLastFrame()); }
/** * Get the current transitions (if any) * * @return The current transitions */ public Iterator<Transition> getTransitions() { Iterator<Transition> result; result = null; if (frameNumber >= 0) result = transitions.iterator(frameNumber); return result; }
/** * Add a Superimposition * * Note: This method does not ensure that * the order is correct * * @param si The Superimposition to add */ public void addSuperimposition(Superimposition si) { superimpositions.add(si, si.getFirstFrame(), si.getLastFrame()); }
/** * Get the current superimpositions (if any) * * @return The current superimpositions */ public Iterator<Superimposition> getSuperimpositions() { Iterator<Superimposition> result; result = null; if (frameNumber >= 0) result = superimpositions.iterator(frameNumber); return result; }
ScreenRenderer
Class/** * Operations to perform before rendering * * @param g The rendering engine * @param model The Visualization containing the content * @param view The component presenting the content */ public void preRendering(Graphics g, Visualization model, VisualizationView view) { Graphics2D g2; int frameNumber; Screen smodel; Iterator<Transition> transitions; Iterator<Superimposition> superimpositions; g2 = (Graphics2D)g; oldComposite = g2.getComposite(); view.setDoubleBuffered(true); // Get information from the model smodel = (Screen)model; transitions = smodel.getTransitions(); superimpositions = smodel.getSuperimpositions(); frameNumber = smodel.getFrameNumber(); // Apply the transitions if (transitions != null) { while (transitions.hasNext()) { transitions.next().preRendering(g, frameNumber); } } // Apply the superimpositions if (superimpositions != null) { while (superimpositions.hasNext()) { superimpositions.next().preRendering(g, frameNumber); } } }
/** * Operations to perform after rendering * * @param g The rendering engine * @param model The Visualization containing the content * @param view The component presenting the content */ public void postRendering(Graphics g, Visualization model, VisualizationView view) { Graphics2D g2; int frameNumber; Screen smodel; Iterator<Transition> transitions; Iterator<Superimposition> superimpositions; g2 = (Graphics2D)g; g2.setComposite(oldComposite); view.setDoubleBuffered(true); // Get information from the model smodel = (Screen)model; frameNumber = smodel.getFrameNumber(); transitions = smodel.getTransitions(); superimpositions = smodel.getSuperimpositions(); // Apply the transitions if (transitions != null) { while (transitions.hasNext()) { transitions.next().postRendering(g, frameNumber); } } // Apply the superimpositions if (superimpositions != null) { while (superimpositions.hasNext()) { superimpositions.next().postRendering(g, frameNumber); } } }
/** * Render the content contained in the model. * * * @param g The rendering engine * @param model The Visualization containing the content * @param view The component presenting the content */ public void render(Graphics g, Visualization model, VisualizationView view) { decorated.render(g, model, view); }