├── .GitHub └── workflows │ └── nodejs.yml ├── .env.template ├── .gitattributes ├── .gitignore ├── .idx └── dev.nix ├── .npmrc ├── README.md ├── SVELTE-SETUP.md ├── index.js ├── package-lock.json ├── package.json ├── src ├── app.html ├── lib │ ├── AccountStatus │ │ └── Standing.svelte │ ├── Alert │ │ ├── Alert.svelte │ │ └── StatusAlert.svelte │ ├── AutoLocalizedText │ │ └── Node.svelte │ ├── Badge.svelte │ ├── BarButton │ │ └── Button.svelte │ ├── BarPage │ │ └── Button.svelte │ ├── BarSearch │ │ └── Search.svelte │ ├── Button │ │ └── Button.svelte │ ├── Captcha.svelte │ ├── ChecksBox │ │ └── ChecksBox.svelte │ ├── CircularProgress.svelte │ ├── ClickableProject │ │ └── Project.svelte │ ├── ContentCategory │ │ └── Component.svelte │ ├── Event │ │ └── Component.svelte │ ├── Game404 │ │ └── Component.svelte │ ├── LoadingSpinner │ │ ├── Spinner.svelte │ │ └── Tips.json │ ├── LocalizedText │ │ └── Node.svelte │ ├── NavigationBar │ │ ├── NavMargin.svelte │ │ └── NavigationBar.svelte │ ├── Project │ │ └── Project.svelte │ ├── UserDisplay │ │ └── Display.svelte │ ├── scratchblocks.js │ └── statsComponent │ │ └── stats.svelte ├── resources │ ├── authentication.js │ ├── autoTranslate.js │ ├── badges.js │ ├── basiccensorship.js │ ├── blobanddataurl.js │ ├── country-lookup.json │ ├── emojis-compat.js │ ├── emojis.js │ ├── html.js │ ├── icons │ │ ├── Penguin │ │ │ └── confused.svelte │ │ └── Search │ │ │ └── icon.svelte │ ├── language.js │ ├── markdown │ │ ├── devposts │ │ │ ├── pages.js │ │ │ └── shutdownincident.md │ │ ├── events │ │ │ ├── en │ │ │ │ ├── example.md │ │ │ │ ├── penguinjamspring2025.md │ │ │ │ └── penguinjamwinter2024.md │ │ │ ├── es-419 │ │ │ │ └── example.md │ │ │ ├── es │ │ │ │ ├── penguinjamspring2025.md │ │ │ │ └── penguinjamwinter2024.md │ │ │ ├── fr │ │ │ │ └── penguinjamwinter2024.md │ │ │ ├── ja │ │ │ │ ├── penguinjamspring2025.md │ │ │ │ └── penguinjamwinter2024.md │ │ │ ├── pages.js │ │ │ ├── pl │ │ │ │ └── penguinjamspring2025.md │ │ │ ├── ru │ │ │ │ └── penguinjamwinter2024.md │ │ │ ├── tr │ │ │ │ ├── penguinjamspring2025.md │ │ │ │ └── penguinjamwinter2024.md │ │ │ └── uk │ │ │ │ └── penguinjamspring2025.md │ │ └── guidelines │ │ │ ├── moderation.md │ │ │ ├── pages.js │ │ │ └── uploading.md │ ├── projectapi.js │ ├── translations.js │ ├── urls.js │ └── vr │ │ ├── htmlrenderer │ │ ├── element.js │ │ ├── elements │ │ │ ├── button.js │ │ │ ├── cube.js │ │ │ ├── img.js │ │ │ ├── modifier.js │ │ │ ├── p.js │ │ │ └── parsererror.js │ │ ├── index.js │ │ ├── tagpairs.js │ │ └── texture │ │ │ ├── font.js │ │ │ ├── index.js │ │ │ ├── nineslice.js │ │ │ └── text.js │ │ ├── index.js │ │ ├── menus │ │ ├── home.xml │ │ ├── index.js │ │ ├── loading.xml │ │ └── login.xml │ │ └── util │ │ ├── direction.js │ │ └── typeconverter.js ├── routes │ ├── +error.svelte │ ├── +page.svelte │ ├── PenguinMod-Packager │ │ └── +page.svelte │ ├── canceldonation │ │ └── +page.svelte │ ├── changepassword │ │ └── +page.svelte │ ├── devposts │ │ ├── +layout.svelte │ │ ├── +page.svelte │ │ └── [slug] │ │ │ ├── +page.js │ │ │ └── +page.svelte │ ├── donate │ │ └── +page.svelte │ ├── edit │ │ └── +page.svelte │ ├── embed │ │ ├── editor │ │ │ └── +page.svelte │ │ └── vote │ │ │ └── +page.svelte │ ├── events │ │ ├── +layout.svelte │ │ ├── +page.svelte │ │ └── [slug] │ │ │ ├── +page.js │ │ │ └── +page.svelte │ ├── forgotpassword │ │ └── +page.svelte │ ├── guidelines │ │ ├── +layout.svelte │ │ ├── +page.svelte │ │ └── [slug] │ │ │ ├── +page.js │ │ │ └── +page.svelte │ ├── loading │ │ └── +page.svelte │ ├── messages │ │ └── +page.svelte │ ├── mystuff │ │ └── +page.svelte │ ├── oauthchangepasswordintermediate │ │ └── +page.svelte │ ├── panel │ │ ├── +page.svelte │ │ ├── filetypes.js │ │ ├── filetypes.json │ │ ├── magicNumbers.js │ │ └── quickRejects.svelte │ ├── privacy │ │ └── +page.svelte │ ├── profile │ │ └── +page.svelte │ ├── project │ │ └── [slug] │ │ │ ├── +page.js │ │ │ └── +page.svelte │ ├── redirect │ │ └── +page.svelte │ ├── report │ │ └── +page.svelte │ ├── resetpassword │ │ └── +page.svelte │ ├── restore │ │ └── +page.svelte │ ├── search │ │ └── +page.svelte │ ├── settings │ │ └── +page.svelte │ ├── signin │ │ └── +page.svelte │ ├── signup │ │ └── +page.svelte │ ├── standing │ │ └── +page.svelte │ ├── successdonation │ │ └── +page.svelte │ ├── sucessdonation │ │ └── +page.svelte │ ├── terms │ │ └── +page.svelte │ ├── unfinishedsignup │ │ └── +page.svelte │ ├── upload │ │ └── +page.svelte │ ├── userpanel │ │ └── +page.svelte │ └── vr │ │ ├── +page.js │ │ └── +page.svelte └── translations │ ├── !all-locale-text.js │ ├── !csv-to.json.js │ ├── !json-to-csv.js │ ├── !locales.js │ ├── !make-test-lang.js │ ├── !merge-locales.js │ ├── cs.json │ ├── da.json │ ├── de.json │ ├── en-reversed.json │ ├── en.json │ ├── es-419.json │ ├── es.json │ ├── fa.json │ ├── fr-ca.json │ ├── fr.json │ ├── he.json │ ├── hr.json │ ├── id.json │ ├── it.json │ ├── ja.json │ ├── no.json │ ├── pl.json │ ├── pt-br.json │ ├── ro.json │ ├── ru.json │ ├── sk.json │ ├── sv.json │ ├── test.json │ ├── tr.json │ ├── uk.json │ └── vi.json ├── static ├── 05-02-1998_test_screenshot.png ├── 8ball.html ├── MENGUINPOD-THETRUTH.html ├── Scratch_S.svg ├── Sprite1.pms ├── account │ ├── add.svg │ ├── hidepassword.svg │ ├── lock.svg │ ├── profile_sheet.png │ ├── remove.svg │ ├── showpassword.svg │ ├── standing_arrow.png │ ├── status_banned.svg │ ├── status_good.svg │ ├── status_limited.svg │ ├── status_warn.svg │ └── temp.html ├── addons.html ├── ads.txt ├── alert_icon.svg ├── alert_icon_white.svg ├── badges │ ├── admin.png │ ├── approver.png │ ├── aprilfools.png │ ├── artistblue.png │ ├── betatester.png │ ├── birthday.png │ ├── birthdayaccount.png │ ├── bugreportbeta.png │ ├── contributor.png │ ├── crowngreen.png │ ├── crownpurple.png │ ├── crownred.png │ ├── crownwhite.png │ ├── crownyellow.png │ ├── developer.png │ ├── discord_mod.png │ ├── donator.png │ ├── donator2.png │ ├── eventmanager.png │ ├── eventwinner.png │ ├── eventwinnerbronze.png │ ├── eventwinnersilver.png │ ├── extension_developer.png │ ├── featured_creator.png │ ├── featured_studio_creator.png │ ├── followers_many.png │ ├── hatpurple.png │ ├── hatyellow.png │ ├── large_studio.png │ ├── likes_many.png │ ├── modfrontpaged.png │ ├── multifeature.png │ ├── owner.png │ ├── participant.png │ ├── partner.png │ ├── penguinjambronze.png │ ├── penguinjamdiamond.png │ ├── penguinjamgold.png │ ├── penguinjamobsidian.png │ ├── penguinjamplatinum.png │ ├── penguinjamsilver.png │ ├── starpink.png │ ├── starred.png │ ├── studiopenguinjamwinner.png │ ├── test.png │ ├── test2.png │ ├── test3.png │ ├── translator.png │ ├── verified.png │ ├── verifiedofficial.png │ ├── votes_many.png │ └── vulnerability_report.png ├── cashapp.png ├── cat │ ├── dave.png │ └── speak.mp3 ├── censor │ ├── costume.svg │ └── sound.mp3 ├── checkmark.png ├── create.png ├── credits.html ├── devposts │ └── 3-18-2025-shutdown-incident.webp ├── discord_black.png ├── discord_dark.png ├── discord_light.png ├── discord_white.png ├── dismiss.svg ├── dot_blue.png ├── dots.svg ├── dropdown-caret-hd.png ├── dropdown-caret.png ├── eao.html ├── editor.html ├── empty-project.png ├── errors │ ├── 401.png │ ├── 403.png │ ├── 405.png │ ├── 408.png │ ├── 409.png │ ├── 413.png │ ├── 415.png │ ├── 429.png │ ├── 501.png │ ├── 502.png │ ├── 503.png │ ├── 504.png │ ├── 507.png │ └── 508.png ├── events │ ├── en │ │ ├── example.webp │ │ ├── penguinjamspring2025.webp │ │ └── penguinjamwinter2024.webp │ ├── es-419 │ │ └── example.webp │ ├── es │ │ ├── penguinjamspring2025.webp │ │ └── penguinjamwinter2024.webp │ ├── example.pdn │ ├── example.png │ ├── fr │ │ └── penguinjamwinter2024.webp │ ├── ja │ │ ├── penguinjamspring2025.webp │ │ └── penguinjamwinter2024.webp │ ├── news │ │ ├── penguinjamspring2025.png │ │ └── penguinjamwinter2024.png │ ├── pl │ │ └── penguinjamspring2025.webp │ ├── ru │ │ └── penguinjamwinter2024.webp │ ├── template.png │ ├── tr │ │ ├── penguinjamspring2025.webp │ │ └── penguinjamwinter2024.webp │ └── uk │ │ └── penguinjamspring2025.webp ├── favicon.ico ├── favicon.png ├── favicon.svg ├── favicon_green.png ├── feature.svg ├── fonts │ └── ofl │ │ ├── BadComic-Regular.ttf │ │ ├── Eirian-Regular.ttf │ │ ├── Grand9K-Pixel.ttf │ │ ├── Griffy-Regular.ttf │ │ ├── Knewave.ttf │ │ ├── OFL.txt │ │ ├── QTKooper.otf │ │ ├── Sono-Medium.woff2 │ │ ├── UnispaceRg.otf │ │ └── handlee-regular.ttf ├── fullscreen.html ├── github-mark │ ├── github-mark-white.png │ ├── github-mark-white.svg │ ├── github-mark.png │ └── github-mark.svg ├── globe.svg ├── google.svg ├── googled66079463a3978a9.html ├── happy.svg ├── heart.svg ├── line_blue.png ├── loading.png ├── loading_corner.png ├── loading_corner_white.png ├── loading_corners.png ├── loading_corners_white.png ├── loading_thingy.png ├── loading_thingy_white.png ├── loading_white.png ├── messagesstatic │ ├── badge.png │ ├── create.svg │ ├── download.png │ ├── featured.png │ ├── follow.png │ ├── messages.svg │ ├── mystuff.svg │ ├── panel.svg │ ├── profile.svg │ ├── translate.png │ └── upload.svg ├── moon.svg ├── navicon.png ├── navicon_dark.png ├── notallowed.png ├── paint.svg ├── paypal.png ├── pencil.png ├── penguins │ ├── cheer.svg │ ├── confused.svg │ ├── despair.svg │ ├── donate.svg │ ├── donating.svg │ ├── frontpage.svg │ ├── rankup.svg │ ├── server.svg │ └── signin.svg ├── privacy.html ├── projectimages │ ├── love.svg │ ├── view.svg │ └── vote.svg ├── rating │ ├── blood.png │ ├── e.png │ ├── e10.png │ ├── flashing.png │ ├── horror.png │ ├── none.png │ ├── strong_language.png │ ├── t.png │ └── t_v2.png ├── recompilepls.txt ├── redirect.css ├── remix.svg ├── report_flag.png ├── secret │ ├── brick.png │ ├── bricks.png │ ├── floor.png │ ├── pengin.svg │ ├── penginDead.svg │ └── sky.png ├── stage_controls │ ├── flag_angled.svg │ ├── gradient │ │ ├── flag.svg │ │ ├── pause.svg │ │ ├── play.svg │ │ └── stop.svg │ ├── outline │ │ ├── flag.svg │ │ ├── pause.svg │ │ ├── play.svg │ │ └── stop.svg │ ├── recolored │ │ ├── flag.svg │ │ ├── pause.svg │ │ ├── play.svg │ │ └── stop.svg │ ├── recolored_simple │ │ ├── flag.svg │ │ ├── pause.svg │ │ ├── play.svg │ │ └── stop.svg │ └── simple │ │ ├── flag.svg │ │ ├── pause.svg │ │ ├── play.svg │ │ └── stop.svg ├── stripe.png ├── test.png ├── tryit.svg ├── unknown_user.png ├── update.svg ├── upload.svg ├── view.svg ├── vote │ ├── feature.svg │ ├── feature_white.svg │ ├── heart.svg │ ├── heart_white.svg │ ├── view.svg │ └── view_white.svg ├── vr │ ├── deselect.mp3 │ ├── enter.mp3 │ ├── exit.png │ ├── hover.mp3 │ ├── nineslices │ │ └── button.png │ ├── platform.png │ ├── skybox_back.png │ ├── skybox_bottom.png │ ├── skybox_front.png │ ├── skybox_left.png │ ├── skybox_right.png │ ├── skybox_top.png │ ├── themes │ │ ├── firstblocks_01.mp3 │ │ ├── firstblocks_02.mp3 │ │ ├── firstblocks_03.mp3 │ │ ├── home.mp3 │ │ ├── home_firstloop.mp3 │ │ └── selection.mp3 │ ├── type.mp3 │ ├── warning.png │ ├── white.png │ └── wind.mp3 ├── warning.png └── warning_yellow.png ├── svelte.config.js └── vite.config.js /.GitHub/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Remote Dispatch Action Initiator 2 | 3 | #test 4 | 5 | on: [push] 6 | 7 | jobs: 8 | 9 | ping-pong: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | 15 | - name: Repository Dispatch 16 | 17 | uses: peter-evans/repository-dispatch@v2.0.1 18 | 19 | with: 20 | 21 | token: ${{ secrets.t }} 22 | 23 | event-type: update 24 | 25 | repository: PenguinMod/penguinmod.github.io 26 | 27 | - name: Repository Dispatch2 28 | 29 | uses: peter-evans/repository-dispatch@v2.0.1 30 | 31 | with: 32 | 33 | token: ${{ secrets.t }} 34 | 35 | event-type: update 36 | 37 | repository: PenguinMod/PenguinMod-Packager 38 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | PUBLIC_API_URL=https://projects.penguinmod.com 2 | PUBLIC_STUDIO_URL=https://studio.penguinmod.com 3 | PUBLIC_MAX_UPLOAD_SIZE=32 # in megabytes 4 | PUBLIC_CAPTCHA_ENABLED=true # If this is false, it must also be false on API to function correctly. -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | !.env.example 8 | vite.config.js.timestamp-* 9 | vite.config.ts.timestamp-* 10 | .vercel/* 11 | -------------------------------------------------------------------------------- /.idx/dev.nix: -------------------------------------------------------------------------------- 1 | {pkgs}: { 2 | channel = "stable-24.05"; 3 | packages = [ 4 | pkgs.nodejs_20 5 | ]; 6 | env = { 7 | PUBLIC_API_URL="https://projects.penguinmod.com"; 8 | PUBLIC_STUDIO_URL="https://studio.penguinmod.com"; 9 | PUBLIC_MAX_UPLOAD_SIZE="32"; 10 | PUBLIC_CAPTCHA_ENABLED="true"; 11 | }; 12 | idx.extensions = [ 13 | "svelte.svelte-vscode" 14 | "vue.volar" 15 | ]; 16 | idx.previews = { 17 | previews = { 18 | web = { 19 | command = [ 20 | "npm" 21 | "run" 22 | "dev" 23 | "--" 24 | "--port" 25 | "$PORT" 26 | "--host" 27 | "0.0.0.0" 28 | ]; 29 | manager = "web"; 30 | }; 31 | }; 32 | }; 33 | idx.workspace.onCreate = { 34 | npm-install = "npm i --force"; 35 | }; 36 | } -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | resolution-mode=highest 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PenguinMod-Home 2 | The main home page for PenguinMod, which has community-made projects and other content. 3 | -------------------------------------------------------------------------------- /SVELTE-SETUP.md: -------------------------------------------------------------------------------- 1 | # create-svelte 2 | 3 | Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte). 4 | 5 | ## Creating a project 6 | 7 | If you're seeing this, you've probably already done this step. Congrats! 8 | 9 | ```bash 10 | # create a new project in the current directory 11 | npm create svelte@latest 12 | 13 | # create a new project in my-app 14 | npm create svelte@latest my-app 15 | ``` 16 | 17 | ## Developing 18 | 19 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 20 | 21 | ```bash 22 | npm run dev 23 | 24 | # or start the server and open the app in a new browser tab 25 | npm run dev -- --open 26 | ``` 27 | 28 | ## Building 29 | 30 | To create a production version of your app: 31 | 32 | ```bash 33 | npm run build 34 | ``` 35 | 36 | You can preview the production build with `npm run preview`. 37 | 38 | > To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. 39 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 1 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "penguinmod-home", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "vite build", 8 | "preview": "vite preview" 9 | }, 10 | "devDependencies": { 11 | "@sveltejs/adapter-vercel": "^2.4.3", 12 | "@sveltejs/kit": "^1.5.0", 13 | "svelte": "^3.54.0", 14 | "vite": "^4.3.0" 15 | }, 16 | "type": "module", 17 | "dependencies": { 18 | "file-saver": "^2.0.5", 19 | "jszip": "^3.10.1", 20 | "markdown-it": "^13.0.2", 21 | "pmp-protobuf": "^1.2.0", 22 | "protobufjs": "^7.3.2", 23 | "svelte-confetti": "^1.4.0", 24 | "three": "^0.158.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/Alert/StatusAlert.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 | {#if currentStatus.type !== "empty"} 29 | 38 | {/if} 39 | -------------------------------------------------------------------------------- /src/lib/AutoLocalizedText/Node.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | {text} 16 | -------------------------------------------------------------------------------- /src/lib/Badge.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | 64 | 65 | -------------------------------------------------------------------------------- /src/lib/BarButton/Button.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | {#if link} 11 | 17 | 21 | 22 | {/if} 23 | {#if !link} 24 | 32 | {/if} 33 | 34 | 68 | -------------------------------------------------------------------------------- /src/lib/BarPage/Button.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 | {#if !link} 33 | 43 | {:else} 44 | 51 | 55 | 56 | {/if} 57 | 58 | 94 | -------------------------------------------------------------------------------- /src/lib/Captcha.svelte: -------------------------------------------------------------------------------- 1 | 46 | 47 | 48 | {#if String(PUBLIC_CAPTCHA_ENABLED) !== "false"} 49 | 50 | {/if} 51 | 52 | 53 | {#if String(PUBLIC_CAPTCHA_ENABLED) === "false"} 54 |
55 | 56 | {#if mockExpired} 57 | You took too long 58 | {:else if mockCompleted} 59 | ✅ Success! 60 | {:else} 61 | 62 | I'm not a robot 63 | {/if} 64 | 65 |
66 |
67 | 68 | 69 |
70 | Emulator, Captcha is disabled in .env, see PUBLIC_CAPTCHA_ENABLED 71 |
72 | {:else} 73 |
80 | {/if} -------------------------------------------------------------------------------- /src/lib/ChecksBox/ChecksBox.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 |
22 | {#each items as item} 23 |
24 | 25 | 26 | {#if item.value === "loading"} 27 | 28 | {:else if item.value} 29 | Success 30 | {:else} 31 | Fail 32 | {/if} 33 | 34 | 39 |
40 | {/each} 41 |
42 | 43 | -------------------------------------------------------------------------------- /src/lib/CircularProgress.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 | 34 | 35 |
-------------------------------------------------------------------------------- /src/lib/ContentCategory/Component.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 |
23 | {#if header} 24 |
25 |

{header}

26 | {#if seemore} 27 |

28 | 29 | 34 | 35 |

36 | {/if} 37 |
38 | {/if} 39 |
40 |
41 | 42 | 76 | -------------------------------------------------------------------------------- /src/lib/Event/Component.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | {#if isHavingEvent} 24 | 25 | 32 | 33 | {/if} 34 | 35 | 69 | -------------------------------------------------------------------------------- /src/lib/LoadingSpinner/Spinner.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 | {#if single} 27 | Loading 33 | {:else} 34 |
35 | Loading 41 | {#if enableTips} 42 |
43 |

44 | 49 |

50 | {/if} 51 |
52 | {/if} 53 | 54 | 90 | -------------------------------------------------------------------------------- /src/lib/LocalizedText/Node.svelte: -------------------------------------------------------------------------------- 1 | 44 | 45 | {#if shouldHTML} 46 | {@html displayText} 47 | {:else} 48 | {displayText} 49 | {/if} 50 | -------------------------------------------------------------------------------- /src/lib/NavigationBar/NavMargin.svelte: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /src/lib/UserDisplay/Display.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | 11 | Display Author 12 | 13 |
14 | {text} 15 | {author} 16 |
17 |
18 | 19 | 62 | -------------------------------------------------------------------------------- /src/lib/statsComponent/stats.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | {#if stats_data && render} 20 | {#each stats_data as stat} 21 | {#if typeof stat === 'object'} 22 | 23 |

{object_clicks[stat.name] = !object_clicks[stat.name]}} 26 | > 27 | {object_clicks[stat.name] ? "v" : ">"} {stat.name} 28 |

29 |
30 | 31 |
32 | {:else} 33 | {stat}
34 | {/if} 35 | {/each} 36 | {/if} 37 | 38 | -------------------------------------------------------------------------------- /src/resources/autoTranslate.js: -------------------------------------------------------------------------------- 1 | class AutoTranslate { 2 | static languageCodes = [ 3 | 'en', 4 | 'cy', 5 | 'zu', 6 | 'ko', 7 | 'az', 8 | 'he', 9 | 'mk', 10 | 'am', 11 | 'mr', 12 | 'cs', 13 | 'zh-cn', 14 | 'la', 15 | 'nn', 16 | 'my', 17 | 'ga', 18 | 'es', 19 | 'nl', 20 | 'zh-tw', 21 | 'pt-br', 22 | 'kn', 23 | 'uz', 24 | 'ja', 25 | 'is', 26 | 'sk', 27 | 'ht', 28 | 'bg', 29 | 'de', 30 | 'gd', 31 | 'et', 32 | 'fi', 33 | 'ar', 34 | 'hu', 35 | 'mt', 36 | 'ro', 37 | 'fa', 38 | 'hi', 39 | 'eo', 40 | 'lt', 41 | 'it', 42 | 'el', 43 | 'mi', 44 | 'hr', 45 | 'ca', 46 | 'th', 47 | 'hy', 48 | 'id', 49 | 'eu', 50 | 'da', 51 | 'ru', 52 | 'sr', 53 | 'gl', 54 | 'lv', 55 | 'nb', 56 | 'tr', 57 | 'fr', 58 | 'sv', 59 | 'sl', 60 | 'ml', 61 | 'be', 62 | 'pl', 63 | 'pt', 64 | 'ku', 65 | 'sq', 66 | 'ms', 67 | 'vi', 68 | 'te', 69 | 'uk', 70 | 'mn', 71 | 'es-419', 72 | 'ja-hira' 73 | ]; 74 | static getClosestLanguageCode(lc) { 75 | if (typeof lc !== 'string') return 'en'; 76 | lc = lc.toLowerCase(); 77 | if (AutoTranslate.languageCodes.includes(lc)) return lc; 78 | const split = lc.split('-'); 79 | if (AutoTranslate.languageCodes.includes(split[0])) return split[0]; 80 | return 'en'; 81 | } 82 | static async translate(text, lc) { 83 | const languageCode = AutoTranslate.getClosestLanguageCode(lc); 84 | const res = await fetch(`https://trampoline.turbowarp.org/translate/translate?language=${languageCode}&text=${encodeURIComponent(text)}`); 85 | const json = await res.json(); 86 | if (!res.ok) { 87 | throw new Error(json); 88 | } 89 | return json.result; 90 | } 91 | } 92 | 93 | export default AutoTranslate; -------------------------------------------------------------------------------- /src/resources/badges.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // dev 3 | owner: "owner", 4 | dev: "developer", 5 | contributor: "contributor", 6 | 7 | // admin 8 | admin: "admin", 9 | 10 | // "official" stuff 11 | verifiedofficial: "verifiedofficial", 12 | verified: "verified", 13 | partner: "partner", 14 | 15 | // event 16 | // penguinjam 17 | studiojam: "studiopenguinjamwinner", 18 | // penguinjamdiamond: "penguinjamdiamond", // no translation 19 | penguinjamplatinum: "penguinjamplatinum", 20 | penguinjamobsidian: "penguinjamobsidian", 21 | penguinjamgold: "penguinjamgold", 22 | penguinjamsilver: "penguinjamsilver", 23 | penguinjambronze: "penguinjambronze", 24 | // generic 25 | event: "eventwinner", 26 | participant: "participant", 27 | 28 | // status 29 | donator: "donator", 30 | translator: "translator", 31 | extensions: "extension_developer", 32 | bugreport: "vulnerability_report", 33 | betatesterbugreporter: "bugreportbeta", 34 | betatester: "betatester", 35 | birthday: "birthday", 36 | 37 | // auto 38 | modfrontpaged: "modfrontpaged", 39 | multifeature: "multifeature", 40 | studiofeatured: "featured_studio_creator", 41 | featured: "featured_creator", 42 | studio: "large_studio", 43 | votes: "votes_many", 44 | likes: "likes_many", 45 | followers: "followers_many", 46 | 47 | // auto but a bunch of stuff 48 | // studio50: "studio50", 49 | // likes100: "likes100", 50 | // votes100: "votes100", 51 | // followers100: "followers100", 52 | // studio100: "studio100", 53 | // likes250: "likes250", 54 | // votes250: "votes250", 55 | // followers250: "followers250", 56 | // studio250: "studio250", 57 | // likes500: "likes500", 58 | // votes500: "votes500", 59 | // followers500: "followers500", 60 | // studio500: "studio500", 61 | // views500: "views500", 62 | // likes1000: "likes1000", 63 | // votes1000: "votes1000", 64 | // followers1000: "followers1000", 65 | // studio1000: "studio1000", 66 | // views1000: "views1000", 67 | // likes5000: "likes5000", 68 | // votes5000: "votes5000", 69 | // followers5000: "followers5000", 70 | // studio5000: "studio5000", 71 | // views5000: "views5000", 72 | // likes10000: "likes10000", 73 | // votes10000: "votes10000", 74 | // followers10000: "followers10000", 75 | // studio10000: "studio10000", 76 | // views10000: "views10000", 77 | }; -------------------------------------------------------------------------------- /src/resources/basiccensorship.js: -------------------------------------------------------------------------------- 1 | const token = "❤"; 2 | 3 | function t(length) { 4 | const arr = []; 5 | for (let i = 0; i < length; i++) { 6 | arr.push(token); 7 | } 8 | return arr.join(''); 9 | } 10 | 11 | function censor(text) { 12 | return String(text) 13 | .replace(/(shitting)+/gim, t(8)) 14 | .replace(/(fucking|shut up)+/gim, t(7)) 15 | .replace(/(bitch|pussy)+/gim, t(5)) 16 | .replace(/(fuck|shit|dick|cock|fock)+/gim, t(4)) 17 | .replace(/(ass|sex|fok)+/gim, t(3)); 18 | } 19 | 20 | export default censor; -------------------------------------------------------------------------------- /src/resources/blobanddataurl.js: -------------------------------------------------------------------------------- 1 | import MagicNumbers from '../routes/panel/magicNumbers'; 2 | import FileTypes from '../routes/panel/filetypes'; 3 | 4 | class BlobAndDataUrl { 5 | static fileTypeFromDataArray = MagicNumbers.detectFileType; 6 | static fileTypeFromBlob = async (blob) => { 7 | const arrayBuffer = await BlobAndDataUrl.blobToArrayBuffer(blob); 8 | return BlobAndDataUrl.fileTypeFromDataArray(arrayBuffer); 9 | }; 10 | 11 | /** 12 | * Convert a Base64 Data URL (data:) to a Blob. 13 | * @param {string} url The data URL to convert. 14 | * @returns {Blob} The Blob that the data URL was converted to. 15 | */ 16 | static base64DataURLtoBlob(url) { 17 | var arr = url.split(','), bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n); 18 | while (n--) { 19 | u8arr[n] = bstr.charCodeAt(n); 20 | } 21 | const mime = BlobAndDataUrl.fileTypeFromDataArray(u8arr); 22 | return new Blob([u8arr], { type: mime }); 23 | } 24 | /** 25 | * Convert a Base64 Data URL (data:) to a Uint8Array. 26 | * @param {string} url The data URL to convert. 27 | * @returns {Uint8Array} The Uint8Array that the data URL was converted to. 28 | */ 29 | static base64DataURLtoUint8Array(url) { 30 | var arr = url.split(','), bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n); 31 | while (n--) { 32 | u8arr[n] = bstr.charCodeAt(n); 33 | } 34 | return u8arr; 35 | } 36 | 37 | static blobToDataURL(blob) { 38 | return new Promise((resolve, reject) => { 39 | const reader = new FileReader(); 40 | reader.onerror = reject; 41 | reader.onabort = reject; 42 | reader.onload = function (event) { 43 | resolve(event.target.result); 44 | }; 45 | reader.readAsDataURL(blob); 46 | }); 47 | } 48 | 49 | static arrayBufferToBlob(arrayBuffer, optMimeType) { 50 | let fileType; 51 | if (!optMimeType) { 52 | fileType = BlobAndDataUrl.fileTypeFromDataArray(arrayBuffer); 53 | optMimeType = FileTypes.mimeTypePairs[fileType]; 54 | } 55 | if (!optMimeType) { 56 | return new Blob([arrayBuffer]); 57 | } 58 | return new Blob([arrayBuffer], { type: optMimeType }); 59 | } 60 | static blobToArrayBuffer(blob) { 61 | return new Promise((resolve, reject) => { 62 | const reader = new FileReader(); 63 | reader.onerror = reject; 64 | reader.onabort = reject; 65 | reader.onload = function (event) { 66 | resolve(event.target.result); 67 | }; 68 | reader.readAsArrayBuffer(blob); 69 | }); 70 | } 71 | } 72 | 73 | export default BlobAndDataUrl; -------------------------------------------------------------------------------- /src/resources/emojis.js: -------------------------------------------------------------------------------- 1 | import { mockRequest } from "./emojis-compat"; 2 | 3 | const emojiHtmlUrl = 'https://gextapi.derpygamer2142.com/emojis'; 4 | const useLocalCopy = false; 5 | 6 | class EmojiHandler { 7 | static emojis = []; 8 | static failed = false; 9 | static loaded = false; 10 | static loading = false; 11 | static error = ''; 12 | static fetch () { 13 | return new Promise((resolve, reject) => { 14 | if (EmojiHandler.emojis.length > 0) { 15 | EmojiHandler.loading = false; 16 | EmojiHandler.failed = false; 17 | EmojiHandler.loaded = true; 18 | EmojiHandler.error = ''; 19 | return resolve(EmojiHandler.emojis); 20 | } 21 | EmojiHandler.failed = false; 22 | EmojiHandler.loaded = false; 23 | EmojiHandler.loading = true; 24 | EmojiHandler.error = ''; 25 | (async () => { 26 | let emojis; 27 | if (useLocalCopy) { 28 | emojis = mockRequest(); 29 | } else { 30 | const response = await fetch(emojiHtmlUrl); 31 | emojis = await response.json(); 32 | if (!response.ok) { 33 | throw { 34 | error: new Error('API responded NOT OK'), 35 | detail: emojis 36 | }; 37 | } 38 | } 39 | 40 | EmojiHandler.emojis = emojis; 41 | EmojiHandler.loading = false; 42 | EmojiHandler.failed = false; 43 | EmojiHandler.loaded = true; 44 | EmojiHandler.error = ''; 45 | resolve(emojis); 46 | })().catch(err => { 47 | EmojiHandler.failed = true; 48 | EmojiHandler.loading = false; 49 | EmojiHandler.loaded = false; 50 | EmojiHandler.error = err; 51 | reject(err); 52 | }); 53 | }); 54 | } 55 | } 56 | 57 | export default EmojiHandler; 58 | -------------------------------------------------------------------------------- /src/resources/html.js: -------------------------------------------------------------------------------- 1 | class HTMLUtility { 2 | static isDescendantOf(parent, child) { 3 | if (!parent) return false; 4 | if (!child) return false; 5 | return parent.contains(child); 6 | } 7 | static isRightClick(pointerEvent) { 8 | let isRightMB = false; 9 | 10 | if ("which" in pointerEvent) // Gecko (Firefox), WebKit (Safari/Chrome) & Opera 11 | isRightMB = pointerEvent.which == 3; 12 | else if ("button" in e) // IE, Opera 13 | isRightMB = pointerEvent.button == 2; 14 | 15 | return isRightMB; 16 | } 17 | } 18 | 19 | export default HTMLUtility; -------------------------------------------------------------------------------- /src/resources/icons/Search/icon.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | 15 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/resources/language.js: -------------------------------------------------------------------------------- 1 | import TranslationHandler from "./translations"; 2 | 3 | let lastAlertLang = ''; 4 | class Language { 5 | static _eventListeners = []; 6 | static onChange(cb) { 7 | Language._eventListeners.push({ callback: cb, type: "CHANGE" }); 8 | } 9 | static fireChange(lang) { 10 | Language._eventListeners.forEach(thinkle => { 11 | if (thinkle.type === "CHANGE") { 12 | thinkle.callback(lang); 13 | } 14 | }) 15 | } 16 | /** 17 | * Should be called in onMount. 18 | * Can be called more than once. 19 | */ 20 | static forceUpdate() { 21 | let lang = localStorage.getItem('pm:language'); 22 | // if no lang, default to browser lang 23 | if (!lang) { 24 | lang = navigator.language; 25 | lang = TranslationHandler.tryConvertingLocale(lang); 26 | } 27 | // if no browser lang, check top level lang 28 | if (!TranslationHandler.isLanguageAvailable(lang)) { 29 | lang = navigator.language.split('-')[0]; 30 | lang = TranslationHandler.tryConvertingLocale(lang); 31 | // if no top level lang go to en 32 | if (!TranslationHandler.isLanguageAvailable(lang)) { 33 | lang = 'en'; 34 | } 35 | } 36 | // scam alert messages 37 | if (lang !== lastAlertLang) { 38 | const warnings = [ 39 | TranslationHandler.text("console.warning1", lang), 40 | TranslationHandler.text("console.warning2", lang), 41 | TranslationHandler.text("console.warning3", lang), 42 | ]; 43 | console.log( 44 | `%c${warnings[0]} %c${warnings[1]}`, 45 | "color:red;font-family:system-ui;font-size:2rem;-webkit-text-stroke: 1px black;font-weight:bold", 46 | "color:black;font-family:system-ui;font-size:1.75rem;font-weight:bold" 47 | ); 48 | console.log(warnings[2]); 49 | lastAlertLang = lang; 50 | } 51 | // change page direction if needed 52 | document.documentElement.dir = "ltr"; 53 | if (TranslationHandler.rtlLanguages.includes(lang)) { 54 | document.documentElement.dir = "rtl"; 55 | } 56 | // fire change event 57 | Language.fireChange(lang); 58 | } 59 | } 60 | 61 | export default Language; -------------------------------------------------------------------------------- /src/resources/markdown/devposts/pages.js: -------------------------------------------------------------------------------- 1 | import DevPostShutdownIncident from "./shutdownincident.md?raw"; 2 | 3 | export default { 4 | "3-18-2025-shutdown-incident": DevPostShutdownIncident, 5 | }; -------------------------------------------------------------------------------- /src/resources/markdown/events/en/example.md: -------------------------------------------------------------------------------- 1 | # Example Event 2 | 3 | ```host 4 | my dog 5 | ``` 6 | ```collab 7 | a cool company 8 | ``` 9 | 10 | this is so 11 | epic 12 | 13 | this is not a real event, simply just made for testing -------------------------------------------------------------------------------- /src/resources/markdown/events/es-419/example.md: -------------------------------------------------------------------------------- 1 | # Evento de ejemplo 2 | 3 | ```host 4 | mi perro 5 | ``` 6 | 7 | no se como poner las accentos en Windows 10, y mi espanol no esta bueno 8 | 9 | esto evento es nomas para experimento, no es por los todos a ver -------------------------------------------------------------------------------- /src/resources/markdown/events/pages.js: -------------------------------------------------------------------------------- 1 | // ExampleEvent 2 | import ExampleEventEn from "./en/example.md?raw"; 3 | import ExampleEventEs419 from "./es-419/example.md?raw"; 4 | // PenguinJamWinter2024 5 | import PenguinJamWinter2024En from "./en/penguinjamwinter2024.md?raw"; 6 | import PenguinJamWinter2024Es from "./es/penguinjamwinter2024.md?raw"; 7 | import PenguinJamWinter2024Fr from "./fr/penguinjamwinter2024.md?raw"; 8 | import PenguinJamWinter2024Tr from "./tr/penguinjamwinter2024.md?raw"; 9 | import PenguinJamWinter2024Ru from "./ru/penguinjamwinter2024.md?raw"; 10 | import PenguinJamWinter2024Ja from "./ja/penguinjamwinter2024.md?raw"; 11 | // PenguinJamSpring2025 12 | import PenguinJamSpring2025En from "./en/penguinjamspring2025.md?raw"; 13 | import PenguinJamSpring2025Es from "./es/penguinjamspring2025.md?raw"; 14 | import PenguinJamSpring2025Ja from "./ja/penguinjamspring2025.md?raw"; 15 | import PenguinJamSpring2025Pl from "./pl/penguinjamspring2025.md?raw"; 16 | import PenguinJamSpring2025Tr from "./tr/penguinjamspring2025.md?raw"; 17 | import PenguinJamSpring2025Uk from "./uk/penguinjamspring2025.md?raw"; 18 | 19 | export default { 20 | "example": { 21 | "en": ExampleEventEn, 22 | "es-419": ExampleEventEs419, 23 | }, 24 | "penguinjamwinter2024": { 25 | "en": PenguinJamWinter2024En, 26 | "es": PenguinJamWinter2024Es, 27 | "fr": PenguinJamWinter2024Fr, 28 | "tr": PenguinJamWinter2024Tr, 29 | "ru": PenguinJamWinter2024Ru, 30 | "ja": PenguinJamWinter2024Ja, 31 | }, 32 | "penguinjamspring2025": { 33 | "en": PenguinJamSpring2025En, 34 | "es": PenguinJamSpring2025Es, 35 | "ja": PenguinJamSpring2025Ja, 36 | "pl": PenguinJamSpring2025Pl, 37 | "tr": PenguinJamSpring2025Tr, 38 | "uk": PenguinJamSpring2025Uk, 39 | }, 40 | }; -------------------------------------------------------------------------------- /src/resources/markdown/guidelines/pages.js: -------------------------------------------------------------------------------- 1 | import GuidelinesUploading from "./uploading.md?raw"; 2 | import GuidelinesModeration from "./moderation.md?raw"; 3 | 4 | export default { 5 | "uploading": GuidelinesUploading, 6 | "moderation": GuidelinesModeration, 7 | }; -------------------------------------------------------------------------------- /src/resources/translations.js: -------------------------------------------------------------------------------- 1 | // TRANSLATION DEFINITIONS HAVE MOVED TO src/translations/!locales.js 2 | // TRANSLATION DEFINITIONS HAVE MOVED TO src/translations/!locales.js 3 | // TRANSLATION DEFINITIONS HAVE MOVED TO src/translations/!locales.js 4 | // TRANSLATION DEFINITIONS HAVE MOVED TO src/translations/!locales.js 5 | import Locales from "../translations/!locales"; 6 | import { getLocaleFinishedPercentage } from "../translations/!all-locale-text"; 7 | 8 | const languages = Locales.languages; 9 | const rtlLanguages = Locales.rtlLanguages; 10 | const jokeLanguages = Locales.jokeLanguages; 11 | 12 | class TranslationHandler { 13 | static text(key, lang) { 14 | // return key; 15 | if (!languages[lang]) lang = 'en'; 16 | const language = languages[lang]; 17 | if (language[key]) return language[key]; 18 | return null; 19 | } 20 | static textSafe(key, lang, defaultText) { 21 | const langText = TranslationHandler.text(key, lang); 22 | if (langText) return langText; 23 | const englishText = TranslationHandler.text(key, "en"); 24 | if (englishText) return englishText; 25 | 26 | return defaultText; 27 | } 28 | 29 | static getLanguageFinishedPercentage(lang) { 30 | return getLocaleFinishedPercentage(lang, languages[lang]); 31 | } 32 | static isLanguageAvailable(lang) { 33 | return (lang in languages); 34 | } 35 | static isLanguageDone(lang) { 36 | return TranslationHandler.getLanguageFinishedPercentage(lang) >= 1; 37 | } 38 | static tryConvertingLocale(languageCode) { 39 | if (languageCode in Locales.autoLocale) { 40 | return Locales.autoLocale[languageCode]; 41 | } 42 | return languageCode; 43 | } 44 | 45 | static get languages() { 46 | return languages; 47 | } 48 | static get rtlLanguages() { 49 | return rtlLanguages; 50 | } 51 | static get jokeLanguages() { 52 | return jokeLanguages; 53 | } 54 | } 55 | 56 | 57 | export default TranslationHandler; 58 | -------------------------------------------------------------------------------- /src/resources/urls.js: -------------------------------------------------------------------------------- 1 | import ProjectApi from "./projectapi" 2 | 3 | import { PUBLIC_STUDIO_URL } from "$env/static/public"; 4 | 5 | export default { 6 | /** 7 | * PenguinMod's normal page 8 | */ 9 | base: `${PUBLIC_STUDIO_URL}/`, 10 | 11 | /** 12 | * PenguinMod's editor page 13 | */ 14 | editor: `${PUBLIC_STUDIO_URL}/editor.html`, 15 | 16 | /** 17 | * PenguinMod's credits page 18 | */ 19 | credits: `${PUBLIC_STUDIO_URL}/credits.html`, 20 | 21 | /** 22 | * PenguinMod's contact page 23 | */ 24 | contact: `${PUBLIC_STUDIO_URL}/contact.html`, 25 | 26 | /** 27 | * PenguinMod's terms of service page 28 | */ 29 | terms: "/terms", 30 | 31 | /** 32 | * PenguinMod's privacy policy page 33 | */ 34 | privacy: "/privacy", 35 | 36 | /** 37 | * PenguinMod's guideline pages for services 38 | */ 39 | guidelines: { 40 | /** 41 | * PenguinMod's project uploading guidelines 42 | */ 43 | projects: `${PUBLIC_STUDIO_URL}/PenguinMod-Guidelines/PROJECTS` 44 | }, 45 | 46 | /** 47 | * Donation pages for sites 48 | */ 49 | donate: { 50 | scratch: "https://www.scratchfoundation.org/donate", 51 | turbowarp: "https://github.com/sponsors/GarboMuffin" 52 | }, 53 | 54 | /** 55 | * PenguinMod's project page 56 | */ 57 | projects: `${ProjectApi.OriginApiUrl}/`, 58 | 59 | /** 60 | * PenguinMod's my stuff page 61 | */ 62 | mystuff: `${ProjectApi.OriginApiUrl}/mystuff`, 63 | 64 | /** 65 | * PenguinMod's home page 66 | */ 67 | home: "https://penguinmod.com/", 68 | 69 | /** 70 | * PenguinMod's packager page 71 | */ 72 | packager: `${PUBLIC_STUDIO_URL}/PenguinMod-Packager/`, 73 | 74 | /** 75 | * PenguinMod's unofficial wiki 76 | */ 77 | wiki: "https://wiki.penguinmod.com/wiki/Main_Page", 78 | 79 | /** 80 | * PenguinMod's Discord invite 81 | */ 82 | discord: "https://discord.gg/NZ9MBMYTZh", 83 | 84 | /** 85 | * Scratch's website 86 | */ 87 | scratch: "https://scratch.mit.edu", 88 | 89 | /** 90 | * TurboWarp's website 91 | */ 92 | turbowarp: "https://turbowarp.org", 93 | 94 | /** 95 | * PenguinMod's github page 96 | */ 97 | github: "https://github.com/PenguinMod/", 98 | 99 | /** 100 | * PenguinMod's basic API 101 | */ 102 | basicApi: "https://penguinmod-basic-api.derpygamer2142.com/", 103 | 104 | /** 105 | * The admin panel for pm projects api 106 | */ 107 | adminPanel: "https://penguinmod.com/panel" 108 | } 109 | -------------------------------------------------------------------------------- /src/resources/vr/htmlrenderer/element.js: -------------------------------------------------------------------------------- 1 | class VHTMLElement { 2 | constructor(element, parentObject, renderer) { 3 | this.parentObject = parentObject; 4 | this.element = element; 5 | 6 | this.renderer = renderer; 7 | } 8 | 9 | getAttributes() { 10 | const attributes = {}; 11 | const attribNames = this.element.getAttributeNames(); 12 | for (const name of attribNames) { 13 | attributes[name] = String(this.element.getAttribute(name)); 14 | } 15 | return attributes; 16 | } 17 | } 18 | 19 | export default VHTMLElement; -------------------------------------------------------------------------------- /src/resources/vr/htmlrenderer/elements/button.js: -------------------------------------------------------------------------------- 1 | import * as Three from "three"; 2 | import VHTMLElement from "../element"; 3 | import Direction from "../../util/direction"; 4 | 5 | class VHTMLButtonElement extends VHTMLElement { 6 | constructor(element, parentObject, renderer) { 7 | super(element, parentObject, renderer); 8 | } 9 | 10 | render() { 11 | const attributes = this.getAttributes(); 12 | const position = (attributes.position || '0 0 0').split(' '); 13 | const size = (attributes.size || '1 1 1').split(' '); 14 | 15 | const text = String(attributes.text) || 'Button'; 16 | // this code is really bad but its all canvas 2d's fault 17 | this.renderer.texCreator.ctx.font = '36px Arial'; 18 | this.renderer.texCreator.ctx.textAlign = 'center'; 19 | const textSize = this.renderer.texCreator.getTextureSizeText(text); 20 | const paddingSize = Number(attributes.padding); 21 | const padding2Size = paddingSize * 2; 22 | const imageSize = { 23 | width: textSize.width + padding2Size, 24 | height: textSize.height + padding2Size 25 | } 26 | this.renderer.texCreator.prepare(imageSize.width, imageSize.height); 27 | this.renderer.texCreator.ctx.fillStyle = 'white'; 28 | this.renderer.texCreator.ctx.strokeStyle = 'black'; 29 | this.renderer.texCreator.ctx.font = '36px Arial'; 30 | this.renderer.texCreator.ctx.textAlign = 'center'; 31 | this.renderer.texCreator.drawSliced('button', 0, 0, textSize.width + padding2Size, textSize.height + padding2Size); 32 | this.renderer.texCreator.createText(text, true, (textSize.width + padding2Size) / 2, paddingSize); 33 | 34 | const div = 64; 35 | const textTextureURL = this.renderer.texCreator.export(); 36 | const imageTexture = this.renderer.texLoader.load(textTextureURL); 37 | const imgGeometry = new Three.PlaneGeometry(imageSize.width / div, imageSize.height / div); 38 | const imgMaterial = new Three.MeshBasicMaterial({ 39 | map: imageTexture, 40 | transparent: true, 41 | side: Three.DoubleSide, 42 | }); 43 | 44 | const imgObject = new Three.Mesh(imgGeometry, imgMaterial); 45 | imgObject.position.set(...position); 46 | imgObject.scale.set(...size); 47 | imgObject.rotateX(Direction.toRad(0)); 48 | 49 | this.object = imgObject; 50 | this.parentObject.add(imgObject); 51 | } 52 | } 53 | 54 | export default VHTMLButtonElement; -------------------------------------------------------------------------------- /src/resources/vr/htmlrenderer/elements/cube.js: -------------------------------------------------------------------------------- 1 | import * as Three from "three"; 2 | import VHTMLElement from "../element"; 3 | 4 | class VHTMLCubeElement extends VHTMLElement { 5 | constructor(element, parentObject, renderer) { 6 | super(element, parentObject, renderer); 7 | } 8 | 9 | /** 10 | * @param {HTMLElement} element 11 | */ 12 | render() { 13 | const attributes = this.getAttributes(); 14 | const color = new Three.Color(attributes.color || 'black'); 15 | const position = (attributes.position || '0 0 0').split(' '); 16 | const size = (attributes.size || '0 0 0').split(' '); 17 | 18 | const cubeGeometry = new Three.BoxGeometry(...size); 19 | const cubeMaterial = new Three.MeshBasicMaterial({ 20 | color: color 21 | }); 22 | const cubeObject = new Three.Mesh(cubeGeometry, cubeMaterial); 23 | cubeObject.position.set(...position); 24 | 25 | this.object = cubeObject; 26 | this.parentObject.add(cubeObject); 27 | } 28 | } 29 | 30 | export default VHTMLCubeElement; -------------------------------------------------------------------------------- /src/resources/vr/htmlrenderer/elements/img.js: -------------------------------------------------------------------------------- 1 | import * as Three from "three"; 2 | import VHTMLElement from "../element"; 3 | import Direction from "../../util/direction"; 4 | 5 | class VHTMLImageElement extends VHTMLElement { 6 | constructor(element, parentObject, renderer) { 7 | super(element, parentObject, renderer); 8 | } 9 | 10 | render() { 11 | const attributes = this.getAttributes(); 12 | const position = (attributes.position || '0 0 0').split(' '); 13 | const size = (attributes.size || '1 1').split(' '); 14 | 15 | const imageTexture = this.renderer.texLoader.load(attributes.src); 16 | const imgGeometry = new Three.PlaneGeometry(...size); 17 | const imgMaterial = new Three.MeshBasicMaterial({ 18 | map: imageTexture, 19 | transparent: true, 20 | side: Three.DoubleSide, 21 | }); 22 | 23 | const imgObject = new Three.Mesh(imgGeometry, imgMaterial); 24 | imgObject.position.set(...position); 25 | imgObject.rotateX(Direction.toRad(0)); 26 | 27 | this.object = imgObject; 28 | this.parentObject.add(imgObject); 29 | } 30 | } 31 | 32 | export default VHTMLImageElement; -------------------------------------------------------------------------------- /src/resources/vr/htmlrenderer/elements/modifier.js: -------------------------------------------------------------------------------- 1 | import * as Three from "three"; 2 | import VHTMLElement from "../element"; 3 | import Direction from "../../util/direction"; 4 | 5 | class VHTMLModifierElement extends VHTMLElement { 6 | constructor(element, parentObject, renderer) { 7 | super(element, parentObject, renderer); 8 | } 9 | 10 | render() { 11 | const attributes = this.getAttributes(); 12 | this.attributes = attributes; 13 | this.current = { 14 | dx: 0, 15 | dy: 0, 16 | dz: 0, 17 | }; 18 | 19 | if (attributes.turn == 'true' && attributes.turnspeed) { 20 | this.current.dz = Number(attributes.turnspeed); 21 | } 22 | 23 | const modifierObject = new Three.Group(); 24 | modifierObject.rotateX(Direction.toRad(0)); 25 | 26 | this.object = modifierObject; 27 | this.parentObject.add(modifierObject); 28 | } 29 | animate() { 30 | if (!this.attributes) return; 31 | if (!this.current) return; 32 | if (!this.object) return; 33 | 34 | this.object.rotateX(Direction.toRad(this.current.dx)); 35 | this.object.rotateY(Direction.toRad(this.current.dy)); 36 | this.object.rotateZ(Direction.toRad(this.current.dz)); 37 | } 38 | } 39 | 40 | export default VHTMLModifierElement; -------------------------------------------------------------------------------- /src/resources/vr/htmlrenderer/elements/p.js: -------------------------------------------------------------------------------- 1 | import * as Three from "three"; 2 | import VHTMLElement from "../element"; 3 | import Direction from "../../util/direction"; 4 | 5 | class VHTMLPElement extends VHTMLElement { 6 | constructor(element, parentObject, renderer) { 7 | super(element, parentObject, renderer); 8 | } 9 | 10 | render() { 11 | const attributes = this.getAttributes(); 12 | const position = (attributes.position || '0 0 0').split(' '); 13 | 14 | const textGroup = new Three.Group(); 15 | textGroup.position.set(...position); 16 | textGroup.rotateX(Direction.toRad(0)); 17 | 18 | this.object = textGroup; 19 | this.parentObject.add(textGroup); 20 | } 21 | } 22 | 23 | export default VHTMLPElement; -------------------------------------------------------------------------------- /src/resources/vr/htmlrenderer/elements/parsererror.js: -------------------------------------------------------------------------------- 1 | import * as Three from "three"; 2 | import VHTMLElement from "../element"; 3 | import Direction from "../../util/direction"; 4 | 5 | class VHTMLParserErrorElement extends VHTMLElement { 6 | constructor(element, parentObject, renderer) { 7 | super(element, parentObject, renderer); 8 | } 9 | 10 | render() { 11 | const imageTexture = this.renderer.texLoader.load("https://penguinmod.com/vr/warning.png"); 12 | const imgGeometry = new Three.PlaneGeometry(1.5, 1.5); 13 | const imgMaterial = new Three.MeshBasicMaterial({ 14 | map: imageTexture, 15 | transparent: true, 16 | side: Three.DoubleSide, 17 | }); 18 | 19 | const imgObject = new Three.Mesh(imgGeometry, imgMaterial); 20 | imgObject.position.set(0, 0, 0.5); 21 | imgObject.rotateX(Direction.toRad(0)); 22 | 23 | this.object = imgObject; 24 | this.parentObject.add(imgObject); 25 | } 26 | } 27 | 28 | export default VHTMLParserErrorElement; -------------------------------------------------------------------------------- /src/resources/vr/htmlrenderer/tagpairs.js: -------------------------------------------------------------------------------- 1 | import VHTMLParserErrorElement from './elements/parsererror'; 2 | import VHTMLCubeElement from "./elements/cube"; 3 | import VHTMLImageElement from "./elements/img"; 4 | import VHTMLModifierElement from './elements/modifier'; 5 | import VHTMLPElement from './elements/p'; 6 | import VHTMLButtonElement from './elements/button'; 7 | 8 | export default { 9 | parsererror: VHTMLParserErrorElement, 10 | cube: VHTMLCubeElement, 11 | img: VHTMLImageElement, 12 | p: VHTMLPElement, 13 | modifier: VHTMLModifierElement, 14 | button: VHTMLButtonElement, 15 | }; -------------------------------------------------------------------------------- /src/resources/vr/htmlrenderer/texture/font.js: -------------------------------------------------------------------------------- 1 | class FontUtil { 2 | static getFontSize(font = '') { 3 | const fontProp = font; 4 | // tear out font names specified within quotes 5 | const noUnsafeFontNamesProp = fontProp.replace(/(['"]).*?\1/gm, ''); 6 | // add spaces around everything so we can only read font size surrounded by spaces 7 | const readableFontProp = ` ${noUnsafeFontNamesProp.trim()} `; 8 | const fontSizeMatches = readableFontProp.match(/ \d+px /gm); 9 | if (!fontSizeMatches) { 10 | return 0; 11 | } 12 | const sanitized = fontSizeMatches[0] 13 | .replace('px', '') 14 | .trim(); 15 | return Number(sanitized); 16 | } 17 | } 18 | 19 | export default FontUtil; -------------------------------------------------------------------------------- /src/resources/vr/htmlrenderer/texture/index.js: -------------------------------------------------------------------------------- 1 | import FontUtil from "./font"; 2 | import TextUtil from "./text"; 3 | import NineSlices from "./nineslice"; 4 | 5 | class TextureCreator { 6 | constructor() { 7 | this.canvas = document.createElement('canvas'); 8 | this.canvas.style = 'display: none;'; 9 | this.canvas.width = 640; 10 | this.canvas.height = 360; 11 | document.body.appendChild(this.canvas); 12 | 13 | this.ctx = this.canvas.getContext('2d'); 14 | TextUtil.ctx = this.ctx; 15 | NineSlices.ctx = this.ctx; 16 | } 17 | 18 | prepare(width, height) { 19 | this.canvas.width = width; 20 | this.canvas.height = height; 21 | this.ctx.clearRect(0, 0, width, height); 22 | } 23 | export() { 24 | const url = this.canvas.toDataURL('image/png'); 25 | return url; 26 | } 27 | 28 | getTextureSizeText(text) { 29 | const measurement = this.ctx.measureText(text); 30 | const imageHeight = FontUtil.getFontSize(this.ctx.font) * text.split('\n').length; 31 | return { 32 | width: measurement.width, 33 | height: imageHeight 34 | } 35 | } 36 | drawSliced(sliceName, x, y, width, height) { 37 | const slice = NineSlices.images[sliceName]; 38 | NineSlices.drawTexture(slice.image, slice.corner, x, y, width, height); 39 | } 40 | createText(text, stroke, x, y) { 41 | this.ctx.textBaseline = 'top'; 42 | if (stroke) { 43 | // i love canvas 2d strokeText!!!!!! 44 | for (let i = 0; i < 5; i++) { 45 | TextUtil.strokeTextNL(text, x, y); 46 | } 47 | } 48 | TextUtil.fillTextNL(text, x, y); 49 | } 50 | } 51 | 52 | export default TextureCreator; -------------------------------------------------------------------------------- /src/resources/vr/htmlrenderer/texture/nineslice.js: -------------------------------------------------------------------------------- 1 | const loadImage = (url) => { 2 | return new Promise((resolve, reject) => { 3 | const image = new Image(); 4 | image.onload = () => resolve(image); 5 | image.onerror = reject; 6 | image.src = url; 7 | }); 8 | }; 9 | 10 | class NineSlices { 11 | static images = { 12 | button: { 13 | url: './vr/nineslices/button.png', 14 | corner: 24 15 | } 16 | }; 17 | static async loadImages() { 18 | for (const sliceName in NineSlices.images) { 19 | const slice = NineSlices.images[sliceName]; 20 | const url = slice.url; 21 | const image = await loadImage(url); 22 | slice.image = image; 23 | } 24 | }; 25 | 26 | /** 27 | * @type {CanvasRenderingContext2D} 28 | */ 29 | static ctx = null; 30 | 31 | static drawTexture(image, cornerSize, x, y, width, height) { 32 | // center 33 | this.ctx.drawImage(image, cornerSize, cornerSize, image.width - (cornerSize * 2), image.height - (cornerSize * 2), x + cornerSize, y + cornerSize, width - (cornerSize * 2), height - (cornerSize * 2)); 34 | // corners 35 | // oh nah 36 | this.ctx.drawImage(image, 0, 0, cornerSize, cornerSize, x, y, cornerSize, cornerSize); // TL 37 | this.ctx.drawImage(image, image.width - cornerSize, 0, cornerSize, cornerSize, x + (width - cornerSize), y, cornerSize, cornerSize); // TR 38 | this.ctx.drawImage(image, 0, image.height - cornerSize, cornerSize, cornerSize, x, y + (height - cornerSize), cornerSize, cornerSize); // BL 39 | this.ctx.drawImage(image, image.width - cornerSize, image.height - cornerSize, cornerSize, cornerSize, x + (width - cornerSize), y + (height - cornerSize), cornerSize, cornerSize); // BR 40 | // edges 41 | // i am going insane 42 | this.ctx.drawImage(image, 0, cornerSize, cornerSize, image.height - (cornerSize * 2), x, y + cornerSize, cornerSize, height - (cornerSize * 2)); // L 43 | this.ctx.drawImage(image, image.width - cornerSize, cornerSize, cornerSize, image.height - (cornerSize * 2), x + (width - cornerSize), y + cornerSize, cornerSize, height - (cornerSize * 2)); // R 44 | this.ctx.drawImage(image, cornerSize, 0, image.width - (cornerSize * 2), cornerSize, x + cornerSize, y, width - (cornerSize * 2), cornerSize); // T 45 | this.ctx.drawImage(image, cornerSize, image.height - cornerSize, image.width - (cornerSize * 2), cornerSize, x + cornerSize, y + (height - cornerSize), width - (cornerSize * 2), cornerSize); // B 46 | } 47 | } 48 | 49 | export default NineSlices; -------------------------------------------------------------------------------- /src/resources/vr/htmlrenderer/texture/text.js: -------------------------------------------------------------------------------- 1 | import FontUtil from "./font"; 2 | 3 | class TextUtil { 4 | static ctx = null; 5 | 6 | static _drawTextNLType(type, text, x, y, ...args) { 7 | const split = text.split('\n'); 8 | const fontSize = FontUtil.getFontSize(this.ctx.font); 9 | let initialY = y; 10 | for (const line of split) { 11 | this.ctx[type](line, x, initialY, ...args); 12 | initialY += fontSize; 13 | } 14 | } 15 | static fillTextNL(text, x, y, ...args) { 16 | this._drawTextNLType('fillText', text, x, y, ...args); 17 | } 18 | static strokeTextNL(text, x, y, ...args) { 19 | this._drawTextNLType('strokeText', text, x, y, ...args); 20 | } 21 | } 22 | 23 | export default TextUtil; -------------------------------------------------------------------------------- /src/resources/vr/menus/home.xml: -------------------------------------------------------------------------------- 1 | 2 |

{{$generic.temporary}}

3 |
-------------------------------------------------------------------------------- /src/resources/vr/menus/index.js: -------------------------------------------------------------------------------- 1 | import pageLoading from './loading.xml?raw'; 2 | import pageLogin from './login.xml?raw'; 3 | import pageHome from './home.xml?raw'; 4 | 5 | export default { 6 | pageLoading, 7 | pageLogin, 8 | pageHome, 9 | }; -------------------------------------------------------------------------------- /src/resources/vr/menus/loading.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/resources/vr/menus/login.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

You are not logged in. Please exit and follow instructions to login.

4 |