[Python] 借助 Turtle 逐字展示《醉翁亭记》和《兰亭集序》

87 阅读8分钟

背景

turtle — Turtle graphics 中提供了关于 turtle\text{turtle} 模块的介绍。我想到可以用 turtle\text{turtle} 来展示经典的古文,比如《醉翁亭记》《兰亭集序》,于是用 trae 完成了代码(我做了少量调整)。

正文

整体思路

我想借助 turtle — Turtle graphics 来逐字展示《醉翁亭记》,示例效果如下图所示 ⬇️

image.png

有几个核心的问题

  • 界面的长和宽需要根据字数动态计算,而不是 hard-code
  • 每过一个固定的时间,就多展示一个字,我们可以在 main 方法里实现这个主循环

我们可以用 trae 来处理这些细节。本文的代码,主要是 trae 完成的,我只做了些微小的调整。

完整的代码

import math
import time
from turtle import Screen, Turtle

ARTICLE = (
    "環滁皆山也。 其西南諸峰,林壑尤美。望之蔚然而深秀者,琅琊也。"
    "山行六七里,漸聞水聲潺潺,而瀉出於兩峰之間者,讓泉也。"
    "峰回路轉,有亭翼然臨於泉上者,醉翁亭也。作亭者誰?山之僧曰智僊也。"
    "名之者誰?太守自謂也。太守與客來飲於此,飲少輒醉,而年又最高,"
    "故自號曰醉翁也。醉翁之意不在酒,在乎山水之間也。"
    "山水之樂,得之心而寓之酒也。"
    "\n\n"
    "若夫日出而林霏開,雲歸而巖穴暝,晦明變化者,山間之朝暮也。"
    "野芳發而幽香,佳木秀而繁陰,風霜高潔,水落而石出者,山間之四時也。"
    "朝而往,暮而歸,四時之景不同,而樂亦無窮也。"
    "\n\n"
    "至於負者歌於途,行者休於樹,前者呼,後者應,傴僂提攜,"
    "往來而不絕者,滁人遊也。臨谿而漁,谿深而魚肥;釀泉為酒,泉香而酒洌;"
    "山肴野蔌,雜然而前陳者,太守宴也。宴酣之樂,非絲非竹,射者中,"
    "弈者勝,觥籌交錯,起坐而諠譁者,眾賓懽也;蒼顏白髮,"
    "頹然乎其間者,太守醉也。"
    "\n\n"
    "已而夕陽在山,人影散亂,太守歸而賓客從也。樹林陰翳,鳴聲上下,"
    "遊人去而禽鳥樂也。然而禽鳥知山林之樂,而不知人之樂;"
    "人知從太守遊而樂,而不知太守之樂其樂也。醉能同其樂,"
    "醒能述以文者,太守也。太守謂誰?廬陵歐陽脩也。"
)

CHAR_SIZE = 20
CHAR_WIDTH = 36
CHARS_PER_COLUMN = 20
COLUMN_SPACING = 40
MARGIN = 80

CHAR_DELAY_MS = 150

FONT_PATH = "/System/Library/Fonts/PingFang.ttc"

PUNCTUATION = set(",。、;:?!「」『』()【】《》""''……—·〈〉﹝﹞〔〕")


def load_article():
    return "".join(
        ch for ch in ARTICLE if ch not in PUNCTUATION and not ch.isspace()
    )


def draw_char(x, y, ch, pen):
    pen.penup()
    pen.goto(x, y)
    pen.pendown()
    font = (FONT_PATH, CHAR_SIZE, "normal")
    pen.write(ch, align="center", font=font)


def setup_screen(width, height):
    screen = Screen()
    screen.setup(width, height)
    screen.title("醉翁亭記")
    screen.tracer(0)
    return screen


def setup_pen():
    pen = Turtle()
    pen.hideturtle()
    pen.color("black")
    return pen


def draw_next_char(position, characters, screen, pen, start_x, start_y):
    col = position // CHARS_PER_COLUMN
    row = position % CHARS_PER_COLUMN

    x = start_x - col * COLUMN_SPACING
    y = start_y - row * CHAR_WIDTH

    ch = characters[position]
    draw_char(x, y, ch, pen)
    screen.update()


def main():
    characters = list(load_article())

    total = len(characters)
    column_count = math.ceil(total / CHARS_PER_COLUMN)
    content_width = (column_count - 1) * COLUMN_SPACING
    content_height = (CHARS_PER_COLUMN - 1) * CHAR_WIDTH + CHAR_SIZE

    screen_width = content_width + 2 * MARGIN
    screen_height = content_height + 2 * MARGIN

    start_x = screen_width // 2 - MARGIN
    start_y = screen_height // 2 - MARGIN - CHAR_SIZE

    screen = setup_screen(screen_width, screen_height)
    pen = setup_pen()

    for position in range(total):
        draw_next_char(position, characters, screen, pen, start_x, start_y)
        time.sleep(CHAR_DELAY_MS / 1000)

    screen.mainloop()


