XY Pad with Optional Motion + Speed Control (HISE Script)
-
Hey everyone,
After my question yesterday I wanted to share a the script I’ve been working on. Would love to hear thoughts on updates as to whether this is the most sensible approach. Particular mention should go to @David-Healey whose original XY Pad videos sent me down the rabbit whole with this and did a lot of the heavy lifting!
Overall it’s an XY Pad that supports:
- Manual user control via mouse or XY knobs
- Optional motion driven by a simple sine-wave “LFO”
- Adjustable motion speed
- Fully modular and easy to extend
The idea is that you can move the XY pad automatically, but still allow the user to take manual control at any time.
Features
- btnMotion: toggle automatic motion on/off
- knbMotionSpeed: adjust the speed of the motion
- Motion is applied as a small offset on top of the manual X/Y position
- Gains are mapped from the XY pad to two gain modules, with scaling
How it works
- baseX/baseY store the manual (user) position
- lfoX/lfoY store motion offsets generated by the LFO
- updateXY() combines base + motion values and clamps them 0–1
- LFO speed is multiplied by motionSpeed for easy adjustment
/// =============================== /// XY PAD STATE /// =============================== // Manual (user) position reg baseX = 0.5; reg baseY = 0.5; // Motion offsets reg lfoX = 0.0; reg lfoY = 0.0; // Motion enable reg motionEnabled = false; /// Speed reg motionSpeed = 1.0; /// =============================== /// COMPONENTS /// =============================== const var xypad = Content.getComponent("xypad"); const var btnMotion = Content.getComponent("btnMotion"); const var knbMotionSpeed = Content.getComponent("knbMotionSpeed"); const SIZE = 10; /// =============================== /// GAINS /// =============================== const gains1 = [ Synth.getEffect("Simple Gain1"), Synth.getEffect("Simple Gain2") ]; /// =============================== /// XY PAD CORE CALLBACK /// =============================== xypad.setControlCallback(onxypadControl); inline function onxypadControl(component, value) { local x = component.data.x; local y = component.data.y; local gainX = -50 + (65 * x); local gainY = -50 + (65 * (1 - y)); gains1[0].setAttribute(gains1[0].GainValue, gainX); gains1[1].setAttribute(gains1[1].GainValue, gainY); knbxy[0].setValue(x); knbxy[1].setValue(y); component.repaint(); } /// =============================== /// PAINT ROUTINE /// =============================== xypad.setPaintRoutine(function(g) { g.fillAll(this.get("bgColour")); var x = Math.range(this.data.x * this.getWidth(), 0, this.getWidth() - SIZE); var y = Math.range(this.data.y * this.getHeight(), 0, this.getHeight() - SIZE); g.setColour(this.get("itemColour")); g.fillEllipse([x, y, SIZE, SIZE]); }); /// =============================== /// CENTRAL XY UPDATE /// =============================== inline function updateXY() { local modX = motionEnabled ? lfoX : 0.0; local modY = motionEnabled ? lfoY : 0.0; xypad.data.x = Math.range(baseX + modX, 0, 1); xypad.data.y = Math.range(baseY + modY, 0, 1); xypad.changed(); } /// =============================== /// MOUSE INPUT /// =============================== xypad.setMouseCallback(function(event) { if (event.clicked || event.drag) { baseX = Math.range(event.x / this.getWidth(), 0, 1); baseY = Math.range(event.y / this.getHeight(), 0, 1); updateXY(); } }); /// =============================== /// XY KNOBS /// =============================== const knbxy = []; for (i = 0; i < 2; i++) { knbxy.push(Content.getComponent("knbxy" + i)); knbxy[i].setControlCallback(onknbxyControl); } inline function onknbxyControl(component, value) { baseX = knbxy[0].getValue(); baseY = knbxy[1].getValue(); updateXY(); } /// =============================== /// MOTION BUTTON /// =============================== inline function onbtnMotionControl(component, value) { motionEnabled = value; }; Content.getComponent("btnMotion").setControlCallback(onbtnMotionControl); /// =============================== /// SPEED CONTROL /// =============================== inline function onknbMotionSpeedControl(component, value) { motionSpeed = value; }; Content.getComponent("knbMotionSpeed").setControlCallback(onknbMotionSpeedControl); /// =============================== /// LFO ENGINE /// =============================== const LFO_RATE_X = 0.35; const LFO_RATE_Y = 0.25; const LFO_DEPTH = 0.25; reg phaseX = 0.0; reg phaseY = 0.0; xypad.startTimer(30); xypad.setTimerCallback(function() { phaseX += LFO_RATE_X * motionSpeed * 0.03; phaseY += LFO_RATE_Y * motionSpeed * 0.03; if (phaseX > 1.0) phaseX -= 1.0; if (phaseY > 1.0) phaseY -= 1.0; var waveX = Math.sin(phaseX * Math.PI * 2); var waveY = Math.sin(phaseY * Math.PI * 2); lfoX = waveX * LFO_DEPTH * 0.5; lfoY = waveY * LFO_DEPTH * 0.5; updateXY(); }); -
any reason you used sin twice with extra logic rather than cos?
-
I think something like this should do about the same.
/// =============================== /// LFO ENGINE /// =============================== const LFO_RATE = 0.3; const LFO_DEPTH = 0.25; const TIME_SCALE = 0.03; reg phase = 0.0; xypad.startTimer(30); xypad.setTimerCallback(function() { phase = (phase + LFO_RATE * motionSpeed * TIME_SCALE) % 1.0; const angle = phase * Math.PI * 2; lfoX = Math.sin(angle) * LFO_DEPTH * 0.5; lfoY = Math.cos(angle) * LFO_DEPTH * 0.5; updateXY(); }); -
@xm4ddy Thanks for this I will have to drop it in and see how it works. Plenty to learn still!