<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>就决定是你了,百变怪!</title>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.min.css"
/>
<style>
* {
box-sizing: border-box;
}
:root {
--size: 60;
--margin: 20;
--pokeball-size: 50;
--pokeball-white: #e6e6e6;
--pokeball-red: #f20d0d;
--beam-color: #f20d59;
}
@media (max-width: 600px) {
:root {
--size: 85;
}
}
body {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background: #d1ecfa;
}
.ditto {
--hue: 320;
--lightness: 75;
--stroke: 5;
height: calc(var(--size) * 1vmin);
width: calc(var(--size) * 1vmin);
z-index: 2;
}
.ditto > g {
transform: translate(200px, 200px);
}
.ditto g {
fill: hsla(
var(--hue),
100%,
calc(var(--lightness) * 1%),
var(--alpha, 1)
);
stroke: #000;
stroke-width: calc(var(--stroke) * 1px);
transition: d 0.15s ease;
}
.ditto path {
transition: d 0.15s ease;
}
.ditto__real {
opacity: 0;
}
.ditto__body {
--lightness: 45;
--stroke: 0;
}
.ditto__clone {
--lightness: 80;
--stroke: 0;
}
.ditto__stroke {
--hue: 0;
--alpha: 0;
--stroke: 5;
--lightness: 0;
fill: none;
}
.ditto__outline {
--hue: 340;
--lightness: 50;
--stroke: 0;
}
.pokeball {
--level: 50;
height: 100%;
width: 100%;
border-radius: 50%;
border: 2px solid #000;
position: relative;
overflow: hidden;
transition: all 0.15s ease;
cursor: pointer;
background: var(--pokeball-white);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
outline: transparent;
}
.pokeball:after {
content: '';
position: absolute;
top: 25%;
left: 75%;
background: rgba(255, 255, 255, 0.5);
border-radius: 50%;
height: 10px;
width: 10px;
transform: translate(-25%, -25%) rotate(-20deg);
z-index: 2;
}
.pokeball:before {
content: '';
position: absolute;
top: 50%;
left: 50%;
height: 80%;
width: 80%;
border: 2px solid #595959;
z-index: 2;
border-radius: 50%;
transform: translate(-50%, -50%);
-webkit-clip-path: polygon(50% 50%, 100% 65%, 100% 100%, 65% 100%);
clip-path: polygon(50% 50%, 100% 65%, 100% 100%, 65% 100%);
}
.pokeball__beam {
position: absolute;
bottom: 50%;
left: 50%;
transform: translate(-50%, 0);
width: calc(var(--pokeball-size) * 0.25px);
background: var(--beam-color);
filter: blur(2px);
height: calc(
(var(--size) * 0.22vmin) + (var(--margin) * 1vmin) +
(var(--pokeball-size) * 0.5px)
);
z-index: -1;
}
.pokeball__wrapper {
transform-style: preserve-3d;
height: calc(var(--pokeball-size) * 1px);
width: calc(var(--pokeball-size) * 1px);
position: relative;
margin-top: calc(var(--margin) * 1vmin);
}
.pokeball:hover {
--level: 0;
}
.pokeball__face {
height: 100%;
background: linear-gradient(
var(--pokeball-red) calc(50% - 1px),
#000 calc(50% - 1px) calc(50% + 1px),
var(--pokeball-white) calc(50% + 1px)
);
width: 100%;
position: absolute;
top: calc(var(--level) * 1%);
left: 0;
transition: all 0.15s ease;
transform: translate(0, -50%);
}
.pokeball__face:after {
content: '';
height: 5px;
width: 5px;
border: 2px solid #000;
background: #fff;
border-radius: 50%;
position: absolute;
top: calc(var(--level) * 1%);
left: 50%;
transform: translate(-50%, -50%);
}
.pokeball__face:before {
content: '';
height: 12px;
width: 12px;
border: 2px solid #000;
border-radius: 50%;
background: #bfbfbf;
position: absolute;
top: calc(var(--level) * 1%);
left: 50%;
transition: all 0.15s ease;
transform: translate(-50%, -50%);
}
</style>
</head>
<body>
<svg class="ditto" viewBox="0 0 400 400">
<defs>
<path class="ditto__path" id="ditto__path"></path>
<clipPath id="ditto__clip">
<use xlink:href="#ditto__path"></use>
</clipPath>
</defs>
<g class="ditto__real">
<g class="ditto__body">
<use xlink:href="#ditto__path"></use>
</g>
<g class="ditto__clone" clip-path="url(#ditto__clip)">
<use xlink:href="#ditto__path" transform="translate(0, -20)"></use>
</g>
<g class="ditto__stroke">
<use xlink:href="#ditto__path"></use>
</g>
<g class="ditto__face">
<g class="ditto__eyes">
<circle
class="ditto__eye"
cx="-35"
cy="-65"
r="5"
fill="#000"
stroke="none"
></circle>
<circle
class="ditto__eye"
cx="35"
cy="-60"
r="5"
fill="#000"
stroke="none"
></circle>
</g>
<path
class="ditto__mouth"
d="M -40 -35 q -20 -0 80 0"
stroke-width="4"
stroke="#000"
fill="none"
stroke-linecap="round"
></path>
</g>
</g>
<g class="ditto__outline">
<use xlink:href="#ditto__path"></use>
</g>
</svg>
<div class="pokeball__wrapper">
<span class="pokeball__beam"></span>
<button class="pokeball" title="Change Ditto">
<span class="pokeball__face"></span>
</button>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.2.6/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.16.0/d3.min.js"></script>
<script>
const {
gsap: { to, timeline, set },
d3: { curveBasisClosed, curveStep, lineRadial },
} = window;
const DITTO = document.querySelector('.ditto');
const DITTO_PATH = document.querySelector('.ditto__path');
const DITTO_MOUTH = document.querySelector('.ditto__mouth');
const DITTO_BEAM = document.querySelector('.ditto__outline');
const POKEBALL_BEAM = document.querySelector('.pokeball__beam');
const DITTO_REAL = document.querySelector('.ditto__real');
const BUTTON = document.querySelector('button');
const CURVES = [curveBasisClosed, curveStep];
const DITTO_MOUTHS = [
'M -40 -42.5 q 40 15 80 0',
'M -40 -35 q -20 -0 80 0',
];
const DEFAULT_POINTS = [
[0, 100],
[15, 120],
[30, 130],
[50, 95],
[70, 140],
[80, 150],
[90, 130],
[100, 90],
[120, 160],
[130, 170],
[140, 160],
[145, 130],
[180, 150],
[215, 130],
[220, 160],
[230, 180],
[270, 80],
[280, 160],
[300, 190],
[315, 80],
[325, 140],
[345, 160],
];
const SQUARE_POINTS = [
[45, 150],
[135, 150],
[225, 150],
[315, 150],
];
const SWAY = {
X: 10,
Y: 40,
};
const randomInRange = (min, max) =>
Math.floor(Math.random() * (max - min + 1)) + min;
const getPoints = () => {
if (Math.random() > 0.75) return SQUARE_POINTS;
const POINTS = DEFAULT_POINTS.map((point) => [
randomInRange(point[0] - SWAY.X, point[0] + SWAY.X),
randomInRange(point[1] - SWAY.Y, point[1] + SWAY.Y),
]);
return POINTS;
};
const drawDitto = (
points = DEFAULT_POINTS,
curveBasis = curveBasisClosed,
hue = 320
) => {
const PATH = lineRadial().curve(curveBasis)(
points.map((point) => [point[0] * (Math.PI / 180), point[1]])
);
DITTO.style.setProperty('--hue', hue);
DITTO_PATH.setAttribute(
'd',
PATH.charAt(PATH.length).toLowerCase() !== 'z' ? `${PATH}z` : PATH
);
};
drawDitto();
const STATE = {
ACTIVE: false,
DITTO_OUT: false,
RAN: false,
};
const CONFIG = {
POKEBALL_SPEED: 0.15,
};
set(DITTO_BEAM, { transformOrigin: '50% 50%', scale: 1.05, opacity: 0 });
set(POKEBALL_BEAM, { transformOrigin: '50% 100%', scaleY: 0 });
const onComplete = () => {
BUTTON.removeAttribute('style');
STATE.DITTO_OUT = !STATE.DITTO_OUT;
STATE.ACTIVE = false;
};
const onStart = () => {
STATE.ACTIVE = true;
};
BUTTON.addEventListener('click', () => {
if (STATE.ACTIVE) return;
if (STATE.RAN && !STATE.DITTO_OUT) {
drawDitto(
Math.random() > 0.5 ? DEFAULT_POINTS : getPoints(),
Math.random() > 0.75 ? CURVES[1] : CURVES[0],
Math.random() > 0.75 ? 180 : 320
);
}
if (!STATE.RAN) STATE.RAN = true;
set(BUTTON, {
'--level': 0,
transformOrigin: '50% 100%',
rotateX: -20,
});
if (STATE.DITTO_OUT) {
new timeline({
onStart,
onComplete,
})
.set(POKEBALL_BEAM, { transformOrigin: '50% 100%', opacity: 1 })
.to(DITTO_BEAM, { duration: CONFIG.POKEBALL_SPEED, opacity: 1 }, 0)
.to(
POKEBALL_BEAM,
{ duration: CONFIG.POKEBALL_SPEED, scaleY: 1 },
0
)
.to(DITTO_REAL, { duration: CONFIG.POKEBALL_SPEED, opacity: 0 }, 0)
.to(
DITTO_BEAM,
{ duration: CONFIG.POKEBALL_SPEED, opacity: 0 },
CONFIG.POKEBALL_SPEED
)
.to(
POKEBALL_BEAM,
{ duration: CONFIG.POKEBALL_SPEED, scaleY: 0 },
CONFIG.POKEBALL_SPEED
);
} else {
new timeline({
onStart,
onComplete,
})
.to(
POKEBALL_BEAM,
{ duration: CONFIG.POKEBALL_SPEED, scaleY: 1 },
0
)
.to(DITTO_BEAM, { duration: CONFIG.POKEBALL_SPEED, opacity: 1 }, 0)
.to(
DITTO_BEAM,
{ duration: CONFIG.POKEBALL_SPEED, opacity: 0 },
CONFIG.POKEBALL_SPEED
)
.to(
DITTO_REAL,
{ duration: CONFIG.POKEBALL_SPEED, opacity: 1 },
CONFIG.POKEBALL_SPEED
)
.to(
POKEBALL_BEAM,
{
duration: CONFIG.POKEBALL_SPEED,
opacity: 0,
transformOrigin: '50% 0',
scaleY: 0,
},
CONFIG.POKEBALL_SPEED
);
}
});
const smileOrNoSmile = () => {
const MOUTH_INDEX = randomInRange(0, DITTO_MOUTHS.length - 1);
DITTO_MOUTH.setAttribute('d', DITTO_MOUTHS[MOUTH_INDEX]);
setTimeout(smileOrNoSmile, randomInRange(4000, 10000));
};
smileOrNoSmile();
const EYES = document.querySelector('.ditto__eyes');
const blink = () => {
set(EYES, { scaleY: 1 });
if (EYES.BLINK_TL) EYES.BLINK_TL.kill();
EYES.BLINK_TL = new timeline({
delay: Math.floor(Math.random() * 4) + 1,
onComplete: () => blink(EYES),
}).to(EYES, {
duration: 0.05,
transformOrigin: '50% 50%',
scaleY: 0,
yoyo: true,
repeat: 1,
});
};
blink();
</script>
</body>
</html>