feat: use canvas to render mii

This commit is contained in:
Ash Monty 2022-08-14 18:44:19 +02:00
parent 82d9688b41
commit 97d90edbe7
No known key found for this signature in database
GPG Key ID: 740B7C88251D49B6
3 changed files with 178 additions and 65 deletions

View File

@ -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;
width: 100%;
height: 100%;
max-height: 384px;
overflow: hidden;
}
img#mii-img {
position: absolute;
bottom: 0;
canvas#miiCanvas {
margin: auto;
width: auto;
height: 512px;
z-index: 2;
height: auto;
transform-origin: top center;
transition: transform 200ms, filter 200ms;
}
div.mii-img-wrapper::before {
content: "";
position: absolute;
@ -453,10 +453,6 @@ button * {
gap: 12px;
}
div.mii-img-wrapper {
left: -36px;
}
div.params-wrapper {
margin-right: 24px;
}
@ -482,10 +478,6 @@ button * {
gap: 8px;
}
div.mii-img-wrapper {
left: -36px;
}
div.params-wrapper {
margin-right: 24px;
}
@ -532,12 +524,8 @@ 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 {
@ -564,11 +552,6 @@ button * {
margin-top: 1rem;
}
div.mii-img-wrapper {
width: 100%;
max-width: 512px;
}
fieldset {
order: 1;
}
@ -684,6 +667,11 @@ button * {
height: 36px;
}
div.pagination {
transform: scale(0.7);
transform-origin: right;
}
.has-textinput {
grid-template-columns: 1fr;
}

View File

@ -6,15 +6,81 @@ window.addEventListener('beforeunload', function (e) {
e.returnValue = '';
});
const Mii = require('mii-js');
let currentTab = '';
// this makes it so the canvas fits in the target x and y
function setCanvasScale() {
let targetX;
let targetY;
if (currentTab === 'size'||
currentTab === 'miiscTab' ||
currentTab === 'gender' ||
currentTab === 'favoriteColor' ||
currentTab === 'saveTab' ||
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();
});
// an array of mii bodies which can be accessed with bodies[mii.gender][mii.favoriteColor]
const bodies = [
[
'1/2/9/302cf838e',
'a/1/a/f566b5882',
'8/0/5/4e6c8e40f',
'c/9/d/debd43468',
'7/9/c/385972617',
'b/5/b/cd851b631',
'9/0/a/e1f7181a6',
'6/3/4/cf2bc03a3',
'7/a/d/5eaa9736c',
'8/2/d/3a32ad1cd',
'0/9/0/cc8eb01cc',
'9/a/3/d0e61957d'
],
[
'7/e/8/606f4fe49',
'7/a/b/1cf206fac',
'd/e/2/526e11ecf',
'f/0/f/52cae5867',
'c/1/a/7436d0d18',
'e/6/4/5c0a064e8',
'f/5/7/20e676637',
'0/0/e/5b151a232',
'8/c/e/344a00817',
'7/d/d/81a00dd54',
'3/b/f/29b5e1311',
'c/8/e/766672439'
]
];
// 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();
console.log(encodedUserMiiData);
// We initialize the Mii object
const mii = new Mii(Buffer.from(encodedUserMiiData, 'base64'));
@ -36,13 +102,74 @@ document.querySelector('.mii-comparison.confirmed img.old-mii').src =
// Initial mii render
renderMii();
// This function renders the Mii on the page
function renderMii(type) {
const miiStudioUrl = mii.studioUrl({
// This function renders the Mii on the canvas
function renderMii() {
const canvas = document.querySelector('canvas#miiCanvas');
const ctx = canvas.getContext('2d');
// we set the filter to show the user something's loading
canvas.style.filter = 'blur(4px) brightness(70%)';
// we create a copy of the mii and make it bald
const baldMii = Object.create(
Object.getPrototypeOf(mii),
Object.getOwnPropertyDescriptors(mii)
);
baldMii.hairType = 30;
const baldMiiFaceOnlyUrl = baldMii.studioUrl({
width: 512,
bgColor: '13173300',
type: type || 'all_body',
type: 'face_only',
});
const miiFaceOnlyUrl = mii.studioUrl({
width: 512,
bgColor: '13173300',
type: 'face_only',
});
const miiBodyUrl = `https://mii-studio.akamaized.net/editor/1/webp/1024/${bodies[mii.gender][mii.favoriteColor]}.webp`;
const miiFaceImg = new Image();
miiFaceImg.src = miiFaceOnlyUrl;
const baldMiiFaceImg = new Image();
baldMiiFaceImg.src = baldMiiFaceOnlyUrl;
const miiBodyImg = new Image();
miiBodyImg.src = miiBodyUrl;
const bodyWidth = (mii.build * 1.7 + 220) * (0.003 * mii.height + 0.6);
const bodyHeight = mii.height * 3.5 + 227;
const bodyXPos = (canvas.width - bodyWidth) / 2;
const bodyYPos = canvas.height - bodyHeight;
const headYPos = bodyYPos - 408;
// we make sure every image is loaded before rendering
miiFaceImg.onload = () => {
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);
// 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 = '';
}
const faceMiiStudioUrl = mii.studioUrl({
width: 512,
@ -55,28 +182,16 @@ function renderMii(type) {
expression: 'smile'
});
// sets the mii
document.querySelector('img#mii-img').src = miiStudioUrl;
// 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 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`;
// 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`;
console.log(mii);
}
// This function updates a prop of the Mii and rerenders it
function updateMii(e, type) {
function updateMii(e) {
const prop = e.target.name;
let value = e.target.value || e.target.defaultValue;
@ -93,7 +208,7 @@ function updateMii(e, type) {
// Handle booleans, on/offs and strings
if (value === 'true' || value === 'false') {
mii[prop] = value === 'true';
} else if (value === 'on' || value === 'off') {
} else if (value === 'on' || value === 'off') {
mii[prop] = value === 'on';
} else if (isNaN(parseInt(value))) {
mii[prop] = value;
@ -101,7 +216,7 @@ function updateMii(e, type) {
mii[prop] = parseInt(value);
}
renderMii(type);
renderMii();
}
function handleCalendar(e) {
@ -124,14 +239,18 @@ document.querySelectorAll('fieldset').forEach((fieldset) => {
});
document.querySelectorAll('input[type=\'range\']').forEach((input) => {
input.addEventListener('input', (e) => updateMii(e));
input.addEventListener('change', (e) => updateMii(e));
});
document.querySelectorAll('input[type=\'text\'], input[type=\'number\']').forEach((input) => {
input.addEventListener('blur', (e) => preventEmpty(e));
});
document
.querySelectorAll('input[type=\'text\'], input[type=\'number\']')
.forEach((input) => {
input.addEventListener('blur', (e) => preventEmpty(e));
});
document.querySelector('input[type=\'date\']#birthDate').addEventListener('change', (e) => handleCalendar(e));
document
.querySelector('input[type=\'date\']#birthDate')
.addEventListener('change', (e) => handleCalendar(e));
// FORM
@ -166,10 +285,7 @@ document.querySelector('input[type=\'date\']#birthDate').addEventListener('chang
console.log(`[info] preselected value for ${prop}`);
});
[
'favorite',
'allowCopying'
].forEach((prop) => {
['favorite', 'allowCopying'].forEach((prop) => {
const el = document.querySelector(`#${prop}`);
if (el) {
el.checked = mii[prop];
@ -215,8 +331,9 @@ console.log('[info] preselected value for disableSharing');
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}`;
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
@ -256,6 +373,9 @@ function openTab(e, tabType) {
document.querySelector(`#${selectedID} .subtabbtn`)?.click();
}
currentTab = selectedID;
setCanvasScale();
// We hide all subpages
document.querySelectorAll('.subpage').forEach((el) => {
el.classList.remove('active');
@ -348,6 +468,9 @@ document
.classList.add('centered-mii-img');
}, 2000);
alert(mii.encode().toString('base64'));
const miiData = mii.encode().toString('base64');
alert(miiData);
console.log('mii data:', miiData);
// CHECK IF MII IS VALID SERVERSIDE
});

View File

@ -24,8 +24,10 @@
</g>
</svg>
<div class="mii-img-wrapper">
<img id="mii-img" draggable="false" />
<div class="canvas-wrapper">
<canvas id="miiCanvas" width="512" height="1080">
Your browser does not support the canvas element.
</canvas>
</div>
<div class="params-wrapper">