1
|
|
|
<?php |
2
|
|
|
/** Created by Gorlum 21.01.2024 01:07 */ |
3
|
|
|
|
4
|
|
|
namespace Tools; |
5
|
|
|
|
6
|
|
|
use GIFEndec\Decoder; |
7
|
|
|
use GIFEndec\Events\FrameDecodedEvent; |
8
|
|
|
use GIFEndec\Frame; |
9
|
|
|
use GIFEndec\IO\FileStream; |
10
|
|
|
|
11
|
|
|
class SpriteLineGif extends SpriteLine { |
12
|
|
|
/** @var bool $expandFrame Should frame be expanded for CSS animation? */ |
13
|
|
|
protected $expandFrame = true; |
14
|
|
|
|
15
|
|
|
/** @var Frame[] $frames */ |
16
|
|
|
protected $frames = []; |
17
|
|
|
/** |
18
|
|
|
* @var int |
19
|
|
|
*/ |
20
|
|
|
protected $maxWidth = 0; |
21
|
|
|
|
22
|
|
|
protected function addImage($imageFile) { |
23
|
|
|
$this->files[] = $imageFile; |
24
|
|
|
|
25
|
|
|
$this->frames = []; |
26
|
|
|
|
27
|
|
|
$this->height = $this->width = $this->maxWidth = 0; |
28
|
|
|
|
29
|
|
|
/** Open GIF as FileStream */ |
30
|
|
|
// TODO - own class from loaded |
31
|
|
|
$gifStream = new FileStream($imageFile->fullPath); |
32
|
|
|
/** Create Decoder instance from MemoryStream */ |
33
|
|
|
$gifDecoder = new Decoder($gifStream); |
34
|
|
|
|
35
|
|
|
/** Run decoder. Pass callback function to process decoded Frames when they're ready. */ |
36
|
|
|
$gifDecoder->decode(function (FrameDecodedEvent $event) { |
37
|
|
|
$this->frames[] = $event->decodedFrame; |
38
|
|
|
|
39
|
|
|
$this->width += $event->decodedFrame->getSize()->getWidth(); |
40
|
|
|
$this->maxWidth = max($this->maxWidth, $event->decodedFrame->getSize()->getWidth()); |
41
|
|
|
|
42
|
|
|
$this->height = max($this->height, $event->decodedFrame->getSize()->getHeight()); |
43
|
|
|
}); |
44
|
|
|
// For EXPAND_FRAME delta width would be equal size of the largest frame |
45
|
|
|
// $this->width = count($this->frames) * reset($this->frames)->getSize()->getWidth(); |
46
|
|
|
if ($this->expandFrame) { |
47
|
|
|
$this->width = count($this->frames) * $this->maxWidth; |
48
|
|
|
} |
49
|
|
|
} |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* GIF image line considered always full |
53
|
|
|
* |
54
|
|
|
* @param $gridSize |
55
|
|
|
* |
56
|
|
|
* @return bool |
57
|
|
|
*/ |
58
|
|
|
protected function isFull($gridSize) { |
59
|
|
|
return true; |
60
|
|
|
} |
61
|
|
|
|
62
|
|
|
public function generate($posY, $scaleToPx) { |
63
|
|
|
// Extracting file name from full path |
64
|
|
|
$file = reset($this->files); |
65
|
|
|
$onlyName = explode('.', $file->fileName); |
66
|
|
|
if (count($onlyName) > 1) { |
67
|
|
|
array_pop($onlyName); |
68
|
|
|
} |
69
|
|
|
$onlyName = implode('.', $onlyName); |
70
|
|
|
// You can't have this chars in CSS qualifier |
71
|
|
|
$onlyName = str_replace(['.', '#'], '_', $onlyName); |
72
|
|
|
|
73
|
|
|
// Expanding frames. Their sizes can change due to offset |
74
|
|
|
foreach ($this->frames as $i => $frame) { |
75
|
|
|
$this->expandFrame($i); |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
// $firstFrame = reset($this->frames); |
79
|
|
|
// $this->width = imagesx($firstFrame->gdImage) * count($this->frames); |
80
|
|
|
// $this->height = imagesy($firstFrame->gdImage); |
81
|
|
|
// $maxDimension = max(imagesx($firstFrame->gdImage), imagesy($firstFrame->gdImage)); |
82
|
|
|
$maxDimension = max($this->maxWidth, $this->height); |
83
|
|
|
|
84
|
|
|
// Recreating image - if any |
85
|
|
|
unset($this->image); |
86
|
|
|
$this->image = ImageContainer::create($this->width, $this->height); |
87
|
|
|
|
88
|
|
|
$durations = []; |
89
|
|
|
$position = 0; |
90
|
|
|
foreach ($this->frames as $i => $frame) { |
91
|
|
|
// $frameGdImage = $this->expandFrame($i); |
92
|
|
|
$frameGdImage = $frame->gdImage; |
|
|
|
|
93
|
|
|
|
94
|
|
|
$width = imagesx($frameGdImage); |
95
|
|
|
$height = imagesy($frameGdImage); |
96
|
|
|
|
97
|
|
|
$this->image->copyFromGd($frameGdImage, $position, 0); |
98
|
|
|
|
99
|
|
|
// $frame = $this->frames[$i]; |
100
|
|
|
// Fixing duration 0 to 10 |
101
|
|
|
$durations[$i] = ($duration = $frame->getDuration()) ? $duration : 10; |
102
|
|
|
|
103
|
|
|
$css = "%1\$s{$onlyName}_{$i}%2\$s{background-position: -{$position}px -{$posY}px;"; |
104
|
|
|
|
105
|
|
|
// Extra info about frame |
106
|
|
|
$size = $frame->getSize(); |
107
|
|
|
$offset = $frame->getOffset(); |
108
|
|
|
$css = "/* Frame {$size->getWidth()}x{$size->getHeight()} @ ({$offset->getX()},{$offset->getY()}) duration {$frame->getDuration()} disposition {$frame->getDisposalMethod()} */" . $css; |
109
|
|
|
|
110
|
|
|
if ($scaleToPx > 0) { |
111
|
|
|
if ($maxDimension != $scaleToPx) { |
112
|
|
|
$css .= "zoom: calc({$scaleToPx}/{$maxDimension});"; |
113
|
|
|
} |
114
|
|
|
} |
115
|
|
|
$css .= "width: {$width}px;height: {$height}px;}\n"; |
116
|
|
|
|
117
|
|
|
if ($i === 0) { |
118
|
|
|
// If it's first frame - generating CSS for static image |
119
|
|
|
$css = "%1\$s{$onlyName}%2\$s,\n" . $css; |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
$this->css .= $css; |
123
|
|
|
|
124
|
|
|
$position += $width; |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
$totalDuration = array_sum($durations); |
128
|
|
|
$durInSec = round($totalDuration / 100, 4); |
129
|
|
|
|
130
|
|
|
$animation = ''; |
131
|
|
|
$cumulative = 0; |
132
|
|
|
$position = 0; |
133
|
|
|
foreach ($durations as $i => $duration) { |
134
|
|
|
$animation .= $cumulative . "%% {background-position-x: {$position}px;}\n"; |
135
|
|
|
|
136
|
|
|
$cumulative += round($duration / $totalDuration * 100, 3); |
137
|
|
|
$position -= imagesx($this->frames[$i]->gdImage); |
138
|
|
|
} |
139
|
|
|
$animation = "%1\$s{$onlyName}%2\$s {animation: {$onlyName}_animation%2\$s {$durInSec}s step-end infinite;}\n" . |
140
|
|
|
"@keyframes {$onlyName}_animation%2\$s {\n" . |
141
|
|
|
$animation . |
142
|
|
|
"}"; |
143
|
|
|
|
144
|
|
|
$this->css .= $animation; |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
/** |
148
|
|
|
* @param int $i |
149
|
|
|
* |
150
|
|
|
* @return resource|\GdImage |
151
|
|
|
*/ |
152
|
|
|
protected function expandFrame($i) { |
153
|
|
|
/** |
154
|
|
|
* Disposal method |
155
|
|
|
* Values : |
156
|
|
|
* 0 - No disposal specified. The decoder is not required to take any action. |
157
|
|
|
* 1 - Do not dispose. The graphic is to be left in place. |
158
|
|
|
* 2 - Restore to background color. The area used by the graphic must be restored to the background color. |
159
|
|
|
* 3 - Restore to previous. The decoder is required to restore the area overwritten by the graphic with |
160
|
|
|
* what was there prior to rendering the graphic. |
161
|
|
|
*/ |
162
|
|
|
$thisFrame = $this->frames[$i]; |
163
|
|
|
if (!$this->expandFrame) { |
164
|
|
|
return $thisFrame->gdImage = $thisFrame->createGDImage(); |
|
|
|
|
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
if ($i === 0) { |
168
|
|
|
// This is first frame |
169
|
|
|
$sizeX = $this->maxWidth; |
170
|
|
|
$sizeY = $this->height; |
171
|
|
|
// $sizeX = $thisFrame->getSize()->getWidth() + $thisFrame->getOffset()->getX(); |
172
|
|
|
// $sizeY = $thisFrame->getSize()->getHeight() + $thisFrame->getOffset()->getY(); |
173
|
|
|
} else { |
174
|
|
|
$prevFrame = $this->frames[$i - 1]; |
175
|
|
|
if (!in_array($prevFrame->getDisposalMethod(), [0, 1, 2])) { |
176
|
|
|
die("Disposal method {$prevFrame->getDisposalMethod()} does not supported yet"); |
|
|
|
|
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
// Creating detached copy of previous frame image |
180
|
|
|
$sizeX = imagesx($prevFrame->gdImage); |
181
|
|
|
$sizeY = imagesy($prevFrame->gdImage); |
182
|
|
|
} |
183
|
|
|
$newGdImage = imagecreatetruecolor($sizeX, $sizeY); |
184
|
|
|
imagealphablending($newGdImage, false); |
185
|
|
|
imagesavealpha($newGdImage, true); |
186
|
|
|
$color = imagecolorallocatealpha($newGdImage, 0, 0, 0, 127); |
187
|
|
|
imagefill($newGdImage, 0, 0, $color); |
188
|
|
|
|
189
|
|
|
if ($i !== 0) { |
190
|
|
|
imagecopy($newGdImage, $prevFrame->gdImage, |
|
|
|
|
191
|
|
|
0, 0, |
192
|
|
|
0, 0, imagesx($prevFrame->gdImage), imagesy($prevFrame->gdImage) |
193
|
|
|
); |
194
|
|
|
|
195
|
|
|
if ($prevFrame->getDisposalMethod() === 2) { |
196
|
|
|
imagefilledrectangle($newGdImage, |
197
|
|
|
$prevFrame->getOffset()->getX(), $prevFrame->getOffset()->getY(), |
198
|
|
|
$prevFrame->getOffset()->getX() + ($prevFrame->getSize()->getWidth() - 1), |
199
|
|
|
$prevFrame->getOffset()->getY() + ($prevFrame->getSize()->getHeight() - 1), |
200
|
|
|
$color |
201
|
|
|
); |
202
|
|
|
} |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
$anImage = $thisFrame->createGDImage(); |
206
|
|
|
imagecopy($newGdImage, $anImage, |
207
|
|
|
$thisFrame->getOffset()->getX(), $thisFrame->getOffset()->getY(), |
208
|
|
|
0, 0, imagesx($anImage), imagesy($anImage) |
209
|
|
|
); |
210
|
|
|
|
211
|
|
|
return $thisFrame->gdImage = $newGdImage; |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
} |
215
|
|
|
|