feat(hero): 3D-наклон фото на главной по курсору (parallax + блик)
This commit is contained in:
1 parent
b6161c92cd
commit
2b7361664e
3 files changed
+426
-18
No files matched your search
+358
-18
@@ -252,24 +252,37 @@ body {
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
opacity: var(--mesh-opacity-current, 1);
|
||||
}
|
||||
|
||||
.page-mesh-bg canvas {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
opacity: 0.4; /* приглушаем яркость живого шейдера */
|
||||
}
|
||||
|
||||
/* Вертикальный градиент-оверлэй: снизу фон закрыт цветом страницы (--bg,
|
||||
100% непрозрачности), кверху растворяется — фон становится виден на 100%.
|
||||
--bg берётся из темы, поэтому на светлой теме верх светлый, на тёмной — тёмный. */
|
||||
.page-mesh-bg::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
var(--bg) 0%,
|
||||
color-mix(in srgb, var(--bg) 65%, transparent) 35%,
|
||||
transparent 70%
|
||||
);
|
||||
}
|
||||
|
||||
html[data-mesh-bg="off"] .page-mesh-bg {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.page-mesh-bg::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: var(--page-bg);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100dvh;
|
||||
background-position: top center;
|
||||
background-blend-mode: normal;
|
||||
opacity: var(--mesh-opacity-current);
|
||||
}
|
||||
|
||||
.mesh-panel {
|
||||
position: fixed;
|
||||
left: max(0.75rem, env(safe-area-inset-left, 0px));
|
||||
@@ -1317,10 +1330,10 @@ a.fab--secondary:visited {
|
||||
}
|
||||
|
||||
.hero__eyebrow {
|
||||
margin: 0 0 0.5rem;
|
||||
margin: 0 0 0.6rem;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
color: color-mix(in srgb, var(--text) 65%, transparent);
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
@@ -2865,7 +2878,6 @@ a.btn--ghost:visited {
|
||||
position: relative;
|
||||
border-radius: var(--radius-card);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--surface);
|
||||
box-shadow: var(--shadow-strong);
|
||||
}
|
||||
@@ -2946,6 +2958,73 @@ a.btn--ghost:visited {
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* 3D-наклон портрета по курсору (initHero3DTilt).
|
||||
* perspective уже задан на .hero__visual; здесь — поворот самой карточки,
|
||||
* параллакс слоёв и мягкий блик под курсором.
|
||||
*/
|
||||
.hero__portrait-shell {
|
||||
--tilt-rx: 0deg;
|
||||
--tilt-ry: 0deg;
|
||||
--tilt-px: 50%;
|
||||
--tilt-py: 36%;
|
||||
transform: perspective(1300px) rotateX(var(--tilt-rx)) rotateY(var(--tilt-ry));
|
||||
transform-style: preserve-3d;
|
||||
transition: transform 320ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.hero__portrait-shell--tilting {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.hero__portrait-shell::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 3;
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
opacity: 0;
|
||||
transition: opacity 320ms ease;
|
||||
background: radial-gradient(
|
||||
260px circle at var(--tilt-px) var(--tilt-py),
|
||||
hsla(0, 0%, 100%, 0.35),
|
||||
hsla(0, 0%, 100%, 0) 60%
|
||||
);
|
||||
mix-blend-mode: soft-light;
|
||||
}
|
||||
|
||||
.hero__portrait-shell--tilting::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Параллакс: фоновый слой чуть глубже, motion-слой выше курсора. */
|
||||
.hero__portrait-shell--tilting .hero__portrait-layer--bg {
|
||||
transform: translateZ(-18px) scale(1.06);
|
||||
}
|
||||
|
||||
.hero__portrait-shell--tilting .hero__portrait-layer--motion {
|
||||
transform: translateZ(24px);
|
||||
}
|
||||
|
||||
.hero__portrait-layer {
|
||||
transition: transform 320ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
|
||||
.hero__portrait-shell--tilting .hero__portrait-layer {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.hero__portrait-shell {
|
||||
transform: none;
|
||||
}
|
||||
.hero__portrait-shell::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme="light"] .hero__portrait-shell::after {
|
||||
background: hsla(var(--theme-h), 25%, 24%, 0.28);
|
||||
filter: blur(12px);
|
||||
@@ -5501,7 +5580,7 @@ body.content-edit-shift-active.content-edit-mode-active .content-edit-fab-stack
|
||||
.marquee-grid__cell {
|
||||
margin: 0;
|
||||
flex: 0 0 auto;
|
||||
opacity: 0.2;
|
||||
opacity: 0.4;
|
||||
animation: tagFlicker var(--tag-cycle, 15s) ease-in-out infinite;
|
||||
}
|
||||
.marquee-grid__cell .hero__tag {
|
||||
@@ -5516,6 +5595,7 @@ body.content-edit-shift-active.content-edit-mode-active .content-edit-fab-stack
|
||||
font-size: inherit;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Flicker: теги держим на базовом уровне 20%, по очереди (по 3 за раз, вразнобой) вспыхивают до 100%.
|
||||
@@ -5540,8 +5620,8 @@ body.content-edit-shift-active.content-edit-mode-active .content-edit-fab-stack
|
||||
@keyframes tagFlicker {
|
||||
0% { opacity: 1; }
|
||||
14% { opacity: 1; }
|
||||
19% { opacity: 0.2; }
|
||||
95% { opacity: 0.2; }
|
||||
19% { opacity: 0.4; }
|
||||
95% { opacity: 0.4; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
@@ -5592,6 +5672,266 @@ body.content-edit-shift-active.content-edit-mode-active .content-edit-fab-stack
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
/* Резюме: страница /resume/ (article + section/ol/dl/time) */
|
||||
.resume-doc {
|
||||
max-width: 52rem;
|
||||
margin: 0 auto;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-card);
|
||||
padding: clamp(1.5rem, 3vw, 2.5rem);
|
||||
}
|
||||
.resume-header h1 {
|
||||
margin: 0 0 0.25rem;
|
||||
}
|
||||
/* Верхняя часть документа: flex-колонка, чтобы вертикальные margin не схлопывались */
|
||||
.resume-doc__head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
/* Список должностей под именем: обычный текст, каждая с новой строки */
|
||||
.resume-roles {
|
||||
line-height: 1.45;
|
||||
color: var(--text);
|
||||
}
|
||||
.resume-header {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.resume-photo {
|
||||
width: auto;
|
||||
height: 160px;
|
||||
max-width: 40%;
|
||||
border-radius: var(--radius-sm);
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
order: 1;
|
||||
margin-left: auto;
|
||||
}
|
||||
@media (max-width: 32rem) {
|
||||
.resume-photo {
|
||||
height: 128px;
|
||||
max-width: 55%;
|
||||
}
|
||||
}
|
||||
@media (max-width: 32rem) {
|
||||
.resume-header {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
.resume-photo {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
/* H2 — раздел документа: самый крупный уровень, акцентный шрифт. Разделение — отступом, не линией */
|
||||
.resume-section__title {
|
||||
margin: 4.5rem 0 1.5rem;
|
||||
font-family: var(--font-accent);
|
||||
font-size: clamp(1.3rem, 2.4vw, 1.6rem);
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
color: var(--text);
|
||||
letter-spacing: -0.015em;
|
||||
}
|
||||
.resume-doc .section__prose + .section__prose {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
.resume-doc .section__prose a {
|
||||
color: var(--accent);
|
||||
}
|
||||
.resume-meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1.5rem 3rem;
|
||||
margin: 1.5rem 0;
|
||||
font-size: 0.92rem;
|
||||
color: var(--text);
|
||||
}
|
||||
@media (max-width: 32rem) {
|
||||
.resume-meta {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
.resume-meta dt {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
letter-spacing: 0.03em;
|
||||
margin-bottom: 0.1rem;
|
||||
}
|
||||
.resume-meta dd {
|
||||
margin: 0;
|
||||
}
|
||||
.resume-meta a {
|
||||
color: var(--accent);
|
||||
}
|
||||
.resume-exp {
|
||||
margin: 3rem 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.resume-exp__header {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
/* H3 — компания: второй уровень. Без акцентного цвета, чтобы не мимикрировать под ссылку */
|
||||
.resume-exp__company {
|
||||
margin: 0;
|
||||
font-family: var(--font-accent);
|
||||
font-weight: 700;
|
||||
font-size: clamp(1.05rem, 1.6vw, 1.2rem);
|
||||
line-height: 1.25;
|
||||
color: var(--text);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.resume-exp__meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.resume-exp__meta-row a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid var(--accent);
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.resume-exp__meta-row a:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.resume-exp__sep {
|
||||
color: var(--line);
|
||||
}
|
||||
.resume-exp__meta-row time {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.resume-exp__period {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.resume-exp__desc {
|
||||
color: var(--text);
|
||||
font-size: 0.92rem;
|
||||
margin: 0.5rem 0;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.resume-exp__section {
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
.resume-exp__section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
/* Разделитель-отступ между проектами внутри одного места работы */
|
||||
.resume-exp__divider {
|
||||
height: 1.5rem;
|
||||
}
|
||||
.resume-exp__cols {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 10rem;
|
||||
gap: 0 1.5rem;
|
||||
}
|
||||
@media (max-width: 48rem) {
|
||||
.resume-exp__cols {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
/* H4 — продукт внутри компании: мельче H3, со сдвигом и направляющей (визуальная вложенность) */
|
||||
.resume-exp__section {
|
||||
padding-left: 0.9rem;
|
||||
border-left: 2px solid var(--line);
|
||||
}
|
||||
.resume-exp__section-title {
|
||||
margin: 0 0 0.5rem;
|
||||
font-weight: 600;
|
||||
font-size: clamp(0.88rem, 1.1vw, 0.95rem);
|
||||
line-height: 1.35;
|
||||
color: var(--text);
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
/* Метки колонок (H4/H5): тот же визуальный стиль, что у .resume-skill__title */
|
||||
.resume-exp__label {
|
||||
font-family: inherit;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: none;
|
||||
margin: 0.75rem 0 0.4rem;
|
||||
}
|
||||
/* H4-метка (без продуктов) — тот же размер: различие только по уровню заголовка */
|
||||
h4.resume-exp__label {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.resume-exp-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.resume-exp ul {
|
||||
margin: 0;
|
||||
padding-left: 1.1rem;
|
||||
color: var(--text);
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
/* Буллеты: залитая точка, цвет как у текста */
|
||||
.resume-exp ul {
|
||||
list-style: none;
|
||||
}
|
||||
.resume-exp ul li {
|
||||
position: relative;
|
||||
padding-left: 0.85rem;
|
||||
}
|
||||
.resume-exp ul li::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0.62em;
|
||||
width: 0.3rem;
|
||||
height: 0.3rem;
|
||||
border-radius: 50%;
|
||||
background: var(--text);
|
||||
}
|
||||
.resume-exp ul li + li {
|
||||
margin-top: 0.6rem;
|
||||
}
|
||||
.resume-skills {
|
||||
margin: 1rem 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
.resume-skill {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.resume-skill__title {
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
min-width: 8rem;
|
||||
flex-shrink: 0;
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 0.02em;
|
||||
margin: 0;
|
||||
}
|
||||
.resume-skill__desc {
|
||||
color: var(--text);
|
||||
margin: 0;
|
||||
}
|
||||
.resume-about {
|
||||
margin: 1.5rem 0;
|
||||
font-size: 0.92rem;
|
||||
color: var(--text);
|
||||
line-height: 1.55;
|
||||
}
|
||||
/* Слайдер изображений в лонгриде кейса: одна картинка + стрелки + точки */
|
||||
.case-media-slider {
|
||||
--slider-gap: 0;
|
||||
|
||||
@@ -17,6 +17,8 @@ import { initContactCopy } from "./modules/ui/contact-copy.js";
|
||||
import { initCookieNotice } from "./modules/ui/cookie-notice.js";
|
||||
import { initHeroWordSlot } from "./modules/ui/hero-word-slot.js";
|
||||
import { initHeroMotionVideo } from "./modules/ui/hero-motion-video.js";
|
||||
import { initHero3DTilt } from "./modules/ui/hero-3d-tilt.js";
|
||||
import { initPageMeshShader } from "./modules/ui/page-mesh-shader.js";
|
||||
import { initPageMeshBackground } from "./modules/ui/page-mesh-bg.js";
|
||||
import { initPrimaryButtonShine } from "./modules/ui/primary-button-shine.js";
|
||||
import { initProjectsCarousel } from "./modules/ui/projects-carousel.js";
|
||||
@@ -44,6 +46,8 @@ initNavOverflow();
|
||||
initCookieNotice();
|
||||
initHeroWordSlot();
|
||||
initHeroMotionVideo();
|
||||
initHero3DTilt();
|
||||
initPageMeshShader();
|
||||
initPageMeshBackground();
|
||||
initExperienceExpand(experienceMasonry);
|
||||
initMiniProjectsExpand(miniProjectsMasonry);
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// 3D-наклон портрета на главной по движению курсора.
|
||||
// Эффект "парящей карточки": карточка поворачивается к курсору,
|
||||
// слои дают параллакс, поверх ложится мягкий блик.
|
||||
|
||||
const MAX_TILT = 9; // градусов по каждой оси
|
||||
const GLIDE = 320; // ms возврата в покой
|
||||
|
||||
function isTouchOrReduced() {
|
||||
const touch = window.matchMedia("(hover: none), (pointer: coarse)").matches;
|
||||
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
return touch || reduced;
|
||||
}
|
||||
|
||||
export function initHero3DTilt() {
|
||||
const shell = document.querySelector(".hero__portrait-shell");
|
||||
if (!shell || isTouchOrReduced()) return;
|
||||
|
||||
let rafId = null;
|
||||
let targetX = 0;
|
||||
let targetY = 0;
|
||||
let currentX = 0;
|
||||
let currentY = 0;
|
||||
let active = false;
|
||||
|
||||
function onMove(e) {
|
||||
const rect = shell.getBoundingClientRect();
|
||||
const px = (e.clientX - rect.left) / rect.width; // 0..1
|
||||
const py = (e.clientY - rect.top) / rect.height; // 0..1
|
||||
targetY = (px - 0.5) * 2 * MAX_TILT; // rotateY
|
||||
targetX = (0.5 - py) * 2 * MAX_TILT; // rotateX
|
||||
shell.style.setProperty("--tilt-px", `${px * 100}%`);
|
||||
shell.style.setProperty("--tilt-py", `${py * 100}%`);
|
||||
if (!active) {
|
||||
active = true;
|
||||
shell.classList.add("hero__portrait-shell--tilting");
|
||||
}
|
||||
if (!rafId) rafId = requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
function tick() {
|
||||
currentX += (targetX - currentX) * 0.18;
|
||||
currentY += (targetY - currentY) * 0.18;
|
||||
shell.style.setProperty("--tilt-rx", `${currentX.toFixed(2)}deg`);
|
||||
shell.style.setProperty("--tilt-ry", `${currentY.toFixed(2)}deg`);
|
||||
const done =
|
||||
Math.abs(targetX - currentX) < 0.01 && Math.abs(targetY - currentX) < 0.01;
|
||||
if (done && !active) {
|
||||
rafId = null;
|
||||
} else {
|
||||
rafId = requestAnimationFrame(tick);
|
||||
}
|
||||
}
|
||||
|
||||
function onLeave() {
|
||||
active = false;
|
||||
targetX = 0;
|
||||
targetY = 0;
|
||||
shell.classList.remove("hero__portrait-shell--tilting");
|
||||
if (!rafId) rafId = requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
shell.addEventListener("mousemove", onMove);
|
||||
shell.addEventListener("mouseleave", onLeave);
|
||||
}
|
||||
Reference in new issue
Block a user