PhantomScroll


/*
 *  PhantomScroll.java 1.0 25 Jan 2000
 *  Copyright (c) 2000 Uldarico Muico Jr.
 *
 *  This program 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; either version 2
 *  of the License, or (at your option) any later version.
 *
 *  This program 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.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

import java.applet.*;
import java.awt.*;
import java.awt.image.*;
import java.net.*;
import java.io.*;
import java.util.*;

/**
 *  The PhantomScroll applet is a horizontal text scroller with fade effects.
 *  @version    1.0 25 Jan 2000
 *  @author     Uldarico Muico Jr.
 */
public class PhantomScroll extends Applet implements Runnable
{
    /**
     *  Ease-of-use constant for bgDisplay parameter of setBackground method. Indicates that the
     *  background image should be tiled.
     *  @see    #setBackground(Image, Color, int)
     */
    public  static final int    TILE            = 0;

    /**
     *  Ease-of-use constant for bgDisplay parameter of setBackground method. Indicates that the
     *  background image should be centered.
     *  @see    #setBackground(Image, Color, int)
     */
    public  static final int    CENTER          = 1;

    private Thread              scrollThread    = null;
    private int[]               bgPixels;
    private int[]               fgPixels;
    private int[]               backPixels;
    private int[]               canvasPixels;
    private int[]               textWidths;
    private int[][]             fadePixels;
    private int[][]             textPixels;
    private int[][][]           linkRanges;
    private Hashtable[][]       links;
    private Dimension           bgSize          = new Dimension();
    private Dimension           fgSize          = new Dimension();
    private Image               bgImage;
    private Image               fgImage;
    private int                 textHeight      = 0;
    private int                 currentText     = 0;
    private int                 width;
    private int                 height;
    private int                 offset;
    private int                 bgDisplay;
    private int                 textDisp;
    private int                 speed;
    private int                 delay;
    private int                 fadeIndex       = 0;
    private int                 clearAmount;
    private int                 count;
    private int                 textCount       = 0;
    private int                 currentLink     = -1;
    private int                 pressedLink     = -1;
    private int                 mouseX;
    private int                 mouseY;
    private int                 amplitude;
    private int                 period;
    private Color               bgColor;
    private Color               fgColor;
    private Color               linkColor;
    private Color               activeLinkColor;
    private boolean             fadeIncremented = true;
    private boolean             pressed;
    private boolean             entered         = false;
    private String              defaultTarget;

    /**
     *  Returns information about this applet.
     *  @return the information
     */
    public String getAppletInfo()
    {
        return  "Uldarico Muico Jr., um@mail.com\n\r"
             +  "PhantomScroll 1.0 January 25, 2000\n\r"
             +  "Copyright (c) 2000 Uldarico Muico, Jr.\n\r";
    }

    /**
     *  Returns information about the parameters that are understood by this applet.
     *  @return the information
     */
    public String[][] getParameterInfo()
    {
        String[][] info = { { "text",               "HTML",     "message to be displayed" },
                            { "textFile",           "URL",      "text file that contains message" },
                            { "font",               "String",   "name of font" },
                            { "fontStyle",          "0-4",      "font style" },
                            { "fontSize",           "int",      "font size" },
                            { "bgImage",            "URL",      "background image" },
                            { "bgDisplay",          "0-1",      "method of displaying background image" },
                            { "bgColor",            "RGB",      "background color" },
                            { "fgImage",            "URL",      "foreground/text image pattern" },
                            { "fgColor",            "RGB",      "foreground/text color" },
                            { "linkColor",          "RGB",      "color of hyperlinks" },
                            { "activeLinkColor",    "RGB",      "color of active hyperlinks" },
                            { "speed",              "int",      "average pixels per iteration" },
                            { "delay",              "int",      "milliseconds per iteration" },
                            { "amplitude",          "int",      "amplitude (pixels) of oscillation" },
                            { "period",             "int",      "period (iterations) per oscillation" },
                            { "trail",              "0.0-1.0",  "intensity of text trail" },
                            { "target",             "String",   "name of the default target frame" } };
        return info;
    }

    /**
     *  Initializes the applet. This method loads and processes the parameters.
     */
    public void init()
    {
        String value;
        width           = size().width;
        height          = size().height;
        linkColor       = parseColor(getParameter("linkColor"), new Color(0x0000FF));
        activeLinkColor = parseColor(getParameter("activeLinkColor"), new Color(0xFF0000));
        setForeground(parseColor(getParameter("fgColor"), new Color(0xFFFFFF)));
        setBackground(parseColor(getParameter("bgColor"), new Color(0x000000)));
        setTarget(getParameter("target"));
        value = getParameter("font");
        Font font = new Font((value == null) ? "Helvetica" : value,
                             parseInt(getParameter("fontStyle"), Font.PLAIN),
                             parseInt(getParameter("fontSize"), 12));
        value = getParameter("text");
        if (value != null)
            setText(value, font);
        else
            setText(parseURL(getParameter("textFile"), null, true), font);
        value = getParameter("bgImage");
        if (value != null)
            setBackground(parseImage(value, null), null, parseInt(getParameter("bgDisplay"), TILE) % 2);
        value = getParameter("fgImage");
        if (value != null)
            setForeground(parseImage(value, null));
        setSpeed(parseInt(getParameter("speed"), 5));
        setDelay(parseInt(getParameter("delay"), 80));
        setAmplitude(parseInt(getParameter("amplitude"), 6));
        setPeriod(parseInt(getParameter("period"), 30));
        setTrail(parseUnity(getParameter("trail"), 0.8));
    }

    /**
     *  Starts the scrolling animation.
     *  @see    #stop()
     */
    public void start()
    {
        if (scrollThread == null)
        {
            scrollThread = new Thread(this);
            scrollThread.start();
        }
    }

    /**
     *  Stops the scrolling animation.
     *  @see    #start()
     */
    public void stop()
    {
        scrollThread = null;
    }

