Added javafx mandelbrot zoomer demo.
authorNot Zed <notzed@gmail.com>
Sat, 25 Jan 2020 01:58:03 +0000 (12:28 +1030)
committerNot Zed <notzed@gmail.com>
Sat, 25 Jan 2020 01:58:03 +0000 (12:28 +1030)
Added ability to run demos from make.

Makefile
README
config.make.in
nbproject/project.properties
src/notzed.zcl.fxdemo/classes/fxdemo/fract/Calculate.java [new file with mode: 0644]
src/notzed.zcl.fxdemo/classes/fxdemo/fract/Mandelbrot.java [new file with mode: 0644]
src/notzed.zcl.fxdemo/classes/fxdemo/fract/mandelbrot.cl [new file with mode: 0644]
src/notzed.zcl.fxdemo/classes/fxdemo/fract/usage.txt [new file with mode: 0644]
src/notzed.zcl.fxdemo/classes/module-info.java [new file with mode: 0644]

index bcc5f51..45703c9 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -10,7 +10,28 @@ dist_EXTRA=README                            \
 
 include config.make
 
-java_MODULES=notzed.zcl notzed.zcl.demo
+java_MODULES=notzed.zcl notzed.zcl.demo notzed.zcl.fxdemo
 notzed.zcl.demo_JDEPMOD=notzed.zcl
 
+notzed.zcl.fxdemo_JDEPMOD=notzed.zcl
+notzed.zcl.fxdemo_JAVAMODPATH=$(JAVAFX_HOME)/lib
+
 include java.make
+
+# ######################################################################
+# Work in progress idea for java.make extension to create execution templates
+
+notzed.zcl.demo_DEMOS=au.notzed.zcl.tools.clinfo
+notzed.zcl.fxdemo_DEMOS=fxdemo.fract.Mandelbrot
+
+DEMOFLAGS=--add-exports jdk.incubator.foreign/jdk.incubator.foreign.unsafe=notzed.zcl
+
+# module class basename(class)
+define java_demo=
+run-$(3): $1
+       $(JAVA_HOME)/bin/java \
+               $(DEMOFLAGS) \
+               --module-path $$(subst $$(S),:,bin/modules $(JAVAMODPATH) $$($1_JAVAMODPATH)) \
+               -m $1/$2 $(ARGV)
+endef
+$(foreach module,$(java_MODULES),$(foreach demo,$($(module)_DEMOS),$(eval $(call java_demo,$(module),$(demo),$(lastword $(subst ., ,$(demo)))))))
diff --git a/README b/README
index 591c71f..673c5b7 100644 (file)
--- a/README
+++ b/README
@@ -8,6 +8,13 @@ other issues.
 Various native support code is in the temporary package api.*,
 eventually to be moved to a new nativez.
 
+I've added 'run-X' targets to run the demos.  Arguments are passed
+using ARGV.
+
+make run-clinfo
+make run-Mandelbrot
+make run-Mandelbrot ARGV="--gamma=15 --rotate=0"
+
 
 INTRODUCTION
 ------------
index dba9824..9c4401b 100644 (file)
@@ -2,6 +2,7 @@
 TARGET ?= linux-amd64
 
 JAVA_HOME ?= /usr/local/jdk-13+33
+JAVAFX_HOME ?= /home/notzed/src/jfx/build/sdk
 
 JAVACFLAGS += -source 13
 JAVACFLAGS += --add-exports jdk.incubator.foreign/jdk.incubator.foreign.unsafe=notzed.zcl
index 20b0bb1..faa0316 100644 (file)
@@ -43,7 +43,8 @@ javac.classpath=
 javac.compilerargs=-Xlint:unchecked --add-exports jdk.incubator.foreign/jdk.incubator.foreign.unsafe=notzed.zcl
 javac.deprecation=false
 javac.external.vm=false
-javac.modulepath=
+javac.modulepath=\
+    ${libs.JavaFX_13.classpath}
 javac.processormodulepath=
 javac.processorpath=\
     ${javac.classpath}
