/* 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 import scala.collection.JavaConverters._ // object Sketch{ // def apply(gf:DrawContext => Unit) = new Sketch(gf) // } // class Sketch(gf:DrawContext => Unit) import javafx.scene.image.{Image => FXImage} object ImgFX{ def loadFXImage(file:File):Option[FXImage] = Img.findInput(file,List("png","jpg")) .map{in => new FXImage(in.is)} //TODO do we need to close this stream? } case class ImgFX(img:FXImage, box:Rect=Rect(100,100)) extends Img{ def moveTo(thatBox:Rect):ImgFX = copy(box = thatBox) def draw(implicit g:DrawContext) = g ! this } trait NodeFX{ import javafx.scene.{Group,Scene,Node} def node:Node private var init = false def draw(box:Rect, scale:Double, g:DrawContext):Unit = { //--load it up on the first frame if(!init){ for(scene <- g.asInstanceOf[DrawContextFX].scene){ scene.getRoot().asInstanceOf[Group].getChildren.add(0,node) //Note: this sends the video to the back TODO make configurable } init = true } //--adjust the size and location on every frame! // view.setX(box.a.x) //translate may work just as well and be generic to the node // view.setY(box.a.y) // val localBox = boxOf(node) // val s = 1d //box.width/localBox.width //FIXME TODO choose height or width based on size // val s = 2d // node.setScaleX(scale)//pivot point is the center of the object so not useful // node.setScaleY(scale) val translateTransform = new javafx.scene.transform.Translate(box.a.x, box.a.y) val scaleTransform = new javafx.scene.transform.Scale(scale,scale,0d,0d) //pivot around origin node.getTransforms.setAll(translateTransform, scaleTransform) // Log(scale, box, localBox) // node.setScaleX(scale) // node.setScaleY(scale) // val box = rawSize fitIn img.box //fit the picture size including aspect ratio into the drawing box //TODO check if rotations can also work here... () } def visible:Boolean = node.isVisible //TODO check if these work def visible_=(v:Boolean) = node.setVisible(v) } class VideoFX(file:File) extends MediaRemote with NodeFX{ import javafx.scene.media.{Media => FXMedia,_} //--media lazy val media = new FXMedia(file.url.toString) //--player and view(node) lazy val player = new MediaPlayer(media) lazy val node = new MediaView(player) //--media remote methods def play:Unit = player.play def stop:Unit = player.stop def pause:Unit = player.pause def isPlaying:Boolean = player.getStatus == MediaPlayer.Status.PLAYING def cursor:Time = Time(player.getCurrentTime) def cursor_=(t:Time):Unit = player.seek(t) def speedup:Double = player.getCurrentRate /** note changing the rate during playback causes some blocking but not terrible and only if changed*/ def speedup_=(v:Double):Unit = player.setRate(v) //--cached fields private var _length:Option[Time] = None def length:Option[Time] = { if(_length.isEmpty) _length = media.getDuration.noneIf{d => d.isUnknown|| d.isIndefinite}.map{Time.apply} _length } private var _size:Option[Vec] = None def size:Option[Vec] = { if(_size.isEmpty){ val w = media.getWidth val h = media.getHeight if(w == 0 || h == 0) _size = Some(Vec(w,h)) } _size } def snapshot:Img = ImgFX(DrawContextFX.snapshot(node)) def snapshot(size:Vec):Img = ImgFX(DrawContextFX.snapshot(node,size)) override def draw(box:Rect, scale:Double, g:DrawContext):Unit = { super.draw(box,scale,g) node.setFitHeight(box.height) node.setFitWidth(box.width) () } } case class WebFX(file:File) extends NodeFX{ import javafx.scene.web.{WebView, WebEngine} val node = new WebView val engine = node.getEngine val content:String = Img.findInput(file,List("html","svg")) .map{_.toString} .getOrElse(s"<html><body>${file.canon.path}</body></html>") engine.loadContent(content) // def size:Vec = Vec(node.getMeasuredWidth,node.getMeasuredHeight) // engine.load(url) } object DrawContextFX{ //-- constructors def apply(c:javafx.scene.canvas.Canvas) = new DrawContextFX(c.getGraphicsContext2D) def apply(gc:javafx.scene.canvas.GraphicsContext) = new DrawContextFX(gc) //TODO other constructors //-- helper utilities // import javafx.scene.Node private def nodeBox(node:Node):Rect = { val b = node.getBoundsInLocal() Rect(Vec(b.getMinX, b.getMinY), Vec(b.getMaxX, b.getMaxY)) } private def nodeSize(node:Node):Vec = { val b = node.getBoundsInLocal() Vec(b.getWidth, b.getHeight) } private def nodeSizeInParent(node:Node):Vec = { val b = node.getBoundsInParent Vec(b.getWidth, b.getHeight) } def snapshot(node:Node):FXImage = snapshot(node, nodeSizeInParent(node), false) def snapshot(node:Node, size:Vec):FXImage = snapshot(node, size, true) private def snapshot(node:Node, size:Vec, useOptimalScale:Boolean):FXImage = { val sp = new javafx.scene.SnapshotParameters() sp.setFill(javafx.scene.paint.Color.TRANSPARENT) //enable background transparency if(useOptimalScale){ val orgSize = nodeSizeInParent(node) val scale:Double = (size.x/orgSize.x) min (size.y/orgSize.y) sp.setTransform( new javafx.scene.transform.Scale(scale,scale)) } val wi = new javafx.scene.image.WritableImage(size.x.round.toInt, size.y.round.toInt) node.snapshot(sp, wi) wi } def save(img:FXImage, file:File):Try[File] = { file.mkParents.flatMap{f => Try{ javax.imageio.ImageIO.write( javafx.embed.swing.SwingFXUtils.fromFXImage(img,null), f.ext, f.file ) file } } } def save(node:Node, file:File):Try[File] = save(snapshot(node), file) } // javafx.scene.canvas.GraphicsContext // class DrawContextFX(val g:javafx.scene.canvas.GraphicsContext, val scene:Option[javafx.scene.Scene]=None) extends DrawContext{ import javafx.scene.text.{Font => FXFont, TextAlignment} import javafx.geometry.VPos import scala.language.implicitConversions import Style._ private var _stroke:Option[Color] = None private var _fill:Option[Color] = None private var _weight:Double = 1d //TODO move to protected so only the drawing contexts can use these lookup values def stroke:Option[Color] = _stroke def fill:Option[Color] = _fill def weight:Double = _weight override def !(p:Default.type):Unit = { //g.background(White) super.!(p) //--canvas/context _stroke = None _fill = None } private implicit def colorToFxColor(c:Color) = javafx.scene.paint.Color.rgb(c.r,c.g,c.b,c.a.toDouble/255) private def canvas = g.getCanvas def size:Vec = Vec(canvas.getWidth, canvas.getHeight) //--required properties `p` // note this argument based dispatching is applied at compile time and does not need runtime pattern matching //---coloring def !(p:Background):Unit = { val s = screen g.clearRect(s.a.x, s.a.y, s.width, s.height) //always clear the screen but only draw a background color if visible if(!p.c.isFullyTransparent){ //fully transparent is essentially a null case so dont' use anything in that case g.setFill(p.c) //use the background color g.fillRect(s.a.x, s.a.y, s.width, s.height) } } def !(p:Fill):Unit = {_fill = Some(p.c); g.setFill(p.c)} def !(p:Stroke):Unit = {_stroke = Some(p.c); g.setStroke(p.c)} def !(p:Weight):Unit = {_weight=p.value; g.setLineWidth(p.value)} def !(p:FillNone.type):Unit = _fill = None //TODO do we need to call noFill here? def !(p:StrokeNone.type):Unit = _stroke = None //TODO do we need to call noStroke here? def !(p:Font):Unit = g.setFont(fontCache(p).fxFont) def style(f: =>Unit):Unit = {g.save; f; g.restore} //--cache to auto load and keep resources, this removes the initialization requirement and implementation details to the surface drawn context /**compute the width of represented string*/ def textSize(text:Text,font:Font):Vec = fontCache(font).sizeOf(text) class FontFX(val font:Font){ lazy val fxFont = Try{FXFont.font(font.name, font.size)} getOrElse FXFont.getDefault //private lazy val metrics = new FontMetrics(fxFont) awt mechanism def sizeOf(text:Text):Vec = { //FIXME make this box work right import javafx.scene.text.{Text => FXText,TextBoundsType} val txt = new FXText(text.value) txt.setFont(fxFont) // txt.setBoundsType(TextBoundsType.VISUAL) //LOGICAL, VISUAL, LOGICAL_VERTICAL_CENTER //--use intersect to find size https://stackoverflow.com/a/32291954/622016 // import javafx.scene.shape.{Rectangle,Shape} // val b = txt.getBoundsInLocal // val clip = new Rectangle(b.getMinX, b.getMinY, b.getWidth, b.getHeight) // val shape = Shape.intersect(txt, clip) //--report size DrawContextFX.nodeSize(txt) } } private val fontCache:Cache[Style.Font,FontFX] = Cache{font:Style.Font => new FontFX(font) } private val svgCache = Cache{xml:String => //new PShapeSVG(processing.data.XML.parse(xml)) ??? } private val webCache = Cache{f:File => new WebFX(f) } private val imgCache = Cache{f:File => ImgFX.loadFXImage(f).get //FIXME don't use the dangerous get (offer an alternative) } private val videoCache = Cache{f:File => new VideoFX(f) } //--video def remote(video:Video):MediaRemote = videoCache(video.file) def !(video:Video):Unit = videoCache(video.file).draw(video.box, 1d, this) def !(html:Html):Unit = webCache(html.file).draw(html.box, html.scale, this) def emptyCache() = { //TODO or use the word clear fontCache.empty webCache.empty svgCache.empty //TODO remove and reference webCache instead imgCache.empty videoCache.empty } def close():Unit = { for(v <- videoCache.values) v.stop //stop all media from playing // emptyCache(0 } //--Alignment def !(p:Align):Unit = { g.setTextAlign(p.horz match { case Left => TextAlignment.LEFT case Center => TextAlignment.CENTER case Right => TextAlignment.RIGHT }) g.setTextBaseline(p.vert match { case Top => VPos.TOP case Midline => VPos.CENTER case Bottom => VPos.BOTTOM }) } //--transform (use affine matrix instead?) def !(p:Translate):Unit = g.translate(p.t.x,p.t.y) def !(p:ScaleProperty):Unit = g.scale(p.t.x,p.t.y) def !(p:Rotate):Unit = g.rotate(p.r.deg) //fx strangely specified in degrees instead of rad //-- shapes `s` def !(p:Path):Unit = path{ val head:Vec = p.vertices.head.last val rest:Iterable[Vertex] = p.vertices.tail g.moveTo(head.x, head.y) rest.foreach{ //TODO it would be nice if these didn't require pattern match but to make something working (run with it for now and optimize/generalize later) case Vec(x,y,_) => g.lineTo(x,y) case BezierVertex(ca,cb,b) => g.bezierCurveTo(ca.x,ca.y, cb.x,cb.y, b.x,b.y) } if(p.isClosed) g.closePath() } def !(s:Circ):Unit = ellipse(s.c, Vec(s.r,s.r)) def !(s:Line):Unit = { if(_stroke.isDefined) g.strokeLine(s.a.x,s.a.y, s.b.x,s.b.y) } def !(s:Rect):Unit = { if(_fill.isDefined) g.fillRect( s.a.x , s.a.y , s.width , s.height) if(_stroke.isDefined) g.strokeRect(s.a.x , s.a.y , s.width , s.height) } def !(s:Text):Unit = { if(_fill.isDefined) g.fillText( s.value, s.pos.x , s.pos.y) if(_stroke.isDefined) g.strokeText(s.value, s.pos.x , s.pos.y) } private def path(f: =>Unit):Unit = { g.beginPath() f if(_fill.isDefined) g.fill() if(_stroke.isDefined) g.stroke() } def !(s:Poly):Unit = path{ val head = s.vertices.head g.moveTo(head.x, head.y) s.vertices.tail.foreach{v => g.lineTo(v.x, v.y)} g.closePath() } def !(tri:Tri):Unit = path{ g.moveTo(tri.a.x, tri.a.y) g.lineTo(tri.b.x, tri.b.y) g.lineTo(tri.c.x, tri.c.y) g.lineTo(tri.a.x, tri.a.y) g.closePath() } def !(a:Arc):Unit = { g.save g.setLineWidth(a.w) // TODO g.setStrokeCap(SQUARE) is already default in FX val w = a.r*2 //g.ellipseMode(is set to CENTER as default which means RADIUS ???) g.strokeArc(a.c.x,a.c.y, w,w, a.angle.min.deg,a.angle.max.deg, javafx.scene.shape.ArcType.OPEN) g.restore } private def ellipse(c:Vec, r:Vec):Unit = { if(_fill.isDefined) g.fillOval( c.x-r.x , c.y-r.y , r.x*2 , r.y*2) //fx oval is upper left referenced and width and height if(_stroke.isDefined) g.strokeOval(c.x-r.x , c.y-r.y , r.x*2 , r.y*2) } def !(e:Ellipse):Unit = { if(e.rotation == Angle(0)) ellipse(e.c, e.r) else{ g.save g.translate(e.c.x, e.c.y) g.rotate(e.rotation.deg) ellipse(Vec.zero, e.r) g.restore } } private def rawImageOf(img:Img):FXImage = img match { case ImgFile(file, _) => imgCache(file) case ImgFX(img,_) => img //raw wrapped image case _ => ??? //TODO Img wrapped Pimage } def !(img:Img):Unit = { val raw:FXImage = rawImageOf(img) val rawSize = Rect(raw.getWidth,raw.getHeight) val box = rawSize fitIn img.box //fit the picture size including aspect ratio into the drawing box //TODO check if rotations can also work here... g.drawImage(raw, box.a.x, box.a.y, box.width, box.height) } def !(svg:Svg):Unit = ???//g.shape(svgCache(svg.xml)) // def !(arrow:Arrow):Unit = ??? def !(star:Star):Unit = ???//shape{for(v <- star.vertices) g.vertex(v.x, v.y)} override def !(z:Bezier):Unit = path{ g.moveTo(z.a.x, z.a.y) g.bezierCurveTo(z.ca.x, z.ca.y, z.cb.x, z.cb.y, z.b.x,z.b.y) //general bezier } //--sketch //initialize a draw context for a sketch operations with a specific draw function on events with this concrete draw context and let it run //TODO add a stop sketch func on the return type //this should only be called once on a draw context so maybe move it to a constructor? def sketchWith(draw:DrawContext => Unit):Unit = sketchWith(draw, None) def sketchWith(frameRate:Frequency)(draw:DrawContext => Unit):Unit = sketchWith(draw, Some(frameRate.hz.round.toInt)) def sketchWith(draw:DrawContext => Unit, maxFPS:Option[Int]):Unit = { canvas.setFocusTraversable(true) //this is not on by default and needed for key events https://stackoverflow.com/a/24127625/622016 val listener = Java.InvalidationListener{_ => draw(this)} canvas.widthProperty addListener listener canvas.heightProperty addListener listener def update():Unit = {draw(this); frameFinished()} //--mouse events //root:works scene:works canvas:works (mouse moved at least) // getX:The entire window getSceneX:works getScreenX:Entire monitor window def handleMouse(f:Vec => Unit) = Java.EventHandler{e:javafx.scene.input.MouseEvent => f(Vec(e.getX, e.getY)) update() } canvas.setOnMouseMoved ( handleMouse ( this mouseMoved _ ) ) canvas.setOnMouseClicked ( handleMouse ( this mouseClicked _ ) ) canvas.setOnMousePressed ( handleMouse ( this mousePressed _ ) ) canvas.setOnMouseReleased ( handleMouse ( this mouseReleased _ ) ) canvas.setOnMouseDragged ( handleMouse ( this mouseDragged _ ) ) //--touch events def handleTouch(f:List[Vec] => Unit) = Java.EventHandler{e:javafx.scene.input.TouchEvent => f(e.getTouchPoints.asScala.toList map {p => Vec(p.getX, p.getY)}) update() } canvas.setOnTouchMoved ( handleTouch ( this touchMoved _ ) ) canvas.setOnTouchPressed ( handleTouch ( this touchPressed _ ) ) canvas.setOnTouchReleased ( handleTouch ( this touchReleased _ ) ) //--scroll events def handleScroll(f:Vec => Unit) = Java.EventHandler{e:javafx.scene.input.ScrollEvent => f(Vec(e.getDeltaX, e.getDeltaY)) update() } canvas.setOnScroll( handleScroll ( this scrollMoved _ ) ) // https://stackoverflow.com/a/32597817/622016 import javafx.scene.input.DragEvent def handleDrag(f:String => Unit) = Java.EventHandler{e:javafx.scene.input.DragEvent => val db = e.getDragboard val res = if(db.hasFiles) Some(db.getFiles.asScala.map{f => File(f).path}.mkString(";")) else if(db.hasString) Some(db.getString) else None e.getEventType match { case DragEvent.DRAG_OVER => if(e.getGestureSource != canvas){ for(content <- res) { e.acceptTransferModes(javafx.scene.input.TransferMode.ANY:_*) // TransferMode.COPY_OR_MOVE:_*) f(content) } } case DragEvent.DRAG_DROPPED => for(content <- res) f(content) e.setDropCompleted(res.isDefined) case _ => } e.consume } canvas.setOnDragDropped( handleDrag( this dragDropped _) ) canvas.setOnDragOver( handleDrag( this dragOver _) ) //--keyboard events def handleKey(f:Keyboard.KeyCode => Unit) = Java.EventHandler{e:javafx.scene.input.KeyEvent => val fxCode = e.getCode val drxCode = Keyboard.KeyCode.fromFX(fxCode) // Log(fxCode,drxCode) f(drxCode) e.consume() //TODO is this consume required?? update() } canvas.setOnKeyPressed (handleKey ( this keyPressed _ ) ) canvas.setOnKeyReleased (handleKey ( this keyReleased _ ) ) // TODO canvas.setOnKeyReleased??? // TODO canvas.setOnKeyTyped ??? //--scroll events // TODO canvas.setOnScroll //--init draw without any events update() //--setup animation frame rate //https://stackoverflow.com/a/30151889/622016 // if framerate is defined then launch it in a repeated loop for(fps <- maxFPS if fps > 0){ // (1.s/fps) repeat update() //Note bad things happen when using the scheduled context execution to do the animations val frameNanos = (1.s/fps).ns.toLong val timer = new javafx.animation.AnimationTimer{ private var last:Long = 0L override def handle(now:Long):Unit = { if(now - last > frameNanos){ last = now //update the last time //before the update call update() //do the drawing update } } } timer.start() } } override def save(img:Img, file:File):Try[File] = img match { case ImgFX(img,_) => DrawContextFX.save(img, file) case _ => ??? } override def save(file:File):Try[File] = DrawContextFX.save(canvas, file) override def snapshot(size:Vec):Img = ImgFX(DrawContextFX.snapshot(canvas,size)) override def snapshot:Img = ImgFX(DrawContextFX.snapshot(canvas)) } class SketchApp { //--override-able def size:Vec = Vec(600,400) def title:String = this.toString.takeWhile(_ != '$').reverse.takeWhile(_ != '.').reverse //"cc.drx.SketchApp" def icon:ImgFile = if(Img(title).toFX.isDefined) Img(title) else Img("drx.png") def maxFPS:Option[Int] = Some(60) def transparent:Boolean = false def clearEachFrame:Boolean = true def background:Color = Transparent protected[drx] var _args:Array[String] = Array() def args:Array[String] = _args //--sketch function final def onDraw(f:DrawContext => Unit):Unit = DrawContextFXApp := f //--init function // final def onInit(f:DrawContext => Unit):Unit = DrawContextFXApp.onInit(f) //--implemented final def main(args:Array[String]):Unit = {DrawContextFXApp.launch(this, args)} //singleton launcher.. //--settings helper functions for JavaFX (could exist in an object).. final def applySettings(stage:javafx.stage.Stage):Unit = { if(transparent) stage.initStyle(javafx.stage.StageStyle.TRANSPARENT) //transparent backgrounds icon.toFX.foreach(stage.getIcons.add) //if icon file or resource exists add it new javafx.scene.image.Image("path to image... or resourse as stream")) stage.setTitle(title) } final def applySettings(scene:javafx.scene.Scene):Unit = { if(transparent) scene.setFill(null) } } object DrawContextFXApp{ import javafx.application.{Application=>FXApplication} private var _app:SketchApp = new SketchApp //initial settings without any override def main(args:Array[String]):Unit = {_app._args = args; FXApplication.launch(classOf[DrawApp], args: _*)} def launch:Unit = main(Array()) def launch(app:SketchApp):Unit = {_app=app; main(Array())} def launch(app:SketchApp,args:Array[String]):Unit = {_app=app; main(args)} def sketchWith(f:DrawContext => Unit):Unit = { this += f; launch} private var drawFunctions:List[DrawContext => Unit] = List() def +=(f:DrawContext => Unit) = drawFunctions = f :: drawFunctions def :=(f:DrawContext => Unit) = drawFunctions = List(f) // private var _init:DrawContext => Unit = {_ => ()} //empty init function // def onInit(f:DrawContext => Unit) = _init = f def draw(g:DrawContext):Unit = { //--by default the each frame is clearEachFrame is drawn (clear the buffer), or do it just the first time if(_app.clearEachFrame || g.frame == 0) g ! Style.Background(_app.background) //--draw all specified drawFunctions for(f <- drawFunctions) f(g) } class DrawApp extends FXApplication{ final override def start(stage:javafx.stage.Stage):Unit = { //--canvas val canvas = new javafx.scene.canvas.Canvas(_app.size.x.toInt, _app.size.y.toInt) //--root val root = new javafx.scene.Group() root.getChildren.add(canvas) //--scene val scene = new javafx.scene.Scene(root, _app.size.x.toInt, _app.size.y.toInt) // bind the scene size to the canvas canvas.widthProperty bind scene.widthProperty canvas.heightProperty bind scene.heightProperty //--make a drawcontext and apply a sketch redraw context and initiate a sketch with listeners for redraw events // // val ctx = new DrawContextFX(c.getGraphicsContext2D) val ctx = new DrawContextFX(canvas.getGraphicsContext2D, Some(scene)) ctx.sketchWith(DrawContextFXApp.draw, _app.maxFPS) //--setup app shutdown events //https://stackoverflow.com/a/20374691/622016 stage setOnCloseRequest Java.EventHandler{e:javafx.stage.WindowEvent => ctx.close() javafx.application.Platform.exit() System.exit(0) } //--TODO let the scene be dragged/moved via the ctx execution function??? //--stage settings _app applySettings stage //--scene settings _app applySettings scene //--add the scene and launch the stage with the show command stage.setScene(scene) stage.show() } } }