[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.

some refactoring

Florian (Feb 8, 2025, 8:04 PM +0100) 1c3ed76d b7e44108

+362 -374
+165 -127
src/lib/Creature.svelte
··· 4 4 // @ts-ignore 5 5 import { WiggleBone } from 'wiggle'; 6 6 import { onMount } from 'svelte'; 7 - import { createFaceTexture, createFurTexture, updateBackground, updateFace } from './face/face'; 7 + import { createFace } from './face/face'; 8 8 import { T, useTask, useThrelte } from '@threlte/core'; 9 9 import { 10 10 color, ··· 20 20 vec3, 21 21 vec4 22 22 } from 'three/tsl'; 23 - import { colors } from './state.svelte'; 23 + import { options } from './state.svelte'; 24 24 25 25 let rootBone: THREE.Bone; 26 26 const wiggleBones: WiggleBone[] = []; 27 27 let creature: THREE.Group | null = $state(null); 28 28 29 - let canvasTexture: THREE.CanvasTexture; 29 + let faceTexture: THREE.CanvasTexture; 30 30 let furTexture: THREE.CanvasTexture; 31 31 32 32 const { canvas } = useThrelte(); 33 + 34 + let { 35 + scale = [1, 1, 1], 36 + position = [0, 0, 0] 37 + }: { scale?: [number, number, number]; position?: [number, number, number] } = $props(); 33 38 34 39 let mouse = new THREE.Vector2(); 35 40 ··· 40 45 let primaryColors: THREE.TSL.ShaderNodeObject<THREE.UniformNode<THREE.Color>>[] = []; 41 46 let secondaryColors: THREE.TSL.ShaderNodeObject<THREE.UniformNode<THREE.Color>>[] = []; 42 47 43 - onMount(async () => { 48 + let face: Awaited<ReturnType<typeof createFace>>; 49 + 50 + async function createTextures() { 51 + face = await createFace({ 52 + width: 256, 53 + height: 256, 54 + scale: 0.25 55 + }); 56 + 57 + faceTexture = new THREE.CanvasTexture(face.faceCanvas); 58 + faceTexture.minFilter = THREE.NearestFilter; 59 + faceTexture.magFilter = THREE.NearestFilter; 60 + 61 + furTexture = new THREE.CanvasTexture(face.furCanvas); 62 + } 63 + 64 + function createFur(mesh: THREE.SkinnedMesh) { 65 + const furMeshes = []; 66 + 67 + let total = 12; 68 + for (let i = 0; i < total; i++) { 69 + const material = new THREE.MeshStandardNodeMaterial(); 70 + 71 + // move vertices along normal to make each shell slightly bigger than the last 72 + material.positionNode = positionGeometry 73 + .add(normalGeometry.mul(i * 0.01)) 74 + // simulate some gravity 75 + .add(vec3(0, -0.002, 0).mul(i)); 76 + 77 + const primaryColor = uniform(new THREE.Color(options.colors.primary)); 78 + const secondaryColor = uniform(new THREE.Color(options.colors.secondary)); 79 + 80 + primaryColors.push(primaryColor); 81 + secondaryColors.push(secondaryColor); 82 + 83 + material.colorNode = Fn(() => { 84 + const t = texture(furTexture); 85 + 86 + // noise color 87 + const c = mix( 88 + color(primaryColor), 89 + color(secondaryColor), 90 + mx_noise_float(positionGeometry.mul(10).mul(0.5).add(0.5)) 91 + ); 92 + // const c = color(0xec4899); 93 + 94 + // how much fur is there 95 + // more transparent -> less fur (0 = no fur) 96 + const a = t.a; 97 + 98 + // if fur texture is black, use noise color 99 + // make lower layers darker 100 + const s = select(t.rgb.lengthSq().lessThan(0.00001), c, t).mul( 101 + float(i / total) 102 + .add(a.mul(-1).add(1)) 103 + .mul(0.8) 104 + .add(0.2) 105 + ); 106 + 107 + // make more parts of the upper layers transparent 108 + const opacity = select( 109 + mx_noise_float(positionGeometry.mul(float(200).add(a.mul(-100)))).greaterThan( 110 + float((i / total) * 2 - 1).add(a.mul(-2).add(2)) 111 + ), 112 + 1.0, 113 + 0.0 114 + ); 115 + 116 + return vec4(s.rgb, opacity); 117 + })(); 118 + 119 + material.alphaTestNode = float(0.9); 120 + 121 + const fur = mesh.clone(); 122 + fur.name = `fur-${i}`; 123 + fur.material = material; 124 + 125 + furMeshes.push(fur); 126 + } 127 + 128 + return furMeshes; 129 + } 130 + 131 + function createWiggleBones(mesh: THREE.SkinnedMesh) { 132 + mesh.skeleton.bones.forEach((bone) => { 133 + if (!(bone.parent instanceof THREE.Bone) || !bone.parent?.isBone) { 134 + rootBone = bone; 135 + 136 + rootBone.scale.set(scale[0], scale[1], scale[2]); 137 + } else { 138 + const wiggleBone = new WiggleBone(bone, { 139 + velocity: 0.4 140 + }); 141 + wiggleBones.push(wiggleBone); 142 + } 143 + }); 144 + } 145 + 146 + function addEventListeners() { 44 147 // add mouse movement 45 148 canvas.addEventListener('mousemove', (event) => { 46 149 mouse.set(event.clientX / window.innerWidth, event.clientY / window.innerHeight); ··· 58 161 jump(); 59 162 } 60 163 }); 164 + } 61 165 62 - const { app } = await createFaceTexture({ 63 - width: 1024, 64 - height: 1024 65 - }); 166 + onMount(async () => { 167 + addEventListeners(); 66 168 67 - canvasTexture = new THREE.CanvasTexture(app.canvas); 68 - 69 - const fur = await createFurTexture({ 70 - width: 512, 71 - height: 512 72 - }); 73 - 74 - furTexture = new THREE.CanvasTexture(fur.app.canvas); 169 + let texturePromise = createTextures(); 75 170 76 171 const loader = new GLTFLoader(); 77 172 78 - loader.load('/tiny-creature/model.glb', ({ scene: obj }) => { 79 - console.log(obj); 173 + loader.load('/tiny-creature/model.glb', async ({ scene: obj }) => { 80 174 const mesh = obj.getObjectByName('Icosphere'); 81 - if (!mesh) return; 82 - 83 - creature = obj; 84 - let scale = 1.4; 85 - creature.scale.set(scale, scale, scale); 86 - 87 175 if (!(mesh instanceof THREE.SkinnedMesh)) return; 88 176 89 - // add face and fur 90 - const material = new THREE.MeshStandardNodeMaterial(); 91 - material.colorNode = texture(canvasTexture); 177 + await texturePromise; 92 178 93 - material.bumpMap = canvasTexture; 179 + creature = obj; 94 180 95 - mesh.material = material; 181 + mesh.material = new THREE.MeshStandardMaterial({ map: faceTexture }); 96 182 97 - const furMeshes = []; 183 + let fur = createFur(mesh); 184 + obj.add(...fur); 98 185 99 - let total = 12; 100 - for (let i = 0; i < total; i++) { 101 - const material = new THREE.MeshStandardNodeMaterial(); 102 - 103 - // move vertices along normal to make each shell slightly bigger than the last 104 - material.positionNode = positionGeometry 105 - .add(normalGeometry.mul(i * 0.01)) 106 - // simulate some gravity 107 - .add(vec3(0, -0.002, 0).mul(i)); 108 - 109 - const primaryColor = uniform(new THREE.Color(colors.primary)); 110 - const secondaryColor = uniform(new THREE.Color(colors.secondary)); 111 - 112 - primaryColors.push(primaryColor); 113 - secondaryColors.push(secondaryColor); 114 - 115 - material.colorNode = Fn(() => { 116 - const t = texture(furTexture); 117 - 118 - // noise color 119 - const c = mix( 120 - color(primaryColor), 121 - color(secondaryColor), 122 - mx_noise_float(positionGeometry.mul(10).mul(0.5).add(0.5)) 123 - ); 124 - // const c = color(0xec4899); 125 - 126 - // how much fur is there 127 - // more transparent -> less fur (0 = no fur) 128 - const a = t.a; 129 - 130 - // if fur texture is black, use noise color 131 - // make lower layers darker 132 - const s = select(t.rgb.lengthSq().lessThan(0.00001), c, t).mul( 133 - float(i / total) 134 - .add(a.mul(-1).add(1)) 135 - .mul(0.8) 136 - .add(0.2) 137 - ); 138 - 139 - // make more parts of the upper layers transparent 140 - const opacity = select( 141 - mx_noise_float(positionGeometry.mul(float(200).add(a.mul(-100)))).greaterThan( 142 - float((i / total) * 2 - 1).add(a.mul(-2).add(2)) 143 - ), 144 - 1.0, 145 - 0.0 146 - ); 147 - 148 - return vec4(s.rgb, opacity); 149 - })(); 150 - 151 - material.alphaTestNode = float(0.9); 152 - 153 - const fur = mesh.clone(); 154 - fur.name = `fur-${i}`; 155 - fur.material = material; 156 - 157 - furMeshes.push(fur); 158 - } 159 - 160 - mesh.add(...furMeshes); 161 - 162 - // add wiggle bones 163 - mesh.skeleton.bones.forEach((bone) => { 164 - if (!(bone.parent instanceof THREE.Bone) || !bone.parent?.isBone) { 165 - rootBone = bone; 166 - } else { 167 - const wiggleBone = new WiggleBone(bone, { 168 - velocity: 0.4 169 - }); 170 - wiggleBones.push(wiggleBone); 171 - } 172 - }); 186 + createWiggleBones(mesh); 173 187 }); 174 188 }); 175 189 176 190 let total = 0; 177 191 let ySpeed = 0; 178 - 179 - let lastColors = { 180 - primary: colors.primary.toLowerCase(), 181 - secondary: colors.secondary.toLowerCase() 182 - }; 183 192 184 193 useTask((dt) => { 185 194 total += dt; ··· 187 196 wiggleBone.update(); 188 197 }); 189 198 190 - if (canvasTexture) { 191 - updateFace(dt, (mouse.x - 0.5) * 2, -(mouse.y - 0.5) * 2); 192 - canvasTexture.needsUpdate = true; 193 - } 194 - 195 - if (furTexture) { 196 - furTexture.needsUpdate = true; 197 - } 198 - 199 199 if (rootBone) { 200 - rootBone.position.x = Math.sin(total * 2) * 0.5; 200 + rootBone.position.x = (Math.sin(total * 2) * 0.5 + position[0]); 201 201 202 202 rootBone.position.y += ySpeed * dt; 203 - rootBone.position.y = Math.max(rootBone.position.y, -1); 203 + rootBone.position.y = Math.max(rootBone.position.y, -1) + position[1]; 204 + rootBone.position.z = position[2]; 204 205 205 206 ySpeed -= 10 * dt; 206 207 } 207 208 208 - if ( 209 - lastColors.primary !== colors.primary.toLowerCase() || 210 - lastColors.secondary !== colors.secondary.toLowerCase() 211 - ) { 212 - updateBackground(colors.primary); 209 + if (!face) return; 210 + 211 + face.update(dt); 212 + face.lookAt((mouse.x - 0.5) * 2, -(mouse.y - 0.5) * 2); 213 + 214 + faceTexture.needsUpdate = true; 215 + furTexture.needsUpdate = true; 216 + 217 + faceTexture.needsUpdate = true; 218 + 219 + if (options.hasChanged) { 220 + console.log('updating creature'); 221 + 222 + face.eyebrowLeft.options.set('curve', options.leftEyebrow.curve); 223 + face.eyebrowLeft.options.set('stroke', options.leftEyebrow.stroke); 224 + face.eyebrowLeft.options.set('width', options.leftEyebrow.width); 225 + face.eyebrowLeft.options.set('x', options.leftEyebrow.x); 226 + face.eyebrowLeft.options.set('y', options.leftEyebrow.y); 227 + face.eyebrowLeft.options.set('angle', options.leftEyebrow.angle); 228 + 229 + face.eyebrowRight.options.set('curve', options.rightEyebrow.curve); 230 + face.eyebrowRight.options.set('stroke', options.rightEyebrow.stroke); 231 + face.eyebrowRight.options.set('width', options.rightEyebrow.width); 232 + face.eyebrowRight.options.set('x', options.rightEyebrow.x); 233 + face.eyebrowRight.options.set('y', options.rightEyebrow.y); 234 + face.eyebrowRight.options.set('angle', options.rightEyebrow.angle); 235 + 236 + face.mouth.options.set('curve', options.mouth.curve); 237 + face.mouth.options.set('stroke', options.mouth.stroke); 238 + face.mouth.options.set('width', options.mouth.width); 239 + face.mouth.options.set('x', options.mouth.x); 240 + face.mouth.options.set('y', options.mouth.y); 241 + 242 + face.furMouth.options.set('curve', options.mouth.curve); 243 + face.furMouth.options.set('stroke', options.mouth.stroke); 244 + face.furMouth.options.set('width', options.mouth.width); 245 + face.furMouth.options.set('x', options.mouth.x); 246 + face.furMouth.options.set('y', options.mouth.y); 247 + 248 + face.updateBackground(options.colors.primary); 213 249 214 250 primaryColors.forEach((primaryColor) => { 215 - primaryColor.value = new THREE.Color(colors.primary); 251 + primaryColor.value = new THREE.Color(options.colors.primary); 216 252 }); 217 253 218 254 secondaryColors.forEach((secondaryColor) => { 219 - secondaryColor.value = new THREE.Color(colors.secondary); 255 + secondaryColor.value = new THREE.Color(options.colors.secondary); 220 256 }); 257 + 258 + options.hasChanged = false; 221 259 } 222 260 }); 223 261 </script>
+23 -84
src/lib/Pane.svelte
··· 1 1 <script lang="ts"> 2 2 import { 3 - Checkbox, 4 3 Pane, 5 4 Slider, 6 - Textarea, 7 5 Folder, 8 6 Color, 9 7 ThemeUtils, 10 - Button 11 8 } from 'svelte-tweakpane-ui'; 12 - import { eyebrowLeft, eyebrowRight, furMouth, mouth } from './face/face'; 13 - import { colors } from './state.svelte'; 14 - 15 - const options = $state({ 16 - leftEyebrow: { 17 - x: 75 / 2, 18 - y: 40, 19 - width: 50, 20 - curve: 10, 21 - stroke: 10 22 - }, 23 - rightEyebrow: { 24 - x: -75 / 2, 25 - y: 40, 26 - width: 50, 27 - curve: 10, 28 - stroke: 10 29 - }, 30 - mouth: { 31 - x: 0, 32 - y: -130, 33 - stroke: 20, 34 - color: 0, 35 - width: 100, 36 - curve: -40 37 - } 38 - }); 9 + import { options } from './state.svelte'; 39 10 40 11 function change() { 41 - console.log(eyebrowLeft); 42 - eyebrowLeft?.options.set('curve', options.leftEyebrow.curve); 43 - eyebrowLeft?.options.set('stroke', options.leftEyebrow.stroke); 44 - eyebrowLeft?.options.set('width', options.leftEyebrow.width); 45 - eyebrowLeft?.options.set('x', options.leftEyebrow.x); 46 - eyebrowLeft?.options.set('y', options.leftEyebrow.y); 47 - 48 - eyebrowRight?.options.set('curve', options.rightEyebrow.curve); 49 - eyebrowRight?.options.set('stroke', options.rightEyebrow.stroke); 50 - eyebrowRight?.options.set('width', options.rightEyebrow.width); 51 - eyebrowRight?.options.set('x', options.rightEyebrow.x); 52 - eyebrowRight?.options.set('y', options.rightEyebrow.y); 53 - 54 - mouth?.options.set('curve', options.mouth.curve); 55 - mouth?.options.set('stroke', options.mouth.stroke); 56 - mouth?.options.set('width', options.mouth.width); 57 - mouth?.options.set('x', options.mouth.x); 58 - mouth?.options.set('y', options.mouth.y); 59 - 60 - furMouth?.options.set('curve', options.mouth.curve); 61 - furMouth?.options.set('stroke', options.mouth.stroke); 62 - furMouth?.options.set('width', options.mouth.width); 63 - furMouth?.options.set('x', options.mouth.x); 64 - furMouth?.options.set('y', options.mouth.y); 12 + options.hasChanged = true; 65 13 } 66 14 </script> 67 15 ··· 105 53 max={100} 106 54 on:change={change} 107 55 /> 56 + 57 + <Slider 58 + label="angle" 59 + bind:value={options.leftEyebrow.angle} 60 + min={-100} 61 + max={100} 62 + on:change={change} 63 + /> 108 64 </Folder> 109 65 <Folder expanded={false} title="Right Eyebrow"> 110 66 <Slider ··· 142 98 max={100} 143 99 on:change={change} 144 100 /> 101 + <Slider 102 + label="angle" 103 + bind:value={options.rightEyebrow.angle} 104 + min={-100} 105 + max={100} 106 + on:change={change} 107 + /> 145 108 </Folder> 146 109 <Folder expanded={false} title="Mouth"> 147 110 <Slider ··· 158 121 max={120} 159 122 on:change={change} 160 123 /> 161 - <Slider 162 - label="width" 163 - bind:value={options.mouth.width} 164 - min={1} 165 - max={200} 166 - on:change={change} 167 - /> 168 - <Slider 169 - label="x" 170 - bind:value={options.mouth.x} 171 - min={-100} 172 - max={100} 173 - on:change={change} 174 - /> 175 - <Slider 176 - label="y" 177 - bind:value={options.mouth.y} 178 - min={-200} 179 - max={100} 180 - on:change={change} 181 - /> 124 + <Slider label="width" bind:value={options.mouth.width} min={1} max={200} on:change={change} /> 125 + <Slider label="x" bind:value={options.mouth.x} min={-100} max={100} on:change={change} /> 126 + <Slider label="y" bind:value={options.mouth.y} min={-200} max={100} on:change={change} /> 182 127 </Folder> 183 128 184 129 <Folder expanded={false} title="Colors"> 185 - <Color 186 - label="Primary" 187 - bind:value={colors.primary} 188 - /> 189 - <Color 190 - label="Secondary" 191 - bind:value={colors.secondary} 192 - /> 130 + <Color label="Primary" bind:value={options.colors.primary} on:change={change} /> 131 + <Color label="Secondary" bind:value={options.colors.secondary} on:change={change} /> 193 132 </Folder> 194 133 195 134 <!-- <Button ··· 199 138 title="Log" 200 139 /> --> 201 140 </Pane> 202 - </div> 141 + </div>
+6 -3
src/lib/Scene.svelte
··· 19 19 document.body.appendChild(stats.dom); 20 20 }); 21 21 22 - 23 22 const { renderStage } = useThrelte(); 24 23 25 24 const afterRenderStage = useStage('after-render', { 26 25 after: renderStage 27 26 }); 28 - 27 + 29 28 useTask(() => { 30 29 stats.begin(); 31 30 }); ··· 43 42 44 43 <Creature /> 45 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 + 46 49 <Environment isBackground={false} url={'/tiny-creature/workshop.jpg'} /> 47 50 48 51 <T.DirectionalLight position={[10, 10, 10]} intensity={1} /> 49 52 50 - <T.DirectionalLight position={[-10, 10, -10]} intensity={0.5} /> 53 + <T.DirectionalLight position={[-10, 10, -10]} intensity={0.5} />
+30 -3
src/lib/state.svelte.ts
··· 1 - export const colors = $state({ 2 - primary: '#ec4899', 3 - secondary: '#fb923c' 1 + export const options = $state({ 2 + hasChanged: false, 3 + leftEyebrow: { 4 + x: 75, 5 + y: 80, 6 + width: 100, 7 + curve: 20, 8 + stroke: 10, 9 + angle: 0 10 + }, 11 + rightEyebrow: { 12 + x: -75, 13 + y: 80, 14 + width: 100, 15 + curve: 20, 16 + stroke: 10, 17 + angle: 0 18 + }, 19 + mouth: { 20 + x: 0, 21 + y: -130, 22 + stroke: 20, 23 + color: 0, 24 + width: 100, 25 + curve: -40 26 + }, 27 + colors: { 28 + primary: '#ec4899', 29 + secondary: '#fb923c' 30 + } 4 31 });
+27 -10
src/routes/+page.svelte
··· 1 1 <script lang="ts"> 2 2 import { Canvas } from '@threlte/core'; 3 3 import Scene from '$lib/Scene.svelte'; 4 - import { WebGPURenderer } from 'three/webgpu' 4 + import { WebGPURenderer } from 'three/webgpu'; 5 5 import Pane from '$lib/Pane.svelte'; 6 6 7 + import { Tween } from 'svelte/motion'; 8 + import { fromStore } from 'svelte/store'; 9 + import { useProgress } from '@threlte/extras'; 10 + const { progress } = useProgress(); 11 + const p = fromStore(progress); 12 + const tweenedProgress = Tween.of(() => p.current, { 13 + duration: 150 14 + }); 15 + const progressWidth = $derived(100 * tweenedProgress.current); 16 + const progressLessThanOne = $derived(tweenedProgress.current < 1); 7 17 </script> 8 18 9 19 <div class="h-screen w-screen bg-gradient-to-t from-stone-950 to-stone-900/50"> 10 - <Canvas createRenderer={(canvas) => { 11 - return new WebGPURenderer({ 12 - canvas, 13 - antialias: true, 14 - forceWebGL: false 15 - }) 16 - }}> 20 + <Canvas 21 + createRenderer={(canvas) => { 22 + return new WebGPURenderer({ 23 + canvas, 24 + antialias: true, 25 + forceWebGL: false 26 + }); 27 + }} 28 + > 17 29 <!-- <Studio> --> 18 - <Scene /> 30 + <Scene /> 19 31 <!-- </Studio> --> 20 32 </Canvas> 21 33 </div> 22 34 35 + <Pane /> 23 36 24 - <Pane /> 37 + {#if progressLessThanOne} 38 + <div class="absolute inset-0 w-full h-full flex items-center justify-start px-10 bg-stone-950"> 39 + <div class="h-10 bg-white rounded-full" style="width: {progressWidth}%"></div> 40 + </div> 41 + {/if}
+6 -1
src/lib/face/curve.ts
··· 11 11 12 12 x: number; 13 13 y: number; 14 + 15 + angle: number; 14 16 }; 15 17 16 18 export default class Curve { ··· 27 29 width: opts?.width ?? 50, 28 30 curve: opts?.curve ?? 10, 29 31 x: opts?.x ?? 0, 30 - y: opts?.y ?? 0 32 + y: opts?.y ?? 0, 33 + angle: opts?.angle ?? 0 31 34 }); 32 35 this.graphics = new PIXI.Graphics(); 33 36 this.color = opts?.color ?? 0x212121; ··· 42 45 const stroke = this.options.get('stroke'); 43 46 const x = this.options.get('x'); 44 47 const y = this.options.get('y'); 48 + const angle = this.options.get('angle'); 45 49 46 50 this.graphics.clear(); 47 51 this.graphics ··· 53 57 cap: 'round' 54 58 }); 55 59 60 + this.graphics.angle = angle; 56 61 this.graphics.position.set(x * scale, y * scale); 57 62 } 58 63
-1
src/lib/face/eye.ts
··· 59 59 targetY: number = 0; 60 60 61 61 constructor(opts: Partial<EyeOptions>) { 62 - console.log('Eye', opts); 63 62 this.container = new PIXI.Container(); 64 63 65 64 this.eyeContainer = new PIXI.Container();
+105 -145
src/lib/face/face.ts
··· 1 - import { Application, Container, Graphics, type Renderer } from 'pixi.js'; 1 + import { Application, Container, Graphics } from 'pixi.js'; 2 2 import Eye from './eye'; 3 3 import Curve from './curve'; 4 4 5 - let leftEye: Eye; 6 - let rightEye: Eye; 7 - 8 - let mouth: Curve; 9 - let furMouth: Curve; 10 - 11 - let eyebrowLeft: Curve; 12 - let eyebrowRight: Curve; 13 - 14 - let background: Graphics; 15 - 16 - const mouthSettings = { 17 - x: 0, 18 - y: -130, 19 - stroke: 20, 20 - color: 0, 21 - width: 100, 22 - curve: -40 23 - }; 24 - 25 - const size = { 26 - width: 512, 27 - height: 512 28 - }; 29 - 30 - export async function createFaceTexture({ 31 - width = size.width, 32 - height = size.height 5 + export async function createFace({ 6 + width = 512, 7 + height = 512, 8 + scale = 1 33 9 }: { 34 10 width?: number; 35 11 height?: number; 36 - }): Promise<{ app: Application<Renderer> }> { 37 - size.width = width; 38 - size.height = height; 12 + scale?: number; 13 + }): Promise<{ 14 + faceCanvas: HTMLCanvasElement; 15 + furCanvas: HTMLCanvasElement; 16 + lookAt: (x: number, y: number) => void; 17 + update: (dt: number) => void; 18 + updateBackground: (color: string, alpha?: number) => void; 19 + leftEye: Eye; 20 + rightEye: Eye; 21 + mouth: Curve; 22 + eyebrowLeft: Curve; 23 + eyebrowRight: Curve; 24 + furMouth: Curve; 25 + }> { 26 + const xScale = 0.4 * scale; 27 + const yScale = 1.5 * scale; 28 + const faceApp = new Application(); 39 29 40 - const xScale = 0.4; 41 - const yScale = 1.5; 42 - const app = new Application(); 30 + const mouthSettings = { 31 + x: 0, 32 + y: -130, 33 + stroke: 20, 34 + color: 0, 35 + width: 100, 36 + curve: -40 37 + }; 43 38 44 - await app.init({ 39 + await faceApp.init({ 45 40 width, 46 41 height, 47 42 background: 'black' 48 43 }); 49 44 50 - background = new Graphics(); 45 + const background = new Graphics(); 51 46 background.rect(0, 0, width, height); 52 47 background.fill({ color: '#be185d' }); 53 48 background.alpha = 0.7; 54 - app.stage.addChild(background); 49 + faceApp.stage.addChild(background); 55 50 56 51 const container = new Container(); 57 52 container.x = width * 0.5; 58 53 container.y = height * 0.625; 59 54 container.scale.set(xScale, yScale); 60 - app.stage.addChild(container); 55 + faceApp.stage.addChild(container); 61 56 62 - rightEye = new Eye({ 57 + const rightEye = new Eye({ 63 58 x: 75, 64 59 y: 0, 65 60 size: 50 66 61 }); 67 62 68 - leftEye = new Eye({ 63 + const leftEye = new Eye({ 69 64 x: -75, 70 65 y: 0, 71 66 size: 50 ··· 74 69 container.addChild(rightEye.container); 75 70 container.addChild(leftEye.container); 76 71 77 - mouth = new Curve(mouthSettings); 72 + const mouth = new Curve(mouthSettings); 78 73 container.addChild(mouth.graphics); 79 74 80 - return { 81 - app 82 - }; 83 - } 75 + const furApp = new Application(); 84 76 85 - export async function createFurTexture({ 86 - width = 512, 87 - height = 512 88 - }: { 89 - width?: number; 90 - height?: number; 91 - }): Promise<{ app: Application<Renderer> }> { 92 - const xScale = 0.4; 93 - const yScale = 1.5; 94 - const app = new Application(); 95 - 96 - await app.init({ 97 - width, 98 - height, 99 - backgroundAlpha: 0.0, 100 - antialias: true 77 + await furApp.init({ 78 + width: width, 79 + height: height, 80 + backgroundAlpha: 0.0 101 81 }); 102 82 103 - const container = new Container(); 104 - container.x = width * 0.5; 105 - container.y = height * 0.625; 106 - container.scale.set(xScale, yScale); 107 - app.stage.addChild(container); 83 + const furContainer = new Container(); 84 + furContainer.x = width * 0.5; 85 + furContainer.y = height * 0.625; 86 + furContainer.scale.set(xScale, yScale); 87 + furApp.stage.addChild(furContainer); 108 88 109 - const background = new Graphics(); 110 - background.rect(0, 0, width * 5, height * 5); 111 - background.pivot.set(width * 2.5, height * 2.5); 112 - background.fill({ color: 'black' }); 113 - container.addChild(background); 114 - 115 - // draw 50 random colored circles in random positions 116 - // for (let i = 0; i < 500; i++) { 117 - // const circle = new Graphics(); 118 - // circle.circle(0, 0, 1 + Math.random() * 20 + 10); 119 - // circle.x = (Math.random() * width * 0.8 - width * 0.4) / xScale; 120 - // circle.y = (Math.random() * height * 0.4 - height * 0.2) / yScale; 121 - 122 - // // if (Math.hypot(circle.x, circle.y) < 150) continue; 123 - 124 - // circle.fill({ 125 - // color: { 126 - // r: Math.random() * 256, 127 - // g: Math.random() * 256, 128 - // b: Math.random() * 256, 129 - // a: Math.random() 130 - // } 131 - // }); 132 - // container.addChild(circle); 133 - // } 89 + const furBackground = new Graphics(); 90 + furBackground.rect(0, 0, width / xScale, (height / yScale) * 2); 91 + furBackground.pivot.set(width / xScale / 2, height / yScale); 92 + furBackground.fill({ color: 'black' }); 93 + furContainer.addChild(furBackground); 134 94 135 95 // add circle mask 136 96 const circleMask = new Graphics(); ··· 138 98 circleMask.blendMode = 'erase'; 139 99 140 100 circleMask.fill({ color: '#ffffff' }); 141 - container.addChild(circleMask); 101 + furContainer.addChild(circleMask); 142 102 143 103 for (let i = 0; i < 21; i++) { 144 - // add circle 145 104 const circle = new Graphics(); 146 105 circle.circle(0, 0, 5 * i - 2.5); 147 106 circle.stroke({ color: { r: 0, g: 0, b: 0, a: Math.pow(i / 21, 10) + 0.7 }, width: 5 }); 148 - container.addChild(circle); 107 + furContainer.addChild(circle); 149 108 } 150 109 151 - // const circle = new Graphics(); 152 - // circle.circle(0, 0, 100); 153 - // circle.fill({ color: { r: 256, g: 256, b: 256, a: 0.65 } }); 154 - // container.addChild(circle); 155 - 156 110 const eyeMaskLeft = new Graphics(); 157 - eyeMaskLeft.circle(0, 0, 25); 158 - eyeMaskLeft.x = -75 / 2; 111 + eyeMaskLeft.circle(0, 0, 50); 112 + eyeMaskLeft.x = -75; 159 113 eyeMaskLeft.y = 0; 160 114 eyeMaskLeft.blendMode = 'erase'; 161 115 eyeMaskLeft.fill({ color: '#ffffff' }); 162 - container.addChild(eyeMaskLeft); 116 + furContainer.addChild(eyeMaskLeft); 163 117 164 118 const eyeMaskRight = new Graphics(); 165 - eyeMaskRight.circle(0, 0, 25); 166 - eyeMaskRight.x = 75 / 2; 119 + eyeMaskRight.circle(0, 0, 50); 120 + eyeMaskRight.x = 75; 167 121 eyeMaskRight.y = 0; 168 122 eyeMaskRight.blendMode = 'erase'; 169 123 eyeMaskRight.fill({ color: '#ffffff' }); 170 - container.addChild(eyeMaskRight); 124 + furContainer.addChild(eyeMaskRight); 171 125 172 - eyebrowLeft = new Curve({ 173 - x: 75 / 2, 174 - y: 40, 175 - width: 50, 176 - curve: 10, 126 + const eyebrowLeft = new Curve({ 127 + x: 75, 128 + y: 80, 129 + width: 100, 130 + curve: 20, 177 131 stroke: 10 178 132 }); 179 133 eyebrowLeft.graphics.alpha = 0.5; 180 - container.addChild(eyebrowLeft.graphics); 134 + furContainer.addChild(eyebrowLeft.graphics); 181 135 182 - eyebrowRight = new Curve({ 183 - x: -75 / 2, 184 - y: 40, 185 - width: 50, 186 - curve: 10, 136 + const eyebrowRight = new Curve({ 137 + x: -75, 138 + y: 80, 139 + width: 100, 140 + curve: 20, 187 141 stroke: 10 188 142 }); 189 143 eyebrowRight.graphics.alpha = 0.5; 190 - container.addChild(eyebrowRight.graphics); 144 + furContainer.addChild(eyebrowRight.graphics); 191 145 192 - furMouth = new Curve({ 193 - ...mouthSettings, 194 - scale: 0.5 146 + const furMouth = new Curve({ 147 + ...mouthSettings 195 148 }); 196 149 197 150 furMouth.graphics.blendMode = 'erase'; 198 - container.addChild(furMouth.graphics); 151 + furContainer.addChild(furMouth.graphics); 199 152 200 153 return { 201 - app 154 + faceCanvas: faceApp.canvas, 155 + furCanvas: furApp.canvas, 156 + lookAt: (x: number, y: number) => { 157 + leftEye?.lookAt(x, y); 158 + rightEye?.lookAt(x, y); 159 + }, 160 + update: (dt: number) => { 161 + eyebrowLeft.update(dt); 162 + eyebrowRight.update(dt); 163 + 164 + mouth.update(dt); 165 + furMouth.update(dt); 166 + 167 + leftEye.update(dt); 168 + rightEye.update(dt); 169 + }, 170 + updateBackground: (color: string, alpha: number | undefined = undefined) => { 171 + background.clear(); 172 + background.rect(0, 0, width, height); 173 + background.fill({ color }); 174 + if (alpha) { 175 + background.alpha = alpha; 176 + } 177 + }, 178 + leftEye, 179 + rightEye, 180 + mouth, 181 + eyebrowLeft, 182 + eyebrowRight, 183 + furMouth 202 184 }; 203 - } 204 - 205 - export { leftEye, rightEye, mouth, eyebrowLeft, eyebrowRight, furMouth }; 206 - 207 - export function updateBackground(color: string) { 208 - background.clear(); 209 - background.rect(0, 0, size.width, size.height); 210 - background.fill({ color }); 211 - } 212 - 213 - export function updateFace(dt: number, x: number, y: number) { 214 - leftEye?.lookAt(x, y); 215 - rightEye?.lookAt(x, y); 216 - 217 - eyebrowLeft?.update(dt); 218 - eyebrowRight?.update(dt); 219 - 220 - mouth?.update(dt); 221 - furMouth?.update(dt); 222 - 223 - leftEye?.update(dt); 224 - rightEye?.update(dt); 225 185 }