<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>自定义自行车</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1, viewport-fit=cover"
/>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=DM+Sans&display=swap"
/>
<style>
* {
border: 0;
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--hue: 223;
--primary1: hsl(var(--hue), 90%, 50%);
--primary2: hsl(var(--hue), 90%, 70%);
--black: hsl(0, 0%, 0%);
--white: hsl(0, 0%, 100%);
--gray1: hsl(var(--hue), 10%, 90%);
--gray2: hsl(var(--hue), 10%, 80%);
--gray3: hsl(var(--hue), 10%, 70%);
--gray4: hsl(var(--hue), 10%, 60%);
--gray5: hsl(var(--hue), 10%, 50%);
--gray6: hsl(var(--hue), 10%, 40%);
--gray7: hsl(var(--hue), 10%, 30%);
--gray8: hsl(var(--hue), 10%, 20%);
--gray9: hsl(var(--hue), 10%, 10%);
--trans-dur: 0.3s;
font-size: calc(20px + (30 - 20) * (100vw - 320px) / (3840 - 320));
}
body,
button,
input {
font: 1em/1.5 'DM Sans', sans-serif;
}
body {
background-color: var(--gray1);
color: var(--gray9);
transition: background-color var(--trans-dur), color var(--trans-dur);
}
main {
container-name: main;
container-type: inline-size;
margin: auto;
padding: 1.5em 0;
width: 100%;
max-width: 33em;
}
.bike {
display: block;
margin: 0 auto 1.5em auto;
width: 100%;
max-width: 16.5em;
height: auto;
}
.bike__group-x,
.bike__group-y,
.bike__pedals-spin,
.bike__spokes-spin,
.bike__tire {
animation-duration: 2s;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
.bike__group-x {
animation-name: bike-move;
animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1);
}
.bike__group-y {
animation-name: bike-bounce;
transform-origin: 50% 28px;
}
.bike__pedals-spin {
animation-name: bike-pedals-spin;
}
.bike__spokes-spin {
animation-name: bike-spokes-spin;
}
.bike__tire {
animation-name: bike-tire-bounce;
}
.controls {
display: grid;
gap: 1.5em;
padding: 0 0.75em;
width: 100%;
}
.controls__button,
.controls__color,
.controls__range,
.controls__switch {
cursor: pointer;
-webkit-appearance: none;
appearance: none;
-webkit-tap-highlight-color: transparent;
}
.controls__color,
.controls__range,
.controls__switch {
flex-shrink: 0;
}
.controls__button,
.controls__row {
background-color: var(--white);
border-radius: 0.5em;
padding: 0.5em 0.75em;
position: relative;
transition: background-color var(--trans-dur);
}
.controls__button {
color: var(--primary1);
width: 100%;
text-align: inherit;
transition: background-color var(--trans-dur), color var(--trans-dur);
}
.controls__button:focus-visible,
.controls__button:hover {
background-color: var(--gray2);
}
.controls__color {
background-color: transparent;
border-radius: 0.25em;
box-shadow: 0 0 0 1px var(--gray1);
width: 1.5em;
height: 1.5em;
transition: box-shadow var(--trans-dur);
}
.controls__color::-webkit-color-swatch {
border: 0;
border-radius: 0.25em;
}
.controls__color::-moz-color-swatch {
border: 0;
border-radius: 0.25em;
}
.controls__color::-webkit-color-swatch-wrapper {
padding: 0;
}
.controls__icon,
.controls__label {
margin-inline: 0 0.75em;
}
.controls__icon {
flex-shrink: 0;
width: 1.5em;
height: 1.5em;
}
.controls__label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.controls__label--sr {
position: absolute;
width: 1px;
height: 1px;
}
.controls__range {
background-color: var(--gray1);
background-image: linear-gradient(
90deg,
var(--primary1),
var(--primary1)
);
background-position: 0 0;
background-repeat: no-repeat;
border-radius: 0.125em;
cursor: grab;
flex: 1;
min-width: 0;
height: 0.25em;
transition: background-color var(--trans-dur);
-webkit-appearance: none;
appearance: none;
-webkit-tap-highlight-color: transparent;
}
.controls__range::-webkit-slider-thumb {
background-color: var(--white);
border: 0;
border-radius: 50%;
box-shadow: 0 0 0.125em hsla(var(--hue), 10%, 50%, 0.3), 0 0.25em
0.375em hsla(var(--hue), 10%, 50%, 0.3);
width: 1.5em;
height: 1.5em;
transition: transform var(--trans-dur);
-webkit-appearance: none;
appearance: none;
}
.controls__range::-webkit-slider-thumb:active {
cursor: grabbing;
transform: scale(0.9);
}
.controls__range::-moz-range-thumb {
background-color: var(--white);
border: 0;
border-radius: 50%;
box-shadow: 0 0 0.125em hsla(var(--hue), 10%, 50%, 0.3), 0 0.25em
0.375em hsla(var(--hue), 10%, 50%, 0.3);
width: 1.5em;
height: 1.5em;
transition: transform var(--trans-dur);
}
.controls__range::-moz-range-thumb:active {
cursor: grabbing;
transform: scale(0.9);
}
.controls__range + .controls__icon {
margin-inline: 0.75em 0;
}
.controls__row {
display: flex;
justify-content: space-between;
align-items: center;
min-width: 0;
padding: 0.5em 0.75em;
transition: background-color var(--trans-dur);
}
.controls__row:empty {
background-color: transparent;
}
.controls__row-group {
min-width: 0;
}
.controls__row-group-title {
color: var(--gray6);
font-size: 0.75em;
line-height: 2;
padding-inline: 0.75rem 0;
text-transform: uppercase;
transition: color var(--trans-dur);
}
.controls__row-group .controls__button,
.controls__row-group .controls__row {
border-radius: 0;
}
.controls__row-group .controls__button:first-child,
.controls__row-group .controls__row:first-child {
border-radius: 0.5em 0.5em 0 0;
}
.controls__row-group .controls__button:last-child,
.controls__row-group .controls__row:last-child {
border-radius: 0 0 0.5em 0.5em;
}
.controls__row-group .controls__button:not(:last-child):after,
.controls__row-group .controls__row:not(:last-child):after {
background-color: var(--gray2);
content: '';
display: block;
position: absolute;
margin-inline: 0.75em 0;
right: 0;
bottom: -1px;
left: 0;
height: 1px;
transition: background-color var(--trans-dur);
z-index: 1;
}
.controls__switch {
background-color: var(--gray1);
border-radius: 0.75em;
width: 2.5em;
height: 1.5em;
transition: background-color var(--trans-dur);
}
.controls__switch:before {
background-color: var(--white);
border-radius: 50%;
box-shadow: 0 0.125em 0.25em hsla(var(--hue), 10%, 50%, 0.3);
content: '';
display: block;
margin: 0.125em;
width: 1.25em;
height: 1.25em;
transition: transform var(--trans-dur), transform-origin var(--trans-dur);
transform-origin: 0 50%;
}
.controls__switch:active:before {
transform: scale(1.25, 1);
}
.controls__switch:checked {
background-color: var(--primary1);
}
.controls__switch:checked:before {
transform: translateX(1em);
transform-origin: 100% 50%;
}
.controls__switch:checked:active:before {
transform: translateX(1em) scale(1.25, 1);
}
[dir='rtl'] .controls__switch:before {
transform-origin: 100% 50%;
}
[dir='rtl'] .controls__switch:checked:before {
transform: translateX(-1em);
transform-origin: 0 50%;
}
[dir='rtl'] .controls__switch:checked:active:before {
transform: translateX(-1em) scale(1.25, 1);
}
.controls__value-wrap {
color: var(--gray6);
display: inline-flex;
gap: 0.75em;
transition: color var(--trans-dur);
}
@media (prefers-color-scheme: dark) {
body {
background-color: var(--black);
color: var(--gray1);
}
.controls__button,
.controls__row {
background-color: var(--gray9);
}
.controls__row-group .controls__button:not(:last-child):after,
.controls__row-group .controls__row:not(:last-child):after {
background-color: var(--gray7);
}
.controls__button {
color: var(--primary2);
}
.controls__range,
.controls__switch,
.controls__button:focus-visible,
.controls__button:hover {
background-color: var(--gray7);
}
.controls__color {
box-shadow: 0 0 0 1px var(--gray8);
}
.controls__row-group-title,
.controls__value-wrap {
color: var(--gray4);
}
}
@container main (min-width: 700px) {
.controls {
grid-template-columns: repeat(2, 1fr);
}
}
@keyframes bike-move {
from,
to {
transform: translate(6.5px, 1px);
}
50% {
transform: translate(7.5px, 1px);
}
}
@keyframes bike-bounce {
from,
25%,
50%,
75%,
to {
transform: translate(0, 0);
}
12.5%,
37.5%,
62.5%,
87.5% {
transform: translate(0, 0.5px);
}
}
@keyframes bike-tire-bounce {
from,
25%,
50%,
75%,
to {
transform: scale(1, 1);
}
12.5%,
37.5%,
62.5%,
87.5% {
transform: scale(1.04, 0.96);
}
}
@keyframes bike-pedals-spin {
from {
transform: rotate(0.1875turn);
}
to {
transform: rotate(4.1875turn);
}
}
@keyframes bike-spokes-spin {
from {
transform: rotate(0);
}
to {
transform: rotate(4turn);
}
}
</style>
</head>
<body>
<div id="root"></div>
<script type="module">
import React, { StrictMode, useState } from 'https://esm.sh/react';
import { createRoot } from 'https://esm.sh/react-dom/client';
createRoot(document.getElementById('root')).render(
React.createElement(StrictMode, null, React.createElement(App, null))
);
function App() {
const [thickness, setThickness] = useState(1);
const [solidTires, setSolidTires] = useState(false);
const [color1, setColor1] = useState('#f2180d');
const [color2, setColor2] = useState('#737a8c');
function swapColors() {
const color1Previous = color1;
setColor1(color2);
setColor2(color1Previous);
}
return React.createElement(
'main',
null,
React.createElement(IconSprites, null),
React.createElement(Result, {
thickness: thickness,
solidTires: solidTires,
color1: color1,
color2: color2,
}),
React.createElement(
Controls,
null,
React.createElement(
RowGroup,
{ title: 'Lines' },
React.createElement(RowRange, {
id: 'thickness',
label: 'Thickness',
iconMin: 'thin',
iconMax: 'thick',
step: 0.1,
min: 0.2,
max: 2,
value: thickness,
valueChange: setThickness,
}),
React.createElement(RowSwitch, {
id: 'solid-tires',
label: 'Solid Tires',
value: solidTires,
valueChange: setSolidTires,
})
),
React.createElement(
RowGroup,
{ title: 'Colors' },
React.createElement(RowColor, {
id: 'color1',
label: 'Color 1',
value: color1,
valueChange: setColor1,
}),
React.createElement(RowColor, {
id: 'color2',
label: 'Color 2',
value: color2,
valueChange: setColor2,
}),
React.createElement(
RowButton,
{ action: swapColors },
'Swap Colors'
)
)
)
);
}
function Controls({ children }) {
return React.createElement('form', { className: 'controls' }, children);
}
function Icon({ name }) {
return React.createElement(
'svg',
{
className: 'controls__icon',
width: '24px',
height: '24px',
'aria-hidden': 'true',
},
React.createElement('use', { href: `#${name}` })
);
}
function IconSprites() {
const viewBox = '0 0 24 24';
const curvePoints = 'M 0 4 C 3 0, 6 0, 10 4 S 17 8, 20 4';
return React.createElement(
'svg',
{ display: 'none' },
React.createElement(
'symbol',
{ id: 'thin', viewBox: viewBox },
React.createElement('path', {
fill: 'none',
stroke: 'currentcolor',
strokeLinecap: 'round',
strokeWidth: '1',
d: curvePoints,
transform: 'translate(2,8)',
})
),
React.createElement(
'symbol',
{ id: 'thick', viewBox: viewBox },
React.createElement('path', {
fill: 'none',
stroke: 'currentcolor',
strokeLinecap: 'round',
strokeWidth: '4',
d: curvePoints,
transform: 'translate(2,8)',
})
)
);
}
function Result({ thickness, solidTires, color1, color2 }) {
return React.createElement(
'svg',
{
className: 'bike',
viewBox: '0 0 64 32',
width: '64px',
height: '32px',
'aria-label': 'Bicycle traveling to the right',
role: 'img',
},
React.createElement(
'g',
{
className: 'bike__group-x',
fill: 'none',
strokeLinecap: 'round',
strokeLinejoin: 'round',
strokeWidth: thickness,
transform: 'translate(7,1)',
},
React.createElement(
'g',
{ className: 'bike__group-y' },
React.createElement(
'g',
{ id: 'back-tire', transform: 'translate(9.5,19)' },
React.createElement(
'g',
{ className: 'bike__tire' },
React.createElement('circle', {
r: '9',
fill: solidTires ? color2 : 'none',
stroke: color2,
}),
React.createElement('circle', {
className: 'bike__spokes-spin',
stroke: solidTires ? '#ffffff' : color2,
strokeDasharray: '31.416 31.416',
strokeDashoffset: '23.562',
r: '5',
transform: 'rotate(90,0,0)',
})
)
),
React.createElement(
'g',
{ id: 'pedals', transform: 'translate(22,19)' },
React.createElement(
'g',
{
className: 'bike__pedals-spin',
stroke: color2,
strokeDasharray: '31.416 31.416',
strokeDashoffset: '-27.489',
transform: 'rotate(67.5,0,0)',
},
React.createElement('circle', { r: '5' }),
React.createElement('circle', {
r: '5',
transform: 'rotate(180,0,0)',
})
)
),
React.createElement(
'g',
{
id: 'front-tire',
stroke: color2,
transform: 'translate(40,19)',
},
React.createElement(
'g',
{ className: 'bike__tire' },
React.createElement('circle', {
r: '9',
fill: solidTires ? color2 : 'none',
stroke: color2,
}),
React.createElement('circle', {
className: 'bike__spokes-spin',
stroke: solidTires ? '#ffffff' : color2,
strokeDasharray: '31.416 31.416',
strokeDashoffset: '23.562',
r: '5',
transform: 'rotate(90,0,0)',
})
)
),
React.createElement(
'g',
{ id: 'body', stroke: color1 },
React.createElement('path', { d: 'm31,2h6s1,0,1,1-1,1-1,1' }),
React.createElement('polyline', { points: '33 2,40 19' }),
React.createElement('polyline', { points: '16 3,22 19' }),
React.createElement('polyline', {
points: '35.75 9,22 19,9.5 19,18 8,35 7',
}),
React.createElement('polyline', {
stroke: color2,
points: '14 3,18 3',
})
)
)
)
);
}
function RowButton({ action, children }) {
return React.createElement(
'button',
{ className: 'controls__button', type: 'button', onClick: action },
children
);
}
function RowColor({ id, label, value, valueChange }) {
return React.createElement(
'div',
{ className: 'controls__row' },
React.createElement(
'label',
{ htmlFor: id, className: 'controls__label' },
label
),
React.createElement(
'span',
{ className: 'controls__value-wrap' },
React.createElement('span', { id: `${id}-value` }, value),
React.createElement('input', {
id: id,
className: 'controls__color',
type: 'color',
value: value,
'aria-describedby': `${id}-value`,
onChange: (e) => valueChange(e.target.value),
})
)
);
}
function RowGroup({ title, children }) {
return React.createElement(
'div',
{ className: 'controls__row-group' },
React.createElement(
'div',
{ className: 'controls__row-group-title' },
title
),
React.createElement('div', null, children)
);
}
function RowRange({
id,
label,
iconMin,
iconMax,
step,
min,
max,
value,
valueChange,
}) {
const isRTL = document.dir === 'rtl';
const fillX = ((value - min) / (max - min)) * 100;
const fill = {
backgroundSize: `${fillX}% 100%`,
backgroundPosition: `${isRTL ? '100%' : '0'} 0`,
};
return React.createElement(
'div',
{ className: 'controls__row' },
React.createElement(Icon, { name: iconMin }),
React.createElement(
'label',
{ htmlFor: id, className: 'controls__label controls__label--sr' },
label
),
React.createElement('input', {
id: id,
className: 'controls__range',
type: 'range',
step: step,
min: min,
value: value,
max: max,
onChange: (e) => valueChange(+e.target.value),
style: fill,
}),
React.createElement(Icon, { name: iconMax })
);
}
function RowSwitch({ id, label, value, valueChange }) {
return React.createElement(
'div',
{ className: 'controls__row' },
React.createElement(
'label',
{ htmlFor: id, className: 'controls__label' },
label
),
React.createElement('input', {
id: id,
className: 'controls__switch',
type: 'checkbox',
checked: value,
onChange: () => valueChange(!value),
})
);
}
</script>
</body>
</html>