| @@ -7,4 +7,19 @@ | |||
| svg { | |||
| width: 100%; | |||
| height: 100%; | |||
| } | |||
| .back { | |||
| fill: white; | |||
| } | |||
| .front { | |||
| fill: #096E11; | |||
| } | |||
| pad-icon[type="awoken-count"][number="full"][special] .back { | |||
| fill: #FFFFD4; | |||
| } | |||
| pad-icon[type="awoken-count"][number="full"][special] .front { | |||
| fill: #F3DC69; | |||
| } | |||
| pad-icon[type="awoken-count"][latent] .front { | |||
| fill: #378DE8; | |||
| } | |||
| @@ -0,0 +1,62 @@ | |||
| <svg id="图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 123 75"> | |||
| <style> | |||
| .back { | |||
| fill: #FFFFD4; | |||
| } | |||
| .front { | |||
| fill: #F3DC69; | |||
| } | |||
| </style> | |||
| <defs> | |||
| <filter id="inset-shadow" x="-50%" y="-50%" width="200%" height="200%" name="inset-shadow"> | |||
| <feComponentTransfer in="SourceAlpha"> | |||
| <feFuncA tableValues="1 0" type="table"/> | |||
| </feComponentTransfer> | |||
| <feGaussianBlur stdDeviation="1" result="blur"/> | |||
| <feFlood flood-color="white" /> | |||
| <feComposite in2="blur" operator="in"/> | |||
| <feOffset dx="0" dy="3"/> | |||
| <feComposite in2="SourceAlpha" operator="in" result="offsetblur-bottom"/> | |||
| <feFlood flood-color="black" /> | |||
| <feComposite in2="blur" operator="in"/> | |||
| <feOffset dx="0" dy="-3"/> | |||
| <feComposite in2="SourceAlpha" operator="in" result="offsetblur-top"/> | |||
| <feMerge> | |||
| <feMergeNode in="SourceGraphic"/> | |||
| <feMergeNode in="offsetblur-bottom"/> | |||
| <feMergeNode in="offsetblur-top"/> | |||
| </feMerge> | |||
| </filter> | |||
| <filter id="out-shadow" x="-50%" y="-50%" width="200%" height="200%" name="inset-shadow"> | |||
| <feComponentTransfer in="SourceAlpha"> | |||
| <feFuncA tableValues="0 1" type="table"/> | |||
| </feComponentTransfer> | |||
| <feGaussianBlur stdDeviation="1" result="blur"/> | |||
| <feOffset dx="0" dy="3"/> | |||
| <feComposite in2="SourceAlpha" operator="out" result="offsetblur"/> | |||
| <feMerge> | |||
| <feMergeNode in="SourceGraphic"/> | |||
| <feMergeNode in="offsetblur"/> | |||
| </feMerge> | |||
| </filter> | |||
| </defs> | |||
| <symbol id="awoken-count-bg" viewBox="0 0 36 40"> | |||
| <g> | |||
| <circle class="back" filter="url(#out-shadow)" cx="18" cy="16" r="16"/> | |||
| <circle class="front" filter="url(#inset-shadow)" cx="18" cy="16" r="13"/> | |||
| </g> | |||
| </symbol> | |||
| <image width="123" height="40" xlink:href="../../../CTFX/Desktop/%E6%9C%AA%E5%91%BD%E5%90%8D-3.png"/> | |||
| <g id="bg-enable-be-assist"> | |||
| <circle class="back" filter="url(#out-shadow)" cx="23" cy="49" r="16"/> | |||
| <circle class="front" filter="url(#inset-shadow)" cx="23" cy="49" r="13"/> | |||
| </g> | |||
| <g id="bg-latent"> | |||
| <circle fill="white" filter="url(#out-shadow)" cx="60" cy="49" r="16"/> | |||
| <circle fill="#378DE8" filter="url(#inset-shadow)" cx="60" cy="49" r="13"/> | |||
| </g> | |||
| <g id="bg-unable-be-assist"> | |||
| <circle fill="white" filter="url(#out-shadow)" cx="103" cy="49" r="16"/> | |||
| <circle fill="#096E11" filter="url(#inset-shadow)" cx="103" cy="49" r="13"/> | |||
| </g> | |||
| </svg> | |||
| @@ -1,19 +1,26 @@ | |||
| const fs = require('fs'); | |||
| const path = require('path'); | |||
| const mime = require('mime'); //需要安装 | |||
| const sizeOf = require('image-size'); //需要安装 | |||
| const { DOMImplementation, XMLSerializer } = require('@xmldom/xmldom'); //需要安装 | |||
| const xmlFormatter = require('xml-formatter'); | |||
| const xmlFormatter = require('xml-formatter'); //需要安装 | |||
| const sharp = require('sharp'); //需要安装 | |||
| const directory = './awokens'; | |||
| const files = fs.readdirSync(directory); | |||
| const svgNS = 'http://www.w3.org/2000/svg'; | |||
| class Icon { | |||
| constructor(file, dir) { | |||
| this.fileName = file; | |||
| this.directory = dir; | |||
| this.buffer = fs.readFileSync(this.path()); | |||
| this.size = sizeOf(this.buffer); | |||
| this.sharp = sharp(this.buffer); | |||
| this.init(); | |||
| } | |||
| async init() { | |||
| this.buffer = await this.sharp.metadata(); | |||
| const {data, info} = await this.sharp.webp({ nearLossless : true}) //近似无损,而非绝对无损 | |||
| .toBuffer({ resolveWithObject: true }); | |||
| this.webpBuffer = data; | |||
| this.webpInfo = info; | |||
| } | |||
| path() { | |||
| return path.join(this.directory, this.fileName); | |||
| @@ -21,45 +28,63 @@ class Icon { | |||
| base64() { | |||
| return `data:${mime.getType(this.fileName)};base64,${this.buffer.toString('base64')}`; | |||
| } | |||
| webpBase64() { | |||
| return `data:${mime.getType('webp')};base64,${this.webpBuffer.toString('base64')}`; | |||
| } | |||
| } | |||
| const iconArr = []; | |||
| for (const file of files) | |||
| { | |||
| const icon = new Icon(file, directory); | |||
| iconArr.push(icon); | |||
| } | |||
| iconArr.sort((a,b)=>{ | |||
| function nameNum(fileName){return parseInt(/^\d+/.exec(fileName)[0])} | |||
| return (nameNum(a.fileName) - nameNum(b.fileName)) || //先判断数字 | |||
| (a.fileName.length - b.fileName.length); //然后判断文件名长度 | |||
| }); | |||
| const svgNS = 'http://www.w3.org/2000/svg'; | |||
| //const dt = new DOMImplementation().createDocumentType('svg:svg', '-//W3C//DTD SVG 1.1//EN', 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'); | |||
| //const svg = new DOMImplementation().createDocument(svgNS, 'svg', dt); | |||
| const svg = new DOMImplementation().createDocument(svgNS, 'svg'); | |||
| for (const icon of iconArr) | |||
| { | |||
| console.log('正在处理 %s', icon.fileName); | |||
| const symbol = svg.createElement('symbol'); | |||
| const parseName = path.parse(icon.fileName); | |||
| const regRes = /^(\d+)(.*)$/ig.exec(parseName.name); | |||
| let aid = regRes ? `${parseInt(regRes[1])}${regRes[2]}` : parseName.name; | |||
| symbol.setAttribute('id', `awoken-${aid}`); | |||
| symbol.setAttribute('viewBox', `0 0 32 32`); | |||
| svg.documentElement.appendChild(symbol); | |||
| const image = svg.createElement('image'); | |||
| image.setAttribute('width', icon.size.width); | |||
| image.setAttribute('height', icon.size.height); | |||
| image.setAttribute('href', icon.base64()); | |||
| symbol.appendChild(image); | |||
| async function main({directory, idPre, svgFilename}) { | |||
| const files = fs.readdirSync(directory); | |||
| const iconArr = []; | |||
| for (const file of files) | |||
| { | |||
| const icon = new Icon(file, directory); | |||
| await icon.init(); | |||
| iconArr.push(icon); | |||
| } | |||
| iconArr.sort((a,b)=>{ | |||
| function nameNum(fileName){return parseInt(/^\d+/.exec(fileName)[0] || 0)} | |||
| return (nameNum(a.fileName) - nameNum(b.fileName)) || //先判断数字 | |||
| (a.fileName.length - b.fileName.length); //然后判断文件名长度 | |||
| }); | |||
| const svgDoc = new DOMImplementation().createDocument(svgNS, 'svg'); | |||
| for (const icon of iconArr) | |||
| { | |||
| console.log('正在处理 %s %s', directory, icon.fileName); | |||
| const symbol = svgDoc.createElement('symbol'); | |||
| const parseName = path.parse(icon.fileName); | |||
| const regRes = /^(\d+)(.*)$/ig.exec(parseName.name); | |||
| let aid = regRes ? `${parseInt(regRes[1])}${regRes[2]}` : parseName.name; | |||
| symbol.setAttribute('id', `${idPre}-${aid}`); | |||
| symbol.setAttribute('viewBox', `0 0 32 32`); | |||
| svgDoc.documentElement.appendChild(symbol); | |||
| const image = svgDoc.createElement('image'); | |||
| image.setAttribute('width', icon.webpInfo.width); | |||
| image.setAttribute('height', icon.webpInfo.height); | |||
| image.setAttribute('href', icon.webpBase64()); | |||
| symbol.appendChild(image); | |||
| } | |||
| const serialized = new XMLSerializer().serializeToString(svgDoc); | |||
| const formattedXml = xmlFormatter(serialized, { | |||
| indentation: '\t', | |||
| filter: (node) => node.type !== 'Comment', | |||
| collapseContent: true, | |||
| lineSeparator: '\n' | |||
| }); | |||
| fs.writeFileSync(svgFilename, formattedXml); | |||
| } | |||
| const serialized = new XMLSerializer().serializeToString(svg); | |||
| const formattedXml = xmlFormatter(serialized, { | |||
| indentation: '\t', | |||
| filter: (node) => node.type !== 'Comment', | |||
| collapseContent: true, | |||
| lineSeparator: '\n' | |||
| }); | |||
| fs.writeFileSync('../icon-awoken.svg', formattedXml); | |||
| const tasks = [ | |||
| {directory: './awokens', idPre: 'awoken', svgFilename: '../icon-awoken.svg'}, | |||
| {directory: './types', idPre: 'type', svgFilename: '../icon-type.svg'}, | |||
| ]; | |||
| tasks.forEach(main); | |||
| @@ -1,9 +1,9 @@ | |||
| { | |||
| "dependencies": { | |||
| "@xmldom/xmldom": "^0.8.2", | |||
| "image-size": "^1.0.2", | |||
| "mime": "^3.0.0", | |||
| "opencc-js": "^1.0.4", | |||
| "sharp": "^0.30.7", | |||
| "xml-formatter": "^2.6.1" | |||
| } | |||
| } | |||
| @@ -114,22 +114,26 @@ class PadIcon extends HTMLElement { | |||
| // Specify observed attributes so that | |||
| // attributeChangedCallback will work | |||
| static get observedAttributes() { | |||
| return ['iid','type','lang']; | |||
| return [ | |||
| 'type', //图标类型 | |||
| 'number', //编号或数字,必须是数字 | |||
| 'lang', //英语、中文特殊图标的设定 | |||
| 'icon-name',//子图标名称 | |||
| 'icon-value',//子图标的值 | |||
| 'full',//觉醒打满 | |||
| 'special',//是否是特殊颜色 | |||
| ]; | |||
| } | |||
| #iid = 0; | |||
| #number = 0; | |||
| #type = "awoken"; | |||
| get iid() { this.#iid; } | |||
| /** | |||
| * @param {string | number} x | |||
| */ | |||
| set iid(x) { | |||
| this.setAttribute('iid', x); | |||
| this.#iid = x; | |||
| get number() { this.#number; } | |||
| set number(x) { | |||
| const number = Number(x); | |||
| if (Number.isNaN(number)) throw new Error('传入的 number 不是数字!'); | |||
| this.setAttribute('number', number); | |||
| this.#number = number; | |||
| } | |||
| get type() { this.#type; } | |||
| /** | |||
| * @param {string} x | |||
| */ | |||
| set type(x) { | |||
| this.setAttribute('type', x); | |||
| this.#type = x; | |||
| @@ -154,24 +158,48 @@ class PadIcon extends HTMLElement { | |||
| this.update(); | |||
| } | |||
| attributeChangedCallback(name, oldValue, newValue) { //自定义标签属性改变 | |||
| if (name == 'iid') this.#iid = parseInt(newValue); | |||
| if (name == 'number') { | |||
| const number = Number(newValue); | |||
| this.#number = Number.isNaN(number) ? 0 : number; | |||
| } | |||
| if (name == 'type') this.#type = newValue; | |||
| this.update(); | |||
| } | |||
| update() { | |||
| let iid = this.#iid || 0; | |||
| let number = this.#number; | |||
| const type = this.#type; | |||
| const lang = this.getAttribute('lang') || currentLanguage.i18n; | |||
| const shadow = this.shadowRoot; | |||
| const use = shadow.querySelector('use'); | |||
| const svg = shadow.querySelector('svg'); | |||
| const use = svg.querySelector('use'); | |||
| switch (type) { | |||
| case 'awoken': { | |||
| if (/^(?:en|ko)/.test(lang) && [40,46,47,48].includes(iid)) iid += '-en'; //英文不一样的觉醒 | |||
| if (/^(?:zh)/.test(lang) && [46,47].includes(iid)) iid += '-zh'; //中文不一样的觉醒 | |||
| use.setAttribute("href",`images/icon-awoken.svg#awoken-${iid}`); | |||
| if (/^(?:en|ko)/.test(lang) && [40,46,47,48].includes(number)) number += '-en'; //英文不一样的觉醒 | |||
| if (/^(?:zh)/.test(lang) && [46,47].includes(number)) number += '-zh'; //中文不一样的觉醒 | |||
| use.setAttribute("href",`images/icon-awoken.svg#awoken-${number}`); | |||
| break; | |||
| } | |||
| case 'type': { | |||
| if (/^(?:en|ko)/.test(lang) && [9,12].includes(number)) number += '-en'; //英文不一样的类型 | |||
| use.setAttribute("href",`images/icon-type.svg#type-${number}`); | |||
| break; | |||
| } | |||
| case 'awoken-count': { | |||
| use.setAttribute("href",`images/icon-awoken-count.svg#awoken-count-bg`); | |||
| const text = svg.appendChild(document.createElementNS(svgNS, 'text')); | |||
| const iconName = this.getAttribute('icon-name'); | |||
| svg.setAttribute("icon-name", iconName); | |||
| //awoken,latent,8-latent | |||
| //full,enable-assist-full,latent-full,8-latent,8-latent-full | |||
| const full = this.getAttribute('full') != null; | |||
| const special = this.getAttribute('special') != null; | |||
| text.textContent = full ? '★' : number; | |||
| text.setAttribute("x", "50%"); | |||
| text.setAttribute("y", "50%"); | |||
| text.setAttribute("class", "number"); | |||
| break; | |||
| } | |||
| case 'type': | |||
| case 'latent': | |||
| case 'badge': | |||
| case 'attr': | |||