-- ************************************************** -- Provide Moho with the name of this script object -- ************************************************** ScriptName = "HS_Hypotrochoid" -- ************************************************** -- General information about this script -- ************************************************** HS_Hypotrochoid = {} function HS_Hypotrochoid:Name() return "Spirograph" end function HS_Hypotrochoid:Version() return "1.03a" end function HS_Hypotrochoid:IsBeginnerScript() return false end function HS_Hypotrochoid:Description() return "Hypotrochoids (Spirograph)" end function HS_Hypotrochoid:Creator() return "Hayasidist" end function HS_Hypotrochoid:UILabel() return HS_Hypotrochoid:Name() .. " Vers: " .. HS_Hypotrochoid:Version() end function HS_Hypotrochoid:ColorizeIcon() return true end --****************************** -- local utility subroutines -- ***************************** -- ************************************************************ -- Globals for use in the UI -- the values here are irrelevant -- -- ************************************************************ HS_Hypotrochoid.OuterCircle = 0 HS_Hypotrochoid.InnerCircle = 0 HS_Hypotrochoid.Offset = 0 HS_Hypotrochoid.Cusps = 1 --****************************** -- prefs -- ***************************** function HS_Hypotrochoid:LoadPrefs(prefs) self.OuterCircle = prefs:GetFloat("HS_Hypotrochoid.OuterCircle", 1) self.InnerCircle = prefs:GetFloat("HS_Hypotrochoid.InnerCircle", .5) self.Offset = prefs:GetFloat("HS_Hypotrochoid.Offset", .5) self.Cusps = prefs:GetFloat("HS_Hypotrochoid.Cusps", 1) end function HS_Hypotrochoid:SavePrefs(prefs) prefs:SetFloat("HS_Hypotrochoid.OuterCircle", self.OuterCircle) prefs:SetFloat("HS_Hypotrochoid.InnerCircle", self.InnerCircle) prefs:SetFloat("HS_Hypotrochoid.Offset", self.Offset) prefs:SetFloat("HS_Hypotrochoid.Cusps", self.Cusps) end function HS_Hypotrochoid:ResetPrefs() self.OuterCircle = 1 self.InnerCircle = .5 self.Offset = .5 self.Cusps = 1 end -- ************************************************** -- Recurring values -- -- ************************************************** local thisUUT = "B" -- ************************************************** -- Drawing controls for the current shape -- -- ************************************************** HS_Hypotrochoid.startPoint = 0 HS_Hypotrochoid.drawnPoints = 0 HS_Hypotrochoid.startVec = LM.Vector2:new_local() HS_Hypotrochoid.endVec = LM.Vector2:new_local() HS_Hypotrochoid.activeLayer = nil -- which layer did we last write the points on? --****************************** -- global utility subroutines -- ***************************** -- ************************************************** -- The guts of this script -- ************************************************** -- -- relevant / enabled?? -- function HS_Hypotrochoid:IsEnabled(moho) local mesh = moho:DrawingMesh() if (mesh == nil) then return false end if (moho.drawingLayer:CurrentAction() ~= "") then return false -- creating new objects in the middle of an action can lead to unexpected results end return true end function HS_Hypotrochoid:IsRelevant(moho) local rtx = false local vectorLayer = moho:LayerAsVector(moho.drawingLayer) local MohoVers if type (MOHO.ScriptInterface.AppVersion) == "function" then MohoVers = tonumber((string.gsub(moho:AppVersion(), "^(%d+)(%.%d+)(%..+)", "%1%2"))) else MohoVers = 0 end if MohoVers < 12 then rtx = false elseif (moho:DisableDrawingTools()) then rtx = false elseif (vectorLayer) then rtx = true end return rtx end local function makeLoop (moho, points) local mesh = moho:DrawingMesh() local n = mesh:CountPoints() mesh:SelectNone() local vec = LM.Vector2:new_local() local curv = .30 vec:Set(0,0) mesh:AddLonePoint(vec, 0) for i = 0, points-1 do mesh:AppendPoint(vec, 0) end mesh:WeldPoints(n + points, n, 0) for i = 0, points-1 do mesh:Point(n+i):SetCurvature(curv, 0) end mesh:SelectConnected() local shapeID = moho:CreateShape(true, false, 0) if (shapeID >= 0) then mesh:Shape(shapeID).fHasOutline = true end HS_Hypotrochoid.startPoint = n end function HS_Hypotrochoid:Draw(moho) if self.activeLayer ~= moho.drawingLayer then return false end local i, j, s, y moho.document:PrepUndo(moho.drawingLayer) local drawingFrame = moho.drawingFrame local ptX = {} local ptY = {} local theta = 0 local step = 2*math.pi/self.drawnPoints self.InnerCircle = self.OuterCircle / self.Cusps -- (a-b)*COS(I60)+h*(COS(((a-b)/b)*I60)) -- (a-b)*SIN(I60)-h*(SIN(((a-b)/b)*I60)) for i = 0, self.drawnPoints - 1 do ptX[i] = (self.OuterCircle-self.InnerCircle)*math.cos(theta) + self.Offset*(math.cos(((self.OuterCircle-self.InnerCircle)/self.InnerCircle)*theta)) ptY[i] = (self.OuterCircle-self.InnerCircle)*math.sin(theta) - self.Offset*(math.sin(((self.OuterCircle-self.InnerCircle)/self.InnerCircle)*theta)) theta = theta + step end local xLow = math.min(table.unpack(ptX)) local xHi = math.max(table.unpack(ptX)) local yLow = math.min(table.unpack(ptY)) local yHi = math.max(table.unpack(ptY)) local Pt local mesh = moho:DrawingMesh() local xScale = (self.endVec.x-self.startVec.x)/ (xHi - xLow) local xOffset = (self.endVec.x+self.startVec.x)/2 local yScale = (self.endVec.y-self.startVec.y) / (yLow - yHi) local yOffset = self.endVec.y - (yScale * yLow) for i = 0, self.drawnPoints - 1 do local pt = mesh:Point(self.startPoint + i) pt.fPos.x = xOffset + (xScale * ptX[i]) pt.fPos.y = yOffset + (yScale * ptY[i]) end mesh:SelectConnected() moho:AddPointKeyframe(drawingFrame) moho:UpdateSelectedChannels() end --***************************** -- mouse actions --***************************** function HS_Hypotrochoid:OnMouseDown(moho, mouseEvent) local mesh = moho:DrawingMesh() if (mesh == nil) then return end self.activeLayer = moho.drawingLayer moho.document:PrepUndo(moho.drawingLayer) moho.document:SetDirty() -- -- to get a point at the "rose" intersections - the number of points in each cusp depends on the number of cusps -- {9,14,12,12,10,12,14,16,18,10,22,12,26,14,30} points for -- {3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15,16,17} cusps -- this might be useful to try to get a filled shape without holes, but it's a real struggle to create such a shape... so for now it's fixed local ptsPerCusp = 12 self.drawnPoints = math.min(360, ptsPerCusp*HS_Hypotrochoid.Cusps) makeLoop (moho, self.drawnPoints) mouseEvent.view:DrawMe() end function HS_Hypotrochoid:OnMouseMoved(moho, mouseEvent) local mesh = moho:DrawingMesh() if (mesh == nil) then return end self.startVec:Set(mouseEvent.drawingStartVec) self.endVec:Set(mouseEvent.drawingVec) if (moho.gridOn) then moho:SnapToGrid(self.startVec) moho:SnapToGrid(self.endVec) end if mouseEvent.shiftKey then -- constrain width = height if (self.endVec.x > self.startVec.x) then if (self.endVec.y > self.startVec.y) then self.endVec.y = self.startVec.y + (self.endVec.x - self.startVec.x) else self.endVec.y = self.startVec.y - (self.endVec.x - self.startVec.x) end else if (self.endVec.y > self.startVec.y) then self.endVec.y = self.startVec.y - (self.endVec.x - self.startVec.x) else self.endVec.y = self.startVec.y + (self.endVec.x - self.startVec.x) end end end if mouseEvent.altKey then -- draw from center point self.startVec.x = self.startVec.x - (self.endVec.x - self.startVec.x) self.startVec.y = self.startVec.y - (self.endVec.y - self.startVec.y) end self:Draw(moho) mouseEvent.view:DrawMe() end function HS_Hypotrochoid:OnMouseUp(moho, mouseEvent) moho:UpdateUI() -- self.startVec:Set(mouseEvent.drawingStartVec) -- self.endVec:Set(mouseEvent.drawingVec) end local function OnKeyDownNew (moho, keyEvent) local mesh = moho:DrawingMesh() if (mesh == nil) then return false end HS_Hypotrochoid.activeLayer = nil -- inactive until mouse click for draw mesh:SelectNone() return true end local function OnKeyDownDelete (moho, keyEvent) local mesh = moho:DrawingMesh() if (mesh == nil) then return false end local deleted = false if moho:CountSelectedPoints() > 0 then moho.document:PrepUndo(moho.drawingLayer) moho.document:SetDirty() mesh:DeleteSelectedPoints() deleted = true end return deleted end function HS_Hypotrochoid:OnKeyDown(moho, keyEvent) local keyDispatch = { {LM.GUI.KEY_DELETE, OnKeyDownDelete}, {LM.GUI.KEY_BACKSPACE, OnKeyDownDelete}, {LM.GUI.KEY_ESCAPE, OnKeyDownNew}, {27, OnKeyDownNew}, -- the escape key {LM.GUI.KEY_BIND, OnKeyDownNew} } local updateView = false --[[ local s = HS_Test_harness:diagnoseModifierKeys (keyEvent.ctrlKey, keyEvent.shiftKey, keyEvent.altKey) HS_Test_harness:diagnosePrint (thisUUT, "key code", keyEvent.keyCode, "mod keys", s) ]] if keyEvent.ctrlKey then -- HS_Test_harness:diagnosePrint (thisUUT, "using factory point tool") LM_SelectPoints:OnKeyDown(moho, keyEvent) return end local i for i = 1, #keyDispatch do if keyEvent.keyCode == keyDispatch[i][1] then updateView = keyDispatch[i][2] (moho, keyEvent) break end end if updateView then -- HS_Test_harness:diagnosePrint (thisUUT, "updating view") moho:UpdateSelectedChannels() keyEvent.view:DrawMe() end end -- ************************************************** -- ************************************************** -- ************************************************** -- *************************************************************************************************************************************** UI -- ************************************************** -- ************************************************** -- Tool options - data values -- ************************************************** local presetBase = MOHO.MSG_BASE + 20 HS_Hypotrochoid.PRESET = presetBase + 1 HS_Hypotrochoid.ALT_PRESET = presetBase - 1 HS_Hypotrochoid.OUTER = MOHO.MSG_BASE + 110 HS_Hypotrochoid.INNER = MOHO.MSG_BASE + 120 HS_Hypotrochoid.OFFSET = MOHO.MSG_BASE + 130 HS_Hypotrochoid.CUSPS = MOHO.MSG_BASE + 140 --[[ local function doHypoC () HS_Hypotrochoid.OuterCircleIn:Enable(false) HS_Hypotrochoid.OffsetIn:Enable(false) HS_Hypotrochoid.OuterCircle = 2 * HS_Hypotrochoid.Cusps -- HS_Hypotrochoid.InnerCircle (as calc) HS_Hypotrochoid.Offset = HS_Hypotrochoid.OuterCircle / HS_Hypotrochoid.Cusps -- HS_Hypotrochoid.Cusps (as entered) return local function doEllipse () HS_Hypotrochoid.OuterCircleIn:Enable(false) HS_Hypotrochoid.OuterCircle = 2 * HS_Hypotrochoid.Cusps -- HS_Hypotrochoid.InnerCircle (as calc) -- HS_Hypotrochoid.Offset (as entered) -- HS_Hypotrochoid.Cusps (as entered) return local function doRose () HS_Hypotrochoid.OffsetIn:Enable(false) -- HS_Hypotrochoid.OuterCircle (as entered) -- HS_Hypotrochoid.InnerCircle (as calc) HS_Hypotrochoid.Offset = HS_Hypotrochoid.OuterCircle - HS_Hypotrochoid.InnerCircle -- HS_Hypotrochoid.Cusps (as entered) return local function doEntry () HS_Hypotrochoid.OuterCircleIn:Enable(true) HS_Hypotrochoid.OffsetIn:Enable(true) HS_Hypotrochoid.CuspsIn:Enable(true) return ]] local presets = { {name="hypocycloid", outer=6, cusps=3, offset=2, fnc=doHypoC}, {name="ellipse", outer=4, cusps=2, offset=.5, fnc=doEllipse}, {name="rose (12)", outer=4, cusps=12, offset=(4 - 1/3), fnc=doRose}, {name="data entry", outer=nil, cusps=nil, offset=nil, fnc=doEntry} -- end marker } local presetIx = #presets-1 -- counts from 0 local entered_Outer = HS_Hypotrochoid.OuterCircle local entered_Inner = HS_Hypotrochoid.InnerCircle local entered_Offset = HS_Hypotrochoid.Offset local entered_Cusps = HS_Hypotrochoid.Cusps -- ************************************************** -- Create and respond to tool's UI -- ************************************************** function HS_Hypotrochoid:DoLayout(moho, layout) local i self.butt = LM.GUI.Button(presets[presetIx+1].name, self.PRESET) self.butt:SetAlternateMessage(self.ALT_PRESET) layout:AddChild(self.butt) -- self.OuterCircleText = LM.GUI.StaticText("Outer Circle") -- layout:AddChild(self.OuterCircleText, LM.GUI.ALIGN_RIGHT) self.OuterCircleIn = LM.GUI.TextControl (0, "0.0000", self.OUTER, LM.GUI.FIELD_UFLOAT, "Outer Circle") layout:AddChild(self.OuterCircleIn, LM.GUI.ALIGN_RIGHT) -- self.CuspsText = LM.GUI.StaticText("Cusps (loops)") -- layout:AddChild(self.CuspsText, LM.GUI.ALIGN_RIGHT) self.CuspsIn = LM.GUI.TextControl (0, "00", self.CUSPS, LM.GUI.FIELD_UINT, "Cusps (loops)") layout:AddChild(self.CuspsIn, LM.GUI.ALIGN_RIGHT) self.CuspsIn:SetWheelInc(1) --[[ self.InnerCircleText = LM.GUI.StaticText("Inner Circle") layout:AddChild(self.InnerCircleText, LM.GUI.ALIGN_RIGHT) self.InnerCircleIn = LM.GUI.TextControl (0, "0.0000", self.INNER, LM.GUI.FIELD_FLOAT) layout:AddChild(self.InnerCircleIn, LM.GUI.ALIGN_RIGHT) ]] -- self.OffsetText = LM.GUI.StaticText("Offset") -- layout:AddChild(self.OffsetText, LM.GUI.ALIGN_RIGHT) self.OffsetIn = LM.GUI.TextControl (0, "0.0000", self.OFFSET, LM.GUI.FIELD_FLOAT, "Offset") layout:AddChild(self.OffsetIn, LM.GUI.ALIGN_RIGHT) end function HS_Hypotrochoid:UpdateWidgets(moho) self.OuterCircleIn:SetValue(self.OuterCircle) -- self.InnerCircleIn:SetValue(self.InnerCircle) self.OffsetIn:SetValue(self.Offset) self.CuspsIn:SetValue(self.Cusps) local function setWheel (x) x = math.abs(x) local i = math.floor(math.log (x, 10)) if i < -1 then i = -2 else i = i - 1 end -- HS_Test_harness:diagnosePrint (thisUUT, "set Wheel", x, math.log (x, 10), i) return 10^i end -- HS_Test_harness:diagnosePrint (thisUUT, "for set Wheel", self.OuterCircle, self.Offset) self.OuterCircleIn:SetWheelInc(setWheel(self.OuterCircle)) -- self.InnerCircleIn:SetWheelInc(self.wheel) self.OffsetIn:SetWheelInc(setWheel(self.Offset)) -- self.CuspsIn:SetWheelInc(1) self.butt:SetLabel(presets[presetIx+1].name, false) self.butt:Redraw() -- HS_Test_harness:diagnosePrint (thisUUT, "update widgets", wheelIx, wheelInc[wheelIx]) HS_Hypotrochoid.OuterCircleIn:Redraw() -- HS_Hypotrochoid.InnerCircleIn:Redraw() HS_Hypotrochoid.OffsetIn:Redraw() HS_Hypotrochoid.CuspsIn:Redraw() end function HS_Hypotrochoid:HandleMessage(moho, view, msg) if (msg == self.OUTER) then self.OuterCircle = self.OuterCircleIn:FloatValue() -- elseif (msg == self.INNER) then --- self.InnerCircle = self.InnerCircleIn:FloatValue() elseif (msg == self.OFFSET) then self.Offset = self.OffsetIn:FloatValue() elseif (msg == self.CUSPS) then self.Cusps = self.CuspsIn:IntValue() if self.Cusps < 2 then self.Cusps = 2 end elseif (msg == self.PRESET) or (msg == self.ALT_PRESET) then presetIx = math.fmod(presetIx + #presets + (msg-presetBase), #presets) if presets[presetIx+1].outer then -- if it's data entry then restore the entered values and put in the presets entered_Outer = self.OuterCircle entered_Inner = self.InnerCircle entered_Offset = self.Offset entered_Cusps = self.Cusps self.OuterCircle = presets[presetIx+1].outer -- self.InnerCircle (calculated in mainline) self.Offset = presets[presetIx+1].offset self.Cusps = presets[presetIx+1].cusps else self.OuterCircle = entered_Outer self.InnerCircle = entered_Inner self.Offset = entered_Offset self.Cusps = entered_Cusps end end self:UpdateWidgets(moho) HS_Hypotrochoid:Draw(moho) end
Spirograph
Listed
Author: hayasidist
View Script
Script type: Tool
Uploaded: Oct 23 2022, 14:54
Last modified: May 12 2023, 04:56
Draws a path similarly to drawing with a spirograph.
A hypotrochoid is generated by a fixed point on a circle rolling inside a fixed circle. This script allows the user to select the radius of the outer (fixed) circle, the number of nodes (i.e. how many times the inner circle will roll round the outer circle) and how far off centre the "pen" is.
The shape is created on frame 0. It can be modified after it is created either on frame 0 or in the timeline by changing the input parameters.
This is a preliminary version. The pace of updates will depend on feedback on its usefulness!
Here's a video that uses the script to generate many of the shapes.
2023-05-12 Vers 1.03a - Minor update to remove an unnecessary global variable. No functional changes.
This script, and all other scripts on this site are distributed as free software under the GNU General Public License 3.0 or later.
Downloads count: 388

Spirograph
Listed
Author: hayasidist
View Script
Script type: Tool
Uploaded: Oct 23 2022, 14:54
Last modified: May 12 2023, 04:56
Draws a path similarly to drawing with a spirograph.
A hypotrochoid is generated by a fixed point on a circle rolling inside a fixed circle. This script allows the user to select the radius of the outer (fixed) circle, the number of nodes (i.e. how many times the inner circle will roll round the outer circle) and how far off centre the "pen" is.
The shape is created on frame 0. It can be modified after it is created either on frame 0 or in the timeline by changing the input parameters.
This is a preliminary version. The pace of updates will depend on feedback on its usefulness!
Here's a video that uses the script to generate many of the shapes.
2023-05-12 Vers 1.03a - Minor update to remove an unnecessary global variable. No functional changes.
The shape is created on frame 0. It can be modified after it is created either on frame 0 or in the timeline by changing the input parameters.
This is a preliminary version. The pace of updates will depend on feedback on its usefulness!
Here's a video that uses the script to generate many of the shapes.
2023-05-12 Vers 1.03a - Minor update to remove an unnecessary global variable. No functional changes.
This script, and all other scripts on this site are distributed as free software under the GNU General Public License 3.0 or later.
Downloads count: 388