jbilcke-hf HF staff commited on
Commit
797cbb8
1 Parent(s): 8f617f6

working to allow ppl to pick the layout

Browse files
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
- milou: {
 
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 = "japanese_manga"
 
 
389
 
390
- export const getPreset = (preset?: PresetName): Preset => presets[preset || defaultPreset] || presets[defaultPreset]
 
 
 
 
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
- import { see } from "@/app/engine/caption"
15
- import { writeIntoBubble } from "@/lib/writeIntoBubble"
16
- // import { Bubble } from "./bubble"
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 transformMask = async () => {
197
- if (rendered.maskUrl) {
198
- const imgSrc = await writeIntoBubble(
199
- rendered.maskUrl,
200
- "LOREM IPSUM! Dolor sit amet.."
201
- )
202
- setNewMask(imgSrc)
 
 
 
 
203
  }
204
  }
205
- transformMask()
206
- }, [rendered.maskUrl])
 
 
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 && <img
228
- src={rendered.assetUrl}
 
 
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
- export function TopMenu() {
21
- const font = useStore(state => state.font)
22
- const setFont = useStore(state => state.setFont)
 
 
23
 
24
- const preset = useStore(state => state.preset)
25
- const setPreset = useStore(state => state.setPreset)
 
 
 
 
26
 
 
 
 
 
27
  const prompt = useStore(state => state.prompt)
28
- const setPrompt = useStore(state => state.setPrompt)
 
 
 
29
 
30
  const isGeneratingStory = useStore(state => state.isGeneratingStory)
31
  const atLeastOnePanelIsBusy = useStore(state => state.atLeastOnePanelIsBusy)
32
  const isBusy = isGeneratingStory || atLeastOnePanelIsBusy
33
 
34
- const [draft, setDraft] = useState("")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- <div className={cn(
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
- <Label className="flex text-sm md:w-24">Preset:</Label>
 
 
52
  <Select
53
  defaultValue={defaultPreset}
54
- onValueChange={(value) => { setPreset(getPreset(value as FontName)) }}
55
  disabled={isBusy}
56
  >
57
  <SelectTrigger className="flex-grow">
58
- <SelectValue className="text-sm" placeholder="Type" />
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
- setDraft(e.target.value)
103
  }}
104
  onKeyDown={({ key }) => {
105
- if (isBusy) {
106
- return
107
- }
108
  if (key === 'Enter') {
109
- if (draft.trim() !== prompt.trim()) {
110
- setPrompt(draft.trim())
111
- }
112
  }
113
  }}
114
- value={draft}
115
  />
116
  <Button
117
  onClick={() => {
118
- if (isBusy) {
119
- return
120
- }
121
- if (draft.trim() !== prompt.trim()) {
122
- setPrompt(draft.trim())
123
- }
124
  }}
125
- disabled={!draft?.trim().length || isBusy}
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 Layout2() {
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 Layout3() {
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 Layout4() {
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 Layout5() {
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 Layout6() {
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, Layout2, Layout3, Layout4, Layout5, Layout6 }
221
- export const allLayouts = {
 
 
222
  Layout1,
223
- Layout5,
224
- Layout6
 
 
 
 
 
 
 
 
225
  }
226
 
227
  export type LayoutName = keyof typeof allLayouts
228
 
229
- export function getRandomLayoutName(): LayoutName {
230
- return pick(Object.keys(allLayouts) as LayoutName[]) as LayoutName
 
 
 
 
 
 
231
  }
232
 
233
  export function getRandomLayoutNames(): LayoutName[] {
234
- return Object.keys(allLayouts).sort(() => Math.random() - 0.5) as LayoutName[]
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, useRef, useState, useTransition } from "react"
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 { FontName, defaultFont, fonts } from "@/lib/fonts"
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
- }