From 9a50c68bd73b1ce6bde4fc80450c44fc118eda03 Mon Sep 17 00:00:00 2001 From: John Hendrikx Date: Mon, 10 Nov 2025 14:59:01 +0100 Subject: [PATCH] Proof of concept drawing context for WritableImage --- .../sun/javafx/tk/quantum/QuantumToolkit.java | 4 +- .../java/com/sun/javafx/util/FXCleaner.java | 31 + .../main/java/com/sun/marlin/MarlinUtils.java | 9 - .../java/com/sun/marlin/OffHeapArray.java | 5 +- .../java/com/sun/marlin/RendererStats.java | 4 +- .../com/sun/prism/sw/SWArgbPreTexture.java | 8 +- .../com/sun/prism/sw/SWDrawingContext.java | 578 +++++++++++++ .../java/com/sun/prism/sw/SWRTTexture.java | 7 +- .../javafx/scene/image/DrawingContext.java | 816 ++++++++++++++++++ .../javafx/scene/image/WritableImage.java | 31 +- tests/manual/graphics/RandomShapesDemo.java | 129 +++ 11 files changed, 1604 insertions(+), 18 deletions(-) create mode 100644 modules/javafx.graphics/src/main/java/com/sun/javafx/util/FXCleaner.java create mode 100644 modules/javafx.graphics/src/main/java/com/sun/prism/sw/SWDrawingContext.java create mode 100644 modules/javafx.graphics/src/main/java/javafx/scene/image/DrawingContext.java create mode 100644 tests/manual/graphics/RandomShapesDemo.java diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/QuantumToolkit.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/QuantumToolkit.java index 3ce2914e553..7f39d8f69f1 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/QuantumToolkit.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/QuantumToolkit.java @@ -1506,8 +1506,8 @@ public void factoryReleased() { @Override public PlatformImage createPlatformImage(int w, int h) { - ByteBuffer bytebuf = ByteBuffer.allocate(w * h * 4); - return com.sun.prism.Image.fromByteBgraPreData(bytebuf, w, h); + IntBuffer buf = IntBuffer.allocate(w * h); + return com.sun.prism.Image.fromIntArgbPreData(buf, w, h); } @Override diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/util/FXCleaner.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/util/FXCleaner.java new file mode 100644 index 00000000000..53d1840a5e6 --- /dev/null +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/util/FXCleaner.java @@ -0,0 +1,31 @@ +package com.sun.javafx.util; + +import java.lang.ref.Cleaner; +import java.lang.ref.Cleaner.Cleanable; + +/** + * A module-wide Cleaner utility for registering cleanup actions on objects + * that become phantom-reachable. This class maintains a single shared + * {@link Cleaner} instance for the module, avoiding multiple daemon threads. + *

+ * Usage example: + *

+ *     FXCleaner.register(resource, () -> resource.dispose());
+ * 
+ */ +public class FXCleaner { + private static final Cleaner CLEANER = Cleaner.create(); + + /** + * Registers a cleanup action to be run when {@code obj} becomes + * phantom-reachable. + * + * @param obj the object to monitor, cannot be {@code null} + * @param action the cleanup action to run, cannot be {@code null} + * @return a {@link Cleanable} that can be used to cancel the cleanup, never {@code null} + * @throws NullPointerException when any argument is {@code null} + */ + public static Cleanable register(Object obj, Runnable action) { + return CLEANER.register(obj, action); + } +} diff --git a/modules/javafx.graphics/src/main/java/com/sun/marlin/MarlinUtils.java b/modules/javafx.graphics/src/main/java/com/sun/marlin/MarlinUtils.java index 55c775a06f7..a4654b659eb 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/marlin/MarlinUtils.java +++ b/modules/javafx.graphics/src/main/java/com/sun/marlin/MarlinUtils.java @@ -77,13 +77,4 @@ public static ThreadGroup getRootThreadGroup() { } return currentTG; } - - // JavaFX specific Cleaner for Marlin-FX: - // Module issue with jdk.internal.ref.Cleaner - private final static java.lang.ref.Cleaner cleaner - = java.lang.ref.Cleaner.create(); - - static java.lang.ref.Cleaner getCleaner() { - return cleaner; - } } diff --git a/modules/javafx.graphics/src/main/java/com/sun/marlin/OffHeapArray.java b/modules/javafx.graphics/src/main/java/com/sun/marlin/OffHeapArray.java index f655beee2fe..ee6aaec63c3 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/marlin/OffHeapArray.java +++ b/modules/javafx.graphics/src/main/java/com/sun/marlin/OffHeapArray.java @@ -26,6 +26,9 @@ package com.sun.marlin; import static com.sun.marlin.MarlinConst.LOG_OFF_HEAP_MALLOC; + +import com.sun.javafx.util.FXCleaner; + import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; import java.lang.foreign.ValueLayout; @@ -79,7 +82,7 @@ final class OffHeapArray { if (!global) { // Register a cleaning function to ensure freeing off-heap memory: - MarlinUtils.getCleaner().register(parent, this::free); + FXCleaner.register(parent, this::free); } } diff --git a/modules/javafx.graphics/src/main/java/com/sun/marlin/RendererStats.java b/modules/javafx.graphics/src/main/java/com/sun/marlin/RendererStats.java index 23f134e781d..acde99e1a2c 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/marlin/RendererStats.java +++ b/modules/javafx.graphics/src/main/java/com/sun/marlin/RendererStats.java @@ -28,6 +28,8 @@ import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.ConcurrentLinkedQueue; + +import com.sun.javafx.util.FXCleaner; import com.sun.marlin.ArrayCacheConst.CacheStats; import static com.sun.marlin.MarlinUtils.logInfo; import com.sun.marlin.stats.Histogram; @@ -383,7 +385,7 @@ void add(final Object parent, final RendererStats stats) { allStats.add(stats); // Register a cleaning function to ensure removing dead entries: - MarlinUtils.getCleaner().register(parent, () -> remove(stats)); + FXCleaner.register(parent, () -> remove(stats)); } void remove(final RendererStats stats) { diff --git a/modules/javafx.graphics/src/main/java/com/sun/prism/sw/SWArgbPreTexture.java b/modules/javafx.graphics/src/main/java/com/sun/prism/sw/SWArgbPreTexture.java index c039e179c7b..9679163e312 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/prism/sw/SWArgbPreTexture.java +++ b/modules/javafx.graphics/src/main/java/com/sun/prism/sw/SWArgbPreTexture.java @@ -45,8 +45,14 @@ class SWArgbPreTexture extends SWTexture { private boolean hasAlpha = true; SWArgbPreTexture(SWResourceFactory factory, WrapMode wrapMode, int w, int h) { + this(factory, wrapMode, w, h, null); + } + + SWArgbPreTexture(SWResourceFactory factory, WrapMode wrapMode, int w, int h, int[] data) { super(factory, wrapMode, w, h); - offset = 0; + + this.allocated = data != null; + this.data = data; } SWArgbPreTexture(SWArgbPreTexture sharedTex, WrapMode altMode) { diff --git a/modules/javafx.graphics/src/main/java/com/sun/prism/sw/SWDrawingContext.java b/modules/javafx.graphics/src/main/java/com/sun/prism/sw/SWDrawingContext.java new file mode 100644 index 00000000000..2e2e23ab3ef --- /dev/null +++ b/modules/javafx.graphics/src/main/java/com/sun/prism/sw/SWDrawingContext.java @@ -0,0 +1,578 @@ +/* + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.sun.prism.sw; + +import com.sun.glass.ui.Screen; +import com.sun.glass.utils.NativeLibLoader; +import com.sun.javafx.geom.Arc2D; +import com.sun.javafx.geom.Path2D; +import com.sun.javafx.geom.Rectangle; +import com.sun.javafx.tk.Toolkit; +import com.sun.javafx.util.FXCleaner; +import com.sun.prism.BasicStroke; +import com.sun.prism.CompositeMode; +import com.sun.prism.Graphics; +import com.sun.prism.PixelFormat; +import com.sun.prism.Texture; +import com.sun.prism.Texture.Usage; + +import java.nio.IntBuffer; +import java.util.Objects; +import java.util.function.Consumer; + +import javafx.scene.effect.BlendMode; +import javafx.scene.image.DrawingContext; +import javafx.scene.image.Image; +import javafx.scene.image.PixelBuffer; +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; + +// TODO dashes +// TODO save/restore +/** + * A software-based drawing context for {@link com.sun.prism.Image} that allows direct rendering of shapes, paths, and + * images into a Prism image buffer. + *

