How curves are calculated in Moho - a sample code

Moho allows users to write new tools and plugins. Discuss scripting ideas and problems here.

Moderators: Víctor Paredes, Belgarath, slowtiger

Post Reply
User avatar
Fazek
Posts: 246
Joined: Thu Apr 13, 2006 1:37 pm
Location: Hungary
Contact:

How curves are calculated in Moho - a sample code

Post by Fazek »

Finally I've discovered how Moho calculates the curves on the vector layers. First of all, the curves are simplified cubic Bezier curves. This means a curve has four control points, cp0,cp1,cp2,cp3. The cp0 and cp3 are the endpoints of the curve and these points are standing on the curve. These are the mesh Point()s so you can see and modify the position of these points with the tools in Moho. The cp1 and cp2 are the internal control points, and these are not on the curve (except if the points are co-linear, then the curve is a line). These internal control points are not visible and calculated from the positions of the actual and neighbouring Mesh points and the magic "Curvature" value of the points.

The key to understand how a Moho curve transformed into a regular cubic Bezier curve, is the way Moho calculates these internal points:

On the curve the point P1 has two neighbour points, P0 and P2.

P1 has two internal control points on each side, one for the P0-P1 curve and one for the P1-P2. To calculate the position of these points, get the vector between the P0 and P2 neighbours, normalize it (rescale its length to 1) then multiply it with the P1 point's Curvature value.

N= Norm(P2-P0) * Curvature(P1)

For the P0-P1 curve, calculate the length of the P1-P0 vector, and multiply the normalized "direction" vector with it:

CP01= P1 - N * Mag(P1 - P0)

And for the P1-P2 curve, calculate the length of the P2-P1 vector, and multiply the
normalized "direction" vector with it:

CP12= P1 + N * Mag(P2 - P1)

At the ends of an open curve, where only one neighbour exists for the endpoint, use the endpoint itself instead of the missing neighbour, ie. P0= P1 or P2= P1.

The sample below is a tool's DrawMe() callback function. You can copy it into any of the existing tools to see the internal control point handles and the calculated curves. It calculates the things in a bit "optimized" way, but the calculation basically the same as above.

Code: Select all

function FA_TryMe:DrawMe(moho, view)
	local mesh = moho:Mesh()
	if (mesh == nil) then return end

	local gfx= view:Graphics()
	local matrix= LM.Matrix:new_local()

	moho.layer:GetFullTransform(moho.frame, matrix, moho.document)
	gfx:Push()
	gfx:ApplyMatrix(matrix)

	gfx:SetColor(255, 0, 0)

	local cveN= mesh:CountCurves()

	for i= 0, cveN - 1 do
		local cve= mesh:Curve(i)
		local ptN= cve:CountPoints()

		if (ptN > 1) then
			local oldPt= cve:Point(0)
			local oldCurvature= cve:GetCurvature(0, moho.frame)

		--cp0..cp3 are the cubic Bezier curve points

			local cp0x= oldPt.fPos.x
			local cp0y= oldPt.fPos.y

			local cp1x,cp1y,cp2x,cp2y,cp3x,cp3y, len

			local newPt= cve:Point(1)
			local newCurvature= cve:GetCurvature(1, moho.frame)

		--setting the neighbours of the first point

			local prevPt= oldPt
			local handle= LM.Vector2:new_local()
			local vec= LM.Vector2:new_local()

			if (cve.fClosed) then
				prevPt= cve:Point(ptN - 1)
			end

		--first point's parameters

			handle:Set(newPt.fPos.x - prevPt.fPos.x, newPt.fPos.y - prevPt.fPos.y)
			handle:NormMe()

			vec:Set(newPt.fPos.x - oldPt.fPos.x, newPt.fPos.y - oldPt.fPos.y)
			local oldLen= vec:Mag()

			gfx:DrawFatMarker(cp0x,cp0y, 4)

		--CountSegments() is ptN-1 for open curves...

			for j= 2, cve:CountSegments() + 1 do

		--calculating the inner control points

				cp3x= newPt.fPos.x
				cp3y= newPt.fPos.y

		--distance to the previous neighbour: control point #1

				len= oldLen * oldCurvature

				cp1x= cp0x + handle.x * len
				cp1y= cp0y + handle.y * len

				len= oldLen * newCurvature

		--administration and getting the next point

				prevPt= oldPt
				oldPt= newPt
				oldCurvature= newCurvature

				if (j < ptN) then
					newPt= cve:Point(j)
					newCurvature= cve:GetCurvature(j, moho.frame)

				--the next neighbour vector

					vec:Set(newPt.fPos.x - oldPt.fPos.x, newPt.fPos.y - oldPt.fPos.y)
					oldLen= vec:Mag()

				elseif (cve.fClosed) then
			--closed curve, last two segments
						newPt= cve:Point(j - ptN)
						newCurvature= cve:GetCurvature(j - ptN, moho.frame)
				end

			--open curve, last segment: keep the original newPt for the handle vector

			--handle of the endpoint: normalized vector between the neighbours

				handle:Set(newPt.fPos.x - prevPt.fPos.x, newPt.fPos.y - prevPt.fPos.y)
				handle:NormMe()

		--distance to the next neigbour: control point #2

				cp2x= cp3x - handle.x * len
				cp2y= cp3y - handle.y * len

		--drawing the control handles

				gfx:DrawLine(cp0x,cp0y,cp1x,cp1y)
				gfx:DrawLine(cp2x,cp2y,cp3x,cp3y)

		--drawing the Bezier curve, based on http://en.wikipedia.org/wiki/B%C3%A9zier_curve

				local cx = 3.0 * (cp1x - cp0x)
				local bx = 3.0 * (cp2x - cp1x) - cx
				local ax = cp3x - cp0x - cx - bx

				local cy = 3.0 * (cp1y - cp0y)
				local by = 3.0 * (cp2y - cp1y) - cy
				local ay = cp3y - cp0y - cy - by
				local oldX = nil
				local oldY

				for t= 0, 1, 0.05 do
					local tSquared = t * t
					local tCubed = tSquared * t
					local resultX = (ax * tCubed) + (bx * tSquared) + (cx * t) + cp0x
					local resultY = (ay * tCubed) + (by * tSquared) + (cy * t) + cp0y

					if (oldX) then
						gfx:DrawLine(oldX,oldY,resultX,resultY)
					end
					oldX= resultX
					oldY= resultY
				end

				cp0x= cp3x
				cp0y= cp3y
			end
			gfx:DrawFatMarker(cp0x,cp0y, 4)
		end
	end
        gfx:Pop()
end
- - - Fazek
User avatar
Fazek
Posts: 246
Joined: Thu Apr 13, 2006 1:37 pm
Location: Hungary
Contact:

Post by Fazek »

Based on this knowledge, now I am in a development of an algorythm to simplify segment sequences (very similar to the one used in the freehand drawing tool), with minimizing the control points and calculating the optimal curvature value for each points. I am trying these algorythms to see which is better:

- a modified version of the freehand tool's technique, it calculates the curvature from the angle between the neighbouring vectors, and finds "required" points.

- a "brute force" algorythm to try all possible combinations of positions and curvatures. I hope this would show better results, but it will be slower.

- Maybe a combination of the two techniques.

I think a Moho-specific algorythm required and I can't use algorythms already made for Bezier curves (but I can use similar techniques). Moho's curves are less local than regular Bezier curves (a point determines the shape beyond the two neighbouring curves too).
- - - Fazek
User avatar
Fazek
Posts: 246
Joined: Thu Apr 13, 2006 1:37 pm
Location: Hungary
Contact:

Post by Fazek »

Now I uploaded a toy if you want to try how my Bezier approximation works. You can find the actual version at:
http://www.firkafilm.com/moho/fa_bezier_simulator.lua

Put this file to the scripts/tool directory. It is just a rough version and I think it will change a lot until it becomes perfect. You can see a Bezier curve with red line and its Moho-style approximation with black color. The dots on the black lines are the usual mesh points. The big dots are the "requied" points, marked by the program. You can move the Bezier control points and even you can add new points if you click on the curve.

