
Prompt
User interactions puppeteer a set of glitchy avatars rendered with Three.js. Each avatar emits a unique sound patch from Tone.js and reacts unpredictably to clicks and drags. The avatars occasionally “break” apart into pixel shards that float and recombine, creating a chaotic dance of sound and form.
A system prompt was added to support web rendering
```html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Chaotic Dance of Sound and Form</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<style>
body {
margin: 0;
overflow: hidden;
background-color: #050505;
color: #fff;
font-family: monospace;
}
canvas {
display: block;
}
#info {
position: absolute;
top: 10px;
width: 100%;
text-align: center;
z-index: 100;
display:block;
text-shadow: 0 0 5px #00aaff;
}
#info.hidden {
display: none;
}
</style>
</head>
<body>
<div id="info">Click or drag the avatars</div>
<script async src="https://unpkg.com/es-module-shims@1.8.0/dist/es-module-shims.js"></script>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.157.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.157.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// --- Tone.js ---
// Since Tone.js is not an ES module on most CDNs, we'll load it the old-fashioned way
// and attach it to the window.
await new Promise(resolve => {
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/tone/14.7.77/Tone.js';
script.onload = resolve;
document.head.appendChild(script);
});
// --- GLOBAL VARIABLES ---
let scene, camera, renderer, clock, controls;
let raycaster, pointer;
const avatars = [];
let selectedAvatar = null;
let audioInitialized = false;
// --- SHADER DEFINITIONS ---
// Shader for the intact, "glitchy" avatar
const glitchyVertexShader = `
uniform float u_time;
uniform float u_glitch_amount;
varying vec2 vUv;
// Simple noise function
float noise(vec3 p) {
return sin(p.x * 10.0 + u_time) * cos(p.y * 10.0 + u_time) * sin(p.z * 10.0 + u_time);
}
void main() {
vUv = uv;
vec3 displaced = position + normal * noise(position) * u_glitch_amount;
gl_Position = projectionMatrix * modelViewMatrix * vec4(displaced, 1.0);
}
`;
const glitchyFragmentShader = `
uniform float u_time;
uniform vec3 u_color;
varying vec2 vUv;
void main() {
float scanline = sin(vUv.y * 800.0 + u_time * 2.0) * 0.05;
vec3 finalColor = u_color + vec3(scanline);
float vignette = 1.0 - length(vUv - vec2(0.5)) * 1.2;
gl_FragColor = vec4(finalColor * vignette, 1.0);
}
`;
// Shader for the "shattering" particles
const particleVertexShader = `
uniform float u_time;
uniform float u_progress; // 0: fully shattered, 1: fully recombined
uniform float u_duration;
attribute vec3 initialVelocity;
// Easing function: easeInOutQuad
float easeInOut(float t) {
return t < 0.5 ? 2.0 * t * t : -1.0 + (4.0 - 2.0 * t) * t;
}
void main() {
float t = u_time / u_duration;
// Position when fully shattered (parabolic arc)
vec3 shatteredPos = position + initialVelocity * t - vec3(0.0, 2.0, 0.0) * t * t;
// Mix between shattered and original position based on progress
vec3 finalPos = mix(shatteredPos, position, easeInOut(u_progress));
vec4 mvPosition = modelViewMatrix * vec4(finalPos, 1.0);
gl_PointSize = (3.0 / -mvPosition.z) * (1.0 - u_progress + 0.1);
gl_Position = projectionMatrix * mvPosition;
}
`;
const particleFragmentShader = `
uniform vec3 u_color;
uniform float u_progress;
void main() {
float alpha = 1.0 - u_progress;
if (distance(gl_PointCoord, vec2(0.5, 0.5)) > 0.45) discard;
gl_FragColor = vec4(u_color * (alpha + 0.5), alpha);
}
`;
// --- AVATAR CLASS ---
class GlitchyAvatar {
constructor(soundPatch, color, position) {
this.sound = soundPatch;
this.color = new THREE.Color(color);
this.state = 'INTACT'; // INTACT, SHATTERING, RECOMBINING
this.animation = {
time: 0,
duration: 0,
progress: 0
};
// 1. The intact mesh
const geometry = new THREE.IcosahedronGeometry(1.5, 3);
this.material = new THREE.ShaderMaterial({
uniforms: {
u_time: { value: 0 },
u_glitch_amount: { value: 0.0 },
u_color: { value: this.color }
},
vertexShader: glitchyVertexShader,
fragmentShader: glitchyFragmentShader
});
this.mesh = new THREE.Mesh(geometry, this.material);
// 2. The particle system for shattering
const particleCount = geometry.attributes.position.count;
const particleGeometry = new THREE.BufferGeometry();
particleGeometry.setAttribute('position', geometry.attributes.position.clone());
const velocities = new Float32Array(particleCount * 3);
for (let i = 0; i < particleCount; i++) {
const normal = new THREE.Vector3(
geometry.attributes.normal.getX(i),
geometry.attributes.normal.getY(i),
geometry.attributes.normal.getZ(i)
);
const speed = 2.0 + Math.random() * 4.0;
normal.multiplyScalar(speed);
velocities[i * 3] = normal.x;
velocities[i * 3 + 1] = normal.y;
velocities[i * 3 + 2] = normal.z;
}
particleGeometry.setAttribute('initialVelocity', new THREE.BufferAttribute(velocities, 3));
this.particleMaterial = new THREE.ShaderMaterial({
uniforms: {
u_time: { value: 0 },
u_progress: { value: 0 },
u_duration: { value: 0},
u_color: { value: this.color }
},
vertexShader: particleVertexShader,
fragmentShader: particleFragmentShader,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending
});
this.particles = new THREE.Points(particleGeometry, this.particleMaterial);
// Group to hold both and manage position
this.group = new THREE.Group();
this.group.add(this.mesh);
this.group.add(this.particles);
this.group.position.copy(position);
this.particles.visible = false;
// Add a reference back to this class instance from the Three.js object
this.mesh.userData.avatar = this;
}
shatter() {
if (this.state !== 'INTACT') return;
// Play sound
this.sound.triggerAttackRelease(Tone.Frequency(200 + Math.random() * 600).toNote(), '0.5');
this.state = 'SHATTERING';
this.mesh.visible = false;
this.particles.visible = true;
this.animation.time = 0;
this.animation.duration = 2 + Math.random() * 2; // Lasts 2-4 seconds
this.particleMaterial.uniforms.u_duration.value = this.animation.duration;
}
update(delta) {
// Update shader time for constant glitch effect
this.material.uniforms.u_time.value += delta;
this.material.uniforms.u_glitch_amount.value = this.state === 'INTACT' ? 0.05 + (Math.sin(clock.getElapsedTime() * 5) * 0.05) : 0;
if (this.state === 'INTACT') return;
this.animation.time += delta;
if (this.state === 'SHATTERING') {
this.particleMaterial.uniforms.u_time.value = this.animation.time;
this.particleMaterial.uniforms.u_progress.value = 0;
if (this.animation.time >= this.animation.duration) {
this.state = 'RECOMBINING';
this.animation.time = 0;
this.animation.duration = 1.5 + Math.random() * 1.5; // Recombine over 1.5-3 seconds
}
} else if (this.state === 'RECOMBINING') {
this.animation.progress = Math.min(this.animation.time / this.animation.duration, 1.0);
this.particleMaterial.uniforms.u_progress.value = this.animation.progress;
if (this.animation.progress >= 1.0) {
this.state = 'INTACT';
this.mesh.visible = true;
this.particles.visible = false;
}
}
}
}
// --- MAIN FUNCTIONS ---
function init() {
// Scene
scene = new THREE.Scene();
clock = new THREE.Clock();
// Camera
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 15;
// Renderer
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
// Controls
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.minDistance = 5;
controls.maxDistance = 50;
// Lights
scene.add(new THREE.AmbientLight(0xffffff, 0.3));
const dirLight = new THREE.DirectionalLight(0xffffff, 1.0);
dirLight.position.set(5, 10, 7);
scene.add(dirLight);
// Raycasting
raycaster = new THREE.Raycaster();
pointer = new THREE.Vector2();
// Create Audio Synths
const synth1 = new Tone.FMSynth({
harmonicity: 3.01,
modulationIndex: 14,
envelope: { attack: 0.01, decay: 0.2, release: 0.5 },
modulationEnvelope: { attack: 0.01, decay: 0.3, release: 0.5 }
}).toDestination();
const synth2 = new Tone.MembraneSynth({
pitchDecay: 0.08,
octaves: 6,
envelope: { attack: 0.001, decay: 0.5, sustain: 0.01, release: 0.4 }
}).toDestination();
const synth3 = new Tone.AMSynth({
harmonicity: 1.5,
envelope: { attack: 0.05, decay: 0.3, release: 1 },
modulationEnvelope: { attack: 0.1, decay: 0.2, release: 0.5 }
}).toDestination();
const reverb = new Tone.Reverb(2).toDestination();
synth1.connect(reverb);
synth2.connect(reverb);
synth3.connect(reverb);
// Create Avatars
const avatarData = [
{ sound: synth1, color: 0x00aaff, position: new THREE.Vector3(-5, 2, 0) },
{ sound: synth2, color: 0xff4400, position: new THREE.Vector3(5, -2, -2) },
{ sound: synth3, color: 0x00ff88, position: new THREE.Vector3(0, 0, 3) }
];
avatarData.forEach(data => {
const avatar = new GlitchyAvatar(data.sound, data.color, data.position);
avatars.push(avatar);
scene.add(avatar.group);
});
// Event Listeners
window.addEventListener('resize', onWindowResize);
window.addEventListener('pointerdown', onPointerDown);
window.addEventListener('pointermove', onPointerMove);
window.addEventListener('pointerup', onPointerUp);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function onPointerDown(event) {
// First user interaction initializes audio context
if (!audioInitialized) {
Tone.start();
audioInitialized = true;
document.getElementById('info').classList.add('hidden');
}
updatePointer(event);
const intersects = raycaster.intersectObjects(avatars.map(a => a.mesh));
if (intersects.length > 0) {
const avatar = intersects[0].object.userData.avatar;
if (avatar) {
controls.enabled = false;
selectedAvatar = avatar;
avatar.shatter(); // Trigger the effect on click
}
}
}
function onPointerMove(event) {
if (selectedAvatar) {
updatePointer(event);
// Project pointer to a plane at the avatar's distance
const plane = new THREE.Plane(new THREE.Vector3(0,0,1), -selectedAvatar.group.position.z);
const intersection = new THREE.Vector3();
raycaster.ray.intersectPlane(plane, intersection);
// Unpredictable drag motion
const dragForce = intersection.sub(selectedAvatar.group.position).multiplyScalar(0.1);
selectedAvatar.group.position.add(dragForce);
// Occasionally trigger a smaller sound
if (Math.random() > 0.98) {
selectedAvatar.sound.triggerAttackRelease(Tone.Frequency(400 + Math.random() * 200).toNote(), '0.1');
}
}
}
function onPointerUp() {
if (selectedAvatar) {
selectedAvatar = null;
controls.enabled = true;
}
}
function updatePointer(event) {
pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
pointer.y = - (event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(pointer, camera);
}
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
controls.update();
avatars.forEach(avatar => {
avatar.update(delta);
// add a slight bobbing motion
avatar.group.position.y += Math.sin(clock.getElapsedTime() * 0.5 + avatar.group.position.x) * 0.005;
});
renderer.render(scene, camera);
}
// --- RUN ---
init();
animate();
</script>
</body>
</html>
```Here's a complete HTML implementation that creates glitchy Three.js avatars with Tone.js sound interactions:
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Glitchy Audio Avatars</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #000;
}
canvas {
display: block;
}
</style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.10.4/gsap.min.js"></script>
<script>
// Initialize Three.js scene
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// Initialize Tone.js
const synths = [];
const reverb = new Tone.Reverb(3).toDestination();
reverb.decay = 2.5;
reverb.wet.value = 0.4;
// Avatar class
class GlitchAvatar {
constructor(x, y, z) {
this.x = x;
this.y = y;
this.z = z;
this.isBroken = false;
this.shards = [];
this.velocity = new THREE.Vector3();
// Create random geometry (mix between sphere and box)
const geometrySeed = Math.random();
let geometry;
if (geometrySeed < 0.3) {
geometry = new THREE.SphereGeometry(0.5, 12, 12);
} else if (geometrySeed < 0.6) {
geometry = new THREE.BoxGeometry(0.8, 0.8, 0.8);
} else {
geometry = new THREE.IcosahedronGeometry(0.6, 0);
}
// Apply glitchy distortion to geometry
const positions = geometry.attributes.position;
for (let i = 0; i < positions.count; i++) {
positions.setXYZ(
i,
positions.getX(i) + (Math.random() * 0.2 - 0.1),
positions.getY(i) + (Math.random() * 0.2 - 0.1),
positions.getZ(i) + (Math.random() * 0.2 - 0.1)
);
}
// Create material with random color and slight transparency
const hue = Math.random();
this.material = new THREE.MeshStandardMaterial({
color: new THREE.Color().setHSL(hue, 0.7, 0.5),
metalness: 0.2,
roughness: 0.7,
transparent: true,
opacity: 0.9
});
// Create mesh
this.mesh = new THREE.Mesh(geometry, this.material);
this.mesh.position.set(x, y, z);
scene.add(this.mesh);
// Create synth for this avatar
this.synth = new Tone.PolySynth(Tone.Synth).connect(reverb);
this.synth.volume.value = -12;
// Store synth parameters
this.note = Math.floor(Math.random() * 24) + 48; // MIDI note
this.detune = Math.random() * 100 - 50;
this.envelope = {
attack: Math.random() * 0.2 + 0.01,
decay: Math.random() * 0.3,
sustain: 0.4 + Math.random() * 0.3,
release: Math.random() * 1 + 0.5
};
this.synth.set({
oscillator: {
type: Math.random() < 0.5 ? "sine" : "sawtooth",
detune: this.detune
},
envelope: this.envelope
});
}
breakApart() {
if (this.isBroken) return;
this.isBroken = true;
// Hide main mesh
this.mesh.visible = false;
// Get geometry data for shards
const geometry = this.mesh.geometry;
const positions = geometry.attributes.position;
const colors = [];
// Create color array if doesn't exist
if (!geometry.attributes.color) {
const color = this.mesh.material.color;
for (let i = 0; i < positions.count; i++) {
colors.push(color.r, color.g, color.b);
}
} else {
for (let i = 0; i < positions.count; i++) {
colors.push(
geometry.attributes.color.getX(i),
geometry.attributes.color.getY(i),
geometry.attributes.color.getZ(i)
);
}
}
// Create shards (individual points)
const shardGeometry = new THREE.BufferGeometry();
shardGeometry.setAttribute('position', new THREE.Float32BufferAttribute(positions.array, 3));
shardGeometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
// Create points material
const shardMaterial = new THREE.PointsMaterial({
size: 0.05,
vertexColors: true,
transparent: true,
opacity: 0.9,
sizeAttenuation: true
});
// Create points system and add to scene
this.points = new THREE.Points(shardGeometry, shardMaterial);
this.points.position.copy(this.mesh.position);
scene.add(this.points);
// Create velocity data for each point
this.pointVelocities = [];
for (let i = 0; i < positions.count; i++) {
this.pointVelocities.push(new THREE.Vector3(
(Math.random() - 0.5) * 0.05,
(Math.random() - 0.5) * 0.05,
(Math.random() - 0.5) * 0.05
));
}
// Play break sound
this.playSound();
}
reassemble() {
if (!this.isBroken) return;
this.isBroken = false;
// Show main mesh
this.mesh.visible = true;
// Remove points system
if (this.points) {
scene.remove(this.points);
this.points = null;
}
// Play reassemble sound
this.playSound();
}
playSound() {
// Trigger sound with some randomness
const now = Tone.now();
this.synth.triggerAttackRelease(
Tone.Frequency(this.note, "midi").transpose(Math.random() * 7 - 3.5),
Math.random() * 0.5 + 0.1,
now,
Math.random() * 0.3
);
}
update() {
if (this.isBroken) {
// Animate broken shards
const positions = this.points.geometry.attributes.position;
for (let i = 0; i < positions.count; i++) {
const vel = this.pointVelocities[i];
// Update position
positions.setXYZ(
i,
positions.getX(i) + vel.x,
positions.getY(i) + vel.y,
positions.getZ(i) + vel.z
);
// Apply some randomness to velocity
vel.x += (Math.random() - 0.5) * 0.001;
vel.y += (Math.random() - 0.5) * 0.001;
vel.z += (Math.random() - 0.5) * 0.001;
// Apply gentle pull back to center
vel.x -= (positions.getX(i) - this.x) * 0.01;
vel.y -= (positions.getY(i) - this.y) * 0.01;
vel.z -= (positions.getZ(i) - this.z) * 0.01;
}
positions.needsUpdate = true;
// Random chance to reassemble
if (Math.random() < 0.005) {
this.reassemble();
}
} else {
// Normal avatar movement
this.velocity.x += (Math.random() - 0.5) * 0.001;
this.velocity.y += (Math.random() - 0.5) * 0.001;
this.velocity.z += (Math.random() - 0.5) * 0.001;
// Apply bounds
const bounds = 5;
if (this.mesh.position.x < -bounds || this.mesh.position.x > bounds) this.velocity.x *= -1;
if (this.mesh.position.y < -bounds || this.mesh.position.y > bounds) this.velocity.y *= -1;
if (this.mesh.position.z < -bounds || this.mesh.position.z > bounds) this.velocity.z *= -1;
// Move position
this.mesh.position.add(this.velocity);
// Random rotation
this.mesh.rotation.x += 0.005;
this.mesh.rotation.y += 0.008;
// Random glitch effect
if (Math.random() < 0.01) {
const pos = this.mesh.geometry.attributes.position;
for (let i = 0; i < pos.count; i++) {
if (Math.random() < 0.1) {
pos.setXYZ(
i,
pos.getX(i) + (Math.random() - 0.5) * 0.2,
pos.getY(i) + (Math.random() - 0.5) * 0.2,
pos.getZ(i) + (Math.random() - 0.5) * 0.2
);
}
}
pos.needsUpdate = true;
}
// Random chance to break apart
if (Math.random() < 0.003) {
this.breakApart();
}
}
}
}
// Create avatars
const avatars = [];
const avatarCount = 8;
for (let i = 0; i < avatarCount; i++) {
const x = Math.random() * 10 - 5;
const y = Math.random() * 6 - 3;
const z = Math.random() * 10 - 5;
avatars.push(new GlitchAvatar(x, y, z));
}
// Add lights
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
const pointLight = new THREE.PointLight(0xffaa00, 0.5, 50);
pointLight.position.set(5, 5, 5);
scene.add(pointLight);
// Camera position
camera.position.z = 10;
// Raycaster for mouse interactions
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
// Mouse move handler
function onMouseMove(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
}
// Click handler
function onClick() {
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(
avatars.map(avatar => avatar.isBroken ? avatar.points : avatar.mesh)
);
if (intersects.length > 0) {
const pointIndex = intersects[0].index;
const object = intersects[0].object;
// Find which avatar was clicked
for (const avatar of avatars) {
if (avatar.isBroken && avatar.points === object) {
// Explode shards more when clicked
if (pointIndex !== undefined) {
const vel = avatar.pointVelocities[pointIndex];
const power = 0.2;
vel.x += (Math.random() - 0.5) * power;
vel.y += (Math.random() - 0.5) * power;
vel.z += (Math.random() - 0.5) * power;
}
avatar.playSound();
break;
} else if (!avatar.isBroken && avatar.mesh === object) {
avatar.breakApart();
break;
}
}
}
}
// Drag variables
let isDragging = false;
let draggedAvatar = null;
let dragOffset = new THREE.Vector3();
function onMouseDown(event) {
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(
avatars.map(avatar => avatar.isBroken ? avatar.points : avatar.mesh)
);
if (intersects.length > 0) {
const object = intersects[0].object;
for (const avatar of avatars) {
if (!avatar.isBroken && avatar.mesh === object) {
isDragging = true;
draggedAvatar = avatar;
// Calculate offset between mouse and avatar position
const vector = new THREE.Vector3(
mouse.x, mouse.y, 0.5
).unproject(camera);
dragOffset = new THREE.Vector3().copy(avatar.mesh.position).sub(vector);
break;
}
}
}
}
function onMouseUp() {
isDragging = false;
draggedAvatar = null;
}
function onMouseMoveDrag(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
if (isDragging && draggedAvatar) {
const vector = new THREE.Vector3(
mouse.x, mouse.y, 0.5
).unproject(camera);
draggedAvatar.mesh.position.copy(vector.add(dragOffset));
// Add some chaotic movement when dragging
draggedAvatar.velocity.set(
(Math.random() - 0.5) * 0.1,
(Math.random() - 0.5) * 0.1,
(Math.random() - 0.5) * 0.1
);
// Random chance to trigger sound while dragging
if (Math.random() < 0.1) {
draggedAvatar.playSound();
}
}
}
// Register event listeners
window.addEventListener('mousemove', onMouseMove, false);
window.addEventListener('click', onClick, false);
window.addEventListener('mousedown', onMouseDown, false);
window.addEventListener('mouseup', onMouseUp, false);
window.addEventListener('mousemove', onMouseMoveDrag, false);
// Handle window resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// Animation loop
function animate() {
requestAnimationFrame(animate);
// Update avatars
for (const avatar of avatars) {
avatar.update();
}
renderer.render(scene, camera);
}
// Start audio context on first interaction
document.body.addEventListener('click', () => {
Tone.start();
document.body.removeEventListener('click', this);
}, { once: true });
// Start animation
animate();
</script>
</body>
</html>
```
This implementation creates:
1. Multiple glitchy avatars with Three.js that have:
- Random geometric shapes (spheres, boxes, icosahedrons)
- Jittery animations and random movement
- "Broken" states where they explode into particle shards
- Automatic reassembly after being broken
2. Tone.js audio features:
- Each avatar has a unique synth sound
- Sounds trigger when avatars break/reassemble or are clicked
- Random note variations for chaotic effects
- Reverb for atmosphere
3. Interactive elements:
- Click to break avatars apart
- Click broken avatars to scatter their shards
- Drag avatars around the screen
- Mouse tracking for interaction
The avatars will autonomously glitch, move, break apart, and reform while emitting procedural sounds. User interactions amplify the chaotic behavior.```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Glitchy Avatar Chaos</title>
<style>
body {
margin: 0;
padding: 0;
background: #000;
overflow: hidden;
font-family: 'Courier New', monospace;
cursor: crosshair;
}
#container {
position: relative;
width: 100vw;
height: 100vh;
}
#info {
position: absolute;
top: 10px;
left: 10px;
color: #00ff41;
font-size: 12px;
z-index: 100;
text-shadow: 0 0 10px #00ff41;
}
.glitch {
animation: glitch 0.3s infinite;
}
@keyframes glitch {
0% { transform: translate(0); }
20% { transform: translate(-2px, 2px); }
40% { transform: translate(-2px, -2px); }
60% { transform: translate(2px, 2px); }
80% { transform: translate(2px, -2px); }
100% { transform: translate(0); }
}
</style>
</head>
<body>
<div id="container">
<div id="info">
GLITCH_AVATARS_v2.1<br>
[CLICK/DRAG TO CORRUPT]<br>
STATUS: CHAOTIC
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js"></script>
<script>
// Scene setup
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: false });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x000000);
document.getElementById('container').appendChild(renderer.domElement);
// Audio setup
let audioStarted = false;
async function startAudio() {
if (!audioStarted) {
await Tone.start();
audioStarted = true;
}
}
// Glitch Avatar Class
class GlitchAvatar {
constructor(x, y, z) {
this.originalPosition = new THREE.Vector3(x, y, z);
this.position = new THREE.Vector3(x, y, z);
this.isDragging = false;
this.isExploded = false;
this.glitchIntensity = 0;
this.shards = [];
this.createMesh();
this.createSound();
this.setupBehavior();
}
createMesh() {
// Create glitchy avatar geometry
const geometry = new THREE.ConeGeometry(0.5, 1.5, 6);
const material = new THREE.MeshBasicMaterial({
color: new THREE.Color().setHSL(Math.random(), 0.8, 0.6),
wireframe: Math.random() > 0.5,
transparent: true,
opacity: 0.8
});
this.mesh = new THREE.Mesh(geometry, material);
this.mesh.position.copy(this.position);
this.mesh.userData = this;
// Add glitch effect
this.addGlitchEffect();
scene.add(this.mesh);
}
addGlitchEffect() {
// Random vertex displacement for glitch effect
const positions = this.mesh.geometry.attributes.position;
this.originalPositions = positions.array.slice();
setInterval(() => {
if (this.glitchIntensity > 0) {
for (let i = 0; i < positions.count; i++) {
const i3 = i * 3;
positions.array[i3] = this.originalPositions[i3] + (Math.random() - 0.5) * this.glitchIntensity;
positions.array[i3 + 1] = this.originalPositions[i3 + 1] + (Math.random() - 0.5) * this.glitchIntensity;
positions.array[i3 + 2] = this.originalPositions[i3 + 2] + (Math.random() - 0.5) * this.glitchIntensity;
}
positions.needsUpdate = true;
}
}, 100);
}
createSound() {
// Create unique sound patch
const sounds = [
() => new Tone.FMSynth({
harmonicity: Math.random() * 5,
modulationIndex: Math.random() * 100,
envelope: { attack: 0.01, decay: 0.2, sustain: 0.1, release: 0.8 }
}),
() => new Tone.AMSynth({
harmonicity: Math.random() * 3,
envelope: { attack: 0.1, decay: 0.3, sustain: 0.4, release: 0.9 }
}),
() => new Tone.NoiseSynth({
noise: { type: ['white', 'brown', 'pink'][Math.floor(Math.random() * 3)] },
envelope: { attack: 0.005, decay: 0.1, sustain: 0.0 }
}),
() => new Tone.MetalSynth({
frequency: 200 + Math.random() * 400,
envelope: { attack: 0.001, decay: 0.1, release: 0.8 },
harmonicity: 5.1,
modulationIndex: 32
})
];
this.synth = sounds[Math.floor(Math.random() * sounds.length)]();
// Random effects chain
this.effects = [
new Tone.Distortion(Math.random()),
new Tone.BitCrusher(Math.floor(Math.random() * 8) + 1),
new Tone.FrequencyShifter(Math.random() * 100 - 50),
new Tone.AutoFilter(Math.random() * 10 + 0.5).start()
];
// Chain effects randomly
let chain = this.synth;
for (let i = 0; i < Math.floor(Math.random() * 3) + 1; i++) {
const effect = this.effects[Math.floor(Math.random() * this.effects.length)];
chain = chain.connect(effect);
}
chain.toDestination();
this.baseFreq = 100 + Math.random() * 500;
}
setupBehavior() {
// Random autonomous behavior
setInterval(() => {
if (!this.isDragging && Math.random() > 0.95) {
this.triggerGlitch();
}
if (!this.isDragging && Math.random() > 0.98) {
this.explode();
}
// Autonomous movement
if (!this.isDragging && !this.isExploded) {
this.position.x += (Math.random() - 0.5) * 0.02;
this.position.y += (Math.random() - 0.5) * 0.02;
this.mesh.position.lerp(this.position, 0.1);
}
}, 100);
}
triggerGlitch() {
this.glitchIntensity = 0.5 + Math.random() * 0.5;
// Visual glitch
this.mesh.material.color.setHSL(Math.random(), 0.8, 0.6);
this.mesh.rotation.x += Math.random() * 0.5;
this.mesh.rotation.z += Math.random() * 0.5;
// Audio glitch
if (audioStarted) {
const freq = this.baseFreq + (Math.random() - 0.5) * 200;
const duration = 0.1 + Math.random() * 0.3;
if (this.synth.triggerAttackRelease) {
this.synth.triggerAttackRelease(freq, duration);
} else {
this.synth.triggerAttackRelease(duration);
}
}
// Decay glitch
setTimeout(() => {
this.glitchIntensity *= 0.8;
}, 200);
}
explode() {
if (this.isExploded) return;
this.isExploded = true;
scene.remove(this.mesh);
// Create pixel shards
for (let i = 0; i < 20; i++) {
const shard = this.createShard();
this.shards.push(shard);
scene.add(shard.mesh);
}
// Audio explosion
if (audioStarted) {
for (let i = 0; i < 5; i++) {
setTimeout(() => {
const freq = this.baseFreq + (Math.random() - 0.5) * 400;
if (this.synth.triggerAttackRelease) {
this.synth.triggerAttackRelease(freq, 0.05);
} else {
this.synth.triggerAttackRelease(0.05);
}
}, i * 50);
}
}
// Recombine after delay
setTimeout(() => {
this.recombine();
}, 3000 + Math.random() * 2000);
}
createShard() {
const geometry = new THREE.BoxGeometry(0.05, 0.05, 0.05);
const material = new THREE.MeshBasicMaterial({
color: this.mesh.material.color.clone(),
transparent: true,
opacity: 0.8
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.copy(this.mesh.position);
const velocity = new THREE.Vector3(
(Math.random() - 0.5) * 2,
(Math.random() - 0.5) * 2,
(Math.random() - 0.5) * 2
);
return { mesh, velocity };
}
recombine() {
// Animate shards back
const duration = 1000;
const startTime = Date.now();
const animate = () => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
this.shards.forEach(shard => {
shard.mesh.position.lerp(this.originalPosition, progress * 0.02);
shard.mesh.material.opacity = 1 - progress;
});
if (progress < 1) {
requestAnimationFrame(animate);
} else {
// Remove shards and restore avatar
this.shards.forEach(shard => scene.remove(shard.mesh));
this.shards = [];
this.isExploded = false;
this.position.copy(this.originalPosition);
scene.add(this.mesh);
this.mesh.position.copy(this.position);
this.triggerGlitch();
}
};
animate();
}
onInteraction(type, position) {
if (type === 'click') {
this.triggerGlitch();
if (Math.random() > 0.7) {
this.explode();
}
} else if (type === 'drag') {
this.isDragging = true;
this.position.copy(position);
this.glitchIntensity = 0.3;
this.triggerGlitch();
}
}
onDragEnd() {
this.isDragging = false;
this.glitchIntensity = 0.1;
}
}
// Create avatars
const avatars = [];
for (let i = 0; i < 8; i++) {
const x = (Math.random() - 0.5) * 8;
const y = (Math.random() - 0.5) * 4;
const z = (Math.random() - 0.5) * 4;
avatars.push(new GlitchAvatar(x, y, z));
}
// Interaction handling
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let selectedAvatar = null;
function onMouseMove(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
if (selectedAvatar) {
raycaster.setFromCamera(mouse, camera);
const distance = 5;
const position = raycaster.ray.origin.clone().add(raycaster.ray.direction.multiplyScalar(distance));
selectedAvatar.onInteraction('drag', position);
}
}
function onMouseDown(event) {
startAudio();
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(scene.children.filter(obj => obj.userData instanceof GlitchAvatar));
if (intersects.length > 0) {
selectedAvatar = intersects[0].object.userData;
selectedAvatar.onInteraction('click', intersects[0].point);
}
}
function onMouseUp() {
if (selectedAvatar) {
selectedAvatar.onDragEnd();
selectedAvatar = null;
}
}
renderer.domElement.addEventListener('mousemove', onMouseMove);
renderer.domElement.addEventListener('mousedown', onMouseDown);
renderer.domElement.addEventListener('mouseup', onMouseUp);
// Camera setup
camera.position.z = 8;
camera.position.y = 2;
// Animation loop
function animate() {
requestAnimationFrame(animate);
// Camera drift
camera.position.x += Math.sin(Date.now() * 0.001) * 0.001;
camera.position.y += Math.cos(Date.now() * 0.0007) * 0.001;
// Update avatars
avatars.forEach(avatar => {
if (avatar.mesh.parent && !avatar.isDragging) {
avatar.mesh.rotation.y += 0.01;
avatar.mesh.rotation.x += 0.005;
}
});
renderer.render(scene, camera);
}
// Handle resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// Global glitch effect
setInterval(() => {
if (Math.random() > 0.95) {
document.getElementById('info').classList.toggle('glitch');
setTimeout(() => {
document.getElementById('info').classList.remove('glitch');
}, 300);
}
}, 1000);
animate();
</script>
</body>
</html>
```
This creates a chaotic interactive experience with glitchy avatars that:
- **Unique Sound Patches**: Each avatar generates different synthesized sounds using Tone.js (FM, AM, Noise, Metal synths) with random effects chains
- **Unpredictable Reactions**: Clicking triggers glitches and occasional explosions, dragging causes continuous glitch effects
- **Pixel Shard System**: Avatars randomly "break apart" into floating pixel shards that recombine after a few seconds
- **Glitch Effects**: Real-time vertex displacement, color shifting, and visual corruption
- **Autonomous Behavior**: Avatars move around randomly and trigger their own glitch events
- **Audio-Visual Sync**: Sounds trigger with visual glitches for a cohesive chaotic experience
The interface deliberately feels unstable and unpredictable, creating a digital ecosystem where sound and form dance in constant flux.Below is a compact, self-contained demo that combines Three.js and Tone.js to create:
• Three “glitch avatars” (colored cubes).
• Each avatar owns a unique Tone.js synth patch.
• Click an avatar: it plays a note, gains a random spin and may “explode” into voxel-shards that drift, then magnetically recombine.
• Drag over an avatar: pitch shifts unpredictably.
• Avatars may also self-destruct at random intervals.
Feel free to expand the geometry, textures, or synth settings—the structure is in place for you to swap in more complex models and richer sounds.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Glitchy Avatars – Three.js + Tone.js</title>
<style>
html,body{margin:0;height:100%;overflow:hidden;background:#000;color:#fff;font-family:sans-serif;}
#info{position:fixed;top:10px;left:10px;font-size:14px;opacity:.8;}
a{color:#0af}
</style>
</head>
<body>
<div id="info">
CLICK / DRAG an avatar. <br>
Audio will start the first time you interact.
</div>
<script type="module">
import * as THREE from 'https://unpkg.com/three@0.161.0/build/three.module.js';
import { OrbitControls } from 'https://unpkg.com/three@0.161.0/examples/jsm/controls/OrbitControls.js';
import * as Tone from 'https://cdn.skypack.dev/tone@14.8.46';
let scene, camera, renderer, controls, raycaster;
let pointer = new THREE.Vector2();
const avatars = [];
init();
animate(0);
//----- SETUP -----
function init() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000);
camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 100);
camera.position.set(0, 2.5, 7);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(devicePixelRatio);
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.enablePan = false;
raycaster = new THREE.Raycaster();
// lights
const ambient = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambient);
const dir = new THREE.DirectionalLight(0xffffff, 0.8);
dir.position.set(5, 10, 7);
scene.add(dir);
// floor grid
const grid = new THREE.GridHelper(20, 40, 0x444444, 0x222222);
scene.add(grid);
// create avatars
const colors = [0x00ffff, 0xff00ff, 0xffff00];
const patches = [
new Tone.MembraneSynth({ pitchDecay: 0.05, octaves: 10 }).toDestination(),
new Tone.FMSynth({ modulationIndex: 12, harmonicity: 6 }).toDestination(),
new Tone.Synth({ oscillator: { type: "sawtooth" }, envelope: { attack: 0.01, decay: 0.2, sustain: 0.1, release: 0.5 } }).toDestination()
];
for (let i = 0; i < 3; i++) {
const avatar = new GlitchAvatar({
color: colors[i],
synth: patches[i],
position: new THREE.Vector3((i - 1) * 3, 0.5, 0)
});
avatars.push(avatar);
scene.add(avatar.group);
}
window.addEventListener('resize', onResize);
window.addEventListener('pointermove', onPointerMove);
window.addEventListener('pointerdown', onPointerDown);
}
//----- AVATAR CLASS -----
class GlitchAvatar {
constructor({ color, synth, position }) {
this.synth = synth;
this.group = new THREE.Group();
this.group.position.copy(position);
// main mesh
const geo = new THREE.BoxGeometry(1, 1, 1);
const mat = new THREE.MeshStandardMaterial({ color, roughness: 0.4, metalness: 0.1 });
this.mesh = new THREE.Mesh(geo, mat);
this.group.add(this.mesh);
// shard container
this.shards = [];
this.broken = false;
this.breakTimer = 0;
this.reformTimer = 0;
// schedule random self-breaks
this.nextAutoBreak = performance.now() + THREE.MathUtils.randInt(4000, 9000);
}
triggerNote(freq = 220) {
this.synth.triggerAttackRelease(freq, "8n", undefined, 0.8);
}
breakApart() {
if (this.broken) return;
this.broken = true;
this.mesh.visible = false;
const shardSize = 0.2;
const half = 0.5 - shardSize / 2;
for (let x = -half; x <= half; x += shardSize) {
for (let y = -half; y <= half; y += shardSize) {
for (let z = -half; z <= half; z += shardSize) {
const geo = new THREE.BoxGeometry(shardSize, shardSize, shardSize);
const mat = new THREE.MeshStandardMaterial({ color: this.mesh.material.color, roughness: 0.5 });
const shard = new THREE.Mesh(geo, mat);
shard.position.set(x, y, z);
shard.original = shard.position.clone();
shard.velocity = new THREE.Vector3(
THREE.MathUtils.randFloatSpread(2),
THREE.MathUtils.randFloat(0.5, 2.5),
THREE.MathUtils.randFloatSpread(2)
);
this.group.add(shard);
this.shards.push(shard);
}
}
}
this.breakTimer = 1500; // ms shards fly out
this.reformTimer = 1500; // ms they glide back
}
update(dt) {
// autodestruct
if (!this.broken && performance.now() > this.nextAutoBreak) {
this.breakApart();
this.nextAutoBreak = performance.now() + THREE.MathUtils.randInt(6000, 12000);
}
// rotate whole avatar a bit for life
this.group.rotation.y += dt * 0.2;
if (this.broken) {
if (this.breakTimer > 0) {
// flying apart
this.shards.forEach(s => {
s.position.addScaledVector(s.velocity, dt);
s.velocity.multiplyScalar(0.98); // drag
});
this.breakTimer -= dt * 1000;
if (this.breakTimer <= 0) {
// start reform
this.shards.forEach(s => s.velocity.set(0,0,0));
}
} else if (this.reformTimer > 0) {
// coming back
this.shards.forEach(s => {
s.position.lerp(s.original, 0.04);
});
this.reformTimer -= dt * 1000;
if (this.reformTimer <= 0) {
// done
this.shards.forEach(s => this.group.remove(s));
this.shards.length = 0;
this.mesh.visible = true;
this.broken = false;
}
}
}
}
}
//----- INTERACTION HANDLERS -----
function onPointerMove(e) {
updatePointer(e);
if (e.buttons) { // dragging
intersectAvatars(i => {
const pitch = THREE.MathUtils.mapLinear(pointer.y, -1, 1, 120, 40); // invert axis
i.object.__avatar.triggerNote(pitch);
});
}
}
function onPointerDown(e) {
updatePointer(e);
Tone.start(); // first human gesture starts Web Audio
intersectAvatars(i => {
const avatar = i.object.__avatar;
avatar.triggerNote(THREE.MathUtils.randInt(60, 800));
avatar.group.rotation.x += Math.random() * 2;
avatar.group.rotation.y += Math.random() * 2;
if (Math.random() < 0.7) avatar.breakApart();
});
}
function updatePointer(e) {
const rect = renderer.domElement.getBoundingClientRect();
pointer.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
pointer.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
}
function intersectAvatars(callback) {
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObjects(avatars.map(a => a.mesh), false);
if (intersects.length) callback(intersects[0]);
}
//----- ANIMATION LOOP -----
let prev = performance.now();
function animate() {
requestAnimationFrame(animate);
const now = performance.now();
const dt = (now - prev) / 1000;
prev = now;
avatars.forEach(a => a.update(dt));
controls.update();
renderer.render(scene, camera);
}
function onResize() {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
}
</script>
</body>
</html>
```
How to extend:
1. Replace the cube geometry with GLTF models or custom shaders for richer “glitch” visuals.
2. Experiment with Tone.js effects (BitCrusher, FeedbackDelay, Reverb) for deeper sonic chaos.
3. Add post-processing (e.g., GlitchPass from Three.js examples) for screen-wide distortion.```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Glitchy Avatars</title>
<style>
body {
margin: 0;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
background-color: #000;
}
canvas {
display: block;
}
#instructions {
position: absolute;
top: 20px;
color: white;
font-family: monospace;
text-align: center;
width: 100%;
pointer-events: none; /* Allow clicks to pass through */
font-size: 1.2em;
text-shadow: 0 0 5px rgba(255, 255, 255, 0.5);
background-color: rgba(0, 0, 0, 0.5);
padding: 10px;
box-sizing: border-box;
opacity: 1;
transition: opacity 1s ease-in-out;
z-index: 10;
}
#instructions.fade-out {
opacity: 0;
pointer-events: none;
}
</style>
</head>
<body>
<div id="instructions">Click an avatar to make it glitch! Drag to move.</div>
<!-- Three.js CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/0.160.0/three.min.js"></script>
<!-- Tone.js CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.min.js"></script>
<script>
// --- Three.js Setup ---
let scene, camera, renderer, raycaster, mouse;
let avatars = [];
let intersected = null; // The currently hovered avatar mesh
let isDragging = false;
let selectedAvatar = null; // The avatar currently being dragged
let offset = new THREE.Vector3(); // Offset for dragging calculation
// Audio Context state - Tone.js requires a user interaction to start
let audioStarted = false;
// --- Avatar States and Class ---
const AVATAR_STATES = {
NORMAL: 'NORMAL',
CLICKED: 'CLICKED', // Brief visual/sound feedback on click
GLITCHING: 'GLITCHING',
BROKEN: 'BROKEN', // Geometry is distorted (pixel shards floating out)
RECOMBINING: 'RECOMBINING' // Geometry is returning to normal
};
class Avatar {
constructor(id) {
this.id = id;
this.originalPosition = new THREE.Vector3();
this.originalRotation = new THREE.Euler(0, 0, 0); // Store initial rotation
this.originalScale = new THREE.Vector3(1, 1, 1);
this.state = AVATAR_STATES.NORMAL;
this.stateStartTime = 0;
this.stateDuration = 0; // How long to remain in the current state
// Geometries and Material
this.normalGeometry = new THREE.BoxGeometry(1, 1, 1); // The default shape
this.material = new THREE.MeshPhongMaterial({
color: new THREE.Color(Math.random(), Math.random(), Math.random()).getHex(),
shininess: 50,
emissive: 0x000000, // For hover/glitch glow
specular: 0x777777
});
this.mesh = new THREE.Mesh(this.normalGeometry, this.material);
scene.add(this.mesh);
// For vertex manipulation in broken state
this.originalVertices = new Float32Array(this.normalGeometry.attributes.position.array); // Store original vertex data
this.currentVerticesData = new Float32Array(this.originalVertices.length); // Mutable data buffer for BufferGeometry
this.velocityVertices = []; // Stores velocity for each "shard" when broken
this.centerOfGravity = new THREE.Vector3(); // Center point for explosion calculation
this.normalGeometry.computeBoundingSphere(); // Compute sphere for the BoxGeometry
this.centerOfGravity.copy(this.normalGeometry.boundingSphere.center);
// Tone.js Synth
this.synth = this.createSynth();
// 3D panner to give spatial audio effect
this.panner = new Tone.Panner3D().toDestination();
this.synth.connect(this.panner);
}
// Creates a unique Tone.js synthesizer patch
createSynth() {
const synthType = Math.floor(Math.random() * 3);
let synth;
switch (synthType) {
case 0: // AM Synth (amplitude modulation)
synth = new Tone.AMSynth({
harmonicity: Tone.Midi.mtof(Math.random() * 12 + 60), // Random harmonicity
detune: Math.random() * 200 - 100, // Random detune
oscillator: { type: "sine" },
envelope: { attack: 0.005, decay: 0.1, sustain: 0.05, release: 0.2 },
modulation: { type: "square" },
modulationEnvelope: { attack: 0.01, decay: 0.05, sustain: 0.2, release: 0.1 }
});
break;
case 1: // FM Synth (frequency modulation)
synth = new Tone.FMSynth({
harmonicity: Math.random() * 2 + 0.5,
modulationIndex: Math.random() * 10 + 1,
detune: Math.random() * 100 - 50,
oscillator: { type: "triangle" },
envelope: { attack: 0.001, decay: 0.2, sustain: 0.05, release: 0.5 },
modulation: { type: Math.random() > 0.5 ? "sine" : "sawtooth" },
modulationEnvelope: { attack: 0.005, decay: 0.1, sustain: 0.1, release: 0.2 }
});
break;
case 2: // Noise Synth with AutoFilter (noisy, sweeping sound)
synth = new Tone.NoiseSynth({
noise: { type: "pink" },
envelope: { attack: 0.005, decay: 0.05, sustain: 0.1, release: 0.1 }
});
const filter = new Tone.AutoFilter({
frequency: Math.random() * 5 + 0.1,
depth: Math.random() * 1,
baseFrequency: 200,
octaves: 4
}).start();
synth.connect(filter);
filter.toDestination(); // Connect the filter to the destination
break;
}
return synth;
}
// Sets the initial position and updates the 3D panner
setPosition(x, y, z) {
this.mesh.position.set(x, y, z);
this.originalPosition.copy(this.mesh.position);
this.panner.positionX.value = x;
this.panner.positionY.value = y;
this.panner.positionZ.value = z;
}
// Ensures Tone.js AudioContext is started on first user interaction
triggerActivation() {
if (!audioStarted) {
Tone.start();
audioStarted = true;
console.log("AudioContext started.");
// Fade out instructions after first interaction
document.getElementById('instructions').classList.add('fade-out');
}
}
// Handles avatar click interaction
onClick() {
this.triggerActivation();
if (this.state === AVATAR_STATES.NORMAL) {
this.setState(AVATAR_STATES.CLICKED, 0.15); // Brief visual feedback
// Play sound at Tone.now() (current time) with random volume
this.synth.triggerAttackRelease("C4", "8n", 0, Math.random() * 0.5 + 0.5);
} else if (this.state === AVATAR_STATES.GLITCHING) {
// Clicking a glitching avatar makes it break
this.breakApart();
} else {
// Clicking a broken/recombining avatar makes it recombine faster
this.recombine();
}
}
// Sets the avatar's state and state timer
setState(newState, duration = 0) {
this.state = newState;
this.stateStartTime = performance.now();
this.stateDuration = duration * 1000; // Convert duration from seconds to milliseconds
}
// Initiates the glitching visual and sound effects
glitch() {
if (this.state !== AVATAR_STATES.GLITCHING) {
this.setState(AVATAR_STATES.GLITCHING, 3 + Math.random() * 5); // Glitch for 3-8 seconds
this.synth.triggerAttackRelease(
Math.random() > 0.5 ? "A2" : "D#2", // Random low notes
"2n", 0, 0.7
);
}
this.glitchIntensity = 0.5 + Math.random() * 0.5; // Random intensity for jitter
}
// Causes the avatar to break apart into "pixel shards"
breakApart() {
if (this.state === AVATAR_STATES.BROKEN) return;
this.setState(AVATAR_STATES.BROKEN, 3 + Math.random() * 5); // Broken for 3-8 seconds
// Play a sharp, breaking sound
this.synth.triggerAttackRelease(
Math.random() > 0.5 ? "G#1" : "F1",
"4n", 0, 1.0
);
// Copy original vertices to the mutable data array
this.currentVerticesData.set(this.originalVertices);
this.velocityVertices = []; // Clear previous velocities
// Calculate a random velocity for each vertex based on its position relative to the center
for (let i = 0; i < this.originalVertices.length; i += 3) {
const vertexPos = new THREE.Vector3(
this.originalVertices[i], this.originalVertices[i + 1], this.originalVertices[i + 2]
);
const dir = vertexPos.sub(this.centerOfGravity).normalize(); // Direction from center
const speed = 0.05 + Math.random() * 0.1; // Base explosion speed
this.velocityVertices.push(
dir.x * speed + (Math.random() - 0.5) * 0.02, // Add some randomness for chaotic motion
dir.y * speed + (Math.random() - 0.5) * 0.02,
dir.z * speed + (Math.random() - 0.5) * 0.02
);
}
// If mesh is currently using basic geometry, dispose it and switch to BufferGeometry
if (this.mesh.geometry !== this.normalGeometry) {
this.mesh.geometry.dispose();
}
const brokenGeo = new THREE.BufferGeometry();
brokenGeo.setAttribute('position', new THREE.BufferAttribute(this.currentVerticesData, 3));
this.mesh.geometry = brokenGeo; // Assign the new BufferGeometry
// Keep mesh's global position/rotation/scale fixed while vertices move internally
this.mesh.scale.copy(this.originalScale);
this.mesh.rotation.copy(this.originalRotation);
this.mesh.position.copy(this.originalPosition);
}
// Initiates the recombination process for fractured avatars
recombine() {
if (this.state === AVATAR_STATES.RECOMBINING || this.state === AVATAR_STATES.NORMAL) return;
this.setState(AVATAR_STATES.RECOMBINING, 2 + Math.random() * 3); // Recombine over 2-5 seconds
// Play a more evolving, soothing sound
this.synth.triggerAttackRelease(
Math.random() > 0.5 ? "C2" : "F#2",
"1n", 0, 0.8
);
}
// Main update loop for avatar behavior
update(deltaTime) {
const now = performance.now();
const elapsedSinceStateChange = (now - this.stateStartTime);
const progress = this.stateDuration > 0 ? elapsedSinceStateChange / this.stateDuration : 0;
// Handle state transitions once duration is over
if (this.stateDuration > 0 && progress >= 1) {
if (this.state === AVATAR_STATES.CLICKED) {
this.glitch(); // After brief click feedback, start glitching
} else if (this.state === AVATAR_STATES.GLITCHING) {
if (Math.random() < 0.2) { // 20% chance to break after glitching
this.breakApart();
} else {
this.setState(AVATAR_STATES.NORMAL); // Otherwise, return to normal
}
} else if (this.state === AVATAR_STATES.BROKEN) {
this.recombine(); // After being broken for some time, start recombining
} else if (this.state === AVATAR_STATES.RECOMBINING) {
this.setState(AVATAR_STATES.NORMAL); // Once recombined, return to normal
}
}
// Apply state-specific visual and audial updates
switch (this.state) {
case AVATAR_STATES.NORMAL:
// Restore original geometry if it was a BufferGeometry from broken state
if (this.mesh.geometry !== this.normalGeometry) {
this.mesh.geometry.dispose(); // Dispose the temporary BufferGeometry
this.mesh.geometry = this.normalGeometry; // Revert to original BoxGeometry
}
this.mesh.visible = true; // Ensure avatar is visible
this.mesh.material.emissive.setHex(0x000000); // No emissive glow
this.mesh.scale.copy(this.originalScale); // Restore scale
this.mesh.rotation.x = this.originalRotation.x;
this.mesh.rotation.z = this.originalRotation.z; // Restore X and Z rotation
this.mesh.rotation.y += 0.005; // Gentle constant rotation
this.mesh.position.y = this.originalPosition.y + Math.sin(now * 0.001 + this.id) * 0.1; // Gentle bobbing motion
// Restore synth volume (if it was modulated during broken/recombining)
if (this.synth.volume) this.synth.volume.value = 0;
break;
case AVATAR_STATES.CLICKED:
// Brief scale pulse for click feedback
const clickScale = 1 + Math.sin(progress * Math.PI) * 0.2;
this.mesh.scale.set(clickScale, clickScale, clickScale);
this.mesh.material.emissive.setHex(0xFFFFFF); // Flash white
this.mesh.material.needsUpdate = true;
break;
case AVATAR_STATES.GLITCHING:
this.mesh.scale.copy(this.originalScale); // Ensure original scale during glitch
// Random visual flashes
if (Math.random() < 0.1) {
this.mesh.material.emissive.setHex(0xFF00FF); // Magenta flash
this.mesh.material.needsUpdate = true;
} else {
this.mesh.material.emissive.setHex(0x000000);
}
// Jitter position and rotation
this.mesh.position.x = this.originalPosition.x + (Math.random() - 0.5) * 0.1 * this.glitchIntensity;
this.mesh.position.y = this.originalPosition.y + (Math.random() - 0.5) * 0.1 * this.glitchIntensity;
this.mesh.position.z = this.originalPosition.z + (Math.random() - 0.5) * 0.1 * this.glitchIntensity;
this.mesh.rotation.x = this.originalRotation.x + (Math.random() - 0.5) * 0.5 * this.glitchIntensity;
this.mesh.rotation.y = this.originalRotation.y + (Math.random() - 0.5) * 0.5 * this.glitchIntensity;
this.mesh.rotation.z = this.originalRotation.z + (Math.random() - 0.5) * 0.5 * this.glitchIntensity;
// Modulate synth properties for glitchy sound
if (this.synth.harmonicity) this.synth.harmonicity.value = 0.5 + Math.random() * 3;
if (this.synth.detune) this.synth.detune.value = Math.random() * 2000 - 1000;
if (this.synth.volume) this.synth.volume.value = 0;
break;
case AVATAR_STATES.BROKEN:
// Animate vertices outwards (explosion effect)
for (let i = 0; i < this.currentVerticesData.length; i += 3) {
this.currentVerticesData[i] += this.velocityVertices[i] * deltaTime * 10;
this.currentVerticesData[i + 1] += this.velocityVertices[i + 1] * deltaTime * 10;
this.currentVerticesData[i + 2] += this.velocityVertices[i + 2] * deltaTime * 10;
}
this.mesh.geometry.attributes.position.needsUpdate = true; // Flag Three.js to use updated vertex data
// Fade out synth volume
if (this.synth.volume) {
this.synth.volume.value = Math.max(-40, Tone.Math.linToLog(1 - progress) -10);
}
break;
case AVATAR_STATES.RECOMBINING:
// Animate vertices back to original positions (implosion effect)
const recombineFactor = Math.sin(progress * Math.PI / 2); // Easing curve for smooth motion
for (let i = 0; i < this.currentVerticesData.length; i += 3) {
this.currentVerticesData[i] = THREE.MathUtils.lerp(
this.currentVerticesData[i], this.originalVertices[i], recombineFactor * 0.15
);
this.currentVerticesData[i + 1] = THREE.MathUtils.lerp(
this.currentVerticesData[i + 1], this.originalVertices[i + 1], recombineFactor * 0.15
);
this.currentVerticesData[i + 2] = THREE.MathUtils.lerp(
this.currentVerticesData[i + 2], this.originalVertices[i + 2], recombineFactor * 0.15
);
}
this.mesh.geometry.attributes.position.needsUpdate = true;
// Fade in synth volume
if (this.synth.volume) {
this.synth.volume.value = Math.min(0, Tone.Math.linToLog(progress) - 10);
}
break;
}
// Update panner position in Tone.js to match the avatar's global position.
// Note: when broken/recombining, the mesh's global position is fixed, but internal vertices move.
// The panner should follow the original static position for consistency during these states.
if (this.state === AVATAR_STATES.NORMAL || this.state === AVATAR_STATES.CLICKED || this.state === AVATAR_STATES.GLITCHING) {
this.panner.positionX.value = this.mesh.position.x;
this.panner.positionY.value = this.mesh.position.y;
this.panner.positionZ.value = this.mesh.position.z;
}
}
}
// --- Three.js Initialization ---
function init() {
// Scene: where all objects, cameras, and lights live
scene = new THREE.Scene();
scene.background = new THREE.Color(0x050510); // Dark blue background
scene.fog = new THREE.Fog(0x050510, 5, 20); // Fog to create depth effect
// Camera: defines what we see
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 2, 5); // Position the camera
camera.lookAt(0, 0, 0); // Point camera at the origin
// Renderer: renders the scene
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio); // Maintain quality on high-DPI screens
document.body.appendChild(renderer.domElement); // Add canvas to HTML
// Lights: illuminate the scene
const ambientLight = new THREE.AmbientLight(0x404040, 2); // Soft white ambient light
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1); // Directional light for shadows/highlights
directionalLight.position.set(5, 10, 7.5);
directionalLight.castShadow = true;
scene.add(directionalLight);
// Raycaster for detecting mouse interactions with 3D objects
raycaster = new THREE.Raycaster();
mouse = new THREE.Vector2(); // Stores normalized mouse coordinates (-1 to +1)
// Create Avatars and position them in a circle
const numAvatars = 5;
const radius = 3;
for (let i = 0; i < numAvatars; i++) {
const avatar = new Avatar(i);
const angle = (i / numAvatars) * Math.PI * 2; // Distribute evenly in a circle
avatar.setPosition(
Math.cos(angle) * radius,
0, // All avatars on the ground plane
Math.sin(angle) * radius
);
avatars.push(avatar);
}
// Event Listeners for user input
renderer.domElement.addEventListener('pointerdown', onPointerDown);
renderer.domElement.addEventListener('pointermove', onPointerMove);
renderer.domElement.addEventListener('pointerup', onPointerUp);
window.addEventListener('resize', onWindowResize); // Handle window resizing
animate(); // Start the animation loop
}
// --- Animation Loop ---
let lastTime = 0;
function animate(time) {
requestAnimationFrame(animate); // Request next frame
const deltaTime = (time - lastTime) / 1000; // Calculate time elapsed since last frame (in seconds)
lastTime = time;
avatars.forEach(avatar => avatar.update(deltaTime)); // Update each avatar's state and animation
// Update raycaster for hover effects only if not currently dragging an object
if (!isDragging) {
raycaster.setFromCamera(mouse, camera); // Set raycaster origin and direction from camera and mouse
const intersects = raycaster.intersectObjects(avatars.map(a => a.mesh)); // Find intersections with avatar meshes
if (intersects.length > 0) {
// If a new object is intersected, clear previous highlight and set new one
if (intersected != intersects[0].object) {
if (intersected && intersected.material.emissive) intersected.material.emissive.setHex(0x000000);
intersected = intersects[0].object;
if (intersected.material.emissive) intersected.material.emissive.setHex(0x333333); // Subtle hover glow
}
} else {
// No object intersected, clear any existing highlight
if (intersected && intersected.material.emissive) {
intersected.material.emissive.setHex(0x000000);
}
intersected = null;
}
}
renderer.render(scene, camera); // Render the scene
}
// --- Event Handlers ---
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function onPointerDown(event) {
// Convert mouse coordinates to normalized device coordinates (-1 to +1)
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
let targetMesh = null;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(avatars.map(a => a.mesh));
if (intersects.length > 0) {
targetMesh = intersects[0].object;
selectedAvatar = avatars.find(a => a.mesh === targetMesh);
if (selectedAvatar) {
isDragging = true;
selectedAvatar.onClick(); // Trigger avatar's click behavior
// Calculate the offset between the intersection point and the object's origin
// This prevents the object from "jumping" to the mouse position when dragged
const intersectionPoint = intersects[0].point;
offset.copy(selectedAvatar.mesh.position).sub(intersectionPoint);
// Clear hover highlight if an object is selected to drag
if(intersected && intersected.material.emissive) intersected.material.emissive.setHex(0x000000);
intersected = null;
}
} else {
selectedAvatar = null; // Clicked on empty space
}
}
function onPointerMove(event) {
if (!isDragging || !selectedAvatar) return;
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
// Create a plane at the object's current depth, parallel to the camera's view
const movePlane = new THREE.Plane().setFromNormalAndCoplanarPoint(
camera.getWorldDirection(new THREE.Vector3()),
selectedAvatar.mesh.position
);
const intersectPoint = new THREE.Vector3();
// Find the intersection point of the ray with this plane
raycaster.ray.intersectPlane(movePlane, intersectPoint);
if (intersectPoint) {
// Apply the calculated offset to get the new position
const newPosition = intersectPoint.add(offset);
selectedAvatar.mesh.position.copy(newPosition);
selectedAvatar.originalPosition.copy(newPosition); // Update original position for later reference
}
}
function onPointerUp() {
isDragging = false;
selectedAvatar = null;
}
// Start the application
init();
</script>
</body>
</html>
``````html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Glitch Avatars</title>
<style>
body { margin: 0; overflow: hidden; background-color: #000; }
canvas { display: block; }
</style>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.7/dat.gui.min.css">
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.7.77/Tone.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.7/dat.gui.min.js"></script>
<script>
(function() {
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
const avatars = [];
const numAvatars = 5; // Adjustable number of avatars
const shards = [];
const maxShardsPerAvatar = 30;
// Tone.js setup
const synth = new Tone.Synth().toDestination();
const reverb = new Tone.Reverb(0.5).toDestination();
function getRandomNote() {
const notes = ["C4", "D4", "E4", "F4", "G4", "A4", "B4", "C5", "D5"];
return notes[Math.floor(Math.random() * notes.length)];
}
function playSound() {
const note = getRandomNote();
const duration = "2n";
const velocity = 0.5 + Math.random() * 0.5;
synth.triggerAttackRelease(note, duration, velocity);
reverb.process(synth);
}
class Avatar {
constructor(index) {
this.index = index;
this.geometry = new THREE.SphereGeometry( 1, 32, 32 );
const material = new THREE.MeshBasicMaterial( { color: Math.random() * 0xffffff } );
this.mesh = new THREE.Mesh( this.geometry, material );
scene.add( this.mesh );
this.mesh.position.x = (Math.random() - 0.5) * 4;
this.mesh.position.y = Math.random() * 6 - 3;
this.mesh.position.z = (Math.random() - 0.5) * 4;
this.isBreaking = false;
this.breakTimer = 0;
this.onClick = () => {
playSound();
if (!this.isBreaking) {
this.breakApart();
}
};
this.onDrag = () => {
playSound();
// do something on drag
this.mesh.rotation.x += 0.05;
this.mesh.rotation.y += 0.05;
};
this.initEventListeners();
}
initEventListeners() {
this.mesh.addEventListener('click', this.onClick, false);
let isDragging = false;
let prevMouseX = 0;
let prevMouseY = 0;
this.mesh.addEventListener('mousedown', (event) => {
isDragging = true;
prevMouseX = event.clientX;
prevMouseY = event.clientY;
this.onDrag();
}, false);
document.addEventListener('mouseup', () => {
isDragging = false;
}, false);
document.addEventListener('mousemove', (event) => {
if (isDragging) {
const deltaX = event.clientX - prevMouseX;
const deltaY = event.clientY - prevMouseY;
this.mesh.rotation.y += deltaX * 0.005;
this.mesh.rotation.x += deltaY * 0.005;
prevMouseX = event.clientX;
prevMouseY = event.clientY;
this.onDrag();
}
}, false);
}
breakApart() {
this.isBreaking = true;
this.breakTimer = 0;
for (let i = 0; i < maxShardsPerAvatar; i++) {
const shardGeometry = new THREE.SphereGeometry( 0.1, 16, 16 );
const shardMaterial = new THREE.MeshBasicMaterial( { color: this.mesh.material.color.clone() } );
const shard = new THREE.Mesh( shardGeometry, shardMaterial );
shard.position.copy(this.mesh.position);
shard.position.x += (Math.random() - 0.5) * 2;
shard.position.y += (Math.random() - 0.5) * 2;
shard.position.z += (Math.random() - 0.5) * 2;
const velocityX = (Math.random() - 0.5) * 5;
const velocityY = (Math.random() - 0.5) * 5;
const velocityZ = (Math.random() - 0.5) * 5;
shards.push({
mesh: shard,
velocityX: velocityX,
velocityY: velocityY,
velocityZ: velocityZ
});
scene.add(shard);
}
this.mesh.visible = false;
}
recombine() {
this.isBreaking = false;
this.mesh.visible = true;
this.mesh.position.x = (Math.random() - 0.5) * 4;
this.mesh.position.y = Math.random() * 6 - 3;
this.mesh.position.z = (Math.random() - 0.5) * 4;
}
}
for (let i = 0; i < numAvatars; i++) {
avatars.push(new Avatar(i));
}
function animate() {
requestAnimationFrame( animate );
// Shard movement and recombination
for (let i = 0; i < shards.length; i++) {
const shard = shards[i];
shard.mesh.position.x += shard.velocityX * 0.01;
shard.mesh.position.y += shard.velocityY * 0.01;
shard.mesh.position.z += shard.velocityZ * 0.01;
shard.velocityX *= 0.99;
shard.velocityY *= 0.99;
shard.velocityZ *= 0.99;
if (shard.mesh.position.y < -5) {
scene.remove(shard.mesh);
shards.splice(i, 1);
i--;
}
}
//Check if avatars should recombine;
avatars.forEach(avatar => {
if (avatar.isBreaking) {
avatar.breakTimer += 1;
if (avatar.breakTimer > 100) {
avatar.recombine();
}
}
});
camera.lookAt(new THREE.Vector3(0, 0, 0));
renderer.render( scene, camera );
}
animate();
window.addEventListener( 'resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
}, false );
})();
</script>
</body>
</html>
``````html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Glitchy Avatars with Sound & Chaos</title>
<style>
html, body {
background: #181924;
margin: 0; padding: 0;
width: 100vw; height: 100vh;
overflow: hidden;
}
#ui {
position: fixed;
top: 10px; left: 10px;
color: #FFF;
font-family: monospace;
font-size: 15px;
user-select: none;
z-index: 10;
text-shadow: 0 1px 7px #0008;
background: #222b;
border-radius: 7px;
padding: 7px 12px;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/three@0.155.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.155.0/examples/js/controls/OrbitControls.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/tone@14.8.49/build/Tone.min.js"></script>
</head>
<body>
<div id="ui">
<b>Glitchy Avatars</b><br>
Click/drag avatars to glitch. <br>
Listen to the sound chaos.<br>
<small>By AI</small>
</div>
<script>
// === PARAMETERS ===
const NUM_AVATARS = 5; // How many avatars
const SHARD_COLORS = [0x87e0ff, 0xff58b2, 0xffec89, 0x7cfd90, 0xb08afe, 0xff5252];
const AVATAR_COLORS = [0x46e6e6, 0xd2fdff, 0xff949c, 0x99ff7f, 0x836fff];
const AVATAR_SIZES = [1.6, 1.3, 1.8, 1.1, 1.5];
const GLICH_PROB = 0.04;
// === THREE.JS BOOTSTRAP ===
let scene = new THREE.Scene();
scene.background = new THREE.Color(0x181924);
let camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 100);
camera.position.set(0, 3.6, 9);
let renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setClearColor(0x181924);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// Orbit controls just for fun (disable on pointer down)
let controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
// Lights
{
const l1 = new THREE.DirectionalLight(0xffffff, 1.0);
l1.position.set(5,9,5);
scene.add(l1);
scene.add(new THREE.AmbientLight(0x404050, 0.65));
}
// === AVATAR SYSTEM ===
class GlitchyAvatar {
constructor(idx, position, color, soundPatch) {
// Parameters
this.idx = idx;
this.state = "normal"; // normal|glitching|broken
this.glitchTimer = 0.0;
this.dragged = false;
this.shatterTimeout = null;
this.soundPatch = soundPatch;
// Avatar object group
this.group = new THREE.Group();
this.group.position.copy(position);
// Avatar "figure": head, odd body, arms
let bodyColor = color;
this.components = [];
// Body
let bodyGeom = new THREE.CylinderGeometry(0.5, 0.7, 1.7, 4, 2, true);
let bodyMat = new THREE.MeshPhysicalMaterial({color: bodyColor, roughness:0.45, metalness:0.3, transmission:0.25, clearcoat:0.3});
let body = new THREE.Mesh(bodyGeom, bodyMat);
body.position.y = 0.8;
body.rotation.y = Math.PI/6 + 0.1*(Math.random()-0.5);
this.group.add(body); this.components.push(body);
// Head
let headGeom = new THREE.OctahedronGeometry(0.48,1);
let headMat = new THREE.MeshPhysicalMaterial({color: bodyColor, roughness:0.27, metalness:0.5, emissive:0x1ff});
let head = new THREE.Mesh(headGeom, headMat);
head.position.set(0, 1.8, 0);
head.rotation.y = Math.random() * Math.PI;
this.group.add(head); this.components.push(head);
// Arms
for (let side of [-1, 1]) {
let armGeom = new THREE.BoxGeometry(0.15, 0.7, 0.15);
let armMat = new THREE.MeshPhysicalMaterial({color: bodyColor, roughness:0.6, metalness:0.15});
let arm = new THREE.Mesh(armGeom, armMat);
arm.position.set(side*0.52, 1.1, Math.random()*0.12-0.06);
arm.rotation.z = side * Math.PI/7;
arm.rotation.x = 0.2*(Math.random()-0.5);
this.group.add(arm); this.components.push(arm);
}
// Glitchy "aura" (pixel plane shards hidden by default)
this.shards = [];
this.group.add(this.createShards());
// Add to scene
scene.add(this.group);
}
createShards() {
// Create a group of pixel floating shards, initially hidden
let g = new THREE.Group();
g.visible = false;
for (let i = 0; i < 32+Math.floor(16*Math.random()); i++) {
let sz = Math.random()*0.23 + 0.09;
let mat = new THREE.MeshPhysicalMaterial({
color: SHARD_COLORS[Math.floor(Math.random()*SHARD_COLORS.length)],
roughness: 0.45, metalness: 0.5,
flatShading: true, transparent:true, opacity:0.8
});
let shard = new THREE.Mesh(
new THREE.BoxGeometry(sz, sz, sz),
mat
);
shard.position.set(
THREE.MathUtils.randFloat(-0.7,0.7),
THREE.MathUtils.randFloat(0.3,2.1),
THREE.MathUtils.randFloat(-0.7,0.7)
);
shard.startPos = shard.position.clone();
g.add(shard);
this.shards.push(shard);
}
return g;
}
setState(newState) {
if(this.state === newState) return;
this.state = newState;
if(newState === "broken") {
this.group.children.forEach(c=>{
if (!c.geometry) return;
c.visible = false;
});
if(this.group.children[this.group.children.length-1]) {
this.group.children[this.group.children.length-1].visible = true;
}
this.soundPatch.triggerAttackRelease('C5', '8n', '+0.01');
}
else if(newState === "normal") {
this.group.children.forEach(c=>{
if(!c.geometry) return;
c.visible = true;
});
if(this.group.children[this.group.children.length-1]) {
this.group.children[this.group.children.length-1].visible = false;
}
}
}
glitch(now) {
if(this.state === "broken") return;
this.state = "glitching";
this.glitchTimer = now + 0.44 + Math.random()*0.4;
this.group.children.forEach((c, i)=>{
if(c.geometry) {
c.rotation.x = (Math.sin(now*4+(this.idx+i)*2) * 0.5) * Math.random();
c.rotation.y = (Math.cos(now*4-(this.idx+i)*2) * 0.5) * Math.random();
}
});
// Play a unpredictable sound blip when glitching
let freq = 440 + Math.random()*350;
let dur = "32n";
this.soundPatch.triggerAttackRelease(freq, dur, '+0.01');
}
breakApart() {
this.setState("broken");
// Animate shards outwards with random velocity
this.shards.forEach(shard=>{
shard.position.copy(shard.startPos);
shard.userData.vel = new THREE.Vector3(
(Math.random()-0.5)*1.5,
Math.random()*2.5,
(Math.random()-0.5)*1.5
);
shard.userData.floatY = (Math.random()-0.5)*2;
});
// After random time, recombine
clearTimeout(this.shatterTimeout);
this.shatterTimeout = setTimeout(()=>{
this.recombine();
}, 950 + Math.random()*1800);
}
recombine() {
// Animate shards to original positions & hide shards
let t0 = performance.now();
let that = this;
function lerpShards() {
let t = (performance.now() - t0) / 650;
if (t < 1) {
that.shards.forEach(shard => {
shard.position.lerp(shard.startPos, t);
});
requestAnimationFrame(lerpShards);
} else {
that.shards.forEach(shard => {
shard.position.copy(shard.startPos);
});
that.setState("normal");
}
}
lerpShards();
}
// Update internal animations; if broken, animate shards
update(now, delta) {
if (this.state === "glitching" && now > this.glitchTimer) {
// Chance to break apart after glitching
if (Math.random() < 0.77) this.setState("normal");
else this.breakApart();
}
if (this.state === "broken") {
// Animate shards floating outward and upwards
if(this.group.children[this.group.children.length-1]) {
let g = this.group.children[this.group.children.length-1];
g.visible = true;
this.shards.forEach((shard,i)=>{
shard.position.addScaledVector(shard.userData.vel, delta*0.73);
shard.position.y += Math.sin(now*3+i*0.5)*0.007*shard.userData.floatY;
// Slow gravity
shard.userData.vel.y -= delta*0.53;
if(shard.position.y < 0.2) shard.userData.vel.y = Math.abs(shard.userData.vel.y)*0.47 + Math.random()*0.12;
});
}
}
}
// Check if a world position is close to this group (for picking)
isNear(worldPos) {
let dist = this.group.position.distanceTo(worldPos);
return dist < 1.4;
}
}
// === CREATE SOUND PATCHES — Tone.js Synths ===
function makePatch(idx) {
// Each patch is a poly synth with a different preset
switch(idx % 5) {
case 0: // glitchy FM
return new Tone.Synth({
oscillator: { type: 'fatsawtooth', count: 3, spread: 15 },
envelope: { attack: 0.02, decay: 0.13, sustain: 0.33, release: 0.23 }
}).connect(new Tone.Chorus(2, 1.5, 0.6).toDestination()).toDestination();
case 1: // glassy
return new Tone.AMSynth({
harmonicity: 1.95,
envelope: { attack: 0.1, decay: 0.11, sustain: 0.46, release: 0.17 }
}).connect(new Tone.Reverb(1.3)).toDestination();
case 2: // pinky metallic
return new Tone.DuoSynth().connect(new Tone.PingPongDelay('4n', 0.25)).toDestination();
case 3: // crispy digital
return new Tone.FMSynth({
modulationIndex: 2.7,
envelope: { attack: 0.03, decay: 0.1, sustain: 0.21, release: 0.09 }
}).connect(new Tone.BitCrusher(2)).toDestination();
case 4: // lofi percussive
return new Tone.MembraneSynth({
envelope: { attack: 0.005, decay: 0.1, sustain: 0.1, release: 0.2 }
}).toDestination();
}
}
// === INIT AVATARS ===
let AVATARS = [];
for (let i=0; i<NUM_AVATARS; ++i) {
let theta = i/NUM_AVATARS * Math.PI*2;
let rad = 3.2 + 0.7*Math.random();
let pos = new THREE.Vector3(
Math.cos(theta)*rad,
0,
Math.sin(theta)*rad
);
AVATARS.push(
new GlitchyAvatar(
i,
pos,
AVATAR_COLORS[i%AVATAR_COLORS.length],
makePatch(i)
)
);
}
// === RAYCASTING & DRAG SYSTEM ===
let raycaster = new THREE.Raycaster(), pointer = new THREE.Vector2();
let draggingAvatar = null, dragOffset = new THREE.Vector3();
function getWorldPosFromPointer(x, y) {
pointer.x = (x/renderer.domElement.width)*2 - 1;
pointer.y = - (y/renderer.domElement.height)*2 + 1;
raycaster.setFromCamera(pointer, camera);
let groundPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
let intersection = new THREE.Vector3();
raycaster.ray.intersectPlane(groundPlane, intersection);
return intersection;
}
function pickAvatar(x, y) {
for(let av of AVATARS) {
let worldPos = getWorldPosFromPointer(x, y);
if(av.isNear(worldPos)) return av;
}
return null;
}
renderer.domElement.addEventListener('pointerdown', e=>{
// Resume Tone.js if necessary
if(Tone && Tone.getContext() && Tone.context.state !== 'running') Tone.start();
let av = pickAvatar(e.offsetX, e.offsetY);
if(av) {
draggingAvatar = av;
av.dragged = true;
let wp = getWorldPosFromPointer(e.offsetX, e.offsetY);
dragOffset = av.group.position.clone().sub(wp);
// Trigger glitch for fun
av.glitch(performance.now()*0.001);
}
});
renderer.domElement.addEventListener('pointermove', e=>{
if (draggingAvatar) {
let wp = getWorldPosFromPointer(e.offsetX, e.offsetY);
draggingAvatar.group.position.copy(wp.clone().add(dragOffset));
// Add a little extra random shakiness
draggingAvatar.group.position.x += Math.sin(performance.now()*0.0021)*0.09;
draggingAvatar.group.position.z += Math.cos(performance.now()*0.0021)*0.09;
if (Math.random() < 0.15) draggingAvatar.glitch(performance.now()*0.001);
}
});
renderer.domElement.addEventListener('pointerup', ()=>{
if (draggingAvatar) {
draggingAvatar.dragged = false;
draggingAvatar = null;
}
});
// === AVATAR REACTIVITY — spontaneous chaos ===
function spontaneousGlitch(now) {
// Each has a small chance to auto-glitch or break apart on its own
AVATARS.forEach(av=>{
if(av.state === "normal" && Math.random()<GLICH_PROB) av.glitch(now);
else if(av.state === "normal" && Math.random()<GLICH_PROB*0.3) av.breakApart();
});
}
// === CHAOTIC MAIN RENDER-UPDATE LOOP ===
let lastTime = performance.now();
function animate() {
let now = performance.now();
let dt = (now-lastTime)/1000.0;
lastTime = now;
let nowSec = now*0.001;
spontaneousGlitch(nowSec);
AVATARS.forEach((av,i)=>{
// Let avatars wiggle subtly
if(av.state === "normal") {
av.group.children.forEach((c,j)=>{
if(c.geometry) {
c.rotation.x = Math.sin(nowSec*1.4+av.idx*1.35+j)*0.13 + Math.sin(nowSec*4.8+j)*0.03;
c.rotation.y += Math.sin(nowSec*7.1+i+j)*0.015;
}
});
av.group.position.y = Math.sin(nowSec*0.83+av.idx)*0.10 + 0.02*(Math.random()-0.5);
}
av.update(nowSec, dt);
});
controls.enabled = !draggingAvatar;
controls.update();
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
animate();
window.addEventListener('resize', ()=>{
camera.aspect = window.innerWidth/window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>
```
```html
<!DOCTYPE html>
<html>
<head>
<title>Glitch Avatars</title>
<style>body{margin:0;overflow:hidden;background:black}</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r148/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.148.0/examples/js/controls/OrbitControls.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.7.79/Tone.min.js"></script>
</head>
<body>
<script>
let scene, camera, renderer, controls, shardsArray = [];
const avatars = [];
init();
function init() {
// Three.js setup
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000);
camera.position.z = 10;
renderer = new THREE.WebGLRenderer({antialias: true, alpha: true});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
controls = new THREE.OrbitControls(camera, renderer.domElement);
// Create avatars
for (let i = 0; i < 3; i++) {
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({
color: Math.random() * 0xffffff,
wireframe: true });
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(
(Math.random() - 0.5) * 12,
(Math.random() - 0.5) * 8,
(Math.random() - 0.5) * 8
);
scene.add(mesh);
// Tone.js setup
const synth = new Tone.PolySynth(Tone.Synth).toDestination();
synth.set({
oscillator: { type: ['sine', 'square', 'sawtooth'][i % 3] },
envelope: { attack: 0.1, decay: 0.2, sustain: 0.5, release: 1 }
});
avatars.push({ mesh, geometry, material, synth });
}
// Event listeners
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth/window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
document.addEventListener('click', (e) => {
const mouse = new THREE.Vector2(
(e.clientX / window.innerWidth) * 2 - 1,
-(e.clientY / window.innerHeight) * 2 + 1
);
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(avatars.map(a => a.mesh));
if (intersects.length > 0) {
const avatar = avatars.find(a => a.mesh === intersects[0].object);
if (Tone.Transport.state !== 'started') Tone.Transport.start();
avatar.synth.triggerAttackRelease('C4', '8n');
breakAvatar(avatar);
}
});
animate();
}
function breakAvatar(avatar) {
// Remove original mesh
scene.remove(avatar.mesh);
const originalPos = avatar.mesh.position.clone();
// Create shards
const shardCount = 15;
const shards = [];
for (let i = 0; i < shardCount; i++) {
const shardGeo = new THREE.BoxGeometry(0.15, 0.15, 0.15);
const shardMat = new THREE.MeshBasicMaterial({
color: avatar.material.color.getHex() });
const shard = new THREE.Mesh(shardGeo, shardMat);
shard.position.set(
originalPos.x + (Math.random() - 0.5) * 2,
originalPos.y + (Math.random() - 0.5) * 2,
originalPos.z + (Math.random() - 0.5) * 2
);
scene.add(shard);
shards.push(shard); // Store reference
}
shardsArray.push(...shards); // Add to global array
// Schedule recombination
setTimeout(() => {
// Remove shards
shards.forEach(s => {
scene.remove(s);
s.geometry.dispose();
s.material.dispose();
});
// Recreate avatar
const newMesh = new THREE.Mesh(avatar.geometry, avatar.material.clone());
newMesh.position.copy(originalPos);
scene.add(newMesh);
avatar.mesh = newMesh;
}, 2000);
}
function animate() {
requestAnimationFrame(animate);
// Animate shards
shardsArray = shardsArray.filter(shard => {
if (!scene.children.includes(shard)) return false;
shard.rotation.x += 0.05 + Math.random() * 0.02;
shard.rotation.y += 0.05 + Math.random() * 0.02;
shard.position.y += 0.02;
return true;
});
renderer.render(scene, camera);
}
</script>
</body>
</html>
```Here's a complete HTML code that creates a scene with glitchy avatars using Three.js, Tone.js for sound, and user interactions:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Glitchy Avatars</title>
<style>
body { margin: 0; overflow: hidden; }
canvas { display: block; }
</style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.7.78/Tone.min.js"></script>
<script>
// Scene setup
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// Lighting
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
// Tone.js setup
const synths = [];
for (let i = 0; i < 5; i++) {
synths.push(new Tone.Synth().toDestination());
}
// Avatar class
class Avatar {
constructor(x, y, z, color, size) {
this.group = new THREE.Group();
this.color = color;
this.size = size;
this.shards = [];
this.isBroken = false;
this.synth = synths[Math.floor(Math.random() * synths.length)];
// Create shards
for (let i = 0; i < 10; i++) {
const shardGeometry = new THREE.BoxGeometry(size / 5, size / 5, size / 5);
const shardMaterial = new THREE.MeshPhongMaterial({ color: color });
const shard = new THREE.Mesh(shardGeometry, shardMaterial);
shard.position.set(
(Math.random() - 0.5) * size,
(Math.random() - 0.5) * size,
(Math.random() - 0.5) * size
);
this.shards.push(shard);
this.group.add(shard);
}
// Main avatar geometry
const geometry = new THREE.BoxGeometry(size, size, size);
const material = new THREE.MeshPhongMaterial({ color: color });
this.main = new THREE.Mesh(geometry, material);
this.group.add(this.main);
// Position the avatar
this.group.position.set(x, y, z);
scene.add(this.group);
// Randomize the avatar
this.randomize();
}
randomize() {
this.main.rotation.set(
Math.random() * Math.PI,
Math.random() * Math.PI,
Math.random() * Math.PI
);
}
breakApart() {
if (this.isBroken) return;
this.isBroken = true;
this.main.visible = false;
for (let shard of this.shards) {
shard.userData.velocity = {
x: (Math.random() - 0.5) * 0.1,
y: (Math.random() - 0.5) * 0.1,
z: (Math.random() - 0.5) * 0.1
};
}
this.synth.triggerAttackRelease("C4", "8n");
}
recombine() {
if (!this.isBroken) return;
this.isBroken = false;
this.main.visible = true;
for (let shard of this.shards) {
shard.position.set(
(Math.random() - 0.5) * this.size,
(Math.random() - 0.5) * this.size,
(Math.random() - 0.5) * this.size
);
shard.userData.velocity = { x: 0, y: 0, z: 0 };
}
this.synth.triggerAttackRelease("E4", "8n");
}
update() {
if (this.isBroken) {
for (let shard of this.shards) {
shard.position.x += shard.userData.velocity.x;
shard.position.y += shard.userData.velocity.y;
shard.position.z += shard.userData.velocity.z;
shard.userData.velocity.y -= 0.001; // Gravity
}
}
}
}
// Create avatars
const avatars = [];
const colors = [0xff0000, 0x00ff00, 0x0000ff, 0xffff00, 0xff00ff];
for (let i = 0; i < 5; i++) {
avatars.push(new Avatar(
(Math.random() - 0.5) * 20,
(Math.random() - 0.5) * 20,
(Math.random() - 0.5) * 20,
colors[i],
1 + Math.random() * 2
));
}
// Camera position
camera.position.z = 25;
// Raycaster for interactions
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
// Handle window resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// Handle mouse move
window.addEventListener('mousemove', (event) => {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
});
// Handle mouse click
window.addEventListener('click', (event) => {
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(avatars.map(a => a.group));
if (intersects.length > 0) {
const avatar = avatars.find(a => a.group === intersects[0].object.parent);
if (avatar) {
if (Math.random() > 0.5) {
avatar.breakApart();
} else {
avatar.recombine();
}
}
}
});
// Handle touch events
window.addEventListener('touchmove', (event) => {
if (event.touches.length > 0) {
mouse.x = (event.touches[0].clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.touches[0].clientY / window.innerHeight) * 2 + 1;
}
});
window.addEventListener('touchstart', (event) => {
if (event.touches.length > 0) {
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(avatars.map(a => a.group));
if (intersects.length > 0) {
const avatar = avatars.find(a => a.group === intersects[0].object.parent);
if (avatar) {
if (Math.random() > 0.5) {
avatar.breakApart();
} else {
avatar.recombine();
}
}
}
}
});
// Animation loop
function animate() {
requestAnimationFrame(animate);
// Randomly break or recombine avatars
if (Math.random() < 0.01) {
const avatar = avatars[Math.floor(Math.random() * avatars.length)];
if (Math.random() > 0.5) {
avatar.breakApart();
} else {
avatar.recombine();
}
}
// Update avatars
for (let avatar of avatars) {
avatar.update();
}
// Rotate camera slightly
camera.position.x = Math.sin(Date.now() * 0.0005) * 20;
camera.position.y = Math.sin(Date.now() * 0.0003) * 10;
camera.lookAt(0, 0, 0);
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>
```
This code creates a scene with 5 avatars that can be interacted with by clicking or touching them. Each avatar has a unique color and sound. The avatars can break apart into pixel shards and recombine, creating a chaotic dance of sound and form. The camera slowly orbits around the scene.Here's a full HTML code that creates a set of glitchy avatars using Three.js and Tone.js. The avatars react to user interactions and emit unique sounds. Please note that you'll need to host this on a server to properly load the external libraries and assets.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Glitchy Avatars</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #000;
}
canvas {
display: block;
}
</style>
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
<script src="https://cdn.jsdelivr.net/npm/tone@14.7.77/build/Tone.min.js"></script>
<script>
// Initialize Three.js scene
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// Add orbit controls
const controls = new THREE.OrbitControls(camera, renderer.domElement);
camera.position.z = 5;
// Initialize Tone.js
const synths = [];
const now = Tone.now();
// Create avatars
const avatars = [];
const avatarCount = 5;
const avatarGeometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
const avatarMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, wireframe: true });
for (let i = 0; i < avatarCount; i++) {
const avatar = new THREE.Mesh(avatarGeometry, avatarMaterial.clone());
avatar.position.x = (Math.random() - 0.5) * 10;
avatar.position.y = (Math.random() - 0.5) * 10;
avatar.position.z = (Math.random() - 0.5) * 10;
// Create a unique sound for each avatar
const synth = new Tone.Synth({
oscillator: {
type: ['sine', 'square', 'triangle', 'sawtooth'][Math.floor(Math.random() * 4)]
},
envelope: {
attack: Math.random() * 0.5,
decay: Math.random() * 0.5,
sustain: Math.random(),
release: Math.random() * 1.5
}
}).toDestination();
synths.push(synth);
avatars.push({ mesh: avatar, synth: synth, glitching: false, glitchProgress: 0 });
scene.add(avatar);
}
// Handle clicks and drags
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let draggedAvatar = null;
function onMouseMove(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
if (draggedAvatar) {
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(avatars.map(avatar => avatar.mesh));
if (intersects.length > 0) {
draggedAvatar.mesh.position.copy(intersects[0].point);
}
}
}
function onMouseDown(event) {
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(avatars.map(avatar => avatar.mesh));
if (intersects.length > 0) {
draggedAvatar = avatars.find(avatar => avatar.mesh === intersects[0].object);
draggedAvatar.synth.triggerAttack('C4', now);
}
}
function onMouseUp() {
if (draggedAvatar) {
draggedAvatar.synth.triggerRelease(now + 0.1);
draggedAvatar = null;
}
}
window.addEventListener('mousemove', onMouseMove, false);
window.addEventListener('mousedown', onMouseDown, false);
window.addEventListener('mouseup', onMouseUp, false);
// Animation loop
function animate() {
requestAnimationFrame(animate);
avatars.forEach(avatar => {
if (!avatar.glitching && Math.random() < 0.01) {
avatar.glitching = true;
avatar.glitchProgress = 0;
}
if (avatar.glitching) {
avatar.glitchProgress += 0.05;
const glitchFactor = Math.sin(avatar.glitchProgress * Math.PI * 2) * 0.5;
avatar.mesh.geometry.vertices.forEach(vertex => {
vertex.x += (Math.random() - 0.5) * glitchFactor * 0.1;
vertex.y += (Math.random() - 0.5) * glitchFactor * 0.1;
vertex.z += (Math.random() - 0.5) * glitchFactor * 0.1;
});
avatar.mesh.geometry.verticesNeedUpdate = true;
if (avatar.glitchProgress >= 1) {
avatar.glitching = false;
avatar.mesh.geometry.vertices.forEach(vertex => {
vertex.x = vertex._originalX || vertex.x;
vertex.y = vertex._originalY || vertex.y;
vertex.z = vertex._originalZ || vertex.z;
});
avatar.mesh.geometry.verticesNeedUpdate = true;
} else {
avatar.mesh.geometry.vertices.forEach(vertex => {
vertex._originalX = vertex._originalX || vertex.x;
vertex._originalY = vertex._originalY || vertex.y;
vertex._originalZ = vertex._originalZ || vertex.z;
});
}
}
});
controls.update();
renderer.render(scene, camera);
}
animate();
// Handle window resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>
```
This code creates a 3D scene with glitchy avatars that react to user interactions. Each avatar emits a unique sound when clicked and dragged. The avatars occasionally "break" apart into pixel shards and recombine, creating a chaotic dance of sound and form. The code includes all necessary libraries (Three.js and Tone.js) and handles user interactions such as clicks and drags.```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Glitchy Avatars with Three.js and Tone.js</title>
<style>
body { margin: 0; overflow: hidden; background: #000; }
canvas { display: block; }
</style>
<!-- Include Three.js and Tone.js via CDN -->
<script src="https://cdn.jsdelivr.net/npm/three@0.150.1/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/tone@14.8.33/build/Tone.min.js"></script>
</head>
<body>
<script>
// --- Initialization ---
let scene, camera, renderer, avatars = [], shards = [], raycaster, mouse = new THREE.Vector2();
let interactionActive = false;
const avatarCount = 4; // Number of avatars
const shardCount = 200; // Number of pixel shards
init();
animate();
function init() {
// Scene and Camera
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 1000);
camera.position.z = 50;
// Renderer
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// Raycaster for interaction
raycaster = new THREE.Raycaster();
// Light
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(0, 50, 50);
scene.add(light);
// Create avatars
for(let i=0; i<avatarCount; i++) {
const avatar = createAvatar(i);
scene.add(avatar.mesh);
avatars.push(avatar);
}
// Create shards
for(let i=0; i<shardCount; i++) {
const shard = createShard();
scene.add(shard.mesh);
shards.push(shard);
}
// Event listeners
window.addEventListener('resize', onWindowResize, false);
window.addEventListener('mousedown', onMouseDown, false);
window.addEventListener('mouseup', onMouseUp, false);
window.addEventListener('mousemove', onMouseMove, false);
}
// Avatar class
class Avatar {
constructor(index) {
this.index = index;
this.group = new THREE.Group();
// Create a simple mascot shape - a sphere with different colors
const geometry = new THREE.SphereGeometry(2, 32, 32);
const material = new THREE.MeshStandardMaterial({ color: new THREE.Color(`hsl(${index * 90}, 70%, 50%)`) });
this.mesh = new THREE.Mesh(geometry, material);
this.mesh.position.x = (index - 1.5) * 8;
// Add slight pulsing animation
this.pulseDirection = 1;
this.scale = 1;
// Sound setup
this.synth = new Tone.Synth().toDestination();
this.setupSoundPatch();
// State
this.breaking = false;
this.shards = [];
}
setupSoundPatch() {
const waveformOptions = ['sine', 'square', 'triangle', 'sawtooth'];
const waveType = waveformOptions[this.index % waveformOptions.length];
this.synth.type = waveType;
}
triggerSound() {
const freq = Math.random() * 400 + 200;
this.synth.triggerAttackRelease(freq, "8n");
}
breakApart() {
if(this.breaking) return;
this.breaking = true;
// Generate shards
for(let i=0; i<20; i++) {
const shard = createShard();
shard.mesh.position.copy(this.mesh.position);
// Sprinkle in velocity
shard.velocity = new THREE.Vector3(
(Math.random() - 0.5) * 2,
Math.random() * 2,
(Math.random() - 0.5) * 2
);
shards.push(shard);
}
// Remove avatar mesh
scene.remove(this.mesh);
}
update(delta) {
// Pulsing animation
this.scale += this.pulseDirection * delta * 0.5;
if(this.scale > 1.2 || this.scale < 0.8) {
this.pulseDirection *= -1;
}
this.mesh.scale.setScalar(this.scale);
// if breaking, animate shards
if(this.breaking) return;
}
}
// Create an avatar
function createAvatar(index) {
return new Avatar(index);
}
// Shard class
class Shard {
constructor() {
const geometry = new THREE.PlaneGeometry(0.5, 0.5);
const material = new THREE.MeshStandardMaterial({ color: Math.random() * 0xffffff, transparent: true, opacity: 0.8 });
this.mesh = new THREE.Mesh(geometry, material);
this.velocity = new THREE.Vector3();
}
update(delta) {
this.mesh.position.addScaledVector(this.velocity, delta);
// Optional: wrap around
if(this.mesh.position.y < -50) {
scene.remove(this.mesh);
}
}
}
function createShard() {
return new Shard();
}
// --- Animation Loop ---
function animate() {
requestAnimationFrame(animate);
const delta = 0.016; // approx 60fps
// Animate avatars
avatars.forEach(avatar => {
avatar.update(delta);
});
// Animate shards
for(let i=shards.length -1; i>=0; i--) {
const shard = shards[i];
shard.update(delta);
// Remove if out of bounds
if(shard.mesh.position.y < -50) {
scene.remove(shard.mesh);
shards.splice(i,1);
}
}
renderer.render(scene, camera);
}
// --- Interaction ---
function onWindowResize() {
camera.aspect = window.innerWidth/window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function getIntersects(event) {
mouse.x = ( event.clientX / window.innerWidth) * 2 - 1;
mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const objects = avatars.map(a => a.mesh);
const intersects = raycaster.intersectObjects(objects);
return intersects;
}
function onMouseDown(event) {
const intersects = getIntersects(event);
if(intersects.length > 0) {
const avatarMesh = intersects[0].object;
const avatar = avatars.find(a => a.mesh === avatarMesh);
if(avatar) {
avatar.triggerSound();
avatar.breakApart();
}
}
}
function onMouseUp(event) {
// Could trigger reactions on release
}
function onMouseMove(event) {
// Could implement drag reactions
}
</script>
</body>
</html>
```Here’s a self-contained HTML example that sets up a Three.js scene of “glitchy” avatars, each tied to its own Tone.js synth. Avatars respond to clicks/drags, occasionally shatter into pixel shards, make chaotic motion and sound, then reassemble.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Glitchy Avatars with Three.js & Tone.js</title>
<style>
body { margin: 0; overflow: hidden; background: #000; }
canvas { display: block; }
</style>
<!-- Three.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r152/three.min.js"></script>
<!-- Tone.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.54/Tone.min.js"></script>
</head>
<body>
<script>
// === Scene Setup ===
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, innerWidth/innerHeight, 0.1, 1000);
camera.position.z = 5;
const renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
// Raycaster for interaction
const ray = new THREE.Raycaster();
const mouse = new THREE.Vector2();
// Tone.js context
Tone.start();
// Avatar container
const avatars = [];
// Utility: random between
function rnd(a,b){return a + (b-a)*Math.random();}
// Build N avatars
const N = 5;
for(let i=0;i<N;i++){
// Plane geometry as avatar
const geo = new THREE.PlaneGeometry(1,1,10,10);
const mat = new THREE.ShaderMaterial({
uniforms: {
time: {value:0},
colorOffset: {value:Math.random()}
},
vertexShader:`
varying vec2 vUv;
void main(){
vUv = uv;
gl_Position = projectionMatrix*modelViewMatrix*vec4(position,1.);
}
`,
fragmentShader:`
uniform float time, colorOffset;
varying vec2 vUv;
float rand(vec2 c){return fract(sin(dot(c,vec2(12.9898,78.233)))*43758.5453);}
void main(){
vec2 uv = vUv;
// glitch: offset rows
float y = floor(uv.y*10.0)/10.0;
float shift = (rand(vec2(y,time)) - 0.5)*0.2;
uv.x += shift;
// color modulation
vec3 col = vec3(0.2+0.8*uv.x, 0.2+0.8*uv.y, 0.5+0.5*sin(time+colorOffset*6.28));
gl_FragColor = vec4(col,1.0);
}
`
});
const mesh = new THREE.Mesh(geo, mat);
mesh.position.x = rnd(-3,3);
mesh.position.y = rnd(-2,2);
mesh.userData = { broken:false, shards:[], drag:false, offset:new THREE.Vector3() };
scene.add(mesh);
avatars.push(mesh);
// Each avatar gets its own synth
const synth = new Tone.Synth({
oscillator: { type: "square" },
envelope: { attack:0.01, decay:0.2, sustain:0.3, release:1 }
}).toDestination();
mesh.userData.synth = synth;
}
// Handle resize
window.addEventListener('resize', ()=> {
camera.aspect = innerWidth/innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth,innerHeight);
});
// Interaction
renderer.domElement.addEventListener('pointerdown', e=>{
mouse.x = (e.clientX/innerWidth)*2 -1;
mouse.y = -(e.clientY/innerHeight)*2 +1;
ray.setFromCamera(mouse,camera);
const hits = ray.intersectObjects(avatars);
if(hits.length){
const a = hits[0].object;
a.userData.drag = true;
// record offset
const planeZ = new THREE.Plane(new THREE.Vector3(0,0,1), -a.position.z);
const intersect = new THREE.Vector3();
ray.ray.intersectPlane(planeZ, intersect);
a.userData.offset.copy(intersect).sub(a.position);
// play a random note
const note = ['C4','E4','G4','B4','D5'][Math.floor(Math.random()*5)];
a.userData.synth.triggerAttackRelease(note, '8n');
// small glitch animation
a.material.uniforms.time.value += 1.0;
}
});
renderer.domElement.addEventListener('pointermove', e=>{
mouse.x = (e.clientX/innerWidth)*2 -1;
mouse.y = -(e.clientY/innerHeight)*2 +1;
ray.setFromCamera(mouse,camera);
const planeZ = new THREE.Plane(new THREE.Vector3(0,0,1), 0);
const intersect = new THREE.Vector3();
ray.ray.intersectPlane(planeZ, intersect);
avatars.forEach(a=>{
if(a.userData.drag && !a.userData.broken){
a.position.copy(intersect).sub(a.userData.offset);
}
});
});
renderer.domElement.addEventListener('pointerup', ()=> {
avatars.forEach(a=> a.userData.drag = false);
});
// Breaking/Recombining logic
function shatter(avatar){
if(avatar.userData.broken) return;
avatar.userData.broken = true;
const mat = avatar.material.clone();
const shards = [];
const N = 20;
for(let i=0;i<N;i++){
const g = new THREE.PlaneGeometry(0.2,0.2);
const m = mat.clone();
const s = new THREE.Mesh(g,m);
s.position.copy(avatar.position);
// random direction
s.userData.velocity = new THREE.Vector3(rnd(-1,1),rnd(-1,1),rnd(-0.5,0.5));
scene.add(s);
shards.push(s);
}
avatar.visible = false;
avatar.userData.shards = shards;
// play burst sound
avatar.userData.synth.triggerAttackRelease('C6','16n');
// schedule reassembly
setTimeout(()=>{
shards.forEach(s=> scene.remove(s));
avatar.visible = true;
avatar.userData.broken = false;
}, rnd(1000,2000));
}
// Animation loop
const clock = new THREE.Clock();
function animate(){
requestAnimationFrame(animate);
const t = clock.getElapsedTime();
avatars.forEach(a=>{
a.material.uniforms.time.value = t*0.5;
// random jitter
if(!a.userData.broken){
a.rotation.z += 0.001 * Math.sin(t+a.material.uniforms.colorOffset.value*10);
}
// animate shards
a.userData.shards.forEach(s=>{
s.position.addScaledVector(s.userData.velocity, 0.02);
s.rotation.x += 0.1;
s.rotation.y += 0.1;
});
// occasionally break
if(Math.random() < 0.0008) shatter(a);
});
renderer.render(scene,camera);
}
animate();
</script>
</body>
</html>
```
How it works:
• Creates N plane-mesh avatars with a simple “glitch” fragment shader.
• Each avatar has its own Tone.js Synth. Clicking triggers a note and a glitch spike.
• Drag-and-drop via raycasting.
• On rare random ticks, an avatar shatters into tiny plane-shards that fly and then reassemble.
• Shards carry their own velocity and rotate, while the avatar is hidden, then pops back.
Feel free to swap in real avatar textures, tweak fragment shader glitch patterns, adjust sound patches or shard geometry for richer visuals and audio chaos!```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Glitchy Avatars</title>
<style>
body {
margin: 0;
background-color: #000;
}
#canvas {
width: 100%;
height: 100vh;
display: block;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tone.js/14.7.58/Tone.min.js"></script>
<script>
// Create the scene, camera, and renderer
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({
canvas: document.getElementById('canvas'),
antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
// Create the avatars
const avatars = [];
for (let i = 0; i < 10; i++) {
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: Math.random() * 0xffffff });
const avatar = new THREE.Mesh(geometry, material);
avatar.position.x = Math.random() * 10 - 5;
avatar.position.y = Math.random() * 10 - 5;
avatar.position.z = Math.random() * 10 - 5;
scene.add(avatar);
avatars.push(avatar);
}
// Create the sound patches
const soundPatches = [];
for (let i = 0; i < 10; i++) {
const synth = new Tone.PolySynth().toDestination();
soundPatches.push(synth);
}
// Animate the avatars
function animate() {
requestAnimationFrame(animate);
for (let i = 0; i < avatars.length; i++) {
const avatar = avatars[i];
avatar.rotation.x += 0.01;
avatar.rotation.y += 0.01;
if (Math.random() < 0.1) {
// Break apart into pixel shards
const shards = [];
for (let j = 0; j < 100; j++) {
const shard = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.1, 0.1), new THREE.MeshBasicMaterial({ color: Math.random() * 0xffffff }));
shard.position.x = avatar.position.x + Math.random() * 2 - 1;
shard.position.y = avatar.position.y + Math.random() * 2 - 1;
shard.position.z = avatar.position.z + Math.random() * 2 - 1;
scene.add(shard);
shards.push(shard);
}
// Float and recombine
for (let j = 0; j < shards.length; j++) {
const shard = shards[j];
shard.position.x += Math.random() * 0.1 - 0.05;
shard.position.y += Math.random() * 0.1 - 0.05;
shard.position.z += Math.random() * 0.1 - 0.05;
if (Math.random() < 0.1) {
scene.remove(shard);
}
}
}
// Play sound patch
soundPatches[i].triggerAttackRelease('C4', '8n');
}
renderer.render(scene, camera);
}
animate();
// Handle user interactions
document.addEventListener('click', (event) => {
for (let i = 0; i < avatars.length; i++) {
const avatar = avatars[i];
// React unpredictably to clicks
if (Math.random() < 0.5) {
avatar.position.x += Math.random() * 2 - 1;
avatar.position.y += Math.random() * 2 - 1;
avatar.position.z += Math.random() * 2 - 1;
}
}
});
document.addEventListener('dragover', (event) => {
for (let i = 0; i < avatars.length; i++) {
const avatar = avatars[i];
// React unpredictably to drags
if (Math.random() < 0.5) {
avatar.rotation.x += Math.random() * 2 - 1;
avatar.rotation.y += Math.random() * 2 - 1;
avatar.rotation.z += Math.random() * 2 - 1;
}
}
});
</script>
</body>
</html>
``````html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Glitchy Avatars Puppeteer</title>
<style>
body, html {
margin: 0; padding: 0; overflow: hidden; background: #111;
user-select: none;
height: 100vh; width: 100vw;
}
canvas {
display: block;
position: absolute; top: 0; left: 0;
}
#info {
position: absolute;
top: 10px; left: 50%;
transform: translateX(-50%);
color: #eee;
font-family: monospace;
user-select: none;
pointer-events: none;
z-index: 10;
font-size: 14px;
}
</style>
</head>
<body>
<div id="info">Click or drag avatars to glitch their sound & form</div>
<script src="https://cdn.jsdelivr.net/npm/three@0.150.1/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.150.1/examples/js/controls/OrbitControls.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/tone@14.8.52/build/Tone.js"></script>
<script>
// ----- Setup Three.js Scene -----
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x111111);
const camera = new THREE.PerspectiveCamera(45, window.innerWidth/window.innerHeight, 0.1, 1000);
camera.position.set(0, 2, 8);
const renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enablePan = false;
controls.minDistance = 4;
controls.maxDistance = 12;
// Ambient light and subtle directional light
scene.add(new THREE.AmbientLight(0x555555));
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
dirLight.position.set(5, 10, 7);
scene.add(dirLight);
// Utility: random between min,max inclusive
function randRange(min,max){
return Math.random()*(max-min)+min;
}
// Glitchy Avatar Class
class GlitchyAvatar {
constructor(id, position){
this.id = id;
this.position = position.clone();
// Create a simple avatar geometry: a low-poly icosahedron
this.geometry = new THREE.IcosahedronGeometry(0.8, 1);
this.material = new THREE.MeshStandardMaterial({
color: new THREE.Color(`hsl(${randRange(150, 240)}, 80%, 55%)`),
roughness: 0.45,
metalness: 0.6,
flatShading: true,
transparent: true,
});
this.mesh = new THREE.Mesh(this.geometry, this.material);
this.mesh.position.copy(this.position);
scene.add(this.mesh);
// Original vertices copied for reset
this.originalVertices = this.geometry.attributes.position.array.slice();
// State
this.isBroken = false;
this.shards = null;
this.shardVelocities = null;
this.shardLifeTime = 0;
// Setup Tone.js sound patch
this.synth = this.makeUniqueSynth();
this.synth.volume.value = -12;
// Sound parameters fluctuating randomly
this.soundParams = {
frequency: randRange(220, 550),
detune: randRange(-120,120),
tremoloFreq: randRange(3, 8),
filterFreq: randRange(400, 1200),
};
this.soundPlaying = false;
// Start continuous ambient sound but very low volume & subtle modulation
this.tremolo = new Tone.Tremolo(this.soundParams.tremoloFreq, 0.1).start();
this.filter = new Tone.Filter(this.soundParams.filterFreq, "lowpass");
this.synth.chain(this.filter, this.tremolo, Tone.Destination);
this.playAmbientSound();
// Interaction state
this.dragging = false;
// Bind methods
this.update = this.update.bind(this);
this.onClick = this.onClick.bind(this);
this.onDrag = this.onDrag.bind(this);
this.breakApart = this.breakApart.bind(this);
this.recombine = this.recombine.bind(this);
}
makeUniqueSynth(){
// Use a Tone.Synth with slightly different oscillator types and envelopes per avatar
const oscTypes = ['triangle', 'sine', 'square', 'sawtooth'];
const oscType = oscTypes[Math.floor(randRange(0, oscTypes.length))];
const attack = randRange(0.01, 0.2);
const release = randRange(0.2, 0.45);
return new Tone.Synth({
oscillator: { type: oscType },
envelope: { attack, decay:0.01, sustain:0.05, release }
});
}
playAmbientSound(){
if(this.soundPlaying) return;
const now = Tone.now();
this.synth.triggerAttack(this.soundParams.frequency, now);
this.soundPlaying = true;
}
stopAmbientSound(){
if(!this.soundPlaying) return;
this.synth.triggerRelease(Tone.now());
this.soundPlaying = false;
}
// Unpredictable glitch on click: modulate sound and distort avatar vertices
onClick(){
if(this.isBroken) return; // ignore clicks while broken
// Randomly detune frequency, filter freq and trigger small envelope pulses
this.soundParams.detune += randRange(-100, 100);
this.soundParams.filterFreq += randRange(-200, 200);
// Clamp filter freq to sane range
this.soundParams.filterFreq = Math.min(Math.max(this.soundParams.filterFreq, 300), 2000);
this.filter.frequency.rampTo(this.soundParams.filterFreq, 0.3);
this.synth.detune.rampTo(this.soundParams.detune, 0.2);
// Quick retrigger a short note for glitch effect
this.synth.triggerAttackRelease(this.soundParams.frequency * (1 + randRange(-0.05, 0.05)), "16n");
// Distort vertices randomly (small glitch)
const posAttr = this.geometry.attributes.position;
for(let i=0; i<posAttr.count; i++){
if(Math.random() < 0.3){
let x = posAttr.getX(i);
let y = posAttr.getY(i);
let z = posAttr.getZ(i);
x += randRange(-0.08, 0.08);
y += randRange(-0.08, 0.08);
z += randRange(-0.08, 0.08);
posAttr.setXYZ(i, x, y, z);
}
}
posAttr.needsUpdate = true;
}
// On drag: unpredictably move avatar and pulse sound/tremolo freq
onDrag(movementX, movementY){
if(this.isBroken) return;
// Move avatar position loosely with some randomness
this.mesh.position.x += movementX * 0.008 * (Math.random()*1.5 + 0.3);
this.mesh.position.y -= movementY * 0.008 * (Math.random()*1.5 + 0.3);
// Modulate sound tremolo freq wildly based on drag speed
let nextFreq = this.soundParams.tremoloFreq + movementX*0.5 + movementY*0.5;
nextFreq = Math.min(Math.max(nextFreq, 1.5), 15);
this.tremolo.frequency.rampTo(nextFreq, 0.1);
this.soundParams.tremoloFreq = nextFreq;
// Pulse a note randomly to add chaotic feel
if(Math.random() < 0.1) {
this.synth.triggerAttackRelease(
this.soundParams.frequency * (1 + randRange(-0.2, 0.2)),
"32n"
);
}
// Slight vertex jitter on drag
const posAttr = this.geometry.attributes.position;
for(let i=0; i<posAttr.count; i++){
if(Math.random() < 0.2){
let x = posAttr.getX(i);
let y = posAttr.getY(i);
let z = posAttr.getZ(i);
x += randRange(-0.05, 0.05);
y += randRange(-0.05, 0.05);
z += randRange(-0.05, 0.05);
posAttr.setXYZ(i, x, y, z);
}
}
posAttr.needsUpdate = true;
// Occasionally break apart on strong drags
if(Math.abs(movementX) + Math.abs(movementY) > 25 && !this.isBroken && Math.random() < 0.7){
this.breakApart();
}
}
breakApart(){
// Destroy original mesh geometry & create pixel shards
this.isBroken = true;
scene.remove(this.mesh);
// Create shards: small spheres replacing vertices scattered around original position
const shardCount = this.geometry.attributes.position.count;
this.shards = new THREE.Group();
// Create spheres for each vertex
let positions = this.geometry.attributes.position.array;
for(let i=0; i<shardCount; i++){
const shardGeo = new THREE.SphereGeometry(0.05, 4, 4);
const color = new THREE.Color(this.material.color.getHex());
color.offsetHSL(randRange(-0.1,0.1), randRange(-0.2,0.2), randRange(-0.1,0.1));
const shardMat = new THREE.MeshStandardMaterial({
color: color,
flatShading: true,
metalness: 0.3,
roughness: 0.6,
transparent: true,
opacity: 1.0
});
const mesh = new THREE.Mesh(shardGeo, shardMat);
// Initial position from original vertex in world relative to avatar position
let vx = positions[i*3 + 0];
let vy = positions[i*3 + 1];
let vz = positions[i*3 + 2];
mesh.position.set(vx, vy, vz);
this.shards.add(mesh);
}
this.shards.position.copy(this.position);
scene.add(this.shards);
// Give shards random velocities for floating apart
this.shardVelocities = [];
for(let i=0; i<shardCount; i++){
this.shardVelocities.push(new THREE.Vector3(
randRange(-0.05,0.05),
randRange(0.03,0.15),
randRange(-0.05,0.05)
));
}
this.shardLifeTime = 0;
// Stop ambient sound & trigger chaotic glitch sound burst
this.stopAmbientSound();
this.triggerGlitchExplosionSound();
}
triggerGlitchExplosionSound(){
const now = Tone.now();
for(let i=0; i<5; i++){
const freq = this.soundParams.frequency * randRange(0.7, 1.5);
const dur = "32n";
this.synth.triggerAttackRelease(freq, dur, now + i*0.05);
}
}
recombine(){
// Slowly recombine shards back into the avatar mesh
if(!this.isBroken) return;
this.shardLifeTime += 0.013;
let done = false;
const targetVertices = this.geometry.attributes.position;
const shardCount = targetVertices.count;
for(let i=0; i<shardCount; i++){
const shard = this.shards.children[i];
if(!shard) continue;
// Move shard toward original vertex position with easing
const origX = this.originalVertices[i*3];
const origY = this.originalVertices[i*3+1];
const origZ = this.originalVertices[i*3+2];
// Lerp position
shard.position.x += (origX - shard.position.x) * 0.08;
shard.position.y += (origY - shard.position.y) * 0.08;
shard.position.z += (origZ - shard.position.z) * 0.08;
// Reduce velocity
this.shardVelocities[i].multiplyScalar(0.85);
// Fade shards opacity out
shard.material.opacity -= 0.012;
if(shard.material.opacity < 0) shard.material.opacity = 0;
// Add small jitter to shards to keep chaotic feel
shard.position.add(this.shardVelocities[i]);
if(shard.material.opacity > 0.15) done = false;
else done = true; // Tentative - will finalize after loop
}
// When shards faded & aligned sufficiently close, restore mesh
if(done) {
scene.remove(this.shards);
this.shards.traverse(child => {
if(child.geometry) child.geometry.dispose();
if(child.material) child.material.dispose();
});
this.shards = null;
this.isBroken = false;
// Restore mesh & reset vertices
this.mesh.geometry.attributes.position.array.set(this.originalVertices);
this.mesh.geometry.attributes.position.needsUpdate = true;
this.mesh.position.copy(this.position);
scene.add(this.mesh);
// Re-enable ambient sound gradually with low volume burst
this.soundParams.filterFreq = 1000;
this.filter.frequency.cancelScheduledValues(Tone.now());
this.filter.frequency.rampTo(this.soundParams.filterFreq, 0.6);
this.synth.detune.cancelScheduledValues(Tone.now());
this.synth.detune.rampTo(0, 0.6);
this.playAmbientSound();
// Small pulse on recombine
this.synth.triggerAttackRelease(this.soundParams.frequency*1.1, "16n");
}
}
update(){
if(this.isBroken) {
this.recombine();
} else {
// Subtle oscillations in vertices to keep glitch alive
const posAttr = this.geometry.attributes.position;
const time = performance.now()*0.001;
for(let i=0; i<posAttr.count; i++){
const ox = this.originalVertices[i*3];
const oy = this.originalVertices[i*3+1];
const oz = this.originalVertices[i*3+2];
// Small noise-based oscillation
const nx = ox + 0.04 * Math.sin(i*3 + time*4 + this.id);
const ny = oy + 0.04 * Math.cos(i*2 + time*5 + this.id*1.5);
const nz = oz + 0.04 * Math.sin(i + time*3 + this.id*2);
posAttr.setXYZ(i, nx, ny, nz);
}
posAttr.needsUpdate = true;
// Slight rotation continuous & subtle position oscillation
this.mesh.rotation.x += 0.002+this.id*0.0003;
this.mesh.rotation.y += 0.004+this.id*0.0005;
this.mesh.position.y = this.position.y + Math.sin(time + this.id*10)*0.07;
}
}
}
// --- Setup multiple avatars ---
const avatars = [];
const avatarCount = 7;
const radius = 4.3;
// Position avatars in rough circle
for(let i=0; i<avatarCount; i++){
const angle = (i / avatarCount) * Math.PI * 2;
const pos = new THREE.Vector3(Math.cos(angle)*radius, 0, Math.sin(angle)*radius);
avatars.push(new GlitchyAvatar(i, pos));
}
// --- Raycasting for interaction ---
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
let selectedAvatar = null;
let dragStart = null;
function onPointerDown(event){
pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
pointer.y = - (event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObjects(avatars.map(a => a.mesh));
if(intersects.length > 0){
selectedAvatar = avatars.find(a => a.mesh === intersects[0].object);
if(selectedAvatar && !selectedAvatar.isBroken){
dragStart = {x: event.clientX, y: event.clientY};
selectedAvatar.onClick();
}
}
}
function onPointerMove(event){
if(selectedAvatar && dragStart){
const movementX = event.clientX - dragStart.x;
const movementY = event.clientY - dragStart.y;
dragStart.x = event.clientX;
dragStart.y = event.clientY;
selectedAvatar.onDrag(movementX, movementY);
}
}
function onPointerUp(event){
selectedAvatar = null;
dragStart = null;
}
window.addEventListener('pointerdown', onPointerDown);
window.addEventListener('pointermove', onPointerMove);
window.addEventListener('pointerup', onPointerUp);
// --- Animation Loop ---
function animate(){
requestAnimationFrame(animate);
avatars.forEach(av => av.update());
controls.update();
renderer.render(scene, camera);
}
animate();
// Window resize handling
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth/window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// Start Tone on user interaction (some browsers require implicit start)
window.addEventListener('click', async function startTone(){
if(Tone.context.state !== 'running'){
await Tone.start();
}
window.removeEventListener('click', startTone);
});
</script>
</body>
</html>
```
# Glitchy Avatar Soundscape
I'll create an interactive experience with glitchy abstract avatars that react to interactions with unpredictable animations and sound. Each avatar uses Three.js for visual effects and Tone.js for unique audio reactions.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Glitchy Avatar Soundscape</title>
<style>
body {
margin: 0;
overflow: hidden;
background: #000;
font-family: 'Courier New', monospace;
}
#scene-container {
position: relative;
}
#controls {
position: absolute;
bottom: 20px;
left: 20px;
color: #0f0;
background: rgba(0, 20, 0, 0.7);
padding: 15px;
border-radius: 8px;
border: 1px solid #0f0;
}
#title {
position: absolute;
top: 20px;
left: 0;
width: 100%;
text-align: center;
color: #0f0;
font-size: 2.2em;
text-shadow: 0 0 10px #0f0;
letter-spacing: 3px;
text-transform: uppercase;
}
button {
background: rgba(0, 30, 0, 0.8);
color: #0f0;
border: 1px solid #0f0;
padding: 10px 15px;
margin: 8px 5px;
border-radius: 4px;
cursor: pointer;
font-family: 'Courier New', monospace;
transition: all 0.3s;
}
button:hover {
background: rgba(0, 200, 0, 0.3);
}
p {
margin: 5px 0;
font-size: 1.1em;
}
.glitch-text {
color: #f0f;
animation: glitch 1s infinite;
}
@keyframes glitch {
0% { text-shadow: 3px 0 0px #0f0, -3px 0 0px #ff00c7; }
50% { text-shadow: -4px 0 0px #0f0, 4px 0 0px #ff00c7; }
100% { text-shadow: 2px 0 0px #0f0, -2px 0 0px #ff00c7; }
}
</style>
</head>
<body>
<div id="title">GLITCHY AVATAR SOUNDSCAPE</div>
<div id="scene-container">
<div id="controls">
<p>Click/drag avatars to create chaos</p>
<p>Each reacts with unique visuals & sounds</p>
<button id="add-btn">ADD AVATAR</button>
<button id="break-all">BREAK ALL</button>
<button id="mutate">MUTATE WORLD</button>
</div>
<canvas id="scene"></canvas>
</div>
<!-- Libraries -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.37/Tone.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.7.1/dist/gsap.min.js"></script>
<script>
// Extend Three.js with glitch materials and effects
THREE.GlitchShader = {
uniforms: {
"tDiffuse": { value: null },
"time": { value: 0.0 },
"factor": { value: 0.1 },
"resolution": { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
}
`,
fragmentShader: `
uniform sampler2D tDiffuse;
uniform float time;
uniform float factor;
uniform vec2 resolution;
varying vec2 vUv;
float rand(vec2 c){
return fract(sin(dot(c.xy ,vec2(12.9898,78.233))) * 43758.5453);
}
void main() {
vec2 uv = vUv;
vec4 color = texture2D(tDiffuse, uv);
// Random glitches
if (rand(vec2(time * factor, uv.y)) > 0.95) {
float dist = rand(vec2(time * factor, uv.x)) * 0.05;
uv.x += dist * factor * 0.5;
uv.y += dist * factor * 0.5;
color = texture2D(tDiffuse, uv);
color.r *= 0.4 + rand(uv) * 0.6;
color.g *= 0.8 + rand(uv.yx) * 0.2;
}
// Color channel offset
float offset = factor * sin(time * 0.01) * 0.01;
if (rand(uv) > 0.9) {
color.r = texture2D(tDiffuse, uv + vec2(offset, 0.0)).r;
color.g = texture2D(tDiffuse, uv + vec2(-offset, -offset)).g;
color.b = texture2D(tDiffuse, uv + vec2(0.0, offset)).b;
}
gl_FragColor = color;
}
`
};
// Main application
class GlitchyScene {
constructor() {
// Setup
this.initScene();
this.initLights();
this.initSoundEngine();
this.createAvatars();
this.setupEventListeners();
this.setupGlitchEffect();
// Start
this.animate();
}
initScene() {
// Renderer
this.renderer = new THREE.WebGLRenderer({
canvas: document.getElementById('scene'),
antialias: true,
alpha: true
});
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.setPixelRatio(window.devicePixelRatio);
// Scene
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0x001003);
// Camera
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth/innerHeight, 0.1, 1000);
this.camera.position.z = 12;
// Controls for dragging
this.mouse = new THREE.Vector2();
this.raycaster = new THREE.Raycaster();
this.intersected = null;
this.selected = null;
// Object holders
this.avatars = [];
this.idleTime = 0;
// Particle systems for shattering effect
this.particleSprites = new THREE.TextureLoader().load('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAcklEQVQYV2O0tbX9TwMAA5GKCgYFBGxzcHBj4eK6wMLGdhyIBZEsNhk9vbtn2djYz2LIc3NzS2hqan6CKYTYCNf5//9/DiDXDowVGRm5gOgcSFjpfjB+/vy51blz5zSQNcTWbnDB/PnzofIdyMdQBgAwWRkPFRSUEQAAAABJRU5ErkJggg==');
}
initLights() {
// Ambient light
const ambient = new THREE.AmbientLight(0x222222);
this.scene.add(ambient);
// Directional lights
const light = new THREE.DirectionalLight(0x00ffcc, 1.5);
light.position.set(1, 1, 1);
this.scene.add(light);
const light2 = new THREE.DirectionalLight(0xff00aa, 0.8);
light2.position.set(-1, -1, 1);
this.scene.add(light2);
// Point lights
const pointLight = new THREE.PointLight(0x00e1ff, 1.2, 30);
pointLight.position.set(0, 10, 2);
this.scene.add(pointLight);
}
initSoundEngine() {
// Set up Tone.js synthesizers
this.synths = {
synth1: new Tone.PolySynth({
oscillator: {
type: 'fatsquare4',
count: 4,
spread: 30
},
envelope: {
attack: 0.02,
decay: 0.1,
sustain: 0.3,
release: 0.4
}
}).toDestination(),
synth2: new Tone.MonoSynth({
oscillator: {
type: 'fmsquare',
modulationType: 'sawtooth',
modulationIndex: 5,
harmonicity: 1.5
},
envelope: {
attack: 0.01,
decay: 0.2,
sustain: 0.1,
release: 1.2
},
filterEnvelope: {
attack: 0.01,
decay: 0.1,
sustain: 0.5,
release: 0.8,
baseFrequency: 300,
octaves: 4
}
}).toDestination(),
synth3: new Tone.MetalSynth({
frequency: 150,
envelope: {
attack: 0.001,
decay: 0.1,
release: 0.1
},
harmonicity: 8.5,
modulationIndex: particlegravity,
resonance: 400,
octaves: 1.5
}).toDestination()
};
// Effects chain
this.compressor = new Tone.Compressor(-20, 4).toDestination();
this.pingPong = new Tone.PingPongDelay("8n", 0.2).connect(this.compressor);
this.pitchShift = new Tone.PitchShift(-6).connect(this.pingPong);
// Connect synths to effects
Object.values(this.synths).forEach(synth => {
synth.connect(this.pitchShift);
});
}
createAvatars(count = 5) {
for (let i = 0; i < count; i++) {
// Random position
const x = (Math.random() - 0.5) * 20;
const y = (Math.random() - 0.5) * 10;
const z = (Math.random() - 0.5) * 12;
// Random shape
const shapeTypes = ['box', 'sphere', 'cone', 'torus'];
const shapeType = shapeTypes[Math.floor(Math.random() * shapeTypes.length)];
// Create avatar object
this.addAvatar(x, y, z, shapeType);
}
// Schedule random break events
setTimeout(() => {
if (this.avatars.length > 0) {
const randomAvatar = this.avatars[Math.floor(Math.random() * this.avatars.length)];
this.breakAvatar(randomAvatar);
}
}, Math.random() * 10000 + 5000);
}
addAvatar(x, y, z, shapeType) {
const colors = [0xff33aa, 0x38ffcc, 0x9a1eff, 0xffd83d, 0x42ffdb];
const mat = new THREE.MeshStandardMaterial({
color: colors[Math.floor(Math.random() * colors.length)],
metalness: 0.2,
roughness: 0.8,
emissive: colors[Math.floor(Math.random() * colors.length)],
emissiveIntensity: 0.3,
wireframe: Math.random() > 0.7
});
let geometry;
switch(shapeType) {
case 'box':
geometry = new THREE.BoxGeometry(1.5, 1.5, 1.5);
break;
case 'sphere':
geometry = new THREE.SphereGeometry(0.9, 20, 20);
break;
case 'cone':
geometry = new THREE.ConeGeometry(1, 1.8, 32);
break;
case 'torus':
geometry = new THREE.TorusGeometry(1, 0.4, 12, 24);
break;
}
const mesh = new THREE.Mesh(geometry, mat);
mesh.position.set(x, y, z);
mesh.rotation.set(
Math.random() * Math.PI * 2,
Math.random() * Math.PI * 2,
Math.random() * Math.PI * 2
);
// Physics-like properties for movement
mesh.velocity = new THREE.Vector3(
(Math.random() - 0.5) * 0.02,
(Math.random() - 0.5) * 0.02,
(Math.random() - 0.5) * 0.02
);
mesh.originalPos = new THREE.Vector3(x, y, z);
mesh.rotationSpeed = new THREE.Vector3(
(Math.random() - 0.5) * 0.01,
(Math.random() - 0.5) * 0.01,
(Math.random() - 0.5) * 0.01
);
// Assign avatar
const avatar = {
mesh: mesh,
shapeType: shapeType,
synthType: ['synth1', 'synth2', 'synth3'][Math.floor(Math.random() * 3)],
soundType: ['synth', 'break', 'hit', 'recombine'][Math.floor(Math.random() * 4)],
broken: false,
breaking: false,
originalScale: mesh.scale.clone(),
particleSystem: null
};
this.scene.add(mesh);
this.avatars.push(avatar);
// Play a unique creation sound
this.playAvatarSound(avatar, 'synth');
return avatar;
}
breakAvatar(avatar) {
if (avatar.broken || avatar.breaking) return;
avatar.breaking = true;
this.playAvatarSound(avatar, 'break');
// Create particles for the shatter effect
const particles = new THREE.BufferGeometry();
const count = 150;
const positions = new Float32Array(count * 3);
for (let i = 0; i < count * 3; i += 3) {
positions[i] = (Math.random() - 0.5) * 2;
positions[i+1] = (Math.random() - 0.5) * 2;
positions[i+2] = (Math.random() - 0.5) * 2;
}
particles.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const particleMat = new THREE.PointsMaterial({
color: avatar.mesh.material.color,
size: 0.1,
map: this.particleSprites,
alphaTest: 0.5,
transparent: true,
blending: THREE.AdditiveBlending
});
const particleSystem = new THREE.Points(particles, particleMat);
particleSystem.position.copy(avatar.mesh.position);
particleSystem.scale.copy(avatar.mesh.originalScale);
// Store particles in avatar
avatar.particleSystem = particleSystem;
avatar.broken = true;
// Hide original mesh
avatar.mesh.visible = false;
this.scene.add(particleSystem);
// Animate particles outwards
const positionsArray = particleSystem.geometry.attributes.position.array;
const originalPositions = positionsArray.slice();
avatar.particleVelocities = Array.from({ length: count }, () =>
new THREE.Vector3(
(Math.random() - 0.5) * 0.05,
(Math.random() - 0.5) * 0.05,
(Math.random() - 0.5) * 0.05
)
);
// Start recombination after delay
setTimeout(() => {
avatar.broken = false;
// Create transition animations
const positions = particleSystem.geometry.attributes.position;
const originalPosCount = originalPositions.length;
const animateRecombine = () => {
if (avatar.broken) return;
let allReturned = true;
for (let i = 0; i < originalPosCount; i += 3) {
const index = i/3;
// Move back towards center (original positions)
positions.array[i] += (originalPositions[i] - positions.array[i]) * 0.05;
positions.array[i+1] += (originalPositions[i+1] - positions.array[i+1]) * 0.05;
positions.array[i+2] += (originalPositions[i+2] - positions.array[i+2]) * 0.05;
// Check if reached original
if (Math.abs(positions.array[i] - originalPositions[i]) > 0.01 ||
Math.abs(positions.array[i+1] - originalPositions[i+1]) > 0.01 ||
Math.abs(positions.array[i+2] - originalPositions[i+2]) > 0.01) {
allReturned = false;
}
}
positions.needsUpdate = true;
if (allReturned) {
// Recombination complete
this.playAvatarSound(avatar, 'recombine');
avatar.breaking = false;
avatar.mesh.visible = true;
this.scene.remove(particleSystem);
avatar.particleSystem = null;
} else {
requestAnimationFrame(animateRecombine);
}
};
animateRecombine();
}, 3000);
}
playAvatarSound(avatar, type) {
// Select note based on avatar and sound type
const notes = ['C4', 'D#4', 'G4', 'A4', 'B4', 'C5', 'D#5', 'F5', 'G5'];
const note = notes[Math.floor(Math.random() * notes.length)];
const duration = Math.random() * 0.5 + 0.1;
const velocity = Math.random() * 0.4 + 0.5;
switch(type) {
case 'synth': // Creation sound
this.synths[avatar.synthType].triggerAttackRelease(note, duration, '+0.1', velocity);
break;
case 'break': // Shattering sound
this.synths.synth3.triggerAttackRelease('C3', duration * 2, '+0.1', velocity * 1.2);
break;
case 'recombine': // Recombination sound
this.synths.synth1.triggerAttackRelease([note, notes[Math.floor(Math.random() * 3)]],
duration * 1.5, '+0.1', velocity * 0.8);
break;
case 'hit': // Interaction response
this.synths[Math.random() > 0.5 ? 'synth2' : 'synth1']
.triggerAttackRelease(note, duration, '+0.01', velocity);
break;
}
// Add extra audio glitches sometimes
if (Math.random() > 0.6) {
this.synths.synth3.triggerAttackRelease(
Math.random() > 0.5 ? 'G1' : 'D#2',
Math.random() * 0.3,
'+0.2',
Math.random() * 0.4
);
}
}
setupGlitchEffect() {
// Create rendering targets for glitch effect
this.glitchTarget = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight);
this.glitchMaterial = new THREE.ShaderMaterial({
uniforms: THREE.UniformsUtils.clone(THREE.GlitchShader.uniforms),
vertexShader: THREE.GlitchShader.vertexShader,
fragmentShader: THREE.GlitchShader.fragmentShader
});
this.glitchMaterial.uniforms['tDiffuse'].value = this.glitchTarget.texture;
this.glitchMaterial.uniforms['resolution'].value =
new THREE.Vector2(window.innerWidth, window.innerHeight);
// Create a full-screen plane for post-processing
this.quad = new THREE.Mesh(
new THREE.PlaneGeometry(2, 2),
this.glitchMaterial
);
this.scene.add(this.quad);
}
setupEventListeners() {
// Window resize handler
window.addEventListener('resize', () => {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
if (this.glitchTarget) {
this.glitchTarget.setSize(window.innerWidth, window.innerHeight);
}
});
// Mouse interaction
document.getElementById('scene').addEventListener('mousedown', (e) => {
this.mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
this.mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
this.raycaster.setFromCamera(this.mouse, this.camera);
const intersects = this.raycaster.intersectObjects(
this.avatars.map(a => a.mesh)
);
if (intersects.length > 0) {
this.selected = intersects[0].object;
// Find avatar from mesh
const avatar = this.avatars.find(a => a.mesh === this.selected);
if (!avatar) return;
// Play sound on interaction
this.playAvatarSound(avatar, 'hit');
// Add unpredictable physics effects
avatar.mesh.velocity.x = (Math.random() - 0.5) * 0.2;
avatar.mesh.velocity.y = Math.random() * 0.1;
avatar.mesh.velocity.z = (Math.random() - 0.5) * 0.1;
// Break avatar with random chance
if (Math.random() > 0.85) {
this.breakAvatar(avatar);
}
}
});
// UI controls
document.getElementById('add-btn').addEventListener('click', () => {
const x = (Math.random() - 0.5) * 20;
const y = 0;
const z = (Math.random() - 0.5) * 6;
const shapes = ['box', 'sphere', 'cone', 'torus'];
const shapeType = shapes[Math.floor(Math.random() * shapes.length)];
this.addAvatar(x, y, z, shapeType);
});
document.getElementById('break-all').addEventListener('click', () => {
this.avatars.forEach(avatar => {
if (!avatar.broken && !avatar.breaking) {
this.breakAvatar(avatar);
}
});
});
document.getElementById('mutate').addEventListener('click', () => {
// Randomize sound parameters
this.pingPong.delayTime.value = Math.random() * 0.5;
this.pingPong.wet.value = Math.random();
this.pitchShift.pitch = Math.random() * 12 - 6;
// Add visual mutation
this.camera.position.x = (Math.random() - 0.5) * 8;
this.camera.position.y = (Math.random() - 0.5) * 4;
// Add a random break for fun
if (this.avatars.length > 0 && Math.random() > 0.3) {
const avatar = this.avatars[Math.floor(Math.random() * this.avatars.length)];
if (!avatar.broken && !avatar.breaking) {
this.breakAvatar(avatar);
}
}
});
}
animate() {
requestAnimationFrame(() => this.animate());
// Update all avatars
for (const avatar of this.avatars) {
const mesh = avatar.mesh;
// Only animate non-broken avatars
if (!avatar.broken && !avatar.breaking) {
// Rotation
mesh.rotation.x += mesh.rotationSpeed.x;
mesh.rotation.y += mesh.rotationSpeed.y;
mesh.rotation.z += mesh.rotationSpeed.z;
// Position with boundaries
mesh.position.x += mesh.velocity.x;
mesh.position.y += mesh.velocity.y;
mesh.position.z += mesh.velocity.z;
// Apply boundary constraints
if (mesh.position.x > 12 || mesh.position.x < -12) mesh.velocity.x *= -1;
if (mesh.position.y > 8 || mesh.position.y < -8) mesh.velocity.y *= -0.8;
if (mesh.position.z > 8 || mesh.position.z < -8) mesh.velocity.z *= -1;
mesh.position.x = Math.min(Math.max(mesh.position.x, -12), 12);
mesh.position.y = Math.min(Math.max(mesh.position.y, -8), 8);
mesh.position.z = Math.min(Math.max(mesh.position.z, -8), 8);
}
// Update particle systems if broken and not recombining
if (avatar.broken && avatar.particleSystem && !avatar.breaking) {
const positions = avatar.particleSystem.geometry.attributes.position.array;
for (let i = 0; i < positions.length; i += 3) {
positions[i] += avatar.particleVelocities[i/3].x;
positions[i+1] += avatar.particleVelocities[i/3].y;
positions[i+2] += avatar.particleVelocities[i/3].z;
// Random movement changes
if (Math.random() > 0.98) {
avatar.particleVelocities[i/3].x += (Math.random() - 0.5) * 0.02;
avatar.particleVelocities[i/3].y += (Math.random() - 0.5) * 0.02;
avatar.particleVelocities[i/3].z += (Math.random() - 0.5) * 0.02;
}
}
avatar.particleSystem.geometry.attributes.position.needsUpdate = true;
}
}
// Idle behavior - trigger rare random breaks when no interaction
this.idleTime++;
if (this.idleTime > 1200 && this.avatars.length > 0 && Math.random() > 0.99) {
if (this.avatars.length > 0) {
const randomAvatar = this.avatars[Math.floor(Math.random() * this.avatars.length)];
if (!randomAvatar.broken && !randomAvatar.breaking) {
this.breakAvatar(randomAvatar);
}
}
this.idleTime = 0;
}
// Update glitch post-processing uniforms
this.glitchMaterial.uniforms['time'].value = Date.now() / 1000;
this.glitchMaterial.uniforms['factor'].value = 0.1 + Math.sin(Date.now() / 3000) * 0.1;
// Render with glitch effect
this.renderer.setRenderTarget(this.glitchTarget);
this.renderer.render(this.scene, this.camera);
this.renderer.setRenderTarget(null);
this.renderer.render(this.scene, this.camera);
}
}
// Start when loaded
window.addEventListener('load', () => {
// Start audio context on user interaction (required by browsers)
const startAudio = () => {
if (Tone.context.state !== 'running') {
Tone.context.resume();
}
// Remove event listener after first interaction
document.removeEventListener('click', startAudio);
document.removeEventListener('touchstart', startAudio);
// Create the scene
new GlitchyScene();
};
// Mobile compatibility
document.addEventListener('click', startAudio);
document.addEventListener('touchstart', startAudio);
// Trigger on button click for visibility
document.getElementById('controls').addEventListener('click', startAudio);
});
</script>
</body>
</html>
```
This interactive experience creates a world of abstract, glitchy avatars that:
1. Each avatar has a unique geometric shape with random colors and movements
2. Interactions trigger unpredictable visual reactions and synth sounds
3. Avatars periodically shatter into pixel shards and recombine
4. All UI elements are rendered with a cyberpunk glitch aesthetic
The application uses Three.js for all 3D rendering and physics-based movement. Tone.js creates the unique sound patches based on each avatar's properties. The glitch visual effects utilize custom shader materials combined with unpredictable motion behaviors.
You can interact with the scene by:
- Clicking avatars to hear their unique sound and cause unpredictable reactions
- Adding more avatars with the ADD button
- Shattering all avatars at once with BREAK ALL
- Mutating the world to change audio parameters and camera position