/lua/animation.lua

https://bitbucket.org/bvanevery/tolandsunknown · Lua · 444 lines · 311 code · 41 blank · 92 comment · 66 complexity · 750abe52f969d6e597400c72d3a3ed23 MD5 · raw file

  1. --[=[
  2. [animate_path]
  3. Author: Alarantalara (username on the Battle for Wesnoth forum)
  4. animate_path is a new tag that allows for movement of an object along paths not restricted by the hex grid
  5. Required keys:
  6. x,y: a sequence of points relative to the center of the reference hex in pixels through which the animation will travel
  7. hex_x, hex_y: a hex on the map for which all other coordinates will be relative to
  8. image: the image to display. It should have a 72 pixel transparent border surrounding it
  9. frame_length: the amount of time each frame will be visible
  10. Optional keys:
  11. frames (default: number of images specified in image): the number of frames to display in the movement, must be at least 2
  12. linger (default: no): if yes, then leave the final frame visible
  13. transpose (default: no): if yes, then the interpolation methods marked function will calculate based on y-values rather than x-values
  14. interpolation: (default: linear) The method used to travel between points. Allowed values are: linear, bspline, parabola
  15. Method Details:
  16. Methods marked (function) require that the x values (y values if transpose is yes) be distinct and sorted (increasing or decreasing)
  17. Currently this is not checked, provide points out of order at your own risk
  18. linear:
  19. bspline: requires that at least 4 points be specified
  20. parabola (function): requires exactly 3 points be specified
  21. cubic_spline (function):
  22. Example:
  23. [animate_path]
  24. x=0,100,1000
  25. y=0,0,1000
  26. hex_x=10
  27. hex_y=10
  28. image=the_image.png
  29. frames=20
  30. frame_length=20
  31. [/animate_path]
  32. Note for those who want more options:
  33. This file returns a table of interpolation methods
  34. You can add an initialization function to it to provide your own path function.
  35. The initialization function receives a list of x values, a list of y values and the total number of locations
  36. The number of x and y values are guaranteed to be the same
  37. Your initialization function must return 3 functions:
  38. The first function returns the length of each segment of the path, the number of segments and the total length of the path
  39. It takes no parameters
  40. <lengths>, num_lengths, total_length = length_function()
  41. <lengths> must be an array indexed from [1..n] where n is the number of lengths
  42. All lengths must be positive
  43. The second function is called for each point of the path specified by the user
  44. It takes one parameter specifying which segment in the path was reached (0..n where n is number of segments)
  45. There are no return values
  46. The third function is called containing the distance travelled along the current segment
  47. This value will be in the range [0..length[segment_number]]
  48. The function must return the absolute x,y coordinates of the associated point
  49. x, y = get_point_on_current_segment_from_offset( offset )
  50. ]=]
  51. local helper = wesnoth.require "lua/helper.lua"
  52. local items = wesnoth.require "lua/wml/items.lua"
  53. -- Linear Algebra
  54. local epsilon = 0.0000000001
  55. local function solve_system(A, b)
  56. -- solve a system of n equations in n unknowns
  57. -- A is a square matrix
  58. local size = #A
  59. for i = 1,size do
  60. -- find largest element as pivot
  61. local largest = i
  62. for j = i,size do
  63. if math.abs(A[largest][i]) < math.abs(A[j][i]) then
  64. largest = j
  65. end
  66. end
  67. -- swap if larger element found
  68. if math.abs(A[largest][i]) < epsilon then
  69. -- largest element remaining is 0, no unique solution
  70. return nil
  71. end
  72. if largest ~= i then
  73. A[i], A[largest] = A[largest], A[i]
  74. b[i], b[largest] = b[largest], b[i]
  75. end
  76. -- reduce
  77. for k = i+1,size do
  78. local m = A[k][i] / A[i][i]
  79. for j = i+1,size do
  80. A[k][j] = A[k][j] - m * A[i][j]
  81. end
  82. b[k] = b[k] - m * b[i]
  83. end
  84. end
  85. -- back substitute
  86. for i = size,1,-1 do
  87. for j = size,i+1,-1 do
  88. b[i] = b[i] - A[i][j] * b[j]
  89. end
  90. b[i] = b[i] / A[i][i]
  91. end
  92. return b
  93. end
  94. -- Image Placement Functions
  95. local function get_image_name_with_offset(hex_x, hex_y, x, y, image)
  96. -- since halo doesn't have a key to offset an image, use the CROP
  97. -- function built into the wesnoth image placement to fake it
  98. -- requires a 72 pixel border around the image to work properly
  99. x = x*2
  100. y = y*2
  101. local w, h = wesnoth.get_image_size(image)
  102. w = w-math.abs(x)
  103. if w <= 0 then
  104. return
  105. end
  106. h = h-math.abs(y)
  107. if h <= 0 then
  108. return
  109. end
  110. if x > 0 then
  111. x = 0
  112. else
  113. x = -x
  114. end
  115. if y > 0 then
  116. y = 0
  117. else
  118. y = -y
  119. end
  120. return string.format("%s~CROP(%d,%d,%d,%d)",image,x,y,w,h)
  121. end
  122. local function calc_image_hex_offset(hex_x, hex_y, x, y)
  123. -- given a reference hex and an offset in pixels
  124. -- find the hex closest to the target and adjust the offset to be relative to that hex
  125. -- returns the new hex coordinates followed by the new pixel offset
  126. local hex_off_x = math.floor((x + 27) / 54)
  127. local k = 0
  128. if math.abs(hex_off_x) % 2 == 1 then
  129. if math.abs(hex_x) % 2 == 0 then
  130. k = 36
  131. else
  132. y = y - 36
  133. end
  134. end
  135. local hex_off_y = math.floor((y + 36) / 72)
  136. local new_x = x - hex_off_x * 54
  137. local new_y = y - (hex_off_y * 72) + k
  138. if new_y > 36 then
  139. new_y = new_y - 72
  140. hex_off_y = hex_off_y+1
  141. end
  142. return hex_x+hex_off_x, hex_y+hex_off_y, new_x, new_y
  143. end
  144. -- Miscellaneous Utilities
  145. local function load_list(list)
  146. -- this loads a comma separated list into a 0-based array
  147. -- the 0 base simplifies later modular arithmetic
  148. local items = {}
  149. local num_items = 0
  150. for item in string.gmatch(list, "[^%s,][^,]*") do
  151. items[num_items] = item
  152. num_items = num_items + 1
  153. end
  154. return items, num_items
  155. end
  156. -- Interpolation Functions
  157. local interpolation_methods = {}
  158. function interpolation_methods.linear( x_locs, y_locs, num_locs )
  159. -- encapsulates the linear interpolation algorithm
  160. local function calc_linear_path_length()
  161. if num_locs == 1 then
  162. return {}, 0, 0
  163. end
  164. local total_length = 0
  165. local lengths = {}
  166. local last_x = x_locs[0]
  167. local last_y = y_locs[0]
  168. local cur_x, cur_y
  169. local num_lengths = 0
  170. for i = 1,num_locs-1 do
  171. cur_x = x_locs[i]
  172. cur_y = y_locs[i]
  173. lengths[i] = math.sqrt( (cur_x-last_x)^2 + (cur_y-last_y)^2 )
  174. total_length = total_length + lengths[i]
  175. last_x = cur_x
  176. last_y = cur_y
  177. num_lengths = num_lengths + 1
  178. end
  179. return lengths, num_lengths, total_length
  180. end
  181. local function reached_point(point)
  182. start_x = x_locs[point] or 0
  183. start_y = y_locs[point] or 0
  184. delta_x = (x_locs[point+1] or start_x) - start_x
  185. delta_y = (y_locs[point+1] or start_y) - start_y
  186. end
  187. local function get_location(offset)
  188. local x = (delta_x * offset) + start_x
  189. local y = (delta_y * offset) + start_y
  190. return x,y
  191. end
  192. local start_x, start_y
  193. local delta_x, delta_y
  194. return calc_linear_path_length, reached_point, get_location
  195. end
  196. function interpolation_methods.bspline( x_locs, y_locs, num_locs )
  197. -- implements uniform cubic B-splines
  198. local function calc_uniform_path_length()
  199. local lengths = {}
  200. for i = 1,num_locs-3 do
  201. lengths[i] = 1
  202. end
  203. return lengths, num_locs-3, num_locs-3
  204. end
  205. local function reached_point(point)
  206. index = point
  207. end
  208. local function get_location(offset)
  209. local u3 = offset*offset*offset
  210. local u2 = offset*offset
  211. local u = offset
  212. local b0 = (-1*u3 + 3*u2 - 3*u + 1)
  213. local b1 = ( 3*u3 - 6*u2 + 4)
  214. local b2 = (-3*u3 + 3*u2 + 3*u + 1)
  215. local b3 = u3
  216. local x = b0*x_locs[index] + b1*x_locs[index+1] + b2*x_locs[index+2]
  217. local y = b0*y_locs[index] + b1*y_locs[index+1] + b2*y_locs[index+2]
  218. if index < num_locs-3 then
  219. x = x + b3*x_locs[index+3]
  220. y = y + b3*y_locs[index+3]
  221. end
  222. return x/6, y/6
  223. end
  224. if num_locs < 4 then
  225. helper.wml_error("[animate_path]: A B-spline path requires at least 4 points be specified")
  226. end
  227. local index
  228. return calc_uniform_path_length, reached_point, get_location
  229. end
  230. function interpolation_methods.parabola( x_locs, y_locs, num_locs )
  231. -- implements simple parabolas
  232. -- assumes that the parabola opens up or down and that the points are specified in
  233. -- either increasing or decreasing order (second assumption allows determination of direction of travel)
  234. if num_locs ~= 3 then
  235. helper.wml_error("[animate_path]: A parabola requires that exactly 3 points be specified")
  236. end
  237. local A, b, index
  238. A = {{x_locs[0]*x_locs[0], x_locs[0], 1},
  239. {x_locs[1]*x_locs[1], x_locs[1], 1},
  240. {x_locs[2]*x_locs[2], x_locs[2], 1}}
  241. b = {y_locs[0], y_locs[1], y_locs[2]} -- have to copy since input is 0-based
  242. b = solve_system(A, b)
  243. A = nil
  244. if b == nil then
  245. helper.wml_error("[animate_path]: The provided points do not form a parabola")
  246. end
  247. local function get_parabola_path_length()
  248. return {1},1,1
  249. end
  250. local function reached_point(point)
  251. index = point
  252. end
  253. local function get_location(offset)
  254. local x
  255. if index == 1 then
  256. x = x_locs[2]
  257. else
  258. x = offset*(x_locs[2] - x_locs[0]) + x_locs[0]
  259. end
  260. local y = b[1]*x*x + b[2]*x + b[3]
  261. return x, y
  262. end
  263. return get_parabola_path_length, reached_point, get_location
  264. end
  265. function interpolation_methods.cubic_spline( x_locs, y_locs, num_locs )
  266. -- implements natural cubic spline interpolation
  267. if num_locs <= 2 then
  268. return interpolation_methods.linear( x_locs, y_locs, num_locs )
  269. end
  270. local M = {}
  271. local mt = {__index = function () return 0 end}
  272. local a = {}
  273. local b = {}
  274. local c = {}
  275. local h = {}
  276. for i = 1,num_locs-1 do
  277. h[i] = x_locs[i] - x_locs[i-1]
  278. end
  279. for i = 1,num_locs-2 do
  280. M[i] = {}
  281. setmetatable(M[i], mt)
  282. M[i][i-1] = h[i] / 6
  283. M[i][i] = (h[i] + h[i+1]) / 3
  284. M[i][i+1] = h[i+1] / 6
  285. a[i] = (y_locs[i+1] - y_locs[i]) / h[i+1] - (y_locs[i] - y_locs[i-1]) / h[i]
  286. end
  287. -- TODO: write tridiagonal solver using the Thomas method to improve runtime
  288. -- O(n) instead of O(n^2)
  289. -- for now, use metatables to fill in all the 0s the Gaussian solver needs
  290. a = solve_system(M, a)
  291. M = nil
  292. a[0] = 0
  293. a[num_locs-1] = 0
  294. for i = 1,num_locs-1 do
  295. b[i] = y_locs[i-1] / h[i] - (a[i-1] * h[i]) / 6
  296. c[i] = y_locs[i] / h[i] - (a[i] * h[i]) / 6
  297. end
  298. local index, delta_x
  299. local function get_cubic_path_length()
  300. -- since I don't want to calculate the arc length at this time
  301. -- I currently just return the absolute value of the
  302. -- x differences to provide a constant x-velocity
  303. local total_length = 0
  304. local lengths = {}
  305. local num_lengths = 0
  306. for i = 1,num_locs-1 do
  307. lengths[i] = math.abs(x_locs[i]-x_locs[i-1])
  308. total_length = total_length + lengths[i]
  309. num_lengths = num_lengths + 1
  310. end
  311. return lengths, num_lengths, total_length
  312. end
  313. local function reached_point(point)
  314. index = point+1
  315. delta_x = x_locs[point+1] - x_locs[point] or 0
  316. end
  317. local function get_location(offset)
  318. local x = (delta_x * offset) + x_locs[index-1]
  319. local y = a[index-1] * (x_locs[index] - x)^3 / (6 * h[index]) +
  320. a[index] * (x - x_locs[index-1])^3 / (6 * h[index]) +
  321. b[index] * (x_locs[index] - x ) +
  322. c[index] * (x - x_locs[index-1])
  323. return x, y
  324. end
  325. return get_cubic_path_length, reached_point, get_location
  326. end
  327. function wesnoth.wml_actions.animate_path(cfg)
  328. if wesnoth.get_image_size == nil then
  329. wesnoth.message("Animation skipped. To see the animation, upgrade to Battle for Wesnoth version 1.9.4 or later")
  330. return
  331. end
  332. local hex_x = tonumber(cfg.hex_x) or helper.wml_error("Missing required hex_x= attribute in [animate_path]")
  333. local hex_y = tonumber(cfg.hex_y) or helper.wml_error("Missing required hex_y= attribute in [animate_path]")
  334. local temp = cfg.image or helper.wml_error("[animate_path] missing required image= attribute")
  335. local images, num_images = load_list(temp)
  336. local frames = tonumber(cfg.frames) or num_images
  337. if frames < 2 then
  338. helper.wml_error("[animate_path] requires frames be at least 2")
  339. end
  340. local delay = tonumber(cfg.frame_length) or helper.wml_error("Missing required frame_length= attribute in [animate_path]")
  341. local linger = cfg.linger
  342. temp = cfg.x or helper.wml_error("[animate_path] missing required x= attribute")
  343. local x_locs, num_locs = load_list(temp)
  344. temp = cfg.y or helper.wml_error("[animate_path] missing required y= attribute")
  345. local y_locs, num_y_locs = load_list(temp)
  346. if num_locs ~= num_y_locs then
  347. helper.wml_error("The number of x and y values must be the same in [animate_path]")
  348. end
  349. local transpose = cfg.transpose
  350. local interpolation = cfg.interpolation or "linear"
  351. if not interpolation_methods[interpolation] then
  352. helper.wml_error("[animate_path]: Unknown interpolation method: "..interpolation)
  353. end
  354. if transpose then
  355. x_locs, y_locs = y_locs, x_locs
  356. end
  357. local calc_path_length, reached_point, get_location = interpolation_methods[interpolation]( x_locs, y_locs, num_locs )
  358. local lengths, num_lengths, total_length = calc_path_length()
  359. local length_seen = 0
  360. local next_point = 1
  361. -- subtract 1 from frames to avoid fencepost problems
  362. frames = frames - 1
  363. local length_per_frame = total_length / frames
  364. local x, y, target_hex_x, target_hex_y, image_name
  365. reached_point(0)
  366. for i = 0, frames do
  367. local cur_offset = i * length_per_frame - length_seen
  368. while next_point <= num_lengths and cur_offset > lengths[next_point] do
  369. reached_point(next_point)
  370. cur_offset = cur_offset - lengths[next_point]
  371. length_seen = length_seen + lengths[next_point]
  372. next_point = next_point + 1
  373. end
  374. if next_point <= num_lengths then
  375. cur_offset = cur_offset / lengths[next_point]
  376. else
  377. -- avoid rounding error at end of path
  378. cur_offset = 0
  379. end
  380. x, y = get_location(cur_offset)
  381. if transpose then
  382. x, y = y, x
  383. end
  384. target_hex_x, target_hex_y, x, y = calc_image_hex_offset(hex_x,hex_y,x,y)
  385. image_name = get_image_name_with_offset( target_hex_x, target_hex_y, x, y, images[i%num_images])
  386. wesnoth.add_tile_overlay(target_hex_x, target_hex_y, {x = target_hex_x, y = target_hex_y, halo = image_name})
  387. wesnoth.delay(delay)
  388. wesnoth.remove_tile_overlay(target_hex_x, target_hex_y, image_name)
  389. end
  390. if linger then
  391. items.place_halo(target_hex_x, target_hex_y, image_name)
  392. end
  393. end
  394. return interpolation_methods