jbilcke-hf HF staff commited on
Commit
2e12a66
1 Parent(s): 1cb278e

show speech bubbles by default

Browse files
src/app/interface/about/index.tsx CHANGED
@@ -28,7 +28,7 @@ export function About() {
28
  <DialogTrigger asChild>
29
  <Button variant="outline">
30
  <span className="hidden md:inline">About</span>
31
- <span className="inline md:hidden">About</span>
32
  </Button>
33
  </DialogTrigger>
34
  <DialogContent className="w-full sm:max-w-[500px] md:max-w-[600px] overflow-y-scroll h-[100vh] sm:h-[550px]">
 
28
  <DialogTrigger asChild>
29
  <Button variant="outline">
30
  <span className="hidden md:inline">About</span>
31
+ <span className="inline md:hidden">?</span>
32
  </Button>
33
  </DialogTrigger>
34
  <DialogContent className="w-full sm:max-w-[500px] md:max-w-[600px] overflow-y-scroll h-[100vh] sm:h-[550px]">
src/app/interface/advert/index.tsx CHANGED
@@ -9,7 +9,7 @@ export function Advert() {
9
  window.open("https://huggingface.co/spaces/jbilcke-hf/ai-stories-factory", "_blank")
10
  }}>
11
  <span className="hidden md:inline">Make AI stories</span>
12
- <span className="inline md:hidden">Make AI stories</span>
13
  </Button>
14
  )
15
  }
 
9
  window.open("https://huggingface.co/spaces/jbilcke-hf/ai-stories-factory", "_blank")
10
  }}>
11
  <span className="hidden md:inline">Make AI stories</span>
12
+ <span className="inline md:hidden">...</span>
13
  </Button>
14
  )
15
  }
src/app/interface/discord/index.tsx CHANGED
@@ -13,7 +13,8 @@ export function Discord() {
13
  href="https://discord.gg/AEruz9B92B"
14
  target="_blank">
15
  <div><FaDiscord size={24} /></div>
16
- <div className="text-sm ml-1.5">Discord</div>
 
17
  </a>
18
  )
19
  }
 
13
  href="https://discord.gg/AEruz9B92B"
14
  target="_blank">
15
  <div><FaDiscord size={24} /></div>
16
+ <span className="text-sm ml-1.5 hidden md:inline">Discord</span>
17
+ <span className="text-sm ml-1.5 inline md:hidden"></span>
18
  </a>
19
  )
20
  }
src/app/interface/top-menu/index.tsx CHANGED
@@ -28,6 +28,7 @@ import { localStorageKeys } from "../settings-dialog/localStorageKeys"
28
  import { defaultSettings } from "../settings-dialog/defaultSettings"
29
  import { AuthWall } from "../auth-wall"
30
  import { SelectLayout } from "../select-layout"
 
31
 
