12
votes

I need to create complex segmented bezier curves so I want update the code to create illustrator/photoshop-like pen tool bezier curves. This video shows how the pentool behaves.

Please note that instead of creating a quadratic bezier curve with the first two anchor points (as shown in the video) I would rather prefer a cubic (as in the linked code example).

The following are features I've realised of the illustrator/photoshop pen tool that are necessary for replication in Unity.

  • all anchor/control points are created at the same mouse point on the first click (object is created on the first click)

  • as the mouse point is moved from the first click (not pressed) the control points fall in with the two anchor points to create a straight line (cubic curve)

  • when the mouse is clicked and dragged (any distance from the first click) a control points move away from the straight line to form a curve based on the direction of the drag, they also increase in length as the drag increases in distance from the second click.

  • the path should be closed when the first anchor point is re-selected during the curves creation

I'm also not sure how to solve the above stated points but here is the code I've written so far:

BPath:

[System.Serializable]
public class BPath
{

    [SerializeField, HideInInspector]
    List<Vector2> points;

    [SerializeField, HideInInspector]
    public bool isContinuous;

    public BPath(Vector2 centre)
    {
        points = new List<Vector2>
    {
        centre+Vector2.left,
            centre+Vector2.left,
            centre+Vector2.left,
            centre+Vector2.left
    };
    }

    public Vector2 this[int i]
    {
        get
        {
            return points[i];
        }
    }

    public int NumPoints
    {
        get
        {
            return points.Count;
        }
    }

    public int NumSegments
    {
        get
        {
            return (points.Count - 4) / 3 + 1;
        }
    }

    public void AddSegment(Vector2 anchorPos)
    {
        points.Add(points[points.Count - 1] * 2 - points[points.Count - 2]);
        points.Add((points[points.Count - 1] + anchorPos) * .5f);
        points.Add(anchorPos);
    }

    public Vector2[] GetPointsInSegment(int i)
    {
        return new Vector2[] { points[i * 3], points[i * 3 + 1], points[i * 3 + 2], points[i * 3 + 3] };
    }

    public void MovePoint(int i, Vector2 pos)
    {

        if (isContinuous)
        {

            Vector2 deltaMove = pos - points[i];
            points[i] = pos;

            if (i % 3 == 0)
            {
                if (i + 1 < points.Count)
                {
                    points[i + 1] += deltaMove;
                }
                if (i - 1 >= 0)
                {
                    points[i - 1] += deltaMove;
                }
            }
            else
            {
                bool nextPointIsAnchor = (i + 1) % 3 == 0;
                int correspondingControlIndex = (nextPointIsAnchor) ? i + 2 : i - 2;
                int anchorIndex = (nextPointIsAnchor) ? i + 1 : i - 1;

                if (correspondingControlIndex >= 0 && correspondingControlIndex < points.Count)
                {
                    float dst = (points[anchorIndex] - points[correspondingControlIndex]).magnitude;
                    Vector2 dir = (points[anchorIndex] - pos).normalized;
                    points[correspondingControlIndex] = points[anchorIndex] + dir * dst;
                }
            }
        }


    else {
         points[i] = pos;
    }
    }
}

BPathCreator:

public class BPathCreator : MonoBehaviour
{

    [HideInInspector]
    public BPath path;


    public void CreatePath()
    {
        path = new BPath(transform.position);
    }
}

BPathEditor:

[CustomEditor(typeof(BPathCreator))]
public class BPathEditor : Editor
{

    BPathCreator creator;
    BPath path;

    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();
        EditorGUI.BeginChangeCheck();

        bool continuousControlPoints = GUILayout.Toggle(path.isContinuous, "Set Continuous Control Points");
        if (continuousControlPoints != path.isContinuous)
        {
            Undo.RecordObject(creator, "Toggle set continuous controls");
            path.isContinuous = continuousControlPoints;
        }

        if (EditorGUI.EndChangeCheck())
        {
            SceneView.RepaintAll();
        }
    }

    void OnSceneGUI()
    {
        Input();
        Draw();
    }

    void Input()
    {
        Event guiEvent = Event.current;
        Vector2 mousePos = HandleUtility.GUIPointToWorldRay(guiEvent.mousePosition).origin;

        if (guiEvent.type == EventType.MouseDown && guiEvent.button == 0 && guiEvent.shift)
        {
            Undo.RecordObject(creator, "Add segment");
            path.AddSegment(mousePos);
        }
    }

    void Draw()
    {

        for (int i = 0; i < path.NumSegments; i++)
        {
            Vector2[] points = path.GetPointsInSegment(i);
            Handles.color = Color.black;
            Handles.DrawLine(points[1], points[0]);
            Handles.DrawLine(points[2], points[3]);
            Handles.DrawBezier(points[0], points[3], points[1], points[2], Color.green, null, 2);
        }

        Handles.color = Color.red;
        for (int i = 0; i < path.NumPoints; i++)
        {
            Vector2 newPos = Handles.FreeMoveHandle(path[i], Quaternion.identity, .1f, Vector2.zero, Handles.CylinderHandleCap);
            if (path[i] != newPos)
            {
                Undo.RecordObject(creator, "Move point");
                path.MovePoint(i, newPos);
            }
        }
    }

    void OnEnable()
    {
        creator = (BPathCreator)target;
        if (creator.path == null)
        {
            creator.CreatePath();
        }
        path = creator.path;
    }
}
2
You want to display it on Editor or in-game? You could use a LineRenderer on the second case.z3nth10n
I've had good experiences with the Ara Trail asset, which you can also feed custom points.Philipp Lenssen

2 Answers

0
votes

Your're asking a lot of things. It would be hard to answer without giving a full implementation. But then it probably wouldn't be the one you wanted.

Some of the points you mention (and the video you show), suggest that a Hermite Spline might be more suitable. It's what i used to create behaviour like the one in the video.

0
votes

Use SpriteShape, Unity's official plugin. It is open source and supports modification at runtime. This uses the Photoshop behavior for tangents (anchor points).