-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* [WIP] Add a page to view the costumes. * Partial fix for costume parsing (#21) * Partial fix for costume parsing * A few refinements * Minor refactoring. * Set the number of frames. * Fix sprites rendering. * Improve display. * Bit of cleaning. * Add support for different costume sets. --------- Co-authored-by: Gamaiel Zavala <[email protected]>
- Loading branch information
Showing
10 changed files
with
313 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import ColumnListHeader from './ColumnListHeader'; | ||
import ColumnListItem from './ColumnListItem'; | ||
|
||
const CostumesList = ({ costumeSets, currentSetId, currentId }) => { | ||
return ( | ||
<> | ||
{costumeSets.map((costumeSet, costumeSetId) => ( | ||
<div key={costumeSetId}> | ||
<ColumnListHeader>Costume set {costumeSetId}</ColumnListHeader> | ||
{costumeSet.sprdesc.map((unused, id) => { | ||
const selected = costumeSetId === currentSetId && id === currentId; | ||
const path = `/costumes/${costumeSetId}/${id}`; | ||
const label = `Costume ${id}`; | ||
|
||
return ( | ||
<ColumnListItem | ||
key={id} | ||
path={selected ? null : path}> | ||
{label} | ||
</ColumnListItem> | ||
); | ||
})} | ||
</div> | ||
))} | ||
</> | ||
); | ||
}; | ||
|
||
export default CostumesList; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
import { useRef, useState, useEffect } from 'react'; | ||
import { clsx } from 'clsx'; | ||
import { getPalette } from '../lib/paletteUtils'; | ||
|
||
// Display a costume on a canvas. | ||
|
||
// prettier-ignore | ||
const darkpalette = [ | ||
0x2d, 0x1d, 0x2d, 0x3d, | ||
0x2d, 0x1d, 0x2d, 0x3d, | ||
0x2d, 0x1d, 0x2d, 0x3d, | ||
0x2d, 0x1d, 0x2d, 0x3d, | ||
]; | ||
|
||
const CostumeCanvasContainer = ({ | ||
id, | ||
frame, | ||
gfx, | ||
sprdesc, | ||
sproffs, | ||
sprlens, | ||
sprdata, | ||
sprpals, | ||
zoom = 1, | ||
}) => { | ||
const canvasRef = useRef(null); | ||
const [isComputing, setIsComputing] = useState(true); | ||
|
||
const desc = sprdesc[id]; | ||
// this was 3 bytes per sprite in the data but has been parsed down to 1 byte | ||
const offset = sproffs[desc + frame] / 3; | ||
const spritesNum = sprlens[desc + frame]; | ||
const palette = sprpals.palette; | ||
|
||
// Compute the bounding box. | ||
let left = 239; | ||
let right = 0; | ||
let top = 239; | ||
let bottom = 0; | ||
for (let i = 0; i < spritesNum; i++) { | ||
const { x, y } = sprdata[offset + i]; | ||
|
||
left = Math.min(left, x); | ||
right = Math.max(right, x + 8); | ||
top = Math.min(top, y); | ||
bottom = Math.max(bottom, y + 8); | ||
} | ||
|
||
const width = right - left; | ||
const height = bottom - top; | ||
|
||
useEffect(() => { | ||
const canvas = canvasRef.current; | ||
const ctx = canvas.getContext('2d'); | ||
|
||
setTimeout(() => { | ||
draw( | ||
ctx, | ||
gfx.gfx, | ||
sprdata, | ||
offset, | ||
spritesNum, | ||
palette, | ||
left, | ||
top, | ||
width, | ||
height, | ||
); | ||
setIsComputing(false); | ||
}); | ||
}, [ | ||
frame, | ||
gfx, | ||
sprdata, | ||
offset, | ||
spritesNum, | ||
palette, | ||
left, | ||
top, | ||
width, | ||
height, | ||
]); | ||
|
||
return ( | ||
<canvas | ||
ref={canvasRef} | ||
width={width} | ||
height={height} | ||
className={clsx( | ||
'rounded', | ||
isComputing ? 'opacity-0' : 'opacity-100 transition-opacity', | ||
)} | ||
style={{ width: width * zoom, height: height * zoom }} | ||
/> | ||
); | ||
}; | ||
|
||
const draw = ( | ||
ctx, | ||
gfx, | ||
sprdata, | ||
offset, | ||
spritesNum, | ||
palette, | ||
left, | ||
top, | ||
width, | ||
height, | ||
) => { | ||
// Clear the canvas. | ||
ctx.fillStyle = 'lightgrey'; | ||
ctx.fillRect(0, 0, width, height); | ||
|
||
for (let i = 0; i < spritesNum; i++) { | ||
const { x, y, tile, flip, paletteId } = sprdata[offset + i]; | ||
|
||
const pal = getPalette([ | ||
palette[paletteId], | ||
palette[paletteId + 1], | ||
palette[paletteId + 2], | ||
palette[paletteId + 3], | ||
]); | ||
|
||
for (let j = 0; j < 8; j++) { | ||
const n1 = gfx[tile * 2 * 8 + j]; | ||
const n2 = gfx[(tile * 2 + 1) * 8 + j]; | ||
for (let k = 0; k < 8; k++) { | ||
const mask = 1 << k; | ||
const val = (n1 & mask ? 1 : 0) | ((n2 & mask ? 1 : 0) << 1); | ||
|
||
// Skip the transparent palette colour. | ||
if (val === 0) { | ||
continue; | ||
} | ||
|
||
ctx.fillStyle = pal[val]; | ||
if (flip) { | ||
ctx.fillRect(k + x - left, j + y - top, 1, 1); | ||
} else { | ||
ctx.fillRect(7 - k + x - left, j + y - top, 1, 1); | ||
} | ||
} | ||
} | ||
} | ||
}; | ||
|
||
export default CostumeCanvasContainer; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import { useParams } from 'react-router-dom'; | ||
import PrimaryColumn from '../components/PrimaryColumn'; | ||
import CostumesList from '../components/CostumesList'; | ||
import Main from '../components/Main'; | ||
import MainHeader from '../components/MainHeader'; | ||
import ResourceMetadata from '../components/ResourceMetadata'; | ||
import CostumeCanvasContainer from './CostumeCanvasContainer'; | ||
|
||
// @todo Parse it from 3DAED-3DB05 instead of hardcoding. | ||
// prettier-ignore | ||
const costumeIdLookupTable = [ | ||
0x00, 0x03, 0x01, 0x06, 0x08, | ||
0x02, 0x00, 0x07, 0x0c, 0x04, | ||
0x09, 0x0a, 0x12, 0x0b, 0x14, | ||
0x0d, 0x11, 0x0f, 0x0e, 0x10, | ||
0x17, 0x00, 0x01, 0x05, 0x16, | ||
]; | ||
|
||
const CostumesContainer = ({ | ||
costumegfx, | ||
costumes, | ||
sprpals, | ||
sprdesc, | ||
sproffs, | ||
sprlens, | ||
sprdata, | ||
}) => { | ||
const { setId, id } = useParams(); | ||
|
||
const currentSetId = | ||
typeof setId === 'undefined' ? null : parseInt(setId, 10); | ||
const currentId = typeof id === 'undefined' ? null : parseInt(id, 10); | ||
const costume = | ||
costumes.find(({ metadata }) => metadata.id === currentId) || null; | ||
const costumeId = | ||
currentSetId === 0 ? costumeIdLookupTable[currentId] : currentId; | ||
|
||
const getFramesNumbersFromCostumeId = (costumeId = 0) => { | ||
if (costumeId === sprdesc[currentSetId].sprdesc.length - 1) { | ||
// @todo Find a better way than hardcoding it. | ||
return currentSetId === 0 ? 2 : 1; | ||
} | ||
|
||
return ( | ||
sprdesc[currentSetId].sprdesc[costumeId + 1] - | ||
sprdesc[currentSetId].sprdesc[costumeId] | ||
); | ||
}; | ||
|
||
const frameNum = getFramesNumbersFromCostumeId(costumeId); | ||
|
||
if (!costume) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<> | ||
<PrimaryColumn> | ||
<CostumesList | ||
costumeSets={sprdesc} | ||
currentSetId={currentSetId} | ||
currentId={currentId} | ||
/> | ||
</PrimaryColumn> | ||
|
||
<Main> | ||
<MainHeader title={`Costume ${currentId}`}> | ||
<ResourceMetadata metadata={costume.metadata} /> | ||
</MainHeader> | ||
<div className="flex flex-row flex-wrap gap-4"> | ||
{Array(frameNum) | ||
.fill() | ||
.map((unused, frame) => ( | ||
<CostumeCanvasContainer | ||
key={frame} | ||
id={costumeId} | ||
frame={frame} | ||
gfx={costumegfx[currentSetId]} | ||
sprdesc={sprdesc[currentSetId].sprdesc} | ||
sproffs={sproffs[currentSetId].sproffs} | ||
sprlens={sprlens[currentSetId].sprlens} | ||
sprdata={sprdata[currentSetId].sprdata} | ||
sprpals={sprpals[currentSetId]} | ||
zoom={2} | ||
/> | ||
))} | ||
</div> | ||
</Main> | ||
</> | ||
); | ||
}; | ||
|
||
export default CostumesContainer; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters