├── public ├── CNAME ├── roms │ ├── 3.nes │ ├── ma.nes │ ├── Kage.nes │ ├── Motor.nes │ ├── emc.nes │ ├── hun.nes │ ├── lkr.nes │ ├── rjbq.nes │ ├── sg1.nes │ ├── tanke.nes │ ├── xueren.nes │ ├── zhadan.nes │ ├── life-force.nes │ ├── maoxiandao.nes │ ├── Cross Fire (J).nes │ ├── Double Dragon2.nes │ ├── Zhong Guo Xiang Qi.nes │ ├── (J) (V1.2) Yie Ar Kung-Fu [!].nes │ └── Super Mario Bros. (JU) (PRG0) [!].nes ├── media │ └── bgm.mp3 └── favicon.svg ├── src ├── components │ ├── GameBgm.vue │ ├── SponsorAdsense.vue │ ├── controller │ │ ├── GithubLink.vue │ │ ├── ControllerFunction.vue │ │ ├── ControllerAction.vue │ │ └── ControllerJoystick.vue │ ├── GameMenu.vue │ └── GamePad.vue ├── shims-vue.d.ts ├── main.ts ├── App.vue ├── lib │ ├── console.ts │ ├── control.ts │ └── nes.ts ├── assets │ └── roms-list.json └── index.scss ├── .gitignore ├── vite.config.ts ├── tsconfig.json ├── .github └── workflows │ └── gh-pages.yml ├── package.json ├── README.md ├── LICENSE └── index.html /public/CNAME: -------------------------------------------------------------------------------- 1 | fc.elpsy.cn 2 | -------------------------------------------------------------------------------- /public/roms/3.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElpsyCN/fc/HEAD/public/roms/3.nes -------------------------------------------------------------------------------- /public/roms/ma.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElpsyCN/fc/HEAD/public/roms/ma.nes -------------------------------------------------------------------------------- /public/media/bgm.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElpsyCN/fc/HEAD/public/media/bgm.mp3 -------------------------------------------------------------------------------- /public/roms/Kage.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElpsyCN/fc/HEAD/public/roms/Kage.nes -------------------------------------------------------------------------------- /public/roms/Motor.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElpsyCN/fc/HEAD/public/roms/Motor.nes -------------------------------------------------------------------------------- /public/roms/emc.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElpsyCN/fc/HEAD/public/roms/emc.nes -------------------------------------------------------------------------------- /public/roms/hun.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElpsyCN/fc/HEAD/public/roms/hun.nes -------------------------------------------------------------------------------- /public/roms/lkr.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElpsyCN/fc/HEAD/public/roms/lkr.nes -------------------------------------------------------------------------------- /public/roms/rjbq.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElpsyCN/fc/HEAD/public/roms/rjbq.nes -------------------------------------------------------------------------------- /public/roms/sg1.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElpsyCN/fc/HEAD/public/roms/sg1.nes -------------------------------------------------------------------------------- /public/roms/tanke.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElpsyCN/fc/HEAD/public/roms/tanke.nes -------------------------------------------------------------------------------- /public/roms/xueren.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElpsyCN/fc/HEAD/public/roms/xueren.nes -------------------------------------------------------------------------------- /public/roms/zhadan.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElpsyCN/fc/HEAD/public/roms/zhadan.nes -------------------------------------------------------------------------------- /public/roms/life-force.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElpsyCN/fc/HEAD/public/roms/life-force.nes -------------------------------------------------------------------------------- /public/roms/maoxiandao.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElpsyCN/fc/HEAD/public/roms/maoxiandao.nes -------------------------------------------------------------------------------- /public/roms/Cross Fire (J).nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElpsyCN/fc/HEAD/public/roms/Cross Fire (J).nes -------------------------------------------------------------------------------- /public/roms/Double Dragon2.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElpsyCN/fc/HEAD/public/roms/Double Dragon2.nes -------------------------------------------------------------------------------- /public/roms/Zhong Guo Xiang Qi.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElpsyCN/fc/HEAD/public/roms/Zhong Guo Xiang Qi.nes -------------------------------------------------------------------------------- /public/roms/(J) (V1.2) Yie Ar Kung-Fu [!].nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElpsyCN/fc/HEAD/public/roms/(J) (V1.2) Yie Ar Kung-Fu [!].nes -------------------------------------------------------------------------------- /public/roms/Super Mario Bros. (JU) (PRG0) [!].nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElpsyCN/fc/HEAD/public/roms/Super Mario Bros. (JU) (PRG0) [!].nes -------------------------------------------------------------------------------- /src/components/GameBgm.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import { DefineComponent } from "vue"; 3 | const component: DefineComponent<{}, {}, any>; 4 | export default component; 5 | } 6 | 7 | declare module "jsnes"; 8 | -------------------------------------------------------------------------------- /src/components/SponsorAdsense.vue: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Custom 2 | .DS_Store 3 | yarn.lock 4 | package-lock.json 5 | pnpm-lock.yaml 6 | 7 | # Build 8 | dist 9 | dist-ssr 10 | 11 | # Logs 12 | *.log 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | 17 | # Dependency directories 18 | node_modules/ 19 | 20 | # dotenv environment variables file 21 | .env 22 | .env.test 23 | *.local 24 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "./App.vue"; 3 | import { consoleAllInfo } from "./lib/console"; 4 | 5 | import VueGtag from "vue-gtag"; 6 | 7 | import "./index.scss"; 8 | 9 | const app = createApp(App); 10 | app.use(VueGtag, { 11 | config: { 12 | id: "G-XMGX6YJVP8", 13 | }, 14 | }); 15 | app.mount("#app"); 16 | 17 | consoleAllInfo(); 18 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 19 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import vue from "@vitejs/plugin-vue"; 3 | import Icons, { ViteIconsResolver } from "vite-plugin-icons"; 4 | import Components from "vite-plugin-components"; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [ 9 | vue(), 10 | Icons(), 11 | Components({ 12 | customComponentResolvers: ViteIconsResolver(), 13 | }), 14 | ], 15 | }); 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "jsx": "preserve", 8 | "sourceMap": true, 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "lib": ["esnext", "dom"], 12 | "types": ["vite/client"] 13 | }, 14 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Pages 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | 12 | - name: Setup Node 13 | uses: actions/setup-node@v2 14 | with: 15 | node-version: "14.x" 16 | 17 | - run: npm install 18 | - run: npm run build 19 | 20 | - name: Deploy 21 | uses: peaceiris/actions-gh-pages@v3 22 | with: 23 | github_token: ${{ secrets.GITHUB_TOKEN }} 24 | publish_dir: ./dist 25 | force_orphan: true 26 | -------------------------------------------------------------------------------- /src/lib/console.ts: -------------------------------------------------------------------------------- 1 | import pkg from "../../package.json"; 2 | 3 | /** 4 | * 控制台输出信息 5 | * @param name 名称 6 | * @param link 链接 7 | * @param color 颜色 8 | * @param emoji 图标 9 | */ 10 | function consoleInfo( 11 | name: string, 12 | link: string, 13 | color?: string, 14 | emoji?: string 15 | ) { 16 | if (!color) { 17 | color = "#0078E7"; 18 | } 19 | console.log( 20 | `%c ${emoji || "☁️"} ${name} %c ${link}`, 21 | `color: white; background: ${color}; padding:5px 0;`, 22 | `padding:4px;border:1px solid ${color};` 23 | ); 24 | } 25 | 26 | export function consoleAllInfo() { 27 | consoleInfo("fc", pkg.repository.url, "#DA4A4A", "🎮"); 28 | consoleInfo("@" + pkg.author.name, pkg.repository.url); 29 | } 30 | -------------------------------------------------------------------------------- /src/components/controller/GithubLink.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | 20 | 21 | 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fc", 3 | "version": "0.1.0", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/ElpsyCN/fc" 8 | }, 9 | "author": { 10 | "url": "https://www.yunyoujun.cn", 11 | "email": "me@yunyoujun.cn", 12 | "name": "YunYouJun" 13 | }, 14 | "scripts": { 15 | "dev": "vite", 16 | "build": "vite build", 17 | "serve": "vite preview" 18 | }, 19 | "devDependencies": { 20 | "@vitejs/plugin-vue": "^1.2.2", 21 | "@vue/compiler-sfc": "^3.0.11", 22 | "sass": "^1.34.0", 23 | "typescript": "^4.3.2", 24 | "vite": "^2.3.4", 25 | "vite-plugin-components": "^0.10.2" 26 | }, 27 | "dependencies": { 28 | "@iconify/json": "^1.1.349", 29 | "axios": "^0.21.1", 30 | "jsnes": "^1.1.0", 31 | "vite-plugin-icons": "^0.5.1", 32 | "vue": "^3.0.11", 33 | "vue-gtag": "^2.0.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/controller/ControllerFunction.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 红白机 FC 2 | 3 | [![GitHub Pages](https://github.com/ElpsyCN/fc/actions/workflows/gh-pages.yml/badge.svg)](https://github.com/ElpsyCN/fc/actions/workflows/gh-pages.yml) 4 | 5 | > 预览地址: 6 | > 开发版预览: 7 | 8 | 使用 vue + vite + ts 重构 [dafeiyu/jsnes](https://gitee.com/feiyu22/jsnes)。 9 | 10 | ROM 基于 [JSNES](https://github.com/bfirsh/jsnes) 运行。 11 | 12 | ## Usage 13 | 14 | ```bash 15 | # 安装依赖 16 | # yarn 17 | pnpm i 18 | ``` 19 | 20 | ```bash 21 | # 启动 http://localhost:3000 22 | # yarn dev 23 | pnpm dev 24 | ``` 25 | 26 | ### 按键 27 | 28 | #### PC 端 29 | 30 | | 游戏按键 | 键盘 | 31 | | -------- | ---------------- | 32 | | 上下左右 | 方向键 | 33 | | START | Enter | 34 | | SELECT | Space | 35 | | A | A | 36 | | B | S | 37 | 38 | ## Features 39 | 40 | - 使用 Vue + Vite + TypeScript 重构 41 | - 增加了点击按钮时的样式反馈 42 | - 添加按钮名称 43 | - 绑定了 PC 按钮 44 | - 使用新版的 [JSNES](https://jsnes.org/) CDN 45 | 46 | ## Todo 47 | 48 | - 使用 Vue + Vite + TypeScript 重构 49 | - 添加谷歌统计 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 YunYouJun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 24 | 25 | 怀旧游戏机 26 | 27 | 28 |
29 | 30 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/components/GameMenu.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 42 | 43 | 52 | -------------------------------------------------------------------------------- /src/components/controller/ControllerAction.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | 16 | 54 | -------------------------------------------------------------------------------- /src/lib/control.ts: -------------------------------------------------------------------------------- 1 | import jsnes from "jsnes"; 2 | 3 | /** 4 | * 5 | * @param callback 6 | * @param event 7 | */ 8 | function keyboard(callback: Function, event: any) { 9 | var player = 1; 10 | switch (event.keyCode) { 11 | case 38: // UP 12 | callback(player, jsnes.Controller.BUTTON_UP); 13 | break; 14 | case 40: // Down 15 | callback(player, jsnes.Controller.BUTTON_DOWN); 16 | break; 17 | case 37: // Left 18 | callback(player, jsnes.Controller.BUTTON_LEFT); 19 | break; 20 | case 39: // Right 21 | callback(player, jsnes.Controller.BUTTON_RIGHT); 22 | break; 23 | case 65: // 'a' - qwerty, dvorak 24 | case 81: // 'q' - azerty 25 | callback(player, jsnes.Controller.BUTTON_A); 26 | break; 27 | case 83: // 's' - qwerty, azerty 28 | case 79: // 'o' - dvorak 29 | callback(player, jsnes.Controller.BUTTON_B); 30 | break; 31 | // case 9: // Tab 32 | case 32: 33 | callback(player, jsnes.Controller.BUTTON_SELECT); 34 | break; 35 | case 13: // Return 36 | callback(player, jsnes.Controller.BUTTON_START); 37 | break; 38 | default: 39 | break; 40 | } 41 | } 42 | 43 | /** 44 | * 为 nes 绑定时间 45 | * @param nes JSNES 实例 46 | */ 47 | export function bindKeyboard(nes: any) { 48 | // 绑定全局键盘按键 49 | document.addEventListener("keydown", (event) => { 50 | keyboard(nes.buttonDown, event); 51 | }); 52 | 53 | document.addEventListener("keyup", (event) => { 54 | keyboard(nes.buttonUp, event); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /src/assets/roms-list.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "超级玛丽", 4 | "path": "Super Mario Bros. (JU) (PRG0) [!].nes" 5 | }, 6 | { 7 | "name": "冒险岛", 8 | "path": "maoxiandao.nes" 9 | }, 10 | { 11 | "name": "魂斗罗", 12 | "path": "hun.nes" 13 | }, 14 | { 15 | "name": "中国象棋", 16 | "path": "Zhong Guo Xiang Qi.nes" 17 | }, 18 | { 19 | "name": "坦克大战", 20 | "path": "tanke.nes" 21 | }, 22 | { 23 | "name": "雪人兄弟", 24 | "path": "xueren.nes" 25 | }, 26 | { 27 | "name": "越野摩托", 28 | "path": "Motor.nes" 29 | }, 30 | { 31 | "name": "赤影战士", 32 | "path": "Kage.nes" 33 | }, 34 | { 35 | "name": "炸弹人", 36 | "path": "zhadan.nes" 37 | }, 38 | { 39 | "name": "马戏团", 40 | "path": "ma.nes" 41 | }, 42 | { 43 | "name": "影子传说", 44 | "path": "Kage.nes" 45 | }, 46 | { 47 | "name": "沙罗曼蛇", 48 | "path": "life-force.nes" 49 | }, 50 | { 51 | "name": "双截龙2", 52 | "path": "Double Dragon2.nes" 53 | }, 54 | { 55 | "name": "脱狱", 56 | "path": "Cross Fire (J).nes" 57 | }, 58 | { 59 | "name": "功夫", 60 | "path": "(J) (V1.2) Yie Ar Kung-Fu [!].nes" 61 | }, 62 | { 63 | "name": "三目童子", 64 | "path": "3.nes" 65 | }, 66 | { "name": "恶魔城1", "path": "emc.nes" }, 67 | { 68 | "name": "洛克人1", 69 | "path": "lkr.nes" 70 | }, 71 | { "name": "人间兵器", "path": "rjbq.nes" }, 72 | { 73 | "name": "忍者神龟1", 74 | "path": "sg1.nes" 75 | }, 76 | { 77 | "name": "激龟快打", 78 | "path": "sg4.nes" 79 | }, 80 | { 81 | "name": "泡泡龙2", 82 | "path": "ppl2.nes" 83 | }, 84 | { 85 | "name": "西游记1", 86 | "path": "xyj.nes" 87 | }, 88 | 89 | { 90 | "name": "淘金者", 91 | "path": "Championship Lode Runner (J).nes" 92 | } 93 | ] 94 | -------------------------------------------------------------------------------- /src/components/GamePad.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 88 | -------------------------------------------------------------------------------- /src/lib/nes.ts: -------------------------------------------------------------------------------- 1 | import jsnes from "jsnes"; 2 | 3 | // 屏幕宽度 4 | const SCREEN_WIDTH = 256; 5 | // 屏幕高度 6 | const SCREEN_HEIGHT = 240; 7 | const FRAMEBUFFER_SIZE = SCREEN_WIDTH * SCREEN_HEIGHT; 8 | 9 | /** 10 | * 创建 JSNES 实例 11 | * @param canvasId 画布 ID 12 | * @returns 13 | */ 14 | export function createNes(canvasId: string) { 15 | // init 16 | const canvas = document.getElementById(canvasId) as HTMLCanvasElement; 17 | const ctx = canvas.getContext("2d"); 18 | if (!ctx) { 19 | console.log("画布不存在"); 20 | return; 21 | } 22 | const imageData = ctx.getImageData(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); 23 | 24 | ctx.fillStyle = "black"; 25 | ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); 26 | 27 | // Allocate framebuffer array. 28 | const buffer = new ArrayBuffer(imageData.data.length); 29 | const framebuffer_u8 = new Uint8ClampedArray(buffer); 30 | const framebuffer_u32 = new Uint32Array(buffer); 31 | 32 | // Setup audio. 33 | const AUDIO_BUFFERING = 512; 34 | const audioCtx = new window.AudioContext(); 35 | const script_processor = audioCtx.createScriptProcessor( 36 | AUDIO_BUFFERING, 37 | 0, 38 | 2 39 | ); 40 | script_processor.onaudioprocess = audio_callback; 41 | script_processor.connect(audioCtx.destination); 42 | 43 | const SAMPLE_COUNT = 4 * 1024; 44 | const SAMPLE_MASK = SAMPLE_COUNT - 1; 45 | const audio_samples_L = new Float32Array(SAMPLE_COUNT); 46 | const audio_samples_R = new Float32Array(SAMPLE_COUNT); 47 | 48 | let audio_write_cursor = 0; 49 | let audio_read_cursor = 0; 50 | 51 | const nes = new jsnes.NES({ 52 | onFrame(framebuffer_24: Uint32Array) { 53 | for (let i = 0; i < FRAMEBUFFER_SIZE; i++) 54 | framebuffer_u32[i] = 0xff000000 | framebuffer_24[i]; 55 | }, 56 | onAudioSample(l: any, r: any) { 57 | audio_samples_L[audio_write_cursor] = l; 58 | audio_samples_R[audio_write_cursor] = r; 59 | audio_write_cursor = (audio_write_cursor + 1) & SAMPLE_MASK; 60 | }, 61 | }); 62 | 63 | function onAnimationFrame() { 64 | if (!ctx) { 65 | return; 66 | } 67 | 68 | window.requestAnimationFrame(onAnimationFrame); 69 | 70 | imageData.data.set(framebuffer_u8); 71 | ctx.putImageData(imageData, 0, 0); 72 | } 73 | 74 | function audio_remain() { 75 | return (audio_write_cursor - audio_read_cursor) & SAMPLE_MASK; 76 | } 77 | 78 | function audio_callback(event: any) { 79 | const dst = event.outputBuffer; 80 | const len = dst.length; 81 | 82 | // Attempt to avoid buffer underruns. 83 | if (audio_remain() < AUDIO_BUFFERING) nes.frame(); 84 | 85 | let dst_l = dst.getChannelData(0); 86 | let dst_r = dst.getChannelData(1); 87 | for (let i = 0; i < len; i++) { 88 | const src_idx = (audio_read_cursor + i) & SAMPLE_MASK; 89 | dst_l[i] = audio_samples_L[src_idx]; 90 | dst_r[i] = audio_samples_R[src_idx]; 91 | } 92 | 93 | audio_read_cursor = (audio_read_cursor + len) & SAMPLE_MASK; 94 | } 95 | 96 | return { 97 | /** 98 | * 实例 99 | */ 100 | instance: nes, 101 | 102 | /** 103 | * 绑定按钮 104 | * @param button 105 | * @returns 106 | */ 107 | bindButton(name: string) { 108 | const buttonName = "BUTTON_" + name; 109 | const btn = document.querySelector( 110 | `[role="${buttonName}"]` 111 | ) as HTMLElement; 112 | if (!btn) { 113 | return; 114 | } 115 | 116 | const onButtonDown = () => { 117 | nes.buttonDown(1, jsnes.Controller[buttonName]); 118 | }; 119 | 120 | const onButtonUp = () => { 121 | nes.buttonUp(1, jsnes.Controller[buttonName]); 122 | }; 123 | 124 | btn.ontouchstart = onButtonDown; 125 | btn.onmousedown = onButtonDown; 126 | btn.ontouchend = onButtonUp; 127 | btn.onmouseup = onButtonUp; 128 | }, 129 | 130 | /** 131 | * 加载 ROM 132 | * @param url 路径 133 | */ 134 | async load(url: string) { 135 | const req = new XMLHttpRequest(); 136 | req.open("GET", url); 137 | req.overrideMimeType("text/plain; charset=x-user-defined"); 138 | req.onload = () => { 139 | if (req.status === 200) { 140 | // console.log(this.responseText); 141 | // nes_boot(); 142 | this.boot(req.responseText); 143 | } else if (req.status === 0) { 144 | // Aborted, so ignore error 145 | } else { 146 | console.log(`Error loading ${url}: ${req.statusText}`); 147 | } 148 | }; 149 | req.send(); 150 | return req.responseText; 151 | }, 152 | 153 | /** 154 | * 启动 ROM 155 | * @param rom_data 156 | */ 157 | boot(rom_data: string) { 158 | nes.loadROM(rom_data); 159 | window.requestAnimationFrame(onAnimationFrame); 160 | }, 161 | }; 162 | } 163 | -------------------------------------------------------------------------------- /src/components/controller/ControllerJoystick.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 74 | 75 | 76 | 185 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | height: 100%; 5 | background: #d0e7f9; 6 | -webkit-touch-callout: none; 7 | -webkit-user-select: none; 8 | -khtml-user-select: none; 9 | -moz-user-select: none; 10 | -ms-user-select: none; 11 | user-select: none; 12 | } 13 | 14 | button { 15 | cursor: pointer; 16 | } 17 | 18 | .main { 19 | position: absolute; 20 | left: 0; 21 | top: 0; 22 | right: 0; 23 | bottom: 0; 24 | box-sizing: border-box; 25 | max-width: 812px; 26 | border-radius: 10px; 27 | margin: auto; 28 | box-shadow: 0 0 0 5px #da4a4a, 0 0 0 20px #474f51; 29 | background: #da4a4a; 30 | height: 100%; 31 | max-height: 414px; 32 | padding: 10px; 33 | } 34 | 35 | .panel { 36 | position: relative; 37 | height: 100%; 38 | padding: 20px; 39 | box-sizing: border-box; 40 | background: #f8f1d7; 41 | border-radius: 15px; 42 | display: flex; 43 | flex-direction: row; 44 | box-shadow: inset 8px 8px #fffef7; 45 | } 46 | 47 | .function-area { 48 | flex: 1; 49 | display: flex; 50 | padding: 0 20px; 51 | justify-content: center; 52 | flex-direction: column; 53 | } 54 | 55 | .controller-area { 56 | position: relative; 57 | z-index: 10; 58 | display: flex; 59 | flex-direction: row; 60 | perspective: 400px; 61 | } 62 | 63 | .action-area { 64 | display: flex; 65 | flex-direction: row; 66 | } 67 | 68 | .function { 69 | display: flex; 70 | padding: 8px 15px; 71 | border-radius: 50px; 72 | align-self: center; 73 | background: #da4a4a; 74 | box-shadow: 5px 5px 0 rgba(255, 255, 255, 0.8); 75 | } 76 | 77 | .sign { 78 | position: absolute; 79 | font-weight: bold; 80 | font-size: 20px; 81 | font-style: italic; 82 | height: 50px; 83 | right: 0; 84 | top: 0; 85 | background: #da4a4a; 86 | color: #f8f1d7; 87 | text-shadow: 0 -2px #fffef7; 88 | padding: 0 0 15px 15px; 89 | letter-spacing: 0.1em; 90 | border-bottom-left-radius: 15px; 91 | filter: drop-shadow(0 8px #fffef7); 92 | } 93 | 94 | .sign::before { 95 | content: ""; 96 | position: absolute; 97 | width: 15px; 98 | height: 15px; 99 | left: -15px; 100 | top: 0; 101 | background: radial-gradient( 102 | circle at left bottom, 103 | transparent 14px, 104 | #da4a4a 15px 105 | ); 106 | } 107 | 108 | .sign::after { 109 | content: ""; 110 | position: absolute; 111 | width: 15px; 112 | height: 15px; 113 | bottom: -15px; 114 | right: 0; 115 | background: radial-gradient( 116 | circle at left bottom, 117 | transparent 14px, 118 | #da4a4a 15px 119 | ); 120 | } 121 | 122 | .readme { 123 | position: absolute; 124 | left: 0; 125 | top: 90px; 126 | width: 90px; 127 | height: 30px; 128 | background: #857b7a; 129 | border-radius: 5px; 130 | text-align: center; 131 | line-height: 30px; 132 | color: #f8f1d7; 133 | font-weight: bold; 134 | font-size: 20px; 135 | } 136 | 137 | .screen { 138 | flex: 1; 139 | display: flex; 140 | width: 100%; 141 | transition: 0.3s; 142 | background: #000; 143 | margin-bottom: 5px; 144 | border-radius: 10px; 145 | align-items: center; 146 | justify-content: center; 147 | overflow: hidden; 148 | } 149 | 150 | .screen canvas { 151 | max-width: 100%; 152 | max-height: 100%; 153 | } 154 | 155 | .nes-roms { 156 | margin: 0 auto; 157 | text-align: center; 158 | 159 | select { 160 | width: 200px; 161 | } 162 | } 163 | 164 | .nes-controls { 165 | margin-top: 2px; 166 | } 167 | 168 | @media screen and (orientation: portrait) { 169 | /*竖屏 css*/ 170 | .main { 171 | max-height: 100%; 172 | } 173 | .function-area { 174 | position: absolute; 175 | left: 0; 176 | right: 0; 177 | top: 85px; 178 | bottom: 100px; 179 | padding-bottom: 80px; 180 | } 181 | .function { 182 | position: absolute; 183 | bottom: 0; 184 | align-self: flex-end; 185 | transform: translateX(20px); 186 | padding: 8px 10px; 187 | border-radius: 50px 0 0 50px; 188 | box-shadow: 0px 5px 0 rgba(255, 255, 255, 0.8); 189 | } 190 | .function::before { 191 | content: ""; 192 | position: absolute; 193 | width: 15px; 194 | height: 15px; 195 | right: 0; 196 | top: -15px; 197 | background: radial-gradient( 198 | circle at left top, 199 | transparent 14px, 200 | #da4a4a 15px 201 | ); 202 | } 203 | .function::after { 204 | content: ""; 205 | position: absolute; 206 | width: 15px; 207 | height: 15px; 208 | bottom: -15px; 209 | right: 0; 210 | background: radial-gradient( 211 | circle at left bottom, 212 | transparent 14px, 213 | #da4a4a 15px 214 | ); 215 | } 216 | .screen { 217 | margin-bottom: 0; 218 | max-height: 300px; 219 | } 220 | .action-area { 221 | flex: 1; 222 | justify-content: flex-end; 223 | } 224 | } 225 | 226 | @media screen and (orientation: landscape) { 227 | /*横屏 css*/ 228 | } 229 | --------------------------------------------------------------------------------