
代码结构与模块拆分
代码延续了点阵动画的核心框架,主要分为常量配置 、数据存储类 、工具函数 、核心逻辑类 和渲染控制五部分:
1. 常量配置(参数化控制)
python
# 画布与位置参数
CANVAS_WIDTH = 600
CANVAS_HEIGHT = 400
CANVAS_CENTER_X = CANVAS_WIDTH / 2 # 画布中心X坐标
CANVAS_CENTER_Y = CANVAS_HEIGHT / 2 # 画布中心Y坐标
# 视觉与颜色参数(蓝色系核心)
IMAGE_ENLARGE = 32 # 缩放比例(控制鱼的大小)
FISH_BODY = "#42a5f5" # 鱼身亮蓝
FISH_FIN = "#1e88e5" # 鱼鳍深蓝(增强立体感)
FISH_EYE = "#000000" # 纯黑眼睛(高对比度,突出鱼头)
FISH_EYE_WHITE = "#ffffff" # 眼球高光(增强真实感)
# 动画与点数量参数
FRAME_DELAY = 120 # 帧延迟(控制动画速度,值越小越快)
NUM_BODY = 700 # 身体点数量
NUM_HEAD = 400 # 鱼头点数量(单独设置,强化清晰度)
NUM_FIN = 200 # 鱼鳍点数量
NUM_TAIL = 300 # 尾部点数量
- 颜色参数采用蓝色系渐变,从亮蓝(鱼身)到深蓝(鱼鳍),配合黑色眼睛形成清晰视觉层次
- 鱼头点数量(
NUM_HEAD)单独设置且密度较高,是鱼头清晰的关键参数
2. 数据存储类(FishParams)
python
class FishParams:
def __init__(self):
self.body = set() # 身体点集合
self.head = set() # 鱼头点集合(独立存储,便于单独处理)
self.fin = set() # 鱼鳍点集合
self.tail = set() # 尾部点集合
self.eye = set() # 黑眼球点集合
self.eye_white = set() # 眼球高光点集合
self.all_points = {} # 按帧存储所有点数据(帧号: 点列表)
- 通过分集合存储不同部位的点,实现对鱼头、眼睛等关键部位的精细化控制
- 使用
set避免重复点,保证动画流畅性;all_points预存每帧数据,减少实时计算压力
3. 工具函数(形状生成与点处理)
这些函数是构建鱼形和动态效果的核心:
fish_shape(t, shrink_ratio):生成鱼的基础轮廓点 基于分段极坐标方程生成鱼的轮廓:
python
if 0 <= t < pi/2: # 头部前端(圆润饱满)
x = (t - pi/4) / (pi/4) * 1.5 # 横向范围扩大
y = 0.8 * cos(t) # 上下宽度增加
elif pi/2 <= t < pi: # 头部到身体过渡
x = (t - pi/2) / (pi/2) * 2 + 1.5
y = 0.8 * cos(t)
else: # 身体到尾部(收窄)
x = (3*pi/2 - t) / (pi/2) * 3 + 1.5
y = -0.6 * cos(t)
-
head_details(x, y):生成鱼头细节点专门强化鱼头特征:- 黑眼球:2x2 像素块(
eye集合),位置固定在头部前端 - 眼球高光:1x1 白色点(
eye_white集合),模拟反光 - 头部轮廓点:围绕眼睛的弧形点(
head_edge),强化头部边界
- 黑眼球:2x2 像素块(
-
scatter_points(x, y, is_head):点的散射处理对基础点进行随机偏移,模拟轮廓的自然过渡:- 鱼头点(
is_head=True)散射率降低 50%,避免模糊(ratio_x * 0.3) - 身体和尾部点散射率较高,形成柔和边缘
- 鱼头点(
-
shrink_points(x, y, ratio, is_head):点的收缩处理基于点到中心的距离计算收缩力,控制点的聚集效果:- 鱼头点收缩幅度更小(
dx * 0.12),保持清晰形状 - 尾部点收缩幅度更大,增强摆动感
- 鱼头点收缩幅度更小(
-
swim_curve(p):游动节奏控制函数基于正弦函数生成周期性曲线,控制点的运动幅度:
python
return 1.5 * (sin(3.5 * p) + 0.3 * sin(6.5 * p)) / (2 * pi)
4. 核心逻辑类(Fish)
封装了鱼的构建、动画计算和渲染逻辑:
-
build():初始化生成所有基础点- 身体点:通过循环调用
fish_shape生成NUM_BODY个点 - 鱼头点:专门在头部角度范围(
0 <= t < pi/1.5)生成NUM_HEAD个点,密度更高 - 细节点:调用
head_details生成眼睛、高光和头部轮廓点 - 鱼鳍和尾部点:在对应区域生成辅助点,强化结构
- 身体点:通过循环调用
-
calc(frame):计算每帧的点位置- 根据
swim_curve计算当前帧的运动比例(ratio) - 分区域计算点的位置:头部点摆动幅度小(
ratio * 0.7),尾部点摆动幅度大(ratio * 2) - 所有点位置存储到
all_points[frame],供渲染使用
- 根据
-
render(canvas, frame):绘制当前帧- 从
all_points中获取当前帧的点数据 - 按点类型(头部 / 身体 / 鳍 / 眼睛等)分配对应颜色,调用
create_rectangle绘制点阵 - 眼睛和高光单独处理,确保清晰可见
- 从
5. 渲染控制(draw函数与主程序)
draw函数:递归实现动画循环
python
def draw(main, canvas, fish, frame=0):
canvas.delete('all') # 清空画布
fish.render(canvas, frame) # 绘制当前帧
main.after(FRAME_DELAY, draw, main, canvas, fish, frame+1) # 延迟后绘制下一帧
主程序:初始化窗口和画布,启动动画
python
root = Tk()
canvas = Canvas(root, bg='#e0f7fa', ...) # 浅蓝色背景模拟水面
fish = Fish() # 创建鱼实例
draw(root, canvas, fish) # 启动动画
root.mainloop()
关键技术亮点
- 分区域精细化控制 :通过独立的点集合(
head/eye等)和处理逻辑,实现鱼头清晰化 ------ 头部点密度高、散射少、摆动小,与身体 / 尾部形成差异。 - 颜色系统设计 :蓝色系从亮到暗的渐变(
#42a5f5→#1e88e5)配合高对比度的黑白眼睛,既符合水中生物特征,又强化了关键部位的辨识度。 - 物理模拟简化:通过 "散射 + 收缩" 函数模拟点的自然聚集 / 扩散,用正弦曲线控制摆动节奏,在保证视觉效果的同时降低计算复杂度。
- 预计算帧数据 :初始化时计算所有帧的点位置(
all_points),避免实时计算压力,使动画更流畅。
可调整方向
- 增大
NUM_HEAD可进一步提升鱼头清晰度;减小FRAME_DELAY可加快游动速度。 - 调整
FISH_BODY和FISH_FIN的颜色值,可实现不同深浅的蓝色效果(如深海蓝、浅湖蓝)。 - 修改
fish_shape中的三角函数参数,可调整鱼的体型(如更胖 / 更瘦的身体、更长 / 更短的尾部)。
整体而言,代码通过模块化设计和参数化控制,在保持点阵动画特色的同时,成功实现了 "清晰鱼头 + 蓝色小鱼" 的视觉效果,且具有良好的可扩展性。
代码
python
import random
from math import sin, cos, pi, log
from tkinter import *
# 常量设置(蓝色系调整)
CANVAS_WIDTH = 600
CANVAS_HEIGHT = 400
CANVAS_CENTER_X = CANVAS_WIDTH / 2
CANVAS_CENTER_Y = CANVAS_HEIGHT / 2
IMAGE_ENLARGE = 32
FISH_BODY = "#42a5f5" # 鱼身亮蓝
FISH_FIN = "#1e88e5" # 鱼鳍深蓝
FISH_EYE = "#000000" # 鱼眼纯黑
FISH_EYE_WHITE = "#ffffff" # 眼球高光
SCATTER_BETA = 0.1
SHRINK_RATIO = 11
CURVE_RATIO = 5
FRAME_DELAY = 120
NUM_BODY = 700
NUM_HEAD = 400 # 鱼头点密度
NUM_FIN = 200
NUM_TAIL = 300
class FishParams:
def __init__(self):
self.body = set()
self.head = set()
self.fin = set()
self.tail = set()
self.eye = set()
self.eye_white = set()
self.all_points = {}
# 鱼形函数(保持清晰头部轮廓)
def fish_shape(t, shrink_ratio=IMAGE_ENLARGE):
if 0 <= t < pi / 2: # 头部前端(圆润饱满)
x = (t - pi / 4) / (pi / 4) * 1.5
y = 0.8 * cos(t)
elif pi / 2 <= t < pi: # 头部到身体过渡
x = (t - pi / 2) / (pi / 2) * 2 + 1.5
y = 0.8 * cos(t)
else: # 身体到尾部
x = (3 * pi / 2 - t) / (pi / 2) * 3 + 1.5
y = -0.6 * cos(t)
if t > 1.8 * pi:
x = x * 1.4
x *= shrink_ratio
y *= shrink_ratio
x += CANVAS_CENTER_X - 50 # 左移定位
y += CANVAS_CENTER_Y
return int(x), int(y)
# 头部细节生成(眼睛+轮廓)
def head_details(x, y):
details = {
'eye': [],
'eye_white': [],
'head_edge': []
}
# 眼睛位置(头部前端)
eye_x = x - 35
eye_y = y - 5
details['eye'].extend([
(eye_x, eye_y), (eye_x + 1, eye_y),
(eye_x, eye_y + 1), (eye_x + 1, eye_y + 1)
])
# 眼球高光
details['eye_white'].append((eye_x + 0.5, eye_y + 0.5))
# 头部轮廓强化点
details['head_edge'].extend([
(x - 40, y - 10), (x - 38, y - 12), (x - 35, y - 13),
(x - 32, y - 12), (x - 30, y - 10)
])
return details
# 点散射(头部低散射保持清晰)
def scatter_points(x, y, beta=SCATTER_BETA, is_head=False):
ratio_x = -beta * log(random.random()) * 0.3 if is_head else 0.5
ratio_y = -beta * log(random.random()) * 0.3 if is_head else 0.6
dx = ratio_x * (x - CANVAS_CENTER_X) * 0.15
dy = ratio_y * (y - CANVAS_CENTER_Y) * 0.15
x_new = max(0, min(CANVAS_WIDTH, x - dx))
y_new = max(0, min(CANVAS_HEIGHT, y - dy))
return x_new, y_new
# 收缩函数(头部变形小)
def shrink_points(x, y, ratio, is_head=False):
force = -1 / (((x - CANVAS_CENTER_X) ** 2 + (y - CANVAS_CENTER_Y) ** 2) ** 0.7 + 1e-6)
dx = ratio * force * (x - CANVAS_CENTER_X) * 0.12 if is_head else 0.18
dy = ratio * force * (y - CANVAS_CENTER_Y) * 0.12 if is_head else 0.22
return x - dx, y - dy
# 游动曲线
def swim_curve(p):
return 1.5 * (sin(3.5 * p) + 0.3 * sin(6.5 * p)) / (2 * pi)
class Fish:
def __init__(self, generate_frame=28):
self.params = FishParams()
self.generate_frame = generate_frame
self.build()
for frame in range(generate_frame):
self.calc(frame)
def build(self):
# 身体点
for _ in range(NUM_BODY):
t = random.uniform(0, 2 * pi)
x, y = fish_shape(t)
self.params.body.add((x, y))
# 鱼头点(高密度)
for _ in range(NUM_HEAD):
t = random.uniform(0, pi / 1.5)
x, y = fish_shape(t)
x, y = scatter_points(x, y, is_head=True)
self.params.head.add((x, y))
# 头部细节
head_list = list(self.params.head)
if head_list:
x, y = random.choice(head_list)
details = head_details(x, y)
for ex, ey in details['eye']:
self.params.eye.add((int(ex), int(ey)))
for wx, wy in details['eye_white']:
self.params.eye_white.add((int(wx), int(wy)))
for hx, hy in details['head_edge']:
self.params.head.add((hx, hy))
# 鱼鳍
body_list = list(self.params.body)
head_body = [p for p in body_list if p[0] < CANVAS_CENTER_X]
if head_body:
x, y = random.choice(head_body)
self.params.fin.add((x - 8, y - 15))
self.params.fin.add((x, y - 20))
self.params.fin.add((x + 8, y - 15))
# 尾部
for _ in range(NUM_TAIL):
t = random.uniform(1.6 * pi, 2 * pi)
x, y = fish_shape(t)
for _ in range(2):
tx, ty = scatter_points(x, y)
self.params.tail.add((tx, ty))
@staticmethod
def calc_position(x, y, ratio, frame, part='body'):
swim_offset = sin(frame / 7) * 2
if part == 'head' or part == 'eye':
ratio = ratio * 0.7
swim_offset = swim_offset * 0.5
elif part == 'tail':
ratio = ratio * 2
force_denominator = ((x - CANVAS_CENTER_X) ** 2 + (y - CANVAS_CENTER_Y) ** 2) ** 0.6 + 1e-6
force = 1 / force_denominator
dx = ratio * force * (x - CANVAS_CENTER_X) * 0.2 + swim_offset
dy = ratio * force * (y - CANVAS_CENTER_Y) * 0.25 + random.randint(-1, 1)
return max(0, min(CANVAS_WIDTH, x - dx)), max(0, min(CANVAS_HEIGHT, y - dy))
def calc(self, frame):
ratio = CURVE_RATIO * swim_curve(frame / 14 * pi)
halo_radius = int(2 + 4 * (1 + swim_curve(frame / 14 * pi)))
halo_number = int(800 + 1200 * abs(swim_curve(frame / 14 * pi) ** 2))
all_points = []
# 光晕
halo = set()
for _ in range(halo_number):
t = random.uniform(0, 2 * pi)
x, y = fish_shape(t, SHRINK_RATIO)
is_head = t < pi / 1.5
x, y = shrink_points(x, y, halo_radius, is_head)
if (x, y) not in halo:
halo.add((x, y))
x += random.randint(-6, 6)
y += random.randint(-5, 5)
all_points.append(('halo', x, y, 1))
# 身体点
for x, y in self.params.body:
x, y = self.calc_position(x, y, ratio, frame)
all_points.append(('body', x, y, random.randint(1, 2)))
# 鱼头点
for x, y in self.params.head:
x, y = self.calc_position(x, y, ratio, frame, 'head')
all_points.append(('head', x, y, random.randint(1, 2)))
# 鱼鳍
for x, y in self.params.fin:
x, y = self.calc_position(x, y, ratio, frame)
all_points.append(('fin', x, y, 1))
# 尾部
for x, y in self.params.tail:
x, y = self.calc_position(x, y, ratio, frame, 'tail')
all_points.append(('tail', x, y, random.randint(1, 2)))
# 眼睛
for x, y in self.params.eye:
x, y = self.calc_position(x, y, ratio, frame, 'eye')
all_points.append(('eye', int(x), int(y), 1))
for x, y in self.params.eye_white:
x, y = self.calc_position(x, y, ratio, frame, 'eye')
all_points.append(('eye_white', int(x), int(y), 1))
self.params.all_points[frame] = all_points
def render(self, canvas, frame):
try:
points = self.params.all_points[frame % self.generate_frame]
except KeyError:
points = []
for p_type, x, y, size in points:
if 0 <= x < CANVAS_WIDTH and 0 <= y < CANVAS_HEIGHT:
# 蓝色系配色
if p_type in ('body', 'head', 'halo'):
color = FISH_BODY
elif p_type in ('fin', 'tail'):
color = FISH_FIN
elif p_type == 'eye':
color = FISH_EYE
elif p_type == 'eye_white':
color = FISH_EYE_WHITE
canvas.create_rectangle(x, y, x + size, y + size, width=0, fill=color)
def draw(main, canvas, fish, frame=0):
canvas.delete('all')
fish.render(canvas, frame)
main.after(FRAME_DELAY, draw, main, canvas, fish, frame + 1)
if __name__ == '__main__':
root = Tk()
root.title('蓝色小鱼(清晰鱼头)')
canvas = Canvas(root, bg='#e0f7fa', height=CANVAS_HEIGHT, width=CANVAS_WIDTH) # 浅蓝背景
canvas.pack()
try:
fish = Fish()
draw(root, canvas, fish)
root.mainloop()
except Exception as e:
print(f"错误: {e}")
root.mainloop()