NetRexx logo BezierCurves.nrx
/* NetRexx */

import javax.swing.event -- to cover hyperlinkListener

class BezierCurves public extends JFrame

    properties private

        isTrue = 1
        isFalse = 0
-- A modeless JDialog with this (JFrame) as parent
        myDialog = JDialog JDialog(this, "Exploring Bezier curves with NetRexx", isFalse)
-- An on this (JFrame) dependent class for drawing the Bezier curves
        myCanvas = DrawCanvas
-- The wanted dimensions for  the JPanel(i.e. Canvas) and JDialog
        canvas_width = 300
        canvas_height = 300
        dialog_width = 600
        dialog_height = 770
/**
 * The constructor for the BezierCurves class instances, call to super is automatically
 * provided/inserted by NetRexx
 */
   
    method BezierCurves
/**
 * Create the HELP information in a un-editable JEditorPane
 */

        myPane = JEditorPane JEditorPane()
        myPane.setEditable(isFalse)
        myPane.setContentType("text/html")
-- The help info in html
        myPane.setText("<h1>Exploring Beziercurves with:</h1>" -
                    || "<img src='http://netrexx.org/images/NetRexxKingSmall.gif' >" -
                    || "<a href='http://netrexx.org/'><h1>NetRexx</h1></a>(http://netrexx.org)" -
                    || "<p>The code is based on the information provided on " -
                    || "<a href='https://pomax.github.io/bezierinfo/'>this website</a><br>(https://pomac.github.io/bezierinfo/).</br></p>" -  
                    || "<p>You can use this code in the following way:</p>" -
                    || "<ul>" -
                    || "<li>Execution starts with a predefined set of startpoint, endpoint and 3 controlpoints.</li>" -
                    || "<li>With the left mousebutton all points can be dragged.</li>" -
                    || "<li>Clicking the left mousebutton on one of the points will add an additional controlpoint " -
                    || "between the one clicked and the next point.</li>" -
                    || "<li>Clicking the right mousebutton on one of the points will remove that controlpoint.</li>" -
                    || "<li>Double clicking the left mousebutton allows you the change the number of flattening steps.</li>" -
                    || "</ul>" -
                    || "<p>Go ahead, have some fun, I hope!</p><p>      Ruurd Idenburg</p>")
-- The HyperlinkListener enables browsing of the referred websites
        hll = HyperlinkListener myHyperlinkListener()
        myPane.addHyperlinkListener(hll)
-- Add the JEditorPane to the JDialog
        myDialog.getContentPane().add(myPane, BorderLayout.CENTER)
-- and set the necessary options for the JDialog
        myDialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE)
        myDialog.pack()
        myDialog.setSize(dialog_width,dialog_height)
        myDialog.setVisible(isTrue)
-- Create the canvas for drawing the curves
        myCanvas = DrawCanvas(25)
        myCanvas.setPreferredSize(Dimension(canvas_width, canvas_height))
-- Add the drawing canvas to the JFrame
        this.getContentPane().add(myCanvas)
-- and set the necessary options for the JFrame
        this.setTitle("Exploring Beziercurves.....")
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)
        this.pack()
        this.setSize(canvas_width,canvas_height)
        this.setLocationRelativeTo(null)
        this.setVisible(isTrue)
/**
 * Start the application in the most thread-safe way.
 */

    method main(args=String[] ) static
        SwingUtilities.invokeLater(myRunnable())

/**
* Define inner class DrawCanvas, which is a JPanel used for custom drawing.
*/

class BezierCurves.DrawCanvas dependent private  extends JPanel -
      adapter implements MouseListener, MouseMotionListener

    properties public
-- first point is start, last point is end, in between are control
        points=ArrayList()
-- curvePoint is updated every flattening step
        curvePoint
-- size of a curvepoint
        curvePointRadius = 5
-- distance between X and also Y axes
        skeletonAxesIncrement = 25
-- The number of flattening steps to calculate the Bezier curve
        flatteningSteps = 10

    method DrawCanvas(steps)
-- Create the initial start-, end- and control-points
        points.add(110 150)
        points.add(25 190)
        points.add(210 250)
        points.add(230 110)
        points.add(210 30)
-- Set the number of flattening steps
        flatteningSteps = steps
