談談貝塞爾曲線

談談貝塞爾曲線

最近在做項目的時候,需要用到一個動畫,非常簡單的動畫,簡單到就是直接對一個View做平移… 然而雖然動畫簡單,但是卻很不自然,嘗試了UIView Animation提供的各類參數,都無法達到想要的動畫效果。這時候,我的腦子里突然想起一個詞… “貝塞爾曲線”…. 這個詞經常看到,但卻從沒有去了解過,這次就趁著有求于它的雅興,好好做個入門了解好了。

首先,什么是貝塞爾曲線?

顯而易見的是,貝塞爾曲線,應該就是是一個叫貝塞爾的人發明的曲線吧,然而歷史劇本卻不是這么寫的。貝塞爾曲線所依據的最原始的數學公式,是早在1912年就廣為人知的伯恩斯坦多項式。OK,now,What is boensitan duoxiangshi?!簡單來說,伯恩斯坦多項式可以用來證明,在[ a, b ] 區間上所有的連續函數都可以用多項式來逼近,并且收斂性很強,也就是一致收斂。再簡單點,就是一個連續函數,你可以將它寫成若干個伯恩斯坦多項式相加的形式,并且,隨著 n→∞,這個多項式將一致收斂到原函數,這個就是伯恩斯坦斯的逼近性質。

不知道在說什么鬼?沒關系,接著說..

到了1959年,當時就職于雪鐵龍的法國數學家 Paul de Casteljau 開始對伯恩斯坦多項式進行了圖形化的嘗試,并且提供了一種數值穩定的德卡斯特里奧(de Casteljau) 算法。根據這個算法,就可以只通過很少的控制點,去生成復雜的平滑曲線,也就是貝塞爾曲線。

而貝塞爾曲線的得名,得歸功于1962年就職于雷諾的法國工程師皮埃爾·貝塞爾(Pierre Bézier),他使用這種方法來輔助汽車的車體工業設計,并且廣泛宣傳,因此大家才都稱他為貝塞爾曲線 。

貝塞爾曲線是怎么畫出來的?

首先,我們在平面內選3個不同線的點并且依次用線段連接。如下所示..

接著,我們在AB和BC線段上找出點D和點E,使得AD/AB = BE/BC。

再接著,連接DE,并在DE上找出一點F,使得DF/DE = AD/AB = BE/BC。

然后,根據我們高中所學的極限的知識,讓選取的點D在第一條線段上從起點A,移動到終點B,找出所有點F,并將它們連起來。最后你會發現,你得到了一條非常光滑的曲線,這條就是傳說中的,貝塞爾曲線。

看這里…

這是二階貝塞爾曲線。

下面是三階四階和五階。

最后是… 一階….

所以貝塞爾曲線的厲害之處就在這里,從1-n階的連續函數,他都可以計算得到一條光滑曲線。

那么,貝塞爾曲線有什么用?為什么經常會聽到這個名稱?

由于貝塞爾曲線控制簡便,而且它具有很強的描述能力,因此它在工業設計上已經被廣泛使用了。不僅如此,在計算機圖形學領域(特別是矢量圖形學),貝塞爾曲線也有著舉足輕重的地位。而作為程序猿,我們經常會用貝塞爾曲線來繪圖(由貝塞爾曲線畫出來的圖很光滑~),來做動畫(很自然的動畫)等等。也就是由于它可以發揮的作用領域太廣了,因此我們時不時都會聽到這個名字。

好的,那我們要如何用貝塞爾曲線?

首先,要明確的一點是,對于貝塞爾曲線來說,最重要的點是,數據點和控制點。

數據點: 指一條路徑的起始點和終止點。

控制點:控制點決定了一條路徑的彎曲軌跡,根據控制點的個數,貝塞爾曲線被分為一階貝塞爾曲線(0個控制點)、二階貝塞爾曲線(1個控制點)、三階貝塞爾曲線(2個控制點)等等。

而系統給我們提供了一個叫做UIBezierPath類,用它可以畫簡單的圓形,橢圓,矩形,圓角矩形,也可以通過添加點去生成任意的圖形,還可以簡單的創建一條二階貝塞爾曲線和三階貝塞爾曲線。

用法1:簡單地畫圖形

這里的簡單用法就不細講,雖然類名叫UIBezierPath,但畫圓形啥的跟貝塞爾也沒啥關系,直接貼代碼。

畫圓形

UIBezierPath *bPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(300, 300) radius:50

