├── .gitignore ├── library.properties ├── done.txt ├── changes.md ├── processing4-javafx.iml ├── README.md ├── todo.txt └── src └── processing └── javafx ├── PSurfaceFX.java └── PGraphicsFX2D.java /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *~ 3 | 4 | # files for the build 5 | /bin 6 | 7 | # how to point to processing.core 8 | /local.properties 9 | 10 | # ignore the sdk download files 11 | javafx-*.zip 12 | 13 | # everything is downloaded from online 14 | /library 15 | 16 | # the build files 17 | /dist 18 | -------------------------------------------------------------------------------- /library.properties: -------------------------------------------------------------------------------- 1 | name = JavaFX 2 | authors = The Processing Foundation 3 | url = https://github.com/processing/processing4-javafx 4 | category = Renderer 5 | sentence = The FX2D renderer for Processing 4 6 | paragraph = Since Processing 4.0 beta 4, the FX2D renderer is available as a separate library. 7 | version = 1281 8 | prettyVersion = 4.0 beta ?? 9 | minRevision = 1280 10 | maxRevision = 0 11 | -------------------------------------------------------------------------------- /done.txt: -------------------------------------------------------------------------------- 1 | 1280 (4.0b5) 2 | X change the name of the subfolder from processing4-javafx to javafx 3 | X the former didn't work at all 4 | X had to manually modify the download .zip for the previous release 5 | X add handlers for window move and resize events 6 | X https://github.com/processing/processing4/issues/53 7 | 8 | 9 | 1279 (4.0b4) 10 | o "Tint on FX2D now works properly, and improved JavaFX/Scenebuilder integration" 11 | o https://github.com/processing/processing/pull/6051 12 | X contains too many arbitrary changes (deleted lines, fields made public) 13 | X uses new image cache object, not clear why 14 | X cache mechanism would make memory run out 15 | X add the five (now six) platforms to the javafx build 16 | X need internet connection for the build 17 | X but too complicated to check whether each platform is available 18 | X and not nearly as onerous a requirement now that it's not part of core 19 | 20 | contribs 21 | X make offscreen PGraphicsFX2D work (from @GKFX) 22 | X https://github.com/processing/processing/issues/4638 23 | X https://github.com/processing/processing/pull/4698 24 | -------------------------------------------------------------------------------- /changes.md: -------------------------------------------------------------------------------- 1 | # Revision 1280 (for Processing 4.0 beta 5) 2 | 3 | *3 February 2022* 4 | 5 | * Fixes for the folder layout (that were manually added while the previous release was being posted). 6 | 7 | * Updating handlers and functions for moving and resizing windows. [\#53](https://github.com/processing/processing4/issues/53) 8 | 9 | 10 | # Revision 1279 (for Processing 4.0 beta 4) 11 | 12 | *23 January 2022* 13 | 14 | The JavaFX binaries have grown very large in size so it was necessary to move JavaFX to its own library. This avoids making the Processing download excessively large, given that not everyone uses JavaFX. More discussion/explanation [here](https://github.com/processing/processing4/issues/348). 15 | 16 | 17 | ## Changes 18 | 19 | * For Processing 4.0 beta 4, now a separate library to be installed from the Contributions Manager. 20 | 21 | * Now building for our six base platforms described [here](https://github.com/processing/processing4/wiki/Supported-Platforms). 22 | 23 | * An internet connection is necessary for the build, because it checks the downloads. (Otherwise the build script is more complicated than necessary to see if all six files are available.) 24 | 25 | 26 | ## Fixes 27 | 28 | + Make offscreen `PGraphicsFX2D` work, a patch from @GKFX. [#4638](https://github.com/processing/processing/issues/4638), [#4698](https://github.com/processing/processing/pull/4698) 29 | -------------------------------------------------------------------------------- /processing4-javafx.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `FX2D` 2 | 3 | This repository contains the JavaFX renderer for Processing 4, also known as `FX2D`. 4 | 5 | The JavaFX binaries have grown very large in size so this was a necessary step to avoid making the Processing download excessively large, given that not everyone uses JavaFX. More discussion/explanation [here](https://github.com/processing/processing4/issues/348). 6 | 7 | This version of the JavaFX library will be required for `FX2D` sketches starting with 4.0 beta 4. 8 | 9 | 10 | ## This library is large 11 | 12 | The build script creates a very large download (hundreds of megabytes), because Processing supports several platforms (3 officially, 3 more unofficially) and we want to make it easy to use all of them. This is an acceptable tradeoff for a standalone library like this, but if you want a smaller build, you have options: 13 | 14 | 1. Disable `` stanzas for platforms you don't care about. 15 | 16 | 2. Un-comment the `` line so that the WebKit library is removed. 17 | 18 | These are not done by default because we've seen projects that (1) run on all those platforms, and (2) use the full browser implementation available with the WebKit support. Again, because this is an optional download, this seems the correct tradeoff. 19 | 20 | 21 | ## Pushing a new release 22 | 23 | 1. Roll the version/revision numbers in `mode.properties` 24 | 25 | 2. Tag the latest and push 26 | 27 | git tag -a rev1280 -m 'Revision 1280 (Processing 4.0b5)' 28 | git push origin --tags 29 | 30 | 3. Delete the previous `latest` tag 31 | 32 | git tag -d latest 33 | git push origin :refs/tags/latest 34 | 35 | 4. Create new `latest` tag with the current state of the repo 36 | 37 | git tag -f -a latest -m 'Revision 1280 (Processing 4.0b5)' 38 | git push -f --tags 39 | 40 | 5. Create the distribution 41 | 42 | ant dist 43 | 44 | Then upload dist/processing4-javafx.zip and dist/processing4-javafx.txt to the `latest` tag on Github. Can also upload them to the tag for the current version, for anyone installing manually. 45 | 46 | 6. Add changes in MarkDown format to the release: 47 | -------------------------------------------------------------------------------- /todo.txt: -------------------------------------------------------------------------------- 1 | 1281 2 | X fix the names of the dist files 3 | X change type to "Renderer" (new category for 4.0 beta 8) 4 | 5 | 6 | getting away from reliance on AWT 7 | _ better JavaFX-specific ways to handle loadImage()? 8 | _ https://github.com/processing/processing4/issues/56 9 | _ implement selectInput/Output/Folder methods in PSurfaceFX 10 | _ implement openLink() in PSurfaceFX 11 | 12 | 13 | issues 14 | _ Remove usage of com.sun.* in JavaFX library 15 | _ https://github.com/processing/processing4/issues/208 16 | _ many shift- keys not working properly in FX2D (added a test sketch) 17 | _ https://github.com/processing/processing/issues/5317 18 | _ Hitting ESC in FX2D app on macOS throws IllegalStateException 19 | _ https://github.com/processing/processing/issues/5249 20 | _ wrong window size with fullScreen() 21 | _ https://github.com/processing/processing/issues/4737 22 | _ menu bar not hiding properly in exported applications with FX2D 23 | _ https://github.com/processing/processing/issues/4527 24 | _ hideMenuBar() called from setup() works fine 25 | _ just call it around setup time? 26 | _ the --hide-stop option not working (FX only? traces?) 27 | _ make wiki about quirks 28 | _ starving the thread makes things really slow down 29 | _ keyPressed() is always uppercase, keyTyped() will be correct 30 | _ do we really need setTextFont/Size when we already have Impl? 31 | _ need keyPressed() to do lower and upper case 32 | _ static mode sketches (draw once and halt w/o closing window) 33 | _ fix display handling, line up the device order with AWT 34 | _ https://docs.oracle.com/javafx/2/api/javafx/stage/Screen.html 35 | _ noLoop() 36 | _ present mode not working at all 37 | _ stage in the center, clear the rest of the screen 38 | _ createGraphics() should probably create PGraphicsJava2D 39 | _ or is Canvas specific to the PGraphics, and we get another Context2D? 40 | _ http://docs.oracle.com/javafx/2/api/javafx/scene/canvas/Canvas.html 41 | _ loadPixels() (also 2x) 42 | _ text and fonts? 43 | _ maybe helpful: https://wiki.openjdk.java.net/display/OpenJFX/Font+Setup 44 | _ updatePixels() 45 | _ save() and saveFrame() 46 | _ get() and set() 47 | _ clip/noClip 48 | _ https://github.com/processing/processing/issues/3274 49 | _ getNative() in PImage problematic because it gives back a BufferedImage 50 | _ move loadImage() into PGraphics, with AWT version the default 51 | _ or pass createImage() through to renderer? 52 | _ implement external messages (moving the window) 53 | _ implement PSurfaceFX.setIcon() 54 | _ javafx not supported with ARM (so we're screwed on raspberry pi) 55 | _ https://www.linkedin.com/pulse/oracle-just-removed-javafx-support-arm-jan-snelders 56 | -------------------------------------------------------------------------------- /src/processing/javafx/PSurfaceFX.java: -------------------------------------------------------------------------------- 1 | /* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ 2 | 3 | /* 4 | Part of the Processing project - http://processing.org 5 | 6 | Copyright (c) 2015 The Processing Foundation 7 | 8 | This library is free software; you can redistribute it and/or 9 | modify it under the terms of the GNU Lesser General Public 10 | License as published by the Free Software Foundation, version 2.1. 11 | 12 | This library is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | Lesser General Public License for more details. 16 | 17 | You should have received a copy of the GNU Lesser General 18 | Public License along with this library; if not, write to the 19 | Free Software Foundation, Inc., 59 Temple Place, Suite 330, 20 | Boston, MA 02111-1307 USA 21 | */ 22 | 23 | package processing.javafx; 24 | 25 | import com.sun.glass.ui.Screen; 26 | 27 | import java.awt.GraphicsDevice; 28 | import java.awt.GraphicsEnvironment; 29 | import java.awt.Rectangle; 30 | import java.io.File; 31 | import java.net.URL; 32 | import java.util.ArrayList; 33 | import java.util.HashMap; 34 | import java.util.List; 35 | import java.util.Map; 36 | import java.util.concurrent.SynchronousQueue; 37 | 38 | import javafx.animation.Animation; 39 | import javafx.animation.KeyFrame; 40 | import javafx.animation.Timeline; 41 | import javafx.application.Application; 42 | import javafx.application.Platform; 43 | import javafx.beans.value.ChangeListener; 44 | import javafx.beans.value.ObservableValue; 45 | import javafx.event.ActionEvent; 46 | import javafx.event.EventHandler; 47 | import javafx.event.EventType; 48 | import javafx.scene.Cursor; 49 | import javafx.scene.ImageCursor; 50 | import javafx.scene.Scene; 51 | import javafx.scene.SceneAntialiasing; 52 | import javafx.scene.canvas.Canvas; 53 | import javafx.scene.image.Image; 54 | import javafx.scene.image.PixelFormat; 55 | import javafx.scene.image.WritableImage; 56 | import javafx.scene.input.KeyCode; 57 | import javafx.scene.input.KeyEvent; 58 | import javafx.scene.input.MouseEvent; 59 | import javafx.scene.input.ScrollEvent; 60 | import javafx.scene.layout.StackPane; 61 | import javafx.stage.Stage; 62 | import javafx.stage.StageStyle; 63 | import javafx.stage.WindowEvent; 64 | import javafx.util.Duration; 65 | import processing.awt.ShimAWT; 66 | import processing.core.*; 67 | 68 | 69 | public class PSurfaceFX implements PSurface { 70 | PApplet sketch; 71 | 72 | PGraphicsFX2D fx; 73 | Stage stage; 74 | Canvas canvas; 75 | 76 | final Animation animation; 77 | float frameRate = 60; 78 | 79 | private SynchronousQueue drawExceptionQueue = new SynchronousQueue<>(); 80 | 81 | public PSurfaceFX(PGraphicsFX2D graphics) { 82 | fx = graphics; 83 | canvas = new ResizableCanvas(); 84 | 85 | // set up main drawing loop 86 | KeyFrame keyFrame = new KeyFrame(Duration.millis(1000), 87 | new EventHandler() { 88 | public void handle(ActionEvent event) { 89 | long startNanoTime = System.nanoTime(); 90 | try { 91 | sketch.handleDraw(); 92 | } catch (Throwable e) { 93 | // Let exception handler thread crash with our exception 94 | drawExceptionQueue.offer(e); 95 | // Stop animating right now so nothing runs afterwards 96 | // and crash frame can be for example traced by println() 97 | animation.stop(); 98 | return; 99 | } 100 | long drawNanos = System.nanoTime() - startNanoTime; 101 | 102 | if (sketch.exitCalled()) { 103 | // using Platform.runLater() didn't work 104 | // Platform.runLater(new Runnable() { 105 | // public void run() { 106 | // instead of System.exit(), safely shut down JavaFX this way 107 | Platform.exit(); 108 | // } 109 | // }); 110 | } 111 | if (sketch.frameCount > 5) { 112 | animation.setRate(-PApplet.min(1e9f / drawNanos, frameRate)); 113 | } 114 | } 115 | }); 116 | animation = new Timeline(keyFrame); 117 | animation.setCycleCount(Animation.INDEFINITE); 118 | 119 | // key frame has duration of 1 second, so the rate of the animation 120 | // should be set to frames per second 121 | 122 | // setting rate to negative so that event fires at the start of 123 | // the key frame and first frame is drawn immediately 124 | animation.setRate(-frameRate); 125 | } 126 | 127 | 128 | public Object getNative() { 129 | return canvas; 130 | } 131 | 132 | 133 | class ResizableCanvas extends Canvas { 134 | 135 | public ResizableCanvas() { 136 | widthProperty().addListener(new ChangeListener() { 137 | @Override 138 | public void changed(ObservableValue value, 139 | Number oldWidth, Number newWidth) { 140 | // sketch.width = newWidth.intValue(); 141 | sketch.setSize(newWidth.intValue(), sketch.height); 142 | // draw(); 143 | fx.setSize(sketch.width, sketch.height); 144 | } 145 | }); 146 | heightProperty().addListener(new ChangeListener() { 147 | @Override 148 | public void changed(ObservableValue value, 149 | Number oldHeight, Number newHeight) { 150 | // sketch.height = newHeight.intValue(); 151 | sketch.setSize(sketch.width, newHeight.intValue()); 152 | // draw(); 153 | fx.setSize(sketch.width, sketch.height); 154 | } 155 | }); 156 | 157 | //addEventHandler(eventType, eventHandler); 158 | 159 | EventHandler mouseHandler = new EventHandler<>() { 160 | public void handle(MouseEvent e) { 161 | fxMouseEvent(e); 162 | } 163 | }; 164 | 165 | setOnMousePressed(mouseHandler); 166 | setOnMouseReleased(mouseHandler); 167 | setOnMouseClicked(mouseHandler); 168 | setOnMouseEntered(mouseHandler); 169 | setOnMouseExited(mouseHandler); 170 | 171 | setOnMouseDragged(mouseHandler); 172 | setOnMouseMoved(mouseHandler); 173 | 174 | setOnScroll(new EventHandler() { 175 | public void handle(ScrollEvent e) { 176 | fxScrollEvent(e); 177 | } 178 | }); 179 | 180 | EventHandler keyHandler = new EventHandler<>() { 181 | public void handle(KeyEvent e) { 182 | fxKeyEvent(e); 183 | } 184 | }; 185 | 186 | setOnKeyPressed(keyHandler); 187 | setOnKeyReleased(keyHandler); 188 | setOnKeyTyped(keyHandler); 189 | 190 | setFocusTraversable(false); // prevent tab from de-focusing 191 | 192 | focusedProperty().addListener(new ChangeListener() { 193 | public void changed(ObservableValue value, 194 | Boolean oldValue, Boolean newValue) { 195 | if (newValue.booleanValue()) { 196 | sketch.focused = true; 197 | sketch.focusGained(); 198 | } else { 199 | sketch.focused = false; 200 | sketch.focusLost(); 201 | } 202 | } 203 | }); 204 | } 205 | 206 | public Stage getStage() { 207 | return stage; 208 | } 209 | 210 | @Override 211 | public boolean isResizable() { 212 | return true; 213 | } 214 | 215 | @Override 216 | public double prefWidth(double height) { 217 | return getWidth(); 218 | } 219 | 220 | @Override 221 | public double prefHeight(double width) { 222 | return getHeight(); 223 | } 224 | } 225 | 226 | 227 | // TODO rewrite before 4.0 release 228 | public PImage loadImage(String path, Object... args) { 229 | return ShimAWT.loadImage(sketch, path, args); 230 | } 231 | 232 | 233 | @Override 234 | public void selectInput(String prompt, String callbackMethod, 235 | File file, Object callbackObject) { 236 | ShimAWT.selectInput(prompt, callbackMethod, file, callbackObject); 237 | } 238 | 239 | 240 | @Override 241 | public void selectOutput(String prompt, String callbackMethod, 242 | File file, Object callbackObject) { 243 | ShimAWT.selectOutput(prompt, callbackMethod, file, callbackObject); 244 | } 245 | 246 | 247 | @Override 248 | public void selectFolder(String prompt, String callbackMethod, 249 | File file, Object callbackObject) { 250 | ShimAWT.selectFolder(prompt, callbackMethod, file, callbackObject); 251 | } 252 | 253 | 254 | public void initOffscreen(PApplet sketch) { 255 | } 256 | 257 | 258 | static public class PApplicationFX extends Application { 259 | static public PSurfaceFX surface; 260 | // static String title; // title set at launch 261 | // static boolean resizable; // set at launch 262 | 263 | public PApplicationFX() { } 264 | 265 | @Override 266 | public void start(final Stage stage) { 267 | // if (title != null) { 268 | // stage.setTitle(title); 269 | // } 270 | 271 | PApplet sketch = surface.sketch; 272 | 273 | // See JEP 263 274 | float renderScale = Screen.getMainScreen().getRecommendedOutputScaleX(); 275 | if (PApplet.platform == PConstants.MACOS) { 276 | for (Screen s : Screen.getScreens()) { 277 | renderScale = Math.max(renderScale, s.getRecommendedOutputScaleX()); 278 | } 279 | } 280 | if (sketch.pixelDensity == 2 && renderScale < 2) { 281 | sketch.pixelDensity = 1; 282 | sketch.g.pixelDensity = 1; 283 | System.err.println("pixelDensity(2) is not available for this display"); 284 | } 285 | 286 | // Use AWT display code, because FX orders screens in different way 287 | GraphicsDevice displayDevice = null; 288 | 289 | GraphicsEnvironment environment = 290 | GraphicsEnvironment.getLocalGraphicsEnvironment(); 291 | 292 | int displayNum = sketch.sketchDisplay(); 293 | if (displayNum > 0) { // if -1, use the default device 294 | GraphicsDevice[] devices = environment.getScreenDevices(); 295 | if (displayNum <= devices.length) { 296 | displayDevice = devices[displayNum - 1]; 297 | } else { 298 | System.err.format("Display %d does not exist, " + 299 | "using the default display instead.%n", displayNum); 300 | for (int i = 0; i < devices.length; i++) { 301 | System.err.format("Display %d is %s%n", (i+1), devices[i]); 302 | } 303 | } 304 | } 305 | if (displayDevice == null) { 306 | displayDevice = environment.getDefaultScreenDevice(); 307 | } 308 | 309 | boolean fullScreen = sketch.sketchFullScreen(); 310 | boolean spanDisplays = sketch.sketchDisplay() == PConstants.SPAN; 311 | 312 | Rectangle primaryScreenRect = displayDevice.getDefaultConfiguration().getBounds(); 313 | Rectangle screenRect = primaryScreenRect; 314 | if (fullScreen || spanDisplays) { 315 | double minX = screenRect.getMinX(); 316 | double maxX = screenRect.getMaxX(); 317 | double minY = screenRect.getMinY(); 318 | double maxY = screenRect.getMaxY(); 319 | if (spanDisplays) { 320 | for (GraphicsDevice s : environment.getScreenDevices()) { 321 | Rectangle bounds = s.getDefaultConfiguration().getBounds(); 322 | minX = Math.min(minX, bounds.getMinX()); 323 | maxX = Math.max(maxX, bounds.getMaxX()); 324 | minY = Math.min(minY, bounds.getMinY()); 325 | maxY = Math.max(maxY, bounds.getMaxY()); 326 | } 327 | } 328 | screenRect = new Rectangle((int) minX, (int) minY, 329 | (int) (maxX - minX), (int) (maxY - minY)); 330 | } 331 | 332 | // Set the displayWidth/Height variables inside PApplet, so that they're 333 | // usable and can even be returned by the sketchWidth()/Height() methods. 334 | sketch.displayWidth = (int) screenRect.getWidth(); 335 | sketch.displayHeight = (int) screenRect.getHeight(); 336 | 337 | int sketchWidth = sketch.sketchWidth(); 338 | int sketchHeight = sketch.sketchHeight(); 339 | 340 | if (fullScreen || spanDisplays) { 341 | sketchWidth = (int) screenRect.getWidth(); 342 | sketchHeight = (int) screenRect.getHeight(); 343 | 344 | stage.initStyle(StageStyle.UNDECORATED); 345 | stage.setX(screenRect.getMinX()); 346 | stage.setY(screenRect.getMinY()); 347 | stage.setWidth(screenRect.getWidth()); 348 | stage.setHeight(screenRect.getHeight()); 349 | } 350 | 351 | Canvas canvas = surface.canvas; 352 | surface.fx.context = canvas.getGraphicsContext2D(); 353 | StackPane stackPane = new StackPane(); 354 | stackPane.getChildren().add(canvas); 355 | canvas.widthProperty().bind(stackPane.widthProperty()); 356 | canvas.heightProperty().bind(stackPane.heightProperty()); 357 | 358 | int width = sketchWidth; 359 | int height = sketchHeight; 360 | int smooth = sketch.sketchSmooth(); 361 | 362 | // Workaround for https://bugs.openjdk.java.net/browse/JDK-8136495 363 | // https://github.com/processing/processing/issues/3823 364 | if ((PApplet.platform == PConstants.MACOS || 365 | PApplet.platform == PConstants.LINUX) && 366 | PApplet.javaVersionName.compareTo("1.8.0_60") >= 0 && 367 | PApplet.javaVersionName.compareTo("1.8.0_72") < 0) { 368 | System.err.println("smooth() disabled for JavaFX with this Java version due to Oracle bug"); 369 | System.err.println("https://github.com/processing/processing/issues/3795"); 370 | smooth = 0; 371 | } 372 | 373 | SceneAntialiasing sceneAntialiasing = (smooth == 0) ? 374 | SceneAntialiasing.DISABLED : SceneAntialiasing.BALANCED; 375 | 376 | stage.setScene(new Scene(stackPane, width, height, false, sceneAntialiasing)); 377 | 378 | // initFrame in different thread is waiting for 379 | // the stage, assign it only when it is all set up 380 | surface.stage = stage; 381 | } 382 | 383 | @Override 384 | public void stop() throws Exception { 385 | surface.sketch.dispose(); 386 | } 387 | } 388 | 389 | 390 | //public Frame initFrame(PApplet sketch, java.awt.Color backgroundColor, 391 | public void initFrame(PApplet sketch) {/*, int backgroundColor, 392 | int deviceIndex, boolean fullScreen, 393 | boolean spanDisplays) {*/ 394 | this.sketch = sketch; 395 | PApplicationFX.surface = this; 396 | //Frame frame = new DummyFrame(); 397 | new Thread(new Runnable() { 398 | public void run() { 399 | Application.launch(PApplicationFX.class); 400 | } 401 | }).start(); 402 | 403 | // wait for stage to be initialized on its own thread before continuing 404 | while (stage == null) { 405 | try { 406 | //System.out.println("waiting for launch"); 407 | Thread.sleep(5); 408 | } catch (InterruptedException e) { } 409 | } 410 | 411 | startExceptionHandlerThread(); 412 | setProcessingIcon(stage); 413 | addWindowListeners(); 414 | } 415 | 416 | 417 | private void startExceptionHandlerThread() { 418 | Thread exceptionHandlerThread = new Thread(() -> { 419 | Throwable drawException; 420 | try { 421 | drawException = drawExceptionQueue.take(); 422 | } catch (InterruptedException e) { 423 | return; 424 | } 425 | // Adapted from PSurfaceJOGL 426 | if (drawException != null) { 427 | if (drawException instanceof ThreadDeath) { 428 | // System.out.println("caught ThreadDeath"); 429 | // throw (ThreadDeath)cause; 430 | } else if (drawException instanceof RuntimeException) { 431 | throw (RuntimeException) drawException; 432 | } else if (drawException instanceof UnsatisfiedLinkError) { 433 | throw new UnsatisfiedLinkError(drawException.getMessage()); 434 | } else { 435 | throw new RuntimeException(drawException); 436 | } 437 | } 438 | }); 439 | exceptionHandlerThread.setDaemon(true); 440 | exceptionHandlerThread.setName("Processing-FX-ExceptionHandler"); 441 | exceptionHandlerThread.start(); 442 | } 443 | 444 | 445 | /** Set the window (and dock, or whatever necessary) title. */ 446 | public void setTitle(String title) { 447 | // PApplicationFX.title = title; // store this in case the stage still null 448 | // if (stage != null) { 449 | stage.setTitle(title); 450 | // } 451 | } 452 | 453 | 454 | ChangeListener stageMovedListener = (observable, oldValue, newValue) -> 455 | sketch.postWindowMoved((int) stage.getX(), (int) stage.getY()); 456 | 457 | ChangeListener stageResizedListener = (observable, oldValue, newValue) -> 458 | sketch.postWindowResized((int) stage.getWidth(), (int) stage.getHeight()); 459 | 460 | 461 | protected void addWindowListeners() { 462 | /* 463 | stage.xProperty().addListener(new ChangeListener() { 464 | @Override 465 | public void changed(ObservableValue value, 466 | Number oldX, Number newX) { 467 | sketch.postWindowPosition(newX.intValue(), stage.yProperty().intValue()); 468 | } 469 | }); 470 | 471 | stage.yProperty().addListener(new ChangeListener() { 472 | @Override 473 | public void changed(ObservableValue value, 474 | Number oldY, Number newY) { 475 | sketch.postWindowPosition(stage.xProperty().intValue(), newY.intValue()); 476 | } 477 | }); 478 | */ 479 | 480 | stage.xProperty().addListener(stageMovedListener); 481 | stage.yProperty().addListener(stageMovedListener); 482 | 483 | stage.widthProperty().addListener(stageResizedListener); 484 | stage.heightProperty().addListener(stageResizedListener); 485 | 486 | stage.setOnCloseRequest(new EventHandler() { 487 | public void handle(WindowEvent we) { 488 | sketch.exit(); 489 | } 490 | }); 491 | } 492 | 493 | 494 | /** Show or hide the window. */ 495 | @Override 496 | public void setVisible(final boolean visible) { 497 | Platform.runLater(new Runnable() { 498 | public void run() { 499 | if (visible) { 500 | stage.show(); 501 | canvas.requestFocus(); 502 | } else { 503 | stage.hide(); 504 | } 505 | } 506 | }); 507 | } 508 | 509 | 510 | /** Set true if we want to resize things (default is not resizable) */ 511 | public void setResizable(boolean resizable) { 512 | // PApplicationFX.resizable = resizable; 513 | // if (stage != null) { 514 | stage.setResizable(resizable); 515 | // } 516 | } 517 | 518 | 519 | public void setIcon(PImage icon) { 520 | int w = icon.pixelWidth; 521 | int h = icon.pixelHeight; 522 | WritableImage im = new WritableImage(w, h); 523 | im.getPixelWriter().setPixels(0, 0, w, h, 524 | PixelFormat.getIntArgbInstance(), 525 | icon.pixels, 526 | 0, w); 527 | 528 | Stage stage = (Stage) canvas.getScene().getWindow(); 529 | stage.getIcons().clear(); 530 | stage.getIcons().add(im); 531 | } 532 | 533 | 534 | List iconImages; 535 | 536 | protected void setProcessingIcon(Stage stage) { 537 | // Adapted from PSurfaceAWT 538 | // Note: FX chooses wrong icon size, should be fixed in Java 9, see: 539 | // https://bugs.openjdk.java.net/browse/JDK-8091186 540 | // Removing smaller sizes helps a bit, but big ones are downsized 541 | try { 542 | if (iconImages == null) { 543 | iconImages = new ArrayList<>(); 544 | final int[] sizes = { 48, 64, 128, 256, 512 }; 545 | 546 | for (int sz : sizes) { 547 | URL url = PApplet.class.getResource("/icon/icon-" + sz + ".png"); 548 | Image image = new Image(url.toString()); 549 | iconImages.add(image); 550 | } 551 | } 552 | List icons = stage.getIcons(); 553 | icons.clear(); 554 | icons.addAll(iconImages); 555 | } catch (Exception e) { } // harmless; keep this to ourselves 556 | } 557 | 558 | 559 | @Override 560 | public void setAlwaysOnTop(boolean always) { 561 | stage.setAlwaysOnTop(always); 562 | } 563 | 564 | 565 | /* 566 | @Override 567 | public void placeWindow(int[] location) { 568 | //setFrameSize(); 569 | 570 | if (location != null) { 571 | // a specific location was received from the Runner 572 | // (applet has been run more than once, user placed window) 573 | stage.setX(location[0]); 574 | stage.setY(location[1]); 575 | 576 | } else { // just center on screen 577 | // Can't use frame.setLocationRelativeTo(null) because it sends the 578 | // frame to the main display, which undermines the --display setting. 579 | // frame.setLocation(screenRect.x + (screenRect.width - sketchWidth) / 2, 580 | // screenRect.y + (screenRect.height - sketchHeight) / 2); 581 | } 582 | if (stage.getY() < 0) { 583 | // Windows actually allows you to place frames where they can't be 584 | // closed. Awesome. http://dev.processing.org/bugs/show_bug.cgi?id=1508 585 | //frame.setLocation(frameLoc.x, 30); 586 | stage.setY(30); 587 | } 588 | 589 | //setCanvasSize(); 590 | 591 | // TODO add window closing behavior 592 | // frame.addWindowListener(new WindowAdapter() { 593 | // @Override 594 | // public void windowClosing(WindowEvent e) { 595 | // System.exit(0); 596 | // } 597 | // }); 598 | 599 | // TODO handle frame resizing events 600 | // setupFrameResizeListener(); 601 | 602 | if (sketch.getGraphics().displayable()) { 603 | setVisible(true); 604 | } 605 | } 606 | */ 607 | 608 | 609 | @Override 610 | public void placeWindow(int[] location, int[] editorLocation) { 611 | if (sketch.sketchFullScreen()) { 612 | PApplet.hideMenuBar(); 613 | return; 614 | } 615 | 616 | int wide = sketch.width; // stage.getWidth() is NaN here 617 | //int high = sketch.height; // stage.getHeight() 618 | 619 | if (location != null) { 620 | // a specific location was received from the Runner 621 | // (applet has been run more than once, user placed window) 622 | stage.setX(location[0]); 623 | stage.setY(location[1]); 624 | 625 | } else if (editorLocation != null) { 626 | int locationX = editorLocation[0] - 20; 627 | int locationY = editorLocation[1]; 628 | 629 | if (locationX - wide > 10) { 630 | // if it fits to the left of the window 631 | stage.setX(locationX - wide); 632 | stage.setY(locationY); 633 | 634 | } else { // doesn't fit 635 | stage.centerOnScreen(); 636 | } 637 | } else { // just center on screen 638 | stage.centerOnScreen(); 639 | } 640 | sketch.postWindowMoved((int) stage.getX(), (int) stage.getY()); 641 | } 642 | 643 | 644 | // http://download.java.net/jdk8/jfxdocs/javafx/stage/Stage.html#setFullScreenExitHint-java.lang.String- 645 | // http://download.java.net/jdk8/jfxdocs/javafx/stage/Stage.html#setFullScreenExitKeyCombination-javafx.scene.input.KeyCombination- 646 | public void placePresent(int stopColor) { 647 | // TODO Auto-generated method stub 648 | PApplet.hideMenuBar(); 649 | } 650 | 651 | 652 | public void setLocation(int x, int y) { 653 | stage.setX(x); 654 | stage.setY(y); 655 | } 656 | 657 | 658 | public void setSize(int wide, int high) { 659 | // When the surface is set to resizable via surface.setResizable(true), 660 | // a crash may occur if the user sets the window to size zero. 661 | // https://github.com/processing/processing/issues/5052 662 | if (high <= 0) { 663 | high = 1; 664 | } 665 | if (wide <= 0) { 666 | wide = 1; 667 | } 668 | 669 | //System.out.format("%s.setSize(%d, %d)%n", getClass().getSimpleName(), width, height); 670 | Scene scene = stage.getScene(); 671 | double decorH = stage.getWidth() - scene.getWidth(); 672 | double decorV = stage.getHeight() - scene.getHeight(); 673 | stage.setWidth(wide + decorH); 674 | stage.setHeight(high + decorV); 675 | fx.setSize(wide, high); 676 | } 677 | 678 | 679 | // public Component getComponent() { 680 | // return null; 681 | // } 682 | 683 | 684 | public void setSmooth(int level) { 685 | // TODO Auto-generated method stub 686 | 687 | } 688 | 689 | 690 | public void setFrameRate(float fps) { 691 | // setting rate to negative so that event fires at the start of 692 | // the key frame and first frame is drawn immediately 693 | if (fps > 0) { 694 | frameRate = fps; 695 | animation.setRate(-frameRate); 696 | } 697 | } 698 | 699 | 700 | // @Override 701 | // public void requestFocus() { 702 | // canvas.requestFocus(); 703 | // } 704 | 705 | Cursor lastCursor = Cursor.DEFAULT; 706 | 707 | public void setCursor(int kind) { 708 | Cursor c; 709 | switch (kind) { 710 | case PConstants.ARROW: c = Cursor.DEFAULT; break; 711 | case PConstants.CROSS: c = Cursor.CROSSHAIR; break; 712 | case PConstants.HAND: c = Cursor.HAND; break; 713 | case PConstants.MOVE: c = Cursor.MOVE; break; 714 | case PConstants.TEXT: c = Cursor.TEXT; break; 715 | case PConstants.WAIT: c = Cursor.WAIT; break; 716 | default: c = Cursor.DEFAULT; break; 717 | } 718 | lastCursor = c; 719 | canvas.getScene().setCursor(c); 720 | } 721 | 722 | 723 | public void setCursor(PImage image, int hotspotX, int hotspotY) { 724 | int w = image.pixelWidth; 725 | int h = image.pixelHeight; 726 | WritableImage im = new WritableImage(w, h); 727 | im.getPixelWriter().setPixels(0, 0, w, h, 728 | PixelFormat.getIntArgbInstance(), 729 | image.pixels, 730 | 0, w); 731 | ImageCursor c = new ImageCursor(im, hotspotX, hotspotY); 732 | lastCursor = c; 733 | canvas.getScene().setCursor(c); 734 | } 735 | 736 | 737 | public void showCursor() { 738 | canvas.getScene().setCursor(lastCursor); 739 | } 740 | 741 | 742 | public void hideCursor() { 743 | canvas.getScene().setCursor(Cursor.NONE); 744 | } 745 | 746 | 747 | public boolean openLink(String url) { 748 | return ShimAWT.openLink(url); 749 | } 750 | 751 | 752 | public void startThread() { 753 | animation.play(); 754 | } 755 | 756 | 757 | public void pauseThread() { 758 | animation.pause(); 759 | } 760 | 761 | 762 | public void resumeThread() { 763 | animation.play(); 764 | } 765 | 766 | 767 | public boolean stopThread() { 768 | animation.stop(); 769 | return true; 770 | } 771 | 772 | 773 | public boolean isStopped() { 774 | return animation.getStatus() == Animation.Status.STOPPED; 775 | } 776 | 777 | 778 | // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 779 | 780 | 781 | /* 782 | protected void addListeners() { 783 | 784 | canvas.addMouseListener(new MouseListener() { 785 | 786 | public void mousePressed(java.awt.event.MouseEvent e) { 787 | nativeMouseEvent(e); 788 | } 789 | 790 | public void mouseReleased(java.awt.event.MouseEvent e) { 791 | nativeMouseEvent(e); 792 | } 793 | 794 | public void mouseClicked(java.awt.event.MouseEvent e) { 795 | nativeMouseEvent(e); 796 | } 797 | 798 | public void mouseEntered(java.awt.event.MouseEvent e) { 799 | nativeMouseEvent(e); 800 | } 801 | 802 | public void mouseExited(java.awt.event.MouseEvent e) { 803 | nativeMouseEvent(e); 804 | } 805 | }); 806 | 807 | canvas.addMouseMotionListener(new MouseMotionListener() { 808 | 809 | public void mouseDragged(java.awt.event.MouseEvent e) { 810 | nativeMouseEvent(e); 811 | } 812 | 813 | public void mouseMoved(java.awt.event.MouseEvent e) { 814 | nativeMouseEvent(e); 815 | } 816 | }); 817 | 818 | canvas.addMouseWheelListener(new MouseWheelListener() { 819 | 820 | public void mouseWheelMoved(MouseWheelEvent e) { 821 | nativeMouseEvent(e); 822 | } 823 | }); 824 | 825 | canvas.addKeyListener(new KeyListener() { 826 | 827 | public void keyPressed(java.awt.event.KeyEvent e) { 828 | nativeKeyEvent(e); 829 | } 830 | 831 | 832 | public void keyReleased(java.awt.event.KeyEvent e) { 833 | nativeKeyEvent(e); 834 | } 835 | 836 | 837 | public void keyTyped(java.awt.event.KeyEvent e) { 838 | nativeKeyEvent(e); 839 | } 840 | }); 841 | 842 | canvas.addFocusListener(new FocusListener() { 843 | 844 | public void focusGained(FocusEvent e) { 845 | sketch.focused = true; 846 | sketch.focusGained(); 847 | } 848 | 849 | public void focusLost(FocusEvent e) { 850 | sketch.focused = false; 851 | sketch.focusLost(); 852 | } 853 | }); 854 | } 855 | */ 856 | 857 | 858 | static Map, Integer> mouseMap = 859 | new HashMap<>(); 860 | static { 861 | mouseMap.put(MouseEvent.MOUSE_PRESSED, processing.event.MouseEvent.PRESS); 862 | mouseMap.put(MouseEvent.MOUSE_RELEASED, processing.event.MouseEvent.RELEASE); 863 | mouseMap.put(MouseEvent.MOUSE_CLICKED, processing.event.MouseEvent.CLICK); 864 | mouseMap.put(MouseEvent.MOUSE_DRAGGED, processing.event.MouseEvent.DRAG); 865 | mouseMap.put(MouseEvent.MOUSE_MOVED, processing.event.MouseEvent.MOVE); 866 | mouseMap.put(MouseEvent.MOUSE_ENTERED, processing.event.MouseEvent.ENTER); 867 | mouseMap.put(MouseEvent.MOUSE_EXITED, processing.event.MouseEvent.EXIT); 868 | } 869 | 870 | protected void fxMouseEvent(MouseEvent fxEvent) { 871 | // the 'amount' is the number of button clicks for a click event, 872 | // or the number of steps/clicks on the wheel for a mouse wheel event. 873 | int count = fxEvent.getClickCount(); 874 | 875 | int action = mouseMap.get(fxEvent.getEventType()); 876 | 877 | int modifiers = 0; 878 | if (fxEvent.isShiftDown()) { 879 | modifiers |= processing.event.Event.SHIFT; 880 | } 881 | if (fxEvent.isControlDown()) { 882 | modifiers |= processing.event.Event.CTRL; 883 | } 884 | if (fxEvent.isMetaDown()) { 885 | modifiers |= processing.event.Event.META; 886 | } 887 | if (fxEvent.isAltDown()) { 888 | modifiers |= processing.event.Event.ALT; 889 | } 890 | 891 | int button = 0; 892 | switch (fxEvent.getButton()) { 893 | case PRIMARY: 894 | button = PConstants.LEFT; 895 | break; 896 | case SECONDARY: 897 | button = PConstants.RIGHT; 898 | break; 899 | case MIDDLE: 900 | button = PConstants.CENTER; 901 | break; 902 | case NONE: 903 | case BACK: 904 | case FORWARD: 905 | // not currently handled 906 | break; 907 | } 908 | 909 | //long when = nativeEvent.getWhen(); // from AWT 910 | long when = System.currentTimeMillis(); 911 | int x = (int) fxEvent.getX(); // getSceneX()? 912 | int y = (int) fxEvent.getY(); 913 | 914 | sketch.postEvent(new processing.event.MouseEvent(fxEvent, when, 915 | action, modifiers, 916 | x, y, button, count)); 917 | } 918 | 919 | // https://docs.oracle.com/javase/8/javafx/api/javafx/scene/input/ScrollEvent.html 920 | protected void fxScrollEvent(ScrollEvent fxEvent) { 921 | // the number of steps/clicks on the wheel for a mouse wheel event. 922 | int count = (int) -(fxEvent.getDeltaY() / fxEvent.getMultiplierY()); 923 | 924 | int action = processing.event.MouseEvent.WHEEL; 925 | 926 | int modifiers = 0; 927 | if (fxEvent.isShiftDown()) { 928 | modifiers |= processing.event.Event.SHIFT; 929 | } 930 | if (fxEvent.isControlDown()) { 931 | modifiers |= processing.event.Event.CTRL; 932 | } 933 | if (fxEvent.isMetaDown()) { 934 | modifiers |= processing.event.Event.META; 935 | } 936 | if (fxEvent.isAltDown()) { 937 | modifiers |= processing.event.Event.ALT; 938 | } 939 | 940 | // FX does not supply button info 941 | int button = 0; 942 | 943 | long when = System.currentTimeMillis(); 944 | int x = (int) fxEvent.getX(); // getSceneX()? 945 | int y = (int) fxEvent.getY(); 946 | 947 | sketch.postEvent(new processing.event.MouseEvent(fxEvent, when, 948 | action, modifiers, 949 | x, y, button, count)); 950 | } 951 | 952 | 953 | protected void fxKeyEvent(javafx.scene.input.KeyEvent fxEvent) { 954 | int action = 0; 955 | EventType et = fxEvent.getEventType(); 956 | if (et == KeyEvent.KEY_PRESSED) { 957 | action = processing.event.KeyEvent.PRESS; 958 | } else if (et == KeyEvent.KEY_RELEASED) { 959 | action = processing.event.KeyEvent.RELEASE; 960 | } else if (et == KeyEvent.KEY_TYPED) { 961 | action = processing.event.KeyEvent.TYPE; 962 | } 963 | 964 | int modifiers = 0; 965 | if (fxEvent.isShiftDown()) { 966 | modifiers |= processing.event.Event.SHIFT; 967 | } 968 | if (fxEvent.isControlDown()) { 969 | modifiers |= processing.event.Event.CTRL; 970 | } 971 | if (fxEvent.isMetaDown()) { 972 | modifiers |= processing.event.Event.META; 973 | } 974 | if (fxEvent.isAltDown()) { 975 | modifiers |= processing.event.Event.ALT; 976 | } 977 | 978 | long when = System.currentTimeMillis(); 979 | 980 | char keyChar = getKeyChar(fxEvent); 981 | int keyCode = getKeyCode(fxEvent); 982 | sketch.postEvent(new processing.event.KeyEvent(fxEvent, when, 983 | action, modifiers, 984 | keyChar, keyCode)); 985 | } 986 | 987 | 988 | private int getKeyCode(KeyEvent fxEvent) { 989 | if (fxEvent.getEventType() == KeyEvent.KEY_TYPED) { 990 | return 0; 991 | } 992 | 993 | KeyCode kc = fxEvent.getCode(); 994 | switch (kc) { 995 | case ALT_GRAPH: 996 | return PConstants.ALT; 997 | default: 998 | break; 999 | } 1000 | return kc.getCode(); 1001 | } 1002 | 1003 | 1004 | private char getKeyChar(KeyEvent fxEvent) { 1005 | KeyCode kc = fxEvent.getCode(); 1006 | 1007 | // Overriding chars for some 1008 | // KEY_PRESSED and KEY_RELEASED events 1009 | switch (kc) { 1010 | case UP: 1011 | case KP_UP: 1012 | case DOWN: 1013 | case KP_DOWN: 1014 | case LEFT: 1015 | case KP_LEFT: 1016 | case RIGHT: 1017 | case KP_RIGHT: 1018 | case ALT: 1019 | case ALT_GRAPH: 1020 | case CONTROL: 1021 | case SHIFT: 1022 | case CAPS: 1023 | case META: 1024 | case WINDOWS: 1025 | case CONTEXT_MENU: 1026 | case HOME: 1027 | case PAGE_UP: 1028 | case PAGE_DOWN: 1029 | case END: 1030 | case PAUSE: 1031 | case PRINTSCREEN: 1032 | case INSERT: 1033 | case NUM_LOCK: 1034 | case SCROLL_LOCK: 1035 | case F1: 1036 | case F2: 1037 | case F3: 1038 | case F4: 1039 | case F5: 1040 | case F6: 1041 | case F7: 1042 | case F8: 1043 | case F9: 1044 | case F10: 1045 | case F11: 1046 | case F12: 1047 | return PConstants.CODED; 1048 | case ENTER: 1049 | return '\n'; 1050 | case DIVIDE: 1051 | return '/'; 1052 | case MULTIPLY: 1053 | return '*'; 1054 | case SUBTRACT: 1055 | return '-'; 1056 | case ADD: 1057 | return '+'; 1058 | case NUMPAD0: 1059 | return '0'; 1060 | case NUMPAD1: 1061 | return '1'; 1062 | case NUMPAD2: 1063 | return '2'; 1064 | case NUMPAD3: 1065 | return '3'; 1066 | case NUMPAD4: 1067 | return '4'; 1068 | case NUMPAD5: 1069 | return '5'; 1070 | case NUMPAD6: 1071 | return '6'; 1072 | case NUMPAD7: 1073 | return '7'; 1074 | case NUMPAD8: 1075 | return '8'; 1076 | case NUMPAD9: 1077 | return '9'; 1078 | case DECIMAL: 1079 | // KEY_TYPED does not go through here and will produce 1080 | // dot or comma based on the keyboard layout. 1081 | // For KEY_PRESSED and KEY_RELEASED, let's just go with 1082 | // the dot. Users can detect the key by its keyCode. 1083 | return '.'; 1084 | case UNDEFINED: 1085 | // KEY_TYPED has KeyCode: UNDEFINED 1086 | // and falls through here 1087 | break; 1088 | default: 1089 | break; 1090 | } 1091 | 1092 | // Just go with what FX gives us for the rest of 1093 | // KEY_PRESSED and KEY_RELEASED and all of KEY_TYPED 1094 | String ch; 1095 | if (fxEvent.getEventType() == KeyEvent.KEY_TYPED) { 1096 | ch = fxEvent.getCharacter(); 1097 | } else { 1098 | ch = kc.getChar(); 1099 | } 1100 | 1101 | if (ch.length() < 1) return PConstants.CODED; 1102 | if (ch.startsWith("\r")) return '\n'; // normalize enter key 1103 | return ch.charAt(0); 1104 | } 1105 | } 1106 | -------------------------------------------------------------------------------- /src/processing/javafx/PGraphicsFX2D.java: -------------------------------------------------------------------------------- 1 | /* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ 2 | 3 | /* 4 | Part of the Processing project - http://processing.org 5 | 6 | Copyright (c) 2015 The Processing Foundation 7 | 8 | This library is free software; you can redistribute it and/or 9 | modify it under the terms of the GNU Lesser General Public 10 | License as published by the Free Software Foundation, version 2.1. 11 | 12 | This library is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | Lesser General Public License for more details. 16 | 17 | You should have received a copy of the GNU Lesser General 18 | Public License along with this library; if not, write to the 19 | Free Software Foundation, Inc., 59 Temple Place, Suite 330, 20 | Boston, MA 02111-1307 USA 21 | */ 22 | 23 | package processing.javafx; 24 | 25 | import com.sun.javafx.geom.Path2D; 26 | import com.sun.javafx.geom.PathIterator; 27 | import com.sun.javafx.geom.Shape; 28 | 29 | import java.nio.IntBuffer; 30 | import java.util.HashMap; 31 | import java.util.HashSet; 32 | import java.util.LinkedHashMap; 33 | import java.util.Map; 34 | 35 | import javafx.scene.SnapshotParameters; 36 | import javafx.scene.canvas.Canvas; 37 | import javafx.scene.canvas.GraphicsContext; 38 | import javafx.scene.effect.BlendMode; 39 | import javafx.scene.image.PixelFormat; 40 | import javafx.scene.image.PixelReader; 41 | import javafx.scene.image.PixelWriter; 42 | import javafx.scene.image.WritableImage; 43 | import javafx.scene.image.WritablePixelFormat; 44 | import javafx.scene.paint.Color; 45 | import javafx.scene.shape.ArcType; 46 | import javafx.scene.shape.StrokeLineCap; 47 | import javafx.scene.shape.StrokeLineJoin; 48 | import javafx.scene.text.Font; 49 | import javafx.scene.text.Text; 50 | import javafx.scene.transform.Affine; 51 | import javafx.scene.transform.Transform; 52 | 53 | import processing.core.*; 54 | 55 | 56 | public class PGraphicsFX2D extends PGraphics { 57 | GraphicsContext context; 58 | 59 | static final WritablePixelFormat argbFormat = 60 | PixelFormat.getIntArgbInstance(); 61 | 62 | WritableImage snapshotImage; 63 | 64 | Path2D workPath = new Path2D(); 65 | Path2D auxPath = new Path2D(); 66 | boolean openContour; 67 | boolean adjustedForThinLines; 68 | /// break the shape at the next vertex (next vertex() call is a moveto()) 69 | boolean breakShape; 70 | 71 | private float[] pathCoordsBuffer = new float[6]; 72 | 73 | /// coordinates for internal curve calculation 74 | float[] curveCoordX; 75 | float[] curveCoordY; 76 | float[] curveDrawX; 77 | float[] curveDrawY; 78 | 79 | int transformCount; 80 | Affine[] transformStack = new Affine[MATRIX_STACK_DEPTH]; 81 | 82 | // Line2D.Float line = new Line2D.Float(); 83 | // Ellipse2D.Float ellipse = new Ellipse2D.Float(); 84 | // Rectangle2D.Float rect = new Rectangle2D.Float(); 85 | // Arc2D.Float arc = new Arc2D.Float(); 86 | // 87 | // protected Color tintColorObject; 88 | // 89 | // protected Color fillColorObject; 90 | // public boolean fillGradient; 91 | // public Paint fillGradientObject; 92 | // 93 | // protected Color strokeColorObject; 94 | // public boolean strokeGradient; 95 | // public Paint strokeGradientObject; 96 | 97 | 98 | 99 | ////////////////////////////////////////////////////////////// 100 | 101 | // INTERNAL 102 | 103 | 104 | public PGraphicsFX2D() { } 105 | 106 | 107 | //public void setParent(PApplet parent) 108 | 109 | 110 | //public void setPrimary(boolean primary) 111 | 112 | 113 | //public void setPath(String path) 114 | 115 | 116 | public void setSize(int width, int height) { 117 | if (!primaryGraphics && context == null) { 118 | context = new Canvas(width, height).getGraphicsContext2D(); 119 | } 120 | super.setSize(width, height); 121 | } 122 | 123 | 124 | //public void dispose() 125 | 126 | 127 | @Override 128 | public PSurface createSurface() { 129 | return surface = new PSurfaceFX(this); 130 | } 131 | 132 | 133 | /** Returns the javafx.scene.canvas.GraphicsContext used by this renderer. */ 134 | @Override 135 | public Object getNative() { 136 | return context; 137 | } 138 | 139 | 140 | ////////////////////////////////////////////////////////////// 141 | 142 | // FRAME 143 | 144 | 145 | // @Override 146 | // public boolean canDraw() { 147 | // return true; 148 | // } 149 | 150 | 151 | @Override 152 | public void beginDraw() { 153 | checkSettings(); 154 | resetMatrix(); // reset model matrix 155 | vertexCount = 0; 156 | } 157 | 158 | 159 | @Override 160 | public void endDraw() { 161 | flush(); 162 | 163 | if (!primaryGraphics) { 164 | // TODO this is probably overkill for most tasks... 165 | loadPixels(); 166 | // Make the image cache reload this. 167 | modified = true; 168 | } 169 | } 170 | 171 | 172 | 173 | ////////////////////////////////////////////////////////////// 174 | 175 | // SETTINGS 176 | 177 | 178 | //protected void checkSettings() 179 | 180 | 181 | //protected void defaultSettings() 182 | 183 | 184 | //protected void reapplySettings() 185 | 186 | 187 | 188 | ////////////////////////////////////////////////////////////// 189 | 190 | // HINT 191 | 192 | 193 | //public void hint(int which) 194 | 195 | 196 | 197 | ////////////////////////////////////////////////////////////// 198 | 199 | // SHAPE CREATION 200 | 201 | 202 | //protected PShape createShapeFamily(int type) 203 | 204 | 205 | //protected PShape createShapePrimitive(int kind, float... p) 206 | 207 | 208 | 209 | ////////////////////////////////////////////////////////////// 210 | 211 | // SHAPE 212 | 213 | 214 | @Override 215 | public void beginShape(int kind) { 216 | shape = kind; 217 | vertexCount = 0; 218 | curveVertexCount = 0; 219 | 220 | workPath.reset(); 221 | auxPath.reset(); 222 | 223 | flushPixels(); 224 | 225 | if (drawingThinLines()) { 226 | adjustedForThinLines = true; 227 | translate(0.5f, 0.5f); 228 | } 229 | } 230 | 231 | 232 | //public boolean edge(boolean e) 233 | 234 | 235 | //public void normal(float nx, float ny, float nz) { 236 | 237 | 238 | //public void textureMode(int mode) 239 | 240 | 241 | @Override 242 | public void texture(PImage image) { 243 | showMethodWarning("texture"); 244 | } 245 | 246 | 247 | @Override 248 | public void vertex(float x, float y) { 249 | if (vertexCount == vertices.length) { 250 | float[][] temp = new float[vertexCount<<1][VERTEX_FIELD_COUNT]; 251 | System.arraycopy(vertices, 0, temp, 0, vertexCount); 252 | vertices = temp; 253 | //message(CHATTER, "allocating more vertices " + vertices.length); 254 | } 255 | // not everyone needs this, but just easier to store rather 256 | // than adding another moving part to the code... 257 | vertices[vertexCount][X] = x; 258 | vertices[vertexCount][Y] = y; 259 | vertexCount++; 260 | 261 | switch (shape) { 262 | 263 | case POINTS: 264 | point(x, y); 265 | break; 266 | 267 | case LINES: 268 | if ((vertexCount % 2) == 0) { 269 | line(vertices[vertexCount-2][X], 270 | vertices[vertexCount-2][Y], x, y); 271 | } 272 | break; 273 | 274 | case TRIANGLES: 275 | if ((vertexCount % 3) == 0) { 276 | triangle(vertices[vertexCount - 3][X], 277 | vertices[vertexCount - 3][Y], 278 | vertices[vertexCount - 2][X], 279 | vertices[vertexCount - 2][Y], 280 | x, y); 281 | } 282 | break; 283 | 284 | case TRIANGLE_STRIP: 285 | if (vertexCount >= 3) { 286 | triangle(vertices[vertexCount - 2][X], 287 | vertices[vertexCount - 2][Y], 288 | vertices[vertexCount - 1][X], 289 | vertices[vertexCount - 1][Y], 290 | vertices[vertexCount - 3][X], 291 | vertices[vertexCount - 3][Y]); 292 | } 293 | break; 294 | 295 | case TRIANGLE_FAN: 296 | if (vertexCount >= 3) { 297 | // This is an unfortunate implementation because the stroke for an 298 | // adjacent triangle will be repeated. However, if the stroke is not 299 | // redrawn, it will replace the adjacent line (when it lines up 300 | // perfectly) or show a faint line (when off by a small amount). 301 | // The alternative would be to wait, then draw the shape as a 302 | // polygon fill, followed by a series of vertices. But that's a 303 | // poor method when used with PDF, DXF, or other recording objects, 304 | // since discrete triangles would likely be preferred. 305 | triangle(vertices[0][X], 306 | vertices[0][Y], 307 | vertices[vertexCount - 2][X], 308 | vertices[vertexCount - 2][Y], 309 | x, y); 310 | } 311 | break; 312 | 313 | case QUAD: 314 | case QUADS: 315 | if ((vertexCount % 4) == 0) { 316 | quad(vertices[vertexCount - 4][X], 317 | vertices[vertexCount - 4][Y], 318 | vertices[vertexCount - 3][X], 319 | vertices[vertexCount - 3][Y], 320 | vertices[vertexCount - 2][X], 321 | vertices[vertexCount - 2][Y], 322 | x, y); 323 | } 324 | break; 325 | 326 | case QUAD_STRIP: 327 | // 0---2---4 328 | // | | | 329 | // 1---3---5 330 | if ((vertexCount >= 4) && ((vertexCount % 2) == 0)) { 331 | quad(vertices[vertexCount - 4][X], 332 | vertices[vertexCount - 4][Y], 333 | vertices[vertexCount - 2][X], 334 | vertices[vertexCount - 2][Y], 335 | x, y, 336 | vertices[vertexCount - 3][X], 337 | vertices[vertexCount - 3][Y]); 338 | } 339 | break; 340 | 341 | case POLYGON: 342 | if (workPath.getNumCommands() == 0 || breakShape) { 343 | workPath.moveTo(x, y); 344 | breakShape = false; 345 | } else { 346 | workPath.lineTo(x, y); 347 | } 348 | break; 349 | } 350 | } 351 | 352 | 353 | @Override 354 | public void vertex(float x, float y, float z) { 355 | showDepthWarningXYZ("vertex"); 356 | } 357 | 358 | 359 | @Override 360 | public void vertex(float[] v) { 361 | vertex(v[X], v[Y]); 362 | } 363 | 364 | 365 | @Override 366 | public void vertex(float x, float y, float u, float v) { 367 | showVariationWarning("vertex(x, y, u, v)"); 368 | } 369 | 370 | 371 | @Override 372 | public void vertex(float x, float y, float z, float u, float v) { 373 | showDepthWarningXYZ("vertex"); 374 | } 375 | 376 | 377 | @Override 378 | public void beginContour() { 379 | if (openContour) { 380 | PGraphics.showWarning("Already called beginContour()"); 381 | return; 382 | } 383 | 384 | // draw contours to auxiliary path so main path can be closed later 385 | Path2D contourPath = auxPath; 386 | auxPath = workPath; 387 | workPath = contourPath; 388 | 389 | if (contourPath.getNumCommands() > 0) { // first contour does not break 390 | breakShape = true; 391 | } 392 | 393 | openContour = true; 394 | } 395 | 396 | 397 | @Override 398 | public void endContour() { 399 | if (!openContour) { 400 | PGraphics.showWarning("Need to call beginContour() first"); 401 | return; 402 | } 403 | 404 | if (workPath.getNumCommands() > 0) workPath.closePath(); 405 | 406 | Path2D temp = workPath; 407 | workPath = auxPath; 408 | auxPath = temp; 409 | 410 | openContour = false; 411 | } 412 | 413 | 414 | @Override 415 | public void endShape(int mode) { 416 | if (openContour) { // correct automagically, notify user 417 | endContour(); 418 | PGraphics.showWarning("Missing endContour() before endShape()"); 419 | } 420 | if (workPath.getNumCommands() > 0) { 421 | if (shape == POLYGON) { 422 | if (mode == CLOSE) { 423 | workPath.closePath(); 424 | } 425 | if (auxPath.getNumCommands() > 0) { 426 | workPath.append(auxPath, false); 427 | } 428 | drawShape(workPath); 429 | } 430 | } 431 | shape = 0; 432 | if (adjustedForThinLines) { 433 | adjustedForThinLines = false; 434 | translate(-0.5f, -0.5f); 435 | } 436 | loaded = false; 437 | } 438 | 439 | 440 | private void drawShape(Shape s) { 441 | context.beginPath(); 442 | PathIterator pi = s.getPathIterator(null); 443 | while (!pi.isDone()) { 444 | int piType = pi.currentSegment(pathCoordsBuffer); 445 | switch (piType) { 446 | case PathIterator.SEG_MOVETO: 447 | context.moveTo(pathCoordsBuffer[0], pathCoordsBuffer[1]); 448 | break; 449 | case PathIterator.SEG_LINETO: 450 | context.lineTo(pathCoordsBuffer[0], pathCoordsBuffer[1]); 451 | break; 452 | case PathIterator.SEG_QUADTO: 453 | context.quadraticCurveTo(pathCoordsBuffer[0], pathCoordsBuffer[1], 454 | pathCoordsBuffer[2], pathCoordsBuffer[3]); 455 | break; 456 | case PathIterator.SEG_CUBICTO: 457 | context.bezierCurveTo(pathCoordsBuffer[0], pathCoordsBuffer[1], 458 | pathCoordsBuffer[2], pathCoordsBuffer[3], 459 | pathCoordsBuffer[4], pathCoordsBuffer[5]); 460 | break; 461 | case PathIterator.SEG_CLOSE: 462 | context.closePath(); 463 | break; 464 | default: 465 | showWarning("Unknown segment type " + piType); 466 | } 467 | pi.next(); 468 | } 469 | if (fill) context.fill(); 470 | if (stroke) context.stroke(); 471 | } 472 | 473 | 474 | 475 | ////////////////////////////////////////////////////////////// 476 | 477 | // CLIPPING 478 | 479 | 480 | @Override 481 | protected void clipImpl(float x1, float y1, float x2, float y2) { 482 | //g2.setClip(new Rectangle2D.Float(x1, y1, x2 - x1, y2 - y1)); 483 | showTodoWarning("clip()", 3274); 484 | } 485 | 486 | 487 | @Override 488 | public void noClip() { 489 | //g2.setClip(null); 490 | showTodoWarning("noClip()", 3274); 491 | } 492 | 493 | 494 | 495 | ////////////////////////////////////////////////////////////// 496 | 497 | // BLEND 498 | 499 | 500 | @Override 501 | protected void blendModeImpl() { 502 | BlendMode mode = BlendMode.SRC_OVER; 503 | switch (blendMode) { 504 | case REPLACE: showWarning("blendMode(REPLACE) is not supported"); break; 505 | case BLEND: break; // this is SRC_OVER, the default 506 | case ADD: mode = BlendMode.ADD; break; // everyone's favorite 507 | case SUBTRACT: showWarning("blendMode(SUBTRACT) is not supported"); break; 508 | case LIGHTEST: mode = BlendMode.LIGHTEN; break; 509 | case DARKEST: mode = BlendMode.DARKEN; break; 510 | case DIFFERENCE: mode = BlendMode.DIFFERENCE; break; 511 | case EXCLUSION: mode = BlendMode.EXCLUSION; break; 512 | case MULTIPLY: mode = BlendMode.MULTIPLY; break; 513 | case SCREEN: mode = BlendMode.SCREEN; break; 514 | case OVERLAY: mode = BlendMode.OVERLAY; break; 515 | case HARD_LIGHT: mode = BlendMode.HARD_LIGHT; break; 516 | case SOFT_LIGHT: mode = BlendMode.SOFT_LIGHT; break; 517 | case DODGE: mode = BlendMode.COLOR_DODGE; break; 518 | case BURN: mode = BlendMode.COLOR_BURN; break; 519 | } 520 | context.setGlobalBlendMode(mode); 521 | } 522 | 523 | 524 | 525 | ////////////////////////////////////////////////////////////// 526 | 527 | // BEZIER VERTICES 528 | 529 | 530 | @Override 531 | protected void bezierVertexCheck() { 532 | if (shape == 0 || shape != POLYGON) { 533 | throw new RuntimeException("beginShape() or beginShape(POLYGON) " + 534 | "must be used before bezierVertex() or quadraticVertex()"); 535 | } 536 | if (workPath.getNumCommands() == 0) { 537 | throw new RuntimeException("vertex() must be used at least once " + 538 | "before bezierVertex() or quadraticVertex()"); 539 | } 540 | } 541 | 542 | @Override 543 | public void bezierVertex(float x1, float y1, 544 | float x2, float y2, 545 | float x3, float y3) { 546 | bezierVertexCheck(); 547 | workPath.curveTo(x1, y1, x2, y2, x3, y3); 548 | } 549 | 550 | 551 | @Override 552 | public void bezierVertex(float x2, float y2, float z2, 553 | float x3, float y3, float z3, 554 | float x4, float y4, float z4) { 555 | showDepthWarningXYZ("bezierVertex"); 556 | } 557 | 558 | 559 | 560 | ////////////////////////////////////////////////////////////// 561 | 562 | // QUADRATIC BEZIER VERTICES 563 | 564 | 565 | @Override 566 | public void quadraticVertex(float ctrlX, float ctrlY, 567 | float endX, float endY) { 568 | bezierVertexCheck(); 569 | workPath.quadTo(ctrlX, ctrlY, endX, endY); 570 | } 571 | 572 | 573 | @Override 574 | public void quadraticVertex(float x2, float y2, float z2, 575 | float x4, float y4, float z4) { 576 | showDepthWarningXYZ("quadVertex"); 577 | } 578 | 579 | 580 | 581 | ////////////////////////////////////////////////////////////// 582 | 583 | // CURVE VERTICES 584 | 585 | 586 | @Override 587 | protected void curveVertexSegment(float x1, float y1, 588 | float x2, float y2, 589 | float x3, float y3, 590 | float x4, float y4) { 591 | if (curveCoordX == null) { 592 | curveCoordX = new float[4]; 593 | curveCoordY = new float[4]; 594 | curveDrawX = new float[4]; 595 | curveDrawY = new float[4]; 596 | } 597 | 598 | curveCoordX[0] = x1; 599 | curveCoordY[0] = y1; 600 | 601 | curveCoordX[1] = x2; 602 | curveCoordY[1] = y2; 603 | 604 | curveCoordX[2] = x3; 605 | curveCoordY[2] = y3; 606 | 607 | curveCoordX[3] = x4; 608 | curveCoordY[3] = y4; 609 | 610 | curveToBezierMatrix.mult(curveCoordX, curveDrawX); 611 | curveToBezierMatrix.mult(curveCoordY, curveDrawY); 612 | 613 | // since the paths are continuous, 614 | // only the first point needs the actual moveto 615 | if (workPath.getNumCommands() == 0) { 616 | workPath.moveTo(curveDrawX[0], curveDrawY[0]); 617 | breakShape = false; 618 | } 619 | 620 | workPath.curveTo(curveDrawX[1], curveDrawY[1], 621 | curveDrawX[2], curveDrawY[2], 622 | curveDrawX[3], curveDrawY[3]); 623 | } 624 | 625 | 626 | @Override 627 | public void curveVertex(float x, float y, float z) { 628 | showDepthWarningXYZ("curveVertex"); 629 | } 630 | 631 | 632 | 633 | ////////////////////////////////////////////////////////////// 634 | 635 | // RENDERER 636 | 637 | @Override 638 | public void flush() { 639 | flushPixels(); 640 | } 641 | 642 | 643 | protected void flushPixels() { 644 | boolean hasPixels = modified && pixels != null; 645 | if (hasPixels) { 646 | // If the user has been manipulating individual pixels, 647 | // the changes need to be copied to the screen before 648 | // drawing any new geometry. 649 | int mx1 = getModifiedX1(); 650 | int mx2 = getModifiedX2(); 651 | int my1 = getModifiedY1(); 652 | int my2 = getModifiedY2(); 653 | int mw = mx2 - mx1; 654 | int mh = my2 - my1; 655 | 656 | if (pixelDensity == 1) { 657 | PixelWriter pw = context.getPixelWriter(); 658 | pw.setPixels(mx1, my1, mw, mh, argbFormat, pixels, 659 | mx1 + my1 * pixelWidth, pixelWidth); 660 | } else { 661 | // The only way to push all the pixels is to draw a scaled-down image 662 | if (snapshotImage == null || 663 | snapshotImage.getWidth() != pixelWidth || 664 | snapshotImage.getHeight() != pixelHeight) { 665 | snapshotImage = new WritableImage(pixelWidth, pixelHeight); 666 | } 667 | 668 | PixelWriter pw = snapshotImage.getPixelWriter(); 669 | pw.setPixels(mx1, my1, mw, mh, argbFormat, pixels, 670 | mx1 + my1 * pixelWidth, pixelWidth); 671 | context.save(); 672 | resetMatrix(); 673 | context.scale(1d / pixelDensity, 1d / pixelDensity); 674 | context.drawImage(snapshotImage, mx1, my1, mw, mh, mx1, my1, mw, mh); 675 | context.restore(); 676 | } 677 | } 678 | 679 | modified = false; 680 | } 681 | 682 | 683 | protected void beforeContextDraw() { 684 | flushPixels(); 685 | loaded = false; 686 | } 687 | 688 | 689 | ////////////////////////////////////////////////////////////// 690 | 691 | // POINT, LINE, TRIANGLE, QUAD 692 | 693 | 694 | @Override 695 | public void point(float x, float y) { 696 | if (stroke) { 697 | // if (strokeWeight > 1) { 698 | line(x, y, x + EPSILON, y + EPSILON); 699 | // } else { 700 | // set((int) screenX(x, y), (int) screenY(x, y), strokeColor); 701 | // } 702 | } 703 | } 704 | 705 | 706 | @Override 707 | public void line(float x1, float y1, float x2, float y2) { 708 | beforeContextDraw(); 709 | if (drawingThinLines()) { 710 | x1 += 0.5f; 711 | x2 += 0.5f; 712 | y1 += 0.5f; 713 | y2 += 0.5f; 714 | } 715 | context.strokeLine(x1, y1, x2, y2); 716 | } 717 | 718 | 719 | @Override 720 | public void triangle(float x1, float y1, float x2, float y2, 721 | float x3, float y3) { 722 | beforeContextDraw(); 723 | if (drawingThinLines()) { 724 | x1 += 0.5f; 725 | x2 += 0.5f; 726 | x3 += 0.5f; 727 | y1 += 0.5f; 728 | y2 += 0.5f; 729 | y3 += 0.5f; 730 | } 731 | context.beginPath(); 732 | context.moveTo(x1, y1); 733 | context.lineTo(x2, y2); 734 | context.lineTo(x3, y3); 735 | context.closePath(); 736 | if (fill) context.fill(); 737 | if (stroke) context.stroke(); 738 | } 739 | 740 | 741 | @Override 742 | public void quad(float x1, float y1, float x2, float y2, 743 | float x3, float y3, float x4, float y4) { 744 | beforeContextDraw(); 745 | if (drawingThinLines()) { 746 | x1 += 0.5f; 747 | x2 += 0.5f; 748 | x3 += 0.5f; 749 | x4 += 0.5f; 750 | y1 += 0.5f; 751 | y2 += 0.5f; 752 | y3 += 0.5f; 753 | y4 += 0.5f; 754 | } 755 | context.beginPath(); 756 | context.moveTo(x1, y1); 757 | context.lineTo(x2, y2); 758 | context.lineTo(x3, y3); 759 | context.lineTo(x4, y4); 760 | context.closePath(); 761 | if (fill) context.fill(); 762 | if (stroke) context.stroke(); 763 | } 764 | 765 | 766 | 767 | ////////////////////////////////////////////////////////////// 768 | 769 | // RECT 770 | 771 | 772 | //public void rectMode(int mode) 773 | 774 | 775 | //public void rect(float a, float b, float c, float d) 776 | 777 | 778 | @Override 779 | protected void rectImpl(float x1, float y1, float x2, float y2) { 780 | beforeContextDraw(); 781 | if (drawingThinLines()) { 782 | x1 += 0.5f; 783 | x2 += 0.5f; 784 | y1 += 0.5f; 785 | y2 += 0.5f; 786 | } 787 | if (fill) context.fillRect(x1, y1, x2 - x1, y2 - y1); 788 | if (stroke) context.strokeRect(x1, y1, x2 - x1, y2 - y1); 789 | } 790 | 791 | 792 | 793 | ////////////////////////////////////////////////////////////// 794 | 795 | // ELLIPSE 796 | 797 | 798 | //public void ellipseMode(int mode) 799 | 800 | 801 | //public void ellipse(float a, float b, float c, float d) 802 | 803 | 804 | @Override 805 | protected void ellipseImpl(float x, float y, float w, float h) { 806 | beforeContextDraw(); 807 | if (drawingThinLines()) { 808 | x += 0.5f; 809 | y += 0.5f; 810 | } 811 | if (fill) context.fillOval(x, y, w, h); 812 | if (stroke) context.strokeOval(x, y, w, h); 813 | } 814 | 815 | 816 | 817 | ////////////////////////////////////////////////////////////// 818 | 819 | // ARC 820 | 821 | 822 | //public void arc(float a, float b, float c, float d, 823 | // float start, float stop) 824 | 825 | 826 | @Override 827 | protected void arcImpl(float x, float y, float w, float h, 828 | float start, float stop, int mode) { 829 | beforeContextDraw(); 830 | 831 | if (drawingThinLines()) { 832 | x += 0.5f; 833 | y += 0.5f; 834 | } 835 | 836 | // 0 to 90 in java would be 0 to -90 for p5 renderer 837 | // but that won't work, so -90 to 0? 838 | start = -start; 839 | stop = -stop; 840 | 841 | float sweep = stop - start; 842 | 843 | // The defaults, before 2.0b7, were to stroke as Arc2D.OPEN, and then fill 844 | // using Arc2D.PIE. That's a little wonky, but it's here for compatibility. 845 | ArcType fillMode = ArcType.ROUND; // Arc2D.PIE 846 | ArcType strokeMode = ArcType.OPEN; 847 | 848 | if (mode == OPEN) { 849 | fillMode = ArcType.OPEN; 850 | 851 | } else if (mode == PIE) { 852 | strokeMode = ArcType.ROUND; // PIE 853 | 854 | } else if (mode == CHORD) { 855 | fillMode = ArcType.CHORD; 856 | strokeMode = ArcType.CHORD; 857 | } 858 | 859 | if (fill) { 860 | context.fillArc(x, y, w, h, PApplet.degrees(start), PApplet.degrees(sweep), fillMode); 861 | } 862 | if (stroke) { 863 | context.strokeArc(x, y, w, h, PApplet.degrees(start), PApplet.degrees(sweep), strokeMode); 864 | } 865 | } 866 | 867 | 868 | 869 | ////////////////////////////////////////////////////////////// 870 | 871 | // BOX 872 | 873 | 874 | //public void box(float size) 875 | 876 | 877 | @Override 878 | public void box(float w, float h, float d) { 879 | showMethodWarning("box"); 880 | } 881 | 882 | 883 | 884 | ////////////////////////////////////////////////////////////// 885 | 886 | // SPHERE 887 | 888 | 889 | //public void sphereDetail(int res) 890 | 891 | 892 | //public void sphereDetail(int ures, int vres) 893 | 894 | 895 | @Override 896 | public void sphere(float r) { 897 | showMethodWarning("sphere"); 898 | } 899 | 900 | 901 | 902 | ////////////////////////////////////////////////////////////// 903 | 904 | // BEZIER 905 | 906 | 907 | //public float bezierPoint(float a, float b, float c, float d, float t) 908 | 909 | 910 | //public float bezierTangent(float a, float b, float c, float d, float t) 911 | 912 | 913 | //protected void bezierInitCheck() 914 | 915 | 916 | //protected void bezierInit() 917 | 918 | 919 | /** Ignored (not needed) by this renderer. */ 920 | @Override 921 | public void bezierDetail(int detail) { } 922 | 923 | 924 | //public void bezier(float x1, float y1, 925 | // float x2, float y2, 926 | // float x3, float y3, 927 | // float x4, float y4) 928 | 929 | 930 | //public void bezier(float x1, float y1, float z1, 931 | // float x2, float y2, float z2, 932 | // float x3, float y3, float z3, 933 | // float x4, float y4, float z4) 934 | 935 | 936 | 937 | ////////////////////////////////////////////////////////////// 938 | 939 | // CURVE 940 | 941 | 942 | //public float curvePoint(float a, float b, float c, float d, float t) 943 | 944 | 945 | //public float curveTangent(float a, float b, float c, float d, float t) 946 | 947 | 948 | /** Ignored (not needed) by this renderer. */ 949 | @Override 950 | public void curveDetail(int detail) { } 951 | 952 | 953 | //public void curveTightness(float tightness) 954 | 955 | 956 | //protected void curveInitCheck() 957 | 958 | 959 | //protected void curveInit() 960 | 961 | 962 | //public void curve(float x1, float y1, 963 | // float x2, float y2, 964 | // float x3, float y3, 965 | // float x4, float y4) 966 | 967 | 968 | //public void curve(float x1, float y1, float z1, 969 | // float x2, float y2, float z2, 970 | // float x3, float y3, float z3, 971 | // float x4, float y4, float z4) 972 | 973 | 974 | 975 | ////////////////////////////////////////////////////////////// 976 | 977 | // SMOOTH 978 | 979 | 980 | // @Override 981 | // public void smooth() { 982 | // smooth = true; 983 | // 984 | // if (quality == 0) { 985 | // quality = 4; // change back to bicubic 986 | // } 987 | // } 988 | 989 | 990 | // @Override 991 | // public void smooth(int quality) { 992 | //// this.quality = quality; 993 | //// if (quality == 0) { 994 | //// noSmooth(); 995 | //// } else { 996 | //// smooth(); 997 | //// } 998 | // showMissingWarning("smooth"); 999 | // } 1000 | // 1001 | // 1002 | // @Override 1003 | // public void noSmooth() { 1004 | // showMissingWarning("noSmooth"); 1005 | // } 1006 | 1007 | 1008 | 1009 | ////////////////////////////////////////////////////////////// 1010 | 1011 | // IMAGE 1012 | 1013 | 1014 | //public void imageMode(int mode) 1015 | 1016 | 1017 | //public void image(PImage image, float x, float y) 1018 | 1019 | 1020 | //public void image(PImage image, float x, float y, float c, float d) 1021 | 1022 | 1023 | //public void image(PImage image, 1024 | // float a, float b, float c, float d, 1025 | // int u1, int v1, int u2, int v2) 1026 | 1027 | 1028 | /** 1029 | * Handle renderer-specific image drawing. 1030 | */ 1031 | @Override 1032 | protected void imageImpl(PImage who, 1033 | float x1, float y1, float x2, float y2, 1034 | int u1, int v1, int u2, int v2) { 1035 | // Image not ready yet, or an error 1036 | if (who.width <= 0 || who.height <= 0) return; 1037 | 1038 | ImageCache cash = (ImageCache) getCache(who); 1039 | 1040 | // Nuke the cache if the image was resized 1041 | if (cash != null) { 1042 | if (who.pixelWidth != cash.image.getWidth() || 1043 | who.pixelHeight != cash.image.getHeight()) { 1044 | cash = null; 1045 | } 1046 | } 1047 | 1048 | if (cash == null) { 1049 | //System.out.println("making new image cache"); 1050 | cash = new ImageCache(); //who); 1051 | setCache(who, cash); 1052 | who.updatePixels(); // mark the whole thing for update 1053 | who.setModified(); 1054 | } 1055 | 1056 | // If image previously was tinted, or the color changed 1057 | // or the image was tinted, and tint is now disabled 1058 | if ((tint && !cash.tinted) || 1059 | (tint && (cash.tintedColor != tintColor)) || 1060 | (!tint && cash.tinted)) { 1061 | // For tint change, mark all pixels as needing update. 1062 | who.updatePixels(); 1063 | } 1064 | 1065 | if (who.isModified()) { 1066 | if (who.pixels == null) { 1067 | // This might be a PGraphics that hasn't been drawn to yet. 1068 | // Can't just bail because the cache has been created above. 1069 | // https://github.com/processing/processing/issues/2208 1070 | who.pixels = new int[who.pixelWidth * who.pixelHeight]; 1071 | } 1072 | cash.update(who, tint, tintColor); 1073 | who.setModified(false); 1074 | } 1075 | 1076 | u1 *= who.pixelDensity; 1077 | v1 *= who.pixelDensity; 1078 | u2 *= who.pixelDensity; 1079 | v2 *= who.pixelDensity; 1080 | 1081 | context.drawImage(((ImageCache) getCache(who)).image, 1082 | u1, v1, u2-u1, v2-v1, 1083 | x1, y1, x2-x1, y2-y1); 1084 | } 1085 | 1086 | 1087 | static class ImageCache { 1088 | boolean tinted; 1089 | int tintedColor; 1090 | int[] tintedTemp; // one row of tinted pixels 1091 | //BufferedImage image; 1092 | WritableImage image; 1093 | 1094 | /** 1095 | * Update the pixels of the cache image. Already determined that the tint 1096 | * has changed, or the pixels have changed, so should just go through 1097 | * with the update without further checks. 1098 | */ 1099 | public void update(PImage source, boolean tint, int tintColor) { 1100 | //int bufferType = BufferedImage.TYPE_INT_ARGB; 1101 | int targetType = ARGB; 1102 | boolean opaque = (tintColor & 0xFF000000) == 0xFF000000; 1103 | if (source.format == RGB) { 1104 | if (!tint || opaque) { 1105 | //bufferType = BufferedImage.TYPE_INT_RGB; 1106 | targetType = RGB; 1107 | } 1108 | } 1109 | // boolean wrongType = (image != null) && (image.getType() != bufferType); 1110 | // if ((image == null) || wrongType) { 1111 | // image = new BufferedImage(source.width, source.height, bufferType); 1112 | // } 1113 | // Must always use an ARGB image, otherwise will write zeros 1114 | // in the alpha channel when drawn to the screen. 1115 | // https://github.com/processing/processing/issues/2030 1116 | // if (image == null) { 1117 | // image = new BufferedImage(source.width, source.height, 1118 | // BufferedImage.TYPE_INT_ARGB); 1119 | // } 1120 | if (image == null) { 1121 | image = new WritableImage(source.pixelWidth, source.pixelHeight); 1122 | } 1123 | 1124 | //WritableRaster wr = image.getRaster(); 1125 | PixelWriter pw = image.getPixelWriter(); 1126 | if (tint) { 1127 | if (tintedTemp == null || tintedTemp.length != source.pixelWidth) { 1128 | tintedTemp = new int[source.pixelWidth]; 1129 | } 1130 | int a2 = (tintColor >> 24) & 0xff; 1131 | // System.out.println("tint color is " + a2); 1132 | // System.out.println("source.pixels[0] alpha is " + (source.pixels[0] >>> 24)); 1133 | int r2 = (tintColor >> 16) & 0xff; 1134 | int g2 = (tintColor >> 8) & 0xff; 1135 | int b2 = (tintColor) & 0xff; 1136 | 1137 | //if (bufferType == BufferedImage.TYPE_INT_RGB) { 1138 | if (targetType == RGB) { 1139 | // The target image is opaque, meaning that the source image has no 1140 | // alpha (is not ARGB), and the tint has no alpha. 1141 | int index = 0; 1142 | for (int y = 0; y < source.pixelHeight; y++) { 1143 | for (int x = 0; x < source.pixelWidth; x++) { 1144 | int argb1 = source.pixels[index++]; 1145 | int r1 = (argb1 >> 16) & 0xff; 1146 | int g1 = (argb1 >> 8) & 0xff; 1147 | int b1 = (argb1) & 0xff; 1148 | 1149 | // Prior to 2.1, the alpha channel was commented out here, 1150 | // but can't remember why (just thought unnecessary b/c of RGB?) 1151 | // https://github.com/processing/processing/issues/2030 1152 | tintedTemp[x] = 0xFF000000 | 1153 | (((r2 * r1) & 0xff00) << 8) | 1154 | ((g2 * g1) & 0xff00) | 1155 | (((b2 * b1) & 0xff00) >> 8); 1156 | } 1157 | //wr.setDataElements(0, y, source.width, 1, tintedTemp); 1158 | pw.setPixels(0, y, source.pixelWidth, 1, argbFormat, tintedTemp, 0, source.pixelWidth); 1159 | } 1160 | // could this be any slower? 1161 | // float[] scales = { tintR, tintG, tintB }; 1162 | // float[] offsets = new float[3]; 1163 | // RescaleOp op = new RescaleOp(scales, offsets, null); 1164 | // op.filter(image, image); 1165 | 1166 | } else { // targetType == ARGB 1167 | if (source.format == RGB && 1168 | (tintColor & 0xffffff) == 0xffffff) { 1169 | int hi = tintColor & 0xff000000; 1170 | int index = 0; 1171 | for (int y = 0; y < source.pixelHeight; y++) { 1172 | for (int x = 0; x < source.pixelWidth; x++) { 1173 | tintedTemp[x] = hi | (source.pixels[index++] & 0xFFFFFF); 1174 | } 1175 | //wr.setDataElements(0, y, source.width, 1, tintedTemp); 1176 | pw.setPixels(0, y, source.pixelWidth, 1, argbFormat, tintedTemp, 0, source.pixelWidth); 1177 | } 1178 | } else { 1179 | int index = 0; 1180 | for (int y = 0; y < source.pixelHeight; y++) { 1181 | if (source.format == RGB) { 1182 | int alpha = tintColor & 0xFF000000; 1183 | for (int x = 0; x < source.pixelWidth; x++) { 1184 | int argb1 = source.pixels[index++]; 1185 | int r1 = (argb1 >> 16) & 0xff; 1186 | int g1 = (argb1 >> 8) & 0xff; 1187 | int b1 = (argb1) & 0xff; 1188 | tintedTemp[x] = alpha | 1189 | (((r2 * r1) & 0xff00) << 8) | 1190 | ((g2 * g1) & 0xff00) | 1191 | (((b2 * b1) & 0xff00) >> 8); 1192 | } 1193 | } else if (source.format == ARGB) { 1194 | for (int x = 0; x < source.pixelWidth; x++) { 1195 | int argb1 = source.pixels[index++]; 1196 | int a1 = (argb1 >> 24) & 0xff; 1197 | int r1 = (argb1 >> 16) & 0xff; 1198 | int g1 = (argb1 >> 8) & 0xff; 1199 | int b1 = (argb1) & 0xff; 1200 | tintedTemp[x] = 1201 | (((a2 * a1) & 0xff00) << 16) | 1202 | (((r2 * r1) & 0xff00) << 8) | 1203 | ((g2 * g1) & 0xff00) | 1204 | (((b2 * b1) & 0xff00) >> 8); 1205 | } 1206 | } else if (source.format == ALPHA) { 1207 | int lower = tintColor & 0xFFFFFF; 1208 | for (int x = 0; x < source.pixelWidth; x++) { 1209 | int a1 = source.pixels[index++]; 1210 | tintedTemp[x] = 1211 | (((a2 * a1) & 0xff00) << 16) | lower; 1212 | } 1213 | } 1214 | //wr.setDataElements(0, y, source.width, 1, tintedTemp); 1215 | pw.setPixels(0, y, source.pixelWidth, 1, argbFormat, tintedTemp, 0, source.pixelWidth); 1216 | } 1217 | } 1218 | // Not sure why ARGB images take the scales in this order... 1219 | // float[] scales = { tintR, tintG, tintB, tintA }; 1220 | // float[] offsets = new float[4]; 1221 | // RescaleOp op = new RescaleOp(scales, offsets, null); 1222 | // op.filter(image, image); 1223 | } 1224 | } else { // !tint 1225 | if (targetType == RGB && (source.pixels[0] >> 24 == 0)) { 1226 | // If it's an RGB image and the high bits aren't set, need to set 1227 | // the high bits to opaque because we're drawing ARGB images. 1228 | source.filter(OPAQUE); 1229 | // Opting to just manipulate the image here, since it shouldn't 1230 | // affect anything else (and alpha(get(x, y)) should return 0xff). 1231 | // Wel also make no guarantees about the values of the pixels array 1232 | // in a PImage and how the high bits will be set. 1233 | } 1234 | // If no tint, just shove the pixels on in there verbatim 1235 | //wr.setDataElements(0, 0, source.width, source.height, source.pixels); 1236 | //System.out.println("moving the big one"); 1237 | pw.setPixels(0, 0, source.pixelWidth, source.pixelHeight, 1238 | argbFormat, source.pixels, 0, source.pixelWidth); 1239 | } 1240 | this.tinted = tint; 1241 | this.tintedColor = tintColor; 1242 | 1243 | // GraphicsConfiguration gc = parent.getGraphicsConfiguration(); 1244 | // compat = gc.createCompatibleImage(image.getWidth(), 1245 | // image.getHeight(), 1246 | // Transparency.TRANSLUCENT); 1247 | // 1248 | // Graphics2D g = compat.createGraphics(); 1249 | // g.drawImage(image, 0, 0, null); 1250 | // g.dispose(); 1251 | } 1252 | } 1253 | 1254 | 1255 | 1256 | ////////////////////////////////////////////////////////////// 1257 | 1258 | // SHAPE 1259 | 1260 | 1261 | //public void shapeMode(int mode) 1262 | 1263 | 1264 | //public void shape(PShape shape) 1265 | 1266 | 1267 | //public void shape(PShape shape, float x, float y) 1268 | 1269 | 1270 | //public void shape(PShape shape, float x, float y, float c, float d) 1271 | 1272 | 1273 | ////////////////////////////////////////////////////////////// 1274 | 1275 | // SHAPE I/O 1276 | 1277 | 1278 | @Override 1279 | public PShape loadShape(String filename) { 1280 | return loadShape(filename, null); 1281 | } 1282 | 1283 | 1284 | @Override 1285 | public PShape loadShape(String filename, String options) { 1286 | String extension = PApplet.getExtension(filename); 1287 | if (extension.equals("svg") || extension.equals("svgz")) { 1288 | return new PShapeSVG(parent.loadXML(filename)); 1289 | } 1290 | PGraphics.showWarning("Unsupported format: " + filename); 1291 | return null; 1292 | } 1293 | 1294 | 1295 | 1296 | ////////////////////////////////////////////////////////////// 1297 | 1298 | // TEXT ATTRIBUTES 1299 | 1300 | 1301 | protected FontCache fontCache = new FontCache(); 1302 | 1303 | // Is initialized when defaultFontOrDeath() is called 1304 | // and mirrors PGraphics.textFont field 1305 | protected FontInfo textFontInfo; 1306 | 1307 | 1308 | @Override 1309 | protected PFont createFont(String name, float size, 1310 | boolean smooth, char[] charset) { 1311 | PFont font = super.createFont(name, size, smooth, charset); 1312 | if (font.isStream()) { 1313 | fontCache.nameToFilename.put(font.getName(), name); 1314 | } 1315 | return font; 1316 | } 1317 | 1318 | 1319 | @Override 1320 | protected void defaultFontOrDeath(String method, float size) { 1321 | super.defaultFontOrDeath(method, size); 1322 | handleTextFont(textFont, size); 1323 | } 1324 | 1325 | 1326 | @Override 1327 | protected boolean textModeCheck(int mode) { 1328 | return mode == MODEL; 1329 | } 1330 | 1331 | 1332 | @Override 1333 | public float textAscent() { 1334 | if (textFont == null) { 1335 | defaultFontOrDeath("textAscent"); 1336 | } 1337 | if (textFontInfo.font == null) { 1338 | return super.textAscent(); 1339 | } 1340 | return textFontInfo.ascent; 1341 | } 1342 | 1343 | 1344 | @Override 1345 | public float textDescent() { 1346 | if (textFont == null) { 1347 | defaultFontOrDeath("textDescent"); 1348 | } 1349 | if (textFontInfo.font == null) { 1350 | return super.textDescent(); 1351 | } 1352 | return textFontInfo.descent; 1353 | } 1354 | 1355 | 1356 | static final class FontInfo { 1357 | // TODO: this should be based on memory consumption 1358 | // this should be enough e.g. for all grays and alpha combos 1359 | static final int MAX_CACHED_COLORS_PER_FONT = 1 << 16; 1360 | 1361 | // used only when there is native font 1362 | Font font; 1363 | float ascent; 1364 | float descent; 1365 | 1366 | // used only when there is no native font 1367 | // maps 32-bit color to the arrays of tinted glyph images 1368 | Map tintCache; 1369 | } 1370 | 1371 | 1372 | static final class FontCache { 1373 | static final int MAX_CACHE_SIZE = 512; 1374 | 1375 | // keeps track of filenames of fonts loaded from ttf and otf files 1376 | Map nameToFilename = new HashMap<>(); 1377 | 1378 | // keeps track of fonts which should be rendered as pictures 1379 | // so we don't go through native font search process every time 1380 | final HashSet nonNativeNames = new HashSet<>(); 1381 | 1382 | // keeps all created fonts for reuse up to MAX_CACHE_SIZE limit 1383 | // when the limit is reached, the least recently used font is removed 1384 | // TODO: this should be based on memory consumption 1385 | final LinkedHashMap cache = 1386 | new LinkedHashMap<>(16, 0.75f, true) { 1387 | @Override 1388 | protected boolean removeEldestEntry(Map.Entry eldest) { 1389 | return size() > MAX_CACHE_SIZE; 1390 | } 1391 | }; 1392 | 1393 | // key for retrieving fonts from cache; don't use for insertion, 1394 | // every font has to have its own new Key instance 1395 | final Key retrievingKey = new Key(); 1396 | 1397 | // text node used for measuring sizes of text 1398 | final Text measuringText = new Text(); 1399 | 1400 | FontInfo get(String name, float size) { 1401 | if (nonNativeNames.contains(name)) { 1402 | // Don't have native font, using glyph images. 1403 | // Size is set to zero, because all sizes of this font 1404 | // should share one FontInfo with one tintCache. 1405 | size = 0; 1406 | } 1407 | retrievingKey.name = name; 1408 | retrievingKey.size = size; 1409 | return cache.get(retrievingKey); 1410 | } 1411 | 1412 | void put(String name, float size, FontInfo fontInfo) { 1413 | if (fontInfo.font == null) { 1414 | // Don't have native font, using glyph images. 1415 | // Size is set to zero, because all sizes of this font 1416 | // should share one FontInfo with one tintCache. 1417 | nonNativeNames.add(name); 1418 | size = 0; 1419 | } 1420 | Key key = new Key(); 1421 | key.name = name; 1422 | key.size = size; 1423 | cache.put(key, fontInfo); 1424 | } 1425 | 1426 | FontInfo createFontInfo(Font font) { 1427 | FontInfo result = new FontInfo(); 1428 | result.font = font; 1429 | if (font != null) { 1430 | // measure ascent and descent 1431 | measuringText.setFont(result.font); 1432 | measuringText.setText(" "); 1433 | float lineHeight = (float) measuringText.getLayoutBounds().getHeight(); 1434 | result.ascent = (float) measuringText.getBaselineOffset(); 1435 | result.descent = lineHeight - result.ascent; 1436 | } 1437 | return result; 1438 | } 1439 | 1440 | static final class Key { 1441 | String name; 1442 | float size; 1443 | 1444 | @Override 1445 | public boolean equals(Object o) { 1446 | if (this == o) return true; 1447 | if (o == null || getClass() != o.getClass()) return false; 1448 | Key that = (Key) o; 1449 | if (Float.compare(that.size, size) != 0) return false; 1450 | return name.equals(that.name); 1451 | } 1452 | 1453 | @Override 1454 | public int hashCode() { 1455 | int result = name.hashCode(); 1456 | result = 31 * result + (size != +0.0f ? Float.floatToIntBits(size) : 0); 1457 | return result; 1458 | } 1459 | } 1460 | } 1461 | 1462 | 1463 | /////////////////////////////////////////////////////////////// 1464 | 1465 | // TEXT 1466 | 1467 | // None of the variations of text() are overridden from PGraphics. 1468 | 1469 | 1470 | 1471 | ////////////////////////////////////////////////////////////// 1472 | 1473 | // TEXT IMPL 1474 | 1475 | 1476 | @Override 1477 | protected void textFontImpl(PFont which, float size) { 1478 | handleTextFont(which, size); 1479 | handleTextSize(size); 1480 | } 1481 | 1482 | 1483 | @Override 1484 | protected void textSizeImpl(float size) { 1485 | handleTextFont(textFont, size); 1486 | handleTextSize(size); 1487 | } 1488 | 1489 | 1490 | /** 1491 | * FX specific. When setting font or size, new font has to 1492 | * be created. Both textFontImpl and textSizeImpl call this one. 1493 | * @param which font to be set, not null 1494 | * @param size size to be set, greater than zero 1495 | */ 1496 | protected void handleTextFont(PFont which, float size) { 1497 | textFont = which; 1498 | 1499 | String fontName = which.getName(); 1500 | String fontPsName = which.getPostScriptName(); 1501 | 1502 | textFontInfo = fontCache.get(fontName, size); 1503 | if (textFontInfo == null) { 1504 | Font font = null; 1505 | 1506 | if (which.isStream()) { 1507 | // Load from ttf or otf file 1508 | String filename = fontCache.nameToFilename.get(fontName); 1509 | font = Font.loadFont(parent.createInput(filename), size); 1510 | } 1511 | 1512 | if (font == null) { 1513 | // Look up font name 1514 | font = new Font(fontName, size); 1515 | if (!fontName.equalsIgnoreCase(font.getName())) { 1516 | // Look up font postscript name 1517 | font = new Font(fontPsName, size); 1518 | if (!fontPsName.equalsIgnoreCase(font.getName())) { 1519 | font = null; // Done with it 1520 | } 1521 | } 1522 | } 1523 | 1524 | if (font == null && which.getNative() != null) { 1525 | // Ain't got nothing, but AWT has something, so glyph images are not 1526 | // going to be used for this font; go with the default font then 1527 | font = new Font(size); 1528 | } 1529 | 1530 | textFontInfo = fontCache.createFontInfo(font); 1531 | fontCache.put(fontName, size, textFontInfo); 1532 | } 1533 | 1534 | context.setFont(textFontInfo.font); 1535 | } 1536 | 1537 | 1538 | @Override 1539 | protected void textLineImpl(char[] buffer, int start, int stop, float x, float y) { 1540 | if (textFontInfo.font == null) { 1541 | super.textLineImpl(buffer, start, stop, x, y); 1542 | } else { 1543 | context.fillText(new String(buffer, start, stop - start), x, y); 1544 | } 1545 | } 1546 | 1547 | 1548 | protected PImage getTintedGlyphImage(PFont.Glyph glyph, int tintColor) { 1549 | if (textFontInfo.tintCache == null) { 1550 | textFontInfo.tintCache = new LinkedHashMap<>(16, 0.75f, true) { 1551 | @Override 1552 | protected boolean removeEldestEntry(Map.Entry eldest) { 1553 | return size() > FontInfo.MAX_CACHED_COLORS_PER_FONT; 1554 | } 1555 | }; 1556 | } 1557 | PImage[] tintedGlyphs = textFontInfo.tintCache.get(tintColor); 1558 | int index = glyph.index; 1559 | if (tintedGlyphs == null || tintedGlyphs.length <= index) { 1560 | PImage[] newArray = new PImage[textFont.getGlyphCount()]; 1561 | if (tintedGlyphs != null) { 1562 | System.arraycopy(tintedGlyphs, 0, newArray, 0, tintedGlyphs.length); 1563 | } 1564 | tintedGlyphs = newArray; 1565 | textFontInfo.tintCache.put(tintColor, tintedGlyphs); 1566 | } 1567 | PImage tintedGlyph = tintedGlyphs[index]; 1568 | if (tintedGlyph == null) { 1569 | tintedGlyph = glyph.image.copy(); 1570 | tintedGlyphs[index] = tintedGlyph; 1571 | } 1572 | return tintedGlyph; 1573 | } 1574 | 1575 | 1576 | @Override 1577 | protected void textCharImpl(char ch, float x, float y) { //, float z) { 1578 | PFont.Glyph glyph = textFont.getGlyph(ch); 1579 | if (glyph != null) { 1580 | if (textMode == MODEL) { 1581 | float bitmapSize = textFont.getSize(); 1582 | float high = glyph.height / bitmapSize; 1583 | float wide = glyph.width / bitmapSize; 1584 | float leftExtent = glyph.leftExtent / bitmapSize; 1585 | float topExtent = glyph.topExtent / bitmapSize; 1586 | 1587 | float x1 = x + leftExtent * textSize; 1588 | float y1 = y - topExtent * textSize; 1589 | float x2 = x1 + wide * textSize; 1590 | float y2 = y1 + high * textSize; 1591 | 1592 | PImage glyphImage = (fillColor == 0xFFFFFFFF) ? 1593 | glyph.image : getTintedGlyphImage(glyph, fillColor); 1594 | 1595 | textCharModelImpl(glyphImage, 1596 | x1, y1, x2, y2, 1597 | glyph.width, glyph.height); 1598 | } 1599 | } else if (ch != ' ' && ch != 127) { 1600 | showWarning("No glyph found for the " + ch + 1601 | " (\\u" + PApplet.hex(ch, 4) + ") character"); 1602 | } 1603 | } 1604 | 1605 | 1606 | @Override 1607 | protected float textWidthImpl(char[] buffer, int start, int stop) { 1608 | if (textFont == null) { 1609 | defaultFontOrDeath("textWidth"); 1610 | } 1611 | 1612 | if (textFontInfo.font == null) { 1613 | return super.textWidthImpl(buffer, start, stop); 1614 | } 1615 | 1616 | fontCache.measuringText.setFont(textFontInfo.font); 1617 | fontCache.measuringText.setText(new String(buffer, start, stop - start)); 1618 | return (float) fontCache.measuringText.getLayoutBounds().getWidth(); 1619 | } 1620 | 1621 | 1622 | 1623 | ////////////////////////////////////////////////////////////// 1624 | 1625 | // MATRIX STACK 1626 | 1627 | 1628 | @Override 1629 | public void pushMatrix() { 1630 | if (transformCount == transformStack.length) { 1631 | throw new RuntimeException("pushMatrix() cannot use push more than " + 1632 | transformStack.length + " times"); 1633 | } 1634 | transformStack[transformCount] = context.getTransform(transformStack[transformCount]); 1635 | transformCount++; 1636 | } 1637 | 1638 | 1639 | @Override 1640 | public void popMatrix() { 1641 | if (transformCount == 0) { 1642 | throw new RuntimeException("missing a pushMatrix() " + 1643 | "to go with that popMatrix()"); 1644 | } 1645 | transformCount--; 1646 | context.setTransform(transformStack[transformCount]); 1647 | } 1648 | 1649 | 1650 | 1651 | ////////////////////////////////////////////////////////////// 1652 | 1653 | // MATRIX TRANSFORMS 1654 | 1655 | 1656 | @Override 1657 | public void translate(float tx, float ty) { 1658 | context.translate(tx, ty); 1659 | } 1660 | 1661 | 1662 | //public void translate(float tx, float ty, float tz) 1663 | 1664 | 1665 | @Override 1666 | public void rotate(float angle) { 1667 | context.rotate(PApplet.degrees(angle)); 1668 | } 1669 | 1670 | 1671 | @Override 1672 | public void rotateX(float angle) { 1673 | showDepthWarning("rotateX"); 1674 | } 1675 | 1676 | 1677 | @Override 1678 | public void rotateY(float angle) { 1679 | showDepthWarning("rotateY"); 1680 | } 1681 | 1682 | 1683 | @Override 1684 | public void rotateZ(float angle) { 1685 | showDepthWarning("rotateZ"); 1686 | } 1687 | 1688 | 1689 | @Override 1690 | public void rotate(float angle, float vx, float vy, float vz) { 1691 | showVariationWarning("rotate"); 1692 | } 1693 | 1694 | 1695 | @Override 1696 | public void scale(float s) { 1697 | context.scale(s, s); 1698 | } 1699 | 1700 | 1701 | @Override 1702 | public void scale(float sx, float sy) { 1703 | context.scale(sx, sy); 1704 | } 1705 | 1706 | 1707 | @Override 1708 | public void scale(float sx, float sy, float sz) { 1709 | showDepthWarningXYZ("scale"); 1710 | } 1711 | 1712 | 1713 | @Override 1714 | public void shearX(float angle) { 1715 | Affine temp = new Affine(); 1716 | temp.appendShear(Math.tan(angle), 0); 1717 | context.transform(temp); 1718 | } 1719 | 1720 | 1721 | @Override 1722 | public void shearY(float angle) { 1723 | Affine temp = new Affine(); 1724 | temp.appendShear(0, Math.tan(angle)); 1725 | context.transform(temp); 1726 | } 1727 | 1728 | 1729 | 1730 | ////////////////////////////////////////////////////////////// 1731 | 1732 | // MATRIX MORE 1733 | 1734 | 1735 | @Override 1736 | public void resetMatrix() { 1737 | context.setTransform(new Affine()); 1738 | } 1739 | 1740 | 1741 | //public void applyMatrix(PMatrix2D source) 1742 | 1743 | 1744 | @Override 1745 | public void applyMatrix(float n00, float n01, float n02, 1746 | float n10, float n11, float n12) { 1747 | context.transform(n00, n10, n01, n11, n02, n12); 1748 | } 1749 | 1750 | 1751 | //public void applyMatrix(PMatrix3D source) 1752 | 1753 | 1754 | @Override 1755 | public void applyMatrix(float n00, float n01, float n02, float n03, 1756 | float n10, float n11, float n12, float n13, 1757 | float n20, float n21, float n22, float n23, 1758 | float n30, float n31, float n32, float n33) { 1759 | showVariationWarning("applyMatrix"); 1760 | } 1761 | 1762 | 1763 | 1764 | ////////////////////////////////////////////////////////////// 1765 | 1766 | // MATRIX GET/SET 1767 | 1768 | 1769 | @Override 1770 | public PMatrix getMatrix() { 1771 | return getMatrix((PMatrix2D) null); 1772 | } 1773 | 1774 | 1775 | @Override 1776 | public PMatrix2D getMatrix(PMatrix2D target) { 1777 | if (target == null) { 1778 | target = new PMatrix2D(); 1779 | } 1780 | //double[] transform = new double[6]; 1781 | // TODO This is not tested; apparently Affine is a full 3x4 1782 | Affine t = context.getTransform(); //.getMatrix(transform); 1783 | // target.set((float) transform[0], (float) transform[2], (float) transform[4], 1784 | // (float) transform[1], (float) transform[3], (float) transform[5]); 1785 | target.set((float) t.getMxx(), (float) t.getMxy(), (float) t.getTx(), 1786 | (float) t.getMyx(), (float) t.getMyy(), (float) t.getTy()); 1787 | return target; 1788 | } 1789 | 1790 | 1791 | @Override 1792 | public PMatrix3D getMatrix(PMatrix3D target) { 1793 | showVariationWarning("getMatrix"); 1794 | return target; 1795 | } 1796 | 1797 | 1798 | //public void setMatrix(PMatrix source) 1799 | 1800 | 1801 | @Override 1802 | public void setMatrix(PMatrix2D source) { 1803 | context.setTransform(source.m00, source.m10, 1804 | source.m01, source.m11, 1805 | source.m02, source.m12); 1806 | } 1807 | 1808 | 1809 | @Override 1810 | public void setMatrix(PMatrix3D source) { 1811 | showVariationWarning("setMatrix"); 1812 | } 1813 | 1814 | 1815 | @Override 1816 | public void printMatrix() { 1817 | getMatrix((PMatrix2D) null).print(); 1818 | } 1819 | 1820 | 1821 | 1822 | // ////////////////////////////////////////////////////////////// 1823 | // 1824 | // // CAMERA and PROJECTION 1825 | // 1826 | // // Inherit the plaintive warnings from PGraphics 1827 | // 1828 | // 1829 | // //public void beginCamera() 1830 | // //public void endCamera() 1831 | // //public void camera() 1832 | // //public void camera(float eyeX, float eyeY, float eyeZ, 1833 | // // float centerX, float centerY, float centerZ, 1834 | // // float upX, float upY, float upZ) 1835 | // //public void printCamera() 1836 | // 1837 | // //public void ortho() 1838 | // //public void ortho(float left, float right, 1839 | // // float bottom, float top, 1840 | // // float near, float far) 1841 | // //public void perspective() 1842 | // //public void perspective(float fov, float aspect, float near, float far) 1843 | // //public void frustum(float left, float right, 1844 | // // float bottom, float top, 1845 | // // float near, float far) 1846 | // //public void printProjection() 1847 | 1848 | 1849 | 1850 | ////////////////////////////////////////////////////////////// 1851 | 1852 | // SCREEN and MODEL transforms 1853 | 1854 | 1855 | @Override 1856 | public float screenX(float x, float y) { 1857 | return (float) context.getTransform().transform(x, y).getX(); 1858 | } 1859 | 1860 | 1861 | @Override 1862 | public float screenY(float x, float y) { 1863 | return (float) context.getTransform().transform(x, y).getY(); 1864 | } 1865 | 1866 | 1867 | @Override 1868 | public float screenX(float x, float y, float z) { 1869 | showDepthWarningXYZ("screenX"); 1870 | return 0; 1871 | } 1872 | 1873 | 1874 | @Override 1875 | public float screenY(float x, float y, float z) { 1876 | showDepthWarningXYZ("screenY"); 1877 | return 0; 1878 | } 1879 | 1880 | 1881 | @Override 1882 | public float screenZ(float x, float y, float z) { 1883 | showDepthWarningXYZ("screenZ"); 1884 | return 0; 1885 | } 1886 | 1887 | 1888 | //public float modelX(float x, float y, float z) 1889 | 1890 | 1891 | //public float modelY(float x, float y, float z) 1892 | 1893 | 1894 | //public float modelZ(float x, float y, float z) 1895 | 1896 | 1897 | 1898 | // ////////////////////////////////////////////////////////////// 1899 | // 1900 | // // STYLE 1901 | // 1902 | // // pushStyle(), popStyle(), style() and getStyle() inherited. 1903 | 1904 | 1905 | 1906 | ////////////////////////////////////////////////////////////// 1907 | 1908 | // STROKE CAP/JOIN/WEIGHT 1909 | 1910 | 1911 | @Override 1912 | public void strokeCap(int cap) { 1913 | super.strokeCap(cap); 1914 | if (strokeCap == ROUND) { 1915 | context.setLineCap(StrokeLineCap.ROUND); 1916 | } else if (strokeCap == PROJECT) { 1917 | context.setLineCap(StrokeLineCap.SQUARE); 1918 | } else { 1919 | context.setLineCap(StrokeLineCap.BUTT); 1920 | } 1921 | } 1922 | 1923 | 1924 | @Override 1925 | public void strokeJoin(int join) { 1926 | super.strokeJoin(join); 1927 | if (strokeJoin == MITER) { 1928 | context.setLineJoin(StrokeLineJoin.MITER); 1929 | } else if (strokeJoin == ROUND) { 1930 | context.setLineJoin(StrokeLineJoin.ROUND); 1931 | } else { 1932 | context.setLineJoin(StrokeLineJoin.BEVEL); 1933 | } 1934 | } 1935 | 1936 | 1937 | @Override 1938 | public void strokeWeight(float weight) { 1939 | super.strokeWeight(weight); 1940 | context.setLineWidth(weight); 1941 | } 1942 | 1943 | 1944 | 1945 | ////////////////////////////////////////////////////////////// 1946 | 1947 | // STROKE 1948 | 1949 | // noStroke() and stroke() inherited from PGraphics. 1950 | 1951 | 1952 | @Override 1953 | protected void strokeFromCalc() { 1954 | super.strokeFromCalc(); 1955 | context.setStroke(new Color(strokeR, strokeG, strokeB, strokeA)); 1956 | } 1957 | 1958 | 1959 | protected boolean drawingThinLines() { 1960 | // align strokes to pixel centers when drawing thin lines 1961 | return stroke && strokeWeight == 1; 1962 | } 1963 | 1964 | 1965 | 1966 | ////////////////////////////////////////////////////////////// 1967 | 1968 | // TINT 1969 | 1970 | // noTint() and tint() inherited from PGraphics. 1971 | 1972 | 1973 | 1974 | ////////////////////////////////////////////////////////////// 1975 | 1976 | // FILL 1977 | 1978 | // noFill() and fill() inherited from PGraphics. 1979 | 1980 | 1981 | @Override 1982 | protected void fillFromCalc() { 1983 | super.fillFromCalc(); 1984 | context.setFill(new Color(fillR, fillG, fillB, fillA)); 1985 | } 1986 | 1987 | 1988 | 1989 | // ////////////////////////////////////////////////////////////// 1990 | // 1991 | // // MATERIAL PROPERTIES 1992 | // 1993 | // 1994 | // //public void ambient(int rgb) 1995 | // //public void ambient(float gray) 1996 | // //public void ambient(float x, float y, float z) 1997 | // //protected void ambientFromCalc() 1998 | // //public void specular(int rgb) 1999 | // //public void specular(float gray) 2000 | // //public void specular(float x, float y, float z) 2001 | // //protected void specularFromCalc() 2002 | // //public void shininess(float shine) 2003 | // //public void emissive(int rgb) 2004 | // //public void emissive(float gray) 2005 | // //public void emissive(float x, float y, float z ) 2006 | // //protected void emissiveFromCalc() 2007 | // 2008 | // 2009 | // 2010 | // ////////////////////////////////////////////////////////////// 2011 | // 2012 | // // LIGHTS 2013 | // 2014 | // 2015 | // //public void lights() 2016 | // //public void noLights() 2017 | // //public void ambientLight(float red, float green, float blue) 2018 | // //public void ambientLight(float red, float green, float blue, 2019 | // // float x, float y, float z) 2020 | // //public void directionalLight(float red, float green, float blue, 2021 | // // float nx, float ny, float nz) 2022 | // //public void pointLight(float red, float green, float blue, 2023 | // // float x, float y, float z) 2024 | // //public void spotLight(float red, float green, float blue, 2025 | // // float x, float y, float z, 2026 | // // float nx, float ny, float nz, 2027 | // // float angle, float concentration) 2028 | // //public void lightFalloff(float constant, float linear, float quadratic) 2029 | // //public void lightSpecular(float x, float y, float z) 2030 | // //protected void lightPosition(int num, float x, float y, float z) 2031 | // //protected void lightDirection(int num, float x, float y, float z) 2032 | 2033 | 2034 | 2035 | ////////////////////////////////////////////////////////////// 2036 | 2037 | // BACKGROUND 2038 | 2039 | 2040 | @Override 2041 | public void backgroundImpl() { 2042 | 2043 | // if pixels are modified, we don't flush them (just mark them flushed) 2044 | // because they would be immediately overwritten by the background anyway 2045 | modified = false; 2046 | loaded = false; 2047 | 2048 | // Save drawing context (transform, fill, blend mode, etc.) 2049 | context.save(); 2050 | 2051 | // Reset transform to identity 2052 | context.setTransform(new Affine()); 2053 | 2054 | // This only takes into account cases where this is the primary surface. 2055 | // Not sure what we do with offscreen anyway. 2056 | context.setFill(new Color(backgroundR, backgroundG, backgroundB, backgroundA)); 2057 | context.setGlobalBlendMode(BlendMode.SRC_OVER); 2058 | context.fillRect(0, 0, width, height); 2059 | 2060 | // Restore drawing context (transform, fill, blend mode, etc.) 2061 | context.restore(); 2062 | } 2063 | 2064 | 2065 | 2066 | // ////////////////////////////////////////////////////////////// 2067 | // 2068 | // // COLOR MODE 2069 | // 2070 | // // All colorMode() variations are inherited from PGraphics. 2071 | // 2072 | // 2073 | // 2074 | // ////////////////////////////////////////////////////////////// 2075 | // 2076 | // // COLOR CALC 2077 | // 2078 | // // colorCalc() and colorCalcARGB() inherited from PGraphics. 2079 | // 2080 | // 2081 | // 2082 | // ////////////////////////////////////////////////////////////// 2083 | // 2084 | // // COLOR DATATYPE STUFFING 2085 | // 2086 | // // final color() variations inherited. 2087 | // 2088 | // 2089 | // 2090 | // ////////////////////////////////////////////////////////////// 2091 | // 2092 | // // COLOR DATATYPE EXTRACTION 2093 | // 2094 | // // final methods alpha, red, green, blue, 2095 | // // hue, saturation, and brightness all inherited. 2096 | // 2097 | // 2098 | // 2099 | // ////////////////////////////////////////////////////////////// 2100 | // 2101 | // // COLOR DATATYPE INTERPOLATION 2102 | // 2103 | // // both lerpColor variants inherited. 2104 | // 2105 | // 2106 | // 2107 | // ////////////////////////////////////////////////////////////// 2108 | // 2109 | // // BEGIN/END RAW 2110 | // 2111 | // 2112 | // @Override 2113 | // public void beginRaw(PGraphics recorderRaw) { 2114 | // showMethodWarning("beginRaw"); 2115 | // } 2116 | // 2117 | // 2118 | // @Override 2119 | // public void endRaw() { 2120 | // showMethodWarning("endRaw"); 2121 | // } 2122 | // 2123 | // 2124 | // 2125 | // ////////////////////////////////////////////////////////////// 2126 | // 2127 | // // WARNINGS and EXCEPTIONS 2128 | // 2129 | // // showWarning and showException inherited. 2130 | // 2131 | // 2132 | // 2133 | // ////////////////////////////////////////////////////////////// 2134 | // 2135 | // // RENDERER SUPPORT QUERIES 2136 | // 2137 | // 2138 | // //public boolean displayable() // true 2139 | // 2140 | // 2141 | // //public boolean is2D() // true 2142 | // 2143 | // 2144 | // //public boolean is3D() // false 2145 | 2146 | 2147 | 2148 | ////////////////////////////////////////////////////////////// 2149 | 2150 | // PIMAGE METHODS 2151 | 2152 | 2153 | @Override 2154 | public void loadPixels() { 2155 | if ((pixels == null) || (pixels.length != pixelWidth * pixelHeight)) { 2156 | pixels = new int[pixelWidth * pixelHeight]; 2157 | loaded = false; 2158 | } 2159 | 2160 | if (!loaded) { 2161 | if (snapshotImage == null || 2162 | snapshotImage.getWidth() != pixelWidth || 2163 | snapshotImage.getHeight() != pixelHeight) { 2164 | snapshotImage = new WritableImage(pixelWidth, pixelHeight); 2165 | } 2166 | 2167 | SnapshotParameters sp = new SnapshotParameters(); 2168 | if (!primaryGraphics) { 2169 | // Alpha channel should not be made white (but only if offscreen) 2170 | sp.setFill(Color.TRANSPARENT); 2171 | } 2172 | if (pixelDensity != 1) { 2173 | sp.setTransform(Transform.scale(pixelDensity, pixelDensity)); 2174 | } 2175 | snapshotImage = context.getCanvas().snapshot(sp, snapshotImage); 2176 | PixelReader pr = snapshotImage.getPixelReader(); 2177 | pr.getPixels(0, 0, pixelWidth, pixelHeight, argbFormat, pixels, 0, pixelWidth); 2178 | 2179 | loaded = true; 2180 | modified = false; 2181 | } 2182 | } 2183 | 2184 | 2185 | 2186 | ////////////////////////////////////////////////////////////// 2187 | 2188 | // GET/SET PIXELS 2189 | 2190 | 2191 | @Override 2192 | public int get(int x, int y) { 2193 | loadPixels(); 2194 | return super.get(x, y); 2195 | } 2196 | 2197 | 2198 | @Override 2199 | protected void getImpl(int sourceX, int sourceY, 2200 | int sourceWidth, int sourceHeight, 2201 | PImage target, int targetX, int targetY) { 2202 | loadPixels(); 2203 | super.getImpl(sourceX, sourceY, sourceWidth, sourceHeight, 2204 | target, targetX, targetY); 2205 | } 2206 | 2207 | 2208 | @Override 2209 | public void set(int x, int y, int argb) { 2210 | loadPixels(); 2211 | super.set(x, y, argb); 2212 | } 2213 | 2214 | 2215 | @Override 2216 | protected void setImpl(PImage sourceImage, 2217 | int sourceX, int sourceY, 2218 | int sourceWidth, int sourceHeight, 2219 | int targetX, int targetY) { 2220 | sourceImage.loadPixels(); 2221 | 2222 | int sourceOffset = sourceX + sourceImage.pixelWidth * sourceY; 2223 | 2224 | PixelWriter pw = context.getPixelWriter(); 2225 | pw.setPixels(targetX, targetY, sourceWidth, sourceHeight, 2226 | argbFormat, 2227 | sourceImage.pixels, 2228 | sourceOffset, 2229 | sourceImage.pixelWidth); 2230 | 2231 | // Let's keep them loaded 2232 | if (loaded) { 2233 | int sourceStride = sourceImage.pixelWidth; 2234 | int targetStride = pixelWidth; 2235 | int targetOffset = targetX + targetY * targetStride; 2236 | for (int i = 0; i < sourceHeight; i++) { 2237 | System.arraycopy(sourceImage.pixels, sourceOffset + i * sourceStride, 2238 | pixels, targetOffset + i * targetStride, sourceWidth); 2239 | } 2240 | } 2241 | } 2242 | 2243 | 2244 | ////////////////////////////////////////////////////////////// 2245 | 2246 | // MASK 2247 | 2248 | 2249 | static final String MASK_WARNING = 2250 | "mask() cannot be used on the main drawing surface"; 2251 | 2252 | 2253 | @Override 2254 | public void mask(PImage alpha) { 2255 | showWarning(MASK_WARNING); 2256 | } 2257 | 2258 | 2259 | 2260 | ////////////////////////////////////////////////////////////// 2261 | 2262 | // FILTER 2263 | 2264 | // Because the PImage versions call loadPixels() and 2265 | // updatePixels(), no need to override anything here. 2266 | 2267 | 2268 | //public void filter(int kind) 2269 | 2270 | 2271 | //public void filter(int kind, float param) 2272 | 2273 | 2274 | 2275 | ////////////////////////////////////////////////////////////// 2276 | 2277 | // COPY 2278 | 2279 | 2280 | // @Override 2281 | // public void copy(int sx, int sy, int sw, int sh, 2282 | // int dx, int dy, int dw, int dh) { 2283 | // if ((sw != dw) || (sh != dh)) { 2284 | // g2.drawImage(image, dx, dy, dx + dw, dy + dh, sx, sy, sx + sw, sy + sh, null); 2285 | // 2286 | // } else { 2287 | // dx = dx - sx; // java2d's "dx" is the delta, not dest 2288 | // dy = dy - sy; 2289 | // g2.copyArea(sx, sy, sw, sh, dx, dy); 2290 | // } 2291 | // } 2292 | 2293 | 2294 | // @Override 2295 | // public void copy(PImage src, 2296 | // int sx, int sy, int sw, int sh, 2297 | // int dx, int dy, int dw, int dh) { 2298 | // g2.drawImage((Image) src.getNative(), 2299 | // dx, dy, dx + dw, dy + dh, 2300 | // sx, sy, sx + sw, sy + sh, null); 2301 | // } 2302 | 2303 | 2304 | 2305 | ////////////////////////////////////////////////////////////// 2306 | 2307 | // BLEND 2308 | 2309 | 2310 | //static public int blendColor(int c1, int c2, int mode) 2311 | 2312 | 2313 | //public void blend(int sx, int sy, int sw, int sh, 2314 | // int dx, int dy, int dw, int dh, int mode) 2315 | 2316 | 2317 | //public void blend(PImage src, 2318 | // int sx, int sy, int sw, int sh, 2319 | // int dx, int dy, int dw, int dh, int mode) 2320 | 2321 | 2322 | 2323 | ////////////////////////////////////////////////////////////// 2324 | 2325 | // SAVE 2326 | 2327 | 2328 | //public void save(String filename) 2329 | 2330 | 2331 | 2332 | ////////////////////////////////////////////////////////////// 2333 | 2334 | /** 2335 | * Display a warning that the specified method is simply unavailable. 2336 | */ 2337 | static public void showTodoWarning(String method, int issue) { 2338 | showWarning(method + "() is not yet available: " + 2339 | "https://github.com/processing/processing/issues/" + issue); 2340 | } 2341 | } 2342 | --------------------------------------------------------------------------------