    /**
     *  Invoked when the scroller's internal thread is started through a call to the <i>start</i>
     *  method. This method animates the scroller.
     *  @see    #start()
     */
    public void run()
    {
        while (scrollThread != null)
        {
            repaint();
            count++;
            if (!pressed)
            {
                int disp = -speed;
                if (period != 0)
                    disp -= (int) (amplitude * Math.cos(2.0 * Math.PI * count / period));
                handleDisplacement(disp);
            }
            if (fadeIncremented)
                fadeIndex++;
            else
                fadeIndex--;
            if (fadeIndex >= fadePixels.length)
            {
                fadeIndex--;
                fadeIncremented = false;
            }
            else if (fadeIndex < 0)
            {
                fadeIndex++;
                fadeIncremented = true;
            }
            try
            {
                scrollThread.sleep(delay);
            }
            catch (InterruptedException e)
            {
                scrollThread = null;
            }
        }
    }

    /**
     *  Performs a binary search of the hyperlink to which the cursor is pointing.
     *  @return the index of the link
     */
    private int searchActiveLink()
    {
        int top = linkRanges[currentText].length - 1;
        int bottom = 0;
        while (bottom <= top)
        {
            int middle = (top + bottom) / 2;
            int position = mouseX - offset;
            if (position < linkRanges[currentText][middle][0])
                top = middle - 1;
            else if (position > linkRanges[currentText][middle][1])
                bottom = middle + 1;
            else
                return middle;
        }
        return -1;
    }

    /**
     *  Redraws the portion of the canvas that has been affected since the last animation frame.
     *  @param  g the graphics context
     *  @see    #paint(Graphics)
     */
    public void update(Graphics g)
    {
        if (textPixels != null)
        {
            int prevLink = currentLink;
            if (entered && textDisp <= mouseY && mouseY <= textDisp + textHeight && linkRanges[currentText].length > 0)
            {
                currentLink = searchActiveLink();
                if (currentLink != prevLink)
                    showStatus((currentLink == -1) ? "" : ((String) links[currentText][currentLink].get("alt")));
            }
            else if (prevLink >= 0)
            {
                showStatus("");
                currentLink = -1;
            }
            overlayPixels(backPixels, width, textHeight, 0, 0, width, textHeight,
                          canvasPixels, width, textHeight, 0, 0, true, false);
            overlayPixels(textPixels[currentText], textWidths[currentText], textHeight,
                          0, 0, textWidths[currentText], textHeight,
                          canvasPixels, width, textHeight, offset, 0, false, true);
            Image textImage = createImage(new MemoryImageSource(width, textHeight, canvasPixels, 0, width));
            g.drawImage(textImage, 0, textDisp, null);
            textImage.flush();
        }
    }

    /**
     *  Paints the component. This method is called when the contents of the component should be
     *  painted in response to the component first being shown or damage needing repair.
     *  @param  g the graphics context
     *  @see    #update(Graphics)
     */
    public void paint(Graphics g)
    {
        if (bgPixels != null)
        {
            if (bgDisplay == TILE)
                for (int j = 0; j < height; j += bgSize.height)
                    for (int i = 0; i < width; i += bgSize.width)
                        g.drawImage(bgImage, i, j, bgColor, null);
            else if (bgDisplay == CENTER)
            {
                g.setColor(bgColor);
                g.fillRect(0, 0, width, height);
                g.drawImage(bgImage, (width - bgSize.width) / 2, (height - bgSize.height) / 2, null);
            }
        }
        else
        {
            g.setColor(bgColor);
            g.fillRect(0, 0, width, height);
        }
        update(g);
    }

    /**
     *  Paints the component and returns as soon as the component has completed painting itself.
     */
    private void paint()
    {
        Graphics graphics = getGraphics();
        if (graphics != null)
        {
            paint(graphics);
            graphics.dispose();
        }
    }

    /**
     *  Repaints the component when the image has changed. This method begins processing the
     *  background and foreground images after it has been notified that they have been completely
     *  loaded.
     *  @param  img     the image being observed
     *  @param  flags   see imageUpdate for more information
     *  @param  x       the x coordinate
     *  @param  y       the y coordinate
     *  @param  width   the width
     *  @param  height  the height
     *  @return true if the flags indicate that the image is completely loaded; false otherwise
     */
    public boolean imageUpdate(Image img, int flags, int x, int y, int w, int h)
    {
        if ((flags & ALLBITS) != 0)
        {
            if (img == bgImage)
                setBackground(bgImage, bgColor, bgDisplay);
            else if (img == fgImage)
                setForeground(fgImage);
        }
        return super.imageUpdate(img, flags, x, y, w, h);
    }

    /**
     *  Determines the current text and offset
     *  @param  disp    the displacement on the current offset
     */
    private void handleDisplacement(int disp)
    {
        offset += disp;
        if (offset + textWidths[currentText] < 0)
        {
            offset = width + offset + textWidths[currentText];
            currentText++;
            if (currentText == textCount)
                currentText = 0;
        }
        else if (offset > width)
        {
            currentText--;
            if (currentText < 0)
                currentText = textCount - 1;
            offset = offset - width - textWidths[currentText];
        }
    }

    /**
     *  This method is called when the mouse first enters this component.
     *  @param  event   the event that caused the action
     *  @param  x       the x coordinate
     *  @param  y       the y coordiante
     *  @return true
     */
    public boolean mouseEnter(Event event, int x, int y)
    {
        mouseX = x;
        mouseY = y;
        entered = true;
        return true;
    }

    /**
     *  This method is called when the mouse exits this component.
     *  @param  event   the event that caused the action
     *  @param  x       the x coordinate
     *  @param  y       the y coordiante
     *  @return true
     */
    public boolean mouseExit(Event event, int x, int y)
    {
        entered = false;
        return true;
    }

