[READ-ONLY] Mirror of https://github.com/flo-bit/tiny-creature. three.js journey #16 - tamagotchi flo-bit.dev/tiny-creature/
0

Configure Feed

Select the types of activity you want to include in your feed.

add some emotions

Florian (Feb 9, 2025, 2:50 AM +0100) 9eab8a73 b95902c8

+571 -204
+53 -26
src/lib/Creature.svelte
··· 39 39 let mouse = new THREE.Vector2(); 40 40 41 41 function jump() { 42 - ySpeed = 5; 42 + if (rootBone?.position.y <= -1) ySpeed = 5; 43 43 } 44 44 45 45 let primaryColors: THREE.TSL.ShaderNodeObject<THREE.UniformNode<THREE.Color>>[] = []; ··· 66 66 67 67 let total = 12; 68 68 for (let i = 0; i < total; i++) { 69 - const material = new THREE.MeshStandardNodeMaterial(); 69 + const material = new THREE.MeshStandardNodeMaterial({ 70 + side: THREE.DoubleSide 71 + }); 70 72 71 73 // move vertices along normal to make each shell slightly bigger than the last 72 74 material.positionNode = positionGeometry ··· 166 168 onMount(async () => { 167 169 addEventListeners(); 168 170 169 - await createTextures(); 171 + await createTextures(); 170 172 171 173 const loader = new GLTFLoader(); 172 174 ··· 201 203 rootBone.position.y = Math.max(rootBone.position.y, -1) + position[1]; 202 204 rootBone.position.z = position[2]; 203 205 204 - ySpeed -= 10 * dt; 206 + ySpeed -= 20 * dt; 205 207 } 206 208 207 209 if (!face) return; ··· 217 219 if (options.hasChanged) { 218 220 console.log('updating creature'); 219 221 220 - face.eyebrowLeft.options.set('curve', options.leftEyebrow.curve); 221 - face.eyebrowLeft.options.set('stroke', options.leftEyebrow.stroke); 222 - face.eyebrowLeft.options.set('width', options.leftEyebrow.width); 223 - face.eyebrowLeft.options.set('x', options.leftEyebrow.x); 224 - face.eyebrowLeft.options.set('y', options.leftEyebrow.y); 225 - face.eyebrowLeft.options.set('angle', options.leftEyebrow.angle); 222 + if (options.currentEmotion) { 223 + const emotion: Record<string, any> = options.emotions[options.currentEmotion]; 224 + 225 + for (const key in emotion) { 226 + const option = emotion[key]; 227 + 228 + for (const optionKey in option) { 229 + const optionValue = option[optionKey]; 230 + 231 + options[key][optionKey] = optionValue; 232 + } 233 + } 234 + options.currentEmotion = ''; 235 + } 236 + 237 + const colorEyebrowLeft = new THREE.Color(options.leftEyebrow.color); 238 + face.eyebrowLeft.options.setMultiple({ 239 + ...options.leftEyebrow, 240 + colorRed: colorEyebrowLeft.r * 256, 241 + colorGreen: colorEyebrowLeft.g * 256, 242 + colorBlue: colorEyebrowLeft.b * 256 243 + }); 244 + 245 + const colorEyebrowRight = new THREE.Color(options.rightEyebrow.color); 246 + face.eyebrowRight.options.setMultiple({ 247 + ...options.rightEyebrow, 248 + colorRed: colorEyebrowRight.r * 256, 249 + colorGreen: colorEyebrowRight.g * 256, 250 + colorBlue: colorEyebrowRight.b * 256 251 + }); 226 252 227 - face.eyebrowRight.options.set('curve', options.rightEyebrow.curve); 228 - face.eyebrowRight.options.set('stroke', options.rightEyebrow.stroke); 229 - face.eyebrowRight.options.set('width', options.rightEyebrow.width); 230 - face.eyebrowRight.options.set('x', options.rightEyebrow.x); 231 - face.eyebrowRight.options.set('y', options.rightEyebrow.y); 232 - face.eyebrowRight.options.set('angle', options.rightEyebrow.angle); 253 + const colorMouth = new THREE.Color(options.mouth.color); 254 + face.mouth.options.setMultiple({ 255 + ...options.mouth, 256 + colorRed: colorMouth.r * 256, 257 + colorGreen: colorMouth.g * 256, 258 + colorBlue: colorMouth.b * 256 259 + }); 260 + 261 + face.leftEye.options.setMultiple({ 262 + ...options.leftEye, 263 + }); 233 264 234 - face.mouth.options.set('curve', options.mouth.curve); 235 - face.mouth.options.set('stroke', options.mouth.stroke); 236 - face.mouth.options.set('width', options.mouth.width); 237 - face.mouth.options.set('x', options.mouth.x); 238 - face.mouth.options.set('y', options.mouth.y); 265 + face.rightEye.options.setMultiple({ 266 + ...options.rightEye, 267 + }); 239 268 240 - face.furMouth.options.set('curve', options.mouth.curve); 241 - face.furMouth.options.set('stroke', options.mouth.stroke); 242 - face.furMouth.options.set('width', options.mouth.width); 243 - face.furMouth.options.set('x', options.mouth.x); 244 - face.furMouth.options.set('y', options.mouth.y); 269 + face.furMouth.options.setMultiple({ 270 + ...options.mouth, 271 + }); 245 272 246 273 face.updateBackground(options.colors.primary); 247 274
+150 -29
src/lib/Pane.svelte
··· 5 5 Folder, 6 6 Color, 7 7 ThemeUtils, 8 + Button, 9 + Text, 10 + Separator 8 11 } from 'svelte-tweakpane-ui'; 9 12 import { options } from './state.svelte'; 13 + import { onMount } from 'svelte'; 10 14 11 15 function change() { 12 16 options.hasChanged = true; 13 17 } 18 + 19 + function saveEmotions() { 20 + localStorage.setItem('emotions', JSON.stringify(options.emotions)); 21 + } 22 + 23 + onMount(() => { 24 + const emotions = localStorage.getItem('emotions'); 25 + if (emotions) { 26 + options.emotions = { 27 + ...options.emotions, 28 + ...JSON.parse(emotions) 29 + }; 30 + 31 + console.log(emotions) 32 + } 33 + }); 34 + 35 + let newEmotion = ''; 14 36 </script> 15 37 16 38 <div ··· 21 43 <Slider 22 44 label="curve" 23 45 bind:value={options.leftEyebrow.curve} 24 - min={-40} 25 - max={40} 46 + min={-100} 47 + max={100} 26 48 on:change={change} 27 49 /> 28 50 <Slider 29 51 label="stroke" 30 52 bind:value={options.leftEyebrow.stroke} 31 53 min={1} 32 - max={10} 54 + max={30} 33 55 on:change={change} 34 56 /> 35 57 <Slider ··· 39 61 max={200} 40 62 on:change={change} 41 63 /> 42 - <Slider 43 - label="x" 44 - bind:value={options.leftEyebrow.x} 45 - min={-100} 46 - max={100} 47 - on:change={change} 48 - /> 64 + <Slider label="x" bind:value={options.leftEyebrow.x} min={0} max={200} on:change={change} /> 49 65 <Slider 50 66 label="y" 51 67 bind:value={options.leftEyebrow.y} 52 68 min={-100} 53 - max={100} 69 + max={200} 54 70 on:change={change} 55 71 /> 56 72 ··· 61 77 max={100} 62 78 on:change={change} 63 79 /> 80 + 81 + <Slider 82 + label="sharpness" 83 + bind:value={options.leftEyebrow.sharpness} 84 + min={0} 85 + max={1} 86 + step={1} 87 + on:change={change} 88 + /> 89 + <Color label="color" bind:value={options.leftEyebrow.color} on:change={change} /> 64 90 </Folder> 65 91 <Folder expanded={false} title="Right Eyebrow"> 66 92 <Slider 67 93 label="curve" 68 94 bind:value={options.rightEyebrow.curve} 69 - min={-40} 70 - max={40} 95 + min={-100} 96 + max={100} 71 97 on:change={change} 72 98 /> 73 99 <Slider 74 100 label="stroke" 75 101 bind:value={options.rightEyebrow.stroke} 76 102 min={1} 77 - max={10} 103 + max={30} 78 104 on:change={change} 79 105 /> 80 106 <Slider ··· 84 110 max={200} 85 111 on:change={change} 86 112 /> 113 + <Slider label="x" bind:value={options.rightEyebrow.x} min={-200} max={0} on:change={change} /> 87 114 <Slider 88 - label="x" 89 - bind:value={options.rightEyebrow.x} 115 + label="y" 116 + bind:value={options.rightEyebrow.y} 117 + min={-100} 118 + max={200} 119 + on:change={change} 120 + /> 121 + <Slider 122 + label="angle" 123 + bind:value={options.rightEyebrow.angle} 90 124 min={-100} 91 125 max={100} 92 126 on:change={change} 93 127 /> 94 128 <Slider 95 - label="y" 96 - bind:value={options.rightEyebrow.y} 97 - min={-100} 129 + label="sharpness" 130 + bind:value={options.rightEyebrow.sharpness} 131 + min={0} 132 + max={1} 133 + step={1} 134 + on:change={change} 135 + /> 136 + <Color label="color" bind:value={options.rightEyebrow.color} on:change={change} /> 137 + </Folder> 138 + 139 + <Folder expanded={false} title="Left Eye"> 140 + <Slider label="x" bind:value={options.leftEye.x} min={0} max={200} on:change={change} /> 141 + <Slider label="y" bind:value={options.leftEye.y} min={-100} max={100} on:change={change} /> 142 + <Slider 143 + label="sizeX" 144 + bind:value={options.leftEye.sizeX} 145 + min={1} 146 + max={100} 147 + on:change={change} 148 + /> 149 + <Slider 150 + label="sizeY" 151 + bind:value={options.leftEye.sizeY} 152 + min={1} 153 + max={100} 154 + on:change={change} 155 + /> 156 + <Slider 157 + label="pupilSizeX" 158 + bind:value={options.leftEye.pupilSizeX} 159 + min={1} 160 + max={100} 161 + on:change={change} 162 + /> 163 + <Slider 164 + label="pupilSizeY" 165 + bind:value={options.leftEye.pupilSizeY} 166 + min={1} 167 + max={100} 168 + on:change={change} 169 + /> 170 + </Folder> 171 + <Folder expanded={false} title="Right Eye"> 172 + <Slider label="x" bind:value={options.rightEye.x} min={-200} max={0} on:change={change} /> 173 + <Slider label="y" bind:value={options.rightEye.y} min={-100} max={100} on:change={change} /> 174 + <Slider 175 + label="sizeX" 176 + bind:value={options.rightEye.sizeX} 177 + min={1} 98 178 max={100} 99 179 on:change={change} 100 180 /> 101 181 <Slider 102 - label="angle" 103 - bind:value={options.rightEyebrow.angle} 104 - min={-100} 182 + label="sizeY" 183 + bind:value={options.rightEye.sizeY} 184 + min={1} 185 + max={100} 186 + on:change={change} 187 + /> 188 + <Slider 189 + label="pupilSizeX" 190 + bind:value={options.rightEye.pupilSizeX} 191 + min={1} 192 + max={100} 193 + on:change={change} 194 + /> 195 + <Slider 196 + label="pupilSizeY" 197 + bind:value={options.rightEye.pupilSizeY} 198 + min={1} 105 199 max={100} 106 200 on:change={change} 107 201 /> ··· 118 212 label="stroke" 119 213 bind:value={options.mouth.stroke} 120 214 min={1} 121 - max={120} 215 + max={200} 122 216 on:change={change} 123 217 /> 124 218 <Slider label="width" bind:value={options.mouth.width} min={1} max={200} on:change={change} /> 125 219 <Slider label="x" bind:value={options.mouth.x} min={-100} max={100} on:change={change} /> 126 220 <Slider label="y" bind:value={options.mouth.y} min={-200} max={100} on:change={change} /> 221 + 222 + <Color label="color" bind:value={options.mouth.color} on:change={change} /> 127 223 </Folder> 128 224 129 225 <Folder expanded={false} title="Colors"> ··· 131 227 <Color label="Secondary" bind:value={options.colors.secondary} on:change={change} /> 132 228 </Folder> 133 229 134 - <!-- <Button 135 - on:click={() => { 136 - console.log(JSON.stringify(options, null, 2)); 137 - }} 138 - title="Log" 139 - /> --> 230 + <Folder expanded={false} title="Emotions"> 231 + {#each Object.keys(options.emotions) as emotion} 232 + <Button 233 + on:click={() => { 234 + options.currentEmotion = emotion; 235 + options.hasChanged = true; 236 + }} 237 + title={emotion} 238 + /> 239 + {/each} 240 + 241 + <Separator /> 242 + 243 + <Text label="Emotion name" bind:value={newEmotion} /> 244 + <Button 245 + on:click={() => { 246 + options.emotions[newEmotion] = { 247 + leftEyebrow: { ...options.leftEyebrow }, 248 + rightEyebrow: { ...options.rightEyebrow }, 249 + mouth: { ...options.mouth }, 250 + leftEye: { ...options.leftEye }, 251 + rightEye: { ...options.rightEye } 252 + }; 253 + 254 + saveEmotions(); 255 + 256 + newEmotion = ''; 257 + }} 258 + title="Save" 259 + /> 260 + </Folder> 140 261 </Pane> 141 262 </div>
+15 -5
src/lib/Scene.svelte
··· 11 11 12 12 var stats = new Stats(); 13 13 14 + let distance = 5; 15 + 14 16 onMount(() => { 15 17 renderer.toneMapping = CineonToneMapping; 16 18 scene.environmentIntensity = 0.2; 17 19 20 + if (window.innerWidth < 768) { 21 + distance = 8; 22 + } 23 + 18 24 stats.showPanel(0); 19 25 document.body.appendChild(stats.dom); 26 + 27 + window.addEventListener('resize', () => { 28 + if (window.innerWidth < 768) { 29 + distance = 8; 30 + } else { 31 + distance = 5; 32 + } 33 + }); 20 34 }); 21 35 22 36 const { renderStage } = useThrelte(); ··· 36 50 ); 37 51 </script> 38 52 39 - <T.PerspectiveCamera makeDefault position={[0, 0, 8]}> 53 + <T.PerspectiveCamera makeDefault position={[0, 0, distance]}> 40 54 <OrbitControls /> 41 55 </T.PerspectiveCamera> 42 56 43 57 <Creature /> 44 - 45 - <!-- <Creature position={[3, 0, 0]} scale={[1, 0.7, 1]} /> 46 - 47 - <Creature position={[-3, 0, 0]} scale={[1, 1.3, 1]} /> --> 48 58 49 59 <Environment isBackground={false} url={'/tiny-creature/workshop.jpg'} /> 50 60
+31 -15
src/lib/face/curve.ts
··· 7 7 width: number; 8 8 curve: number; 9 9 10 - color: PIXI.ColorSource; 10 + colorRed: number; 11 + colorGreen: number; 12 + colorBlue: number; 11 13 12 14 x: number; 13 15 y: number; 14 16 15 17 angle: number; 18 + sharpness: number; 16 19 }; 17 20 18 21 export default class Curve { 19 22 graphics: PIXI.Graphics; 20 23 21 - color: PIXI.ColorSource = 0x515151; 22 - 23 - options: AnimatedProperties<Omit<CurveOptions, 'color'>>; 24 + options: AnimatedProperties<CurveOptions>; 24 25 25 26 constructor(opts: Partial<CurveOptions>) { 26 27 this.options = new AnimatedProperties({ ··· 30 31 curve: opts?.curve ?? 10, 31 32 x: opts?.x ?? 0, 32 33 y: opts?.y ?? 0, 33 - angle: opts?.angle ?? 0 34 + angle: opts?.angle ?? 0, 35 + sharpness: opts?.sharpness ?? 0, 36 + colorRed: opts?.colorRed ?? 20, 37 + colorGreen: opts?.colorGreen ?? 20, 38 + colorBlue: opts?.colorBlue ?? 20 34 39 }); 35 40 this.graphics = new PIXI.Graphics(); 36 - this.color = opts?.color ?? 0x212121; 37 41 38 42 this.draw(); 39 43 } ··· 46 50 const x = this.options.get('x'); 47 51 const y = this.options.get('y'); 48 52 const angle = this.options.get('angle'); 53 + const sharpness = this.options.get('sharpness'); 54 + const color = new PIXI.Color({ 55 + r: this.options.get('colorRed'), 56 + g: this.options.get('colorGreen'), 57 + b: this.options.get('colorBlue') 58 + }); 49 59 50 60 this.graphics.clear(); 51 - this.graphics 52 - .moveTo((-width / 2) * scale, 0) 53 - .quadraticCurveTo(0, curve * scale, (width / 2) * scale, 0, -10) 54 - .stroke({ 55 - color: this.color, 56 - width: stroke * scale, 57 - cap: 'round' 58 - }); 59 - 61 + if (sharpness > 0.5) { 62 + this.graphics 63 + .moveTo((-width / 2) * scale, 0) 64 + .lineTo(0, curve * scale) 65 + .lineTo((width / 2) * scale, 0); 66 + } else { 67 + this.graphics 68 + .moveTo((-width / 2) * scale, 0) 69 + .quadraticCurveTo(0, curve * scale, (width / 2) * scale, 0); 70 + } 71 + this.graphics.stroke({ 72 + color: color, 73 + width: stroke * scale, 74 + cap: 'round' 75 + }); 60 76 this.graphics.angle = angle; 61 77 this.graphics.position.set(x * scale, y * scale); 62 78 }
+63 -71
src/lib/face/eye.ts
··· 1 - import * as PIXI from "pixi.js"; 2 - import { AnimatedProperty, degreesToRadians } from './math-helper'; 1 + import * as PIXI from 'pixi.js'; 2 + import { AnimatedProperties, AnimatedProperty, degreesToRadians } from './math-helper'; 3 3 4 4 export type EyeOptions = { 5 - container: PIXI.Container; 6 - 7 - x: number; 8 - y: number; 9 - 10 - eyeColor: PIXI.ColorSource; 11 - pupilColor: PIXI.ColorSource; 12 - highlightColor: PIXI.ColorSource; 13 - 14 - size: number; 15 - 16 - eyebrow: Partial<{ 17 - length: number; 5 + x: number; 6 + y: number; 18 7 19 - width: number; 8 + sizeX: number; 9 + sizeY: number; 20 10 21 - y: number; 22 - x: number; 23 - 24 - curve: number; 25 - 26 - color: PIXI.ColorSource; 27 - }>; 28 - 29 - blinkStart: number; 30 - blinkTimer: number; 11 + pupilSizeX: number; 12 + pupilSizeY: number; 31 13 }; 32 14 33 15 export default class Eye { 34 - eyeContainer: PIXI.Container; 35 - 36 16 container: PIXI.Container; 37 17 38 18 base?: PIXI.Graphics; 39 19 pupil?: PIXI.Graphics; 40 - highlight?: PIXI.Sprite; 41 20 42 21 mask?: PIXI.Graphics; 43 22 ··· 53 32 dx: number = 0; 54 33 dy: number = 0; 55 34 56 - size: number = 1; 57 - 58 35 targetX: number = 0; 59 36 targetY: number = 0; 60 37 61 - constructor(opts: Partial<EyeOptions>) { 62 - this.container = new PIXI.Container(); 38 + options: AnimatedProperties<EyeOptions>; 63 39 64 - this.eyeContainer = new PIXI.Container(); 40 + furMask?: PIXI.Graphics; 65 41 66 - this.container.position.set(opts.x ?? 0, opts.y ?? 0); 42 + constructor(opts: Partial<EyeOptions>) { 43 + this.options = new AnimatedProperties({ 44 + x: opts.x ?? 0, 45 + y: opts.y ?? 0, 46 + sizeX: opts.sizeX ?? 50, 47 + sizeY: opts.sizeY ?? 50, 48 + pupilSizeX: opts.pupilSizeX ?? (opts.sizeX ?? 50) * 0.5, 49 + pupilSizeY: opts.pupilSizeY ?? (opts.sizeY ?? 50) * 0.5 50 + }); 67 51 68 - this.size = opts.size ?? 1; 52 + this.container = new PIXI.Container(); 69 53 70 - if (opts.blinkStart) { 71 - this.blinkTimer = opts.blinkStart; 72 - } 73 - if (opts.blinkTimer) { 74 - this.blinkTimer = opts.blinkTimer; 75 - } 54 + this.container.position.set(this.options.get('x'), this.options.get('y')); 76 55 77 56 this._x = new AnimatedProperty(); 78 57 this._y = new AnimatedProperty(); 79 58 80 - this.container.addChild(this.eyeContainer); 59 + this.base = new PIXI.Graphics(); 60 + this.container.addChild(this.base); 81 61 82 - if (opts.container) opts.container.addChild(this.container); 62 + this.pupil = new PIXI.Graphics(); 63 + this.container.addChild(this.pupil); 64 + this.mask = new PIXI.Graphics(); 65 + this.container.addChild(this.mask); 66 + this.container.mask = this.mask; 83 67 84 - this.base = new PIXI.Graphics() 85 - .ellipse(0, 0, this.size, this.size * 0.9) 86 - .fill(opts.eyeColor ?? 0xffffff); 87 - this.base.alpha = 1; 88 - this.eyeContainer.addChild(this.base); 68 + this.furMask = new PIXI.Graphics(); 69 + this.furMask.blendMode = 'erase'; 70 + this.furMask.x = this.options.get('x'); 71 + this.furMask.y = this.options.get('y'); 89 72 90 - this.pupil = new PIXI.Graphics() 91 - .circle(0, 0, this.size * 0.6) 92 - .fill(opts.pupilColor ?? 0x000000) 93 - .circle(this.size * 0.3, this.size * 0.3, 0.2 * this.size) 94 - .fill({ color: 0xffffff, alpha: 0.7 }); 95 - this.eyeContainer.addChild(this.pupil); 73 + this.draw(); 74 + } 75 + 76 + draw() { 77 + if (!this.base || !this.pupil || !this.mask || !this.furMask) return; 78 + const sizeX = this.options.get('sizeX'); 79 + const sizeY = this.options.get('sizeY'); 80 + 81 + const pupilSizeX = this.options.get('pupilSizeX'); 82 + const pupilSizeY = this.options.get('pupilSizeY'); 96 83 97 - this.mask = new PIXI.Graphics().ellipse(0, 0, this.size, this.size * 0.9).fill(0xffffff); 98 - this.eyeContainer.addChild(this.mask); 99 - this.eyeContainer.mask = this.mask; 84 + this.mask.clear(); 85 + this.mask.ellipse(0, 0, sizeX, sizeY).fill(0xffffff); 100 86 101 - const eyebrowWidth = opts?.eyebrow?.width ?? 2; 102 - const eyebrowCurve = opts?.eyebrow?.curve ?? 0.2; 87 + this.base.clear(); 88 + this.base.ellipse(0, 0, sizeX, sizeY).fill(0xffffff); 103 89 104 - const eyebrow = new PIXI.Graphics() 105 - .moveTo((-eyebrowWidth / 2) * this.size, 0) 106 - .quadraticCurveTo(0, eyebrowCurve * this.size, (eyebrowWidth / 2) * this.size, 0) 107 - .stroke({ 108 - color: opts.eyebrow?.color ?? 0, 109 - width: opts.eyebrow?.width ?? this.size * 0.2 110 - }); 111 - eyebrow.position.set(opts.eyebrow?.x ?? 0, (opts.eyebrow?.y ?? 0.9) * this.size); 90 + this.pupil.clear(); 91 + this.pupil.ellipse(0, 0, pupilSizeX, pupilSizeY).fill(0x000000); 92 + this.pupil 93 + .circle(pupilSizeX * 0.5, pupilSizeY * 0.5, 0.2 * sizeX) 94 + .fill({ color: 0xffffff, alpha: 0.7 }); 112 95 113 - // this.container.addChild(eyebrow); 96 + this.furMask.clear(); 97 + this.furMask.ellipse(0, 0, sizeX, sizeY).fill({ color: '#ffffff' }); 114 98 } 115 99 116 100 lookInDirection(direction: number) { ··· 120 104 } 121 105 122 106 lookAt(x: number, y: number) { 123 - this._x.set(x * this.size * 0.3); 124 - this._y.set(y * this.size * 0.3); 107 + this._x.set(x * this.options.get('sizeX') * 0.3); 108 + this._y.set(y * this.options.get('sizeY') * 0.3); 125 109 } 126 110 127 111 rotate(angle: number) { ··· 138 122 const updateY = this._y.update(deltaTime); 139 123 if (updateX || updateY) { 140 124 this.pupil?.position.set(this._x.value, this._y.value); 125 + } 126 + 127 + if (this.options.update(deltaTime)) { 128 + this.container.position.set(this.options.get('x'), this.options.get('y')); 129 + 130 + this.furMask?.position.set(this.options.get('x'), this.options.get('y')); 131 + 132 + this.draw(); 141 133 } 142 134 143 135 if (this.mask) {
+20 -55
src/lib/face/face.ts
··· 1 - import { Application, Container, Graphics } from 'pixi.js'; 1 + import { Application, Color, Container, Graphics } from 'pixi.js'; 2 2 import Eye from './eye'; 3 3 import Curve from './curve'; 4 + import { options } from '$lib/state.svelte'; 4 5 5 6 export async function createFace({ 6 7 width = 512, ··· 23 24 eyebrowRight: Curve; 24 25 furMouth: Curve; 25 26 }> { 26 - const xScale = 0.4 * scale; 27 + const xScale = 0.45 * scale; 27 28 const yScale = 1.5 * scale; 28 29 const faceApp = new Application(); 29 30 30 - const mouthSettings = { 31 - x: 0, 32 - y: -130, 33 - stroke: 20, 34 - color: 0, 35 - width: 100, 36 - curve: -40 37 - }; 38 - 39 31 await faceApp.init({ 40 32 width, 41 33 height, ··· 45 37 const background = new Graphics(); 46 38 background.rect(0, 0, width, height); 47 39 background.fill({ color: '#be185d' }); 48 - background.alpha = 0.7; 40 + background.alpha = 0.5; 49 41 faceApp.stage.addChild(background); 50 42 51 43 const container = new Container(); ··· 54 46 container.scale.set(xScale, yScale); 55 47 faceApp.stage.addChild(container); 56 48 57 - const rightEye = new Eye({ 58 - x: 75, 59 - y: 0, 60 - size: 50 61 - }); 49 + const rightEye = new Eye(options.rightEye); 62 50 63 - const leftEye = new Eye({ 64 - x: -75, 65 - y: 0, 66 - size: 50 67 - }); 51 + const leftEye = new Eye(options.leftEye); 68 52 69 53 container.addChild(rightEye.container); 70 54 container.addChild(leftEye.container); 71 55 72 - const mouth = new Curve(mouthSettings); 56 + const mouthColor = new Color(options.mouth.color); 57 + const mouth = new Curve({ 58 + ...options.mouth, 59 + colorRed: mouthColor.red * 256, 60 + colorGreen: mouthColor.green * 256, 61 + colorBlue: mouthColor.blue * 256 62 + }); 73 63 container.addChild(mouth.graphics); 74 64 75 65 const furApp = new Application(); ··· 107 97 furContainer.addChild(circle); 108 98 } 109 99 110 - const eyeMaskLeft = new Graphics(); 111 - eyeMaskLeft.circle(0, 0, 50); 112 - eyeMaskLeft.x = -75; 113 - eyeMaskLeft.y = 0; 114 - eyeMaskLeft.blendMode = 'erase'; 115 - eyeMaskLeft.fill({ color: '#ffffff' }); 116 - furContainer.addChild(eyeMaskLeft); 100 + if (leftEye.furMask && rightEye.furMask) { 101 + furContainer.addChild(leftEye.furMask); 102 + furContainer.addChild(rightEye.furMask); 103 + } 117 104 118 - const eyeMaskRight = new Graphics(); 119 - eyeMaskRight.circle(0, 0, 50); 120 - eyeMaskRight.x = 75; 121 - eyeMaskRight.y = 0; 122 - eyeMaskRight.blendMode = 'erase'; 123 - eyeMaskRight.fill({ color: '#ffffff' }); 124 - furContainer.addChild(eyeMaskRight); 125 - 126 - const eyebrowLeft = new Curve({ 127 - x: 75, 128 - y: 80, 129 - width: 100, 130 - curve: 20, 131 - stroke: 10 132 - }); 105 + const eyebrowLeft = new Curve(options.leftEyebrow); 133 106 eyebrowLeft.graphics.alpha = 0.5; 134 107 furContainer.addChild(eyebrowLeft.graphics); 135 108 136 - const eyebrowRight = new Curve({ 137 - x: -75, 138 - y: 80, 139 - width: 100, 140 - curve: 20, 141 - stroke: 10 142 - }); 109 + const eyebrowRight = new Curve(options.rightEyebrow); 143 110 eyebrowRight.graphics.alpha = 0.5; 144 111 furContainer.addChild(eyebrowRight.graphics); 145 112 146 - const furMouth = new Curve({ 147 - ...mouthSettings 148 - }); 113 + const furMouth = new Curve(options.mouth); 149 114 150 115 furMouth.graphics.blendMode = 'erase'; 151 116 furContainer.addChild(furMouth.graphics);
+8
src/lib/face/math-helper.ts
··· 193 193 this.properties[key].set(value); 194 194 } 195 195 196 + setMultiple(values: Partial<T>) { 197 + for (const key in values) { 198 + if (values[key] !== undefined && this.properties[key]) { 199 + this.properties[key].set(values[key]); 200 + } 201 + } 202 + } 203 + 196 204 // Update all animated values 197 205 update(dt: number): boolean { 198 206 let anyUpdated = false;
+179 -3
src/lib/state.svelte.ts
··· 1 1 export const options = $state({ 2 2 hasChanged: false, 3 + 3 4 leftEyebrow: { 4 5 x: 75, 5 6 y: 80, 6 7 width: 100, 7 8 curve: 20, 8 9 stroke: 10, 9 - angle: 0 10 + angle: 0, 11 + sharpness: 0, 12 + color: '#515151' 10 13 }, 11 14 rightEyebrow: { 12 15 x: -75, ··· 14 17 width: 100, 15 18 curve: 20, 16 19 stroke: 10, 17 - angle: 0 20 + angle: 0, 21 + sharpness: 0, 22 + color: '#515151' 18 23 }, 19 24 mouth: { 20 25 x: 0, 21 26 y: -130, 22 27 stroke: 20, 23 - color: 0, 28 + color: '#000000', 24 29 width: 100, 25 30 curve: -40 31 + }, 32 + leftEye: { 33 + x: 75, 34 + y: 0, 35 + sizeX: 50, 36 + sizeY: 50, 37 + pupilSizeX: 20, 38 + pupilSizeY: 20 39 + }, 40 + rightEye: { 41 + x: -75, 42 + y: 0, 43 + sizeX: 50, 44 + sizeY: 50, 45 + pupilSizeX: 20, 46 + pupilSizeY: 20 47 + }, 48 + 49 + currentEmotion: '', 50 + 51 + emotions: { 52 + base: { 53 + leftEyebrow: { 54 + x: 75, 55 + y: 80, 56 + width: 100, 57 + curve: 20, 58 + stroke: 10, 59 + angle: 0, 60 + sharpness: 0, 61 + color: '#515151' 62 + }, 63 + rightEyebrow: { 64 + x: -75, 65 + y: 80, 66 + width: 100, 67 + curve: 20, 68 + stroke: 10, 69 + angle: 0, 70 + sharpness: 0, 71 + color: '#515151' 72 + }, 73 + mouth: { x: 0, y: -130, stroke: 20, color: '#000000', width: 100, curve: -40 }, 74 + leftEye: { x: 75, y: 0, sizeX: 50, sizeY: 50, pupilSizeX: 20, pupilSizeY: 20 }, 75 + rightEye: { x: -75, y: 0, sizeX: 50, sizeY: 50, pupilSizeX: 20, pupilSizeY: 20 } 76 + }, 77 + scared: { 78 + leftEyebrow: { 79 + x: 110, 80 + y: 80, 81 + width: 70, 82 + curve: -20, 83 + stroke: 10, 84 + angle: -18, 85 + sharpness: 0, 86 + color: '#515151' 87 + }, 88 + rightEyebrow: { 89 + x: -110, 90 + y: 80, 91 + width: 70, 92 + curve: -20, 93 + stroke: 10, 94 + angle: 18, 95 + sharpness: 0, 96 + color: '#515151' 97 + }, 98 + mouth: { x: 0, y: -130, stroke: 30, color: '#000000', width: 150, curve: 40 }, 99 + leftEye: { x: 75, y: 0, sizeX: 50, sizeY: 40, pupilSizeX: 20, pupilSizeY: 20 }, 100 + rightEye: { x: -75, y: 0, sizeX: 50, sizeY: 40, pupilSizeX: 20, pupilSizeY: 20 } 101 + }, 102 + surprised: { 103 + leftEyebrow: { 104 + x: 90, 105 + y: 120, 106 + width: 100, 107 + curve: 100, 108 + stroke: 15, 109 + angle: -10, 110 + sharpness: 0, 111 + color: '#515151' 112 + }, 113 + rightEyebrow: { 114 + x: -90, 115 + y: 120, 116 + width: 100, 117 + curve: 100, 118 + stroke: 15, 119 + angle: 10, 120 + sharpness: 0, 121 + color: '#515151' 122 + }, 123 + mouth: { x: 0, y: -160, stroke: 100, color: '#000000', width: 40, curve: -5 }, 124 + leftEye: { x: 75, y: 0, sizeX: 70, sizeY: 70, pupilSizeX: 30, pupilSizeY: 30 }, 125 + rightEye: { x: -75, y: 0, sizeX: 70, sizeY: 70, pupilSizeX: 30, pupilSizeY: 30 } 126 + }, 127 + angry: { 128 + leftEyebrow: { 129 + x: 75, 130 + y: 80, 131 + width: 50, 132 + curve: -10, 133 + stroke: 10, 134 + angle: 30, 135 + sharpness: 0, 136 + color: '#515151' 137 + }, 138 + rightEyebrow: { 139 + x: -75, 140 + y: 80, 141 + width: 50, 142 + curve: -10, 143 + stroke: 10, 144 + angle: -30, 145 + sharpness: 0, 146 + color: '#515151' 147 + }, 148 + mouth: { x: 0, y: -130, stroke: 20, color: '#000000', width: 70, curve: 0 }, 149 + leftEye: { x: 75, y: -20, sizeX: 50, sizeY: 40, pupilSizeX: 20, pupilSizeY: 20 }, 150 + rightEye: { x: -75, y: -20, sizeX: 50, sizeY: 40, pupilSizeX: 20, pupilSizeY: 20 } 151 + }, 152 + sad: { 153 + leftEyebrow: { 154 + x: 75, 155 + y: 80, 156 + width: 100, 157 + curve: -20, 158 + stroke: 10, 159 + angle: -10, 160 + sharpness: 0, 161 + color: '#515151' 162 + }, 163 + rightEyebrow: { 164 + x: -75, 165 + y: 80, 166 + width: 100, 167 + curve: -20, 168 + stroke: 10, 169 + angle: 10, 170 + sharpness: 0, 171 + color: '#515151' 172 + }, 173 + mouth: { x: 0, y: -130, stroke: 20, color: '#000000', width: 100, curve: 20 }, 174 + leftEye: { x: 75, y: 0, sizeX: 60, sizeY: 60, pupilSizeX: 25, pupilSizeY: 25 }, 175 + rightEye: { x: -75, y: 0, sizeX: 60, sizeY: 60, pupilSizeX: 25, pupilSizeY: 25 } 176 + }, 177 + excited: { 178 + leftEyebrow: { 179 + x: 75, 180 + y: 100, 181 + width: 100, 182 + curve: 40, 183 + stroke: 10, 184 + angle: 0, 185 + sharpness: 0, 186 + color: '#515151' 187 + }, 188 + rightEyebrow: { 189 + x: -75, 190 + y: 100, 191 + width: 100, 192 + curve: 40, 193 + stroke: 10, 194 + angle: 0, 195 + sharpness: 0, 196 + color: '#515151' 197 + }, 198 + mouth: { x: 0, y: -130, stroke: 40, color: '#000000', width: 150, curve: -80 }, 199 + leftEye: { x: 75, y: 0, sizeX: 50, sizeY: 60, pupilSizeX: 25, pupilSizeY: 25 }, 200 + rightEye: { x: -75, y: 0, sizeX: 50, sizeY: 60, pupilSizeX: 25, pupilSizeY: 25 } 201 + } 26 202 }, 27 203 colors: { 28 204 primary: '#ec4899',
+52
src/routes/+page.svelte
··· 8 8 import { fromStore } from 'svelte/store'; 9 9 import { useProgress } from '@threlte/extras'; 10 10 import { onMount } from 'svelte'; 11 + import { options } from '$lib/state.svelte'; 11 12 const { progress } = useProgress(); 12 13 const p = fromStore(progress); 13 14 const tweenedProgress = Tween.of(() => p.current, { ··· 20 21 21 22 onMount(() => { 22 23 window.addEventListener('keydown', (e) => { 24 + // check if e.srcElement is text input 25 + if (e.srcElement instanceof HTMLInputElement) { 26 + return; 27 + } 28 + 23 29 if (e.key === 's') { 24 30 showSettings = !showSettings; 25 31 } 26 32 }); 27 33 }); 34 + 35 + let currentEmotion = $state('base'); 36 + 37 + function setEmotion(emotion: string) { 38 + currentEmotion = emotion; 39 + options.currentEmotion = emotion; 40 + options.hasChanged = true; 41 + } 42 + 43 + let emotions: Record<string, string> = { 44 + base: '🙂', 45 + scared: '😧', 46 + excited: '😄', 47 + sad: '🙁', 48 + angry: '😠', 49 + surprised: '😲' 50 + }; 28 51 </script> 29 52 30 53 <div class="h-screen w-screen bg-gradient-to-t from-stone-950 to-stone-900/50"> ··· 63 86 </div> 64 87 </div> 65 88 {/if} 89 + 90 + <div 91 + class="fixed bottom-2 left-0 right-0 flex w-full items-end justify-between px-2 text-sm text-white" 92 + > 93 + <div> 94 + made by <a href="https://flo-bit.dev" target="_blank" class="font-semibold text-pink-400" 95 + >flo-bit</a 96 + > 97 + for 98 + <a 99 + href="https://threejs-journey.com/challenges/tamagotchi" 100 + target="_blank" 101 + class="font-semibold text-pink-400">3js journey #016</a 102 + > 103 + </div> 104 + 105 + <div class="flex flex-col gap-2 text-4xl sm:flex-row"> 106 + {#each Object.keys(emotions) as emotion} 107 + <button 108 + class="text-white {currentEmotion === emotion 109 + ? 'rounded-xl ring-2 ring-amber-400' 110 + : 'opacity-80'}" 111 + onclick={() => { 112 + setEmotion(emotion); 113 + }}>{emotions[emotion]}</button 114 + > 115 + {/each} 116 + </div> 117 + </div>