Commit
•
797cbb8
1
Parent(s):
8f617f6
working to allow ppl to pick the layout
Browse files- package-lock.json +65 -0
- package.json +1 -0
- public/layouts/layout0.jpg +0 -0
- public/layouts/layout1.jpg +0 -0
- public/layouts/layout2.jpg +0 -0
- public/layouts/layout3.jpg +0 -0
- src/app/engine/presets.ts +29 -3
- src/app/interface/panel/index.tsx +28 -24
- src/app/interface/top-menu/index.tsx +111 -33
- src/app/layouts/index.tsx +65 -13
- src/app/main.tsx +2 -35
- src/app/ocr.tsx +3 -0
- src/app/store/index.ts +31 -14
- src/lib/computePercentage.ts +4 -0
- src/lib/loadImageToCanvas.ts +28 -0
- src/lib/replaceTextInSpeechBubbles.ts +98 -0
- src/lib/writeIntoBubble.ts +0 -115
- src/lib/writeIntoBubbles.ts +0 -65
package-lock.json
CHANGED
@@ -53,6 +53,7 @@
|
|
53 |
"tailwind-merge": "^1.13.2",
|
54 |
"tailwindcss": "3.3.3",
|
55 |
"tailwindcss-animate": "^1.0.6",
|
|
|
56 |
"ts-node": "^10.9.1",
|
57 |
"typescript": "5.1.6",
|
58 |
"usehooks-ts": "^2.9.1",
|
@@ -4236,6 +4237,11 @@
|
|
4236 |
"node": ">=8"
|
4237 |
}
|
4238 |
},
|
|
|
|
|
|
|
|
|
|
|
4239 |
"node_modules/brace-expansion": {
|
4240 |
"version": "1.1.11",
|
4241 |
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
@@ -6078,6 +6084,11 @@
|
|
6078 |
"resolved": "https://registry.npmjs.org/hyphen/-/hyphen-1.6.6.tgz",
|
6079 |
"integrity": "sha512-XtqmnT+b9n5MX+MsqluFAVTIenbtC25iskW0Z+jLd+awfhA+ZbWKWQMIvLJccGoa2bM1R6juWJ27cZxIFOmkWw=="
|
6080 |
},
|
|
|
|
|
|
|
|
|
|
|
6081 |
"node_modules/ignore": {
|
6082 |
"version": "5.2.4",
|
6083 |
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
|
@@ -6249,6 +6260,11 @@
|
|
6249 |
"url": "https://github.com/sponsors/ljharb"
|
6250 |
}
|
6251 |
},
|
|
|
|
|
|
|
|
|
|
|
6252 |
"node_modules/is-extglob": {
|
6253 |
"version": "2.1.1",
|
6254 |
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
@@ -7028,6 +7044,14 @@
|
|
7028 |
"wrappy": "1"
|
7029 |
}
|
7030 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7031 |
"node_modules/optionator": {
|
7032 |
"version": "0.9.3",
|
7033 |
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
|
@@ -8138,6 +8162,34 @@
|
|
8138 |
"node": ">=6"
|
8139 |
}
|
8140 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8141 |
"node_modules/text-segmentation": {
|
8142 |
"version": "1.0.3",
|
8143 |
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
@@ -8603,6 +8655,11 @@
|
|
8603 |
"node": ">= 6"
|
8604 |
}
|
8605 |
},
|
|
|
|
|
|
|
|
|
|
|
8606 |
"node_modules/watchpack": {
|
8607 |
"version": "2.4.0",
|
8608 |
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
|
@@ -8752,6 +8809,14 @@
|
|
8752 |
"url": "https://github.com/sponsors/sindresorhus"
|
8753 |
}
|
8754 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8755 |
"node_modules/zod": {
|
8756 |
"version": "3.21.4",
|
8757 |
"resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz",
|
|
|
53 |
"tailwind-merge": "^1.13.2",
|
54 |
"tailwindcss": "3.3.3",
|
55 |
"tailwindcss-animate": "^1.0.6",
|
56 |
+
"tesseract.js": "^4.1.2",
|
57 |
"ts-node": "^10.9.1",
|
58 |
"typescript": "5.1.6",
|
59 |
"usehooks-ts": "^2.9.1",
|
|
|
4237 |
"node": ">=8"
|
4238 |
}
|
4239 |
},
|
4240 |
+
"node_modules/bmp-js": {
|
4241 |
+
"version": "0.1.0",
|
4242 |
+
"resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz",
|
4243 |
+
"integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw=="
|
4244 |
+
},
|
4245 |
"node_modules/brace-expansion": {
|
4246 |
"version": "1.1.11",
|
4247 |
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
|
|
6084 |
"resolved": "https://registry.npmjs.org/hyphen/-/hyphen-1.6.6.tgz",
|
6085 |
"integrity": "sha512-XtqmnT+b9n5MX+MsqluFAVTIenbtC25iskW0Z+jLd+awfhA+ZbWKWQMIvLJccGoa2bM1R6juWJ27cZxIFOmkWw=="
|
6086 |
},
|
6087 |
+
"node_modules/idb-keyval": {
|
6088 |
+
"version": "6.2.1",
|
6089 |
+
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz",
|
6090 |
+
"integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg=="
|
6091 |
+
},
|
6092 |
"node_modules/ignore": {
|
6093 |
"version": "5.2.4",
|
6094 |
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
|
|
|
6260 |
"url": "https://github.com/sponsors/ljharb"
|
6261 |
}
|
6262 |
},
|
6263 |
+
"node_modules/is-electron": {
|
6264 |
+
"version": "2.2.2",
|
6265 |
+
"resolved": "https://registry.npmjs.org/is-electron/-/is-electron-2.2.2.tgz",
|
6266 |
+
"integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg=="
|
6267 |
+
},
|
6268 |
"node_modules/is-extglob": {
|
6269 |
"version": "2.1.1",
|
6270 |
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
|
|
7044 |
"wrappy": "1"
|
7045 |
}
|
7046 |
},
|
7047 |
+
"node_modules/opencollective-postinstall": {
|
7048 |
+
"version": "2.0.3",
|
7049 |
+
"resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz",
|
7050 |
+
"integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==",
|
7051 |
+
"bin": {
|
7052 |
+
"opencollective-postinstall": "index.js"
|
7053 |
+
}
|
7054 |
+
},
|
7055 |
"node_modules/optionator": {
|
7056 |
"version": "0.9.3",
|
7057 |
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
|
|
|
8162 |
"node": ">=6"
|
8163 |
}
|
8164 |
},
|
8165 |
+
"node_modules/tesseract.js": {
|
8166 |
+
"version": "4.1.2",
|
8167 |
+
"resolved": "https://registry.npmjs.org/tesseract.js/-/tesseract.js-4.1.2.tgz",
|
8168 |
+
"integrity": "sha512-riCr/rbwt0siIf/HiC0W2J/6ndbEcRWVZvwTxeDRPgMwP4douBLYq7i4NgMigWAmJea7GA0r3XGLa2GknRYmPA==",
|
8169 |
+
"hasInstallScript": true,
|
8170 |
+
"dependencies": {
|
8171 |
+
"bmp-js": "^0.1.0",
|
8172 |
+
"idb-keyval": "^6.2.0",
|
8173 |
+
"is-electron": "^2.2.2",
|
8174 |
+
"is-url": "^1.2.4",
|
8175 |
+
"node-fetch": "^2.6.9",
|
8176 |
+
"opencollective-postinstall": "^2.0.3",
|
8177 |
+
"regenerator-runtime": "^0.13.3",
|
8178 |
+
"tesseract.js-core": "^4.0.4",
|
8179 |
+
"wasm-feature-detect": "^1.2.11",
|
8180 |
+
"zlibjs": "^0.3.1"
|
8181 |
+
}
|
8182 |
+
},
|
8183 |
+
"node_modules/tesseract.js-core": {
|
8184 |
+
"version": "4.0.4",
|
8185 |
+
"resolved": "https://registry.npmjs.org/tesseract.js-core/-/tesseract.js-core-4.0.4.tgz",
|
8186 |
+
"integrity": "sha512-MJ+vtktjAaT0681uPl6TDUPhbRbpD/S9emko5rtorgHRZpQo7R3BG7h+3pVHgn1KjfNf1bvnx4B7KxEK8YKqpg=="
|
8187 |
+
},
|
8188 |
+
"node_modules/tesseract.js/node_modules/regenerator-runtime": {
|
8189 |
+
"version": "0.13.11",
|
8190 |
+
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
8191 |
+
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
|
8192 |
+
},
|
8193 |
"node_modules/text-segmentation": {
|
8194 |
"version": "1.0.3",
|
8195 |
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
|
|
8655 |
"node": ">= 6"
|
8656 |
}
|
8657 |
},
|
8658 |
+
"node_modules/wasm-feature-detect": {
|
8659 |
+
"version": "1.5.1",
|
8660 |
+
"resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.5.1.tgz",
|
8661 |
+
"integrity": "sha512-GHr23qmuehNXHY4902/hJ6EV5sUANIJC3R/yMfQ7hWDg3nfhlcJfnIL96R2ohpIwa62araN6aN4bLzzzq5GXkg=="
|
8662 |
+
},
|
8663 |
"node_modules/watchpack": {
|
8664 |
"version": "2.4.0",
|
8665 |
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
|
|
|
8809 |
"url": "https://github.com/sponsors/sindresorhus"
|
8810 |
}
|
8811 |
},
|
8812 |
+
"node_modules/zlibjs": {
|
8813 |
+
"version": "0.3.1",
|
8814 |
+
"resolved": "https://registry.npmjs.org/zlibjs/-/zlibjs-0.3.1.tgz",
|
8815 |
+
"integrity": "sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==",
|
8816 |
+
"engines": {
|
8817 |
+
"node": "*"
|
8818 |
+
}
|
8819 |
+
},
|
8820 |
"node_modules/zod": {
|
8821 |
"version": "3.21.4",
|
8822 |
"resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz",
|
package.json
CHANGED
@@ -54,6 +54,7 @@
|
|
54 |
"tailwind-merge": "^1.13.2",
|
55 |
"tailwindcss": "3.3.3",
|
56 |
"tailwindcss-animate": "^1.0.6",
|
|
|
57 |
"ts-node": "^10.9.1",
|
58 |
"typescript": "5.1.6",
|
59 |
"usehooks-ts": "^2.9.1",
|
|
|
54 |
"tailwind-merge": "^1.13.2",
|
55 |
"tailwindcss": "3.3.3",
|
56 |
"tailwindcss-animate": "^1.0.6",
|
57 |
+
"tesseract.js": "^4.1.2",
|
58 |
"ts-node": "^10.9.1",
|
59 |
"typescript": "5.1.6",
|
60 |
"usehooks-ts": "^2.9.1",
|
public/layouts/layout0.jpg
ADDED
![]() |
public/layouts/layout1.jpg
ADDED
![]() |
public/layouts/layout2.jpg
ADDED
![]() |
public/layouts/layout3.jpg
ADDED
![]() |
src/app/engine/presets.ts
CHANGED
@@ -1,4 +1,5 @@
|
|
1 |
import { FontName, actionman, komika, vtc } from "@/lib/fonts"
|
|
|
2 |
import { NextFontWithVariable } from "next/dist/compiled/@next/font"
|
3 |
|
4 |
export type ComicFamily =
|
@@ -12,6 +13,7 @@ export type ComicColor =
|
|
12 |
| "monochrome"
|
13 |
|
14 |
export interface Preset {
|
|
|
15 |
label: string
|
16 |
family: ComicFamily
|
17 |
color: ComicColor
|
@@ -24,7 +26,18 @@ export interface Preset {
|
|
24 |
// ATTENTION!! negative prompts are not supported by the VideoChain API yet
|
25 |
|
26 |
export const presets: Record<string, Preset> = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
japanese_manga: {
|
|
|
28 |
label: "Japanese",
|
29 |
family: "asian",
|
30 |
color: "grayscale",
|
@@ -51,6 +64,7 @@ export const presets: Record<string, Preset> = {
|
|
51 |
],
|
52 |
},
|
53 |
franco_belgian: {
|
|
|
54 |
label: "Franco-Belgian",
|
55 |
family: "european",
|
56 |
color: "color",
|
@@ -75,6 +89,7 @@ export const presets: Record<string, Preset> = {
|
|
75 |
],
|
76 |
},
|
77 |
american_comic_90: {
|
|
|
78 |
label: "American (modern)",
|
79 |
family: "american",
|
80 |
color: "color",
|
@@ -134,6 +149,7 @@ export const presets: Record<string, Preset> = {
|
|
134 |
},
|
135 |
*/
|
136 |
american_comic_50: {
|
|
|
137 |
label: "American (1950)",
|
138 |
family: "american",
|
139 |
color: "color",
|
@@ -194,6 +210,7 @@ export const presets: Record<string, Preset> = {
|
|
194 |
|
195 |
|
196 |
flying_saucer: {
|
|
|
197 |
label: "Flying saucer",
|
198 |
family: "european",
|
199 |
color: "color",
|
@@ -224,6 +241,7 @@ export const presets: Record<string, Preset> = {
|
|
224 |
},
|
225 |
|
226 |
humanoid: {
|
|
|
227 |
label: "Humanoid",
|
228 |
family: "european",
|
229 |
color: "color",
|
@@ -251,7 +269,8 @@ export const presets: Record<string, Preset> = {
|
|
251 |
"3D render"
|
252 |
],
|
253 |
},
|
254 |
-
|
|
|
255 |
label: "Haddock",
|
256 |
family: "european",
|
257 |
color: "color",
|
@@ -282,6 +301,7 @@ export const presets: Record<string, Preset> = {
|
|
282 |
],
|
283 |
},
|
284 |
armorican: {
|
|
|
285 |
label: "Armorican",
|
286 |
family: "european",
|
287 |
color: "monochrome",
|
@@ -312,6 +332,7 @@ export const presets: Record<string, Preset> = {
|
|
312 |
],
|
313 |
},
|
314 |
render: {
|
|
|
315 |
label: "3D Render",
|
316 |
family: "european",
|
317 |
color: "color",
|
@@ -385,6 +406,11 @@ export const presets: Record<string, Preset> = {
|
|
385 |
|
386 |
export type PresetName = keyof typeof presets
|
387 |
|
388 |
-
export const defaultPreset: PresetName = "
|
|
|
|
|
389 |
|
390 |
-
export const
|
|
|
|
|
|
|
|
1 |
import { FontName, actionman, komika, vtc } from "@/lib/fonts"
|
2 |
+
import { pick } from "@/lib/pick"
|
3 |
import { NextFontWithVariable } from "next/dist/compiled/@next/font"
|
4 |
|
5 |
export type ComicFamily =
|
|
|
13 |
| "monochrome"
|
14 |
|
15 |
export interface Preset {
|
16 |
+
id: string
|
17 |
label: string
|
18 |
family: ComicFamily
|
19 |
color: ComicColor
|
|
|
26 |
// ATTENTION!! negative prompts are not supported by the VideoChain API yet
|
27 |
|
28 |
export const presets: Record<string, Preset> = {
|
29 |
+
random: {
|
30 |
+
id: "random",
|
31 |
+
label: "Random style",
|
32 |
+
family: "european",
|
33 |
+
color: "color",
|
34 |
+
font: "actionman",
|
35 |
+
llmPrompt: "",
|
36 |
+
imagePrompt: (prompt: string) => [],
|
37 |
+
negativePrompt: () => [],
|
38 |
+
},
|
39 |
japanese_manga: {
|
40 |
+
id: "japanese_manga",
|
41 |
label: "Japanese",
|
42 |
family: "asian",
|
43 |
color: "grayscale",
|
|
|
64 |
],
|
65 |
},
|
66 |
franco_belgian: {
|
67 |
+
id: "franco_belgian",
|
68 |
label: "Franco-Belgian",
|
69 |
family: "european",
|
70 |
color: "color",
|
|
|
89 |
],
|
90 |
},
|
91 |
american_comic_90: {
|
92 |
+
id: "american_comic_90",
|
93 |
label: "American (modern)",
|
94 |
family: "american",
|
95 |
color: "color",
|
|
|
149 |
},
|
150 |
*/
|
151 |
american_comic_50: {
|
152 |
+
id: "american_comic_50",
|
153 |
label: "American (1950)",
|
154 |
family: "american",
|
155 |
color: "color",
|
|
|
210 |
|
211 |
|
212 |
flying_saucer: {
|
213 |
+
id: "flying_saucer",
|
214 |
label: "Flying saucer",
|
215 |
family: "european",
|
216 |
color: "color",
|
|
|
241 |
},
|
242 |
|
243 |
humanoid: {
|
244 |
+
id: "humanoid",
|
245 |
label: "Humanoid",
|
246 |
family: "european",
|
247 |
color: "color",
|
|
|
269 |
"3D render"
|
270 |
],
|
271 |
},
|
272 |
+
haddock: {
|
273 |
+
id: "haddock",
|
274 |
label: "Haddock",
|
275 |
family: "european",
|
276 |
color: "color",
|
|
|
301 |
],
|
302 |
},
|
303 |
armorican: {
|
304 |
+
id: "armorican",
|
305 |
label: "Armorican",
|
306 |
family: "european",
|
307 |
color: "monochrome",
|
|
|
332 |
],
|
333 |
},
|
334 |
render: {
|
335 |
+
id: "render",
|
336 |
label: "3D Render",
|
337 |
family: "european",
|
338 |
color: "color",
|
|
|
406 |
|
407 |
export type PresetName = keyof typeof presets
|
408 |
|
409 |
+
export const defaultPreset: PresetName = "random"
|
410 |
+
|
411 |
+
export const getPreset = (preset?: PresetName): Preset => presets[preset || defaultPreset] || presets[defaultPreset]
|
412 |
|
413 |
+
export const getRandomPreset = (): Preset => {
|
414 |
+
const presetName = pick(Object.keys(presets).filter(preset => preset !== "random")) as PresetName
|
415 |
+
return getPreset(presetName)
|
416 |
+
}
|
src/app/interface/panel/index.tsx
CHANGED
@@ -11,9 +11,9 @@ import { useStore } from "@/app/store"
|
|
11 |
import { cn } from "@/lib/utils"
|
12 |
import { getInitialRenderedScene } from "@/lib/getInitialRenderedScene"
|
13 |
import { Progress } from "@/app/interface/progress"
|
14 |
-
|
15 |
-
import {
|
16 |
-
// import {
|
17 |
|
18 |
export function Panel({
|
19 |
panel,
|
@@ -26,10 +26,12 @@ export function Panel({
|
|
26 |
width?: number
|
27 |
height?: number
|
28 |
}) {
|
|
|
29 |
const font = useStore(state => state.font)
|
30 |
const preset = useStore(state => state.preset)
|
31 |
const setGeneratingImages = useStore(state => state.setGeneratingImages)
|
32 |
|
|
|
33 |
const panels = useStore(state => state.panels)
|
34 |
const prompt = panels[panel] || ""
|
35 |
|
@@ -190,20 +192,27 @@ export function Panel({
|
|
190 |
`print:border-[1.5px] print:shadow-none`,
|
191 |
)
|
192 |
|
193 |
-
const [newMask, setNewMask] = useState("")
|
194 |
|
|
|
|
|
195 |
useEffect(() => {
|
196 |
-
const
|
197 |
-
if (rendered.
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
|
|
|
|
|
|
|
|
203 |
}
|
204 |
}
|
205 |
-
|
206 |
-
|
|
|
|
|
207 |
|
208 |
|
209 |
if (prompt && !rendered.assetUrl) {
|
@@ -211,7 +220,7 @@ export function Panel({
|
|
211 |
<div className={cn(
|
212 |
frameClassName,
|
213 |
`flex flex-col items-center justify-center`,
|
214 |
-
className
|
215 |
)}>
|
216 |
<Progress isLoading />
|
217 |
</div>
|
@@ -224,20 +233,15 @@ export function Panel({
|
|
224 |
{ "grayscale": preset.color === "grayscale" },
|
225 |
className
|
226 |
)}>
|
227 |
-
{rendered.assetUrl &&
|
228 |
-
|
|
|
|
|
229 |
width={width}
|
230 |
height={height}
|
231 |
alt={rendered.alt}
|
232 |
-
className="h-full max-w-fit print:w-full print:object-cover"
|
233 |
/>}
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
{/*<Bubble className="absolute top-4 left-4">
|
238 |
-
Hello, world!
|
239 |
-
</Bubble>
|
240 |
-
*/}
|
241 |
</div>
|
242 |
)
|
243 |
}
|
|
|
11 |
import { cn } from "@/lib/utils"
|
12 |
import { getInitialRenderedScene } from "@/lib/getInitialRenderedScene"
|
13 |
import { Progress } from "@/app/interface/progress"
|
14 |
+
|
15 |
+
// import { see } from "@/app/engine/caption"
|
16 |
+
// import { replaceTextInSpeechBubbles } from "@/lib/replaceTextInSpeechBubbles"
|
17 |
|
18 |
export function Panel({
|
19 |
panel,
|
|
|
26 |
width?: number
|
27 |
height?: number
|
28 |
}) {
|
29 |
+
const ref = useRef<HTMLImageElement>(null)
|
30 |
const font = useStore(state => state.font)
|
31 |
const preset = useStore(state => state.preset)
|
32 |
const setGeneratingImages = useStore(state => state.setGeneratingImages)
|
33 |
|
34 |
+
const [imageWithText, setImageWithText] = useState("")
|
35 |
const panels = useStore(state => state.panels)
|
36 |
const prompt = panels[panel] || ""
|
37 |
|
|
|
192 |
`print:border-[1.5px] print:shadow-none`,
|
193 |
)
|
194 |
|
|
|
195 |
|
196 |
+
/*
|
197 |
+
text detection (doesn't work)
|
198 |
useEffect(() => {
|
199 |
+
const fn = async () => {
|
200 |
+
if (!rendered.assetUrl || !ref.current) {
|
201 |
+
return
|
202 |
+
}
|
203 |
+
|
204 |
+
const result = await replaceTextInSpeechBubbles(
|
205 |
+
rendered.assetUrl,
|
206 |
+
"Lorem ipsum dolor sit amet, dolor ipsum. Sit amet? Ipsum! Dolor!!!"
|
207 |
+
)
|
208 |
+
if (result) {
|
209 |
+
setImageWithText(result)
|
210 |
}
|
211 |
}
|
212 |
+
fn()
|
213 |
+
|
214 |
+
}, [rendered.assetUrl, ref.current])
|
215 |
+
*/
|
216 |
|
217 |
|
218 |
if (prompt && !rendered.assetUrl) {
|
|
|
220 |
<div className={cn(
|
221 |
frameClassName,
|
222 |
`flex flex-col items-center justify-center`,
|
223 |
+
className,
|
224 |
)}>
|
225 |
<Progress isLoading />
|
226 |
</div>
|
|
|
233 |
{ "grayscale": preset.color === "grayscale" },
|
234 |
className
|
235 |
)}>
|
236 |
+
{rendered.assetUrl &&
|
237 |
+
<img
|
238 |
+
ref={ref}
|
239 |
+
src={imageWithText || rendered.assetUrl}
|
240 |
width={width}
|
241 |
height={height}
|
242 |
alt={rendered.alt}
|
243 |
+
className="w-full object-cover md:h-full md:max-w-fit print:w-full print:object-cover"
|
244 |
/>}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
245 |
</div>
|
246 |
)
|
247 |
}
|
src/app/interface/top-menu/index.tsx
CHANGED
@@ -1,6 +1,8 @@
|
|
1 |
"use client"
|
2 |
|
3 |
-
import { useState } from "react"
|
|
|
|
|
4 |
|
5 |
import {
|
6 |
Select,
|
@@ -11,27 +13,67 @@ import {
|
|
11 |
} from "@/components/ui/select"
|
12 |
import { Label } from "@/components/ui/label"
|
13 |
import { cn } from "@/lib/utils"
|
14 |
-
import { FontName, fontList, fonts } from "@/lib/fonts"
|
15 |
import { Input } from "@/components/ui/input"
|
16 |
-
import { defaultPreset, getPreset, presets } from "@/app/engine/presets"
|
17 |
import { useStore } from "@/app/store"
|
18 |
import { Button } from "@/components/ui/button"
|
|
|
19 |
|
20 |
-
|
21 |
-
|
22 |
-
|
|
|
|
|
23 |
|
24 |
-
|
25 |
-
|
|
|
|
|
|
|
|
|
26 |
|
|
|
|
|
|
|
|
|
27 |
const prompt = useStore(state => state.prompt)
|
28 |
-
const
|
|
|
|
|
|
|
29 |
|
30 |
const isGeneratingStory = useStore(state => state.isGeneratingStory)
|
31 |
const atLeastOnePanelIsBusy = useStore(state => state.atLeastOnePanelIsBusy)
|
32 |
const isBusy = isGeneratingStory || atLeastOnePanelIsBusy
|
33 |
|
34 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
35 |
return (
|
36 |
<div className={cn(
|
37 |
`print:hidden`,
|
@@ -44,18 +86,20 @@ export function TopMenu() {
|
|
44 |
`space-y-2 md:space-y-0 md:space-x-3 lg:space-x-6`
|
45 |
)}>
|
46 |
<div className="flex flex-row space-x-6 md:space-x-3">
|
47 |
-
|
48 |
`transition-all duration-200 ease-in-out`,
|
49 |
`flex flex-row items-center justify-start space-x-3 font-mono w-1/2 md:w-auto`
|
50 |
)}>
|
51 |
-
|
|
|
|
|
52 |
<Select
|
53 |
defaultValue={defaultPreset}
|
54 |
-
onValueChange={(value) => {
|
55 |
disabled={isBusy}
|
56 |
>
|
57 |
<SelectTrigger className="flex-grow">
|
58 |
-
<SelectValue className="text-sm" placeholder="
|
59 |
</SelectTrigger>
|
60 |
<SelectContent>
|
61 |
{Object.entries(presets).map(([key, preset]) =>
|
@@ -64,18 +108,57 @@ export function TopMenu() {
|
|
64 |
</SelectContent>
|
65 |
</Select>
|
66 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
67 |
<div className={cn(
|
68 |
`transition-all duration-200 ease-in-out`,
|
69 |
`flex flex-row items-center space-x-3 font-mono w-1/2 md:w-auto md:hidden`
|
70 |
)}>
|
71 |
-
<Label className="flex text-sm md:w-24">Font:</Label>
|
72 |
<Select
|
73 |
defaultValue={fontList.includes(preset.font) ? preset.font : "cartoonist"}
|
74 |
onValueChange={(value) => { setFont(value as FontName) }}
|
75 |
disabled={atLeastOnePanelIsBusy}
|
76 |
>
|
77 |
<SelectTrigger className="flex-grow">
|
78 |
-
<SelectValue className="text-sm" placeholder="Type" />
|
79 |
</SelectTrigger>
|
80 |
<SelectContent>
|
81 |
{Object.keys(fonts)
|
@@ -89,6 +172,7 @@ export function TopMenu() {
|
|
89 |
</SelectContent>
|
90 |
</Select>
|
91 |
</div>
|
|
|
92 |
</div>
|
93 |
<div className={cn(
|
94 |
`transition-all duration-200 ease-in-out`,
|
@@ -99,39 +183,32 @@ export function TopMenu() {
|
|
99 |
className="w-full bg-neutral-300 text-neutral-800 dark:bg-neutral-300 dark:text-neutral-800"
|
100 |
// disabled={atLeastOnePanelIsBusy}
|
101 |
onChange={(e) => {
|
102 |
-
|
103 |
}}
|
104 |
onKeyDown={({ key }) => {
|
105 |
-
if (isBusy) {
|
106 |
-
return
|
107 |
-
}
|
108 |
if (key === 'Enter') {
|
109 |
-
|
110 |
-
setPrompt(draft.trim())
|
111 |
-
}
|
112 |
}
|
113 |
}}
|
114 |
-
value={
|
115 |
/>
|
116 |
<Button
|
117 |
onClick={() => {
|
118 |
-
|
119 |
-
return
|
120 |
-
}
|
121 |
-
if (draft.trim() !== prompt.trim()) {
|
122 |
-
setPrompt(draft.trim())
|
123 |
-
}
|
124 |
}}
|
125 |
-
disabled={!
|
126 |
>
|
127 |
Generate
|
128 |
</Button>
|
129 |
</div>
|
|
|
|
|
|
|
130 |
<div className={cn(
|
131 |
`transition-all duration-200 ease-in-out`,
|
132 |
`hidden md:flex flex-row items-center space-x-3 font-mono w-full md:w-auto`
|
133 |
)}>
|
134 |
-
<Label className="flex text-sm w-24">Font:</Label>
|
135 |
<Select
|
136 |
defaultValue={fontList.includes(preset.font) ? preset.font : "actionman"}
|
137 |
onValueChange={(value) => { setFont(value as FontName) }}
|
@@ -139,7 +216,7 @@ export function TopMenu() {
|
|
139 |
disabled={true}
|
140 |
>
|
141 |
<SelectTrigger className="flex-grow">
|
142 |
-
<SelectValue className="text-sm" placeholder="Type" />
|
143 |
</SelectTrigger>
|
144 |
<SelectContent>
|
145 |
{Object.keys(fonts)
|
@@ -153,6 +230,7 @@ export function TopMenu() {
|
|
153 |
</SelectContent>
|
154 |
</Select>
|
155 |
</div>
|
|
|
156 |
</div>
|
157 |
)
|
158 |
}
|
|
|
1 |
"use client"
|
2 |
|
3 |
+
import { useEffect, useState } from "react"
|
4 |
+
import { useSearchParams } from "next/navigation"
|
5 |
+
import Image from "next/image"
|
6 |
|
7 |
import {
|
8 |
Select,
|
|
|
13 |
} from "@/components/ui/select"
|
14 |
import { Label } from "@/components/ui/label"
|
15 |
import { cn } from "@/lib/utils"
|
16 |
+
import { FontName, defaultFont, fontList, fonts } from "@/lib/fonts"
|
17 |
import { Input } from "@/components/ui/input"
|
18 |
+
import { PresetName, defaultPreset, getPreset, getRandomPreset, presets } from "@/app/engine/presets"
|
19 |
import { useStore } from "@/app/store"
|
20 |
import { Button } from "@/components/ui/button"
|
21 |
+
import { LayoutName, allLayoutLabels, allLayouts, defaultLayout } from "@/app/layouts"
|
22 |
|
23 |
+
import layoutPreview0 from "../../../../public/layouts/layout0.jpg"
|
24 |
+
import layoutPreview1 from "../../../../public/layouts/layout1.jpg"
|
25 |
+
import layoutPreview2 from "../../../../public/layouts/layout2.jpg"
|
26 |
+
import layoutPreview3 from "../../../../public/layouts/layout3.jpg"
|
27 |
+
import { StaticImageData } from "next/image"
|
28 |
|
29 |
+
const layoutIcons: Partial<Record<LayoutName, StaticImageData>> = {
|
30 |
+
Layout0: layoutPreview0,
|
31 |
+
Layout1: layoutPreview1,
|
32 |
+
Layout2: layoutPreview2,
|
33 |
+
Layout3: layoutPreview3
|
34 |
+
}
|
35 |
|
36 |
+
export function TopMenu() {
|
37 |
+
// const font = useStore(state => state.font)
|
38 |
+
// const setFont = useStore(state => state.setFont)
|
39 |
+
const preset = useStore(state => state.preset)
|
40 |
const prompt = useStore(state => state.prompt)
|
41 |
+
const layout = useStore(state => state.layout)
|
42 |
+
const setLayout = useStore(state => state.setLayout)
|
43 |
+
|
44 |
+
const generate = useStore(state => state.generate)
|
45 |
|
46 |
const isGeneratingStory = useStore(state => state.isGeneratingStory)
|
47 |
const atLeastOnePanelIsBusy = useStore(state => state.atLeastOnePanelIsBusy)
|
48 |
const isBusy = isGeneratingStory || atLeastOnePanelIsBusy
|
49 |
|
50 |
+
const searchParams = useSearchParams()
|
51 |
+
|
52 |
+
const requestedPreset = (searchParams.get('preset') as PresetName) || defaultPreset
|
53 |
+
const requestedFont = (searchParams.get('font') as FontName) || defaultFont
|
54 |
+
const requestedPrompt = (searchParams.get('prompt') as string) || ""
|
55 |
+
const requestedLayout = (searchParams.get('prompt') as LayoutName) || defaultLayout
|
56 |
+
|
57 |
+
const [draftPrompt, setDraftPrompt] = useState(requestedPrompt)
|
58 |
+
const [draftPreset, setDraftPreset] = useState<PresetName>(requestedPreset)
|
59 |
+
const [draftLayout, setDraftLayout] = useState<LayoutName>(requestedLayout)
|
60 |
+
|
61 |
+
const handleSubmit = () => {
|
62 |
+
const promptChanged = draftPrompt.trim() !== prompt.trim()
|
63 |
+
const presetChanged = draftPreset !== preset.id
|
64 |
+
const layoutChanged = draftLayout !== layout
|
65 |
+
if (!isBusy && (promptChanged || presetChanged || layoutChanged)) {
|
66 |
+
generate(draftPrompt, draftPreset, draftLayout)
|
67 |
+
}
|
68 |
+
}
|
69 |
+
|
70 |
+
useEffect(() => {
|
71 |
+
const layoutChanged = draftLayout !== layout
|
72 |
+
if (layoutChanged && !isBusy) {
|
73 |
+
setLayout(draftLayout)
|
74 |
+
}
|
75 |
+
}, [layout, draftLayout, isBusy])
|
76 |
+
|
77 |
return (
|
78 |
<div className={cn(
|
79 |
`print:hidden`,
|
|
|
86 |
`space-y-2 md:space-y-0 md:space-x-3 lg:space-x-6`
|
87 |
)}>
|
88 |
<div className="flex flex-row space-x-6 md:space-x-3">
|
89 |
+
<div className={cn(
|
90 |
`transition-all duration-200 ease-in-out`,
|
91 |
`flex flex-row items-center justify-start space-x-3 font-mono w-1/2 md:w-auto`
|
92 |
)}>
|
93 |
+
|
94 |
+
{/* <Label className="flex text-xs md:text-sm md:w-24">Style:</Label> */}
|
95 |
+
|
96 |
<Select
|
97 |
defaultValue={defaultPreset}
|
98 |
+
onValueChange={(value) => { setDraftPreset(value as PresetName) }}
|
99 |
disabled={isBusy}
|
100 |
>
|
101 |
<SelectTrigger className="flex-grow">
|
102 |
+
<SelectValue className="text-xs md:text-sm" placeholder="Style" />
|
103 |
</SelectTrigger>
|
104 |
<SelectContent>
|
105 |
{Object.entries(presets).map(([key, preset]) =>
|
|
|
108 |
</SelectContent>
|
109 |
</Select>
|
110 |
</div>
|
111 |
+
<div className={cn(
|
112 |
+
`transition-all duration-200 ease-in-out`,
|
113 |
+
`flex flex-row items-center justify-start space-x-3 font-mono w-1/2 md:w-auto`
|
114 |
+
)}>
|
115 |
+
|
116 |
+
{/* <Label className="flex text-xs md:text-sm md:w-24">Style:</Label> */}
|
117 |
+
|
118 |
+
<Select
|
119 |
+
defaultValue={defaultLayout}
|
120 |
+
onValueChange={(value) => { setDraftLayout(value as LayoutName) }}
|
121 |
+
disabled={isBusy}
|
122 |
+
>
|
123 |
+
<SelectTrigger className="flex-grow">
|
124 |
+
<SelectValue className="text-xs md:text-sm" placeholder="Layout" />
|
125 |
+
</SelectTrigger>
|
126 |
+
<SelectContent>
|
127 |
+
{Object.keys(allLayouts).map(key =>
|
128 |
+
<SelectItem key={key} value={key} className="w-full">
|
129 |
+
<div className="space-x-6 flex flex-row items-center justify-between font-mono">
|
130 |
+
<div className="flex">{
|
131 |
+
(allLayoutLabels as any)[key]
|
132 |
+
}</div>
|
133 |
+
|
134 |
+
{(layoutIcons as any)[key]
|
135 |
+
? <Image
|
136 |
+
className="rounded-sm opacity-75"
|
137 |
+
src={(layoutIcons as any)[key]}
|
138 |
+
width={20}
|
139 |
+
height={18}
|
140 |
+
alt={key}
|
141 |
+
/> : null}
|
142 |
+
|
143 |
+
</div>
|
144 |
+
</SelectItem>
|
145 |
+
)}
|
146 |
+
</SelectContent>
|
147 |
+
</Select>
|
148 |
+
</div>
|
149 |
+
{/*
|
150 |
<div className={cn(
|
151 |
`transition-all duration-200 ease-in-out`,
|
152 |
`flex flex-row items-center space-x-3 font-mono w-1/2 md:w-auto md:hidden`
|
153 |
)}>
|
154 |
+
<Label className="flex text-xs md:text-sm md:w-24">Font:</Label>
|
155 |
<Select
|
156 |
defaultValue={fontList.includes(preset.font) ? preset.font : "cartoonist"}
|
157 |
onValueChange={(value) => { setFont(value as FontName) }}
|
158 |
disabled={atLeastOnePanelIsBusy}
|
159 |
>
|
160 |
<SelectTrigger className="flex-grow">
|
161 |
+
<SelectValue className="text-xs md:text-sm" placeholder="Type" />
|
162 |
</SelectTrigger>
|
163 |
<SelectContent>
|
164 |
{Object.keys(fonts)
|
|
|
172 |
</SelectContent>
|
173 |
</Select>
|
174 |
</div>
|
175 |
+
*/}
|
176 |
</div>
|
177 |
<div className={cn(
|
178 |
`transition-all duration-200 ease-in-out`,
|
|
|
183 |
className="w-full bg-neutral-300 text-neutral-800 dark:bg-neutral-300 dark:text-neutral-800"
|
184 |
// disabled={atLeastOnePanelIsBusy}
|
185 |
onChange={(e) => {
|
186 |
+
setDraftPrompt(e.target.value)
|
187 |
}}
|
188 |
onKeyDown={({ key }) => {
|
|
|
|
|
|
|
189 |
if (key === 'Enter') {
|
190 |
+
handleSubmit()
|
|
|
|
|
191 |
}
|
192 |
}}
|
193 |
+
value={draftPrompt}
|
194 |
/>
|
195 |
<Button
|
196 |
onClick={() => {
|
197 |
+
handleSubmit()
|
|
|
|
|
|
|
|
|
|
|
198 |
}}
|
199 |
+
disabled={!draftPrompt?.trim().length || isBusy}
|
200 |
>
|
201 |
Generate
|
202 |
</Button>
|
203 |
</div>
|
204 |
+
{/*
|
205 |
+
Let's add this feature later, because right now people
|
206 |
+
are confused about why they can't activate it
|
207 |
<div className={cn(
|
208 |
`transition-all duration-200 ease-in-out`,
|
209 |
`hidden md:flex flex-row items-center space-x-3 font-mono w-full md:w-auto`
|
210 |
)}>
|
211 |
+
<Label className="flex text-xs md:text-sm w-24">Font:</Label>
|
212 |
<Select
|
213 |
defaultValue={fontList.includes(preset.font) ? preset.font : "actionman"}
|
214 |
onValueChange={(value) => { setFont(value as FontName) }}
|
|
|
216 |
disabled={true}
|
217 |
>
|
218 |
<SelectTrigger className="flex-grow">
|
219 |
+
<SelectValue className="text-xs md:text-sm" placeholder="Type" />
|
220 |
</SelectTrigger>
|
221 |
<SelectContent>
|
222 |
{Object.keys(fonts)
|
|
|
230 |
</SelectContent>
|
231 |
</Select>
|
232 |
</div>
|
233 |
+
*/}
|
234 |
</div>
|
235 |
)
|
236 |
}
|
src/app/layouts/index.tsx
CHANGED
@@ -4,6 +4,41 @@ import { Panel } from "@/app/interface/panel"
|
|
4 |
import { pick } from "@/lib/pick"
|
5 |
import { Grid } from "@/app/interface/grid"
|
6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7 |
export function Layout1() {
|
8 |
return (
|
9 |
<Grid className="grid-cols-2 grid-rows-3">
|
@@ -39,7 +74,7 @@ export function Layout1() {
|
|
39 |
)
|
40 |
}
|
41 |
|
42 |
-
export function
|
43 |
return (
|
44 |
<Grid className="grid-cols-2 grid-rows-3">
|
45 |
<div className="bg-gray-100 row-span-3 col-span-1">
|
@@ -74,7 +109,7 @@ export function Layout2() {
|
|
74 |
)
|
75 |
}
|
76 |
|
77 |
-
export function
|
78 |
return (
|
79 |
<Grid className="grid-cols-5 grid-rows-2">
|
80 |
<div className="bg-zinc-100 col-span-3">
|
@@ -111,7 +146,7 @@ export function Layout3() {
|
|
111 |
)
|
112 |
}
|
113 |
|
114 |
-
export function
|
115 |
return (
|
116 |
<Grid className="grid-cols-2 grid-rows-3">
|
117 |
<div className="bg-slate-100 row-span-2">
|
@@ -147,7 +182,7 @@ export function Layout4() {
|
|
147 |
}
|
148 |
|
149 |
|
150 |
-
export function
|
151 |
return (
|
152 |
<Grid className="grid-cols-3 grid-rows-2">
|
153 |
<div className="bg-zinc-100 col-span-1 row-span-1">
|
@@ -182,7 +217,7 @@ export function Layout5() {
|
|
182 |
)
|
183 |
}
|
184 |
|
185 |
-
export function
|
186 |
return (
|
187 |
<Grid className="grid-cols-3 grid-rows-2">
|
188 |
<div className="bg-zinc-100 col-span-2 row-span-1">
|
@@ -217,19 +252,36 @@ export function Layout6() {
|
|
217 |
)
|
218 |
}
|
219 |
|
220 |
-
// export const layouts = { Layout1,
|
221 |
-
export const allLayouts = {
|
|
|
|
|
222 |
Layout1,
|
223 |
-
|
224 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
225 |
}
|
226 |
|
227 |
export type LayoutName = keyof typeof allLayouts
|
228 |
|
229 |
-
export
|
230 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
231 |
}
|
232 |
|
233 |
export function getRandomLayoutNames(): LayoutName[] {
|
234 |
-
return
|
235 |
-
}
|
|
|
|
4 |
import { pick } from "@/lib/pick"
|
5 |
import { Grid } from "@/app/interface/grid"
|
6 |
|
7 |
+
export function Layout0() {
|
8 |
+
return (
|
9 |
+
<Grid className="grid-cols-2 grid-rows-2">
|
10 |
+
<div className="bg-stone-100 col-span-1 row-span-1">
|
11 |
+
<Panel
|
12 |
+
panel={0}
|
13 |
+
width={1024}
|
14 |
+
height={1024}
|
15 |
+
/>
|
16 |
+
</div>
|
17 |
+
<div className="bg-zinc-100 col-span-1 row-span-1">
|
18 |
+
<Panel
|
19 |
+
panel={1}
|
20 |
+
width={1024}
|
21 |
+
height={1024}
|
22 |
+
/>
|
23 |
+
</div>
|
24 |
+
<div className="bg-gray-100 col-span-1 row-span-1">
|
25 |
+
<Panel
|
26 |
+
panel={2}
|
27 |
+
width={1024}
|
28 |
+
height={1024}
|
29 |
+
/>
|
30 |
+
</div>
|
31 |
+
<div className="bg-slate-100 col-span-1 row-span-1">
|
32 |
+
<Panel
|
33 |
+
panel={3}
|
34 |
+
width={1024}
|
35 |
+
height={1024}
|
36 |
+
/>
|
37 |
+
</div>
|
38 |
+
</Grid>
|
39 |
+
)
|
40 |
+
}
|
41 |
+
|
42 |
export function Layout1() {
|
43 |
return (
|
44 |
<Grid className="grid-cols-2 grid-rows-3">
|
|
|
74 |
)
|
75 |
}
|
76 |
|
77 |
+
export function Layout2_todo() {
|
78 |
return (
|
79 |
<Grid className="grid-cols-2 grid-rows-3">
|
80 |
<div className="bg-gray-100 row-span-3 col-span-1">
|
|
|
109 |
)
|
110 |
}
|
111 |
|
112 |
+
export function Layout3_todo() {
|
113 |
return (
|
114 |
<Grid className="grid-cols-5 grid-rows-2">
|
115 |
<div className="bg-zinc-100 col-span-3">
|
|
|
146 |
)
|
147 |
}
|
148 |
|
149 |
+
export function Layout4_todo() {
|
150 |
return (
|
151 |
<Grid className="grid-cols-2 grid-rows-3">
|
152 |
<div className="bg-slate-100 row-span-2">
|
|
|
182 |
}
|
183 |
|
184 |
|
185 |
+
export function Layout2() {
|
186 |
return (
|
187 |
<Grid className="grid-cols-3 grid-rows-2">
|
188 |
<div className="bg-zinc-100 col-span-1 row-span-1">
|
|
|
217 |
)
|
218 |
}
|
219 |
|
220 |
+
export function Layout3() {
|
221 |
return (
|
222 |
<Grid className="grid-cols-3 grid-rows-2">
|
223 |
<div className="bg-zinc-100 col-span-2 row-span-1">
|
|
|
252 |
)
|
253 |
}
|
254 |
|
255 |
+
// export const layouts = { Layout1, Layout2_todo, Layout3_todo, Layout4_todo, Layout2, Layout3 }
|
256 |
+
export const allLayouts = {
|
257 |
+
random: <></>,
|
258 |
+
Layout0,
|
259 |
Layout1,
|
260 |
+
Layout2,
|
261 |
+
Layout3
|
262 |
+
}
|
263 |
+
|
264 |
+
export const allLayoutLabels = {
|
265 |
+
random: "Random layout",
|
266 |
+
Layout0: "Layout 0",
|
267 |
+
Layout1: "Layout 1",
|
268 |
+
Layout2: "Layout 2",
|
269 |
+
Layout3: "Layout 3",
|
270 |
}
|
271 |
|
272 |
export type LayoutName = keyof typeof allLayouts
|
273 |
|
274 |
+
export const defaultLayout: LayoutName = "random"
|
275 |
+
|
276 |
+
export type LayoutCategory = "square" | "fluid"
|
277 |
+
|
278 |
+
export const nonRandomLayouts = Object.keys(allLayouts).filter(layout => layout !== "random")
|
279 |
+
|
280 |
+
export const getRandomLayoutName = (): LayoutName => {
|
281 |
+
return pick(nonRandomLayouts) as LayoutName
|
282 |
}
|
283 |
|
284 |
export function getRandomLayoutNames(): LayoutName[] {
|
285 |
+
return nonRandomLayouts.sort(() => Math.random() - 0.5) as LayoutName[]
|
286 |
+
}
|
287 |
+
|
src/app/main.tsx
CHANGED
@@ -1,13 +1,10 @@
|
|
1 |
"use client"
|
2 |
|
3 |
-
import { useEffect,
|
4 |
-
import { useSearchParams } from "next/navigation"
|
5 |
-
|
6 |
-
import { PresetName, defaultPreset, getPreset } from "@/app/engine/presets"
|
7 |
|
8 |
import { cn } from "@/lib/utils"
|
9 |
import { TopMenu } from "./interface/top-menu"
|
10 |
-
import {
|
11 |
import { getRandomLayoutName } from "./layouts"
|
12 |
import { useStore } from "./store"
|
13 |
import { Zoom } from "./interface/zoom"
|
@@ -17,23 +14,13 @@ import { Page } from "./interface/page"
|
|
17 |
|
18 |
export default function Main() {
|
19 |
const [_isPending, startTransition] = useTransition()
|
20 |
-
const searchParams = useSearchParams()
|
21 |
-
|
22 |
-
const requestedPreset = (searchParams.get('preset') as PresetName) || defaultPreset
|
23 |
-
const requestedFont = (searchParams.get('font') as FontName) || defaultFont
|
24 |
-
const requestedPrompt = (searchParams.get('prompt') as string) || ""
|
25 |
|
26 |
const isGeneratingStory = useStore(state => state.isGeneratingStory)
|
27 |
const setGeneratingStory = useStore(state => state.setGeneratingStory)
|
28 |
|
29 |
const font = useStore(state => state.font)
|
30 |
-
const setFont = useStore(state => state.setFont)
|
31 |
-
|
32 |
const preset = useStore(state => state.preset)
|
33 |
-
const setPreset = useStore(state => state.setPreset)
|
34 |
-
|
35 |
const prompt = useStore(state => state.prompt)
|
36 |
-
const setPrompt = useStore(state => state.setPrompt)
|
37 |
|
38 |
const setLayouts = useStore(state => state.setLayouts)
|
39 |
|
@@ -43,18 +30,6 @@ export default function Main() {
|
|
43 |
|
44 |
const [waitABitMore, setWaitABitMore] = useState(false)
|
45 |
|
46 |
-
// react to URL params
|
47 |
-
useEffect(() => {
|
48 |
-
if (requestedPreset && requestedPreset !== preset.label) { setPreset(getPreset(requestedPreset)) }
|
49 |
-
}, [requestedPreset])
|
50 |
-
|
51 |
-
useEffect(() => {
|
52 |
-
if (requestedFont && requestedFont !== font) { setFont(requestedFont) }
|
53 |
-
}, [requestedFont])
|
54 |
-
|
55 |
-
useEffect(() => {
|
56 |
-
if (requestedPrompt && requestedPrompt !== prompt) { setPrompt(requestedPrompt) }
|
57 |
-
}, [requestedPrompt])
|
58 |
|
59 |
// react to prompt changes
|
60 |
useEffect(() => {
|
@@ -64,14 +39,6 @@ export default function Main() {
|
|
64 |
setWaitABitMore(false)
|
65 |
setGeneratingStory(true)
|
66 |
|
67 |
-
const newLayouts = [
|
68 |
-
getRandomLayoutName(),
|
69 |
-
getRandomLayoutName(),
|
70 |
-
]
|
71 |
-
|
72 |
-
console.log("using layouts " + newLayouts)
|
73 |
-
setLayouts(newLayouts)
|
74 |
-
|
75 |
try {
|
76 |
|
77 |
const llmResponse = await getStory({ preset, prompt })
|
|
|
1 |
"use client"
|
2 |
|
3 |
+
import { useEffect, useState, useTransition } from "react"
|
|
|
|
|
|
|
4 |
|
5 |
import { cn } from "@/lib/utils"
|
6 |
import { TopMenu } from "./interface/top-menu"
|
7 |
+
import { fonts } from "@/lib/fonts"
|
8 |
import { getRandomLayoutName } from "./layouts"
|
9 |
import { useStore } from "./store"
|
10 |
import { Zoom } from "./interface/zoom"
|
|
|
14 |
|
15 |
export default function Main() {
|
16 |
const [_isPending, startTransition] = useTransition()
|
|
|
|
|
|
|
|
|
|
|
17 |
|
18 |
const isGeneratingStory = useStore(state => state.isGeneratingStory)
|
19 |
const setGeneratingStory = useStore(state => state.setGeneratingStory)
|
20 |
|
21 |
const font = useStore(state => state.font)
|
|
|
|
|
22 |
const preset = useStore(state => state.preset)
|
|
|
|
|
23 |
const prompt = useStore(state => state.prompt)
|
|
|
24 |
|
25 |
const setLayouts = useStore(state => state.setLayouts)
|
26 |
|
|
|
30 |
|
31 |
const [waitABitMore, setWaitABitMore] = useState(false)
|
32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
|
34 |
// react to prompt changes
|
35 |
useEffect(() => {
|
|
|
39 |
setWaitABitMore(false)
|
40 |
setGeneratingStory(true)
|
41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
42 |
try {
|
43 |
|
44 |
const llmResponse = await getStory({ preset, prompt })
|
src/app/ocr.tsx
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { createWorker } from "tesseract.js"
|
src/app/store/index.ts
CHANGED
@@ -3,8 +3,8 @@
|
|
3 |
import { create } from "zustand"
|
4 |
|
5 |
import { FontName } from "@/lib/fonts"
|
6 |
-
import { Preset, getPreset } from "@/app/engine/presets"
|
7 |
-
import { LayoutName, getRandomLayoutNames } from "../layouts"
|
8 |
import html2canvas from "html2canvas"
|
9 |
|
10 |
export const useStore = create<{
|
@@ -14,6 +14,7 @@ export const useStore = create<{
|
|
14 |
nbFrames: number
|
15 |
panels: string[]
|
16 |
captions: Record<string, string>
|
|
|
17 |
layouts: LayoutName[]
|
18 |
zoomLevel: number
|
19 |
page: HTMLDivElement
|
@@ -25,6 +26,7 @@ export const useStore = create<{
|
|
25 |
setFont: (font: FontName) => void
|
26 |
setPreset: (preset: Preset) => void
|
27 |
setPanels: (panels: string[]) => void
|
|
|
28 |
setLayouts: (layouts: LayoutName[]) => void
|
29 |
setCaption: (panelId: number, caption: string) => void
|
30 |
setZoomLevel: (zoomLevel: number) => void
|
@@ -34,6 +36,7 @@ export const useStore = create<{
|
|
34 |
setGeneratingText: (isGeneratingText: boolean) => void
|
35 |
pageToImage: () => Promise<string>
|
36 |
download: () => Promise<void>
|
|
|
37 |
}>((set, get) => ({
|
38 |
prompt: "",
|
39 |
font: "actionman",
|
@@ -41,6 +44,7 @@ export const useStore = create<{
|
|
41 |
nbFrames: 1,
|
42 |
panels: [],
|
43 |
captions: {},
|
|
|
44 |
layouts: getRandomLayoutNames(),
|
45 |
zoomLevel: 60,
|
46 |
page: undefined as unknown as HTMLDivElement,
|
@@ -53,9 +57,6 @@ export const useStore = create<{
|
|
53 |
if (prompt === existingPrompt) { return }
|
54 |
set({
|
55 |
prompt,
|
56 |
-
layouts: getRandomLayoutNames(),
|
57 |
-
panels: [],
|
58 |
-
captions: {},
|
59 |
})
|
60 |
},
|
61 |
setFont: (font: FontName) => {
|
@@ -63,9 +64,6 @@ export const useStore = create<{
|
|
63 |
if (font === existingFont) { return }
|
64 |
set({
|
65 |
font,
|
66 |
-
layouts: getRandomLayoutNames(),
|
67 |
-
panels: [],
|
68 |
-
captions: {}
|
69 |
})
|
70 |
},
|
71 |
setPreset: (preset: Preset) => {
|
@@ -73,9 +71,6 @@ export const useStore = create<{
|
|
73 |
if (preset.label === existingPreset.label) { return }
|
74 |
set({
|
75 |
preset,
|
76 |
-
layouts: getRandomLayoutNames(),
|
77 |
-
panels: [],
|
78 |
-
captions: {}
|
79 |
})
|
80 |
},
|
81 |
setNbFrames: (nbFrames: number) => {
|
@@ -83,9 +78,6 @@ export const useStore = create<{
|
|
83 |
if (nbFrames === existingNbFrames) { return }
|
84 |
set({
|
85 |
nbFrames,
|
86 |
-
layouts: getRandomLayoutNames(),
|
87 |
-
panels: [],
|
88 |
-
captions: {}
|
89 |
})
|
90 |
},
|
91 |
setPanels: (panels: string[]) => set({ panels }),
|
@@ -97,6 +89,16 @@ export const useStore = create<{
|
|
97 |
}
|
98 |
})
|
99 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
100 |
setLayouts: (layouts: LayoutName[]) => set({ layouts }),
|
101 |
setZoomLevel: (zoomLevel: number) => set({ zoomLevel }),
|
102 |
setPage: (page: HTMLDivElement) => {
|
@@ -144,5 +146,20 @@ export const useStore = create<{
|
|
144 |
} else {
|
145 |
window.open(data)
|
146 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
147 |
}
|
148 |
}))
|
|
|
3 |
import { create } from "zustand"
|
4 |
|
5 |
import { FontName } from "@/lib/fonts"
|
6 |
+
import { Preset, PresetName, getPreset, getRandomPreset } from "@/app/engine/presets"
|
7 |
+
import { LayoutName, getRandomLayoutName, getRandomLayoutNames } from "../layouts"
|
8 |
import html2canvas from "html2canvas"
|
9 |
|
10 |
export const useStore = create<{
|
|
|
14 |
nbFrames: number
|
15 |
panels: string[]
|
16 |
captions: Record<string, string>
|
17 |
+
layout: LayoutName
|
18 |
layouts: LayoutName[]
|
19 |
zoomLevel: number
|
20 |
page: HTMLDivElement
|
|
|
26 |
setFont: (font: FontName) => void
|
27 |
setPreset: (preset: Preset) => void
|
28 |
setPanels: (panels: string[]) => void
|
29 |
+
setLayout: (layout: LayoutName) => void
|
30 |
setLayouts: (layouts: LayoutName[]) => void
|
31 |
setCaption: (panelId: number, caption: string) => void
|
32 |
setZoomLevel: (zoomLevel: number) => void
|
|
|
36 |
setGeneratingText: (isGeneratingText: boolean) => void
|
37 |
pageToImage: () => Promise<string>
|
38 |
download: () => Promise<void>
|
39 |
+
generate: (prompt: string, presetName: PresetName, layoutName: LayoutName) => void
|
40 |
}>((set, get) => ({
|
41 |
prompt: "",
|
42 |
font: "actionman",
|
|
|
44 |
nbFrames: 1,
|
45 |
panels: [],
|
46 |
captions: {},
|
47 |
+
layout: "Layout1",
|
48 |
layouts: getRandomLayoutNames(),
|
49 |
zoomLevel: 60,
|
50 |
page: undefined as unknown as HTMLDivElement,
|
|
|
57 |
if (prompt === existingPrompt) { return }
|
58 |
set({
|
59 |
prompt,
|
|
|
|
|
|
|
60 |
})
|
61 |
},
|
62 |
setFont: (font: FontName) => {
|
|
|
64 |
if (font === existingFont) { return }
|
65 |
set({
|
66 |
font,
|
|
|
|
|
|
|
67 |
})
|
68 |
},
|
69 |
setPreset: (preset: Preset) => {
|
|
|
71 |
if (preset.label === existingPreset.label) { return }
|
72 |
set({
|
73 |
preset,
|
|
|
|
|
|
|
74 |
})
|
75 |
},
|
76 |
setNbFrames: (nbFrames: number) => {
|
|
|
78 |
if (nbFrames === existingNbFrames) { return }
|
79 |
set({
|
80 |
nbFrames,
|
|
|
|
|
|
|
81 |
})
|
82 |
},
|
83 |
setPanels: (panels: string[]) => set({ panels }),
|
|
|
89 |
}
|
90 |
})
|
91 |
},
|
92 |
+
setLayout: (layoutName: LayoutName) => {
|
93 |
+
const layout = layoutName === "random"
|
94 |
+
? getRandomLayoutName()
|
95 |
+
: layoutName
|
96 |
+
|
97 |
+
set({
|
98 |
+
layout,
|
99 |
+
layouts: [layout, layout]
|
100 |
+
})
|
101 |
+
},
|
102 |
setLayouts: (layouts: LayoutName[]) => set({ layouts }),
|
103 |
setZoomLevel: (zoomLevel: number) => set({ zoomLevel }),
|
104 |
setPage: (page: HTMLDivElement) => {
|
|
|
146 |
} else {
|
147 |
window.open(data)
|
148 |
}
|
149 |
+
},
|
150 |
+
generate: (prompt: string, presetName: PresetName, layoutName: LayoutName) => {
|
151 |
+
const layout = layoutName === "random"
|
152 |
+
? getRandomLayoutName()
|
153 |
+
: layoutName
|
154 |
+
set({
|
155 |
+
prompt,
|
156 |
+
panels: [],
|
157 |
+
captions: {},
|
158 |
+
preset: presetName === "random"
|
159 |
+
? getRandomPreset()
|
160 |
+
: getPreset(presetName),
|
161 |
+
layout,
|
162 |
+
layouts: [layout, layout],
|
163 |
+
})
|
164 |
}
|
165 |
}))
|
src/lib/computePercentage.ts
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export function computePercentage(input: string | number) {
|
2 |
+
// TODO something
|
3 |
+
return 0
|
4 |
+
}
|
src/lib/loadImageToCanvas.ts
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export async function loadImageToCanvas(imageBase64: string): Promise<HTMLCanvasElement> {
|
2 |
+
return new Promise((resolve, reject) => {
|
3 |
+
// create a new image object
|
4 |
+
let img = new Image();
|
5 |
+
// specify a function to run when the image is fully loaded
|
6 |
+
img.onload = () => {
|
7 |
+
// create a canvas element
|
8 |
+
let canvas = document.createElement('canvas');
|
9 |
+
canvas.width = img.width;
|
10 |
+
canvas.height = img.height;
|
11 |
+
// get the context of the canvas
|
12 |
+
let ctx = canvas.getContext('2d');
|
13 |
+
if (ctx) {
|
14 |
+
// draw the image into the canvas
|
15 |
+
ctx.drawImage(img, 0, 0);
|
16 |
+
// resolve the promise with the canvas
|
17 |
+
resolve(canvas);
|
18 |
+
} else {
|
19 |
+
reject('Error creating the context of canvas');
|
20 |
+
}
|
21 |
+
};
|
22 |
+
// specify a function to run when the image could not be loaded
|
23 |
+
img.onerror = () => {
|
24 |
+
reject('Image could not be loaded');
|
25 |
+
};
|
26 |
+
img.src = imageBase64; // must be a data;image/.... prefixed URL string
|
27 |
+
});
|
28 |
+
}
|
src/lib/replaceTextInSpeechBubbles.ts
ADDED
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { createWorker } from "tesseract.js"
|
4 |
+
import { loadImageToCanvas } from "./loadImageToCanvas";
|
5 |
+
|
6 |
+
export async function replaceTextInSpeechBubbles(image: string, customText: string) {
|
7 |
+
console.log('creating OCR worker to find bubbles inside', image);
|
8 |
+
|
9 |
+
const worker = await createWorker({
|
10 |
+
logger: (info) => {
|
11 |
+
console.log(info)
|
12 |
+
},
|
13 |
+
});
|
14 |
+
|
15 |
+
const canvas = await loadImageToCanvas(image)
|
16 |
+
|
17 |
+
const ctx = canvas.getContext('2d')!;
|
18 |
+
|
19 |
+
try {
|
20 |
+
await worker.load();
|
21 |
+
await worker.loadLanguage('eng');
|
22 |
+
await worker.initialize('eng');
|
23 |
+
|
24 |
+
const { data } = await worker.recognize(canvas);
|
25 |
+
const lines = data.lines || [];
|
26 |
+
|
27 |
+
// Draw the lines on the image
|
28 |
+
ctx.fillStyle = "white";
|
29 |
+
|
30 |
+
lines.forEach((line) => {
|
31 |
+
ctx.fillRect(line.bbox.x0, line.bbox.y0, line.bbox.x1 - line.bbox.x0, line.bbox.y1 - line.bbox.y0);
|
32 |
+
|
33 |
+
const bubbleWidth = line.bbox.x1 - line.bbox.x0;
|
34 |
+
const bubbleHeight = line.bbox.y1 - line.bbox.y0;
|
35 |
+
let fontSize = 18;
|
36 |
+
ctx.font = `${fontSize}px Arial`;
|
37 |
+
|
38 |
+
/*
|
39 |
+
while (
|
40 |
+
ctx.measureText(customText).width > bubbleWidth || fontSize * 1.2 // line height
|
41 |
+
> bubbleHeight) {
|
42 |
+
fontSize -= 1;
|
43 |
+
ctx.font = `${fontSize}px Arial`;
|
44 |
+
}
|
45 |
+
|
46 |
+
const lines = wrapText(ctx, customText, line.bbox.x0, line.bbox.y0, bubbleWidth, fontSize);
|
47 |
+
|
48 |
+
ctx.fillStyle = "black";
|
49 |
+
lines.forEach((text, i) => {
|
50 |
+
ctx.fillText(text, line.bbox.x0, line.bbox.y0 + (i * fontSize * 1.2));
|
51 |
+
});
|
52 |
+
*/
|
53 |
+
})
|
54 |
+
|
55 |
+
await worker.terminate();
|
56 |
+
|
57 |
+
// Convert the Canvas to image data
|
58 |
+
const imgAsDataURL = canvas.toDataURL('image/png');
|
59 |
+
|
60 |
+
if (typeof window !== "undefined") {
|
61 |
+
const foo = (window as any)
|
62 |
+
if (!foo.debugJujul) {
|
63 |
+
foo.debugJujul = []
|
64 |
+
}
|
65 |
+
foo.debugJujul.push({
|
66 |
+
lines
|
67 |
+
})
|
68 |
+
}
|
69 |
+
console.log("lines:", lines)
|
70 |
+
|
71 |
+
return imgAsDataURL;
|
72 |
+
|
73 |
+
} catch (err) {
|
74 |
+
console.error(err);
|
75 |
+
}
|
76 |
+
return "";
|
77 |
+
}
|
78 |
+
|
79 |
+
function wrapText(context: CanvasRenderingContext2D, text: string, x: number, y: number, maxWidth: number, lineHeight: number) {
|
80 |
+
const words = text.split(' ');
|
81 |
+
let line = '';
|
82 |
+
const lines = [];
|
83 |
+
|
84 |
+
for(let n = 0; n < words.length; n++) {
|
85 |
+
let testLine = line + words[n] + ' ';
|
86 |
+
let metrics = context.measureText(testLine);
|
87 |
+
let testWidth = metrics.width;
|
88 |
+
if (testWidth > maxWidth && n > 0) {
|
89 |
+
lines.push(line);
|
90 |
+
line = words[n] + ' ';
|
91 |
+
}
|
92 |
+
else {
|
93 |
+
line = testLine;
|
94 |
+
}
|
95 |
+
}
|
96 |
+
lines.push(line);
|
97 |
+
return lines;
|
98 |
+
}
|
src/lib/writeIntoBubble.ts
DELETED
@@ -1,115 +0,0 @@
|
|
1 |
-
/*
|
2 |
-
I have a PNG image which contains a colored shape (roughly in the shape of a speech bubble), surrounded by white
|
3 |
-
|
4 |
-
Please write a TypeScript function (it should work in the browser) to:
|
5 |
-
|
6 |
-
1. replace all the white pixels with a transparent PNG pixel
|
7 |
-
2. replace all the colored pixels with a white pixel
|
8 |
-
3. write some input text into the colored shape
|
9 |
-
4. Make sure line returns are handled
|
10 |
-
5. It should have some padding (eg. 20px)
|
11 |
-
6. use Comic Sans MS
|
12 |
-
|
13 |
-
You can use the canvas for your operation. The signature should be something like:
|
14 |
-
|
15 |
-
- Please adjust the font size, based on the available number of pixels inside the bubble, taking some margin into account.
|
16 |
-
- The text should not be below 8px
|
17 |
-
- If there is not enough room to display it without going outside the shape, then crop the text.
|
18 |
-
- in other words, NEVER write outside the shape!
|
19 |
-
|
20 |
-
The function should be something like:
|
21 |
-
|
22 |
-
writeIntoBubble(image: string, text: string): Promise<string>
|
23 |
-
*/
|
24 |
-
|
25 |
-
export async function writeIntoBubble(image: string, text: string): Promise<string> {
|
26 |
-
const padding = 20; // Pixels
|
27 |
-
return new Promise((resolve, reject) => {
|
28 |
-
const img = new Image();
|
29 |
-
img.onload = () => {
|
30 |
-
const physicalWidth = img.width;
|
31 |
-
const physicalHeight = img.height;
|
32 |
-
const canvas = document.createElement('canvas');
|
33 |
-
const ctx = canvas.getContext('2d');
|
34 |
-
if (!ctx) {
|
35 |
-
reject('Unable to get canvas context');
|
36 |
-
return;
|
37 |
-
}
|
38 |
-
canvas.width = physicalWidth;
|
39 |
-
canvas.height = physicalHeight;
|
40 |
-
ctx.drawImage(img, 0, 0, physicalWidth, physicalHeight);
|
41 |
-
|
42 |
-
const imageData = ctx.getImageData(0, 0, physicalWidth, physicalHeight);
|
43 |
-
const data = imageData.data;
|
44 |
-
|
45 |
-
let minX = physicalWidth, minY = physicalHeight, maxX = 0, maxY = 0;
|
46 |
-
|
47 |
-
for (let y = 0; y < physicalHeight; y++) {
|
48 |
-
for (let x = 0; x < physicalWidth; x++) {
|
49 |
-
const i = (y * physicalWidth + x) * 4;
|
50 |
-
if (data[i] !== 255 || data[i + 1] !== 255 || data[i + 2] !== 255) {
|
51 |
-
minX = Math.min(minX, x);
|
52 |
-
minY = Math.min(minY, y);
|
53 |
-
maxX = Math.max(maxX, x);
|
54 |
-
maxY = Math.max(maxY, y);
|
55 |
-
data[i] = data[i + 1] = data[i + 2] = 255;
|
56 |
-
data[i + 3] = 255;
|
57 |
-
} else {
|
58 |
-
data[i + 3] = 0;
|
59 |
-
}
|
60 |
-
}
|
61 |
-
}
|
62 |
-
|
63 |
-
ctx.putImageData(imageData, 0, 0);
|
64 |
-
|
65 |
-
ctx.save();
|
66 |
-
ctx.setTransform(1, 0, 0, 1, 0, 0); // Reset transforms to handle padding correctly
|
67 |
-
|
68 |
-
const textX = minX + padding;
|
69 |
-
const textY = minY + padding;
|
70 |
-
const textWidth = (maxX - minX) - 2 * padding;
|
71 |
-
const textHeight = (maxY - minY) - 2 * padding;
|
72 |
-
|
73 |
-
ctx.restore();
|
74 |
-
|
75 |
-
ctx.rect(textX, textY, textWidth, textHeight);
|
76 |
-
ctx.clip(); // Clip outside of the region
|
77 |
-
|
78 |
-
let fontSize = 20; // Start with a large size
|
79 |
-
let lines = [];
|
80 |
-
do {
|
81 |
-
ctx.font = `${fontSize}px Comic Sans MS`;
|
82 |
-
lines = wrapText(ctx, text, textWidth);
|
83 |
-
fontSize -= 2; // Reduce size and try again if text doesn't fit
|
84 |
-
} while(lines.length > textHeight / fontSize && fontSize > 8);
|
85 |
-
ctx.font = `${fontSize}px Comic Sans MS`;
|
86 |
-
|
87 |
-
lines.forEach((line, i) => ctx.fillText(line, textX, textY + padding + i * fontSize));
|
88 |
-
|
89 |
-
resolve(canvas.toDataURL());
|
90 |
-
};
|
91 |
-
img.onerror = reject;
|
92 |
-
img.src = image;
|
93 |
-
});
|
94 |
-
}
|
95 |
-
|
96 |
-
// Function to wrap text into lines that fit inside a specified width
|
97 |
-
function wrapText(context: CanvasRenderingContext2D, text: string, maxWidth: number): string[] {
|
98 |
-
const words = text.split(' ');
|
99 |
-
const lines = [];
|
100 |
-
let line = '';
|
101 |
-
|
102 |
-
for (let n = 0; n < words.length; n++) {
|
103 |
-
const testLine = line + words[n] + ' ';
|
104 |
-
const metrics = context.measureText(testLine);
|
105 |
-
const testWidth = metrics.width;
|
106 |
-
if (testWidth > maxWidth && n > 0) {
|
107 |
-
lines.push(line);
|
108 |
-
line = words[n] + ' ';
|
109 |
-
} else {
|
110 |
-
line = testLine;
|
111 |
-
}
|
112 |
-
}
|
113 |
-
lines.push(line);
|
114 |
-
return lines;
|
115 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/lib/writeIntoBubbles.ts
DELETED
@@ -1,65 +0,0 @@
|
|
1 |
-
import { loadImage } from "./loadImage"
|
2 |
-
import { writeIntoBubble } from "./writeIntoBubble"
|
3 |
-
|
4 |
-
export async function writeIntoBubbles(image: string, texts: string[]): Promise<string> {
|
5 |
-
const loadImg = await loadImage(image);
|
6 |
-
|
7 |
-
const canvas = document.createElement('canvas');
|
8 |
-
const context = canvas.getContext('2d');
|
9 |
-
|
10 |
-
canvas.width = loadImg.width;
|
11 |
-
canvas.height = loadImg.height;
|
12 |
-
context?.drawImage(loadImg, 0, 0, loadImg.width, loadImg.height);
|
13 |
-
|
14 |
-
const untouchedImageData = context?.getImageData(0, 0, loadImg.width, loadImg.height);
|
15 |
-
if (!untouchedImageData) {
|
16 |
-
throw new Error("untouchedImageData is invalid")
|
17 |
-
}
|
18 |
-
const colorSet = new Set<string>(); // This is the unique color container
|
19 |
-
|
20 |
-
for(let i = 0; i < untouchedImageData?.data.length; i += 4){
|
21 |
-
const r = untouchedImageData?.data[i];
|
22 |
-
const g = untouchedImageData?.data[i+1];
|
23 |
-
const b = untouchedImageData?.data[i+2];
|
24 |
-
const colorString = `rgb(${r},${g},${b})`;
|
25 |
-
|
26 |
-
if(!colorSet.has(colorString)){
|
27 |
-
colorSet.add(colorString);
|
28 |
-
var newCanvas = document.createElement('canvas');
|
29 |
-
newCanvas.width = loadImg.width;
|
30 |
-
newCanvas.height = loadImg.height;
|
31 |
-
|
32 |
-
var newContext = newCanvas.getContext('2d');
|
33 |
-
newContext?.drawImage(loadImg, 0, 0, loadImg.width, loadImg.height);
|
34 |
-
var newImageData = newContext?.getImageData(0, 0, loadImg.width, loadImg.height);
|
35 |
-
if (!newImageData) {
|
36 |
-
throw new Error("newImageData is invalid")
|
37 |
-
}
|
38 |
-
|
39 |
-
for(let j = 0; j < newImageData?.data.length; j += 4){
|
40 |
-
const _r = newImageData?.data[j];
|
41 |
-
const _g = newImageData?.data[j+1];
|
42 |
-
const _b = newImageData?.data[j+2];
|
43 |
-
const _colorString = `rgb(${_r},${_g},${_b})`;
|
44 |
-
|
45 |
-
if(_colorString !== colorString){
|
46 |
-
newImageData?.data.set([0,0,0,0], j);
|
47 |
-
}
|
48 |
-
}
|
49 |
-
|
50 |
-
newContext?.putImageData(newImageData as ImageData, 0, 0);
|
51 |
-
|
52 |
-
let imageBase64 = newCanvas.toDataURL();
|
53 |
-
|
54 |
-
if(texts.length > 0){
|
55 |
-
let text = texts.shift() as string;
|
56 |
-
if (imageBase64 != '') {
|
57 |
-
const processedBase64 = await writeIntoBubble(imageBase64, text);
|
58 |
-
const newImg = await loadImage(processedBase64);
|
59 |
-
context?.drawImage(newImg, 0, 0, loadImg.width, loadImg.height);
|
60 |
-
}
|
61 |
-
}
|
62 |
-
}
|
63 |
-
}
|
64 |
-
return canvas.toDataURL();
|
65 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|