startAngle: DEGREES_TO_RADIANS(135) endAngle:M_PI*2 clockwise:YES];

[bPath setLineWidth:5];

//繪制

[bPath stroke];

畫橢圓

UIBezierPath *ovalPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(200, 150, 100, 200)];

[ovalPath setLineWidth:5];

[ovalPath stroke];

畫矩形

UIBezierPath *myBezierPath = [UIBezierPath bezierPathWithRect:CGRectMake(20, 20, 100, 50)];

[[UIColor blackColor]setStroke];

[myBezierPath setLineWidth:5];

[myBezierPath stroke];

畫圓角矩形

//UIRectCorner可以設置 哪幾個角是圓角,其他不變

UIBezierPath *tBPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(220, 20, 100, 100)

byRoundingCorners:UIRectCornerTopLeft | UIRectCornerBottomLeft cornerRadii:CGSizeMake(20, 20)];

[[UIColor greenColor]setStroke];

[tBPath setLineWidth:5];

[tBPath stroke];

通過任意點畫任意圖形

UIBezierPath* aPath = [UIBezierPath bezierPath];

aPath.lineWidth = 15.0;

aPath.lineCapStyle = kCGLineCapButt;? //線條終點

//round 圓形

//butt 平的 默認值 把線連接到精準的終點

//Square 平的,會把線延伸到終點再加上線寬的一半

aPath.lineJoinStyle = kCGLineJoinBevel;? //拐點處理

//bevel 斜角斜面,角的外側是平的不圓滑

//miter 斜接 角的外側是尖的

//round 圓角

//這是起點

[aPath moveToPoint:CGPointMake(100.0, 200.0)];

//添加點

[aPath addLineToPoint:CGPointMake(200.0, 240.0)];

[aPath addLineToPoint:CGPointMake(160, 340)];

[aPath addLineToPoint:CGPointMake(40.0, 340)];

[aPath addLineToPoint:CGPointMake(10.0, 240.0)];

[aPath closePath]; //第五條線通過調用closePath方法得到的

[aPath stroke]; //Draws line 根據坐標點連線

畫二階貝塞爾

UIBezierPath* twoPath = [UIBezierPath bezierPath];

twoPath.lineWidth = 5.0;//寬度

twoPath.lineCapStyle = kCGLineCapRound;? //線條拐角

twoPath.lineJoinStyle = kCGLineJoinRound;? //終點處理

//起始點

[twoPath moveToPoint:CGPointMake(20, 100)];

//添加兩個控制點

[twoPath addQuadCurveToPoint:CGPointMake(220, 100) controlPoint:CGPointMake(170, 0)];

//劃線

[twoPath stroke];

畫三階貝塞爾

UIBezierPath* bPath = [UIBezierPath bezierPath];

bPath.lineWidth = 5.0;

bPath.lineCapStyle = kCGLineCapRound;? //線條拐角

bPath.lineJoinStyle = kCGLineCapRound;? //終點處理

//起始點

[bPath moveToPoint:CGPointMake(20, 250)];

//添加兩個控制點

[bPath addCurveToPoint:CGPointMake(350, 250) controlPoint1:CGPointMake(310, 200) controlPoint2:CGPointMake(210, 400)];

[bPath stroke];

用法2:用貝塞爾曲線圓滑繪圖

這個用法可以說是處女座的福音。

假設這么一個場景:產品提了個需求,來吧,咱們來做一個你畫我猜的APP。你畫我猜?肯定是要先有畫了。簡單!新建個UIView的子類,然后在它的初始化方法中創建Path和手勢。

// Create a path to connect lines

path = [UIBezierPath bezierPath];

// Capture touches

UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];

pan.maximumNumberOfTouches = pan.minimumNumberOfTouches = 1;

[self addGestureRecognizer:pan];

再將捕獲到的pan事件location數據依次加入到path中,并且用直線連接兩點。

- (void)pan:(UIPanGestureRecognizer *)pan {

? ? CGPoint currentPoint = [pan locationInView:self];

? ? if (pan.state == UIGestureRecognizerStateBegan) {

? ? ? ? [path moveToPoint:currentPoint];

? ? } else if (pan.state == UIGestureRecognizerStateChanged) {

? ? ? ? [path addLineToPoint:currentPoint];

? ? }

? ? [self setNeedsDisplay];

}

最后畫出軌跡。

- (void)drawRect:(CGRect)rect {

? ? [[UIColor blackColor] setStroke];

? ? [path stroke];

}

