futranbg commited on
Commit
332f20e
·
verified ·
1 Parent(s): 2448a8c

Update static/js/solar_animation.js

Browse files
Files changed (1) hide show
  1. static/js/solar_animation.js +188 -117
static/js/solar_animation.js CHANGED
@@ -11,6 +11,8 @@ const solarPanel = { x: 50, y: 50, width: 120, height: 90 };
11
  const inverter = { x: 280, y: 150, width: 90, height: 70 };
12
  const battery = { x: 480, y: 250, width: 80, height: 110 };
13
  const homeLoad = { x: 650, y: 150, width: 80, height: 70 };
 
 
14
 
15
  // Định nghĩa màu sắc và thuộc tính
16
  const colors = {
@@ -22,6 +24,8 @@ const colors = {
22
  batteryAccent: '#BBDEFB', // Xanh dương nhạt cho điểm nhấn pin
23
  homeRoof: '#E91E63', // Hồng cho mái nhà
24
  homeBody: '#FFEB3B', // Vàng cho thân nhà
 
 
25
  flowPrimary: '#FF9800', // Cam sáng cho dòng chảy chính
26
  flowSecondary: 'rgba(255, 255, 255, 0.7)', // Trắng trong suốt cho hiệu ứng dòng chảy
27
  text: '#212121', // Đen gần cho văn bản
@@ -30,17 +34,20 @@ const colors = {
30
 
31
  // --- Hàm vẽ các biểu tượng chi tiết hơn ---
32
 
 
 
 
 
 
 
 
 
 
 
 
33
  // Vẽ biểu tượng tấm pin mặt trời
34
  function drawSolarPanelIcon(x, y, width, height) {
35
- // Nền tấm pin (hơi nghiêng)
36
- ctx.fillStyle = colors.solarDark;
37
- ctx.beginPath();
38
- ctx.moveTo(x, y + height * 0.1);
39
- ctx.lineTo(x + width, y);
40
- ctx.lineTo(x + width, y + height);
41
- ctx.lineTo(x, y + height * 0.9);
42
- ctx.closePath();
43
- ctx.fill();
44
 
45
  // Các ô pin
46
  ctx.strokeStyle = colors.solarLight;
@@ -49,7 +56,7 @@ function drawSolarPanelIcon(x, y, width, height) {
49
  const cellHeight = height / 3;
50
  for (let i = 0; i < 4; i++) {
51
  for (let j = 0; j < 3; j++) {
52
- ctx.strokeRect(x + i * cellWidth, y + j * cellHeight + (j * cellHeight * 0.1), cellWidth, cellHeight);
53
  }
54
  }
55
 
@@ -59,13 +66,17 @@ function drawSolarPanelIcon(x, y, width, height) {
59
  gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
60
  ctx.fillStyle = gradient;
61
  ctx.beginPath();
62
- ctx.moveTo(x, y + height * 0.1);
63
  ctx.lineTo(x + width * 0.3, y);
64
  ctx.lineTo(x + width * 0.5, y + height * 0.3);
65
  ctx.lineTo(x, y + height * 0.5);
66
  ctx.closePath();
67
  ctx.fill();
68
 
 
 
 
 
69
  ctx.fillStyle = colors.text;
70
  ctx.font = '14px Arial';
71
  ctx.textAlign = 'center';
@@ -74,21 +85,11 @@ function drawSolarPanelIcon(x, y, width, height) {
74
 
75
  // Vẽ biểu tượng biến tần (inverter)
76
  function drawInverterIcon(x, y, width, height) {
77
- // Thân biến tần
78
- ctx.fillStyle = colors.inverterBody;
79
- ctx.fillRect(x, y, width, height);
80
 
81
- // Viền và bóng đổ
82
  ctx.strokeStyle = '#333';
83
  ctx.lineWidth = 2;
84
- ctx.strokeRect(x, y, width, height);
85
-
86
- ctx.shadowColor = colors.shadow;
87
- ctx.shadowBlur = 8;
88
- ctx.shadowOffsetX = 3;
89
- ctx.shadowOffsetY = 3;
90
- ctx.fillRect(x, y, width, height); // Vẽ lại để áp dụng bóng đổ
91
- ctx.shadowBlur = 0; // Đặt lại bóng đổ
92
 
93
  // Các cổng kết nối
94
  ctx.fillStyle = colors.inverterAccent;
@@ -109,21 +110,11 @@ function drawInverterIcon(x, y, width, height) {
109
 
110
  // Vẽ biểu tượng pin lưu trữ (battery)
111
  function drawBatteryIcon(x, y, width, height) {
112
- // Thân pin
113
- ctx.fillStyle = colors.batteryBody;
114
- ctx.fillRect(x, y, width, height);
115
 
116
- // Viền và bóng đổ
117
  ctx.strokeStyle = '#333';
118
  ctx.lineWidth = 2;
119
- ctx.strokeRect(x, y, width, height);
120
-
121
- ctx.shadowColor = colors.shadow;
122
- ctx.shadowBlur = 8;
123
- ctx.shadowOffsetX = 3;
124
- ctx.shadowOffsetY = 3;
125
- ctx.fillRect(x, y, width, height); // Vẽ lại để áp dụng bóng đổ
126
- ctx.shadowBlur = 0; // Đặt lại bóng đổ
127
 
128
  // Các cực
129
  ctx.fillStyle = colors.batteryAccent;
@@ -144,9 +135,7 @@ function drawBatteryIcon(x, y, width, height) {
144
 
145
  // Vẽ biểu tượng nhà (home load)
146
  function drawHomeIcon(x, y, width, height) {
147
- // Thân nhà
148
- ctx.fillStyle = colors.homeBody;
149
- ctx.fillRect(x, y + height * 0.4, width, height * 0.6);
150
 
151
  // Mái nhà
152
  ctx.fillStyle = colors.homeRoof;
@@ -157,7 +146,6 @@ function drawHomeIcon(x, y, width, height) {
157
  ctx.closePath();
158
  ctx.fill();
159
 
160
- // Viền và bóng đổ
161
  ctx.strokeStyle = '#333';
162
  ctx.lineWidth = 2;
163
  ctx.strokeRect(x, y + height * 0.4, width, height * 0.6);
@@ -168,19 +156,6 @@ function drawHomeIcon(x, y, width, height) {
168
  ctx.closePath();
169
  ctx.stroke();
170
 
171
- ctx.shadowColor = colors.shadow;
172
- ctx.shadowBlur = 8;
173
- ctx.shadowOffsetX = 3;
174
- ctx.shadowOffsetY = 3;
175
- ctx.fillRect(x, y + height * 0.4, width, height * 0.6);
176
- ctx.beginPath();
177
- ctx.moveTo(x - 10, y + height * 0.4);
178
- ctx.lineTo(x + width / 2, y);
179
- ctx.lineTo(x + width + 10, y + height * 0.4);
180
- ctx.closePath();
181
- ctx.fill();
182
- ctx.shadowBlur = 0; // Đặt lại bóng đổ
183
-
184
  // Cửa sổ
185
  ctx.fillStyle = 'rgba(173, 216, 230, 0.8)'; // Xanh da trời nhạt
186
  ctx.fillRect(x + width * 0.2, y + height * 0.5, width * 0.25, height * 0.25);
@@ -190,56 +165,131 @@ function drawHomeIcon(x, y, width, height) {
190
  ctx.fillText('Tải nhà', x + width / 2, y + height + 20);
191
  }
192
 
193
- // Hàm vẽ mũi tên di chuyển
194
- function drawMovingArrow(p1, p2, offset, arrowSize = 10) {
195
- const dx = p2.x - p1.x;
196
- const dy = p2.y - p1.y;
197
- const distance = Math.sqrt(dx * dx + dy * dy);
198
 
199
- // Không vẽ nếu đường quá ngắn
200
- if (distance < arrowSize * 2) return;
 
201
 
202
- // Đường dẫn chính của dòng chảy
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  ctx.strokeStyle = colors.flowPrimary;
204
  ctx.lineWidth = 4;
205
  ctx.beginPath();
206
- ctx.moveTo(p1.x, p1.y);
207
- ctx.lineTo(p2.x, p2.y);
 
 
208
  ctx.stroke();
209
 
210
- ctx.fillStyle = colors.flowSecondary; // Màu trắng cho mũi tên
211
  ctx.strokeStyle = '#333';
212
  ctx.lineWidth = 1;
213
 
214
- // Tính toán số lượng mũi tên và vị trí
215
- const numArrows = Math.floor(distance / (arrowSize * 2.5)); // Số lượng mũi tên trên đường
216
- const arrowSpacing = distance / numArrows;
 
 
 
 
 
 
 
 
217
 
218
  for (let i = 0; i < numArrows; i++) {
219
- let currentPos = (offset + i * arrowSpacing) % distance;
220
- if (currentPos < 0) currentPos += distance; // Xử lý offset âm
221
-
222
- const ratio = currentPos / distance;
223
- const arrowX = p1.x + dx * ratio;
224
- const arrowY = p1.y + dy * ratio;
225
-
226
- // Tính góc của đường
227
- const angle = Math.atan2(dy, dx);
228
-
229
- ctx.save();
230
- ctx.translate(arrowX, arrowY);
231
- ctx.rotate(angle);
232
-
233
- // Vẽ hình tam giác (mũi tên)
234
- ctx.beginPath();
235
- ctx.moveTo(-arrowSize, -arrowSize / 2);
236
- ctx.lineTo(0, 0); // Đầu mũi tên
237
- ctx.lineTo(-arrowSize, arrowSize / 2);
238
- ctx.closePath();
239
- ctx.fill();
240
- ctx.stroke();
241
-
242
- ctx.restore();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  }
244
  }
245
 
@@ -253,35 +303,56 @@ function draw() {
253
  drawInverterIcon(inverter.x, inverter.y, inverter.width, inverter.height);
254
  drawBatteryIcon(battery.x, battery.y, battery.width, battery.height);
255
  drawHomeIcon(homeLoad.x, homeLoad.y, homeLoad.width, homeLoad.height);
 
 
256
 
257
- // Vẽ luồng năng lượng với mũi tên
258
- // Pin -> Biến tần (DC)
259
- drawMovingArrow(
260
  { x: solarPanel.x + solarPanel.width / 2, y: solarPanel.y + solarPanel.height },
261
- { x: inverter.x + inverter.width * 0.25, y: inverter.y + inverter.height / 2 },
262
- energyFlowOffset
263
- );
264
-
265
- // Biến tần -> Tải nhà (AC)
266
- drawMovingArrow(
267
- { x: inverter.x + inverter.width * 0.75, y: inverter.y + inverter.height / 2 },
268
- { x: homeLoad.x, y: homeLoad.y + homeLoad.height / 2 },
269
- energyFlowOffset
270
- );
271
-
272
- // Biến tần -> Pin lưu trữ (năng lượng dư thừa, AC sang DC)
273
- drawMovingArrow(
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  { x: inverter.x + inverter.width / 2, y: inverter.y + inverter.height },
275
- { x: battery.x + battery.width / 2, y: battery.y },
276
- energyFlowOffset
277
- );
 
 
 
 
 
 
 
 
 
 
 
 
278
 
279
- // Pin lưu trữ -> Biến tần (khi tải cần, DC sang AC)
280
- drawMovingArrow(
281
- { x: battery.x + battery.width / 2, y: battery.y },
282
- { x: inverter.x + inverter.width / 2, y: inverter.y + inverter.height },
283
- -energyFlowOffset // Sử dụng offset ngược lại để tạo hiệu ứng mũi tên đi ngược
284
- );
285
 
286
  // Cập nhật offset để tạo hoạt hình dòng chảy
287
  energyFlowOffset = (energyFlowOffset + 2) % 100; // Thay đổi tốc độ và khoảng cách mũi tên
@@ -289,9 +360,9 @@ function draw() {
289
  animationFrameId = requestAnimationFrame(draw); // Lặp lại hoạt hình
290
  }
291
 
292
- // Các hàm điều khiển hoạt hình
293
  function startAnimation() {
294
- if (!animationFrameId) { // Chỉ bắt đầu nếu chưa có hoạt hình đang chạy
295
  draw();
296
  }
297
  }
 
11
  const inverter = { x: 280, y: 150, width: 90, height: 70 };
12
  const battery = { x: 480, y: 250, width: 80, height: 110 };
13
  const homeLoad = { x: 650, y: 150, width: 80, height: 70 };
14
+ const grid = { x: 400, y: 50, width: 100, height: 70 }; // Lưới điện
15
+ const generator = { x: 100, y: 250, width: 100, height: 70 }; // Máy phát điện
16
 
17
  // Định nghĩa màu sắc và thuộc tính
18
  const colors = {
 
24
  batteryAccent: '#BBDEFB', // Xanh dương nhạt cho điểm nhấn pin
25
  homeRoof: '#E91E63', // Hồng cho mái nhà
26
  homeBody: '#FFEB3B', // Vàng cho thân nhà
27
+ gridBody: '#795548', // Nâu cho lưới điện
28
+ generatorBody: '#FF5722', // Cam đỏ cho máy phát điện
29
  flowPrimary: '#FF9800', // Cam sáng cho dòng chảy chính
30
  flowSecondary: 'rgba(255, 255, 255, 0.7)', // Trắng trong suốt cho hiệu ứng dòng chảy
31
  text: '#212121', // Đen gần cho văn bản
 
34
 
35
  // --- Hàm vẽ các biểu tượng chi tiết hơn ---
36
 
37
+ // Hàm vẽ chung cho các thành phần có bóng đổ
38
+ function drawRectWithShadow(x, y, width, height, color) {
39
+ ctx.shadowColor = colors.shadow;
40
+ ctx.shadowBlur = 8;
41
+ ctx.shadowOffsetX = 3;
42
+ ctx.shadowOffsetY = 3;
43
+ ctx.fillStyle = color;
44
+ ctx.fillRect(x, y, width, height);
45
+ ctx.shadowBlur = 0; // Reset shadow for subsequent drawings
46
+ }
47
+
48
  // Vẽ biểu tượng tấm pin mặt trời
49
  function drawSolarPanelIcon(x, y, width, height) {
50
+ drawRectWithShadow(x, y, width, height, colors.solarDark); // Nền tấm pin
 
 
 
 
 
 
 
 
51
 
52
  // Các ô pin
53
  ctx.strokeStyle = colors.solarLight;
 
56
  const cellHeight = height / 3;
57
  for (let i = 0; i < 4; i++) {
58
  for (let j = 0; j < 3; j++) {
59
+ ctx.strokeRect(x + i * cellWidth, y + j * cellHeight, cellWidth, cellHeight);
60
  }
61
  }
62
 
 
66
  gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
67
  ctx.fillStyle = gradient;
68
  ctx.beginPath();
69
+ ctx.moveTo(x, y);
70
  ctx.lineTo(x + width * 0.3, y);
71
  ctx.lineTo(x + width * 0.5, y + height * 0.3);
72
  ctx.lineTo(x, y + height * 0.5);
73
  ctx.closePath();
74
  ctx.fill();
75
 
76
+ ctx.strokeStyle = '#333';
77
+ ctx.lineWidth = 2;
78
+ ctx.strokeRect(x, y, width, height); // Viền ngoài
79
+
80
  ctx.fillStyle = colors.text;
81
  ctx.font = '14px Arial';
82
  ctx.textAlign = 'center';
 
85
 
86
  // Vẽ biểu tượng biến tần (inverter)
87
  function drawInverterIcon(x, y, width, height) {
88
+ drawRectWithShadow(x, y, width, height, colors.inverterBody); // Thân biến tần
 
 
89
 
 
90
  ctx.strokeStyle = '#333';
91
  ctx.lineWidth = 2;
92
+ ctx.strokeRect(x, y, width, height); // Viền ngoài
 
 
 
 
 
 
 
93
 
94
  // Các cổng kết nối
95
  ctx.fillStyle = colors.inverterAccent;
 
110
 
111
  // Vẽ biểu tượng pin lưu trữ (battery)
112
  function drawBatteryIcon(x, y, width, height) {
113
+ drawRectWithShadow(x, y, width, height, colors.batteryBody); // Thân pin
 
 
114
 
 
115
  ctx.strokeStyle = '#333';
116
  ctx.lineWidth = 2;
117
+ ctx.strokeRect(x, y, width, height); // Viền ngoài
 
 
 
 
 
 
 
118
 
119
  // Các cực
120
  ctx.fillStyle = colors.batteryAccent;
 
135
 
136
  // Vẽ biểu tượng nhà (home load)
137
  function drawHomeIcon(x, y, width, height) {
138
+ drawRectWithShadow(x, y + height * 0.4, width, height * 0.6, colors.homeBody); // Thân nhà
 
 
139
 
140
  // Mái nhà
141
  ctx.fillStyle = colors.homeRoof;
 
146
  ctx.closePath();
147
  ctx.fill();
148
 
 
149
  ctx.strokeStyle = '#333';
150
  ctx.lineWidth = 2;
151
  ctx.strokeRect(x, y + height * 0.4, width, height * 0.6);
 
156
  ctx.closePath();
157
  ctx.stroke();
158
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  // Cửa sổ
160
  ctx.fillStyle = 'rgba(173, 216, 230, 0.8)'; // Xanh da trời nhạt
161
  ctx.fillRect(x + width * 0.2, y + height * 0.5, width * 0.25, height * 0.25);
 
165
  ctx.fillText('Tải nhà', x + width / 2, y + height + 20);
166
  }
167
 
168
+ // Vẽ biểu tượng Lưới điện quốc gia (Grid)
169
+ function drawGridIcon(x, y, width, height) {
170
+ drawRectWithShadow(x, y, width, height, colors.gridBody);
 
 
171
 
172
+ ctx.strokeStyle = '#333';
173
+ ctx.lineWidth = 2;
174
+ ctx.strokeRect(x, y, width, height);
175
 
176
+ ctx.fillStyle = '#FFF';
177
+ ctx.font = 'bold 16px Arial';
178
+ ctx.textAlign = 'center';
179
+ ctx.textBaseline = 'middle';
180
+ ctx.fillText('Lưới điện', x + width / 2, y + height / 2 - 10);
181
+ ctx.fillText('Quốc gia', x + width / 2, y + height / 2 + 10);
182
+
183
+ ctx.fillStyle = colors.text;
184
+ ctx.fillText('Lưới điện', x + width / 2, y + height + 20);
185
+ }
186
+
187
+ // Vẽ biểu tượng Máy phát điện (Generator)
188
+ function drawGeneratorIcon(x, y, width, height) {
189
+ drawRectWithShadow(x, y, width, height, colors.generatorBody);
190
+
191
+ ctx.strokeStyle = '#333';
192
+ ctx.lineWidth = 2;
193
+ ctx.strokeRect(x, y, width, height);
194
+
195
+ // Hình trụ
196
+ ctx.beginPath();
197
+ ctx.arc(x + width / 2, y + height * 0.4, width * 0.2, 0, Math.PI * 2);
198
+ ctx.fillStyle = 'rgba(0,0,0,0.5)';
199
+ ctx.fill();
200
+ ctx.stroke();
201
+
202
+ // Ống xả
203
+ ctx.fillStyle = '#333';
204
+ ctx.fillRect(x + width * 0.7, y + height * 0.2, width * 0.1, height * 0.2);
205
+
206
+ ctx.fillStyle = '#FFF';
207
+ ctx.font = 'bold 14px Arial';
208
+ ctx.textAlign = 'center';
209
+ ctx.textBaseline = 'middle';
210
+ ctx.fillText('Máy phát', x + width / 2, y + height / 2 + 5);
211
+
212
+ ctx.fillStyle = colors.text;
213
+ ctx.fillText('Máy phát điện', x + width / 2, y + height + 20);
214
+ }
215
+
216
+
217
+ // --- Hàm vẽ các đường thẳng/ngang và mũi tên di chuyển ---
218
+
219
+ function drawLinearFlow(points, offset, arrowSize = 10) {
220
  ctx.strokeStyle = colors.flowPrimary;
221
  ctx.lineWidth = 4;
222
  ctx.beginPath();
223
+ ctx.moveTo(points[0].x, points[0].y);
224
+ for (let i = 1; i < points.length; i++) {
225
+ ctx.lineTo(points[i].x, points[i].y);
226
+ }
227
  ctx.stroke();
228
 
229
+ ctx.fillStyle = colors.flowSecondary;
230
  ctx.strokeStyle = '#333';
231
  ctx.lineWidth = 1;
232
 
233
+ // Tính toán tổng chiều dài đường dẫn
234
+ let totalLength = 0;
235
+ for (let i = 0; i < points.length - 1; i++) {
236
+ totalLength += Math.sqrt(
237
+ Math.pow(points[i+1].x - points[i].x, 2) +
238
+ Math.pow(points[i+1].y - points[i].y, 2)
239
+ );
240
+ }
241
+
242
+ const numArrows = Math.floor(totalLength / (arrowSize * 2.5));
243
+ const arrowSpacing = totalLength / numArrows;
244
 
245
  for (let i = 0; i < numArrows; i++) {
246
+ let currentPathPos = (offset + i * arrowSpacing) % totalLength;
247
+ if (currentPathPos < 0) currentPathPos += totalLength;
248
+
249
+ let segmentLength = 0;
250
+ let p1 = null, p2 = null;
251
+ let segmentIndex = -1;
252
+
253
+ // Tìm đoạn đường mà mũi tên đang ở trên
254
+ for (let j = 0; j < points.length - 1; j++) {
255
+ const segLen = Math.sqrt(
256
+ Math.pow(points[j+1].x - points[j].x, 2) +
257
+ Math.pow(points[j+1].y - points[j].y, 2)
258
+ );
259
+ if (currentPathPos >= segmentLength && currentPathPos <= segmentLength + segLen) {
260
+ p1 = points[j];
261
+ p2 = points[j+1];
262
+ segmentIndex = j;
263
+ break;
264
+ }
265
+ segmentLength += segLen;
266
+ }
267
+
268
+ if (p1 && p2) {
269
+ const dx = p2.x - p1.x;
270
+ const dy = p2.y - p1.y;
271
+ const segmentRelativePos = currentPathPos - segmentLength;
272
+ const ratio = segLen > 0 ? segmentRelativePos / segLen : 0;
273
+
274
+ const arrowX = p1.x + dx * ratio;
275
+ const arrowY = p1.y + dy * ratio;
276
+
277
+ const angle = Math.atan2(dy, dx);
278
+
279
+ ctx.save();
280
+ ctx.translate(arrowX, arrowY);
281
+ ctx.rotate(angle);
282
+
283
+ ctx.beginPath();
284
+ ctx.moveTo(-arrowSize, -arrowSize / 2);
285
+ ctx.lineTo(0, 0); // Đầu mũi tên
286
+ ctx.lineTo(-arrowSize, arrowSize / 2);
287
+ ctx.closePath();
288
+ ctx.fill();
289
+ ctx.stroke();
290
+
291
+ ctx.restore();
292
+ }
293
  }
294
  }
295
 
 
303
  drawInverterIcon(inverter.x, inverter.y, inverter.width, inverter.height);
304
  drawBatteryIcon(battery.x, battery.y, battery.width, battery.height);
305
  drawHomeIcon(homeLoad.x, homeLoad.y, homeLoad.width, homeLoad.height);
306
+ drawGridIcon(grid.x, grid.y, grid.width, grid.height); // Lưới điện
307
+ drawGeneratorIcon(generator.x, generator.y, generator.width, generator.height); // Máy phát điện
308
 
309
+ // Vẽ luồng năng lượng bằng đường thẳng/ngang
310
+ // 1. Tấm pin -> Biến tần (DC)
311
+ drawLinearFlow([
312
  { x: solarPanel.x + solarPanel.width / 2, y: solarPanel.y + solarPanel.height },
313
+ { x: solarPanel.x + solarPanel.width / 2, y: inverter.y + inverter.height / 2 },
314
+ { x: inverter.x, y: inverter.y + inverter.height / 2 }
315
+ ], energyFlowOffset);
316
+
317
+ // 2. Biến tần -> Tải nhà (AC)
318
+ drawLinearFlow([
319
+ { x: inverter.x + inverter.width, y: inverter.y + inverter.height / 2 },
320
+ { x: homeLoad.x, y: homeLoad.y + homeLoad.height / 2 }
321
+ ], energyFlowOffset);
322
+
323
+ // 3. Biến tần <-> Lưới điện (AC)
324
+ drawLinearFlow([
325
+ { x: inverter.x + inverter.width / 2, y: inverter.y },
326
+ { x: inverter.x + inverter.width / 2, y: grid.y + grid.height / 2 },
327
+ { x: grid.x, y: grid.y + grid.height / 2 }
328
+ ], energyFlowOffset); // Biến tần -> Lưới
329
+
330
+ drawLinearFlow([
331
+ { x: grid.x, y: grid.y + grid.height / 2 }, // Điểm này phải đúng
332
+ { x: inverter.x + inverter.width / 2, y: grid.y + grid.height / 2 },
333
+ { x: inverter.x + inverter.width / 2, y: inverter.y }
334
+ ], -energyFlowOffset); // Lưới -> Biến tần (ngược chiều)
335
+
336
+
337
+ // 4. Biến tần <-> Pin lưu trữ (AC/DC)
338
+ drawLinearFlow([
339
  { x: inverter.x + inverter.width / 2, y: inverter.y + inverter.height },
340
+ { x: inverter.x + inverter.width / 2, y: battery.y + battery.height / 2 },
341
+ { x: battery.x + battery.width, y: battery.y + battery.height / 2 } // Vị trí kết nối bên phải pin
342
+ ], energyFlowOffset); // Biến tần -> Pin
343
+
344
+ drawLinearFlow([
345
+ { x: battery.x + battery.width, y: battery.y + battery.height / 2 },
346
+ { x: inverter.x + inverter.width / 2, y: battery.y + battery.height / 2 },
347
+ { x: inverter.x + inverter.width / 2, y: inverter.y + inverter.height }
348
+ ], -energyFlowOffset); // Pin -> Biến tần (ngược chiều)
349
+
350
+ // 5. Máy phát điện -> Biến tần (hoặc thẳng vào nhà nếu không có biến tần, nhưng ở đây kết nối qua biến tần)
351
+ drawLinearFlow([
352
+ { x: generator.x + generator.width, y: generator.y + generator.height / 2 },
353
+ { x: inverter.x, y: inverter.y + inverter.height / 2 } // Nối vào biến tần
354
+ ], energyFlowOffset);
355
 
 
 
 
 
 
 
356
 
357
  // Cập nhật offset để tạo hoạt hình dòng chảy
358
  energyFlowOffset = (energyFlowOffset + 2) % 100; // Thay đổi tốc độ và khoảng cách mũi tên
 
360
  animationFrameId = requestAnimationFrame(draw); // Lặp lại hoạt hình
361
  }
362
 
363
+ // Các hàm điều khiển hoạt hình (không thay đổi)
364
  function startAnimation() {
365
+ if (!animationFrameId) {
366
  draw();
367
  }
368
  }