+ * This class provides a familiar JavaFX-style drawing API for Prism images, enabling modification of the image contents + * using lines, rectangles, ovals, rounded rectangles, arcs, polygons, and images without the need for a Canvas or + * {@link javafx.scene.image.PixelWriter}. + *

+ * It uses Prism's {@link Graphics} and {@link BasicStroke} for rendering, supporting strokes, fills, alpha + * transparency, limited blend modes, and automatic dirty-region tracking to efficiently mark affected pixels. + *

+ * Features include: + *

+ *

+ * Limitations: + *

+ *

+ * This class is intended for use in contexts where direct drawing into a Prism image is needed, such as the + * {@code getDrawingContext()} method in WritableImage, providing a more convenient API than PixelWriter or snapshotting + * a Canvas. + * + * @see DrawingContext + * @see com.sun.prism.Image + * @since 26 + */ +public class SWDrawingContext implements DrawingContext { + private static final double SQRT2 = Math.sqrt(2); + + static { + NativeLibLoader.loadLibrary("prism_sw"); + } + + private final Graphics graphics; + private final SWResourceFactory resourceFactory; + private final Consumer pixelsDirty; + + // Common rendering attributes + private double globalAlpha = 1.0; + private BlendMode globalBlendMode = BlendMode.SRC_OVER; + + // Fill attributes + private Paint fill = Color.BLACK; + + // Stroke attributes + private Paint stroke = Color.BLACK; + private double lineWidth = 1.0; + private StrokeLineCap lineCap = StrokeLineCap.SQUARE; + private StrokeLineJoin lineJoin = StrokeLineJoin.MITER; + private double miterLimit = 10.0; + + // Path attributes + private FillRule fillRule = FillRule.NON_ZERO; + + // Image attributes + private boolean imageSmoothing = true; + + // Cached prism values + private com.sun.prism.paint.Paint prismFillPaint = com.sun.prism.paint.Color.BLACK; + private com.sun.prism.paint.Paint prismStrokePaint = com.sun.prism.paint.Color.BLACK; + private BasicStroke prismStroke; + + /** + * Constructs a new instance. + *

+ * The provided image must be backed by a writable {@link PixelBuffer} with + * format {@link PixelFormat#INT_ARGB_PRE INT_ARGB_PRE}. + * + * @param img a prism image, cannot be {@code null} + * @param pixelsDirty a consumer of dirty rectangles, cannot be {@code null} + * @throws NullPointerException if any argument is {@code null} + * @throws IllegalStateException if the image is not backed by a writable pixel buffer + * in the correct format. + */ + public SWDrawingContext(com.sun.prism.Image img, Consumer pixelsDirty) { + int[] data = switch (img.getPixelBuffer()) { + case IntBuffer ib -> ib.array(); + default -> throw new IllegalStateException("img must contain an accessible int buffer backed by an int array"); + }; + + this.pixelsDirty = Objects.requireNonNull(pixelsDirty, "pixelsDirty"); + this.resourceFactory = new SWResourceFactory(Screen.getMainScreen()); // Note, actual screen is irrelevant, we just need one + + SWRTTexture texture = new SWRTTexture(resourceFactory, img.getWidth(), img.getHeight(), data); + + this.graphics = texture.createGraphics(); + + FXCleaner.register(this, new StateCleaner(resourceFactory, texture)); + } + + private record StateCleaner(SWResourceFactory resourceFactory, SWRTTexture texture) implements Runnable { + @Override + public void run() { + texture.dispose(); + resourceFactory.dispose(); + } + } + + @Override + public Paint getStroke() { + return stroke; + } + + @Override + public void setStroke(Paint p) { + if (p != null) { + this.stroke = p; + this.prismStrokePaint = (com.sun.prism.paint.Paint)Toolkit.getToolkit().getPaint(p); + } + } + + @Override + public Paint getFill() { + return fill; + } + + @Override + public void setFill(Paint p) { + if (p != null) { + this.fill = p; + this.prismFillPaint = (com.sun.prism.paint.Paint)Toolkit.getToolkit().getPaint(p); + } + } + + @Override + public double getGlobalAlpha() { + return globalAlpha; + } + + @Override + public void setGlobalAlpha(double alpha) { + this.globalAlpha = Math.clamp(alpha, 0.0, 1.0); + + graphics.setExtraAlpha((float) globalAlpha); + } + + @Override + public BlendMode getGlobalBlendMode() { + return globalBlendMode; + } + + @Override + public void setGlobalBlendMode(BlendMode op) { + if (op != null) { + CompositeMode cm = switch (op) { + case SRC_OVER -> CompositeMode.SRC_OVER; + case ADD -> CompositeMode.ADD; + default -> throw new IllegalArgumentException("Unsupported blend mode: " + op); + }; + + this.globalBlendMode = op; + + graphics.setCompositeMode(cm); + } + } + + @Override + public FillRule getFillRule() { + return fillRule; + } + + @Override + public void setFillRule(FillRule fillRule) { + if(fillRule != null) { + this.fillRule = fillRule; + } + } + + @Override + public double getLineWidth() { + return lineWidth; + } + + @Override + public void setLineWidth(double lw) { + if(lw > 0 && lw < Double.POSITIVE_INFINITY && lw != lineWidth) { + this.lineWidth = lw; + + invalidateStroke(); + } + } + + @Override + public StrokeLineCap getLineCap() { + return lineCap; + } + + @Override + public void setLineCap(StrokeLineCap cap) { + if(cap != null && cap != lineCap) { + this.lineCap = cap; + + invalidateStroke(); + } + } + + @Override + public StrokeLineJoin getLineJoin() { + return lineJoin; + } + + @Override + public void setLineJoin(StrokeLineJoin join) { + if (join != null && join != lineJoin) { + this.lineJoin = join; + + invalidateStroke(); + } + } + + @Override + public double getMiterLimit() { + return miterLimit; + } + + @Override + public void setMiterLimit(double ml) { + if (ml > 0.0 && ml < Double.POSITIVE_INFINITY && ml != miterLimit) { + this.miterLimit = ml; + + invalidateStroke(); + } + } + + @Override + public boolean isImageSmoothing() { + return imageSmoothing; + } + + @Override + public void setImageSmoothing(boolean imageSmoothing) { + this.imageSmoothing = imageSmoothing; + } + + @Override + public void strokeLine(double x1, double y1, double x2, double y2) { + applyStrokeParameters(); + + graphics.drawLine((float)x1, (float)y1, (float)x2, (float)y2); + + markStrokeRectDirty(x1, y1, Math.abs(x2 - x1), Math.abs(y2 - y1)); + } + + @Override + public void strokeRect(double x, double y, double w, double h) { + if(w != 0 || h != 0) { + applyStrokeParameters(); + + graphics.drawRect((float)x, (float)y, (float)w, (float)h); + + markStrokeRectDirty(x, y, w, h); + } + } + + @Override + public void clearRect(double x, double y, double w, double h) { + if (w != 0 && h != 0) { + graphics.clearQuad((float)x, (float)y, (float)(x + w), (float)(x + h)); + + markRectDirty(x, y, w, h); + } + } + + @Override + public void fillRect(double x, double y, double w, double h) { + if (w != 0 && h != 0) { + graphics.setPaint(prismFillPaint); + graphics.fillRect((float)x, (float)y, (float)w, (float)h); + + markRectDirty(x, y, w, h); + } + } + + @Override + public void strokeRoundRect(double x, double y, double w, double h, double arcWidth, double arcHeight) { + if (w != 0 || h != 0) { + applyStrokeParameters(); + + graphics.drawRoundRect((float)x, (float)y, (float)w, (float)h, (float)arcWidth, (float)arcHeight); + + markStrokeRectDirty(x, y, w, h); + } + } + + @Override + public void fillRoundRect(double x, double y, double w, double h, double arcWidth, double arcHeight) { + if (w != 0 && h != 0) { + graphics.setPaint(prismFillPaint); + graphics.fillRoundRect((float)x, (float)y, (float)w, (float)h, (float)arcWidth, (float)arcHeight); + + markRectDirty(x, y, w, h); + } + } + + @Override + public void strokeOval(double x, double y, double w, double h) { + if (w != 0 || h != 0) { + applyStrokeParameters(); + + graphics.drawEllipse((float)x, (float)y, (float)w, (float)h); + + markStrokeRectDirty(x, y, w, h); + } + } + + @Override + public void fillOval(double x, double y, double w, double h) { + if (w != 0 && h != 0) { + applyStrokeParameters(); + + graphics.setPaint(prismFillPaint); + graphics.fillEllipse((float)x, (float)y, (float)w, (float)h); + + markRectDirty(x, y, w, h); + } + } + + @Override + public void strokeArc(double x, double y, double w, double h, double startAngle, double arcExtent, ArcType closure) { + if (w != 0 && h != 0 && closure != null) { + int arcType = switch (closure) { + case CHORD -> Arc2D.CHORD; + case OPEN -> Arc2D.OPEN; + case ROUND -> Arc2D.PIE; + }; + + applyStrokeParameters(); + + graphics.draw(new Arc2D((float)x, (float)y, (float)w, (float)h, (float)startAngle, (float)arcExtent, arcType)); + + markStrokeRectDirty(x, y, w, h); + } + } + + @Override + public void fillArc(double x, double y, double w, double h, double startAngle, double arcExtent, ArcType closure) { + if (w != 0 && h != 0 && closure != null) { + int arcType = switch (closure) { + case CHORD -> Arc2D.CHORD; + case OPEN -> Arc2D.OPEN; + case ROUND -> Arc2D.PIE; + }; + + graphics.setPaint(prismFillPaint); + graphics.fill(new Arc2D((float)x, (float)y, (float)w, (float)h, (float)startAngle, (float)arcExtent, arcType)); + + markRectDirty(x, y, w, h); + } + } + + @Override + public void strokePolyline(double[] xPoints, double[] yPoints, int nPoints) { + strokePolyline(xPoints, yPoints, nPoints, false); + } + + @Override + public void strokePolygon(double[] xPoints, double[] yPoints, int nPoints) { + strokePolyline(xPoints, yPoints, nPoints, true); + } + + private void strokePolyline(double[] xPoints, double[] yPoints, int nPoints, boolean close) { + if (xPoints != null && yPoints != null && nPoints >= 2 && xPoints.length >= nPoints && yPoints.length >= nPoints) { + Path2D path = new Path2D(); + double minX = xPoints[0]; + double maxX = xPoints[0]; + double minY = yPoints[0]; + double maxY = yPoints[0]; + + path.moveTo((float)xPoints[0], (float)yPoints[0]); + + for (int i = 1; i < nPoints; i++) { + path.lineTo((float)xPoints[i], (float)yPoints[i]); + + minX = Math.min(minX, xPoints[i]); + minY = Math.min(minY, yPoints[i]); + maxX = Math.max(maxX, xPoints[i]); + maxY = Math.max(maxY, yPoints[i]); + } + + if (close) { + path.closePath(); + } + + applyStrokeParameters(); + + graphics.draw(path); + + markStrokeRectDirty(minX, minY, maxX - minX, maxY - minY); + } + } + + @Override + public void fillPolygon(double[] xPoints, double[] yPoints, int nPoints) { + if (xPoints != null && yPoints != null && nPoints >= 3 && xPoints.length >= nPoints && yPoints.length >= nPoints) { + Path2D path = new Path2D(switch (fillRule) { + case EVEN_ODD -> Path2D.WIND_EVEN_ODD; + case NON_ZERO -> Path2D.WIND_NON_ZERO; + }); + double minX = xPoints[0]; + double maxX = xPoints[0]; + double minY = yPoints[0]; + double maxY = yPoints[0]; + + path.moveTo((float)xPoints[0], (float)yPoints[0]); + + for (int i = 1; i < nPoints; i++) { + path.lineTo((float)xPoints[i], (float)yPoints[i]); + + minX = Math.min(minX, xPoints[i]); + minY = Math.min(minY, yPoints[i]); + maxX = Math.max(maxX, xPoints[i]); + maxY = Math.max(maxY, yPoints[i]); + } + + path.closePath(); + + graphics.setPaint(prismFillPaint); + graphics.fill(path); + + markRectDirty(minX, minY, maxX - minX, maxY - minY); + } + } + + @Override + public void drawImage(Image img, double sx, double sy, double sw, double sh, double dx, double dy, double dw, double dh) { + if (img == null || img.getProgress() < 1.0) { + return; + } + + Object platformImage = Toolkit.getImageAccessor().getPlatformImage(img); + + // Ensure it's a Prism image + if (!(platformImage instanceof com.sun.prism.Image prismImage)) { + throw new IllegalArgumentException("PlatformImage must be a Prism Image"); + } + + // Create a texture from the Prism image + Texture tex = resourceFactory.createTexture(prismImage, Usage.DEFAULT, Texture.WrapMode.CLAMP_TO_EDGE); + + if (tex == null) { + throw new IllegalStateException("Unable to draw image, insufficient resources"); + } + + try { + tex.setLinearFiltering(imageSmoothing); + + graphics.drawTexture(tex, (float)dx, (float)dy, (float)(dx + dw), (float)(dy + dh), (float)sx, (float)sy, (float)(sx + sw), (float)(sy + sh)); + + markRectDirty(dx, dy, dw, dh); + } + finally { + tex.dispose(); + } + } + + private void applyStrokeParameters() { + if(prismStroke == null) { + this.prismStroke = new BasicStroke( + (float)lineWidth, + switch (lineCap) { + case BUTT -> BasicStroke.CAP_BUTT; + case ROUND -> BasicStroke.CAP_ROUND; + case SQUARE -> BasicStroke.CAP_SQUARE; + }, + switch (lineJoin) { + case BEVEL -> BasicStroke.JOIN_BEVEL; + case ROUND -> BasicStroke.JOIN_ROUND; + case MITER -> BasicStroke.JOIN_MITER; + }, + (float)miterLimit + ); + } + + graphics.setStroke(prismStroke); + graphics.setPaint(prismStrokePaint); + } + + private void invalidateStroke() { + this.prismStroke = null; + } + + private void markStrokeRectDirty(double x, double y, double w, double h) { + // Base half-width expansion + double halfWidth = lineWidth * 0.5; + + // Determine additional expansion factor based on caps and joins + double expansionFactor = switch (lineJoin) { + case MITER -> Math.max(miterLimit, lineCap == StrokeLineCap.SQUARE ? SQRT2 : 1.0); + case BEVEL, ROUND -> lineCap == StrokeLineCap.SQUARE ? SQRT2 : 1.0; + }; + + // Total expansion radius + double r = halfWidth * expansionFactor; + + // Expand the rectangle + double dirtyX = x - r; + double dirtyY = y - r; + double dirtyW = w + r * 2.0; + double dirtyH = h + r * 2.0; + + markRectDirty(dirtyX, dirtyY, dirtyW, dirtyH); + } + + // TODO It seems bufferDirty only remembers the last rect; may need to update this only once per frame + // Note: if called multiple times per frame, then it just updates everything (optimize?) + private void markRectDirty(double x, double y, double w, double h) { + int fx = (int)Math.floor(x); + int fy = (int)Math.floor(y); + + pixelsDirty.accept(new Rectangle(fx, fy, (int)Math.ceil(x + w) - fx, (int)Math.ceil(y + h) - fy)); + } +} diff --git a/modules/javafx.graphics/src/main/java/com/sun/prism/sw/SWRTTexture.java b/modules/javafx.graphics/src/main/java/com/sun/prism/sw/SWRTTexture.java index 75b8828bbd7..6e50cc1898c 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/prism/sw/SWRTTexture.java +++ b/modules/javafx.graphics/src/main/java/com/sun/prism/sw/SWRTTexture.java @@ -46,7 +46,12 @@ class SWRTTexture extends SWArgbPreTexture implements RTTexture { private boolean isOpaque; SWRTTexture(SWResourceFactory factory, int w, int h) { - super(factory, WrapMode.CLAMP_TO_ZERO, w, h); + this(factory, w, h, null); + } + + SWRTTexture(SWResourceFactory factory, int w, int h, int[] data) { + super(factory, WrapMode.CLAMP_TO_ZERO, w, h, data); + this.allocate(); this.surface = new JavaSurface(getDataNoClone(), RendererBase.TYPE_INT_ARGB_PRE, w, h); this.dimensions.setBounds(0, 0, w, h); diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/image/DrawingContext.java b/modules/javafx.graphics/src/main/java/javafx/scene/image/DrawingContext.java new file mode 100644 index 00000000000..f71ce64ae17 --- /dev/null +++ b/modules/javafx.graphics/src/main/java/javafx/scene/image/DrawingContext.java @@ -0,0 +1,816 @@ +/* + * Copyright (c) 2012, 2025, 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.image; + +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.canvas.Canvas; +import javafx.scene.effect.BlendMode; +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; + +/** + * Interface providing basic drawing operations. An instance of this interface + * is provided by {@link WritableImage} and {@link Canvas} via {@link WritableImage#getDrawingContext()} + * and {@link Canvas#getGraphicsContext2D()}. + *

+ * The provider of this interface may be associated with a {@link Node} which may + * be attached to a {@link Scene}. If the associated node is not attached to any scene, + * then the operations provided here can be used from any thread, as long as it is only + * used from one thread at a time. Once the node is attached to a scene, the operations must + * be called from the JavaFX Application Thread. + *

+ * TODO A {@code DrawingContext} also manages a stack of state objects that can + * be saved or restored at anytime. + *

+ * The {@code DrawingContext} maintains the following rendering attributes + * which affect various subsets of the rendering methods: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
List of Rendering Attributes
AttributeSave/Restore?Default valueDescription
Common Rendering Attributes
{@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. + *
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. + *
Path Attributes
{@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)}. + *
+ *

+ * + * The various rendering methods on the {@code DrawingContext} use the + * following sets of rendering attributes: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Rendering Attributes Table
MethodCommon Rendering AttributesFill AttributesStroke AttributesPath AttributesImage 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()} + * YesYesNoNoNo
+ * {@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()} + * YesNoYesNoNo
+ * {@link #clearRect(double, double, double, double) clearRect()} + * YesNoNoNoNo
+ * {@link #fillPolygon(double[], double[], int) fillPolygon()} + * YesYesNoYes [1]No
+ * {@link #strokePolygon(double[], double[], int) strokePolygon()}, + * {@link #strokePolyline(double[], double[], int) strokePolyline()} + * YesNoYesNoNo
+ * [1] Only the Fill Rule applies to fillPolygon() + *
Image Rendering
+ * {@link #drawImage(javafx.scene.image.Image, double, double) drawImage(all forms)} + * YesNoNoNoYes
+ * + *

Example:

+ * + *
+ * import javafx.scene.*;
+ * import javafx.scene.image.*;
+ * import javafx.scene.paint.*;
+ *
+ * WritableImage writableImage = new WritableImage(250,250);
+ * ImageView root = new ImageView(writableImage);
+ * Scene s = new Scene(root, 300, 300, Color.BLACK);
+ *
+ * DrawingContext c = writableImage.getDrawingContext();
+ *
+ * c.setFill(Color.BLUE);
+ * c.fillRect(75,75,100,100);
+ * 
+ * + * @see Canvas + * @see WritableImage + * @since 26 + */ +public interface DrawingContext { + + /** + * 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}. + */ + Paint getStroke(); + + /** + * 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. + */ + void setStroke(Paint p); + + /** + * 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 The {@code Paint} to be used as the fill {@code Paint}. + */ + Paint getFill(); + + /** + * 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. + */ + void setFill(Paint p); + + /** + * 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. + */ + double getGlobalAlpha(); + + /** + * 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. + */ + void setGlobalAlpha(double alpha); + + /** + * 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. + */ + BlendMode getGlobalBlendMode(); + + /** + * 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. + */ + void setGlobalBlendMode(BlendMode op); + + /** + * 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. + */ + FillRule getFillRule(); + + /** + * 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. + */ + void setFillRule(FillRule fillRule); + + /** + * 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. + */ + double getLineWidth(); + + /** + * 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. + */ + void setLineWidth(double lw); + + /** + * 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. + */ + StrokeLineCap getLineCap(); + + /** + * 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. + */ + void setLineCap(StrokeLineCap cap); + + /** + * 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. + */ + StrokeLineJoin getLineJoin(); + + /** + * 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. + */ + void setLineJoin(StrokeLineJoin join); + + /** + * 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} + */ + double getMiterLimit(); + + /** + * 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. + */ + void setMiterLimit(double ml); + + /** + * Gets the current image smoothing state. + * + * @defaultValue {@code true} + * @return image smoothing state + */ + boolean isImageSmoothing(); + + /** + * 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 + */ + void setImageSmoothing(boolean imageSmoothing); + + /** + * 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. + */ + void strokeLine(double x1, double y1, double x2, double y2); + + /** + * 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. + */ + void strokeRect(double x, double y, double w, double h); + + /** + * 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. + */ + void clearRect(double x, double y, double w, double h); + + /** + * 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. + */ + void fillRect(double x, double y, double w, double h); + + /** + * 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. + */ + void strokeRoundRect(double x, double y, double w, double h, double arcWidth, double arcHeight); + + /** + * 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. + */ + void fillRoundRect(double x, double y, double w, double h, double arcWidth, double arcHeight); + + /** + * 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. + */ + void strokeOval(double x, double y, double w, double h); + + /** + * 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. + */ + void fillOval(double x, double y, double w, double h); + + /** + * 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 + */ + void strokeArc(double x, double y, double w, double h, double startAngle, double arcExtent, ArcType closure); + + /** + * 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. + */ + void fillArc(double x, double y, double w, double h, double startAngle, double arcExtent, ArcType closure); + + /** + * 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. + */ + void strokePolyline(double xPoints[], double yPoints[], int nPoints); + + /** + * 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. + */ + void strokePolygon(double[] xPoints, double[] yPoints, int nPoints); + + /** + * 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. + */ + void fillPolygon(double xPoints[], double yPoints[], int nPoints); + + /** + * 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. + */ + default void drawImage(Image img, double x, double y) { + if (img == null || img.getProgress() < 1.0) { + return; + } + + drawImage(img, x, y, img.getWidth(), img.getHeight()); + } + + /** + * 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. + */ + default void drawImage(Image img, double x, double y, double w, double h) { + if (img == null || img.getProgress() < 1.0) { + return; + } + + drawImage(img, 0, 0, img.getWidth(), img.getHeight(), 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. + */ + void drawImage( + Image img, + double sx, double sy, double sw, double sh, + double dx, double dy, double dw, double dh + ); + +} diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/image/WritableImage.java b/modules/javafx.graphics/src/main/java/javafx/scene/image/WritableImage.java index 6b1440f75b7..4be7db4f5f1 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/image/WritableImage.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/image/WritableImage.java @@ -29,15 +29,18 @@ import com.sun.javafx.tk.ImageLoader; import com.sun.javafx.tk.PlatformImage; import com.sun.javafx.tk.Toolkit; -import javafx.beans.NamedArg; -import javafx.beans.property.ReadOnlyObjectProperty; -import javafx.scene.paint.Color; +import com.sun.prism.sw.SWDrawingContext; +import java.lang.ref.WeakReference; import java.nio.Buffer; import java.nio.ByteBuffer; import java.nio.IntBuffer; import java.util.Objects; +import javafx.beans.NamedArg; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.scene.paint.Color; + /** * The {@code WritableImage} class represents a custom graphical image * that is constructed from pixels supplied by the application, and possibly @@ -157,6 +160,28 @@ public WritableImage(@NamedArg("reader") PixelReader reader, getPixelWriter().setPixels(0, 0, width, height, reader, x, y); } + private WeakReference drawingContextRef; + + /** + * Returns the {@link DrawingContext} associated with this image. + * + * @return the {@link DrawingContext} associated with this image, never {@code null} + */ + public DrawingContext getDrawingContext() { + SWDrawingContext context = drawingContextRef == null ? null : drawingContextRef.get(); + + if (context == null) { + if (!(getWritablePlatformImage() instanceof com.sun.prism.Image img)) { + throw new IllegalStateException("platformImage must be a prism image"); + } + + context = new SWDrawingContext(img, rect -> bufferDirty(rect)); + drawingContextRef = new WeakReference<>(context); + } + + return context; + } + @Override boolean isAnimation() { return true; diff --git a/tests/manual/graphics/RandomShapesDemo.java b/tests/manual/graphics/RandomShapesDemo.java new file mode 100644 index 00000000000..910815d1c20 --- /dev/null +++ b/tests/manual/graphics/RandomShapesDemo.java @@ -0,0 +1,129 @@ +import java.util.Random; + +import javafx.animation.Animation; +import javafx.animation.Animation.Status; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; +import javafx.application.Application; +import javafx.scene.Scene; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.image.DrawingContext; +import javafx.scene.image.ImageView; +import javafx.scene.image.WritableImage; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.stage.Stage; +import javafx.util.Duration; + +/** + * Shows a WritableImage and Canvas side by side performing + * the same drawing operations. + */ +public class RandomShapesDemo extends Application { + private static final int WIDTH = 400; + private static final int HEIGHT = 400; + private static final int SHAPES = 20; + + private final Random rnd = new Random(); + + @Override + public void start(Stage primaryStage) { + WritableImage wimg = new WritableImage(WIDTH, HEIGHT); + DrawingContext dc = wimg.getDrawingContext(); + ImageView imageView = new ImageView(wimg); + Canvas canvas = new Canvas(WIDTH, HEIGHT); + GraphicsContext gc = canvas.getGraphicsContext2D(); + + gc.setFill(Color.WHITE); + gc.fillRect(0, 0, WIDTH, HEIGHT); + + dc.setFill(Color.WHITE); + dc.fillRect(0, 0, WIDTH, HEIGHT); + + drawRandomShapes(dc, gc); + + BorderPane root = new BorderPane(); + Label label = new Label("WritableImage in ImageView"); + Label label2 = new Label("Canvas"); + + label.setStyle("-fx-text-fill: white; -fx-font-weight: bold"); + label2.setStyle("-fx-text-fill: white; -fx-font-weight: bold"); + + HBox hbox = new HBox(10, new VBox(label, imageView), new VBox(label2, canvas)); + Button button = new Button("Toggle Animation"); + + Timeline timeline = new Timeline( + new KeyFrame(Duration.ZERO, e -> addRandomShape(dc, gc)), + new KeyFrame(Duration.millis(200)) + ); + + timeline.setCycleCount(Animation.INDEFINITE); + + button.setOnAction(e -> { + if(timeline.getStatus() == Status.RUNNING) { + timeline.stop(); + } + else { + timeline.playFromStart(); + } + }); + + hbox.setSpacing(10); + hbox.setStyle("-fx-background-color: BLACK; -fx-border-width: 10; -fx-border-color: BLACK;"); + + root.setTop(button); + root.setCenter(hbox); + + Scene scene = new Scene(root); + + primaryStage.setScene(scene); + primaryStage.setTitle("Random Shapes: WritableImage vs Canvas"); + primaryStage.show(); + } + + private void drawRandomShapes(DrawingContext ctx, GraphicsContext gc) { + for (int i = 0; i < SHAPES; i++) { + addRandomShape(ctx, gc); + } + } + + private void addRandomShape(DrawingContext dc, GraphicsContext gc) { + Color randomFill = randomColor(); + Color randomStroke = randomColor(); + double width = rnd.nextDouble() * 10; + + dc.setFill(randomFill); + dc.setStroke(randomStroke); + dc.setLineWidth(width); + gc.setFill(randomFill); + gc.setStroke(randomStroke); + gc.setLineWidth(width); + + double x = rnd.nextDouble() * WIDTH; + double y = rnd.nextDouble() * HEIGHT; + double w = 20 + rnd.nextDouble() * 80; + double h = 20 + rnd.nextDouble() * 80; + + if (rnd.nextBoolean()) { + dc.fillRect(x, y, w, h); + gc.fillRect(x, y, w, h); + } + else { + dc.strokeOval(x, y, w, h); + gc.strokeOval(x, y, w, h); + } + } + + private Color randomColor() { + return Color.color(rnd.nextDouble(), rnd.nextDouble(), rnd.nextDouble(), rnd.nextDouble()); + } + + public static void main(String[] args) { + launch(args); + } +}