Каталог
<!--Поместить в Zero-блок (HTML)-->
<style>
.pointer-none {
pointer-events: none;
}
#gn-head-wrap {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: transparent;
z-index: 10000000;
pointer-events: none;
}
#gn-head-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
pointer-events: none;
}
</style>
<div id="gn-head-wrap">
<canvas id="gn-head-canvas"></canvas>
</div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.1/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.1/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import {
GLTFLoader
} from 'three/addons/loaders/GLTFLoader.js';
const MODEL_URL =
'https://raw.githubusercontent.com/sonsam240/modelyoutube/main/model.glb';
const MATCAP_URL =
'https://static.tildacdn.com/tild6338-3761-4762-b566-613566636337/MatCap.jpg';
const SETTINGS = {
modelSize: 2.45,
cameraZ: 19,
followX: 1.25,
followY: 0.85,
followEase: 0.065,
maxRotateX: 0.95,
maxRotateY: 1.15,
blinkTimeScale: 1,
kickTimeScale: 1,
sleepTimeScale: 1,
blinkMinDelay: 2.5,
blinkMaxDelay: 3.5,
sleepStopDelay: 2000,
sleepTiltX: 0.08,
sleepTiltZ: -0.12,
sleepTiltEase: 0.035,
wakeShakeAmount: 0.11,
wakeShakeCycles: 2,
wakeShakeDuration: 0.8,
maxPixelRatio: 4
};
const container =
document.querySelector(
'#gn-head-wrap'
);
const canvas =
document.querySelector(
'#gn-head-canvas'
);
const containerParent =
container
? container.parentElement
: null;
if (
containerParent &&
getComputedStyle(containerParent).position === 'static'
) {
containerParent.style.position =
'relative';
}
const renderer =
new THREE.WebGLRenderer({
canvas,
alpha: true,
antialias: true,
powerPreference:
'high-performance'
});
renderer.outputColorSpace =
THREE.SRGBColorSpace;
renderer.setClearColor(
0xffffff,
0
);
const scene =
new THREE.Scene();
const camera =
new THREE.PerspectiveCamera(
35,
1,
0.01,
100
);
camera.position.set(
0,
0,
SETTINGS.cameraZ
);
const headRig =
new THREE.Group();
scene.add(
headRig
);
const textureLoader =
new THREE.TextureLoader();
textureLoader.setCrossOrigin(
'anonymous'
);
const matcap =
await textureLoader.loadAsync(
MATCAP_URL
);
matcap.colorSpace =
THREE.SRGBColorSpace;
matcap.anisotropy =
Math.min(
16,
renderer
.capabilities
.getMaxAnisotropy()
);
const matcapMaterial =
new THREE.MeshMatcapMaterial({
matcap,
color:
0xffffff
});
const loader =
new GLTFLoader();
loader.setCrossOrigin(
'anonymous'
);
const gltf =
await loader.loadAsync(
MODEL_URL
);
const model =
gltf.scene;
headRig.add(
model
);
const clickableMeshes =
[];
model.traverse(
child => {
if (
!child.isMesh
) {
return;
}
clickableMeshes.push(
child
);
child.material =
matcapMaterial;
child.frustumCulled =
false;
}
);
model.updateMatrixWorld(
true
);
const box =
new THREE.Box3()
.setFromObject(
model
);
const size =
new THREE.Vector3();
const center =
new THREE.Vector3();
box.getSize(
size
);
box.getCenter(
center
);
model.position.x -=
center.x;
model.position.y -=
center.y;
model.position.z -=
center.z;
const maxDimension =
Math.max(
size.x,
size.y,
size.z
);
const modelScale =
SETTINGS.modelSize
/
maxDimension;
headRig.scale.setScalar(
modelScale
);
const clock =
new THREE.Clock();
const mixer =
new THREE.AnimationMixer(
model
);
const clips =
gltf.animations || [];
const clipsByName =
new Map();
clips.forEach(
clip => {
clipsByName.set(
clip.name,
clip
);
}
);
console.log(
'[GN HEAD] animations:',
clips.map(
clip => clip.name
)
);
const CLIP = {
BLINK:
'blinking',
LEFT_KICK:
'left-kick',
RIGHT_KICK:
'right-kick',
SLEEP:
'head-shake'
};
function getAction(
name
) {
const clip =
clipsByName.get(
name
);
if (
!clip
) {
console.warn(
'[GN HEAD] clip not found:',
name
);
return null;
}
return mixer.clipAction(
clip
);
}
const blinkAction =
getAction(
CLIP.BLINK
);
const leftKickAction =
getAction(
CLIP.LEFT_KICK
);
const rightKickAction =
getAction(
CLIP.RIGHT_KICK
);
const sleepAction =
getAction(
CLIP.SLEEP
);
let blinkTimer =
null;
let sleepStopTimer =
null;
let blinkPlaying =
false;
let kickPlaying =
false;
let sleepPlaying =
false;
let wakeShakePlaying =
false;
let currentKickAction =
null;
function randomBlinkDelay() {
return THREE.MathUtils.randFloat(
SETTINGS.blinkMinDelay,
SETTINGS.blinkMaxDelay
) * 1000;
}
function scheduleBlink() {
if (
document.hidden ||
sleepPlaying ||
kickPlaying ||
wakeShakePlaying
) {
return;
}
clearTimeout(
blinkTimer
);
blinkTimer =
setTimeout(
playBlink,
randomBlinkDelay()
);
}
function stopBlink() {
clearTimeout(
blinkTimer
);
blinkTimer =
null;
if (
blinkAction
) {
blinkAction.stop();
blinkAction.reset();
}
blinkPlaying =
false;
}
function playBlink() {
if (
!blinkAction ||
blinkPlaying ||
kickPlaying ||
sleepPlaying ||
wakeShakePlaying ||
document.hidden
) {
scheduleBlink();
return;
}
blinkPlaying =
true;
blinkAction.stop();
blinkAction.reset();
blinkAction.enabled =
true;
blinkAction.paused =
false;
blinkAction.timeScale =
SETTINGS.blinkTimeScale;
blinkAction.setEffectiveWeight(
1
);
blinkAction.setLoop(
THREE.LoopOnce,
1
);
blinkAction.clampWhenFinished =
false;
blinkAction.play();
const onFinished =
event => {
if (
event.action !==
blinkAction
) {
return;
}
mixer.removeEventListener(
'finished',
onFinished
);
blinkAction.stop();
blinkAction.reset();
blinkPlaying =
false;
scheduleBlink();
};
mixer.addEventListener(
'finished',
onFinished
);
}
function playActionOnce(
action,
timeScale = 1,
onFinished = null
) {
if (
!action
) {
return null;
}
action.stop();
action.reset();
action.enabled =
true;
action.paused =
false;
action.timeScale =
timeScale;
action.setEffectiveWeight(
1
);
action.setLoop(
THREE.LoopOnce,
1
);
action.clampWhenFinished =
false;
action.play();
if (
onFinished
) {
const finishedHandler =
event => {
if (
event.action !==
action
) {
return;
}
mixer.removeEventListener(
'finished',
finishedHandler
);
onFinished(
action
);
};
mixer.addEventListener(
'finished',
finishedHandler
);
}
return action;
}
let targetRotationX =
0;
let targetRotationY =
0;
let currentRotationX =
0;
let currentRotationY =
0;
let currentSleepTiltX =
0;
let currentSleepTiltZ =
0;
let targetSleepTiltX =
0;
let targetSleepTiltZ =
0;
let wakeShakeTime =
0;
let wakeShakeRotationY =
0;
function isMobileNoFollow() {
return window.innerWidth < 480;
}
function stopSleep() {
if (
!sleepPlaying
) {
return;
}
sleepPlaying =
false;
targetSleepTiltX =
0;
targetSleepTiltZ =
0;
if (
sleepAction
) {
sleepAction.stop();
sleepAction.reset();
}
}
function playWakeShake() {
stopBlink();
wakeShakePlaying =
true;
wakeShakeTime =
0;
wakeShakeRotationY =
0;
targetRotationX =
0;
targetRotationY =
0;
currentRotationX =
0;
currentRotationY =
0;
}
function wakeUpNow() {
if (
document.hidden ||
!sleepPlaying
) {
return false;
}
clearTimeout(
sleepStopTimer
);
sleepStopTimer =
null;
stopSleep();
playWakeShake();
return true;
}
window.addEventListener(
'pointermove',
event => {
if (
isMobileNoFollow()
) {
targetRotationX =
0;
targetRotationY =
0;
return;
}
if (
sleepPlaying &&
!document.hidden
) {
wakeUpNow();
return;
}
if (
wakeShakePlaying
) {
return;
}
const width =
Math.max(
1,
window.innerWidth
);
const height =
Math.max(
1,
window.visualViewport
? window.visualViewport.height
: window.innerHeight
);
const x =
event.clientX
/
width;
const y =
event.clientY
/
height;
targetRotationY =
THREE.MathUtils.clamp(
(
x
-
0.5
)
*
SETTINGS.followX,
-SETTINGS.maxRotateY,
SETTINGS.maxRotateY
);
targetRotationX =
THREE.MathUtils.clamp(
(
y
-
0.5
)
*
SETTINGS.followY,
-SETTINGS.maxRotateX,
SETTINGS.maxRotateX
);
},
{
passive:
true
}
);
function resetHeadFollow() {
targetRotationX =
0;
targetRotationY =
0;
}
window.addEventListener(
'blur',
resetHeadFollow
);
document.addEventListener(
'mouseout',
event => {
if (
!event.relatedTarget
) {
resetHeadFollow();
}
}
);
let kickIndex =
0;
function playKickAnimation() {
if (
sleepPlaying ||
wakeShakePlaying ||
document.hidden
) {
return;
}
const action =
kickIndex % 2 === 0
? leftKickAction
: rightKickAction;
kickIndex++;
if (
!action
) {
return;
}
stopBlink();
if (
currentKickAction
) {
currentKickAction.stop();
currentKickAction.reset();
}
currentKickAction =
action;
kickPlaying =
true;
playActionOnce(
action,
SETTINGS.kickTimeScale,
finishedAction => {
finishedAction.stop();
finishedAction.reset();
if (
currentKickAction ===
finishedAction
) {
currentKickAction =
null;
}
kickPlaying =
false;
scheduleBlink();
}
);
}
function startSleep() {
clearTimeout(
sleepStopTimer
);
sleepStopTimer =
null;
wakeShakePlaying =
false;
wakeShakeTime =
0;
wakeShakeRotationY =
0;
if (
!sleepAction ||
sleepPlaying
) {
return;
}
stopBlink();
if (
currentKickAction
) {
currentKickAction.stop();
currentKickAction.reset();
currentKickAction =
null;
}
kickPlaying =
false;
sleepPlaying =
true;
targetRotationX =
0;
targetRotationY =
0;
targetSleepTiltX =
SETTINGS.sleepTiltX;
targetSleepTiltZ =
SETTINGS.sleepTiltZ;
sleepAction.stop();
sleepAction.reset();
sleepAction.enabled =
true;
sleepAction.paused =
false;
sleepAction.timeScale =
SETTINGS.sleepTimeScale;
sleepAction.setEffectiveWeight(
1
);
sleepAction.setLoop(
THREE.LoopRepeat,
Infinity
);
sleepAction.clampWhenFinished =
false;
sleepAction.play();
}
document.addEventListener(
'visibilitychange',
() => {
clearTimeout(
sleepStopTimer
);
sleepStopTimer =
null;
if (
document.hidden
) {
startSleep();
}
else {
sleepStopTimer =
setTimeout(
() => {
sleepStopTimer =
null;
if (
document.hidden ||
!sleepPlaying
) {
return;
}
stopSleep();
playWakeShake();
},
SETTINGS.sleepStopDelay
);
}
}
);
const raycaster =
new THREE.Raycaster();
const pointer =
new THREE.Vector2();
window.addEventListener(
'pointerdown',
event => {
if (
sleepPlaying &&
!document.hidden
) {
wakeUpNow();
return;
}
if (
wakeShakePlaying ||
document.hidden
) {
return;
}
const rect =
canvas
.getBoundingClientRect();
if (
rect.width <= 0 ||
rect.height <= 0
) {
return;
}
pointer.x =
(
(
event.clientX
-
rect.left
)
/
rect.width
)
*
2
-
1;
pointer.y =
-(
(
event.clientY
-
rect.top
)
/
rect.height
)
*
2
+
1;
raycaster.setFromCamera(
pointer,
camera
);
const hits =
raycaster.intersectObjects(
clickableMeshes,
true
);
if (
!hits.length
) {
return;
}
playKickAnimation();
}
);
function getTildaZoomScale() {
if (
!container
) {
return 1;
}
const rect =
container.getBoundingClientRect();
const offsetWidth =
container.offsetWidth;
const offsetHeight =
container.offsetHeight;
if (
!offsetWidth ||
!offsetHeight
) {
return 1;
}
const scaleX =
rect.width
/
offsetWidth;
const scaleY =
rect.height
/
offsetHeight;
const scale =
Math.max(
scaleX,
scaleY
);
if (
!Number.isFinite(scale) ||
scale <= 0
) {
return 1;
}
return scale;
}
function resize() {
const width =
Math.max(
1,
container.clientWidth
);
const height =
Math.max(
1,
container.clientHeight
);
const tildaZoom =
getTildaZoomScale();
const devicePixelRatio =
window.devicePixelRatio || 1;
const renderPixelRatio =
Math.min(
devicePixelRatio
*
tildaZoom,
SETTINGS.maxPixelRatio
);
renderer.setPixelRatio(
renderPixelRatio
);
renderer.setSize(
width,
height,
false
);
camera.aspect =
width
/
height;
camera.updateProjectionMatrix();
if (
isMobileNoFollow()
) {
targetRotationX =
0;
targetRotationY =
0;
currentRotationX =
0;
currentRotationY =
0;
}
}
resize();
window.addEventListener(
'resize',
resize,
{
passive:
true
}
);
if (
window.visualViewport
) {
window.visualViewport.addEventListener(
'resize',
resize,
{
passive:
true
}
);
}
const resizeObserver =
new ResizeObserver(
() => {
resize();
}
);
resizeObserver.observe(
container
);
if (
isMobileNoFollow()
) {
targetRotationX =
0;
targetRotationY =
0;
currentRotationX =
0;
currentRotationY =
0;
}
if (
document.hidden
) {
startSleep();
}
else {
scheduleBlink();
}
function render() {
requestAnimationFrame(
render
);
const delta =
clock.getDelta();
mixer.update(
delta
);
if (
isMobileNoFollow()
) {
targetRotationX =
0;
targetRotationY =
0;
currentRotationX =
0;
currentRotationY =
0;
}
else {
currentRotationX +=
(
targetRotationX
-
currentRotationX
)
*
SETTINGS.followEase;
currentRotationY +=
(
targetRotationY
-
currentRotationY
)
*
SETTINGS.followEase;
}
currentSleepTiltX +=
(
targetSleepTiltX
-
currentSleepTiltX
)
*
SETTINGS.sleepTiltEase;
currentSleepTiltZ +=
(
targetSleepTiltZ
-
currentSleepTiltZ
)
*
SETTINGS.sleepTiltEase;
if (
wakeShakePlaying
) {
wakeShakeTime +=
delta;
const progress =
THREE.MathUtils.clamp(
wakeShakeTime
/
SETTINGS.wakeShakeDuration,
0,
1
);
const fade =
1 - THREE.MathUtils.smoothstep(
progress,
0,
1
);
wakeShakeRotationY =
Math.sin(
progress
*
Math.PI
*
2
*
SETTINGS.wakeShakeCycles
)
*
SETTINGS.wakeShakeAmount
*
fade;
if (
progress >= 1
) {
wakeShakePlaying =
false;
wakeShakeRotationY =
0;
scheduleBlink();
}
}
headRig.rotation.x =
currentRotationX
+
currentSleepTiltX;
headRig.rotation.y =
currentRotationY
+
wakeShakeRotationY;
headRig.rotation.z =
currentSleepTiltZ;
renderer.render(
scene,
camera
);
}
render();
</script>