-- World map zoom: the mouse wheel enlarges the map toward the cursor (x 1.25 per step, 1 to 4) -- and a left-button drag pans the zoomed map; a click without drag keeps its meaning. -- Camelot zooms through map art layers (mapcanvas_scrollcontainermixin.lua), which 3.3.5 -- lacks. The zoom returns to 1 when the map changes, changes mode or closes. -- The map is ENLARGED, not scaled: WorldMapDetailFrame and WorldMapButton get their width and -- height x zoom at their own scale. The client places every marker at its map fraction x -- their width, so the markers move with the map and keep their size, as in camelot. The art -- (tiles, explored areas) has fixed sizes in the client: it is laid out and cropped here. -- Units: "map units" are those of the whole map (1002 x 668); the frames' are map units x zoom. local W = ForeverUI.WorldMap local Z = { z = 1, cx = 0.5, cy = 0.5, active = false, STEP = 1.25, MAX = 4, THRESHOLD = 4 } W.zoom = Z local CARD_W, CARD_H = 1002, 668 local TILE, COLUMNS, ROWS = 256, 4, 3 local function tile(k) return _G["WorldMapDetailTile" .. k] end local function ready() return W.built and W.canvas and WorldMapDetailFrame and WorldMapButton and WORLDMAP_SETTINGS end -- Screen pixels per map unit local function pixelsPerUnit() return WorldMapDetailFrame:GetEffectiveScale() * Z.z end -- The view: the canvas rectangle in map units, origin at the map's top left local function viewSize() local c, d = W.canvas, WorldMapDetailFrame local k = c:GetEffectiveScale() / d:GetEffectiveScale() / Z.z return c:GetWidth() * k, c:GetHeight() * k end local function view() local vw, vh = viewSize() local x, y = Z.cx * CARD_W, Z.cy * CARD_H return x - vw / 2, y - vh / 2, x + vw / 2, y + vh / 2 end Z.view = view -- Keep the center where the view stays inside the map local function clampView() local vw, vh = viewSize() local halfX, halfY = vw / 2 / CARD_W, vh / 2 / CARD_H if halfX >= 0.5 then Z.cx = 0.5 else Z.cx = math.max(halfX, math.min(1 - halfX, Z.cx)) end if halfY >= 0.5 then Z.cy = 0.5 else Z.cy = math.max(halfY, math.min(1 - halfY, Z.cy)) end end -- ------------------------------------------------------------------ Art -- 3.3.5 clips nothing outside a ScrollFrame and client frames are never reparented, so each -- piece of art is cropped to the view: its visible part, at its place x zoom, with matching -- tex coords. x, y, w, h: the piece in map units; u0, u1, v0, v1: its tex coords. local function cropRegion(t, x, y, w, h, u0, u1, v0, v1, x0, y0, x1, y1) local d, z = WorldMapDetailFrame, Z.z local ix0, iy0 = math.max(x, x0), math.max(y, y0) local ix1 = math.min(x + w, CARD_W, x1) local iy1 = math.min(y + h, CARD_H, y1) t:ClearAllPoints() if ix1 - ix0 < 0.01 or iy1 - iy0 < 0.01 then t:SetPoint("TOPLEFT", d, "TOPLEFT", x * z, -y * z) t:SetWidth(1) t:SetHeight(1) t:SetAlpha(0) else local du, dv = (u1 - u0) / w, (v1 - v0) / h t:SetPoint("TOPLEFT", d, "TOPLEFT", ix0 * z, -iy0 * z) t:SetWidth((ix1 - ix0) * z) t:SetHeight((iy1 - iy0) * z) t:SetTexCoord(u0 + (ix0 - x) * du, u0 + (ix1 - x) * du, v0 + (iy0 - y) * dv, v0 + (iy1 - y) * dv) t:SetAlpha(1) end end -- Explored areas (WorldMapOverlay, laid out by WorldMapFrame_Update in map units from the -- map's top left): each piece's place, size and tex coords, read after the client sets them local function overlay(i) return _G["WorldMapOverlay" .. i] end local function recordOverlays() for i = 1, NUM_WORLDMAP_OVERLAYS or 0 do local o = overlay(i) if o and o:IsShown() then local _, _, _, x, y = o:GetPoint(1) local ulx, uly, llx, lly, urx = o:GetTexCoord() o.foreverZoomArt = { x or 0, -(y or 0), o:GetWidth(), o:GetHeight(), ulx or 0, urx or 1, uly or 0, lly or 1 } elseif o then o.foreverZoomArt = nil end end end local function restoreOverlays() for i = 1, NUM_WORLDMAP_OVERLAYS or 0 do local o = overlay(i) local a = o and o.foreverZoomArt if a then o:ClearAllPoints() o:SetPoint("TOPLEFT", WorldMapDetailFrame, "TOPLEFT", a[1], -a[2]) o:SetWidth(a[3]) o:SetHeight(a[4]) o:SetTexCoord(a[5], a[6], a[7], a[8]) o:SetAlpha(1) o.foreverZoomArt = nil end end end local function cropTexture() local x0, y0, x1, y1 = view() for rowLine = 1, ROWS do for column = 1, COLUMNS do local t = tile((rowLine - 1) * COLUMNS + column) if t then cropRegion(t, (column - 1) * TILE, (rowLine - 1) * TILE, TILE, TILE, 0, 1, 0, 1, x0, y0, x1, y1) end end end for i = 1, NUM_WORLDMAP_OVERLAYS or 0 do local o = overlay(i) local a = o and o.foreverZoomArt if a and o:IsShown() then cropRegion(o, a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], x0, y0, x1, y1) end end end -- Tile anchors from WorldMapFrame.xml (541-636): the first at the corner, each right of the -- previous one, each row under the first tile of the row above; then the small-window crop -- (WorldMap.lua) local function rechainTiles() for k = 1, COLUMNS * ROWS do local t = tile(k) if t then t:ClearAllPoints() t:SetAlpha(1) if k == 1 then t:SetPoint("TOPLEFT", WorldMapDetailFrame, "TOPLEFT", 0, 0) elseif (k - 1) % COLUMNS == 0 then t:SetPoint("TOPLEFT", tile(k - COLUMNS), "BOTTOMLEFT", 0, 0) else t:SetPoint("TOPLEFT", tile(k - 1), "TOPRIGHT", 0, 0) end end end if W.cropTiles then W.cropTiles(true) end end -- --------------------------------------------------------------- Markers -- A marker out of the view fades out and releases the mouse (invisible, it would still -- steal hover and clicks from the quest log or the frame); back in view, it gets its alpha -- and mouse back local function turnOff(f) f.foreverZoomAlpha = f:GetAlpha() f.foreverZoomMouse = f.IsMouseEnabled and f:IsMouseEnabled() and true or false f:SetAlpha(0) if f.foreverZoomMouse then f:EnableMouse(false) end end local function restore(f) f:SetAlpha(f.foreverZoomAlpha) if f.foreverZoomMouse then f:EnableMouse(true) end f.foreverZoomAlpha, f.foreverZoomMouse = nil, nil end -- Fade out the markers outside the view, restore those back inside local function sortBy() local c = W.canvas local e = c:GetEffectiveScale() local left, down, right, top = c:GetLeft(), c:GetBottom(), c:GetRight(), c:GetTop() if not (left and down and right and top) then return end left, down, right, top = left * e, down * e, right * e, top * e for _, parent in ipairs({ WorldMapButton, WorldMapPOIFrame }) do if parent then for _, f in ipairs({ parent:GetChildren() }) do if f ~= WorldMapFrameAreaFrame and f:IsShown() then local x, y = f:GetCenter() local fe = f:GetEffectiveScale() local inside = not x or (x * fe >= left and x * fe <= right and y * fe >= down and y * fe <= top) if inside then if f.foreverZoomAlpha then restore(f) end elseif not f.foreverZoomAlpha then turnOff(f) end end end end end end local function restoreChildren() for _, parent in ipairs({ WorldMapButton, WorldMapPOIFrame }) do if parent then for _, f in ipairs({ parent:GetChildren() }) do if f.foreverZoomAlpha then restore(f) end end end end end -- Quest POIs (children of WorldMapPOIFrame, 32 x 32 at its scale) keep on screen the size -- they have in the small window: the full screen scales WorldMapFrame up to fit the map to the -- screen, which doubled them. Their scale is the small window's over the current one. local function markerScale(parent) if parent ~= WorldMapPOIFrame or not W.reducedScale or not UIParent then return 1 end local now = WorldMapPOIFrame:GetEffectiveScale() / UIParent:GetEffectiveScale() if not now or now <= 0 then return 1 end return W.reducedScale() / now end -- Markers anchored to the map's top left. Their offsets (from the client, in their parent's -- units) are kept as a base at zoom 1, then written x zoom / their scale: landmarks and quest -- POIs placed once follow the zoom, quest POIs keep their size. Markers the client places on -- each frame (units) already follow the map's width. foreverZoomSet remembers what was -- written here, to tell it from offsets the client wrote since, at zoom clientZoom. local function placeMarkers(clientZoom) local d, b, p = WorldMapDetailFrame, WorldMapButton, WorldMapPOIFrame for _, parent in ipairs({ b, p, d }) do if parent then local m = markerScale(parent) for _, f in ipairs({ parent:GetChildren() }) do if f ~= b and f ~= p and f ~= WorldMapBlobFrame and f ~= WorldMapFrameAreaFrame and f:GetNumPoints() == 1 then local point, rel, relPoint, x, y = f:GetPoint(1) if (rel == d or rel == b or rel == p) and relPoint == "TOPLEFT" and x and y then local s = f.foreverZoomSet local bx, by if s and math.abs(s[1] - x) < 1e-3 and math.abs(s[2] - y) < 1e-3 then bx, by = s[3], s[4] else bx, by = x / clientZoom, y / clientZoom end if parent == p and math.abs((f:GetScale() or 1) - m) > 1e-6 then f:SetScale(m) end local nx, ny = bx * Z.z / m, by * Z.z / m if math.abs(nx - x) > 1e-3 or math.abs(ny - y) > 1e-3 then f:ClearAllPoints() f:SetPoint(point, rel, relPoint, nx, ny) end f.foreverZoomSet = { nx, ny, bx, by } end end end end end end Z.placeMarkers = placeMarkers -- ------------------------------------------------------------- Apply -- Current map key: a map change resets the zoom to 1 function Z.mapKey() return tostring(GetCurrentMapAreaID and GetCurrentMapAreaID()) .. ":" .. tostring(GetCurrentMapDungeonLevel and GetCurrentMapDungeonLevel()) end -- The enlarged frames: WorldMapDetailFrame and WorldMapButton (siblings, same scale and corner) local function enlarge() for f, s in pairs(Z.sizes) do f:SetWidth(s[1] * Z.z) f:SetHeight(s[2] * Z.z) end end -- The quest blob (WorldMapBlobFrame) is drawn by the engine in one piece and would overflow, -- so it hides while zoomed; WorldMapButton's hit rect is limited to the view. -- old: the zoom before this change (markers placed once follow it) function Z.apply(old) local d = WorldMapDetailFrame if not Z.active then Z.active = true Z.points = {} for i = 1, d:GetNumPoints() do Z.points[i] = { d:GetPoint(i) } end Z.sizes = { [d] = { d:GetWidth(), d:GetHeight() }, [WorldMapButton] = { WorldMapButton:GetWidth(), WorldMapButton:GetHeight() }, } Z.key = Z.mapKey() recordOverlays() old = 1 end old = old or Z.z local z = Z.z enlarge() clampView() d:ClearAllPoints() d:SetPoint("CENTER", W.canvas, "CENTER", (0.5 - Z.cx) * CARD_W * z, (Z.cy - 0.5) * CARD_H * z) if WorldMapBlobFrame then WorldMapBlobFrame:SetAlpha(0) if WorldMapBlobFrame_CalculateHitTranslations then WorldMapBlobFrame_CalculateHitTranslations() end end local a = WorldMapFrameAreaFrame if a then a:ClearAllPoints() a:SetPoint("TOP", W.canvas, "TOP", 0, -10) end local x0, y0, x1, y1 = view() WorldMapButton:SetHitRectInsets(math.max(0, x0) * z, math.max(0, CARD_W - x1) * z, math.max(0, y0) * z, math.max(0, CARD_H - y1) * z) cropTexture() placeMarkers(old) sortBy() end -- Back to the whole map. After a mode change, the client and WorldMap.lua have just set -- their own anchors: the map's is kept then. function Z.reset(isModeChange) if not Z.active then Z.z = 1 return end local old = Z.z Z.z = 1 Z.active = false enlarge() local d = WorldMapDetailFrame if not isModeChange and Z.points then d:ClearAllPoints() for _, p in ipairs(Z.points) do d:SetPoint(p[1], p[2], p[3], p[4], p[5]) end end if WorldMapBlobFrame then WorldMapBlobFrame:SetAlpha(1) if WorldMapBlobFrame_CalculateHitTranslations then WorldMapBlobFrame_CalculateHitTranslations() end end local a = WorldMapFrameAreaFrame if a then a:ClearAllPoints() a:SetPoint("TOP", WorldMapButton, "TOP", 0, -10) end WorldMapButton:SetHitRectInsets(0, 0, 0, 0) WorldMapButton:EnableMouse(true) Z.pressPoint, Z.dragging = nil, false rechainTiles() restoreOverlays() placeMarkers(old) restoreChildren() Z.cx, Z.cy = 0.5, 0.5 end -- ------------------------------------------------------------- Mouse wheel -- Map point under the cursor, and the cursor offset from the canvas center in screen pixels local function pointUnderCursor() local x, y = GetCursorPosition() local c = W.canvas local cx, cy = c:GetCenter() if not cx then return nil end local ce = c:GetEffectiveScale() local dx, dy = x - cx * ce, y - cy * ce local de = pixelsPerUnit() return Z.cx + dx / (CARD_W * de), Z.cy - dy / (CARD_H * de), dx, dy, de end function Z.wheel(direction) if not ready() then return end local old = Z.z local new = direction > 0 and old * Z.STEP or old / Z.STEP if new > Z.MAX then new = Z.MAX end if new < 1.0001 then new = 1 end if new == old then return end if new == 1 then Z.reset(false) return end if not Z.active then Z.cx, Z.cy = 0.5, 0.5 end -- Keep the point under the cursor in place (camelot: SetPanTarget) local fx, fy, dx, dy, de = pointUnderCursor() Z.z = new if fx then local de2 = de * new / old Z.cx = fx - dx / (CARD_W * de2) Z.cy = fy + dy / (CARD_H * de2) end Z.apply(old) end -- --------------------------------------------------------------- Watcher local mapKey = Z.mapKey -- Child of the map frame, so it only runs while the map is open local watcher = CreateFrame("Frame", nil, WorldMapFrame) watcher:SetScript("OnUpdate", function(_, elapsed) -- Markers the client placed since the last pass (ten passes a second) Z.markWait = (Z.markWait or 0) - elapsed if Z.markWait <= 0 and ready() and not Z.dragging then Z.markWait = 0.1 placeMarkers(Z.z) end if not (Z.active and ready()) then Z.key = ready() and mapKey() or nil return end if mapKey() ~= Z.key then Z.reset(false) Z.key = mapKey() return end -- Drag: past THRESHOLD pixels the map follows the cursor. WorldMapButton_OnClick navigates -- on mouse up, so WorldMapButton releases the mouse until the button is up. The client code -- is not wrapped: it closes dropdowns and would taint their state if called from the addon. local pressPoint = Z.pressPoint if pressPoint then local x, y = GetCursorPosition() if not IsMouseButtonDown("LeftButton") then Z.pressPoint = nil if Z.dragging then Z.dragging = false WorldMapButton:EnableMouse(true) end else if not Z.dragging and math.abs(x - pressPoint.x) + math.abs(y - pressPoint.y) > Z.THRESHOLD then Z.dragging = true WorldMapButton:EnableMouse(false) end if Z.dragging then local de = pixelsPerUnit() Z.cx = pressPoint.cx - (x - pressPoint.x) / (CARD_W * de) Z.cy = pressPoint.cy + (y - pressPoint.y) / (CARD_H * de) Z.apply() return end end end -- Markers move with their units: ten passes a second are enough (Z.apply sorts at once). -- Each pass reads every child of the map buttons (about sixty) and builds three tables. Z.sortWait = (Z.sortWait or 0) - elapsed if Z.sortWait <= 0 then Z.sortWait = 0.1 sortBy() end end) Z.watcher = watcher -- ------------------------------------------------------------ Hooks if WorldMapButton then WorldMapButton:EnableMouseWheel(true) WorldMapButton:SetScript("OnMouseWheel", function(_, direction) Z.wheel(direction) end) WorldMapButton:HookScript("OnMouseDown", function(_, button) if button == "LeftButton" and Z.active then local x, y = GetCursorPosition() Z.pressPoint = { x = x, y = y, cx = Z.cx, cy = Z.cy } Z.dragging = false end end) -- Player arrow: WorldMapButton_OnUpdate places it (position x the map's width x -- WORLDMAP_SETTINGS.size); place it again, or hide it out of the view WorldMapButton:HookScript("OnUpdate", function() if not Z.active then return end local x, y = GetPlayerMapPosition("player") if not x or (x == 0 and y == 0) then return end local px, py = x * CARD_W, y * CARD_H local x0, y0, x1, y1 = view() if px < x0 or px > x1 or py < y0 or py > y1 then if ShowWorldMapArrowFrame then ShowWorldMapArrowFrame(nil) end elseif PositionWorldMapArrowFrame then local s = WORLDMAP_SETTINGS.size * Z.z PositionWorldMapArrowFrame("CENTER", "WorldMapDetailFrame", "TOPLEFT", px * s, -py * s) end end) end if WorldMapFrame then WorldMapFrame:HookScript("OnHide", function() Z.reset(false) end) end -- WorldMapFrame_Update reloads the tiles and lays out the explored areas again -- (WORLD_MAP_UPDATE): crop them again while zoomed if WorldMapFrame_Update then hooksecurefunc("WorldMapFrame_Update", function() if Z.active then recordOverlays() cropTexture() end if ready() then placeMarkers(Z.z) end end) end -- The quest POIs, placed again on each quest update if WorldMapFrame_DisplayQuests then hooksecurefunc("WorldMapFrame_DisplayQuests", function() if ready() then placeMarkers(Z.z) end end) end -- Mode or view change: back to 1, without undoing what the client has just set for _, name in ipairs({ "WorldMap_ToggleSizeDown", "WorldMap_ToggleSizeUp", "WorldMapFrame_SetMiniMode", "WorldMapFrame_SetQuestMapView", "WorldMapFrame_SetFullMapView" }) do if _G[name] then hooksecurefunc(name, function() Z.reset(true) if ready() then placeMarkers(Z.z) end end) end end