diff --git a/frontend/src/utils/gameView.test.ts b/frontend/src/utils/gameView.test.ts
index 09f06c3..b218a9c 100644
--- a/frontend/src/utils/gameView.test.ts
+++ b/frontend/src/utils/gameView.test.ts
@@ -31,20 +31,20 @@ describe('gameViewGeometry', () => {
])
})
- it('uses the tablet camera zoom levels around the crop center', () => {
+ it('keeps 0.5x full-frame while higher zoom levels crop around the center', () => {
const wideGeometry = gameViewGeometry(1920, 1080, 540, 720, 0.5)
expect(Array.from(wideGeometry.textureCoordinates)).toEqual([
- expect.closeTo(0.08, 2),
+ expect.closeTo(0.29, 2),
0,
- expect.closeTo(0.92, 2),
+ expect.closeTo(0.71, 2),
0,
- expect.closeTo(0.08, 2),
+ expect.closeTo(0.29, 2),
1,
- expect.closeTo(0.92, 2),
+ expect.closeTo(0.71, 2),
1,
])
expect(Array.from(wideGeometry.positions)).toEqual([
- -1, -0.5, 1, -0.5, -1, 0.5, 1, 0.5,
+ -1, -1, 1, -1, -1, 1, 1, 1,
])
const zoomedGeometry = gameViewGeometry(1920, 1080, 540, 720, 2)
diff --git a/frontend/src/utils/gameView.ts b/frontend/src/utils/gameView.ts
index b23e492..9dd0ef1 100644
--- a/frontend/src/utils/gameView.ts
+++ b/frontend/src/utils/gameView.ts
@@ -58,9 +58,7 @@ export function gameViewGeometry(
bottom = 1 - top
}
- const baseWidth = right - left
- const baseHeight = bottom - top
- const normalizedZoom = Math.min(3, Math.max(0.5, zoom))
+ const normalizedZoom = Math.min(3, Math.max(1, zoom))
const centerX = (left + right) / 2
const centerY = (top + bottom) / 2
left = Math.max(0, centerX + (left - centerX) / normalizedZoom)
@@ -68,26 +66,8 @@ export function gameViewGeometry(
top = Math.max(0, centerY + (top - centerY) / normalizedZoom)
bottom = Math.min(1, centerY + (bottom - centerY) / normalizedZoom)
- const positionScaleX = Math.min(
- 1,
- (normalizedZoom * (right - left)) / baseWidth,
- )
- const positionScaleY = Math.min(
- 1,
- (normalizedZoom * (bottom - top)) / baseHeight,
- )
-
return {
- positions: new Float32Array([
- -positionScaleX,
- -positionScaleY,
- positionScaleX,
- -positionScaleY,
- -positionScaleX,
- positionScaleY,
- positionScaleX,
- positionScaleY,
- ]),
+ positions: new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
textureCoordinates: new Float32Array([
left,
top,
diff --git a/frontend/src/views/apps/CameraApp.vue b/frontend/src/views/apps/CameraApp.vue
index 7fa7fd8..dce9f32 100644
--- a/frontend/src/views/apps/CameraApp.vue
+++ b/frontend/src/views/apps/CameraApp.vue
@@ -232,6 +232,7 @@ function setZoom(zoom: (typeof zoomLevels)[number]): void {
selectedZoom.value = zoom
resizeGameView()
window.postMessage({ data: { zoom }, type: 'camera:zoom' }, '*')
+ void nuiCall('camera:setZoom', { zoom })
}
function resizeGameView(entry?: ResizeObserverEntry): void {
@@ -392,7 +393,7 @@ onBeforeUnmount(() => {
diff --git a/sky_phone/source/client/camera.lua b/sky_phone/source/client/camera.lua
index f6f5993..ca78183 100644
--- a/sky_phone/source/client/camera.lua
+++ b/sky_phone/source/client/camera.lua
@@ -2,6 +2,7 @@ local first_person_view_mode = 4
local third_person_view_mode = 1
local front_camera_view_mode = 0
local front_camera_fov = 25.0
+local ultrawide_fov_multiplier = 2.0
local front_camera_distance = 0.75
local front_camera_height = 0.05
local front_camera_target_height = 0.03
@@ -13,10 +14,12 @@ local camera_state = {
focus_watcher = false,
front_camera = false,
front_camera_handle = nil,
+ ultrawide_camera_handle = nil,
nui_focused = true,
previous_ped_view = nil,
previous_radar_hidden = nil,
previous_vehicle_view = nil,
+ zoom = 1.0,
}
local function rotation_to_direction(rotation)
@@ -77,11 +80,50 @@ local function ensure_front_camera(ped)
return
end
camera_state.front_camera_handle = CreateCam("DEFAULT_SCRIPTED_CAMERA", true)
- SetCamFov(camera_state.front_camera_handle, front_camera_fov)
+ SetCamFov(
+ camera_state.front_camera_handle,
+ camera_state.zoom == 0.5 and front_camera_fov * ultrawide_fov_multiplier or front_camera_fov
+ )
SetCamActive(camera_state.front_camera_handle, true)
RenderScriptCams(true, false, 0, true, true)
end
+local function ensure_ultrawide_camera()
+ if camera_state.ultrawide_camera_handle and DoesCamExist(camera_state.ultrawide_camera_handle) then
+ return
+ end
+ local camera_coords = GetGameplayCamCoord()
+ local camera_rotation = GetGameplayCamRot(2)
+ camera_state.ultrawide_camera_handle = CreateCam("DEFAULT_SCRIPTED_CAMERA", true)
+ SetCamCoord(
+ camera_state.ultrawide_camera_handle,
+ camera_coords.x,
+ camera_coords.y,
+ camera_coords.z
+ )
+ SetCamRot(
+ camera_state.ultrawide_camera_handle,
+ camera_rotation.x,
+ camera_rotation.y,
+ camera_rotation.z,
+ 2
+ )
+ SetCamFov(
+ camera_state.ultrawide_camera_handle,
+ math.min(GetGameplayCamFov() * ultrawide_fov_multiplier, 120.0)
+ )
+ SetCamActive(camera_state.ultrawide_camera_handle, true)
+ RenderScriptCams(true, false, 0, true, true)
+end
+
+local function clear_ultrawide_camera()
+ if camera_state.ultrawide_camera_handle and DoesCamExist(camera_state.ultrawide_camera_handle) then
+ RenderScriptCams(false, false, 0, true, true)
+ DestroyCam(camera_state.ultrawide_camera_handle, false)
+ end
+ camera_state.ultrawide_camera_handle = nil
+end
+
local function clear_front_camera()
if camera_state.front_camera_handle and DoesCamExist(camera_state.front_camera_handle) then
RenderScriptCams(false, false, 0, true, true)
@@ -139,7 +181,9 @@ local function set_camera_active(active)
camera_state.active = active
if active then
camera_state.front_camera = false
+ camera_state.zoom = 1.0
clear_front_camera()
+ clear_ultrawide_camera()
camera_state.previous_ped_view = GetFollowPedCamViewMode()
camera_state.previous_vehicle_view = GetFollowVehicleCamViewMode()
camera_state.previous_radar_hidden = IsRadarHidden()
@@ -165,6 +209,23 @@ local function set_camera_active(active)
camera_position.z
)
PointCamAtCoord(camera_state.front_camera_handle, target.x, target.y, target.z)
+ elseif camera_state.zoom == 0.5 then
+ ensure_ultrawide_camera()
+ local camera_coords = GetGameplayCamCoord()
+ local camera_rotation = GetGameplayCamRot(2)
+ SetCamCoord(
+ camera_state.ultrawide_camera_handle,
+ camera_coords.x,
+ camera_coords.y,
+ camera_coords.z
+ )
+ SetCamRot(
+ camera_state.ultrawide_camera_handle,
+ camera_rotation.x,
+ camera_rotation.y,
+ camera_rotation.z,
+ 2
+ )
end
local now = GetGameTimer()
if now >= next_apply then
@@ -180,6 +241,7 @@ local function set_camera_active(active)
camera_state.flash_enabled = false
camera_state.front_camera = false
clear_front_camera()
+ clear_ultrawide_camera()
restore_camera_view()
if not camera_state.nui_focused then
camera_state.nui_focused = true
@@ -197,13 +259,39 @@ local function set_front_camera(active)
return
end
if active then
+ clear_ultrawide_camera()
ensure_front_camera(PlayerPedId())
else
clear_front_camera()
+ if camera_state.zoom == 0.5 then
+ ensure_ultrawide_camera()
+ end
end
apply_camera_view()
end
+local function set_camera_zoom(zoom)
+ if zoom ~= 0.5 and zoom ~= 1.0 and zoom ~= 2.0 and zoom ~= 3.0 then
+ return false
+ end
+ camera_state.zoom = zoom
+ if not camera_state.active then
+ return true
+ end
+ if camera_state.front_camera then
+ ensure_front_camera(PlayerPedId())
+ SetCamFov(
+ camera_state.front_camera_handle,
+ zoom == 0.5 and front_camera_fov * ultrawide_fov_multiplier or front_camera_fov
+ )
+ elseif zoom == 0.5 then
+ ensure_ultrawide_camera()
+ else
+ clear_ultrawide_camera()
+ end
+ return true
+end
+
RegisterNUICallback("camera:setActive", function(data, cb)
set_camera_active(data and data.active == true)
cb({ success = true })
@@ -226,6 +314,10 @@ RegisterNUICallback("camera:setFacing", function(data, cb)
cb({ success = true })
end)
+RegisterNUICallback("camera:setZoom", function(data, cb)
+ cb({ success = set_camera_zoom(tonumber(data and data.zoom)) })
+end)
+
RegisterNUICallback("media:requestUpload", function(data, cb)
TriggerServerEvent("sky_phone:media:request-upload", data or {})
cb({ success = true })