The regular Bezier curves has the advantage that they are simple mathematical functions, so it is possible to make equations to calculate the possible positions of the control points. But the Moho version is a bit more difficult and the above way to calculate it is not elegant. Now I am trying to make a simple mathematical function for the whole calculation (maybe LM using it to calculate the curve faster?). Then I can see how to find the best matching curvatures and positions for the internal points between the required points.

TODO:

- better way to calculate the extra points between "required" points,
- better way to calculate the curvature on points,
- how to make it faster.

I accept any ideas, algorythms to improve this.
- - - Fazek
User avatar
Fazek
Posts: 246
Joined: Thu Apr 13, 2006 1:37 pm
Location: Hungary
Contact:

Post by Fazek »

- - - Fazek
User avatar
Fazek
Posts: 246
Joined: Thu Apr 13, 2006 1:37 pm
Location: Hungary
Contact:

Post by Fazek »

I have another toy to download (into scripts/tool):
http://www.firkafilm.com/moho/fa_moho_to_bezier.lua
I extended LM's original Curvature calculation into a 4-parameters/point version, what is compatible with the cubic Bezier curves and it uses the original Curvature value, as one of these parameters. By default, it calculates exactly the same curves as LM's original.

LM's Curvature is very interesting because it seems quite stable against the usual transformations, like point translation, scaling or shearing. My new solution is not so good,
and I will find another solution for the perpendicular component of the vectors (perhaps based on angles and rotation?).

But I think it shows the possibility and I hope LM will think about the using of something similar in Moho.
- - - Fazek
User avatar
Rai López
Posts: 2235
Joined: Sun Aug 08, 2004 1:41 pm
Location: Spain
Contact:

Post by Rai López »

...WOW! I LOVE (too) this last new toy Fazek! I think you are near to get the precission and automation that could conciliate the two ways of think about this old issue... I hope your work and dedication be considerer and some day we can see this compatible way of work inside the own Moho not only to play with it... THANKS for all! :)

EDIT: BTW, I tried in all the ways but, in Moho 5.4 and Windows XP, I could not (never) make work your first lines of code and I still don't know why...
User avatar
Fazek
Posts: 246
Joined: Thu Apr 13, 2006 1:37 pm
Location: Hungary
Contact:

Post by Fazek »

You must modify the FA_TryMe:DrawMe into the name of the actual tool, like LM_BindPoints:DrawMe. And of course, there must not be another DrawMe function in that file (BindPoints is a good choice to try).
- - - Fazek
User avatar
Rai López
Posts: 2235
Joined: Sun Aug 08, 2004 1:41 pm
Location: Spain
Contact:

Post by Rai López »

EY/HI! A little related question... somebody knows how could I transform this kind of graphic Tool Scripts into Embedded Scripts? I need to draw marks here and there in the Main Window by embedded scripts and I have been trying to put this codes after the typical "function LayerScript(moho)" but I only get errors (especially related with that "view:Graphics()" function) and... well, it's clear that I'm missing something cause it shouldn't be so difficult, is it? Well, the worse thing is that there are no embedded examples (as far as I know) to get something like this anywhere and I'd be veeery grateful for any help :)
User avatar
Fazek
Posts: 246
Joined: Thu Apr 13, 2006 1:37 pm
Location: Hungary
Contact:

Post by Fazek »

Hello Ramon,

I think you cannot draw directly from the LayerScript. Maybe AS calls it not only when drawing to the editor window, but when rendering, creating SWF file etc. So sometimes simply there is no view or view:Graphics() at all. You should try the following instead: Create a vector layer, for example with one or more cross-hairs images, and move the mesh points wherever you want with the layerscript.
- - - Fazek
User avatar
Rasheed
Posts: 2008
Joined: Tue May 17, 2005 8:30 am
Location: The Netherlands

Post by Rasheed »

