com.sun.javafx.sg.prism.ShapeEvaluator Maven / Gradle / Ivy
/*
* Copyright (c) 2007, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code 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
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.sun.javafx.sg.prism;
import java.util.Vector;
import com.sun.javafx.geom.FlatteningPathIterator;
import com.sun.javafx.geom.IllegalPathStateException;
import com.sun.javafx.geom.Path2D;
import com.sun.javafx.geom.PathIterator;
import com.sun.javafx.geom.Point2D;
import com.sun.javafx.geom.RectBounds;
import com.sun.javafx.geom.Rectangle;
import com.sun.javafx.geom.Shape;
import com.sun.javafx.geom.transform.BaseTransform;
/**
* A {@link KeyFrame} {@link Evaluator} for {@link Shape} objects.
* This {@code Evaluator} can be used to morph between the geometries
* of two relatively arbitrary shapes with the only restrictions being
* that the two different numbers of subpaths or two shapes with
* disparate winding rules may not blend together in a pleasing
* manner.
* The ShapeEvaluator will do the best job it can if the shapes do
* not match in winding rule or number of subpaths, but the geometry
* of the shapes may need to be adjusted by other means to make the
* shapes more like each other for best aesthetic effect.
*
* Note that the process of comparing two geometries and finding similar
* structures between them to blend for the morphing operation can be
* expensive.
* Instances of {@code ShapeEvaluator} will properly perform the necessary
* geometric analysis of their arguments on every method call and attempt
* to cache the information so that they can operate more quickly if called
* multiple times in a row on the same pair of {@code Shape} objects.
* As a result attempting to mutate a {@code Shape} object that is stored
* in one of their keyframes may not have any effect if the associated
* {@code ShapeEvaluator} has already cached the geometry.
* Also, it is advisable to use different instances of {@code ShapeEvaluator}
* for every pair of keyframes being morphed so that the cached information
* can be reused as much as possible.
*
* An example of proper usage:
*
* SGShape s = ...;
* Shape s0 = ...;
* Shape s1 = ...;
* Shape s2 = ...;
* KeyFrame k0 = KeyFrame.create(0.0f, s0, new ShapeEvaluator());
* KeyFrame k1 = KeyFrame.create(0.6f, s1, new ShapeEvaluator());
* KeyFrame k2 = KeyFrame.create(1.0f, s2, new ShapeEvaluator());
* KeyFrames morphFrames = KeyFrames.create(s, "shape", k0, k1, k2);
* Clip.create(5000, 1, morphFrames).start();
*
*
*/
class ShapeEvaluator {
private Shape savedv0;
private Shape savedv1;
private Geometry geom0;
private Geometry geom1;
public Shape evaluate(Shape v0, Shape v1, float fraction) {
if (savedv0 != v0 || savedv1 != v1) {
if (savedv0 == v1 && savedv1 == v0) {
// Just swap the geometries
Geometry gtmp = geom0;
geom0 = geom1;
geom1 = gtmp;
} else {
recalculate(v0, v1);
}
savedv0 = v0;
savedv1 = v1;
}
return getShape(fraction);
}
private void recalculate(Shape v0, Shape v1) {
geom0 = new Geometry(v0);
geom1 = new Geometry(v1);
float tvals0[] = geom0.getTvals();
float tvals1[] = geom1.getTvals();
float combinedTvals[] = mergeTvals(tvals0, tvals1);
geom0.setTvals(combinedTvals);
geom1.setTvals(combinedTvals);
}
private Shape getShape(float fraction) {
return new MorphedShape(geom0, geom1, fraction);
}
private static float[] mergeTvals(float tvals0[], float tvals1[]) {
int count = sortTvals(tvals0, tvals1, null);
float newtvals[] = new float[count];
sortTvals(tvals0, tvals1, newtvals);
return newtvals;
}
private static int sortTvals(float tvals0[],
float tvals1[],
float newtvals[])
{
int i0 = 0;
int i1 = 0;
int numtvals = 0;
while (i0 < tvals0.length && i1 < tvals1.length) {
float t0 = tvals0[i0];
float t1 = tvals1[i1];
if (t0 <= t1) {
if (newtvals != null) newtvals[numtvals] = t0;
i0++;
}
if (t1 <= t0) {
if (newtvals != null) newtvals[numtvals] = t1;
i1++;
}
numtvals++;
}
return numtvals;
}
private static float interp(float v0, float v1, float t) {
return (v0 + ((v1 - v0) * t));
}
private static class Geometry {
static final float THIRD = (1f / 3f);
static final float MIN_LEN = 0.001f;
float bezierCoords[];
int numCoords;
int windingrule;
float myTvals[];
public Geometry(Shape s) {
// Multiple of 6 plus 2 more for initial moveto
bezierCoords = new float[20];
PathIterator pi = s.getPathIterator(null);
windingrule = pi.getWindingRule();
if (pi.isDone()) {
// We will have 1 segment and it will be all zeros
// It will have 8 coordinates (2 for moveto, 6 for cubic)
numCoords = 8;
}
float coords[] = new float[6];
int type = pi.currentSegment(coords);
pi.next();
if (type != PathIterator.SEG_MOVETO) {
throw new IllegalPathStateException("missing initial moveto");
}
float curx, cury, movx, movy;
bezierCoords[0] = curx = movx = coords[0];
bezierCoords[1] = cury = movy = coords[1];
float newx, newy;
Vector savedpathendpoints = new Vector<>();
numCoords = 2;
while (!pi.isDone()) {
switch (pi.currentSegment(coords)) {
case PathIterator.SEG_MOVETO:
if (curx != movx || cury != movy) {
appendLineTo(curx, cury, movx, movy);
curx = movx;
cury = movy;
}
newx = coords[0];
newy = coords[1];
if (curx != newx || cury != newy) {
savedpathendpoints.add(new Point2D(movx, movy));
appendLineTo(curx, cury, newx, newy);
curx = movx = newx;
cury = movy = newy;
}
break;
case PathIterator.SEG_CLOSE:
if (curx != movx || cury != movy) {
appendLineTo(curx, cury, movx, movy);
curx = movx;
cury = movy;
}
break;
case PathIterator.SEG_LINETO:
newx = coords[0];
newy = coords[1];
appendLineTo(curx, cury, newx, newy);
curx = newx;
cury = newy;
break;
case PathIterator.SEG_QUADTO:
float ctrlx = coords[0];
float ctrly = coords[1];
newx = coords[2];
newy = coords[3];
appendQuadTo(curx, cury, ctrlx, ctrly, newx, newy);
curx = newx;
cury = newy;
break;
case PathIterator.SEG_CUBICTO:
appendCubicTo(coords[0], coords[1],
coords[2], coords[3],
curx = coords[4], cury = coords[5]);
break;
}
pi.next();
}
// Add closing segment if either:
// - we only have initial moveto - expand it to an empty cubic
// - or we are not back to the starting point
if ((numCoords < 8) || curx != movx || cury != movy) {
appendLineTo(curx, cury, movx, movy);
curx = movx;
cury = movy;
}
// Now retrace our way back through all of the connecting
// inter-subpath segments
for (int i = savedpathendpoints.size()-1; i >= 0; i--) {
Point2D p = savedpathendpoints.get(i);
newx = p.x;
newy = p.y;
if (curx != newx || cury != newy) {
appendLineTo(curx, cury, newx, newy);
curx = newx;
cury = newy;
}
}
// Now find the segment endpoint with the smallest Y coordinate
int minPt = 0;
float minX = bezierCoords[0];
float minY = bezierCoords[1];
for (int ci = 6; ci < numCoords; ci += 6) {
float x = bezierCoords[ci];
float y = bezierCoords[ci + 1];
if (y < minY || (y == minY && x < minX)) {
minPt = ci;
minX = x;
minY = y;
}
}
// If the smallest Y coordinate is not the first coordinate,
// rotate the points so that it is...
if (minPt > 0) {
// Keep in mind that first 2 coords == last 2 coords
float newCoords[] = new float[numCoords];
// Copy all coordinates from minPt to the end of the
// array to the beginning of the new array
System.arraycopy(bezierCoords, minPt,
newCoords, 0,
numCoords - minPt);
// Now we do not want to copy 0,1 as they are duplicates
// of the last 2 coordinates which we just copied. So
// we start the source copy at index 2, but we still
// copy a full minPt coordinates which copies the two
// coordinates that were at minPt to the last two elements
// of the array, thus ensuring that thew new array starts
// and ends with the same pair of coordinates...
System.arraycopy(bezierCoords, 2,
newCoords, numCoords - minPt,
minPt);
bezierCoords = newCoords;
}
/* Clockwise enforcement:
* - This technique is based on the formula for calculating
* the area of a Polygon. The standard formula is:
* Area(Poly) = 1/2 * sum(x[i]*y[i+1] - x[i+1]y[i])
* - The returned area is negative if the polygon is
* "mostly clockwise" and positive if the polygon is
* "mostly counter-clockwise".
* - One failure mode of the Area calculation is if the
* Polygon is self-intersecting. This is due to the
* fact that the areas on each side of the self-intersection
* are bounded by segments which have opposite winding
* direction. Thus, those areas will have opposite signs
* on the acccumulation of their area summations and end
* up canceling each other out partially.
* - This failure mode of the algorithm in determining the
* exact magnitude of the area is not actually a big problem
* for our needs here since we are only using the sign of
* the resulting area to figure out the overall winding
* direction of the path. If self-intersections cause
* different parts of the path to disagree as to the
* local winding direction, that is no matter as we just
* wait for the final answer to tell us which winding
* direction had greater representation. If the final
* result is zero then the path was equal parts clockwise
* and counter-clockwise and we do not care about which
* way we order it as either way will require half of the
* path to unwind and re-wind itself.
*/
float area = 0;
// Note that first and last points are the same so we
// do not need to process coords[0,1] against coords[n-2,n-1]
curx = bezierCoords[0];
cury = bezierCoords[1];
for (int i = 2; i < numCoords; i += 2) {
newx = bezierCoords[i];
newy = bezierCoords[i + 1];
area += curx * newy - newx * cury;
curx = newx;
cury = newy;
}
if (area < 0) {
/* The area is negative so the shape was clockwise
* in a Euclidean sense. But, our screen coordinate
* systems have the origin in the upper left so they
* are flipped. Thus, this path "looks" ccw on the
* screen so we are flipping it to "look" clockwise.
* Note that the first and last points are the same
* so we do not need to swap them.
* (Not that it matters whether the paths end up cw
* or ccw in the end as long as all of them are the
* same, but above we called this section "Clockwise
* Enforcement", so we do not want to be liars. ;-)
*/
// Note that [0,1] do not need to be swapped with [n-2,n-1]
// So first pair to swap is [2,3] and [n-4,n-3]
int i = 2;
int j = numCoords - 4;
while (i < j) {
curx = bezierCoords[i];
cury = bezierCoords[i + 1];
bezierCoords[i] = bezierCoords[j];
bezierCoords[i + 1] = bezierCoords[j + 1];
bezierCoords[j] = curx;
bezierCoords[j + 1] = cury;
i += 2;
j -= 2;
}
}
}
private void appendLineTo(float x0, float y0,
float x1, float y1)
{
appendCubicTo(// A third of the way from xy0 to xy1:
interp(x0, x1, THIRD),
interp(y0, y1, THIRD),
// A third of the way from xy1 back to xy0:
interp(x1, x0, THIRD),
interp(y1, y0, THIRD),
x1, y1);
}
private void appendQuadTo(float x0, float y0,
float ctrlx, float ctrly,
float x1, float y1)
{
appendCubicTo(// A third of the way from ctrlxy back to xy0:
interp(ctrlx, x0, THIRD),
interp(ctrly, y0, THIRD),
// A third of the way from ctrlxy to xy1:
interp(ctrlx, x1, THIRD),
interp(ctrly, y1, THIRD),
x1, y1);
}
private void appendCubicTo(float ctrlx1, float ctrly1,
float ctrlx2, float ctrly2,
float x1, float y1)
{
if (numCoords + 6 > bezierCoords.length) {
// Keep array size to a multiple of 6 plus 2
int newsize = (numCoords - 2) * 2 + 2;
float newCoords[] = new float[newsize];
System.arraycopy(bezierCoords, 0, newCoords, 0, numCoords);
bezierCoords = newCoords;
}
bezierCoords[numCoords++] = ctrlx1;
bezierCoords[numCoords++] = ctrly1;
bezierCoords[numCoords++] = ctrlx2;
bezierCoords[numCoords++] = ctrly2;
bezierCoords[numCoords++] = x1;
bezierCoords[numCoords++] = y1;
}
public int getWindingRule() {
return windingrule;
}
public int getNumCoords() {
return numCoords;
}
public float getCoord(int i) {
return bezierCoords[i];
}
public float[] getTvals() {
if (myTvals != null) {
return myTvals;
}
// assert(numCoords >= 8);
// assert(((numCoords - 2) % 6) == 0);
float tvals[] = new float[(numCoords - 2) / 6 + 1];
// First calculate total "length" of path
// Length of each segment is averaged between
// the length between the endpoints (a lower bound for a cubic)
// and the length of the control polygon (an upper bound)
float segx = bezierCoords[0];
float segy = bezierCoords[1];
float tlen = 0;
int ci = 2;
int ti = 0;
while (ci < numCoords) {
float prevx, prevy, newx, newy;
prevx = segx;
prevy = segy;
newx = bezierCoords[ci++];
newy = bezierCoords[ci++];
prevx -= newx;
prevy -= newy;
float len = (float) Math.sqrt(prevx * prevx + prevy * prevy);
prevx = newx;
prevy = newy;
newx = bezierCoords[ci++];
newy = bezierCoords[ci++];
prevx -= newx;
prevy -= newy;
len += (float) Math.sqrt(prevx * prevx + prevy * prevy);
prevx = newx;
prevy = newy;
newx = bezierCoords[ci++];
newy = bezierCoords[ci++];
prevx -= newx;
prevy -= newy;
len += (float) Math.sqrt(prevx * prevx + prevy * prevy);
// len is now the total length of the control polygon
segx -= newx;
segy -= newy;
len += (float) Math.sqrt(segx * segx + segy * segy);
// len is now sum of linear length and control polygon length
len /= 2;
// len is now average of the two lengths
/* If the result is zero length then we will have problems
* below trying to do the math and bookkeeping to split
* the segment or pair it against the segments in the
* other shape. Since these lengths are just estimates
* to map the segments of the two shapes onto corresponding
* segments of "approximately the same length", we will
* simply modify the length of this segment to be at least
* a minimum value and it will simply grow from zero or
* near zero length to a non-trivial size as it morphs.
*/
if (len < MIN_LEN) {
len = MIN_LEN;
}
tlen += len;
tvals[ti++] = tlen;
segx = newx;
segy = newy;
}
// Now set tvals for each segment to its proportional
// part of the length
float prevt = tvals[0];
tvals[0] = 0;
for (ti = 1; ti < tvals.length - 1; ti++) {
float nextt = tvals[ti];
tvals[ti] = prevt / tlen;
prevt = nextt;
}
tvals[ti] = 1;
return (myTvals = tvals);
}
public void setTvals(float newTvals[]) {
float oldCoords[] = bezierCoords;
float newCoords[] = new float[2 + (newTvals.length - 1) * 6];
float oldTvals[] = getTvals();
int oldci = 0;
float x0, xc0, xc1, x1;
float y0, yc0, yc1, y1;
x0 = xc0 = xc1 = x1 = oldCoords[oldci++];
y0 = yc0 = yc1 = y1 = oldCoords[oldci++];
int newci = 0;
newCoords[newci++] = x0;
newCoords[newci++] = y0;
float t0 = 0;
float t1 = 0;
int oldti = 1;
int newti = 1;
while (newti < newTvals.length) {
if (t0 >= t1) {
x0 = x1;
y0 = y1;
xc0 = oldCoords[oldci++];
yc0 = oldCoords[oldci++];
xc1 = oldCoords[oldci++];
yc1 = oldCoords[oldci++];
x1 = oldCoords[oldci++];
y1 = oldCoords[oldci++];
t1 = oldTvals[oldti++];
}
float nt = newTvals[newti++];
// assert(nt > t0);
if (nt < t1) {
// Make nt proportional to [t0 => t1] range
float relt = (nt - t0) / (t1 - t0);
newCoords[newci++] = x0 = interp(x0, xc0, relt);
newCoords[newci++] = y0 = interp(y0, yc0, relt);
xc0 = interp(xc0, xc1, relt);
yc0 = interp(yc0, yc1, relt);
xc1 = interp(xc1, x1, relt);
yc1 = interp(yc1, y1, relt);
newCoords[newci++] = x0 = interp(x0, xc0, relt);
newCoords[newci++] = y0 = interp(y0, yc0, relt);
xc0 = interp(xc0, xc1, relt);
yc0 = interp(yc0, yc1, relt);
newCoords[newci++] = x0 = interp(x0, xc0, relt);
newCoords[newci++] = y0 = interp(y0, yc0, relt);
} else {
newCoords[newci++] = xc0;
newCoords[newci++] = yc0;
newCoords[newci++] = xc1;
newCoords[newci++] = yc1;
newCoords[newci++] = x1;
newCoords[newci++] = y1;
}
t0 = nt;
}
bezierCoords = newCoords;
numCoords = newCoords.length;
myTvals = newTvals;
}
}
private static class MorphedShape extends Shape {
Geometry geom0;
Geometry geom1;
float t;
MorphedShape(Geometry geom0, Geometry geom1, float t) {
this.geom0 = geom0;
this.geom1 = geom1;
this.t = t;
}
public Rectangle getRectangle() {
return new Rectangle(getBounds());
}
@Override
public RectBounds getBounds() {
int n = geom0.getNumCoords();
float xmin, ymin, xmax, ymax;
xmin = xmax = interp(geom0.getCoord(0), geom1.getCoord(0), t);
ymin = ymax = interp(geom0.getCoord(1), geom1.getCoord(1), t);
for (int i = 2; i < n; i += 2) {
float x = interp(geom0.getCoord(i), geom1.getCoord(i), t);
float y = interp(geom0.getCoord(i+1), geom1.getCoord(i+1), t);
if (xmin > x) {
xmin = x;
}
if (ymin > y) {
ymin = y;
}
if (xmax < x) {
xmax = x;
}
if (ymax < y) {
ymax = y;
}
}
return new RectBounds(xmin, ymin, xmax, ymax);
}
@Override
public boolean contains(float x, float y) {
return Path2D.contains(getPathIterator(null), x, y);
}
@Override
public boolean intersects(float x, float y, float w, float h) {
return Path2D.intersects(getPathIterator(null), x, y, w, h);
}
@Override
public boolean contains(float x, float y, float width, float height) {
return Path2D.contains(getPathIterator(null), x, y, width, height);
}
@Override
public PathIterator getPathIterator(BaseTransform at) {
return new Iterator(at, geom0, geom1, t);
}
@Override
public PathIterator getPathIterator(BaseTransform at, float flatness) {
return new FlatteningPathIterator(getPathIterator(at), flatness);
}
@Override
public Shape copy() {
return new Path2D(this);
}
}
private static class Iterator implements PathIterator {
BaseTransform at;
Geometry g0;
Geometry g1;
float t;
int cindex;
public Iterator(BaseTransform at,
Geometry g0, Geometry g1,
float t) {
this.at = at;
this.g0 = g0;
this.g1 = g1;
this.t = t;
}
/**
* @{inheritDoc}
*/
@Override
public int getWindingRule() {
return (t < 0.5 ? g0.getWindingRule() : g1.getWindingRule());
}
/**
* @{inheritDoc}
*/
@Override
public boolean isDone() {
return (cindex > g0.getNumCoords());
}
/**
* @{inheritDoc}
*/
@Override
public void next() {
if (cindex == 0) {
cindex = 2;
} else {
cindex += 6;
}
}
/**
* @{inheritDoc}
*/
@Override
public int currentSegment(float coords[]) {
int type;
int n;
if (cindex == 0) {
type = SEG_MOVETO;
n = 2;
} else if (cindex >= g0.getNumCoords()) {
type = SEG_CLOSE;
n = 0;
} else {
type = SEG_CUBICTO;
n = 6;
}
if (n > 0) {
for (int i = 0; i < n; i++) {
coords[i] = interp(g0.getCoord(cindex + i), g1.getCoord(cindex + i), t);
}
if (at != null) {
at.transform(coords, 0, coords, 0, n / 2);
}
}
return type;
}
}
}