├── .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 |
18 |
19 | Copyright © 2024 Honoka55 |
20 | Repo |
21 | Donate |
22 |
23 |
24 |
25 |
26 |
Watermark |
27 |
33 |
34 |
35 |
60 |
61 |
62 |
63 |
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 |
--------------------------------------------------------------------------------