-- Set the mouse listeners we need and implement
        addMouseListener(this)
        addMouseMotionListener(this)

    method mouseDragged(event=MouseEvent)
        moveCurvePoint(event)

    method mouseReleased(event=MouseEvent)
        moveCurvePoint(event)

    method moveCurvePoint(event = MouseEvent)
        ex = event.getX
        ey = event.getY
        r = curvePointRadius
        loop label redraw i=0 while i<points.size
            px = (Rexx points.get(i)).word(1)
            py = (Rexx points.get(i)).word(2)
            if (ex>=px-r & px<=px+r & ey>=py-r & ey<=py+r) then do
                points.set(i, ex ey)
                repaint()          
                leave redraw
            end
        end
/**
 * Handle single click and double click and left or right mouse action
 * Double click allows altering the number of flattenening steps
 * Single click with left mouse button allows adding a control point
 * Single click with right mouse button allows removing a control point
 */

    method mouseClicked(event=MouseEvent)
        if event.getClickCount>1 & \event.isConsumed() then do
            event.consume()
            input = JOptionPane.showInputDialog("Enter the number of curve flattening steps\nShould be in the range 0-100",flatteningSteps)
            if (input\=null &\input.isEmpty()) then do
                value = Rexx input
                if (value.dataType('W') & value>=0 & value<=100) then do
                    flatteningSteps = value
                    repaint()
                end
                else do
                    JOptionPane.showMessageDialog(null, "Number of flattening steps not in range 0-100!")
                end

            end
        end
        if event.getClickCount=1 then do
            ex = event.getX
            ey = event.getY
            r = curvePointRadius
            loop label redraw i=0 while i<points.size
                px = (Rexx points.get(i)).word(1)
                py = (Rexx points.get(i)).word(2)
                if (ex>=px-r & px<=px+r & ey>=py-r & ey<=py+r) then do
                    mouseButton = event.getButton
                    if mouseButton==MouseEvent.BUTTON1 then addControlPoint(i)
                    if mouseButton==MouseEvent.BUTTON3 then removeControlPoint(i)
                    leave redraw
                end
            end
        end

/**
 * A new control point is added halfway between the clicked one and the next control point.
 * Adding a control point beyond the end point is not allowed
 */

    method addControlPoint(i)
        if i<points.size-1 then do
            one = Rexx points.get(i)
            two = Rexx points.get(i+1)
            x1 = one.word(1)
            x2 = two.word(1)
            y1 = one.word(2)
            y2 = two.word(2)
            newX = ((x1+x2)/2).format(null,0)
            newY = ((y1+y2)/2).format(null,0)
            points.add(i+1, newX newY)
            repaint()
        end
        else do
            JOptionPane.showMessageDialog(null, "Adding a Controlpoint after Endpoint is not allowed!")
        end
/**
 * Removing start or end point is not allowed, dragging them is the way to change them.
 */

    method removeControlPoint(i)
        if (i>0 & i<points.size-1) then do
            oldpts = ArrayList points
            points = ArrayList()
            loop j=0 while j<oldpts.size
                --say oldpts.get(j)
                if j<>i then points.add(oldpts.get(j))
            end
            repaint()
        end
        else do
            JOptionPane.showMessageDialog(null, "Removing start or end point is not allowed!")
        end

