|
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
|
|
|
|