├── src
├── ComicBubble.js
├── fonts
│ ├── slkscr.woff
│ └── slkscr.woff2
├── index.js
├── components
│ ├── Link.js
│ ├── GitHub.js
│ ├── Footer.js
│ └── Icons.js
├── index.html
├── App.js
└── index.css
├── README.md
├── .gitignore
└── package.json
/src/ComicBubble.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/fonts/slkscr.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/midudev/komic.app/master/src/fonts/slkscr.woff
--------------------------------------------------------------------------------
/src/fonts/slkscr.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/midudev/komic.app/master/src/fonts/slkscr.woff2
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { h, render } from 'preact'
2 | import App from './App'
3 |
4 | render(
5 | , document.body
6 | )
--------------------------------------------------------------------------------
/src/components/Link.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact'
2 |
3 | export default function ({children, href, external}) {
4 | const rel = external ? 'noopener nofollow' : undefined
5 | return {children}
6 | }
--------------------------------------------------------------------------------
/src/components/GitHub.js:
--------------------------------------------------------------------------------
1 | import {h} from 'preact'
2 | import Link from './Link'
3 |
4 | export default () => (
5 |
6 |
7 |
8 |
9 |
10 | )
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # komik.app 🗯️
2 | Comic bubbles without hassle
3 |
4 | ## TODO
5 | - [x] Add Download functionality
6 | - [ ] Add Copy to Clipboard button.
7 | - [ ] Let user enable/disable text-transform uppercase.
8 | - [ ] Let user change spike direction.
9 | - [ ] Let user change size of bubble.
10 | - [ ] Add animation of writing when user enters
11 | - [ ] Be able to customize color of bubble and border.
12 | - [ ] Select between different fonts.
13 |
--------------------------------------------------------------------------------
/src/components/Footer.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact'
2 | import Link from './Link'
3 |
4 | // · based on the mythical wigflip tool
5 |
6 | export default () => (
7 |
10 | )
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Create your own Comic Bubble
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/components/Icons.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact'
2 |
3 | export const FloopyIcon = () => (
4 |
5 | )
6 |
7 | export const SettingsIcon = () => (
8 |
9 | )
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | firebase-debug.log*
8 |
9 | # Firebase cache
10 | .firebase/
11 |
12 | # Firebase config
13 |
14 | # Uncomment this if you'd like others to create their own Firebase project.
15 | # For a team working on the same Firebase project(s), it is recommended to leave
16 | # it commented so all members can deploy to the same project(s) in .firebaserc.
17 | # .firebaserc
18 |
19 | # Runtime data
20 | pids
21 | *.pid
22 | *.seed
23 | *.pid.lock
24 |
25 | # Directory for instrumented libs generated by jscoverage/JSCover
26 | lib-cov
27 |
28 | # Coverage directory used by tools like istanbul
29 | coverage
30 |
31 | # nyc test coverage
32 | .nyc_output
33 |
34 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
35 | .grunt
36 |
37 | # Bower dependency directory (https://bower.io/)
38 | bower_components
39 |
40 | # node-waf configuration
41 | .lock-wscript
42 |
43 | # Compiled binary addons (http://nodejs.org/api/addons.html)
44 | build/Release
45 |
46 | # Dependency directories
47 | node_modules/
48 |
49 | # Optional npm cache directory
50 | .npm
51 |
52 | # Optional eslint cache
53 | .eslintcache
54 |
55 | # Optional REPL history
56 | .node_repl_history
57 |
58 | # Output of 'npm pack'
59 | *.tgz
60 |
61 | # Yarn Integrity file
62 | .yarn-integrity
63 |
64 | # dotenv environment variables file
65 | .env
66 |
67 | # Parcel stuff
68 | .cache
69 | dist
70 |
71 | # Firebase stuff
72 | .firebase
73 |
74 | package-lock.json
75 |
76 | .DS_Store
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "comic-bubble",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "dev": "parcel src/index.html",
8 | "build": "parcel build",
9 | "test": "echo \"Error: no test specified\" && exit 1"
10 | },
11 | "dependencies": {
12 | "downloadjs": "1.4.7",
13 | "file-drop-element": "0.2.0",
14 | "html-to-image": "0.1.1",
15 | "preact": "10.3.1"
16 | },
17 | "author": "Miguel Ángel Durán García - @midudev",
18 | "license": "ISC",
19 | "devDependencies": {
20 | "babel-eslint": "10.0.3",
21 | "eslint": "6.8.0",
22 | "eslint-config-prettier": "6.10.0",
23 | "eslint-config-standard": "14.1.0",
24 | "eslint-config-standard-react": "9.2.0",
25 | "eslint-plugin-import": "2.20.1",
26 | "eslint-plugin-node": "11.0.0",
27 | "eslint-plugin-prettier": "3.1.2",
28 | "eslint-plugin-promise": "4.2.1",
29 | "eslint-plugin-react": "7.18.3",
30 | "eslint-plugin-standard": "4.0.1",
31 | "parcel-bundler": "1.12.4"
32 | },
33 | "eslintConfig": {
34 | "extends": [
35 | "standard",
36 | "standard-react",
37 | "prettier"
38 | ],
39 | "parser": "babel-eslint",
40 | "env": {
41 | "browser": true,
42 | "node": true,
43 | "es6": true
44 | },
45 | "plugins": [
46 | "react",
47 | "prettier"
48 | ],
49 | "settings": {
50 | "react": {
51 | "pragma": "h",
52 | "version": "preact"
53 | }
54 | },
55 | "parserOptions": {
56 | "ecmaVersion": 2018,
57 | "ecmaFeatures": {
58 | "jsx": true
59 | }
60 | },
61 | "rules": {
62 | "no-console": 1,
63 | "no-empty": 0,
64 | "semi": [
65 | "error",
66 | "never"
67 | ],
68 | "keyword-spacing": 2,
69 | "react/prop-types": 0,
70 | "react/no-string-refs": 2,
71 | "react/no-find-dom-node": 2,
72 | "react/no-is-mounted": 2,
73 | "react/jsx-no-comment-textnodes": 2,
74 | "react/jsx-curly-spacing": 2,
75 | "react/jsx-no-undef": 2,
76 | "react/jsx-uses-react": 2,
77 | "react/jsx-uses-vars": 2
78 | }
79 | },
80 | "prettier": {
81 | "trailingComma": "es5",
82 | "tabWidth": 2,
83 | "semi": false,
84 | "useTabs": true,
85 | "singleQuote": true,
86 | "endOfLine": "lf"
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact'
2 | import {useRef, useState, useEffect} from 'preact/hooks'
3 |
4 | import 'file-drop-element'
5 | import {FloopyIcon} from './components/Icons'
6 | import Footer from './components/Footer'
7 | import GitHub from './components/GitHub'
8 |
9 | export default function App () {
10 | const dropTargetRef = useRef()
11 | const imgRef = useRef()
12 |
13 | const [withImage, setWithImage] = useState(false)
14 |
15 | function downloadImage () {
16 | const selector = withImage ? '#result > div' : '#result-bubble'
17 | const result = document.querySelector(selector)
18 |
19 | Promise.all([
20 | import('html-to-image'),
21 | import('downloadjs')
22 | ]).then(([{toPng}, download]) => {
23 | toPng(result).then(dataUrl => download(dataUrl, 'komic.png'))
24 | })
25 | }
26 |
27 | useEffect(function () {
28 | document.querySelector('[autofocus]').focus()
29 |
30 | dropTargetRef.current.addEventListener('filedrop', (e) => {
31 | imgRef.current.removeAttribute('hidden')
32 | imgRef.current.src = URL.createObjectURL(e.files[0])
33 | setWithImage(true)
34 | })
35 | }, [])
36 |
37 | useEffect(function () {
38 | const dragItem = document.querySelector("#result-bubble")
39 | const container = document.querySelector("#result")
40 |
41 | let active = false
42 | let currentX
43 | let currentY
44 | let initialX
45 | let initialY
46 | let xOffset = 0
47 | let yOffset = 0
48 |
49 | function dragStart(event) {
50 | const from = event.type === 'touchstart'
51 | ? event.touches[0]
52 | : event
53 |
54 | initialX = from.clientX - xOffset
55 | initialY = from.clientY - yOffset
56 |
57 | if (event.target === dragItem) {
58 | active = true
59 | }
60 | }
61 |
62 | function dragEnd() {
63 | initialX = currentX
64 | initialY = currentY
65 |
66 | active = false
67 | }
68 |
69 | function drag(event) {
70 | if (active) {
71 | event.preventDefault()
72 |
73 | const from = event.type === 'touchmove'
74 | ? event.touches[0]
75 | : event
76 |
77 | currentX = from.clientX - initialX
78 | currentY = from.clientY - initialY
79 |
80 | xOffset = currentX
81 | yOffset = currentY
82 |
83 | setTranslate(currentX, currentY, dragItem)
84 | }
85 | }
86 |
87 | function setTranslate(xPos, yPos, el) {
88 | el.style.transform = "translate3d(" + xPos + "px, " + yPos + "px, 0)"
89 | }
90 |
91 | container.addEventListener("touchstart", dragStart, false)
92 | container.addEventListener("touchend", dragEnd, false)
93 | container.addEventListener("touchmove", drag, false)
94 |
95 | container.addEventListener("mousedown", dragStart, false)
96 | container.addEventListener("mouseup", dragEnd, false)
97 | container.addEventListener("mousemove", drag, false)
98 |
99 | return () => {
100 | container.removeEventListener("touchstart", dragStart, false)
101 | container.removeEventListener("touchend", dragEnd, false)
102 | container.removeEventListener("touchmove", drag, false)
103 |
104 | container.removeEventListener("mousedown", dragStart, false)
105 | container.removeEventListener("mouseup", dragEnd, false)
106 | container.removeEventListener("mousemove", drag, false)
107 | }
108 | })
109 |
110 | return (
111 |
112 |
113 |
114 |
![]()
115 |
116 |
117 | HEY! WRITE HERE YOUR CONTENT! ✍️
118 |
119 |
120 |
121 |
122 |
123 |
127 |
128 |
129 |
130 |
131 | )
132 | }
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --c-bubble-shadow: #ccccccaa;
3 | }
4 |
5 | @font-face {
6 | font-family: "Pixel";
7 | src: url("./fonts/slkscr.woff2") format("woff2"),
8 | url("./fonts/slkscr.woff") format("woff")
9 | }
10 |
11 | @keyframes blink {
12 | from, to {
13 | color: transparent;
14 | }
15 | 50% {
16 | color: black;
17 | }
18 | }
19 |
20 | html {
21 | box-sizing: border-box;
22 | }
23 |
24 | *, *:before, *:after {
25 | box-sizing: inherit;
26 | }
27 |
28 | [contenteditable] {
29 | outline: 0;
30 | }
31 |
32 | body {
33 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
34 | background-image: radial-gradient(#ddd 1px,transparent 0),radial-gradient(#ddd 1px,transparent 0);
35 | background-position: 0 0,25px 25px;
36 | background-attachment: fixed;
37 | background-size: 50px 50px;
38 | margin: 0;
39 |
40 | align-items: center;
41 | justify-content: center;
42 | flex-direction: column;
43 | display: flex;
44 | margin: 0 auto;
45 | max-width: 800px;
46 | min-height: 100vh;
47 | }
48 |
49 | #app {
50 | align-items: center;
51 | justify-content: center;
52 | display: flex;
53 | margin: 0 auto;
54 | max-width: 800px;
55 | min-height: 100vh;
56 | }
57 |
58 | file-drop {
59 | width: 100%;
60 | }
61 |
62 | #result {
63 | margin: 0 auto 64px;
64 | }
65 |
66 | #result img {
67 | height: auto;
68 | max-width: 100%;
69 | }
70 |
71 | #result.with-image > div {
72 | position: relative;
73 | }
74 |
75 | #result.with-image #result-bubble {
76 | cursor: move;
77 | position: absolute;
78 | left: 0;
79 | bottom: 0;
80 | top: 0;
81 | right: 0;
82 | margin: auto;
83 | }
84 |
85 | #result-bubble {
86 | display: inline-block;
87 | }
88 |
89 | #actions {
90 |
91 | }
92 |
93 | .bubble {
94 | cursor: text;
95 | font-family: 'Pixel', serif;
96 | font-size: 16px;
97 | letter-spacing: -2px;
98 | line-height: 1.2;
99 | text-transform: uppercase;
100 | margin: 10px 10px 24px;
101 | min-width: 40px;
102 | position: relative;
103 | display: inline-block;
104 | text-align: center;
105 | background-color: #fff;
106 | color: #000;
107 | padding: 5px 5px 0 5px;
108 | box-shadow:
109 | 0 -3px #fff,
110 | 0 -6px #000,
111 | 3px 0 #fff,
112 | 3px -3px #000,
113 | 6px 0 #000,
114 | 0 3px #fff,
115 | 0 6px #000,
116 | -3px 0 #fff,
117 | -3px 3px #000,
118 | -6px 0 #000,
119 | -3px -3px #000,
120 | 3px 3px #000,
121 | 3px 9px var(--c-bubble-shadow),
122 | 6px 6px var(--c-bubble-shadow),
123 | 9px 3px var(--c-bubble-shadow);
124 | }
125 |
126 | .bubble::before,
127 | .bubble::after {
128 | content: '';
129 | display: block;
130 | position: absolute;
131 | box-sizing: border-box;
132 | left: 25%;
133 | }
134 |
135 | .bubble::after {
136 | background: #fff;
137 | width: 9px;
138 | height: 3px;
139 | bottom: -14px;
140 | margin-left: 6px;
141 | box-shadow:
142 | -3px 0 #000,
143 | 3px 0 #000,
144 | 3px 3px #fff,
145 | 0px 3px #000,
146 | 6px 3px #000,
147 | 9px 3px var(--c-bubble-shadow),
148 | 3px 6px #000,
149 | 6px 6px #000,
150 | 9px 6px var(--c-bubble-shadow),
151 | 6px 9px var(--c-bubble-shadow);
152 | }
153 |
154 | .bubble::before {
155 | width: 15px;
156 | height: 8px;
157 | background: #fff;
158 | bottom: -11px;
159 | border-left: 3px solid #000;
160 | border-right: 3px solid #000;
161 | }
162 |
163 | button {
164 | background: #f3f3f3;
165 | box-shadow: 0px -3px #000, -3px 0 #000, 3px 0 #000, 0px 3px #000, 6px 3px var(--c-bubble-shadow), 2px 6px var(--c-bubble-shadow), 3px 6px var(--c-bubble-shadow), -1px 0 #000;
166 | border: 0;
167 | cursor: pointer;
168 | letter-spacing: -2px;
169 | font-family: 'Pixel', serif;
170 | text-transform: uppercase;
171 | padding: 4px 12px;
172 |
173 | display: inline-flex;
174 | justify-content: center;
175 | align-items: center;
176 | flex-direction: column;
177 | transition: all .3s ease;
178 | }
179 |
180 | button svg {
181 | fill: #000;
182 | width: 36px;
183 | }
184 |
185 | button:hover {
186 | background: #fff;
187 | box-shadow: 0px -3px #000, -3px 0 #000, 3px 0 #000, 0px 3px #000, 8px 3px var(--c-bubble-shadow), 2px 8px var(--c-bubble-shadow), 2px 6px var(--c-bubble-shadow), -1px 0 #000
188 | }
189 |
190 | button span {
191 | padding-top: 4px;
192 | font-size: 14px;
193 | }
194 |
195 | section {
196 | margin: 0 auto 64px;
197 | max-width: 500px;
198 | text-align: center;
199 | }
200 |
201 | footer {
202 | bottom: 0;
203 | color: #444;
204 | left: 0;
205 | font-size: 9px;
206 | text-align: center;
207 | position: absolute;
208 | right: 0;
209 | }
210 |
211 | footer a {
212 | color: #209cee;
213 | text-decoration: none;
214 | }
215 |
216 | .pixel-icon {
217 | position: relative;
218 | display: inline-block;
219 | width: 16px;
220 | height: 16px;
221 | margin-right: 16px;
222 | margin-bottom: 16px;
223 | transform: scale(2);
224 | transform-origin: top left;
225 | }
226 |
227 | .pixel-icon::before {
228 | position: absolute;
229 | top: -1px;
230 | left: -1px;
231 | display: block;
232 | content: "";
233 | background: 0 0;
234 | }
235 |
236 | .pixel-icon.github::before {
237 | width: 1px;
238 | height: 1px;
239 | color: #333;
240 | box-shadow: 2px 1px, 3px 1px, 4px 1px, 5px 1px, 6px 1px, 7px 1px, 8px 1px, 9px 1px, 10px 1px, 11px 1px, 12px 1px, 13px 1px, 14px 1px, 15px 1px, 1px 2px, 2px 2px, 3px 2px, 4px 2px, 5px 2px #fff, 6px 2px, 7px 2px, 8px 2px, 9px 2px, 10px 2px, 11px 2px, 12px 2px, 13px 2px, 14px 2px #fff, 15px 2px, 16px 2px, 1px 3px, 2px 3px, 3px 3px, 4px 3px, 5px 3px #fff, 6px 3px #fff, 7px 3px, 8px 3px, 9px 3px, 10px 3px, 11px 3px, 12px 3px, 13px 3px #fff, 14px 3px #fff, 15px 3px, 16px 3px, 1px 4px, 2px 4px, 3px 4px, 4px 4px, 5px 4px #fff, 6px 4px #fff, 7px 4px #fff, 8px 4px #fff, 9px 4px #fff, 10px 4px #fff, 11px 4px #fff, 12px 4px #fff, 13px 4px #fff, 14px 4px #fff, 15px 4px, 16px 4px, 1px 5px, 2px 5px, 3px 5px, 4px 5px #fff, 5px 5px #fff, 6px 5px #fff, 7px 5px #fff, 8px 5px #fff, 9px 5px #fff, 10px 5px #fff, 11px 5px #fff, 12px 5px #fff, 13px 5px #fff, 14px 5px #fff, 15px 5px #fff, 16px 5px, 1px 6px, 2px 6px, 3px 6px, 4px 6px #fff, 5px 6px #fff, 6px 6px #fff, 7px 6px #fff, 8px 6px #fff, 9px 6px #fff, 10px 6px #fff, 11px 6px #fff, 12px 6px #fff, 13px 6px #fff, 14px 6px #fff, 15px 6px #fff, 16px 6px, 1px 7px, 2px 7px, 3px 7px, 4px 7px #fff, 5px 7px #fff, 6px 7px #fff, 7px 7px #fff, 8px 7px #fff, 9px 7px #fff, 10px 7px #fff, 11px 7px #fff, 12px 7px #fff, 13px 7px #fff, 14px 7px #fff, 15px 7px #fff, 16px 7px, 1px 8px, 2px 8px, 3px 8px, 4px 8px #fff, 5px 8px #fff, 6px 8px #fff, 7px 8px #fff, 8px 8px #fff, 9px 8px #fff, 10px 8px #fff, 11px 8px #fff, 12px 8px #fff, 13px 8px #fff, 14px 8px #fff, 15px 8px #fff, 16px 8px, 1px 9px, 2px 9px, 3px 9px, 4px 9px, 5px 9px #fff, 6px 9px #fff, 7px 9px #fff, 8px 9px #fff, 9px 9px #fff, 10px 9px #fff, 11px 9px #fff, 12px 9px #fff, 13px 9px #fff, 14px 9px #fff, 15px 9px, 16px 9px, 1px 10px, 2px 10px, 3px 10px, 4px 10px, 5px 10px, 6px 10px #fff, 7px 10px #fff, 8px 10px #fff, 9px 10px #fff, 10px 10px #fff, 11px 10px #fff, 12px 10px #fff, 13px 10px #fff, 14px 10px, 15px 10px, 16px 10px, 1px 11px, 2px 11px #fff, 3px 11px #fff, 4px 11px, 5px 11px, 6px 11px, 7px 11px, 8px 11px #fff, 9px 11px #fff, 10px 11px #fff, 11px 11px #fff, 12px 11px, 13px 11px, 14px 11px, 15px 11px, 16px 11px, 1px 12px, 2px 12px, 3px 12px, 4px 12px #fff, 5px 12px, 6px 12px, 7px 12px #fff, 8px 12px #fff, 9px 12px #fff, 10px 12px #fff, 11px 12px #fff, 12px 12px #fff, 13px 12px, 14px 12px, 15px 12px, 16px 12px, 1px 13px, 2px 13px, 3px 13px, 4px 13px, 5px 13px #fff, 6px 13px #fff, 7px 13px #fff, 8px 13px #fff, 9px 13px #fff, 10px 13px #fff, 11px 13px #fff, 12px 13px #fff, 13px 13px, 14px 13px, 15px 13px, 16px 13px, 1px 14px, 2px 14px, 3px 14px, 4px 14px, 5px 14px, 6px 14px, 7px 14px #fff, 8px 14px #fff, 9px 14px #fff, 10px 14px #fff, 11px 14px #fff, 12px 14px #fff, 13px 14px, 14px 14px, 15px 14px, 16px 14px, 1px 15px, 2px 15px, 3px 15px, 4px 15px, 5px 15px, 6px 15px, 7px 15px #fff, 8px 15px #fff, 9px 15px #fff, 10px 15px #fff, 11px 15px #fff, 12px 15px #fff, 13px 15px, 14px 15px, 15px 15px, 16px 15px, 2px 16px, 3px 16px, 4px 16px, 5px 16px, 6px 16px, 7px 16px, 8px 16px, 9px 16px, 10px 16px, 11px 16px, 12px 16px, 13px 16px, 14px 16px, 15px 16px;
241 | }
242 |
243 | .github-wrapper {
244 | position: fixed;
245 | top: 16px;
246 | right: 16px;
247 | }
--------------------------------------------------------------------------------