From 59b5d154235b8b36e214383010712c3e16db6fcf Mon Sep 17 00:00:00 2001 From: Benjythebee Date: Wed, 20 Nov 2024 09:48:52 +1300 Subject: [PATCH 1/4] add emotionManager --- src/components/Emotions.jsx | 77 ++++++++ src/components/Emotions.module.css | 110 +++++++++++ src/components/RightPanel.jsx | 15 +- src/components/RightPanel.module.css | 4 +- src/context/SceneContext.jsx | 20 +- src/images/emotion.png | Bin 0 -> 1540 bytes src/library/EmotionManager.js | 268 +++++++++++++++++++++++++++ src/library/characterManager.js | 25 ++- 8 files changed, 509 insertions(+), 10 deletions(-) create mode 100644 src/components/Emotions.jsx create mode 100644 src/components/Emotions.module.css create mode 100644 src/images/emotion.png create mode 100644 src/library/EmotionManager.js diff --git a/src/components/Emotions.jsx b/src/components/Emotions.jsx new file mode 100644 index 00000000..668f202c --- /dev/null +++ b/src/components/Emotions.jsx @@ -0,0 +1,77 @@ +import React, { useEffect } from "react" +import styles from "./Emotions.module.css" +import MenuTitle from "./MenuTitle" +import { SceneContext } from "../context/SceneContext"; +import Slider from "./Slider"; + +// import 'react-dropdown/style.css'; + +export default function Emotions(){ + + const { characterManager,moveCamera } = React.useContext(SceneContext) + + const [isConstant, setConstant] = React.useState(false) + const [intensity, setIntensity] = React.useState(1) + + const availableEmotions = characterManager.emotionManager.availableEmotions + + useEffect(() => { + moveCamera({ targetY:1.8, distance:2}) + }, []) + + const playEmotion = (emotion)=>{ + characterManager.emotionManager.playEmotion(emotion,undefined,isConstant,intensity) + } + + return ( + +
+
+ +
+
+ View different emotions +
+ +
+
+
+ + +
+
Constant Emotion
+ + +
+
+
+ Intensity: {parseFloat(intensity.toFixed(2))} +
+ + setIntensity(parseFloat(e.currentTarget.value.toString()))} min={0} max={1} step={0.01}/> +
+ + {availableEmotions.map((emotion, index) => { + return ( +
{ + playEmotion(emotion) + }}> +
{emotion}
+
+ ) + })} +
+
+
+ ) +} \ No newline at end of file diff --git a/src/components/Emotions.module.css b/src/components/Emotions.module.css new file mode 100644 index 00000000..d75dcd17 --- /dev/null +++ b/src/components/Emotions.module.css @@ -0,0 +1,110 @@ + +.InformationContainerPos { + position: fixed; + right: 132px; + top: 98px; + width:250px; + height: -webkit-calc(100vh - 176px); + height: calc(100vh - 176px); + backdrop-filter: blur(22.5px); + background: rgba(5, 11, 14, 0.8); + z-index: 1000; + user-select: none; + } + + .scrollContainer { + height: 100%; + width: 80%; + overflow-y: scroll; + position: relative; + overflow-x: hidden !important; + margin: 30px; + height: -webkit-calc(100% - 40px); + height: calc(100% - 40px); + } + .centerAlign{ + text-align: center; + } + .traitInfoTitle { + text-align: center; + color: white; + text-transform: uppercase; + text-shadow: 1px 1px 2px black; + font-size: 14px; + word-spacing: 2px; + margin-bottom: 10px; + } + + +.traitInfoText { + color: rgb(179, 179, 179); + /* text-transform: uppercase; */ + text-shadow: 1px 1px 2px black; + font-size: 14px; + word-spacing: 2px; + margin-bottom: 6px; + display: flex; + justify-content: left; + } + +/* Hide the default checkbox */ +.custom-checkbox input[type="checkbox"] { + display: none; + } + + /* Style the custom checkbox */ + .custom-checkbox .checkbox-container { + display: inline-block; + width: 20px; + height: 20px; + border: 2px solid #284b39; /* Change border color as needed */ + border-radius: 5px; + cursor: pointer; + } + + .custom-checkbox .checkbox-container.checked { + background-color: #5eb086; /* Change background color when checked */ + } + + .custom-checkbox .checkbox-container .checkmark { + display: none; + } + + /* Style the checkmark when the checkbox is checked */ + .custom-checkbox input[type="checkbox"]:checked + .checkbox-container { + background-color: #5eb086; /* Change background color when checked */ + } + + .custom-checkbox input[type="checkbox"]:checked + .checkbox-container .checkmark { + display: block; + } + +.checkboxHolder { + display: flex; + gap: 5px; + align-items: center; + justify-content: left; + height: 40px; + } + + + + .actionButton{ + margin: 10px auto; + text-align: center; + outline-color: #3b434f; + color: #d1d7df; + outline-width: 2px; + outline-style: solid; + background-color: #1e2530; + height: 30px; + width: 80%; + font-family: "TTSC-Bold"; + text-transform: uppercase !important; + font-size:x-small; + display: flex; + justify-content: center; + align-items : center; + user-select: none; + cursor: pointer; + } \ No newline at end of file diff --git a/src/components/RightPanel.jsx b/src/components/RightPanel.jsx index ddba3d74..216781cd 100644 --- a/src/components/RightPanel.jsx +++ b/src/components/RightPanel.jsx @@ -1,8 +1,9 @@ -import React, { useContext, useState, useEffect } from "react" +import React from "react" import styles from "./RightPanel.module.css" import MenuTitle from "./MenuTitle" import traitsIcon from "../images/t-shirt.png" import genSpriteIcon from "../images/users.png" +import emotionIcon from "../images/emotion.png" import genLoraIcon from "../images/paste.png" import genThumbIcon from "../images/portraits.png" import { TokenBox } from "../components/token-box/TokenBox" @@ -10,6 +11,7 @@ import TraitInformation from "../components/TraitInformation" import LoraCreation from "./LoraCreation" import SpriteCreation from "./SpriteCreation" import ThumbnailCreation from "./ThumbnailCreation" +import Emotions from "./Emotions" export default function RightPanel({selectedTrait, selectedVRM, traitGroupName}){ const [selectedOption, setSelectedOption] = React.useState("") @@ -27,6 +29,7 @@ export default function RightPanel({selectedTrait, selectedVRM, traitGroupName}) {selectedOption=="LoraCreation" && } {selectedOption=="SpriteCreation" && } {selectedOption=="ThumbnailCreation" && } + {selectedOption=="EmotionManager" && }
@@ -71,6 +74,16 @@ export default function RightPanel({selectedTrait, selectedVRM, traitGroupName}) rarity={selectedOption == "ThumbnailCreation" ? "mythic" : "none"} />
+ {selectedTrait &&
{setSelectedOptionString("EmotionManager")}} + > + +
}
diff --git a/src/components/RightPanel.module.css b/src/components/RightPanel.module.css index 9b6b7966..37bb5b58 100644 --- a/src/components/RightPanel.module.css +++ b/src/components/RightPanel.module.css @@ -4,7 +4,7 @@ right: 32px; top: 98px; width:90px; - height: 270px; + height: auto; backdrop-filter: blur(22.5px); background: rgba(5, 11, 14, 0.8); z-index: 1000; @@ -19,7 +19,7 @@ position: relative; overflow-x: hidden !important; margin: 16px; - height: 270px; + height: 280px; } .options-container { user-select: none; diff --git a/src/context/SceneContext.jsx b/src/context/SceneContext.jsx index 7efa737f..0bf549d3 100644 --- a/src/context/SceneContext.jsx +++ b/src/context/SceneContext.jsx @@ -1,16 +1,30 @@ import React, { createContext, useEffect, useState } from "react" import gsap from "gsap" -import { local } from "../library/store" import { sceneInitializer } from "../library/sceneInitializer" import { LoraDataGenerator } from "../library/loraDataGenerator" import { SpriteAtlasGenerator } from "../library/spriteAtlasGenerator" import { ThumbnailGenerator } from "../library/thumbnailsGenerator" -export const SceneContext = createContext() +export const SceneContext = createContext({ + /** + * @typedef {import('../library/characterManager').CharacterManager} CharacterManager + * @type {CharacterManager} + */ + characterManager: null, + /** + * @typedef {Object} MoveCameraParam + * @property {number} targetX + * @property {number} targetY + * @property {number} targetZ + * @property {number} distance + * @param {MoveCameraParam} _value + */ + // eslint-disable-next-line no-unused-vars + moveCamera: (_value) => {}, +}) export const SceneProvider = (props) => { - const [characterManager, setCharacterManager] = useState(null) const [loraDataGenerator, setLoraDataGenerator] = useState(null) const [spriteAtlasGenerator, setSpriteAtlasGenerator] = useState(null) diff --git a/src/images/emotion.png b/src/images/emotion.png new file mode 100644 index 0000000000000000000000000000000000000000..f6c023fcbcc3b9f2515514a44b3a78692a813b9b GIT binary patch literal 1540 zcmaKs`(Mj@9LGQ3ZMv*Ot&28=BIFdN5{}fW4O4Vc=t#yRbevLBq1(Pok`r17=P0R7 zAyI}p>2@|9S(laRwr;CUO|2~rYuT8c`2)^*KOV35^YMN^-jCPg_0#)O;O_17(TmUk zfcfMd#9%$)?=nK_yHb>=)&n{nyxj*HXmzIQ2jjRMVW$B=KKU+a_WU$izpM)M4hom1rejFJUeg6DeqtQ%GPBwD8)oQgup_rYWWxp0r zO-z8QdwYk5h6Y9@kK3er z|Nj2|=JxL4;bE;-%kS#>`t|F?#DrKZKFeaZb#@Dd!jX}Y!NI|qnHf%Nk4z@(>+2gI zAFt>3%H?vANFvMY~1n=GhbUFm2cm3b`kAqt-7kdCeG08;lkh6u;1z!Rz z*PC%`hM8q$*vnW2nc!f6@}??u0jZQ!;=Gbq*i<`DBg|;gnfR2Nnlv6S>0aYjpGvRc zq?%`+wBhx_nERn*@8G^u8+&u!iycT-d)-#I;}@YIM)tF_Mp#0#$`tpC0zyQQ{^L!cF!5uW`q^d3tb>}0`|yaNb>+UZ0y7k;C>fDyW|@!_00YE@A{H1&p<7Qw zFHvY78kC~YEv+CE)z+E~MWRv+7<{%NoJ~;I%|UcCIR=={L;wUrA{#b^16Zp-x$Z5* zGQR$JW(f$nRsdi>242vsthr}%;=&c-(o*pm)BsBgC_)S=*+4~g!2}c@NXBLOEWmVe3MUb^*Y-<2ReM4|6bc8;L#oP zeZeY9^3;|EEpirK*gE}w>J~725kv`zyk}8t%pO4zA3bN_IFm3t{g~gphXFC`C1IMC zz~!Com!_@>sOpi)K7Tliu)N+w`$yyJ-#h5vf|k8)3s1AKOMR1FQ9N>|_O4YOGZxCM zD0~I%NWTFGO-H>yI}<%0uktRP0cI`^9BF{DTXFQ=54ps{|EO}N0zpZ|I%{W_CuGYy zP-YI{^Lo7;GdkVZ10vcms!(X>*4cO*$vJ}Myzi!dTg*HqUSllMox#9Wb9vRnYk6Ej zM7mJMI9&g!=HeR#wc&G?W27>bgQw2s_DVM+OtiL08zVoly71!4;8HaqAe(MFIPl_elM7)#hht39#TlU93jzbCCa{^y@)jY;9|w8rEJciJ;5BNB1kkQ8`ibb;J{ z^$jpcvM9hV5v36&7|xalO_0ZJN=oYyVEN@m$-@tKJTq6tdmVNIFAoGos=d0dx|U2Y Sn*OGLdVoyYO|0~xWc&+{2?TNg literal 0 HcmV?d00001 diff --git a/src/library/EmotionManager.js b/src/library/EmotionManager.js new file mode 100644 index 00000000..18b063a2 --- /dev/null +++ b/src/library/EmotionManager.js @@ -0,0 +1,268 @@ +import { VRMExpressionPresetName } from "@pixiv/three-vrm"; +import { Clock } from "three"; + +/** + * @typedef {import('@pixiv/three-vrm').VRMExpressionPresetName} VRMExpressionPresetName + * @typedef {import('@pixiv/three-vrm').VRM} VRM + * + */ + +export class EmotionManager { + /** + * @type {VRM[]} + */ + vrmEmotion + /** + * @type {'ready'|'animating'|'stopping'|'transition'} + */ + mode + /** + * @type {Clock} + */ + clock + continuous = false; + /** + * @type {VRMExpressionPresetName|null} + */ + emotionPlaying =null; + /** + * @type {number} + */ + emotionValue = 0; + /** + * @type {number} + */ + intensity = 1; + /** + * Time for the emotion to go from 0 to 1 (divide by two if you want fast in and out) + * @type {number} + */ + emotionTime = 0.6; + /** + * @type {boolean} + */ + isTakingScreenShot = false; + + /** + * For transitioning to the next emotion from the current one; + * @type {VRMExpressionPresetName|null} + */ + _nextEmotion = null; + /** + * @type {number} + */ + _nextEmotionTime = 0; + /** + * @type {number} + */ + _nextEmotionValue = 0; + /** + * @type {number} + */ + _nextIntensity = 1; + _nextIsContinuous = false; + + constructor( ) { + this.vrmEmotion = []; + this.mode = 'ready'; + + this.clock = new Clock(); + + this.isTakingScreenShot = false; + + this.update() + } + + get availableEmotions(){ + const keys = Object.keys(VRMExpressionPresetName).map((t)=>t.toLowerCase()) + const available= [] + for(const vrm of this.vrmEmotion){ + for(const key of keys){ + if(key==='blink') continue + if(available.includes(key)) continue + const express =vrm.expressionManager?.getExpression(key) + + if(express && express._binds.length > 0){ + available.push(key) + } + } + } + return available + } + + /** + * @param {VRM} vrm + */ + addVRM(vrm){ + if(!vrm.expressionManager) return + this.vrmEmotion.push(vrm) + } + /** + * + * @param {'aa'|'ee'|'ii'|'oo'|'uu'|'blink'|'joy'|'angry'|'sorrow'|'fun'|'lookUp'|'lookDown'|'lookLeft'|'lookRight'} emotion + */ + hasEmotion(emotion){ + return this.availableEmotions.some(emo => emo === emotion) + } + + /** + * @param {VRM} vrm + */ + removeVRM(vrm) { + const index = this.vrmEmotion.indexOf(vrm); + + if (index !== -1) { + this.vrmEmotion.splice(index, 1); + } + } + + enableScreenshot() { + this.isTakingScreenShot = true; + this.emotionPlaying = null; + this._updateEmotions(); + } + + disableScreenshot() { + this.isTakingScreenShot = false; + } + + _isBlink(emotion){ + return emotion === 'blink' + } + /** + * + * @param {'aa'|'ee'|'ii'|'oo'|'uu'|'blink'|'joy'|'angry'|'sorrow'|'fun'|'lookUp'|'lookDown'|'lookLeft'|'lookRight'} emotion + * @param {number} [time] + * @param {boolean} [continuous] + * @param {number} [intensity] + */ + playEmotion(emotion, time=undefined, continuous=false, intensity = 1){ + if (!this.hasEmotion(emotion)) { + console.warn(`Emotion ${emotion} not available`) + return + } + if(this._isBlink(emotion)){ + console.warn(`Blink is handled by the BlinkManager, ignoring`) + return + } + if(emotion === this.emotionPlaying){ + if(intensity === this.intensity){ + return + } + } + const intensity_ = Math.min(1,Math.max(0,intensity)) + if(this.mode === 'animating' && this.emotionPlaying){ + this.continuous = false; + // transition to the next emotion + this.nextEmotion = emotion + this._nextEmotionTime = time || this.emotionTime + this._nextEmotionValue = 0 + this._nextIntensity = intensity_ + this._nextIsContinuous = continuous || false + this.mode = 'transition' + return + } + + this.emotionPlaying = emotion + this.intensity = intensity_ + if(time){ + this.emotionTime = time + } + this.continuous = continuous || false + this.mode = 'animating' + } + + _setIsReady(){ + this.emotionValue = 0 + this.intensity = 1 + this.emotionPlaying = null + this.continuous = false + this.mode = 'ready' + } + + _removeNextEmotion(){ + this.nextEmotion = null + this._nextIntensity = 1 + this._nextEmotionValue = 0 + this._nextEmotionTime = 0 + this._nextIsContinuous = false + } + + update(){ + setInterval(() => { + if (this.isTakingScreenShot) { + return; + } + const deltaTime = this.clock.getDelta() + switch (this.mode){ + + case 'animating': + if ( this.emotionPlaying){ + if(this.emotionValue < this.intensity){ + this.emotionValue += deltaTime / this.emotionTime; + this.emotionValue = Math.min(1,this.emotionValue) + } + + if(!this.continuous && this.emotionValue >= this.intensity){ + this.mode = 'stopping' + } + + }else{ + this._setIsReady() + } + this._updateEmotions(); + break; + case 'stopping': + if ( this.emotionPlaying){ + if(this.emotionValue>0){ + this.emotionValue -= deltaTime / this.emotionTime; + this.emotionValue = Math.max(0,this.emotionValue) + } + if(this.emotionValue <= 0){ + this._setIsReady() + } + }else{ + this._setIsReady() + } + this._updateEmotions(); + break; + case 'transition': + if(this._nextEmotion){ + if(this._nextEmotionValue < this._nextIntensity){ + this._nextEmotionValue += deltaTime / this._nextEmotionTime; + this.emotionValue = Math.min(this._nextIntensity,this.emotionValue) + } + + if(this.emotionValue > 0){ + this.emotionValue -=deltaTime / this._nextEmotionTime + }else{ + this.emotionValue = this._nextEmotionValue + this.emotionTime = this._nextEmotionTime + this.emotionPlaying = this._nextEmotion + this.intensity = this._nextIntensity + this.continuous = this._nextIsContinuous + this.mode = 'animating' + this._removeNextEmotion() + } + + }else{ + if(this.emotionPlaying){ + this.mode = 'animating' + } + } + this._updateEmotions(); + } + }, 1000/30); + } + + _updateEmotions(){ + if(!this.emotionPlaying) return + this.vrmEmotion.forEach(vrm => { + if(this._nextEmotion){ + vrm.expressionManager?.setValue(this._nextEmotion, this._nextEmotionValue) + } + vrm.expressionManager?.setValue(this.emotionPlaying, this.emotionValue) + vrm.expressionManager?.update() + }); + } +} diff --git a/src/library/characterManager.js b/src/library/characterManager.js index 736668e0..3da23788 100644 --- a/src/library/characterManager.js +++ b/src/library/characterManager.js @@ -3,7 +3,7 @@ import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader" import { AnimationManager } from "./animationManager" import { ScreenshotManager } from "./screenshotManager"; import { BlinkManager } from "./blinkManager"; - +import { EmotionManager } from "./EmotionManager"; import { VRMLoaderPlugin, VRMSpringBoneCollider } from "@pixiv/three-vrm"; import { getAsArray, disposeVRM, renameVRMBones, addModelData } from "./utils"; import { downloadGLB, downloadVRMWithAvatar } from "../library/download-utils" @@ -12,12 +12,27 @@ import { cullHiddenMeshes, setTextureToChildMeshes, addChildAtFirst } from "./ut import { LipSync } from "./lipsync"; import { LookAtManager } from "./lookatManager"; import { CharacterManifestData } from "./CharacterManifestData"; - const mouse = new THREE.Vector2(); const raycaster = new THREE.Raycaster(); const localVector3 = new THREE.Vector3(); export class CharacterManager { + /** + * @type {EmotionManager} + */ + emotionManager = null; + /** + * @type {AnimationManager} + */ + animationManager = null; + /** + * @type {BlinkManager} + */ + blinkManager + /** + * @type {ScreenshotManager} + */ + screenshotManager constructor(options){ this._start(options); } @@ -46,7 +61,7 @@ export class CharacterManager { this.animationManager = new AnimationManager(); this.screenshotManager = new ScreenshotManager(this, parentModel || this.rootModel); this.blinkManager = new BlinkManager(0.1, 0.1, 0.5, 5) - + this.emotionManager = new EmotionManager(); this.rootModel.add(this.characterModel) this.renderCamera = renderCamera; @@ -1356,6 +1371,7 @@ export class CharacterManager { _applyManagers(vrm){ this.blinkManager.addVRM(vrm) + this.emotionManager.addVRM(vrm) if (this.lookAtManager) this.lookAtManager.addVRM(vrm); @@ -1407,7 +1423,8 @@ export class CharacterManager { _disposeTrait(vrm){ this.blinkManager.removeVRM(vrm) - + this.emotionManager.removeVRM(vrm) + if (this.lookAtManager) this.lookAtManager.removeVRM(vrm); From 95be7ddafc1a3f07490745b3c7cc02c0d3ee0361 Mon Sep 17 00:00:00 2001 From: Benjythebee Date: Wed, 20 Nov 2024 09:54:53 +1300 Subject: [PATCH 2/4] fix --- src/library/EmotionManager.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/library/EmotionManager.js b/src/library/EmotionManager.js index 18b063a2..fb6794ef 100644 --- a/src/library/EmotionManager.js +++ b/src/library/EmotionManager.js @@ -154,7 +154,7 @@ export class EmotionManager { if(this.mode === 'animating' && this.emotionPlaying){ this.continuous = false; // transition to the next emotion - this.nextEmotion = emotion + this._nextEmotion = emotion this._nextEmotionTime = time || this.emotionTime this._nextEmotionValue = 0 this._nextIntensity = intensity_ @@ -181,7 +181,7 @@ export class EmotionManager { } _removeNextEmotion(){ - this.nextEmotion = null + this._nextEmotion = null this._nextIntensity = 1 this._nextEmotionValue = 0 this._nextEmotionTime = 0 @@ -230,9 +230,8 @@ export class EmotionManager { if(this._nextEmotion){ if(this._nextEmotionValue < this._nextIntensity){ this._nextEmotionValue += deltaTime / this._nextEmotionTime; - this.emotionValue = Math.min(this._nextIntensity,this.emotionValue) + this.emotionValue = Math.min(this.intensity,this.emotionValue) } - if(this.emotionValue > 0){ this.emotionValue -=deltaTime / this._nextEmotionTime }else{ From 9e1863153d0c94d03d88bf4d5077a755df0280c9 Mon Sep 17 00:00:00 2001 From: Benjythebee Date: Wed, 20 Nov 2024 10:00:52 +1300 Subject: [PATCH 3/4] remove conditional rendering --- src/components/RightPanel.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/RightPanel.jsx b/src/components/RightPanel.jsx index 216781cd..8c2fd19f 100644 --- a/src/components/RightPanel.jsx +++ b/src/components/RightPanel.jsx @@ -29,7 +29,7 @@ export default function RightPanel({selectedTrait, selectedVRM, traitGroupName}) {selectedOption=="LoraCreation" && } {selectedOption=="SpriteCreation" && } {selectedOption=="ThumbnailCreation" && } - {selectedOption=="EmotionManager" && } + {selectedOption=="EmotionManager" && }
@@ -74,7 +74,7 @@ export default function RightPanel({selectedTrait, selectedVRM, traitGroupName}) rarity={selectedOption == "ThumbnailCreation" ? "mythic" : "none"} />
- {selectedTrait &&
{setSelectedOptionString("EmotionManager")}} > @@ -83,7 +83,7 @@ export default function RightPanel({selectedTrait, selectedVRM, traitGroupName}) icon={emotionIcon} rarity={selectedOption == "EmotionManager" ? "mythic" : "none"} /> -
} +
From a36caf4a42930281bf364cab19476150c1894f60 Mon Sep 17 00:00:00 2001 From: memelotsqui Date: Wed, 27 Nov 2024 15:46:22 -0600 Subject: [PATCH 4/4] minor changes, set higher css, faster default transition and updated thumbnail --- src/components/RightPanel.module.css | 2 +- src/images/emotion.png | Bin 1540 -> 4019 bytes src/library/EmotionManager.js | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/RightPanel.module.css b/src/components/RightPanel.module.css index 37bb5b58..9cf115b0 100644 --- a/src/components/RightPanel.module.css +++ b/src/components/RightPanel.module.css @@ -19,7 +19,7 @@ position: relative; overflow-x: hidden !important; margin: 16px; - height: 280px; + height: 300px; } .options-container { user-select: none; diff --git a/src/images/emotion.png b/src/images/emotion.png index f6c023fcbcc3b9f2515514a44b3a78692a813b9b..fabe71135aaba827ad9ec1873b1a045974e9bdaf 100644 GIT binary patch literal 4019 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 delta 1514 zcmaJ>dtA$V0R8^9>FKJew5e2tx}j0Rm0FK+Q>2&26_!sP*DWO#di{ROBsa7!?j_Nt zRMKO}ol1A(%37^Vul3qXO|3=4S~h04`~Usi^UwKw&gY!-Ie(o;j?}p{ybT%vpq3VD zX9+qPu_XW+;H`iQoo!)Izaun?G%JTQ(*6HP;BvV%8qHuZ)U`-Q6e_`x;^W7UVu|$g z=g&HwZhU;ancJ<^YE>%L^z=0Qm26^SqJrI_(P;Xm6MR9RLZOJJQ`Ks z|HXe8;C}-**PQPG0K*6l@{c-Q{I2LrnDuH)E^C-kS&6%dQw0-Uj4=MN$`F=G&J3DU z`VyDv&>fCK67{!?JPC_p*W_&YHhB0g90V<}Amj}gRGK}Z~wlR`N z)HcpQG|NdWu$bBpP$;=_*a8V-E(cYHHxSeGYTeW#5Ot*pAR#cFAnaAM%I%JwyCiZf zlP!j#hPXwCuUBlYEe;n;vpp)=_!KE(E055s2YyJ9Am7NTEZi z;MVOkSw<(9tPhBI>|r0dwuC8Ho_JruZdrBK-cA{ZJ4T`?n^4Vh1R#@vvR7=0UJ^BdDAY5ej?#Tyr|TSA!rk&VUdFz+D|iz=&%F~xwwr-8OF8QHMd*}LGmSf^ zK3(jN+_>Z;@SU!XNA`>ryz*IzrF&`!UGn9JAo>UD0L*<2Og?F&zt*}J2o|3Xbp=nh z{yDw$>Z%1Clt^&#!iBly!eer;4oLwTSgpV(Jf-g}GIoFJIRko*uw(o)^x6Y4yie^G zk5NqQ8*tRwzNuSz6i^%-X;^+u>ccCYw$jaF&4_Xzs?{LxauDuUmK?YF5#&j~-4N3B zsycW@h2NqZ*%+iS{!F6QI}_b#?S>%=wSocL!?ByUwqUkovn~r0S3~^W5A2=tiF6x* z^OLl7^O4lZUy^;Bjf%<@hePmHKt|}kclKOHnSL_g1uvqiOF>=)j<}xEaBQb9rQ!H) zU&`a;pMIb;r^R?vn$!1tQy#(eIMfm2t)v|XCG#eo-L8Q_l2s9Iku)nvj^%8;_Z;%M zMb6+I1lC`kmEQkw$CtW1*)P@$yx1Ear}gW)>{