// // SignatureViewM.m // HelloTriangle // // Created by Rui Zhang on 4/20/23. // Copyright © 2023 Apple. All rights reserved. // #import "SignatureViewM.h" #import "const.h" #import "RAUtils.h" //#import "AAPLRenderer.h" #define STROKE_WIDTH_SMOOTHING 0.5 // Low pass filter alpha #define VELOCITY_CLAMP_MIN 20 #define VELOCITY_CLAMP_MAX 5000 #define QUADRATIC_DISTANCE_TOLERANCE 3.0 // Minimum distance to make a curve #define MAXIMUM_VERTECES 100000 static vector_float4 StrokeColor = { 0, 0, 0, 1 }; static vector_float4 clearColor = { 1, 1, 1, 0 }; //typedef struct //{ // //} AAPLVertex; static const uint maxLength = MAXIMUM_VERTECES; static inline void addVertex(uint *Totallength,NSMutableArray *partlength, PPSSignaturePoint v,PPSSignaturePoint* dest) { if ((*Totallength) >= maxLength) { return; } dest[*Totallength]=v; // // GLvoid *data = glMapBufferOES(GL_ARRAY_BUFFER, GL_WRITE_ONLY_OES); // memcpy(data + sizeof(PPSSignaturePoint) * (*length), &v, sizeof(PPSSignaturePoint)); // glUnmapBufferOES(GL_ARRAY_BUFFER); // partlength[partlength.count-1]=@([partlength[partlength.count-1] intValue] +1); (*Totallength)++; #ifdef DEBUG NSLog(@"dest %p total %d part %ld ,partlength %@ vertex (%f,%f)(%f,%f,%f,%f)",dest,*Totallength,partlength.count-1, partlength[partlength.count-1],v.position[0],v.position[1],v.color[0],v.color[1],v.color[2],v.color[3]); #endif } static inline CGPoint QuadraticPointInCurve(CGPoint start, CGPoint end, CGPoint controlPoint, float percent) { double a = pow((1.0 - percent), 2.0); double b = 2.0 * percent * (1.0 - percent); double c = pow(percent, 2.0); return (CGPoint) { a * start.x + b * controlPoint.x + c * end.x, a * start.y + b * controlPoint.y + c * end.y }; } static float generateRandom(float from, float to) { return random() % 10000 / 10000.0 * (to - from) + from; } static float clamp(float min, float max, float value) { return fmaxf(min, fminf(max, value)); } // Find perpendicular vector from two other vectors to compute triangle strip around line static vector_float2 perpendicular(PPSSignaturePoint p1, PPSSignaturePoint p2) { vector_float2 ret={0,0}; // GLKVector3 ret; ret[0] = p2.position[1] - p1.position[1]; ret[1] = -1 * (p2.position[0] - p1.position[0]); return ret; } static PPSSignaturePoint ViewPointToMTK(CGPoint viewPoint, CGSize view_size,CGSize drawable_size ,vector_float4 color) { vector_float2 inverseViewSize={1.0f / view_size.width, 1.0f / view_size.height}; // passed in a buffer float clipX = (2.0f * viewPoint.x * inverseViewSize.x) - 1.0f; float clipY = (2.0f * -viewPoint.y * inverseViewSize.y) + 1.0f; return (PPSSignaturePoint) { { clipX*drawable_size.width/2, clipY*drawable_size.height/2 }, {color[0],color[1],color[2],color[3]} }; // // float4 clipPosition(clipX, clipY, 0.0f, 1.0f); } @interface SignatureViewM () @property(nonatomic,assign)CGPoint mixPoint; @property(nonatomic,assign)CGPoint maxPoint; //@property (assign, nonatomic) NSMutableArray *drawable; @end @interface SignatureViewM () { // OpenGL state // EAGLContext *context; // GLKBaseEffect *effect; // GLuint vertexArray; // GLuint vertexBuffer; // GLuint dotsArray; // GLuint dotsBuffer; // // Array of verteces, with current length PPSSignaturePoint SignatureVertexData[maxLength]; uint vertexTotal; NSMutableArray* vertexLength; PPSSignaturePoint SignatureDotsData[maxLength]; uint dotsTotal; // NSMutableArray* dotsLength; // Width of line at current and previous vertex float penThickness; float previousThickness; // Previous points for quadratic bezier computations CGPoint previousPoint; CGPoint previousMidPoint; PPSSignaturePoint previousVertex; PPSSignaturePoint currentVelocity; } @end @implementation SignatureViewM SignatureRenderer *_renderer; - (void)commonInit { [self setLineWidth:5]; self.framebufferOnly=false; vertexLength = [NSMutableArray new]; [vertexLength addObject:@0]; // dotsLength = [NSMutableArray new]; //#define STROKE_WIDTH_MIN 0.004 // Stroke width determined by touch velocity //#define STROKE_WIDTH_MAX 0.010 self.MaxLineWidth = 0.010*1000; self.MinLineWidth = 0.004*1000; self.device = MTLCreateSystemDefaultDevice(); NSAssert(self.device, @"Metal is not supported on this device"); _renderer = [[SignatureRenderer alloc] initWithMetalKitView:self]; _renderer.parts =vertexLength; _renderer.SignatureVertexData = SignatureVertexData; NSAssert(_renderer, @"Renderer failed initialization"); // Initialize our renderer with the view size [_renderer mtkView:self drawableSizeWillChange:self.drawableSize]; self.delegate = _renderer; // context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; // // if (context) { // time(NULL); // // self.backgroundColor = [UIColor clearColor]; // self.opaque = YES; // // self.context = context; // self.drawableDepthFormat = GLKViewDrawableDepthFormat24; // self.enableSetNeedsDisplay = YES; // // // Turn on antialiasing // self.drawableMultisample = GLKViewDrawableMultisample4X; // // [self setupGL]; // // Capture touches UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)]; pan.maximumNumberOfTouches = pan.minimumNumberOfTouches = 1; pan.cancelsTouchesInView = YES; [self addGestureRecognizer:pan]; // For dotting your i's UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tap:)]; tap.cancelsTouchesInView = YES; tap.numberOfTapsRequired = 1; [self addGestureRecognizer:tap]; // // Erase with long press // UILongPressGestureRecognizer *longer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPress:)]; // longer.cancelsTouchesInView = YES; // [self addGestureRecognizer:longer]; // // } else [NSException raise:@"NSOpenGLES2ContextException" format:@"Failed to create OpenGL ES2 context"]; } - (id)initWithCoder:(NSCoder *)aDecoder { if (self = [super initWithCoder:aDecoder]) { [self commonInit]; _mixPoint =CGPointMake(self.bounds.size.width, self.bounds.size.height); _maxPoint =CGPointMake(0, 0); } return self; } - (instancetype)initWithFrame:(CGRect)frameRect device:(id)device { if(self=[super initWithFrame:frameRect device:device]) { [self commonInit]; _mixPoint =CGPointMake(self.bounds.size.width, self.bounds.size.height); _maxPoint =CGPointMake(0, 0); } return self; } - (void)setLineWidth:(CGFloat)width { self.MaxLineWidth = width;//400.0; self.MinLineWidth = width*0.4;//400.0*0.4; penThickness = (self.MaxLineWidth - self.MinLineWidth)/2.0; } //清除画布 - (void)clear { [vertexLength removeAllObjects]; [vertexLength addObject:@0]; vertexTotal =0; // _renderer.SignatureVertexData = SignatureVertexData; // dotsLength = nil; self.isSigned = NO; _mixPoint =CGPointMake(self.bounds.size.width, self.bounds.size.height); _maxPoint =CGPointMake(0, 0); [self setNeedsDisplay]; } - (UIImage *)signatureImage { return [self Signature2Image]; } - (NSData *)signatureData { UIImage* signatureImage = [self Signature2Image]; return UIImagePNGRepresentation(signatureImage); } - (UIImage*)Signature2Image { if (!self.isSigned) return nil; UIImage *screenshot = [self snapshot]; if(screenshot==nil) { NSAssert(false, @"screenshot is null"); } float x = _mixPoint.x - MAX_LINEWIDTH /2.0; float y = _mixPoint.y - MAX_LINEWIDTH / 2.0; if(x<0) x=0; if(y<0) y=0; float screen_scale = [[UIScreen mainScreen] scale]; CGRect rect = CGRectMake(x*screenshot.scale*screen_scale, y*screenshot.scale*screen_scale, (_maxPoint.x-_mixPoint.x+MAX_LINEWIDTH)*screenshot.scale*screen_scale, (_maxPoint.y-_mixPoint.y+MAX_LINEWIDTH)*screenshot.scale*screen_scale); CGImageRef imageRef=screenshot.CGImage; CGImageRef imagePartRef=CGImageCreateWithImageInRect(imageRef,rect); UIImage*cropImage=[UIImage imageWithCGImage:imagePartRef]; CGImageRelease(imagePartRef);; if(cropImage==nil) { NSAssert(false, @"Signature2Image: cropImage is nil"); // int debug = 1; } return cropImage; } /** 按给定的方向旋转图片 */ - (UIImage*)rotate:(UIImage*)source orient:(UIImageOrientation)orient { CGRect bnds = CGRectZero; UIImage* copy = nil; CGContextRef ctxt = nil; CGImageRef imag = source.CGImage; CGRect rect = CGRectZero; CGAffineTransform tran = CGAffineTransformIdentity; rect.size.width = CGImageGetWidth(imag); rect.size.height = CGImageGetHeight(imag); bnds = rect; switch (orient) { case UIImageOrientationUp: return source; case UIImageOrientationUpMirrored: tran = CGAffineTransformMakeTranslation(rect.size.width, 0.0); tran = CGAffineTransformScale(tran, -1.0, 1.0); break; case UIImageOrientationDown: tran = CGAffineTransformMakeTranslation(rect.size.width, rect.size.height); tran = CGAffineTransformRotate(tran, M_PI); break; case UIImageOrientationDownMirrored: tran = CGAffineTransformMakeTranslation(0.0, rect.size.height); tran = CGAffineTransformScale(tran, 1.0, -1.0); break; // case UIImageOrientationLeft: // bnds = swapWidthAndHeight(bnds); // tran = CGAffineTransformMakeTranslation(0.0, rect.size.width); // tran = CGAffineTransformRotate(tran, 3.0 * M_PI / 2.0); // break; // // case UIImageOrientationLeftMirrored: // bnds = swapWidthAndHeight(bnds); // tran = CGAffineTransformMakeTranslation(rect.size.height, // rect.size.width); // tran = CGAffineTransformScale(tran, -1.0, 1.0); // tran = CGAffineTransformRotate(tran, 3.0 * M_PI / 2.0); // break; // // case UIImageOrientationRight: // bnds = swapWidthAndHeight(bnds); // tran = CGAffineTransformMakeTranslation(rect.size.height, 0.0); // tran = CGAffineTransformRotate(tran, M_PI / 2.0); // break; // // case UIImageOrientationRightMirrored: // bnds = swapWidthAndHeight(bnds); // tran = CGAffineTransformMakeScale(-1.0, 1.0); // tran = CGAffineTransformRotate(tran, M_PI / 2.0); // break; default: return source; } UIGraphicsBeginImageContext(bnds.size); ctxt = UIGraphicsGetCurrentContext(); switch (orient) { case UIImageOrientationLeft: case UIImageOrientationLeftMirrored: case UIImageOrientationRight: case UIImageOrientationRightMirrored: CGContextScaleCTM(ctxt, -1.0, 1.0); CGContextTranslateCTM(ctxt, -rect.size.height, 0.0); break; default: CGContextScaleCTM(ctxt, 1.0, -1.0); CGContextTranslateCTM(ctxt, 0.0, -rect.size.height); break; } CGContextConcatCTM(ctxt, tran); CGContextDrawImage(UIGraphicsGetCurrentContext(), rect, imag); copy = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return copy; } -(UIImage*) snapshot { CIContext * context = [CIContext contextWithMTLDevice:self.device]; CIImage *outputImage = [[CIImage alloc] initWithMTLTexture:self.currentDrawable.texture options:@{kCIImageColorSpace:(__bridge_transfer id)CGColorSpaceCreateDeviceRGB()}]; CGImageRef cgImg = [context createCGImage:outputImage fromRect:CGRectMake(0, 0, [UIScreen mainScreen].nativeBounds.size.width, [UIScreen mainScreen].nativeBounds.size.height)]; UIImage *resultImg = [UIImage imageWithCGImage:cgImg scale:1.0 orientation:UIImageOrientationUp]; resultImg = [self rotate:resultImg orient: UIImageOrientationDownMirrored]; // // UIImageOrientationUp, // default orientation // UIImageOrientationDown, // 180 deg rotation // UIImageOrientationLeft, // 90 deg CCW // UIImageOrientationRight, // 90 deg CW // UIImageOrientationUpMirrored, // as above but image mirrored along other axis. horizontal flip // UIImageOrientationDownMirrored, // horizontal flip // UIImageOrientationLeftMirrored, // vertical flip // UIImageOrientationRightMirrored, // vertical flip CGImageRelease(cgImg); return resultImg; // id lastDrawableDisplayed = [self.currentDrawable texture]; // // int width = (int)[lastDrawableDisplayed width]; // int height = (int)[lastDrawableDisplayed height]; // int rowBytes = width * 4; // int selfturesize = width * height * 4; // // void *p = malloc(selfturesize); // // // // [lastDrawableDisplayed getBytes:p bytesPerRow:rowBytes fromRegion:MTLRegionMake2D(0, 0, width, height) mipmapLevel:0]; // // CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); // CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Little | kCGImageAlphaFirst; // // CGDataProviderRef provider = CGDataProviderCreateWithData(nil, p, selfturesize, nil); // CGImageRef cgImageRef = CGImageCreate(width, height, 8, 32, rowBytes, colorSpace, bitmapInfo, provider, nil, true, (CGColorRenderingIntent)kCGRenderingIntentDefault); // // UIImage *getImage = [UIImage imageWithCGImage:cgImageRef]; // CGImageRelease(cgImageRef); // // CGDataProviderRelease(provider); // CGColorSpaceRelease(colorSpace); // free(p); // return getImage; // // NSData *pngData = UIImagePNGRepresentation(getImage); // UIImage *pngImage = [UIImage imageWithData:pngData]; } - (void)updateStrokeColor { CGFloat red = 0.0, green = 0.0, blue = 0.0, alpha = 0.0, white = 0.0; if ( self.strokeColor && [self.strokeColor getRed:&red green:&green blue:&blue alpha:&alpha]) { StrokeColor[0]=red; StrokeColor[1]=green; StrokeColor[2]=blue; StrokeColor[3]=alpha; } else if (self.strokeColor && [self.strokeColor getWhite:&white alpha:&alpha]) { StrokeColor[0]=white; StrokeColor[1]=white; StrokeColor[2]=white; StrokeColor[3]=alpha; } else { StrokeColor[0]=0; StrokeColor[1]=0; StrokeColor[2]=0; StrokeColor[3]=1; } // StrokeColor } - (void)setBackgroundColor:(UIColor *)backgroundColor { [super setBackgroundColor:backgroundColor]; CGFloat red, green, blue, alpha, white; if ([backgroundColor getRed:&red green:&green blue:&blue alpha:&alpha]) { clearColor[0] = red; clearColor[1] = green; clearColor[2] = blue; } else if ([backgroundColor getWhite:&white alpha:&alpha]) { clearColor[0] = white; clearColor[1] = white; clearColor[2] = white; } } - (void)setStrokeColor:(UIColor *)strokeColor { _strokeColor = strokeColor; [self updateStrokeColor]; } - (void)updateRectWithPoint:(CGPoint)p { if (p.x<_mixPoint.x) { _mixPoint.x =p.x; } if (p.y<_mixPoint.y) { _mixPoint.y =p.y; } if (p.x>_maxPoint.x) { _maxPoint.x =p.x; } if (p.y>_maxPoint.y) { _maxPoint.y =p.y; } } /* // Only override drawRect: if you perform custom drawing. // An empty implementation adversely affects performance during animation. - (void)drawRect:(CGRect)rect { // Drawing code } */ #pragma mark - Gesture Recognizers - (void)tap:(UITapGestureRecognizer *)t { DebugLog(@"on tap"); CGPoint l = [t locationInView:self]; // l=CGPointMake(self.bounds.size.width/2, 0); [self updateRectWithPoint:l]; if (t.state == UIGestureRecognizerStateRecognized) { self.isSigned = YES; DebugLog(@"UIGestureRecognizerStateRecognized"); // glBindBuffer(GL_ARRAY_BUFFER, dotsBuffer); // // [dotsLength addObject:@0]; PPSSignaturePoint touchPoint = ViewPointToMTK(l, self.bounds.size, self.drawableSize, StrokeColor); addVertex(&vertexTotal,vertexLength, touchPoint,SignatureVertexData); // PPSSignaturePoint centerPoint = touchPoint; centerPoint.color =StrokeColor; addVertex(&vertexTotal,vertexLength, centerPoint,SignatureVertexData); // static int segments = 20; vector_float2 radius = (vector_float2){ clamp(1, 8, penThickness * generateRandom(2, 12)), clamp(1, 8, penThickness * generateRandom(2, 12)) }; vector_float2 velocityRadius = radius; float angle = 0; for (int i = 0; i <= segments; i++) { PPSSignaturePoint p = centerPoint; p.position.x += velocityRadius.x * cosf(angle); p.position.y += velocityRadius.y * sinf(angle); addVertex(&vertexTotal,vertexLength, p,SignatureVertexData); addVertex(&vertexTotal,vertexLength, centerPoint,SignatureVertexData); angle += M_PI * 2.0 / segments; } addVertex(&vertexTotal,vertexLength, touchPoint,SignatureVertexData); // // glBindBuffer(GL_ARRAY_BUFFER, 0); } [self setNeedsDisplay]; } //- (void)longPress:(UILongPressGestureRecognizer *)lp { // //注释regturn,可以打开长按清除 // return; // //// [self signatureImage]; // [self clear]; //} - (void)pan:(UIPanGestureRecognizer *)p { // glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); // CGPoint v = [p velocityInView:self]; CGPoint l = [p locationInView:self]; [self updateRectWithPoint:l]; currentVelocity = ViewPointToMTK(v, self.bounds.size, self.drawableSize,StrokeColor); float distance = 0.; if (previousPoint.x > 0) { distance = sqrtf((l.x - previousPoint.x) * (l.x - previousPoint.x) + (l.y - previousPoint.y) * (l.y - previousPoint.y)); } float velocityMagnitude = sqrtf(v.x*v.x + v.y*v.y); float clampedVelocityMagnitude = clamp(VELOCITY_CLAMP_MIN, VELOCITY_CLAMP_MAX, velocityMagnitude); float normalizedVelocity = (clampedVelocityMagnitude - VELOCITY_CLAMP_MIN) / (VELOCITY_CLAMP_MAX - VELOCITY_CLAMP_MIN); float lowPassFilterAlpha = STROKE_WIDTH_SMOOTHING; float newThickness = (self.MaxLineWidth - self.MinLineWidth) * (1 - normalizedVelocity) + self.MinLineWidth; penThickness = penThickness * lowPassFilterAlpha + newThickness * (1 - lowPassFilterAlpha); if ([p state] == UIGestureRecognizerStateBegan) { previousPoint = l; previousMidPoint = l; PPSSignaturePoint startPoint = ViewPointToMTK(l, self.bounds.size, self.drawableSize,StrokeColor); previousVertex = startPoint; previousThickness = penThickness; addVertex(&vertexTotal,vertexLength, startPoint,SignatureVertexData); addVertex(&vertexTotal,vertexLength, previousVertex,SignatureVertexData); self.isSigned = YES; } else if ([p state] == UIGestureRecognizerStateChanged) { CGPoint mid = CGPointMake((l.x + previousPoint.x) / 2.0, (l.y + previousPoint.y) / 2.0); if (distance > QUADRATIC_DISTANCE_TOLERANCE) { // Plot quadratic bezier instead of line unsigned int i; int segments = (int) distance / 1.5; float startPenThickness = previousThickness; float endPenThickness = penThickness; previousThickness = penThickness; for (i = 0; i < segments; i++) { penThickness = startPenThickness + ((endPenThickness - startPenThickness) / segments) * i; CGPoint quadPoint = QuadraticPointInCurve(previousMidPoint, mid, previousPoint, (float)i / (float)(segments)); PPSSignaturePoint v = ViewPointToMTK(quadPoint, self.bounds.size,self.drawableSize, StrokeColor); [self addTriangleStripPointsForPrevious:previousVertex next:v]; previousVertex = v; } } else if (distance > 1.0) { PPSSignaturePoint v = ViewPointToMTK(l, self.bounds.size,self.drawableSize, StrokeColor); [self addTriangleStripPointsForPrevious:previousVertex next:v]; previousVertex = v; previousThickness = penThickness; } previousPoint = l; previousMidPoint = mid; } else if (p.state == UIGestureRecognizerStateEnded | p.state == UIGestureRecognizerStateCancelled) { PPSSignaturePoint v = ViewPointToMTK(l, self.bounds.size, self.drawableSize,StrokeColor); addVertex(&vertexTotal,vertexLength, v,SignatureVertexData); previousVertex = v; addVertex(&vertexTotal,vertexLength, previousVertex,SignatureVertexData); } [self setNeedsDisplay]; } - (void)addTriangleStripPointsForPrevious:(PPSSignaturePoint)previous next:(PPSSignaturePoint)next { float toTravel = penThickness / 2.0; for (int i = 0; i < 2; i++) { vector_float2 p = perpendicular(previous, next); vector_float2 p1 = next.position; vector_float2 ref = p1+p; float distance = simd_distance(p1,ref); float difX = p1.x - ref.x; float difY = p1.y - ref.y; float ratio = -1.0 * (toTravel / distance); difX = difX * ratio; difY = difY * ratio; PPSSignaturePoint stripPoint = { { p1.x + difX, p1.y + difY}, StrokeColor }; addVertex(&vertexTotal,vertexLength, stripPoint,SignatureVertexData); toTravel *= -1; } } @end