Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create an illustrator/photoshop-like pentool for creating bezier curves in Unity

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;
    }
}
like image 214
hutfelt Avatar asked Jan 20 '26 00:01

hutfelt


2 Answers

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.

like image 109
Paul Sumpner Avatar answered Jan 21 '26 14:01

Paul Sumpner


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

like image 40
DoctorPangloss Avatar answered Jan 21 '26 13:01

DoctorPangloss



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!