ZetCode-图形教程-二-
ZetCode 图形教程(二)
原文:ZetCode
PyCairo 中的变换
在 PyCairo 图形编程教程的这一部分中,我们将讨论变换。
仿射变换由零个或多个线性变换(旋转,缩放或剪切)和平移(移位)组成。 几个线性变换可以组合成一个矩阵。 旋转是使刚体绕固定点移动的变换。 缩放比例是一种放大或缩小对象的变换。 比例因子在所有方向上都是相同的。 平移是一种变换,可以使每个点在指定方向上移动恒定的距离。 剪切是一种将对象垂直于给定轴移动的变换,该值在轴的一侧比另一侧更大。
数据来源:(wikipedia.org,freedictionary.com)
平移
以下示例描述了一个简单的平移。
def on_draw(self, wid, cr):
cr.set_source_rgb(0.2, 0.3, 0.8)
cr.rectangle(10, 10, 30, 30)
cr.fill()
cr.translate(20, 20)
cr.set_source_rgb(0.8, 0.3, 0.2)
cr.rectangle(0, 0, 30, 30)
cr.fill()
cr.translate(30, 30)
cr.set_source_rgb(0.8, 0.8, 0.2)
cr.rectangle(0, 0, 30, 30)
cr.fill()
cr.translate(40, 40)
cr.set_source_rgb(0.3, 0.8, 0.8)
cr.rectangle(0, 0, 30, 30)
cr.fill()
该示例绘制一个矩形。 然后,我们进行平移并再次绘制相同的矩形几次。
cr.translate(20, 20)
translate()
函数通过变换用户空间原点来修改当前变换矩阵。 在我们的例子中,我们在两个方向上将原点移动了 20 个单位。
图:平移 operation
剪切
在以下示例中,我们执行剪切操作。 剪切是沿特定轴的对象变形。 此操作没有剪切方法。 我们需要创建自己的变换矩阵。 注意,可以通过创建变换矩阵来执行每个仿射变换。
def on_draw(self, wid, cr):
cr.set_source_rgb(0.6, 0.6, 0.6)
cr.rectangle(20, 30, 80, 50)
cr.fill()
mtx = cairo.Matrix(1.0, 0.5,
0.0, 1.0,
0.0, 0.0)
cr.transform(mtx)
cr.rectangle(130, 30, 80, 50)
cr.fill()
在此代码示例中,我们执行一个简单的剪切操作。
mtx = cairo.Matrix(1.0, 0.5,
0.0, 1.0,
0.0, 0.0)
此变换将 y 值剪切为 x 值的 0.5。
cr.transform(mtx)
我们使用transform()
方法执行变换。
图:抖动 operation
缩放
下一个示例演示了缩放操作。 缩放是一种变换操作,其中对象被放大或缩小。
def on_draw(self, wid, cr):
cr.set_source_rgb(0.2, 0.3, 0.8)
cr.rectangle(10, 10, 90, 90)
cr.fill()
cr.scale(0.6, 0.6)
cr.set_source_rgb(0.8, 0.3, 0.2)
cr.rectangle(30, 30, 90, 90)
cr.fill()
cr.scale(0.8, 0.8)
cr.set_source_rgb(0.8, 0.8, 0.2)
cr.rectangle(50, 50, 90, 90)
cr.fill()
我们绘制三个90x90px
的矩形。 在其中两个上,我们执行缩放操作。
cr.scale(0.6, 0.6)
cr.set_source_rgb(0.8, 0.3, 0.2)
cr.rectangle(30, 30, 90, 90)
cr.fill()
我们将矩形均匀缩放 0.6 倍。
cr.scale(0.8, 0.8)
cr.set_source_rgb(0.8, 0.8, 0.2)
cr.rectangle(50, 50, 90, 90)
cr.fill()
在这里,我们以 0.8 的系数执行另一个缩放操作。 如果看图片,我们可以看到第三个黄色矩形是最小的一个。 即使我们使用了较小的比例因子。 这是因为变换操作是累加的。 实际上,第三个矩形的缩放比例为 0.528(0.6x0.8
)。
图:缩放 operation
隔离变换
变换操作是累加的。 为了将一个操作与另一个操作隔离开,我们可以使用save()
和restore()
方法。 save()
方法复制图形上下文的当前状态,并将其保存在保存状态的内部栈中。 restore()
方法将把上下文重新建立为保存状态。
def on_draw(self, wid, cr):
cr.set_source_rgb(0.2, 0.3, 0.8)
cr.rectangle(10, 10, 90, 90)
cr.fill()
cr.save()
cr.scale(0.6, 0.6)
cr.set_source_rgb(0.8, 0.3, 0.2)
cr.rectangle(30, 30, 90, 90)
cr.fill()
cr.restore()
cr.save()
cr.scale(0.8, 0.8)
cr.set_source_rgb(0.8, 0.8, 0.2)
cr.rectangle(50, 50, 90, 90)
cr.fill()
cr.restore()
在示例中,我们缩放了两个矩形。 这次我们将缩放操作相互隔离。
cr.save()
cr.scale(0.6, 0.6)
cr.set_source_rgb(0.8, 0.3, 0.2)
cr.rectangle(30, 30, 90, 90)
cr.fill()
cr.restore()
我们通过将scale()
方法放在save()
和restore()
方法之间来隔离缩放操作。
图:隔离转换
现在,第三个黄色矩形大于第二个红色矩形。
甜甜圈
在下面的示例中,我们通过旋转一堆椭圆来创建复杂的形状。
#!/usr/bin/python
'''
ZetCode PyCairo tutorial
This program creates a 'donut' shape
in PyCairo.
author: Jan Bodnar
website: zetcode.com
last edited: August 2012
'''
from gi.repository import Gtk
import cairo
import math
class Example(Gtk.Window):
def __init__(self):
super(Example, self).__init__()
self.init_ui()
def init_ui(self):
darea = Gtk.DrawingArea()
darea.connect("draw", self.on_draw)
self.add(darea)
self.set_title("Donut")
self.resize(350, 250)
self.set_position(Gtk.WindowPosition.CENTER)
self.connect("delete-event", Gtk.main_quit)
self.show_all()
def on_draw(self, wid, cr):
cr.set_line_width(0.5)
w, h = self.get_size()
cr.translate(w/2, h/2)
cr.arc(0, 0, 120, 0, 2*math.pi)
cr.stroke()
for i in range(36):
cr.save()
cr.rotate(i*math.pi/36)
cr.scale(0.3, 1)
cr.arc(0, 0, 120, 0, 2*math.pi)
cr.restore()
cr.stroke()
def main():
app = Example()
Gtk.main()
if __name__ == "__main__":
main()
我们将进行旋转和缩放操作。 我们还将保存和还原 PyCairo 上下文。
cr.translate(w/2, h/2)
cr.arc(0, 0, 120, 0, 2*math.pi)
cr.stroke()
在 GTK 窗口的中间,我们创建了一个圆。 这将是我们椭圆的边界圆。
for i in range(36):
cr.save()
cr.rotate(i*math.pi/36)
cr.scale(0.3, 1)
cr.arc(0, 0, 120, 0, 2*math.pi)
cr.restore()
cr.stroke()
我们沿着边界圆的路径创建了 36 个椭圆。 我们使用save()
和restore()
方法将每个旋转和缩放操作相互隔离。
图:多纳圈
星星
下一个示例显示了一个旋转和缩放的星星。
#!/usr/bin/python
'''
ZetCode PyCairo tutorial
This is a star example which
demonstrates scaling, translating and
rotating operations in PyCairo.
author: Jan Bodnar
website: zetcode.com
last edited: August 2012
'''
from gi.repository import Gtk, GLib
import cairo
class cv(object):
points = (
( 0, 85 ),
( 75, 75 ),
( 100, 10 ),
( 125, 75 ),
( 200, 85 ),
( 150, 125 ),
( 160, 190 ),
( 100, 150 ),
( 40, 190 ),
( 50, 125 ),
( 0, 85 )
)
SPEED = 20
TIMER_ID = 1
class Example(Gtk.Window):
def __init__(self):
super(Example, self).__init__()
self.init_ui()
self.init_vars()
def init_ui(self):
self.darea = Gtk.DrawingArea()
self.darea.connect("draw", self.on_draw)
self.add(self.darea)
self.set_title("Star")
self.resize(400, 300)
self.set_position(Gtk.WindowPosition.CENTER)
self.connect("delete-event", Gtk.main_quit)
self.show_all()
def init_vars(self):
self.angle = 0
self.scale = 1
self.delta = 0.01
GLib.timeout_add(cv.SPEED, self.on_timer)
def on_timer(self):
if self.scale < 0.01:
self.delta = -self.delta
elif self.scale > 0.99:
self.delta = -self.delta
self.scale += self.delta
self.angle += 0.01
self.darea.queue_draw()
return True
def on_draw(self, wid, cr):
w, h = self.get_size()
cr.set_source_rgb(0, 0.44, 0.7)
cr.set_line_width(1)
cr.translate(w/2, h/2)
cr.rotate(self.angle)
cr.scale(self.scale, self.scale)
for i in range(10):
cr.line_to(cv.points[i][0], cv.points[i][1])
cr.fill()
def main():
app = Example()
Gtk.main()
if __name__ == "__main__":
main()
在此示例中,我们创建一个星形对象。 我们将对其进行平移,旋转和缩放。
points = (
( 0, 85 ),
( 75, 75 ),
( 100, 10 ),
( 125, 75 ),
( 200, 85 ),
...
从这些点将构造星形对象。
def init_vars(self):
self.angle = 0
self.scale = 1
self.delta = 0.01
...
在init_vars()
方法中,我们初始化了三个变量。 self.angle
用于旋转,self.scale
用于缩放星形对象。 self.delta
变量控制星星何时生长和何时收缩。
glib.timeout_add(cv.SPEED, self.on_timer)
on_timer()
方法的每个cv.SPEED
ms 被调用。
if self.scale < 0.01:
self.delta = -self.delta
elif self.scale > 0.99:
self.delta = -self.delta
这些线控制星星是要增长还是要缩小。
cr.translate(w/2, h/2)
cr.rotate(self.angle)
cr.scale(self.scale, self.scale)
我们将星星移到窗口中间。 旋转并缩放比例。
for i in range(10):
cr.line_to(cv.points[i][0], cv.points[i][1])
cr.fill()
在这里,我们绘制星形对象。
在 PyCairo 教程的这一部分中,我们讨论了变换。
PyCairo 中的文字
在 PyCairo 教程的这一部分中,我们将处理文本。
灵魂伴侣
在第一个示例中,我们将在窗口上显示一些歌词。
def on_draw(self, wid, cr):
cr.set_source_rgb(0.1, 0.1, 0.1)
cr.select_font_face("Purisa", cairo.FONT_SLANT_NORMAL,
cairo.FONT_WEIGHT_NORMAL)
cr.set_font_size(13)
cr.move_to(20, 30)
cr.show_text("Most relationships seem so transitory")
cr.move_to(20, 60)
cr.show_text("They're all good but not the permanent one")
cr.move_to(20, 120)
cr.show_text("Who doesn't long for someone to hold")
cr.move_to(20, 150)
cr.show_text("Who knows how to love without being told")
cr.move_to(20, 180)
cr.show_text("Somebody tell me why I'm on my own")
cr.move_to(20, 210)
cr.show_text("If there's a soulmate for everyone")
在此代码中,我们显示了 Natasha Bedingfields Soulmate 歌曲的部分歌词。
cr.select_font_face("Purisa", cairo.FONT_SLANT_NORMAL,
cairo.FONT_WEIGHT_NORMAL)
在这里,我们选择字体。 该方法采用三个参数:字体系列,字体倾斜度和字体粗细。
cr.set_font_size(13)
在这里,我们指定字体大小。
cr.move_to(20, 30)
cr.show_text("Most relationships seem so transitory")
我们通过指定文本的位置并调用show_text()
方法在窗口上显示文本。
图:灵魂伴侣
居中文字
接下来,我们将展示如何在窗口上居中放置文本。
def on_draw(self, wid, cr):
w, h = self.get_size()
cr.select_font_face("Courier", cairo.FONT_SLANT_NORMAL,
cairo.FONT_WEIGHT_BOLD)
cr.set_font_size(60)
(x, y, width, height, dx, dy) = cr.text_extents("ZetCode")
cr.move_to(w/2 - width/2, h/2)
cr.show_text("ZetCode")
该代码将使文本在窗口上居中。 即使我们调整窗口大小,它仍然居中。
w, h = self.get_size()
为了使文本在窗口上居中,有必要获取窗口工作区的大小。
cr.select_font_face("Courier", cairo.FONT_SLANT_NORMAL,
cairo.FONT_WEIGHT_BOLD)
cr.set_font_size(60)
我们选择要显示的字体及其大小。
(x, y, width, height, dx, dy) = cr.text_extents("ZetCode")
我们得到了文本范围。 这些是描述文字的数字。 我们的示例需要文本的宽度。
cr.move_to(w/2 - width/2, h/2)
cr.show_text("ZetCode")
我们将文本放置在窗口的中间,并使用show_text()
方法显示它。
图:居中文本
带阴影的文字
现在,我们将在窗口上创建一个阴影文本。
def on_draw(self, wid, cr):
cr.select_font_face("Serif", cairo.FONT_SLANT_NORMAL,
cairo.FONT_WEIGHT_BOLD)
cr.set_font_size(50)
cr.set_source_rgb(0, 0, 0)
cr.move_to(40, 60)
cr.show_text("ZetCode")
cr.set_source_rgb(0.5, 0.5, 0.5)
cr.move_to(43, 63)
cr.show_text("ZetCode")
要创建阴影,我们将文本绘制两次。 以不同的颜色。 第二个文本向右和向下移动一点。
cr.set_source_rgb(0, 0, 0)
cr.move_to(40, 60)
cr.show_text("ZetCode")
第一个文本用黑色墨水绘制。 它充当阴影。
cr.set_source_rgb(0.5, 0.5, 0.5)
cr.move_to(43, 63)
cr.show_text("ZetCode")
第二个文本用灰色墨水绘制。 它向右和向下移动了 3px。
图:阴影文本
渐变填充文本
以下示例将产生很好的效果。 我们将使用一些线性渐变填充文本。
def on_draw(self, wid, cr):
cr.set_source_rgb(0.2, 0.2, 0.2)
cr.paint()
h = 90
cr.select_font_face("Serif", cairo.FONT_SLANT_ITALIC,
cairo.FONT_WEIGHT_BOLD)
cr.set_font_size(h)
lg = cairo.LinearGradient(0, 15, 0, h*0.8)
lg.set_extend(cairo.EXTEND_REPEAT)
lg.add_color_stop_rgb(0.0, 1, 0.6, 0)
lg.add_color_stop_rgb(0.5, 1, 0.3, 0)
cr.move_to(15, 80)
cr.text_path("ZetCode")
cr.set_source(lg)
cr.fill()
我们在充满线性渐变的窗口上绘制文本。 颜色是一些橙色。
cr.set_source_rgb(0.2, 0.2, 0.2)
cr.paint()
为了使其更具视觉吸引力,我们将背景涂成深灰色。
lg = cairo.LinearGradient(0, 15, 0, h*0.8)
lg.set_extend(cairo.EXTEND_REPEAT)
lg.add_color_stop_rgb(0.0, 1, 0.6, 0)
lg.add_color_stop_rgb(0.5, 1, 0.3, 0)
将创建线性渐变。
cr.move_to(15, 80)
cr.text_path("ZetCode")
cr.set_source(lg)
cr.fill()
文本显示在窗口上。 我们使用渐变作为绘画源。
图:用渐变填充的文本
逐个字母
为此,我们将逐个字母显示一个文本。 这些字母将被绘制得有些延迟。
#!/usr/bin/python
'''
ZetCode PyCairo tutorial
This program shows text letter by
letter.
author: Jan Bodnar
website: zetcode.com
last edited: August 2012
'''
from gi.repository import Gtk, GLib
import cairo
class cv(object):
SPEED = 800
TEXT_SIZE = 35
COUNT_MAX = 8
class Example(Gtk.Window):
def __init__(self):
super(Example, self).__init__()
self.init_ui()
self.init_vars()
def init_ui(self):
self.darea = Gtk.DrawingArea()
self.darea.connect("draw", self.on_draw)
self.add(self.darea)
GLib.timeout_add(cv.SPEED, self.on_timer)
self.set_title("Letter by letter")
self.resize(350, 200)
self.set_position(Gtk.WindowPosition.CENTER)
self.connect("delete-event", Gtk.main_quit)
self.show_all()
def init_vars(self):
self.timer = True
self.count = 0
self.text = [ "Z", "e", "t", "C", "o", "d", "e" ]
def on_timer(self):
if not self.timer: return False
self.darea.queue_draw()
return True
def on_draw(self, wid, cr):
cr.select_font_face("Courier", cairo.FONT_SLANT_NORMAL,
cairo.FONT_WEIGHT_BOLD)
cr.set_font_size(cv.TEXT_SIZE)
dis = 0
for i in range(self.count):
(x, y, width, height, dx, dy) = cr.text_extents(self.text[i])
dis += width + 2
cr.move_to(dis + 30, 50)
cr.show_text(self.text[i])
self.count += 1
if self.count == cv.COUNT_MAX:
self.timer = False
self.count = 0
def main():
app = Example()
Gtk.main()
if __name__ == "__main__":
main()
在我们的示例中,我们将在 GTK 窗口上逐个字母地绘制"ZetCode"
字符串,并稍作延迟。
self.text = [ "Z", "e", "t", "C", "o", "d", "e" ]
这是要在窗口上显示的字母列表。
cr.select_font_face("Courier", cairo.FONT_SLANT_NORMAL,
cairo.FONT_WEIGHT_BOLD)
我们选择粗体的 Courier 字体。
for i in range(self.count):
(x, y, width, height, dx, dy) = cr.text_extents(self.text[i])
dis += width + 2
cr.move_to(dis + 30, 50)
cr.show_text(self.text[i])
在这里,我们逐个字母地绘制文本。 我们获得每个字母的宽度并计算 x 轴上的距离。
字形
show_text()
方法仅适用于简单的文本呈现。 Cairo 开发者将其称为玩具方法。 使用字形可以完成更专业的文本渲染。 标志符号是图形符号,可提供字符形式。 字符提供含义。 它可以有多个字形。 角色没有内在的外观。 字形没有内在的含义。
请注意,Pango 库解决了许多常见的编程要求,包括文本。
def on_draw(self, wid, cr):
cr.select_font_face("Serif", cairo.FONT_SLANT_NORMAL,
cairo.FONT_WEIGHT_NORMAL)
cr.set_font_size(13)
glyphs = []
index = 0
for y in range(20):
for x in range(35):
glyphs.append((index, x*15 + 20, y*18 + 20))
index += 1
cr.show_glyphs(glyphs)
该代码显示了所选字体的 700 个字形。
glyphs = []
字形列表将存储三个整数值。 第一个值是字形到所选字体类型的索引。 第二和第三值是字形的 x,y 位置。
cr.show_glyphs(glyphs)
show_glyphs()
方法在窗口上显示字形。
图:字形
本章介绍了 PyCairo 中的文本。
PyCairo 中的图像
在 PyCairo 教程的这一部分中,我们将讨论图像。 我们将展示如何在 GTK 窗口上显示 PNG 和 JPEG 图像。 我们还将在图像上绘制一些文本。
显示 PNG 图像
在第一个示例中,我们将显示一个 PNG 图像。
#!/usr/bin/python
'''
ZetCode PyCairo tutorial
This program shows how to draw
an image on a GTK window in PyCairo.
author: Jan Bodnar
website: zetcode.com
last edited: August 2012
'''
from gi.repository import Gtk
import cairo
class Example(Gtk.Window):
def __init__(self):
super(Example, self).__init__()
self.init_ui()
self.load_image()
def init_ui(self):
darea = Gtk.DrawingArea()
darea.connect("draw", self.on_draw)
self.add(darea)
self.set_title("Image")
self.resize(300, 170)
self.set_position(Gtk.WindowPosition.CENTER)
self.connect("delete-event", Gtk.main_quit)
self.show_all()
def load_image(self):
self.ims = cairo.ImageSurface.create_from_png("stmichaelschurch.png")
def on_draw(self, wid, cr):
cr.set_source_surface(self.ims, 10, 10)
cr.paint()
def main():
app = Example()
Gtk.main()
if __name__ == "__main__":
main()
该示例显示图像。
self.ims = cairo.ImageSurface.create_from_png("stmichaelschurch.png")
我们从 PNG 图像创建图像表面。
cr.set_source_surface(self.ims, 10, 10)
我们为先前创建的图像表面设置了绘画源。
cr.paint()
我们在窗口上绘制源。
图:显示图像
显示 JPEG 图像
PyCairo 仅对 PNG 图像提供内置支持。 可以通过GdkPixbuf.Pixbuf
对象显示其他图像。 它是用于处理图像的 GTK 对象。
#!/usr/bin/python
'''
ZetCode PyCairo tutorial
This program shows how to draw
an image on a GTK window in PyCairo.
author: Jan Bodnar
website: zetcode.com
last edited: August 2012
'''
from gi.repository import Gtk, Gdk, GdkPixbuf
import cairo
class Example(Gtk.Window):
def __init__(self):
super(Example, self).__init__()
self.init_ui()
self.load_image()
def init_ui(self):
darea = Gtk.DrawingArea()
darea.connect("draw", self.on_draw)
self.add(darea)
self.set_title("Image")
self.resize(300, 170)
self.set_position(Gtk.WindowPosition.CENTER)
self.connect("delete-event", Gtk.main_quit)
self.show_all()
def load_image(self):
self.pb = GdkPixbuf.Pixbuf.new_from_file("stmichaelschurch.jpg")
def on_draw(self, wid, cr):
Gdk.cairo_set_source_pixbuf(cr, self.pb, 5, 5)
cr.paint()
def main():
app = Example()
Gtk.main()
if __name__ == "__main__":
main()
在此示例中,我们在窗口上显示 JPEG 图像。
from gi.repository import Gtk, Gdk, GdkPixbuf
除了Gtk
,我们还将需要Gdk
和GdkPixbuf
模块。
self.pb = GdkPixbuf.Pixbuf.new_from_file("stmichaelschurch.jpg")
我们从 JPEG 文件创建一个GdkPixbuf.Pixbuf
。
Gdk.cairo_set_source_pixbuf(cr, self.pb, 5, 5)
cr.paint()
Gdk.cairo_set_source_pixbuf()
方法将pixbuf
设置为绘画源。
水印
在图像上绘制信息是很常见的。 写在图像上的文本称为水印。 水印用于识别图像。 它们可能是版权声明或图像创建时间。
#!/usr/bin/python
'''
ZetCode PyCairo tutorial
This program draws a watermark
on an image.
author: Jan Bodnar
website: zetcode.com
last edited: August 2012
'''
from gi.repository import Gtk
import cairo
class Example(Gtk.Window):
def __init__(self):
super(Example, self).__init__()
self.init_ui()
self.load_image()
self.draw_mark()
def init_ui(self):
darea = Gtk.DrawingArea()
darea.connect("draw", self.on_draw)
self.add(darea)
self.set_title("Watermark")
self.resize(350, 250)
self.set_position(Gtk.WindowPosition.CENTER)
self.connect("delete-event", Gtk.main_quit)
self.show_all()
def load_image(self):
self.ims = cairo.ImageSurface.create_from_png("beckov.png")
def draw_mark(self):
cr = cairo.Context(self.ims)
cr.set_font_size(11)
cr.set_source_rgb(0.9 , 0.9 , 0.9)
cr.move_to(20 , 30)
cr.show_text(" Beckov 2012 , (c) Jan Bodnar ")
cr.stroke()
def on_draw(self, wid, cr):
cr.set_source_surface(self.ims, 10, 10)
cr.paint()
def main():
app = Example()
Gtk.main()
if __name__ == "__main__":
main()
我们在图像上绘制版权信息。
def load_image(self):
self.ims = cairo.ImageSurface.create_from_png("beckov.png")
在load_image()
方法中,我们从 PNG 图像创建图像表面。
def draw_mark(self):
cr = cairo.Context(self.ims)
...
在draw_mark()
方法中,我们在图像上绘制版权信息。 首先,我们从图像表面创建一个绘图上下文。
cr.set_font_size(11)
cr.set_source_rgb(0.9 , 0.9 , 0.9)
cr.move_to(20 , 30)
cr.show_text(" Beckov 2012 , (c) Jan Bodnar ")
cr.stroke()
然后,我们用白色绘制一个小的文本。
def on_draw(self, wid, cr):
cr.set_source_surface(self.ims, 10, 10)
cr.paint()
最后,在窗口上绘制图像表面。
图:水印
本章介绍了 PyCairo 中的图像。
根窗口
在 PyCairo 教程的这一部分中,我们将使用根窗口。 根窗口是我们通常具有图标快捷方式的桌面窗口。
可以使用根窗口进行操作。 从程序员的角度来看,它只是一种特殊的窗口。
透明窗口
我们的第一个示例将创建一个透明窗口。 我们将看到窗口对象下方的内容。
#!/usr/bin/python
'''
ZetCode PyCairo tutorial
This code example shows how to
create a transparent window.
author: Jan Bodnar
website: zetcode.com
last edited: August 2012
'''
from gi.repository import Gtk
import cairo
class Example(Gtk.Window):
def __init__(self):
super(Example, self).__init__()
self.tran_setup()
self.init_ui()
def init_ui(self):
self.connect("draw", self.on_draw)
self.set_title("Transparent window")
self.resize(300, 250)
self.set_position(Gtk.WindowPosition.CENTER)
self.connect("delete-event", Gtk.main_quit)
self.show_all()
def tran_setup(self):
self.set_app_paintable(True)
screen = self.get_screen()
visual = screen.get_rgba_visual()
if visual != None and screen.is_composited():
self.set_visual(visual)
def on_draw(self, wid, cr):
cr.set_source_rgba(0.2, 0.2, 0.2, 0.4)
cr.set_operator(cairo.OPERATOR_SOURCE)
cr.paint()
def main():
app = Example()
Gtk.main()
if __name__ == "__main__":
main()
为了创建透明窗口,我们获得了屏幕对象的视觉效果并将其设置为我们的窗口。 在on_draw()
方法中,我们绘制屏幕的可视对象。 这造成了部分透明的幻觉。
self.set_app_paintable(True)
我们必须设置要绘制的应用。
screen = self.get_screen()
get_screen()
方法返回屏幕对象。
visual = screen.get_rgba_visual()
从屏幕窗口中,我们可以看到它。 视觉内容包含低级显示信息。
if visual != None and screen.is_composited():
self.set_visual(visual)
并非所有的显示器都支持此操作。 因此,我们检查屏幕是否支持合成并且返回的视觉效果不是NULL
。 我们将屏幕的视觉效果设置为窗口的视觉效果。
def on_draw(self, wid, cr):
cr.set_source_rgba(0.2, 0.2, 0.2, 0.4)
cr.set_operator(cairo.OPERATOR_SOURCE)
cr.paint()
我们使用部分透明的源来绘制屏幕窗口。 cairo.OPERATOR_SOURCE
在我们绘制源代码的地方创建了合成操作。 这是屏幕窗口。 为了获得完全透明,我们将 alpha 值设置为 0 或使用cairo.OPERATOR_CLEAR
运算符。
图:透明窗口
截屏
根窗口对于截图也是必不可少的。
#!/usr/bin/python
'''
ZetCode PyCairo tutorial
This code example takes a screenshot.
author: Jan Bodnar
website: zetcode.com
last edited: August 2012
'''
from gi.repository import Gdk
import cairo
def main():
root_win = Gdk.get_default_root_window()
width = root_win.get_width()
height = root_win.get_height()
ims = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
pb = Gdk.pixbuf_get_from_window(root_win, 0, 0, width, height)
cr = cairo.Context(ims)
Gdk.cairo_set_source_pixbuf(cr, pb, 0, 0)
cr.paint()
ims.write_to_png("screenshot.png")
if __name__ == "__main__":
main()
该示例捕获整个屏幕的快照。
root_win = Gdk.get_default_root_window()
我们通过Gdk.get_default_root_window()
方法调用获得了根窗口。
width = root_win.get_width()
height = root_win.get_height()
我们确定根窗口的宽度和高度。
ims = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
空的图像表面被创建。 它具有根窗口的大小。
pb = Gdk.pixbuf_get_from_window(root_win, 0, 0, width, height)
我们使用Gdk.pixbuf_get_from_window()
方法调用从根窗口中获得一个pixbuf
。 pixbuf
是描述内存中图像的对象。 GTK 库使用它。
cr = cairo.Context(ims)
Gdk.cairo_set_source_pixbuf(cr, pb, 0, 0)
cr.paint()
在上述代码行中,我们在之前创建的图像表面上创建了 Cairo 绘图上下文。 我们将 pixbuf 放在绘图上下文上并将其绘制在表面上。
ims.write_to_png("screenshot.png")
使用write_to_png()
方法将图像表面写入 PNG 图像。
显示信息
在第三个示例中,我们将在桌面窗口上显示一条消息。
#!/usr/bin/python
'''
ZetCode PyCairo tutorial
This code example shows a message on the desktop
window.
author: Jan Bodnar
website: zetcode.com
last edited: August 2012
'''
from gi.repository import Gtk, Gdk, Pango
import cairo
class Example(Gtk.Window):
def __init__(self):
super(Example, self).__init__()
self.setup()
self.init_ui()
def setup(self):
self.set_app_paintable(True)
self.set_type_hint(Gdk.WindowTypeHint.DOCK)
self.set_keep_below(True)
screen = self.get_screen()
visual = screen.get_rgba_visual()
if visual != None and screen.is_composited():
self.set_visual(visual)
def init_ui(self):
self.connect("draw", self.on_draw)
lbl = Gtk.Label()
text = "ZetCode, tutorials for programmers."
lbl.set_text(text)
fd = Pango.FontDescription("Serif 20")
lbl.modify_font(fd)
lbl.modify_fg(Gtk.StateFlags.NORMAL,Gdk.color_parse("white"))
self.add(lbl)
self.resize(300, 250)
self.set_position(Gtk.WindowPosition.CENTER)
self.connect("delete-event", Gtk.main_quit)
self.show_all()
def on_draw(self, wid, cr):
cr.set_operator(cairo.OPERATOR_CLEAR)
cr.paint()
cr.set_operator(cairo.OPERATOR_OVER)
def main():
app = Example()
Gtk.main()
if __name__ == "__main__":
import signal
signal.signal(signal.SIGINT, signal.SIG_DFL)
main()
该代码在根窗口上显示消息标签。
self.set_app_paintable(True)
我们将操纵应用窗口,因此我们使其可绘制。
self.set_type_hint(Gdk.WindowTypeHint.DOCK)
实现此窗口提示会删除窗口边框和装饰。
self.set_keep_below(True)
我们始终将应用始终放在根窗口的底部。
screen = self.get_screen()
visual = screen.get_rgba_visual()
if visual != None and screen.is_composited():
self.set_visual(visual)
我们将屏幕的外观设置为应用的外观。
lbl = Gtk.Label()
text = "ZetCode, tutorials for programmers."
lbl.set_text(text)
我们在应用窗口上放置一个消息标签。
fd = Pango.FontDescription("Serif 20")
lbl.modify_font(fd)
lbl.modify_fg(Gtk.StateFlags.NORMAL,Gdk.color_parse("white"))
在 Pango 模块的帮助下,我们更改了文本的外观。
def on_draw(self, wid, cr):
cr.set_operator(cairo.OPERATOR_CLEAR)
cr.paint()
cr.set_operator(cairo.OPERATOR_OVER)
我们使用cairo.OPERATOR_CLEAR
运算符清除窗口背景。 然后我们设置cairo.OPERATOR_CLEAR
以绘制标签窗口小部件。
if __name__ == "__main__":
import signal
signal.signal(signal.SIGINT, signal.SIG_DFL)
main()
有一个较旧的错误,该错误不允许我们使用 Ctrl + C
快捷方式终止从终端启动的应用。 为此添加两行是一种解决方法。
图:根窗口上的消息
在本章中,我们使用了 PyCairo 中的桌面窗口。
HTML5 画布教程
这是 HTML5 画布教程。 在本教程中,我们将学习 HTML5 canvas 中 JavaScript 编程的基础。 HTML5 canvas 教程适合初学者和中级程序员。
目录
HTML5 画布
画布是 HTML5 元素,可呈现 2D 形状和位图图像。 画布上的绘图使用 JavaScript 语言执行。
相关教程
Java 2D 教程讲授 Java 2D 图形。 Cairo 教程用 Cairo 库和 C 语言讲授 2D 图形。
介绍
HTML5 画布教程的这一部分是对 JavaScript 语言的 HTML5 画布编程的介绍。
关于
这是 HTML5 画布教程。 它是针对初学者的。 本教程将教您 HTML5 canvas
元素的 JavaScript 图形编程基础。 可以在此处下载本教程中使用的图像。
HTML5 画布元素提供了可同时处理矢量和栅格图形的工具。
HTML5 画布
HTML5 canvas
元素提供了一个与分辨率有关的位图区域,该区域可用于动态绘制图形,游戏图形,艺术作品或其他可视图像。 简单来说,canvas
是 HTML5 中的新元素,它使您可以使用 JavaScript 绘制图形。 Canvas
无需将插件插入 Flash,Silverlight 或 Java,即可将动画带入网页。
HTML5 canvas
最初由 Apple 于 2004 年推出,用于 Mac OS X WebKit,以为仪表板应用及其 Safari Web 浏览器提供动力。 从那时起,它已被 Mozilla 和 Opera 所采用。 后来,W3C 在 HTML5 规范中采用了它。 如今,所有现代 Web 浏览器都支持它。
画布上下文
canvas
上下文是一个对象,它公开画布 API 来执行绘图。 它提供对象,方法和属性,以在画布绘图表面上绘制和操纵图形。 使用getContext()
方法检索上下文。 方法的参数指定所需的 API:用于二维图形的"2d"
或用于二维和三维图形的"webgl"
。 如果不支持给定的上下文 ID,则返回null
。
画一个矩形
我们创建一个简单的 HTML5 canvas
图形渲染示例。
rectangle.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 canvas rectangle</title>
<script>
function draw() {
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
ctx.fillStyle = "cadetblue";
ctx.fillRect(0, 0, 100, 100);
}
</script>
</head>
<body onload="draw();">
<canvas id="myCanvas" width="250" height="150">
</canvas>
</body>
</html>
该代码示例在网页的左上角绘制一个cadetblue
矩形。
<!DOCTYPE html>
文档类型声明或DOCTYPE
是对 Web 浏览器的有关 HTML 文档性质的指令。 这个特定的声明告诉浏览器该网页是 HTML5 文档。 canvas
元素最初是在 HTML5 标准中引入的。
<script>
function draw() {
...
}
</script>
在自定义draw()
函数中执行绘制。 加载 HTML 文档的正文时会调用它。
var canvas = document.getElementById('myCanvas');
通过getElementById()
方法,我们获得对canvas
元素的引用。
var ctx = canvas.getContext('2d');
使用getContext()
方法检索渲染上下文。
ctx.fillStyle = "cadetblue";
上下文的fillStyle
属性指定在内部形状中使用的颜色或样式。 然后在后续的绘图操作中使用该样式。
ctx.fillRect(0, 0, 100, 100);
我们用指定的颜色绘制一个矩形。 矩形的大小在方法的参数中给出。 前两个参数是 x 和 y 坐标。 接下来的两个参数是矩形的宽度和高度。
<body onload="draw();">
onload
属性为窗口的加载事件定义事件处理器。 加载事件在文档加载过程结束时触发。 至此,文档中的所有对象都在 DOM 中,并且所有图像,脚本,链接和子框架均已完成加载。 在我们的例子中,我们调用draw()
方法,该方法在画布上执行绘制。
<canvas id="myCanvas" width="250" height="150">
</canvas>
canvas
元素是使用<canvas>
和</canvas>
标签创建的。 width
和height
属性设置页面内 canvas 元素的大小。 id
属性标识 DOM 层次结构中的元素。
图:HTML5 画布矩形
阴影
HTML5 画布包含用于创建阴影的属性。
shadow.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 canvas shadow</title>
<script>
function draw() {
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 5;
ctx.shadowBlur = 4;
ctx.shadowColor = "#888";
ctx.fillStyle = "#000000";
ctx.fillRect(10, 10, 80, 80);
}
</script>
</head>
<body onload="draw();">
<canvas id="myCanvas" width="250" height="150">
</canvas>
</body>
</html>
在示例中,我们在矩形下方创建阴影。
ctx.shadowOffsetX = 5;
shadowOffsetX
属性指定阴影在水平方向上偏移的距离。
ctx.shadowOffsetY = 5;
shadowOffsetY
属性指定阴影在垂直方向上将偏移的距离。
ctx.shadowBlur = 4;
shadowBlur
属性指定模糊效果的级别。
ctx.shadowColor = "#888";
shadowColor
属性指定阴影的颜色。
图:HTML5 画布阴影
参考
以下资源用于创建本教程:
HTML5 画布教程的这一部分是对 HTML5 画布的 JavaScript 编程的介绍。
HTML5 画布中的直线
线是简单的图形基元。 线是连接两个点的对象。
在 HTML5 画布中,使用path
对象创建一行。 路径是由线段连接的点的列表,这些线段可以具有不同的形状(弯曲或不弯曲),不同的宽度和不同的颜色。 在路径对象内,使用lineTo()
方法创建一条线。
直线
下面的示例绘制两条线。
lines.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 canvas lines</title>
<script>
function draw() {
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
ctx.beginPath();
ctx.moveTo(20, 20);
ctx.lineTo(250, 150);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(20, 20);
ctx.lineTo(250, 250);
ctx.lineWidth = 5;
ctx.stroke();
}
</script>
</head>
<body onload="draw();">
<canvas id="myCanvas" width="350" height="350">
</canvas>
</body>
</html>
画了两条线。 第二行较粗。
ctx.beginPath();
beginPath()
方法创建一个新路径。 创建后,随后的绘图命令将直接进入路径并用于构建路径。
ctx.moveTo(20, 20);
moveTo()
方法将笔移动到 x 和 y 指定的坐标。
ctx.lineTo(250, 150);
lineTo()
方法从当前绘制位置到 x 和 y 指定的位置绘制一条线。
ctx.stroke();
stroke()
方法通过描边轮廓绘制线条。
ctx.lineWidth = 5;
lineWidth
设置第二行的宽度; 线较粗。
图:直线
模糊直线
具有奇数宽度的线似乎模糊。 这是因为这些线是在画布的网格线之间绘制的。 有一个快速的解决方案-将坐标移动半个单位。
crisp_lines.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 canvas crisp lines</title>
<script>
function draw() {
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
ctx.lineWidth = 1
ctx.beginPath();
ctx.moveTo(20, 20);
ctx.lineTo(250, 20);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(20, 40);
ctx.lineTo(250, 40);
ctx.stroke();
ctx.translate(0.5, 0.5);
ctx.beginPath();
ctx.moveTo(20, 60);
ctx.lineTo(250, 60);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(20, 80);
ctx.lineTo(250, 80);
ctx.stroke();
}
</script>
</head>
<body onload="draw();">
<canvas id="myCanvas" width="350" height="350">
</canvas>
</body>
</html>
该示例绘制了四行。 前两个略微模糊,其他两个更平滑。
ctx.lineWidth = 1
我们有一条宽度奇数的线。
ctx.translate(0.5, 0.5);
这是消除线条模糊的快速解决方案。 translate()
方法将坐标系移动半个单位。
图:酥脆的线
笔划线
可以使用各种笔划线来绘制线。 笔划线是通过混合不透明部分和透明部分而创建的图案。 使用画布上下文的setLineDash()
方法指定笔划线。
line_dashes.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 canvas line dashes</title>
<script>
function draw() {
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
ctx.translate(0.5, 0.5);
ctx.beginPath();
ctx.setLineDash([2]);
ctx.moveTo(10, 10);
ctx.lineTo(250, 10);
ctx.stroke();
ctx.beginPath();
ctx.setLineDash([7, 2]);
ctx.moveTo(10, 20);
ctx.lineTo(250, 20);
ctx.stroke();
ctx.beginPath();
ctx.setLineDash([4, 4, 1]);
ctx.moveTo(10, 30);
ctx.lineTo(250, 30);
ctx.stroke();
}
</script>
</head>
<body onload="draw();">
<canvas id="myCanvas" width="350" height="250">
</canvas>
</body>
</html>
该示例绘制了三条具有不同笔划线图案的线。
ctx.setLineDash([2]);
这条线将交替显示 2 个坐标单位的不透明和透明部分。
ctx.setLineDash([4, 4, 1]);
在这里,笔划线由以下模式组成:4 个单位已绘制,4 个未绘制单位,1 个已绘制单位。
图:虚线
端盖
端盖是应用于未封闭子路径和破折线段末端的装饰。 Java 2D 中有三种不同的端盖:'square'
,'round'
和'butt'
。
'butt'
- 结束未封闭的子路径和虚线段,不添加任何修饰。'round'
- 用圆形装饰结束未封闭的子路径和虚线段,该圆形装饰的半径等于笔的宽度的一半。'square'
- 以方形投影结束未封闭的子路径和虚线段,该方形投影超出段的末端并延伸到等于线宽一半的距离。
line_caps.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 canvas line caps</title>
<script>
function draw() {
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
ctx.translate(0.5, 0.5);
ctx.lineWidth = 8;
ctx.beginPath();
ctx.lineCap = 'square';
ctx.moveTo(10, 10);
ctx.lineTo(250, 10);
ctx.stroke();
ctx.beginPath();
ctx.lineCap = 'round';
ctx.moveTo(10, 30);
ctx.lineTo(250, 30);
ctx.stroke();
ctx.beginPath();
ctx.lineCap = 'butt';
ctx.moveTo(10, 50);
ctx.lineTo(250, 50);
ctx.stroke();
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(10, 0);
ctx.lineTo(10, 60);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(250, 0);
ctx.lineTo(250, 60);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(254, 0);
ctx.lineTo(254, 60);
ctx.stroke();
}
</script>
</head>
<body onload="draw();">
<canvas id="myCanvas" width="350" height="250">
</canvas>
</body>
</html>
在我们的示例中,我们显示了所有三种类型的端盖。
ctx.lineWidth = 8;
我们增加线条的宽度,以便更好地看到盖帽。
ctx.lineCap = 'square';
线宽由lineCap
上下文属性指定。
图:直线的端帽
垂直线强调线的大小差异。
连接
线连接是应用于两个路径段的交点以及子路径端点的交点的修饰。 一共有三种装饰:'bevel'
,'miter'
和'round'
。
'bevel'
- 通过将宽轮廓的外角与直线段相连来连接路径段。'miter'
- 通过扩展路径段的外部边缘直到它们交汇来连接路径段。'round'
- 通过以线宽一半的半径四舍五入拐角来连接路径段。
line_joins.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 canvas line joins</title>
<script>
function draw() {
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
ctx.translate(0.5, 0.5);
ctx.lineWidth = 8;
ctx.lineJoin = 'miter';
ctx.strokeRect(10, 10, 100, 100);
ctx.lineJoin = 'bevel';
ctx.strokeRect(130, 10, 100, 100);
ctx.lineJoin = 'round';
ctx.strokeRect(260, 10, 100, 100);
}
</script>
</head>
<body onload="draw();">
<canvas id="myCanvas" width="450" height="350">
</canvas>
</body>
</html>
此代码示例显示了三个不同的线联接在起作用。
ctx.lineWidth = 8;
用细线很难分辨连接类型之间的区别。 因此,通过将lineWidth
设置为 8 个单位,可以使线条更粗。
ctx.lineJoin = 'miter';
线连接使用lineJoin
属性设置。
图:Joins
贝塞尔曲线
贝塞尔曲线是由数学公式定义的曲线(样条线)。 绘制曲线的数学方法由 PierreBézier 在 1960 年代后期创建,用于雷诺的汽车制造。
画布上下文的bezierCurveTo()
方法将三次贝塞尔曲线添加到路径。 它需要三个点:前两个点是控制点,第三个点是终点。 起点是当前路径中的最后一个点,可以在创建贝塞尔曲线之前使用moveTo()
对其进行更改。 通过移动控制点来修改形状。
bezier_curve.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 canvas Bézier curve</title>
<meta charset="utf-8">
<script>
function draw() {
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
ctx.beginPath();
ctx.moveTo(20, 60);
ctx.bezierCurveTo(80, 20, 180, 160, 250, 50);
ctx.stroke();
ctx.fillStyle = 'cadetblue';
ctx.fillRect(80, 20, 4, 4);
ctx.fillRect(180, 160, 4, 4);
}
</script>
</head>
<body onload="draw();">
<canvas id="myCanvas" width="350" height="350">
</canvas>
</body>
</html>
该示例绘制了一条贝塞尔曲线。
ctx.moveTo(20, 60);
通过moveTo()
方法,我们定义了曲线的起点。
ctx.bezierCurveTo(80, 20, 180, 160, 250, 50);
使用bezierCurveTo()
方法,我们在路径上添加了贝塞尔曲线。 前两个点是控制点; 最后一点是曲线的终点。
ctx.fillStyle = 'cadetblue';
ctx.fillRect(80, 20, 4, 4);
ctx.fillRect(180, 160, 4, 4);
这些线绘制曲线的控制点。
图:贝塞尔曲线
在 HTML5 画布教程的这一部分中,我们使用了线条。
HTML5 画布形状
在 HTML5 画布教程的这一部分中,我们创建一些基本的和更高级的几何形状。
长方形
第一个程序绘制两个矩形。
rectangles.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 canvas rectangles</title>
<script>
function draw() {
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
ctx.fillStyle = 'gray';
ctx.fillRect(10, 10, 60, 60);
ctx.fillRect(100, 10, 100, 60);
}
</script>
</head>
<body onload="draw();">
<canvas id="myCanvas" width="350" height="250">
</canvas>
</body>
</html>
该示例使用drawRect()
方法绘制矩形。
ctx.fillStyle = 'gray';
矩形的内部被涂成灰色。
ctx.fillRect(10, 10, 60, 60);
ctx.fillRect(100, 10, 100, 60);
fillRect()
方法用于绘制正方形和矩形。 前两个参数是要绘制的形状的 x 和 y 坐标。 最后两个参数是形状的宽度和高度。
图:矩形
基本形状
在下面的程序中,我们绘制一些基本形状。
shapes.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 canvas shapes</title>
<script>
function draw() {
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
ctx.fillStyle = 'gray';
ctx.fillRect(10, 10, 60, 60);
ctx.fillRect(100, 10, 90, 60);
ctx.beginPath();
ctx.arc(250, 40, 32, 0, 2*Math.PI);
ctx.fill();
ctx.beginPath();
ctx.moveTo(10, 160);
ctx.lineTo(90, 160);
ctx.lineTo(50, 110);
ctx.closePath();
ctx.fill();
ctx.save();
ctx.scale(2, 1);
ctx.beginPath();
ctx.arc(72, 130, 25, 0, 2*Math.PI);
ctx.fill();
ctx.restore();
ctx.beginPath();
ctx.arc(250, 120, 40, 0, Math.PI);
ctx.fill();
}
</script>
</head>
<body onload="draw();">
<canvas id="myCanvas" width="350" height="350">
</canvas>
</body>
</html>
画布上绘制了六个不同的形状。
ctx.fillStyle = 'gray';
形状将被涂成灰色。
ctx.fillRect(10, 10, 60, 60);
ctx.fillRect(100, 10, 90, 60);
使用fillRect()
方法绘制矩形。 矩形是唯一未通过beginPath()
方法启动的形状。
ctx.beginPath();
ctx.arc(250, 40, 32, 0, 2*Math.PI);
ctx.fill();
用arc()
方法绘制一个圆。 该方法将弧添加到创建的路径。 前两个参数定义圆弧中心点的 x 和 y 坐标。 接下来的两个参数指定圆弧的起点和终点。 角度以弧度定义。 最后一个参数是可选的; 它指定绘制圆弧的方向。 默认方向为顺时针。
ctx.beginPath();
ctx.moveTo(10, 160);
ctx.lineTo(90, 160);
ctx.lineTo(50, 110);
ctx.closePath();
ctx.fill();
使用moveTo()
和lineTo()
方法,我们绘制了一个三角形。 closePath()
方法使笔移回当前子路径的起点。 在我们的例子中,它完成了三角形的形状。
ctx.save();
ctx.scale(2, 1);
ctx.beginPath();
ctx.arc(72, 130, 25, 0, 2*Math.PI);
ctx.fill();
ctx.restore();
通过缩放圆形来绘制椭圆形。 这些操作位于save()
和restore()
方法之间,因此缩放操作不会影响后续图形。
ctx.beginPath();
ctx.arc(250, 120, 40, 0, Math.PI);
ctx.fill();
最后的形状(半圆)用arc()
方法绘制。
图:基本形状
饼形图
饼图是圆形图,将其分成多个切片以说明数值比例。
piechart.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 canvas pie chart</title>
<style>
canvas {background: #bbb}
</style>
<script>
function draw() {
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
var beginAngle = 0;
var endAngle = 0;
var data = [170, 60, 45];
var total = 0;
var colours = ["#95B524", "#AFCC4C", "#C1DD54"];
const SPACE = 10;
for (var i = 0; i < data.length; i++) {
total += data[i];
}
ctx.strokeStyle = 'white';
ctx.lineWidth = 2;
for (var j = 0; j < data.length; j++) {
endAngle = beginAngle + (Math.PI * 2 * (data[j] / total));
ctx.fillStyle = colours[j];
ctx.beginPath();
ctx.moveTo(canvas.width/2, canvas.height/2);
ctx.arc(canvas.width/2, canvas.height/2, canvas.height/2 - SPACE,
beginAngle, endAngle, false);
ctx.closePath();
ctx.fill();
ctx.stroke();
beginAngle = endAngle;
}
}
</script>
</head>
<body onload="draw();">
<canvas id="myCanvas" width="350" height="300">
</canvas>
</body>
</html>
该示例绘制了一个饼图。 它有三片,分别涂有不同的绿色阴影。
<style>
canvas {background: #bbb}
</style>
为了使图表的白色轮廓清晰可见,我们将画布的背景色更改为灰色。
var data = [170, 60, 45];
这是饼图说明的数据。
const SPACE = 10;
SPACE
常数是从饼图到画布边界的距离。
endAngle = beginAngle + (Math.PI * 2 * (data[j] / total));
该公式计算当前绘制的切片的结束角度。
ctx.moveTo(canvas.width/2, canvas.height/2);
ctx.arc(canvas.width/2, canvas.height/2, canvas.height/2 - SPACE,
beginAngle, endAngle, false);
ctx.closePath();
三种方法可用于绘制当前切片:moveTo()
,arc()
和closePath()
。
ctx.fill();
ctx.stroke();
我们绘制形状的内部和轮廓。
beginAngle = endAngle;
对于下一个切片,最后的终止角度成为起始角度。
图:饼图
星星
下面的示例创建一个星形。
star.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 canvas star shape</title>
<script>
function draw() {
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
ctx.fillStyle = 'gray';
var points = [ [ 0, 85 ], [ 75, 75 ], [ 100, 10 ], [ 125, 75 ],
[ 200, 85 ], [ 150, 125 ], [ 160, 190 ], [ 100, 150 ],
[ 40, 190 ], [ 50, 125 ], [ 0, 85 ] ];
var len = points.length;
ctx.beginPath();
ctx.moveTo(points[0][0], points[0][1]);
for (var i = 0; i < len; i++) {
ctx.lineTo(points[i][0], points[i][1]);
}
ctx.fill();
}
</script>
</head>
<body onload="draw();">
<canvas id="myCanvas" width="350" height="250">
</canvas>
</body>
</html>
从一系列点创建星形。
var points = [ [ 0, 85 ], [ 75, 75 ], [ 100, 10 ], [ 125, 75 ],
[ 200, 85 ], [ 150, 125 ], [ 160, 190 ], [ 100, 150 ],
[ 40, 190 ], [ 50, 125 ], [ 0, 85 ] ];
这些是星星的坐标。
ctx.moveTo(points[0][0], points[0][1]);
我们使用moveTo()
方法移动到形状的初始坐标。
for (var i = 0; i < len; i++) {
ctx.lineTo(points[i][0], points[i][1]);
}
在这里,我们使用lineTo()
方法连接星星的所有坐标。
ctx.fill();
fill()
方法使用定义的(灰色)颜色填充星形内部。
图:星星
三个圆圈
可以通过合成来创建新形状。 合成是确定画布中形状混合方式的规则。
three_circles.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 Canvas three circles</title>
<script>
function draw() {
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
ctx.lineWidth = 3;
ctx.fillStyle = 'gray';
ctx.beginPath();
ctx.arc(90, 90, 60, 0, 2*Math.PI);
ctx.stroke();
ctx.beginPath();
ctx.arc(120, 150, 60, 0, 2*Math.PI);
ctx.stroke();
ctx.beginPath();
ctx.arc(150, 100, 60, 0, 2*Math.PI);
ctx.stroke();
ctx.globalCompositeOperation='destination-out';
ctx.beginPath();
ctx.arc(90, 90, 60, 0, 2*Math.PI);
ctx.fill();
ctx.beginPath();
ctx.arc(120, 150, 60, 0, 2*Math.PI);
ctx.fill();
ctx.beginPath();
ctx.arc(150, 100, 60, 0, 2*Math.PI);
ctx.fill();
}
</script>
</head>
<body onload="draw();">
<canvas id="myCanvas" width="400" height="350">
</canvas>
</body>
</html>
该示例通过组合三个圆的轮廓来创建形状。 三个圆圈重叠。
ctx.beginPath();
ctx.arc(90, 90, 60, 0, 2*Math.PI);
ctx.stroke();
在画布上绘制了一个圆圈。
ctx.globalCompositeOperation='destination-out';
合成操作设置为destination-out
。 在此模式下,目的地和来源不重叠的任何地方都会显示目的地。 在其他任何地方都显示透明性。
ctx.beginPath();
ctx.arc(90, 90, 60, 0, 2*Math.PI);
ctx.fill();
现在,相同的圆圈充满了灰色。 新图形将删除两个重叠的现有图形。 结果,仅保留轮廓。
图:三个圆圈
在 HTML5 画布教程的这一部分中,我们介绍了 HTML5 画布中的一些基本形状和更高级的形状。
HTML5 画布填充
在 HTML5 画布教程的这一部分中,我们将使用填充。
填充用于绘制形状的内部。 共有三种基本填充:颜色,渐变和图案。 要设置形状的内部样式,我们使用fillStyle
属性。
色彩
代表计算机中颜色的常见系统是 RGB。 颜色表示为红色,绿色和蓝色强度值的组合。
colours.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 Canvas colour fills</title>
<script>
function draw() {
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
ctx.fillStyle = 'brown';
ctx.fillRect(10, 10, 90, 60);
ctx.fillStyle = 'rgb(217, 146, 54)';
ctx.fillRect(130, 10, 90, 60);
ctx.fillStyle = '#3F79BA';
ctx.fillRect(250, 10, 90, 60);
}
</script>
</head>
<body onload="draw();">
<canvas id="myCanvas" width="350" height="250">
</canvas>
</body>
</html>
在示例中,我们绘制了三个彩色矩形。 颜色以三种不同的格式指定。
ctx.fillStyle = 'brown';
在这一行中,使用一个字符串值来设置颜色值。
ctx.fillStyle = 'rgb(217, 146, 54)';
在这里,我们使用 RGB 系统。
ctx.fillStyle = '#3F79BA';
第三个矩形的颜色由 RGB 系统的十六进制表示法设置。
图:颜色
线性渐变
在计算机图形学中,渐变是从浅到深或从一种颜色到另一种颜色的阴影的平滑混合。 在 2D 绘图程序和绘画程序中,渐变用于创建彩色背景和特殊效果以及模拟灯光和阴影。
有两种类型的渐变:线性渐变和径向渐变。 第一个示例演示了 HTML5 canvas
中的线性渐变。
linear_gradient.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 canvas linear gradient</title>
<script>
function draw() {
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
var lgr = ctx.createLinearGradient(150, 0, 150, 160);
lgr.addColorStop(0.2, "black");
lgr.addColorStop(0.5, "red");
lgr.addColorStop(0.8, "black");
ctx.fillStyle = lgr;
ctx.fillRect(0, 0, 300, 160);
}
</script>
</head>
<body onload="draw();">
<canvas id="myCanvas" width="350" height="350">
</canvas>
</body>
</html>
该代码示例使用线性渐变填充矩形。 线性渐变是沿直线创建的渐变。
var lgr = ctx.createLinearGradient(150, 0, 150, 160);
createLinearGradient()
方法沿着由参数表示的坐标给出的直线创建一个渐变。 参数是起点和终点的 x 和 y 坐标。
lgr.addColorStop(0.2, "black");
lgr.addColorStop(0.5, "red");
lgr.addColorStop(0.8, "black");
addColorStop()
方法使用指定的偏移量和颜色在渐变上定义新的色标。 在我们的情况下,颜色停止设置,黑色和红色开始和结束。
ctx.fillStyle = lgr;
创建的线性梯度设置为当前fillStyle
。
ctx.fillRect(0, 0, 300, 160);
使用fillRect()
方法绘制一个矩形。 矩形的内部充满了我们的线性渐变。
图:线性渐变
径向渐变
径向渐变是两个圆之间颜色或阴影的混合。
radial_gradient.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 canvas radial gradient</title>
<script>
function draw() {
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
var rgr = ctx.createRadialGradient(canvas.width/2, canvas.height/2, 5,
canvas.width/2, canvas.height/2, 100);
rgr.addColorStop(0, "black");
rgr.addColorStop(0.3, "yellow");
rgr.addColorStop(1, "black");
ctx.fillStyle = rgr;
ctx.fillRect(0, 0, 250, 250);
}
</script>
</head>
<body onload="draw();">
<canvas id="myCanvas" width="250" height="250">
</canvas>
</body>
</html>
该代码示例使用径向渐变填充矩形。
var rgr = ctx.createRadialGradient(canvas.width/2, canvas.height/2, 5,
canvas.width/2, canvas.height/2, 100);
createRadialGradient()
方法创建由参数表示的两个圆的坐标给定的径向渐变。 我们将圆圈设置在画布的中间。 前两个参数设置起始圆的 x 和 y 坐标。 第三个参数是起始圆的半径。 接下来的两个参数是结束圆的 x 和 y 坐标。 最后一个参数指定末端线圈的半径。
rgr.addColorStop(0, "black");
rgr.addColorStop(0.3, "yellow");
rgr.addColorStop(1, "black");
addColorStop()
方法设置径向渐变中的交替颜色:黑色和黄色。
ctx.fillStyle = rgr;
ctx.fillRect(0, 0, 250, 250);
用径向渐变填充绘制一个矩形。
图:径向渐变
图案
图案是应用于形状的图像。 也称为图像纹理。
pattern.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 canvas pattern</title>
<script>
function draw() {
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
var img = new Image();
img.src = 'crack.png';
img.onload = function() {
var pattern = ctx.createPattern(img, 'repeat');
ctx.rect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = pattern;
ctx.fill();
};
}
</script>
</head>
<body onload="draw();">
<canvas id="myCanvas" width="250" height="250">
</canvas>
</body>
</html>
该示例使用重复的图像纹理填充整个画布。
var img = new Image();
Image()
是 HTML 图像元素的 HTML5 构造器。
img.src = 'crack.png';
在src
属性中,设置图像的 URL。
var pattern = ctx.createPattern(img, 'repeat');
createPattern()
方法使用指定的图像创建图案。 它按照重复参数指定的方向重复源。 'repeat'
值在两个方向上重复该图案。
图:图案
在 HTML5 画布教程的这一部分中,我们进行了各种填充。
HTML5 画布中的透明度
在 HTML5 画布教程的这一部分中,我们讨论透明度。 我们提供一些基本定义和两个示例。
透明度说明
透明性是指能够透视材料的质量。 了解透明度的最简单方法是想象一块玻璃或水。 从技术上讲,光线可以穿过玻璃,这样我们就可以看到玻璃后面的物体。
在计算机图形学中,我们可以使用 alpha 合成实现透明效果。 Alpha 合成是将图像与背景组合以创建部分透明外观的过程。 合成过程使用 alpha 通道。 Alpha 通道是图形文件格式的 8 位层,用于表达半透明性(透明度)。 每个像素的额外八位用作掩码,表示 256 级半透明。
透明矩形
下一个示例绘制了十个具有不同透明度级别的矩形。
transparent_rectangles.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 canvas transparent rectangles</title>
<script>
function draw() {
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
ctx.fillStyle = "blue";
for (var i = 1; i <= 10; i++) {
var alpha = i * 0.1;
ctx.globalAlpha = alpha;
ctx.fillRect(50*i, 20, 40, 40);
}
}
</script>
</head>
<body onload="draw();">
<canvas id="myCanvas" width="550" height="200">
</canvas>
</body>
</html>
我们绘制了十个具有不同透明度级别的蓝色矩形。
ctx.fillStyle = "blue";
矩形将填充蓝色。
var alpha = i * 0.1;
alpha
值在for
循环中动态变化。 在每个循环中,它减少了 10% 。
ctx.globalAlpha = alpha;
globalAlpha
属性指定在形状和图像绘制到画布上之前应用于其的 alpha 值。 该值的范围是 0.0(完全透明)到 1.0(完全不透明)。
ctx.fillRect(50*i, 20, 40, 40);
fillRect()
方法绘制一个填充的矩形。 它的参数是 x 和 y 坐标以及矩形的宽度和高度。
图:透明矩形
淡出演示
在下一个示例中,我们将淡出图像。 图像将逐渐变得更加透明,直到完全不可见为止。
fadeout.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 canvas fade out demo</title>
<script>
var alpha = 1.0;
var ctx;
var canvas;
var img;
function init() {
canvas = document.getElementById('myCanvas');
ctx = canvas.getContext('2d');
img = new Image();
img.src = 'mushrooms.jpg';
img.onload = function() {
ctx.drawImage(img, 10, 10);
};
fadeOut();
}
function fadeOut() {
if (alpha <= 0) {
return;
}
requestAnimationFrame(fadeOut);
ctx.clearRect(0,0, canvas.width, canvas.height);
ctx.globalAlpha = alpha;
ctx.drawImage(img, 10, 10);
alpha += -0.01;
}
</script>
</head>
<body onload="init();">
<canvas id="myCanvas" width="350" height="250">
</canvas>
</body>
</html>
该示例具有动画效果。 在每个动画周期中,alpha 值都会减小,并重新绘制图像。
img = new Image();
img.src = 'mushrooms.jpg';
img.onload = function() {
ctx.drawImage(img, 10, 10);
}
这些行将在画布上加载并显示图像。
fadeOut();
在init()
函数内部,我们调用fadeOut()
函数,该函数将启动动画。
if (alpha <= 0) {
return;
}
当alpha
值达到零时,动画终止。 requestAnimationFrame()
函数不再调用fadeOut()
函数。
requestAnimationFrame(fadeOut);
requestAnimationFrame()
是创建动画的便捷函数。 它告诉浏览器执行动画。 该参数是在重画之前要调用的函数。 浏览器为动画选择最佳帧速率。
ctx.clearRect(0,0, canvas.width, canvas.height);
clearRect()
方法删除画布。
ctx.globalAlpha = alpha;
ctx.drawImage(img, 10, 10);
考虑到新的 Alpha 值,将设置新的全局 Alpha 值并重新绘制图像。
alpha += -0.01;
最后,alpha
降低。 在下一个动画周期中,将以降低的alpha
值绘制图像。
在 HTML5 画布教程的这一部分中,我们讨论了透明度。
碰撞检测
原文: https://zetcode.com/tutorials/javagamestutorial/collision/
在 Java 2D 游戏教程的这一部分中,我们将讨论碰撞检测。
许多游戏都需要处理碰撞,尤其是街机游戏。 简而言之,我们需要检测两个对象何时在屏幕上碰撞。
在下一个代码示例中,我们将扩展上一个示例。 我们添加了一个新的外星人精灵。 我们将检测两种类型的碰撞:当导弹击中外星人的飞船和我们的航天器与外星人相撞时。
射击外星人
在示例中,我们有一个航天器和外星人。 我们可以使用光标键在板上移动航天器。 用空格键发射消灭外星人的导弹。
Sprite.java
package com.zetcode;
import java.awt.Image;
import java.awt.Rectangle;
import javax.swing.ImageIcon;
public class Sprite {
protected int x;
protected int y;
protected int width;
protected int height;
protected boolean visible;
protected Image image;
public Sprite(int x, int y) {
this.x = x;
this.y = y;
visible = true;
}
protected void getImageDimensions() {
width = image.getWidth(null);
height = image.getHeight(null);
}
protected void loadImage(String imageName) {
ImageIcon ii = new ImageIcon(imageName);
image = ii.getImage();
}
public Image getImage() {
return image;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public boolean isVisible() {
return visible;
}
public void setVisible(Boolean visible) {
this.visible = visible;
}
public Rectangle getBounds() {
return new Rectangle(x, y, width, height);
}
}
可以由所有子画面(工艺品,外星人和导弹)共享的代码放在Sprite
类中。
public Rectangle getBounds() {
return new Rectangle(x, y, width, height);
}
getBounds()
方法返回精灵图像的边界矩形。 在碰撞检测中我们需要边界。
SpaceShip.java
package com.zetcode;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.List;
public class SpaceShip extends Sprite {
private int dx;
private int dy;
private List<Missile> missiles;
public SpaceShip(int x, int y) {
super(x, y);
initCraft();
}
private void initCraft() {
missiles = new ArrayList<>();
loadImage("src/resources/spaceship.png");
getImageDimensions();
}
public void move() {
x += dx;
y += dy;
if (x < 1) {
x = 1;
}
if (y < 1) {
y = 1;
}
}
public List<Missile> getMissiles() {
return missiles;
}
public void keyPressed(KeyEvent e) {
int key = e.getKeyCode();
if (key == KeyEvent.VK_SPACE) {
fire();
}
if (key == KeyEvent.VK_LEFT) {
dx = -1;
}
if (key == KeyEvent.VK_RIGHT) {
dx = 1;
}
if (key == KeyEvent.VK_UP) {
dy = -1;
}
if (key == KeyEvent.VK_DOWN) {
dy = 1;
}
}
public void fire() {
missiles.add(new Missile(x + width, y + height / 2));
}
public void keyReleased(KeyEvent e) {
int key = e.getKeyCode();
if (key == KeyEvent.VK_LEFT) {
dx = 0;
}
if (key == KeyEvent.VK_RIGHT) {
dx = 0;
}
if (key == KeyEvent.VK_UP) {
dy = 0;
}
if (key == KeyEvent.VK_DOWN) {
dy = 0;
}
}
}
此类代表航天器。
private List<Missile> missiles;
航天器发射的所有导弹都存储在missiles
列表中。
public void fire() {
missiles.add(new Missile(x + width, y + height / 2));
}
当我们发射导弹时,新的Missile
对象将添加到missiles
列表中。 它会保留在列表中,直到与外星人碰撞或离开窗口为止。
Board.java
package com.zetcode;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JPanel;
import javax.swing.Timer;
public class Board extends JPanel implements ActionListener {
private Timer timer;
private SpaceShip spaceship;
private List<Alien> aliens;
private boolean ingame;
private final int ICRAFT_X = 40;
private final int ICRAFT_Y = 60;
private final int B_WIDTH = 400;
private final int B_HEIGHT = 300;
private final int DELAY = 15;
private final int[][] pos = {
{2380, 29}, {2500, 59}, {1380, 89},
{780, 109}, {580, 139}, {680, 239},
{790, 259}, {760, 50}, {790, 150},
{980, 209}, {560, 45}, {510, 70},
{930, 159}, {590, 80}, {530, 60},
{940, 59}, {990, 30}, {920, 200},
{900, 259}, {660, 50}, {540, 90},
{810, 220}, {860, 20}, {740, 180},
{820, 128}, {490, 170}, {700, 30}
};
public Board() {
initBoard();
}
private void initBoard() {
addKeyListener(new TAdapter());
setFocusable(true);
setBackground(Color.BLACK);
ingame = true;
setPreferredSize(new Dimension(B_WIDTH, B_HEIGHT));
spaceship = new SpaceShip(ICRAFT_X, ICRAFT_Y);
initAliens();
timer = new Timer(DELAY, this);
timer.start();
}
public void initAliens() {
aliens = new ArrayList<>();
for (int[] p : pos) {
aliens.add(new Alien(p[0], p[1]));
}
}
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
if (ingame) {
drawObjects(g);
} else {
drawGameOver(g);
}
Toolkit.getDefaultToolkit().sync();
}
private void drawObjects(Graphics g) {
if (spaceship.isVisible()) {
g.drawImage(spaceship.getImage(), spaceship.getX(), spaceship.getY(),
this);
}
List<Missile> ms = spaceship.getMissiles();
for (Missile missile : ms) {
if (missile.isVisible()) {
g.drawImage(missile.getImage(), missile.getX(),
missile.getY(), this);
}
}
for (Alien alien : aliens) {
if (alien.isVisible()) {
g.drawImage(alien.getImage(), alien.getX(), alien.getY(), this);
}
}
g.setColor(Color.WHITE);
g.drawString("Aliens left: " + aliens.size(), 5, 15);
}
private void drawGameOver(Graphics g) {
String msg = "Game Over";
Font small = new Font("Helvetica", Font.BOLD, 14);
FontMetrics fm = getFontMetrics(small);
g.setColor(Color.white);
g.setFont(small);
g.drawString(msg, (B_WIDTH - fm.stringWidth(msg)) / 2,
B_HEIGHT / 2);
}
@Override
public void actionPerformed(ActionEvent e) {
inGame();
updateShip();
updateMissiles();
updateAliens();
checkCollisions();
repaint();
}
private void inGame() {
if (!ingame) {
timer.stop();
}
}
private void updateShip() {
if (spaceship.isVisible()) {
spaceship.move();
}
}
private void updateMissiles() {
List<Missile> ms = spaceship.getMissiles();
for (int i = 0; i < ms.size(); i++) {
Missile m = ms.get(i);
if (m.isVisible()) {
m.move();
} else {
ms.remove(i);
}
}
}
private void updateAliens() {
if (aliens.isEmpty()) {
ingame = false;
return;
}
for (int i = 0; i < aliens.size(); i++) {
Alien a = aliens.get(i);
if (a.isVisible()) {
a.move();
} else {
aliens.remove(i);
}
}
}
public void checkCollisions() {
Rectangle r3 = spaceship.getBounds();
for (Alien alien : aliens) {
Rectangle r2 = alien.getBounds();
if (r3.intersects(r2)) {
spaceship.setVisible(false);
alien.setVisible(false);
ingame = false;
}
}
List<Missile> ms = spaceship.getMissiles();
for (Missile m : ms) {
Rectangle r1 = m.getBounds();
for (Alien alien : aliens) {
Rectangle r2 = alien.getBounds();
if (r1.intersects(r2)) {
m.setVisible(false);
alien.setVisible(false);
}
}
}
}
private class TAdapter extends KeyAdapter {
@Override
public void keyReleased(KeyEvent e) {
spaceship.keyReleased(e);
}
@Override
public void keyPressed(KeyEvent e) {
spaceship.keyPressed(e);
}
}
}
这是Board
类。
private final int[][] pos = {
{2380, 29}, {2500, 59}, {1380, 89},
{780, 109}, {580, 139}, {680, 239},
{790, 259}, {760, 50}, {790, 150},
{980, 209}, {560, 45}, {510, 70},
{930, 159}, {590, 80}, {530, 60},
{940, 59}, {990, 30}, {920, 200},
{900, 259}, {660, 50}, {540, 90},
{810, 220}, {860, 20}, {740, 180},
{820, 128}, {490, 170}, {700, 30}
};
这些是外星飞船的初始位置。
public void initAliens() {
aliens = new ArrayList<>();
for (int[] p : pos) {
aliens.add(new Alien(p[0], p[1]));
}
}
initAliens()
方法创建一个外星对象列表。 外星人从pos
数组中占据初始位置。
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
if (ingame) {
drawObjects(g);
} else {
drawGameOver(g);
}
Toolkit.getDefaultToolkit().sync();
}
在paintComponent()
方法内,我们绘制游戏精灵或通过消息编写游戏。 这取决于ingame
变量。
private void drawObjects(Graphics g) {
if (spaceship.isVisible()) {
g.drawImage(spaceship.getImage(), spaceship.getX(), spaceship.getY(),
this);
}
...
}
drawObjects()
方法在窗口上绘制游戏精灵。 首先,我们绘制工艺精灵。
for (Alien alien : aliens) {
if (alien.isVisible()) {
g.drawImage(alien.getImage(), alien.getX(), alien.getY(), this);
}
}
在这个循环中,我们吸引了所有外星人。 仅在以前未销毁它们的情况下才绘制它们。 通过isVisible()
方法检查。
g.setColor(Color.WHITE);
g.drawString("Aliens left: " + aliens.size(), 5, 15);
在窗口的左上角,我们绘制了剩余的外星人数量。
private void drawGameOver(Graphics g) {
String msg = "Game Over";
Font small = new Font("Helvetica", Font.BOLD, 14);
FontMetrics fm = getFontMetrics(small);
g.setColor(Color.white);
g.setFont(small);
g.drawString(msg, (B_WIDTH - fm.stringWidth(msg)) / 2,
B_HEIGHT / 2);
}
drawGameOver()
在窗口中间的消息上方绘制游戏。 该消息显示在游戏结束时,当我们摧毁所有外星飞船或与其中一艘发生碰撞时。
@Override
public void actionPerformed(ActionEvent e) {
inGame();
updateShip();
updateMissiles();
updateAliens();
checkCollisions();
repaint();
}
每个动作事件代表一个游戏周期。 游戏逻辑被纳入特定方法中。 例如,updateMissiles()
会移动所有可用的导弹。
private void updateAliens() {
if (aliens.isEmpty()) {
ingame = false;
return;
}
for (int i = 0; i < aliens.size(); i++) {
Alien a = aliens.get(i);
if (a.isVisible()) {
a.move();
} else {
aliens.remove(i);
}
}
}
在updateAliens()
方法内部,我们首先检查aliens
列表中是否还有任何异物。 如果列表为空,则游戏结束。 如果它不为空,则浏览列表并移动其所有项目。 被摧毁的外星人将从名单中删除。
public void checkCollisions() {
Rectangle r3 = spaceship.getBounds();
for (Alien alien : aliens) {
Rectangle r2 = alien.getBounds();
if (r3.intersects(r2)) {
spaceship.setVisible(false);
alien.setVisible(false);
ingame = false;
}
}
...
}
checkCollisions()
方法检查可能的冲突。 首先,我们检查工艺对象是否与任何外来对象发生碰撞。 我们使用getBounds()
方法获得对象的矩形。 intersects()
方法检查两个矩形是否相交。
List<Missile> ms = spaceship.getMissiles();
for (Missile m : ms) {
Rectangle r1 = m.getBounds();
for (Alien alien : aliens) {
Rectangle r2 = alien.getBounds();
if (r1.intersects(r2)) {
m.setVisible(false);
alien.setVisible(false);
}
}
}
该代码检查导弹与外星人之间的碰撞。
Alien.java
package com.zetcode;
public class Alien extends Sprite {
private final int INITIAL_X = 400;
public Alien(int x, int y) {
super(x, y);
initAlien();
}
private void initAlien() {
loadImage("src/resources/alien.png");
getImageDimensions();
}
public void move() {
if (x < 0) {
x = INITIAL_X;
}
x -= 1;
}
}
这是Alien
类。
public void move() {
if (x < 0) {
x = INITIAL_X;
}
x -= 1;
}
外星人消失在左侧后,他们会返回到右侧的屏幕。
Missile.java
package com.zetcode;
public class Missile extends Sprite {
private final int BOARD_WIDTH = 390;
private final int MISSILE_SPEED = 2;
public Missile(int x, int y) {
super(x, y);
initMissile();
}
private void initMissile() {
loadImage("src/resources/missile.png");
getImageDimensions();
}
public void move() {
x += MISSILE_SPEED;
if (x > BOARD_WIDTH)
visible = false;
}
}
这是Missile
类。
public void move() {
x += MISSILE_SPEED;
if (x > BOARD_WIDTH)
visible = false;
}
导弹只能向一个方向移动。 它们到达右窗口边框后消失。
CollisionEx.java
package com.zetcode;
import java.awt.EventQueue;
import javax.swing.JFrame;
public class CollisionEx extends JFrame {
public CollisionEx() {
initUI();
}
private void initUI() {
add(new Board());
setResizable(false);
pack();
setTitle("Collision");
setLocationRelativeTo(null);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
CollisionEx ex = new CollisionEx();
ex.setVisible(true);
});
}
}
最后,这是主要类。
图:射击外星人
本章是关于碰撞检测的。
HTML5 画布合成
在 HTML5 canvas
教程的这一部分中,我们使用合成操作。
合成是将来自不同来源的视觉元素组合成单个图像。 它们被用来创建一种幻觉,即所有这些元素都是同一场景的一部分。 合成在电影行业中被广泛使用来创造人群,否则将是昂贵或不可能创造的整个新世界。
本教程的形状章节中的三个圆圈示例使用destination-out
合成操作来创建新形状。
合成操作
developer.mozilla.org 在其合成和剪裁一章中列出了 26 种不同的合成操作。 我们在下一个代码示例中展示其中的一些。
假设我们要在画布上绘制两个对象。 绘制的第一个对象称为目标,第二个称为源。 canvas
上下文的globalCompositeOperation
属性确定如何将这两个对象混合在一起。 例如,在source-over
规则(这是默认的构图操作)中,新形状会在现有形状的顶部绘制。
compositing.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 Canvas compositing operations</title>
<style>
canvas {border: 1px solid brown}
select {vertical-align:top}
</style>
<script>
var canvas;
var ctx;
var value = 'source-over';
var operations = ['source-over', 'source-in', 'source-out',
'source-atop', 'destination-over', 'destination-in', 'destination-out',
'destination-atop', 'lighter', 'copy', 'xor'];
function init() {
canvas = document.getElementById('myCanvas');
ctx = canvas.getContext('2d');
draw();
}
function getOperation(sel) {
value = operations[sel.value];
draw();
}
function draw() {
ctx.save();
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'blue';
ctx.fillRect(20, 20, 40, 40);
ctx.globalCompositeOperation = value;
ctx.fillStyle = 'green';
ctx.fillRect(25, 25, 40, 40);
ctx.restore();
}
</script>
</head>
<body onload="init();">
<canvas id="myCanvas" width="150" height="100">
</canvas>
<select id="opers" onchange="getOperation(this)">
<option value="0" selected="selected">source-over</option>
<option value="1">source-in</option>
<option value="2">source-out</option>
<option value="3">source-atop</option>
<option value="4">destination-over</option>
<option value="5">destination-in</option>
<option value="6">destination-out</option>
<option value="7">destination-atop</option>
<option value="8">lighter</option>
<option value="9">copy</option>
<option value="10">xor</option>
</select>
</body>
</html>
在示例中,我们有一个合成操作的下拉列表。 所选操作将应用于两个重叠矩形的图形。
var operations = ['source-over', 'source-in', 'source-out',
'source-atop', 'destination-over', 'destination-in', 'destination-out',
'destination-atop', 'lighter', 'copy', 'xor'];
operations
数组包含 11 个合成操作。
function init() {
canvas = document.getElementById('myCanvas');
ctx = canvas.getContext('2d');
draw();
}
在init()
函数内部,我们获得对canvas
对象及其绘制上下文的引用。
ctx.save();
...
ctx.restore();
每次我们从下拉列表中选择一个选项时,都会使用新的合成操作重新绘制画布。 为了获得正确的结果,我们必须在save()
和restore()
方法之间放置绘图代码。 这样,操作彼此隔离。
ctx.clearRect(0, 0, canvas.width, canvas.height);
clearRect()
方法清除先前的输出。
ctx.globalCompositeOperation = value;
globalCompositeOperation
设置为从下拉列表中选择的值。
图:合成
上图显示了xor
合成操作。 在此规则中,将形状透明化,使其在其他地方重叠并正常绘制。
在 HTML5 画布教程的这一部分中,我们讨论了图像合成。
HTML5 canvas
中的变换
在 HTML5 画布教程的这一部分中,我们将讨论变换。
仿射变换由零个或多个线性变换(旋转,缩放或剪切)和平移(移位)组成。 几个线性变换可以组合成一个矩阵。 旋转是使刚体绕固定点移动的变换。 缩放是一种放大或缩小对象的变换。 比例因子在所有方向上都是相同的。 平移是使每个点在指定方向上移动恒定距离的变换。 剪切是一种使对象垂直于给定轴移动的变换,该值在轴的一侧比另一侧更大。
有一种transform()
方法,该方法将当前变换与该方法的参数所描述的矩阵相乘。 我们能够缩放,旋转,移动和剪切上下文。 还有执行特定变换的方法:translate()
,rotate
和scale()
。
平移
以下示例显示了一个简单的平移。
translation.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 canvas translation</title>
<style>
canvas {border: 1px solid #bbbbbb}
</style>
<script>
function draw() {
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
ctx.fillStyle = 'gray';
ctx.translate(canvas.width/2, canvas.height/2);
ctx.beginPath();
ctx.arc(0, 0, 30, 0, 2*Math.PI);
ctx.fill();
}
</script>
</head>
<body onload="draw();">
<canvas id="myCanvas" width="150" height="150">
</canvas>
</body>
</html>
该示例在画布的中间绘制一个圆圈。
ctx.translate(canvas.width/2, canvas.height/2);
translate()
方法将坐标系的原点移到画布的中间。
ctx.beginPath();
ctx.arc(0, 0, 30, 0, 2*Math.PI);
ctx.fill();
画一个圆。 它的中心点是画布的中间。
图:平移
旋转
下一个示例演示了旋转。
rotation.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 canvas rotation</title>
<script>
function draw() {
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
ctx.fillStyle = 'gray';
ctx.rotate(Math.PI/10);
ctx.fillRect(50, 10, 120, 80);
}
</script>
</head>
<body onload="draw();">
<canvas id="myCanvas" width="200" height="200">
</canvas>
</body>
</html>
该示例对矩形执行旋转。
ctx.rotate(Math.PI/10);
rotate()
方法执行旋转。 角度自变量表示顺时针旋转角度并以弧度表示。
图:旋转
缩放
缩放是通过scale()
方法完成的。 该方法采用两个参数:x 比例因子和 y 比例因子。
scaling.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 canvas scaling</title>
<script>
function draw() {
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
ctx.fillStyle = 'gray';
ctx.fillRect(20, 20, 80, 50);
ctx.save();
ctx.translate(110, 22);
ctx.scale(0.5, 0.5);
ctx.fillRect(0, 0, 80, 50);
ctx.restore();
ctx.translate(170, 20);
ctx.scale(1.5, 1.5);
ctx.fillRect(0, 0, 80, 50);
}
</script>
</head>
<body onload="draw();">
<canvas id="myCanvas" width="300" height="200">
</canvas>
</body>
</html>
在示例中,有一个矩形对象。 首先,我们缩小比例,然后再放大一点。
ctx.save();
ctx.translate(110, 22);
ctx.scale(0.5, 0.5);
ctx.fillRect(0, 0, 80, 50);
ctx.restore();
变换操作是累加的。 为了隔离变换和缩放操作,我们将它们放在save()
和restore()
方法之间。 save()
方法保存画布的整个状态,restore()
方法恢复画布的整个状态。
图:缩放
剪切
剪切是使对象的形状沿两个轴中的一个或两个变形的变换。 像缩放和平移一样,可以仅沿一个或两个坐标轴进行剪切。
shearing.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 canvas shearing</title>
<script>
function draw() {
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
ctx.translate(0.5, 0.5);
ctx.setLineDash([2]);
ctx.save();
ctx.strokeStyle = 'green';
ctx.strokeRect(50, 90, 160, 50);
ctx.restore();
ctx.save();
ctx.strokeStyle = 'blue';
ctx.transform(1, 1, 0, 1, 0, 0);
ctx.strokeRect(50, 40, 80, 50);
ctx.restore();
ctx.save();
ctx.strokeStyle = 'red';
ctx.transform(1, 1, 0, 1, 0, -130);
ctx.strokeRect(130, 10, 80, 50);
ctx.restore();
}
</script>
</head>
<body onload="draw();">
<canvas id="myCanvas" width="300" height="300">
</canvas>
</body>
</html>
没有特定的剪切方法,我们使用通用的transform()
方法。
ctx.translate(0.5, 0.5);
这条线使矩形的线更平滑。
ctx.save();
ctx.strokeStyle = 'blue';
ctx.transform(1, 1, 0, 1, 0, 0);
ctx.strokeRect(50, 40, 80, 50);
ctx.restore();
蓝色矩形被水平剪切(倾斜)。 transform()
方法的参数为:水平缩放,水平剪切,垂直剪切,垂直缩放,水平平移和垂直平移。
图:抖动
甜甜圈
在下面的示例中,我们通过旋转椭圆来创建复杂的形状。
DonutEx.java
<!DOCTYPE html>
<html>
<head>
<title>HTML5 canvas donut</title>
<script>
function draw() {
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
ctx.fillStyle = 'gray';
ctx.translate(0.5, 0.5);
var x = canvas.width/2;
var y = canvas.height/2;
for (var deg = 0; deg < 360; deg += 5) {
var rad = deg * Math.PI / 180;
ctx.beginPath();
ctx.ellipse(x, y, 30, 130, rad, 0, 2*Math.PI);
ctx.stroke();
}
}
</script>
</head>
<body onload="draw();">
<canvas id="myCanvas" width="300" height="300">
</canvas>
</body>
</html>
该示例使用ellipse()
方法,在撰写本教程时,并非所有浏览器都支持该方法。 该示例在 Chrome 和 Opera 上运行。
var x = canvas.width/2;
var y = canvas.height/2;
椭圆的中心位于画布的中心。
for (var deg = 0; deg < 360; deg += 5) {
var rad = deg * Math.PI / 180;
ctx.beginPath();
ctx.ellipse(x, y, 30, 130, rad, 0, 2*Math.PI);
ctx.stroke();
}
我们创建 36 个椭圆。 椭圆旋转。 ellipse()
方法采用以下参数:椭圆中心点的 x 和 y 坐标,椭圆的长轴半径,椭圆的短轴半径,旋转,起始角度和终止角度。
在 Java 2D 教程的这一部分中,我们讨论了变换。
HTML5 画布中的文字
在 HTML5 画布教程的这一部分中,我们将处理文本。
文字和字体
字符是表示诸如字母,数字或标点符号之类的项目的符号。 字形是用于呈现字符或字符序列的形状。 在拉丁字母中,字形通常代表一个字符。 在其他书写系统中,一个字符可能由几个字形组成,例如ť,ž,ú,ô。 这些是带有重音符号的拉丁字符。
可以使用各种字体在画布上绘制文本。 字体是一组具有特定字体设计和大小的字体字符。 各种字体包括 Helvetica,Georgia,Times 或 Verdana。 具有特定样式的字形的集合形成字体面。 字体的集合构成字体家族。
绘制文字
HTML5 canvas
上下文有两种绘制文本的方法:strokeText()
和fillText()
。 不同之处在于fillText()
方法绘制文本的内部,而strokeText()
方法绘制文本的轮廓。
drawing_text.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 canvas drawing text</title>
<script>
function draw() {
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
ctx.font = "28px serif";
ctx.fillText("ZetCode", 15, 25);
ctx.font = "36px sans-serif";
ctx.strokeText("ZetCode", 30, 80);
}
</script>
</head>
<body onload="draw();">
<canvas id="myCanvas" width="200" height="150">
</canvas>
</body>
</html>
该示例在画布上绘制两个字符串。
ctx.font = "28px serif";
canvas
上下文font
属性指定绘制文本时使用的当前文本样式。 我们指定字体大小和字体系列。
ctx.fillText("ZetCode", 15, 25);
fillText()
方法的第一个参数是要渲染的文本。 接下来的两个参数是文本起点的 x 和 y 坐标。
图:绘制文本
字形
在下面的示例中,我们演示了几种字体属性。
text_font.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 canvas drawing text</title>
<script>
function draw() {
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
ctx.font = "normal bold 18px serif";
ctx.fillText('nice', 10, 20);
ctx.font = "italic 18px serif";
ctx.fillText('pretty', 70, 20);
ctx.font = "small-caps bolder 18px serif";
ctx.fillText('beautiful', 160, 20);
}
</script>
</head>
<body onload="draw();">
<canvas id="myCanvas" width="350" height="150">
</canvas>
</body>
</html>
该示例绘制了三个具有不同字体样式,变体和粗细的单词。
ctx.font = "normal bold 18px serif";
此行设置normal
字体样式和bold
字体粗细。
ctx.font = "small-caps bolder 18px serif";
此行设置small-caps
字体变体和bolder
字体粗细。
图:文本字体
文字基线
Canvas 2D API 的textBaseline
属性指定在绘制文本时使用的当前文本基线。 它接受以下值:顶部,底部,中间,字母,悬挂,表意。 默认为字母。
text_baseline.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 canvas text baseline</title>
<style>
canvas {border: 1px solid #bbbbbb}
</style>
<script>
function draw() {
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
ctx.translate(0.5, 0.5);
ctx.setLineDash([2]);
ctx.fillStyle = 'gray';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, 100);
ctx.lineTo(canvas.width, 100);
ctx.stroke();
ctx.font = '20px serif';
ctx.textBaseline = "top";
ctx.fillText('Top', 5, 100);
ctx.textBaseline = "bottom";
ctx.fillText('Bottom', 60, 100);
ctx.textBaseline = "middle";
ctx.fillText('Middle', 150, 100);
ctx.textBaseline = "alphabetic";
ctx.fillText('Alphabetic', 240, 100);
ctx.textBaseline = "hanging";
ctx.fillText('Hanging', 360, 100);
ctx.textBaseline = "ideographic";
ctx.fillText('Ideographic', 460, 100);
}
</script>
</head>
<body onload="draw();">
<canvas id="myCanvas" width="600" height="200">
</canvas>
</body>
</html>
该示例使用所有可用的文本基线绘制字符串。
ctx.textBaseline = "top";
ctx.fillText('Top', 5, 100);
这些行在顶部基线模式下绘制文本。
图:文字基线
文字对齐
Canvas 2D API 的textAlign
属性指定在绘制文本时使用的当前文本对齐方式。 对齐基于fillText()
方法的 x 值。 可能的值为:左,右,居中,开始和结束。
text_alignment.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 canvas text alignment</title>
<style>
canvas {border: 1px solid #bbbbbb}
</style>
<script>
function draw() {
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
var cw = canvas.width/2;
ctx.fillStyle = 'gray';
ctx.translate(0.5, 0.5);
ctx.setLineDash([2]);
ctx.beginPath();
ctx.moveTo(cw, 0);
ctx.lineTo(cw, canvas.height);
ctx.stroke();
ctx.font = "16px serif";
ctx.textAlign = "center";
ctx.fillText("center", cw, 20);
ctx.textAlign = "start";
ctx.fillText("start", cw, 60);
ctx.textAlign = "end";
ctx.fillText("end", cw, 100);
ctx.textAlign = "left";
ctx.fillText("left", cw, 140);
ctx.textAlign = "right";
ctx.fillText("right", cw, 180);
}
</script>
</head>
<body onload="draw();">
<canvas id="myCanvas" width="350" height="200">
</canvas>
</body>
</html>
该示例使用所有可用的文本对齐方式绘制文本。
var cw = canvas.width/2;
我们计算画布中间点的 x 坐标。 我们的文字围绕这一点对齐。
ctx.beginPath();
ctx.moveTo(cw, 0);
ctx.lineTo(cw, canvas.height);
ctx.stroke();
为了更好地视觉理解,我们在画布的中间绘制了一条细的垂直线。
ctx.textAlign = "center";
ctx.fillText("center", cw, 20);
这些线使文本水平居中。
图:文本对齐
在 HTML5 画布教程的这一部分中,我们介绍了文本。
HTML5 画布中的动画
在本章中,我们将在 HTML5 canvas
中创建动画。
动画是连续的图像,使人产生了运动的幻觉。 但是,动画不仅限于运动。 随时间改变对象的背景也被视为动画。
在 HTML5 canvas
中创建动画的函数有以下三个:
setInterval(function, delay)
setTimeut(function, delay)
requestAnimationFrame(callback)
setInterval()
函数每隔延迟毫秒重复执行一次传递的函数。 setTimeout()
以毫秒为单位执行指定的功能。 为了创建动画,从执行的函数中调用setTimeout()
。 requestAnimationFrame()
函数允许浏览器在下一次重绘之前调用指定的函数来更新动画。 浏览器进行了一些优化。
沿曲线移动
在第一个动画中,对象沿曲线移动。
move_along_curve.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 canvas move along curve</title>
<style>
canvas { border: 1px solid #bbbbbb }
</style>
<script>
var canvas;
var ctx;
var x = 20;
var y = 80;
const DELAY = 30;
const RADIUS = 10;
function init() {
canvas = document.getElementById('myCanvas');
ctx = canvas.getContext('2d');
setInterval(move_ball, DELAY);
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.fillStyle = "cadetblue";
ctx.arc(x, y, RADIUS, 0, 2*Math.PI);
ctx.fill();
}
function move_ball() {
x += 1;
if (x > canvas.width + RADIUS) {
x = 0;
}
y = Math.sin(x/32)*30 + 80;
draw();
}
</script>
</head>
<body onload="init();">
<canvas id="myCanvas" width="350" height="150">
</canvas>
</body>
</html>
该示例沿正弦曲线移动一个圆。 圆圈移过画布的末端后,它再次出现在左侧。
setInterval(move_ball, DELAY);
setInterval()
函数使move_ball()
函数每DELAY
ms 调用一次。
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.fillStyle = "cadetblue";
ctx.arc(x, y, RADIUS, 0, 2*Math.PI);
ctx.fill();
}
draw()
方法使用clearRect()
方法清除画布,并绘制具有更新的 x 和 y 坐标的新圆。
function move_ball() {
x += 1;
if (x > canvas.width + RADIUS) {
x = 0;
}
y = Math.sin(x/32)*30 + 80;
draw();
}
在move_ball()
函数中,我们更新圆心的 x 和 y 坐标。 我们检查球是否已通过画布的右边缘,然后调用draw()
方法重绘画布。
淡出
淡出是改变对象状态的动画。 这是一种过渡动画。
fading_out.html
<!DOCTYPE html>
<html>
<head>
<style>
canvas {border: 1px solid #bbbbbb}
</style>
<title>HTML5 canvas fading out</title>
<script>
var canvas;
var ctx;
var alpha = 1;
var rx = 20;
var ry = 20;
var rw = 120;
var rh = 80;
const DELAY = 20;
function init() {
canvas = document.getElementById('myCanvas');
ctx = canvas.getContext('2d');
canvas.addEventListener("click", onClicked);
ctx.fillRect(rx, ry, rw, rh)
}
function onClicked(e) {
var cx = e.x;
var cy = e.y;
if (cx >= rx && cx <= rx + rw &&
cy >= ry && cy <= ry + rh) {
fadeout();
}
}
function fadeout() {
if (alpha < 0) {
canvas.removeEventListener("click", onClicked);
ctx.globalAlpha = 1;
ctx.fillStyle = 'white';
ctx.fillRect(rx, ry, rw, rh);
return;
}
ctx.clearRect(rx, ry, rw, rh);
ctx.globalAlpha = alpha;
ctx.fillRect(rx, ry, rw, rh)
alpha -= 0.01;
setTimeout(fadeout, DELAY);
}
</script>
</head>
<body onload="init();">
<canvas id="myCanvas" width="350" height="250">
</canvas>
</body>
</html>
有一个矩形对象。 当我们单击矩形时,它开始淡出。
canvas.addEventListener("click", onClicked);
通过addEventListener()
方法将click
监听器添加到画布。 再次单击鼠标后,将调用onClicked()
函数。
ctx.fillRect(rx, ry, rw, rh)
最初,在画布上以默认的黑色填充绘制了一个矩形。
function onClicked(e) {
var cx = e.x;
var cy = e.y;
if (cx >= rx && cx <= rx + rw &&
cy >= ry && cy <= ry + rh) {
fadeout();
}
}
在onClicked()
函数内,我们可以计算出鼠标单击的 x 和 y 坐标。 我们将鼠标坐标与矩形的外部边界进行比较,如果鼠标坐标落在矩形的区域内,则将调用fadeout()
方法。
if (alpha < 0) {
canvas.removeEventListener("click", onClicked);
ctx.globalAlpha = 1;
ctx.fillStyle = 'white';
ctx.fillRect(rx, ry, rw, rh);
return;
}
当矩形完全透明时,我们将移走监听器并以不透明的白色填充该区域。 return
语句结束fadeout()
函数的递归调用。
ctx.clearRect(rx, ry, rw, rh);
ctx.globalAlpha = alpha;
ctx.fillRect(rx, ry, rw, rh)
矩形的区域被清除并填充有更新的 alpha 状态。
alpha -= 0.01;
alpha
值减小一小部分。
setTimeout(fadeout, DELAY);
在DELAY
ms 之后,从其内部调用fadeout()
方法。 这种做法称为递归。
泡泡
以下示例受到 Java 2D 演示示例的启发。
bubbles.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 canvas bubbles</title>
<style>
canvas {
border: 1px solid #bbb;
background: #000;
}
</style>
<script>
var cols = ["blue", "cadetblue", "green", "orange", "red", "yellow",
"gray", "white"];
const NUMBER_OF_CIRCLES = 35;
const DELAY = 30;
var maxSize;
var canvas;
var ctx;
var circles;
function Circle(x, y, r, c) {
this.x = x;
this.y = y;
this.r = r;
this.c = c;
}
function init() {
canvas = document.getElementById('myCanvas');
ctx = canvas.getContext('2d');
circles = new Array(NUMBER_OF_CIRCLES);
initCircles();
doStep();
}
function initCircles() {
var w = canvas.width;
var h = canvas.height;
maxSize = w / 10;
for (var i = 0; i < circles.length; i++) {
var rc = getRandomCoordinates();
var r = Math.floor(maxSize * Math.random());
var c = cols[Math.floor(Math.random()*cols.length)]
circles[i] = new Circle(rc[0], rc[1], r, c);
}
}
function doStep() {
for (var i = 0; i < circles.length; i++) {
var c = circles[i];
c.r += 1;
if (c.r > maxSize) {
var rc = getRandomCoordinates();
c.x = rc[0];
c.y = rc[1];
c.r = 1;
}
}
drawCircles();
setTimeout(doStep, DELAY);
}
function getRandomCoordinates() {
var w = canvas.width;
var h = canvas.height;
var x = Math.floor(Math.random() * (w - (maxSize / 2)));
var y = Math.floor(Math.random() * (h - (maxSize / 2)));
return [x, y];
}
function drawCircles() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (var i = 0; i < circles.length; i++) {
ctx.beginPath();
ctx.lineWidth = 2.5;
var c = circles[i];
ctx.strokeStyle = c.c;
ctx.arc(c.x, c.y, c.r, 0, 2*Math.PI);
ctx.stroke();
}
}
</script>
</head>
<body onload="init();">
<canvas id="myCanvas" width="350" height="250">
</canvas>
</body>
</html>
在该示例中,有越来越多的彩色气泡在屏幕上随机出现和消失。
var cols = ["blue", "cadetblue", "green", "orange", "red", "yellow",
"gray", "white"];
这些颜色用于绘制气泡。
function Circle(x, y, r, c) {
this.x = x;
this.y = y;
this.r = r;
this.c = c;
}
这是Circle
对象的构造器。 除了 x 和 y 坐标以及半径之外,它还包含颜色值的属性。
circles = new Array(NUMBER_OF_CIRCLES);
circles
数组用于容纳圆形对象。
for (var i = 0; i < circles.length; i++) {
var rc = getRandomCoordinates();
var r = Math.floor(maxSize * Math.random());
var c = cols[Math.floor(Math.random()*cols.length)]
circles[i] = new Circle(rc[0], rc[1], r, c);
}
circles
数组用圆圈填充。 我们计算随机坐标,随机初始半径和随机颜色值。
function doStep() {
doStep()
表示程序的动画周期。
for (var i = 0; i < circles.length; i++) {
var c = circles[i];
c.r += 1;
if (c.r > maxSize) {
var rc = getRandomCoordinates();
c.x = rc[0];
c.y = rc[1];
c.r = 1;
}
}
我们遍历circles
数组并增加每个圆的半径。 当圆达到最大大小时,它会随机重新定位并最小化。
setTimeout(doStep, DELAY);
setTimeout()
方法用于创建动画。 您可能需要调整DELAY
值以适合您的硬件。
function drawCircles() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (var i = 0; i < circles.length; i++) {
ctx.beginPath();
ctx.lineWidth = 2.5;
var c = circles[i];
ctx.strokeStyle = c.c;
ctx.arc(c.x, c.y, c.r, 0, 2*Math.PI);
ctx.stroke();
}
}
drawCircles()
函数清除画布并绘制数组中的所有圆圈。
星空
下面的示例创建一个星空动画。
starfield.html
<!DOCTYPE html>
<html>
<head>
<title>HTML5 canvas star field</title>
<script>
var canvas_w;
var canvas_h;
var canvas;
var ctx;
var layer1;
var layer2;
var layer3;
const DELAY = 20;
const N_STARS = 60;
const SPEED1 = 3;
const SPEED2 = 2;
const SPEED3 = 1;
function init() {
canvas = document.getElementById("myCanvas");
ctx = canvas.getContext("2d");
canvas_w = canvas.width;
canvas_h = canvas.height;
layer1 = new layer(N_STARS, SPEED1, "#ffffff");
layer2 = new layer(N_STARS, SPEED2, "#dddddd");
layer3 = new layer(N_STARS, SPEED3, "#999999");
setTimeout("drawLayers()", DELAY);
}
function star() {
this.x = Math.floor(Math.random()*canvas_w);
this.y = Math.floor(Math.random()*canvas_h);
this.move = function(speed) {
this.y = this.y + speed;
if (this.y > canvas_h) {
this.y = 0;
this.x = Math.floor(Math.random()*canvas_w);
}
}
this.draw = function(col) {
ctx.fillStyle = col;
ctx.fillRect(this.x, this.y , 1, 1);
}
}
function layer(n, sp, col) {
this.n = n;
this.sp = sp;
this.col = col;
this.stars = new Array(this.n);
for (var i=0; i < this.n; i++) {
this.stars[i] = new star();
}
this.moveLayer = function() {
for (var i=0; i < this.n; i++) {
this.stars[i].move(this.sp);
}
}
this.drawLayer = function() {
for (var i=0; i < this.n; i++) {
this.stars[i].draw(this.col);
}
}
}
function drawLayers() {
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, canvas_w, canvas_h);
layer1.moveLayer();
layer2.moveLayer();
layer3.moveLayer();
layer1.drawLayer();
layer2.drawLayer();
layer3.drawLayer();
setTimeout("drawLayers()", DELAY);
}
</script>
</head>
<body onload="init();">
<canvas id="myCanvas" width="800" height="600">
</canvas>
</body>
</html>
通过产生三个不同的图层来创建星空动画。 每层由具有不同速度和颜色阴影的星星(小点)组成。 前层的星星更亮,移动速度更快,背面的星星更暗,移动速度更慢。
layer1 = new layer(N_STARS, SPEED1, "#ffffff");
layer2 = new layer(N_STARS, SPEED2, "#dddddd");
layer3 = new layer(N_STARS, SPEED3, "#999999");
创建了三层星星。 它们具有不同的速度和颜色阴影。
function star() {
this.x = Math.floor(Math.random()*canvas_w);
this.y = Math.floor(Math.random()*canvas_h);
...
创建星后,它会被赋予随机坐标。
this.move = function(speed) {
this.y = this.y + speed;
if (this.y > canvas_h) {
this.y = 0;
this.x = Math.floor(Math.random()*canvas_w);
}
}
move()
方法移动星星; 它增加了它的 y 坐标。
this.draw = function(col) {
ctx.fillStyle = col;
ctx.fillRect(this.x, this.y , 1, 1);
}
draw()
方法在画布上绘制星星。 它使用fillRect()
方法以给定的颜色绘制一个小矩形。
function layer(n, sp, col) {
this.n = n;
this.sp = sp;
this.col = col;
this.stars = new Array(this.n);
...
一层是具有给定速度和颜色阴影的n
星的集合。 星星存储在stars
数组中。
for (var i=0; i < this.n; i++) {
this.stars[i] = new star();
}
创建图层后,stars
数组将填充星形对象。
this.moveLayer = function() {
for (var i=0; i < this.n; i++) {
this.stars[i].move(this.sp);
}
}
moveLayer()
方法遍历星星数组,并调用每个星星的move()
方法。
this.drawLayer = function() {
for (var i=0; i < this.n; i++) {
this.stars[i].draw(this.col);
}
}
同样,drawLayer()
方法调用每个星星的draw()
方法。
function drawLayers() {
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, canvas_w, canvas_h);
layer1.moveLayer();
layer2.moveLayer();
layer3.moveLayer();
layer1.drawLayer();
layer2.drawLayer();
layer3.drawLayer();
setTimeout("drawLayers()", DELAY);
}
drawLayers()
函数可移动每一层的星星并将其绘制在画布上。 它在DELAY
ms 之后调用自己,从而创建动画。
在 HTML5 画布教程的这一章中,我们介绍了动画。
HTML5 画布中的贪食蛇
在 HTML5 画布教程的这一部分中,我们将创建一个 Snake 游戏克隆。
Snake
贪食蛇是较旧的经典视频游戏。 它最初是在 70 年代后期创建的。 后来它被带到 PC 上。 在这个游戏中,玩家控制蛇。 目的是尽可能多地吃苹果。 蛇每次吃一个苹果,它的身体就会长大。 蛇必须避开墙壁和自己的身体。 该游戏有时称为 Nibbles 。
开发
蛇的每个关节的大小为 10 像素。 蛇由光标键控制。 最初,蛇具有三个关节。 如果游戏结束,则画布中间会显示"Game Over"
消息。
snake.html
<!DOCTYPE html>
<html>
<head>
<title>Snake in HTML5 canvas</title>
<style>
canvas {background: black}
</style>
<script>
var canvas;
var ctx;
var head;
var apple;
var ball;
var dots;
var apple_x;
var apple_y;
var leftDirection = false;
var rightDirection = true;
var upDirection = false;
var downDirection = false;
var inGame = true;
const DOT_SIZE = 10;
const ALL_DOTS = 900;
const MAX_RAND = 29;
const DELAY = 140;
const C_HEIGHT = 300;
const C_WIDTH = 300;
const LEFT_KEY = 37;
const RIGHT_KEY = 39;
const UP_KEY = 38;
const DOWN_KEY = 40;
var x = new Array(ALL_DOTS);
var y = new Array(ALL_DOTS);
function init() {
canvas = document.getElementById('myCanvas');
ctx = canvas.getContext('2d');
loadImages();
createSnake();
locateApple();
setTimeout("gameCycle()", DELAY);
}
function loadImages() {
head = new Image();
head.src = 'head.png';
ball = new Image();
ball.src = 'dot.png';
apple = new Image();
apple.src = 'apple.png';
}
function createSnake() {
dots = 3;
for (var z = 0; z < dots; z++) {
x[z] = 50 - z * 10;
y[z] = 50;
}
}
function checkApple() {
if ((x[0] == apple_x) && (y[0] == apple_y)) {
dots++;
locateApple();
}
}
function doDrawing() {
ctx.clearRect(0, 0, C_WIDTH, C_HEIGHT);
if (inGame) {
ctx.drawImage(apple, apple_x, apple_y);
for (var z = 0; z < dots; z++) {
if (z == 0) {
ctx.drawImage(head, x[z], y[z]);
} else {
ctx.drawImage(ball, x[z], y[z]);
}
}
} else {
gameOver();
}
}
function gameOver() {
ctx.fillStyle = 'white';
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
ctx.font = 'normal bold 18px serif';
ctx.fillText('Game over', C_WIDTH/2, C_HEIGHT/2);
}
function checkApple() {
if ((x[0] == apple_x) && (y[0] == apple_y)) {
dots++;
locateApple();
}
}
function move() {
for (var z = dots; z > 0; z--) {
x[z] = x[(z - 1)];
y[z] = y[(z - 1)];
}
if (leftDirection) {
x[0] -= DOT_SIZE;
}
if (rightDirection) {
x[0] += DOT_SIZE;
}
if (upDirection) {
y[0] -= DOT_SIZE;
}
if (downDirection) {
y[0] += DOT_SIZE;
}
}
function checkCollision() {
for (var z = dots; z > 0; z--) {
if ((z > 4) && (x[0] == x[z]) && (y[0] == y[z])) {
inGame = false;
}
}
if (y[0] >= C_HEIGHT) {
inGame = false;
}
if (y[0] < 0) {
inGame = false;
}
if (x[0] >= C_WIDTH) {
inGame = false;
}
if (x[0] < 0) {
inGame = false;
}
}
function locateApple() {
var r = Math.floor(Math.random() * MAX_RAND);
apple_x = r * DOT_SIZE;
r = Math.floor(Math.random() * MAX_RAND);
apple_y = r * DOT_SIZE;
}
function gameCycle() {
if (inGame) {
checkApple();
checkCollision();
move();
doDrawing();
setTimeout("gameCycle()", DELAY);
}
}
onkeydown = function(e) {
var key = e.keyCode;
if ((key == LEFT_KEY) && (!rightDirection)) {
leftDirection = true;
upDirection = false;
downDirection = false;
}
if ((key == RIGHT_KEY) && (!leftDirection)) {
rightDirection = true;
upDirection = false;
downDirection = false;
}
if ((key == UP_KEY) && (!downDirection)) {
upDirection = true;
rightDirection = false;
leftDirection = false;
}
if ((key == DOWN_KEY) && (!upDirection)) {
downDirection = true;
rightDirection = false;
leftDirection = false;
}
};
</script>
</head>
<body onload="init();">
<canvas id="myCanvas" width="300" height="300">
</canvas>
</body>
</html>
首先,我们将定义游戏中使用的常量。
const DOT_SIZE = 10;
const ALL_DOTS = 900;
const MAX_RAND = 29;
const DELAY = 140;
const C_HEIGHT = 300;
const C_WIDTH = 300;
DOT_SIZE
是苹果的大小和蛇的点。 ALL_DOTS
常数定义画布上可能的最大点数(900 = 300 * 300/10 * 10
)。 MAX_RAND
常数用于计算苹果的随机位置。 DELAY
常数确定游戏的速度。 C_HEIGHT
和C_WIDTH
常数存储画布的大小。
const LEFT_KEY = 37;
const RIGHT_KEY = 39;
const UP_KEY = 38;
const DOWN_KEY = 40;
这些常量存储箭头键的值。 它们用于提高可读性。
var x = new Array(ALL_DOTS);
var y = new Array(ALL_DOTS);
这两个数组存储蛇的所有关节的 x 和 y 坐标。
function init() {
canvas = document.getElementById('myCanvas');
ctx = canvas.getContext('2d');
loadImages();
createSnake();
locateApple();
setTimeout("gameCycle()", DELAY);
}
init()
函数获取对画布对象及其上下文的引用。 调用loadImages()
,createSnake()
和locateApple()
函数来执行特定任务。 setTimeout()
开始动画。
function loadImages() {
head = new Image();
head.src = 'head.png';
ball = new Image();
ball.src = 'dot.png';
apple = new Image();
apple.src = 'apple.png';
}
在loadImages()
函数中,我们检索游戏的图像。
function createSnake() {
dots = 3;
for (var z = 0; z < dots; z++) {
x[z] = 50 - z * 10;
y[z] = 50;
}
}
在createSnake()
函数中,我们创建蛇对象。 首先,它具有三个关节。
function checkApple() {
if ((x[0] == apple_x) && (y[0] == apple_y)) {
dots++;
locateApple();
}
}
如果头部与苹果相撞,我们会增加蛇的关节数。 我们称locateApple()
方法为随机放置一个新的Apple
对象。
在move()
方法中,我们有游戏的关键算法。 要了解它,请看一下蛇是如何运动的。 我们控制蛇的头。 我们可以使用光标键更改其方向。 其余关节在链上向上移动一个位置。 第二关节移动到第一个关节的位置,第三关节移动到第二个关节的位置,依此类推。
for (var z = dots; z > 0; z--) {
x[z] = x[(z - 1)];
y[z] = y[(z - 1)];
}
该代码将关节向上移动。
if (leftDirection) {
x[0] -= DOT_SIZE;
}
这条线将头向左移动。
在checkCollision()
方法中,我们确定蛇是否击中了自己或边界之一。
for (var z = dots; z > 0; z--) {
if ((z > 4) && (x[0] == x[z]) && (y[0] == y[z])) {
inGame = false;
}
}
如果蛇用头撞到其关节之一,则游戏结束。
if (y[0] >= C_HEIGHT) {
inGame = false;
}
如果蛇撞到画布底部,则游戏结束。
function locateApple() {
var r = Math.floor(Math.random() * MAX_RAND);
apple_x = r * DOT_SIZE;
r = Math.floor(Math.random() * MAX_RAND);
apple_y = r * DOT_SIZE;
}
locateApple()
随机选择苹果对象的 x 和 y 坐标。 apple_x
和apple_y
是苹果图像左上点的坐标。
function gameCycle() {
if (inGame) {
checkApple();
checkCollision();
move();
doDrawing();
setTimeout("gameCycle()", DELAY);
}
}
gameCycle()
函数形成游戏周期。 如果游戏尚未完成,我们将执行碰撞检测,移动和绘画。 setTimeout()
函数递归调用gameCycle()
函数。
if ((key == LEFT_KEY) && (!rightDirection)) {
leftDirection = true;
upDirection = false;
downDirection = false;
}
如果单击左光标键,则将leftDirection
变量设置为 true。 move()
函数中使用此变量来更改蛇对象的坐标。 还要注意,当蛇向右行驶时,我们不能立即向左转。
图:贪食蛇
这是 HTML5 画布中的贪食蛇游戏。
Java 益智游戏
在 Java 游戏教程的这一部分中,我们创建一个 Java 解谜游戏克隆。 源代码和图像可以在作者的 Github Java Swing 益智游戏存储库中找到。
Java 益智游戏要点
- 使用 Swing 和 Java 2D 图形来构建游戏。
- 用
Collections.shuffle()
随机打乱播放按钮。 - 用
ImageIO.read()
加载图像。 - 用
BufferedImage
调整图像大小。 - 用
CropImageFilter
裁剪图像。 - 用
GridLayout
布局按钮。 - 使用两个点的
ArrayLists
检查解决方案。
Java 益智游戏示例
这个小游戏的目的是形成一张图片。 通过单击包含图像的按钮来移动它们。 只能移动与空按钮相邻的按钮。
PuzzleEx.java
package com.zetcode;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.Image;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.awt.image.CropImageFilter;
import java.awt.image.FilteredImageSource;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
class MyButton extends JButton {
private boolean isLastButton;
public MyButton() {
super();
initUI();
}
public MyButton(Image image) {
super(new ImageIcon(image));
initUI();
}
private void initUI() {
isLastButton = false;
BorderFactory.createLineBorder(Color.gray);
addMouseListener(new MouseAdapter() {
@Override
public void mouseEntered(MouseEvent e) {
setBorder(BorderFactory.createLineBorder(Color.yellow));
}
@Override
public void mouseExited(MouseEvent e) {
setBorder(BorderFactory.createLineBorder(Color.gray));
}
});
}
public void setLastButton() {
isLastButton = true;
}
public boolean isLastButton() {
return isLastButton;
}
}
public class PuzzleEx extends JFrame {
private JPanel panel;
private BufferedImage source;
private BufferedImage resized;
private Image image;
private MyButton lastButton;
private int width, height;
private List<MyButton> buttons;
private List<Point> solution;
private final int NUMBER_OF_BUTTONS = 12;
private final int DESIRED_WIDTH = 300;
public PuzzleEx() {
initUI();
}
private void initUI() {
solution = new ArrayList<>();
solution.add(new Point(0, 0));
solution.add(new Point(0, 1));
solution.add(new Point(0, 2));
solution.add(new Point(1, 0));
solution.add(new Point(1, 1));
solution.add(new Point(1, 2));
solution.add(new Point(2, 0));
solution.add(new Point(2, 1));
solution.add(new Point(2, 2));
solution.add(new Point(3, 0));
solution.add(new Point(3, 1));
solution.add(new Point(3, 2));
buttons = new ArrayList<>();
panel = new JPanel();
panel.setBorder(BorderFactory.createLineBorder(Color.gray));
panel.setLayout(new GridLayout(4, 3, 0, 0));
try {
source = loadImage();
int h = getNewHeight(source.getWidth(), source.getHeight());
resized = resizeImage(source, DESIRED_WIDTH, h,
BufferedImage.TYPE_INT_ARGB);
} catch (IOException ex) {
Logger.getLogger(PuzzleEx.class.getName()).log(
Level.SEVERE, null, ex);
}
width = resized.getWidth(null);
height = resized.getHeight(null);
add(panel, BorderLayout.CENTER);
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 3; j++) {
image = createImage(new FilteredImageSource(resized.getSource(),
new CropImageFilter(j * width / 3, i * height / 4,
(width / 3), height / 4)));
MyButton button = new MyButton(image);
button.putClientProperty("position", new Point(i, j));
if (i == 3 && j == 2) {
lastButton = new MyButton();
lastButton.setBorderPainted(false);
lastButton.setContentAreaFilled(false);
lastButton.setLastButton();
lastButton.putClientProperty("position", new Point(i, j));
} else {
buttons.add(button);
}
}
}
Collections.shuffle(buttons);
buttons.add(lastButton);
for (int i = 0; i < NUMBER_OF_BUTTONS; i++) {
MyButton btn = buttons.get(i);
panel.add(btn);
btn.setBorder(BorderFactory.createLineBorder(Color.gray));
btn.addActionListener(new ClickAction());
}
pack();
setTitle("Puzzle");
setResizable(false);
setLocationRelativeTo(null);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
private int getNewHeight(int w, int h) {
double ratio = DESIRED_WIDTH / (double) w;
int newHeight = (int) (h * ratio);
return newHeight;
}
private BufferedImage loadImage() throws IOException {
BufferedImage bimg = ImageIO.read(new File("src/resources/icesid.jpg"));
return bimg;
}
private BufferedImage resizeImage(BufferedImage originalImage, int width,
int height, int type) throws IOException {
BufferedImage resizedImage = new BufferedImage(width, height, type);
Graphics2D g = resizedImage.createGraphics();
g.drawImage(originalImage, 0, 0, width, height, null);
g.dispose();
return resizedImage;
}
private class ClickAction extends AbstractAction {
@Override
public void actionPerformed(ActionEvent e) {
checkButton(e);
checkSolution();
}
private void checkButton(ActionEvent e) {
int lidx = 0;
for (MyButton button : buttons) {
if (button.isLastButton()) {
lidx = buttons.indexOf(button);
}
}
JButton button = (JButton) e.getSource();
int bidx = buttons.indexOf(button);
if ((bidx - 1 == lidx) || (bidx + 1 == lidx)
|| (bidx - 3 == lidx) || (bidx + 3 == lidx)) {
Collections.swap(buttons, bidx, lidx);
updateButtons();
}
}
private void updateButtons() {
panel.removeAll();
for (JComponent btn : buttons) {
panel.add(btn);
}
panel.validate();
}
}
private void checkSolution() {
List<Point> current = new ArrayList<>();
for (JComponent btn : buttons) {
current.add((Point) btn.getClientProperty("position"));
}
if (compareList(solution, current)) {
JOptionPane.showMessageDialog(panel, "Finished",
"Congratulation", JOptionPane.INFORMATION_MESSAGE);
}
}
public static boolean compareList(List ls1, List ls2) {
return ls1.toString().contentEquals(ls2.toString());
}
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
PuzzleEx puzzle = new PuzzleEx();
puzzle.setVisible(true);
}
});
}
}
我们使用的是冰河世纪电影中 Sid 角色的图像。 我们缩放图像并将其切成十二块。 这些片段由JButton
组件使用。 最后一块没有使用; 我们有一个空按钮。 您可以下载一些相当大的图片并在游戏中使用它。
addMouseListener(new MouseAdapter() {
@Override
public void mouseEntered(MouseEvent e) {
setBorder(BorderFactory.createLineBorder(Color.yellow));
}
@Override
public void mouseExited(MouseEvent e) {
setBorder(BorderFactory.createLineBorder(Color.gray));
}
});
当我们将鼠标指针悬停在按钮上时,其边框变为黄色。
public boolean isLastButton() {
return isLastButton;
}
有一个按钮称为最后一个按钮。 它是没有图像的按钮。 其他按钮与此空间交换空间。
private final int DESIRED_WIDTH = 300;
我们用来形成的图像被缩放以具有所需的宽度。 使用getNewHeight()
方法,我们可以计算新的高度,并保持图像的比例。
solution.add(new Point(0, 0));
solution.add(new Point(0, 1));
solution.add(new Point(0, 2));
solution.add(new Point(1, 0));
...
解决方案数组列表存储形成图像的按钮的正确顺序。 每个按钮由一个Point
标识。
panel.setLayout(new GridLayout(4, 3, 0, 0));
我们使用GridLayout
存储我们的组件。 布局由 4 行和 3 列组成。
image = createImage(new FilteredImageSource(resized.getSource(),
new CropImageFilter(j * width / 3, i * height / 4,
(width / 3), height / 4)));
CropImageFilter
用于从已调整大小的图像源中切出矩形。 它旨在与FilteredImageSource
对象结合使用,以生成现有图像的裁剪版本。
button.putClientProperty("position", new Point(i, j));
按钮由其position
客户端属性标识。 这是包含按钮在图片中正确的行和列的位置的点。 这些属性用于确定窗口中按钮的顺序是否正确。
if (i == 3 && j == 2) {
lastButton = new MyButton();
lastButton.setBorderPainted(false);
lastButton.setContentAreaFilled(false);
lastButton.setLastButton();
lastButton.putClientProperty("position", new Point(i, j));
} else {
buttons.add(button);
}
没有图像的按钮称为最后一个按钮。 它放置在右下角网格的末端。 该按钮与被单击的相邻按钮交换位置。 我们使用setLastButton()
方法设置其isLastButton
标志。
Collections.shuffle(buttons);
buttons.add(lastButton);
我们随机重新排列buttons
列表的元素。 最后一个按钮,即没有图像的按钮,被插入到列表的末尾。 它不应该被改组,它总是在我们开始益智游戏时结束。
for (int i = 0; i < NUMBER_OF_BUTTONS; i++) {
MyButton btn = buttons.get(i);
panel.add(btn);
btn.setBorder(BorderFactory.createLineBorder(Color.gray));
btn.addActionListener(new ClickAction());
}
buttons
列表中的所有组件都放置在面板上。 我们在按钮周围创建一些灰色边框,并添加一个点击动作监听器。
private int getNewHeight(int w, int h) {
double ratio = DESIRED_WIDTH / (double) w;
int newHeight = (int) (h * ratio);
return newHeight;
}
getNewHeight()
方法根据所需的宽度计算图像的高度。 图像的比例保持不变。 我们使用这些值缩放图像。
private BufferedImage loadImage() throws IOException {
BufferedImage bimg = ImageIO.read(new File("src/resources/icesid.jpg"));
return bimg;
}
从磁盘加载了 JPG 图像。 ImageIO
的read()
方法返回BufferedImage
,这是 Swing 操纵图像的重要类。
private BufferedImage resizeImage(BufferedImage originalImage, int width,
int height, int type) throws IOException {
BufferedImage resizedImage = new BufferedImage(width, height, type);
Graphics2D g = resizedImage.createGraphics();
g.drawImage(originalImage, 0, 0, width, height, null);
g.dispose();
return resizedImage;
}
通过创建具有新大小的新BufferedImage
来调整原始图像的大小。 我们将原始图像绘制到此新的缓冲图像中。
private void checkButton(ActionEvent e) {
int lidx = 0;
for (MyButton button : buttons) {
if (button.isLastButton()) {
lidx = buttons.indexOf(button);
}
}
JButton button = (JButton) e.getSource();
int bidx = buttons.indexOf(button);
if ((bidx - 1 == lidx) || (bidx + 1 == lidx)
|| (bidx - 3 == lidx) || (bidx + 3 == lidx)) {
Collections.swap(buttons, bidx, lidx);
updateButtons();
}
}
按钮存储在数组列表中。 然后,此列表将映射到面板的网格。 我们得到最后一个按钮和被单击按钮的索引。 如果它们相邻,则使用Collections.swap()
进行交换。
private void updateButtons() {
panel.removeAll();
for (JComponent btn : buttons) {
panel.add(btn);
}
panel.validate();
}
updateButtons()
方法将列表映射到面板的网格。 首先,使用removeAll()
方法删除所有组件。 for
循环用于通过buttons
列表,将重新排序的按钮添加回面板的布局管理器。 最后,validate()
方法实现了新的布局。
private void checkSolution() {
List<Point> current = new ArrayList<>();
for (JComponent btn : buttons) {
current.add((Point) btn.getClientProperty("position"));
}
if (compareList(solution, current)) {
JOptionPane.showMessageDialog(panel, "Finished",
"Congratulation", JOptionPane.INFORMATION_MESSAGE);
}
}
通过将正确排序的按钮的点列表与包含窗口中按钮顺序的当前列表进行比较,来完成解决方案检查。 如果解决方案出现,则会显示一个消息对话框。
图:创建图像
这是用 Java 创建的益智游戏。
Java 贪食蛇
在 Java 2D 游戏教程的这一部分中,我们创建一个 Java 贪食蛇游戏克隆。 源代码和图像可以在作者的 Github Java-Snake-Game 存储库中找到。
贪食蛇
贪食蛇是较旧的经典视频游戏。 它最初是在 70 年代后期创建的。 后来它被带到 PC 上。 在这个游戏中,玩家控制蛇。 目的是尽可能多地吃苹果。 蛇每吃一个苹果,它的身体就会长大。 蛇必须避开墙壁和自己的身体。 该游戏有时称为 Nibbles 。
Java 贪食蛇游戏的开发
蛇的每个关节的大小为 10 像素。 蛇由光标键控制。 最初,蛇具有三个关节。 如果游戏结束,则在面板中间显示"Game Over"
消息。
Board.java
package com.zetcode;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import javax.swing.ImageIcon;
import javax.swing.JPanel;
import javax.swing.Timer;
public class Board extends JPanel implements ActionListener {
private final int B_WIDTH = 300;
private final int B_HEIGHT = 300;
private final int DOT_SIZE = 10;
private final int ALL_DOTS = 900;
private final int RAND_POS = 29;
private final int DELAY = 140;
private final int x[] = new int[ALL_DOTS];
private final int y[] = new int[ALL_DOTS];
private int dots;
private int apple_x;
private int apple_y;
private boolean leftDirection = false;
private boolean rightDirection = true;
private boolean upDirection = false;
private boolean downDirection = false;
private boolean inGame = true;
private Timer timer;
private Image ball;
private Image apple;
private Image head;
public Board() {
initBoard();
}
private void initBoard() {
addKeyListener(new TAdapter());
setBackground(Color.black);
setFocusable(true);
setPreferredSize(new Dimension(B_WIDTH, B_HEIGHT));
loadImages();
initGame();
}
private void loadImages() {
ImageIcon iid = new ImageIcon("src/resources/dot.png");
ball = iid.getImage();
ImageIcon iia = new ImageIcon("src/resources/apple.png");
apple = iia.getImage();
ImageIcon iih = new ImageIcon("src/resources/head.png");
head = iih.getImage();
}
private void initGame() {
dots = 3;
for (int z = 0; z < dots; z++) {
x[z] = 50 - z * 10;
y[z] = 50;
}
locateApple();
timer = new Timer(DELAY, this);
timer.start();
}
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
doDrawing(g);
}
private void doDrawing(Graphics g) {
if (inGame) {
g.drawImage(apple, apple_x, apple_y, this);
for (int z = 0; z < dots; z++) {
if (z == 0) {
g.drawImage(head, x[z], y[z], this);
} else {
g.drawImage(ball, x[z], y[z], this);
}
}
Toolkit.getDefaultToolkit().sync();
} else {
gameOver(g);
}
}
private void gameOver(Graphics g) {
String msg = "Game Over";
Font small = new Font("Helvetica", Font.BOLD, 14);
FontMetrics metr = getFontMetrics(small);
g.setColor(Color.white);
g.setFont(small);
g.drawString(msg, (B_WIDTH - metr.stringWidth(msg)) / 2, B_HEIGHT / 2);
}
private void checkApple() {
if ((x[0] == apple_x) && (y[0] == apple_y)) {
dots++;
locateApple();
}
}
private void move() {
for (int z = dots; z > 0; z--) {
x[z] = x[(z - 1)];
y[z] = y[(z - 1)];
}
if (leftDirection) {
x[0] -= DOT_SIZE;
}
if (rightDirection) {
x[0] += DOT_SIZE;
}
if (upDirection) {
y[0] -= DOT_SIZE;
}
if (downDirection) {
y[0] += DOT_SIZE;
}
}
private void checkCollision() {
for (int z = dots; z > 0; z--) {
if ((z > 4) && (x[0] == x[z]) && (y[0] == y[z])) {
inGame = false;
}
}
if (y[0] >= B_HEIGHT) {
inGame = false;
}
if (y[0] < 0) {
inGame = false;
}
if (x[0] >= B_WIDTH) {
inGame = false;
}
if (x[0] < 0) {
inGame = false;
}
if (!inGame) {
timer.stop();
}
}
private void locateApple() {
int r = (int) (Math.random() * RAND_POS);
apple_x = ((r * DOT_SIZE));
r = (int) (Math.random() * RAND_POS);
apple_y = ((r * DOT_SIZE));
}
@Override
public void actionPerformed(ActionEvent e) {
if (inGame) {
checkApple();
checkCollision();
move();
}
repaint();
}
private class TAdapter extends KeyAdapter {
@Override
public void keyPressed(KeyEvent e) {
int key = e.getKeyCode();
if ((key == KeyEvent.VK_LEFT) && (!rightDirection)) {
leftDirection = true;
upDirection = false;
downDirection = false;
}
if ((key == KeyEvent.VK_RIGHT) && (!leftDirection)) {
rightDirection = true;
upDirection = false;
downDirection = false;
}
if ((key == KeyEvent.VK_UP) && (!downDirection)) {
upDirection = true;
rightDirection = false;
leftDirection = false;
}
if ((key == KeyEvent.VK_DOWN) && (!upDirection)) {
downDirection = true;
rightDirection = false;
leftDirection = false;
}
}
}
}
首先,我们将定义游戏中使用的常量。
private final int B_WIDTH = 300;
private final int B_HEIGHT = 300;
private final int DOT_SIZE = 10;
private final int ALL_DOTS = 900;
private final int RAND_POS = 29;
private final int DELAY = 140;
B_WIDTH
和B_HEIGHT
常数确定电路板的大小。 DOT_SIZE
是苹果的大小和蛇的点。 ALL_DOTS
常数定义了板上可能的最大点数(900 = (300 * 300) / (10 * 10)
)。 RAND_POS
常数用于计算苹果的随机位置。 DELAY
常数确定游戏的速度。
private final int x[] = new int[ALL_DOTS];
private final int y[] = new int[ALL_DOTS];
这两个数组存储蛇的所有关节的 x 和 y 坐标。
private void loadImages() {
ImageIcon iid = new ImageIcon("src/resources/dot.png");
ball = iid.getImage();
ImageIcon iia = new ImageIcon("src/resources/apple.png");
apple = iia.getImage();
ImageIcon iih = new ImageIcon("src/resources/head.png");
head = iih.getImage();
}
在loadImages()
方法中,我们获得了游戏的图像。 ImageIcon
类用于显示 PNG 图像。
private void initGame() {
dots = 3;
for (int z = 0; z < dots; z++) {
x[z] = 50 - z * 10;
y[z] = 50;
}
locateApple();
timer = new Timer(DELAY, this);
timer.start();
}
在initGame()
方法中,我们创建蛇,在板上随机放置一个苹果,然后启动计时器。
private void checkApple() {
if ((x[0] == apple_x) && (y[0] == apple_y)) {
dots++;
locateApple();
}
}
如果苹果与头部碰撞,我们会增加蛇的关节数。 我们称locateApple()
方法为随机放置一个新的Apple
对象。
在move()
方法中,我们有游戏的关键算法。 要了解它,请看一下蛇是如何运动的。 我们控制蛇的头。 我们可以使用光标键更改其方向。 其余关节在链上向上移动一个位置。 第二关节移动到第一个关节的位置,第三关节移动到第二个关节的位置,依此类推。
for (int z = dots; z > 0; z--) {
x[z] = x[(z - 1)];
y[z] = y[(z - 1)];
}
该代码将关节向上移动。
if (leftDirection) {
x[0] -= DOT_SIZE;
}
这条线将头向左移动。
在checkCollision()
方法中,我们确定蛇是否击中了自己或撞墙之一。
for (int z = dots; z > 0; z--) {
if ((z > 4) && (x[0] == x[z]) && (y[0] == y[z])) {
inGame = false;
}
}
如果蛇用头撞到其关节之一,则游戏结束。
if (y[0] >= B_HEIGHT) {
inGame = false;
}
如果蛇击中了棋盘的底部,则游戏结束。
Snake.java
package com.zetcode;
import java.awt.EventQueue;
import javax.swing.JFrame;
public class Snake extends JFrame {
public Snake() {
initUI();
}
private void initUI() {
add(new Board());
setResizable(false);
pack();
setTitle("Snake");
setLocationRelativeTo(null);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
JFrame ex = new Snake();
ex.setVisible(true);
});
}
}
这是主要的类。
setResizable(false);
pack();
setResizable()
方法会影响某些平台上JFrame
容器的插入。 因此,在pack()
方法之前调用它很重要。 否则,蛇的头部与右边界和底边界的碰撞可能无法正常进行。
图:贪食蛇
这是 Java 中的贪食蛇游戏。
Breakout 游戏
原文: https://zetcode.com/tutorials/javagamestutorial/breakout/
在 Java 2D 游戏教程的这一部分中,我们创建一个简单的 Breakout 游戏克隆。 源代码和图像可以在作者的 Github Java-Breakout-Game 存储库中找到。
Breakout 是一款最初由 Atari Inc. 开发的街机游戏。该游戏创建于 1976 年。
在此游戏中,玩家移动屏幕上的桨叶并弹起一个或多个球。 目的是销毁窗口顶部的砖块。
开发
在我们的游戏中,我们只有一个桨,一个球和 30 块砖。 我在 Inkscape 中为球,桨和砖块创建了图像。 我们使用计时器来创建游戏周期。 我们不使用角度,只是改变方向。 顶部,底部,左侧和右侧。 pybreakout 游戏启发了我。 它是由 Nathan Dawson 在 PyGame 库中开发的。
游戏包含七个文件:Commons.java
,Sprite.java
,Ball.java
,Paddle.java
,Brick.java
,Board.java
和Breakout.java
。
com/zetcode/Commons.java
package com.zetcode;
public interface Commons {
int WIDTH = 300;
int HEIGHT = 400;
int BOTTOM_EDGE = 390;
int N_OF_BRICKS = 30;
int INIT_PADDLE_X = 200;
int INIT_PADDLE_Y = 360;
int INIT_BALL_X = 230;
int INIT_BALL_Y = 355;
int PERIOD = 10;
}
Commons.java
文件具有一些公共常数。 WIDTH
和HEIGHT
常数存储电路板的大小。 当球通过BOTTOM_EDGE
时,比赛结束。 N_OF_BRICKS
是游戏中的积木数量。 INIT_PADDLE_X
和INIT_PADDLE_Y
是桨状对象的初始坐标。 INIT_BALL_X
和INIT_BALL_Y
是球对象的初始坐标。 DELAY
是执行任务之前的初始延迟(以毫秒为单位),PERIOD
是形成游戏周期的连续任务执行之间的时间(以毫秒为单位)。
com/zetcode/Sprite.java
package com.zetcode;
import java.awt.Image;
import java.awt.Rectangle;
public class Sprite {
int x;
int y;
int imageWidth;
int imageHeight;
Image image;
protected void setX(int x) {
this.x = x;
}
int getX() {
return x;
}
protected void setY(int y) {
this.y = y;
}
int getY() {
return y;
}
int getImageWidth() {
return imageWidth;
}
int getImageHeight() {
return imageHeight;
}
Image getImage() {
return image;
}
Rectangle getRect() {
return new Rectangle(x, y,
image.getWidth(null), image.getHeight(null));
}
void getImageDimensions() {
imageWidth = image.getWidth(null);
imageHeight = image.getHeight(null);
}
}
Sprite
类是Board
中所有对象的基类。 我们将Ball
,Brick
和Paddle
对象中的所有方法和变量都放在此处,例如getImage()
或getX()
方法。
com/zetcode/Brick.java
package com.zetcode;
import javax.swing.ImageIcon;
public class Brick extends Sprite {
private boolean destroyed;
public Brick(int x, int y) {
initBrick(x, y);
}
private void initBrick(int x, int y) {
this.x = x;
this.y = y;
destroyed = false;
loadImage();
getImageDimensions();
}
private void loadImage() {
var ii = new ImageIcon("src/resources/brick.png");
image = ii.getImage();
}
boolean isDestroyed() {
return destroyed;
}
void setDestroyed(boolean val) {
destroyed = val;
}
}
这是Brick
类。
private boolean destroyed;
在destroyed
变量中,我们保留砖的状态。
com/zetcode/Ball.java
package com.zetcode;
import javax.swing.ImageIcon;
public class Ball extends Sprite {
private int xdir;
private int ydir;
public Ball() {
initBall();
}
private void initBall() {
xdir = 1;
ydir = -1;
loadImage();
getImageDimensions();
resetState();
}
private void loadImage() {
var ii = new ImageIcon("src/resources/ball.png");
image = ii.getImage();
}
void move() {
x += xdir;
y += ydir;
if (x == 0) {
setXDir(1);
}
if (x == Commons.WIDTH - imageWidth) {
System.out.println(imageWidth);
setXDir(-1);
}
if (y == 0) {
setYDir(1);
}
}
private void resetState() {
x = Commons.INIT_BALL_X;
y = Commons.INIT_BALL_Y;
}
void setXDir(int x) {
xdir = x;
}
void setYDir(int y) {
ydir = y;
}
int getYDir() {
return ydir;
}
}
这是Ball
类。
void move() {
x += xdir;
y += ydir;
if (x == 0) {
setXDir(1);
}
if (x == Commons.WIDTH - imageWidth) {
System.out.println(imageWidth);
setXDir(-1);
}
if (y == 0) {
setYDir(1);
}
}
move()
方法将球移到Board
上。 如果球撞到边界,方向将相应更改。
void setXDir(int x) {
xdir = x;
}
void setYDir(int y) {
ydir = y;
}
当球击中桨或砖时,将调用这两种方法。
com/zetcode/Paddle.java
package com.zetcode;
import java.awt.event.KeyEvent;
import javax.swing.ImageIcon;
public class Paddle extends Sprite {
private int dx;
public Paddle() {
initPaddle();
}
private void initPaddle() {
loadImage();
getImageDimensions();
resetState();
}
private void loadImage() {
var ii = new ImageIcon("src/resources/paddle.png");
image = ii.getImage();
}
void move() {
x += dx;
if (x <= 0) {
x = 0;
}
if (x >= Commons.WIDTH - imageWidth) {
x = Commons.WIDTH - imageWidth;
}
}
void keyPressed(KeyEvent e) {
int key = e.getKeyCode();
if (key == KeyEvent.VK_LEFT) {
dx = -1;
}
if (key == KeyEvent.VK_RIGHT) {
dx = 1;
}
}
void keyReleased(KeyEvent e) {
int key = e.getKeyCode();
if (key == KeyEvent.VK_LEFT) {
dx = 0;
}
if (key == KeyEvent.VK_RIGHT) {
dx = 0;
}
}
private void resetState() {
x = Commons.INIT_PADDLE_X;
y = Commons.INIT_PADDLE_Y;
}
}
这是Paddle
类。 它封装了 Breakout 游戏中的桨对象。 操纵杆通过左右箭头键控制。 通过按箭头键,我们设置方向变量。 通过释放箭头键,将dx
变量设置为零。 这样,桨停止移动。
void move() {
x += dx;
if (x <= 0) {
x = 0;
}
if (x >= Commons.WIDTH - imageWidth) {
x = Commons.WIDTH - imageWidth;
}
}
桨叶仅在水平方向上移动,因此我们仅更新 x 坐标。 如果条件确保桨不通过窗口边缘。
com/zetcode/Board.java
package com.zetcode;
import javax.swing.JPanel;
import javax.swing.Timer;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
public class Board extends JPanel {
private Timer timer;
private String message = "Game Over";
private Ball ball;
private Paddle paddle;
private Brick[] bricks;
private boolean inGame = true;
public Board() {
initBoard();
}
private void initBoard() {
addKeyListener(new TAdapter());
setFocusable(true);
setPreferredSize(new Dimension(Commons.WIDTH, Commons.HEIGHT));
gameInit();
}
private void gameInit() {
bricks = new Brick[Commons.N_OF_BRICKS];
ball = new Ball();
paddle = new Paddle();
int k = 0;
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 6; j++) {
bricks[k] = new Brick(j * 40 + 30, i * 10 + 50);
k++;
}
}
timer = new Timer(Commons.PERIOD, new GameCycle());
timer.start();
}
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
var g2d = (Graphics2D) g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY);
if (inGame) {
drawObjects(g2d);
} else {
gameFinished(g2d);
}
Toolkit.getDefaultToolkit().sync();
}
private void drawObjects(Graphics2D g2d) {
g2d.drawImage(ball.getImage(), ball.getX(), ball.getY(),
ball.getImageWidth(), ball.getImageHeight(), this);
g2d.drawImage(paddle.getImage(), paddle.getX(), paddle.getY(),
paddle.getImageWidth(), paddle.getImageHeight(), this);
for (int i = 0; i < Commons.N_OF_BRICKS; i++) {
if (!bricks[i].isDestroyed()) {
g2d.drawImage(bricks[i].getImage(), bricks[i].getX(),
bricks[i].getY(), bricks[i].getImageWidth(),
bricks[i].getImageHeight(), this);
}
}
}
private void gameFinished(Graphics2D g2d) {
var font = new Font("Verdana", Font.BOLD, 18);
FontMetrics fontMetrics = this.getFontMetrics(font);
g2d.setColor(Color.BLACK);
g2d.setFont(font);
g2d.drawString(message,
(Commons.WIDTH - fontMetrics.stringWidth(message)) / 2,
Commons.WIDTH / 2);
}
private class TAdapter extends KeyAdapter {
@Override
public void keyReleased(KeyEvent e) {
paddle.keyReleased(e);
}
@Override
public void keyPressed(KeyEvent e) {
paddle.keyPressed(e);
}
}
private class GameCycle implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
doGameCycle();
}
}
private void doGameCycle() {
ball.move();
paddle.move();
checkCollision();
repaint();
}
private void stopGame() {
inGame = false;
timer.stop();
}
private void checkCollision() {
if (ball.getRect().getMaxY() > Commons.BOTTOM_EDGE) {
stopGame();
}
for (int i = 0, j = 0; i < Commons.N_OF_BRICKS; i++) {
if (bricks[i].isDestroyed()) {
j++;
}
if (j == Commons.N_OF_BRICKS) {
message = "Victory";
stopGame();
}
}
if ((ball.getRect()).intersects(paddle.getRect())) {
int paddleLPos = (int) paddle.getRect().getMinX();
int ballLPos = (int) ball.getRect().getMinX();
int first = paddleLPos + 8;
int second = paddleLPos + 16;
int third = paddleLPos + 24;
int fourth = paddleLPos + 32;
if (ballLPos < first) {
ball.setXDir(-1);
ball.setYDir(-1);
}
if (ballLPos >= first && ballLPos < second) {
ball.setXDir(-1);
ball.setYDir(-1 * ball.getYDir());
}
if (ballLPos >= second && ballLPos < third) {
ball.setXDir(0);
ball.setYDir(-1);
}
if (ballLPos >= third && ballLPos < fourth) {
ball.setXDir(1);
ball.setYDir(-1 * ball.getYDir());
}
if (ballLPos > fourth) {
ball.setXDir(1);
ball.setYDir(-1);
}
}
for (int i = 0; i < Commons.N_OF_BRICKS; i++) {
if ((ball.getRect()).intersects(bricks[i].getRect())) {
int ballLeft = (int) ball.getRect().getMinX();
int ballHeight = (int) ball.getRect().getHeight();
int ballWidth = (int) ball.getRect().getWidth();
int ballTop = (int) ball.getRect().getMinY();
var pointRight = new Point(ballLeft + ballWidth + 1, ballTop);
var pointLeft = new Point(ballLeft - 1, ballTop);
var pointTop = new Point(ballLeft, ballTop - 1);
var pointBottom = new Point(ballLeft, ballTop + ballHeight + 1);
if (!bricks[i].isDestroyed()) {
if (bricks[i].getRect().contains(pointRight)) {
ball.setXDir(-1);
} else if (bricks[i].getRect().contains(pointLeft)) {
ball.setXDir(1);
}
if (bricks[i].getRect().contains(pointTop)) {
ball.setYDir(1);
} else if (bricks[i].getRect().contains(pointBottom)) {
ball.setYDir(-1);
}
bricks[i].setDestroyed(true);
}
}
}
}
}
这是Board
类。 这里我们把游戏逻辑。
private void gameInit() {
bricks = new Brick[Commons.N_OF_BRICKS];
ball = new Ball();
paddle = new Paddle();
int k = 0;
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 6; j++) {
bricks[k] = new Brick(j * 40 + 30, i * 10 + 50);
k++;
}
}
timer = new Timer(Commons.PERIOD, new GameCycle());
timer.start();
}
在gameInit()
方法中,我们创建一个球,一个球拍和三十块积木。 然后,我们创建并启动一个计时器。
if (inGame) {
drawObjects(g2d);
} else {
gameFinished(g2d);
}
根据inGame
变量,我们可以使用drawObjects()
方法绘制所有对象,也可以使用gameFinished()
方法完成游戏。
private void drawObjects(Graphics2D g2d) {
g2d.drawImage(ball.getImage(), ball.getX(), ball.getY(),
ball.getImageWidth(), ball.getImageHeight(), this);
g2d.drawImage(paddle.getImage(), paddle.getX(), paddle.getY(),
paddle.getImageWidth(), paddle.getImageHeight(), this);
for (int i = 0; i < Commons.N_OF_BRICKS; i++) {
if (!bricks[i].isDestroyed()) {
g2d.drawImage(bricks[i].getImage(), bricks[i].getX(),
bricks[i].getY(), bricks[i].getImageWidth(),
bricks[i].getImageHeight(), this);
}
}
}
drawObjects()
方法绘制游戏的所有对象。 使用drawImage()
方法绘制精灵。
private void gameFinished(Graphics2D g2d) {
var font = new Font("Verdana", Font.BOLD, 18);
FontMetrics fontMetrics = this.getFontMetrics(font);
g2d.setColor(Color.BLACK);
g2d.setFont(font);
g2d.drawString(message,
(Commons.WIDTH - fontMetrics.stringWidth(message)) / 2,
Commons.WIDTH / 2);
}
gameFinished()
方法将"Game Over"
或"Victory"
绘制到窗口的中间。
private class GameCycle implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
doGameCycle();
}
}
计时器会定期调用actionPerformed()
方法,该方法又调用doGameCycle()
方法,从而创建游戏周期。
private void doGameCycle() {
ball.move();
paddle.move();
checkCollision();
repaint();
}
doGameCycle()
移动球和球拍。 我们检查是否可能发生碰撞并重新粉刷屏幕。
private void checkCollision() {
if (ball.getRect().getMaxY() > Commons.BOTTOM_EDGE) {
stopGame();
}
...
如果球触底,我们将停止比赛。
for (int i = 0, j = 0; i < Commons.N_OF_BRICKS; i++) {
if (bricks[i].isDestroyed()) {
j++;
}
if (j == Commons.N_OF_BRICKS) {
message = "Victory";
stopGame();
}
}
我们检查了多少砖被破坏了。 如果我们摧毁了所有N_OF_BRICKS bricks
,我们将赢得这场比赛。
if (ballLPos < first) {
ball.setXDir(-1);
ball.setYDir(-1);
}
如果球碰到了桨的第一部分,我们会将球的方向更改为西北。
if (bricks[i].getRect().contains(pointTop)) {
ball.setYDir(1);
}...
如果球撞击砖的底部,我们将改变球的 y 方向; 它下降了。
com/zetcode/Breakout.java
package com.zetcode;
import javax.swing.JFrame;
import java.awt.EventQueue;
public class Breakout extends JFrame {
public Breakout() {
initUI();
}
private void initUI() {
add(new Board());
setTitle("Breakout");
setDefaultCloseOperation(EXIT_ON_CLOSE);
setLocationRelativeTo(null);
setResizable(false);
pack();
}
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
var game = new Breakout();
game.setVisible(true);
});
}
}
这是具有主要输入方法的Breakout
类。
图:打砖块游戏
这是打砖块游戏。