字符串和文本绘制


本教程适用于 Processing 的 Python 模式。如果您发现任何错误或有建议,请告诉我们。本教程改编自 Daniel Shiffman 所著的《Learning Processing》一书,由 Morgan Kaufmann Publishers 出版,版权所有 © 2008 Elsevier Inc. 保留所有权利。

字符串类

如果你想在Processing中在屏幕上显示文本,你必须首先熟悉Python内置的字符串类。字符串对你来说可能不是一个全新的概念,很可能你之前已经处理过它们。例如,如果你曾经向消息窗口打印过一些文本或从文件加载过图像,你就写过这样的代码:

print("printing some text to the message window!")  # 打印字符串
img = loadImage("filename.jpg")                     # 使用字符串作为文件名

然而,尽管你可能在这里和那里使用过字符串,现在是时候释放它们的全部潜力了。

在哪里找到字符串类的文档?

虽然从技术上讲是Python类,但由于字符串使用如此频繁,Processing.py在其参考文档中包含了文档。在这里你会找到像通用字符串类这样的参考页面。

本页只涵盖了字符串类的一些可用方法。完整文档可以在Python的字符串页面上找到。

什么是字符串?

字符串,就其核心而言,实际上只是一种存储字符列表的奇特方式。如果我们没有字符串类,我们可能不得不写一些像这样的代码:

sometext = ['H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd']

显然,这在Processing中会是一个巨大的痛苦。做以下事情并创建一个字符串对象要简单得多:

sometext = "How do I make string? Type some characters between quotation marks!"

从上面可以看出,字符串只不过是引号之间的字符列表。然而,这只是字符串的数据。我们必须记住,字符串是一个具有方法的对象(你可以在参考页面上找到)。这就像我们在像素教程中学到的PImage既存储与图像相关的数据,也存储功能:copy()、loadPixels()等。

例如,在字符串末尾使用方括号(即myString[0])返回字符串中给定索引处的单个字符。这实际上只是调用字符串的__getitem__()方法的一种简单方式!注意,字符串就像列表一样,第一个字符是索引#0!

message = "some text here."
c = message[3]
print(c)                   # 结果是 'e'

另一个有用的函数是len()。通过调用len()函数并将我们的字符串作为参数,我们可以检索字符串的长度。

message = "This String is 34 characters long."
print(len(message))                          # 打印 34

我们还可以使用.upper()方法将字符串更改为全大写(.lower也可用)。

uppercase = message.upper()
print(uppercase)

你可能会注意到这里有点奇怪。为什么我们不简单地说"message.upper()",然后打印"message"变量?相反,我们将"message.upper"的结果分配给一个具有不同名称的新变量——"uppercase"。

这是因为字符串是一种特殊的对象。它是不可变的。不可变对象是其数据永远不能改变的对象。一旦我们创建一个字符串,它就会终生保持不变。任何时候我们想要改变字符串,我们都必须创建一个新的。所以在转换为大写的情况下,方法.upper()返回一个全大写的字符串对象副本。

最后,让我们看看==(相等)运算符。现在,字符串可以用"=="运算符进行比较,如下所示:

one = "hello"
two = "hello"
print(one == two) # True!

虽然在其他语言中字符串有自己的相等性检查方法,但在Python中,相等性检查内置在默认的==(相等)运算符中。这使得比较字符串变得超级简单!

字符串对象的另一个特性是连接,将两个字符串连接在一起。字符串用"+"运算符连接。当然,加号通常意味着数字的加法。当与字符串一起使用时,它意味着连接。

helloworld = "Hello" + "World"

变量也可以使用字符串格式化引入到字符串中

x = 10
message = "The value of x is: %i" % (x)
print(message)

codeMsg = "spooky ghost"
secretMsg = "The secret message is: %s" % codeMsg

注意上面我在将不同类型的变量放入字符串时使用了不同的字符(%i表示整数,%s表示字符串)。你可以在这里找到这些字符串格式化操作的完整列表。

显示文本

显示字符串最简单的方法是在消息窗口中打印它。这可能是你在调试时做过的事情。例如,如果你需要知道鼠标的水平位置,你会写:

print(mouseX)

或者如果你需要确定代码的某个部分被执行了,你可能会打印出一个描述性消息。

print("We got here and we're printing out the mouse location!!!")

虽然这对调试很有价值,但它不会帮助我们为用户显示文本的目标。要在屏幕上放置文本,我们必须遵循一系列简单的步骤。

首先,我们创建一个PFont对象。为了做到这一点,我们将使用createFont()函数。作为参数,我们引用字体名称和大小。这应该只做一次,通常在setup()期间。就像加载图像一样,加载字体是一个内存密集且缓慢的过程,如果放在像draw()这样频繁调用的函数中,会严重影响你的草图的性能。由于Java(Processing.py是用它创建的)的限制,不是所有字体都可以使用,有些可能在一个操作系统上工作,而在其他系统上不工作。当与他人分享草图或在网上发布时,你可能需要在草图的数据目录中包含字体的.ttf或.otf版本,因为其他人可能没有在他们的计算机上安装该字体。只有可以合法分发的字体才应该包含在草图中。除了字体名称,你还可以指定大小以及是否开启抗锯齿。

f = createFont("Arial", 16, True) # f将表示16号Arial,抗锯齿开启

接下来,我们使用textFont()指定字体。textFont()接受一个或两个参数,字体变量和字体大小,后者是可选的。如果你不包含字体大小,字体将以最初加载的大小显示。在可能的情况下,text()函数将使用原生字体而不是用createFont()在幕后创建的位图版本,这样你就有机会动态缩放字体。当使用P2D时,草图的实际原生版本字体将被使用,提高绘制质量和性能。使用P3D渲染器时,将使用位图版本,因此指定与加载的字体大小不同的字体大小可能导致像素化文本。

textFont(f,36)

然后,使用fill()指定颜色。

fill(255)

最后,调用text()函数来显示文本。(这个函数就像形状或图像绘制一样,它接受3个参数——要显示的文本,以及显示该文本的x和y坐标。)

text("Hello Strings!",10,100)

这里是所有步骤一起:

示例:简单显示文本

def setup():
    global f
    size(200,200)
    f = createFont("Arial",16)

def draw():
    global f
    background(255)
    textFont(f,16)            
    fill(0)                       
    text("Hello Strings!",10,100)

字体也可以使用"工具"→"创建字体"来创建。这将创建并放置一个VLW字体文件在你的数据目录中,你可以使用loadFont()将其加载到PFont对象中。

f = loadFont("ArialMT-16.vlw")

动画文本

让我们看看与显示文本相关的两个更有用的Processing函数:

textAlign()——为文本指定RIGHT、LEFT或CENTER对齐。

示例:文本对齐

def setup():
    global f
    size(400,200)
    f = createFont("Arial",16,True)

def draw():
    global f
    background(255)

    stroke(175)
    line(width/2,0,width/2,height)

    textFont(f)       
    fill(0)

    textAlign(CENTER)
    text("This text is centered.",width/2,60) 

    textAlign(LEFT)
    text("This text is left aligned.",width/2,100)

    textAlign(RIGHT)
    text("This text is right aligned.",width/2,140)

textWidth()——计算并返回任何字符或文本字符串的宽度。

假设我们想创建一个新闻滚动条,其中文本从屏幕底部从左到右滚动。当新闻标题离开窗口时,它重新出现在右侧并再次滚动。如果我们知道文本开始位置的x坐标,并且我们知道该文本的宽度,我们就可以确定它何时不再可见。textWidth()给了我们那个宽度。

首先,我们声明headline、font和x位置变量,在setup()中初始化它们。

headline = "New study shows computer programming lowers cholesterol."

def setup():
  global f, x
  f = createFont("Arial",16,true)  # 加载字体
  x = width # 将标题初始化到屏幕右侧

在draw()中,我们在适当的位置显示文本。

# 在x位置显示标题
textFont(f,16)     
textAlign(LEFT)
text(headline,x,180)

我们通过速度值改变x(在这种情况下是负数,所以文本向左移动。)

# 递减x
x = x - 3