/**
 * The code to (re)paint the drawing canvas
 */

    method paintComponent( g=Graphics )
        super.paintComponent(g)
    -- draw the skeleton
        incr = skeletonAxesIncrement
        height = getHeight()
        width = getWidth()
        g.setColor(Color.lightgray)
        loop i=0 by incr while i<=(Rexx height).max(width)
            g.drawLine(i,0,i,height)
            g.drawLIne(0,i,width,i)
        end
    -- show the number of flattening steps
        g.setColor(Color.BLACK)
        g.setFont(Font("Monospaced",Font.BOLD,10))
        xPos = 5
        yPos = height-5
        g.drawString("Flattening steps:" flatteningSteps, xPos, yPos)
    -- draw the lines between the points
        if points.size>1 then do
            px = 0
            py = 0
            loop i=0 while i<points.size
                x = (Rexx points.get(i)).word(1)
                y = (Rexx points.get(i)).word(2)
                if i>0 then g.drawLine(px,py,x,y)
                px = x
                py = y
            end
        end
    -- draw the points and information text
        if points.size>0 then do
            loop i=0 while i<points.size
                coords = String (i+1) "("(Rexx points.get(i)).word(1)","(Rexx points.get(i)).word(2)")"
                g.setColor(Color.BLACK)
                g.drawString(coords,(Rexx points.get(i)).word(1)+8, (Rexx points.get(i)).word(2)+8)
                g.setColor(Color.blue)
                if i=0 then g.setColor(Color.green)
                if i=points.size-1 then g.setColor(Color.red)
                g.fillOval( (Rexx points.get(i)).word(1)-4, (Rexx points.get(i)).word(2)-4,8 ,8)
            end
        end
    -- draw the flattened Bezier curve
        if points.size>1 then do
            t = 0.0
            prevptx = 0
            prevpty = 0
            loop while t<=1
                g.setColor(Color.blue)
                stepCurvePoint(points, t)
                ptx = curvePoint.word(1)
                pty = curvePoint.word(2)
                g.drawOval(int ptx-1,int  pty-1, 2, 2)
                if t<>0 then do
                    g.drawLine(int prevptx,int prevpty,int ptx,int pty)
                end
                prevptx = ptx
                prevpty = pty
                t = t+(100/flatteningSteps/100)
            end
        end

/**
 * If we want to draw B├ęzier curves, we can run through all values of t from 0 to 1 and
 * then compute the weighted basis function at each value, getting the x/y values we need
 * to plot. Unfortunately, the more complex the curve gets, the more expensive this
 * computation becomes. Instead, we can use de Casteljau's algorithm to draw curves.
 * This is a geometric approach to curve drawing, and it's really easy to implement.
 * So easy, in fact, you can do it by hand with a pencil and ruler.
 * <ol>
 * <li> Treat t as a ratio (which it is). t=0 is 0% along a line, t=1 is 100% along a line.
 * <li> Take all lines between the curve's defining points. For an order n curve, that's n lines.
 * <li> Place markers along each of these line, at distance t. So if t is 0.2, place the mark at 20% from the start, 80% from the end.
 * <li> Now form lines between those points. This gives n-1 lines.
 * <li> Place markers along each of these line at distance t.
 * <li> Form lines between those points. This'll be n-2 lines.
 * <li> Place markers, form lines, place markers, etc.
 * <li> Repeat this until you have only one line left. The point t on that line coincides with the original curve point at t.
 * </ol>
 */
               
    method stepCurvePoint(_points=ArrayList, t)
        if _points.size>1 then do
            newPoints= ArrayList()
            --say newPoints.length
            loop i=0 while i<_points.size-1
                x = (1-t)*(Rexx _points.get(i)).word(1)+t*(Rexx _points.get(i+1)).word(1)
                y = (1-t)*(Rexx _points.get(i)).word(2)+t*(Rexx _points.get(i+1)).word(2)
                newPoints.add(x y)
            end
            stepCurvePoint(newPoints, t)
        end
        else do
            x = (Rexx _points.get(0)).word(1).format(null,0)
            y = (Rexx _points.get(0)).word(2).format(null,0)
            curvePoint = x y
        end

/**
 * The HyperlinkListener is triggered by the hyperlinkUpdate event
 * The event type can be ENTERED, EXITED or ACTIVATED. If the hyperlink is activated
 * the (default) desktop browser is invoked to try to browse the activated link.
 */

class myHyperlinkListener implements HyperlinkListener

    method hyperlinkUpdate(event = HyperlinkEvent)
        if (event.getEventType == HyperlinkEvent.EventType.ACTIVATED) then do
            theURL = URL event.getURL()
            if (theURL\=null) then do
                dt = Desktop Desktop.getDesktop()
                dt.browse(theURL.toURI)
            catch ex=Exception
                ex.printStackTrace
            end
        end

/**
 * Start the application in a thread-safe way
 */

class myRunnable private implements Runnable
    method run
        BezierCurves()

 
If you feel inclined to make corrections, suggestions etc., please mail me any.
All content © Ruurd Idenburg, 2007–2023, except where marked otherwise. All rights reserved. This page is primarily for non-commercial use only. The Idenburg website records no personal information and sets no ‘cookies’. This site is hosted on a VPS(Virtual Private System) rented from Transip.nl, a Dutch company, falling under Dutch (privacy) laws (I think).

This page updated on Wed, 25 May 2022 12:35:13 +0200 by Ruurd Idenburg.