diff --git a/README.md b/README.md
index 680642f..6dfca9e 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,12 @@
# iOS-GPUImage-Plus
GPU accelerated filters for iOS based on OpenGL.
-__New feature__: Face effects will be created with the ios11's `VNSequenceRequestHandler` & `VNDetectFaceLandmarksRequest`.
+__Features__:
+- Hundreds of built-in image filters
+- Real-time camera filters
+- Video player with filters
+- **Image Deformation/Liquify** - Interactive mesh-based image deformation with forward, bloat, wrinkle, and restore tools (similar to Photoshop's Liquify tool)
+- Face effects with iOS 11's `VNSequenceRequestHandler` & `VNDetectFaceLandmarksRequest`
>Android version: [https://github.com/wysaid/android-gpuimage-plus](https://github.com/wysaid/android-gpuimage-plus "http://wysaid.org")
@@ -97,6 +102,57 @@ En: [https://github.com/wysaid/android-gpuimage-plus/wiki/Parsing-String-Rule-En
Ch: [https://github.com/wysaid/android-gpuimage-plus/wiki/Parsing-String-Rule](https://github.com/wysaid/android-gpuimage-plus/wiki/Parsing-String-Rule "http://wysaid.org")
+### 4. Image Deformation (Liquify) ###
+
+The library includes a powerful mesh-based image deformation feature, similar to Photoshop's Liquify tool. This allows interactive image manipulation with various deformation modes:
+
+- **Forward**: Push pixels in the direction of your finger movement
+- **Bloat**: Expand/inflate pixels outward from the touch point
+- **Wrinkle**: Contract/shrink pixels toward the touch point
+- **Restore**: Restore deformed areas back to the original image
+
+___Sample Code for Image Deformation___
+```objc
+#import "cgeDeformFilterWrapper.h"
+
+// Create a deform filter wrapper
+CGEDeformFilterWrapper* deformWrapper = [CGEDeformFilterWrapper createWithWidth:imageWidth
+ height:imageHeight
+ stride:10.0f];
+
+// Set undo steps
+[deformWrapper setUndoSteps:50];
+
+// Forward deform (push)
+[deformWrapper forwardDeformWithStartX:startX startY:startY
+ endX:endX endY:endY
+ width:canvasWidth height:canvasHeight
+ radius:100.0f intensity:0.15f];
+
+// Bloat deform (expand)
+[deformWrapper bloatDeformWithX:x y:y
+ width:canvasWidth height:canvasHeight
+ radius:100.0f intensity:0.15f];
+
+// Wrinkle deform (shrink)
+[deformWrapper wrinkleDeformWithX:x y:y
+ width:canvasWidth height:canvasHeight
+ radius:100.0f intensity:0.15f];
+
+// Undo/Redo
+if ([deformWrapper canUndo]) {
+ [deformWrapper undo];
+}
+
+// Restore mesh to original
+[deformWrapper restore];
+
+// Show/hide mesh overlay
+[deformWrapper showMesh:YES];
+```
+
+Check out the **Image Deform Demo** in the demo app to see this feature in action!
+
## Tool ##
Some utils are available for creating filters: [https://github.com/wysaid/cge-tools](https://github.com/wysaid/cge-tools "http://wysaid.org")
diff --git a/demo/cgeDemo/Base.lproj/Main.storyboard b/demo/cgeDemo/Base.lproj/Main.storyboard
index cf404f8..df8137c 100644
--- a/demo/cgeDemo/Base.lproj/Main.storyboard
+++ b/demo/cgeDemo/Base.lproj/Main.storyboard
@@ -197,6 +197,14 @@
+
@@ -326,5 +334,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demo/cgeDemo/ImageDeformViewController.h b/demo/cgeDemo/ImageDeformViewController.h
new file mode 100644
index 0000000..1babe11
--- /dev/null
+++ b/demo/cgeDemo/ImageDeformViewController.h
@@ -0,0 +1,13 @@
+//
+// ImageDeformViewController.h
+// cgeDemo
+//
+// Created on 2024-11-28
+// Description: Demo view controller for image deformation/liquify feature
+//
+
+#import
+
+@interface ImageDeformViewController : UIViewController
+
+@end
diff --git a/demo/cgeDemo/ImageDeformViewController.mm b/demo/cgeDemo/ImageDeformViewController.mm
new file mode 100644
index 0000000..10c80b5
--- /dev/null
+++ b/demo/cgeDemo/ImageDeformViewController.mm
@@ -0,0 +1,540 @@
+//
+// ImageDeformViewController.m
+// cgeDemo
+//
+// Created on 2024-11-28
+// Description: Demo view controller for image deformation/liquify feature
+//
+
+#import "ImageDeformViewController.h"
+#import "cgeDeformFilterWrapper.h"
+#import "cgeImageViewHandler.h"
+#import "cgeUtilFunctions.h"
+#import "demoUtils.h"
+#import "cgeImageHandlerIOS.h"
+#include "cgeMultipleEffects.h"
+
+// Constants for deform parameters
+static const CGFloat kDefaultTouchRadius = 100.0f;
+static const CGFloat kDefaultTouchIntensity = 0.15f;
+static const CGFloat kRadiusIncrement = 20.0f;
+static const CGFloat kMinRadius = 20.0f;
+static const CGFloat kMaxRadius = 400.0f;
+static const CGFloat kIntensityIncrement = 0.05f;
+static const CGFloat kMinIntensity = 0.02f;
+static const CGFloat kMaxIntensity = 0.9f;
+static const CGFloat kMaxImageDimension = 2048.0f;
+static const CGFloat kMaxProcessingDimension = 1280.0f;
+static const int kDefaultUndoSteps = 200;
+
+typedef NS_ENUM(NSInteger, DeformMode) {
+ DeformModeRestore,
+ DeformModeForward,
+ DeformModeBloat,
+ DeformModeWrinkle
+};
+
+@interface ImageDeformViewController()
+{
+ CGEImageHandlerIOS* _imageHandler;
+ CGEDeformFilterWrapper* _deformWrapper;
+ DeformMode _deformMode;
+ CGFloat _touchRadius;
+ CGFloat _touchIntensity;
+ CGPoint _lastTouchPoint;
+ BOOL _isMoving;
+ BOOL _hasMotion;
+ BOOL _showMesh;
+}
+
+@property (nonatomic, strong) GLKView* glkView;
+@property (nonatomic, strong) CGESharedGLContext* sharedContext;
+@property (nonatomic, strong) UISlider* restoreSlider;
+@property (nonatomic, strong) UIScrollView* buttonScrollView;
+@property (nonatomic, strong) UIImage* currentImage;
+@property (nonatomic) CGRect viewArea;
+@property (nonatomic) CGSize imageSize;
+
+@end
+
+@implementation ImageDeformViewController
+
+- (void)viewDidLoad {
+ [super viewDidLoad];
+
+ self.view.backgroundColor = [UIColor whiteColor];
+
+ // Default values
+ _deformMode = DeformModeForward;
+ _touchRadius = kDefaultTouchRadius;
+ _touchIntensity = kDefaultTouchIntensity;
+ _showMesh = NO;
+ _isMoving = NO;
+ _hasMotion = NO;
+
+ // Setup shared context
+ _sharedContext = [CGESharedGLContext createGlobalSharedContext];
+
+ // Setup UI
+ [self setupUI];
+
+ // Load default image
+ UIImage* defaultImage = [UIImage imageNamed:@"test2.jpg"];
+ if (defaultImage) {
+ [self setImage:defaultImage];
+ }
+}
+
+- (void)setupUI {
+ CGRect screenRect = [[UIScreen mainScreen] bounds];
+
+ // Quit button
+ UIButton* quitBtn = [UIButton buttonWithType:UIButtonTypeSystem];
+ [quitBtn setTitle:@"Quit" forState:UIControlStateNormal];
+ [quitBtn setFrame:CGRectMake(20, 50, 50, 30)];
+ [quitBtn addTarget:self action:@selector(quitBtnClicked:) forControlEvents:UIControlEventTouchUpInside];
+ [self.view addSubview:quitBtn];
+
+ // Restore slider
+ _restoreSlider = [[UISlider alloc] initWithFrame:CGRectMake(20, 90, screenRect.size.width - 40, 30)];
+ _restoreSlider.minimumValue = 0.0f;
+ _restoreSlider.maximumValue = 1.0f;
+ _restoreSlider.value = 0.0f;
+ [_restoreSlider addTarget:self action:@selector(restoreSliderChanged:) forControlEvents:UIControlEventValueChanged];
+ [self.view addSubview:_restoreSlider];
+
+ // GLKView for rendering
+ CGFloat glkViewY = 130;
+ CGFloat buttonScrollHeight = 60;
+ CGFloat glkViewHeight = screenRect.size.height - glkViewY - buttonScrollHeight - 20;
+
+ _glkView = [[GLKView alloc] initWithFrame:CGRectMake(0, glkViewY, screenRect.size.width, glkViewHeight)];
+ [_glkView setDrawableColorFormat:GLKViewDrawableColorFormatRGBA8888];
+ [_glkView setContext:[[CGESharedGLContext globalGLContext] context]];
+ [_glkView setEnableSetNeedsDisplay:NO];
+ [_glkView setBackgroundColor:[UIColor blackColor]];
+ [self.view addSubview:_glkView];
+
+ // Add touch gesture to GLKView
+ UIPanGestureRecognizer* panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)];
+ [_glkView addGestureRecognizer:panGesture];
+
+ // Button scroll view at the bottom
+ _buttonScrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, screenRect.size.height - buttonScrollHeight - 10, screenRect.size.width, buttonScrollHeight)];
+ _buttonScrollView.showsHorizontalScrollIndicator = YES;
+ [self.view addSubview:_buttonScrollView];
+
+ // Create buttons
+ NSArray* buttonTitles = @[@"Gallery", @"Save", @"Restore", @"Forward", @"RestoreMode", @"Bloat", @"Wrinkle", @"Undo", @"Redo", @"R+", @"R-", @"I+", @"I-", @"Mesh"];
+ NSArray* buttonSelectors = @[@"galleryBtnClicked:", @"saveBtnClicked:", @"restoreBtnClicked:", @"forwardModeBtnClicked:", @"restoreModeBtnClicked:", @"bloatModeBtnClicked:", @"wrinkleModeBtnClicked:", @"undoBtnClicked:", @"redoBtnClicked:", @"radiusIncClicked:", @"radiusDecClicked:", @"intensityIncClicked:", @"intensityDecClicked:", @"showMeshBtnClicked:"];
+
+ CGFloat buttonWidth = 80;
+ CGFloat buttonHeight = 40;
+ CGFloat xOffset = 10;
+
+ for (NSUInteger i = 0; i < buttonTitles.count; i++) {
+ UIButton* btn = [UIButton buttonWithType:UIButtonTypeSystem];
+ [btn setTitle:buttonTitles[i] forState:UIControlStateNormal];
+ [btn setFrame:CGRectMake(xOffset, 10, buttonWidth, buttonHeight)];
+ [btn.layer setBorderColor:[UIColor blueColor].CGColor];
+ [btn.layer setBorderWidth:1.0f];
+ [btn.layer setCornerRadius:8.0f];
+ [btn addTarget:self action:NSSelectorFromString(buttonSelectors[i]) forControlEvents:UIControlEventTouchUpInside];
+ [_buttonScrollView addSubview:btn];
+ xOffset += buttonWidth + 10;
+ }
+
+ _buttonScrollView.contentSize = CGSizeMake(xOffset, buttonHeight);
+}
+
+- (void)setImage:(UIImage*)image {
+ if (image == nil) return;
+
+ _currentImage = image;
+ _imageSize = image.size;
+
+ [_sharedContext syncProcessingQueue:^{
+ [_sharedContext makeCurrent];
+
+ // Clean up existing handler
+ if (_imageHandler != nullptr) {
+ delete _imageHandler;
+ _imageHandler = nullptr;
+ }
+
+ // Release existing deform wrapper
+ if (_deformWrapper != nil) {
+ [_deformWrapper releaseWithDeleteNativeFilter:YES];
+ _deformWrapper = nil;
+ }
+
+ // Create new handler
+ _imageHandler = new CGE::CGEImageHandlerIOS();
+ if (!_imageHandler->initWithUIImage(image, true, true)) {
+ delete _imageHandler;
+ _imageHandler = nullptr;
+ NSLog(@"Failed to initialize image handler");
+ return;
+ }
+
+ CGE::TextureDrawer* drawer = _imageHandler->getResultDrawer();
+ if (drawer != nullptr) {
+ drawer->setFlipScale(1.0f, -1.0f);
+ }
+
+ // Create deform wrapper with scaled size
+ int w = (int)image.size.width;
+ int h = (int)image.size.height;
+ float scaling = MIN(kMaxProcessingDimension / w, kMaxProcessingDimension / h);
+ if (scaling < 1.0f) {
+ w = (int)(w * scaling);
+ h = (int)(h * scaling);
+ }
+
+ _deformWrapper = [CGEDeformFilterWrapper createWithWidth:w height:h stride:10.0f];
+ if (_deformWrapper) {
+ [_deformWrapper setUndoSteps:kDefaultUndoSteps];
+
+ // Add filter to handler
+ CGE::CGEMutipleEffectFilter* filter = new CGE::CGEMutipleEffectFilter();
+ filter->setTextureLoadFunction(cgeGlobalTextureLoadFunc, nullptr);
+ filter->initCustomize();
+ filter->addFilter((CGE::CGEImageFilterInterface*)[_deformWrapper nativeAddress]);
+ _imageHandler->addImageFilter(filter);
+ _imageHandler->processingFilters();
+ }
+ }];
+
+ [self updateViewArea];
+ [self renderImage];
+}
+
+- (void)updateViewArea {
+ float viewWidth = _glkView.frame.size.width * [UIScreen mainScreen].scale;
+ float viewHeight = _glkView.frame.size.height * [UIScreen mainScreen].scale;
+
+ float scaling = _imageSize.width / _imageSize.height;
+ float viewRatio = viewWidth / viewHeight;
+ float s = scaling / viewRatio;
+
+ float w, h;
+ if (s < 1.0) {
+ w = viewHeight * scaling;
+ h = viewHeight;
+ } else {
+ w = viewWidth;
+ h = viewWidth / scaling;
+ }
+
+ _viewArea = CGRectMake((viewWidth - w) / 2, (viewHeight - h) / 2, w, h);
+}
+
+- (void)renderImage {
+ if (_imageHandler == nullptr) return;
+
+ [_sharedContext syncProcessingQueue:^{
+ [_sharedContext makeCurrent];
+
+ [_glkView bindDrawable];
+ glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
+ glClear(GL_COLOR_BUFFER_BIT);
+
+ if (_imageHandler->getTargetTextureID() != 0) {
+ glViewport(_viewArea.origin.x, _viewArea.origin.y, _viewArea.size.width, _viewArea.size.height);
+ _imageHandler->drawResult();
+ }
+
+ [_glkView display];
+ }];
+}
+
+- (CGPoint)convertTouchPointToImageCoordinates:(CGPoint)touchPoint {
+ float scale = [UIScreen mainScreen].scale;
+ float viewWidth = _glkView.frame.size.width * scale;
+ float viewHeight = _glkView.frame.size.height * scale;
+
+ // Convert to OpenGL coordinates (flip Y)
+ float x = touchPoint.x * scale - _viewArea.origin.x;
+ float y = (viewHeight - touchPoint.y * scale) - _viewArea.origin.y;
+
+ return CGPointMake(x, y);
+}
+
+#pragma mark - Touch Handling
+
+- (void)handlePanGesture:(UIPanGestureRecognizer*)gesture {
+ if (_deformWrapper == nil) return;
+
+ CGPoint touchPoint = [gesture locationInView:_glkView];
+ CGPoint imagePoint = [self convertTouchPointToImageCoordinates:touchPoint];
+
+ float w = _viewArea.size.width;
+ float h = _viewArea.size.height;
+
+ switch (gesture.state) {
+ case UIGestureRecognizerStateBegan:
+ _isMoving = YES;
+ _lastTouchPoint = imagePoint;
+ _hasMotion = NO;
+ // Always push a deform step at the start of a new gesture for undo functionality
+ [_deformWrapper pushDeformStep];
+ break;
+
+ case UIGestureRecognizerStateChanged: {
+ if (_restoreSlider.value != 0) {
+ [_restoreSlider setValue:0 animated:NO];
+ }
+
+ [_sharedContext syncProcessingQueue:^{
+ [_sharedContext makeCurrent];
+
+ switch (_deformMode) {
+ case DeformModeRestore:
+ [_deformWrapper restoreWithPointX:imagePoint.x y:imagePoint.y width:w height:h radius:_touchRadius intensity:_touchIntensity];
+ break;
+ case DeformModeForward:
+ [_deformWrapper forwardDeformWithStartX:_lastTouchPoint.x startY:_lastTouchPoint.y endX:imagePoint.x endY:imagePoint.y width:w height:h radius:_touchRadius intensity:_touchIntensity];
+ break;
+ case DeformModeBloat:
+ [_deformWrapper bloatDeformWithX:imagePoint.x y:imagePoint.y width:w height:h radius:_touchRadius intensity:_touchIntensity];
+ break;
+ case DeformModeWrinkle:
+ [_deformWrapper wrinkleDeformWithX:imagePoint.x y:imagePoint.y width:w height:h radius:_touchRadius intensity:_touchIntensity];
+ break;
+ }
+
+ _imageHandler->revertToKeptResult();
+ _imageHandler->processingFilters();
+ _hasMotion = YES;
+ }];
+
+ [self renderImage];
+ _lastTouchPoint = imagePoint;
+ break;
+ }
+
+ case UIGestureRecognizerStateEnded:
+ case UIGestureRecognizerStateCancelled:
+ _isMoving = NO;
+ if (_hasMotion) {
+ [_sharedContext syncProcessingQueue:^{
+ [_deformWrapper pushDeformStep];
+ }];
+ _hasMotion = NO;
+ }
+ break;
+
+ default:
+ break;
+ }
+}
+
+#pragma mark - Button Actions
+
+- (void)quitBtnClicked:(id)sender {
+ [self dismissViewControllerAnimated:YES completion:nil];
+}
+
+- (void)restoreSliderChanged:(UISlider*)slider {
+ if (_deformWrapper == nil) return;
+
+ [_sharedContext syncProcessingQueue:^{
+ [_sharedContext makeCurrent];
+ [_deformWrapper restoreWithIntensity:slider.value];
+ _imageHandler->revertToKeptResult();
+ _imageHandler->processingFilters();
+ }];
+
+ [self renderImage];
+}
+
+- (void)galleryBtnClicked:(id)sender {
+ UIImagePickerController* picker = [[UIImagePickerController alloc] init];
+ picker.sourceType = UIImagePickerControllerSourceTypeSavedPhotosAlbum;
+ picker.delegate = self;
+ picker.allowsEditing = NO;
+ [self presentViewController:picker animated:YES completion:nil];
+}
+
+- (void)saveBtnClicked:(id)sender {
+ if (_imageHandler == nullptr) return;
+
+ __block UIImage* resultImage = nil;
+ [_sharedContext syncProcessingQueue:^{
+ [_sharedContext makeCurrent];
+ resultImage = _imageHandler->getResultUIImage();
+ }];
+
+ if (resultImage) {
+ [DemoUtils saveImage:resultImage];
+ [self showToast:@"Image saved!"];
+ }
+}
+
+- (void)restoreBtnClicked:(id)sender {
+ if (_deformWrapper == nil) return;
+
+ [_sharedContext syncProcessingQueue:^{
+ [_sharedContext makeCurrent];
+ [_deformWrapper restore];
+ _imageHandler->revertToKeptResult();
+ _imageHandler->processingFilters();
+ }];
+
+ [self renderImage];
+ [self showToast:@"Restored to original"];
+}
+
+- (void)forwardModeBtnClicked:(id)sender {
+ _deformMode = DeformModeForward;
+ [self showToast:@"Forward mode"];
+}
+
+- (void)restoreModeBtnClicked:(id)sender {
+ _deformMode = DeformModeRestore;
+ [self showToast:@"Restore mode"];
+}
+
+- (void)bloatModeBtnClicked:(id)sender {
+ _deformMode = DeformModeBloat;
+ [self showToast:@"Bloat mode"];
+}
+
+- (void)wrinkleModeBtnClicked:(id)sender {
+ _deformMode = DeformModeWrinkle;
+ [self showToast:@"Wrinkle mode"];
+}
+
+- (void)undoBtnClicked:(id)sender {
+ if (_deformWrapper == nil) return;
+
+ [_sharedContext syncProcessingQueue:^{
+ [_sharedContext makeCurrent];
+ if ([_deformWrapper undo]) {
+ _imageHandler->revertToKeptResult();
+ _imageHandler->processingFilters();
+ } else {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [self showToast:@"Nothing to undo"];
+ });
+ }
+ }];
+
+ [self renderImage];
+}
+
+- (void)redoBtnClicked:(id)sender {
+ if (_deformWrapper == nil) return;
+
+ [_sharedContext syncProcessingQueue:^{
+ [_sharedContext makeCurrent];
+ if ([_deformWrapper redo]) {
+ _imageHandler->revertToKeptResult();
+ _imageHandler->processingFilters();
+ } else {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [self showToast:@"Nothing to redo"];
+ });
+ }
+ }];
+
+ [self renderImage];
+}
+
+- (void)radiusIncClicked:(id)sender {
+ _touchRadius += kRadiusIncrement;
+ if (_touchRadius > kMaxRadius) _touchRadius = kMaxRadius;
+ [self showToast:[NSString stringWithFormat:@"Radius: %.0f", _touchRadius]];
+}
+
+- (void)radiusDecClicked:(id)sender {
+ _touchRadius -= kRadiusIncrement;
+ if (_touchRadius < kMinRadius) _touchRadius = kMinRadius;
+ [self showToast:[NSString stringWithFormat:@"Radius: %.0f", _touchRadius]];
+}
+
+- (void)intensityIncClicked:(id)sender {
+ _touchIntensity += kIntensityIncrement;
+ if (_touchIntensity > kMaxIntensity) _touchIntensity = kMaxIntensity;
+ [self showToast:[NSString stringWithFormat:@"Intensity: %.2f", _touchIntensity]];
+}
+
+- (void)intensityDecClicked:(id)sender {
+ _touchIntensity -= kIntensityIncrement;
+ if (_touchIntensity < kMinIntensity) _touchIntensity = kMinIntensity;
+ [self showToast:[NSString stringWithFormat:@"Intensity: %.2f", _touchIntensity]];
+}
+
+- (void)showMeshBtnClicked:(id)sender {
+ _showMesh = !_showMesh;
+
+ [_sharedContext syncProcessingQueue:^{
+ [_sharedContext makeCurrent];
+ [_deformWrapper showMesh:_showMesh];
+ _imageHandler->revertToKeptResult();
+ _imageHandler->processingFilters();
+ }];
+
+ [self renderImage];
+ [self showToast:_showMesh ? @"Mesh shown" : @"Mesh hidden"];
+}
+
+#pragma mark - UIImagePickerControllerDelegate
+
+- (void)imagePickerController:(UIImagePickerController*)picker didFinishPickingMediaWithInfo:(NSDictionary*)info {
+ UIImage* image = [info objectForKey:UIImagePickerControllerOriginalImage];
+
+ if (image) {
+ // Scale down if needed
+ int w = (int)image.size.width;
+ int h = (int)image.size.height;
+ float s = MAX(w / kMaxImageDimension, h / kMaxImageDimension);
+
+ if (s > 1.0f) {
+ w = (int)(w / s);
+ h = (int)(h / s);
+ image = [self scaleImage:image toSize:CGSizeMake(w, h)];
+ }
+
+ [self setImage:image];
+ }
+
+ [picker dismissViewControllerAnimated:YES completion:nil];
+}
+
+- (UIImage*)scaleImage:(UIImage*)image toSize:(CGSize)size {
+ UIGraphicsBeginImageContextWithOptions(size, NO, 1.0);
+ [image drawInRect:CGRectMake(0, 0, size.width, size.height)];
+ UIImage* scaledImage = UIGraphicsGetImageFromCurrentImageContext();
+ UIGraphicsEndImageContext();
+ return scaledImage;
+}
+
+#pragma mark - Helper Methods
+
+- (void)showToast:(NSString*)message {
+ UIAlertController* alert = [UIAlertController alertControllerWithTitle:nil message:message preferredStyle:UIAlertControllerStyleAlert];
+ [self presentViewController:alert animated:YES completion:nil];
+
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
+ [alert dismissViewControllerAnimated:YES completion:nil];
+ });
+}
+
+- (void)dealloc {
+ if (_imageHandler != nullptr) {
+ [_sharedContext syncProcessingQueue:^{
+ [_sharedContext makeCurrent];
+ delete _imageHandler;
+ _imageHandler = nullptr;
+ }];
+ }
+
+ if (_deformWrapper != nil) {
+ // Don't delete native filter as it's owned by the handler's filter chain
+ [_deformWrapper releaseWithDeleteNativeFilter:NO];
+ _deformWrapper = nil;
+ }
+}
+
+@end
diff --git a/library/cge.framework/Headers/cge.h b/library/cge.framework/Headers/cge.h
index 92bc553..7889964 100644
--- a/library/cge.framework/Headers/cge.h
+++ b/library/cge.framework/Headers/cge.h
@@ -59,6 +59,7 @@ FOUNDATION_EXPORT const unsigned char cgeVersionString[];
#import
#import
#import
+#import
#ifdef __cplusplus
diff --git a/library/cge.framework/Headers/cgeDeformFilterWrapper.h b/library/cge.framework/Headers/cgeDeformFilterWrapper.h
new file mode 100644
index 0000000..227ba96
--- /dev/null
+++ b/library/cge.framework/Headers/cgeDeformFilterWrapper.h
@@ -0,0 +1,132 @@
+/*
+ * cgeDeformFilterWrapper.h
+ *
+ * Created on: 2024-11-28
+ * Author: Wang Yang
+ * Mail: admin@wysaid.org
+ * Description: Objective-C wrapper for liquify/deform filter
+ */
+
+#import
+#import
+#import "cgeSharedGLContext.h"
+
+@interface CGEDeformFilterWrapper : NSObject
+
+@property (nonatomic, readonly) void* nativeAddress;
+
+/**
+ * Create a deform filter wrapper
+ * @param width The real width of the picture in pixels
+ * @param height The real height of the picture in pixels
+ * @param stride Set the mesh in real pixels, the mesh size would be (width / stride, height / stride)
+ * @return CGEDeformFilterWrapper instance or nil if creation failed
+ */
++ (instancetype)createWithWidth:(float)width height:(float)height stride:(float)stride;
+
+/**
+ * Release the native filter
+ * @param shouldDeleteNativeFilter If NO, the native filter will not be deleted (useful when the filter is bound to an image handler)
+ */
+- (void)releaseWithDeleteNativeFilter:(BOOL)shouldDeleteNativeFilter;
+
+/**
+ * Restore the mesh to its original state
+ */
+- (void)restore;
+
+/**
+ * Restore the mesh with intensity
+ * @param intensity Range [0, 1]. 0 for no effect and 1 for origin.
+ */
+- (void)restoreWithIntensity:(float)intensity;
+
+/**
+ * Forward deform the mesh
+ * @param startX Start X position of the cursor
+ * @param startY Start Y position of the cursor
+ * @param endX End X position of the cursor
+ * @param endY End Y position of the cursor
+ * @param w Canvas width (the max 'x' of the cursor)
+ * @param h Canvas height (the max 'y' of the cursor)
+ * @param radius The deform radius in real pixels
+ * @param intensity Range (0, 1], 0 for origin. Better not more than 0.5
+ */
+- (void)forwardDeformWithStartX:(float)startX startY:(float)startY endX:(float)endX endY:(float)endY width:(float)w height:(float)h radius:(float)radius intensity:(float)intensity;
+
+/**
+ * Restore mesh at a specific point
+ * @param x X position of the cursor
+ * @param y Y position of the cursor
+ * @param w Canvas width
+ * @param h Canvas height
+ * @param radius The restore radius in real pixels
+ * @param intensity Restore intensity
+ */
+- (void)restoreWithPointX:(float)x y:(float)y width:(float)w height:(float)h radius:(float)radius intensity:(float)intensity;
+
+/**
+ * Bloat (expand) deform at a specific point
+ * @param x X position of the cursor
+ * @param y Y position of the cursor
+ * @param w Canvas width
+ * @param h Canvas height
+ * @param radius The bloat radius in real pixels
+ * @param intensity Bloat intensity
+ */
+- (void)bloatDeformWithX:(float)x y:(float)y width:(float)w height:(float)h radius:(float)radius intensity:(float)intensity;
+
+/**
+ * Wrinkle (shrink) deform at a specific point
+ * @param x X position of the cursor
+ * @param y Y position of the cursor
+ * @param w Canvas width
+ * @param h Canvas height
+ * @param radius The wrinkle radius in real pixels
+ * @param intensity Wrinkle intensity
+ */
+- (void)wrinkleDeformWithX:(float)x y:(float)y width:(float)w height:(float)h radius:(float)radius intensity:(float)intensity;
+
+/**
+ * Set the maximum number of undo steps
+ * @param undoSteps Maximum number of undo steps
+ */
+- (void)setUndoSteps:(int)undoSteps;
+
+/**
+ * Check if undo is available
+ * @return YES if undo is available
+ */
+- (BOOL)canUndo;
+
+/**
+ * Check if redo is available
+ * @return YES if redo is available
+ */
+- (BOOL)canRedo;
+
+/**
+ * Undo the last deform operation
+ * @return YES if undo was successful
+ */
+- (BOOL)undo;
+
+/**
+ * Redo the last undone deform operation
+ * @return YES if redo was successful
+ */
+- (BOOL)redo;
+
+/**
+ * Push the current deform step to the undo stack
+ * @return YES if push was successful, NO if the steps count meets the max step limit
+ */
+- (BOOL)pushDeformStep;
+
+/**
+ * Show or hide the mesh overlay
+ * @param show YES to show the mesh, NO to hide it
+ */
+- (void)showMesh:(BOOL)show;
+
+@end
diff --git a/library/cge/include/cge.h b/library/cge/include/cge.h
index 92bc553..7889964 100644
--- a/library/cge/include/cge.h
+++ b/library/cge/include/cge.h
@@ -59,6 +59,7 @@ FOUNDATION_EXPORT const unsigned char cgeVersionString[];
#import
#import
#import
+#import
#ifdef __cplusplus
diff --git a/library/cge/interfaces/cgeDeformFilterWrapper.h b/library/cge/interfaces/cgeDeformFilterWrapper.h
new file mode 100644
index 0000000..227ba96
--- /dev/null
+++ b/library/cge/interfaces/cgeDeformFilterWrapper.h
@@ -0,0 +1,132 @@
+/*
+ * cgeDeformFilterWrapper.h
+ *
+ * Created on: 2024-11-28
+ * Author: Wang Yang
+ * Mail: admin@wysaid.org
+ * Description: Objective-C wrapper for liquify/deform filter
+ */
+
+#import
+#import
+#import "cgeSharedGLContext.h"
+
+@interface CGEDeformFilterWrapper : NSObject
+
+@property (nonatomic, readonly) void* nativeAddress;
+
+/**
+ * Create a deform filter wrapper
+ * @param width The real width of the picture in pixels
+ * @param height The real height of the picture in pixels
+ * @param stride Set the mesh in real pixels, the mesh size would be (width / stride, height / stride)
+ * @return CGEDeformFilterWrapper instance or nil if creation failed
+ */
++ (instancetype)createWithWidth:(float)width height:(float)height stride:(float)stride;
+
+/**
+ * Release the native filter
+ * @param shouldDeleteNativeFilter If NO, the native filter will not be deleted (useful when the filter is bound to an image handler)
+ */
+- (void)releaseWithDeleteNativeFilter:(BOOL)shouldDeleteNativeFilter;
+
+/**
+ * Restore the mesh to its original state
+ */
+- (void)restore;
+
+/**
+ * Restore the mesh with intensity
+ * @param intensity Range [0, 1]. 0 for no effect and 1 for origin.
+ */
+- (void)restoreWithIntensity:(float)intensity;
+
+/**
+ * Forward deform the mesh
+ * @param startX Start X position of the cursor
+ * @param startY Start Y position of the cursor
+ * @param endX End X position of the cursor
+ * @param endY End Y position of the cursor
+ * @param w Canvas width (the max 'x' of the cursor)
+ * @param h Canvas height (the max 'y' of the cursor)
+ * @param radius The deform radius in real pixels
+ * @param intensity Range (0, 1], 0 for origin. Better not more than 0.5
+ */
+- (void)forwardDeformWithStartX:(float)startX startY:(float)startY endX:(float)endX endY:(float)endY width:(float)w height:(float)h radius:(float)radius intensity:(float)intensity;
+
+/**
+ * Restore mesh at a specific point
+ * @param x X position of the cursor
+ * @param y Y position of the cursor
+ * @param w Canvas width
+ * @param h Canvas height
+ * @param radius The restore radius in real pixels
+ * @param intensity Restore intensity
+ */
+- (void)restoreWithPointX:(float)x y:(float)y width:(float)w height:(float)h radius:(float)radius intensity:(float)intensity;
+
+/**
+ * Bloat (expand) deform at a specific point
+ * @param x X position of the cursor
+ * @param y Y position of the cursor
+ * @param w Canvas width
+ * @param h Canvas height
+ * @param radius The bloat radius in real pixels
+ * @param intensity Bloat intensity
+ */
+- (void)bloatDeformWithX:(float)x y:(float)y width:(float)w height:(float)h radius:(float)radius intensity:(float)intensity;
+
+/**
+ * Wrinkle (shrink) deform at a specific point
+ * @param x X position of the cursor
+ * @param y Y position of the cursor
+ * @param w Canvas width
+ * @param h Canvas height
+ * @param radius The wrinkle radius in real pixels
+ * @param intensity Wrinkle intensity
+ */
+- (void)wrinkleDeformWithX:(float)x y:(float)y width:(float)w height:(float)h radius:(float)radius intensity:(float)intensity;
+
+/**
+ * Set the maximum number of undo steps
+ * @param undoSteps Maximum number of undo steps
+ */
+- (void)setUndoSteps:(int)undoSteps;
+
+/**
+ * Check if undo is available
+ * @return YES if undo is available
+ */
+- (BOOL)canUndo;
+
+/**
+ * Check if redo is available
+ * @return YES if redo is available
+ */
+- (BOOL)canRedo;
+
+/**
+ * Undo the last deform operation
+ * @return YES if undo was successful
+ */
+- (BOOL)undo;
+
+/**
+ * Redo the last undone deform operation
+ * @return YES if redo was successful
+ */
+- (BOOL)redo;
+
+/**
+ * Push the current deform step to the undo stack
+ * @return YES if push was successful, NO if the steps count meets the max step limit
+ */
+- (BOOL)pushDeformStep;
+
+/**
+ * Show or hide the mesh overlay
+ * @param show YES to show the mesh, NO to hide it
+ */
+- (void)showMesh:(BOOL)show;
+
+@end
diff --git a/library/cge/interfaces/cgeDeformFilterWrapper.mm b/library/cge/interfaces/cgeDeformFilterWrapper.mm
new file mode 100644
index 0000000..507c9ee
--- /dev/null
+++ b/library/cge/interfaces/cgeDeformFilterWrapper.mm
@@ -0,0 +1,172 @@
+/*
+ * cgeDeformFilterWrapper.mm
+ *
+ * Created on: 2024-11-28
+ * Author: Wang Yang
+ * Mail: admin@wysaid.org
+ * Description: Objective-C wrapper for liquify/deform filter
+ */
+
+#import "cgeDeformFilterWrapper.h"
+#import "cgeLiquidationFilter.h"
+#import "cgeVec.h"
+
+using namespace CGE;
+
+@interface CGEDeformFilterWrapper()
+{
+ CGELiquidationFilter* _filter;
+}
+@end
+
+@implementation CGEDeformFilterWrapper
+
++ (instancetype)createWithWidth:(float)width height:(float)height stride:(float)stride
+{
+ // Validate input parameters
+ if (width <= 0 || height <= 0 || stride <= 0) {
+ NSLog(@"CGEDeformFilterWrapper.create failed: Invalid parameters (width: %.2f, height: %.2f, stride: %.2f). All values must be positive.", width, height, stride);
+ return nil;
+ }
+
+ CGEDeformFilterWrapper* wrapper = [[CGEDeformFilterWrapper alloc] initWithWidth:width height:height stride:stride];
+ if (wrapper && wrapper->_filter == nullptr) {
+ wrapper = nil;
+ NSLog(@"CGEDeformFilterWrapper.create failed!");
+ }
+ return wrapper;
+}
+
+- (instancetype)initWithWidth:(float)width height:(float)height stride:(float)stride
+{
+ self = [super init];
+ if (self) {
+ _filter = new CGELiquidationFilter();
+ if (!_filter->initWithMesh(width, height, stride)) {
+ delete _filter;
+ _filter = nullptr;
+ }
+ }
+ return self;
+}
+
+- (void)dealloc
+{
+ if (_filter != nullptr) {
+ delete _filter;
+ _filter = nullptr;
+ }
+}
+
+- (void*)nativeAddress
+{
+ return _filter;
+}
+
+- (void)releaseWithDeleteNativeFilter:(BOOL)shouldDeleteNativeFilter
+{
+ if (_filter != nullptr) {
+ if (shouldDeleteNativeFilter) {
+ delete _filter;
+ }
+ _filter = nullptr;
+ }
+}
+
+- (void)restore
+{
+ if (_filter != nullptr) {
+ _filter->restoreMesh();
+ }
+}
+
+- (void)restoreWithIntensity:(float)intensity
+{
+ if (_filter != nullptr) {
+ _filter->restoreMeshWithIntensity(intensity);
+ }
+}
+
+- (void)forwardDeformWithStartX:(float)startX startY:(float)startY endX:(float)endX endY:(float)endY width:(float)w height:(float)h radius:(float)radius intensity:(float)intensity
+{
+ if (_filter != nullptr) {
+ _filter->forwardDeformMesh(Vec2f(startX, startY), Vec2f(endX, endY), w, h, radius, intensity);
+ }
+}
+
+- (void)restoreWithPointX:(float)x y:(float)y width:(float)w height:(float)h radius:(float)radius intensity:(float)intensity
+{
+ if (_filter != nullptr) {
+ _filter->restoreMeshWithPoint(Vec2f(x, y), w, h, radius, intensity);
+ }
+}
+
+- (void)bloatDeformWithX:(float)x y:(float)y width:(float)w height:(float)h radius:(float)radius intensity:(float)intensity
+{
+ if (_filter != nullptr) {
+ _filter->bloatMeshWithPoint(Vec2f(x, y), w, h, radius, intensity);
+ }
+}
+
+- (void)wrinkleDeformWithX:(float)x y:(float)y width:(float)w height:(float)h radius:(float)radius intensity:(float)intensity
+{
+ if (_filter != nullptr) {
+ _filter->wrinkleMeshWithPoint(Vec2f(x, y), w, h, radius, intensity);
+ }
+}
+
+- (void)setUndoSteps:(int)undoSteps
+{
+ if (_filter != nullptr) {
+ _filter->setUndoSteps(undoSteps);
+ }
+}
+
+- (BOOL)canUndo
+{
+ if (_filter != nullptr) {
+ return _filter->canUndo();
+ }
+ return NO;
+}
+
+- (BOOL)canRedo
+{
+ if (_filter != nullptr) {
+ return _filter->canRedo();
+ }
+ return NO;
+}
+
+- (BOOL)undo
+{
+ if (_filter != nullptr) {
+ return _filter->undo();
+ }
+ return NO;
+}
+
+- (BOOL)redo
+{
+ if (_filter != nullptr) {
+ return _filter->redo();
+ }
+ return NO;
+}
+
+- (BOOL)pushDeformStep
+{
+ if (_filter != nullptr) {
+ return _filter->pushMesh();
+ }
+ return NO;
+}
+
+- (void)showMesh:(BOOL)show
+{
+ if (_filter != nullptr) {
+ _filter->showMesh(show);
+ }
+}
+
+@end