    /**
     *  This method is called when the mouse button is released inside this component.
     *  @param  event   the event that caused the action
     *  @param  x       the x coordinate
     *  @param  y       the y coordiante
     *  @return true
     */
    public boolean mouseUp(Event event, int x, int y)
    {
        pressed = false;
        if (currentLink >= 0 && currentLink == pressedLink)
        {
            String target = (String) links[currentText][currentLink].get("target");
            getAppletContext().showDocument(
                (URL) links[currentText][currentLink].get("href"),
                (target == null) ? defaultTarget : target);
        }
        return true;
    }

    /**
     *  This method is called when the mouse button is pushed inside this component.
     *  @param  event   the event that caused the action
     *  @param  x       the x coordinate
     *  @param  y       the y coordiante
     *  @return true
     */
    public boolean mouseDown(Event event, int x, int y)
    {
        pressed = true;
        pressedLink = (currentLink >= 0) ? currentLink : -1;
        return true;
    }

    /**
     *  This method is called when the mouse is moved inside this component with the mouse button
     *  not pushed.
     *  @param  event   the event that caused the action
     *  @param  x       the x coordinate
     *  @param  y       the y coordiante
     *  @return true
     */
    public boolean mouseMove(Event event, int x, int y)
    {
        mouseX = x;
        mouseY = y;
        if (scrollThread == null)
            repaint();
        return true;
    }

    /**
     *  This method is called when the mouse button is moved inside this component with the button
     *  pushed.
     *  @param  event   the event that caused the action
     *  @param  x       the x coordinate
     *  @param  y       the y coordiante
     *  @return true
     */
    public boolean mouseDrag(Event event, int x, int y)
    {
        handleDisplacement(x - mouseX);
        mouseX = x;
        mouseY = y;
        repaint();
        return true;
    }

    /**
     *  Resizes this component to the specified width and height.
     *  @param  w   the width
     *  @param  h   the height
     */
    public void resize(int w, int h)
    {
        width = w;
        height = h;
        super.resize(w, h);
        width = size().width;
        height = size().height;
        updateBackground();
    }

    /**
     *  Determines the preferred size of the component.
     *  @return the preferred size of this component
     */
    public Dimension preferredSize()
    {
        return new Dimension(width, height);
    }

    /**
     *  Returns the background color.
     *  @return the background color
     */
    public Color getBackgroundColor()
    {
        return new Color(bgColor.getRGB());
    }

    /**
     *  Returns the background image.
     *  @return the background image
     */
    public Image getBackgroundImage()
    {
        return bgImage;
    }

    /**
     *  Returns the foreground color.
     *  @return the foreground color
     */
    public Color getForegroundColor()
    {
        return new Color(fgColor.getRGB());
    }

    /**
     *  Returns the foreground image.
     *  @return the foreground image
     */
    public Image getForegroundImage()
    {
        return fgImage;
    }

    /**
     *  Returns the hyperlink color.
     *  @return the hyperlink color
     */
    public Color getLinkColor()
    {
        return new Color(linkColor.getRGB());
    }

    /**
     *  Returns the active hyperlink color.
     *  @return the active hyperlink color
     */
    public Color getActiveLinkColor()
    {
        return new Color(activeLinkColor.getRGB());
    }

    /**
     *  Returns the speed
     *  @return the number of pixels to shift between frames/iterations
     */
    public int getSpeed()
    {
        return speed;
    }

    /**
     *  Returns the delay between frames/iterations.
     *  @return the delay in milliseconds
     */
    public int getDelay()
    {
        return delay;
    }

    /**
     *  Returns the amplitude of the oscillation.
     *  @return the number of pixels by which to oscillate in either direction
     */
    public int getAmplitude()
    {
        return amplitude;
    }

    /**
     *  Returns the period of the oscillation.
     *  @return the number of frames/iteration to complet an oscillation
     */
    public int getPeriod()
    {
        return period;
    }

    /**
     *  Returns the default target frame.
     *  @return the name of default target frame
     */
    public String getTarget()
    {
        return new String(defaultTarget);
    }

    /**
     *  Updates the background pixels. This method should be called when any aspect of the
     *  background has changed.
     */
    private void updateBackground()
    {
        if (textPixels != null)
        {
            textDisp        = (height - textHeight) / 2;
            backPixels      = new int[width * textHeight];
            canvasPixels    = new int[width * textHeight];
            int bgPixel = bgColor.getRGB();
            int i, j;
            for (i = 0; i < width * textHeight; i++)
                canvasPixels[i] = 0xFF000000 | bgPixel;
            if (bgPixels != null)
            {
                if (bgDisplay == TILE)
                    for (j = -(textDisp % bgSize.height); j < textHeight; j += bgSize.height)
                        for (i = 0; i < width; i += bgSize.width)
                            overlayPixels(bgPixels, bgSize.width, bgSize.height, 0, 0, bgSize.width, bgSize.height,
                                          canvasPixels, width, textHeight, i, j, false, false);
                else if (bgDisplay == CENTER)
                    overlayPixels(bgPixels, bgSize.width, bgSize.height, 0, 0, bgSize.width, bgSize.height,
                                  canvasPixels, width, textHeight,
                                  (width - bgSize.width) / 2, (textHeight - bgSize.height) / 2, false, false);
            }
            for (i = 0; i < width * textHeight; i++)
                backPixels[i] = 0xFF000000 | (0xFFFFFF & canvasPixels[i]);
        }
    }

    /**
     *  Sets the background image.
     *  @param  image   the image to be displayed in the background
     *  @param  color   the background color to use for any transparent pixels
     *  @param  display the method by which to display the image
     *  @see    #TILE
     *  @see    #CENTER
     */
    public void setBackground(Image image, Color color, int display)
    {
        bgImage     = image;
        bgDisplay   = display % 2;
        if (color != null)
            bgColor = new Color(color.getRGB());
        if (prepareImage(image, this))
        {
            bgPixels        = getPixels(bgImage);
            bgSize.width    = bgImage.getWidth(null);
            bgSize.height   = bgImage.getHeight(null);
            updateBackground();
            paint();
        }
    }

