├── .dockerignore ├── .editorconfig ├── .github └── workflows │ └── build-and-push.yml ├── .gitignore ├── .prettierrc ├── Dockerfile ├── README.md ├── bun.lockb ├── frontend ├── images │ ├── bg-trans.png │ ├── color_profile-1.png │ └── defragment-1.png ├── index.css ├── index.html └── index.js ├── package.json └── src ├── index.js ├── source.svg └── svg.js /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/.next 12 | **/.cache 13 | **/*.*proj.user 14 | **/*.dbmdl 15 | **/*.jfm 16 | **/charts 17 | **/docker-compose* 18 | **/compose.y*ml 19 | **/Dockerfile* 20 | **/node_modules 21 | **/npm-debug.log 22 | **/obj 23 | **/secrets.dev.yaml 24 | **/values.dev.yaml 25 | **/build 26 | **/dist 27 | LICENSE 28 | README.md -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.yml] 12 | indent_style = space 13 | -------------------------------------------------------------------------------- /.github/workflows/build-and-push.yml: -------------------------------------------------------------------------------- 1 | name: Build and Push 2 | 3 | permissions: 4 | contents: read 5 | packages: write 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - 17 | name: Set up Docker Buildx 18 | uses: docker/setup-buildx-action@v1 19 | - 20 | name: Login to GHCR 21 | uses: docker/login-action@v1 22 | with: 23 | registry: ghcr.io 24 | username: ${{ github.repository_owner }} 25 | password: ${{ github.token }} 26 | - 27 | name: Build and push 28 | id: docker_build 29 | uses: docker/build-push-action@v2 30 | with: 31 | push: true 32 | tags: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}:latest 33 | - 34 | name: Image digest 35 | run: echo ${{ steps.docker_build.outputs.digest }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | 3 | logs 4 | _.log 5 | npm-debug.log_ 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | .pnpm-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | 13 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 14 | 15 | # Runtime data 16 | 17 | pids 18 | _.pid 19 | _.seed 20 | \*.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | 28 | coverage 29 | \*.lcov 30 | 31 | # nyc test coverage 32 | 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 | 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | 41 | bower_components 42 | 43 | # node-waf configuration 44 | 45 | .lock-wscript 46 | 47 | # Compiled binary addons (https://nodejs.org/api/addons.html) 48 | 49 | build/Release 50 | 51 | # Dependency directories 52 | 53 | node_modules/ 54 | jspm_packages/ 55 | 56 | # Snowpack dependency directory (https://snowpack.dev/) 57 | 58 | web_modules/ 59 | 60 | # TypeScript cache 61 | 62 | \*.tsbuildinfo 63 | 64 | # Optional npm cache directory 65 | 66 | .npm 67 | 68 | # Optional eslint cache 69 | 70 | .eslintcache 71 | 72 | # Optional stylelint cache 73 | 74 | .stylelintcache 75 | 76 | # Microbundle cache 77 | 78 | .rpt2_cache/ 79 | .rts2_cache_cjs/ 80 | .rts2_cache_es/ 81 | .rts2_cache_umd/ 82 | 83 | # Optional REPL history 84 | 85 | .node_repl_history 86 | 87 | # Output of 'npm pack' 88 | 89 | \*.tgz 90 | 91 | # Yarn Integrity file 92 | 93 | .yarn-integrity 94 | 95 | # dotenv environment variable files 96 | 97 | .env 98 | .env.development.local 99 | .env.test.local 100 | .env.production.local 101 | .env.local 102 | 103 | # parcel-bundler cache (https://parceljs.org/) 104 | 105 | .cache 106 | .parcel-cache 107 | 108 | # Next.js build output 109 | 110 | .next 111 | out 112 | 113 | # Nuxt.js build / generate output 114 | 115 | .nuxt 116 | dist 117 | 118 | # Gatsby files 119 | 120 | .cache/ 121 | 122 | # Comment in the public line in if your project uses Gatsby and not Next.js 123 | 124 | # https://nextjs.org/blog/next-9-1#public-directory-support 125 | 126 | # public 127 | 128 | # vuepress build output 129 | 130 | .vuepress/dist 131 | 132 | # vuepress v2.x temp and cache directory 133 | 134 | .temp 135 | .cache 136 | 137 | # Docusaurus cache and generated files 138 | 139 | .docusaurus 140 | 141 | # Serverless directories 142 | 143 | .serverless/ 144 | 145 | # FuseBox cache 146 | 147 | .fusebox/ 148 | 149 | # DynamoDB Local files 150 | 151 | .dynamodb/ 152 | 153 | # TernJS port file 154 | 155 | .tern-port 156 | 157 | # Stores VSCode versions used for testing VSCode extensions 158 | 159 | .vscode-test 160 | 161 | # yarn v2 162 | 163 | .yarn/cache 164 | .yarn/unplugged 165 | .yarn/build-state.yml 166 | .yarn/install-state.gz 167 | .pnp.\* 168 | 169 | # wrangler project 170 | 171 | .dev.vars 172 | .wrangler/ 173 | 174 | # some aras proudly use macOS 175 | .DS_Store -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 140, 3 | "singleQuote": true, 4 | "semi": true, 5 | "useTabs": true 6 | } 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM oven/bun:1-alpine 2 | 3 | RUN mkdir -p /home/bun/app/node_modules && chown -R bun:bun /home/bun/app 4 | 5 | WORKDIR /home/bun/app 6 | 7 | COPY --chown=bun:bun package.json bun.lockb ./ 8 | 9 | USER bun 10 | 11 | RUN bun install --frozen-lockfile --production 12 | 13 | COPY --chown=bun:bun . . 14 | 15 | EXPOSE 3000 16 | 17 | CMD [ "bun", "run", "src/index.js" ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # badge-gen 2 | 3 | ### an 88x31 badge pride flag mashup generator, for all your queer web 1.0 needs! 4 | 5 | https://badge.les.bi -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oake/badge-gen/8039ec8155db5396fbbd3504874be48e201cf7d9/bun.lockb -------------------------------------------------------------------------------- /frontend/images/bg-trans.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oake/badge-gen/8039ec8155db5396fbbd3504874be48e201cf7d9/frontend/images/bg-trans.png -------------------------------------------------------------------------------- /frontend/images/color_profile-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oake/badge-gen/8039ec8155db5396fbbd3504874be48e201cf7d9/frontend/images/color_profile-1.png -------------------------------------------------------------------------------- /frontend/images/defragment-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oake/badge-gen/8039ec8155db5396fbbd3504874be48e201cf7d9/frontend/images/defragment-1.png -------------------------------------------------------------------------------- /frontend/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --text-color: #222222; 3 | --surface: #c0c0c0; 4 | --button-highlight: #ffffff; 5 | --button-face: #dfdfdf; 6 | --button-shadow: #808080; 7 | --window-frame: #0a0a0a; 8 | --dialog-blue: #000080; 9 | --dialog-blue-light: #1084d0; 10 | --dialog-gray: #808080; 11 | --dialog-gray-light: #b5b5b5; 12 | --link-blue: #0000ff; 13 | --border-field: inset -1px -1px var(--button-highlight), 14 | inset 1px 1px var(--button-shadow), inset -2px -2px var(--button-face), 15 | inset 2px 2px var(--window-frame); 16 | } 17 | 18 | ::-webkit-scrollbar-button:vertical:start, 19 | ::-webkit-scrollbar-button:vertical:end { 20 | display: none; 21 | } 22 | 23 | body { 24 | user-select: none; 25 | background-color: #008080; 26 | 27 | height: 100vh; 28 | margin: 0; 29 | display: flex; 30 | justify-content: center; 31 | align-items: center; 32 | } 33 | 34 | .hidden { 35 | display: none !important; 36 | } 37 | 38 | .main { 39 | width: fit-content; 40 | } 41 | 42 | .window-body { 43 | gap: 10px; 44 | } 45 | 46 | .windowarea { 47 | display: grid; 48 | height: 120px; 49 | width: fit-content; 50 | align-items: center; 51 | 52 | padding: 12px 8px; 53 | margin: 0; 54 | margin-top: 6px; 55 | gap: 10px; 56 | } 57 | 58 | .settings-fieldset { 59 | display: grid; 60 | align-items: center; 61 | gap: 5px; 62 | width: 100px; 63 | } 64 | 65 | .download-fieldset { 66 | width: 100%; 67 | } 68 | 69 | .download-buttons { 70 | display: inline-flex; 71 | margin-left: 10px; 72 | gap: 5px; 73 | align-items: center; 74 | } 75 | 76 | .download-radio { 77 | display: inline-flex; 78 | justify-content: space-between; 79 | width: 100%; 80 | margin-bottom: 10px; 81 | } 82 | 83 | .space-windowbody { 84 | display: flex; 85 | place-content: space-between; 86 | } 87 | 88 | button.active { 89 | box-shadow: inset -1px -1px #ffffff, inset 1px 1px #0a0a0a, 90 | inset -2px -2px #dfdfdf, inset 2px 2px #808080; 91 | text-shadow: 1px 1px #222; 92 | } 93 | 94 | .options-pane { 95 | overflow-y: scroll; 96 | background-image: url("images/bg-trans.png"); 97 | background-color: lightcyan; 98 | } 99 | 100 | #dummy-flag { 101 | content: ''; 102 | width: 88px; 103 | flex-shrink: 0; 104 | } 105 | 106 | .flag { 107 | image-rendering: pixelated; 108 | width: 88px; 109 | height: 31px; 110 | } 111 | 112 | #final-badge-pane { 113 | background-image: url("images/bg-trans.png"); 114 | background-color: khaki; 115 | } 116 | 117 | textarea { 118 | width: 100%; 119 | overflow: hidden; 120 | height: fit-content; 121 | resize: none; 122 | } 123 | 124 | .window-field-stack { 125 | display: flex; 126 | flex-direction: column; 127 | } 128 | 129 | .border { 130 | box-shadow: var(--border-field); 131 | } 132 | 133 | .focused { 134 | outline: 1px dotted #000000; 135 | outline-offset: 2px; 136 | } -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 88x31 Badge Generator 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |
18 | 88x31 Pride Badge Generator 19 |
20 |
21 | 22 | 23 | 24 |
25 |
26 | 27 |
28 |
29 | Select a setting 30 | 31 | 32 | 33 | 34 | 35 |
36 | 37 |
38 | 39 |
40 | 46 | 47 | 49 | 50 |
51 |
52 | 53 |
54 | 55 | 58 | 59 | 62 |
63 |
64 | 65 |
66 | 67 |
68 | 69 |
70 |
71 |
72 | 73 |
74 |
75 | Export 76 |
77 |
78 |
79 | 80 | 81 |
82 |
83 | 84 | 85 |
86 |
87 |
88 | 89 | 90 | 91 |
92 |
93 |
94 | 95 |
96 | 97 |
98 |
99 | 100 |
101 |

by anya & maeve with love

102 |

les.bi

103 |

GitHub

104 |
105 |
106 | 107 | 108 | -------------------------------------------------------------------------------- /frontend/index.js: -------------------------------------------------------------------------------- 1 | var options; 2 | 3 | var currentChoice; 4 | var currentPane = 'flag1'; 5 | 6 | var flagPaneScroll = {}; 7 | 8 | var noFlagEl; 9 | var optionsEl; 10 | var flagOptionsEl; 11 | var clipOptionsEl; 12 | var overlayOptionsEl; 13 | var finalBadgeEl; 14 | var settingsButtons; 15 | var miaows = 0; 16 | 17 | var exportType = 'png'; 18 | 19 | const baseUrl = 'https://badge.les.bi'; 20 | 21 | async function init() { 22 | noFlagEl = document.getElementById('no-flag'); 23 | optionsEl = document.getElementById('options'); 24 | flagOptionsEl = document.getElementById('flag-options'); 25 | clipOptionsEl = document.getElementById('clip-options'); 26 | overlayOptionsEl = document.getElementById('overlay-options'); 27 | finalBadgeEl = document.getElementById('final-badge'); 28 | settingsButtons = document.querySelectorAll('#settings .setting'); 29 | 30 | let res = await fetch('/options.json'); 31 | options = await res.json(); 32 | 33 | document.getElementById('dummy-flag').remove(); 34 | options.flags.forEach((flag) => { 35 | let img = document.createElement('img'); 36 | img.setAttribute('class', 'flag'); 37 | img.setAttribute('id', flag); 38 | img.setAttribute('title', flag); 39 | img.src = `/88x31/${flag}.png`; 40 | flagOptionsEl.append(img); 41 | }); 42 | 43 | currentChoice = { 44 | flag2: options.flags[1], 45 | clip: options.clips[0], 46 | overlay: options.overlays[0], 47 | }; 48 | 49 | await selectOption(options.flags[0]); 50 | await refreshView(); 51 | 52 | optionsEl.addEventListener('click', async function (event) { 53 | if (event.target && event.target.tagName === 'IMG') { 54 | await selectOption(event.target.id); 55 | } 56 | }); 57 | 58 | document.getElementById('settings').addEventListener('click', async function (event) { 59 | if (event.target && event.target.classList.contains('setting')) { 60 | await switchPane(event.target.id); 61 | } 62 | }); 63 | 64 | document.getElementById('svg').addEventListener('click', async function (event) { 65 | exportType = 'svg'; 66 | await refreshExport(); 67 | }); 68 | 69 | document.getElementById('png').addEventListener('click', async function (event) { 70 | exportType = 'png'; 71 | await refreshExport(); 72 | }); 73 | } 74 | 75 | function paneToElement(pane) { 76 | switch (pane) { 77 | case 'flag1': 78 | return flagOptionsEl; 79 | case 'flag2': 80 | return flagOptionsEl; 81 | case 'clip': 82 | return clipOptionsEl; 83 | case 'overlay': 84 | return overlayOptionsEl; 85 | } 86 | } 87 | 88 | function choiceToUrl(choice, ext) { 89 | let url = `/88x31/${choice.flag1}`; 90 | if (choice.flag2 && choice.flag2 !== 'no-flag') { 91 | url += `/${choice.flag2}`; 92 | if (choice.clip) { 93 | url += `/${choice.clip}`; 94 | } 95 | } 96 | if (choice.overlay && choice.overlay !== 'no-overlay') { 97 | url += `/${choice.overlay}`; 98 | } 99 | url += `.${ext}`; 100 | return url; 101 | } 102 | 103 | async function switchPane(pane) { 104 | if (pane === currentPane) { 105 | return; 106 | } 107 | 108 | if (currentPane.slice(0, -1) === 'flag') { 109 | flagPaneScroll[currentPane] = flagOptionsEl.scrollTop; 110 | let focusedFlag = currentChoice[currentPane]; 111 | if (focusedFlag) { 112 | document.getElementById(focusedFlag).classList.remove('focused'); 113 | } 114 | } 115 | if (pane.slice(0, -1) === 'flag') { 116 | flagOptionsEl.scrollTop = flagPaneScroll[pane] ?? 0; 117 | let focusedFlag = currentChoice[pane]; 118 | if (focusedFlag) { 119 | document.getElementById(focusedFlag).classList.add('focused'); 120 | } 121 | } 122 | if (pane === 'flag2') { 123 | noFlagEl.classList.remove('hidden'); 124 | } else { 125 | noFlagEl.classList.add('hidden'); 126 | } 127 | 128 | settingsButtons.forEach((button) => { 129 | if (button.id === pane) { 130 | button.classList.add('active'); 131 | } 132 | if (button.id === currentPane) { 133 | button.classList.remove('active'); 134 | } 135 | }); 136 | 137 | paneToElement(currentPane).classList.add('hidden'); 138 | paneToElement(pane).classList.remove('hidden'); 139 | 140 | currentPane = pane; 141 | } 142 | 143 | async function selectOption(option) { 144 | let currentOption = currentChoice[currentPane]; 145 | if (currentOption === option) { 146 | return; 147 | } 148 | if (currentOption) { 149 | document.getElementById(currentOption).classList.remove('focused'); 150 | } 151 | currentChoice[currentPane] = option; 152 | document.getElementById(option).classList.add('focused'); 153 | if (currentPane === 'flag2') { 154 | document.querySelector('#settings #clip').disabled = option === 'no-flag'; 155 | } 156 | await refreshView(); 157 | } 158 | 159 | function miaow() { 160 | if (miaows == 69) { 161 | var audio = new Audio('https://img.birb.cc/j7sgRaw5.m4a'); 162 | audio.play(); 163 | miaows++; 164 | } else { 165 | miaows++; 166 | } 167 | } 168 | 169 | async function refreshView() { 170 | if (currentPane !== 'clip') { 171 | clipOptionsEl.innerHTML = ''; 172 | options.clips.forEach((clip) => { 173 | let choice = { ...currentChoice }; 174 | choice.clip = clip; 175 | let img = document.createElement('img'); 176 | img.setAttribute('class', 'flag'); 177 | img.setAttribute('id', clip); 178 | if (clip === currentChoice.clip) { 179 | img.classList.add('focused'); 180 | } 181 | img.src = choiceToUrl(choice, 'png'); 182 | clipOptionsEl.append(img); 183 | }); 184 | } 185 | if (currentPane !== 'overlay') { 186 | overlayOptionsEl.innerHTML = ''; 187 | let overlays = options.overlays.slice(); 188 | overlays.unshift('no-overlay'); 189 | overlays.forEach((overlay) => { 190 | let choice = { ...currentChoice }; 191 | choice.overlay = overlay; 192 | let img = document.createElement('img'); 193 | img.setAttribute('class', 'flag'); 194 | img.setAttribute('id', overlay); 195 | if (overlay === currentChoice.overlay) { 196 | img.classList.add('focused'); 197 | } 198 | img.src = choiceToUrl(choice, 'png'); 199 | overlayOptionsEl.append(img); 200 | }); 201 | } 202 | finalBadgeEl.src = choiceToUrl(currentChoice, 'png'); 203 | await refreshExport(); 204 | } 205 | 206 | async function refreshExport() { 207 | let url = choiceToUrl(currentChoice, exportType); 208 | let title = currentChoice.flag1; 209 | if (currentChoice.flag2 !== 'no-flag') { 210 | title += ` ${currentChoice.flag2}`; 211 | } 212 | document.getElementById('html-textarea').value = 213 | ``; 214 | document 215 | .getElementById('copyHTML') 216 | .setAttribute( 217 | 'onclick', 218 | 'copyToClipboard(`' + 219 | `` + 220 | '`)', 221 | ); 222 | document.getElementById('copyURL').setAttribute('onclick', 'copyToClipboard(`' + baseUrl + url + '`)'); 223 | document.getElementById('download').setAttribute('onclick', 'window.open(`' + url + '`)'); 224 | } 225 | 226 | function copyToClipboard(text) { 227 | navigator.clipboard.writeText(text); 228 | } 229 | 230 | document.addEventListener('DOMContentLoaded', init); 231 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "badge-gen", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "nodemon src/index.js" 8 | }, 9 | "dependencies": { 10 | "@xmldom/xmldom": "^0.8.10", 11 | "hono": "^4.5.1", 12 | "svg2png-wasm": "^1.4.1" 13 | }, 14 | "devDependencies": { 15 | "nodemon": "^3.1.4", 16 | "prettier": "3.3.3" 17 | } 18 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | import { initialize, svg2png } from 'svg2png-wasm'; 4 | import { serveStatic } from 'hono/bun'; 5 | await initialize(fs.readFileSync('./node_modules/svg2png-wasm/svg2png_wasm_bg.wasm')); 6 | 7 | import { parseSourceSvg, buildBadgeSvg } from './svg.js'; 8 | 9 | let sourceSvg = fs.readFileSync('src/source.svg', 'utf8'); 10 | const { template, flags, clips, overlays } = parseSourceSvg(sourceSvg); 11 | 12 | import { Hono } from 'hono'; 13 | import { cors } from 'hono/cors'; 14 | const app = new Hono(); 15 | 16 | const w88h31 = new Hono(); 17 | 18 | w88h31.get('/:flag1/:flag2/:clip/:overlay{.+\\.(png|svg)$}', renderBadge); 19 | w88h31.get('/:flag1/:flag2/:clip{.+\\.(png|svg)$}', renderBadge); 20 | w88h31.get('/:flag1/:overlay{.+\\.(png|svg)$}', renderBadge); 21 | w88h31.get('/:flag1{.+\\.(png|svg)$}', renderBadge); 22 | 23 | app.route('/88x31', w88h31); 24 | 25 | function processParams(params) { 26 | for (const [key, value] of Object.entries(params)) { 27 | if (value.endsWith('.svg')) { 28 | params[key] = value.replace(/\.svg$/, ''); 29 | params.type = 'svg'; 30 | } else if (value.endsWith('.png')) { 31 | params[key] = value.replace(/\.png$/, ''); 32 | params.type = 'png'; 33 | } 34 | } 35 | return params; 36 | } 37 | 38 | async function renderBadge(c) { 39 | const params = processParams(c.req.param()); 40 | 41 | const flag1 = flags[params.flag1]; 42 | const flag2 = flags[params.flag2]; 43 | const clip = clips[params.clip]; 44 | const overlay = overlays[params.overlay]; 45 | 46 | if (params.flag2 && (!flag1 || !flag2 || !clip)) { 47 | return c.text('invalid flags or clip', 400); 48 | } 49 | 50 | if (params.overlay && !overlay) { 51 | return c.text('invalid overlay', 400); 52 | } 53 | 54 | const svg = buildBadgeSvg(template, flag1, flag2, clip, overlay); 55 | if (params.type === 'svg') { 56 | return c.body(svg, 200, { 'content-type': 'image/svg+xml' }); 57 | } 58 | const buf = await svg2png(svg, {}); 59 | return c.body(buf, 200, { 'content-type': 'image/png' }); 60 | } 61 | 62 | app.use('/options.json', cors()); 63 | 64 | app.get('/options.json', (c) => { 65 | return c.json({ 66 | flags: Object.keys(flags), 67 | clips: Object.keys(clips), 68 | overlays: Object.keys(overlays), 69 | }); 70 | }); 71 | 72 | app.use('/*', serveStatic({ root: './frontend' })); 73 | 74 | console.log('Serving...'); 75 | 76 | export default app; 77 | -------------------------------------------------------------------------------- /src/source.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | -------------------------------------------------------------------------------- /src/svg.js: -------------------------------------------------------------------------------- 1 | import { DOMParser } from '@xmldom/xmldom'; 2 | 3 | function childrenToDictionary(node) { 4 | let dict = {}; 5 | for (var i = 0; i < node.childNodes.length; i++) { 6 | let child = node.childNodes[i]; 7 | if (child.nodeType !== 1) { 8 | continue; 9 | } 10 | child.setAttribute('shape-rendering', 'crispEdges'); 11 | dict[child.getAttribute('id')] = child; 12 | } 13 | return dict; 14 | } 15 | 16 | export const parseSourceSvg = (sourceSvg) => { 17 | const doc = new DOMParser().parseFromString(sourceSvg); 18 | 19 | const flags = childrenToDictionary(doc.getElementById('flags')); 20 | const clips = childrenToDictionary(doc.getElementById('clips')); 21 | const overlays = childrenToDictionary(doc.getElementById('overlays')); 22 | 23 | let svg = doc.getElementsByTagName('svg')[0]; 24 | let emptySvg = svg.cloneNode(false); 25 | 26 | return { template: emptySvg, flags: flags, clips: clips, overlays: overlays }; 27 | }; 28 | 29 | export const buildBadgeSvg = (svg, flag1, flag2, clip, overlay) => { 30 | const tmp = svg.cloneNode(true); 31 | if (clip) { 32 | const doc = svg.ownerDocument; 33 | const defs = doc.createElement('defs'); 34 | const clipPath = doc.createElement('clipPath'); 35 | clipPath.setAttribute('id', 'clip'); 36 | clipPath.appendChild(clip); 37 | defs.appendChild(clipPath); 38 | tmp.appendChild(defs); 39 | } 40 | 41 | if (flag2) { 42 | flag2 = flag2.cloneNode(true); 43 | tmp.appendChild(flag2); 44 | } 45 | 46 | flag1 = flag1.cloneNode(true); 47 | flag1.setAttribute('clip-path', 'url(#clip)'); 48 | tmp.appendChild(flag1); 49 | 50 | if (overlay) { 51 | tmp.appendChild(overlay); 52 | } 53 | 54 | return tmp.toString(); 55 | }; 56 | --------------------------------------------------------------------------------