ai-chatbot/particle-scatter-effect.html
Your Name be5b9aef12
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
chore: sync latest workspace changes
2026-04-05 02:08:26 +08:00

299 lines
39 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Neural Particle - Scatter Effect</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { overflow: hidden; background-color: #000; }
canvas { display: block; }
#particle-container { width: 100vw; height: 100vh; }
</style>
</head>
<body>
<div id="particle-container"></div>
<script>
const CONFIG = {
effect: "scatter",
effectMode: 1,
particleSize: 191,
particleCount: 60000,
uploadedImage: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAyAAAAMgCAYAAADbcAZoAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3BpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDkuMS1jMDAyIDc5LmExY2QxMmY0MSwgMjAyNC8xMS8wOC0xNjowOToyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowODJGMTk3QTI1QjgxMUYwQkU3OEY1OEExNTUzMDRCNSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpCOUVCQUJGQTFDOTkxMUYxQThBM0VCOEU0MkIxNzEyRiIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpCOUVCQUJGOTFDOTkxMUYxQThBM0VCOEU0MkIxNzEyRiIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjUuMSAoTWFjaW50b3NoKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjZkOTA5MGM2LWQ3MGQtNGE0NC1hNzM1LTFjMWZmN2QwMTQ0YyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDowODJGMTk3QTI1QjgxMUYwQkU3OEY1OEExNTUzMDRCNSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Ph5sOxgAACDsSURBVHja7N15sJ1lfQfwcwxgCKJiQUBIcrOBxC0UtUWhhgoJEEJAFChaHYgiKlortco4uFCnrWIVC1gZ6wzaFgk7yl6FIEiCyCJLQZLcJQsgoOwYAsnt73hpCnpDzrnPPe/6+cw8vPkP5nuZN/nm9zzv0xwcHGwAAABk4SUiAAAAFBAAAEABAQAAUEAAAAAFBAAAQAEBAAAUEAAAQAEBAABQQAAAAAUEAABAAQEAABQQAABAAQEAAFBAAAAABQQAAEABAQAAFBAAAEABAQAAUEAAAAAFBAAAQAEBAAAUEAAAQAEBAABQQAAAAAUEAABAAQEAABQQAABAAQEAAFBAAAAABQQAAEABAQAAFBAAAAAFBAAAUEAAAAAFBAAAQAEBAAAUEAAAAAUEAABQQAAAAAUEAABAAQEAABQQAAAABQQAAFBAAAAABQQAAEABAQAAFBAAAAAFBAAAUEAAAAAFBAAAQAEBAAAUEAAAAAUEAABQQAAAAAUEAABAAQEAABQQAAAABQQAAFBAAAAAFBAAAEABAQAAFBAAAAAFBAAAUEAAAAAUEAAAQAEBAAAUEAAAAAUEAABQQAAAABQQAABAAQEAABQQAAAABQQAAFBAAAAAFBAAAEABAQAAFBAAAAAFBAAAUEAAAAAUEAAAQAEBAAAUEAAAAAUEAABQQAAAABQQAABAAQEAABQQEQAAAAoIAACggAAAACggAACAAgIAAKCAAAAACggAAKCAAAAAKCAAAIACAgAAoIAAAAAKCAAAoIAAAAAoIAAAgAICAACggAAAAAoIAACggAAAACggAACAAgIAAKCAAAAACggAAKCAAAAAKCAAAIACAgAAoIAAAAAKCAAAoIAAAAAoIAAAgAICAACggAAAAAoIAACAAgIAACggAACAAgIAAKCAAAAACggAAIACAgAAKCAAAIACAgAAoIAAAAAKCAAAgAICAAAoIAAAgAICAACggAAAAAoIAACAAgIAACggAACAAgIAANA1m4iAFGPPffgdUgCAeln97q2ukQIKCHlZKAIAqJ2mCBgpW7AAAAAFBAAAUEAAAABGzBkQEg2KAACAtpmAAAAACggAAKCAAAAAKCAAAIACAgAAoIAAAAAKCAAAUGHuASFNUwQAALTPBAQAAFBAAACA6rEFi0SDIgAAoG0mIAAAgAICAAAoIAAAAAoIAACggAAAACggAABA9nyGlyQuQgcAoBMmIAAAgAICAABUjy1YpLEHCwAABYTsDIoAAIC22YIFAAAoIAAAgAICAACggAAAAAoIAACAAgIAACggAACAAgIAAKCAAAAACggAAMAf20QEJGkOygAAgLaZgAAAAAoIAACggAAAACggAACAAgIAAKCAAAAACggAAKCAAAAAKCAAAECJuAmdJM2Gm9ABAGifCQgAAJAZExDSNEUAAED7TEAAAAAFBAAAUEAAAAAUEAAAQAEBAABQQAAAAAUEAACoMPeAkMhN6AAAtM8EBAAAUEAAAAAFBAAAYMScASFNUwQAALTPBAQAAFBAAAAABQQAAEABAQAAFBAAAID1fAWLRG5CBwCgfSYgAACAAgIAACggAAAAI+YMCElchA4AgAKCBgIAQCHZggUAACggAACAAgIAAKCAAAAAxecQOonchA4AQPtMQAAAAAUEAABQQAAAABQQAABAAQEAAFBAAACA7PkML2maIgAAoH0mIAAAgAICAABUjy1YJHITOgAA7TMBAQAAFBAAAEABAQAAUEAAAAAFBAAAQAEBAACy5zO8JHEROgAACggaCAAACghV5CJCAADa5wwIAACggAAAAAoIAACAAgIAACggAAAACggAAKCAAAAACggAAIACAgAAlIib0EnTdBM6AADtMwEBAAAUEAAAQAEBAABQQAAAAAUEAABAAQEAABQQAABAAQEAAFBAAACAEnETOonchA4AgAJCRppNGQAA0D5bsAAAAAUEAABQQAAAABQQAABAAQEAAFBAAAAABQQAAKgw94CQyEWEAAC0zwQEAABQQAAAgOqxBYs0TREAANA+ExAAAEABAQAAFBAAAAAFBAAAUEAAAADW8xUsErmIEACA9pmAAAAACggAAKCAAAAAjJgzIKRxEzoAAAoI+gcAAEVkCxYAAKCAAAAACggAAMCIOQNCIhcRAgDQPhMQAABAAQEAABQQAAAABQQAAFBAAAAA1vMVLNK4Ch0AgA6YgAAAAAoIAABQPbZgkchFhHT1f67HxAAjsmmscWIAFBCA9i1/bPakHjFA515+Rd9B8bhAEkAR2YIFAAAoIAAAgAICAACggAAAAAoIAADAer6C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',
shape: "default",
interactionMode: "auto",
manualControlTarget: "camera"
};
let uploadedImage = CONFIG.uploadedImage;
let particleSize = CONFIG.particleSize;
const PARTICLE_COUNT = CONFIG.particleCount;
let sceneData = null;
let currentEffect = CONFIG.effect;
let currentShape = CONFIG.shape;
let interactionMode = CONFIG.interactionMode;
let manualControlTarget = CONFIG.manualControlTarget;
let effectIntensity = 1;
let targetEffectIntensity = 1;
let explosionTriggered = false;
let explosionTime = 0;
const cameraControlState = {
target: null,
yaw: 0,
pitch: 0,
distance: 45,
minDistance: 20,
maxDistance: 100,
isLeftDragging: false,
isRightDragging: false,
previousX: 0,
previousY: 0
};
const mouseState = { isDragging: false, previousX: 0, previousY: 0 };
const vertexShader = "uniform float uTime;uniform float uMorph;uniform float uPointSize;uniform int uEffectMode;uniform float uEffectIntensity;uniform float uExplosionTime;attribute vec3 targetPosition;attribute vec3 targetColor;attribute vec3 color;attribute vec3 randomOffset;varying vec3 vColor;varying float vDistance;vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}vec2 mod289(vec2 x){return x-floor(x*(1.0/289.0))*289.0;}vec3 permute(vec3 x){return mod289(((x*34.0)+1.0)*x);}float snoise(vec2 v){const vec4 C=vec4(0.211324865405187,0.366025403784439,-0.577350269189626,0.024390243902439);vec2 i=floor(v+dot(v,C.yy));vec2 x0=v-i+dot(i,C.xx);vec2 i1=(x0.x>x0.y)?vec2(1.0,0.0):vec2(0.0,1.0);vec4 x12=x0.xyxy+C.xxzz;x12.xy-=i1;i=mod289(i);vec3 p=permute(permute(i.y+vec3(0.0,i1.y,1.0)) + i.x + vec3(0.0,i1.x,1.0));vec3 m=max(0.5-vec3(dot(x0,x0),dot(x12.xy,x12.xy),dot(x12.zw,x12.zw)),0.0);m=m*m;m=m*m;vec3 x=2.0*fract(p*C.www)-1.0;vec3 h=abs(x)-0.5;vec3 ox=floor(x+0.5);vec3 a0=x-ox;m*=1.79284291400159-0.85373472095314*(a0*a0+h*h);vec3 g;g.x=a0.x*x0.x+h.x*x0.y;g.yz=a0.yz*x12.xz+h.yz*x12.yw;return 130.0*dot(m,g);}void main(){vColor=mix(color,targetColor,uMorph);vec3 pos=mix(position,targetPosition,uMorph);vec3 originalPos=pos;float effectMix=uEffectIntensity;if(uEffectMode==0){float noise=sin(uTime*1.5+position.x*0.3)*cos(uTime*1.5+position.y*0.3);pos+=normalize(pos)*noise*(0.2*(1.0-uMorph));pos.x+=sin(uTime*0.3+position.z)*0.1;pos.y+=cos(uTime*0.3+position.x)*0.1;}else if(uEffectMode==1){vec3 scatterDir=normalize(pos+randomOffset*0.5);float scatterDist=length(pos)*0.5+randomOffset.x*3.0;vec3 scattered=pos+scatterDir*scatterDist*effectMix*2.5;float turb=snoise(pos.xy*0.3+uTime*0.5);scattered+=vec3(turb,turb*0.5,turb*0.3)*effectMix*1.5;pos=mix(originalPos,scattered,effectMix);}else if(uEffectMode==2){float explodeProgress=min(uExplosionTime*2.0,1.0);float returnProgress=max(0.0,(uExplosionTime-0.5)*2.0);vec3 explodeDir=normalize(pos+randomOffset);float explodeDist=(5.0+randomOffset.x*8.0)*sin(explodeProgress*3.14159);vec3 exploded=originalPos+explodeDir*explodeDist*effectMix;float spin=explodeProgress*6.28318*(0.5+randomOffset.y);exploded.x+=cos(spin)*explodeDist*0.3;exploded.z+=sin(spin)*explodeDist*0.3;pos=mix(originalPos,exploded,effectMix*(1.0-returnProgress*0.7));}else if(uEffectMode==3){float angle=atan(pos.z,pos.x);float radius=length(pos.xz);float height=pos.y;float spiralSpeed=uTime*2.0+height*0.3;float newAngle=angle+spiralSpeed*effectMix;float vortexPull=(1.0-abs(height)/20.0)*effectMix;float newRadius=radius*(1.0-vortexPull*0.5)+sin(uTime*3.0+height)*effectMix;float lift=effectMix*5.0*(1.0-radius/20.0);pos.x=cos(newAngle)*newRadius;pos.z=sin(newAngle)*newRadius;pos.y=height+lift*sin(uTime+radius);}else if(uEffectMode==4){float pulsePhase=uTime*2.5;float pulseFactor=1.0+sin(pulsePhase)*0.4*effectMix;float waveFactor=sin(pulsePhase+length(pos)*0.3)*0.3*effectMix;vec3 pulsed=pos*pulseFactor;pulsed+=normalize(pos)*waveFactor*3.0;float colorPulse=sin(pulsePhase*0.5)*0.5+0.5;vColor=mix(vColor,vec3(1.0,0.4,0.8),colorPulse*effectMix*0.3);pos=pulsed;}else if(uEffectMode==5){float waveX=sin(pos.x*0.5+uTime*2.0)*effectMix*3.0;float waveZ=cos(pos.z*0.5+uTime*1.5)*effectMix*2.0;float waveY=sin(pos.x*0.3+pos.z*0.3+uTime*2.5)*effectMix*4.0;waveY+=sin(pos.x*0.8-uTime*1.8)*effectMix*1.5;waveY+=cos(pos.z*0.6+uTime*1.2)*effectMix*1.0;pos.x+=waveX*0.3;pos.y+=waveY;pos.z+=waveZ*0.3;}vec4 mvPosition=modelViewMatrix*vec4(pos,1.0);float dist=length(pos);vDistance=dist;float sizeMultiplier=1.0;if(uEffectMode==2&&effectMix>0.1)sizeMultiplier=1.0+sin(uExplosionTime*10.0)*0.3;if(uEffectMode==4)sizeMultiplier=1.0+sin(uTime*2.5)*0.2*effectMix;gl_PointSize=(uPointSize/-mvPosition.z)*(1.2+sin(uTime*3.0+dist*0.15)*0.5)*sizeMultiplier;gl_Position=projectionMatrix*mvPosition;}";
const fragmentShader = "uniform float uTime;varying vec3 vColor;varying float vDistance;void main(){float dist=distance(gl_PointCoord,vec2(0.5));if(dist>0.5)discard;float strength=pow(1.0-dist*2.0,1.6);vec3 finalColor=vColor*2.0;float alpha=strength*(0.8+sin(vDistance*0.3+uTime)*0.2);gl_FragColor=vec4(finalColor,alpha);}";
function updateCameraFromState(camera) {
const cp = Math.cos(cameraControlState.pitch);
const sp = Math.sin(cameraControlState.pitch);
const cy = Math.cos(cameraControlState.yaw);
const sy = Math.sin(cameraControlState.yaw);
camera.position.set(
cameraControlState.target.x + cameraControlState.distance * sy * cp,
cameraControlState.target.y + cameraControlState.distance * sp,
cameraControlState.target.z + cameraControlState.distance * cy * cp
);
camera.lookAt(cameraControlState.target);
}
function initParticleCanvas() {
const container = document.getElementById("particle-container");
const width = window.innerWidth, height = window.innerHeight;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(35, width / height, 0.1, 1000);
camera.position.z = 45;
cameraControlState.target = new THREE.Vector3(0, 0, 0);
updateCameraFromState(camera);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, powerPreference: "high-performance" });
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setClearColor(0x000000, 0);
container.appendChild(renderer.domElement);
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(PARTICLE_COUNT * 3);
const targetPositions = new Float32Array(PARTICLE_COUNT * 3);
const colors = new Float32Array(PARTICLE_COUNT * 3);
const targetColors = new Float32Array(PARTICLE_COUNT * 3);
const randomOffsets = new Float32Array(PARTICLE_COUNT * 3);
const greenColor = new THREE.Color(0x00ff66);
const brightWhite = new THREE.Color(0xffffff);
for (let i = 0; i < PARTICLE_COUNT; i++) {
const i3 = i * 3;
const t = (Math.random() - 0.5) * 5.0;
const angle = Math.random() * Math.PI * 2;
const radiusBase = 0.4 + Math.pow(Math.abs(t), 2.4);
const radius = radiusBase * (0.75 + Math.random() * 0.55);
let x = radius * Math.cos(angle) * 2.9;
let z = radius * Math.sin(angle) * 2.9;
let y = t * 7.5;
positions[i3] = x; positions[i3 + 1] = y; positions[i3 + 2] = z;
targetPositions[i3] = x; targetPositions[i3 + 1] = y; targetPositions[i3 + 2] = z;
randomOffsets[i3] = (Math.random() - 0.5) * 2;
randomOffsets[i3 + 1] = (Math.random() - 0.5) * 2;
randomOffsets[i3 + 2] = (Math.random() - 0.5) * 2;
const color = Math.random() > 0.7 ? greenColor : brightWhite;
colors[i3] = color.r; colors[i3 + 1] = color.g; colors[i3 + 2] = color.b;
targetColors[i3] = color.r; targetColors[i3 + 1] = color.g; targetColors[i3 + 2] = color.b;
}
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
geometry.setAttribute("targetPosition", new THREE.BufferAttribute(targetPositions, 3));
geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
geometry.setAttribute("targetColor", new THREE.BufferAttribute(targetColors, 3));
geometry.setAttribute("randomOffset", new THREE.BufferAttribute(randomOffsets, 3));
const material = new THREE.ShaderMaterial({
vertexShader, fragmentShader, transparent: true,
uniforms: { uTime: { value: 0 }, uMorph: { value: 0 }, uPointSize: { value: particleSize }, uEffectMode: { value: CONFIG.effectMode }, uEffectIntensity: { value: 1 }, uExplosionTime: { value: 0 } },
depthWrite: false, blending: THREE.AdditiveBlending
});
const points = new THREE.Points(geometry, material);
scene.add(points);
sceneData = { scene, camera, renderer, points, geometry, material, originalPositions: positions.slice(), targetPositions, originalColors: colors.slice(), targetColors };
if (uploadedImage) processImage(uploadedImage);
else if (currentShape && currentShape !== "default") generateShape(currentShape);
let time = 0, morphFactor = 0;
const effectModes = { "default": 0, "scatter": 1, "explode": 2, "vortex": 3, "pulse": 4, "wave": 5 };
function animate() {
requestAnimationFrame(animate);
time += 0.008;
if (!sceneData) return;
const { renderer, scene, camera, points, material } = sceneData;
if (interactionMode === "manual") {
if (manualControlTarget === "camera") {
updateCameraFromState(camera);
}
} else {
let rotationSpeed = 0.0025;
if (currentEffect === "vortex") rotationSpeed = 0.008 * (0.5 + effectIntensity);
else if (currentEffect === "explode" && explosionTriggered) rotationSpeed = 0.001;
points.rotation.y += rotationSpeed;
points.rotation.z += 0.001;
points.rotation.x = Math.sin(time * 0.15) * 0.12;
}
material.uniforms.uTime.value = time;
const targetMorph = uploadedImage ? 1.0 : 0.0;
morphFactor += (targetMorph - morphFactor) * 0.05;
material.uniforms.uMorph.value = morphFactor;
effectIntensity += (targetEffectIntensity - effectIntensity) * 0.08;
material.uniforms.uEffectIntensity.value = effectIntensity;
material.uniforms.uEffectMode.value = effectModes[currentEffect] || 0;
if (explosionTriggered) { explosionTime += 0.016; if (explosionTime > 2.0) explosionTime = 0; }
material.uniforms.uExplosionTime.value = explosionTime;
renderer.render(scene, camera);
}
animate();
window.addEventListener("resize", function() {
if (!sceneData) return;
sceneData.camera.aspect = window.innerWidth / window.innerHeight;
sceneData.camera.updateProjectionMatrix();
sceneData.renderer.setSize(window.innerWidth, window.innerHeight);
});
renderer.domElement.addEventListener("mousedown", function(e) {
if (interactionMode === "manual" && manualControlTarget === "camera") {
if (e.button === 0) {
cameraControlState.isLeftDragging = true;
} else if (e.button === 2) {
cameraControlState.isRightDragging = true;
}
cameraControlState.previousX = e.clientX;
cameraControlState.previousY = e.clientY;
}
if (interactionMode === "manual" && manualControlTarget === "object") {
mouseState.isDragging = true;
mouseState.previousX = e.clientX;
mouseState.previousY = e.clientY;
}
});
window.addEventListener("mousemove", function(e) {
if (!sceneData) return;
if (interactionMode === "manual" && manualControlTarget === "camera") {
const deltaX = e.clientX - cameraControlState.previousX;
const deltaY = e.clientY - cameraControlState.previousY;
if (cameraControlState.isLeftDragging) {
cameraControlState.yaw -= deltaX * 0.005;
cameraControlState.pitch -= deltaY * 0.005;
const maxPitch = Math.PI * 0.499;
if (cameraControlState.pitch > maxPitch) cameraControlState.pitch = maxPitch;
if (cameraControlState.pitch < -maxPitch) cameraControlState.pitch = -maxPitch;
} else if (cameraControlState.isRightDragging) {
const panScale = cameraControlState.distance * 0.002;
const forward = new THREE.Vector3();
sceneData.camera.getWorldDirection(forward);
const right = new THREE.Vector3().crossVectors(forward, sceneData.camera.up).normalize();
const up = new THREE.Vector3().copy(sceneData.camera.up).normalize();
cameraControlState.target.addScaledVector(right, -deltaX * panScale);
cameraControlState.target.addScaledVector(up, deltaY * panScale);
}
cameraControlState.previousX = e.clientX;
cameraControlState.previousY = e.clientY;
}
if (interactionMode === "manual" && manualControlTarget === "object" && mouseState.isDragging) {
const deltaX = e.clientX - mouseState.previousX;
const deltaY = e.clientY - mouseState.previousY;
sceneData.points.rotation.y += deltaX * 0.005;
sceneData.points.rotation.x += deltaY * 0.005;
mouseState.previousX = e.clientX;
mouseState.previousY = e.clientY;
}
});
window.addEventListener("mouseup", function() {
mouseState.isDragging = false;
cameraControlState.isLeftDragging = false;
cameraControlState.isRightDragging = false;
});
renderer.domElement.addEventListener("wheel", function(e) {
if (interactionMode === "manual" && manualControlTarget === "camera") {
e.preventDefault();
cameraControlState.distance += e.deltaY * 0.03;
if (cameraControlState.distance < cameraControlState.minDistance) {
cameraControlState.distance = cameraControlState.minDistance;
}
if (cameraControlState.distance > cameraControlState.maxDistance) {
cameraControlState.distance = cameraControlState.maxDistance;
}
}
}, { passive: false });
renderer.domElement.addEventListener("contextmenu", function(e) {
e.preventDefault();
});
window.addEventListener("keydown", function(e) {
if (e.code === "Space" && !e.repeat) {
e.preventDefault();
interactionMode = interactionMode === "auto" ? "manual" : "auto";
}
});
}
function processImage(imageUrl) {
if (!imageUrl || !sceneData) return;
const img = new Image();
img.crossOrigin = "anonymous";
img.src = imageUrl;
img.onload = function() {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const resolution = 200;
const aspect = img.width / img.height;
let drawWidth = aspect > 1 ? resolution : resolution * aspect;
let drawHeight = aspect > 1 ? resolution / aspect : resolution;
canvas.width = resolution; canvas.height = resolution;
ctx.fillStyle = "black"; ctx.fillRect(0, 0, resolution, resolution);
ctx.drawImage(img, (resolution - drawWidth) / 2, (resolution - drawHeight) / 2, drawWidth, drawHeight);
const imgData = ctx.getImageData(0, 0, resolution, resolution).data;
const validPoints = [];
for (let y = 0; y < resolution; y++) {
for (let x = 0; x < resolution; x++) {
const idx = (y * resolution + x) * 4;
const r = imgData[idx], g = imgData[idx + 1], b = imgData[idx + 2];
if ((r + g + b) / 3 > 15) validPoints.push({ pos: [(x / resolution - 0.5) * 38, (0.5 - y / resolution) * 38, ((r + g + b) / 765 - 0.5) * 12], col: [r / 255, g / 255, b / 255] });
}
}
if (validPoints.length > 0) {
const { targetPositions, targetColors, geometry } = sceneData;
for (let i = 0; i < PARTICLE_COUNT; i++) {
const i3 = i * 3, point = validPoints[i % validPoints.length];
targetPositions[i3] = point.pos[0] + (Math.random() - 0.5) * 0.4;
targetPositions[i3 + 1] = point.pos[1] + (Math.random() - 0.5) * 0.4;
targetPositions[i3 + 2] = point.pos[2] + (Math.random() - 0.5) * 1.5;
targetColors[i3] = point.col[0]; targetColors[i3 + 1] = point.col[1]; targetColors[i3 + 2] = point.col[2];
}
geometry.attributes.targetPosition.needsUpdate = true;
geometry.attributes.targetColor.needsUpdate = true;
}
};
}
function generateShape(shapeName) {
if (!sceneData) return;
const { targetPositions, targetColors, geometry } = sceneData;
const points = [];
const shapeGreen = new THREE.Color(0x00ff66);
const shapeWhite = new THREE.Color(0xffffff);
switch (shapeName) {
case "heart": for (let i = 0; i < PARTICLE_COUNT; i++) { const t = (i / PARTICLE_COUNT) * Math.PI * 2; const u = Math.random() * Math.PI * 2; const v = Math.random() * Math.PI; const scale = 18; const x = scale * (16 * Math.pow(Math.sin(t), 3)) / 16 * Math.sin(v) * Math.cos(u); const y = scale * (13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t)) / 16; const z = scale * (16 * Math.pow(Math.sin(t), 3)) / 16 * Math.sin(v) * Math.sin(u) * 0.8; const spread = 3.0; const color = Math.random() > 0.7 ? shapeGreen : shapeWhite; points.push({ pos: [x + (Math.random() - 0.5) * spread, y + (Math.random() - 0.5) * spread, z + (Math.random() - 0.5) * spread], col: [color.r, color.g, color.b] }); } break;
case "butterfly": for (let i = 0; i < PARTICLE_COUNT; i++) { const t = (i / PARTICLE_COUNT) * Math.PI * 12; const scale = 5.5; const exp = Math.exp(Math.cos(t)) - 2 * Math.cos(4 * t) - Math.pow(Math.sin(t / 12), 5); const x = Math.sin(t) * exp * scale; const y = Math.cos(t) * exp * scale; const z = (Math.random() - 0.5) * 15; const spread = 2.5; const color = Math.random() > 0.7 ? shapeGreen : shapeWhite; points.push({ pos: [x + (Math.random() - 0.5) * spread, y + (Math.random() - 0.5) * spread, z], col: [color.r, color.g, color.b] }); } break;
case "rose": for (let i = 0; i < PARTICLE_COUNT; i++) { const t = (i / PARTICLE_COUNT) * Math.PI * 14; const k = 5; const r = Math.cos(k * t) * 15; const h = (Math.random() - 0.5) * 20; const x = r * Math.cos(t); const y = h; const z = r * Math.sin(t); const spread = 2.0; const color = Math.random() > 0.7 ? shapeGreen : shapeWhite; points.push({ pos: [x + (Math.random() - 0.5) * spread, y, z + (Math.random() - 0.5) * spread], col: [color.r, color.g, color.b] }); } break;
case "cube": const cubeSize = 18; for (let i = 0; i < PARTICLE_COUNT; i++) { const face = Math.floor(Math.random() * 6); let x, y, z; const a = (Math.random() - 0.5) * cubeSize; const b = (Math.random() - 0.5) * cubeSize; switch (face) { case 0: x = cubeSize / 2; y = a; z = b; break; case 1: x = -cubeSize / 2; y = a; z = b; break; case 2: x = a; y = cubeSize / 2; z = b; break; case 3: x = a; y = -cubeSize / 2; z = b; break; case 4: x = a; y = b; z = cubeSize / 2; break; case 5: x = a; y = b; z = -cubeSize / 2; break; } const spread = 2.0; const color = Math.random() > 0.7 ? shapeGreen : shapeWhite; points.push({ pos: [x + (Math.random() - 0.5) * spread, y + (Math.random() - 0.5) * spread, z + (Math.random() - 0.5) * spread], col: [color.r, color.g, color.b] }); } break;
case "pyramid": const pyrHeight = 25; const pyrBase = 20; for (let i = 0; i < PARTICLE_COUNT; i++) { const onBase = Math.random() < 0.3; let x, y, z; if (onBase) { x = (Math.random() - 0.5) * pyrBase; z = (Math.random() - 0.5) * pyrBase; y = -pyrHeight / 2; } else { const t = Math.random(); const baseX = (Math.random() - 0.5) * pyrBase * (1 - t); const baseZ = (Math.random() - 0.5) * pyrBase * (1 - t); x = baseX; y = -pyrHeight / 2 + t * pyrHeight; z = baseZ; } const spread = 2.0; const color = Math.random() > 0.7 ? shapeGreen : shapeWhite; points.push({ pos: [x + (Math.random() - 0.5) * spread, y + (Math.random() - 0.5) * spread, z + (Math.random() - 0.5) * spread], col: [color.r, color.g, color.b] }); } break;
case "spiral": for (let i = 0; i < PARTICLE_COUNT; i++) { const t = (i / PARTICLE_COUNT) * Math.PI * 10; const r = 10 + Math.sin(t * 3) * 4; const x = r * Math.cos(t); const y = (i / PARTICLE_COUNT - 0.5) * 40; const z = r * Math.sin(t); const spread = 2.5; const color = Math.random() > 0.7 ? shapeGreen : shapeWhite; points.push({ pos: [x + (Math.random() - 0.5) * spread, y + (Math.random() - 0.5) * spread, z + (Math.random() - 0.5) * spread], col: [color.r, color.g, color.b] }); } break;
case "star": for (let i = 0; i < PARTICLE_COUNT; i++) { const angle = Math.random() * Math.PI * 2; const starPoints = 5; const innerR = 8; const outerR = 18; const pointAngle = Math.floor(angle / (Math.PI * 2 / starPoints)) * (Math.PI * 2 / starPoints); const t = (angle - pointAngle) / (Math.PI / starPoints); let r; if (t < 1) { r = outerR - (outerR - innerR) * t; } else { r = innerR + (outerR - innerR) * (t - 1); } const x = r * Math.cos(angle); const z = r * Math.sin(angle); const y = (Math.random() - 0.5) * 15; const spread = 2.0; const color = Math.random() > 0.7 ? shapeGreen : shapeWhite; points.push({ pos: [x + (Math.random() - 0.5) * spread, y, z + (Math.random() - 0.5) * spread], col: [color.r, color.g, color.b] }); } break;
case "sphere": for (let i = 0; i < PARTICLE_COUNT; i++) { const theta = Math.random() * Math.PI * 2; const phi = Math.acos(2 * Math.random() - 1); const r = 16; const x = r * Math.sin(phi) * Math.cos(theta); const y = r * Math.sin(phi) * Math.sin(theta); const z = r * Math.cos(phi); const spread = 2.5; const color = Math.random() > 0.7 ? shapeGreen : shapeWhite; points.push({ pos: [x + (Math.random() - 0.5) * spread, y + (Math.random() - 0.5) * spread, z + (Math.random() - 0.5) * spread], col: [color.r, color.g, color.b] }); } break;
case "dna": for (let i = 0; i < PARTICLE_COUNT; i++) { const t = (i / PARTICLE_COUNT) * Math.PI * 8; const y = (i / PARTICLE_COUNT - 0.5) * 45; const r = 10; const strand = i % 3; let x, z; if (strand === 0) { x = r * Math.cos(t); z = r * Math.sin(t); } else if (strand === 1) { x = r * Math.cos(t + Math.PI); z = r * Math.sin(t + Math.PI); } else { const barPos = Math.random(); x = r * Math.cos(t) * (1 - barPos) + r * Math.cos(t + Math.PI) * barPos; z = r * Math.sin(t) * (1 - barPos) + r * Math.sin(t + Math.PI) * barPos; } const spread = 2.0; const color = Math.random() > 0.7 ? shapeGreen : shapeWhite; points.push({ pos: [x + (Math.random() - 0.5) * spread, y + (Math.random() - 0.5) * spread, z + (Math.random() - 0.5) * spread], col: [color.r, color.g, color.b] }); } break;
case "infinity": const infScale = 25; for (let i = 0; i < PARTICLE_COUNT; i++) { const t = (i / PARTICLE_COUNT) * Math.PI * 6; const denom = 1 + Math.sin(t) * Math.sin(t); let x = infScale * Math.cos(t) / denom; let y = infScale * Math.sin(t) * Math.cos(t) / denom; const verticalSpread = (Math.random() - 0.5) * 20; const z = (Math.random() - 0.5) * 15 + Math.sin(t * 2) * 5; const thickness = 4.0; const offsetX = (Math.random() - 0.5) * thickness; const offsetY = (Math.random() - 0.5) * thickness + verticalSpread * 0.3; const offsetZ = (Math.random() - 0.5) * thickness; const color = Math.random() > 0.7 ? shapeGreen : shapeWhite; points.push({ pos: [x + offsetX, y + offsetY, z + offsetZ], col: [color.r, color.g, color.b] }); } break;
}
if (points.length > 0) {
for (let i = 0; i < PARTICLE_COUNT; i++) { const i3 = i * 3; const point = points[i % points.length]; targetPositions[i3] = point.pos[0]; targetPositions[i3 + 1] = point.pos[1]; targetPositions[i3 + 2] = point.pos[2]; targetColors[i3] = point.col[0]; targetColors[i3 + 1] = point.col[1]; targetColors[i3 + 2] = point.col[2]; }
geometry.attributes.targetPosition.needsUpdate = true;
geometry.attributes.targetColor.needsUpdate = true;
uploadedImage = "shape";
}
}
document.addEventListener("DOMContentLoaded", initParticleCanvas);
</script>
</body>
</html>