X-Git-Url: http://git.megacz.com/?p=org.ibex.core.git;a=blobdiff_plain;f=src%2Forg%2Fibex%2Fgraphics%2FPath.java;fp=src%2Forg%2Fibex%2Fgraphics%2FPath.java;h=79fecba5185e8f04eaa9cfe946f02399bf4873d5;hp=0000000000000000000000000000000000000000;hb=4daeeb4119b901d53b44913c86f8af3ce67db925;hpb=da1f843588c8bd2b2c7cc74a5b4ffff8d57ab712 diff --git a/src/org/ibex/graphics/Path.java b/src/org/ibex/graphics/Path.java new file mode 100644 index 0000000..79fecba --- /dev/null +++ b/src/org/ibex/graphics/Path.java @@ -0,0 +1,814 @@ +// FIXME +// Copyright 2002 Adam Megacz, see the COPYING file for licensing [GPL] +package org.ibex; +import java.util.*; + +// FIXME: offer a "subpixel" mode where we pass floats to the Platform and don't do any snapping +// FIXME: fracture when realizing instead of when parsing? + +/* + v1.0 + - textpath + - gradients + - patterns + - clipping/masking + - filters (filtering of a group must be performed AFTER the group is assembled; sep. canvas) + + v1.1 + - bump caps [requires Paint that can fill circles...] [remember to distinguish between closed/unclosed] + - line joins + - mitre (hard) + - bevel (easy) + - bump (easy, but requires 'round' Paint) + - subtree sharing? otherwise the memory consumption might be outrageous... clone="" attribute? + - better clipping + - intersect clip regions (linearity) + - clip on trapezoids, not pixels + - faster gradients and patterns: + - transform each corner of the trapezoid and then interpolate +*/ + +/** Ibex's fully conformant Static SVG Viewer; see SVG spec, section G.7 */ +public final class VectorGraphics { + + // Private Constants /////////////////////////////////////////////////////////////////// + + private static final int DEFAULT_PATHLEN = 1000; + private static final float PI = (float)Math.PI; + + + // Public entry points ///////////////////////////////////////////////////////////////// + + public static VectorPath parseVectorPath(String s) { + if (s == null) return null; + PathTokenizer t = new PathTokenizer(s); + VectorPath ret = new VectorPath(); + char last_command = 'M'; + boolean first = true; + while(t.hasMoreTokens()) { + char command = t.parseCommand(); + if (first && command != 'M') throw new RuntimeException("the first command of a path must be 'M'"); + first = false; + boolean relative = Character.toLowerCase(command) == command; + command = Character.toLowerCase(command); + ret.parseSingleCommandAndArguments(t, command, relative); + last_command = command; + } + return ret; + } + + + // Affine ////////////////////////////////////////////////////////////////////////////// + + /** an affine transform; all operations are destructive */ + public static final class Affine { + + // [ a b e ] + // [ c d f ] + // [ 0 0 1 ] + public float a, b, c, d, e, f; + + Affine(float _a, float _b, float _c, float _d, float _e, float _f) { a = _a; b = _b; c = _c; d = _d; e = _e; f = _f; } + public String toString() { return "[ " + a + ", " + b + ", " + c + ", " + d + ", " + e + ", " + f + " ]"; } + public Affine copy() { return new Affine(a, b, c, d, e, f); } + public static Affine identity() { return new Affine(1, 0, 0, 1, 0, 0); } + public static Affine scale(float sx, float sy) { return new Affine(sx, 0, 0, sy, 0, 0); } + public static Affine shear(float degrees) { + return new Affine(1, 0, (float)Math.tan(degrees * (float)(Math.PI / 180.0)), 1, 0, 0); } + public static Affine translate(float tx, float ty) { return new Affine(1, 0, 0, 1, tx, ty); } + public static Affine flip(boolean horiz, boolean vert) { return new Affine(horiz ? -1 : 1, 0, 0, vert ? -1 : 1, 0, 0); } + public float multiply_px(float x, float y) { return x * a + y * c + e; } + public float multiply_py(float x, float y) { return x * b + y * d + f; } + public boolean equalsIgnoringTranslation(Affine x) { return a == x.a && b == x.b && c == x.c && d == x.d; } + + public boolean equals(Object o) { + if (!(o instanceof Affine)) return false; + Affine x = (Affine)o; + return a == x.a && b == x.b && c == x.c && d == x.d && e == x.e && f == x.f; + } + + public static Affine rotate(float degrees) { + float s = (float)Math.sin(degrees * (float)(Math.PI / 180.0)); + float c = (float)Math.cos(degrees * (float)(Math.PI / 180.0)); + return new Affine(c, s, -s, c, 0, 0); + } + + /** this = this * a */ + public Affine multiply(Affine A) { + float _a = this.a * A.a + this.b * A.c; + float _b = this.a * A.b + this.b * A.d; + float _c = this.c * A.a + this.d * A.c; + float _d = this.c * A.b + this.d * A.d; + float _e = this.e * A.a + this.f * A.c + A.e; + float _f = this.e * A.b + this.f * A.d + A.f; + a = _a; b = _b; c = _c; d = _d; e = _e; f = _f; + return this; + } + + public void invert() { + float det = 1 / (a * d - b * c); + float _a = d * det; + float _b = -1 * b * det; + float _c = -1 * c * det; + float _d = a * det; + float _e = -1 * e * a - f * c; + float _f = -1 * e * b - f * d; + a = _a; b = _b; c = _c; d = _d; e = _e; f = _f; + } + } + + + // PathTokenizer ////////////////////////////////////////////////////////////////////////////// + + public static Affine parseTransform(String t) { + if (t == null) return null; + t = t.trim(); + Affine ret = VectorGraphics.Affine.identity(); + while (t.length() > 0) { + if (t.startsWith("skewX(")) { + // FIXME + + } else if (t.startsWith("shear(")) { + // FIXME: nonstandard; remove this + ret.multiply(VectorGraphics.Affine.shear(Float.parseFloat(t.substring(t.indexOf('(') + 1, t.indexOf(')'))))); + + } else if (t.startsWith("skewY(")) { + // FIXME + + } else if (t.startsWith("rotate(")) { + String sub = t.substring(t.indexOf('(') + 1, t.indexOf(')')); + if (sub.indexOf(',') != -1) { + float angle = Float.parseFloat(sub.substring(0, sub.indexOf(','))); + sub = sub.substring(sub.indexOf(',') + 1); + float cx = Float.parseFloat(sub.substring(0, sub.indexOf(','))); + sub = sub.substring(sub.indexOf(',') + 1); + float cy = Float.parseFloat(sub); + ret.multiply(VectorGraphics.Affine.translate(cx, cy)); + ret.multiply(VectorGraphics.Affine.rotate(angle)); + ret.multiply(VectorGraphics.Affine.translate(-1 * cx, -1 * cy)); + } else { + ret.multiply(VectorGraphics.Affine.rotate(Float.parseFloat(t.substring(t.indexOf('(') + 1, t.indexOf(')'))))); + } + + } else if (t.startsWith("translate(")) { + String sub = t.substring(t.indexOf('(') + 1, t.indexOf(')')); + if (sub.indexOf(',') > -1) { + ret.multiply(VectorGraphics.Affine.translate(Float.parseFloat(t.substring(t.indexOf('(') + 1, t.indexOf(','))), + Float.parseFloat(t.substring(t.indexOf(',') + 1, t.indexOf(')'))))); + } else { + ret.multiply(VectorGraphics.Affine.translate(Float.parseFloat(t.substring(t.indexOf('(') + 1, t.indexOf(','))), 0)); + } + + } else if (t.startsWith("flip(")) { + String which = t.substring(t.indexOf('(') + 1, t.indexOf(')')); + ret.multiply(VectorGraphics.Affine.flip(which.equals("horizontal"), which.equals("vertical"))); + + } else if (t.startsWith("scale(")) { + String sub = t.substring(t.indexOf('(') + 1, t.indexOf(')')); + if (sub.indexOf(',') > -1) { + ret.multiply(VectorGraphics.Affine.scale(Float.parseFloat(t.substring(t.indexOf('(') + 1, t.indexOf(','))), + Float.parseFloat(t.substring(t.indexOf(',') + 1, t.indexOf(')'))))); + } else { + ret.multiply(VectorGraphics.Affine.scale(Float.parseFloat(t.substring(t.indexOf('(') + 1, t.indexOf(','))), + Float.parseFloat(t.substring(t.indexOf('(') + 1, t.indexOf(','))))); + } + + } else if (t.startsWith("matrix(")) { + // FIXME: is this mapped right? + float d[] = new float[6]; + StringTokenizer st = new StringTokenizer(t, ",", false); + for(int i=0; i<6; i++) + d[i] = Float.parseFloat(st.nextToken()); + ret.multiply(new VectorGraphics.Affine(d[0], d[1], d[2], d[3], d[4], d[5])); + } + t = t.substring(t.indexOf(')') + 1).trim(); + } + return ret; + } + + public static final float PX_PER_INCH = 72; + public static final float INCHES_PER_CM = (float)0.3937; + public static final float INCHES_PER_MM = INCHES_PER_CM / 10; + + public static class PathTokenizer { + // FIXME: check array bounds exception for improperly terminated string + String s; + int i = 0; + char lastCommand = 'M'; + public PathTokenizer(String s) { this.s = s; } + private void consumeWhitespace() { + while(i < s.length() && (Character.isWhitespace(s.charAt(i)))) i++; + if (i < s.length() && s.charAt(i) == ',') i++; + while(i < s.length() && (Character.isWhitespace(s.charAt(i)))) i++; + } + public boolean hasMoreTokens() { consumeWhitespace(); return i < s.length(); } + public char parseCommand() { + consumeWhitespace(); + char c = s.charAt(i); + if (!Character.isLetter(c)) return lastCommand; + i++; + return lastCommand = c; + } + public float parseFloat() { + consumeWhitespace(); + int start = i; + float multiplier = 1; + for(; i < s.length(); i++) { + char c = s.charAt(i); + if (Character.isWhitespace(c) || c == ',' || (c == '-' && i != start)) break; + if (!((c >= '0' && c <= '9') || c == '.' || c == 'e' || c == 'E' || c == '-')) { + if (c == '%') { // FIXME + } else if (s.regionMatches(i, "pt", 0, i+2)) { // FIXME + } else if (s.regionMatches(i, "em", 0, i+2)) { // FIXME + } else if (s.regionMatches(i, "pc", 0, i+2)) { // FIXME + } else if (s.regionMatches(i, "ex", 0, i+2)) { // FIXME + } else if (s.regionMatches(i, "mm", 0, i+2)) { i += 2; multiplier = INCHES_PER_MM * PX_PER_INCH; break; + } else if (s.regionMatches(i, "cm", 0, i+2)) { i += 2; multiplier = INCHES_PER_CM * PX_PER_INCH; break; + } else if (s.regionMatches(i, "in", 0, i+2)) { i += 2; multiplier = PX_PER_INCH; break; + } else if (s.regionMatches(i, "px", 0, i+2)) { i += 2; break; + } else if (Character.isLetter(c)) break; + throw new RuntimeException("didn't expect character \"" + c + "\" in a numeric constant"); + } + } + if (start == i) throw new RuntimeException("FIXME"); + return Float.parseFloat(s.substring(start, i)) * multiplier; + } + } + + + // Abstract Path ////////////////////////////////////////////////////////////////////////////// + + /** an abstract path; may contain splines and arcs */ + public static class VectorPath { + + // the number of vertices on this path + int numvertices = 0; + + // the vertices of the path + float[] x = new float[DEFAULT_PATHLEN]; + float[] y = new float[DEFAULT_PATHLEN]; + + // the type of each edge; type[i] is the type of the edge from x[i],y[i] to x[i+1],y[i+1] + byte[] type = new byte[DEFAULT_PATHLEN]; + + // bezier control points + float[] c1x = new float[DEFAULT_PATHLEN]; // or rx (arcto) + float[] c1y = new float[DEFAULT_PATHLEN]; // or ry (arcto) + float[] c2x = new float[DEFAULT_PATHLEN]; // or x-axis-rotation (arcto) + float[] c2y = new float[DEFAULT_PATHLEN]; // or large-arc << 1 | sweep (arcto) + + boolean closed = false; + + static final byte TYPE_MOVETO = 0; + static final byte TYPE_LINETO = 1; + static final byte TYPE_ARCTO = 2; + static final byte TYPE_CUBIC = 3; + static final byte TYPE_QUADRADIC = 4; + + /** Creates a concrete vector path transformed through the given matrix. */ + public RasterPath realize(Affine a) { + + RasterPath ret = new RasterPath(); + int NUMSTEPS = 5; // FIXME + ret.numvertices = 1; + ret.x[0] = (int)Math.round(a.multiply_px(x[0], y[0])); + ret.y[0] = (int)Math.round(a.multiply_py(x[0], y[0])); + + for(int i=1; i> 1; + float fs = ((int)c2y[i]) & 1; + float x1 = x[i]; + float y1 = y[i]; + float x2 = x[i+1]; + float y2 = y[i+1]; + + // F.6.5: given x1,y1,x2,y2,fa,fs, compute cx,cy,theta1,dtheta + float x1_ = (float)Math.cos(phi) * (x1 - x2) / 2 + (float)Math.sin(phi) * (y1 - y2) / 2; + float y1_ = -1 * (float)Math.sin(phi) * (x1 - x2) / 2 + (float)Math.cos(phi) * (y1 - y2) / 2; + float tmp = (float)Math.sqrt((rx * rx * ry * ry - rx * rx * y1_ * y1_ - ry * ry * x1_ * x1_) / + (rx * rx * y1_ * y1_ + ry * ry * x1_ * x1_)); + float cx_ = (fa == fs ? -1 : 1) * tmp * (rx * y1_ / ry); + float cy_ = (fa == fs ? -1 : 1) * -1 * tmp * (ry * x1_ / rx); + float cx = (float)Math.cos(phi) * cx_ - (float)Math.sin(phi) * cy_ + (x1 + x2) / 2; + float cy = (float)Math.sin(phi) * cx_ + (float)Math.cos(phi) * cy_ + (y1 + y2) / 2; + + // F.6.4 Conversion from center to endpoint parameterization + float ux = 1, uy = 0, vx = (x1_ - cx_) / rx, vy = (y1_ - cy_) / ry; + float det = ux * vy - uy * vx; + float theta1 = (det < 0 ? -1 : 1) * + (float)Math.acos((ux * vx + uy * vy) / + ((float)Math.sqrt(ux * ux + uy * uy) * (float)Math.sqrt(vx * vx + vy * vy))); + ux = (x1_ - cx_) / rx; uy = (y1_ - cy_) / ry; + vx = (-1 * x1_ - cx_) / rx; vy = (-1 * y1_ - cy_) / ry; + det = ux * vy - uy * vx; + float dtheta = (det < 0 ? -1 : 1) * + (float)Math.acos((ux * vx + uy * vy) / + ((float)Math.sqrt(ux * ux + uy * uy) * (float)Math.sqrt(vx * vx + vy * vy))); + dtheta = dtheta % (float)(2 * Math.PI); + + if (fs == 0 && dtheta > 0) theta1 -= 2 * PI; + if (fs == 1 && dtheta < 0) theta1 += 2 * PI; + + if (fa == 1 && dtheta < 0) dtheta = 2 * PI + dtheta; + else if (fa == 1 && dtheta > 0) dtheta = -1 * (2 * PI - dtheta); + + // FIXME: integrate F.6.6 + // FIXME: isn't quite ending where it should... + + // F.6.3: Parameterization alternatives + float theta = theta1; + for(int j=0; j 0) ret.sort(0, ret.numedges - 1, false); + return ret; + } + + protected void parseSingleCommandAndArguments(PathTokenizer t, char command, boolean relative) { + if (numvertices == 0 && command != 'm') throw new RuntimeException("first command MUST be an 'm'"); + if (numvertices > x.length - 2) { + float[] new_x = new float[x.length * 2]; System.arraycopy(x, 0, new_x, 0, x.length); x = new_x; + float[] new_y = new float[y.length * 2]; System.arraycopy(y, 0, new_y, 0, y.length); y = new_y; + } + switch(command) { + case 'z': { + int where; + type[numvertices-1] = TYPE_LINETO; + for(where = numvertices - 1; where > 0; where--) + if (type[where - 1] == TYPE_MOVETO) break; + x[numvertices] = x[where]; + y[numvertices] = y[where]; + numvertices++; + closed = true; + break; + } + + case 'm': { + if (numvertices > 0) type[numvertices-1] = TYPE_MOVETO; + x[numvertices] = t.parseFloat() + (relative ? x[numvertices - 1] : 0); + y[numvertices] = t.parseFloat() + (relative ? y[numvertices - 1] : 0); + numvertices++; + break; + } + + case 'l': case 'h': case 'v': { + type[numvertices-1] = TYPE_LINETO; + float first = t.parseFloat(), second; + if (command == 'h') { + second = relative ? 0 : y[numvertices - 1]; + } else if (command == 'v') { + second = first; first = relative ? 0 : x[numvertices - 1]; + } else { + second = t.parseFloat(); + } + x[numvertices] = first + (relative ? x[numvertices - 1] : 0); + y[numvertices] = second + (relative ? y[numvertices - 1] : 0); + numvertices++; + break; + } + + case 'a': { + type[numvertices-1] = TYPE_ARCTO; + c1x[numvertices-1] = t.parseFloat() + (relative ? x[numvertices - 1] : 0); + c1y[numvertices-1] = t.parseFloat() + (relative ? y[numvertices - 1] : 0); + c2x[numvertices-1] = (t.parseFloat() / 360) * 2 * PI; + c2y[numvertices-1] = (((int)t.parseFloat()) << 1) | (int)t.parseFloat(); + x[numvertices] = t.parseFloat() + (relative ? x[numvertices - 1] : 0); + y[numvertices] = t.parseFloat() + (relative ? y[numvertices - 1] : 0); + numvertices++; + break; + } + + case 's': case 'c': { + type[numvertices-1] = TYPE_CUBIC; + if (command == 'c') { + c1x[numvertices-1] = t.parseFloat() + (relative ? x[numvertices - 1] : 0); + c1y[numvertices-1] = t.parseFloat() + (relative ? y[numvertices - 1] : 0); + } else if (numvertices > 1 && type[numvertices-2] == TYPE_CUBIC) { + c1x[numvertices-1] = 2 * x[numvertices - 1] - c2x[numvertices-2]; + c1y[numvertices-1] = 2 * y[numvertices - 1] - c2y[numvertices-2]; + } else { + c1x[numvertices-1] = x[numvertices-1]; + c1y[numvertices-1] = y[numvertices-1]; + } + c2x[numvertices-1] = t.parseFloat() + (relative ? x[numvertices - 1] : 0); + c2y[numvertices-1] = t.parseFloat() + (relative ? y[numvertices - 1] : 0); + x[numvertices] = t.parseFloat() + (relative ? x[numvertices - 1] : 0); + y[numvertices] = t.parseFloat() + (relative ? y[numvertices - 1] : 0); + numvertices++; + break; + } + + case 't': case 'q': { + type[numvertices-1] = TYPE_QUADRADIC; + if (command == 'q') { + c1x[numvertices-1] = t.parseFloat() + (relative ? x[numvertices - 1] : 0); + c1y[numvertices-1] = t.parseFloat() + (relative ? y[numvertices - 1] : 0); + } else if (numvertices > 1 && type[numvertices-2] == TYPE_QUADRADIC) { + c1x[numvertices-1] = 2 * x[numvertices - 1] - c1x[numvertices-2]; + c1y[numvertices-1] = 2 * y[numvertices - 1] - c1y[numvertices-2]; + } else { + c1x[numvertices-1] = x[numvertices-1]; + c1y[numvertices-1] = y[numvertices-1]; + } + x[numvertices] = t.parseFloat() + (relative ? x[numvertices - 1] : 0); + y[numvertices] = t.parseFloat() + (relative ? y[numvertices - 1] : 0); + numvertices++; + break; + } + + default: + // FIXME + } + + /* + // invariant: after this loop, no two lines intersect other than at a vertex + // FIXME: cleanup + int index = numvertices - 2; + for(int i=0; i Math.min(x[i+1], x[i]) && _x < Math.max(x[i+1], x[i]) && + _x > Math.min(x[j+1], x[j]) && _x < Math.max(x[j+1], x[j])) { + // FIXME: something's not right in here. See if we can do without fracturing line 'i'. + for(int k = ++numvertices; k>i; k--) { x[k] = x[k - 1]; y[k] = y[k - 1]; } + x[i+1] = _x; + y[i+1] = _y; + x[numvertices] = x[numvertices - 1]; x[numvertices - 1] = _x; + y[numvertices] = y[numvertices - 1]; y[numvertices - 1] = _y; + edges[numedges++] = numvertices - 1; numvertices++; + index++; + break; // actually 'continue' the outermost loop + } + } + } + */ + + } + } + + + + + // Rasterized Vector Path ////////////////////////////////////////////////////////////////////////////// + + /** a vector path */ + public static class RasterPath { + + // the vertices of this path + int[] x = new int[DEFAULT_PATHLEN]; + int[] y = new int[DEFAULT_PATHLEN]; + int numvertices = 0; + + /** + * A list of the vertices on this path which *start* an *edge* (rather than a moveto), sorted by increasing y. + * example: x[edges[1]],y[edges[1]] - x[edges[i]+1],y[edges[i]+1] is the second-topmost edge + * note that if x[i],y[i] - x[i+1],y[i+1] is a MOVETO, then no element in edges will be equal to i + */ + int[] edges = new int[DEFAULT_PATHLEN]; + int numedges = 0; + + /** translate a rasterized path */ + public void translate(int dx, int dy) { for(int i=0; i left && y[edges[--j]] > y[edges[right]]); + if (i >= j) break; + s = edges[i]; edges[i] = edges[j]; edges[j] = s; + } + s = edges[right]; edges[right] = edges[i]; edges[i] = s; + return i; + } else { + if (left >= right) return 0; + int p = sort(left, right, true); + sort(left, p - 1, false); + sort(p + 1, right, false); + return 0; + } + } + + /** finds the x value at which the line intercepts the line y=_y */ + private int intercept(int i, float _y, boolean includeTop, boolean includeBottom) { + if (includeTop ? (_y < Math.min(y[i], y[i+1])) : (_y <= Math.min(y[i], y[i+1]))) + return Integer.MIN_VALUE; + if (includeBottom ? (_y > Math.max(y[i], y[i+1])) : (_y >= Math.max(y[i], y[i+1]))) + return Integer.MIN_VALUE; + return (int)Math.round((((float)(x[i + 1] - x[i])) / + ((float)(y[i + 1] - y[i])) ) * ((float)(_y - y[i])) + x[i]); + } + + /** fill the interior of the path */ + public void fill(PixelBuffer buf, Paint paint) { + if (numedges == 0) return; + int y0 = y[edges[0]], y1 = y0; + boolean useEvenOdd = false; + + // we iterate over all endpoints in increasing y-coordinate order + for(int index = 1; index x1) continue; + if (midpoint == x1 && i >= rightSegment) continue; + rightSegment = i; + x1 = midpoint; + } + if (leftSegment == rightSegment || rightSegment == Integer.MAX_VALUE) break; + if (leftSegment != -1) + if ((useEvenOdd && count % 2 != 0) || (!useEvenOdd && count != 0)) + paint.fillTrapezoid(intercept(edges[leftSegment], y0, true, true), + intercept(edges[rightSegment], y0, true, true), y0, + intercept(edges[leftSegment], y1, true, true), + intercept(edges[rightSegment], y1, true, true), y1, + buf); + if (useEvenOdd) count++; + else count += (y[edges[rightSegment]] < y[edges[rightSegment]+1]) ? -1 : 1; + leftSegment = rightSegment; x0 = x1; + } + } + } + + /** stroke the outline of the path */ + public void stroke(PixelBuffer buf, int width, int color) { stroke(buf, width, color, null, 0, 0); } + public void stroke(PixelBuffer buf, int width, int color, String dashArray, int dashOffset, float segLength) { + + if (dashArray == null) { + for(int i=0; i 0) { + float actualLength = 0; + for(int i=0; i _x2) { float _x0 = _x1; _x1 = _x2; _x2 = _x0; } + + for(float x=_x1; x<_x2; x++) { + + float distance = isLinear ? + // length of projection of onto the gradient vector == { \dot {grad \over |grad|}} + (x * (x2 - x1) + y * (y2 - y1)) / (float)Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)) : + + // radial form is simple! FIXME, not quite right + (float)Math.sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy)); + + // FIXME: offsets are 0..1, not 0..length(gradient) + int i = 0; for(; i= stop_offsets.length) continue; + + // gradate from offsets[i - 1] to offsets[i] + float percentage = ((distance - stop_offsets[i - 1]) / (stop_offsets[i] - stop_offsets[i - 1])); + + int a = (int)((((stop_colors[i] >> 24) & 0xff) - ((stop_colors[i - 1] >> 24) & 0xff)) * percentage) + + ((stop_colors[i - 1] >> 24) & 0xff); + int r = (int)((((stop_colors[i] >> 16) & 0xff) - ((stop_colors[i - 1] >> 16) & 0xff)) * percentage) + + ((stop_colors[i - 1] >> 16) & 0xff); + int g = (int)((((stop_colors[i] >> 8) & 0xff) - ((stop_colors[i - 1] >> 8) & 0xff)) * percentage) + + ((stop_colors[i - 1] >> 8) & 0xff); + int b = (int)((((stop_colors[i] >> 0) & 0xff) - ((stop_colors[i - 1] >> 0) & 0xff)) * percentage) + + ((stop_colors[i - 1] >> 0) & 0xff); + int argb = (a << 24) | (r << 16) | (g << 8) | b; + buf.drawPoint((int)x, (int)Math.floor(y), argb); + } + } + } + } + + public static class LinearGradientPaint extends GradientPaint { + public LinearGradientPaint(float x1, float y1, float x2, float y2, boolean reflect, boolean repeat, + Affine gradientTransform, int[] stop_colors, float[] stop_offsets) { + super(reflect, repeat, gradientTransform, stop_colors, stop_offsets); + this.x1 = x1; this.x2 = x2; this.y1 = y1; this.y2 = y2; + } + float x1 = 0, y1 = 0, x2 = 300, y2 = 300; + } + + public static class RadialGradientPaint extends GradientPaint { + public RadialGradientPaint(float cx, float cy, float fx, float fy, float r, boolean reflect, boolean repeat, + Affine gradientTransform, int[] stop_colors, float[] stop_offsets) { + super(reflect, repeat, gradientTransform, stop_colors, stop_offsets); + this.cx = cx; this.cy = cy; this.fx = fx; this.fy = fy; this.r = r; + } + + float cx, cy, r, fx, fy; + + } + */ +