在编程中发现数学之美——使用python和Processing绘制几何图形

在利用Processing绘出很酷的图形之前,你需要先学习Python编程语言的基础知识。本文假设你已经安装PythonProcessing,如果没有,欢迎你访问我在B站的视频课程,你可以在编程入门—使用python语言开发游戏课程中学习下载Python并安装以及Python的基础操作,可以在在python编程中发现数学之美中学习如何下载和安装Processing。本文是在Python编程中发现数学之美的第五章的部分内容,视频内容在这儿

这篇文章的最终目的是绘制一个如下的图形:

在几何课上,你学的所有东西都是关于空间里的形状和尺寸。一般来说你先学习一维的直线,然后学习二维的圆、正方形或三角形,然后学习三维的物体如立方体和球体。当今时代,利用很多先进的技术和免费的软件可以很容易地创建几何图形,但是要处理和改变你的图形,可能就有点挑战性了。

下面我们从简单的圆和三角形开始,学习怎样利用processing处理和改变图形。这些简单的图形是我们后面将要学到的分形和细胞繁殖的基础。你还将学习如何将复杂的物体分解成简单的部件。

画圆

我们先画一个简单的圆。在processing中打开新的绘图版,保存为文件名geometry,然后输入下面的代码:

def setup():
    size(600,600)

def draw():
    ellipse(200,100,20,20)

绘制圆之前,我们首先需要定义绘图窗口的尺寸,或者叫坐标平面。这个例子中我们使用size函数声明我们的绘图板将有600个像素宽600个像素高。

定义了坐标系统之后,我们就可以使用draw()中的ellipse函数绘制椭圆。前面两个参数200和100定义这个圆的圆心所在的位置。200是圆心的x坐标,100是y坐标。后面两个参数定义椭圆的宽度和高度,以像素为单位。这个例子中,这个形状是20个像素宽20个像素高,因为这两个参数是相等的,也就是说圆的边缘上的点离圆心的距离是相等的,所以这个椭圆是一个圆形。

单机run按钮,一个新的窗口弹出来,上面有我们绘制的圆。

现在你了解了在processing中如何绘制圆,为了创建动态的交互式的图形,我们还需要学习图形的位置和变换,让我们从位置开始。

使用坐标系为图形定位

上一节的代码中,ellipse函数的前两个参数指定了椭圆的圆心的位置。我们使用processing绘制的每一个图形,都需要指定它在坐标系统中的位置,一般是用两个点来表示:x和y。在传统的几何数学中,原点一般在图像的中心位置。

然而在计算机图形中,坐标系统与传统的几何系统不一样。计算机图形系统中的原点,在屏幕的左上角,x和y随着屏幕向右向下而增加。

上面屏幕中的每一个坐标,表达了屏幕上的每一个像素。你可能已经注意到了,在这样的坐标系统中,不需要处理负的坐标。我们将使用函数在上面这样的坐标系统中,逐渐地实现图形的转换和变换。

画一个单个的圆相对很容易,但是画多个圆可能就变得有点儿复杂,例如,我们需要设计下面的图形:

为每一个小圆确定位置,需要输入许多行相似的代码。幸运的是,你不需要精确的知道每一个圆的x和y坐标。在processing中,可以很容易的把坐标上的任何图形放到想放的地方。让我们从一个简单的例子开始。

转移函数

你可能还记得在几何课堂上使用纸和铅笔,如何费力的做几何图形的转换。在计算机中图形转换变得非常有趣而且容易。在processing中,可以很容易的移动或旋转一个三角形或类似的图形。

使用translate移动坐标系

几何中的图形移动,表示在坐标系中被移动的图形上的每个点都移动相同的方向、相同的距离。或者说图形的移动,就是改变图形的位置,但是图形的形状没有任何改变。

在数学课堂上移动物体,牵扯到重新计算图像中的每个点的坐标。但是在processing中,物体的移动,只需要移动坐标系本身,物体本身不会改变。让我们用下面的矩形作为例子。先修改上面讲的一段代码。

def setup():
    size(600,600)

def draw():
    rect(20,40,50,30)

这儿我们使用rect函数绘制了一个矩形。前两个参数告诉processing这个矩形左上角的坐标,第3和第4个参数指明长方形的宽度和高度。运行代码:

我们使用下面的代码,移动这个矩形。注意我们不会改变这个矩形的坐标。

def setup():
    size(600,600)

