Passed
Push — develop ( 535216...2a3c68 )
by Paul
07:18
created

PixelAvatar::hslToHex()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 20
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
eloc 16
dl 0
loc 20
ccs 0
cts 15
cp 0
rs 9.7333
c 0
b 0
f 0
cc 3
nc 3
nop 3
crap 12
1
<?php
2
3
/**
4
 * Adapted from: https://github.com/BinaryMoon/wp-toolbelt/tree/master/modules/avatars.
5
 * @see: https://www.binarymoon.co.uk/2020/08/pixel-avatars-a-privacy-first-gravatar-replacement/
6
 */
7
8
namespace GeminiLabs\SiteReviews\Modules\Avatars;
9
10
use GeminiLabs\Spatie\Color\Hsl;
11
12
class PixelAvatar extends AbstractSvgAvatar
13
{
14
    public const HEIGHT = 11;
15
    public const WIDTH = 11;
16
17
    public array $data = [];
18
19
    public string $hash = '';
20
21
    public int $hashIndex = 0;
22
23
    public array $pixels = [
24
        'palette' => [
25
            'all' => [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200, 210, 215, 220, 230, 240, 250, 260, 270, 280, 290, 300, 310, 320, 330, 340, 350],
26
            'skin' => [60, 80, 100, 120, 140, 160, 180, 220, 240, 280, 300, 320, 340],
27
        ],
28
        'face' => [
29
            [
30
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
31
                [0, 0, 2, 1, 9, 1, 9, 1, 2, 0, 0],
32
                [0, 0, 2, 1, 1, 1, 1, 1, 2, 0, 0],
33
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
34
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
35
                [0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0],
36
                [0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0],
37
            ],
38
            [
39
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
40
                [0, 0, 2, 1, 9, 1, 9, 1, 2, 0, 0],
41
                [0, 0, 2, 1, 1, 1, 1, 1, 2, 0, 0],
42
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
43
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
44
                [0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0],
45
                [0, 0, 1, 0, 0, 2, 0, 0, 1, 0, 0],
46
                [0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0],
47
            ],
48
            [
49
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
50
                [0, 0, 2, 1, 9, 1, 9, 1, 2, 0, 0],
51
                [0, 0, 2, 1, 1, 1, 1, 1, 2, 0, 0],
52
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
53
                [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
54
                [0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0],
55
                [0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0],
56
            ],
57
            [
58
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
59
                [0, 2, 1, 1, 9, 1, 9, 1, 1, 2, 0],
60
                [0, 2, 1, 1, 1, 1, 1, 1, 1, 2, 0],
61
                [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
62
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
63
                [0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0],
64
                [0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0],
65
            ],
66
            [
67
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
68
                [0, 0, 2, 1, 9, 1, 9, 1, 2, 0, 0],
69
                [0, 0, 2, 1, 1, 1, 1, 1, 2, 0, 0],
70
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
71
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
72
                [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
73
                [0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0],
74
            ],
75
            [
76
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
77
                [0, 0, 2, 1, 9, 1, 9, 1, 2, 0, 0],
78
                [0, 0, 2, 1, 1, 1, 1, 1, 2, 0, 0],
79
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
80
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
81
                [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
82
                [0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0],
83
                [0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0],
84
            ],
85
            [
86
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
87
                [0, 0, 2, 1, 9, 1, 9, 1, 2, 0, 0],
88
                [0, 0, 2, 1, 1, 1, 1, 1, 2, 0, 0],
89
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
90
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
91
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
92
                [0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0],
93
            ],
94
        ],
95
        'mouth' => [
96
            [1, 1, 1],
97
            [1, 1, 1],
98
            [1, 1, 1],
99
            [0, 1, 0],
100
            [0, 1, 1],
101
            [1, 1, 0],
102
        ],
103
        'body' => [
104
            [
105
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
106
                [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
107
                [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
108
            ],
109
            [
110
                [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
111
                [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
112
                [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
113
            ],
114
            [
115
                [0, 0, 1, 1, 2, 2, 2, 1, 1, 0, 0],
116
                [0, 0, 1, 1, 2, 2, 2, 1, 1, 0, 0],
117
                [0, 0, 1, 1, 2, 2, 2, 1, 1, 0, 0],
118
            ],
119
            [
120
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
121
                [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
122
                [0, 1, 1, 2, 1, 1, 1, 2, 1, 1, 0],
123
            ],
124
            [
125
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
126
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
127
                [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
128
            ],
129
            [
130
                [0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0],
131
                [0, 0, 1, 1, 2, 2, 2, 1, 1, 0, 0],
132
                [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
133
            ],
134
            [
135
                [0, 0, 0, 1, 2, 2, 2, 1, 0, 0, 0],
136
                [0, 0, 1, 1, 2, 2, 2, 1, 1, 0, 0],
137
                [0, 0, 1, 1, 1, 2, 1, 1, 1, 0, 0],
138
            ],
139
            [
140
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
141
                [0, 0, 1, 2, 1, 2, 1, 2, 1, 0, 0],
142
                [0, 0, 1, 2, 1, 2, 1, 2, 1, 0, 0],
143
            ],
144
            [
145
                [0, 0, 0, 0, 2, 9, 2, 0, 0, 0, 0],
146
                [0, 0, 1, 1, 1, 9, 1, 1, 1, 0, 0],
147
                [0, 0, 1, 1, 1, 9, 1, 1, 1, 0, 0],
148
            ],
149
        ],
150
        'hair' => [
151
            [
152
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
153
            ],
154
            [
155
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
156
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
157
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
158
            ],
159
            [
160
                [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
161
                [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
162
                [0, 0, 0, 2, 2, 2, 2, 2, 0, 0, 0],
163
            ],
164
            [
165
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
166
                [0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0],
167
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
168
            ],
169
            [
170
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
171
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
172
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
173
                [0, 0, 0, 2, 2, 0, 2, 2, 0, 0, 0],
174
            ],
175
            [
176
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
177
                [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
178
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
179
                [0, 0, 0, 2, 2, 0, 2, 2, 0, 0, 0],
180
            ],
181
            [
182
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
183
                [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
184
                [0, 0, 0, 0, 2, 1, 2, 0, 0, 0, 0],
185
                [0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0],
186
            ],
187
            [
188
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
189
                [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
190
                [0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0],
191
            ],
192
            [
193
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
194
                [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
195
                [0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0],
196
            ],
197
            [
198
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
199
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
200
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
201
                [0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0],
202
            ],
203
            [
204
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
205
                [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
206
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
207
                [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0],
208
                [0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0],
209
            ],
210
            [
211
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
212
                [0, 0, 0, 0, 2, 1, 2, 0, 0, 0, 0],
213
                [0, 0, 0, 1, 2, 1, 2, 1, 0, 0, 0],
214
                [0, 0, 0, 2, 2, 2, 2, 2, 0, 0, 0],
215
            ],
216
            [
217
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
218
                [0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0],
219
                [0, 0, 0, 0, 2, 2, 2, 1, 0, 0, 0],
220
                [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0],
221
            ],
222
            [
223
                [0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0],
224
                [0, 0, 0, 1, 2, 2, 2, 1, 0, 0, 0],
225
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
226
            ],
227
            [
228
                [0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0],
229
                [0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0],
230
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
231
            ],
232
            [
233
                [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
234
                [0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0],
235
                [0, 0, 0, 1, 2, 2, 2, 0, 0, 0, 0],
236
                [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0],
237
            ],
238
            [
239
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
240
                [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
241
                [0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0],
242
            ],
243
            [
244
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
245
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
246
                [0, 0, 2, 2, 2, 2, 2, 2, 2, 0, 0],
247
            ],
248
            [
249
                [0, 0, 0, 8, 1, 1, 1, 0, 0, 0, 0],
250
                [0, 0, 0, 1, 8, 1, 1, 1, 0, 0, 0],
251
                [0, 0, 2, 2, 2, 2, 2, 2, 2, 0, 0],
252
            ],
253
            [
254
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
255
                [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
256
                [0, 1, 1, 1, 2, 2, 2, 1, 1, 1, 0],
257
                [0, 1, 1, 2, 0, 0, 0, 2, 1, 1, 0],
258
                [0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0],
259
                [0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0],
260
            ],
261
            [
262
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
263
                [0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0],
264
                [0, 0, 1, 2, 2, 1, 2, 2, 1, 0, 0],
265
                [0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0],
266
            ],
267
            [
268
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
269
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
270
                [0, 0, 1, 2, 2, 2, 2, 2, 1, 0, 0],
271
                [0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0],
272
                [0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0],
273
                [0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0],
274
            ],
275
            [
276
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
277
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
278
                [0, 0, 0, 1, 2, 2, 2, 1, 0, 0, 0],
279
            ],
280
            [
281
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
282
                [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
283
                [0, 0, 1, 1, 2, 2, 2, 1, 1, 0, 0],
284
                [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0],
285
            ],
286
            [
287
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
288
                [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
289
                [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
290
                [0, 0, 1, 2, 0, 0, 0, 2, 1, 0, 0],
291
                [0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0],
292
                [0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0],
293
            ],
294
            [
295
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
296
                [0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0],
297
                [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
298
                [0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0],
299
                [0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0],
300
                [0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0],
301
            ],
302
        ],
303
    ];
304
305
    public function generate(string $from): string
306
    {
307
        $this->data = $this->newData();
308
        $this->hash = $this->filename($from);
309
        $this->hashIndex = 0;
310
        $this->addBody();
311
        $this->addFace();
312
        $this->addMouth();
313
        $this->addHair();
314
        return $this->draw();
315
    }
316
317
    protected function addBody(): void
318
    {
319
        $color = $this->getColor(50, 45);
320
        $pixels = $this->getPixels('body');
321
        $yOffset = 8;
322
        for ($y = 0; $y < static::HEIGHT - $yOffset; ++$y) {
323
            for ($x = 0; $x < static::WIDTH; ++$x) {
324
                $pixelColor = $this->setPixelColour($pixels[$y][$x], $this->data[$y + $yOffset][$x], $color);
325
                $this->data[$y + $yOffset][$x] = $pixelColor;
326
            }
327
        }
328
    }
329
330
    protected function addFace(): void
331
    {
332
        $color = $this->getColor(40, 65, 'skin');
333
        $pixels = $this->getPixels('face');
334
        $numPixels = count($pixels);
335
        $yOffset = 3;
336
        for ($y = 0; $y < $numPixels; ++$y) {
337
            for ($x = 0; $x < static::WIDTH; ++$x) {
338
                $pixelColor = $this->setPixelColour($pixels[$y][$x], $this->data[$y + $yOffset][$x], $color);
339
                $this->data[$y + $yOffset][$x] = $pixelColor;
340
            }
341
        }
342
    }
343
344
    protected function addHair(): void
345
    {
346
        $color = $this->getColor(70, 45);
347
        $pixels = $this->getPixels('hair');
348
        $numPixels = count($pixels);
349
        for ($y = 0; $y < $numPixels; ++$y) {
350
            for ($x = 0; $x < static::WIDTH; ++$x) {
351
                $pixelColor = $this->setPixelColour($pixels[$y][$x], $this->data[$y][$x], $color);
352
                $this->data[$y][$x] = $pixelColor;
353
            }
354
        }
355
    }
356
357
    protected function addMouth(): void
358
    {
359
        $color = $this->getColor(60, 30);
360
        $pixels = $this->getPixels('mouth');
361
        if (1 === $pixels[0]) {
362
            $this->data[6][4] = $color[0];
363
        }
364
        if (1 === $pixels[1]) {
365
            $this->data[6][5] = $color[0];
366
        }
367
        if (1 === $pixels[2]) {
368
            $this->data[6][6] = $color[0];
369
        }
370
    }
371
372
    protected function draw(): string
373
    {
374
        $paths = [];
375
        $background = $this->getColor(85, 85);
376
        $commands = [];
377
        $commands[$background[0]] = sprintf('M0 0h%dv%dH0z', static::WIDTH, static::HEIGHT);
378
        for ($y = 0; $y < static::HEIGHT; ++$y) {
379
            for ($x = 0; $x < static::WIDTH; ++$x) {
380
                if ($fill = $this->data[$y][$x]) {
381
                    $commands[$fill] = $commands[$fill] ?? '';
382
                    $commands[$fill] .= sprintf('M%d %dh1v1H%dz', $x, $y, $x);
383
                }
384
            }
385
        }
386
        foreach ($commands as $fill => $d) {
387
            $paths[] = sprintf('<path fill="%s" d="%s"/>', $fill, $d);
388
        }
389
        return sprintf('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %d %d">%s</svg>',
390
            static::WIDTH,
391
            static::HEIGHT,
392
            implode('', $paths)
393
        );
394
    }
395
396
    protected function filename(string $from): string
397
    {
398
        $hash = md5(strtolower(trim($from)));
399
        $hash = substr($hash, 0, 15);
400
        return $hash;
401
    }
402
403
    protected function getColor(int $saturation, int $lightness, string $paletteKey = 'all'): array
404
    {
405
        $palette = $this->pixels['palette'][$paletteKey];
406
        $index = $this->indexVal() % count($palette);
407
        $hue = $palette[$index];
408
        return [
409
            (new Hsl($hue, $saturation / 100, $lightness / 100))->toHex(),
410
            (new Hsl($hue, ($saturation + 10) / 100, ($lightness - 20) / 100))->toHex(),
411
        ];
412
    }
413
414
    protected function getPixels(string $name): array
415
    {
416
        $index = $this->indexVal() % count($this->pixels[$name]);
417
        return $this->pixels[$name][$index];
418
    }
419
420
    /**
421
     * Get the value of the next character in the hash.
422
     */
423
    protected function indexVal(): int
424
    {
425
        ++$this->hashIndex;
426
        $this->hashIndex = $this->hashIndex % strlen($this->hash);
427
        return ord($this->hash[$this->hashIndex]) + (ord("\0") << 8);
428
    }
429
430
    protected function newData(): array
431
    {
432
        $data = [];
433
        for ($y = 0; $y < static::HEIGHT; ++$y) {
434
            $data[$y] = [];
435
            for ($x = 0; $x < static::WIDTH; ++$x) {
436
                $data[$y][$x] = null;
437
            }
438
        }
439
        return $data;
440
    }
441
442
    protected function setPixelColour(int $pixel, string $current, array $palette): string
443
    {
444
        $color = $current;
445
        switch ($pixel) {
446
            case 1:
447
                $color = $palette[0];
448
                break;
449
            case 2:
450
                $color = $palette[1];
451
                break;
452
            case 8:
453
                $color = '#fff';
454
                break;
455
            case 9:
456
                $color = '#000';
457
                break;
458
        }
459
        return $color;
460
    }
461
}
462