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