The snippet below can be loaded into https://amoshydra.github.io/CodeVinci
Last active
May 25, 2026 18:46
-
-
Save amoshydra/beee25fce3fc0d17be4bc2b146a08660 to your computer and use it in GitHub Desktop.
CodeVinci - Snippets
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| document.body.insertAdjacentHTML("beforeend", ` | |
| <style> | |
| body { | |
| font-family: system-ui, -apple-system, sans-serif; | |
| padding: 2rem; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| background-color: #f8f9fa; | |
| } | |
| form { | |
| background: white; | |
| padding: 2rem; | |
| border-radius: 12px; | |
| box-shadow: 0 4px 6px rgba(0,0,0,0.1); | |
| width: 100%; | |
| max-width: 400px; | |
| } | |
| h2 { margin-top: 0; font-size: 1.25rem; } | |
| .instructions { font-size: 0.85rem; color: #666; margin-bottom: 1.5rem; } | |
| button[type="submit"] { | |
| margin-top: 1rem; | |
| width: 100%; | |
| padding: 0.5rem; | |
| cursor: pointer; | |
| } | |
| </style> | |
| <form id="myForm"> | |
| <h2>Custom Component Form</h2> | |
| <p class="instructions">Open the <strong>Console</strong> to see the events fire.</p> | |
| <clearable-input id="myInput" name="username" placeholder="Type something..."></clearable-input> | |
| <button type="submit">Submit</button> | |
| </form> | |
| `); | |
| class ClearableInput extends HTMLElement { | |
| constructor() { | |
| super(); | |
| this.attachShadow({ mode: 'open' }); | |
| this.shadowRoot.innerHTML = ` | |
| <style> | |
| :host { | |
| display: inline-flex; | |
| align-items: center; | |
| border: 1px solid #ccc; | |
| border-radius: 6px; | |
| padding: 4px 10px; | |
| background: white; | |
| transition: border-color 0.2s; | |
| } | |
| :host(:focus-within) { | |
| border-color: #007bff; | |
| outline: 2px solid rgba(0,123,255,0.25); | |
| } | |
| input { | |
| border: none; | |
| outline: none; | |
| font-size: 1rem; | |
| flex: 1; | |
| padding: 4px 0; | |
| width: 100%; | |
| } | |
| button { | |
| background: #efefef; | |
| border: none; | |
| border-radius: 50%; | |
| width: 22px; | |
| height: 22px; | |
| margin-left: 8px; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: #666; | |
| font-weight: bold; | |
| user-select: none; | |
| } | |
| button:hover { background: #e0e0e0; color: #333; } | |
| </style> | |
| <input type="text" /> | |
| <button type="button" id="clearBtn" aria-label="Clear input">✕</button> | |
| `; | |
| this._input = this.shadowRoot.querySelector('input'); | |
| this._clearBtn = this.shadowRoot.querySelector('#clearBtn'); | |
| } | |
| connectedCallback() { | |
| // Initial attribute sync | |
| this._input.placeholder = this.getAttribute('placeholder') || ''; | |
| this._input.value = this.getAttribute('value') || ''; | |
| // 1. CLEAR BUTTON LOGIC | |
| this._clearBtn.addEventListener('click', () => { | |
| if (this._input.value !== '') { | |
| this._input.value = ''; | |
| this._input.focus(); | |
| // Manually trigger events since the change was programatic | |
| this.dispatchEvent(new Event('input', { bubbles: true, composed: true })); | |
| this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); | |
| } | |
| }); | |
| // 2. THE 'CHANGE' BRIDGE | |
| // Native 'change' is NOT composed (won't cross shadow boundary). | |
| // We catch it inside and fire a composed version from the host. | |
| this._input.addEventListener('change', (e) => { | |
| this.dispatchEvent(new Event('change', { bubbles: true, composed: true })); | |
| }); | |
| // NOTE: 'input' events ARE naturally composed, so they bubble out | |
| // on their own. We don't need a bridge for them, preventing double-fires. | |
| } | |
| // Expose value so that event.target.value works in external scripts | |
| get value() { return this._input.value; } | |
| set value(val) { this._input.value = val; } | |
| // Support standard attribute reflection if needed | |
| static get observedAttributes() { return ['placeholder', 'value']; } | |
| attributeChangedCallback(name, oldVal, newVal) { | |
| if (this._input && oldVal !== newVal) { | |
| this._input[name] = newVal; | |
| } | |
| } | |
| } | |
| // Register component | |
| customElements.define('clearable-input', ClearableInput); | |
| // --- EVENT LISTENERS (External) --- | |
| const inputComponent = document.getElementById('myInput'); | |
| // Test oninput | |
| inputComponent.oninput = (e) => { | |
| console.log('INPUT EVENT:', { | |
| value: e.target.value, | |
| type: e.type, | |
| target: e.target.tagName | |
| }); | |
| }; | |
| // Test onchange | |
| inputComponent.onchange = (e) => { | |
| console.log('CHANGE EVENT (Fires on blur or clear):', { | |
| value: e.target.value | |
| }); | |
| }; | |
| // Form Submit handling | |
| document.getElementById('myForm').onsubmit = (e) => { | |
| e.preventDefault(); | |
| console.log('FORM SUBMITTED. Component Value:', inputComponent.value); | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| document.body.insertAdjacentHTML("beforeend", ` | |
| <style> | |
| body { | |
| background: linear-gradient(88.26deg, rgba(238, 242, 245, 0.75) 2.62%, rgba(199, 207, 213, 0.75) 105.19%); | |
| } | |
| </style> | |
| `); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import 'https://esm.sh/tailwindcss@4.0.0'; | |
| import * as THREE from 'https://esm.sh/three@0.128.0'; | |
| import { OrbitControls } from 'https://esm.sh/three@0.128.0/examples/jsm/controls/OrbitControls.js'; | |
| import gsap from 'https://esm.sh/gsap@3.12.2'; | |
| // --- Configuration & State --- | |
| const config = { | |
| shadows: true, | |
| pose: 'static', // static, idle, mouse | |
| mouseTarget: new THREE.Vector2(), | |
| windowHalfX: window.innerWidth / 2, | |
| windowHalfY: window.innerHeight / 2 | |
| }; | |
| let scene, camera, renderer, controls; | |
| let mannequin, ground; | |
| let clock = new THREE.Clock(); | |
| let targetHeadRotation = { x: 0, y: 0 }; | |
| // --- Initialization --- | |
| function init() { | |
| const container = document.getElementById('canvas-container'); | |
| // Scene | |
| scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0xf2e6e0); // Warm tint | |
| scene.fog = new THREE.Fog(0xf2e6e0, 20, 60); | |
| // Camera | |
| camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000); | |
| camera.position.set(5, 10, 20); // Angled down view like reference | |
| // Renderer | |
| renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); | |
| renderer.shadowMap.enabled = true; | |
| renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
| renderer.toneMapping = THREE.ACESFilmicToneMapping; | |
| renderer.toneMappingExposure = 1.1; | |
| container.appendChild(renderer.domElement); | |
| // Controls | |
| controls = new OrbitControls(camera, renderer.domElement); | |
| controls.enableDamping = true; | |
| controls.dampingFactor = 0.05; | |
| controls.minDistance = 5; | |
| controls.maxDistance = 20; | |
| controls.maxPolarAngle = Math.PI / 2 - 0.05; // Prevent going below ground | |
| // Lighting | |
| setupLighting(); | |
| // Build Character | |
| mannequin = createMannequin(); | |
| scene.add(mannequin.mesh); | |
| // Environment | |
| createEnvironment(); | |
| // Listeners | |
| window.addEventListener('resize', onWindowResize); | |
| document.addEventListener('mousemove', onDocumentMouseMove); | |
| // Hide Loader | |
| document.getElementById('loader').style.display = 'none'; | |
| // Start Loop | |
| animate(); | |
| } | |
| function setupLighting() { | |
| // Ambient - Warm fill | |
| const ambientLight = new THREE.AmbientLight(0xffeadd, 0.6); | |
| scene.add(ambientLight); | |
| // Key Light - Top Right, Warm, Sharp shadows | |
| const keyLight = new THREE.DirectionalLight(0xffdcb4, 1.2); | |
| keyLight.position.set(5, 10, 5); | |
| keyLight.castShadow = true; | |
| keyLight.shadow.mapSize.width = 2048; | |
| keyLight.shadow.mapSize.height = 2048; | |
| keyLight.shadow.camera.near = 0.5; | |
| keyLight.shadow.camera.far = 30; | |
| keyLight.shadow.bias = -0.001; | |
| // Soften shadow edges | |
| keyLight.shadow.radius = 4; | |
| // Adjust shadow camera box to fit the character tightly | |
| const d = 5; | |
| keyLight.shadow.camera.left = -d; | |
| keyLight.shadow.camera.right = d; | |
| keyLight.shadow.camera.top = d; | |
| keyLight.shadow.camera.bottom = -d; | |
| scene.add(keyLight); | |
| // Rim/Back Light - Cooler, subtle | |
| const rimLight = new THREE.DirectionalLight(0xcceeff, 0.5); | |
| rimLight.position.set(-5, 5, -5); | |
| scene.add(rimLight); | |
| // Point light for softer general reflection | |
| const pointLight = new THREE.PointLight(0xffaa88, 0.5, 20); | |
| pointLight.position.set(0, 5, 5); | |
| scene.add(pointLight); | |
| } | |
| function createEnvironment() { | |
| // Ground Plane | |
| const planeGeometry = new THREE.PlaneGeometry(100, 100); | |
| const planeMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0xf0e0d5, | |
| roughness: 0.8, | |
| metalness: 0.1 | |
| }); | |
| const plane = new THREE.Mesh(planeGeometry, planeMaterial); | |
| plane.rotation.x = -Math.PI / 2; | |
| plane.receiveShadow = true; | |
| scene.add(plane); | |
| // Grid Helper (Optional, but reference looks like smooth floor. Let's keep it clean) | |
| // Instead, let's add a soft circle under the character to fake AO/Contact shadow | |
| const shadowPlaneGeo = new THREE.PlaneGeometry(3, 3); | |
| const shadowPlaneMat = new THREE.MeshBasicMaterial({ | |
| color: 0x000000, | |
| transparent: true, | |
| opacity: 0.15 | |
| }); | |
| // Create a soft texture for the shadow circle procedurally | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = 128; | |
| canvas.height = 128; | |
| const context = canvas.getContext('2d'); | |
| const gradient = context.createRadialGradient(64, 64, 0, 64, 64, 64); | |
| gradient.addColorStop(0, 'rgba(0,0,0,1)'); | |
| gradient.addColorStop(0.5, 'rgba(0,0,0,0.5)'); | |
| gradient.addColorStop(1, 'rgba(0,0,0,0)'); | |
| context.fillStyle = gradient; | |
| context.fillRect(0, 0, 128, 128); | |
| const shadowTexture = new THREE.CanvasTexture(canvas); | |
| shadowPlaneMat.map = shadowTexture; | |
| shadowPlaneMat.alphaMap = shadowTexture; // Use alpha map for transparency | |
| shadowPlaneMat.alphaTest = 0.01; | |
| const shadowPlane = new THREE.Mesh(shadowPlaneGeo, shadowPlaneMat); | |
| shadowPlane.rotation.x = -Math.PI / 2; | |
| shadowPlane.position.y = 0.01; // Just above ground | |
| scene.add(shadowPlane); | |
| } | |
| // --- Mannequin Construction --- | |
| // Helper to create a pivot joint + mesh | |
| function createLimb(width, height, depth, color, x, y, z) { | |
| const group = new THREE.Group(); | |
| group.position.set(x, y, z); | |
| const geometry = new THREE.BoxGeometry(width, height, depth); | |
| const material = new THREE.MeshStandardMaterial({ | |
| color: color, | |
| roughness: 0.7, // Clay/Wood look | |
| metalness: 0.1 | |
| }); | |
| const mesh = new THREE.Mesh(geometry, material); | |
| // Offset mesh so the group origin is at the top/connection point | |
| mesh.position.y = -height / 2; | |
| mesh.castShadow = true; | |
| mesh.receiveShadow = true; | |
| group.add(mesh); | |
| return { group, mesh, height }; | |
| } | |
| const INITIAL_CHEST_POSITION_Y = 1.25; | |
| function createMannequin() { | |
| const root = new THREE.Group(); | |
| const skinColor = 0xeecfa1; // Warm beige | |
| const jointColor = 0xd4b483; | |
| // --- Hips/Pelvis --- | |
| // Custom shape for pelvis using a slightly tapered cylinder or scaled box | |
| const pelvisGeo = new THREE.BoxGeometry(1, 0.6, 0.7); | |
| // Taper the top vertices to create the waist narrowing | |
| const posAttribute = pelvisGeo.attributes.position; | |
| for (let i = 0; i < posAttribute.count; i++) { | |
| const y = posAttribute.getY(i); | |
| if (y > 0) { // Top vertices | |
| const x = posAttribute.getX(i); | |
| const z = posAttribute.getZ(i); | |
| posAttribute.setX(i, x * 0.8); | |
| posAttribute.setZ(i, z * 0.8); | |
| } | |
| } | |
| pelvisGeo.computeVertexNormals(); | |
| const pelvisMat = new THREE.MeshStandardMaterial({ color: skinColor, roughness: 0.6 }); | |
| const pelvis = new THREE.Mesh(pelvisGeo, pelvisMat); | |
| pelvis.position.y = 2.8; | |
| pelvis.castShadow = true; | |
| pelvis.receiveShadow = true; | |
| root.add(pelvis); | |
| // --- Spine / Torso --- | |
| const spineGroup = new THREE.Group(); | |
| spineGroup.position.set(0, 0.3, 0); // Top of pelvis | |
| pelvis.add(spineGroup); | |
| const waist = createLimb(0.7, 0.4, 0.55, skinColor, 0, 0, 0); | |
| waist.group.position.y = 0; // Relative to spineGroup | |
| spineGroup.add(waist.group); | |
| const chest = createLimb(1.25, 1.3, 0.85, skinColor, 0, INITIAL_CHEST_POSITION_Y, 0); | |
| // Shape chest box to be trapezoidal (Broad shoulders, narrow waist) | |
| // We can simulate this by scaling the box and rotating shoulders out, or just a simple box for now. | |
| // Let's adjust the box geometry vertices for a more "mannequin" look. | |
| const chestGeo = chest.mesh.geometry; | |
| const chestPos = chestGeo.attributes.position; | |
| for(let i=0; i<chestPos.count; i++){ | |
| const y = chestPos.getY(i); | |
| const x = chestPos.getX(i); | |
| if (y > 0) { // Top is wider | |
| chestPos.setX(i, x * 1.2); | |
| } | |
| } | |
| chestGeo.computeVertexNormals(); | |
| chest.mesh.castShadow = true; | |
| chest.mesh.receiveShadow = true; | |
| waist.group.add(chest.group); | |
| // --- Neck --- | |
| const neck = createLimb(0.3, 0.3, 0.3, skinColor, 0, 0.25, 0); | |
| chest.group.add(neck.group); | |
| // --- Head --- | |
| const headGroup = new THREE.Group(); | |
| headGroup.position.set(0, 0, 0); | |
| neck.group.add(headGroup); | |
| const headGeo = new THREE.BoxGeometry(0.8, 0.9, 0.9); | |
| const head = new THREE.Mesh(headGeo, pelvisMat); | |
| head.position.y = 0.45; // Center head on pivot | |
| head.castShadow = true; | |
| head.receiveShadow = true; | |
| headGroup.add(head); | |
| // --- Arms --- | |
| function createArm(side) { // side: 1 for left, -1 for right | |
| const shoulderX = side * 1.1; | |
| // Shoulder Joint | |
| const shoulderGeo = new THREE.SphereGeometry(0.25, 16, 16); | |
| const shoulder = new THREE.Mesh(shoulderGeo, pelvisMat); | |
| shoulder.position.set(shoulderX, 0.0, 0); // Relative to Chest | |
| chest.group.add(shoulder); | |
| // Upper Arm | |
| const upperArm = createLimb(0.35, 1.2, 0.35, skinColor, 0, 0, 0); | |
| upperArm.group.position.set(0, 0, 0); // Relative to Shoulder Joint | |
| shoulder.add(upperArm.group); | |
| // Elbow Joint | |
| const elbowGeo = new THREE.SphereGeometry(0.2, 16, 16); | |
| const elbow = new THREE.Mesh(elbowGeo, pelvisMat); | |
| elbow.position.set(0, -1.2, 0); // Relative to Upper Arm | |
| upperArm.group.add(elbow); | |
| // Forearm | |
| const foreArm = createLimb(0.3, 1.1, 0.3, skinColor, 0, 0, 0); | |
| foreArm.group.position.set(0, 0, 0); // Relative to Elbow | |
| elbow.add(foreArm.group); | |
| // Hand | |
| const handGeo = new THREE.BoxGeometry(0.25, 0.35, 0.1); | |
| const hand = new THREE.Mesh(handGeo, pelvisMat); | |
| hand.position.set(0, -1.1, 0); | |
| hand.castShadow = true; | |
| foreArm.group.add(hand); | |
| return { shoulder, upperArm, elbow, foreArm, hand }; | |
| } | |
| const leftArm = createArm(1); | |
| const rightArm = createArm(-1); | |
| // --- Legs --- | |
| function createLeg(side) { | |
| const hipX = side * 0.35; | |
| // Hip Joint (Sphere) | |
| const hipJointGeo = new THREE.SphereGeometry(0.3, 16, 16); | |
| const hipJoint = new THREE.Mesh(hipJointGeo, pelvisMat); | |
| hipJoint.position.set(hipX, -0.2, 0); // Relative to Pelvis bottom | |
| pelvis.add(hipJoint); | |
| // Upper Leg (Thigh) | |
| const thigh = createLimb(0.45, 1.4, 0.45, skinColor, 0, 0, 0); | |
| thigh.group.position.set(0, 0, 0); | |
| hipJoint.add(thigh.group); | |
| // Knee | |
| const kneeGeo = new THREE.SphereGeometry(0.25, 16, 16); | |
| const knee = new THREE.Mesh(kneeGeo, pelvisMat); | |
| knee.position.set(0, -1.4, 0); | |
| thigh.group.add(knee); | |
| // Lower Leg (Shin) | |
| const shin = createLimb(0.35, 1.5, 0.35, skinColor, 0, 0, 0); | |
| shin.group.position.set(0, 0, 0); | |
| knee.add(shin.group); | |
| // Foot | |
| const footGeo = new THREE.BoxGeometry(0.4, 0.15, 0.8); | |
| const foot = new THREE.Mesh(footGeo, pelvisMat); | |
| foot.position.set(0, -1.5, 0.2); // Offset forward | |
| foot.castShadow = true; | |
| shin.group.add(foot); | |
| return { hipJoint, thigh, knee, shin, foot }; | |
| } | |
| const leftLeg = createLeg(1); | |
| const rightLeg = createLeg(-1); | |
| // --- Initial Pose (The Reference Image Pose) --- | |
| // Head tilt | |
| headGroup.rotation.x = 0.1; // Look slightly up/forward | |
| headGroup.rotation.y = 0.2; // Turn head slightly left | |
| // Spine rotation/twist | |
| chest.group.rotation.y = 0.2; // Twist torso slightly | |
| // Right Arm (on hip) | |
| rightArm.shoulder.rotation.z = Math.PI / 4; // Raise shoulder slightly | |
| rightArm.shoulder.rotation.x = 0.2; // Forward slightly | |
| rightArm.upperArm.group.rotation.z = -Math.PI / 4; // Bring arm out | |
| rightArm.upperArm.group.rotation.x = 0.3; // Forward | |
| rightArm.elbow.rotation.z = Math.PI / 1.8; // Bend elbow heavily (Forearm parallel to ground approx) | |
| rightArm.elbow.rotation.y = 0.5; // Bring hand in towards body | |
| rightArm.foreArm.group.rotation.z = 0.2; // Adjust angle | |
| // Left Arm (hanging down) | |
| leftArm.shoulder.rotation.z = -0.2; // Relaxed | |
| leftArm.elbow.rotation.x = 0.1; // Slight bend | |
| // Legs | |
| leftLeg.hipJoint.rotation.z = -0.05; | |
| leftLeg.hipJoint.rotation.x = 0.05; | |
| leftLeg.knee.rotation.x = 0.05; | |
| rightLeg.hipJoint.rotation.z = 0.05; | |
| rightLeg.knee.rotation.x = 0.1; | |
| // Return structure with references for animation | |
| return { | |
| mesh: root, | |
| parts: { | |
| head: headGroup, | |
| neck: neck.group, | |
| chest: chest.group, | |
| pelvis: pelvis, | |
| leftArm: leftArm, | |
| rightArm: rightArm, | |
| leftLeg: leftLeg, | |
| rightLeg: rightLeg | |
| } | |
| }; | |
| } | |
| // --- Logic & Animation --- | |
| function setPose(type) { | |
| config.pose = type; | |
| // Reset active buttons | |
| document.querySelectorAll('.btn').forEach(b => b.classList.remove('active')); | |
| if(type === 'static') { | |
| // Reset to static reference pose | |
| gsap.to(mannequin.parts.head.rotation, { x: 0.1, y: 0.2, duration: 1 }); | |
| gsap.to(mannequin.parts.chest.rotation, { y: 0.2, duration: 1 }); | |
| // Right Arm | |
| gsap.to(mannequin.parts.rightArm.shoulder.rotation, { z: Math.PI/4, x: 0.2, duration: 1 }); | |
| gsap.to(mannequin.parts.rightArm.upperArm.group.rotation, { z: -Math.PI/4, x: 0.3, duration: 1 }); | |
| gsap.to(mannequin.parts.rightArm.elbow.rotation, { z: Math.PI/1.8, y: 0.5, duration: 1 }); | |
| // Left Arm | |
| gsap.to(mannequin.parts.leftArm.shoulder.rotation, { z: -0.2, duration: 1 }); | |
| gsap.to(mannequin.parts.leftArm.elbow.rotation, { x: 0.1, duration: 1 }); | |
| event.target.classList.add('active'); | |
| } | |
| else if (type === 'idle') { | |
| // Just ensure base positions are set for idle to take over smoothly | |
| // Arms hang loosely | |
| gsap.to(mannequin.parts.leftArm.shoulder.rotation, { x: 0, z: 0.1, duration: 1 }); | |
| gsap.to(mannequin.parts.rightArm.shoulder.rotation, { x: 0, z: -0.1, duration: 1 }); | |
| gsap.to(mannequin.parts.leftArm.elbow.rotation, { x: 0.1, duration: 1 }); | |
| gsap.to(mannequin.parts.rightArm.elbow.rotation, { x: 0.1, duration: 1 }); | |
| event.target.classList.add('active'); | |
| } | |
| else if (type === 'mouse') { | |
| // Arms hang loosely but ready | |
| gsap.to(mannequin.parts.leftArm.shoulder.rotation, { x: 0, z: 0.1, duration: 1 }); | |
| gsap.to(mannequin.parts.rightArm.shoulder.rotation, { x: 0, z: -0.1, duration: 1 }); | |
| event.target.classList.add('active'); | |
| } | |
| } | |
| function toggleShadows() { | |
| config.shadows = !config.shadows; | |
| renderer.shadowMap.enabled = config.shadows; | |
| scene.traverse(child => { | |
| if (child.material) child.material.needsUpdate = true; | |
| }); | |
| } | |
| function onDocumentMouseMove(event) { | |
| config.mouseTarget.x = (event.clientX - config.windowHalfX) / 2; | |
| config.mouseTarget.y = (event.clientY - config.windowHalfY) / 2; | |
| } | |
| function onWindowResize() { | |
| config.windowHalfX = window.innerWidth / 2; | |
| config.windowHalfY = window.innerHeight / 2; | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| } | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| const delta = clock.getDelta(); | |
| const time = clock.getElapsedTime(); | |
| // Idle Animation (Breathing) | |
| if (config.pose === 'idle') { | |
| const breath = Math.sin(time * 2) * 0.08; | |
| mannequin.parts.chest.rotation.x = breath * 0.5; | |
| mannequin.parts.chest.position.y = INITIAL_CHEST_POSITION_Y + breath; | |
| // Subtle Sway | |
| mannequin.mesh.rotation.y = Math.sin(time * 0.5) * 0.05; | |
| // Arm Sway | |
| mannequin.parts.leftArm.shoulder.rotation.z = 0.1 + Math.sin(time * 2 + 1) * 0.02; | |
| mannequin.parts.rightArm.shoulder.rotation.z = -0.1 - Math.sin(time * 2) * 0.02; | |
| } | |
| // Mouse Look Logic | |
| if (config.pose === 'mouse') { | |
| // Smoothly interpolate current rotation to target | |
| const targetX = THREE.MathUtils.clamp(config.mouseTarget.y * -0.001, -0.5, 0.5); | |
| const targetY = THREE.MathUtils.clamp(config.mouseTarget.x * 0.001, -0.8, 0.8); | |
| mannequin.parts.head.rotation.x += (targetX - mannequin.parts.head.rotation.x) * 0.1; | |
| mannequin.parts.head.rotation.y += (-targetY - mannequin.parts.head.rotation.y) * 0.1; | |
| // Torso follows slightly | |
| mannequin.parts.chest.rotation.y += (-targetY * 0.3 - mannequin.parts.chest.rotation.y) * 0.05; | |
| } | |
| controls.update(); | |
| renderer.render(scene, camera); | |
| } | |
| document.body.insertAdjacentHTML("beforeend", ` | |
| <style> | |
| body { margin: 0; overflow: hidden; background-color: #2a2a2a; font-family: 'Inter', sans-serif; } | |
| #canvas-container { width: 100vw; height: 100vh; display: block; } | |
| .ui-overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: space-between; | |
| padding: 2rem; | |
| } | |
| .ui-panel { | |
| pointer-events: auto; | |
| background: rgba(0, 0, 0, 0.4); | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| border-radius: 1rem; | |
| padding: 1.5rem; | |
| color: white; | |
| max-width: 320px; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.2); | |
| transition: all 0.3s ease; | |
| } | |
| .btn { | |
| background: rgba(255,255,255,0.1); | |
| border: 1px solid rgba(255,255,255,0.2); | |
| color: white; | |
| padding: 0.5rem 1rem; | |
| border-radius: 0.5rem; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| font-size: 0.9rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| font-weight: 600; | |
| margin-top: 0.5rem; | |
| } | |
| .btn:hover { background: white; color: black; } | |
| .btn.active { background: white; color: black; } | |
| /* Loading Spinner */ | |
| #loader { | |
| position: absolute; | |
| top: 50%; left: 50%; | |
| transform: translate(-50%, -50%); | |
| color: white; | |
| font-size: 1.5rem; | |
| letter-spacing: 0.2em; | |
| animation: pulse 1.5s infinite; | |
| } | |
| @keyframes pulse { 0% { opacity: 0.5; } 50% { opacity: 1; } 100% { opacity: 0.5; } } | |
| </style> | |
| <div id="loader">INITIALIZING SCENE</div> | |
| <div id="canvas-container"></div> | |
| <div class="ui-overlay"> | |
| <div class="ui-panel"> | |
| <h1 class="text-2xl font-bold mb-2">Mannequin Viz</h1> | |
| <p class="text-sm text-gray-300 mb-4">Interactive procedural character recreation based on reference imagery.</p> | |
| <div class="flex flex-col gap-2"> | |
| <div class="text-xs uppercase tracking-widest text-gray-400 mb-1">Pose Control</div> | |
| <button class="btn active" data-btn="setPose" data-btn-value="static">Static Reference</button> | |
| <button class="btn" data-btn="setPose" data-btn-value="idle">Idle / Breathe</button> | |
| <button class="btn" data-btn="setPose" data-btn-value="mouse">Mouse Look</button> | |
| </div> | |
| <div class="mt-4 flex flex-col gap-2"> | |
| <div class="text-xs uppercase tracking-widest text-gray-400 mb-1">Lighting</div> | |
| <button class="btn" data-btn="toggleShadows">Toggle Shadows</button> | |
| </div> | |
| </div> | |
| <div class="ui-panel self-end text-right"> | |
| <p class="text-xs text-gray-400">Drag to Rotate • Scroll to Zoom</p> | |
| </div> | |
| </div> | |
| `); | |
| document.querySelectorAll("button[data-btn]").forEach((element) => { | |
| if (element.dataset.btn === "setPose") { | |
| element.addEventListener("click", () => { | |
| setPose(element.dataset.btnValue) | |
| }) | |
| } | |
| if (element.dataset.btn === "toggleShadows") { | |
| element.addEventListener("click", () => { | |
| setPose(element.dataset.btnValue) | |
| }) | |
| } | |
| }) | |
| // Initialize | |
| init(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import * as THREE from "https://esm.sh/three@0.182.0"; | |
| import gsap from "https://esm.sh/gsap@3.14.2"; | |
| document.body.insertAdjacentHTML("beforeend", ` | |
| <style> | |
| body { margin: 0; overflow: hidden; background: #111; font-family: 'Segoe UI', sans-serif; } | |
| .controls { | |
| position: absolute; bottom: 30px; left: 50%; transform: translateX(-50%); | |
| display: flex; gap: 10px; background: rgba(0,0,0,0.8); padding: 12px; border-radius: 50px; | |
| } | |
| button { | |
| padding: 10px 18px; border: none; border-radius: 20px; cursor: pointer; | |
| background: #333; color: white; transition: 0.2s; font-size: 12px; | |
| } | |
| button.active { background: #0088ff; box-shadow: 0 0 10px #0088ff; } | |
| #hud { position: absolute; top: 20px; left: 20px; color: #0088ff; font-weight: bold; text-transform: uppercase; letter-spacing: 2px; } | |
| #help { position: absolute; top: 20px; right: 20px; color: #666; text-align: right; font-size: 13px; } | |
| </style> | |
| <div id="hud">Status: Idle</div> | |
| <div id="help">WASD to Move<br>SHIFT to Run<br>SPACE to Attack</div> | |
| <div class="controls"> | |
| <button onclick="setState('SIT')" id="btn-SIT">Sit Down</button> | |
| <button onclick="setState('IDLE')" id="btn-IDLE">Reset</button> | |
| </div> | |
| `); | |
| // --- SCENE SETUP --- | |
| const scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x050505); | |
| scene.fog = new THREE.Fog(0x050505, 5, 15); | |
| const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100); | |
| const renderer = new THREE.WebGLRenderer({ antialias: true }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.shadowMap.enabled = true; | |
| document.body.appendChild(renderer.domElement); | |
| const ambient = new THREE.AmbientLight(0xffffff, 0.4); | |
| const sun = new THREE.DirectionalLight(0xffffff, 1.2); | |
| sun.position.set(5, 10, 5); | |
| sun.castShadow = true; | |
| scene.add(ambient, sun); | |
| // Floor Grid | |
| const grid = new THREE.GridHelper(50, 50, 0x222222, 0x111111); | |
| scene.add(grid); | |
| const floor = new THREE.Mesh(new THREE.PlaneGeometry(50, 50), new THREE.MeshStandardMaterial({ color: 0x050505 })); | |
| floor.rotation.x = -Math.PI / 2; | |
| floor.receiveShadow = true; | |
| scene.add(floor); | |
| // --- CHARACTER SYSTEM ---\ | |
| const player = new THREE.Group(); | |
| scene.add(player); | |
| function createLimb(radius, height, color = 0xE8BEAC) { | |
| const pivot = new THREE.Group(); | |
| const mesh = new THREE.Mesh( | |
| new THREE.CapsuleGeometry(radius, height, 4, 10), | |
| new THREE.MeshStandardMaterial({ color, roughness: 0.7 }) | |
| ); | |
| mesh.position.y = -height / 2; | |
| mesh.castShadow = true; | |
| pivot.add(mesh); | |
| return pivot; | |
| } | |
| // Body creation | |
| const hips = new THREE.Group(); hips.position.y = 1; player.add(hips); | |
| const torso = createLimb(0.2, 0.45, 0x2955dd); torso.position.y = 0.65; hips.add(torso); | |
| const head = new THREE.Mesh(new THREE.SphereGeometry(0.14), new THREE.MeshStandardMaterial({ color: 0xE8BEAC })); | |
| head.position.y = 0.35; torso.add(head); | |
| const leftArm = createLimb(0.06, 0.4); leftArm.position.set(0.25, 0.1, 0); torso.add(leftArm); | |
| const leftForearm = createLimb(0.05, 0.35); leftForearm.position.y = -0.45; leftArm.add(leftForearm); | |
| const rightArm = createLimb(0.06, 0.4); rightArm.position.set(-0.25, 0.1, 0); torso.add(rightArm); | |
| const rightForearm = createLimb(0.05, 0.35); rightForearm.position.y = -0.45; rightArm.add(rightForearm); | |
| const leftThigh = createLimb(0.08, 0.45); leftThigh.position.set(0.15, 0, 0); hips.add(leftThigh); | |
| const rightThigh = createLimb(0.08, 0.45); rightThigh.position.set(-0.15, 0, 0); hips.add(rightThigh); | |
| const leftCalf = createLimb(0.07, 0.45); leftCalf.position.y = -0.5; leftThigh.add(leftCalf); | |
| const rightCalf = createLimb(0.07, 0.45); rightCalf.position.y = -0.5; rightThigh.add(rightCalf); | |
| // --- MOVEMENT LOGIC --- | |
| let state = 'IDLE'; | |
| const keys = { w: false, a: false, s: false, d: false, shift: false }; | |
| const velocity = new THREE.Vector3(); | |
| let targetRotation = 0; | |
| window.addEventListener('keydown', (e) => { | |
| const key = e.key.toLowerCase(); | |
| if (keys.hasOwnProperty(key)) keys[key] = true; | |
| if (e.code === 'Space') attack(); | |
| }); | |
| window.addEventListener('keyup', (e) => { | |
| const key = e.key.toLowerCase(); | |
| if (keys.hasOwnProperty(key)) keys[key] = false; | |
| }); | |
| window.setState = (s) => { | |
| state = s; | |
| document.getElementById('hud').innerText = `Status: ${s}`; | |
| if (s === 'SIT') { | |
| gsap.to(hips.position, { y: 0.45, duration: 0.5 }); | |
| gsap.to([leftThigh.rotation, rightThigh.rotation], { x: -Math.PI/2, duration: 0.5 }); | |
| gsap.to([leftCalf.rotation, rightCalf.rotation], { x: Math.PI/2, duration: 0.5 }); | |
| } else { | |
| gsap.to(hips.position, { y: 1, duration: 0.3 }); | |
| gsap.to([leftThigh.rotation, rightThigh.rotation, leftCalf.rotation, rightCalf.rotation], { x: 0, duration: 0.3 }); | |
| } | |
| }; | |
| // Sword Generation | |
| const sword = new THREE.Group(); | |
| const blade = new THREE.Mesh(new THREE.BoxGeometry(0.04, 0.7, 0.01), new THREE.MeshStandardMaterial({color: 0xffffff, metalness: 1})); | |
| blade.position.y = 0.4; sword.add(blade); | |
| const hilt = new THREE.Mesh(new THREE.BoxGeometry(0.15, 0.03, 0.04), new THREE.MeshStandardMaterial({color: 0x333333})); | |
| hilt.position.y = 0.1; sword.add(hilt); | |
| sword.rotation.x = Math.PI / 2; | |
| sword.position.y = -0.35; | |
| rightForearm.add(sword); | |
| function attack() { | |
| if (state === 'ATTACK') return; | |
| const prevState = state; | |
| state = 'ATTACK'; | |
| const tl = gsap.timeline({ onComplete: () => state = prevState }); | |
| tl.to(rightArm.rotation, { x: -Math.PI * 0.8, duration: 0.1 }) | |
| .to(rightArm.rotation, { x: 0, duration: 0.3, ease: "back.out" }); | |
| } | |
| // --- MAIN LOOP --- | |
| const clock = new THREE.Clock(); | |
| const camOffset = new THREE.Vector3(0, 3, 5); | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| const dt = clock.getDelta(); | |
| const t = clock.getElapsedTime(); | |
| // 1. Calculate Input | |
| let moveX = Number(keys.d) - Number(keys.a); | |
| let moveZ = Number(keys.s) - Number(keys.w); | |
| const isMoving = moveX !== 0 || moveZ !== 0; | |
| if (state !== 'SIT' && state !== 'ATTACK') { | |
| if (isMoving) { | |
| state = keys.shift ? 'RUN' : 'WALK'; | |
| // Determine rotation angle based on keys | |
| targetRotation = Math.atan2(moveX, moveZ); | |
| player.rotation.y = THREE.MathUtils.lerp(player.rotation.y, targetRotation, 0.15); | |
| // Move Forward relative to character facing | |
| const moveSpeed = state === 'RUN' ? 5 : 2.5; | |
| player.translateZ(moveSpeed * dt); | |
| } else { | |
| state = 'IDLE'; | |
| } | |
| } | |
| document.getElementById('hud').innerText = `Status: ${state}`; | |
| // 2. Procedural Animation Math | |
| if (state === 'WALK' || state === 'RUN') { | |
| const speedMult = state === 'RUN' ? 12 : 7; | |
| const amp = state === 'RUN' ? 0.8 : 0.5; | |
| leftThigh.rotation.x = Math.sin(t * speedMult) * amp; | |
| rightThigh.rotation.x = Math.sin(t * speedMult + Math.PI) * amp; | |
| leftCalf.rotation.x = Math.max(0, Math.sin(t * speedMult - 1) * amp * 1.2); | |
| rightCalf.rotation.x = Math.max(0, Math.sin(t * speedMult + Math.PI - 1) * amp * 1.2); | |
| leftArm.rotation.x = Math.sin(t * speedMult + Math.PI) * amp; | |
| if (state !== 'ATTACK') rightArm.rotation.x = Math.sin(t * speedMult) * amp; | |
| hips.position.y = 1 + Math.abs(Math.sin(t * speedMult)) * 0.05; | |
| } else if (state === 'IDLE') { | |
| hips.position.y = 1 + Math.sin(t * 2) * 0.02; | |
| leftArm.rotation.z = 0.1 + Math.sin(t * 2) * 0.02; | |
| rightArm.rotation.z = -0.1 - Math.sin(t * 2) * 0.02; | |
| } | |
| // 3. Smooth Follow Camera | |
| const targetCamPos = player.position.clone().add(camOffset); | |
| camera.position.lerp(targetCamPos, 0.05); | |
| camera.lookAt(player.position.x, player.position.y + 0.5, player.position.z); | |
| renderer.render(scene, camera); | |
| } | |
| animate(); | |
| window.addEventListener('resize', () => { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| document.body.insertAdjacentHTML("beforeend", ` | |
| <div id="hand"> | |
| <div id="origin"></div> | |
| <div id="marker"></div> | |
| </div> | |
| <div id="output"></div> | |
| <style> | |
| :root { | |
| color-scheme: dark; | |
| font-family: sans-serif; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| } | |
| body { | |
| margin: 0; | |
| width: 100%; | |
| height: 100%; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| cursor: ew-resize; | |
| } | |
| #hand { | |
| width: min(40vh, 40vw); | |
| transform-origin: 0% 0%; | |
| translate: 50% 0%; | |
| rotate: calc((-1 * var(--rotation, 0deg)) + 90deg); | |
| height: 2px; | |
| background-color: red; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| #origin { | |
| content: ""; | |
| background: green; | |
| width: 8px; | |
| height: 8px; | |
| display: block; | |
| position: relative; | |
| translate: -50% 0; | |
| border-radius: 99rem; | |
| } | |
| #marker { | |
| content: ""; | |
| background: green; | |
| width: 24px; | |
| height: 24px; | |
| display: block; | |
| position: relative; | |
| translate: 25% 0%; | |
| border-radius: 99rem; | |
| } | |
| #output { | |
| position: fixed; | |
| bottom: 0; | |
| right: 0; | |
| padding: 1rem; | |
| background: rgba(0, 0, 0, 0.2); | |
| } | |
| </style> | |
| `); | |
| const hand = document.getElementById("hand"); | |
| const origin = document.getElementById("origin"); | |
| const marker = document.getElementById("marker"); | |
| const output = document.getElementById("output"); | |
| let rotation = 0; | |
| let touched = false; | |
| function updateOutput() { | |
| const directionMap = ["front", "left", "back", "right"]; | |
| const directionIndex = ((rotation + 45) / 90 | 0) % 4; | |
| output.innerHTML = ` | |
| <div> | |
| <div>${rotation.toFixed(2)}</div> | |
| <div>${directionIndex}</div> | |
| <div>${directionMap[directionIndex]}</div> | |
| </div> | |
| `; | |
| } | |
| updateOutput(); | |
| window.addEventListener("pointerdown", () => { | |
| touched = true; | |
| }); | |
| window.addEventListener("pointerup", () => { | |
| touched = false; | |
| }); | |
| window.addEventListener("pointermove", (e) => { | |
| if (!touched) return; | |
| rotation -= e.movementX / 4; | |
| rotation = (360 + rotation) % 360; | |
| hand.style.setProperty("--rotation", `${rotation}deg`); | |
| updateOutput(); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment