import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.NoninvertibleTransformException;
-import java.awt.geom.PathIterator;
-import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.text.AttributedCharacterIterator;
import java.util.ArrayList;
import java.util.HashMap;
-import java.util.Iterator;
import java.util.Map;
/**
{
/**
+ * The default font to use on the graphics object.
+ */
+ private static final Font FONT = new Font("SansSerif", Font.PLAIN, 12);
+
+ /**
* Accuracy of the sampling in the anti-aliasing shape filler.
* Lower values give more speed, while higher values give more quality.
* It is advisable to choose powers of two.
* Caches certain shapes to avoid massive creation of such Shapes in
* the various draw* and fill* methods.
*/
- private static final ThreadLocal shapeCache = new ThreadLocal();
+ private static final ThreadLocal<ShapeCache> shapeCache =
+ new ThreadLocal<ShapeCache>();
+
+ /**
+ * The scanline converters by thread.
+ */
+ private static final ThreadLocal<ScanlineConverter> scanlineConverters =
+ new ThreadLocal<ScanlineConverter>();
/**
* The transformation for this Graphics2D instance
private Paint paint;
/**
+ * The paint context during rendering.
+ */
+ private PaintContext paintContext;
+
+ /**
* The background.
*/
private Color background;
*/
private boolean isOptimized = true;
+ private static final BasicStroke STANDARD_STROKE = new BasicStroke();
+
+ private static final HashMap STANDARD_HINTS;
+ static {
+ HashMap hints = new HashMap();
+ hints.put(RenderingHints.KEY_TEXT_ANTIALIASING,
+ RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT);
+ hints.put(RenderingHints.KEY_ANTIALIASING,
+ RenderingHints.VALUE_ANTIALIAS_DEFAULT);
+ STANDARD_HINTS = hints;
+ }
/**
* Creates a new AbstractGraphics2D instance.
*/
transform = new AffineTransform();
background = Color.WHITE;
composite = AlphaComposite.SrcOver;
- stroke = new BasicStroke();
- HashMap hints = new HashMap();
- hints.put(RenderingHints.KEY_TEXT_ANTIALIASING,
- RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT);
- hints.put(RenderingHints.KEY_ANTIALIASING,
- RenderingHints.VALUE_ANTIALIAS_DEFAULT);
- renderingHints = new RenderingHints(hints);
+ stroke = STANDARD_STROKE;
+ renderingHints = new RenderingHints(STANDARD_HINTS);
}
/**
*/
public void drawGlyphVector(GlyphVector gv, float x, float y)
{
- int numGlyphs = gv.getNumGlyphs();
translate(x, y);
- // TODO: We could use fill(gv.getOutline()), but that seems to be
- // slightly more inefficient.
- for (int i = 0; i < numGlyphs; i++)
- {
- Shape o = gv.getGlyphOutline(i);
- fillShape(o, true);
- }
+ fillShape(gv.getOutline(), true);
translate(-x, -y);
}
antialias = (v == RenderingHints.VALUE_ANTIALIAS_ON);
}
- Rectangle2D userBounds = s.getBounds2D();
- Rectangle2D deviceBounds = new Rectangle2D.Double();
- ArrayList segs = getSegments(s, transform, deviceBounds, false);
- Rectangle2D clipBounds = new Rectangle2D.Double();
- ArrayList clipSegs = getSegments(clip, transform, clipBounds, true);
- segs.addAll(clipSegs);
- Rectangle2D inclClipBounds = new Rectangle2D.Double();
- Rectangle2D.union(clipBounds, deviceBounds, inclClipBounds);
- if (segs.size() > 0)
+ ScanlineConverter sc = getScanlineConverter();
+ int resolution = 0;
+ if (antialias)
{
- if (antialias)
- fillShapeAntialias(segs, deviceBounds, userBounds, inclClipBounds);
- else
- fillShapeImpl(segs, deviceBounds, userBounds, inclClipBounds);
+ // Adjust resolution according to rendering hints.
+ resolution = 2;
}
+ sc.renderShape(this, s, clip, transform, resolution);
}
/**
}
/**
- * Fills the specified polygon without anti-aliasing.
- */
- private void fillShapeImpl(ArrayList segs, Rectangle2D deviceBounds2D,
- Rectangle2D userBounds,
- Rectangle2D inclClipBounds)
- {
- // This is an implementation of a polygon scanline conversion algorithm
- // described here:
- // http://www.cs.berkeley.edu/~ug/slide/pipeline/assignments/scan/
-
- // Create table of all edges.
- // The edge buckets, sorted and indexed by their Y values.
-
- double minX = deviceBounds2D.getMinX();
- double minY = deviceBounds2D.getMinY();
- double maxX = deviceBounds2D.getMaxX();
- double maxY = deviceBounds2D.getMaxY();
- double icMinY = inclClipBounds.getMinY();
- double icMaxY = inclClipBounds.getMaxY();
- Rectangle deviceBounds = new Rectangle((int) minX, (int) minY,
- (int) Math.ceil(maxX) - (int) minX,
- (int) Math.ceil(maxY) - (int) minY);
- PaintContext pCtx = paint.createContext(getColorModel(), deviceBounds,
- userBounds, transform, renderingHints);
-
- ArrayList[] edgeTable = new ArrayList[(int) Math.ceil(icMaxY)
- - (int) Math.ceil(icMinY) + 1];
-
- for (Iterator i = segs.iterator(); i.hasNext();)
- {
- PolyEdge edge = (PolyEdge) i.next();
- int yindex = (int) Math.ceil(edge.y0) - (int) Math.ceil(icMinY);
- if (edgeTable[yindex] == null) // Create bucket when needed.
- edgeTable[yindex] = new ArrayList();
- edgeTable[yindex].add(edge); // Add edge to the bucket of its line.
- }
-
- // TODO: The following could be useful for a future optimization.
-// // Sort all the edges in the edge table within their buckets.
-// for (int y = 0; y < edgeTable.length; y++)
-// {
-// if (edgeTable[y] != null)
-// Collections.sort(edgeTable[y]);
-// }
-
- // The activeEdges list contains all the edges of the current scanline
- // ordered by their intersection points with this scanline.
- ArrayList activeEdges = new ArrayList();
- PolyEdgeComparator comparator = new PolyEdgeComparator();
-
- // Scan all relevant lines.
- int minYInt = (int) Math.ceil(icMinY);
-
- Rectangle devClip = getDeviceBounds();
- int scanlineMax = (int) Math.min(maxY, devClip.getMaxY());
- for (int y = minYInt; y < scanlineMax; y++)
- {
- ArrayList bucket = edgeTable[y - minYInt];
- // Update all the x intersections in the current activeEdges table
- // and remove entries that are no longer in the scanline.
- for (Iterator i = activeEdges.iterator(); i.hasNext();)
- {
- PolyEdge edge = (PolyEdge) i.next();
- if (y > edge.y1)
- i.remove();
- else
- {
- edge.xIntersection += edge.slope;
- //edge.xIntersection = edge.x0 + edge.slope * (y - edge.y0);
- //System.err.println("edge.xIntersection: " + edge.xIntersection);
- }
- }
-
- if (bucket != null)
- activeEdges.addAll(bucket);
-
- // Sort current edges. We are using a bubble sort, because the order
- // of the intersections will not change in most situations. They
- // will only change, when edges intersect each other.
- int size = activeEdges.size();
- if (size > 1)
- {
- for (int i = 1; i < size; i++)
- {
- PolyEdge e1 = (PolyEdge) activeEdges.get(i - 1);
- PolyEdge e2 = (PolyEdge) activeEdges.get(i);
- if (comparator.compare(e1, e2) > 0)
- {
- // Swap e2 with its left neighbor until it 'fits'.
- int j = i;
- do
- {
- activeEdges.set(j, e1);
- activeEdges.set(j - 1, e2);
- j--;
- if (j >= 1)
- e1 = (PolyEdge) activeEdges.get(j - 1);
- } while (j >= 1 && comparator.compare(e1, e2) > 0);
- }
- }
- }
-
- // Now draw all pixels inside the polygon.
- // This is the last edge that intersected the scanline.
- PolyEdge previous = null; // Gets initialized below.
- boolean insideShape = false;
- boolean insideClip = false;
- //System.err.println("scanline: " + y);
- for (Iterator i = activeEdges.iterator(); i.hasNext();)
- {
- PolyEdge edge = (PolyEdge) i.next();
- if (edge.y1 <= y)
- continue;
-
- // Draw scanline when we are inside the shape AND inside the
- // clip.
- if (insideClip && insideShape)
- {
- int x0 = (int) previous.xIntersection;
- int x1 = (int) edge.xIntersection;
- if (x0 < x1)
- fillScanline(pCtx, x0, x1, y);
- }
- // Update state.
- previous = edge;
- if (edge.isClip)
- insideClip = ! insideClip;
- else
- insideShape = ! insideShape;
- }
- }
- pCtx.dispose();
- }
-
- /**
* Paints a scanline between x0 and x1. Override this when your backend
* can efficiently draw/fill horizontal lines.
*
* @param x1 the right offset
* @param y the scanline
*/
- protected void fillScanline(PaintContext pCtx, int x0, int x1, int y)
+ protected void fillScanline(int x0, int x1, int y)
{
+ PaintContext pCtx = paintContext;
Raster paintRaster = pCtx.getRaster(x0, y, x1 - x0, 1);
ColorModel paintColorModel = pCtx.getColorModel();
CompositeContext cCtx = composite.createContext(paintColorModel,
cCtx.dispose();
}
- /**
- * Fills arbitrary shapes in an anti-aliased fashion.
- *
- * @param segs the line segments which define the shape which is to be filled
- */
- private void fillShapeAntialias(ArrayList segs, Rectangle2D deviceBounds2D,
- Rectangle2D userBounds,
- Rectangle2D inclClipBounds)
- {
- // This is an implementation of a polygon scanline conversion algorithm
- // described here:
- // http://www.cs.berkeley.edu/~ug/slide/pipeline/assignments/scan/
- // The antialiasing is implemented using a sampling technique, we do
- // not scan whole lines but fractions of the line.
-
- double minX = deviceBounds2D.getMinX();
- double minY = deviceBounds2D.getMinY();
- double maxX = deviceBounds2D.getMaxX();
- double maxY = deviceBounds2D.getMaxY();
- double icMinY = inclClipBounds.getMinY();
- double icMaxY = inclClipBounds.getMaxY();
- double icMinX = inclClipBounds.getMinX();
- double icMaxX = inclClipBounds.getMaxX();
- Rectangle deviceBounds = new Rectangle((int) minX, (int) minY,
- (int) Math.ceil(maxX) - (int) minX,
- (int) Math.ceil(maxY) - (int) minY);
- PaintContext pCtx = paint.createContext(ColorModel.getRGBdefault(),
- deviceBounds,
- userBounds, transform,
- renderingHints);
-
- // This array will contain the oversampled transparency values for
- // each pixel in the scanline.
- int numScanlines = (int) Math.ceil(icMaxY) - (int) icMinY;
- int numScanlinePixels = (int) Math.ceil(icMaxX) - (int) icMinX + 1;
- if (alpha == null || alpha.length < (numScanlinePixels + 1))
- alpha = new int[numScanlinePixels + 1];
-
- int firstLine = (int) icMinY;
- //System.err.println("minY: " + minY);
- int firstSubline = (int) (Math.ceil((icMinY - Math.floor(icMinY)) * AA_SAMPLING));
- double firstLineDouble = firstLine + firstSubline / (double) AA_SAMPLING;
- //System.err.println("firstSubline: " + firstSubline);
-
- // Create table of all edges.
- // The edge buckets, sorted and indexed by their Y values.
- //System.err.println("numScanlines: " + numScanlines);
- if (edgeTable == null
- || edgeTable.length < numScanlines * AA_SAMPLING + AA_SAMPLING)
- edgeTable = new ArrayList[numScanlines * AA_SAMPLING + AA_SAMPLING];
-
- //System.err.println("firstLineDouble: " + firstLineDouble);
-
- for (Iterator i = segs.iterator(); i.hasNext();)
- {
- PolyEdge edge = (PolyEdge) i.next();
- int yindex = (int) (Math.ceil((edge.y0 - firstLineDouble) * AA_SAMPLING));
- //System.err.println("yindex: " + yindex + " for y0: " + edge.y0);
- // Initialize edge's slope and initial xIntersection.
- edge.slope = ((edge.x1 - edge.x0) / (edge.y1 - edge.y0)) / AA_SAMPLING;
- if (edge.y0 == edge.y1) // Horizontal edge.
- edge.xIntersection = Math.min(edge.x0, edge.x1);
- else
- {
- double alignedFirst = Math.ceil(edge.y0 * AA_SAMPLING) / AA_SAMPLING;
- edge.xIntersection = edge.x0 + (edge.slope * AA_SAMPLING) * (alignedFirst - edge.y0);
- }
- //System.err.println(edge);
- // FIXME: Sanity check should not be needed when clipping works.
- if (yindex >= 0 && yindex < edgeTable.length)
- {
- if (edgeTable[yindex] == null) // Create bucket when needed.
- edgeTable[yindex] = new ArrayList();
- edgeTable[yindex].add(edge); // Add edge to the bucket of its line.
- }
- }
-
- // The activeEdges list contains all the edges of the current scanline
- // ordered by their intersection points with this scanline.
- ArrayList activeEdges = new ArrayList();
- PolyEdgeComparator comparator = new PolyEdgeComparator();
-
- // Scan all lines.
- int yindex = 0;
- //System.err.println("firstLine: " + firstLine + ", maxY: " + maxY + ", firstSubline: " + firstSubline);
- for (int y = firstLine; y <= icMaxY; y++)
- {
- int leftX = (int) icMaxX;
- int rightX = (int) icMinX;
- boolean emptyScanline = true;
- for (int subY = firstSubline; subY < AA_SAMPLING; subY++)
- {
- //System.err.println("scanline: " + y + ", subScanline: " + subY);
- ArrayList bucket = edgeTable[yindex];
- // Update all the x intersections in the current activeEdges table
- // and remove entries that are no longer in the scanline.
- for (Iterator i = activeEdges.iterator(); i.hasNext();)
- {
- PolyEdge edge = (PolyEdge) i.next();
- // TODO: Do the following using integer arithmetics.
- if ((y + ((double) subY / (double) AA_SAMPLING)) > edge.y1)
- i.remove();
- else
- {
- edge.xIntersection += edge.slope;
- //System.err.println("edge: " + edge);
- //edge.xIntersection = edge.x0 + edge.slope * (y - edge.y0);
- //System.err.println("edge.xIntersection: " + edge.xIntersection);
- }
- }
-
- if (bucket != null)
- {
- activeEdges.addAll(bucket);
- edgeTable[yindex].clear();
- }
-
- // Sort current edges. We are using a bubble sort, because the order
- // of the intersections will not change in most situations. They
- // will only change, when edges intersect each other.
- int size = activeEdges.size();
- if (size > 1)
- {
- for (int i = 1; i < size; i++)
- {
- PolyEdge e1 = (PolyEdge) activeEdges.get(i - 1);
- PolyEdge e2 = (PolyEdge) activeEdges.get(i);
- if (comparator.compare(e1, e2) > 0)
- {
- // Swap e2 with its left neighbor until it 'fits'.
- int j = i;
- do
- {
- activeEdges.set(j, e1);
- activeEdges.set(j - 1, e2);
- j--;
- if (j >= 1)
- e1 = (PolyEdge) activeEdges.get(j - 1);
- } while (j >= 1 && comparator.compare(e1, e2) > 0);
- }
- }
- }
-
- // Now draw all pixels inside the polygon.
- // This is the last edge that intersected the scanline.
- PolyEdge previous = null; // Gets initialized below.
- boolean insideClip = false;
- boolean insideShape = false;
- //System.err.println("scanline: " + y + ", subscanline: " + subY);
- for (Iterator i = activeEdges.iterator(); i.hasNext();)
- {
- PolyEdge edge = (PolyEdge) i.next();
- if (edge.y1 <= (y + (subY / (double) AA_SAMPLING)))
- continue;
-
- if (insideClip && insideShape)
- {
- // TODO: Use integer arithmetics here.
- if (edge.y1 > (y + (subY / (double) AA_SAMPLING)))
- {
- //System.err.println(edge);
- // TODO: Eliminate the aligments.
- int x0 = (int) Math.min(Math.max(previous.xIntersection, minX), maxX);
- int x1 = (int) Math.min(Math.max(edge.xIntersection, minX), maxX);
- //System.err.println("minX: " + minX + ", x0: " + x0 + ", x1: " + x1 + ", maxX: " + maxX);
- // TODO: Pull out cast.
- int left = x0 - (int) minX;
- int right = x1 - (int) minX + 1;
- alpha[left]++;
- alpha[right]--;
- leftX = Math.min(x0, leftX);
- rightX = Math.max(x1+2, rightX);
- emptyScanline = false;
- }
- }
- previous = edge;
- if (edge.isClip)
- insideClip = ! insideClip;
- else
- insideShape = ! insideShape;
- }
- yindex++;
- }
- firstSubline = 0;
- // Render full scanline.
- //System.err.println("scanline: " + y);
- if (! emptyScanline)
- fillScanlineAA(alpha, leftX, y, rightX - leftX, pCtx, (int) minX);
- }
-
- pCtx.dispose();
- }
/**
* Fills a horizontal line between x0 and x1 for anti aliased rendering.
cCtx.dispose();
}
-
/**
* Initializes this graphics object. This must be called by subclasses in
* order to correctly initialize the state of this object.
protected void init()
{
setPaint(Color.BLACK);
- setFont(new Font("SansSerif", Font.PLAIN, 12));
+ setFont(FONT);
isOptimized = true;
-
- // FIXME: Should not be necessary. A clip of null should mean
- // 'clip against device bounds.
- destinationRaster = getDestinationRaster();
- clip = getDeviceBounds();
}
/**
}
/**
- * Converts the specified shape into a list of segments.
- *
- * @param s the shape to convert
- * @param t the transformation to apply before converting
- * @param deviceBounds an output parameter; holds the bounding rectangle of
- * s in device space after return
- * @param isClip true when the shape is a clip, false for normal shapes;
- * this influences the settings in the created PolyEdge instances.
+ * Returns the ShapeCache for the calling thread.
*
- * @return a list of PolyEdge that form the shape in device space
+ * @return the ShapeCache for the calling thread
*/
- private ArrayList getSegments(Shape s, AffineTransform t,
- Rectangle2D deviceBounds, boolean isClip)
+ private ShapeCache getShapeCache()
{
- // Flatten the path. TODO: Determine the best flattening factor
- // wrt to speed and quality.
- PathIterator path = s.getPathIterator(getTransform(), 1.0);
-
- // Build up polygons and let the native backend render this using
- // rawFillShape() which would provide a default implementation for
- // drawPixel using a PolyScan algorithm.
- double[] seg = new double[6];
-
- // TODO: Use ArrayList<PolyEdge> here when availble.
- ArrayList segs = new ArrayList();
- double segX = 0.; // The start point of the current edge.
- double segY = 0.;
- double polyX = 0.; // The start point of the current polygon.
- double polyY = 0.;
-
- double minX = Integer.MAX_VALUE;
- double maxX = Integer.MIN_VALUE;
- double minY = Integer.MAX_VALUE;
- double maxY = Integer.MIN_VALUE;
-
- //System.err.println("fill polygon");
- while (! path.isDone())
+ ShapeCache sc = shapeCache.get();
+ if (sc == null)
{
- int segType = path.currentSegment(seg);
- minX = Math.min(minX, seg[0]);
- maxX = Math.max(maxX, seg[0]);
- minY = Math.min(minY, seg[1]);
- maxY = Math.max(maxY, seg[1]);
-
- //System.err.println("segment: " + segType + ", " + seg[0] + ", " + seg[1]);
- if (segType == PathIterator.SEG_MOVETO)
- {
- segX = seg[0];
- segY = seg[1];
- polyX = seg[0];
- polyY = seg[1];
- }
- else if (segType == PathIterator.SEG_CLOSE)
- {
- // Close the polyline.
- PolyEdge edge = new PolyEdge(segX, segY,
- polyX, polyY, isClip);
- segs.add(edge);
- }
- else if (segType == PathIterator.SEG_LINETO)
- {
- PolyEdge edge = new PolyEdge(segX, segY,
- seg[0], seg[1], isClip);
- segs.add(edge);
- segX = seg[0];
- segY = seg[1];
- }
- path.next();
+ sc = new ShapeCache();
+ shapeCache.set(sc);
}
- deviceBounds.setRect(minX, minY, maxX - minX, maxY - minY);
- return segs;
+ return sc;
}
/**
- * Returns the ShapeCache for the calling thread.
+ * Returns the scanline converter for this thread.
*
- * @return the ShapeCache for the calling thread
+ * @return the scanline converter for this thread
*/
- private ShapeCache getShapeCache()
+ private ScanlineConverter getScanlineConverter()
{
- ShapeCache sc = (ShapeCache) shapeCache.get();
+ ScanlineConverter sc = scanlineConverters.get();
if (sc == null)
{
- sc = new ShapeCache();
- shapeCache.set(sc);
+ sc = new ScanlineConverter();
+ scanlineConverters.set(sc);
}
return sc;
}