We are used to great paint tools like TVPaint (ex-Aura) which are able to follow precisely the mouse/pen user movement. Especially when the user is fast, the software draws nice curves/splines, and not (as in other software) only lines.
Add to this that we would like to plot brushes at regular intervals, handle antialiasing and thickness. And... It should be multiplatform.
Here's a first code not optimised that I've created as a model. I'm using my own curve routines, but it's not optimal, and I'm working on a Bresenham way of doing this. But it's difficult, and any help would be appreciated.
Edit : modified code for better multiplatform, thanks to Trond and davido
Code: Select all
;
; ------------------------------------------------------------
;
; PureBasic - Drawing following mouse movement (curve approximation)
; by djes@free.fr in 2015
;
; (c) 2005 - Background code by Fantaisie Software
; 2015 - MouseMovementFollow by djes for blendman (aka super-smooth)
;
; ------------------------------------------------------------
;
CompilerIf #PB_Compiler_Thread = 0
CompilerError "Merci d'activer la gestion des threads"
CompilerEndIf
#DebugLevel = 1
DebugLevel #DebugLevel
;---- Constants
#CurveDeviation = 1.0
#CurveMinimalDistance = 10
;---- Structures
Structure vertex
x.f
y.f
EndStructure
;---- Global variables
Global Semaphore = CreateSemaphore()
Global Mutex = CreateMutex()
Global NewList Coords.vertex()
Global Dim SplineCoords.vertex(5)
Global CanvasWidth, CanvasHeight
;---- Procedures
Procedure.d min(a.d, b.d)
If a<b
ProcedureReturn a
Else
ProcedureReturn b
EndIf
EndProcedure
Procedure.d max(a.d, b.d)
If a>b
ProcedureReturn a
Else
ProcedureReturn b
EndIf
EndProcedure
Procedure.d VectorLength(vx.d, vy.d)
ProcedureReturn Sqr(vx*vx + vy*vy)
EndProcedure
Procedure SetPixelAA( x0, y0, color, blend = 0)
Define.i r = Red(color), g = Green(color), b=Blue(color), a=Alpha(color), c
If x0 >= 0 And y0 >= 0 And x0 < CanvasWidth-1 And y0 < CanvasHeight-1
c = Point(x0, y0)
Plot(x0, y0, RGBA((blend*Red(c) + (255-blend)*r) / 255, (blend*Green(c) + (255-blend)*g) / 255, (blend*Blue(c) + (255-blend)*b) / 255, (blend*Alpha(c) + (255-blend)*a) / 255))
EndIf
EndProcedure
Procedure LineAA(x0.i, y0.i, x1.i, y1.i, color.i)
; /* draws an anti-aliased line on background */
; adapted by djes upon Alois Zingl's work - http://members.chello.at/~easyfilter/bresenham.html
Define.i dx = Abs(x1 - x0), sx : If x0 < x1 : sx = 1 : Else : sx = -1 : EndIf
Define.i dy = Abs(y1 - y0), sy : If y0 < y1 : sy = 1 : Else : sy = -1 : EndIf
Define.i x2, e2, err = dx - dy; /* error value e_xy */
Define.i ed : If dx + dy = 0 : ed = 0 : Else : ed = Sqr(dx*dx + dy*dy) : EndIf
Define.i r = Red(color), g = Green(color), b=Blue(color), a=Alpha(color), c
Define.i blend
While #True ; pixel loop
If x0 >= 0 And y0 >= 0 And x0 < CanvasWidth-1 And y0 < CanvasHeight-1
blend = 255*Abs(err - dx+dy)/ed : c = Point(x0,y0)
Plot(x0, y0, RGBA((blend*Red(c) + (255-blend)*r) / 255, (blend*Green(c) + (255-blend)*g) / 255, (blend*Blue(c) + (255-blend)*b) / 255, (blend*Alpha(c) + (255-blend)*a) / 255))
EndIf
e2 = err : x2 = x0;
If (2*e2 >= -dx) ;{ /* x Step */
If (x0 = x1) : Break : EndIf
If (e2+dy < ed)
If x0 >= 0 And (y0+sy) >= 0 And x0 < CanvasWidth-1 And (y0+sy) < CanvasHeight-1
blend = 255*(e2+dy)/ed : c = Point(x0,y0+sy)
Plot(x0, y0+sy, RGBA((blend*Red(c) + (255-blend)*r) / 255, (blend*Green(c) + (255-blend)*g) / 255, (blend*Blue(c) + (255-blend)*b) / 255, (blend*Alpha(c) + (255-blend)*a) / 255))
EndIf
EndIf
err - dy : x0 + sx;
EndIf
If (2*e2 <= dy) ;{ /* y Step */
If (y0 = y1) : Break : EndIf
If (dx-e2 < ed)
If (x2+sx) >= 0 And (y0) >= 0 And (x2+sx) < CanvasWidth-1 And y0 < CanvasHeight-1
blend = 255*(dx-e2)/ed : c = Point(x2+sx,y0)
Plot(x2+sx,y0, RGBA((blend*Red(c) + (255-blend)*r) / 255, (blend*Green(c) + (255-blend)*g) / 255, (blend*Blue(c) + (255-blend)*b) / 255, (blend*Alpha(c) + (255-blend)*a) / 255))
EndIf
EndIf
err + dx : y0 + sy
EndIf
Wend
EndProcedure
Procedure PlotLineWidth(x0.i, y0.i, x1.i, y1.i, wd.f, color)
; /* draws an anti-aliased line of wd pixel width
; adapted by djes upon Alois Zingl's work - http://members.chello.at/~easyfilter/bresenham.html
Define.i dx, dy, sx, sy, err, e2, x2, y2 ; /* error value e_xy */
Define.d ed, dx2, dy2
dx = Abs(x1-x0) : sx = -1 : If x0 < x1 : sx = 1 : EndIf
dy = Abs(y1-y0) : sy = -1 : If y0 < y1 : sy = 1 : EndIf
err = dx-dy
dx2 = dx*dx : dy2 = dy*dy
If dx+dy = 0 : ed = 1 : Else : ed = Sqr(dx2+dy2) : EndIf
wd = (wd+1)/2
While #True ; /* pixel loop */
SetPixelAA(x0, y0, color, max(0, 255*(Abs(err-dx+dy)/ed-wd+1)));
e2 = err : x2 = x0;
If 2*e2 >= -dx ; /* x Step */
e2 + dy : y2 = y0
While e2 < ed*wd And (y1 <> y2 Or dx > dy); )
y2 + sy
SetPixelAA(x0, y2, color, max(0,255*(Abs(e2)/ed-wd+1)));
e2 + dx
Wend
If (x0 = x1) : Break : EndIf
e2 = err : err - dy : x0 + sx;
EndIf
If 2*e2 <= dy ; /* y Step */
e2 = dx-e2
While e2 < ed*wd And (x1 <> x2 Or dx < dy)
x2 + sx
SetPixelAA(x2, y0, color, max(0,255*(Abs(e2)/ed-wd+1)));
e2 + dy
Wend
If (y0 = y1) : Break : EndIf
err + dx : y0 + sy
EndIf
Wend
EndProcedure
;-------------------------------------------
; Curves routines For smooth mouse following
; by djes@free.fr in 2015
; spline between p0-p1, tangent with p1-p2
Procedure CurveStart( x0.d, y0.d, x1.d, y1.d, x2.d, y2.d, color = 0)
;Curve is between p1 and p0
vx.d = x1 - x0
vy.d = y1 - y0
d.d = VectorLength(vx, vy)
t2x.d = x2 - x1
t2y.d = y2 - y1
;tangeant
n2.d = VectorLength(t2x, t2y)
If n2 = 0 : n2 = 0.00001 : EndIf
f2.d = 1.0/n2 * #CurveDeviation
;minimal distance between p0 and p1
If d >= #CurveMinimalDistance And n2 > 1
d/5 ;subdivide curve into 5 pixels segments
interv.d = 1/d
u.d = 0
ox = x0 : oy = y0
While u < 1
;For i = 1 To d
x.d = x0 + u*vx - Sqr( Sin(u*#PI) * f2 ) * Tan(u) * t2x
y.d = y0 + u*vy - Sqr( Sin(u*#PI) * f2 ) * Tan(u) * t2y
;SetPixelAA(x, y, color)
LineAA(ox, oy, x, y, color)
ox = x : oy = y
u + interv
;Next i
Wend
LineAA(x, y, x1, y1, color)
Else
LineAA(x0, y0, x1, y1, color)
EndIf
EndProcedure
;Spline between p1-p2 with two tangeants given by p0-p1 and p2-p3
Procedure CurveMiddle( x0.d, y0.d, x1.d, y1.d, x2.d, y2.d, x3.d, y3.d, color = 0)
CompilerIf #DebugLevel = 2
LineXY(x1, y1, x2, y2, 0)
Line(x1-4, y1, 8, 1, RGB(0, 0, 255))
Line(x1, y1-4, 1, 8, RGB(0, 0, 255))
Line(x2-4, y2, 8, 1, RGB(0, 255, 0))
Line(x2, y2-4, 1, 8, RGB(0, 255, 0))
CompilerEndIf
;The curve is between p1 and p2
vx.d = x2 - x1
vy.d = y2 - y1
d.d = VectorLength(vx, vy)
;1st tangeant
t1x.d = x1 - x0
t1y.d = y1 - y0
n1.d = VectorLength(t1x, t1y)
If n1 = 0 : n1 = 0.00001 : EndIf
f1.d = 1.0/n1 * #CurveDeviation
;2st tangeant
t2x.d = x3 - x2
t2y.d = y3 - y2
n2.d = VectorLength(t2x, t2y)
If n2 = 0 : n2 = 0.00001 : EndIf
f2.d = 1.0/n2 * #CurveDeviation
Debug d
;minimal distance between p1 and p2
If d >= #CurveMinimalDistance And n1 > 1 And n2 > 1
d/5 ;subdivide curve into 5 pixels segments
interv.d = 1.0/d
u.d = 0
ox = x1 : oy = y1
While u < 1
;For i = 1 To cl
; x.d = x1 + u*vx + ( Sqr( Sin(u*#PI) *f1) * Pow( Sin((1-u)*#PI/2), 2) * t1x - Sqr( Sin(u*#PI) *f2) * Pow( Sin(u*#PI/2), 2) * t2x ) /2
; y.d = y1 + u*vy + ( Sqr( Sin(u*#PI) *f1) * Pow( Sin((1-u)*#PI/2), 2) * t1y - Sqr( Sin(u*#PI) *f2) * Pow( Sin(u*#PI/2), 2) * t2y ) /2
; x.d = x1 + u*vx + ( Sqr( Sin(u*#PI) *f1) * Tan( Sin((1-u)*#PI/2)) * t1x - Sqr( Sin(u*#PI) *f2) * Tan( Sin(u*#PI/2)) * t2x ) /2
; y.d = y1 + u*vy + ( Sqr( Sin(u*#PI) *f1) * Tan( Sin((1-u)*#PI/2)) * t1y - Sqr( Sin(u*#PI) *f2) * Tan( Sin(u*#PI/2)) * t2y ) /2
x.d = x1 + u*vx + ( Sqr( Sin(u*#PI) * f1) * Tan(1-u) * t1x - Sqr( Sin(u*#PI) * f2) * Tan(u) * t2x ) / 2
y.d = y1 + u*vy + ( Sqr( Sin(u*#PI) * f1) * Tan(1-u) * t1y - Sqr( Sin(u*#PI) * f2) * Tan(u) * t2y ) / 2
;SetPixelAA(x, y, 0)
LineAA(ox, oy, x, y, color)
ox = x : oy = y
u + interv
;Next i
Wend
LineAA(x, y, x2, y2, color)
Else
LineAA(x1, y1, x2, y2, color)
EndIf
EndProcedure
;Spline between p1-p2 with tangeant given by p0-p1
Procedure CurveEnd( x0.d, y0.d, x1.d, y1.d, x2.d, y2.d, color = 0)
;The curve is between p1 and p2
vx.d = x2 - x1
vy.d = y2 - y1
d.d = VectorLength(vx, vy)
;tangeant
t1x.d = x1 - x0
t1y.d = y1 - y0
n1.d = VectorLength(t1x, t1y)
If n1 = 0 : n1 = 0.00001 : EndIf
f1.d = 1.0/n1 * #CurveDeviation
;minimal distance between p1 and p2
If d >= #CurveMinimalDistance And n1 > 1
d/5 ;subdivide curve into 5 pixels segments
interv.d = 1/d
u.d = 0
ox = x1 : oy = y1
While u < 1
;For i = 1 To d
x.d = x1 + u*vx + Sqr( Sin(u*#PI) * f1) * Tan(1-u) * t1x
y.d = y1 + u*vy + Sqr( Sin(u*#PI) * f1) * Tan(1-u) * t1y
;SetPixelAA(x, y, color)
LineAA(ox, oy, x, y, color)
ox = x : oy = y
u + interv
;Next i
Wend
LineAA(x, y, x2, y2, color)
Else
LineAA(x1, y1, x2, y2, color)
EndIf
EndProcedure
;-------------------------------------------
; Wait mouse input movement sequence and draw
; corresponding plot/lines/curves
Procedure MouseMovementFollow(Null)
While #True
WaitSemaphore(Semaphore)
If StartDrawing(CanvasOutput(0))
LockMutex(Mutex)
n = ListSize(Coords())
While n > 0
n = 0
FirstElement(Coords())
SplineCoords(0)\x = Coords()\x : SplineCoords(0)\y = Coords()\y
If SplineCoords(0)\x = -2
;Sequence : -2 -> Nothing
Debug "Sequence : -2"
DeleteElement(Coords(), 1)
n = ListSize(Coords())
ElseIf SplineCoords(0)\x = -1
If NextElement(Coords())
SplineCoords(1)\x = Coords()\x : SplineCoords(1)\y = Coords()\y
If SplineCoords(1)\x = -2
;Sequence : -1-2 -> Nothing
Debug "Sequence : -1-2"
;Impossible... Anyway !
FirstElement(Coords()) : DeleteElement(Coords(), 1)
FirstElement(Coords()) : DeleteElement(Coords(), 1)
n = ListSize(Coords()) ;Continue processing
Else
If NextElement(Coords())
SplineCoords(2)\x = Coords()\x : SplineCoords(2)\y = Coords()\y
If SplineCoords(2)\x = -2
;Sequence : -1P-2 -> Pixel
Debug "Sequence : -1P-2"
SetPixelAA(SplineCoords(1)\x, SplineCoords(1)\y, RGB(255, 0, 0))
FirstElement(Coords()) : DeleteElement(Coords(), 1)
FirstElement(Coords()) : DeleteElement(Coords(), 1)
FirstElement(Coords()) : DeleteElement(Coords(), 1)
n = ListSize(Coords()) ;Continue processing
Else
If NextElement(Coords())
SplineCoords(3)\x = Coords()\x : SplineCoords(3)\y = Coords()\y
If SplineCoords(3)\x = -2
;Sequence : -1PP-2 -> Line
Debug "Sequence : -1PP-2"
LineAA(SplineCoords(1)\x, SplineCoords(1)\y, SplineCoords(2)\x, SplineCoords(2)\y, RGB(255, 0, 0))
FirstElement(Coords()) : DeleteElement(Coords(), 1)
FirstElement(Coords()) : DeleteElement(Coords(), 1)
FirstElement(Coords()) : DeleteElement(Coords(), 1)
FirstElement(Coords()) : DeleteElement(Coords(), 1)
n = ListSize(Coords()) ;Continue processing
Else
;Sequence : -1PPP -> Start of curve (only one tangeant)
Debug "Sequence : -1PPP"
CurveStart(SplineCoords(1)\x, SplineCoords(1)\y, SplineCoords(2)\x, SplineCoords(2)\y, SplineCoords(3)\x, SplineCoords(3)\y, RGB(255, 0, 0))
FirstElement(Coords()) : DeleteElement(Coords(), 1)
n = ListSize(Coords()) ;Continue processing
EndIf
EndIf
EndIf
EndIf
EndIf
EndIf
Else
If NextElement(Coords())
SplineCoords(1)\x = Coords()\x : SplineCoords(1)\y = Coords()\y
If SplineCoords(1)\x = -2
;Sequence : P-2 -> Pixel
Debug "Sequence : P-2"
;Impossible... Anyway !
SetPixelAA(SplineCoords(0)\x, SplineCoords(0)\y, RGB(255, 0, 0))
FirstElement(Coords()) : DeleteElement(Coords(), 1)
FirstElement(Coords()) : DeleteElement(Coords(), 1)
n = ListSize(Coords()) ;Continue processing
Else
If NextElement(Coords())
SplineCoords(2)\x = Coords()\x : SplineCoords(2)\y = Coords()\y
If SplineCoords(2)\x = -2
;Sequence : PP-2 -> Line
Debug "Sequence : PP-2"
LineAA(SplineCoords(0)\x, SplineCoords(0)\y, SplineCoords(1)\x, SplineCoords(1)\y, RGB(255, 0, 0))
FirstElement(Coords()) : DeleteElement(Coords(), 1)
FirstElement(Coords()) : DeleteElement(Coords(), 1)
FirstElement(Coords()) : DeleteElement(Coords(), 1)
n = ListSize(Coords()) ;Continue processing
Else
If NextElement(Coords())
SplineCoords(3)\x = Coords()\x : SplineCoords(3)\y = Coords()\y
If SplineCoords(3)\x = -2
;Sequence : PPP-2 -> End of curve (one tangeant)
Debug "Sequence : PPP-2"
CurveEnd(SplineCoords(0)\x, SplineCoords(0)\y, SplineCoords(1)\x, SplineCoords(1)\y, SplineCoords(2)\x, SplineCoords(2)\y, RGB(255, 0, 0))
FirstElement(Coords()) : DeleteElement(Coords(), 1)
FirstElement(Coords()) : DeleteElement(Coords(), 1)
FirstElement(Coords()) : DeleteElement(Coords(), 1)
FirstElement(Coords()) : DeleteElement(Coords(), 1)
n = ListSize(Coords()) ;Continue processing
Else
;Sequence : PPPP -> Full curve (two tangeants)
Debug "Sequence : PPPP"
CurveMiddle(SplineCoords(0)\x, SplineCoords(0)\y, SplineCoords(1)\x, SplineCoords(1)\y, SplineCoords(2)\x, SplineCoords(2)\y, SplineCoords(3)\x, SplineCoords(3)\y, RGB(255, 0, 0))
FirstElement(Coords()) : DeleteElement(Coords(), 1)
n = ListSize(Coords()) ;Continue processing
EndIf
EndIf
EndIf
EndIf
EndIf
EndIf
EndIf
Wend
UnlockMutex(Mutex)
CompilerIf #PB_Compiler_OS = #PB_OS_Linux
gdk_threads_enter_();
StopDrawing()
gdk_threads_leave_();
CompilerElse
StopDrawing()
CompilerEndIf
EndIf
Wend
EndProcedure
;---- Main
CreateThread(@MouseMovementFollow(), 0)
; Ouverture de la fenêtre
OpenWindow(0, 0, 0, 800, 600, "Drawing with curve approximation",#PB_Window_ScreenCentered|#PB_Window_SystemMenu)
CanvasWidth = 800 : CanvasHeight = 600
CanvasGadget(0, 0, 0, CanvasWidth, CanvasHeight)
lmb = #False : mmb = #False : rmb = #False
x = GetGadgetAttribute(0, #PB_Canvas_MouseX)
y = GetGadgetAttribute(0, #PB_Canvas_MouseY)
If y < 100 : y = 100 : EndIf
Repeat
Repeat
Event = WindowEvent()
Select Event
Case #PB_Event_Gadget
Select EventGadget()
Case 0
Select EventType()
Case #PB_EventType_LeftButtonDown
lmb = #True
LockMutex(Mutex)
AddElement(Coords())
Coords()\x = -1
;Signal to the drawing routine that we start a new curve
UnlockMutex(Mutex)
SignalSemaphore(Semaphore)
Case #PB_EventType_LeftButtonUp
lmb = #False
LockMutex(Mutex)
AddElement(Coords())
Coords()\x = -2
;Signal to the drawing routine that we end the curve
UnlockMutex(Mutex)
SignalSemaphore(Semaphore)
Case #PB_EventType_MiddleButtonDown
mmb = #True
Case #PB_EventType_MiddleButtonUp
mmb = #False
Case #PB_EventType_RightButtonDown
rmb = #True
Case #PB_EventType_RightButtonUp
rmb = #False
Case #PB_EventType_MouseWheel
tiks = GetGadgetAttribute(0, #PB_Canvas_WheelDelta)
Case #PB_EventType_MouseMove
x = GetGadgetAttribute(0, #PB_Canvas_MouseX)
y = GetGadgetAttribute(0, #PB_Canvas_MouseY)
If y < 100 : y = 100 : EndIf
EndSelect
EndSelect
Case #PB_Event_CloseWindow
End
EndSelect
Until Event = 0
If lmb
If ox <> x Or oy <> y
;Mouse moved
LockMutex(Mutex)
AddElement(Coords())
Coords()\x = x
Coords()\y = y
ox = x : oy = y
UnlockMutex(Mutex)
SignalSemaphore(Semaphore)
EndIf
EndIf
;- Main drawing
StartDrawing(CanvasOutput(0))
If rmb
Box(0, 0, 800, 600, #White)
EndIf
;-Spline test
;x1 = 200 : y1 = 200 : Plot(x1, y1, RGB(0, 0, 255))
;x2 = 200 : y2 = 250 : Plot(x2, y2, RGB(0, 0, 255))
;x3 = 350 : y3 = 250 : Plot(x3, y3, RGB(0, 0, 255))
;x4 = 400 : y4 = 250 : Plot(x4, y4, RGB(0, 0, 255))
;CurveStart(x1, y1, x2, y2, x, y, rgb(255, 0, 0))
;CurveMiddle(x, y, x2, y2, x3, y3, x4, y4, rgb(255, 0, 0))
;CurveEnd(x, y, x2, y2, x3, y3, rgb(255, 0, 0))
PlotLineWidth(500, 500, 10, 50, 5, RGB(55, 125, 25))
DrawText(0,5, "X=" + Str(x) + " Y=" + Str(y) + " " )
If lmb
DrawText(0,30, "Left button", RGB(255,0,0),RGB(255,255,0))
Else
DrawText(0,30, "Left button", RGB(255,255,255))
EndIf
If mmb
DrawText(150,30, "Middle button", RGB(255,0,0),RGB(255,255,0))
Else
DrawText(150,30, "Middle button", RGB(255,255,255))
EndIf
If rmb
DrawText(300,30, "Right button", RGB(255,0,0),RGB(255,255,0))
Else
DrawText(300,30, "Right button", RGB(255,255,255))
EndIf
If tiks <> 0
DrawText(450,30, "Scroll wheel", RGB(255,0,0),RGB(255,255,0))
Else
DrawText(450,30, "Scroll wheel", RGB(255,255,255))
EndIf
CompilerIf #PB_Compiler_OS = #PB_OS_Linux
gdk_threads_enter_();
StopDrawing()
gdk_threads_leave_();
CompilerElse
StopDrawing()
CompilerEndIf
ForEver
End
End ; All the opened windows are closed automatically by PureBasic