├── .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 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/chevron-up.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/chevron-left.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/chevron-down.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/chevron-right.svg:
--------------------------------------------------------------------------------
1 |
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 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/duplicate-light.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/camera.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/photo.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/save.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
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 |
16 |
--------------------------------------------------------------------------------
/src/assets/icons/add-image-light.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 | 
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 |
119 |
120 |
121 |
122 | Emoji picker
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
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 |