//:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** @author John Miller * @version 1.0 * @date Mon Sep 21 15:05:06 EDT 2009 * @see LICENSE (MIT style license file). */ package scalation.scala2d import actors.Actor import math.{abs, pow, sqrt} import swing.{MainFrame, Panel} import scalation.scala2d.Colors._ import scalation.scala2d.QCurve.{calcControlPoint, distance} import scalation.scala2d.Shapes.{Dimension, Graphics2D} import scalation.util.Error //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** The `QCurve` class enhances the `QuadCurve.Double` class (from the java.awt.geom * package) by allowing entities to move along such quadratic curves as well as * lines. Although the curve could be developed as a quadratic function where * 'y = ax^2 + bx + c'. The following quadratic bezier formulation is used: * p(t) = (x(t), y(t)) = [(1-t)^2 * p1] + [2 * (1-t) * t * pc] + [t^2 * p2]. * @param p1 the starting point for the quad curve * @param pc the control point for the quad curve * @param p2 the ending point for the quad curve * @param straight whether the quad curve is straight (i.e., a line) */ case class QCurve (var p1: R2 = R2 (0.0, 0.0), var pc: R2 = R2 (0.0, 0.0), var p2: R2 = R2 (0.0, 0.0), var straight: Boolean = true) extends java.awt.geom.QuadCurve2D.Double (p1.x, p1.y, pc.x, pc.y, p2.x, p2.y) with CurvilinearShape with Error { /** Length of the QCurve */ lazy private val _length = (distance (p1, p2) + distance (p1, pc) + distance (p2, pc)) / 2.0 /** Trajectory parameter t ranges from 0. to 1. (indicates how far along the curve) */ private var t = 0.0 /** Number of discrete steps to take along trajectory */ private var steps = 200 //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** Construct a straight line (degenerate quad curve). * @param p1 the starting point * @param p2 the ending point */ def this (p1: R2, p2: R2) { this (p1, calcControlPoint (p1, p2), p2, true) } // constructor //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** Construct a quad curve where bend indicates the distance to the control * point. * @param p1 the starting point * @param p2 the ending point * @param bend the bend or curvature (1. => line length) */ def this (p1: R2, p2: R2, bend: Double) { this (p1, calcControlPoint (p1, p2, bend), p2, false) } // constructor //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** Construct a quad curve using an explicitly given control point. * @param p1 the starting point * @param pc the control point * @param p2 the ending point */ def this (p1: R2, pc: R2, p2: R2) { this (p1, pc, p2, false) } // constructor //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** Return the current trajectory (t) of the curve. */ def trajectory: Double = t //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** Set the trajectory (t) to a new value. * @param traj the new trajectory for the curve */ def setTrajectory (traj: Double) { t = traj } //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** Get the x-coordinate of the center of the line/curve. */ def getCenterX (): Double = { if (straight) (p1.x + p2.x) / 2.0 else (p1.x + 2.0 * pc.x + p2.x) / 4.0 } // getCenterX //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** Get the y-coordinate of the center of the line/curve. */ def getCenterY (): Double = { if (straight) (p1.y + p2.y) / 2.0 else (p1.y + 2.0 * pc.y + p2.y) / 4.0 } // getCenterY //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** Set (or reset) the location for the QCurve as a line. * @param _p1 the starting point * @param _p2 the ending point */ def setLine (_p1: R2, _p2: R2) { p1 = _p1; p2 = _p2 pc = calcControlPoint (p1, p2) // middle, on line => line super.setCurve (p1, pc, p2) } // setLine //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** Set (or reset) the location for the QCurve as a curve using bend to * calculate the control point. * @param _p1 the starting point * @param _p2 the ending point * @param bend the bend or curvature (1. => line-length) */ def setLine (_p1: R2, _p2: R2, bend: Double) { p1 = _p1; p2 = _p2 pc = calcControlPoint (p1, p2, bend) // off line => curve straight = false super.setCurve (p1, pc, p2) } // setLine //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** Set (or reset) the location for the QCurve as a curve using an explicitly * given control point. * @param _p1 the starting point * @param _pc the control point * @param _p2 the ending point */ override def setLine (_p1: R2, _pc: R2, _p2: R2) { p1 = _p1; pc = _pc; p2 = _p2 straight = false super.setCurve (p1, pc, p2) } // setLine //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** Get the first/start point of the quad curve. */ def getFirst: R2 = p1 //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** Get the first/start point of the quad curve, adjusted from top-left to * center coordinates. * @param width the width of object traversing the curve * @param height the height of object traversing the curve */ def getFirst (width: Double, height: Double): R2 = { R2 (p1.x + width / 2.0, p1.y + height / 2.0) } // getFirst //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** Get the control point of the quad curve. */ def getControl: R2 = pc //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** Get the last/end point of the quad curve. */ def getLast: R2 = p2 //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** Get the last/end point of the quad curve, adjusted from top-left to * center coordinates. * @param width the width of object traversing the curve * @param height the height of object traversing the curve */ def getLast (width: Double, height: Double): R2 = { R2 (p2.x + width / 2.0, p2.y + height / 2.0) } // getLast //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** Are (x, y) and (xe, ye) essentially the same? */ def isSame (x: Double, y: Double, xe: Double, ye: Double, step: Double): Boolean = { (xe - x) * (xe - x) + (ye - y) * (ye -y) < step * step } // isSame //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** Given a value for the trajectory parameter t (in [0., 1.]) calculate * the point on the curve using the Quadratic Bezier equation. * See http://en.wikipedia.org/wiki/Bézier_curve#Quadratic_curves */ def eval (): R2 = { R2 (pow (1.0-t, 2) * p1.x + 2.0 * (1.0-t) * t * pc.x + pow (t, 2) * p2.x, pow (1.0-t, 2) * p1.y + 2.0 * (1.0-t) * t * pc.y + pow (t, 2) * p2.y) } // eval //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** Return the next point on the quad curve (one step beyond current point). * Return null if t > 1. (i.e., past end point). */ def next (): R2 = { var q: R2 = null // the next point along the curve if (t > 1.0) { t = 0.0 // reset trajectory } else { q = eval () // calculate the new point } // if t += 1.0 / steps.asInstanceOf [Double] // increment trajectory parameter // println ("QCurve.next: q = " + q) q } // next //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** Return the next point on the quad curve (one step beyond current point) * and adjust from top-left to center coordinates for the object traversing * the curve based on its width and height. * Return null if t > 1. (i.e., past end point). * @param width the width of object traversing the curve * @param height the height of object traversing the curve */ override def next (width: Double, height: Double): R2 = { val q = next () if (q != null) R2 (q.x - width / 2.0, q.y - height / 2.0) else null } // next //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** Set the number of steps for tokens to take as move along the quad curve. * @param steps the number of steps to take along the quad curve */ def setSteps (_steps: Int) { steps = _steps } // setSteps //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** Return the length of this QCurve */ def length: Double = _length //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** Show the start, control and end points of the the QCurve. */ override def toString: String = { "QCurve ( " + p1 + " , " + pc + " , " + p2 + " )" } // toString } // QCurve class /**::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: * The `QCurve` companion object provides formulas used by the `QCurve` class. */ object QCurve { /** Tolerance for comparing real numbers */ private val EPSILON = 1E-7 //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** Calculate the slope of the line defined by points p1 and p2. * Note: if deltaX is 0, the method returns infinity. * @param p1 the starting point * @param p2 the ending point */ def slope (p1: R2, p2: R2): Double = { (p2.y - p1.y) / (p2.x - p1.x) } // slope //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** Calculate the distance or the length of the line connecting points p1 * and p2. * @param p1 the starting point * @param p2 the ending point */ def distance (p1: R2, p2: R2): Double = { sqrt (pow (p2.x - p1.x, 2) + pow (p2.y - p1.y, 2)) } // slope //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** Calculate the location (x, y) of the control point. It is positioned * orthogonally to the mid point of the line connecting p1 and p2 at a * distance dist, where dist = bend * || p2 - p1 ||. A bend of 0. gives * a straight line, while 2./-2. gives a huge bend up-right/down-left. * @param p1 the starting point * @param p2 the ending point * @param bend the bend or curvature */ def calcControlPoint (p1: R2, p2: R2, bend: Double = 0.0): R2 = { val mid = R2 ((p1.x + p2.x) / 2.0, (p1.y + p2.y) / 2.0) if (abs (bend) < EPSILON) { mid } else { val m = slope (p1, p2) val dist = bend * distance (p1, p2) if (m.isInfinity) { R2 (mid.x + dist, mid.y) } else { R2 (mid.x + dist * m / sqrt (1.0 + pow (m, 2)), mid.y - dist / sqrt (1.0 + pow (m, 2))) } // if } // if } // calcControlPoint } // QCurve object //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** The `QCurveTest` object tests the `QCurve` classes' quad curves. */ object QCurveTest extends App { private val line1 = new QCurve (R2 (200, 200), R2 (400, 200)) private val line2 = new QCurve (R2 (200, 200), R2 (200, 400)) private val line3 = new QCurve (R2 (200, 200), R2 (400, 400)) private val curve1 = new QCurve (R2 (200, 200), R2 (400, 200), 1.0) private val curve2 = new QCurve (R2 (200, 200), R2 (200, 400), 1.0) private val curve3 = new QCurve (R2 (200, 200), R2 (400, 400), 1.0) private val curve4 = new QCurve (R2 (200, 200), R2 (400, 200), -2.0) private val curve5 = new QCurve (R2 (200, 200), R2 (200, 400), -2.0) private val curve6 = new QCurve (R2 (200, 200), R2 (400, 400), -2.0) private val canvas = new Panel { background = white preferredSize = new Dimension (600, 600) override def paintComponent (g2d: Graphics2D) { super.paintComponent (g2d) g2d.setPaint (red) g2d.draw (line1) g2d.draw (curve1) g2d.draw (curve4) g2d.setPaint (blue) g2d.draw (line2) g2d.draw (curve2) g2d.draw (curve5) g2d.setPaint (purple) g2d.draw (line3) g2d.draw (curve3) g2d.draw (curve6) } // paintComponent } // canvas Panel private def top = new MainFrame { title = "QCurveTest" contents = canvas visible = true } // top MainFrame println ("Run QCurveTest") top } // QCurveTest object //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** The `QCurveTest2` object tests traversals of `QCurve`'s (quad curves). */ object QCurveTest2 extends App { private class QCurveAnimator extends MainFrame with Actor { val curve = Array (new QCurve (R2 (100, 200), R2 (500, 200)), new QCurve (R2 (100, 200), R2 (500, 200), .5), new QCurve (R2 (100, 200), R2 (500, 200), -.5)) val ball = Ellipse () val canvas = new Panel { background = white preferredSize = new Dimension (600, 600) override def paintComponent (g2d: Graphics2D) { super.paintComponent (g2d) g2d.setPaint (red) // R in RGB order g2d.draw (curve(0)) g2d.setPaint (green) // G in RGB order g2d.draw (curve(1)) g2d.setPaint (blue) // B in RGB order g2d.draw (curve(2)) g2d.setPaint (purple) g2d.fill (ball) } // paintComponent } // canvas Panel def act () { val size = 10.0 var loc: R2 = null for (i <- 0 until curve.length) { println ("Move ball along RGB curve " + i) loc = curve(i).next (size, size) while (loc != null) { Thread.sleep (50) ball.setFrame (loc.x, loc.y, size, size) repaint () loc = curve(i).next (size, size) } // while } // for } // act title = "QCurveTest2" contents = canvas visible = true start () } // QCurveAnimator class println ("Run QCurveTest2") new QCurveAnimator () } // QCurveTest2 object