fix(blobbi): reposition dirt marks to lower body edges, avoid facial zones

Protected zones (dirt marks NEVER appear here):
- Eyes, mouth, eyebrows
- Tears, saliva/drool, blush marks, sparkles
- Upper-center body area where face elements live

Preferred dirt placement zones:
- Lower-left edge of body silhouette
- Lower-right edge of body silhouette
- Bottom edge (well below face region)

Variant differences:
- Egg: dust at lower outer shell edges only, no center-front placement
- Baby (100x100): safe zone y > 72, prefer x < 35 or x > 65
- Adult (200x200): safe zone y > 120, prefer x < 85 or x > 115

Also updated dust particle positions to follow same rules.
This commit is contained in:
filemon
2026-03-30 14:30:37 -03:00
parent a74f7037ff
commit 5ab16fbbf3
3 changed files with 121 additions and 119 deletions
-25
View File
@@ -4706,7 +4706,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4720,7 +4719,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4734,7 +4732,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4748,7 +4745,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4762,7 +4758,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4776,7 +4771,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4790,7 +4784,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4804,7 +4797,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4818,7 +4810,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4832,7 +4823,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4846,7 +4836,6 @@
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4860,7 +4849,6 @@
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4874,7 +4862,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4888,7 +4875,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4902,7 +4888,6 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4916,7 +4901,6 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4930,7 +4914,6 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4944,7 +4927,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4958,7 +4940,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4972,7 +4953,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4986,7 +4966,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5000,7 +4979,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5014,7 +4992,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5028,7 +5005,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5042,7 +5018,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
+65 -61
View File
@@ -702,15 +702,20 @@ export const EggGraphic: React.FC<EggGraphicProps> = ({
{/* ── Status Effects ─────────────────────────────────────────────── */}
{/* Dirty effect: sweat droplet + dust particles (front + back layers) */}
{/* Dirty effect: sweat droplet + dust at lower shell edges
Placement rules for egg:
- Sweat droplet stays at upper-left (outside egg, not on face)
- Dust particles only at lower outer shell edges
- Avoid center-front placement entirely
*/}
{statusEffects?.dirty && (
<>
{/* Sweat droplet - upper left of egg */}
{/* Sweat droplet - upper left outside egg shell */}
<div
className="absolute animate-egg-sweat-drop"
style={{
top: '15%',
left: '8%',
left: '5%',
width: '0.6em',
height: '0.9em',
background: 'linear-gradient(180deg, rgba(147, 197, 253, 0.9) 0%, rgba(59, 130, 246, 0.7) 100%)',
@@ -718,85 +723,84 @@ export const EggGraphic: React.FC<EggGraphicProps> = ({
zIndex: 20,
}}
/>
{/* Dust particles underneath (back layer) */}
{/* Dust particles underneath (back layer) - at lower edges */}
<div
className="absolute animate-egg-dust"
style={{
bottom: '-5%',
left: '25%',
width: '50%',
left: '20%',
width: '60%',
height: '0.4em',
display: 'flex',
justifyContent: 'space-around',
justifyContent: 'space-between',
zIndex: 5,
}}
>
{[0, 1, 2].map((i) => (
<div
key={i}
style={{
width: '0.3em',
height: '0.3em',
background: 'rgba(87, 83, 78, 0.7)', // Stronger brown color
borderRadius: '50%',
animationDelay: `${i * 0.3}s`,
}}
className="animate-egg-dust-particle"
/>
))}
</div>
{/* Front-layer dust particles - floating in front of egg */}
<div
className="absolute"
style={{
top: '60%',
left: '15%',
width: '70%',
height: '30%',
pointerEvents: 'none',
zIndex: 25, // In front of egg
}}
>
{/* Front dust particle 1 - lower left */}
{/* Left edge particle */}
<div
className="absolute animate-egg-dust-particle"
style={{
bottom: '10%',
left: '5%',
width: '0.25em',
height: '0.25em',
background: 'rgba(68, 64, 60, 0.75)', // Darker, more visible
borderRadius: '50%',
animationDelay: '0.1s',
}}
/>
{/* Front dust particle 2 - center */}
<div
className="absolute animate-egg-dust-particle"
style={{
bottom: '25%',
left: '45%',
width: '0.2em',
height: '0.2em',
width: '0.3em',
height: '0.3em',
background: 'rgba(87, 83, 78, 0.7)',
borderRadius: '50%',
animationDelay: '0.5s',
}}
className="animate-egg-dust-particle"
/>
{/* Front dust particle 3 - lower right */}
{/* Right edge particle */}
<div
className="absolute animate-egg-dust-particle"
style={{
bottom: '5%',
right: '10%',
width: '0.22em',
height: '0.22em',
background: 'rgba(68, 64, 60, 0.7)',
width: '0.28em',
height: '0.28em',
background: 'rgba(87, 83, 78, 0.65)',
borderRadius: '50%',
animationDelay: '0.8s',
animationDelay: '0.4s',
}}
className="animate-egg-dust-particle"
/>
</div>
{/* Front-layer dust - ONLY at lower-left and lower-right edges */}
{/* Lower-left edge particle */}
<div
className="absolute animate-egg-dust-particle"
style={{
bottom: '12%',
left: '8%',
width: '0.22em',
height: '0.22em',
background: 'rgba(68, 64, 60, 0.7)',
borderRadius: '50%',
zIndex: 25,
animationDelay: '0.1s',
}}
/>
{/* Lower-right edge particle */}
<div
className="absolute animate-egg-dust-particle"
style={{
bottom: '15%',
right: '6%',
width: '0.2em',
height: '0.2em',
background: 'rgba(68, 64, 60, 0.65)',
borderRadius: '50%',
zIndex: 25,
animationDelay: '0.5s',
}}
/>
{/* Very bottom left particle */}
<div
className="absolute animate-egg-dust-particle"
style={{
bottom: '5%',
left: '15%',
width: '0.18em',
height: '0.18em',
background: 'rgba(87, 83, 78, 0.6)',
borderRadius: '50%',
zIndex: 25,
animationDelay: '0.8s',
}}
/>
</>
)}
+56 -33
View File
@@ -69,41 +69,63 @@ function estimatePathBounds(pathD: string): { minY: number; maxY: number } {
// ─── Dirt Marks ───────────────────────────────────────────────────────────────
/**
* Protected facial zones (where dirt marks should NEVER appear):
* - Eyes, mouth, eyebrows
* - Tears, saliva/drool, blush marks
* - Sparkles and other facial extras
* - Upper-center body area where face elements live
*
* Preferred dirt placement zones:
* - Lower-left edge of body silhouette
* - Lower-right edge of body silhouette
* - Bottom edge (below face region)
* - Side contours in lower half of body
*
* Baby (100x100 viewBox):
* - Face region: x: 30-70, y: 35-70 (AVOID)
* - Safe lower edges: y > 72, prefer x < 35 or x > 65
*
* Adult (200x200 viewBox):
* - Face region: x: 80-120, y: 70-115 (AVOID)
* - Safe lower edges: y > 120, prefer x < 85 or x > 115
*/
/**
* Dirt mark positions for baby variant (100x100 viewBox).
* Baby body path: roughly x: 25-75, y: 15-88, center at x=50
* Positioned at lower-left and lower-right edges, avoiding face region.
*/
const BABY_DIRT_POSITIONS = [
// Primary marks - well within the droplet body silhouette
{ x: 40, y: 60, angle: 12, length: 3 }, // left-center
{ x: 55, y: 55, angle: -8, length: 2.5 }, // right-center
{ x: 48, y: 50, angle: 5, length: 2.5 }, // center, mid
// Additional marks for higher counts
{ x: 52, y: 68, angle: -5, length: 2 }, // center, lower
{ x: 42, y: 45, angle: 15, length: 2 }, // left, upper-mid
// Primary marks - lower side edges, well below face
{ x: 30, y: 76, angle: 25, length: 2.5 }, // lower-left edge
{ x: 68, y: 74, angle: -20, length: 2.5 }, // lower-right edge
{ x: 32, y: 82, angle: 15, length: 2 }, // very low left
// Additional marks for higher counts - still at edges
{ x: 66, y: 80, angle: -15, length: 2 }, // very low right
{ x: 50, y: 84, angle: 5, length: 2 }, // bottom center (safe - well below face)
];
/**
* Dirt mark positions for adult variant (200x200 viewBox).
* Adult body varies by form, but generally centered around x=100.
* Using conservative positions that work across most forms.
* Positioned at lower side edges, avoiding face region entirely.
*/
const ADULT_DIRT_POSITIONS = [
// Primary marks - centered in the main body area
{ x: 88, y: 95, angle: 12, length: 5 }, // left of center
{ x: 108, y: 90, angle: -8, length: 4.5 }, // right of center
{ x: 98, y: 85, angle: 5, length: 4 }, // center
// Primary marks - lower side edges
{ x: 78, y: 125, angle: 30, length: 4 }, // lower-left side
{ x: 122, y: 122, angle: -25, length: 4 }, // lower-right side
{ x: 80, y: 138, angle: 20, length: 3.5 }, // very low left
// Additional marks for higher counts
{ x: 102, y: 100, angle: -5, length: 3.5 }, // center, lower
{ x: 92, y: 80, angle: 15, length: 3.5 }, // left, upper-mid
{ x: 120, y: 135, angle: -20, length: 3.5 }, // very low right
{ x: 100, y: 145, angle: 5, length: 3 }, // bottom center (safe - well below face)
];
/**
* Generate dirt marks/scratches on the body.
*
* Positions are constrained to the Blobbi body silhouette:
* - Baby (100x100): centered around x=50, within ~38-62 range
* - Adult (200x200): centered around x=100, scaled appropriately
* Placement rules:
* - AVOID face region (eyes, mouth, eyebrows, tears, drool, blush, sparkles)
* - PREFER lower-left and lower-right edges of body silhouette
* - Bottom edge placement is safe (well below face elements)
*
* @param config - Dirt marks configuration including variant
* @returns SVG markup for dirt marks
@@ -148,38 +170,39 @@ export function generateDirtMarks(config: DirtMarksConfig): string {
/**
* Dust particle positions for baby variant (100x100 viewBox).
* Front-layer particles are positioned in front of the body for stronger dirty read.
* All particles positioned at lower edges, avoiding face region.
*/
const BABY_DUST_POSITIONS = {
// Back layer - below the body
back: [
{ x: 40, y: 90, r: 1.5, delay: 0 },
{ x: 35, y: 90, r: 1.5, delay: 0 },
{ x: 50, y: 92, r: 1.2, delay: 0.3 },
{ x: 58, y: 89, r: 1.3, delay: 0.6 },
{ x: 65, y: 89, r: 1.3, delay: 0.6 },
],
// Front layer - floating in front of body
// Front layer - at lower side edges, NOT in face region
front: [
{ x: 38, y: 70, r: 1.2, delay: 0.1 },
{ x: 58, y: 65, r: 1.0, delay: 0.4 },
{ x: 48, y: 75, r: 0.9, delay: 0.7 },
{ x: 28, y: 78, r: 1.0, delay: 0.1 }, // lower-left edge
{ x: 70, y: 76, r: 0.9, delay: 0.4 }, // lower-right edge
{ x: 32, y: 84, r: 0.8, delay: 0.7 }, // very low left
],
};
/**
* Dust particle positions for adult variant (200x200 viewBox).
* All particles positioned at lower edges, avoiding face region.
*/
const ADULT_DUST_POSITIONS = {
// Back layer - below the body
back: [
{ x: 85, y: 180, r: 2.5, delay: 0 },
{ x: 100, y: 183, r: 2.2, delay: 0.3 },
{ x: 115, y: 178, r: 2.3, delay: 0.6 },
{ x: 80, y: 175, r: 2.5, delay: 0 },
{ x: 100, y: 180, r: 2.2, delay: 0.3 },
{ x: 120, y: 173, r: 2.3, delay: 0.6 },
],
// Front layer - floating in front of body
// Front layer - at lower side edges, NOT in face region
front: [
{ x: 82, y: 105, r: 2.0, delay: 0.1 },
{ x: 115, y: 95, r: 1.8, delay: 0.4 },
{ x: 98, y: 110, r: 1.6, delay: 0.7 },
{ x: 75, y: 130, r: 1.8, delay: 0.1 }, // lower-left side
{ x: 125, y: 128, r: 1.6, delay: 0.4 }, // lower-right side
{ x: 78, y: 142, r: 1.4, delay: 0.7 }, // very low left
],
};