    /**
     *  Sets the background to a solid color.
     *  @param  color   the background color
     */
    public void setBackground(Color color)
    {
        bgColor     = new Color(color.getRGB());
        bgPixels    = null;
        bgImage     = null;
        updateBackground();
        paint();
    }

    /**
     *  Sets the foreground image pattern.
     *  @param  image   the foreground image
     */
    public void setForeground(Image image)
    {
        fgImage = image;
        if (prepareImage(image, this))
        {
            fgPixels        = getPixels(fgImage);
            fgSize.width    = fgImage.getWidth(null);
            fgSize.height   = fgImage.getHeight(null);
            updateForeground();
            repaint();
        }
    }

    /**
     *  Sets the foreground to a solid color.
     *  @param  color   the foreground color
     */
    public void setForeground(Color color)
    {
        fgColor = new Color(color.getRGB());
        fgPixels = null;
        updateForeground();
        repaint();
    }

    /**
     *  Updates the foreground pixels. This method should be called when any aspect of the
     *  foreground has changed.
     */
    private void updateForeground()
    {
        if (textPixels != null)
        {
            if (fgPixels != null)
            {
                for (int k = 0; k < textCount; k++)
                    for (int j = 0; j < textHeight; j++)
                        for (int i = 0; i < textWidths[k]; i++)
                        {
                            int index = j * textWidths[k] + i;
                            textPixels[k][index] = (textPixels[k][index] & 0xFF000000)
                                | (0xFFFFFF & fgPixels[(j % fgSize.height) * fgSize.width + (i % fgSize.width)]);
                        }
            }
            else
            {
                int pixel = 0xFFFFFF & fgColor.getRGB();
                for (int k = 0; k < textCount; k++)
                    for (int i = 0; i < textWidths[k] * textHeight; i++)
                        textPixels[k][i] = (textPixels[k][i] & 0xFF000000) | pixel;
            }
            for (int i = 0; i < textCount; i++)
                for (int j = 0; j < linkRanges[i].length; j++)
                {
                    int w = linkRanges[i][j][1] - linkRanges[i][j][0] + 1;
                    colorPixels(textPixels[i], textWidths[i], textHeight, linkRanges[i][j][0] - w / 8, 0,
                                5 * w / 4, textHeight, linkColor.getRGB());
                }
        }
    }

    /**
     *  Sets the color of the hyperlinks.
     *  @param  color   the hyperlink color
     */
    public void setLinkColor(Color color)
    {
        linkColor = new Color(color.getRGB());
        updateForeground();
    }

    /**
     *  Sets the color of the hyperlink to which the mouse cursor is pointing
     *  @param  color   the hyperlink color
     */
    public void setActiveLinkColor(Color color)
    {
        activeLinkColor = new Color(color.getRGB());
    }

    /**
     *  Sets the speed of the scrolling motion
     *  @param  speed   the number of pixels per animation frame
     */
    public void setSpeed(int speed)
    {
        this.speed = speed;
    }

    /**
     *  Sets the frame rate.
     *  @param  delay   the number of milliseconds between animation frames
     */
    public void setDelay(int delay)
    {
        this.delay = delay;
    }

    /**
     *  Sets the amplitude of the oscillation.
     *  @param  amplitude   the number of pixels by which to oscillate in either direction
     */
    public void setAmplitude(int amplitude)
    {
        this.amplitude = amplitude;
    }

    /**
     *  Sets the period (inverse of frequency) of the oscillation.
     *  @param  period  the number of iterations/frames to complete one oscillation
     */
    public void setPeriod(int period)
    {
        this.period = period;
    }

    /**
     *  Sets the fading trail effect.
     *  @param  intensity   the intensity of the trail effect. Values range from 0.0 to 1.0.
     */
    public void setTrail(double intensity)
    {
        clearAmount = 0xFF - (int) (0xFF * normalize(intensity));
    }

    /**
     *  Sets the default target frame.
     *  @param  target the default target
     */
    public void setTarget(String target)
    {
        defaultTarget = (target == null) ? "_self" : new String(target);
    }

    /**
     *  Sets the text to be scrolled from a text file.
     *  @param  path    the path of the file that contains the text to be scrolled
     *  @param  font    the font to use in displaying the text
     */
    public void setText(URL path, Font font)
    {
        String defaultText = "PhantomScroll was developed by <a href=\"mailto:um@mail.com\">Uldarico Muico Jr.</a>";
        String contents = defaultText;
        if (path != null)
        {
            try
            {
                DataInputStream stream = new DataInputStream(path.openConnection().getInputStream());
                contents = stream.readLine();
                String line;
                while ((line = stream.readLine()) != null)
                    contents = contents + line;
                stream.close();

            }
            catch (IOException e)
            {
                e.printStackTrace();
                contents = defaultText;
            }
        }
        setText(contents, font);
    }