def draw():
    translate(50,80)
    rect(20,40,50,30)

这儿我们使用translate移动这个矩形。我们提供了两个参数,第1个参数告诉processing在水平方向也就是x方向移动的数量,第2个参数是在垂直方向也就是y方向上移动的数量。所以translate(50,80)将会向右50个像素向下80个像素移动整个坐标系,像下面图中显示的那样。

我们还是习惯坐标系的原点在屏幕的正中间,就像我们在数学课上学到的那样。你可以很容易地使用translate把坐标的原点移到屏幕的中间。你也可以使用它改变窗口画布的高度和宽度。让我们看看processing的内置的widthheight变量,这两个变量可以让你很容易的改变画布的宽度和高度。让我们来更新以前的代码:

def setup():
    size(600,600)

def draw():
    translate(width/2, height/2)
    rect(0,0,50,30)

你在size中声明的参数将成为画布的宽度和高度。在这个例子中,因为使用了size(600,600),它们都是600个像素。我们调用translate时,两个参数没有使用具体的数值而使用了变量width/2height/2,我们告诉processing将原点移动到当前窗口的中心,不管当前窗口的宽度和高度是多少。这也表示,如果你改变了窗口的尺寸,processing会自动的更新widthheight而不必手动修改。

运行代码你将会看到下面的图像:

注意原点仍然被标记为(0,0),实际上我们并没有移动原点,只是移动了整个坐标系,这样把原点移动到屏幕中央来了。

使用rotate旋转对象

在几何中旋转表示将一个物体围绕着一个中心转动。Processing中rotate函数围绕着原点旋转坐标系。它只接受一个参数,这个参数是旋转坐标系的角度。角度的单位是弧度。旋转一周是360度,换算成弧度就是2pi,大概6.28弧度。如果你像我一样习惯了使用度而不是弧度,你可以使用radians()函数方便的把度转化为弧度。

下面的代码展示了rotate函数如何工作,修改代码然后运行:

上面的代码表示围绕着原点旋转坐标系20度,这儿的原点在窗口的左上角。下面的图形首先移动原点到窗口中心,然后旋转20度。

Rotate函数使在圆上绘制对象变得很容易:

  1. 移动原点,到你想要画的圆的圆心。
  2. 旋转坐标系,将你想要绘制的图形,绘制在圆的边上。

绘制圆形组成的圆形

要绘制上面的图形,我们需要用到for循环,在循环中绘制圆,并且确保每个圆之间的距离是相等的。我们要考虑要画的圆之间的角度是多少,注意一个圆周是360度,输入下面的代码:

def setup():
    size(600,600)

def draw():
    translate(width/2, height/2)
    for i in range(12):
        ellipase(200,0,50,50)
        rotate(radians(360/12))

注意,translate函数将坐标系的原点移动到屏幕的中央。接下来我们开始了一个for循环,循环中创建圆,圆心坐标在(200,0),半径是50。然后旋转坐标系360÷12度或者说30度,但是在旋转之前我们需要将它转换成弧度。这也就是说每个小圆之间的角度是30度。

绘制由方块组成的圆

修改上一节的代码,将圆换成正方形。只需要改变ellipse函数为rect函数。

def setup():
    size(600,600)

def draw():
    translate(width/2, height/2)
    for i in range(12):
        rect(200,0,50,50)
        rotate(radians(360/12))

使对象动起来

Processing使用对象创建动画的功能很强大。对于你的第一个动画,咱们使用rotate函数。正常来说,rotate是瞬间发生的,所以你几乎不可能看到动作的发生,只看到rotate的结果,但是这次我们会用一个时间变量t,它可以使我们实时看到旋转。

创建t变量

让我们使用由方块组成的圆来创建动画程序,开始之前先创建t变量,然后把它初始化为0。然后插入下面的代码。

t = 0

def setup():
    size(600,600)
    
def draw():
    translate(width/2, height/2)
    rotate(radians(t))
    for i in range(12):
        rect(200,0,50,50)
        rotate(radians(360/12))
    t += 1

但是如果运行这个代码的话,你会得到下面的错误提示。

UnboundLocalError: local variable 't' referenced before assignment

这是因为Python语言不知道我们在函数中引用的变量t是全局变量。所以我们需要声明global t,输入完整的代码如下:

t = 0

def setup():
    size(600,600)
    