if __name__ == "__main__":
    main()

运行效果

请将以上代码保存为 main.py。使用如下命令可以运行 main.py

python3 main.py

运行中的效果如下图所示 ⬇️

image.png

全文都展示好之后,效果如下图所示 ⬇️

image.png

说明

字体

我是在 Mac 电脑上进行操作的,代码里有一个地方用到了系统相关的路径(具体位置如下图所示),如果您用的是其他系统,请将其替换为合适的值

image.png

《醉翁亭记》

为了便于读者使用,我把《醉翁亭记》全文嵌在代码里了,这并不是好的编程风格。

image.png

《醉翁亭记》的内容,我是从 这个链接 复制过来(为了方便展示,我删掉了其中的两个注释符号)的。《醉翁亭记》是北宋欧阳修的作品,所以没有版权问题。

逐字展示《兰亭集序》

正文中提供了可以逐字展示《醉翁亭记》的代码,我们只要稍作调整,就可以将其转化为逐字展示《兰亭集序》的代码。需要调整的地方如下图所示 ⬇️

image.png

调整后的完整代码如下 ⬇️

import math
import time
from turtle import Screen, Turtle

ARTICLE = (
    "永和九年,歲在癸丑,暮春之初,會於會稽山陰之蘭亭,修禊事也。"
    "群賢畢至,少長咸集。此地有崇山峻嶺,茂林修竹;"
    "又有清流激湍,映帶左右,引以為流觴曲水,列坐其次。"
    "雖無絲竹管弦之盛,一觴一詠,亦足以暢敘幽情。"
    "\n"
    "是日也,天朗氣清,惠風和暢,仰觀宇宙之大,俯察品類之盛,"
    "所以游目騁懷,足以極視聽之娛,信可樂也。"
    "\n"
    "夫人之相與,俯仰一世,或取諸懷抱,晤言一室之內;"
    "或因寄所托,放浪形骸之外。雖取舍萬殊,靜躁不同,"
    "當其欣於所遇,暫得於己,快然自足,不知老之將至。"
    "及其所之既倦,情隨事遷,感慨系之矣。"
    "向之所欣,俯仰之間,已為陳跡,猶不能不以之興懷。"
    "況修短隨化,終期於盡。古人云:「死生亦大矣。」豈不痛哉!"
    "\n"
    "每覽昔人興感之由,若合一契,未嘗不臨文嗟悼,不能喻之於懷。"
    "固知一死生為虛誕,齊彭殤為妄作。後之視今,亦猶今之視昔。"
    "悲夫!故列敘時人,錄其所述,雖世殊事異,所以興懷,其致一也。"
    "後之覽者,亦將有感於斯文。"
)

CHAR_SIZE = 20
CHAR_WIDTH = 36
CHARS_PER_COLUMN = 20
COLUMN_SPACING = 40
MARGIN = 80

CHAR_DELAY_MS = 150

FONT_PATH = "/System/Library/Fonts/PingFang.ttc"

PUNCTUATION = set(",。、;:?!「」『』()【】《》""''……—·〈〉﹝﹞〔〕")


def load_article():
    return "".join(
        ch for ch in ARTICLE if ch not in PUNCTUATION and not ch.isspace()
    )


def draw_char(x, y, ch, pen):
    pen.penup()
    pen.goto(x, y)
    pen.pendown()
    font = (FONT_PATH, CHAR_SIZE, "normal")
    pen.write(ch, align="center", font=font)


def setup_screen(width, height):
    screen = Screen()
    screen.setup(width, height)
    screen.title("蘭亭集序")
    screen.tracer(0)
    return screen


def setup_pen():
    pen = Turtle()
    pen.hideturtle()
    pen.color("black")
    return pen


def draw_next_char(position, characters, screen, pen, start_x, start_y):
    col = position // CHARS_PER_COLUMN
    row = position % CHARS_PER_COLUMN

    x = start_x - col * COLUMN_SPACING
    y = start_y - row * CHAR_WIDTH

    ch = characters[position]
    draw_char(x, y, ch, pen)
    screen.update()