现在来了更困难的部分。测试圆圈何时到达屏幕左侧很容易。我们只需问:x是否小于0?然而,对于文本,由于它是左对齐的,当x等于零时,它仍然在屏幕上可见。相反,当x小于0减去文本宽度时,文本将不可见(见下图)。当出现这种情况时,我们将x重置回窗口的右侧,即width。

# 如果x小于负宽度,那么它完全离开屏幕
w = textWidth(headline)
if (x < -w):
  x = width

这是完整的示例,每次前一个标题离开屏幕时显示不同的标题。标题存储在字符串列表中。

示例:滚动标题

# 新闻标题列表
headlines = [ "Processing downloads break downloading record.", 
              "New study shows computer programming lowers cholesterol."]

def setup():
    global f, x, index
    size(400,200)
    f = createFont("Arial",16,True)
    # 将标题初始化到屏幕右侧
    x = width 
    index = 0

def draw():
    global f, x, index, headlines
    background(255)
    fill(0)

    # 在x位置显示标题
    textFont(f,16)       
    textAlign(LEFT)
    text(headlines[index],x,180)

    # 递减x
    x = x - 3

    # 如果x小于负宽度,那么它离开屏幕
    w = textWidth(headlines[index])
    if (x < -w):
      x = width 
      index = (index + 1) % len(headlines)

除了textAlign()和textWidth(),Processing还提供textLeading()、textMode()、textSize()函数用于额外的显示功能。

旋转文本

平移和旋转也可以应用于文本。例如,要围绕其中心旋转文本,平移到原点并使用textAlign(CENTER)来显示文本。

示例:旋转文本

message = "this text is spinning"
def setup():
    global f, theta
    size(200, 200)
    f = createFont("Arial",20,True)
    theta = 0
            
def draw():
    global message, f, theta
    background(255)
    fill(0)
    textFont(f)                  # 设置字体
    translate(width/2,height/2)  # 平移到中心
    rotate(theta)                # 按theta旋转
    textAlign(CENTER)            
    text(message,0,0)            
    theta += 0.05                # 增加旋转

逐字符显示文本

在某些图形应用程序中,需要逐字符渲染显示文本。例如,如果每个字符需要独立移动或着色,那么简单地说

text("a bunch of letters",0,0)

是不够的。

解决方案是遍历字符串,一次显示一个字符。

让我们先看一个一次性显示所有文本的示例。

message = "Each character is not written individually."

def setup():
    global f
    size(400, 200)
    f = createFont("Arial",20,True)

def draw(): 
    global f, message
    background(255)
    fill(0)
    textFont(f)         
    # 使用text()一次性显示文本块。
    text(message,10,height/2)

我们可以重写代码以在循环中显示每个字符,使用[](方括号/获取项目)运算符。

message = "Each character is written individually."

# 第一个字符在像素10处。
x = 10
for i in range(len(message)):
  # 每个字符一次显示一个,使用charAt()函数。
  text(message[i],x,height/2)
  # 所有字符间隔10像素。
  x += 10

为每个字符调用text()函数将允许我们更多的灵活性(用于着色、调整大小和在字符串内单独放置字符)。然而,上面的代码有一个相当大的缺陷——x位置为每个字符增加10像素。虽然这大致正确,但因为每个字符不是正好10像素宽,间距是不对的。

可以使用textWidth()函数实现正确的间距,如下面的代码所示。注意这个示例如何实现正确的间距,即使每个字符都是随机大小!

message = "Each character is written individually."

def setup():
    global f
    size(400, 150)
    f = createFont("Arial",20,True)

def draw(): 
    global f, message
    background(255)
    fill(0)
    textFont(f)         
    x = 10
    for i in range(len(message)):
        textSize(random(12,36))
        text(message[i],x,height/2)

        # textWidth()正确间隔字符。
        x += textWidth(message[i]) 
    noLoop()

这种"逐字母"方法也可以应用于字符串中的字符独立移动的草图。以下示例使用面向对象设计,使原始字符串中的每个字符成为Letter对象,允许它既在其正确位置显示,又可以在屏幕上单独移动。

示例:文本分解

message = "click mouse to shake it up"

