diff --git a/header.lua b/header.lua index 329d4a1..2c311e5 100644 --- a/header.lua +++ b/header.lua @@ -1,7 +1,7 @@ !if LOVE2D then require("playbit.graphics") ---[[ since there is no CoreLibs/playdate, this file should always +--[[ since there is no CoreLibs/playdate, this file should always be included here so the methods are always available ]]-- require("playdate.playdate") --[[ not really a way around including this one, but probably doesn't really @@ -35,6 +35,8 @@ math.randomseed(os.time()) local font = playdate.graphics.font.new("fonts/Phozon/Phozon") playdate.graphics.setFont(font) +local updateCoroutine + function love.draw() -- must be changed at start of frame when canvas is not active local newCanvasWidth, newCanvasHeight = playbit.graphics.getCanvasSize() @@ -66,9 +68,8 @@ function love.draw() -- render to canvas to allow 2x scaling love.graphics.setCanvas(playbit.graphics.canvas) - love.graphics.setShader(playbit.graphics.shader) - --[[ + --[[ Love2d won't allow a canvas to be set outside of the draw function, so we need to do this on the first frame of draw. Otherwise setting the bg color outside of playdate.update() won't be consistent with PD. --]] @@ -86,7 +87,14 @@ function love.draw() love.graphics.translate(playbit.graphics.drawOffset.x, playbit.graphics.drawOffset.y) -- main update - playdate.update() + if not updateCoroutine or coroutine.status(updateCoroutine) == "dead" then + updateCoroutine = coroutine.create(playdate.update) + end + + local ok, err = coroutine.resume(updateCoroutine) + if not ok then + error(err) + end -- debug draw if playdate.debugDraw then @@ -102,19 +110,16 @@ function love.draw() love.graphics.setCanvas() -- clear shader so that canvas is rendered normally - love.graphics.setShader() - - -- always render pure white so its not tinted - local r, g, b = love.graphics.getColor() - love.graphics.setColor(1, 1, 1, 1) + local shader = love.graphics.getShader() + love.graphics.setShader(playbit.graphics.shaders.final) -- draw canvas to screen local currentCanvasScale = playbit.graphics.getCanvasScale() local x, y = playbit.graphics.getCanvasPosition() love.graphics.draw(playbit.graphics.canvas, x, y, 0, currentCanvasScale, currentCanvasScale) - -- reset back to set color - love.graphics.setColor(r, g, b, 1) + -- reset back the shader + love.graphics.setShader(shader) -- update emulated input playdate.updateInput() diff --git a/playbit/graphics.lua b/playbit/graphics.lua index aed02ca..d659657 100644 --- a/playbit/graphics.lua +++ b/playbit/graphics.lua @@ -10,6 +10,21 @@ module.COLOR_BLACK = { 49 / 255, 47 / 255, 40 / 255, 1 } module.colorWhite = module.COLOR_WHITE module.colorBlack = module.COLOR_BLACK + +module.shaders = +{ + final = love.graphics.newShader("playbit/shaders/final.glsl"), + color = love.graphics.newShader("playbit/shaders/color.glsl"), + pattern = love.graphics.newShader("playbit/shaders/pattern.glsl"), + image = { } +} + +local shader = love.filesystem.read("playbit/shaders/image.glsl") +for i = 0, 9 do + local src = "#define DRAW_MODE " .. i .. "\n" .. shader + module.shaders.image[i] = love.graphics.newShader(src) +end + module.shader = love.graphics.newShader("playdate/shader") module.drawOffset = { x = 0, y = 0} module.drawColorIndex = 1 @@ -17,13 +32,15 @@ module.drawColor = module.colorWhite module.backgroundColorIndex = 0 module.backgroundColor = module.colorBlack module.activeFont = {} -module.drawMode = "copy" +module.imageDrawMode = 0 +module.drawMode = nil module.canvas = love.graphics.newCanvas() module.contextStack = {} -- shared quad to reduce gc module.quad = love.graphics.newQuad(0, 0, 1, 1, 1, 1) module.lastClearColor = module.colorWhite -module.drawPattern = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} +module.drawPattern = nil +module.lineWidth = 1 local canvasScale = 1 local canvasWidth = 400 @@ -107,44 +124,33 @@ end ---@param white table An array of 4 values that correspond to RGBA that range from 0 to 1. ---@param black table An array of 4 values that correspond to RGBA that range from 0 to 1. function module.setColors(white, black) - if white == nil then - white = module.COLOR_WHITE - end - if black == nil then - black = module.COLOR_BLACK - end - - module.colorWhite = white - module.colorBlack = black - module.shader:send("white", white) - module.shader:send("black", black) - - if module.backgroundColorIndex == 1 then - module.backgroundColor = module.colorWhite - else - module.backgroundColor = module.colorBlack - end - - if module.drawColorIndex == 1 then - module.drawColor = module.colorWhite - else - module.drawColor = module.colorBlack - end + module.colorWhite = white or module.COLOR_WHITE + module.colorBlack = black or module.COLOR_BLACK + module.shaders.final:send("white", white) + module.shaders.final:send("black", black) end -function module.updateContext() - if #module.contextStack == 0 then - return - end +local function getShader(mode) + if mode == "line" then + return module.shaders.color - local activeContext = module.contextStack[#module.contextStack] + elseif mode == "fill" then + if module.drawPattern then + return module.shaders.pattern + else + return module.shaders.color + end - -- love2d doesn't allow calling newImageData() when canvas is active - love.graphics.setCanvas() - local imageData = activeContext._canvas:newImageData() - love.graphics.setCanvas(activeContext._canvas) + elseif mode == "image" then + return module.shaders.image[module.imageDrawMode] + end +end - -- update image - activeContext.data:replacePixels(imageData) +function module.setDrawMode(mode) + if module.drawMode ~= mode then + module.drawMode = mode + local shader = getShader(mode) + love.graphics.setShader(shader) + end end -!end \ No newline at end of file +!end diff --git a/playbit/shaders/color.glsl b/playbit/shaders/color.glsl new file mode 100644 index 0000000..9ce8922 --- /dev/null +++ b/playbit/shaders/color.glsl @@ -0,0 +1,11 @@ +#pragma language glsl3 + +extern vec4 drawColor; + +vec4 effect(vec4 color, Image tex, vec2 tex_coords, vec2 screen_coords) +{ + float outColor = drawColor.r; + float outAlpha = drawColor.a; + + return vec4(outColor, outColor, outColor, outAlpha); +} diff --git a/playbit/shaders/final.glsl b/playbit/shaders/final.glsl new file mode 100644 index 0000000..ea12c55 --- /dev/null +++ b/playbit/shaders/final.glsl @@ -0,0 +1,15 @@ +#pragma language glsl3 + +extern vec4 white = vec4(176.0f / 255.0f, 174.0f / 255.0f, 167.0f / 255.0f, 1); +extern vec4 black = vec4( 49.0f / 255.0f, 47.0f / 255.0f, 40.0f / 255.0f, 1); + +vec4 effect(vec4 color, Image tex, vec2 tex_coords, vec2 screen_coords) +{ + vec4 inColor = Texel(tex, tex_coords) * color; + + vec4 outColor; + outColor.rgb = mix(black.rgb, white.rgb, inColor.r); + outColor.a = 1; + + return outColor; +} \ No newline at end of file diff --git a/playbit/shaders/image.glsl b/playbit/shaders/image.glsl new file mode 100644 index 0000000..b929534 --- /dev/null +++ b/playbit/shaders/image.glsl @@ -0,0 +1,49 @@ +#pragma language glsl3 + +extern Image canvas; + +vec4 effect(vec4 color, Image tex, vec2 tex_coords, vec2 screen_coords) +{ + vec4 texColor = Texel(tex, tex_coords); + float grayscale = dot(texColor.rgb, vec3(0.2126, 0.7152, 0.0722)); + float inColor = step(0.5, grayscale); + float inAlpha = step(0.5, texColor.a); + +#if DRAW_MODE == 1 // White Transparent + float outColor = inColor; + float outAlpha = inAlpha * (1.0 - inColor); + +#elif DRAW_MODE == 2 // Black Transparent + float outColor = inColor; + float outAlpha = inAlpha * inColor; + +#elif DRAW_MODE == 3 // Fill White + float outColor = 1; + float outAlpha = inAlpha; + +#elif DRAW_MODE == 4 // Fill Black + float outColor = 0; + float outAlpha = inAlpha; + +#elif DRAW_MODE == 5 // XOR + vec4 canvasColor = Texel(canvas, screen_coords / love_ScreenSize.xy); + float outColor = abs(canvasColor.r - inColor); + float outAlpha = inAlpha; + +#elif DRAW_MODE == 6 // NXOR + vec4 canvasColor = Texel(canvas, screen_coords / love_ScreenSize.xy); + float outColor = 1.0 - abs(canvasColor.r - inColor); + float outAlpha = inAlpha; + +#elif DRAW_MODE == 7 // Inverted + float outColor = 1.0 - inColor; + float outAlpha = inAlpha; + +#else // Copy + float outColor = inColor; + float outAlpha = inAlpha; + +#endif + + return vec4(outColor, outColor, outColor, outAlpha); +} diff --git a/playbit/shaders/pattern.glsl b/playbit/shaders/pattern.glsl new file mode 100644 index 0000000..56bb565 --- /dev/null +++ b/playbit/shaders/pattern.glsl @@ -0,0 +1,15 @@ +#pragma language glsl3 + +extern float pattern[64]; + +vec4 effect(vec4 color, Image tex, vec2 tex_coords, vec2 screen_coords) +{ + // Use mod() to get the position of the current pixel within the 8x8 pattern + int x = int(mod(screen_coords.x, 8.0)); + int y = int(mod(screen_coords.y, 8.0)); + + float outColor = pattern[x + y * 8]; + float outAlpha = 1; + + return vec4(outColor, outColor, outColor, outAlpha); +} diff --git a/playbit/util.lua b/playbit/util.lua index 985cf74..b1f90f7 100644 --- a/playbit/util.lua +++ b/playbit/util.lua @@ -23,4 +23,24 @@ function module.sign(a) else return 0 end +end + +function module.clamp01(x) + if x <= 0 then + return 0 + elseif x >= 1 then + return 1 + else + return x + end +end + +function module.clamp(x, min, max) + if x <= min then + return min + elseif x >= max then + return max + else + return x + end end \ No newline at end of file diff --git a/playdate/affineTransform.lua b/playdate/affineTransform.lua new file mode 100644 index 0000000..e928d1e --- /dev/null +++ b/playdate/affineTransform.lua @@ -0,0 +1,240 @@ +local module = {} +playdate.geometry.affineTransform = module + +local meta = {} +meta.__index = meta +module.__index = meta + +local function concat(a, b) + return + a.m11 * b.m11 + a.m21 * b.m12, + a.m12 * b.m11 + a.m22 * b.m12, + a.m11 * b.m21 + a.m21 * b.m22, + a.m12 * b.m21 + a.m22 * b.m22, + a.tx * b.m11 + a.ty * b.m12 + b.tx, + a.tx * b.m21 + a.ty * b.m22 + b.ty +end + + +local function transform(t, x, y) + return + x * t.m11 + y * t.m12 + t.tx, + x * t.m21 + y * t.m22 + t.ty +end + +local function translation(dx, dy) + return module.new(1, 0, 0, 1, dx, dy) +end + +local function scaling(sx, sy) + sy = sy or sx + return module.new(sx, 0, 0, sy, 0, 0) +end + +local function rotation(angle) + angle = math.rad(angle) + local c = math.cos(angle) + local s = math.sin(angle) + return module.new(c, -s, s, c, 0, 0) +end + +local function rotationAround(angle, px, py) + local rad = math.rad(angle) + local c = math.cos(rad) + local s = math.sin(rad) + local tx = px - px * c + py * s + local ty = py - px * s - py * c + return module.new(c, -s, s, c, tx, ty) +end + +local function skewing(sx, sy) + local rx = math.tan(math.rad(sx)) + local ry = math.tan(math.rad(sy)) + return module.new(1, rx, ry, 1, 0, 0) +end + +function module.new(m11, m12, m21, m22, tx, ty) + local o = {} + + o.m11 = m11 or 1 + o.m12 = m12 or 0 + o.m21 = m21 or 0 + o.m22 = m22 or 1 + o.tx = tx or 0 + o.ty = ty or 0 + + setmetatable(o, meta) + return o +end + +function meta:copy() + return module.new(self.m11, self.m12, self.m21, self.m22, self.tx, self.ty) +end + +function meta:unpack() + return self.m11, self.m12, self.m21, self.m22, self.tx, self.ty +end + +function meta:invert() + local a, b = self.m11, self.m12 + local c, d = self.m21, self.m22 + local tx, ty = self.tx, self.ty + + local det = a * d - b * c + local invDet = 1 / det + + local im11 = d * invDet + local im12 = -b * invDet + local im21 = -c * invDet + local im22 = a * invDet + + local itx = -(im11 * tx + im12 * ty) + local ity = -(im21 * tx + im22 * ty) + + self.m11 = im11 + self.m12 = im12 + self.m21 = im21 + self.m22 = im22 + self.tx = itx + self.ty = ity +end + +function meta:reset() + self.m11 = 1 + self.m12 = 0 + self.m21 = 0 + self.m22 = 1 + self.tx = 0 + self.ty = 0 +end + +function meta:concat(b) + self.m11, self.m12, self.m21, self.m22, self.tx, self.ty = concat(self, b) +end + +function meta:translate(dx, dy) + local t = translation(dx, dy) + self:concat(t) +end + +function meta:translatedBy(dx, dy) + local t = self:copy() + t:translate(dx, dy) + return t +end + +function meta:scale(sx, sy) + local t = scaling(sx, sy) + self:concat(t) +end + +function meta:scaledBy(sx, sy) + local t = self:copy() + t:scale(sx, sy) + return t +end + +function meta:rotate(angle, x, y) + local t + + if x then + if y then + t = rotationAround(angle, x, y) + else + local pt = x + t = rotationAround(angle, pt.x, pt.y) + end + else + t = rotation(angle) + end + + self:concat(t) +end + +function meta:rotatedBy(angle, pointOrX, y) + local t = self:copy() + t:rotate(angle, pointOrX, y) + return t +end + +function meta:skew(sx, sy) + local t = skewing(sx, sy) + self:concat(t) +end + +function meta:skewedBy(sx, sy) + local t = self:copy() + t:skew(sx, sy) + return t +end + +function meta:transformXY(x, y) + return transform(self, x, y) +end + +function meta:transformPoint(p) + p.x, p.y = transform(self, p.x, p.y) +end + +function meta:transformedPoint(p) + local c = p:copy() + self:transformPoint(c) + return c +end + +function meta:transformLineSegment(ls) + ls.x1, ls.y1 = transform(self, ls.x1, ls.y1) + ls.x2, ls.y2 = transform(self, ls.x2, ls.y2) +end + +function meta:transformedLineSegment(ls) + local c = ls:copy() + self:transformLineSegment(c) + return c +end + +function meta:transformAABB(r) + error("[ERR] playdate.geometry.affineTransform:transformAABB() is not yet implemented.") +end + +function meta:transformedAABB(r) + local c = r:copy() + self:transformAABB(c) + return c +end + +function meta:transformPolygon(p) + local pts = p._points + for i = 1, #pts, 2 do + local j = i + 1 + pts[i], pts[j] = transform(self, pts[i], pts[j]) + end + -- reset cached length + p._length = nil +end + +function meta:transformedPolygon(p) + local c = p:copy() + self:transformPolygon(c) + return c +end + +meta.__mul = function(a, b) + if b._type == "point" then + local x, y = transform(a, b.x, b.y) + return playdate.geometry.point.new(x, y) + + elseif b._type == "vector2D" then + local dx, dy = transform(a, b.dx, b.dy) + return playdate.geometry.vector2D.new(dx, dy) + + else + local m11, m12, m21, m22, tx, ty = concat(a, b) + return module.new(m11, m12, m21, m22, tx, ty) + end +end + +meta.__tostring = function(t) + return string.format("(m11=%s, m12=%s, m21=%s, m22=%s, tx=%s, ty=%s)", + t.m11, t.m12, t.m21, t.m22, t.tx, t.ty) +end \ No newline at end of file diff --git a/playdate/animator.lua b/playdate/animator.lua index af07b23..8389766 100644 --- a/playdate/animator.lua +++ b/playdate/animator.lua @@ -1,6 +1,6 @@ -- docs: https://sdk.play.date/2.6.2/Inside%20Playdate.html#C-graphics.animator -require("easing") +require("playdate.easing") playdate.graphics = playdate.graphics or {} @@ -11,33 +11,281 @@ local meta = {} meta.__index = meta module.__index = meta --- note: this function has 5 overloaded definitions as of 2.6.2. +local function normalizeTime(anim, t) + t = t - anim._startTimeOffset + + if t < 0 then + return 0, false + end + + local dur = anim._duration + if anim.reverses then + dur = dur * 2 + end + + local repeats = math.floor(t / dur) + + t = t % dur + + if t > anim._duration then + t = 2 * anim._duration - t + end + + if not anim.repeats and anim.repeatCount >= 0 and repeats > anim.repeatCount then + if anim.reverses then + return 0, true + else + return anim._duration, true + end + end + + return t, false +end + +local function updateAnimator(anim) + if not anim._ended then + local t = playdate.getCurrentTimeMilliseconds() - anim._startTime + anim._currentTime, anim._ended = normalizeTime(anim, t) + end +end + +local function getValueForNumbers(anim, time) + return anim._easingFunction(time, anim._startValue, anim._endValue - anim._startValue, anim._duration) +end + +local function getValueForPoints(anim, time) + local startValue = anim._startValue + local endValue = anim._endValue + local x = anim._easingFunction(time, startValue.x, endValue.x - startValue.x, anim._duration) + local y = anim._easingFunction(time, startValue.y, endValue.y - startValue.y, anim._duration) + return playdate.geometry.point.new(x, y) +end + +local function getValueForLineSegment(anim, time) + local lineSegment = anim._lineSegment + local dist = anim._easingFunction(time, 0, lineSegment:length(), anim._duration, anim.s or anim.easingAmplitude, + anim.easingPeriod) + return lineSegment:pointOnLine(dist, true) +end + +local function getValueForArc(anim, time) + local arc = anim._arc + local dist = anim._easingFunction(time, 0, arc:length(), anim._duration, anim.s or anim.easingAmplitude, + anim.easingPeriod) + return arc:pointOnArc(dist, true) +end + +local function getValueForPolygon(anim, time) + local polygon = anim._polygon + local dist = anim._easingFunction(time, 0, polygon:length(), anim._duration, anim.s or anim.easingAmplitude, + anim.easingPeriod) + return polygon:pointOnPolygon(dist, true) +end + +local function getValueForPart(part, dist) + if part._type == "lineSegment" then + return part:pointOnLine(dist, true) + elseif part._type == "arc" then + return part:pointOnArc(dist, true) + elseif part._type == "polygon" then + return part:pointOnPolygon(dist, true) + end +end + +local function getValueForParts(anim, time) + local parts = anim._parts + local lengths = anim._lengths + local dist = anim._easingFunction(time, 0, anim._totalLength, anim._duration, anim.s or anim.easingAmplitude, anim.easingPeriod) + + local i = 1 + while i < #parts and dist > lengths[i] do + i = i + 1 + end + + local part = parts[i] + local dist = dist - (lengths[i - 1] or 0) + + return getValueForPart(part, dist) +end + +local function getValueForPartsWithDurations(anim, time) + local parts = anim._parts + local lengths = anim._lengths + local durations = anim._durations + + local i = 1 + while time > durations[i] do + time = time - durations[i] + i = i + 1 + end + + local part = parts[i] + local easingFunction = anim._easingFunctions[i] + local dist = easingFunction(time, 0, part:length(), durations[i], anim.s or anim.easingAmplitude, anim.easingPeriod) + + return getValueForPart(part, dist) +end + + +local function newAnimator(duration, easingFunction, startTimeOffset) + local anim = setmetatable({}, meta) + anim._duration = duration + anim._startTime = playdate.getCurrentTimeMilliseconds() + anim._startTimeOffset = startTimeOffset or 0 + anim._easingFunction = easingFunction or playdate.easingFunctions.linear + + anim.repeatCount = 0 + anim.reverses = false + anim.easingAmplitude = nil + anim.easingPeriod = nil + return anim +end + +local function newAnimatorFromNumber(duration, startValue, endValue, easingFunction, startTimeOffset) + local anim = newAnimator(duration, easingFunction, startTimeOffset) + anim._startValue = startValue + anim._endValue = endValue + anim._getValue = getValueForNumbers + return anim +end + +local function newAnimatorFromPoints(duration, startValue, endValue, easingFunction, startTimeOffset) + local anim = newAnimator(duration, easingFunction, startTimeOffset) + anim._startValue = startValue + anim._endValue = endValue + anim._getValue = getValueForNumbers + return anim +end + +local function newAnimatorFromLineSegment(duration, lineSegment, easingFunction, startTimeOffset) + local anim = newAnimator(duration, easingFunction, startTimeOffset) + anim._lineSegment = lineSegment + anim._getValue = getValueForLineSegment + return anim +end + +local function newAnimatorFromArc(duration, arc, easingFunction, startTimeOffset) + local anim = newAnimator(duration, easingFunction, startTimeOffset) + anim._arc = arc + anim._getValue = getValueForArc + return anim +end + +local function newAnimatorFromPolygon(duration, polygon, easingFunction, startTimeOffset) + local anim = newAnimator(duration, easingFunction, startTimeOffset) + anim._polygon = polygon + anim._getValue = getValueForPolygon + return anim +end + +local function calculatePartsLength(parts) + local totalLength = 0 + local lengths = {} + for i = 1, #parts do + local part = parts[i] + totalLength = totalLength + part:length() + lengths[i] = totalLength + end + return lengths, totalLength +end + +local function newAnimatorFromPartsWithDurations(durations, parts, easingFunctions, startTimeOffset) + assert(#durations == #parts) + assert(#easingFunctions == #parts) + + local totalDuration = 0 + for i = 1, #durations do + totalDuration = totalDuration + durations[i] + end + + local anim = newAnimator(totalDuration, nil, startTimeOffset) + anim._durations = durations + anim._easingFunctions = easingFunctions + anim._parts = parts + anim._getValue = getValueForPartsWithDurations + anim._lengths, anim._totalLength = calculatePartsLength(parts) + + return anim +end + +local function newAnimatorFromParts(duration, parts, easingFunction, startTimeOffset) + local anim = newAnimator(duration, easingFunction, startTimeOffset) + anim._parts = parts + anim._getValue = getValueForParts + anim._lengths, anim._totalLength = calculatePartsLength(parts) + return anim +end + +-- note: this function has 5 overloaded definitions as of 2.6.2. -- the parameters will first need to be interpreted, then passed off to an appropriate local function for processing. function module.new(a, b, c, d, e) - error("[ERR] playdate.graphics.animator.new() is not yet implemented.") + if type(b) == "table" then + if b._type then + if b._type == "point" then + return newAnimatorFromPoints(a, b, c, d, e) + elseif b._type == "lineSegment" then + return newAnimatorFromLineSegment(a, b, c, d) + elseif b._type == "arc" then + return newAnimatorFromArc(a, b, c, d) + elseif b._type == "polygon" then + return newAnimatorFromPolygon(a, b, c, d) + end + else + if type(a) == "number" then + return newAnimatorFromParts(a, b, c, d) + else + return newAnimatorFromPartsWithDurations(a, b, c, d) + end + end + else + return newAnimatorFromNumber(a, b, c, d, e) + end end function meta:currentValue() - error("[ERR] playdate.graphics.animator:currentValue() is not yet implemented.") + updateAnimator(self) + return self:_getValue(self._currentTime) end function meta:valueAtTime(time) - error("[ERR] playdate.graphics.animator:valueAtTime() is not yet implemented.") + time = normalizeTime(self, time) + return self:_getValue(time) end function meta:progress() - error("[ERR] playdate.graphics.animator:progress() is not yet implemented.") + updateAnimator(self) + + if self.repeats or self.repeatCount < 0 then + return nil + end + + if self._ended then + return 1 + end + + local dur = self._duration + if self.reverses then + dur = dur * 2 + end + + dur = dur + self.repeatCount * dur + + local t = playdate.getCurrentTimeMilliseconds() - self._startTime + + return playbit.util.clamp01(t / dur) end function meta:reset(duration) - error("[ERR] playdate.graphics.animator:reset() is not yet implemented.") + self._duration = duration or self._duration + self._startTime = playdate.getCurrentTimeMilliseconds() + self._currentTime = 0 + self._ended = false end function meta:ended() - error("[ERR] playdate.graphics.animator:ended() is not yet implemented.") + updateAnimator(self) + return self._ended end module.easingAmplitude = nil module.easingPeriod = nil -module.repeatCount = nil -module.reverses = nil \ No newline at end of file diff --git a/playdate/arc.lua b/playdate/arc.lua new file mode 100644 index 0000000..a222e36 --- /dev/null +++ b/playdate/arc.lua @@ -0,0 +1,79 @@ +require("playbit.util") + +local module = {} +playdate.geometry.arc = module + +local meta = {} +meta.__index = meta +module.__index = meta + +function module.new(x, y, radius, startAngle, endAngle, direction) + local o = {} + + o._type = "arc" + o.x = x + o.y = y + o.radius = radius + o.startAngle = startAngle + o.endAngle = endAngle + + if direction == nil then + o.clockwise = o.endAngle >= o.startAngle + else + o.clockwise = direction + end + + setmetatable(o, meta) + return o +end + +function meta:copy() + return module.new(self.x, self.y, self.radius, self.startAngle, self.endAngle, self.clockwise) +end + +function meta:length() + local angle = self.endAngle - self.startAngle + + if not self.clockwise then + angle = -angle + end + + -- this is not correct but this is how PD works, + -- confirmed by experiments + if angle < 0 then + angle = math.abs(angle + 360) + end + + return self.radius * math.rad(angle) +end + +function meta:isClockwise() + return self.clockwise +end + +function meta:setIsClockwise(flag) + self.clockwise = flag +end + +function meta:pointOnArc(distance, extend) + if not extend then + local length = self:length() + distance = playbit.util.clamp(distance, 0, length) + end + + local startAngleRad = math.rad(self.startAngle) + local deltaAngleRad = distance / self.radius + local angleRad + + if self.clockwise then + angleRad = startAngleRad + deltaAngleRad + else + angleRad = startAngleRad - deltaAngleRad + end + + -- on PD angle 0 is Up. + local x = self.x + self.radius * math.sin(angleRad) + local y = self.y - self.radius * math.cos(angleRad) + + return playdate.geometry.point.new(x, y) +end diff --git a/playdate/font.lua b/playdate/font.lua index b2f823b..2e8c45e 100644 --- a/playdate/font.lua +++ b/playdate/font.lua @@ -39,7 +39,7 @@ function module.getSystemFont(variant) end function meta:getTextWidth(str) - --[[ + --[[ NOTE: width returned will not be the same as on Playdate if a tracking value is set in the font (.fnt) https://github.com/GamesRightMeow/playbit/issues/12 @@ -79,10 +79,10 @@ function meta:drawText(str, x, y, width, height, leadingAdjustment, wrapMode, al @@ASSERT(wrapMode == nil, "[ERR] Parameter wrapMode is not yet implemented.") @@ASSERT(alignment == nil, "[ERR] Parameter alignment is not yet implemented.") local currentFont = love.graphics.getFont() + playbit.graphics.setDrawMode("image") love.graphics.setFont(self.data) love.graphics.print(str, x, y) love.graphics.setFont(currentFont) - playbit.graphics.updateContext() end -- 0=left 1=right 2=center @@ -94,22 +94,22 @@ function meta:drawTextAligned(str, x, y, alignment, leadingAdjustment) x = x - width elseif alignment == 2 then -- center - x = x - width * 0.5 + x = x - width * 0.5 end -- left, draw normally - + local currentFont = love.graphics.getFont() + playbit.graphics.setDrawMode("image") love.graphics.setFont(self.data) love.graphics.print(str, x, y) love.graphics.setFont(currentFont) - playbit.graphics.updateContext() end function meta:_drawTextInRect(text, x, y, width, height, leadingAdjustment, truncationString, textAlignment) y = y - 1 - + local lineHeight = self:getHeight() + self:getLeading() + leadingAdjustment - + if lineHeight > height then -- even one line won't fit return 0, 0, false @@ -131,7 +131,7 @@ function meta:_drawTextInRect(text, x, y, width, height, leadingAdjustment, trun -- trimm trailing space line = string.sub(line, 1, #line - 1) - if lineHeight * (lineCount + 1) > height + if lineHeight * (lineCount + 1) > height or lineHeight * (lineCount + 2) > height then -- this line or the next line surpasses specified max height line = line..truncationString @@ -169,6 +169,6 @@ function meta:_drawTextInRect(text, x, y, width, height, leadingAdjustment, trun largestLineWidth = lineWidth end end - + return largestLineWidth, (lineHeight * lineCount) - leadingAdjustment, truncated end \ No newline at end of file diff --git a/playdate/geometry.lua b/playdate/geometry.lua new file mode 100644 index 0000000..70fc703 --- /dev/null +++ b/playdate/geometry.lua @@ -0,0 +1,28 @@ +local module = {} +playdate.geometry = module + +require("playdate.affineTransform") +require("playdate.arc") +require("playdate.lineSegment") +require("playdate.point") +require("playdate.polygon") +require("playdate.rect") +require("playdate.size") +require("playdate.vector2D") + +module.kUnflipped = 0 +module.kFlippedX = 1 +module.kFlippedY = 2 +module.kFlippedXY = 3 + +function module.squaredDistanceToPoint(x1, y1, x2, y2) + local dx = x2 - x1 + local dy = y2 - y1 + return dx * dx + dy * dy +end + +function module.distanceToPoint(x1, y1, x2, y2) + local dx = x2 - x1 + local dy = y2 - y1 + return math.sqrt(dx * dx + dy * dy) +end diff --git a/playdate/graphics.lua b/playdate/graphics.lua index 4f77ea4..03d3907 100644 --- a/playdate/graphics.lua +++ b/playdate/graphics.lua @@ -24,12 +24,40 @@ module.kColorWhite = 1 module.kColorBlack = 0 -- TODO: clear and XOR support +module.kStrokeCentered = 0 +module.kStrokeInside = 1 +module.kStrokeOutside = 2 + +module.kLineCapStyleButt = 0 +module.kLineCapStyleSquare = 1 +module.kLineCapStyleRound = 2 + +module.kPolygonFillNonZero = 0 +module.kPolygonFillEvenOdd = 1 + kTextAlignment = { left = 0, right = 1, center = 2, } +local textToDrawMode = { + ["copy"] = module.kDrawModeCopy, + ["inverted"] = module.kDrawModeInverted, + ["xor"] = module.kDrawModeXOR, + ["nxor"] = module.kDrawModeNXOR, + ["whitetransparent"] = module.kDrawModeWhiteTransparent, + ["blacktransparent"] = module.kDrawModeBlackTransparent, + ["fillwhite"] = module.kDrawModeFillWhite, + ["fillblack"] = module.kDrawModeFillBlack +} + +local colorByIndex = { + [0] = { 0, 0, 0, 1 }, + [1] = { 1, 1, 1, 1 }, + [2] = { 0, 0, 0, 0 } +} + function module.setDrawOffset(x, y) playbit.graphics.drawOffset.x = x playbit.graphics.drawOffset.y = y @@ -45,35 +73,32 @@ end function module.setBackgroundColor(color) @@ASSERT(color == 1 or color == 0, "Only values of 0 (black) or 1 (white) are supported.") playbit.graphics.backgroundColorIndex = color - if color == 1 then - playbit.graphics.backgroundColor = playbit.graphics.colorWhite - else - playbit.graphics.backgroundColor = playbit.graphics.colorBlack - end + playbit.graphics.backgroundColor = colorByIndex[color] -- don't actually set love's bg color here since doing so immediately sets the color, and this is not consistent with PD end +function module.getBackgroundColor(color) + return playbit.graphics.backgroundColorIndex +end + function module.setColor(color) @@ASSERT(color == 1 or color == 0, "Only values of 0 (black) or 1 (white) are supported.") playbit.graphics.drawColorIndex = color - -- when drawing without a pattern, we must flip the pattern mask for white/black because of the way the shader draws patterns - if color == 1 then - local c = playbit.graphics.colorWhite - playbit.graphics.drawColor = c - -- reset pattern, as per PD behavior - module.setPattern({0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}) - love.graphics.setColor(c[1], c[2], c[3], c[4]) - else - local c = playbit.graphics.colorBlack - playbit.graphics.drawColor = c - -- reset pattern, as per PD behavior - module.setPattern({0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}) - love.graphics.setColor(c[1], c[2], c[3], c[4]) - end + local c = colorByIndex[color] + playbit.graphics.drawColor = c + playbit.graphics.shaders.color:send("drawColor", c) + -- color and pattern modes are mutually exclusive + playbit.graphics.drawPattern = nil + playbit.graphics.drawMode = nil +end + +function module.getColor() + return playbit.graphics.drawColorIndex end function module.setPattern(pattern) playbit.graphics.drawPattern = pattern + playbit.graphics.drawMode = nil -- bitshifting does not work in shaders, so do it here in Lua local pixels = {} @@ -87,8 +112,12 @@ function module.setPattern(pattern) end end end - - playbit.graphics.shader:send("pattern", unpack(pixels)) + + playbit.graphics.shaders.pattern:send("pattern", unpack(pixels)) +end + +function module.setDitherPattern(alpha, ditherType) + error("[ERR] playdate.graphics.setDitherPattern() is not yet implemented.") end function module.clear(color) @@ -98,136 +127,268 @@ function module.clear(color) playbit.graphics.lastClearColor = c else @@ASSERT(color == 1 or color == 0, "Only values of 0 (black) or 1 (white) are supported.") - if color == 1 then - local c = playbit.graphics.colorWhite - love.graphics.clear(c[1], c[2], c[3], c[4]) - playbit.graphics.lastClearColor = c - else - local c = playbit.graphics.colorBlack - love.graphics.clear(c[1], c[2], c[3], c[4]) - playbit.graphics.lastClearColor = c - end + local c = colorByIndex[color] + love.graphics.clear(c[1], c[2], c[3], c[4]) + playbit.graphics.lastClearColor = c end - playbit.graphics.updateContext() end -- "copy", "inverted", "XOR", "NXOR", "whiteTransparent", "blackTransparent", "fillWhite", or "fillBlack". function module.setImageDrawMode(mode) - playbit.graphics.drawMode = mode - if mode == module.kDrawModeCopy or mode == "copy" then - playbit.graphics.shader:send("mode", 0) - elseif mode == module.kDrawModeFillWhite or mode == "fillWhite" then - playbit.graphics.shader:send("mode", 1) - elseif mode == module.kDrawModeFillBlack or mode == "fillBlack" then - playbit.graphics.shader:send("mode", 2) - elseif mode == module.kDrawModeInverted or mode == "inverted" then - playbit.graphics.shader:send("mode", 6) - elseif mode == module.kDrawModeWhiteTransparent or mode == "whiteTransparent" then - playbit.graphics.shader:send("mode", 4) - else - error("[ERR] Draw mode '"..mode.."' is not yet implemented.") + if type(mode) == "string" then + mode = textToDrawMode[string.lower(mode)] end + + playbit.graphics.imageDrawMode = mode + playbit.graphics.drawMode = nil +end + +function module.getImageDrawMode() + return playbit.graphics.imageDrawMode end function module.drawCircleAtPoint(x, y, radius) - playbit.graphics.shader:send("mode", 8) + playbit.graphics.setDrawMode("line") - love.graphics.circle("line", x, y, radius) - playbit.graphics.updateContext() + if type(x) ~= "number" then + local pt = x + radius = y + x, y = pt.x, pt.y + end - module.setImageDrawMode(playbit.graphics.drawMode) + love.graphics.circle("line", x, y, radius) end function module.fillCircleAtPoint(x, y, radius) - playbit.graphics.shader:send("mode", 8) + playbit.graphics.setDrawMode("fill") + + if type(x) ~= "number" then + local pt = x + radius = y + x, y = pt.x, pt.y + end love.graphics.circle("fill", x, y, radius) - playbit.graphics.updateContext() +end - module.setImageDrawMode(playbit.graphics.drawMode) +function module.drawEllipseInRect(x, y, width, height, startAngle, endAngle) + error("[ERR] playdate.graphics.drawEllipseInRect() is not yet implemented.") +end + +function module.fillEllipseInRect(x, y, width, height, startAngle, endAngle) + error("[ERR] playdate.graphics.fillEllipseInRect() is not yet implemented.") +end + +function module.drawPolygon(x1, y1, x2, y2, ...) + error("[ERR] playdate.graphics.drawPolygon() is not yet implemented.") +end + +function module.fillPolygon(x1, y1, x2, y2, ...) + error("[ERR] playdate.graphics.fillPolygon() is not yet implemented.") +end + +function module.setPolygonFillRule(rule) + error("[ERR] playdate.graphics.setPolygonFillRule() is not yet implemented.") +end + +function module.drawTriangle(x1, y1, x2, y2, x3, y3) + error("[ERR] playdate.graphics.drawTriangle() is not yet implemented.") +end + +function module.fillTriangle(x1, y1, x2, y2, x3, y3) + error("[ERR] playdate.graphics.fillTriangle() is not yet implemented.") end function module.setLineWidth(width) + -- PD examples use line width 0 but love2d does not support it. + if width < 1 then width = 1 end + playbit.graphics.lineWidth = width love.graphics.setLineWidth(width) end +function module.getLineWidth() + return playbit.graphics.lineWidth +end + +function module.setLineCapStyle(style) + error("[ERR] playdate.graphics.setLineCapStyle() is not yet implemented.") +end + function module.drawRect(x, y, width, height) - playbit.graphics.shader:send("mode", 8) + playbit.graphics.setDrawMode("line") - love.graphics.rectangle("line", x, y, width, height) - playbit.graphics.updateContext() + if type(x) ~= "number" then + local r = x + x, y, width, height = r:unpack() + end - module.setImageDrawMode(playbit.graphics.drawMode) + love.graphics.rectangle("line", x, y, width, height) end function module.fillRect(x, y, width, height) - playbit.graphics.shader:send("mode", 8) + playbit.graphics.setDrawMode("fill") - love.graphics.rectangle("fill", x, y, width, height) - playbit.graphics.updateContext() + if type(x) ~= "number" then + local r = x + x, y, width, height = r:unpack() + end - module.setImageDrawMode(playbit.graphics.drawMode) + love.graphics.rectangle("fill", x, y, width, height) end function module.drawRoundRect(x, y, width, height, radius) -- TODO: love's rectangle function doesn't draw the same way as Playdate's - -- playbit.graphics.shader:send("mode", 8) + -- playbit.graphics.setDrawMode("line") -- love.graphics.rectangle("line", x, y, width, height, radius, radius, 0) - -- playbit.graphics.updateContext() - - -- module.setImageDrawMode(playbit.graphics.drawMode) error("[ERR] playdate.graphics.drawRoundRect() is not yet implemented.") end function module.fillRoundRect(x, y, width, height, radius) -- TODO: love's rectangle function doesn't draw the same way as Playdate's - -- playbit.graphics.shader:send("mode", 8) + -- playbit.graphics.setDrawMode("fill") -- love.graphics.rectangle("fill", x, y, width, height, radius, radius, 0) - -- playbit.graphics.updateContext() - - -- module.setImageDrawMode(playbit.graphics.drawMode) error("[ERR] playdate.graphics.fillRoundRect() is not yet implemented.") end function module.drawLine(x1, y1, x2, y2) - playbit.graphics.shader:send("mode", 8) + playbit.graphics.setDrawMode("line") + + if type(x1) ~= "number" then + local ls = x1 + x1, y1, x2, y2 = ls:unpack() + end love.graphics.line(x1, y1, x2, y2) - playbit.graphics.updateContext() +end + +function module.drawPolygon(x1, y1, x2, y2, ...) + playbit.graphics.setDrawMode("line") - module.setImageDrawMode(playbit.graphics.drawMode) + if type(x1) ~= "number" then + local poly = x1 + if poly:isClosed() then + love.graphics.polygon("line", unpack(poly._points)) + else + love.graphics.line(unpack(poly._points)) + end + else + love.graphics.polygon("line", x1, y1, x2, y2, ...) + end end function module.drawArc(x, y, radius, startAngle, endAngle) - playbit.graphics.shader:send("mode", 8) + + local function normalizeAngle(deg) + return (deg % 360 + 360) % 360 + end + + if type(x) ~= "number" then + local arc = x + x, y, radius, startAngle, endAngle = arc.x, arc.y, arc.radius, arc.startAngle, arc.endAngle + end + + -- Bring angles to interval [0, 360) + startAngle = normalizeAngle(startAngle) + endAngle = normalizeAngle(endAngle) + + -- PD always draws from startAngle to endAngle clockwise. + if startAngle >= endAngle then + endAngle = endAngle + 360 + end -- 0 degrees is 270 when drawing an arc on PD... startAngle = startAngle - 90 endAngle = endAngle - 90 - if startAngle == endAngle then - -- if startAngle and endAngle are the same, PD draws a full circle - love.graphics.arc("line", "open", x, y, radius, math.rad(startAngle), math.rad(endAngle + 360), 16) - elseif startAngle > endAngle then - -- love2d adjusts for when the startAngle is larger, but PD does not, so we need to compensate - love.graphics.arc("line", "open", x, y, radius, math.rad(startAngle), math.rad(endAngle + 360), 16) - else - love.graphics.arc("line", "open", x, y, radius, math.rad(endAngle), math.rad(startAngle), 16) - end - playbit.graphics.updateContext() + playbit.graphics.setDrawMode("line") - module.setImageDrawMode(playbit.graphics.drawMode) + love.graphics.arc("line", "open", x, y, radius, math.rad(startAngle), math.rad(endAngle), 32) end function module.drawPixel(x, y) - playbit.graphics.shader:send("mode", 8) + playbit.graphics.setDrawMode("line") love.graphics.points(x, y) - playbit.graphics.updateContext() +end + +function module.perlin(x, y, z, rep, octaves, persistence) + error("[ERR] playdate.graphics.perlin() is not yet implemented.") +end + +function module.perlinArray(count, x, dx, y, dy, z, dz, rep, octaves, persistence) + error("[ERR] playdate.graphics.perlinArray() is not yet implemented.") +end + +function module.generateQRCode(stringToEncode, desiredEdgeDimension, callback) + error("[ERR] playdate.graphics.generateQRCode() is not yet implemented.") +end + +function module.drawSineWave(startX, startY, endX, endY, startAmplitude, endAmplitude, period, phaseShift) + error("[ERR] playdate.graphics.drawSineWave() is not yet implemented.") +end + +function module.setClipRect(x, y, width, height) + error("[ERR] playdate.graphics.setClipRect() is not yet implemented.") +end + +function module.getClipRect() + error("[ERR] playdate.graphics.getClipRect() is not yet implemented.") +end + +function module.setScreenClipRect(x, y, width, height) + error("[ERR] playdate.graphics.setScreenClipRect() is not yet implemented.") +end + +function module.getScreenClipRect() + error("[ERR] playdate.graphics.getScreenClipRect() is not yet implemented.") +end + +function module.clearClipRect() + error("[ERR] playdate.graphics.clearClipRect() is not yet implemented.") +end + +function module.setStencilImage(image, tile) + error("[ERR] playdate.graphics.setStencilImage() is not yet implemented.") +end + +-- setStencilPattern(pattern) +-- setStencilPattern(level, [ditherType]) +function module.setStencilPattern(row1, row2, row3, row4, row5, row6, row7, row8) + error("[ERR] playdate.graphics.setStencilPattern() is not yet implemented.") +end - module.setImageDrawMode(playbit.graphics.drawMode) +function module.clearStencil() + error("[ERR] playdate.graphics.clearStencil() is not yet implemented.") +end + +function module.clearStencilImage() + error("[ERR] playdate.graphics.clearStencilImage() is not yet implemented.") +end + +function module.setStrokeLocation(location) + error("[ERR] playdate.graphics.setStrokeLocation() is not yet implemented.") +end + +function module.getStrokeLocation() + error("[ERR] playdate.graphics.getStrokeLocation() is not yet implemented.") +end + +function module.lockFocus(image) + error("[ERR] playdate.graphics.lockFocus() is not yet implemented.") +end + +function module.unlockFocus() + error("[ERR] playdate.graphics.unlockFocus() is not yet implemented.") +end + +function module.getDisplayImage() + error("[ERR] playdate.graphics.getDisplayImage() is not yet implemented.") +end + +function module.getWorkingImage() + error("[ERR] playdate.graphics.getWorkingImage() is not yet implemented.") end function module.setFont(font) @@ -239,6 +400,22 @@ function module.getFont() return playbit.graphics.activeFont end +function module.setFontFamily(fontFamily) + error("[ERR] playdate.graphics.setFontFamily() is not yet implemented.") +end + +function module.setFontTracking(pixels) + error("[ERR] playdate.graphics.setFontTracking() is not yet implemented.") +end + +function module.getFontTracking() + error("[ERR] playdate.graphics.getFontTracking() is not yet implemented.") +end + +function module.getSystemFont(variant) + error("[ERR] playdate.graphics.getSystemFont() is not yet implemented.") +end + function module.getTextSize(str, fontFamily, leadingAdjustment) @@ASSERT(fontFamily == nil, "[ERR] Parameter fontFamily is not yet implemented.") @@ASSERT(leadingAdjustment == nil, "[ERR] Parameter leadingAdjustment is not yet implemented.") @@ -247,7 +424,7 @@ function module.getTextSize(str, fontFamily, leadingAdjustment) return font:getWidth(str), font:getHeight() end --- playdate.graphics.drawTextInRect(str, x, y, width, height, [leadingAdjustment, [truncationString, [alignment, [font]]]]) +-- playdate.graphics.drawTextInRect(str, x, y, width, height, [leadingAdjustment, [truncationString, [alignment, [font]]]]) function module.drawTextInRect(text, x, ...) local y, width, height, leadingAdjustment, truncationString, textAlignment, font if type(x) == "number" then @@ -272,7 +449,6 @@ function module.drawText(text, x, y, width, height, fontFamily, leadingAdjustmen @@ASSERT(text ~= nil, "Text is nil") local font = playbit.graphics.activeFont font:drawText(text, x, y, fontFamily, leadingAdjustment) - playbit.graphics.updateContext() end -- TODO: handle the overloaded signature (key, rect, language, leadingAdjustment) @@ -310,31 +486,53 @@ function module.checkAlphaCollision(image1, x1, y1, flip1, image2, x2, y2, flip2 end function module.pushContext(image) - -- TODO: PD docs say image is optional, but not passing an image just results in drawing to last context? - @@ASSERT(image, "Missing image parameter.") + local context = { + drawOffset = playbit.graphics.drawOffset, + drawColorIndex = playbit.graphics.drawColorIndex, + drawColor = playbit.graphics.drawColor, + backgroundColorIndex = playbit.graphics.backgroundColorIndex, + backgroundColor = playbit.graphics.backgroundColor, + activeFont = playbit.graphics.activeFont, + imageDrawMode = playbit.graphics.imageDrawMode, + drawMode = playbit.graphics.drawMode, + canvas = playbit.graphics.canvas, + drawPattern = playbit.graphics.drawPattern, + lineWidth = playbit.graphics.lineWidth + } - -- create canvas if it doesn't exist - if not image._canvas then - image._canvas = love.graphics.newCanvas(image:getSize()) - end - -- push context - table.insert(playbit.graphics.contextStack, image) + table.insert(playbit.graphics.contextStack, context) - -- update current render target - love.graphics.setCanvas(image._canvas) + if image then + -- create canvas if it doesn't exist + if not image._canvas then + image._canvas = love.graphics.newCanvas(image:getSize()) + end + + -- update current render target + module.canvas = image._canvas + love.graphics.setCanvas(image._canvas) + end end function module.popContext() @@ASSERT(#playbit.graphics.contextStack > 0, "No pushed context.") -- pop context - table.remove(playbit.graphics.contextStack) - -- update current render target - if #playbit.graphics.contextStack == 0 then - love.graphics.setCanvas(playbit.graphics.canvas) - else - local activeContext = playbit.graphics.contextStack[#playbit.graphics.contextStack] - love.graphics.setCanvas(activeContext._canvas) + local context = table.remove(playbit.graphics.contextStack) + + -- restore canvas + playbit.graphics.canvas = context.canvas + love.graphics.setCanvas(context.canvas) + + module.setImageDrawMode(context.imageDrawMode) + module.setDrawOffset(context.drawOffset.x, context.drawOffset.y) + module.setBackgroundColor(context.backgroundColorIndex) + module.setColor(context.drawColorIndex) + module.setFont(context.activeFont) + module.setLineWidth(context.lineWidth) + + if context.drawPattern then + module.setPattern(context.drawPattern) end -end \ No newline at end of file +end diff --git a/playdate/image.lua b/playdate/image.lua index c20d64f..2165298 100644 --- a/playdate/image.lua +++ b/playdate/image.lua @@ -14,10 +14,16 @@ function module.new(widthOrPath, height, bgcolor) if height then -- creating empty image with dimensions local imageData = love.image.newImageData(widthOrPath, height) - img.data = love.graphics.newImage(imageData) + img.data = love.graphics.newImage(imageData) else -- creating image from file - img.data = love.graphics.newImage(widthOrPath..".png") + if love.filesystem.getInfo(widthOrPath..".png") then + img.data = love.graphics.newImage(widthOrPath..".png") + elseif love.filesystem.getInfo(widthOrPath) then + img.data = love.graphics.newImage(widthOrPath) + else + return nil + end end return img @@ -28,11 +34,22 @@ function meta:load(path) end function meta:copy() - error("[ERR] playdate.graphics.image:copy() is not yet implemented.") + local img = setmetatable({}, meta) + img.data = self.data + img.sx = self.sx + img.sy = self.sy + return img end function meta:getSize() - return self.data:getWidth(), self.data:getHeight() + local w, h = self.data:getWidth(), self.data:getHeight() + + if self.sx then + w = math.floor(w * self.sx) + h = math.floor(h * self.sy) + end + + return w, h end function module.imageSizeAtPath(path) @@ -43,15 +60,10 @@ end -- (x, y, flip, sourceRect) -- (p, flip, sourceRect) function meta:draw(x, y, flip, qx, qy, qw, qh) - -- always render pure white so its not tinted - local r, g, b = love.graphics.getColor() - love.graphics.setColor(1, 1, 1, 1) - local sx = 1 local sy = 1 if flip then - local w = self.data:getWidth() - local h = self.data:getHeight() + local w, h = self:getSize() if flip == playdate.graphics.kImageFlippedX then sx = -1 x = x + w @@ -65,17 +77,20 @@ function meta:draw(x, y, flip, qx, qy, qw, qh) y = y + h end end - + + playbit.graphics.setDrawMode("image") + if qx and qy and qw and qh then local w, h = self:getSize() playbit.graphics.quad:setViewport(qx, qy, qw, qh, w, h) - love.graphics.draw(self.data, playbit.graphics.quad, x, y, sx, sy) + love.graphics.draw(self.data, playbit.graphics.quad, x, y, 0, sx, sy) else + if self.sx then + sx = sx * self.sx + sy = sy * self.sy + end love.graphics.draw(self.data, x, y, 0, sx, sy) end - - love.graphics.setColor(r, g, b, 1) - playbit.graphics.updateContext() end function meta:drawAnchored(x, y, ax, ay, flip) @@ -98,12 +113,7 @@ function meta:drawRotated(x, y, angle, scale, yscale) @@ASSERT(scale == nil, "[ERR] Parameter scale is not yet implemented.") @@ASSERT(yscale == nil, "[ERR] Parameter yscale is not yet implemented.") - -- always render pure white so its not tinted - local r, g, b = love.graphics.getColor() - love.graphics.setColor(1, 1, 1, 1) - -- playdate.image.drawRotated() draws the texture centered, so emulate that - love.graphics.push() local w = self.data:getWidth() * 0.5 local h = self.data:getHeight() * 0.5 @@ -111,13 +121,12 @@ function meta:drawRotated(x, y, angle, scale, yscale) w = math.floor(w) h = math.floor(h) - love.graphics.translate(x, y) - love.graphics.rotate(math.rad(angle)) - love.graphics.draw(self.data, -w, -h) - love.graphics.pop() + local sx = self.sx or 1 + local sy = self.sy or 1 - love.graphics.setColor(r, g, b, 1) - playbit.graphics.updateContext() + playbit.graphics.setDrawMode("image") + + love.graphics.draw(self.data, x, y, math.rad(angle), sx, sy, w, h) end function meta:rotatedImage(angle, scale, yscale) @@ -127,22 +136,27 @@ end function meta:drawScaled(x, y, scale, yscale) yscale = yscale or scale - -- always render pure white so its not tinted - local r, g, b = love.graphics.getColor() - love.graphics.setColor(1, 1, 1, 1) + local sx = self.sx or 1 + local sy = self.sy or 1 + + sx = sx * scale + sy = sy * (yscale or scale) - love.graphics.push() - love.graphics.translate(x, y) - love.graphics.scale(scale, yscale) - love.graphics.draw(self.data, 0, 0) - love.graphics.pop() + playbit.graphics.setDrawMode("image") - love.graphics.setColor(r, g, b, 1) - playbit.graphics.updateContext() + love.graphics.draw(self.data, x, y, 0, sx, sy) end function meta:scaledImage(scale, yscale) - error("[ERR] playdate.graphics.image:scaledImage() is not yet implemented.") + local img = self:copy() + + local sx = img.sx or 1 + local sy = img.sy or 1 + + img.sx = sx * scale + img.sy = sy * (yscale or scale) + + return img end diff --git a/playdate/lineSegment.lua b/playdate/lineSegment.lua new file mode 100644 index 0000000..ce91f63 --- /dev/null +++ b/playdate/lineSegment.lua @@ -0,0 +1,105 @@ +require("playbit.util") + +local module = {} +playdate.geometry.lineSegment = module + +local meta = {} +meta.__index = meta +module.__index = meta + +function module.new(x1, y1, x2, y2) + local o = {} + + o._type = "lineSegment" + o.x1 = x1 + o.y1 = y1 + o.x2 = x2 + o.y2 = y2 + + setmetatable(o, meta) + return o +end + +function module.fast_intersection(x1, y1, x2, y2, x3, y3, x4, y4) + error("[ERR] playdate.geometry.lineSegment.fast_intersection() is not yet implemented.") +end + +function meta:copy() + return module.new(self.x1, self.y1, self.x2, self.y2) +end + +function meta:unpack() + return self.x1, self.y1, self.x2, self.y2 +end + +function meta:length() + local dx = self.x2 - self.x1 + local dy = self.y2 - self.y1 + return math.sqrt(dx * dx + dy * dy) +end + +function meta:offset(dx, dy) + self.x1 = self.x1 + dx + self.x2 = self.x2 + dx + self.y1 = self.y1 + dy + self.y2 = self.y2 + dy +end + +function meta:offsetBy(dx, dy) + return module.new(self.x1 + dx, self.y1 + dy, self.x2 + dx, self.y2 + dy) +end + +function meta:midPoint() + return playdate.geometry.point.new((self.x1 + self.x2) / 2, (self.y1 + self.y2) / 2) +end + +function meta:pointOnLine(distance, extend) + local len = self:length() + + if not extend then + distance = playbit.util.clamp(distance, 0, len) + end + + local d = distance / len + local x = self.x1 + (self.x2 - self.x1) * d + local y = self.y1 + (self.y2 - self.y1) * d + + return playdate.geometry.point.new(x, y) +end + +-- TODO: Check what PD does here. +function meta:segmentVector() + return playdate.geometry.vector2D.new(self.x2 - self.x1, self.y2 - self.y1) +end + +function meta:closestPointOnLineToPoint(p, extend) + local vx = p.x - self.x1 + local vy = p.y - self.y1 + + local dx = self.x2 - self.x1 + local dy = self.y2 - self.y1 + + local d = (vx * dx + vy * dy) / (dx * dx + dy * dy) + + if not extend or extend ~= true then + if d < 0 then + d = 0 + elseif d > 1 then + d = 1 + end + end + + return playdate.geometry.point.new(self.x1 + d * dx, self.y1 + d * dy) +end + +function meta:intersectsLineSegment(ls) + error("[ERR] playdate.geometry.lineSegment:intersectsLineSegment() is not yet implemented.") +end + +function meta:intersectsPolygon(poly) + error("[ERR] playdate.geometry.lineSegment:intersectsPolygon() is not yet implemented.") +end + +function meta:intersectsRect(rect) + error("[ERR] playdate.geometry.lineSegment:intersectsRect() is not yet implemented.") +end diff --git a/playdate/math.lua b/playdate/math.lua new file mode 100644 index 0000000..1787a2b --- /dev/null +++ b/playdate/math.lua @@ -0,0 +1,6 @@ +local module = {} +playdate.math = module + +function module.lerp(min, max, t) + return min + t * (max - min) +end \ No newline at end of file diff --git a/playdate/playdate.lua b/playdate/playdate.lua index a6dbce6..ba5080d 100644 --- a/playdate/playdate.lua +++ b/playdate/playdate.lua @@ -3,19 +3,23 @@ playdate = module require("playbit.geometry") require("playdate.metadata") +require("playdate.math") require("playdate.sound") require("playdate.file") require("playdate.datastore") require("playdate.accelerometer") require("playdate.json") +require("playdate.geometry") -- ████████╗██╗███╗ ███╗███████╗ -- ╚══██╔══╝██║████╗ ████║██╔════╝ --- ██║ ██║██╔████╔██║█████╗ --- ██║ ██║██║╚██╔╝██║██╔══╝ +-- ██║ ██║██╔████╔██║█████╗ +-- ██║ ██║██║╚██╔╝██║██╔══╝ -- ██║ ██║██║ ╚═╝ ██║███████╗ -- ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ - + +local startTime = love.timer.getTime() + function module.getTime() local seconds = os.time() local date = os.date("*t", seconds) @@ -32,10 +36,78 @@ function module.getTime() } end +function module.wait(milliseconds) + love.timer.sleep(milliseconds / 1000) +end + +function module.stop() + error("[ERR] playdate.stop() is not yet implemented.") +end + +function module.start() + error("[ERR] playdate.start() is not yet implemented.") +end + +function module.restart(arg) + error("[ERR] playdate.restart() is not yet implemented.") +end + +function module.restart() + error("[ERR] playdate.restart() is not yet implemented.") +end + +function module.getSystemMenu() + error("[ERR] playdate.getSystemMenu() is not yet implemented.") +end + +function module.setMenuImage(image, xOffset) + error("[ERR] playdate.setMenuImage() is not yet implemented.") +end + +function module.getSystemLanguage() + error("[ERR] playdate.getSystemLanguage() is not yet implemented.") +end + +function module.getReduceFlashing() + error("[ERR] playdate.getReduceFlashing() is not yet implemented.") +end + +function module.getFlipped() + error("[ERR] playdate.getFlipped() is not yet implemented.") +end + +function module.startAccelerometer() + error("[ERR] playdate.startAccelerometer() is not yet implemented.") +end + +function module.stopAccelerometer() + error("[ERR] playdate.stopAccelerometer() is not yet implemented.") +end + +function module.readAccelerometer() + error("[ERR] playdate.readAccelerometer() is not yet implemented.") +end + +function module.accelerometerIsRunning() + error("[ERR] playdate.accelerometerIsRunning() is not yet implemented.") +end + +function module.setAutoLockDisabled(disable) + error("[ERR] playdate.setAutoLockDisabled() is not yet implemented.") +end + function module.getCurrentTimeMilliseconds() return love.timer.getTime() * 1000 end +function module.resetElapsedTime() + startTime = love.timer.getTime() +end + +function module.getElapsedTime() + return love.timer.getTime() - startTime +end + function module.getSecondsSinceEpoch() -- os.time() without params always returns in system local time, so we must convert to UTC local nowLocal = os.time() @@ -55,13 +127,45 @@ function module.getSecondsSinceEpoch() return os.difftime(nowUtc, playdateEpochUtc), milliseconds end +function module.getTime() + error("[ERR] playdate.getTime() is not yet implemented.") +end + +function module.getGMTTime() + error("[ERR] playdate.getGMTTime() is not yet implemented.") +end + +function module.epochFromTime(time) + error("[ERR] playdate.epochFromTime() is not yet implemented.") +end + +function module.epochFromGMTTime(time) + error("[ERR] playdate.epochFromTime() is not yet implemented.") +end + +function module.timeFromEpoch(seconds, milliseconds) + error("[ERR] playdate.timeFromEpoch() is not yet implemented.") +end + +function module.GMTTimeFromEpoch(seconds, milliseconds) + error("[ERR] playdate.GMTTimeFromEpoch() is not yet implemented.") +end + +function module.getServerTime(callback) + error("[ERR] playdate.getServerTime() is not yet implemented.") +end + +function module.shouldDisplay24HourTime() + error("[ERR] playdate.shouldDisplay24HourTime() is not yet implemented.") +end + -- ██╗███╗ ██╗██████╗ ██╗ ██╗████████╗ -- ██║████╗ ██║██╔══██╗██║ ██║╚══██╔══╝ --- ██║██╔██╗ ██║██████╔╝██║ ██║ ██║ --- ██║██║╚██╗██║██╔═══╝ ██║ ██║ ██║ --- ██║██║ ╚████║██║ ╚██████╔╝ ██║ --- ╚═╝╚═╝ ╚═══╝╚═╝ ╚═════╝ ╚═╝ - +-- ██║██╔██╗ ██║██████╔╝██║ ██║ ██║ +-- ██║██║╚██╗██║██╔═══╝ ██║ ██║ ██║ +-- ██║██║ ╚████║██║ ╚██████╔╝ ██║ +-- ╚═╝╚═╝ ╚═══╝╚═╝ ╚═════╝ ╚═╝ + local lastActiveJoystick = nil local isCrankDocked = false local crankPos = 0 @@ -91,7 +195,7 @@ local JUST_RELEASED = 3 local inputStates = {} function module.buttonIsPressed(button) - local key = module._buttonToKey[button] + local key = module._buttonToKey[string.lower(button)] if not inputStates[key] then -- no entry, assume no input return false @@ -101,7 +205,7 @@ function module.buttonIsPressed(button) end function module.buttonJustPressed(button) - local key = module._buttonToKey[button] + local key = module._buttonToKey[string.lower(button)] if not inputStates[key] then -- no entry, assume no input return false @@ -111,7 +215,7 @@ function module.buttonJustPressed(button) end function module.buttonJustReleased(button) - local key = module._buttonToKey[button] + local key = module._buttonToKey[string.lower(button)] if not inputStates[key] then -- no entry, assume no input return false @@ -121,11 +225,15 @@ function module.buttonJustReleased(button) end function module.getButtonState(button) - local key = module._buttonToKey[button] + local key = module._buttonToKey[string.lower(button)] local value = inputStates[key] return value == PRESSED, value == PRESSED, value == JUST_RELEASED end +function module.setButtonQueueSize(size) + error("[ERR] playdate.setButtonQueueSize() is not yet implemented.") +end + function module.isCrankDocked() if not lastActiveJoystick then return isCrankDocked @@ -147,7 +255,7 @@ end function module.getCrankChange() local change = playbit.geometry.angleDiff(lastCrankPos, crankPos) -- TODO: how does the playdate accelerate this? - local acceleratedChange = change + local acceleratedChange = change return change, acceleratedChange end @@ -173,6 +281,14 @@ function module.getCrankPosition() return degrees end +function module.getCrankTicks(ticksPerRevolution) + error("[ERR] playdate.getCrankTicks() is not yet implemented.") +end + +function module.setCrankSoundsDisabled(disable) + error("[ERR] playdate.getCrankTicks() is not yet implemented.") +end + function love.joystickadded(joystick) -- always take most recently added joystick as active joystick lastActiveJoystick = joystick @@ -218,7 +334,7 @@ function love.wheelmoved(x, y) -- TODO: emulate PD crank acceleration? -- TODO: configure scroll sensitivity? crankPos = crankPos + -y * 6 - + if crankPos < 0 then crankPos = 359 elseif crankPos > 359 then @@ -226,6 +342,88 @@ function love.wheelmoved(x, y) end end +-- playdate itself is the default input handler +-- https://sdk.play.date/3.0.2/Inside%20Playdate.html#buttonCallbacks +local inputHandlers = { { handler = playdate } } + +module.inputHandlers = { } + +function module.inputHandlers.push(handler, masksPreviousHandlers) + local entry = { handler = handler, masksPreviousHandlers = masksPreviousHandlers } + table.insert(inputHandlers, entry) +end + +function module.inputHandlers.pop() + table.remove(inputHandlers) +end + +local inputHandlersEvents = { + [JUST_PRESSED] = { + up = "upButtonDown", + down = "downButtonDown", + left = "leftButtonDown", + right = "rightButtonDown", + a = "AButtonDown", + b = "BButtonDown", + }, + [JUST_RELEASED] = { + up = "upButtonUp", + down = "downButtonUp", + left = "leftButtonUp", + right = "rightButtonUp", + a = "AButtonUp", + b = "BButtonUp", + }, + [PRESSED] = { + a = "AButtonHeld", + b = "BButtonHeld", + } +} + +local function postInputHandlersEvent(evt) + for i = #inputHandlers, 1, -1 do + local entry = inputHandlers[i] + local func = entry.handler[evt] + if func then + func() + return + elseif entry.masksPreviousHandlers then + return + end + end +end + +local function postInputHandlersCrankedEvent(change, acceleratedChange) + for i = #inputHandlers, 1, -1 do + local entry = inputHandlers[i] + local cranked = entry.handler.cranked + if cranked then + cranked(change, acceleratedChange) + end + if entry.masksPreviousHandlers == true then + break; + end + end +end + +local function updateInputHandlers() + for k,v in pairs(module._buttonToKey) do + local state = inputStates[v] + local events = inputHandlersEvents[state] + if events then + local buttonEvent = events[k] + if buttonEvent then + postInputHandlersEvent(buttonEvent) + end + end + end + + if lastCrankPos ~= crankPos then + local change, acceleratedChange = module.getCrankChange() + postInputHandlersCrankedEvent(change, acceleratedChange) + end +end + -- emulate the keys that PD simulator supports -- https://sdk.play.date/Inside%20Playdate.html#c-keyPressed local supportedCallbackKeys = { @@ -289,7 +487,7 @@ function love.keypressed(key) end end -function love.keyreleased(key) +function love.keyreleased(key) inputStates["kb_"..key] = JUST_RELEASED if playbit.keyReleased then @@ -304,6 +502,9 @@ function love.keyreleased(key) end function module.updateInput() + -- update input handlers before advancing JUST_PRESSED and JUST_RELEASED states + updateInputHandlers(); + -- only update keys that are mapped for k,v in pairs(module._buttonToKey) do if inputStates[v] == JUST_PRESSED then @@ -315,13 +516,25 @@ function module.updateInput() lastCrankPos = crankPos end --- ██╗ ██╗ ██╗ █████╗ +function module.setNewlinePrinted(flag) + error("[ERR] playdate.setNewlinePrinted() is not yet implemented.") +end + +function module.getFPS() + return love.timer.getFPS() +end + +function module.drawFPS(x, y) + -- not implemented yet, but do not produce errors +end + +-- ██╗ ██╗ ██╗ █████╗ -- ██║ ██║ ██║██╔══██╗ -- ██║ ██║ ██║███████║ -- ██║ ██║ ██║██╔══██║ -- ███████╗╚██████╔╝██║ ██║ -- ╚══════╝ ╚═════╝ ╚═╝ ╚═╝ - + function table.indexOfElement(table, element) for i = 1, #table do if table[i] == element then @@ -340,6 +553,14 @@ function sample() error("[ERR] sample() is not yet implemented.") end +function module.getStats() + error("[ERR] playdate.getStats() is not yet implemented.") +end + +function module.setStatsInterval(seconds) + error("[ERR] playdate.setStatsInterval() is not yet implemented.") +end + function where() error("[ERR] where() is not yet implemented.") end @@ -347,4 +568,36 @@ end function module.apiVersion() -- TODO: return Playbit version instead? error("[ERR] playdate.apiVersion() is not yet implemented.") -end \ No newline at end of file +end + +function module.getPowerStatus() + error("[ERR] playdate.getPowerStatus() is not yet implemented.") +end + +function module.getBatteryPercentage() + error("[ERR] playdate.getBatteryPercentage() is not yet implemented.") +end + +function module.getBatteryVoltage() + error("[ERR] playdate.getBatteryVoltage() is not yet implemented.") +end + +function module.clearConsole() + error("[ERR] playdate.clearConsole() is not yet implemented.") +end + +function module.setDebugDrawColor(r, g, b, a) + error("[ERR] playdate.setDebugDrawColor() is not yet implemented.") +end + +function module.setCollectsGarbage(flag) + error("[ERR] playdate.setCollectsGarbage() is not yet implemented.") +end + +function module.setMinimumGCTime(ms) + error("[ERR] playdate.setMinimumGCTime() is not yet implemented.") +end + +function module.setGCScaling(min, max) + error("[ERR] playdate.setGCScaling() is not yet implemented.") +end diff --git a/playdate/point.lua b/playdate/point.lua new file mode 100644 index 0000000..9b058f0 --- /dev/null +++ b/playdate/point.lua @@ -0,0 +1,74 @@ +local module = {} +playdate.geometry.point = module + +local meta = {} + +meta.__index = meta + +meta.__add = function(a, b) + return module.new(a.x + b.dx, a.y + b.dy) +end + +meta.__sub = function(a, b) + if b._type then + if b._type == "point" then + return playdate.geometry.vector2D.new(a.x - b.x, a.y - b.y) + elseif b._type == "vector2D" then + return playdate.geometry.point.new(a.x - b.dx, a.y - b.dy) + end + end +end + +meta.__mul = function(a, b) + return b:transformedPoint(a) +end + +meta.__concat = function(a, b) + return playdate.geometry.lineSegment.new(a.x, a.y, b.x, b.y) +end + +meta.__tostring = function(p) + return string.format("(%s, %s)", p.x, p.y) +end + +module.__index = meta + +function module.new(x, y) + local o = {} + + o._type = "point" + o.x = x + o.y = y + + setmetatable(o, meta) + return o +end + +function meta:copy() + return module.new(self.x, self.y) +end + +function meta:unpack() + return self.x, self.y +end + +function meta:offset(dx, dy) + self.x = self.x + dx + self.y = self.y + dy +end + +function meta:offsetBy(dx, dy) + return module.new(self.x + dx, self.y + dy) +end + +function meta:squaredDistanceToPoint(p) + local dx = self.x - p.x + local dy = self.y - p.y + return dx * dx + dy * dy +end + +function meta:distanceToPoint(p) + local dx = self.x - p.x + local dy = self.y - p.y + return math.sqrt(dx * dx + dy * dy) +end diff --git a/playdate/polygon.lua b/playdate/polygon.lua new file mode 100644 index 0000000..5ce2d3b --- /dev/null +++ b/playdate/polygon.lua @@ -0,0 +1,196 @@ +require("playbit.vector") +require("playbit.util") + +local module = {} +playdate.geometry.polygon = module + +local meta = {} + +meta.__index = meta + +meta.__mul = function(a, b) + error("[ERR] playdate.geometry.polygon.__mul is not yet implemented.") +end + +module.__index = meta + +function module.new(x1, y1, ...) + local o = {} + + o._type = "polygon" + o._points = {...} + o._closed = false + + local pts = { } + + if type(x1) == "number" then + if y1 then + pts = { x1, y1, ... } + else + local numberOfVertices = x1 + for i = 1, numberOfVertices, 2 do + pts[i], pts[i + 1] = 0, 0 + end + end + else + local args = {...} + for i = 1, #args do + local pt = args[i] + pts[i * 2 - 1] = pt.x + pts[i * 2] = pt.y + end + end + + o._points = pts + + setmetatable(o, meta) + return o +end + +function meta:copy() + local o = module.new(table.unpack(self._points)) + o._closed = self._closed + return o +end + +function meta:close() + self._closed = true + self._length = nil +end + +function meta:isClosed() + return self._closed +end + +function meta:containsPoint(p, fillRule) + error("[ERR] playdate.geometry.polygon:containsPoint() is not yet implemented.") +end + +function meta:containsPoint(x, y, fillRule) + error("[ERR] playdate.geometry.polygon:containsPoint() is not yet implemented.") +end + +-- Returns multiple values (x, y, width, height) giving the axis-aligned bounding box for the polygon. +function meta:getBounds() + error("[ERR] playdate.geometry.polygon:getBounds() is not yet implemented.") +end + +function meta:getBoundsRect() + local x, y, w, h = self:getBounds() + return playdate.geometry.rect.new(x, y, w, h) +end + +-- Returns the number of points in the polygon. +function meta:count() + return #self._points / 2 +end + +local function calculateLength(pts, closed) + if #pts < 4 then return 0 end + + local len = 0 + local x1, y1 = pts[1], pts[2] + + for i = 3, #pts, 2 do + local x2, y2 = pts[i], pts[i + 1] + len = len + playbit.vector.distance(x1, y1, x2, y2) + x1, y1 = x2, y2 + end + + if closed then + len = len + playbit.vector.distance(x1, y1, pts[1], pts[2]) + end + + return len +end + +-- Returns the total length of all line segments in the polygon. +function meta:length() + + if not self._length then + self._length = calculateLength(self._points, self._closed) + end + + return self._length +end + +function meta:setPointAt(n, x, y) + self._points[n * 2 + 1] = x + self._points[n * 2 + 2] = y + self._length = nil +end + +-- TODO: Check the return type because Playdate docs does not specify it. +function meta:getPointAt(n) + return self._points[n * 2 + 1], self._points[n * 2 + 2] +end + +function meta:intersects(p) + error("[ERR] playdate.geometry.polygon:intersects() is not yet implemented.") +end + +local function pointOnLine(dist, len, x1, y1, x2, y2) + local d = dist / len + local x = x1 + (x2 - x1) * d + local y = y1 + (y2 - y1) * d + return playdate.geometry.point.new(x, y) +end + +function meta:pointOnPolygon(distance, extend) + local pts = self._points + + -- todo: Check what PD returns for 0 or 1 point polygons. + if #pts < 4 then return end + + local length = self:length() + + if not extend and not self._closed then + distance = playbit.util.clamp(distance, 0, length) + end + + if self._closed then + -- Normalize distance making it positive. + distance = (distance % length + length) % length + + else + -- Extrapolate the first segment. + if distance < 0 then + local x1, y1, x2, y2 = pts[1], pts[2], pts[3], pts[4] + local len = playbit.vector.distance(x1, y1, x2, y2) + return pointOnLine(distance, len, x1, y1, x2, y2) + end + + -- Extrapolate the last segment. + if distance >= length then + local cnt = #pts + local x1, y1, x2, y2 = pts[cnt - 3], pts[cnt - 2], pts[cnt - 1], pts[cnt] + local len = playbit.vector.distance(x1, y1, x2, y2) + return pointOnLine(distance - length + len, len, x1, y1, x2, y2) + end + end + + local x1, y1 = pts[1], pts[2] + local dist = distance + for i = 3, #pts, 2 do + local x2, y2 = pts[i], pts[i + 1] + local len = playbit.vector.distance(x1, y1, x2, y2) + if dist <= len and len > 1e-5 then + return pointOnLine(dist, len, x1, y1, x2, y2) + end + dist = dist - len + x1, y1 = x2, y2 + end + + local x2, y2 = pts[1], pts[2] + local len = playbit.vector.distance(x1, y1, x2, y2) + return pointOnLine(dist, len, x1, y1, x2, y2) +end + +function meta:translate(dx, dy) + local pts = self._points + for i = 1, #pts, 2 do + local j = i + 1 + pts[i] = pts[i] + dx + pts[j] = pts[j] + dy + end +end diff --git a/playdate/rect.lua b/playdate/rect.lua new file mode 100644 index 0000000..3b1e0ea --- /dev/null +++ b/playdate/rect.lua @@ -0,0 +1,151 @@ +local module = {} +playdate.geometry.rect = module + +local readonly_keys = { top = true, left = true, right = true, bottom = true } +local meta = {} + +meta.__index = function(table, key) + if key == "top" then + return table.y + + elseif key == "bottom" then + return table.y + table.height + + elseif key == "left" then + return table.x + + elseif key == "right" then + return table.x + table.width + + elseif key == "size" then + return playdate.geometry.size.new(table.width, table.height) + + else + return rawget(meta, key) + + end +end + +meta.__newindex = function(table, key, value) + if readonly_keys[key] then + error(string.format("field '%s' is read-only", key)) + + else + rawset(table, key, value) + + end +end + +module.__index = meta + +function module.new(x, y, width, height) + local o = {} + + o._type = "rect" + o.x = x + o.y = y + o.width = width + o.height = height + + setmetatable(o, meta) + return o +end + +function meta:copy() + return module.new(self.x, self.y, self.width, self.height) +end + +function meta:toPolygon() + local poly = playdategeometry.polygon.new( + self.x, self.y, + self.x + self.width, self.y, + self.x + self.width, self.y + self.height, + self.x, self.y + self.height) + + poly:close() + return poly +end + +function meta:unpack() + return self.x, self.y, self.width, self.height +end + +function meta:isEmpty() + return self.width == 0 or self.height == 0 +end + +function meta:isEqual(r2) + local r1 = self + return r1.x == r2.x and r1.y == r2.y and r1.width == r2.width and r1.height == r2.height +end + +function meta:intersects(r2) + local r1 = self + return r2.left <= r1.right and r2.right >= r1.left and r2.top <= r1.bottom and r2.bottom >= r1.top +end + +function meta:intersection(r2) + local r1 = self + local left = math.max(r1.left, r2.left) + local right = math.min(r1.right, r2.right) + local top = math.max(r1.top, r2.top) + local bottom = math.min(r1.bottom, r2.bottom) + + local width = math.max(0, right - left) + local height = math.max(1, bottom - top) + + error("[ERR] playdate.geometry.rect:intersection() is not yet implemented.") +end + +function meta:union(r2) + local r1 = self + local l = math.min(r1.left, r2.left) + local r = math.max(r1.right, r2.right) + local t = math.min(r1.top, r2.top) + local b = math.max(r1.bottom, r2.bottom) + return module.new(l, t, r - l, b - t) +end + +function meta:inset(dx, dy) + self.x = self.x + dx + self.y = self.y + dy + self.width = self.width - dx - dx + self.height = self.height - dy - dy +end + +function meta:insetBy(dx, dy) + return module.new(self.x + dx, self.y + dy, self.width - dx - dx, self.height - dy - dy) +end + +function meta:offset(dx, dy) + self.x = self.x + dx + self.y = self.y + dy +end + +function meta:offsetBy(dx, dy) + return module.new(self.x + dx, self.y + dy, self.width, self.height) +end + +function meta:containsRect(xOrRect, y, width, height) + error("[ERR] playdate.geometry.rect:containsRect() is not yet implemented.") +end + +function meta:containsPoint(xOrPoint, y) + local right = self.x + self.width + local bottom = self.y + self.height + + if y then + return x >= self.x and x <= right and y >= self.y and y <= bottom + else + local p = xOrPoint + return p.x >= self.x and p.x <= right and p.y >= self.y and p.y <= bottom + end +end + +function meta:centerPoint() + return playdate.geometry.point.new(self.x + self.width / 2, self.y + self.height / 2) +end + +function meta:flipRelativeToRect(r2, flip) + error("[ERR] playdate.geometry.rect:flipRelativeToRect() is not yet implemented.") +end diff --git a/playdate/size.lua b/playdate/size.lua new file mode 100644 index 0000000..62f8193 --- /dev/null +++ b/playdate/size.lua @@ -0,0 +1,24 @@ +local module = {} +playdate.geometry.size = module + +local meta = {} +meta.__index = meta +module.__index = meta + +function module.new(width, height) + local o = {} + + o.width = width + o.height = height + + setmetatable(o, meta) + return o +end + +function meta:copy() + return module.new(self.width, self.height) +end + +function meta:unpack() + return self.width, self.height +end diff --git a/playdate/tilemap.lua b/playdate/tilemap.lua index 3f422b2..f00022d 100644 --- a/playdate/tilemap.lua +++ b/playdate/tilemap.lua @@ -29,12 +29,18 @@ function meta:setSize(width, height) self._tiles = {} end -function meta:setTileAtPosition(x, y, index) - self._tiles[x][y] = index -- index into the tilemap's imagetable +function meta:setTileAtPosition(x, y, tile) + if x >= 1 and x <= self._width and y >= 1 and y <= self._height then + local index = (y - 1) * self._width + x + self._tiles[index] = tile -- index into the tilemap's imagetable + end end function meta:getTileAtPosition(x, y) - local index = x * y + if x < 1 or x > self._width or y < 1 or y > self._height then + return nil + end + local index = (y - 1) * self._width + x if index > #self._tiles then return 0 end @@ -42,28 +48,32 @@ function meta:getTileAtPosition(x, y) end function meta:draw(x, y, sourceRect) - @@ASSERT(x == nil, "[ERR] Parameter x is not yet implemented.") - @@ASSERT(y == nil, "[ERR] Parameter y is not yet implemented.") @@ASSERT(sourceRect == nil, "[ERR] Parameter sourceRect is not yet implemented.") - -- always render pure white so its not tinted - local r, g, b = love.graphics.getColor() - love.graphics.setColor(1, 1, 1, 1) - local frameWidth = self._imagetable._frameWidth local frameHeight = self._imagetable._frameHeight - for i = 1, self._length do - local j = i - 1 - local tile = self._tiles[i] - -- TODO: fix - overwriting the parameter values x, y - local x = math.floor(j % self._width) * frameHeight - local y = math.floor(j / self._width) * frameWidth - love.graphics.draw(self._imagetable._images[tile].data, x, y) + local draw = love.graphics.draw + local images = self._imagetable._images + local imagesCount = #images + local tiles = self._tiles + local index = 1 + local sy = y + + playbit.graphics.setDrawMode("image") + + for j = 1, self._height do + local sx = x + for i = 1, self._width do + local tile = tiles[index] + if tile and tile > 0 and tile <= imagesCount then + draw(images[tile].data, sx, sy) + end + sx = sx + frameWidth + index = index + 1 + end + sy = sy + frameHeight end - - love.graphics.setColor(r, g, b, 1) - playbit.graphics.updateContext() end function meta:getTiles() @@ -72,6 +82,7 @@ end function meta:setTiles(data, width) self._width = width + self._height = math.floor(#data / width) self._length = width * self._height self._tiles = data end @@ -85,11 +96,11 @@ function meta:drawIgnoringOffset(x, y, sourceRect) end function meta:getSize() - error("[ERR] playdate.graphics.tilemap:getSize() is not yet implemented.") + return self._width, self._height end function meta:getPixelSize() - error("[ERR] playdate.graphics.tilemap:getPixelSize() is not yet implemented.") + return self._width * self._imagetable._frameWidth, self._height * self._imagetable._frameHeight end function meta:getCollisionRects(emptyIDs) diff --git a/playdate/vector2D.lua b/playdate/vector2D.lua new file mode 100644 index 0000000..5a6c8d4 --- /dev/null +++ b/playdate/vector2D.lua @@ -0,0 +1,131 @@ +local module = {} +playdate.geometry.vector2D = module + +local meta = {} +meta.__index = meta + +meta.__unm = function(v) + return module.new(-v.dx, -v.dy) +end + +meta.__add = function(v1, v2) + return module.new(v1.dx + v2.dx, v1.dy + v2.dy) +end + +meta.__sub = function(v1, v2) + return module.new(v1.dx - v2.dx, v1.dy - v2.dy) +end + +meta.__mul = function(v1, v2) + if type(v2) == "table" and v2._type then + if v2._type == "vector2D" then + return v1.dx * v2.dx + v1.dy * v2.dy + + elseif v2._type == "affineTransform" then + return module.new(v1.dx * v2.m11 + v1.dy * v2.m12, v1.dx * v2.m21 + v1.dy * v2.m22) + + end + + else + local s = v2 + return module.new(v1.dx * s, v1.dy * s) + + end +end + +meta.__div = function(v1, s) + return module.new(v1.dx / s, v1.dy / s) +end + +module.__index = meta + +function module.new(dx, dy) + local o = {} + + o._type = "vector2D" + o.dx = dx + o.dy = dy + + setmetatable(o, meta) + return o +end + +function module.newPolar(length, angle) + local o = {} + + angle = angle * math.pi / 180 + + o._type = "vector2D" + o.dx = length * math.sin(angle) + o.dy = -length * math.cos(angle) + + setmetatable(o, meta) + return o +end + +function meta:copy() + return module.new(self.dx, self.dy) +end + +function meta:unpack() + return self.dx, self.dy +end + +function meta:addVector(v) + self.dx = self.dx + v.dx + self.dy = self.dy + v.dy +end + +function meta:scale(s) + self.dx = self.dx * s + self.dy = self.dy * s +end + +function meta:scaledBy(s) + return module.new(self.dx * s, self.dy * s) +end + +function meta:normalize() + local len = self:magnitude() + self.dx = self.dx / len + self.dy = self.dy / len +end + +function meta:normalized() + local len = self:magnitude() + return module.new(self.dx / len, self.dy / len) +end + +function meta:dotProduct(v) + return self.dx * v.dx + self.dy * v.dy +end + +function meta:magnitude() + local dx, dy = self.dx, self.dy + return math.sqrt(dx * dx + dy * dy) +end + +function meta:magnitudeSquared() + local dx, dy = self.dx, self.dy + return dx * dx + dy * dy +end + +function meta:projectAlong(v) + error("[ERR] playdate.geometry.vector2D:projectAlong() is not yet implemented.") +end + +function meta:projectedAlong(v) + error("[ERR] playdate.geometry.vector2D:projectedAlong() is not yet implemented.") +end + +function meta:angleBetween(v) + error("[ERR] playdate.geometry.vector2D:angleBetween() is not yet implemented.") +end + +function meta:leftNormal() + return module.new(self.dy, -self.dx) +end + +function meta:rightNormal() + return module.new(-self.dy, self.dx) +end