32
  export function TopMenu() {
33
  const searchParams = useSearchParams()
@@ -68,6 +69,7 @@ export function TopMenu() {
68
  requestedStoryPrompt
69
  )
70
 
 
71
  // TODO should be in the store
72
  const [draftPromptA, setDraftPromptA] = useState(lastDraftPromptA)
73
  const [draftPromptB, setDraftPromptB] = useState(lastDraftPromptB)
@@ -91,6 +93,11 @@ export function TopMenu() {
91
  useEffect(() => { if (lastDraftPromptB !== draftPromptB) { setLastDraftPromptB(draftPromptB) } }, [draftPromptB])
92
  useEffect(() => { if (lastDraftPromptB !== draftPromptB) { setDraftPromptB(lastDraftPromptB) } }, [lastDraftPromptB])
93
 
 
 
 
 
 
94
  const handleSubmit = () => {
95
  if (enableOAuthWall && hasGeneratedAtLeastOnce && !isLoggedIn) {
96
  setShowAuthWall(true)
@@ -169,18 +176,19 @@ export function TopMenu() {
169
  onCheckedChange={setShowCaptions}
170
  />
171
  <Label className="text-gray-200 dark:text-gray-200">
172
- <span className="hidden md:inline">Caption</span>
173
- <span className="inline md:hidden">Cap.</span>
174
  </Label>
175
  </div>
176
  <div className="flex flex-row items-center space-x-3">
177
  <Switch
178
  checked={showSpeeches}
179
  onCheckedChange={setShowSpeeches}
 
180
  />
181
  <Label className="text-gray-200 dark:text-gray-200">
182
- <span className="hidden md:inline">Beta</span>
183
- <span className="inline md:hidden">Beta</span>
184
  </Label>
185
  </div>
186
  {/*
 
28
  import { defaultSettings } from "../settings-dialog/defaultSettings"
29
  import { AuthWall } from "../auth-wall"
30
  import { SelectLayout } from "../select-layout"
31
+ import { getLocalStorageShowSpeeches } from "@/lib/getLocalStorageShowSpeeches"
32
 
33
  export function TopMenu() {
34
  const searchParams = useSearchParams()
 
69
  requestedStoryPrompt
70
  )
71
 
72
+
73
  // TODO should be in the store
74
  const [draftPromptA, setDraftPromptA] = useState(lastDraftPromptA)
75
  const [draftPromptB, setDraftPromptB] = useState(lastDraftPromptB)
 
93
  useEffect(() => { if (lastDraftPromptB !== draftPromptB) { setLastDraftPromptB(draftPromptB) } }, [draftPromptB])
94
  useEffect(() => { if (lastDraftPromptB !== draftPromptB) { setDraftPromptB(lastDraftPromptB) } }, [lastDraftPromptB])
95
 
96
+ // we need a use effect to properly read the local storage
97
+ useEffect(() => {
98
+ setShowSpeeches(getLocalStorageShowSpeeches(true))
99
+ }, [])
100
+
101
  const handleSubmit = () => {
102
  if (enableOAuthWall && hasGeneratedAtLeastOnce && !isLoggedIn) {
103
  setShowAuthWall(true)
 
176
  onCheckedChange={setShowCaptions}
177
  />
178
  <Label className="text-gray-200 dark:text-gray-200">
179
+ <span className="hidden lg:inline">📖&nbsp;Captions</span>
180
+ <span className="inline lg:hidden">📖</span>
181
  </Label>
182
  </div>
183
  <div className="flex flex-row items-center space-x-3">
184
  <Switch
185
  checked={showSpeeches}
186
  onCheckedChange={setShowSpeeches}
187
+ defaultChecked={showSpeeches}
188
  />
189
  <Label className="text-gray-200 dark:text-gray-200">
190
+ <span className="hidden lg:inline">💬&nbsp;Bubbles</span>
191
+ <span className="inline lg:hidden">💬</span>
192
  </Label>
193
  </div>
194
  {/*
src/app/queries/getSystemPrompt.ts CHANGED
@@ -19,9 +19,9 @@ export function getSystemPrompt({
19
  }) {
20
  return [
21
  `You are a writer specialized in ${preset.llmPrompt}`,
22
- `Please write detailed drawing instructions and short (2-3 sentences long) speeches and narrator captions for the ${firstNextOrLast} ${nbPanelsToGenerate} panels (out of ${maxNbPanels} in total) of a new story, but keep it open-ended (it will be continued and expanded later). Please make sure each of those ${nbPanelsToGenerate} panels include info about character gender, age, origin, clothes, colors, location, lights, etc. Only generate those ${nbPanelsToGenerate} panels, but take into account the fact the panels are part of a longer story (${maxNbPanels} panels long).`,
23
  `Give your response as a VALID JSON array like this: \`Array<{ panel: number; instructions: string; speech: string; caption: string; }>\`.`,
24
  // `Give your response as Markdown bullet points.`,
25
- `Be brief in the instructions, the speeches and the narrative captions of those ${nbPanelsToGenerate} panels, don't add your own comments. The speech must be captivating, smart, entertaining, usually a sentence or two. Be straight to the point, return JSON and never reply things like "Sure, I can.." etc. Reply using valid JSON!! Important: Write valid JSON!`
26
  ].filter(item => item).join("\n")
27
  }
 
19
  }) {
20
  return [
21
  `You are a writer specialized in ${preset.llmPrompt}`,
22
+ `Please write detailed drawing instructions and short (2-3 sentences long) speeches and narrator captions for the ${firstNextOrLast} ${nbPanelsToGenerate} panels (out of ${maxNbPanels} in total) of a new story, but keep it open-ended (it will be continued and expanded later). Please make sure each of those ${nbPanelsToGenerate} panels include info about character gender, age, origin, clothes, colors, location, lights, etc. Speeches are the dialogues, so they MUST be written in 1st person style. Only generate those ${nbPanelsToGenerate} panels, but take into account the fact the panels are part of a longer story (${maxNbPanels} panels long).`,
23
  `Give your response as a VALID JSON array like this: \`Array<{ panel: number; instructions: string; speech: string; caption: string; }>\`.`,
24
  // `Give your response as Markdown bullet points.`,
25
+ `Be brief in the instructions, the speeches and the narrative captions of those ${nbPanelsToGenerate} panels, don't add your own comments. Write speeces in 1st person style, with intensity, humor etc. The speech must be captivating, smart, entertaining, usually a sentence or two. Be straight to the point, return JSON and never reply things like "Sure, I can.." etc. Reply using valid JSON!! Important: Write valid JSON!`
26
  ].filter(item => item).join("\n")
27
  }
src/app/store/index.ts CHANGED
@@ -12,6 +12,7 @@ import { LayoutName, defaultLayout, getRandomLayoutName } from "../layouts"
12
  import { putTextInInput } from "@/lib/putTextInInput"
13
  import { parsePresetFromPrompts } from "@/lib/parsePresetFromPrompts"
14
  import { parseLayoutFromStoryboards } from "@/lib/parseLayoutFromStoryboards"
 
15
 
16
  export const useStore = create<{
17
  prompt: string
@@ -96,6 +97,11 @@ export const useStore = create<{
96
  loadClap: (blob: Blob) => Promise<void>
97
  downloadClap: () => Promise<void>
98
  }>((set, get) => ({
 
 
 
 
 
99
  prompt:
100
  (getParam("stylePrompt", "") || getParam("storyPrompt", ""))
101
  ? `${getParam("stylePrompt", "")}||${getParam("storyPrompt", "")}`
@@ -117,7 +123,7 @@ export const useStore = create<{
117
  captions: [],
118
  upscaleQueue: {} as Record<string, RenderedScene>,
119
  renderedScenes: {} as Record<string, RenderedScene>,
120
- showSpeeches: getParam("showSpeeches", false),
121
  showCaptions: getParam("showCaptions", false),
122
 
123
  // deprecated?
@@ -301,6 +307,11 @@ export const useStore = create<{
301
  set({
302
  showSpeeches,
303
  })
 
 
 
 
 
304
  },
305
  setPanelSpeech: (newSpeech, index) => {
306
  const { speeches } = get()
 
12
  import { putTextInInput } from "@/lib/putTextInInput"
13
  import { parsePresetFromPrompts } from "@/lib/parsePresetFromPrompts"
14
  import { parseLayoutFromStoryboards } from "@/lib/parseLayoutFromStoryboards"
15
+ import { getLocalStorageShowSpeeches } from "@/lib/getLocalStorageShowSpeeches"
16
 
17
  export const useStore = create<{
18
  prompt: string
 
97
  loadClap: (blob: Blob) => Promise<void>
98
  downloadClap: () => Promise<void>
99
  }>((set, get) => ({
100
+
101
+ // -------- note --------------------------------------------------
102
+ // do not read the local storage in this block, results might be empty
103
+ // ----------------------------------------------------------------
104
+
105
  prompt:
106
  (getParam("stylePrompt", "") || getParam("storyPrompt", ""))
107
  ? `${getParam("stylePrompt", "")}||${getParam("storyPrompt", "")}`
 
123
  captions: [],
124
  upscaleQueue: {} as Record<string, RenderedScene>,
125
  renderedScenes: {} as Record<string, RenderedScene>,
126
+ showSpeeches: true,
127
  showCaptions: getParam("showCaptions", false),
128
 
129
  // deprecated?
 
307
  set({
308
  showSpeeches,
309
  })
310
+ try {
311
+ localStorage.setItem("AI_COMIC_FACTORY_SHOW_SPEECHES", `${showSpeeches || false}`)
312
+ } catch (err) {
313
+ console.error(`failed to persist "showSpeeches" for value "${showSpeeches}"`)
314
+ }
315
  },
316
  setPanelSpeech: (newSpeech, index) => {
317
  const { speeches } = get()
src/lib/bubble/injectSpeechBubbleInTheBackground.ts CHANGED
@@ -59,7 +59,7 @@ export async function injectSpeechBubbleInTheBackground(params: {
59
  if (segmentationResult.categoryMask) {
60
  const mask = segmentationResult.categoryMask.getAsUint8Array();
61
  characterBoundingBox = findCharacterBoundingBox(mask, image.width, image.height);
62
-
63
  if (debug) {
64
  drawSegmentationMask(ctx, mask, image.width, image.height);
65
  }
@@ -208,62 +208,162 @@ function hslToRgb(h: number, s: number, l: number): [number, number, number] {
208
 
209
  function drawSpeechBubble(
210
  ctx: CanvasRenderingContext2D,
211
- location: { x: number, y: number },
212
  text: string,
213
  shape: "oval" | "rectangular" | "cloud" | "thought",
214
  line: "handdrawn" | "straight" | "bubble" | "chaotic",
215
  font: string,
216
  characterBoundingBox: BoundingBox | null,
217
  imageWidth: number,
218
- imageHeight: number
 
219
  ) {
220
- const bubbleWidth = Math.min(300, imageWidth * 0.4);
221
- const bubbleHeight = Math.min(150, imageHeight * 0.3);
222
  const padding = 24;
 
223
 
224
  const fontSize = 20;
225
  ctx.font = `${fontSize}px ${font}`;
226
 
227
- const wrappedText = wrapText(ctx, text, bubbleWidth - padding * 2, fontSize);
 
 
228
  const textDimensions = measureTextDimensions(ctx, wrappedText, fontSize);
229
 
230
- const finalWidth = Math.max(bubbleWidth, textDimensions.width + padding * 2);
231
- const finalHeight = Math.max(bubbleHeight, textDimensions.height + padding * 2);
 
232
 
233
- const bubbleLocation = {
234
- x: Math.max(finalWidth / 2 + padding, Math.min(imageWidth - finalWidth / 2 - padding, location.x)),
235
- y: Math.max(finalHeight / 2 + padding, Math.min(imageHeight - finalHeight / 2 - padding, location.y))
236
- };
237
-
238
- ctx.fillStyle = 'white';
239
-
240
- // let's disable the border for now
241
- ctx.strokeStyle = 'white'; // 'black';
242
- ctx.lineWidth = 2;
243
 
244
  let tailTarget = null;
245
  if (characterBoundingBox) {
246
  tailTarget = {
247
  x: characterBoundingBox.left + characterBoundingBox.width / 2,
248
- y: characterBoundingBox.top + characterBoundingBox.height * 0.2
249
  };
250
  }
251
 
 
 
 
 
252
  ctx.beginPath();
253
  drawBubbleShape(ctx, shape, bubbleLocation, finalWidth, finalHeight, tailTarget);
254
  ctx.fill();
255
  ctx.stroke();
256
 
 
257
  if (tailTarget) {
258
  drawTail(ctx, bubbleLocation, finalWidth, finalHeight, tailTarget, shape);
259
  }
260
 
 
 
 
 
 
 
 
261
  ctx.fillStyle = 'black';
262
  ctx.textAlign = 'center';
263
  ctx.textBaseline = 'middle';
264
  drawFormattedText(ctx, wrappedText, bubbleLocation.x, bubbleLocation.y, finalWidth - padding * 2, fontSize);
265
  }
266
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  function drawBubbleShape(
268
  ctx: CanvasRenderingContext2D,
269
  shape: "oval" | "rectangular" | "cloud" | "thought",
@@ -351,73 +451,6 @@ function drawThoughtBubble(ctx: CanvasRenderingContext2D, location: { x: number,
351
  // The tail for thought bubbles is handled in the drawTail function
352
  }
353
 
354
- function drawTail(
355
- ctx: CanvasRenderingContext2D,
356
- bubbleLocation: { x: number, y: number },
357
- bubbleWidth: number,
358
- bubbleHeight: number,
359
- tailTarget: { x: number, y: number },
360
- shape: string
361
- ) {
362
- // Calculate new maximum length for tail
363
- const bubbleCenterX = bubbleLocation.x;
364
- const bubbleCenterY = bubbleLocation.y;
365
- const maxTailLength = Math.max(bubbleHeight, bubbleWidth) * 0.6;
366
-
367
- const tailBaseWidth = 30; // 50% larger than before
368
- const tailHeight = 30;
369
-
370
- // Calculate the length from bubble center to tail target
371
- const deltaX = tailTarget.x - bubbleCenterX;
372
- const deltaY = tailTarget.y - bubbleCenterY;
373
- const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
374
-
375
- // Normalize the length if it exceeds the max tail length
376
- const limitedDistance = Math.min(distance, maxTailLength);
377
- const tailEndX = bubbleCenterX + (deltaX / distance) * limitedDistance;
378
- const tailEndY = bubbleCenterY + (deltaY / distance) * limitedDistance;
379
-
380
- ctx.beginPath();
381
- ctx.moveTo(bubbleCenterX, bubbleCenterY);
382
-
383
- const controlPoint1 = {
384
- x: bubbleCenterX + deltaX / 3,
385
- y: bubbleCenterY + deltaY / 3
386
- };
387
-
388
- const controlPoint2 = {
389
- x: bubbleCenterX + (deltaX * 2) / 3,
390
- y: bubbleCenterY + (deltaY * 2) / 3
391
- };
392
-
393
- ctx.bezierCurveTo(
394
- controlPoint1.x, controlPoint1.y,
395
- controlPoint2.x, controlPoint2.y,
396
- tailEndX, tailEndY
397
- );
398
-
399
- // Mirror to create the width at base
400
- const mirroredControlPoint1 = {
401
- x: controlPoint1.x + tailBaseWidth / 3,
402
- y: controlPoint1.y
403
- };
404
-
405
- const mirroredControlPoint2 = {
406
- x: controlPoint2.x + (tailBaseWidth * 2) / 3,
407
- y: controlPoint2.y
408
- };
409
-
410
- ctx.bezierCurveTo(
411
- mirroredControlPoint2.x, mirroredControlPoint2.y,
412
- mirroredControlPoint1.x, mirroredControlPoint1.y,
413
- bubbleCenterX + tailBaseWidth, bubbleCenterY
414
- );
415
-
416
- ctx.closePath();
417
- ctx.fill();
418
- ctx.stroke();
419
- }
420
-
421
  function wrapText(ctx: CanvasRenderingContext2D, text: string, maxWidth: number, lineHeight: number): string[] {
422
  const words = text.split(' ');
423
  const lines: string[] = [];
 
59
  if (segmentationResult.categoryMask) {
60
  const mask = segmentationResult.categoryMask.getAsUint8Array();
61
  characterBoundingBox = findCharacterBoundingBox(mask, image.width, image.height);
62
+ console.log(segmentationResult)
63
  if (debug) {
64
  drawSegmentationMask(ctx, mask, image.width, image.height);
65
  }
 
208
 
209
  function drawSpeechBubble(
210
  ctx: CanvasRenderingContext2D,
211
+ location: { x: number; y: number },
212
  text: string,
213
  shape: "oval" | "rectangular" | "cloud" | "thought",
214
  line: "handdrawn" | "straight" | "bubble" | "chaotic",
215
  font: string,
216
  characterBoundingBox: BoundingBox | null,
217
  imageWidth: number,
218
+ imageHeight: number,
219
+ safetyMargin: number = 0.1 // Default safety margin is 10%
220
  ) {
 
 
221
  const padding = 24;
222
+ const borderPadding = Math.max(10, Math.min(imageWidth, imageHeight) * safetyMargin);
223
 
224
  const fontSize = 20;
225
  ctx.font = `${fontSize}px ${font}`;
226
 
227
+ // Adjust maximum width to account for border padding
228
+ const maxBubbleWidth = imageWidth - 2 * borderPadding;
229
+ const wrappedText = wrapText(ctx, text, maxBubbleWidth - padding * 2, fontSize);
230
  const textDimensions = measureTextDimensions(ctx, wrappedText, fontSize);
231
 
232
+ // Adjust bubble size based on text content
233
+ const finalWidth = Math.min(Math.max(textDimensions.width + padding * 2, 100), maxBubbleWidth);
234
+ const finalHeight = Math.min(Math.max(textDimensions.height + padding * 2, 50), imageHeight - 2 * borderPadding);
235
 
236
+ const bubbleLocation = adjustBubbleLocation(location, finalWidth, finalHeight, characterBoundingBox, imageWidth, imageHeight, borderPadding);
 
 
 
 
 
 
 
 
 
237
 
238
  let tailTarget = null;
239
  if (characterBoundingBox) {
240
  tailTarget = {
241
  x: characterBoundingBox.left + characterBoundingBox.width / 2,
242
+ y: characterBoundingBox.top + characterBoundingBox.height * 0.3
243
  };
244
  }
245
 
246
+ // Draw the main bubble
247
+ ctx.fillStyle = 'white';
248
+ ctx.strokeStyle = 'black';
249
+ ctx.lineWidth = 2;
250
  ctx.beginPath();
251
  drawBubbleShape(ctx, shape, bubbleLocation, finalWidth, finalHeight, tailTarget);
252
  ctx.fill();
253
  ctx.stroke();
254
 
255
+ // Draw the tail
256
  if (tailTarget) {
257
  drawTail(ctx, bubbleLocation, finalWidth, finalHeight, tailTarget, shape);
258
  }
259
 
260
+ // Draw a white oval to blend the tail with the bubble
261
+ ctx.fillStyle = 'white';
262
+ ctx.beginPath();
263
+ drawBubbleShape(ctx, shape, bubbleLocation, finalWidth, finalHeight, null);
264
+ ctx.fill();
265
+
266
+ // Draw the text
267
  ctx.fillStyle = 'black';
268
  ctx.textAlign = 'center';
269
  ctx.textBaseline = 'middle';
270
  drawFormattedText(ctx, wrappedText, bubbleLocation.x, bubbleLocation.y, finalWidth - padding * 2, fontSize);
271
  }
272
 
273
+ function drawTail(
274
+ ctx: CanvasRenderingContext2D,
275
+ bubbleLocation: { x: number; y: number },
276
+ bubbleWidth: number,
277
+ bubbleHeight: number,
278
+ tailTarget: { x: number; y: number },
279
+ shape: string
280
+ ) {
281
+ const bubbleCenterX = bubbleLocation.x;
282
+ const bubbleCenterY = bubbleLocation.y;
283
+ const tailBaseWidth = 40;
284
+
285
+ // Calculate the distance from bubble center to tail target
286
+ const deltaX = tailTarget.x - bubbleCenterX;
287
+ const deltaY = tailTarget.y - bubbleCenterY;
288
+ const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
289
+
290
+ // Set the tail length to 30% of the distance
291
+ const tailLength = distance * 0.3;
292
+
293
+ // Calculate the tail end point
294
+ const tailEndX = bubbleCenterX + (deltaX / distance) * tailLength;
295
+ const tailEndY = bubbleCenterY + (deltaY / distance) * tailLength;
296
+
297
+ // Calculate the angle of the tail
298
+ const angle = Math.atan2(deltaY, deltaX);
299
+
300
+ // Calculate the base points of the tail
301
+ const perpAngle = angle + Math.PI / 2;
302
+ const basePoint1 = {
303
+ x: bubbleCenterX + Math.cos(perpAngle) * tailBaseWidth / 2,
304
+ y: bubbleCenterY + Math.sin(perpAngle) * tailBaseWidth / 2
305
+ };
306
+ const basePoint2 = {
307
+ x: bubbleCenterX - Math.cos(perpAngle) * tailBaseWidth / 2,
308
+ y: bubbleCenterY - Math.sin(perpAngle) * tailBaseWidth / 2
309
+ };
310
+
311
+ // Calculate control points for the Bézier curves
312
+ const controlPointDistance = tailLength * 0.3;
313
+ const controlPoint1 = {
314
+ x: basePoint1.x + Math.cos(angle) * controlPointDistance,
315
+ y: basePoint1.y + Math.sin(angle) * controlPointDistance
316
+ };
317
+ const controlPoint2 = {
318
+ x: basePoint2.x + Math.cos(angle) * controlPointDistance,
319
+ y: basePoint2.y + Math.sin(angle) * controlPointDistance
320
+ };
321
+
322
+ // Draw the tail
323
+ ctx.beginPath();
324
+ ctx.moveTo(basePoint1.x, basePoint1.y);
325
+ ctx.quadraticCurveTo(controlPoint1.x, controlPoint1.y, tailEndX, tailEndY);
326
+ ctx.quadraticCurveTo(controlPoint2.x, controlPoint2.y, basePoint2.x, basePoint2.y);
327
+ ctx.closePath();
328
+
329
+ // Fill and stroke the tail
330
+ ctx.fillStyle = 'white';
331
+ ctx.fill();
332
+ ctx.strokeStyle = 'black';
333
+ ctx.stroke();
334
+ }
335
+
336
+ function adjustBubbleLocation(
337
+ location: { x: number; y: number },
338
+ width: number,
339
+ height: number,
340
+ characterBoundingBox: BoundingBox | null,
341
+ imageWidth: number,
342
+ imageHeight: number,
343
+ borderPadding: number
344
+ ): { x: number; y: number } {
345
+ let adjustedX = location.x;
346
+ let adjustedY = location.y;
347
+
348
+ // Ensure the bubble doesn't overlap with the character
349
+ if (characterBoundingBox) {
350
+ if (
351
+ adjustedX > characterBoundingBox.left &&
352
+ adjustedX < characterBoundingBox.left + characterBoundingBox.width
353
+ ) {
354
+ adjustedX = characterBoundingBox.left > imageWidth / 2
355
+ ? characterBoundingBox.left - width / 2 - 10
356
+ : characterBoundingBox.left + characterBoundingBox.width + width / 2 + 10;
357
+ }
358
+ }
359
+
360
+ // Ensure the bubble (including text) is fully visible
361
+ adjustedX = Math.max(width / 2 + borderPadding, Math.min(imageWidth - width / 2 - borderPadding, adjustedX));
362
+ adjustedY = Math.max(height / 2 + borderPadding, Math.min(imageHeight - height / 2 - borderPadding, adjustedY));
363
+
364
+ return { x: adjustedX, y: adjustedY };
365
+ }
366
+
367
  function drawBubbleShape(
368
  ctx: CanvasRenderingContext2D,
369
  shape: "oval" | "rectangular" | "cloud" | "thought",
 
451
  // The tail for thought bubbles is handled in the drawTail function
452
  }
453
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
454
  function wrapText(ctx: CanvasRenderingContext2D, text: string, maxWidth: number, lineHeight: number): string[] {
455
  const words = text.split(' ');
456
  const lines: string[] = [];
src/lib/getLocalStorageShowSpeeches.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function getLocalStorageShowSpeeches(defaultValue: boolean): boolean {
2
+ try {
3
+ const result = localStorage.getItem("AI_COMIC_FACTORY_SHOW_SPEECHES")
4
+ if (typeof result !== "string") {
5
+ return defaultValue
6
+ }
7
+ if (result === "true") { return true }
8
+ if (result === "false") { return false }
9
+ return defaultValue
10
+ } catch (err) {
11
+ return defaultValue
12
+ }
13
+ }