diff --git a/package-lock.json b/package-lock.json index 2c751d3..e0b1e09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,11 +25,11 @@ "gray-matter": "^4.0.3", "kaitai-struct": "^0.9.0", "marked": "^4.0.10", + "mii-js": "github:PretendoNetwork/mii-js#v1.0.4", "mongoose": "^6.4.0", "morgan": "^1.10.0", "nodemailer": "^6.7.5", "stripe": "^9.9.0", - "trello": "^0.11.0", "uuid": "^8.3.2" }, "devDependencies": { @@ -576,6 +576,11 @@ "node": ">= 0.8" } }, + "node_modules/bit-buffer": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/bit-buffer/-/bit-buffer-0.2.5.tgz", + "integrity": "sha512-x1yGnmXvFg6e3DiyRztElbcn1bsCTFSoM/ncAzY62uE0JdTl5xlKJd0ooqLYoPbhdsnpehSIQrdIvclcZJYwiA==" + }, "node_modules/bn.js": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", @@ -1489,11 +1494,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es6-promise": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.0.2.tgz", - "integrity": "sha1-AQ1YWEI6XxGJeWZfRkhqlcbuK7Y=" - }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -2915,6 +2915,14 @@ "node": ">= 0.6" } }, + "node_modules/mii-js": { + "version": "1.0.4", + "resolved": "git+ssh://git@github.com/PretendoNetwork/mii-js.git#5d8eb8013514a13b0df6eb4a5bfd8b5a63fb9861", + "license": "GNU AFFERO GPLV3", + "dependencies": { + "bit-buffer": "^0.2.5" + } + }, "node_modules/miller-rabin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", @@ -3661,33 +3669,6 @@ "lowercase-keys": "^2.0.0" } }, - "node_modules/restler": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/restler/-/restler-3.3.0.tgz", - "integrity": "sha512-TrnPEY3DJgFJgPd5vJmHpOj0WlAzLcX2eIcAqc/EB/YlgPFz0aMD55GkQN16d3nX0ydlq93lthZWGKyBpVnFEg==", - "dependencies": { - "iconv-lite": "0.2.11", - "qs": "1.2.0", - "xml2js": "0.4.0", - "yaml": "0.2.3" - }, - "engines": { - "node": ">= 0.10.x" - } - }, - "node_modules/restler/node_modules/iconv-lite": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", - "integrity": "sha1-HOYKOleGSiktEyH/RgnKS7llrcg=", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/restler/node_modules/qs": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-1.2.0.tgz", - "integrity": "sha512-XUf0O7rlGjbH+n7uqyT+xn362fmoPe4ehtHL6VK1nbSgQ7CqG0ZZLr1nU2EyXlRq++YphPdQ/5scjIWNMSPnhg==" - }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -3734,11 +3715,6 @@ "node": ">=6" } }, - "node_modules/sax": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/sax/-/sax-0.5.8.tgz", - "integrity": "sha1-1HLbIo6zMcJQaw6MFVJK25OdEsE=" - }, "node_modules/section-matter": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", @@ -4274,19 +4250,6 @@ "node": ">=12" } }, - "node_modules/trello": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/trello/-/trello-0.11.0.tgz", - "integrity": "sha512-WMjfSTA+ybGnd36ZoMX7Ia7wfpU9KP2lsafv9mjLwFcE1ECp8b9/rX3uW4ivNO7jVLnHRyQ1pHsirbMvK0Ix2Q==", - "dependencies": { - "es6-promise": "~3.0.2", - "object-assign": "~4.1.0", - "restler": "~3.3.0" - }, - "engines": { - "node": ">= 0.10.x" - } - }, "node_modules/tslib": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", @@ -4578,23 +4541,6 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, - "node_modules/xml2js": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.0.tgz", - "integrity": "sha1-Ek/EEUtBKcgQgA7LKshs8lRiy5o=", - "dependencies": { - "sax": "0.5.x", - "xmlbuilder": ">=0.4.2" - } - }, - "node_modules/xmlbuilder": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", - "engines": { - "node": ">=8.0" - } - }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -4608,14 +4554,6 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true - }, - "node_modules/yaml": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-0.2.3.tgz", - "integrity": "sha1-tUUOkudu82td0k42YAkeuu7z5cc=", - "engines": { - "node": "*" - } } }, "dependencies": { @@ -5045,6 +4983,11 @@ "safe-buffer": "5.1.2" } }, + "bit-buffer": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/bit-buffer/-/bit-buffer-0.2.5.tgz", + "integrity": "sha512-x1yGnmXvFg6e3DiyRztElbcn1bsCTFSoM/ncAzY62uE0JdTl5xlKJd0ooqLYoPbhdsnpehSIQrdIvclcZJYwiA==" + }, "bn.js": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", @@ -5812,11 +5755,6 @@ "is-symbol": "^1.0.2" } }, - "es6-promise": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.0.2.tgz", - "integrity": "sha1-AQ1YWEI6XxGJeWZfRkhqlcbuK7Y=" - }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -6869,6 +6807,13 @@ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" }, + "mii-js": { + "version": "git+ssh://git@github.com/PretendoNetwork/mii-js.git#5d8eb8013514a13b0df6eb4a5bfd8b5a63fb9861", + "from": "mii-js@github:PretendoNetwork/mii-js#v1.0.4", + "requires": { + "bit-buffer": "^0.2.5" + } + }, "miller-rabin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", @@ -7450,29 +7395,6 @@ "lowercase-keys": "^2.0.0" } }, - "restler": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/restler/-/restler-3.3.0.tgz", - "integrity": "sha512-TrnPEY3DJgFJgPd5vJmHpOj0WlAzLcX2eIcAqc/EB/YlgPFz0aMD55GkQN16d3nX0ydlq93lthZWGKyBpVnFEg==", - "requires": { - "iconv-lite": "0.2.11", - "qs": "1.2.0", - "xml2js": "0.4.0", - "yaml": "0.2.3" - }, - "dependencies": { - "iconv-lite": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", - "integrity": "sha1-HOYKOleGSiktEyH/RgnKS7llrcg=" - }, - "qs": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-1.2.0.tgz", - "integrity": "sha512-XUf0O7rlGjbH+n7uqyT+xn362fmoPe4ehtHL6VK1nbSgQ7CqG0ZZLr1nU2EyXlRq++YphPdQ/5scjIWNMSPnhg==" - } - } - }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -7510,11 +7432,6 @@ "sparse-bitfield": "^3.0.3" } }, - "sax": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/sax/-/sax-0.5.8.tgz", - "integrity": "sha1-1HLbIo6zMcJQaw6MFVJK25OdEsE=" - }, "section-matter": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", @@ -7932,16 +7849,6 @@ "punycode": "^2.1.1" } }, - "trello": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/trello/-/trello-0.11.0.tgz", - "integrity": "sha512-WMjfSTA+ybGnd36ZoMX7Ia7wfpU9KP2lsafv9mjLwFcE1ECp8b9/rX3uW4ivNO7jVLnHRyQ1pHsirbMvK0Ix2Q==", - "requires": { - "es6-promise": "~3.0.2", - "object-assign": "~4.1.0", - "restler": "~3.3.0" - } - }, "tslib": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", @@ -8166,20 +8073,6 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, - "xml2js": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.0.tgz", - "integrity": "sha1-Ek/EEUtBKcgQgA7LKshs8lRiy5o=", - "requires": { - "sax": "0.5.x", - "xmlbuilder": ">=0.4.2" - } - }, - "xmlbuilder": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==" - }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -8190,11 +8083,6 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true - }, - "yaml": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-0.2.3.tgz", - "integrity": "sha1-tUUOkudu82td0k42YAkeuu7z5cc=" } } } diff --git a/package.json b/package.json index e89df07..e3896dd 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "gray-matter": "^4.0.3", "kaitai-struct": "^0.9.0", "marked": "^4.0.10", + "mii-js": "github:PretendoNetwork/mii-js#v1.0.4", "mongoose": "^6.4.0", "morgan": "^1.10.0", "nodemailer": "^6.7.5", diff --git a/public/assets/css/account.css b/public/assets/css/account.css index dbfbf68..6852c5c 100644 --- a/public/assets/css/account.css +++ b/public/assets/css/account.css @@ -76,12 +76,34 @@ border-color: #5AFF15; } -.account-sidebar .user .mii { +.account-sidebar .user a.mii { + position: relative; + display: block; width: 128px; height: 128px; + overflow: hidden; border-radius: 100%; background: var(--bg-shade-3); } +.account-sidebar .user a.mii::after { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: no-repeat center/40% url("/assets/images/edit.svg"), rgba(55, 60, 101, 0.7); + opacity: 0; + transition: opacity 150ms; +} +.account-sidebar .user a.mii:hover::after { + opacity: 1; +} + +.account-sidebar .user .mii { + width: 100%; + height: 100%; +} .account-sidebar .buttons a { display: flex; flex-flow: column; diff --git a/public/assets/css/miieditor.css b/public/assets/css/miieditor.css index f7986e8..eb99b14 100644 --- a/public/assets/css/miieditor.css +++ b/public/assets/css/miieditor.css @@ -28,14 +28,14 @@ svg.logotype { max-width: 1920px; height: 100%; margin: auto; - gap: 0 48px; + gap: 0 120px; } .params-wrapper::before { content: ""; display: block; position: absolute; background: var(--bg-shade-1); - border-radius: 100%; + border-radius: 100% 0 0 100%; width: 1300px; height: 1700px; top: 50%; @@ -59,23 +59,23 @@ svg.logotype { z-index: -1; } -div.mii-img-wrapper { +.canvas-wrapper { position: relative; - margin: auto; display: flex; justify-content: center; - width: 512px; - left: -48px; + align-items: center; + width: 100%; height: 100%; - max-height: 384px; + overflow: hidden; } -img#mii-img { - position: absolute; - bottom: 0; + +canvas#miiCanvas { width: auto; - height: 512px; - z-index: 2; + height: auto; + transform-origin: center; + transition: transform 200ms, filter 200ms; } + div.mii-img-wrapper::before { content: ""; position: absolute; @@ -190,6 +190,41 @@ div.subtabs .subtabbtn.active:hover::before { transform: scale(3); } +.has-textinput { + grid-template-columns: 1fr 1fr; + grid-template-rows: auto; + grid-auto-rows: auto; + gap: 24px; +} +.has-textinput label { + display: block; + margin-bottom: 6px; + text-transform: uppercase; + font-size: 12px; +} + +.has-textinput .icons { + display: flex; + flex-flow: row nowrap; + justify-content: space-between; +} +.has-textinput .icons input[type="checkbox"] { + box-sizing: border-box; + margin: 0; + height: 49px; + width: 49px; +} + +input[type="checkbox"]#allowCopying:checked { + background: no-repeat center/80% url(../images/copy.svg), var(--accent-shade-0); +} +input[type="checkbox"]#disableSharing:checked { + background: no-repeat center/80% url(../images/share.svg), var(--accent-shade-0); +} +input[type="checkbox"]#favorite:checked { + background: no-repeat center/80% url(../images/star.svg), var(--accent-shade-0); +} + form.params { grid-template-columns: repeat(2, auto); height: 618px; @@ -418,10 +453,6 @@ button * { gap: 12px; } - div.mii-img-wrapper { - left: -36px; - } - div.params-wrapper { margin-right: 24px; } @@ -447,10 +478,6 @@ button * { gap: 8px; } - div.mii-img-wrapper { - left: -36px; - } - div.params-wrapper { margin-right: 24px; } @@ -497,17 +524,16 @@ button * { } @media screen and (max-width: 1080px) { - div.mii-img-wrapper { - left: 0; - top: 0; - } - img#mii-img { - max-height: 150%; + .canvas-wrapper { + height: calc(100% - 12px); } svg.logotype { left: 0; } + svg.logotype text#Pretendo { + display: none; + } .miieditor-wrapper { grid-template-columns: auto; @@ -529,11 +555,6 @@ button * { margin-top: 1rem; } - div.mii-img-wrapper { - width: 100%; - max-width: 512px; - } - fieldset { order: 1; } @@ -593,7 +614,7 @@ button * { grid-row: 4; } - fieldset:not(.has-sliders) { + fieldset:not(.has-sliders, .has-textinput) { position: relative; margin-bottom: -60px; } @@ -648,6 +669,15 @@ button * { width: 36px; height: 36px; } + + div.pagination { + transform: scale(0.7); + transform-origin: right; + } + + .has-textinput { + grid-template-columns: 1fr; + } } @media screen and (max-width: 360px) { diff --git a/public/assets/images/copy.svg b/public/assets/images/copy.svg new file mode 100644 index 0000000..67e47f7 --- /dev/null +++ b/public/assets/images/copy.svg @@ -0,0 +1 @@ + diff --git a/public/assets/images/edit.svg b/public/assets/images/edit.svg new file mode 100644 index 0000000..d19401d --- /dev/null +++ b/public/assets/images/edit.svg @@ -0,0 +1 @@ + diff --git a/public/assets/images/miieditor.svg b/public/assets/images/miieditor.svg index 27724a6..0ce9d34 100644 --- a/public/assets/images/miieditor.svg +++ b/public/assets/images/miieditor.svg @@ -27,11 +27,11 @@ inkscape:deskcolor="#d1d1d1" showgrid="false" inkscape:lockguides="false" - inkscape:zoom="5.979798" - inkscape:cx="65.888513" - inkscape:cy="316.23142" + inkscape:zoom="5.6568543" + inkscape:cx="104.56341" + inkscape:cy="351.43207" inkscape:window-width="1920" - inkscape:window-height="1023" + inkscape:window-height="1025" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" @@ -75,7 +75,7 @@ d="m 38.391224,328.78155 c -0.402278,0.001 -0.847854,0.0668 -1.343327,0.20523 a 0.43421497,0.4342385 0 0 0 -0.041,0.0153 c -0.576068,0.22485 -0.849154,0.72713 -0.944648,1.11432 -0.095,0.3872 -0.053,0.72084 -0.053,0.72084 a 0.43421497,0.4342385 0 0 0 0.642766,0.32564 c 1.156837,-0.6469 1.853099,-0.71378 2.39647,-0.61567 0.54327,0.0981 0.981247,0.4064 1.575614,0.64112 a 0.43421497,0.4342385 0 0 0 0.537671,-0.61568 c -0.252786,-0.45231 -0.562569,-0.89979 -1.012545,-1.23983 -0.450075,-0.34005 -1.039843,-0.5546 -1.758805,-0.55123 z m 7.218009,0 c -0.718861,-0.004 -1.308729,0.21119 -1.758704,0.55123 -0.450076,0.34005 -0.759759,0.78753 -1.012546,1.23982 a 0.43421497,0.4342385 0 0 0 0.537671,0.61568 c 0.594368,-0.2347 1.032244,-0.54301 1.575615,-0.64111 0.54327,-0.0981 1.239633,-0.0313 2.39647,0.61567 a 0.43421497,0.4342385 0 0 0 0.642665,-0.32565 c 0,0 0.043,-0.33363 -0.053,-0.72083 -0.09499,-0.38719 -0.36858,-0.88946 -0.944649,-1.11432 a 0.43421497,0.4342385 0 0 0 -0.041,-0.0153 c -0.495374,-0.13835 -0.941049,-0.20326 -1.343228,-0.20522 z" /> + + + + + + + diff --git a/public/assets/images/share.svg b/public/assets/images/share.svg new file mode 100644 index 0000000..23fa1fb --- /dev/null +++ b/public/assets/images/share.svg @@ -0,0 +1 @@ + diff --git a/public/assets/images/star.svg b/public/assets/images/star.svg new file mode 100644 index 0000000..d035a75 --- /dev/null +++ b/public/assets/images/star.svg @@ -0,0 +1 @@ + diff --git a/public/assets/js/MiiClass.js b/public/assets/js/MiiClass.js deleted file mode 100644 index 9a097cb..0000000 --- a/public/assets/js/MiiClass.js +++ /dev/null @@ -1,214 +0,0 @@ -// Fetched from https://github.com/PretendoNetwork/account/blob/master/src/mii.js on 2021-12-16 - -const KaitaiStream = require('kaitai-struct/KaitaiStream'); - -class Mii extends KaitaiStream { - constructor(arrayBuffer, byteOffset) { - super(arrayBuffer, byteOffset); - - this.decode(); - } - - decode() { - // Decode raw data - // A lot of this goes unused - this.unknown1 = this.readU1(); - this.characterSet = this.readBitsIntBe(2); - this.regionLock = this.readBitsIntBe(2); - this.profanityFlag = this.readBitsIntBe(1) !== 0; - this.copying = this.readBitsIntBe(1) !== 0; - this.unknown2 = this.readBitsIntBe(2); - this.slotIndex = this.readBitsIntBe(4); - this.pageIndex = this.readBitsIntBe(4); - this.version = this.readBitsIntBe(4); - this.unknown3 = this.readBitsIntBe(4); - this.systemId = Array(8).fill().map(() => this.readU1()); - this.avatarId = Array(4).fill().map(() => this.readU1()); - this.clientId = Array(6).fill().map(() => this.readU1()); - this.padding = this.readU2le(); - this.miiMetaData = this.readU2le(); - this.miiName = Buffer.from(this.readBytes(20)).toString('utf16le'); - this.height = this.readU1(); - this.build = this.readU1(); - this.faceColor = this.readBitsIntBe(3); - this.faceType = this.readBitsIntBe(4); - this.mingle = this.readBitsIntBe(1) !== 0; - this.faceMakeup = this.readBitsIntBe(4); - this.faceWrinkles = this.readBitsIntBe(4); - this.alignToByte(); - this.hairType = this.readU1(); - this.unknown5 = this.readBitsIntBe(4); - this.hairFlip = this.readBitsIntBe(1) !== 0; - this.hairColor = this.readBitsIntBe(3); - this.alignToByte(); - this.eyeData = this.readU4le(); - this.eyebrowData = this.readU4le(); - this.noseData = this.readU2le(); - this.mouthData = this.readU2le(); - this.mouthData2 = this.readU2le(); - this.facialHairData = this.readU2le(); - this.glassesData = this.readU2le(); - this.moleData = this.readU2le(); - this.creatorName = Buffer.from(this.readBytes(20)).toString('utf16le'); - this.padding2 = this.readU2le(); - this.checksum = this.readU2le(); - - // Carve out more specific data from the above values - // TODO: read these bits directly instead of getting them later - - this.gender = (this.miiMetaData & 1); - this.birthMonth = ((this.miiMetaData >> 1) & 15); - this.birthDay = ((this.miiMetaData >> 5) & 31); - this.favoriteColor = ((this.miiMetaData >> 10) & 15); - this.favorite = ((this.miiMetaData >> 14) & 1); - - this.eyeType = (this.eyeData & 63); - this.eyeColor = ((this.eyeData >> 6) & 7); - this.eyeSize = ((this.eyeData >> 9) & 7); - this.eyeStretch = ((this.eyeData >> 13) & 7); - this.eyeRotation = ((this.eyeData >> 16) & 31); - this.eyeHorizontal = ((this.eyeData >> 21) & 15); - this.eyeVertical = ((this.eyeData >> 25) & 31); - - this.eyebrowType = (this.eyebrowData & 31); - this.eyebrowColor = ((this.eyebrowData >> 5) & 7); - this.eyebrowSize = ((this.eyebrowData >> 8) & 15); - this.eyebrowStretch = ((this.eyebrowData >> 12) & 7); - this.eyebrowRotation = ((this.eyebrowData >> 16) & 15); - this.eyebrowHorizontal = ((this.eyebrowData >> 21) & 15); - this.eyebrowVertical = ((this.eyebrowData >> 25) & 31); - - this.noseType = (this.noseData & 31); - this.noseSize = ((this.noseData >> 5) & 15); - this.noseVertical = ((this.noseData >> 9) & 31); - - - this.mouthType = (this.mouthData & 63); - this.mouthColor = ((this.mouthData >> 6) & 7); - this.mouthSize = ((this.mouthData >> 9) & 15); - this.mouthStretch = ((this.mouthData >> 13) & 7); - - this.mouthVertical = (this.mouthData2 & 31); - this.facialHairMustache = ((this.mouthData2 >> 5) & 7); - - this.facialHairType = (this.facialHairData & 7); - this.facialHairColor = ((this.facialHairData >> 3) & 7); - this.facialHairSize = ((this.facialHairData >> 6) & 15); - this.facialHairVertical = ((this.facialHairData >> 10) & 31); - - this.glassesType = (this.glassesData & 15); - this.glassesColor = (this.glassesData >> 4) & 7; - this.glassesSize = (this.glassesData >> 7) & 15; - this.glassesVertical = (this.glassesData >> 11) & 15; - - this.moleEnable = (this.moleData >> 15); - this.moleSize = ((this.moleData >> 1) & 15); - this.moleHorizontal = ((this.moleData >> 5) & 31); - this.moleVertical = ((this.moleData >> 10) & 31); - } - - toStudioMii() { - /* - Can also disable randomization with: - let miiStudioData = Buffer.alloc(0x2F); - let next = 256; - and removing "randomizer" and the "miiStudioData.writeUInt8(randomizer);" call - */ - const miiStudioData = Buffer.alloc(0x2F); - const randomizer = Math.floor(256 * Math.random()); - let next = randomizer; - let pos = 1; - - function encodeMiiPart(partValue) { - const encoded = (7 + (partValue ^ next)) % 256; - next = encoded; - - miiStudioData.writeUInt8(encoded, pos); - pos++; - } - - miiStudioData.writeUInt8(randomizer); - - if (this.facialHairColor === 0) { - encodeMiiPart(8); - } else { - encodeMiiPart(this.facialHairColor); - } - - encodeMiiPart(this.facialHairType); - encodeMiiPart(this.build); - encodeMiiPart(this.eyeStretch); - encodeMiiPart(this.eyeColor + 8); - encodeMiiPart(this.eyeRotation); - encodeMiiPart(this.eyeSize); - encodeMiiPart(this.eyeType); - encodeMiiPart(this.eyeHorizontal); - encodeMiiPart(this.eyeVertical); - encodeMiiPart(this.eyebrowStretch); - - if (this.eyebrowColor === 0) { - encodeMiiPart(8); - } else { - encodeMiiPart(this.eyebrowColor); - } - - encodeMiiPart(this.eyebrowRotation); - encodeMiiPart(this.eyebrowSize); - encodeMiiPart(this.eyebrowType); - encodeMiiPart(this.eyebrowHorizontal); - encodeMiiPart(this.eyebrowVertical); - encodeMiiPart(this.faceColor); - encodeMiiPart(this.faceMakeup); - encodeMiiPart(this.faceType); - encodeMiiPart(this.faceWrinkles); - encodeMiiPart(this.favoriteColor); - encodeMiiPart(this.gender); - - if (this.glassesColor == 0) { - encodeMiiPart(8); - } else if (this.glassesColor < 6) { - encodeMiiPart(this.glassesColor + 13); - } else { - encodeMiiPart(0); - } - - encodeMiiPart(this.glassesSize); - encodeMiiPart(this.glassesType); - encodeMiiPart(this.glassesVertical); - - if (this.hairColor == 0) { - encodeMiiPart(8); - } else { - encodeMiiPart(this.hairColor); - } - - encodeMiiPart(this.hairFlip ? 1 : 0); - encodeMiiPart(this.hairType); - encodeMiiPart(this.height); - encodeMiiPart(this.moleSize); - encodeMiiPart(this.moleEnable); - encodeMiiPart(this.moleHorizontal); - encodeMiiPart(this.moleVertical); - encodeMiiPart(this.mouthStretch); - - if (this.mouthColor < 4) { - encodeMiiPart(this.mouthColor + 19); - } else { - encodeMiiPart(0); - } - - encodeMiiPart(this.mouthSize); - encodeMiiPart(this.mouthType); - encodeMiiPart(this.mouthVertical); - encodeMiiPart(this.facialHairSize); - encodeMiiPart(this.facialHairMustache); - encodeMiiPart(this.facialHairVertical); - encodeMiiPart(this.noseSize); - encodeMiiPart(this.noseType); - encodeMiiPart(this.noseVertical); - - return miiStudioData; - } -} - -module.exports = Mii; diff --git a/public/assets/js/miieditor.js b/public/assets/js/miieditor.js index 928510d..c431fe0 100644 --- a/public/assets/js/miieditor.js +++ b/public/assets/js/miieditor.js @@ -1,4 +1,4 @@ -// This file gets automatically bundled with browserify when running the start script. This also means that after any update you're gonna need to restart the server. +// This file gets automatically bundled with browserify when running the start script. This also means that after any update you're gonna need to restart the server to see any changes. // Prevent the user from reloading or leaving the page window.addEventListener('beforeunload', function (e) { @@ -6,79 +6,233 @@ window.addEventListener('beforeunload', function (e) { e.returnValue = ''; }); -const Mii = require('./MiiClass.js'); +// this makes it so the canvas fits in the target element +function setCanvasScale() { + let targetX; + let targetY; + + if (window.innerWidth <= 1080) { + const canvasWrapper = document.querySelector('.canvas-wrapper'); + + targetX = canvasWrapper.offsetWidth; + targetY = canvasWrapper.offsetHeight; + } else { + targetX = window.innerWidth * 0.9; + targetY = window.innerHeight * 0.9; + } + + const canvas = document.querySelector('canvas#miiCanvas'); + const XScale = targetX / canvas.width; + const YScale = targetY / canvas.height; + canvas.style.transform = `scale(${Math.min(XScale, YScale)})`; +} + +setCanvasScale(); +window.addEventListener('resize', () => { + setCanvasScale(); +}); // MII RENDERER +const Mii = require('mii-js'); + // The Mii data is stored in a script tag in the HTML, so we can just grab it and then remove the element const encodedUserMiiData = document.querySelector( 'script#encodedUserMiiData' ).textContent; document.querySelector('script#encodedUserMiiData').remove(); -// We initialize the Mii object with the encoded data and render the Mii +console.log('encodedMiiData', encodedUserMiiData); + +// We initialize the Mii object const mii = new Mii(Buffer.from(encodedUserMiiData, 'base64')); + +// We set the img sources for the unedited miis in the save animation +const miiStudioNeutralUrl = mii.studioUrl({ + width: 512, + bgColor: '13173300', +}); +const miiStudioSorrowUrl = mii.studioUrl({ + width: 512, + bgColor: '13173300', + expression: 'sorrow', +}); +document.querySelector('.mii-comparison img.old-mii').src = miiStudioNeutralUrl; +document.querySelector('.mii-comparison.confirmed img.old-mii').src = + miiStudioSorrowUrl; + +// we keeep the images here so we can cache them when we need to change the build/height +const miiFaceImg = new Image(); +const baldMiiFaceImg = new Image(); +const miiBodyImg = new Image(); + +// Initial mii render renderMii(); +// This function renders the Mii on the canvas +function renderMii(heightOverride, buildOverride) { + const canvas = document.querySelector('canvas#miiCanvas'); + const ctx = canvas.getContext('2d'); + const height = heightOverride || mii.height; + const build = buildOverride || mii.build; -const oldMiiStudioData = mii.toStudioMii().toString('hex'); + // if there isn't an override or the images haven't been cached, we load the images + if ((!heightOverride && !buildOverride) || !miiFaceImg.src || !baldMiiFaceImg.src || !miiBodyImg.src) { + canvas.style.filter = 'blur(4px) brightness(70%)'; -// initial setup for saving animation -document.querySelector('.mii-comparison img.old-mii').src = `https://studio.mii.nintendo.com/miis/image.png?data=${oldMiiStudioData}&type=face&expression=normal&width=512&bgColor=13173300`; -document.querySelector('.mii-comparison.confirmed img.old-mii').src = `https://studio.mii.nintendo.com/miis/image.png?data=${oldMiiStudioData}&type=face&expression=sorrow&width=512&bgColor=13173300`; + // we create a copy of the mii and make it bald + const baldMii = Object.create( + Object.getPrototypeOf(mii), + Object.getOwnPropertyDescriptors(mii) + ); + baldMii.hairType = 30; + baldMiiFaceImg.src = baldMii.studioUrl({ + width: 512, + bgColor: '13173300', + type: 'face_only', + }); + miiFaceImg.src = mii.studioUrl({ + width: 512, + bgColor: '13173300', + type: 'face_only', + }); + miiBodyImg.src = mii.studioAssetUrlBody(); + } -// This function renders the Mii on the page -function renderMii(type) { - type = type || 'all_body'; // Can be 'all_body' or 'face' + // misc calculations + const bodyWidth = (build * 1.7 + 220) * (0.003 * height + 0.6); + const bodyHeight = height * 3.5 + 227; + const bodyXPos = (canvas.width - bodyWidth) / 2; + const bodyYPos = canvas.height - bodyHeight; + const headYPos = bodyYPos - 408; - const miiStudioData = mii.toStudioMii().toString('hex'); - document.querySelector( - 'img#mii-img' - ).src = `https://studio.mii.nintendo.com/miis/image.png?data=${miiStudioData}&type=${type}&expression=normal&width=512&bgColor=13173300`; + // we make sure every image is loaded before rendering + if (miiFaceImg.complete) { + onMiiFaceImgLoad(); + } else { + miiFaceImg.onload = () => { + onMiiFaceImgLoad(); + }; + } + function onMiiFaceImgLoad() { + if (miiBodyImg.complete) { + onBodyImgLoad(); + } else { + miiBodyImg.onload = () => { + onBodyImgLoad(); + }; + } + } + function onBodyImgLoad() { + if (baldMiiFaceImg.complete) { + onBaldMiiFaceImgLoad(); + } else { + baldMiiFaceImg.onload = () => { + onBaldMiiFaceImgLoad(); + }; + } + } + function onBaldMiiFaceImgLoad() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.drawImage(miiFaceImg, 0, headYPos); + ctx.drawImage(miiBodyImg, bodyXPos, bodyYPos, bodyWidth, bodyHeight); - // sets the new mii in the save tab to the new mii - document.querySelector( - '.mii-comparison img.new-mii' - ).src = `https://studio.mii.nintendo.com/miis/image.png?data=${miiStudioData}&type=face&expression=normal&width=512&bgColor=13173300`; - document.querySelector( - '.mii-comparison.confirmed img.new-mii' - ).src = `https://studio.mii.nintendo.com/miis/image.png?data=${miiStudioData}&type=face&expression=smile&width=512&bgColor=13173300`; + // we draw a portion of the bald mii on top of the normal mii to hide the mii's neck (see https://i.imgur.com/U0fpkwi.png) + ctx.drawImage(baldMiiFaceImg, 196, 384, 120, 120, 196, headYPos + 384, 120, 120); + canvas.style.filter = ''; + } - // this sets the mii height so that the face width stays the same - document.querySelector('img#mii-img').style.height = `${ - mii.height * mii.height * 0.0023 + mii.height * 1.058 + 384 - }px`; + if (!heightOverride && !buildOverride) { + const faceMiiStudioUrl = mii.studioUrl({ + width: 512, + bgColor: '13173300', + }); - // this sets the bottom position so that the mii's feet stay in the same position - document.querySelector('img#mii-img').style.bottom = `${ - mii.height * mii.height * -0.00468135 + mii.height * 0.28 - 0.052435 - }px`; + const faceMiiStudioSmileUrl = mii.studioUrl({ + width: 512, + bgColor: '13173300', + expression: 'smile' + }); - console.log(mii); + // sets the new mii in the save tab to the new mii + document.querySelector('.mii-comparison img.new-mii').src = faceMiiStudioUrl; + document.querySelector('.mii-comparison.confirmed img.new-mii').src = faceMiiStudioSmileUrl; + } } // This function updates a prop of the Mii and rerenders it -function updateMii(e, type) { +function updateMii(e) { const prop = e.target.name; - const value = e.target.value; - mii[prop] = parseInt(value); - renderMii(type); + let value = e.target.value || e.target.defaultValue; + + // if the value comes from a checkbox, we use the checked property + if (value === 'on' || value === 'off') { + value = e.target.checked; + } + + // if the prop is disableSharing, we set the value to the opposite of the current value + if (prop === 'disableSharing') { + value = !value; + } + + // Handle booleans, on/offs and strings + if (value === 'true' || value === 'false') { + mii[prop] = value === 'true'; + } else if (value === 'on' || value === 'off') { + mii[prop] = value === 'on'; + } else if (isNaN(parseInt(value))) { + mii[prop] = value; + } else { + mii[prop] = parseInt(value); + } + + // if the user is editing the height or the build, we render the mii with the correct override, else we do a straight render + if (prop === 'height') { + renderMii(value, false); + } else if (prop === 'build') { + renderMii(false, value); + } else { + renderMii(); + console.log(mii); + } +} + +function handleCalendar(e) { + const valueArray = e.target.value.split('-'); + const day = valueArray[2]; + const month = valueArray[1]; + + mii.birthDay = parseInt(day); + mii.birthMonth = parseInt(month); +} + +function preventEmpty(e) { + if (e.target.value !== '') return; + + e.target.value = e.target.defaultValue; } document.querySelectorAll('fieldset').forEach((fieldset) => { - fieldset.addEventListener('change', (e) => updateMii(e)); + fieldset.addEventListener('change', updateMii); }); - -document.querySelectorAll('input[type=\'range\']').forEach((fieldset) => { - fieldset.addEventListener('input', (e) => updateMii(e)); +document.querySelectorAll('input[type=\'range\']').forEach((input) => { + input.addEventListener('input', updateMii); }); +document + .querySelectorAll('input[type=\'text\'], input[type=\'number\']') + .forEach((input) => { + input.addEventListener('blur', preventEmpty); + }); +document + .querySelector('input[type=\'date\']#birthDate') + .addEventListener('change', handleCalendar); // FORM // Here we preselect the options corresponding to the Mii's current values [ 'faceType', - 'faceColor', - 'faceMakeup', - 'faceWrinkles', + 'skinColor', + 'makeupType', + 'wrinklesType', 'hairType', 'hairColor', 'eyebrowType', @@ -90,45 +244,70 @@ document.querySelectorAll('input[type=\'range\']').forEach((fieldset) => { 'mouthColor', 'glassesType', 'glassesColor', - 'facialHairType', + 'beardType', 'facialHairColor', - 'facialHairMustache', - 'moleEnable', + 'mustacheType', + 'moleEnabled', 'gender', - 'favoriteColor' + 'favoriteColor', ].forEach((prop) => { - document.querySelector(`#${prop}${mii[prop]}`).checked = true; + const el = document.querySelector(`#${prop}${mii[prop]}`); + if (el) { + el.checked = true; + } + console.log(`[info] preselected value for ${prop}`); }); +['favorite', 'allowCopying'].forEach((prop) => { + const el = document.querySelector(`#${prop}`); + if (el) { + el.checked = mii[prop]; + } + console.log(`[info] preselected value for ${prop}`); +}); + +document.querySelector('#disableSharing').checked = !mii.disableSharing; +console.log('[info] preselected value for disableSharing'); + [ - 'eyebrowVertical', - 'eyebrowHorizontal', + 'eyebrowYPosition', + 'eyebrowSpacing', 'eyebrowRotation', - 'eyebrowSize', - 'eyebrowStretch', - 'eyeVertical', - 'eyeHorizontal', + 'eyebrowScale', + 'eyebrowVerticalStretch', + 'eyeYPosition', + 'eyeSpacing', 'eyeRotation', - 'eyeSize', - 'eyeStretch', - 'noseVertical', - 'noseSize', - 'mouthVertical', - 'mouthSize', - 'mouthStretch', - 'glassesVertical', - 'glassesSize', - 'facialHairVertical', - 'facialHairSize', - 'moleVertical', - 'moleHorizontal', - 'moleSize', + 'eyeScale', + 'eyeVerticalStretch', + 'noseYPosition', + 'noseScale', + 'mouthYPosition', + 'mouthScale', + 'mouthHorizontalStretch', + 'glassesYPosition', + 'glassesScale', + 'mustacheYPosition', + 'mustacheScale', + 'moleYPosition', + 'moleXPosition', + 'moleScale', 'height', - 'build' + 'build', + 'miiName', + 'creatorName', ].forEach((prop) => { document.querySelector(`#${prop}`).value = mii[prop]; + document.querySelector(`#${prop}`).defaultValue = mii[prop]; + console.log(`[info] preselected value for ${prop}`); }); +const paddedBirthDay = mii.birthDay.toString().padStart(2, '0'); +const paddedBirthMonth = mii.birthMonth.toString().padStart(2, '0'); +document.querySelector( + 'input[type=\'date\']#birthDate' +).value = `2024-${paddedBirthMonth}-${paddedBirthDay}`; +console.log('[info] preselected value for birthMonth && birthDay'); // TABS, SUBTABS, AND ALL THE INHERENT JANK @@ -136,19 +315,22 @@ function openTab(e, tabType) { e.preventDefault(); // Deselect all subpages - document.querySelectorAll('.subtab.has-subpages .subpage.active').forEach((el) => { - el.classList?.remove('active'); - }); + document + .querySelectorAll('.subtab.has-subpages .subpage.active') + .forEach((el) => { + el.classList?.remove('active'); + }); document.querySelectorAll('.subtab.active').forEach((el) => { el.classList?.remove('active'); }); - const buttonReplacement = tabType.charAt(0).toUpperCase() + tabType.slice(1); + const buttonReplacement = + tabType.charAt(0).toUpperCase() + tabType.slice(1); - document.querySelectorAll(`.${tabType}.active`).forEach(el => { + document.querySelectorAll(`.${tabType}.active`).forEach((el) => { el?.classList?.remove('active'); }); - document.querySelectorAll(`.${tabType}btn.active`).forEach(el => { + document.querySelectorAll(`.${tabType}btn.active`).forEach((el) => { el?.classList?.remove('active'); }); @@ -164,15 +346,15 @@ function openTab(e, tabType) { document.querySelector(`#${selectedID} .subtabbtn`)?.click(); } + setCanvasScale(); + // We hide all subpages document.querySelectorAll('.subpage').forEach((el) => { el.classList.remove('active'); }); // Selects the first subpage if there is one - document - .querySelector(`#${selectedID} .subpage`) - ?.classList?.add('active'); + document.querySelector(`#${selectedID} .subpage`)?.classList?.add('active'); } // Here we bind all of the functions to the corresponding buttons @@ -223,26 +405,44 @@ document.querySelectorAll('button.page-btn').forEach((el) => { }); // mii saving business (animation jank & actual saving) -document.querySelector('#saveTab #saveButton').addEventListener('click', (e) => { - e.preventDefault(); +document + .querySelector('#saveTab #saveButton') + .addEventListener('click', (e) => { + e.preventDefault(); - document.querySelector('#saveTab #saveButton').classList.add('inactive', 'fade-out'); - document.querySelector('.tabs').style.pointerEvents = 'none'; - document.querySelector('.mii-comparison.confirmed').style.opacity = 1; - document.querySelector('#saveTab p.save-prompt').classList.add('fade-out'); + document + .querySelector('#saveTab #saveButton') + .classList.add('inactive', 'fade-out'); + document.querySelector('.tabs').style.pointerEvents = 'none'; + document.querySelector('.mii-comparison.confirmed').style.opacity = 1; + document + .querySelector('#saveTab p.save-prompt') + .classList.add('fade-out'); - setTimeout(() => { - document.querySelector('.mii-comparison.unconfirmed').style.opacity = 0; - }, 500); + setTimeout(() => { + document.querySelector( + '.mii-comparison.unconfirmed' + ).style.opacity = 0; + }, 500); - setTimeout(() => { - document.querySelector('.mii-comparison.confirmed .old-mii').classList.add('fade-out'); - document.querySelector('.mii-comparison.confirmed svg').classList.add('fade-out'); - }, 1500); + setTimeout(() => { + document + .querySelector('.mii-comparison.confirmed .old-mii') + .classList.add('fade-out'); + document + .querySelector('.mii-comparison.confirmed svg') + .classList.add('fade-out'); + }, 1500); - setTimeout(() => { - document.querySelector('.mii-comparison.confirmed .new-mii-wrapper').classList.add('centered-mii-img'); - }, 2000); + setTimeout(() => { + document + .querySelector('.mii-comparison.confirmed .new-mii-wrapper') + .classList.add('centered-mii-img'); + }, 2000); + const miiData = mii.encode().toString('base64'); -}); + alert(miiData); + console.log('mii data:', miiData); + // CHECK IF MII IS VALID SERVERSIDE + }); diff --git a/views/account/account.handlebars b/views/account/account.handlebars index c94cbff..0110804 100644 --- a/views/account/account.handlebars +++ b/views/account/account.handlebars @@ -7,7 +7,9 @@