Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions frontend/src/app/layout/app-header.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
@keyframes app-header-glyph-wave {
0%,
100% {
transform: translate3d(0, 0, 0) rotate(0deg);
}
30% {
transform: translate3d(-0.5px, -4px, 0) rotate(-2deg);
}
58% {
transform: translate3d(0.6px, 1px, 0) rotate(1.25deg);
}
78% {
transform: translate3d(-0.2px, -0.75px, 0) rotate(-0.4deg);
}
}

.app-header-text-wave .app-header-wave-glyph {
transform-origin: center 80%;
animation: app-header-glyph-wave 380ms cubic-bezier(0.2, 0.75, 0.25, 1) both;
}

/*
* 生成中那个小机器人。方脸方眼是照产品本体的像素语汇画的——圆脸圆眼是通用
* emoji 语汇,跟"做像素角色资产"这件事没关系,排在中文字旁边像贴上去的。
*
* 动的只有瞳孔,脸不动:脸跟着晃就成了摇头,那是"不同意"不是"在张望"。
* 视线和眨眼分成两条动画,周期故意不整除(3.6s / 4.3s),合起来的节奏不重复。
* 每次移动占约 8% 周期(≈290ms)滑过去,到位后停住十几个百分点再走下一步。
*/
@keyframes app-header-bot-gaze {
0%,
22% {
transform: translate(0, 0);
}
/* 瞟左边 */
30%,
44% {
transform: translate(-2px, 0);
}
/* 扫到右边 */
52%,
64% {
transform: translate(2px, 0);
}
/* 抬头想一下 */
72%,
84% {
transform: translate(0, -2px);
}
92%,
100% {
transform: translate(0, 0);
}
}

@keyframes app-header-bot-blink {
0%,
92% {
transform: scaleY(1);
}
94.5% {
transform: scaleY(0.15);
}
97%,
100% {
transform: scaleY(1);
}
}

.app-header-bot-gaze {
animation: app-header-bot-gaze 3600ms cubic-bezier(0.5, 0, 0.15, 1) infinite;
}

.app-header-bot-blink {
/* SVG 默认绕用户坐标原点缩放,不套 fill-box 的话眨眼会把眼睛甩出脸外。 */
transform-box: fill-box;
transform-origin: center;
animation: app-header-bot-blink 4300ms cubic-bezier(0.4, 0, 0.55, 1) infinite;
}

@media (prefers-reduced-motion: reduce) {
.app-header-text-wave .app-header-wave-glyph,
.app-header-bot-gaze,
.app-header-bot-blink {
animation: none;
}
}
1 change: 1 addition & 0 deletions frontend/src/app/layout/app-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
useProductPopoverMotion,
} from '@/shared/ui'
import { PageBackButton } from './page-back-button'
import './app-header.css'

interface ProductNavigationItem {
motionKey: string
Expand Down
24 changes: 24 additions & 0 deletions frontend/src/app/layout/page-back-button.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
@keyframes app-header-back-strip {
from {
transform: translate3d(0, 0, 0);
}
to {
transform: translate3d(-26px, 0, 0);
}
}

.app-header-back-strip {
transform: translate3d(0, 0, 0);
will-change: transform;
}

.app-header-back-in-flight .app-header-back-strip {
animation: app-header-back-strip 220ms cubic-bezier(0.65, 0, 0.35, 1) both;
}

@media (prefers-reduced-motion: reduce) {
.app-header-back-in-flight .app-header-back-strip {
animation-duration: 0.01ms;
animation-delay: 0ms;
}
}
2 changes: 2 additions & 0 deletions frontend/src/app/layout/page-back-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { ArrowLeft } from '@phosphor-icons/react'
import { useEffect, useRef, useState } from 'react'
import { useLocation, useNavigate } from 'react-router'

import './page-back-button.css'

const backTransitionDurationMs = 230

function fallbackPath(pathname: string): string {
Expand Down
78 changes: 78 additions & 0 deletions frontend/src/app/route-motion.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
@keyframes route-page-enter-forward {
from {
transform: translate3d(14px, 0, 0);
}

68% {
transform: translate3d(1px, 0, 0);
}

to {
transform: translate3d(0, 0, 0);
}
}

@keyframes route-page-enter-backward {
from {
transform: translate3d(-14px, 0, 0);
}

68% {
transform: translate3d(-1px, 0, 0);
}

to {
transform: translate3d(0, 0, 0);
}
}

@keyframes route-page-enter-lateral {
from {
transform: translate3d(8px, 0, 0);
}

to {
transform: translate3d(0, 0, 0);
}
}

.route-motion-surface[data-motion-active='true']
:where(header, main, aside, footer, section[aria-label], [role='region'], h1:not(header *)) {
backface-visibility: hidden;
--route-motion-lag: 0ms;
}

.route-motion-surface[data-motion-active='true']
:where(main, section[aria-label], [role='region']) {
--route-motion-lag: 18ms;
}

.route-motion-surface[data-motion-active='true'] :where(aside, footer) {
--route-motion-lag: 34ms;
}

.route-motion-surface[data-motion-active='true'][data-motion-direction='forward']
:where(header, main, aside, footer, section[aria-label], [role='region'], h1:not(header *)) {
animation: route-page-enter-forward 222ms cubic-bezier(0.16, 1, 0.3, 1) both;
animation-delay: var(--route-motion-lag);
}

.route-motion-surface[data-motion-active='true'][data-motion-direction='backward']
:where(header, main, aside, footer, section[aria-label], [role='region'], h1:not(header *)) {
animation: route-page-enter-backward 222ms cubic-bezier(0.16, 1, 0.3, 1) both;
animation-delay: var(--route-motion-lag);
}

.route-motion-surface[data-motion-active='true'][data-motion-direction='lateral']
:where(header, main, aside, footer, section[aria-label], [role='region'], h1:not(header *)) {
animation: route-page-enter-lateral 222ms cubic-bezier(0.16, 1, 0.3, 1) both;
animation-delay: var(--route-motion-lag);
}

@media (prefers-reduced-motion: reduce) {
.route-motion-surface,
.route-motion-surface
:where(header, main, aside, footer, section[aria-label], [role='region'], h1:not(header *)) {
animation: none !important;
}
}
1 change: 1 addition & 0 deletions frontend/src/app/route-motion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useEffect, useRef, type ReactNode } from 'react'
import { useLocation } from 'react-router'

import { getRouteMotionDirection } from './route-motion-model'
import './route-motion.css'

function motionPathname(pathname: string): string {
return /^\/projects\/[^/]+$/.test(pathname) ? `${pathname}/assets` : pathname
Expand Down
Loading
Loading