    /**
     *  Sets the text to be scrolled. Hyperlinks and breaks may be indicated as in HTML.
     *  @param  text    the text to be scrolled
     *  @param  font    the font to use in displaying the text
     */
    public void setText(String text, Font font)
    {
        text = text + "<br>";
        Vector  modifiedText    = new Vector();
        Vector  stack           = new Vector();
        Vector  data            = new Vector();
        int     prevIndex       = 0;
        textCount = 0;
        while (true)
        {
            int index = text.indexOf('<', prevIndex);
            if (index == -1) break;
            int endIndex = searchBracket(text, index);
            Properties tagProps = parseTagProperties(text.substring(index + 1, endIndex));
            while (modifiedText.size() <= textCount)
                modifiedText.addElement(new String());
            while (data.size() <= textCount)
                data.addElement(new Vector());
            String s = (String) modifiedText.elementAt(textCount);
            modifiedText.setElementAt(s.concat(text.substring(prevIndex, index)), textCount);
            String type = tagProps.getProperty("type");
            if (type.equals("br"))
            {
                if (stack.size() > 0)
                {
                    String tail = text.substring(endIndex + 1);
                    text = text.substring(0, endIndex + 1);
                    for (int i = 0; i < stack.size(); i++)
                    {
                        Hashtable table = (Hashtable) stack.elementAt(i);
                        String t = (String) table.get("type");
                        text = text + "</" + t + ">";
                        tail = ((String) table.get("tag")) + tail;
                        table.remove("tag");
                    }
                    text = text + "<br>" + tail;
                }
                else
                    textCount++;
            }
            else if (type.equals("a"))
            {
                URL         url     = parseURL(tagProps.getProperty("href"), getDocumentBase(), false);
                String      target  = tagProps.getProperty("target");
                String      alt     = tagProps.getProperty("alt");
                Hashtable   table   = new Hashtable();
                table.put("type", "a");
                table.put("href", url);
                if (target != null)
                    table.put("target", target);
                int[] range = new int[2];
                range[0] = ((String) modifiedText.elementAt(textCount)).length();
                table.put("range", range);
                table.put("tag", text.substring(index, endIndex + 1));
                table.put("alt", (alt == null) ? url.toString() : alt);
                stack.addElement(table);
            }
            else if (type.equals("/a"))
            {
                int i = stack.size() - 1;
                while (i >= 0)
                {
                    Hashtable table = (Hashtable) stack.elementAt(i);
                    if (table.get("type") == "a")
                    {
                        int[] range = (int[]) table.get("range");
                        range[1] = ((String) modifiedText.elementAt(textCount)).length() - 1;
                        ((Vector) data.elementAt(textCount)).addElement(table);
                        stack.removeElementAt(i);
                        break;
                    }
                    i--;
                }
            }
            prevIndex = endIndex + 1;
        }
        stack.removeAllElements();
        FontMetrics metrics = getFontMetrics(font);
        textWidths  = new int[textCount];
        links       = new Hashtable[textCount][];
        linkRanges  = new int[textCount][][];
        textPixels  = new int[textCount][];
        for (int i = 0; i < textCount; i++)
        {
            String      t       = (String) modifiedText.elementAt(i);
            int[]       buffer  = createTextPixels(t, font, fgColor);
            Dimension   size    = getTextSize(t, metrics);
            textWidths[i]   = size.width;
            textHeight      = size.height;
            textPixels[i]   = new int[textWidths[i] * textHeight];
            Vector textData = (Vector) data.elementAt(i);
            linkRanges[i]   = new int[textData.size()][];
            links[i]        = new Hashtable[textData.size()];
            for (int j = 0; j < textData.size(); j++)
            {
                links[i][j]         = (Hashtable) textData.elementAt(j);
                int[] indices       = (int[]) links[i][j].get("range");
                linkRanges[i][j]    = new int[2];
                linkRanges[i][j][0] = metrics.stringWidth(t.substring(0, indices[0])) + metrics.charWidth(' ');
                linkRanges[i][j][1] = linkRanges[i][j][0] + metrics.stringWidth(t.substring(indices[0], indices[1] + 1));
                for (int k = linkRanges[i][j][0]; k <= linkRanges[i][j][1] - 1; k++)
                    buffer[(metrics.getAscent() + 2) * textWidths[i] + k] |= 0xFF000000;

            }
            filterPixels(buffer, textWidths[i], textHeight,
                        0, 0, textWidths[i], textHeight,
                        textPixels[i], textWidths[i], textHeight, 0, 0, 0);
        }
        offset = width;
        updateBackground();
        updateForeground();
        createFade();
    }

    /**
     *  Searches for a left angle bracket.
     *  @param  s       the string in which to search
     *  @param  index   the starting index into the string
     *  @return the index of the first matching bracket
     */
    private int searchBracket(String s, int index)
    {
        while (true)
        {
            char ch = s.charAt(index);
            if (ch == '>')
                return index;
            else if (ch == '"' || ch == '\'')
                index = s.indexOf(ch, index + 1);
            if (index == -1)
                return -1;
            index++;
        }
    }