最后將這個view添加到控制器上,很開心的Command + R,讓程序跑起來。

開始畫~

然后你就會發現,畫出來的曲線是這樣的。。

WHAT THE FXXK!!

怎么可以有這么多鋸齒。。

所以這個時候,貝塞爾曲線就很有用了。它的定義是可以找到兩點之間的光滑曲線,因為我們之前手勢移動的時候,兩點之間都是使用直線連接,如果我們可以使用貝塞爾曲線連接,那應該就不會出現這個問題了。

試一下。

首先寫一個計算中點的方法,我們到時會使用這個中點作為控制點。

static CGPoint midpoint(CGPoint p0, CGPoint p1) {

? ? return (CGPoint) {

? ? ? ? (p0.x + p1.x) / 2.0,

? ? ? ? (p0.y + p1.y) / 2.0

? ?};

}

最后將手勢處理中的連接方式替換成使用貝塞爾曲線。

復制代碼

- (void)pan:(UIPanGestureRecognizer *)pan {

? ? CGPoint currentPoint = [pan locationInView:self];

? ? CGPoint midPoint = midpoint(previousPoint, currentPoint);

? ? if (pan.state == UIGestureRecognizerStateBegan) {

? ? ? ? [path moveToPoint:currentPoint];

? ? } else if (pan.state == UIGestureRecognizerStateChanged) {

? ? ? ?[path addQuadCurveToPoint:midPoint controlPoint:previousPoint];

? ? }

? ? previousPoint = currentPoint;

? ? [self setNeedsDisplay];

}

再Run一次…

看,光滑多了~

所以很多時候,當我們遇到畫出的圖形太不自然的時候,就可以試著用貝塞爾曲線解決這些問題,用到越高階的曲線,畫出的圖形越光滑。

用法3:用貝塞爾曲線做變形

網上看到的大多數比較酷炫的動畫,都是通過修改曲線的控制點,對曲線進行變形而做的。

比如,我們要實現如下一個動畫。

這個動畫最難地方就是手勢拖拽的時候,直線的變形,可以首先的想到的是使用貝塞爾。通過創建path,添加控制點畫出曲線,然后通過更改控制點的位置來達到讓曲線進行變形的目的。

如上圖所示,這里添加了7個點,從左到右依次為l3、l2、l1、c、 r1、 r2、 r3。屏幕最左和最右兩邊的l3和r3沒有在圖中顯示出來,然后我們就可以以l3和l2為控制點,從l3到l1建立一條二階貝塞爾曲線,再以c和r1為控制點建一條從l3到r1的曲線,最后以r1和r2為控制點建一條從r1到r3的曲線。 主要代碼如下:

- (CGPathRef)currentPath {

? ? CGFloat width = self.view.bounds.size.width;

? ? UIBezierPath *path = [UIBezierPath bezierPath];

? ? [path moveToPoint:CGPointMake(0, 0)];

? ? [path addLineToPoint:CGPointMake(0, self.l3ControlPointView.center.y)];

? ? [path addCurveToPoint:self.l1ControlPointView.center

? ? controlPoint1:self.l3ControlPointView.center

? ? controlPoint2:self.l2ControlPointView.center];

? ? [path addCurveToPoint:self.r1ControlPointView.center

? ? ? ? ? ? ? ? ? ? controlPoint1:self.cControlPointView.center?

? ? ? ? ? ? ? ? ? ?controlPoint2:self.r1ControlPointView.center];?

? ? [path addCurveToPoint:self.r3ControlPointView.center

? ? ? ? ? ? ? ? ? ? controlPoint1:self.r1ControlPointView.center

? ? ? ? ? ? ? ? ? ?controlPoint2:self.r2ControlPointView.center];

? ? [path addLineToPoint:CGPointMake(width, 0)];

? ? [path closePath];

? ? return path.CGPath;

}

建立好路徑之后,就可以通過手勢操作來修改控制點的坐標達到我們的目的了。

在這里也就是修改l3到r3的中心點坐標。主要代碼如下:

- (void)panDidMove:(UIPanGestureRecognizer *)gesture {

? ? if (gesture.state == UIGestureRecognizerStateEnded ||

? ? ? ? gesture.state == UIGestureRecognizerStateFailed ||

? ? ? ? gesture.state == UIGestureRecognizerStateCancelled) {

? ? } else {

? ? ? ? CGFloat additionalHeight = MAX([gesture translationInView:self.view].y, 0);

? ? ? ? CGFloat waveHeight = MIN(additionalHeight*0.6, kMaxWaveHeight);

? ? ? ? CGFloat baseHeight = kMiniHeight + additionalHeight - waveHeight;

? ? ? ? CGFloat locationX = [gesture locationInView:gesture.view].x;

? ? ? ? [self layoutControlPoints:baseHeight waveHeight:waveHeight locationX:locationX];

? ? ? ? [self updateShapeLayer];

? ? }

}

