fix(wish-tree): 参考设计图重新绘制大树
Publish Mini Program Dev Version / publish (push) Successful in 49s

- 祈福树:粗壮扭曲树干、螺旋纹理、卷曲树枝、蓬松树冠、彩色装饰、发光效果
- 姻缘树:纤细树干、分散树枝、粉色蓬松树冠、飘落樱花
- 事业树:多根竹竿、竹节、摇摆竹叶、许愿条挂饰
- 添加草丛地面装饰
- 树枝使用三次贝塞尔曲线,带卷曲效果
This commit is contained in:
gouki
2026-08-07 20:08:34 +00:00
parent 97c0d4d4e1
commit a2d4085bf4
+336 -168
View File
@@ -230,36 +230,41 @@ Page({
// 获取树枝位置
getTreeBranches(treeType, width, height) {
const centerX = width / 2;
const baseY = height * 0.75;
const baseY = height * 0.78;
// 根据树类型返回不同的树枝配置
const configs = {
pine: [
{ x: centerX - 60, y: baseY - 180 },
{ x: centerX + 55, y: baseY - 190 },
{ x: centerX - 35, y: baseY - 220 },
{ x: centerX + 40, y: baseY - 230 },
{ x: centerX, y: baseY - 250 },
{ x: centerX - 80, y: baseY - 150 },
{ x: centerX + 75, y: baseY - 160 },
{ x: centerX - 20, y: baseY - 200 },
{ x: centerX + 25, y: baseY - 210 },
{ x: centerX - 70, y: baseY - 260 },
{ x: centerX + 65, y: baseY - 270 },
{ x: centerX - 45, y: baseY - 300 },
{ x: centerX + 50, y: baseY - 310 },
{ x: centerX, y: baseY - 330 },
{ x: centerX - 85, y: baseY - 220 },
{ x: centerX + 80, y: baseY - 230 },
{ x: centerX - 25, y: baseY - 280 },
{ x: centerX + 30, y: baseY - 290 },
{ x: centerX - 55, y: baseY - 340 },
{ x: centerX + 60, y: baseY - 350 },
],
sakura: [
{ x: centerX - 70, y: baseY - 180 },
{ x: centerX + 65, y: baseY - 190 },
{ x: centerX - 45, y: baseY - 220 },
{ x: centerX + 50, y: baseY - 230 },
{ x: centerX, y: baseY - 250 },
{ x: centerX - 30, y: baseY - 200 },
{ x: centerX + 35, y: baseY - 210 },
{ x: centerX - 60, y: baseY - 240 },
{ x: centerX + 55, y: baseY - 250 },
{ x: centerX - 40, y: baseY - 280 },
{ x: centerX + 45, y: baseY - 290 },
{ x: centerX, y: baseY - 310 },
{ x: centerX - 30, y: baseY - 260 },
{ x: centerX + 35, y: baseY - 270 },
],
bamboo: [
{ x: centerX - 30, y: baseY - 160 },
{ x: centerX, y: baseY - 200 },
{ x: centerX + 30, y: baseY - 140 },
{ x: centerX - 25, y: baseY - 120 },
{ x: centerX + 25, y: baseY - 180 },
{ x: centerX - 40, y: baseY - 100 },
{ x: centerX - 15, y: baseY - 140 },
{ x: centerX + 10, y: baseY - 120 },
{ x: centerX + 35, y: baseY - 80 },
{ x: centerX - 40, y: baseY - 160 },
{ x: centerX - 15, y: baseY - 200 },
{ x: centerX + 10, y: baseY - 180 },
{ x: centerX + 35, y: baseY - 140 },
],
};
@@ -396,7 +401,7 @@ Page({
// 绘制大树
drawTree(ctx, tree, width, height) {
const centerX = width / 2;
const baseY = height * 0.75;
const baseY = height * 0.78;
// 根据树类型绘制不同风格的树
const treeType = tree.type || 'pine';
@@ -410,65 +415,92 @@ Page({
}
},
// 绘制松树(祈福树)
// 绘制祈福树(装饰性大树,参考第一张图
drawPineTree(ctx, centerX, baseY) {
// 树干 - 用贝塞尔曲线画自然弯曲
ctx.setFillStyle('#3d2914');
const time = Date.now() / 1000;
// 地面装饰 - 草丛
this.drawGrass(ctx, centerX, baseY);
// 粗壮树干 - 参考第三张图的扭曲感
ctx.setFillStyle('#5a3a2a');
ctx.beginPath();
ctx.moveTo(centerX - 20, baseY);
// 左边缘曲线
ctx.moveTo(centerX - 30, baseY);
// 左边缘 - 扭曲向上
ctx.bezierCurveTo(
centerX - 18, baseY - 50,
centerX - 12, baseY - 100,
centerX - 8, baseY - 140
centerX - 35, baseY - 40,
centerX - 25, baseY - 80,
centerX - 20, baseY - 120
);
// 树干顶部
ctx.lineTo(centerX + 8, baseY - 140);
// 右边缘曲线
ctx.bezierCurveTo(
centerX + 12, baseY - 100,
centerX + 18, baseY - 50,
centerX + 20, baseY
centerX - 15, baseY - 160,
centerX - 10, baseY - 180,
centerX - 5, baseY - 200
);
// 顶部
ctx.lineTo(centerX + 5, baseY - 200);
// 右边缘
ctx.bezierCurveTo(
centerX + 10, baseY - 180,
centerX + 15, baseY - 160,
centerX + 20, baseY - 120
);
ctx.bezierCurveTo(
centerX + 25, baseY - 80,
centerX + 35, baseY - 40,
centerX + 30, baseY
);
ctx.closePath();
ctx.fill();
// 树纹理
ctx.setStrokeStyle('#2a1d0f');
ctx.setLineWidth(1);
for (let i = 0; i < 5; i++) {
const y = baseY - 20 - i * 30;
// 树纹理 - 螺旋感
ctx.setStrokeStyle('#4a2a1a');
ctx.setLineWidth(2);
for (let i = 0; i < 6; i++) {
const y = baseY - 30 - i * 35;
const offset = Math.sin(i * 0.8) * 8;
ctx.beginPath();
ctx.moveTo(centerX - 15 + i * 2, y);
ctx.quadraticCurveTo(centerX, y - 10, centerX + 15 - i * 2, y);
ctx.moveTo(centerX - 20 + offset, y);
ctx.quadraticCurveTo(
centerX + offset, y - 15,
centerX + 20 + offset, y
);
ctx.stroke();
}
// 主枝干
this.drawBranch(ctx, centerX, baseY - 140, -60, 80, 3);
this.drawBranch(ctx, centerX, baseY - 140, 60, 80, 3);
this.drawBranch(ctx, centerX, baseY - 160, -30, 60, 2);
this.drawBranch(ctx, centerX, baseY - 160, 30, 60, 2);
this.drawBranch(ctx, centerX, baseY - 180, 0, 50, 2);
// 树冠 - 用多个不规则圆形叠加,模拟松针层次
const crownLayers = [
{ offsetY: -200, radiusX: 100, radiusY: 60, color: '#1a3d1f', alpha: 0.9 },
{ offsetY: -240, radiusX: 85, radiusY: 55, color: '#2d5a2d', alpha: 0.85 },
{ offsetY: -280, radiusX: 70, radiusY: 50, color: '#3d7a3d', alpha: 0.8 },
{ offsetY: -320, radiusX: 55, radiusY: 45, color: '#4d9a4d', alpha: 0.75 },
{ offsetY: -350, radiusX: 40, radiusY: 35, color: '#5dba5d', alpha: 0.7 },
// 主枝干 - 曲线分散,参考第一张图
const branches = [
{ angle: -70, length: 100, width: 4, curl: 20 },
{ angle: 70, length: 100, width: 4, curl: -20 },
{ angle: -45, length: 90, width: 3, curl: 15 },
{ angle: 45, length: 90, width: 3, curl: -15 },
{ angle: -20, length: 80, width: 3, curl: 10 },
{ angle: 20, length: 80, width: 3, curl: -10 },
{ angle: 0, length: 70, width: 2, curl: 0 },
];
crownLayers.forEach(layer => {
branches.forEach(b => {
this.drawCurlyBranch(ctx, centerX, baseY - 200, b.angle, b.length, b.width, b.curl);
});
// 树冠 - 多层蓬松感,参考第一张图
const crownLayers = [
{ offsetY: -280, radiusX: 110, radiusY: 70, color: '#2a5a1a', alpha: 0.85 },
{ offsetY: -320, radiusX: 95, radiusY: 65, color: '#3a7a2a', alpha: 0.8 },
{ offsetY: -360, radiusX: 80, radiusY: 60, color: '#4a9a3a', alpha: 0.75 },
{ offsetY: -400, radiusX: 65, radiusY: 50, color: '#5aba4a', alpha: 0.7 },
{ offsetY: -430, radiusX: 50, radiusY: 40, color: '#6ada5a', alpha: 0.65 },
];
crownLayers.forEach((layer, idx) => {
ctx.save();
ctx.setGlobalAlpha(layer.alpha);
ctx.setFillStyle(layer.color);
// 绘制不规则椭圆
// 蓬松不规则形
ctx.beginPath();
for (let angle = 0; angle < Math.PI * 2; angle += 0.1) {
const noise = Math.sin(angle * 5) * 8 + Math.cos(angle * 3) * 5;
for (let angle = 0; angle < Math.PI * 2; angle += 0.08) {
const noise = Math.sin(angle * 6 + idx) * 12 + Math.cos(angle * 4 + idx * 2) * 8;
const rx = layer.radiusX + noise;
const ry = layer.radiusY + noise * 0.6;
const x = centerX + Math.cos(angle) * rx;
@@ -484,80 +516,174 @@ Page({
ctx.restore();
});
// 松针细节
ctx.setStrokeStyle('#6dca6d');
ctx.setLineWidth(1.5);
for (let i = 0; i < 30; i++) {
const angle = (i / 30) * Math.PI * 2;
const r = 50 + Math.random() * 40;
const x = centerX + Math.cos(angle) * r;
const y = baseY - 280 + Math.sin(angle) * r * 0.5;
const len = 8 + Math.random() * 8;
// 装饰性卷曲枝条 - 参考第一张图的藤蔓感
ctx.setStrokeStyle('#3a6a2a');
ctx.setLineWidth(2);
for (let i = 0; i < 12; i++) {
const angle = (i / 12) * Math.PI * 2;
const startR = 60;
const startX = centerX + Math.cos(angle) * startR;
const startY = baseY - 320 + Math.sin(angle) * startR * 0.5;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + Math.cos(angle) * len, y + Math.sin(angle) * len);
ctx.moveTo(startX, startY);
// 卷曲线条
for (let t = 0; t < 1; t += 0.1) {
const r = startR + t * 40;
const a = angle + t * 2;
const x = centerX + Math.cos(a) * r;
const y = baseY - 320 + Math.sin(a) * r * 0.5 - t * 30;
ctx.lineTo(x, y);
}
ctx.stroke();
}
// 装饰果实(松果)
ctx.setFillStyle('#8b6914');
for (let i = 0; i < 6; i++) {
const angle = (i / 6) * Math.PI * 2 + 0.5;
const r = 45 + Math.random() * 30;
const x = centerX + Math.cos(angle) * r;
const y = baseY - 260 + Math.sin(angle) * r * 0.4;
// 彩色装饰物 - 参考第一张图的彩色叶子/果实
const decorations = [
{ color: '#ff6b6b', size: 8 }, // 红
{ color: '#ffd93d', size: 7 }, // 黄
{ color: '#6bcf7f', size: 7 }, // 绿
{ color: '#4d96ff', size: 6 }, // 蓝
{ color: '#ff9f43', size: 7 }, // 橙
{ color: '#a55eea', size: 6 }, // 紫
];
// 画松果形状
for (let i = 0; i < 25; i++) {
const dec = decorations[i % decorations.length];
const angle = (i / 25) * Math.PI * 2 + Math.random() * 0.5;
const r = 50 + Math.random() * 60;
const x = centerX + Math.cos(angle) * r;
const y = baseY - 350 + Math.sin(angle) * r * 0.6 + Math.sin(time * 2 + i) * 3;
ctx.setFillStyle(dec.color);
ctx.beginPath();
ctx.ellipse(x, y, 5, 8, angle, 0, Math.PI * 2);
// 画叶子形状
ctx.ellipse(x, y, dec.size, dec.size * 1.5, angle, 0, Math.PI * 2);
ctx.fill();
// 高光
ctx.setFillStyle('rgba(255,255,255,0.3)');
ctx.beginPath();
ctx.ellipse(x - 2, y - 2, dec.size * 0.3, dec.size * 0.5, angle, 0, Math.PI * 2);
ctx.fill();
}
// 发光效果 - 参考第三张图
ctx.save();
ctx.setGlobalAlpha(0.15 + Math.sin(time) * 0.05);
const glowGradient = ctx.createRadialGradient(
centerX, baseY - 350, 0,
centerX, baseY - 350, 150
);
glowGradient.addColorStop(0, '#ffff00');
glowGradient.addColorStop(0.5, '#88ff00');
glowGradient.addColorStop(1, 'transparent');
ctx.setFillStyle(glowGradient);
ctx.beginPath();
ctx.arc(centerX, baseY - 350, 150, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
},
// 绘制草丛
drawGrass(ctx, centerX, baseY) {
ctx.setStrokeStyle('#3a6a2a');
ctx.setLineWidth(2);
for (let i = 0; i < 15; i++) {
const x = centerX - 100 + i * 15;
const h = 15 + Math.random() * 20;
const sway = Math.sin(Date.now() / 500 + i) * 3;
ctx.beginPath();
ctx.moveTo(x, baseY);
ctx.quadraticCurveTo(x + sway, baseY - h * 0.5, x + sway * 2, baseY - h);
ctx.stroke();
}
},
// 绘制樱花树(姻缘树)
drawSakuraTree(ctx, centerX, baseY) {
// 树干 - 更纤细优雅
ctx.setFillStyle('#4a3728');
// 绘制卷曲树枝
drawCurlyBranch(ctx, startX, startY, angle, length, width, curl) {
const rad = (angle * Math.PI) / 180;
const endX = startX + Math.sin(rad) * length;
const endY = startY - Math.cos(rad) * length;
ctx.setStrokeStyle('#5a3a2a');
ctx.setLineWidth(width);
ctx.setLineCap('round');
ctx.beginPath();
ctx.moveTo(centerX - 12, baseY);
ctx.moveTo(startX, startY);
// 三次贝塞尔曲线,带卷曲
const cp1x = startX + Math.sin(rad) * length * 0.3 + curl;
const cp1y = startY - Math.cos(rad) * length * 0.3;
const cp2x = startX + Math.sin(rad) * length * 0.7 - curl;
const cp2y = startY - Math.cos(rad) * length * 0.7;
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, endX, endY);
ctx.stroke();
// 小分支
if (width > 2) {
const midX = (startX + endX) / 2;
const midY = (startY + endY) / 2;
this.drawCurlyBranch(ctx, midX, midY, angle - 30, length * 0.5, width - 1, curl * 0.5);
this.drawCurlyBranch(ctx, midX, midY, angle + 30, length * 0.5, width - 1, curl * 0.5);
}
},
// 绘制姻缘树(樱花树)
drawSakuraTree(ctx, centerX, baseY) {
const time = Date.now() / 1000;
// 地面装饰
this.drawGrass(ctx, centerX, baseY);
// 纤细树干
ctx.setFillStyle('#4a3a2a');
ctx.beginPath();
ctx.moveTo(centerX - 15, baseY);
ctx.bezierCurveTo(
centerX - 10, baseY - 60,
centerX - 6, baseY - 120,
centerX - 4, baseY - 160
centerX - 12, baseY - 60,
centerX - 8, baseY - 120,
centerX - 5, baseY - 180
);
ctx.lineTo(centerX + 4, baseY - 160);
ctx.lineTo(centerX + 5, baseY - 180);
ctx.bezierCurveTo(
centerX + 6, baseY - 120,
centerX + 10, baseY - 60,
centerX + 12, baseY
centerX + 8, baseY - 120,
centerX + 12, baseY - 60,
centerX + 15, baseY
);
ctx.closePath();
ctx.fill();
// 主枝干 - 更分散
this.drawBranch(ctx, centerX, baseY - 160, -70, 90, 2);
this.drawBranch(ctx, centerX, baseY - 160, 70, 90, 2);
this.drawBranch(ctx, centerX, baseY - 180, -40, 70, 2);
this.drawBranch(ctx, centerX, baseY - 180, 40, 70, 2);
this.drawBranch(ctx, centerX, baseY - 200, 0, 60, 2);
// 樱花树冠 - 粉色系
const sakuraLayers = [
{ offsetY: -220, radiusX: 90, radiusY: 50, color: '#ffb7c5', alpha: 0.6 },
{ offsetY: -260, radiusX: 75, radiusY: 45, color: '#ffc9d4', alpha: 0.55 },
{ offsetY: -300, radiusX: 60, radiusY: 40, color: '#ffdbe3', alpha: 0.5 },
{ offsetY: -330, radiusX: 45, radiusY: 35, color: '#ffedf2', alpha: 0.45 },
// 分散树枝
const branches = [
{ angle: -60, length: 90, width: 3 },
{ angle: 60, length: 90, width: 3 },
{ angle: -35, length: 80, width: 2 },
{ angle: 35, length: 80, width: 2 },
{ angle: 0, length: 70, width: 2 },
];
sakuraLayers.forEach(layer => {
branches.forEach(b => {
this.drawCurlyBranch(ctx, centerX, baseY - 180, b.angle, b.length, b.width, 15);
});
// 樱花树冠 - 粉色蓬松
const sakuraLayers = [
{ offsetY: -260, radiusX: 100, radiusY: 60, color: '#ffb7c5', alpha: 0.5 },
{ offsetY: -300, radiusX: 85, radiusY: 55, color: '#ffc9d6', alpha: 0.45 },
{ offsetY: -340, radiusX: 70, radiusY: 50, color: '#ffdbe5', alpha: 0.4 },
{ offsetY: -370, radiusX: 55, radiusY: 40, color: '#ffedf4', alpha: 0.35 },
];
sakuraLayers.forEach((layer, idx) => {
ctx.save();
ctx.setGlobalAlpha(layer.alpha);
ctx.setFillStyle(layer.color);
ctx.beginPath();
for (let angle = 0; angle < Math.PI * 2; angle += 0.1) {
const noise = Math.sin(angle * 7) * 10 + Math.cos(angle * 4) * 6;
for (let angle = 0; angle < Math.PI * 2; angle += 0.08) {
const noise = Math.sin(angle * 8 + idx) * 10 + Math.cos(angle * 5 + idx) * 6;
const rx = layer.radiusX + noise;
const ry = layer.radiusY + noise * 0.5;
const x = centerX + Math.cos(angle) * rx;
@@ -573,94 +699,136 @@ Page({
ctx.restore();
});
// 樱花花瓣
ctx.setFillStyle('#fff0f5');
for (let i = 0; i < 20; i++) {
const angle = Math.random() * Math.PI * 2;
const r = 40 + Math.random() * 50;
const x = centerX + Math.cos(angle) * r;
const y = baseY - 260 + Math.sin(angle) * r * 0.5;
const size = 3 + Math.random() * 4;
// 飘落樱花
for (let i = 0; i < 15; i++) {
const x = centerX - 80 + Math.random() * 160;
const y = baseY - 350 + Math.random() * 100 + Math.sin(time + i) * 10;
const size = 4 + Math.random() * 4;
const rotation = Math.random() * Math.PI;
// 画花瓣形状
ctx.save();
ctx.translate(x, y);
ctx.rotate(rotation);
ctx.setFillStyle('#fff0f5');
ctx.beginPath();
ctx.ellipse(x, y, size, size * 1.5, angle, 0, Math.PI * 2);
// 樱花花瓣形状
ctx.moveTo(0, -size);
ctx.bezierCurveTo(size, -size, size, size * 0.5, 0, size);
ctx.bezierCurveTo(-size, size * 0.5, -size, -size, 0, -size);
ctx.fill();
ctx.restore();
}
},
// 绘制竹子(事业树)
// 绘制事业树(许愿竹,参考第二张图
drawBambooTree(ctx, centerX, baseY) {
// 竹竿 - 多根
const bambooPositions = [
{ offsetX: -30, height: 200 },
{ offsetX: 0, height: 240 },
{ offsetX: 30, height: 180 },
const time = Date.now() / 1000;
// 地面装饰 - 小石子
ctx.setFillStyle('#8a8a7a');
for (let i = 0; i < 8; i++) {
const x = centerX - 60 + i * 15;
const size = 3 + Math.random() * 4;
ctx.beginPath();
ctx.arc(x, baseY - 5, size, 0, Math.PI * 2);
ctx.fill();
}
// 竹竿 - 多根,参考第二张图
const bambooStalks = [
{ offsetX: -40, height: 220, width: 8 },
{ offsetX: -15, height: 260, width: 10 },
{ offsetX: 10, height: 240, width: 9 },
{ offsetX: 35, height: 200, width: 7 },
];
bambooPositions.forEach(pos => {
const x = centerX + pos.offsetX;
const topY = baseY - pos.height;
bambooStalks.forEach((stalk, idx) => {
const x = centerX + stalk.offsetX;
const topY = baseY - stalk.height;
const sway = Math.sin(time * 2 + idx) * 3;
// 竹竿主体
ctx.setFillStyle('#5a8a3a');
// 竹竿主体 - 带摇摆
ctx.setFillStyle('#6aaa4a');
ctx.beginPath();
ctx.moveTo(x - 6, baseY);
ctx.lineTo(x - 5, topY);
ctx.lineTo(x + 5, topY);
ctx.lineTo(x + 6, baseY);
ctx.moveTo(x - stalk.width / 2, baseY);
ctx.quadraticCurveTo(
x - stalk.width / 2 + sway, baseY - stalk.height / 2,
x - stalk.width / 2 + sway * 2, topY
);
ctx.lineTo(x + stalk.width / 2 + sway * 2, topY);
ctx.quadraticCurveTo(
x + stalk.width / 2 + sway, baseY - stalk.height / 2,
x + stalk.width / 2, baseY
);
ctx.closePath();
ctx.fill();
// 竹节
ctx.setStrokeStyle('#4a7a2a');
ctx.setStrokeStyle('#5a9a3a');
ctx.setLineWidth(2);
for (let y = baseY - 40; y > topY; y -= 40) {
for (let y = baseY - 30; y > topY + 20; y -= 35) {
const nodeSway = sway * (1 - (baseY - y) / stalk.height);
ctx.beginPath();
ctx.moveTo(x - 6, y);
ctx.lineTo(x + 6, y);
ctx.moveTo(x - stalk.width / 2 + nodeSway, y);
ctx.lineTo(x + stalk.width / 2 + nodeSway, y);
ctx.stroke();
}
// 竹叶
ctx.setFillStyle('#6aaa4a');
for (let i = 0; i < 8; i++) {
const leafY = topY + 20 + i * 20;
// 竹叶 - 更自然
ctx.setFillStyle('#7aba5a');
for (let i = 0; i < 6; i++) {
const leafY = topY + 15 + i * 25;
const side = i % 2 === 0 ? 1 : -1;
const leafSway = sway * 0.5 + Math.sin(time * 3 + i) * 2;
ctx.save();
ctx.translate(x + side * 5, leafY);
ctx.rotate(side * 0.3 + leafSway * 0.02);
ctx.beginPath();
ctx.moveTo(x, leafY);
ctx.quadraticCurveTo(
x + side * 25, leafY - 10,
x + side * 40, leafY + 5
);
ctx.quadraticCurveTo(
x + side * 25, leafY + 15,
x, leafY + 10
);
ctx.moveTo(0, 0);
ctx.quadraticCurveTo(side * 30, -15, side * 50, 0);
ctx.quadraticCurveTo(side * 30, 15, 0, 10);
ctx.closePath();
ctx.fill();
ctx.restore();
}
});
},
// 绘制树枝
drawBranch(ctx, startX, startY, angle, length, width) {
const rad = (angle * Math.PI) / 180;
const endX = startX + Math.sin(rad) * length;
const endY = startY - Math.cos(rad) * length;
// 许愿条挂在竹子上 - 参考第二张图
if (this.ribbons && this.ribbons.length > 0) {
this.ribbons.forEach((ribbon, idx) => {
const stalk = bambooStalks[idx % bambooStalks.length];
const x = centerX + stalk.offsetX;
const y = baseY - 80 - (idx % 5) * 30;
ctx.setStrokeStyle('#3d2914');
ctx.setLineWidth(width);
ctx.setLineCap('round');
// 画许愿条 - 长方形纸片
ctx.save();
ctx.translate(x, y);
ctx.rotate(Math.sin(time * 2 + idx) * 0.1);
ctx.beginPath();
ctx.moveTo(startX, startY);
// 用曲线让树枝更自然
const midX = startX + Math.sin(rad) * length * 0.5 + (Math.random() - 0.5) * 10;
const midY = startY - Math.cos(rad) * length * 0.5;
ctx.quadraticCurveTo(midX, midY, endX, endY);
ctx.stroke();
// 纸片
ctx.setFillStyle(ribbon.color);
ctx.fillRect(-8, 0, 16, 40);
// 顶部挂绳
ctx.setStrokeStyle('#8a6a4a');
ctx.setLineWidth(1);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, -10);
ctx.stroke();
// 文字
ctx.setFillStyle('#ffffff');
ctx.setFontSize(10);
ctx.setTextAlign('center');
ctx.fillText(ribbon.wish.content.substring(0, 4), 0, 25);
ctx.restore();
});
}
},
// 绘制丝带