spine-runtimes/spine-lua/PathConstraint.lua
Mario Zechner 5b1814cff3 spine-lua, spine-love, spine-corona update to 3.4.02 (#722)
The spine-lua API has been updated to be compatible with Spine version 3.4.02 (latest stable). The spine-lua API now supports path constraints, transform constraints, uses the new way we encode meshes etc. There are no API breaking changes, only API additions, such as PathConstraints and TransformConstraints as well as additional methods to Skeleton and similar classes. The internals of the spine-lua API have also been updated to follow Lua best performance practices by localizing heavily and using meta tables for "class methods". The spine-lua API now also loads texture atlases as exported by Spine. All that is required for a consumer is to supply an image loading function for their specific engine/framework. We provide implementations for spine-love and spine-corona.

The spine-love API can now render all Spine attachment types, including meshes and linked meshes. The API has changed. Where previously a "class" Skeleton existed with a draw function, the new spine-love API introduces a new SkeletonRenderer. See the example on API usage.

The spine-corona API can now also render all Spine attachment types. The API has not changed.
2016-10-11 16:33:25 +02:00

517 lines
15 KiB
Lua

-------------------------------------------------------------------------------
-- Spine Runtimes Software License
-- Version 2.3
--
-- Copyright (c) 2013-2015, Esoteric Software
-- All rights reserved.
--
-- You are granted a perpetual, non-exclusive, non-sublicensable and
-- non-transferable license to use, install, execute and perform the Spine
-- Runtimes Software (the "Software") and derivative works solely for personal
-- or internal use. Without the written permission of Esoteric Software (see
-- Section 2 of the Spine Software License Agreement), you may not (a) modify,
-- translate, adapt or otherwise create derivative works, improvements of the
-- Software or develop new applications using the Software or (b) remove,
-- delete, alter or obscure any trademarks or any copyright, trademark, patent
-- or other intellectual property or proprietary rights notices on or in the
-- Software, including any copy thereof. Redistributions in binary or source
-- form must include this license and terms.
--
-- THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
-- IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
-- MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
-- EVENT SHALL ESOTERIC SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
-- PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
-- OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
-- WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
-- OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
-- ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-------------------------------------------------------------------------------
-- FIXME the logic in this file uses 0-based indexing. Each array
-- access adds 1 to the calculated index. We should switch the logic
-- to 1-based indexing eventually.
local setmetatable = setmetatable
local AttachmentType = require "spine-lua.attachments.AttachmentType"
local PathConstraintData = require "spine-lua.PathConstraintData"
local utils = require "spine-lua.utils"
local math_pi = math.pi
local math_pi2 = math.pi * 2
local math_atan2 = math.atan2
local math_sqrt = math.sqrt
local math_acos = math.acos
local math_sin = math.sin
local math_cos = math.cos
local table_insert = table.insert
local math_deg = math.deg
local math_rad = math.rad
local math_abs = math.abs
local math_max = math.max
local PathConstraint = {}
PathConstraint.__index = PathConstraint
PathConstraint.NONE = -1
PathConstraint.BEFORE = -2
PathConstraint.AFTER = -3
function PathConstraint.new (data, skeleton)
if not data then error("data cannot be nil", 2) end
if not skeleton then error("skeleton cannot be nil", 2) end
local self = {
data = data,
bones = {},
target = skeleton:findSlot(data.target.name),
position = data.position,
spacing = data.spacing,
rotateMix = data.rotateMix,
translateMix = data.translateMix,
spaces = {},
positions = {},
world = {},
curves = {},
lengths = {},
segments = {}
}
setmetatable(self, PathConstraint)
for i,boneData in ipairs(data.bones) do
table_insert(self.bones, skeleton:findBone(boneData.name))
end
return self
end
function PathConstraint:apply ()
self:update()
end
function PathConstraint:update ()
local attachment = self.target.attachment
if not (attachment.type == AttachmentType.path) then return end
local rotateMix = self.rotateMix
local translateMix = self.translateMix
local translate = translateMix > 0
local rotate = rotateMix > 0
if not translate and not rotate then return end
local data = self.data;
local spacingMode = data.spacingMode
local lengthSpacing = spacingMode == PathConstraintData.SpacingMode.length
local rotateMode = data.rotateMode
local tangents = rotateMode == PathConstraintData.RotateMode.tangent
local scale = rotateMode == PathConstraintData.RotateMode.chainscale
local bones = self.bones
local boneCount = #bones
local spacesCount = boneCount + 1
if tangents then spacesCount = boneCount end
local spaces = utils.setArraySize(self.spaces, spacesCount)
local lengths = nil
local spacing = self.spacing
if scale or lengthSpacing then
if scale then lengths = utils.setArraySize(self.lengths, boneCount) end
local i = 0
local n = spacesCount - 1
while i < n do
local bone = bones[i + 1];
local length = bone.data.length
local x = length * bone.a
local y = length * bone.c
length = math_sqrt(x * x + y * y)
if scale then lengths[i + 1] = length end
i = i + 1
if lengthSpacing then spaces[i + 1] = math_max(0, length + spacing) else spaces[i + 1] = spacing end
end
else
local i = 1
while i < spacesCount do
spaces[i + 1] = spacing
i = i + 1
end
end
local positions = self:computeWorldPositions(attachment, spacesCount, tangents, data.positionMode == PathConstraintData.PositionMode.percent, spacingMode == PathConstraintData.SpacingMode.percent)
local skeleton = self.target.bone.skeleton
local skeletonX = skeleton.x
local skeletonY = skeleton.y
local boneX = positions[1]
local boneY = positions[2]
local offsetRotation = data.offsetRotation
local tip = rotateMode == PathConstraintData.RotateMode.chain and offsetRotation == 0
local i = 0
local p = 3
while i < boneCount do
local bone = bones[i + 1]
bone.worldX = bone.worldX + (boneX - skeletonX - bone.worldX) * translateMix
bone.worldY = bone.worldY + (boneY - skeletonY - bone.worldY) * translateMix
local x = positions[p + 1]
local y = positions[p + 2]
local dx = x - boneX
local dy = y - boneY
if scale then
local length = lengths[i + 1]
if length ~= 0 then
local s = (math_sqrt(dx * dx + dy * dy) / length - 1) * rotateMix + 1
bone.a = bone.a * s
bone.c = bone.c * s
end
end
boneX = x
boneY = y
if rotate then
local a = bone.a
local b = bone.b
local c = bone.c
local d = bone.d
local r = 0
local cos = 0
local sin = 0
if tangents then
r = positions[p - 1 + 1]
elseif spaces[i + 1 + 1] == 0 then
r = positions[p + 2 + 1]
else
r = math_atan2(dy, dx)
end
r = r - (math_atan2(c, a) - math_rad(offsetRotation))
if tip then
cos = math_cos(r)
sin = math_sin(r)
local length = bone.data.length
boneX = boneX + (length * (cos * a - sin * c) - dx) * rotateMix;
boneY = boneY + (length * (sin * a + cos * c) - dy) * rotateMix;
end
if r > math_pi then
r = r - math_pi2
elseif r < -math_pi then
r = r + math_pi2
end
r = r * rotateMix
cos = math_cos(r)
sin = math.sin(r)
bone.a = cos * a - sin * c
bone.b = cos * b - sin * d
bone.c = sin * a + cos * c
bone.d = sin * b + cos * d
end
i = i + 1
p = p + 3
end
end
function PathConstraint:computeWorldPositions (path, spacesCount, tangents, percentPosition, percentSpacing)
local target = self.target
local position = self.position
local spaces = self.spaces
local out = utils.setArraySize(self.positions, spacesCount * 3 + 2)
local world = nil
local closed = path.closed
local verticesLength = path.worldVerticesLength
local curveCount = verticesLength / 6
local prevCurve = PathConstraint.NONE
if not path.constantSpeed then
local lengths = path.lengths
if closed then curveCount = curveCount - 1 else curveCount = curveCount - 2 end
local pathLength = lengths[curveCount + 1];
if percentPosition then position = position * pathLength end
if percentSpacing then
local i = 0
while i < spacesCount do
spaces[i + 1] = spaces[i + 1] * pathLength
i = i + 1
end
end
world = utils.setArraySize(self.world, 8);
local i = 0
local o = 0
local curve = 0
while i < spacesCount do
local space = spaces[i + 1];
position = position + space
local p = position
local skip = false
if closed then
p = p % pathLength
if p < 0 then p = p + pathLength end
curve = 0
elseif p < 0 then
if prevCurve ~= PathConstraint.BEFORE then
prevCurve = PathConstraint.BEFORE
path:computeWorldVerticesWith(target, 2, 4, world, 0)
end
self:addBeforePosition(p, world, 0, out, o)
skip = true
elseif p > pathLength then
if prevCurve ~= PathConstraint.AFTER then
prevCurve = PathConstraint.AFTER
path:computeWorldVerticesWith(target, verticesLength - 6, 4, world, 0)
end
self:addAfterPosition(p - pathLength, world, 0, out, o)
skip = true
end
if not skip then
-- Determine curve containing position.
while true do
local length = lengths[curve + 1]
if p <= length then
if curve == 0 then
p = p / length
else
local prev = lengths[curve - 1 + 1]
p = (p - prev) / (length - prev)
end
break
end
curve = curve + 1
end
if curve ~= prevCurve then
prevCurve = curve
if closed and curve == curveCount then
path:computeWorldVerticesWith(target, verticesLength - 4, 4, world, 0)
path:computeWorldVerticesWith(target, 0, 4, world, 4)
else
path:computeWorldVerticesWith(target, curve * 6 + 2, 8, world, 0)
end
end
self:addCurvePosition(p, world[1], world[2], world[3], world[4], world[5], world[6], world[7], world[8], out, o, tangents or (i > 0 and space == 0))
end
i = i + 1
o = o + 3
end
return out
end
-- World vertices.
if closed then
verticesLength = verticesLength + 2
world = utils.setArraySize(self.world, verticesLength)
path:computeWorldVerticesWith(target, 2, verticesLength - 4, world, 0)
path:computeWorldVerticesWith(target, 0, 2, world, verticesLength - 4)
world[verticesLength - 2 + 1] = world[0 + 1]
world[verticesLength - 1 + 1] = world[1 + 1]
else
curveCount = curveCount - 1
verticesLength = verticesLength - 4;
world = utils.setArraySize(self.world, verticesLength)
path:computeWorldVerticesWith(target, 2, verticesLength, world, 0)
end
-- Curve lengths.
local curves = utils.setArraySize(self.curves, curveCount)
local pathLength = 0;
local x1 = world[0 + 1]
local y1 = world[1 + 1]
local cx1 = 0
local cy1 = 0
local cx2 = 0
local cy2 = 0
local x2 = 0
local y2 = 0
local tmpx = 0
local tmpy = 0
local dddfx = 0
local dddfy = 0
local ddfx = 0
local ddfy = 0
local dfx = 0
local dfy = 0
i = 0
local w = 2
while i < curveCount do
cx1 = world[w + 1]
cy1 = world[w + 2]
cx2 = world[w + 3]
cy2 = world[w + 4]
x2 = world[w + 5]
y2 = world[w + 6]
tmpx = (x1 - cx1 * 2 + cx2) * 0.1875
tmpy = (y1 - cy1 * 2 + cy2) * 0.1875
dddfx = ((cx1 - cx2) * 3 - x1 + x2) * 0.09375
dddfy = ((cy1 - cy2) * 3 - y1 + y2) * 0.09375
ddfx = tmpx * 2 + dddfx
ddfy = tmpy * 2 + dddfy
dfx = (cx1 - x1) * 0.75 + tmpx + dddfx * 0.16666667
dfy = (cy1 - y1) * 0.75 + tmpy + dddfy * 0.16666667
pathLength = pathLength + math_sqrt(dfx * dfx + dfy * dfy)
dfx = dfx + ddfx
dfy = dfy + ddfy
ddfx = ddfx + dddfx
ddfy = ddfy + dddfy
pathLength = pathLength + math_sqrt(dfx * dfx + dfy * dfy)
dfx = dfx + ddfx
dfy = dfy + ddfy
pathLength = pathLength + math_sqrt(dfx * dfx + dfy * dfy)
dfx = dfx + ddfx + dddfx
dfy = dfy + ddfy + dddfy
pathLength = pathLength + math_sqrt(dfx * dfx + dfy * dfy)
curves[i + 1] = pathLength
x1 = x2
y1 = y2
i = i + 1
w = w + 6
end
if percentPosition then position = position * pathLength end
if percentSpacing then
local i = 0
while i < spacesCount do
spaces[i + 1] = spaces[i + 1] * pathLength
i = i + 1
end
end
local segments = self.segments
local curveLength = 0
local i = 0
local o = 0
local curve = 0
local segment = 0
while i < spacesCount do
local space = spaces[i + 1]
position = position + space
local p = position
local skip = false
if closed then
p = p % pathLength
if p < 0 then p = p + pathLength end
curve = 0
elseif p < 0 then
self:addBeforePosition(p, world, 0, out, o)
skip = true
elseif p > pathLength then
self:addAfterPosition(p - pathLength, world, verticesLength - 4, out, o)
skip = true
end
if not skip then
-- Determine curve containing position.
while true do
local length = curves[curve + 1]
if p <= length then
if curve == 0 then
p = p / length
else
local prev = curves[curve - 1 + 1]
p = (p - prev) / (length - prev)
end
break
end
curve = curve + 1
end
-- Curve segment lengths.
if curve ~= prevCurve then
prevCurve = curve
local ii = curve * 6
x1 = world[ii + 1]
y1 = world[ii + 2]
cx1 = world[ii + 3]
cy1 = world[ii + 4]
cx2 = world[ii + 5]
cy2 = world[ii + 6]
x2 = world[ii + 7]
y2 = world[ii + 8]
tmpx = (x1 - cx1 * 2 + cx2) * 0.03
tmpy = (y1 - cy1 * 2 + cy2) * 0.03
dddfx = ((cx1 - cx2) * 3 - x1 + x2) * 0.006
dddfy = ((cy1 - cy2) * 3 - y1 + y2) * 0.006
ddfx = tmpx * 2 + dddfx
ddfy = tmpy * 2 + dddfy
dfx = (cx1 - x1) * 0.3 + tmpx + dddfx * 0.16666667
dfy = (cy1 - y1) * 0.3 + tmpy + dddfy * 0.16666667
curveLength = math_sqrt(dfx * dfx + dfy * dfy)
segments[1] = curveLength
ii = 1
while ii < 8 do
dfx = dfx + ddfx
dfy = dfy + ddfy
ddfx = ddfx + dddfx
ddfy = ddfy + dddfy
curveLength = curveLength + math_sqrt(dfx * dfx + dfy * dfy)
segments[ii + 1] = curveLength
ii = ii + 1
end
dfx = dfx + ddfx
dfy = dfy + ddfy
curveLength = curveLength + math_sqrt(dfx * dfx + dfy * dfy)
segments[9] = curveLength
dfx = dfx + ddfx + dddfx
dfy = dfy + ddfy + dddfy
curveLength = curveLength + math_sqrt(dfx * dfx + dfy * dfy)
segments[10] = curveLength
segment = 0
end
-- Weight by segment length.
p = p * curveLength
while true do
local length = segments[segment + 1]
if p <= length then
if segment == 0 then
p = p / length
else
local prev = segments[segment - 1 + 1]
p = segment + (p - prev) / (length - prev)
end
break;
end
segment = segment + 1
end
self:addCurvePosition(p * 0.1, x1, y1, cx1, cy1, cx2, cy2, x2, y2, out, o, tangents or (i > 0 and space == 0))
end
i = i + 1
o = o + 3
end
return out
end
function PathConstraint:addBeforePosition (p, temp, i, out, o)
local x1 = temp[i + 1]
local y1 = temp[i + 2]
local dx = temp[i + 3] - x1
local dy = temp[i + 4] - y1
local r = math_atan2(dy, dx)
out[o + 1] = x1 + p * math_cos(r)
out[o + 2] = y1 + p * math_sin(r)
out[o + 3] = r
end
function PathConstraint:addAfterPosition(p, temp, i, out, o)
local x1 = temp[i + 3]
local y1 = temp[i + 4]
local dx = x1 - temp[i + 1]
local dy = y1 - temp[i + 2]
local r = math_atan2(dy, dx)
out[o + 1] = x1 + p * math_cos(r)
out[o + 2] = y1 + p * math_sin(r)
out[o + 3] = r
end
function PathConstraint:addCurvePosition(p, x1, y1, cx1, cy1, cx2, cy2, x2, y2, out, o, tangents)
if p == 0 then p = 0.0001 end
local tt = p * p
local ttt = tt * p
local u = 1 - p
local uu = u * u
local uuu = uu * u
local ut = u * p
local ut3 = ut * 3
local uut3 = u * ut3
local utt3 = ut3 * p
local x = x1 * uuu + cx1 * uut3 + cx2 * utt3 + x2 * ttt
local y = y1 * uuu + cy1 * uut3 + cy2 * utt3 + y2 * ttt
out[o + 1] = x
out[o + 2] = y
if tangents then out[o + 3] = math_atan2(y - (y1 * uu + cy1 * ut * 2 + cy2 * tt), x - (x1 * uu + cx1 * ut * 2 + cx2 * tt)) end
end
return PathConstraint