jbilcke-hf HF staff commited on
Commit
dbc8f44
1 Parent(s): c985fd8
package-lock.json CHANGED
@@ -39,6 +39,7 @@
39
  "date-fns": "^2.30.0",
40
  "eslint": "8.45.0",
41
  "eslint-config-next": "13.4.10",
 
42
  "lucide-react": "^0.260.0",
43
  "next": "13.4.10",
44
  "pick": "^0.0.1",
@@ -4200,6 +4201,14 @@
4200
  "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
4201
  "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
4202
  },
 
 
 
 
 
 
 
 
4203
  "node_modules/base64-js": {
4204
  "version": "1.5.1",
4205
  "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -4820,6 +4829,14 @@
4820
  "node": ">=4"
4821
  }
4822
  },
 
 
 
 
 
 
 
 
4823
  "node_modules/css-to-react-native": {
4824
  "version": "3.2.0",
4825
  "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
@@ -6026,6 +6043,18 @@
6026
  "resolved": "https://registry.npmjs.org/hsl-to-rgb-for-reals/-/hsl-to-rgb-for-reals-1.1.1.tgz",
6027
  "integrity": "sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg=="
6028
  },
 
 
 
 
 
 
 
 
 
 
 
 
6029
  "node_modules/htmlparser2": {
6030
  "version": "8.0.2",
6031
  "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
@@ -8109,6 +8138,14 @@
8109
  "node": ">=6"
8110
  }
8111
  },
 
 
 
 
 
 
 
 
8112
  "node_modules/text-table": {
8113
  "version": "0.2.0",
8114
  "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -8532,6 +8569,14 @@
8532
  "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
8533
  "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
8534
  },
 
 
 
 
 
 
 
 
8535
  "node_modules/uuid": {
8536
  "version": "9.0.0",
8537
  "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
 
39
  "date-fns": "^2.30.0",
40
  "eslint": "8.45.0",
41
  "eslint-config-next": "13.4.10",
42
+ "html2canvas": "^1.4.1",
43
  "lucide-react": "^0.260.0",
44
  "next": "13.4.10",
45
  "pick": "^0.0.1",
 
4201
  "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
4202
  "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
4203
  },
4204
+ "node_modules/base64-arraybuffer": {
4205
+ "version": "1.0.2",
4206
+ "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
4207
+ "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
4208
+ "engines": {
4209
+ "node": ">= 0.6.0"
4210
+ }
4211
+ },
4212
  "node_modules/base64-js": {
4213
  "version": "1.5.1",
4214
  "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
 
4829
  "node": ">=4"
4830
  }
4831
  },
4832
+ "node_modules/css-line-break": {
4833
+ "version": "2.1.0",
4834
+ "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
4835
+ "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
4836
+ "dependencies": {
4837
+ "utrie": "^1.0.2"
4838
+ }
4839
+ },
4840
  "node_modules/css-to-react-native": {
4841
  "version": "3.2.0",
4842
  "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
 
6043
  "resolved": "https://registry.npmjs.org/hsl-to-rgb-for-reals/-/hsl-to-rgb-for-reals-1.1.1.tgz",
6044
  "integrity": "sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg=="
6045
  },
6046
+ "node_modules/html2canvas": {
6047
+ "version": "1.4.1",
6048
+ "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
6049
+ "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
6050
+ "dependencies": {
6051
+ "css-line-break": "^2.1.0",
6052
+ "text-segmentation": "^1.0.3"
6053
+ },
6054
+ "engines": {
6055
+ "node": ">=8.0.0"
6056
+ }
6057
+ },
6058
  "node_modules/htmlparser2": {
6059
  "version": "8.0.2",
6060
  "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
 
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",
8144
+ "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
8145
+ "dependencies": {
8146
+ "utrie": "^1.0.2"
8147
+ }
8148
+ },
8149
  "node_modules/text-table": {
8150
  "version": "0.2.0",
8151
  "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
 
8569
  "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
8570
  "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
8571
  },
