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();
});