├── 1.png ├── 2.png ├── LICENSE ├── README.md ├── icon.png ├── main.lua └── trail.lua /1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ulydev/trail/292365d62ba074a4b54fb34bb96c18e6e6550bca/1.png -------------------------------------------------------------------------------- /2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ulydev/trail/292365d62ba074a4b54fb34bb96c18e6e6550bca/2.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ulysse Ramage 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | trail 2 | ============== 3 | 4 | trail is a LÖVE library that... generates trails, you guessed it! 5 | 6 | ![demo][demo] 7 | 8 | Setup 9 | ---------------- 10 | 11 | ```lua 12 | local trail = require "trail" --require the library 13 | ``` 14 | 15 | Usage 16 | ---------------- 17 | 18 | Create new trail 19 | ```lua 20 | function love.load() 21 | myTrail = trail:new({ 22 | type = "mesh", 23 | content = { 24 | source = love.graphics.newImage("image.png"), 25 | width = 26, 26 | mode = "stretch" 27 | }, 28 | duration = .4 29 | }) 30 | myTrail:setMotion(600, 0) 31 | myTrail:setPosition(x, y) 32 | end 33 | ``` 34 | 35 | Update library 36 | ```lua 37 | function love.update(dt) 38 | trail:update(dt) 39 | end 40 | ``` 41 | 42 | Draw trail 43 | ```lua 44 | function love.draw() 45 | myTrail:draw() 46 | end 47 | ``` 48 | 49 | Trail settings 50 | ---------------- 51 | 52 | ```lua 53 | settings = { 54 | type, 55 | content = {}, --depends on trail type 56 | duration, 57 | amount, 58 | fade 59 | } 60 | ``` 61 | 62 | 63 | 64 | Trail types 65 | ---------------- 66 | 67 | Point 68 | ```lua 69 | settings = { 70 | type = "point", 71 | content = { 72 | -- 73 | type = "image", 74 | source, 75 | -- 76 | type = "rectangle", 77 | width, 78 | height, 79 | -- 80 | type = "circle", 81 | radius 82 | -- 83 | }, 84 | fade --"shrink" or "grow" 85 | } 86 | ``` 87 | 88 | Mesh 89 | ```lua 90 | settings = { 91 | type = "mesh", 92 | content = { 93 | source, --image 94 | width, 95 | mode 96 | } 97 | } 98 | ``` 99 | 100 | Methods 101 | ---------------- 102 | 103 | Methods can be chained. 104 | ```lua 105 | myTrail:setPosition(...):setMotion(...) 106 | ``` 107 | 108 | Update trails 109 | ```lua 110 | trail:update(dt) 111 | --or update each trail separately 112 | myTrail:update(dt) 113 | ``` 114 | 115 | Draw trail 116 | ```lua 117 | --affected by love.graphics.setColor 118 | myTrail:draw() 119 | ``` 120 | 121 | Create trail 122 | ```lua 123 | myTrail = trail:new(settings) 124 | ``` 125 | 126 | Set trail position 127 | ```lua 128 | myTrail:setPosition(x, y) 129 | ``` 130 | 131 | Set trail motion 132 | ```lua 133 | myTrail:setMotion(x, y, vx, vy) 134 | ``` 135 | 136 | Set trail width (mesh) 137 | ```lua 138 | myTrail:setWidth(width) 139 | ``` 140 | 141 | Enable/disable trail 142 | ```lua 143 | myTrail:enable() 144 | myTrail:disable() 145 | 146 | myTrail:toggle() 147 | ``` 148 | 149 | ---------- 150 | 151 | Thanks to Spyro and Taehl for their work on [trail meshes](https://love2d.org/forums/viewtopic.php?f=5&t=77608). 152 | 153 | [demo]: http://img4.hostingpics.net/pics/43495675et.gif 154 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ulydev/trail/292365d62ba074a4b54fb34bb96c18e6e6550bca/icon.png -------------------------------------------------------------------------------- /main.lua: -------------------------------------------------------------------------------- 1 | io.stdout:setvbuf'no' 2 | 3 | trail = require "trail" --require the library 4 | 5 | local function lerp(a, b, k) --smooth transitions 6 | if a == b then 7 | return a 8 | else 9 | if math.abs(a-b) < 0.002 then return b else return a * (1-k) + b * k end 10 | end 11 | end 12 | 13 | function love.load() 14 | 15 | love.window.setTitle("Click to enable trail") 16 | 17 | image = love.graphics.newImage("icon.png") 18 | image1 = love.graphics.newImage("1.png") 19 | image2 = love.graphics.newImage("2.png") 20 | 21 | -- 22 | 23 | x, y = 200, 200 24 | dx, dy = 0, 0 25 | 26 | test = {} 27 | 28 | test[1] = trail 29 | :new({ 30 | type = "mesh", 31 | content = { 32 | source = image2, 33 | width = 26, 34 | mode = "stretch" 35 | }, 36 | duration = .4 37 | }) 38 | :setMotion(600, 0, 0, 0) 39 | :setPosition(x, y) 40 | 41 | test[2] = trail 42 | :new({ 43 | type = "mesh", 44 | content = { 45 | source = image1, 46 | width = 6, 47 | mode = "stretch" 48 | }, 49 | duration = 1.5 50 | }) 51 | :setPosition(x, y) 52 | 53 | test[3] = trail 54 | :new({ 55 | type = "mesh", 56 | content = { 57 | source = image1, 58 | width = 6, 59 | mode = "stretch" 60 | }, 61 | duration = 1.5 62 | }) 63 | :setPosition(x, y) 64 | 65 | test[4] = trail 66 | :new({ 67 | type = "point", 68 | content = { 69 | type = "image", 70 | source = image 71 | }, 72 | duration = .4, 73 | amount = 32, 74 | fade = "shrink" 75 | }) 76 | :setMotion(200, 60) 77 | :setPosition(x, y) 78 | 79 | test[5] = trail 80 | :new({ 81 | type = "point", 82 | content = { 83 | type = "image", 84 | source = image 85 | }, 86 | duration = .4, 87 | amount = 32, 88 | fade = "shrink" 89 | }) 90 | :setMotion(200, -60) 91 | :setPosition(x, y) 92 | 93 | end 94 | 95 | function love.update(dt) 96 | 97 | dx, dy = x, y 98 | x, y = lerp(x, love.mouse.getX(), 4*dt), lerp(y, love.mouse.getY(), 4*dt) 99 | dx, dy = x - dx, y - dy 100 | 101 | local time = love.timer.getTime() 102 | 103 | test[1] 104 | :setMotion(600, 0, 0, math.sin(time * math.pi * 4) * 100) 105 | :setPosition(x, y) 106 | 107 | test[2] 108 | :setPosition(x + math.cos(time * 3.2) * 160 * math.cos(time * .2), y + math.sin(time * 3.2) * 160 * math.sin(time * .2)) 109 | 110 | test[3] 111 | :setPosition(x - math.cos(time * 3.2) * 160 * math.cos(time * .2), y - math.sin(time * 3.2) * 160 * math.sin(time * .2)) 112 | 113 | test[4]:setPosition(x, y) 114 | test[5]:setPosition(x, y) 115 | 116 | trail:update(dt) 117 | 118 | end 119 | 120 | function love.draw() 121 | 122 | love.graphics.setColor(255, 255, 255, 100) 123 | test[4]:draw() 124 | test[5]:draw() 125 | 126 | love.graphics.setColor(255, 255, 255) 127 | test[1]:draw() 128 | test[2]:draw() 129 | test[3]:draw() 130 | 131 | local time = love.timer.getTime() 132 | 133 | local rotation = math.cos(time * math.pi * 2) * .2 134 | local size = math.cos(time * math.pi * 4) * .02 + 1.02 135 | love.graphics.draw(image, x, y, rotation, size, size, 32, 32) 136 | 137 | end 138 | 139 | function love.mousepressed(x, y, button) 140 | 141 | test[4]:toggle() 142 | test[5]:toggle() 143 | 144 | end -------------------------------------------------------------------------------- /trail.lua: -------------------------------------------------------------------------------- 1 | -- trail.lua v0.1 2 | -- inspired by Spyro and Taehl's work ( https://love2d.org/forums/ucp.php?i=pm&mode=compose&u=134819 ) 3 | 4 | -- Copyright (c) 2016 Ulysse Ramage 5 | -- 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: 6 | -- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | -- 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. 8 | 9 | local trail, trailObject = { 10 | var = {} 11 | }, {} 12 | 13 | local default = { 14 | --point 15 | duration = 1, 16 | amount = 100, 17 | rotation = 0, 18 | fill = "fill", 19 | 20 | --mesh 21 | mode = "stretch", 22 | width = 5 23 | } 24 | 25 | setmetatable(trail, trail) 26 | 27 | --[[ Public ]]-- 28 | 29 | function trail:new(f) 30 | 31 | local _object = f 32 | 33 | assert(_object.content, "No content set.") 34 | 35 | _object.points = {} 36 | _object.duration = f.duration or default.duration 37 | _object.amount = f.amount or default.amount * (_object.duration / default.duration) 38 | _object.fade = f.fade 39 | _object.delay = _object.duration / _object.amount 40 | _object.time = _object.delay 41 | 42 | if f.type == "point" then 43 | 44 | _object.content.rotation = default.rotation 45 | 46 | elseif f.type == "mesh" then 47 | 48 | _object.content.mode = _object.content.mode or default.mode 49 | _object.width = f.content.width or default.width 50 | _object.content.mesh = love.graphics.newMesh(_object.duration / _object.delay / _object.delay, "strip") 51 | _object.content.mesh:setTexture(_object.content.source) 52 | 53 | end 54 | 55 | _object.active = true 56 | 57 | setmetatable(_object, { __index = trailObject }) 58 | 59 | _object:reset() 60 | _object:setMotion() 61 | 62 | self.var[_object] = _object 63 | 64 | return _object 65 | 66 | end 67 | 68 | function trailObject:reset() 69 | 70 | if self.type == "mesh" then 71 | if self.content.mode == "repeat" then 72 | self.content.length = 0 73 | end 74 | self.vertices = {} 75 | end 76 | 77 | end 78 | 79 | --{ x, y, duration, width, pmotion=pm} 80 | --{ x, y, alpha, rotation, pmotion=pm} 81 | 82 | function trailObject:add(x, y) 83 | 84 | if self.type == "point" then 85 | 86 | table.insert(self.points, 1, { x, y, 1, self.rotation, motion = self:copyMotion() }) 87 | 88 | elseif self.type == "mesh" then 89 | 90 | if self.content.mode == "repeat" then 91 | self.content.length = self.points[1][5] 92 | end 93 | 94 | table.insert(self.points, 1, { self.x, self.y, 1 }) 95 | 96 | end 97 | 98 | end 99 | 100 | -- 101 | 102 | function trail:update(dt) 103 | 104 | for k, v in pairs(self.var) do 105 | v:update(dt) 106 | end 107 | 108 | end 109 | 110 | function trailObject:update(dt) 111 | 112 | assert(self.x and self.y, "No position set.") 113 | 114 | if self.active then 115 | self.time = self.time - dt 116 | if self.time < 0 then 117 | if self.type == "point" then print("added stuff") end 118 | self:add(self.x, self.y) 119 | self.time = self.time % self.delay 120 | end 121 | end 122 | 123 | if self.type == "mesh" then 124 | self.points[1] = { 125 | self.x, 126 | self.y, 127 | 1, 128 | self.width, 129 | motion = self:copyMotion() 130 | } 131 | end 132 | 133 | for i = #self.points, 1, -1 do 134 | local point = self.points[i] 135 | 136 | point[3] = math.max(point[3] - dt / self.duration, 0) 137 | 138 | -- hack: point motion! 139 | local _motion = point.motion 140 | if _motion then 141 | point[1] = point[1] + _motion.x * dt 142 | point[2] = point[2] + _motion.y * dt 143 | _motion.x = _motion.x + _motion.vx * dt 144 | _motion.y = _motion.y + _motion.vy * dt 145 | end 146 | 147 | if self.type == "mesh" then 148 | 149 | self:updateVertex(i) 150 | 151 | end 152 | 153 | if point[3] <= 0 then 154 | table.remove(self.points, #self.points) 155 | if self.type == "mesh" then 156 | table.remove(self.vertices, #self.vertices) 157 | table.remove(self.vertices, #self.vertices) 158 | end 159 | end 160 | 161 | end 162 | 163 | if self.type == "mesh" then 164 | 165 | if #self.vertices > 3 then 166 | 167 | self.content.mesh:setVertices(self.vertices) 168 | 169 | end 170 | 171 | end 172 | 173 | return self 174 | 175 | end 176 | 177 | function trailObject:updateVertex(index) 178 | 179 | local pv = self.points[index+1] 180 | local v = self.points[index] 181 | local nv = self.points[index-1] 182 | 183 | local vertices = self.vertices 184 | 185 | if self.content.mode == "stretch" then 186 | 187 | if index == 1 then 188 | 189 | local dist = ((v[1]-pv[1])^2+(v[2]-pv[2])^2)^.5 190 | local vert = {(v[2]-pv[2])*v[4]/(dist*2), (v[1]-pv[1])*v[4]/(dist*2)} 191 | 192 | vertices[1] = { 193 | v[1]+vert[1], v[2]-vert[2], v[3], 0 194 | } 195 | vertices[2] = { 196 | v[1]-vert[1], v[2]+vert[2], v[3], 1 197 | } 198 | 199 | elseif index == #self.points then 200 | 201 | local dist = math.sqrt((nv[1]-v[1])^2+(nv[2]-v[2])^2) 202 | local vert = {(nv[2]-v[2])*v[4]/(dist*2), -(nv[1]-v[1])*v[4]/(dist*2)} 203 | 204 | vertices[#self.points*2-1] = { 205 | v[1]+vert[1], v[2]-vert[2], v[3], 0 206 | } 207 | vertices[#self.points*2] = { 208 | v[1]-vert[1], v[2]+vert[2], v[3], 1 209 | } 210 | 211 | else 212 | 213 | local dist = math.sqrt((nv[1]-pv[1])^2+(nv[2]-pv[2])^2) 214 | local vert = {(nv[2]-pv[2])*v[4]/(dist*2), (nv[1]-pv[1])*v[4]/(dist*2)} 215 | 216 | vertices[index*2-1] = { 217 | v[1]+vert[1], v[2]-vert[2], v[3], 0 218 | } 219 | vertices[index*2] = { 220 | v[1]-vert[1], v[2]+vert[2], v[3], 1 221 | } 222 | 223 | end 224 | 225 | elseif self.content.mode == "repeat" then 226 | 227 | if index == 1 then 228 | local dist = math.sqrt((v[1]-pv[1])^2+(v[2]-pv[2])^2) 229 | local vert = {(v[2]-pv[2])*v[4]/(dist*2), (v[1]-pv[1])*v[4]/(dist*2)} 230 | 231 | v[5] = dist / (self.content.source:getWidth()/(self.content.source:getHeight()/self.width)) + self.content.length 232 | 233 | vertices[1] = { 234 | v[1]+vert[1], v[2]-vert[2], v[5], 0, 255, 255, 255, 255*v[3] 235 | } 236 | vertices[2] = { 237 | v[1]-vert[1], v[2]+vert[2], v[5], 1, 255, 255, 255, 255*v[3] 238 | } 239 | 240 | elseif index == #self.points then 241 | 242 | local dist = math.sqrt((nv[1]-v[1])^2+(nv[2]-v[2])^2) 243 | local vert = {(nv[2]-v[2])*v[4]/(dist*2), -(nv[1]-v[1])*v[4]/(dist*2)} 244 | 245 | vertices[#self.points*2-1] = { 246 | v[1]+vert[1], v[2]-vert[2], v[5], 0, 255, 255, 255, 1 247 | } 248 | vertices[#self.points*2] = { 249 | v[1]-vert[1], v[2]+vert[2], v[5], 1, 255, 255, 255, 1 250 | } 251 | 252 | else 253 | 254 | local dist = math.sqrt((nv[1]-pv[1])^2+(nv[2]-pv[2])^2) 255 | local vert = {(nv[2]-pv[2])*v[4]/(dist*2), (nv[1]-pv[1])*v[4]/(dist*2)} 256 | 257 | vertices[index*2-1] = { 258 | v[1]+vert[1], v[2]-vert[2], v[5], 0, 255, 255, 255, 255*(v[3] + self.delay) 259 | } 260 | vertices[index*2] = { 261 | v[1]-vert[1], v[2]+vert[2], v[5], 1, 255, 255, 255, 255*(v[3] + self.delay) 262 | } 263 | 264 | end 265 | 266 | end 267 | 268 | end 269 | 270 | -- 271 | 272 | function trailObject:setPosition(x, y) 273 | 274 | local _init = not (self.x and self.y) 275 | 276 | self.x, self.y = x, y 277 | 278 | if _init then 279 | self.points = { 280 | { x, y, 1, self.width, 0 }, 281 | { x, y, 1 - self.delay, self.width, 0 } 282 | } 283 | end 284 | 285 | return self 286 | 287 | end 288 | 289 | function trailObject:setDuration(duration) 290 | 291 | self.duration = duration 292 | return self 293 | 294 | end 295 | 296 | function trailObject:setMotion(x, y, vx, vy) 297 | 298 | self.motion = { 299 | x = x or 0, 300 | y = y or 0, 301 | vx = vx or 0, 302 | vy = vy or 0 303 | } 304 | return self 305 | 306 | end 307 | 308 | function trailObject:copyMotion() 309 | 310 | return { 311 | x = self.motion.x, 312 | y = self.motion.y, 313 | vx = self.motion.vx, 314 | vy = self.motion.vy 315 | } 316 | 317 | end 318 | 319 | function trailObject:setRotation(rotation) 320 | 321 | self.rotation = rotation 322 | return self 323 | 324 | end 325 | 326 | -- 327 | 328 | function trailObject:draw() 329 | 330 | local col = {} 331 | col[1], col[2], col[3], col[4] = love.graphics.getColor() 332 | 333 | if self.type == "point" then 334 | 335 | for i = #self.points, 1, -1 do 336 | 337 | local point = self.points[i] 338 | 339 | local size = self.fade and (self.fade == "grow" and (2-point[3]) or point[3]) or 1 340 | love.graphics.setColor(col[1], col[2], col[3], col[4] * point[3]) 341 | if self.content.type == "image" then 342 | local width, height = self.content.source:getWidth() * size, self.content.source:getHeight() * size 343 | love.graphics.draw(self.content.source, point[1], point[2], point[4], size, size, (width/size)*.5, (height/size)*.5) 344 | elseif self.content.type == "rectangle" then 345 | local width, height = self.content.width * size, self.content.height * size 346 | love.graphics.rectangle(self.content.fill or default.fill, point[1]-width*.5, point[2]-height*.5, width, height) 347 | elseif self.content.type == "circle" then 348 | local radius = self.content.radius * size 349 | love.graphics.circle(self.content.fill or default.fill, point[1], point[2], radius) 350 | end 351 | 352 | end 353 | 354 | elseif self.type == "mesh" then 355 | 356 | self.content.mesh:setDrawRange() 357 | love.graphics.draw(self.content.mesh) 358 | 359 | end 360 | 361 | love.graphics.setColor(unpack(col)) 362 | 363 | end 364 | 365 | function trailObject:enable() 366 | if self.type == "mesh" then return self end 367 | 368 | self.active = true 369 | if self.type == "point" then self.time = self.delay end 370 | return self 371 | 372 | end 373 | 374 | function trailObject:disable() 375 | if self.type == "mesh" then return self end 376 | 377 | self.active = false 378 | return self 379 | 380 | end 381 | 382 | function trailObject:setWidth(width) 383 | 384 | self.width = width 385 | return self 386 | 387 | end 388 | 389 | --[[ Aliases ]]-- 390 | 391 | function trailObject:toggle() return self.active and self:disable() or self:enable() end 392 | 393 | --[[ End ]]-- 394 | 395 | return trail --------------------------------------------------------------------------------