From 97d90edbe7130e310b6fb20bb716ace021970020 Mon Sep 17 00:00:00 2001 From: Ash Monty Date: Sun, 14 Aug 2022 18:44:19 +0200 Subject: [PATCH] feat: use canvas to render mii --- public/assets/css/miieditor.css | 50 +++----- public/assets/js/miieditor.js | 187 ++++++++++++++++++++++++----- views/account/miieditor.handlebars | 6 +- 3 files changed, 178 insertions(+), 65 deletions(-) diff --git a/public/assets/css/miieditor.css b/public/assets/css/miieditor.css index 90bdedd..d58df55 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; + 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; } diff --git a/public/assets/js/miieditor.js b/public/assets/js/miieditor.js index d04f671..2946cd9 100644 --- a/public/assets/js/miieditor.js +++ b/public/assets/js/miieditor.js @@ -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 }); diff --git a/views/account/miieditor.handlebars b/views/account/miieditor.handlebars index b519eee..84faca1 100644 --- a/views/account/miieditor.handlebars +++ b/views/account/miieditor.handlebars @@ -24,8 +24,10 @@ -
- +
+ + Your browser does not support the canvas element. +