8572
+ "node_modules/utrie": {
8573
+ "version": "1.0.2",
8574
+ "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
8575
+ "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
8576
+ "dependencies": {
8577
+ "base64-arraybuffer": "^1.0.2"
8578
+ }
8579
+ },
8580
  "node_modules/uuid": {
8581
  "version": "9.0.0",
8582
  "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
package.json CHANGED
@@ -40,6 +40,7 @@
40
  "date-fns": "^2.30.0",
41
  "eslint": "8.45.0",
42
  "eslint-config-next": "13.4.10",
 
43
  "lucide-react": "^0.260.0",
44
  "next": "13.4.10",
45
  "pick": "^0.0.1",
 
40
  "date-fns": "^2.30.0",
41
  "eslint": "8.45.0",
42
  "eslint-config-next": "13.4.10",
43
+ "html2canvas": "^1.4.1",
44
  "lucide-react": "^0.260.0",
45
  "next": "13.4.10",
46
  "pick": "^0.0.1",
src/app/interface/bottom-bar/index.tsx ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useStore } from "@/app/store"
2
+ import { Button } from "@/components/ui/button"
3
+ import { cn } from "@/lib/utils"
4
+
5
+ export function BottomBar() {
6
+ const download = useStore(state => state.download)
7
+ const isGeneratingStory = useStore(state => state.isGeneratingStory)
8
+ const prompt = useStore(state => state.prompt)
9
+ const panelGenerationStatus = useStore(state => state.panelGenerationStatus)
10
+
11
+ const remainingImages = Object.values(panelGenerationStatus).reduce((acc, s) => (acc + (s ? 1 : 0)), 0)
12
+
13
+ return (
14
+ <div className={cn(
15
+ `fixed bottom-8 right-8`,
16
+ `flex flex-row`,
17
+ `animation-all duration-300 ease-in-out`,
18
+ isGeneratingStory ? `scale-0 opacity-0` : ``,
19
+ )}>
20
+ <div>
21
+ <Button onClick={download} disabled={!prompt?.length}>{
22
+ remainingImages ? `${remainingImages} remaining..` : `Download`
23
+ }</Button>
24
+ </div>
25
+ </div>
26
+ )
27
+ }
src/app/interface/grid/index.tsx CHANGED
@@ -7,6 +7,7 @@ import { useStore } from "@/app/store"
7
 