- (void)layoutControlPoints:(CGFloat)baseHeight

? ? ? ? ? ? ? ? ? ? ? ? ?waveHeight:(CGFloat)waveHeight

? ? ? ? ? ? ? ? ? ? ? ? ? ? ?locationX:(CGFloat)locationX {

? ? CGFloat width = self.view.bounds.size.width;

? ? CGFloat minLeftX = MIN(locationX-width/2*0.28, 0);

? ? CGFloat maxRightX = MAX(width+(locationX-width)/2 *0.28, width);

? ? CGFloat leftPartWidth = locationX - minLeftX;

? ? CGFloat rightPartWidth = maxRightX - locationX;

? ? ?self.l3ControlPointView.center = CGPointMake(minLeftX, baseHeight);

? ? self.l2ControlPointView.center = CGPointMake(minLeftX+leftPartWidth*0.44, baseHeight);

? ? self.l1ControlPointView.center = CGPointMake(minLeftX+leftPartWidth*0.71, baseHeight+waveHeight*0.64);

? ?self.cControlPointView.center = CGPointMake(locationX, baseHeight+waveHeight*1.36);

? ? self.r1ControlPointView.center = CGPointMake(maxRightX-rightPartWidth*0.71, ? baseHeight+waveHeight*0.64);

? ? self.r2ControlPointView.center = CGPointMake(maxRightX-(rightPartWidth*0.44), baseHeight);

? ? self.r3ControlPointView.center = CGPointMake(maxRightX, baseHeight);

}

- (void)updateShapeLayer {

? ? self.shapeLayer.path = [self currentPath];

}

通過這個思路,我們可以做出很多有意思而且有生命力的動畫,這里一般還會經常和 CADisplayLink 一起用,先留個坑。

用法4:用貝塞爾曲線做緩沖動畫

做動畫最主要的一點,就是要讓動畫達到很自然的效果。這就要涉及到一些現實中的物理知識,比如重力彈力和速度等等,所以有時候,我們需要對動畫的速度進行控制,有時候需要先快再慢,有時候需要先慢再快然后再慢,有時候又需要快慢超慢非常慢…

這個時候就不得不提到 CAMediaTimingFunction 。

CAMediaTimingFunction 的主要用法可以理解為我們在一個二維坐標系上建議一條或曲線或直線的函數,這個函數的斜率就是動畫的速度,斜率的改變量也就是導數則為加速度。理論上來說,這個坐標系上的任何曲線都可以用來當做加速動畫。然而CAMediaTimingFunction 只給我們提供了一個三次貝塞爾曲線的函數,它可以生成三次貝塞爾曲線所能生成的所有緩沖函數。

這里剛好可以介紹 一個 兩個好用的網站: http://www.roblaplaca.com/examples/bezierBuilder

這個網站可以做到可視化的修改兩個控制點,來達到生成一條三階貝塞爾曲線,并且還會給出兩個控制點的具體坐標,以及右邊還可以看到這條曲線產生的動畫會做怎樣的速度改變。也就是說,只要我們能拿到兩個控制點的坐標,就可以用來控制動畫了。

http://easings.net

這個網站提供了豐富的曲線類型可供選擇,圖表旁還有一個小動畫預覽,非常直觀。

比如下面這段代碼,就可以讓我們把相冊從4:3 切換到1:1 的時候,展示一個先快后慢的過渡效果,這個效果跟系統相機的還是蠻接近的。

CABasicAnimation *animation = [CABasicAnimation animation];

animation.keyPath = @"borderWidth";

animation.repeatCount = 1;

animation.duration = 0.4;

animation.removedOnCompletion = NO;

animation.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0 :1 :1 :1];

animation.fillMode = kCAFillModeForwards;

animation.fromValue = 0.f;

animation.toValue = 40.f;

[self.previewMask addAnimation:animation forKey:@"changeBorderWith"];

效果如下:

用法5:用貝塞爾曲線做擬合計算

