Skip to content

Commit d80027e

Browse files
committed
新增玻璃碎片三角形效果,优化动态背景动画逻辑,减少内存分配与GC开销,增强交互表现力与动画流畅度。
1 parent 3d9ab4e commit d80027e

File tree

1 file changed

+225
-19
lines changed

1 file changed

+225
-19
lines changed

composeApp/src/wasmJsMain/kotlin/love/forte/simbot/codegen/components/OptimizedAnimatedBackground.kt

Lines changed: 225 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import androidx.compose.ui.ExperimentalComposeUiApi
99
import androidx.compose.ui.Modifier
1010
import androidx.compose.ui.geometry.Offset
1111
import androidx.compose.ui.graphics.Color
12+
import androidx.compose.ui.graphics.Path
1213
import androidx.compose.ui.graphics.drawscope.DrawScope
1314
import androidx.compose.ui.platform.LocalDensity
1415
import androidx.compose.ui.unit.dp
@@ -28,10 +29,36 @@ private class MutableParticle(
2829
val alpha: Float
2930
)
3031

32+
/**
33+
* Performance-optimized connection representation using value class.
34+
* Packs two integers into a single Long to avoid object allocation overhead
35+
* compared to Pair<Int, Int>. This reduces GC pressure in the animation loop
36+
* where connections are created frequently for triangle detection.
37+
*/
38+
@Immutable
39+
@Stable
40+
private value class Connection(private val value: Long) {
41+
constructor(first: Int, second: Int) : this((first.toLong() shl 32) or (second.toLong() and 0xFFFFFFFF))
42+
43+
@Stable
44+
inline val first: Int get() = (value shr 32).toInt()
45+
46+
@Stable
47+
inline val second: Int get() = value.toInt()
48+
49+
@Stable
50+
@Suppress("NOTHING_TO_INLINE")
51+
inline operator fun component1(): Int = first
52+
53+
@Stable
54+
@Suppress("NOTHING_TO_INLINE")
55+
inline operator fun component2(): Int = second
56+
}
57+
3158
/**
3259
* Ultra-optimized animated background with theme-aware performance scaling:
3360
* 1. Eliminated O(n²) algorithm with distance culling
34-
* 2. Replaced expensive sqrt() with squared distance comparisons
61+
* 2. Replaced expensive sqrt() with squared distance comparisons
3562
* 3. Fixed excessive recomposition issues
3663
* 4. Used mutable particles to reduce object allocation
3764
* 5. Cached color objects to reduce GC pressure
@@ -49,7 +76,7 @@ fun OptimizedAnimatedBackground(
4976
) {
5077
val density = LocalDensity.current
5178
val colorScheme = MaterialTheme.colorScheme
52-
79+
5380
// Theme-aware performance scaling - reduce animation during theme transitions
5481
val performanceScale = remember(themeEffects?.isTransitioning) {
5582
if (themeEffects?.isTransitioning == true) 0.3f else 1f // 70% reduction during transitions
@@ -64,14 +91,15 @@ fun OptimizedAnimatedBackground(
6491
// Mutable particles list for better performance
6592
val particles = remember { mutableListOf<MutableParticle>() }
6693
var canvasSize by remember { mutableStateOf(Offset.Zero) }
67-
94+
6895
// Theme-aware cached colors with sci-fi glow effects
6996
val cachedColors = remember(colorScheme, themeEffects?.glowIntensity) {
7097
val glowIntensity = themeEffects?.glowIntensity ?: 0f
7198
ParticleColors(
7299
particleColor = colorScheme.primary.copy(alpha = 0.65f + glowIntensity * 0.2f),
73100
connectionColor = colorScheme.secondary.copy(alpha = 0.4f + glowIntensity * 0.15f),
74-
mouseConnectionColor = colorScheme.secondary.copy(alpha = 0.48f + glowIntensity * 0.25f)
101+
mouseConnectionColor = colorScheme.secondary.copy(alpha = 0.48f + glowIntensity * 0.25f),
102+
triangleFillColor = colorScheme.secondary.copy(alpha = 0.08f + glowIntensity * 0.03f)
75103
)
76104
}
77105

@@ -91,15 +119,15 @@ fun OptimizedAnimatedBackground(
91119
// Consolidated LaunchedEffect for both initialization and updates with theme-aware scaling
92120
LaunchedEffect(canvasSize, scaledParticleCount, animationTime) {
93121
if (canvasSize == Offset.Zero) return@LaunchedEffect
94-
122+
95123
// Initialize particles if needed with scaled count
96124
if (particles.size != scaledParticleCount) {
97125
particles.clear()
98126
repeat(scaledParticleCount) {
99127
particles.add(createRandomMutableParticle(canvasSize.x, canvasSize.y, scaledParticleSpeed))
100128
}
101129
}
102-
130+
103131
// Update existing particles in-place with scaled speed
104132
particles.forEach { particle ->
105133
updateMutableParticle(particle, canvasSize.x, canvasSize.y, scaledParticleSpeed)
@@ -138,7 +166,8 @@ fun OptimizedAnimatedBackground(
138166
private data class ParticleColors(
139167
val particleColor: Color,
140168
val connectionColor: Color,
141-
val mouseConnectionColor: Color
169+
val mouseConnectionColor: Color,
170+
val triangleFillColor: Color
142171
)
143172

144173
/**
@@ -158,7 +187,12 @@ private fun createRandomMutableParticle(canvasWidth: Float, canvasHeight: Float,
158187
/**
159188
* Update mutable particle in-place (no object allocation)
160189
*/
161-
private fun updateMutableParticle(particle: MutableParticle, canvasWidth: Float, canvasHeight: Float, baseSpeed: Float) {
190+
private fun updateMutableParticle(
191+
particle: MutableParticle,
192+
canvasWidth: Float,
193+
canvasHeight: Float,
194+
baseSpeed: Float
195+
) {
162196
// Update position
163197
particle.x += particle.vx
164198
particle.y += particle.vy
@@ -183,39 +217,204 @@ private fun updateMutableParticle(particle: MutableParticle, canvasWidth: Float,
183217
particle.vy = particle.vy.coerceIn(-maxSpeed, maxSpeed)
184218
}
185219

220+
/**
221+
* Draw glass shard triangles formed by connected particles and mouse
222+
*/
223+
private fun DrawScope.drawTriangles(
224+
particles: List<MutableParticle>,
225+
connections: List<Connection>,
226+
mouseConnections: List<Int>,
227+
mousePosition: Offset?,
228+
connectionDistanceSquared: Float,
229+
colors: ParticleColors
230+
) {
231+
232+
// Find triangles formed by particle-to-particle connections
233+
for (i in connections.indices) {
234+
for (j in i + 1 until connections.size) {
235+
val conn1 = connections[i]
236+
val conn2 = connections[j]
237+
238+
// Check if two connections share a common particle (form a triangle)
239+
val sharedParticle = when {
240+
conn1.first == conn2.first -> conn1.first
241+
conn1.first == conn2.second -> conn1.first
242+
conn1.second == conn2.first -> conn1.second
243+
conn1.second == conn2.second -> conn1.second
244+
else -> null
245+
}
246+
247+
if (sharedParticle != null) {
248+
// Get the three particles forming the triangle
249+
val p1Index = if (conn1.first == sharedParticle) conn1.second else conn1.first
250+
val p2Index = sharedParticle
251+
val p3Index = if (conn2.first == sharedParticle) conn2.second else conn2.first
252+
253+
// Verify the third edge exists (p1 to p3)
254+
val p1 = particles[p1Index]
255+
val p3 = particles[p3Index]
256+
val dx = p1.x - p3.x
257+
val dy = p1.y - p3.y
258+
val distanceSquared = dx * dx + dy * dy
259+
260+
if (distanceSquared <= connectionDistanceSquared) {
261+
drawGlassTriangle(
262+
particles[p1Index],
263+
particles[p2Index],
264+
particles[p3Index],
265+
connectionDistanceSquared,
266+
colors.triangleFillColor
267+
)
268+
}
269+
}
270+
}
271+
}
272+
273+
// Find triangles involving the mouse position
274+
mousePosition?.let { mouse ->
275+
if (mouseConnections.size >= 2) {
276+
// Check each pair of mouse-connected particles
277+
for (i in mouseConnections.indices) {
278+
for (j in i + 1 until mouseConnections.size) {
279+
val p1Index = mouseConnections[i]
280+
val p2Index = mouseConnections[j]
281+
282+
// Check if these two particles are also connected to each other
283+
if (connections.any {
284+
(it.first == p1Index && it.second == p2Index) ||
285+
(it.first == p2Index && it.second == p1Index)
286+
}) {
287+
drawGlassTriangleWithMouse(
288+
particles[p1Index],
289+
particles[p2Index],
290+
mouse,
291+
connectionDistanceSquared,
292+
colors.triangleFillColor
293+
)
294+
}
295+
}
296+
}
297+
}
298+
}
299+
}
300+
301+
/**
302+
* Draw a glass shard triangle between three particles
303+
*/
304+
private fun DrawScope.drawGlassTriangle(
305+
p1: MutableParticle,
306+
p2: MutableParticle,
307+
p3: MutableParticle,
308+
connectionDistanceSquared: Float,
309+
baseColor: Color
310+
) {
311+
// Calculate distances of all three edges to find the longest
312+
val dist1Sq = (p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y)
313+
val dist2Sq = (p2.x - p3.x) * (p2.x - p3.x) + (p2.y - p3.y) * (p2.y - p3.y)
314+
val dist3Sq = (p3.x - p1.x) * (p3.x - p1.x) + (p3.y - p1.y) * (p3.y - p1.y)
315+
316+
// Use the longest edge for alpha calculation
317+
val longestDistanceSquared = maxOf(dist1Sq, dist2Sq, dist3Sq)
318+
val longestDistance = kotlin.math.sqrt(longestDistanceSquared)
319+
val maxDistance = kotlin.math.sqrt(connectionDistanceSquared)
320+
321+
// Make triangles fainter and decay faster than connection lines
322+
// Base alpha factor is 0.3 (much fainter than lines) and use squared ratio for faster decay
323+
val distanceRatio = (longestDistance / maxDistance).coerceIn(0f, 1f)
324+
val alpha = (1 - distanceRatio * distanceRatio) * baseColor.alpha * 0.3f
325+
326+
val trianglePath = Path().apply {
327+
moveTo(p1.x, p1.y)
328+
lineTo(p2.x, p2.y)
329+
lineTo(p3.x, p3.y)
330+
close()
331+
}
332+
333+
drawPath(
334+
path = trianglePath,
335+
color = baseColor.copy(alpha = alpha)
336+
)
337+
}
338+
339+
/**
340+
* Draw a glass shard triangle involving mouse position
341+
*/
342+
private fun DrawScope.drawGlassTriangleWithMouse(
343+
p1: MutableParticle,
344+
p2: MutableParticle,
345+
mousePos: Offset,
346+
connectionDistanceSquared: Float,
347+
baseColor: Color
348+
) {
349+
// Calculate distances of all three edges to find the longest
350+
val dist1Sq = (p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y)
351+
val dist2Sq = (p2.x - mousePos.x) * (p2.x - mousePos.x) + (p2.y - mousePos.y) * (p2.y - mousePos.y)
352+
val dist3Sq = (mousePos.x - p1.x) * (mousePos.x - p1.x) + (mousePos.y - p1.y) * (mousePos.y - p1.y)
353+
354+
// Use the longest edge for alpha calculation
355+
val longestDistanceSquared = maxOf(dist1Sq, dist2Sq, dist3Sq)
356+
val longestDistance = kotlin.math.sqrt(longestDistanceSquared)
357+
val maxDistance = kotlin.math.sqrt(connectionDistanceSquared)
358+
359+
// Make triangles fainter and decay faster than connection lines
360+
// Mouse triangles get slightly higher intensity (0.4 vs 0.3) but still much fainter than lines
361+
val distanceRatio = (longestDistance / maxDistance).coerceIn(0f, 1f)
362+
val alpha = (1 - distanceRatio * distanceRatio) * baseColor.alpha * 0.4f
363+
364+
val trianglePath = Path().apply {
365+
moveTo(p1.x, p1.y)
366+
lineTo(p2.x, p2.y)
367+
lineTo(mousePos.x, mousePos.y)
368+
close()
369+
}
370+
371+
drawPath(
372+
path = trianglePath,
373+
color = baseColor.copy(alpha = alpha)
374+
)
375+
}
376+
186377
/**
187378
* Optimized drawing function with major performance improvements:
188379
* 1. Eliminated nested loop O(n²) algorithm using distance culling
189380
* 2. Uses squared distance comparison (no expensive sqrt)
190381
* 3. Early termination for distant particles
191382
* 4. Cached color objects
192383
* 5. Reduced temporary object allocation
384+
* 6. Glass shard triangles when three points are connected
193385
*/
194386
private fun DrawScope.drawOptimizedParticlesAndConnections(
195387
particles: List<MutableParticle>,
196388
connectionDistanceSquared: Float,
197389
colors: ParticleColors,
198390
mousePosition: Offset? = null
199391
) {
200-
// Optimized connection drawing with distance culling
392+
// Collect all connected pairs for triangle detection
393+
val connections = mutableListOf<Connection>()
394+
val mouseConnections = mutableListOf<Int>()
395+
396+
// Optimized connection drawing with distance culling and connection tracking
201397
// Still O(n²) but with early termination for better average case performance
202398
for (i in particles.indices) {
203399
val particle1 = particles[i]
204-
400+
205401
for (j in i + 1 until particles.size) {
206402
val particle2 = particles[j]
207-
403+
208404
// Fast squared distance calculation (no sqrt needed)
209405
val dx = particle1.x - particle2.x
210406
val dy = particle1.y - particle2.y
211407
val distanceSquared = dx * dx + dy * dy
212-
408+
213409
// Early termination if too far apart
214410
if (distanceSquared <= connectionDistanceSquared) {
411+
// Track connection for triangle detection
412+
connections.add(Connection(i, j))
413+
215414
// Only calculate sqrt when we know we need to draw
216415
val distance = kotlin.math.sqrt(distanceSquared)
217416
val alpha = (1 - distance / kotlin.math.sqrt(connectionDistanceSquared)) * colors.connectionColor.alpha
218-
417+
219418
drawLine(
220419
color = colors.connectionColor.copy(alpha = alpha),
221420
start = Offset(particle1.x, particle1.y),
@@ -226,17 +425,21 @@ private fun DrawScope.drawOptimizedParticlesAndConnections(
226425
}
227426
}
228427

229-
// Optimized mouse connections
428+
// Optimized mouse connections with tracking for triangle detection
230429
mousePosition?.let { mouse ->
231-
particles.forEach { particle ->
430+
particles.forEachIndexed { index, particle ->
232431
val dx = particle.x - mouse.x
233432
val dy = particle.y - mouse.y
234433
val distanceSquared = dx * dx + dy * dy
235-
434+
236435
if (distanceSquared <= connectionDistanceSquared) {
436+
// Track mouse connection for triangle detection
437+
mouseConnections.add(index)
438+
237439
val distance = kotlin.math.sqrt(distanceSquared)
238-
val alpha = (1 - distance / kotlin.math.sqrt(connectionDistanceSquared)) * colors.mouseConnectionColor.alpha
239-
440+
val alpha =
441+
(1 - distance / kotlin.math.sqrt(connectionDistanceSquared)) * colors.mouseConnectionColor.alpha
442+
240443
drawLine(
241444
color = colors.mouseConnectionColor.copy(alpha = alpha),
242445
start = Offset(particle.x, particle.y),
@@ -247,6 +450,9 @@ private fun DrawScope.drawOptimizedParticlesAndConnections(
247450
}
248451
}
249452

453+
// Draw glass shard triangles
454+
drawTriangles(particles, connections, mouseConnections, mousePosition, connectionDistanceSquared, colors)
455+
250456
// Draw particles with cached colors
251457
particles.forEach { particle ->
252458
drawCircle(
@@ -255,4 +461,4 @@ private fun DrawScope.drawOptimizedParticlesAndConnections(
255461
center = Offset(particle.x, particle.y)
256462
)
257463
}
258-
}
464+
}

0 commit comments

Comments
 (0)