├── .gitignore ├── LICENSE ├── README.md ├── figure.woff ├── i18n.js ├── index.html ├── kai.woff2 ├── kana.woff ├── kanji.woff2 ├── script.js └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | ref.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Honoka55 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 25ji-generator 2 | A tool to generate Nightcord at 25:00-styled logo images. 3 | 4 | [Try it!](https://honoka55.github.io/25ji-generator) 5 | 6 | ## License 7 | - Source codes are licensed under the [MIT license](LICENSE). 8 | - `kai.woff2` is licensed under the [1999 Arphic Public License](https://www.freedesktop.org/wiki/Arphic_Public_License). Other font files are licensed under the [SIL Open Font License v1.1](https://scripts.sil.org/OFL). For detailed information, please refer to the license and copyright information within each font file. 9 | -------------------------------------------------------------------------------- /figure.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Honoka55/25ji-generator/c9c7a71d444c404999c446c5c8f199577d805941/figure.woff -------------------------------------------------------------------------------- /i18n.js: -------------------------------------------------------------------------------- 1 | const i18n = { 2 | data: { 3 | 'zh-hans': { 4 | 'title': '25点,logo生成器见。', 5 | 'header': '点,logo生成器见。', 6 | 'transparent-background': '透明背景', 7 | 'background-color': '背景色:', 8 | 'submit-btn': '生成', 9 | 'submit-btn-update': '更新', 10 | 'download-btn': '保存', 11 | 'font-min': '宋体', 12 | 'font-kai': '楷体' 13 | }, 14 | 'zh-hant': { 15 | 'title': '25點,logo產生器見。', 16 | 'header': '點,logo產生器見。', 17 | 'transparent-background': '透明背景', 18 | 'background-color': '背景色:', 19 | 'submit-btn': '產生', 20 | 'submit-btn-update': '更新', 21 | 'download-btn': '儲存', 22 | 'font-min': '宋體', 23 | 'font-kai': '楷體' 24 | }, 25 | 'ja': { 26 | 'title': '25時、ロゴジェネレーターで。', 27 | 'header': '時、ロゴジェネレーターで。', 28 | 'transparent-background': '透明背景', 29 | 'background-color': '背景色:', 30 | 'submit-btn': '作成', 31 | 'submit-btn-update': '更新', 32 | 'download-btn': '保存', 33 | 'font-min': '明朝体', 34 | 'font-kai': '楷書体' 35 | }, 36 | 'ko': { 37 | 'title': '25시, 로고 제너레이터에서.', 38 | 'header': '시, 로고 제너레이터에서.', 39 | 'transparent-background': '투명 배경', 40 | 'background-color': '배경색:', 41 | 'submit-btn': '작성', 42 | 'submit-btn-update': '경신', 43 | 'download-btn': '저장', 44 | 'font-min': '명조체', 45 | 'font-kai': '해서체' 46 | } 47 | }, 48 | currentLanguage: '', 49 | setLanguage(language) { 50 | document.querySelector('html').setAttribute('lang', language); 51 | this.currentLanguage = language; 52 | let elements = document.querySelectorAll('[data-i18n]'); 53 | elements.forEach((element) => { 54 | let key = element.getAttribute('data-i18n'); 55 | element.textContent = this.getText(key); 56 | }); 57 | }, 58 | getText(key) { 59 | return this.data[this.currentLanguage][key]; 60 | }, 61 | detectLanguage() { 62 | let language = navigator.language; 63 | if (!this.data[language]) { 64 | language = 'zh-hans'; 65 | } 66 | document.getElementById('language').value = language; 67 | this.setLanguage(language); 68 | } 69 | }; 70 | 71 | const languageSelect = document.getElementById('language'); 72 | languageSelect.addEventListener('change', () => { 73 | i18n.setLanguage(languageSelect.value); 74 | }); 75 | 76 | i18n.detectLanguage(); 77 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |

13 | 14 | 15 | 25 16 | 17 | 34 |

35 |
36 | 37 | 38 | 42 |
43 | 44 |
45 | 46 | 47 |
48 | 49 | 50 |
51 | 52 | 53 | 54 |

56 | 57 | 58 | 59 |
60 |
61 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /kai.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Honoka55/25ji-generator/c9c7a71d444c404999c446c5c8f199577d805941/kai.woff2 -------------------------------------------------------------------------------- /kana.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Honoka55/25ji-generator/c9c7a71d444c404999c446c5c8f199577d805941/kana.woff -------------------------------------------------------------------------------- /kanji.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Honoka55/25ji-generator/c9c7a71d444c404999c446c5c8f199577d805941/kanji.woff2 -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | const canvasContainer = document.getElementById('canvas-container'); 2 | const h1 = document.querySelector('h1'); 3 | const text1Input = document.getElementById('text1'); 4 | const text2Input = document.getElementById('text2'); 5 | const tenSelect = document.getElementById('ten'); 6 | const text3Input = document.getElementById('text3'); 7 | const slider = document.getElementById('slider'); 8 | const submitBtn = document.getElementById('submit-btn'); 9 | const downloadBtn = document.getElementById('download-btn'); 10 | const nightcord = document.getElementById('nightcord'); 11 | const copyright = document.getElementById('copyright'); 12 | const watermarkCheck = document.getElementById('watermark'); 13 | const transparentCheck = document.getElementById('transparent'); 14 | const transparentText = document.getElementById('transparent-text'); 15 | const colorPicker = document.getElementById('color-picker'); 16 | const fontSelector = document.querySelectorAll('input[type="radio"][name="kanji-font"]'); 17 | const overlay = document.getElementById('overlay'); 18 | const popup = document.getElementById('popup'); 19 | let firstTime = true; 20 | let currentFont = 'kanji'; 21 | 22 | text1Input.addEventListener('focus', () => { 23 | text1Input.previousValue = text1Input.value; 24 | }); 25 | text1Input.addEventListener('keydown', () => { 26 | text1Input.previousValue = text1Input.value; 27 | }); 28 | text1Input.addEventListener('input', () => { 29 | text1Input.value = text1Input.value.replace(/^0+/, ''); 30 | if (text1Input.value === '') { 31 | text1Input.value = '0'; 32 | } 33 | if (!text1Input.validity.valid) { 34 | text1Input.value = text1Input.previousValue; 35 | } 36 | }); 37 | 38 | const nightcordEvent = () => { 39 | if (copyright.style.maxHeight == '0px') { 40 | copyright.style.maxHeight = copyright.scrollHeight + 'px'; 41 | } else { 42 | copyright.style.maxHeight = '0px'; 43 | } 44 | }; 45 | 46 | transparentCheck.addEventListener('change', () => { 47 | if (transparentCheck.checked) { 48 | transparentText.dataset.i18n = 'transparent-background'; 49 | transparentText.textContent = i18n.getText('transparent-background'); 50 | colorPicker.style.visibility = 'hidden'; 51 | } else { 52 | transparentText.dataset.i18n = 'background-color'; 53 | transparentText.textContent = i18n.getText('background-color'); 54 | colorPicker.style.visibility = 'visible'; 55 | } 56 | }); 57 | 58 | fontSelector.forEach((radio) => { 59 | radio.addEventListener('change', () => { 60 | if (radio.checked) { 61 | if (radio.value === 'min') { 62 | h1.style.fontFamily = 'figure,kana,kanji'; 63 | currentFont = 'kanji'; 64 | } else if (radio.value === 'kai') { 65 | h1.style.fontFamily = 'figure,kana,kai,kanji'; 66 | currentFont = 'kai, kanji'; 67 | } 68 | } 69 | }); 70 | }); 71 | 72 | overlay.addEventListener('click', (e) => { 73 | const closeButton = document.getElementById('close-btn'); 74 | if (e.target === overlay || e.target === closeButton) { 75 | overlay.style.display = 'none'; 76 | } 77 | }); 78 | 79 | window.onload = () => { 80 | fetch('kana.woff') 81 | .then((res) => res.arrayBuffer()) 82 | .then((data) => { 83 | const kanaFont = opentype.parse(data); 84 | fetch('figure.woff') 85 | .then((res) => res.arrayBuffer()) 86 | .then((data) => { 87 | const figureFont = opentype.parse(data); 88 | submitBtn.removeAttribute('disabled'); 89 | 90 | const generateLogo = (text1, text2, ten, text3, hikari2X = false) => { 91 | slider.setAttribute('text1', text1); 92 | slider.setAttribute('text2', text2); 93 | slider.setAttribute('ten', ten); 94 | slider.setAttribute('text3', text3); 95 | 96 | canvasContainer.innerHTML = ''; 97 | const canvas = document.createElement('canvas'); 98 | canvas.width = 2000; 99 | canvas.height = 366; 100 | canvasContainer.appendChild(canvas); 101 | const ctx = canvas.getContext('2d'); 102 | 103 | ctx.lineWidth = 16; 104 | let strokeErase; 105 | 106 | if (transparentCheck.checked) { 107 | strokeErase = () => { 108 | ctx.globalCompositeOperation = 'destination-out'; 109 | ctx.stroke(); 110 | ctx.globalCompositeOperation = 'source-over'; 111 | }; 112 | } else { 113 | ctx.fillStyle = colorPicker.value; 114 | ctx.fillRect(0, 0, canvas.width, canvas.height); 115 | strokeErase = () => { 116 | ctx.strokeStyle = colorPicker.value; 117 | ctx.stroke(); 118 | }; 119 | } 120 | 121 | const drawGradientChar = (char, charX, charY) => { 122 | const measure = ctx.measureText(char); 123 | const gradient = ctx.createLinearGradient(charX + measure.width, charY - measure.actualBoundingBoxAscent, charX, charY + measure.actualBoundingBoxDescent); 124 | gradient.addColorStop(0.66, '#232838'); 125 | gradient.addColorStop(1, '#a6a1d1'); 126 | ctx.fillStyle = gradient; 127 | ctx.fillText(char, charX, charY); 128 | }; 129 | 130 | const getKerning = (font, char1, char2, size = 0) => { 131 | const glyph1 = font.charToGlyph(char1); 132 | const glyph2 = font.charToGlyph(char2); 133 | if (!glyph1 || !glyph2) { 134 | return 0; 135 | } 136 | const kerning = font.getKerningValue(glyph1, glyph2); 137 | if (size > 0) { 138 | const unitsPerEm = font.unitsPerEm; 139 | return (kerning * size) / unitsPerEm; 140 | } else { 141 | return kerning; 142 | } 143 | }; 144 | 145 | const drawHikari = (startX, startY, size = false) => { 146 | let endX, endY, ctrlX, ctrlY; 147 | if (size) { 148 | endX = startX + 96; 149 | endY = startY + 110; 150 | ctrlX = startX + 53; 151 | ctrlY = startY + 49; 152 | } else { 153 | endX = startX - 171; 154 | endY = startY + 294; 155 | ctrlX = startX - 88; 156 | ctrlY = startY + 130; 157 | } 158 | 159 | ctx.moveTo(startX, startY); 160 | ctx.quadraticCurveTo(ctrlX, ctrlY, endX, endY); 161 | ctx.quadraticCurveTo(startX + endX - ctrlX, startY + endY - ctrlY, startX, startY); 162 | 163 | const gradient = ctx.createLinearGradient(startX, startY, endX, endY); 164 | gradient.addColorStop(0.06, '#597cf7'); 165 | gradient.addColorStop(0.2, '#bfb6f7'); 166 | gradient.addColorStop(0.3, '#94b9ef'); 167 | gradient.addColorStop(0.37, '#a8e8fe'); 168 | gradient.addColorStop(0.45, '#dcf3cd'); 169 | gradient.addColorStop(0.53, '#c4bcf8'); 170 | gradient.addColorStop(0.65, '#6e8df7'); 171 | gradient.addColorStop(0.75, '#b2bdfb'); 172 | gradient.addColorStop(0.82, '#c6f0f7'); 173 | gradient.addColorStop(0.86, '#9aa0d3'); 174 | gradient.addColorStop(0.91, '#2c4184'); 175 | 176 | return gradient; 177 | }; 178 | 179 | const drawTriangle = (x1, y1, x2, y2, x3, y3) => { 180 | ctx.moveTo(x1, y1); 181 | ctx.lineTo(x2, y2); 182 | ctx.lineTo(x3, y3); 183 | ctx.lineTo(x1, y1); 184 | 185 | const gradient = ctx.createLinearGradient(Math.max(x1, x2, x3), Math.min(y1, y2, y3), Math.min(x1, x2, x3), Math.max(y1, y2, y3)); 186 | gradient.addColorStop(0, '#2b2e44'); 187 | gradient.addColorStop(1, '#555'); 188 | return gradient; 189 | }; 190 | 191 | const drawSet = (paths) => { 192 | ctx.beginPath(); 193 | for (let i = 0; i < paths.length; i++) { 194 | const [func, ...args] = paths[i]; 195 | func(...args); 196 | } 197 | ctx.moveTo(paths[0][1], paths[0][2]); 198 | ctx.closePath(); 199 | strokeErase(); 200 | 201 | for (let i = 0; i < paths.length; i++) { 202 | const [func, ...args] = paths[i]; 203 | ctx.beginPath(); 204 | ctx.fillStyle = func(...args); 205 | ctx.closePath(); 206 | ctx.fill(); 207 | } 208 | }; 209 | 210 | ctx.font = '255px figure'; 211 | text1 = text1.slice(0, -1).replace(/\d/g, (digit) => '⁰¹²³⁴⁵⁶⁷⁸⁹'[digit]) + text1.slice(-1); 212 | let textX = 44; 213 | for (let i = 0; i < text1.length; i++) { 214 | const textY = 208; 215 | const char = text1[i]; 216 | drawGradientChar(char, textX, textY); 217 | textX += ctx.measureText(char).width; 218 | if (i < text1.length - 1) { 219 | const kerning = getKerning(figureFont, text1[i], text1[i + 1], 255); 220 | textX += kerning; 221 | } 222 | } 223 | const hikari1X = textX + 76.58; 224 | 225 | ctx.fillStyle = '#000'; 226 | ctx.font = `90px kana, ${currentFont}, serif`; 227 | textX = hikari1X - 77; 228 | let textY = 230; 229 | let tenX = textX; 230 | let tenY = textY; 231 | for (let [i, char] of [...text2].entries()) { 232 | drawGradientChar(char, textX, textY); 233 | tenX = textX; 234 | tenY = textY; 235 | switch (i) { 236 | case 0: 237 | textX += 88; 238 | textY -= 30; 239 | break; 240 | case 1: 241 | textX -= 23; 242 | textY += 88; 243 | tenX -= 23; 244 | tenY += 23; 245 | break; 246 | default: 247 | break; 248 | } 249 | } 250 | tenX += 78; 251 | tenY += 6; 252 | ctx.font = '86px kana'; 253 | ctx.fillText(ten, tenX, tenY); 254 | textX = tenX + 54; 255 | 256 | ctx.font = `99px kana, ${currentFont}, serif`; 257 | textY = 240; 258 | let rdIndex = [text3.length - 1]; 259 | if (text3.indexOf('cord') != -1) { 260 | rdIndex.push(text3.indexOf('cord') + 2); 261 | rdIndex.push(text3.indexOf('cord') + 3); 262 | } 263 | text3 = text3.replace(/gg/g, '󰁧g'); 264 | for (let [i, char] of [...text3].entries()) { 265 | if (i == 0) { 266 | let newChar = String.fromCharCode(char.charCodeAt(0) + 0xb000); 267 | if (char.charCodeAt(0) + 0xb000 >= 0xe000 && char.charCodeAt(0) + 0xb000 <= 0xf8ff && kanaFont.charToGlyph(newChar).unicode && text2.length < 3) { 268 | char = newChar; 269 | } else { 270 | textX -= 16; 271 | } 272 | drawGradientChar(char, textX, textY); 273 | } else if (rdIndex.includes(i)) { 274 | drawGradientChar(char, textX, textY); 275 | } else { 276 | ctx.fillStyle = '#000'; 277 | ctx.fillText(char, textX, textY); 278 | } 279 | textX += ctx.measureText(char).width; 280 | if (i < text3.length - 1) { 281 | const kerning = getKerning(kanaFont, text3[i], text3[i + 1], 99); 282 | textX += kerning; 283 | } 284 | } 285 | ctx.font = 'bold 90px kana'; 286 | drawGradientChar('。', textX - 9, textY); 287 | 288 | drawSet([ 289 | [drawHikari, hikari1X, 40], 290 | [drawTriangle, hikari1X - 31, 120, hikari1X - 23, 106, hikari1X - 18, 113], 291 | [drawTriangle, hikari1X - 138, 252, hikari1X - 150, 252, hikari1X - 147, 258], 292 | [drawTriangle, hikari1X - 99, 233, hikari1X - 113, 263, hikari1X - 97, 265], 293 | [drawTriangle, hikari1X - 93, 224, hikari1X - 87, 223, hikari1X - 89, 235] 294 | ]); 295 | 296 | const sliderMax = Math.round(textX + 33); 297 | const sliderMin = Math.min(sliderMax, Math.round(tenX + 180)); 298 | const sliderValue = Math.max(sliderMin, Math.min(sliderMax, Math.round(textX - 0.18 * (textX - tenX)))); 299 | slider.setAttribute('min', sliderMin); 300 | slider.setAttribute('max', sliderMax); 301 | slider.setAttribute('value', sliderValue); 302 | slider.removeAttribute('disabled'); 303 | 304 | if (!hikari2X) { 305 | hikari2X = sliderValue; 306 | } 307 | if (hikari2X < sliderMin) { 308 | hikari2X = sliderMin; 309 | } 310 | if (hikari2X > sliderMax) { 311 | hikari2X = sliderMax; 312 | } 313 | drawSet([ 314 | [drawHikari, hikari2X - 150, 156, 1], 315 | [drawHikari, hikari2X, 35], 316 | [drawTriangle, hikari2X - 139, 153, hikari2X - 145, 143, hikari2X - 138, 141], 317 | [drawTriangle, hikari2X + 9, 131, hikari2X + 23, 117, hikari2X + 28, 126], 318 | [drawTriangle, hikari2X - 96, 249, hikari2X - 107, 255, hikari2X - 91, 270], 319 | [drawTriangle, hikari2X - 116, 279, hikari2X - 126, 293, hikari2X - 117, 296] 320 | ]); 321 | 322 | const data = ctx.getImageData(0, 0, canvas.width, canvas.height); 323 | canvas.width = textX + 73; 324 | ctx.putImageData(data, 0, 0); 325 | 326 | submitBtn.dataset.i18n = 'submit-btn-update'; 327 | submitBtn.textContent = i18n.getText('submit-btn-update'); 328 | 329 | downloadBtn.removeAttribute('disabled'); 330 | downloadBtn.addEventListener('click', () => { 331 | if (watermarkCheck.checked) { 332 | ctx.font = '17px figure'; 333 | ctx.globalAlpha = 0.7; 334 | ctx.fillText('焰', canvas.width - 205, 15); 335 | ctx.globalAlpha = 1; 336 | } 337 | 338 | const img = new Image(); 339 | img.src = canvas.toDataURL('image/png'); 340 | popup.innerHTML = '×'; 341 | popup.appendChild(img); 342 | overlay.style.display = 'flex'; 343 | 344 | if (transparentCheck.checked) { 345 | ctx.clearRect(canvas.width - 205, 0, canvas.width, 20); 346 | } else { 347 | ctx.fillStyle = colorPicker.value; 348 | ctx.fillRect(canvas.width - 205, 0, canvas.width, 20); 349 | } 350 | }); 351 | 352 | nightcord.style.cursor = 'pointer'; 353 | nightcord.addEventListener('click', nightcordEvent); 354 | }; 355 | 356 | const submit = () => { 357 | const text1 = text1Input.value; 358 | const text2 = text2Input.value; 359 | const ten = tenSelect.value; 360 | const text3 = text3Input.value; 361 | generateLogo(text1, text2, ten, text3, parseInt(slider.value)); 362 | if (firstTime) { 363 | firstTime = false; 364 | watermarkCheck.addEventListener('change', submit); 365 | transparentCheck.addEventListener('change', submit); 366 | colorPicker.addEventListener('input', submit); 367 | } 368 | }; 369 | 370 | submitBtn.addEventListener('click', submit); 371 | 372 | slider.addEventListener('input', () => { 373 | generateLogo(slider.getAttribute('text1'), slider.getAttribute('text2'), slider.getAttribute('ten'), slider.getAttribute('text3'), parseInt(slider.value)); 374 | }); 375 | }); 376 | }); 377 | }; 378 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | @import 'https://fonts.googleapis.com/css2?family=Andika:ital,wght@0,400;0,700;1,400;1,700&display=swap'; 2 | @font-face { 3 | font-family: kana; 4 | src: url(kana.woff) format('woff'); 5 | } 6 | @font-face { 7 | font-family: figure; 8 | src: url(figure.woff) format('woff'); 9 | } 10 | @font-face { 11 | font-family: kanji; 12 | src: url(kanji.woff2) format('woff2'); 13 | } 14 | @font-face { 15 | font-family: kai; 16 | src: url(kai.woff2) format('woff2'); 17 | } 18 | 19 | :root { 20 | --zh-hans-font-family: 'Source Han Sans', 'Noto Sans SC', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Heiti SC', SimHei; 21 | --zh-hant-font-family: 'Source Han Sans', 'Noto Sans HK', 'Noto Sans TC', 'PingFang HK', 'PingFang TC', 'Hiragino Sans CNS', 'Microsoft JhengHei', 'Heiti TC'; 22 | --ja-font-family: 'Source Han Sans', 'Noto Sans JP', 'Hiragino Kaku Gothic ProN', 'Hiragino Kaku Gothic Pro', 'Meiryo', 'Yu Gothic', 'MS PGothic'; 23 | --ko-font-family: 'Source Han Sans', 'Noto Sans KR', 'Apple SD Gothic Neo', NanumBarunGothic, 'Malgun Gothic', Dotum; 24 | } 25 | 26 | body { 27 | background-color: #87c5c1; 28 | margin: 0; 29 | padding: 0; 30 | display: flex; 31 | flex-direction: column; 32 | align-items: center; 33 | justify-content: center; 34 | height: 100vh; 35 | -webkit-user-select: none; 36 | user-select: none; 37 | } 38 | 39 | h1 { 40 | color: #849; 41 | font-size: 42px; 42 | margin: 0 auto; 43 | font-family: figure, kana, kanji; 44 | font-weight: normal; 45 | white-space: nowrap; 46 | } 47 | 48 | body:lang(zh-hans), 49 | #copyright:lang(zh-hans), 50 | input:lang(zh-hans), 51 | select:lang(zh-hans), 52 | button:lang(zh-hans) { 53 | font-family: Andika, var(--zh-hans-font-family), sans-serif; 54 | } 55 | body:lang(zh-hant), 56 | #copyright:lang(zh-hant), 57 | input:lang(zh-hant), 58 | select:lang(zh-hant), 59 | button:lang(zh-hant) { 60 | font-family: Andika, var(--zh-hant-font-family), sans-serif; 61 | } 62 | body:lang(ja), 63 | #copyright:lang(ja), 64 | input:lang(ja), 65 | select:lang(ja), 66 | button:lang(ja) { 67 | font-family: Andika, var(--ja-font-family), sans-serif; 68 | } 69 | body:lang(ko), 70 | #copyright:lang(ko), 71 | input:lang(ko), 72 | select:lang(ko), 73 | button:lang(ko) { 74 | font-family: Andika, var(--ko-font-family), sans-serif; 75 | } 76 | 77 | #copyright { 78 | font-size: 12px; 79 | color: #111; 80 | text-align: center; 81 | margin-top: 7px; 82 | overflow: hidden; 83 | transition: max-height 0.3s ease-out; 84 | } 85 | #copyright > span { 86 | vertical-align: middle; 87 | } 88 | 89 | .switch { 90 | position: relative; 91 | display: inline-block; 92 | width: 25px; 93 | height: 12px; 94 | vertical-align: middle; 95 | } 96 | .switch input { 97 | display: none; 98 | } 99 | .switch-toggle { 100 | position: absolute; 101 | cursor: pointer; 102 | top: 0; 103 | left: 0; 104 | right: 0; 105 | bottom: 0; 106 | background-color: #ccc; 107 | border-radius: 6px; 108 | transition: background-color 0.3s ease; 109 | } 110 | .switch-toggle::before { 111 | content: ''; 112 | position: absolute; 113 | height: 10px; 114 | width: 10px; 115 | left: 1px; 116 | bottom: 1px; 117 | background-color: white; 118 | border-radius: 50%; 119 | transition: transform 0.3s ease; 120 | } 121 | .switch input:checked + .switch-toggle { 122 | background-color: #849; 123 | } 124 | .switch input:checked + .switch-toggle::before { 125 | transform: translateX(13px); 126 | } 127 | 128 | #input-container { 129 | max-width: calc(100% - 20px); 130 | margin: 20px 10px; 131 | background-color: #fff; 132 | border-radius: 8px; 133 | padding: 20px 30px; 134 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); 135 | } 136 | 137 | input, 138 | select { 139 | padding: 8px; 140 | margin: 4px; 141 | border: 1px solid #ccc; 142 | border-radius: 4px; 143 | font-size: 14px; 144 | position: relative; 145 | vertical-align: middle; 146 | } 147 | #text1:focus { 148 | outline-color: #88c; 149 | } 150 | #text2:focus { 151 | outline-color: #b68; 152 | } 153 | #ten:focus { 154 | outline-color: #dac; 155 | } 156 | #text3:focus { 157 | outline-color: #ca8; 158 | } 159 | 160 | input[type='range'] { 161 | width: 280px; 162 | -webkit-appearance: none; 163 | -moz-appearance: none; 164 | appearance: none; 165 | height: 18px; 166 | border-radius: 18px; 167 | background: #e0dcdd; 168 | margin: 4px; 169 | vertical-align: middle; 170 | } 171 | input[type='range']::-webkit-slider-thumb { 172 | -webkit-appearance: none; 173 | -moz-appearance: none; 174 | appearance: none; 175 | width: 20px; 176 | height: 20px; 177 | border-radius: 50%; 178 | background: #3cb; 179 | cursor: pointer; 180 | } 181 | input[type='range'][disabled]::-webkit-slider-thumb { 182 | background-color: #bcc; 183 | cursor: not-allowed; 184 | } 185 | input[type='range']::-moz-range-thumb { 186 | width: 20px; 187 | height: 20px; 188 | border-radius: 50%; 189 | background: #3cb; 190 | cursor: pointer; 191 | } 192 | 193 | #language { 194 | padding: 0; 195 | margin: 0; 196 | border: none; 197 | border-radius: 4px; 198 | font-size: 12px; 199 | } 200 | 201 | #transparent-switch { 202 | width: 50px; 203 | height: 24px; 204 | } 205 | #transparent-toggle { 206 | border-radius: 12px; 207 | } 208 | #transparent-toggle::before { 209 | height: 20px; 210 | width: 20px; 211 | left: 2px; 212 | bottom: 2px; 213 | } 214 | #transparent-switch input:checked + #transparent-toggle::before { 215 | transform: translateX(26px); 216 | } 217 | 218 | #transparent-text { 219 | display: inline-block; 220 | width: 70px; 221 | line-height: 1; 222 | text-align: center; 223 | vertical-align: middle; 224 | overflow: hidden; 225 | } 226 | 227 | input[type='color'] { 228 | -webkit-appearance: none; 229 | -moz-appearance: none; 230 | appearance: none; 231 | width: 28px; 232 | height: 28px; 233 | background-color: transparent; 234 | padding: 0; 235 | cursor: pointer; 236 | } 237 | 238 | .font-selector { 239 | display: inline-block; 240 | vertical-align: middle; 241 | } 242 | 243 | input[type='radio'] { 244 | display: none; 245 | } 246 | 247 | .font-selector label { 248 | display: inline-block; 249 | text-align: center; 250 | cursor: pointer; 251 | margin: -2.5px; 252 | padding: 1.3px 0; 253 | width: 68px; 254 | border: 1px solid #ccc; 255 | border-radius: 4px 0 0 4px; 256 | } 257 | .font-selector label:last-child { 258 | border-radius: 0 4px 4px 0; 259 | } 260 | 261 | input[type='radio']:checked + label { 262 | background-color: #849; 263 | color: #fff; 264 | } 265 | 266 | button { 267 | display: inline-block; 268 | padding: 5px 18px; 269 | margin: 4px; 270 | background-color: #849; 271 | color: #fff; 272 | border: none; 273 | border-radius: 4px; 274 | text-decoration: none; 275 | cursor: pointer; 276 | font-size: 16px; 277 | width: 68px; 278 | transition: background-color 0.3s; 279 | vertical-align: middle; 280 | } 281 | button:hover { 282 | background-color: #6a3377; 283 | } 284 | button[disabled] { 285 | background-color: #ccc; 286 | color: #666; 287 | pointer-events: none; 288 | cursor: not-allowed; 289 | } 290 | 291 | #canvas-container { 292 | max-width: 100%; 293 | overflow-x: auto; 294 | } 295 | #canvas-container canvas { 296 | max-width: calc(100% - 20px); 297 | margin: 0 10px; 298 | background-color: #fff; 299 | background-image: linear-gradient(45deg, #ccc 25%, transparent 0, transparent 75%, #ccc 0), linear-gradient(45deg, #ccc 25%, transparent 0, transparent 75%, #ccc 0); 300 | background-position: 0 0, 8px 8px; 301 | background-size: 16px 16px; 302 | border-radius: 8px; 303 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); 304 | } 305 | 306 | a:not(#download-btn) { 307 | box-shadow: 0 -2px rgba(209, 159, 254, 0.5) inset; 308 | transition: all 0.5s ease; 309 | text-decoration: none; 310 | color: #363169; 311 | } 312 | a:not(#download-btn):hover { 313 | cursor: pointer; 314 | box-shadow: 0px -1.85rem 0px rgba(209, 159, 254, 0.7) inset; 315 | } 316 | 317 | #overlay { 318 | position: fixed; 319 | top: 0; 320 | left: 0; 321 | width: 100%; 322 | height: 100%; 323 | background-color: rgba(0, 0, 0, 0.7); 324 | justify-content: center; 325 | align-items: center; 326 | } 327 | #popup { 328 | background-color: #cdebe9; 329 | position: relative; 330 | max-width: 80%; 331 | max-height: 80%; 332 | padding: 20px; 333 | border-radius: 8px; 334 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); 335 | } 336 | #popup img { 337 | width: calc(100% - 40px); 338 | height: auto; 339 | display: block; 340 | margin: 0 auto; 341 | } 342 | #close-btn { 343 | font-size: 18px; 344 | position: absolute; 345 | top: 10px; 346 | right: 10px; 347 | cursor: pointer; 348 | width: 15px; 349 | height: 15px; 350 | border-radius: 50%; 351 | background-color: #ff6059; 352 | color: #fff; 353 | display: flex; 354 | justify-content: center; 355 | align-items: center; 356 | line-height: 1; 357 | } 358 | 359 | @media (max-width: 576px) { 360 | h1 { 361 | font-size: 28px; 362 | } 363 | 364 | #copyright, 365 | #language { 366 | font-size: 10px; 367 | } 368 | 369 | #popup img { 370 | width: 100%; 371 | } 372 | #close-btn { 373 | display: none; 374 | } 375 | 376 | #input-container { 377 | padding: 20px 6px; 378 | text-align: center; 379 | } 380 | 381 | input[type='range'] { 382 | width: 120px; 383 | height: 6px; 384 | margin: 2px; 385 | } 386 | input[type='range'][disabled] { 387 | display: none; 388 | } 389 | input[type='range']::-webkit-slider-thumb { 390 | width: 10px; 391 | height: 10px; 392 | } 393 | input[type='range']::-moz-range-thumb { 394 | width: 10px; 395 | height: 10px; 396 | } 397 | } 398 | --------------------------------------------------------------------------------