1/************************************************************************
2 ************************************************************************
3 FAUST Architecture File
4 Copyright (C) 2003-2012 GRAME, Centre National de Creation Musicale
5 ---------------------------------------------------------------------
6
7 This is sample code. This file is provided as an example of minimal
8 FAUST architecture file. Redistribution and use in source and binary
9 forms, with or without modification, in part or in full are permitted.
10 In particular you can create a derived work of this FAUST architecture
11 and distribute that work under terms of your choice.
12
13 This sample code is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
16 ************************************************************************
17 ************************************************************************/
18
19/************************************************************************
20 ************************************************************************
21 Based on DCControls - https://github.com/domesticcatsoftware/DCControls
22 Copyright (C) 2011 by Patrick Richards - http://domesticcat.com.au/
23
24 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
30 THE SOFTWARE.
31 ************************************************************************
32 ************************************************************************/
33
34#import "FIKnob.h"
35
36#define kStdKnobHintSpace         10
37
38@implementation FIKnob
39@synthesize biDirectional, arcStartAngle, cutoutSize, valueArcWidth;
40@synthesize doubleTapValue, tripleTapValue;
41
42#pragma mark -
43#pragma mark Init
44
45- (id)initWithDelegate:(id)aDelegate
46{
47	if ((self = [super initWithDelegate:aDelegate]))
48	{
49        _hint = nil;
50		self.arcStartAngle = 90.0;
51		self.cutoutSize = 60.0;
52
53		// add the gesture recognizers for double & triple taps
54		UITapGestureRecognizer *doubleTapGesture = [[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doubleTap:)] autorelease];
55		doubleTapGesture.numberOfTapsRequired = 2;
56		[self addGestureRecognizer:doubleTapGesture];
57
58		UITapGestureRecognizer *tripleTapGesture = [[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tripleTap:)] autorelease];
59		tripleTapGesture.numberOfTapsRequired = 3;
60		[self addGestureRecognizer:tripleTapGesture];
61	}
62
63	return self;
64}
65
66- (void)dealloc
67{
68    if (_hint) [_hint dealloc];
69    [super dealloc];
70}
71
72// overridden to make sure the frame is always square.
73- (void)setFrame:(CGRect)frame
74{
75	if (frame.size.width != frame.size.height)
76	{
77		if (frame.size.width > frame.size.height)
78			frame = CGRectMake(frame.origin.x, frame.origin.y, frame.size.width, frame.size.width);
79		else
80			frame = CGRectMake(frame.origin.x, frame.origin.y, frame.size.height, frame.size.height);
81	}
82
83	[super setFrame:frame];
84}
85
86#pragma mark -
87#pragma mark Gestures
88
89- (void)doubleTap:(UIGestureRecognizer *)gestureRecognizer
90{
91	if (self.allowsGestures)
92	{
93        CGPoint thisPoint = [gestureRecognizer locationInView:self];
94        CGFloat newValue = [self valueFromPoint:thisPoint];
95
96        if (newValue > self.value)
97        {
98            self.value = self.value + self.step;
99        }
100        else if (newValue < self.value)
101        {
102            self.value = self.value - self.step;
103        }
104	}
105}
106
107- (void)tripleTap:(UIGestureRecognizer *)gestureRecognizer
108{
109}
110
111- (void)setValueFromGesture:(NSNumber *)newValue
112{
113	self.value = [newValue floatValue];
114}
115
116#pragma mark -
117#pragma mark Touch Handling
118
119- (void)updateHint
120{
121    if (_hint)
122    {
123        UIView*     scrollView = self.superview.superview.superview;
124        CGRect      absHandleRect = [self convertRect:CGRectMake(0.f, 0.f, self.frame.size.width, self.frame.size.height)
125                                               toView:scrollView];
126
127        _hint.title = [NSString stringWithFormat:@"%2.1f%@", self.value, self.suffixe];
128
129        // Top
130        if (absHandleRect.origin.y >= _hint.frame.size.height + kStdKnobHintSpace
131            && absHandleRect.origin.x + (absHandleRect.size.width - _hint.frame.size.width) / 2.f + _hint.frame.size.width <= scrollView.frame.size.width
132            && absHandleRect.origin.x + (absHandleRect.size.width - _hint.frame.size.width) / 2.f >= 0.f)
133        {
134            _hint.position = 0;
135            [_hint setFrame:CGRectMake(absHandleRect.origin.x + (absHandleRect.size.width - _hint.frame.size.width) / 2.f,
136                                       absHandleRect.origin.y - _hint.frame.size.height - kStdKnobHintSpace,
137                                       _hint.frame.size.width,
138                                       _hint.frame.size.height)];
139        }
140
141        // Left
142        else if (absHandleRect.origin.x >= _hint.frame.size.width + kStdKnobHintSpace)
143        {
144            _hint.position = 2;
145            [_hint setFrame:CGRectMake(absHandleRect.origin.x - _hint.frame.size.width - kStdKnobHintSpace,
146                                       absHandleRect.origin.y,
147                                       _hint.frame.size.width,
148                                       _hint.frame.size.height)];
149        }
150
151        // Right
152        else if (scrollView.frame.size.width - absHandleRect.origin.x - absHandleRect.size.width >= _hint.frame.size.width + kStdKnobHintSpace)
153        {
154            _hint.position = 3;
155            [_hint setFrame:CGRectMake(absHandleRect.origin.x + absHandleRect.size.width + kStdKnobHintSpace,
156                                       absHandleRect.origin.y,
157                                       _hint.frame.size.width,
158                                       _hint.frame.size.height)];
159        }
160
161        // Bottom
162        else
163        {
164            _hint.position = 1;
165            [_hint setFrame:CGRectMake(absHandleRect.origin.x + (absHandleRect.size.width - _hint.frame.size.width) / 2.f,
166                                       absHandleRect.origin.y + absHandleRect.size.height + kStdKnobHintSpace,
167                                       _hint.frame.size.width,
168                                       _hint.frame.size.height)];
169        }
170
171        [_hint setNeedsDisplay];
172    }
173}
174
175- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
176{
177    self.motionBlocked = YES;
178
179	CGPoint thisPoint = [[touches anyObject] locationInView:self];
180	CGPoint centerPoint = CGPointMake(self.frame.size.width / 2.0, self.frame.size.width / 2.0);
181	initialAngle = angleBetweenPoints(thisPoint, centerPoint);
182
183	// create the initial angle and initial transform
184	initialTransform = [self initialTransform];
185
186    if (!_hint)
187    {
188        _hint = [[FIHint alloc] init];
189        [self.superview.superview.superview addSubview:_hint];
190    }
191
192    if (_hint)
193    {
194        _hint.title = [NSString stringWithFormat:@"%2.1f%@", self.value, self.suffixe];
195        [self updateHint];
196    }
197}
198
199- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
200{
201    self.motionBlocked = YES;
202
203	CGPoint thisPoint = [[touches anyObject] locationInView:self];
204	CGPoint centerPoint = CGPointMake(self.frame.size.width / 2.0, self.frame.size.width / 2.0);
205
206	CGFloat currentAngle = angleBetweenPoints(thisPoint, centerPoint);
207	CGFloat angleDiff = (initialAngle - currentAngle);
208	CGAffineTransform newTransform = CGAffineTransformRotate(initialTransform, angleDiff);
209
210	CGFloat newValue = [self newValueFromTransform:newTransform];
211
212	// only set the new value if it doesn't flip the knob around
213	CGFloat diff = self.value - newValue;
214	diff = (diff < 0) ? -diff : diff;
215	if (diff < (self.max - self.min) / 10.0)
216	{
217		self.value = newValue;
218	}
219	else
220	{
221		// reset the initial angle & transform using the current value
222		initialTransform = [self initialTransform];
223		initialAngle = angleBetweenPoints(thisPoint, centerPoint);
224	}
225
226    [self updateHint];
227}
228
229- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
230{
231    self.motionBlocked = NO;
232
233    if (_hint)
234    {
235        [_hint removeFromSuperview];
236        [_hint release];
237        _hint = nil;
238    }
239}
240
241- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
242{
243    self.motionBlocked = NO;
244
245    if (_hint)
246    {
247        [_hint removeFromSuperview];
248        [_hint release];
249        _hint = nil;
250    }
251}
252
253#pragma mark -
254#pragma mark Helper Methods
255
256- (CGFloat)valueFromPoint:(CGPoint)point
257{
258    CGPoint centerPoint = CGPointMake(self.frame.size.width / 2.0, self.frame.size.width / 2.0);
259    CGFloat currentAngle = 450.f + angleBetweenPoints(point, centerPoint) * 57.29577951308232;
260    int normAngle = 360 - ((int)floor(currentAngle) % 360);
261	CGFloat newValue;
262    float course = 360.f - self.cutoutSize;
263    float courseOffset = self.cutoutSize / 2.f;
264
265    newValue = normAngle - courseOffset;
266    if (newValue < 0.f) newValue = 0.f;
267    else if (newValue > course) newValue = course;
268
269    newValue = newValue / course;
270    newValue = self.min + newValue * (self.max - self.min);
271
272	return newValue;
273}
274
275
276- (CGAffineTransform)initialTransform
277{
278	CGFloat pValue = (self.value - self.min) / (self.max - self.min);
279	pValue = (pValue * kDCKnobRatio * 2) - kDCKnobRatio;
280	return CGAffineTransformMakeRotation(pValue);
281}
282
283- (CGFloat)newValueFromTransform:(CGAffineTransform)transform
284{
285	CGFloat newValue = atan2(transform.b, transform.a);
286	newValue = (newValue + kDCKnobRatio) / (kDCKnobRatio * 2);
287	newValue = self.min + (newValue * (self.max - self.min));
288	return newValue;
289}
290
291#pragma mark -
292#pragma mark Drawing
293
294- (void)drawRect:(CGRect)rect
295{
296    if (self.hideOnGUI) return;
297
298	CGContextRef context = UIGraphicsGetCurrentContext();
299	CGRect boundsRect = self.bounds;
300	CGFloat maxHalf = self.min + (self.max - self.min) / 2;
301	float x = boundsRect.size.width / 2;
302	float y = boundsRect.size.height / 2;
303
304	CGContextSaveGState(context);
305	CGContextSetLineWidth(context, self.valueArcWidth);
306
307    self.backgroundColor = [UIColor blackColor];
308	if (self.backgroundColorAlpha > 0.02)
309	{
310		// outline semi circle
311		UIColor *backgroundColor = [UIColor colorWithRed:0.3 green:0.3 blue:0.3 alpha:1.];
312		[backgroundColor set];
313
314		CGContextAddArc(context,
315						x,
316						y,
317						(boundsRect.size.width / 2) - self.valueArcWidth / 2,
318						FIDegreesToRadians(self.arcStartAngle + self.cutoutSize / 2),
319						FIDegreesToRadians(self.arcStartAngle + 360 - self.cutoutSize / 2),
320						0);
321		CGContextStrokePath(context);
322	}
323
324    // Gradient
325    context = UIGraphicsGetCurrentContext();
326
327    UIColor *lightGradientColor = [UIColor colorWithRed:0.7 green:0.7 blue:0.7 alpha:1.];
328    UIColor *darkGradientColor = [UIColor colorWithRed:0. green:0. blue:0. alpha:1.];
329
330    CGFloat locations[2] = {0.0, 1.0};
331    CFArrayRef colors = (CFArrayRef) [NSArray arrayWithObjects:(id)lightGradientColor.CGColor,
332                                      (id)darkGradientColor.CGColor,
333                                      nil];
334
335    CGColorSpaceRef colorSpc = CGColorSpaceCreateDeviceRGB();
336    CGGradientRef gradient = CGGradientCreateWithColors(colorSpc, colors, locations);
337
338    CGContextSetBlendMode(context, kCGBlendModeMultiply);
339
340    CGContextDrawLinearGradient(context,
341                                gradient,
342                                CGPointMake(0.0, 0.0),
343                                CGPointMake(rect.size.width, rect.size.height),
344                                kCGGradientDrawsAfterEndLocation); //Adjust second point according to your view height
345
346    CGColorSpaceRelease(colorSpc);
347    CGGradientRelease(gradient);
348    CGContextSetBlendMode(context, kCGBlendModeNormal);
349    // End gradient
350
351	// draw the value semi circle
352	[self.color set];
353	CGFloat valueAdjusted = (self.value - self.min) / (self.max - self.min);
354	if (self.biDirectional)
355	{
356		CGContextAddArc(context,
357						x,
358						y,
359						(boundsRect.size.width / 2) - self.valueArcWidth / 2,
360						FIDegreesToRadians(self.arcStartAngle + 180),
361						FIDegreesToRadians(self.arcStartAngle + self.cutoutSize / 2 + (360 - self.cutoutSize) * valueAdjusted),
362						self.value <= maxHalf);
363	}
364	else
365	{
366		CGContextAddArc(context,
367						x,
368						y,
369						(boundsRect.size.width / 2) - self.valueArcWidth / 2,
370						FIDegreesToRadians(self.arcStartAngle + self.cutoutSize / 2),
371						FIDegreesToRadians(self.arcStartAngle + self.cutoutSize / 2 + (360 - self.cutoutSize) * valueAdjusted),
372						0);
373	}
374	CGContextStrokePath(context);
375
376	// draw the value string as needed
377	if (self.displaysValue)
378	{
379		if (self.labelColor)
380			[self.labelColor set];
381		else
382			[self.color set];
383		NSString *valueString = nil;
384        float multiplier = 1.f;
385
386        if ([self.suffixe compare:@""] == NSOrderedSame)
387        {
388            if (self.step < 0.01) valueString = [NSString stringWithFormat:@"%2.3f", self.value];
389            else if (self.step < 0.1) valueString = [NSString stringWithFormat:@"%2.2f", self.value];
390            else valueString = [NSString stringWithFormat:@"%2.1f", self.value];
391        }
392        else
393        {
394            if (self.step < 0.01) valueString = [NSString stringWithFormat:@"%2.3f\r%@", self.value, self.suffixe];
395            else if (self.step < 0.1) valueString = [NSString stringWithFormat:@"%2.2f\r%@", self.value, self.suffixe];
396            else valueString = [NSString stringWithFormat:@"%2.1f\r%@", self.value, self.suffixe];
397            multiplier = 2.f;
398        }
399		CGSize valueStringSize = [valueString sizeWithFont:self.labelFont
400												  forWidth:boundsRect.size.width
401											 lineBreakMode:NSLineBreakByTruncatingTail];
402		[valueString drawInRect:CGRectMake(floorf((boundsRect.size.width - valueStringSize.width) / 2.0 + self.labelOffset.x),
403										   floorf((boundsRect.size.height - valueStringSize.height) / 2.0 + self.labelOffset.y),
404										   valueStringSize.width,
405										   multiplier * valueStringSize.height)
406					   withFont:self.labelFont
407				  lineBreakMode:NSLineBreakByTruncatingTail
408                      alignment:NSTextAlignmentCenter];
409	}
410
411    // Draw assignation
412    if (self.assignated)
413    {
414        CGContextSetLineWidth(context, 3.);
415        [self.color set];
416        [self context:context addRoundedRect:boundsRect cornerRadius:3.f];
417        CGContextStrokePath(context);
418    }
419
420    // Draw selection
421    if (self.responderSelected)
422    {
423        CGContextSetLineWidth(context, 15.);
424        [self.color set];
425        [self context:context addRoundedRect:boundsRect cornerRadius:3.f];
426        CGContextStrokePath(context);
427    }
428
429	CGContextRestoreGState(context);
430}
431
432@end
433