We use cookies to provide the best site experience.
Experiments with writing code using LLMs (ChatGPT, Claude, DeepSeek, and others) to create interactive web graphics.
 
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Infinity blood</title>
<style>
/* Отключаем выбор текста и контекстное меню */
body, #three-container {
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
margin: 0;
padding: 0;
}
/* Контейнер, увеличенный в 1.5 раза по max-width */
#three-container {
width: 90vw;
max-width: 1200px; /* Максимальная ширина для расчёта */
aspect-ratio: 1/1;
margin: 0 auto;
background: #fff;
position: relative;
}
/* Стили для заголовка */
#header-text {
position: absolute;
font-style: italic;
font-weight: bold;
pointer-events: none;
z-index: 10;
transform: translate(-50%, -50%);
white-space: nowrap; /* Заголовок всегда в одну строку */
}
/* Каждый символ выводим как inline-block для возможности отдельного позиционирования */
#header-text span {
display: inline-block;
/* Светло-серый цвет, практически белый */
color: rgb(245, 245, 245);
}
@media (min-width: 768px) {
#three-container {
width: 70vw;
}
}
</style>
</head>
<body>
<div id="three-container">
<!-- Заголовок разбитый на span-элементы -->
<div id="header-text">Infinity blood</div>
</div>
<!-- Подключаем Three.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
const container = document.getElementById('three-container');
const headerText = document.getElementById('header-text');
// Разбиваем текст заголовка на отдельные span-элементы для каждой буквы
const originalText = headerText.innerText;
headerText.innerHTML = "";
for (const char of originalText) {
const span = document.createElement("span");
// Если символ – пробел, заменяем его на неразрывный пробел
span.innerHTML = (char === " ") ? "&nbsp;" : char;
headerText.appendChild(span);
}
// Функция для установки адаптивного размера заголовка с учетом разных версий
function updateHeaderFontSize() {
const containerWidth = container.clientWidth;
// Пороговые значения для мобильной и максимальной версии
const mobileThreshold = 768;
const desktopThreshold = 1200;
// Базовый коэффициент для мобильной версии (раньше был 1.875)
const mobileMultiplier = 1.875;
// Для максимальной версии коэффициент 1/1.5 ≈ 0.6667
const desktopMultiplier = 1 / 1.5;
let multiplier;
if (containerWidth <= mobileThreshold) {
// Если ширина между 480 и 640, применяем дополнительное уменьшение (на 10%)
if (containerWidth >= 480 && containerWidth <= 640) {
multiplier = mobileMultiplier * 0.9;
} else {
multiplier = mobileMultiplier;
}
} else if (containerWidth >= desktopThreshold) {
multiplier = desktopMultiplier;
} else {
// Линейная интерполяция между мобильной и максимальной версиями
multiplier = mobileMultiplier - ((containerWidth - mobileThreshold) / (desktopThreshold - mobileThreshold)) * (mobileMultiplier - desktopMultiplier);
}
const width = container.clientWidth;
const height = container.clientHeight;
const viewSize = Math.min(width, height);
const baseContainerSize = 800; // базовый размер для расчёта адаптивных коэффициентов
const scale = viewSize / baseContainerSize;
// Базовый размер шрифта 40px, умножаем на scale и вычисленный multiplier
headerText.style.fontSize = (40 * scale * multiplier) + "px";
}
// Начальный вызов
updateHeaderFontSize();
// Определяем, мобильное ли устройство
const isMobile = /Mobi|Android/i.test(navigator.userAgent);
// Начальные параметры, зависящие от размеров контейнера
let width = container.clientWidth;
let height = container.clientHeight;
let viewSize = Math.min(width, height);
const baseContainerSize = 800;
let scale = viewSize / baseContainerSize;
let mouseEffectRadius = 200 * scale; 
let attractionRadius = 400 * scale; 
let deadZone = 20 * scale; 
// Коэффициент для уменьшения масштаба потока (чтобы поток оставался прежнего размера относительно контейнера)
const flowScale = 1 / 1.5;
// Дополнительный коэффициент для мобильной версии (ширина кривой увеличена)
const mobileFactor = isMobile ? 1.5 : 1.0;
// Параметр, задающий расстояние от нижней границы кривой до заголовка (в пикселях)
const headerOffset = 80;
// "Курсорная" позиция. Начальные значения далеко за пределами видимой области
const neutralPosition = new THREE.Vector2(9999, 9999);
let mouse = new THREE.Vector2().copy(neutralPosition);
let targetMouse = new THREE.Vector2().copy(neutralPosition);
// Переменные для мобильного длительного нажатия
let pressStartTime = 0;
let isTouching = false;
let isPressActive = false;
const pressThreshold = 30;
let currentTouchPosition = new THREE.Vector2().copy(neutralPosition);
// Создаем сцену
const scene = new THREE.Scene();
// Создаем ортографическую камеру с центром в (0,0)
let camera = new THREE.OrthographicCamera(
-width / 2, width / 2,
height / 2, -height / 2,
-1000, 1000
);
camera.position.z = 10;
// Создаем рендерер
let renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(width, height);
renderer.setClearColor(0xffffff, 1);
container.appendChild(renderer.domElement);
// Параметры частиц
const PARTICLE_COUNT = 900;
const TWO_PI = Math.PI * 2;
const maxParticleMultiplier = 2;
const attractStrength = 40;
const streamThickness = 20;
// Создаем частицы как отдельные меши
const particles = [];
for (let i = 0; i < PARTICLE_COUNT; i++) {
const particleGeometry = new THREE.CircleGeometry(1, 16);
const particleMaterial = new THREE.MeshBasicMaterial({ color: 0x000000 });
const particleMesh = new THREE.Mesh(particleGeometry, particleMaterial);
scene.add(particleMesh);
particles.push({
curveProgress: Math.random(),
curveSpeed: (1/6) * (0.3 + Math.random() * 0.1),
lifeProgress: Math.random(),
lifeSpeed: 0.5 + Math.random() * 0.5,
phase: Math.random() * TWO_PI,
thicknessOffset: (Math.random() - 0.5) * streamThickness,
mesh: particleMesh
});
}
// Обработчик для desktop: обновление позиции курсора при движении мыши
if (!isMobile) {
container.addEventListener('mousemove', function(e) {
const rect = container.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
mouse.set(x - width / 2, height / 2 - y);
targetMouse.copy(mouse);
});
container.addEventListener('mouseleave', function() {
mouse.copy(neutralPosition);
targetMouse.copy(neutralPosition);
});
}
// Для мобильных устройств: обрабатываем длительное нажатие
if (isMobile) {
const resetCursorPosition = function() {
isTouching = false;
isPressActive = false;
pressStartTime = 0;
mouse.copy(neutralPosition);
targetMouse.copy(neutralPosition);
currentTouchPosition.copy(neutralPosition);
};
container.addEventListener('touchstart', function(e) {
e.preventDefault();
if (e.touches.length > 0) {
isTouching = true;
pressStartTime = Date.now();
const touch = e.touches[0];
const rect = container.getBoundingClientRect();
const x = touch.clientX - rect.left;
const y = touch.clientY - rect.top;
currentTouchPosition.set(x - width / 2, height / 2 - y);
}
}, { passive: false });
container.addEventListener('touchmove', function(e) {
if (e.touches.length > 0) {
const touch = e.touches[0];
const rect = container.getBoundingClientRect();
const x = touch.clientX - rect.left;
const y = touch.clientY - rect.top;
currentTouchPosition.set(x - width / 2, height / 2 - y);
}
}, { passive: false });
container.addEventListener('touchend', resetCursorPosition);
container.addEventListener('touchcancel', resetCursorPosition);
}
// Функция кривой бесконечности (перевёрнутая восьмерка) с учетом мобильного фактора
function infinityCurve(t) {
const a = (viewSize * flowScale * mobileFactor) / 3;
let x = a * Math.cos(t);
let y = a * Math.sin(t) * Math.cos(t);
return new THREE.Vector2(x, y);
}
// Функция для вычисления касательной к кривой
function tangentAt(t) {
const a = (viewSize * flowScale * mobileFactor) / 3;
let dx = -a * Math.sin(t);
let dy = a * Math.cos(2 * t);
return new THREE.Vector2(dx, dy).normalize();
}
// Цвет для временных расчетов
const color = new THREE.Color();
// Анимация
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
if (isMobile) {
if (isTouching && Date.now() - pressStartTime >= pressThreshold) {
isPressActive = true;
targetMouse.copy(currentTouchPosition);
} else if (!isTouching) {
targetMouse.copy(neutralPosition);
}
}
if (!isMobile || isPressActive) {
mouse.lerp(targetMouse, 0.05);
} else {
mouse.copy(neutralPosition);
}
for (let i = 0; i < PARTICLE_COUNT; i++) {
const p = particles[i];
const mesh = p.mesh;
p.curveProgress += p.curveSpeed * delta;
if (p.curveProgress > 1) p.curveProgress -= 1;
p.lifeProgress += p.lifeSpeed * delta;
if (p.lifeProgress > 1) p.lifeProgress -= 1;
let t = p.curveProgress * TWO_PI;
let basePos = infinityCurve(t);
const fluctAmplitude = 10;
let fluctX = fluctAmplitude * Math.sin(p.phase + t);
let fluctY = fluctAmplitude * Math.cos(p.phase + t);
let pos = new THREE.Vector2(basePos.x + fluctX, basePos.y + fluctY);
let tan = tangentAt(t);
let perp = new THREE.Vector2(-tan.y, tan.x);
pos.add(perp.multiplyScalar(p.thicknessOffset));
if (basePos.distanceTo(mouse) < attractionRadius && basePos.distanceTo(mouse) > deadZone) {
let effectiveDist = Math.max(basePos.distanceTo(mouse), deadZone);
let attractForce = Math.pow((attractionRadius - effectiveDist) / attractionRadius, 2);
let attractDir = new THREE.Vector2().subVectors(mouse, basePos).normalize();
let attractOffset = attractDir.multiplyScalar(attractForce * attractStrength);
pos.add(attractOffset);
}
let scaleVal = Math.sin(Math.PI * p.lifeProgress);
let finalScale = scaleVal * maxParticleMultiplier;
if (basePos.distanceTo(mouse) < mouseEffectRadius) {
let enlargeFactor = 1 + 2 * ((mouseEffectRadius - basePos.distanceTo(mouse)) / mouseEffectRadius);
finalScale *= enlargeFactor;
}
mesh.position.set(pos.x, pos.y, 0);
mesh.scale.set(finalScale, finalScale, 1);
let distToMouse = basePos.distanceTo(mouse);
if (distToMouse < mouseEffectRadius) {
let redFactor = 1 - (distToMouse / mouseEffectRadius);
redFactor = Math.pow(redFactor, 0.5);
color.setRGB(redFactor, 0, 0);
mesh.material.color.copy(color);
} else {
mesh.material.color.setRGB(0, 0, 0);
}
}
// Позиционирование заголовка без изменения цвета
const aValue = (viewSize * flowScale * mobileFactor) / 3;
const headerCenterX = width / 2;
const headerCenterY = height / 2 - aValue - headerOffset;
headerText.style.left = headerCenterX + "px";
headerText.style.top = headerCenterY + "px";
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize', function() {
width = container.clientWidth;
height = container.clientHeight;
viewSize = Math.min(width, height);
scale = viewSize / baseContainerSize;
mouseEffectRadius = 200 * scale;
attractionRadius = 400 * scale;
deadZone = 20 * scale;
camera.left = -width / 2;
camera.right = width / 2;
camera.top = height / 2;
camera.bottom = -height / 2;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
updateHeaderFontSize();
});
});
</script>
</body>
</html>
 

 
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>infinity nature</title>
<style>
body, #three-container {
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
margin: 0;
padding: 0;
}
#three-container {
width: 90vw;
max-width: 1200px;
aspect-ratio: 1/1;
margin: 0 auto;
background: #fff;
position: relative;
}
#header-text {
position: absolute;
font-style: italic;
font-weight: bold;
pointer-events: none;
z-index: 10;
transform: translate(-50%, -50%);
white-space: nowrap;
}
#header-text span {
display: inline-block;
color: rgb(245,245,245);
}
@media (min-width: 768px) {
#three-container {
width: 70vw;
}
}
</style>
</head>
<body>
<div id="three-container">
<div id="header-text">infinity nature</div>
</div>
<!-- Подключаем Three.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
// Глобальные настройки
const CONFIG = {
particles: {
count: 450,
minThickness: 0.1,
maxThickness: 3.0,
numPoints: 15
},
interaction: {
baseCursorRadius: 100,
greenEffectRadius: 100,
pressThreshold: 30
},
animation: {
flowScale: 1 / 1.5,
headerOffset: 80
}
};
const container = document.getElementById('three-container');
const headerText = document.getElementById('header-text');
const TWO_PI = Math.PI * 2;
// Подготавливаем заголовок
initializeHeaderText();
updateHeaderFontSize();
// Определяем базовые параметры сцены
const isMobile = /Mobi|Android/i.test(navigator.userAgent);
let dimensions = calculateDimensions();
// Точки для отслеживания курсора
const neutralPosition = new THREE.Vector2(9999, 9999);
let mouse = new THREE.Vector2().copy(neutralPosition);
let targetMouse = new THREE.Vector2().copy(neutralPosition);
// Переменные для сенсорного взаимодействия
let pressStartTime = 0, isTouching = false, isPressActive = false;
let currentTouchPosition = new THREE.Vector2().copy(neutralPosition);
// Инициализация ThreeJS
const scene = new THREE.Scene();
const camera = createCamera();
const renderer = createRenderer();
container.appendChild(renderer.domElement);
// Создаем частицы
const particles = createParticles();
// Настраиваем обработчики событий
setupInteractions();
// Запускаем анимацию
const clock = new THREE.Clock();
animate();
// Обработчик изменения размера окна
window.addEventListener('resize', handleResize);
// Функции инициализации
function initializeHeaderText() {
const originalText = headerText.innerText;
headerText.innerHTML = "";
for (const char of originalText) {
const span = document.createElement("span");
span.innerHTML = (char === " ") ? " " : char;
headerText.appendChild(span);
}
}
function calculateDimensions() {
const width = container.clientWidth;
const height = container.clientHeight;
const viewSize = Math.min(width, height);
const baseContainerSize = 800;
const scale = viewSize / baseContainerSize;
const mobileFactor = isMobile ? 1.5 : 1.0;
let cursorAttractRadius = CONFIG.interaction.baseCursorRadius * scale;
let greenEffectRadius = CONFIG.interaction.greenEffectRadius * scale;
if (isMobile || (width >= 480 && width <= 640)) {
cursorAttractRadius *= 2;
greenEffectRadius *= 2;
}
return {
width, height, viewSize, scale, mobileFactor,
cursorAttractRadius: cursorAttractRadius,
greenEffectRadius: greenEffectRadius,
attractionRadius: 400 * scale,
deadZone: 20 * scale
};
}
function updateHeaderFontSize() {
const containerWidth = container.clientWidth;
const mobileThreshold = 768;
const desktopThreshold = 1200;
const mobileMultiplier = 1.875;
const desktopMultiplier = 1 / 1.5;
let multiplier;
if (containerWidth <= mobileThreshold) {
multiplier = (containerWidth >= 480 && containerWidth <= 640) ? mobileMultiplier * 0.9 : mobileMultiplier;
} else if (containerWidth >= desktopThreshold) {
multiplier = desktopMultiplier;
} else {
multiplier = mobileMultiplier - ((containerWidth - mobileThreshold) / (desktopThreshold - mobileThreshold)) * (mobileMultiplier - desktopMultiplier);
}
const width = container.clientWidth;
const height = container.clientHeight;
const viewSize = Math.min(width, height);
const baseContainerSize = 800;
const scale = viewSize / baseContainerSize;
headerText.style.fontSize = (40 * scale * multiplier) + "px";
}
function createCamera() {
const camera = new THREE.OrthographicCamera(
-dimensions.width / 2, dimensions.width / 2,
dimensions.height / 2, -dimensions.height / 2,
-1000, 1000
);
camera.position.z = 10;
return camera;
}
function createRenderer() {
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true,
powerPreference: 'high-performance'
});
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(dimensions.width, dimensions.height);
renderer.setClearColor(0xffffff, 1);
return renderer;
}
function createParticles() {
const particles = [];
const { count, numPoints, minThickness, maxThickness } = CONFIG.particles;
for (let i = 0; i < count; i++) {
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(2 * numPoints * 3);
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const indices = [];
for (let j = 0; j < numPoints - 1; j++) {
const i0 = 2 * j, i1 = 2 * j + 1, i2 = 2 * (j + 1), i3 = 2 * (j + 1) + 1;
indices.push(i0, i1, i2);
indices.push(i1, i3, i2);
}
geometry.setIndex(indices);
geometry.computeBoundingSphere();
const material = new THREE.MeshBasicMaterial({ color: 0x000000, side: THREE.DoubleSide });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
particles.push({
geometry: geometry,
mesh: mesh,
curveProgress: Math.random(),
curveSpeed: (1/6) * (0.3 + Math.random() * 0.1),
lifeProgress: Math.random(),
lifeSpeed: 0.5 + Math.random() * 0.5,
phase: Math.random() * TWO_PI,
delta: 0.05 + Math.random() * 0.35,
thickness: minThickness + Math.random() * (maxThickness - minThickness),
sensitivity: 0.5 + Math.random()
});
}
return particles;
}
function setupInteractions() {
if (!isMobile) {
const throttledMouseMove = throttle(function(e) {
const rect = container.getBoundingClientRect();
const x = e.clientX - rect.left, y = e.clientY - rect.top;
mouse.set(x - dimensions.width / 2, dimensions.height / 2 - y);
targetMouse.copy(mouse);
}, 16);
container.addEventListener('mousemove', throttledMouseMove);
container.addEventListener('mouseleave', function() {
mouse.copy(neutralPosition);
targetMouse.copy(neutralPosition);
});
}
if (isMobile) {
const resetCursorPosition = function() {
isTouching = false; isPressActive = false; pressStartTime = 0;
mouse.copy(neutralPosition);
targetMouse.copy(neutralPosition);
currentTouchPosition.copy(neutralPosition);
};
container.addEventListener('touchstart', function(e) {
e.preventDefault();
if (e.touches.length > 0) {
isTouching = true;
pressStartTime = Date.now();
const touch = e.touches[0];
const rect = container.getBoundingClientRect();
const x = touch.clientX - rect.left, y = touch.clientY - rect.top;
currentTouchPosition.set(x - dimensions.width / 2, dimensions.height / 2 - y);
}
}, { passive: false });
const throttledTouchMove = throttle(function(e) {
if (e.touches.length > 0) {
const touch = e.touches[0];
const rect = container.getBoundingClientRect();
const x = touch.clientX - rect.left, y = touch.clientY - rect.top;
currentTouchPosition.set(x - dimensions.width / 2, dimensions.height / 2 - y);
}
}, 16);
container.addEventListener('touchmove', throttledTouchMove, { passive: false });
container.addEventListener('touchend', resetCursorPosition);
container.addEventListener('touchcancel', resetCursorPosition);
}
}
function throttle(func, limit) {
let lastFunc, lastRan;
return function() {
const context = this;
const args = arguments;
if (!lastRan) {
func.apply(context, args);
lastRan = Date.now();
} else {
clearTimeout(lastFunc);
lastFunc = setTimeout(function() {
if (Date.now() - lastRan >= limit) {
func.apply(context, args);
lastRan = Date.now();
}
}, limit - (Date.now() - lastRan));
}
};
}
function infinityCurve(t) {
const a = (dimensions.viewSize * CONFIG.animation.flowScale * dimensions.mobileFactor) / 3;
let x = a * Math.cos(t);
let y = a * Math.sin(t) * Math.cos(t);
return new THREE.Vector2(x, y);
}
function tangentAt(t) {
const a = (dimensions.viewSize * CONFIG.animation.flowScale * dimensions.mobileFactor) / 3;
let dx = -a * Math.sin(t);
let dy = a * Math.cos(2 * t);
return new THREE.Vector2(dx, dy).normalize();
}
function animate() {
requestAnimationFrame(animate);
if (document.hidden) return;
const delta = clock.getDelta();
if (isMobile) {
if (isTouching && Date.now() - pressStartTime >= CONFIG.interaction.pressThreshold) {
isPressActive = true;
targetMouse.copy(currentTouchPosition);
} else if (!isTouching) {
targetMouse.copy(neutralPosition);
}
}
if (!isMobile || isPressActive) {
const dist = mouse.distanceTo(targetMouse);
const lerpFactor = (dist > 50) ? 0.02 : 0.05;
mouse.lerp(targetMouse, lerpFactor);
} else {
mouse.copy(neutralPosition);
}
const posCenter = new THREE.Vector2();
const normal = new THREE.Vector2();
const attractOffset = new THREE.Vector2();
const leftPos = new THREE.Vector2();
const rightPos = new THREE.Vector2();
for (let i = 0; i < particles.length; i++) {
const p = particles[i];
const mesh = p.mesh;
const geometry = p.geometry;
const positions = geometry.attributes.position.array;
p.curveProgress += p.curveSpeed * delta;
if (p.curveProgress > 1) p.curveProgress -= 1;
p.lifeProgress += p.lifeSpeed * delta;
if (p.lifeProgress > 1) p.lifeProgress -= 1;
let t0 = p.curveProgress * TWO_PI;
const fluctAmplitude = 10;
let dynamicDelta = p.delta * (1 + 0.3 * Math.sin(Math.PI * p.lifeProgress));
for (let j = 0; j < CONFIG.particles.numPoints; j++) {
let lerpT = j / (CONFIG.particles.numPoints - 1);
let currentT = t0 + lerpT * dynamicDelta;
let basePos = infinityCurve(currentT);
let fluctX = fluctAmplitude * Math.sin(p.phase + currentT);
let fluctY = fluctAmplitude * Math.cos(p.phase + currentT);
posCenter.set(basePos.x + fluctX, basePos.y + fluctY);
let tan = tangentAt(currentT);
normal.set(-tan.y, tan.x);
let distToCursor = posCenter.distanceTo(mouse);
if (distToCursor < dimensions.cursorAttractRadius) {
let effectiveSensitivity = Math.pow((p.sensitivity - 0.5) / 1, 3);
if (p.sensitivity > 1) effectiveSensitivity *= 2;
let attractStrengthVertex = 0.8 * effectiveSensitivity;
attractOffset.subVectors(mouse, posCenter)
.multiplyScalar(attractStrengthVertex * (dimensions.cursorAttractRadius - distToCursor) / dimensions.cursorAttractRadius);
posCenter.add(attractOffset);
}
let thicknessFactor = 0.5;
if (distToCursor < dimensions.cursorAttractRadius) {
thicknessFactor = 1 + (p.sensitivity * ((dimensions.cursorAttractRadius - distToCursor) / dimensions.cursorAttractRadius)) * 3;
}
let halfWidth = 0.5 * p.thickness * 1.3 * thicknessFactor;
leftPos.copy(posCenter).add(normal.clone().multiplyScalar(halfWidth));
rightPos.copy(posCenter).add(normal.clone().multiplyScalar(-halfWidth));
let li = 2 * j * 3;
positions[li] = leftPos.x;
positions[li+1] = leftPos.y;
positions[li+2] = 0;
let ri = (2 * j + 1) * 3;
positions[ri] = rightPos.x;
positions[ri+1] = rightPos.y;
positions[ri+2] = 0;
}
geometry.attributes.position.needsUpdate = true;
let baseDist = infinityCurve(t0).distanceTo(mouse);
if (baseDist < dimensions.greenEffectRadius) {
let factor = 1 - (baseDist / dimensions.greenEffectRadius);
factor = Math.pow(factor, 0.5) * p.sensitivity;
mesh.material.color.setRGB(0.2 * factor, 0.6 * factor, 0.2 * factor);
} else {
mesh.material.color.setRGB(0, 0, 0);
}
}
const aValue = (dimensions.viewSize * CONFIG.animation.flowScale * dimensions.mobileFactor) / 3;
const headerCenterX = dimensions.width / 2;
const headerCenterY = dimensions.height / 2 - aValue - CONFIG.animation.headerOffset;
headerText.style.left = headerCenterX + "px";
headerText.style.top = headerCenterY + "px";
renderer.render(scene, camera);
}
function handleResize() {
dimensions = calculateDimensions();
camera.left = -dimensions.width / 2;
camera.right = dimensions.width / 2;
camera.top = dimensions.height / 2;
camera.bottom = -dimensions.height / 2;
camera.updateProjectionMatrix();
renderer.setSize(dimensions.width, dimensions.height);
updateHeaderFontSize();
}
});
</script>
</body>
</html>
 

 
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Мозаика с квадратами и частицами</title>
<style>
#three-container {
width: 90vw;
max-width: 800px;
aspect-ratio: 1/1;
margin: 0 auto;
background: #fff;
position: relative;
}
/* Для веб-версии: если ширина экрана больше 768px, уменьшаем область до 70vw */
@media (min-width: 768px) {
#three-container {
width: 70vw;
}
}
</style>
</head>
<body>
<div id="three-container"></div>
<!-- Подключаем Three.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
const container = document.getElementById('three-container');
// Создаем сцену и камеру
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera(-100, 100, 100, -100, 0.1, 1000);
camera.position.set(0, 0, 10);
camera.lookAt(0, 0, 0);
// Рендерер
const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setClearColor(0xffffff, 1);
renderer.setSize(container.clientWidth, container.clientHeight);
container.appendChild(renderer.domElement);
// Шейдеры для мозаичных квадратов
const vertexShader = `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShader = `
uniform sampler2D uTexture;
uniform vec4 uRegion;
uniform float uScale;
varying vec2 vUv;
void main() {
vec2 globalUV = uRegion.xy + vUv * uRegion.zw;
vec4 color = texture2D(uTexture, globalUV);
if(uScale <= 0.5) {
float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
color = vec4(vec3(gray), color.a);
}
gl_FragColor = color;
}
`;
// Параметры мозаики и анимации
const gridCount = 40; // сетка 40x40
const baseScale = 0.4;
const maxScaleDelta = 1.5;
const influenceRadius = 150;
const inertiaSpeed = 0.1;
const offsetInertiaSpeed = 0.05;
const maxRotation = 0.1; // минимальный поворот квадратов
// Параметр для регулировки числа частиц при спауне
const particlesPerSpawn = 10;
// Параметры для регулировки размера лепестков
const minPetalFactor = 0.5; // минимальный множитель размера лепестка
const maxPetalFactor = 6.5; // максимальный множитель размера лепестка
// Флаг, отслеживающий движение мыши
let mouseActive = false;
// Переменная для хранения координат курсора (в нормализованных координатах)
const mouse = new THREE.Vector2();
window.addEventListener('mousemove', (event) => {
mouseActive = true;
const rect = container.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / container.clientWidth) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / container.clientHeight) * 2 + 1;
});
// Начальная анимация сборки: длительность 2000 мс (2 секунды)
let initialAnimationStart = performance.now();
const initialAnimationDuration = 2000; // мс
// Функция easeInOutQuad для плавного интерполирования (ease in/out)
function easeInOutQuad(t) {
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
}
// Загружаем изображение (новый URL)
const textureLoader = new THREE.TextureLoader();
textureLoader.load(
'https://i.postimg.cc/vT23SywV/wak-47-made-from-a-lot-of-green.png',
(texture) => {
texture.wrapS = THREE.ClampToEdgeWrapping;
texture.wrapT = THREE.ClampToEdgeWrapping;
texture.minFilter = THREE.LinearFilter;
texture.magFilter = THREE.LinearFilter;
const imgWidth = texture.image.width;
const imgHeight = texture.image.height;
const shortSide = Math.min(imgWidth, imgHeight);
// Вычисляем область для кадрирования (если изображение не квадратное)
let uMin = 0, uMax = 1, vMin = 0, vMax = 1;
if (imgWidth > imgHeight) {
const offset = (imgWidth - shortSide) / 2;
uMin = offset / imgWidth;
uMax = (offset + shortSide) / imgWidth;
} else if (imgHeight > imgWidth) {
const offset = (imgHeight - shortSide) / 2;
vMin = offset / imgHeight;
vMax = (offset + shortSide) / imgHeight;
}
// Настраиваем камеру по размеру изображения
camera.left = -shortSide / 2;
camera.right = shortSide / 2;
camera.top = shortSide / 2;
camera.bottom = -shortSide / 2;
camera.near = -1000;
camera.far = 1000;
camera.updateProjectionMatrix();
// Создаем offscreen canvas для выборки цвета из изображения
const offscreenCanvas = document.createElement('canvas');
offscreenCanvas.width = texture.image.width;
offscreenCanvas.height = texture.image.height;
const offscreenCtx = offscreenCanvas.getContext('2d');
offscreenCtx.drawImage(texture.image, 0, 0);
// Функция создания геометрии лепестка с формой лепестка
function createPetalGeometry(scale) {
const shape = new THREE.Shape();
// Рисуем контур лепестка с помощью квадратичных кривых
shape.moveTo(0, 0);
shape.quadraticCurveTo(10, 15, 0, 30);
shape.quadraticCurveTo(-10, 15, 0, 0);
const geometry = new THREE.ShapeGeometry(shape);
geometry.scale(scale, scale, scale);
return geometry;
}
// Создаем мозаичные квадраты
const pieces = [];
const cellSize = shortSide / gridCount;
for (let i = 0; i < gridCount; i++) {
for (let j = 0; j < gridCount; j++) {
const uRange = uMax - uMin;
const vRange = vMax - vMin;
const uLeft = uMin + uRange * (j / gridCount);
const uRight = uMin + uRange * ((j + 1) / gridCount);
const vTop = vMax - vRange * (i / gridCount);
const vBottom = vMax - vRange * ((i + 1) / gridCount);
const region = new THREE.Vector4(uLeft, vBottom, uRight - uLeft, vTop - vBottom);
const material = new THREE.ShaderMaterial({
uniforms: {
uTexture: { value: texture },
uRegion: { value: region },
uScale: { value: baseScale }
},
vertexShader: vertexShader,
fragmentShader: fragmentShader,
transparent: true
});
// Используем PlaneGeometry для создания квадратов
const geometry = new THREE.PlaneGeometry(cellSize, cellSize);
const cellMesh = new THREE.Mesh(geometry, material);
const baseX = j * cellSize - shortSide / 2 + cellSize / 2;
const baseY = -(i * cellSize - shortSide / 2 + cellSize / 2);
// Финальная (базовая) позиция
cellMesh.userData.basePosition = new THREE.Vector2(baseX, baseY);
cellMesh.userData.initialPosition = new THREE.Vector2(
baseX + (Math.random() - 0.5) * shortSide * 0.2,
baseY + (Math.random() - 0.5) * shortSide * 0.2
);
cellMesh.userData.baseScale = baseScale;
cellMesh.userData.currentScale = 0;
cellMesh.userData.currentOffset = new THREE.Vector2(0, 0);
cellMesh.userData.currentRotation = 0;
// Изначально помещаем квадрат в начальную позицию с нулевым масштабом
cellMesh.position.set(cellMesh.userData.initialPosition.x, cellMesh.userData.initialPosition.y, 0);
cellMesh.scale.set(0, 0, 0);
cellMesh.rotation.z = 0;
scene.add(cellMesh);
pieces.push(cellMesh);
}
}
// Массив для частиц и переменные для управления их рождением
let particles = [];
let lastParticleSpawnTime = 0;
const gravity = 0.05;
// Функция выборки цвета из изображения по мировым координатам
function sampleColorAtWorld(x, y) {
const u = uMin + ((x + shortSide / 2) / shortSide) * (uMax - uMin);
const v = vMin + ((y + shortSide / 2) / shortSide) * (vMax - vMin);
const imgX = Math.floor(u * texture.image.width);
const imgY = Math.floor((1 - v) * texture.image.height);
const pixel = offscreenCtx.getImageData(imgX, imgY, 1, 1).data;
return { r: pixel[0], g: pixel[1], b: pixel[2], a: pixel[3] / 255 };
}
// Функция создания частицы в виде лепестка с случайным размером
function spawnParticle(position) {
const colorSample = sampleColorAtWorld(position.x, position.y);
if (colorSample.r > 250 && colorSample.g > 250 && colorSample.b > 250) {
return;
}
const color = new THREE.Color(colorSample.r / 255, colorSample.g / 255, colorSample.b / 255);
const randomSizeFactor = minPetalFactor + Math.random() * (maxPetalFactor - minPetalFactor);
const petalScale = (cellSize / 6 * randomSizeFactor) / 30;
const petalGeometry = createPetalGeometry(petalScale);
const particleMaterial = new THREE.MeshBasicMaterial({ color: color, transparent: true });
const particleMesh = new THREE.Mesh(petalGeometry, particleMaterial);
particleMesh.position.set(position.x, position.y, 1);
particleMesh.rotation.z = Math.random() * Math.PI * 2;
scene.add(particleMesh);
const angle = Math.random() * Math.PI * 2;
const speed = Math.random() * 2 + 1;
const velocity = new THREE.Vector2(Math.cos(angle) * speed, Math.sin(angle) * speed);
const particle = {
mesh: particleMesh,
velocity: velocity,
creationTime: performance.now(),
lifetime: 3000
};
particles.push(particle);
}
let prevMousePoint = new THREE.Vector2();
function animate() {
requestAnimationFrame(animate);
const currentTime = performance.now();
let progress = Math.min((currentTime - initialAnimationStart) / initialAnimationDuration, 1);
let easedProgress = easeInOutQuad(progress);
if (progress < 1) {
pieces.forEach((cell) => {
const basePos = cell.userData.basePosition;
const initPos = cell.userData.initialPosition;
cell.position.x = THREE.MathUtils.lerp(initPos.x, basePos.x, easedProgress);
cell.position.y = THREE.MathUtils.lerp(initPos.y, basePos.y, easedProgress);
let s = THREE.MathUtils.lerp(0, cell.userData.baseScale, easedProgress);
cell.scale.set(s, s, s);
cell.userData.currentScale = s;
});
} else {
if (!mouseActive) { mouse.set(2, 2); }
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, camera);
const planeZ = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);
const intersectPoint = new THREE.Vector3();
raycaster.ray.intersectPlane(planeZ, intersectPoint);
const mousePoint = new THREE.Vector2(intersectPoint.x, intersectPoint.y);
const movement = mousePoint.distanceTo(prevMousePoint);
if (movement > 0.1 && performance.now() - lastParticleSpawnTime > 100) {
for (let k = 0; k < particlesPerSpawn; k++) {
spawnParticle(mousePoint);
}
lastParticleSpawnTime = performance.now();
}
prevMousePoint.copy(mousePoint);
pieces.forEach((cell) => {
const basePos = cell.userData.basePosition;
const distance = basePos.distanceTo(mousePoint);
let targetScale = baseScale;
if (distance < influenceRadius) {
targetScale = baseScale + maxScaleDelta * (1 - distance / influenceRadius);
}
cell.userData.currentScale += inertiaSpeed * (targetScale - cell.userData.currentScale);
let targetOffset = new THREE.Vector2(0, 0);
if (distance < influenceRadius) {
const dir = new THREE.Vector2().subVectors(basePos, mousePoint).normalize();
const offsetMag = (cellSize / 2) * (distance / influenceRadius);
targetOffset = dir.multiplyScalar(offsetMag);
}
cell.userData.currentOffset.lerp(targetOffset, offsetInertiaSpeed);
const dx = basePos.x - mousePoint.x;
const factor = Math.min(distance / influenceRadius, 1);
const targetRotation = maxRotation * factor * (dx >= 0 ? 1 : -1);
cell.userData.currentRotation += inertiaSpeed * (targetRotation - cell.userData.currentRotation);
cell.position.x = basePos.x + cell.userData.currentOffset.x;
cell.position.y = basePos.y + cell.userData.currentOffset.y;
cell.scale.set(cell.userData.currentScale, cell.userData.currentScale, cell.userData.currentScale);
cell.rotation.z = cell.userData.currentRotation;
cell.material.uniforms.uScale.value = cell.userData.currentScale;
});
}
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
let elapsed = performance.now() - p.creationTime;
let t = elapsed / p.lifetime;
let scaleFactor = 1 - t;
p.mesh.scale.set(scaleFactor, scaleFactor, scaleFactor);
p.velocity.y -= gravity;
p.mesh.position.x += p.velocity.x;
p.mesh.position.y += p.velocity.y;
if (elapsed > p.lifetime) {
scene.remove(p.mesh);
particles.splice(i, 1);
}
}
renderer.render(scene, camera);
}
animate();
},
undefined,
(error) => {
console.error('Ошибка загрузки текстуры:', error);
}
);
window.addEventListener('resize', () => {
renderer.setSize(container.clientWidth, container.clientHeight);
});
</script>
</body>
</html>
 

 
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Адаптивная Three.js Мозаика – эффект взлёта с двойным тапом на мобильных</title>
<style>
#three-container {
width: 90vw;
max-width: 800px;
aspect-ratio: 1 / 1;
margin: 0 auto;
background: #fff;
position: relative;
}
</style>
</head>
<body>
<div id="three-container"></div>
<!-- Подключаем Three.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
const container = document.getElementById('three-container');
// Создаем сцену
const scene = new THREE.Scene();
// Камера – ортографическая
const camera = new THREE.OrthographicCamera(-100, +100, +100, -100, 0.1, 1000);
camera.position.set(0, 0, 10);
camera.lookAt(0, 0, 0);
// Рендерер
const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setClearColor(0xffffff, 1);
renderer.setSize(container.clientWidth, container.clientHeight);
container.appendChild(renderer.domElement);
// Шейдеры для материала
const vertexShader = `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShader = `
uniform sampler2D uTexture;
uniform vec4 uRegion;
varying vec2 vUv;
void main() {
vec2 globalUV = uRegion.xy + vUv * uRegion.zw;
gl_FragColor = texture2D(uTexture, globalUV);
}
`;
// Параметры мозаики и анимации
const gridCount = 20;
const baseScale = 0.1;
const maxScaleDelta = 1.2;
const influenceRadius = 170;
const maxOffset = 20;
const maxRotation = 0.3;
const inertiaSpeed = 0.1;
const offsetInertiaSpeed = 0.05;
// Загружаем изображение
const textureLoader = new THREE.TextureLoader();
textureLoader.load(
'https://i.postimg.cc/MTRjMdNC/Frame-72.png',
(texture) => {
texture.wrapS = THREE.ClampToEdgeWrapping;
texture.wrapT = THREE.ClampToEdgeWrapping;
texture.minFilter = THREE.LinearFilter;
texture.magFilter = THREE.LinearFilter;
const imgWidth = texture.image.width;
const imgHeight = texture.image.height;
const shortSide = Math.min(imgWidth, imgHeight);
let uMin = 0, uMax = 1, vMin = 0, vMax = 1;
if (imgWidth > imgHeight) {
const offset = (imgWidth - shortSide) / 2;
uMin = offset / imgWidth;
uMax = (offset + shortSide) / imgWidth;
} else if (imgHeight > imgWidth) {
const offset = (imgHeight - shortSide) / 2;
vMin = offset / imgHeight;
vMax = (offset + shortSide) / imgHeight;
}
camera.left = -shortSide / 2;
camera.right = shortSide / 2;
camera.top = shortSide / 2;
camera.bottom = -shortSide / 2;
camera.near = -1000;
camera.far = 1000;
camera.updateProjectionMatrix();
// Создаем мозаичные квадраты
const pieces = [];
const cellSize = shortSide / gridCount;
for (let i = 0; i < gridCount; i++) {
for (let j = 0; j < gridCount; j++) {
const uRange = uMax - uMin;
const vRange = vMax - vMin;
const uLeft = uMin + uRange * (j / gridCount);
const uRight = uMin + uRange * ((j + 1) / gridCount);
const vTop = vMax - vRange * (i / gridCount);
const vBottom = vMax - vRange * ((i + 1) / gridCount);
const region = new THREE.Vector4(uLeft, vBottom, uRight - uLeft, vTop - vBottom);
const material = new THREE.ShaderMaterial({
uniforms: {
uTexture: { value: texture },
uRegion: { value: region }
},
vertexShader: vertexShader,
fragmentShader: fragmentShader,
transparent: true
});
const geometry = new THREE.PlaneGeometry(cellSize, cellSize);
const cellMesh = new THREE.Mesh(geometry, material);
const baseX = j * cellSize - shortSide / 2 + cellSize / 2;
const baseY = -(i * cellSize - shortSide / 2 + cellSize / 2);
cellMesh.userData.basePosition = new THREE.Vector2(baseX, baseY);
cellMesh.userData.currentOffset = new THREE.Vector2(0, 0);
cellMesh.userData.currentScale = baseScale;
cellMesh.userData.baseScale = baseScale;
cellMesh.userData.currentRotation = 0;
// Свойство для эффекта взрыва
cellMesh.userData.explosionOffset = new THREE.Vector2(0, 0);
cellMesh.position.set(baseX, baseY, 0);
cellMesh.scale.set(baseScale, baseScale, baseScale);
cellMesh.rotation.z = 0;
scene.add(cellMesh);
pieces.push(cellMesh);
}
}
const mouse = new THREE.Vector2();
const raycaster = new THREE.Raycaster();
// Обновление координат указателя для десктопа
window.addEventListener('mousemove', (event) => {
const rect = container.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / container.clientWidth) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / container.clientHeight) * 2 + 1;
});
// Функция для определения мобильного устройства
function isMobile() {
return 'ontouchstart' in window;
}
// На мобильных устройствах обновляем позицию через touch-события
if (isMobile()) {
container.addEventListener('touchmove', (event) => {
const touch = event.touches[0];
const rect = container.getBoundingClientRect();
mouse.x = ((touch.clientX - rect.left) / container.clientWidth) * 2 - 1;
mouse.y = -((touch.clientY - rect.top) / container.clientHeight) * 2 + 1;
});
container.addEventListener('touchstart', (event) => {
const touch = event.touches[0];
const rect = container.getBoundingClientRect();
mouse.x = ((touch.clientX - rect.left) / container.clientWidth) * 2 - 1;
mouse.y = -((touch.clientY - rect.top) / container.clientHeight) * 2 + 1;
});
}
// Обработчик для эффекта взрыва: для мобильных – по двойному тапу, для десктопа – по клику
if (isMobile()) {
let lastTapTime = 0;
container.addEventListener('touchend', (event) => {
const currentTime = new Date().getTime();
const tapLength = currentTime - lastTapTime;
if (tapLength < 300 && tapLength > 0) {
const touch = event.changedTouches[0];
const rect = container.getBoundingClientRect();
const touchPoint = new THREE.Vector2();
touchPoint.x = ((touch.clientX - rect.left) / container.clientWidth) * 2 - 1;
touchPoint.y = -((touch.clientY - rect.top) / container.clientHeight) * 2 + 1;
raycaster.setFromCamera(touchPoint, camera);
const planeZ = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);
const clickPoint = new THREE.Vector3();
raycaster.ray.intersectPlane(planeZ, clickPoint);
const clickVec2 = new THREE.Vector2(clickPoint.x, clickPoint.y);
pieces.forEach(cell => {
let direction = new THREE.Vector2().subVectors(cell.userData.basePosition, clickVec2);
if (direction.length() === 0) {
direction = new THREE.Vector2(Math.random() - 0.5, Math.random() - 0.5);
}
direction.normalize();
const force = (Math.random() * 40 + 20) * 2;
cell.userData.explosionOffset.copy(direction.multiplyScalar(force));
});
}
lastTapTime = currentTime;
});
} else {
container.addEventListener('click', (event) => {
const rect = container.getBoundingClientRect();
const mouseClick = new THREE.Vector2();
mouseClick.x = ((event.clientX - rect.left) / container.clientWidth) * 2 - 1;
mouseClick.y = -((event.clientY - rect.top) / container.clientHeight) * 2 + 1;
raycaster.setFromCamera(mouseClick, camera);
const planeZ = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);
const clickPoint = new THREE.Vector3();
raycaster.ray.intersectPlane(planeZ, clickPoint);
const clickVec2 = new THREE.Vector2(clickPoint.x, clickPoint.y);
pieces.forEach(cell => {
let direction = new THREE.Vector2().subVectors(cell.userData.basePosition, clickVec2);
if (direction.length() === 0) {
direction = new THREE.Vector2(Math.random() - 0.5, Math.random() - 0.5);
}
direction.normalize();
const force = (Math.random() * 40 + 20) * 2;
cell.userData.explosionOffset.copy(direction.multiplyScalar(force));
});
});
}
function animate() {
requestAnimationFrame(animate);
raycaster.setFromCamera(mouse, camera);
const planeZ = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);
const intersectPoint = new THREE.Vector3();
raycaster.ray.intersectPlane(planeZ, intersectPoint);
const mousePoint = new THREE.Vector2(intersectPoint.x, intersectPoint.y);
pieces.forEach((cell) => {
const basePos = cell.userData.basePosition;
const distance = basePos.distanceTo(mousePoint);
let targetScale = baseScale;
if (distance < influenceRadius) {
targetScale = baseScale + maxScaleDelta * (1 - distance / influenceRadius);
}
cell.userData.currentScale += inertiaSpeed * (targetScale - cell.userData.currentScale);
let targetOffset = new THREE.Vector2(0, 0);
if (distance < influenceRadius) {
const dir = new THREE.Vector2().subVectors(basePos, mousePoint).normalize();
const offsetMag = maxOffset * (distance / influenceRadius);
targetOffset = dir.multiplyScalar(offsetMag);
} else {
const dir = new THREE.Vector2().subVectors(basePos, mousePoint).normalize();
targetOffset = dir.multiplyScalar(maxOffset);
}
cell.userData.currentOffset.lerp(targetOffset, offsetInertiaSpeed);
const dx = cell.userData.basePosition.x - mousePoint.x;
const factor = Math.min(distance / influenceRadius, 1);
const targetRotation = maxRotation * factor * (dx >= 0 ? 1 : -1);
cell.userData.currentRotation += inertiaSpeed * (targetRotation - cell.userData.currentRotation);
cell.position.x = basePos.x + cell.userData.currentOffset.x + cell.userData.explosionOffset.x;
cell.position.y = basePos.y + cell.userData.currentOffset.y + cell.userData.explosionOffset.y;
cell.scale.set(cell.userData.currentScale, cell.userData.currentScale, cell.userData.currentScale);
cell.rotation.z = cell.userData.currentRotation;
cell.userData.explosionOffset.lerp(new THREE.Vector2(0, 0), 0.02);
});
renderer.render(scene, camera);
}
animate();
},
undefined,
(error) => {
console.error('Ошибка загрузки текстуры:', error);
}
);
window.addEventListener('resize', () => {
renderer.setSize(container.clientWidth, container.clientHeight);
});
</script>
</body>
</html>
 

 
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>2D Цветок - Розовая Хризантема</title>
<style>
body, #three-container {
margin: 0;
padding: 0;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
background: #000;
}
#three-container {
width: 90vw;
max-width: 1200px;
aspect-ratio: 1/1;
margin: 0 auto;
background: #000;
position: relative;
}
</style>
</head>
<body>
<div id="three-container"></div>
<!-- Подключаем Three.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
const container = document.getElementById("three-container");
let width = container.clientWidth;
let height = container.clientHeight;
// Создаем сцену и камеру
const scene = new THREE.Scene();
scene.background = null;
const camera = new THREE.OrthographicCamera(
width / -2, width / 2,
height / 2, height / -2,
-1000, 1000
);
camera.position.z = 10;
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true,
sortObjects: true,
premultipliedAlpha: false
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(width, height);
container.appendChild(renderer.domElement);
// Фоновый свет
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
scene.add(ambientLight);
// Свет от курсора – слегка красный
const cursorLight = new THREE.PointLight(0xff6666, 2, 300);
cursorLight.position.set(0, 0, 50);
scene.add(cursorLight);
// Контейнер для цветка
const flowerGroup = new THREE.Group();
scene.add(flowerGroup);
flowerGroup.scale.set(1.5, 1.5, 1.5);
// Группа для "мушек" вокруг курсора
const flyGroup = new THREE.Group();
scene.add(flyGroup);
const numFlies = 5;
const flies = [];
for (let i = 0; i < numFlies; i++) {
const flyGeo = new THREE.SphereGeometry(0.67, 8, 8);
const flyMat = new THREE.MeshBasicMaterial({ color: 0xff0000 });
const fly = new THREE.Mesh(flyGeo, flyMat);
fly.userData.phase = Math.random() * Math.PI * 2;
fly.userData.speed = 0.001 + Math.random() * 0.002;
fly.userData.radius = 30 + Math.random() * 20;
fly.userData.colorSpeed = 0.001 + Math.random() * 0.001;
fly.userData.colorPhase = Math.random() * Math.PI * 2;
flyGroup.add(fly);
flies.push(fly);
}
// Функция создания формы лепестка
function createPetalShape(w, length) {
const shape = new THREE.Shape();
shape.moveTo(0, 0);
shape.quadraticCurveTo(10, 15, 0, 30);
shape.quadraticCurveTo(-10, 15, 0, 0);
return shape;
}
function createPetalGradientTexture() {
const canvas = document.createElement("canvas");
canvas.width = 64;
canvas.height = 256;
const ctx = canvas.getContext("2d");
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
gradient.addColorStop(0, "#B22222");
gradient.addColorStop(0.33, "#C85050");
gradient.addColorStop(0.66, "#E9967A");
gradient.addColorStop(1, "#F7B7C1");
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
return new THREE.CanvasTexture(canvas);
}
function createUniformPetalGradientTexture() {
const canvas = document.createElement("canvas");
canvas.width = 64;
canvas.height = 256;
const ctx = canvas.getContext("2d");
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
gradient.addColorStop(0, "#B22222");
gradient.addColorStop(0.33, "#C85050");
gradient.addColorStop(0.66, "#E9967A");
gradient.addColorStop(1, "#F7B7C1");
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
return new THREE.CanvasTexture(canvas);
}
function createThirdLayerPetalGradientTexture() {
const canvas = document.createElement("canvas");
canvas.width = 64;
canvas.height = 256;
const ctx = canvas.getContext("2d");
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
gradient.addColorStop(0, "#8B0000");
gradient.addColorStop(0.33, "#993333");
gradient.addColorStop(0.66, "#7A1F1F");
gradient.addColorStop(1, "#660000");
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
return new THREE.CanvasTexture(canvas);
}
function createDarkPetalGradientTexture() {
const canvas = document.createElement("canvas");
canvas.width = 64;
canvas.height = 256;
const ctx = canvas.getContext("2d");
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
gradient.addColorStop(0, "#d8a0a5");
gradient.addColorStop(0.3, "#b86a75");
gradient.addColorStop(0.6, "#8e4a52");
gradient.addColorStop(1, "#4c2c2f");
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
return new THREE.CanvasTexture(canvas);
}
const petalTexture = createPetalGradientTexture();
const darkPetalTexture = createDarkPetalGradientTexture();
const thirdLayerPetalTexture = createThirdLayerPetalGradientTexture();
const petalTextureUniform = createUniformPetalGradientTexture();
const petalMaterial1 = new THREE.MeshLambertMaterial({
map: petalTexture,
side: THREE.DoubleSide,
transparent: true,
depthTest: true,
depthWrite: true
});
const petalMaterial2 = new THREE.MeshLambertMaterial({
map: petalTextureUniform,
side: THREE.DoubleSide,
color: 0xeeeeee,
transparent: true,
depthTest: true,
depthWrite: true 
});
const petalMaterial3 = new THREE.MeshLambertMaterial({
map: thirdLayerPetalTexture,
side: THREE.DoubleSide,
color: 0xdddddd,
transparent: true,
depthTest: true,
depthWrite: true 
});
const petalMaterial4 = new THREE.MeshLambertMaterial({
map: darkPetalTexture,
side: THREE.DoubleSide,
transparent: true,
depthTest: true,
depthWrite: true
});
const layer3Group = new THREE.Group();
const layer2Group = new THREE.Group();
const layer1Group = new THREE.Group();
const layer4Group = new THREE.Group();
flowerGroup.add(layer3Group);
flowerGroup.add(layer2Group);
flowerGroup.add(layer1Group);
flowerGroup.add(layer4Group);
function setRandomBaseScale(petal, minScale = 0.9, maxScale = 1.1) {
petal.userData.baseScale = new THREE.Vector3(
minScale + Math.random() * (maxScale - minScale),
minScale + Math.random() * (maxScale - minScale),
1
);
}
function setMinimalBaseScale(petal, minScale = 0.97, maxScale = 1.04) {
petal.userData.baseScale = new THREE.Vector3(
minScale + Math.random() * (maxScale - minScale),
minScale + Math.random() * (maxScale - minScale),
1
);
}
const numPetalsLayer1 = 10;
const petalWidth1_val = 30;
const petalLength1_val = 40;
const petalGeometry1 = new THREE.ShapeGeometry(createPetalShape(petalWidth1_val, petalLength1_val));
petalGeometry1.computeBoundingBox();
const bbox1 = petalGeometry1.boundingBox;
const uvAttr1 = petalGeometry1.attributes.uv;
for (let i = 0; i < uvAttr1.count; i++) {
const uv = new THREE.Vector2().fromBufferAttribute(uvAttr1, i);
uv.y = (uv.y - bbox1.min.y) / (bbox1.max.y - bbox1.min.y);
uvAttr1.setXY(i, uv.x, uv.y);
}
const petalsLayer1 = [];
const baseZ = 6;
const deltaZ = 0.3;
for (let i = 0; i < numPetalsLayer1; i++) {
const petal = new THREE.Mesh(petalGeometry1, petalMaterial1.clone());
const angle = (i / numPetalsLayer1) * Math.PI * 2;
petal.userData.baseRotation = angle;
petal.rotation.z = angle;
petal.userData.progress = 0.3;
petal.userData.minProgress = 0.3;
petal.scale.set(0.001, 0.001, 0.001);
petal.userData.speed = 0.85 + Math.random() * 0.3;
petal.userData.phase = Math.random() * Math.PI * 2;
petal.userData.prevRotationOffset = 0;
petal.userData.tilt = 0.1;
petal.renderOrder = 10 + i;
setRandomBaseScale(petal);
layer1Group.add(petal);
petalsLayer1.push(petal);
}
const numPetalsLayer2 = 16;
const petalWidth2_val = 35;
const petalLength2_val = 54;
const petalGeometry2 = new THREE.ShapeGeometry(createPetalShape(petalWidth2_val, petalLength2_val));
petalGeometry2.computeBoundingBox();
const bbox2 = petalGeometry2.boundingBox;
const uvAttr2 = petalGeometry2.attributes.uv;
for (let i = 0; i < uvAttr2.count; i++) {
const uv = new THREE.Vector2().fromBufferAttribute(uvAttr2, i);
uv.y = (uv.y - bbox2.min.y) / (bbox2.max.y - bbox2.min.y);
uvAttr2.setXY(i, uv.x, uv.y);
}
const petalsLayer2 = [];
for (let i = 0; i < numPetalsLayer2; i++) {
const petal = new THREE.Mesh(petalGeometry2, petalMaterial2.clone());
const angle = (i / numPetalsLayer2) * Math.PI * 2;
petal.userData.baseRotation = angle;
petal.rotation.z = angle;
petal.scale.set(0.001, 0.001, 0.001);
petal.position.z = -4 - i * 0.1;
petal.userData.speed = 0.65 + Math.random() * 0.2;
petal.userData.progress = 0.3;
petal.userData.phase = Math.random() * Math.PI * 2;
petal.userData.prevRotationOffset = 0;
petal.userData.tilt = 0.12;
petal.renderOrder = 5 + i;
setMinimalBaseScale(petal);
layer2Group.add(petal);
petalsLayer2.push(petal);
}
const numPetalsLayer3 = 24;
const petalWidth3_val = 40;
const petalLength3_val = 60;
const petalGeometry3 = new THREE.ShapeGeometry(createPetalShape(petalWidth3_val, petalLength3_val));
petalGeometry3.computeBoundingBox();
const bbox3 = petalGeometry3.boundingBox;
const uvAttr3 = petalGeometry3.attributes.uv;
for (let i = 0; i < uvAttr3.count; i++) {
const uv = new THREE.Vector2().fromBufferAttribute(uvAttr3, i);
uv.y = (uv.y - bbox3.min.y) / (bbox3.max.y - bbox3.min.y);
uvAttr3.setXY(i, uv.x, uv.y);
}
const petalsLayer3 = [];
for (let i = 0; i < numPetalsLayer3; i++) {
const petal = new THREE.Mesh(petalGeometry3, petalMaterial3.clone());
const angle = (i / numPetalsLayer3) * Math.PI * 2;
petal.userData.baseRotation = angle;
petal.rotation.z = angle;
petal.scale.set(0.001, 0.001, 0.001);
petal.position.z = -12 - i * 0.1;
petal.userData.speed = 0.55 + Math.random() * 0.2;
petal.userData.progress = 0.3;
petal.userData.phase = Math.random() * Math.PI * 2;
petal.userData.prevRotationOffset = 0;
petal.userData.tilt = 0.15;
petal.renderOrder = i;
setMinimalBaseScale(petal);
layer3Group.add(petal);
petalsLayer3.push(petal);
}
const numPetalsLayer4 = 10;
const petalWidth4_val = petalWidth1_val / 5;
const petalLength4_val = petalLength1_val / 5;
const petalGeometry4 = new THREE.ShapeGeometry(createPetalShape(petalWidth4_val, petalLength4_val));
petalGeometry4.computeBoundingBox();
const bbox4 = petalGeometry4.boundingBox;
const uvAttr4 = petalGeometry4.attributes.uv;
for (let i = 0; i < uvAttr4.count; i++) {
const uv = new THREE.Vector2().fromBufferAttribute(uvAttr4, i);
uv.y = (uv.y - bbox4.min.y) / (bbox4.max.y - bbox4.min.y);
uvAttr4.setXY(i, uv.x, uv.y);
}
const petalsLayer4 = [];
for (let i = 0; i < numPetalsLayer4; i++) {
const petal = new THREE.Mesh(petalGeometry4, petalMaterial4.clone());
const angle = (i / numPetalsLayer4) * Math.PI * 2;
petal.userData.baseRotation = angle;
petal.rotation.z = angle;
petal.userData.progress = 0.3;
petal.userData.minProgress = 0.3;
petal.scale.set(0.001, 0.001, 0.001);
petal.position.z = 10 + i * 0.1;
petal.userData.speed = 0.85 + Math.random() * 0.3;
petal.userData.phase = Math.random() * Math.PI * 2;
petal.userData.prevRotationOffset = 0;
petal.userData.tilt = 0.1;
petal.renderOrder = 20 + i;
setRandomBaseScale(petal);
layer4Group.add(petal);
petalsLayer4.push(petal);
}
function createStemTexture() {
const canvas = document.createElement("canvas");
canvas.width = 256;
canvas.height = 16;
const ctx = canvas.getContext("2d");
const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
gradient.addColorStop(0, "#445122");
gradient.addColorStop(0.3, "#3B5323");
gradient.addColorStop(1, "#000000");
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
const texture = new THREE.CanvasTexture(canvas);
texture.wrapS = THREE.ClampToEdgeWrapping;
texture.wrapT = THREE.ClampToEdgeWrapping;
texture.minFilter = THREE.LinearFilter;
texture.magFilter = THREE.LinearFilter;
texture.flipY = false;
texture.needsUpdate = true;
return texture;
}
const stemLength = 225;
const fixedBottom = new THREE.Vector3(0, -250, 0);
let stemCurve = new THREE.CatmullRomCurve3([
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(20, -stemLength/2, 0),
fixedBottom
]);
let stemGeometry = new THREE.TubeGeometry(stemCurve, 20, 2, 8, false);
const stemMaterial = new THREE.MeshLambertMaterial({
map: createStemTexture(),
side: THREE.DoubleSide
});
const stemMesh = new THREE.Mesh(stemGeometry, stemMaterial);
stemMesh.renderOrder = -1;
scene.add(stemMesh);
if (window.innerWidth >= 1200) {
flowerGroup.scale.set(2.25, 2.25, 2.25);
stemMesh.scale.set(1.5, 1.5, 1.5);
}
let cursorDistance = 1000;
let mousePos = new THREE.Vector2(0, 1000);
let targetMousePos = new THREE.Vector2(0, 1000);
container.addEventListener("mousemove", function(e) {
const rect = container.getBoundingClientRect();
const mouseX = e.clientX - rect.left - width / 2;
const mouseY = height / 2 - (e.clientY - rect.top);
targetMousePos.set(mouseX, mouseY);
});
container.addEventListener("mouseleave", function() {
targetMousePos.set(0, 1000);
});
function lerp(start, end, factor) {
return start + (end - start) * factor;
}
let lastTime = performance.now();
function animate() {
requestAnimationFrame(animate);
const currentTime = performance.now();
const dt = Math.min(currentTime - lastTime, 50);
lastTime = currentTime;
mousePos.x = lerp(mousePos.x, targetMousePos.x, dt * 0.005);
mousePos.y = lerp(mousePos.y, targetMousePos.y, dt * 0.005);
cursorDistance = Math.sqrt(mousePos.x * mousePos.x + mousePos.y * mousePos.y);
let target = 0;
if (cursorDistance <= 200) {
target = 1;
} else if (cursorDistance < 500) {
target = (500 - cursorDistance) / 300;
}
let newIntensity = 1.5;
if (cursorDistance < 100) {
newIntensity = 0.5 + 1.0 * Math.pow(cursorDistance / 100, 2);
}
cursorLight.intensity = lerp(cursorLight.intensity, newIntensity, dt * 0.005);
flies.forEach(fly => {
fly.position.x = mousePos.x + fly.userData.radius * Math.cos(currentTime * fly.userData.speed + fly.userData.phase);
fly.position.y = mousePos.y + fly.userData.radius * Math.sin(currentTime * fly.userData.speed + fly.userData.phase);
fly.position.z = 60;
const t = (Math.sin(currentTime * fly.userData.colorSpeed + fly.userData.colorPhase) + 1) / 2;
const colorRed = new THREE.Color(0xff0000);
const colorBlack = new THREE.Color(0x000000);
fly.material.color.copy(colorRed).lerp(colorBlack, t);
});
function updatePetals(petals, zBaseOffset, zStepPerPetal, fixedDepth = false) {
petals.forEach((petal, index) => {
let factor = (target > petal.userData.progress) ? dt/750 : dt/375;
petal.userData.progress += (target - petal.userData.progress) * factor * petal.userData.speed;
if (petal.userData.minProgress !== undefined) {
petal.userData.progress = Math.max(petal.userData.minProgress, petal.userData.progress);
}
petal.userData.progress = Math.max(0, Math.min(1, petal.userData.progress));
const easeProgress = 1 - Math.pow(1 - petal.userData.progress, 3);
petal.scale.set(
easeProgress * petal.userData.baseScale.x,
easeProgress * petal.userData.baseScale.y,
easeProgress * petal.userData.baseScale.z
);
const swayAmplitude = 0.04;
const naturalSway = swayAmplitude * Math.sin(currentTime * 0.0015 * petal.userData.speed + petal.userData.phase);
const maxBendAmount = 0.25;
const cursorDir = new THREE.Vector2(mousePos.x, mousePos.y).normalize();
const petalDir = new THREE.Vector2(Math.cos(petal.userData.baseRotation), Math.sin(petal.userData.baseRotation));
const perpDir = new THREE.Vector2(-petalDir.y, petalDir.x);
const bendFactor = cursorDir.dot(perpDir);
let bendAngle = bendFactor * maxBendAmount * easeProgress * Math.min(1, Math.max(0, (500 - cursorDistance) / 500));
petal.userData.prevRotationOffset = lerp(petal.userData.prevRotationOffset, bendAngle, dt * 0.01);
petal.rotation.z = petal.userData.baseRotation + naturalSway + petal.userData.prevRotationOffset;
petal.rotation.x = petal.userData.tilt;
if (fixedDepth) {
petal.position.z = (index % 2 === 1) ? zBaseOffset - deltaZ : zBaseOffset;
} else {
const angularPosition = petal.userData.baseRotation % (Math.PI * 2);
const zVariation = Math.sin(angularPosition) * 0.5;
petal.position.z = zBaseOffset + index * zStepPerPetal + zVariation;
}
});
}
updatePetals(petalsLayer1, baseZ, 0.1, true);
updatePetals(petalsLayer2, -4, 0.1);
updatePetals(petalsLayer3, -12, 0.1);
updatePetals(petalsLayer4, 10, 0.1);
flowerGroup.position.x = lerp(flowerGroup.position.x, mousePos.x * 0.15, dt * 0.005);
flowerGroup.position.y = lerp(flowerGroup.position.y, mousePos.y * 0.05, dt * 0.005);
if (cursorDistance < 500) {
const tiltAmount = 0.15 * Math.min(1, Math.max(0, (500 - cursorDistance) / 500));
flowerGroup.rotation.x = lerp(flowerGroup.rotation.x, -mousePos.y / height * tiltAmount, dt * 0.002);
flowerGroup.rotation.y = lerp(flowerGroup.rotation.y, mousePos.x / width * tiltAmount, dt * 0.002);
} else {
flowerGroup.rotation.x = lerp(flowerGroup.rotation.x, 0, dt * 0.002);
flowerGroup.rotation.y = lerp(flowerGroup.rotation.y, 0, dt * 0.002);
}
cursorLight.position.set(mousePos.x, mousePos.y, 50);
const flowerBox = new THREE.Box3().setFromObject(flowerGroup);
const stemTop = new THREE.Vector3();
flowerBox.getCenter(stemTop);
const top = stemTop;
const bottom = fixedBottom;
const mid = top.clone().lerp(bottom, 0.5).add(new THREE.Vector3(20, 0, 0));
stemCurve.points = [ top, mid, bottom ];
stemMesh.geometry.dispose();
stemMesh.geometry = new THREE.TubeGeometry(stemCurve, 20, 2, 8, false);
stemMesh.position.z = -50;
renderer.render(scene, camera);
}
animate();
window.addEventListener("resize", function() {
width = container.clientWidth;
height = container.clientHeight;
camera.left = width / -2;
camera.right = width / 2;
camera.top = height / 2;
camera.bottom = height / -2;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
});
});
</script>
</body>
</html>
 

Made on
Tilda