Add support for animated textures

This commit is contained in:
Brian Beck 2024-08-26 00:35:47 -07:00
parent 12295fe980
commit dd55f3b728
36 changed files with 347 additions and 133 deletions

View file

@ -47,13 +47,11 @@ export async function getSkinConfig() {
models.map((name) => globby(`./public/textures/*.${name}.png`))
),
Promise.all(
models.map((name) =>
globby(path.join(T2_SKINS_PATH, `docs/skins/*.${name}.png`))
)
models.map((name) => globby(`${T2_SKINS_PATH}/docs/skins/*.${name}.png`))
),
Promise.all(
weaponModels.map((name) =>
globby(path.join(T2_SKINS_PATH, `docs/skins/*/weapon_${name}.png`))
globby(`${T2_SKINS_PATH}/docs/skins/*/weapon_${name}.png`)
)
),
]);
@ -240,6 +238,8 @@ export async function getSkinConfig() {
alphaMode: "BLEND",
metallicFactor: 0,
roughnessFactor: 1,
frameCount: 6,
frameTimings: [21, 1, 1, 1, 1, 1],
},
],
chaingun: [{ label: "Chaingun", name: "weapon_chaingun" }],

File diff suppressed because one or more lines are too long

View file

@ -1,2 +0,0 @@
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[445],{9583:function(t,a,c){c.d(a,{AMf:function(){return FaTrashAlt},D5B:function(){return FaUnlock},hJX:function(){return FaGithub},kUi:function(){return FaLock}});var n=c(8357);function FaGithub(t){return(0,n.w_)({tag:"svg",attr:{viewBox:"0 0 496 512"},child:[{tag:"path",attr:{d:"M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"}}]})(t)}function FaLock(t){return(0,n.w_)({tag:"svg",attr:{viewBox:"0 0 448 512"},child:[{tag:"path",attr:{d:"M400 224h-24v-72C376 68.2 307.8 0 224 0S72 68.2 72 152v72H48c-26.5 0-48 21.5-48 48v192c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V272c0-26.5-21.5-48-48-48zm-104 0H152v-72c0-39.7 32.3-72 72-72s72 32.3 72 72v72z"}}]})(t)}function FaTrashAlt(t){return(0,n.w_)({tag:"svg",attr:{viewBox:"0 0 448 512"},child:[{tag:"path",attr:{d:"M32 464a48 48 0 0 0 48 48h288a48 48 0 0 0 48-48V128H32zm272-256a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zM432 32H312l-9.4-18.7A24 24 0 0 0 281.1 0H166.8a23.72 23.72 0 0 0-21.4 13.3L136 32H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16z"}}]})(t)}function FaUnlock(t){return(0,n.w_)({tag:"svg",attr:{viewBox:"0 0 448 512"},child:[{tag:"path",attr:{d:"M400 256H152V152.9c0-39.6 31.7-72.5 71.3-72.9 40-.4 72.7 32.1 72.7 72v16c0 13.3 10.7 24 24 24h32c13.3 0 24-10.7 24-24v-16C376 68 307.5-.3 223.5 0 139.5.3 72 69.5 72 153.5V256H48c-26.5 0-48 21.5-48 48v160c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V304c0-26.5-21.5-48-48-48z"}}]})(t)}}}]);
//# sourceMappingURL=1bfc9850-93cf1d387cb4594a.js.map

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,2 @@
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[445],{9583:function(t,a,c){c.d(a,{AMf:function(){return FaTrashAlt},D5B:function(){return FaUnlock},Dli:function(){return FaChevronRight},bUI:function(){return FaChevronLeft},hJX:function(){return FaGithub},kUi:function(){return FaLock}});var n=c(8357);function FaGithub(t){return(0,n.w_)({tag:"svg",attr:{viewBox:"0 0 496 512"},child:[{tag:"path",attr:{d:"M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"}}]})(t)}function FaChevronLeft(t){return(0,n.w_)({tag:"svg",attr:{viewBox:"0 0 320 512"},child:[{tag:"path",attr:{d:"M34.52 239.03L228.87 44.69c9.37-9.37 24.57-9.37 33.94 0l22.67 22.67c9.36 9.36 9.37 24.52.04 33.9L131.49 256l154.02 154.75c9.34 9.38 9.32 24.54-.04 33.9l-22.67 22.67c-9.37 9.37-24.57 9.37-33.94 0L34.52 272.97c-9.37-9.37-9.37-24.57 0-33.94z"}}]})(t)}function FaChevronRight(t){return(0,n.w_)({tag:"svg",attr:{viewBox:"0 0 320 512"},child:[{tag:"path",attr:{d:"M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.475 239.03c9.373 9.372 9.373 24.568.001 33.941z"}}]})(t)}function FaLock(t){return(0,n.w_)({tag:"svg",attr:{viewBox:"0 0 448 512"},child:[{tag:"path",attr:{d:"M400 224h-24v-72C376 68.2 307.8 0 224 0S72 68.2 72 152v72H48c-26.5 0-48 21.5-48 48v192c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V272c0-26.5-21.5-48-48-48zm-104 0H152v-72c0-39.7 32.3-72 72-72s72 32.3 72 72v72z"}}]})(t)}function FaTrashAlt(t){return(0,n.w_)({tag:"svg",attr:{viewBox:"0 0 448 512"},child:[{tag:"path",attr:{d:"M32 464a48 48 0 0 0 48 48h288a48 48 0 0 0 48-48V128H32zm272-256a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zM432 32H312l-9.4-18.7A24 24 0 0 0 281.1 0H166.8a23.72 23.72 0 0 0-21.4 13.3L136 32H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16z"}}]})(t)}function FaUnlock(t){return(0,n.w_)({tag:"svg",attr:{viewBox:"0 0 448 512"},child:[{tag:"path",attr:{d:"M400 256H152V152.9c0-39.6 31.7-72.5 71.3-72.9 40-.4 72.7 32.1 72.7 72v16c0 13.3 10.7 24 24 24h32c13.3 0 24-10.7 24-24v-16C376 68 307.5-.3 223.5 0 139.5.3 72 69.5 72 153.5V256H48c-26.5 0-48 21.5-48 48v160c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V304c0-26.5-21.5-48-48-48z"}}]})(t)}}}]);
//# sourceMappingURL=1bfc9850-b4ceccea4b74407c.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
{"version":3,"file":"static/chunks/767.5a1b83173dac696e.js","mappings":"+QAGO,SAASA,cACdC,CAAiD,EAEjD,IAAMC,EAAM,GAAIC,CAAAA,GAAAA,EAChB,IAAK,IAAMC,KAAQH,EACjBC,EAAIE,IAAI,CAAC,kBAA4BC,MAAA,CAAVD,EAAKE,IAAI,EAAIF,EAAKG,IAAI,EAEnD,OAAOL,CACT,CAEO,eAAeM,YAAYN,CAAU,CAAEI,CAAY,EACxD,IAAMG,EAAO,MAAMP,EAAIQ,aAAa,CAAC,CAAEC,KAAM,MAAO,GACpDC,CAAAA,EAAAA,EAAAA,MAAAA,EAAOH,EAAMH,EACf,CAEO,SAASO,YAAYC,CAAgB,CAAER,CAAY,EACxDM,CAAAA,EAAAA,EAAAA,MAAAA,EAAOE,EAAUR,EACnB","sources":["webpack://_N_E/./src/exportUtils.ts","webpack://_N_E/<anon>"],"sourcesContent":["import JSZip from \"jszip\";\nimport { saveAs } from \"file-saver\";\n\nexport function createZipFile(\n files: Array<{ name: string; data: ArrayBuffer }>\n) {\n const zip = new JSZip();\n for (const file of files) {\n zip.file(`textures/skins/${file.name}`, file.data);\n }\n return zip;\n}\n\nexport async function saveZipFile(zip: JSZip, name: string) {\n const blob = await zip.generateAsync({ type: \"blob\" });\n saveAs(blob, name);\n}\n\nexport function savePngFile(imageUrl: string, name: string) {\n saveAs(imageUrl, name);\n}\n"],"names":["createZipFile","files","zip","JSZip","file","concat","name","data","saveZipFile","blob","generateAsync","type","saveAs","savePngFile","imageUrl"],"sourceRoot":""}
{"version":3,"file":"static/chunks/767.5a1b83173dac696e.js","mappings":"+QAGO,SAASA,cACdC,CAAiD,EAEjD,IAAMC,EAAM,GAAIC,CAAAA,GAAAA,EAChB,IAAK,IAAMC,KAAQH,EACjBC,EAAIE,IAAI,CAAC,kBAA4BC,MAAA,CAAVD,EAAKE,IAAI,EAAIF,EAAKG,IAAI,EAEnD,OAAOL,CACT,CAEO,eAAeM,YAAYN,CAAU,CAAEI,CAAY,EACxD,IAAMG,EAAO,MAAMP,EAAIQ,aAAa,CAAC,CAAEC,KAAM,MAAO,GACpDC,CAAAA,EAAAA,EAAAA,MAAAA,EAAOH,EAAMH,EACf,CAEO,SAASO,YAAYC,CAAgB,CAAER,CAAY,EACxDM,CAAAA,EAAAA,EAAAA,MAAAA,EAAOE,EAAUR,EACnB","sources":["webpack://_N_E/./src/exportUtils.ts","webpack://_N_E/<anon>"],"sourcesContent":["import JSZip from \"jszip\";\r\nimport { saveAs } from \"file-saver\";\r\n\r\nexport function createZipFile(\r\n files: Array<{ name: string; data: ArrayBuffer }>\r\n) {\r\n const zip = new JSZip();\r\n for (const file of files) {\r\n zip.file(`textures/skins/${file.name}`, file.data);\r\n }\r\n return zip;\r\n}\r\n\r\nexport async function saveZipFile(zip: JSZip, name: string) {\r\n const blob = await zip.generateAsync({ type: \"blob\" });\r\n saveAs(blob, name);\r\n}\r\n\r\nexport function savePngFile(imageUrl: string, name: string) {\r\n saveAs(imageUrl, name);\r\n}\r\n"],"names":["createZipFile","files","zip","JSZip","file","concat","name","data","saveZipFile","blob","generateAsync","type","saveAs","savePngFile","imageUrl"],"sourceRoot":""}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
{"version":3,"file":"static/chunks/pages/_app-3fa88258652f2cb2.js","mappings":"oFAAAA,EAAAC,OAAA,CAAAC,EAAA,4BCCA,CAAAC,OAAAC,QAAA,CAAAD,OAAAC,QAAA,MAAAC,IAAA,EACA,QACA,WACA,OAAeH,EAAQ,IACvB,EACA,kFCFAI,EAAA,QAAeC","sources":["webpack://_N_E/./node_modules/next/app.js","webpack://_N_E/?b089","webpack://_N_E/./src/pages/_app.ts","webpack://_N_E/./node_modules/rc-slider/assets/index.css","webpack://_N_E/./src/styles/global.css","webpack://_N_E/<anon>"],"sourcesContent":["module.exports = require('./dist/pages/_app')\n","\n (window.__NEXT_P = window.__NEXT_P || []).push([\n \"/_app\",\n function () {\n return require(\"private-next-pages/_app.ts\");\n }\n ]);\n if(module.hot) {\n module.hot.dispose(function () {\n window.__NEXT_P.push([\"/_app\"])\n });\n }\n ","import App from \"next/app\";\nimport \"../styles/global.css\";\nimport \"rc-slider/assets/index.css\";\n\nexport default App;\n","// extracted by mini-css-extract-plugin","// extracted by mini-css-extract-plugin"],"names":["module","exports","__webpack_require__","window","__NEXT_P","push","__webpack_exports__","App"],"sourceRoot":""}
{"version":3,"file":"static/chunks/pages/_app-3fa88258652f2cb2.js","mappings":"oFAAAA,EAAAC,OAAA,CAAAC,EAAA,4BCCA,CAAAC,OAAAC,QAAA,CAAAD,OAAAC,QAAA,MAAAC,IAAA,EACA,QACA,WACA,OAAeH,EAAQ,IACvB,EACA,kFCFAI,EAAA,QAAeC","sources":["webpack://_N_E/./node_modules/next/app.js","webpack://_N_E/?b089","webpack://_N_E/./src/pages/_app.ts","webpack://_N_E/./node_modules/rc-slider/assets/index.css","webpack://_N_E/./src/styles/global.css","webpack://_N_E/<anon>"],"sourcesContent":["module.exports = require('./dist/pages/_app')\n","\n (window.__NEXT_P = window.__NEXT_P || []).push([\n \"/_app\",\n function () {\n return require(\"private-next-pages/_app.ts\");\n }\n ]);\n if(module.hot) {\n module.hot.dispose(function () {\n window.__NEXT_P.push([\"/_app\"])\n });\n }\n ","import App from \"next/app\";\r\nimport \"../styles/global.css\";\r\nimport \"rc-slider/assets/index.css\";\r\n\r\nexport default App;\r\n","// extracted by mini-css-extract-plugin","// extracted by mini-css-extract-plugin"],"names":["module","exports","__webpack_require__","window","__NEXT_P","push","__webpack_exports__","App"],"sourceRoot":""}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,2 +1,2 @@
!function(){"use strict";var e,r,_,t,n,i,u,c={},o={};function __webpack_require__(e){var r=o[e];if(void 0!==r)return r.exports;var _=o[e]={exports:{}},t=!0;try{c[e].call(_.exports,_,_.exports,__webpack_require__),t=!1}finally{t&&delete o[e]}return _.exports}__webpack_require__.m=c,e=[],__webpack_require__.O=function(r,_,t,n){if(_){n=n||0;for(var i=e.length;i>0&&e[i-1][2]>n;i--)e[i]=e[i-1];e[i]=[_,t,n];return}for(var u=1/0,i=0;i<e.length;i++){for(var _=e[i][0],t=e[i][1],n=e[i][2],c=!0,o=0;o<_.length;o++)u>=n&&Object.keys(__webpack_require__.O).every(function(e){return __webpack_require__.O[e](_[o])})?_.splice(o--,1):(c=!1,n<u&&(u=n));if(c){e.splice(i--,1);var a=t()}}return a},__webpack_require__.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return __webpack_require__.d(r,{a:r}),r},__webpack_require__.d=function(e,r){for(var _ in r)__webpack_require__.o(r,_)&&!__webpack_require__.o(e,_)&&Object.defineProperty(e,_,{enumerable:!0,get:r[_]})},__webpack_require__.f={},__webpack_require__.e=function(e){return Promise.all(Object.keys(__webpack_require__.f).reduce(function(r,_){return __webpack_require__.f[_](e,r),r},[]))},__webpack_require__.u=function(e){return"static/chunks/"+(737===e?"fb7d5399":e)+"."+({250:"a985869c089b2fd3",354:"c8f476539d33c65e",737:"bc4a70b34221e8c8",767:"5a1b83173dac696e",848:"fc0fe21cdc2e6431"})[e]+".js"},__webpack_require__.miniCssF=function(e){return"static/css/dc34af488e4eed17.css"},__webpack_require__.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||Function("return this")()}catch(e){if("object"==typeof window)return window}}(),__webpack_require__.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},r={},_="_N_E:",__webpack_require__.l=function(e,t,n,i){if(r[e]){r[e].push(t);return}if(void 0!==n)for(var u,c,o=document.getElementsByTagName("script"),a=0;a<o.length;a++){var p=o[a];if(p.getAttribute("src")==e||p.getAttribute("data-webpack")==_+n){u=p;break}}u||(c=!0,(u=document.createElement("script")).charset="utf-8",u.timeout=120,__webpack_require__.nc&&u.setAttribute("nonce",__webpack_require__.nc),u.setAttribute("data-webpack",_+n),u.src=__webpack_require__.tu(e)),r[e]=[t];var onScriptComplete=function(_,t){u.onerror=u.onload=null,clearTimeout(b);var n=r[e];if(delete r[e],u.parentNode&&u.parentNode.removeChild(u),n&&n.forEach(function(e){return e(t)}),_)return _(t)},b=setTimeout(onScriptComplete.bind(null,void 0,{type:"timeout",target:u}),12e4);u.onerror=onScriptComplete.bind(null,u.onerror),u.onload=onScriptComplete.bind(null,u.onload),c&&document.head.appendChild(u)},__webpack_require__.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},__webpack_require__.tt=function(){return void 0===t&&(t={createScriptURL:function(e){return e}},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(t=trustedTypes.createPolicy("nextjs#bundler",t))),t},__webpack_require__.tu=function(e){return __webpack_require__.tt().createScriptURL(e)},__webpack_require__.p="/t2-model-skinner/_next/",n={272:0},__webpack_require__.f.j=function(e,r){var _=__webpack_require__.o(n,e)?n[e]:void 0;if(0!==_){if(_)r.push(_[2]);else if(272!=e){var t=new Promise(function(r,t){_=n[e]=[r,t]});r.push(_[2]=t);var i=__webpack_require__.p+__webpack_require__.u(e),u=Error();__webpack_require__.l(i,function(r){if(__webpack_require__.o(n,e)&&(0!==(_=n[e])&&(n[e]=void 0),_)){var t=r&&("load"===r.type?"missing":r.type),i=r&&r.target&&r.target.src;u.message="Loading chunk "+e+" failed.\n("+t+": "+i+")",u.name="ChunkLoadError",u.type=t,u.request=i,_[1](u)}},"chunk-"+e,e)}else n[e]=0}},__webpack_require__.O.j=function(e){return 0===n[e]},i=function(e,r){var _,t,i=r[0],u=r[1],c=r[2],o=0;if(i.some(function(e){return 0!==n[e]})){for(_ in u)__webpack_require__.o(u,_)&&(__webpack_require__.m[_]=u[_]);if(c)var a=c(__webpack_require__)}for(e&&e(r);o<i.length;o++)t=i[o],__webpack_require__.o(n,t)&&n[t]&&n[t][0](),n[t]=0;return __webpack_require__.O(a)},(u=self.webpackChunk_N_E=self.webpackChunk_N_E||[]).forEach(i.bind(null,0)),u.push=i.bind(null,u.push.bind(u))}();
//# sourceMappingURL=webpack-87a6ffe25f8aa4fa.js.map
!function(){"use strict";var e,r,_,t,n,i,u,c={},o={};function __webpack_require__(e){var r=o[e];if(void 0!==r)return r.exports;var _=o[e]={exports:{}},t=!0;try{c[e].call(_.exports,_,_.exports,__webpack_require__),t=!1}finally{t&&delete o[e]}return _.exports}__webpack_require__.m=c,e=[],__webpack_require__.O=function(r,_,t,n){if(_){n=n||0;for(var i=e.length;i>0&&e[i-1][2]>n;i--)e[i]=e[i-1];e[i]=[_,t,n];return}for(var u=1/0,i=0;i<e.length;i++){for(var _=e[i][0],t=e[i][1],n=e[i][2],c=!0,o=0;o<_.length;o++)u>=n&&Object.keys(__webpack_require__.O).every(function(e){return __webpack_require__.O[e](_[o])})?_.splice(o--,1):(c=!1,n<u&&(u=n));if(c){e.splice(i--,1);var a=t()}}return a},__webpack_require__.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return __webpack_require__.d(r,{a:r}),r},__webpack_require__.d=function(e,r){for(var _ in r)__webpack_require__.o(r,_)&&!__webpack_require__.o(e,_)&&Object.defineProperty(e,_,{enumerable:!0,get:r[_]})},__webpack_require__.f={},__webpack_require__.e=function(e){return Promise.all(Object.keys(__webpack_require__.f).reduce(function(r,_){return __webpack_require__.f[_](e,r),r},[]))},__webpack_require__.u=function(e){return"static/chunks/"+(737===e?"fb7d5399":e)+"."+({250:"a985869c089b2fd3",354:"c8f476539d33c65e",737:"bc4a70b34221e8c8",767:"5a1b83173dac696e",848:"fc0fe21cdc2e6431"})[e]+".js"},__webpack_require__.miniCssF=function(e){return"static/css/8f9c54e3d59c6be4.css"},__webpack_require__.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||Function("return this")()}catch(e){if("object"==typeof window)return window}}(),__webpack_require__.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},r={},_="_N_E:",__webpack_require__.l=function(e,t,n,i){if(r[e]){r[e].push(t);return}if(void 0!==n)for(var u,c,o=document.getElementsByTagName("script"),a=0;a<o.length;a++){var p=o[a];if(p.getAttribute("src")==e||p.getAttribute("data-webpack")==_+n){u=p;break}}u||(c=!0,(u=document.createElement("script")).charset="utf-8",u.timeout=120,__webpack_require__.nc&&u.setAttribute("nonce",__webpack_require__.nc),u.setAttribute("data-webpack",_+n),u.src=__webpack_require__.tu(e)),r[e]=[t];var onScriptComplete=function(_,t){u.onerror=u.onload=null,clearTimeout(f);var n=r[e];if(delete r[e],u.parentNode&&u.parentNode.removeChild(u),n&&n.forEach(function(e){return e(t)}),_)return _(t)},f=setTimeout(onScriptComplete.bind(null,void 0,{type:"timeout",target:u}),12e4);u.onerror=onScriptComplete.bind(null,u.onerror),u.onload=onScriptComplete.bind(null,u.onload),c&&document.head.appendChild(u)},__webpack_require__.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},__webpack_require__.tt=function(){return void 0===t&&(t={createScriptURL:function(e){return e}},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(t=trustedTypes.createPolicy("nextjs#bundler",t))),t},__webpack_require__.tu=function(e){return __webpack_require__.tt().createScriptURL(e)},__webpack_require__.p="/t2-model-skinner/_next/",n={272:0},__webpack_require__.f.j=function(e,r){var _=__webpack_require__.o(n,e)?n[e]:void 0;if(0!==_){if(_)r.push(_[2]);else if(272!=e){var t=new Promise(function(r,t){_=n[e]=[r,t]});r.push(_[2]=t);var i=__webpack_require__.p+__webpack_require__.u(e),u=Error();__webpack_require__.l(i,function(r){if(__webpack_require__.o(n,e)&&(0!==(_=n[e])&&(n[e]=void 0),_)){var t=r&&("load"===r.type?"missing":r.type),i=r&&r.target&&r.target.src;u.message="Loading chunk "+e+" failed.\n("+t+": "+i+")",u.name="ChunkLoadError",u.type=t,u.request=i,_[1](u)}},"chunk-"+e,e)}else n[e]=0}},__webpack_require__.O.j=function(e){return 0===n[e]},i=function(e,r){var _,t,i=r[0],u=r[1],c=r[2],o=0;if(i.some(function(e){return 0!==n[e]})){for(_ in u)__webpack_require__.o(u,_)&&(__webpack_require__.m[_]=u[_]);if(c)var a=c(__webpack_require__)}for(e&&e(r);o<i.length;o++)t=i[o],__webpack_require__.o(n,t)&&n[t]&&n[t][0](),n[t]=0;return __webpack_require__.O(a)},(u=self.webpackChunk_N_E=self.webpackChunk_N_E||[]).forEach(i.bind(null,0)),u.push=i.bind(null,u.push.bind(u))}();
//# sourceMappingURL=webpack-0d2be6fe958413db.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
self.__BUILD_MANIFEST={__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},"/":["static/chunks/78e521c3-3739cc27b3254d35.js","static/chunks/95b64a6e-a0ff77d56afeed48.js","static/chunks/31664189-69d752d1129a4958.js","static/chunks/545f34e4-3e66c340444ca8b2.js","static/chunks/1bfc9850-93cf1d387cb4594a.js","static/chunks/d7eeaac4-d223ea230e13423c.js","static/chunks/f580fadb-2911e2fbf64aae5a.js","static/chunks/50-a8a6240a880bd3e0.js","static/chunks/pages/index-3a28f1c89241d3c8.js"],"/_error":["static/chunks/pages/_error-54b9fcf45cb5bc62.js"],sortedPages:["/","/_app","/_error"]},self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();
self.__BUILD_MANIFEST={__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},"/":["static/chunks/78e521c3-3739cc27b3254d35.js","static/chunks/95b64a6e-a0ff77d56afeed48.js","static/chunks/31664189-69d752d1129a4958.js","static/chunks/545f34e4-3e66c340444ca8b2.js","static/chunks/1bfc9850-b4ceccea4b74407c.js","static/chunks/d7eeaac4-d223ea230e13423c.js","static/chunks/f580fadb-2911e2fbf64aae5a.js","static/chunks/50-a8a6240a880bd3e0.js","static/chunks/pages/index-e955a68f1762f24b.js"],"/_error":["static/chunks/pages/_error-54b9fcf45cb5bc62.js"],sortedPages:["/","/_app","/_error"]},self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();

