Skip to content

Commit 6072e8c

Browse files
committed
feat: add SongCanvas component for rendering songs with WASM support
1 parent 5b64d0d commit 6072e8c

File tree

4 files changed

+157
-1
lines changed

4 files changed

+157
-1
lines changed

web/public/nbs-player-rs.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/public/nbs_player_rs.wasm

1.71 MB
Binary file not shown.

web/src/modules/song/components/SongPage.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { SongPreviewDto } from '@shared/validation/song/dto/SongPreview.dto';
22
import { SongViewDtoType } from '@shared/validation/song/dto/types';
3-
import Image from 'next/image';
43

54
import axios from '@web/src/lib/axios';
65

76
import { LicenseInfo } from './client/LicenseInfo';
7+
import { SongCanvas } from './client/SongCanvas';
88
import { SongDetails } from './SongDetails';
99
import {
1010
DownloadSongButton,
@@ -49,6 +49,7 @@ export async function SongPage({ id }: { id: string }) {
4949
<div className='col-span-full lg:col-span-5 flex flex-col gap-4'>
5050
{/* Song thumbnail */}
5151
{/* TODO: implement loading https://github.com/vercel/next.js/discussions/50617 */}
52+
{/*
5253
<picture className='bg-zinc-800 aspect-[5/3] rounded-xl'>
5354
<Image
5455
width={1280}
@@ -58,6 +59,9 @@ export async function SongPage({ id }: { id: string }) {
5859
className='w-full h-full rounded-xl'
5960
/>
6061
</picture>
62+
*/}
63+
64+
<SongCanvas song={song} />
6165

6266
<h1 className='text-xl font-bold'>{song.title}</h1>
6367

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
'use client';
2+
3+
import { SongViewDtoType } from '@shared/validation/song/dto/types';
4+
import { useEffect, useRef } from 'react';
5+
6+
import axios from '@web/src/lib/axios';
7+
8+
export const SongCanvas = ({ song }: { song: SongViewDtoType }) => {
9+
const canvasContainerRef = useRef<HTMLDivElement>(null);
10+
const wasmModuleRef = useRef<any>(null);
11+
12+
useEffect(() => {
13+
if (!canvasContainerRef.current) return;
14+
15+
const element = canvasContainerRef.current;
16+
const canvas = element.querySelector('canvas');
17+
18+
if (!canvas) return;
19+
20+
const handleKeyDown = (event: KeyboardEvent) => {
21+
if (event.code === 'Space') event.preventDefault();
22+
};
23+
24+
window.addEventListener('keydown', handleKeyDown);
25+
26+
// Calculate window dimensions
27+
let fullscreen_window_width = window.screen.width;
28+
let fullscreen_window_height = window.screen.height;
29+
30+
if (fullscreen_window_width < fullscreen_window_height) {
31+
[fullscreen_window_width, fullscreen_window_height] = [
32+
fullscreen_window_height,
33+
fullscreen_window_width,
34+
];
35+
}
36+
37+
// 720p resolution
38+
//const window_width = 1280;
39+
//const window_height = 720;
40+
41+
const argumentsData = {
42+
font_id: 1, // Math.floor(Math.random() * 6),
43+
window_width: fullscreen_window_width,
44+
window_height: fullscreen_window_height,
45+
theme: {
46+
background_color: '#7EC850', // Grass-like green
47+
accent_color: '#FF6A00', // Orange (like Minecraft's iconic dirt/wood)
48+
text_color: '#F0F0F0', // Light gray
49+
white_key_color: '#F5F5DC', // Beige (like sand)
50+
black_key_color: '#3B2F2F', // Dark brown (like wood)
51+
white_text_key_color: '#1A1A1A', // Dark gray
52+
black_text_key_color: '#F0F0F0', // Light gray
53+
},
54+
};
55+
56+
const scriptTag = document.createElement('script');
57+
58+
scriptTag.src = '/nbs-player-rs.js';
59+
60+
scriptTag.async = true; // Load the script asynchronously
61+
62+
//scriptTag.onload = () => {
63+
//if (!window.Module) return;
64+
65+
wasmModuleRef.current = window.Module; // Store for cleanup
66+
67+
window.Module = {
68+
canvas: canvas,
69+
arguments: [JSON.stringify(argumentsData)],
70+
noInitialRun: true,
71+
preInit: async function () {
72+
// wait 2 seconds before starting
73+
await new Promise((resolve) => setTimeout(resolve, 200));
74+
75+
const response_url = await axios.get(`/song/${song.publicId}/open`, {
76+
headers: {
77+
src: 'downloadButton',
78+
},
79+
});
80+
81+
const song_url = response_url.data;
82+
console.log('Song URL:', song_url);
83+
84+
const response = await fetch(song_url);
85+
const arrayBuffer = await response.arrayBuffer();
86+
const byteArray = new Uint8Array(arrayBuffer);
87+
88+
if (window.FS) {
89+
window.FS.writeFile('/song.nbsx', byteArray);
90+
} else {
91+
console.error('FS is not defined');
92+
}
93+
94+
if (window.callMain) {
95+
window.callMain([JSON.stringify(argumentsData)]);
96+
} else {
97+
console.error('callMain is not defined');
98+
}
99+
},
100+
};
101+
//};
102+
103+
// Append the script tag to the body
104+
element.appendChild(scriptTag);
105+
106+
return () => {
107+
window.removeEventListener('keydown', handleKeyDown);
108+
109+
if (canvas) {
110+
canvas.removeEventListener('keydown', () => undefined);
111+
}
112+
113+
// Remove script tag
114+
const script = element.querySelector('script[src="/nbs-player-rs.js"]');
115+
if (script) script.remove();
116+
117+
// Properly destroy WASM module
118+
if (wasmModuleRef.current) {
119+
if (wasmModuleRef.current.destroy) {
120+
wasmModuleRef.current.destroy();
121+
}
122+
123+
wasmModuleRef.current = null;
124+
}
125+
126+
// Clear global Module reference
127+
if (window.Module) {
128+
if (window.Module.delete) window.Module.delete();
129+
delete window.Module;
130+
}
131+
132+
// Force garbage collection
133+
if (window.gc) window.gc();
134+
};
135+
}, [song.publicId]);
136+
137+
return (
138+
<div
139+
ref={canvasContainerRef}
140+
id='song-renderer-container'
141+
className='bg-zinc-800 aspect-[5/3] rounded-xl'
142+
>
143+
<canvas
144+
id='song-renderer'
145+
width={1280}
146+
height={720}
147+
className='w-full h-full rounded-xl'
148+
/>
149+
</div>
150+
);
151+
};

0 commit comments

Comments
 (0)