Merge remote-tracking branch 'origin/beta'

This commit is contained in:
Bruno Herbelin
2025-11-18 17:45:49 +01:00
43 changed files with 1232 additions and 257 deletions

View File

@@ -103,6 +103,30 @@ if(UNIX)
${X11_INCLUDE_DIR}
)
# XInput2 for tablet pressure support (optional)
find_package(X11 COMPONENTS Xi)
if(X11_Xi_FOUND)
add_definitions(-DHAVE_X11TABLETINPUT)
macro_log_feature(X11_Xi_FOUND "XInput2" "X11 Input extension for tablet support" "https://www.x.org/wiki/" FALSE)
else()
# alternatively libinput and libudev for tablet pressure support
if (PKG_CONFIG_FOUND)
pkg_check_modules(LIBINPUT libinput>=1.19)
pkg_check_modules(LIBUDEV libudev)
endif()
if(LIBINPUT_FOUND AND LIBUDEV_FOUND)
include_directories(${LIBINPUT_INCLUDE_DIRS} ${LIBUDEV_INCLUDE_DIRS})
link_directories(${LIBINPUT_LIBRARY_DIRS} ${LIBUDEV_LIBRARY_DIRS})
add_definitions(-DHAVE_LIBINPUT)
macro_log_feature(LIBINPUT_FOUND "libinput" "Input device library for tablet support" "https://wayland.freedesktop.org/libinput" FALSE)
macro_log_feature(LIBUDEV_FOUND "libudev" "Device management library" "https://www.freedesktop.org/software/systemd/man/libudev.html" FALSE)
else()
message(STATUS "XInput2 (Xi) or libinput or libudev not found - tablet pressure support will be disabled")
endif()
endif()
endif()
add_definitions(-DUNIX)
elseif(WIN32)

View File

@@ -17,7 +17,7 @@ Publishing new a release
https://github.com/brunoherbelin/vimix/tree/master/flatpak
- Test the snap for Beta
$ snap remove vimix
$ snapcraft (from vimix base dir)
$ snapcraft pack (from vimix base dir)
$ snap install ./vimix_0.X.Y_amd64.snap --devmode
$ snap connections vimix

View File

@@ -32,11 +32,8 @@ If not already installed, install the builder and the flathub repository:
Install the runtime environments:
flatpak install org.gnome.Sdk/x86_64
flatpak install org.gnome.Platform
_Select version **49** in the list of proposed versions_
flatpak install org.freedesktop.Sdk/x86_64/25.08
flatpak install org.freedesktop.Platform/25.08
### 2. Build vimix flatpak

View File

@@ -1,15 +1,17 @@
{
"app-id": "io.github.brunoherbelin.Vimix",
"runtime": "org.gnome.Platform",
"runtime-version": "49",
"sdk": "org.gnome.Sdk",
"runtime": "org.freedesktop.Platform",
"runtime-version": "25.08",
"sdk": "org.freedesktop.Sdk",
"command": "launch_vimix.sh",
"rename-desktop-file": "vimix.desktop",
"rename-icon": "vimix",
"finish-args": [
"--socket=x11",
"--socket=wayland",
"--socket=fallback-x11",
"--socket=pulseaudio",
"--socket=session-bus",
"--share=ipc",
"--share=network",
"--device=dri",
@@ -49,35 +51,6 @@
}
]
},
{
"name": "x264",
"config-opts": [
"--enable-shared"
],
"sources": [
{
"type": "git",
"commit": "b35605ace3ddf7c1a5d67a2eb553f034aef41d55",
"url": "https://code.videolan.org/videolan/x264.git"
}
]
},
{
"name": "x265",
"buildsystem": "cmake-ninja",
"subdir": "source",
"config-opts": [
"-DCMAKE_BUILD_TYPE=Release",
"-DCMAKE_POSITION_INDEPENDENT_CODE=ON"
],
"sources": [
{
"type": "git",
"commit": "b858f483959b805895f77e700d81544be84e4c6b",
"url": "https://bitbucket.org/multicoreware/x265_git.git"
}
]
},
{
"name": "srt",
"buildsystem": "cmake-ninja",
@@ -110,45 +83,6 @@
}
]
},
{
"name": "gstreamer",
"buildsystem": "meson",
"builddir": true,
"config-opts": [
"-Ddoc=disabled",
"-Dpython=disabled",
"-Dges=disabled",
"-Drs=disabled",
"-Dgst-examples=disabled",
"-Drtsp_server=disabled",
"-Ddevtools=disabled",
"-Dqt5=disabled",
"-Dlibav=enabled",
"-Dbase=enabled",
"-Dgood=enabled",
"-Dgst-plugins-good:v4l2=enabled",
"-Dgpl=enabled",
"-Dbad=enabled",
"-Dgst-plugins-bad:x265=enabled",
"-Dgst-plugins-bad:srt=enabled",
"-Dgst-plugins-bad:shm=enabled",
"-Dugly=enabled",
"-Dgst-plugins-ugly:x264=enabled",
"-Dvaapi=enabled"
],
"build-options": {
"build-args": [ "--share=network" ]
},
"sources": [
{
"type": "git",
"tag": "1.26.8",
"commit": "16d77e12ad213ef24e76a8cc34d347b8221c9975",
"url": "https://gitlab.freedesktop.org/gstreamer/gstreamer.git",
"disable-submodules": false
}
]
},
{
"name": "shmdata",
"buildsystem": "cmake-ninja",

122
osx/TabletInput_macos.mm Normal file
View File

@@ -0,0 +1,122 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2025 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#ifdef APPLE
#include "TabletInput.h"
#include "Log.h"
#import <Cocoa/Cocoa.h>
TabletInput::TabletInput()
: data_({0.0f, false, 0.0f, 0.0f, false, false})
, active_(false)
, monitor_(nullptr)
{
}
TabletInput::~TabletInput()
{
terminate();
}
bool TabletInput::init()
{
// Create event monitor for tablet events
NSEventMask mask = NSEventMaskTabletPoint |
NSEventMaskTabletProximity |
NSEventMaskLeftMouseDown |
NSEventMaskLeftMouseUp |
NSEventMaskLeftMouseDragged;
// Capture reference to data_ for the block
TabletData *dataPtr = &data_;
id monitor = [NSEvent addLocalMonitorForEventsMatchingMask:mask
handler:^NSEvent *(NSEvent *event) {
// Check for tablet point events
if (event.type == NSEventTypeTabletPoint ||
(event.subtype == NSEventSubtypeTabletPoint)) {
dataPtr->has_pressure = true;
dataPtr->pressure = event.pressure;
dataPtr->tilt_x = event.tilt.x;
dataPtr->tilt_y = event.tilt.y;
dataPtr->tip_down = (event.pressure > 0.0f);
dataPtr->in_proximity = true;
}
// Handle proximity events
else if (event.type == NSEventTypeTabletProximity) {
dataPtr->in_proximity = event.isEnteringProximity;
if (!dataPtr->in_proximity) {
dataPtr->pressure = 0.0f;
dataPtr->tilt_x = 0.0f;
dataPtr->tilt_y = 0.0f;
dataPtr->tip_down = false;
}
}
// Fallback to regular mouse events if tablet is in proximity
else if (event.type == NSEventTypeLeftMouseDown) {
if (dataPtr->in_proximity && dataPtr->pressure == 0.0f) {
// Set minimal pressure if tablet is near but not reporting pressure
dataPtr->pressure = 0.1f;
dataPtr->tip_down = true;
}
}
else if (event.type == NSEventTypeLeftMouseUp) {
if (dataPtr->in_proximity) {
dataPtr->tip_down = false;
if (!dataPtr->in_proximity) {
dataPtr->pressure = 0.0f;
}
}
}
return event;
}];
monitor_ = (__bridge_retained void*)monitor;
active_ = true;
Log::Info("TabletInput: macOS tablet input initialized (NSEvent)");
return true;
}
void TabletInput::pollEvents()
{
// Events are handled automatically by the monitor callback
// Nothing to do here for macOS
}
void TabletInput::terminate()
{
if (monitor_) {
id monitor = (__bridge_transfer id)monitor_;
[NSEvent removeMonitor:monitor];
monitor_ = nullptr;
}
data_.pressure = 0.0f;
data_.tilt_x = 0.0f;
data_.tilt_y = 0.0f;
data_.in_proximity = false;
data_.tip_down = false;
active_ = false;
}
#endif // APPLE

Binary file not shown.

View File

@@ -1,19 +1,21 @@
#!/bin/bash
# Test if running under wayland
if [ -z "$WAYLAND_DISPLAY" ]; then
# not Wayland, nothing special
vimix "$@"
else
# Wayland: test if there is an nvidia GPU
gpu=$(lspci | grep -i '.* vga .* nvidia .*')
shopt -s nocasematch
if [[ $gpu == *' nvidia '* ]]; then
# with nvidia, request Wayland render offload
printf 'Nvidia GPU present: %s\n' "$gpu"
__NV_PRIME_RENDER_OFFLOAD=1 __GLX_VENDOR_LIBRARY_NAME=nvidia vimix "$@"
# test if there is an nvidia GPU
gpu=$(lspci | grep -i '.* vga .* nvidia .*')
shopt -s nocasematch
if [[ $gpu == *' nvidia '* ]]; then
# with nvidia, request Wayland render offload
printf 'Nvidia GPU present: %s\n' "$gpu"
# Test if running under wayland
if [ -z "$WAYLAND_DISPLAY" ]; then
# not Wayland
__GLX_VENDOR_LIBRARY_NAME=nvidia vimix "$@"
else
# otherwise, nothing special
vimix "$@"
# Wayland:
__NV_PRIME_RENDER_OFFLOAD=1 __GLX_VENDOR_LIBRARY_NAME=nvidia vimix "$@"
fi
else
# otherwise, nothing special
vimix "$@"
fi

View File

@@ -21,26 +21,27 @@ uniform vec3 brush;
uniform int option;
uniform int effect;
float sdBox( in vec2 p, in float b)
float sdBox( in vec2 v1, in vec2 v2, float r )
{
vec2 q = abs(p) - vec2(b);
vec2 ba = v2 - v1;
vec2 pa = -v1;
float h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 );
vec2 q = abs(pa -h*ba) - vec2(r);
float d = min( max(q.x, q.y), 0.0) + length(max(q,0.0));
return 1.0 - abs(d) * step(d, 0.0);
return 1.0 - abs( mix( 0.5, 1.0, d) ) * step(d, 0.0);
}
float sdCircle( in vec2 p, in float b)
float sdSegment( in vec2 v1, in vec2 v2, float r )
{
return ( length( p ) / b );
vec2 ba = v2 - v1;
vec2 pa = -v1;
float h = clamp( dot(pa,ba) / dot(ba,ba), 0.0, 1.0 );
return length(pa-h*ba) / r;
}
float sdElipse( in vec2 p, in float b)
{
return ( length( p ) / b );
}
const mat3 KERNEL = mat3( 0.0625, 0.125, 0.0625,
0.125, 0.25, 0.125,
0.0625, 0.125, 0.0625);
const mat3 KERNEL = mat3( 0.0555555, 0.111111, 0.0555555,
0.111111, 0.333334, 0.111111,
0.0555555, 0.111111, 0.0555555);
vec3 gaussian()
{
@@ -71,14 +72,15 @@ void main()
// fragment coordinates
vec2 uv = -1.0 + 2.0 * gl_FragCoord.xy / iResolution.xy;
// adjust coordinates to match scaling area
uv.x *= cursor.z ;
uv.y *= cursor.w ;
uv.x *= size.x ;
uv.y *= size.y ;
// cursor coordinates
vec2 cursor = vec2(cursor.x, - cursor.y);
vec2 cur = vec2(cursor.x, - cursor.y);
vec2 cur_prev = vec2(cursor.z, - cursor.w);
// use distance function relative to length brush.x (size), depending on brush.z (shape):
// - brush.z = 0 : circle shape
// - brush.z = 1 : square shape
float d = (1.0 -brush.z) * sdCircle(cursor-uv, brush.x) + brush.z * sdBox(uv-cursor, brush.x);
float d = (1.0 -brush.z) * sdSegment(cur_prev - uv, cur - uv, brush.x) + brush.z * sdBox(cur_prev - uv, cur - uv, brush.x);
// modify only the pixels inside the brush
if( d < 1.0 )

View File

@@ -33,6 +33,12 @@
<binary>vimix</binary>
</provides>
<releases>
<release version="0.8.5" date="2025-11-18">
<description>
<p>Version 0.8.5</p>
</description>
<url>https://github.com/brunoherbelin/vimix/releases/tag/0.8.5</url>
</release>
<release version="0.8.4" date="2025-01-04">
<description>
<p>Version 0.8.4</p>

View File

@@ -1,6 +1,6 @@
name: vimix
base: core24
version: '0.8.4'
version: '0.8.5'
summary: Video live mixer
title: vimix
description: |

View File

@@ -103,12 +103,35 @@ set(VMIX_SRCS
Audio.cpp
TextSource.cpp
ShaderSource.cpp
TabletInput.cpp
)
#####
##### DEFINE THE TARGET (OS specific)
#####
IF(APPLE)
# Add macOS tablet input implementation
list(APPEND VMIX_SRCS ${CMAKE_SOURCE_DIR}/osx/TabletInput_macos.mm)
# Enable Objective-C++
set_source_files_properties(${CMAKE_SOURCE_DIR}/osx/TabletInput_macos.mm PROPERTIES COMPILE_FLAGS "-x objective-c++")
ELSE(APPLE)
# Add Linux tablet input implementation
if(X11_Xi_FOUND)
# Use X11/XInput2 version (works in Flatpak and everywhere)
list(APPEND VMIX_SRCS TabletInput_x11.cpp)
else()
if(LIBINPUT_FOUND AND LIBUDEV_FOUND)
# Use X11/XInput2 version (works in Flatpak and everywhere)
list(APPEND VMIX_SRCS TabletInput_linux_libinput.cpp)
endif()
endif()
ENDIF(APPLE)
IF(APPLE)
# set icon
@@ -149,6 +172,16 @@ ELSE(APPLE)
X11::xcb
)
# Add XInput2 library for tablet support on Linux (if available)
if(X11_Xi_FOUND)
list(APPEND PLATFORM_LIBS ${X11_Xi_LIB})
endif()
# Add libinput and libudev for tablet support on Linux (if available)
if(LIBINPUT_FOUND AND LIBUDEV_FOUND)
list(APPEND PLATFORM_LIBS ${LIBINPUT_LIBRARIES} ${LIBUDEV_LIBRARIES})
endif()
ENDIF(APPLE)
#####

