ideipro logotyp

Сделай это красиво: как на Gemini 3 собрать 3D-сайт с миллионами частиц, управляемых руками

Никаких бесконечных туториалов. Gemini 3 уже умеет генерировать рабочий код под интерактивный 3D прямо из пары фраз. Открываете Google AI Studio, кидаете промпт — и через минуту у вас index.html с Three.js, распознаванием обеих рук через камеру и панелью шаблонов: hearts, flowers, saturn, fireworks. Частицы реагируют на «напряжение» и «сжатие» ладоней, стиль не ломается, интерфейс минималистичный. Ниже — пошаговый сценарий и полностью готовый файл, который можно открыть в браузере и сразу играться с миллионами точек в воздухе. Да, руками.

Что вы получите за 10 минут

  • Реальный интерактивный 3D-сайт на Three.js с millions-style частицами.
  • Управление жестами обеих рук: «сжимаю — сжимаются», «расслабляю — разлетаются».
  • Переключение форм: hearts / flowers / saturn / fireworks.
  • Выбор цвета частиц.
  • Чистый, минималистичный UI без лишнего.

«Мы собрали промо-сцену за утро. Самое дорогое — это ощущение “оно слушается рук” без лагов», — арт-директор небольшой студии.

Быстрый путь: 4 шага от подсказки к сайту

  1. Откройте Google AI Studio и вставьте промпт: Создай интерактивную 3D систему частиц в реальном времени на Three.js. Требования: распознавать обе руки через камеру и управлять масштабом частиц и их расширением по силе напряжения и сжатию ладони; добавить панель переключения шаблонов hearts / flowers / saturn / fireworks; добавить выбор цвета для изменения цвета частиц; мгновенная реакция; интерфейс простой, современный, минималистичный. Сервис: aistudio.google.com
  2. Скопируйте сгенерированный код в Gemini 3 и попросите доработки, если нужно: «добавь ярлык color», «сделай сглаживание жестов».
  3. Создайте текстовый файл, вставьте код и сохраните как index.html.
  4. Откройте файл в браузере. Включите камеру.

Важная ремарка про камеру

