Even though the Facebook and Messenger apps are bad, they have really cute stickers. But you try to download, there is no way to download as a gif file file. Anyway, it appears already, there is no way to download. Try inspect element, we will see it appears as a spritesheet set to background-image
and use background-position
to change the frame.
This is the spritesheet.
So now we will do like this:
- Get the frames from spritesheet
- Merge into
gif
file
Split frames from spritesheet
Looking at the spritesheet up there, you can see that it has 8 frames, each size is 288px * 288px
. You can manually cut it out by any app. Or we will write a script to cut out. We will use the Canvas API to render frames from spritesheet.
Suppose we have a spritesheet loaded on a page like this
1 2 | <span class="token tag"><span class="token tag"><span class="token punctuation"><</span> img</span> <span class="token attr-name">id</span> <span class="token attr-value"><span class="token punctuation">=</span> <span class="token punctuation">"</span> spritesheet <span class="token punctuation">"</span></span> <span class="token attr-name">src</span> <span class="token attr-value"><span class="token punctuation">=</span> <span class="token punctuation">"</span> https://scontent.fhan5-7.fna.fbcdn.net/v/t39.1997-6/72568563_526222821444483_279572336263299072_n.png?_nc_cat=100&_nc_sid=0572db&_nc_oc=AQlkAKjDakbfs1blUQC66vLLLnC5bCz1Eh6KJf_9JCgjaxqJ4kO1GhPF-CAkq3MZqGX5m_ar6Gu7tbuCFn06FXnA&_nc_ht=scontent.fhan5-7.fna&oh=fff5b86d6e73bd2c11d359b4f5b63b96&oe=5EE5DA80 <span class="token punctuation">"</span></span> <span class="token punctuation">></span></span> |
To cut out each frame, we will create a canvas of size 288px * 288px
and render the corresponding part on the sprite sheet onto that canvas. For example, to cut the first frame, we do like this
1 2 3 4 5 6 7 8 9 10 11 | <span class="token keyword">const</span> spritesheet <span class="token operator">=</span> document <span class="token punctuation">.</span> <span class="token function">getElementById</span> <span class="token punctuation">(</span> <span class="token string">'spritesheet'</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token keyword">const</span> canvas <span class="token operator">=</span> document <span class="token punctuation">.</span> <span class="token function">createElement</span> <span class="token punctuation">(</span> <span class="token string">'canvas'</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> canvas <span class="token punctuation">.</span> width <span class="token operator">=</span> <span class="token number">288</span> <span class="token punctuation">;</span> canvas <span class="token punctuation">.</span> height <span class="token operator">=</span> <span class="token number">288</span> <span class="token punctuation">;</span> <span class="token keyword">const</span> ctx <span class="token operator">=</span> canvas <span class="token punctuation">.</span> <span class="token function">getContext</span> <span class="token punctuation">(</span> <span class="token string">'2d'</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> ctx <span class="token punctuation">.</span> <span class="token function">drawImage</span> <span class="token punctuation">(</span> spritesheet <span class="token punctuation">,</span> <span class="token number">0</span> <span class="token punctuation">,</span> <span class="token number">0</span> <span class="token punctuation">,</span> <span class="token number">288</span> <span class="token punctuation">,</span> <span class="token number">288</span> <span class="token punctuation">,</span> <span class="token number">0</span> <span class="token punctuation">,</span> <span class="token number">0</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> document <span class="token punctuation">.</span> body <span class="token punctuation">.</span> <span class="token function">appendChild</span> <span class="token punctuation">(</span> canvas <span class="token punctuation">)</span> <span class="token punctuation">;</span> |
You should see the first frame like this
The second and third parameters of drawImage
will be the position of the frame on the spritesheet. Document details here .
To cut all the frames, we make a loop like this
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | <span class="token keyword">const</span> frames <span class="token operator">=</span> <span class="token punctuation">[</span> <span class="token punctuation">]</span> <span class="token punctuation">;</span> <span class="token keyword">while</span> <span class="token punctuation">(</span> y <span class="token operator"><</span> spritesheet <span class="token punctuation">.</span> height <span class="token punctuation">)</span> <span class="token punctuation">{</span> x <span class="token operator">=</span> <span class="token number">0</span> <span class="token punctuation">;</span> <span class="token keyword">while</span> <span class="token punctuation">(</span> x <span class="token operator"><</span> spritesheet <span class="token punctuation">.</span> width <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">const</span> canvas <span class="token operator">=</span> document <span class="token punctuation">.</span> <span class="token function">createElement</span> <span class="token punctuation">(</span> <span class="token string">'canvas'</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> canvas <span class="token punctuation">.</span> width <span class="token operator">=</span> canvasWidth <span class="token punctuation">;</span> canvas <span class="token punctuation">.</span> height <span class="token operator">=</span> canvasHeight <span class="token punctuation">;</span> <span class="token keyword">const</span> ctx <span class="token operator">=</span> canvas <span class="token punctuation">.</span> <span class="token function">getContext</span> <span class="token punctuation">(</span> <span class="token string">'2d'</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> ctx <span class="token punctuation">.</span> <span class="token function">drawImage</span> <span class="token punctuation">(</span> spritesheet <span class="token punctuation">,</span> x <span class="token punctuation">,</span> y <span class="token punctuation">,</span> <span class="token number">288</span> <span class="token punctuation">,</span> <span class="token number">288</span> <span class="token punctuation">,</span> <span class="token number">0</span> <span class="token punctuation">,</span> <span class="token number">0</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token keyword">const</span> isEmpty <span class="token operator">=</span> ctx <span class="token punctuation">.</span> <span class="token function">getImageData</span> <span class="token punctuation">(</span> <span class="token number">0</span> <span class="token punctuation">,</span> <span class="token number">0</span> <span class="token punctuation">,</span> canvasWidth <span class="token punctuation">,</span> canvasHeight <span class="token punctuation">)</span> <span class="token punctuation">.</span> data <span class="token punctuation">.</span> <span class="token function">every</span> <span class="token punctuation">(</span> channel <span class="token operator">=></span> channel <span class="token operator">===</span> <span class="token number">0</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span> <span class="token operator">!</span> isEmpty <span class="token punctuation">)</span> <span class="token punctuation">{</span> frames <span class="token punctuation">.</span> <span class="token function">push</span> <span class="token punctuation">(</span> canvas <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> x <span class="token operator">+=</span> originalWidth <span class="token punctuation">;</span> <span class="token punctuation">}</span> y <span class="token operator">+=</span> originalHeight <span class="token punctuation">;</span> <span class="token punctuation">}</span> |
You will notice that we have an extra empty frame, so we have to add a check to see if the frame we just cut has data before adding to the array frames
. Simply check whether all its pixels have data or not.
1 2 | <span class="token keyword">const</span> isEmpty <span class="token operator">=</span> ctx <span class="token punctuation">.</span> <span class="token function">getImageData</span> <span class="token punctuation">(</span> <span class="token number">0</span> <span class="token punctuation">,</span> <span class="token number">0</span> <span class="token punctuation">,</span> canvasWidth <span class="token punctuation">,</span> canvasHeight <span class="token punctuation">)</span> <span class="token punctuation">.</span> data <span class="token punctuation">.</span> <span class="token function">every</span> <span class="token punctuation">(</span> channel <span class="token operator">=></span> channel <span class="token operator">===</span> <span class="token number">0</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> |
Merge frames into GIF image
If I have frames then I can stitch them together. I will use the gif.js
package to create gif images. Creating images from frames is as simple as this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <span class="token keyword">const</span> fps <span class="token operator">=</span> <span class="token number">8</span> <span class="token punctuation">;</span> <span class="token keyword">const</span> gif <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">GIF</span> <span class="token punctuation">(</span> <span class="token punctuation">{</span> workers <span class="token punctuation">:</span> <span class="token number">2</span> <span class="token punctuation">,</span> quality <span class="token punctuation">:</span> <span class="token number">1</span> <span class="token punctuation">,</span> <span class="token punctuation">}</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> frames <span class="token punctuation">.</span> <span class="token function">forEach</span> <span class="token punctuation">(</span> frame <span class="token operator">=></span> gif <span class="token punctuation">.</span> <span class="token function">addFrame</span> <span class="token punctuation">(</span> frame <span class="token punctuation">,</span> <span class="token punctuation">{</span> delay <span class="token punctuation">:</span> <span class="token number">1000</span> <span class="token operator">/</span> fps <span class="token punctuation">,</span> <span class="token punctuation">}</span> <span class="token punctuation">)</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> gif <span class="token punctuation">.</span> <span class="token function">on</span> <span class="token punctuation">(</span> <span class="token string">'finished'</span> <span class="token punctuation">,</span> <span class="token punctuation">(</span> blob <span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token keyword">const</span> url <span class="token operator">=</span> <span class="token constant">URL</span> <span class="token punctuation">.</span> <span class="token function">createObjectURL</span> <span class="token punctuation">(</span> blob <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token keyword">const</span> img <span class="token operator">=</span> document <span class="token punctuation">.</span> <span class="token function">createElement</span> <span class="token punctuation">(</span> <span class="token string">'img'</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> img <span class="token punctuation">.</span> <span class="token function">setAttribute</span> <span class="token punctuation">(</span> <span class="token string">'src'</span> <span class="token punctuation">,</span> url <span class="token punctuation">)</span> <span class="token punctuation">;</span> document <span class="token punctuation">.</span> body <span class="token punctuation">.</span> <span class="token function">appendChild</span> <span class="token punctuation">(</span> img <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> gif <span class="token punctuation">.</span> <span class="token function">render</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> |
Just add the frames and the delay between frames then render. To make it easy to calculate, we use the concept of frame rate, often the Facebook stickers I see have frame rate from 8-12 fps. The result will be raw data so we use URL.createObjectURL
to create a temporary URL. Our result is like this.
It’s okay, except for the black background ra. This is because our image is partially transparent, so the gif is rendered. If you add options transparent
to gif.js
like this
1 2 3 4 5 6 | <span class="token keyword">const</span> gif <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">GIF</span> <span class="token punctuation">(</span> <span class="token punctuation">{</span> workers <span class="token punctuation">:</span> <span class="token number">2</span> <span class="token punctuation">,</span> quality <span class="token punctuation">:</span> <span class="token number">1</span> <span class="token punctuation">,</span> transparent <span class="token punctuation">:</span> <span class="token string">'rgba(0, 0, 0, 0)'</span> <span class="token punctuation">,</span> <span class="token punctuation">}</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> |
Then we get results like this.
There is a transparent background but the contours are not very good. This is due to the limitation of the GIF
format. Normally, with a transparent image such as PNG, the transition from the image to the transparent place will be a lot of pixels with reduced transparency like this to make the border of the image smooth.
However, with the GIF format, each px can only be colored or completely transparent, not half transparent like PNG. So the contour looks like a low-quality serrated pattern. So a gif that has a transparent background usually has a small white border (or something that overlaps with the background where people plan to put the gif on) to make the border look smoother. For simplicity, I will give the white background as well.
1 2 3 | ctx <span class="token punctuation">.</span> fillStyle <span class="token operator">=</span> <span class="token string">'#fff'</span> <span class="token punctuation">;</span> ctx <span class="token punctuation">.</span> <span class="token function">fillRect</span> <span class="token punctuation">(</span> <span class="token number">0</span> <span class="token punctuation">,</span> <span class="token number">0</span> <span class="token punctuation">,</span> canvasWidth <span class="token punctuation">,</span> canvasHeight <span class="token punctuation">)</span> <span class="token punctuation">;</span> |
Remember to color before drawImage
or it will overwrite the image. Our result is like this.
This is the whole code if you want to play with it.
Link codepen if the embed doesn’t load https://codepen.io/thphuong/pen/qBOyRaz
Bonus
Regarding how Facebook works, why use a spritesheet without using a GIF image. Doing this also has some benefits.
- Better photos, GIF images have 256 colors instead of 16 million colors like PNG.
- No problem with transparent as I mentioned above anymore.
- No need to be copied.
But there must also be some that are not beneficial
- No resizing. Because using
background-image
andbackground-position
, the size of the sticker is fixed, to change spritesheet to change. - Running RAM with CPU.