/* Copyright 2010 Aaron J. Radke Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cc.drx.p5 //import java.util.Date //import scala.collection.JavaConverters._ import scala.language.implicitConversions import scala.collection.concurrent.TrieMap //import scala.util.{Either,Left,Right} import processing.core._ import processing.core.PConstants._ import processing.core.PApplet._ import cc.drx._ import java.awt.event._ //import java.awt.event.WindowStateListener //import java.awt.event.WindowEvent import java.awt.Frame /* TODO: describe p5(data driven drawing) and the similarities but difference between p5(data driven documents) d3: scales , data merge animations underscoreio/doodle: shape composition: beside, above, below node2: data driven, cross platform, realtime, data adjust processing: low barrier to entry (single file app, no boilerplate code), draw loop, mouse interaction contextFree, structureSynth: simple method modifications rotate translate color : rx t c */ case class RenderMode(className:String) extends AnyVal object RenderMode{ //def launch = TODO make a launcher?? def find(name:String):Option[RenderMode] = list.find(_.className endsWith name) lazy val list:List[RenderMode] = List(JAVA2D, FX2D, P2D, P3D, PDF) lazy val JAVA2D = RenderMode(PConstants.JAVA2D) //canvas lazy val FX2D = RenderMode(PConstants.FX2D) //javaFX lazy val P2D = RenderMode(PConstants.P2D) //2d opengl lazy val P3D = RenderMode(PConstants.P3D) //3d opengl lazy val PDF = RenderMode(PConstants.PDF) //requires itext?? } trait P5App{ private def p5className:String = this.getClass.getName.dropRight(1) def launch:Unit = P5.launch(Array(p5className)) def launch(args:Array[String]):Unit = P5.launch(p5className +: args) def main(args:Array[String]) = launch(args) } object P5{ def launch[A <: PApplet](klass:Class[A]):Unit = PApplet.main(Array(klass.getName)) //TODO add a feature so the classOf is not required def launch(args:Array[String]):Unit = PApplet.main(args) @deprecated("use RenderMode instead","v0.2.14") val renderClasses:Set[String] = Set(JAVA2D, P2D, P3D, FX2D) @deprecated("use RenderMode instead","v0.2.14") def renderClass(k:Symbol):String = renderClasses.find(_ contains k.name.toUpperCase) getOrElse JAVA2D } @deprecated("use some version of mouseDrag and scale","v0.1.15") class CanvasZoom{ var poffset = Vec(0,0) var offset = Vec(0,0) var scale = 1.0 def zoom(fraction:Double,center:Vec=Vec(0,0)) = { val tmp = scale - scale*fraction val new_scale = if(tmp <= 0.0) 0.01 else tmp val pc = (center - offset)/scale offset = center - (pc*new_scale) scale = new_scale } def initMove = poffset = offset def move(relative:Vec) = offset = poffset + relative def draw(drawFunc:PGraphics=>Unit)(implicit g:PGraphics):Unit = { g.pushMatrix g.scale(scale) g.translate(offset.x/scale, offset.y/scale) drawFunc(g) g.popMatrix } def draw(drawFunc: =>Unit)(implicit ctx:PApplet):Unit = { ctx.pushMatrix ctx.scale(scale) ctx.translate(offset.x/scale, offset.y/scale) drawFunc ctx.popMatrix } } trait FullScreen extends P5{ override def settings = { fullScreen(renderMode.className) } } class P5 extends PApplet{ import P5._ import Color._ //--wip external launcher frame's /**Interfacing with java (this will make a jframe) */ lazy val getFrame:javax.swing.JFrame = { import javax.swing.JFrame val frame = new JFrame(this.getClass.getSimpleName) frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //TODO get size from the jframe instead // val h = frame.getSize.getHeight // val w = frame.getSize.getWidgth //--add the native surface to the frame frame.add(getCanvasAWT) //--adjust frame size frame.setSize(sketchSize.x.toInt, sketchSize.y.toInt) frame.setResizable(true) frame.setVisible(true) //TODO add boolean argument to prevent thread startup //--TODO add resize listeners... frame } private lazy val getStartedSurface:processing.core.PSurface = { val ps:processing.core.PSurface = if(surface == null) this.initSurface() else surface //TODO make sure this null check is really the right way to test this, it is required if something else like the applet already made an initSurface ps.setResizable(true) ps.setSize(sketchSize.x.toInt, sketchSize.y.toInt) ps.startThread()//the animation thread //TODO add boolean argument to prevent thread startup ps //return the that has already been started surface } /**Interfacing with javafx (this will also spin up the animation threads) * https://docs.oracle.com/javase/8/javafx/api/javafx/scene/canvas/Canvas.html * https://github.com/processing/processing/blob/master/core/src/processing/javafx/PSurfaceFX.java * */ private def requireJAVA2D = require(renderMode == RenderMode.JAVA2D,s"returning a canvas requires the `override renderMode == JAVA2D`, it is currently set to `$renderMode`") private def requireFX = require(renderMode == RenderMode.FX2D,s"returning a canvas requires the `override renderMode == FX2D`, it is currently set to `$renderMode`") lazy val getCanvasFX:javafx.scene.canvas.Canvas = { requireFX; getStartedSurface.getNative.asInstanceOf[javafx.scene.canvas.Canvas] } /**this stage will make sure the fx app will close on window events*/ private lazy val getStageFXWithoutShow:javafx.stage.Stage = { requireFX val jbSurface = JailBreak(getStartedSurface, classOf[processing.javafx.PSurfaceFX]) val stage = jbSurface[javafx.stage.Stage]("stage") //get the private stage value from the psurface object getCanvasFX.requestFocus() //this is the necessary one to make the keyEvents start working in the embeded FX app, there are lots of places this works, but it seems all stages should have and if it works outside of a runLater then it shoudl be put there as it does //--stop the fx app on close stage.setOnCloseRequest(new javafx.event.EventHandler[javafx.stage.WindowEvent]{ override def handle(e:javafx.stage.WindowEvent) = { javafx.application.Platform.exit() System.exit(0) } }) stage } /**this stage will also show the stage in a Platform runLater thread, if you dont' want to auto show it with runLater use getSTageFXWithoutShow*/ lazy val getStageFX:javafx.stage.Stage = { requireFX val stage = getStageFXWithoutShow //--show the stage //TODO maybe don't do this yet to let other apps make modifications first or add a boolean //TODO the mouse, wheel events work, but for some reason the keyboard events are not working ... the PApplet.launch seems to work though with JAVAFX so somethign with this getStageFX method javafx.application.Platform.runLater(new Runnable(){def run = { stage.show() //Note this crashes with a "Not on FX application thread" unless it lives inside the runLater runable }}) stage //return the fx stage } @deprecated("use the more specific getCanvasAWT instead", "v0.2.14") lazy val getCanvas:java.awt.Canvas = getCanvasAWT /**Interfacing with java (this will also spin up the animation threads) */ lazy val getCanvasAWT:java.awt.Canvas = { //requireJAVA2D //TODO require the check for JAVA2D if no other renders also use the java.awt.Canvas val canvas = getStartedSurface.getNative.asInstanceOf[processing.awt.PSurfaceAWT$SmoothCanvas] //java2d AWT? canvas.addComponentListener( new java.awt.event.ComponentAdapter{//ComponentListener{ override def componentResized(e:java.awt.event.ComponentEvent):Unit = { // println(s"event: $e") for(c <- Option(e.getComponent)) { val h = c.getSize.getHeight.toInt val w = c.getSize.getWidth.toInt // println(s"Resized native canvas to: $w x $h") surface.setSize(w,h) redraw() } } } ) canvas } def sketchSize = Vec(1280,720) def renderMode:RenderMode = RenderMode.JAVA2D //TODO replace all renderClass settings with the renderMode /**note this is important that this renderclass is a def so that a val is not loaded as null in the JailBreak override of PApplet.renderer */ @deprecated("use RenderMode instead","v0.2.14") def renderClass:String = JAVA2D //FX2D // P2D // P3D // OPENGL // JAVA2D // FX2D //--WARNING the following is an initialization hack to change the default renderer //this is used in an embedded app since non-launched frames may not use the PApplet.size(_,_,render) setting to set the renderer private val jailBreak = JailBreak(this,classOf[processing.core.PApplet]) jailBreak("renderer") = renderMode.className // println("proccessing app found render:" + jailBreak[String]("renderer") + " from renderClass:"+renderClass) override def settings = { size(sketchSize.x.toInt, sketchSize.y.toInt, renderMode.className) //fullScreen(renderMode.className) smooth(4) pixelDensity(displayDensity) } override def setup() = { surface.setResizable(true) frameRate(60)//limit the framerate, no need to over do things Draw.defaults(g) //background(White) hint(ENABLE_KEY_REPEAT) Option(java.awt.SplashScreen.getSplashScreen) foreach {_.close} } implicit lazy val keyboard = renderMode match { case RenderMode.FX2D => Keyboard.Java2d //TODO make sure this works right case RenderMode.JAVA2D => Keyboard.Java2d case _ => Keyboard.Jogl } // @deprecated("use keyboard.count(up,down) instead", "v0.2.13") //TODO def keyCount(s:String) = keyboard.count(s(0).toString, s(1).toString) // @deprecated("use keyboard.count(up) instead", "v0.2.13") //TODO def keyCount(c:Char) = keyboard.count(c.toString) // @deprecated("use keyboard("alt-c") instead", "v0.2.13") //TODO // def isCoded(c:Char):Boolean = keyboard.keyState.codes contains Keyboard.KeyCode(c.toInt) private var _wheelCount:Int = 0 def wheelCount:Int = _wheelCount override def mouseWheel(e:processing.event.MouseEvent) = _wheelCount += e.getCount() /**this should be called at the end of an overridden keyReleasedd*/ override def keyReleased() = { if(keyboard("f12") || keyboard("print")) save(s"export/screen-${Date.now.krypton take 6}.png") // _savePng = true keyboard -= Keyboard.KeyCode(if (key==CODED) -keyCode else keyCode) if(keyboard("shift-f12") || keyboard("shift-print")) OS.browse(f"export") // open the export directory //if(key == CODED && keyCode == 154 /*PrtScn*/) //if(key == 'q') {stop; dispose; System.exit(0)} //if(coded(ALT) && key == 'q'){ stop; dispose; System.exit(0)} } def logKeyEvent(root:String="KeyEvent") = println(s"$root key:'$key' key.toInt:${key.toInt} keyCode:$keyCode coded:${key == CODED}") override def keyPressed() = { keyboard += Keyboard.KeyCode(if (key==CODED) -keyCode else keyCode) if(key == ESC) key = 0 //hack to stop processing from quiting the application on ESC } /**clear UI state like the keyHeld state, keyCount, WheelCount*/ def clearState() = { keyboard.clear() _wheelCount = 0 //TODO roll in wheel count ot the Keyboard state mouseDrag = None } override def clear() = { super.clear() clearState() } /**defines the line that the current mouseDrag has drawn, returns None if no mouse drag*/ var mouseDrag:Option[Line] = None //TODO make the var private /**defines the press of an event, this value is setup at the begining of a draw loop and cleared at the end*/ var mousePress:Option[Vec] = None //TODO make the var private override def mouseDragged = { mouseDrag = mouseDrag match { case Some(Line(start,_)) => Some(Line(start,mouse)) case None => Some(Line(mouse,mouse)) } } override def mouseReleased = { super.mouseReleased mouseDrag = None mousePress = None } override def mousePressed = { super.mousePressed() //the () are required to differentiate between the callback and the variable name mousePress = Some(mouse) } //---mouse functions as vectors def mouse = Vec(mouseX,mouseY) def pmouse = Vec(pmouseX,pmouseY) def size = Vec(width,height) def screen = Rect(Vec(0,0),size) //def center = size/2 //def gmouse = frame.loc + mouse @deprecated("just use surface.setTitle since procesing doesn't always use frames or overload the resource /icon/icon-{sz}.png files", "0.1.15") def setAppIcon(title:String,img:PImage,iconSize:Int=64) = { //val pg = draw(iconSize,iconSize){_.image(img,0,0,iconSize,iconSize)} //frame.setIconImage(pg.image) surface.setTitle(title) } //---new draw factories @deprecated("Use implicit RichPGraphics.draw{drawFunc} instead","v0.2.0") def draw(drawFunc: => Unit)(implicit g:PGraphics):PGraphics= { draw{g => drawFunc} } @deprecated("This makes it far to easy to make expensive graphics buffers use createGraphics(w,h) instead","v0.2.0") def draw(drawFunc:PGraphics => Unit)(implicit g:PGraphics):PGraphics= { g.beginDraw g.pushMatrix drawFunc(g) g.popMatrix g.endDraw g } override def createGraphics(w:Int,h:Int):PGraphics = Draw.defaults(super.createGraphics(w,h)) def launch:Unit = PApplet.main(Array(this.getClass.getName)) }