agent-Specialization/static/demo_stadium_transition.html

231 lines
7.4 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>体育场形裂变动画 Demo</title>
<style>
:root {
font-family: "SF Pro Display", "PingFang SC", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #12100f;
color: #f3efe6;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 42px 16px;
background: transparent;
}
.demo-card {
width: min(880px, 95vw);
padding: 0;
background: transparent;
border-radius: 0;
box-shadow: none;
border: none;
}
.demo-card h1,
.demo-card p,
.hint {
display: none;
}
.shell-wrapper {
position: relative;
}
.input-shell {
position: relative;
width: 100%;
background: linear-gradient(180deg, #fdf7ea 0%, #f2c46e 100%);
padding: 14px 70px;
box-shadow: 0 20px 55px rgba(0, 0, 0, 0.55);
min-height: 64px;
border: 1px solid rgba(255, 255, 255, 0.35);
transition: padding 0.25s ease, box-shadow 0.3s ease, border-color 0.3s ease;
clip-path: url(#stadiumClip);
-webkit-clip-path: url(#stadiumClip);
}
.input-shell.expanded {
padding-top: 22px;
padding-bottom: 22px;
box-shadow: 0 45px 90px rgba(0, 0, 0, 0.65);
border-color: rgba(255, 255, 255, 0.55);
}
textarea {
width: 100%;
min-height: 28px;
max-height: 240px;
border: none;
resize: none;
background: transparent;
font-size: 16px;
line-height: 1.6;
font-family: inherit;
color: #1f160c;
outline: none;
overflow-y: auto;
}
textarea::-webkit-scrollbar {
width: 6px;
height: 6px;
}
textarea::-webkit-scrollbar-thumb {
background: rgba(60, 53, 40, 0.25);
border-radius: 12px;
}
svg.clip-defs {
width: 0;
height: 0;
position: absolute;
}
</style>
</head>
<body>
<div class="demo-card">
<h1>体育场形 → 圆角矩形(裂解动画示例)</h1>
<p>单行时保持典型体育场形,输入多行后,左右半圆沿水平中线裂开成四个四分之一圆,并拉伸出上下两条直边。</p>
<div class="shell-wrapper">
<div class="input-shell" id="shell">
<textarea id="demoInput" rows="1" placeholder="粘贴或输入多行内容,观察动画..."></textarea>
</div>
<svg class="clip-defs">
<clipPath id="stadiumClip" clipPathUnits="userSpaceOnUse">
<path id="stadiumPath" d="" />
</clipPath>
</svg>
</div>
<div class="hint">提示Shift+Enter 换行;窗口尺寸改变也会重新计算形状。</div>
</div>
<script>
(function () {
const textarea = document.getElementById('demoInput');
const shell = document.getElementById('shell');
const path = document.getElementById('stadiumPath');
if (!textarea || !shell || !path) {
return;
}
let animationFrame = null;
let currentProgress = 0;
let targetProgress = 0;
let baseHeight = 0;
let baseRadius = 0;
const clamp = (val, min, max) => Math.min(Math.max(val, min), max);
const ensureBase = () => {
if (baseHeight > 0) {
return;
}
baseHeight = shell.offsetHeight;
baseRadius = baseHeight / 2;
shell.dataset.baseHeight = String(baseHeight);
shell.dataset.baseRadius = String(baseRadius);
};
const buildPath = (width, height, progress) => {
if (width <= 0 || height <= 0) {
return '';
}
ensureBase();
const radius = baseRadius;
const midY = height / 2;
const targetTopCenter = radius;
const targetBottomCenter = Math.max(height - radius, radius);
const cyTop = midY - (midY - targetTopCenter) * progress;
const cyBottom = midY + (targetBottomCenter - midY) * progress;
const cxLeft = radius;
const cxRight = width - radius;
const topY = cyTop - radius;
const bottomY = cyBottom + radius;
return [
`M ${cxLeft} ${topY}`,
`H ${cxRight}`,
`A ${radius} ${radius} 0 0 1 ${width} ${cyTop}`,
`V ${cyBottom}`,
`A ${radius} ${radius} 0 0 1 ${cxRight} ${bottomY}`,
`H ${cxLeft}`,
`A ${radius} ${radius} 0 0 1 0 ${cyBottom}`,
`V ${cyTop}`,
`A ${radius} ${radius} 0 0 1 ${cxLeft} ${topY}`,
'Z'
].join(' ');
};
const animateProgress = () => {
if (Math.abs(currentProgress - targetProgress) < 0.002) {
currentProgress = targetProgress;
animationFrame = null;
} else {
currentProgress += (targetProgress - currentProgress) * 0.18;
animationFrame = requestAnimationFrame(animateProgress);
}
updatePath();
};
const updatePath = () => {
ensureBase();
const rect = shell.getBoundingClientRect();
const width = rect.width;
const height = shell.offsetHeight;
const d = buildPath(width, height, currentProgress);
path.setAttribute('d', d);
};
const resizeTextarea = () => {
ensureBase();
textarea.style.height = 'auto';
const computed = window.getComputedStyle(textarea);
const lineHeight = parseFloat(computed.lineHeight || '20') || 20;
const maxHeight = lineHeight * 6;
const nextHeight = Math.min(textarea.scrollHeight, maxHeight);
textarea.style.height = `${nextHeight}px`;
const lineCount = Math.max(1, Math.round(nextHeight / lineHeight));
targetProgress = clamp((lineCount - 1) / 4, 0, 1);
shell.classList.toggle('expanded', lineCount > 1);
if (!animationFrame) {
animationFrame = requestAnimationFrame(animateProgress);
}
};
textarea.addEventListener('input', resizeTextarea);
window.addEventListener('resize', () => {
updatePath();
});
// 初始化
textarea.value = '';
resizeTextarea();
updatePath();
})();
</script>
</body>
</html>