@@ -75,6 +76,7 @@ jlink.additionalparam=
 jlink.launcher=true
 jlink.launcher.name=notzed.zcl
 platform.active=JDK_15
+project.license=gpl3-notzed
 run.classpath=
 # Space-separated list of JVM arguments used when running the project.
 # You may also define separate properties like run-sys-prop.name=value instead of -Dname=value.
diff --git a/src/notzed.zcl.fxdemo/classes/fxdemo/fract/Calculate.java b/src/notzed.zcl.fxdemo/classes/fxdemo/fract/Calculate.java
new file mode 100644 (file)
index 0000000..ab069b1
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2020 Michael Zucchi
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package fxdemo.fract;
+
+import static au.notzed.zcl.CL.*;
+import au.notzed.zcl.CLBuffer;
+import au.notzed.zcl.CLCommandQueue;
+import au.notzed.zcl.CLContext;
+import au.notzed.zcl.CLDevice;
+import au.notzed.zcl.CLException;
+import au.notzed.zcl.CLKernel;
+import au.notzed.zcl.CLObject;
+import au.notzed.zcl.CLPlatform;
+import au.notzed.zcl.CLProgram;
+import static java.lang.Math.cos;
+import static java.lang.Math.sin;
+import java.nio.ByteBuffer;
+
+public class Calculate {
+
+       CLDevice[] devs;
+       CLContext cl;
+       CLCommandQueue q;
+       CLBuffer image;
+       CLProgram prog;
+       int width, height;
+       CLKernel mandelbrot;
+       CLKernel dmandelbrot;
+
+       Calculate(int width, int height) throws CLException {
+               devs = new CLDevice[]{CLPlatform.getBestDevice(CL_DEVICE_TYPE_ALL)};
+               cl = CLContext.createContext(null, devs);
+               q = cl.createCommandQueue(devs[0], 0);
+
+               this.width = width;
+               this.height = height;
+
+               image = cl.createBuffer(0, width * height * 4);
+
+               prog = cl.createProgramWithSource(getClass().getResourceAsStream("mandelbrot.cl"));
+               prog.buildProgram(devs, null);
+
+               mandelbrot = prog.createKernel("mandelbrot");
+               mandelbrot.setArg(0, image);
+               mandelbrot.setArg(1, width, height);
+
+               dmandelbrot = prog.createKernel("dmandelbrot");
+               dmandelbrot.setArg(0, image);
+               dmandelbrot.setArg(1, width, height);
+       }
+
+       void release() {
+               CLObject.release(dmandelbrot, mandelbrot, prog, image, q, cl);
+               CLObject.release(devs);
+       }
+
+       public void run(ByteBuffer buffer, double ox, double oy, double sx, double sy, double arg, int m) {
+               try {
+                       mandelbrot.setArg(2, ox, oy);
+                       mandelbrot.setArg(3, sx, sy);
+                       mandelbrot.setArg(4, sin(arg), cos(arg));
+                       mandelbrot.setArg(5, m);
+
+                       q.enqueue2DKernel(mandelbrot, 0, 0, width, height, 8, 8, null, null);
+                       q.enqueueReadBuffer(image, true, 0, buffer.limit(), buffer, null, null);
+                       q.finish();
+               } catch (CLException ex) {
+                       ex.printStackTrace();
+               }
+       }
+
+       public void rund(ByteBuffer buffer, double ox, double oy, double sx, double sy, double arg, int m) {
+               try {
+                       dmandelbrot.setArg(2, ox, oy);
+                       dmandelbrot.setArg(3, sx, sy);
+                       dmandelbrot.setArg(4, sin(arg), cos(arg));
+                       dmandelbrot.setArg(5, m);
+
+                       q.enqueue2DKernel(dmandelbrot, 0, 0, width, height, 8, 8, null, null);
+                       q.enqueueReadBuffer(image, true, 0, buffer.limit(), buffer, null, null);
+                       q.finish();
+               } catch (CLException ex) {
+                       ex.printStackTrace();
+               }
+
+       }
+
+}
diff --git a/src/notzed.zcl.fxdemo/classes/fxdemo/fract/Mandelbrot.java b/src/notzed.zcl.fxdemo/classes/fxdemo/fract/Mandelbrot.java
new file mode 100644 (file)
index 0000000..4a2cb1c
--- /dev/null
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2020 Michael Zucchi
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package fxdemo.fract;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import javafx.animation.Interpolator;
+import javafx.animation.KeyFrame;
+import javafx.animation.Timeline;
+import javafx.animation.Transition;
+import javafx.application.Application;
+import javafx.application.Platform;
+import javafx.event.ActionEvent;
+import javafx.scene.Scene;
+import javafx.scene.image.ImageView;
+import javafx.scene.image.PixelFormat;
+import javafx.scene.image.WritableImage;
+import javafx.scene.input.KeyCombination;
+import javafx.scene.layout.AnchorPane;
+import javafx.scene.text.Text;
+import javafx.stage.Stage;
+import javafx.util.Duration;
+import java.nio.ByteBuffer;
+import au.notzed.zcl.CLMemory;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.stream.Stream;
+import javafx.geometry.Rectangle2D;
+import javafx.stage.Screen;
+
+/**
+ * Mandelbrot rot-zoomer.
+ * <p>
+ */
+public class Mandelbrot extends Application {
+
+       int width = 1920 / 2;
+       int height = 1200 / 2;
+       Calculate calc;
+       WritableImage image;
+       ByteBuffer buffer;
+       ExecutorService queue = Executors.newSingleThreadExecutor((r) -> {
+               Thread t = new Thread(r);
+               t.setDaemon(true);
+               return t;
+       });
+
+       long nframes;
+       long lframes;
+
+       static class Target {
+
+               String name;
+               double cx, cy;
+               double beta;
+
+               public Target(String name, double cx, double cy, double beta) {
+                       this.name = name;
+                       this.cx = cx;
+                       this.cy = cy;
+                       this.beta = beta;
+               }
+
+       }
+
+       // some from here, i made up the names http://paulbourke.net/fractals/mandelbrot/
+       static Target[] targets = {
+               new Target("jewel", -0.7746806106269039, -0.1374168856037867, 30),
+               new Target("starfish", -0.812223315621338, -0.185453926110785, 10),
+               new Target("spirals", -0.761574, -0.0847596, 12),
+               new Target("spikes", -1.62917, -0.0203968, 16),};
+
+       Target target = targets[0];
+       boolean isdouble = true;
+       double alpha = 0.005;
+       double beta = 0;
+       double gamma = 80;
+       double delta = -360;
+       boolean synchronous = false;
+       double time = 15;
+       double rate = 60;
+       boolean fullscreen;
+
+       static void usage() {
+               try (InputStream is = Mandelbrot.class.getResourceAsStream("usage.txt")) {
+                       is.transferTo(System.out);
+                       System.exit(0);
+               } catch (IOException ex) {
+                       ex.printStackTrace();
+               }
+       }
+
+       @Override
+       public void start(Stage stage) throws Exception {
+               // parse arguments
+               getParameters().getNamed().forEach((c, v) -> {
+                       switch (c) {
+                       case "target":
+                               if (v.equals("list")) {
+                                       System.out.println("Available targets:");
+                                       for (Target t : targets) {
+                                               System.out.print("\t");
+                                               System.out.println(t.name);
+                                       }
+                                       System.exit(0);
+                               }
+                               target = Stream.of(targets).filter((t) -> t.name.equals(v)).findFirst().orElseGet(() -> {
+                                       String coords[] = v.split(",");
+                                       return new Target("user", Double.valueOf(coords[0].trim()), Double.valueOf(coords[1].trim()), 15);
+                               });
+                               break;
+                       case "a":
+                       case "alpha":
+                               alpha = Double.valueOf(v);
+                               break;
+                       case "b":
+                       case "beta":
+                               beta = Double.valueOf(v);
+                               break;
+                       case "c":
+                       case "gamma":
+                               gamma = Double.valueOf(v);
+                               break;
+                       case "d":
+                       case "rotate":
+                               delta = Double.valueOf(v);
+                               break;
+                       case "time":
+                               time = Double.valueOf(v);
+                               break;
+                       case "rate":
+                               rate = Double.valueOf(v);
+                               break;
+                       case "size":
+                               if (v.equals("full")) {
+                                       Rectangle2D size = Screen.getPrimary().getBounds();
+                                       width = (int) size.getWidth();
+                                       height = (int) size.getHeight();
+                                       fullscreen = true;
+                               } else {
+                                       String coords[] = v.split(",");
+                                       width = Integer.valueOf(coords[0]);
+                                       height = Integer.valueOf(coords[1]);
+                               }
+                               break;
+                       default:
+                               System.out.println("Unknown option: '" + c + "'");
+                               usage();
+                               break;
+                       }
+               });
+               getParameters().getUnnamed().forEach((c) -> {
+                       switch (c) {
+                       case "-f":
+                               isdouble = false;
+                               break;
+                       case "-d":
+                               isdouble = true;
+                               break;
+                       case "--sync":
+                       case "-s":
+                               synchronous = true;
+                               break;
+                       default:
+                               System.out.println("Unknown option: " + c);
+                       case "-h":
+                       case "--help":
+                               usage();
+                       }
+               });
+
+               calc = new Calculate(width, height);
+               image = new WritableImage(width, height);
+
+               if (beta == 0.0)
+                       beta = isdouble ? target.beta : Math.min(12, target.beta);
+
+               ImageView iv = new ImageView(image);
+               Text fps = new Text();
+
+               // sort of pixietube
+               fps.setStyle("-fx-font: 24 monospace; -fx-fill: orange; -fx-effect: dropshadow(two-pass-box, orangered, 6, 0.5, 0, 0);");
+
+               Scene scene = new Scene(new AnchorPane(iv, fps));
+
+               AnchorPane.setLeftAnchor(fps, 24.0);
+               AnchorPane.setTopAnchor(fps, 24.0);
+
+               scene.getAccelerators().put(KeyCombination.valueOf("ESC"), stage::close);
+               stage.setFullScreen(fullscreen);
+
+               stage.setScene(scene);
+               stage.show();
+
+               buffer = CLMemory.alloc(width * height * 4);
+
+               Timeline tofps = new Timeline(new KeyFrame(Duration.seconds(0.1), (ActionEvent event) -> {
+                       fps.setText(String.format("fps %4d maxiter %d", (nframes - lframes) * 10, lastiter));
+                       lframes = nframes;
+               }));
+               tofps.setCycleCount(Timeline.INDEFINITE);
+               tofps.play();
+
+               if (delta != 0.0) {
+                       Transition spin = new Transition(rate) {
+                               {
+                                       setCycleDuration(Duration.seconds(360 / (Math.abs(delta))));
+                               }
+
+                               @Override
+                               protected void interpolate(double frac) {
+                                       arg = Math.toRadians(frac * delta);
+                               }
+                       };
+                       spin.setCycleCount(Timeline.INDEFINITE);
+                       spin.setInterpolator(Interpolator.LINEAR);
+                       spin.play();
+               } else {
+                       arg = Math.toRadians(90);
+               }
+
+               Zoom zoom = synchronous ? new Zoom() : new Zoom(rate);
+               zoom.setInterpolator(Interpolator.EASE_BOTH);
+               zoom.setAutoReverse(true);
+               zoom.setCycleCount(Timeline.INDEFINITE);
+               zoom.play();
+       }
+
+       @Override
+       public void stop() throws Exception {
+               calc.release();
+       }
+
+       class Zoom extends Transition {
+
+               Zoom() {
+                       setCycleDuration(Duration.seconds(time));
+               }
+
+               Zoom(double rate) {
+                       super(rate);
+                       setCycleDuration(Duration.seconds(time));
+               }
+
+               @Override
+               protected void interpolate(double frac) {
+                       double scale = alpha / Math.exp(frac * beta);
+                       int depth = (int) (-Math.log(scale) * gamma);
+
+                       recalc(target, isdouble, scale, arg, depth);
+               }
+       }
+
+       boolean busy;
+       double arg = 0;
+       int lastiter;
+
+       void recalc(Target target, boolean isdouble, double scale, double arg, int m) {
+               double x = target.cx - width * scale * 0.5;
+               double y = target.cy - height * scale * 0.5;
+
+               lastiter = m;
+
+               if (!synchronous) {
+                       if (busy)
+                               return;
+                       busy = true;
+
+                       queue.submit(() -> {
+                               if (isdouble)
+                                       calc.rund(buffer, x, y, scale, scale, arg, m);
+                               else
+                                       calc.run(buffer, x, y, scale, scale, arg, m);
+
+                               Platform.runLater(() -> {
+                                       nframes++;
+                                       image.getPixelWriter()
+                                               .setPixels(0, 0, width, height, PixelFormat.getByteBgraInstance(), buffer, width * 4);
+                                       busy = false;
+                               });
+                       });
+               } else {
+
+                       if (isdouble)
+                               calc.rund(buffer, x, y, scale, scale, arg, m);
+                       else
+                               calc.run(buffer, x, y, scale, scale, arg, m);
+                       nframes++;
+                       image.getPixelWriter()
+                               .setPixels(0, 0, width, height, PixelFormat.getByteBgraInstance(), buffer, width * 4);
+               }
+       }
+}
diff --git a/src/notzed.zcl.fxdemo/classes/fxdemo/fract/mandelbrot.cl b/src/notzed.zcl.fxdemo/classes/fxdemo/fract/mandelbrot.cl
new file mode 100644 (file)
index 0000000..bb7009c
--- /dev/null
@@ -0,0 +1,97 @@
+
+#pragma OPENCL EXTENSION cl_khr_fp64 : enable
+
+#define LWS_X 8
+#define LWS_Y 8
+
+kernel void
+__attribute__((reqd_work_group_size(LWS_X, LWS_Y, 1)))
+mandelbrot(global uchar4 *image, int2 size, double2 origin, double2 scale, double2 sincos, int m) {
+       int dx = get_global_id(0);
+       int dy = get_global_id(1);
+
+       if (dx < size.x && dy < size.y) {
+#if 0
+               float2 c = (float2)(dx, dy) * scale + origin;
+#else
+               double2 s = convert_double2(size);
+               double2 v;
+
+               v = (double2)(dx, dy) - s * 0.5;
+               v = (double2)(v.x * sincos.x + v.y * sincos.y,
+                            v.x * sincos.y - v.y * sincos.x);
+               v += s * 0.5;
+
+               v *= scale;
+               v += origin;
+
+               float2 c = convert_float2(v);
+#endif
+               int n = 0;
+               float2 z = 0;
+
+               do {
+                       z = (float2)(z.x * z.x - z.y * z.y, 2.0f * z.x * z.y) + c;
+                       n++;
+               } while (dot(z, z) < 4 && n < m);
+
+               // we use a simple cosine palette to determine color:
+               // http://iquilezles.org/www/articles/palettes/palettes.htm
+               float t = n * 10.0f / m;
+               float3 d = (float3)(0.5f, 0.5f, 0.5f);
+               float3 e = (float3)(0.5f, 0.5f, 0.5f);
+               float3 f = (float3)(1.0f, 1.0f, 1.0f);
+               float3 g = (float3)(0.00f, 0.33f, 0.67f);
+               float4 colour = (float4)( d + e * cos(6.28318f * (f * t + g)), 1.0f);
+
+               if (convert_int(n) == m)
+                       colour = (float4)(0, 0, 0, 1);
+
+               // store the rendered mandelbrot set into a storage buffer:
+               image[dx + dy * size.x] = convert_uchar4(colour * 255.0f);
+       }
+}
+
+// rot-zoomer
+kernel void
+__attribute__((reqd_work_group_size(LWS_X, LWS_Y, 1)))
+dmandelbrot(global uchar4 *image, int2 size, double2 origin, double2 scale, double2 sincos, int m) {
+       int dx = get_global_id(0);
+       int dy = get_global_id(1);
+
+       if (dx < size.x && dy < size.y) {
+               double2 s = convert_double2(size);
+               double2 c;
+
+               c = (double2)(dx, dy) - s * 0.5;
+               c = (double2)(c.x * sincos.x + c.y * sincos.y,
+                             c.x * sincos.y - c.y * sincos.x);
+               c += s * 0.5;
+
+               c *= scale;
+               c += origin;
+
+               int n = 0;
+               double2 z = 0;
+
+               do {
+                       z = (double2)(z.x * z.x - z.y * z.y, 2.0f * z.x * z.y) + c;
+                       n++;
+               } while (dot(z, z) < 4 && n < m);
+
+               // we use a simple cosine palette to determine color:
+               // http://iquilezles.org/www/articles/palettes/palettes.htm
+               float t = n * 10.0f / m;
+               float3 d = (float3)(0.5f, 0.5f, 0.5f);
+               float3 e = (float3)(0.5f, 0.5f, 0.5f);
+               float3 f = (float3)(1.0f, 1.0f, 1.0f);
+               float3 g = (float3)(0.00f, 0.33f, 0.67f);
+               float4 colour = (float4)( d + e * cos(6.28318f * (f * t + g)), 1.0f);
+
+               if (convert_int(n) == m)
+                       colour = (float4)(0, 0, 0, 1);
+
+               // store the rendered mandelbrot set into a storage buffer:
+               image[dx + dy * size.x] = convert_uchar4(colour * 255.0f);
+       }
+}
diff --git a/src/notzed.zcl.fxdemo/classes/fxdemo/fract/usage.txt b/src/notzed.zcl.fxdemo/classes/fxdemo/fract/usage.txt
new file mode 100644 (file)
index 0000000..0b363af
--- /dev/null
@@ -0,0 +1,50 @@
+Usage: mandelbrot [args]
+
+  -f, -d
+       Set precision, float or double.
+
+  --size=full
+  --size=width,height
+        Set window size.  Full will enable fullscreen mode.
+
+  --target=list
+  --target=named-target
+  --target=centre-x,centre-y
+       Set the centre zoom target point.  list will list some
+       predefined locations from
+       http://paulbourke.net/fractals/mandelbrot/
+
+  --a=alpha [0.005]
+       Set zoom alpha parameter.  Loosly the maximum outer zoom,
+       range is around 0.005.
+
+  --b=beta [target dependent]
+       Set the maximum inner zoom.  This is an inverse exponential
+       and the maximum value depends on the precision.  For float the
+       maximum is about 12, for double it is more than 30.
+
+  --c=gamma [80]
+       Set the iteration scaling rate.  This will the precision,
+       speed of rendering, and some of the colour mapping.  If you
+       have the horsepower 80-100 works well.
+
+  --rotate=degrees-per-second [360]
+       Set rotation rate in degrees per second.  Negative values are
+       allowed.  Use 0 to turn it off.
+
+  --time=seconds
+       Time in seconds for one zoom.
+
+  --rate=framerate
+       Target framereate.
+
+  --sync, -s
+       Set synchronous mode.  In synchronous mode rendering occurs on
+       the javafx thread and there is no rate limiting.
+
+  --help, -h
+       Usage.
+
+  Press Escape to quit.
+
+  Try --gamma=20 or 15 for a trippier colouration.
diff --git a/src/notzed.zcl.fxdemo/classes/module-info.java b/src/notzed.zcl.fxdemo/classes/module-info.java
new file mode 100644 (file)
index 0000000..1774cb3
--- /dev/null
@@ -0,0 +1,15 @@
+
+/**
+ * OpenCL demos that use JavaFX.
+ */
+module notzed.zcl.fxdemo {
+       requires javafx.base;
+       requires javafx.controls;
+       requires javafx.graphics;
+       requires javafx.swing;
+
+       requires notzed.zcl;
+
+       exports fxdemo.fract to javafx.graphics;
+       
+}