123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370 |
- --[[
- MIT License
- Copyright (c) 2017 SSYGEN
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- ]]--
- local function lerp(a, b, x) return a + (b - a)*x end
- local function csnap(v, x) return math.ceil(v/x)*x - x/2 end
- -- Shake according to https://jonny.morrill.me/en/blog/gamedev-how-to-implement-a-camera-shake-effect/
- local function newShake(amplitude, duration, frequency)
- local self = {
- amplitude = amplitude or 0,
- duration = duration or 0,
- frequency = frequency or 60,
- samples = {},
- start_time = love.timer.getTime()*1000,
- t = 0,
- shaking = true,
- }
- local sample_count = (self.duration/1000)*self.frequency
- for i = 1, sample_count do self.samples[i] = 2*love.math.random()-1 end
- return self
- end
- local function updateShake(self, dt)
- self.t = love.timer.getTime()*1000 - self.start_time
- if self.t > self.duration then self.shaking = false end
- end
- local function shakeNoise(self, s)
- if s >= #self.samples then return 0 end
- return self.samples[s] or 0
- end
- local function shakeDecay(self, t)
- if t > self.duration then return 0 end
- return (self.duration - t)/self.duration
- end
- local function getShakeAmplitude(self, t)
- if not t then
- if not self.shaking then return 0 end
- t = self.t
- end
- local s = (t/1000)*self.frequency
- local s0 = math.floor(s)
- local s1 = s0 + 1
- local k = shakeDecay(self, t)
- return self.amplitude*(shakeNoise(self, s0) + (s - s0)*(shakeNoise(self, s1) - shakeNoise(self, s0)))*k
- end
- -- Camera
- local Camera = {}
- Camera.__index = Camera
- local function new(x, y, w, h, scale, rotation)
- return setmetatable({
- x = x or (w or love.graphics.getWidth())/2, y = y or (h or love.graphics.getHeight())/2,
- mx = x or (w or love.graphics.getWidth())/2, my = y or (h or love.graphics.getHeight())/2,
- screen_x = x or (w or love.graphics.getWidth())/2, screen_y = y or (h or love.graphics.getHeight())/2,
- w = w or love.graphics.getWidth(), h = h or love.graphics.getHeight(),
- scale = scale or 1,
- rotation = rotation or 0,
- horizontal_shakes = {}, vertical_shakes = {},
- target_x = nil, target_y = nil,
- scroll_x = 0, scroll_y = 0,
- last_target_x = nil, last_target_y = nil,
- follow_lerp_x = 1, follow_lerp_y = 1,
- follow_lead_x = 0, follow_lead_y = 0,
- deadzone = nil, bound = nil,
- draw_deadzone = false,
- flash_duration = 1, flash_timer = 0, flash_color = {0, 0, 0, 1},
- last_horizontal_shake_amount = 0, last_vertical_shake_amount = 0,
- fade_duration = 1, fade_timer = 0, fade_color = {0, 0, 0, 0},
- }, Camera)
- end
- function Camera:attach()
- love.graphics.push()
- love.graphics.translate(self.w/2, self.h/2)
- love.graphics.scale(self.scale)
- love.graphics.rotate(self.rotation)
- love.graphics.translate(-self.x, -self.y)
- end
- function Camera:detach()
- love.graphics.pop()
- end
- function Camera:move(dx, dy)
- self.x, self.y = self.x + dx, self.y + dy
- end
- function Camera:toWorldCoords(x, y)
- local c, s = math.cos(self.rotation), math.sin(self.rotation)
- x, y = (x - self.w/2)/self.scale, (y - self.h/2)/self.scale
- x, y = c*x - s*y, s*x + c*y
- return x + self.x, y + self.y
- end
- function Camera:toCameraCoords(x, y)
- local c, s = math.cos(self.rotation), math.sin(self.rotation)
- x, y = x - self.x, y - self.y
- x, y = c*x - s*y, s*x + c*y
- return x*self.scale + self.w/2, y*self.scale + self.h/2
- end
- function Camera:getMousePosition()
- return self:toWorldCoords(love.mouse.getPosition())
- end
- function Camera:shake(intensity, duration, frequency, axes)
- if not axes then axes = 'XY' end
- axes = string.upper(axes)
- if string.find(axes, 'X') then table.insert(self.horizontal_shakes, newShake(intensity, duration*1000, frequency)) end
- if string.find(axes, 'Y') then table.insert(self.vertical_shakes, newShake(intensity, duration*1000, frequency)) end
- end
- function Camera:update(dt)
- self.mx, self.my = self:toWorldCoords(love.mouse.getPosition())
- -- Flash --
- if self.flashing then
- self.flash_timer = self.flash_timer + dt
- if self.flash_timer > self.flash_duration then
- self.flash_timer = 0
- self.flashing = false
- end
- end
- -- Fade --
- if self.fading then
- self.fade_timer = self.fade_timer + dt
- self.fade_color = {
- lerp(self.base_fade_color[1], self.target_fade_color[1], self.fade_timer/self.fade_duration),
- lerp(self.base_fade_color[2], self.target_fade_color[2], self.fade_timer/self.fade_duration),
- lerp(self.base_fade_color[3], self.target_fade_color[3], self.fade_timer/self.fade_duration),
- lerp(self.base_fade_color[4], self.target_fade_color[4], self.fade_timer/self.fade_duration),
- }
- if self.fade_timer > self.fade_duration then
- self.fade_timer = 0
- self.fading = false
- if self.fade_action then self.fade_action() end
- end
- end
- -- Shake --
- local horizontal_shake_amount, vertical_shake_amount = 0, 0
- for i = #self.horizontal_shakes, 1, -1 do
- updateShake(self.horizontal_shakes[i], dt)
- horizontal_shake_amount = horizontal_shake_amount + getShakeAmplitude(self.horizontal_shakes[i])
- if not self.horizontal_shakes[i].shaking then table.remove(self.horizontal_shakes, i) end
- end
- for i = #self.vertical_shakes, 1, -1 do
- updateShake(self.vertical_shakes[i], dt)
- vertical_shake_amount = vertical_shake_amount + getShakeAmplitude(self.vertical_shakes[i])
- if not self.vertical_shakes[i].shaking then table.remove(self.vertical_shakes, i) end
- end
- self.x, self.y = self.x - self.last_horizontal_shake_amount, self.y - self.last_vertical_shake_amount
- self:move(horizontal_shake_amount, vertical_shake_amount)
- self.last_horizontal_shake_amount, self.last_vertical_shake_amount = horizontal_shake_amount, vertical_shake_amount
- -- Follow --
- if not self.target_x and not self.target_y then return end
- -- Set follow style deadzones
- if self.follow_style == 'LOCKON' then
- local w, h = self.w/16, self.w/16
- self:setDeadzone((self.w - w)/2, (self.h - h)/2, w, h)
- elseif self.follow_style == 'PLATFORMER' then
- local w, h = self.w/8, self.h/3
- self:setDeadzone((self.w - w)/2, (self.h - h)/2 - h*0.25, w, h)
- elseif self.follow_style == 'TOPDOWN' then
- local s = math.max(self.w, self.h)/4
- self:setDeadzone((self.w - s)/2, (self.h - s)/2, s, s)
- elseif self.follow_style == 'TOPDOWN_TIGHT' then
- local s = math.max(self.w, self.h)/8
- self:setDeadzone((self.w - s)/2, (self.h - s)/2, s, s)
- elseif self.follow_style == 'SCREEN_BY_SCREEN' then
- self:setDeadzone(0, 0, 0, 0)
- elseif self.follow_style == 'NO_DEADZONE' then
- self.deadzone = nil
- end
- -- No deadzone means we just track the target with no lerp
- if not self.deadzone then
- self.x, self.y = self.target_x, self.target_y
- if self.bound then
- self.x = math.min(math.max(self.x, self.bounds_min_x + self.w/2), self.bounds_max_x - self.w/2)
- self.y = math.min(math.max(self.y, self.bounds_min_y + self.h/2), self.bounds_max_y - self.h/2)
- end
- return
- end
- -- Convert appropriate variables to camera coordinates since the deadzone is applied in terms of the camera and not the world
- local dx1, dy1, dx2, dy2 = self.deadzone_x, self.deadzone_y, self.deadzone_x + self.deadzone_w, self.deadzone_y + self.deadzone_h
- local scroll_x, scroll_y = 0, 0
- local target_x, target_y = self:toCameraCoords(self.target_x, self.target_y)
- local x, y = self:toCameraCoords(self.x, self.y)
- -- Screen by screen follow mode needs to be handled a bit differently
- if self.follow_style == 'SCREEN_BY_SCREEN' then
- -- Don't change self.screen_x/y if already at the boundaries
- if self.bound then
- if self.x > self.bounds_min_x + self.w/2 and target_x < 0 then self.screen_x = csnap(self.screen_x - self.w/self.scale, self.w/self.scale) end
- if self.x < self.bounds_max_x - self.w/2 and target_x >= self.w then self.screen_x = csnap(self.screen_x + self.w/self.scale, self.w/self.scale) end
- if self.y > self.bounds_min_y + self.h/2 and target_y < 0 then self.screen_y = csnap(self.screen_y - self.h/self.scale, self.h/self.scale) end
- if self.y < self.bounds_max_y - self.h/2 and target_y >= self.h then self.screen_y = csnap(self.screen_y + self.h/self.scale, self.h/self.scale) end
- -- Move to the next screen if the target is outside the screen boundaries
- else
- if target_x < 0 then self.screen_x = csnap(self.screen_x - self.w/self.scale, self.w/self.scale) end
- if target_x >= self.w then self.screen_x = csnap(self.screen_x + self.w/self.scale, self.w/self.scale) end
- if target_y < 0 then self.screen_y = csnap(self.screen_y - self.h/self.scale, self.h/self.scale) end
- if target_y >= self.h then self.screen_y = csnap(self.screen_y + self.h/self.scale, self.h/self.scale) end
- end
- self.x = lerp(self.x, self.screen_x, self.follow_lerp_x)
- self.y = lerp(self.y, self.screen_y, self.follow_lerp_y)
- -- Apply bounds
- if self.bound then
- self.x = math.min(math.max(self.x, self.bounds_min_x + self.w/2), self.bounds_max_x - self.w/2)
- self.y = math.min(math.max(self.y, self.bounds_min_y + self.h/2), self.bounds_max_y - self.h/2)
- end
- -- All other follow modes
- else
- -- Figure out how much the camera needs to scroll
- if target_x < x + (dx1 + dx2 - x) then
- local d = target_x - dx1
- if d < 0 then scroll_x = d end
- end
- if target_x > x - (dx1 + dx2 - x) then
- local d = target_x - dx2
- if d > 0 then scroll_x = d end
- end
- if target_y < y + (dy1 + dy2 - y) then
- local d = target_y - dy1
- if d < 0 then scroll_y = d end
- end
- if target_y > y - (dy1 + dy2 - y) then
- local d = target_y - dy2
- if d > 0 then scroll_y = d end
- end
- -- Apply lead
- if not self.last_target_x and not self.last_target_y then self.last_target_x, self.last_target_y = self.target_x, self.target_y end
- scroll_x = scroll_x + (self.target_x - self.last_target_x)*self.follow_lead_x
- scroll_y = scroll_y + (self.target_y - self.last_target_y)*self.follow_lead_y
- self.last_target_x, self.last_target_y = self.target_x, self.target_y
- -- Scroll towards target with lerp
- self.x = lerp(self.x, self.x + scroll_x, self.follow_lerp_x)
- self.y = lerp(self.y, self.y + scroll_y, self.follow_lerp_y)
- -- Apply bounds
- if self.bound then
- self.x = math.min(math.max(self.x, self.bounds_min_x + self.w/2), self.bounds_max_x - self.w/2)
- self.y = math.min(math.max(self.y, self.bounds_min_y + self.h/2), self.bounds_max_y - self.h/2)
- end
- end
- end
- function Camera:draw()
- if self.draw_deadzone and self.deadzone then
- local n = love.graphics.getLineWidth()
- love.graphics.setLineWidth(2)
- love.graphics.line(self.deadzone_x - 1, self.deadzone_y, self.deadzone_x + 6, self.deadzone_y)
- love.graphics.line(self.deadzone_x, self.deadzone_y, self.deadzone_x, self.deadzone_y + 6)
- love.graphics.line(self.deadzone_x - 1, self.deadzone_y + self.deadzone_h, self.deadzone_x + 6, self.deadzone_y + self.deadzone_h)
- love.graphics.line(self.deadzone_x, self.deadzone_y + self.deadzone_h, self.deadzone_x, self.deadzone_y + self.deadzone_h - 6)
- love.graphics.line(self.deadzone_x + self.deadzone_w + 1, self.deadzone_y + self.deadzone_h, self.deadzone_x + self.deadzone_w - 6, self.deadzone_y + self.deadzone_h)
- love.graphics.line(self.deadzone_x + self.deadzone_w, self.deadzone_y + self.deadzone_h, self.deadzone_x + self.deadzone_w, self.deadzone_y + self.deadzone_h - 6)
- love.graphics.line(self.deadzone_x + self.deadzone_w + 1, self.deadzone_y, self.deadzone_x + self.deadzone_w - 6, self.deadzone_y)
- love.graphics.line(self.deadzone_x + self.deadzone_w, self.deadzone_y, self.deadzone_x + self.deadzone_w, self.deadzone_y + 6)
- love.graphics.setLineWidth(n)
- end
- if self.flashing then
- local r, g, b, a = love.graphics.getColor()
- love.graphics.setColor(self.flash_color)
- love.graphics.rectangle('fill', 0, 0, self.w, self.h)
- love.graphics.setColor(r, g, b, a)
- end
- local r, g, b, a = love.graphics.getColor()
- love.graphics.setColor(self.fade_color)
- love.graphics.rectangle('fill', 0, 0, self.w, self.h)
- love.graphics.setColor(r, g, b, a)
- end
- function Camera:follow(x, y)
- self.target_x, self.target_y = x, y
- end
- function Camera:setDeadzone(x, y, w, h)
- self.deadzone = true
- self.deadzone_x = x
- self.deadzone_y = y
- self.deadzone_w = w
- self.deadzone_h = h
- end
- function Camera:setBounds(x, y, w, h)
- self.bound = true
- self.bounds_min_x = x
- self.bounds_min_y = y
- self.bounds_max_x = x + w
- self.bounds_max_y = y + h
- end
- function Camera:setFollowStyle(follow_style)
- self.follow_style = follow_style
- end
- function Camera:setFollowLerp(x, y)
- self.follow_lerp_x = x
- self.follow_lerp_y = y or x
- end
- function Camera:setFollowLead(x, y)
- self.follow_lead_x = x
- self.follow_lead_y = y or x
- end
- function Camera:flash(duration, color)
- self.flash_duration = duration
- self.flash_color = color or self.flash_color
- self.flash_timer = 0
- self.flashing = true
- end
- function Camera:fade(duration, color, action)
- self.fade_duration = duration
- self.base_fade_color = self.fade_color
- self.target_fade_color = color
- self.fade_timer = 0
- self.fade_action = action
- self.fading = true
- end
- return setmetatable({new = new}, {__call = function(_, ...) return new(...) end})
|