Чтобы браузер дал доступ к камере, страницу нужно открыть как минимум по http://localhost или https. Проще всего:

  • Запустите локальный сервер: macOS/Linux: python3 -m http.server 8000 Windows (PowerShell): py -m http.server 8000
  • Откройте http://localhost:8000/index.html Без этого некоторые браузеры блокируют getUserMedia для файлов file://.
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> <title>Particles x Hands — Three.js + MediaPipe</title> <style> html, body { margin:0; height:100%; background:#0b0b0b; color:#ececec; font-family:Inter,system-ui,Arial,sans-serif; } #app { position:fixed; inset:0; } .ui { position: fixed; top: 16px; right: 16px; display: grid; gap: 10px; background: rgba(20,20,20,.6); backdrop-filter: blur(6px); border: 1px solid rgba(255,255,255,.08); border-radius: 12px; padding: 12px 14px; } .ui label { font-size:12px; opacity:.85; } .row { display:flex; align-items:center; gap:8px; } select, input[type=color] { background:#121212; color:#eee; border:1px solid rgba(255,255,255,.12); border-radius:8px; padding:6px 8px; font-size:12px; } .hint { position:fixed; left:16px; bottom:16px; font-size:12px; opacity:.6 } video { position: fixed; right: 16px; bottom: 16px; width: 160px; height: 120px; object-fit: cover; border-radius: 8px; border:1px solid rgba(255,255,255,.12); opacity:.25; } @media (max-width: 560px){ video{ display:none; } } </style> </head> <body> <div id="app"></div> <div class="ui"> <div class="row"> <label for="shape">Шаблон</label> <select id="shape"> <option value="hearts">hearts</option> <option value="flowers">flowers</option> <option value="saturn">saturn</option> <option value="fireworks">fireworks</option> </select> </div> <div class="row"> <label for="color">Цвет</label> <input id="color" type="color" value="#7dd3fc" /> </div> </div> <div class="hint">Сожмите/разожмите ладони перед камерой • ЛКМ — вращать, колесо — зум</div> <video id="video" playsinline muted></video> <!— Three.js —> <script src="https://unpkg.com/three@0.160.0/build/three.min.js"></script> <script src="https://unpkg.com/three@0.160.0/examples/js/controls/OrbitControls.js"></script> <!— MediaPipe Hands (classic JS API) —> <script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js"></script> <script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js"></script> <script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js"></script> <script> (() => { // ———- Scene ———- const app = document.getElementById('app'); const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false }); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setClearColor(0x0b0b0b, 1); app.appendChild(renderer.domElement); const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 2000); camera.position.set(0, 0.5, 6); const controls = new THREE.OrbitControls(camera, renderer.domElement); controls.enableDamping = true; const light = new THREE.AmbientLight(0xffffff, 0.6); scene.add(light); const dir = new THREE.DirectionalLight(0xffffff, 0.6); dir.position.set(2, 3, 4); scene.add(dir); // ———- Particles ———- const COUNT = 120000; // 120k — осторожно с мобилками const positions = new Float32Array(COUNT * 3); const targets = new Float32Array(COUNT * 3); const initial = new Float32Array(COUNT * 3); // random sphere baseline for (let i = 0; i < COUNT; i++) { const i3 = i * 3; const v = randomPointOnSphere(Math.random() * 0.9 + 0.1); positions[i3] = initial[i3] = v.x; positions[i3+1] = initial[i3+1] = v.y; positions[i3+2] = initial[i3+2] = v.z; targets[i3] = v.x; targets[i3+1] = v.y; targets[i3+2] = v.z; } const geometry = new THREE.BufferGeometry(); geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); geometry.setAttribute('initial', new THREE.BufferAttribute(initial, 3)); geometry.setAttribute('target', new THREE.BufferAttribute(targets, 3)); const uniforms = { uTime: { value: 0 }, uSize: { value: 2.0 }, uSpread: { value: 1.0 }, uColor: { value: new THREE.Color('#7dd3fc') }, uLerp: { value: 1.0 } }; const material = new THREE.ShaderMaterial({ transparent: true, depthWrite: false, uniforms, vertexShader: ` uniform float uTime; uniform float uSpread; uniform float uLerp; attribute vec3 initial; attribute vec3 target; void main() { vec3 pos = mix(initial, target, uLerp); pos *= uSpread; vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0); gl_PointSize = 2.0 * (300.0 / -mvPosition.z); gl_Position = projectionMatrix * mvPosition; } `, fragmentShader: ` precision mediump float; uniform vec3 uColor; void main() { vec2 uv = gl_PointCoord — 0.5; float d = dot(uv, uv); if (d > 0.25) discard; float alpha = smoothstep(0.25, 0.0, d); gl_FragColor = vec4(uColor, alpha); } ` });
const points = new THREE.Points(geometry, material); scene.add(points); // ———- Shapes ———- const SHAPERS = { hearts: heartCloud, flowers: flowerCloud, saturn: saturnCloud, fireworks: fireworksCloud }; function morphTo(name) { const generator = SHAPERS[name] || SHAPERS.hearts; generator(targets); geometry.attributes.target.needsUpdate = true; // animate lerp-in lerpProgress = 0; } // hearts shape function heartCloud(out) { for (let i = 0; i < COUNT; i++) { const t = Math.random() * Math.PI * 2; const s = (Math.random() ** 0.5) * 0.8 + 0.2; // 2D heart curve in XY, extruded slightly in Z const x = 16 * Math.pow(Math.sin(t), 3); const y = 13 * Math.cos(t) — 5 * Math.cos(2*t) — 2 * Math.cos(3*t) — Math.cos(4*t); const z = (Math.random() — 0.5) * 2.0; const i3 = i * 3; out[i3] = (x / 18) * s; out[i3+1] = (y / 18) * s; out[i3+2] = (z / 6) * s; } } // flowers: polar rose r = cos(k*theta) function flowerCloud(out) { const k = 5; // petals for (let i = 0; i < COUNT; i++) { const theta = Math.random() * Math.PI * 2; const r = Math.cos(k * theta) * 0.9; const s = (Math.random() ** 0.6) * 0.9 + 0.1; const x = r * Math.cos(theta) * s * 1.2; const y = r * Math.sin(theta) * s * 1.2; const z = (Math.random() — 0.5) * 0.8 * s; const i3 = i * 3; out[i3] = x; out[i3+1] = y; out[i3+2] = z; } } // saturn: sphere + ring function saturnCloud(out) { for (let i = 0; i < COUNT; i++) { const i3 = i * 3; if (i < COUNT * 0.6) { // sphere const v = randomPointOnSphere(1); out[i3] = v.x * 0.8; out[i3+1] = v.y * 0.8; out[i3+2] = v.z * 0.8; } else { // ring const a = Math.random() * Math.PI * 2; const r = 1.3 + Math.random() * 0.15; out[i3] = Math.cos(a) * r; out[i3+1] = (Math.random() — 0.5) * 0.05; out[i3+2] = Math.sin(a) * r; } } } // fireworks: expanding shell with noise function fireworksCloud(out) { for (let i = 0; i < COUNT; i++) { const i3 = i * 3; const v = randomPointOnSphere(1); const shell = (Math.random() ** 0.25) * 1.4 + 0.1; out[i3] = v.x * shell; out[i3+1] = v.y * shell; out[i3+2] = v.z * shell; } } function randomPointOnSphere(r=1) { const u = Math.random(); const v = Math.random(); const theta = 2 * Math.PI * u; const phi = Math.acos(2*v — 1); return new THREE.Vector3( r * Math.sin(phi) * Math.cos(theta), r * Math.sin(phi) * Math.sin(theta), r * Math.cos(phi) ); }
// ———- Hands ———- const video = document.getElementById('video'); let lastGesture = { strength: 0, spread: 1 }; let smoothStrength = 0, smoothSpread = 1; const hands = new Hands({ locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}` }); hands.setOptions({ maxNumHands: 2, selfieMode: true, modelComplexity: 1, minDetectionConfidence: 0.6, minTrackingConfidence: 0.6 }); hands.onResults(onHands); // camera const cam = new Camera(video, { onFrame: async () => { await hands.send({ image: video }); }, width: 640, height: 480 }); navigator.mediaDevices.getUserMedia({ video: true }).then(() => cam.start()) .catch(err => console.warn('Camera blocked or unavailable:', err)); function onHands(results) { // Aggregate both hands: compute openness based on average fingertip-to-palm distances let values = []; (results.multiHandLandmarks || []).forEach(landmarks => { values.push(handOpenness(landmarks)); }); const value = values.length ? values.reduce((a,b)=>a+b,0)/values.length : 0; // Map: value in [0..1] -> spread [0.6..1.8], strength used to point size const spread = THREE.MathUtils.clamp(0.6 + value * 1.2, 0.6, 1.8); lastGesture.strength = value; lastGesture.spread = spread; } // Openness metric: normalized distances of tips (4,8,12,16,20) to wrist (0) vs palm width (5..17) function handOpenness(L) { const wrist = L[0]; const palmW = dist(L[5], L[17]) + 1e-6; const tips = [L[4], L[8], L[12], L[16], L[20]]; let s = 0; for (const t of tips) s += dist(t, wrist) / palmW; s /= tips.length; // ~ open hand ~1.8..2.4, fist ~0.9..1.2 // normalize to 0..1 return THREE.MathUtils.clamp((s — 1.1) / (2.2 — 1.1), 0, 1); } function dist(a,b){ const dx=a.x-b.x, dy=a.y-b.y, dz=(a.z||0)-(b.z||0); return Math.hypot(dx,dy,dz); } // ———- UI ———- const shapeSel = document.getElementById('shape'); const colorInp = document.getElementById('color'); shapeSel.addEventListener('change', () => morphTo(shapeSel.value)); colorInp.addEventListener('input', () => uniforms.uColor.value.set(colorInp.value)); // init morphTo(shapeSel.value); uniforms.uColor.value.set(colorInp.value); // ———- Animate ———- let clock = new THREE.Clock(); let lerpProgress = 1; function animate() { requestAnimationFrame(animate); const dt = Math.min(clock.getDelta(), 0.033); uniforms.uTime.value += dt; // Smooth gesture for stability smoothStrength = THREE.MathUtils.damp(smoothStrength, lastGesture.strength, 8, dt); smoothSpread = THREE.MathUtils.damp(smoothSpread, lastGesture.spread, 8, dt); // Apply to uniforms uniforms.uSpread.value = smoothSpread; // Size: base 2..5 px depending on strength const size = 1.5 + smoothStrength * 3.0; // We can piggyback point size through vertex math (gl_PointSize); just adjust a scalar here material.needsUpdate = true; // Hack: modulate perspective size by moving model scale subtly points.scale.setScalar(1 + smoothStrength * 0.05); // shape morphing lerpProgress = Math.min(1, lerpProgress + dt * 0.8); uniforms.uLerp.value = lerpProgress; controls.update(); renderer.render(scene, camera); } animate(); // Resize window.addEventListener('resize', () => { const w = window.innerWidth, h = window.innerHeight; renderer.setSize(w, h); camera.aspect = w / h; camera.updateProjectionMatrix(); }); })(); </script> </body> </html>

Готовый файл index.html

Скопируйте всё ниже в index.html, запустите локальный сервер и откройте страницу. В коде используются CDN Three.js и MediaPipe Hands, простой шейдер для частиц, морфинг между формами, мгновенная реакция на жесты обеих рук и минималистичная панель управления.

Источник: vc.ru

✅ Найденные теги: 3D-сайт, Gemini 3, Красота, новости, Сделай, Управление, Частицы

ОСТАВЬТЕ СВОЙ КОММЕНТАРИЙ

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Каталог бесплатных опенсорс-решений, которые можно развернуть локально и забыть о подписках

галерея

Фото сгенерированных лиц: исследование показывает, что люди не могут отличить настоящие лица от сгенерированных
Нейросети построили капитализм за трое суток: 100 агентов Claude заперли…
Скетч: цифровой осьминог и виртуальный мир внутри компьютера с человечком.
Сцена с жестами пальцами, где один жест символизирует "VPN", а другой "KHP".
‼️Paramount купила Warner Bros. Discovery — сумма сделки составила безумные…
Скриншот репозитория GitHub "Claude Scientific Skills" AI для научных исследований.
Структура эффективного запроса Claude с элементами задачи, контекста и референса.
Эскиз и готовая веб-страница платформы для AI-дизайна в современном темном режиме.
ideipro logotyp
Image Not Found
Звёздное небо с галактиками и туманностями, космос, Вселенная, астрофотография.

Система оповещения обсерватории Рубина отправила 800 000 сигналов в первую ночь наблюдений.

Астрономы будут получать оповещения о небесных явлениях в течение нескольких минут после их обнаружения. Теренс О'Брайен, редактор раздела «Выходные». Публикации этого автора будут добавляться в вашу ежедневную рассылку по электронной почте и в ленту новостей на главной…

Мар 2, 2026
Женщина с длинными тёмными волосами в синем свете, нейтральный фон.

Расследование в отношении 61-фунтовой машины, которая «пожирает» пластик и выплевывает кирпичи.

Обзор компактного пресса для мягкого пластика Clear Drop — и что будет дальше. Шон Холлистер, старший редактор Публикации этого автора будут добавляться в вашу ежедневную рассылку по электронной почте и в ленту новостей на главной странице вашего…

Мар 2, 2026
Черный углеродное волокно с текстурой плетения, отражающий свет.

Материал будущего: как работает «бессмертный» композит

Учёные из Университета штата Северная Каролина представили композит нового поколения, способный самостоятельно восстанавливаться после серьёзных повреждений.  Речь идёт о модифицированном армированном волокном полимере (FRP), который не просто сохраняет прочность при малом весе, но и способен «залечивать» внутренние…

Мар 2, 2026
Круглый экран с изображением замка и горы, рядом электронная плата.

Круглый дисплей Waveshare для креативных проектов

Круглый 7-дюймовый сенсорный дисплей от Waveshare создан для разработчиков и дизайнеров, которым нужен нестандартный экран.  Это IPS-панель с разрешением 1 080×1 080 пикселей, поддержкой 10-точечного ёмкостного сенсора, оптической склейкой и защитным закалённым стеклом, выполненная в круглом форм-факторе.…

Мар 2, 2026

Впишите свой почтовый адрес и мы будем присылать вам на почту самые свежие новости в числе самых первых