diff --git a/scripts/api/color.lua b/scripts/api/color.lua new file mode 100644 index 0000000..5424e70 --- /dev/null +++ b/scripts/api/color.lua @@ -0,0 +1,74 @@ +local util = require("common.util") + +---@class CColorRGBA +ColorRGBA = { + ---Create a new Color instance + ---@param r integer # red or monochrome value + ---@param g? integer # green value + ---@param b? integer # blue value + ---@param a? integer # alpha value, default 255 + ---@return ColorRGBA + new = function (r, g , b, a) + ---@class ColorRGBA : CColorRGBA + ---@field r integer + ---@field g integer + ---@field b integer + ---@field a integer + local o = { + r = r or 0, + g = g or r, + b = b or r, + a = a or 255, + } + + setmetatable(o, ColorRGBA) + return o + end, + + ---Mix two colors + ---@param color1 ColorRGBA + ---@param color2 ColorRGBA + ---@param factor number + ---@return ColorRGBA + mix = function (color1, color2, factor) + local r = math.floor(util.mix(color1.r, color2.r, factor)) + local g = math.floor(util.mix(color1.g, color2.g, factor)) + local b = math.floor(util.mix(color1.b, color2.b, factor)) + local a = math.floor(util.mix(color1.a, color2.a, factor)) + return ColorRGBA.new(r, g, b, a) + end +} + +ColorRGBA.__index = ColorRGBA +ColorRGBA.BLACK = ColorRGBA.new(0) +ColorRGBA.GREY = ColorRGBA.new(128) +ColorRGBA.WHITE = ColorRGBA.new(255) +ColorRGBA.RED = ColorRGBA.new(255, 0, 0) +ColorRGBA.GREEN = ColorRGBA.new(0, 255, 0) +ColorRGBA.BLUE = ColorRGBA.new(0, 0, 255) +ColorRGBA.YELLOW = ColorRGBA.new(255, 255, 0) +ColorRGBA.CYAN = ColorRGBA.new(0, 255, 255) +ColorRGBA.MAGENTA = ColorRGBA.new(255, 0, 255) + +---Split to components +---@return integer # red +---@return integer # green +---@return integer # blue +---@return integer # alpha +function ColorRGBA:components() + ---@cast self ColorRGBA + + return self.r, self.g, self.b, self.a +end + +---Split to components scaled to [0.0, 1.0] +---@return number # red +---@return number # green +---@return number # blue +---@return number # alpha +function ColorRGBA:componentsFloat() + ---@cast self ColorRGBA + local scale = 255 + + return self.r / scale, self.g / scale, self.b / scale, self.a / scale +end diff --git a/scripts/api/point2d.lua b/scripts/api/point2d.lua new file mode 100644 index 0000000..84cfdf1 --- /dev/null +++ b/scripts/api/point2d.lua @@ -0,0 +1,28 @@ +---@class CPoint2D +Point2D = { + ---Create a Point2D instance + ---@param x? number # default 0.0 + ---@param y? number # default 0.0 + ---@return Point2D + new = function(x, y) + ---@class Point2D : CPoint2D + ---@field x number + ---@field y number + local o = { + x = x + .0 or .0, + y = y + .0 or .0, + } + + setmetatable(o, Point2D) + return o + end +} + +Point2D.__index = Point2D +Point2D.ZERO = Point2D.new(0, 0) + +function Point2D:coords() + ---@cast self Point2D + + return self.x, self.y +end diff --git a/scripts/common/util.lua b/scripts/common/util.lua index e7ab595..7f10481 100644 --- a/scripts/common/util.lua +++ b/scripts/common/util.lua @@ -59,6 +59,10 @@ local function lerp(x, x0, y0, x1, y1) return y0 + (x - x0) * (y1 - y0) / (x1 - x0) end +local function mix(x, y, a) + return (1 - a) * x + a * y +end + --modulo operation for index value local function modIndex(index, mod) return (index - 1) % mod + 1 @@ -84,6 +88,7 @@ return { roundToZero = roundToZero, areaOverlap = areaOverlap, lerp = lerp, + mix = mix, modIndex = modIndex, firstAlphaNum = firstAlphaNum, } diff --git a/scripts/components/radar.lua b/scripts/components/radar.lua new file mode 100644 index 0000000..75f77ae --- /dev/null +++ b/scripts/components/radar.lua @@ -0,0 +1,443 @@ +--[[ +S2 song attribute radar component +Original code thanks to RealFD, he's a real homie +]] + +require("common.globals") +require("api.point2d") +require("api.color") + +local Dim = require("common.dimensions") + +Dim.updateResolution() + +local RADAR_PURPLE = ColorRGBA.new(238, 130, 238) +local RADAR_MAGENTA = ColorRGBA.new(191, 70, 235) +local RADAR_GREEN = ColorRGBA.new(0, 255, 100) + +---@param p1 Point2D +---@param p2 Point2D +---@param width number +---@param color ColorRGBA +local function drawLine(p1, p2, width, color) + gfx.BeginPath() + gfx.MoveTo(p1:coords()) + gfx.LineTo(p2:coords()) + gfx.StrokeColor(color:components()) + gfx.StrokeWidth(width) + gfx.Stroke() +end + +---@param pos Point2D +---@param text string +---@param outlineWidth number +---@param color ColorRGBA +local function renderOutlinedText(pos, text, outlineWidth, color) + local x, y = pos:coords() + + gfx.FillColor(ColorRGBA.BLACK:components()); + gfx.Text(text, x - outlineWidth, y + outlineWidth); + gfx.Text(text, x - outlineWidth, y - outlineWidth); + gfx.Text(text, x + outlineWidth, y + outlineWidth); + gfx.Text(text, x + outlineWidth, y - outlineWidth); + + gfx.FillColor(color:components()); + gfx.Text(text, x, y); +end + +---@param pos Point2D +---@param graphdata table +local function drawDebugText(pos, graphdata) + local color = ColorRGBA.WHITE + gfx.Save() + gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_CENTER) + --renderOutlinedText(x, 20, '"' .. txtFilePath .. '"', 1, 255, 255, 255) + renderOutlinedText(pos, "NOTES = " .. graphdata.notes, 1, color) + renderOutlinedText(pos, "PEAK = " .. graphdata.peak, 1, color) + renderOutlinedText(pos, "TSUMAMI = " .. graphdata.tsumami, 1, color) + renderOutlinedText(pos, "TRICKY = " .. graphdata.tricky, 1, color) + renderOutlinedText(pos, "ONE-HAND = " .. graphdata.onehand, 1, color) + renderOutlinedText(pos, "HAND-TRIP = " .. graphdata.handtrip, 1, color) + --renderOutlinedText(pos, "NOTES (Relative) = " .. graphdata.notes_relative, 1, color) + --renderOutlinedText(pos, "TOTAL-MESURES = " .. graphdata.measures, 1, color) + gfx.Restore() +end + +---@class CRadarAttributes +RadarAttributes = { + ---Create RadarAttributes instance + ---@param text? string # default "" + ---@param pos? Point2D # default (0, 0) + ---@param color? ColorRGBA # default BLACK + ---@return RadarAttributes + new = function (text, pos, color) + ---@class RadarAttributes + ---@field text string + ---@field pos Point2D + ---@field color ColorRGBA + local o = { + text = text or "", + pos = pos or Point2D.ZERO, + color = color or ColorRGBA.BLACK + } + + setmetatable(o, RadarAttributes) + return o + end +} +RadarAttributes.__index = RadarAttributes + + +---@class CRadar +Radar = { + ---@type RadarAttributes[][] + ATTRIBUTES = { + {RadarAttributes.new("notes", Point2D.new(0, -30), ColorRGBA.CYAN),}, + {RadarAttributes.new("peak", Point2D.new(40, -20), ColorRGBA.RED),}, + {RadarAttributes.new("tsumami", Point2D.new(40, 20), RADAR_PURPLE),}, + {RadarAttributes.new("tricky", Point2D.new(0, 30), ColorRGBA.YELLOW),}, + { + RadarAttributes.new("hand", Point2D.new(-40, 20), RADAR_MAGENTA), + RadarAttributes.new("trip", Point2D.new(5, 18), RADAR_MAGENTA), + }, + { + RadarAttributes.new("one", Point2D.new(-40, -20), RADAR_GREEN), + RadarAttributes.new("hand", Point2D.new(-5, 18), RADAR_GREEN), + } + }, + RADIUS = 100.0, + + ---Create Radar instance + ---@param pos Point2D + ---@param radius? number + ---@return Radar + new = function (pos, radius) + ---@class Radar : CRadar + local o = { + _graphdata = { + notes = 0, + peak = 0, + tsumami = 0, + tricky = 0, + handtrip = 0, + onehand = 0, + }, + _hexagonMesh = gfx.CreateShadedMesh("radar"), + outlineVertices = {}, + attributePositions = {}, + pos = pos or Point2D.ZERO, + scale = radius and radius / Radar.RADIUS or 1.0, + } + + local sides = #Radar.ATTRIBUTES + local angleStep = (2 * math.pi) / sides + local rotationAngle = angleStep / 2 + + local outlineRadius = Radar.RADIUS + local attributeRadius = Radar.RADIUS + 20 + + for i = 0, sides - 1 do + local attrIdx = i + 1 + local angle = i * angleStep - rotationAngle + local cosAngle = math.cos(angle) + local sinAngle = math.sin(angle) + -- cache outline vertices + table.insert(o.outlineVertices, Point2D.new(outlineRadius * cosAngle, outlineRadius * sinAngle)) + -- cache attribute positions + table.insert(o.attributePositions, {}) + for j = 1, #Radar.ATTRIBUTES[attrIdx] do + local attr = Radar.ATTRIBUTES[attrIdx][j] + local attributePos = Point2D.new(attributeRadius * cosAngle, attributeRadius * sinAngle) + attributePos.x = attributePos.x + attr.pos.x + attributePos.y = attributePos.y + attr.pos.y + table.insert(o.attributePositions, j, attributePos) + end + end + + setmetatable(o, Radar) + return o + end, +} +Radar.__index = Radar + +---@param w number +---@param color ColorRGBA +function Radar:drawOutline(w, color) + ---@cast self Radar + + for i = 1, #self.outlineVertices do + local j = i % #self.outlineVertices + 1 + drawLine(self.outlineVertices[i], self.outlineVertices[j], w, color) + end +end + +---@param color ColorRGBA +---@param ticks? integer +function Radar:drawRadialTicks(color, ticks) + ---@cast self Radar + + ticks = ticks or 3 + + gfx.Save() + gfx.StrokeColor(color:components()) + + for i, vertex in ipairs(self.outlineVertices) do + gfx.BeginPath() + gfx.MoveTo(0, 0) + gfx.LineTo(vertex.x, vertex.y) + gfx.Stroke() + + local lineLength = math.sqrt(vertex.x * vertex.x + vertex.y * vertex.y) + local tinyLineLength = 10 + + local tinyLineAngle = math.atan(vertex.y / vertex.x) + if vertex.x < 0 then + tinyLineAngle = tinyLineAngle + math.pi + end + + local halfTinyLineLength = tinyLineLength / 2 + + for j = 1, ticks do + local distanceFromCenter = j * lineLength / (ticks + 1) -- Adjusted for 3 middle lines + + local offsetX = distanceFromCenter * (vertex.x / lineLength) + local offsetY = distanceFromCenter * (vertex.y / lineLength) + + local endX = halfTinyLineLength * math.cos(tinyLineAngle - math.pi / 2) -- Rotate by -90 degrees + local endY = halfTinyLineLength * math.sin(tinyLineAngle - math.pi / 2) -- Rotate by -90 degrees + + local offsetX2 = halfTinyLineLength * math.cos(tinyLineAngle + math.pi / 2) + local offsetY2 = halfTinyLineLength * math.sin(tinyLineAngle + math.pi / 2) + + gfx.BeginPath() + gfx.MoveTo(offsetX - offsetX2, offsetY - offsetY2) + gfx.LineTo(endX + offsetX + offsetX2 + offsetX2, endY + offsetY + offsetY2 + offsetY2) + gfx.Stroke() + end + end + + gfx.Restore() +end + +---@param fillColor ColorRGBA +function Radar:drawBackground(fillColor) + ---@cast self Radar + + gfx.Save() + + gfx.BeginPath() + gfx.MoveTo(self.outlineVertices[1].x, self.outlineVertices[1].y) + for i = 2, #self.outlineVertices do + gfx.LineTo(self.outlineVertices[i].x, self.outlineVertices[i].y) + end + gfx.ClosePath() + + gfx.FillColor(fillColor:components()) + gfx.Fill() + + gfx.Restore() +end + +function Radar:drawAttributes() + ---@cast self Radar + + gfx.Save() + + gfx.FontSize(20) + for i = 1, #self.attributePositions do + local attrPos = self.attributePositions[i] + for j = 1, #attrPos do + local pos = attrPos[j] + local attr = Radar.ATTRIBUTES[i][j] + gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_CENTER) + renderOutlinedText(pos, string.upper(attr.text), 1, attr.color) + end + end + + gfx.Restore() +end + +function Radar:drawRadarMesh() + ---@cast self Radar + + local scaleFact = { + self._graphdata.notes, + self._graphdata.peak, + self._graphdata.tsumami, + self._graphdata.tricky, + self._graphdata.handtrip, + self._graphdata.onehand, + } + + local color1 = ColorRGBA.new(255, 12, 48, 230) -- magenta-ish + local color2 = ColorRGBA.new(112, 119, 255, 230) -- light blue-ish purple + + local currentTime = 0 + + -- Calculate the maximum size based on the constraint + local maxSize = self.RADIUS * self.scale + 10 + local maxLineLength = maxSize + (maxSize / 2) + self._hexagonMesh:SetParam("maxSize", maxLineLength + .0) + + -- Set the color of the hexagon + self._hexagonMesh:SetParamVec4("colorMax", color1:componentsFloat()) + self._hexagonMesh:SetParamVec4("colorCenter", color2:componentsFloat()) + + -- Set the primitive type to triangles + self._hexagonMesh:SetPrimitiveType(self._hexagonMesh.PRIM_TRIFAN) + + -- Calculate the vertices of the hexagon + local sides = #Radar.ATTRIBUTES + local angleStep = (2 * math.pi) / sides + local rotationAngle = angleStep / 2 + --local rotationAngle = -math.pi / 2 + local vertices = {} + + table.insert(vertices, {{0, 0}, {0, 0}}) + for i = 0, sides do + local j = i % sides + 1 + local angle = i * angleStep - rotationAngle + + --local angle = math.rad(60 * (i-1)) + rotationAngle + local scale = scaleFact[j] + local lineLength = maxLineLength * scale + lineLength = math.min(lineLength, maxLineLength) -- Cap the length + local px = lineLength * math.cos(angle) + local py = lineLength * math.sin(angle) + table.insert(vertices, {{px, py}, {0, 0}}) + end + + -- Set the hexagon's vertices + self._hexagonMesh:SetData(vertices) + --self._hexagonMesh:SetPosition(-maxSize, maxSize) + self._hexagonMesh:Draw() + + --NOTE: Bug: forcerender resets every transformation, need to re-setup view transform afterwards + gfx.ForceRender() +end + +function Radar:drawGraph() + ---@cast self Radar + + gfx.Save() + gfx.Reset() + gfx.ResetScissor() + + Dim.updateResolution() + Dim.transformToScreenSpace() + + gfx.FontSize(28) + gfx.Translate(self.pos.x, self.pos.y) + gfx.Scale(self.scale, self.scale) + + local strokeColor = ColorRGBA.new(255, 255, 255, 100) + local fillColor = ColorRGBA.new(0, 0, 0, 191) + + self:drawBackground(strokeColor, fillColor) + self:drawOutline(3, ColorRGBA.WHITE) + self:drawRadarMesh() + self:drawRadialTicks() + self:drawAttributes() + + local pos = Point2D.new(self.pos:coords()) + pos.y = pos.y - self.RADIUS + --drawDebugText(pos, self._graphdata) + + gfx.Restore() + + --NOTE: Bug workaround: forcerender resets every transformation, re-setup view transform + Dim.transformToScreenSpace() +end + +---Compute radar attribute values from ksh +---@param info string # chart directory path +---@param dif string # chart name without extension +function Radar:updateGraph(info, dif) + ---@cast self Radar + + --local pattern = "(.*[\\/])" + --local extractedSubstring = info:match(pattern) + --local txtFilePath = extractedSubstring .. "radar\\" .. dif .. ".txt" + + --local song = io.open(txtFilePath, "r") + local song = io.open(info.."/"..dif..".ksh") + game.Log(info.."/"..dif..".ksh", game.LOGGER_DEBUG) + game.Log(song and "file open" or "file not found", game.LOGGER_DEBUG) + if song then + local chartData = song:read("*all") + song:close() + + local notesCount, knobCount, oneHandCount, handTripCount = 0, 0, 0, 0 + local trickyValue = 0 + local totalMeasures = 0 + local totalSongLength = 0 + local tsumamiValue = 0 + + for line in chartData:gmatch("[^\r\n]+") do + local noteType, fxType, laserType = line:match("(%d%d%d%d)|(%d%d)|([%-%a ]+)") + + if noteType and fxType then + local noteCount = noteType:match("1") and 1 or 0 + + notesCount = notesCount + noteCount + knobCount = knobCount + (fxType == "02" and 1 or 0) + + oneHandCount = oneHandCount + (laserType:match("[79A-D:]") and 1 or 0) + handTripCount = handTripCount + (laserType:match("[HKPUV:]") and 1 or 0) + if laserType ~= "--" then + tsumamiValue = tsumamiValue + 0.5 + end + end + + if line == "--" then + totalMeasures = totalMeasures + 1 + elseif line:match("t=(%d+)") then + local bpm = tonumber(line:match("t=(%d+)")) + totalSongLength = math.max(totalSongLength, bpm) + end + + if line:match("beat=") or line:match("stop=") or + line:match("zoom_top=") or line:match("zoom_bottom=") or line:match("center_split=") then + trickyValue = trickyValue + 0.5 + end + end + + local graphValues = { + notes = (notesCount / totalSongLength), + peak = (notesCount / totalMeasures) * 10, + tsumami = tsumamiValue, + tricky = trickyValue, + handtrip = handTripCount, + onehand = oneHandCount, + } + + game.Log("graphValues", game.LOGGER_DEBUG) + for k,v in pairs(graphValues) do + game.Log(k..": "..v, game.LOGGER_DEBUG) + end + + local scaleFactors = { + notes = 2, + peak = 50, + tsumami = 500, + tricky = 50, + handtrip = 100, + onehand = 50, + } + + for key, factor in pairs(scaleFactors) do + -- Apply the scaling factor to each value + self._graphdata[key] = graphValues[key] / factor + + -- Limit to a maximum of 125% + self._graphdata[key] = math.min(1.25, self._graphdata[key]) + end + + game.Log("_graphdata", game.LOGGER_DEBUG) + for k,v in pairs(self._graphdata) do + game.Log(k..": "..v, game.LOGGER_DEBUG) + end + end +end + +return Radar diff --git a/scripts/songselect/songwheel.lua b/scripts/songselect/songwheel.lua index 01e7c60..6b53e78 100644 --- a/scripts/songselect/songwheel.lua +++ b/scripts/songselect/songwheel.lua @@ -9,6 +9,9 @@ local Numbers = require('components.numbers') local VolforceCalc = require('components.volforceCalc') +require("api.point2d") +require("components.radar") + local dataPanelImage = gfx.CreateSkinImage("song_select/data_bg_overlay.png", 1) local dataGlowOverlayImage = gfx.CreateSkinImage("song_select/data_panel/data_glow_overlay.png", 1) local gradeBgImage = gfx.CreateSkinImage("song_select/data_panel/grade_bg.png", 1) @@ -39,8 +42,6 @@ local searchInfoPanelImage = gfx.CreateSkinImage("song_select/search_info_panel. local defaultJacketImage = gfx.CreateSkinImage("song_select/loading.png", 0) - - local difficultyLabelImages = { gfx.CreateSkinImage("song_select/plate/difficulty_labels/novice.png", 1), gfx.CreateSkinImage("song_select/plate/difficulty_labels/advanced.png", 1), @@ -125,6 +126,9 @@ local songPlateHeight = 172 local selectedIndex = 1 local selectedDifficulty = 1 +local radar = Radar.new(Point2D.new(0, 0)) +local updateRadar = true + local jacketCache = {} local top50diffs = {} @@ -424,8 +428,8 @@ function drawData() -- Draws the song data on the left panel gfx.ImageRect(96, 529, 410*0.85, 168*0.85, top50JacketOverlayImage, 1, 0) end - gfx.Save() -- Draw best score + gfx.Save() gfx.BeginPath() local scoreNumber = 0 @@ -464,29 +468,32 @@ function drawData() -- Draws the song data on the left panel gfx.Restore() -- Draw BPM + gfx.Save() gfx.BeginPath() gfx.FontSize(24) gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_MIDDLE) - gfx.Save() gfx.LoadSkinFont('Digital-Serial-Bold.ttf') gfx.GlobalAlpha(transitionAfterscrollDataOverlayAlpha) -- TODO: split this out gfx.Text(song.bpm, 85, 920) gfx.Restore() -- Draw song title + gfx.Save() gfx.FontSize(28) gfx.GlobalAlpha(transitionAfterscrollTextSongTitle) gfx.Text(song.title, 30+(1-transitionAfterscrollTextSongTitle)*20, 955) + gfx.Restore() -- Draw artist + gfx.Save() gfx.GlobalAlpha(transitionAfterscrollTextSongArtist) gfx.Text(song.artist, 30+(1-transitionAfterscrollTextSongArtist)*30, 997) - - gfx.GlobalAlpha(1) + gfx.Restore() -- Draw difficulties local DIFF_X_START = 98.5 local DIFF_GAP = 114.8 + gfx.Save() gfx.GlobalAlpha(transitionAfterscrollDifficultiesAlpha) for i, diff in ipairs(song.difficulties) do gfx.BeginPath() @@ -508,20 +515,20 @@ function drawData() -- Draws the song data on the left panel gfx.BeginPath() gfx.ImageRect(DIFF_X_START+(index-1)*DIFF_GAP-tw/2, 1050, tw, th, diffLabelImage, 1, 0) end - gfx.GlobalAlpha(1) - + gfx.Restore() -- Scoreboard drawLocalLeaderboard(diff) drawIrLeaderboard() + gfx.Save() gfx.FontSize(22) gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_MIDDLE) gfx.GlobalAlpha(transitionAfterscrollDataOverlayAlpha) gfx.Text(diff.effector, 270, 1180) -- effected by gfx.Text(diff.illustrator, 270, 1210) -- illustrated by - gfx.GlobalAlpha(1) + gfx.Restore() end @@ -1069,6 +1076,30 @@ function tickTransitions(deltaTime) end end +---This function is basically a workaround for the ForceRender call +local function drawRadar() + gfx.FontSize(28) + gfx.Translate(500, 500) + + local strokeColor = ColorRGBA.new(255, 255, 255, 255) + local fillColor = ColorRGBA.new(0, 0, 0, 191) + + gfx.ResetScissor() + radar:drawBackground(fillColor) + radar:drawOutline(3, strokeColor) + + --NOTE: Bug: forcerender resets every transformation, need to re-setup view transform afterwards + radar:drawRadarMesh() + + Dim.transformToScreenSpace() + + gfx.Save() + gfx.Translate(500,500) + radar:drawRadialTicks(strokeColor) + radar:drawAttributes() + gfx.Restore() +end + draw_songwheel = function(deltaTime) drawBackground(deltaTime) @@ -1077,6 +1108,9 @@ draw_songwheel = function(deltaTime) isFilterWheelActive = game.GetSkinSetting('_songWheelOverlayActive') == 1 drawData() + + drawRadar() + drawCursor() drawFilterInfo(deltaTime) @@ -1105,6 +1139,13 @@ render = function (deltaTime) Sound.stopMusic() + if updateRadar then + local difficultyNames = {"nov","adv","exh","mxm","inf","grv","hvn","vvd","exc"} + local diff = songwheel.songs[selectedIndex].difficulties[selectedDifficulty].difficulty + 1 + radar:updateGraph(songwheel.songs[selectedIndex].path, difficultyNames[diff]) + updateRadar = false + end + Dim.updateResolution() Wallpaper.render() @@ -1148,6 +1189,7 @@ songs_changed = function (withAll) game.SetSkinSetting('_volforce', totalForce) end + set_index = function(newIndex) transitionScrollScale = 0 transitionAfterscrollScale = 0 @@ -1162,11 +1204,15 @@ set_index = function(newIndex) scrollingUp = true end + updateRadar = true + game.PlaySample('song_wheel/cursor_change.wav') selectedIndex = newIndex end +local json = require("common.json") + set_diff = function(newDiff) if newDiff ~= selectedDifficulty then jacketCache = {} -- Clear the jacket cache for the new diff jackets @@ -1174,6 +1220,8 @@ set_diff = function(newDiff) game.PlaySample('song_wheel/diff_change.wav') end + updateRadar = true + selectedDifficulty = newDiff irLeaderboard = {} irRequestStatus = 1 diff --git a/shaders/radar.fs b/shaders/radar.fs new file mode 100644 index 0000000..ae5cbbd --- /dev/null +++ b/shaders/radar.fs @@ -0,0 +1,66 @@ +#version 330 +#extension GL_ARB_separate_shader_objects : enable + +const float PI = 3.1415926535897932384626433832795; +const float PI_2 = 1.57079632679489661923; + +layout(location=1) in vec2 fsTex; +layout(location=2) in vec3 fragPos; + +layout(location=0) out vec4 target; + +uniform vec4 colorCenter; +uniform vec4 colorMax; + +uniform float maxSize; + +float lerp(float x, vec2 p0, vec2 p1) +{ + return p0.y + (p1.y - p0.y) * ((x - p0.x)/(p1.x - p0.x)); +} + +vec4 toHSV(vec4 rgb) +{ + vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0); + vec4 p = mix(vec4(rgb.bg, K.wz), vec4(rgb.gb, K.xy), step(rgb.b, rgb.g)); + vec4 q = mix(vec4(p.xyw, rgb.r), vec4(rgb.r, p.yzx), step(p.x, rgb.r)); + + float d = q.x - min(q.w, q.y); + float e = 1.0e-10; + return vec4(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x, rgb.a); +} + +vec4 toRGB(vec4 hsv) +{ + vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); + vec3 p = abs(fract(hsv.xxx + K.xyz) * 6.0 - K.www); + return vec4(hsv.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), hsv.y), hsv.a); +} + +// x is in [0.0, 1.0] +vec4 lerpColor(float x, vec4 color1, vec4 color2) +{ + // convert RGB color space to HSV + vec4 hsv1 = toHSV(color1), hsv2 = toHSV(color2); + float hue = (mod(mod((hsv2.x-hsv1.x), 1.) + 1.5, 1.)-0.5)*x + hsv1.x; + vec4 hsv = vec4(hue, mix(hsv1.yzw, hsv2.yzw, x)); + // convert HSV color space back to RGB + return toRGB(hsv); +} + +float EaseInSine(float x) +{ + return 1.0 - cos((x * PI) / 2.0); +} + +vec4 coordToColor(vec2 pos, float maxSize) +{ + float r = length(pos); + float factor = lerp(r, vec2(0,0), vec2(maxSize,1)); + return lerpColor(EaseInSine(factor), colorCenter, colorMax); +} + +void main() +{ + target = coordToColor(fragPos.xy, maxSize); +} diff --git a/shaders/radar.vs b/shaders/radar.vs new file mode 100644 index 0000000..2826d10 --- /dev/null +++ b/shaders/radar.vs @@ -0,0 +1,22 @@ +#version 330 +#extension GL_ARB_separate_shader_objects : enable + +layout(location=0) in vec2 inPos; +layout(location=1) in vec2 inTex; + +out gl_PerVertex +{ + vec4 gl_Position; +}; +layout(location=1) out vec2 fsTex; +layout(location=2) out vec3 fragPos; + +uniform mat4 proj; +uniform mat4 world; + +void main() +{ + fsTex = inTex; + gl_Position = proj * world * vec4(inPos.xy, 0, 1); + fragPos = vec3(inPos.xy, 0); +} diff --git a/shaders/radarVertex.fs b/shaders/radarVertex.fs new file mode 100644 index 0000000..3e1f4ce --- /dev/null +++ b/shaders/radarVertex.fs @@ -0,0 +1,11 @@ +#version 330 +#extension GL_ARB_separate_shader_objects : enable + +layout(location=1) in vec2 fsTex; +layout(location=0) out vec4 target; +layout(location=2) in vec4 inColor; + +void main() +{ + target = inColor; +} diff --git a/shaders/radarVertex.vs b/shaders/radarVertex.vs new file mode 100644 index 0000000..3a6193b --- /dev/null +++ b/shaders/radarVertex.vs @@ -0,0 +1,94 @@ +#version 330 +#extension GL_ARB_separate_shader_objects : enable + +const float PI = 3.1415926535897932384626433832795; +const float PI_2 = 1.57079632679489661923; + +layout(location=0) in vec2 inPos; +layout(location=1) in vec2 inTex; + +out gl_PerVertex +{ + vec4 gl_Position; +}; +layout(location=1) out vec2 fsTex; +layout(location=2) out vec4 vertexColor; + +uniform mat4 proj; +uniform mat4 world; + +uniform vec4 colorCenter; +uniform vec4 colorMax; + +uniform float maxSize; + +// Polar coordinate utility functions +float hypot(vec2 pos) +{ + return sqrt(pos.x * pos.x + pos.y * pos.y); +} + +/* +float atan2(vec2 pos) +{ + if (pos.x > 0) + { + return atan(pos.y / pos.x); + } + + if (pos.x < 0 && pos.y >= 0) + { + return atan(pos.y / pos.x) + PI; + } + + if (pos.x < 0 && pos.y < 0) + { + return atan(pos.y / pos.x) - PI; + } + + if (pos.x == 0 && pos.y > 0) + { + return PI_2; + } + + if (pos.x == 0 && pos.y < 0) + { + return -PI_2; + } + + // The following is not mathematically correct, as it's undefined + return 0.0; +} + +vec2 toPolar(vec2 cartesian) +{ + return vec2(hypot(pos), atan2(pos)); +} +*/ + +float lerp(float x, vec2 p0, vec2 p1) +{ + return p0.y + (p1.y - p0.y) * ((x - p0.x)/(p1.x - p0.x)); +} + +// x is in [0.0, 1.0] +vec4 lerpColor(float x, vec4 color1, vec4 color2) +{ + return color1 + (color2 - color1) * x; +} + +vec4 coordToColor(vec2 pos, float maxSize) +{ + float r = hypot(pos); + //float phi = atan2(pos); + + float factor = lerp(r, vec2(0,0), vec2(maxSize,1)); + + return lerpColor(factor, colorCenter, colorMax); +} + +void main() +{ + gl_Position = proj * world * vec4(inPos.xy, 0, 1); + vertexColor = coordToColor(inPos.xy, maxSize); +}