Add character-based chat bubble system with new directory structure
- Implement 5:1 ratio comic-style chat bubbles with character images - Create modular design with character-specific assets - Organize assets in /characters/{name}/ directories - Add fallback system using Example character - Support both speech and thought bubble types - Maintain backward compatibility with style parameter 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
34bd7e44e6
commit
e0d54bd1b6
115
Generator.md
Normal file
115
Generator.md
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
# SillyBubble Image Generator - Design Document
|
||||||
|
|
||||||
|
## Component Architecture
|
||||||
|
|
||||||
|
The SillyBubble image generator creates comic-style chat visualizations with a modular component system. The final image has a 5:1 width-to-height ratio, composed of layered elements.
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
1. **Background Layer**
|
||||||
|
- Full 5:1 ratio base image for setting style/mood
|
||||||
|
- Can be themed (e.g., fantasy, sci-fi, cozy, etc.)
|
||||||
|
- Provides consistent canvas for all other elements
|
||||||
|
|
||||||
|
2. **Character Layer (Chibi)**
|
||||||
|
- Positioned on the left side of the image
|
||||||
|
- Various character images with different expressions/poses
|
||||||
|
- Takes approximately 20% of the total width
|
||||||
|
- Named by character (e.g., "bianca.png", "ruby.png")
|
||||||
|
|
||||||
|
3. **Bubble Layer**
|
||||||
|
- Semi-transparent speech bubble
|
||||||
|
- Positioned to the right of the character
|
||||||
|
- Includes a pointer/tail connecting to the character
|
||||||
|
- Takes approximately 70% of the total width
|
||||||
|
- Various styles (rounded, square, cloud, thought, etc.)
|
||||||
|
|
||||||
|
4. **Text Layer**
|
||||||
|
- Dynamically rendered text content
|
||||||
|
- Positioned within the bubble boundaries
|
||||||
|
- Supports word-wrapping and styling
|
||||||
|
- Font options compatible with the overall theme
|
||||||
|
|
||||||
|
## Implementation Approach
|
||||||
|
|
||||||
|
### File Structure and Naming Convention
|
||||||
|
```
|
||||||
|
/characters/ - Main directory for all characters
|
||||||
|
/Example/ - Example character (fallback)
|
||||||
|
background.png - Background image
|
||||||
|
character.png - Character image
|
||||||
|
speech.png - Speech bubble
|
||||||
|
thought.png - Thought bubble
|
||||||
|
/Bianca/ - Another character
|
||||||
|
background.png - Background image
|
||||||
|
character.png - Character image
|
||||||
|
speech.png - Speech bubble
|
||||||
|
thought.png - Thought bubble
|
||||||
|
/fonts/*.ttf - Font files
|
||||||
|
```
|
||||||
|
|
||||||
|
Each character has their own directory containing all assets. If a specific character's asset is missing, the system will fall back to the Example character's corresponding asset.
|
||||||
|
|
||||||
|
### Image Dimensions
|
||||||
|
- Total Image: 2000×400px (5:1 ratio)
|
||||||
|
- Background: Full 2000×400px canvas
|
||||||
|
- Character: ~400×400px (20% of width)
|
||||||
|
- Bubble: ~1400×300px (70% of width)
|
||||||
|
- Text Area: ~1300×250px (inside bubble)
|
||||||
|
- Remaining 10% (200px width) for margins and spacing
|
||||||
|
|
||||||
|
### Parameter System
|
||||||
|
The enhanced image.php will accept:
|
||||||
|
- `q`: Text content (required)
|
||||||
|
- `character`: Character to use (e.g., "bianca") - if not provided, defaults to "Example"
|
||||||
|
- `bubble_type`: "speech" or "thought" (defaults to "speech")
|
||||||
|
- `style`: Legacy parameter, can be used instead of character parameter
|
||||||
|
|
||||||
|
**Backward Compatibility**:
|
||||||
|
- If only `style` is provided (no `character`), the script will use the style value as the character name
|
||||||
|
- This ensures the SillyTavern extension doesn't need modification
|
||||||
|
|
||||||
|
### Image Composition Process
|
||||||
|
1. Load or create background layer (full canvas)
|
||||||
|
2. Check if character exists and overlay on left side
|
||||||
|
3. Position and overlay appropriate bubble template
|
||||||
|
4. Calculate text boundaries within bubble
|
||||||
|
5. Render text with proper wrapping and styling
|
||||||
|
6. Output final composed image
|
||||||
|
|
||||||
|
### Dynamic Bubble Generation
|
||||||
|
- If no bubble template exists, dynamically draw a bubble
|
||||||
|
- Support both template-based and on-the-fly bubble generation
|
||||||
|
- Ensure proper connection between character and bubble
|
||||||
|
|
||||||
|
## Visual Representation
|
||||||
|
|
||||||
|
```
|
||||||
|
FINAL COMPOSITION (5:1 ratio):
|
||||||
|
+--------------------------------------------------------------------------------------+
|
||||||
|
| |
|
||||||
|
| +--------+ +--------------------------------------------------------------+ |
|
||||||
|
| | | | | |
|
||||||
|
| | CHIBI |<---+ TEXT CONTENT | |
|
||||||
|
| | | | | |
|
||||||
|
| +--------+ +--------------------------------------------------------------+ |
|
||||||
|
| |
|
||||||
|
+--------------------------------------------------------------------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
## Feature Roadmap
|
||||||
|
|
||||||
|
1. **Basic Implementation**
|
||||||
|
- Support for character-based styling
|
||||||
|
- Simple bubble positioning
|
||||||
|
- Proper text wrapping
|
||||||
|
|
||||||
|
2. **Enhanced Features**
|
||||||
|
- Multiple character positions (left/right)
|
||||||
|
- Various bubble styles
|
||||||
|
- Expression selection for characters
|
||||||
|
|
||||||
|
3. **Advanced Features**
|
||||||
|
- Multiple characters in one image
|
||||||
|
- Animated GIF output option
|
||||||
|
- Theme-based text styling
|
BIN
characters/Example/background.png
Normal file
BIN
characters/Example/background.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.3 KiB |
BIN
characters/Example/character.png
Normal file
BIN
characters/Example/character.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.1 KiB |
BIN
characters/Example/speech.png
Normal file
BIN
characters/Example/speech.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.0 KiB |
BIN
characters/Example/thought.png
Normal file
BIN
characters/Example/thought.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.2 KiB |
394
image.php
394
image.php
@ -2,12 +2,14 @@
|
|||||||
/**
|
/**
|
||||||
* SillyBubble Image Generator
|
* SillyBubble Image Generator
|
||||||
*
|
*
|
||||||
* This script generates chat bubble images from text.
|
* This script generates comic-style chat bubble images with characters.
|
||||||
* It takes a 'q' parameter for the text content and an optional 'style' parameter.
|
* It supports character-based styling with speech or thought bubbles.
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* image.php?q=Hello+World
|
* image.php?q=Hello+World
|
||||||
* image.php?q=Hello+World&style=modern
|
* image.php?q=Hello+World&character=Example
|
||||||
|
* image.php?q=Hello+World&character=Example&bubble_type=thought
|
||||||
|
* image.php?q=Hello+World&style=Example (legacy style parameter maps to character)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Set content type to PNG image
|
// Set content type to PNG image
|
||||||
@ -15,97 +17,301 @@ header('Content-Type: image/png');
|
|||||||
|
|
||||||
// Get parameters
|
// Get parameters
|
||||||
$text = isset($_GET['q']) ? urldecode($_GET['q']) : 'No text provided';
|
$text = isset($_GET['q']) ? urldecode($_GET['q']) : 'No text provided';
|
||||||
|
$character = isset($_GET['character']) ? $_GET['character'] : 'Example';
|
||||||
|
$bubble_type = isset($_GET['bubble_type']) ? $_GET['bubble_type'] : 'speech';
|
||||||
$style = isset($_GET['style']) ? $_GET['style'] : 'default';
|
$style = isset($_GET['style']) ? $_GET['style'] : 'default';
|
||||||
|
|
||||||
// Define styles
|
// If style parameter is used but no character is specified, use style as character name
|
||||||
|
if (!isset($_GET['character']) && isset($_GET['style']) && $style != 'default') {
|
||||||
|
$character = $style;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate bubble_type - only 'speech' or 'thought' allowed
|
||||||
|
if ($bubble_type != 'speech' && $bubble_type != 'thought') {
|
||||||
|
$bubble_type = 'speech';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define canvas dimensions (5:1 ratio)
|
||||||
|
$canvasWidth = 2000;
|
||||||
|
$canvasHeight = 400;
|
||||||
|
$charWidth = 400; // 20% of canvas width
|
||||||
|
$bubbleWidth = 1400; // 70% of canvas width
|
||||||
|
$bubbleMargin = 100; // 5% margin on each side
|
||||||
|
$bubblePadding = 50; // Padding inside the bubble
|
||||||
|
|
||||||
|
// Define legacy styles for backward compatibility
|
||||||
$styles = [
|
$styles = [
|
||||||
'default' => [
|
'default' => [
|
||||||
'bg_color' => [245, 245, 245],
|
'bg_color' => [245, 245, 245],
|
||||||
'text_color' => [50, 50, 50],
|
'text_color' => [50, 50, 50],
|
||||||
'border_color' => [200, 200, 200],
|
'border_color' => [200, 200, 200],
|
||||||
'padding' => 20,
|
'font_size' => 24, // Increased for higher resolution
|
||||||
'rounded' => 15,
|
'line_height' => 30,
|
||||||
'font_size' => 14,
|
'font' => __DIR__ . '/fonts/NotoSans-Regular.ttf',
|
||||||
'max_width' => 600,
|
|
||||||
'line_height' => 20,
|
|
||||||
'font' => __DIR__ . '/fonts/arial.ttf', // Adjust path as needed
|
|
||||||
],
|
],
|
||||||
'modern' => [
|
'modern' => [
|
||||||
'bg_color' => [66, 133, 244],
|
'bg_color' => [66, 133, 244],
|
||||||
'text_color' => [255, 255, 255],
|
'text_color' => [255, 255, 255],
|
||||||
'border_color' => [59, 120, 220],
|
'border_color' => [59, 120, 220],
|
||||||
'padding' => 20,
|
'font_size' => 24,
|
||||||
'rounded' => 20,
|
'line_height' => 30,
|
||||||
'font_size' => 14,
|
'font' => __DIR__ . '/fonts/NotoSans-Regular.ttf',
|
||||||
'max_width' => 600,
|
|
||||||
'line_height' => 20,
|
|
||||||
'font' => __DIR__ . '/fonts/arial.ttf', // Adjust path as needed
|
|
||||||
],
|
],
|
||||||
'retro' => [
|
'retro' => [
|
||||||
'bg_color' => [255, 204, 102],
|
'bg_color' => [255, 204, 102],
|
||||||
'text_color' => [51, 51, 51],
|
'text_color' => [51, 51, 51],
|
||||||
'border_color' => [204, 153, 0],
|
'border_color' => [204, 153, 0],
|
||||||
'padding' => 20,
|
'font_size' => 24,
|
||||||
'rounded' => 5,
|
'line_height' => 30,
|
||||||
'font_size' => 14,
|
'font' => __DIR__ . '/fonts/NotoSans-Regular.ttf',
|
||||||
'max_width' => 600,
|
|
||||||
'line_height' => 20,
|
|
||||||
'font' => __DIR__ . '/fonts/arial.ttf', // Adjust path as needed
|
|
||||||
],
|
],
|
||||||
'minimal' => [
|
'minimal' => [
|
||||||
'bg_color' => [255, 255, 255],
|
'bg_color' => [255, 255, 255],
|
||||||
'text_color' => [0, 0, 0],
|
'text_color' => [0, 0, 0],
|
||||||
'border_color' => [220, 220, 220],
|
'border_color' => [220, 220, 220],
|
||||||
'padding' => 15,
|
'font_size' => 24,
|
||||||
'rounded' => 0,
|
'line_height' => 30,
|
||||||
'font_size' => 14,
|
'font' => __DIR__ . '/fonts/NotoSans-Regular.ttf',
|
||||||
'max_width' => 600,
|
|
||||||
'line_height' => 20,
|
|
||||||
'font' => __DIR__ . '/fonts/arial.ttf', // Adjust path as needed
|
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
// Use default style if specified style doesn't exist
|
// Set default styling
|
||||||
if (!isset($styles[$style])) {
|
$config = [
|
||||||
$style = 'default';
|
'bg_color' => [245, 245, 245],
|
||||||
}
|
'text_color' => [50, 50, 50],
|
||||||
|
'border_color' => [200, 200, 200],
|
||||||
|
'font_size' => 24,
|
||||||
|
'line_height' => 30,
|
||||||
|
'font' => __DIR__ . '/fonts/NotoSans-Regular.ttf',
|
||||||
|
];
|
||||||
|
|
||||||
// Get style settings
|
// Ensure required directories exist
|
||||||
$config = $styles[$style];
|
$dirs = [
|
||||||
|
__DIR__ . '/characters/Example',
|
||||||
|
__DIR__ . '/fonts'
|
||||||
|
];
|
||||||
|
|
||||||
// Check for fonts directory and create if it doesn't exist
|
foreach ($dirs as $dir) {
|
||||||
$fontsDir = __DIR__ . '/fonts';
|
if (!is_dir($dir)) {
|
||||||
if (!is_dir($fontsDir)) {
|
mkdir($dir, 0755, true);
|
||||||
mkdir($fontsDir, 0755, true);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to a built-in font if the specified font file doesn't exist
|
// Fallback to a built-in font if the specified font file doesn't exist
|
||||||
if (!file_exists($config['font'])) {
|
if (!file_exists($config['font'])) {
|
||||||
// Try to find any TTF font in the fonts directory
|
// Try to find any TTF font in the fonts directory
|
||||||
$foundFont = false;
|
$fontFiles = glob(__DIR__ . '/fonts/*.ttf');
|
||||||
if (is_dir($fontsDir)) {
|
if (!empty($fontFiles)) {
|
||||||
$fontFiles = glob($fontsDir . '/*.ttf');
|
$config['font'] = $fontFiles[0];
|
||||||
if (!empty($fontFiles)) {
|
} else {
|
||||||
$config['font'] = $fontFiles[0];
|
|
||||||
$foundFont = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no TTF font found, use built-in font
|
|
||||||
if (!$foundFont) {
|
|
||||||
$config['use_builtin_font'] = true;
|
$config['use_builtin_font'] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a temporary image to calculate text dimensions
|
// Create the canvas
|
||||||
$tempImage = imagecreatetruecolor(100, 100);
|
$canvas = imagecreatetruecolor($canvasWidth, $canvasHeight);
|
||||||
$textColor = imagecolorallocate($tempImage, $config['text_color'][0], $config['text_color'][1], $config['text_color'][2]);
|
|
||||||
|
// Set default white background
|
||||||
|
$whiteColor = imagecolorallocate($canvas, 255, 255, 255);
|
||||||
|
imagefill($canvas, 0, 0, $whiteColor);
|
||||||
|
|
||||||
|
// Set text color
|
||||||
|
$textColor = imagecolorallocate($canvas, $config['text_color'][0], $config['text_color'][1], $config['text_color'][2]);
|
||||||
|
|
||||||
|
// Function to get image path with fallback to Example
|
||||||
|
function getAssetPath($assetType, $character) {
|
||||||
|
// Primary path in character's directory
|
||||||
|
$primaryPath = __DIR__ . '/characters/' . $character . '/' . $assetType . '.png';
|
||||||
|
|
||||||
|
// Fallback to Example character
|
||||||
|
$fallbackPath = __DIR__ . '/characters/Example/' . $assetType . '.png';
|
||||||
|
|
||||||
|
return file_exists($primaryPath) ? $primaryPath : $fallbackPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for and load the background image
|
||||||
|
$backgroundPath = getAssetPath('background', $character);
|
||||||
|
$backgroundLoaded = false;
|
||||||
|
|
||||||
|
if (file_exists($backgroundPath)) {
|
||||||
|
$backgroundImage = imagecreatefrompng($backgroundPath);
|
||||||
|
if ($backgroundImage) {
|
||||||
|
// Resize if needed
|
||||||
|
$bgWidth = imagesx($backgroundImage);
|
||||||
|
$bgHeight = imagesy($backgroundImage);
|
||||||
|
|
||||||
|
// Copy background to canvas, resizing if necessary
|
||||||
|
imagecopyresampled($canvas, $backgroundImage, 0, 0, 0, 0, $canvasWidth, $canvasHeight, $bgWidth, $bgHeight);
|
||||||
|
imagedestroy($backgroundImage);
|
||||||
|
$backgroundLoaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no background was loaded, use a solid color background
|
||||||
|
if (!$backgroundLoaded) {
|
||||||
|
// Use legacy style background color if it exists
|
||||||
|
if (isset($styles[$style])) {
|
||||||
|
$bgColor = imagecolorallocate($canvas,
|
||||||
|
$styles[$style]['bg_color'][0],
|
||||||
|
$styles[$style]['bg_color'][1],
|
||||||
|
$styles[$style]['bg_color'][2]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$bgColor = imagecolorallocate($canvas, 245, 245, 245); // Default light gray
|
||||||
|
}
|
||||||
|
imagefilledrectangle($canvas, 0, 0, $canvasWidth-1, $canvasHeight-1, $bgColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for and load the character image
|
||||||
|
$characterPath = getAssetPath('character', $character);
|
||||||
|
$characterLoaded = false;
|
||||||
|
|
||||||
|
if (file_exists($characterPath)) {
|
||||||
|
$characterImage = imagecreatefrompng($characterPath);
|
||||||
|
if ($characterImage) {
|
||||||
|
// Enable alpha blending
|
||||||
|
imagesavealpha($characterImage, true);
|
||||||
|
|
||||||
|
// Calculate position (center vertically, align left)
|
||||||
|
$charWidth = imagesx($characterImage);
|
||||||
|
$charHeight = imagesy($characterImage);
|
||||||
|
$charX = $bubbleMargin;
|
||||||
|
$charY = ($canvasHeight - $charHeight) / 2;
|
||||||
|
|
||||||
|
// Copy character to canvas
|
||||||
|
imagecopy($canvas, $characterImage, $charX, $charY, 0, 0, $charWidth, $charHeight);
|
||||||
|
imagedestroy($characterImage);
|
||||||
|
$characterLoaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for and load the bubble image
|
||||||
|
$bubblePath = getAssetPath($bubble_type, $character);
|
||||||
|
$bubbleLoaded = false;
|
||||||
|
|
||||||
|
if (file_exists($bubblePath)) {
|
||||||
|
$bubbleImage = imagecreatefrompng($bubblePath);
|
||||||
|
if ($bubbleImage) {
|
||||||
|
// Enable alpha blending
|
||||||
|
imagesavealpha($bubbleImage, true);
|
||||||
|
|
||||||
|
// Calculate position (center vertically, align right of character)
|
||||||
|
$bubbleWidth = imagesx($bubbleImage);
|
||||||
|
$bubbleHeight = imagesy($bubbleImage);
|
||||||
|
$bubbleX = $charWidth + $bubbleMargin * 2;
|
||||||
|
$bubbleY = ($canvasHeight - $bubbleHeight) / 2;
|
||||||
|
|
||||||
|
// Copy bubble to canvas
|
||||||
|
imagecopy($canvas, $bubbleImage, $bubbleX, $bubbleY, 0, 0, $bubbleWidth, $bubbleHeight);
|
||||||
|
imagedestroy($bubbleImage);
|
||||||
|
$bubbleLoaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no bubble was loaded, draw a simple bubble
|
||||||
|
if (!$bubbleLoaded) {
|
||||||
|
// Calculate bubble dimensions and position
|
||||||
|
$bubbleX = $charWidth + $bubbleMargin * 2;
|
||||||
|
$bubbleY = $canvasHeight * 0.1;
|
||||||
|
$bubbleWidth = $canvasWidth - $bubbleX - $bubbleMargin;
|
||||||
|
$bubbleHeight = $canvasHeight * 0.8;
|
||||||
|
|
||||||
|
// Determine bubble style based on legacy styles if available
|
||||||
|
if (isset($styles[$style])) {
|
||||||
|
$bubbleBgColor = imagecolorallocate($canvas,
|
||||||
|
$styles[$style]['bg_color'][0],
|
||||||
|
$styles[$style]['bg_color'][1],
|
||||||
|
$styles[$style]['bg_color'][2]
|
||||||
|
);
|
||||||
|
$bubbleBorderColor = imagecolorallocate($canvas,
|
||||||
|
$styles[$style]['border_color'][0],
|
||||||
|
$styles[$style]['border_color'][1],
|
||||||
|
$styles[$style]['border_color'][2]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$bubbleBgColor = imagecolorallocate($canvas, 245, 245, 245); // Default light gray
|
||||||
|
$bubbleBorderColor = imagecolorallocate($canvas, 200, 200, 200); // Default gray border
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw bubble based on type
|
||||||
|
if ($bubble_type == 'thought') {
|
||||||
|
// Draw a thought bubble (rounded rectangle with smaller circles)
|
||||||
|
$radius = 40;
|
||||||
|
|
||||||
|
// Main bubble
|
||||||
|
imagefilledrectangle($canvas, $bubbleX + $radius, $bubbleY, $bubbleX + $bubbleWidth - $radius, $bubbleY + $bubbleHeight, $bubbleBgColor);
|
||||||
|
imagefilledrectangle($canvas, $bubbleX, $bubbleY + $radius, $bubbleX + $bubbleWidth, $bubbleY + $bubbleHeight - $radius, $bubbleBgColor);
|
||||||
|
|
||||||
|
// Corners
|
||||||
|
imagefilledarc($canvas, $bubbleX + $radius, $bubbleY + $radius, $radius * 2, $radius * 2, 180, 270, $bubbleBgColor, IMG_ARC_PIE);
|
||||||
|
imagefilledarc($canvas, $bubbleX + $bubbleWidth - $radius, $bubbleY + $radius, $radius * 2, $radius * 2, 270, 360, $bubbleBgColor, IMG_ARC_PIE);
|
||||||
|
imagefilledarc($canvas, $bubbleX + $radius, $bubbleY + $bubbleHeight - $radius, $radius * 2, $radius * 2, 90, 180, $bubbleBgColor, IMG_ARC_PIE);
|
||||||
|
imagefilledarc($canvas, $bubbleX + $bubbleWidth - $radius, $bubbleY + $bubbleHeight - $radius, $radius * 2, $radius * 2, 0, 90, $bubbleBgColor, IMG_ARC_PIE);
|
||||||
|
|
||||||
|
// Draw small circles leading to character
|
||||||
|
$circleCount = 3;
|
||||||
|
$startX = $bubbleX - 20;
|
||||||
|
$startY = $bubbleY + $bubbleHeight/2 + 20;
|
||||||
|
|
||||||
|
for ($i = 0; $i < $circleCount; $i++) {
|
||||||
|
$circleRadius = 15 - ($i * 4);
|
||||||
|
$circleX = $startX - ($i * 30);
|
||||||
|
$circleY = $startY + ($i * 20);
|
||||||
|
imagefilledellipse($canvas, $circleX, $circleY, $circleRadius*2, $circleRadius*2, $bubbleBgColor);
|
||||||
|
imageellipse($canvas, $circleX, $circleY, $circleRadius*2, $circleRadius*2, $bubbleBorderColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Border
|
||||||
|
imagerectangle($canvas, $bubbleX + $radius, $bubbleY, $bubbleX + $bubbleWidth - $radius, $bubbleY + $bubbleHeight, $bubbleBorderColor);
|
||||||
|
imagerectangle($canvas, $bubbleX, $bubbleY + $radius, $bubbleX + $bubbleWidth, $bubbleY + $bubbleHeight - $radius, $bubbleBorderColor);
|
||||||
|
imagearc($canvas, $bubbleX + $radius, $bubbleY + $radius, $radius * 2, $radius * 2, 180, 270, $bubbleBorderColor);
|
||||||
|
imagearc($canvas, $bubbleX + $bubbleWidth - $radius, $bubbleY + $radius, $radius * 2, $radius * 2, 270, 360, $bubbleBorderColor);
|
||||||
|
imagearc($canvas, $bubbleX + $radius, $bubbleY + $bubbleHeight - $radius, $radius * 2, $radius * 2, 90, 180, $bubbleBorderColor);
|
||||||
|
imagearc($canvas, $bubbleX + $bubbleWidth - $radius, $bubbleY + $bubbleHeight - $radius, $radius * 2, $radius * 2, 0, 90, $bubbleBorderColor);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Draw a speech bubble (rounded rectangle with a pointer)
|
||||||
|
$radius = 40;
|
||||||
|
|
||||||
|
// Main bubble
|
||||||
|
imagefilledrectangle($canvas, $bubbleX + $radius, $bubbleY, $bubbleX + $bubbleWidth - $radius, $bubbleY + $bubbleHeight, $bubbleBgColor);
|
||||||
|
imagefilledrectangle($canvas, $bubbleX, $bubbleY + $radius, $bubbleX + $bubbleWidth, $bubbleY + $bubbleHeight - $radius, $bubbleBgColor);
|
||||||
|
|
||||||
|
// Corners
|
||||||
|
imagefilledarc($canvas, $bubbleX + $radius, $bubbleY + $radius, $radius * 2, $radius * 2, 180, 270, $bubbleBgColor, IMG_ARC_PIE);
|
||||||
|
imagefilledarc($canvas, $bubbleX + $bubbleWidth - $radius, $bubbleY + $radius, $radius * 2, $radius * 2, 270, 360, $bubbleBgColor, IMG_ARC_PIE);
|
||||||
|
imagefilledarc($canvas, $bubbleX + $radius, $bubbleY + $bubbleHeight - $radius, $radius * 2, $radius * 2, 90, 180, $bubbleBgColor, IMG_ARC_PIE);
|
||||||
|
imagefilledarc($canvas, $bubbleX + $bubbleWidth - $radius, $bubbleY + $bubbleHeight - $radius, $radius * 2, $radius * 2, 0, 90, $bubbleBgColor, IMG_ARC_PIE);
|
||||||
|
|
||||||
|
// Draw pointer
|
||||||
|
$pointerX = [$bubbleX, $bubbleX - 40, $bubbleX];
|
||||||
|
$pointerY = [$bubbleY + $bubbleHeight/2 - 40, $bubbleY + $bubbleHeight/2, $bubbleY + $bubbleHeight/2 + 40];
|
||||||
|
imagefilledpolygon($canvas, $pointerX, $pointerY, 3, $bubbleBgColor);
|
||||||
|
|
||||||
|
// Border
|
||||||
|
imagerectangle($canvas, $bubbleX + $radius, $bubbleY, $bubbleX + $bubbleWidth - $radius, $bubbleY + $bubbleHeight, $bubbleBorderColor);
|
||||||
|
imagerectangle($canvas, $bubbleX, $bubbleY + $radius, $bubbleX + $bubbleWidth, $bubbleY + $bubbleHeight - $radius, $bubbleBorderColor);
|
||||||
|
imagearc($canvas, $bubbleX + $radius, $bubbleY + $radius, $radius * 2, $radius * 2, 180, 270, $bubbleBorderColor);
|
||||||
|
imagearc($canvas, $bubbleX + $bubbleWidth - $radius, $bubbleY + $radius, $radius * 2, $radius * 2, 270, 360, $bubbleBorderColor);
|
||||||
|
imagearc($canvas, $bubbleX + $radius, $bubbleY + $bubbleHeight - $radius, $radius * 2, $radius * 2, 90, 180, $bubbleBorderColor);
|
||||||
|
imagearc($canvas, $bubbleX + $bubbleWidth - $radius, $bubbleY + $bubbleHeight - $radius, $radius * 2, $radius * 2, 0, 90, $bubbleBorderColor);
|
||||||
|
|
||||||
|
// Pointer border
|
||||||
|
imagepolygon($canvas, $pointerX, $pointerY, 3, $bubbleBorderColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set text area dimensions
|
||||||
|
$textAreaX = $bubbleX + $bubblePadding;
|
||||||
|
$textAreaY = $bubbleY + $bubblePadding;
|
||||||
|
$textAreaWidth = $bubbleWidth - ($bubblePadding * 2);
|
||||||
|
$textAreaHeight = $bubbleHeight - ($bubblePadding * 2);
|
||||||
|
|
||||||
// Word wrap the text
|
// Word wrap the text
|
||||||
$words = explode(' ', $text);
|
$words = explode(' ', $text);
|
||||||
$lines = [];
|
$lines = [];
|
||||||
$currentLine = '';
|
$currentLine = '';
|
||||||
$maxWidth = $config['max_width'] - (2 * $config['padding']);
|
$maxWidth = $textAreaWidth;
|
||||||
|
|
||||||
foreach ($words as $word) {
|
foreach ($words as $word) {
|
||||||
$testLine = $currentLine . ' ' . $word;
|
$testLine = $currentLine . ' ' . $word;
|
||||||
@ -130,92 +336,22 @@ if ($currentLine !== '') {
|
|||||||
$lines[] = $currentLine;
|
$lines[] = $currentLine;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate image dimensions
|
|
||||||
$lineCount = count($lines);
|
|
||||||
if (isset($config['use_builtin_font'])) {
|
|
||||||
$lineHeight = imagefontheight(5);
|
|
||||||
$textHeight = $lineHeight * $lineCount;
|
|
||||||
|
|
||||||
// Find the longest line to determine width
|
|
||||||
$textWidth = 0;
|
|
||||||
foreach ($lines as $line) {
|
|
||||||
$lineWidth = imagefontwidth(5) * strlen($line);
|
|
||||||
$textWidth = max($textWidth, $lineWidth);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$lineHeight = $config['line_height'];
|
|
||||||
$textHeight = $lineHeight * $lineCount;
|
|
||||||
|
|
||||||
// Find the longest line to determine width
|
|
||||||
$textWidth = 0;
|
|
||||||
foreach ($lines as $line) {
|
|
||||||
$bbox = imagettfbbox($config['font_size'], 0, $config['font'], $line);
|
|
||||||
$lineWidth = $bbox[2] - $bbox[0];
|
|
||||||
$textWidth = max($textWidth, $lineWidth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add padding to dimensions
|
|
||||||
$imgWidth = min($config['max_width'], $textWidth + (2 * $config['padding']));
|
|
||||||
$imgHeight = $textHeight + (2 * $config['padding']);
|
|
||||||
|
|
||||||
// Create the bubble image
|
|
||||||
$image = imagecreatetruecolor($imgWidth, $imgHeight);
|
|
||||||
|
|
||||||
// Allocate colors
|
|
||||||
$bgColor = imagecolorallocate($image, $config['bg_color'][0], $config['bg_color'][1], $config['bg_color'][2]);
|
|
||||||
$borderColor = imagecolorallocate($image, $config['border_color'][0], $config['border_color'][1], $config['border_color'][2]);
|
|
||||||
$textColor = imagecolorallocate($image, $config['text_color'][0], $config['text_color'][1], $config['text_color'][2]);
|
|
||||||
|
|
||||||
// Fill background
|
|
||||||
imagefill($image, 0, 0, $bgColor);
|
|
||||||
|
|
||||||
// Draw rounded rectangle (if rounded corners are requested)
|
|
||||||
if ($config['rounded'] > 0) {
|
|
||||||
// Draw filled rectangle
|
|
||||||
imagefilledrectangle($image, 0, 0, $imgWidth - 1, $imgHeight - 1, $bgColor);
|
|
||||||
|
|
||||||
// Draw rounded corners - basic simulation for rounded corners
|
|
||||||
// This could be improved with proper arc drawing
|
|
||||||
$r = $config['rounded'];
|
|
||||||
|
|
||||||
// Top left corner
|
|
||||||
imagefilledarc($image, $r, $r, $r * 2, $r * 2, 180, 270, $bgColor, IMG_ARC_PIE);
|
|
||||||
|
|
||||||
// Top right corner
|
|
||||||
imagefilledarc($image, $imgWidth - $r - 1, $r, $r * 2, $r * 2, 270, 360, $bgColor, IMG_ARC_PIE);
|
|
||||||
|
|
||||||
// Bottom left corner
|
|
||||||
imagefilledarc($image, $r, $imgHeight - $r - 1, $r * 2, $r * 2, 90, 180, $bgColor, IMG_ARC_PIE);
|
|
||||||
|
|
||||||
// Bottom right corner
|
|
||||||
imagefilledarc($image, $imgWidth - $r - 1, $imgHeight - $r - 1, $r * 2, $r * 2, 0, 90, $bgColor, IMG_ARC_PIE);
|
|
||||||
|
|
||||||
// Draw border
|
|
||||||
imagerectangle($image, 0, 0, $imgWidth - 1, $imgHeight - 1, $borderColor);
|
|
||||||
} else {
|
|
||||||
// Draw simple rectangle
|
|
||||||
imagefilledrectangle($image, 0, 0, $imgWidth - 1, $imgHeight - 1, $bgColor);
|
|
||||||
imagerectangle($image, 0, 0, $imgWidth - 1, $imgHeight - 1, $borderColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw text
|
// Draw text
|
||||||
$y = $config['padding'];
|
$y = $textAreaY + $config['font_size']; // Start position for text
|
||||||
foreach ($lines as $line) {
|
foreach ($lines as $line) {
|
||||||
if (isset($config['use_builtin_font'])) {
|
if (isset($config['use_builtin_font'])) {
|
||||||
// Use built-in font (less nice but always available)
|
// Use built-in font (less nice but always available)
|
||||||
imagestring($image, 5, $config['padding'], $y, $line, $textColor);
|
imagestring($canvas, 5, $textAreaX, $y, $line, $textColor);
|
||||||
$y += imagefontheight(5);
|
$y += imagefontheight(5);
|
||||||
} else {
|
} else {
|
||||||
// Use TrueType font (nicer but requires font file)
|
// Use TrueType font (nicer but requires font file)
|
||||||
imagettftext($image, $config['font_size'], 0, $config['padding'], $y + $config['font_size'], $textColor, $config['font'], $line);
|
imagettftext($canvas, $config['font_size'], 0, $textAreaX, $y, $textColor, $config['font'], $line);
|
||||||
$y += $lineHeight;
|
$y += $config['line_height'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output the image
|
// Output the image
|
||||||
imagepng($image);
|
imagepng($canvas);
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
imagedestroy($image);
|
imagedestroy($canvas);
|
||||||
imagedestroy($tempImage);
|
|
Loading…
x
Reference in New Issue
Block a user