javafx.scene.canvas.GraphicsContext Maven / Gradle / Ivy
/*
* Copyright (c) 2012, 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 javafx.scene.canvas;
import com.sun.javafx.geom.Arc2D;
import com.sun.javafx.geom.IllegalPathStateException;
import com.sun.javafx.geom.Path2D;
import com.sun.javafx.geom.PathIterator;
import com.sun.javafx.geom.transform.Affine2D;
import com.sun.javafx.geom.transform.NoninvertibleTransformException;
import com.sun.javafx.image.*;
import com.sun.javafx.image.impl.ByteBgraPre;
import com.sun.javafx.sg.prism.GrowableDataBuffer;
import com.sun.javafx.sg.prism.NGCanvas;
import com.sun.javafx.scene.text.FontHelper;
import com.sun.javafx.tk.Toolkit;
import com.sun.scenario.effect.EffectHelper;
import javafx.geometry.NodeOrientation;
import javafx.geometry.VPos;
import javafx.scene.effect.BlendMode;
import javafx.scene.effect.Effect;
import javafx.scene.image.Image;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.PixelReader;
import javafx.scene.image.PixelWriter;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.ArcType;
import javafx.scene.shape.FillRule;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.shape.StrokeLineJoin;
import javafx.scene.text.Font;
import javafx.scene.text.TextAlignment;
import javafx.scene.transform.Affine;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.util.Arrays;
import java.util.LinkedList;
import javafx.scene.text.FontSmoothingType;
/**
* This class is used to issue draw calls to a {@link Canvas} using a buffer.
*
* Each call pushes the necessary parameters onto the buffer
* where they will be later rendered onto the image of the {@code Canvas} node
* by the rendering thread at the end of a pulse.
*
* A {@code Canvas} only contains one {@code GraphicsContext}, and only one buffer.
* If it is not attached to any scene, then it can be modified by any thread,
* as long as it is only used from one thread at a time. Once a {@code Canvas}
* node is attached to a scene, it must be modified on the JavaFX Application
* Thread.
*
* Calling any method on the {@code GraphicsContext} is considered modifying
* its corresponding {@code Canvas} and is subject to the same threading
* rules.
*
* A {@code GraphicsContext} also manages a stack of state objects that can
* be saved or restored at anytime.
*
* The {@code GraphicsContext} maintains the following rendering attributes
* which affect various subsets of the rendering methods:
*
* List of Rendering Attributes
*
* Attribute
* Save/Restore?
* Default value
* Description
*
* Common Rendering Attributes
*
* {@link #clip() Clip}
* Yes
* No clipping
*
* An anti-aliased intersection of various clip paths to which rendering
* is restricted.
*
*
* {@link #setGlobalAlpha(double) Global Alpha}
* Yes
* {@code 1.0}
*
* An opacity value that controls the visibility or fading of each rendering
* operation.
*
*
* {@link #setGlobalBlendMode(javafx.scene.effect.BlendMode) Global Blend Mode}
* Yes
* {@link BlendMode#SRC_OVER SRC_OVER}
*
* A {@link BlendMode} enum value that controls how pixels from each rendering
* operation are composited into the existing image.
*
*
* {@link #setTransform(javafx.scene.transform.Affine) Transform}
* Yes
* {@code Identity}
*
* A 3x2 2D affine transformation matrix that controls how coordinates are
* mapped onto the logical pixels of the canvas image.
*
*
* {@link #setEffect(javafx.scene.effect.Effect) Effect}
* Yes
* {@code null}
*
* An {@link Effect} applied individually to each rendering operation.
*
* Fill Attributes
*
* {@link #setFill(javafx.scene.paint.Paint) Fill Paint}
* Yes
* {@link Color#BLACK BLACK}
*
* The {@link Paint} to be applied to the interior of shapes in a
* fill operation.
*
* Stroke Attributes
*
* {@link #setStroke(javafx.scene.paint.Paint) Stroke Paint}
* Yes
* {@link Color#BLACK BLACK}
*
* The {@link Paint} to be applied to the boundary of shapes in a
* stroke operation.
*
*
* {@link #setLineWidth(double) Line Width}
* Yes
* {@code 1.0}
*
* The width of the stroke applied to the boundary of shapes in a
* stroke operation.
*
*
* {@link #setLineCap(javafx.scene.shape.StrokeLineCap) Line Cap}
* Yes
* {@link StrokeLineCap#SQUARE SQUARE}
*
* The style of the end caps applied to the beginnings and ends of each
* dash and/or subpath in a stroke operation.
*
*
* {@link #setLineJoin(javafx.scene.shape.StrokeLineJoin) Line Join}
* Yes
* {@link StrokeLineJoin#MITER MITER}
*
* The style of the joins applied between individual segments in the boundary
* paths of shapes in a stroke operation.
*
*
* {@link #setMiterLimit(double) Miter Limit}
* Yes
* {@code 10.0}
*
* The ratio limit of how far a {@link StrokeLineJoin#MITER MITER} line join
* may extend in the direction of a sharp corner between segments in the
* boundary path of a shape, relative to the line width, before it is truncated
* to a {@link StrokeLineJoin#BEVEL BEVEL} join in a stroke operation.
*
*
* {@link #setLineDashes(double...) Dashes}
* Yes
* {@code null}
*
* The array of dash lengths to be applied to the segments in the boundary
* of shapes in a stroke operation.
*
*
* {@link #setLineDashOffset(double) Dash Offset}
* Yes
* {@code 0.0}
*
* The distance offset into the array of dash lengths at which to start the
* dashing of the segments in the boundary of shapes in a stroke operation.
*
* Text Attributes
*
* {@link #setFont(javafx.scene.text.Font) Font}
* Yes
* {@link Font#getDefault() Default Font}
*
* The font used for all fill and stroke text operations.
*
*
* {@link #setTextAlign(javafx.scene.text.TextAlignment) Text Align}
* Yes
* {@link TextAlignment#LEFT LEFT}
*
* The horizontal alignment of text with respect to the {@code X} coordinate
* specified in the text operation.
*
*
* {@link #setTextBaseline(javafx.geometry.VPos) Text Baseline}
* Yes
* {@link VPos#BASELINE BASELINE}
*
* The vertical position of the text relative to the {@code Y} coordinate
* specified in the text operation.
*
*
* {@link #setFontSmoothingType(javafx.scene.text.FontSmoothingType) Font Smoothing}
* Yes
* {@link FontSmoothingType#GRAY GRAY}
*
* The type of smoothing (antialiasing) applied to the glyphs in the font
* for all fill text operations.
*
* Path Attributes
*
* {@link #beginPath() Current Path}
* No
* Empty path
*
* The path constructed using various path construction methods to be used
* in various path filling, stroking, or clipping operations.
*
*
* {@link #setFillRule(javafx.scene.shape.FillRule) Fill Rule}
* Yes
* {@link FillRule#NON_ZERO NON_ZERO}
*
* The method used to determine the interior of paths for a path fill or
* clip operation.
*
* Image Attributes
*
* {@link #setImageSmoothing(boolean) Image Smoothing}
* Yes
* {@code true}
*
* A boolean state which enables or disables image smoothing for
* {@link #drawImage(javafx.scene.image.Image, double, double) drawImage(all forms)}.
*
*
*
* Rendering Attributes Table
*
* Method
* Common Rendering Attributes
* Fill Attributes
* Stroke Attributes
* Text Attributes
* Path Attributes
* Image Attributes
*
* Basic Shape Rendering
*
*
* {@link #fillRect(double, double, double, double) fillRect()},
* {@link #fillRoundRect(double, double, double, double, double, double) fillRoundRect()},
* {@link #fillOval(double, double, double, double) fillOval()},
* {@link #fillArc(double, double, double, double, double, double, javafx.scene.shape.ArcType) fillArc()}
*
* Yes
* Yes
* No
* No
* No
* No
*
*
*
* {@link #strokeLine(double, double, double, double) strokeLine()},
* {@link #strokeRect(double, double, double, double) strokeRect()},
* {@link #strokeRoundRect(double, double, double, double, double, double) strokeRoundRect()},
* {@link #strokeOval(double, double, double, double) strokeOval()},
* {@link #strokeArc(double, double, double, double, double, double, javafx.scene.shape.ArcType) strokeArc()}
*
* Yes
* No
* Yes
* No
* No
* No
*
*
*
* {@link #clearRect(double, double, double, double) clearRect()}
*
* Yes [1]
* No
* No
* No
* No
* No
*
*
*
* {@link #fillPolygon(double[], double[], int) fillPolygon()}
*
* Yes
* Yes
* No
* No
* Yes [2]
* No
*
*
*
* {@link #strokePolygon(double[], double[], int) strokePolygon()},
* {@link #strokePolyline(double[], double[], int) strokePolyline()}
*
* Yes
* No
* Yes
* No
* No
* No
*
*
* [1] Only the Transform, Clip, and Effect apply to clearRect()
* [2] Only the Fill Rule applies to fillPolygon(), the current path is left unchanged
*
* Text Rendering
*
*
* {@link #fillText(java.lang.String, double, double) fillText()},
* {@link #fillText(java.lang.String, double, double, double) fillText(with maxWidth)}
*
* Yes
* Yes
* No
* Yes [3]
* No
* No
*
*
*
* {@link #strokeText(java.lang.String, double, double) strokeText()},
* {@link #strokeText(java.lang.String, double, double, double) strokeText(with maxWidth)}
*
* Yes
* No
* Yes
* Yes [3]
* No
* No
*
*
* [3] The Font Smoothing attribute only applies to filled text
*
* Path Rendering
*
*
* {@link #beginPath() beginPath()},
* {@link #moveTo(double, double) moveTo()},
* {@link #lineTo(double, double) lineTo()},
* {@link #quadraticCurveTo(double, double, double, double) quadraticCurveTo()},
* {@link #bezierCurveTo(double, double, double, double, double, double) bezierCurveTo()},
* {@link #arc(double, double, double, double, double, double) arc()},
* {@link #arcTo(double, double, double, double, double) arcTo()},
* {@link #appendSVGPath(java.lang.String) appendSVGPath()},
* {@link #closePath() closePath()},
* {@link #rect(double, double, double, double) rect()}
*
* Yes [4]
* No
* No
* No
* No
* No
*
*
*
* {@link #fill() fill()}
*
* Yes [4]
* Yes
* No
* No
* Yes
* No
*
*
*
* {@link #stroke() stroke()}
*
* Yes [4]
* No
* Yes
* No
* Yes [5]
* No
*
*
*
* {@link #clip() clip()}
*
* No
* No
* No
* No
* Yes
* No
*
*
* [4] Transform applied only during path construction
* [5] Fill Rule only used for fill() and clip()
*
* Image Rendering
*
*
* {@link #drawImage(javafx.scene.image.Image, double, double) drawImage(all forms)}
*
* Yes
* No
* No
* No
* No
* Yes
*
* Miscellaneous
*
*
* {@link #applyEffect(javafx.scene.effect.Effect) applyEffect()},
* {@link #getPixelWriter() PixelWriter methods}
*
* No
* No
* No
* No
* No
* No
*
*
*
* Example:
*
*
* import javafx.scene.*;
* import javafx.scene.paint.*;
* import javafx.scene.canvas.*;
*
* Group root = new Group();
* Scene s = new Scene(root, 300, 300, Color.BLACK);
*
* final Canvas canvas = new Canvas(250,250);
* GraphicsContext gc = canvas.getGraphicsContext2D();
*
* gc.setFill(Color.BLUE);
* gc.fillRect(75,75,100,100);
*
* root.getChildren().add(canvas);
*
*
* @see Canvas
* @since JavaFX 2.2
*/
public final class GraphicsContext {
Canvas theCanvas;
Path2D path;
boolean pathDirty;
State curState;
LinkedList stateStack;
LinkedList clipStack;
GraphicsContext(Canvas theCanvas) {
this.theCanvas = theCanvas;
this.path = new Path2D();
pathDirty = true;
this.curState = new State();
this.stateStack = new LinkedList<>();
this.clipStack = new LinkedList<>();
}
static class State {
double globalAlpha;
BlendMode blendop;
Affine2D transform;
Paint fill;
Paint stroke;
double linewidth;
StrokeLineCap linecap;
StrokeLineJoin linejoin;
double miterlimit;
double dashes[];
double dashOffset;
int numClipPaths;
Font font;
FontSmoothingType fontsmoothing;
TextAlignment textalign;
VPos textbaseline;
Effect effect;
FillRule fillRule;
boolean imageSmoothing = true;
State() {
init();
}
final void init() {
set(1.0, BlendMode.SRC_OVER,
new Affine2D(),
Color.BLACK, Color.BLACK,
1.0, StrokeLineCap.SQUARE, StrokeLineJoin.MITER, 10.0,
null, 0.0,
0,
Font.getDefault(), FontSmoothingType.GRAY,
TextAlignment.LEFT, VPos.BASELINE,
null, FillRule.NON_ZERO, true);
}
State(State copy) {
set(copy.globalAlpha, copy.blendop,
new Affine2D(copy.transform),
copy.fill, copy.stroke,
copy.linewidth, copy.linecap, copy.linejoin, copy.miterlimit,
copy.dashes, copy.dashOffset,
copy.numClipPaths,
copy.font, copy.fontsmoothing, copy.textalign, copy.textbaseline,
copy.effect, copy.fillRule, copy.imageSmoothing);
}
final void set(double globalAlpha, BlendMode blendop,
Affine2D transform, Paint fill, Paint stroke,
double linewidth, StrokeLineCap linecap,
StrokeLineJoin linejoin, double miterlimit,
double dashes[], double dashOffset,
int numClipPaths,
Font font, FontSmoothingType smoothing,
TextAlignment align, VPos baseline,
Effect effect, FillRule fillRule, boolean imageSmoothing)
{
this.globalAlpha = globalAlpha;
this.blendop = blendop;
this.transform = transform;
this.fill = fill;
this.stroke = stroke;
this.linewidth = linewidth;
this.linecap = linecap;
this.linejoin = linejoin;
this.miterlimit = miterlimit;
this.dashes = dashes;
this.dashOffset = dashOffset;
this.numClipPaths = numClipPaths;
this.font = font;
this.fontsmoothing = smoothing;
this.textalign = align;
this.textbaseline = baseline;
this.effect = effect;
this.fillRule = fillRule;
this.imageSmoothing = imageSmoothing;
}
State copy() {
return new State(this);
}
void restore(GraphicsContext ctx) {
ctx.setGlobalAlpha(globalAlpha);
ctx.setGlobalBlendMode(blendop);
ctx.setTransform(transform.getMxx(), transform.getMyx(),
transform.getMxy(), transform.getMyy(),
transform.getMxt(), transform.getMyt());
ctx.setFill(fill);
ctx.setStroke(stroke);
ctx.setLineWidth(linewidth);
ctx.setLineCap(linecap);
ctx.setLineJoin(linejoin);
ctx.setMiterLimit(miterlimit);
ctx.setLineDashes(dashes);
ctx.setLineDashOffset(dashOffset);
GrowableDataBuffer buf = ctx.getBuffer();
while (ctx.curState.numClipPaths > numClipPaths) {
ctx.curState.numClipPaths--;
ctx.clipStack.removeLast();
buf.putByte(NGCanvas.POP_CLIP);
}
ctx.setFillRule(fillRule);
ctx.setFont(font);
ctx.setFontSmoothingType(fontsmoothing);
ctx.setTextAlign(textalign);
ctx.setTextBaseline(textbaseline);
ctx.setEffect(effect);
ctx.setImageSmoothing(imageSmoothing);
}
}
private GrowableDataBuffer getBuffer() {
return theCanvas.getBuffer();
}
private float coords[] = new float[6];
private static final byte pgtype[] = {
NGCanvas.MOVETO,
NGCanvas.LINETO,
NGCanvas.QUADTO,
NGCanvas.CUBICTO,
NGCanvas.CLOSEPATH,
};
private static final int numsegs[] = { 2, 2, 4, 6, 0, };
private void markPathDirty() {
pathDirty = true;
}
private void writePath(byte command) {
updateTransform();
GrowableDataBuffer buf = getBuffer();
if (pathDirty) {
buf.putByte(NGCanvas.PATHSTART);
PathIterator pi = path.getPathIterator(null);
while (!pi.isDone()) {
int pitype = pi.currentSegment(coords);
buf.putByte(pgtype[pitype]);
for (int i = 0; i < numsegs[pitype]; i++) {
buf.putFloat(coords[i]);
}
pi.next();
}
buf.putByte(NGCanvas.PATHEND);
pathDirty = false;
}
buf.putByte(command);
}
private void writePaint(Paint p, byte command) {
GrowableDataBuffer buf = getBuffer();
buf.putByte(command);
buf.putObject(Toolkit.getPaintAccessor().getPlatformPaint(p));
}
private void writeArcType(ArcType closure) {
byte type;
switch (closure) {
case OPEN: type = NGCanvas.ARC_OPEN; break;
case CHORD: type = NGCanvas.ARC_CHORD; break;
case ROUND: type = NGCanvas.ARC_PIE; break;
default: return; // ignored for consistency with other attributes
}
writeParam(type, NGCanvas.ARC_TYPE);
}
private void writeRectParams(GrowableDataBuffer buf,
double x, double y, double w, double h,
byte command)
{
buf.putByte(command);
buf.putFloat((float) x);
buf.putFloat((float) y);
buf.putFloat((float) w);
buf.putFloat((float) h);
}
private void writeOp4(double x, double y, double w, double h, byte command) {
updateTransform();
writeRectParams(getBuffer(), x, y, w, h, command);
}
private void writeOp6(double x, double y, double w, double h,
double v1, double v2, byte command)
{
updateTransform();
GrowableDataBuffer buf = getBuffer();
buf.putByte(command);
buf.putFloat((float) x);
buf.putFloat((float) y);
buf.putFloat((float) w);
buf.putFloat((float) h);
buf.putFloat((float) v1);
buf.putFloat((float) v2);
}
private float polybuf[] = new float[512];
private void flushPolyBuf(GrowableDataBuffer buf,
float polybuf[], int n, byte command)
{
curState.transform.transform(polybuf, 0, polybuf, 0, n/2);
for (int i = 0; i < n; i += 2) {
buf.putByte(command);
buf.putFloat(polybuf[i]);
buf.putFloat(polybuf[i+1]);
command = NGCanvas.LINETO;
}
}
private void writePoly(double xPoints[], double yPoints[], int nPoints,
boolean close, byte command)
{
if (xPoints == null || yPoints == null) return;
GrowableDataBuffer buf = getBuffer();
buf.putByte(NGCanvas.PATHSTART);
int pos = 0;
byte polycmd = NGCanvas.MOVETO;
for (int i = 0; i < nPoints; i++) {
if (pos >= polybuf.length) {
flushPolyBuf(buf, polybuf, pos, polycmd);
pos = 0;
polycmd = NGCanvas.LINETO;
}
polybuf[pos++] = (float) xPoints[i];
polybuf[pos++] = (float) yPoints[i];
}
flushPolyBuf(buf, polybuf, pos, polycmd);
if (close) {
buf.putByte(NGCanvas.CLOSEPATH);
}
buf.putByte(NGCanvas.PATHEND);
// Transform needs to be updated for rendering attributes even though
// we have already transformed the points as we sent them.
updateTransform();
buf.putByte(command);
// Now that we have changed the PG layer path, we need to mark our path dirty.
markPathDirty();
}
private void writeImage(Image img,
double dx, double dy, double dw, double dh)
{
if (img == null || img.getProgress() < 1.0) return;
Object platformImg = Toolkit.getImageAccessor().getPlatformImage(img);
if (platformImg == null) return;
updateTransform();
GrowableDataBuffer buf = getBuffer();
writeRectParams(buf, dx, dy, dw, dh, NGCanvas.DRAW_IMAGE);
buf.putObject(platformImg);
}
private void writeImage(Image img,
double dx, double dy, double dw, double dh,
double sx, double sy, double sw, double sh)
{
if (img == null || img.getProgress() < 1.0) return;
Object platformImg = Toolkit.getImageAccessor().getPlatformImage(img);
if (platformImg == null) return;
updateTransform();
GrowableDataBuffer buf = getBuffer();
writeRectParams(buf, dx, dy, dw, dh, NGCanvas.DRAW_SUBIMAGE);
buf.putFloat((float) sx);
buf.putFloat((float) sy);
buf.putFloat((float) sw);
buf.putFloat((float) sh);
buf.putObject(platformImg);
}
private void writeText(String text, double x, double y, double maxWidth,
byte command)
{
if (text == null) return;
updateTransform();
GrowableDataBuffer buf = getBuffer();
buf.putByte(command);
buf.putFloat((float) x);
buf.putFloat((float) y);
buf.putFloat((float) maxWidth);
buf.putBoolean(theCanvas.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT);
buf.putObject(text);
}
void writeParam(double v, byte command) {
GrowableDataBuffer buf = getBuffer();
buf.putByte(command);
buf.putFloat((float) v);
}
private void writeParam(byte v, byte command) {
GrowableDataBuffer buf = getBuffer();
buf.putByte(command);
buf.putByte(v);
}
private boolean txdirty;
private void updateTransform() {
if (txdirty) {
txdirty = false;
GrowableDataBuffer buf = getBuffer();
buf.putByte(NGCanvas.TRANSFORM);
buf.putDouble(curState.transform.getMxx());
buf.putDouble(curState.transform.getMxy());
buf.putDouble(curState.transform.getMxt());
buf.putDouble(curState.transform.getMyx());
buf.putDouble(curState.transform.getMyy());
buf.putDouble(curState.transform.getMyt());
}
}
void updateDimensions() {
GrowableDataBuffer buf = getBuffer();
buf.putByte(NGCanvas.SET_DIMS);
buf.putFloat((float) theCanvas.getWidth());
buf.putFloat((float) theCanvas.getHeight());
}
private void reset() {
GrowableDataBuffer buf = getBuffer();
// Only reset if we have a significant amount of data to omit,
// this prevents a common occurrence of "setFill(bg); fillRect();"
// at the start of a session from invoking a reset.
// But, do a reset anyway if the rendering layer has been falling
// behind because that lets the synchronization step throw out the
// older buffers that have been backing up.
if (buf.writeValuePosition() > Canvas.DEFAULT_VAL_BUF_SIZE ||
theCanvas.isRendererFallingBehind())
{
buf.reset();
buf.putByte(NGCanvas.RESET);
updateDimensions();
txdirty = true;
pathDirty = true;
State s = this.curState;
int numClipPaths = this.curState.numClipPaths;
this.curState = new State();
for (int i = 0; i < numClipPaths; i++) {
Path2D clip = clipStack.get(i);
buf.putByte(NGCanvas.PUSH_CLIP);
buf.putObject(clip);
}
this.curState.numClipPaths = numClipPaths;
s.restore(this);
}
}
private void resetIfCovers(Paint p, double x, double y, double w, double h) {
Affine2D tx = this.curState.transform;
if (tx.isTranslateOrIdentity()) {
x += tx.getMxt();
y += tx.getMyt();
if (x > 0 || y > 0 ||
(x+w) < theCanvas.getWidth() ||
(y+h) < theCanvas.getHeight())
{
return;
}
} else {
// quad test for coverage...?
return;
}
if (p != null) {
if (this.curState.blendop != BlendMode.SRC_OVER) return;
if (!p.isOpaque() || this.curState.globalAlpha < 1.0) return;
}
if (this.curState.numClipPaths > 0) return;
if (this.curState.effect != null) return;
reset();
}
/**
* Gets the {@code Canvas} that the {@code GraphicsContext} is issuing draw
* commands to. There is only ever one {@code Canvas} for a
* {@code GraphicsContext}.
*
* @return Canvas the canvas that this {@code GraphicsContext} is issuing draw
* commands to.
*/
public Canvas getCanvas() {
return theCanvas;
}
/**
* Saves the following attributes onto a stack.
*
* - Global Alpha
* - Global Blend Operation
* - Transform
* - Fill Paint
* - Stroke Paint
* - Line Width
* - Line Cap
* - Line Join
* - Miter Limit
* - Clip
* - Font
* - Text Align
* - Text Baseline
* - Effect
* - Fill Rule
*
* This method does NOT alter the current state in any way. Also, note that
* the current path is not saved.
*/
public void save() {
stateStack.push(curState.copy());
}
/**
* Pops the state off of the stack, setting the following attributes to their
* value at the time when that state was pushed onto the stack. If the stack
* is empty then nothing is changed.
*
*
* - Global Alpha
* - Global Blend Operation
* - Transform
* - Fill Paint
* - Stroke Paint
* - Line Width
* - Line Cap
* - Line Join
* - Miter Limit
* - Clip
* - Font
* - Text Align
* - Text Baseline
* - Effect
* - Fill Rule
*
* Note that the current path is not restored.
*/
public void restore() {
if (!stateStack.isEmpty()) {
State savedState = stateStack.pop();
savedState.restore(this);
txdirty = true;
}
}
/**
* Translates the current transform by x, y.
* @param x value to translate along the x axis.
* @param y value to translate along the y axis.
*/
public void translate(double x, double y) {
curState.transform.translate(x, y);
txdirty = true;
}
/**
* Scales the current transform by x, y.
* @param x value to scale in the x axis.
* @param y value to scale in the y axis.
*/
public void scale(double x, double y) {
curState.transform.scale(x, y);
txdirty = true;
}
/**
* Rotates the current transform in degrees.
* @param degrees value in degrees to rotate the current transform.
*/
public void rotate(double degrees) {
curState.transform.rotate(Math.toRadians(degrees));
txdirty = true;
}
/**
* Concatenates the input with the current transform.
*
* @param mxx - the X coordinate scaling element of the 3x4 matrix
* @param myx - the Y coordinate shearing element of the 3x4 matrix
* @param mxy - the X coordinate shearing element of the 3x4 matrix
* @param myy - the Y coordinate scaling element of the 3x4 matrix
* @param mxt - the X coordinate translation element of the 3x4 matrix
* @param myt - the Y coordinate translation element of the 3x4 matrix
*/
public void transform(double mxx, double myx,
double mxy, double myy,
double mxt, double myt)
{
curState.transform.concatenate(mxx, mxy, mxt,
myx, myy, myt);
txdirty = true;
}
/**
* Concatenates the input with the current transform. Only 2D transforms are
* supported. The only values used are the X and Y scaling, translation, and
* shearing components of a transform. A {@code null} value is treated as identity.
*
* @param xform The affine to be concatenated with the current transform or null.
*/
public void transform(Affine xform) {
if (xform == null) return;
curState.transform.concatenate(xform.getMxx(), xform.getMxy(), xform.getTx(),
xform.getMyx(), xform.getMyy(), xform.getTy());
txdirty = true;
}
/**
* Sets the current transform.
* @param mxx - the X coordinate scaling element of the 3x4 matrix
* @param myx - the Y coordinate shearing element of the 3x4 matrix
* @param mxy - the X coordinate shearing element of the 3x4 matrix
* @param myy - the Y coordinate scaling element of the 3x4 matrix
* @param mxt - the X coordinate translation element of the 3x4 matrix
* @param myt - the Y coordinate translation element of the 3x4 matrix
*/
public void setTransform(double mxx, double myx,
double mxy, double myy,
double mxt, double myt)
{
curState.transform.setTransform(mxx, myx,
mxy, myy,
mxt, myt);
txdirty = true;
}
/**
* Sets the current transform. Only 2D transforms are supported. The only
* values used are the X and Y scaling, translation, and shearing components
* of a transform.
*
* @param xform The affine to be copied and used as the current transform.
*/
public void setTransform(Affine xform) {
curState.transform.setTransform(xform.getMxx(), xform.getMyx(),
xform.getMxy(), xform.getMyy(),
xform.getTx(), xform.getTy());
txdirty = true;
}
/**
* Copies the current transform into the supplied object, creating
* a new {@link Affine} object if it is null, and returns the object
* containing the copy.
*
* @param xform A transform object that will be used to hold the result.
* If xform is non null, then this method will copy the current transform
* into that object. If xform is null a new transform object will be
* constructed. In either case, the return value is a copy of the current
* transform.
*
* @return A copy of the current transform.
*/
public Affine getTransform(Affine xform) {
if (xform == null) {
xform = new Affine();
}
xform.setMxx(curState.transform.getMxx());
xform.setMxy(curState.transform.getMxy());
xform.setMxz(0);
xform.setTx(curState.transform.getMxt());
xform.setMyx(curState.transform.getMyx());
xform.setMyy(curState.transform.getMyy());
xform.setMyz(0);
xform.setTy(curState.transform.getMyt());
xform.setMzx(0);
xform.setMzy(0);
xform.setMzz(1);
xform.setTz(0);
return xform;
}
/**
* Returns a copy of the current transform.
*
* @return a copy of the transform of the current state.
*/
public Affine getTransform() {
return getTransform(null);
}
/**
* Sets the global alpha of the current state.
* The default value is {@code 1.0}.
* Any valid double can be set, but only values in the range
* {@code [0.0, 1.0]} are valid and the nearest value in that
* range will be used for rendering.
* The global alpha is a common attribute
* used for nearly all rendering methods as specified in the
* Rendering Attributes Table.
*
* @param alpha the new alpha value, clamped to {@code [0.0, 1.0]}
* during actual use.
*/
public void setGlobalAlpha(double alpha) {
if (curState.globalAlpha != alpha) {
curState.globalAlpha = alpha;
alpha = (alpha > 1.0) ? 1.0 : (alpha < 0.0) ? 0.0 : alpha;
writeParam(alpha, NGCanvas.GLOBAL_ALPHA);
}
}
/**
* Gets the current global alpha.
* The default value is {@code 1.0}.
* The global alpha is a common attribute
* used for nearly all rendering methods as specified in the
* Rendering Attributes Table.
*
* @return the current global alpha.
*/
public double getGlobalAlpha() {
return curState.globalAlpha;
}
/**
* Sets the global blend mode.
* The default value is {@link BlendMode#SRC_OVER SRC_OVER}.
* A {@code null} value will be ignored and the current value will remain unchanged.
* The blend mode is a common attribute
* used for nearly all rendering methods as specified in the
* Rendering Attributes Table.
*
* @param op the {@code BlendMode} that will be set or null.
*/
public void setGlobalBlendMode(BlendMode op) {
if (op != null && op != curState.blendop) {
GrowableDataBuffer buf = getBuffer();
curState.blendop = op;
buf.putByte(NGCanvas.COMP_MODE);
buf.putObject(EffectHelper.getToolkitBlendMode(op));
}
}
/**
* Gets the global blend mode.
* The default value is {@link BlendMode#SRC_OVER SRC_OVER}.
* The blend mode is a common attribute
* used for nearly all rendering methods as specified in the
* Rendering Attributes Table.
*
* @return the global {@code BlendMode} of the current state.
*/
public BlendMode getGlobalBlendMode() {
return curState.blendop;
}
/**
* Sets the current fill paint attribute.
* The default value is {@link Color#BLACK BLACK}.
* The fill paint is a fill attribute
* used for any of the fill methods as specified in the
* Rendering Attributes Table.
* A {@code null} value will be ignored and the current value will remain unchanged.
*
* @param p The {@code Paint} to be used as the fill {@code Paint} or null.
*/
public void setFill(Paint p) {
if (p != null && curState.fill != p) {
curState.fill = p;
writePaint(p, NGCanvas.FILL_PAINT);
}
}
/**
* Gets the current fill paint attribute.
* The default value is {@link Color#BLACK BLACK}.
* The fill paint is a fill attribute
* used for any of the fill methods as specified in the
* Rendering Attributes Table.
*
* @return p The {@code Paint} to be used as the fill {@code Paint}.
*/
public Paint getFill() {
return curState.fill;
}
/**
* Sets the current stroke paint attribute.
* The default value is {@link Color#BLACK BLACK}.
* The stroke paint is a stroke attribute
* used for any of the stroke methods as specified in the
* Rendering Attributes Table.
* A {@code null} value will be ignored and the current value will remain unchanged.
*
* @param p The Paint to be used as the stroke Paint or null.
*/
public void setStroke(Paint p) {
if (p != null && curState.stroke != p) {
curState.stroke = p;
writePaint(p, NGCanvas.STROKE_PAINT);
}
}
/**
* Gets the current stroke.
* The default value is {@link Color#BLACK BLACK}.
* The stroke paint is a stroke attribute
* used for any of the stroke methods as specified in the
* Rendering Attributes Table.
*
* @return the {@code Paint} to be used as the stroke {@code Paint}.
*/
public Paint getStroke() {
return curState.stroke;
}
/**
* Sets the current line width.
* The default value is {@code 1.0}.
* The line width is a stroke attribute
* used for any of the stroke methods as specified in the
* Rendering Attributes Table.
* An infinite or non-positive value outside of the range {@code (0, +inf)}
* will be ignored and the current value will remain unchanged.
*
* @param lw value in the range {0-positive infinity}, with any other value
* being ignored and leaving the value unchanged.
*/
public void setLineWidth(double lw) {
// Per W3C spec: On setting, zero, negative, infinite, and NaN
// values must be ignored, leaving the value unchanged
if (lw > 0 && lw < Double.POSITIVE_INFINITY) {
if (curState.linewidth != lw) {
curState.linewidth = lw;
writeParam(lw, NGCanvas.LINE_WIDTH);
}
}
}
/**
* Gets the current line width.
* The default value is {@code 1.0}.
* The line width is a stroke attribute
* used for any of the stroke methods as specified in the
* Rendering Attributes Table.
*
* @return value between 0 and infinity.
*/
public double getLineWidth() {
return curState.linewidth;
}
/**
* Sets the current stroke line cap.
* The default value is {@link StrokeLineCap#SQUARE SQUARE}.
* The line cap is a stroke attribute
* used for any of the stroke methods as specified in the
* Rendering Attributes Table.
* A {@code null} value will be ignored and the current value will remain unchanged.
*
* @param cap {@code StrokeLineCap} with a value of Butt, Round, or Square or null.
*/
public void setLineCap(StrokeLineCap cap) {
if (cap != null && curState.linecap != cap) {
byte v;
switch (cap) {
case BUTT: v = NGCanvas.CAP_BUTT; break;
case ROUND: v = NGCanvas.CAP_ROUND; break;
case SQUARE: v = NGCanvas.CAP_SQUARE; break;
default: return;
}
curState.linecap = cap;
writeParam(v, NGCanvas.LINE_CAP);
}
}
/**
* Gets the current stroke line cap.
* The default value is {@link StrokeLineCap#SQUARE SQUARE}.
* The line cap is a stroke attribute
* used for any of the stroke methods as specified in the
* Rendering Attributes Table.
*
* @return {@code StrokeLineCap} with a value of Butt, Round, or Square.
*/
public StrokeLineCap getLineCap() {
return curState.linecap;
}
/**
* Sets the current stroke line join.
* The default value is {@link StrokeLineJoin#MITER}.
* The line join is a stroke attribute
* used for any of the stroke methods as specified in the
* Rendering Attributes Table.
* A {@code null} value will be ignored and the current value will remain unchanged.
*
* @param join {@code StrokeLineJoin} with a value of Miter, Bevel, or Round or null.
*/
public void setLineJoin(StrokeLineJoin join) {
if (join != null && curState.linejoin != join) {
byte v;
switch (join) {
case MITER: v = NGCanvas.JOIN_MITER; break;
case BEVEL: v = NGCanvas.JOIN_BEVEL; break;
case ROUND: v = NGCanvas.JOIN_ROUND; break;
default: return;
}
curState.linejoin = join;
writeParam(v, NGCanvas.LINE_JOIN);
}
}
/**
* Gets the current stroke line join.
* The default value is {@link StrokeLineJoin#MITER}.
* The line join is a stroke attribute
* used for any of the stroke methods as specified in the
* Rendering Attributes Table.
*
* @return {@code StrokeLineJoin} with a value of Miter, Bevel, or Round.
*/
public StrokeLineJoin getLineJoin() {
return curState.linejoin;
}
/**
* Sets the current miter limit.
* The default value is {@code 10.0}.
* The miter limit is a stroke attribute
* used for any of the stroke methods as specified in the
* Rendering Attributes Table.
* An infinite or non-positive value outside of the range {@code (0, +inf)}
* will be ignored and the current value will remain unchanged.
*
* @param ml miter limit value between 0 and positive infinity with
* any other value being ignored and leaving the value unchanged.
*/
public void setMiterLimit(double ml) {
// Per W3C spec: On setting, zero, negative, infinite, and NaN
// values must be ignored, leaving the value unchanged
if (ml > 0.0 && ml < Double.POSITIVE_INFINITY) {
if (curState.miterlimit != ml) {
curState.miterlimit = ml;
writeParam(ml, NGCanvas.MITER_LIMIT);
}
}
}
/**
* Gets the current miter limit.
* The default value is {@code 10.0}.
* The miter limit is a stroke attribute
* used for any of the stroke methods as specified in the
* Rendering Attributes Table.
*
* @return the miter limit value in the range {@code 0.0-positive infinity}
*/
public double getMiterLimit() {
return curState.miterlimit;
}
/**
* Sets the current stroke line dash pattern to a normalized copy of
* the argument.
* The default value is {@code null}.
* The line dash array is a stroke attribute
* used for any of the stroke methods as specified in the
* Rendering Attributes Table.
* If the array is {@code null} or empty or contains all {@code 0} elements
* then dashing will be disabled and the current dash array will be set
* to {@code null}.
* If any of the elements of the array are a negative, infinite, or NaN
* value outside the range {@code [0, +inf)} then the entire array will
* be ignored and the current dash array will remain unchanged.
* If the array is an odd length then it will be treated as if it
* were two copies of the array appended to each other.
*
* @param dashes the array of finite non-negative dash lengths
* @since JavaFX 8u40
*/
public void setLineDashes(double... dashes) {
if (dashes == null || dashes.length == 0) {
if (curState.dashes == null) {
return;
}
curState.dashes = null;
} else {
boolean allZeros = true;
for (int i = 0; i < dashes.length; i++) {
double d = dashes[i];
if (d >= 0.0 && d < Double.POSITIVE_INFINITY) {
// Non-NaN, finite, non-negative
// Test cannot be inverted or it will not implicitly test for NaN
if (d > 0) {
allZeros = false;
}
} else {
return;
}
}
if (allZeros) {
if (curState.dashes == null) {
return;
}
curState.dashes = null;
} else {
int dashlen = dashes.length;
if ((dashlen & 1) == 0) {
curState.dashes = Arrays.copyOf(dashes, dashlen);
} else {
curState.dashes = Arrays.copyOf(dashes, dashlen * 2);
System.arraycopy(dashes, 0, curState.dashes, dashlen, dashlen);
}
}
}
GrowableDataBuffer buf = getBuffer();
buf.putByte(NGCanvas.DASH_ARRAY);
buf.putObject(curState.dashes);
}
/**
* Gets a copy of the current line dash array.
* The default value is {@code null}.
* The array may be normalized by the validation tests in the
* {@link #setLineDashes(double...)} method.
* The line dash array is a stroke attribute
* used for any of the stroke methods as specified in the
* Rendering Attributes Table.
*
* @return a copy of the current line dash array.
* @since JavaFX 8u40
*/
public double[] getLineDashes() {
if (curState.dashes == null) {
return null;
}
return Arrays.copyOf(curState.dashes, curState.dashes.length);
}
/**
* Sets the line dash offset.
* The default value is {@code 0.0}.
* The line dash offset is a stroke attribute
* used for any of the stroke methods as specified in the
* Rendering Attributes Table.
* An infinite or NaN value outside of the range {@code (-inf, +inf)}
* will be ignored and the current value will remain unchanged.
*
* @param dashOffset the line dash offset in the range {@code (-inf, +inf)}
* @since JavaFX 8u40
*/
public void setLineDashOffset(double dashOffset) {
// Per W3C spec: On setting, infinite, and NaN
// values must be ignored, leaving the value unchanged
if (dashOffset > Double.NEGATIVE_INFINITY && dashOffset < Double.POSITIVE_INFINITY) {
curState.dashOffset = dashOffset;
writeParam(dashOffset, NGCanvas.DASH_OFFSET);
}
}
/**
* Gets the current line dash offset.
* The default value is {@code 0.0}.
* The line dash offset is a stroke attribute
* used for any of the stroke methods as specified in the
* Rendering Attributes Table.
*
* @return the line dash offset in the range {@code (-inf, +inf)}
* @since JavaFX 8u40
*/
public double getLineDashOffset() {
return curState.dashOffset;
}
/**
* Sets the current Font.
* The default value is specified by {@link Font#getDefault()}.
* The font is a text attribute
* used for any of the text methods as specified in the
* Rendering Attributes Table.
* A {@code null} value will be ignored and the current value will remain unchanged.
*
* @param f the Font or null.
*/
public void setFont(Font f) {
if (f != null && curState.font != f) {
curState.font = f;
GrowableDataBuffer buf = getBuffer();
buf.putByte(NGCanvas.FONT);
buf.putObject(FontHelper.getNativeFont(f));
}
}
/**
* Gets the current Font.
* The default value is specified by {@link Font#getDefault()}.
* The font is a text attribute
* used for any of the text methods as specified in the
* Rendering Attributes Table.
*
* @return the Font
*/
public Font getFont() {
return curState.font;
}
/**
* Sets the current Font Smoothing Type.
* The default value is {@link FontSmoothingType#GRAY GRAY}.
* The font smoothing type is a text attribute
* used for any of the text methods as specified in the
* Rendering Attributes Table.
* A {@code null} value will be ignored and the current value will remain unchanged.
*
* Note that the {@code FontSmoothingType} value of
* {@link FontSmoothingType#LCD LCD} is only supported over an opaque
* background. {@code LCD} text will generally appear as {@code GRAY}
* text over transparent or partially transparent pixels, and in some
* implementations it may not be supported at all on a {@link Canvas}
* because the required support does not exist for surfaces which contain
* an alpha channel as all {@code Canvas} objects do.
*
* @param fontsmoothing the {@link FontSmoothingType} or null
* @since JavaFX 8u40
*/
public void setFontSmoothingType(FontSmoothingType fontsmoothing) {
if (fontsmoothing != null && fontsmoothing != curState.fontsmoothing) {
curState.fontsmoothing = fontsmoothing;
writeParam((byte) fontsmoothing.ordinal(), NGCanvas.FONT_SMOOTH);
}
}
/**
* Gets the current Font Smoothing Type.
* The default value is {@link FontSmoothingType#GRAY GRAY}.
* The font smoothing type is a text attribute
* used for any of the text methods as specified in the
* Rendering Attributes Table.
*
* @return the {@link FontSmoothingType}
* @since JavaFX 8u40
*/
public FontSmoothingType getFontSmoothingType() {
return curState.fontsmoothing;
}
/**
* Defines horizontal text alignment, relative to the text {@code x} origin.
* The default value is {@link TextAlignment#LEFT LEFT}.
* The text alignment is a text attribute
* used for any of the text methods as specified in the
* Rendering Attributes Table.
*
* Let horizontal bounds represent the logical width of a single line of
* text. Where each line of text has a separate horizontal bounds.
*
* Then TextAlignment is specified as:
*
* - Left: the left edge of the horizontal bounds will be at {@code x}.
*
- Center: the center, halfway between left and right edge, of the
* horizontal bounds will be at {@code x}.
*
- Right: the right edge of the horizontal bounds will be at {@code x}.
*
*
*
* Note: Canvas does not support line wrapping, therefore the text
* alignment Justify is identical to left aligned text.
*
* A {@code null} value will be ignored and the current value will remain unchanged.
*
* @param align {@code TextAlignment} with values of Left, Center, Right or null.
*/
public void setTextAlign(TextAlignment align) {
if (align != null && curState.textalign != align) {
byte a;
switch (align) {
case LEFT: a = NGCanvas.ALIGN_LEFT; break;
case CENTER: a = NGCanvas.ALIGN_CENTER; break;
case RIGHT: a = NGCanvas.ALIGN_RIGHT; break;
case JUSTIFY: a = NGCanvas.ALIGN_JUSTIFY; break;
default: return;
}
curState.textalign = align;
writeParam(a, NGCanvas.TEXT_ALIGN);
}
}
/**
* Gets the current {@code TextAlignment}.
* The default value is {@link TextAlignment#LEFT LEFT}.
* The text alignment is a text attribute
* used for any of the text methods as specified in the
* Rendering Attributes Table.
*
* @return {@code TextAlignment} with values of Left, Center, Right, or
* Justify.
*/
public TextAlignment getTextAlign() {
return curState.textalign;
}
/**
* Sets the current Text Baseline.
* The default value is {@link VPos#BASELINE BASELINE}.
* The text baseline is a text attribute
* used for any of the text methods as specified in the
* Rendering Attributes Table.
* A {@code null} value will be ignored and the current value will remain unchanged.
*
* @param baseline {@code VPos} with values of Top, Center, Baseline, or Bottom or null.
*/
public void setTextBaseline(VPos baseline) {
if (baseline != null && curState.textbaseline != baseline) {
byte b;
switch (baseline) {
case TOP: b = NGCanvas.BASE_TOP; break;
case CENTER: b = NGCanvas.BASE_MIDDLE; break;
case BASELINE: b = NGCanvas.BASE_ALPHABETIC; break;
case BOTTOM: b = NGCanvas.BASE_BOTTOM; break;
default: return;
}
curState.textbaseline = baseline;
writeParam(b, NGCanvas.TEXT_BASELINE);
}
}
/**
* Gets the current Text Baseline.
* The default value is {@link VPos#BASELINE BASELINE}.
* The text baseline is a text attribute
* used for any of the text methods as specified in the
* Rendering Attributes Table.
*
* @return {@code VPos} with values of Top, Center, Baseline, or Bottom
*/
public VPos getTextBaseline() {
return curState.textbaseline;
}
/**
* Fills the given string of text at position x, y
* with the current fill paint attribute.
* A {@code null} text value will be ignored.
*
* This method will be affected by any of the
* global common,
* fill,
* or text
* attributes as specified in the
* Rendering Attributes Table.
*
*
* @param text the string of text or null.
* @param x position on the x axis.
* @param y position on the y axis.
*/
public void fillText(String text, double x, double y) {
writeText(text, x, y, 0, NGCanvas.FILL_TEXT);
}
/**
* Draws the given string of text at position x, y
* with the current stroke paint attribute.
* A {@code null} text value will be ignored.
*
* This method will be affected by any of the
* global common,
* stroke,
* or text
* attributes as specified in the
* Rendering Attributes Table.
*
*
* @param text the string of text or null.
* @param x position on the x axis.
* @param y position on the y axis.
*/
public void strokeText(String text, double x, double y) {
writeText(text, x, y, 0, NGCanvas.STROKE_TEXT);
}
/**
* Fills text and includes a maximum width of the string.
* If the width of the text extends past max width, then it will be sized
* to fit.
* A {@code null} text value will be ignored.
*
* This method will be affected by any of the
* global common,
* fill,
* or text
* attributes as specified in the
* Rendering Attributes Table.
*
*
* @param text the string of text or null.
* @param x position on the x axis.
* @param y position on the y axis.
* @param maxWidth maximum width the text string can have.
*/
public void fillText(String text, double x, double y, double maxWidth) {
if (maxWidth <= 0) return;
writeText(text, x, y, maxWidth, NGCanvas.FILL_TEXT);
}
/**
* Draws text with stroke paint and includes a maximum width of the string.
* If the width of the text extends past max width, then it will be sized
* to fit.
* A {@code null} text value will be ignored.
*
* This method will be affected by any of the
* global common,
* stroke,
* or text
* attributes as specified in the
* Rendering Attributes Table.
*
*
* @param text the string of text or null.
* @param x position on the x axis.
* @param y position on the y axis.
* @param maxWidth maximum width the text string can have.
*/
public void strokeText(String text, double x, double y, double maxWidth) {
if (maxWidth <= 0) return;
writeText(text, x, y, maxWidth, NGCanvas.STROKE_TEXT);
}
/**
* Set the filling rule attribute for determining the interior of paths
* in fill or clip operations.
* The default value is {@code FillRule.NON_ZERO}.
* A {@code null} value will be ignored and the current value will remain unchanged.
* The fill rule is a path attribute
* used for any of the fill or clip path methods as specified in the
* Rendering Attributes Table.
*
* @param fillRule {@code FillRule} with a value of Even_odd or Non_zero or null.
*/
public void setFillRule(FillRule fillRule) {
if (fillRule != null && curState.fillRule != fillRule) {
byte b;
if (fillRule == FillRule.EVEN_ODD) {
b = NGCanvas.FILL_RULE_EVEN_ODD;
} else {
b = NGCanvas.FILL_RULE_NON_ZERO;
}
curState.fillRule = fillRule;
writeParam(b, NGCanvas.FILL_RULE);
}
}
/**
* Get the filling rule attribute for determining the interior of paths
* in fill and clip operations.
* The default value is {@code FillRule.NON_ZERO}.
* The fill rule is a path attribute
* used for any of the fill or clip path methods as specified in the
* Rendering Attributes Table.
*
* @return current fill rule.
*/
public FillRule getFillRule() {
return curState.fillRule;
}
/**
* Sets the image smoothing state.
* Image smoothing is an Image attribute
* used to enable or disable image smoothing for
* {@link #drawImage(javafx.scene.image.Image, double, double) drawImage(all forms)}
* as specified in the Rendering Attributes Table.
* If image smoothing is {@code true}, images will be scaled using a higher
* quality filtering when transforming or scaling the source image to fit
* in the destination rectangle.
* If image smoothing is {@code false}, images will be scaled without filtering
* (or by using a lower quality filtering) when transforming or scaling the
* source image to fit in the destination rectangle.
*
* @defaultValue {@code true}
* @param imageSmoothing {@code true} to enable or {@code false} to disable smoothing
* @since 12
*/
public void setImageSmoothing(boolean imageSmoothing) {
if (curState.imageSmoothing != imageSmoothing) {
curState.imageSmoothing = imageSmoothing;
GrowableDataBuffer buf = getBuffer();
buf.putByte(NGCanvas.IMAGE_SMOOTH);
buf.putBoolean(curState.imageSmoothing);
}
}
/**
* Gets the current image smoothing state.
*
* @defaultValue {@code true}
* @return image smoothing state
* @since 12
*/
public boolean isImageSmoothing() {
return curState.imageSmoothing;
}
/**
* Resets the current path to empty.
* The default path is empty.
* The current path is a path attribute
* used for any of the path methods as specified in the
* Rendering Attributes Table
* and is not affected by the {@link #save()} and
* {@link #restore()} operations.
*/
public void beginPath() {
path.reset();
markPathDirty();
}
/**
* Issues a move command for the current path to the given x,y coordinate.
* The coordinates are transformed by the current transform as they are
* added to the path and unaffected by subsequent changes to the transform.
* The current path is a path attribute
* used for any of the path methods as specified in the
* Rendering Attributes Table
* and is not affected by the {@link #save()} and
* {@link #restore()} operations.
*
* @param x0 the X position for the move to command.
* @param y0 the Y position for the move to command.
*/
public void moveTo(double x0, double y0) {
coords[0] = (float) x0;
coords[1] = (float) y0;
curState.transform.transform(coords, 0, coords, 0, 1);
path.moveTo(coords[0], coords[1]);
markPathDirty();
}
/**
* Adds segments to the current path to make a line to the given x,y
* coordinate.
* The coordinates are transformed by the current transform as they are
* added to the path and unaffected by subsequent changes to the transform.
* The current path is a path attribute
* used for any of the path methods as specified in the
* Rendering Attributes Table
* and is not affected by the {@link #save()} and
* {@link #restore()} operations.
*
* @param x1 the X coordinate of the ending point of the line.
* @param y1 the Y coordinate of the ending point of the line.
*/
public void lineTo(double x1, double y1) {
coords[0] = (float) x1;
coords[1] = (float) y1;
curState.transform.transform(coords, 0, coords, 0, 1);
if (path.getNumCommands() == 0) {
path.moveTo(coords[0], coords[1]);
}
path.lineTo(coords[0], coords[1]);
markPathDirty();
}
/**
* Adds segments to the current path to make a quadratic Bezier curve.
* The coordinates are transformed by the current transform as they are
* added to the path and unaffected by subsequent changes to the transform.
* The current path is a path attribute
* used for any of the path methods as specified in the
* Rendering Attributes Table
* and is not affected by the {@link #save()} and
* {@link #restore()} operations.
*
* @param xc the X coordinate of the control point
* @param yc the Y coordinate of the control point
* @param x1 the X coordinate of the end point
* @param y1 the Y coordinate of the end point
*/
public void quadraticCurveTo(double xc, double yc, double x1, double y1) {
coords[0] = (float) xc;
coords[1] = (float) yc;
coords[2] = (float) x1;
coords[3] = (float) y1;
curState.transform.transform(coords, 0, coords, 0, 2);
if (path.getNumCommands() == 0) {
path.moveTo(coords[0], coords[1]);
}
path.quadTo(coords[0], coords[1], coords[2], coords[3]);
markPathDirty();
}
/**
* Adds segments to the current path to make a cubic Bezier curve.
* The coordinates are transformed by the current transform as they are
* added to the path and unaffected by subsequent changes to the transform.
* The current path is a path attribute
* used for any of the path methods as specified in the
* Rendering Attributes Table
* and is not affected by the {@link #save()} and
* {@link #restore()} operations.
*
* @param xc1 the X coordinate of first Bezier control point.
* @param yc1 the Y coordinate of the first Bezier control point.
* @param xc2 the X coordinate of the second Bezier control point.
* @param yc2 the Y coordinate of the second Bezier control point.
* @param x1 the X coordinate of the end point.
* @param y1 the Y coordinate of the end point.
*/
public void bezierCurveTo(double xc1, double yc1, double xc2, double yc2, double x1, double y1) {
coords[0] = (float) xc1;
coords[1] = (float) yc1;
coords[2] = (float) xc2;
coords[3] = (float) yc2;
coords[4] = (float) x1;
coords[5] = (float) y1;
curState.transform.transform(coords, 0, coords, 0, 3);
if (path.getNumCommands() == 0) {
path.moveTo(coords[0], coords[1]);
}
path.curveTo(coords[0], coords[1], coords[2], coords[3], coords[4], coords[5]);
markPathDirty();
}
/**
* Adds segments to the current path to make an arc.
* The coordinates are transformed by the current transform as they are
* added to the path and unaffected by subsequent changes to the transform.
* The current path is a path attribute
* used for any of the path methods as specified in the
* Rendering Attributes Table
* and is not affected by the {@link #save()} and
* {@link #restore()} operations.
*
* If {@code p0} is the current point in the path and {@code p1} is the
* point specified by {@code (x1, y1)} and {@code p2} is the point
* specified by {@code (x2, y2)}, then the arc segments appended will
* be segments along the circumference of a circle of the specified
* radius touching and inscribed into the convex (interior) side of
* {@code p0->p1->p2}. The path will contain a line segment (if
* needed) to the tangent point between that circle and {@code p0->p1}
* followed by circular arc segments to reach the tangent point between
* the circle and {@code p1->p2} and will end with the current point at
* that tangent point (not at {@code p2}).
* Note that the radius and circularity of the arc segments will be
* measured or considered relative to the current transform, but the
* resulting segments that are computed from those untransformed
* points will then be transformed when they are added to the path.
* Since all computation is done in untransformed space, but the
* pre-existing path segments are all transformed, the ability to
* correctly perform the computation may implicitly depend on being
* able to inverse transform the current end of the current path back
* into untransformed coordinates.
*
*
* If there is no way to compute and inscribe the indicated circle
* for any reason then the entire operation will simply append segments
* to force a line to point {@code p1}. Possible reasons that the
* computation may fail include:
*
* - The current path is empty.
* - The segments {@code p0->p1->p2} are colinear.
* - the current transform is non-invertible so that the current end
* point of the current path cannot be untransformed for computation.
*
*
* @param x1 the X coordinate of the first point of the arc.
* @param y1 the Y coordinate of the first point of the arc.
* @param x2 the X coordinate of the second point of the arc.
* @param y2 the Y coordinate of the second point of the arc.
* @param radius the radius of the arc in the range {0.0-positive infinity}.
*/
public void arcTo(double x1, double y1, double x2, double y2, double radius) {
if (path.getNumCommands() == 0) {
moveTo(x1, y1);
lineTo(x1, y1);
} else if (!tryArcTo((float) x1, (float) y1, (float) x2, (float) y2,
(float) radius))
{
lineTo(x1, y1);
}
}
private static double lenSq(double x0, double y0, double x1, double y1) {
x1 -= x0;
y1 -= y0;
return x1 * x1 + y1 * y1;
}
private boolean tryArcTo(float x1, float y1, float x2, float y2, float radius) {
float x0, y0;
if (curState.transform.isTranslateOrIdentity()) {
x0 = (float) (path.getCurrentX() - curState.transform.getMxt());
y0 = (float) (path.getCurrentY() - curState.transform.getMyt());
} else {
coords[0] = path.getCurrentX();
coords[1] = path.getCurrentY();
try {
curState.transform.inverseTransform(coords, 0, coords, 0, 1);
} catch (NoninvertibleTransformException e) {
return false;
}
x0 = coords[0];
y0 = coords[1];
}
// call x1,y1 the corner point
// If 2*theta is the angle described by p0->p1->p2
// then theta is the angle described by p0->p1->centerpt and
// centerpt->p1->p2
// We know that the distance from the arc center to the tangent points
// is r, and if A is the distance from the corner to the tangent point
// then we know:
// tan(theta) = r/A
// A = r / sin(theta)
// B = A * cos(theta) = r * (sin/cos) = r * tan
// We use the cosine rule on the triangle to get the 2*theta angle:
// cosB = (a^2 + c^2 - b^2) / (2ac)
// where a and c are the adjacent sides and b is the opposite side
// i.e. a = p0->p1, c=p1->p2, b=p0->p2
// Then we can use the tan^2 identity to compute B:
// tan^2 = (1 - cos(2theta)) / (1 + cos(2theta))
double lsq01 = lenSq(x0, y0, x1, y1);
double lsq12 = lenSq(x1, y1, x2, y2);
double lsq02 = lenSq(x0, y0, x2, y2);
double len01 = Math.sqrt(lsq01);
double len12 = Math.sqrt(lsq12);
double cosnum = lsq01 + lsq12 - lsq02;
double cosden = 2.0 * len01 * len12;
if (cosden == 0.0 || radius <= 0f) {
return false;
}
double cos_2theta = cosnum / cosden;
double tansq_den = (1.0 + cos_2theta);
if (tansq_den == 0.0) {
return false;
}
double tansq_theta = (1.0 - cos_2theta) / tansq_den;
double A = radius / Math.sqrt(tansq_theta);
double tx0 = x1 + (A / len01) * (x0 - x1);
double ty0 = y1 + (A / len01) * (y0 - y1);
double tx1 = x1 + (A / len12) * (x2 - x1);
double ty1 = y1 + (A / len12) * (y2 - y1);
// The midpoint between the two tangent points
double mx = (tx0 + tx1) / 2.0;
double my = (ty0 + ty1) / 2.0;
// similar triangles tell us that:
// len(m,center)/len(m,tangent) = len(m,tangent)/len(corner,m)
// len(m,center) = lensq(m,tangent)/len(corner,m)
// center = m + (m - p1) * len(m,center) / len(corner,m)
// = m + (m - p1) * (lensq(m,tangent) / lensq(corner,m))
double lenratioden = lenSq(mx, my, x1, y1);
if (lenratioden == 0.0) {
return false;
}
double lenratio = lenSq(mx, my, tx0, ty0) / lenratioden;
double cx = mx + (mx - x1) * lenratio;
double cy = my + (my - y1) * lenratio;
if (!(cx == cx && cy == cy)) {
return false;
}
// Looks like we are good to draw, first we have to get to the
// initial tangent point with a line segment.
if (tx0 != x0 || ty0 != y0) {
lineTo(tx0, ty0);
}
// We need sin(arc/2), cos(arc/2)
// and possibly sin(arc/4), cos(arc/4) if we need 2 cubic beziers
// We have tan(theta) = tan(tri/2)
// arc = 180-tri
// arc/2 = (180-tri)/2 = 90-(tri/2)
// sin(arc/2) = sin(90-(tri/2)) = cos(tri/2)
// cos(arc/2) = cos(90-(tri/2)) = sin(tri/2)
// 2theta = tri, therefore theta = tri/2
// cos(tri/2)^2 = (1+cos(tri)) / 2.0 = (1+cos_2theta)/2.0
// sin(tri/2)^2 = (1-cos(tri)) / 2.0 = (1-cos_2theta)/2.0
// sin(arc/2) = cos(tri/2) = sqrt((1+cos_2theta)/2.0)
// cos(arc/2) = sin(tri/2) = sqrt((1-cos_2theta)/2.0)
// We compute cos(arc/2) here as we need it in either case below
double coshalfarc = Math.sqrt((1.0 - cos_2theta) / 2.0);
boolean ccw = (ty0 - cy) * (tx1 - cx) > (ty1 - cy) * (tx0 - cx);
// If the arc covers more than 90 degrees then we must use 2
// cubic beziers to get a decent approximation.
// arc = 180-tri
// arc = 180-2*theta
// arc > 90 implies 2*theta < 90
// 2*theta < 90 implies cos_2theta > 0
// So, we need 2 cubics if cos_2theta > 0
if (cos_2theta <= 0.0) {
// 1 cubic bezier
double sinhalfarc = Math.sqrt((1.0 + cos_2theta) / 2.0);
double cv = 4.0 / 3.0 * sinhalfarc / (1.0 + coshalfarc);
if (ccw) cv = -cv;
double cpx0 = tx0 - cv * (ty0 - cy);
double cpy0 = ty0 + cv * (tx0 - cx);
double cpx1 = tx1 + cv * (ty1 - cy);
double cpy1 = ty1 - cv * (tx1 - cx);
bezierCurveTo(cpx0, cpy0, cpx1, cpy1, tx1, ty1);
} else {
// 2 cubic beziers
// We need sin(arc/4) and cos(arc/4)
// We computed cos(arc/2), so we can compute them as follows:
// sin(arc/4) = sqrt((1 - cos(arc/2)) / 2)
// cos(arc/4) = sart((1 + cos(arc/2)) / 2)
double sinqtrarc = Math.sqrt((1.0 - coshalfarc) / 2.0);
double cosqtrarc = Math.sqrt((1.0 + coshalfarc) / 2.0);
double cv = 4.0 / 3.0 * sinqtrarc / (1.0 + cosqtrarc);
if (ccw) cv = -cv;
double midratio = radius / Math.sqrt(lenratioden);
double midarcx = cx + (x1 - mx) * midratio;
double midarcy = cy + (y1 - my) * midratio;
double cpx0 = tx0 - cv * (ty0 - cy);
double cpy0 = ty0 + cv * (tx0 - cx);
double cpx1 = midarcx + cv * (midarcy - cy);
double cpy1 = midarcy - cv * (midarcx - cx);
bezierCurveTo(cpx0, cpy0, cpx1, cpy1, midarcx, midarcy);
cpx0 = midarcx - cv * (midarcy - cy);
cpy0 = midarcy + cv * (midarcx - cx);
cpx1 = tx1 + cv * (ty1 - cy);
cpy1 = ty1 - cv * (tx1 - cx);
bezierCurveTo(cpx0, cpy0, cpx1, cpy1, tx1, ty1);
}
return true;
}
/**
* Adds path elements to the current path to make an arc that uses Euclidean
* degrees. This Euclidean orientation sweeps from East to North, then West,
* then South, then back to East.
* The coordinates are transformed by the current transform as they are
* added to the path and unaffected by subsequent changes to the transform.
* The current path is a path attribute
* used for any of the path methods as specified in the
* Rendering Attributes Table
* and is not affected by the {@link #save()} and
* {@link #restore()} operations.
*
* @param centerX the center x position of the arc.
* @param centerY the center y position of the arc.
* @param radiusX the x radius of the arc.
* @param radiusY the y radius of the arc.
* @param startAngle the starting angle of the arc in the range {@code 0-360.0}
* @param length the length of the baseline of the arc.
*/
public void arc(double centerX, double centerY,
double radiusX, double radiusY,
double startAngle, double length)
{
Arc2D arc = new Arc2D((float) (centerX - radiusX), // x
(float) (centerY - radiusY), // y
(float) (radiusX * 2.0), // w
(float) (radiusY * 2.0), // h
(float) startAngle,
(float) length,
Arc2D.OPEN);
path.append(arc.getPathIterator(curState.transform), true);
markPathDirty();
}
/**
* Adds path elements to the current path to make a rectangle.
* The coordinates are transformed by the current transform as they are
* added to the path and unaffected by subsequent changes to the transform.
* The current path is a path attribute
* used for any of the path methods as specified in the
* Rendering Attributes Table
* and is not affected by the {@link #save()} and
* {@link #restore()} operations.
*
* @param x x position of the upper left corner of the rectangle.
* @param y y position of the upper left corner of the rectangle.
* @param w width of the rectangle.
* @param h height of the rectangle.
*/
public void rect(double x, double y, double w, double h) {
coords[0] = (float) x;
coords[1] = (float) y;
coords[2] = (float) w;
coords[3] = 0;
coords[4] = 0;
coords[5] = (float) h;
curState.transform.deltaTransform(coords, 0, coords, 0, 3);
float x0 = coords[0] + (float) curState.transform.getMxt();
float y0 = coords[1] + (float) curState.transform.getMyt();
float dx1 = coords[2];
float dy1 = coords[3];
float dx2 = coords[4];
float dy2 = coords[5];
path.moveTo(x0, y0);
path.lineTo(x0+dx1, y0+dy1);
path.lineTo(x0+dx1+dx2, y0+dy1+dy2);
path.lineTo(x0+dx2, y0+dy2);
path.closePath();
markPathDirty();
// path.moveTo(x0, y0); // not needed, closepath leaves pen at moveto
}
/**
* Appends an SVG Path string to the current path. If there is no current
* path the string must then start with either type of move command.
* A {@code null} value or incorrect SVG path will be ignored.
* The coordinates are transformed by the current transform as they are
* added to the path and unaffected by subsequent changes to the transform.
* The current path is a path attribute
* used for any of the path methods as specified in the
* Rendering Attributes Table
* and is not affected by the {@link #save()} and
* {@link #restore()} operations.
*
* @param svgpath the SVG Path string.
*/
public void appendSVGPath(String svgpath) {
if (svgpath == null) return;
boolean prependMoveto = true;
boolean skipMoveto = true;
for (int i = 0; i < svgpath.length(); i++) {
switch (svgpath.charAt(i)) {
case ' ':
case '\t':
case '\r':
case '\n':
continue;
case 'M':
prependMoveto = skipMoveto = false;
break;
case 'm':
if (path.getNumCommands() == 0) {
// An initial relative moveTo becomes absolute
prependMoveto = false;
}
// Even if we prepend an initial moveTo in the temp
// path, we do not want to delete the resulting initial
// moveTo because the relative moveto will be folded
// into it by an optimization in the Path2D object.
skipMoveto = false;
break;
}
break;
}
Path2D p2d = new Path2D();
if (prependMoveto && path.getNumCommands() > 0) {
float x0, y0;
if (curState.transform.isTranslateOrIdentity()) {
x0 = (float) (path.getCurrentX() - curState.transform.getMxt());
y0 = (float) (path.getCurrentY() - curState.transform.getMyt());
} else {
coords[0] = path.getCurrentX();
coords[1] = path.getCurrentY();
try {
curState.transform.inverseTransform(coords, 0, coords, 0, 1);
} catch (NoninvertibleTransformException e) {
}
x0 = coords[0];
y0 = coords[1];
}
p2d.moveTo(x0, y0);
} else {
skipMoveto = false;
}
try {
p2d.appendSVGPath(svgpath);
PathIterator pi = p2d.getPathIterator(curState.transform);
if (skipMoveto) {
// We need to delete the initial moveto and let the path
// extend from the actual existing geometry.
pi.next();
}
path.append(pi, false);
} catch (IllegalArgumentException | IllegalPathStateException ex) {
//Ignore incorrect path
}
}
/**
* Closes the path.
* The current path is a path attribute
* used for any of the path methods as specified in the
* Rendering Attributes Table
* and is not affected by the {@link #save()} and
* {@link #restore()} operations.
*/
public void closePath() {
if (path.getNumCommands() > 0) {
path.closePath();
markPathDirty();
}
}
/**
* Fills the path with the current fill paint.
*
* This method will be affected by any of the
* global common,
* fill,
* or path
* attributes as specified in the
* Rendering Attributes Table.
* Note that the path segments were transformed as they were originally
* added to the current path so the current transform will not affect
* those path segments again, but it may affect other attributes in
* affect at the time of the {@code fill()} operation.
*
*/
public void fill() {
writePath(NGCanvas.FILL_PATH);
}
/**
* Strokes the path with the current stroke paint.
*
* This method will be affected by any of the
* global common,
* stroke,
* or path
* attributes as specified in the
* Rendering Attributes Table.
* Note that the path segments were transformed as they were originally
* added to the current path so the current transform will not affect
* those path segments again, but it may affect other attributes in
* affect at the time of the {@code stroke()} operation.
*
*/
public void stroke() {
writePath(NGCanvas.STROKE_PATH);
}
/**
* Intersects the current clip with the current path and applies it to
* subsequent rendering operation as an anti-aliased mask.
* The current clip is a common attribute
* used for nearly all rendering operations as specified in the
* Rendering Attributes Table.
*
* This method will itself be affected only by the
* path
* attributes as specified in the
* Rendering Attributes Table.
* Note that the path segments were transformed as they were originally
* added to the current path so the current transform will not affect
* those path segments again, but it may affect other attributes in
* affect at the time of the {@code stroke()} operation.
*
*/
public void clip() {
Path2D clip = new Path2D(path);
clipStack.addLast(clip);
curState.numClipPaths++;
GrowableDataBuffer buf = getBuffer();
buf.putByte(NGCanvas.PUSH_CLIP);
buf.putObject(clip);
}
/**
* Returns true if the the given x,y point is inside the path.
*
* @param x the X coordinate to use for the check.
* @param y the Y coordinate to use for the check.
* @return true if the point given is inside the path, false
* otherwise.
*/
public boolean isPointInPath(double x, double y) {
// TODO: HTML5 considers points on the path to be inside, but we
// implement a halfin-halfout approach...
return path.contains((float) x, (float) y);
}
/**
* Clears a portion of the canvas with a transparent color value.
*
* This method will be affected only by the current transform, clip,
* and effect.
*
*
* @param x X position of the upper left corner of the rectangle.
* @param y Y position of the upper left corner of the rectangle.
* @param w width of the rectangle.
* @param h height of the rectangle.
*/
public void clearRect(double x, double y, double w, double h) {
if (w != 0 && h != 0) {
resetIfCovers(null, x, y, w, h);
writeOp4(x, y, w, h, NGCanvas.CLEAR_RECT);
}
}
/**
* Fills a rectangle using the current fill paint.
*
* This method will be affected by any of the
* global common
* or fill
* attributes as specified in the
* Rendering Attributes Table.
*
*
* @param x the X position of the upper left corner of the rectangle.
* @param y the Y position of the upper left corner of the rectangle.
* @param w the width of the rectangle.
* @param h the height of the rectangle.
*/
public void fillRect(double x, double y, double w, double h) {
if (w != 0 && h != 0) {
resetIfCovers(this.curState.fill, x, y, w, h);
writeOp4(x, y, w, h, NGCanvas.FILL_RECT);
}
}
/**
* Strokes a rectangle using the current stroke paint.
*
* This method will be affected by any of the
* global common
* or stroke
* attributes as specified in the
* Rendering Attributes Table.
*
*
* @param x the X position of the upper left corner of the rectangle.
* @param y the Y position of the upper left corner of the rectangle.
* @param w the width of the rectangle.
* @param h the height of the rectangle.
*/
public void strokeRect(double x, double y, double w, double h) {
if (w != 0 || h != 0) {
writeOp4(x, y, w, h, NGCanvas.STROKE_RECT);
}
}
/**
* Fills an oval using the current fill paint.
*
* This method will be affected by any of the
* global common
* or fill
* attributes as specified in the
* Rendering Attributes Table.
*
*
* @param x the X coordinate of the upper left bound of the oval.
* @param y the Y coordinate of the upper left bound of the oval.
* @param w the width at the center of the oval.
* @param h the height at the center of the oval.
*/
public void fillOval(double x, double y, double w, double h) {
if (w != 0 && h != 0) {
writeOp4(x, y, w, h, NGCanvas.FILL_OVAL);
}
}
/**
* Strokes an oval using the current stroke paint.
*
* This method will be affected by any of the
* global common
* or stroke
* attributes as specified in the
* Rendering Attributes Table.
*
*
* @param x the X coordinate of the upper left bound of the oval.
* @param y the Y coordinate of the upper left bound of the oval.
* @param w the width at the center of the oval.
* @param h the height at the center of the oval.
*/
public void strokeOval(double x, double y, double w, double h) {
if (w != 0 || h != 0) {
writeOp4(x, y, w, h, NGCanvas.STROKE_OVAL);
}
}
/**
* Fills an arc using the current fill paint. A {@code null} ArcType or
* non positive width or height will cause the render command to be ignored.
*
* This method will be affected by any of the
* global common
* or fill
* attributes as specified in the
* Rendering Attributes Table.
*
*
* @param x the X coordinate of the arc.
* @param y the Y coordinate of the arc.
* @param w the width of the arc.
* @param h the height of the arc.
* @param startAngle the starting angle of the arc in degrees.
* @param arcExtent the angular extent of the arc in degrees.
* @param closure closure type (Round, Chord, Open) or null.
*/
public void fillArc(double x, double y, double w, double h,
double startAngle, double arcExtent, ArcType closure)
{
if (w != 0 && h != 0 && closure != null) {
writeArcType(closure);
writeOp6(x, y, w, h, startAngle, arcExtent, NGCanvas.FILL_ARC);
}
}
/**
* Strokes an Arc using the current stroke paint. A {@code null} ArcType or
* non positive width or height will cause the render command to be ignored.
*
* This method will be affected by any of the
* global common
* or stroke
* attributes as specified in the
* Rendering Attributes Table.
*
*
* @param x the X coordinate of the arc.
* @param y the Y coordinate of the arc.
* @param w the width of the arc.
* @param h the height of the arc.
* @param startAngle the starting angle of the arc in degrees.
* @param arcExtent arcExtent the angular extent of the arc in degrees.
* @param closure closure type (Round, Chord, Open) or null
*/
public void strokeArc(double x, double y, double w, double h,
double startAngle, double arcExtent, ArcType closure)
{
if (w != 0 && h != 0 && closure != null) {
writeArcType(closure);
writeOp6(x, y, w, h, startAngle, arcExtent, NGCanvas.STROKE_ARC);
}
}
/**
* Fills a rounded rectangle using the current fill paint.
*
* This method will be affected by any of the
* global common
* or fill
* attributes as specified in the
* Rendering Attributes Table.
*
*
* @param x the X coordinate of the upper left bound of the oval.
* @param y the Y coordinate of the upper left bound of the oval.
* @param w the width at the center of the oval.
* @param h the height at the center of the oval.
* @param arcWidth the arc width of the rectangle corners.
* @param arcHeight the arc height of the rectangle corners.
*/
public void fillRoundRect(double x, double y, double w, double h,
double arcWidth, double arcHeight)
{
if (w != 0 && h != 0) {
writeOp6(x, y, w, h, arcWidth, arcHeight, NGCanvas.FILL_ROUND_RECT);
}
}
/**
* Strokes a rounded rectangle using the current stroke paint.
*
* This method will be affected by any of the
* global common
* or stroke
* attributes as specified in the
* Rendering Attributes Table.
*
*
* @param x the X coordinate of the upper left bound of the oval.
* @param y the Y coordinate of the upper left bound of the oval.
* @param w the width at the center of the oval.
* @param h the height at the center of the oval.
* @param arcWidth the arc width of the rectangle corners.
* @param arcHeight the arc height of the rectangle corners.
*/
public void strokeRoundRect(double x, double y, double w, double h,
double arcWidth, double arcHeight)
{
if (w != 0 && h != 0) {
writeOp6(x, y, w, h, arcWidth, arcHeight, NGCanvas.STROKE_ROUND_RECT);
}
}
/**
* Strokes a line using the current stroke paint.
*
* This method will be affected by any of the
* global common
* or stroke
* attributes as specified in the
* Rendering Attributes Table.
*
*
* @param x1 the X coordinate of the starting point of the line.
* @param y1 the Y coordinate of the starting point of the line.
* @param x2 the X coordinate of the ending point of the line.
* @param y2 the Y coordinate of the ending point of the line.
*/
public void strokeLine(double x1, double y1, double x2, double y2) {
writeOp4(x1, y1, x2, y2, NGCanvas.STROKE_LINE);
}
/**
* Fills a polygon with the given points using the currently set fill paint.
* A {@code null} value for any of the arrays will be ignored and nothing will be drawn.
*
* This method will be affected by any of the
* global common,
* fill,
* or Fill Rule
* attributes as specified in the
* Rendering Attributes Table.
*
*
* @param xPoints array containing the x coordinates of the polygon's points or null.
* @param yPoints array containing the y coordinates of the polygon's points or null.
* @param nPoints the number of points that make the polygon.
*/
public void fillPolygon(double xPoints[], double yPoints[], int nPoints) {
if (nPoints >= 3) {
writePoly(xPoints, yPoints, nPoints, true, NGCanvas.FILL_PATH);
}
}
/**
* Strokes a polygon with the given points using the currently set stroke paint.
* A {@code null} value for any of the arrays will be ignored and nothing will be drawn.
*
* This method will be affected by any of the
* global common
* or stroke
* attributes as specified in the
* Rendering Attributes Table.
*
*
* @param xPoints array containing the x coordinates of the polygon's points or null.
* @param yPoints array containing the y coordinates of the polygon's points or null.
* @param nPoints the number of points that make the polygon.
*/
public void strokePolygon(double xPoints[], double yPoints[], int nPoints) {
if (nPoints >= 2) {
writePoly(xPoints, yPoints, nPoints, true, NGCanvas.STROKE_PATH);
}
}
/**
* Strokes a polyline with the given points using the currently set stroke
* paint attribute.
* A {@code null} value for any of the arrays will be ignored and nothing will be drawn.
*
* This method will be affected by any of the
* global common
* or stroke
* attributes as specified in the
* Rendering Attributes Table.
*
*
* @param xPoints array containing the x coordinates of the polyline's points or null.
* @param yPoints array containing the y coordinates of the polyline's points or null.
* @param nPoints the number of points that make the polyline.
*/
public void strokePolyline(double xPoints[], double yPoints[], int nPoints) {
if (nPoints >= 2) {
writePoly(xPoints, yPoints, nPoints, false, NGCanvas.STROKE_PATH);
}
}
/**
* Draws an image at the given x, y position using the width
* and height of the given image.
* A {@code null} image value or an image still in progress will be ignored.
*
* This method will be affected by any of the
* global common
* or image
* attributes as specified in the
* Rendering Attributes Table.
*
*
* @param img the image to be drawn or null.
* @param x the X coordinate on the destination for the upper left of the image.
* @param y the Y coordinate on the destination for the upper left of the image.
*/
public void drawImage(Image img, double x, double y) {
if (img == null) return;
double sw = img.getWidth();
double sh = img.getHeight();
writeImage(img, x, y, sw, sh);
}
/**
* Draws an image into the given destination rectangle of the canvas. The
* Image is scaled to fit into the destination rectangle.
* A {@code null} image value or an image still in progress will be ignored.
*
* This method will be affected by any of the
* global common
* or image
* attributes as specified in the
* Rendering Attributes Table.
*
*
* @param img the image to be drawn or null.
* @param x the X coordinate on the destination for the upper left of the image.
* @param y the Y coordinate on the destination for the upper left of the image.
* @param w the width of the destination rectangle.
* @param h the height of the destination rectangle.
*/
public void drawImage(Image img, double x, double y, double w, double h) {
writeImage(img, x, y, w, h);
}
/**
* Draws the specified source rectangle of the given image to the given
* destination rectangle of the Canvas.
* A {@code null} image value or an image still in progress will be ignored.
*
* This method will be affected by any of the
* global common
* or image
* attributes as specified in the
* Rendering Attributes Table.
*
*
* @param img the image to be drawn or null.
* @param sx the source rectangle's X coordinate position.
* @param sy the source rectangle's Y coordinate position.
* @param sw the source rectangle's width.
* @param sh the source rectangle's height.
* @param dx the destination rectangle's X coordinate position.
* @param dy the destination rectangle's Y coordinate position.
* @param dw the destination rectangle's width.
* @param dh the destination rectangle's height.
*/
public void drawImage(Image img,
double sx, double sy, double sw, double sh,
double dx, double dy, double dw, double dh)
{
writeImage(img, dx, dy, dw, dh, sx, sy, sw, sh);
}
private PixelWriter writer;
/**
* Returns a {@link PixelWriter} object that can be used to modify
* the pixels of the {@link Canvas} associated with this
* {@code GraphicsContext}.
* All coordinates in the {@code PixelWriter} methods on the returned
* object will be in device space since they refer directly to pixels
* and no other rendering attributes will be applied when modifying
* pixels using this object.
*
* @return the {@code PixelWriter} for modifying the pixels of this
* {@code Canvas}
*/
public PixelWriter getPixelWriter() {
if (writer == null) {
writer = new PixelWriter() {
@Override
public PixelFormat getPixelFormat() {
return PixelFormat.getByteBgraPreInstance();
}
private BytePixelSetter getSetter() {
return ByteBgraPre.setter;
}
@Override
public void setArgb(int x, int y, int argb) {
GrowableDataBuffer buf = getBuffer();
buf.putByte(NGCanvas.PUT_ARGB);
buf.putInt(x);
buf.putInt(y);
buf.putInt(argb);
}
@Override
public void setColor(int x, int y, Color c) {
if (c == null) throw new NullPointerException("Color cannot be null");
int a = (int) Math.round(c.getOpacity() * 255.0);
int r = (int) Math.round(c.getRed() * 255.0);
int g = (int) Math.round(c.getGreen() * 255.0);
int b = (int) Math.round(c.getBlue() * 255.0);
setArgb(x, y, (a << 24) | (r << 16) | (g << 8) | b);
}
private void writePixelBuffer(int x, int y, int w, int h,
byte[] pixels)
{
GrowableDataBuffer buf = getBuffer();
buf.putByte(NGCanvas.PUT_ARGBPRE_BUF);
buf.putInt(x);
buf.putInt(y);
buf.putInt(w);
buf.putInt(h);
buf.putObject(pixels);
}
private int[] checkBounds(int x, int y, int w, int h,
PixelFormat extends Buffer> pf,
int scan)
{
// assert (w >= 0 && h >= 0) - checked by caller
int cw = (int) Math.ceil(theCanvas.getWidth());
int ch = (int) Math.ceil(theCanvas.getHeight());
if (x >= 0 && y >= 0 && x+w <= cw && y+h <= ch) {
return null;
}
int offset = 0;
if (x < 0) {
w += x;
if (w < 0) return null;
if (pf != null) {
switch (pf.getType()) {
case BYTE_BGRA:
case BYTE_BGRA_PRE:
offset -= x * 4;
break;
case BYTE_RGB:
offset -= x * 3;
break;
case BYTE_INDEXED:
case INT_ARGB:
case INT_ARGB_PRE:
offset -= x;
break;
default:
throw new InternalError("unknown Pixel Format");
}
}
x = 0;
}
if (y < 0) {
h += y;
if (h < 0) return null;
offset -= y * scan;
y = 0;
}
if (x + w > cw) {
w = cw - x;
if (w < 0) return null;
}
if (y + h > ch) {
h = ch - y;
if (h < 0) return null;
}
return new int[] {
x, y, w, h, offset
};
}
@Override
public void
setPixels(int x, int y, int w, int h,
PixelFormat pixelformat,
T buffer, int scan)
{
if (pixelformat == null) throw new NullPointerException("PixelFormat cannot be null");
if (buffer == null) throw new NullPointerException("Buffer cannot be null");
if (w <= 0 || h <= 0) return;
int offset = buffer.position();
int adjustments[] = checkBounds(x, y, w, h,
pixelformat, scan);
if (adjustments != null) {
x = adjustments[0];
y = adjustments[1];
w = adjustments[2];
h = adjustments[3];
offset += adjustments[4];
}
byte pixels[] = new byte[w * h * 4];
ByteBuffer dst = ByteBuffer.wrap(pixels);
PixelGetter getter = PixelUtils.getGetter(pixelformat);
PixelConverter converter =
PixelUtils.getConverter(getter, getSetter());
converter.convert(buffer, offset, scan,
dst, 0, w * 4,
w, h);
writePixelBuffer(x, y, w, h, pixels);
}
@Override
public void setPixels(int x, int y, int w, int h,
PixelFormat pixelformat,
byte[] buffer, int offset, int scanlineStride)
{
if (pixelformat == null) throw new NullPointerException("PixelFormat cannot be null");
if (buffer == null) throw new NullPointerException("Buffer cannot be null");
if (w <= 0 || h <= 0) return;
int adjustments[] = checkBounds(x, y, w, h,
pixelformat, scanlineStride);
if (adjustments != null) {
x = adjustments[0];
y = adjustments[1];
w = adjustments[2];
h = adjustments[3];
offset += adjustments[4];
}
byte pixels[] = new byte[w * h * 4];
BytePixelGetter getter = PixelUtils.getByteGetter(pixelformat);
ByteToBytePixelConverter converter =
PixelUtils.getB2BConverter(getter, getSetter());
converter.convert(buffer, offset, scanlineStride,
pixels, 0, w * 4,
w, h);
writePixelBuffer(x, y, w, h, pixels);
}
@Override
public void setPixels(int x, int y, int w, int h,
PixelFormat pixelformat,
int[] buffer, int offset, int scanlineStride)
{
if (pixelformat == null) throw new NullPointerException("PixelFormat cannot be null");
if (buffer == null) throw new NullPointerException("Buffer cannot be null");
if (w <= 0 || h <= 0) return;
int adjustments[] = checkBounds(x, y, w, h,
pixelformat, scanlineStride);
if (adjustments != null) {
x = adjustments[0];
y = adjustments[1];
w = adjustments[2];
h = adjustments[3];
offset += adjustments[4];
}
byte pixels[] = new byte[w * h * 4];
IntPixelGetter getter = PixelUtils.getIntGetter(pixelformat);
IntToBytePixelConverter converter =
PixelUtils.getI2BConverter(getter, getSetter());
converter.convert(buffer, offset, scanlineStride,
pixels, 0, w * 4,
w, h);
writePixelBuffer(x, y, w, h, pixels);
}
@Override
public void setPixels(int dstx, int dsty, int w, int h,
PixelReader reader, int srcx, int srcy)
{
if (reader == null) throw new NullPointerException("Reader cannot be null");
if (w <= 0 || h <= 0) return;
int adjustments[] = checkBounds(dstx, dsty, w, h, null, 0);
if (adjustments != null) {
int newx = adjustments[0];
int newy = adjustments[1];
srcx += newx - dstx;
srcy += newy - dsty;
dstx = newx;
dsty = newy;
w = adjustments[2];
h = adjustments[3];
}
byte pixels[] = new byte[w * h * 4];
reader.getPixels(srcx, srcy, w, h,
PixelFormat.getByteBgraPreInstance(),
pixels, 0, w * 4);
writePixelBuffer(dstx, dsty, w, h, pixels);
}
};
}
return writer;
}
/**
* Sets the effect to be applied after the next draw call, or null to
* disable effects.
* The current effect is a common attribute
* used for nearly all rendering operations as specified in the
* Rendering Attributes Table.
*
* @param e the effect to use, or null to disable effects
*/
public void setEffect(Effect e) {
GrowableDataBuffer buf = getBuffer();
buf.putByte(NGCanvas.EFFECT);
if (e == null) {
curState.effect = null;
buf.putObject(null);
} else {
curState.effect = EffectHelper.copy(e);
EffectHelper.sync(curState.effect);
buf.putObject(EffectHelper.getPeer(curState.effect));
}
}
/**
* Gets a copy of the effect to be applied after the next draw call.
* A null return value means that no effect will be applied after subsequent
* rendering calls.
* The current effect is a common attribute
* used for nearly all rendering operations as specified in the
* Rendering Attributes Table.
*
* @param e an {@code Effect} object that may be used to store the
* copy of the current effect, if it is of a compatible type
* @return the current effect used for all rendering calls,
* or null if there is no current effect
*/
public Effect getEffect(Effect e) {
return curState.effect == null ? null : EffectHelper.copy(curState.effect);
}
/**
* Applies the given effect to the entire bounds of the canvas and stores
* the result back into the same canvas.
* A {@code null} value will be ignored.
* The effect will be applied without any other rendering attributes and
* under an Identity coordinate transform.
* Since the effect is applied to the entire bounds of the canvas, some
* effects may have a confusing result, such as a Reflection effect
* that will apply its reflection off of the bottom of the canvas even if
* only a portion of the canvas has been rendered to and will not be
* visible unless a negative offset is used to bring the reflection back
* into view.
*
* @param e the effect to apply onto the entire destination or null.
*/
public void applyEffect(Effect e) {
if (e == null) return;
GrowableDataBuffer buf = getBuffer();
buf.putByte(NGCanvas.FX_APPLY_EFFECT);
Effect effect = EffectHelper.copy(e);
EffectHelper.sync(effect);
buf.putObject(EffectHelper.getPeer(effect));
}
}