Java实现动态爱心树:递归分形与Swing动画实战 📅 发布时间:2026/9/17 4:13:37 👁 浏览次数: 简介一份以“爱心树”为主题的Java表白特效源码资源定位给Java初学者、编程爱好者以及想用代码表达心意的人群旨在解决“如何用编程绘制一颗动态爱心树并配乐展示”的问题。资源共13个文件核心逻辑以js脚本为主配合html页面结构、css样式与mp3背景音乐并内置zip子包便于二次开发压缩包整体19.48MB。目前已有12986人学习下载属于人气较高的Java趣味编程范例。从实际内容看资源覆盖字符串处理、循环嵌套、条件判断、面向对象封装、异常处理等基础知识点同时引入jscex异步调度库与jquery动画机制通过实际代码体会递归思想并帮助读者理解分层绘制、粒子生成和音乐联动的完整流程。它既是一份可直接运行的爱心树项目也可作为表白页面模板替换文字、颜色或音乐后快速使用适合课堂演示、社团活动或个人练手。1. 爱心树 java 源码要解决什么问题如果把“爱心树 java 源码”拆开一半是“java 代码爱心”一半是“分形树”。网上流传的版本大多只打印一个由*组成的爱心好看但没有生命力另一类用递归画树却缺少表白味道。真正值得写的是把两件事合并用递归生成枝条在末端长出爱心再让它动起来。这个标题对初学者的价值不在语法而在坐标系、递归和事件线程这三件事。它能解决一个很实际的需求不依赖外部框架用标准 JDK 写一个能运行的表白程序同时练到递归、坐标变换和 Swing 的事件分发机制面试时也能讲清楚为什么这样设计。2. 用 java 代码爱心控制台字符画的最小实现2.1 用隐式方程圈定爱心区域可以先放弃图形界面用控制台字符画完成最小验证。这里的核心不是打印循环而是判断“某个字符位置是否落在爱心内部”。最常用的隐式方程为(x^2 y^2 - 1)^3 - x^2 * y^3 0当点(x, y)使等式左边小于 0 时它位于心形内部约等于 0 时在心形轮廓上。由于控制台只能显示等宽字符通常把横向 60 列、纵向 30 行的二维数组当作画布把每个格子映射到[-1, 1]坐标系再逐格判断。import java.util.Arrays; public class ConsoleHeart { public static void main(String[] args) { int rows 30, cols 60; char[][] canvas new char[rows][cols]; for (char[] line : canvas) { Arrays.fill(line, ); } for (int row 0; row rows; row) { for (int col 0; col cols; col) { // 把像素坐标映射到 [-1,1]留一点边距 double x (col - cols / 2.0) * 2.0 / cols; double y (rows / 2.0 - row) * 2.0 / rows; double value Math.pow(x * x y * y - 1, 3) - x * x * y * y * y; if (value 0) { canvas[row][col] *; } } } for (char[] line : canvas) { System.out.println(new String(line)); } } }这里把列号col减半再乘2 / cols得到[-1, 1]范围y必须用rows / 2.0 - row因为屏幕第 0 行在顶部而心形公式中y向上为正。映射后x和y的取值范围相同但字符的宽高比不是 1:1所以打印出来会纵向拉长可以通过把rows设成cols / 2来缓解。参数建议值作用rows30字符行数控制台高度cols60字符列数控制台宽度rows / 2.0 - row屏幕坐标把 y 轴翻转向上value 0填充条件心形内部为负这个版本只依赖java.lang和java.util在任何 JDK 8 环境都能编译运行。运行后如果只看到少量星号说明 y 映射或归一化除数有问题可以先把value的绝对值打印出来调试。2.2 要轮廓不要填充有时想得到心形的边框只需要修改填充条件Math.abs(value) 0.05时输出*否则输出空格。需要注意阈值越大线条越粗但超过 0.1 后轮廓会黏在一起一般取 0.02~0.06。if (Math.abs(value) 0.05) { canvas[row][col] *; }对比填充版本轮廓版本更接近“手绘”适合放在表白文字上面。由于隐式方程在高曲率区域变化剧烈固定阈值在某些部位会断线可以在判断轮廓前后额外输出一次外围的点但控制台场景不必追求完美。2.3 加一点颜色ANSI 转义现在终端对 ANSI 颜色支持已经很普遍。给星号包上转义码可以让红色爱心更直观代码只改输出循环for (char[] line : canvas) { StringBuilder sb new StringBuilder(); for (char c : line) { if (c *) { sb.append(\u001B[31m*\u001B[0m); } else { sb.append( ); } } System.out.println(sb); }\u001B[31m设置前景红色\u001B[0m恢复默认。如果目标是 Windows 老版本 cmd可能不显示颜色这属于终端能力问题与 Java 代码无关。做法上先跑无颜色版本确认形状再加 ANSI排错时分开定位。这一段也是 java 基础里转义字符的常见用法面试聊天时很容易被追问。3. 用 Java Swing 画爱心树递归分形与坐标变换3.1 分支递归一棵树的最小模型控制台爱心是二维平面上的静态图案爱心树则要引入分形递归。树的每次递归调用要做的事情是从当前点出发按角度画一段树枝然后在树枝末端画出两到三条倾角不同的子枝。设计成drawBranch(x, y, angle, length, depth) 当 depth 0 时在末端画一个爱心 计算末端坐标 x2 x length * cos(angle), y2 y length * sin(angle) 画线段 (x, y) - (x2, y2) 对 left/right 两个方向递归调用 drawBranch其中angle以弧度表示向上为-PI/2。屏幕坐标系 y 向下所以length乘以sin(angle)后y2会小于y也就是往上生长。每次递归length乘以一个小于 1 的系数深度越多枝条越密。3.2 完整可运行的 HeartTree 源码下面代码用 Java Swing 实现一个 800x700 窗口树根在底部居中递归深度为 10。每个叶子节点画一个粉红色心形。标准 JDK 编译即可运行不需要第三方库。import javax.swing.*; import java.awt.*; import java.awt.geom.Path2D; import java.util.Random; public class HeartTree extends JPanel { private final Random random new Random(42); Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 (Graphics2D) g; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // 深色夜空背景 g2.setColor(new Color(0x20, 0x14, 0x10)); g2.fillRect(0, 0, getWidth(), getHeight()); // 树干颜色从底部向上画 g2.setColor(new Color(0x8B, 0x5A, 0x2B)); int baseX getWidth() / 2; int baseY getHeight() - 40; drawBranch(g2, baseX, baseY, -Math.PI / 2, 180, 10); } private void drawBranch(Graphics2D g, int x, int y, double angle, double length, int depth) { if (depth 0) { drawHeart(g, x, y, Math.max(8, (int) (length * 0.8))); return; } int x2 x (int) (length * Math.cos(angle)); int y2 y (int) (length * Math.sin(angle)); g.setStroke(new BasicStroke(Math.max(1f, depth * 0.7f))); g.drawLine(x, y, x2, y2); // 分叉角度在 30 度附近加入随机扰动 double spread Math.PI / 6 random.nextDouble() * 0.15; drawBranch(g, x2, y2, angle spread, length * 0.75, depth - 1); drawBranch(g, x2, y2, angle - spread, length * 0.72, depth - 1); } private void drawHeart(Graphics2D g, int cx, int cy, int size) { Path2D heart new Path2D.Double(); boolean first true; for (double t 0; t 2 * Math.PI; t 0.02) { // 心形参数方程 double hx 16 * Math.pow(Math.sin(t), 3); double hy 13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t); int px cx (int) (hx * size / 16.0); int py cy - (int) (hy * size / 16.0); if (first) { heart.moveTo(px, py); first false; } else { heart.lineTo(px, py); } } heart.closePath(); g.setColor(new Color(0xFF, 0x45, 0x8C)); g.fill(heart); } public static void main(String[] args) { JFrame frame new JFrame(爱心树 Java 表白代码); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.add(new HeartTree()); frame.setSize(800, 700); frame.setLocationRelativeTo(null); frame.setVisible(true); } }说明几个关键选择drawBranch在depth 0时调用drawHeart这样所有分叉末端都会长出一颗心。Math.max(1f, depth * 0.7f)让靠近树根的分支更粗叶子分支更细。spread控制在 30° 左右并加入随机扰动每次运行形态都不同。固定 seed 为 42 便于复现想随机就删掉种子。drawHeart使用参数曲线采样 300 多个点再填充为实心图形。size / 16.0是把公式里的 16 缩放成期望像素直径。参数调整参考表参数建议值效果initial length180树的高度depth8~11分叉层数超过 12 会有大量爱心length * 0.75 / 0.720.7~0.8层间缩短比例越小越稀疏spreadPI/6 附近分叉角度过大树冠散开random seed42固定形态去掉则每次随机3.3 稀疏爱心的概率控制如果每个末端都画心视觉上太密。常见做法是在递归基时用random.nextDouble()控制出现概率if (depth 0) { if (random.nextDouble() 0.7) { drawHeart(g, x, y, Math.max(8, (int) (length * 0.8))); } return; }这样大约 30% 的叶子只留下光秃枝头树冠更有透气感。也可以把概率与深度关联比如0.4 depth * 0.05让更深的末端更容易长心。这些参数会因为窗口尺寸不同而不同建议把初始长度和 depth 做成字段运行时用 JSlider 调整方便舒服地调到好看的形态。4. 让表白代码动起来Swing Timer、鼠标交互与红色心跳4.1 为什么动画要放 Swing Timer而不是 while 循环Swing 是单线程库所有组件状态必须在事件分发线程EDT上创建和修改。如果在 main 线程里写while (true) { Thread.sleep(30); repaint(); }repaint只是提交请求不保证马上重绘很容易出现界面卡顿。Swing Timer 在 EDT 上触发回调每次执行后自动排队下一次是这类动画的常规做法。更不要在paintComponent里出现Thread.sleep那会直接冻结整个 UI。4.2 心跳实现三行改动在HeartTree类里增加一个时间字段public double time;然后修改drawHeart的调用把爱心大小乘上一个心跳系数double beat 1 0.12 * Math.sin(time * 3); drawHeart(g, x, y, (int) (Math.max(8, length * 0.8) * beat));在 main 中启动 Timer30 毫秒刷新一次HeartTree panel new HeartTree(); Timer timer new Timer(30, e - { panel.time 0.1; panel.repaint(); }); timer.start();参数说明Timer 延迟 30ms约 33 FPS对简单分形足够。time * 3控制心跳频率3 表示约 3 弧度每秒视觉上偏快可调到 2.2。振幅 0.12 让爱心大小变化 12%再大会出现重叠和闪烁。4.3 鼠标交互点击换色与文字变化加入 MouseListener点击时换一个颜色属于表白代码里常被问到的“事件绑定点”。给HeartTree增加一个Color heartColor字段panel.addMouseListener(new MouseAdapter() { Override public void mousePressed(MouseEvent e) { Color[] colors { new Color(0xFF, 0x45, 0x8C), new Color(0xFF, 0x6F, 0x61), new Color(0xE0, 0x4F, 0x5F), new Color(0xC2, 0x18, 0x07) }; panel.heartColor colors[(panel.clickCount) % colors.length]; panel.repaint(); } });paint里drawHeart使用heartColor而不是固定的粉色。点击换色是最简单展示监听器机制的方式比把逻辑堆在一个类里更容易被阅读。还可以在点击后把窗口标题改成爱心树 · count把这个计数做成字段即可。4.4 动画排错递归次数、坐标翻转与控制台编码现象原因解决办法树头朝下angle 用了正数 PI/2改用 -PI/2或调整 sin 的符号深度一高就卡drawBranch 按 2^depth 增长depth 控制在 10 以内或加概率剪枝中文窗口标题乱码源文件编码与 JVM 默认不一致javac -encoding UTF-8并检查 java 环境变量配置爱心叠加成色块size 太大导致末端重叠调小 size或让 length 缩短系数更小其中 javac 编码问题本质是平台默认字符集不同。配置好 JAVA_HOME 和 PATH 后用javac -encoding UTF-8 HeartTree.java编译多数乱码立即消失。这也是 java 学习路线里容易忽略的一步。5. 把爱心树保存成 PNG 和可执行 Jar最终交付5.1 用 BufferedImage 离屏渲染导出 PNG动画调好之后还能把静态画面导出成图片让对方在不能运行 Java 的环境也能看到。给HeartTree增加一个方法离屏绘制 800x700 的图并写入文件public void saveAsPng(String path) throws IOException { BufferedImage image new BufferedImage( getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB); Graphics2D g2 image.createGraphics(); paint(g2); g2.dispose(); ImageIO.write(image, png, new File(path)); }注意不要调用paintComponent而调paint因为paint内部会经过 JComponent 的透明层管理。如果你已经创建了 JFrame还可以先frame.pack()再导出避免宽高为 0。5.2 打包成可执行 Jar 的最终命令把动画和图片导出能力合在一个类里编译运行命令如下javac -encoding UTF-8 HeartTree.java jar cfe HeartTree.jar HeartTree *.class java -jar HeartTree.jarjar cfe中的e指定入口类名 HeartTree。若需要让 jar 双击可运行要保证系统默认 javaw 已关联 jar 文件。也可以写一个简单的启动脚本但表白场景里一个 jar 文件足够。如果想在启动时自动导出 PNG在 main 里加一行panel.setSize(800, 700); panel.saveAsPng(heart-tree.png);注意先设置尺寸再导出。本文还有配套的精品资源点击获取