ScriptName = 'WP_platterControl' -- ************************************************** -- Control a platter with speed scaling -- Created by Maarten de Haas, Wigglepixel -- ************************************************** --[[ ***** Licence & Warranty ***** Copyright 2023/2024 - Maarten de Haas / Wigglepixel Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 Conditions require preservation of copyright and license notices. You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works. You can: Use - use/reuse freely, even commercially Adapt - remix, transform, and build upon for any purpose Share - redistribute the material in any medium or format Adapt / Share under the following terms: Attribution - You must give appropriate credit, provide a link to the Apache 2.0 license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use. Licensed works, modifications and larger works may be distributed under different License terms and without source code. Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. The Developer Maarten de Haas, Wigglepixel will not be liable for any direct, indirect or consequential loss of actual or anticipated - data, revenue, profits, business, trade or goodwill that is suffered as a result of the use of the software provided. ]] -- ************************************************** -- General information about this script -- ************************************************** WP_platterControl = {} WP_platterControl.fps = nil WP_platterControl.WP_UTILS_REQUIRED_VERSION = { 1, 3, 0 } WP_platterControl.HIDDEN_BONE_NAME = '--- ' WP_platterControl.STR_ARR_TO_CONCAT_STRING_DIVIDER = '%%WPDIV%%' -- FOR JOINING ARRAY STRINGS INTO A SINGLE STRING FOR PREFS STORAGE -- SPEED DIAL DIRECTION MODES WP_platterControl.SPEED_DIAL_MODE_PRESET_LEFT = 0 WP_platterControl.SPEED_DIAL_MODE_PRESET_UP = 1 WP_platterControl.SPEED_DIAL_MODE_PRESET_RIGHT = 2 WP_platterControl.SPEED_DIAL_MODE_PRESET_DOWN = 3 -- CYCLES MODES WP_platterControl.CYCLES_MODE_SINGLE_CYCLE = 0 -- Single cycle in repeat (with pingpong option) WP_platterControl.CYCLES_MODE_MULTI_CYCLES_UNLIMITED = 1 -- Endlessly Increasing cycles WP_platterControl.CYCLES_MODE_MULTI_CYCLES_LIMITED = 2 -- Multiple cycles in repeat (with cycle count setting, with pingpong option, with randomize option) WP_platterControl.CYCLES_MODE_MULTI_CYCLES_SEQUENCE = 3 -- Cycle sequence (with sequence numbers text, pingpong and randomize options) function WP_platterControl:Name() return 'Platter Control' end function WP_platterControl:Version() return '1.3.0' end function WP_platterControl:UILabel() return 'Platter Control' end function WP_platterControl:Description() return 'Control Platter Speed by Bone Angle' end function WP_platterControl:Creator() return 'Maarten de Haas, Wigglepixel' end function WP_platterControl:ColorizeIcon() return false end -- ************************************************** -- Is Relevant / Is Enabled -- ************************************************** function WP_platterControl:IsRelevant(moho) if (not _did_my_resources_package_path) then package.path = package.path..';'..moho:UserAppDir()..'/scripts/ScriptResources/?.lua;' _did_my_resources_package_path = true end return true end function WP_platterControl:IsEnabled(moho) return (moho.document:CurrentDocAction() == '' and moho.layer:IsBoneType()) end -- ************************************************** -- Settings -- ************************************************** WP_platterControl.rangeStartFr = nil WP_platterControl.rangeEndFr = nil WP_platterControl.frameInterval = 1 WP_platterControl.clearAllKeyframes = true -- REMOVE ALL KEYFRAMES ON PLATTER BONE ROTATION CHANNEL BEFORE WRITING NEW ONES WP_platterControl.alwaysWriteKeyAtRangeStartAndEnd = true WP_platterControl.onlyWriteKeyWhenSpeedChanges = false WP_platterControl.frameIntervalOffsetFrames = 0 WP_platterControl.startFrameIntervalAtTimelineStart = false WP_platterControl.rpm = 10 WP_platterControl.ccwDir = false WP_platterControl.showDoneConfirmation = true WP_platterControl.speedBoneId = nil WP_platterControl.platterBoneIds = nil WP_platterControl.filterSpeedBonesListOnKeys = false WP_platterControl.speedSteps = 0 WP_platterControl.rotationSteps = 0 WP_platterControl.useRealSpeedAngle = false -- TO USE fAngle (instead of fAnimAngle) for speed bone angle (is slower, but also works when speed bone is controlled by smart bone) WP_platterControl.speedDialDirMode = WP_platterControl.SPEED_DIAL_MODE_PRESET_LEFT WP_platterControl.cyclesMode = WP_platterControl.CYCLES_MODE_SINGLE_CYCLE WP_platterControl.cyclesCount = 1 WP_platterControl.cyclesSequence = '' WP_platterControl.cyclesPingpong = false -- ************************************************** -- Preferences -- ************************************************** function WP_platterControl:ResetPrefs() self.frameInterval = 1 self.rpm = 10 self.ccwDir = false self.showDoneConfirmation = true self.speedBoneId = nil self.speedBoneLabelCheck = nil self.platterBoneIds = nil -- ARRAY self.platterBoneLabelChecks = nil -- ARRAY self.alwaysWriteKeyAtRangeStartAndEnd = true self.onlyWriteKeyWhenSpeedChanges = false self.frameIntervalOffsetFrames = 0 self.startFrameIntervalAtTimelineStart = false self.clearAllKeyframes = true self.filterSpeedBonesListOnKeys = false self.speedSteps = 0 self.rotationSteps = 0 WP_platterControl.useRealSpeedAngle = false WP_platterControl.speedDialDirMode = WP_platterControl.SPEED_DIAL_MODE_PRESET_LEFT WP_platterControl.cyclesMode = WP_platterControl.CYCLES_MODE_SINGLE_CYCLE WP_platterControl.cyclesCount = 1 WP_platterControl.cyclesSequence = '' WP_platterControl.cyclesPingpong = false end function WP_platterControl:SavePrefs(prefs) self.rpm = prefs:GetFloat('WP_platterControl.rpm', 10.0) self.ccwDir = prefs:GetBool('WP_platterControl.ccwDir', false) self.showDoneConfirmation = prefs:GetBool('WP_platterControl.showDoneConfirmation', true) self.speedBoneId = prefs:GetInt('WP_platterControl.speedBoneId', nil) self.speedBoneLabelCheck = prefs:GetString('WP_platterControl.speedBoneLabelCheck', nil) self.platterBoneIds = prefs:GetString('WP_platterControl.platterBoneIds', nil) if (self.platterBoneIds ~= nil) then local platterBonesCount = WP_utils:arrLen(self.platterBoneIds) self.platterBoneIds = WP_utils:split(self.platterBoneIds, ',') for i = 1, platterBonesCount, 1 do self.platterBoneIds[i] = WP_utils:toInt(self.platterBoneIds[i]) end end self.platterBoneLabelChecks = prefs:GetString('WP_platterControl.platterBoneLabelChecks', nil) if (self.platterBoneLabelChecks ~= nil) then self.platterBoneLabelChecks = WP_utils:split(self.platterBoneLabelChecks, WP_platterControl.STR_ARR_TO_CONCAT_STRING_DIVIDER) end self.frameInterval = prefs:GetInt('WP_platterControl.frameInterval', 1) self.alwaysWriteKeyAtRangeStartAndEnd = prefs:GetBool('WP_platterControl.alwaysWriteKeyAtRangeStartAndEnd', true) self.onlyWriteKeyWhenSpeedChanges = prefs:GetBool('WP_platterControl.onlyWriteKeyWhenSpeedChanges', false) self.frameIntervalOffsetFrames = prefs:GetInt('WP_platterControl.frameIntervalOffsetFrames', 0) self.startFrameIntervalAtTimelineStart = prefs:GetBool('WP_platterControl.startFrameIntervalAtTimelineStart', false) self.clearAllKeyframes = prefs:GetBool('WP_platterControl.clearAllKeyframes', true) self.filterSpeedBonesListOnKeys = prefs:GetBool('WP_platterControl.filterSpeedBonesListOnKeys', false) self.speedSteps = prefs:GetInt('WP_platterControl.frameInterval', 1) self.rotationSteps = prefs:GetInt('WP_platterControl.frameInterval', 1) self.useRealSpeedAngle = prefs:GetBool('WP_platterControl.useRealSpeedAngle', false) self.speedDialDirMode = prefs:GetInt('WP_platterControl.speedDialDirMode', WP_platterControl.SPEED_DIAL_MODE_PRESET_LEFT) self.cyclesMode = prefs:GetInt('WP_platterControl.cyclesMode', WP_platterControl.CYCLES_MODE_SINGLE_CYCLE) self.cyclesCount = prefs:GetInt('WP_platterControl.cyclesCount', 1) self.cyclesSequence = prefs:GetString('WP_platterControl.cyclesSequence', '') self.cyclesPingpong = prefs:GetBool('WP_platterControl.cyclesPingpong', false) end function WP_platterControl:LoadPrefs(prefs) prefs:SetInt('WP_platterControl.rpm', self.rpm) prefs:SetBool('WP_platterControl.ccwDir', self.ccwDir) prefs:SetBool('WP_platterControl.showDoneConfirmation', self.showDoneConfirmation) prefs:SetInt('WP_platterControl.speedBoneId', self.speedBoneId) prefs:SetString('WP_platterControl.speedBoneLabelCheck', self.speedBoneLabelCheck) if (self.platterBoneIds == nil) then prefs:SetString('WP_platterControl.platterBoneIds', '') else prefs:SetString('WP_platterControl.platterBoneIds', table.concat(self.platterBoneIds, ',')) end if (self.platterBoneLabelChecks == nil) then prefs:SetString('WP_platterControl.platterBoneLabelChecks', '') else prefs:SetString('WP_platterControl.platterBoneLabelChecks', table.concat(self.platterBoneLabelChecks, WP_platterControl.STR_ARR_TO_CONCAT_STRING_DIVIDER)) end prefs:SetInt('WP_platterControl.frameInterval', self.frameInterval) prefs:SetBool('WP_platterControl.alwaysWriteKeyAtRangeStartAndEnd', self.alwaysWriteKeyAtRangeStartAndEnd) prefs:SetBool('WP_platterControl.onlyWriteKeyWhenSpeedChanges', self.onlyWriteKeyWhenSpeedChanges) prefs:SetInt('WP_platterControl.frameIntervalOffsetFrames', self.frameIntervalOffsetFrames) prefs:SetBool('WP_platterControl.startFrameIntervalAtTimelineStart', self.startFrameIntervalAtTimelineStart) prefs:SetBool('WP_platterControl.clearAllKeyframes', self.clearAllKeyframes) prefs:SetBool('WP_platterControl.filterSpeedBonesListOnKeys', self.filterSpeedBonesListOnKeys) prefs:SetInt('WP_platterControl.speedSteps', self.speedSteps) prefs:SetInt('WP_platterControl.rotationSteps', self.rotationSteps) prefs:SetBool('WP_platterControl.useRealSpeedAngle', self.useRealSpeedAngle) prefs:SetInt('WP_platterControl.speedDialDirMode', self.speedDialDirMode) prefs:SetInt('WP_platterControl.cyclesMode', self.cyclesMode) prefs:SetInt('WP_platterControl.cyclesCount', self.cyclesCount) prefs:SetString('WP_platterControl.cyclesSequence', self.cyclesSequence) prefs:SetBool('WP_platterControl.cyclesPingpong', self.cyclesPingpong) end -- ************************************************** -- Dialog -- ************************************************** WP_platterControlDialog = {} WP_platterControlDialog.MSG_RESET = MOHO.MSG_BASE WP_platterControlDialog.MSG_SHOW_DONE_CONFIRMATION = MOHO.MSG_BASE + 1 WP_platterControlDialog.MSG_RANGE_START_FR = MOHO.MSG_BASE + 2 WP_platterControlDialog.MSG_RANGE_END_FR = MOHO.MSG_BASE + 3 WP_platterControlDialog.MSG_CLEAR_ALL_KEYFRAMES = MOHO.MSG_BASE + 4 WP_platterControlDialog.MSG_FRAME_INTERVAL = MOHO.MSG_BASE + 5 WP_platterControlDialog.MSG_START_FRAME_INTERVAL_AT_TIMELINE_START = MOHO.MSG_BASE + 6 WP_platterControlDialog.MSG_FRAME_INTERVAL_OFFSET_FRAMES = MOHO.MSG_BASE + 7 WP_platterControlDialog.MSG_FRAME_INTERVAL_PRESET_1 = MOHO.MSG_BASE + 8 WP_platterControlDialog.MSG_FRAME_INTERVAL_PRESET_2 = MOHO.MSG_BASE + 9 WP_platterControlDialog.MSG_FRAME_INTERVAL_PRESET_3 = MOHO.MSG_BASE + 10 WP_platterControlDialog.MSG_FRAME_INTERVAL_PRESET_4 = MOHO.MSG_BASE + 11 WP_platterControlDialog.MSG_RPM = MOHO.MSG_BASE + 12 WP_platterControlDialog.MSG_CYCLE_LENGTH = MOHO.MSG_BASE + 13 WP_platterControlDialog.MSG_USE_CCWS_DIRECTION = MOHO.MSG_BASE + 14 WP_platterControlDialog.MSG_RPM_PRESET_5 = MOHO.MSG_BASE + 15 WP_platterControlDialog.MSG_RPM_PRESET_10 = MOHO.MSG_BASE + 16 WP_platterControlDialog.MSG_RPM_PRESET_15 = MOHO.MSG_BASE + 17 WP_platterControlDialog.MSG_RPM_PRESET_20 = MOHO.MSG_BASE + 18 WP_platterControlDialog.MSG_RPM_PRESET_VINYL_LP = MOHO.MSG_BASE + 19 WP_platterControlDialog.MSG_RPM_PRESET_VINYL_SINGLE = MOHO.MSG_BASE + 20 WP_platterControlDialog.MSG_RPM_PRESET_COMP_CASSETTE = MOHO.MSG_BASE + 21 WP_platterControlDialog.MSG_SPEED_BONE = MOHO.MSG_BASE + 22 WP_platterControlDialog.MSG_PLATTER_BONE = MOHO.MSG_BASE + 23 WP_platterControlDialog.MSG_ONLY_WRITE_KEY_WHEN_SPEED_CHANGES = MOHO.MSG_BASE + 24 WP_platterControlDialog.MSG_ALWAYS_WRITE_KEY_AT_RANGE_START_AND_END = MOHO.MSG_BASE + 25 WP_platterControlDialog.MSG_FILTER_SPEED_BONES_LIST_ON_KEYS = MOHO.MSG_BASE + 26 WP_platterControlDialog.MSG_SPEED_STEPS = MOHO.MSG_BASE + 27 WP_platterControlDialog.MSG_ROTATION_STEPS = MOHO.MSG_BASE + 28 WP_platterControlDialog.MSG_USE_REAL_SPEED_ANGLE = MOHO.MSG_BASE + 29 WP_platterControlDialog.MSG_SPEED_DIAL_MODE_PRESET_LEFT = MOHO.MSG_BASE + 30 WP_platterControlDialog.MSG_SPEED_DIAL_MODE_PRESET_UP = MOHO.MSG_BASE + 31 WP_platterControlDialog.MSG_SPEED_DIAL_MODE_PRESET_RIGHT = MOHO.MSG_BASE + 32 WP_platterControlDialog.MSG_SPEED_DIAL_MODE_PRESET_DOWN = MOHO.MSG_BASE + 33 WP_platterControlDialog.MSG_CYCLES_MODE_SINGLE_CYCLE = MOHO.MSG_BASE + 34 WP_platterControlDialog.MSG_CYCLES_MODE_MULTI_CYCLES_LIMITED = MOHO.MSG_BASE + 35 WP_platterControlDialog.MSG_CYCLES_MODE_MULTI_CYCLES_UNLIMITED = MOHO.MSG_BASE + 36 WP_platterControlDialog.MSG_CYCLES_MODE_MULTI_CYCLES_SEQUENCE = MOHO.MSG_BASE + 37 WP_platterControlDialog.MSG_CYCLES_COUNT = MOHO.MSG_BASE + 38 WP_platterControlDialog.MSG_CYCLES_SEQUENCE = MOHO.MSG_BASE + 39 WP_platterControlDialog.MSG_CYCLES_PINGPONG = MOHO.MSG_BASE + 40 -- RETURN THE SELECTED OPTION (0-BASED) FROM THE BUTTONS IN THE SUPPLIED RADION LIST function WP_platterControlDialog:GetSelectedIndexFromRadionList(radionList) for i, btn in ipairs(radionList) do if (btn:Value()) then return i - 1 end end return nil end function WP_platterControlDialog:fillBoneList(list, skeleton, filterOnKeys) local filterOnKeys = filterOnKeys or false local bone -- CLEAR LIST for i = list:CountItems() - 1, 0, -1 do list:RemoveItem(list:CountItems() - 1) end if (skeleton:CountBones() > 0) then list:AddItem('Select Bone') end for i = 0, skeleton:CountBones() - 1 do bone = skeleton:Bone(i) if (filterOnKeys == false or bone.fAnimAngle:CountKeys() > 1) then list:AddItem(bone:Name()) else list:AddItem(WP_platterControl.HIDDEN_BONE_NAME) end end list:SetSelItem(list:GetItem(0), true, false) end function WP_platterControlDialog:new(mohoDoc, skeleton) local d = LM.GUI.SimpleDialog('Wigglepixel Platter Control v'..WP_platterControl:Version(), self) local l = d:GetLayout() d.mohoDoc = mohoDoc d.skeleton = skeleton -- RESET BUTTON d.btnReset = LM.GUI.Button('RESET SETTINGS', d.MSG_RESET) d.btnReset:SetToolTip('Reset platter control settings') l:AddChild(d.btnReset, LM.GUI.ALIGN_FILL, 0) -- --------------------------------------------- -- BONE LISTS l:PushH(LM.GUI.ALIGN_LEFT, 10) -- SPEED DIAL BONE l:PushV(LM.GUI.ALIGN_TOP, 5) l:PushH(LM.GUI.ALIGN_LEFT, 10) -- LABEL d.lblSpeedBone = LM.GUI.StaticText('SOURCE: Select Speed Dial Bone') d.lblSpeedBone:SetToolTip('Select Speed Dial Controller Bone which functions as the source to create the animation') l:AddChild(d.lblSpeedBone, LM.GUI.ALIGN_LEFT, 0) -- FILTER ON BONES WITH KEYS d.chkFilterSpeedBonesListOnKeys = LM.GUI.CheckBox('Reveal bones with keyframes', d.MSG_FILTER_SPEED_BONES_LIST_ON_KEYS) d.chkFilterSpeedBonesListOnKeys:SetToolTip('When checked only bones with keyframes will be listed') l:AddChild(d.chkFilterSpeedBonesListOnKeys, LM.GUI.ALIGN_LEFT, 0) l:Pop() -- SPEED STEPS d.txtSpeedSteps = LM.GUI.TextControl(50, '0', d.MSG_SPEED_STEPS, LM.GUI.FIELD_UINT, 'Speed steps') d.txtSpeedSteps:SetToolTip('Set higher than zero to set a stepped interval in speed values (0 = stepless)') -- l:AddChild(d.txtSpeedSteps, LM.GUI.ALIGN_LEFT, 0) -- LIST d.lstSpeedBone = LM.GUI.ImageTextList(400, 200, d.MSG_SPEED_BONE) l:AddChild(d.lstSpeedBone, LM.GUI.ALIGN_LEFT, 0) l:Pop() -- PLATTER BONE l:PushV(LM.GUI.ALIGN_TOP, 5) -- LABEL d.lblPlatterBone = LM.GUI.StaticText('TARGET: Select one or more Platter Bones') d.lblPlatterBone:SetToolTip('Select Platter Bone to which the rotation keyframes will be written to') l:AddChild(d.lblPlatterBone, LM.GUI.ALIGN_LEFT, 0) -- -- ROTATION STEPS d.txtRotationSteps = LM.GUI.TextControl(50, '0', d.MSG_ROTATION_STEPS, LM.GUI.FIELD_UINT, 'Rotation steps') d.txtRotationSteps:SetToolTip('Set higher than zero to set a stepped interval in rotation angle values (0 = stepless)') -- l:AddChild(d.txtRotationSteps, LM.GUI.ALIGN_LEFT, 0) -- LIST d.lstPlatterBone = LM.GUI.ImageTextList(400, 200, d.MSG_PLATTER_BONE) d.lstPlatterBone:SetAllowsMultipleSelection(true) l:AddChild(d.lstPlatterBone, LM.GUI.ALIGN_LEFT, 0) l:Pop() l:Pop() -- FILL BONE LISTS self:fillBoneList(d.lstSpeedBone, d.skeleton, WP_platterControl.filterSpeedBonesListOnKeys) self:fillBoneList(d.lstPlatterBone, d.skeleton, false) l:PushH(LM.GUI.ALIGN_LEFT, 20) -- SPEED DIAL BONE HEADER l:AddChild(LM.GUI.StaticText('SPEED DIAL BONE'), LM.GUI.ALIGN_LEFT, 0) -- SPEED DIAL MODE OPTIONS local speedDialDirModePresets = { { d.MSG_SPEED_DIAL_MODE_PRESET_LEFT, 'Left' }, { d.MSG_SPEED_DIAL_MODE_PRESET_UP, 'Up' }, { d.MSG_SPEED_DIAL_MODE_PRESET_RIGHT, 'Right' }, { d.MSG_SPEED_DIAL_MODE_PRESET_DOWN, 'Down' }, } l:PushH(LM.GUI.ALIGN_LEFT, 5) d.lblSpeedDialDirMode = LM.GUI.StaticText('Zero direction is') l:AddChild(d.lblSpeedDialDirMode, LM.GUI.ALIGN_LEFT, 0) d.radSpeedDialDirMode = {} for i, data in ipairs(speedDialDirModePresets) do d.radSpeedDialDirMode[i] = LM.GUI.RadioButton(data[2], data[1]) d.radSpeedDialDirMode[i]:SetToolTip('Click to set speed dial direction to '..data[2]) l:AddChild(d.radSpeedDialDirMode[i], LM.GUI.ALIGN_LEFT, 0) end l:Pop() l:Pop() -- DIVIDER l:AddChild(LM.GUI.Divider(false), LM.GUI.ALIGN_FILL) -- CYCLES HEADER l:AddChild(LM.GUI.StaticText('CYCLES'), LM.GUI.ALIGN_LEFT, 0) -- CYCLES MODE PRESET BUTTONS local cyclesModePresets = { { d.MSG_CYCLES_MODE_SINGLE_CYCLE, 'Single 360 deg Cycle on Repeat' }, { d.MSG_CYCLES_MODE_MULTI_CYCLES_UNLIMITED, 'Unlimited Increasing Cycles' }, { d.MSG_CYCLES_MODE_MULTI_CYCLES_LIMITED, 'Cycle Range on repeat' }, -- { d.MSG_CYCLES_MODE_MULTI_CYCLES_SEQUENCE, 'Cycles Sequence on repeat' }, } l:PushH(LM.GUI.ALIGN_LEFT, 5) -- CYCLE MODES d.radCyclesMode = {} d.lblCyclesMode = LM.GUI.StaticText('Mode') l:AddChild(d.lblCyclesMode, LM.GUI.ALIGN_LEFT, 0) for i, data in ipairs(cyclesModePresets) do d.radCyclesMode[i] = LM.GUI.RadioButton(data[2], data[1]) d.radCyclesMode[i]:SetToolTip('Click to set Cycles Mode to '..data[2]) l:AddChild(d.radCyclesMode[i], LM.GUI.ALIGN_LEFT, 0) end -- CYCLES COUNT d.txtCyclesCount = LM.GUI.TextControl(50, 0, d.MSG_CYCLES_MODE, LM.GUI.FIELD_UINT, 'Cycles') d.txtCyclesCount:SetToolTip('Amount of cycles to use in repeat') l:AddChild(d.txtCyclesCount, LM.GUI.ALIGN_LEFT, 0) l:Pop() -- RPM PRESET BUTTONS d.rpmPresets = { { d.MSG_RPM_PRESET_5, '5' }, { d.MSG_RPM_PRESET_10, '10' }, { d.MSG_RPM_PRESET_15, '15' }, { d.MSG_RPM_PRESET_20, '20' }, { d.MSG_RPM_PRESET_VINYL_LP, 'Vinyl LP' }, { d.MSG_RPM_PRESET_VINYL_SINGLE, 'Vinyl Single' }, { d.MSG_RPM_PRESET_COMP_CASSETTE, 'Compact Cassette' }, } l:PushH(LM.GUI.ALIGN_LEFT, 5) local rpmTooltip = 'Revolutions Per Minute' local rpmPresetBtn l:AddChild(LM.GUI.StaticText('RPM'), LM.GUI.ALIGN_LEFT, 0) for _, data in ipairs(d.rpmPresets) do rpmPresetBtn = LM.GUI.Button(data[2], data[1]) rpmPresetBtn:SetToolTip('Click to set rpm to '..data[2]) l:AddChild(rpmPresetBtn, LM.GUI.ALIGN_LEFT, 0) end l:AddChild(LM.GUI.StaticText('-->'), LM.GUI.ALIGN_LEFT, 0) -- RPM d.txtRpm = LM.GUI.TextControl(50, '33', d.MSG_RPM, LM.GUI.FIELD_FLOAT, '') d.txtRpm:SetToolTip(rpmTooltip) l:AddChild(d.txtRpm, LM.GUI.ALIGN_LEFT, 0) d.txtCycleLength = LM.GUI.TextControl(180, 0, d.MSG_CYCLE_LENGTH, LM.GUI.FIELD_TEXT) l:AddChild(d.txtCycleLength, LM.GUI.ALIGN_LEFT, 0) l:Pop() -- COUNTER CLOCKWISE (INVERT DIRECTION) d.chkCcwDir = LM.GUI.CheckBox('Counter Clockwise (Invert Direction)', d.MSG_USE_CCWS_DIRECTION) d.chkCcwDir:SetToolTip('When checked forward movements will rotate counter clockwise (rotations inverted)') l:AddChild(d.chkCcwDir, LM.GUI.ALIGN_LEFT, 0) l:PushH(LM.GUI.ALIGN_LEFT, 20) -- CYCLES SEQUENCE d.txtCyclesSequence = LM.GUI.TextControl(150, 0, d.MSG_CYCLES_SEQUENCE, LM.GUI.FIELD_TEXT, 'Cycles Sequence') d.txtCyclesSequence:SetToolTip('Amount of cycles to use in repeat (1-based)') -- l:AddChild(d.txtCyclesSequence, LM.GUI.ALIGN_LEFT, 0) -- CYCLES PINGPONG d.chkCyclesPingPong = LM.GUI.CheckBox('Ping Pong', d.MSG_CYCLES_PINGPONG) d.chkCyclesPingPong:SetToolTip('When checked the cycle range will be played forward and backwards') -- l:AddChild(d.chkCyclesPingPong, LM.GUI.ALIGN_LEFT, 0) l:Pop() -- DIVIDER l:AddChild(LM.GUI.Divider(false), LM.GUI.ALIGN_FILL) -- KEYFRAMES HEADER l:AddChild(LM.GUI.StaticText('KEYFRAME WRITING'), LM.GUI.ALIGN_LEFT, 0) -- FRAME RANGE l:PushH(LM.GUI.ALIGN_LEFT, 20) l:AddChild(LM.GUI.StaticText('Range'), LM.GUI.ALIGN_LEFT, 0) -- START FRAME d.txtRangeStartFr = LM.GUI.TextControl(50, WP_utils:toInt(d.mohoDoc:StartFrame()), d.MSG_RANGE_START_FR, LM.GUI.FIELD_UINT, '') d.txtRangeStartFr:SetToolTip('Set the first frame for writing keyframes to the timeline') l:AddChild(d.txtRangeStartFr, LM.GUI.ALIGN_LEFT, 0) -- HYPHEN l:AddChild(LM.GUI.StaticText('-'), LM.GUI.ALIGN_CENTER, 0) -- END FRAME d.txtRangeEndFr = LM.GUI.TextControl(50, WP_utils:toInt(d.mohoDoc:EndFrame()), d.MSG_RANGE_END_FR, LM.GUI.FIELD_UINT, '') d.txtRangeEndFr:SetToolTip('Set the last frame for writing keyframes to the timeline') l:AddChild(d.txtRangeEndFr, LM.GUI.ALIGN_LEFT, 0) l:Pop() -- CLEAR ALL EXISTING KEYFRAMES ON PLATTER ROTATION CHANNEL d.chkClearAllKeyframes = LM.GUI.CheckBox('Start by clearing the Platter bone\'s Rotation channel of the complete timeline', d.MSG_CLEAR_ALL_KEYFRAMES) d.chkClearAllKeyframes:SetToolTip('When checked the rotation channel of the platter bone will be cleared from keyframes prior to writing the new keyframes') l:AddChild(d.chkClearAllKeyframes, LM.GUI.ALIGN_LEFT, 0) -- ONLY WRITE KEY WHEN SPEED CHANGES d.chkOnlyWriteKeyWhenSpeedChanges = LM.GUI.CheckBox('[experimental] Only write Keys if Speed Changes', d.MSG_ONLY_WRITE_KEY_WHEN_SPEED_CHANGES) d.chkOnlyWriteKeyWhenSpeedChanges:SetToolTip('Not fully tested yet! When checked keyframes will only be written to the platter bone on frames where the speed dial has a keyframe') l:AddChild(d.chkOnlyWriteKeyWhenSpeedChanges, LM.GUI.ALIGN_LEFT, 0) -- USE REAL SPEED ANGLE d.chkUseRealSpeedAngle = LM.GUI.CheckBox('Use Real Speed Bone Angle (Extremely slow baking, but NEEDED when Speed Bone is controlled by Smart or Controller Bone)', d.MSG_USE_REAL_SPEED_ANGLE) d.chkUseRealSpeedAngle:SetToolTip('Turning this on gives extremely slow baking compared to leaving this off, but it always uses the right angles of the speed bone so it\'s way safer.') l:AddChild(d.chkUseRealSpeedAngle, LM.GUI.ALIGN_LEFT, 0) -- ALWAYS WRITE KEY AT START AND END OF RANGE d.chkAlwaysWriteKeyAtRangeStartAndEnd = LM.GUI.CheckBox('Always write key on start and end of range', d.MSG_ALWAYS_WRITE_KEY_AT_RANGE_START_AND_END) d.chkAlwaysWriteKeyAtRangeStartAndEnd:SetToolTip('When checked at the start and end of the given frame range there will always be written a keyframe') l:AddChild(d.chkAlwaysWriteKeyAtRangeStartAndEnd, LM.GUI.ALIGN_LEFT, 0) -- INTERVAL HEADER l:AddChild(LM.GUI.StaticText('INTERVAL'), LM.GUI.ALIGN_LEFT, 0) -- FRAME INTERVAL PRESET BUTTONS d.frameIntervalPresets = { { d.MSG_FRAME_INTERVAL_PRESET_1, 'One\'s' }, { d.MSG_FRAME_INTERVAL_PRESET_2, 'Two\'s' }, { d.MSG_FRAME_INTERVAL_PRESET_3, 'Three\'s' }, { d.MSG_FRAME_INTERVAL_PRESET_4, 'Four\'s' }, } l:PushH(LM.GUI.ALIGN_LEFT, 5) local frameIntervalTooltip = 'Frame interval, when set to a value higher than 1 not every frame gets a keyframe' local frameIntervalPresetBtn for _, data in ipairs(d.frameIntervalPresets) do frameIntervalPresetBtn = LM.GUI.Button(data[2], data[1]) frameIntervalPresetBtn:SetToolTip('Click to set interval to '..data[2]) l:AddChild(frameIntervalPresetBtn, LM.GUI.ALIGN_LEFT, 0) end l:AddChild(LM.GUI.StaticText('-->'), LM.GUI.ALIGN_LEFT, 0) -- FRAME INTERVAL d.txtFrameInterval = LM.GUI.TextControl(50, '1', d.MSG_FRAME_INTERVAL, LM.GUI.FIELD_UINT, '') d.txtFrameInterval:SetToolTip(frameIntervalTooltip) l:AddChild(d.txtFrameInterval, LM.GUI.ALIGN_LEFT, 0) l:AddChild(LM.GUI.StaticText(' '), LM.GUI.ALIGN_LEFT, 0) -- FRAME INTERVAL OFFSET FRAMES d.txtFrameIntervalOffsetFrames = LM.GUI.TextControl(50, '0', d.MSG_FRAME_INTERVAL_OFFSET_FRAMES, LM.GUI.FIELD_UINT, 'Offset') d.txtFrameIntervalOffsetFrames:SetToolTip('Set to a value above 0 to give the frame interval an offset in frames') l:AddChild(d.txtFrameIntervalOffsetFrames, LM.GUI.ALIGN_LEFT, 0) l:AddChild(LM.GUI.StaticText(' '), LM.GUI.ALIGN_LEFT, 0) -- START FRAME INTERVAL AT START OF TIMELINE d.chkStartFrameIntervalAtTimelineStart = LM.GUI.CheckBox('Calculate Interval from TL Start', d.MSG_START_FRAME_INTERVAL_AT_TIMELINE_START) d.chkStartFrameIntervalAtTimelineStart:SetToolTip('When checked the start of the frame interval will be calculated from the start of the timeline (if unchecked the interval will start at the start of the given range)') l:AddChild(d.chkStartFrameIntervalAtTimelineStart, LM.GUI.ALIGN_LEFT, 0) l:Pop() -- --------------------------------------------- -- DIVIDER l:AddChild(LM.GUI.Divider(false), LM.GUI.ALIGN_FILL) -- MISC HEADER l:AddChild(LM.GUI.StaticText('MISC'), LM.GUI.ALIGN_LEFT, 0) -- SHOW DONE CONFIRMATION d.chkShowDoneConfirmation = LM.GUI.CheckBox('Show confirmation dialog when done', d.MSG_SHOW_DONE_CONFIRMATION) d.chkShowDoneConfirmation:SetToolTip('Uncheck to hide confirmation message after writing keyframes') l:AddChild(d.chkShowDoneConfirmation, LM.GUI.ALIGN_LEFT, 0) return d end function WP_platterControlDialog:getCycleLengthText(rpm) if (rpm ~= nil) then if (rpm > 0) then local totalInMin = 1 / rpm local totalInFrames = totalInMin * 60 * WP_platterControl.fps return 'One Cycle = '..WP_utils:toInt(totalInFrames)..' frames' else return '' end else return '' end end function WP_platterControlDialog:updateCyclesCountInputToMode(mode) -- VALUE if (mode == WP_platterControl.CYCLES_MODE_SINGLE_CYCLE or mode == WP_platterControl.CYCLES_MODE_MULTI_CYCLES_LIMITED) then self.txtCyclesCount:SetValue('1') else self.txtCyclesCount:SetValue('0') end -- ENABLE/DISABLE self.txtCyclesCount:Enable(mode == WP_platterControl.CYCLES_MODE_MULTI_CYCLES_LIMITED) end function WP_platterControlDialog:UpdateWidgets() -- RPM AND CYCLE LENGTH self.txtRpm:SetValue(WP_platterControl.rpm) self.txtCycleLength:SetValue(WP_platterControlDialog:getCycleLengthText(WP_platterControl.rpm)) -- COUNTER CLOCKWISE self.chkCcwDir:SetValue(WP_platterControl.ccwDir) -- CLEAR ALL KEYFRAMES self.chkClearAllKeyframes:SetValue(WP_platterControl.clearAllKeyframes) -- FRAME RANGE self.txtRangeStartFr:SetValue(WP_utils:toInt(self.mohoDoc:StartFrame())) self.txtRangeEndFr:SetValue(WP_utils:toInt(self.mohoDoc:EndFrame())) -- FRAME INTERVAL self.chkStartFrameIntervalAtTimelineStart:SetValue(WP_platterControl.startFrameIntervalAtTimelineStart) self.txtFrameInterval:SetValue(WP_platterControl.frameInterval) self.txtFrameIntervalOffsetFrames:SetValue(WP_platterControl.frameIntervalOffsetFrames) -- SET SELECTED SPEED BONE local selItemLabelTmp local boneItemCount = self.lstSpeedBone:CountItems() if (WP_platterControl.speedBoneId ~= nil and WP_platterControl.speedBoneId < boneItemCount) then selItemLabelTmp = self.lstSpeedBone:GetItem(WP_platterControl.speedBoneId + 1) if (selItemLabelTmp == WP_platterControl.speedBoneLabelCheck) then self.lstSpeedBone:SetSelItem(selItemLabelTmp, true, false) else self.lstSpeedBone:SetSelItem(self.lstSpeedBone:GetItem(0), true, false) end else self.lstSpeedBone:SetSelItem(self.lstSpeedBone:GetItem(0), true, false) end -- RETURNS INDEX OF VALUE IN TABLE/ARRAY. RETURNS -1 IF NOT FOUND function indexOfValue(arr, value) local len = WP_utils:arrLen(arr) for i = 1, len, 1 do if (arr[i] == value) then return i end end return -1 end -- SET SELECTED PLATTER BONE local hasSelection = false if (WP_platterControl.platterBoneIds ~= nil) then local platterBoneIdsCount = WP_utils:arrLen(WP_platterControl.platterBoneIds) for i = 1, platterBoneIdsCount, 1 do local boneId = WP_platterControl.platterBoneIds[i] if (boneId < boneItemCount) then selItemLabelTmp = self.lstPlatterBone:GetItem(boneId + 1) if (indexOfValue(WP_platterControl.platterBoneLabelChecks, selItemLabelTmp) ~= -1) then self.lstPlatterBone:SetSelItem(selItemLabelTmp, true, hasSelection) hasSelection = true; end end end end if (hasSelection == false) then self.lstPlatterBone:SetSelItem(self.lstPlatterBone:GetItem(0), true, false) end -- ONLY WRITE WHEN SPEED CHANGES self.chkOnlyWriteKeyWhenSpeedChanges:SetValue(WP_platterControl.onlyWriteKeyWhenSpeedChanges) -- ALWAYS WRITE KEY WHEN ON START OR END OF RANGE self.chkAlwaysWriteKeyAtRangeStartAndEnd:SetValue(WP_platterControl.alwaysWriteKeyAtRangeStartAndEnd) -- SHOW DONE CONFIRMATION self.chkShowDoneConfirmation:SetValue(WP_platterControl.showDoneConfirmation) -- FILTER SPEED BONES LIST ON KEYFRAMES self.chkFilterSpeedBonesListOnKeys:SetValue(WP_platterControl.filterSpeedBonesListOnKeys) -- SPEED STEPS self.txtSpeedSteps:SetValue(WP_platterControl.speedSteps) -- ROTATION STEPS self.txtRotationSteps:SetValue(WP_platterControl.rotationSteps) -- USE REAL SPEED ANGLE self.chkUseRealSpeedAngle:SetValue(WP_platterControl.useRealSpeedAngle) -- SPEED DIAL DIRECTION MODE self.radSpeedDialDirMode[WP_platterControl.speedDialDirMode + 1]:SetValue(true) -- CYCLES MODE self.radCyclesMode[WP_platterControl.cyclesMode + 1]:SetValue(true) -- CYCLES COUNT self:updateCyclesCountInputToMode(WP_platterControl.cyclesMode) -- CYCLES SEQUENCE self.txtCyclesSequence:SetValue(WP_platterControl.cyclesSequence) -- CYCLES PINGPONG self.chkCyclesPingPong:SetValue(WP_platterControl.cyclesPingpong) end function WP_platterControlDialog:HandleMessage(msg) if msg == self.MSG_RESET then local resetAlert = WP_utils:alert('This will reset all Platter Control preferences, are you sure?', true) if (resetAlert == 1) then return nil end WP_platterControl:ResetPrefs() self:UpdateWidgets() elseif msg == self.MSG_CYCLES_MODE_SINGLE_CYCLE then self:updateCyclesCountInputToMode(WP_platterControl.CYCLES_MODE_SINGLE_CYCLE) elseif msg == self.MSG_CYCLES_MODE_MULTI_CYCLES_UNLIMITED then self:updateCyclesCountInputToMode(WP_platterControl.CYCLES_MODE_MULTI_CYCLES_UNLIMITED) elseif msg == self.MSG_CYCLES_MODE_MULTI_CYCLES_LIMITED then self:updateCyclesCountInputToMode(WP_platterControl.CYCLES_MODE_MULTI_CYCLES_LIMITED) elseif msg == self.MSG_CYCLES_MODE_MULTI_CYCLES_SEQUENCE then self:updateCyclesCountInputToMode(WP_platterControl.CYCLES_MODE_MULTI_CYCLES_SEQUENCE) -- HANDLE FRAME INTERVAL PRESET BUTTONS elseif msg == self.MSG_FRAME_INTERVAL_PRESET_1 then self.txtFrameInterval:SetValue('1') elseif msg == self.MSG_FRAME_INTERVAL_PRESET_2 then self.txtFrameInterval:SetValue('2') elseif msg == self.MSG_FRAME_INTERVAL_PRESET_3 then self.txtFrameInterval:SetValue('3') elseif msg == self.MSG_FRAME_INTERVAL_PRESET_4 then self.txtFrameInterval:SetValue('4') -- HANDLE RPM PRESET BUTTONS elseif msg == self.MSG_RPM_PRESET_5 then self.txtRpm:SetValue('5') self.txtCycleLength:SetValue(WP_platterControlDialog:getCycleLengthText(self.txtRpm:FloatValue())) elseif msg == self.MSG_RPM_PRESET_10 then self.txtRpm:SetValue('10') self.txtCycleLength:SetValue(WP_platterControlDialog:getCycleLengthText(self.txtRpm:FloatValue())) elseif msg == self.MSG_RPM_PRESET_15 then self.txtRpm:SetValue('15') self.txtCycleLength:SetValue(WP_platterControlDialog:getCycleLengthText(self.txtRpm:FloatValue())) elseif msg == self.MSG_RPM_PRESET_20 then self.txtRpm:SetValue('20') self.txtCycleLength:SetValue(WP_platterControlDialog:getCycleLengthText(self.txtRpm:FloatValue())) elseif msg == self.MSG_RPM_PRESET_VINYL_LP then self.txtRpm:SetValue('33.33') self.txtCycleLength:SetValue(WP_platterControlDialog:getCycleLengthText(self.txtRpm:FloatValue())) elseif msg == self.MSG_RPM_PRESET_VINYL_SINGLE then self.txtRpm:SetValue('45') self.txtCycleLength:SetValue(WP_platterControlDialog:getCycleLengthText(self.txtRpm:FloatValue())) elseif msg == self.MSG_RPM_PRESET_COMP_CASSETTE then self.txtRpm:SetValue('56.25') self.txtCycleLength:SetValue(WP_platterControlDialog:getCycleLengthText(self.txtRpm:FloatValue())) elseif msg == self.MSG_RPM then self.txtCycleLength:SetValue(WP_platterControlDialog:getCycleLengthText(self.txtRpm:FloatValue())) elseif msg == WP_platterControlDialog.MSG_FILTER_SPEED_BONES_LIST_ON_KEYS then self:fillBoneList(self.lstSpeedBone, self.skeleton, self.chkFilterSpeedBonesListOnKeys:Value()) end end function WP_platterControlDialog:OnValidate() -- GET START AND END FRAME VALUES local rangeStartFr = self.txtRangeStartFr:IntValue(); local rangeEndFr = self.txtRangeEndFr:IntValue(); -- CHECK START RANGE FRAME if (rangeStartFr < self.mohoDoc:StartFrame()) then WP_utils:alert('Frame Range cannot be outside of Document/Render Start Frame ('..WP_utils:toInt(self.mohoDoc:StartFrame())..')') return false end -- CHECK END RANGE FRAME if (rangeEndFr > self.mohoDoc:EndFrame()) then WP_utils:alert('Frame Range cannot be outside of Document/Render End Frame ('..WP_utils:toInt(self.mohoDoc:EndFrame())..')') return false end -- CHECK FRAME RANGE if (rangeEndFr - rangeStartFr <= 0) then WP_utils:alert('There must be a frame range of at least 1 frame to be able to proceed') return false end -- CHECK FRAME INTERVAL VALUE local frameInterval = WP_utils:toInt(self.txtFrameInterval:IntValue()) if (frameInterval < 1) then WP_utils:alert('Sub frames aren\'t supported. The Frame Interval must be 1 or higher') return false end -- CHECK SPEED BONE local speedBoneId = self.lstSpeedBone:SelItem() if (speedBoneId <= 0 or speedBoneId == nil) then WP_utils:alert('Please select a Speed Dial Bone') return false end -- CHECK PLATTER BONE(S) if (self.lstPlatterBone:NumSelectedItems() == 0 or (self.lstPlatterBone:NumSelectedItems() == 1 and self.lstPlatterBone:IsItemSelected(0))) then WP_utils:alert('Please select one or more Platter Bones') return false end for i = 1, self.lstPlatterBone:CountItems(), 1 do if (self.lstPlatterBone:IsItemSelected(i) and speedBoneId == i) then WP_utils:alert('Platter bones may not be the same as the speed bone') return false end end -- CHECK FOR NOT SELECTED HIDDEN BONE local speedBoneLabel = self.lstSpeedBone:SelItemLabel() if (speedBoneLabel == WP_platterControl.HIDDEN_BONE_NAME) then WP_utils:alert('Please select a Speed Dial Bone') return false end -- CHECK SPEED AND PLATTER BONE MAY NOT BE THE SAME local platterBoneIdTmp for i = 1, self.lstPlatterBone:CountItems(), 1 do if (self.lstPlatterBone:IsItemSelected(i) and speedBoneId == i - 1) then WP_utils:alert('Speed Dial Bone and Platter Bone may not be the same') return false end end if (self.txtRpm:FloatValue() == 0) then WP_utils:alert('Please set a RPM other than zero') return false end -- CHECK SEQUENCE local selectedCyclesMode = WP_platterControlDialog:GetSelectedIndexFromRadionList(self.radCyclesMode) if (selectedCyclesMode == WP_platterControl.CYCLES_MODE_MULTI_CYCLES_SEQUENCE) then local seqParts = WP_utils:split(self.txtCyclesSequence:Value(), ',') for i, part in ipairs(seqParts) do local partNumber = tonumber(part) if (partNumber == nil) then WP_utils:alert('Cycles sequence may only contain numbers') return false else partNumber = WP_utils:toInt(partNumber) if (partNumber < 1) then WP_utils:alert('Cycles sequence may only contain positive numbers of 1 and up (1-based)') return false end end end end return true end function WP_platterControlDialog:OnOK(moho) WP_platterControl.rangeStartFr = self.txtRangeStartFr:IntValue() WP_platterControl.rangeEndFr = self.txtRangeEndFr:IntValue() WP_platterControl.frameInterval = WP_utils:toInt(self.txtFrameInterval:IntValue()) WP_platterControl.rpm = self.txtRpm:FloatValue() WP_platterControl.ccwDir = self.chkCcwDir:Value() WP_platterControl.speedBoneId = self.lstSpeedBone:SelItem() - 1 WP_platterControl.speedBoneLabelCheck = self.lstSpeedBone:SelItemLabel() WP_platterControl.platterBoneIds = nil WP_platterControl.platterBoneLabelChecks = nil if (self.lstPlatterBone:NumSelectedItems() > 0) then for i = 1, self.lstPlatterBone:CountItems(), 1 do if (self.lstPlatterBone:IsItemSelected(i)) then if (WP_platterControl.platterBoneIds == nil) then WP_platterControl.platterBoneIds = {} WP_platterControl.platterBoneLabelChecks = {} end table.insert(WP_platterControl.platterBoneIds, i - 1) table.insert(WP_platterControl.platterBoneLabelChecks, self.lstPlatterBone:GetItem(i)) end end end WP_platterControl.clearAllKeyframes = self.chkClearAllKeyframes:Value() WP_platterControl.showDoneConfirmation = self.chkShowDoneConfirmation:Value() WP_platterControl.alwaysWriteKeyAtRangeStartAndEnd = self.chkAlwaysWriteKeyAtRangeStartAndEnd:Value() WP_platterControl.onlyWriteKeyWhenSpeedChanges = self.chkOnlyWriteKeyWhenSpeedChanges:Value() WP_platterControl.frameIntervalOffsetFrames = self.txtFrameIntervalOffsetFrames:IntValue() WP_platterControl.startFrameIntervalAtTimelineStart = self.chkStartFrameIntervalAtTimelineStart:Value() WP_platterControl.filterSpeedBonesListOnKeys = self.chkFilterSpeedBonesListOnKeys:Value() WP_platterControl.speedSteps = self.txtSpeedSteps:IntValue() WP_platterControl.rotationSteps = self.txtRotationSteps:IntValue() WP_platterControl.useRealSpeedAngle = self.chkUseRealSpeedAngle:Value() -- GET SELECTED SPEED DIAL DIRECTION MODE WP_platterControl.speedDialDirMode = WP_platterControlDialog:GetSelectedIndexFromRadionList(self.radSpeedDialDirMode) -- GET CYCLES MODE WP_platterControl.cyclesMode = WP_platterControlDialog:GetSelectedIndexFromRadionList(self.radCyclesMode) -- CYCLES COUNT WP_platterControl.cyclesCount = WP_utils:toInt(self.txtCyclesCount:IntValue()) -- GET CYCLES SEQUENCE WP_platterControl.cyclesSequence = self.txtCyclesSequence:Value() WP_platterControl.cyclesPingpong = self.chkCyclesPingPong:Value() end -- ************************************************** -- GO -- ************************************************** function WP_platterControl:Run(moho) -- CHECK UTILS VERSION local utilsVersionCheck = WP_utils:compareVersion(WP_platterControl.WP_UTILS_REQUIRED_VERSION, WP_utils.VERSION) if (utilsVersionCheck < 0) then WP_utils:alert("Halted. This script requires at least version "..WP_utils:ver2str(WP_platterControl.WP_UTILS_REQUIRED_VERSION).." of 'wp_utils'") return elseif (utilsVersionCheck > 0) then WP_utils:alert("Caution! This loaded 'wp_utils' is newer than the expected version "..WP_utils:ver2str(WP_platterControl.WP_UTILS_REQUIRED_VERSION)) end local mohoDoc = moho.document -- GET VALUES WP_platterControl.fps = mohoDoc:Fps() local docStartFrame = mohoDoc:StartFrame() local docEndFrame = mohoDoc:EndFrame() -- GET BONE LAYER local boneLayer = mohoDoc:GetSelectedLayer() boneLayer = moho:LayerAsBone(boneLayer) local skeleton = boneLayer:Skeleton() -- OPEN DIALOG local d = WP_platterControlDialog:new(mohoDoc, skeleton) if (d:DoModal() == LM.GUI.MSG_CANCEL) then return end -- CALC ROT PER FRAME local rotPerFr = self.rpm / (60 * WP_platterControl.fps) -- GET SPEED BONE DIAL RANGE if (WP_platterControl.speedDialDirMode == WP_platterControl.SPEED_DIAL_MODE_PRESET_LEFT) then speedBoneAngleRangeDeg = { from = 180, to = 90 } elseif (WP_platterControl.speedDialDirMode == WP_platterControl.SPEED_DIAL_MODE_PRESET_UP) then speedBoneAngleRangeDeg = { from = 90, to = 0 } elseif (WP_platterControl.speedDialDirMode == WP_platterControl.SPEED_DIAL_MODE_PRESET_RIGHT) then speedBoneAngleRangeDeg = { from = 0, to = -90 } elseif (WP_platterControl.speedDialDirMode == WP_platterControl.SPEED_DIAL_MODE_PRESET_DOWN) then speedBoneAngleRangeDeg = { from = 270, to = 180 } end -- GET BONES local speedBone = skeleton:Bone(WP_platterControl.speedBoneId) local platterBones = {} local platterBonesCount = 0 if (WP_platterControl.platterBoneIds ~= nil) then platterBonesCount = WP_utils:arrLen(WP_platterControl.platterBoneIds) for i = 1, platterBonesCount, 1 do table.insert(platterBones, skeleton:Bone(WP_platterControl.platterBoneIds[i])) end end -- CHECK IF SPEED BONE HAS ROTATION KEYFRAMES if (speedBone.fAnimAngle:CountKeys() == 0) then local resetAlert = WP_utils:alert('The speed bone you selected doesn\'t have any keyframes on the rotation channel. Are you sure you want to continue?', true) if (resetAlert == 1) then return nil end end -- PREP MULTI UNDO AND RAISE DIRTY FLAG mohoDoc:PrepMultiUndo() mohoDoc:SetDirty() local platterBonesCount = WP_utils:arrLen(platterBones) -- WRITE KEYFRAMES local dir if (WP_platterControl.ccwDir == true) then dir = 1 else dir = -1 end local speed local speedAngleDeg local speedRangeDeg = speedBoneAngleRangeDeg.to - speedBoneAngleRangeDeg.from local platterBoneRotRad = {} for i = 1, platterBonesCount, 1 do platterBoneRotRad[i] = platterBones[i].fAnimAngle:GetValue(docStartFrame) end local isIntervalFrame local isFrameToWrite local prevSpeed = nil local inRangeFr = 0 -- FRAME NUMBER FROM START OF RANGE (1-BASED, LIKE MOHO'S TIMELINE FRAME NUMBER) local calcStartFrame = docStartFrame local calcEndFrame = math.min(docEndFrame, WP_platterControl.rangeEndFr) -- CLEAR KEYFRAMES BEFORE WRITING NEW ONES if (WP_platterControl.clearAllKeyframes) then -- CLEAR COMPLETE TIMELINE ON THE PLATTER BONES' ANGLE CHANNELS for i = 1, platterBonesCount, 1 do platterBones[i].fAnimAngle:Clear() end else -- ONLY CLEAR RANGE ON THE PLATTER BONES' ANGLE CHANNELS for i = 1, platterBonesCount, 1 do for fr = WP_platterControl.rangeStartFr, WP_platterControl.rangeEndFr, 1 do platterBones[i].fAnimAngle:DeleteKey(fr) end end end for fr = calcStartFrame, calcEndFrame, 1 do if (WP_platterControl.useRealSpeedAngle) then -- GET ACTUAL ANGLE VALUE (ALSO WORKS WHEN SPEED BONE IS SET BY SMART BONE ACTION OR CONTROLLER BONE, BUT IS SUPER SLOW) moho:SetCurFrame(fr) speedAngleDeg = WP_utils:rad2deg(speedBone.fAngle) else -- GET ANIMATION VALUE FOR SPEED BONE (DOESN'T WORK WHEN IS CONTROLLED BY SMART BONE ACTION OR CONTROLER BONE, BUT IS SUPER FAST) speedAngleDeg = WP_utils:rad2deg(speedBone.fAnimAngle:GetValue(fr)) end -- CALC SPEED speed = (speedAngleDeg - speedBoneAngleRangeDeg.from) / speedRangeDeg -- CALCULATE AND SET PLATTER ROTATION for i = 1, platterBonesCount, 1 do -- CALC NEW ANGLE platterBoneRotRad[i] = platterBoneRotRad[i] + (2 * math.pi * rotPerFr * dir * speed); -- LIMIT ANGLE TO CYCLES if (WP_platterControl.cyclesMode == WP_platterControl.CYCLES_MODE_SINGLE_CYCLE or WP_platterControl.cyclesMode == WP_platterControl.CYCLES_MODE_MULTI_CYCLES_LIMITED) then platterBoneRotRad[i] = math.fmod(platterBoneRotRad[i], 2 * math.pi * WP_platterControl.cyclesCount) if (platterBoneRotRad[i] > 0) then platterBoneRotRad[i] = -2 * math.pi * WP_platterControl.cyclesCount + platterBoneRotRad[i] end end end -- SHOULD KEY BE WRITTEN? if (fr >= WP_platterControl.rangeStartFr and fr <= WP_platterControl.rangeEndFr) then -- INCREASE FRAME INDEX IN RANGE inRangeFr = inRangeFr + 1 if (WP_platterControl.alwaysWriteKeyAtRangeStartAndEnd and (fr == WP_platterControl.rangeStartFr or fr == WP_platterControl.rangeEndFr)) then isFrameToWrite = true else if (WP_platterControl.onlyWriteKeyWhenSpeedChanges) then -- CHECK IF CURRENT POSITION IS ON KEYFRAME FRAME OF SPEED BONE isFrameToWrite = prevSpeed == nil or speed ~= prevSpeed else -- ONLY WRITE KEYFRAME WHEN ON INTERVAL local frForInterval if (WP_platterControl.startFrameIntervalAtTimelineStart) then frForInterval = fr else frForInterval = inRangeFr end isIntervalFrame = math.fmod( frForInterval - 1 - WP_platterControl.frameIntervalOffsetFrames, WP_platterControl.frameInterval) == 0 isFrameToWrite = isIntervalFrame end end else isFrameToWrite = false end -- WRITE KEYFRAME if (isFrameToWrite) then for i = 1, platterBonesCount, 1 do platterBones[i].fAnimAngle:SetValue(fr, platterBoneRotRad[i]) if (WP_platterControl.frameInterval == 1) then platterBones[i].fAnimAngle:SetKeyInterp(fr, MOHO.INTERP_SMOOTH) else platterBones[i].fAnimAngle:SetKeyInterp(fr, MOHO.INTERP_STEP) end end end prevSpeed = speed end -- GO TO START moho:SetCurFrame(0) -- END if (WP_platterControl.showDoneConfirmation) then WP_utils:alert('Done!') end end
WP Platter Control
Listed
Author: wigglepixel
View Script
Script type: Tool
Uploaded: Dec 07 2023, 18:59
Last modified: Jan 02 2024, 07:46
Script Version: 1.3.0
Animate (rotational) Bone Speeds with Accerelation/Deceleration with ease by Keyframing Speed instead of Angles.
DEMO/TUTORIAL
Short tutorial video on how to use the script (older version, but the basics are still the same) to generate rotating animations in Moho 14:
EXAMPLES
Example of an animation using the script to control a Reel to Reel tape deck (including controling smart bones controlling cut tapes on the tape passing the play head):
----------------------------------------------------------------------------------------------------------
DISCLAIMER
----------------------------------------------------------------------------------------------------------
I've tried my best to make this plugin as stable and useful as possible. But please bare in mind; I'm sharing this script, which I've created for doing my own animation work, for others to use for free as I think this could help others too. But this script/this plugin is 'as is'. There might be an update in the future, but that's not garanteed and there is no active support. Nor a garantee (always backup first!). That said; I hope you enjoy it and it helps you with your animation work!
----------------------------------------------------------------------------------------------------------
VERSION HISTORY
----------------------------------------------------------------------------------------------------------
1.3.0 [current]
- Added: New mode added: 'Cycle Range on repeat'. This new mode makes it possible to repeat over more than a single cycle (360 degrees) of the platterbone while still being limited to the amount of cycles entered in the 'Cycles' field (so platter bone angle won't unlimitedly increase forever but stays within cycles range). This way you can for example have a platter bone with a smart bone action ranging from 0 to -720 degrees (= 2 cycles of 360 degrees each) in which you can do all kinds of things, like letting a character walk forwards from 0 to -360 degrees and backwards from -360 to -719 degrees to create things like pingpong loops. So in this example; when the speed is 100% the first cycle will show the character walking forward and the second cycle will be the character walking backwards and after that repeats that sequence forever while the speed is 100% (or plays it accordingly to a different speed value or even in reverse when speed is -100% ???? ).
- Added: You can now apply speed changes to multiple platter bones at once using multi select in the platter bones list
- Added/Changed: The frame range that will be written in the rotation channels of platter bones will now be cleared from keyframes before starting writing new ones so there's never a left over keyframe there before writing new ones
- Changed: On higher frame intervals written keys now get the 'Step' interpolation. On one's these will now always be set to 'smooth'. So keyframes now explicitely gets set to an interpolation type.
1.2.0
- Added: Speed Dial Bone Zero Speed direction setting. With this setting you can set what direction of that bone is representing the zero speed value. You can choose between Left [default, and as before], Up, Right or Down
- Added: Introducing Cycles modes. Currently there are two modes: one to render only single 360 degrees cycles on repeat and one to keep increasing angles to go over a single cycle. This also replaces the old 'Keep angles within 360 degrees' checkbox.
- Improved: Now stops calculating when end of set range is reached. So you don't need to wait 'till the end of the timeline no more.
- Changed: Added 'Experimental' to 'Only write Keys if Speed changes' because it's not fully tested and not sure if it's a valid option
- Added: more RPM presets
- Added: Show how many frames a cycle takes when choosing an RPM
- Fixed: some little issues
- Improved: Better dialog window layout
1.1.3
- Changed: Made compatible with newer utils file
1.1.2
- Added/Fixed: Moho doesn't always return the real value of the bone's angle. The script couldn't get the speed angle's value when the speed bone was controlled by a smart bone action causing the speed to never change and the script to fail.
To work around this/fix the script could use another method to get the REAL angle values of the speed bone, but that method is extremely slow. The difference is literally between the blink of an eye and waiting seconds to minutes for the baking to finish.
So I added a setting to enable this way slower, but always working, baking method. By default it's turned off (= speed baking), but when you are controlling the speed bone not by it's own keyframes, but from within a smart bone action (or controller bone), than you need to turn this setting on, otherwise the script won't work as expected.
- Changed/Fixed: Removed the setting to only write keys on frames where the speed bone dial had a keyframe. This turned out to not be very accurate/useful. Replaced by a new setting to only write keys when the speed actually changes.
- Removed: The experimental 'never repeat the same keyframe' turned out to be not useful. This was meant to optimize keyframe writing, but it's missing important features and caused issues so removed this for now in favour of real optimizations later.
- Changed: The speed bone list isn't by default filtered on only bones with keyframes any longer. Because bones controlled by smart bones don't have keyframes themselves, but are still great speed bones to use with the script. You can still the filtering on if you like.
- Added: Now shows the script version number in the dialog box title
1.1.1
- Changed: Now by default the script limits writing rotation values to a 360 degrees range. This fixes an issue where smart bone actions only ran the first 360 degrees cycle and weren't doing anything anymore after passing the (minus) 360 degrees range.
- Added: Checkbox to turn off the 360 degrees limitation (advised to never turn this off though, unless you know what you're doing and really need to go beyond 360!)
- Added: Shows script version number in dialog title.
1.1.0
- Initial release of script
----------------------------------------------------------------------------------------------------------
HOW TO INSTALL?
----------------------------------------------------------------------------------------------------------
How to install the script in Moho?
* Click on the 'Download for Install Script command''-button below to download the zip-file
* Unzip the downloaded file
* Start Moho
* Open the Scripts menu --> choose 'Install Script' --> Browse through the unziped folder and choose the folder named 'wp_platter_control_for-moho-install-script-command' --> It should now install the script
* Once installation is done Moho shows a dialog saying 'Installation Complete!'
* Restart Moho (only after restarting the tool will become available)
* You should now see the tool in the tools panel. Ready to use!
----------------------------------------------------------------------------------------------------------
ABOUT THE CURRENT VERSION
----------------------------------------------------------------------------------------------------------
Why I created this script?
For the quick music video's I create for my music on YouTube I often struggled with the limitations of both Moho Pro and Spine Pro in relation to rotating elements because both are focussed on animating from pose to pose, but and aren't very convenient to animate cycling rotations which need to be able to accelerate, decelerate, change speed, keep running 'endlessly' at an exact speed when at full speed (given by RPM) and also do all this scalable and quickly without any manual calculations and complexity.
To create this kind of animations we need to animate speed, instead of angles. But Moho cannot animate speed, but only angles. So that's why this script converts angles to speed to make all this very easy now for everything that needs to gradually speed up, slow down and reverse! And with just four keyframes on the speed dial bone we can now create a record speeding up, run for some minutes and slow down to a stop at the end, within seconds to minutes instead of hours (and a lot of headaches)!
Especially because Moho has the great feature of Smart Bones, with this script we can now even accelerate and decelerated looped action timelines of smart bones!
The possibilities are as endless as the cycling platters. It's now easy to animate all kinds of elements that need to change speed, like windmills, records players, tape decks, car wheels, airplane propellors, walkcycles, car traffic on a high way etc. Just to name a few.
What does the script do?
The scripts generates keyframes on the rotation channel of a (platter) bone to make it rotate at a certain speed by reading the animated angle from a Speed Dial bone. Basically it's converting an angle of a bone to rotating cycles with speed on another bone. Like this it's possible to quickly and painlessly generate rotating cycles with acceleration, deceleration etc.
How to use the script?
This is a new script! Backup your moho file before use just to be sure!
Please watch the short tutorial video above for more instructions.
Quick start
1. Make sure that you have a bone that needs to rotate using this script (the 'platter bone' ).
2. Add a bone to use as speed dial. Caution! While Moho works with counter clockwise angles, this script intentionally works differently and uses clockwise to make things a little easier compared to the real world:
- Point the bone to the left (the bone angle in moho is 180 degrees) for zero speed
- Point the bone up (the bone angle in Moho is 90 degrees) for full forward speed (100%)
- Point the bone down (the bone angle in Moho is 270 degrees) for full reversed speed (-100%)
(Since version 1.2.0 it's also possible to have the zero speed bone direction pointing up, right or down by a new setting in the script, but having zero speed when the bone is pointing to the left is the default)
3. Make sure this speed dial bone is not influencing any image or vector layer when rotating:
- Go to frame 0 (setup frame)
- Select the bone layer and the speed dial bone
- Select the bone strength tool (S on your keyboard)
- Change the Bone strength of the bone to 0 in the tool properties
4. Animate the rotation of this speed dial bone to change the speed over time (see above for the range).
5. Open the WP Platter Control Tool. Please watch the short tutorial video for more instructions, but the core part is:
- Select the speed bone in the list on the left
- Select the platter bone in the list on the right
- Choose a cycle speed (RPM), or leave it at the default
- Hit OK to generate. The script will now look from frame to frame at the angle of the speed dial bone you just animated, and generate keyframes on the platter bone's rotation channel accordingly to convert this angle into platter speed.
6. Play the timeline!
Advanced: About creating a smart bone action to control with this script
This script only generates rotation cycles with speeds, acceleration and deceleration. But when using this to control a smart bone action you can basically control every other movement and complete timelines even! This makes the platter script even more powerful.
When you like to control a smart bone action with the script make sure your smart bone rotates CLOCKWISE. Meaning that while setting up the smart bone timeline the smart bone needs to rotate from 0 degrees to minus (!!) 359 degrees. That's considered forward by the script and is called a single cycle.
Even if you like to have that bone rotate counter clockwise when controlling it later, it's important to set it up like this. You can always turn on 'Counter Clockwise' when running the script to reverse direction!
[update since v1.3.0]
From v1.3.0 unwards it's possible to let the smart bone action contain more than a single cycle of 360 degrees. When you switch the mode of the Platter Control to 'Cycle Range on repeat' you can set a higher amount of cycles (360 degrees each) you want to use in the smart bone action. For instance; let's say you want to let a character walk from left to right in the first cycle (0 to -359 degrees on the smartbone) and than back to left again (-360 to -719 degrees on the smartbone) you can do that now by switching to that mode and setting the amount of Cycles to 2.
The difference of this mode compared to the 'Unlimited Increasing Cycles' mode is that this mode has a limited amount of cycles (and therefor limited agrees on the platter bone) while the 'Unlimited Increasing Cycles' mode doesn't have any limit and keeps increasing degrees 'forever', never looping.
-----------------------------------------------------------------------------------------------------------
KNOWN LIMITATIONS AND WORKAROUNDS
-----------------------------------------------------------------------------------------------------------
Speed bones that are controlled by Smart Bone Actions or Controller bones gives unexpected results
Moho doesn't always return the real value of the bone's angle. The script can't by default get the speed angle's value when the speed bone is controlled by a smart bone action causing the speed to never change and the script to fail.
Since version 1.1.2 there's a setting to fix this. It's making the baking extremely slow unfortunately, but it get's the real angle value of the speed dial and so also works in scenarios where the speed dial doesn't have keyframes itself, but is controlled from somewhere else.
Just turn on the setting 'Use Real Speed Bone Angle' under 'Keyframe writing' and grab a cup of coffee while the baking is working...
Supported Moho version
Tested in Moho 14.1
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: 596
WP Platter Control
Listed
Author: wigglepixel
View Script
Script type: Tool
Uploaded: Dec 07 2023, 18:59
Last modified: Jan 02 2024, 07:46
Script Version: 1.3.0
Animate (rotational) Bone Speeds with Accerelation/Deceleration with ease by Keyframing Speed instead of Angles.
DEMO/TUTORIAL
Short tutorial video on how to use the script (older version, but the basics are still the same) to generate rotating animations in Moho 14:
EXAMPLES
Example of an animation using the script to control a Reel to Reel tape deck (including controling smart bones controlling cut tapes on the tape passing the play head):
----------------------------------------------------------------------------------------------------------
DISCLAIMER
----------------------------------------------------------------------------------------------------------
I've tried my best to make this plugin as stable and useful as possible. But please bare in mind; I'm sharing this script, which I've created for doing my own animation work, for others to use for free as I think this could help others too. But this script/this plugin is 'as is'. There might be an update in the future, but that's not garanteed and there is no active support. Nor a garantee (always backup first!). That said; I hope you enjoy it and it helps you with your animation work!
----------------------------------------------------------------------------------------------------------
VERSION HISTORY
----------------------------------------------------------------------------------------------------------
1.3.0 [current]
- Added: New mode added: 'Cycle Range on repeat'. This new mode makes it possible to repeat over more than a single cycle (360 degrees) of the platterbone while still being limited to the amount of cycles entered in the 'Cycles' field (so platter bone angle won't unlimitedly increase forever but stays within cycles range). This way you can for example have a platter bone with a smart bone action ranging from 0 to -720 degrees (= 2 cycles of 360 degrees each) in which you can do all kinds of things, like letting a character walk forwards from 0 to -360 degrees and backwards from -360 to -719 degrees to create things like pingpong loops. So in this example; when the speed is 100% the first cycle will show the character walking forward and the second cycle will be the character walking backwards and after that repeats that sequence forever while the speed is 100% (or plays it accordingly to a different speed value or even in reverse when speed is -100% ???? ).
- Added: You can now apply speed changes to multiple platter bones at once using multi select in the platter bones list
- Added/Changed: The frame range that will be written in the rotation channels of platter bones will now be cleared from keyframes before starting writing new ones so there's never a left over keyframe there before writing new ones
- Changed: On higher frame intervals written keys now get the 'Step' interpolation. On one's these will now always be set to 'smooth'. So keyframes now explicitely gets set to an interpolation type.
1.2.0
- Added: Speed Dial Bone Zero Speed direction setting. With this setting you can set what direction of that bone is representing the zero speed value. You can choose between Left [default, and as before], Up, Right or Down
- Added: Introducing Cycles modes. Currently there are two modes: one to render only single 360 degrees cycles on repeat and one to keep increasing angles to go over a single cycle. This also replaces the old 'Keep angles within 360 degrees' checkbox.
- Improved: Now stops calculating when end of set range is reached. So you don't need to wait 'till the end of the timeline no more.
- Changed: Added 'Experimental' to 'Only write Keys if Speed changes' because it's not fully tested and not sure if it's a valid option
- Added: more RPM presets
- Added: Show how many frames a cycle takes when choosing an RPM
- Fixed: some little issues
- Improved: Better dialog window layout
1.1.3
- Changed: Made compatible with newer utils file
1.1.2
- Added/Fixed: Moho doesn't always return the real value of the bone's angle. The script couldn't get the speed angle's value when the speed bone was controlled by a smart bone action causing the speed to never change and the script to fail.
To work around this/fix the script could use another method to get the REAL angle values of the speed bone, but that method is extremely slow. The difference is literally between the blink of an eye and waiting seconds to minutes for the baking to finish.
So I added a setting to enable this way slower, but always working, baking method. By default it's turned off (= speed baking), but when you are controlling the speed bone not by it's own keyframes, but from within a smart bone action (or controller bone), than you need to turn this setting on, otherwise the script won't work as expected.
- Changed/Fixed: Removed the setting to only write keys on frames where the speed bone dial had a keyframe. This turned out to not be very accurate/useful. Replaced by a new setting to only write keys when the speed actually changes.
- Removed: The experimental 'never repeat the same keyframe' turned out to be not useful. This was meant to optimize keyframe writing, but it's missing important features and caused issues so removed this for now in favour of real optimizations later.
- Changed: The speed bone list isn't by default filtered on only bones with keyframes any longer. Because bones controlled by smart bones don't have keyframes themselves, but are still great speed bones to use with the script. You can still the filtering on if you like.
- Added: Now shows the script version number in the dialog box title
1.1.1
- Changed: Now by default the script limits writing rotation values to a 360 degrees range. This fixes an issue where smart bone actions only ran the first 360 degrees cycle and weren't doing anything anymore after passing the (minus) 360 degrees range.
- Added: Checkbox to turn off the 360 degrees limitation (advised to never turn this off though, unless you know what you're doing and really need to go beyond 360!)
- Added: Shows script version number in dialog title.
1.1.0
- Initial release of script
----------------------------------------------------------------------------------------------------------
HOW TO INSTALL?
----------------------------------------------------------------------------------------------------------
How to install the script in Moho?
* Click on the 'Download for Install Script command''-button below to download the zip-file
* Unzip the downloaded file
* Start Moho
* Open the Scripts menu --> choose 'Install Script' --> Browse through the unziped folder and choose the folder named 'wp_platter_control_for-moho-install-script-command' --> It should now install the script
* Once installation is done Moho shows a dialog saying 'Installation Complete!'
* Restart Moho (only after restarting the tool will become available)
* You should now see the tool in the tools panel. Ready to use!
----------------------------------------------------------------------------------------------------------
ABOUT THE CURRENT VERSION
----------------------------------------------------------------------------------------------------------
Why I created this script?
For the quick music video's I create for my music on YouTube I often struggled with the limitations of both Moho Pro and Spine Pro in relation to rotating elements because both are focussed on animating from pose to pose, but and aren't very convenient to animate cycling rotations which need to be able to accelerate, decelerate, change speed, keep running 'endlessly' at an exact speed when at full speed (given by RPM) and also do all this scalable and quickly without any manual calculations and complexity.
To create this kind of animations we need to animate speed, instead of angles. But Moho cannot animate speed, but only angles. So that's why this script converts angles to speed to make all this very easy now for everything that needs to gradually speed up, slow down and reverse! And with just four keyframes on the speed dial bone we can now create a record speeding up, run for some minutes and slow down to a stop at the end, within seconds to minutes instead of hours (and a lot of headaches)!
Especially because Moho has the great feature of Smart Bones, with this script we can now even accelerate and decelerated looped action timelines of smart bones!
The possibilities are as endless as the cycling platters. It's now easy to animate all kinds of elements that need to change speed, like windmills, records players, tape decks, car wheels, airplane propellors, walkcycles, car traffic on a high way etc. Just to name a few.
What does the script do?
The scripts generates keyframes on the rotation channel of a (platter) bone to make it rotate at a certain speed by reading the animated angle from a Speed Dial bone. Basically it's converting an angle of a bone to rotating cycles with speed on another bone. Like this it's possible to quickly and painlessly generate rotating cycles with acceleration, deceleration etc.
How to use the script?
This is a new script! Backup your moho file before use just to be sure!
Please watch the short tutorial video above for more instructions.
Quick start
1. Make sure that you have a bone that needs to rotate using this script (the 'platter bone' ).
2. Add a bone to use as speed dial. Caution! While Moho works with counter clockwise angles, this script intentionally works differently and uses clockwise to make things a little easier compared to the real world:
- Point the bone to the left (the bone angle in moho is 180 degrees) for zero speed
- Point the bone up (the bone angle in Moho is 90 degrees) for full forward speed (100%)
- Point the bone down (the bone angle in Moho is 270 degrees) for full reversed speed (-100%)
(Since version 1.2.0 it's also possible to have the zero speed bone direction pointing up, right or down by a new setting in the script, but having zero speed when the bone is pointing to the left is the default)
3. Make sure this speed dial bone is not influencing any image or vector layer when rotating:
- Go to frame 0 (setup frame)
- Select the bone layer and the speed dial bone
- Select the bone strength tool (S on your keyboard)
- Change the Bone strength of the bone to 0 in the tool properties
4. Animate the rotation of this speed dial bone to change the speed over time (see above for the range).
5. Open the WP Platter Control Tool. Please watch the short tutorial video for more instructions, but the core part is:
- Select the speed bone in the list on the left
- Select the platter bone in the list on the right
- Choose a cycle speed (RPM), or leave it at the default
- Hit OK to generate. The script will now look from frame to frame at the angle of the speed dial bone you just animated, and generate keyframes on the platter bone's rotation channel accordingly to convert this angle into platter speed.
6. Play the timeline!
Advanced: About creating a smart bone action to control with this script
This script only generates rotation cycles with speeds, acceleration and deceleration. But when using this to control a smart bone action you can basically control every other movement and complete timelines even! This makes the platter script even more powerful.
When you like to control a smart bone action with the script make sure your smart bone rotates CLOCKWISE. Meaning that while setting up the smart bone timeline the smart bone needs to rotate from 0 degrees to minus (!!) 359 degrees. That's considered forward by the script and is called a single cycle.
Even if you like to have that bone rotate counter clockwise when controlling it later, it's important to set it up like this. You can always turn on 'Counter Clockwise' when running the script to reverse direction!
[update since v1.3.0]
From v1.3.0 unwards it's possible to let the smart bone action contain more than a single cycle of 360 degrees. When you switch the mode of the Platter Control to 'Cycle Range on repeat' you can set a higher amount of cycles (360 degrees each) you want to use in the smart bone action. For instance; let's say you want to let a character walk from left to right in the first cycle (0 to -359 degrees on the smartbone) and than back to left again (-360 to -719 degrees on the smartbone) you can do that now by switching to that mode and setting the amount of Cycles to 2.
The difference of this mode compared to the 'Unlimited Increasing Cycles' mode is that this mode has a limited amount of cycles (and therefor limited agrees on the platter bone) while the 'Unlimited Increasing Cycles' mode doesn't have any limit and keeps increasing degrees 'forever', never looping.
-----------------------------------------------------------------------------------------------------------
KNOWN LIMITATIONS AND WORKAROUNDS
-----------------------------------------------------------------------------------------------------------
Speed bones that are controlled by Smart Bone Actions or Controller bones gives unexpected results
Moho doesn't always return the real value of the bone's angle. The script can't by default get the speed angle's value when the speed bone is controlled by a smart bone action causing the speed to never change and the script to fail.
Since version 1.1.2 there's a setting to fix this. It's making the baking extremely slow unfortunately, but it get's the real angle value of the speed dial and so also works in scenarios where the speed dial doesn't have keyframes itself, but is controlled from somewhere else.
Just turn on the setting 'Use Real Speed Bone Angle' under 'Keyframe writing' and grab a cup of coffee while the baking is working...
Supported Moho version
Tested in Moho 14.1
Short tutorial video on how to use the script (older version, but the basics are still the same) to generate rotating animations in Moho 14:
EXAMPLES
Example of an animation using the script to control a Reel to Reel tape deck (including controling smart bones controlling cut tapes on the tape passing the play head):
----------------------------------------------------------------------------------------------------------
DISCLAIMER
----------------------------------------------------------------------------------------------------------
I've tried my best to make this plugin as stable and useful as possible. But please bare in mind; I'm sharing this script, which I've created for doing my own animation work, for others to use for free as I think this could help others too. But this script/this plugin is 'as is'. There might be an update in the future, but that's not garanteed and there is no active support. Nor a garantee (always backup first!). That said; I hope you enjoy it and it helps you with your animation work!
----------------------------------------------------------------------------------------------------------
VERSION HISTORY
----------------------------------------------------------------------------------------------------------
1.3.0 [current]
- Added: New mode added: 'Cycle Range on repeat'. This new mode makes it possible to repeat over more than a single cycle (360 degrees) of the platterbone while still being limited to the amount of cycles entered in the 'Cycles' field (so platter bone angle won't unlimitedly increase forever but stays within cycles range). This way you can for example have a platter bone with a smart bone action ranging from 0 to -720 degrees (= 2 cycles of 360 degrees each) in which you can do all kinds of things, like letting a character walk forwards from 0 to -360 degrees and backwards from -360 to -719 degrees to create things like pingpong loops. So in this example; when the speed is 100% the first cycle will show the character walking forward and the second cycle will be the character walking backwards and after that repeats that sequence forever while the speed is 100% (or plays it accordingly to a different speed value or even in reverse when speed is -100% ???? ).
- Added: You can now apply speed changes to multiple platter bones at once using multi select in the platter bones list
- Added/Changed: The frame range that will be written in the rotation channels of platter bones will now be cleared from keyframes before starting writing new ones so there's never a left over keyframe there before writing new ones
- Changed: On higher frame intervals written keys now get the 'Step' interpolation. On one's these will now always be set to 'smooth'. So keyframes now explicitely gets set to an interpolation type.
1.2.0
- Added: Speed Dial Bone Zero Speed direction setting. With this setting you can set what direction of that bone is representing the zero speed value. You can choose between Left [default, and as before], Up, Right or Down
- Added: Introducing Cycles modes. Currently there are two modes: one to render only single 360 degrees cycles on repeat and one to keep increasing angles to go over a single cycle. This also replaces the old 'Keep angles within 360 degrees' checkbox.
- Improved: Now stops calculating when end of set range is reached. So you don't need to wait 'till the end of the timeline no more.
- Changed: Added 'Experimental' to 'Only write Keys if Speed changes' because it's not fully tested and not sure if it's a valid option
- Added: more RPM presets
- Added: Show how many frames a cycle takes when choosing an RPM
- Fixed: some little issues
- Improved: Better dialog window layout
1.1.3
- Changed: Made compatible with newer utils file
1.1.2
- Added/Fixed: Moho doesn't always return the real value of the bone's angle. The script couldn't get the speed angle's value when the speed bone was controlled by a smart bone action causing the speed to never change and the script to fail.
To work around this/fix the script could use another method to get the REAL angle values of the speed bone, but that method is extremely slow. The difference is literally between the blink of an eye and waiting seconds to minutes for the baking to finish.
So I added a setting to enable this way slower, but always working, baking method. By default it's turned off (= speed baking), but when you are controlling the speed bone not by it's own keyframes, but from within a smart bone action (or controller bone), than you need to turn this setting on, otherwise the script won't work as expected.
- Changed/Fixed: Removed the setting to only write keys on frames where the speed bone dial had a keyframe. This turned out to not be very accurate/useful. Replaced by a new setting to only write keys when the speed actually changes.
- Removed: The experimental 'never repeat the same keyframe' turned out to be not useful. This was meant to optimize keyframe writing, but it's missing important features and caused issues so removed this for now in favour of real optimizations later.
- Changed: The speed bone list isn't by default filtered on only bones with keyframes any longer. Because bones controlled by smart bones don't have keyframes themselves, but are still great speed bones to use with the script. You can still the filtering on if you like.
- Added: Now shows the script version number in the dialog box title
1.1.1
- Changed: Now by default the script limits writing rotation values to a 360 degrees range. This fixes an issue where smart bone actions only ran the first 360 degrees cycle and weren't doing anything anymore after passing the (minus) 360 degrees range.
- Added: Checkbox to turn off the 360 degrees limitation (advised to never turn this off though, unless you know what you're doing and really need to go beyond 360!)
- Added: Shows script version number in dialog title.
1.1.0
- Initial release of script
----------------------------------------------------------------------------------------------------------
HOW TO INSTALL?
----------------------------------------------------------------------------------------------------------
How to install the script in Moho?
* Click on the 'Download for Install Script command''-button below to download the zip-file
* Unzip the downloaded file
* Start Moho
* Open the Scripts menu --> choose 'Install Script' --> Browse through the unziped folder and choose the folder named 'wp_platter_control_for-moho-install-script-command' --> It should now install the script
* Once installation is done Moho shows a dialog saying 'Installation Complete!'
* Restart Moho (only after restarting the tool will become available)
* You should now see the tool in the tools panel. Ready to use!
----------------------------------------------------------------------------------------------------------
ABOUT THE CURRENT VERSION
----------------------------------------------------------------------------------------------------------
Why I created this script?
For the quick music video's I create for my music on YouTube I often struggled with the limitations of both Moho Pro and Spine Pro in relation to rotating elements because both are focussed on animating from pose to pose, but and aren't very convenient to animate cycling rotations which need to be able to accelerate, decelerate, change speed, keep running 'endlessly' at an exact speed when at full speed (given by RPM) and also do all this scalable and quickly without any manual calculations and complexity.
To create this kind of animations we need to animate speed, instead of angles. But Moho cannot animate speed, but only angles. So that's why this script converts angles to speed to make all this very easy now for everything that needs to gradually speed up, slow down and reverse! And with just four keyframes on the speed dial bone we can now create a record speeding up, run for some minutes and slow down to a stop at the end, within seconds to minutes instead of hours (and a lot of headaches)!
Especially because Moho has the great feature of Smart Bones, with this script we can now even accelerate and decelerated looped action timelines of smart bones!
The possibilities are as endless as the cycling platters. It's now easy to animate all kinds of elements that need to change speed, like windmills, records players, tape decks, car wheels, airplane propellors, walkcycles, car traffic on a high way etc. Just to name a few.
What does the script do?
The scripts generates keyframes on the rotation channel of a (platter) bone to make it rotate at a certain speed by reading the animated angle from a Speed Dial bone. Basically it's converting an angle of a bone to rotating cycles with speed on another bone. Like this it's possible to quickly and painlessly generate rotating cycles with acceleration, deceleration etc.
How to use the script?
This is a new script! Backup your moho file before use just to be sure!
Please watch the short tutorial video above for more instructions.
Quick start
1. Make sure that you have a bone that needs to rotate using this script (the 'platter bone' ).
2. Add a bone to use as speed dial. Caution! While Moho works with counter clockwise angles, this script intentionally works differently and uses clockwise to make things a little easier compared to the real world:
- Point the bone to the left (the bone angle in moho is 180 degrees) for zero speed
- Point the bone up (the bone angle in Moho is 90 degrees) for full forward speed (100%)
- Point the bone down (the bone angle in Moho is 270 degrees) for full reversed speed (-100%)
(Since version 1.2.0 it's also possible to have the zero speed bone direction pointing up, right or down by a new setting in the script, but having zero speed when the bone is pointing to the left is the default)
3. Make sure this speed dial bone is not influencing any image or vector layer when rotating:
- Go to frame 0 (setup frame)
- Select the bone layer and the speed dial bone
- Select the bone strength tool (S on your keyboard)
- Change the Bone strength of the bone to 0 in the tool properties
4. Animate the rotation of this speed dial bone to change the speed over time (see above for the range).
5. Open the WP Platter Control Tool. Please watch the short tutorial video for more instructions, but the core part is:
- Select the speed bone in the list on the left
- Select the platter bone in the list on the right
- Choose a cycle speed (RPM), or leave it at the default
- Hit OK to generate. The script will now look from frame to frame at the angle of the speed dial bone you just animated, and generate keyframes on the platter bone's rotation channel accordingly to convert this angle into platter speed.
6. Play the timeline!
Advanced: About creating a smart bone action to control with this script
This script only generates rotation cycles with speeds, acceleration and deceleration. But when using this to control a smart bone action you can basically control every other movement and complete timelines even! This makes the platter script even more powerful.
When you like to control a smart bone action with the script make sure your smart bone rotates CLOCKWISE. Meaning that while setting up the smart bone timeline the smart bone needs to rotate from 0 degrees to minus (!!) 359 degrees. That's considered forward by the script and is called a single cycle.
Even if you like to have that bone rotate counter clockwise when controlling it later, it's important to set it up like this. You can always turn on 'Counter Clockwise' when running the script to reverse direction!
[update since v1.3.0]
From v1.3.0 unwards it's possible to let the smart bone action contain more than a single cycle of 360 degrees. When you switch the mode of the Platter Control to 'Cycle Range on repeat' you can set a higher amount of cycles (360 degrees each) you want to use in the smart bone action. For instance; let's say you want to let a character walk from left to right in the first cycle (0 to -359 degrees on the smartbone) and than back to left again (-360 to -719 degrees on the smartbone) you can do that now by switching to that mode and setting the amount of Cycles to 2.
The difference of this mode compared to the 'Unlimited Increasing Cycles' mode is that this mode has a limited amount of cycles (and therefor limited agrees on the platter bone) while the 'Unlimited Increasing Cycles' mode doesn't have any limit and keeps increasing degrees 'forever', never looping.
-----------------------------------------------------------------------------------------------------------
KNOWN LIMITATIONS AND WORKAROUNDS
-----------------------------------------------------------------------------------------------------------
Speed bones that are controlled by Smart Bone Actions or Controller bones gives unexpected results
Moho doesn't always return the real value of the bone's angle. The script can't by default get the speed angle's value when the speed bone is controlled by a smart bone action causing the speed to never change and the script to fail.
Since version 1.1.2 there's a setting to fix this. It's making the baking extremely slow unfortunately, but it get's the real angle value of the speed dial and so also works in scenarios where the speed dial doesn't have keyframes itself, but is controlled from somewhere else.
Just turn on the setting 'Use Real Speed Bone Angle' under 'Keyframe writing' and grab a cup of coffee while the baking is working...
Supported Moho version
Tested in Moho 14.1
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: 596