mobile_plate_generator.html 55 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
  6. <title>高精车牌在线物理渲染器 (移动端)</title>
  7. <!-- Google Fonts for premium typography -->
  8. <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@800;900&family=Oswald:wght@600;700&family=Montserrat:wght@800&display=swap" rel="stylesheet">
  9. <style>
  10. :root {
  11. --bg-color: #080c14;
  12. --panel-bg: rgba(17, 24, 39, 0.95);
  13. --panel-border: rgba(255, 255, 255, 0.08);
  14. --text-color: #f8fafc;
  15. --accent-color: #3b82f6;
  16. --accent-glow: rgba(59, 130, 246, 0.3);
  17. /* Default Plate Color Styles (Blue) */
  18. --plate-bg: linear-gradient(135deg, #0d58ca 0%, #003692 50%, #002d7c 100%);
  19. --plate-border: #ffffff;
  20. --plate-text: #ffffff;
  21. --plate-text-shadow: -0.5px -0.5px 0px rgba(255, 255, 255, 0.6),
  22. 0.8px 0.8px 0px rgba(0, 0, 0, 0.9),
  23. 1.5px 1.5px 2px rgba(0, 0, 0, 0.7);
  24. --plate-border-shadow: inset 0 2px 4px rgba(255,255,255,0.4),
  25. inset 0 -2px 4px rgba(0,0,0,0.6),
  26. 0 4px 8px rgba(0,0,0,0.5);
  27. }
  28. * {
  29. box-sizing: border-box;
  30. margin: 0;
  31. padding: 0;
  32. user-select: none;
  33. -webkit-user-select: none;
  34. -webkit-tap-highlight-color: transparent;
  35. }
  36. body {
  37. font-family: 'Noto Sans SC', sans-serif;
  38. background-color: var(--bg-color);
  39. color: var(--text-color);
  40. height: 100dvh;
  41. display: flex;
  42. flex-direction: column;
  43. overflow: hidden;
  44. background-image:
  45. radial-gradient(circle at 50% 15%, rgba(29, 78, 216, 0.15) 0%, transparent 60%);
  46. }
  47. /* 3D SHOWCASE REGION (TOP 35%) */
  48. .showcase-container {
  49. height: 35dvh;
  50. min-height: 200px;
  51. position: relative;
  52. background-color: #03070c;
  53. border-bottom: 1px solid var(--panel-border);
  54. display: flex;
  55. align-items: center;
  56. justify-content: center;
  57. padding: 16px;
  58. perspective: 800px;
  59. z-index: 10;
  60. box-shadow: 0 4px 20px rgba(0,0,0,0.5);
  61. }
  62. /* Spotlight effect */
  63. .spotlight {
  64. position: absolute;
  65. width: 250px;
  66. height: 250px;
  67. border-radius: 50%;
  68. background: radial-gradient(circle, rgba(59, 130, 246, 0.1) 0%, transparent 70%);
  69. pointer-events: none;
  70. z-index: 0;
  71. }
  72. /* Floating action tip */
  73. .fullscreen-tip {
  74. position: absolute;
  75. bottom: 10px;
  76. left: 50%;
  77. transform: translateX(-50%);
  78. display: flex;
  79. align-items: center;
  80. gap: 6px;
  81. background: rgba(0, 0, 0, 0.6);
  82. backdrop-filter: blur(8px);
  83. -webkit-backdrop-filter: blur(8px);
  84. border: 1px solid rgba(255, 255, 255, 0.1);
  85. padding: 4px 12px;
  86. border-radius: 20px;
  87. font-size: 0.7rem;
  88. color: #94a3b8;
  89. pointer-events: none;
  90. z-index: 5;
  91. letter-spacing: 0.5px;
  92. }
  93. /* Scale holder wrapper */
  94. .plate-wrapper {
  95. width: 100%;
  96. max-width: 420px;
  97. z-index: 1;
  98. transition: transform 0.1s ease-out;
  99. transform-style: preserve-3d;
  100. display: flex;
  101. justify-content: center;
  102. align-items: center;
  103. }
  104. /* LICENSE PLATE MAIN STYLING */
  105. .plate {
  106. aspect-ratio: 440 / 140; /* Standard ratio */
  107. background: var(--plate-bg);
  108. border-radius: 0.22em;
  109. position: relative;
  110. display: flex;
  111. align-items: center;
  112. justify-content: space-between;
  113. padding: 0 0.45em;
  114. box-sizing: border-box;
  115. box-shadow:
  116. 0 15px 35px rgba(0,0,0,0.7),
  117. var(--plate-border-shadow);
  118. overflow: hidden;
  119. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  120. transform-style: preserve-3d;
  121. }
  122. /* Special overrides during html2canvas rendering to prevent 3D clipping */
  123. .plate.rendering {
  124. transform: none !important;
  125. box-shadow: var(--plate-border-shadow) !important;
  126. }
  127. .plate.rendering .plate-char {
  128. transform: none !important;
  129. }
  130. /* Metal texture overlay */
  131. .plate::after {
  132. content: '';
  133. position: absolute;
  134. inset: 0;
  135. background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
  136. opacity: 0.04;
  137. mix-blend-mode: overlay;
  138. pointer-events: none;
  139. z-index: 2;
  140. }
  141. /* Plate outer border */
  142. .plate-border-outer {
  143. position: absolute;
  144. inset: 0.08em;
  145. border: 0.05em solid var(--plate-border);
  146. border-radius: 0.16em;
  147. pointer-events: none;
  148. z-index: 3;
  149. box-shadow:
  150. inset 0 1px 1px rgba(255,255,255,0.4),
  151. 0 1px 1px rgba(0,0,0,0.5);
  152. }
  153. /* Plate inner parallel line */
  154. .plate-border-inner {
  155. position: absolute;
  156. inset: 0.18em;
  157. border: 0.025em solid var(--plate-border);
  158. border-radius: 0.1em;
  159. pointer-events: none;
  160. z-index: 3;
  161. }
  162. /* Screws (4 corner screws) */
  163. .screw {
  164. position: absolute;
  165. width: 0.32em;
  166. height: 0.32em;
  167. background: radial-gradient(circle, #f0f0f0 0%, #c8c8c8 45%, #6e6e6e 85%, #444 100%);
  168. border-radius: 50%;
  169. box-shadow:
  170. inset 0 1px 1px rgba(255,255,255,0.8),
  171. 0 1px 2px rgba(0,0,0,0.6);
  172. z-index: 4;
  173. display: flex;
  174. align-items: center;
  175. justify-content: center;
  176. }
  177. .screw-cap {
  178. font-size: 0.12em;
  179. font-weight: 900;
  180. color: rgba(0, 0, 0, 0.65);
  181. text-shadow: 0.5px 0.5px 0px rgba(255, 255, 255, 0.4);
  182. transform: scale(0.9);
  183. }
  184. .screw.top-left { top: 3.5%; left: 24.5%; }
  185. .screw.top-right { top: 3.5%; right: 24.5%; }
  186. .screw.bottom-left { bottom: 3.5%; left: 24.5%; }
  187. .screw.bottom-right { bottom: 3.5%; right: 24.5%; }
  188. /* Glossy reflection effect */
  189. .plate-gloss {
  190. position: absolute;
  191. inset: 0;
  192. background: linear-gradient(135deg,
  193. rgba(255, 255, 255, 0.18) 0%,
  194. rgba(255, 255, 255, 0.06) 35%,
  195. transparent 35.1%,
  196. transparent 100%
  197. );
  198. pointer-events: none;
  199. z-index: 5;
  200. transition: all 0.1s ease;
  201. }
  202. /* Plate Content Layout */
  203. .plate-content {
  204. display: flex;
  205. align-items: center;
  206. width: 100%;
  207. height: 100%;
  208. z-index: 3;
  209. position: relative;
  210. justify-content: center;
  211. padding-top: 0.1em;
  212. }
  213. .char-group-left {
  214. display: flex;
  215. align-items: center;
  216. gap: 0.1em;
  217. }
  218. .char-group-right {
  219. display: flex;
  220. align-items: center;
  221. gap: 0.06em;
  222. }
  223. .plate-char {
  224. font-family: 'Oswald', 'Noto Sans SC', sans-serif;
  225. font-weight: 800;
  226. font-size: 1.95em;
  227. color: var(--plate-text);
  228. text-shadow: var(--plate-text-shadow);
  229. line-height: 1;
  230. display: inline-block;
  231. transform: translateZ(10px);
  232. letter-spacing: -0.05em;
  233. }
  234. /* Province character squish */
  235. .plate-char.province {
  236. font-family: 'Noto Sans SC', sans-serif;
  237. font-weight: 900;
  238. font-size: 1.85em;
  239. margin-right: 0.05em;
  240. }
  241. .plate-char.city {
  242. font-family: 'Montserrat', sans-serif;
  243. font-weight: 800;
  244. font-size: 1.9em;
  245. }
  246. /* Divider */
  247. .char-divider {
  248. width: 0.8em;
  249. display: flex;
  250. align-items: center;
  251. justify-content: center;
  252. position: relative;
  253. height: 100%;
  254. }
  255. .dot {
  256. width: 0.22em;
  257. height: 0.22em;
  258. background-color: var(--plate-text);
  259. border-radius: 50%;
  260. box-shadow:
  261. -0.5px -0.5px 0px rgba(255, 255, 255, 0.7),
  262. 0.5px 0.5px 0px rgba(0, 0, 0, 0.8),
  263. 1px 1px 2px rgba(0,0,0,0.5);
  264. display: inline-block;
  265. }
  266. /* New Energy Logo in Center */
  267. .nev-logo {
  268. display: none;
  269. width: 0.68em;
  270. height: 0.68em;
  271. z-index: 6;
  272. filter: drop-shadow(1px 1px 1px rgba(0,0,0,0.3));
  273. }
  274. /* Active NEV style adjusts layout */
  275. .plate.nev {
  276. aspect-ratio: 480 / 140; /* NEV standard size */
  277. background: linear-gradient(to bottom, #ffffff 20%, #e8f9ed 45%, #15b043 100%);
  278. }
  279. .plate.nev .dot {
  280. display: none;
  281. }
  282. .plate.nev .nev-logo {
  283. display: block;
  284. }
  285. /* Special Red characters for Police/Embassy */
  286. .char-special-red {
  287. color: #e11d48 !important;
  288. text-shadow:
  289. -0.5px -0.5px 0px rgba(255, 255, 255, 0.8),
  290. 0.8px 0.8px 0px rgba(0, 0, 0, 0.9),
  291. 1.5px 1.5px 2px rgba(0, 0, 0, 0.7) !important;
  292. }
  293. /* Dirt / Grunge Worn Effect */
  294. .plate-dirt {
  295. position: absolute;
  296. inset: 0;
  297. background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.05' numOctaves='3' result='noise'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.85 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
  298. opacity: 0;
  299. mix-blend-mode: multiply;
  300. pointer-events: none;
  301. z-index: 4;
  302. transition: opacity 0.3s;
  303. }
  304. /* CONTROL PANEL (BOTTOM 65%) */
  305. .control-panel {
  306. height: 65dvh;
  307. background: var(--panel-bg);
  308. border-top: 1px solid var(--panel-border);
  309. border-radius: 24px 24px 0 0;
  310. padding: 20px 16px;
  311. overflow-y: auto;
  312. -webkit-overflow-scrolling: touch;
  313. display: flex;
  314. flex-direction: column;
  315. gap: 20px;
  316. z-index: 20;
  317. box-shadow: 0 -8px 30px rgba(0, 0, 0, 0.5);
  318. padding-bottom: calc(20px + env(safe-area-inset-bottom));
  319. }
  320. .section-title {
  321. font-size: 0.95rem;
  322. font-weight: 800;
  323. color: #cbd5e1;
  324. border-left: 4px solid var(--accent-color);
  325. padding-left: 8px;
  326. margin-bottom: 10px;
  327. display: flex;
  328. justify-content: space-between;
  329. align-items: center;
  330. }
  331. .form-group {
  332. display: flex;
  333. flex-direction: column;
  334. gap: 6px;
  335. }
  336. .plate-input-wrapper {
  337. display: flex;
  338. gap: 8px;
  339. }
  340. /* Input styling */
  341. .province-trigger {
  342. width: 58px;
  343. height: 48px;
  344. background: rgba(255, 255, 255, 0.05);
  345. border: 1px solid rgba(255, 255, 255, 0.1);
  346. border-radius: 10px;
  347. color: #ffffff;
  348. font-size: 1.25rem;
  349. font-weight: 900;
  350. display: flex;
  351. align-items: center;
  352. justify-content: center;
  353. cursor: pointer;
  354. transition: all 0.2s ease;
  355. }
  356. .province-trigger:active {
  357. background: rgba(255, 255, 255, 0.12);
  358. border-color: var(--accent-color);
  359. }
  360. .text-input-field {
  361. flex: 1;
  362. height: 48px;
  363. background: rgba(255, 255, 255, 0.05);
  364. border: 1px solid rgba(255, 255, 255, 0.1);
  365. border-radius: 10px;
  366. color: #ffffff;
  367. font-size: 1.2rem;
  368. font-family: 'Oswald', sans-serif;
  369. font-weight: 700;
  370. padding: 0 14px;
  371. letter-spacing: 2px;
  372. text-transform: uppercase;
  373. outline: none;
  374. transition: all 0.2s ease;
  375. }
  376. .text-input-field:focus {
  377. border-color: var(--accent-color);
  378. background: rgba(255, 255, 255, 0.08);
  379. }
  380. /* Suffix buttons */
  381. .suffix-list {
  382. display: flex;
  383. flex-wrap: wrap;
  384. gap: 6px;
  385. }
  386. .suffix-btn {
  387. background: rgba(255, 255, 255, 0.04);
  388. border: 1px solid rgba(255, 255, 255, 0.06);
  389. color: #94a3b8;
  390. padding: 6px 12px;
  391. border-radius: 8px;
  392. font-size: 0.8rem;
  393. font-weight: 700;
  394. cursor: pointer;
  395. transition: all 0.15s;
  396. }
  397. .suffix-btn:active {
  398. background: rgba(255, 255, 255, 0.12);
  399. color: #ffffff;
  400. border-color: var(--accent-color);
  401. }
  402. /* Color cards layout */
  403. .color-selector {
  404. display: flex;
  405. overflow-x: auto;
  406. gap: 10px;
  407. padding-bottom: 6px;
  408. -webkit-overflow-scrolling: touch;
  409. scrollbar-width: none; /* Firefox */
  410. }
  411. .color-selector::-webkit-scrollbar {
  412. display: none; /* Chrome/Safari */
  413. }
  414. .color-tab {
  415. flex-shrink: 0;
  416. width: 110px;
  417. height: 64px;
  418. border-radius: 12px;
  419. border: 2px solid transparent;
  420. cursor: pointer;
  421. display: flex;
  422. flex-direction: column;
  423. justify-content: center;
  424. align-items: center;
  425. font-size: 0.75rem;
  426. font-weight: 800;
  427. gap: 4px;
  428. position: relative;
  429. overflow: hidden;
  430. box-shadow: 0 4px 10px rgba(0,0,0,0.3);
  431. transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
  432. }
  433. .color-tab::before {
  434. content: '';
  435. position: absolute;
  436. top: 0;
  437. left: 0;
  438. right: 0;
  439. height: 35%;
  440. background: linear-gradient(rgba(255, 255, 255, 0.15), transparent);
  441. pointer-events: none;
  442. }
  443. .color-tab.active {
  444. border-color: #ffffff;
  445. box-shadow: 0 0 12px rgba(255, 255, 255, 0.15);
  446. transform: scale(1.02);
  447. }
  448. /* Presets definition */
  449. .color-tab.preset-blue {
  450. background: linear-gradient(135deg, #0d58ca 0%, #002d7c 100%);
  451. color: #ffffff;
  452. }
  453. .color-tab.preset-green {
  454. background: linear-gradient(to bottom, #ffffff 30%, #15b043 100%);
  455. color: #0a0f1d;
  456. border: 1px solid rgba(0,0,0,0.08);
  457. }
  458. .color-tab.preset-yellow {
  459. background: linear-gradient(135deg, #ffcf33 0%, #ffb800 100%);
  460. color: #0a0f1d;
  461. }
  462. .color-tab.preset-white {
  463. background: #ffffff;
  464. color: #0a0f1d;
  465. border: 1px solid #ddd;
  466. }
  467. .color-tab.preset-black {
  468. background: #1e1e1e;
  469. color: #ffffff;
  470. border: 1px solid rgba(255,255,255,0.08);
  471. }
  472. /* Sliders */
  473. .sliders-group {
  474. display: flex;
  475. flex-direction: column;
  476. gap: 10px;
  477. background: rgba(255, 255, 255, 0.02);
  478. border: 1px solid rgba(255, 255, 255, 0.04);
  479. border-radius: 12px;
  480. padding: 12px;
  481. }
  482. .slider-row {
  483. display: flex;
  484. justify-content: space-between;
  485. align-items: center;
  486. }
  487. .slider-label {
  488. font-size: 0.78rem;
  489. color: #94a3b8;
  490. font-weight: 600;
  491. }
  492. .switch {
  493. position: relative;
  494. display: inline-block;
  495. width: 40px;
  496. height: 22px;
  497. }
  498. .switch input {
  499. opacity: 0;
  500. width: 0;
  501. height: 0;
  502. }
  503. .slider-round {
  504. position: absolute;
  505. cursor: pointer;
  506. top: 0;
  507. left: 0;
  508. right: 0;
  509. bottom: 0;
  510. background-color: rgba(255, 255, 255, 0.1);
  511. transition: .2s;
  512. border-radius: 22px;
  513. }
  514. .slider-round:before {
  515. position: absolute;
  516. content: "";
  517. height: 16px;
  518. width: 16px;
  519. left: 3px;
  520. bottom: 3px;
  521. background-color: white;
  522. transition: .2s;
  523. border-radius: 50%;
  524. }
  525. input:checked + .slider-round {
  526. background-color: var(--accent-color);
  527. }
  528. input:checked + .slider-round:before {
  529. transform: translateX(18px);
  530. }
  531. .range-slider {
  532. width: 100%;
  533. -webkit-appearance: none;
  534. background: rgba(255, 255, 255, 0.1);
  535. height: 4px;
  536. border-radius: 2px;
  537. outline: none;
  538. margin-top: 4px;
  539. }
  540. .range-slider::-webkit-slider-thumb {
  541. -webkit-appearance: none;
  542. width: 14px;
  543. height: 14px;
  544. border-radius: 50%;
  545. background: var(--accent-color);
  546. cursor: pointer;
  547. box-shadow: 0 0 6px var(--accent-color);
  548. }
  549. /* Buttons block */
  550. .actions-group {
  551. display: flex;
  552. gap: 10px;
  553. margin-top: 8px;
  554. }
  555. .btn {
  556. flex: 1;
  557. height: 48px;
  558. border-radius: 12px;
  559. font-size: 0.92rem;
  560. font-weight: 800;
  561. border: none;
  562. outline: none;
  563. display: flex;
  564. align-items: center;
  565. justify-content: center;
  566. gap: 8px;
  567. cursor: pointer;
  568. transition: all 0.2s ease;
  569. }
  570. .btn-primary {
  571. background: var(--accent-color);
  572. color: #ffffff;
  573. box-shadow: 0 4px 15px var(--accent-glow);
  574. }
  575. .btn-primary:active {
  576. background: #2563eb;
  577. transform: translateY(1px);
  578. }
  579. .btn-secondary {
  580. background: rgba(255, 255, 255, 0.05);
  581. color: #cbd5e1;
  582. border: 1px solid rgba(255, 255, 255, 0.08);
  583. }
  584. .btn-secondary:active {
  585. background: rgba(255, 255, 255, 0.08);
  586. }
  587. /* NATIVE-LIKE BOTTOM SHEET FOR PROVINCES */
  588. .bottom-sheet {
  589. position: fixed;
  590. left: 0;
  591. right: 0;
  592. bottom: 0;
  593. background: rgba(13, 17, 28, 0.95);
  594. backdrop-filter: blur(25px);
  595. -webkit-backdrop-filter: blur(25px);
  596. border-top: 1px solid var(--panel-border);
  597. border-radius: 24px 24px 0 0;
  598. z-index: 100;
  599. transform: translateY(100%);
  600. transition: transform 0.35s cubic-bezier(0.16, 1, 0.3, 1);
  601. box-shadow: 0 -10px 40px rgba(0, 0, 0, 0.8);
  602. padding: 20px 16px calc(20px + env(safe-area-inset-bottom));
  603. }
  604. .bottom-sheet.active {
  605. transform: translateY(0);
  606. }
  607. .sheet-overlay {
  608. position: fixed;
  609. inset: 0;
  610. background: rgba(0,0,0,0.6);
  611. z-index: 99;
  612. opacity: 0;
  613. pointer-events: none;
  614. transition: opacity 0.3s ease;
  615. backdrop-filter: blur(4px);
  616. -webkit-backdrop-filter: blur(4px);
  617. }
  618. .sheet-overlay.active {
  619. opacity: 1;
  620. pointer-events: auto;
  621. }
  622. .sheet-header {
  623. display: flex;
  624. justify-content: space-between;
  625. align-items: center;
  626. margin-bottom: 16px;
  627. }
  628. .sheet-header h3 {
  629. font-size: 1.05rem;
  630. font-weight: 800;
  631. color: #ffffff;
  632. }
  633. .sheet-close-btn {
  634. background: rgba(255, 255, 255, 0.05);
  635. border: none;
  636. color: #94a3b8;
  637. width: 32px;
  638. height: 32px;
  639. border-radius: 50%;
  640. font-size: 1.2rem;
  641. display: flex;
  642. align-items: center;
  643. justify-content: center;
  644. cursor: pointer;
  645. }
  646. .province-grid {
  647. display: grid;
  648. grid-template-columns: repeat(6, 1fr);
  649. gap: 8px;
  650. max-height: 40vh;
  651. overflow-y: auto;
  652. -webkit-overflow-scrolling: touch;
  653. padding-bottom: 10px;
  654. }
  655. .province-btn {
  656. background: rgba(255, 255, 255, 0.04);
  657. border: 1px solid rgba(255, 255, 255, 0.06);
  658. border-radius: 8px;
  659. height: 44px;
  660. color: #ffffff;
  661. font-size: 1.1rem;
  662. font-weight: 800;
  663. cursor: pointer;
  664. display: flex;
  665. align-items: center;
  666. justify-content: center;
  667. }
  668. .province-btn:active {
  669. background: var(--accent-color);
  670. border-color: var(--accent-color);
  671. transform: scale(0.95);
  672. }
  673. /* FULLSCREEN OVERLAY FOR ORIENTED PREVIEW */
  674. .fullscreen-overlay {
  675. position: fixed;
  676. inset: 0;
  677. background: #000000;
  678. z-index: 1000;
  679. display: flex;
  680. flex-direction: column;
  681. align-items: center;
  682. justify-content: center;
  683. opacity: 0;
  684. pointer-events: none;
  685. transition: opacity 0.3s cubic-bezier(0.16, 1, 0.3, 1);
  686. }
  687. .fullscreen-overlay.active {
  688. opacity: 1;
  689. pointer-events: auto;
  690. }
  691. .fullscreen-close-btn {
  692. position: absolute;
  693. top: max(20px, env(safe-area-inset-top));
  694. right: max(20px, env(safe-area-inset-right));
  695. width: 40px;
  696. height: 40px;
  697. border-radius: 50%;
  698. background: rgba(255, 255, 255, 0.15);
  699. border: 1px solid rgba(255, 255, 255, 0.25);
  700. color: #ffffff;
  701. font-size: 1.5rem;
  702. display: flex;
  703. align-items: center;
  704. justify-content: center;
  705. cursor: pointer;
  706. z-index: 1020;
  707. backdrop-filter: blur(10px);
  708. -webkit-backdrop-filter: blur(10px);
  709. }
  710. .fullscreen-tip-top {
  711. position: absolute;
  712. top: max(20px, env(safe-area-inset-top));
  713. left: 50%;
  714. transform: translateX(-50%);
  715. background: rgba(0, 0, 0, 0.7);
  716. border: 1px solid rgba(255, 255, 255, 0.1);
  717. padding: 6px 16px;
  718. border-radius: 20px;
  719. font-size: 0.75rem;
  720. color: #ffffff;
  721. pointer-events: none;
  722. z-index: 1010;
  723. letter-spacing: 0.5px;
  724. white-space: nowrap;
  725. }
  726. .fullscreen-image-container {
  727. width: 90vw;
  728. display: flex;
  729. align-items: center;
  730. justify-content: center;
  731. }
  732. .fullscreen-image-container img {
  733. width: 100%;
  734. height: auto;
  735. border-radius: 1.5vw;
  736. box-shadow: 0 20px 50px rgba(0,0,0,0.8);
  737. max-height: 85vh;
  738. object-fit: contain;
  739. }
  740. /* Portrait layout: rotate 90deg to simulate landscape */
  741. @media (orientation: portrait) {
  742. .fullscreen-image-container {
  743. width: 90dvh;
  744. transform: rotate(90deg);
  745. transform-origin: center center;
  746. }
  747. .fullscreen-image-container img {
  748. max-width: 82dvh;
  749. max-height: 82dvw;
  750. }
  751. .fullscreen-tip-top {
  752. top: auto;
  753. bottom: max(20px, env(safe-area-inset-bottom));
  754. }
  755. }
  756. /* Landscape adjustments for tiny screens */
  757. @media (orientation: landscape) and (max-height: 480px) {
  758. .fullscreen-tip-top {
  759. top: max(10px, env(safe-area-inset-top));
  760. }
  761. .fullscreen-image-container img {
  762. max-height: 90vh;
  763. }
  764. }
  765. /* Toast notification styling */
  766. .toast {
  767. position: fixed;
  768. bottom: 40px;
  769. left: 50%;
  770. transform: translateX(-50%) translateY(100px);
  771. background: #0f172a;
  772. border: 1px solid var(--accent-color);
  773. color: #ffffff;
  774. padding: 10px 20px;
  775. border-radius: 10px;
  776. box-shadow: 0 10px 20px rgba(0,0,0,0.4), 0 0 10px var(--accent-glow);
  777. z-index: 1000;
  778. font-size: 0.8rem;
  779. font-weight: 700;
  780. transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
  781. pointer-events: none;
  782. white-space: nowrap;
  783. }
  784. .toast.active {
  785. transform: translateX(-50%) translateY(0);
  786. }
  787. </style>
  788. </head>
  789. <body>
  790. <!-- 3D PREVIEW STAGE (TOP) -->
  791. <div class="showcase-container" id="showcase-container">
  792. <div class="spotlight"></div>
  793. <div class="fullscreen-tip">
  794. <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="vertical-align: middle; margin-top:-2px;"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>
  795. <span>点击车牌全屏横屏展示</span>
  796. </div>
  797. <div class="plate-wrapper" id="plate-wrapper" style="cursor: pointer;">
  798. <!-- Real physical plate -->
  799. <div class="plate" id="license-plate">
  800. <!-- Mounting bolts -->
  801. <div class="screw top-left"><div class="screw-cap">粤</div></div>
  802. <div class="screw top-right"><div class="screw-cap">粤</div></div>
  803. <div class="screw bottom-left"><div class="screw-cap">粤</div></div>
  804. <div class="screw bottom-right"><div class="screw-cap">粤</div></div>
  805. <!-- Double borders -->
  806. <div class="plate-border-outer"></div>
  807. <div class="plate-border-inner"></div>
  808. <!-- Texture/Shader effects -->
  809. <div class="plate-gloss" id="plate-gloss-layer"></div>
  810. <div class="plate-dirt" id="plate-dirt-layer"></div>
  811. <!-- Dynamic text container -->
  812. <div class="plate-content" id="plate-content-box"></div>
  813. </div>
  814. </div>
  815. </div>
  816. <!-- CONTROL PANEL (BOTTOM) -->
  817. <div class="control-panel">
  818. <!-- Section 1: Number Input -->
  819. <div class="form-group">
  820. <div class="section-title">
  821. <span>1. 车牌号码输入</span>
  822. </div>
  823. <div class="plate-input-wrapper">
  824. <button class="province-trigger" id="province-trigger">粤</button>
  825. <input type="text" class="text-input-field" id="plate-input" value="B88888" placeholder="输入字母和数字" maxlength="6" autocomplete="off" spellcheck="false">
  826. </div>
  827. </div>
  828. <!-- Suffix Shortcuts -->
  829. <div class="form-group" style="margin-top: -6px;">
  830. <div class="suffix-list">
  831. <button class="suffix-btn">警</button>
  832. <button class="suffix-btn">学</button>
  833. <button class="suffix-btn">挂</button>
  834. <button class="suffix-btn">港</button>
  835. <button class="suffix-btn">澳</button>
  836. <button class="suffix-btn">领</button>
  837. </div>
  838. </div>
  839. <!-- Section 2: Color selector -->
  840. <div class="form-group">
  841. <div class="section-title">
  842. <span>2. 车牌颜色类型</span>
  843. </div>
  844. <div class="color-selector">
  845. <div class="color-tab preset-blue active" data-preset="blue">
  846. <span>🔵 蓝底白字</span>
  847. </div>
  848. <div class="color-tab preset-green" data-preset="green">
  849. <span>🟢 渐变绿底黑字</span>
  850. </div>
  851. <div class="color-tab preset-yellow" data-preset="yellow">
  852. <span>🟡 黄底黑字</span>
  853. </div>
  854. <div class="color-tab preset-white" data-preset="white">
  855. <span>⚪ 白底黑字</span>
  856. </div>
  857. <div class="color-tab preset-black" data-preset="black">
  858. <span>⚫ 黑底白字</span>
  859. </div>
  860. </div>
  861. </div>
  862. <!-- Section 3: Physical Customizations -->
  863. <div class="form-group">
  864. <div class="section-title">
  865. <span>3. 物理材质微调</span>
  866. </div>
  867. <div class="sliders-group">
  868. <div class="slider-row">
  869. <span class="slider-label">固定防盗螺丝螺帽</span>
  870. <label class="switch">
  871. <input type="checkbox" id="screw-toggle" checked>
  872. <span class="slider-round"></span>
  873. </label>
  874. </div>
  875. </div>
  876. <div class="sliders-group">
  877. <div class="slider-row">
  878. <span class="slider-label">3D 字符凹凸力度: <span id="emboss-val">70%</span></span>
  879. </div>
  880. <input type="range" class="range-slider" id="emboss-slider" min="10" max="100" value="70">
  881. </div>
  882. <div class="sliders-group">
  883. <div class="slider-row">
  884. <span class="slider-label">漆面反光高光强度: <span id="gloss-val">100%</span></span>
  885. </div>
  886. <input type="range" class="range-slider" id="gloss-slider" min="0" max="100" value="100">
  887. </div>
  888. <div class="sliders-group">
  889. <div class="slider-row">
  890. <span class="slider-label">表面泥渍/陈旧磨损: <span id="dirt-val">0%</span></span>
  891. </div>
  892. <input type="range" class="range-slider" id="dirt-slider" min="0" max="80" value="0">
  893. </div>
  894. </div>
  895. <!-- Actions -->
  896. <div class="actions-group">
  897. <button class="btn btn-secondary" id="action-fullscreen">
  898. <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>
  899. 全屏预览
  900. </button>
  901. <button class="btn btn-primary" id="action-download">
  902. <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>
  903. 导出高清图
  904. </button>
  905. </div>
  906. </div>
  907. <!-- PROVINCE BOTTOM SHEET -->
  908. <div class="sheet-overlay" id="sheet-overlay"></div>
  909. <div class="bottom-sheet" id="province-sheet">
  910. <div class="sheet-header">
  911. <h3>选择省份简称</h3>
  912. <button class="sheet-close-btn" id="sheet-close">&times;</button>
  913. </div>
  914. <div class="province-grid" id="province-grid"></div>
  915. </div>
  916. <!-- FULLSCREEN OVERLAY PREVIEW -->
  917. <div class="fullscreen-overlay" id="fullscreen-overlay">
  918. <button class="fullscreen-close-btn" id="fullscreen-close-btn">&times;</button>
  919. <div class="fullscreen-tip-top">
  920. <span>💡 长按图片保存至手机相册 | 点击任意区域返回</span>
  921. </div>
  922. <div class="fullscreen-image-container" id="fullscreen-image-container">
  923. <img src="" alt="车牌高精渲染" id="fullscreen-plate-img">
  924. </div>
  925. </div>
  926. <!-- TOAST NOTIFICATION -->
  927. <div class="toast" id="toast">图片渲染生成中...</div>
  928. <!-- html2canvas library for DOM rendering -->
  929. <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
  930. <script>
  931. // --- State Management ---
  932. let currentProvince = "粤";
  933. let activePreset = "blue";
  934. // --- DOM Elements ---
  935. const provinceTrigger = document.getElementById("province-trigger");
  936. const plateInput = document.getElementById("plate-input");
  937. const provinceSheet = document.getElementById("province-sheet");
  938. const sheetOverlay = document.getElementById("sheet-overlay");
  939. const sheetClose = document.getElementById("sheet-close");
  940. const provinceGrid = document.getElementById("province-grid");
  941. const licensePlate = document.getElementById("license-plate");
  942. const plateWrapper = document.getElementById("plate-wrapper");
  943. const showcaseContainer = document.getElementById("showcase-container");
  944. const contentBox = document.getElementById("plate-content-box");
  945. const screwToggle = document.getElementById("screw-toggle");
  946. const embossSlider = document.getElementById("emboss-slider");
  947. const glossSlider = document.getElementById("gloss-slider");
  948. const dirtSlider = document.getElementById("dirt-slider");
  949. const embossVal = document.getElementById("emboss-val");
  950. const glossVal = document.getElementById("gloss-val");
  951. const dirtVal = document.getElementById("dirt-val");
  952. const actionFullscreen = document.getElementById("action-fullscreen");
  953. const actionDownload = document.getElementById("action-download");
  954. const toast = document.getElementById("toast");
  955. const fullscreenOverlay = document.getElementById("fullscreen-overlay");
  956. const fullscreenCloseBtn = document.getElementById("fullscreen-close-btn");
  957. const fullscreenPlateImg = document.getElementById("fullscreen-plate-img");
  958. const fullscreenImageContainer = document.getElementById("fullscreen-image-container");
  959. // --- Data Lists ---
  960. const provinces = [
  961. "京", "津", "冀", "晋", "蒙", "辽", "吉", "黑", "沪", "苏",
  962. "浙", "皖", "闽", "赣", "鲁", "豫", "鄂", "湘", "粤", "桂",
  963. "琼", "渝", "川", "贵", "云", "藏", "陕", "甘", "青", "宁",
  964. "新", "港", "澳", "学", "警", "使", "领"
  965. ];
  966. const presets = {
  967. blue: {
  968. bg: "linear-gradient(135deg, #0d58ca 0%, #003692 50%, #002d7c 100%)",
  969. border: "#ffffff",
  970. text: "#ffffff",
  971. textShadow: (intensity) => `
  972. -0.5px -0.5px 0px rgba(255, 255, 255, ${0.4 + intensity * 0.4}),
  973. 0.8px 0.8px 0px rgba(0, 0, 0, ${0.6 + intensity * 0.3}),
  974. 1.5px 1.5px ${1.5 + intensity * 1.5}px rgba(0, 0, 0, 0.7)
  975. `,
  976. borderShadow: "inset 0 2px 3px rgba(255,255,255,0.4), inset 0 -2px 3px rgba(0,0,0,0.6), 0 3px 6px rgba(0,0,0,0.4)"
  977. },
  978. green: {
  979. bg: "linear-gradient(to bottom, #ffffff 20%, #e8f9ed 45%, #15b043 100%)",
  980. border: "#111111",
  981. text: "#111111",
  982. textShadow: (intensity) => `
  983. -0.4px -0.4px 0px rgba(0, 0, 0, 0.25),
  984. 0.6px 0.6px 0px rgba(255, 255, 255, 0.8),
  985. 1px 1px 1.5px rgba(0, 0, 0, 0.25)
  986. `,
  987. borderShadow: "inset 0 1px 2px rgba(255,255,255,0.8), inset 0 -1.5px 2px rgba(0,0,0,0.2), 0 3px 5px rgba(0,0,0,0.3)"
  988. },
  989. yellow: {
  990. bg: "linear-gradient(135deg, #ffcf33 0%, #ffb800 100%)",
  991. border: "#111111",
  992. text: "#111111",
  993. textShadow: (intensity) => `
  994. -0.4px -0.4px 0px rgba(0, 0, 0, 0.35),
  995. 0.6px 0.6px 0px rgba(255, 255, 255, 0.85),
  996. 1.2px 1.2px 1.8px rgba(0, 0, 0, 0.3)
  997. `,
  998. borderShadow: "inset 0 1.5px 2px rgba(255,255,255,0.7), inset 0 -1.5px 2px rgba(0,0,0,0.4), 0 3px 5px rgba(0,0,0,0.4)"
  999. },
  1000. white: {
  1001. bg: "#ffffff",
  1002. border: "#e11d48", // Police red border
  1003. text: "#111111",
  1004. textShadow: (intensity) => `
  1005. -0.3px -0.3px 0px rgba(0, 0, 0, 0.15),
  1006. 0.6px 0.6px 0px rgba(0, 0, 0, 0.15),
  1007. 1px 1px 1.5px rgba(0, 0, 0, 0.1)
  1008. `,
  1009. borderShadow: "inset 0 1px 2px rgba(255,255,255,0.8), inset 0 -1.5px 2px rgba(0,0,0,0.1), 0 2px 4px rgba(0,0,0,0.2)"
  1010. },
  1011. black: {
  1012. bg: "linear-gradient(135deg, #222222 0%, #111111 100%)",
  1013. border: "#ffffff",
  1014. text: "#ffffff",
  1015. textShadow: (intensity) => `
  1016. -0.5px -0.5px 0px rgba(255, 255, 255, 0.4),
  1017. 0.8px 0.8px 0px rgba(0, 0, 0, 0.95),
  1018. 1.5px 1.5px 2px rgba(0, 0, 0, 0.7)
  1019. `,
  1020. borderShadow: "inset 0 2px 3px rgba(255,255,255,0.2), inset 0 -2px 3px rgba(0,0,0,0.8), 0 3px 6px rgba(0,0,0,0.5)"
  1021. }
  1022. };
  1023. // --- Core Functions ---
  1024. // Sets font size to width / 10 to ensure it is always larger than mobile min-size limits.
  1025. function adjustPlateFontSize() {
  1026. const width = plateWrapper.clientWidth || plateWrapper.offsetWidth;
  1027. if (width > 0) {
  1028. licensePlate.style.fontSize = (width / 10) + "px";
  1029. licensePlate.style.width = width + "px";
  1030. const ratio = activePreset === "green" ? (140 / 480) : (140 / 440);
  1031. licensePlate.style.height = (width * ratio) + "px";
  1032. }
  1033. }
  1034. function showToast(msg, duration = 2000) {
  1035. toast.textContent = msg;
  1036. toast.classList.add("active");
  1037. setTimeout(() => {
  1038. toast.classList.remove("active");
  1039. }, duration);
  1040. }
  1041. // Initialize bottom sheet buttons
  1042. function initProvinceSheet() {
  1043. provinces.forEach(prov => {
  1044. const btn = document.createElement("button");
  1045. btn.className = "province-btn";
  1046. btn.textContent = prov;
  1047. btn.onclick = () => {
  1048. currentProvince = prov;
  1049. provinceTrigger.textContent = prov;
  1050. document.querySelectorAll(".screw-cap").forEach(cap => cap.textContent = prov);
  1051. closeProvinceSheet();
  1052. updatePlate();
  1053. };
  1054. provinceGrid.appendChild(btn);
  1055. });
  1056. }
  1057. function openProvinceSheet() {
  1058. provinceSheet.classList.add("active");
  1059. sheetOverlay.classList.add("active");
  1060. }
  1061. function closeProvinceSheet() {
  1062. provinceSheet.classList.remove("active");
  1063. sheetOverlay.classList.remove("active");
  1064. }
  1065. // Filters non-standard characters (only allowing alphanumeric + valid plate Chinese characters)
  1066. function formatInput(text) {
  1067. return text.toUpperCase()
  1068. .replace(/[^A-Z0-9京津冀晋蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼渝川贵云藏陕甘青宁新港澳使领警学挂]/g, "")
  1069. .replace(/[IO]/g, ""); // standard plate ban on I and O
  1070. }
  1071. function applyStyles() {
  1072. const preset = presets[activePreset];
  1073. const intensity = parseInt(embossSlider.value) / 100;
  1074. const gloss = parseInt(glossSlider.value) / 100;
  1075. const dirt = parseInt(dirtSlider.value) / 100;
  1076. embossVal.textContent = Math.round(intensity * 100) + "%";
  1077. glossVal.textContent = Math.round(gloss * 100) + "%";
  1078. dirtVal.textContent = Math.round(dirt * 100) + "%";
  1079. // Set variables
  1080. document.documentElement.style.setProperty("--plate-bg", preset.bg);
  1081. document.documentElement.style.setProperty("--plate-border", preset.border);
  1082. document.documentElement.style.setProperty("--plate-text", preset.text);
  1083. document.documentElement.style.setProperty("--plate-text-shadow", preset.textShadow(intensity));
  1084. document.documentElement.style.setProperty("--plate-border-shadow", preset.borderShadow);
  1085. // Toggle screws
  1086. document.querySelectorAll(".screw").forEach(screw => {
  1087. screw.style.display = screwToggle.checked ? "flex" : "none";
  1088. });
  1089. // Adjust specific border settings for Police White plate
  1090. if (activePreset === "white") {
  1091. document.documentElement.style.setProperty("--plate-border", "#e11d48");
  1092. }
  1093. // Adjust textures opacity
  1094. document.getElementById("plate-gloss-layer").style.opacity = gloss * 0.22;
  1095. document.getElementById("plate-dirt-layer").style.opacity = dirt;
  1096. }
  1097. function updatePlate() {
  1098. applyStyles();
  1099. let textVal = formatInput(plateInput.value);
  1100. // Auto-detect and strip starting province if user typed it
  1101. if (textVal.length > 0) {
  1102. const firstChar = textVal.charAt(0);
  1103. const startingProvinces = ["京", "津", "冀", "晋", "蒙", "辽", "吉", "黑", "沪", "苏", "浙", "皖", "闽", "赣", "鲁", "豫", "鄂", "湘", "粤", "桂", "琼", "渝", "川", "贵", "云", "藏", "陕", "甘", "青", "宁", "新", "使"];
  1104. if (startingProvinces.includes(firstChar)) {
  1105. currentProvince = firstChar;
  1106. provinceTrigger.textContent = firstChar;
  1107. document.querySelectorAll(".screw-cap").forEach(cap => cap.textContent = firstChar);
  1108. textVal = textVal.slice(1);
  1109. }
  1110. }
  1111. const maxChars = (activePreset === "green") ? 7 : 6;
  1112. if (textVal.length > maxChars) {
  1113. textVal = textVal.slice(0, maxChars);
  1114. }
  1115. plateInput.value = textVal;
  1116. plateInput.maxLength = maxChars;
  1117. if (activePreset === "green") {
  1118. licensePlate.classList.add("nev");
  1119. } else {
  1120. licensePlate.classList.remove("nev");
  1121. }
  1122. const cityChar = textVal.charAt(0) || "B";
  1123. const remainder = textVal.slice(1);
  1124. contentBox.innerHTML = "";
  1125. // Group Left
  1126. const leftGroup = document.createElement("div");
  1127. leftGroup.className = "char-group-left";
  1128. const provSpan = document.createElement("span");
  1129. provSpan.className = "plate-char province";
  1130. provSpan.textContent = currentProvince;
  1131. leftGroup.appendChild(provSpan);
  1132. const citySpan = document.createElement("span");
  1133. citySpan.className = "plate-char city";
  1134. citySpan.textContent = cityChar;
  1135. leftGroup.appendChild(citySpan);
  1136. contentBox.appendChild(leftGroup);
  1137. // Middle Divider
  1138. const divider = document.createElement("div");
  1139. divider.className = "char-divider";
  1140. const dot = document.createElement("span");
  1141. dot.className = "dot";
  1142. divider.appendChild(dot);
  1143. // NEV Leaf SVG
  1144. const svgNS = "http://www.w3.org/2000/svg";
  1145. const nevLogo = document.createElementNS(svgNS, "svg");
  1146. nevLogo.setAttribute("class", "nev-logo");
  1147. nevLogo.setAttribute("viewBox", "0 0 100 100");
  1148. const circle = document.createElementNS(svgNS, "circle");
  1149. circle.setAttribute("cx", "50"); circle.setAttribute("cy", "50"); circle.setAttribute("r", "44");
  1150. circle.setAttribute("fill", "none"); circle.setAttribute("stroke", "#19af46"); circle.setAttribute("stroke-width", "5");
  1151. nevLogo.appendChild(circle);
  1152. const leaf = document.createElementNS(svgNS, "path");
  1153. leaf.setAttribute("d", "M 50 30 Q 30 30 30 50 Q 48 50 50 40 Q 70 40 70 50 Q 52 50 50 40");
  1154. leaf.setAttribute("fill", "none"); leaf.setAttribute("stroke", "#19af46"); leaf.setAttribute("stroke-width", "5");
  1155. nevLogo.appendChild(leaf);
  1156. const plug = document.createElementNS(svgNS, "rect");
  1157. plug.setAttribute("x", "43"); plug.setAttribute("y", "58"); plug.setAttribute("width", "14"); plug.setAttribute("height", "10"); plug.setAttribute("rx", "1"); plug.setAttribute("fill", "#19af46");
  1158. nevLogo.appendChild(plug);
  1159. const prong1 = document.createElementNS(svgNS, "line");
  1160. prong1.setAttribute("x1", "47"); prong1.setAttribute("y1", "52"); prong1.setAttribute("x2", "47"); prong1.setAttribute("y2", "58"); prong1.setAttribute("stroke", "#19af46"); prong1.setAttribute("stroke-width", "3");
  1161. nevLogo.appendChild(prong1);
  1162. const prong2 = document.createElementNS(svgNS, "line");
  1163. prong2.setAttribute("x1", "53"); prong2.setAttribute("y1", "52"); prong2.setAttribute("x2", "53"); prong2.setAttribute("y2", "58"); prong2.setAttribute("stroke", "#19af46"); prong2.setAttribute("stroke-width", "3");
  1164. nevLogo.appendChild(prong2);
  1165. const wire = document.createElementNS(svgNS, "path");
  1166. wire.setAttribute("d", "M 50 68 L 50 78 L 62 78");
  1167. wire.setAttribute("fill", "none"); wire.setAttribute("stroke", "#19af46"); wire.setAttribute("stroke-width", "4");
  1168. nevLogo.appendChild(wire);
  1169. divider.appendChild(nevLogo);
  1170. contentBox.appendChild(divider);
  1171. // Group Right
  1172. const rightGroup = document.createElement("div");
  1173. rightGroup.className = "char-group-right";
  1174. const paddedRemainder = remainder.padEnd(maxChars - 1, "8");
  1175. for (let i = 0; i < paddedRemainder.length; i++) {
  1176. const char = paddedRemainder.charAt(i);
  1177. const charSpan = document.createElement("span");
  1178. charSpan.className = "plate-char";
  1179. charSpan.textContent = char;
  1180. if ((char === "警" || char === "使" || char === "领" || char === "学") && activePreset === "white") {
  1181. charSpan.classList.add("char-special-red");
  1182. }
  1183. rightGroup.appendChild(charSpan);
  1184. }
  1185. contentBox.appendChild(rightGroup);
  1186. adjustPlateFontSize();
  1187. }
  1188. // --- Touch 3D Tilt Orbit Effect ---
  1189. let touchStartX = 0;
  1190. let touchStartY = 0;
  1191. let isDragging = false;
  1192. showcaseContainer.addEventListener("touchstart", (e) => {
  1193. if (e.touches.length === 1) {
  1194. const touch = e.touches[0];
  1195. touchStartX = touch.clientX;
  1196. touchStartY = touch.clientY;
  1197. isDragging = false;
  1198. }
  1199. });
  1200. showcaseContainer.addEventListener("touchmove", (e) => {
  1201. if (e.touches.length === 1) {
  1202. const touch = e.touches[0];
  1203. const deltaX = Math.abs(touch.clientX - touchStartX);
  1204. const deltaY = Math.abs(touch.clientY - touchStartY);
  1205. if (deltaX > 6 || deltaY > 6) {
  1206. isDragging = true;
  1207. }
  1208. // Prevent page scroll when rotating plate
  1209. e.preventDefault();
  1210. const rect = showcaseContainer.getBoundingClientRect();
  1211. const x = touch.clientX - rect.left;
  1212. const y = touch.clientY - rect.top;
  1213. const xOffset = (x / rect.width) - 0.5;
  1214. const yOffset = (y / rect.height) - 0.5;
  1215. const rotateY = xOffset * 16;
  1216. const rotateX = -yOffset * 16;
  1217. plateWrapper.style.transform = `rotateX(${rotateX}deg) rotateY(${rotateY}deg) scale(1.04)`;
  1218. const glossLayer = document.getElementById("plate-gloss-layer");
  1219. glossLayer.style.background = `linear-gradient(${135 + rotateY}deg, rgba(255, 255, 255, 0.22) 0%, rgba(255, 255, 255, 0.05) ${35 + xOffset * 10}%, transparent 35.1%, transparent 100%)`;
  1220. }
  1221. }, { passive: false });
  1222. showcaseContainer.addEventListener("touchend", () => {
  1223. plateWrapper.style.transform = "rotateX(0deg) rotateY(0deg) scale(1)";
  1224. const glossLayer = document.getElementById("plate-gloss-layer");
  1225. glossLayer.style.background = `linear-gradient(135deg, rgba(255, 255, 255, 0.18) 0%, rgba(255, 255, 255, 0.06) 35%, transparent 35.1%, transparent 100%)`;
  1226. });
  1227. // Mouse support for desktop testing
  1228. showcaseContainer.addEventListener("mousemove", (e) => {
  1229. const rect = showcaseContainer.getBoundingClientRect();
  1230. const x = e.clientX - rect.left;
  1231. const y = e.clientY - rect.top;
  1232. const xOffset = (x / rect.width) - 0.5;
  1233. const yOffset = (y / rect.height) - 0.5;
  1234. const rotateY = xOffset * 16;
  1235. const rotateX = -yOffset * 16;
  1236. plateWrapper.style.transform = `rotateX(${rotateX}deg) rotateY(${rotateY}deg) scale(1.04)`;
  1237. });
  1238. showcaseContainer.addEventListener("mouseleave", () => {
  1239. plateWrapper.style.transform = "rotateX(0deg) rotateY(0deg) scale(1)";
  1240. });
  1241. // --- Fullscreen and Render Exporter ---
  1242. function renderToCanvas(callback) {
  1243. plateWrapper.style.transform = "rotateX(0deg) rotateY(0deg) scale(1)";
  1244. licensePlate.classList.add("rendering");
  1245. adjustPlateFontSize();
  1246. setTimeout(() => {
  1247. html2canvas(licensePlate, {
  1248. scale: 3.5, // Ultra crisp HD
  1249. backgroundColor: null,
  1250. useCORS: true,
  1251. allowTaint: false,
  1252. logging: false
  1253. }).then(canvas => {
  1254. licensePlate.classList.remove("rendering");
  1255. adjustPlateFontSize();
  1256. callback(canvas);
  1257. }).catch(err => {
  1258. console.error("html2canvas generation error", err);
  1259. licensePlate.classList.remove("rendering");
  1260. adjustPlateFontSize();
  1261. showToast("❌ 渲染失败,请重试");
  1262. });
  1263. }, 250);
  1264. }
  1265. function triggerFullscreen() {
  1266. showToast("📸 正在高精物理渲染号牌...");
  1267. renderToCanvas(canvas => {
  1268. const imgData = canvas.toDataURL("image/png");
  1269. fullscreenPlateImg.src = imgData;
  1270. fullscreenOverlay.classList.add("active");
  1271. showToast("🎉 渲染成功!长按车牌可保存至相册");
  1272. });
  1273. }
  1274. function downloadImage() {
  1275. showToast("📸 正在高精物理渲染号牌...");
  1276. renderToCanvas(canvas => {
  1277. const link = document.createElement("a");
  1278. const plateNumber = currentProvince + formatInput(plateInput.value);
  1279. link.download = `${plateNumber}_${activePreset}_license_plate.png`;
  1280. link.href = canvas.toDataURL("image/png");
  1281. link.click();
  1282. showToast("🎉 高清车牌图片导出成功!");
  1283. });
  1284. }
  1285. // --- Event Setup ---
  1286. provinceTrigger.onclick = openProvinceSheet;
  1287. sheetClose.onclick = closeProvinceSheet;
  1288. sheetOverlay.onclick = closeProvinceSheet;
  1289. plateInput.addEventListener("input", updatePlate);
  1290. // Quick Suffix buttons
  1291. document.querySelectorAll(".suffix-btn").forEach(btn => {
  1292. btn.onclick = () => {
  1293. const val = plateInput.value;
  1294. const max = (activePreset === "green") ? 7 : 6;
  1295. if (val.length < max) {
  1296. plateInput.value += btn.textContent;
  1297. } else {
  1298. plateInput.value = val.slice(0, max - 1) + btn.textContent;
  1299. }
  1300. updatePlate();
  1301. };
  1302. });
  1303. // Color Presets tabs
  1304. document.querySelectorAll(".color-tab").forEach(tab => {
  1305. tab.onclick = () => {
  1306. document.querySelector(".color-tab.active").classList.remove("active");
  1307. tab.classList.add("active");
  1308. activePreset = tab.dataset.preset;
  1309. updatePlate();
  1310. };
  1311. });
  1312. // Sliders updates
  1313. [embossSlider, glossSlider, dirtSlider].forEach(slider => {
  1314. slider.addEventListener("input", updatePlate);
  1315. });
  1316. screwToggle.addEventListener("change", updatePlate);
  1317. // Click plate trigger fullscreen
  1318. showcaseContainer.addEventListener("click", () => {
  1319. if (!isDragging) {
  1320. triggerFullscreen();
  1321. }
  1322. isDragging = false;
  1323. });
  1324. actionFullscreen.onclick = triggerFullscreen;
  1325. actionDownload.onclick = downloadImage;
  1326. // Fullscreen close
  1327. fullscreenCloseBtn.onclick = (e) => {
  1328. e.stopPropagation();
  1329. fullscreenOverlay.classList.remove("active");
  1330. };
  1331. fullscreenOverlay.onclick = () => {
  1332. fullscreenOverlay.classList.remove("active");
  1333. };
  1334. fullscreenPlateImg.onclick = (e) => {
  1335. e.stopPropagation();
  1336. };
  1337. // Resize layout handler
  1338. window.addEventListener("resize", adjustPlateFontSize);
  1339. // --- Bootstrap Init ---
  1340. initProvinceSheet();
  1341. updatePlate();
  1342. document.addEventListener("DOMContentLoaded", adjustPlateFontSize);
  1343. window.addEventListener("load", adjustPlateFontSize);
  1344. </script>
  1345. </body>
  1346. </html>