0
votes

I am looking to build a drawing application and have been able to do regular lines and dashed line but I wanted a 'wavy' line also known as serpent line. Any thoughts on how to accomplish this in AS3?

Updated: The code replied does answer the question but was hoping to be more freehand serpent so on mouse move it would be able to draw something like below (although it doesn't need the line through the waves).

enter image description here

1
Does it have to be free-hand? One typical (non-free hand) solution for this involves letting the user pick multiple points and then interpolating the Bezier Curve, allowing them to then adjust the interpolation parameters. - Justin L.
I would settle for even a straight line point to point. - user1013448
Isn't that what a "regular line" is? - Justin L.
No, sorry, I meant still serpent but only two points vs freehand. So instead of a line, it draws a squiggly line between 2 points. - user1013448

1 Answers

1
votes

You can either do it using beizer curves, but the simplest way is probably to draw line segments. Here's a working example:

package 
{
    import flash.display.*;
    import flash.events.Event;
    import flash.geom.*;

    public class Main extends Sprite {

        public function Main():void {
            if (stage) init();
            else addEventListener(Event.ADDED_TO_STAGE, init);
        }

        private function init(e:Event = null):void {
            this.graphics.lineStyle(2, 0x0, 0.5);

            // step = 1 creates a high-quality wave
            drawSerpentLine(this.graphics, new Point(50, 50), new Point(150, 250), 4, 20, 1);

            // step = 8 and the wave is not as smooth
            drawSerpentLine(this.graphics, new Point(300, 30), new Point(233, 150), 6, 10, 8);
        }

        // takes a graphics reference, and draws a serpent line between the two
        // specified points using the g's current lineStyle
        // frequency: determines how many waves 
        // amplitude: how "much" wave 
        // step: the quality (lower means smoother lines at the cost of speed)
        private function drawSerpentLine(g : Graphics, from : Point, to : Point, frequency : int = 5, amplitude : int = 20, step : int = 2):void {
            // the angle between the two points
            var ang : Number = Math.atan2(to.y - from.y, to.x - from.x);

            // the distance between the points
            var dis : Number = Point.distance(from, to);

            // a point which we use to store the current position to draw
            var currPoint : Point = new Point(from.x, from.y);

            for (var i:int = 0; i <= dis; i += step) {
                // how far away (perpendicularly) from the straight lines the current points should be
                var waveOffsetLength : Number = Math.sin((i / dis) * Math.PI * frequency) * amplitude;

                // calculate the current point. 
                currPoint.x = from.x + Math.cos(ang) * i + Math.cos(ang - Math.PI/2)*waveOffsetLength;
                currPoint.y = from.y + Math.sin(ang) * i + Math.sin(ang - Math.PI/2)*waveOffsetLength;

                if (i > 0) {
                    this.graphics.lineTo(currPoint.x, currPoint.y);
                } else {
                    this.graphics.moveTo(currPoint.x, currPoint.y);
                }
            }

            // close the last line so we end up at the end point
            this.graphics.lineTo(to.x, to.y);
        }
    }
}

Update:

Here's a modified version which draws freehand. The approach is similar: Each mouse move we draw serpent between two points. But the "phase" of the serpent needs to be stored in a variable so that the different drawSerpentLine() calls continue at the last call's phase. Also, we can't just draw a serpent between the last mouse coord and the current mouse coord because that would not create a smooth look. So we average the coordinates.

package 
{
    import flash.display.*;
    import flash.events.Event;
    import flash.events.MouseEvent;
    import flash.geom.*;

    public class Main extends Sprite {

        private var m_mouseIsDown : Boolean;
        private var m_lastPoint : Point = new Point();
        private var m_currPoint : Point = new Point();
        private var m_phase : Number = 0;

        private var m_firstDrawAfterMouseDown : Boolean;

        public function Main():void {
            if (stage) init();
            else addEventListener(Event.ADDED_TO_STAGE, init);
        }

        private function init(e:Event = null):void {
            this.graphics.lineStyle(2, 0x0, 0.5);

            this.stage.addEventListener(MouseEvent.MOUSE_DOWN, onMouseEvent);
            this.stage.addEventListener(MouseEvent.MOUSE_MOVE, onMouseEvent);
            this.stage.addEventListener(MouseEvent.MOUSE_UP, onMouseEvent);
        }

        private function drawSerpentLine(g : Graphics, from : Point, to : Point, frequency : Number = 0.15, amplitude : int = 6, step : int = 1):void {
            // the angle between the two points
            var ang : Number = Math.atan2(to.y - from.y, to.x - from.x);

            // the distance between the points
            var dis : Number = Point.distance(from, to);

            for (var i:int = 0; i <= dis; i += step) {
                m_phase += frequency;

                // how far away (perpendicularly) from the straight lines the current points should be
                var waveOffsetLength : Number = Math.sin(m_phase) * amplitude;

                // calculate the current point. 
                var x : Number = from.x + Math.cos(ang) * i + Math.cos(ang - Math.PI/2)*waveOffsetLength;
                var y : Number = from.y + Math.sin(ang) * i + Math.sin(ang - Math.PI/2)*waveOffsetLength;

                if (m_firstDrawAfterMouseDown) {
                    this.graphics.moveTo(x, y);
                    m_firstDrawAfterMouseDown = false;
                } else {
                    this.graphics.lineTo(x, y);
                }
            }
        }

        private function onMouseEvent(event : MouseEvent):void {
            switch(event.type) {
                case MouseEvent.MOUSE_DOWN:
                    m_mouseIsDown = m_firstDrawAfterMouseDown = true;
                    m_currPoint.x = m_lastPoint.x = this.mouseX;
                    m_currPoint.y = m_lastPoint.y = this.mouseY;
                    break;
                case MouseEvent.MOUSE_MOVE:
                    if (m_mouseIsDown) {
                        // to create a smoother look  we average the mouse coords
                        // where only 10% of the new mouse position is used, 90% is the old position
                        // the lower the first percentage the smoother the look, but the more
                        // the serpent will lag behind the actual mouse position (until you release the mouse)
                        m_currPoint.x = this.mouseX * 0.1 + m_lastPoint.x * 0.9;
                        m_currPoint.y = this.mouseY * 0.1 + m_lastPoint.y * 0.9;

                        drawSerpentLine(this.graphics, m_lastPoint, m_currPoint);

                        m_lastPoint.x = m_currPoint.x;
                        m_lastPoint.y = m_currPoint.y;
                    }
                    break;
                case MouseEvent.MOUSE_UP:
                    m_mouseIsDown = false;

                    // when the user releases, we complete the serpent
                    // by drawing the full distance to the current position
                    // (no percentages like in mouse move)
                    m_currPoint.x = this.mouseX;
                    m_currPoint.y = this.mouseY;
                    drawSerpentLine(this.graphics, m_lastPoint, m_currPoint);
                    break;
            }
        }
    }
}