diff --git a/applications/metadata.js b/applications/metadata.js index 055103a..cb14853 100644 --- a/applications/metadata.js +++ b/applications/metadata.js @@ -152,6 +152,15 @@ var metadata = { "applications": "jsfile": "home/app_home.js", "icon": "home/appicon_i.png", "caption": "HOME" + }, + { + "isActive": true, + "directory": "tabletCam", + "name": "Camera Snap-Pro", + "description": "The Camera 'Snap-Pro' allows you to take high quality in-world photos and selfies (Low, Normal, 4K, and 'EXTREME' resolution). It has two cameras on the tablet, front-facing/rear-facing, and one detachable to allow more flexibility. It supports different aspect ratio formats: 8x10, 2x3, 9x16 and 'Square'. It comes with an Optical Zoom, a trigger on the VR hand controller and other features.", + "jsfile": "tabletCam/tabletCam_app.js", + "icon": "tabletCam/appIcons/snap-pro-i.svg", + "caption": "SNAP-PRO" } ] }; diff --git a/applications/tabletCam/README.md b/applications/tabletCam/README.md new file mode 100644 index 0000000..97a1601 --- /dev/null +++ b/applications/tabletCam/README.md @@ -0,0 +1,77 @@ +# Tablet Cam + +## Note + +This was a personal project by [Zach Fox](https://github.com/zfox23/), a member of the High Fidelity Experiences Team. It has not undergone the same code review or QA process as the rest of the projects in this repository. + +## Description + +The Tablet Cam app allows you to **easily take selfies and regular photos in High Fidelity/Overte** using your Tablet or hand controllers! + +## Features + +- **Front-facing and rear-facing** cameras and flashes with optional **custom positioning** +- Works in **Desktop Mode and in VR Mode** +- **Persistent "Camera Roll"** for reviewing photos that you recently took +- Optical **zoom** +- Photo **quality settings**: Low, Normal, 4k, and _EXTREME_! +- **Aspect ratio settings**: 8x10, 2x3, 9x16, Square +- Editable photo directory + +## Changelog + +### v2.4 (2022-08-22) by Alezia Kurdis + +- Fixed a but in HMD where the rear-facing camera was pointed to the tablet instead of the scene. +- Replaced the camera model by a new one (nicer and less heavy, 198k instead of 576k, less but more optimal polygons, 1 PBR material.) +- Renamed the tool: Camera Snap-Pro +- Replaced the icons for something more easy to identify as a device to take picture ("camera"), as an advance version of the "Snap" app. +- Add a notice in the settings page to informe the user about the trigger on the thumbstick of the right VR hand controller. + +### v2.3 (2019-10-15) + +- Removed unnecessary tone curve correction present in captured Tablet Cam output image. +- Adjusted y-offset of Desktop-mode selfie cam to better serve Virtual You avatars. +- Added `"Head"` as a backup joint to `"HeadTop_End"` for certain operations. + +### v2.2 (2019-07-09) + +- Fixed a hack in which the secondary camera feed was darkened to compensate for it being rendered too light. +- If your camera looks too light, it means you have an older version of Interface (pre PR #15862) and should go back to the previous version of this script. +- If your camera looks too dark, it means you have a newer version of Interface (post PR #15682) and should update to the current version of this script. + +### v2.1 (2019-06-27) + +- We've moved the code for this project into a new remote folder, which necessitated a version bump. There are no changes to functionality. + +### v2.0 (2019-04) + +**I've made some huge changes in v2.0!** + +- In Desktop mode, when using the rear-facing camera and while you're using Third Person Camera, Tablet Cam will now be parented to Interface's Third Person Camera! +- The camera's viewpoint can now be detached from its default position! Snap a photo from a unique viewpoint. +- Fixed a bug that caused zoom settings to be saved incorrectly between restarts. +- Fixed a bug that caused the camera preview to show up as a corrupted image in HMD mode. +- Fixed a bug that caused the position of the rear-facing camera to be incorrect in HMD mode. +- Switched Overlay usage to Local Entities. +- Now you can use Tablet Cam in Desktop mode while "Desktop Tablet becomes Toolbar" is unchecked - although I'm not sure why you'd want to do that. :) +- Fixed some interface bugs. + +### v1.1 (2019-02) + +- Fixed a bug that caused Tablet Cam to erroneously appear on the Tablet after switching domains when it was previously active. + +### Tablet Cam v1.0 (2019-01) + +- Tablet Cam v1.0 is an update to Selfie Cam v1.0. It is a complete overhaul of the app. All of its features are new! + +### Selfie Cam v1.0 (2018-12) + +- Initial Release! + +## Attributions +- "snap.wav" from "Camera Shutter, Fast, A.wav" by InspectorJ (www.jshaw.co.uk) of Freesound.org +- "switchCams.svg" from "rotate camera" by Diego Naive from the Noun Project +- "orientation.svg" from "orientation" by Atif Arshad from the Noun Project +- "camera.fbx" from "Digital camera" by Nick Ladd: https://poly.google.com/view/4A3SYVh_smq +- "camera-a.svg" and "camera-i.svg" from "Selfie" by Path Lord from the Noun Project \ No newline at end of file diff --git a/applications/tabletCam/appIcons/snap-pro-a.svg b/applications/tabletCam/appIcons/snap-pro-a.svg new file mode 100644 index 0000000..69f9dd8 --- /dev/null +++ b/applications/tabletCam/appIcons/snap-pro-a.svg @@ -0,0 +1,226 @@ + + + + + snap-pro-a + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + snap-pro-a + + + + diff --git a/applications/tabletCam/appIcons/snap-pro-i.svg b/applications/tabletCam/appIcons/snap-pro-i.svg new file mode 100644 index 0000000..9559317 --- /dev/null +++ b/applications/tabletCam/appIcons/snap-pro-i.svg @@ -0,0 +1,226 @@ + + + + + snap-pro-i + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + snap-pro-i + + + + diff --git a/applications/tabletCam/models/camera.fbx b/applications/tabletCam/models/camera.fbx new file mode 100644 index 0000000..5c63092 Binary files /dev/null and b/applications/tabletCam/models/camera.fbx differ diff --git a/applications/tabletCam/modules/appUi.js b/applications/tabletCam/modules/appUi.js new file mode 100644 index 0000000..5b2904a --- /dev/null +++ b/applications/tabletCam/modules/appUi.js @@ -0,0 +1,388 @@ +"use strict"; +/* global Tablet, Script */ +// +// libraries/appUi.js +// +// Created by Howard Stearns on 3/20/18. +// Modified by Zach Fox on 2019-04-13 +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +function AppUi(properties) { + var request = Script.require('request').request; + /* Example development order: + 1. var AppUi = Script.require('appUi'); + 2. Put appname-i.svg, appname-a.svg in graphicsDirectory (where non-default graphicsDirectory can be added in #3). + 3. ui = new AppUi({buttonName: "APPNAME", home: "qml-or-html-path"}); + (And if converting an existing app, + define var tablet = ui.tablet, button = ui.button; as needed. + remove button.clicked.[dis]connect and tablet.remove(button).) + 4. Define onOpened and onClosed behavior in #3, if any. + (And if converting an existing app, remove screenChanged.[dis]connect.) + 5. Define onMessage and sendMessage in #3, if any. onMessage is wired/unwired on open/close. If you + want a handler to be "always on", connect it yourself at script startup. + (And if converting an existing app, remove code that [un]wires that message handling such as + fromQml/sendToQml or webEventReceived/emitScriptEvent.) + 6. (If converting an existing app, cleanup stuff that is no longer necessary, like references to button, tablet, + and use isOpen, open(), and close() as needed.) + 7. lint! + */ + var that = this; + function defaultButton(name, suffix) { + var base = that[name] || (that.buttonPrefix + suffix); + that[name] = (base.indexOf('/') >= 0) ? base : (that.graphicsDirectory + base); // poor man's merge + } + + // Defaults: + that.tabletName = "com.highfidelity.interface.tablet.system"; + that.inject = ""; + that.graphicsDirectory = "icons/tablet-icons/"; // Where to look for button svgs. See below. + that.additionalAppScreens = []; + that.checkIsOpen = function checkIsOpen(type, tabletUrl) { // Are we active? Value used to set isOpen. + // Actual url may have prefix or suffix. + return that.currentVisibleUrl && + ((that.home.indexOf(that.currentVisibleUrl) > -1) || + (that.additionalAppScreens.indexOf(that.currentVisibleUrl) > -1)); + }; + that.setCurrentVisibleScreenMetadata = function setCurrentVisibleScreenMetadata(type, url) { + that.currentVisibleScreenType = type; + that.currentVisibleUrl = url; + }; + that.open = function open(optionalUrl, optionalInject) { // How to open the app. + var url = optionalUrl || that.home; + var inject = optionalInject || that.inject; + + if (that.isQMLUrl(url)) { + that.tablet.loadQMLSource(url); + } else { + that.tablet.gotoWebScreen(url, inject); + } + }; + // Opens some app on top of the current app (on desktop, opens new window) + that.openNewAppOnTop = function openNewAppOnTop(url, optionalInject) { + var inject = optionalInject || ""; + if (that.isQMLUrl(url)) { + that.tablet.loadQMLOnTop(url); + } else { + that.tablet.loadWebScreenOnTop(url, inject); + } + }; + that.close = function close() { // How to close the app. + that.currentVisibleUrl = ""; + // for toolbar-mode: go back to home screen, this will close the window. + that.tablet.gotoHomeScreen(); + }; + that.buttonActive = function buttonActive(isActive) { // How to make the button active (white). + that.button.editProperties({isActive: isActive}); + }; + that.isQMLUrl = function isQMLUrl(url) { + var type = /.qml$/.test(url) ? 'QML' : 'Web'; + return type === 'QML'; + }; + that.isCurrentlyOnQMLScreen = function isCurrentlyOnQMLScreen() { + return that.currentVisibleScreenType === 'QML'; + }; + + // + // START Notification Handling Defaults + // + that.messagesWaiting = function messagesWaiting(isWaiting) { // How to indicate a message light on button. + // Note that waitingButton doesn't have to exist unless someone explicitly calls this with isWaiting true. + that.button.editProperties({ + icon: isWaiting ? that.normalMessagesButton : that.normalButton, + activeIcon: isWaiting ? that.activeMessagesButton : that.activeButton + }); + }; + that.notificationPollTimeout = [false]; + that.notificationPollTimeoutMs = [60000]; + that.notificationPollEndpoint = [false]; + that.notificationPollStopPaginatingConditionMet = [false]; + that.notificationDataProcessPage = function (data) { + return data; + }; + that.notificationPollCallback = [that.ignore]; + that.notificationPollCaresAboutSince = [false]; + that.notificationInitialCallbackMade = [false]; + that.notificationDisplayBanner = function (message) { + if (!that.isOpen) { + Window.displayAnnouncement(message); + } + }; + // + // END Notification Handling Defaults + // + + // Handlers + that.onScreenChanged = function onScreenChanged(type, url) { + // Set isOpen, wireEventBridge, set buttonActive as appropriate, + // and finally call onOpened() or onClosed() IFF defined. + that.setCurrentVisibleScreenMetadata(type, url); + + if (that.checkIsOpen(type, url)) { + that.wireEventBridge(true); + if (!that.isOpen) { + if (that.onOpened) { + that.onOpened(); + } + that.buttonActive(true); + that.isOpen = true; + } + } else { + // A different screen is now visible, or the tablet has been closed. + // Tablet visibility is controlled separately by `tabletShownChanged()` + that.wireEventBridge(false); + if (that.isOpen) { + if (that.onClosed) { + that.onClosed(); + } + that.buttonActive(false); + that.isOpen = false; + } + } + }; + + // Overwrite with the given properties: + Object.keys(properties).forEach(function (key) { + that[key] = properties[key]; + }); + + // + // START Notification Handling + // + + var currentDataPageToRetrieve = []; + var concatenatedServerResponse = []; + for (var i = 0; i < that.notificationPollEndpoint.length; i++) { + currentDataPageToRetrieve[i] = 1; + concatenatedServerResponse[i] = new Array(); + } + + var MAX_LOG_LENGTH_CHARACTERS = 300; + function requestCallback(error, response, optionalParams) { + var indexOfRequest = optionalParams.indexOfRequest; + var urlOfRequest = optionalParams.urlOfRequest; + + if (error || (response.status !== 'success')) { + print("Error: unable to complete request from URL. Error:", error || response.status); + startNotificationTimer(indexOfRequest); + return; + } + + if (!that.notificationPollStopPaginatingConditionMet[indexOfRequest] || + that.notificationPollStopPaginatingConditionMet[indexOfRequest](response)) { + startNotificationTimer(indexOfRequest); + + var notificationData; + if (concatenatedServerResponse[indexOfRequest].length) { + notificationData = concatenatedServerResponse[indexOfRequest]; + } else { + notificationData = that.notificationDataProcessPage[indexOfRequest](response); + } + console.debug(that.buttonName, + 'truncated notification data for processing:', + JSON.stringify(notificationData).substring(0, MAX_LOG_LENGTH_CHARACTERS)); + that.notificationPollCallback[indexOfRequest](notificationData); + that.notificationInitialCallbackMade[indexOfRequest] = true; + currentDataPageToRetrieve[indexOfRequest] = 1; + concatenatedServerResponse[indexOfRequest] = new Array(); + } else { + concatenatedServerResponse[indexOfRequest] = + concatenatedServerResponse[indexOfRequest].concat(that.notificationDataProcessPage[indexOfRequest](response)); + currentDataPageToRetrieve[indexOfRequest]++; + request({ + json: true, + uri: (urlOfRequest + "&page=" + currentDataPageToRetrieve[indexOfRequest]) + }, requestCallback, optionalParams); + } + } + + + var METAVERSE_BASE = Account.metaverseServerURL; + var MS_IN_SEC = 1000; + that.notificationPoll = function (i) { + if (!that.notificationPollEndpoint[i]) { + return; + } + + // User is "appearing offline" or is not logged in + if (GlobalServices.findableBy === "none" || Account.username === "Unknown user") { + // The notification polling will restart when the user changes their availability + // or when they log in, so it's not necessary to restart a timer here. + console.debug(that.buttonName + " Notifications: User is appearing offline or not logged in. " + + that.buttonName + " will poll for notifications when user logs in and has their availability " + + "set to not appear offline."); + return; + } + + var url = METAVERSE_BASE + that.notificationPollEndpoint[i]; + + var settingsKey = "notifications/" + that.notificationPollEndpoint[i] + "/lastPoll"; + var currentTimestamp = new Date().getTime(); + var lastPollTimestamp = Settings.getValue(settingsKey, currentTimestamp); + if (that.notificationPollCaresAboutSince[i]) { + url = url + "&since=" + lastPollTimestamp / MS_IN_SEC; + } + Settings.setValue(settingsKey, currentTimestamp); + + request({ + json: true, + uri: url + }, + requestCallback, + { + indexOfRequest: i, + urlOfRequest: url + }); + }; + + // This won't do anything if there isn't a notification endpoint set + for (i = 0; i < that.notificationPollEndpoint.length; i++) { + that.notificationPoll(i); + } + + function startNotificationTimer(indexOfRequest) { + that.notificationPollTimeout[indexOfRequest] = Script.setTimeout(function () { + that.notificationPoll(indexOfRequest); + }, that.notificationPollTimeoutMs[indexOfRequest]); + } + + function restartNotificationPoll() { + for (var j = 0; j < that.notificationPollEndpoint.length; j++) { + that.notificationInitialCallbackMade[j] = false; + if (that.notificationPollTimeout[j]) { + Script.clearTimeout(that.notificationPollTimeout[j]); + that.notificationPollTimeout[j] = false; + } + that.notificationPoll(j); + } + } + // + // END Notification Handling + // + + // Properties: + that.tablet = Tablet.getTablet(that.tabletName); + // Must be after we gather properties. + that.buttonPrefix = that.buttonPrefix || that.buttonName.toLowerCase() + "-"; + defaultButton('normalButton', 'i.svg'); + defaultButton('activeButton', 'a.svg'); + defaultButton('normalMessagesButton', 'i-msg.svg'); + defaultButton('activeMessagesButton', 'a-msg.svg'); + var buttonOptions = { + icon: that.normalButton, + activeIcon: that.activeButton, + text: that.buttonName + }; + // `TabletScriptingInterface` looks for the presence of a `sortOrder` key. + // What it SHOULD do is look to see if the value inside that key is defined. + // To get around the current code, we do this instead. + if (that.sortOrder) { + buttonOptions.sortOrder = that.sortOrder; + } + that.button = that.tablet.addButton(buttonOptions); + that.ignore = function ignore() { }; + that.hasOutboundEventBridge = false; + that.hasInboundQmlEventBridge = false; + that.hasInboundHtmlEventBridge = false; + // HTML event bridge uses strings, not objects. Here we abstract over that. + // (Although injected javascript still has to use JSON.stringify/JSON.parse.) + that.sendToHtml = function (messageObject) { + that.tablet.emitScriptEvent(JSON.stringify(messageObject)); + }; + that.fromHtml = function (messageString) { + var parsedMessage = JSON.parse(messageString); + parsedMessage.messageSrc = "HTML"; + that.onMessage(parsedMessage); + }; + that.sendMessage = that.ignore; + that.wireEventBridge = function wireEventBridge(on) { + // Uniquivocally sets that.sendMessage(messageObject) to do the right thing. + // Sets has*EventBridge and wires onMessage to the proper event bridge as appropriate, IFF onMessage defined. + var isCurrentlyOnQMLScreen = that.isCurrentlyOnQMLScreen(); + // Outbound (always, regardless of whether there is an inbound handler). + if (on) { + that.sendMessage = isCurrentlyOnQMLScreen ? that.tablet.sendToQml : that.sendToHtml; + that.hasOutboundEventBridge = true; + } else { + that.sendMessage = that.ignore; + that.hasOutboundEventBridge = false; + } + + if (!that.onMessage) { + return; + } + + // Inbound + if (on) { + if (isCurrentlyOnQMLScreen && !that.hasInboundQmlEventBridge) { + console.debug(that.buttonName, 'connecting', that.tablet.fromQml); + that.tablet.fromQml.connect(that.onMessage); + that.hasInboundQmlEventBridge = true; + } else if (!isCurrentlyOnQMLScreen && !that.hasInboundHtmlEventBridge) { + console.debug(that.buttonName, 'connecting', that.tablet.webEventReceived); + that.tablet.webEventReceived.connect(that.fromHtml); + that.hasInboundHtmlEventBridge = true; + } + } else { + if (that.hasInboundQmlEventBridge) { + console.debug(that.buttonName, 'disconnecting', that.tablet.fromQml); + that.tablet.fromQml.disconnect(that.onMessage); + that.hasInboundQmlEventBridge = false; + } + if (that.hasInboundHtmlEventBridge) { + console.debug(that.buttonName, 'disconnecting', that.tablet.webEventReceived); + that.tablet.webEventReceived.disconnect(that.fromHtml); + that.hasInboundHtmlEventBridge = false; + } + } + }; + that.isOpen = false; + // To facilitate incremental development, only wire onClicked to do something when "home" is defined in properties. + that.onClicked = that.home + ? function onClicked() { + // Call open() or close(), and reset type based on current home property. + if (that.isOpen) { + that.close(); + } else { + that.open(); + } + } : that.ignore; + that.onScriptEnding = function onScriptEnding() { + // Close if necessary, clean up any remaining handlers, and remove the button. + GlobalServices.myUsernameChanged.disconnect(restartNotificationPoll); + GlobalServices.findableByChanged.disconnect(restartNotificationPoll); + that.tablet.screenChanged.disconnect(that.onScreenChanged); + if (that.isOpen) { + that.close(); + that.onScreenChanged("", ""); + } + if (that.button) { + if (that.onClicked) { + that.button.clicked.disconnect(that.onClicked); + } + that.tablet.removeButton(that.button); + } + for (var i = 0; i < that.notificationPollTimeout.length; i++) { + if (that.notificationPollTimeout[i]) { + Script.clearInterval(that.notificationPollTimeout[i]); + that.notificationPollTimeout[i] = false; + } + } + }; + // Set up the handlers. + that.tablet.screenChanged.connect(that.onScreenChanged); + that.button.clicked.connect(that.onClicked); + Script.scriptEnding.connect(that.onScriptEnding); + GlobalServices.findableByChanged.connect(restartNotificationPoll); + GlobalServices.myUsernameChanged.connect(restartNotificationPoll); + if (that.buttonName === Settings.getValue("startUpApp")) { + Settings.setValue("startUpApp", ""); + Script.setTimeout(function () { + that.open(); + }, 1000); + } +} +module.exports = AppUi; diff --git a/applications/tabletCam/sounds/flashOff.wav b/applications/tabletCam/sounds/flashOff.wav new file mode 100644 index 0000000..fef7668 Binary files /dev/null and b/applications/tabletCam/sounds/flashOff.wav differ diff --git a/applications/tabletCam/sounds/flashOn.wav b/applications/tabletCam/sounds/flashOn.wav new file mode 100644 index 0000000..f7e95c9 Binary files /dev/null and b/applications/tabletCam/sounds/flashOn.wav differ diff --git a/applications/tabletCam/sounds/snap.wav b/applications/tabletCam/sounds/snap.wav new file mode 100644 index 0000000..d0143f1 Binary files /dev/null and b/applications/tabletCam/sounds/snap.wav differ diff --git a/applications/tabletCam/tabletCam_app.js b/applications/tabletCam/tabletCam_app.js new file mode 100644 index 0000000..a2c1939 --- /dev/null +++ b/applications/tabletCam/tabletCam_app.js @@ -0,0 +1,646 @@ +"use strict"; +/*jslint vars:true, plusplus:true, forin:true*/ +/*global Tablet, Script, */ +/* eslint indent: ["error", 4, { "outerIIFEBody": 1 }] */ +// +// tabletCam_app.js +// +// Created by Zach Fox on 2019-04-14 +// Copyright 2022 Overte e.V. +// +// Camera with more advanced features than the SNAP application, with an higher resolution capability. +// +// Distributed under the Apache License, Version 2.0 +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function () { // BEGIN LOCAL_SCOPE + var AppUi = Script.require('./modules/appUi.js'); + + var secondaryCameraConfig = Render.getConfig("SecondaryCamera"); + var tabletCamAvatarEntity = false; + var previousNearClipDistance = false; + var previousFarClipDistance = false; + var previousvFoV = false; + var NEAR_CLIP_DISTANCE = 0.001; + var FAR_CLIP_DISTANCE = 16384; + var vFoV = Settings.getValue("tabletCam/vFoV", 60); + var secondaryCameraResolutionWidth = 1000; + var secondaryCameraResolutionHeight = secondaryCameraResolutionWidth / aspectRatio; + + var PREVIEW_SHORT_SIDE_RESOLUTION = 400; + var secondaryCameraResolutionPreviewWidth = PREVIEW_SHORT_SIDE_RESOLUTION; + var secondaryCameraResolutionPreviewHeight = PREVIEW_SHORT_SIDE_RESOLUTION / aspectRatio; + + var CAMERA_ENTITY_NAME = "CAMERA SNAP-PRO OV-22"; + var APPLICATION_CAPTION = "SNAP-PRO"; + + var tabletCamRunning = false; + + var TABLET_CAM_ENTITY_PROPERTIES = { + "type": "Model", + "modelURL": Script.resolvePath("models/camera.fbx"), + "shapeType": "simple-hull", + "dimensions": {"x":0.1600, "y":0.1021, "z":0.1137}, + "damping": 0, + "angularDamping": 0, + "shape": "Cube", + "isVisibleInSecondaryCamera": false, + "name": CAMERA_ENTITY_NAME, + "grab": { + "grabbable": true + }, + "registrationPoint": { + "x": 0.42, + "y": 0.4, + "z": 0 + } + }; + + function enableTabletCam() { + if (!tabletCamRunning) { + wireSignals(true); + + setTakePhotoControllerMappingStatus(true); + + secondaryCameraConfig.enableSecondaryCameraRenderConfigs(true); + setSnapshotQuality(snapshotQuality); + var props = TABLET_CAM_ENTITY_PROPERTIES; + var dynamicProps = getDynamicTabletCamAvatarEntityProperties(); + for (var key in dynamicProps) { + props[key] = dynamicProps[key]; + } + tabletCamAvatarEntity = Entities.addEntity(props, "avatar"); + previousFarClipDistance = secondaryCameraConfig.farClipPlaneDistance; + previousNearClipDistance = secondaryCameraConfig.nearClipPlaneDistance; + previousvFoV = secondaryCameraConfig.vFoV; + secondaryCameraConfig.nearClipPlaneDistance = NEAR_CLIP_DISTANCE; + secondaryCameraConfig.farClipPlaneDistance = FAR_CLIP_DISTANCE; + secondaryCameraConfig.vFoV = vFoV; + + secondaryCameraConfig.attachedEntityId = tabletCamAvatarEntity; + tabletCamRunning = true; + } + + updateTabletCamLocalEntity(); + + // Remove the existing tabletCamAvatarEntity model from the domain if one exists. + // It's easy for this to happen if the user crashes while the Tablet Cam is on. + // We do this down here (after the new one is rezzed) so that we don't accidentally delete + // the newly-rezzed model. + var entityIDs = Entities.findEntitiesByName(CAMERA_ENTITY_NAME, MyAvatar.position, 100, false); + entityIDs.forEach(function (currentEntityID) { + var currentEntityOwner = Entities.getEntityProperties(currentEntityID, ['owningAvatarID']).owningAvatarID; + if (currentEntityOwner === MyAvatar.sessionUUID && currentEntityID !== tabletCamAvatarEntity) { + Entities.deleteEntity(currentEntityID); + } + }); + } + + var frontCamInUse = Settings.getValue("tabletCam/frontCamInUse", true); + function switchCams(forceFrontCamValue) { + if (!tabletCamAvatarEntity || (!!HMD.tabletID && !tabletCamLocalEntity)) { + console.log("User tried to switch cams, but TabletCam wasn't ready!"); + return; + } + + frontCamInUse = forceFrontCamValue || !frontCamInUse; + Settings.setValue("tabletCam/frontCamInUse", frontCamInUse); + + var newTabletCamAvatarEntityProps = getDynamicTabletCamAvatarEntityProperties(); + Entities.editEntity(tabletCamAvatarEntity, newTabletCamAvatarEntityProps); + + updateTabletCamLocalEntity(); + } + + function disableTabletCam() { + function deleteTabletCamAvatarEntity() { + if (flash) { + Entities.deleteEntity(flash); + flash = false; + } + + if (tabletCamAvatarEntity) { + Entities.deleteEntity(tabletCamAvatarEntity); + tabletCamAvatarEntity = false; + detached = false; // TO BE CONFIRMED + Settings.setValue("tabletCam/detached", detached); // TO BE CONFIRMED + } + } + + wireSignals(false); + + setTakePhotoControllerMappingStatus(false); + + if (tabletCamRunning) { + secondaryCameraConfig.farClipPlaneDistance = previousFarClipDistance; + secondaryCameraConfig.nearClipPlaneDistance = previousNearClipDistance; + secondaryCameraConfig.vFoV = previousvFoV; + secondaryCameraConfig.attachedEntityId = false; + secondaryCameraConfig.enableSecondaryCameraRenderConfigs(false); + } + + deleteTabletCamAvatarEntity(); + + if (tabletCamLocalEntity) { + Entities.deleteEntity(tabletCamLocalEntity); + tabletCamLocalEntity = false; + detached = false; // TO BE CONFIRMED + Settings.setValue("tabletCam/detached", detached); // TO BE CONFIRMED + } + + tabletCamRunning = false; + } + + var tabletCamLocalEntityWidth = 0.282; + var tabletCamLocalEntityHeight = 0.282; + var tabletCamLocalEntityDim = { x: tabletCamLocalEntityWidth, y: tabletCamLocalEntityHeight }; + var tabletCamLocalEntity = false; + var LOCAL_ENTITY_STATIC_PROPERTIES = { + type: "Image", + imageURL: "resource://spectatorCameraFrame", + emissive: true, + grab: { + "grabbable": false + }, + alpha: 1, + triggerable: false + }; + function updateTabletCamLocalEntity() { + if (!HMD.tabletID) { + return; + } + + if (tabletCamLocalEntity) { + Entities.deleteEntity(tabletCamLocalEntity); + tabletCamLocalEntity = false; + } + var props = LOCAL_ENTITY_STATIC_PROPERTIES; + props.dimensions = tabletCamLocalEntityDim; + if (!!HMD.tabletID) { + props.parentID = HMD.tabletID; + props.localPosition = [0, 0.0225, -0.008]; + if (frontCamInUse) { + props.localRotation = Quat.fromVec3Degrees([0, 180, 180]); + } else { + props.localRotation = Quat.fromVec3Degrees([0, 0, 180]); + } + } else { + props.parentID = Uuid.NULL; + props.localPosition = inFrontOf(0.5); + props.localRotation = MyAvatar.orientation; + } + + tabletCamLocalEntity = Entities.addEntity(props, "local"); + } + + function onDomainChanged() { + if (tabletCamRunning) { + disableTabletCam(); + } + } + + function tabletVisibilityChanged() { + if (!ui.tablet.tabletShown && ui.isOpen) { + ui.close(); + } + } + + var flash = Settings.getValue("tabletCam/flashEnabled", false);; + function setFlashStatus(enabled) { + if (!tabletCamAvatarEntity) { + return; + } + + Settings.setValue("tabletCam/flashEnabled", enabled); + + var cameraPosition = Entities.getEntityProperties(tabletCamAvatarEntity, ["positon"]).position; + if (enabled) { + Audio.playSound(SOUND_FLASH_ON, { + position: cameraPosition, + localOnly: true, + volume: 0.8 + }); + flash = Entities.addEntity({ + "collisionless": true, + "collidesWith": "", + "collisionMask": 0, + "color": { + "blue": 173, + "green": 252, + "red": 255 + }, + "cutoff": 90, + "dimensions": { + "x": 4, + "y": 4, + "z": 4 + }, + "dynamic": false, + "falloffRadius": 0.20000000298023224, + "intensity": 27, + "isSpotlight": true, + "localRotation": { w: 1, x: 0, y: 0, z: 0 }, + "localPosition": { x: 0, y: 0, z: -0.005 }, + "name": "Tablet Camera Flash", + "type": "Light", + "parentID": tabletCamAvatarEntity, + }, "avatar"); + } else { + if (flash) { + Audio.playSound(SOUND_FLASH_OFF, { + position: cameraPosition, + localOnly: true, + volume: 0.8 + }); + Entities.deleteEntity(flash); + flash = false; + } + } + } + + function takePhoto() { + var tabletCamAvatarEntityPosition = Entities.getEntityProperties(tabletCamAvatarEntity, ["position"]).position; + Audio.playSound(SOUND_SNAPSHOT, { + position: { x: tabletCamAvatarEntityPosition.x, y: tabletCamAvatarEntityPosition.y, z: tabletCamAvatarEntityPosition.z }, + localOnly: true, + volume: 0.2 + }); + Window.takeSecondaryCameraSnapshot(); + } + + function maybeTakePhoto() { + if (tabletCamAvatarEntity) { + secondaryCameraConfig.resetSizeSpectatorCamera(secondaryCameraResolutionWidth, secondaryCameraResolutionHeight); + // Wait a moment before taking the photo for the resolution to update + Script.setTimeout(function () { + takePhoto(); + }, 250); + } + } + + var snapshotQuality = Settings.getValue("tabletCam/quality", "normal"); + function setSnapshotQuality(quality) { + snapshotQuality = quality; + Settings.setValue("tabletCam/quality", snapshotQuality); + + var shortSideTargetResolution = 1000; + if (snapshotQuality === "low") { + shortSideTargetResolution = 500; + } else if (snapshotQuality === "normal") { + shortSideTargetResolution = 1000; + } else if (snapshotQuality === "high") { + shortSideTargetResolution = 2160; + } else if (snapshotQuality === "extreme") { + shortSideTargetResolution = 4320; + } + + if (tallOrientation && !HMD.active) { + secondaryCameraResolutionWidth = shortSideTargetResolution; + secondaryCameraResolutionHeight = secondaryCameraResolutionWidth / aspectRatio; + + secondaryCameraResolutionPreviewWidth = PREVIEW_SHORT_SIDE_RESOLUTION; + secondaryCameraResolutionPreviewHeight = secondaryCameraResolutionPreviewWidth / aspectRatio; + } else { + secondaryCameraResolutionHeight = shortSideTargetResolution; + secondaryCameraResolutionWidth = secondaryCameraResolutionHeight / aspectRatio; + + secondaryCameraResolutionPreviewHeight = PREVIEW_SHORT_SIDE_RESOLUTION; + secondaryCameraResolutionPreviewWidth = secondaryCameraResolutionPreviewHeight / aspectRatio; + } + + secondaryCameraConfig.resetSizeSpectatorCamera(secondaryCameraResolutionPreviewWidth, secondaryCameraResolutionPreviewHeight); + } + + var aspectRatio = parseFloat(Settings.getValue("tabletCam/aspectRatio", "0.8")); + function setAspectRatio(ratio) { + aspectRatio = ratio; + Settings.setValue("tabletCam/aspectRatio", aspectRatio); + + setSnapshotQuality(snapshotQuality); + } + + var tallOrientation = Settings.getValue("tabletCam/tallOrientation", true); + function setOrientation(orientation) { + tallOrientation = orientation; + Settings.setValue("tabletCam/tallOrientation", tallOrientation); + + setSnapshotQuality(snapshotQuality); + } + + function photoDirChanged(snapshotPath) { + Window.browseDirChanged.disconnect(photoDirChanged); + if (snapshotPath !== "") { // not cancelled + Snapshot.setSnapshotsLocation(snapshotPath); + ui.sendMessage({ + method: "photoDirectoryChanged", + photoDirectory: snapshotPath + }); + } + } + + function fromQml(message) { + switch (message.method) { + case 'switchCams': + switchCams(message.frontCamInUse); + break; + case 'switchOrientation': + setOrientation(!tallOrientation); + break; + case 'setFlashStatus': + setFlashStatus(message.enabled); + break; + case 'takePhoto': + maybeTakePhoto(); + break; + case 'updateCameravFoV': + vFoV = message.vFoV; + secondaryCameraConfig.vFoV = vFoV; + Settings.setValue("tabletCam/vFoV", vFoV); + break; + case 'setSnapshotQuality': + setSnapshotQuality(message.quality); + break; + case 'setAspectRatio': + setAspectRatio(message.aspectRatio); + break; + case 'activeViewChanged': + if (message.activeView === "settingsView" || message.activeView === "reviewView") { + //disableTabletCam(); + } else { + enableTabletCam(); + } + break; + case 'setPhotoDirectory': + Window.browseDirChanged.connect(photoDirChanged); + Window.browseDirAsync("Choose Photo Directory", "", ""); + break; + case 'setDetached': + detached = message.detached; + Settings.setValue("tabletCam/detached", detached); + var newTabletCamAvatarEntityProps = getDynamicTabletCamAvatarEntityProperties(); + Entities.editEntity(tabletCamAvatarEntity, newTabletCamAvatarEntityProps); + break; + default: + print('Unrecognized message from TabletCam.qml.'); + } + } + + function setTakePhotoControllerMappingStatus(status) { + if (!takePhotoControllerMapping) { + return; + } + if (status) { + takePhotoControllerMapping.enable(); + } else { + takePhotoControllerMapping.disable(); + } + } + + var takePhotoControllerMapping; + var takePhotoControllerMappingName = 'Hifi-TabletCam-Mapping-TakePhoto'; + function registerTakePhotoControllerMapping() { + takePhotoControllerMapping = Controller.newMapping(takePhotoControllerMappingName); + if (controllerType === "OculusTouch") { + takePhotoControllerMapping.from(Controller.Standard.RS).to(function (value) { + if (value === 1.0) { + maybeTakePhoto(); + } + return; + }); + } else if (controllerType === "Vive") { + takePhotoControllerMapping.from(Controller.Standard.RightPrimaryThumb).to(function (value) { + if (value === 1.0) { + maybeTakePhoto(); + } + return; + }); + } + } + + var controllerType = "Other"; + function registerButtonMappings() { + var VRDevices = Controller.getDeviceNames().toString(); + if (VRDevices) { + if (VRDevices.indexOf("Vive") !== -1) { + controllerType = "Vive"; + } else if (VRDevices.indexOf("OculusTouch") !== -1) { + controllerType = "OculusTouch"; + } else { + return; // Neither Vive nor Touch detected + } + } + + if (!takePhotoControllerMapping) { + registerTakePhotoControllerMapping(); + } + } + + function onHMDChanged(isHMDMode) { + registerButtonMappings(); + disableTabletCam(); + } + + var cameraRollPaths = JSON.parse(Settings.getValue("tabletCam/cameraRollPaths", '{"paths": []}')); + function onStillSnapshotTaken(path) { + var tempObject = {}; + tempObject.imagePath = "file:///" + path; + + cameraRollPaths.paths.unshift(tempObject); + if (cameraRollPaths.paths.length > 15) { + cameraRollPaths.paths.pop(); + } + Settings.setValue("tabletCam/cameraRollPaths", JSON.stringify(cameraRollPaths)); + + secondaryCameraConfig.resetSizeSpectatorCamera(secondaryCameraResolutionPreviewWidth, secondaryCameraResolutionPreviewHeight); + ui.sendMessage({ + method: 'stillSnapshotTaken', + lastStillSnapshotPath: tempObject.imagePath + }); + } + + var signalsWired = false; + function wireSignals(shouldWire) { + if (signalsWired === shouldWire) { + return; + } + + signalsWired = shouldWire; + + if (shouldWire) { + Window.stillSnapshotTaken.connect(onStillSnapshotTaken); + } else { + Window.stillSnapshotTaken.disconnect(onStillSnapshotTaken); + } + } + + function inFrontOf(distance, position, orientation) { + return Vec3.sum(position || MyAvatar.position, + Vec3.multiply(distance, Quat.getForward(orientation || MyAvatar.orientation))); + } + + var detached = false; + Settings.setValue("tabletCam/detached", detached); + function getDynamicTabletCamAvatarEntityProperties() { + var dynamicProps = { + dimensions: {"x":0.1600, "y":0.1021, "z":0.1137} + }; + + if (detached) { + //print("DETACHED MODE"); + dynamicProps.collisionless = false; + dynamicProps.ignoreForCollisions = false; + dynamicProps.grab = { + "grabbable": true, + "equippableLeftRotation": { + "x": -0.0000152587890625, + "y": -0.0000152587890625, + "z": -0.0000152587890625, + "w": 1 + }, + "equippableRightRotation": { + "x": -0.0000152587890625, + "y": -0.0000152587890625, + "z": -0.0000152587890625, + "w": 1 + } + }; + dynamicProps.visible = true; + dynamicProps.parentID = Uuid.NULL; + dynamicProps.parentJointIndex = 65535; + dynamicProps.triggerable = true; + if (tabletCamAvatarEntity) { + var currentProps = Entities.getEntityProperties(tabletCamAvatarEntity, ["position", "rotation"]); + if (!!HMD.tabletID) { + dynamicProps.position = inFrontOf(0.2, currentProps.position, currentProps.rotation); + } else { + dynamicProps.position = currentProps.position; + } + dynamicProps.rotation = currentProps.rotation; + } else { + dynamicProps.position = inFrontOf(0.5); + dynamicProps.rotation = MyAvatar.orientation; + } + dynamicProps.velocity = [0, 0, 0]; + dynamicProps.angularVelocity = [0, 0, 0]; + } else { + dynamicProps.triggerable = false; + dynamicProps.collisionless = true; + dynamicProps.ignoreForCollisions = true; + dynamicProps.grab = { + "grabbable": false + }; + dynamicProps.visible = false; + + if (!!HMD.tabletID) { + //print("TABLET MODE"); + dynamicProps.parentID = HMD.tabletID; + dynamicProps.parentJointIndex = 65535; + dynamicProps.dimensions = [0.01, 0.01, 0.01]; + } else { + //print("DESKTOP USER CAMERA MODE"); + var cameraMode = Camera.mode; + // If: + // - User is in third person mode + // - User is using the rear-facing camera + if (cameraMode !== "first person" && !frontCamInUse) { + dynamicProps.parentID = MyAvatar.sessionUUID; + dynamicProps.parentJointIndex = MyAvatar.getJointIndex("_CAMERA_MATRIX"); + } else { + dynamicProps.parentID = MyAvatar.sessionUUID; + var jointIndex = MyAvatar.getJointIndex("HeadTop_End"); + if (jointIndex === -1) { + jointIndex = MyAvatar.getJointIndex("Head"); + } + dynamicProps.parentJointIndex = jointIndex; + } + } + + dynamicProps.localPosition = { + "x": 0, + "y": !!HMD.tabletID ? 0.215 : (frontCamInUse ? -0.03 : (Camera.mode !== "first person" ? 0 : -0.02)), + "z": !!HMD.tabletID ? (frontCamInUse ? -0.02 : 0.1) : (frontCamInUse ? 1 : (Camera.mode !== "first person" ? 0 : 0.05)) + }; + + if (!!HMD.tabletID) { + dynamicProps.localRotation = { + "x": 0, + "y": frontCamInUse ? 0 : 1, + "z": 0, + "w": frontCamInUse ? 1 : 0 + }; + } else { + dynamicProps.localRotation = { + "x": 0, + "y": frontCamInUse || (!frontCamInUse && Camera.mode !== "first person") ? 0 : 1, + "z": 0, + "w": frontCamInUse || (!frontCamInUse && Camera.mode !== "first person") ? 1 : 0 + }; + } + } + + return dynamicProps; + } + + function onModeUpdated(newMode) { + if (tabletCamAvatarEntity) { + var newTabletCamAvatarEntityProps = getDynamicTabletCamAvatarEntityProperties(); + Entities.editEntity(tabletCamAvatarEntity, newTabletCamAvatarEntityProps); + } + } + + function onClosed() { + if (!detached) { + disableTabletCam(); + } + + if (tabletCamLocalEntity) { + Entities.deleteEntity(tabletCamLocalEntity); + tabletCamLocalEntity = false; + } + } + + function buttonActive(isActive) { + ui.button.editProperties({isActive: isActive || tabletCamRunning}); + } + + var ui; + function startup() { + ui = new AppUi({ + buttonName: APPLICATION_CAPTION, + home: Script.resolvePath("./ui/TabletCam.qml"), + // Selfie by Path Lord from the Noun Project + graphicsDirectory: Script.resolvePath("appIcons/"), + onOpened: enableTabletCam, + onClosed: onClosed, + onMessage: fromQml, + buttonActive: buttonActive + }); + + Window.domainChanged.connect(onDomainChanged); + ui.tablet.tabletShownChanged.connect(tabletVisibilityChanged); + HMD.displayModeChanged.connect(onHMDChanged); + Camera.modeUpdated.connect(onModeUpdated); + + registerButtonMappings(); + } + startup(); + + function shutdown() { + disableTabletCam(); + Window.domainChanged.disconnect(onDomainChanged); + ui.tablet.tabletShownChanged.disconnect(tabletVisibilityChanged); + HMD.displayModeChanged.disconnect(onHMDChanged); + Camera.modeUpdated.disconnect(onModeUpdated); + if (takePhotoControllerMapping) { + takePhotoControllerMapping.disable(); + } + wireSignals(false); + } + Script.scriptEnding.connect(shutdown); + + // "Camera Shutter, Fast, A.wav" by InspectorJ (www.jshaw.co.uk) of Freesound.org + var SOUND_SNAPSHOT = SoundCache.getSound(Script.resolvePath("sounds/snap.wav")); + var SOUND_FLASH_ON = SoundCache.getSound(Script.resolvePath("sounds/flashOn.wav")); + var SOUND_FLASH_OFF = SoundCache.getSound(Script.resolvePath("sounds/flashOff.wav")); +}()); // END LOCAL_SCOPE diff --git a/applications/tabletCam/ui/TabletCam.qml b/applications/tabletCam/ui/TabletCam.qml new file mode 100644 index 0000000..40103a7 --- /dev/null +++ b/applications/tabletCam/ui/TabletCam.qml @@ -0,0 +1,907 @@ +// +// TabletCam.qml +// qml/hifi +// +// Tablet Cam v2.2 +// +// Created by Zach Fox on 2019-04-14 +// Copyright 2022 Overte e.V. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 as Hifi +import QtQuick 2.7 +import QtQuick.Controls 2.3 +import stylesUit 1.0 as HifiStylesUit +import controlsUit 1.0 as HifiControlsUit + +Rectangle { + id: root; + property bool flashEnabled: Settings.getValue("tabletCam/flashEnabled", false); + property string snapshotQuality: Settings.getValue("tabletCam/quality", "normal"); + property real aspectRatio: Settings.getValue("tabletCam/aspectRatio", (8 / 10)); + property bool detached: Settings.getValue("tabletCam/detached", false); + property bool frontCamInUse: Settings.getValue("tabletCam/frontCamInUse", true); + property string activeView: "mainView"; + + HifiStylesUit.HifiConstants { id: hifi; } + color: hifi.colors.black; + + onFlashEnabledChanged: { + sendToScript({method: 'setFlashStatus', enabled: root.flashEnabled}); + } + + onDetachedChanged: { + sendToScript({method: 'setDetached', detached: root.detached}); + } + + onFrontCamInUseChanged: { + sendToScript({method: 'switchCams', frontCamInUse: root.frontCamInUse}); + } + + onActiveViewChanged: { + root.flashEnabled = false; + sendToScript({method: 'activeViewChanged', activeView: root.activeView}); + + if (root.activeView === "settingsView") { + photoDirectoryTextField.text = Settings.getValue("snapshotsLocation", ""); + } + } + + Item { + id: mainView; + visible: root.activeView === "mainView"; + anchors.fill: parent; + + Rectangle { + id: helpTextContainer; + visible: !!Settings.getValue('tabletCam/firstRun', true) && HMD.active; + width: parent.width; + height: topBarContainer_main.height; + anchors.left: parent.left; + anchors.top: parent.top; + color: "#121212"; + + HifiStylesUit.RalewaySemiBold { + text: "Try clicking right thumbstick for photos!"; + // Anchors + anchors.left: parent.left; + anchors.leftMargin: 8; + anchors.verticalCenter: parent.verticalCenter; + size: 22; + // Style + color: hifi.colors.white; + // Alignment + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignVCenter; + wrapMode: Text.Wrap; + } + + HifiControlsUit.Button { + text: "OK"; + colorScheme: hifi.colorSchemes.dark; + color: hifi.buttons.blue; + anchors.verticalCenter: parent.verticalCenter; + anchors.right: parent.right; + anchors.rightMargin: 8; + width: 50; + height: 35; + onClicked: { + helpTextContainer.visible = false; + Settings.setValue('tabletCam/firstRun', false); + } + } + } + + Rectangle { + id: topBarContainer_main; + visible: !helpTextContainer.visible; + width: parent.width; + height: 42; + anchors.left: parent.left; + anchors.top: parent.top; + color: "#121212"; + + HifiControlsUit.CheckBox { + id: detachCheckbox; + text: "Detach" + checked: root.detached; + boxSize: 24; + height: 32; + anchors.verticalCenter: parent.verticalCenter; + anchors.left: parent.left; + anchors.leftMargin: 8; + onClicked: { + root.detached = checked; + } + } + + HifiControlsUit.GlyphButton { + id: flashButton; + height: 26; + width: height; + anchors.verticalCenter: parent.verticalCenter; + anchors.right: fakeFlash.left; + anchors.rightMargin: 8; + glyph: hifi.glyphs.lightning; + color: root.flashEnabled ? hifi.buttons.blue : hifi.buttons.none; + onClicked: { + root.flashEnabled = !root.flashEnabled; + } + } + + Rectangle { + id: fakeCamera; + width: 34; + height: width; + radius: width; + anchors.centerIn: parent; + color: hifi.colors.black; + + Rectangle { + visible: root.frontCamInUse && !root.detached; + width: parent.width - 12; + height: width; + radius: width; + anchors.centerIn: parent; + color: "#230000"; + } + } + + Rectangle { + id: fakeFlash; + width: 12; + height: width; + radius: width; + anchors.verticalCenter: fakeCamera.verticalCenter; + anchors.right: fakeCamera.left; + anchors.rightMargin: 4; + color: root.flashEnabled && root.frontCamInUse ? "#fffcad" : "#000000"; + } + + Image { + id: switchCams; + height: 26; + width: height; + anchors.verticalCenter: parent.verticalCenter; + anchors.left: fakeCamera.right; + anchors.leftMargin: 8; + source: "./images/switchCams.svg"; // rotate camera by Diego Naive from the Noun Project + mipmap: true; + MouseArea { + anchors.fill: parent; + enabled: !root.detached; + + onClicked: { + root.frontCamInUse = !root.frontCamInUse; + } + } + } + + HifiControlsUit.GlyphButton { + id: settingsButton; + height: 26; + width: height; + anchors.verticalCenter: parent.verticalCenter; + anchors.right: parent.right; + anchors.rightMargin: 8; + glyph: hifi.glyphs.settings; + color: hifi.buttons.none; + onClicked: { + root.activeView = "settingsView"; + } + } + } + + Rectangle { + visible: !secondaryCameraPreview.visible && HMD.tabletID !== "{00000000-0000-0000-0000-000000000000}"; + anchors.fill: secondaryCameraPreview; + color: hifi.colors.white; + } + + // Secondary Camera Preview + Hifi.ResourceImageItem { + id: secondaryCameraPreview; + visible: HMD.tabletID !== "{00000000-0000-0000-0000-000000000000}"; + url: "resource://spectatorCameraFrame"; + ready: visible; + mirrorVertically: true; + anchors.top: topBarContainer_main.bottom; + anchors.bottom: bottomBarContainer_main.top; + anchors.left: parent.left; + anchors.right: parent.right; + onVisibleChanged: { + update(); + } + } + + Rectangle { + id: bottomBarContainer_main; + height: 88; + anchors.left: parent.left; + anchors.bottom: parent.bottom; + anchors.right: parent.right; + color: "#121212"; + + Item { + id: fieldOfView; + anchors.left: parent.left; + anchors.leftMargin: 12; + anchors.verticalCenter: parent.verticalCenter; + anchors.right: takeSnapshotButton.left; + anchors.rightMargin: 12; + height: 35; + + HifiControlsUit.GlyphButton { + id: resetvFoV; + anchors.verticalCenter: parent.verticalCenter; + anchors.left: parent.left; + height: parent.height - 8; + width: height; + glyph: hifi.glyphs.reload; + onClicked: { + fieldOfViewSlider.value = 60.0; + } + } + + HifiControlsUit.Slider { + id: fieldOfViewSlider; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.right: parent.right; + anchors.left: resetvFoV.right; + anchors.leftMargin: 8; + colorScheme: hifi.colorSchemes.dark; + from: 8.0; + to: 120.0; + value: (to - Settings.getValue("tabletCam/vFoV", 60.0) + from); + stepSize: 1; + + onValueChanged: { + sendToScript({method: 'updateCameravFoV', vFoV: to - value + from}); + } + onPressedChanged: { + if (!pressed) { + sendToScript({method: 'updateCameravFoV', vFoV: to - value + from}); + } + } + } + } + + Rectangle { + id: takeSnapshotButton; + color: "#EA4C5F"; + anchors.horizontalCenter: parent.horizontalCenter; + anchors.verticalCenter: parent.verticalCenter; + height: 72; + width: height; + radius: height; + border.width: 3; + border.color: hifi.colors.white; + + MouseArea { + anchors.fill: parent; + hoverEnabled: true; + onEntered: { + parent.color = "#C62147"; + } + onExited: { + parent.color = "#EA4C5F"; + } + onClicked: { + if (HMD.tabletID !== "{00000000-0000-0000-0000-000000000000}") { + secondaryCameraPreview.visible = false; + } + sendToScript({method: 'takePhoto'}); + } + } + } + + Image { + visible: !HMD.active; + source: "./images/orientation.svg"; // orientation by Atif Arshad from the Noun Project + height: 24; + width: height; + anchors.left: takeSnapshotButton.right; + anchors.leftMargin: 24; + anchors.verticalCenter: parent.verticalCenter; + + MouseArea { + anchors.fill: parent; + onClicked: { + sendToScript({method: 'switchOrientation'}); + } + } + } + + Rectangle { + id: galleryButton; + anchors.right: parent.right; + anchors.rightMargin: 12; + anchors.verticalCenter: parent.verticalCenter; + height: 72; + width: height; + color: hifi.colors.black; + + Image { + id: galleryButtonImage; + source: JSON.parse(Settings.getValue("tabletCam/cameraRollPaths", '{"paths": ["imagePath": ""]}')).paths[0].imagePath; + fillMode: Image.PreserveAspectCrop; + anchors.fill: parent; + mipmap: true; + } + + MouseArea { + enabled: galleryButtonImage.source !== ""; + anchors.fill: parent; + onClicked: { + cameraRollSwipeView.setCurrentIndex(0); + cameraRollModel.clear(); + + var settingsString = Settings.getValue("tabletCam/cameraRollPaths", '{"paths": []}'); + cameraRollModel.append(JSON.parse(settingsString).paths); + + root.activeView = "reviewView"; + } + } + } + } + } + + Item { + id: reviewView; + visible: root.activeView === "reviewView"; + anchors.fill: parent; + + Rectangle { + id: topBarContainer_review; + width: parent.width; + height: 42; + anchors.left: parent.left; + anchors.top: parent.top; + color: "#121212"; + + HifiControlsUit.Button { + text: "BACK"; + colorScheme: hifi.colorSchemes.dark; + color: hifi.buttons.noneBorderlessWhite; + anchors.verticalCenter: parent.verticalCenter; + anchors.left: parent.left; + anchors.leftMargin: 8; + width: 50; + height: 30; + onClicked: { + root.activeView = "mainView"; + } + } + + HifiStylesUit.RalewaySemiBold { + text: "CAMERA ROLL"; + // Anchors + anchors.horizontalCenter: parent.horizontalCenter; + anchors.verticalCenter: parent.verticalCenter; + size: 22; + // Style + color: hifi.colors.white; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + wrapMode: Text.Wrap; + } + } + + ListModel { + id: cameraRollModel; + } + + SwipeView { + id: cameraRollSwipeView; + + anchors.top: topBarContainer_review.bottom; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: bottomBarContainer_review.top; + + Repeater { + model: cameraRollModel; + + Image { + source: imagePath; + fillMode: Image.PreserveAspectFit; + mipmap: true; + } + } + } + + PageIndicator { + id: indicator; + interactive: true; + count: cameraRollSwipeView.count; + currentIndex: cameraRollSwipeView.currentIndex + + anchors.bottom: cameraRollSwipeView.bottom; + anchors.horizontalCenter: cameraRollSwipeView.horizontalCenter; + + delegate: Rectangle { + implicitWidth: 15; + implicitHeight: 15; + radius: width; + color: "#00b4ef"; + opacity: index === cameraRollSwipeView.currentIndex ? 0.95 : 0.45; + + border.color: "#FFFFFF"; + border.width: index === cameraRollSwipeView.currentIndex ? 2 : 0; + + Behavior on opacity { + OpacityAnimator { + duration: 100; + } + } + } + } + + Rectangle { + id: bottomBarContainer_review; + height: 88; + anchors.left: parent.left; + anchors.bottom: parent.bottom; + anchors.right: parent.right; + color: "#121212"; + + HifiControlsUit.Button { + text: "SHOW IN DESKTOP FILE BROWSER"; + colorScheme: hifi.colorSchemes.dark; + color: hifi.buttons.blue; + anchors.verticalCenter: parent.verticalCenter; + anchors.horizontalCenter: parent.horizontalCenter; + width: 240; + height: 30; + onClicked: { + var currentImagePath = cameraRollModel.get(cameraRollSwipeView.index).imagePath; + Qt.openUrlExternally(currentImagePath.substring(0, currentImagePath.lastIndexOf('/'))); + } + } + } + } + + Rectangle { + id: settingsView; + visible: root.activeView === "settingsView"; + anchors.fill: parent; + color: hifi.colors.black; + + Rectangle { + id: topBarContainer_settings; + width: parent.width; + height: 42; + anchors.left: parent.left; + anchors.top: parent.top; + color: "#121212"; + + HifiControlsUit.Button { + text: "BACK"; + colorScheme: hifi.colorSchemes.dark; + color: hifi.buttons.noneBorderlessWhite; + anchors.verticalCenter: parent.verticalCenter; + anchors.left: parent.left; + anchors.leftMargin: 8; + width: 50; + height: 30; + onClicked: { + root.activeView = "mainView"; + } + } + + HifiStylesUit.RalewaySemiBold { + text: "SETTINGS"; + // Anchors + anchors.horizontalCenter: parent.horizontalCenter; + anchors.verticalCenter: parent.verticalCenter; + size: 22; + // Style + color: hifi.colors.white; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + wrapMode: Text.Wrap; + } + } + + Item { + id: settingsContainer; + anchors.top: topBarContainer_settings.bottom; + anchors.topMargin: 16; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 8; + + Item { + id: qualityContainer; + anchors.top: parent.top; + anchors.left: parent.left; + anchors.right: parent.right; + height: childrenRect.height; + + HifiStylesUit.RalewaySemiBold { + id: qualityHeaderText; + text: "Photo Quality"; + // Anchors + anchors.left: parent.left; + anchors.top: parent.top; + height: 22; + size: 18; + // Style + color: hifi.colors.white; + // Alignment + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignTop; + } + + HifiControlsUit.RadioButton { + id: lowRadioButton; + checked: root.snapshotQuality === "low"; + text: "Low"; + width: 70; + height: 35; + anchors.left: parent.left; + anchors.top: qualityHeaderText.bottom; + colorScheme: hifi.colorSchemes.dark; + onClicked: { + if (!lowRadioButton.checked) { + lowRadioButton.checked = true; + } + if (normalRadioButton.checked) { + normalRadioButton.checked = false; + } + if (highRadioButton.checked) { + highRadioButton.checked = false; + } + if (extremeRadioButton.checked) { + extremeRadioButton.checked = false; + } + } + onCheckedChanged: { + if (checked) { + sendToScript({method: 'setSnapshotQuality', quality: "low"}); + } + } + } + + HifiControlsUit.RadioButton { + id: normalRadioButton; + checked: root.snapshotQuality === "normal"; + text: "Normal"; + width: 100; + height: 35; + anchors.left: lowRadioButton.right; + anchors.leftMargin: 16; + anchors.top: qualityHeaderText.bottom; + colorScheme: hifi.colorSchemes.dark; + onClicked: { + if (lowRadioButton.checked) { + lowRadioButton.checked = false; + } + if (!normalRadioButton.checked) { + normalRadioButton.checked = true; + } + if (highRadioButton.checked) { + highRadioButton.checked = false; + } + if (extremeRadioButton.checked) { + extremeRadioButton.checked = false; + } + } + onCheckedChanged: { + if (checked) { + sendToScript({method: 'setSnapshotQuality', quality: "normal"}); + } + } + } + + HifiControlsUit.RadioButton { + id: highRadioButton; + checked: root.snapshotQuality === "high"; + text: "4k"; + width: 75; + height: 35; + anchors.left: normalRadioButton.right; + anchors.leftMargin: 16; + anchors.top: qualityHeaderText.bottom; + colorScheme: hifi.colorSchemes.dark; + onClicked: { + if (lowRadioButton.checked) { + lowRadioButton.checked = false; + } + if (normalRadioButton.checked) { + normalRadioButton.checked = false; + } + if (!highRadioButton.checked) { + highRadioButton.checked = true; + } + if (extremeRadioButton.checked) { + extremeRadioButton.checked = false; + } + } + onCheckedChanged: { + if (checked) { + sendToScript({method: 'setSnapshotQuality', quality: "high"}); + } + } + } + + HifiControlsUit.RadioButton { + id: extremeRadioButton; + checked: root.snapshotQuality === "extreme"; + text: "EXTREME"; + width: 120; + height: 35; + anchors.left: highRadioButton.right; + anchors.leftMargin: 16; + anchors.top: qualityHeaderText.bottom; + colorScheme: hifi.colorSchemes.dark; + onClicked: { + if (lowRadioButton.checked) { + lowRadioButton.checked = false; + } + if (normalRadioButton.checked) { + normalRadioButton.checked = false; + } + if (highRadioButton.checked) { + highRadioButton.checked = false; + } + if (!extremeRadioButton.checked) { + extremeRadioButton.checked = true; + } + } + onCheckedChanged: { + if (checked) { + sendToScript({method: 'setSnapshotQuality', quality: "extreme"}); + } + } + } + } + + Item { + id: aspectRatioContainer; + anchors.top: qualityContainer.bottom; + anchors.topMargin: 16; + anchors.left: parent.left; + anchors.right: parent.right; + height: childrenRect.height; + + HifiStylesUit.RalewaySemiBold { + id: aspectRatioHeaderText; + text: "Aspect Ratio"; + // Anchors + anchors.left: parent.left; + anchors.top: parent.top; + height: 22; + size: 18; + // Style + color: hifi.colors.white; + // Alignment + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignTop; + } + + HifiControlsUit.RadioButton { + id: eightByTenRadioButton; + checked: parseFloat(root.aspectRatio) === (8 / 10); + text: "8x10"; + width: 70; + height: 35; + anchors.left: parent.left; + anchors.top: aspectRatioHeaderText.bottom; + colorScheme: hifi.colorSchemes.dark; + onClicked: { + if (!eightByTenRadioButton.checked) { + eightByTenRadioButton.checked = true; + } + if (twoByThreeRadioButton.checked) { + twoByThreeRadioButton.checked = false; + } + if (nineBySixteenRadioButton.checked) { + nineBySixteenRadioButton.checked = false; + } + if (oneByOneRadioButton.checked) { + oneByOneRadioButton.checked = false; + } + } + onCheckedChanged: { + if (checked) { + sendToScript({method: 'setAspectRatio', aspectRatio: (8 / 10)}); + } + } + } + + HifiControlsUit.RadioButton { + id: twoByThreeRadioButton; + checked: parseFloat(root.aspectRatio) === (2 / 3); + text: "2x3"; + width: 100; + height: 35; + anchors.left: eightByTenRadioButton.right; + anchors.leftMargin: 16; + anchors.top: aspectRatioHeaderText.bottom; + colorScheme: hifi.colorSchemes.dark; + onClicked: { + if (eightByTenRadioButton.checked) { + eightByTenRadioButton.checked = false; + } + if (!twoByThreeRadioButton.checked) { + twoByThreeRadioButton.checked = true; + } + if (nineBySixteenRadioButton.checked) { + nineBySixteenRadioButton.checked = false; + } + if (oneByOneRadioButton.checked) { + oneByOneRadioButton.checked = false; + } + } + onCheckedChanged: { + if (checked) { + sendToScript({method: 'setAspectRatio', aspectRatio: (2 / 3)}); + } + } + } + + HifiControlsUit.RadioButton { + id: nineBySixteenRadioButton; + checked: parseFloat(root.aspectRatio) === 9 / 16; + text: "9x16"; + width: 75; + height: 35; + anchors.left: twoByThreeRadioButton.right; + anchors.leftMargin: 16; + anchors.top: aspectRatioHeaderText.bottom; + colorScheme: hifi.colorSchemes.dark; + onClicked: { + if (eightByTenRadioButton.checked) { + eightByTenRadioButton.checked = false; + } + if (twoByThreeRadioButton.checked) { + twoByThreeRadioButton.checked = false; + } + if (!nineBySixteenRadioButton.checked) { + nineBySixteenRadioButton.checked = true; + } + if (oneByOneRadioButton.checked) { + oneByOneRadioButton.checked = false; + } + } + onCheckedChanged: { + if (checked) { + sendToScript({method: 'setAspectRatio', aspectRatio: (9 / 16)}); + } + } + } + + HifiControlsUit.RadioButton { + id: oneByOneRadioButton; + checked: parseFloat(root.aspectRatio) === 1 / 1; + text: "Square"; + width: 83; + height: 35; + anchors.left: nineBySixteenRadioButton.right; + anchors.leftMargin: 16; + anchors.top: aspectRatioHeaderText.bottom; + colorScheme: hifi.colorSchemes.dark; + onClicked: { + if (eightByTenRadioButton.checked) { + eightByTenRadioButton.checked = false; + } + if (twoByThreeRadioButton.checked) { + twoByThreeRadioButton.checked = false; + } + if (nineBySixteenRadioButton.checked) { + nineBySixteenRadioButton.checked = false; + } + if (!oneByOneRadioButton.checked) { + oneByOneRadioButton.checked = true; + } + } + onCheckedChanged: { + if (checked) { + sendToScript({method: 'setAspectRatio', aspectRatio: 1}); + } + } + } + } + + Item { + id: photoDirectoryContainer; + anchors.top: aspectRatioContainer.bottom; + anchors.topMargin: 16; + anchors.left: parent.left; + anchors.right: parent.right; + height: childrenRect.height; + + HifiStylesUit.RalewaySemiBold { + id: photoDirectoryHeaderText; + text: "Photo Directory"; + // Anchors + anchors.left: parent.left; + anchors.top: parent.top; + height: 22; + size: 18; + // Style + color: hifi.colors.white; + // Alignment + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignTop; + } + + HifiControlsUit.TextField { + id: photoDirectoryTextField; + readOnly: true; + text: Settings.getValue("snapshotsDirectory", ""); + colorScheme: hifi.colorSchemes.dark; + // Anchors + anchors.top: photoDirectoryHeaderText.bottom; + anchors.topMargin: 8; + anchors.left: parent.left; + anchors.right: parent.right; + height: 50; + + MouseArea { + anchors.fill: parent; + + onClicked: { + sendToScript({method: 'setPhotoDirectory'}); + } + } + } + + HifiControlsUit.Button { + text: "CHANGE"; + colorScheme: hifi.colorSchemes.dark; + color: hifi.buttons.blue; + anchors.top: photoDirectoryTextField.bottom; + anchors.topMargin: 4; + anchors.right: parent.right; + width: 100; + height: 35; + onClicked: { + sendToScript({method: 'setPhotoDirectory'}); + } + } + } + + HifiStylesUit.FiraSansRegular { + text: "Hint:\nIn HMD, using the detached camera, you can press on the\nthumbsticks of your right controller to take a photo.\n\n\nv2.4"; + // Anchors + anchors.bottom: parent.bottom; + anchors.left: parent.left; + size: 16; + // Style + color: hifi.colors.lightGrayText; + } + } + } + + function fromScript(message) { + switch (message.method) { + case 'stillSnapshotTaken': + Settings.setValue('tabletCam/firstRun', false); + helpTextContainer.visible = false; + galleryButtonImage.source = message.lastStillSnapshotPath; + if (HMD.tabletID !== "{00000000-0000-0000-0000-000000000000}") { + secondaryCameraPreview.visible = true; + } + break; + case 'photoDirectoryChanged': + photoDirectoryTextField.text = message.photoDirectory; + break; + case 'inspectionCertificate_resetCert': + break; + default: + console.log('Unrecognized message from TabletCam.js.'); + } + } + signal sendToScript(var message); +} diff --git a/applications/tabletCam/ui/images/orientation.svg b/applications/tabletCam/ui/images/orientation.svg new file mode 100644 index 0000000..9bf7520 --- /dev/null +++ b/applications/tabletCam/ui/images/orientation.svg @@ -0,0 +1,3 @@ +Created by Atif Arshad +from the Noun Project + diff --git a/applications/tabletCam/ui/images/switchCams.svg b/applications/tabletCam/ui/images/switchCams.svg new file mode 100644 index 0000000..28c752e --- /dev/null +++ b/applications/tabletCam/ui/images/switchCams.svg @@ -0,0 +1,9 @@ + + rotate-cam-3 + Created with Sketch. + + + + Created by Diego Naive + from the Noun Project +