8
  export function Grid({ children, className }: { children: ReactNode; className: string }) {
9
  const zoomLevel = useStore(state => state.zoomLevel)
 
10
  return (
11
  <div
12
  // the "fixed" width ensure our comic keeps a consistent ratio
@@ -23,4 +24,3 @@ export function Grid({ children, className }: { children: ReactNode; className:
23
  )
24
  }
25
 
26
-
 
7
 
8
  export function Grid({ children, className }: { children: ReactNode; className: string }) {
9
  const zoomLevel = useStore(state => state.zoomLevel)
10
+
11
  return (
12
  <div
13
  // the "fixed" width ensure our comic keeps a consistent ratio
 
24
  )
25
  }
26
 
 
src/app/interface/zoom/index.tsx CHANGED
@@ -5,11 +5,14 @@ import { cn } from "@/lib/utils"
5
  export function Zoom() {
6
  const zoomLevel = useStore((state) => state.zoomLevel)
7
  const setZoomLevel = useStore((state) => state.setZoomLevel)
 
8
 
9
  return (
10
  <div className={cn(
11
  // `fixed flex items-center justify-center bottom-8 top-32 right-8 z-10 h-screen`,
12
- `fixed flex flex-col items-center bottom-8 top-32 md:top-20 right-6 z-10`
 
 
13
  )}>
14
  <div className="font-mono text-xs pb-1 text-stone-700">
15
  Zoom
 
5
  export function Zoom() {
6
  const zoomLevel = useStore((state) => state.zoomLevel)
7
  const setZoomLevel = useStore((state) => state.setZoomLevel)
8
+ const isGeneratingStory = useStore((state) => state.isGeneratingStory)
9
 
10
  return (
11
  <div className={cn(
12
  // `fixed flex items-center justify-center bottom-8 top-32 right-8 z-10 h-screen`,
13
+ `fixed flex flex-col items-center bottom-8 top-32 md:top-20 right-6 z-10`,
14
+ `animation-all duration-300 ease-in-out`,
15
+ isGeneratingStory ? `scale-0 opacity-0` : ``,
16
  )}>
17
  <div className="font-mono text-xs pb-1 text-stone-700">
18
  Zoom
src/app/main.tsx CHANGED
@@ -1,6 +1,6 @@
1
  "use client"
2
 
3
- import { useEffect, useTransition } from "react"
4
  import { useSearchParams } from "next/navigation"
5
 
6
  import { PresetName, defaultPreset, getPreset } from "@/app/engine/presets"
@@ -12,6 +12,7 @@ import { getRandomLayoutName, layouts } from "./layouts"
12
  import { useStore } from "./store"
13
  import { Zoom } from "./interface/zoom"
14
  import { getStory } from "./queries/getStory"
 
15
 
16
  export default function Main() {
17
  const [_isPending, startTransition] = useTransition()
@@ -40,6 +41,15 @@ export default function Main() {
40
 
41
  const zoomLevel = useStore(state => state.zoomLevel)
42
 
 
 
 
 
 
 
 
 
 
43
  // react to URL params
44
  useEffect(() => {
45
  if (requestedPreset && requestedPreset !== preset.label) { setPreset(getPreset(requestedPreset)) }
@@ -103,7 +113,7 @@ export default function Main() {
103
  )}>
104
  <div className="flex flex-col items-center w-full">
105
  <div
106
-
107
  className={cn(
108
  `flex flex-col items-center justify-start`,
109
 
@@ -126,6 +136,7 @@ export default function Main() {
126
  </div>
127
  </div>
128
  <Zoom />
 
129
  <div className={cn(
130
  `z-20 fixed inset-0`,
131
  `flex flex-row items-center justify-center`,
 
1
  "use client"
2
 
3
+ import { useEffect, useRef, useTransition } from "react"
4
  import { useSearchParams } from "next/navigation"
5
 
6
  import { PresetName, defaultPreset, getPreset } from "@/app/engine/presets"
 
12
  import { useStore } from "./store"
13
  import { Zoom } from "./interface/zoom"
14
  import { getStory } from "./queries/getStory"
15
+ import { BottomBar } from "./interface/bottom-bar"
16
 
17
  export default function Main() {
18
  const [_isPending, startTransition] = useTransition()
 
41
 
42
  const zoomLevel = useStore(state => state.zoomLevel)
43
 
44
+ const setPage = useStore(state => state.setPage)
45
+ const pageRef = useRef<HTMLDivElement>(null)
46
+
47
+ useEffect(() => {
48
+ const element = pageRef.current
49
+ if (!element) { return }
50
+ setPage(element)
51
+ }, [pageRef.current])
52
+
53
  // react to URL params
54
  useEffect(() => {
55
  if (requestedPreset && requestedPreset !== preset.label) { setPreset(getPreset(requestedPreset)) }
 
113
  )}>
114
  <div className="flex flex-col items-center w-full">
115
  <div
116
+ ref={pageRef}
117
  className={cn(
118
  `flex flex-col items-center justify-start`,
119
 
 
136
  </div>
137
  </div>
138
  <Zoom />
139
+ <BottomBar />
140
  <div className={cn(
141
  `z-20 fixed inset-0`,
142
  `flex flex-row items-center justify-center`,
src/app/store/index.ts CHANGED
@@ -5,6 +5,7 @@ import { create } from "zustand"
5
  import { FontName } from "@/lib/fonts"
6
  import { Preset, getPreset } from "@/app/engine/presets"
7
  import { LayoutName, getRandomLayoutName } from "../layouts"
 
8
 
9
  export const useStore = create<{
10
  prompt: string
@@ -14,6 +15,7 @@ export const useStore = create<{
14
  captions: Record<string, string>
15
  layout: LayoutName
16
  zoomLevel: number
 
17
  isGeneratingStory: boolean
18
  panelGenerationStatus: Record<number, boolean>
19
  isGeneratingText: boolean
@@ -25,9 +27,11 @@ export const useStore = create<{
25
  setLayout: (layout: LayoutName) => void
26
  setCaption: (panelId: number, caption: string) => void
27
  setZoomLevel: (zoomLevel: number) => void
 
28
  setGeneratingStory: (isGeneratingStory: boolean) => void
29
  setGeneratingImages: (panelId: number, value: boolean) => void
30
  setGeneratingText: (isGeneratingText: boolean) => void
 
31
  }>((set, get) => ({
32
  prompt: "",
33
  font: "actionman",
@@ -36,6 +40,7 @@ export const useStore = create<{
36
  captions: {},
37
  layout: getRandomLayoutName(),
38
  zoomLevel: 50,
 
39
  isGeneratingStory: false,
40
  panelGenerationStatus: {},
41
  isGeneratingText: false,
@@ -81,6 +86,10 @@ export const useStore = create<{
81
  },
82
  setLayout: (layout: LayoutName) => set({ layout }),
83
  setZoomLevel: (zoomLevel: number) => set({ zoomLevel }),
 
 
 
 
84
  setGeneratingStory: (isGeneratingStory: boolean) => set({ isGeneratingStory }),
85
  setGeneratingImages: (panelId: number, value: boolean) => {
86
 
@@ -97,4 +106,26 @@ export const useStore = create<{
97
  })
98
  },
99
  setGeneratingText: (isGeneratingText: boolean) => set({ isGeneratingText }),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  }))
 
5
  import { FontName } from "@/lib/fonts"
6
  import { Preset, getPreset } from "@/app/engine/presets"
7
  import { LayoutName, getRandomLayoutName } from "../layouts"
8
+ import html2canvas from "html2canvas"
9
 
10
  export const useStore = create<{
11
  prompt: string
 
15
  captions: Record<string, string>
16
  layout: LayoutName
17
  zoomLevel: number
18
+ page: HTMLDivElement
19
  isGeneratingStory: boolean
20
  panelGenerationStatus: Record<number, boolean>
21
  isGeneratingText: boolean
 
27
  setLayout: (layout: LayoutName) => void
28
  setCaption: (panelId: number, caption: string) => void
29
  setZoomLevel: (zoomLevel: number) => void
30
+ setPage: (page: HTMLDivElement) => void
31
  setGeneratingStory: (isGeneratingStory: boolean) => void
32
  setGeneratingImages: (panelId: number, value: boolean) => void
33
  setGeneratingText: (isGeneratingText: boolean) => void
34
+ download: () => void
35
  }>((set, get) => ({
36
  prompt: "",
37
  font: "actionman",
 
40
  captions: {},
41
  layout: getRandomLayoutName(),
42
  zoomLevel: 50,
43
+ page: undefined as unknown as HTMLDivElement,
44
  isGeneratingStory: false,
45
  panelGenerationStatus: {},
46
  isGeneratingText: false,
 
86
  },
87
  setLayout: (layout: LayoutName) => set({ layout }),
88
  setZoomLevel: (zoomLevel: number) => set({ zoomLevel }),
89
+ setPage: (page: HTMLDivElement) => {
90
+ if (!page) { return }
91
+ set({ page })
92
+ },
93
  setGeneratingStory: (isGeneratingStory: boolean) => set({ isGeneratingStory }),
94
  setGeneratingImages: (panelId: number, value: boolean) => {
95
 
 
106
  })
107
  },
108
  setGeneratingText: (isGeneratingText: boolean) => set({ isGeneratingText }),
109
+ download: async () => {
110
+ console.log("download called!")
111
+ const { page } = get()
112
+ console.log("page:", page)
113
+ if (!page) { return }
114
+
115
+ const canvas = await html2canvas(page)
116
+ console.log("canvas:", canvas)
117
+
118
+ const data = canvas.toDataURL('image/jpg')
119
+ const link = document.createElement('a')
120
+
121
+ if (typeof link.download === 'string') {
122
+ link.href = data
123
+ link.download = 'comic.jpg'
124
+ document.body.appendChild(link)
125
+ link.click()
126
+ document.body.removeChild(link)
127
+ } else {
128
+ window.open(data)
129
+ }
130
+ }
131
  }))