|
| 1 | +# ํซ์ Emotion ์ ๋๋ฉ์ด์
์ถ๊ฐํ๊ธฐ |
| 2 | + |
| 3 | +## ๊ฐ์ |
| 4 | + |
| 5 | +ํซ์ emotion ์ ๋๋ฉ์ด์
์ ์ถ๊ฐํ๋ฉด, `loadSvg` ํธ์ถ ์ ๋๋คํ ํ์ด๋ฐ์ emotion์ด ์ฌ์๋ฉ๋๋ค. |
| 6 | +emotion์ด ์ฌ์๋๋ ๋์ base ํซ์ ์จ๊ฒจ์ง๊ณ , emotion SVG๊ฐ ๋์ ํ์๋ฉ๋๋ค. |
| 7 | +์ด๋(act)๊ณผ ๋ฐฉํฅ์ ํ(flip)์ emotion ์ค์๋ ์ ์ง๋ฉ๋๋ค. |
| 8 | + |
| 9 | +## ํ์ผ ๊ตฌ์กฐ |
| 10 | + |
| 11 | +``` |
| 12 | +src/main/resources/persona/animal/ |
| 13 | +โโโ {pet-name}.svg # base ํซ SVG ํ
ํ๋ฆฟ |
| 14 | +โโโ emotion/{pet-short-name}/ # emotion SVG ๋๋ ํ ๋ฆฌ |
| 15 | + โโโ error.svg |
| 16 | + โโโ happy.svg |
| 17 | + โโโ idle-follow.svg |
| 18 | + โโโ notification.svg |
| 19 | + โโโ thinking.svg |
| 20 | + โโโ typing.svg |
| 21 | +
|
| 22 | +src/main/kotlin/org/gitanimals/core/ |
| 23 | +โโโ PersonaType.kt # ํซ enum (loadSvg, act, addEmotions) |
| 24 | +โโโ PersonaEmotionType.kt # emotion ํ์
enum (ERROR, HAPPY, IDLE_FOLLOW, NOTIFICATION, THINKING, TYPING) |
| 25 | +โโโ Svgs.kt # SVG ํ์ผ ๋ก๋ฉ |
| 26 | +``` |
| 27 | + |
| 28 | +## ๋จ๊ณ๋ณ ๊ตฌํ |
| 29 | + |
| 30 | +### 1. Emotion SVG ํ์ผ ์์ฑ |
| 31 | + |
| 32 | +`src/main/resources/persona/animal/emotion/{pet-short-name}/` ๋๋ ํ ๋ฆฌ์ 6๊ฐ์ emotion SVG๋ฅผ ์์ฑํฉ๋๋ค. |
| 33 | + |
| 34 | +**ํ์ ๊ท์น:** |
| 35 | + |
| 36 | +- viewBox: ํซ ๋ณธ์ฒด + props(๋๋ํ, ์คํํด, ๋งํ์ ๋ฑ)๊ฐ ๋ชจ๋ ํฌํจ๋๋๋ก ์ค์ |
| 37 | +- ID ํจํด: `{pet-name}-*{id}-{emotion}-{part}` (์: `dessert-fox-*{id}-error-shadow`) |
| 38 | +- `*{id}`๋ ๋ฐํ์์ `animationId`๋ก ์นํ๋ฉ๋๋ค |
| 39 | +- ๊ฐ SVG๋ ๋
๋ฆฝ์ ์ผ๋ก ๋์ํ๋ ์์ ํ ์ ๋๋ฉ์ด์
์ด์ด์ผ ํฉ๋๋ค (style + ๊ตฌ์กฐ ํฌํจ) |
| 40 | + |
| 41 | +**SVG ๋ด๋ถ ๊ตฌ์กฐ:** |
| 42 | + |
| 43 | +```xml |
| 44 | +<svg xmlns="http://www.w3.org/2000/svg" viewBox="-15 -25 45 45"> |
| 45 | + <style> |
| 46 | + @keyframes {pet-name}-*{id}-{emotion}-shadow { ... } |
| 47 | + @keyframes {pet-name}-*{id}-{emotion}-obj { ... } |
| 48 | + /* ๊ธฐํ keyframes */ |
| 49 | + |
| 50 | + #{pet-name}-*{id}-{emotion}-shadow { animation: ... } |
| 51 | + #{pet-name}-*{id}-{emotion}-obj { animation: ... } |
| 52 | + </style> |
| 53 | + |
| 54 | + <!-- Shadow --> |
| 55 | + <g id="{pet-name}-*{id}-{emotion}-shadow" transform="translate(3, 7)">...</g> |
| 56 | + |
| 57 | + <!-- obj: legs + body + head --> |
| 58 | + <g id="{pet-name}-*{id}-{emotion}-obj"> |
| 59 | + <!-- Legs --> |
| 60 | + <g id="{pet-name}-*{id}-{emotion}-leg-left" transform="translate(5, 5)">...</g> |
| 61 | + <g id="{pet-name}-*{id}-{emotion}-leg-right" transform="translate(8, 5)">...</g> |
| 62 | + <!-- Body --> |
| 63 | + <g transform="translate(-2, 2)">...</g> |
| 64 | + <!-- Head --> |
| 65 | + <g transform="translate(0, -5)"> |
| 66 | + <g id="{pet-name}-*{id}-{emotion}-head-nod">...</g> |
| 67 | + </g> |
| 68 | + </g> |
| 69 | + |
| 70 | + <!-- Props (!, sparkles ๋ฑ โ obj ๋ฐ๊นฅ์ ๋ฐฐ์น) --> |
| 71 | + <g id="{pet-name}-*{id}-{emotion}-exclam" transform="translate(15, -13)">...</g> |
| 72 | +</svg> |
| 73 | +``` |
| 74 | + |
| 75 | +**emotion๋ณ ํน์ง:** |
| 76 | + |
| 77 | +| emotion | ํฌ์ฆ | ํน์ ํจ๊ณผ | |
| 78 | +|---------|------|-----------| |
| 79 | +| error | standing | X X ๋, ์ข์ฐ ํ๋ค๋ฆผ, ๋นจ๊ฐ ! ๊น๋นก์ | |
| 80 | +| happy | standing | ^^ ๋, ์ ํ ๋ฐ์ด์ค, ์คํํด | |
| 81 | +| idle-follow | standing | ์ผ๋ฐ ๋, ๋ถ๋๋ฌ์ด ๋ฐ์ด์ค | |
| 82 | +| notification | standing | ์ผ๋ฐ ๋, ๋๋ ์ ํ, ๋
ธ๋ ! ํ์ธ | |
| 83 | +| thinking | standing | ์ผ๋ฐ ๋, ๋งํ์ + ๋ก๋ฉ dots | |
| 84 | +| typing | **sitting** | ์ผ๋ฐ ๋, ๋
ธํธ๋ถ prop, ๊ณ ๊ฐ ๋๋์ | |
| 85 | + |
| 86 | +### 2. Svgs.kt์ SVG ๋ก๋ฉ ์ถ๊ฐ |
| 87 | + |
| 88 | +```kotlin |
| 89 | +// src/main/kotlin/org/gitanimals/core/Svgs.kt |
| 90 | + |
| 91 | +val {petName}ErrorEmotionSvg: String = ClassPathResource("persona/animal/emotion/{pet-short-name}/error.svg") |
| 92 | + .getContentAsString(Charset.defaultCharset()) |
| 93 | + |
| 94 | +val {petName}HappyEmotionSvg: String = ClassPathResource("persona/animal/emotion/{pet-short-name}/happy.svg") |
| 95 | + .getContentAsString(Charset.defaultCharset()) |
| 96 | + |
| 97 | +val {petName}IdleFollowEmotionSvg: String = ClassPathResource("persona/animal/emotion/{pet-short-name}/idle-follow.svg") |
| 98 | + .getContentAsString(Charset.defaultCharset()) |
| 99 | + |
| 100 | +val {petName}NotificationEmotionSvg: String = ClassPathResource("persona/animal/emotion/{pet-short-name}/notification.svg") |
| 101 | + .getContentAsString(Charset.defaultCharset()) |
| 102 | + |
| 103 | +val {petName}ThinkingEmotionSvg: String = ClassPathResource("persona/animal/emotion/{pet-short-name}/thinking.svg") |
| 104 | + .getContentAsString(Charset.defaultCharset()) |
| 105 | + |
| 106 | +val {petName}TypingEmotionSvg: String = ClassPathResource("persona/animal/emotion/{pet-short-name}/typing.svg") |
| 107 | + .getContentAsString(Charset.defaultCharset()) |
| 108 | +``` |
| 109 | + |
| 110 | +### 3. Base ํซ SVG ํ
ํ๋ฆฟ ์์ |
| 111 | + |
| 112 | +base SVG์ 3๊ฐ์ง ํ๋ ์ด์คํ๋๋ฅผ ์ถ๊ฐํฉ๋๋ค. |
| 113 | + |
| 114 | +**3-1. `*{emotion-style}` ํ๋ ์ด์คํ๋ ์ถ๊ฐ** |
| 115 | + |
| 116 | +`<style>` ๋ธ๋ก ์์ `*{emotion-style}`์ ์ถ๊ฐํฉ๋๋ค: |
| 117 | + |
| 118 | +```xml |
| 119 | +<style> |
| 120 | + *{act} |
| 121 | + *{emotion-style} |
| 122 | + |
| 123 | + @keyframes {pet-name}-*{id}-leg-left-move { ... } |
| 124 | + ... |
| 125 | +</style> |
| 126 | +``` |
| 127 | + |
| 128 | +**3-2. base ํซ ์ฝํ
์ธ ๋ฅผ `<g id="{pet-name}-*{id}-base">`๋ก ๊ฐ์ธ๊ธฐ** |
| 129 | + |
| 130 | +emotion ์ฌ์ ์ base๋ฅผ ์จ๊ธฐ๊ธฐ ์ํด wrapper ๊ทธ๋ฃน์ ์ถ๊ฐํฉ๋๋ค: |
| 131 | + |
| 132 | +```xml |
| 133 | +<svg width="600" height="300" viewBox="0 0 200 100" fill="none" overflow="visible"> |
| 134 | + <g id="{pet-name}-*{id}-base"> |
| 135 | + <!-- ๊ธฐ์กด shadow, obj, body, head ๋ฑ ๋ชจ๋ base ์ฝํ
์ธ --> |
| 136 | + </g> |
| 137 | + *{emotions} |
| 138 | +</svg> |
| 139 | +``` |
| 140 | + |
| 141 | +**3-3. `*{emotions}` ํ๋ ์ด์คํ๋ ์ถ๊ฐ** |
| 142 | + |
| 143 | +base ์ฝํ
์ธ `</g>` ๋ซํ ํ๊ทธ ๋ฐ๋ก ๋ค, `</svg>` ์์ ์ถ๊ฐํฉ๋๋ค. |
| 144 | + |
| 145 | +### 4. PersonaType.kt์์ loadSvg ๊ตฌํ |
| 146 | + |
| 147 | +`buildEmotionAnimation()` ๊ณตํต ๋ฉ์๋๋ฅผ ์ฌ์ฉํฉ๋๋ค. |
| 148 | + |
| 149 | +```kotlin |
| 150 | +{PET_NAME}(weight) { |
| 151 | + override fun loadSvg(name: String, animationId: Long, level: Long, mode: Mode): String { |
| 152 | + val emotion = buildEmotionAnimation( |
| 153 | + idPrefix = "{pet-name}", // base SVG์ ID prefix์ ๋์ผํด์ผ ํจ |
| 154 | + animationId = animationId, |
| 155 | + totalDuration = 180.0, // ์ ์ฒด ์ ๋๋ฉ์ด์
์ฌ์ดํด (์ด) |
| 156 | + emotionDuration = 3.0, // ๊ฐ emotion ์ฌ์ ์๊ฐ (์ด) |
| 157 | + emotionSvgs = listOf( |
| 158 | + {petName}ErrorEmotionSvg, // index 0 |
| 159 | + {petName}HappyEmotionSvg, // index 1 |
| 160 | + {petName}IdleFollowEmotionSvg, // index 2 |
| 161 | + {petName}NotificationEmotionSvg, // index 3 |
| 162 | + {petName}ThinkingEmotionSvg, // index 4 |
| 163 | + {petName}TypingEmotionSvg, // index 5 |
| 164 | + ), |
| 165 | + emotionYOffsets = listOf( |
| 166 | + 5.0, // error (standing) |
| 167 | + 5.0, // happy (standing) |
| 168 | + 5.0, // idle (standing) |
| 169 | + 5.0, // notif (standing) |
| 170 | + 5.0, // thinking (standing) |
| 171 | + 2.5, // typing (sitting โ ์์์์ผ๋ฏ๋ก Y ๋ณด์ ์ด ๋ค๋ฆ) |
| 172 | + ), |
| 173 | + minGap = 5.0, // emotion ์ฌ์ด ์ต์ ๊ฐ๊ฒฉ (์ด) |
| 174 | + maxGap = 30.0, // emotion ์ฌ์ด ์ต๋ ๊ฐ๊ฒฉ (์ด) |
| 175 | + ) |
| 176 | + |
| 177 | + return {petName}Svg |
| 178 | + .replace("*{act}", act(animationId)) |
| 179 | + .replace("*{emotion-style}", emotion.css) |
| 180 | + .replace("*{emotions}", emotion.content) |
| 181 | + .replace("*{id}", animationId.toString()) |
| 182 | + .replace("*{level}", level.toSvg(14.0, 2.0)) |
| 183 | + .replace("*{levelx}", (-3 + (-1 * (level.toString().length))).toString()) |
| 184 | + .replace("*{username}", name.toSvg(14.0, 25.0)) |
| 185 | + .replace("*{usernamex}", (23 + (-3 * name.length)).toString()) |
| 186 | + } |
| 187 | + |
| 188 | + override fun addEmotions(emotionType: PersonaEmotionType): String { |
| 189 | + return when (emotionType) { |
| 190 | + ERROR -> {petName}ErrorEmotionSvg |
| 191 | + HAPPY -> {petName}HappyEmotionSvg |
| 192 | + IDLE_FOLLOW -> {petName}IdleFollowEmotionSvg |
| 193 | + NOTIFICATION -> {petName}NotificationEmotionSvg |
| 194 | + THINKING -> {petName}ThinkingEmotionSvg |
| 195 | + TYPING -> {petName}TypingEmotionSvg |
| 196 | + } |
| 197 | + } |
| 198 | +} |
| 199 | +``` |
| 200 | + |
| 201 | +### 5. emotionYOffsets ๊ฒฐ์ ํ๊ธฐ |
| 202 | + |
| 203 | +`emotionYOffsets`๋ emotion SVG๋ฅผ base ํซ ์์น์ ์ ๋ ฌํ๊ธฐ ์ํ Y์ถ ๋ณด์ ๊ฐ์
๋๋ค. |
| 204 | +base SVG์ emotion SVG์ ์ขํ๊ณ๊ฐ ๋ค๋ฅด๊ธฐ ๋๋ฌธ์ ํ์ํ๋ฉฐ, ์ ํํ ๊ฐ์ ์๋ฒ๋ฅผ ์ง์ ๋์์ ํ์ธํ๋ฉฐ ์กฐ์ ํด์ผ ํฉ๋๋ค. |
| 205 | +Agent๊ฐ ์๋ ์๊ฑด์ ๋ฐ๋ผ์ ์ค์ค๋ก ์๋ฒ๋ฅผ ๋์ฐ๊ณ ํ
์คํธํ์ฌ ์ ํํ ๊ฐ์ ์กฐ์ ํ์ธ์. |
| 206 | + |
| 207 | +**์๋ฒ ๋์ฐ๊ธฐ:** |
| 208 | + |
| 209 | +```bash |
| 210 | +./gradlew bootRun --args="--spring.profiles.active=local" |
| 211 | +``` |
| 212 | + |
| 213 | +**ํ
์คํธ ๋ฐ์ดํฐ ์ค์ :** |
| 214 | + |
| 215 | +local MySQL์ ์ ์ํ์ฌ ํ
์คํธํ ํซ ํ์
์ผ๋ก ๋ณ๊ฒฝํฉ๋๋ค: |
| 216 | + |
| 217 | +```sql |
| 218 | +USE gitanimalsrender; |
| 219 | +UPDATE persona SET type = '{PET_NAME}'; |
| 220 | +``` |
| 221 | + |
| 222 | +**ํ์ธ:** |
| 223 | + |
| 224 | +๋ธ๋ผ์ฐ์ ์์ `localhost:8080/lines/{username}` ์ผ๋ก ์กฐํํ๋ฉด SVG๊ฐ ์๋ต๋ฉ๋๋ค. |
| 225 | +emotion์ด base ํซ๊ณผ ๋์ผํ ์์น์ ๋ํ๋๋๋ก `emotionYOffsets` ๊ฐ์ ์กฐ์ ํ์ธ์. |
| 226 | + |
| 227 | +- ๋๋ฒ๊น
์ `minGap`๊ณผ `maxGap`์ `1.0`์ผ๋ก ์ค์ ํ๋ฉด 1์ด๋ง๋ค emotion์ด ๋ํ๋์ ์์น ํ์ธ์ด ์ฌ์ |
| 228 | +- standing ํฌ์ฆ์ sitting ํฌ์ฆ๋ ๋ณด์ ๊ฐ์ด ๋ค๋ฅผ ์ ์์ (DESSERT_FOX ๊ธฐ์ค: standing `5.0`, sitting `2.5`) |
| 229 | + |
| 230 | +### 6. PersonaEmotionAssets ๊ตฌํํ๊ธฐ |
| 231 | + |
| 232 | +`src/main/kotlin/org/gitanimals/render/app/PersonaEmotionAssets.kt` ํ์ผ์ `PersonaEmotionAssets` sealed interface์ ์๋ก์ด ํซ์ ๊ตฌํ์ฒด๋ฅผ ์ถ๊ฐํด์ผ ํฉ๋๋ค. ์ด ์ธํฐํ์ด์ค๋ ํซ์ ๋ฉํ๋ฐ์ดํฐ์ ์ ๋๋ฉ์ด์
๋ค์ด๋ก๋ URL ์ ๋ณด๋ฅผ ํฌํจํฉ๋๋ค. |
| 233 | + |
| 234 | +**๊ตฌํ ์์ (DESSERT_FOX ์ฐธ๊ณ ):** |
| 235 | + |
| 236 | +```kotlin |
| 237 | +data object {PetClassName} : PersonaEmotionAssets { |
| 238 | + override val personaType: PersonaType = PersonaType.{PET_ENUM_NAME} |
| 239 | + override val name: String = "{Display Name}" |
| 240 | + override val author: String = "{Author Name}" |
| 241 | + override val description: String = "{Detailed Description}" |
| 242 | + |
| 243 | + // SVG ์ขํ๊ณ ์ค์ (๋ณดํต -15 -25 45 45 ์ฌ์ฉ) |
| 244 | + override val viewBox = ViewBox(x = -15, y = -25, width = 45, height = 45) |
| 245 | + |
| 246 | + // ๋ ์ด์์ ์ค์ |
| 247 | + override val layout = Layout( |
| 248 | + contentBox = Box(x = -2, y = -2, width = 20, height = 18), |
| 249 | + centerX = 7.5, |
| 250 | + baselineY = 15.0, |
| 251 | + visibleHeightRatio = 0.58, |
| 252 | + baselineBottomRatio = 0.05 |
| 253 | + ) |
| 254 | + |
| 255 | + // ๋ ์ถ์ (Eye Tracking) ์ค์ (๋ฏธ์ง์ ์ enabled = false) |
| 256 | + override val eyeTracking = EyeTracking( |
| 257 | + enabled = false, |
| 258 | + states = listOf("idle"), |
| 259 | + eyeRatioX = 0.52, |
| 260 | + eyeRatioY = 0.45, |
| 261 | + maxOffset = 1.5, |
| 262 | + bodyScale = 0.25, |
| 263 | + shadowStretch = 0.15, |
| 264 | + shadowShift = 0.3, |
| 265 | + ids = EyeTrackingIds(eyes = "eyes-js", body = "body-js", shadow = "shadow-js"), |
| 266 | + shadowOrigin = "7.5px 14px" |
| 267 | + ) |
| 268 | + |
| 269 | + // ์ ๋๋ฉ์ด์
ํ์ด๋ฐ ์ค์ (ms ๋จ์) |
| 270 | + override val timings = Timings( |
| 271 | + minDisplay = mapOf( |
| 272 | + "attention" to 4000, |
| 273 | + "error" to 5000, |
| 274 | + "notification" to 2500, |
| 275 | + "working" to 1000, |
| 276 | + "thinking" to 1000 |
| 277 | + ), |
| 278 | + autoReturn = mapOf( |
| 279 | + "attention" to 4000, |
| 280 | + "error" to 5000, |
| 281 | + "notification" to 2500 |
| 282 | + ), |
| 283 | + mouseIdleTimeout = 20000, |
| 284 | + mouseSleepTimeout = 60000, |
| 285 | + wakeDuration = 5000 |
| 286 | + ) |
| 287 | + |
| 288 | + // ํํธ๋ฐ์ค ์ค์ |
| 289 | + override val hitBoxes = HitBoxes( |
| 290 | + default = Box(x = -3, y = -8, width = 22, height = 20), |
| 291 | + sleeping = Box(x = -3, y = -5, width = 22, height = 18) |
| 292 | + ) |
| 293 | + |
| 294 | + override val sleepingHitboxFiles = listOf("sleeping.svg") |
| 295 | + override val miniMode = MiniMode(supported = false) |
| 296 | + |
| 297 | + // ์ค๋ธ์ ํธ ์ค์ผ์ผ ๋ณด์ |
| 298 | + override val objectScale = ObjectScale( |
| 299 | + widthRatio = 1.9, |
| 300 | + heightRatio = 1.3, |
| 301 | + offsetX = -0.45, |
| 302 | + offsetY = -0.25 |
| 303 | + ) |
| 304 | + |
| 305 | + // ์ค์ SVG ์ปจํ
์ธ ๋ฐํ ๋ก์ง ๊ตฌํ |
| 306 | + override fun getAsset(emotion: String): String { |
| 307 | + return when (emotion) { |
| 308 | + "error" -> {petName}ErrorEmotionSvg |
| 309 | + "happy" -> {petName}HappyEmotionSvg |
| 310 | + "idleFollow" -> {petName}IdleFollowEmotionSvg |
| 311 | + "notification" -> {petName}NotificationEmotionSvg |
| 312 | + "thinking" -> {petName}ThinkingEmotionSvg |
| 313 | + "typing" -> {petName}TypingEmotionSvg |
| 314 | + else -> throw IllegalArgumentException("Invalid emotion: $emotion") |
| 315 | + } |
| 316 | + } |
| 317 | +} |
| 318 | +``` |
| 319 | + |
| 320 | +**์ฐธ๊ณ ์ฌํญ:** |
| 321 | +- `error`, `happy`, `idleFollow` ๋ฑ์ ํ๋กํผํฐ๋ ์ธํฐํ์ด์ค์ ๊ธฐ๋ณธ ๊ตฌํ(`get()` ์ ๊ทผ์)์ ํตํด ์๋์ผ๋ก ๋ค์ด๋ก๋ URL (`/assets/images?...`)์ ๋ฐํํฉ๋๋ค. |
| 322 | +- `getAsset` ๋ฉ์๋๋ `AnimationController`์ `/assets/images` API์์ ์ค์ SVG ํ์ผ์ ์๋ตํ ๋ ์ฌ์ฉ๋ฉ๋๋ค. |
| 323 | +- ์๋ก์ด ํซ ๊ตฌํ์ฒด๋ฅผ ์ถ๊ฐํ ํ `companion object`์ `from` ๋ฉ์๋์์ ํด๋น ํซ์ด ์ฌ๋ฐ๋ฅด๊ฒ ์กฐํ๋๋์ง ํ์ธํ์ธ์ (Sealed interface์ `sealedSubclasses`๋ฅผ ์ฌ์ฉํ๋ฏ๋ก ์๋์ผ๋ก ํฌํจ๋ฉ๋๋ค). |
| 324 | + |
| 325 | +## buildEmotionAnimation ํ๋ผ๋ฏธํฐ |
| 326 | + |
| 327 | +| ํ๋ผ๋ฏธํฐ | ํ์
| ์ค๋ช
| |
| 328 | +|----------|------|------| |
| 329 | +| `idPrefix` | String | CSS ID prefix. base SVG์ `*{id}` ์ ๋ถ๋ถ๊ณผ ๋์ผํด์ผ ํจ | |
| 330 | +| `animationId` | Long | ํซ ์ธ์คํด์ค ๊ณ ์ ID. ID ์ถฉ๋ ๋ฐฉ์ง์ฉ | |
| 331 | +| `totalDuration` | Double | ์ ์ฒด ์ ๋๋ฉ์ด์
์ฌ์ดํด ๊ธธ์ด (์ด) | |
| 332 | +| `emotionDuration` | Double | ๊ฐ emotion์ด ํ์๋๋ ์๊ฐ (์ด) | |
| 333 | +| `emotionSvgs` | List\<String\> | emotion SVG ๋ฌธ์์ด ๋ฆฌ์คํธ | |
| 334 | +| `emotionYOffsets` | List\<Double\> | ๊ฐ emotion์ Y์ถ ๋ณด์ ๊ฐ. emotionSvgs์ ๊ฐ์ ์์ | |
| 335 | +| `minGap` | Double | emotion ์ฌ์ด ์ต์ ๊ฐ๊ฒฉ (์ด). ๊ธฐ๋ณธ๊ฐ 5.0 | |
| 336 | +| `maxGap` | Double | emotion ์ฌ์ด ์ต๋ ๊ฐ๊ฒฉ (์ด). ๊ธฐ๋ณธ๊ฐ 30.0 | |
| 337 | + |
| 338 | +## ๋์ ์๋ฆฌ |
| 339 | + |
| 340 | +1. `Random(animationId)` ์๋๋ก ๊ฒฐ์ ๋ก ์ ์ค์ผ์ค ์์ฑ |
| 341 | +2. base ํซ์ `opacity` ํ ๊ธ CSS ์ ์ฉ โ emotion ๊ตฌ๊ฐ์์ base ์จ๊น |
| 342 | +3. ๊ฐ emotion์๋ `opacity` ํ ๊ธ CSS ์ ์ฉ โ ํด๋น ๊ตฌ๊ฐ์์๋ง ํ์ |
| 343 | +4. `steps(1, end)` timing function์ผ๋ก ์ฆ์ ์ ํ (ํ์ด๋ ์์) |
| 344 | +5. emotion SVG ๋ด๋ถ์ ์์ฒด ์ ๋๋ฉ์ด์
(๋ฐ์ด์ค, ํ๋ค๋ฆผ ๋ฑ)์ ๋
๋ฆฝ์ ์ผ๋ก ๋์ |
| 345 | +6. `*{id}`๋ ์ต์ข
`.replace("*{id}", animationId.toString())`์์ ์ผ๊ด ์นํ |
| 346 | + |
| 347 | +## ๋๋ฒ๊น
ํ |
| 348 | + |
| 349 | +- `minGap`๊ณผ `maxGap`์ `1.0`์ผ๋ก ์ค์ ํ๋ฉด 1์ด๋ง๋ค emotion์ด ๋ํ๋จ |
| 350 | +- `localhost:8080/lines/{username}`์ผ๋ก ๊ฒฐ๊ณผ ํ์ธ |
| 351 | +- emotion์ด ์ ๋ณด์ด๋ฉด: CSS `animation` shorthand ๋์ longhand ์ฌ์ฉ ํ์ธ |
| 352 | +- ์์น๊ฐ ๋ง์ง ์์ผ๋ฉด: `emotionYOffsets` ๊ฐ ์กฐ์ |
| 353 | + |
| 354 | +## ์ฐธ๊ณ ๊ตฌํ |
| 355 | + |
| 356 | +DESSERT_FOX ๊ตฌํ์ ์ฐธ๊ณ ํ์ธ์: |
| 357 | +- base SVG: `src/main/resources/persona/animal/dessert-fox.svg` |
| 358 | +- emotion SVGs: `src/main/resources/persona/animal/emotion/fox/` |
| 359 | +- PersonaType: `PersonaType.kt` DESSERT_FOX ํญ๋ชฉ |
| 360 | +- SVG ๋ก๋ฉ: `Svgs.kt` dessertFox*EmotionSvg ๋ณ์๋ค |
0 commit comments