Remember, Ramón, menu scripts and tools scripts are meant to be used for user actions (they let the user do something) and layer scripts are meant to be used to respond to what goes on in a layer. In short, menu and tool scripts act, layer scripts react.

This means the strength of menu and tool scripts are control, and of menu scripts response. A good example of a layer script could be to have a floor or a wall (in general: a boundary); nothing in the layer can go beyond this boundary. Another example could be gravity, where the position between keys is calculated on the basis off a down force. I'm curious if it is possible to program these kinds of physics into layers.

Of course, unlike real life, anvils in cartoons fall faster than anything else, but that be cartoon physics for ye! ;)

Cool piece of deduction there, Fazek!
User avatar
Rai López
Posts: 2235
Joined: Sun Aug 08, 2004 1:41 pm
Location: Spain
Contact:

Post by Rai López »

...D'OH! :( Well, THANK YOU very much to both, that "view" error was making me mad and I didn't know what more could I try, lately I started to afraid it was not possible, yes... a pity cause Embedded Scripts could be a very good way to show interesting information on screen not only for manipulate things...

But well, it was better be sure and, as Fazek said, think in other possibilities instead, the problem is that swap information between different layers always can bring problems and/or instability, and in this case I'd need to draw a lot of marks based in the number of points/curves that even could change in number, hmmm, not impossible but I should study it and I'll continue thinking on your proporsal cause, although I have used similar solutions in the past (for other pourposes), never had thought on it for use with Embedded Scripts, so... I'll take into account! :)

Now... well, only for be totally sure, do you know if could be some way to deceive/cheat AS with this? I mean, making a Embedded script interact with some function writed in the "...Moho/scripts/utility" folder or interacting with some tool or something? Well, maybe a silly thing, but I had to wonder only for curiosity :roll: ...THANKS!!!

PS: I'd like to see some Embedded Script with codes for something like gravity or boundaries in Moho too although it be only for fun :)
User avatar
Fazek
Posts: 246
Joined: Thu Apr 13, 2006 1:37 pm
Location: Hungary
Contact:

Post by Fazek »

An idea: if you want to view something on the editor window, you can override the DrawMe() functions of EVERY tools. You just parse through the _G global table, and find function type DrawMe() table elements. You can surely recognise a valid tool table if you look for other function names too. Whenever you find a table.DrawMe element, you can replace it with your own function. Of course, at the end of your function, you must call the original function, so save its value somewhere. You must build a failsafe mechanism to handle multiple (embedded) redirections and avoid dead loops. That is specially important, because LM's tools are calling other tools' DrawMe() functions.

It is something similar how viruses are working...
- - - Fazek
User avatar
Rai López
Posts: 2235
Joined: Sun Aug 08, 2004 1:41 pm
Location: Spain
Contact:

Post by Rai López »

Oh! Thank you very macho! :) But it seems to me that I can't understand that ( _G global table? :roll: ) ...Uf, a pity! But well...

...The last thing I'm trying is to write that Display Functions into my own "RL_SharedUtils.lua" and add entries into that tools where I need those View functions, much more practice and quick for updates and in all repects that work into separate tools and his separated sections with duplicated codes... And well, I know that I'd prefer can work only into Embeded Scripts cause I'd have all the controll in the same place and I know too this "solution" is nothing new, but I never had used this "utility folder" posibilities and is, at least, like a great relief... Anyway I'll continue thinking...

Well, but (of course) THANK YOU again! Cause for me all advices/ideas/sugestions/knowledges on this area are always welcomed :) ...CIAO!
User avatar
Fazek
Posts: 246
Joined: Thu Apr 13, 2006 1:37 pm
Location: Hungary
Contact:

Post by Fazek »

_G is the Lua table of the global variables. That means the name "variable" and "_G.variable" means the same (because of this, _G._G is _G). To parse the variables in a table you can use a cycle like this:

Code: Select all

for name, value in table do
   if (type(value) == "function") then
         ....
   end
end
For more examples, see fa_printvalue.lua in my "scriptwriting" tools.
- - - Fazek
Post Reply