貝塞爾曲線有個非常常用的動畫效果,叫MetaBall算法。什么是MetaBall?就是我們平時看到的QQ的小紅點消除啦~ 像下面這樣。

這個是怎么實現的?

矩形擬合

首先我們需要了解一下簡單的矩形擬合原理

如圖所示的兩個圓,我們通過給它添加一個矩形(綠色部分),矩形較短的兩邊分別頂住兩個圓各自的一條直徑上,然后通過改變矩形較長的兩邊的弧度(紅色部分),達到擬合的效果。

這種做法當兩個圓較小的時候,幾乎是沒有問題的。但是當圓稍微大點的時候,就會出現很明顯的相交區域,擬合效果非常不好。

所以這種簡單的矩形擬合在圓較大的時候是很不嚴格的。這個時候就需要更嚴謹的切線擬合。

切線擬合

我們知道,之前的矩形擬合之所以才圓大的時候會出現擬合不嚴謹的情況。為什么?

正如上圖所示,兩條曲線的畫法都是由A1和B1為起點和終點,C點為控制點和A1、B2為起點和終點,C為控制點畫出的二階貝塞爾曲線。

而要做到完美的擬合,必須達到的一點要求就是,貝塞爾曲線與圓的連接點,也就是A1、B1、A2、B2,他們與控制點C的連線,一定要是圓的切線。這樣就不管圓大小怎么變,都不會出現明顯的相交區域了。

圖片引用: http://www.lxweimin.com/p/55c721887568

于是,現在解決問題的關鍵就轉變成了:如何計算這些擬合的關鍵點?

圖片引用: http://pandara.xyz/2015/10/27/ios_slime

我們現在要做的,就是求出點ABCDMN這六個點的坐標,就可以實現完美擬合了。

結合上面兩張圖,通過三角函數的各種計算,我們最終可以得到如下代碼:

- (void)reloadBezierPath {

CGFloat r1 = self.trailDot.frame.size.width / 2.0f;

CGFloat r2 = self.headDot.frame.size.width / 2.0f;

CGFloat x1 = self.trailDot.center.x;

CGFloat y1 = self.trailDot.center.y;

CGFloat x2 = self.headDot.center.x;

CGFloat y2 = self.headDot.center.y;

CGFloat distance = sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));

CGFloat sinDegree = (x2 - x1) / distance;

CGFloat cosDegree = (y2 - y1) / distance;

CGPoint pointA = CGPointMake(x1 - r1 * cosDegree, y1 + r1 * sinDegree);

CGPoint pointB = CGPointMake(x1 + r1 * cosDegree, y1 - r1 * sinDegree);

CGPoint pointC = CGPointMake(x2 + r2 * cosDegree, y2 - r2 * sinDegree);

CGPoint pointD = CGPointMake(x2 - r2 * cosDegree, y2 + r2 * sinDegree);

CGPoint pointN = CGPointMake(pointB.x + (distance / 2) * sinDegree, pointB.y + (distance / 2) * cosDegree);

CGPoint pointM = CGPointMake(pointA.x + (distance / 2) * sinDegree, pointA.y + (distance / 2) * cosDegree);

UIBezierPath *path = [UIBezierPath bezierPath];

[path moveToPoint:pointA];

[path addLineToPoint:pointB];

[path addQuadCurveToPoint:pointC controlPoint:pointN];

[path addLineToPoint:pointD];

[path addQuadCurveToPoint:pointA controlPoint:pointM];

self.shapeLayer.path = path.CGPath;

}

現在我們已經可以做到非常完美擬合的時候了,這時候再結合前面的通過修改控制點來實現圖形曲線變換,我們就可以做到類似QQ小紅點消除一樣的效果了,具體做法不再贅述。

Ending

至此,我們已基本了解了貝塞爾曲線的歷史出處公式性質及各種用法。在不斷學習的過程中,我發現一些比較牛逼的實現方法,都涉及到了較多較復雜的數學公式,奈何大學高數沒有好好學,導致需要回頭去看很多東西,這也是這篇博客耗費了較多時間的原因之一。不過在掌握了這些基礎和基本用法之后,就可以再去研究一下比較高級和酷炫的用法了,也留下了很多坑,會在以后慢慢填補的…

如果以后還想補的話….

文中如果有什么不足之處歡迎指正,這也是Share的目的之一。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,428評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,024評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,285評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,548評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,328評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,878評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,971評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,098評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,616評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,554評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,725評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,243評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,971評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,361評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,613評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,339評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,695評論 2 370

推薦閱讀更多精彩內容