View File

@@ -970,6 +970,13 @@ bool Control::receiveSourceAttribute(Source *target, const std::string &attribut
}
target->setImageProcessingEnabled(on > 0.5f);
}
else if ( attribute.compare(OSC_SOURCE_FLAG) == 0) {
float f = -1.f;
if (!arguments.Eos()) {
arguments >> f >> osc::EndMessage;
}
target->call( new Flag( f ));
}
/// e.g. '/vimix/current/seek f 0.25' ; seek to 25% of duration
/// e.g. '/vimix/current/seek iiii 0 0 25 500' ; seek to time
else if ( attribute.compare(OSC_SOURCE_SEEK) == 0) {

View File

@@ -78,6 +78,7 @@
#define OSC_SOURCE_FILTER "/filter"
#define OSC_SOURCE_UNIFORM "/uniform"
#define OSC_SOURCE_BLENDING "/blending"
#define OSC_SOURCE_FLAG "/flag"
#define OSC_SESSION "/session"
#define OSC_SESSION_VERSION "/version"

View File

@@ -441,9 +441,15 @@ GstBusSyncReply FrameGrabber::signal_handler(GstBus *, GstMessage *msg, gpointer
// inform user
GError *error;
gst_message_parse_error(msg, &error, NULL);
Log::Warning("FrameGrabber Error %s : %s",
std::to_string(reinterpret_cast<FrameGrabber *>(ptr)->id()).c_str(),
error->message);
FrameGrabber *fg = reinterpret_cast<FrameGrabber *>(ptr);
if (fg) {
Log::Warning("FrameGrabber Error %s : %s",
std::to_string(fg->id()).c_str(),
error->message);
fg->endofstream_=true;
}
else
Log::Warning("FrameGrabber Error : %s", error->message);
g_error_free(error);
// } else {
// g_printerr("FrameGrabber msg %s \n", GST_MESSAGE_TYPE_NAME(msg));

View File

@@ -1,8 +1,6 @@
#ifndef GEOMETRYVIEW_H
#define GEOMETRYVIEW_H
#define ENABLE_CANVAS
#include "View.h"
struct Canvas

View File

@@ -140,7 +140,7 @@ void MaskShader::reset()
size = glm::vec2(1.f, 1.f);
// default brush
cursor = glm::vec4(-10.f, -10.f, 1.f, 1.f);
cursor = glm::vec4(-10.f, -10.f, -10.f, -10.f);
brush = glm::vec3(0.5f, 0.1f, 0.f);
option = 0;
effect = 0;

View File

@@ -17,6 +17,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include "MediaSource.h"
#include <string>
#include <regex>
@@ -39,6 +40,7 @@
#include "SourceCallback.h"
#include "ControlManager.h"
#include "Metronome.h"
#include "MediaPlayer.h"
#include "InputMappingWindow.h"
@@ -119,9 +121,9 @@ Target InputMappingWindow::ComboSelectTarget(const Target &current)
return selected;
}
uint InputMappingWindow::ComboSelectCallback(uint current, bool imageprocessing)
uint InputMappingWindow::ComboSelectCallback(uint current, bool imageprocessing, bool ismediaplayer)
{
const char* callback_names[23] = { "Select",
const char* callback_names[24] = { "Select",
ICON_FA_BULLSEYE " Alpha",
ICON_FA_BULLSEYE " Loom",
ICON_FA_OBJECT_UNGROUP " Geometry",
@@ -133,6 +135,7 @@ uint InputMappingWindow::ComboSelectCallback(uint current, bool imageprocessing)
ICON_FA_PLAY_CIRCLE " Speed",
ICON_FA_PLAY_CIRCLE " Fast forward",
ICON_FA_PLAY_CIRCLE " Seek",
ICON_FA_PLAY_CIRCLE " Flag",
" None",
" None",
" None",
@@ -148,7 +151,8 @@ uint InputMappingWindow::ComboSelectCallback(uint current, bool imageprocessing)
uint selected = 0;
if (ImGui::BeginCombo("##ComboSelectCallback", callback_names[current]) ) {
for (uint i = SourceCallback::CALLBACK_ALPHA; i <= SourceCallback::CALLBACK_SEEK; ++i){
for (uint i = SourceCallback::CALLBACK_ALPHA;
i <= (ismediaplayer ? SourceCallback::CALLBACK_FLAG : SourceCallback::CALLBACK_PLAY) ; ++i){
if ( ImGui::Selectable( callback_names[i]) ) {
selected = i;
}
@@ -422,72 +426,56 @@ void InputMappingWindow::SliderParametersCallback(SourceCallback *callback, cons
bool bd = edited->bidirectional();
if ( ImGuiToolkit::IconToggle(2, 13, 3, 13, &bd, press_tooltip ) )
edited->setBidirectional(bd);
// get value (gst time) and convert to hh mm s.ms
guint64 ms = GST_TIME_AS_MSECONDS(edited->value());
guint64 hh = ms / 3600000;
guint64 mm = (ms % 3600000) / 60000;
ms -= (hh * 3600000 + mm * 60000);
float sec = (float) (ms) / 1000.f;
// filtering for reading MM:SS.MS text entry
static bool valid = true;
static std::regex RegExTime("([0-9]+\\:)?([0-9]+\\:)?([0-5][0-9]|[0-9])((\\.|\\,)[0-9]+)?");
struct TextFilters { static int FilterTime(ImGuiInputTextCallbackData* data) {
if (data->EventChar < 256 && strchr("0123456789.,:", (char)data->EventChar)) return 0; return 1; }
};
char buf6[64] = "";
snprintf(buf6, 64, "%lu:%lu:%.2f", (unsigned long) hh, (unsigned long) mm, sec );
// Text input field for MM:SS:MS seek target time
ImGui::SetNextItemWidth(right_align);
ImGui::SameLine(0, IMGUI_SAME_LINE / 2);
ImGui::PushStyleColor(ImGuiCol_Text,
ImVec4(1.0f, valid ? 1.0f : 0.2f, valid ? 1.0f : 0.2f, 1.f));
ImGui::InputText("##CALLBACK_SEEK",
buf6,
64,
ImGuiInputTextFlags_CallbackCharFilter,
TextFilters::FilterTime);
valid = std::regex_match(buf6, RegExTime);
if (ImGui::IsItemDeactivatedAfterEdit()) {
if (valid) {
ms = 0;
sec = 0.f;
// user confirmed the entry and the input is valid
// split the "HH:MM:SS.ms" string in HH MM SS.ms
std::string time(buf6);
std::size_t found = time.find_last_of(':');
// read the right part SS.ms as a value
if (std::string::npos != found && BaseToolkit::is_a_value(time.substr(found + 1), &sec)) {
ms = (glm::uint64)(sec * 1000.f);
// read right part MM as a number
time = time.substr(0, found);
found = time.find_last_of(':');
int min = 0;
if (std::string::npos != found && BaseToolkit::is_a_number(time.substr(found + 1), &min)) {
ms += 60000 * (glm::uint64) min;
// read right part HH as a number
time = time.substr(0, found);
int hour = 0;
if (std::string::npos != found && BaseToolkit::is_a_number(time, &hour)) {
ms += 3600000 * (glm::uint64) hour;
}
}
}
// set time in mili seconds
edited->setValue( GST_MSECOND * ms );
}
// force to test validity next frame
valid = false;
guint64 duration = GST_SECOND * 1000;
if (Source * const* v = std::get_if<Source *>(&target)) {
MediaSource *ms = dynamic_cast<MediaSource*>(*v);
if (ms)
duration = ms->mediaplayer()->timeline()->duration();
}
static bool valid = false;
guint64 target_time = edited->value();
if ( ImGuiToolkit::InputTime("##CALLBACK_SEEK", &target_time, duration, &valid) ){
if (valid)
edited->setValue( target_time );
}
ImGui::PopStyleColor();
ImGui::SameLine(0, IMGUI_SAME_LINE / 3);
ImGuiToolkit::Indication("Target time (HH:MM:SS.MS) to set where to jump to in a video source.", 15, 7);
}
break;
case SourceCallback::CALLBACK_FLAG:
{
Flag *edited = static_cast<Flag*>(callback);
ImGuiToolkit::Indication(press_tooltip[0], 2, 13);
ImGui::SameLine(0, IMGUI_SAME_LINE / 2);
int max = -1;
if (Source * const* v = std::get_if<Source *>(&target)) {
MediaSource *ms = dynamic_cast<MediaSource*>(*v);
if (ms)
max = ms->mediaplayer()->timeline()->numFlags() - 1;
}
int val = MIN( (int) edited->value(), max);
ImGui::SetNextItemWidth(right_align);
ImGui::SameLine(0, IMGUI_SAME_LINE / 2);
if (ImGui::SliderInt("##CALLBACK_PLAY_FLAG", &val, -1, max, val < 0 ? "Next Flag" : "Flag <%d>"))
edited->setValue(val );
ImGui::SameLine(0, IMGUI_SAME_LINE / 3);
ImGuiToolkit::Indication("Flag to jump to in a video source.", 12, 6);
}
break;
case SourceCallback::CALLBACK_BRIGHTNESS:
{
SetBrightness *edited = static_cast<SetBrightness*>(callback);
@@ -1362,17 +1350,19 @@ void InputMappingWindow::Render()
}
// check if target is a Source with image processing enabled
bool ismediaplayer = false;
bool withimageprocessing = false;
if ( target.index() == 1 ) {
if (Source * const* v = std::get_if<Source *>(&target)) {
withimageprocessing = (*v)->imageProcessingEnabled();
ismediaplayer = dynamic_cast<MediaSource*>(*v) != nullptr;
}
}
// Select Reaction
ImGui::SameLine(0, IMGUI_SAME_LINE);
ImGui::SetNextItemWidth(w);
uint type = ComboSelectCallback( callback->type(), withimageprocessing );
uint type = ComboSelectCallback( callback->type(), withimageprocessing, ismediaplayer );
if (type > 0) {
// remove previous callback
S->deleteInputCallback(callback);
@@ -1431,16 +1421,18 @@ void InputMappingWindow::Render()
// possible new target
if (temp_new_target.index() > 0) {
// check if target is a Source with image processing enabled
bool mediaplayer = false;
bool withimageprocessing = false;
if ( temp_new_target.index() == 1 ) {
if (Source * const* v = std::get_if<Source *>(&temp_new_target)) {
withimageprocessing = (*v)->imageProcessingEnabled();
mediaplayer = dynamic_cast<MediaSource*>(*v) != nullptr;
}
}
// step 3: Get input for callback type
ImGui::SameLine(0, IMGUI_SAME_LINE);
ImGui::SetNextItemWidth(w);
temp_new_callback = ComboSelectCallback( temp_new_callback, withimageprocessing );
temp_new_callback = ComboSelectCallback( temp_new_callback, withimageprocessing, mediaplayer );
// user selected a callback type
if (temp_new_callback > 0) {
// step 4 : create new callback and add it to source

View File

@@ -16,7 +16,7 @@ class InputMappingWindow : public WorkspaceWindow
uint current_input_;
Target ComboSelectTarget(const Target &current);
uint ComboSelectCallback(uint current, bool imageprocessing);
uint ComboSelectCallback(uint current, bool imageprocessing, bool mediaplayer);
void SliderParametersCallback(SourceCallback *callback, const Target &target);
public:

View File

@@ -1091,7 +1091,7 @@ void MediaPlayer::step(uint milisecond)
else {
// step event
if (milisecond < media_.dt)
milisecond = media_.dt;
milisecond = timeline_.step() < media_.dt ? timeline_.step() : media_.dt;
GstEvent *stepevent = gst_event_new_step (GST_FORMAT_TIME, milisecond, ABS(rate_), TRUE, FALSE);
// Metronome
@@ -1134,6 +1134,9 @@ bool MediaPlayer::go_to(GstClockTime pos)
if (ABS_DIFF (position_, jumpPts) > 2 * timeline_.step() ) {
ret = true;
seek( jumpPts );
// Revert loop status to default
loop_status_ = LoopStatus::LOOP_STATUS_DEFAULT;
}
}
return ret;
@@ -1614,10 +1617,14 @@ bool MediaPlayer::go_to_flag(TimeInterval flag)
current_flag_ = flag;
// change timeline status accordingly
if ( flag.type == (int) LoopStatus::LOOP_STATUS_BLACKOUT)
if ( flag.type == (int) LoopStatus::LOOP_STATUS_BLACKOUT) {
loop_status_ = LoopStatus::LOOP_STATUS_BLACKOUT;
else if ( flag.type == (int) LoopStatus::LOOP_STATUS_STOPPED)
loop_status_ = LoopStatus::LOOP_STATUS_DEFAULT;
execute_play_command(false);
}
else if ( flag.type == (int) LoopStatus::LOOP_STATUS_STOPPED) {
loop_status_ = LoopStatus::LOOP_STATUS_STOPPED;
execute_play_command(false);
}
}
}
return ret;

View File

@@ -17,6 +17,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <glm/geometric.hpp>
#include <glm/gtc/random.hpp> // for diskRand
#include "imgui.h"
@@ -24,6 +25,8 @@
#include "Metronome.h"
#include "View.h"
#include "Mixer.h"
#include "TabletInput.h"
//#include "RenderingManager.h"
#include "MousePointer.h"
@@ -35,6 +38,7 @@ std::vector< std::tuple<int, int, std::string, std::string> > Pointer::Modes = {
{ ICON_POINTER_LINEAR, "Line", "Speed" },
{ ICON_POINTER_SPRING, "Spring", "Mass" },
{ ICON_POINTER_WIGGLY, "Wiggly", "Radius" },
{ ICON_POINTER_BROWNIAN, "Brownian", "Radius" },
{ ICON_POINTER_METRONOME, "Metronome", "Jump" }
};
@@ -111,10 +115,14 @@ void PointerLinear::draw()
void PointerWiggly::update(const glm::vec2 &pos, float)
{
current_ = pos;
float radius = POINTER_WIGGLY_MIN_RADIUS + (POINTER_WIGGLY_MAX_RADIUS - POINTER_WIGGLY_MIN_RADIUS) * strength_;
radius_ = (POINTER_WIGGLY_MAX_RADIUS - POINTER_WIGGLY_MIN_RADIUS) * strength_;
if (TabletInput::instance().hasPressure() && TabletInput::instance().isPressed()) {
radius_ *= TabletInput::instance().getPressure();
}
radius_ += POINTER_WIGGLY_MIN_RADIUS;
// change pos to a random point in a close radius
glm::vec2 p = pos + glm::diskRand( radius );
glm::vec2 p = pos + glm::diskRand( radius_ );
// smooth a little and apply
const float emaexp = 2.0 / float( POINTER_WIGGLY_SMOOTHING + 1);
@@ -126,10 +134,55 @@ void PointerWiggly::draw()
const ImU32 color = ImGui::GetColorU32(ImGuiCol_HeaderActive);
ImGui::GetBackgroundDrawList()->AddLine(IMVEC_IO(current_), IMVEC_IO(target_), color, 5.f);
const float radius = POINTER_WIGGLY_MIN_RADIUS + (POINTER_WIGGLY_MAX_RADIUS - POINTER_WIGGLY_MIN_RADIUS) * strength_;
ImGui::GetBackgroundDrawList()->AddCircle(IMVEC_IO(current_), radius * 0.5f, color, 0, 2.f + 4.f * strength_);
const float max = POINTER_WIGGLY_MIN_RADIUS + (POINTER_WIGGLY_MAX_RADIUS - POINTER_WIGGLY_MIN_RADIUS) * strength_;
if (TabletInput::instance().hasPressure() && TabletInput::instance().isPressed())
ImGui::GetBackgroundDrawList()->AddCircle(IMVEC_IO(current_), radius_ * 0.5f, color, 0);
ImGui::GetBackgroundDrawList()->AddCircle(IMVEC_IO(current_), max * 0.5f, color, 0, 2.f + 4.f * strength_);
}
void PointerBrownian::update(const glm::vec2 &pos, float)
{
current_ = pos;
radius_ = (POINTER_WIGGLY_MAX_RADIUS - POINTER_WIGGLY_MIN_RADIUS) * strength_;
radius_ += POINTER_WIGGLY_MIN_RADIUS;
// Brownian motion: add small random displacement in 2D
// Generate random step using gaussian distribution for each axis
glm::vec2 random_step = glm::gaussRand(glm::vec2(0.0f), glm::vec2(1.f) );
// Scale by radius and apply damping to keep motion bounded
float factor = 0.3f;
if (TabletInput::instance().hasPressure() && TabletInput::instance().isPressed()) {
factor *= TabletInput::instance().getPressure();
}
float damping = 0.92f;
brownian_offset_ = brownian_offset_ * damping + random_step * radius_ * factor;
// Clamp offset to stay within maximum radius
float offset_length = glm::length(brownian_offset_);
if (offset_length > radius_) {
brownian_offset_ = brownian_offset_ * (radius_ / offset_length);
}
glm::vec2 p = pos + brownian_offset_;
// smooth a little and apply
const float emaexp = 2.0 / float( POINTER_WIGGLY_SMOOTHING + 1);
target_ = emaexp * p + (1.f - emaexp) * target_;
}
void PointerBrownian::draw()
{
const ImU32 color = ImGui::GetColorU32(ImGuiCol_HeaderActive);
ImGui::GetBackgroundDrawList()->AddLine(IMVEC_IO(current_), IMVEC_IO(target_), color, 5.f);
const float max = POINTER_WIGGLY_MIN_RADIUS + (POINTER_WIGGLY_MAX_RADIUS - POINTER_WIGGLY_MIN_RADIUS) * strength_;
if (TabletInput::instance().hasPressure() && TabletInput::instance().isPressed())
ImGui::GetBackgroundDrawList()->AddCircle(IMVEC_IO(current_), radius_ * 0.8f, color, 0);
ImGui::GetBackgroundDrawList()->AddCircle(IMVEC_IO(current_), max * 0.8f, color, 0, 2.f + 4.f * strength_);
}
#define POINTER_METRONOME_RADIUS 36.f
void PointerMetronome::initiate(const glm::vec2 &pos)
@@ -186,13 +239,17 @@ void PointerSpring::update(const glm::vec2 &pos, float dt)
// damping : opposite direction of force, non proportional to mass
const float damping = 60.0;
// mass as a percentage of min to max
const float mass = POINTER_SPRING_MIN_MASS + (POINTER_SPRING_MAX_MASS - POINTER_SPRING_MIN_MASS) * strength_;
mass_ = (POINTER_SPRING_MAX_MASS - POINTER_SPRING_MIN_MASS) * strength_;
if (TabletInput::instance().hasPressure() && TabletInput::instance().isPressed()) {
mass_ *= 1.f - TabletInput::instance().getPressure();
}
mass_ += POINTER_SPRING_MIN_MASS;
// compute delta betwen initial and current position
glm::vec2 delta = pos - target_;
if ( glm::length(delta) > 0.0001f ) {
// apply force on velocity : spring stiffness / mass
velocity_ += delta * ( (POINTER_SPRING_MAX_MASS * stiffness) / mass );
velocity_ += delta * ( (POINTER_SPRING_MAX_MASS * stiffness) / mass_ );
// apply damping dynamics
velocity_ -= damping * glm::max(dt,0.001f) * glm::normalize(delta);
// compute new position : add velocity x time
@@ -230,8 +287,10 @@ void PointerSpring::draw()
ImGui::GetBackgroundDrawList()->AddBezierCurve(IMVEC_IO(current_), IMVEC_IO(_third), IMVEC_IO(_twothird), IMVEC_IO(_end), color, 5.f);
// represent the weight with a filled circle
const float mass = POINTER_SPRING_MIN_MASS + (POINTER_SPRING_MAX_MASS - POINTER_SPRING_MIN_MASS) * strength_;
ImGui::GetBackgroundDrawList()->AddCircleFilled(IMVEC_IO(_end), mass, color, 0);
const float max = POINTER_SPRING_MIN_MASS + (POINTER_SPRING_MAX_MASS - POINTER_SPRING_MIN_MASS) * strength_;
if (TabletInput::instance().hasPressure())
ImGui::GetBackgroundDrawList()->AddCircle(IMVEC_IO(_end), max, color, 0);
ImGui::GetBackgroundDrawList()->AddCircleFilled(IMVEC_IO(_end), mass_, color, 0);
}
@@ -242,6 +301,7 @@ MousePointer::MousePointer() : mode_(Pointer::POINTER_DEFAULT)
pointer_[Pointer::POINTER_LINEAR] = new PointerLinear;
pointer_[Pointer::POINTER_SPRING] = new PointerSpring;
pointer_[Pointer::POINTER_WIGGLY] = new PointerWiggly;
pointer_[Pointer::POINTER_BROWNIAN] = new PointerBrownian;
pointer_[Pointer::POINTER_METRONOME] = new PointerMetronome;
}
@@ -252,5 +312,6 @@ MousePointer::~MousePointer()
delete pointer_[Pointer::POINTER_LINEAR];
delete pointer_[Pointer::POINTER_SPRING];
delete pointer_[Pointer::POINTER_WIGGLY];
delete pointer_[Pointer::POINTER_BROWNIAN];
delete pointer_[Pointer::POINTER_METRONOME];
}

View File

@@ -12,6 +12,7 @@
#define ICON_POINTER_LINEAR 14, 9
#define ICON_POINTER_GRID 15, 9
#define ICON_POINTER_WIGGLY 16, 9
#define ICON_POINTER_BROWNIAN 11, 9
#define ICON_POINTER_METRONOME 6, 13
///
@@ -33,6 +34,7 @@ public:
POINTER_LINEAR,
POINTER_SPRING,
POINTER_WIGGLY,
POINTER_BROWNIAN,
POINTER_METRONOME,
POINTER_INVALID
} Mode;
@@ -87,6 +89,7 @@ public:
///
class PointerSpring : public Pointer
{
float mass_;
glm::vec2 velocity_;
public:
PointerSpring() {}
@@ -101,8 +104,23 @@ public:
///
class PointerWiggly : public Pointer
{
float radius_;
public:
PointerWiggly() {}
PointerWiggly() : radius_(0.0f) {}
void update(const glm::vec2 &pos, float) override;
void draw() override;
};
///
/// \brief The PointerBrownian moves with a Brownian movement
/// Strength modulates the radius of the movement
///
class PointerBrownian : public Pointer
{
float radius_;
glm::vec2 brownian_offset_;
public:
PointerBrownian() : brownian_offset_(0.0f, 0.0f) {}
void update(const glm::vec2 &pos, float) override;
void draw() override;
};

View File

@@ -164,25 +164,18 @@ const std::vector<std::string> VideoRecorder::profile_description {
// fast (5)
"video/x-raw, format=I420 ! x264enc tune=\"zerolatency\" pass=4 quantizer=22 speed-preset=2 ! video/x-h264, profile=baseline ! h264parse ! ",
"video/x-raw, format=Y444_10LE ! x264enc tune=\"zerolatency\" pass=4 quantizer=18 speed-preset=3 ! video/x-h264, profile=(string)high-4:4:4 ! h264parse ! ",
// Control x265 encoder quality :
// NB: apparently x265 only accepts I420 format :(
// speed-preset
// superfast (2)
// veryfast (3)
// faster (4)
// fast (5)
// Tune
// psnr (1)
// ssim (2) DEFAULT
// grain (3)
// zerolatency (4) Encoder latency is removed
// fastdecode (5)
// animation (6) optimize the encode quality for animation content without impacting the encode speed
// crf Quality-controlled variable bitrate [0 51]
// default 28
// 24 for x265 should be visually transparent; anything lower will probably just waste file size
"video/x-raw, format=I420 ! x265enc tune=\"zerolatency\" speed-preset=2 option-string=\"crf=24\" ! video/x-h265, profile=(string)main ! h265parse ! ",
"video/x-raw, format=I420 ! x265enc tune=\"zerolatency\" speed-preset=5 option-string=\"crf=12\" ! video/x-h265, profile=(string)main ! h265parse ! ",
// Control vah265enc encoder quality :
// target-usage : The target usage to control and balance the encoding speed/quality
// The lower value has better quality but slower speed, the higher value has faster speed but lower quality.
// Unsigned Integer. Range: 1 - 7 Default: 4
// max-qp : Maximum quantizer value for each frame
// Unsigned Integer. Range: 0 - 51 Default: 51
// rate-control : The desired rate control mode for the encoder
// (2): cbr - Constant Bitrate
// (4): vbr - Variable Bitrate
// (16): cqp - Constant Quantizer
"video/x-raw, format=NV12 ! vah265enc rate-control=\"cqp\" target-usage=5 ! video/x-h265, profile=(string)main ! h265parse ! ",
"video/x-raw, format=NV12 ! vah265enc rate-control=\"cqp\" max-qp=18 target-usage=2 ! video/x-h265, profile=(string)main ! h265parse ! ",
// Apple ProRes encoding parameters
// pass
// cbr (0) Constant Bitrate Encoding
@@ -249,8 +242,8 @@ std::vector<std::string> nvidia_profile_description {
"video/x-raw, format=RGBA ! nvh264enc rc-mode=1 zerolatency=true ! video/x-h264, profile=(string)main ! h264parse ! ",
"video/x-raw, format=RGBA ! nvh264enc rc-mode=1 qp-const=18 ! video/x-h264, profile=(string)high-4:4:4 ! h264parse ! ",
// Control nvh265enc encoder
"video/x-raw, format=RGBA ! nvh265enc rc-mode=1 zerolatency=true ! video/x-h265, profile=(string)main-10 ! h265parse ! ",
"video/x-raw, format=RGBA ! nvh265enc rc-mode=1 qp-const=18 ! video/x-h265, profile=(string)main-444 ! h265parse ! ",
"video/x-raw, format=RGBA ! nvh265enc rc-mode=1 zerolatency=true ! video/x-h265, profile=(string)main ! h265parse ! ",
"video/x-raw, format=RGBA ! nvh265enc rc-mode=1 qp-const=18 ! video/x-h265, profile=(string)main ! h265parse ! ",
"", "", "", ""
};

View File

@@ -69,6 +69,7 @@
#include "Primitives.h"
#include "RenderingManager.h"
#include "TabletInput.h"
// GDBus for screensaver inhibition (works on both X11 and Wayland)
#ifdef GLFW_EXPOSE_NATIVE_GLX
@@ -438,15 +439,18 @@ Rendering::Rendering()
bool Rendering::init()
{
#if GLFW_VERSION_MAJOR > 2 && GLFW_VERSION_MINOR > 3
// Forcing X11 on Gnome makes the server use xWayland which has proper Server Side Decorations as opposed to Wayland.
if (strcmp(getenv("XDG_CURRENT_DESKTOP"), "GNOME") == 0 ||
strcmp(getenv("XDG_CURRENT_DESKTOP"), "Unity") == 0 ){
g_printerr("Forcing X11 on GNOME desktop\n");
glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_X11);
}
else {
g_printerr("Detected %s desktop\n", getenv("XDG_CURRENT_DESKTOP"));
glfwInitHint(GLFW_PLATFORM, GLFW_ANY_PLATFORM);
const char* desktop = getenv("XDG_CURRENT_DESKTOP");
if (desktop) {
// Forcing X11 on Gnome makes the server use xWayland which has proper Server Side Decorations as opposed to Wayland.
if (strstr(desktop, "GNOME") != nullptr ||
strstr(desktop, "Unity") != nullptr ){
g_printerr("Forcing X11 / xWayland on %s desktop\n", desktop);
glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_X11);
}
else {
g_printerr("Detected %s desktop\n", desktop);
glfwInitHint(GLFW_PLATFORM, GLFW_ANY_PLATFORM);
}
}
#endif
//
@@ -567,6 +571,9 @@ bool Rendering::init()
#endif
#endif
// Initialize tablet input for pressure support
TabletInput::instance().init();
return true;
}
@@ -620,6 +627,11 @@ void Rendering::draw()
// Generally you may always pass all inputs to dear imgui, and hide them from your application based on those two flags.
glfwPollEvents();
// Poll tablet input for pressure support
#ifndef WIN32
TabletInput::instance().pollEvents();
#endif
// change windows fullscreen mode if requested
main_.changeFullscreen_();
for (auto it = outputs_.begin(); it != outputs_.end(); ++it)
@@ -675,6 +687,11 @@ void Rendering::draw()
void Rendering::terminate()
{
// Terminate tablet input
#ifndef WIN32
TabletInput::instance().terminate();
#endif
// terminate all windows
for (auto it = outputs_.begin(); it != outputs_.end(); ++it)
it->terminate();

View File

@@ -1703,6 +1703,12 @@ void SessionLoader::visit (Seek &c)
xmlCurrent_->QueryBoolAttribute("bidirectional", &b);
c.setBidirectional(b);
}
void SessionLoader::visit (Flag &c)
{
int v = -1;
xmlCurrent_->QueryIntAttribute("value", &v);
c.setValue(v);
}
void SessionLoader::visit (SetAlpha &c)
{

View File

@@ -93,6 +93,7 @@ public:
void visit (Play&) override;
void visit (PlayFastForward&) override;
void visit (Seek&) override;
void visit (Flag&) override;
static void XMLToNode(const tinyxml2::XMLElement *xml, Node &n);
static void XMLToSourcecore(tinyxml2::XMLElement *xml, SourceCore &s);

View File

@@ -1058,6 +1058,11 @@ void SessionVisitor::visit (PlayFastForward &c)
xmlCurrent_->SetAttribute("duration", c.duration());
}
void SessionVisitor::visit (Flag &c)
{
xmlCurrent_->SetAttribute("value", (int) c.value());
}
void SessionVisitor::visit (Seek &c)
{
xmlCurrent_->SetAttribute("value", (uint64_t) c.value());

View File

@@ -99,6 +99,7 @@ public:
void visit (Play&) override;
void visit (PlayFastForward&) override;
void visit (Seek&) override;
void visit (Flag&) override;
static tinyxml2::XMLElement *NodeToXML(const Node &n, tinyxml2::XMLDocument *doc);
static tinyxml2::XMLElement *ImageToXML(const FrameBufferImage *img, tinyxml2::XMLDocument *doc);

View File

@@ -247,6 +247,7 @@ void Settings::Save(uint64_t runtime, const std::string &filename)
// Brush
XMLElement *BrushNode = xmlDoc.NewElement( "Brush" );
BrushNode->InsertEndChild( XMLElementFromGLM(&xmlDoc, application.brush) );
BrushNode->SetAttribute("brush_pressure_mode", application.brush_pressure_mode);
pRoot->InsertEndChild(BrushNode);
// Pointer
@@ -700,6 +701,7 @@ void Settings::Load(const std::string &filename)
XMLElement * brushnode = pRoot->FirstChildElement("Brush");
if (brushnode != nullptr) {
tinyxml2::XMLElementToGLM( brushnode->FirstChildElement("vec3"), application.brush);
brushnode->QueryIntAttribute("brush_pressure_mode", &application.brush_pressure_mode);
}
// Pointer

View File

@@ -325,6 +325,7 @@ struct Application
// settings brush texture paint
glm::vec3 brush;
int brush_pressure_mode;
// settings render
RenderConfig render;
@@ -396,6 +397,7 @@ struct Application
current_view = 1;
current_workspace= 3;
brush = glm::vec3(0.5f, 0.1f, 0.f);
brush_pressure_mode = 0;
num_output_windows = 1;
windows = std::vector<WindowConfig>(1+MAX_OUTPUT_WINDOW);
windows[0].w = 1600;

View File

@@ -406,6 +406,15 @@ Source::Source(uint64_t id) : SourceCore(), id_(id), ready_(false), symbol_(null
Source::~Source()
{
// clear and delete callbacks
access_callbacks_.lock();
for (auto iter=update_callbacks_.begin(); iter != update_callbacks_.end(); ) {
SourceCallback *callback = *iter;
iter = update_callbacks_.erase(iter);
delete callback;
}
access_callbacks_.unlock();
// inform links that they lost their target
while ( !links_.empty() )
links_.front()->disconnect();
@@ -431,15 +440,6 @@ Source::~Source()
overlays_.clear();
frames_.clear();
handles_.clear();
// clear and delete callbacks
access_callbacks_.lock();
for (auto iter=update_callbacks_.begin(); iter != update_callbacks_.end(); ) {
SourceCallback *callback = *iter;
iter = update_callbacks_.erase(iter);
delete callback;
}
access_callbacks_.unlock();
}
void Source::setName (const std::string &name)

View File

@@ -72,6 +72,9 @@ SourceCallback *SourceCallback::create(CallbackType type)
case SourceCallback::CALLBACK_SEEK:
loadedcallback = new Seek;
break;
case SourceCallback::CALLBACK_FLAG:
loadedcallback = new Flag;
break;
case SourceCallback::CALLBACK_REPLAY:
loadedcallback = new RePlay;
break;
@@ -718,6 +721,73 @@ void Seek::accept(Visitor& v)
v.visit(*this);
}
Flag::Flag(int target)
: SourceCallback()
, flag_index_(target)
{}
void Flag::update(Source *s, float dt)
{
SourceCallback::update(s, dt);
// access media player if target source is a media source
MediaSource *ms = dynamic_cast<MediaSource *>(s);
if (ms != nullptr) {
// can operate on flags if there are some
int num = ms->mediaplayer()->timeline()->numFlags();
if (num > 1) {
// default flag index is -1 to mean next flag
if (flag_index_ < 0) {
GstClockTime _time = ms->mediaplayer()->position();
if( ms->mediaplayer()->playSpeed() < 0 ) {
// Go to previous flag when playing backward
TimeInterval target_flag = ms->mediaplayer()->timeline()->getPreviousFlag( _time );
bool has_prev = target_flag.is_valid() &&
( ms->mediaplayer()->loop() == MediaPlayer::LOOP_REWIND || (target_flag.end < _time) );
if( has_prev)
ms->mediaplayer()->go_to_flag( target_flag );
}
else {
// go to next flag when playing forward
TimeInterval target_flag = ms->mediaplayer()->timeline()->getNextFlag( _time );
bool has_next = target_flag.is_valid() &&
( ms->mediaplayer()->loop() == MediaPlayer::LOOP_REWIND || (target_flag.begin > _time) );
if( has_next )
ms->mediaplayer()->go_to_flag( target_flag );
}
}
else if (flag_index_ < num) {
int index = 0;
const TimeIntervalSet flags = ms->mediaplayer()->timeline()->flags();
for (const auto &flag_Interval : flags) {
if ( index == flag_index_) {
ms->mediaplayer()->go_to_flag( flag_Interval );
break;
}
++index;
}
}
}
}
status_ = FINISHED;
}
SourceCallback *Flag::clone() const
{
return new Flag(flag_index_);
}
void Flag::accept(Visitor& v)
{
SourceCallback::accept(v);
v.visit(*this);
}
SetGeometry::SetGeometry(const Group *g, float ms, bool revert) : SourceCallback(),
duration_(ms), bidirectional_(revert)
{

View File

@@ -40,6 +40,7 @@ public:
CALLBACK_PLAYSPEED,
CALLBACK_PLAYFFWD,
CALLBACK_SEEK,
CALLBACK_FLAG,
CALLBACK_REPLAY,
CALLBACK_RESETGEO,
CALLBACK_LOCK,
@@ -295,6 +296,22 @@ public:
void accept (Visitor& v) override;
};
class Flag : public SourceCallback
{
int flag_index_;
public:
Flag (int target = -1);
int value () const { return flag_index_; }
void setValue (int t) { flag_index_ = t; }
void update (Source *s, float) override;
SourceCallback *clone() const override;
CallbackType type () const override { return CALLBACK_FLAG; }
void accept (Visitor& v) override;
};
class ResetGeometry : public SourceCallback
{
public:

View File

@@ -17,6 +17,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <glib.h>
#include <iostream>
#include <iomanip>
#include <thread>
@@ -946,6 +947,7 @@ bool TimelineSlider (const char* label, guint64 *time, TimeInterval *flag, Timel
//
// FLAGS
//
int index = 0;
bool flag_pressed = false;
const TimeIntervalSet flags = tl->flags();
for (const auto &flag_Interval : flags) {
@@ -972,7 +974,9 @@ bool TimelineSlider (const char* label, guint64 *time, TimeInterval *flag, Timel
// show time when hovering
if (hovered)
ImGui::SetTooltip(" %s ", GstToolkit::time_to_string(flag_time).c_str());
ImGui::SetTooltip(" <%d> %s ", index, GstToolkit::time_to_string(flag_time).c_str());
++index;
}
//
@@ -2305,7 +2309,7 @@ void SourceControlWindow::RenderMediaPlayer(MediaSource *ms)
// flag buttons
if (rendersize.x > buttons_height_ * 6.0f) {
if ( !mediaplayer_mode_ && mediaplayer_active_->timeline()->numFlags() > 0 ) {
if ( mediaplayer_active_->timeline()->numFlags() > 0 ) {
GstClockTime _paused_time = mediaplayer_active_->position();
@@ -2317,7 +2321,7 @@ void SourceControlWindow::RenderMediaPlayer(MediaSource *ms)
!( mediaplayer_active_->currentFlag().is_valid() && mediaplayer_active_->timeline()->numFlags() == 1) &&
( mediaplayer_active_->loop() == MediaPlayer::LOOP_REWIND || (target_flag.end < _paused_time) );
if( ImGuiToolkit::ButtonIcon(6, 0, "Go to previous flag", has_prev) )
mediaplayer_active_->go_to_flag( target_flag );
seek_flag = target_flag;
}
else {
// go to next flag when playing forward
@@ -2326,11 +2330,11 @@ void SourceControlWindow::RenderMediaPlayer(MediaSource *ms)
!( mediaplayer_active_->currentFlag().is_valid() && mediaplayer_active_->timeline()->numFlags() == 1) &&
( mediaplayer_active_->loop() == MediaPlayer::LOOP_REWIND || (target_flag.begin > _paused_time) );
if( ImGuiToolkit::ButtonIcon(5, 0, "Go to next flag", has_next) )
mediaplayer_active_->go_to_flag( target_flag );
seek_flag = target_flag;
}
// if stopped at a flag, show flag menu
if (mediaplayer_active_->currentFlag().is_valid()) {
if (!mediaplayer_mode_ && mediaplayer_active_->currentFlag().is_valid()) {
ImGui::SameLine(0, h_space_);
if (ImGuiToolkit::IconButton(3, 0) || ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup)) {
counter_menu_timeout=0;
@@ -2396,23 +2400,18 @@ void SourceControlWindow::RenderMediaPlayer(MediaSource *ms)
// play/stop command should be following the playing mode (buttons)
// AND force to stop when the slider is pressed
bool media_play = mediaplayer_mode_ & (!mediaplayer_slider_pressed_);
// Flag pressed in timeline
if (seek_flag.is_valid()) {
// go to the flag position
if ( mediaplayer_active_->go_to_flag(seek_flag) ){
// stop if flag type is 'Stop' (1) or 'Blackout' (2)
if (seek_flag.type > 0)
media_play = false;
}
}
bool media_play = mediaplayer_mode_ & (!mediaplayer_slider_pressed_);
// apply play action to media only if status should change
if ( mediaplayer_active_->isPlaying() != media_play ) {
mediaplayer_active_->play( media_play );
}
// Flag pressed in timeline
if (seek_flag.is_valid()) {
// go to the flag position
mediaplayer_active_->go_to_flag(seek_flag);
}
}
else {
///

View File

@@ -26,7 +26,6 @@
#include <chrono>
#include <algorithm>
#include <vector>
#include <climits>
using namespace std;

30
src/TabletInput.cpp Normal file
View File

@@ -0,0 +1,30 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2023 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include "TabletInput.h"
TabletInput& TabletInput::instance()
{
static TabletInput instance;
return instance;
}
// Platform-specific implementations are in separate files:
// - TabletInput_linux.cpp (Linux with libinput)
// - TabletInput_macos.mm (macOS with NSEvent)

107
src/TabletInput.h Normal file
View File

@@ -0,0 +1,107 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2023 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#ifndef TABLETINPUT_H
#define TABLETINPUT_H
#include <glm/glm.hpp>
// Platform-specific forward declarations
#ifdef LINUX
// X11 forward declarations (for XInput2)
typedef struct _XDisplay Display;
#if defined(HAVE_LIBINPUT)
struct libinput;
struct udev;
#endif
#elif defined(APPLE)
#ifdef __OBJC__
@class NSEvent;
#else
class NSEvent;
#endif
#endif
/**
* @brief Cross-platform tablet/stylus input manager
*
* Provides normalized pressure values (0.0-1.0) from pen/stylus devices
* across Linux (libinput) and macOS (NSEvent)
*/
class TabletInput {
public:
struct TabletData {
float pressure; // 0.0 - 1.0
bool has_pressure; // Stylius has pressure
float tilt_x; // -1.0 to 1.0 (optional)
float tilt_y; // -1.0 to 1.0 (optional)
bool in_proximity; // Is stylus near/touching surface
bool tip_down; // Is stylus tip pressed
};
static TabletInput& instance();
// Initialize tablet input system
bool init();
// Poll for new tablet events (call once per frame)
void pollEvents();
// Clean up resources
void terminate();
// Get current tablet data
const TabletData& getData() const { return data_; }
// Quick accessors
float getPressure() const { return data_.pressure; }
bool isPressed() const { return data_.tip_down && data_.in_proximity; }
// status
bool isEnabled() const { return active_; }
bool hasPressure() const { return isEnabled() && data_.has_pressure; }
private:
TabletInput();
~TabletInput();
TabletData data_;
bool active_;
#ifdef LINUX
#if defined(HAVE_X11TABLETINPUT)
// X11/XInput2 members (used when libinput is not available or in Flatpak)
Display *display_;
int xi_opcode_;
int pressure_valuator_;
int tilt_x_valuator_;
int tilt_y_valuator_;
#endif
#if defined(HAVE_LIBINPUT)
// libinput members (used for native builds with libinput)
struct udev *udev_;
struct libinput *li_;
int fd_;
#endif
#elif defined(APPLE)
void* monitor_; // Event monitor handle
#endif
};
#endif // TABLETINPUT_H

View File

View File

@@ -0,0 +1,206 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2025 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <glib.h>
#ifdef LINUX
#include "TabletInput.h"
#include "Log.h"
#ifdef HAVE_LIBINPUT
#include <libinput.h>
#include <libudev.h>
#include <fcntl.h>
#include <unistd.h>
#include <cerrno>
static int open_restricted(const char *path, int flags, void *user_data)
{
int fd = open(path, flags);
return fd < 0 ? -errno : fd;
}
static void close_restricted(int fd, void *user_data)
{
close(fd);
}
static const struct libinput_interface interface = {
.open_restricted = open_restricted,
.close_restricted = close_restricted,
};
TabletInput::TabletInput()
: data_({0.0f, false, 0.0f, 0.0f, false, false})
, active_(false)
, udev_(nullptr)
, li_(nullptr)
, fd_(-1)
{
}
TabletInput::~TabletInput()
{
terminate();
}
bool TabletInput::init()
{
udev_ = udev_new();
if (!udev_) {
Log::Info("TabletInput: Failed to initialize udev");
return false;
}
li_ = libinput_udev_create_context(&interface, nullptr, udev_);
if (!li_) {
Log::Info("TabletInput: Failed to create libinput context");
udev_unref(udev_);
udev_ = nullptr;
return false;
}
if (libinput_udev_assign_seat(li_, "seat0") != 0) {
Log::Info("TabletInput: Failed to assign seat");
libinput_unref(li_);
li_ = nullptr;
udev_unref(udev_);
udev_ = nullptr;
return false;
}
fd_ = libinput_get_fd(li_);
// Set non-blocking
int flags = fcntl(fd_, F_GETFL, 0);
fcntl(fd_, F_SETFL, flags | O_NONBLOCK);
active_ = true;
Log::Info("TabletInput: Linux tablet input initialized (libinput)");
return true;
}
void TabletInput::pollEvents()
{
if (!li_) return;
libinput_dispatch(li_);
struct libinput_event *event;
while ((event = libinput_get_event(li_)) != nullptr) {
enum libinput_event_type type = libinput_event_get_type(event);
switch (type) {
case LIBINPUT_EVENT_TABLET_TOOL_AXIS:
case LIBINPUT_EVENT_TABLET_TOOL_TIP:
case LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY: {
struct libinput_event_tablet_tool *tablet_event =
libinput_event_get_tablet_tool_event(event);
// Update pressure
if (libinput_event_tablet_tool_pressure_has_changed(tablet_event)) {
data_.has_pressure = true;
data_.pressure = libinput_event_tablet_tool_get_pressure(tablet_event);
}
// Update tilt (if available)
if (libinput_event_tablet_tool_tilt_x_has_changed(tablet_event)) {
data_.tilt_x = libinput_event_tablet_tool_get_tilt_x(tablet_event) / 90.0f;
}
if (libinput_event_tablet_tool_tilt_y_has_changed(tablet_event)) {
data_.tilt_y = libinput_event_tablet_tool_get_tilt_y(tablet_event) / 90.0f;
}
// Update proximity
if (type == LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY) {
data_.in_proximity = (libinput_event_tablet_tool_get_proximity_state(tablet_event)
== LIBINPUT_TABLET_TOOL_PROXIMITY_STATE_IN);
if (!data_.in_proximity) {
data_.has_pressure = true;
data_.pressure = 0.0f;
data_.tip_down = false;
}
}
// Update tip state
if (type == LIBINPUT_EVENT_TABLET_TOOL_TIP) {
data_.tip_down = (libinput_event_tablet_tool_get_tip_state(tablet_event)
== LIBINPUT_TABLET_TOOL_TIP_DOWN);
if (!data_.tip_down) {
data_.has_pressure = true;
data_.pressure = 0.0f;
}
}
break;
}
default:
break;
}
libinput_event_destroy(event);
}
}
void TabletInput::terminate()
{
if (li_) {
libinput_unref(li_);
li_ = nullptr;
}
if (udev_) {
udev_unref(udev_);
udev_ = nullptr;
}
active_ = false;
}
#else // !HAVE_LIBINPUT
// Stub implementation when libinput is not available
TabletInput::TabletInput()
: data_({0.0f, false, 0.0f, 0.0f, false, false})
, active_(false)
{
}
TabletInput::~TabletInput()
{
}
bool TabletInput::init()
{
Log::Info("TabletInput: libinput not available - tablet pressure support disabled");
active_ = false;
return false;
}
void TabletInput::pollEvents()
{
// No-op when libinput is not available
}
void TabletInput::terminate()
{
active_ = false;
}
#endif // HAVE_LIBINPUT
#endif // LINUX

248
src/TabletInput_x11.cpp Normal file
View File

@@ -0,0 +1,248 @@
/*
* This file is part of vimix - video live mixer
*
* **Copyright** (C) 2019-2023 Bruno Herbelin <bruno.herbelin@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include <glib.h>
#ifdef LINUX
#include "TabletInput.h"
#include "Log.h"
#ifdef HAVE_X11TABLETINPUT
#include <X11/Xlib.h>
#include <X11/extensions/XInput2.h>
#include <cstring>
#endif
TabletInput::TabletInput()
: data_({0.0f, false, 0.0f, 0.0f, false, false})
, active_(false)
#ifdef HAVE_X11TABLETINPUT
, display_(nullptr)
, xi_opcode_(-1)
, pressure_valuator_(-1)
, tilt_x_valuator_(-1)
, tilt_y_valuator_(-1)
#endif
{
}
TabletInput::~TabletInput()
{
terminate();
}
bool TabletInput::init()
{
#ifdef HAVE_X11TABLETINPUT
// Open X11 display connection
display_ = XOpenDisplay(nullptr);
if (!display_) {
Log::Info("TabletInput: Failed to open X11 display");
return false;
}
// Check for XInput2 extension
int event, error;
if (!XQueryExtension(display_, "XInputExtension", &xi_opcode_, &event, &error)) {
Log::Info("TabletInput: XInput extension not available");
XCloseDisplay(display_);
display_ = nullptr;
return false;
}
// Check XInput2 version
int major = 2, minor = 2;
if (XIQueryVersion(display_, &major, &minor) != Success) {
Log::Info("TabletInput: XInput2 2.2 not available");
XCloseDisplay(display_);
display_ = nullptr;
return false;
}
// Select events for all devices
XIEventMask eventmask;
unsigned char mask[XIMaskLen(XI_LASTEVENT)] = {0};
XISetMask(mask, XI_Motion);
XISetMask(mask, XI_ButtonPress);
XISetMask(mask, XI_ButtonRelease);
eventmask.deviceid = XIAllDevices;
eventmask.mask_len = sizeof(mask);
eventmask.mask = mask;
Window root = DefaultRootWindow(display_);
XISelectEvents(display_, root, &eventmask, 1);
// Find tablet devices and their pressure valuators
int ndevices;
XIDeviceInfo *devices = XIQueryDevice(display_, XIAllDevices, &ndevices);
for (int i = 0; i < ndevices; i++) {
XIDeviceInfo *device = &devices[i];
// Look for devices that have valuators (tablets, styluses)
if (device->use == XISlavePointer || device->use == XIFloatingSlave) {
// Check valuator classes for pressure
for (int j = 0; j < device->num_classes; j++) {
if (device->classes[j]->type == XIValuatorClass) {
XIValuatorClassInfo *v = (XIValuatorClassInfo*)device->classes[j];
// Try to identify pressure axis
// Pressure is usually valuator 2, but we check the label
Atom pressure_atom = XInternAtom(display_, "Abs Pressure", True);
Atom tilt_x_atom = XInternAtom(display_, "Abs Tilt X", True);
Atom tilt_y_atom = XInternAtom(display_, "Abs Tilt Y", True);
if (v->label == pressure_atom) {
pressure_valuator_ = v->number;
Log::Info("TabletInput: Found pressure valuator %d on device '%s'",
pressure_valuator_, device->name);
}
else if (v->label == tilt_x_atom) {
tilt_x_valuator_ = v->number;
}
else if (v->label == tilt_y_atom) {
tilt_y_valuator_ = v->number;
}
// Fallback: assume valuator 2 is pressure if we haven't found it
else if (pressure_valuator_ == -1 && v->number == 2) {
pressure_valuator_ = v->number;
Log::Info("TabletInput: Using valuator 2 as pressure (fallback)");
}
}
}
}
}
XIFreeDeviceInfo(devices);
if (pressure_valuator_ == -1) {
Log::Info("TabletInput: No pressure valuator found - tablet may not be connected");
// Don't fail init, just continue without pressure detection
}
else {
data_.has_pressure = true;
}
XFlush(display_);
active_ = true;
Log::Info("TabletInput: X11/XInput2 tablet input initialized");
return true;
#else
Log::Info("TabletInput: XInput2 not available - tablet support disabled");
return false;
#endif
}
void TabletInput::pollEvents()
{
#ifdef HAVE_X11TABLETINPUT
if (!display_) return;
// Process all pending X11 events
while (XPending(display_) > 0) {
XEvent ev;
XNextEvent(display_, &ev);
// Check for XInput2 events
if (ev.type == GenericEvent && ev.xcookie.extension == xi_opcode_) {
if (XGetEventData(display_, &ev.xcookie)) {
XIDeviceEvent *device_event = (XIDeviceEvent*)ev.xcookie.data;
switch (ev.xcookie.evtype) {
case XI_Motion:
case XI_ButtonPress:
case XI_ButtonRelease: {
// Extract pressure from valuators
if (pressure_valuator_ >= 0) {
double *values = device_event->valuators.values;
unsigned char *mask = device_event->valuators.mask;
int val_index = 0;
for (int i = 0; i <= pressure_valuator_; i++) {
if (XIMaskIsSet(mask, i)) {
if (i == pressure_valuator_) {
// Normalize pressure (typically 0-65535 range)
data_.pressure = values[val_index] / 65535.0f;
if (data_.pressure > 1.0f) data_.pressure = 1.0f;
if (data_.pressure < 0.0f) data_.pressure = 0.0f;
}
val_index++;
}
}
}
// Extract tilt
if (tilt_x_valuator_ >= 0 || tilt_y_valuator_ >= 0) {
double *values = device_event->valuators.values;
unsigned char *mask = device_event->valuators.mask;
int val_index = 0;
int max_valuator = tilt_x_valuator_ > tilt_y_valuator_ ?
tilt_x_valuator_ : tilt_y_valuator_;
for (int i = 0; i <= max_valuator; i++) {
if (XIMaskIsSet(mask, i)) {
if (i == tilt_x_valuator_) {
data_.tilt_x = (values[val_index] - 32767.5f) / 32767.5f;
}
if (i == tilt_y_valuator_) {
data_.tilt_y = (values[val_index] - 32767.5f) / 32767.5f;
}
val_index++;
}
}
}
// Update button state
if (ev.xcookie.evtype == XI_ButtonPress) {
data_.tip_down = true;
data_.in_proximity = data_.pressure > 0.005f;
}
else if (ev.xcookie.evtype == XI_ButtonRelease) {
data_.tip_down = false;
data_.in_proximity = false;
}
else if (ev.xcookie.evtype == XI_Motion) {
data_.in_proximity = data_.pressure > 0.005f;
}
break;
}
}
XFreeEventData(display_, &ev.xcookie);
}
}
}
#endif
}
void TabletInput::terminate()
{
#ifdef HAVE_X11TABLETINPUT
if (display_) {
XCloseDisplay(display_);
display_ = nullptr;
}
#endif
active_ = false;
}
#endif // LINUX

View File

@@ -17,8 +17,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
**/
#include "IconsFontAwesome5.h"
#include <glad/glad.h>
#include <glm/common.hpp>
#include <glm/ext/vector_float2.hpp>
#include <glm/glm.hpp>
#include <glm/gtc/constants.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/matrix_access.hpp>
#define GLM_ENABLE_EXPERIMENTAL
@@ -42,6 +46,14 @@
#include "ActionManager.h"
#include "DialogToolkit.h"
#include "MousePointer.h"
#include "TabletInput.h"
enum TabletInputFlags_
{
TabletInput_none = 0,
TabletInput_brush_size = 1 << 1,
TabletInput_brush_pressure = 1 << 2
};
#include "TextureView.h"
@@ -226,7 +238,8 @@ TextureView::TextureView() : View(TEXTURE), edit_source_(nullptr), need_edit_upd
stored_mask_size_ = glm::vec3(0.f);
show_cursor_forced_ = false;
scene_brush_pos = glm::vec3(0.f);
scene_brush_pos = glm::vec3(100.f);
previous_scene_brush_pos = glm::vec3(0.f);
// replace grid with appropriate one
translation_grid_ = new TranslationGrid(scene.root());
@@ -450,6 +463,8 @@ std::pair<Node *, glm::vec2> TextureView::pick(glm::vec2 P)
pick = { mask_cursor_circle_, P };
// adapt grid to prepare grab action
adaptGridToSource(current);
// reset previous brush position
previous_scene_brush_pos = glm::zero<glm::vec3>();
return pick;
}
// special case for cropping the mask shape
@@ -792,7 +807,7 @@ void TextureView::draw()
if (mask_cursor_paint_ > 0) {
ImGui::SameLine(0, 50);
if (ImGui::Button(ICON_FA_PEN ICON_FA_SORT_DOWN ))
if (ImGui::Button(ICON_FA_PEN_NIB ICON_FA_SORT_DOWN ))
ImGui::OpenPopup("brush_shape_popup");
if (ImGui::IsItemHovered())
ImGuiToolkit::ToolTip("Shape");
@@ -815,11 +830,22 @@ void TextureView::draw()
ImGuiToolkit::ToolTip("Size");
if (ImGui::BeginPopup("brush_size_popup", ImGuiWindowFlags_NoMove))
{
int pixel_size_min = int(0.05 * edit_source_->frame()->height() );
int pixel_size_max = int(2.0 * edit_source_->frame()->height() );
int pixel_size_min = int(BRUSH_MIN_SIZE * edit_source_->frame()->height() );
int pixel_size_max = int(BRUSH_MAX_SIZE * edit_source_->frame()->height() );
int pixel_size = int(Settings::application.brush.x * edit_source_->frame()->height() );
show_cursor_forced_ = true;
ImGuiToolkit::PushFont(ImGuiToolkit::FONT_DEFAULT);
// toggle to enable tablet input on brush size
if(TabletInput::instance().isEnabled() && TabletInput::instance().hasPressure()) {
static bool enable_tablet_input = false;
enable_tablet_input = Settings::application.brush_pressure_mode & TabletInput_brush_size;
ImGuiToolkit::ButtonIconToggle(13, 0, &enable_tablet_input, "Tablet pressure sensitive");
if (enable_tablet_input)
Settings::application.brush_pressure_mode |= TabletInput_brush_size;
else
Settings::application.brush_pressure_mode &= ~TabletInput_brush_size;
}
// max brush size
ImGuiToolkit::Indication("Large ", 16, 1);
if (ImGui::VSliderInt("##BrushSize", ImVec2(30,260), &pixel_size, pixel_size_min, pixel_size_max, "") ){
Settings::application.brush.x = CLAMP(float(pixel_size) / edit_source_->frame()->height(), BRUSH_MIN_SIZE, BRUSH_MAX_SIZE);
@@ -846,6 +872,17 @@ void TextureView::draw()
if (ImGui::BeginPopup("brush_pressure_popup", ImGuiWindowFlags_NoMove))
{
ImGuiToolkit::PushFont(ImGuiToolkit::FONT_DEFAULT);
// toggle to enable tablet input on brush pressure
if(TabletInput::instance().isEnabled() && TabletInput::instance().hasPressure()) {
static bool enable_tablet_input = false;
enable_tablet_input = Settings::application.brush_pressure_mode & TabletInput_brush_pressure;
ImGuiToolkit::ButtonIconToggle(13, 0, &enable_tablet_input, "Tablet pressure sensitive");
if (enable_tablet_input)
Settings::application.brush_pressure_mode |= TabletInput_brush_pressure;
else
Settings::application.brush_pressure_mode &= ~TabletInput_brush_pressure;
}
// max brush pressure
ImGuiToolkit::Indication("Light ", ICON_FA_FEATHER_ALT);
ImGui::VSliderFloat("##BrushPressure", ImVec2(30,260), &Settings::application.brush.y, BRUSH_MAX_PRESS, BRUSH_MIN_PRESS, "", 0.3f);
if (ImGui::IsItemHovered() || ImGui::IsItemActive() ) {
@@ -889,7 +926,7 @@ void TextureView::draw()
}
if (e>0) {
edit_source_->maskShader()->effect = e;
edit_source_->maskShader()->cursor = glm::vec4(100.0, 100.0, 0.f, 0.f);
edit_source_->maskShader()->cursor = glm::vec4(100.0, 100.0,100.f, 100.f);
edit_source_->touch(Source::SourceUpdate_Mask);
Action::manager().store(oss.str());
}
@@ -1106,16 +1143,38 @@ View::Cursor TextureView::grab (Source *s, glm::vec2 from, glm::vec2 to, std::pa
// set brush coordinates (used in mouse over)
scene_brush_pos = scene_to;
// no previous brush position : restart at same coordinates
if ( glm::length( previous_scene_brush_pos ) < EPSILON)
previous_scene_brush_pos = scene_brush_pos;
if ( pick.first == mask_cursor_circle_ ) {
// snap prush coordinates if grid is active
if (grid->active())
scene_brush_pos = grid->snap(scene_brush_pos);
// inform shader of a cursor action : coordinates and crop scaling
edit_source_->maskShader()->size = edit_source_->mixingsurface_->scale_;
// Apply tablet pressure to brush if available
if (TabletInput::instance().hasPressure() && TabletInput::instance().isPressed()) {
float tablet_pressure = TabletInput::instance().getPressure();
// apply pressure depending on mode
// scale size of brush
if (Settings::application.brush_pressure_mode & TabletInput_brush_size)
edit_source_->maskShader()->brush.x = CLAMP( Settings::application.brush.x * tablet_pressure, BRUSH_MIN_SIZE, BRUSH_MAX_SIZE);
// transparency pressure
if (Settings::application.brush_pressure_mode & TabletInput_brush_pressure)
edit_source_->maskShader()->brush.y = CLAMP( Settings::application.brush.x * tablet_pressure, BRUSH_MIN_PRESS, BRUSH_MAX_PRESS);
}
// inform shader of a cursor action : coordinates and crop scaling
edit_source_->maskShader()->cursor = glm::vec4(scene_brush_pos.x - shift_crop_.x,
scene_brush_pos.y - shift_crop_.y,
edit_source_->mixingsurface_->scale_.x,
edit_source_->mixingsurface_->scale_.y);
previous_scene_brush_pos.x - shift_crop_.x,
previous_scene_brush_pos.y - shift_crop_.y);
edit_source_->touch(Source::SourceUpdate_Mask);
previous_scene_brush_pos = scene_brush_pos;
// action label
info << MaskShader::mask_names[MaskShader::PAINT] << " changed";
// cursor indication - no info, just cursor
@@ -1547,7 +1606,8 @@ void TextureView::terminate(bool force)
// special case for texture paint: store image on mouse release (end of action PAINT)
if ( edit_source_ != nullptr && current_action_.find(MaskShader::mask_names[MaskShader::PAINT]) != std::string::npos ) {
edit_source_->storeMask();
edit_source_->maskShader()->cursor = glm::vec4(100.0, 100.0, 0.f, 0.f);
edit_source_->maskShader()->cursor = glm::vec4(100.0, 100.0, 100.f, 100.f);
edit_source_->maskShader()->size = glm::vec2(stored_mask_size_);
}
// View default termination of action

View File

@@ -72,6 +72,7 @@ private:
void adaptGridToSource(Source *s = nullptr, Node *picked = nullptr);
glm::vec3 scene_brush_pos;
glm::vec3 previous_scene_brush_pos;
TranslationGrid *translation_grid_;
RotationGrid *rotation_grid_;
};

View File

@@ -76,6 +76,7 @@ public:
virtual void visit (class Play&) {}
virtual void visit (class PlayFastForward&) {}
virtual void visit (class Seek&) {}
virtual void visit (class Flag&) {}
};

View File

@@ -90,7 +90,7 @@
#define APPEARANCE_DEFAULT_SCALE 2.f
#define APPEARANCE_MIN_SCALE 0.4f
#define APPEARANCE_MAX_SCALE 7.0f
#define BRUSH_MIN_SIZE 0.05f
#define BRUSH_MIN_SIZE 0.01f
#define BRUSH_MAX_SIZE 2.f
#define BRUSH_MIN_PRESS 0.005f
#define BRUSH_MAX_PRESS 1.f
@@ -111,8 +111,8 @@
#define IMGUI_LABEL_RECENT_FILES " Recent files"
#define IMGUI_LABEL_RECENT_RECORDS " Recent recordings"
#define IMGUI_RIGHT_ALIGN -3.8f * ImGui::GetTextLineHeightWithSpacing()
#define IMGUI_SAME_LINE 8
#define IMGUI_TOP_ALIGN 10
#define IMGUI_SAME_LINE 8.f
#define IMGUI_TOP_ALIGN 10.f
#define IMGUI_COLOR_OVERLAY IM_COL32(5, 5, 5, 150)
#define IMGUI_COLOR_LIGHT_OVERLAY IM_COL32(5, 5, 5, 50)
#define IMGUI_COLOR_CAPTURE 1.0, 0.55, 0.05