def draw():
    global t
    background(255) # set background white
    translate(width/2, height/2)
    rotate(radians(t))
    for i in range(12):
        rect(200,0,50,50)
        rotate(radians(360/12))
    t += 1

这个代码开始的时候t设置为0,然后旋转坐标系,然后t增加1,然后重复。运行代码:

接下来我们试着旋转每个单独的方块。

旋转单独的方块

因为在processing中旋转是围绕着原点的,在循环中我们需要首先移动到我们需要旋转的方块,然后旋转,最后绘制这个方块。修改循环中的代码如下:

for i in range(12):
        translate(200,0)
        rotate(radians(t))
        rect(0,0,50,50)
        rotate(radians(360/12))

我们移动到需要放置方块的位置,旋转坐标系以使方块旋转,然后绘制方块。

使用pushMatrix和popMatrix保存方向

但你运行上面的代码,你将会看到一些奇怪的动作,这些方块没有围绕着中心选择,只是在屏幕上一直在移动。

这是因为改变了坐标系的原点同时也改变了整个坐标系的方向。当移动到方块的位置之后,在移动到下一个方块之前,我们需要再回到方块组成的圆的圆心。可以使用另一个translate函数返回到之前的状态,但是因为需要返回的太多,你会很容易搞混。幸运的是有一个简单的办法。

Processing有两个内置的函数用来保存坐标系在某个点的方向并且返回:pushMatrix()popMatrix()。在这个例子中,我们需要保存原点位于屏幕中心时的方向。要做到这一点,像下面一样修改代码:

    for i in range(12):
        pushMatrix()
        translate(200,0)
        rotate(radians(t))
        rect(0,0,50,50)
        popMatrix()
        rotate(radians(360/12))

pushMatrix()函数保存以方块为圆的圆心的位置时的坐标,然后我们移动到需要绘制方块的位置,旋转坐标系,绘制方块。然后我们使用popMatrix()返回保存的方向,然后重复绘制12个方块。

围绕原点旋转

前面的代码能够正常的工作,但是这个旋转看上去有点奇怪。这是因为processing默认定位矩形是定位在它的左上角,旋转也是围绕着左上角。如果想要方块围绕中心旋转,可以再setup()函数中增加一行代码:

    rectMode(CENTER)

注意在编程中所有全部是大写的代码都非常重要。加上rectMode(CENTER)将使每个方块的旋转是围绕着它的中心。如果想要方块儿旋转的更快,修改rotate这一行,增加里面的t的值。

    rotate(radians(5*t))

在这儿,5是旋转的频率,这就意味着程序在处理5×t的时候,旋转的速度是原来的5倍。修改之后运行看一看。你还可以注释掉循环外部的rotate(radians(t))看看方块在自己位置上转动的效果。

使用translate()rotate()创建动态图形是非常强大的技术,但是如果你弄错了执行的顺序它会产生意想不到的结果。

创建交互式彩虹表格

你学习了如何使用循环和旋转来创建不同的图形,接下来我们会创建一个很漂亮的东西,一个方块组成的表格,里面的颜色会根据你鼠标的颜色来变换,第1步是创建一个表格。

绘制对象的表格

数学编程、游戏编程中(譬如扫雷)都常常需要绘制表格,这个教程中后面章节中许多地方都会用到表格,所以我们将会学习写绘制表格的代码,这些代码应该是可重用的,以备我们将来用到。作为开始,我们会制作一个12×12的方块的表格,这些方块的尺寸和直径的距离都是相等的。逐个绘制表格中的方块看起来好像很费时,但其实使用循环语句实现非常容易。

打开一个新的processing,保存文件名为colorGrid.pyde。我们将会在白色的背景上绘制20×20的格子。绘制方块需要使用rect,还要用到for循环。我们需要每隔30个像素绘制25个像素宽的方块:

rect(30*x, 30*y, 25, 25)

随着xy变量的增加,方块之间的距离是50个像素。我们仍然从setup()draw()函数开始:

def setup():
    size(600,600)

def draw():
    background(255)

上面的代码设置的窗口尺寸是600×600像素,设置背景颜色为白色。我们会写一个嵌套的循环,里面的两个变量都会从0~19,一共循环20次,因为我们需要2020列:

def setup():
    size(600,600)

def draw():
    background(255)
    for x in range(20):
        for y in range(20):
            rect(30*x,30*y,25,25)

现在我们已经创建了20×20的表格,下面的任务是为他们增加颜色。