File diff suppressed because one or more lines are too long

View file

@ -1,30 +1,64 @@
import { FaChevronLeft, FaChevronRight } from "react-icons/fa";
import useTools from "./useTools";
export default function CanvasToggle() {
const { activeCanvasType, setActiveCanvasType, hasMetallic } = useTools();
const {
activeCanvasType,
setActiveCanvasType,
hasMetallic,
hasAnimation,
frameCount,
selectedFrameIndex,
setSelectedFrameIndex,
} = useTools();
return (
<div className="CanvasToggle">
<button
type="button"
data-selected={activeCanvasType === "color" ? "" : undefined}
onClick={() => {
setActiveCanvasType("color");
}}
>
Color
</button>
{hasMetallic ? (
<>
<div className="CanvasToggle">
<button
type="button"
data-selected={activeCanvasType === "metallic" ? "" : undefined}
data-selected={activeCanvasType === "color" ? "" : undefined}
onClick={() => {
setActiveCanvasType("metallic");
setActiveCanvasType("color");
}}
>
Metallic
Color
</button>
{hasMetallic ? (
<button
type="button"
data-selected={activeCanvasType === "metallic" ? "" : undefined}
onClick={() => {
setActiveCanvasType("metallic");
}}
>
Metallic
</button>
) : null}
</div>
{hasAnimation ? (
<div className="FrameSelector">
<button
type="button"
onClick={() => {
setSelectedFrameIndex((index) => (index - 1) % frameCount);
}}
>
<FaChevronLeft />
</button>
<span className="FrameInfo">
{selectedFrameIndex + 1} / {frameCount}
</span>
<button
type="button"
onClick={() => {
setSelectedFrameIndex((index) => (index + 1) % frameCount);
}}
>
<FaChevronRight />
</button>
</div>
) : null}
</div>
</>
);
}

View file

@ -11,13 +11,16 @@ const defaultTextureSize = [512, 512] as [number, number];
export default function ColorCanvas({
materialDef,
frameIndex = 0,
}: {
materialDef: MaterialDefinition;
frameIndex: number;
}) {
const { skinImageUrls, defaultSkinImageUrls } = useWarrior();
const skinImageUrl = skinImageUrls[materialDef.file ?? materialDef.name];
const skinImageUrl =
skinImageUrls[materialDef.file ?? materialDef.name]?.[frameIndex];
const defaultSkinImageUrl =
defaultSkinImageUrls[materialDef.file ?? materialDef.name];
defaultSkinImageUrls[materialDef.file ?? materialDef.name]?.[frameIndex];
const { setColorImageUrl } = useSkin();
const { canvasPadding } = useSettings();
const [noAlphaImageUrl, setNoAlphaImageUrl] = useState<string | null>(null);
@ -37,9 +40,13 @@ export default function ColorCanvas({
width: textureSize[0],
height: textureSize[1],
});
setColorImageUrl(materialDef.file ?? materialDef.name, imageUrl);
setColorImageUrl(
materialDef.file ?? materialDef.name,
imageUrl,
frameIndex
);
},
[textureSize, canvasPadding, setColorImageUrl, materialDef]
[textureSize, canvasPadding, setColorImageUrl, materialDef, frameIndex]
);
useEffect(() => {
@ -79,7 +86,7 @@ export default function ColorCanvas({
loadImage,
]);
const canvasId = `${materialDef.name}:color`;
const canvasId = `${materialDef.name}:color:${frameIndex}`;
return textureSize ? (
<Canvas

View file

@ -28,6 +28,8 @@ export type MaterialDefinition = {
emissiveTexture?: boolean;
metallicFactor?: number;
roughnessFactor?: number;
frameCount?: number;
frameTimings?: number[];
};
function useTexture({
@ -39,13 +41,14 @@ function useTexture({
material: ModelMaterial;
materialDef?: MaterialDefinition;
textureType: "baseColorTexture" | "metallicRoughnessTexture";
imageUrl?: string;
imageUrl?: string[];
}) {
const { modelViewer } = useModelViewer();
const { basePath } = useSettings();
useEffect(() => {
let stale = false;
let animationFrame: ReturnType<typeof requestAnimationFrame>;
const updateTexture = async () => {
if (!materialDef || materialDef.hidden) {
@ -64,8 +67,11 @@ function useTexture({
emissiveTexture = false,
metallicFactor = 1,
roughnessFactor = 1,
frameCount = 1,
frameTimings,
} = materialDef;
let textureUrl = imageUrl ?? `${basePath}/white.png`;
let textureUrls =
imageUrl ?? new Array(frameCount).fill(`${basePath}/white.png`);
switch (textureType) {
case "baseColorTexture":
if (baseColorFactor) {
@ -85,15 +91,32 @@ function useTexture({
material.pbrMetallicRoughness.setMetallicFactor(metallicFactor);
material.pbrMetallicRoughness.setRoughnessFactor(roughnessFactor);
if (metallicFactor === 0 && roughnessFactor === 1) {
textureUrl = `${basePath}/green.png`;
textureUrls = new Array(frameCount).fill(`${basePath}/green.png`);
}
}
const texture = await modelViewer.createTexture(textureUrl);
const textures = await Promise.all(
textureUrls.map((textureUrl) => modelViewer.createTexture(textureUrl))
);
if (!stale) {
material.pbrMetallicRoughness[textureType].setTexture(texture);
if (textureType === "baseColorTexture" && emissiveTexture) {
material.emissiveTexture.setTexture(texture);
}
let frameIndex = 0;
let frameProgress = 0;
const frame: FrameRequestCallback = (timestamp) => {
const frameTiming = frameTimings?.[frameIndex] ?? 1;
const texture = textures[frameIndex];
material.pbrMetallicRoughness[textureType].setTexture(texture);
if (textureType === "baseColorTexture" && emissiveTexture) {
material.emissiveTexture.setTexture(texture);
}
frameProgress += 1;
if (frameCount > 1) {
if (frameProgress >= frameTiming) {
frameIndex = (frameIndex + 1) % frameCount;
frameProgress = 0;
}
animationFrame = requestAnimationFrame(frame);
}
};
animationFrame = requestAnimationFrame(frame);
}
}
};
@ -102,6 +125,7 @@ function useTexture({
return () => {
stale = true;
cancelAnimationFrame(animationFrame);
};
}, [basePath, modelViewer, material, materialDef, textureType, imageUrl]);
}

View file

@ -22,10 +22,26 @@ export default function MaterialCanvases() {
const hasMetallic = !(
materialDef.metallicFactor === 0 && materialDef.roughnessFactor === 1
);
const frameCount = materialDef.frameCount ?? 1;
const frames = new Array(frameCount).fill(null);
return (
<React.Fragment key={`${actualModel}-${materialDef.name}`}>
<ColorCanvas materialDef={materialDef} />
{hasMetallic ? <MetallicCanvas materialDef={materialDef} /> : null}
{frames.map((_, i) => (
<ColorCanvas
materialDef={materialDef}
frameIndex={i}
key={`color:${i}`}
/>
))}
{hasMetallic
? frames.map((_, i) => (
<MetallicCanvas
materialDef={materialDef}
frameIndex={i}
key={`metallic:${i}`}
/>
))
: null}
</React.Fragment>
);
})}

View file

@ -11,13 +11,16 @@ const defaultTextureSize = [512, 512] as [number, number];
export default function MetallicCanvas({
materialDef,
frameIndex = 0,
}: {
materialDef: MaterialDefinition;
frameIndex: number;
}) {
const { skinImageUrls, defaultSkinImageUrls } = useWarrior();
const skinImageUrl = skinImageUrls[materialDef.file ?? materialDef.name];
const skinImageUrl =
skinImageUrls[materialDef.file ?? materialDef.name]?.[frameIndex];
const defaultSkinImageUrl =
defaultSkinImageUrls[materialDef.file ?? materialDef.name];
defaultSkinImageUrls[materialDef.file ?? materialDef.name]?.[frameIndex];
const { setMetallicImageUrl } = useSkin();
const { canvasPadding } = useSettings();
const [alphaImageUrl, setAlphaImageUrl] = useState<string | null>(null);
@ -53,7 +56,8 @@ export default function MetallicCanvas({
if (runningChangeHandlers.current === 0) {
setMetallicImageUrl(
materialDef.file ?? materialDef.name,
outputImageUrl
outputImageUrl,
frameIndex
);
}
},
@ -63,6 +67,7 @@ export default function MetallicCanvas({
setMetallicImageUrl,
convertGrayscaleImageUrlToMetallicRoughness,
materialDef,
frameIndex,
]
);
@ -106,7 +111,7 @@ export default function MetallicCanvas({
loadImage,
]);
const canvasId = `${materialDef.name}:metallic`;
const canvasId = `${materialDef.name}:metallic:${frameIndex}`;
return textureSize ? (
<Canvas

View file

@ -14,24 +14,40 @@ export default function SkinProvider({ children }: { children: ReactNode }) {
};
});
},
setColorImageUrl(materialFile: string, colorImageUrl: string) {
setColorImageUrl(
materialFile: string,
colorImageUrl: string,
frameIndex: number
) {
setMaterialSkins((materialSkins) => {
const newColorImageUrl = Array.from(
materialSkins[materialFile]?.colorImageUrl ?? []
);
newColorImageUrl.splice(frameIndex, 1, colorImageUrl);
return {
...materialSkins,
[materialFile]: {
...materialSkins[materialFile],
colorImageUrl,
colorImageUrl: newColorImageUrl,
},
};
});
},
setMetallicImageUrl(materialFile: string, metallicImageUrl: string) {
setMetallicImageUrl(
materialFile: string,
metallicImageUrl: string,
frameIndex: number
) {
setMaterialSkins((materialSkins) => {
const newMetallicImageUrl = Array.from(
materialSkins[materialFile]?.metallicImageUrl ?? []
);
newMetallicImageUrl.splice(frameIndex, 1, metallicImageUrl);
return {
...materialSkins,
[materialFile]: {
...materialSkins[materialFile],
metallicImageUrl,
metallicImageUrl: newMetallicImageUrl,
},
};
});
@ -46,11 +62,11 @@ export default function SkinProvider({ children }: { children: ReactNode }) {
getSkinImages(materialFile: string) {
return materialSkins[materialFile];
},
getColorImageUrl(materialFile: string) {
return materialSkins[materialFile].colorImageUrl;
getColorImageUrl(materialFile: string, frameIndex: number) {
return materialSkins[materialFile].colorImageUrl?.[frameIndex];
},
getMetallicImageUrl(materialFile: string) {
return materialSkins[materialFile].metallicImageUrl;
getMetallicImageUrl(materialFile: string, frameIndex: number) {
return materialSkins[materialFile].metallicImageUrl?.[frameIndex];
},
...setters,
};

View file

@ -45,9 +45,13 @@ type ObjectFilters = {
export default function ToolsProvider({ children }: { children: ReactNode }) {
const { actualModel, selectedModelType } = useWarrior();
const [selectedMaterialIndex, setSelectedMaterialIndex] = useState(0);
const [selectedFrameIndex, setSelectedFrameIndex] = useState(0);
const materialDefs = materials[actualModel];
const materialDef = materialDefs[selectedMaterialIndex] ?? null;
const frameCount = materialDef.frameCount ?? 1;
const hasAnimation = frameCount > 1;
const textureSize = useMemo(
() => materialDef.size ?? [512, 512],
[materialDef]
@ -63,6 +67,14 @@ export default function ToolsProvider({ children }: { children: ReactNode }) {
setActiveCanvasType("color");
}
if (selectedFrameIndex >= frameCount) {
setSelectedFrameIndex(0);
}
useEffect(() => {
setSelectedFrameIndex(0);
}, [materialDef]);
const [backgroundColor, setBackgroundColor] = useState("magenta");
const [lockedObjects, setLockedObjects] = useState(
() => new Set<fabric.Object>()
@ -77,9 +89,11 @@ export default function ToolsProvider({ children }: { children: ReactNode }) {
);
const activeCanvas = materialDef
? `${materialDef.name}:${activeCanvasType}`
? `${materialDef.name}:${activeCanvasType}:${selectedFrameIndex}`
: null;
const metallicCanvasId = materialDef
? `${materialDef.name}:metallic:${selectedFrameIndex}`
: null;
const metallicCanvasId = materialDef ? `${materialDef.name}:metallic` : null;
const { canvases } = useCanvas();
const { canvas, notifyChange, undo, redo, canUndo, canRedo } =
useCanvas(activeCanvas);
@ -348,54 +362,73 @@ export default function ToolsProvider({ children }: { children: ReactNode }) {
!materialDef.hidden &&
materialDef.selectable !== false
)
.map(async (materialDef: MaterialDefinition) => {
const colorCanvas = canvases[`${materialDef.name}:color`]?.canvas;
const metallicCanvas =
canvases[`${materialDef.name}:metallic`]?.canvas;
.map((materialDef: MaterialDefinition) => {
const frameCount = materialDef.frameCount ?? 1;
const frames = new Array(frameCount).fill(null);
return frames.map(async (_, frameIndex) => {
const colorCanvas =
canvases[`${materialDef.name}:color:${frameIndex}`]?.canvas;
const metallicCanvas =
canvases[`${materialDef.name}:metallic:${frameIndex}`]?.canvas;
const textureSize = materialDef.size ?? [512, 512];
let outputImageUrl;
const textureSize = materialDef.size ?? [512, 512];
let outputImageUrl;
const colorImageUrl = colorCanvas.toDataURL({
top: canvasPadding,
left: canvasPadding,
width: textureSize[0],
height: textureSize[1],
});
if (metallicCanvas) {
const metallicImageUrl = metallicCanvas.toDataURL({
const colorImageUrl = colorCanvas.toDataURL({
top: canvasPadding,
left: canvasPadding,
width: textureSize[0],
height: textureSize[1],
});
outputImageUrl = await combineColorAndAlphaImageUrls({
colorImageUrl,
metallicImageUrl,
});
} else {
outputImageUrl = colorImageUrl;
}
let filename;
switch (selectedModelType) {
case "player":
filename = `${name}.${actualModel}.png`;
break;
case "weapon":
case "vehicle":
if (materialDef) {
filename = `${materialDef.file ?? materialDef.name}.png`;
} else if (selectedModelType === "weapon") {
filename = `weapon_${actualModel}.png`;
} else {
filename = `${actualModel}.png`;
}
}
if (metallicCanvas) {
const metallicImageUrl = metallicCanvas.toDataURL({
top: canvasPadding,
left: canvasPadding,
width: textureSize[0],
height: textureSize[1],
});
outputImageUrl = await combineColorAndAlphaImageUrls({
colorImageUrl,
metallicImageUrl,
});
} else {
outputImageUrl = colorImageUrl;
}
return { imageUrl: outputImageUrl, filename };
let filename;
switch (selectedModelType) {
case "player":
filename = `${name}.${actualModel}.png`;
break;
case "weapon":
case "vehicle":
if (materialDef) {
const frameZeroFile = materialDef.file ?? materialDef.name;
if (frameCount > 1) {
const match = frameZeroFile.match(/^(.+)(\d\d)$/);
if (match) {
const baseName = match[1];
filename = `${baseName}${frameIndex
.toString()
.padStart(2, "0")}.png`;
} else {
throw new Error("Unexpected animation filename");
}
} else {
filename = `${frameZeroFile}.png`;
}
} else if (selectedModelType === "weapon") {
filename = `weapon_${actualModel}.png`;
} else {
filename = `${actualModel}.png`;
}
}
return { imageUrl: outputImageUrl, filename };
});
})
.flat()
);
switch (format) {
@ -484,6 +517,10 @@ export default function ToolsProvider({ children }: { children: ReactNode }) {
setSelectedMaterialIndex,
textureSize,
hasMetallic,
selectedFrameIndex,
setSelectedFrameIndex,
hasAnimation,
frameCount,
}),
[
activeCanvas,
@ -516,6 +553,9 @@ export default function ToolsProvider({ children }: { children: ReactNode }) {
selectedMaterialIndex,
textureSize,
hasMetallic,
selectedFrameIndex,
hasAnimation,
frameCount,
]
);

View file

@ -8,6 +8,20 @@ const { publicRuntimeConfig } = getConfig();
const { materials, modelDefaults } = publicRuntimeConfig;
const baseSkinPath = `https://exogen.github.io/t2-skins/skins`;
function getFrameNames(frameZeroFile: string, frameCount: number) {
if (frameCount < 2) {
return [frameZeroFile];
}
const match = frameZeroFile.match(/^(.+)(\d\d)$/);
if (match) {
const baseName = match[1];
const frames = new Array(frameCount).fill(null);
return frames.map((_, i) => `${baseName}${i.toString().padStart(2, "0")}`);
} else {
throw new Error("Did not match expected frame format");
}
}
export function getSkinImageUrls({
basePath,
actualModel,
@ -20,43 +34,48 @@ export function getSkinImageUrls({
selectedModelType: string;
selectedSkin: string | null;
selectedSkinType: string | null;
}): Record<string, string> {
}): Record<string, string[]> {
const materialDefs = materials[actualModel];
switch (selectedModelType) {
case "player":
switch (selectedSkinType) {
case "default":
return {
base: `${basePath}/textures/${selectedSkin}.${actualModel}.png`,
base: [`${basePath}/textures/${selectedSkin}.${actualModel}.png`],
};
case "custom":
return { base: `${baseSkinPath}/${selectedSkin}.${actualModel}.png` };
return {
base: [`${baseSkinPath}/${selectedSkin}.${actualModel}.png`],
};
}
break;
case "weapon":
case "vehicle":
return materialDefs.reduce(
(
skinImageUrls: Record<string, string>,
skinImageUrls: Record<string, string[]>,
materialDef: MaterialDefinition
) => {
if (materialDef) {
const frameCount = materialDef.frameCount ?? 1;
switch (selectedSkinType) {
case "default":
if (materialDef.hasDefault !== false) {
skinImageUrls[
materialDef.file ?? materialDef.name
] = `${basePath}/textures/${
materialDef.file ?? materialDef.name
}.png`;
skinImageUrls[materialDef.file ?? materialDef.name] =
getFrameNames(
materialDef.file ?? materialDef.name,
frameCount
).map((name) => `${basePath}/textures/${name}.png`);
}
break;
case "custom":
skinImageUrls[
materialDef.file ?? materialDef.name
] = `${baseSkinPath}/${selectedSkin}/${
materialDef.file ?? materialDef.name
}.png`;
skinImageUrls[materialDef.file ?? materialDef.name] =
getFrameNames(
materialDef.file ?? materialDef.name,
frameCount
).map(
(name) => `${baseSkinPath}/${selectedSkin}/${name}.png`
);
break;
}
}
@ -127,7 +146,7 @@ export default function WarriorProvider({ children }: { children: ReactNode }) {
selectedAnimation
);
const [skinImageUrls, setSkinImageUrls] = useState<Record<string, string>>(
const [skinImageUrls, setSkinImageUrls] = useState<Record<string, string[]>>(
() =>
getSkinImageUrls({
basePath,

View file

@ -184,7 +184,7 @@ export default function WarriorSelector() {
});
setSelectedSkin(null);
setSkinImageUrls({
[materialDef.file ?? materialDef.name]: imageUrl,
[materialDef.file ?? materialDef.name]: [imageUrl],
});
}}
type="file"

View file

@ -488,3 +488,34 @@ h6 {
margin-top: 20px;
}
}
.FrameSelector {
display: flex;
align-items: center;
margin-right: auto;
border-radius: 5px;
padding: 2px 0;
background: rgb(74, 193, 171);
color: rgb(5, 69, 76);
}
.FrameSelector button {
display: inline-flex;
align-items: center;
background: transparent;
color: inherit;
border: 0;
margin: 0;
padding: 2px 8px;
font-size: 12px;
line-height: 1;
}
.FrameSelector button:disabled {
opacity: 0.4;
}
.FrameInfo {
font-size: 12px;
font-weight: 500;
}

View file

@ -1,8 +1,8 @@
import React, { useContext } from "react";
export type SkinImages = {
colorImageUrl?: string;
metallicImageUrl?: string;
colorImageUrl?: string[];
metallicImageUrl?: string[];
};
export type MaterialSkins = Record<string, SkinImages>;
@ -11,10 +11,24 @@ interface SkinContextValue {
materialSkins: MaterialSkins;
getSkinImages: (materialFile: string) => SkinImages;
setSkinImages: (materialFile: string, skinImages: SkinImages) => void;
getColorImageUrl: (materialFile: string) => string | undefined;
setColorImageUrl: (materialFile: string, colorImageUrl: string) => void;
getMetallicImageUrl: (materialFile: string) => string | undefined;
setMetallicImageUrl: (materialFile: string, colorImageUrl: string) => void;
getColorImageUrl: (
materialFile: string,
frameIndex: number
) => string | undefined;
setColorImageUrl: (
materialFile: string,
colorImageUrl: string,
frameIndex: number
) => void;
getMetallicImageUrl: (
materialFile: string,
frameIndex: number
) => string | undefined;
setMetallicImageUrl: (
materialFile: string,
metallicImageUrl: string,
frameIndex: number
) => void;
}
const SkinContext = React.createContext<SkinContextValue | null>(null);

View file

@ -41,8 +41,14 @@ interface ToolsContextValue {
setBackgroundColor: (backgroundColor: string) => void;
selectedMaterialIndex: number;
setSelectedMaterialIndex: (materialIndex: number) => void;
selectedFrameIndex: number;
setSelectedFrameIndex: (
frameIndex: number | ((frameIndex: number) => number)
) => void;
textureSize: [number, number];
hasMetallic: boolean;
hasAnimation: boolean;
frameCount: number;
}
const ToolsContext = React.createContext<ToolsContextValue | null>(null);

View file

@ -12,12 +12,14 @@ type WarriorContextValue = {
setAnimationPaused: (
animationPaused: boolean | ((animationPaused: boolean) => boolean)
) => void;
skinImageUrls: Record<string, string>;
defaultSkinImageUrls: Record<string, string>;
skinImageUrls: Record<string, string[]>;
defaultSkinImageUrls: Record<string, string[]>;
setSkinImageUrls: (
value:
| Record<string, string>
| ((prevSkinImageUrls: Record<string, string>) => Record<string, string>)
| Record<string, string[]>
| ((
prevSkinImageUrls: Record<string, string[]>
) => Record<string, string[]>)
) => void;
selectedSkinType: string | null;
setSelectedSkinType: (selectedSkinType: string | null) => void;