/* * @(#)CalligraphyPathWriter.java * * $Date: 2014-03-13 09:15:48 +0100 (Cs, 13 márc. 2014) $ * * Copyright (c) 2011 by Jeremy Wood. * All rights reserved. * * The copyright of this software is owned by Jeremy Wood. * You may not use, copy or modify this software, except in * accordance with the license agreement you entered into with * Jeremy Wood. For details see accompanying license terms. * * This software is probably, but not necessarily, discussed here: * https://javagraphics.java.net/ * * That site should also contain the most recent official version * of this software. (See the SVN repository for more details.) */ package com.bric.awt; import com.bric.geom.GeneralPathWriter; import com.bric.geom.PathSegment; import com.bric.geom.PathWriter; import com.bric.geom.SimplifiedPathIterator; import net.jafama.FastMath; import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.GeneralPath; import java.awt.geom.PathIterator; /** This creates a calligraphic or ribbon-like effect while * tracing a shape. * <p>There are two separate shape bodies being written. If you * paint them separately: this creates an "over" and "under" effect. * If you paint them the same color: then this class is basically * a <code>java.awt.Stroke</code>. */ public class CalligraphyPathWriter extends PathWriter { private final float angle, offset1, offset2; private final PathWriter dest1, dest2; /** null=undefined, true=dest1, false=dest2 */ private Boolean writingTrack1; private PathSegment.Float tail; private float moveX, moveY; private final AffineTransform rotate; /** This is only used internally when we require a PathWriter to * write fragments of paths. */ PathWriter tailWriter = new PathWriter() { @Override public void moveTo(float x, float y) { throw new UnsupportedOperationException("moveTo("+x+", "+y+")"); } @Override public void lineTo(float x, float y) { tail = tail.lineTo(x, y); } @Override public void quadTo(float cx, float cy, float x, float y) { tail = tail.quadTo(cx, cy, x, y); } @Override public void curveTo(float cx1, float cy1, float cx2, float cy2, float x, float y) { tail = tail.cubicTo(cx1, cy1, cx2, cy2, x, y); } @Override public void closePath() { throw new UnsupportedOperationException("closePath()"); } @Override public void flush() {} }; /** Create a new <code>CalligraphyPathWriter</code>. * * @param angle the angle of the nib in this path. * @param offset1 the offset of one side of the traced shape. * One side of shape you trace will be offset by: * <br>(+offset1*cos(angle), +offset1*sin(angle) ) * <p>It is possible for this value to be zero, which means one * side of this path will exactly line up with the original shape * being traced. * <p>If both offsets are equal: then no new path segments will be * visible because they will be pencil then. * <p>To achieve a simple stroke: one offset should be (+width/2), * and the other offset should be (-width/2). * @param offset2 the offset of the other side of the traced shape. * This side of shape will be offset by: * <br>(+offset2*cos(angle), +offset2*sin(angle) ) * @param destination the destination for the data to write. */ public CalligraphyPathWriter(float angle,float offset1,float offset2,GeneralPath destination) { this(angle, offset1, offset2, new GeneralPathWriter(destination), new GeneralPathWriter(destination) ); } /** Create a new <code>CalligraphyPathWriter</code>. * * @param angle the angle of the nib in this path. * @param offset1 the offset of one side of the traced shape. * One side of shape you trace will be offset by: * <br>(+offset1*cos(angle), +offset1*sin(angle) ) * <p>It is possible for this value to be zero, which means one * side of this path will exactly line up with the original shape * being traced. * <p>If both offsets are equal: then no new path segments will be * visible because they will be pencil then. * <p>To achieve a simple stroke: one offset should be (+width/2), * and the other offset should be (-width/2). * @param offset2 the offset of the other side of the traced shape. * This side of shape will be offset by: * <br>(+offset2*cos(angle), +offset2*sin(angle) ) * @param dest1 the destination for half of the calligraphic shape. * @param dest2 the destination for the other half. This may be the * same as <code>dest1</code> if you plan on painting this path * at the same time with the same fill. */ public CalligraphyPathWriter(float angle,float offset1,float offset2,PathWriter dest1,PathWriter dest2) { checkArgument(angle, "angle is invalid"); checkArgument(offset1, "offset1 is invalid"); checkArgument(offset2, "offset2 is invalid"); if(dest1==null && dest2==null) throw new NullPointerException(); this.angle = angle; this.offset1 = offset1; this.offset2 = offset2; this.dest1 = dest1; this.dest2 = dest2; rotate = AffineTransform.getRotateInstance(-angle); } private void checkArgument(float value,String text) { if(Float.isInfinite(value) || Float.isNaN(value)) throw new IllegalArgumentException(text+" ("+value+")"); } @Override public synchronized void moveTo(float x, float y) { checkArgument(x, "x is invalid"); checkArgument(y, "y is invalid"); flush( x, y ); moveX = x; moveY = y; } @Override public synchronized void lineTo(final float x,final float y) { checkArgument(x, "x is invalid"); checkArgument(y, "y is invalid"); if(tail==null) throw new NullPointerException("missing moveTo segment"); PathSegment.Float moveTo = new PathSegment.Float( tail.data[tail.data.length-2], tail.data[tail.data.length-1] ); PathSegment.Float line = moveTo.lineTo(x, y); defineTrack( 0, 1, line); tail = tail.lineTo(x, y); } @Override public synchronized void quadTo(final float cx, final float cy,final float x,final float y) { checkArgument(cx, "cx is invalid"); checkArgument(cy, "cy is invalid"); checkArgument(x, "x is invalid"); checkArgument(y, "y is invalid"); if(tail==null) throw new NullPointerException("missing moveTo segment"); PathSegment.Float moveTo = new PathSegment.Float( tail.data[tail.data.length-2], tail.data[tail.data.length-1] ); PathSegment.Float quad = moveTo.quadTo(cx, cy, x, y); //first, check to see if this is really quad data: int simplifiedSegment = SimplifiedPathIterator.simplify(PathIterator.SEG_QUADTO, moveTo.data[0], moveTo.data[1], quad.data); if(simplifiedSegment==PathIterator.SEG_LINETO) { lineTo( quad.data[0], quad.data[1] ); return; } float[] ry_ = quad.getYCoeffs(rotate); float t = -ry_[1]/(2*ry_[0]); if(t>=0 && t<=1) { float[] x_ = quad.getXCoeffs(); float[] y_ = quad.getYCoeffs(); defineTrack( 0, t, quad); PathWriter.quadTo(tailWriter, 0, t, x_[0], x_[1], x_[2], y_[0], y_[1], y_[2]); flush( quad.getX(t), quad.getY(t) ); defineTrack( t, 1, quad); PathWriter.quadTo(tailWriter, t, 1, x_[0], x_[1], x_[2], y_[0], y_[1], y_[2]); } else { defineTrack( 0, 1, quad); tail = tail.quadTo(cx, cy, x, y); } } @Override public synchronized void curveTo(float cx1, float cy1, float cx2, float cy2, float x, float y) { checkArgument(cx1, "cx1 is invalid"); checkArgument(cy1, "cy1 is invalid"); checkArgument(cx2, "cx2 is invalid"); checkArgument(cy2, "cy2 is invalid"); checkArgument(x, "x is invalid"); checkArgument(y, "y is invalid"); if(tail==null) throw new NullPointerException("missing moveTo segment"); PathSegment.Float moveTo = new PathSegment.Float( tail.data[tail.data.length-2], tail.data[tail.data.length-1] ); PathSegment.Float cubic = moveTo.cubicTo(cx1, cy1, cx2, cy2, x, y); //first, check to see if this is really cubic data: int simplifiedSegment = SimplifiedPathIterator.simplify(PathIterator.SEG_CUBICTO, moveTo.data[0], moveTo.data[1], cubic.data); if(simplifiedSegment==PathIterator.SEG_LINETO) { lineTo( cubic.data[0], cubic.data[1] ); return; } else if(simplifiedSegment==PathIterator.SEG_QUADTO) { quadTo( cubic.data[0], cubic.data[1], cubic.data[2], cubic.data[3] ); return; } float[] ry_ = cubic.getYCoeffs(rotate); float[] times = null; if( Math.abs(ry_[0])<.00001) { //the leading coefficent is effectively 0, so //instead of ay*(t^3)+by*(t^2)+cy*t+dy we have //0+bx*(t^2)+cx*t+dx float t = -ry_[2]/(2*ry_[1]); if(t>=0 && t<=1) { times = new float[] { t }; } } else { float determinant = 4*ry_[1]*ry_[1]-12*ry_[0]*ry_[2]; if(determinant>=0) { float t1 = -1; float t2 = -1; determinant = (float)Math.sqrt(determinant); if(determinant==0) { t1 = (-2*ry_[1])/(6*ry_[0]); } else { t1 = (-2*ry_[1]+determinant)/(6*ry_[0]); t2 = (-2*ry_[1]-determinant)/(6*ry_[0]); } if(cubic.isThetaWellDefined(t1)==false) { t1 = -1; } if(cubic.isThetaWellDefined(t2)==false) { t2 = -1; } //sort our list of 2 elements: if(t2<t1) { float swap = t1; t1 = t2; t2 = swap; } if(t1>=0 && t2<=1) { times = new float[] { t1, t2}; } else if(t1>=0 && t1<=1) { times = new float[] { t1 }; } else if(t2>=0 && t2<=1) { times = new float[] { t2 }; } } } if(times!=null) { float[] x_ = cubic.getXCoeffs(); float[] y_ = cubic.getYCoeffs(); if(times.length==2) { defineTrack( 0, times[0], cubic); PathWriter.cubicTo(tailWriter, 0, times[0], x_[0], x_[1], x_[2], x_[3], y_[0], y_[1], y_[2], y_[3]); flush( cubic.getX(times[0]), cubic.getY(times[0]) ); defineTrack( times[0], times[1], cubic); PathWriter.cubicTo(tailWriter, times[0], times[1], x_[0], x_[1], x_[2], x_[3], y_[0], y_[1], y_[2], y_[3]); flush( cubic.getX(times[1]), cubic.getY(times[1]) ); defineTrack( times[1], 1, cubic); PathWriter.cubicTo(tailWriter, times[1], 1, x_[0], x_[1], x_[2], x_[3], y_[0], y_[1], y_[2], y_[3]); } else { defineTrack( 0, times[0], cubic); PathWriter.cubicTo(tailWriter, 0, times[0], x_[0], x_[1], x_[2], x_[3], y_[0], y_[1], y_[2], y_[3]); flush( cubic.getX(times[0]), cubic.getY(times[0]) ); defineTrack( times[0], 1, cubic); PathWriter.cubicTo(tailWriter, times[0], 1, x_[0], x_[1], x_[2], x_[3], y_[0], y_[1], y_[2], y_[3]); } return; } defineTrack( 0, 1, cubic); tail = tail.cubicTo(cx1, cy1, cx2, cy2, x, y); } private void defineTrack(float t0,float t1,PathSegment.Float segment) { float[] y_ = segment.getYCoeffs(rotate); float t = (t0+t1)/2; boolean b; if(y_.length==2) { b = (y_[0]) > 0; } else if(y_.length==3) { b = (2*y_[0]*t+y_[1]) > 0; } else if(y_.length==4) { b = (3*y_[0]*t*t+2*y_[1]*t+y_[2]) > 0; } else { throw new RuntimeException("unexpected condition"); } if(writingTrack1!=null && writingTrack1.booleanValue()!=b) { float x = segment.getX(t0); float y = segment.getY(t0); flush(x, y); } if(writingTrack1==null) { writingTrack1 = b ? Boolean.TRUE : Boolean.FALSE; } } @Override public synchronized void closePath() { if(tail==null) throw new NullPointerException("missing moveTo segment"); float lastX = tail.data[tail.data.length-2]; float lastY = tail.data[tail.data.length-1]; if(Math.abs(lastX-moveX)>.00001 || Math.abs(lastY-moveY)>.00001) lineTo(moveX, moveY); flush(); } @Override public void write(Shape s) { PathIterator iter = s.getPathIterator(null); write(iter); } private void flush(float moveX,float moveY) { flush(); tail = new PathSegment.Float( moveX, moveY); } @Override public synchronized void flush() { try { /** This occurs if there hasn't been a moveTo */ if(tail==null) return; /** This occurs if a moveTo is followed by a close * with no segment data. */ if(writingTrack1==null) return; PathSegment.Float head = tail.getHead(); PathWriter dest; double cos = FastMath.cos(angle); double sin = FastMath.sin(angle); AffineTransform transform1, transform2; /** Switching the order we apply the transforms fixes winding * problems when dest1 and dest2 are the same path. The same * basic path data is being written either way. */ if(writingTrack1.booleanValue()) { dest = dest1; transform1 = AffineTransform.getTranslateInstance( offset1*cos, offset1*sin); transform2 = AffineTransform.getTranslateInstance( offset2*cos, offset2*sin); } else { dest = dest2; transform1 = AffineTransform.getTranslateInstance( offset2*cos, offset2*sin); transform2 = AffineTransform.getTranslateInstance( offset1*cos, offset1*sin); } /** This can happen if only 1 dest path was provided to write to. */ if(dest==null) return; PathSegment.Float t = head; while(t!=null) { t.write(dest, 0, 1, transform1); t = t.next; } t = tail; double[] pts = new double[t.data.length]; transform2.transform(t.data, 0, pts, 0, t.data.length/2); dest.lineTo( (float)pts[pts.length-2], (float)pts[pts.length-1]); while(t!=null) { if(t.type==PathIterator.SEG_MOVETO) { if(t.prev==null) { dest.closePath(); } else { throw new RuntimeException("Unexpected condition."); } } else { t.write(dest, 1, 0, transform2); } t = t.prev; } dest.flush(); } finally { tail = null; writingTrack1 = null; } } }