LOVE Adapter¶
feel.love is an opt-in adapter for LOVE-specific side effects. It keeps the core runner small while giving LOVE apps convenient handlers for sound, haptics, particles, shaders, post-processing, camera, and screen feedback.
Handlers¶
Pass fx:handlers(extra) to feel.play. The adapter handles registered audio cues and known adapter events first, then calls your extra.audio or extra.emit callback.
feel.play("hit", target, fx:handlers({
emit = function(event, ctx) end,
audio = function(event, ctx) end,
}))
Unknown cues and events are no-ops for the adapter, so user callbacks can still own project-specific effects.
Event Summary¶
| Area | Events |
|---|---|
| Sound | sound.play, sound.stop, sound.pause, sound.resume, sound.volume, sound.pitch, sound.pan |
| Haptics | haptic.play, haptic.stop, haptic.vibrate |
| Particles | particle.emit, particle.start, particle.stop, particle.reset, particle.move |
| Shaders | shader.send, shader.tween, shader.apply, shader.clear |
| Post | post.set, post.tween, post.enable, post.disable, post.weight, post.clear |
| Camera | camera.shake, camera.zoom, camera.move, camera.reset |
| Screen | screen.flash, screen.fade, screen.clear |
Sound¶
Register one cue with fx:sound(name, sourceOrSources, opts) or several with fx:sounds(map).
fx:sounds({
hit = love.audio.newSource("hit.wav", "static"),
ui = {
love.audio.newSource("ui-a.wav", "static"),
love.audio.newSource("ui-b.wav", "static"),
},
})
A cue may be one LOVE Source or an array of alternate sources. Playback chooses one alternate at random. By default playback calls stop() before play(); pass { restart = false } to keep already-playing sources running.
Audio steps play registered cues through fx:handlers().
Sound control events use emit:
{ kind = "emit", event = "sound.volume", payload = { cue = "hit", volume = 0.5, duration = 0.2 } }
{ kind = "emit", event = "sound.pitch", payload = { cue = "hit", pitch = 0.8, duration = 0.15 } }
{ kind = "emit", event = "sound.pan", payload = { cue = "hit", pan = -1, duration = 0.1 } }
Supported sound events are sound.play, sound.stop, sound.pause, sound.resume, sound.volume, sound.pitch, and sound.pan. Use fx:stopSound(name) or fx:stopSounds() for direct stop control.
Sound control tweens stack by default. Add restart = true to replace the previous tween for the same cue/property.
Sound Effect Recipes¶
Fade a looping ambience cue in, then fade it out later:
fx:sound("ambience", love.audio.newSource("ambience.ogg", "stream"), { restart = false, volume = 0 })
feel.define("ambience.in", {
{ kind = "emit", event = "sound.play", payload = { cue = "ambience" } },
{ kind = "emit", event = "sound.volume", payload = { cue = "ambience", volume = 0.65, duration = 1.2, ease = "quadout" } },
})
feel.define("ambience.out", {
{ kind = "emit", event = "sound.volume", payload = { cue = "ambience", volume = 0, duration = 0.8, ease = "quadin" } },
{ kind = "wait", duration = 0.8 },
{ kind = "emit", event = "sound.stop", payload = { cue = "ambience" } },
})
Duck a music bed under an impact, then restore it:
feel.define("impact.with.duck", {
{ kind = "emit", event = "sound.volume", payload = { cue = "music", volume = 0.35, duration = 0.08 } },
{ kind = "audio", cue = "impact" },
{ kind = "wait", duration = 0.18 },
{ kind = "emit", event = "sound.volume", payload = { cue = "music", volume = 1, duration = 0.45, ease = "quadout" } },
})
Bend pitch for slow-motion or power-up feedback:
feel.define("slowmo.hit", {
{ kind = "emit", event = "sound.pitch", payload = { cue = "hit", pitch = 0.72, duration = 0.12 } },
{ kind = "audio", cue = "hit" },
{ kind = "emit", event = "sound.pitch", payload = { cue = "hit", pitch = 1, duration = 0.3, ease = "backout" } },
})
Sweep a cue across stereo space:
feel.define("ui.sweep", {
{ kind = "emit", event = "sound.pan", payload = { cue = "ui", pan = -1 } },
{ kind = "audio", cue = "ui" },
{ kind = "emit", event = "sound.pan", payload = { cue = "ui", pan = 1, duration = 0.25, ease = "quadout" } },
{ kind = "wait", duration = 0.25 },
{ kind = "emit", event = "sound.pan", payload = { cue = "ui", pan = 0, duration = 0.15 } },
})
Haptics¶
Register app-owned joysticks with fx:haptic(name, joystickOrJoysticks, opts) or fx:haptics(map).
Use haptic.play for cross-platform feedback. By default it rumbles registered joystick targets and also calls love.system.vibrate(duration) when LOVE exposes system vibration.
feel.define("impact.rumble", {
{ kind = "emit", event = "haptic.play", payload = { value = 0.6, duration = 0.18 } },
})
value drives both motors. left and right override that value independently for controllers that support separate motors.
feel.define("impact.sweep", {
{ kind = "emit", event = "haptic.play", payload = { left = 0.2, right = 0.9, duration = 0.22 } },
})
For multiplayer games, include name to target one registered joystick. Pass system = false when a controller-only pulse should not also vibrate the mobile/system device.
feel.define("p1.damage", {
{ kind = "emit", event = "haptic.play", payload = { name = "p1", value = 0.8, duration = 0.16, system = false } },
})
Supported haptic events are haptic.play, haptic.stop, and haptic.vibrate. Use fx:stopHaptic(name) or fx:stopHaptics() for direct stop control.
feel.define("rumble.stop", {
{ kind = "emit", event = "haptic.stop", payload = {} },
})
feel.define("mobile.vibrate", {
{ kind = "emit", event = "haptic.vibrate", payload = { duration = 0.2 } },
})
Particles¶
Register app-created LOVE ParticleSystems with fx:particle(name, system, opts) or fx:particles(map).
local image = love.graphics.newImage("spark.png")
local sparks = love.graphics.newParticleSystem(image, 256)
sparks:setParticleLifetime(0.25, 0.7)
sparks:setSpeed(80, 280)
sparks:setSpread(math.pi * 2)
sparks:setColors(0.1, 0.8, 1, 1, 0.1, 0.8, 1, 0)
fx:particle("sparks", sparks)
Particle configuration stays on the LOVE ParticleSystem. The adapter only stores systems, updates them, draws them, and translates recipe events into system calls.
feel.define("hit.sparks", {
{ kind = "emit", event = "particle.emit", payload = { name = "sparks", count = 24, x = 120, y = 80 } },
})
Supported particle events are particle.emit, particle.start, particle.stop, particle.reset, and particle.move.
feel.define("fire.start", {
{ kind = "emit", event = "particle.start", payload = { name = "fire", x = 420, y = 300 } },
})
feel.define("fire.move", {
{ kind = "emit", event = "particle.move", payload = { name = "fire", x = 520, y = 320 } },
})
feel.define("fire.stop", {
{ kind = "emit", event = "particle.stop", payload = { name = "fire" } },
})
Call fx:drawParticles() where the systems should render. If particles should move with the camera adapter, draw them between fx:push() and fx:pop().
Shaders¶
Register app-created LOVE Shaders with fx:shader(name, shader, opts) or fx:shaders(map).
local shader = love.graphics.newShader([[
extern number amount;
vec4 effect(vec4 color, Image tex, vec2 uv, vec2 screen)
{
vec4 pixel = Texel(tex, uv) * color;
pixel.r = min(1.0, pixel.r + amount);
return pixel;
}
]])
fx:shader("glow", shader, { uniforms = { amount = 0 } })
Shader code and uniform names stay app-owned. The adapter stores the shader, sends uniforms, and can tween numeric scalar uniforms through feel.update(dt).
feel.define("glow.pulse", {
{ kind = "emit", event = "shader.apply", payload = { name = "glow" } },
{ kind = "emit", event = "shader.send", payload = { name = "glow", uniform = "amount", value = 0.2 } },
{ kind = "emit", event = "shader.tween", payload = { name = "glow", uniform = "amount", value = 0.8, duration = 0.15 } },
{ kind = "wait", duration = 0.2 },
{ kind = "emit", event = "shader.tween", payload = { name = "glow", uniform = "amount", value = 0, duration = 0.3 } },
})
Supported shader events are shader.send, shader.tween, shader.apply, and shader.clear.
For scoped drawing, wrap only the draw calls that should use the shader:
Post Processing¶
Post-processing captures a draw callback into an adapter-owned canvas, applies built-in screen-space effects, and draws the final result.
Use post.set for immediate values, post.tween for timed parameter changes, and post.weight to blend between the original and processed scene.
feel.define("impact.bloom", {
{ kind = "emit", event = "post.set", payload = { effect = "bloom", values = { threshold = 0.55 } } },
{ kind = "emit", event = "post.tween", payload = { effect = "bloom", values = { intensity = 1 }, duration = 0.12, restart = true } },
{ kind = "wait", duration = 0.16 },
{ kind = "emit", event = "post.tween", payload = { effect = "bloom", values = { intensity = 0 }, duration = 0.35, restart = true } },
})
Supported post events are post.set, post.tween, post.enable, post.disable, post.weight, and post.clear. Built-in effects are bloom, chromatic, grade, lens, and vignette; see Post Processing for parameters and recipes.
post.tween and post.weight stack by default. Add restart = true when the new tween should replace the previous tween for the same effect parameter.
Only drawing inside fx:drawPost(...) is processed. Draw HUD or adapter overlays afterward when they should stay readable.
Camera And Screen¶
Camera events mutate adapter-owned draw state. Wrap world drawing with fx:push() and fx:pop().
Supported camera events are camera.shake, camera.zoom, camera.move, and camera.reset.
Screen events draw overlays through fx:drawOverlay(). Supported screen events are screen.flash, screen.fade, and screen.clear.
Call both feel.update(dt) and fx:update(dt) from love.update(dt). Adapter update advances camera/screen state and registered particle systems.