    /**
     *  Parses the contents of an HTML tag.
     *  @param  value   the text to be parsed excluding the tag delimeters
     *  @return the properties
     */
    private Properties parseTagProperties(String value)
    {
        Properties      properties      = new Properties();
        StreamTokenizer tokenizer       = new StreamTokenizer(new StringBufferInputStream(value));
        String          previousToken   = null;
        tokenizer.resetSyntax();
        tokenizer.wordChars(0x21, 0xFFFF);
        tokenizer.quoteChar('\'');
        tokenizer.quoteChar('"');
        tokenizer.ordinaryChar('=');
        try
        {
            if (tokenizer.nextToken() != tokenizer.TT_EOF)
                properties.put("type", tokenizer.sval);
            int type = tokenizer.nextToken();
            while (type != tokenizer.TT_EOF)
            {
                if (type == '=' && previousToken != null)
                {
                    tokenizer.nextToken();
                    properties.put(previousToken.toLowerCase(), tokenizer.sval);
                    previousToken = null;
                }
                else if (type == tokenizer.TT_WORD)
                    previousToken = tokenizer.sval;
                type = tokenizer.nextToken();
            }
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        return properties;
    }

    /**
     *  Creates the alpha values for those pixels near the left and right edges of the applet.
     */
    private void createFade()
    {
        double  fadeDepth   = 0.8;
        int     fadePeriod  = textHeight;
        int     upperX      = width / 2 + 1;
        int     upperY      = fadePeriod / 2 + 1;
        fadePixels = new int[upperY][];
        for (int j = 0; j < upperY; j++)
        {
            fadePixels[j] = new int[upperX];
            double penetration = fadeDepth * (0.65 - 0.35 * Math.cos(2.0 * Math.PI * j / fadePeriod));
            int i = 0;
            while (i < (int) (penetration * upperX))
            {
                fadePixels[j][i] = (int) ((0.5 - 0.5 * Math.cos(2.0 * Math.PI / penetration * i / width)) * 0xFF);
                i++;
            }
            while (i < upperX)
            {
                fadePixels[j][i] = 0xFF;
                i++;
            }
        }
    }

    /**
     *  Computes the dimensions of text to be displayed on the screen.
     *  @param  text    the text to be displayed
     *  @param  metrics the font metrics
     *  @return the size
     */
    private Dimension getTextSize(String text, FontMetrics metrics)
    {
        Dimension textSize = new Dimension();
        textSize.width = metrics.stringWidth(text) + 2 * metrics.charWidth(' ');
        textSize.height += metrics.getAscent() + metrics.getDescent() + 3;
        return textSize;
    }

    /**
     *  Creates the pixels with the alpha mask that represents the message to be displayed.
     *  @param  text    the string to be displayed
     *  @param  font    the font of the text
     *  @param  color   the initial color of the text
     */
    private int[] createTextPixels(String text, Font font, Color color)
    {
        FontMetrics metrics     = getFontMetrics(font);
        Dimension   textSize    = getTextSize(text, metrics);
        int         size        = textSize.width * textSize.height;
        int         lineHeight  = metrics.getAscent() + metrics.getDescent();
        int         pixel       = ((color == null) ? 0xFF7F7F7F : color.getRGB()) & 0xFFFFFF;
        Image       textImage   = createImage(textSize.width, textSize.height);
        Graphics    graphics    = textImage.getGraphics();
        int[]       values      = new int[4];
        graphics.setColor(new Color(0xFF000000));
        graphics.fillRect(0, 0, textSize.width, textSize.height);
        graphics.setColor(new Color(0xFFFFFFFF));
        graphics.setFont(font);
        graphics.drawString(text, (textSize.width - metrics.stringWidth(text)) / 2, metrics.getAscent());
        graphics.dispose();
        int[] pixels = getPixels(textImage);
        for (int index = 0; index < size; index++)
        {
            decomposePixel(pixels[index], values);
            int value = ((values[1] + values[2] + values[3]) / 3) << 24;
            pixels[index] = value | pixel;
        }
        return pixels;
    }

    /**
     *  Parses a string representation of an URL.
     *  @param  value           the string representation
     *  @param  defaultValue    the value if the the parsing fails
     *  @param  codeBased       indicates whether the relative URL is code-based or document-based
     *  @return the parsed URL
     */
    private URL parseURL(String value, URL defaultValue, boolean codeBased)
    {
        if (value == null)
            return defaultValue;
        try
        {
            if (value.indexOf(":") != -1)
                return new URL(value);
            else
                return new URL((codeBased) ? getCodeBase() : getDocumentBase(), value);
        }
        catch (MalformedURLException e)
        {
            e.printStackTrace();
            return defaultValue;
        }
    }

    /**
     *  Parses a string representation of an image. The string is simply the URL of the image.
     *  @param  value           the string representation
     *  @param  defaultValue    the value if the the parsing fails
     *  @return the image
     */
    private Image parseImage(String value, Image defaultValue)
    {
        if (value != null)
            return getImage(parseURL(value, null, true));
        else
            return defaultValue;
    }

    /**
     *  Parses a string representation of an integer.
     *  @param  value           the string representation
     *  @param  defaultValue    the value if the the parsing fails
     *  @return the parsed integer
     */
    private int parseInt(String value, int defaultValue)
    {
        if (value == null)
            return defaultValue;
        try
        {
            return Integer.parseInt(value);
        }
        catch (NumberFormatException e)
        {
            e.printStackTrace();
            return defaultValue;
        }
    }

    /**
     *  Parses a string representation of a floating-point number in the interval 0.0 and 1.0.
     *  @param  value           the string representation
     *  @param  defaultValue    the value if the the parsing fails
     *  @return the parsed floating-point number
     */
    private double parseUnity(String value, double defaultValue)
    {
        if (value == null)
            return defaultValue;
        try
        {
            return normalize(Double.valueOf(value).doubleValue());
        }
        catch (NumberFormatException e)
        {
            e.printStackTrace();
            return defaultValue;
        }
    }

    /**
     *  Parses a string representation of a color in RGB decimal or hexadecimal format.
     *  @param  value           the string representation
     *  @param  defaultValue    the value if the the parsing fails
     *  @return the parsed color
     */
    private Color parseColor(String value, Color defaultValue)
    {
        if (value == null)
            return defaultValue;
        try
        {
            if (value.indexOf(" ") != -1)
            {
                StringTokenizer tokenizer = new StringTokenizer(value);
                int pixel = 0xFF;
                while (tokenizer.hasMoreTokens())
                    pixel = (pixel << 8) | Integer.parseInt(tokenizer.nextToken());
                return new Color(pixel);
            }
            else
            {
                int pixel = Integer.parseInt(value, 16);
                return new Color(0xFF000000 | pixel);
            }
        }
        catch (NumberFormatException e)
        {
            e.printStackTrace();
            return defaultValue;
        }
    }

    /**
     *  Retrieves the pixels of an image.
     *  @param  image   the image from which to get the pixels
     *  @return the pixels
     */
    private int[] getPixels(Image image)
    {
        int             width   = image.getWidth(null);
        int             height  = image.getHeight(null);
        int[]           pixels  = new int[width * height];
        PixelGrabber    grabber = new PixelGrabber(image, 0, 0, width, height, pixels, 0, width);
        try
        {
            grabber.grabPixels();
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
            return null;
        }
        return pixels;
    }

    /**
     *  Restrains a floating-point number to the interval 0.0 to 1.0.
     *  @param  x   the number to correct
     *  @return the corrected number
     */
    private double normalize(double x)
    {
        if (x < 0.0)
            x = 0.0;
        else if (x > 1.0)
            x = 1.0;
        return x;
    }

    /**
     *  Computes the effective bounds for an image region that linearly maps to another
     *  @param  srcW    the width of the source image
     *  @param  srcH    the height of the source image
     *  @param  srcX    the x coordinate of the starting pixel in the source coordinate system
     *  @param  srcY    the y coordinate of the starting pixel in the source coordinate system
     *  @param  destW   the width of the destination image
     *  @param  destH   the height of the destination image
     *  @param  destX   the x coordinate of the starting pixel in the destination coordinate system
     *  @param  destY   the x coordinate of the starting pixel in the destination coordinate system
     *  @param  w       the width of the region
     *  @param  h       the height of the region
     *  @return the effective bounds in the destination coordinate system
     */
    private Rectangle effectiveBounds(int srcW, int srcH, int srcX, int srcY,
                                      int destW, int destH, int destX, int destY,
                                      int w, int h)
    {
        Rectangle rect = new Rectangle(srcX, srcY, w, h);
        rect = rect.intersection(new Rectangle(srcW, srcH));
        rect.translate(destX - srcX, destY - srcY);
        return rect.intersection(new Rectangle(destW, destH));
    }

    /**
     *  Decomposes the alpha, red, green, and blue components of a pixel.
     *  @param  pixel   the pixel to decompose
     *  @param  values  the buffer to hold the components
     */
    private void decomposePixel(int pixel, int[] values)
    {
        values[0] = (pixel >> 24) & 0xFF;
        values[1] = (pixel >> 16) & 0xFF;
        values[2] = (pixel >> 8)  & 0xFF;
        values[3] =  pixel        & 0xFF;
    }

    /**
     *  Applies an anti-aliasing filter to a region of an image.
     *  @param  srcPixels   the source pixels
     *  @param  srcW        the width of the source image
     *  @param  srcH        the height of the source image
     *  @param  srcX        the x coordinate of the starting pixel in the source coordinate system
     *  @param  srcY        the y coordinate of the starting pixel in the source coordinate system
     *  @param  w           the width of the region
     *  @param  h           the height of the region
     *  @param  destPixels  the destination pixels
     *  @param  destW       the width of the destination image
     *  @param  destH       the height of the destination image
     *  @param  destX       the x coordinate of the starting pixel in the destination coordinate system
     *  @param  destY       the x coordinate of the starting pixel in the destination coordinate system
     *  @param  bgValue     the default background color
     */
    private void filterPixels(int[] srcPixels, int srcW, int srcH,
                             int srcX, int srcY, int w, int h,
                             int[] destPixels, int destW, int destH,
                             int destX, int destY, int bgValue)
    {
        Rectangle   rect        = effectiveBounds(srcW, srcH, srcX, srcY, destW, destH, destX, destY, w, h);
        int         i;
        int         bufferSize  = srcW * srcH;
        int         dx          = destX - srcX;
        int         dy          = destY - srcY;
        int[]       sums        = new int[3];
        bgValue = 0xFFFFFF | bgValue;
        rect.translate(-dx, -dy);
        int upperX = rect.x + rect.width + 1;
        int upperY = rect.y + rect.height + 1;
        for (int column = rect.x - 1; column < upperX; column++)
        {
            for (i = 0; i < 3; i++)
                sums[i] = 0;
            for (int row = rect.y - 1; row < upperY; row++)
            {
                int bottomIndex     = (row + 2) % 3;
                int srcIndex        = (row + 1) * srcW + column;
                sums[bottomIndex]   = 0;
                for (i = -1; i <= 1; i++)
                {
                    int batchIndex = srcIndex + i;
                    if (batchIndex >= 0 && batchIndex < bufferSize && column + i >= 0 && column + i < srcW)
                        sums[bottomIndex] += ((srcPixels[batchIndex] >> 24) & 0xFF) * ((i == 0) ? 3 : 1);
                }
                int destColumn  = column + dx;
                int destRow     = row + dy;
                if (destColumn >= 0 && destColumn < destW && destRow >= 0 && destRow < destH)
                {
                    int sum         = 0;
                    int topIndex    = row + 3;
                    int centerIndex = topIndex + 1;
                    bottomIndex     = topIndex + 2;
                    for (i = topIndex; i <= bottomIndex; i++)
                        sum += ((i == centerIndex) ? 3 : 1) * sums[i % 3];
                    int pixel;
                    if (row >= 0 && row < srcH && column >= 0 & column < srcW)
                        pixel = srcPixels[row * srcW + column] & 0xFFFFFF;
                    else
                        pixel = bgValue;
                    destPixels[destRow * destW + destColumn] = ((sum / 25) << 24) | pixel;
                }
            }
        }
    }

    /**
     *  Overlays a solid color onto a region of an image.
     *  @param  pixels  the pixels
     *  @param  width   the width of the image
     *  @param  height  the height of the image
     *  @param  x       the x coordinate of the starting pixel
     *  @param  x       the y coordinate of the starting pixel
     *  @param  w       the width of the region
     *  @param  h       the height of the region
     */
    private void colorPixels(int[] pixels, int width, int height, int x, int y, int w, int h, int color)
    {
        int         size        = width * height;
        int[]       values      = new int[4];
        int[]       cValues     = new int[4];
        float[]     HSBValues   = new float[3];
        float[]     HSBCValues  = new float[3];
        decomposePixel(color, cValues);
        Color.RGBtoHSB(cValues[1], cValues[2], cValues[3], HSBCValues);
        for (int j = 0; j < h; j++)
            if (y + j >= 0 && y + j < height)
                for (int i = 0; i < w; i++)
                    if (x + i >= 0 && x + i < width)
                    {
                        double intensity = Math.cos(i * Math.PI / w);
                        for (int k = 0; k < 4; k++)
                            intensity *= intensity;
                        intensity = 1.0 - intensity;
                        int amount = (int) (0xFF * intensity);
                        int index = (j + y) * width + i + x;
                        decomposePixel(pixels[index], values);
                        Color.RGBtoHSB(values[1], values[2], values[3], HSBValues);
                        float saturation = HSBValues[1];
                        float brightness = HSBValues[2];
                        values[1]   = (amount * cValues[1] + (0xFF - amount) * values[1]) / 0xFF;
                        values[2]   = (amount * cValues[2] + (0xFF - amount) * values[2]) / 0xFF;
                        values[3]   = (amount * cValues[3] + (0xFF - amount) * values[3]) / 0xFF;
                        Color.RGBtoHSB(values[1], values[2], values[3], HSBValues);
                        HSBValues[1]    = (float) (intensity * HSBCValues[1] + (1.0 - intensity) * saturation);
                        HSBValues[2]    = (float) (0.4 * intensity * HSBCValues[2] + (1.0 - 0.4 * intensity) * brightness);
                        pixels[index]   = (values[0] << 24)
                                        | (0xFFFFFF & Color.HSBtoRGB(HSBValues[0], HSBValues[1], HSBValues[2]));
                    }
    }

    /**
     *  Overlays an image onto another. The alpha pixels determine the opacity of the images.
     *  @param  srcPixels   the source pixels
     *  @param  srcW        the width of the source image
     *  @param  srcH        the height of the source image
     *  @param  srcX        the x coordinate of the starting pixel in the source coordinate system
     *  @param  srcY        the y coordinate of the starting pixel in the source coordinate system
     *  @param  w           the width of the region
     *  @param  h           the height of the region
     *  @param  destPixels  the destination pixels
     *  @param  destW       the width of the destination image
     *  @param  destH       the height of the destination image
     *  @param  destX       the x coordinate of the starting pixel in the destination coordinate system
     *  @param  destY       the x coordinate of the starting pixel in the destination coordinate system
     *  @param  clearing    indicates that the source pixels are refreshing the canvas
     *  @param  fading      indicates that the source pixels are to be faded near the edges
     */
    private void overlayPixels(int[] srcPixels, int srcW, int srcH,
                               int srcX, int srcY, int w, int h,
                               int[] destPixels, int destW, int destH,
                               int destX, int destY, boolean clearing, boolean fading)
    {
        Rectangle   rect        = effectiveBounds(srcW, srcH, srcX, srcY, destW, destH, destX, destY, w, h);
        int         dx          = destX - srcX;
        int         dy          = destY - srcY;
        int         i;
        int         j           = rect.y * destW;
        int         k           = (rect.y - dy) * srcW;
        int         upperI      = rect.x + rect.width;
        int         upperJ      = (rect.y + rect.height) * destW;
        int         fadeI;
        int         fadeJ       = fadeIndex;
        int         center      = width / 2;
        int[]       srcRGB      = new int[4];
        int[]       destRGB     = new int[4];
        int[]       cRGB        = new int[4];
        boolean     incremented = fadeIncremented;
        decomposePixel(activeLinkColor.getRGB(), cRGB);
        while (j < upperJ)
        {
            if (rect.x <= center)
                fadeI = rect.x;
            else
                fadeI = 2 * center - rect.x;
            for (i = rect.x; i < upperI; i++)
            {
                int srcPixel = srcPixels[k + i - dx];
                decomposePixel(srcPixel, srcRGB);
                if (srcRGB[0] != 0)
                {
                    int destIndex = j + i;
                    int destPixel = destPixels[destIndex];
                    if (currentLink >= 0 && !clearing
                        && linkRanges[currentText][currentLink][0] <= i - destX
                        && i - destX <= linkRanges[currentText][currentLink][1])
                    {
                        srcRGB[1]   = cRGB[1];
                        srcRGB[2]   = cRGB[2];
                        srcRGB[3]   = cRGB[3];
                    }
                    if (clearing)
                        srcRGB[0]   = clearAmount;
                    else if (fading)
                        srcRGB[0]   = srcRGB[0] * fadePixels[fadeJ][fadeI] / 0xFF;
                    int destAmount = 0xFF - srcRGB[0];
                    decomposePixel(destPixel, destRGB);
                    destRGB[1]              = (destAmount * destRGB[1] + srcRGB[0] * srcRGB[1]) / 0xFF;
                    destRGB[2]              = (destAmount * destRGB[2] + srcRGB[0] * srcRGB[2]) / 0xFF;
                    destRGB[3]              = (destAmount * destRGB[3] + srcRGB[0] * srcRGB[3]) / 0xFF;
                    destPixels[destIndex]   = 0xFF000000
                                            | (destRGB[1] << 16)
                                            | (destRGB[2] << 8)
                                            |  destRGB[3];
                }
                if (i < center)
                    fadeI++;
                else
                    fadeI--;
            }
            if (incremented)
                fadeJ++;
            else
                fadeJ--;
            if (fadeJ >= fadePixels.length)
            {
                fadeJ--;
                incremented = false;
            }
            else if (fadeJ < 0)
            {
                fadeJ++;
                incremented = true;
            }
            j += destW;
            k += srcW;
        }
    }
}

Back to the PhantomScroll applet

How to Add Java Applets to Your Site

New on the Java Boutique:

New Review:

Time Management Made Easy with the Quartz Enterprise Job Scheduler
Why not just use the Java timer API? This open source scheduling API boasts simplicity, ease-of-integration, a well-rounded feature set, and it's free!

New Applet:

Reverse Complement
Reverse Complement is a simple applet that converts DNA or RNA sequences into three useful formats.

Elsewhere on internet.com:

WebDeveloper Java
Lots of Java information on webdeveloper.com

WDVL Java
Thorough Java resource at the Web Developer's Virtual Library.

ScriptSearch Java
Hundreds of free Java code files to download.

jGuru: Your View of the Java Universe
Customizable portal with online training, FAQs, regular news updates, and tutorials.