为对象增加彩虹颜色

ProcessingcolorMode()函数能够帮我们增加很酷的颜色。它可以在RGBHSB模式之间切换。RGB就是红色,绿色和蓝色。HSB三个数字分别表示色调、饱和度和亮度。这里我们需要改变的只是第1个值,也就是色调,其他的两个值都可以保持在最大值255。下面的图展示了如何通过只改变色调来制造出彩虹颜色效果。方块下面的值就是它们的色调值,饱和度和亮度都是255

我们将方块定位在(30x,30y),我们将要创建一个变量来测量鼠标到这个位置的距离:

    d = dist(30*x, 30*y, mouseX, mouseY)

Processingdist()函数用于测量两个点之间的距离。在这个例子中,就是这个方块和鼠标之间的距离。程序把距离保存在变量d中,我们将会把它与色调联系起来。

def setup():
    size(600,600)
    rectMode(CENTER)
    colorMode(HSB)
    
def draw():
    background(0)
    translate(20,20)
    for x in range(20):
        for y in range(20):
            d = dist(30*x, 30*y, mouseX, mouseY)
            fill(0.5*d, 255, 255)
            rect(30*x, 30*y, 25, 25)

我们使用了colorMode()函数,把HSB作为参数传递给它。在draw()中,我们首先设置背景为黑色,然后我们计算鼠标和方块的距离,下一行中,我们使用HSB的值填充颜色。色调的值是距离的一半,饱和度和亮度的值都是255

唯一改变的就是色调:根据鼠标到方块的距离改变色调值。我们使用dist()测量两个点的距离。

运行代码你会看到,方块上的颜色会根据你所标的位置的变化而变化。

使用三角形创建复杂图形

我们现在学习怎样使用triangle()函数创建等边三角形。首先启动processing,新建一个文件取名叫triangle,输入下面的代码:

def setup():
    size(600,600)
    rectMode(CENTER)
    
t = 0
    
def draw():
    global t
    translate(width/2, height/2)
    rotate(radians(t))
    triangle(0,0,100,100,200,-200)
    t += 0.5

上面的代码中使用的知识都是我们已经学习过的:创建了一个t变量,将坐标系移动到我们想绘制三角形的位置,旋转坐标系,绘制三角形,最后增加t的值。运行代码你将会看到下面的图形:

三角形围绕着它的一个端点旋转,因此三角形外边的点组成了一个圆。你可能也发现了,这个三角形是一个直角三角形,它的一个角的角度是90度,不是等边三角形。

我们需要绘制等边三角形,也就是说每个边的边长相等。还需要找到这个等边三角形的中点,使三角形围绕着它的中心旋转。要实现这些,我们需要确定等边三角形的三个顶点的坐标。想一想,在确定一个等边三角形的中心之后,如何绘制这个等边三角形?

30-60-90度三角形

要确定等边三角形的三个顶点的坐标,我们需要温习一下你在几何课上学到的直角三角形的知识:30-60-90度的三角形,是特殊的直角三角形。看下面的图形:

这个等边三角形是由三个相同的图形组成。中间的点是这个三角形的中点,也是里面三个相同的三角形的交点,它们在这一点相交的角度是120度。在Processing中绘制三角形,需要给triangle函数提供6个参数:三个顶点的x坐标和y坐标。要找到上面图形中三个顶点的坐标,我们把上面的三角形中下面的部分一分为二,就像下面的图形一样:

将下面的三角形分成相同的两部分,就创建了两个直角三角形,两个直角三角形都是经典的30-60-90度的三角形。如果你没有忘记几何老师教给你的知识,应该还记得30-60-90度的三角形,边长可以表示为下面的图形:

如果我们设这个小三角形中短的一边边长为x,那么斜边的边长是2x,另一条长边的边长是[图片上传失败...(image-bb3423-1587535142394)],大约是1.732x。我们将使用上面图形中大三角形的中点到它的一个顶点的距离作为参数来创建一个函数,这个距离正好是30-60-90度三角形中的斜边。我们假设斜边的长度是length,短边的长度就是length÷2,长边的长度就是 [图片上传失败...(image-3224f3-1587535142394)],如下面图形显示的那样:

你已经看到了30-60-90度三角形,其内角分别是306090度,边长成比例。讲到这儿,你应该想起了勾股定理。

