Merge pull request #1 from riccardogiorato/refactoring
Add add-page/delete-page APIs and enhance comic generation with prompt refactoring
This commit is contained in:
27 files changed
+2534
-1183
No files matched your search
@@ -0,0 +1,228 @@
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@clerk/nextjs/server";
|
||||
import Together from "together-ai";
|
||||
import { db } from "@/lib/db";
|
||||
import { pages } from "@/lib/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import {
|
||||
updatePage,
|
||||
createPage,
|
||||
getNextPageNumber,
|
||||
getStoryWithPagesBySlug,
|
||||
getLastPageImage,
|
||||
} from "@/lib/db-actions";
|
||||
import { freeTierRateLimit } from "@/lib/rate-limit";
|
||||
import { uploadImageToS3 } from "@/lib/s3-upload";
|
||||
import { buildComicPrompt } from "@/lib/prompt";
|
||||
|
||||
const NEW_MODEL = false;
|
||||
|
||||
const IMAGE_MODEL = NEW_MODEL
|
||||
? "google/gemini-3-pro-image"
|
||||
: "google/flash-image-2.5";
|
||||
|
||||
const FIXED_DIMENSIONS = NEW_MODEL
|
||||
? { width: 896, height: 1200 }
|
||||
: { width: 864, height: 1184 };
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { userId } = await auth();
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json(
|
||||
{ error: "Authentication required" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const { storyId, pageId, prompt, characterImages = [] } = await request.json();
|
||||
|
||||
if (!storyId || !prompt) {
|
||||
return NextResponse.json(
|
||||
{ error: "Missing required fields: storyId and prompt" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Get the story and all its pages
|
||||
const storyData = await getStoryWithPagesBySlug(storyId);
|
||||
if (!storyData) {
|
||||
return NextResponse.json({ error: "Story not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const { story, pages } = storyData;
|
||||
|
||||
// Check ownership
|
||||
if (story.userId !== userId) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
|
||||
}
|
||||
|
||||
// Apply rate limiting for free tier
|
||||
const hasApiKey = request.headers.get('x-api-key');
|
||||
if (!hasApiKey) {
|
||||
const { success, reset } = await freeTierRateLimit.limit(userId);
|
||||
if (!success) {
|
||||
const resetDate = new Date(reset);
|
||||
const timeUntilReset = Math.ceil(
|
||||
(reset - Date.now()) / (1000 * 60 * 60 * 24)
|
||||
);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: `Free tier limit reached. You can generate 1 comic per week. Try again in ${timeUntilReset} day(s), or provide your own API key.`,
|
||||
resetDate: resetDate.toISOString(),
|
||||
isRateLimited: true,
|
||||
},
|
||||
{ status: 429 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let page;
|
||||
let pageNumber;
|
||||
let isRedraw = false;
|
||||
|
||||
if (pageId) {
|
||||
// Redraw mode: update existing page
|
||||
isRedraw = true;
|
||||
const storyData = await getStoryWithPagesBySlug(storyId);
|
||||
if (!storyData) {
|
||||
return NextResponse.json({ error: "Story not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const existingPage = storyData.pages.find(p => p.id === pageId);
|
||||
if (!existingPage) {
|
||||
return NextResponse.json({ error: "Page not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
page = existingPage;
|
||||
pageNumber = existingPage.pageNumber;
|
||||
} else {
|
||||
// Add new page mode
|
||||
pageNumber = await getNextPageNumber(story.id);
|
||||
page = await createPage({
|
||||
storyId: story.id,
|
||||
pageNumber,
|
||||
prompt,
|
||||
characterImageUrls: characterImages,
|
||||
});
|
||||
}
|
||||
|
||||
const dimensions = FIXED_DIMENSIONS;
|
||||
|
||||
// Collect reference images: previous page + story characters + current characters
|
||||
let referenceImages: string[] = [];
|
||||
|
||||
// Get previous page image for style consistency (unless it's page 1)
|
||||
if (pageNumber > 1) {
|
||||
if (isRedraw) {
|
||||
// For redraw, get all pages and find the previous page's image
|
||||
const storyData = await getStoryWithPagesBySlug(storyId);
|
||||
if (storyData) {
|
||||
const previousPage = storyData.pages.find(p => p.pageNumber === pageNumber - 1);
|
||||
if (previousPage?.generatedImageUrl) {
|
||||
referenceImages.push(previousPage.generatedImageUrl);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For new page, use the last page image
|
||||
const lastPageImage = await getLastPageImage(story.id);
|
||||
if (lastPageImage) {
|
||||
referenceImages.push(lastPageImage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use only the character images sent from the frontend (user's selection)
|
||||
// These are already the most recent/relevant characters the user wants to use
|
||||
referenceImages.push(...characterImages);
|
||||
|
||||
// Build the prompt with continuation context
|
||||
const previousPages = pages.map(p => ({
|
||||
prompt: p.prompt,
|
||||
characterImages: p.characterImageUrls,
|
||||
}));
|
||||
|
||||
const fullPrompt = buildComicPrompt({
|
||||
prompt,
|
||||
style: story.style,
|
||||
characterImages,
|
||||
isAddPage: true,
|
||||
previousPages,
|
||||
});
|
||||
|
||||
const client = new Together({ apiKey: process.env.TOGETHER_API_KEY_DEFAULT });
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await client.images.generate({
|
||||
model: IMAGE_MODEL,
|
||||
prompt: fullPrompt,
|
||||
width: dimensions.width,
|
||||
height: dimensions.height,
|
||||
temperature: 0.1,
|
||||
reference_images: referenceImages.length > 0 ? referenceImages : undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Together AI API error:", error);
|
||||
|
||||
if (error instanceof Error && "status" in error) {
|
||||
const status = (error as any).status;
|
||||
if (status === 402) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Insufficient API credits.",
|
||||
errorType: "credit_limit",
|
||||
},
|
||||
{ status: 402 }
|
||||
);
|
||||
}
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error.message || `Failed to generate image: ${status}`,
|
||||
errorType: "api_error",
|
||||
},
|
||||
{ status: status || 500 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: `Internal server error: ${
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
}`,
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.data || !response.data[0] || !response.data[0].url) {
|
||||
return NextResponse.json(
|
||||
{ error: "No image URL in response" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const imageUrl = response.data[0].url;
|
||||
const s3Key = `${story.id}/page-${page.pageNumber}-${Date.now()}.jpg`;
|
||||
const s3ImageUrl = await uploadImageToS3(imageUrl, s3Key);
|
||||
|
||||
await updatePage(page.id, s3ImageUrl);
|
||||
|
||||
return NextResponse.json({
|
||||
imageUrl: s3ImageUrl,
|
||||
pageId: page.id,
|
||||
pageNumber: page.pageNumber,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error in add-page API:", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: `Internal server error: ${
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
}`,
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@clerk/nextjs/server";
|
||||
import { getStoryWithPagesBySlug, deletePage } from "@/lib/db-actions";
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const { userId } = await auth();
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json(
|
||||
{ error: "Authentication required" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const { storySlug, pageId } = await request.json();
|
||||
|
||||
if (!storySlug || !pageId) {
|
||||
return NextResponse.json(
|
||||
{ error: "Missing required fields: storySlug and pageId" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Get the story to check ownership
|
||||
const storyData = await getStoryWithPagesBySlug(storySlug);
|
||||
if (!storyData) {
|
||||
return NextResponse.json({ error: "Story not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const { story, pages } = storyData;
|
||||
|
||||
// Check ownership
|
||||
if (story.userId !== userId) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
|
||||
}
|
||||
|
||||
// Check if page exists and belongs to the story
|
||||
const pageExists = pages.some(p => p.id === pageId);
|
||||
if (!pageExists) {
|
||||
return NextResponse.json({ error: "Page not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Don't allow deleting the last page
|
||||
if (pages.length <= 1) {
|
||||
return NextResponse.json(
|
||||
{ error: "Cannot delete the last page of a story" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
await deletePage(pageId);
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error("Error deleting page:", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: `Internal server error: ${
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
}`,
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
+147
-70
@@ -3,14 +3,18 @@ import Together from "together-ai";
|
||||
import { auth } from "@clerk/nextjs/server";
|
||||
import {
|
||||
updatePage,
|
||||
updateStory,
|
||||
createStory,
|
||||
createPage,
|
||||
getNextPageNumber,
|
||||
getStoryById,
|
||||
getLastPageImage,
|
||||
getStoryCharacterImages,
|
||||
} from "@/lib/db-actions";
|
||||
import { freeTierRateLimit } from "@/lib/rate-limit";
|
||||
import { COMIC_STYLES } from "@/lib/constants";
|
||||
import { uploadImageToS3 } from "@/lib/s3-upload";
|
||||
import { buildComicPrompt } from "@/lib/prompt";
|
||||
|
||||
const NEW_MODEL = false;
|
||||
|
||||
@@ -22,6 +26,8 @@ const FIXED_DIMENSIONS = NEW_MODEL
|
||||
? { width: 896, height: 1200 }
|
||||
: { width: 864, height: 1184 };
|
||||
|
||||
const TEXT_MODEL = "meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { userId } = await auth();
|
||||
@@ -88,9 +94,11 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
let page;
|
||||
let story;
|
||||
let referenceImages: string[] = [];
|
||||
|
||||
if (storyId) {
|
||||
const story = await getStoryById(storyId);
|
||||
// Continuation: get previous page image and story character images
|
||||
story = await getStoryById(storyId);
|
||||
if (!story) {
|
||||
return NextResponse.json({ error: "Story not found" }, { status: 404 });
|
||||
}
|
||||
@@ -102,7 +110,20 @@ export async function POST(request: NextRequest) {
|
||||
prompt,
|
||||
characterImageUrls: characterImages,
|
||||
});
|
||||
|
||||
// Get previous page image for style consistency (unless it's page 1)
|
||||
if (nextPageNumber > 1) {
|
||||
const lastPageImage = await getLastPageImage(storyId);
|
||||
if (lastPageImage) {
|
||||
referenceImages.push(lastPageImage);
|
||||
}
|
||||
}
|
||||
|
||||
// For continuation pages, character images are sent from frontend
|
||||
// No need to fetch separately - frontend handles selection
|
||||
} else {
|
||||
// New story: no previous page reference
|
||||
// Create story with temporary title, will update with generated title
|
||||
story = await createStory({
|
||||
title: prompt.length > 50 ? prompt.substring(0, 50) + "..." : prompt,
|
||||
description: undefined,
|
||||
@@ -118,78 +139,104 @@ export async function POST(request: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
// Use only the character images sent from the frontend
|
||||
referenceImages.push(...characterImages);
|
||||
|
||||
const dimensions = FIXED_DIMENSIONS;
|
||||
const styleInfo = COMIC_STYLES.find((s) => s.id === style);
|
||||
const styleDesc = styleInfo?.prompt || COMIC_STYLES[2].prompt;
|
||||
|
||||
const continuationContext =
|
||||
isContinuation && previousContext
|
||||
? `\nCONTINUATION CONTEXT:\nThis is a continuation of an existing story. The previous page showed: ${previousContext}\nMaintain visual consistency with the previous panels. Continue the narrative naturally.\n`
|
||||
: "";
|
||||
|
||||
let characterSection = "";
|
||||
if (characterImages.length > 0) {
|
||||
if (characterImages.length === 1) {
|
||||
characterSection = `
|
||||
CRITICAL FACE CONSISTENCY INSTRUCTIONS:
|
||||
- REFERENCE CHARACTER: Use the uploaded image as EXACT reference for the protagonist's face and appearance
|
||||
- FACE MATCHING: The character's face must be IDENTICAL to the reference image - same eyes, nose, mouth, hair, facial structure
|
||||
- APPEARANCE PRESERVATION: Maintain exact skin tone, hair color/style, eye color, and distinctive facial features
|
||||
- CHARACTER CONSISTENCY: This exact same character must appear in ALL 5 panels with the same face throughout
|
||||
- STYLE APPLICATION: Apply ${style} comic art style to the body/pose/action but KEEP THE FACE EXACTLY AS IN THE REFERENCE IMAGE
|
||||
- NO VARIATION: Do not alter, modify, or change the character's face in any way from the reference`;
|
||||
} else if (characterImages.length === 2) {
|
||||
characterSection = `
|
||||
CRITICAL DUAL CHARACTER FACE CONSISTENCY INSTRUCTIONS:
|
||||
- CHARACTER 1 REFERENCE: Use the FIRST uploaded image as EXACT reference for Character 1's face and appearance
|
||||
- CHARACTER 2 REFERENCE: Use the SECOND uploaded image as EXACT reference for Character 2's face and appearance
|
||||
- FACE MATCHING: Both characters' faces must be IDENTICAL to their respective reference images
|
||||
- VISUAL DISTINCTION: Keep both characters clearly visually distinct with their unique faces, hair, and features
|
||||
- CONSISTENT PRESENCE: Both characters must appear together in at least 4 of the 5 panels
|
||||
- STYLE APPLICATION: Apply ${style} comic art style while maintaining EXACT facial features from references
|
||||
- NO FACE VARIATION: Never alter or modify either character's face from their reference images`;
|
||||
}
|
||||
}
|
||||
|
||||
const systemPrompt = `Professional comic book page illustration.
|
||||
${continuationContext}
|
||||
${characterSection}
|
||||
|
||||
CHARACTER CONSISTENCY RULES (HIGHEST PRIORITY):
|
||||
- If reference images are provided, the characters' FACES must be 100% identical to the reference images
|
||||
- Never change hair color, eye color, facial structure, or distinctive features
|
||||
- Apply comic style to body/pose/action but preserve exact facial appearance
|
||||
- Same character must look identical across all panels they appear in
|
||||
|
||||
TEXT AND LETTERING (CRITICAL):
|
||||
- All text in speech bubbles must be PERFECTLY CLEAR, LEGIBLE, and correctly spelled
|
||||
- Use bold clean comic book lettering, large and easy to read
|
||||
- Speech bubbles: crisp white fill, solid black outline, pointed tail toward speaker
|
||||
- Keep dialogue SHORT: maximum 1-2 sentences per bubble
|
||||
- NO blurry, warped, or unreadable text
|
||||
|
||||
PAGE LAYOUT:
|
||||
5-panel comic page arranged as:
|
||||
[Panel 1] [Panel 2] — top row, 2 equal panels
|
||||
[ Panel 3 ] — middle row, 1 large cinematic hero panel
|
||||
[Panel 4] [Panel 5] — bottom row, 2 equal panels
|
||||
- Solid black panel borders with clean white gutters between panels
|
||||
- Each panel clearly separated and distinct
|
||||
|
||||
ART STYLE:
|
||||
${styleDesc}
|
||||
${characterSection}
|
||||
|
||||
COMPOSITION:
|
||||
- Vary camera angles across panels: close-up, medium shot, wide establishing shot
|
||||
- Natural visual flow: left-to-right, top-to-bottom reading order
|
||||
- Dynamic character poses with clear expressive acting
|
||||
- Detailed backgrounds matching the scene and mood`;
|
||||
|
||||
const fullPrompt = `${systemPrompt}\n\nSTORY:\n${prompt}`;
|
||||
const fullPrompt = buildComicPrompt({
|
||||
prompt,
|
||||
style,
|
||||
characterImages,
|
||||
isContinuation,
|
||||
previousContext,
|
||||
});
|
||||
|
||||
const client = new Together({ apiKey: finalApiKey });
|
||||
|
||||
// Generate title and description in parallel with image generation (only for new stories)
|
||||
let titleGenerationPromise: Promise<{
|
||||
title: string;
|
||||
description: string;
|
||||
}> | null = null;
|
||||
if (!storyId) {
|
||||
titleGenerationPromise = (async () => {
|
||||
try {
|
||||
const titlePrompt = `Based on this comic book prompt, generate a compelling title and description for the comic book.
|
||||
|
||||
Prompt: "${prompt}"
|
||||
Style: ${COMIC_STYLES.find((s) => s.id === style)?.name || style}
|
||||
|
||||
Generate:
|
||||
1. A catchy, engaging title (maximum 60 characters)
|
||||
2. A brief description (2-3 sentences, maximum 200 characters)
|
||||
|
||||
Format your response as JSON:
|
||||
{
|
||||
"title": "Title here",
|
||||
"description": "Description here"
|
||||
}
|
||||
|
||||
Only return the JSON, no other text.`;
|
||||
|
||||
const textResponse = await client.chat.completions.create({
|
||||
model: TEXT_MODEL,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content:
|
||||
"You are a creative assistant that generates compelling comic book titles and descriptions. Always respond with valid JSON only.",
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: titlePrompt,
|
||||
},
|
||||
],
|
||||
temperature: 0.8,
|
||||
max_tokens: 300,
|
||||
});
|
||||
|
||||
const content = textResponse.choices[0]?.message?.content?.trim();
|
||||
if (!content) {
|
||||
throw new Error("No response from text generation");
|
||||
}
|
||||
|
||||
// Extract JSON from response (in case there's extra text)
|
||||
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
||||
if (!jsonMatch) {
|
||||
throw new Error("No JSON found in response");
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(jsonMatch[0]);
|
||||
const rawTitle =
|
||||
parsed.title?.trim() ||
|
||||
(prompt.length > 50 ? prompt.substring(0, 50) + "..." : prompt);
|
||||
const rawDescription = parsed.description?.trim();
|
||||
|
||||
// Enforce character limits
|
||||
const title =
|
||||
rawTitle.length > 60 ? rawTitle.substring(0, 57) + "..." : rawTitle;
|
||||
const description =
|
||||
rawDescription && rawDescription.length > 200
|
||||
? rawDescription.substring(0, 197) + "..."
|
||||
: rawDescription;
|
||||
|
||||
return {
|
||||
title,
|
||||
description: description || undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error generating title and description:", error);
|
||||
// Fallback to prompt-based title
|
||||
return {
|
||||
title:
|
||||
prompt.length > 50 ? prompt.substring(0, 50) + "..." : prompt,
|
||||
description: undefined,
|
||||
};
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await client.images.generate({
|
||||
@@ -199,7 +246,7 @@ COMPOSITION:
|
||||
height: dimensions.height,
|
||||
temperature: 0.1, // Lower temperature for more consistent face matching
|
||||
reference_images:
|
||||
characterImages.length > 0 ? characterImages : undefined,
|
||||
referenceImages.length > 0 ? referenceImages : undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Together AI API error:", error);
|
||||
@@ -245,9 +292,37 @@ COMPOSITION:
|
||||
const imageUrl = response.data[0].url;
|
||||
|
||||
// Upload image to S3 for permanent storage
|
||||
const s3Key = `${storyId || story!.id}/page-${page.pageNumber}-${Date.now()}.jpg`;
|
||||
const s3Key = `${storyId || story!.id}/page-${
|
||||
page.pageNumber
|
||||
}-${Date.now()}.jpg`;
|
||||
const s3ImageUrl = await uploadImageToS3(imageUrl, s3Key);
|
||||
|
||||
// Wait for title/description generation if it's a new story
|
||||
let generatedTitle: string | undefined;
|
||||
let generatedDescription: string | undefined;
|
||||
if (titleGenerationPromise) {
|
||||
const titleData = await titleGenerationPromise;
|
||||
generatedTitle = titleData.title;
|
||||
generatedDescription = titleData.description;
|
||||
|
||||
// Update story with generated title and description
|
||||
try {
|
||||
await updateStory(story!.id, {
|
||||
title: generatedTitle,
|
||||
description: generatedDescription,
|
||||
});
|
||||
// Update story object for response
|
||||
story = {
|
||||
...story,
|
||||
title: generatedTitle,
|
||||
description: generatedDescription,
|
||||
};
|
||||
} catch (dbError) {
|
||||
console.error("Error updating story title/description:", dbError);
|
||||
// Continue even if update fails
|
||||
}
|
||||
}
|
||||
|
||||
// Update page in database with S3 URL
|
||||
try {
|
||||
await updatePage(page.id, s3ImageUrl);
|
||||
@@ -267,6 +342,8 @@ COMPOSITION:
|
||||
storySlug: story!.slug,
|
||||
pageId: page.id,
|
||||
pageNumber: page.pageNumber,
|
||||
title: generatedTitle || story!.title,
|
||||
description: generatedDescription || story!.description,
|
||||
};
|
||||
|
||||
return NextResponse.json(responseData);
|
||||
|
||||
@@ -10,19 +10,23 @@ export async function GET(
|
||||
{ params }: { params: Promise<{ storySlug: string }> }
|
||||
) {
|
||||
try {
|
||||
const { userId } = await auth();
|
||||
const authResult = await auth();
|
||||
const { userId } = authResult;
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json(
|
||||
{ error: "Authentication required" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
console.log('API: auth result:', authResult);
|
||||
console.log('API: userId type:', typeof userId, 'value:', userId);
|
||||
console.log('API: timestamp:', new Date().toISOString());
|
||||
|
||||
const { storySlug: slug } = await params;
|
||||
|
||||
// Special case: if slug is "all", return user's stories for debugging
|
||||
if (slug === "all") {
|
||||
if (!userId) {
|
||||
return NextResponse.json(
|
||||
{ error: "Authentication required for this endpoint" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
const userStories = await db.select().from(stories).where(eq(stories.userId, userId));
|
||||
return NextResponse.json({
|
||||
message: "User stories",
|
||||
@@ -47,14 +51,14 @@ export async function GET(
|
||||
}
|
||||
|
||||
// Check if the story belongs to the authenticated user
|
||||
if (result.story.userId !== userId) {
|
||||
return NextResponse.json(
|
||||
{ error: "Access denied" },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
const isOwner = userId ? result.story.userId === userId : false;
|
||||
|
||||
return NextResponse.json(result);
|
||||
// Return the story data with ownership information
|
||||
const responseData = {
|
||||
...result,
|
||||
isOwner,
|
||||
};
|
||||
return NextResponse.json(responseData);
|
||||
} catch (error) {
|
||||
console.error("Error fetching story:", error);
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -2,7 +2,7 @@ import { NextResponse } from "next/server";
|
||||
import { auth } from "@clerk/nextjs/server";
|
||||
import { db } from "@/lib/db";
|
||||
import { stories, pages } from "@/lib/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { eq, desc, sql } from "drizzle-orm";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
@@ -15,22 +15,24 @@ export async function GET() {
|
||||
);
|
||||
}
|
||||
|
||||
// Get all stories for the user with their first page
|
||||
// Get all stories for the user with their pages
|
||||
const userStories = await db
|
||||
.select({
|
||||
id: stories.id,
|
||||
title: stories.title,
|
||||
slug: stories.slug,
|
||||
style: stories.style,
|
||||
createdAt: stories.createdAt,
|
||||
pageCount: pages.pageNumber,
|
||||
coverImage: pages.generatedImageUrl,
|
||||
pageCreatedAt: pages.createdAt,
|
||||
pageUpdatedAt: pages.updatedAt,
|
||||
})
|
||||
.from(stories)
|
||||
.leftJoin(pages, eq(stories.id, pages.storyId))
|
||||
.where(eq(stories.userId, userId))
|
||||
.orderBy(stories.createdAt);
|
||||
.where(eq(stories.userId, userId));
|
||||
|
||||
// Group by story and find the max page number and first page image
|
||||
// Group by story and find the max page number, first page image, and most recent update
|
||||
const storyMap = new Map();
|
||||
|
||||
userStories.forEach((row) => {
|
||||
@@ -40,9 +42,11 @@ export async function GET() {
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
slug: row.slug,
|
||||
style: row.style,
|
||||
createdAt: row.createdAt,
|
||||
pageCount: 0,
|
||||
coverImage: null,
|
||||
lastUpdated: row.createdAt, // Default to story creation date
|
||||
});
|
||||
}
|
||||
|
||||
@@ -53,10 +57,23 @@ export async function GET() {
|
||||
if (row.pageCount === 1 && row.coverImage) {
|
||||
story.coverImage = row.coverImage;
|
||||
}
|
||||
// Track the most recent page update
|
||||
if (row.pageUpdatedAt && row.pageUpdatedAt > story.lastUpdated) {
|
||||
story.lastUpdated = row.pageUpdatedAt;
|
||||
} else if (row.pageCreatedAt && row.pageCreatedAt > story.lastUpdated) {
|
||||
story.lastUpdated = row.pageCreatedAt;
|
||||
}
|
||||
});
|
||||
|
||||
const storiesWithCovers = Array.from(storyMap.values());
|
||||
|
||||
// Sort by most recently updated (stories with newest pages first)
|
||||
storiesWithCovers.sort((a, b) => {
|
||||
const aTime = new Date(a.lastUpdated).getTime();
|
||||
const bTime = new Date(b.lastUpdated).getTime();
|
||||
return bTime - aTime; // Most recent first
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
stories: storiesWithCovers
|
||||
});
|
||||
|
||||
+41
-296
@@ -1,303 +1,48 @@
|
||||
"use client"
|
||||
import { Metadata } from "next";
|
||||
import { getStoryWithPagesBySlug } from "@/lib/db-actions";
|
||||
import { StoryEditorClient } from "./story-editor-client";
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useParams } from "next/navigation"
|
||||
import { useToast } from "@/hooks/use-toast"
|
||||
import { EditorToolbar } from "@/components/editor/editor-toolbar"
|
||||
import { PageSidebar } from "@/components/editor/page-sidebar"
|
||||
import { ComicCanvas } from "@/components/editor/comic-canvas"
|
||||
import { ApiKeyModal } from "@/components/api-key-modal"
|
||||
import { PageInfoSheet } from "@/components/editor/page-info-sheet"
|
||||
import { GeneratePageModal } from "@/components/editor/generate-page-modal"
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ storySlug: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { storySlug: slug } = await params;
|
||||
|
||||
import { useS3Upload } from "next-s3-upload"
|
||||
try {
|
||||
const result = await getStoryWithPagesBySlug(slug);
|
||||
|
||||
interface PageData {
|
||||
id: number // pageNumber for component compatibility
|
||||
title: string
|
||||
image: string
|
||||
prompt: string
|
||||
characterUploads?: string[]
|
||||
style: string
|
||||
dbId?: string // actual database UUID
|
||||
}
|
||||
if (!result) {
|
||||
return {
|
||||
title: "Story Not Found | MakeComics",
|
||||
description: "The requested comic story could not be found.",
|
||||
};
|
||||
}
|
||||
|
||||
interface StoryData {
|
||||
id: string
|
||||
title: string
|
||||
description?: string | null
|
||||
userId?: string | null
|
||||
const { story } = result;
|
||||
const title = `${story.title} | MakeComics`;
|
||||
const description =
|
||||
story.description ||
|
||||
`${story.title} - Create your own comic book with MakeComics`;
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
type: "website",
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error generating metadata:", error);
|
||||
return {
|
||||
title: "MakeComics",
|
||||
description: "Create your own comic book with MakeComics",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default function StoryEditorPage() {
|
||||
const params = useParams()
|
||||
const slug = params.storySlug as string
|
||||
|
||||
const [story, setStory] = useState<StoryData | null>(null)
|
||||
const [pages, setPages] = useState<PageData[]>([])
|
||||
const [currentPage, setCurrentPage] = useState(0)
|
||||
const [showApiModal, setShowApiModal] = useState(false)
|
||||
const [showInfoSheet, setShowInfoSheet] = useState(false)
|
||||
const [showGenerateModal, setShowGenerateModal] = useState(false)
|
||||
const [loadingPageId, setLoadingPageId] = useState<number | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [existingCharacterImages, setExistingCharacterImages] = useState<string[]>([])
|
||||
const { uploadToS3 } = useS3Upload()
|
||||
const { toast } = useToast()
|
||||
|
||||
// Load story and pages from API
|
||||
useEffect(() => {
|
||||
const loadStoryData = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/stories/${slug}`)
|
||||
if (!response.ok) {
|
||||
throw new Error("Story not found")
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
const { story: storyData, pages: pagesData } = result
|
||||
|
||||
setStory(storyData)
|
||||
setPages(pagesData.map((page: any) => ({
|
||||
id: page.pageNumber,
|
||||
title: storyData.title,
|
||||
image: page.generatedImageUrl || "",
|
||||
prompt: page.prompt,
|
||||
characterUploads: page.characterImageUrls,
|
||||
style: storyData.style || "noir",
|
||||
dbId: page.id,
|
||||
})))
|
||||
|
||||
// Load existing character images for reuse
|
||||
const uniqueImages = [...new Set(pagesData.flatMap((page: any) => page.characterImageUrls || []))]
|
||||
setExistingCharacterImages(uniqueImages as string[])
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error loading story:", error)
|
||||
toast({
|
||||
title: "Error loading story",
|
||||
description: "Failed to load story data.",
|
||||
variant: "destructive",
|
||||
duration: 4000,
|
||||
})
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (slug) {
|
||||
loadStoryData()
|
||||
}
|
||||
}, [slug, toast])
|
||||
|
||||
// Keyboard navigation
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "ArrowRight") {
|
||||
setCurrentPage((prev) => (prev < pages.length - 1 ? prev + 1 : prev))
|
||||
} else if (e.key === "ArrowLeft") {
|
||||
setCurrentPage((prev) => (prev > 0 ? prev - 1 : prev))
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown)
|
||||
return () => window.removeEventListener("keydown", handleKeyDown)
|
||||
}, [pages.length])
|
||||
|
||||
const handleAddPage = () => {
|
||||
const storedKey = localStorage.getItem("together_api_key")
|
||||
if (!storedKey && pages.length >= 1) {
|
||||
setShowApiModal(true)
|
||||
return
|
||||
}
|
||||
setShowGenerateModal(true)
|
||||
}
|
||||
|
||||
const handleContinueStory = () => {
|
||||
const storedKey = localStorage.getItem("together_api_key")
|
||||
if (!storedKey) {
|
||||
setShowApiModal(true)
|
||||
return
|
||||
}
|
||||
setShowGenerateModal(true)
|
||||
}
|
||||
|
||||
const handleApiKeyClick = () => {
|
||||
setShowApiModal(true)
|
||||
}
|
||||
|
||||
const handleApiKeySubmit = (key: string) => {
|
||||
localStorage.setItem("together_api_key", key)
|
||||
setShowApiModal(false)
|
||||
const wasGenerating = showGenerateModal
|
||||
if (wasGenerating) {
|
||||
setShowGenerateModal(true)
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "API key saved",
|
||||
description: "Your Together API key has been saved successfully",
|
||||
duration: 3000,
|
||||
})
|
||||
}
|
||||
|
||||
const handleGeneratePage = async (data: {
|
||||
prompt: string
|
||||
style: string
|
||||
characterFiles?: File[]
|
||||
characterUrls?: string[] // For reusing existing characters
|
||||
isContinuation?: boolean
|
||||
}) => {
|
||||
if (!story) return
|
||||
|
||||
setShowGenerateModal(false)
|
||||
|
||||
try {
|
||||
// Handle new character uploads
|
||||
let characterUploads: string[] = data.characterUrls || []
|
||||
|
||||
if (data.characterFiles && data.characterFiles.length > 0) {
|
||||
const newUploads = await Promise.all(
|
||||
data.characterFiles.map((file) => uploadToS3(file).then(({ url }) => url))
|
||||
)
|
||||
characterUploads = [...characterUploads, ...newUploads]
|
||||
}
|
||||
|
||||
// Add loading page to UI
|
||||
const nextPageNumber = pages.length + 1
|
||||
const pageData: PageData = {
|
||||
id: nextPageNumber,
|
||||
title: story.title,
|
||||
image: "",
|
||||
prompt: data.prompt,
|
||||
characterUploads,
|
||||
style: data.style,
|
||||
}
|
||||
|
||||
setPages([...pages, pageData])
|
||||
setCurrentPage(pages.length)
|
||||
setLoadingPageId(nextPageNumber)
|
||||
|
||||
// Generate the comic image
|
||||
const apiKey = localStorage.getItem("together_api_key")
|
||||
if (!apiKey) {
|
||||
throw new Error("API key not found")
|
||||
}
|
||||
|
||||
const previousPage = pages[pages.length - 1]
|
||||
|
||||
const response = await fetch("/api/generate-comic", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
storyId: story?.id,
|
||||
prompt: data.prompt,
|
||||
apiKey,
|
||||
style: data.style,
|
||||
characterImages: characterUploads,
|
||||
isContinuation: data.isContinuation,
|
||||
previousContext: data.isContinuation ? previousPage?.prompt : undefined,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || "Failed to generate image")
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
// Update page with generated image
|
||||
setPages((prevPages) =>
|
||||
prevPages.map((page) =>
|
||||
page.id === nextPageNumber
|
||||
? {
|
||||
...page,
|
||||
image: result.imageUrl,
|
||||
dbId: result.pageId,
|
||||
}
|
||||
: page,
|
||||
),
|
||||
)
|
||||
|
||||
// Update existing character images for future reuse
|
||||
if (characterUploads.length > 0) {
|
||||
const updatedImages = [...new Set([...existingCharacterImages, ...characterUploads])]
|
||||
setExistingCharacterImages(updatedImages)
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "Page generated successfully",
|
||||
description: `Page ${nextPageNumber} is ready`,
|
||||
duration: 4000,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error generating page:", error)
|
||||
toast({
|
||||
title: "Generation failed",
|
||||
description: error instanceof Error ? error.message : "Failed to generate comic page. Please try again.",
|
||||
variant: "destructive",
|
||||
duration: 4000,
|
||||
})
|
||||
|
||||
// Remove failed page from state
|
||||
setPages((prevPages) => prevPages.filter((page) => page.id !== loadingPageId))
|
||||
setCurrentPage(Math.max(0, pages.length - 1))
|
||||
} finally {
|
||||
setLoadingPageId(null)
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="h-screen flex items-center justify-center bg-background">
|
||||
<div className="text-white">Loading story...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!story) {
|
||||
return (
|
||||
<div className="h-screen flex items-center justify-center bg-background">
|
||||
<div className="text-white">Story not found</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col bg-background">
|
||||
<EditorToolbar
|
||||
title={story.title}
|
||||
onContinueStory={handleContinueStory}
|
||||
onInfoClick={() => setShowInfoSheet(true)}
|
||||
/>
|
||||
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
<PageSidebar
|
||||
pages={pages}
|
||||
currentPage={currentPage}
|
||||
onPageSelect={setCurrentPage}
|
||||
onAddPage={handleAddPage}
|
||||
loadingPageId={loadingPageId}
|
||||
onApiKeyClick={handleApiKeyClick}
|
||||
/>
|
||||
<ComicCanvas page={pages[currentPage]} />
|
||||
</div>
|
||||
|
||||
<ApiKeyModal isOpen={showApiModal} onClose={() => setShowApiModal(false)} onSubmit={handleApiKeySubmit} />
|
||||
<GeneratePageModal
|
||||
isOpen={showGenerateModal}
|
||||
onClose={() => setShowGenerateModal(false)}
|
||||
onGenerate={handleGeneratePage}
|
||||
pageNumber={pages.length + 1}
|
||||
previousCharacters={[]} // Will be updated with character reuse
|
||||
previousPagePrompt={pages[pages.length - 1]?.prompt}
|
||||
previousPageStyle={pages[pages.length - 1]?.style?.toLowerCase()}
|
||||
existingCharacterImages={existingCharacterImages}
|
||||
/>
|
||||
<PageInfoSheet isOpen={showInfoSheet} onClose={() => setShowInfoSheet(false)} page={pages[currentPage]} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <StoryEditorClient />;
|
||||
}
|
||||
@@ -0,0 +1,453 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { useApiKey } from "@/hooks/use-api-key";
|
||||
import { EditorToolbar } from "@/components/editor/editor-toolbar";
|
||||
import { PageSidebar } from "@/components/editor/page-sidebar";
|
||||
import { ComicCanvas } from "@/components/editor/comic-canvas";
|
||||
import { ApiKeyModal } from "@/components/api-key-modal";
|
||||
import { PageInfoSheet } from "@/components/editor/page-info-sheet";
|
||||
import { GeneratePageModal } from "@/components/editor/generate-page-modal";
|
||||
import { StoryLoader } from "@/components/ui/story-loader";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
|
||||
interface PageData {
|
||||
id: number; // pageNumber for component compatibility
|
||||
title: string;
|
||||
image: string;
|
||||
prompt: string;
|
||||
characterUploads?: string[];
|
||||
style: string;
|
||||
dbId?: string; // actual database UUID
|
||||
}
|
||||
|
||||
interface StoryData {
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
description?: string | null;
|
||||
style: string;
|
||||
userId?: string | null;
|
||||
isOwner?: boolean;
|
||||
}
|
||||
|
||||
export function StoryEditorClient() {
|
||||
const params = useParams();
|
||||
const slug = params.storySlug as string;
|
||||
|
||||
const [story, setStory] = useState<StoryData | null>(null);
|
||||
const [isOwner, setIsOwner] = useState<boolean>(false);
|
||||
const [pages, setPages] = useState<PageData[]>([]);
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
const [showApiModal, setShowApiModal] = useState(false);
|
||||
const [showInfoSheet, setShowInfoSheet] = useState(false);
|
||||
const [showGenerateModal, setShowGenerateModal] = useState(false);
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const [pageToDelete, setPageToDelete] = useState<number | null>(null);
|
||||
const [loadingPageId, setLoadingPageId] = useState<number | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [existingCharacterImages, setExistingCharacterImages] = useState<
|
||||
string[]
|
||||
>([]);
|
||||
const { toast } = useToast();
|
||||
const [apiKey, setApiKey] = useApiKey();
|
||||
|
||||
// Load story and pages from API
|
||||
useEffect(() => {
|
||||
const loadStoryData = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/stories/${slug}`);
|
||||
if (!response.ok) {
|
||||
throw new Error("Story not found");
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log("Editor: full API response:", result);
|
||||
|
||||
const {
|
||||
story: storyData,
|
||||
pages: pagesData,
|
||||
isOwner: ownerStatus,
|
||||
} = result;
|
||||
|
||||
console.log("Editor: received story data:", storyData);
|
||||
|
||||
setStory(storyData);
|
||||
setIsOwner(ownerStatus ?? false); // Default to false if undefined
|
||||
setPages(
|
||||
pagesData.map((page: any) => ({
|
||||
id: page.pageNumber,
|
||||
title: storyData.title,
|
||||
image: page.generatedImageUrl || "",
|
||||
prompt: page.prompt,
|
||||
characterUploads: page.characterImageUrls,
|
||||
style: storyData.style || "noir",
|
||||
dbId: page.id,
|
||||
}))
|
||||
);
|
||||
|
||||
// Load existing character images for reuse
|
||||
const uniqueImages = [
|
||||
...new Set(
|
||||
pagesData.flatMap((page: any) => page.characterImageUrls || [])
|
||||
),
|
||||
];
|
||||
setExistingCharacterImages(uniqueImages as string[]);
|
||||
} catch (error) {
|
||||
console.error("Error loading story:", error);
|
||||
toast({
|
||||
title: "Error loading story",
|
||||
description: "Failed to load story data.",
|
||||
variant: "destructive",
|
||||
duration: 4000,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (slug) {
|
||||
loadStoryData();
|
||||
}
|
||||
}, [slug, toast]);
|
||||
|
||||
// Keyboard navigation
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
// Don't trigger shortcuts if user is typing in an input field
|
||||
const target = e.target as HTMLElement;
|
||||
if (
|
||||
target.tagName === "INPUT" ||
|
||||
target.tagName === "TEXTAREA" ||
|
||||
target.isContentEditable
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === "ArrowRight") {
|
||||
setCurrentPage((prev) => (prev < pages.length - 1 ? prev + 1 : prev));
|
||||
} else if (e.key === "ArrowLeft") {
|
||||
setCurrentPage((prev) => (prev > 0 ? prev - 1 : prev));
|
||||
} else if (e.key === "i" || e.key === "I") {
|
||||
setShowInfoSheet(true);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [pages.length]);
|
||||
|
||||
const handleAddPage = () => {
|
||||
if (!apiKey && pages.length >= 1) {
|
||||
setShowApiModal(true);
|
||||
return;
|
||||
}
|
||||
setShowGenerateModal(true);
|
||||
};
|
||||
|
||||
const handleRedrawPage = async () => {
|
||||
if (!apiKey) {
|
||||
setShowApiModal(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentPageData = pages[currentPage];
|
||||
if (!currentPageData) return;
|
||||
|
||||
setLoadingPageId(currentPage);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/add-page", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": apiKey,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
storyId: story?.slug,
|
||||
pageId: currentPageData.dbId, // Add pageId to override existing page
|
||||
prompt: currentPageData.prompt,
|
||||
characterImages: currentPageData.characterUploads || [],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || "Failed to redraw page");
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// Update the current page with the new image
|
||||
setPages((prevPages) =>
|
||||
prevPages.map((page, index) =>
|
||||
index === currentPage ? { ...page, image: result.imageUrl } : page
|
||||
)
|
||||
);
|
||||
|
||||
toast({
|
||||
title: "Page redrawn successfully",
|
||||
description: "The page has been regenerated with a fresh image.",
|
||||
duration: 3000,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error redrawing page:", error);
|
||||
toast({
|
||||
title: "Failed to redraw page",
|
||||
description:
|
||||
error instanceof Error ? error.message : "Failed to redraw page",
|
||||
variant: "destructive",
|
||||
duration: 4000,
|
||||
});
|
||||
} finally {
|
||||
setLoadingPageId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleApiKeyClick = () => {
|
||||
setShowApiModal(true);
|
||||
};
|
||||
|
||||
const handleDeletePage = (pageIndex: number) => {
|
||||
setPageToDelete(pageIndex);
|
||||
setShowDeleteDialog(true);
|
||||
};
|
||||
|
||||
const confirmDeletePage = async () => {
|
||||
if (pageToDelete === null) return;
|
||||
|
||||
const pageData = pages[pageToDelete];
|
||||
if (!pageData) return;
|
||||
|
||||
setShowDeleteDialog(false);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/delete-page", {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
storySlug: story?.slug,
|
||||
pageId: pageData.dbId,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || "Failed to delete page");
|
||||
}
|
||||
|
||||
// Remove the page from state
|
||||
setPages((prevPages) => {
|
||||
const newPages = prevPages.filter((_, index) => index !== pageToDelete);
|
||||
// Adjust currentPage if necessary
|
||||
if (currentPage >= newPages.length) {
|
||||
setCurrentPage(Math.max(0, newPages.length - 1));
|
||||
} else if (currentPage > pageToDelete) {
|
||||
setCurrentPage(currentPage - 1);
|
||||
}
|
||||
return newPages;
|
||||
});
|
||||
|
||||
toast({
|
||||
title: "Page deleted successfully",
|
||||
description: "The page has been removed from your comic.",
|
||||
duration: 3000,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error deleting page:", error);
|
||||
toast({
|
||||
title: "Failed to delete page",
|
||||
description:
|
||||
error instanceof Error ? error.message : "Failed to delete page",
|
||||
variant: "destructive",
|
||||
duration: 4000,
|
||||
});
|
||||
} finally {
|
||||
setPageToDelete(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleApiKeySubmit = (key: string) => {
|
||||
setApiKey(key);
|
||||
setShowApiModal(false);
|
||||
const wasGenerating = showGenerateModal;
|
||||
if (wasGenerating) {
|
||||
setShowGenerateModal(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGeneratePage = async (data: {
|
||||
prompt: string;
|
||||
characterUrls?: string[];
|
||||
}): Promise<void> => {
|
||||
if (!apiKey) {
|
||||
setShowApiModal(true);
|
||||
throw new Error("API key required");
|
||||
}
|
||||
|
||||
// Add new page mode
|
||||
const response = await fetch("/api/add-page", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": apiKey,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
storyId: story?.slug,
|
||||
prompt: data.prompt,
|
||||
characterImages: data.characterUrls || [],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || "Failed to generate page");
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// Update character images list with new ones
|
||||
const newCharacterUrls = data.characterUrls || [];
|
||||
setExistingCharacterImages((prev) => {
|
||||
const combined = [...prev, ...newCharacterUrls];
|
||||
// Remove duplicates while preserving order
|
||||
const unique = Array.from(new Set(combined));
|
||||
return unique;
|
||||
});
|
||||
|
||||
setPages((prevPages) => [
|
||||
...prevPages,
|
||||
{
|
||||
id: pages.length + 1,
|
||||
title: story?.title || "",
|
||||
image: result.imageUrl,
|
||||
prompt: data.prompt,
|
||||
characterUploads: data.characterUrls || [],
|
||||
style: story?.style || "noir",
|
||||
dbId: result.pageId,
|
||||
},
|
||||
]);
|
||||
setCurrentPage(pages.length);
|
||||
|
||||
setShowGenerateModal(false);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="h-screen flex items-center justify-center bg-background">
|
||||
<StoryLoader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!story) {
|
||||
return (
|
||||
<div className="h-screen flex items-center justify-center bg-background">
|
||||
<div className="text-white">Story not found</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col bg-background">
|
||||
<EditorToolbar
|
||||
title={story.title}
|
||||
onContinueStory={handleAddPage}
|
||||
isOwner={isOwner}
|
||||
/>
|
||||
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
<PageSidebar
|
||||
pages={pages}
|
||||
currentPage={currentPage}
|
||||
onPageSelect={setCurrentPage}
|
||||
onAddPage={handleAddPage}
|
||||
loadingPageId={loadingPageId}
|
||||
onApiKeyClick={handleApiKeyClick}
|
||||
isOwner={isOwner}
|
||||
/>
|
||||
<ComicCanvas
|
||||
page={pages[currentPage]}
|
||||
pageIndex={currentPage}
|
||||
totalPages={pages.length}
|
||||
isLoading={loadingPageId === currentPage}
|
||||
isOwner={isOwner}
|
||||
onInfoClick={() => setShowInfoSheet(true)}
|
||||
onRedrawClick={handleRedrawPage}
|
||||
onDeletePage={() => handleDeletePage(currentPage)}
|
||||
onNextPage={() =>
|
||||
setCurrentPage((prev) =>
|
||||
prev < pages.length - 1 ? prev + 1 : prev
|
||||
)
|
||||
}
|
||||
onPrevPage={() =>
|
||||
setCurrentPage((prev) => (prev > 0 ? prev - 1 : prev))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ApiKeyModal
|
||||
isOpen={showApiModal}
|
||||
onClose={() => setShowApiModal(false)}
|
||||
onSubmit={handleApiKeySubmit}
|
||||
/>
|
||||
<GeneratePageModal
|
||||
isOpen={showGenerateModal}
|
||||
onClose={() => setShowGenerateModal(false)}
|
||||
onGenerate={handleGeneratePage}
|
||||
pageNumber={pages.length + 1}
|
||||
existingCharacters={existingCharacterImages}
|
||||
lastPageCharacters={
|
||||
pages.length > 0 && pages[pages.length - 1]?.characterUploads
|
||||
? pages[pages.length - 1].characterUploads || []
|
||||
: []
|
||||
}
|
||||
previousPageCharacters={
|
||||
pages.length > 1 && pages[pages.length - 2]?.characterUploads
|
||||
? pages[pages.length - 2].characterUploads || []
|
||||
: []
|
||||
}
|
||||
/>
|
||||
<PageInfoSheet
|
||||
isOpen={showInfoSheet}
|
||||
onClose={() => setShowInfoSheet(false)}
|
||||
page={pages[currentPage]}
|
||||
/>
|
||||
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Page</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete page{" "}
|
||||
{pageToDelete !== null ? pageToDelete + 1 : ""}? This action
|
||||
cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDeletePage}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+15
-24
@@ -3,8 +3,7 @@
|
||||
import { Navbar } from "@/components/landing/navbar"
|
||||
import { Footer } from "@/components/landing/footer"
|
||||
import { LandingHero } from "@/components/landing/hero-section"
|
||||
import { StoryInput } from "@/components/landing/story-input"
|
||||
import { CreateButton } from "@/components/landing/create-button"
|
||||
import { ComicCreationForm } from "@/components/landing/comic-creation-form"
|
||||
import { useState, useEffect } from "react"
|
||||
|
||||
export default function Home() {
|
||||
@@ -43,28 +42,20 @@ export default function Home() {
|
||||
<div className="max-w-xl mx-auto lg:mx-0 w-full z-10">
|
||||
<LandingHero />
|
||||
|
||||
<div className="space-y-4 sm:space-y-5 mt-4 sm:mt-5">
|
||||
<div className="opacity-0 animate-fade-in-up animation-delay-100">
|
||||
<StoryInput
|
||||
prompt={prompt}
|
||||
setPrompt={setPrompt}
|
||||
style={style}
|
||||
setStyle={setStyle}
|
||||
characterFiles={characterFiles}
|
||||
setCharacterFiles={setCharacterFiles}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<div className="opacity-0 animate-fade-in-up animation-delay-200">
|
||||
<CreateButton
|
||||
prompt={prompt}
|
||||
style={style}
|
||||
characterFiles={characterFiles}
|
||||
isLoading={isLoading}
|
||||
setIsLoading={setIsLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4 sm:space-y-5 mt-4 sm:mt-5">
|
||||
<div className="opacity-0 animate-fade-in-up animation-delay-100">
|
||||
<ComicCreationForm
|
||||
prompt={prompt}
|
||||
setPrompt={setPrompt}
|
||||
style={style}
|
||||
setStyle={setStyle}
|
||||
characterFiles={characterFiles}
|
||||
setCharacterFiles={setCharacterFiles}
|
||||
isLoading={isLoading}
|
||||
setIsLoading={setIsLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+98
-68
@@ -5,14 +5,18 @@ import { useRouter } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Plus, Loader2 } from "lucide-react";
|
||||
import { Navbar } from "@/components/landing/navbar";
|
||||
import { StoryLoader } from "@/components/ui/story-loader";
|
||||
import { COMIC_STYLES } from "@/lib/constants";
|
||||
|
||||
interface Story {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
style: string;
|
||||
createdAt: string;
|
||||
pageCount: number;
|
||||
coverImage: string | null;
|
||||
lastUpdated?: string;
|
||||
}
|
||||
|
||||
export default function StoriesPage() {
|
||||
@@ -51,10 +55,7 @@ export default function StoriesPage() {
|
||||
<Navbar />
|
||||
|
||||
<main className="flex-1 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-4" />
|
||||
<p className="text-muted-foreground">Loading your comic library...</p>
|
||||
</div>
|
||||
<StoryLoader text="Loading your comic library..." />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
@@ -85,78 +86,107 @@ export default function StoriesPage() {
|
||||
<div className="fixed inset-0 overflow-hidden pointer-events-none -z-10">
|
||||
<div className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-indigo/10 rounded-full blur-[120px]" />
|
||||
<div className="absolute bottom-[-10%] right-[-10%] w-[40%] h-[40%] bg-blue-900/10 rounded-full blur-[120px]" />
|
||||
<div className="absolute top-[20%] right-[20%] w-[30%] h-[30%] bg-emerald/5 rounded-full blur-[100px]" />
|
||||
<div className="absolute bottom-[30%] left-[15%] w-[25%] h-[25%] bg-purple-500/5 rounded-full blur-[80px]" />
|
||||
</div>
|
||||
|
||||
<Navbar />
|
||||
|
||||
<main className="flex-1 flex flex-col min-h-[calc(100vh-4rem)]">
|
||||
<div className="w-full px-4 sm:px-6 lg:px-12 xl:px-20 py-4 sm:py-6 relative">
|
||||
<div className="max-w-7xl mx-auto w-full z-10 py-8">
|
||||
{stories.length === 0 ? (
|
||||
<div className="text-center py-20">
|
||||
<div className="inline-flex items-center justify-center w-32 h-40 mb-6 bg-white/5 border-2 border-dashed border-border rounded-sm">
|
||||
<Plus className="w-16 h-16 text-muted-foreground/50" />
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold mb-2">No comics yet</h2>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
Create your first comic story to build your library!
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4">
|
||||
{stories.map((story) => (
|
||||
<button
|
||||
key={story.id}
|
||||
onClick={() => router.push(`/editor/${story.slug}`)}
|
||||
className="group relative bg-white aspect-[3/4] p-2 shadow-2xl rounded-sm hover:shadow-indigo/20 hover:shadow-3xl transition-all duration-300 hover:scale-[1.02] hover:-translate-y-1"
|
||||
>
|
||||
<div className="w-full h-full bg-neutral-900 border-4 border-black overflow-hidden relative">
|
||||
{story.coverImage ? (
|
||||
<>
|
||||
<img
|
||||
src={story.coverImage}
|
||||
alt={story.title}
|
||||
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105 opacity-80"
|
||||
/>
|
||||
<main className="flex-1 flex flex-col min-h-[calc(100vh-4rem)]">
|
||||
<div className="w-full px-4 sm:px-6 lg:px-12 xl:px-20 py-4 sm:py-6 relative">
|
||||
<div className="max-w-7xl mx-auto w-full z-10 py-8">
|
||||
{stories.length === 0 ? (
|
||||
<div className="opacity-0 animate-fade-in-up animation-delay-100 text-center py-20">
|
||||
<div className="relative mb-8">
|
||||
<div className="inline-flex items-center justify-center w-40 h-52 glass-panel border-2 border-dashed border-border rounded-lg hover:border-indigo/30 hover:shadow-indigo/10 hover:shadow-lg transition-all duration-300 group">
|
||||
<Plus className="w-20 h-20 text-muted-foreground/50 group-hover:text-indigo/40 transition-colors duration-300" />
|
||||
</div>
|
||||
<div className="absolute -inset-1 bg-gradient-to-r from-indigo/20 to-emerald/20 rounded-lg blur opacity-0 group-hover:opacity-100 transition-opacity duration-300 -z-10" />
|
||||
</div>
|
||||
<h2 className="text-4xl font-heading font-semibold mb-4 text-foreground tracking-tight">
|
||||
Your Comic Library Awaits
|
||||
</h2>
|
||||
<p className="text-muted-foreground mb-10 max-w-lg mx-auto leading-relaxed text-lg">
|
||||
Start crafting your first visual story. Every great comic begins with a single panel.
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => router.push('/')}
|
||||
className="glass-panel glass-panel-hover px-8 py-4 text-base font-medium hover:scale-105 transition-all duration-300 hover:shadow-indigo/20 hover:shadow-lg"
|
||||
>
|
||||
<Plus className="w-5 h-5 mr-3" />
|
||||
Create Your First Comic
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="opacity-0 animate-fade-in-up animation-delay-100 mb-12">
|
||||
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-heading font-semibold text-foreground mb-4 tracking-tight">
|
||||
Your Comic Library
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-base sm:text-lg max-w-3xl leading-relaxed">
|
||||
Browse and continue your comic creations. Each story is a unique visual narrative waiting to unfold.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{story.pageCount > 1 && (
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<div className="absolute top-0 left-0 right-0 bottom-0 translate-x-1 translate-y-1 bg-black/20" />
|
||||
{story.pageCount > 2 && (
|
||||
<div className="absolute top-0 left-0 right-0 bottom-0 translate-x-2 translate-y-2 bg-black/10" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="opacity-0 animate-fade-in-up animation-delay-200 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4 sm:gap-6">
|
||||
{stories.map((story, index) => (
|
||||
<button
|
||||
key={story.id}
|
||||
onClick={() => router.push(`/editor/${story.slug}`)}
|
||||
className={`opacity-0 animate-fade-in-up group relative glass-panel p-3 rounded-lg hover:shadow-indigo/20 hover:shadow-2xl transition-all duration-500 hover:scale-[1.05] hover:-translate-y-2 border border-border/50 hover:border-indigo/30 hover:bg-white/[0.02] backdrop-blur-sm focus:outline-none`}
|
||||
style={{ animationDelay: `${200 + index * 100}ms` }}
|
||||
>
|
||||
<div className="w-full h-full bg-neutral-900 border-4 border-black overflow-hidden relative group-hover:border-indigo/30 transition-colors duration-300">
|
||||
{story.coverImage ? (
|
||||
<>
|
||||
<img
|
||||
src={story.coverImage}
|
||||
alt={story.title}
|
||||
className="w-full h-full object-cover transition-all duration-500 group-hover:scale-105 group-hover:brightness-110 opacity-90"
|
||||
/>
|
||||
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/20 to-transparent" />
|
||||
{story.pageCount > 1 && (
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<div className="absolute top-0 left-0 right-0 bottom-0 translate-x-1 translate-y-1 bg-black/20 group-hover:bg-indigo/10 transition-colors duration-300" />
|
||||
{story.pageCount > 2 && (
|
||||
<div className="absolute top-0 left-0 right-0 bottom-0 translate-x-2 translate-y-2 bg-black/10 group-hover:bg-indigo/5 transition-colors duration-300" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="absolute top-2 right-2 px-1.5 py-0.5 bg-black/70 text-white text-[9px] font-mono uppercase tracking-widest border border-white/10">
|
||||
{story.pageCount}p
|
||||
</div>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/95 via-black/30 to-transparent group-hover:from-black/90 transition-all duration-300" />
|
||||
|
||||
<div className="absolute bottom-0 left-0 right-0 p-2 text-left">
|
||||
<h3 className="font-display text-xs text-white leading-tight line-clamp-2 mb-0.5">
|
||||
{story.title}
|
||||
</h3>
|
||||
<p className="text-[9px] text-white/50 font-mono uppercase tracking-wider">
|
||||
{new Date(story.createdAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-white/40 mx-auto mb-2" />
|
||||
<p className="text-[9px] text-white/50 font-mono uppercase tracking-wider">Generating...</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Subtle glow effect on hover */}
|
||||
<div className="absolute inset-0 opacity-0 group-hover:opacity-20 bg-gradient-to-r from-indigo/20 via-transparent to-emerald/20 transition-opacity duration-500" />
|
||||
|
||||
<div className="absolute top-2 right-2 px-1.5 py-0.5 bg-black/80 text-white text-[9px] font-mono uppercase tracking-widest border border-white/20 group-hover:bg-indigo/80 group-hover:border-indigo/40 transition-colors duration-300">
|
||||
{COMIC_STYLES.find(s => s.id === story.style)?.name.toUpperCase() || story.style.toUpperCase()}
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-0 left-0 right-0 p-3 text-left">
|
||||
<h3 className="font-display text-sm text-white leading-tight line-clamp-1 mb-1 group-hover:text-indigo-100 transition-colors duration-300">
|
||||
{story.title}
|
||||
</h3>
|
||||
<p className="text-[10px] text-white/60 font-mono uppercase tracking-wider group-hover:text-white/80 transition-colors duration-300">
|
||||
{new Date(story.createdAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-neutral-800 to-neutral-900">
|
||||
<div className="text-center">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-indigo/60 mx-auto mb-3" />
|
||||
<p className="text-xs text-white/70 font-mono uppercase tracking-wider">Generating...</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
DialogDescription,
|
||||
} from "@/components/ui/dialog";
|
||||
import { TOGETHER_LINK } from "@/lib/utils";
|
||||
import { useApiKey } from "@/hooks/use-api-key";
|
||||
|
||||
interface ApiKeyModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -21,38 +22,35 @@ interface ApiKeyModalProps {
|
||||
}
|
||||
|
||||
export function ApiKeyModal({ isOpen, onClose, onSubmit }: ApiKeyModalProps) {
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [apiKeyInput, setApiKeyInput] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [existingKey, setExistingKey] = useState<string | null>(null);
|
||||
const [existingKey, setApiKey] = useApiKey();
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined" && isOpen) {
|
||||
const storedKey = localStorage.getItem("together_api_key");
|
||||
setExistingKey(storedKey);
|
||||
setApiKey((current) => {
|
||||
if (storedKey && current === "") {
|
||||
return storedKey;
|
||||
if (isOpen) {
|
||||
setApiKeyInput((current) => {
|
||||
if (existingKey && current === "") {
|
||||
return existingKey;
|
||||
}
|
||||
return current;
|
||||
});
|
||||
}
|
||||
}, [isOpen]);
|
||||
}, [isOpen, existingKey]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!apiKey.trim()) return;
|
||||
if (!apiKeyInput.trim()) return;
|
||||
|
||||
setIsLoading(true);
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
setIsLoading(false);
|
||||
onSubmit(apiKey.trim());
|
||||
setApiKey("");
|
||||
onSubmit(apiKeyInput.trim());
|
||||
setApiKeyInput("");
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
localStorage.removeItem("together_api_key");
|
||||
setExistingKey(null);
|
||||
setApiKey("");
|
||||
setApiKey(null);
|
||||
setApiKeyInput("");
|
||||
onClose();
|
||||
};
|
||||
|
||||
@@ -83,15 +81,15 @@ export function ApiKeyModal({ isOpen, onClose, onSubmit }: ApiKeyModalProps) {
|
||||
<div className="relative">
|
||||
<Input
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
value={apiKeyInput}
|
||||
onChange={(e) => setApiKeyInput(e.target.value)}
|
||||
placeholder={existingKey ? "Your current API key" : "Enter your API key..."}
|
||||
className="bg-secondary border-border/50 text-white placeholder-muted-foreground py-5 pr-10"
|
||||
/>
|
||||
{apiKey && (
|
||||
{apiKeyInput && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setApiKey("")}
|
||||
onClick={() => setApiKeyInput("")}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-white transition-colors"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
@@ -120,7 +118,7 @@ export function ApiKeyModal({ isOpen, onClose, onSubmit }: ApiKeyModalProps) {
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!apiKey.trim() || isLoading}
|
||||
disabled={!apiKeyInput.trim() || isLoading}
|
||||
className="flex-1 gap-2 bg-white hover:bg-neutral-200 text-black"
|
||||
>
|
||||
{isLoading ? "Validating..." : "Continue"}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { RefreshCw, Download } from "lucide-react";
|
||||
import { RefreshCw, Share, Info, Loader2, Trash2 } from "lucide-react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
interface PageData {
|
||||
@@ -8,15 +9,38 @@ interface PageData {
|
||||
title: string;
|
||||
image: string;
|
||||
prompt: string;
|
||||
characterUpload?: string;
|
||||
characterUploads?: string[];
|
||||
style: string;
|
||||
dbId?: string;
|
||||
}
|
||||
|
||||
interface ComicCanvasProps {
|
||||
page: PageData;
|
||||
pageIndex: number;
|
||||
totalPages?: number;
|
||||
isLoading?: boolean;
|
||||
isOwner?: boolean;
|
||||
onInfoClick?: () => void;
|
||||
onRedrawClick?: () => void;
|
||||
onDeletePage?: () => void;
|
||||
onNextPage?: () => void;
|
||||
onPrevPage?: () => void;
|
||||
}
|
||||
|
||||
export function ComicCanvas({ page }: ComicCanvasProps) {
|
||||
export function ComicCanvas({
|
||||
page,
|
||||
pageIndex,
|
||||
totalPages = 1,
|
||||
isLoading = false,
|
||||
isOwner = true,
|
||||
onInfoClick,
|
||||
onRedrawClick,
|
||||
onDeletePage,
|
||||
onNextPage,
|
||||
onPrevPage,
|
||||
}: ComicCanvasProps) {
|
||||
const { toast } = useToast();
|
||||
|
||||
return (
|
||||
<main className="flex-1 overflow-auto p-4 md:p-8 flex items-start justify-center relative">
|
||||
{/* Dot grid background */}
|
||||
@@ -32,7 +56,20 @@ export function ComicCanvas({ page }: ComicCanvasProps) {
|
||||
<img
|
||||
src={page.image || "/placeholder.svg"}
|
||||
alt={`Page ${page.id}`}
|
||||
className="w-full h-full object-cover opacity-90 grayscale-10 contrast-110"
|
||||
className="w-full h-full object-cover opacity-90 grayscale-10 contrast-110 cursor-pointer"
|
||||
onClick={(e) => {
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const clickX = e.clientX - rect.left;
|
||||
const imageWidth = rect.width;
|
||||
|
||||
if (clickX > imageWidth / 2) {
|
||||
// Right half - next page
|
||||
onNextPage?.();
|
||||
} else {
|
||||
// Left half - previous page
|
||||
onPrevPage?.();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="scan-line opacity-30" />
|
||||
@@ -44,25 +81,93 @@ export function ComicCanvas({ page }: ComicCanvasProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action buttons below the page image */}
|
||||
<div className="flex items-center justify-center gap-2 mt-4">
|
||||
{onInfoClick && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="hover:bg-secondary text-muted-foreground hover:text-white h-9 w-9"
|
||||
onClick={onInfoClick}
|
||||
>
|
||||
<Info className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{isOwner && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="hover:bg-secondary text-muted-foreground hover:text-white gap-2 text-xs h-9 px-3"
|
||||
onClick={onRedrawClick}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
)}
|
||||
<span>{isLoading ? "Redrawing..." : "Redraw"}</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{isOwner && totalPages > 1 && onDeletePage && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="hover:bg-red-600/20 text-muted-foreground hover:text-red-400 gap-2 text-xs h-9 px-3"
|
||||
onClick={onDeletePage}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
<span>Delete</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-3 mt-4">
|
||||
{/* <div className="text-xs text-muted-foreground">Page {page.id}</div> */}
|
||||
|
||||
{/* Mobile action buttons */}
|
||||
<div className="flex items-center gap-2 md:hidden">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="hover:bg-secondary text-muted-foreground hover:text-white gap-2 text-xs h-9 px-3 flex-1"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
<span>Redraw</span>
|
||||
</Button>
|
||||
{isOwner && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="hover:bg-secondary text-muted-foreground hover:text-white gap-2 text-xs h-9 px-3 flex-1"
|
||||
onClick={onRedrawClick}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
)}
|
||||
<span>{isLoading ? "Redrawing..." : "Redraw"}</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="hover:bg-secondary text-muted-foreground hover:text-white gap-2 text-xs h-9 px-3 flex-1"
|
||||
onClick={async () => {
|
||||
const url = window.location.href;
|
||||
try {
|
||||
await navigator.clipboard.writeText(url);
|
||||
toast({
|
||||
title: "Link copied!",
|
||||
description: "Story URL has been copied to your clipboard.",
|
||||
duration: 2000,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to copy URL:", err);
|
||||
toast({
|
||||
title: "Failed to copy",
|
||||
description: "Could not copy the URL to clipboard.",
|
||||
variant: "destructive",
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
<span>Download</span>
|
||||
<Share className="w-4 h-4" />
|
||||
<span>Share</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { ArrowLeft, RefreshCw, Download, Plus, Info } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { ArrowLeft, RefreshCw, Share, Plus, Info } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
interface EditorToolbarProps {
|
||||
title: string
|
||||
onContinueStory: () => void
|
||||
onInfoClick: () => void
|
||||
title: string;
|
||||
onContinueStory?: () => void;
|
||||
isOwner?: boolean;
|
||||
}
|
||||
|
||||
export function EditorToolbar({ title, onContinueStory, onInfoClick }: EditorToolbarProps) {
|
||||
const router = useRouter()
|
||||
export function EditorToolbar({
|
||||
title,
|
||||
onContinueStory,
|
||||
isOwner = true,
|
||||
}: EditorToolbarProps) {
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
|
||||
return (
|
||||
<header className="h-14 border-b border-border/50 bg-background/80 backdrop-blur-md flex items-center justify-between px-3 sm:px-4">
|
||||
@@ -19,50 +25,56 @@ export function EditorToolbar({ title, onContinueStory, onInfoClick }: EditorToo
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => router.push("/stories")}
|
||||
className="hover:bg-secondary text-muted-foreground hover:text-white flex-shrink-0"
|
||||
onClick={() => (isOwner ? router.push("/stories") : router.push("/"))}
|
||||
className="hover:bg-secondary text-muted-foreground hover:text-white shrink-0"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 sm:w-5 sm:h-5" />
|
||||
</Button>
|
||||
|
||||
<h1 className="text-sm sm:text-base text-white font-normal tracking-[-0.02em] truncate">{title}</h1>
|
||||
<h1 className="text-sm sm:text-base text-white font-normal tracking-[-0.02em] truncate">
|
||||
{title}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 sm:gap-2 flex-shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="hover:bg-secondary text-muted-foreground hover:text-white h-8 w-8 sm:h-9 sm:w-9"
|
||||
onClick={onInfoClick}
|
||||
>
|
||||
<Info className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center gap-1 sm:gap-2 shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="hover:bg-secondary text-muted-foreground hover:text-white gap-1.5 sm:gap-2 text-xs h-8 sm:h-9 px-2 sm:px-3 hidden md:flex"
|
||||
onClick={async () => {
|
||||
const url = window.location.href;
|
||||
try {
|
||||
await navigator.clipboard.writeText(url);
|
||||
toast({
|
||||
title: "Link copied!",
|
||||
description: "Story URL has been copied to your clipboard.",
|
||||
duration: 2000,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to copy URL:", err);
|
||||
toast({
|
||||
title: "Failed to copy",
|
||||
description: "Could not copy the URL to clipboard.",
|
||||
variant: "destructive",
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<RefreshCw className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
|
||||
<span>Redraw</span>
|
||||
<Share className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
|
||||
<span>Share</span>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="hover:bg-secondary text-muted-foreground hover:text-white gap-1.5 sm:gap-2 text-xs h-8 sm:h-9 px-2 sm:px-3 hidden md:flex"
|
||||
>
|
||||
<Download className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
|
||||
<span>Download PDF</span>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={onContinueStory}
|
||||
className="gap-1.5 sm:gap-2 text-xs bg-white hover:bg-neutral-200 text-black h-8 sm:h-9 px-3 sm:px-4"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
|
||||
<span className="hidden sm:inline">Continue story</span>
|
||||
<span className="sm:hidden">Add</span>
|
||||
</Button>
|
||||
{isOwner && onContinueStory && (
|
||||
<Button
|
||||
onClick={onContinueStory}
|
||||
className="gap-1.5 sm:gap-2 text-xs bg-white hover:bg-neutral-200 text-black h-8 sm:h-9 px-3 sm:px-4"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
|
||||
<span className="hidden sm:inline">Continue story</span>
|
||||
<span className="sm:hidden">Add</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,28 +1,40 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect, useMemo } from "react"
|
||||
import { Upload, X, Loader2 } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import { useToast } from "@/hooks/use-toast"
|
||||
import { validateFileForUpload, generateFilePreview } from "@/lib/file-utils"
|
||||
import { COMIC_STYLES } from "@/lib/constants"
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { Upload, X, Loader2, Check } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogClose,
|
||||
} from "@/components/ui/dialog";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { useKeyboardShortcut } from "@/hooks/use-keyboard-shortcut";
|
||||
import { validateFileForUpload, generateFilePreview } from "@/lib/file-utils";
|
||||
import { useS3Upload } from "next-s3-upload";
|
||||
|
||||
interface CharacterItem {
|
||||
url: string;
|
||||
isNew?: boolean;
|
||||
file?: File;
|
||||
preview?: string;
|
||||
}
|
||||
|
||||
interface GeneratePageModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onGenerate: (data: {
|
||||
prompt: string
|
||||
style: string
|
||||
characterFiles?: File[]
|
||||
characterUrls?: string[] // For reusing existing characters
|
||||
isContinuation?: boolean
|
||||
}) => void
|
||||
pageNumber: number
|
||||
previousCharacters?: File[]
|
||||
previousPagePrompt?: string
|
||||
previousPageStyle?: string
|
||||
existingCharacterImages?: string[] // Character images from previous pages
|
||||
prompt: string;
|
||||
characterUrls?: string[];
|
||||
}) => Promise<void>;
|
||||
pageNumber: number;
|
||||
isRedrawMode?: boolean;
|
||||
existingPrompt?: string;
|
||||
existingCharacters?: string[]; // All characters from the story
|
||||
lastPageCharacters?: string[]; // Characters used on the last page
|
||||
previousPageCharacters?: string[]; // Characters used on the previous page (if last page had < 2)
|
||||
}
|
||||
|
||||
export function GeneratePageModal({
|
||||
@@ -30,55 +42,103 @@ export function GeneratePageModal({
|
||||
onClose,
|
||||
onGenerate,
|
||||
pageNumber,
|
||||
previousCharacters,
|
||||
previousPagePrompt,
|
||||
previousPageStyle,
|
||||
existingCharacterImages = [],
|
||||
isRedrawMode = false,
|
||||
existingPrompt = "",
|
||||
existingCharacters = [],
|
||||
lastPageCharacters = [],
|
||||
previousPageCharacters = [],
|
||||
}: GeneratePageModalProps) {
|
||||
const [prompt, setPrompt] = useState("")
|
||||
const [uploadedFiles, setUploadedFiles] = useState<File[]>(previousCharacters || [])
|
||||
const [selectedExistingCharacters, setSelectedExistingCharacters] = useState<string[]>([])
|
||||
const [previews, setPreviews] = useState<string[]>([])
|
||||
const [showPreview, setShowPreview] = useState<number | null>(null)
|
||||
const [isGenerating, setIsGenerating] = useState(false)
|
||||
const [isContinuing, setIsContinuing] = useState(false)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const { toast } = useToast()
|
||||
|
||||
const selectedStyleId = previousPageStyle || "noir"
|
||||
const selectedStyle = useMemo(
|
||||
() => COMIC_STYLES.find((s) => s.id === selectedStyleId)?.name || "Noir",
|
||||
[selectedStyleId]
|
||||
)
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const [characters, setCharacters] = useState<CharacterItem[]>([]);
|
||||
const [selectedCharacterIndices, setSelectedCharacterIndices] = useState<
|
||||
Set<number>
|
||||
>(new Set());
|
||||
const [showPreview, setShowPreview] = useState<string | null>(null);
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const { toast } = useToast();
|
||||
const { uploadToS3 } = useS3Upload();
|
||||
|
||||
// Reset form and initialize characters when modal opens
|
||||
useEffect(() => {
|
||||
if (previousCharacters && previousCharacters.length > 0) {
|
||||
const newPreviews: string[] = []
|
||||
previousCharacters.forEach((file, index) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
newPreviews[index] = e.target?.result as string
|
||||
if (newPreviews.filter(Boolean).length === previousCharacters.length) {
|
||||
setPreviews([...newPreviews])
|
||||
if (isOpen) {
|
||||
setPrompt(isRedrawMode ? existingPrompt : "");
|
||||
setShowPreview(null);
|
||||
setIsGenerating(false);
|
||||
|
||||
// Initialize characters list with existing ones
|
||||
const existingItems: CharacterItem[] = existingCharacters.map((url) => ({
|
||||
url,
|
||||
isNew: false,
|
||||
}));
|
||||
setCharacters(existingItems);
|
||||
|
||||
// Smart selection: Use last 2 characters from last page, or combine with previous page if needed
|
||||
const defaultSelected = new Set<number>();
|
||||
const charactersToSelect: string[] = [];
|
||||
|
||||
// If last page has 2 characters, use those
|
||||
if (lastPageCharacters.length >= 2) {
|
||||
charactersToSelect.push(...lastPageCharacters.slice(0, 2));
|
||||
} else {
|
||||
// Start with last page characters (if any)
|
||||
charactersToSelect.push(...lastPageCharacters);
|
||||
|
||||
// If we have less than 2, add from previous page (avoiding duplicates)
|
||||
if (
|
||||
charactersToSelect.length < 2 &&
|
||||
previousPageCharacters.length > 0
|
||||
) {
|
||||
for (const charUrl of previousPageCharacters) {
|
||||
if (
|
||||
!charactersToSelect.includes(charUrl) &&
|
||||
charactersToSelect.length < 2
|
||||
) {
|
||||
charactersToSelect.push(charUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
}
|
||||
|
||||
// Find indices of characters to select (preserving order in existingItems)
|
||||
charactersToSelect.forEach((charUrl) => {
|
||||
const index = existingItems.findIndex((item) => item.url === charUrl);
|
||||
if (index !== -1) {
|
||||
defaultSelected.add(index);
|
||||
}
|
||||
});
|
||||
|
||||
setSelectedCharacterIndices(defaultSelected);
|
||||
}
|
||||
}, [previousCharacters])
|
||||
}, [
|
||||
isOpen,
|
||||
isRedrawMode,
|
||||
existingPrompt,
|
||||
existingCharacters,
|
||||
lastPageCharacters,
|
||||
previousPageCharacters,
|
||||
]);
|
||||
|
||||
// Keyboard shortcut for form submission (disabled during generation)
|
||||
useKeyboardShortcut(
|
||||
() => {
|
||||
if (isOpen && !isGenerating && prompt.trim()) {
|
||||
handleGenerate();
|
||||
}
|
||||
},
|
||||
{ disabled: !isOpen || isGenerating }
|
||||
);
|
||||
|
||||
const handleFiles = async (newFiles: FileList | null) => {
|
||||
if (!newFiles) return
|
||||
if (!newFiles) return;
|
||||
|
||||
const filesArray = Array.from(newFiles)
|
||||
const filesArray = Array.from(newFiles);
|
||||
|
||||
// Validate files (including WebP rejection)
|
||||
const validationResults = filesArray.map(file => ({
|
||||
const validationResults = filesArray.map((file) => ({
|
||||
file,
|
||||
validation: validateFileForUpload(file, true)
|
||||
}))
|
||||
validation: validateFileForUpload(file, true),
|
||||
}));
|
||||
|
||||
// Show errors for invalid files
|
||||
validationResults.forEach(({ validation }) => {
|
||||
if (!validation.valid && validation.error) {
|
||||
toast({
|
||||
@@ -86,255 +146,326 @@ export function GeneratePageModal({
|
||||
description: validation.error,
|
||||
variant: "destructive",
|
||||
duration: 4000,
|
||||
})
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const validFiles = validationResults
|
||||
.filter(({ validation }) => validation.valid)
|
||||
.map(({ file }) => file)
|
||||
.map(({ file }) => file);
|
||||
|
||||
if (validFiles.length === 0) return
|
||||
if (validFiles.length === 0) return;
|
||||
|
||||
const totalFiles = [...uploadedFiles, ...validFiles].slice(0, 2) // Max 2 files
|
||||
setUploadedFiles(totalFiles)
|
||||
// Create new character items for the uploaded files
|
||||
const newCharacterItems: CharacterItem[] = await Promise.all(
|
||||
validFiles.map(async (file) => {
|
||||
const preview = await generateFilePreview(file);
|
||||
return {
|
||||
url: "", // Will be set after S3 upload
|
||||
isNew: true,
|
||||
file,
|
||||
preview,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
// Generate previews for all files
|
||||
const newPreviews = await Promise.all(
|
||||
totalFiles.map((file) => generateFilePreview(file))
|
||||
)
|
||||
setPreviews(newPreviews)
|
||||
}
|
||||
// Add new characters to the list
|
||||
setCharacters((prev) => {
|
||||
const updated = [...prev, ...newCharacterItems];
|
||||
const newSelected = new Set(selectedCharacterIndices);
|
||||
|
||||
// Add new characters to selection
|
||||
newCharacterItems.forEach((_, idx) => {
|
||||
newSelected.add(prev.length + idx);
|
||||
});
|
||||
|
||||
// If we have more than 2 selected, deselect the oldest ones (keep most recent 2)
|
||||
if (newSelected.size > 2) {
|
||||
const selectedArray = Array.from(newSelected).sort((a, b) => b - a);
|
||||
const toKeep = selectedArray.slice(0, 2);
|
||||
newSelected.clear();
|
||||
toKeep.forEach((idx) => newSelected.add(idx));
|
||||
}
|
||||
|
||||
setSelectedCharacterIndices(newSelected);
|
||||
return updated;
|
||||
});
|
||||
|
||||
const removeFile = (index: number) => {
|
||||
const newFiles = uploadedFiles.filter((_, i) => i !== index)
|
||||
const newPreviews = previews.filter((_, i) => i !== index)
|
||||
setUploadedFiles(newFiles)
|
||||
setPreviews(newPreviews)
|
||||
setShowPreview(null)
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = ""
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleExistingCharacter = (characterUrl: string) => {
|
||||
setSelectedExistingCharacters(prev =>
|
||||
prev.includes(characterUrl)
|
||||
? prev.filter(url => url !== characterUrl)
|
||||
: [...prev, characterUrl]
|
||||
)
|
||||
}
|
||||
const toggleCharacterSelection = (index: number) => {
|
||||
setSelectedCharacterIndices((prev) => {
|
||||
const newSelected = new Set(prev);
|
||||
if (newSelected.has(index)) {
|
||||
// Allow deselection even if only 2 are selected
|
||||
newSelected.delete(index);
|
||||
} else {
|
||||
// If already at max (2), remove the oldest selected first
|
||||
if (newSelected.size >= 2) {
|
||||
const selectedArray = Array.from(newSelected).sort((a, b) => a - b);
|
||||
newSelected.delete(selectedArray[0]); // Remove oldest
|
||||
}
|
||||
newSelected.add(index);
|
||||
}
|
||||
return newSelected;
|
||||
});
|
||||
};
|
||||
|
||||
const handleGenerate = () => {
|
||||
if (!prompt.trim()) return
|
||||
setIsGenerating(true)
|
||||
onGenerate({
|
||||
prompt,
|
||||
style: selectedStyle,
|
||||
characterFiles: uploadedFiles.length > 0 ? uploadedFiles : undefined,
|
||||
characterUrls: selectedExistingCharacters.length > 0 ? selectedExistingCharacters : undefined,
|
||||
isContinuation: false,
|
||||
})
|
||||
}
|
||||
const removeCharacter = (index: number) => {
|
||||
setCharacters((prev) => {
|
||||
const updated = prev.filter((_, i) => i !== index);
|
||||
|
||||
const handleContinue = () => {
|
||||
setIsContinuing(true)
|
||||
onGenerate({
|
||||
prompt: prompt.trim() || `Continue the story from where it left off. Previous context: ${previousPagePrompt}`,
|
||||
style: selectedStyle,
|
||||
characterFiles: uploadedFiles.length > 0 ? uploadedFiles : undefined,
|
||||
characterUrls: selectedExistingCharacters.length > 0 ? selectedExistingCharacters : undefined,
|
||||
isContinuation: true,
|
||||
})
|
||||
}
|
||||
// Adjust selected indices
|
||||
setSelectedCharacterIndices((prevSelected) => {
|
||||
const newSelected = new Set<number>();
|
||||
prevSelected.forEach((idx) => {
|
||||
if (idx < index) {
|
||||
newSelected.add(idx);
|
||||
} else if (idx > index) {
|
||||
newSelected.add(idx - 1);
|
||||
}
|
||||
// Skip the removed index
|
||||
});
|
||||
return newSelected;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setIsGenerating(false)
|
||||
setIsContinuing(false)
|
||||
setPrompt("")
|
||||
return updated;
|
||||
});
|
||||
setShowPreview(null);
|
||||
};
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (!prompt.trim()) return;
|
||||
setIsGenerating(true);
|
||||
|
||||
try {
|
||||
// Get selected characters
|
||||
const selectedCharacters = Array.from(selectedCharacterIndices)
|
||||
.sort((a, b) => a - b)
|
||||
.map((idx) => characters[idx])
|
||||
.filter(Boolean);
|
||||
|
||||
// Upload new files to S3 and get URLs, reuse existing URLs
|
||||
const characterUrls = await Promise.all(
|
||||
selectedCharacters.map(async (char) => {
|
||||
if (char.isNew && char.file) {
|
||||
// Upload new file to S3
|
||||
const { url } = await uploadToS3(char.file);
|
||||
return url;
|
||||
} else {
|
||||
// Reuse existing URL
|
||||
return char.url;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
await onGenerate({
|
||||
prompt,
|
||||
characterUrls: characterUrls.length > 0 ? characterUrls : undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error generating page:", error);
|
||||
toast({
|
||||
title: "Generation failed",
|
||||
description:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to generate page. Please try again.",
|
||||
variant: "destructive",
|
||||
duration: 4000,
|
||||
});
|
||||
setIsGenerating(false);
|
||||
}
|
||||
}, [isOpen])
|
||||
};
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
// Prevent closing the modal if generation is running
|
||||
if (!open && isGenerating) {
|
||||
return;
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="border border-border/50 rounded-lg bg-background max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl text-white font-heading">Generate Page {pageNumber}</DialogTitle>
|
||||
<DialogTitle className="text-xl text-white font-heading">
|
||||
{isRedrawMode
|
||||
? `Redraw Page ${pageNumber}`
|
||||
: `Generate Page ${pageNumber}`}
|
||||
</DialogTitle>
|
||||
<DialogClose
|
||||
disabled={isGenerating}
|
||||
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogClose>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 mt-4">
|
||||
{/* Prompt Input */}
|
||||
<div className="relative glass-panel p-1 rounded-xl group focus-within:border-indigo/30 transition-colors">
|
||||
<div className="bg-background/80 rounded-lg p-4 border border-border/50">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<label className="text-[10px] uppercase text-muted-foreground tracking-[0.02em] font-medium">
|
||||
Prompt
|
||||
</label>
|
||||
<div className="flex items-center gap-2 text-[10px] text-muted-foreground">
|
||||
<span className="capitalize">{selectedStyle}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
placeholder="Continue the story... Describe what happens next in the comic."
|
||||
placeholder={
|
||||
isRedrawMode
|
||||
? "Tweak the prompt to improve this page..."
|
||||
: "Continue the story... Describe what happens next."
|
||||
}
|
||||
disabled={isGenerating}
|
||||
className="w-full bg-transparent border-none text-sm text-white placeholder-muted-foreground/50 focus:ring-0 focus:outline-none resize-none h-20 leading-relaxed tracking-tight"
|
||||
/>
|
||||
|
||||
<div className="mt-3 pt-3 border-t border-border/30 space-y-3">
|
||||
{/* Existing Characters */}
|
||||
{existingCharacterImages.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs text-muted-foreground uppercase tracking-[0.02em] font-medium">
|
||||
Reuse Characters from Story
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{existingCharacterImages.map((characterUrl, index) => {
|
||||
const isSelected = selectedExistingCharacters.includes(characterUrl)
|
||||
return (
|
||||
<button
|
||||
key={characterUrl}
|
||||
onClick={() => toggleExistingCharacter(characterUrl)}
|
||||
className={`relative w-8 h-8 rounded-md overflow-hidden border-2 transition-all ${
|
||||
isSelected
|
||||
? "border-indigo shadow-sm shadow-indigo/20"
|
||||
: "border-border/50 hover:border-indigo/50"
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src={characterUrl}
|
||||
alt={`Existing character ${index + 1}`}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
{isSelected && (
|
||||
<div className="absolute inset-0 bg-indigo/20 flex items-center justify-center">
|
||||
<div className="w-3 h-3 bg-indigo rounded-full flex items-center justify-center">
|
||||
<div className="w-1 h-1 bg-white rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Character Selection */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] uppercase text-muted-foreground tracking-[0.02em] font-medium">
|
||||
Characters (select up to 2)
|
||||
</label>
|
||||
|
||||
{/* New Character Uploads */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
{uploadedFiles.length > 0 ? (
|
||||
<div className="flex items-center gap-2">
|
||||
{previews.map((preview, index) => (
|
||||
{/* Existing and new characters list */}
|
||||
{characters.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{characters.map((char, index) => {
|
||||
const isSelected =
|
||||
selectedCharacterIndices.has(index);
|
||||
const imageUrl = char.preview || char.url;
|
||||
|
||||
return (
|
||||
<div key={index} className="relative group/thumb">
|
||||
<button
|
||||
onClick={() => setShowPreview(index)}
|
||||
className="w-8 h-8 rounded-md overflow-hidden border border-border/50 hover:border-indigo/50 transition-colors"
|
||||
type="button"
|
||||
onClick={() => toggleCharacterSelection(index)}
|
||||
onDoubleClick={() => setShowPreview(imageUrl)}
|
||||
disabled={isGenerating}
|
||||
className={`w-10 h-10 rounded-md overflow-hidden transition-all disabled:opacity-50 disabled:cursor-not-allowed relative ${
|
||||
isSelected
|
||||
? "border-2 border-indigo-500"
|
||||
: "border-2 border-transparent hover:border-indigo/50"
|
||||
}`}
|
||||
title="Click to select/deselect, double-click to preview"
|
||||
>
|
||||
<img
|
||||
src={preview || "/placeholder.svg"}
|
||||
alt={`New character ${index + 1}`}
|
||||
src={imageUrl || "/placeholder.svg"}
|
||||
alt={`Character ${index + 1}`}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
{/* Selection indicator */}
|
||||
{isSelected && (
|
||||
<div className="absolute top-0 right-0 w-3.5 h-3.5 bg-indigo-500 rounded-full flex items-center justify-center pointer-events-none z-10 border border-background">
|
||||
<Check className="w-2 h-2 text-white" />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
removeFile(index)
|
||||
}}
|
||||
className="absolute -top-1.5 -right-1.5 w-4 h-4 bg-red-500 hover:bg-red-600 rounded-full flex items-center justify-center opacity-0 group-hover/thumb:opacity-100 transition-opacity"
|
||||
>
|
||||
<X className="w-2.5 h-2.5 text-white" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{uploadedFiles.length < 2 && (
|
||||
<button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="w-8 h-8 rounded-md border border-dashed border-border/50 hover:border-indigo/50 flex items-center justify-center text-muted-foreground hover:text-white transition-colors"
|
||||
>
|
||||
<Upload className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="flex items-center gap-2 text-xs text-muted-foreground hover:text-white transition-colors"
|
||||
>
|
||||
<Upload className="w-3.5 h-3.5" />
|
||||
<span>Upload New Characters</span>
|
||||
<span className="text-muted-foreground/50">(Max 2)</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/jpg"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={(e) => handleFiles(e.target.files)}
|
||||
/>
|
||||
{/* Remove button (only for new uploads) */}
|
||||
{char.isNew && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeCharacter(index);
|
||||
}}
|
||||
disabled={isGenerating}
|
||||
className="absolute -top-1.5 -right-1.5 w-4 h-4 bg-red-500 hover:bg-red-600 rounded-full flex items-center justify-center opacity-0 group-hover/thumb:opacity-100 transition-opacity disabled:opacity-50 z-20"
|
||||
>
|
||||
<X className="w-2.5 h-2.5 text-white" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Upload new character button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
!isGenerating && fileInputRef.current?.click()
|
||||
}
|
||||
disabled={isGenerating}
|
||||
className="flex items-center gap-2 text-xs text-muted-foreground hover:text-white transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<Upload className="w-3.5 h-3.5" />
|
||||
<span>
|
||||
{characters.length === 0
|
||||
? "Add characters (optional, max 2)"
|
||||
: "Upload new character"}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/jpg"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={(e) => handleFiles(e.target.files)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
onClick={handleContinue}
|
||||
disabled={isGenerating || isContinuing}
|
||||
variant="outline"
|
||||
className="flex-1 gap-2 border-indigo/30 text-indigo hover:bg-indigo/10 hover:text-indigo tracking-tight bg-transparent"
|
||||
>
|
||||
{isContinuing ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
<span>Continuing...</span>
|
||||
</>
|
||||
) : (
|
||||
`Continue from Page ${pageNumber - 1}`
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleGenerate}
|
||||
disabled={!prompt.trim() || isGenerating || isContinuing}
|
||||
className="flex-1 gap-2 bg-white hover:bg-neutral-200 text-black tracking-tight"
|
||||
>
|
||||
{isGenerating ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
<span>Generating...</span>
|
||||
</>
|
||||
) : (
|
||||
`Generate Page ${pageNumber}`
|
||||
)}
|
||||
</Button>
|
||||
<div className="text-xs text-muted-foreground/70">
|
||||
{isRedrawMode
|
||||
? "Previous pages and characters automatically referenced."
|
||||
: "Previous page automatically referenced. " +
|
||||
`${selectedCharacterIndices.size} selected characters.`}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleGenerate}
|
||||
disabled={!prompt.trim() || isGenerating}
|
||||
className="w-full gap-2 bg-white hover:bg-neutral-200 text-black tracking-tight"
|
||||
>
|
||||
{isGenerating ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
<span>
|
||||
{isRedrawMode ? "Redrawing page..." : "Generating page..."}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
`${isRedrawMode ? "Redraw" : "Generate"} Page ${pageNumber}`
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{showPreview !== null && previews[showPreview] && (
|
||||
{/* Character Preview Modal */}
|
||||
{showPreview && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/80 backdrop-blur-sm z-[100] flex items-center justify-center p-4"
|
||||
className="fixed inset-0 bg-black/80 backdrop-blur-sm z-100 flex items-center justify-center p-4"
|
||||
onClick={() => setShowPreview(null)}
|
||||
>
|
||||
<div className="relative max-w-2xl max-h-[80vh] glass-panel p-4 rounded-xl z-[101]">
|
||||
<div className="relative max-w-sm max-h-[80vh] glass-panel p-4 rounded-xl z-101">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute top-2 right-2 h-8 w-8 hover:bg-white/10 z-[102]"
|
||||
className="absolute top-2 right-2 h-8 w-8 hover:bg-white/10 z-102"
|
||||
onClick={() => setShowPreview(null)}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
<img
|
||||
src={previews[showPreview] || "/placeholder.svg"}
|
||||
src={showPreview || "/placeholder.svg"}
|
||||
alt="Character preview"
|
||||
className="w-full h-full object-contain rounded-lg"
|
||||
/>
|
||||
@@ -342,5 +473,5 @@ export function GeneratePageModal({
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,25 +1,26 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { Plus, Loader2, Key } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { UserButton } from "@clerk/nextjs"
|
||||
import { Plus, Loader2, Key, Trash2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { UserButton, SignedIn } from "@clerk/nextjs";
|
||||
|
||||
interface PageData {
|
||||
id: number
|
||||
title: string
|
||||
image: string
|
||||
prompt: string
|
||||
characterUpload?: string
|
||||
style: string
|
||||
id: number;
|
||||
title: string;
|
||||
image: string;
|
||||
prompt: string;
|
||||
characterUpload?: string;
|
||||
style: string;
|
||||
}
|
||||
|
||||
interface PageSidebarProps {
|
||||
pages: PageData[]
|
||||
currentPage: number
|
||||
onPageSelect: (index: number) => void
|
||||
onAddPage: () => void
|
||||
loadingPageId?: number | null
|
||||
onApiKeyClick?: () => void
|
||||
pages: PageData[];
|
||||
currentPage: number;
|
||||
onPageSelect: (index: number) => void;
|
||||
onAddPage: () => void;
|
||||
loadingPageId?: number | null;
|
||||
onApiKeyClick?: () => void;
|
||||
isOwner?: boolean;
|
||||
}
|
||||
|
||||
export function PageSidebar({
|
||||
@@ -29,39 +30,63 @@ export function PageSidebar({
|
||||
onAddPage,
|
||||
loadingPageId,
|
||||
onApiKeyClick,
|
||||
isOwner = true,
|
||||
}: PageSidebarProps) {
|
||||
return (
|
||||
<aside className="w-16 md:w-20 border-r border-border/50 bg-background/50 flex flex-col items-center py-4 gap-2 justify-between">
|
||||
{/* Top section: page numbers */}
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<aside className="w-20 md:w-24 border-r border-border/50 bg-background/50 flex flex-col items-center py-4 gap-2 justify-between">
|
||||
{/* Top section: page thumbnails */}
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
{pages.map((page, index) => (
|
||||
<button
|
||||
key={page.id}
|
||||
onClick={() => onPageSelect(index)}
|
||||
disabled={loadingPageId === page.id}
|
||||
disabled={loadingPageId === index}
|
||||
className={`
|
||||
w-9 h-9 rounded-md transition-all
|
||||
flex items-center justify-center font-medium text-sm tracking-tight
|
||||
w-16 h-16 rounded-lg transition-all relative overflow-hidden
|
||||
${
|
||||
currentPage === index
|
||||
? "bg-black border-2 border-indigo text-white shadow-lg shadow-indigo/20"
|
||||
: "glass-panel glass-panel-hover text-muted-foreground hover:text-white"
|
||||
? "ring-2 ring-indigo shadow-lg shadow-indigo/20"
|
||||
: "glass-panel glass-panel-hover hover:ring-1 hover:ring-white/20"
|
||||
}
|
||||
${loadingPageId === page.id ? "opacity-50" : ""}
|
||||
${loadingPageId === index ? "opacity-50" : ""}
|
||||
`}
|
||||
>
|
||||
{loadingPageId === page.id ? <Loader2 className="w-4 h-4 animate-spin" /> : index + 1}
|
||||
{loadingPageId === index ? (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-white" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<img
|
||||
src={page.image || "/placeholder.svg"}
|
||||
alt={`Page ${index + 1}`}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div
|
||||
className={`
|
||||
absolute bottom-1 left-1 px-1.5 py-0.5 rounded text-[10px] font-medium tracking-tight
|
||||
${
|
||||
currentPage === index
|
||||
? "bg-indigo text-white"
|
||||
: "bg-black/70 text-white"
|
||||
}
|
||||
`}
|
||||
>
|
||||
{index + 1}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
|
||||
<Button
|
||||
onClick={onAddPage}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="w-9 h-9 bg-white hover:bg-neutral-200 text-black border-0 transition-all group"
|
||||
>
|
||||
<Plus className="w-4 h-4 text-black transition-transform group-hover:scale-110" />
|
||||
</Button>
|
||||
{isOwner && (
|
||||
<button
|
||||
onClick={onAddPage}
|
||||
className="w-16 h-16 rounded-lg border-2 border-dashed border-border/50 hover:border-indigo/50 bg-background/50 hover:bg-background/80 transition-all group flex items-center justify-center"
|
||||
>
|
||||
<Plus className="w-6 h-6 text-muted-foreground group-hover:text-indigo transition-transform group-hover:scale-110" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
@@ -70,22 +95,24 @@ export function PageSidebar({
|
||||
onClick={onApiKeyClick}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="w-9 h-9 glass-panel glass-panel-hover text-muted-foreground hover:text-white"
|
||||
className="w-10 h-10 glass-panel glass-panel-hover text-muted-foreground hover:text-white"
|
||||
title="Manage API Key"
|
||||
>
|
||||
<Key className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
<div className="w-9 h-9 glass-panel glass-panel-hover rounded-md flex items-center justify-center text-muted-foreground hover:text-white transition-colors">
|
||||
<UserButton
|
||||
appearance={{
|
||||
elements: {
|
||||
avatarBox: "w-full h-full rounded-md",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<SignedIn>
|
||||
<div className="w-10 h-10 glass-panel glass-panel-hover rounded-md flex items-center justify-center text-muted-foreground hover:text-white transition-colors">
|
||||
<UserButton
|
||||
appearance={{
|
||||
elements: {
|
||||
avatarBox: "w-full h-full rounded-md",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</SignedIn>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
import { useState, useRef } from "react"
|
||||
import { X, Upload } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useToast } from "@/hooks/use-toast"
|
||||
import { validateFileForUpload, generateFilePreview } from "@/lib/file-utils"
|
||||
|
||||
export function CharacterUploader() {
|
||||
const [preview, setPreview] = useState<string | null>(null)
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const { toast } = useToast()
|
||||
|
||||
const handleFile = async (file: File) => {
|
||||
const validation = validateFileForUpload(file, true)
|
||||
if (validation.valid) {
|
||||
const previewUrl = await generateFilePreview(file)
|
||||
setPreview(previewUrl)
|
||||
} else if (validation.error) {
|
||||
toast({
|
||||
title: "Invalid file",
|
||||
description: validation.error,
|
||||
variant: "destructive",
|
||||
duration: 4000,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setIsDragging(false)
|
||||
const file = e.dataTransfer.files[0]
|
||||
if (file) handleFile(file)
|
||||
}
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setIsDragging(true)
|
||||
}
|
||||
|
||||
const handleDragLeave = () => {
|
||||
setIsDragging(false)
|
||||
}
|
||||
|
||||
const clearPreview = () => {
|
||||
setPreview(null)
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = ""
|
||||
}
|
||||
}
|
||||
|
||||
if (preview) {
|
||||
return (
|
||||
<div className="relative h-24 rounded-lg overflow-hidden glass-panel group transition-all">
|
||||
<img src={preview || "/placeholder.svg"} alt="Character preview" className="w-full h-full object-contain p-2" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute top-2 right-2 h-6 w-6 bg-black/50 hover:bg-black/70 opacity-70 group-hover:opacity-100 transition-opacity"
|
||||
onClick={clearPreview}
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className={`
|
||||
flex items-center gap-2 px-3 py-2 rounded-md transition-all text-xs
|
||||
${
|
||||
isDragging
|
||||
? "glass-panel border-indigo/50 text-white"
|
||||
: "glass-panel glass-panel-hover text-muted-foreground hover:text-white"
|
||||
}
|
||||
`}
|
||||
>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/jpg"
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (file) handleFile(file)
|
||||
}}
|
||||
/>
|
||||
<Upload className="w-3.5 h-3.5" />
|
||||
<span>Upload Character</span>
|
||||
<span className="text-muted-foreground/50">(Optional)</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { useRef, useEffect } from "react";
|
||||
import { Upload, X, Check } from "lucide-react";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Upload, X, Check, ArrowRight, Loader2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { useS3Upload } from "next-s3-upload";
|
||||
import { useAuth, SignInButton } from "@clerk/nextjs";
|
||||
import { COMIC_STYLES } from "@/lib/constants";
|
||||
import { useKeyboardShortcut } from "@/hooks/use-keyboard-shortcut";
|
||||
import { useApiKey } from "@/hooks/use-api-key";
|
||||
|
||||
interface StoryInputProps {
|
||||
interface ComicCreationFormProps {
|
||||
prompt: string;
|
||||
setPrompt: (prompt: string) => void;
|
||||
style: string;
|
||||
@@ -15,9 +19,10 @@ interface StoryInputProps {
|
||||
characterFiles: File[];
|
||||
setCharacterFiles: (files: File[]) => void;
|
||||
isLoading: boolean;
|
||||
setIsLoading: (loading: boolean) => void;
|
||||
}
|
||||
|
||||
export function StoryInput({
|
||||
export function ComicCreationForm({
|
||||
prompt,
|
||||
setPrompt,
|
||||
style,
|
||||
@@ -25,11 +30,21 @@ export function StoryInput({
|
||||
characterFiles,
|
||||
setCharacterFiles,
|
||||
isLoading,
|
||||
}: StoryInputProps) {
|
||||
setIsLoading,
|
||||
}: ComicCreationFormProps) {
|
||||
const router = useRouter();
|
||||
const [loadingStep, setLoadingStep] = useState(0);
|
||||
const { toast } = useToast();
|
||||
const { uploadToS3 } = useS3Upload();
|
||||
const { isSignedIn, isLoaded } = useAuth();
|
||||
const [apiKey] = useApiKey();
|
||||
const hasApiKey = !!apiKey;
|
||||
const [previews, setPreviews] = useState<string[]>([]);
|
||||
const [showPreview, setShowPreview] = useState<number | null>(null);
|
||||
const [showStyleDropdown, setShowStyleDropdown] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) {
|
||||
@@ -37,6 +52,20 @@ export function StoryInput({
|
||||
}
|
||||
}, [isLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
// Auto-focus the textarea when component mounts
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Keyboard shortcut for form submission
|
||||
useKeyboardShortcut(() => {
|
||||
if (!isLoading && prompt.trim()) {
|
||||
handleCreate();
|
||||
}
|
||||
}, { disabled: isLoading });
|
||||
|
||||
const handleFiles = (newFiles: FileList | null) => {
|
||||
if (!newFiles) return;
|
||||
|
||||
@@ -84,6 +113,93 @@ export function StoryInput({
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!prompt.trim()) {
|
||||
toast({
|
||||
title: "Prompt required",
|
||||
description: "Please enter a prompt to generate your comic",
|
||||
variant: "destructive",
|
||||
duration: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setLoadingStep(0);
|
||||
|
||||
try {
|
||||
if (!apiKey) {
|
||||
toast({
|
||||
title: "API key required",
|
||||
description: "Please add your API key to generate comics.",
|
||||
variant: "destructive",
|
||||
duration: 3000,
|
||||
});
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const characterUploads = await Promise.all(
|
||||
characterFiles.map((file) => uploadToS3(file).then(({ url }) => url))
|
||||
);
|
||||
|
||||
// Use API to create story and generate first page
|
||||
const response = await fetch("/api/generate-comic", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
prompt,
|
||||
apiKey,
|
||||
style,
|
||||
characterImages: characterUploads,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
if (response.status === 429 && errorData.isRateLimited) {
|
||||
throw new Error(errorData.error);
|
||||
}
|
||||
throw new Error(errorData.error || "Failed to create story");
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// Redirect to the story editor using slug
|
||||
router.push(`/editor/${result.storySlug}`);
|
||||
} catch (error) {
|
||||
console.error("Error creating comic:", error);
|
||||
toast({
|
||||
title: "Creation failed",
|
||||
description:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to create comic. Please try again.",
|
||||
variant: "destructive",
|
||||
duration: 4000,
|
||||
});
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
const isEnter = e.key === "Enter" || e.key === "\n" || e.keyCode === 13;
|
||||
const isModifierPressed = e.shiftKey || e.ctrlKey || e.metaKey; // metaKey for Cmd on Mac
|
||||
|
||||
if (isEnter && isModifierPressed) {
|
||||
e.preventDefault();
|
||||
handleCreate();
|
||||
}
|
||||
};
|
||||
|
||||
const loadingSteps = [
|
||||
"Enhancing prompt...",
|
||||
"Generating scenes...",
|
||||
"Creating your comic...",
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative glass-panel p-0.5 sm:p-1 rounded-xl group focus-within:border-indigo/30 transition-colors">
|
||||
@@ -95,6 +211,7 @@ export function StoryInput({
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
placeholder="A cyberpunk detective standing in neon rain, holding a glowing datapad, moody lighting, noir style..."
|
||||
@@ -242,6 +359,48 @@ export function StoryInput({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-2">
|
||||
{!isLoaded ? (
|
||||
<div className="h-10" />
|
||||
) : isSignedIn ? (
|
||||
<div className="flex items-center justify-between gap-3 w-full">
|
||||
<Button
|
||||
onClick={handleCreate}
|
||||
disabled={isLoading || !prompt.trim()}
|
||||
className="bg-white hover:bg-neutral-200 text-black px-8 py-2 rounded-md text-sm font-medium transition-colors flex items-center justify-center gap-3 tracking-tight"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
<span className="text-sm font-medium tracking-tight">
|
||||
{loadingSteps[loadingStep]}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Generate
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<div className="text-xs text-muted-foreground whitespace-nowrap">
|
||||
{hasApiKey ? (
|
||||
<>Using your API key (~$0.01 per comic)</>
|
||||
) : (
|
||||
<>1 credit weekly</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<SignInButton mode="modal">
|
||||
<Button className="w-full sm:w-auto sm:min-w-40 bg-white hover:bg-neutral-200 text-black px-8 py-2 rounded-md text-sm font-medium transition-colors flex items-center justify-center gap-3 tracking-tight">
|
||||
Login to create your comic
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</Button>
|
||||
</SignInButton>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ArrowRight, Loader2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { useS3Upload } from "next-s3-upload";
|
||||
import { useAuth, SignInButton } from "@clerk/nextjs";
|
||||
|
||||
interface CreateButtonProps {
|
||||
prompt: string;
|
||||
style: string;
|
||||
characterFiles: File[];
|
||||
isLoading: boolean;
|
||||
setIsLoading: (loading: boolean) => void;
|
||||
}
|
||||
|
||||
export function CreateButton({
|
||||
prompt,
|
||||
style,
|
||||
characterFiles,
|
||||
isLoading,
|
||||
setIsLoading,
|
||||
}: CreateButtonProps) {
|
||||
const router = useRouter();
|
||||
const [loadingStep, setLoadingStep] = useState(0);
|
||||
const { toast } = useToast();
|
||||
const { uploadToS3 } = useS3Upload();
|
||||
const { isSignedIn, isLoaded } = useAuth();
|
||||
const [hasApiKey, setHasApiKey] = useState(false);
|
||||
|
||||
// Check if user has their own API key set
|
||||
useEffect(() => {
|
||||
const checkApiKey = () => {
|
||||
const apiKey = localStorage.getItem("together_api_key");
|
||||
setHasApiKey(!!apiKey);
|
||||
};
|
||||
|
||||
checkApiKey();
|
||||
// Listen for storage changes
|
||||
window.addEventListener("storage", checkApiKey);
|
||||
return () => window.removeEventListener("storage", checkApiKey);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading) return;
|
||||
|
||||
const steps = [
|
||||
"Enhancing prompt...",
|
||||
"Generating scenes...",
|
||||
"Creating your comic...",
|
||||
];
|
||||
let currentStep = 0;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
currentStep += 1;
|
||||
if (currentStep < steps.length) {
|
||||
setLoadingStep(currentStep);
|
||||
} else {
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 2500);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isLoading]);
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!prompt.trim()) {
|
||||
toast({
|
||||
title: "Prompt required",
|
||||
description: "Please enter a prompt to generate your comic",
|
||||
variant: "destructive",
|
||||
duration: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setLoadingStep(0);
|
||||
|
||||
try {
|
||||
const apiKey = localStorage.getItem("together_api_key");
|
||||
const characterUploads = await Promise.all(
|
||||
characterFiles.map((file) => uploadToS3(file).then(({ url }) => url))
|
||||
);
|
||||
|
||||
// Use API to create story and generate first page
|
||||
const response = await fetch("/api/generate-comic", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
prompt,
|
||||
apiKey,
|
||||
style,
|
||||
characterImages: characterUploads,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
if (response.status === 429 && errorData.isRateLimited) {
|
||||
throw new Error(errorData.error);
|
||||
}
|
||||
throw new Error(errorData.error || "Failed to create story");
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// Redirect to the story editor using slug
|
||||
router.push(`/editor/${result.storySlug}`);
|
||||
} catch (error) {
|
||||
console.error("Error creating comic:", error);
|
||||
toast({
|
||||
title: "Creation failed",
|
||||
description:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to create comic. Please try again.",
|
||||
variant: "destructive",
|
||||
duration: 4000,
|
||||
});
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadingSteps = [
|
||||
"Enhancing prompt...",
|
||||
"Generating scenes...",
|
||||
"Creating your comic...",
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="pt-2">
|
||||
{!isLoaded ? (
|
||||
<div className="h-10" />
|
||||
) : isSignedIn ? (
|
||||
<div className="flex items-center justify-between gap-3 w-full">
|
||||
<Button
|
||||
onClick={handleCreate}
|
||||
disabled={isLoading || !prompt.trim()}
|
||||
className="bg-white hover:bg-neutral-200 text-black px-8 py-2 rounded-md text-sm font-medium transition-colors flex items-center justify-center gap-3 tracking-tight"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
<span className="text-sm font-medium tracking-tight">
|
||||
{loadingSteps[loadingStep]}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Generate
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<div className="text-xs text-muted-foreground whitespace-nowrap">
|
||||
{hasApiKey ? (
|
||||
<>Using your API key (~$0.01 per comic)</>
|
||||
) : (
|
||||
<>1 credit weekly</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<SignInButton mode="modal">
|
||||
<Button className="w-full sm:w-auto sm:min-w-40 bg-white hover:bg-neutral-200 text-black px-8 py-2 rounded-md text-sm font-medium transition-colors flex items-center justify-center gap-3 tracking-tight">
|
||||
Login to create your comic
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</Button>
|
||||
</SignInButton>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,15 +6,17 @@ import { Github, Key, BookOpen, User, Plus } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { ApiKeyModal } from "@/components/api-key-modal";
|
||||
import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs";
|
||||
import { useApiKey } from "@/hooks/use-api-key";
|
||||
|
||||
export function Navbar() {
|
||||
const [showApiModal, setShowApiModal] = useState(false);
|
||||
|
||||
const { isLoaded } = useAuth();
|
||||
const pathname = usePathname();
|
||||
const [, setApiKey] = useApiKey();
|
||||
|
||||
const handleApiKeySubmit = (key: string) => {
|
||||
localStorage.setItem("together_api_key", key);
|
||||
setApiKey(key);
|
||||
setShowApiModal(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { Check } from "lucide-react"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { COMIC_STYLES } from "@/lib/constants"
|
||||
|
||||
interface StyleSelectorProps {
|
||||
style: string
|
||||
setStyle: (style: string) => void
|
||||
}
|
||||
|
||||
export function StyleSelector({ style, setStyle }: StyleSelectorProps) {
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<Label className="text-base font-semibold font-display">Choose Your Style</Label>
|
||||
|
||||
{/* Grid for style selection */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{COMIC_STYLES.map((styleOption) => (
|
||||
<button
|
||||
key={styleOption.id}
|
||||
onClick={() => setStyle(styleOption.id)}
|
||||
className={`
|
||||
relative text-left transition-all duration-200 rounded-lg p-3.5 border-2 group
|
||||
hover:scale-[1.02] active:scale-[0.98]
|
||||
${
|
||||
style === styleOption.id
|
||||
? "border-indigo bg-indigo shadow-md"
|
||||
: "border-border hover:border-indigo/50 bg-card hover:bg-muted/20"
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className={`font-semibold text-sm font-display ${
|
||||
style === styleOption.id ? "text-white" : "text-foreground"
|
||||
}`}>{styleOption.name}</h3>
|
||||
</div>
|
||||
</div>
|
||||
{style === styleOption.id && (
|
||||
<div className="bg-white text-indigo p-1 rounded-full shrink-0">
|
||||
<Check className="w-3 h-3" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
const loaderSvgs = ["bang.svg", "oh.svg", "omg.svg"];
|
||||
|
||||
interface StoryLoaderProps {
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export function StoryLoader({ text = "Loading story..." }: StoryLoaderProps) {
|
||||
const [currentSvgIndex, setCurrentSvgIndex] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setCurrentSvgIndex((prev) => (prev + 1) % loaderSvgs.length);
|
||||
}, 1200); // Change every 1200ms for slower transition
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-8">
|
||||
<div className="w-56 h-56 flex items-center justify-center">
|
||||
<img
|
||||
key={currentSvgIndex} // Force re-render for smooth transition
|
||||
src={`/loader/${loaderSvgs[currentSvgIndex]}`}
|
||||
alt="Loading..."
|
||||
className="w-full h-full object-contain animate-pulse transition-all duration-700 ease-in-out transform scale-90 hover:scale-110"
|
||||
style={{ filter: 'invert(1)' }}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-white text-lg">{text}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
|
||||
const STORAGE_KEY = "together_api_key";
|
||||
const STORAGE_EVENT = "apiKeyChanged";
|
||||
|
||||
/**
|
||||
* Reactive hook for managing the Together API key in localStorage.
|
||||
* Automatically syncs across components and tabs when the key changes.
|
||||
*
|
||||
* @returns {[string | null, (key: string | null) => void]} Tuple of [apiKey, setApiKey]
|
||||
*/
|
||||
export function useApiKey(): [string | null, (key: string | null) => void] {
|
||||
const [apiKey, setApiKeyState] = useState<string | null>(null);
|
||||
|
||||
// Initialize from localStorage on mount
|
||||
useEffect(() => {
|
||||
const readFromStorage = () => {
|
||||
if (typeof window !== "undefined") {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
setApiKeyState(stored);
|
||||
}
|
||||
};
|
||||
|
||||
readFromStorage();
|
||||
|
||||
// Listen for storage events (cross-tab updates)
|
||||
const handleStorageChange = (e: StorageEvent) => {
|
||||
if (e.key === STORAGE_KEY) {
|
||||
setApiKeyState(e.newValue);
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for custom events (same-tab updates)
|
||||
const handleCustomStorageChange = () => {
|
||||
readFromStorage();
|
||||
};
|
||||
|
||||
window.addEventListener("storage", handleStorageChange);
|
||||
window.addEventListener(STORAGE_EVENT, handleCustomStorageChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("storage", handleStorageChange);
|
||||
window.removeEventListener(STORAGE_EVENT, handleCustomStorageChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Setter function that updates both localStorage and state, and dispatches event
|
||||
const setApiKey = useCallback((key: string | null) => {
|
||||
if (typeof window !== "undefined") {
|
||||
if (key === null) {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
} else {
|
||||
localStorage.setItem(STORAGE_KEY, key);
|
||||
}
|
||||
setApiKeyState(key);
|
||||
// Dispatch custom event for same-tab reactivity
|
||||
window.dispatchEvent(new CustomEvent(STORAGE_EVENT));
|
||||
}
|
||||
}, []);
|
||||
|
||||
return [apiKey, setApiKey];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export function useKeyboardShortcut(
|
||||
callback: () => void,
|
||||
options: {
|
||||
ctrlOrCmd?: boolean;
|
||||
shift?: boolean;
|
||||
key?: string;
|
||||
disabled?: boolean;
|
||||
} = {}
|
||||
) {
|
||||
const {
|
||||
ctrlOrCmd = true,
|
||||
shift = false,
|
||||
key = 'Enter',
|
||||
disabled = false,
|
||||
} = options;
|
||||
|
||||
useEffect(() => {
|
||||
if (disabled) return;
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const isKeyPressed = e.key === key;
|
||||
const isModifierPressed = ctrlOrCmd
|
||||
? (e.ctrlKey || e.metaKey) // Ctrl on Windows/Linux, Cmd on Mac
|
||||
: shift
|
||||
? e.shiftKey
|
||||
: false;
|
||||
|
||||
if (isKeyPressed && isModifierPressed) {
|
||||
e.preventDefault();
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}, [callback, ctrlOrCmd, shift, key, disabled]);
|
||||
}
|
||||
+48
-5
@@ -42,6 +42,12 @@ export async function updatePage(pageId: string, generatedImageUrl: string): Pro
|
||||
.where(eq(pages.id, pageId));
|
||||
}
|
||||
|
||||
export async function updateStory(storyId: string, data: { title?: string; description?: string }): Promise<void> {
|
||||
await db.update(stories)
|
||||
.set({ ...data, updatedAt: new Date() })
|
||||
.where(eq(stories.id, storyId));
|
||||
}
|
||||
|
||||
export async function getStoryWithPages(storyId: string): Promise<{ story: Story; pages: Page[] } | null> {
|
||||
const storyResult = await db.select().from(stories).where(eq(stories.id, storyId)).limit(1);
|
||||
|
||||
@@ -82,13 +88,46 @@ export async function getStoryWithPagesBySlug(slug: string): Promise<{ story: St
|
||||
}
|
||||
|
||||
export async function getStoryCharacterImages(storyId: string): Promise<string[]> {
|
||||
const storyPages = await db.select({ characterImageUrls: pages.characterImageUrls })
|
||||
const storyPages = await db.select({
|
||||
characterImageUrls: pages.characterImageUrls,
|
||||
pageNumber: pages.pageNumber
|
||||
})
|
||||
.from(pages)
|
||||
.where(eq(pages.storyId, storyId));
|
||||
.where(eq(pages.storyId, storyId))
|
||||
.orderBy(pages.pageNumber);
|
||||
|
||||
// Flatten all character URLs from all pages and remove duplicates
|
||||
const allUrls = storyPages.flatMap(page => page.characterImageUrls);
|
||||
return [...new Set(allUrls)]; // Remove duplicates
|
||||
// Flatten all character URLs from all pages, keeping order by page number
|
||||
const allUrls: string[] = [];
|
||||
const seenUrls = new Set<string>();
|
||||
|
||||
for (const page of storyPages) {
|
||||
for (const url of page.characterImageUrls) {
|
||||
if (!seenUrls.has(url)) {
|
||||
seenUrls.add(url);
|
||||
allUrls.push(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allUrls;
|
||||
}
|
||||
|
||||
export async function getLastPageImage(storyId: string): Promise<string | null> {
|
||||
const allPages = await db.select({ generatedImageUrl: pages.generatedImageUrl, pageNumber: pages.pageNumber })
|
||||
.from(pages)
|
||||
.where(eq(pages.storyId, storyId))
|
||||
.orderBy(pages.pageNumber);
|
||||
|
||||
if (allPages.length === 0) return null;
|
||||
|
||||
// Find the last page that has a generated image
|
||||
for (let i = allPages.length - 1; i >= 0; i--) {
|
||||
if (allPages[i].generatedImageUrl) {
|
||||
return allPages[i].generatedImageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getNextPageNumber(storyId: string): Promise<number> {
|
||||
@@ -102,4 +141,8 @@ export async function getNextPageNumber(storyId: string): Promise<number> {
|
||||
}
|
||||
|
||||
return Math.max(...storyPages.map(p => p.pageNumber)) + 1;
|
||||
}
|
||||
|
||||
export async function deletePage(pageId: string): Promise<void> {
|
||||
await db.delete(pages).where(eq(pages.id, pageId));
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { COMIC_STYLES } from "./constants";
|
||||
|
||||
export function buildComicPrompt({
|
||||
prompt,
|
||||
style,
|
||||
characterImages = [],
|
||||
isContinuation = false,
|
||||
previousContext = "",
|
||||
isAddPage = false,
|
||||
previousPages = [],
|
||||
}: {
|
||||
prompt: string;
|
||||
style?: string;
|
||||
characterImages?: string[];
|
||||
isContinuation?: boolean;
|
||||
previousContext?: string;
|
||||
isAddPage?: boolean;
|
||||
previousPages?: Array<{
|
||||
prompt: string;
|
||||
characterImages: string[];
|
||||
}>;
|
||||
}): string {
|
||||
const styleInfo = COMIC_STYLES.find((s) => s.id === style);
|
||||
const styleDesc = styleInfo?.prompt || COMIC_STYLES[2].prompt;
|
||||
|
||||
let continuationContext = "";
|
||||
if (isContinuation && previousContext) {
|
||||
continuationContext = `\nCONTINUATION CONTEXT:\nThis is a continuation of an existing story. The previous page showed: ${previousContext}\nMaintain visual consistency with the previous panels. Continue the narrative naturally.\n`;
|
||||
}
|
||||
|
||||
if (isAddPage && previousPages.length > 0) {
|
||||
const storyHistory = previousPages
|
||||
.map((page, index) => `Page ${index + 1}: ${page.prompt}`)
|
||||
.join('\n');
|
||||
|
||||
continuationContext = `\nSTORY CONTINUATION CONTEXT:\nThis is a continuation of an existing comic story. Here are the previous pages:\n${storyHistory}\n\nThe new page should naturally continue this story. Maintain the same characters, setting, and narrative style. Reference previous events and build upon them.\n`;
|
||||
}
|
||||
|
||||
let characterSection = "";
|
||||
if (characterImages.length > 0) {
|
||||
if (characterImages.length === 1) {
|
||||
characterSection = `
|
||||
CRITICAL FACE CONSISTENCY INSTRUCTIONS:
|
||||
- REFERENCE CHARACTER: Use the uploaded image as EXACT reference for the protagonist's face and appearance
|
||||
- FACE MATCHING: The character's face must be IDENTICAL to the reference image - same eyes, nose, mouth, hair, facial structure
|
||||
- APPEARANCE PRESERVATION: Maintain exact skin tone, hair color/style, eye color, and distinctive facial features
|
||||
- CHARACTER CONSISTENCY: This exact same character must appear in ALL 5 panels with the same face throughout
|
||||
- STYLE APPLICATION: Apply ${style} comic art style to the body/pose/action but KEEP THE FACE EXACTLY AS IN THE REFERENCE IMAGE
|
||||
- NO VARIATION: Do not alter, modify, or change the character's face in any way from the reference`;
|
||||
} else if (characterImages.length === 2) {
|
||||
characterSection = `
|
||||
CRITICAL DUAL CHARACTER FACE CONSISTENCY INSTRUCTIONS:
|
||||
- CHARACTER 1 REFERENCE: Use the FIRST uploaded image as EXACT reference for Character 1's face and appearance
|
||||
- CHARACTER 2 REFERENCE: Use the SECOND uploaded image as EXACT reference for Character 2's face and appearance
|
||||
- FACE MATCHING: Both characters' faces must be IDENTICAL to their respective reference images
|
||||
- VISUAL DISTINCTION: Keep both characters clearly visually distinct with their unique faces, hair, and features
|
||||
- CONSISTENT PRESENCE: Both characters must appear together in at least 4 of the 5 panels
|
||||
- STYLE APPLICATION: Apply ${style} comic art style while maintaining EXACT facial features from references
|
||||
- NO FACE VARIATION: Never alter or modify either character's face from their reference images`;
|
||||
}
|
||||
}
|
||||
|
||||
const systemPrompt = `Professional comic book page illustration.
|
||||
${continuationContext}
|
||||
${characterSection}
|
||||
|
||||
CHARACTER CONSISTENCY RULES (HIGHEST PRIORITY):
|
||||
- If reference images are provided, the characters' FACES must be 100% identical to the reference images
|
||||
- Never change hair color, eye color, facial structure, or distinctive features
|
||||
- Apply comic style to body/pose/action but preserve exact facial appearance
|
||||
- Same character must look identical across all panels they appear in
|
||||
|
||||
TEXT AND LETTERING (CRITICAL):
|
||||
- All text in speech bubbles must be PERFECTLY CLEAR, LEGIBLE, and correctly spelled
|
||||
- Use bold clean comic book lettering, large and easy to read
|
||||
- Speech bubbles: crisp white fill, solid black outline, pointed tail toward speaker
|
||||
- Keep dialogue SHORT: maximum 1-2 sentences per bubble
|
||||
- NO blurry, warped, or unreadable text
|
||||
|
||||
PAGE LAYOUT:
|
||||
5-panel comic page arranged as:
|
||||
[Panel 1] [Panel 2] — top row, 2 equal panels
|
||||
[ Panel 3 ] — middle row, 1 large cinematic hero panel
|
||||
[Panel 4] [Panel 5] — bottom row, 2 equal panels
|
||||
- Solid black panel borders with clean white gutters between panels
|
||||
- Each panel clearly separated and distinct
|
||||
|
||||
ART STYLE:
|
||||
${styleDesc}
|
||||
${characterSection}
|
||||
|
||||
COMPOSITION:
|
||||
- Vary camera angles across panels: close-up, medium shot, wide establishing shot
|
||||
- Natural visual flow: left-to-right, top-to-bottom reading order
|
||||
- Dynamic character poses with clear expressive acting
|
||||
- Detailed backgrounds matching the scene and mood`;
|
||||
|
||||
return `${systemPrompt}\n\nSTORY:\n${prompt}`;
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
<svg width="691" height="608" viewBox="0 0 691 608" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M385.981 0.541305C385.392 1.13126 384.949 3.34362 384.949 5.40848C384.802 7.47334 383.917 10.8656 382.737 12.9305C381.557 14.9953 380.672 18.3876 380.524 20.4525C380.524 22.6648 379.492 26.7945 378.312 29.5968C377.132 32.5466 376.1 37.7088 376.1 40.9536C376.1 44.4933 375.067 48.0331 373.887 49.213C371.527 51.7203 370.79 60.8647 373.002 62.1921C374.772 63.3721 376.1 59.5373 376.1 53.4902C376.1 50.8354 377.132 47.8856 378.312 46.8532C379.492 45.8207 380.524 42.8709 380.524 40.2161C380.524 37.4138 381.409 33.4316 382.589 31.0717C385.097 26.2046 387.899 15.8803 388.931 8.0633C389.669 2.60616 388.046 -1.52356 385.981 0.541305Z" fill="black" />
|
||||
<path d="M172.563 11.3081C172.563 14.7003 176.988 23.5497 179.643 25.1721C180.528 25.7621 181.413 27.9744 181.413 29.8918C181.413 31.8092 182.003 33.7266 182.74 34.3165C183.478 34.759 184.51 36.9713 185.1 39.3312C185.542 41.691 187.017 44.4933 188.197 45.5258C189.377 46.4107 190.262 48.3281 190.262 49.803C190.262 51.2779 191.295 53.1952 192.475 54.2277C193.654 55.2601 194.687 57.325 194.687 58.9473C194.687 60.4222 195.719 62.0446 196.899 62.4871C198.079 63.0771 199.112 64.9944 199.112 66.9118C199.112 68.6817 199.997 70.894 201.029 71.484C202.209 72.074 203.389 73.8438 203.979 75.1712C204.421 76.6461 205.601 77.8261 206.486 77.8261C208.698 77.8261 208.256 73.9913 205.749 71.9265C204.569 70.894 203.536 68.9767 203.536 67.5018C203.536 66.0269 202.504 64.1095 201.324 63.0771C200.144 62.0446 199.112 59.6848 199.112 57.9149C199.112 56.145 198.079 53.7852 196.899 52.7528C195.719 51.7203 194.687 49.803 194.687 48.3281C194.687 46.8532 193.654 44.9358 192.475 43.9034C191.295 42.8709 190.262 41.2485 190.262 40.3636C190.262 39.3312 188.345 34.6115 185.837 29.8918C183.478 25.1721 181.413 20.1575 181.413 18.6826C181.413 17.3552 180.38 15.4378 179.2 14.4054C178.02 13.3729 176.988 11.603 176.988 10.5706C176.988 9.3907 175.956 8.50576 174.776 8.50576C173.596 8.50576 172.563 9.83317 172.563 11.3081Z" fill="black" />
|
||||
<path d="M283.771 25.1722C283.328 26.3521 284.066 33.1367 285.246 40.3637C286.573 47.5907 287.606 58.7999 287.606 65.437C287.606 73.5489 288.343 78.1211 289.818 80.186C290.998 81.6609 292.03 85.4956 292.03 88.4454C292.03 91.3952 292.62 94.345 293.505 94.7875C295.718 96.1149 296.75 89.1829 294.98 84.4632C294.095 82.1033 293.063 72.5165 292.62 63.2246C291.44 38.4463 286.721 17.6502 283.771 25.1722Z" fill="black" />
|
||||
<path d="M542.911 66.027C529.047 79.7436 517.69 91.6902 517.69 92.5752C517.69 93.3126 516.805 94.0501 515.625 94.0501C513.56 94.0501 481.85 126.498 482.882 127.678C483.767 128.415 489.224 123.696 505.006 108.652C512.38 101.572 525.065 89.6254 533.029 82.1034C559.872 56.8826 570.786 45.8209 570.786 44.1985C570.786 39.3313 565.624 43.3135 542.911 66.027Z" fill="black" />
|
||||
<path d="M499.401 64.9944C490.994 74.1388 487.897 77.8261 482.292 85.2006C479.49 88.8878 474.475 94.6399 471.231 98.0322C467.986 101.424 464.004 106.734 462.381 109.831C460.759 112.929 458.399 116.173 457.219 117.206C456.039 118.091 455.449 119.418 456.187 120.008C457.367 121.188 457.662 120.893 473.001 103.637C478.753 97.1472 484.947 90.0677 486.865 88.0029C494.387 80.0384 508.841 61.6021 508.841 60.1273C508.841 56.8825 504.858 58.9473 499.401 64.9944Z" fill="black" />
|
||||
<path d="M159.584 64.6997C159.879 65.7321 161.059 66.912 162.092 67.207C163.272 67.6495 163.861 67.0595 163.419 65.8796C163.124 64.8472 161.944 63.6672 160.912 63.3723C159.732 62.9298 159.142 63.5197 159.584 64.6997Z" fill="black" />
|
||||
<path d="M369.168 73.4014C369.168 77.0887 369.463 78.5636 369.758 76.6462C370.053 74.8763 370.053 71.9265 369.758 70.0091C369.463 68.2393 369.168 69.7142 369.168 73.4014Z" fill="black" />
|
||||
<path d="M163.714 71.779C163.714 72.5165 164.746 73.8439 165.926 74.8763C167.106 75.9087 168.139 77.6786 168.139 78.711C168.139 79.891 168.876 80.7759 169.614 80.7759C170.499 80.7759 171.826 82.3983 172.563 84.4632C173.301 86.528 174.628 88.1504 175.513 88.1504C177.578 88.1504 177.431 85.7906 175.071 83.4307C174.038 82.3983 172.711 80.3334 172.121 78.8585C171.678 77.5311 170.646 76.3512 169.761 76.3512C169.024 76.3512 168.139 75.0238 167.696 73.4014C166.959 70.4516 163.714 69.1242 163.714 71.779Z" fill="black" />
|
||||
<path d="M208.698 82.2507C208.108 83.1357 208.256 84.6106 209.141 85.4955C211.058 87.4129 212.533 84.9055 211.058 82.3982C210.026 80.9233 209.583 80.9233 208.698 82.2507Z" fill="black" />
|
||||
<path d="M177.283 91.2478C177.578 92.2802 178.758 93.4601 179.79 93.7551C180.97 94.1976 181.56 93.6076 181.118 92.4277C180.823 91.3952 179.643 90.2153 178.611 89.9203C177.431 89.4779 176.841 90.0678 177.283 91.2478Z" fill="black" />
|
||||
<path d="M185.837 104.964C185.837 106.292 186.87 107.324 188.197 107.324C189.525 107.324 190.262 106.587 189.967 105.554C189.082 102.899 185.837 102.457 185.837 104.964Z" fill="black" />
|
||||
<path d="M247.783 108.799C247.783 109.684 248.816 110.716 249.996 111.159C251.175 111.749 252.208 112.929 252.208 114.109C252.208 117.206 262.975 127.383 269.907 130.923C278.904 135.495 292.768 135.2 300.732 130.333C306.632 126.498 314.154 119.271 314.154 117.353C314.154 116.616 313.121 116.174 311.941 116.174C310.761 116.174 309.729 117.206 309.729 118.386C309.729 119.566 308.844 120.598 307.664 120.598C306.484 120.598 305.304 121.483 304.862 122.368C304.419 123.843 303.534 123.843 301.027 122.368C298.225 120.746 297.782 120.893 296.602 123.99C293.063 132.987 271.677 128.268 257.665 115.436C254.715 112.634 252.208 109.684 252.208 108.946C252.208 108.062 251.175 107.324 249.996 107.324C248.816 107.324 247.783 108.062 247.783 108.799Z" fill="black" />
|
||||
<path d="M397.486 111.159C394.241 116.174 395.273 116.911 399.403 112.486C401.173 110.569 402.058 108.652 401.32 108.209C400.583 107.767 398.961 109.094 397.486 111.159Z" fill="black" />
|
||||
<path d="M190.262 110.864C190.262 113.076 192.917 115.436 194.097 114.256C195.129 113.224 193.065 108.799 191.59 108.799C190.852 108.799 190.262 109.684 190.262 110.864Z" fill="black" />
|
||||
<path d="M388.636 121.336C387.014 123.253 384.507 125.17 383.179 125.613C380.229 126.35 379.492 129.448 382.442 129.448C384.212 129.3 390.849 123.106 392.913 119.418C394.683 116.321 391.734 117.648 388.636 121.336Z" fill="black" />
|
||||
<path d="M416.954 129.89C416.512 130.923 416.659 132.692 417.397 133.725C418.134 135.2 418.577 135.052 419.314 133.43C419.757 132.397 419.609 130.628 418.872 129.595C418.134 128.12 417.692 128.268 416.954 129.89Z" fill="black" />
|
||||
<path d="M476.835 131.365C473.591 132.692 472.116 136.822 474.623 136.822C475.95 136.822 481.26 130.923 480.523 130.185C480.228 130.038 478.605 130.48 476.835 131.365Z" fill="black" />
|
||||
<path d="M425.214 137.855C422.117 138.887 423.001 142.722 426.246 142.722C427.869 142.722 429.196 141.837 429.196 140.657C429.196 138.002 427.574 136.97 425.214 137.855Z" fill="black" />
|
||||
<path d="M442.47 140.509C442.47 141.689 443.503 142.722 444.682 142.722C445.862 142.722 446.895 141.689 446.895 140.509C446.895 139.329 445.862 138.297 444.682 138.297C443.503 138.297 442.47 139.329 442.47 140.509Z" fill="black" />
|
||||
<path d="M461.644 141.984C461.644 144.639 462.381 145.672 464.594 145.672C468.281 145.672 470.198 141.394 467.396 139.624C463.709 137.412 461.644 138.15 461.644 141.984Z" fill="black" />
|
||||
<path d="M58.2585 144.344C57.6686 145.229 57.5211 146.114 57.6686 146.261C66.518 152.899 70.5002 155.258 78.7596 159.683C84.2168 162.485 89.6739 164.845 90.8538 164.845C94.0986 164.845 93.3611 162.043 89.6739 160.273C85.9866 158.651 83.6268 157.323 74.1874 151.129C70.6477 148.916 67.2554 147.146 66.518 147.146C65.7805 147.146 64.4531 146.114 63.4207 144.934C61.2083 142.279 59.5859 142.132 58.2585 144.344Z" fill="black" />
|
||||
<path d="M234.214 145.967C232.592 148.474 232.444 150.391 233.624 153.489C235.837 159.388 238.196 157.618 238.639 149.654C239.081 142.132 237.459 140.804 234.214 145.967Z" fill="black" />
|
||||
<path d="M433.768 147.736C432.441 149.211 432.588 150.096 434.211 151.424C435.686 152.751 436.57 152.604 437.898 150.981C439.225 149.506 439.078 148.621 437.455 147.294C435.981 145.966 435.096 146.114 433.768 147.736Z" fill="black" />
|
||||
<path d="M451.614 149.654C451.909 151.424 453.384 152.309 455.744 152.309C458.104 152.309 459.579 151.424 459.874 149.654C460.316 147.736 459.431 147.146 455.744 147.146C452.057 147.146 451.172 147.736 451.614 149.654Z" fill="black" />
|
||||
<path d="M469.018 149.359C469.018 150.539 470.051 151.571 471.231 151.571C472.411 151.571 473.443 150.539 473.443 149.359C473.443 148.179 472.411 147.146 471.231 147.146C470.051 147.146 469.018 148.179 469.018 149.359Z" fill="black" />
|
||||
<path d="M123.892 154.963C123.892 158.208 127.284 160.421 132.299 160.421C137.461 160.421 138.936 157.913 134.658 156.586C133.183 156.143 130.824 155.111 129.496 154.373C125.809 152.456 123.892 152.751 123.892 154.963Z" fill="black" />
|
||||
<path d="M421.379 155.406C419.462 157.323 420.347 163.518 422.559 164.255C425.656 165.583 429.491 162.043 428.901 158.356C428.459 155.258 423.444 153.341 421.379 155.406Z" fill="black" />
|
||||
<path d="M442.765 158.503C443.355 161.601 447.632 162.191 447.632 158.946C447.632 157.766 446.452 156.586 444.978 156.291C442.913 155.848 442.323 156.438 442.765 158.503Z" fill="black" />
|
||||
<path d="M460.169 158.208C460.169 159.388 461.201 160.421 462.381 160.421C463.561 160.421 464.594 159.388 464.594 158.208C464.594 157.028 463.561 155.996 462.381 155.996C461.201 155.996 460.169 157.028 460.169 158.208Z" fill="black" />
|
||||
<path d="M478.9 156.881C477.72 158.061 479.048 160.421 480.965 160.421C481.702 160.421 482.292 159.388 482.292 158.208C482.292 155.996 480.523 155.258 478.9 156.881Z" fill="black" />
|
||||
<path d="M228.02 164.108C223.447 169.565 217.843 170.892 216.22 166.91C215.778 165.73 214.598 165.14 213.713 165.583C212.681 166.32 212.976 167.5 214.451 169.123C215.778 170.597 216.368 172.22 215.778 172.662C215.336 173.252 208.551 173.695 200.734 173.695C192.917 173.695 185.69 174.285 184.658 175.022C181.56 176.939 187.755 178.119 201.324 178.119C210.321 178.119 214.598 177.382 219.023 175.17C226.545 171.335 234.509 164.255 234.509 161.306C234.509 157.618 232.739 158.503 228.02 164.108Z" fill="black" />
|
||||
<path d="M94.6886 166.615C95.1311 167.648 96.7535 168.68 98.5234 168.975C100.146 169.27 102.358 170.45 103.243 171.63C104.275 172.81 106.045 173.695 107.225 173.695C110.323 173.695 108.7 170.155 105.308 169.417C103.833 169.122 101.916 168.09 101.326 166.91C99.8508 164.255 93.9512 164.108 94.6886 166.615Z" fill="black" />
|
||||
<path d="M155.897 165.73C154.275 167.5 156.339 169.27 160.027 169.27C161.944 169.27 163.861 168.68 164.304 167.943C165.779 165.73 157.962 163.665 155.897 165.73Z" fill="black" />
|
||||
<path d="M431.556 166.025C430.228 168.09 434.653 171.187 437.455 170.155C438.635 169.712 439.52 168.385 439.225 167.352C438.34 165.14 432.736 164.108 431.556 166.025Z" fill="black" />
|
||||
<path d="M451.615 167.353C451.91 168.827 453.089 170.007 454.269 170.007C455.449 170.007 456.629 168.827 456.924 167.353C457.219 165.583 456.482 164.845 454.269 164.845C452.057 164.845 451.32 165.583 451.615 167.353Z" fill="black" />
|
||||
<path d="M469.018 167.058C469.018 168.533 470.051 169.27 471.673 168.975C474.77 168.385 474.475 165.73 471.378 165.14C469.903 164.845 469.018 165.583 469.018 167.058Z" fill="black" />
|
||||
<path d="M487.455 166.173C486.422 167.942 489.224 169.712 491.142 168.532C492.764 167.5 491.732 164.845 489.667 164.845C488.929 164.845 488.045 165.435 487.455 166.173Z" fill="black" />
|
||||
<path d="M167.401 170.745C166.369 172.515 173.153 174.285 176.398 173.105C177.578 172.662 178.168 171.63 177.726 170.745C176.546 168.827 168.581 168.827 167.401 170.745Z" fill="black" />
|
||||
<path d="M424.771 170.745C424.771 171.63 425.804 172.662 426.984 173.105C428.164 173.695 429.196 174.874 429.196 176.054C429.196 177.234 429.786 178.119 430.376 178.119C431.113 178.119 433.768 180.184 436.276 182.691C438.93 185.199 441.88 186.969 443.06 186.526C444.092 186.084 446.6 186.821 448.37 188.001C452.204 190.656 469.166 191.836 474.623 189.771C477.425 188.739 477.72 188.001 476.393 185.494C474.328 181.659 470.346 181.659 467.101 185.494C463.856 189.329 462.824 189.181 458.694 184.756C455.892 181.807 454.269 181.217 451.025 181.807C446.747 182.839 445.715 181.364 448.517 178.562C451.025 176.054 448.96 172.22 445.272 172.22C443.503 172.22 441.88 173.4 441.438 175.169C440.258 179.594 434.801 178.857 431.408 173.695C428.459 169.417 424.771 167.795 424.771 170.745Z" fill="black" />
|
||||
<path d="M618.721 170.745C617.541 172.515 621.67 174.285 623.883 172.957C624.768 172.515 625.063 171.335 624.62 170.597C623.44 168.827 619.901 168.827 618.721 170.745Z" fill="black" />
|
||||
<path d="M461.201 173.105C460.611 173.695 460.169 175.464 460.169 176.939C460.169 179.889 463.856 180.627 465.479 177.972C466.659 176.054 462.676 171.63 461.201 173.105Z" fill="black" />
|
||||
<path d="M17.6989 175.907C17.6989 177.087 18.5838 178.119 19.7637 178.119C20.7961 178.119 22.566 179.152 23.5985 180.332C24.6309 181.512 26.8432 182.544 28.6131 182.544C30.383 182.544 33.1853 183.576 34.8077 184.756C36.2826 185.936 38.6424 186.969 39.9698 186.969C41.2973 186.969 43.2146 188.001 44.2471 189.181C45.2795 190.361 47.0494 191.393 48.2293 191.393C49.4092 191.393 52.8015 192.426 55.7513 193.753C71.8277 200.538 77.5798 202.75 85.3968 204.668C88.0516 205.405 91.0014 206.585 91.8863 207.47C94.2462 209.83 102.211 209.535 101.473 207.175C101.031 206.29 99.2608 205.11 97.196 204.963C95.2786 204.668 92.7713 203.635 91.5914 202.75C89.5265 200.98 82.742 198.325 74.1875 195.671C71.5327 194.933 69.0254 193.606 68.5829 192.868C67.993 191.983 66.6655 191.393 65.3381 191.393C64.1582 191.393 60.7659 190.361 57.8161 189.034C54.7189 187.854 50.4416 185.936 47.9343 184.904C45.5745 183.724 40.5598 181.807 36.8726 180.332C33.1853 179.004 28.3181 176.939 26.1058 175.76C20.6487 173.105 17.6989 173.105 17.6989 175.907Z" fill="black" />
|
||||
<path d="M477.868 175.612C477.868 176.792 478.31 178.119 478.753 178.562C480.228 179.889 485.537 176.792 484.505 175.022C483.177 172.957 477.868 173.4 477.868 175.612Z" fill="black" />
|
||||
<path d="M496.304 175.022C495.862 175.907 496.746 177.234 498.369 178.119C501.466 179.742 503.826 177.824 502.204 175.169C501.024 173.252 497.484 173.252 496.304 175.022Z" fill="black" />
|
||||
<path d="M516.805 176.202C516.363 177.677 515.33 180.037 514.593 181.512C513.708 183.134 513.413 184.756 514.003 185.199C515.183 186.526 523.737 176.644 522.852 175.022C521.377 172.81 517.69 173.547 516.805 176.202Z" fill="black" />
|
||||
<path d="M607.659 175.17C605.889 176.35 605.889 176.645 607.954 177.382C610.904 178.562 614.001 177.087 612.821 175.022C611.641 173.4 610.461 173.4 607.659 175.17Z" fill="black" />
|
||||
<path d="M596.597 179.594C595.417 181.364 599.547 183.134 601.759 181.807C602.644 181.364 602.939 180.184 602.497 179.447C601.317 177.677 597.777 177.677 596.597 179.594Z" fill="black" />
|
||||
<path d="M486.717 183.871C486.717 184.756 487.307 185.641 488.045 186.084C489.962 187.264 494.239 185.494 493.207 183.871C492.174 182.102 486.717 182.102 486.717 183.871Z" fill="black" />
|
||||
<path d="M586.568 183.576C584.503 185.494 585.683 187.116 588.78 186.674C592.762 186.084 594.09 182.544 590.403 182.544C588.78 182.544 587.01 182.986 586.568 183.576Z" fill="black" />
|
||||
<path d="M509.578 188.591C508.988 189.476 508.841 190.508 509.283 190.951C510.316 191.983 513.265 190.066 513.265 188.296C513.265 186.526 510.758 186.526 509.578 188.591Z" fill="black" />
|
||||
<path d="M574.769 188.001C572.556 190.066 573.884 191.541 577.718 191.098C582.733 190.508 584.061 186.969 579.341 186.969C577.276 186.969 575.211 187.411 574.769 188.001Z" fill="black" />
|
||||
<path d="M147.048 192.426C146.605 193.163 146.753 194.638 147.343 195.671C148.08 196.703 148.965 196.998 149.408 196.261C149.85 195.523 149.702 194.048 149.113 193.016C148.375 191.983 147.49 191.688 147.048 192.426Z" fill="black" />
|
||||
<path d="M562.674 192.721C562.232 193.458 562.527 194.491 563.412 195.081C565.477 196.408 571.081 194.491 570.049 192.721C568.869 190.951 563.854 190.951 562.674 192.721Z" fill="black" />
|
||||
<path d="M552.35 197.293C551.17 199.063 555.3 200.833 557.512 199.505C558.397 199.063 558.692 197.883 558.25 197.146C557.07 195.376 553.53 195.376 552.35 197.293Z" fill="black" />
|
||||
<path d="M542.321 201.128C540.846 202.75 542.763 204.667 545.861 204.667C547.336 204.667 548.663 203.635 548.663 202.455C548.663 200.243 544.091 199.358 542.321 201.128Z" fill="black" />
|
||||
<path d="M150.44 206.585C150.44 209.387 151.472 214.107 152.652 217.057C154.717 222.071 154.717 222.956 152.652 227.086C151.472 229.593 150.44 232.838 150.44 234.165C150.44 237.853 141.296 249.799 134.511 255.109C126.989 261.009 128.611 263.516 137.018 259.091C145.278 254.814 153.242 245.08 156.782 234.755C159.437 227.086 159.584 225.464 157.814 219.416C156.782 215.729 155.602 210.567 155.012 207.765C153.832 200.833 150.44 199.948 150.44 206.585Z" fill="black" />
|
||||
<path d="M495.272 204.373C494.829 206.142 494.534 224.579 494.682 245.522L494.829 283.575L507.218 284.017C516.215 284.312 520.05 283.87 520.787 282.542C521.525 281.51 522.115 276.053 522.115 270.596C522.115 265.138 523 256.142 524.18 250.684C526.539 239.918 528.309 205.11 526.687 204.078C526.097 203.635 519.017 202.898 510.758 202.308C496.599 201.423 496.009 201.423 495.272 204.373Z" fill="black" />
|
||||
<path d="M105.603 210.42C105.16 211.157 105.75 212.042 106.93 212.632C109.143 213.517 111.207 211.599 109.585 209.977C108.258 208.65 106.488 208.797 105.603 210.42Z" fill="black" />
|
||||
<path d="M379.049 216.467C377.869 217.204 374.035 217.942 370.642 217.942C367.25 217.942 362.678 218.679 360.613 219.416L356.778 220.891L357.663 236.82C358.106 245.522 358.843 258.796 359.286 266.171C359.876 277.527 359.728 279.887 357.958 279.887C356.778 279.887 350.289 270.743 343.652 259.534C336.867 248.472 329.64 236.968 327.428 234.165C325.216 231.363 323.003 227.528 322.413 225.758C321.823 223.989 320.201 222.219 318.873 221.776C314.744 220.449 287.606 223.546 285.098 225.611C282.591 227.528 282.886 236.968 286.131 262.189C286.868 268.236 287.458 278.412 287.606 284.754C287.606 294.489 288.343 297.881 292.03 305.993C294.538 311.155 296.455 316.17 296.455 317.055C296.455 317.792 297.34 320.3 298.372 322.364C299.552 324.577 301.322 328.264 302.502 330.771C304.567 334.901 305.157 335.196 312.089 334.754C323.741 333.869 323.593 334.606 321.381 300.683C320.348 284.46 319.906 270.743 320.348 270.3C322.413 268.236 325.805 272.218 331.558 284.017C344.979 310.86 353.534 326.199 355.746 327.527C357.368 328.559 360.761 328.264 366.808 326.642C371.675 325.167 378.017 324.134 380.967 324.134C388.194 324.134 393.061 319.562 391.734 314.105C391.144 311.893 390.406 290.212 389.964 265.876C389.521 241.54 388.931 220.154 388.489 218.236C387.751 214.697 383.179 213.812 379.049 216.467Z" fill="black" />
|
||||
<path d="M440.258 220.154C436.276 221.334 431.703 222.219 430.376 222.366C426.246 222.366 411.055 232.248 404.123 239.328L397.486 246.112L397.928 281.067C398.371 315.137 398.518 316.17 401.91 321.48C406.335 328.117 417.102 335.786 426.984 339.178C436.128 342.423 450.877 342.571 458.252 339.473C469.608 334.606 474.328 332.246 480.08 328.117L485.98 323.987V313.81C485.98 305.551 486.57 303.191 488.782 301.568C492.174 299.061 491.732 294.931 484.947 270.891C482.292 261.599 481.85 261.304 474.623 263.368C471.083 264.401 466.364 265.876 463.856 266.613C461.496 267.203 457.514 268.531 455.007 269.416C452.647 270.301 448.665 271.333 446.305 271.775C442.175 272.66 442.175 272.808 442.765 281.362C443.06 286.229 443.798 290.654 444.388 291.244C444.978 291.834 448.812 291.392 452.794 290.359C461.496 288.147 464.594 289.622 464.594 295.964C464.594 305.698 444.83 310.418 434.948 303.191C427.426 297.586 424.329 290.802 424.476 279.74C424.476 271.775 425.214 268.826 428.311 263.958C434.653 253.929 440.405 250.537 464.299 243.605C466.216 243.015 465.921 236.378 463.709 225.759C462.086 217.942 454.712 216.172 440.258 220.154Z" fill="black" />
|
||||
<path d="M250.733 252.897C237.311 254.077 221.235 258.206 220.645 260.566C220.35 261.894 219.318 266.613 218.285 271.038C217.4 275.463 215.925 282.1 215.04 285.787C214.156 289.474 212.976 296.111 212.238 300.536C211.648 304.961 210.616 309.975 210.173 311.745C209.436 313.958 210.911 316.612 215.04 321.775C222.71 330.919 225.365 341.686 222.71 351.715C221.678 355.55 219.908 359.827 218.728 361.154C217.695 362.334 216.81 364.547 216.81 365.874C216.81 367.939 218.138 368.234 226.102 367.496C237.017 366.316 238.934 364.547 238.934 354.96C238.934 346.848 239.819 346.258 252.798 346.258C262.532 346.258 265.482 347.88 265.482 353.042C265.482 354.812 266.662 357.762 268.137 359.532C270.644 362.482 271.234 362.629 277.281 361.007C296.603 355.992 301.322 354.37 301.912 352.747C302.65 350.83 296.455 334.754 289.67 320.742C288.491 318.382 287.606 315.727 287.606 314.842C287.606 313.958 285.688 309.09 283.328 304.223C281.116 299.356 278.019 292.424 276.544 288.737C273.889 281.805 271.972 276.938 267.842 267.646C266.515 264.548 265.482 261.304 265.482 260.271C265.482 257.616 259.877 251.864 257.665 252.159C256.633 252.159 253.535 252.602 250.733 252.897ZM249.258 295.521C250.881 299.356 252.208 303.486 252.208 304.961C252.208 306.436 253.24 309.975 254.42 312.778C257.96 321.332 257.37 323.397 250.881 325.019C240.114 327.969 238.786 324.429 242.031 303.191C244.686 285.787 244.833 285.787 249.258 295.521Z" fill="black" />
|
||||
<path d="M138.346 265.138C127.579 268.678 114.157 275.315 111.355 278.413C108.995 281.067 109.438 287.262 113.42 302.748C114.157 305.993 115.632 312.335 116.517 316.76C119.762 333.131 122.859 345.963 126.252 356.73C127.431 360.564 128.316 364.547 128.316 365.874C128.316 367.054 129.644 371.036 131.266 374.871L134.363 381.656H146.605C153.832 381.656 161.502 380.771 165.189 379.443C168.581 378.263 173.153 377.231 175.366 377.231C177.431 377.231 180.528 376.198 182.003 375.018C183.625 373.839 185.985 372.806 187.46 372.806C190.262 372.806 201.619 367.791 205.159 365.137C210.616 360.712 216.81 350.093 216.81 345.225C216.81 330.477 202.504 316.76 187.165 316.76C182.003 316.76 179.938 313.81 183.773 311.745C185.248 310.86 189.377 307.321 192.917 303.781C197.784 298.914 199.112 296.406 199.112 292.572C199.112 282.69 192.18 271.775 181.413 264.696C176.103 261.009 149.702 261.451 138.346 265.138ZM162.829 293.161C165.041 293.899 167.844 296.849 169.171 299.799C171.531 304.813 171.531 305.108 168.581 309.09C164.894 313.958 155.897 318.972 152.8 317.792C151.472 317.202 150.44 315.58 150.44 313.958C150.44 312.335 149.26 308.648 147.933 305.698C145.13 299.651 146.163 296.554 152.062 293.604C156.339 291.392 157.962 291.244 162.829 293.161ZM171.236 327.969C173.743 329.149 176.693 332.394 177.873 335.344C179.79 339.768 179.79 340.948 177.726 344.783C175.218 349.65 168.729 353.632 163.272 353.632C160.469 353.632 159.437 352.305 157.372 346.553C156.045 342.718 154.865 337.261 154.865 334.311C154.865 329.592 155.455 328.854 159.732 327.379C166.221 325.314 166.221 325.314 171.236 327.969Z" fill="black" />
|
||||
<path d="M551.76 265.729C550.433 267.203 550.58 268.088 552.35 269.563C555.153 271.923 558.397 269.416 557.07 266.024C555.89 263.074 553.973 263.074 551.76 265.729Z" fill="black" />
|
||||
<path d="M536.126 272.513C535.684 273.398 536.864 274.578 538.929 275.463C540.994 276.2 542.763 277.528 542.763 278.413C542.763 279.15 544.091 279.887 545.713 279.887C547.336 279.887 548.663 278.855 548.663 277.675C548.663 276.495 547.778 275.463 546.598 275.463C545.566 275.463 543.796 274.43 542.763 273.25C540.698 270.743 537.454 270.301 536.126 272.513Z" fill="black" />
|
||||
<path d="M561.937 277.675C561.937 278.855 562.969 279.887 564.149 279.887C565.329 279.887 566.362 278.855 566.362 277.675C566.362 276.495 565.329 275.463 564.149 275.463C562.969 275.463 561.937 276.495 561.937 277.675Z" fill="black" />
|
||||
<path d="M531.997 283.87C531.407 284.312 530.964 287.262 530.964 290.212C530.964 296.111 535.684 302.011 540.698 302.011C542.173 302.158 545.123 303.043 547.188 304.223C549.253 305.403 552.498 306.288 554.268 306.436C559.135 306.436 557.66 302.896 552.645 302.306C549.548 301.863 548.663 300.979 548.663 298.324C548.663 293.604 546.303 292.424 542.026 295.226C540.109 296.554 537.749 297.586 536.864 297.586C534.651 297.586 535.094 292.277 537.601 290.212C540.551 287.852 540.404 284.902 537.454 283.722C534.504 282.542 533.176 282.542 531.997 283.87Z" fill="black" />
|
||||
<path d="M70.0578 285.787C68.8779 287.704 73.0076 289.327 75.9574 288.147C77.1373 287.704 78.1698 286.672 78.1698 285.787C78.1698 283.87 71.2377 283.87 70.0578 285.787Z" fill="black" />
|
||||
<path d="M553.088 286.524C553.088 287.852 553.825 288.737 554.71 288.442C555.595 288.147 556.332 287.262 556.332 286.524C556.332 285.787 555.595 284.902 554.71 284.607C553.825 284.312 553.088 285.197 553.088 286.524Z" fill="black" />
|
||||
<path d="M561.937 295.226C561.937 296.554 562.969 297.586 564.297 297.586C566.804 297.586 566.362 294.342 563.707 293.457C562.674 293.162 561.937 293.899 561.937 295.226Z" fill="black" />
|
||||
<path d="M497.631 301.126C492.322 303.486 491.142 305.846 491.142 313.81C491.142 319.857 493.207 322.807 499.991 326.347C509.873 331.361 521.967 319.267 518.427 307.911C517.248 304.223 509.431 299.061 505.153 299.061C503.531 299.209 500.139 300.094 497.631 301.126Z" fill="black" />
|
||||
<path d="M31.7104 303.781C3.24478 305.993 0 306.436 0 308.796C0 310.565 4.12972 310.86 23.3034 310.86C35.9876 310.713 51.179 310.123 56.7837 309.385C62.5358 308.648 71.0902 307.616 75.9574 307.173C91.5913 305.698 95.1311 304.813 92.7712 303.338C90.2639 301.716 53.9813 302.011 31.7104 303.781Z" fill="black" />
|
||||
<path d="M585.093 316.17C583.323 317.94 585.683 319.71 589.96 319.71C592.32 319.71 594.532 319.12 594.975 318.382C596.45 316.022 587.305 313.958 585.093 316.17Z" fill="black" />
|
||||
<path d="M571.524 321.332C570.049 323.544 571.966 325.019 573.884 323.102C574.916 322.07 574.916 321.185 574.031 320.595C573.146 320.152 571.966 320.447 571.524 321.332Z" fill="black" />
|
||||
<path d="M644.826 322.659C652.643 323.249 661.935 324.872 665.475 326.199C669.752 327.822 675.062 328.559 681.404 328.264C705.887 327.084 675.799 321.185 646.744 321.48L630.52 321.627L644.826 322.659Z" fill="black" />
|
||||
<path d="M63.4208 325.757C61.5034 326.642 55.3088 327.674 49.8517 328.264C44.2471 328.854 39.8224 329.886 39.8224 330.771C39.8224 333.279 55.8988 333.426 59.881 330.919C61.7984 329.591 66.2231 328.559 69.9104 328.559C78.1698 328.559 82.152 327.232 79.3497 325.462C76.8424 323.839 67.993 323.987 63.4208 325.757Z" fill="black" />
|
||||
<path d="M543.058 331.066C543.353 332.541 544.533 333.721 545.713 333.721C546.893 333.721 548.073 332.541 548.368 331.066C548.663 329.297 547.925 328.559 545.713 328.559C543.501 328.559 542.763 329.297 543.058 331.066Z" fill="black" />
|
||||
<path d="M562.674 330.034C562.085 330.919 562.232 332.394 563.117 333.279C564.002 334.164 564.887 334.164 565.624 332.984C566.214 332.099 566.067 330.624 565.182 329.739C564.297 328.854 563.412 328.854 562.674 330.034Z" fill="black" />
|
||||
<path d="M495.272 336.966C493.797 337.261 492.617 338.293 492.617 339.031C492.617 339.768 489.814 341.686 486.422 343.456C476.54 348.47 473.443 351.862 473.443 358.205C473.443 363.809 481.997 384.753 485.095 386.67C485.98 387.26 486.717 389.03 486.717 390.652C486.717 392.275 487.75 393.897 488.929 394.34C490.109 394.93 491.142 395.962 491.142 396.699C491.142 398.174 499.106 409.236 502.204 411.891C503.089 412.628 504.711 414.693 505.743 416.316C507.366 418.97 507.513 418.823 506.776 414.546C506.186 410.563 506.481 409.826 509.283 409.384C511.348 409.089 512.528 407.909 512.528 405.991C512.528 402.304 507.071 401.272 504.416 404.516C503.383 405.696 502.056 406.729 501.319 406.729C499.106 406.729 499.844 402.009 502.351 399.059C504.416 396.994 504.416 396.109 502.941 394.192C501.909 393.012 499.844 391.98 498.369 391.98C496.451 391.98 495.566 390.652 495.272 387.26C494.977 383.573 493.944 382.245 491.142 381.655C485.685 380.328 485.98 373.691 491.584 373.101C495.124 372.659 495.566 373.101 495.566 377.083C495.566 381.213 496.009 381.655 499.991 381.655C503.973 381.655 504.416 381.213 504.416 377.231C504.416 373.249 504.858 372.806 508.841 372.806C512.528 372.806 513.265 372.216 513.265 369.119C513.265 366.021 512.528 365.432 508.841 365.432C505.153 365.432 504.416 366.021 504.416 369.119C504.416 372.216 503.678 372.806 499.991 372.806C496.304 372.806 495.566 372.216 495.566 369.119C495.566 366.169 496.451 365.137 499.844 364.547C503.236 363.809 504.416 362.777 504.711 359.679C505.006 356.73 506.038 355.697 509.283 355.402C512.675 354.96 513.265 354.222 513.265 350.535C513.265 346.7 513.708 346.258 517.69 346.258C521.082 346.258 522.115 345.52 522.115 343.456C522.115 338.588 518.87 338.146 515.183 342.423C511.2 347.143 505.448 347.143 504.711 342.571C504.268 339.768 500.434 335.786 498.664 336.081C498.221 336.229 496.599 336.523 495.272 336.966ZM504.121 350.978C504.563 354.665 504.121 355.107 500.139 355.107C496.009 355.107 495.566 354.665 495.566 350.535C495.566 346.553 496.009 346.11 499.696 346.553C502.794 346.848 503.826 347.88 504.121 350.978ZM495.272 360.859C495.714 364.989 495.419 365.727 493.354 364.842C492.027 364.399 489.962 363.957 488.782 363.957C486.275 363.957 485.98 357.172 488.634 356.14C492.617 354.37 494.829 355.992 495.272 360.859Z" fill="black" />
|
||||
<path d="M532.734 341.391C533.029 344.34 533.914 345.52 536.126 345.52C540.698 345.52 540.256 338.293 535.684 337.703C532.734 337.261 532.292 337.703 532.734 341.391Z" fill="black" />
|
||||
<path d="M553.678 340.506C553.235 341.391 553.53 342.866 554.415 343.751C555.595 344.93 556.037 344.488 556.037 342.128C556.037 338.588 555.3 337.998 553.678 340.506Z" fill="black" />
|
||||
<path d="M525.802 348.618C523.737 350.83 523.59 351.42 525.507 353.338C528.162 355.845 530.964 354.517 530.964 350.683C530.964 346.7 528.604 345.816 525.802 348.618Z" fill="black" />
|
||||
<path d="M543.206 350.093C542.173 352.747 544.828 355.845 547.041 354.517C549.548 352.895 548.958 347.733 546.451 347.733C545.123 347.733 543.796 348.765 543.206 350.093Z" fill="black" />
|
||||
<path d="M561.052 351.42C559.872 354.37 561.495 356.14 566.362 357.025C568.427 357.32 569.312 357.32 568.279 357.025C567.394 356.73 566.362 354.96 566.067 353.337C565.477 349.355 562.232 348.175 561.052 351.42Z" fill="black" />
|
||||
<path d="M515.92 356.73C514.445 357.467 513.265 359.532 513.265 361.007C513.265 363.367 514.298 363.957 517.838 363.957C521.82 363.957 522.262 363.514 521.82 359.975C521.377 355.55 519.607 354.517 515.92 356.73Z" fill="black" />
|
||||
<path d="M533.471 357.615C531.259 359.68 532.439 363.957 535.241 363.957C539.076 363.957 540.551 361.449 538.339 358.795C536.274 356.435 534.946 355.992 533.471 357.615Z" fill="black" />
|
||||
<path d="M582.291 362.039C584.061 362.334 586.715 362.334 588.19 362.039C589.518 361.597 588.043 361.302 584.798 361.449C581.553 361.449 580.373 361.744 582.291 362.039Z" fill="black" />
|
||||
<path d="M595.27 365.137C594.237 366.907 597.777 368.381 603.529 368.381C609.871 368.381 607.954 364.842 601.464 364.252C598.515 363.957 595.712 364.399 595.27 365.137Z" fill="black" />
|
||||
<path d="M525.065 369.709C525.065 371.331 528.899 372.954 530.079 371.774C530.522 371.331 530.522 370.299 530.079 369.709C529.047 367.939 525.065 368.086 525.065 369.709Z" fill="black" />
|
||||
<path d="M612.969 369.709C612.084 371.036 618.868 372.806 624.768 372.806C628.75 372.806 629.487 370.594 625.948 369.266C622.408 367.939 613.854 368.234 612.969 369.709Z" fill="black" />
|
||||
<path d="M516.658 373.839C515.625 374.134 514.74 376.198 514.74 378.116C514.74 380.918 515.478 381.656 518.575 381.656C521.82 381.656 522.262 381.066 521.82 377.673C521.377 373.691 519.755 372.511 516.658 373.839Z" fill="black" />
|
||||
<path d="M631.11 374.871C632.142 376.198 635.24 377.231 637.747 377.231C640.254 377.378 643.941 378.263 646.006 379.443C650.431 381.95 661.198 382.393 660.46 380.033C659.575 377.231 648.661 373.839 638.632 373.249C630.077 372.659 629.34 372.806 631.11 374.871Z" fill="black" />
|
||||
<path d="M533.029 376.493C532.439 377.821 532.734 379.591 533.619 380.476C535.831 382.688 540.109 380.623 539.519 377.673C538.929 374.281 534.209 373.396 533.029 376.493Z" fill="black" />
|
||||
<path d="M504.416 386.818C504.416 389.915 505.153 390.505 508.841 390.505C512.528 390.505 513.265 389.915 513.265 386.818C513.265 383.72 512.528 383.13 508.841 383.13C505.153 383.13 504.416 383.72 504.416 386.818Z" fill="black" />
|
||||
<path d="M119.467 389.178C119.467 392.717 122.859 392.275 123.597 388.588C123.892 387.113 123.154 386.08 121.827 386.08C120.352 386.08 119.467 387.408 119.467 389.178Z" fill="black" />
|
||||
<path d="M525.802 387.408C524.77 389.177 527.572 390.947 529.489 389.767C531.112 388.735 530.079 386.08 528.014 386.08C527.277 386.08 526.392 386.67 525.802 387.408Z" fill="black" />
|
||||
<path d="M176.841 392.127C175.366 393.012 175.956 393.75 178.758 394.782C180.823 395.52 183.035 396.994 183.625 397.732C184.068 398.617 185.837 399.354 187.312 399.354C188.935 399.354 191.442 401.124 192.769 403.337C194.244 405.696 197.194 409.089 199.554 410.859C203.241 414.103 203.536 415.136 203.536 426.05C203.536 437.259 203.389 437.849 199.849 439.177C196.014 440.652 195.129 442.716 197.637 444.339C199.849 445.666 199.407 450.386 196.899 452.451C194.687 454.368 193.802 459.825 195.867 459.825C196.457 459.678 199.554 457.465 202.651 454.663C207.371 450.533 207.961 449.353 205.896 448.616C202.946 447.436 202.799 441.684 205.749 439.177C207.371 437.702 207.961 434.014 207.961 422.953V408.646L202.504 402.452C198.227 397.879 194.687 395.815 187.755 393.602C182.593 392.127 177.873 391.39 176.841 392.127Z" fill="black" />
|
||||
<path d="M115.927 395.667C115.485 396.847 115.78 398.174 116.37 398.617C117.107 399.059 117.992 398.322 118.582 397.142C119.024 395.962 118.729 394.635 118.14 394.192C117.402 393.75 116.517 394.487 115.927 395.667Z" fill="black" />
|
||||
<path d="M516.363 394.782C513.708 396.552 515.625 399.354 519.312 399.354C521.377 399.354 522.115 398.469 521.82 396.847C521.23 393.897 519.165 393.012 516.363 394.782Z" fill="black" />
|
||||
<path d="M147.638 396.257C144.54 398.174 148.08 399.502 153.537 398.322C157.667 397.584 157.962 397.289 155.602 396.257C151.62 394.782 149.997 394.782 147.638 396.257Z" fill="black" />
|
||||
<path d="M168.434 396.552C168.729 397.437 169.614 398.027 170.351 398.027C171.089 398.027 171.973 397.437 172.268 396.552C172.563 395.667 171.678 394.93 170.351 394.93C169.024 394.93 168.139 395.667 168.434 396.552Z" fill="black" />
|
||||
<path d="M533.177 396.404C532.734 397.142 532.587 397.879 533.029 397.879C533.324 397.879 534.651 398.322 535.979 398.764C537.601 399.354 538.339 398.912 538.339 397.289C538.339 394.635 534.651 394.045 533.177 396.404Z" fill="black" />
|
||||
<path d="M176.988 405.991C176.988 407.171 178.02 408.204 179.2 408.204C180.38 408.204 181.413 407.171 181.413 405.991C181.413 404.811 180.38 403.779 179.2 403.779C178.02 403.779 176.988 404.811 176.988 405.991Z" fill="black" />
|
||||
<path d="M525.802 405.106C524.77 406.876 527.572 408.646 529.489 407.466C531.112 406.434 530.079 403.779 528.014 403.779C527.277 403.779 526.392 404.369 525.802 405.106Z" fill="black" />
|
||||
<path d="M105.603 411.743C103.096 415.578 104.423 417.643 108.258 416.168C110.765 415.283 111.65 408.204 109.29 408.204C108.553 408.204 106.93 409.826 105.603 411.743Z" fill="black" />
|
||||
<path d="M423.296 410.564C419.609 411.744 414.742 414.988 412.53 417.496C405.155 425.903 398.961 442.864 397.338 458.645C396.601 466.905 396.011 468.675 393.798 468.675C391.291 468.675 389.374 465.872 389.374 462.038C389.374 460.12 384.802 449.796 381.409 444.339C369.61 424.723 363.12 417.791 355.009 416.316C347.044 414.841 341.439 416.611 334.802 423.1C328.46 429.147 316.956 446.256 312.236 456.138C310.762 459.383 308.844 463.07 308.107 464.25C307.369 465.43 306.337 467.495 305.894 468.675C305.452 469.855 304.419 471.919 303.682 473.099C302.945 474.279 301.765 476.787 301.027 478.556C300.142 480.179 298.815 482.096 298.077 482.686C297.192 483.129 296.455 485.194 296.455 486.963C296.455 488.881 295.423 491.241 294.243 492.273C291.735 494.338 291.293 499.058 293.505 500.385C294.243 500.827 295.423 499.353 295.865 497.14C296.455 494.928 297.782 492.273 298.815 491.536C299.847 490.651 300.88 488.733 301.175 487.111C301.47 485.489 303.24 483.866 305.304 483.424C308.107 482.834 309.139 481.506 309.434 478.262C310.172 470.592 311.057 469.412 313.269 472.362C315.776 475.902 320.053 474.279 320.053 469.56C320.053 467.347 319.021 465.872 317.104 465.282C315.334 464.84 314.154 463.218 314.154 461.3C314.154 459.383 315.334 457.76 317.104 457.318C318.726 456.875 320.053 455.401 320.053 454.073C320.053 448.026 328.313 445.666 329.198 451.271C329.493 453.188 331.41 455.548 333.77 456.58C338.932 459.088 338.785 464.692 333.622 465.43C330.82 465.872 330.23 466.757 330.673 469.855C330.968 472.952 332 473.984 335.097 474.279C339.67 474.869 340.407 477.377 341.144 493.011C341.439 500.385 341.882 501.86 344.242 502.302C346.749 502.892 346.897 502.302 345.864 497.288C344.537 490.356 344.537 488.881 345.864 481.064C346.897 475.607 346.602 474.722 343.799 473.837C339.522 472.509 339.817 467.495 344.389 465.43C349.109 463.365 349.551 456.875 345.127 456.875C341.882 456.875 338.637 451.123 340.702 449.058C342.914 446.699 347.192 448.763 348.077 452.451C349.551 458.793 358.401 457.465 358.401 450.828C358.401 448.616 359.433 448.026 362.973 448.026C366.955 448.026 367.398 448.469 366.955 452.008C366.66 455.253 365.628 456.285 362.531 456.58C358.991 457.023 358.401 457.76 358.401 461.448C358.401 464.545 359.138 465.725 361.056 465.725C364.448 465.725 367.25 462.923 367.25 459.678C367.25 458.203 368.873 456.875 371.527 456.433C374.035 455.843 375.952 454.368 376.542 452.598C377.427 449.058 380.524 448.321 380.524 451.566C380.524 452.746 379.492 454.663 378.312 455.991C374.772 459.383 375.657 462.775 380.524 464.397C383.032 465.282 384.949 466.905 384.949 468.085C384.949 469.117 387.014 472.067 389.374 474.574C391.881 477.082 393.798 480.621 393.798 482.686C393.798 486.963 394.241 487.111 398.961 484.014C401.468 482.391 402.648 480.326 402.648 477.229C402.648 473.542 403.09 473.099 406.778 473.394C410.17 473.837 410.76 473.247 411.202 469.855C411.645 466.462 411.055 465.872 407.515 465.43C404.27 465.135 403.385 464.25 403.385 461.3C403.385 458.645 404.418 457.465 407.073 456.875C409.58 456.285 410.907 454.811 411.202 452.451C411.497 450.386 412.235 448.174 412.972 447.584C415.037 445.371 419.462 447.731 422.117 452.451C425.214 457.76 428.754 458.203 430.081 453.336C431.261 448.616 429.786 446.551 425.361 446.551C422.559 446.551 421.822 445.814 421.822 442.864C421.822 439.767 422.559 439.177 426.099 439.177C429.491 439.177 430.523 439.914 430.966 442.421C431.556 446.994 437.308 447.731 439.373 443.749C441.143 440.504 440.7 440.062 434.653 438.587C431.408 437.849 430.523 436.817 430.966 434.309C431.261 431.802 432.441 431.065 435.833 431.065C439.373 431.065 440.405 431.802 440.7 434.457C441.143 436.817 442.47 438.144 445.567 438.734C448.517 439.324 450.14 440.652 450.73 443.159C451.467 446.109 452.352 446.551 455.154 445.961C458.104 445.224 458.842 445.814 459.874 449.796C460.759 453.631 461.939 454.663 464.594 454.663C467.101 454.663 468.428 455.696 469.018 458.35C469.608 460.71 471.083 462.185 473.296 462.48C475.36 462.775 477.13 464.545 478.163 467.2C478.9 469.56 480.67 471.919 481.997 472.362C483.472 472.657 491.437 480.031 499.844 488.586C508.398 497.14 515.773 504.072 516.51 504.072C521.23 504.072 514.593 495.518 493.354 474.132C479.933 460.71 469.018 449.059 469.018 448.321C469.018 447.584 467.101 446.109 464.594 445.076C459.431 442.864 458.989 440.799 463.266 437.259C466.659 434.309 466.364 431.36 462.676 430.622C460.906 430.18 460.169 431.212 460.169 433.867C460.169 438.292 460.169 438.292 455.007 435.932C450.582 433.867 450.14 430.917 454.269 429.59C455.892 429.147 457.219 427.967 457.219 427.23C457.219 426.345 458.842 424.133 460.906 422.215L464.741 418.823L459.874 416.316C454.712 413.661 441.733 409.826 434.358 408.794C431.998 408.499 426.984 409.236 423.296 410.564ZM429.786 423.838C431.261 427.82 430.818 428.852 427.131 428.852C423.001 428.852 421.232 426.787 422.411 423.69C423.444 420.74 428.606 420.888 429.786 423.838ZM449.845 425.313C449.845 428.557 449.255 429 445.862 428.557C442.913 428.262 441.585 427.23 441.29 424.72Line truncated
|
||||
<path d="M185.837 414.841C185.837 416.168 186.575 417.053 187.46 416.758C188.345 416.463 189.082 415.578 189.082 414.841C189.082 414.103 188.345 413.218 187.46 412.923C186.575 412.628 185.837 413.513 185.837 414.841Z" fill="black" />
|
||||
<path d="M515.773 413.513C514.298 415.136 516.215 417.053 519.312 417.053C520.787 417.053 522.115 416.021 522.115 414.841C522.115 412.629 517.543 411.744 515.773 413.513Z" fill="black" />
|
||||
<path d="M535.536 413.956C534.062 414.988 534.062 415.431 535.389 416.316C536.274 416.906 537.749 416.758 538.634 415.873C540.551 413.956 538.044 412.481 535.536 413.956Z" fill="black" />
|
||||
<path d="M194.687 423.69C194.687 424.87 195.719 425.903 196.899 425.903C198.079 425.903 199.112 424.87 199.112 423.69C199.112 422.51 198.079 421.478 196.899 421.478C195.719 421.478 194.687 422.51 194.687 423.69Z" fill="black" />
|
||||
<path d="M466.364 423.985C466.659 425.46 469.313 427.82 472.411 429.295C475.803 431.065 477.868 433.13 477.868 434.899C477.868 436.374 478.31 437.702 478.9 437.702C481.26 437.702 489.667 434.015 489.667 432.982C489.667 432.245 486.57 430.327 482.735 428.41C478.753 426.64 475.065 424.28 474.475 423.248C473.885 422.363 471.673 421.478 469.608 421.478C466.806 421.478 465.921 422.215 466.364 423.985Z" fill="black" />
|
||||
<path d="M245.571 425.755C243.211 426.345 237.459 429 233.034 431.507C225.07 435.932 222.12 439.177 225.955 439.177C226.987 439.177 229.052 440.504 230.527 442.126C231.854 443.749 233.919 445.076 235.099 445.076C237.607 445.076 239.376 441.094 237.459 439.914C234.067 437.849 236.279 436.079 240.999 436.964C245.128 437.849 246.308 437.407 247.636 434.899C249.848 430.917 254.568 430.917 256.633 434.752C257.518 436.374 259.583 437.702 261.205 437.702C262.975 437.702 264.597 439.177 265.482 441.389C266.22 443.454 268.579 445.814 270.644 446.846C273.299 448.026 274.332 449.648 274.332 452.598C274.332 455.99 273.594 456.728 270.349 457.17C265.63 457.613 264.892 460.858 268.874 463.365C271.087 464.692 271.972 464.397 273.594 462.038C274.774 460.415 276.396 458.35 277.281 457.318C279.789 454.368 279.051 448.616 274.921 440.062C269.759 429 265.63 425.608 257.075 425.018C253.24 424.723 248.078 425.165 245.571 425.755Z" fill="black" />
|
||||
<path d="M513.56 427.23C514.15 429.147 516.215 429.59 516.363 427.82C516.363 427.23 515.625 426.492 514.74 426.05C513.708 425.755 513.118 426.345 513.56 427.23Z" fill="black" />
|
||||
<path d="M490.109 438.292C488.192 439.177 486.717 440.652 486.717 441.684C486.717 444.929 492.174 443.749 494.682 439.914C497.336 435.784 495.861 435.342 490.109 438.292Z" fill="black" />
|
||||
<path d="M162.387 455.548C148.08 470.002 142.77 477.524 147.343 477.524C148.965 477.524 172.563 451.861 172.563 449.943C172.563 449.648 174.628 447.436 177.136 445.224C179.643 442.864 180.97 440.652 180.233 440.062C179.495 439.619 171.383 446.551 162.387 455.548Z" fill="black" />
|
||||
<path d="M217.253 441.094C216.663 442.274 214.893 443.454 213.566 444.044C212.091 444.486 210.911 445.666 210.911 446.551C210.911 448.469 213.418 448.469 214.598 446.404C215.188 445.666 217.253 444.339 219.318 443.454C221.235 442.716 222.415 441.537 221.973 440.652C220.645 438.587 218.58 438.882 217.253 441.094Z" fill="black" />
|
||||
<path d="M247.783 441.242C247.783 443.601 251.765 445.519 253.83 444.191C256.633 442.569 255.158 439.177 251.47 439.177C249.258 439.177 247.783 440.062 247.783 441.242Z" fill="black" />
|
||||
<path d="M468.281 440.652C466.658 443.306 470.198 445.961 472.853 444.339C474.18 443.454 475.213 442.126 475.36 441.389C475.803 439.324 469.461 438.734 468.281 440.652Z" fill="black" />
|
||||
<path d="M442.618 447.879C440.11 449.501 441.143 454.516 444.24 455.106C445.567 455.401 447.485 454.221 448.222 452.598C449.55 450.238 449.402 449.353 447.19 448.174C445.715 447.436 443.65 447.289 442.618 447.879Z" fill="black" />
|
||||
<path d="M222.12 449.206C221.678 450.091 221.973 451.271 222.857 451.713C225.07 453.188 226.545 451.271 224.627 449.353C223.595 448.321 222.71 448.321 222.12 449.206Z" fill="black" />
|
||||
<path d="M239.229 450.533C239.524 452.303 240.999 453.188 243.359 453.188C248.668 453.188 248.226 448.911 242.916 448.321C239.524 447.879 238.786 448.321 239.229 450.533Z" fill="black" />
|
||||
<path d="M256.633 449.796C256.633 450.828 257.37 452.746 258.108 453.926C259.73 456.581 264.45 454.368 265.187 450.386C265.63 448.616 264.597 448.026 261.205 448.026C258.55 448.026 256.633 448.764 256.633 449.796Z" fill="black" />
|
||||
<path d="M477.868 450.976C477.868 453.188 478.605 453.926 480.522 453.631C481.85 453.336 483.03 452.156 483.03 450.976C483.03 449.796 481.85 448.616 480.522 448.321C478.605 448.026 477.868 448.763 477.868 450.976Z" fill="black" />
|
||||
<path d="M517.69 450.238C517.69 451.418 518.722 452.451 519.902 452.451C521.082 452.451 522.115 451.418 522.115 450.238C522.115 449.059 521.082 448.026 519.902 448.026C518.722 448.026 517.69 449.059 517.69 450.238Z" fill="black" />
|
||||
<path d="M534.061 456.728C531.702 458.203 533.177 461.3 536.274 461.3C537.454 461.3 538.339 461.89 538.339 462.628C538.339 463.808 546.303 468.675 548.221 468.675C549.253 468.675 543.353 460.415 540.551 458.055C537.159 455.106 536.864 454.958 534.061 456.728Z" fill="black" />
|
||||
<path d="M230.084 459.825C230.084 462.038 230.822 462.775 232.739 462.48C234.067 462.185 235.247 461.005 235.247 459.825C235.247 458.645 234.067 457.465 232.739 457.171C230.822 456.876 230.084 457.613 230.084 459.825Z" fill="black" />
|
||||
<path d="M321.528 459.53C321.528 463.512 324.626 466.02 326.985 464.102C330.378 461.153 329.345 456.875 325.216 456.875C322.708 456.875 321.528 457.613 321.528 459.53Z" fill="black" />
|
||||
<path d="M412.677 457.76C411.055 459.235 413.267 463.955 415.922 464.692C419.462 465.577 420.347 464.693 420.347 460.268C420.347 457.76 419.462 456.876 416.954 456.876C415.037 456.876 413.12 457.318 412.677 457.76Z" fill="black" />
|
||||
<path d="M433.031 458.203C431.703 460.268 433.621 464.25 435.833 464.25C438.045 464.25 439.963 460.268 438.635 458.203C438.193 457.465 437.013 456.875 435.833 456.875C434.653 456.875 433.473 457.465 433.031 458.203Z" fill="black" />
|
||||
<path d="M451.319 459.678C451.319 461.3 451.909 462.923 452.647 463.365C454.417 464.545 457.662 460.268 456.482 458.35C454.859 455.843 451.319 456.728 451.319 459.678Z" fill="black" />
|
||||
<path d="M278.461 464.84C275.806 466.757 274.774 473.099 276.986 473.099C277.724 473.099 278.904 474.574 279.789 476.344C281.264 479.589 281.264 479.589 282.296 475.312C282.738 472.804 282.738 469.117 282.148 466.905C281.116 463.513 280.674 463.218 278.461 464.84Z" fill="black" />
|
||||
<path d="M258.698 467.937C257.518 471.035 259.14 474.574 261.647 474.574C264.007 474.574 266.22 469.56 264.745 467.2C263.417 464.987 259.582 465.43 258.698 467.937Z" fill="black" />
|
||||
<path d="M349.551 470.297C349.551 474.132 349.994 474.722 352.944 474.279C355.303 473.984 356.336 472.657 356.631 469.707C357.073 466.315 356.631 465.725 353.386 465.725C350.141 465.725 349.551 466.315 349.551 470.297Z" fill="black" />
|
||||
<path d="M367.545 469.707C367.84 472.952 368.725 473.837 371.675 473.837C374.625 473.837 375.51 472.952 375.805 469.707C376.247 466.167 375.805 465.725 371.675 465.725C367.545 465.725 367.103 466.167 367.545 469.707Z" fill="black" />
|
||||
<path d="M422.264 468.085C421.232 470.887 425.214 475.312 427.574 473.984C428.459 473.394 429.196 471.329 429.196 469.265C429.196 466.757 428.459 465.725 426.246 465.725C424.624 465.725 422.854 466.757 422.264 468.085Z" fill="black" />
|
||||
<path d="M442.47 469.265C442.47 473.837 444.535 475.164 446.895 472.362C449.107 469.707 447.78 465.725 444.682 465.725C443.355 465.725 442.47 467.052 442.47 469.265Z" fill="black" />
|
||||
<path d="M242.179 468.822C242.474 469.707 243.359 470.297 244.096 470.297C244.833 470.297 245.718 469.707 246.013 468.822C246.308 467.937 245.423 467.2 244.096 467.2C242.769 467.2 241.884 467.937 242.179 468.822Z" fill="black" />
|
||||
<path d="M460.464 469.56C461.054 472.657 463.709 472.952 464.299 469.707C464.594 468.232 463.856 467.2 462.381 467.2C460.906 467.2 460.169 468.085 460.464 469.56Z" fill="black" />
|
||||
<path d="M222.12 471.477C221.53 472.214 221.678 473.984 222.12 475.312C222.858 477.229 223.3 477.376 224.332 475.754C225.07 474.722 225.66 472.952 225.66 471.919C225.66 469.855 223.3 469.56 222.12 471.477Z" fill="black" />
|
||||
<path d="M323.003 478.999C323.003 481.949 323.741 483.424 325.216 483.424C326.69 483.424 327.428 481.949 327.428 478.999C327.428 476.049 326.69 474.574 325.216 474.574C323.741 474.574 323.003 476.049 323.003 478.999Z" fill="black" />
|
||||
<path d="M359.433 475.459C358.843 476.049 358.401 478.114 358.401 480.031C358.401 482.981 359.138 483.571 361.793 483.129C364.153 482.834 365.038 481.654 365.038 478.999C365.038 475.459 361.646 473.247 359.433 475.459Z" fill="black" />
|
||||
<path d="M378.164 476.639C376.837 480.179 378.754 483.719 381.557 483.129C383.327 482.834 384.212 481.359 384.212 478.999C384.212 474.722 379.492 472.952 378.164 476.639Z" fill="black" />
|
||||
<path d="M415.037 476.639C413.857 479.884 415.479 483.866 417.839 482.981C418.724 482.686 419.609 480.916 419.609 478.999C419.609 475.017 416.364 473.247 415.037 476.639Z" fill="black" />
|
||||
<path d="M433.916 479.294C434.211 481.211 435.096 482.686 435.833 482.686C436.718 482.686 437.455 481.211 437.75 479.294C438.045 477.082 437.455 476.049 435.833 476.049C434.211 476.049 433.621 477.082 433.916 479.294Z" fill="black" />
|
||||
<path d="M269.464 478.409C268.284 479.589 269.612 481.949 271.529 481.949C272.267 481.949 272.857 480.916 272.857 479.736C272.857 477.524 271.087 476.787 269.464 478.409Z" fill="black" />
|
||||
<path d="M216.81 481.801C216.81 482.539 217.253 484.309 217.695 485.636C218.433 487.553 218.875 487.701 219.908 486.078C221.825 483.129 221.53 480.474 219.023 480.474C217.843 480.474 216.81 481.064 216.81 481.801Z" fill="black" />
|
||||
<path d="M257.223 485.636C256.19 490.061 257.518 494.338 259.582 493.01C261.5 491.831 261.5 483.866 259.582 482.686C258.845 482.244 257.813 483.571 257.223 485.636Z" fill="black" />
|
||||
<path d="M279.494 483.866C278.166 487.111 279.199 493.011 280.969 492.716C281.853 492.568 282.591 490.651 282.886 488.438C283.476 483.719 280.821 480.179 279.494 483.866Z" fill="black" />
|
||||
<path d="M314.744 487.111C314.301 488.291 314.301 489.913 314.891 490.798C316.514 493.453 318.578 492.421 318.578 488.586C318.578 484.604 316.071 483.571 314.744 487.111Z" fill="black" />
|
||||
<path d="M331.853 488.586C331.853 490.946 332.737 492.273 334.065 492.273C335.392 492.273 336.277 490.946 336.277 488.586C336.277 486.226 335.392 484.899 334.065 484.899C332.737 484.899 331.853 486.226 331.853 488.586Z" fill="black" />
|
||||
<path d="M350.141 487.111C348.961 490.061 350.731 492.273 354.124 492.273C357.221 492.273 357.811 489.323 355.156 486.668C352.796 484.309 351.174 484.456 350.141 487.111Z" fill="black" />
|
||||
<path d="M425.804 485.931C424.181 487.406 424.624 492.273 426.246 492.273C428.459 492.273 429.786 487.848 428.164 486.226C427.426 485.489 426.394 485.341 425.804 485.931Z" fill="black" />
|
||||
<path d="M369.02 488.881C369.758 492.716 374.625 493.158 374.625 489.471C374.625 487.258 373.74 486.373 371.527 486.373C369.463 486.373 368.725 487.111 369.02 488.881Z" fill="black" />
|
||||
<path d="M405.893 488.881C406.483 491.978 410.76 492.568 410.76 489.323C410.76 488.143 409.58 486.963 408.105 486.668C406.04 486.226 405.45 486.816 405.893 488.881Z" fill="black" />
|
||||
<path d="M387.899 489.323C387.899 490.061 388.784 490.798 389.964 490.798C391.144 490.798 392.618 490.061 393.061 489.323C393.503 488.438 392.618 487.848 390.996 487.848C389.226 487.848 387.899 488.438 387.899 489.323Z" fill="black" />
|
||||
<path d="M213.418 491.831C212.828 492.273 212.386 493.895 212.386 495.223C212.386 496.698 211.353 498.615 210.173 499.648C208.993 500.68 207.961 502.45 207.961 503.63C207.961 506.727 211.501 505.105 212.238 501.86C212.533 500.385 213.566 498.173 214.745 496.993C215.925 495.96 216.81 494.043 216.81 492.863C216.81 490.651 215.04 490.061 213.418 491.831Z" fill="black" />
|
||||
<path d="M359.433 494.78C358.843 495.223 358.401 496.993 358.401 498.468C358.401 500.385 359.433 501.122 362.088 501.122C366.07 501.122 366.955 498.468 364.005 495.518C361.941 493.453 360.761 493.305 359.433 494.78Z" fill="black" />
|
||||
<path d="M252.945 496.845C252.355 497.73 252.355 500.827 252.945 503.777C253.83 508.349 254.273 508.792 255.158 506.285C257.96 499.648 256.043 491.831 252.945 496.845Z" fill="black" />
|
||||
<path d="M305.304 498.173C305.304 499.795 306.337 501.122 307.517 501.122C308.697 501.122 309.729 499.795 309.729 498.173C309.729 496.55 308.697 495.223 307.517 495.223C306.337 495.223 305.304 496.55 305.304 498.173Z" fill="black" />
|
||||
<path d="M323.003 498.173C323.003 499.795 324.036 501.122 325.216 501.122C326.395 501.122 327.428 499.795 327.428 498.173C327.428 496.55 326.395 495.223 325.216 495.223C324.036 495.223 323.003 496.55 323.003 498.173Z" fill="black" />
|
||||
<path d="M378.312 496.698C377.722 497.73 377.87 499.058 378.754 499.943C380.819 502.007 385.392 500.238 384.654 497.73C383.769 495.37 379.639 494.633 378.312 496.698Z" fill="black" />
|
||||
<path d="M415.479 496.108C413.415 498.32 414.595 501.122 417.544 501.122C419.609 501.122 420.347 500.385 420.052 498.615C419.609 495.96 417.102 494.633 415.479 496.108Z" fill="black" />
|
||||
<path d="M395.716 498.468C396.011 499.5 397.191 500.385 398.223 500.385C399.256 500.385 400.435 499.5 400.878 498.468C401.173 497.435 400.14 496.698 398.223 496.698C396.306 496.698 395.273 497.435 395.716 498.468Z" fill="black" />
|
||||
<path d="M433.621 498.91C433.621 500.09 434.653 501.122 435.833 501.122C437.013 501.122 438.045 500.09 438.045 498.91C438.045 497.73 437.013 496.698 435.833 496.698C434.653 496.698 433.621 497.73 433.621 498.91Z" fill="black" />
|
||||
<path d="M279.936 505.547C279.051 508.202 284.656 518.969 286.278 517.936C288.195 516.756 287.901 508.497 285.983 508.497C284.951 508.497 283.476 507.317 282.443 505.99C281.116 504.22 280.526 504.072 279.936 505.547Z" fill="black" />
|
||||
<path d="M346.012 505.842C343.504 513.807 344.684 524.131 348.371 528.261C348.961 529.146 349.551 534.603 349.551 540.355C349.551 546.254 350.289 552.891 351.174 555.251C352.059 557.464 353.239 565.281 353.976 572.655C355.893 592.714 358.696 607.315 360.908 607.315C363.563 607.315 363.415 591.681 360.613 583.717C359.433 580.325 358.401 574.425 358.401 570.59C358.401 566.903 357.663 561.446 356.778 558.644C355.009 553.481 352.649 535.045 352.501 527.376C352.501 525.163 351.911 523.099 351.026 522.509C348.371 520.886 349.256 510.414 352.206 509.234C354.566 508.349 354.566 508.202 351.911 506.137C348.371 503.63 346.749 503.482 346.012 505.842Z" fill="black" />
|
||||
<path d="M363.858 511.004C363.268 511.447 362.826 513.512 362.826 515.429C362.826 520.296 366.365 518.969 366.955 513.954C367.398 510.119 365.923 508.792 363.858 511.004Z" fill="black" />
|
||||
<path d="M199.997 518.231C198.817 521.476 200.587 524.131 202.209 521.624C203.831 518.969 203.831 515.871 202.209 515.871C201.471 515.871 200.439 516.904 199.997 518.231Z" fill="black" />
|
||||
<path d="M248.816 516.904C248.226 517.346 247.783 519.854 247.783 522.361C247.783 524.868 246.751 528.261 245.571 529.736C244.391 531.358 243.359 534.16 243.359 536.078C243.359 537.995 242.474 541.387 241.294 543.6C240.261 545.812 238.934 550.384 238.344 553.924C237.459 559.529 237.754 560.266 239.966 559.823C241.441 559.529 242.916 557.464 243.359 554.956C243.949 552.449 245.129 549.057 246.013 547.287C247.046 545.517 247.783 541.83 247.783 539.175C247.931 536.52 248.816 532.685 249.996 530.62C253.535 524.278 252.651 513.069 248.816 516.904Z" fill="black" />
|
||||
<path d="M367.398 526.491C367.545 528.408 367.988 531.8 368.578 534.308L369.61 538.732L370.643 532.833C372.117 524.426 371.97 523.246 369.463 523.246C368.135 523.246 367.25 524.573 367.398 526.491Z" fill="black" />
|
||||
<path d="M195.572 526.933C194.392 530.325 194.392 532.095 196.014 532.095C197.932 532.095 199.407 527.376 197.932 525.901C197.047 525.016 196.162 525.458 195.572 526.933Z" fill="black" />
|
||||
<path d="M191.737 536.815C191.737 539.617 192.032 539.765 193.212 537.995C195.129 535.193 195.129 533.57 193.212 533.57C192.475 533.57 191.737 535.045 191.737 536.815Z" fill="black" />
|
||||
<path d="M372.412 547.287C371.232 551.859 372.412 556.284 374.625 554.956C376.395 553.776 376.542 548.319 374.772 545.517C373.592 543.747 373.15 544.19 372.412 547.287Z" fill="black" />
|
||||
<path d="M376.985 562.921C376.395 563.953 376.247 567.198 376.837 570.148C377.87 575.162 378.017 575.31 379.344 572.36C381.114 568.673 378.902 559.823 376.985 562.921Z" fill="black" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 57 KiB |
@@ -0,0 +1,84 @@
|
||||
<svg width="451" height="363" viewBox="0 0 451 363" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M144.004 0.852005C142.529 1.14699 141.349 2.17942 141.349 2.91687C141.349 4.68675 174.829 4.68675 175.862 2.91687C176.304 2.17942 175.714 1.14699 174.387 0.704515C171.732 -0.327915 147.838 -0.180425 144.004 0.852005Z" fill="black" />
|
||||
<path d="M127.337 5.86681C126.747 6.60426 124.83 7.93167 123.208 8.81661C114.358 12.6514 97.1019 26.0729 97.1019 29.1702C97.1019 29.9077 96.0694 31.2351 94.8895 32.2675C93.7096 33.3 92.6772 35.2173 92.6772 36.6922C92.6772 38.1671 91.6447 39.642 90.4648 40.232C89.2849 40.6745 88.2525 42.2968 88.2525 43.9192C88.2525 45.5416 87.515 47.0165 86.6301 47.3115C80.878 49.2289 77.0432 81.2342 81.4679 90.0836C82.7953 92.5909 83.8278 96.7207 83.8278 99.3755C83.8278 102.915 84.7127 104.39 87.515 105.718C90.6123 107.192 91.4972 107.045 92.2347 105.128C92.8246 103.653 92.0872 102.178 90.7598 101.293C89.2849 100.555 88.2525 98.4905 88.2525 96.7207C88.2525 94.9508 87.22 92.1485 86.0401 90.6736C82.9428 86.5439 83.0903 60.7331 86.0401 56.7509C87.22 55.1285 88.2525 52.7686 88.2525 51.2937C88.2525 49.9663 89.2849 48.049 90.4648 47.0165C91.6447 45.9841 92.6772 44.0667 92.6772 42.7393C92.6772 37.2822 118.34 13.0938 124.093 13.0938C125.272 13.0938 127.042 12.0614 128.075 10.8815C129.107 9.70155 131.467 8.66912 133.237 8.66912C135.007 8.66912 137.367 7.63669 138.399 6.45677C140.021 4.5394 139.432 4.24442 134.269 4.24442C130.877 4.24442 127.78 4.98187 127.337 5.86681Z" fill="black" />
|
||||
<path d="M180.434 5.71923C179.844 6.60417 181.466 7.6366 183.973 8.07907C186.481 8.66903 189.283 9.99644 190.168 11.0289C191.053 12.2088 192.823 13.0937 194.003 13.0937C196.658 13.0937 212.144 29.0227 212.144 31.825C212.144 32.8574 213.177 34.1848 214.356 34.6273C215.536 35.2172 216.569 37.1346 216.569 38.9045C216.569 42.2968 218.929 45.0991 220.404 43.6242C222.026 42.0018 218.929 32.1199 215.684 28.5802C213.766 26.5153 212.144 24.4505 212.144 24.008C212.144 22.2381 195.625 8.66903 193.265 8.66903C192.528 8.66903 191.053 7.6366 190.021 6.45668C187.956 3.94935 181.761 3.50688 180.434 5.71923Z" fill="black" />
|
||||
<path d="M300.933 14.8636C301.818 17.5184 305.063 17.9609 305.063 15.4536C305.063 14.1262 304.03 13.0937 302.703 13.0937C301.376 13.0937 300.638 13.8312 300.933 14.8636Z" fill="black" />
|
||||
<path d="M362.879 24.3029C363.026 25.6303 364.206 27.1052 365.239 27.5477C366.566 27.9901 367.156 27.2527 366.714 25.4828C366.566 24.1554 365.386 22.6805 364.354 22.238C363.026 21.7956 362.436 22.533 362.879 24.3029Z" fill="black" />
|
||||
<path d="M367.304 31.6774C367.451 33.0048 368.631 34.4797 369.663 34.9222C370.991 35.3647 371.581 34.6272 371.138 32.8573C370.991 31.5299 369.811 30.055 368.779 29.6125C367.451 29.1701 366.861 29.9075 367.304 31.6774Z" fill="black" />
|
||||
<path d="M372.318 40.6744C371.876 41.1169 371.433 48.0489 371.433 56.0134C371.433 68.55 371.876 70.7624 374.088 71.9423C376.448 73.2697 376.743 72.8272 376.153 68.9925C375.711 66.1902 376.448 63.5354 377.923 61.913C380.135 59.5531 380.135 59.1107 378.07 57.3408C376.595 56.1609 375.858 52.9161 375.858 48.3439C375.858 41.7068 374.531 38.6096 372.318 40.6744Z" fill="black" />
|
||||
<path d="M220.993 49.5237C220.993 50.9986 221.731 53.211 222.468 54.3909C223.501 56.0133 223.943 55.4233 223.943 51.7361C223.943 49.2287 223.353 47.0164 222.468 47.0164C221.731 47.0164 220.993 48.1963 220.993 49.5237Z" fill="black" />
|
||||
<path d="M38.8432 81.0865C37.3683 83.2989 38.9907 84.4788 41.793 83.2989C43.1204 82.8564 43.7104 81.824 43.2679 80.9391C42.088 79.0217 40.0232 79.0217 38.8432 81.0865Z" fill="black" />
|
||||
<path d="M393.262 80.3493C392.819 80.7917 395.622 84.479 399.604 88.4612C403.586 92.5909 406.831 97.0156 406.831 98.343C406.831 99.8179 408.011 102.62 409.338 104.538C413.173 109.995 413.173 144.802 409.486 150.702C408.011 152.767 406.831 156.159 406.831 158.077C406.831 160.141 405.799 162.501 404.619 163.534C403.439 164.566 402.406 166.041 402.406 166.778C402.406 170.171 387.215 188.017 382.495 190.229C380.873 190.967 378.513 192.147 377.333 192.884C372.171 195.981 363.616 198.931 360.372 198.931C355.504 198.931 355.799 200.996 361.699 208.223C370.253 218.99 372.318 230.789 366.861 237.869L363.911 241.703L371.728 245.243C376.006 247.308 381.61 250.995 384.265 253.798C388.395 258.075 388.985 259.845 389.28 267.367C389.575 274.004 389.132 276.363 387.215 277.838C383.97 280.198 383.97 280.493 389.132 286.245C392.819 290.228 393.557 292.292 393.557 298.782C393.557 307.779 389.87 315.891 383.233 321.348C378.218 325.478 377.628 327.248 381.315 327.248C382.79 327.248 386.92 324.003 390.607 320.021C398.277 311.761 402.259 301.437 399.604 296.57C397.244 292.145 397.539 284.918 400.194 283.885C401.374 283.443 402.406 281.673 402.406 280.051C402.406 277.691 401.374 277.101 397.834 277.101C393.557 277.101 393.409 276.806 393.852 271.644C394.147 267.219 395.032 266.039 397.982 265.302C403.439 263.974 403.144 257.337 397.687 256.747C394.294 256.305 393.557 255.567 393.557 252.175C393.557 248.635 392.819 247.75 389.132 247.013C385.15 246.276 384.707 245.538 384.707 240.966C384.707 236.246 385.002 235.804 389.132 235.804C393.262 235.804 393.557 236.246 393.557 240.966C393.557 245.686 393.852 246.128 397.834 246.128C400.784 246.128 402.554 245.243 403.144 243.326C404.471 238.754 402.406 235.804 397.687 235.804C393.704 235.804 393.409 235.509 393.852 231.084C394.294 226.954 395.032 226.069 398.719 225.479C401.964 224.889 403.291 223.857 403.586 221.202C404.176 217.22 401.816 215.155 396.802 215.155C393.852 215.155 393.409 214.418 393.852 211.025C394.147 207.928 395.179 206.896 398.277 206.601C405.356 205.716 405.651 194.507 398.719 194.507C391.787 194.507 392.377 188.754 399.456 186.1C402.849 184.772 403.881 183.445 403.881 180.642C403.881 177.545 404.619 176.808 407.568 176.808C412.288 176.808 414.943 172.088 412.583 168.253C411.551 166.631 411.256 164.271 411.846 162.796C412.731 160.289 413.026 160.289 415.533 162.649C418.483 165.304 423.055 164.419 423.055 161.174C423.055 159.994 421.432 158.371 419.368 157.634C414.648 155.864 414.353 148.932 418.63 145.097C422.17 142 422.465 138.018 419.368 135.511C416.565 133.151 416.565 129.021 419.368 124.596C421.432 121.499 421.432 120.909 418.63 117.369C417.008 115.304 415.68 111.765 415.68 109.552C415.68 107.34 414.648 104.39 413.468 103.063C412.141 101.588 410.961 97.9006 410.813 94.8033C410.518 89.7886 409.928 89.0512 406.831 88.6087C404.176 88.3137 402.554 86.8388 401.374 83.889C399.899 79.7593 395.622 77.9894 393.262 80.3493ZM393.262 200.111C393.704 204.683 393.557 204.978 389.575 204.536C386.477 204.241 385.297 203.208 385.002 200.554C384.413 196.866 386.92 194.212 390.607 194.949C391.935 195.096 392.967 197.161 393.262 200.111ZM382.495 210.73C382.495 213.385 381.61 214.565 379.103 214.86C375.563 215.45 373.498 212.205 374.973 208.371C375.416 207.043 377.186 206.306 379.103 206.601C381.61 206.896 382.495 208.076 382.495 210.73ZM393.557 221.055C393.557 225.037 393.114 225.479 389.132 225.479C385.15 225.479 384.707 225.037 384.707 221.055C384.707 217.073 385.15 216.63 389.132 216.63C393.114 216.63 393.557 217.073 393.557 221.055ZM380.873 226.364C382.2 226.807 383.233 228.872 383.233 230.789C383.233 233.591 382.495 234.329 379.545 234.329C375.121 234.329 373.203 230.789 375.858 227.692C377.923 225.184 377.775 225.184 380.873 226.364Z" fill="black" />
|
||||
<path d="M30.2889 84.9213C28.3716 86.6912 29.2565 88.3136 32.2063 88.3136C33.8287 88.3136 35.1561 87.2812 35.1561 86.1013C35.1561 83.8889 32.0588 83.004 30.2889 84.9213Z" fill="black" />
|
||||
<path d="M47.9877 89.1987C45.9228 91.2636 46.9552 92.886 49.905 92.4435C51.6749 92.1485 53.0023 91.2636 53.0023 90.5261C53.0023 88.7562 49.4626 87.8713 47.9877 89.1987Z" fill="black" />
|
||||
<path d="M37.221 94.9506C36.6311 96.1305 35.3036 97.1629 34.1237 97.1629C32.0589 97.1629 21.8821 107.192 21.8821 109.257C21.8821 109.995 20.8496 110.437 19.6697 110.437C18.4898 110.437 17.4574 111.027 17.4574 111.617C17.4574 112.354 15.54 114.862 13.0327 117.222C10.6728 119.581 8.60795 122.531 8.60795 123.859C8.60795 125.186 7.57552 126.661 6.3956 127.251C3.74078 128.283 3.29831 134.625 6.10062 133.74C6.98556 133.298 8.16548 131.823 8.46046 130.348C8.90293 127.693 10.0829 125.923 14.95 120.614C16.2774 119.139 17.4574 117.517 17.4574 117.074C17.4574 115.599 32.0589 101.588 33.3863 101.588C34.2712 101.588 35.1562 100.703 35.5986 99.5228C36.0411 98.3428 37.9585 97.1629 39.7284 96.8679C41.4982 96.573 43.2681 95.5405 43.7106 94.5081C44.5955 91.8533 38.2534 92.2957 37.221 94.9506Z" fill="black" />
|
||||
<path d="M312.732 98.7854C308.16 100.555 308.308 103.505 316.272 148.047C317.452 154.537 318.337 161.911 318.337 164.566C318.337 169.286 321.729 184.33 323.499 187.132C323.942 188.017 329.399 188.607 335.298 188.607C348.425 188.607 349.605 187.427 347.687 175.333C347.097 170.908 345.77 152.029 345.033 133.593C344.148 115.157 343.115 99.3754 342.673 98.6379C341.493 96.7205 317.305 96.868 312.732 98.7854Z" fill="black" />
|
||||
<path d="M263.766 103.948C257.719 104.98 252.556 105.865 252.409 105.865C251.376 106.16 252.114 115.157 253.589 118.697C254.916 122.531 258.161 156.307 258.456 170.171L258.603 176.07L250.491 176.955C246.067 177.398 240.61 178.283 238.545 178.872C235.3 179.757 234.415 179.315 232.94 175.628C231.908 173.12 230.728 159.699 230.138 143.77C229.4 126.956 228.368 115.747 227.336 114.714C226.303 113.682 220.846 113.534 209.047 114.419L192.233 115.599L192.38 122.236C192.675 136.985 195.92 183.74 196.658 186.1C197.248 187.279 198.133 199.521 198.575 213.238L199.607 238.016L214.946 237.721C224.533 237.574 231.023 236.689 232.35 235.509C234.12 234.034 234.415 230.199 234.12 214.565C233.53 193.032 232.793 194.359 246.657 191.704C257.719 189.639 260.373 190.967 261.258 199.226C261.553 202.766 262.733 211.173 263.913 217.957L265.83 230.199L271.878 229.462C279.989 228.429 299.458 225.184 301.965 224.594C303.588 224.004 303.735 222.087 302.998 215.45C302.408 210.878 301.376 198.046 300.638 187.132C299.901 176.218 298.573 163.681 297.836 159.256C296.951 154.979 296.213 148.342 296.213 144.507C296.213 140.82 295.181 133.003 294.001 127.398C292.821 121.646 291.789 114.419 291.789 111.027C291.789 107.782 291.051 104.39 290.019 103.358C287.806 101.145 278.22 101.293 263.766 103.948Z" fill="black" />
|
||||
<path d="M94.152 121.351C92.0871 122.384 88.1049 124.449 85.3026 125.924C77.4856 129.906 66.7188 142.147 61.9991 152.324C51.0849 175.775 53.1497 200.406 67.8987 220.612C77.6331 234.034 88.8423 241.408 106.099 245.538C115.243 247.75 116.865 247.75 128.075 245.538C148.723 241.261 160.522 231.821 169.519 212.205C176.009 198.194 178.221 180.2 174.682 169.876C173.354 166.041 172.322 161.911 172.322 160.436C172.322 156.896 165.685 143.18 160.522 136.1C157.425 131.823 147.691 124.891 141.496 122.384C133.974 119.287 99.1666 118.549 94.152 121.351ZM126.157 157.929C137.957 164.861 141.349 170.613 141.349 183.74C141.349 192.147 140.759 194.801 137.662 199.374C128.517 212.795 112.883 216.188 101.379 207.338C95.0369 202.471 94.7419 202.028 90.6122 192.589C85.745 181.232 89.4323 167.663 99.6091 159.404C106.836 153.504 117.455 152.914 126.157 157.929Z" fill="black" />
|
||||
<path d="M426.005 130.348C426.005 131.676 427.332 132.561 429.692 132.561C432.052 132.561 433.379 131.676 433.379 130.348C433.379 129.021 432.052 128.136 429.692 128.136C427.332 128.136 426.005 129.021 426.005 130.348Z" fill="black" />
|
||||
<path d="M0.348338 139.198C-0.831583 142.295 1.23328 145.245 2.85567 142.737C4.62555 139.935 4.47806 136.985 2.70818 136.985C1.82324 136.985 0.790808 138.018 0.348338 139.198Z" fill="black" />
|
||||
<path d="M426.89 148.195C423.645 151.439 424.677 153.947 429.102 153.947C431.904 153.947 432.642 153.209 432.642 150.407C432.642 145.982 430.134 144.95 426.89 148.195Z" fill="black" />
|
||||
<path d="M437.066 160.436C436.624 161.174 436.919 162.206 437.804 162.796C439.574 163.829 442.229 162.501 442.229 160.436C442.229 158.666 438.246 158.666 437.066 160.436Z" fill="black" />
|
||||
<path d="M427.037 167.663C425.267 167.958 424.53 169.286 424.825 171.351C425.415 175.923 432.494 175.923 433.084 171.498C433.527 168.106 431.314 166.631 427.037 167.663Z" fill="black" />
|
||||
<path d="M446.653 170.023C446.653 171.35 447.686 172.383 449.013 172.383C451.52 172.383 451.078 169.138 448.423 168.253C447.391 167.958 446.653 168.696 446.653 170.023Z" fill="black" />
|
||||
<path d="M415.68 181.232C415.68 185.214 416.123 185.804 419.073 185.362C421.432 185.067 422.317 183.887 422.317 181.232C422.317 178.577 421.432 177.398 419.073 177.103C416.123 176.66 415.68 177.25 415.68 181.232Z" fill="black" />
|
||||
<path d="M438.099 178.578C436.181 180.495 437.509 184.182 440.164 184.182C442.376 184.182 442.966 179.02 441.049 177.693C440.311 177.25 438.984 177.693 438.099 178.578Z" fill="black" />
|
||||
<path d="M405.651 187.427C402.259 190.819 403.586 194.506 408.158 194.506C413.468 194.506 414.943 192.589 412.731 188.607C410.961 185.214 408.306 184.772 405.651 187.427Z" fill="black" />
|
||||
<path d="M425.12 189.197C423.792 192.441 425.71 194.506 429.839 194.506C432.937 194.506 433.527 193.916 433.084 191.114C432.494 187.279 426.447 185.657 425.12 189.197Z" fill="black" />
|
||||
<path d="M415.975 199.963C416.565 204.683 418.483 205.568 422.317 202.766C424.677 200.996 424.825 200.406 423.202 198.341C422.17 197.014 419.958 195.981 418.335 195.981C416.123 195.981 415.533 196.866 415.975 199.963Z" fill="black" />
|
||||
<path d="M436.919 199.079C435.444 201.586 438.836 204.388 440.901 202.324C442.671 200.554 441.491 197.456 439.279 197.456C438.541 197.456 437.509 198.194 436.919 199.079Z" fill="black" />
|
||||
<path d="M330.136 202.914C322.172 206.158 320.254 218.99 326.744 225.774C334.856 234.181 349.31 229.609 349.31 218.4C349.31 207.338 339.428 199.374 330.136 202.914Z" fill="black" />
|
||||
<path d="M405.651 206.601C401.816 210.435 405.356 217.662 410.223 215.745C414.058 214.27 414.943 209.108 411.846 206.748C408.601 204.388 407.863 204.388 405.651 206.601Z" fill="black" />
|
||||
<path d="M426.005 207.78C423.055 209.55 425.857 214.417 429.839 214.417C433.084 214.417 434.559 209.403 431.609 207.633C429.102 206.011 428.659 206.011 426.005 207.78Z" fill="black" />
|
||||
<path d="M446.653 209.993C446.653 211.32 447.391 212.205 448.276 211.91C449.161 211.615 449.898 210.731 449.898 209.993C449.898 209.256 449.161 208.371 448.276 208.076C447.391 207.781 446.653 208.666 446.653 209.993Z" fill="black" />
|
||||
<path d="M415.68 221.055C415.68 225.037 416.123 225.627 419.073 225.184C421.432 224.889 422.317 223.709 422.317 221.055C422.317 218.4 421.432 217.22 419.073 216.925C416.123 216.482 415.68 217.072 415.68 221.055Z" fill="black" />
|
||||
<path d="M437.804 221.055C437.804 224.594 440.016 225.037 441.344 221.645C442.671 218.4 442.523 218.105 440.016 218.105C438.836 218.105 437.804 219.432 437.804 221.055Z" fill="black" />
|
||||
<path d="M405.061 229.314C403.586 232.411 403.734 233.296 405.504 234.476C408.306 236.246 408.748 236.099 411.846 233.886C414.058 232.264 414.353 231.379 412.878 228.724C410.518 224.447 407.126 224.594 405.061 229.314Z" fill="black" />
|
||||
<path d="M425.562 227.839C424.972 228.429 424.53 230.199 424.53 231.674C424.53 233.591 425.71 234.329 429.102 234.329C432.937 234.329 433.527 233.886 433.084 230.937C432.642 227.692 427.775 225.627 425.562 227.839Z" fill="black" />
|
||||
<path d="M446.948 231.231C447.538 233.149 449.603 233.591 449.751 231.821C449.751 231.231 449.013 230.494 448.128 230.051C447.096 229.756 446.506 230.346 446.948 231.231Z" fill="black" />
|
||||
<path d="M416.27 239.491C414.5 244.063 419.663 246.571 422.907 242.588C424.382 240.818 424.087 240.081 421.727 238.901C417.893 236.836 417.155 236.836 416.27 239.491Z" fill="black" />
|
||||
<path d="M437.361 239.638C436.034 241.113 437.656 243.178 440.164 243.178C441.344 243.178 442.229 242.146 442.229 240.966C442.229 238.754 439.131 237.869 437.361 239.638Z" fill="black" />
|
||||
<path d="M350.342 244.063C348.72 245.685 350.785 247.603 352.997 246.718C354.177 246.128 354.767 245.243 354.324 244.505C353.44 242.883 351.67 242.736 350.342 244.063Z" fill="black" />
|
||||
<path d="M405.651 251.585C405.946 254.535 406.831 255.715 409.043 255.715C413.616 255.715 413.173 248.488 408.601 247.898C405.651 247.455 405.209 247.898 405.651 251.585Z" fill="black" />
|
||||
<path d="M425.267 249.078C423.792 251.585 428.217 255.567 431.167 254.387C432.494 253.945 433.379 252.323 433.084 250.995C432.494 247.898 426.742 246.571 425.267 249.078Z" fill="black" />
|
||||
<path d="M335.446 253.208C335.151 253.798 335.298 255.272 336.036 256.452C337.068 258.075 338.101 258.222 340.018 257.042C344 254.535 344 252.028 339.87 252.028C337.953 252.028 335.888 252.618 335.446 253.208Z" fill="black" />
|
||||
<path d="M415.975 260.434C416.713 264.122 420.105 264.564 420.105 261.024C420.105 259.255 419.22 257.927 417.745 257.927C416.418 257.927 415.68 258.96 415.975 260.434Z" fill="black" />
|
||||
<path d="M37.5159 264.122C41.4981 268.989 46.9552 274.151 47.9877 274.151C48.4301 274.151 51.2325 276.068 54.3298 278.576C57.427 280.936 60.9668 283 62.1467 283C63.3266 283 65.0965 284.033 66.129 285.213C67.1614 286.393 69.5212 287.425 71.5861 287.425C73.5035 287.425 77.0432 288.458 79.4031 289.637C82.2054 291.112 87.3675 291.85 94.1521 291.702C100.494 291.555 105.361 292.145 106.689 293.325C108.459 294.8 109.196 294.505 110.523 291.407C111.408 289.342 113.621 287.278 115.391 286.835C119.373 285.803 128.075 277.838 128.075 275.183C128.075 271.201 126.305 271.644 118.045 276.953C108.311 283.443 96.0694 286.688 86.6301 285.36C69.8162 283.148 66.7189 282.558 65.6865 280.641C65.0965 279.461 62.8842 278.576 61.1143 278.576C59.1969 278.576 57.2795 277.543 56.6896 276.363C56.2471 275.183 55.2147 274.151 54.3298 274.151C52.4124 274.151 50.0525 272.381 43.268 265.597C37.6634 260.139 33.0912 258.96 37.5159 264.122Z" fill="black" />
|
||||
<path d="M226.303 265.154C225.713 266.039 225.861 267.957 226.746 269.284C228.073 271.791 228.221 271.791 229.253 269.284C230.433 266.187 227.778 262.647 226.303 265.154Z" fill="black" />
|
||||
<path d="M406.241 268.989C405.061 272.086 406.388 274.151 409.191 274.151C410.666 274.151 411.256 272.971 410.961 270.906C410.371 266.924 407.568 265.892 406.241 268.989Z" fill="black" />
|
||||
<path d="M415.68 280.788C415.68 281.968 417.008 283 418.63 283C420.252 283 421.58 281.968 421.58 280.788C421.58 279.608 420.252 278.576 418.63 278.576C417.008 278.576 415.68 279.608 415.68 280.788Z" fill="black" />
|
||||
<path d="M126.895 287.278C123.65 289.343 125.567 293.767 130.14 294.947C132.647 295.685 134.712 295.98 135.007 295.832C135.154 295.537 134.564 293.177 133.679 290.67C131.762 285.803 130.287 285.065 126.895 287.278Z" fill="black" />
|
||||
<path d="M234.268 290.965C234.268 293.03 233.678 295.095 232.793 295.537C232.055 295.98 231.318 298.045 231.318 300.109C231.318 302.027 230.58 303.649 229.695 303.649C228.81 303.649 225.418 306.009 222.321 308.811C219.076 311.614 215.536 313.973 214.504 313.973C209.637 313.973 210.374 319.431 215.979 324.888C219.076 328.133 220.551 327.838 223.353 323.56C225.418 320.463 225.418 319.431 223.796 317.513C222.173 315.596 222.468 315.448 226.303 316.186C233.825 317.808 234.268 318.251 234.268 322.085C234.268 324.15 235.3 326.215 236.48 326.658C239.577 327.985 243.117 324.593 243.117 320.463C243.117 314.711 249.459 315.743 253.294 322.085C254.916 324.888 257.129 327.248 258.308 327.248C260.963 327.248 263.766 324.15 263.766 321.2C263.766 318.546 266.568 316.923 271.14 316.923C273.205 316.923 274.09 318.251 274.385 321.643C274.68 325.773 275.417 326.51 278.515 326.51C281.612 326.51 282.349 325.773 282.644 321.643C283.087 317.366 282.792 316.923 279.252 316.923C276.155 316.923 275.417 316.333 276.155 314.563C276.597 313.236 277.04 311.909 277.04 311.614C277.04 309.549 281.464 311.614 282.202 313.973C282.939 316.186 284.414 316.923 288.249 316.923C292.379 316.923 293.559 316.186 294.739 313.089C296.656 308.221 302.555 308.664 303.293 313.531C303.735 316.481 303.145 316.923 299.311 316.923C295.771 316.923 294.739 317.513 294.739 319.726C294.739 328.28 299.606 329.755 302.408 322.085C303.883 317.661 304.915 316.923 308.013 317.218C310.372 317.513 312.437 319.136 313.617 321.643C315.977 326.805 320.254 326.953 323.499 321.938C325.416 318.841 326.891 318.251 330.284 318.693C333.676 319.283 334.561 318.841 334.266 317.071C333.823 315.153 331.464 314.563 323.204 314.268C313.027 313.826 312.437 313.678 306.685 307.779C300.786 302.027 300.491 301.879 295.918 303.797C292.821 304.977 285.004 305.714 274.827 305.862C259.783 305.862 258.161 305.567 252.409 301.879C249.164 299.667 244.002 295.537 241.2 292.587C235.3 286.393 234.268 286.245 234.268 290.965ZM242.38 311.761C242.38 315.448 241.79 316.186 238.692 316.186C235.595 316.186 234.858 315.448 234.563 311.319C234.12 306.746 234.268 306.451 238.25 306.894C241.79 307.336 242.38 308.074 242.38 311.761ZM262.291 312.351C264.356 316.333 264.356 316.333 258.603 316.481C254.621 316.628 253.441 316.038 253.441 314.121C253.441 308.811 259.636 307.631 262.291 312.351Z" fill="black" />
|
||||
<path d="M406.831 290.375C406.831 291.997 407.863 293.325 409.043 293.325C410.223 293.325 411.256 291.997 411.256 290.375C411.256 288.753 410.223 287.425 409.043 287.425C407.863 287.425 406.831 288.753 406.831 290.375Z" fill="black" />
|
||||
<path d="M115.243 300.109C114.063 303.059 117.161 306.304 119.225 304.239C120.995 302.469 119.668 297.749 117.603 297.749C116.866 297.749 115.833 298.782 115.243 300.109Z" fill="black" />
|
||||
<path d="M135.449 301.584C135.449 302.912 136.629 304.239 138.104 304.682C139.579 304.977 141.939 307.336 143.414 309.696C144.741 312.056 147.839 315.006 150.198 316.186C152.558 317.366 154.918 319.725 155.36 321.495C156.245 324.15 157.13 324.445 164.21 323.56C170.109 322.823 172.912 323.118 175.272 324.74C178.369 326.952 182.646 326.658 182.646 324.445C182.646 323.708 178.516 322.085 173.502 320.61C159.785 316.481 144.299 307.631 141.349 302.174C139.432 298.634 135.449 298.192 135.449 301.584Z" fill="black" />
|
||||
<path d="M127.042 309.106C123.503 310.434 124.978 313.973 128.96 313.973C131.615 313.973 132.647 313.236 132.205 311.466C131.762 308.811 129.845 307.926 127.042 309.106Z" fill="black" />
|
||||
<path d="M105.951 311.761C105.951 312.941 106.984 313.973 108.164 313.973C109.344 313.973 110.376 312.941 110.376 311.761C110.376 310.581 109.344 309.549 108.164 309.549C106.984 309.549 105.951 310.581 105.951 311.761Z" fill="black" />
|
||||
<path d="M115.686 319.578C115.243 320.463 115.538 321.643 116.423 322.085C118.635 323.56 120.11 321.643 118.193 319.726C117.16 318.693 116.276 318.693 115.686 319.578Z" fill="black" />
|
||||
<path d="M136.924 321.348C136.924 322.97 137.957 324.298 139.137 324.298C140.316 324.298 141.349 322.97 141.349 321.348C141.349 319.725 140.316 318.398 139.137 318.398C137.957 318.398 136.924 319.725 136.924 321.348Z" fill="black" />
|
||||
<path d="M196.215 320.758C192.528 323.265 192.085 325.183 194.888 326.215C197.69 327.247 206.244 322.823 206.244 320.463C206.244 317.661 200.345 317.808 196.215 320.758Z" fill="black" />
|
||||
<path d="M185.006 324.15C184.563 324.888 184.416 326.51 184.858 327.837C185.301 329.165 185.891 331.23 186.333 332.557C187.513 336.392 191.791 331.967 191.201 327.395C190.758 323.56 186.628 321.348 185.006 324.15Z" fill="black" />
|
||||
<path d="M336.331 324.15C336.921 326.068 338.986 326.51 339.133 324.74C339.133 324.15 338.396 323.413 337.511 322.97C336.478 322.675 335.888 323.265 336.331 324.15Z" fill="black" />
|
||||
<path d="M205.507 328.722C204.327 330.64 207.424 334.622 210.079 334.622C211.259 334.622 212.144 333.147 212.144 330.935C212.144 328.28 211.407 327.247 209.342 327.247C207.719 327.247 205.949 327.985 205.507 328.722Z" fill="black" />
|
||||
<path d="M225.418 330.935C225.418 335.212 230.433 336.244 231.908 332.262C233.088 329.165 231.76 327.247 228.221 327.247C226.156 327.247 225.418 328.28 225.418 330.935Z" fill="black" />
|
||||
<path d="M244.002 328.575C242.38 331.23 244.887 334.622 248.427 334.622C251.229 334.622 251.966 333.884 251.966 330.935C251.966 327.985 251.229 327.247 248.427 327.247C246.362 327.247 244.444 327.837 244.002 328.575Z" fill="black" />
|
||||
<path d="M265.24 330.787C265.24 335.654 267.01 336.539 270.845 333.737C275.565 330.492 274.975 327.248 269.665 327.248C265.978 327.248 265.24 327.838 265.24 330.787Z" fill="black" />
|
||||
<path d="M285.447 328.132C284.119 329.607 287.217 334.622 289.429 334.622C291.494 334.622 293.559 329.46 292.084 328.132C291.051 326.952 286.627 327.1 285.447 328.132Z" fill="black" />
|
||||
<path d="M305.358 330.492C305.653 332.41 306.833 333.885 308.013 333.885C309.193 333.885 310.372 332.41 310.667 330.492C311.11 327.985 310.372 327.248 308.013 327.248C305.653 327.248 304.915 327.985 305.358 330.492Z" fill="black" />
|
||||
<path d="M325.121 328.575C324.089 330.197 329.251 334.917 330.579 333.59C331.169 333.147 331.611 331.377 331.611 329.902C331.611 327.1 326.596 326.068 325.121 328.575Z" fill="black" />
|
||||
<path d="M344.885 329.607C344.885 331.525 372.023 331.525 373.793 329.755C375.121 328.575 375.563 328.575 358.602 328.28C349.162 327.985 344.885 328.427 344.885 329.607Z" fill="black" />
|
||||
<path d="M217.159 338.162C215.979 341.259 217.601 345.389 219.519 344.209C220.256 343.766 220.994 341.996 220.994 340.522C220.994 336.834 218.339 335.212 217.159 338.162Z" fill="black" />
|
||||
<path d="M256.981 338.162C255.801 341.259 257.424 345.389 259.341 344.209C260.078 343.766 260.816 341.996 260.816 340.522C260.816 336.834 258.161 335.212 256.981 338.162Z" fill="black" />
|
||||
<path d="M235.005 339.047C234.415 340.079 234.563 341.407 235.448 342.292C237.512 344.356 242.085 342.586 241.347 340.079C240.462 337.719 236.332 336.982 235.005 339.047Z" fill="black" />
|
||||
<path d="M275.565 340.374C275.565 343.619 279.842 345.684 281.907 343.619C284.119 341.407 281.907 337.572 278.515 337.572C276.597 337.572 275.565 338.604 275.565 340.374Z" fill="black" />
|
||||
<path d="M296.213 340.522C296.213 342.144 297.246 343.471 298.426 343.471C299.606 343.471 300.638 342.144 300.638 340.522C300.638 338.899 299.606 337.572 298.426 337.572C297.246 337.572 296.213 338.899 296.213 340.522Z" fill="black" />
|
||||
<path d="M186.481 348.929C184.711 350.698 185.596 353.796 187.808 353.796C188.988 353.796 190.021 352.911 190.021 351.731C190.021 349.224 187.956 347.601 186.481 348.929Z" fill="black" />
|
||||
<path d="M206.982 349.224C205.802 351.436 207.424 353.796 210.079 353.796C211.407 353.796 212.144 352.763 211.849 351.288C211.259 348.339 208.309 347.159 206.982 349.224Z" fill="black" />
|
||||
<path d="M225.861 350.256C224.828 353.058 226.156 354.386 229.105 353.353C231.908 352.173 231.908 349.961 229.105 348.781C227.778 348.339 226.451 348.929 225.861 350.256Z" fill="black" />
|
||||
<path d="M246.509 348.929C242.97 350.256 244.444 353.796 248.427 353.796C251.081 353.796 252.114 353.058 251.671 351.288C251.229 348.634 249.312 347.749 246.509 348.929Z" fill="black" />
|
||||
<path d="M266.273 348.929C264.208 350.846 265.241 353.796 267.895 353.796C271.73 353.796 272.91 352.173 270.993 349.814C269.075 347.749 267.748 347.454 266.273 348.929Z" fill="black" />
|
||||
<path d="M287.364 350.698C287.364 352.468 288.249 353.796 289.724 353.796C291.051 353.796 291.789 352.763 291.494 351.288C290.756 347.601 287.364 347.159 287.364 350.698Z" fill="black" />
|
||||
<path d="M306.095 348.928C305.505 349.371 305.063 350.698 305.063 351.878C305.063 354.09 308.898 354.533 310.077 352.468C311.11 350.846 307.423 347.601 306.095 348.928Z" fill="black" />
|
||||
<path d="M327.186 351.583C327.186 352.763 328.219 353.796 329.399 353.796C330.579 353.796 331.611 352.763 331.611 351.583C331.611 350.404 330.579 349.371 329.399 349.371C328.219 349.371 327.186 350.404 327.186 351.583Z" fill="black" />
|
||||
<path d="M257.129 359.843C255.654 362.055 257.571 363.53 259.488 361.613C260.521 360.58 260.521 359.695 259.636 359.105C258.751 358.663 257.571 358.958 257.129 359.843Z" fill="black" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 27 KiB |
@@ -0,0 +1,96 @@
|
||||
<svg width="622" height="427" viewBox="0 0 622 427" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M437.786 1.4749C437.786 2.21235 440.884 5.45713 444.866 8.40693C460.5 20.7961 484.246 44.837 495.16 59.291C499.29 64.7481 502.682 69.6153 502.682 70.0578C502.682 71.8276 509.172 79.6446 510.794 79.6446C511.827 79.6446 512.416 76.5473 512.416 71.5327C512.269 63.7157 512.121 63.4207 508.434 63.4207C504.452 63.4207 502.682 61.0609 502.682 56.0462C502.682 54.4238 501.06 52.8014 498.257 51.769C495.898 51.0315 493.833 49.7041 493.833 48.9667C493.833 45.8694 454.6 8.8494 451.208 8.8494C450.323 8.8494 449.586 8.25944 449.586 7.52199C449.586 6.19458 440.589 0 438.671 0C438.229 0 437.786 0.58996 437.786 1.4749Z" fill="black" />
|
||||
<path d="M520.676 76.5472C520.971 77.5797 521.708 78.3171 522.298 78.3171C524.068 78.1696 523.626 76.1047 521.856 75.3673C520.823 75.0723 520.234 75.6623 520.676 76.5472Z" fill="black" />
|
||||
<path d="M529.82 85.6917C528.345 88.199 531.738 91.0014 533.803 88.9365C535.573 87.1666 534.393 84.0693 532.18 84.0693C531.443 84.0693 530.41 84.8068 529.82 85.6917Z" fill="black" />
|
||||
<path d="M389.852 87.609C361.387 93.3611 337.051 118.729 328.496 151.177C324.072 168.434 325.399 185.247 332.479 199.554C333.658 201.914 334.543 204.716 334.543 205.749C334.543 206.781 335.576 207.961 336.756 208.551C337.936 208.993 338.968 210.321 338.968 211.501C338.968 215.63 353.57 228.167 365.074 233.919L376.578 239.524H396.047C410.206 239.524 416.99 238.934 421.12 237.164C424.217 235.837 429.379 233.772 432.624 232.297C435.869 230.969 441.031 227.725 444.128 224.922L449.733 220.055L451.945 193.359C453.125 178.758 454.01 163.566 453.715 159.732L453.273 152.652L444.423 151.767C439.556 151.177 426.872 150.735 416.253 150.587L396.932 150.44L395.309 161.797C394.277 168.139 393.539 174.038 393.539 175.071C393.539 176.103 397.817 177.135 404.601 177.725C417.285 178.905 420.53 181.56 418.76 188.492C417.875 191.59 415.958 193.064 410.353 195.129C399.881 199.112 389.557 196.752 381.15 188.197C371.858 178.905 369.646 167.844 373.776 151.177C376.136 141.738 393.834 124.039 401.209 123.892C402.684 123.892 406.814 123.154 410.353 122.122C415.515 120.794 419.055 120.794 427.315 122.417C433.657 123.597 438.376 123.892 439.114 123.007C439.851 122.269 440.736 115.485 441.179 107.963C442.064 94.5411 441.916 94.2461 438.229 91.8862C431.444 87.4615 403.126 84.9542 389.852 87.609Z" fill="black" />
|
||||
<path d="M307.995 90.7062C303.571 91.8861 294.869 92.7711 288.822 92.9185C282.774 92.9185 276.875 93.5085 275.695 94.246C273.63 95.5734 271.123 101.325 267.288 114.305C266.108 118.287 264.486 123.596 263.748 126.104C263.011 128.464 261.093 135.101 259.324 140.853C257.701 146.457 255.194 154.422 254.014 158.552C252.686 162.534 251.359 168.138 250.917 170.793C250.474 173.448 249.589 175.955 248.704 176.398C246.787 177.578 241.625 172.711 241.625 169.613C241.625 168.286 240.592 165.926 239.412 164.304C238.232 162.829 237.2 160.174 237.2 158.404C237.2 156.782 236.463 154.717 235.578 153.832C234.693 152.947 233.513 149.997 232.775 147.342C230.71 139.23 226.286 127.284 221.566 117.549C220.386 115.19 219.501 112.387 219.501 111.355C219.501 110.322 217.879 108.11 215.814 106.488C212.717 104.128 211.094 103.833 206.522 104.865C203.572 105.603 197.23 106.193 192.658 106.193C184.989 106.193 168.47 109.437 166.7 111.355C166.257 111.797 166.7 118.582 167.732 126.546C168.617 134.658 170.092 152.652 170.829 166.811C171.714 180.97 172.894 195.719 173.632 199.849C174.517 203.831 175.254 214.893 175.402 224.185C175.549 233.477 175.992 243.063 176.582 245.571C177.614 250.585 178.057 250.585 204.752 248.226L218.764 247.046L219.206 238.934C219.501 234.509 218.764 222.12 217.436 211.648C213.897 180.528 213.159 166.221 215.224 166.959C216.256 167.254 217.584 168.581 218.026 169.761C219.354 172.268 223.631 182.15 226.138 188.05C227.171 190.409 228.793 194.392 229.826 196.899C230.858 199.259 232.185 203.684 232.775 206.486C234.103 212.533 236.61 219.612 240.592 228.167C242.805 233.329 244.132 234.509 247.229 234.509C254.014 234.509 257.996 229.789 259.471 220.055C260.356 213.27 262.126 205.306 265.961 190.999C270.533 173.596 271.713 168.581 272.303 162.681C272.598 159.289 273.778 155.749 274.81 154.864C278.645 151.62 280.267 157.519 281.595 181.708C282.332 194.982 283.512 208.403 284.397 211.648C285.134 214.893 285.872 223.152 285.872 230.084C285.872 246.456 286.314 247.046 296.344 244.981C300.768 244.096 307.995 243.063 312.567 242.621C326.284 241.146 326.579 240.704 325.989 219.465C325.694 208.551 324.662 200.144 323.629 198.374C322.597 196.752 320.974 190.704 319.794 184.805C317.877 173.891 318.32 161.649 321.564 146.015C322.449 141.59 323.334 127.431 323.334 114.6C323.482 86.2815 323.924 86.8715 307.995 90.7062Z" fill="black" />
|
||||
<path d="M475.544 89.6739C474.069 92.0337 471.857 126.104 471.709 147.49C471.562 157.667 470.972 171.236 470.234 177.725C469.349 186.28 469.497 189.967 470.677 191.294C471.709 192.327 478.641 193.507 487.196 194.097L501.945 195.129L504.599 184.215C505.927 178.168 507.107 169.908 507.107 165.631C507.107 161.502 508.139 152.8 509.319 146.458C510.499 140.115 511.531 132.593 511.531 129.644C511.531 126.694 512.416 121.679 513.449 118.582C516.104 111.207 517.579 94.0986 515.956 91.5912C514.334 88.9364 477.019 87.1665 475.544 89.6739Z" fill="black" />
|
||||
<path d="M541.767 97.3434C540.587 99.4083 542.209 101.768 544.864 101.768C546.044 101.768 546.929 100.441 546.929 98.8183C546.929 95.7211 543.389 94.6886 541.767 97.3434Z" fill="black" />
|
||||
<path d="M525.838 102.801C523.478 105.013 525.101 109.585 535.277 130.086C538.522 136.428 546.929 161.797 546.929 165.041C546.929 166.369 547.962 170.056 548.994 173.301C553.861 187.312 553.124 224.332 547.667 238.934C542.357 253.093 526.576 268.874 509.319 277.281C505.632 279.051 502.387 281.116 501.797 281.853C501.355 282.591 499.437 283.181 497.668 283.181C495.75 283.181 493.39 284.066 492.505 285.246C491.62 286.426 488.966 287.606 486.753 288.048C484.541 288.491 479.526 289.965 475.397 291.145C471.414 292.473 465.072 294.095 461.385 295.128C457.698 296.013 452.831 297.34 450.323 298.225C447.963 298.962 443.096 300.29 439.556 301.175C433.952 302.502 432.919 303.387 429.527 310.172C427.462 314.449 424.512 320.201 422.743 323.003C421.12 325.805 418.318 330.82 416.695 334.065C414.926 337.31 410.648 344.094 407.256 349.256C403.716 354.419 400.914 358.991 400.914 359.286C400.914 360.318 387.935 377.574 384.838 380.672C383.51 381.999 376.578 389.816 369.204 398.076C361.977 406.335 353.127 414.742 349.735 416.807C345.605 419.167 343.393 421.527 343.393 423.296C343.393 427.131 345.753 426.984 348.85 423.296C350.325 421.674 351.8 420.347 352.39 420.347C353.717 420.347 362.124 413.12 366.106 408.695C367.876 406.777 373.333 401.025 378.496 396.158C386.165 388.784 396.932 376.247 404.601 365.923C405.339 364.89 407.994 361.941 410.501 359.286C412.861 356.778 414.926 353.534 415.221 352.059C415.516 350.584 416.843 347.781 418.17 345.717L420.678 341.882L423.038 345.127C425.545 348.519 427.462 347.781 427.462 343.357C427.462 341.882 426.43 340.702 425.25 340.702C421.71 340.702 422.595 336.572 428.2 327.28C431.739 321.676 434.542 318.578 436.754 318.283C439.261 317.988 440.146 316.661 440.736 312.679C441.326 308.107 442.064 307.517 446.783 306.632C449.881 306.189 453.273 304.714 454.748 303.387C457.993 299.995 460.943 300.29 462.86 303.829C464.482 306.779 469.497 307.664 472.594 305.599C473.627 305.009 474.659 302.945 474.807 300.88C475.249 298.077 476.429 297.045 479.674 296.75C482.034 296.455 484.836 295.275 486.016 294.095C488.818 291.293 496.193 291.44 497.373 294.243C499.29 299.257 507.107 295.57 507.107 289.67C507.107 287.458 508.287 285.983 510.352 285.246C512.122 284.656 515.809 282.001 518.464 279.346C521.118 276.544 524.068 274.331 525.101 274.331C526.133 274.331 528.64 272.562 530.558 270.644C534.982 265.924 537.49 265.925 538.67 270.792C539.407 273.741 540.44 274.479 543.684 274.036C548.847 273.447 550.321 266.367 545.602 264.745C542.062 263.712 541.325 257.518 544.569 255.6C545.602 255.01 547.962 252.208 549.584 249.406C551.206 246.751 554.156 243.801 556.074 242.769C560.203 240.704 561.236 235.689 557.991 233.034C555.189 230.674 555.189 225.66 557.991 221.825C559.171 220.35 560.203 216.81 560.203 214.156C560.203 209.878 560.646 209.436 564.628 209.436C567.43 209.436 569.495 208.551 570.085 207.076C571.56 203.241 568.463 199.849 564.038 200.291C560.498 200.586 560.203 200.291 560.203 195.572C560.203 192.769 559.613 190.115 558.876 189.672C556.664 188.197 556.221 179.938 558.138 177.283C560.941 173.743 560.793 173.153 555.631 167.991C550.469 162.682 550.321 157.814 555.484 155.602C561.826 152.8 557.401 145.868 550.764 148.375C548.847 149.112 548.404 148.227 548.404 144.098C548.404 141.148 547.667 138.346 546.929 137.903C544.569 136.428 539.26 125.219 540.292 124.187C540.882 123.597 541.915 123.007 542.652 123.007C543.389 122.859 544.717 122.712 545.454 122.564C547.814 122.269 547.224 115.042 544.864 115.042C543.684 115.042 542.209 115.632 541.767 116.517C540.145 119.172 535.13 118.14 535.13 115.19C535.13 113.567 535.867 111.797 536.605 111.355C539.26 109.733 538.227 107.963 532.77 104.865C526.871 101.326 527.166 101.473 525.838 102.801Z" fill="black" />
|
||||
<path d="M552.386 107.078C550.469 108.995 551.354 110.618 554.451 110.618C556.074 110.618 557.254 109.88 556.959 108.848C556.221 106.783 553.714 105.75 552.386 107.078Z" fill="black" />
|
||||
<path d="M565.365 116.665C564.775 117.549 564.923 118.877 565.808 119.762C566.693 120.647 567.43 120.352 568.02 118.877C569.053 116.222 566.693 114.305 565.365 116.665Z" fill="black" />
|
||||
<path d="M90.4474 123.744C74.8135 125.956 51.6576 142.475 44.2831 156.487C42.5132 160.027 40.4483 164.009 39.7109 165.189C31.009 181.118 31.8939 216.368 41.3333 233.329C43.8406 237.901 53.28 249.848 54.7549 250.438C55.3448 250.733 57.9996 252.945 60.6545 255.305C65.2266 259.435 69.0614 261.647 83.8104 267.842C96.937 273.446 130.86 270.497 140.004 262.975C141.184 261.942 142.806 261.057 143.396 261.057C145.609 261.057 161.98 247.488 161.98 245.718C161.98 244.538 163.013 242.916 164.192 241.883C167.142 239.524 167.142 216.22 164.192 188.05C163.013 176.693 161.98 162.681 161.98 157.077C161.98 146.752 161.98 146.605 155.786 140.705C148.706 134.068 143.249 130.381 134.989 126.546C128.5 123.597 102.689 121.974 90.4474 123.744ZM116.111 166.664C127.467 172.416 135.432 187.017 135.432 202.209C135.432 212.091 132.335 219.76 125.55 225.807C117.438 233.329 114.488 234.509 104.459 234.509C95.4621 234.509 94.4297 234.214 87.9401 228.609C84.2529 225.365 79.2382 219.465 77.1733 215.63C73.7811 209.731 73.1911 206.928 73.0436 196.899C72.8961 186.722 73.4861 184.362 76.7309 179.2C80.8606 172.563 83.2204 170.498 90.1525 166.516C96.642 162.829 108.884 162.976 116.111 166.664Z" fill="black" />
|
||||
<path d="M551.354 129.791C551.354 131.709 552.386 132.741 554.009 132.741C557.843 132.741 559.023 131.119 557.106 128.759C554.599 125.809 551.354 126.399 551.354 129.791Z" fill="black" />
|
||||
<path d="M573.477 130.529C573.477 131.709 574.51 132.741 575.69 132.741C576.87 132.741 577.902 131.709 577.902 130.529C577.902 129.349 576.87 128.316 575.69 128.316C574.51 128.316 573.477 129.349 573.477 130.529Z" fill="black" />
|
||||
<path d="M562.563 138.493C561.088 140.853 563.301 144.54 566.25 144.54C568.315 144.54 569.053 143.508 569.053 140.853C569.053 138.198 568.315 137.166 566.25 137.166C564.628 137.166 563.006 137.756 562.563 138.493Z" fill="black" />
|
||||
<path d="M574.51 149.997C572.592 151.767 573.477 154.864 575.69 154.864C576.87 154.864 577.902 153.537 577.902 151.915C577.902 148.965 576.28 148.08 574.51 149.997Z" fill="black" />
|
||||
<path d="M561.973 162.534C562.563 166.664 568.168 167.991 569.643 164.451C570.97 160.912 569.495 159.289 565.218 159.289C562.121 159.289 561.531 159.879 561.973 162.534Z" fill="black" />
|
||||
<path d="M582.917 160.912C582.474 161.797 582.769 163.271 583.507 164.009C585.424 165.926 590.144 162.829 588.816 160.617C587.784 158.847 584.244 158.994 582.917 160.912Z" fill="black" />
|
||||
<path d="M574.51 170.646C572.592 172.416 573.477 176.988 575.542 176.988C578.344 176.988 579.672 174.923 578.492 171.973C577.46 169.466 576.132 169.024 574.51 170.646Z" fill="black" />
|
||||
<path d="M562.12 180.97C559.023 182.15 559.908 185.837 563.595 187.165C567.43 188.787 571.412 185.837 570.085 182.298C569.2 179.938 565.808 179.348 562.12 180.97Z" fill="black" />
|
||||
<path d="M582.327 183.625C582.327 185.395 583.359 185.837 586.014 185.542C588.079 185.247 589.849 184.363 589.849 183.625C589.849 182.74 588.079 182.003 586.014 181.708C583.359 181.413 582.327 181.855 582.327 183.625Z" fill="black" />
|
||||
<path d="M573.477 194.687C573.477 198.374 574.067 199.259 576.132 198.817C577.755 198.522 578.64 197.047 578.64 194.687C578.64 192.327 577.755 190.852 576.132 190.557C574.067 190.115 573.477 191 573.477 194.687Z" fill="black" />
|
||||
<path d="M596.191 192.327C595.601 193.654 595.601 195.277 596.191 195.719C596.633 196.309 597.961 195.867 598.846 194.982C600.173 193.654 600.173 192.769 598.846 191.442C597.518 190.115 596.928 190.41 596.191 192.327Z" fill="black" />
|
||||
<path d="M15.2275 197.489C-0.406457 227.872 -2.61881 238.196 2.39585 256.927C5.49314 268.874 18.9147 281.853 37.646 291.145C55.6398 299.995 57.7046 300.879 81.598 307.517C86.4652 308.991 91.1849 310.614 91.9223 311.204C92.8072 311.794 98.1169 312.826 103.722 313.269C109.474 313.859 118.028 315.333 122.895 316.366C127.762 317.546 134.399 318.726 137.644 318.873C142.806 319.316 143.544 319.758 143.986 323.298C144.429 327.28 144.281 327.428 137.497 327.428C132.63 327.428 129.975 326.69 128.795 324.92C127.467 323.15 125.108 322.561 120.535 322.708C103.132 323.593 95.757 327.428 111.686 327.428C123.19 327.428 129.827 328.903 128.942 331.115C128.647 332.147 129.09 333.77 129.975 334.655C132.335 337.015 139.414 336.72 140.447 334.065C140.889 332.737 143.249 331.852 146.494 331.852C151.066 331.852 151.656 332.295 151.656 335.54C151.656 339.817 153.131 340.112 159.178 336.867C164.93 333.917 164.045 331.557 156.67 329.935C149.591 328.46 148.116 327.133 149.443 323.15C150.623 319.463 157.703 319.021 163.455 322.118C165.815 323.593 169.65 323.888 174.959 323.445C179.974 322.855 183.956 323.298 186.463 324.478C189.266 326.1 190.446 326.1 191.773 324.773C193.985 322.561 203.572 322.561 208.439 324.773C211.537 326.1 213.012 326.1 215.224 324.773C218.764 322.561 225.843 322.561 228.056 324.773C229.383 326.1 231.448 326.1 237.052 324.625C243.542 323.003 245.017 323.003 248.852 325.363C252.834 327.723 253.424 327.723 258.291 325.51C264.781 322.413 265.961 322.413 271.123 325.51C275.105 327.723 275.547 327.723 279.087 325.51C283.659 322.413 290.739 322.266 293.246 325.215C294.279 326.395 295.901 327.428 296.933 327.428C297.966 327.428 299.588 326.395 300.621 325.215C301.653 324.035 304.603 323.003 307.258 323.003C309.913 323.003 312.862 324.035 313.895 325.215C316.255 328.018 318.172 328.018 321.269 325.215C324.809 321.971 333.069 321.823 334.691 324.92C336.756 328.608 341.77 328.018 345.163 323.74C346.785 321.676 349.587 320.053 351.505 320.053C353.422 320.053 355.782 321.528 357.257 323.74C360.059 328.018 365.516 328.755 365.516 324.773C365.516 320.938 368.466 318.578 373.333 318.578C376.578 318.578 377.463 317.988 377.021 315.923C376.578 313.711 374.808 313.416 360.354 313.859C351.357 314.154 339.853 315.039 334.543 315.923C318.909 318.283 224.368 319.168 205.49 317.103C196.64 316.071 182.629 314.744 174.517 314.154C157.26 312.974 138.529 310.466 125.108 307.517C119.798 306.484 109.916 304.419 102.984 302.944C96.052 301.617 88.6775 299.552 86.4652 298.372C84.2528 297.34 80.8606 296.455 78.9432 296.455C77.0258 296.455 74.2235 295.422 72.6011 294.242C71.1262 293.062 68.4714 292.03 66.9965 292.03C62.4243 292.03 34.8437 277.871 28.2066 272.119C24.9618 269.317 19.6522 263.122 16.5549 258.55L10.9503 249.995V234.361C10.9503 222.12 11.3927 217.99 13.4576 214.893C14.785 212.828 15.9649 210.173 15.9649 209.288C15.9649 208.256 16.8499 206.338 18.0298 205.158C19.0622 203.831 20.3896 201.029 20.9796 198.669C21.717 195.424 21.4221 194.687 19.3572 194.687C17.8823 194.687 15.9649 196.014 15.2275 197.489Z" fill="black" />
|
||||
<path d="M583.359 201.619C582.769 202.061 582.327 203.831 582.327 205.306C582.327 208.256 588.816 209.141 590.439 206.486C592.504 203.241 586.014 198.816 583.359 201.619Z" fill="black" />
|
||||
<path d="M604.745 205.306C605.778 208.108 610.35 208.551 610.35 205.896C610.35 204.421 609.022 203.536 607.253 203.536C605.483 203.536 604.45 204.274 604.745 205.306Z" fill="black" />
|
||||
<path d="M473.627 215.04C462.86 220.94 459.615 232.592 465.957 242.769C469.349 248.373 475.249 250.143 485.278 248.816C499.437 246.898 503.862 227.43 492.358 218.138C485.573 212.681 479.674 211.648 473.627 215.04Z" fill="black" />
|
||||
<path d="M572.888 213.566C571.56 215.63 573.772 221.235 575.69 221.235C578.05 221.235 579.967 216.073 578.64 213.861C577.607 212.091 573.92 211.943 572.888 213.566Z" fill="black" />
|
||||
<path d="M595.601 215.335C595.601 216.958 596.633 218.285 597.813 218.285C598.993 218.285 600.026 216.958 600.026 215.335C600.026 213.713 598.993 212.386 597.813 212.386C596.633 212.386 595.601 213.713 595.601 215.335Z" fill="black" />
|
||||
<path d="M618.462 214.008C616.987 216.22 618.904 217.695 620.822 215.778C621.854 214.745 621.854 213.861 620.969 213.271C620.084 212.828 618.904 213.123 618.462 214.008Z" fill="black" />
|
||||
<path d="M561.973 222.563C558.433 224.775 560.498 230.822 564.775 231.264C569.79 231.854 572.74 228.315 570.527 224.332C568.905 221.088 565.365 220.35 561.973 222.563Z" fill="black" />
|
||||
<path d="M583.359 223.742C582.769 224.185 582.327 225.955 582.327 227.43C582.327 229.347 583.507 230.084 586.751 230.084C591.324 230.084 592.504 227.577 589.406 224.48C587.489 222.562 584.834 222.12 583.359 223.742Z" fill="black" />
|
||||
<path d="M605.04 225.807C603.713 228.167 606.81 230.674 609.612 229.494C611.677 228.757 611.825 228.167 610.35 226.397C608.138 223.742 606.515 223.595 605.04 225.807Z" fill="black" />
|
||||
<path d="M572.74 235.984C572.297 236.722 572.592 238.786 573.477 240.409C574.805 242.916 575.542 243.211 577.312 241.736C580.409 239.229 579.967 234.509 576.575 234.509C574.952 234.509 573.182 235.099 572.74 235.984Z" fill="black" />
|
||||
<path d="M595.601 237.607C595.601 241.146 598.993 240.704 599.731 237.017C600.026 235.542 599.288 234.509 597.961 234.509C596.486 234.509 595.601 235.837 595.601 237.607Z" fill="black" />
|
||||
<path d="M562.416 245.718C559.466 248.816 560.203 251.323 564.481 251.913C568.905 252.65 571.118 250.291 569.79 246.456C568.61 242.769 565.66 242.474 562.416 245.718Z" fill="black" />
|
||||
<path d="M582.917 247.046C581.737 249.848 583.359 252.208 586.604 252.208C590.291 252.208 591.324 250.733 589.554 247.636C587.784 244.244 584.097 243.949 582.917 247.046Z" fill="black" />
|
||||
<path d="M605.925 249.258C605.925 249.996 606.957 250.733 608.137 250.733C609.317 250.733 610.35 249.996 610.35 249.258C610.35 248.373 609.317 247.783 608.137 247.783C606.957 247.783 605.925 248.373 605.925 249.258Z" fill="black" />
|
||||
<path d="M550.617 256.78C549.142 261.057 552.534 264.155 556.959 262.385C560.498 261.057 561.236 258.255 558.433 255.453C555.631 252.65 551.649 253.388 550.617 256.78Z" fill="black" />
|
||||
<path d="M573.035 257.223C571.855 260.025 575.247 262.68 577.755 261.057C580.262 259.582 578.787 255.158 575.837 255.158C574.805 255.158 573.477 256.043 573.035 257.223Z" fill="black" />
|
||||
<path d="M595.601 258.845C595.601 260.025 596.633 261.057 597.813 261.057C598.993 261.057 600.026 260.025 600.026 258.845C600.026 257.665 598.993 256.633 597.813 256.633C596.633 256.633 595.601 257.665 595.601 258.845Z" fill="black" />
|
||||
<path d="M618.019 257.96C618.314 258.992 619.052 259.73 619.642 259.73C621.412 259.582 620.969 257.517 619.199 256.78C618.167 256.485 617.577 257.075 618.019 257.96Z" fill="black" />
|
||||
<path d="M561.973 269.464C562.563 274.626 568.315 276.101 569.79 271.529C571.117 267.399 569.643 265.482 565.218 265.482C561.973 265.482 561.531 266.072 561.973 269.464Z" fill="black" />
|
||||
<path d="M582.917 267.694C581.737 270.497 583.359 272.857 586.752 272.857C589.554 272.857 591.176 270.349 590.144 267.252C589.259 264.745 583.949 265.04 582.917 267.694Z" fill="black" />
|
||||
<path d="M605.188 266.957C604.155 268.579 605.483 271.382 607.4 271.382C607.99 271.382 608.875 270.497 609.317 269.317C610.202 266.957 606.515 264.892 605.188 266.957Z" fill="black" />
|
||||
<path d="M528.935 277.871C527.903 282.886 529.673 284.951 533.803 284.361C536.162 284.066 537.49 282.738 537.785 280.379C538.67 274.036 530.115 271.677 528.935 277.871Z" fill="black" />
|
||||
<path d="M551.944 278.019C550.911 280.526 552.239 284.656 554.009 284.656C554.599 284.656 556.368 283.771 557.843 282.738C560.351 280.821 560.351 280.673 557.991 278.314C555.041 275.216 552.976 275.069 551.944 278.019Z" fill="black" />
|
||||
<path d="M573.477 280.231C573.477 281.853 574.51 283.181 575.69 283.181C576.87 283.181 577.902 281.853 577.902 280.231C577.902 278.609 576.87 277.281 575.69 277.281C574.51 277.281 573.477 278.609 573.477 280.231Z" fill="black" />
|
||||
<path d="M595.601 281.116C595.601 282.443 596.338 283.181 597.371 282.886C600.026 282.001 600.468 278.756 597.961 278.756C596.633 278.756 595.601 279.789 595.601 281.116Z" fill="black" />
|
||||
<path d="M517.726 290.703C518.316 295.128 522.888 297.93 524.806 294.98C527.166 291.293 525.838 288.343 521.413 287.901C517.726 287.458 517.284 287.901 517.726 290.703Z" fill="black" />
|
||||
<path d="M539.997 289.965C538.965 292.768 542.947 297.192 545.307 295.865C546.192 295.275 546.929 293.21 546.929 291.145C546.929 288.638 546.192 287.606 543.979 287.606C542.357 287.606 540.587 288.638 539.997 289.965Z" fill="black" />
|
||||
<path d="M563.448 290.85C564.185 295.865 569.053 296.307 569.053 291.44C569.053 288.49 568.315 287.605 565.955 287.605C563.743 287.605 563.006 288.343 563.448 290.85Z" fill="black" />
|
||||
<path d="M14.785 293.8C15.2275 294.833 16.7024 296.013 18.0298 296.16C19.7996 296.603 20.5371 296.013 20.0946 294.685C19.6522 293.653 18.1772 292.473 16.8498 292.325C15.08 291.883 14.3425 292.473 14.785 293.8Z" fill="black" />
|
||||
<path d="M486.753 298.225C482.919 302.059 485.573 306.779 491.621 306.779C494.423 306.779 495.898 301.175 493.833 298.667C491.621 296.012 489.113 295.865 486.753 298.225Z" fill="black" />
|
||||
<path d="M508.877 297.782C507.697 298.372 507.107 300.585 507.402 302.502C507.697 305.157 508.729 306.042 511.532 306.042C514.334 306.042 515.366 305.157 515.661 302.502C516.104 299.552 514.039 296.455 511.532 296.455C511.089 296.455 509.909 297.045 508.877 297.782Z" fill="black" />
|
||||
<path d="M529.23 301.47C529.23 306.042 531.295 307.369 535.13 305.304C539.407 302.944 538.227 298.815 533.213 298.225C529.82 297.782 529.23 298.225 529.23 301.47Z" fill="black" />
|
||||
<path d="M552.239 298.962C551.796 299.405 551.354 301.175 551.354 302.649C551.354 304.714 552.239 305.452 554.599 305.009C556.369 304.714 557.991 303.829 558.286 302.797C559.023 300.88 553.714 297.487 552.239 298.962Z" fill="black" />
|
||||
<path d="M574.362 300.437C572.592 302.207 573.477 305.304 575.69 305.304C576.87 305.304 577.902 304.419 577.902 303.24C577.902 300.732 575.837 299.11 574.362 300.437Z" fill="black" />
|
||||
<path d="M29.2391 302.354C29.2391 304.419 33.0738 305.747 35.2862 304.567C36.4661 303.682 36.3186 303.092 34.9912 302.207C32.1889 300.437 29.2391 300.585 29.2391 302.354Z" fill="black" />
|
||||
<path d="M38.0884 306.779C38.0884 308.402 45.168 310.466 46.3479 309.287C46.9378 308.844 46.7904 307.812 46.2004 306.927C45.0205 304.862 38.0884 304.862 38.0884 306.779Z" fill="black" />
|
||||
<path d="M48.7078 311.499C49.4452 313.711 58.7371 315.039 58.7371 312.974C58.7371 311.204 55.1973 309.729 51.0676 309.729C49.4452 309.729 48.4128 310.466 48.7078 311.499Z" fill="black" />
|
||||
<path d="M454.01 313.416C454.01 315.481 455.043 317.546 456.223 317.988C459.173 319.168 462.86 316.071 462.86 312.531C462.86 310.467 461.827 309.729 458.435 309.729C454.748 309.729 454.01 310.319 454.01 313.416Z" fill="black" />
|
||||
<path d="M476.134 313.269C476.134 318.283 478.789 319.463 482.329 315.924C485.868 312.384 484.688 309.729 479.674 309.729C476.871 309.729 476.134 310.466 476.134 313.269Z" fill="black" />
|
||||
<path d="M498.257 313.269C498.257 317.989 500.322 319.168 503.272 316.219C506.812 312.826 506.222 309.729 501.945 309.729C498.995 309.729 498.257 310.467 498.257 313.269Z" fill="black" />
|
||||
<path d="M519.791 311.056C518.464 313.121 520.528 317.251 522.446 316.661C525.101 315.776 525.396 309.729 522.741 309.729C521.561 309.729 520.233 310.319 519.791 311.056Z" fill="black" />
|
||||
<path d="M541.62 311.351C541.177 312.236 541.325 313.564 542.209 314.449C544.127 316.366 546.929 314.744 546.929 311.794C546.929 309.434 543.094 308.992 541.62 311.351Z" fill="black" />
|
||||
<path d="M565.365 311.351C563.891 313.564 565.808 315.039 567.725 313.121C568.758 312.089 568.758 311.204 567.873 310.614C566.988 310.171 565.808 310.466 565.365 311.351Z" fill="black" />
|
||||
<path d="M64.1942 315.039C62.2768 316.956 64.9316 318.578 69.9463 318.578C73.4861 318.578 74.961 317.989 74.666 316.661C73.9285 314.744 65.8166 313.416 64.1942 315.039Z" fill="black" />
|
||||
<path d="M80.1231 319.906C78.7957 322.266 82.3355 323.298 87.9401 322.266C93.1022 321.086 93.2497 321.086 89.71 320.053C84.4003 318.431 81.1556 318.431 80.1231 319.906Z" fill="black" />
|
||||
<path d="M445.456 320.348C443.096 322.708 443.244 325.51 445.751 326.395C448.258 327.428 449.586 326.248 449.586 322.856C449.586 319.463 447.521 318.284 445.456 320.348Z" fill="black" />
|
||||
<path d="M466.694 322.561C465.662 325.068 468.464 327.87 470.529 326.543C471.119 326.1 471.709 324.478 471.709 322.856C471.709 319.463 468.022 319.168 466.694 322.561Z" fill="black" />
|
||||
<path d="M490.145 321.971C489.555 323.446 489.408 325.068 489.998 325.51C491.473 326.985 492.653 324.921 491.915 321.971C491.325 319.463 491.325 319.463 490.145 321.971Z" fill="black" />
|
||||
<path d="M261.978 330.525C257.111 333.327 259.618 337.752 266.108 337.752C267.73 337.752 268.173 336.572 267.878 333.77C267.435 329.345 265.666 328.313 261.978 330.525Z" fill="black" />
|
||||
<path d="M304.603 329.935C302.981 331.41 303.423 337.457 305.193 338.637C306.078 339.08 308.29 338.195 310.06 336.425C313.157 333.18 313.157 333.18 310.355 330.968C307.11 328.608 305.93 328.46 304.603 329.935Z" fill="black" />
|
||||
<path d="M325.841 331.705C324.809 333.622 324.957 335.097 326.284 337.015C327.759 339.079 328.644 339.227 331.299 337.9C334.986 335.982 335.428 333.327 332.773 330.673C330.266 328.165 327.464 328.46 325.841 331.705Z" fill="black" />
|
||||
<path d="M347.965 331.557C345.9 335.54 348.26 338.342 352.832 337.162C357.404 335.982 357.847 332.59 353.865 330.378C350.03 328.46 349.587 328.46 347.965 331.557Z" fill="black" />
|
||||
<path d="M369.204 331.705C368.171 335.245 369.794 339.227 372.301 339.227C373.628 339.227 374.366 337.457 374.366 334.065C374.366 328.46 370.826 326.69 369.204 331.705Z" fill="black" />
|
||||
<path d="M174.222 332.59C171.714 334.507 171.714 334.655 174.222 335.54C177.172 336.72 181.154 335.54 181.154 333.475C181.154 331.115 177.024 330.525 174.222 332.59Z" fill="black" />
|
||||
<path d="M197.968 332.59C196.493 336.13 198.705 337.9 201.95 336.13C204.31 334.95 204.605 334.212 203.13 332.443C200.918 329.788 199 329.788 197.968 332.59Z" fill="black" />
|
||||
<path d="M219.059 332.443C217.731 335.687 220.976 337.9 224.221 336.13C226.433 334.95 226.728 334.212 225.253 332.443C223.041 329.788 220.091 329.788 219.059 332.443Z" fill="black" />
|
||||
<path d="M239.412 332.442C236.758 334.36 236.758 334.507 240.002 336.13C244.28 338.49 248.557 335.687 245.902 332.442C243.837 329.935 242.952 329.935 239.412 332.442Z" fill="black" />
|
||||
<path d="M282.184 331.852C280.857 334.065 284.249 337.752 287.494 337.752C289.559 337.752 290.296 336.72 290.296 334.065C290.296 331.115 289.559 330.378 286.757 330.378C284.692 330.378 282.627 331.115 282.184 331.852Z" fill="black" />
|
||||
<path d="M433.657 332.295C431.444 334.802 432.772 336.277 437.344 336.277C439.999 336.277 440.884 335.54 440.441 333.77C439.851 330.525 435.869 329.64 433.657 332.295Z" fill="black" />
|
||||
<path d="M455.043 331.263C452.683 333.622 454.158 336.277 457.845 336.277C460.647 336.277 461.532 335.54 461.09 333.77C460.647 330.968 456.813 329.493 455.043 331.263Z" fill="black" />
|
||||
<path d="M477.166 331.41C475.102 333.327 476.134 336.277 478.789 336.277C482.624 336.277 483.803 334.655 481.886 332.295C479.969 330.23 478.641 329.935 477.166 331.41Z" fill="black" />
|
||||
<path d="M499.29 331.263C497.225 333.475 498.405 336.277 501.355 336.277C503.419 336.277 504.157 335.54 503.862 333.77C503.42 331.115 500.912 329.788 499.29 331.263Z" fill="black" />
|
||||
<path d="M112.571 333.327C111.981 334.212 114.636 334.802 119.355 334.802C128.352 334.802 128.352 334.065 119.65 332.737C116.111 332.295 113.161 332.442 112.571 333.327Z" fill="black" />
|
||||
<path d="M520.381 334.065C520.381 335.245 521.413 336.277 522.593 336.277C523.773 336.277 524.806 335.245 524.806 334.065C524.806 332.885 523.773 331.853 522.593 331.853C521.413 331.853 520.381 332.885 520.381 334.065Z" fill="black" />
|
||||
<path d="M250.917 343.062C249.737 346.159 252.834 349.256 255.046 347.044C256.964 345.127 255.784 340.702 253.424 340.702C252.539 340.702 251.507 341.734 250.917 343.062Z" fill="black" />
|
||||
<path d="M272.598 344.389C272.598 347.339 273.188 348.224 275.252 347.781C276.58 347.486 277.76 346.012 277.76 344.389C277.76 342.767 276.58 341.292 275.252 340.997C273.188 340.554 272.598 341.439 272.598 344.389Z" fill="black" />
|
||||
<path d="M295.016 344.389C295.311 346.454 296.196 348.224 296.934 348.224C297.818 348.224 298.556 346.454 298.851 344.389C299.146 341.734 298.703 340.702 296.934 340.702C295.164 340.702 294.721 341.734 295.016 344.389Z" fill="black" />
|
||||
<path d="M315.96 342.914C314.927 345.569 316.402 348.076 319.204 348.076C320.384 348.076 321.269 346.601 321.269 344.389C321.269 340.407 317.287 339.227 315.96 342.914Z" fill="black" />
|
||||
<path d="M336.903 342.029C335.428 344.389 337.641 348.076 340.738 348.076C342.803 348.076 343.54 347.191 343.098 344.832C342.655 341.292 338.378 339.522 336.903 342.029Z" fill="black" />
|
||||
<path d="M358.732 342.914C358.142 344.242 358.437 346.011 359.322 346.896C361.534 349.109 365.811 347.044 365.221 344.094C364.631 340.702 359.912 339.817 358.732 342.914Z" fill="black" />
|
||||
<path d="M445.751 342.767C445.161 344.094 445.161 345.717 445.751 346.159C446.193 346.749 447.521 346.307 448.406 345.422C449.733 344.094 449.733 343.209 448.406 341.882C447.078 340.554 446.488 340.849 445.751 342.767Z" fill="black" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 30 KiB |
Reference in new issue
Block a user