def setup():
    global f, message, letters
    size(260, 200)
    # 加载字体
    f = createFont("Arial",20,True)
    textFont(f)
  
    # 创建一个与字符串相同大小的全0列表
    letters = [0] * len(message)
    # 在正确的x位置初始化Letters
    x = 16
    for i in range(len(message)):
        letters[i] = Letter(x,100,message[i]) 
        x += textWidth(message[i])

def draw():
    global f, letters
    background(255)
    for i in range(len(letters)):
        # 显示所有字母
        letters[i].display()
        
        # 如果鼠标被按下,字母会摇晃
        # 如果没有,它们会回到原来的位置
        if (mousePressed):
            letters[i].shake()
        else:
            letters[i].home()

# 描述单个Letter的类
class Letter():
    def __init__(self, x, y, letter):
        self.homex = self.x = x
        self.homey = self.y = y
        self.letter = letter

    # 显示Letter
    def display(self):
        fill(0)
        textAlign(LEFT)
        text(self.letter, self.x, self.y)

    # 随机移动字母
    def shake(self):
        self.x += random(-2,2)
        self.y += random(-2,2)

    # 将字母带回家
    def home(self):
        self.x = self.homex
        self.y = self.homey

逐字符方法也允许我们沿着曲线显示文本。在我们继续讨论字母之前,让我们先看看如何沿着曲线绘制一系列盒子。这个例子大量使用三角函数。

示例:沿着曲线的盒子

# 圆的半径
r = 100.0
# 盒子的宽度和高度
w = 40.0
h = 40.0

def setup():
    size(320, 320)
    smooth()

def draw():
    global r, w, h
    background(255)
    
    # 从中心开始并绘制圆
    translate(width / 2, height / 2)
    noFill()
    stroke(0)
    # 我们的曲线是窗口中心半径为r的圆。
    ellipse(0, 0, r*2, r*2)

    # 沿着曲线的10个盒子
    totalBoxes = 10
    # 我们必须跟踪我们在曲线上的位置
    arclength = 0.0
    
    # 对于每个盒子
    for i in range(totalBoxes):
        # 每个盒子都是居中的,所以我们移动宽度的一半
        arclength += w/2
        # 弧度角是弧长除以半径
        theta = arclength / r    
        
        pushMatrix()
        # 极坐标到笛卡尔坐标转换
        translate(r*cos(theta), r*sin(theta))
        # 旋转盒子
        rotate(theta)
        # 显示盒子
        fill(0,100)
        rectMode(CENTER)
        rect(0,0,w,h)
        popMatrix()
        # 再次移动一半
        arclength += w/2

我们需要做的是用适合盒子内部的字符串字符替换每个盒子。由于字符的宽度都不相同,而不是使用保持恒定的变量"w",沿着曲线的每个盒子将根据textWidth()函数具有可变宽度。

示例:沿着曲线的字符

# 要显示的消息
message = "text along a curve"

# 圆的半径
r = 100

def setup():
    global f
    size(320, 320)
    f = createFont("Georgia",40,True)
    textFont(f)
    # 文本必须居中!
    textAlign(CENTER)
    smooth()

def draw():
    global r, f
    background(255)

    # 从中心开始并绘制圆
    translate(width / 2, height / 2)
    noFill()
    stroke(0)
    ellipse(0, 0, r*2, r*2)

    # 我们必须跟踪我们在曲线上的位置
    arclength = 0

    # 对于每个盒子
    for i in range(len(message)):

        # 而不是恒定宽度,我们检查每个字符的宽度。
        currentChar = message[i]
        w = textWidth(currentChar)

        # 每个盒子都是居中的,所以我们移动宽度的一半
        arclength += w/2
        # 弧度角是弧长除以半径
        # 通过添加PI从圆的左侧开始
        theta = PI + arclength / r   

        pushMatrix()
        # 极坐标到笛卡尔坐标转换
        translate(r*cos(theta), r*sin(theta))
        # 旋转盒子
        rotate(theta+PI/2)   # 旋转偏移90度
        # 显示字符
        fill(0)
        text(currentChar,0,0)
        popMatrix()
        # 再次移动一半
        arclength += w/2

特别感谢Ariel Malka对这个最后的曲线文本示例的建议。