我们假设大的三角形中心到它任意一个顶点的距离是length,也就是30-60-90三角形的斜边。你要明白在这个特殊的三角形中各边长的比例,然后才能绘出这个三角形的各个顶点。右边三角形中和30度角相对的短边的长度总是等于斜边的一半,长边的长度等于短边乘以[图片上传失败...(image-f384fa-1587535142394)] 。所以如果我们根据这个大三角形的中心点位置绘制等边三角形的话,三个顶点的坐标应该如下图所示:

如你所见,这个三角形的三条边是由几个30-60-90三角形组成,我们可以利用比例计算出这个大三角形的三个顶点离中心的距离。

绘制等边三角形

现在你可以利用上面的方法求出等边三角形的三个点的坐标,使用下面的代码:

def setup():
    size(600,600)
    rectMode(CENTER)

t = 0

def draw():
    global t
    translate(width/2, height/2)
    rotate(radians(t))
    tri(200)
    t += 0.5

def tri(length):
    '''Draw an equilateral triangle
    around center of triangle'''
    triangle(0, -length,
             -length*sqrt(3)/2, length/2,
             length*sqrt(3)/2, length/2)

首先我们写了自定义函数tri(),这个函数只有一个参数length,length是从大三角形中分割的30-60-90度三角形的斜边。然后我们使用计算的三个顶点绘制三角形。运行代码你会看到下面的图形:

现在我们可以擦掉所有已经绘制的三角形,通过在draw函数的第1行加入下面的代码:

    background(255)

这行代码会擦掉原来绘制的旋转的三角形,所以我们在屏幕上只剩下一个等边三角形。下来我们要在一个圆上绘制90个三角形,就像我们在这一章中前面部分学过的,我们将使用rotate函数。

绘制多个旋转的三角形

现在你学会了如何绘制旋转的单个三角形,我们需要找到将多个三角形放在一个圆上的办法。这和前面学过的将方块放在圆上的方法类似,这次我们使用tri函数。输入下面的代码:

def setup():
    size(600,600)
    rectMode(CENTER)

t = 0

def draw():
    background(255)
    global t
    translate(width/2, height/2)
    for i in range(90):
        rotate(radians(360/90))
        pushMatrix()
        translate(200,0)
        rotate(radians(t))
        tri(100)
        popMatrix()
    t += 0.5

def tri(length):
    '''Draw an equilateral triangle
    around center of triangle'''
    noFill()
    triangle(0, -length,
             -length*sqrt(3)/2, length/2,
             length*sqrt(3)/2, length/2)

在程序中我们使用for循环重复绘制90个三角形在同一个圆上,通过旋转坐标系360/90来确保三角形之间的距离是相等的。我们使用pullMatrix在移动坐标系之前保存当前坐标系。在循环结束之前,我们使用popMatrix返回保存的坐标系。在tri函数中,我们加入了noFill函数设置三角形为透明。

现在我们绘制了90个旋转的透明的三角形,但是他们旋转的方式完全一样,接下来我们要学习怎么让每个三角形以自己的角度旋转,使图形看上去更有趣。

旋转相移

我们可以使用相移改变三角形旋转的方式,使每个三角形的旋转角度和它的邻居稍有不同,给图形制造一种波浪的效果。循环中的每个三角形已经被赋予了一个值,就是i。我们需要将i加在t上,作为rotate的参数,就像这样:

        rotate(radians(t+i))

保存然后运行,注意在图形中的右侧有一块缺口。这个缺口的出现是因为第1个三角形的相移和最后一个三角形的相移不匹配造成的。我们需要的图形是完美的,平滑的,因此需要是相移的角度是360度的倍数。因为圆中有90个三角形,我们使用360÷90,然后乘以i

        rotate(radians(t+i*360/90))

360÷90=4,我们完全可以直接把4插入代码中,但是不建议这样做,因为要考虑以后也许会绘制不同数量的三角形。现在在运行应该能够得到一个完美的图案:

最后的工作

为了使图形看上去更有趣,我们需要再调整一下相移的角度。在这里你可以自己将角度设成任意的数,看看图形会有什么有趣的变化。

我们将会使相移的角度为i×2,这会使每个三角形和它的邻居相比差别更大一点。将rotate这一行的代码修改成下面的样子:

        rotate(radians(t+2*i*360/90))

修改代码之后运行,你可以看到下面的图案

若您喜欢请赞赏支持。更多视频内容欢迎访问我的B站专栏

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容