/* Juggle.java A site-swap animator applet * This applet animates any pattern in a 'vanilla' site-swap. For * information on what site-swaps are, see * http://www.juggling.org/help/siteswap/ * Author: Alan Nichols (alan.nichols@sun.com) * Date this version: 1/29/96 * Copyright: Alan Nichols, 1996. * This applet is deeply in debt to the people who brought you xjuggle. * This applet is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 1. * This applet is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ import java.awt.*; import java.awt.image.*; import java.net.*; import java.util.*; import java.applet.Applet; /* PatternException class * * This class just exists so that the pattern class can throw its own * unique exceptions. */ class PatternException extends Exception{ private String message; // The error message for this exception /* PatternException constructor * * Takes an error message string and hangs on to it. */ public PatternException(String str){ message = str; } /* toString method * * Returns the stored error message */ public String toString(){ return message; } } /* Pattern class * * This class stores a juggling pattern in site-swap notation. * * The constructor is overloaded such that you can call it with no pattern, * in which case the default patter of '3' is used, call it with a pattern, * or call it with a pattern and a startup pattern. If it is called with * a pattern or a startup pattern and there is a problem with the pattern, * it will throw a PatternException. Once the pattern is created, you * can find the next throw in the pattern by calling the NextThrow method. * * The startup pattern is stored in the reverse order that the throws are * made. IE: if your pattern is '5 1' the startup pattern would be '5 2' * and stored as '2 5'. If there is a startup pattern, tick is initalized * to the negative of the length of the startup pattern, and used to * walk down the startup pattern. */ class Pattern { private int startup[] = {}; // In reverse of sequence of throws. private int pthrows[] = {3}; // Pattern itself. private int tick = 0; // Where we are in the pattern. private int balls = 3; // How many balls are in this pattern /* StrToPat method * * This takes a pattern string and returns the corresponding array of * integers. Will throw PatternException if there is an error in parsing * the pattern, or if there are negative numbers in the pattern. */ private int[] StrToPat(String str) throws PatternException { int index, retval[]; StringTokenizer st = new StringTokenizer(str); retval = new int[st.countTokens()]; for (index = 0; index < retval.length; index++) { String tmpstr = st.nextToken(); try { retval[index] = (Integer.valueOf(tmpstr).intValue()); } catch (NumberFormatException e) { throw new PatternException("Pattern invalid: " + tmpstr + " is not a valid integer"); } if (retval[index] < 0) { throw new PatternException("Pattern invalid: Negitave throw " + tmpstr + " not allowed"); } } return retval; } /* ValidatePattern method * * Takes a pattern array and checks to see if it is a vaild pattern. * Throws PatternException if the pattern fails any checks. * * Checks made: * 1) Sum of throws is evenly divisible by pattern length. * 2) No more than one ball is caught at a time. * 3) No catch is made on a throw of 0. * 4) All throws are fed a ball to throw. * * Returns the number of balls in the pattern. */ private int ValidatePattern(int pat[]) throws PatternException{ int temp, index, len; boolean catches[]; len = pat.length; catches = new boolean[len]; for (temp = 0, index = 0; index < len; index++) { temp += pat[index]; catches[index] = false; } if ((temp % len) != 0) { throw new PatternException("Total of throws " + temp + " not divisible by number of throws " + len); } for (index = 0; index < len; index++){ int endpos = (index + pat[index])%len; if (catches[endpos]) { if (pat[index] == 0){ throw new PatternException("Ball caught at position " + endpos + " when hand should be empty"); } else { throw new PatternException("Two balls caught at position " + endpos); } } catches[endpos] = true; } for (index = 0; index < len; index++){ if (!catches[index]) { throw new PatternException("No catch lands in position " + index); } } return temp / len; } /* Pattern default constructor * * All the variables should be set to reasonable defaults, so currently * does nothing. Could do something here to guard against unreasonable * defaults from being compiled in. */ public Pattern() { } /* Pattern constructor taking a pattern and a startup pattern * * Takes two strings describing a pattern and a startup pattern. * Parses and validates them. Throws PatternException if there * is a problem with the pattern and startup. Relies on ValidatePattern * and StrToPat to do most of the work, just checks to see if the * pattern can successfully start. */ public Pattern(String start, String startupstr) throws PatternException{ int temp, index, tmpstartup[]; pthrows = StrToPat(start); tmpstartup = StrToPat(startupstr); startup = new int[tmpstartup.length]; tick = -startup.length; balls = ValidatePattern(pthrows); /* StrToPat returns the startup pattern in the order the throws are * executed. We need to reverse the order here so that NextThrow * works. */ for (index = 0; index < tmpstartup.length; index++) { startup[index] = tmpstartup[(tmpstartup.length - index)- 1]; } for (index = 0, temp = balls; index < balls; index++, temp--){ if ((index < tmpstartup.length) ? (tmpstartup[index] < temp) : (pthrows[(index%pthrows.length)] < temp)) { throw new PatternException("A ball lands when balls are not yet airborne"); } } } /* Pattern constructor taking a pattern * * Takes a string describing a pattern, parses and validates it. * Throws PatternException if there is a problem with the pattern. * Relies on ValidatePattern and StrToPat to do most of the work, * just checks to see if the pattern can successfully start. */ public Pattern(String start) throws PatternException{ int temp, index; pthrows = StrToPat(start); balls = ValidatePattern(pthrows); for (index = 0, temp = balls; index < balls; index++, temp--){ if (pthrows[(index%pthrows.length)] < temp) { throw new PatternException(String.valueOf("A ball lands when balls are not yet airborne")); } } } /* NextThrow method * * Returns the next throw in the pattern. Uses tick as the index into * the pattern array for the next throw. If tick is negative, uses the * absolute value as the index into the startup array */ public int NextThrow(){ if (tick < 0) { return startup[Math.abs(++tick)]; } else { tick %= pthrows.length; return pthrows[tick++]; } } /* getPattern method * * Returns a string describing the pattern */ public String getPattern() { String retval = ""; int index; for(index = 0; index < pthrows.length; index ++){ retval = retval + String.valueOf(pthrows[index])+ " "; } return retval; } /* getStartup method * * Returns a string describing the startup pattern */ public String getStartup() { String retval = ""; int index; for(index = (startup.length - 1); index >= 0; index--){ retval = retval + String.valueOf(startup[index])+ " "; } return retval; } /* getBalls method * * returns the number of balls in this pattern */ public int getBalls(){ return balls; } public String toString(){ return "Pattern: " + getPattern() + "Startup: " + getStartup() + "Balls: " + balls + " Tick: " + tick; } } /* jParams class * * Initalizes and stores a number of global parameters for the juggling * animation. All the parameters stored here should be tweakable. */ class jParams { int detail = 10; // updates between throw and catch int scoop = 10; // offset between throw and catch point int gravity = 100; // artifical gravity constant int speed = 20; // Speed of the updates int hands = 2; // Number of hands String pattern = "3"; // Pattern to animate String startup = null;// Startup pattern boolean onAcid; // Are we on acid? boolean running; // is the animation running? /* jParams constructor * * needs an applet to be able to get at the applet parameters. */ public jParams(Applet app){ String str = app.getParameter("speed"); speed = (str!= null)?Integer.valueOf(str).intValue() : speed; str = app.getParameter("detail"); detail = (str!= null)?Integer.valueOf(str).intValue() : detail; str = app.getParameter("scoop"); scoop = (str!= null)?Integer.valueOf(str).intValue() : scoop; str = app.getParameter("gravity"); gravity = (str!= null)?Integer.valueOf(str).intValue() : gravity; str = app.getParameter("hands"); hands = (str!= null)?Integer.valueOf(str).intValue() : hands; str = app.getParameter("pattern"); pattern = (str!= null)? str : pattern; str = app.getParameter("startup"); startup = (str!= null)? str : startup; } } /* jHand class * * Stores the position of the hand, as well as the throw and catch points * for the hand. */ class jHand { int x; // X coridnate of the hand. int y; // Y coridnate of the hand. int tx; // X coridnate of the point throws are made from int cx; // X coridnate of the point catches are made int id; // where this hand is in the hands array int scoop; // offset between the X cordinate of the hand and the throw // and catch points /* jHand constructor * * Takes the starting x y coridnates, id and scoop and sets up the * variables accordingly. */ public jHand(int startx, int starty, int sid, int newscoop) { x = startx; y = starty; scoop = newscoop; tx = x + ((((sid%2) == 0) ? 1 : -1) * scoop); cx = x + ((((sid%2) == 0) ? -1 : 1) * scoop); id = sid; } } /* jBall class * * Stores all the information for a paticular ball, updates the position of * the ball, and draws the ball into the animation frame. */ class jBall { private int tick; // number of updates since the last beat private int beat; // which beat of the throw we are in private Color color; // Color of the ball private int curthrow; // height of the current throw private jHand from, to; // to know where the ball is going private jHand hands[]; // used to find where the ball goes next private Pattern pat; // used to find the height of the next throw private int id; // which ball this is private Graphics buf; // tablet to draw the ball on private Point curpos; // point the ball is at private jParams params; // global juggling parameters private boolean inAir; // flag to suppress flight /* jBall constructor * * Currently needs a whole slew of stuff to set up the ball. * Takes the array of hands, the pattern (to find the next throw) * the graphics context to draw itself in, the position of the ball * in the array of balls (also tells it how long to wait before * launching itself into the air), which hand the ball starts in, * and the global parameters. * * Mostly just hangs onto all this for future use. Randomly * sets the color of the ball. */ public jBall (jHand allhands[], Pattern newpat, Graphics g, int pause, int starthand, jParams newparams) { tick = 0; beat = -pause; id = pause; hands = allhands; from = hands[starthand]; buf = g; pat = newpat; params = newparams; while ((curthrow = pat.NextThrow()) <= 0){}; to = hands[(curthrow + from.id) % hands.length]; inAir = (curthrow != params.hands); color = new Color((float)Math.random(), (float)Math.random(), (float)Math.random()); } /* Move method * * Tells the ball to update its position and redraw itself, */ public void Move() { tick++; /* tick should vary between 0 and params.detail. If it * falls below that range, reset to 0 (could be an error) * if it goes over detail, drop tick to 0 and bump up beat. */ if (tick <= 0) { tick = 0; } if (tick > params.detail) { tick = 0; beat++; if ( beat == curthrow) { /* time to go to the next throw. */ from = to; while ((curthrow = pat.NextThrow()) <= 0){}; to = hands[(curthrow + from.id) % hands.length]; if (curthrow < 0) { beat = curthrow; } else { beat = 0; } inAir = (curthrow != params.hands); } } /* draw the ball. */ buf.setColor(color); curpos = normalPos(); buf.fillOval(curpos.x, curpos.y, 10, 10); } /* normalPos method * * Returns the ball's current location. If the ball is not * in the air, the location is the current hand's throw point. * if the ball is in the air, the point is computed depending * on a variety of factors: * start point: from.tx and from.y * end point: to.cx and to.y * gravity: params.gravity * detail: params.detail * curthrow: height of current throw. */ private Point normalPos() { if ((beat < 0) || (curthrow == 0) || (!inAir)) return new Point(from.tx, from.y); int x = from.tx + (int)(((float)(to.cx - from.tx)/ (float)(params.detail * curthrow)) * (float)(tick + (params.detail * beat))); int y = from.y + parabola(params.gravity*curthrow*curthrow, Math.abs(from.y - to.y), tick + (params.detail * beat), params.detail * curthrow ); return new Point(x,y); } /* parabola method * * Used to compute the ball's height at any given point of the throw. * parameters are used as follows: * gravity: Artifical gravity constant, to fiddle with height * y: height at time TotalTicks * ticks: point on path * TotalTicks: Length of path */ private int parabola(int gravity, int y, int ticks, int TotalTicks) { return (((ticks * ((2 * y - gravity) + (gravity * ticks) / TotalTicks)) / 2) / TotalTicks ); } } /* jAnim class * * Class to run the animation thread. Sets up all the other objects necessary * to do the animation (the pattern, the hands and the balls) and runs in * a loop telling the balls to update their positions and redraw themselves. */ class jAnim implements Runnable { /* Juggling objects */ jBall balls[]; // an array of the balls jHand hands[]; // array of the hands Pattern pat; // Pattern that we are animating /* Animation parameters */ jParams params; // global parameters Graphics offscreen; // offscreen graphics context for double-buffering Applet app; // reference to applet -- for getting parameters int height, width; // how big a canvas we have to deal with /* jAnim constructor * * Takes the height and width of the space to draw the balls on, the * juggling parameters object, a graphic content to draw onto, and * an applet from which to read the parameters. */ public jAnim(int newwidth, int newheight, jParams newparams, Graphics newoffscreen, Applet newapp){ int index; width = newwidth; height = newheight; params = newparams; offscreen = newoffscreen; app = newapp; /* Set up the pattern */ if (params.pattern != null) { if (params.startup != null) { try { pat = new Pattern(params.pattern, params.startup); } catch (PatternException e){ app.showStatus(e.toString()); pat = new Pattern(); } } else { try { pat = new Pattern(params.pattern); } catch (PatternException e){ app.showStatus(e.toString()); pat = new Pattern(); } } } else { pat = new Pattern(); } /* The pattern is set up, set the global string to reflect what * actually got set up. */ params.pattern = pat.getPattern(); params.startup = pat.getStartup(); /* Set up the hands */ hands = new jHand[params.hands]; for (index = 0; index < hands.length; index++) { hands[index] = new jHand((index*((width - 50)/(params.hands - 1)))+25, (height - 20), index, params.scoop); } /* Set up the balls */ balls = new jBall[pat.getBalls()]; for (index = 0; index < pat.getBalls(); index++) { balls[index] = new jBall (hands, pat, offscreen, index, index%hands.length, params); } } /* run method * * Required for the 'Runnable' interface. Here is where the work of * the applet is carried out. Loops until told not to (params.running) * redrawing the drawing canvas (if not on acid) and the balls locations * (by telling them to move and redraw themselves). Tells the app to * repaint itself. */ public void run() { int index; params.running = true; Thread.currentThread().setPriority(Thread.MIN_PRIORITY); while (params.running) { if (!params.onAcid) { offscreen.setColor(Color.white); offscreen.fillRect(0,0, width, height); } for (index = 0; index < pat.getBalls(); index++) { (balls[index]).Move(); } app.repaint(); try { Thread.sleep(params.speed); } catch (InterruptedException e){ } } } } /* Juggle class * * Applet to load and run a juggling animation. Sets up the controls and * the image for the animation, handles the events from the controls to * change the animation parameters, and stops and restarts itself when * a new pattern is tried. */ public class Juggle extends java.applet.Applet { jAnim anim; // Animation object jParams params; // Animation parameters Thread kicker = null; // Thread to run the animation in /* Graphics objects */ Image buf; // A buffer to draw the animation on Panel doodle; // Panel to draw the animation in Panel controls; // Panel to draw the controls in Checkbox acidCheck; // To toggle onAcid /* Controls components to fiddle with the value of detail */ Label detailLabel; Scrollbar detailBar; Label detailValue; /* Controls components to fiddle with the value of gravity */ Label gravityLabel; Scrollbar gravityBar; Label gravityValue; /* Controls components to fiddle with the value of speed */ Label speedLabel; Scrollbar speedBar; Label speedValue; /* Labels to show the current number of hands and balls */ Label numHands, numBalls; /* Controls components to display and set the pattern and startup */ Label PatternLabel, StartupLabel; TextField PatternBox, StartupBox; Button PatternDoIt; /* method init * * overrides Applet.init(). Run when the applet is loaded to initalize * the applet and the controls it uses. Fairly straight forward -- * creates the parameters, creates all the window controls and lays * them out in the controls panel. Sets the layout manager for the * applet to BorderLayout, using the south section for the controls * and whatever space is left in the center section for the animation. */ public void init() { int index; /* Pull the relevant parameters from the properties */ params = new jParams(this); /* Set up the graphic components for the controls */ acidCheck = new Checkbox("On Acid?"); params.onAcid = acidCheck.getState(); gravityBar = new Scrollbar(Scrollbar.HORIZONTAL, params.gravity, 1, 10, 200); gravityValue = new Label(Integer.toString(params.gravity)); gravityLabel = new Label("Gravity: 10 - 200"); detailBar = new Scrollbar(Scrollbar.HORIZONTAL, params.detail, 1, 5, 20); detailValue = new Label(Integer.toString(params.detail)); detailLabel = new Label("Detail: 5 - 20"); speedBar = new Scrollbar(Scrollbar.HORIZONTAL, params.speed, 1, 10, 200); speedValue = new Label(Integer.toString(params.speed)); speedLabel = new Label("Speed: 10 - 200"); numHands = new Label("Hands: "); numBalls = new Label("Balls: "); PatternLabel = new Label("Pattern: "); PatternBox = new TextField(); StartupLabel = new Label("Startup: "); StartupBox = new TextField(); PatternDoIt = new Button("Try Pattern"); /* Set up the controls panel */ controls = new Panel(); controls.setLayout(new GridLayout(6,3,5,0)); controls.add(gravityLabel); controls.add(gravityBar); controls.add(gravityValue); controls.add(detailLabel); controls.add(detailBar); controls.add(detailValue); controls.add(speedLabel); controls.add(speedBar); controls.add(speedValue); controls.add(acidCheck); controls.add(numHands); controls.add(numBalls); controls.add(PatternLabel); controls.add(PatternBox); controls.add(PatternDoIt); controls.add(StartupLabel); controls.add(StartupBox); /* Create the animation panel */ doodle = new Panel(); /* Add this stuff to the applet */ setLayout(new BorderLayout()); add("Center", doodle); add("South", controls); } /* method paint * * overrides the default applet paint method. Takes whatever is in the * offscreen buffer (buf) and draws it into the animation panel. */ public void paint(Graphics g) { doodle.getGraphics().drawImage(buf,0,0,this); } /* method start * * overrides the applet start method. Runs once the applet has * been initalized and the screen components drawn, starts the work * to actually do the animation, such as creating the offscreen buffer * to draw into, creating the animation object, and running the * animation object in a new thread. */ public void start() { int height, width; /* create the offscreen buffer */ Dimension tempdim = size(); height = tempdim.height; width = tempdim.width; tempdim = controls.size(); height = height - tempdim.height; buf = createImage(width, height); anim = new jAnim(width, height, params, buf.getGraphics(), this); /* since we only will be able to know what pattern is being animated * now, show it in the controls panel */ PatternBox.setText(params.pattern); StartupBox.setText(params.startup); numHands.setText("Hands: " + Integer.toString(params.hands)); numBalls.setText("Balls: " + Integer.toString(anim.pat.getBalls())); if (kicker == null) { kicker = new Thread(anim); kicker.start(); } } /* method stop * * overrides the applet stop method. Is run to stop anything the applet * might be running -- does this by stopping the animation thread and * setting the 'running' parameter to false. */ public void stop() { if (kicker != null && kicker.isAlive()) { kicker.stop(); } params.running = false; kicker = null; } /* method action * * handles a action from one of the controls. This seems to be a button * getting pressed -- therefore this handles the 'on acid' checkbox * changing state, and the button to change the pattern being pressed. */ public boolean action(Event e, Object o){ if (e.target instanceof Checkbox) { params.onAcid = acidCheck.getState(); } if (e.target instanceof Button) { if (e.target == PatternDoIt) { /* Changing the pattern means that we have to stop the * animation and restart it with the new pattern. */ stop(); params.pattern = PatternBox.getText(); params.startup = StartupBox.getText(); start(); } } return true; } /* method handleEvent * * overrides the default event handler of the applet. Used to catch * events on the scrollbars that fiddle with the animation parameters. * if an event happens on some other object, pass it off to the * overridden event handler. */ public boolean handleEvent(Event e){ if (e.target instanceof Scrollbar) { int temp = ((Scrollbar)e.target).getValue(); if (e.target == gravityBar) { params.gravity = temp; gravityValue.setText(Integer.toString(temp)); return true; } else if (e.target == detailBar) { params.detail = ((Scrollbar)e.target).getValue(); detailValue.setText(Integer.toString(temp)); return true; } else if (e.target == speedBar) { params.speed = ((Scrollbar)e.target).getValue(); speedValue.setText(Integer.toString(temp)); return true; } } return super.handleEvent(e); } }