├── .nvmrc ├── .npmrc ├── .prettierignore ├── .htmlnanorc ├── src ├── assets │ ├── logo.png │ ├── fonts │ │ ├── Pressuru │ │ │ ├── LICENSE.txt │ │ │ └── Pressuru.ttf │ │ ├── Oswald │ │ │ ├── Oswald-Bold.ttf │ │ │ ├── Oswald-Regular.ttf │ │ │ └── OFL.txt │ │ ├── Roboto │ │ │ ├── Roboto-Bold.ttf │ │ │ ├── Roboto-Regular.ttf │ │ │ └── LICENSE.txt │ │ ├── OpenSans │ │ │ ├── OpenSans-Bold.ttf │ │ │ ├── OpenSans-Regular.ttf │ │ │ └── OFL.txt │ │ ├── CourierPrime │ │ │ ├── CourierPrime-Bold.ttf │ │ │ ├── CourierPrime-Regular.ttf │ │ │ └── OFL.txt │ │ └── RobotoCondensed │ │ │ ├── RobotoCondensed-Bold.ttf │ │ │ ├── RobotoCondensed-Regular.ttf │ │ │ └── LICENSE.txt │ ├── app-icons │ │ ├── logo.png │ │ ├── ios │ │ │ └── 180.png │ │ ├── android │ │ │ ├── 192.png │ │ │ ├── 384.png │ │ │ └── 512.png │ │ ├── favicon-192.png │ │ └── screenshots │ │ │ └── screenshot.png │ ├── meme-templates │ │ ├── bernie.jpg │ │ ├── shame.jpg │ │ ├── be-honest.jpg │ │ ├── car-drift.jpg │ │ ├── godfather.jpg │ │ ├── harakiri.jpg │ │ ├── nervous.jpg │ │ ├── bell-curve.jpg │ │ ├── goose-chase.jpg │ │ ├── milk-girls.jpg │ │ ├── obama-medal.jpg │ │ ├── success-kid.jpg │ │ ├── toilet_guy.jpg │ │ ├── two-buttons.jpg │ │ ├── undertaker.jpg │ │ ├── anakin-padme.jpg │ │ ├── bad-luck-brian.jpg │ │ ├── change-my-mind.jpg │ │ ├── disaster-girl.jpg │ │ ├── epic-handshake.jpg │ │ ├── gibson-jesus.jpg │ │ ├── grinning-girl.jpg │ │ ├── group-therapy.jpg │ │ ├── i-dont-always.jpg │ │ ├── laughing-leo.jpg │ │ ├── monkey_puppet.jpg │ │ ├── plague-hackers.jpg │ │ ├── star-wars-yoda.jpg │ │ ├── think-about-it.jpg │ │ ├── this-is-fine.jpg │ │ ├── three-dragons.jpg │ │ ├── x-x-everywhere.jpg │ │ ├── always-has-been.jpg │ │ ├── disappointed-guy.jpg │ │ ├── girls-gossiping.jpg │ │ ├── hack-the-planet.jpg │ │ ├── matrix-morpheus.jpg │ │ ├── monk-temptation.jpg │ │ ├── mr-bean-waiting.jpg │ │ ├── nerd-south-park.jpg │ │ ├── office-congrats.jpg │ │ ├── sparta-leonidas.jpg │ │ ├── sweating-bullets.jpg │ │ ├── the-rock-driving.jpg │ │ ├── trump-interview.jpg │ │ ├── waiting-skeleton.jpg │ │ ├── xkcd-dependency.jpg │ │ ├── afraid-to-ask-andy.jpg │ │ ├── ben-affleck-smoking.jpg │ │ ├── drake-hotline-bling.jpg │ │ ├── finding-neverland.jpg │ │ ├── i-see-dead-people.jpg │ │ ├── look-of-superiority.jpg │ │ ├── one-does-not-simply.jpg │ │ ├── sad-pablo-escobar.jpg │ │ ├── tabasco-eye-drops.jpg │ │ ├── uno-draw-25-cards.jpg │ │ ├── unsheathing-sword.jpg │ │ ├── batman-slapping-robin.jpg │ │ ├── distracted-boyfriend.jpg │ │ ├── hide-the-pain-harold.jpg │ │ ├── i-want-you-uncle-sam.jpg │ │ ├── sad-guy-happy-guy-bus.jpg │ │ ├── shirley-temple-laugh.png │ │ ├── zero-days-since-last.jpg │ │ ├── are-you-sleeping-brain.jpg │ │ ├── captain-picard-facepalm.jpg │ │ ├── drowning-kid-in-the-pool.jpg │ │ ├── grant-gustin-over-grave.jpg │ │ ├── jason-momoa-henry-cavill.jpg │ │ ├── john-wick-consequences.jpg │ │ ├── keep-calm-and-carry-on.jpg │ │ ├── leonardo-dicaprio-cheers.jpg │ │ ├── scooby-doo-mask-reveal.jpg │ │ ├── theyre-the-same-picture.jpg │ │ ├── water-tank-leaking-fix.png │ │ ├── whisper-and-goosebumps.jpg │ │ ├── austin-powers-doctor-evil.jpg │ │ ├── bill-murray-groundhog-day.jpg │ │ ├── jack-sparrow-being-chased.jpg │ │ ├── jurassic-park-no-one-cares.jpg │ │ ├── peter-griffin-running-away.jpg │ │ ├── say-that-again-i-dare-you.jpg │ │ ├── shut-up-and-take-my-money.jpg │ │ ├── you-guys-are-getting-paid.png │ │ ├── greta-thunberg-how-dare-you.jpg │ │ ├── knight-with-arrow-in-helmet.jpg │ │ ├── austin-powers-laughing-villains.jpg │ │ ├── spiderman-pointing-at-spiderman.jpg │ │ ├── terminator-hasta-la-vista-baby.jpg │ │ ├── i-bet-hes-thinking-about-other-women.jpg │ │ └── tell-me-the-truth-I-am-ready-to-hear-it.png │ └── icons │ │ ├── trash.svg │ │ ├── chevron-up.svg │ │ ├── chevron-left.svg │ │ ├── chevron-down.svg │ │ ├── chevron-right.svg │ │ ├── duplicate-dark.svg │ │ ├── duplicate-light.svg │ │ ├── camera.svg │ │ ├── photo.svg │ │ ├── save.svg │ │ ├── share.svg │ │ ├── spinner.svg │ │ ├── add-image-dark.svg │ │ ├── add-image-light.svg │ │ └── gear.svg ├── js │ ├── utils │ │ ├── is-solid-color-selected.js │ │ ├── uid.js │ │ ├── file-from-url.js │ │ └── storage.js │ ├── constants.js │ ├── register-service-worker.js │ ├── theme.js │ ├── custom-fonts.js │ ├── toastify.js │ ├── canvas.js │ ├── textbox.js │ └── index.js ├── manifest.webmanifest ├── css │ └── main.css └── index.html ├── screenshots └── screenshot.png ├── .zed └── settings.json ├── .vscode ├── extensions.json └── settings.json ├── workbox-config.js ├── .prettierrc ├── .editorconfig ├── eslint.config.mjs ├── LICENSE ├── .gitignore ├── package.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-prefix=~ 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.html 2 | **/*.css 3 | **/*.md 4 | -------------------------------------------------------------------------------- /.htmlnanorc: -------------------------------------------------------------------------------- 1 | { 2 | "minifySvg": false, 3 | "removeRedundantAttributes": false 4 | } 5 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/fonts/Pressuru/LICENSE.txt: -------------------------------------------------------------------------------- 1 | You are totally free to use the typeface any way you like. 2 | 3 | -------------------------------------------------------------------------------- /screenshots/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/screenshots/screenshot.png -------------------------------------------------------------------------------- /src/assets/app-icons/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/app-icons/logo.png -------------------------------------------------------------------------------- /src/assets/app-icons/ios/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/app-icons/ios/180.png -------------------------------------------------------------------------------- /src/assets/app-icons/android/192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/app-icons/android/192.png -------------------------------------------------------------------------------- /src/assets/app-icons/android/384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/app-icons/android/384.png -------------------------------------------------------------------------------- /src/assets/app-icons/android/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/app-icons/android/512.png -------------------------------------------------------------------------------- /src/assets/app-icons/favicon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/app-icons/favicon-192.png -------------------------------------------------------------------------------- /src/assets/meme-templates/bernie.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/bernie.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/shame.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/shame.jpg -------------------------------------------------------------------------------- /src/assets/fonts/Oswald/Oswald-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/fonts/Oswald/Oswald-Bold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Pressuru/Pressuru.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/fonts/Pressuru/Pressuru.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Roboto/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/fonts/Roboto/Roboto-Bold.ttf -------------------------------------------------------------------------------- /src/assets/meme-templates/be-honest.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/be-honest.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/car-drift.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/car-drift.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/godfather.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/godfather.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/harakiri.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/harakiri.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/nervous.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/nervous.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/bell-curve.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/bell-curve.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/goose-chase.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/goose-chase.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/milk-girls.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/milk-girls.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/obama-medal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/obama-medal.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/success-kid.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/success-kid.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/toilet_guy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/toilet_guy.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/two-buttons.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/two-buttons.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/undertaker.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/undertaker.jpg -------------------------------------------------------------------------------- /src/assets/fonts/OpenSans/OpenSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/fonts/OpenSans/OpenSans-Bold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Oswald/Oswald-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/fonts/Oswald/Oswald-Regular.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Roboto/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/fonts/Roboto/Roboto-Regular.ttf -------------------------------------------------------------------------------- /src/assets/meme-templates/anakin-padme.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/anakin-padme.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/bad-luck-brian.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/bad-luck-brian.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/change-my-mind.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/change-my-mind.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/disaster-girl.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/disaster-girl.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/epic-handshake.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/epic-handshake.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/gibson-jesus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/gibson-jesus.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/grinning-girl.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/grinning-girl.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/group-therapy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/group-therapy.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/i-dont-always.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/i-dont-always.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/laughing-leo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/laughing-leo.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/monkey_puppet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/monkey_puppet.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/plague-hackers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/plague-hackers.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/star-wars-yoda.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/star-wars-yoda.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/think-about-it.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/think-about-it.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/this-is-fine.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/this-is-fine.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/three-dragons.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/three-dragons.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/x-x-everywhere.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/x-x-everywhere.jpg -------------------------------------------------------------------------------- /src/assets/fonts/OpenSans/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/fonts/OpenSans/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /src/assets/meme-templates/always-has-been.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/always-has-been.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/disappointed-guy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/disappointed-guy.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/girls-gossiping.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/girls-gossiping.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/hack-the-planet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/hack-the-planet.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/matrix-morpheus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/matrix-morpheus.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/monk-temptation.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/monk-temptation.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/mr-bean-waiting.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/mr-bean-waiting.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/nerd-south-park.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/nerd-south-park.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/office-congrats.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/office-congrats.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/sparta-leonidas.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/sparta-leonidas.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/sweating-bullets.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/sweating-bullets.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/the-rock-driving.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/the-rock-driving.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/trump-interview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/trump-interview.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/waiting-skeleton.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/waiting-skeleton.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/xkcd-dependency.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/xkcd-dependency.jpg -------------------------------------------------------------------------------- /src/js/utils/is-solid-color-selected.js: -------------------------------------------------------------------------------- 1 | export const isSolidColorSelected = selectedImage => { 2 | return typeof selectedImage === 'string'; 3 | }; 4 | -------------------------------------------------------------------------------- /src/assets/app-icons/screenshots/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/app-icons/screenshots/screenshot.png -------------------------------------------------------------------------------- /src/assets/meme-templates/afraid-to-ask-andy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/afraid-to-ask-andy.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/ben-affleck-smoking.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/ben-affleck-smoking.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/drake-hotline-bling.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/drake-hotline-bling.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/finding-neverland.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/finding-neverland.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/i-see-dead-people.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/i-see-dead-people.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/look-of-superiority.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/look-of-superiority.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/one-does-not-simply.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/one-does-not-simply.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/sad-pablo-escobar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/sad-pablo-escobar.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/tabasco-eye-drops.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/tabasco-eye-drops.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/uno-draw-25-cards.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/uno-draw-25-cards.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/unsheathing-sword.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/unsheathing-sword.jpg -------------------------------------------------------------------------------- /.zed/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "languages": { 3 | "JavaScript": { 4 | "formatter": "prettier", 5 | "format_on_save": "on" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/assets/fonts/CourierPrime/CourierPrime-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/fonts/CourierPrime/CourierPrime-Bold.ttf -------------------------------------------------------------------------------- /src/assets/meme-templates/batman-slapping-robin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/batman-slapping-robin.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/distracted-boyfriend.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/distracted-boyfriend.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/hide-the-pain-harold.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/hide-the-pain-harold.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/i-want-you-uncle-sam.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/i-want-you-uncle-sam.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/sad-guy-happy-guy-bus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/sad-guy-happy-guy-bus.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/shirley-temple-laugh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/shirley-temple-laugh.png -------------------------------------------------------------------------------- /src/assets/meme-templates/zero-days-since-last.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/zero-days-since-last.jpg -------------------------------------------------------------------------------- /src/assets/fonts/CourierPrime/CourierPrime-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/fonts/CourierPrime/CourierPrime-Regular.ttf -------------------------------------------------------------------------------- /src/assets/meme-templates/are-you-sleeping-brain.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/are-you-sleeping-brain.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/captain-picard-facepalm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/captain-picard-facepalm.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/drowning-kid-in-the-pool.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/drowning-kid-in-the-pool.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/grant-gustin-over-grave.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/grant-gustin-over-grave.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/jason-momoa-henry-cavill.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/jason-momoa-henry-cavill.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/john-wick-consequences.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/john-wick-consequences.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/keep-calm-and-carry-on.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/keep-calm-and-carry-on.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/leonardo-dicaprio-cheers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/leonardo-dicaprio-cheers.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/scooby-doo-mask-reveal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/scooby-doo-mask-reveal.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/theyre-the-same-picture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/theyre-the-same-picture.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/water-tank-leaking-fix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/water-tank-leaking-fix.png -------------------------------------------------------------------------------- /src/assets/meme-templates/whisper-and-goosebumps.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/whisper-and-goosebumps.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/austin-powers-doctor-evil.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/austin-powers-doctor-evil.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/bill-murray-groundhog-day.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/bill-murray-groundhog-day.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/jack-sparrow-being-chased.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/jack-sparrow-being-chased.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/jurassic-park-no-one-cares.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/jurassic-park-no-one-cares.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/peter-griffin-running-away.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/peter-griffin-running-away.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/say-that-again-i-dare-you.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/say-that-again-i-dare-you.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/shut-up-and-take-my-money.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/shut-up-and-take-my-money.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/you-guys-are-getting-paid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/you-guys-are-getting-paid.png -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "dbaeumer.vscode-eslint", 5 | "editorconfig.editorconfig" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src/assets/fonts/RobotoCondensed/RobotoCondensed-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/fonts/RobotoCondensed/RobotoCondensed-Bold.ttf -------------------------------------------------------------------------------- /src/assets/meme-templates/greta-thunberg-how-dare-you.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/greta-thunberg-how-dare-you.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/knight-with-arrow-in-helmet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/knight-with-arrow-in-helmet.jpg -------------------------------------------------------------------------------- /src/assets/fonts/RobotoCondensed/RobotoCondensed-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/fonts/RobotoCondensed/RobotoCondensed-Regular.ttf -------------------------------------------------------------------------------- /src/assets/meme-templates/austin-powers-laughing-villains.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/austin-powers-laughing-villains.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/spiderman-pointing-at-spiderman.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/spiderman-pointing-at-spiderman.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/terminator-hasta-la-vista-baby.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/terminator-hasta-la-vista-baby.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/i-bet-hes-thinking-about-other-women.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/i-bet-hes-thinking-about-other-women.jpg -------------------------------------------------------------------------------- /src/assets/meme-templates/tell-me-the-truth-I-am-ready-to-hear-it.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georapbox/meme-generator/HEAD/src/assets/meme-templates/tell-me-the-truth-I-am-ready-to-hear-it.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true, 4 | "eslint.enable": true, 5 | "eslint.run": "onType", 6 | "eslint.validate": ["javascript", "javascriptreact", "html", "typescriptreact"] 7 | } 8 | -------------------------------------------------------------------------------- /workbox-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | globDirectory: 'dist', 3 | globPatterns: ['**/*.{html,js,css,png,svg,jpg,gif,json,ttf,woff,woff2,eot,ico,webmanifest,map}'], 4 | swDest: 'dist/service-worker.js', 5 | clientsClaim: true, 6 | skipWaiting: true 7 | }; 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "tabWidth": 2, 5 | "useTabs": false, 6 | "printWidth": 120, 7 | "singleQuote": true, 8 | "semi": true, 9 | "trailingComma": "none", 10 | "singleAttributePerLine": false 11 | } 12 | -------------------------------------------------------------------------------- /src/assets/icons/trash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/chevron-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/chevron-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/chevron-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/chevron-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/js/constants.js: -------------------------------------------------------------------------------- 1 | export const ACCEPTED_MIME_TYPES = [ 2 | 'image/jpg', 3 | 'image/jpeg', 4 | 'image/png', 5 | 'image/apng', 6 | 'image/gif', 7 | 'image/webp', 8 | 'image/avif' 9 | ]; 10 | export const MAX_SHADOW_BLUR_SIZE = 20; 11 | export const MAX_STROKE_WIDTH = 20; 12 | export const MAX_ROTATE = 360; 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /src/js/register-service-worker.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | const sw = 'service-worker.js'; // Required because parcel will not recognize this as a file and will throw during build. 3 | 4 | if ('serviceWorker' in navigator) { 5 | navigator.serviceWorker.register(sw).catch(err => { 6 | console.error(err); 7 | }); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/assets/icons/duplicate-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/duplicate-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/camera.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/photo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/save.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from 'globals'; 2 | import pluginJs from '@eslint/js'; 3 | 4 | export default [ 5 | { 6 | languageOptions: { 7 | globals: { 8 | ...globals.browser, 9 | ...globals.node 10 | } 11 | } 12 | }, 13 | pluginJs.configs.recommended, 14 | { 15 | rules: { 16 | 'no-use-before-define': [ 17 | 'error', 18 | { 19 | functions: false 20 | } 21 | ], 22 | curly: ['warn'], 23 | eqeqeq: [ 24 | 'error', 25 | 'always', 26 | { 27 | null: 'ignore' 28 | } 29 | ] 30 | } 31 | }, 32 | { 33 | ignores: ['dist'] 34 | } 35 | ]; 36 | -------------------------------------------------------------------------------- /src/assets/icons/share.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/js/utils/uid.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generates a unique id of the form `${prefix}-${randomString}-${suffix}`. 3 | * 4 | * @param {string} [prefix=''] - The prefix to use for the id. 5 | * @param {string} [suffix=''] - The suffix to use for the id. 6 | * @returns {string} - The unique id. 7 | */ 8 | const uid = (prefix = '', suffix = '') => { 9 | const prefixString = typeof prefix === 'string' && prefix !== '' ? prefix + '-' : ''; 10 | const suffixString = typeof suffix === 'string' && suffix !== '' ? '-' + suffix : ''; 11 | const randomString = Math.random().toString(36).substring(2, 8); // Pseudo-random string of six alphanumeric characters. 12 | 13 | return `${prefixString}${randomString}${suffixString}`; 14 | }; 15 | 16 | export { uid }; 17 | -------------------------------------------------------------------------------- /src/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Meme Generator", 3 | "name": "Meme Generator", 4 | "description": "Create memes by uploading an image or taking a photo.", 5 | "screenshots": [ 6 | { 7 | "src": "assets/app-icons/screenshots/screenshot.png", 8 | "type": "image/png", 9 | "sizes": "2504x1752" 10 | } 11 | ], 12 | "icons": [ 13 | { 14 | "src": "assets/app-icons/android/192.png", 15 | "sizes": "192x192", 16 | "type": "image/png" 17 | }, 18 | { 19 | "src": "assets/app-icons/android/384.png", 20 | "sizes": "384x384", 21 | "type": "image/png" 22 | }, 23 | { 24 | "src": "assets/app-icons/android/512.png", 25 | "sizes": "512x512", 26 | "type": "image/png" 27 | } 28 | ], 29 | "start_url": ".", 30 | "background_color": "#f8f9fa", 31 | "theme_color": "#f8f9fa", 32 | "display": "standalone" 33 | } -------------------------------------------------------------------------------- /src/assets/icons/spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/js/theme.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | function getStoredTheme() { 3 | return window.localStorage.getItem('meme-generator/theme') || 'system'; 4 | } 5 | 6 | function getDeviceTheme() { 7 | return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; 8 | } 9 | 10 | function applyTheme(theme) { 11 | const themeMap = { 12 | dark: 'dark', 13 | light: 'light', 14 | system: getDeviceTheme() 15 | }; 16 | 17 | document.documentElement.setAttribute('data-bs-theme', themeMap[theme] || themeMap.system); 18 | } 19 | 20 | function handleMatchMediaChange(evt) { 21 | if (getStoredTheme() !== 'system') { 22 | return; 23 | } 24 | 25 | applyTheme(evt.matches ? 'dark' : 'light'); 26 | } 27 | 28 | try { 29 | window 30 | .matchMedia('(prefers-color-scheme: dark)') 31 | .addEventListener('change', handleMatchMediaChange); 32 | 33 | applyTheme(getStoredTheme()); 34 | } catch (err) { 35 | console.error(err); 36 | } 37 | })(); 38 | -------------------------------------------------------------------------------- /src/js/utils/file-from-url.js: -------------------------------------------------------------------------------- 1 | import { ACCEPTED_MIME_TYPES } from '../constants.js'; 2 | 3 | /** 4 | * Creates a file from a given URL. 5 | * 6 | * @param {{ url: string, filename: string, mimeType: string }} options 7 | * @param {string} options.url - The URL of the file to fetch. 8 | * @param {string} options.filename - The name of the file to create. 9 | * @param {string} options.mimeType - The MIME type of the file to create. 10 | * @returns {Promise} - A promise that resolves to the file created from the given URL. 11 | */ 12 | export const fileFromUrl = async (options = {}) => { 13 | const res = await fetch(options.url); 14 | const blob = await res.blob(); 15 | const mimeType = options.mimeType || blob.type || ''; 16 | 17 | if (!ACCEPTED_MIME_TYPES.includes(mimeType)) { 18 | throw new Error( 19 | `This is not an accepted image format. Accepted MIME types are: ${ACCEPTED_MIME_TYPES.join(', ')}` 20 | ); 21 | } 22 | 23 | return new File([blob], options.filename || '', blob); 24 | }; 25 | -------------------------------------------------------------------------------- /src/assets/icons/add-image-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/icons/add-image-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-present George Raptis 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 | -------------------------------------------------------------------------------- /src/assets/icons/gear.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # production 61 | dist 62 | lib 63 | 64 | # misc 65 | .DS_Store 66 | .parcel-cache 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "meme-generator", 3 | "version": "1.0.0", 4 | "description": "A Progressive Web App (PWA) for creating memes.", 5 | "source": "src/index.html", 6 | "scripts": { 7 | "lint": "eslint src/js/**", 8 | "format": "prettier --ignore-unknown --write .", 9 | "clean": "rimraf dist .parcel-cache", 10 | "generateSW": "workbox generateSW", 11 | "start": "parcel", 12 | "build:parcel": "parcel build --dist-dir=dist --public-url=./", 13 | "build": "npm-run-all clean build:parcel", 14 | "postbuild": "npm run generateSW", 15 | "predeploy": "npm run build", 16 | "deploy": "gh-pages -d dist" 17 | }, 18 | "keywords": [ 19 | "meme generator" 20 | ], 21 | "author": { 22 | "name": "George Raptis", 23 | "email": "georapbox@gmail.com" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/georapbox/meme-generator.git" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/georapbox/meme-generator/issues" 31 | }, 32 | "homepage": "https://github.com/georapbox/meme-generator#readme", 33 | "license": "MIT", 34 | "devDependencies": { 35 | "@eslint/js": "~9.39.2", 36 | "@parcel/packager-raw-url": "~2.16.3", 37 | "@parcel/transformer-webmanifest": "~2.16.3", 38 | "eslint": "~9.39.2", 39 | "gh-pages": "~6.3.0", 40 | "globals": "~16.5.0", 41 | "npm-run-all": "~4.1.5", 42 | "parcel": "~2.16.3", 43 | "prettier": "~3.7.4", 44 | "rimraf": "~6.1.2", 45 | "workbox-cli": "~7.4.0" 46 | }, 47 | "dependencies": { 48 | "@georapbox/alert-element": "~1.2.2", 49 | "@georapbox/capture-photo-element": "~5.0.0", 50 | "@georapbox/files-dropzone-element": "~2.1.1", 51 | "@georapbox/modal-element": "~1.10.0", 52 | "@georapbox/theme-toggle-element": "~4.0.2", 53 | "@georapbox/web-share-element": "~3.1.1", 54 | "bootstrap": "~5.3.8", 55 | "emoji-picker-element": "~1.28.1", 56 | "insert-text-at-cursor": "~0.3.0" 57 | }, 58 | "browserslist": "> 0.5%, last 2 versions, not dead" 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Meme Generator 2 | 3 | A Progressive Web App (PWA) for creating memes. 4 | 5 | ## About 6 | 7 | This is a web application that allows users to create memes by adding text to images. 8 | The application is built with web technologies such as HTML, CSS, and JavaScript. 9 | It uses the [Canvas API](https://developer.mozilla.org/docs/Web/API/Canvas_API) to draw the meme text on the image. 10 | Processing the image and text is done client-side, so no data is sent to any server. 11 | 12 | ## Live demo 13 | 14 | 👉 [Meme Generator](https://georapbox.github.io/meme-generator/) 15 | 16 | ## Features 17 | 18 | Some of the key features of the application include creating memes by: 19 | - Selecting an image from your device 20 | - Selecting an image from the web (by URL) 21 | - Selecting an image from the gallery 22 | - Taking a photo with your device's web camera 23 | - Using a solid color as background 24 | 25 | ## Screenshots 26 | 27 | The following screenshots show the application in action: 28 | 29 | ![meme](screenshots/screenshot.png) 30 | 31 | ## Development 32 | 33 | Below are the instructions for setting up the development environment. 34 | 35 | ### Prerequisites 36 | 37 | - Node.js (v20.x.x) 38 | - npm (v10.x.x) 39 | 40 | ### Installation 41 | 42 | Clone the repository to your local machine: 43 | 44 | ```sh 45 | git clone git@github.com:georapbox/meme-generator.git 46 | ``` 47 | 48 | Navigate to the project's directory and install the dependencies: 49 | 50 | ```sh 51 | npm install 52 | ``` 53 | 54 | ### Running the application 55 | 56 | To run the application in development mode, run the following command: 57 | 58 | ```sh 59 | npm start -- --open 60 | ``` 61 | 62 | This will start the development server and open the application in your default web browser. 63 | 64 | ### Building the application for production 65 | 66 | To build the application for production, run the following command: 67 | 68 | ```sh 69 | npm run build 70 | ``` 71 | 72 | This will create a `dist` directory containing the production build of the application. 73 | 74 | ### Deployment 75 | 76 | To deploy the application, run the following command: 77 | 78 | ```sh 79 | npm run deploy 80 | ``` 81 | 82 | This will build the application first and then deploy it to GitHub Pages in the `gh-pages` branch. 83 | 84 | ## License 85 | 86 | [The MIT License (MIT)](https://github.com/georapbox/meme-generator/blob/master/LICENSE) 87 | -------------------------------------------------------------------------------- /src/js/utils/storage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A class representing a storage utility for managing data in `localStorage` or `sessionStorage`. 3 | */ 4 | class Storage { 5 | /** 6 | * The prefix to be added to all keys. 7 | * 8 | * @type {string} 9 | * @private 10 | */ 11 | #keyPrefix = null; 12 | 13 | /** 14 | * The storage provider (`localStorage` or `sessionStorage`). 15 | * 16 | * @type {Storage} 17 | * @private 18 | */ 19 | #provider = null; 20 | 21 | /** 22 | * Creates an instance of Storage. 23 | * 24 | * @param {string} prefix - The prefix to be added to all keys when storing/retrieving data. 25 | * @param {Storage} [provider=localStorage] - The storage provider (`localStorage` or `sessionStorage`). 26 | * @throws {Error} If prefix is not provided or if the provider is not supported. 27 | */ 28 | constructor(prefix, provider = localStorage) { 29 | if (!prefix) { 30 | throw new Error('Storage prefix is required'); 31 | } 32 | 33 | if (provider !== localStorage && provider !== sessionStorage) { 34 | throw new Error('Storage provider is not supported'); 35 | } 36 | 37 | this.#keyPrefix = prefix; 38 | this.#provider = provider; 39 | } 40 | 41 | /** 42 | * Sets a key-value pair in the storage. 43 | * 44 | * @param {string} key - The key for the data. 45 | * @param {*} value - The value to be stored (will be converted to JSON string). 46 | */ 47 | set(key, value) { 48 | try { 49 | this.#provider.setItem(`${this.#keyPrefix}${key}`, JSON.stringify(value)); 50 | } catch (err) { 51 | console.error('Error saving to storage', err); 52 | } 53 | } 54 | 55 | /** 56 | * Retrieves the value associated with the given key from the storage. 57 | * 58 | * @param {string} key - The key to retrieve the value for. 59 | * @returns {*} The value associated with the key, or `null` if key is not found or an error occurs. 60 | */ 61 | get(key) { 62 | try { 63 | const value = this.#provider.getItem(`${this.#keyPrefix}${key}`); 64 | return value ? JSON.parse(value) : null; 65 | } catch (err) { 66 | console.error('Error getting from storage', err); 67 | return null; 68 | } 69 | } 70 | } 71 | 72 | const DEFAULT_STORAGE_PREFIX = 'meme-generator/'; 73 | const DEFAULT_STORAGE_PROVIDER = localStorage; 74 | const storage = new Storage(DEFAULT_STORAGE_PREFIX, DEFAULT_STORAGE_PROVIDER); 75 | 76 | export { Storage, storage }; 77 | -------------------------------------------------------------------------------- /src/js/custom-fonts.js: -------------------------------------------------------------------------------- 1 | import Pressuru from 'url:../assets/fonts/Pressuru/Pressuru.ttf'; 2 | import OswaldRegular from 'url:../assets/fonts/Oswald/Oswald-Regular.ttf'; 3 | import OswaldBold from 'url:../assets/fonts/Oswald/Oswald-Bold.ttf'; 4 | import RobotoRegular from 'url:../assets/fonts/Roboto/Roboto-Regular.ttf'; 5 | import RobotoBold from 'url:../assets/fonts/Roboto/Roboto-Bold.ttf'; 6 | import RobotoCondensedRegular from 'url:../assets/fonts/RobotoCondensed/RobotoCondensed-Regular.ttf'; 7 | import RobotoCondensedBold from 'url:../assets/fonts/RobotoCondensed/RobotoCondensed-Bold.ttf'; 8 | import CourierPrimeRegular from 'url:../assets/fonts/CourierPrime/CourierPrime-Regular.ttf'; 9 | import CourierPrimeBold from 'url:../assets/fonts/CourierPrime/CourierPrime-Bold.ttf'; 10 | import OpenSansRegular from 'url:../assets/fonts/OpenSans/OpenSans-Regular.ttf'; 11 | import OpenSansBold from 'url:../assets/fonts/OpenSans/OpenSans-Bold.ttf'; 12 | 13 | export const customFonts = [ 14 | { 15 | name: 'Pressuru', 16 | label: 'Pressuru', 17 | path: Pressuru, 18 | style: 'normal', 19 | weight: '400' 20 | }, 21 | { 22 | name: 'Oswald-Regular', 23 | label: 'Oswald', 24 | path: OswaldRegular, 25 | style: 'normal', 26 | weight: '400' 27 | }, 28 | { 29 | name: 'Oswald-Bold', 30 | label: 'Oswald Bold', 31 | path: OswaldBold, 32 | style: 'normal', 33 | weight: '700' 34 | }, 35 | { 36 | name: 'Roboto-Regular', 37 | label: 'Roboto', 38 | path: RobotoRegular, 39 | style: 'normal', 40 | weight: '400' 41 | }, 42 | { 43 | name: 'Roboto-Bold', 44 | label: 'Roboto Bold', 45 | path: RobotoBold, 46 | style: 'normal', 47 | weight: '700' 48 | }, 49 | { 50 | name: 'RobotoCondensed-Regular', 51 | label: 'Roboto Condensed', 52 | path: RobotoCondensedRegular, 53 | style: 'normal', 54 | weight: '400' 55 | }, 56 | { 57 | name: 'RobotoCondensed-Bold', 58 | label: 'Roboto Condensed Bold', 59 | path: RobotoCondensedBold, 60 | style: 'normal', 61 | weight: '700' 62 | }, 63 | { 64 | name: 'CourierPrime-Regular', 65 | label: 'Courier Prime', 66 | path: CourierPrimeRegular, 67 | style: 'normal', 68 | weight: '400' 69 | }, 70 | { 71 | name: 'CourierPrime-Bold', 72 | label: 'Courier Prime Bold', 73 | path: CourierPrimeBold, 74 | style: 'normal', 75 | weight: '700' 76 | }, 77 | { 78 | name: 'OpenSans-Regular', 79 | label: 'Open Sans', 80 | path: OpenSansRegular, 81 | style: 'normal', 82 | weight: '400' 83 | }, 84 | { 85 | name: 'OpenSans-Bold', 86 | label: 'Open Sans Bold', 87 | path: OpenSansBold, 88 | style: 'normal', 89 | weight: '400' 90 | } 91 | ]; 92 | 93 | export const loadCustomFont = async (name, path, options = {}) => { 94 | try { 95 | const font = new FontFace(name, `url(${path})`, { ...options }); 96 | await font.load(); 97 | document.fonts.add(font); 98 | } catch (err) { 99 | console.error(err); 100 | } 101 | }; 102 | -------------------------------------------------------------------------------- /src/js/toastify.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Displays a toast notification. 3 | * 4 | * @param {string} message - The message to display in the toast. 5 | * @param {Object} [options] - Options for the toast. 6 | * @param {number} [options.duration=5000] - Duration in milliseconds before the toast disappears. 7 | * @param {string} [options.variant='neutral'] - The variant of the toast (e.g., 'success', 'danger', 'info'). 8 | * @param {boolean} [options.trustDangerousInnerHTML=false] - Whether to trust dangerous HTML in the message. 9 | * @returns {Promise} - A promise that resolves to the alert element. 10 | */ 11 | export function toastify(message, options = {}) { 12 | const defaults = { 13 | duration: 5 * 1000, 14 | variant: 'neutral', 15 | countdown: true, 16 | icon: '' 17 | }; 18 | 19 | const cfg = { ...defaults, ...options }; 20 | 21 | const icons = { 22 | info: ` 23 | 27 | `, 28 | success: ` 29 | 33 | `, 34 | warning: ` 35 | 39 | `, 40 | danger: ` 41 | 45 | ` 46 | }; 47 | 48 | const icon = icons[cfg.icon || cfg.variant] || ''; 49 | 50 | const alert = Object.assign(document.createElement('alert-element'), { 51 | closable: true, 52 | duration: cfg.duration, 53 | variant: cfg.variant, 54 | countdown: cfg.countdown, 55 | innerHTML: `${icon ? `${icon}` : ''}${cfg.trustDangerousInnerHTML ? message : escapeHtml(message)}` 56 | }); 57 | 58 | return alert.toast(); 59 | } 60 | 61 | function escapeHtml(html) { 62 | const div = document.createElement('div'); 63 | div.textContent = html; 64 | return div.innerHTML; 65 | } 66 | -------------------------------------------------------------------------------- /src/js/canvas.js: -------------------------------------------------------------------------------- 1 | import { isSolidColorSelected } from './utils/is-solid-color-selected.js'; 2 | import { MAX_SHADOW_BLUR_SIZE, MAX_STROKE_WIDTH, MAX_ROTATE } from './constants.js'; 3 | 4 | export class Canvas { 5 | #canvas = null; 6 | #ctx = null; 7 | 8 | constructor(canvasEl) { 9 | this.#canvas = canvasEl; 10 | this.#ctx = this.#canvas.getContext('2d'); 11 | } 12 | 13 | get width() { 14 | return this.#canvas.width; 15 | } 16 | 17 | set width(value) { 18 | this.#canvas.width = value; 19 | } 20 | 21 | get height() { 22 | return this.#canvas.height; 23 | } 24 | 25 | set height(value) { 26 | this.#canvas.height = value; 27 | } 28 | 29 | getDimensions() { 30 | return { 31 | width: this.width, 32 | height: this.height 33 | }; 34 | } 35 | 36 | setDimensions({ width, height }) { 37 | this.width = width; 38 | this.height = height; 39 | return this; 40 | } 41 | 42 | toDataURL() { 43 | return this.#canvas.toDataURL(); 44 | } 45 | 46 | draw(image, textboxes = new Map()) { 47 | if (image == null) { 48 | return; 49 | } 50 | 51 | const canvas = this.#canvas; 52 | const ctx = this.#ctx; 53 | 54 | ctx.clearRect(0, 0, canvas.width, canvas.height); 55 | 56 | if (isSolidColorSelected(image)) { 57 | ctx.fillStyle = image; 58 | ctx.fillRect(0, 0, canvas.width, canvas.height); 59 | } else { 60 | ctx.drawImage(image, 0, 0, canvas.width, canvas.height); 61 | } 62 | 63 | let multiplier = 0; 64 | 65 | textboxes.forEach(textbox => { 66 | const { data } = textbox; 67 | 68 | multiplier += 1; 69 | 70 | ctx.save(); 71 | 72 | ctx.font = `${data.fontWeight} ${(data.fontSize * canvas.width) / 1000}px ${data.font}`; 73 | ctx.textAlign = data.textAlign; 74 | ctx.strokeStyle = data.strokeColor; 75 | 76 | const xPos = canvas.width / 2; 77 | const shadowBlur = data.shadowBlur; 78 | const text = data.allCaps === true ? data.text.toUpperCase() : data.text; 79 | const textLines = text.split('\n').filter(line => line.trim() !== ''); 80 | const textMetrics = ctx.measureText(textLines[0]); 81 | const textHeight = textMetrics.actualBoundingBoxAscent + textMetrics.actualBoundingBoxDescent; 82 | const bgOffset = data.textBackgroundEnabled ? textHeight / 4 : 0; 83 | const lineHeight = textHeight + data.fontSize / 4 + bgOffset; 84 | 85 | ctx.translate(xPos + data.offsetX, lineHeight * multiplier + data.offsetY); 86 | ctx.rotate((Math.min(data.rotate, MAX_ROTATE) * Math.PI) / 180); 87 | 88 | if (data.textBackgroundEnabled) { 89 | ctx.fillStyle = data.textBackgroundColor; 90 | textLines.forEach((line, index) => { 91 | if (line) { 92 | const textMetrics = ctx.measureText(line); 93 | const textWidth = textMetrics.width; 94 | const textHeight = textMetrics.actualBoundingBoxAscent + textMetrics.actualBoundingBoxDescent; 95 | const xOffset = data.textAlign === 'left' ? 0 : data.textAlign === 'right' ? -textWidth : -textWidth / 2; 96 | 97 | ctx.fillRect( 98 | xOffset - bgOffset, 99 | index * lineHeight - textHeight - bgOffset, 100 | textWidth + bgOffset * 2, 101 | textHeight + bgOffset * 2 102 | ); 103 | } 104 | }); 105 | } 106 | 107 | ctx.save(); 108 | if (shadowBlur !== 0) { 109 | ctx.shadowOffsetX = 0; 110 | ctx.shadowOffsetY = 0; 111 | ctx.shadowBlur = Math.min(shadowBlur, MAX_SHADOW_BLUR_SIZE); 112 | ctx.shadowColor = data.strokeColor; 113 | } 114 | ctx.fillStyle = data.fillColor; 115 | textLines.forEach((text, index) => ctx.fillText(text, 0, index * lineHeight)); 116 | ctx.restore(); 117 | 118 | if (data.strokeWidth > 0) { 119 | ctx.lineWidth = Math.min(data.strokeWidth, MAX_STROKE_WIDTH); 120 | textLines.forEach((text, index) => ctx.strokeText(text, 0, index * lineHeight)); 121 | } 122 | 123 | ctx.restore(); 124 | }); 125 | 126 | return this; 127 | } 128 | 129 | clear() { 130 | this.#ctx.clearRect(0, 0, this.#canvas.width, this.#canvas.height); 131 | return this; 132 | } 133 | 134 | show() { 135 | this.#canvas.hidden = false; 136 | return this; 137 | } 138 | 139 | hide() { 140 | this.#canvas.hidden = true; 141 | return this; 142 | } 143 | 144 | static createInstance(canvasEl) { 145 | return new Canvas(canvasEl); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/assets/fonts/OpenSans/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /src/assets/fonts/Oswald/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2016 The Oswald Project Authors (https://github.com/googlefonts/OswaldFont) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /src/assets/fonts/CourierPrime/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2015 The Courier Prime Project Authors (https://github.com/quoteunquoteapps/CourierPrime). 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /src/js/textbox.js: -------------------------------------------------------------------------------- 1 | import { uid } from './utils/uid.js'; 2 | import { customFonts } from './custom-fonts.js'; 3 | import { MAX_SHADOW_BLUR_SIZE, MAX_STROKE_WIDTH, MAX_ROTATE } from './constants.js'; 4 | 5 | const defaultTextboxData = { 6 | id: '', 7 | text: '', 8 | fillColor: '#ffffff', 9 | strokeColor: '#000000', 10 | font: 'Pressuru', 11 | fontSize: 40, 12 | fontWeight: 'normal', 13 | textAlign: 'center', 14 | shadowBlur: 0, 15 | strokeWidth: 1.5, 16 | offsetY: 0, 17 | offsetX: 0, 18 | rotate: 0, 19 | allCaps: true, 20 | textBackgroundEnabled: false, 21 | textBackgroundColor: '#000000' 22 | }; 23 | 24 | const textboxes = new Map(); 25 | 26 | class Textbox { 27 | constructor(data) { 28 | const id = uid('textbox', Date.now().toString(36)); 29 | 30 | this.data = data ? { ...data, id } : { ...defaultTextboxData, id }; 31 | 32 | textboxes.set(id, this); 33 | 34 | document.dispatchEvent( 35 | new CustomEvent(`textbox-create`, { 36 | bubbles: true, 37 | composed: true, 38 | detail: { textbox: this } 39 | }) 40 | ); 41 | } 42 | 43 | getData() { 44 | return this.data; 45 | } 46 | 47 | static hasDefaultValues(textboxData) { 48 | for (const key of Object.keys(defaultTextboxData)) { 49 | if (key !== 'id' && textboxData[key] !== defaultTextboxData[key]) { 50 | return false; 51 | } 52 | } 53 | return true; 54 | } 55 | 56 | static create(data) { 57 | return new Textbox(data); 58 | } 59 | 60 | static getAll() { 61 | return textboxes; 62 | } 63 | 64 | static getById(id) { 65 | return textboxes.get(id); 66 | } 67 | 68 | static remove(id) { 69 | textboxes.delete(id); 70 | 71 | document.dispatchEvent( 72 | new CustomEvent(`textbox-remove`, { 73 | bubbles: true, 74 | composed: true, 75 | detail: { id } 76 | }) 77 | ); 78 | } 79 | 80 | static createElement(textbox, autoFocus = true) { 81 | if (!(textbox instanceof Textbox)) { 82 | return; 83 | } 84 | 85 | const data = textbox.getData(); 86 | const { 87 | id, 88 | text, 89 | fillColor, 90 | strokeColor, 91 | fontSize, 92 | shadowBlur, 93 | strokeWidth, 94 | offsetX, 95 | offsetY, 96 | rotate, 97 | textBackgroundColor 98 | } = data; 99 | 100 | const template = /* html */ ` 101 |
102 | 103 | 104 | 105 | 106 | 107 |
108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 |
116 |
117 | 118 | 241 | `; 242 | 243 | const fragment = document.createDocumentFragment(); 244 | const div = document.createElement('div'); 245 | 246 | div.setAttribute('id', id); 247 | div.setAttribute('data-section', 'textbox'); 248 | div.className = 'bg-body-tertiary border shadow-sm mb-3 rounded'; 249 | div.innerHTML = template; 250 | div.querySelectorAll('select').forEach(el => (el.value = data[el.dataset.input])); 251 | div.querySelectorAll('input[type="checkbox"]').forEach(el => (el.checked = data[el.dataset.input])); 252 | 253 | const textboxEl = fragment.appendChild(div); 254 | 255 | if (autoFocus) { 256 | setTimeout(() => textboxEl.querySelector('[data-input="text"]').focus(), 0); 257 | } 258 | 259 | return textboxEl; 260 | } 261 | } 262 | 263 | export { Textbox }; 264 | -------------------------------------------------------------------------------- /src/assets/fonts/Roboto/LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /src/assets/fonts/RobotoCondensed/LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /src/css/main.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --move-btn-width: 60px; 3 | --move-btn-height: 34px; 4 | --focus-ring: 0 0 0 0.2rem var(--bs-focus-ring-color); 5 | --focus-ring-error: 0 0 0 0.2rem rgba(var(--bs-danger-rgb), 0.4); 6 | 7 | accent-color: var(--bs-primary); 8 | font-size: 14px; 9 | } 10 | 11 | :not(:defined) { 12 | display: none !important; 13 | } 14 | 15 | body { 16 | overflow-x: hidden; 17 | } 18 | 19 | [hidden] { 20 | display: none !important; 21 | } 22 | 23 | a:focus-visible, 24 | details summary:focus-visible { 25 | outline: 0; 26 | border-radius: var(--bs-border-radius); 27 | box-shadow: var(--focus-ring); 28 | transition: box-shadow 0.15s ease-in-out; 29 | } 30 | 31 | details > summary { 32 | margin-block-end: 0.5rem; 33 | } 34 | 35 | @supports (interpolate-size: allow-keywords) { 36 | :root { 37 | interpolate-size: allow-keywords; 38 | } 39 | 40 | details::details-content { 41 | transition: height 0.2s ease, content-visibility 0.2s allow-discrete; 42 | height: 0; 43 | overflow: clip; 44 | } 45 | 46 | details[open]::details-content { 47 | height: auto; 48 | padding-bottom: 0.5em; 49 | } 50 | } 51 | 52 | .container { 53 | max-width: 1500px; 54 | } 55 | 56 | .site-header { 57 | display: flex; 58 | flex-direction: column; 59 | align-items: center; 60 | row-gap: 0.5rem; 61 | margin: 3rem 0.5rem; 62 | text-align: center; 63 | } 64 | 65 | .page-title { 66 | font-weight: 300; 67 | margin-bottom: 0; 68 | } 69 | 70 | .form-control:out-of-range { 71 | border-color: var(--bs-form-invalid-border-color); 72 | color: var(--bs-form-invalid-color); 73 | } 74 | 75 | .form-control:out-of-range:focus { 76 | box-shadow: var(--focus-ring-error); 77 | } 78 | 79 | /* Canvas */ 80 | .canvas { 81 | display: block; 82 | max-width: 100%; 83 | } 84 | 85 | /* Inputs */ 86 | input[type="color"]::-webkit-color-swatch { 87 | border: 0; 88 | } 89 | 90 | [data-section="basic-settings"] input[type="color"], 91 | .settings-button, 92 | .duplicate-button { 93 | min-width: 30px; 94 | width: 30px; 95 | height: 30px; 96 | margin: 0 2px; 97 | padding: 0; 98 | line-height: 1; 99 | cursor: pointer; 100 | } 101 | 102 | .textboxes-container .meme-text { 103 | min-width: 0; 104 | min-height: calc(1.5em + 0.75rem + 2px); 105 | max-height: 60px; 106 | margin: 0.5rem; 107 | overflow-x: hidden; /* Fix for Firefox bug https://bugzilla.mozilla.org/show_bug.cgi?id=33654 */ 108 | scrollbar-width: thin; 109 | field-sizing: content; 110 | } 111 | 112 | #maxImageDimensionsForm select:disabled { 113 | text-indent: -9999px; 114 | opacity: 0.7; 115 | } 116 | 117 | /* Buttons */ 118 | .settings-button { 119 | background-image: url(../assets/icons/gear.svg); 120 | background-repeat: no-repeat; 121 | background-position: center; 122 | background-size: 64%; 123 | } 124 | 125 | .clear-canvas-button { 126 | position: absolute; 127 | top: 0.5rem; 128 | left: 0.5rem; 129 | display: inline-flex; 130 | align-items: center; 131 | justify-content: center; 132 | padding: 0.375rem; 133 | transition: opacity 0.15s ease-in-out, visibility 0.15s ease-in-out; 134 | } 135 | 136 | @media (hover: hover) { 137 | .clear-canvas-button { 138 | opacity: 0; 139 | visibility: hidden; 140 | } 141 | 142 | .dropzone:hover .clear-canvas-button { 143 | opacity: 1; 144 | visibility: visible; 145 | } 146 | } 147 | 148 | /* Misc */ 149 | .textboxes-container { 150 | flex: 1; 151 | } 152 | 153 | .textboxes-container:empty { 154 | display: none; 155 | } 156 | 157 | .instructions { 158 | display: flex; 159 | flex-direction: column; 160 | row-gap: 1rem; 161 | justify-content: center; 162 | align-items: center; 163 | min-height: 200px; 164 | height: 100%; 165 | padding: 1rem; 166 | text-align: center; 167 | text-wrap: balance; 168 | background-color: var(--bs-tertiary-bg); 169 | color: var(--bs-body-color); 170 | } 171 | 172 | .instructions__image { 173 | width: 60px; 174 | height: 60px; 175 | background-size: 100%; 176 | background-repeat: no-repeat; 177 | background-image: url(../assets/icons/add-image-light.svg); 178 | } 179 | 180 | :root[data-bs-theme="dark"] .instructions__image { 181 | content: url(../assets/icons/add-image-dark.svg); 182 | } 183 | 184 | .errors-container { 185 | position: fixed; 186 | top: 0; 187 | left: 0; 188 | width: 100%; 189 | z-index: 1051; 190 | pointer-events: none; 191 | } 192 | 193 | .errors-container .alert { 194 | pointer-events: all; 195 | } 196 | 197 | button[data-button="duplicate-text-box"], 198 | button[data-button="delete-text-box"] { 199 | width: 28px; 200 | min-width: 28px; 201 | height: 28px; 202 | padding: 0; 203 | margin-inline-start: 0.5rem; 204 | background-repeat: no-repeat; 205 | background-size: 90%; 206 | background-position: center; 207 | } 208 | 209 | button[data-button="duplicate-text-box"] { 210 | background-image: url(../assets/icons/duplicate-light.svg); 211 | background-size: 70%; 212 | } 213 | 214 | :root[data-bs-theme="dark"] button[data-button="duplicate-text-box"] { 215 | background-image: url(../assets/icons/duplicate-dark.svg); 216 | } 217 | 218 | button[data-button="delete-text-box"] { 219 | background-image: url(../assets/icons/trash.svg); 220 | margin-inline-start: 0.25rem; 221 | } 222 | 223 | /* Gallery */ 224 | .gallery { 225 | display: flex; 226 | gap: 0.5rem; 227 | margin: 0; 228 | padding: 0.25rem 0; 229 | overflow-x: auto; 230 | min-height: 112px; 231 | scroll-behavior: smooth; 232 | } 233 | 234 | .gallery > button { 235 | padding: 0; 236 | cursor: pointer; 237 | } 238 | 239 | .gallery > button > img { 240 | display: block; 241 | width: 88px; 242 | height: 88px; 243 | border-radius: var(--bs-border-radius); 244 | object-fit: cover; 245 | } 246 | 247 | .gallery__no-results { 248 | display: flex; 249 | justify-content: center; 250 | align-items: center; 251 | flex: 1; 252 | text-align: center; 253 | } 254 | 255 | /* Move text actions */ 256 | .move-text-actions { 257 | position: relative; 258 | width: calc(var(--move-btn-width) * 3); 259 | height: calc(var(--move-btn-height) * 2); 260 | margin: 0 auto; 261 | } 262 | 263 | .move-text-actions > button { 264 | position: absolute; 265 | width: var(--move-btn-width); 266 | height: var(--move-btn-height); 267 | background-position: center; 268 | background-repeat: no-repeat; 269 | } 270 | 271 | /* UP */ 272 | .move-text-actions > [aria-label="Up"] { 273 | top: -1px; 274 | left: var(--move-btn-width); 275 | background-image: url(../assets/icons/chevron-up.svg); 276 | } 277 | 278 | /* DOWN */ 279 | .move-text-actions > [aria-label="Down"] { 280 | top: calc(var(--move-btn-height) + 1px); 281 | left: var(--move-btn-width); 282 | background-image: url(../assets/icons/chevron-down.svg); 283 | } 284 | 285 | /* LEFT */ 286 | .move-text-actions > [aria-label="Left"] { 287 | top: calc(var(--move-btn-height) / 2); 288 | left: -2px; 289 | background-image: url(../assets/icons/chevron-left.svg); 290 | } 291 | 292 | /* RIGHT */ 293 | .move-text-actions > [aria-label="Right"] { 294 | top: calc(var(--move-btn-height) / 2); 295 | left: calc(var(--move-btn-width) * 2 + 2px); 296 | background-image: url(../assets/icons/chevron-right.svg); 297 | 298 | } 299 | 300 | /* capture-photo */ 301 | capture-photo { 302 | position: relative; 303 | overflow: hidden; 304 | } 305 | 306 | capture-photo::part(capture-button) { 307 | display: none; 308 | } 309 | 310 | capture-photo::part(video) { 311 | width: 100%; 312 | border-radius: calc(var(--bs-border-radius) / 2); 313 | } 314 | 315 | capture-photo[loading]::part(video) { 316 | background-color: #000000; 317 | background-image: url(../assets/icons/spinner.svg); 318 | background-size: 60px; 319 | background-position: center center; 320 | background-repeat: no-repeat; 321 | } 322 | 323 | capture-photo [slot="actions"] { 324 | position: absolute; 325 | left: 0; 326 | top: 0; 327 | width: 100%; 328 | display: flex; 329 | justify-content: space-between; 330 | gap: 0.5rem; 331 | padding: 0.5rem; 332 | } 333 | 334 | capture-photo[loading] [slot="actions"] { 335 | display: none !important; 336 | } 337 | 338 | /* Modal */ 339 | 340 | modal-element { 341 | --me-width: fit-content; 342 | --me-border-radius: var(--bs-border-radius); 343 | --me-border-width: 1px; 344 | --me-border-color: var(--bs-border-color); 345 | --me-box-shadow: 0 0 1rem 0 rgba(0, 0, 0, 0.30); 346 | --me-backdrop-background: rgba(0, 0, 0, 0.7); 347 | --me-backdrop-filter: blur(2px); 348 | } 349 | 350 | #videoModal { 351 | --me-width: 600px; 352 | } 353 | 354 | #videoModal::part(footer) { 355 | padding-block: 0; 356 | } 357 | 358 | #videoModal:has(capture-photo[loading]) #capturePhotoButton { 359 | pointer-events: none; 360 | opacity: 0.7; 361 | } 362 | 363 | #downloadModal { 364 | --me-width: 800px; 365 | } 366 | 367 | modal-element::part(base):focus-visible { 368 | outline: 0; 369 | } 370 | 371 | modal-element [slot="header"] { 372 | margin: 0; 373 | color: var(--bs-body-color); 374 | } 375 | 376 | modal-element::part(close) { 377 | color: var(--bs-body-color); 378 | border-radius: var(--bs-border-radius); 379 | transition: box-shadow 0.15s ease-in-out; 380 | } 381 | 382 | modal-element::part(close):focus-visible { 383 | outline: 0; 384 | box-shadow: var(--focus-ring); 385 | } 386 | 387 | modal-element::part(body) { 388 | padding-block: 0; 389 | } 390 | 391 | modal-element [slot="footer"] { 392 | display: flex; 393 | justify-content: center; 394 | align-items: center; 395 | gap: 0.25rem; 396 | flex-wrap: wrap; 397 | text-align: center; 398 | } 399 | 400 | /* Files dropzone */ 401 | .dropzone { 402 | --dropzone-border-color: var(--bs-secondary); 403 | --dropzone-border-color-hover: var(--bs-primary); 404 | --dropzone-border-color-dragover: var(--bs-primary); 405 | --dropzone-background-color-hover: var(--bs-light); 406 | --dropzone-background-color-dragover: var(--bs-light); 407 | --dropzone-focus-box-shadow: var(--focus-ring); 408 | 409 | position: relative; 410 | height: 100%; 411 | } 412 | 413 | @media screen and (min-width: 768px) { 414 | .meme-column { 415 | border-inline-end: 1px solid var(--bs-border-color); 416 | } 417 | 418 | .dropzone { 419 | position: sticky; 420 | top: 0.25rem; 421 | } 422 | } 423 | 424 | .dropzone:not(.dropzone--accepted) { 425 | max-height: 41rem; 426 | } 427 | 428 | .dropzone--accepted { 429 | width: fit-content; 430 | height: fit-content; 431 | } 432 | 433 | .dropzone::part(dropzone) { 434 | height: 100%; 435 | padding: 0; 436 | } 437 | 438 | .dropzone[disabled]::part(dropzone) { 439 | opacity: 1; 440 | cursor: default; 441 | } 442 | 443 | /* Emoji picker */ 444 | .emoji-picker-details { 445 | margin-bottom: 0.5rem; 446 | background-color: var(--bs-body-bg); 447 | border-radius: var(--bs-border-radius); 448 | border: 1px solid var(--bs-border-color); 449 | } 450 | 451 | .emoji-picker-details > summary { 452 | padding: 0.5rem 1rem; 453 | margin-block-end: 0; 454 | background-color: var(--bs-body-bg); 455 | border-radius: var(--bs-border-radius); 456 | cursor: pointer; 457 | } 458 | 459 | .emoji-picker-details[open] > summary { 460 | border-bottom-right-radius: 0; 461 | border-bottom-left-radius: 0; 462 | } 463 | 464 | emoji-picker { 465 | --border-color: var(--bs-border-color); 466 | --background: var(--bs-bg); 467 | --input-border-color: var(--bs-border-color); 468 | --input-border-radius: var(--bs-border-radius); 469 | --input-padding: 0.375rem 0.75rem; 470 | --outline-color: #0d6efd40; 471 | --outline-size: 4px; 472 | --indicator-color: var(--bs-primary); 473 | --button-hover-background: var(--bs-gray-300); 474 | --input-font-color: var(--bs-body-color); 475 | 476 | width: 100%; 477 | height: 255px; 478 | padding: 0 0.5rem 0.5rem 0.5rem; 479 | } 480 | 481 | /* Theme toggle */ 482 | theme-toggle { 483 | position: absolute; 484 | top: 0.5rem; 485 | right: 0.5rem; 486 | } 487 | 488 | theme-toggle::part(base) { 489 | padding: 0.5rem; 490 | border-radius: var(--bs-border-radius); 491 | font-size: 1.1429rem; 492 | transition: background-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; 493 | } 494 | 495 | theme-toggle::part(base):focus-visible { 496 | outline: 0; 497 | box-shadow: var(--focus-ring); 498 | } 499 | 500 | /* Alert elements */ 501 | alert-element { 502 | --alert-border-color: var(--bs-secondary-bg); 503 | --alert-bg-color: var(--bs-body-bg); 504 | --alert-top-border-width: 0.25rem; 505 | --alert-countdown-height: 0.25rem; 506 | } 507 | 508 | alert-element::part(close) { 509 | font-size: 1.125rem; 510 | } 511 | 512 | alert-element::part(close):focus-visible { 513 | outline: 0; 514 | box-shadow: var(--focus-ring); 515 | transition: box-shadow 0.15s ease-in-out; 516 | } 517 | 518 | /* Utils */ 519 | 520 | .text-underline { 521 | text-decoration: underline; 522 | } 523 | -------------------------------------------------------------------------------- /src/js/index.js: -------------------------------------------------------------------------------- 1 | import 'emoji-picker-element'; 2 | import insertTextAtCursor from 'insert-text-at-cursor'; 3 | import { CapturePhoto } from '@georapbox/capture-photo-element/dist/capture-photo-defined.js'; 4 | import { isWebShareSupported } from '@georapbox/web-share-element/dist/is-web-share-supported.js'; 5 | import '@georapbox/theme-toggle-element/dist/theme-toggle-defined.js'; 6 | import '@georapbox/web-share-element/dist/web-share-defined.js'; 7 | import '@georapbox/modal-element/dist/modal-element-defined.js'; 8 | import '@georapbox/files-dropzone-element/dist/files-dropzone-defined.js'; 9 | import '@georapbox/alert-element/dist/alert-element-defined.js'; 10 | import 'bootstrap/dist/css/bootstrap.min.css'; 11 | import '../css/main.css'; 12 | import { ACCEPTED_MIME_TYPES } from './constants.js'; 13 | import { uid } from './utils/uid.js'; 14 | import { fileFromUrl } from './utils/file-from-url.js'; 15 | import { storage } from './utils/storage.js'; 16 | import { isSolidColorSelected } from './utils/is-solid-color-selected.js'; 17 | import { customFonts, loadCustomFont } from './custom-fonts.js'; 18 | import { Textbox } from './textbox.js'; 19 | import { Canvas } from './canvas.js'; 20 | import { toastify } from './toastify.js'; 21 | 22 | const canvas = Canvas.createInstance(document.getElementById('canvas')); 23 | const videoModal = document.getElementById('videoModal'); 24 | const downloadModal = document.getElementById('downloadModal'); 25 | const capturePhotoEl = document.querySelector('capture-photo'); 26 | const cameraSelect = document.getElementById('cameraSelect'); 27 | const capturePhotoButton = document.getElementById('capturePhotoButton'); 28 | const torchButton = document.getElementById('torchButton'); 29 | const dropzoneEl = document.querySelector('files-dropzone'); 30 | const instructionsEl = document.getElementById('instructions'); 31 | const imageUploadMethodSelect = document.getElementById('imageUploadMethodSelect'); 32 | const fileSelectBtn = document.getElementById('fileSelectBtn'); 33 | const imageUrlForm = document.getElementById('imageUrlForm'); 34 | const addTextboxBtn = document.getElementById('addTextboxBtn'); 35 | const textboxesContainer = document.getElementById('textboxesContainer'); 36 | const generateMemeBtn = document.getElementById('generateMemeBtn'); 37 | const openVideoModalBtn = document.getElementById('openVideoModalBtn'); 38 | const downloadMemeBtn = document.getElementById('downloadMemeBtn'); 39 | const downloadMemePreview = document.getElementById('downloadMemePreview'); 40 | const webShareComponent = document.querySelector('web-share'); 41 | const galleryEl = document.getElementById('gallery'); 42 | const gallerySearchEl = document.getElementById('gallerySearch'); 43 | const galleryNoResultsEl = galleryEl.querySelector('.gallery__no-results'); 44 | const solidColorForm = document.getElementById('solidColorForm'); 45 | const uploadMethodEls = document.querySelectorAll('.upload-method'); 46 | const removeConfirmationModal = document.getElementById('removeConfirmationModal'); 47 | const removeTextForm = document.getElementById('removeTextForm'); 48 | const maxImageDimensionsForm = document.getElementById('maxImageDimensionsForm'); 49 | const maxImageDimensionsSelect = maxImageDimensionsForm['maxImageDimensions']; 50 | const clearCanvasBtn = document.getElementById('clearCanvasBtn'); 51 | const maxImageDimensionsFromStorage = storage.get('maxImageDimensions'); 52 | let shouldFocusOnTextboxCreate = false; 53 | let selectedImage = null; 54 | let reqAnimFrame = null; 55 | 56 | document.addEventListener('tt-theme-change', handlethemeChange); 57 | document.addEventListener('web-share:error', handleWebShareError); 58 | document.addEventListener('capture-photo:video-play', handleCapturePhotoVideoPlay, { once: true }); 59 | document.addEventListener('capture-photo:error', handleCapturePhotoError); 60 | document.addEventListener('capture-photo:success', handleCapturePhotoSuccess); 61 | document.addEventListener('me-open', handleModalOpen); 62 | document.addEventListener('me-close', handleModalClose); 63 | document.addEventListener('emoji-click', handleEmojiPickerSelection); 64 | document.addEventListener('textbox-create', handleTextboxCreate); 65 | document.addEventListener('textbox-remove', handleTextboxDelete); 66 | fileSelectBtn.addEventListener('click', handleFileSelectClick); 67 | openVideoModalBtn.addEventListener('click', handleOpenVideoModalButtonClick); 68 | addTextboxBtn.addEventListener('click', handleAddTextboxBtnClick); 69 | generateMemeBtn.addEventListener('click', generateMeme); 70 | downloadMemeBtn.addEventListener('click', () => (downloadModal.open = false)); 71 | imageUrlForm.addEventListener('submit', handleImageUploadFromURL); 72 | dropzoneEl.addEventListener('files-dropzone-drop-accepted', handleDropFilesAccepted); 73 | textboxesContainer.addEventListener('input', handleTextboxesContainerInput); 74 | textboxesContainer.addEventListener('change', handleTextboxesContainerChange); 75 | textboxesContainer.addEventListener('click', handleTextboxesContainerClick); 76 | textboxesContainer.addEventListener('pointerdown', handleTextboxesContainerPointerdown); 77 | textboxesContainer.addEventListener('pointerup', handleTextboxesContainerPointerup); 78 | textboxesContainer.addEventListener('pointerout', handleTextboxesContainerPointerout); 79 | textboxesContainer.addEventListener('keydown', handleTextboxesContainerKeyDown); 80 | textboxesContainer.addEventListener('keyup', handleTextboxesContainerKeyUp); 81 | imageUploadMethodSelect.addEventListener('change', handleUploadMethodChange); 82 | galleryEl.addEventListener('click', handleGalleryClick); 83 | gallerySearchEl.addEventListener('input', handleGallerySearchInput); 84 | solidColorForm.addEventListener('input', handleSolidColorFormInput); 85 | removeTextForm.addEventListener('submit', handleTextRemoveFormSubmit); 86 | maxImageDimensionsForm.addEventListener('change', handleMaxImageDimensionsFormChange); 87 | clearCanvasBtn.addEventListener('click', handleClearCanvas); 88 | cameraSelect.addEventListener('change', handleCameraSelectChange); 89 | capturePhotoButton.addEventListener('click', handleCapturePhotoButtonClick); 90 | torchButton.addEventListener('click', handleTorchButtonClick); 91 | window.addEventListener('beforeunload', handleBeforeunload); 92 | 93 | galleryEl.querySelectorAll('button > img')?.forEach(image => { 94 | image.setAttribute('title', image.getAttribute('alt')); 95 | }); 96 | 97 | Textbox.create(); 98 | 99 | dropzoneEl.accept = ACCEPTED_MIME_TYPES; 100 | 101 | renderAcceptedImageFormats(ACCEPTED_MIME_TYPES, instructionsEl); 102 | 103 | customFonts.forEach(({ name, path, style, weight }) => { 104 | loadCustomFont(name, path, { style, weight }); 105 | }); 106 | 107 | if (maxImageDimensionsFromStorage) { 108 | maxImageDimensionsSelect.value = maxImageDimensionsFromStorage; 109 | } 110 | 111 | maxImageDimensionsSelect.disabled = false; 112 | 113 | function renderAcceptedImageFormats(acceptedMimeTypes, rootEl) { 114 | if (!rootEl) { 115 | return; 116 | } 117 | 118 | const extensions = acceptedMimeTypes.map(mimeType => mimeType.split('/')[1]); 119 | const str = `Supported image formats: ${extensions.join(', ')}`; 120 | const div = document.createElement('div'); 121 | const small = document.createElement('small'); 122 | 123 | small.textContent = str; 124 | div.appendChild(small); 125 | rootEl.appendChild(small); 126 | } 127 | 128 | async function generateMeme() { 129 | const dataUrl = canvas.toDataURL('image/png'); 130 | const filename = `${uid('meme')}.png`; 131 | 132 | // Prepare download link 133 | const downloadLink = dataUrl.replace('image/png', 'image/octet-stream'); 134 | downloadMemeBtn.download = filename; 135 | downloadMemeBtn.href = downloadLink; 136 | downloadMemePreview.width = canvas.getDimensions().width; 137 | downloadMemePreview.height = canvas.getDimensions().height; 138 | downloadMemePreview.src = downloadLink; 139 | 140 | // Prepare for sharing file 141 | if (isWebShareSupported()) { 142 | try { 143 | const file = await fileFromUrl({ 144 | url: dataUrl, 145 | filename, 146 | mimeType: 'image/png' 147 | }).catch(err => { 148 | toastify(`Error preparing meme for sharing
${err.message}`, { 149 | trustDangerousInnerHTML: true, 150 | variant: 'danger' 151 | }); 152 | }); 153 | 154 | if (file && isWebShareSupported({ files: [file] })) { 155 | webShareComponent.shareFiles = [file]; 156 | webShareComponent.hidden = false; 157 | } 158 | } catch (error) { 159 | console.error(error); 160 | } 161 | } 162 | 163 | window.requestAnimationFrame(() => { 164 | downloadModal.open = true; 165 | }); 166 | } 167 | 168 | function setImageMaxDimensions(image) { 169 | const maxImageDimensionsSelect = maxImageDimensionsForm['maxImageDimensions']; 170 | const [maxWidthValue, maxHeightValue] = maxImageDimensionsSelect.value.split('x'); 171 | const MAX_WIDTH = Number(maxWidthValue) || 800; 172 | const MAX_HEIGHT = Number(maxHeightValue) || 600; 173 | let width = image.width; 174 | let height = image.height; 175 | 176 | if (width > height) { 177 | if (width > MAX_WIDTH) { 178 | height *= MAX_WIDTH / width; 179 | width = MAX_WIDTH; 180 | } 181 | } else { 182 | if (height > MAX_HEIGHT) { 183 | width *= MAX_HEIGHT / height; 184 | height = MAX_HEIGHT; 185 | } 186 | } 187 | 188 | canvas.setDimensions({ width, height }); 189 | } 190 | 191 | function afterImageSelect() { 192 | canvas.draw(selectedImage, Textbox.getAll()).show(); 193 | dropzoneEl.classList.add('dropzone--accepted'); 194 | dropzoneEl.disabled = true; 195 | generateMemeBtn.disabled = false; 196 | instructionsEl.hidden = true; 197 | clearCanvasBtn.hidden = false; 198 | } 199 | 200 | function handleImageLoad(evt) { 201 | selectedImage = evt.target; 202 | setImageMaxDimensions(selectedImage); 203 | afterImageSelect(); 204 | } 205 | 206 | function handleSolidColorFormInput(evt) { 207 | const DEFAULT_WIDTH = 800; 208 | const DEFAULT_HEIGHT = 600; 209 | 210 | if (evt.target === solidColorForm['canvasColor']) { 211 | selectedImage = evt.target.value; 212 | } 213 | 214 | if (isSolidColorSelected(selectedImage)) { 215 | canvas.setDimensions({ 216 | width: Number(solidColorForm['canvasWidth'].value) || DEFAULT_WIDTH, 217 | height: Number(solidColorForm['canvasHeight'].value) || DEFAULT_HEIGHT 218 | }); 219 | 220 | afterImageSelect(); 221 | } 222 | } 223 | 224 | function handleFileSelect(file) { 225 | if (!file) { 226 | return; 227 | } 228 | 229 | const image = new Image(); 230 | const reader = new FileReader(); 231 | 232 | reader.addEventListener('load', function (evt) { 233 | const data = evt.target.result; 234 | image.addEventListener('load', handleImageLoad); 235 | image.src = data; 236 | }); 237 | 238 | reader.readAsDataURL(file); 239 | } 240 | 241 | function handleOpenVideoModalButtonClick() { 242 | videoModal.open = true; 243 | } 244 | 245 | function handleTextPropChange(element, textboxId, prop) { 246 | const textboxData = Textbox.getById(textboxId).getData(); 247 | 248 | switch (element.type) { 249 | case 'checkbox': 250 | textboxData[prop] = element.checked; 251 | break; 252 | case 'number': 253 | textboxData[prop] = Number(element.value); 254 | break; 255 | default: 256 | textboxData[prop] = element.value; 257 | } 258 | 259 | canvas.draw(selectedImage, Textbox.getAll()); 260 | } 261 | 262 | function handleAddTextboxBtnClick() { 263 | return Textbox.create(); 264 | } 265 | 266 | async function handleImageUploadFromURL(evt) { 267 | evt.preventDefault(); 268 | 269 | const form = evt.target; 270 | const submitButton = form.querySelector('button[type="submit"]'); 271 | const imageUrl = form['imageUrl'].value; 272 | const errorMessage = `Error loading image
Failed to fetch image from ${imageUrl || ''}.`; 273 | 274 | if (!imageUrl.trim()) { 275 | return; 276 | } 277 | 278 | submitButton.disabled = true; 279 | submitButton.querySelector('.spinner').hidden = false; 280 | submitButton.querySelector('.label').hidden = true; 281 | 282 | try { 283 | const file = await fileFromUrl({ 284 | url: imageUrl 285 | }).catch(() => { 286 | toastify(errorMessage, { trustDangerousInnerHTML: true, variant: 'danger' }); 287 | }); 288 | 289 | if (file) { 290 | handleFileSelect(file); 291 | } 292 | } catch { 293 | toastify(errorMessage, { trustDangerousInnerHTML: true, variant: 'danger' }); 294 | } finally { 295 | submitButton.disabled = false; 296 | submitButton.querySelector('.spinner').hidden = true; 297 | submitButton.querySelector('.label').hidden = false; 298 | } 299 | } 300 | 301 | function moveTextUsingArrowbuttons(textboxId, direction) { 302 | return function () { 303 | const textboxEl = document.getElementById(textboxId); 304 | const offsetYInput = textboxEl.querySelector('[data-input="offsetY"]'); 305 | const offsetXInput = textboxEl.querySelector('[data-input="offsetX"]'); 306 | const textbox = Textbox.getById(textboxId); 307 | 308 | if (!textbox) { 309 | return; 310 | } 311 | 312 | const textboxData = textbox.getData(); 313 | 314 | direction = direction.toLowerCase(); 315 | 316 | switch (direction) { 317 | case 'up': 318 | textboxData.offsetY -= 1; 319 | offsetYInput.value = textboxData.offsetY; 320 | break; 321 | case 'down': 322 | textboxData.offsetY += 1; 323 | offsetYInput.value = textboxData.offsetY; 324 | break; 325 | case 'left': 326 | textboxData.offsetX -= 1; 327 | offsetXInput.value = textboxData.offsetX; 328 | break; 329 | case 'right': 330 | textboxData.offsetX += 1; 331 | offsetXInput.value = textboxData.offsetX; 332 | break; 333 | } 334 | 335 | canvas.draw(selectedImage, Textbox.getAll()); 336 | 337 | reqAnimFrame = requestAnimationFrame(moveTextUsingArrowbuttons(textboxId, direction)); 338 | }; 339 | } 340 | 341 | function handleUploadMethodChange(evt) { 342 | uploadMethodEls.forEach(el => (el.hidden = el.id !== evt.target.value)); 343 | maxImageDimensionsForm.hidden = evt.target.value === 'solidColorForm'; 344 | } 345 | 346 | function handleFileSelectClick() { 347 | if (typeof dropzoneEl.openFileDialog === 'function') { 348 | // NOTE: Always enable dropzone before opening dialog 349 | // in case it was previously disabled after image selection. 350 | dropzoneEl.disabled = false; 351 | dropzoneEl.openFileDialog(); 352 | } 353 | } 354 | 355 | function handleDropFilesAccepted(evt) { 356 | const [file] = evt.detail.acceptedFiles; 357 | 358 | if (file) { 359 | handleFileSelect(file); 360 | } 361 | } 362 | 363 | function handleTextboxesContainerInput(evt) { 364 | const inputMap = { 365 | text: 'text', 366 | fillColor: 'fillColor', 367 | strokeColor: 'strokeColor', 368 | font: 'font', 369 | fontSize: 'fontSize', 370 | fontWeight: 'fontWeight', 371 | textAlign: 'textAlign', 372 | shadowBlur: 'shadowBlur', 373 | offsetY: 'offsetY', 374 | offsetX: 'offsetX', 375 | rotate: 'rotate', 376 | strokeWidth: 'strokeWidth', 377 | textBackgroundEnabled: 'textBackgroundEnabled', 378 | textBackgroundColor: 'textBackgroundColor' 379 | }; 380 | const element = evt.target; 381 | const prop = inputMap[element.dataset.input]; 382 | 383 | if (!prop) { 384 | return; 385 | } 386 | 387 | const textboxId = element.closest('[data-section="textbox"]').id; 388 | handleTextPropChange(element, textboxId, prop); 389 | } 390 | 391 | function handleTextboxesContainerChange(evt) { 392 | const element = evt.target; 393 | const textboxId = element.closest('[data-section="textbox"]').id; 394 | let prop; 395 | 396 | if (element.matches('[data-input="allCaps"]')) { 397 | prop = 'allCaps'; 398 | } 399 | 400 | if (prop) { 401 | handleTextPropChange(element, textboxId, prop); 402 | } 403 | } 404 | 405 | function handleTextboxesContainerClick(evt) { 406 | const element = evt.target; 407 | 408 | if (element.matches('[data-button="settings"]')) { 409 | const textboxEl = element.closest('[data-section="textbox"]'); 410 | const textboxSettingsEl = textboxEl?.querySelector('[data-section="advanced-settings"]'); 411 | 412 | if (textboxSettingsEl) { 413 | textboxSettingsEl.hidden = !textboxSettingsEl.hidden; 414 | } 415 | } 416 | 417 | if (element.matches('[data-button="duplicate-text-box"')) { 418 | const currentTextboxEl = element.closest('[data-section="textbox"]'); 419 | const currentTextboxData = Textbox.getById(currentTextboxEl.id); 420 | Textbox.create({ ...currentTextboxData.data }); 421 | } 422 | 423 | if (element.matches('[data-button="delete-text-box"]')) { 424 | const textboxId = element.closest('[data-section="textbox"]').id; 425 | const textboxToDelete = Textbox.getById(textboxId); 426 | 427 | if (textboxToDelete && textboxToDelete.data.text.trim()) { 428 | const textboxIdInput = removeTextForm['textbox-id']; 429 | 430 | if (textboxIdInput) { 431 | textboxIdInput.value = textboxId; 432 | removeConfirmationModal.open = true; 433 | } 434 | } else { 435 | Textbox.remove(textboxId); 436 | } 437 | } 438 | } 439 | 440 | function handleTextRemoveFormSubmit(evt) { 441 | evt.preventDefault(); 442 | const textboxId = evt.target['textbox-id'].value; 443 | 444 | if (textboxId) { 445 | Textbox.remove(textboxId); 446 | removeConfirmationModal.open = false; 447 | } 448 | } 449 | 450 | function handleTextboxesContainerPointerdown(evt) { 451 | const element = evt.target; 452 | const textboxEl = element.closest('[data-section="textbox"]'); 453 | 454 | if (!textboxEl) { 455 | return; 456 | } 457 | 458 | if (element.matches('[data-action="move-text"]')) { 459 | reqAnimFrame = requestAnimationFrame(moveTextUsingArrowbuttons(textboxEl.id, element.getAttribute('aria-label'))); 460 | } 461 | } 462 | 463 | function handleTextboxesContainerPointerup(evt) { 464 | const element = evt.target; 465 | 466 | if (element.matches('[data-action="move-text"]')) { 467 | cancelAnimationFrame && cancelAnimationFrame(reqAnimFrame); 468 | reqAnimFrame = null; 469 | } 470 | } 471 | 472 | function handleTextboxesContainerPointerout(evt) { 473 | const element = evt.target; 474 | 475 | if (element.matches('[data-action="move-text"]')) { 476 | cancelAnimationFrame && cancelAnimationFrame(reqAnimFrame); 477 | reqAnimFrame = null; 478 | } 479 | } 480 | 481 | function handleTextboxesContainerKeyDown(evt) { 482 | const element = evt.target; 483 | const textboxEl = element.closest('[data-section="textbox"]'); 484 | 485 | if (element.matches('[data-action="move-text"]')) { 486 | if (evt.key === ' ' || evt.key === 'Enter') { 487 | reqAnimFrame && cancelAnimationFrame(reqAnimFrame); 488 | reqAnimFrame = requestAnimationFrame(moveTextUsingArrowbuttons(textboxEl.id, element.getAttribute('aria-label'))); 489 | } 490 | } 491 | } 492 | 493 | function handleTextboxesContainerKeyUp(evt) { 494 | const element = evt.target; 495 | 496 | if (element.matches('[data-action="move-text"]')) { 497 | if (evt.key === ' ' || evt.key === 'Enter') { 498 | reqAnimFrame && cancelAnimationFrame(reqAnimFrame); 499 | reqAnimFrame = null; 500 | } 501 | } 502 | } 503 | 504 | async function handleGalleryClick(evt) { 505 | const button = evt.target.closest('button'); 506 | 507 | if (!button) { 508 | return; 509 | } 510 | 511 | const img = button.querySelector('img'); 512 | const errorMessage = `Error loading image
Failed to load image: "${img.alt || ''}".`; 513 | 514 | try { 515 | const file = await fileFromUrl({ 516 | url: img.src 517 | }).catch(() => { 518 | toastify(errorMessage, { trustDangerousInnerHTML: true, variant: 'danger' }); 519 | }); 520 | 521 | if (file) { 522 | handleFileSelect(file); 523 | } 524 | } catch { 525 | toastify(errorMessage, { trustDangerousInnerHTML: true, variant: 'danger' }); 526 | } 527 | } 528 | 529 | function handleGallerySearchInput(evt) { 530 | const query = evt.target.value.toLowerCase().trim(); 531 | const galleryItems = galleryEl.querySelectorAll('button'); 532 | 533 | galleryItems.forEach(item => { 534 | const alt = (item.querySelector('img').getAttribute('alt') || '').toLowerCase(); 535 | item.hidden = !alt.includes(query); 536 | }); 537 | 538 | galleryNoResultsEl.hidden = !!galleryEl.querySelector('button:not([hidden])'); 539 | } 540 | 541 | function handleWebShareError() { 542 | downloadModal.open = false; 543 | toastify('Error sharing
There was an error while trying to share your meme.', { 544 | trustDangerousInnerHTML: true, 545 | variant: 'danger' 546 | }); 547 | } 548 | 549 | function handleCapturePhotoError(evt) { 550 | const error = evt.detail.error; 551 | let errorMessage = 'An error occurred while trying to capture photo.'; 552 | 553 | if (error instanceof Error && (error.name === 'NotAllowedError' || error.name === 'NotFoundError')) { 554 | errorMessage += ' Make sure you have a camera connected and you have granted the appropriate permissions.'; 555 | } 556 | 557 | toastify(`Error capturing photo
${errorMessage}`, { 558 | trustDangerousInnerHTML: true, 559 | variant: 'danger' 560 | }); 561 | 562 | videoModal.open = false; 563 | console.error(error); 564 | } 565 | 566 | function handleCapturePhotoSuccess(evt) { 567 | videoModal.open = false; 568 | const image = new Image(); 569 | image.addEventListener('load', handleImageLoad); 570 | image.src = evt.detail.dataURI; 571 | } 572 | 573 | function handleModalOpen(evt) { 574 | if (evt.target.id === 'videoModal') { 575 | if (capturePhotoEl && typeof capturePhotoEl.startVideoStream === 'function') { 576 | capturePhotoEl.startVideoStream(); 577 | } 578 | } 579 | } 580 | 581 | function handleModalClose(evt) { 582 | if (evt.target.id === 'videoModal') { 583 | if (capturePhotoEl && typeof capturePhotoEl.stopVideoStream === 'function') { 584 | capturePhotoEl.stopVideoStream(); 585 | } 586 | } 587 | 588 | if (evt.target.id === 'removeConfirmationModal') { 589 | removeTextForm.reset(); 590 | } 591 | } 592 | 593 | function handleEmojiPickerSelection(evt) { 594 | const textboxEl = evt.target.closest('[data-section="textbox"]'); 595 | 596 | if (textboxEl) { 597 | const input = textboxEl.querySelector('[data-input="text"]'); 598 | const emoji = evt.detail.unicode; 599 | 600 | if (input) { 601 | insertTextAtCursor(input, emoji); 602 | } 603 | } 604 | } 605 | 606 | function handleMaxImageDimensionsFormChange(evt) { 607 | if (evt.target.matches('[name="maxImageDimensions"]')) { 608 | storage.set('maxImageDimensions', evt.target.value); 609 | } 610 | 611 | if (!selectedImage || isSolidColorSelected(selectedImage)) { 612 | return; 613 | } 614 | 615 | setImageMaxDimensions(selectedImage); 616 | canvas.draw(selectedImage, Textbox.getAll()); 617 | } 618 | 619 | function handleTextboxCreate(evt) { 620 | const textbox = evt.detail.textbox; 621 | const textboxEl = Textbox.createElement(textbox, shouldFocusOnTextboxCreate); 622 | 623 | shouldFocusOnTextboxCreate = true; 624 | textboxesContainer.appendChild(textboxEl); 625 | 626 | if (textbox.getData().text) { 627 | canvas.draw(selectedImage, Textbox.getAll()); 628 | } 629 | } 630 | 631 | function handleTextboxDelete(evt) { 632 | const textboxEl = document.getElementById(evt.detail.id); 633 | textboxEl && textboxEl.remove(); 634 | 635 | textboxesContainer.querySelectorAll('[data-section="textbox"]').forEach((el, idx) => { 636 | el.querySelector('[data-input="text"]').setAttribute('placeholder', `Text #${idx + 1}`); 637 | }); 638 | 639 | canvas.draw(selectedImage, Textbox.getAll()); 640 | } 641 | 642 | function handleClearCanvas(evt) { 643 | if (!selectedImage) { 644 | return; 645 | } 646 | 647 | evt.stopPropagation(); 648 | selectedImage = null; 649 | dropzoneEl.classList.remove('dropzone--accepted'); 650 | generateMemeBtn.disabled = true; 651 | instructionsEl.hidden = false; 652 | clearCanvasBtn.hidden = true; 653 | dropzoneEl.disabled = false; 654 | canvas.clear().hide(); 655 | } 656 | 657 | function toggleTorchButtonStatus(options = {}) { 658 | const defaults = { 659 | el: document.getElementById('torchButton'), 660 | isTorchOn: false 661 | }; 662 | const { el, isTorchOn } = { ...defaults, ...options }; 663 | const iconPaths = el.querySelectorAll('svg path'); 664 | 665 | if (iconPaths.length !== 2) { 666 | return; 667 | } 668 | 669 | iconPaths[0].style.display = isTorchOn ? 'none' : 'block'; 670 | iconPaths[1].style.display = isTorchOn ? 'block' : 'none'; 671 | el.setAttribute('aria-label', `Turn ${isTorchOn ? 'off' : 'on'} flash`); 672 | } 673 | 674 | function handleTorchButtonClick(evt) { 675 | if (capturePhotoEl === null) { 676 | return; 677 | } 678 | 679 | capturePhotoEl.torch = !capturePhotoEl.torch; 680 | 681 | toggleTorchButtonStatus({ 682 | el: evt.currentTarget, 683 | isTorchOn: capturePhotoEl.hasAttribute('torch') 684 | }); 685 | } 686 | 687 | async function handleCapturePhotoVideoPlay(evt) { 688 | const trackCapabilities = evt.target.getTrackCapabilities(); 689 | 690 | if (trackCapabilities?.torch) { 691 | torchButton?.removeAttribute('hidden'); 692 | 693 | if (capturePhotoEl?.hasAttribute('torch')) { 694 | toggleTorchButtonStatus({ el: torchButton, isTorchOn: true }); 695 | } 696 | } 697 | 698 | const videoInputDevices = await CapturePhoto.getVideoInputDevices(); 699 | 700 | videoInputDevices.forEach((device, index) => { 701 | const option = document.createElement('option'); 702 | option.value = device.deviceId; 703 | option.textContent = device.label || `Camera ${index + 1}`; 704 | cameraSelect.appendChild(option); 705 | }); 706 | 707 | if (videoInputDevices.length > 1) { 708 | cameraSelect?.removeAttribute('hidden'); 709 | } 710 | } 711 | 712 | function handleCameraSelectChange(evt) { 713 | if ( 714 | capturePhotoEl === null || 715 | typeof capturePhotoEl.restartVideoStream !== 'function' || 716 | capturePhotoEl.hasAttribute('loading') 717 | ) { 718 | return; 719 | } 720 | 721 | const videoDeviceId = evt.target.value || undefined; 722 | capturePhotoEl.restartVideoStream(videoDeviceId); 723 | } 724 | 725 | function handleCapturePhotoButtonClick() { 726 | if ( 727 | capturePhotoEl === null || 728 | typeof capturePhotoEl.capture !== 'function' || 729 | capturePhotoEl.hasAttribute('loading') 730 | ) { 731 | return; 732 | } 733 | 734 | capturePhotoEl.capture(); 735 | } 736 | 737 | function handlethemeChange(evt) { 738 | const theme = evt.detail.theme; 739 | const deviceTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; 740 | document.documentElement.setAttribute('data-bs-theme', theme === 'system' ? deviceTheme : theme); 741 | } 742 | 743 | function handleBeforeunload(evt) { 744 | if (changesHaveOccurred()) { 745 | evt.preventDefault(); 746 | evt.returnValue = true; // Included for legacy support, e.g. Chrome/Edge < 119 747 | } 748 | } 749 | 750 | function changesHaveOccurred() { 751 | let hasChanges = selectedImage !== null; 752 | 753 | for (const v of Textbox.getAll().values()) { 754 | if (!Textbox.hasDefaultValues(v.data)) { 755 | hasChanges = true; 756 | break; 757 | } 758 | } 759 | 760 | return hasChanges; 761 | } 762 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Meme Generator 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 23 | 24 | 25 | 26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | 35 | 45 | 46 |
47 |
48 |
49 | 50 | 51 | 52 | 57 | 58 |
59 |
60 |
61 |

Drag & Drop an image here or click to select an image from your device.

62 |

All uploaded images are processed locally in your browser. No data is sent to any server.

63 |
64 |
65 |
66 |
67 | 68 |
69 |
70 | Image selection options 71 | 72 |
73 |
74 | 75 | 76 | 83 |
84 | 85 | 193 | 194 | 198 | 199 | 213 | 214 | 218 | 219 | 234 | 235 |
236 | 244 | 251 |
252 |
253 |
254 | 255 |
256 | Text options 257 | 258 |
259 | 260 |
261 | 267 |
268 |
269 | 270 | 273 |
274 |
275 | 276 |
277 |
278 | Licensed under The MIT License (MIT) 279 |
280 |
281 |
282 | 283 | 284 |

Capture image

285 | 286 |
287 | 290 | 291 | 297 |
298 |
299 |
300 | 304 |
305 |
306 | 307 | 308 |

Download Meme

309 | 310 | meme preview 311 | 312 |
313 | 314 | 315 | Download 316 | 317 | 318 | 324 |
325 |
326 | 327 | 328 |

Remove text

329 |

Are you sure you want to remove this text box?

330 | 337 |
338 |
339 | 340 | 341 | --------------------------------------------------------------------------------