def main():
    characters = list(load_article())

    total = len(characters)
    column_count = math.ceil(total / CHARS_PER_COLUMN)
    content_width = (column_count - 1) * COLUMN_SPACING
    content_height = (CHARS_PER_COLUMN - 1) * CHAR_WIDTH + CHAR_SIZE

    screen_width = content_width + 2 * MARGIN
    screen_height = content_height + 2 * MARGIN

    start_x = screen_width // 2 - MARGIN
    start_y = screen_height // 2 - MARGIN - CHAR_SIZE

    screen = setup_screen(screen_width, screen_height)
    pen = setup_pen()

    for position in range(total):
        draw_next_char(position, characters, screen, pen, start_x, start_y)
        time.sleep(CHAR_DELAY_MS / 1000)

    screen.mainloop()


if __name__ == "__main__":
    main()

运行效果如下方两图所示 ⬇️

image.png image.png

《兰亭集序》的内容,我是从 这个链接 复制过来的。《兰亭集序》是东晋王羲之的作品,所以没有版权问题。

用楷体展示

如果您的电脑上安装了楷体,那么还可以尝试将文字用楷体展示。霞鹜文楷 中列举了可行的安装方式 ⬇️

image.png

安装成功后,可以把代码调整成这样 ⬇️

import math
import time
from turtle import Screen, Turtle

ARTICLE = (
    "永和九年,歲在癸丑,暮春之初,會於會稽山陰之蘭亭,修禊事也。"
    "群賢畢至,少長咸集。此地有崇山峻嶺,茂林修竹;"
    "又有清流激湍,映帶左右,引以為流觴曲水,列坐其次。"
    "雖無絲竹管弦之盛,一觴一詠,亦足以暢敘幽情。"
    "\n"
    "是日也,天朗氣清,惠風和暢,仰觀宇宙之大,俯察品類之盛,"
    "所以游目騁懷,足以極視聽之娛,信可樂也。"
    "\n"
    "夫人之相與,俯仰一世,或取諸懷抱,晤言一室之內;"
    "或因寄所托,放浪形骸之外。雖取舍萬殊,靜躁不同,"
    "當其欣於所遇,暫得於己,快然自足,不知老之將至。"
    "及其所之既倦,情隨事遷,感慨系之矣。"
    "向之所欣,俯仰之間,已為陳跡,猶不能不以之興懷。"
    "況修短隨化,終期於盡。古人云:「死生亦大矣。」豈不痛哉!"
    "\n"
    "每覽昔人興感之由,若合一契,未嘗不臨文嗟悼,不能喻之於懷。"
    "固知一死生為虛誕,齊彭殤為妄作。後之視今,亦猶今之視昔。"
    "悲夫!故列敘時人,錄其所述,雖世殊事異,所以興懷,其致一也。"
    "後之覽者,亦將有感於斯文。"
)

CHAR_SIZE = 20
CHAR_WIDTH = 36
CHARS_PER_COLUMN = 20
COLUMN_SPACING = 40
MARGIN = 80

CHAR_DELAY_MS = 150

PUNCTUATION = set(",。、;:?!「」『』()【】《》""''……—·〈〉﹝﹞〔〕")


def load_article():
    return "".join(
        ch for ch in ARTICLE if ch not in PUNCTUATION and not ch.isspace()
    )


def draw_char(x, y, ch, pen):
    pen.penup()
    pen.goto(x, y)
    pen.pendown()
    font = ("LXGWWenKai-Regular", CHAR_SIZE, "normal")
    pen.write(ch, align="center", font=font)


def setup_screen(width, height):
    screen = Screen()
    screen.setup(width, height)
    screen.title("蘭亭集序")
    screen.tracer(0)
    return screen


def setup_pen():
    pen = Turtle()
    pen.hideturtle()
    pen.color("black")
    return pen


def draw_next_char(position, characters, screen, pen, start_x, start_y):
    col = position // CHARS_PER_COLUMN
    row = position % CHARS_PER_COLUMN

    x = start_x - col * COLUMN_SPACING
    y = start_y - row * CHAR_WIDTH

    ch = characters[position]
    draw_char(x, y, ch, pen)
    screen.update()


def main():
    characters = list(load_article())

    total = len(characters)
    column_count = math.ceil(total / CHARS_PER_COLUMN)
    content_width = (column_count - 1) * COLUMN_SPACING
    content_height = (CHARS_PER_COLUMN - 1) * CHAR_WIDTH + CHAR_SIZE

    screen_width = content_width + 2 * MARGIN
    screen_height = content_height + 2 * MARGIN

    start_x = screen_width // 2 - MARGIN
    start_y = screen_height // 2 - MARGIN - CHAR_SIZE

    screen = setup_screen(screen_width, screen_height)
    pen = setup_pen()

    for position in range(total):
        draw_next_char(position, characters, screen, pen, start_x, start_y)
        time.sleep(CHAR_DELAY_MS / 1000)

    screen.mainloop()


if __name__ == "__main__":
    main()

这样就能看到楷体字的效果了 ⬇️

image.png

参考资料