この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
前回のブログで、タッチでお絵かきするサンプルを作りましたが、
曲線を引くとカクカクが目立ってしまうので、もう少し滑らかな線でお絵かきできるように修正してみようと思います。
滑らかな線を引く方法はいくつもあるようですが、
今回は、タッチした座標の中点を曲線でつないでいくという簡単な方法で描いてみます。
絵にするとこんな感じです。
青い線は前回のサンプルで使った描き方で、タッチした点を結んで直線を引いているだけなのでカクカクしています。
黒い線は今回対応したもので、線は少し滑らかになり、Retina 対応も行いジャギーも軽減されています。
実装
前準備として前回のブログを参考にして、タッチした点を結んだ直線でお絵かきできるところまで作っておきます。
今回修正するのは線を引いているところだけなので、ViewController.m ファイルを修正していきます。
まず最初に、タッチした座標を保持する変数と、タッチしたまま移動したかどうかのフラグ を作っておきます。
@interface ViewController ()
{
UIBezierPath *bezierPath;
UIImage *lastDrawImage;
NSMutableArray *undoStack;
NSMutableArray *redoStack;
CGPoint lastTouchPoint;
BOOL firstMovedFlg;
}
@end
次に「touchesBegan:withEvent:」メソッドで、移動フラグの初期化とタッチした座標を保持する処理を追加します。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
// タッチした座標を取得します。
CGPoint currentPoint = [[touches anyObject] locationInView:self.canvas];
// ボタン上の場合は処理を終了します。
if (CGRectContainsPoint(self.undoBtn.frame, currentPoint)
|| CGRectContainsPoint(self.redoBtn.frame, currentPoint)
|| CGRectContainsPoint(self.clearBtn.frame, currentPoint)){
return;
}
// パスを初期化します。
bezierPath = [UIBezierPath bezierPath];
bezierPath.lineCapStyle = kCGLineCapRound;
bezierPath.lineWidth = 4.0;
[bezierPath moveToPoint:currentPoint];
firstMovedFlg = NO;
// タッチした座標を保持します。
lastTouchPoint = currentPoint;
}
「touchesMoved:withEvent:」メソッドでは、中点の座標を取得して「addQuadCurveToPoint:controlPoint:」でパスを作成し曲線を描画します。
第1引数には中点の座標をセットし、第2引数のコントロールポイントには前回タッチした座標をセットします。
このメソッドでも処理の最後で、タッチした座標を保持しておきます。
それと、最初の移動のときに描画してしまうと、始点と最初の中点で直線が引かれてしまうので1回だけスキップしています。(下のソースのL11〜L16)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
// タッチ開始時にパスを初期化していない場合は処理を終了します。
if (bezierPath == nil){
return;
}
// タッチした座標を取得します。
CGPoint currentPoint = [[touches anyObject] locationInView:self.canvas];
// 最初の移動はスキップします。
if (!firstMovedFlg){
firstMovedFlg = YES;
lastTouchPoint = currentPoint;
return;
}
// 中点の座標を取得します。
CGPoint middlePoint = CGPointMake((lastTouchPoint.x + currentPoint.x) / 2,
(lastTouchPoint.y + currentPoint.y) / 2);
// パスにポイントを追加します。
[bezierPath addQuadCurveToPoint:middlePoint controlPoint:lastTouchPoint];
// 線を描画します。
[self drawLine:bezierPath];
// タッチした座標を保持します。
lastTouchPoint = currentPoint;
}
「touchesEnded:withEvent:」メソッドでも、「addQuadCurveToPoint:controlPoint:」を使い曲線を描画します。
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
// タッチ開始時にパスを初期化していない場合は処理を終了します。
if (bezierPath == nil){
return;
}
// タッチした座標を取得します。
CGPoint currentPoint = [[touches anyObject] locationInView:self.canvas];
// パスにポイントを追加します。
[bezierPath addQuadCurveToPoint:currentPoint controlPoint:lastTouchPoint];
// 線を描画します。
[self drawLine:bezierPath];
// 今回描画した画像を保持します。
lastDrawImage = self.canvas.image;
// undo用にパスを保持して、redoスタックをクリアします。
[undoStack addObject:bezierPath];
[redoStack removeAllObjects];
bezierPath = nil;
// ボタンのenabledを設定します。
self.undoBtn.enabled = YES;
self.redoBtn.enabled = NO;
}
最後に「drawLine:」メソッドで、Retina 対応をして、線のふちのジャギジャギを軽減します。
「UIGraphicsBeginImageContextWithOptions」 を使うと簡単にスケールを変更でき、Retinaディスプレイ に対応することができます。
ちなみに、第3引数にスケールの値をセットしますが、「0.0」を渡せばOKです。自動で適当なスケールを設定してくれるようです。
- (void)drawLine:(UIBezierPath*)path
{
// 非表示の描画領域を生成します。
UIGraphicsBeginImageContextWithOptions(self.canvas.frame.size, NO, 0.0);
// 描画領域に、前回までに描画した画像を、描画します。
[lastDrawImage drawAtPoint:CGPointZero];
// 色をセットします。
[[UIColor blackColor] setStroke];
// 線を引きます。
[path stroke];
// 描画した画像をcanvasにセットして、画面に表示します。
self.canvas.image = UIGraphicsGetImageFromCurrentImageContext();
// 描画を終了します。
UIGraphicsEndImageContext();
}
これで修正完了です。
動作確認
それでは試し描きをしてみます。
上手に描けましたー!?
が、、この絵だとなめらかな線で描けてるかどうかよくわかりませんので、時間のある方は実際に試してみてもらえたらと思います。。
ではでは。