mirror of
https://github.com/HifiExperiments/overte.git
synced 2025-08-09 14:29:13 +02:00
Merge pull request #9045 from zfox23/animated_snapshots
Animated GIF Snapshots
This commit is contained in:
commit
1c4b2eba25
15 changed files with 349 additions and 61 deletions
20
cmake/externals/GifCreator/CMakeLists.txt
vendored
Normal file
20
cmake/externals/GifCreator/CMakeLists.txt
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
set(EXTERNAL_NAME GifCreator)
|
||||||
|
|
||||||
|
include(ExternalProject)
|
||||||
|
ExternalProject_Add(
|
||||||
|
${EXTERNAL_NAME}
|
||||||
|
URL https://hifi-public.s3.amazonaws.com/dependencies/GifCreator.zip
|
||||||
|
URL_MD5 8ac8ef5196f47c658dce784df5ecdb70
|
||||||
|
CONFIGURE_COMMAND ""
|
||||||
|
BUILD_COMMAND ""
|
||||||
|
INSTALL_COMMAND ""
|
||||||
|
LOG_DOWNLOAD 1
|
||||||
|
)
|
||||||
|
|
||||||
|
# Hide this external target (for ide users)
|
||||||
|
set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals")
|
||||||
|
|
||||||
|
ExternalProject_Get_Property(${EXTERNAL_NAME} INSTALL_DIR)
|
||||||
|
|
||||||
|
string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER)
|
||||||
|
set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${INSTALL_DIR}/src/${EXTERNAL_NAME} CACHE PATH "List of GifCreator include directories")
|
26
cmake/modules/FindGifCreator.cmake
Normal file
26
cmake/modules/FindGifCreator.cmake
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
#
|
||||||
|
# FindGifCreator.cmake
|
||||||
|
#
|
||||||
|
# Try to find GifCreator include path.
|
||||||
|
# Once done this will define
|
||||||
|
#
|
||||||
|
# GIFCREATOR_INCLUDE_DIRS
|
||||||
|
#
|
||||||
|
# Created on 11/15/2016 by Zach Fox
|
||||||
|
# Copyright 2016 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
|
||||||
|
#
|
||||||
|
|
||||||
|
# setup hints for GifCreator search
|
||||||
|
include("${MACRO_DIR}/HifiLibrarySearchHints.cmake")
|
||||||
|
hifi_library_search_hints("GIFCREATOR")
|
||||||
|
|
||||||
|
# locate header
|
||||||
|
find_path(GIFCREATOR_INCLUDE_DIRS "GifCreator/GifCreator.h" HINTS ${GIFCREATOR_SEARCH_DIRS})
|
||||||
|
|
||||||
|
include(FindPackageHandleStandardArgs)
|
||||||
|
find_package_handle_standard_args(GIFCREATOR DEFAULT_MSG GIFCREATOR_INCLUDE_DIRS)
|
||||||
|
|
||||||
|
mark_as_advanced(GIFCREATOR_INCLUDE_DIRS GIFCREATOR_SEARCH_DIRS)
|
|
@ -351,3 +351,7 @@ if (ANDROID)
|
||||||
|
|
||||||
qt_create_apk()
|
qt_create_apk()
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
|
add_dependency_external_projects(GifCreator)
|
||||||
|
find_package(GifCreator REQUIRED)
|
||||||
|
target_include_directories(${TARGET_NAME} PUBLIC ${GIFCREATOR_INCLUDE_DIRS})
|
||||||
|
|
|
@ -152,6 +152,7 @@
|
||||||
#include "ui/LoginDialog.h"
|
#include "ui/LoginDialog.h"
|
||||||
#include "ui/overlays/Cube3DOverlay.h"
|
#include "ui/overlays/Cube3DOverlay.h"
|
||||||
#include "ui/Snapshot.h"
|
#include "ui/Snapshot.h"
|
||||||
|
#include "ui/SnapshotAnimated.h"
|
||||||
#include "ui/StandAloneJSConsole.h"
|
#include "ui/StandAloneJSConsole.h"
|
||||||
#include "ui/Stats.h"
|
#include "ui/Stats.h"
|
||||||
#include "ui/UpdateDialog.h"
|
#include "ui/UpdateDialog.h"
|
||||||
|
@ -5428,19 +5429,27 @@ void Application::toggleLogDialog() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Application::takeSnapshot(bool notify, float aspectRatio) {
|
|
||||||
postLambdaEvent([notify, aspectRatio, this] {
|
void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRatio) {
|
||||||
|
postLambdaEvent([notify, includeAnimated, aspectRatio, this] {
|
||||||
QMediaPlayer* player = new QMediaPlayer();
|
QMediaPlayer* player = new QMediaPlayer();
|
||||||
QFileInfo inf = QFileInfo(PathUtils::resourcesPath() + "sounds/snap.wav");
|
QFileInfo inf = QFileInfo(PathUtils::resourcesPath() + "sounds/snap.wav");
|
||||||
player->setMedia(QUrl::fromLocalFile(inf.absoluteFilePath()));
|
player->setMedia(QUrl::fromLocalFile(inf.absoluteFilePath()));
|
||||||
player->play();
|
player->play();
|
||||||
|
|
||||||
|
// Get a screenshot and save it
|
||||||
QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio));
|
QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio));
|
||||||
|
|
||||||
emit DependencyManager::get<WindowScriptingInterface>()->snapshotTaken(path, notify);
|
// If we're not doing an animated snapshot as well...
|
||||||
|
if (!includeAnimated || !(SnapshotAnimated::alsoTakeAnimatedSnapshot.get())) {
|
||||||
|
// Tell the dependency manager that the capture of the still snapshot has taken place.
|
||||||
|
emit DependencyManager::get<WindowScriptingInterface>()->snapshotTaken(path, "", notify);
|
||||||
|
} else {
|
||||||
|
// Get an animated GIF snapshot and save it
|
||||||
|
SnapshotAnimated::saveSnapshotAnimated(path, aspectRatio, qApp, DependencyManager::get<WindowScriptingInterface>());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void Application::shareSnapshot(const QString& path) {
|
void Application::shareSnapshot(const QString& path) {
|
||||||
postLambdaEvent([path] {
|
postLambdaEvent([path] {
|
||||||
// not much to do here, everything is done in snapshot code...
|
// not much to do here, everything is done in snapshot code...
|
||||||
|
|
|
@ -266,7 +266,7 @@ public:
|
||||||
float getAvatarSimrate() const { return _avatarSimCounter.rate(); }
|
float getAvatarSimrate() const { return _avatarSimCounter.rate(); }
|
||||||
float getAverageSimsPerSecond() const { return _simCounter.rate(); }
|
float getAverageSimsPerSecond() const { return _simCounter.rate(); }
|
||||||
|
|
||||||
void takeSnapshot(bool notify, float aspectRatio = 0.0f);
|
void takeSnapshot(bool notify, bool includeAnimated = false, float aspectRatio = 0.0f);
|
||||||
void shareSnapshot(const QString& filename);
|
void shareSnapshot(const QString& filename);
|
||||||
|
|
||||||
model::SkyboxPointer getDefaultSkybox() const { return _defaultSkybox; }
|
model::SkyboxPointer getDefaultSkybox() const { return _defaultSkybox; }
|
||||||
|
|
|
@ -199,8 +199,8 @@ void WindowScriptingInterface::copyToClipboard(const QString& text) {
|
||||||
QApplication::clipboard()->setText(text);
|
QApplication::clipboard()->setText(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
void WindowScriptingInterface::takeSnapshot(bool notify, float aspectRatio) {
|
void WindowScriptingInterface::takeSnapshot(bool notify, bool includeAnimated, float aspectRatio) {
|
||||||
qApp->takeSnapshot(notify, aspectRatio);
|
qApp->takeSnapshot(notify, includeAnimated, aspectRatio);
|
||||||
}
|
}
|
||||||
|
|
||||||
void WindowScriptingInterface::shareSnapshot(const QString& path) {
|
void WindowScriptingInterface::shareSnapshot(const QString& path) {
|
||||||
|
|
|
@ -52,7 +52,7 @@ public slots:
|
||||||
QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = "");
|
QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = "");
|
||||||
void showAssetServer(const QString& upload = "");
|
void showAssetServer(const QString& upload = "");
|
||||||
void copyToClipboard(const QString& text);
|
void copyToClipboard(const QString& text);
|
||||||
void takeSnapshot(bool notify = true, float aspectRatio = 0.0f);
|
void takeSnapshot(bool notify = true, bool includeAnimated = false, float aspectRatio = 0.0f);
|
||||||
void shareSnapshot(const QString& path);
|
void shareSnapshot(const QString& path);
|
||||||
bool isPhysicsEnabled();
|
bool isPhysicsEnabled();
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ signals:
|
||||||
void domainChanged(const QString& domainHostname);
|
void domainChanged(const QString& domainHostname);
|
||||||
void svoImportRequested(const QString& url);
|
void svoImportRequested(const QString& url);
|
||||||
void domainConnectionRefused(const QString& reasonMessage, int reasonCode, const QString& extraInfo);
|
void domainConnectionRefused(const QString& reasonMessage, int reasonCode, const QString& extraInfo);
|
||||||
void snapshotTaken(const QString& path, bool notify);
|
void snapshotTaken(const QString& pathStillSnapshot, const QString& pathAnimatedSnapshot, bool notify);
|
||||||
void snapshotShared(const QString& error);
|
void snapshotShared(const QString& error);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
#include "LODManager.h"
|
#include "LODManager.h"
|
||||||
#include "Menu.h"
|
#include "Menu.h"
|
||||||
#include "Snapshot.h"
|
#include "Snapshot.h"
|
||||||
|
#include "SnapshotAnimated.h"
|
||||||
#include "UserActivityLogger.h"
|
#include "UserActivityLogger.h"
|
||||||
|
|
||||||
#include "AmbientOcclusionEffect.h"
|
#include "AmbientOcclusionEffect.h"
|
||||||
|
@ -83,6 +84,20 @@ void setupPreferences() {
|
||||||
auto preference = new BrowsePreference(SNAPSHOTS, "Put my snapshots here", getter, setter);
|
auto preference = new BrowsePreference(SNAPSHOTS, "Put my snapshots here", getter, setter);
|
||||||
preferences->addPreference(preference);
|
preferences->addPreference(preference);
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
auto getter = []()->bool { return SnapshotAnimated::alsoTakeAnimatedSnapshot.get(); };
|
||||||
|
auto setter = [](bool value) { SnapshotAnimated::alsoTakeAnimatedSnapshot.set(value); };
|
||||||
|
preferences->addPreference(new CheckPreference(SNAPSHOTS, "Take Animated GIF Snapshot with HUD Button", getter, setter));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto getter = []()->float { return SnapshotAnimated::snapshotAnimatedDuration.get(); };
|
||||||
|
auto setter = [](float value) { SnapshotAnimated::snapshotAnimatedDuration.set(value); };
|
||||||
|
auto preference = new SpinnerPreference(SNAPSHOTS, "Animated Snapshot Duration", getter, setter);
|
||||||
|
preference->setMin(3);
|
||||||
|
preference->setMax(10);
|
||||||
|
preference->setStep(1);
|
||||||
|
preferences->addPreference(preference);
|
||||||
|
}
|
||||||
|
|
||||||
// Scripts
|
// Scripts
|
||||||
{
|
{
|
||||||
|
|
|
@ -51,16 +51,24 @@ SnapshotMetaData* Snapshot::parseSnapshotData(QString snapshotPath) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
QImage shot(snapshotPath);
|
QUrl url;
|
||||||
|
|
||||||
// no location data stored
|
if (snapshotPath.right(3) == "jpg") {
|
||||||
if (shot.text(URL).isEmpty()) {
|
QImage shot(snapshotPath);
|
||||||
|
|
||||||
|
// no location data stored
|
||||||
|
if (shot.text(URL).isEmpty()) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsing URL
|
||||||
|
url = QUrl(shot.text(URL), QUrl::ParsingMode::StrictMode);
|
||||||
|
} else if (snapshotPath.right(3) == "gif") {
|
||||||
|
url = QUrl(DependencyManager::get<AddressManager>()->currentShareableAddress());
|
||||||
|
} else {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// parsing URL
|
|
||||||
QUrl url = QUrl(shot.text(URL), QUrl::ParsingMode::StrictMode);
|
|
||||||
|
|
||||||
SnapshotMetaData* data = new SnapshotMetaData();
|
SnapshotMetaData* data = new SnapshotMetaData();
|
||||||
data->setURL(url);
|
data->setURL(url);
|
||||||
|
|
||||||
|
@ -156,7 +164,11 @@ void Snapshot::uploadSnapshot(const QString& filename) {
|
||||||
file->open(QIODevice::ReadOnly);
|
file->open(QIODevice::ReadOnly);
|
||||||
|
|
||||||
QHttpPart imagePart;
|
QHttpPart imagePart;
|
||||||
imagePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/jpeg"));
|
if (filename.right(3) == "gif") {
|
||||||
|
imagePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/gif"));
|
||||||
|
} else {
|
||||||
|
imagePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/jpeg"));
|
||||||
|
}
|
||||||
imagePart.setHeader(QNetworkRequest::ContentDispositionHeader,
|
imagePart.setHeader(QNetworkRequest::ContentDispositionHeader,
|
||||||
QVariant("form-data; name=\"image\"; filename=\"" + file->fileName() + "\""));
|
QVariant("form-data; name=\"image\"; filename=\"" + file->fileName() + "\""));
|
||||||
imagePart.setBodyDevice(file);
|
imagePart.setBodyDevice(file);
|
||||||
|
|
139
interface/src/ui/SnapshotAnimated.cpp
Normal file
139
interface/src/ui/SnapshotAnimated.cpp
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
//
|
||||||
|
// SnapshotAnimated.cpp
|
||||||
|
// interface/src/ui
|
||||||
|
//
|
||||||
|
// Created by Zach Fox on 11/14/16.
|
||||||
|
// Copyright 2016 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
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <QtCore/QDateTime>
|
||||||
|
#include <QtCore/QObject>
|
||||||
|
#include <QtCore/QString>
|
||||||
|
#include <QtGui/QImage>
|
||||||
|
|
||||||
|
#include <QtConcurrent/qtconcurrentrun.h>
|
||||||
|
#include "SnapshotAnimated.h"
|
||||||
|
|
||||||
|
QTimer* SnapshotAnimated::snapshotAnimatedTimer = NULL;
|
||||||
|
qint64 SnapshotAnimated::snapshotAnimatedTimestamp = 0;
|
||||||
|
qint64 SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = 0;
|
||||||
|
bool SnapshotAnimated::snapshotAnimatedTimerRunning = false;
|
||||||
|
QString SnapshotAnimated::snapshotAnimatedPath;
|
||||||
|
QString SnapshotAnimated::snapshotStillPath;
|
||||||
|
QVector<QImage> SnapshotAnimated::snapshotAnimatedFrameVector;
|
||||||
|
QVector<qint64> SnapshotAnimated::snapshotAnimatedFrameDelayVector;
|
||||||
|
Application* SnapshotAnimated::app;
|
||||||
|
float SnapshotAnimated::aspectRatio;
|
||||||
|
QSharedPointer<WindowScriptingInterface> SnapshotAnimated::snapshotAnimatedDM;
|
||||||
|
GifWriter SnapshotAnimated::snapshotAnimatedGifWriter;
|
||||||
|
|
||||||
|
|
||||||
|
Setting::Handle<bool> SnapshotAnimated::alsoTakeAnimatedSnapshot("alsoTakeAnimatedSnapshot", true);
|
||||||
|
Setting::Handle<float> SnapshotAnimated::snapshotAnimatedDuration("snapshotAnimatedDuration", SNAPSNOT_ANIMATED_DURATION_SECS);
|
||||||
|
|
||||||
|
void SnapshotAnimated::saveSnapshotAnimated(QString pathStill, float aspectRatio, Application* app, QSharedPointer<WindowScriptingInterface> dm) {
|
||||||
|
// If we're not in the middle of capturing an animated snapshot...
|
||||||
|
if (SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp == 0) {
|
||||||
|
SnapshotAnimated::snapshotAnimatedTimer = new QTimer();
|
||||||
|
SnapshotAnimated::aspectRatio = aspectRatio;
|
||||||
|
SnapshotAnimated::app = app;
|
||||||
|
SnapshotAnimated::snapshotAnimatedDM = dm;
|
||||||
|
// Define the output location of the still and animated snapshots.
|
||||||
|
SnapshotAnimated::snapshotStillPath = pathStill;
|
||||||
|
SnapshotAnimated::snapshotAnimatedPath = pathStill;
|
||||||
|
SnapshotAnimated::snapshotAnimatedPath.replace("jpg", "gif");
|
||||||
|
|
||||||
|
// Ensure the snapshot timer is Precise (attempted millisecond precision)
|
||||||
|
SnapshotAnimated::snapshotAnimatedTimer->setTimerType(Qt::PreciseTimer);
|
||||||
|
|
||||||
|
// Connect the snapshotAnimatedTimer QTimer to the lambda slot function
|
||||||
|
QObject::connect((SnapshotAnimated::snapshotAnimatedTimer), &QTimer::timeout, captureFrames);
|
||||||
|
|
||||||
|
// Start the snapshotAnimatedTimer QTimer - argument for this is in milliseconds
|
||||||
|
SnapshotAnimated::snapshotAnimatedTimerRunning = true;
|
||||||
|
SnapshotAnimated::snapshotAnimatedTimer->start(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC);
|
||||||
|
// If we're already in the middle of capturing an animated snapshot...
|
||||||
|
} else {
|
||||||
|
// Just tell the dependency manager that the capture of the still snapshot has taken place.
|
||||||
|
emit dm->snapshotTaken(pathStill, "", false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SnapshotAnimated::captureFrames() {
|
||||||
|
if (SnapshotAnimated::snapshotAnimatedTimerRunning) {
|
||||||
|
// Get a screenshot from the display, then scale the screenshot down,
|
||||||
|
// then convert it to the image format the GIF library needs,
|
||||||
|
// then save all that to the QImage named "frame"
|
||||||
|
QImage frame(SnapshotAnimated::app->getActiveDisplayPlugin()->getScreenshot(SnapshotAnimated::aspectRatio));
|
||||||
|
frame = frame.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH);
|
||||||
|
SnapshotAnimated::snapshotAnimatedFrameVector.append(frame);
|
||||||
|
|
||||||
|
// If that was the first frame...
|
||||||
|
if (SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp == 0) {
|
||||||
|
// Record the current frame timestamp
|
||||||
|
SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch();
|
||||||
|
// Record the first frame timestamp
|
||||||
|
SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = SnapshotAnimated::snapshotAnimatedTimestamp;
|
||||||
|
SnapshotAnimated::snapshotAnimatedFrameDelayVector.append(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10);
|
||||||
|
// If this is an intermediate or the final frame...
|
||||||
|
} else {
|
||||||
|
// Push the current frame delay onto the vector
|
||||||
|
SnapshotAnimated::snapshotAnimatedFrameDelayVector.append(round(((float)(QDateTime::currentMSecsSinceEpoch() - SnapshotAnimated::snapshotAnimatedTimestamp)) / 10));
|
||||||
|
// Record the current frame timestamp
|
||||||
|
SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch();
|
||||||
|
|
||||||
|
// If that was the last frame...
|
||||||
|
if ((SnapshotAnimated::snapshotAnimatedTimestamp - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp) >= (SnapshotAnimated::snapshotAnimatedDuration.get() * MSECS_PER_SECOND)) {
|
||||||
|
SnapshotAnimated::snapshotAnimatedTimerRunning = false;
|
||||||
|
// Reset the current frame timestamp
|
||||||
|
SnapshotAnimated::snapshotAnimatedTimestamp = 0;
|
||||||
|
SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = 0;
|
||||||
|
|
||||||
|
// Kick off the thread that'll pack the frames into the GIF
|
||||||
|
QtConcurrent::run(processFrames);
|
||||||
|
// Stop the snapshot QTimer. This action by itself DOES NOT GUARANTEE
|
||||||
|
// that the slot will not be called again in the future.
|
||||||
|
// See: http://lists.qt-project.org/pipermail/qt-interest-old/2009-October/013926.html
|
||||||
|
SnapshotAnimated::snapshotAnimatedTimer->stop();
|
||||||
|
delete SnapshotAnimated::snapshotAnimatedTimer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SnapshotAnimated::processFrames() {
|
||||||
|
uint32_t width = SnapshotAnimated::snapshotAnimatedFrameVector[0].width();
|
||||||
|
uint32_t height = SnapshotAnimated::snapshotAnimatedFrameVector[0].height();
|
||||||
|
|
||||||
|
// Create the GIF from the temporary files
|
||||||
|
// Write out the header and beginning of the GIF file
|
||||||
|
GifBegin(
|
||||||
|
&(SnapshotAnimated::snapshotAnimatedGifWriter),
|
||||||
|
qPrintable(SnapshotAnimated::snapshotAnimatedPath),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
1); // "1" means "yes there is a delay" with this GifCreator library.
|
||||||
|
for (int itr = 0; itr < SnapshotAnimated::snapshotAnimatedFrameVector.size(); itr++) {
|
||||||
|
// Write each frame to the GIF
|
||||||
|
GifWriteFrame(&(SnapshotAnimated::snapshotAnimatedGifWriter),
|
||||||
|
(uint8_t*)SnapshotAnimated::snapshotAnimatedFrameVector[itr].convertToFormat(QImage::Format_RGBA8888).bits(),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
SnapshotAnimated::snapshotAnimatedFrameDelayVector[itr]);
|
||||||
|
}
|
||||||
|
// Write out the end of the GIF
|
||||||
|
GifEnd(&(SnapshotAnimated::snapshotAnimatedGifWriter));
|
||||||
|
|
||||||
|
// Clear out the frame and frame delay vectors.
|
||||||
|
// Also release the memory not required to store the items.
|
||||||
|
SnapshotAnimated::snapshotAnimatedFrameVector.clear();
|
||||||
|
SnapshotAnimated::snapshotAnimatedFrameVector.squeeze();
|
||||||
|
SnapshotAnimated::snapshotAnimatedFrameDelayVector.clear();
|
||||||
|
SnapshotAnimated::snapshotAnimatedFrameDelayVector.squeeze();
|
||||||
|
|
||||||
|
// Let the dependency manager know that the snapshots have been taken.
|
||||||
|
emit SnapshotAnimated::snapshotAnimatedDM->snapshotTaken(SnapshotAnimated::snapshotStillPath, SnapshotAnimated::snapshotAnimatedPath, false);
|
||||||
|
}
|
58
interface/src/ui/SnapshotAnimated.h
Normal file
58
interface/src/ui/SnapshotAnimated.h
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
//
|
||||||
|
// SnapshotAnimated.h
|
||||||
|
// interface/src/ui
|
||||||
|
//
|
||||||
|
// Created by Zach Fox on 11/14/16.
|
||||||
|
// Copyright 2016 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
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef hifi_SnapshotAnimated_h
|
||||||
|
#define hifi_SnapshotAnimated_h
|
||||||
|
|
||||||
|
#include <QtCore/QVector>
|
||||||
|
#include <Application.h>
|
||||||
|
#include <DependencyManager.h>
|
||||||
|
#include <GifCreator.h>
|
||||||
|
#include <qtimer.h>
|
||||||
|
#include <SettingHandle.h>
|
||||||
|
#include "scripting/WindowScriptingInterface.h"
|
||||||
|
|
||||||
|
// If the snapshot width or the framerate are too high for the
|
||||||
|
// application to handle, the framerate of the output GIF will drop.
|
||||||
|
#define SNAPSNOT_ANIMATED_WIDTH (480)
|
||||||
|
// This value should divide evenly into 100. Snapshot framerate is NOT guaranteed.
|
||||||
|
#define SNAPSNOT_ANIMATED_TARGET_FRAMERATE (25)
|
||||||
|
#define SNAPSNOT_ANIMATED_DURATION_SECS (3)
|
||||||
|
#define SNAPSNOT_ANIMATED_DURATION_MSEC (SNAPSNOT_ANIMATED_DURATION_SECS*1000)
|
||||||
|
|
||||||
|
#define SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC (1000/SNAPSNOT_ANIMATED_TARGET_FRAMERATE)
|
||||||
|
|
||||||
|
class SnapshotAnimated {
|
||||||
|
private:
|
||||||
|
static QTimer* snapshotAnimatedTimer;
|
||||||
|
static qint64 snapshotAnimatedTimestamp;
|
||||||
|
static qint64 snapshotAnimatedFirstFrameTimestamp;
|
||||||
|
static bool snapshotAnimatedTimerRunning;
|
||||||
|
static QString snapshotStillPath;
|
||||||
|
|
||||||
|
static QString snapshotAnimatedPath;
|
||||||
|
static QVector<QImage> snapshotAnimatedFrameVector;
|
||||||
|
static QVector<qint64> snapshotAnimatedFrameDelayVector;
|
||||||
|
static QSharedPointer<WindowScriptingInterface> snapshotAnimatedDM;
|
||||||
|
static Application* app;
|
||||||
|
static float aspectRatio;
|
||||||
|
|
||||||
|
static GifWriter snapshotAnimatedGifWriter;
|
||||||
|
|
||||||
|
static void captureFrames();
|
||||||
|
static void processFrames();
|
||||||
|
public:
|
||||||
|
static void saveSnapshotAnimated(QString pathStill, float aspectRatio, Application* app, QSharedPointer<WindowScriptingInterface> dm);
|
||||||
|
static Setting::Handle<bool> alsoTakeAnimatedSnapshot;
|
||||||
|
static Setting::Handle<float> snapshotAnimatedDuration;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // hifi_SnapshotAnimated_h
|
|
@ -1,48 +1,48 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Share</title>
|
<title>Share</title>
|
||||||
<link rel="stylesheet" type="text/css" href="css/edit-style.css">
|
<link rel="stylesheet" type="text/css" href="css/edit-style.css">
|
||||||
<link rel="stylesheet" type="text/css" href="css/SnapshotReview.css">
|
<link rel="stylesheet" type="text/css" href="css/SnapshotReview.css">
|
||||||
<script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>
|
<script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>
|
||||||
<script type="text/javascript" src="js/eventBridgeLoader.js"></script>
|
<script type="text/javascript" src="js/eventBridgeLoader.js"></script>
|
||||||
<script type="text/javascript" src="js/SnapshotReview.js"></script>
|
<script type="text/javascript" src="js/SnapshotReview.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="snapshot-container">
|
<div class="snapshot-container">
|
||||||
<div class="snapshot-column-left">
|
<div class="snapshot-column-left">
|
||||||
<div class="snapsection">
|
<div class="snapsection">
|
||||||
<label class="title">Snapshot successfully saved!</label>
|
<label class="title">Snapshot successfully saved!</label>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div class="snapsection">
|
<div class="snapsection">
|
||||||
<div id="sharing">
|
<div id="sharing">
|
||||||
<div class="prompt">Would you like to share your pic in the Snapshots feed?</div>
|
<div class="prompt">Would you like to share your pics in the Snapshots feed?</div>
|
||||||
<div class="button">
|
<div class="button">
|
||||||
<span class="compound-button">
|
<span class="compound-button">
|
||||||
<input type="button" class="blue" id="share" value="Share in Feed" onclick="shareSelected()"/>
|
<input type="button" class="blue" id="share" value="Share in Feed" onclick="shareSelected()" />
|
||||||
<span class="glyph"></span>
|
<span class="glyph"></span>
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="button">
|
||||||
|
<input type="button" class="black" id="close" value="Don't Share" onclick="doNotShare()" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<div class="snapsection">
|
||||||
|
<span class="setting">
|
||||||
|
<input type="button" class="glyph naked" id="snapshotSettings" value="@" onclick="snapshotSettings()" />
|
||||||
|
<label for="snapshotSettings">Snapshot settings</label>
|
||||||
|
</span>
|
||||||
|
<span class="setting checkbox">
|
||||||
|
<input id="openFeed" type="checkbox" checked />
|
||||||
|
<label for="openFeed">Open feed after</label>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="button">
|
|
||||||
<input type="button" class="black" id="close" value="Don't Share" onclick="doNotShare()"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<div id="snapshot-images" class="snapshot-column-right">
|
||||||
<div class="snapsection">
|
|
||||||
<span class="setting">
|
|
||||||
<input type="button" class="glyph naked" id="snapshotSettings" value="@" onclick="snapshotSettings()" />
|
|
||||||
<label for="snapshotSettings">Snapshot settings</label>
|
|
||||||
</span>
|
|
||||||
<span class="setting checkbox">
|
|
||||||
<input id="openFeed" type="checkbox" checked/>
|
|
||||||
<label for="openFeed">Open feed after</label>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div id="snapshot-images" class="snapshot-column-right"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -12,6 +12,9 @@
|
||||||
|
|
||||||
var paths = [], idCounter = 0, useCheckboxes;
|
var paths = [], idCounter = 0, useCheckboxes;
|
||||||
function addImage(data) {
|
function addImage(data) {
|
||||||
|
if (!data.localPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
var div = document.createElement("DIV"),
|
var div = document.createElement("DIV"),
|
||||||
input = document.createElement("INPUT"),
|
input = document.createElement("INPUT"),
|
||||||
label = document.createElement("LABEL"),
|
label = document.createElement("LABEL"),
|
||||||
|
@ -20,21 +23,22 @@ function addImage(data) {
|
||||||
function toggle() { data.share = input.checked; }
|
function toggle() { data.share = input.checked; }
|
||||||
img.src = data.localPath;
|
img.src = data.localPath;
|
||||||
div.appendChild(img);
|
div.appendChild(img);
|
||||||
data.share = true;
|
|
||||||
if (useCheckboxes) { // I'd rather use css, but the included stylesheet is quite particular.
|
if (useCheckboxes) { // I'd rather use css, but the included stylesheet is quite particular.
|
||||||
// Our stylesheet(?) requires input.id to match label.for. Otherwise input doesn't display the check state.
|
// Our stylesheet(?) requires input.id to match label.for. Otherwise input doesn't display the check state.
|
||||||
label.setAttribute('for', id); // cannot do label.for =
|
label.setAttribute('for', id); // cannot do label.for =
|
||||||
input.id = id;
|
input.id = id;
|
||||||
input.type = "checkbox";
|
input.type = "checkbox";
|
||||||
input.checked = true;
|
input.checked = (id === "p0");
|
||||||
|
data.share = input.checked;
|
||||||
input.addEventListener('change', toggle);
|
input.addEventListener('change', toggle);
|
||||||
div.class = "property checkbox";
|
div.class = "property checkbox";
|
||||||
div.appendChild(input);
|
div.appendChild(input);
|
||||||
div.appendChild(label);
|
div.appendChild(label);
|
||||||
|
} else {
|
||||||
|
data.share = true;
|
||||||
}
|
}
|
||||||
document.getElementById("snapshot-images").appendChild(div);
|
document.getElementById("snapshot-images").appendChild(div);
|
||||||
paths.push(data);
|
paths.push(data);
|
||||||
|
|
||||||
}
|
}
|
||||||
function handleShareButtons(shareMsg) {
|
function handleShareButtons(shareMsg) {
|
||||||
var openFeed = document.getElementById('openFeed');
|
var openFeed = document.getElementById('openFeed');
|
||||||
|
@ -49,7 +53,7 @@ function handleShareButtons(shareMsg) {
|
||||||
window.onload = function () {
|
window.onload = function () {
|
||||||
// Something like the following will allow testing in a browser.
|
// Something like the following will allow testing in a browser.
|
||||||
//addImage({localPath: 'c:/Users/howar/OneDrive/Pictures/hifi-snap-by--on-2016-07-27_12-58-43.jpg'});
|
//addImage({localPath: 'c:/Users/howar/OneDrive/Pictures/hifi-snap-by--on-2016-07-27_12-58-43.jpg'});
|
||||||
//addImage({localPath: 'http://lorempixel.com/1512/1680'});
|
//addImage({ localPath: 'http://lorempixel.com/1512/1680' });
|
||||||
openEventBridge(function () {
|
openEventBridge(function () {
|
||||||
// Set up a handler for receiving the data, and tell the .js we are ready to receive it.
|
// Set up a handler for receiving the data, and tell the .js we are ready to receive it.
|
||||||
EventBridge.scriptEventReceived.connect(function (message) {
|
EventBridge.scriptEventReceived.connect(function (message) {
|
||||||
|
|
|
@ -522,13 +522,13 @@ function onEditError(msg) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function onSnapshotTaken(path, notify) {
|
function onSnapshotTaken(pathStillSnapshot, pathAnimatedSnapshot, notify) {
|
||||||
if (notify) {
|
if (notify) {
|
||||||
var imageProperties = {
|
var imageProperties = {
|
||||||
path: "file:///" + path,
|
path: "file:///" + pathStillSnapshot,
|
||||||
aspectRatio: Window.innerWidth / Window.innerHeight
|
aspectRatio: Window.innerWidth / Window.innerHeight
|
||||||
};
|
};
|
||||||
createNotification(wordWrap("Snapshot saved to " + path), NotificationType.SNAPSHOT, imageProperties);
|
createNotification(wordWrap("Snapshot saved to " + pathStillSnapshot), NotificationType.SNAPSHOT, imageProperties);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ var SNAPSHOT_REVIEW_URL = Script.resolvePath("html/SnapshotReview.html");
|
||||||
|
|
||||||
var outstanding;
|
var outstanding;
|
||||||
function confirmShare(data) {
|
function confirmShare(data) {
|
||||||
var dialog = new OverlayWebWindow('Snapshot Review', SNAPSHOT_REVIEW_URL, 800, 320);
|
var dialog = new OverlayWebWindow('Snapshot Review', SNAPSHOT_REVIEW_URL, 800, 520);
|
||||||
function onMessage(message) {
|
function onMessage(message) {
|
||||||
// Receives message from the html dialog via the qwebchannel EventBridge. This is complicated by the following:
|
// Receives message from the html dialog via the qwebchannel EventBridge. This is complicated by the following:
|
||||||
// 1. Although we can send POJOs, we cannot receive a toplevel object. (Arrays of POJOs are fine, though.)
|
// 1. Although we can send POJOs, we cannot receive a toplevel object. (Arrays of POJOs are fine, though.)
|
||||||
|
@ -120,11 +120,11 @@ function onClicked() {
|
||||||
|
|
||||||
// take snapshot (with no notification)
|
// take snapshot (with no notification)
|
||||||
Script.setTimeout(function () {
|
Script.setTimeout(function () {
|
||||||
Window.takeSnapshot(false, 1.91);
|
Window.takeSnapshot(false, true, 1.91);
|
||||||
}, SNAPSHOT_DELAY);
|
}, SNAPSHOT_DELAY);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetButtons(path, notify) {
|
function resetButtons(pathStillSnapshot, pathAnimatedSnapshot, notify) {
|
||||||
// show overlays if they were on
|
// show overlays if they were on
|
||||||
if (resetOverlays) {
|
if (resetOverlays) {
|
||||||
Menu.setIsOptionChecked("Overlays", true);
|
Menu.setIsOptionChecked("Overlays", true);
|
||||||
|
@ -141,7 +141,8 @@ function resetButtons(path, notify) {
|
||||||
|
|
||||||
// last element in data array tells dialog whether we can share or not
|
// last element in data array tells dialog whether we can share or not
|
||||||
confirmShare([
|
confirmShare([
|
||||||
{ localPath: path },
|
{ localPath: pathAnimatedSnapshot },
|
||||||
|
{ localPath: pathStillSnapshot },
|
||||||
{
|
{
|
||||||
canShare: !!location.placename,
|
canShare: !!location.placename,
|
||||||
openFeedAfterShare: shouldOpenFeedAfterShare()
|
openFeedAfterShare: shouldOpenFeedAfterShare()
|
||||||
|
|
Loading…
Reference in a new issue