From 3fcaf25c58f1c146ec7aeb7ee7582cc21bf73932 Mon Sep 17 00:00:00 2001 From: laoyang Date: Sun, 25 Oct 2026 18:38:11 +0800 Subject: [PATCH] =?UTF-8?q?3.2.36=20=E6=96=B0=E5=A2=9E=E3=80=8C=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=A4=B4=E5=83=8F=E3=80=8D=EF=BC=9A=E6=8C=89=E9=82=AE?= =?UTF-8?q?=E7=AE=B1=E6=9C=AC=E5=9C=B0=E7=94=9F=E6=88=90=EF=BC=8C14=20?= =?UTF-8?q?=E5=A5=97=20CC0/MIT=20=E9=A3=8E=E6=A0=BC=E5=8F=AF=E9=80=89=20#5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为没有头像的访客按邮箱确定性地生成一张 SVG 头像,替代 Gravatar 那个灰底"神秘人"(cn.gravatar.com 在国内已超时,不勾加速时头像 干脆是空白)。全程纯 PHP,零外网请求、零落盘。 后台「主题设置」新增「自动头像」复选框 + 风格列表:勾上之后才 出现 14 套风格,点一下即时换 16 格预览,保存后全站生效。 实现要点(都是踩出来的): - PRNG 必须与 DiceBear 9.x 逐位一致:xorshift + hashSeed 全程跑在 JS 的 int32 语义里,PHP 64 位整数要逐步 i32() 归一; charCodeAt 按 UTF-16 码元取值,emoji 必须真做代理对拆分。 - pickComponent 有简繁两版:只有 glass/rings/shapes/thumbs 额外取 三次整数(rotation/offsetX/offsetY)。给简版也补三次 ⇒ PRNG 多走 3N 步 ⇒ 31 套里 27 套全错。编译期算 pc_has_ints 才治得住。 - 自带人类调色板:官方默认配色几乎全单值,30 个邮箱只出 2 种配色, 那才是"默认头像丑"的真因,光换画风解决不了。 - 只收录 CC0/MIT(13 CC0 + 1 MIT),CC BY 那 13 套要求署名,一律不收—— 主题装给用户用,用户不会维护署名,收进来等于转嫁合规义务。 接入 get_avatar 的两个关键修正: - 必须先看原 src 是不是 Gravatar 家族域名再决定是否替换。原先无条件 替换会把媒体库头像、外链头像、别的插件处理过的 data: 头像一起盖掉。 - 必须删 srcset。只改 src 留着 srcset,浏览器优先用 srcset, 生成的图根本不会被加载(现象是"代码明明换了,页面还是老样子"), 且不能重建 标签(会丢掉评论头像 CSS 依赖的 class)。 后台三处必须这么做: - body class 由 PHP 输出(admin_body_class),否则每次进设置页/保存后 重载都会先显示风格列表再藏起来,闪一下。 - 事件监听 radio 的 change,不能监听 label 的 click —— label 会把 click 转发给关联 input,冒泡回来再触发一次 ⇒ 同一风格发两次请求 (实测 hits 2→4)。 - 首屏已渲染的 16 格写进 JS 缓存,否则点一圈回到默认风格会白请求一次。 不依赖任何可选扩展:不用 mbstring(未安装时 mb_substr 是 Fatal error, 整站白屏)、不用 gd/imagick。UTF-8 切分/截断/大写全部自己实现。 PHP 7.0 语法,PHP 8.5 下 E_ALL 无任何 warning。 验证: - 14 套 × 6 种子 = 84 个用例与 DiceBear 官方 Node 输出逐字节一致, PHP 8.4 / 8.5 / 无 mbstring(php -n)三种环境均 84/0。 - 静态守卫 55/0;反向验证 17 项注入全部被捕获。 - 后台面板浏览器验证 26/0(显隐、几何、点选、缓存、连点、零报错)。 - 包内内容契约 24/0;包校验反向验证 6 项注入全部被捕获。 - 单次渲染稳态 0.047~0.105ms,峰值内存 2~4MB。 --- functions.php | 6 + inc/avatar-styles/glass.php | 83 + inc/avatar-styles/icons.php | 195 +++ inc/avatar-styles/identicon.php | 133 ++ inc/avatar-styles/initials.php | 40 + inc/avatar-styles/lorelei.php | 327 ++++ inc/avatar-styles/lorelei_neutral.php | 199 +++ inc/avatar-styles/notionists.php | 366 +++++ inc/avatar-styles/notionists_neutral.php | 177 ++ inc/avatar-styles/open_peeps.php | 209 +++ inc/avatar-styles/pixel_art.php | 291 ++++ inc/avatar-styles/pixel_art_neutral.php | 129 ++ inc/avatar-styles/rings.php | 137 ++ inc/avatar-styles/shapes.php | 109 ++ inc/avatar-styles/thumbs.php | 144 ++ inc/theme-avatar.php | 1906 ++++++++++++++++++++++ inc/theme-options.php | 34 +- style.css | 2 +- 18 files changed, 4485 insertions(+), 2 deletions(-) create mode 100644 inc/avatar-styles/glass.php create mode 100644 inc/avatar-styles/icons.php create mode 100644 inc/avatar-styles/identicon.php create mode 100644 inc/avatar-styles/initials.php create mode 100644 inc/avatar-styles/lorelei.php create mode 100644 inc/avatar-styles/lorelei_neutral.php create mode 100644 inc/avatar-styles/notionists.php create mode 100644 inc/avatar-styles/notionists_neutral.php create mode 100644 inc/avatar-styles/open_peeps.php create mode 100644 inc/avatar-styles/pixel_art.php create mode 100644 inc/avatar-styles/pixel_art_neutral.php create mode 100644 inc/avatar-styles/rings.php create mode 100644 inc/avatar-styles/shapes.php create mode 100644 inc/avatar-styles/thumbs.php create mode 100644 inc/theme-avatar.php diff --git a/functions.php b/functions.php index ad70f6e..7b994cc 100644 --- a/functions.php +++ b/functions.php @@ -63,6 +63,12 @@ require get_template_directory() . '/inc/theme-dogecloud.php'; // 挂在 template_redirect 上开输出缓冲,所以本文件晚加载不影响生效。 require get_template_directory() . '/inc/theme-cdn.php'; +// 自动头像:为没有头像的访客按邮箱生成一张(纯 PHP,零外网请求)。 +// 必须排在 theme-core.php 之后:它要用 kratos_option。 +// 它挂在 get_avatar 过滤器上(优先级 20,晚于 theme-core.php 里 +// 那个改 Gravatar 域名的过滤器),所以要保证 theme-core.php 先加载。 +require get_template_directory() . '/inc/theme-avatar.php'; + // 邮件模板:必须先于 theme-mail.php 加载, // 因为后者在发信时会调用 yang_mail_render() require get_template_directory() . '/inc/theme-mail-template.php'; diff --git a/inc/avatar-styles/glass.php b/inc/avatar-styles/glass.php new file mode 100644 index 0000000..a7b5f63 --- /dev/null +++ b/inc/avatar-styles/glass.php @@ -0,0 +1,83 @@ + 'glass', +'meta' => array( + 'title' => 'Glass', + 'creator' => 'DiceBear', + 'source' => 'https://www.dicebear.com', + 'license_name' => 'CC0 1.0', + 'license_url' => 'https://creativecommons.org/publicdomain/zero/1.0/', +), +'view_box' => '0 0 100 100', +'clip_default' => true, +'has_pre_create' => false, +'has_post_create' => false, +'is_initials' => false, +'attr_pairs' => array( + array('viewBox', '0 0 100 100'), + array('fill', 'none'), + array('shape-rendering', 'auto'), +), +'bg_colors' => array('eb4747', 'eb6247', 'eb7e47', 'eb9947', 'ebb447', 'ebd047', 'ebeb47', 'd0eb47', 'b4eb47', '99eb47', '7eeb47', '62eb47', '47eb47', '47eb62', '47eb7e', '47eb99', '47ebb4', '47ebd0', '47ebeb', '47d0eb', '47b4eb', '4799eb', '477eeb', '4762eb', '4747eb', '6247eb', '7e47eb', '9947eb', 'b447eb', 'd047eb', 'eb47eb', 'eb47d0', 'eb4799', 'eb477e', 'eb4762'), +'bg_type' => array('solid'), +'bg_rotation' => array('0', '360'), +'body_template' => '@@k:shape2@@@@k:shape1@@', +'pick_order' => array('shape2', 'shape1'), +'bool_order' => array(), +'color_order' => array( +), +'pc_has_ints' => true, +'pc_meta' => array( + array( + 'group' => 'shape2', + 'values_opt' => 'shape2', + 'width' => 150, + 'height' => 150, + 'rotation' => array('opt' => 'shape2Rotation', 'fallback' => array('0')), + 'offsetX' => array('opt' => 'shape2OffsetX', 'fallback' => array('0')), + 'offsetY' => array('opt' => 'shape2OffsetY', 'fallback' => array('0')), + ), + array( + 'group' => 'shape1', + 'values_opt' => 'shape1', + 'width' => 150, + 'height' => 150, + 'rotation' => array('opt' => 'shape1Rotation', 'fallback' => array('0')), + 'offsetX' => array('opt' => 'shape1OffsetX', 'fallback' => array('0')), + 'offsetY' => array('opt' => 'shape1OffsetY', 'fallback' => array('0')), + ), +), +'defaults' => array( + 'backgroundColor' => array('eb4747', 'eb6247', 'eb7e47', 'eb9947', 'ebb447', 'ebd047', 'ebeb47', 'd0eb47', 'b4eb47', '99eb47', '7eeb47', '62eb47', '47eb47', '47eb62', '47eb7e', '47eb99', '47ebb4', '47ebd0', '47ebeb', '47d0eb', '47b4eb', '4799eb', '477eeb', '4762eb', '4747eb', '6247eb', '7e47eb', '9947eb', 'b447eb', 'd047eb', 'eb47eb', 'eb47d0', 'eb4799', 'eb477e', 'eb4762'), + 'shape1' => array('g', 'r', 'a', 'd', 'i', 'e', 'n', 't'), + 'shape1OffsetX' => array('-80', '80'), + 'shape1OffsetY' => array('-80', '80'), + 'shape1Rotation' => array('-180', '180'), + 'shape2' => array('g', 'r', 'a', 'd', 'i', 'e', 'n', 't'), + 'shape2OffsetX' => array('-80', '80'), + 'shape2OffsetY' => array('-80', '80'), + 'shape2Rotation' => array('-160', '160'), +), +'groups' => array( + 'shape2' => array( + 'g' => '', + 'r' => '', + 'a' => '', + 'd' => '', + 'i' => '', + 'e' => '', + 'n' => '', + 't' => '', + ), + 'shape1' => array( + 'g' => '', + 'r' => '', + 'a' => '', + 'd' => '', + 'i' => '', + 'e' => '', + 'n' => '', + 't' => '', + ), +), +); \ No newline at end of file diff --git a/inc/avatar-styles/icons.php b/inc/avatar-styles/icons.php new file mode 100644 index 0000000..ce8daab --- /dev/null +++ b/inc/avatar-styles/icons.php @@ -0,0 +1,195 @@ + 'icons', +'meta' => array( + 'title' => 'Bootstrap Icons', + 'creator' => 'The Bootstrap Authors', + 'source' => 'https://github.com/twbs/icons', + 'license_name' => 'MIT', + 'license_url' => 'https://github.com/twbs/icons/blob/main/LICENSE', +), +'view_box' => '0 0 24 24', +'clip_default' => true, +'has_pre_create' => false, +'has_post_create' => false, +'is_initials' => false, +'attr_pairs' => array( + array('viewBox', '0 0 24 24'), + array('fill', 'none'), + array('shape-rendering', 'auto'), +), +'bg_colors' => array('ffe082', '90caf9', '80deea', 'ffab91', 'b39ddb', 'a5d6a7', '9fa8da', '81d4fa', 'c5e1a5', 'e6ee9c', 'ffcc80', 'f48fb1', 'ce93d8', 'ef9a9a', '80cbc4'), +'bg_type' => array('solid'), +'bg_rotation' => array('0', '360'), +'body_template' => '@@k:icon@@', +'pick_order' => array('icon'), +'bool_order' => array(), +'color_order' => array( +), +'pc_has_ints' => false, +'pc_meta' => array( + array( + 'group' => 'icon', + 'values_opt' => 'icon', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), +), +'defaults' => array( + 'backgroundColor' => array('ffe082', '90caf9', '80deea', 'ffab91', 'b39ddb', 'a5d6a7', '9fa8da', '81d4fa', 'c5e1a5', 'e6ee9c', 'ffcc80', 'f48fb1', 'ce93d8', 'ef9a9a', '80cbc4'), + 'icon' => array('alarm', 'archive', 'award', 'bag', 'bandaid', 'bank', 'basket', 'basket2', 'basket3', 'bell', 'bicycle', 'binoculars', 'book', 'bookshelf', 'boombox', 'boxSeam', 'box', 'boxes', 'bricks', 'briefcase', 'brightnessHigh', 'brush', 'bucket', 'bug', 'building', 'calculator', 'cameraReels', 'camera', 'cart2', 'cashCoin', 'clock', 'cloudDrizzle', 'cloudMoon', 'cloudSnow', 'cloud', 'clouds', 'coin', 'compass', 'controller', 'cupStraw', 'cup', 'dice5', 'disc', 'display', 'doorClosed', 'doorOpen', 'dpad', 'droplet', 'easel', 'eggFried', 'egg', 'emojiHeartEyes', 'emojiLaughing', 'emojiSmileUpsideDown', 'emojiSmile', 'emojiSunglasses', 'emojiWink', 'envelope', 'eyeglasses', 'flag', 'flower1', 'flower2', 'flower3', 'gem', 'gift', 'globe', 'globe2', 'handThumbsUp', 'handbag', 'hdd', 'heart', 'hourglassSplit', 'hourglass', 'houseDoor', 'house', 'inbox', 'inboxes', 'key', 'keyboard', 'ladder', 'lamp', 'laptop', 'lightbulb', 'lightningCharge', 'lightning', 'lock', 'magic', 'mailbox', 'map', 'megaphone', 'minecartLoaded', 'minecart', 'moonStars', 'moon', 'mortarboard', 'mouse', 'mouse2', 'newspaper', 'paintBucket', 'palette', 'palette2', 'paperclip', 'pen', 'pencil', 'phone', 'piggyBank', 'pinAngle', 'plug', 'printer', 'projector', 'puzzle', 'router', 'scissors', 'sdCard', 'search', 'send', 'shopWindow', 'shop', 'signpost2', 'signpostSplit', 'signpost', 'smartwatch', 'snow', 'snow2', 'snow3', 'speaker', 'star', 'stoplights', 'stopwatch', 'sun', 'tablet', 'thermometer', 'ticketPerforated', 'tornado', 'trash', 'trash2', 'tree', 'trophy', 'truckFlatbed', 'truck', 'tsunami', 'umbrella', 'wallet', 'wallet2', 'watch', 'webcam'), +), +'groups' => array( + 'icon' => array( + 'alarm' => '', + 'archive' => '', + 'award' => '', + 'bag' => '', + 'bandaid' => '', + 'bank' => '', + 'basket' => '', + 'basket2' => '', + 'basket3' => '', + 'bell' => '', + 'bicycle' => '', + 'binoculars' => '', + 'book' => '', + 'bookshelf' => '', + 'boombox' => '', + 'boxSeam' => '', + 'box' => '', + 'boxes' => '', + 'bricks' => '', + 'briefcase' => '', + 'brightnessHigh' => '', + 'brush' => '', + 'bucket' => '', + 'bug' => '', + 'building' => '', + 'calculator' => '', + 'cameraReels' => '', + 'camera' => '', + 'cart2' => '', + 'cashCoin' => '', + 'clock' => '', + 'cloudDrizzle' => '', + 'cloudMoon' => '', + 'cloudSnow' => '', + 'cloud' => '', + 'clouds' => '', + 'coin' => '', + 'compass' => '', + 'controller' => '', + 'cupStraw' => '', + 'cup' => '', + 'dice5' => '', + 'disc' => '', + 'display' => '', + 'doorClosed' => '', + 'doorOpen' => '', + 'dpad' => '', + 'droplet' => '', + 'easel' => '', + 'eggFried' => '', + 'egg' => '', + 'emojiHeartEyes' => '', + 'emojiLaughing' => '', + 'emojiSmileUpsideDown' => '', + 'emojiSmile' => '', + 'emojiSunglasses' => '', + 'emojiWink' => '', + 'envelope' => '', + 'eyeglasses' => '', + 'flag' => '', + 'flower1' => '', + 'flower2' => '', + 'flower3' => '', + 'gem' => '', + 'gift' => '', + 'globe' => '', + 'globe2' => '', + 'handThumbsUp' => '', + 'handbag' => '', + 'hdd' => '', + 'heart' => '', + 'hourglassSplit' => '', + 'hourglass' => '', + 'houseDoor' => '', + 'house' => '', + 'inbox' => '', + 'inboxes' => '', + 'key' => '', + 'keyboard' => '', + 'ladder' => '', + 'lamp' => '', + 'laptop' => '', + 'lightbulb' => '', + 'lightningCharge' => '', + 'lightning' => '', + 'lock' => '', + 'magic' => '', + 'mailbox' => '', + 'map' => '', + 'megaphone' => '', + 'minecartLoaded' => '', + 'minecart' => '', + 'moonStars' => '', + 'moon' => '', + 'mortarboard' => '', + 'mouse' => '', + 'mouse2' => '', + 'newspaper' => '', + 'paintBucket' => '', + 'palette' => '', + 'palette2' => '', + 'paperclip' => '', + 'pen' => '', + 'pencil' => '', + 'phone' => '', + 'piggyBank' => '', + 'pinAngle' => '', + 'plug' => '', + 'printer' => '', + 'projector' => '', + 'puzzle' => '', + 'router' => '', + 'scissors' => '', + 'sdCard' => '', + 'search' => '', + 'send' => '', + 'shopWindow' => '', + 'shop' => '', + 'signpost2' => '', + 'signpostSplit' => '', + 'signpost' => '', + 'smartwatch' => '', + 'snow' => '', + 'snow2' => '', + 'snow3' => '', + 'speaker' => '', + 'star' => '', + 'stoplights' => '', + 'stopwatch' => '', + 'sun' => '', + 'tablet' => '', + 'thermometer' => '', + 'ticketPerforated' => '', + 'tornado' => '', + 'trash' => '', + 'trash2' => '', + 'tree' => '', + 'trophy' => '', + 'truckFlatbed' => '', + 'truck' => '', + 'tsunami' => '', + 'umbrella' => '', + 'wallet' => '', + 'wallet2' => '', + 'watch' => '', + 'webcam' => '', + ), +), +); \ No newline at end of file diff --git a/inc/avatar-styles/identicon.php b/inc/avatar-styles/identicon.php new file mode 100644 index 0000000..6efca02 --- /dev/null +++ b/inc/avatar-styles/identicon.php @@ -0,0 +1,133 @@ + 'identicon', +'meta' => array( + 'title' => 'Identicon', + 'creator' => 'DiceBear', + 'source' => 'https://www.dicebear.com', + 'license_name' => 'CC0 1.0', + 'license_url' => 'https://creativecommons.org/publicdomain/zero/1.0/', +), +'view_box' => '0 0 5 5', +'clip_default' => true, +'has_pre_create' => false, +'has_post_create' => false, +'is_initials' => false, +'attr_pairs' => array( + array('viewBox', '0 0 5 5'), + array('fill', 'none'), + array('shape-rendering', 'crispEdges'), +), +'bg_colors' => array(), +'bg_type' => array('solid'), +'bg_rotation' => array('0', '360'), +'body_template' => '@@k:row1@@@@k:row2@@@@k:row3@@@@k:row4@@@@k:row5@@', +'pick_order' => array('row1', 'row2', 'row3', 'row4', 'row5'), +'bool_order' => array(), +'color_order' => array( + 'row' => 'rowColor', +), +'pc_has_ints' => false, +'pc_meta' => array( + array( + 'group' => 'row1', + 'values_opt' => 'row1', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'row2', + 'values_opt' => 'row2', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'row3', + 'values_opt' => 'row3', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'row4', + 'values_opt' => 'row4', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'row5', + 'values_opt' => 'row5', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), +), +'defaults' => array( + 'row1' => array('xooox', 'xxoxx', 'xoxox', 'oxxxo', 'xxxxx', 'oxoxo', 'ooxoo'), + 'row2' => array('xooox', 'xxoxx', 'xoxox', 'oxxxo', 'xxxxx', 'oxoxo', 'ooxoo'), + 'row3' => array('xooox', 'xxoxx', 'xoxox', 'oxxxo', 'xxxxx', 'oxoxo', 'ooxoo'), + 'row4' => array('xooox', 'xxoxx', 'xoxox', 'oxxxo', 'xxxxx', 'oxoxo', 'ooxoo'), + 'row5' => array('xooox', 'xxoxx', 'xoxox', 'oxxxo', 'xxxxx', 'oxoxo', 'ooxoo'), + 'rowColor' => array('e53935', 'ffb300', '1e88e5', '546e7a', '6d4c41', '00acc1', 'f4511e', '5e35b1', '43a047', '757575', '3949ab', '039be5', '7cb342', 'c0ca33', 'fb8c00', 'd81b60', '8e24aa', '00897b', 'fdd835'), +), +'groups' => array( + 'row1' => array( + 'xooox' => '', + 'xxoxx' => '', + 'xoxox' => '', + 'oxxxo' => '', + 'xxxxx' => '', + 'oxoxo' => '', + 'ooxoo' => '', + ), + 'row2' => array( + 'xooox' => '', + 'xxoxx' => '', + 'xoxox' => '', + 'oxxxo' => '', + 'xxxxx' => '', + 'oxoxo' => '', + 'ooxoo' => '', + ), + 'row3' => array( + 'xooox' => '', + 'xxoxx' => '', + 'xoxox' => '', + 'oxxxo' => '', + 'xxxxx' => '', + 'oxoxo' => '', + 'ooxoo' => '', + ), + 'row4' => array( + 'xooox' => '', + 'xxoxx' => '', + 'xoxox' => '', + 'oxxxo' => '', + 'xxxxx' => '', + 'oxoxo' => '', + 'ooxoo' => '', + ), + 'row5' => array( + 'xooox' => '', + 'xxoxx' => '', + 'xoxox' => '', + 'oxxxo' => '', + 'xxxxx' => '', + 'oxoxo' => '', + 'ooxoo' => '', + ), +), +); \ No newline at end of file diff --git a/inc/avatar-styles/initials.php b/inc/avatar-styles/initials.php new file mode 100644 index 0000000..88be9e5 --- /dev/null +++ b/inc/avatar-styles/initials.php @@ -0,0 +1,40 @@ + 'initials', +'meta' => array( + 'title' => 'Initials', + 'creator' => 'DiceBear', + 'source' => 'https://github.com/dicebear/dicebear', + 'license_name' => 'CC0 1.0', + 'license_url' => 'https://creativecommons.org/publicdomain/zero/1.0/', +), +'view_box' => '0 0 100 100', +'clip_default' => true, +'has_pre_create' => false, +'has_post_create' => false, +'is_initials' => true, +'attr_pairs' => array( + array('viewBox', '0 0 100 100'), +), +'bg_colors' => array('e53935', 'd81b60', '8e24aa', '5e35b1', '3949ab', '1e88e5', '039be5', '00acc1', '00897b', '43a047', '7cb342', 'c0ca33', 'fdd835', 'ffb300', 'fb8c00', 'f4511e'), +'bg_type' => array('solid'), +'bg_rotation' => array('0', '360'), +'body_template' => '', +'pick_order' => array(), +'bool_order' => array(), +'color_order' => array( +), +'pc_has_ints' => false, +'pc_meta' => array( +), +'defaults' => array( + 'backgroundColor' => array('e53935', 'd81b60', '8e24aa', '5e35b1', '3949ab', '1e88e5', '039be5', '00acc1', '00897b', '43a047', '7cb342', 'c0ca33', 'fdd835', 'ffb300', 'fb8c00', 'f4511e'), + 'textColor' => array('ffffff'), + 'fontFamily' => array('Arial', 'sans-serif'), + 'fontSize' => 50, + 'chars' => 2, + 'fontWeight' => 400, +), +'groups' => array( +), +); \ No newline at end of file diff --git a/inc/avatar-styles/lorelei.php b/inc/avatar-styles/lorelei.php new file mode 100644 index 0000000..bf30847 --- /dev/null +++ b/inc/avatar-styles/lorelei.php @@ -0,0 +1,327 @@ + 'lorelei', +'meta' => array( + 'title' => 'Lorelei', + 'creator' => 'Lisa Wischofsky', + 'source' => 'https://www.figma.com/community/file/1198749693280469639', + 'license_name' => 'CC0 1.0', + 'license_url' => 'https://creativecommons.org/publicdomain/zero/1.0/', +), +'view_box' => '0 0 980 980', +'clip_default' => true, +'has_pre_create' => false, +'has_post_create' => true, +'is_initials' => false, +'attr_pairs' => array( + array('viewBox', '0 0 980 980'), + array('fill', 'none'), + array('shape-rendering', 'auto'), +), +'bg_colors' => array(), +'bg_type' => array('solid'), +'bg_rotation' => array('0', '360'), +'body_template' => '@@k:hair@@@@k:hairAccessories@@', +'pick_order' => array('hair', 'hairAccessories', 'head', 'eyes', 'eyebrows', 'earrings', 'freckles', 'nose', 'beard', 'mouth', 'glasses'), +'bool_order' => array('hairAccessories', 'earrings', 'freckles', 'beard', 'glasses'), +'color_order' => array( + 'hair' => 'hairColor', + 'skin' => 'skinColor', + 'earrings' => 'earringsColor', + 'eyebrows' => 'eyebrowsColor', + 'eyes' => 'eyesColor', + 'freckles' => 'frecklesColor', + 'glasses' => 'glassesColor', + 'mouth' => 'mouthColor', + 'nose' => 'noseColor', + 'hairAccessories' => 'hairAccessoriesColor', +), +'pc_has_ints' => false, +'pc_meta' => array( + array( + 'group' => 'hair', + 'values_opt' => 'hair', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'hairAccessories', + 'values_opt' => 'hairAccessories', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'head', + 'values_opt' => 'head', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'eyes', + 'values_opt' => 'eyes', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'eyebrows', + 'values_opt' => 'eyebrows', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'earrings', + 'values_opt' => 'earrings', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'freckles', + 'values_opt' => 'freckles', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'nose', + 'values_opt' => 'nose', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'beard', + 'values_opt' => 'beard', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'mouth', + 'values_opt' => 'mouth', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'glasses', + 'values_opt' => 'glasses', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), +), +'defaults' => array( + 'beard' => array('variant01', 'variant02'), + 'beardProbability' => 5, + 'earrings' => array('variant01', 'variant02', 'variant03'), + 'earringsColor' => array('000000'), + 'earringsProbability' => 10, + 'eyebrows' => array('variant13', 'variant12', 'variant11', 'variant10', 'variant09', 'variant08', 'variant07', 'variant06', 'variant05', 'variant04', 'variant03', 'variant02', 'variant01'), + 'eyebrowsColor' => array('000000'), + 'eyes' => array('variant24', 'variant23', 'variant22', 'variant21', 'variant20', 'variant19', 'variant18', 'variant17', 'variant16', 'variant15', 'variant14', 'variant13', 'variant12', 'variant11', 'variant10', 'variant09', 'variant08', 'variant07', 'variant06', 'variant05', 'variant04', 'variant03', 'variant02', 'variant01'), + 'eyesColor' => array('000000'), + 'freckles' => array('variant01'), + 'frecklesColor' => array('000000'), + 'frecklesProbability' => 5, + 'glasses' => array('variant01', 'variant02', 'variant03', 'variant04', 'variant05'), + 'glassesColor' => array('000000'), + 'glassesProbability' => 10, + 'hair' => array('variant48', 'variant47', 'variant46', 'variant45', 'variant44', 'variant43', 'variant42', 'variant41', 'variant40', 'variant39', 'variant38', 'variant37', 'variant36', 'variant35', 'variant34', 'variant33', 'variant32', 'variant31', 'variant30', 'variant29', 'variant28', 'variant27', 'variant26', 'variant25', 'variant24', 'variant23', 'variant22', 'variant21', 'variant20', 'variant19', 'variant18', 'variant17', 'variant16', 'variant15', 'variant14', 'variant13', 'variant12', 'variant11', 'variant10', 'variant09', 'variant08', 'variant07', 'variant06', 'variant05', 'variant04', 'variant03', 'variant02', 'variant01'), + 'hairAccessories' => array('flowers'), + 'hairAccessoriesColor' => array('000000'), + 'hairAccessoriesProbability' => 5, + 'hairColor' => array('000000'), + 'head' => array('variant04', 'variant03', 'variant02', 'variant01'), + 'mouth' => array('happy01', 'happy02', 'happy03', 'happy04', 'happy05', 'happy06', 'happy07', 'happy08', 'happy18', 'happy09', 'happy10', 'happy11', 'happy12', 'happy13', 'happy14', 'happy17', 'happy15', 'happy16', 'sad01', 'sad02', 'sad03', 'sad04', 'sad05', 'sad06', 'sad07', 'sad08', 'sad09'), + 'mouthColor' => array('000000'), + 'nose' => array('variant01', 'variant02', 'variant03', 'variant04', 'variant05', 'variant06'), + 'noseColor' => array('000000'), + 'skinColor' => array('ffffff'), +), +'groups' => array( + 'hair' => array( + 'variant48' => '@@k:head@@', + 'variant47' => '@@k:head@@', + 'variant46' => '@@k:head@@', + 'variant45' => '@@k:head@@', + 'variant44' => '@@k:head@@', + 'variant43' => '@@k:head@@', + 'variant42' => '@@k:head@@', + 'variant41' => '@@k:head@@', + 'variant40' => '@@k:head@@', + 'variant39' => '@@k:head@@', + 'variant38' => '@@k:head@@', + 'variant37' => '@@k:head@@', + 'variant36' => '@@k:head@@', + 'variant35' => '@@k:head@@', + 'variant34' => '@@k:head@@', + 'variant33' => '@@k:head@@', + 'variant32' => '@@k:head@@', + 'variant31' => '@@k:head@@', + 'variant30' => '@@k:head@@', + 'variant29' => '@@k:head@@', + 'variant28' => '@@k:head@@', + 'variant27' => '@@k:head@@', + 'variant26' => '@@k:head@@', + 'variant25' => '@@k:head@@', + 'variant24' => '@@k:head@@', + 'variant23' => '@@k:head@@', + 'variant22' => '@@k:head@@', + 'variant21' => '@@k:head@@', + 'variant20' => '@@k:head@@', + 'variant19' => '@@k:head@@', + 'variant18' => '@@k:head@@', + 'variant17' => '@@k:head@@', + 'variant16' => '@@k:head@@', + 'variant15' => '@@k:head@@', + 'variant14' => '@@k:head@@', + 'variant13' => '@@k:head@@', + 'variant12' => '@@k:head@@', + 'variant11' => '@@k:head@@', + 'variant10' => '@@k:head@@', + 'variant09' => '@@k:head@@', + 'variant08' => '@@k:head@@', + 'variant07' => '@@k:head@@', + 'variant06' => '@@k:head@@', + 'variant05' => '@@k:head@@', + 'variant04' => '@@k:head@@', + 'variant03' => '@@k:head@@', + 'variant02' => '@@k:head@@', + 'variant01' => '@@k:head@@', + ), + 'hairAccessories' => array( + 'flowers' => '', + ), + 'head' => array( + 'variant04' => '@@k:eyes@@@@k:eyebrows@@@@k:earrings@@@@k:freckles@@@@k:nose@@@@k:beard@@@@k:mouth@@@@k:glasses@@', + 'variant03' => '@@k:eyes@@@@k:eyebrows@@@@k:earrings@@@@k:freckles@@@@k:nose@@@@k:beard@@@@k:mouth@@@@k:glasses@@', + 'variant02' => '@@k:eyes@@@@k:eyebrows@@@@k:earrings@@@@k:freckles@@@@k:nose@@@@k:beard@@@@k:mouth@@@@k:glasses@@', + 'variant01' => '@@k:eyes@@@@k:eyebrows@@@@k:earrings@@@@k:freckles@@@@k:nose@@@@k:beard@@@@k:mouth@@@@k:glasses@@', + ), + 'eyes' => array( + 'variant24' => '', + 'variant23' => '', + 'variant22' => '', + 'variant21' => '', + 'variant20' => '', + 'variant19' => '', + 'variant18' => '', + 'variant17' => '', + 'variant16' => '', + 'variant15' => '', + 'variant14' => '', + 'variant13' => '', + 'variant12' => '', + 'variant11' => '', + 'variant10' => '', + 'variant09' => '', + 'variant08' => '', + 'variant07' => '', + 'variant06' => '', + 'variant05' => '', + 'variant04' => '', + 'variant03' => '', + 'variant02' => '', + 'variant01' => '', + ), + 'eyebrows' => array( + 'variant13' => '', + 'variant12' => '', + 'variant11' => '', + 'variant10' => '', + 'variant09' => '', + 'variant08' => '', + 'variant07' => '', + 'variant06' => '', + 'variant05' => '', + 'variant04' => '', + 'variant03' => '', + 'variant02' => '', + 'variant01' => '', + ), + 'earrings' => array( + 'variant01' => '', + 'variant02' => '', + 'variant03' => '', + ), + 'freckles' => array( + 'variant01' => '', + ), + 'nose' => array( + 'variant01' => '', + 'variant02' => '', + 'variant03' => '', + 'variant04' => '', + 'variant05' => '', + 'variant06' => '', + ), + 'beard' => array( + 'variant01' => '', + 'variant02' => '', + ), + 'mouth' => array( + 'happy01' => '', + 'happy02' => '', + 'happy03' => '', + 'happy04' => '', + 'happy05' => '', + 'happy06' => '', + 'happy07' => '', + 'happy08' => '', + 'happy18' => '', + 'happy09' => '', + 'happy10' => '', + 'happy11' => '', + 'happy12' => '', + 'happy13' => '', + 'happy14' => '', + 'happy17' => '', + 'happy15' => '', + 'happy16' => '', + 'sad01' => '', + 'sad02' => '', + 'sad03' => '', + 'sad04' => '', + 'sad05' => '', + 'sad06' => '', + 'sad07' => '', + 'sad08' => '', + 'sad09' => '', + ), + 'glasses' => array( + 'variant01' => '', + 'variant02' => '', + 'variant03' => '', + 'variant04' => '', + 'variant05' => '', + ), +), +); \ No newline at end of file diff --git a/inc/avatar-styles/lorelei_neutral.php b/inc/avatar-styles/lorelei_neutral.php new file mode 100644 index 0000000..dac953b --- /dev/null +++ b/inc/avatar-styles/lorelei_neutral.php @@ -0,0 +1,199 @@ + 'lorelei-neutral', +'meta' => array( + 'title' => 'Lorelei Neutral', + 'creator' => 'Lisa Wischofsky', + 'source' => 'https://www.figma.com/community/file/1198749693280469639', + 'license_name' => 'CC0 1.0', + 'license_url' => 'https://creativecommons.org/publicdomain/zero/1.0/', +), +'view_box' => '0 0 600 600', +'clip_default' => true, +'has_pre_create' => false, +'has_post_create' => false, +'is_initials' => false, +'attr_pairs' => array( + array('viewBox', '0 0 600 600'), + array('fill', 'none'), + array('shape-rendering', 'auto'), +), +'bg_colors' => array('ffffff'), +'bg_type' => array('solid'), +'bg_rotation' => array('0', '360'), +'body_template' => '@@k:eyebrows@@@@k:eyes@@@@k:freckles@@@@k:mouth@@@@k:nose@@@@k:glasses@@', +'pick_order' => array('eyebrows', 'eyes', 'freckles', 'mouth', 'nose', 'glasses'), +'bool_order' => array('freckles', 'glasses'), +'color_order' => array( + 'eyebrows' => 'eyebrowsColor', + 'eyes' => 'eyesColor', + 'freckles' => 'frecklesColor', + 'glasses' => 'glassesColor', + 'mouth' => 'mouthColor', + 'nose' => 'noseColor', +), +'pc_has_ints' => false, +'pc_meta' => array( + array( + 'group' => 'eyebrows', + 'values_opt' => 'eyebrows', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'eyes', + 'values_opt' => 'eyes', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'freckles', + 'values_opt' => 'freckles', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'mouth', + 'values_opt' => 'mouth', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'nose', + 'values_opt' => 'nose', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'glasses', + 'values_opt' => 'glasses', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), +), +'defaults' => array( + 'backgroundColor' => array('ffffff'), + 'eyebrows' => array('variant13', 'variant12', 'variant11', 'variant10', 'variant09', 'variant08', 'variant07', 'variant06', 'variant05', 'variant04', 'variant03', 'variant02', 'variant01'), + 'eyebrowsColor' => array('000000'), + 'eyes' => array('variant24', 'variant23', 'variant22', 'variant21', 'variant20', 'variant19', 'variant18', 'variant17', 'variant16', 'variant15', 'variant14', 'variant13', 'variant12', 'variant11', 'variant10', 'variant09', 'variant08', 'variant07', 'variant06', 'variant05', 'variant04', 'variant03', 'variant02', 'variant01'), + 'eyesColor' => array('000000'), + 'freckles' => array('variant01'), + 'frecklesColor' => array('000000'), + 'frecklesProbability' => 5, + 'glasses' => array('variant01', 'variant02', 'variant03', 'variant04', 'variant05'), + 'glassesColor' => array('000000'), + 'glassesProbability' => 10, + 'mouth' => array('happy01', 'happy02', 'happy03', 'happy04', 'happy05', 'happy06', 'happy07', 'happy08', 'happy18', 'happy09', 'happy10', 'happy11', 'happy12', 'happy13', 'happy14', 'happy17', 'happy15', 'happy16', 'sad01', 'sad02', 'sad03', 'sad04', 'sad05', 'sad06', 'sad07', 'sad08', 'sad09'), + 'mouthColor' => array('000000'), + 'nose' => array('variant01', 'variant02', 'variant03', 'variant04', 'variant05', 'variant06'), + 'noseColor' => array('000000'), +), +'groups' => array( + 'eyebrows' => array( + 'variant13' => '', + 'variant12' => '', + 'variant11' => '', + 'variant10' => '', + 'variant09' => '', + 'variant08' => '', + 'variant07' => '', + 'variant06' => '', + 'variant05' => '', + 'variant04' => '', + 'variant03' => '', + 'variant02' => '', + 'variant01' => '', + ), + 'eyes' => array( + 'variant24' => '', + 'variant23' => '', + 'variant22' => '', + 'variant21' => '', + 'variant20' => '', + 'variant19' => '', + 'variant18' => '', + 'variant17' => '', + 'variant16' => '', + 'variant15' => '', + 'variant14' => '', + 'variant13' => '', + 'variant12' => '', + 'variant11' => '', + 'variant10' => '', + 'variant09' => '', + 'variant08' => '', + 'variant07' => '', + 'variant06' => '', + 'variant05' => '', + 'variant04' => '', + 'variant03' => '', + 'variant02' => '', + 'variant01' => '', + ), + 'freckles' => array( + 'variant01' => '', + ), + 'mouth' => array( + 'happy01' => '', + 'happy02' => '', + 'happy03' => '', + 'happy04' => '', + 'happy05' => '', + 'happy06' => '', + 'happy07' => '', + 'happy08' => '', + 'happy18' => '', + 'happy09' => '', + 'happy10' => '', + 'happy11' => '', + 'happy12' => '', + 'happy13' => '', + 'happy14' => '', + 'happy17' => '', + 'happy15' => '', + 'happy16' => '', + 'sad01' => '', + 'sad02' => '', + 'sad03' => '', + 'sad04' => '', + 'sad05' => '', + 'sad06' => '', + 'sad07' => '', + 'sad08' => '', + 'sad09' => '', + ), + 'nose' => array( + 'variant01' => '', + 'variant02' => '', + 'variant03' => '', + 'variant04' => '', + 'variant05' => '', + 'variant06' => '', + ), + 'glasses' => array( + 'variant01' => '', + 'variant02' => '', + 'variant03' => '', + 'variant04' => '', + 'variant05' => '', + ), +), +); \ No newline at end of file diff --git a/inc/avatar-styles/notionists.php b/inc/avatar-styles/notionists.php new file mode 100644 index 0000000..532fb98 --- /dev/null +++ b/inc/avatar-styles/notionists.php @@ -0,0 +1,366 @@ + 'notionists', +'meta' => array( + 'title' => 'Notionists', + 'creator' => 'Zoish', + 'source' => 'https://heyzoish.gumroad.com/l/notionists', + 'license_name' => 'CC0 1.0', + 'license_url' => 'https://creativecommons.org/publicdomain/zero/1.0/', +), +'view_box' => '0 0 1744 1744', +'clip_default' => true, +'has_pre_create' => false, +'has_post_create' => false, +'is_initials' => false, +'attr_pairs' => array( + array('viewBox', '0 0 1744 1744'), + array('fill', 'none'), + array('shape-rendering', 'auto'), +), +'bg_colors' => array(), +'bg_type' => array('solid'), +'bg_rotation' => array('0', '360'), +'body_template' => '@@k:base@@@@k:body@@@@k:hair@@@@k:lips@@@@k:beard@@@@k:nose@@@@k:eyes@@@@k:glasses@@@@k:brows@@@@k:gesture@@', +'pick_order' => array('base', 'body', 'hair', 'lips', 'beard', 'nose', 'eyes', 'glasses', 'brows', 'gesture', 'bodyIcon'), +'bool_order' => array('beard', 'glasses', 'gesture', 'bodyIcon'), +'color_order' => array( +), +'pc_has_ints' => false, +'pc_meta' => array( + array( + 'group' => 'base', + 'values_opt' => 'base', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'body', + 'values_opt' => 'body', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'hair', + 'values_opt' => 'hair', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'lips', + 'values_opt' => 'lips', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'beard', + 'values_opt' => 'beard', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'nose', + 'values_opt' => 'nose', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'eyes', + 'values_opt' => 'eyes', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'glasses', + 'values_opt' => 'glasses', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'brows', + 'values_opt' => 'brows', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'gesture', + 'values_opt' => 'gesture', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'bodyIcon', + 'values_opt' => 'bodyIcon', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), +), +'defaults' => array( + 'base' => array('variant01'), + 'beard' => array('variant12', 'variant11', 'variant10', 'variant09', 'variant08', 'variant07', 'variant06', 'variant05', 'variant04', 'variant03', 'variant02', 'variant01'), + 'beardProbability' => 10, + 'body' => array('variant25', 'variant24', 'variant23', 'variant22', 'variant21', 'variant20', 'variant19', 'variant18', 'variant17', 'variant16', 'variant15', 'variant14', 'variant13', 'variant12', 'variant11', 'variant10', 'variant09', 'variant08', 'variant07', 'variant06', 'variant05', 'variant04', 'variant03', 'variant02', 'variant01'), + 'bodyIcon' => array('electric', 'saturn', 'galaxy'), + 'bodyIconProbability' => 75, + 'brows' => array('variant13', 'variant12', 'variant11', 'variant10', 'variant09', 'variant08', 'variant07', 'variant06', 'variant05', 'variant04', 'variant03', 'variant02', 'variant01'), + 'eyes' => array('variant05', 'variant04', 'variant03', 'variant02', 'variant01'), + 'gesture' => array('wavePointLongArms', 'waveOkLongArms', 'waveLongArms', 'waveLongArm', 'pointLongArm', 'okLongArm', 'point', 'ok', 'hand', 'handPhone'), + 'gestureProbability' => 10, + 'glasses' => array('variant11', 'variant10', 'variant09', 'variant08', 'variant07', 'variant06', 'variant05', 'variant04', 'variant03', 'variant02', 'variant01'), + 'glassesProbability' => 20, + 'hair' => array('variant63', 'variant62', 'variant61', 'variant60', 'variant59', 'variant58', 'variant57', 'variant56', 'variant55', 'variant54', 'variant53', 'variant52', 'variant51', 'variant50', 'variant49', 'variant48', 'variant47', 'variant46', 'variant45', 'variant44', 'variant43', 'variant42', 'variant41', 'variant40', 'variant39', 'variant38', 'variant37', 'variant36', 'variant35', 'variant34', 'variant33', 'variant32', 'variant31', 'variant30', 'variant29', 'variant28', 'variant27', 'variant26', 'variant25', 'variant24', 'variant23', 'variant22', 'variant21', 'variant20', 'variant19', 'variant18', 'variant17', 'variant16', 'variant15', 'variant14', 'variant13', 'variant12', 'variant11', 'variant10', 'variant09', 'variant08', 'variant07', 'variant06', 'variant05', 'variant04', 'variant03', 'variant02', 'variant01', 'hat'), + 'lips' => array('variant30', 'variant29', 'variant28', 'variant27', 'variant26', 'variant25', 'variant24', 'variant23', 'variant22', 'variant21', 'variant20', 'variant19', 'variant18', 'variant17', 'variant16', 'variant15', 'variant14', 'variant13', 'variant12', 'variant11', 'variant10', 'variant09', 'variant08', 'variant07', 'variant06', 'variant05', 'variant04', 'variant03', 'variant02', 'variant01'), + 'nose' => array('variant20', 'variant19', 'variant18', 'variant17', 'variant16', 'variant15', 'variant14', 'variant13', 'variant12', 'variant11', 'variant10', 'variant09', 'variant08', 'variant07', 'variant06', 'variant05', 'variant04', 'variant03', 'variant02', 'variant01'), +), +'groups' => array( + 'base' => array( + 'variant01' => '', + ), + 'body' => array( + 'variant25' => '', + 'variant24' => '', + 'variant23' => '', + 'variant22' => '', + 'variant21' => '', + 'variant20' => '', + 'variant19' => '', + 'variant18' => '', + 'variant17' => '', + 'variant16' => '', + 'variant15' => '', + 'variant14' => '', + 'variant13' => '', + 'variant12' => '', + 'variant11' => '', + 'variant10' => '@@k:bodyIcon@@', + 'variant09' => '@@k:bodyIcon@@', + 'variant08' => '@@k:bodyIcon@@', + 'variant07' => '@@k:bodyIcon@@', + 'variant06' => '@@k:bodyIcon@@', + 'variant05' => '@@k:bodyIcon@@', + 'variant04' => '@@k:bodyIcon@@', + 'variant03' => '@@k:bodyIcon@@', + 'variant02' => '@@k:bodyIcon@@', + 'variant01' => '@@k:bodyIcon@@', + ), + 'hair' => array( + 'variant63' => '', + 'variant62' => '', + 'variant61' => '', + 'variant60' => '', + 'variant59' => '', + 'variant58' => '', + 'variant57' => '', + 'variant56' => '', + 'variant55' => '', + 'variant54' => '', + 'variant53' => '', + 'variant52' => '', + 'variant51' => '', + 'variant50' => '', + 'variant49' => '', + 'variant48' => '', + 'variant47' => '', + 'variant46' => '', + 'variant45' => '', + 'variant44' => '', + 'variant43' => '', + 'variant42' => '', + 'variant41' => '', + 'variant40' => '', + 'variant39' => '', + 'variant38' => '', + 'variant37' => '', + 'variant36' => '', + 'variant35' => '', + 'variant34' => '', + 'variant33' => '', + 'variant32' => '', + 'variant31' => '', + 'variant30' => '', + 'variant29' => '', + 'variant28' => '', + 'variant27' => '', + 'variant26' => '', + 'variant25' => '', + 'variant24' => '', + 'variant23' => '', + 'variant22' => '', + 'variant21' => '', + 'variant20' => '', + 'variant19' => '', + 'variant18' => '', + 'variant17' => '', + 'variant16' => '', + 'variant15' => '', + 'variant14' => '', + 'variant13' => '', + 'variant12' => '', + 'variant11' => '', + 'variant10' => '', + 'variant09' => '', + 'variant08' => '', + 'variant07' => '', + 'variant06' => '', + 'variant05' => '', + 'variant04' => '', + 'variant03' => '', + 'variant02' => '', + 'variant01' => '', + 'hat' => '', + ), + 'lips' => array( + 'variant30' => '', + 'variant29' => '', + 'variant28' => '', + 'variant27' => '', + 'variant26' => '', + 'variant25' => '', + 'variant24' => '', + 'variant23' => '', + 'variant22' => '', + 'variant21' => '', + 'variant20' => '', + 'variant19' => '', + 'variant18' => '', + 'variant17' => '', + 'variant16' => '', + 'variant15' => '', + 'variant14' => '', + 'variant13' => '', + 'variant12' => '', + 'variant11' => '', + 'variant10' => '', + 'variant09' => '', + 'variant08' => '', + 'variant07' => '', + 'variant06' => '', + 'variant05' => '', + 'variant04' => '', + 'variant03' => '', + 'variant02' => '', + 'variant01' => '', + ), + 'beard' => array( + 'variant12' => '', + 'variant11' => '', + 'variant10' => '', + 'variant09' => '', + 'variant08' => '', + 'variant07' => '', + 'variant06' => '', + 'variant05' => '', + 'variant04' => '', + 'variant03' => '', + 'variant02' => '', + 'variant01' => '', + ), + 'nose' => array( + 'variant20' => '', + 'variant19' => '', + 'variant18' => '', + 'variant17' => '', + 'variant16' => '', + 'variant15' => '', + 'variant14' => '', + 'variant13' => '', + 'variant12' => '', + 'variant11' => '', + 'variant10' => '', + 'variant09' => '', + 'variant08' => '', + 'variant07' => '', + 'variant06' => '', + 'variant05' => '', + 'variant04' => '', + 'variant03' => '', + 'variant02' => '', + 'variant01' => '', + ), + 'eyes' => array( + 'variant05' => '', + 'variant04' => '', + 'variant03' => '', + 'variant02' => '', + 'variant01' => '', + ), + 'glasses' => array( + 'variant11' => '', + 'variant10' => '', + 'variant09' => '', + 'variant08' => '', + 'variant07' => '', + 'variant06' => '', + 'variant05' => '', + 'variant04' => '', + 'variant03' => '', + 'variant02' => '', + 'variant01' => '', + ), + 'brows' => array( + 'variant13' => '', + 'variant12' => '', + 'variant11' => '', + 'variant10' => '', + 'variant09' => '', + 'variant08' => '', + 'variant07' => '', + 'variant06' => '', + 'variant05' => '', + 'variant04' => '', + 'variant03' => '', + 'variant02' => '', + 'variant01' => '', + ), + 'gesture' => array( + 'wavePointLongArms' => '', + 'waveOkLongArms' => '', + 'waveLongArms' => '', + 'waveLongArm' => '', + 'pointLongArm' => '', + 'okLongArm' => '', + 'point' => '', + 'ok' => '', + 'hand' => '', + 'handPhone' => '', + ), + 'bodyIcon' => array( + 'electric' => '', + 'saturn' => '', + 'galaxy' => '', + ), +), +); \ No newline at end of file diff --git a/inc/avatar-styles/notionists_neutral.php b/inc/avatar-styles/notionists_neutral.php new file mode 100644 index 0000000..04cb5f4 --- /dev/null +++ b/inc/avatar-styles/notionists_neutral.php @@ -0,0 +1,177 @@ + 'notionists-neutral', +'meta' => array( + 'title' => 'Notionists', + 'creator' => 'Zoish', + 'source' => 'https://heyzoish.gumroad.com/l/notionists', + 'license_name' => 'CC0 1.0', + 'license_url' => 'https://creativecommons.org/publicdomain/zero/1.0/', +), +'view_box' => '0 0 560 560', +'clip_default' => true, +'has_pre_create' => false, +'has_post_create' => false, +'is_initials' => false, +'attr_pairs' => array( + array('viewBox', '0 0 560 560'), + array('fill', 'none'), + array('shape-rendering', 'auto'), +), +'bg_colors' => array('ffffff'), +'bg_type' => array('solid'), +'bg_rotation' => array('0', '360'), +'body_template' => '@@k:lips@@@@k:nose@@@@k:eyes@@@@k:glasses@@@@k:brows@@', +'pick_order' => array('lips', 'nose', 'eyes', 'glasses', 'brows'), +'bool_order' => array('glasses'), +'color_order' => array( +), +'pc_has_ints' => false, +'pc_meta' => array( + array( + 'group' => 'lips', + 'values_opt' => 'lips', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'nose', + 'values_opt' => 'nose', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'eyes', + 'values_opt' => 'eyes', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'glasses', + 'values_opt' => 'glasses', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'brows', + 'values_opt' => 'brows', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), +), +'defaults' => array( + 'backgroundColor' => array('ffffff'), + 'brows' => array('variant13', 'variant12', 'variant11', 'variant10', 'variant09', 'variant08', 'variant07', 'variant06', 'variant05', 'variant04', 'variant03', 'variant02', 'variant01'), + 'eyes' => array('variant05', 'variant04', 'variant03', 'variant02', 'variant01'), + 'glasses' => array('variant11', 'variant10', 'variant09', 'variant08', 'variant07', 'variant06', 'variant05', 'variant04', 'variant03', 'variant02', 'variant01'), + 'glassesProbability' => 20, + 'lips' => array('variant30', 'variant29', 'variant28', 'variant27', 'variant26', 'variant25', 'variant24', 'variant23', 'variant22', 'variant21', 'variant20', 'variant19', 'variant18', 'variant17', 'variant16', 'variant15', 'variant14', 'variant13', 'variant12', 'variant11', 'variant10', 'variant09', 'variant08', 'variant07', 'variant06', 'variant05', 'variant04', 'variant03', 'variant02', 'variant01'), + 'nose' => array('variant20', 'variant19', 'variant18', 'variant17', 'variant16', 'variant15', 'variant14', 'variant13', 'variant12', 'variant11', 'variant10', 'variant09', 'variant08', 'variant07', 'variant06', 'variant05', 'variant04', 'variant03', 'variant02', 'variant01'), +), +'groups' => array( + 'lips' => array( + 'variant30' => '', + 'variant29' => '', + 'variant28' => '', + 'variant27' => '', + 'variant26' => '', + 'variant25' => '', + 'variant24' => '', + 'variant23' => '', + 'variant22' => '', + 'variant21' => '', + 'variant20' => '', + 'variant19' => '', + 'variant18' => '', + 'variant17' => '', + 'variant16' => '', + 'variant15' => '', + 'variant14' => '', + 'variant13' => '', + 'variant12' => '', + 'variant11' => '', + 'variant10' => '', + 'variant09' => '', + 'variant08' => '', + 'variant07' => '', + 'variant06' => '', + 'variant05' => '', + 'variant04' => '', + 'variant03' => '', + 'variant02' => '', + 'variant01' => '', + ), + 'nose' => array( + 'variant20' => '', + 'variant19' => '', + 'variant18' => '', + 'variant17' => '', + 'variant16' => '', + 'variant15' => '', + 'variant14' => '', + 'variant13' => '', + 'variant12' => '', + 'variant11' => '', + 'variant10' => '', + 'variant09' => '', + 'variant08' => '', + 'variant07' => '', + 'variant06' => '', + 'variant05' => '', + 'variant04' => '', + 'variant03' => '', + 'variant02' => '', + 'variant01' => '', + ), + 'eyes' => array( + 'variant05' => '', + 'variant04' => '', + 'variant03' => '', + 'variant02' => '', + 'variant01' => '', + ), + 'glasses' => array( + 'variant11' => '', + 'variant10' => '', + 'variant09' => '', + 'variant08' => '', + 'variant07' => '', + 'variant06' => '', + 'variant05' => '', + 'variant04' => '', + 'variant03' => '', + 'variant02' => '', + 'variant01' => '', + ), + 'brows' => array( + 'variant13' => '', + 'variant12' => '', + 'variant11' => '', + 'variant10' => '', + 'variant09' => '', + 'variant08' => '', + 'variant07' => '', + 'variant06' => '', + 'variant05' => '', + 'variant04' => '', + 'variant03' => '', + 'variant02' => '', + 'variant01' => '', + ), +), +); \ No newline at end of file diff --git a/inc/avatar-styles/open_peeps.php b/inc/avatar-styles/open_peeps.php new file mode 100644 index 0000000..88dfa8d --- /dev/null +++ b/inc/avatar-styles/open_peeps.php @@ -0,0 +1,209 @@ + 'open-peeps', +'meta' => array( + 'title' => 'Open Peeps', + 'creator' => 'Pablo Stanley', + 'source' => 'https://www.openpeeps.com/', + 'license_name' => 'CC0 1.0', + 'license_url' => 'https://creativecommons.org/publicdomain/zero/1.0/', +), +'view_box' => '0 0 704 704', +'clip_default' => true, +'has_pre_create' => false, +'has_post_create' => false, +'is_initials' => false, +'attr_pairs' => array( + array('viewBox', '0 0 704 704'), + array('fill', 'none'), + array('shape-rendering', 'auto'), +), +'bg_colors' => array(), +'bg_type' => array('solid'), +'bg_rotation' => array('0', '360'), +'body_template' => '@@k:head@@@@k:face@@@@k:facialHair@@@@k:mask@@@@k:accessories@@', +'pick_order' => array('head', 'face', 'facialHair', 'mask', 'accessories'), +'bool_order' => array('facialHair', 'mask', 'accessories'), +'color_order' => array( + 'skin' => 'skinColor', + 'clothing' => 'clothingColor', + 'headContrast' => 'headContrastColor', +), +'pc_has_ints' => false, +'pc_meta' => array( + array( + 'group' => 'head', + 'values_opt' => 'head', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'face', + 'values_opt' => 'face', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'facialHair', + 'values_opt' => 'facialHair', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'mask', + 'values_opt' => 'mask', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'accessories', + 'values_opt' => 'accessories', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), +), +'defaults' => array( + 'accessories' => array('eyepatch', 'glasses', 'glasses2', 'glasses3', 'glasses4', 'glasses5', 'sunglasses', 'sunglasses2'), + 'accessoriesProbability' => 20, + 'clothingColor' => array('e78276', 'ffcf77', 'fdea6b', '78e185', '9ddadb', '8fa7df', 'e279c7'), + 'face' => array('angryWithFang', 'awe', 'blank', 'calm', 'cheeky', 'concerned', 'concernedFear', 'contempt', 'cute', 'cyclops', 'driven', 'eatingHappy', 'explaining', 'eyesClosed', 'fear', 'hectic', 'lovingGrin1', 'lovingGrin2', 'monster', 'old', 'rage', 'serious', 'smile', 'smileBig', 'smileLOL', 'smileTeethGap', 'solemn', 'suspicious', 'tired', 'veryAngry'), + 'facialHair' => array('chin', 'full', 'full2', 'full3', 'full4', 'goatee1', 'goatee2', 'moustache1', 'moustache2', 'moustache3', 'moustache4', 'moustache5', 'moustache6', 'moustache7', 'moustache8', 'moustache9'), + 'facialHairProbability' => 10, + 'head' => array('afro', 'bangs', 'bangs2', 'bantuKnots', 'bear', 'bun', 'bun2', 'buns', 'cornrows', 'cornrows2', 'dreads1', 'dreads2', 'flatTop', 'flatTopLong', 'grayBun', 'grayMedium', 'grayShort', 'hatBeanie', 'hatHip', 'hijab', 'long', 'longAfro', 'longBangs', 'longCurly', 'medium1', 'medium2', 'medium3', 'mediumBangs', 'mediumBangs2', 'mediumBangs3', 'mediumStraight', 'mohawk', 'mohawk2', 'noHair1', 'noHair2', 'noHair3', 'pomp', 'shaved1', 'shaved2', 'shaved3', 'short1', 'short2', 'short3', 'short4', 'short5', 'turban', 'twists', 'twists2'), + 'headContrastColor' => array('2c1b18', 'e8e1e1', 'ecdcbf', 'd6b370', 'f59797', 'b58143', 'a55728', '724133', '4a312c', 'c93305'), + 'mask' => array('medicalMask', 'respirator'), + 'maskProbability' => 5, + 'skinColor' => array('ffdbb4', 'edb98a', 'd08b5b', 'ae5d29', '694d3d'), +), +'groups' => array( + 'head' => array( + 'afro' => '', + 'bangs' => '', + 'bangs2' => '', + 'bantuKnots' => '', + 'bear' => '', + 'bun' => '', + 'bun2' => '', + 'buns' => '', + 'cornrows' => '', + 'cornrows2' => '', + 'dreads1' => '', + 'dreads2' => '', + 'flatTop' => '', + 'flatTopLong' => '', + 'grayBun' => '', + 'grayMedium' => '', + 'grayShort' => '', + 'hatBeanie' => '', + 'hatHip' => '', + 'hijab' => '', + 'long' => '', + 'longAfro' => '', + 'longBangs' => '', + 'longCurly' => '', + 'medium1' => '', + 'medium2' => '', + 'medium3' => '', + 'mediumBangs' => '', + 'mediumBangs2' => '', + 'mediumBangs3' => '', + 'mediumStraight' => '', + 'mohawk' => '', + 'mohawk2' => '', + 'noHair1' => '', + 'noHair2' => '', + 'noHair3' => '', + 'pomp' => '', + 'shaved1' => '', + 'shaved2' => '', + 'shaved3' => '', + 'short1' => '', + 'short2' => '', + 'short3' => '', + 'short4' => '', + 'short5' => '', + 'turban' => '', + 'twists' => '', + 'twists2' => '', + ), + 'face' => array( + 'angryWithFang' => '', + 'awe' => '', + 'blank' => '', + 'calm' => '', + 'cheeky' => '', + 'concerned' => '', + 'concernedFear' => '', + 'contempt' => '', + 'cute' => '', + 'cyclops' => '', + 'driven' => '', + 'eatingHappy' => '', + 'explaining' => '', + 'eyesClosed' => '', + 'fear' => '', + 'hectic' => '', + 'lovingGrin1' => '', + 'lovingGrin2' => '', + 'monster' => '', + 'old' => '', + 'rage' => '', + 'serious' => '', + 'smile' => '', + 'smileBig' => '', + 'smileLOL' => '', + 'smileTeethGap' => '', + 'solemn' => '', + 'suspicious' => '', + 'tired' => '', + 'veryAngry' => '', + ), + 'facialHair' => array( + 'chin' => '', + 'full' => '', + 'full2' => '', + 'full3' => '', + 'full4' => '', + 'goatee1' => '', + 'goatee2' => '', + 'moustache1' => '', + 'moustache2' => '', + 'moustache3' => '', + 'moustache4' => '', + 'moustache5' => '', + 'moustache6' => '', + 'moustache7' => '', + 'moustache8' => '', + 'moustache9' => '', + ), + 'mask' => array( + 'medicalMask' => '', + 'respirator' => '', + ), + 'accessories' => array( + 'eyepatch' => '', + 'glasses' => '', + 'glasses2' => '', + 'glasses3' => '', + 'glasses4' => '', + 'glasses5' => '', + 'sunglasses' => '', + 'sunglasses2' => '', + ), +), +); \ No newline at end of file diff --git a/inc/avatar-styles/pixel_art.php b/inc/avatar-styles/pixel_art.php new file mode 100644 index 0000000..5dd4044 --- /dev/null +++ b/inc/avatar-styles/pixel_art.php @@ -0,0 +1,291 @@ + 'pixel-art', +'meta' => array( + 'title' => 'Pixel Art', + 'creator' => 'DiceBear', + 'source' => 'https://www.figma.com/community/file/1198754108850888330', + 'license_name' => 'CC0 1.0', + 'license_url' => 'https://creativecommons.org/publicdomain/zero/1.0/', +), +'view_box' => '0 0 16 16', +'clip_default' => true, +'has_pre_create' => false, +'has_post_create' => false, +'is_initials' => false, +'attr_pairs' => array( + array('viewBox', '0 0 16 16'), + array('fill', 'none'), + array('shape-rendering', 'crispEdges'), +), +'bg_colors' => array(), +'bg_type' => array('solid'), +'bg_rotation' => array('0', '360'), +'body_template' => '@@k:accessories@@@@k:clothing@@@@k:eyes@@@@k:glasses@@@@k:beard@@@@k:mouth@@@@k:hair@@@@k:hat@@', +'pick_order' => array('accessories', 'clothing', 'eyes', 'glasses', 'beard', 'mouth', 'hair', 'hat'), +'bool_order' => array('accessories', 'glasses', 'beard', 'hat'), +'color_order' => array( + 'accessories' => 'accessoriesColor', + 'clothing' => 'clothingColor', + 'eyes' => 'eyesColor', + 'glasses' => 'glassesColor', + 'hair' => 'hairColor', + 'hat' => 'hatColor', + 'mouth' => 'mouthColor', + 'skin' => 'skinColor', +), +'pc_has_ints' => false, +'pc_meta' => array( + array( + 'group' => 'accessories', + 'values_opt' => 'accessories', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'clothing', + 'values_opt' => 'clothing', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'eyes', + 'values_opt' => 'eyes', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'glasses', + 'values_opt' => 'glasses', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'beard', + 'values_opt' => 'beard', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'mouth', + 'values_opt' => 'mouth', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'hair', + 'values_opt' => 'hair', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'hat', + 'values_opt' => 'hat', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), +), +'defaults' => array( + 'accessories' => array('variant04', 'variant03', 'variant02', 'variant01'), + 'accessoriesColor' => array('daa520', 'ffd700', 'fafad2', 'd3d3d3', 'a9a9a9'), + 'accessoriesProbability' => 10, + 'beard' => array('variant08', 'variant07', 'variant06', 'variant05', 'variant04', 'variant03', 'variant02', 'variant01'), + 'beardProbability' => 5, + 'clothing' => array('variant23', 'variant22', 'variant21', 'variant20', 'variant19', 'variant18', 'variant17', 'variant16', 'variant15', 'variant14', 'variant13', 'variant12', 'variant11', 'variant10', 'variant09', 'variant08', 'variant07', 'variant06', 'variant05', 'variant04', 'variant03', 'variant02', 'variant01'), + 'clothingColor' => array('5bc0de', '428bca', '03396c', '88d8b0', '44c585', '00b159', 'ff6f69', 'd11141', 'ae0001', 'ffeead', 'ffd969', 'ffc425'), + 'eyes' => array('variant12', 'variant11', 'variant10', 'variant09', 'variant08', 'variant07', 'variant06', 'variant05', 'variant04', 'variant03', 'variant02', 'variant01'), + 'eyesColor' => array('76778b', '697b94', '647b90', '5b7c8b', '588387', '876658'), + 'glasses' => array('light07', 'light06', 'light05', 'light04', 'light03', 'light02', 'light01', 'dark07', 'dark06', 'dark05', 'dark04', 'dark03', 'dark02', 'dark01'), + 'glassesColor' => array('4b4b4b', '323232', '191919', '43677d', '5f705c', 'a04b5d'), + 'glassesProbability' => 20, + 'hair' => array('short24', 'short23', 'short22', 'short21', 'short20', 'short19', 'short18', 'short17', 'short16', 'short15', 'short14', 'short13', 'short12', 'short11', 'short10', 'short09', 'short08', 'short07', 'short06', 'short05', 'short04', 'short03', 'short02', 'short01', 'long21', 'long20', 'long19', 'long18', 'long17', 'long16', 'long15', 'long14', 'long13', 'long12', 'long11', 'long10', 'long09', 'long08', 'long07', 'long06', 'long05', 'long04', 'long03', 'long02', 'long01'), + 'hairColor' => array('cab188', '603a14', '83623b', 'a78961', '611c17', '603015', '612616', '28150a', '009bbd', 'bd1700', '91cb15'), + 'hat' => array('variant10', 'variant09', 'variant08', 'variant07', 'variant06', 'variant05', 'variant04', 'variant03', 'variant02', 'variant01'), + 'hatColor' => array('2e1e05', '2663a3', '989789', '3d8a6b', 'cc6192', '614f8a', 'a62116'), + 'hatProbability' => 5, + 'mouth' => array('sad10', 'sad09', 'sad08', 'sad07', 'sad06', 'sad05', 'sad04', 'sad03', 'sad02', 'sad01', 'happy13', 'happy12', 'happy11', 'happy10', 'happy09', 'happy08', 'happy07', 'happy06', 'happy05', 'happy04', 'happy03', 'happy02', 'happy01'), + 'mouthColor' => array('d29985', 'c98276', 'e35d6a', 'de0f0d'), + 'skinColor' => array('ffdbac', 'f5cfa0', 'eac393', 'e0b687', 'cb9e6e', 'b68655', 'a26d3d', '8d5524'), +), +'groups' => array( + 'accessories' => array( + 'variant04' => '', + 'variant03' => '', + 'variant02' => '', + 'variant01' => '', + ), + 'clothing' => array( + 'variant23' => '', + 'variant22' => '', + 'variant21' => '', + 'variant20' => '', + 'variant19' => '', + 'variant18' => '', + 'variant17' => '', + 'variant16' => '', + 'variant15' => '', + 'variant14' => '', + 'variant13' => '', + 'variant12' => '', + 'variant11' => '', + 'variant10' => '', + 'variant09' => '', + 'variant08' => '', + 'variant07' => '', + 'variant06' => '', + 'variant05' => '', + 'variant04' => '', + 'variant03' => '', + 'variant02' => '', + 'variant01' => '', + ), + 'eyes' => array( + 'variant12' => '', + 'variant11' => '', + 'variant10' => '', + 'variant09' => '', + 'variant08' => '', + 'variant07' => '', + 'variant06' => '', + 'variant05' => '', + 'variant04' => '', + 'variant03' => '', + 'variant02' => '', + 'variant01' => '', + ), + 'glasses' => array( + 'light07' => '', + 'light06' => '', + 'light05' => '', + 'light04' => '', + 'light03' => '', + 'light02' => '', + 'light01' => '', + 'dark07' => '', + 'dark06' => '', + 'dark05' => '', + 'dark04' => '', + 'dark03' => '', + 'dark02' => '', + 'dark01' => '', + ), + 'beard' => array( + 'variant08' => '', + 'variant07' => '', + 'variant06' => '', + 'variant05' => '', + 'variant04' => '', + 'variant03' => '', + 'variant02' => '', + 'variant01' => '', + ), + 'mouth' => array( + 'sad10' => '', + 'sad09' => '', + 'sad08' => '', + 'sad07' => '', + 'sad06' => '', + 'sad05' => '', + 'sad04' => '', + 'sad03' => '', + 'sad02' => '', + 'sad01' => '', + 'happy13' => '', + 'happy12' => '', + 'happy11' => '', + 'happy10' => '', + 'happy09' => '', + 'happy08' => '', + 'happy07' => '', + 'happy06' => '', + 'happy05' => '', + 'happy04' => '', + 'happy03' => '', + 'happy02' => '', + 'happy01' => '', + ), + 'hair' => array( + 'short24' => '', + 'short23' => '', + 'short22' => '', + 'short21' => '', + 'short20' => '', + 'short19' => '', + 'short18' => '', + 'short17' => '', + 'short16' => '', + 'short15' => '', + 'short14' => '', + 'short13' => '', + 'short12' => '', + 'short11' => '', + 'short10' => '', + 'short09' => '', + 'short08' => '', + 'short07' => '', + 'short06' => '', + 'short05' => '', + 'short04' => '', + 'short03' => '', + 'short02' => '', + 'short01' => '', + 'long21' => '', + 'long20' => '', + 'long19' => '', + 'long18' => '', + 'long17' => '', + 'long16' => '', + 'long15' => '', + 'long14' => '', + 'long13' => '', + 'long12' => '', + 'long11' => '', + 'long10' => '', + 'long09' => '', + 'long08' => '', + 'long07' => '', + 'long06' => '', + 'long05' => '', + 'long04' => '', + 'long03' => '', + 'long02' => '', + 'long01' => '', + ), + 'hat' => array( + 'variant10' => '', + 'variant09' => '', + 'variant08' => '', + 'variant07' => '', + 'variant06' => '', + 'variant05' => '', + 'variant04' => '', + 'variant03' => '', + 'variant02' => '', + 'variant01' => '', + ), +), +); \ No newline at end of file diff --git a/inc/avatar-styles/pixel_art_neutral.php b/inc/avatar-styles/pixel_art_neutral.php new file mode 100644 index 0000000..23eb101 --- /dev/null +++ b/inc/avatar-styles/pixel_art_neutral.php @@ -0,0 +1,129 @@ + 'pixel-art-neutral', +'meta' => array( + 'title' => 'Pixel Art Neutral', + 'creator' => 'DiceBear', + 'source' => 'https://www.figma.com/community/file/1198754108850888330', + 'license_name' => 'CC0 1.0', + 'license_url' => 'https://creativecommons.org/publicdomain/zero/1.0/', +), +'view_box' => '0 0 14 14', +'clip_default' => true, +'has_pre_create' => false, +'has_post_create' => false, +'is_initials' => false, +'attr_pairs' => array( + array('viewBox', '0 0 14 14'), + array('fill', 'none'), + array('shape-rendering', 'crispEdges'), +), +'bg_colors' => array('ffdbac', 'f5cfa0', 'eac393', 'e0b687', 'cb9e6e', 'b68655', 'a26d3d', '8d5524'), +'bg_type' => array('solid'), +'bg_rotation' => array('0', '360'), +'body_template' => '@@k:eyes@@@@k:glasses@@@@k:mouth@@', +'pick_order' => array('eyes', 'glasses', 'mouth'), +'bool_order' => array('glasses'), +'color_order' => array( + 'eyes' => 'eyesColor', + 'glasses' => 'glassesColor', + 'mouth' => 'mouthColor', +), +'pc_has_ints' => false, +'pc_meta' => array( + array( + 'group' => 'eyes', + 'values_opt' => 'eyes', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'glasses', + 'values_opt' => 'glasses', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'mouth', + 'values_opt' => 'mouth', + 'width' => 0, + 'height' => 0, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), +), +'defaults' => array( + 'backgroundColor' => array('ffdbac', 'f5cfa0', 'eac393', 'e0b687', 'cb9e6e', 'b68655', 'a26d3d', '8d5524'), + 'eyes' => array('variant12', 'variant11', 'variant10', 'variant09', 'variant08', 'variant07', 'variant06', 'variant05', 'variant04', 'variant03', 'variant02', 'variant01'), + 'eyesColor' => array('76778b', '697b94', '647b90', '5b7c8b', '588387', '876658'), + 'glasses' => array('light07', 'light06', 'light05', 'light04', 'light03', 'light02', 'light01', 'dark07', 'dark06', 'dark05', 'dark04', 'dark03', 'dark02', 'dark01'), + 'glassesColor' => array('4b4b4b', '323232', '191919', '43677d', '5f705c', 'a04b5d'), + 'glassesProbability' => 10, + 'mouth' => array('sad10', 'sad09', 'sad08', 'sad07', 'sad06', 'sad05', 'sad04', 'sad03', 'sad02', 'sad01', 'happy13', 'happy12', 'happy11', 'happy10', 'happy09', 'happy08', 'happy07', 'happy06', 'happy05', 'happy04', 'happy03', 'happy02', 'happy01'), + 'mouthColor' => array('d29985', 'c98276', 'e35d6a', 'de0f0d'), +), +'groups' => array( + 'eyes' => array( + 'variant12' => '', + 'variant11' => '', + 'variant10' => '', + 'variant09' => '', + 'variant08' => '', + 'variant07' => '', + 'variant06' => '', + 'variant05' => '', + 'variant04' => '', + 'variant03' => '', + 'variant02' => '', + 'variant01' => '', + ), + 'glasses' => array( + 'light07' => '', + 'light06' => '', + 'light05' => '', + 'light04' => '', + 'light03' => '', + 'light02' => '', + 'light01' => '', + 'dark07' => '', + 'dark06' => '', + 'dark05' => '', + 'dark04' => '', + 'dark03' => '', + 'dark02' => '', + 'dark01' => '', + ), + 'mouth' => array( + 'sad10' => '', + 'sad09' => '', + 'sad08' => '', + 'sad07' => '', + 'sad06' => '', + 'sad05' => '', + 'sad04' => '', + 'sad03' => '', + 'sad02' => '', + 'sad01' => '', + 'happy13' => '', + 'happy12' => '', + 'happy11' => '', + 'happy10' => '', + 'happy09' => '', + 'happy08' => '', + 'happy07' => '', + 'happy06' => '', + 'happy05' => '', + 'happy04' => '', + 'happy03' => '', + 'happy02' => '', + 'happy01' => '', + ), +), +); \ No newline at end of file diff --git a/inc/avatar-styles/rings.php b/inc/avatar-styles/rings.php new file mode 100644 index 0000000..496e065 --- /dev/null +++ b/inc/avatar-styles/rings.php @@ -0,0 +1,137 @@ + 'rings', +'meta' => array( + 'title' => 'Rings', + 'creator' => 'DiceBear', + 'source' => 'https://www.dicebear.com', + 'license_name' => 'CC0 1.0', + 'license_url' => 'https://creativecommons.org/publicdomain/zero/1.0/', +), +'view_box' => '0 0 100 100', +'clip_default' => true, +'has_pre_create' => false, +'has_post_create' => false, +'is_initials' => false, +'attr_pairs' => array( + array('viewBox', '0 0 100 100'), + array('fill', 'none'), + array('shape-rendering', 'crispEdges'), +), +'bg_colors' => array(), +'bg_type' => array('solid'), +'bg_rotation' => array('0', '360'), +'body_template' => '@@k:ring@@', +'pick_order' => array('ring', 'ringOne', 'ringTwo', 'ringThree', 'ringFour', 'ringFive'), +'bool_order' => array(), +'color_order' => array( + 'ring' => 'ringColor', +), +'pc_has_ints' => true, +'pc_meta' => array( + array( + 'group' => 'ring', + 'values_opt' => 'ring', + 'width' => 100, + 'height' => 100, + 'rotation' => array('opt' => 'ringRotation', 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'ringOne', + 'values_opt' => 'ringOne', + 'width' => 100, + 'height' => 100, + 'rotation' => array('opt' => 'ringOneRotation', 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'ringTwo', + 'values_opt' => 'ringTwo', + 'width' => 100, + 'height' => 100, + 'rotation' => array('opt' => 'ringTwoRotation', 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'ringThree', + 'values_opt' => 'ringThree', + 'width' => 100, + 'height' => 100, + 'rotation' => array('opt' => 'ringThreeRotation', 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'ringFour', + 'values_opt' => 'ringFour', + 'width' => 100, + 'height' => 100, + 'rotation' => array('opt' => 'ringFourRotation', 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'ringFive', + 'values_opt' => 'ringFive', + 'width' => 100, + 'height' => 100, + 'rotation' => array('opt' => 'ringFiveRotation', 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), +), +'defaults' => array( + 'ring' => array('container'), + 'ringColor' => array('ffd54f', '64b5f6', '4dd0e1', 'ff8a65', '9575cd', '81c784', '7986cb', '4fc3f7', 'aed581', 'dce775', 'ffb74d', 'f06292', 'ba68c8', 'e57373', '4db6ac', 'fff176'), + 'ringFive' => array('full'), + 'ringFiveRotation' => array('-180', '180'), + 'ringFour' => array('quarter', 'half'), + 'ringFourRotation' => array('-180', '180'), + 'ringOne' => array('half', 'quarter'), + 'ringOneRotation' => array('-180', '180'), + 'ringRotation' => array('-180', '180'), + 'ringThree' => array('quarter', 'half'), + 'ringThreeRotation' => array('-180', '180'), + 'ringTwo' => array('quarter', 'half'), + 'ringTwoRotation' => array('-180', '180'), +), +'groups' => array( + 'ring' => array( + 'container' => '@@k:ringOne@@@@k:ringTwo@@@@k:ringThree@@@@k:ringFour@@@@k:ringFive@@@@k:ringOne@@@@k:ringTwo@@@@k:ringThree@@@@k:ringFour@@@@k:ringFive@@', + ), + 'ringOne' => array( + 'half' => '', + 'quarter' => '', + 'eighth' => '', + 'full' => '', + ), + 'ringTwo' => array( + 'eighth' => '', + 'quarter' => '', + 'half' => '', + 'full' => '', + ), + 'ringThree' => array( + 'eighth' => '', + 'quarter' => '', + 'half' => '', + 'full' => '', + ), + 'ringFour' => array( + 'eighth' => '', + 'quarter' => '', + 'half' => '', + 'full' => '', + ), + 'ringFive' => array( + 'eighth' => '', + 'quarter' => '', + 'half' => '', + 'full' => '', + ), +), +); \ No newline at end of file diff --git a/inc/avatar-styles/shapes.php b/inc/avatar-styles/shapes.php new file mode 100644 index 0000000..ffe13a3 --- /dev/null +++ b/inc/avatar-styles/shapes.php @@ -0,0 +1,109 @@ + 'shapes', +'meta' => array( + 'title' => 'Shapes', + 'creator' => 'DiceBear', + 'source' => 'https://www.dicebear.com', + 'license_name' => 'CC0 1.0', + 'license_url' => 'https://creativecommons.org/publicdomain/zero/1.0/', +), +'view_box' => '0 0 100 100', +'clip_default' => true, +'has_pre_create' => true, +'has_post_create' => false, +'is_initials' => false, +'attr_pairs' => array( + array('viewBox', '0 0 100 100'), + array('fill', 'none'), + array('shape-rendering', 'auto'), +), +'bg_colors' => array('0a5b83', '1c799f', '69d2e7', 'f1f4dc', 'f88c49'), +'bg_type' => array('solid'), +'bg_rotation' => array('0', '360'), +'body_template' => '@@k:shape1@@@@k:shape2@@@@k:shape3@@', +'pick_order' => array('shape1', 'shape2', 'shape3'), +'bool_order' => array(), +'color_order' => array( + 'shape1' => 'shape1Color', + 'shape2' => 'shape2Color', + 'shape3' => 'shape3Color', +), +'pc_has_ints' => true, +'pc_meta' => array( + array( + 'group' => 'shape1', + 'values_opt' => 'shape1', + 'width' => 100, + 'height' => 100, + 'rotation' => array('opt' => 'shape1Rotation', 'fallback' => array('0')), + 'offsetX' => array('opt' => 'shape1OffsetX', 'fallback' => array('0')), + 'offsetY' => array('opt' => 'shape1OffsetY', 'fallback' => array('0')), + ), + array( + 'group' => 'shape2', + 'values_opt' => 'shape2', + 'width' => 100, + 'height' => 100, + 'rotation' => array('opt' => 'shape2Rotation', 'fallback' => array('0')), + 'offsetX' => array('opt' => 'shape2OffsetX', 'fallback' => array('0')), + 'offsetY' => array('opt' => 'shape2OffsetY', 'fallback' => array('0')), + ), + array( + 'group' => 'shape3', + 'values_opt' => 'shape3', + 'width' => 100, + 'height' => 100, + 'rotation' => array('opt' => 'shape3Rotation', 'fallback' => array('0')), + 'offsetX' => array('opt' => 'shape3OffsetX', 'fallback' => array('0')), + 'offsetY' => array('opt' => 'shape3OffsetY', 'fallback' => array('0')), + ), +), +'defaults' => array( + 'backgroundColor' => array('0a5b83', '1c799f', '69d2e7', 'f1f4dc', 'f88c49'), + 'shape1' => array('rectangleFilled', 'ellipseFilled', 'polygonFilled'), + 'shape1Color' => array('0a5b83', '1c799f', '69d2e7', 'f1f4dc', 'f88c49'), + 'shape1OffsetX' => array('-65', '65'), + 'shape1OffsetY' => array('-45', '45'), + 'shape1Rotation' => array('-160', '160'), + 'shape2' => array('rectangleFilled', 'ellipseFilled', 'polygonFilled', 'line'), + 'shape2Color' => array('0a5b83', '1c799f', '69d2e7', 'f1f4dc', 'f88c49'), + 'shape2OffsetX' => array('-40', '40'), + 'shape2OffsetY' => array('-40', '40'), + 'shape2Rotation' => array('-180', '180'), + 'shape3' => array('rectangle', 'rectangleFilled', 'ellipseFilled', 'ellipse', 'polygonFilled', 'polygon', 'line'), + 'shape3Color' => array('0a5b83', '1c799f', '69d2e7', 'f1f4dc', 'f88c49'), + 'shape3OffsetX' => array('-25', '25'), + 'shape3OffsetY' => array('-25', '25'), + 'shape3Rotation' => array('-180', '180'), +), +'groups' => array( + 'shape1' => array( + 'rectangle' => '', + 'rectangleFilled' => '', + 'ellipseFilled' => '', + 'ellipse' => '', + 'polygonFilled' => '', + 'polygon' => '', + 'line' => '', + ), + 'shape2' => array( + 'rectangle' => '', + 'rectangleFilled' => '', + 'ellipseFilled' => '', + 'ellipse' => '', + 'polygonFilled' => '', + 'polygon' => '', + 'line' => '', + ), + 'shape3' => array( + 'rectangle' => '', + 'rectangleFilled' => '', + 'ellipseFilled' => '', + 'ellipse' => '', + 'polygonFilled' => '', + 'polygon' => '', + 'line' => '', + ), +), +); \ No newline at end of file diff --git a/inc/avatar-styles/thumbs.php b/inc/avatar-styles/thumbs.php new file mode 100644 index 0000000..b096eed --- /dev/null +++ b/inc/avatar-styles/thumbs.php @@ -0,0 +1,144 @@ + 'thumbs', +'meta' => array( + 'title' => 'Thumbs', + 'creator' => 'DiceBear', + 'source' => 'https://www.dicebear.com', + 'license_name' => 'CC0 1.0', + 'license_url' => 'https://creativecommons.org/publicdomain/zero/1.0/', +), +'view_box' => '0 0 100 100', +'clip_default' => true, +'has_pre_create' => false, +'has_post_create' => true, +'is_initials' => false, +'attr_pairs' => array( + array('viewBox', '0 0 100 100'), + array('fill', 'none'), + array('shape-rendering', 'auto'), +), +'bg_colors' => array('0a5b83', '1c799f', '69d2e7', 'f1f4dc', 'f88c49'), +'bg_type' => array('solid'), +'bg_rotation' => array('0', '360'), +'body_template' => '@@k:shape@@', +'pick_order' => array('shape', 'face', 'eyes', 'mouth'), +'bool_order' => array(), +'color_order' => array( + 'shape' => 'shapeColor', + 'mouth' => 'mouthColor', + 'eyes' => 'eyesColor', +), +'pc_has_ints' => true, +'pc_meta' => array( + array( + 'group' => 'shape', + 'values_opt' => 'shape', + 'width' => 100, + 'height' => 140, + 'rotation' => array('opt' => 'shapeRotation', 'fallback' => array('0')), + 'offsetX' => array('opt' => 'shapeOffsetX', 'fallback' => array('0')), + 'offsetY' => array('opt' => 'shapeOffsetY', 'fallback' => array('0')), + ), + array( + 'group' => 'face', + 'values_opt' => 'face', + 'width' => 42, + 'height' => 42, + 'rotation' => array('opt' => 'faceRotation', 'fallback' => array('0')), + 'offsetX' => array('opt' => 'faceOffsetX', 'fallback' => array('0')), + 'offsetY' => array('opt' => 'faceOffsetY', 'fallback' => array('0')), + ), + array( + 'group' => 'eyes', + 'values_opt' => 'eyes', + 'width' => 42, + 'height' => 16, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), + array( + 'group' => 'mouth', + 'values_opt' => 'mouth', + 'width' => 30, + 'height' => 14, + 'rotation' => array('opt' => null, 'fallback' => array('0')), + 'offsetX' => array('opt' => null, 'fallback' => array('0')), + 'offsetY' => array('opt' => null, 'fallback' => array('0')), + ), +), +'defaults' => array( + 'backgroundColor' => array('0a5b83', '1c799f', '69d2e7', 'f1f4dc', 'f88c49'), + 'eyes' => array('variant1W10', 'variant1W12', 'variant1W14', 'variant1W16', 'variant2W10', 'variant2W12', 'variant2W14', 'variant2W16', 'variant3W10', 'variant3W12', 'variant3W14', 'variant3W16', 'variant4W10', 'variant4W12', 'variant4W14', 'variant4W16', 'variant5W10', 'variant5W12', 'variant5W14', 'variant5W16', 'variant6W10', 'variant6W12', 'variant6W14', 'variant6W16', 'variant7W10', 'variant7W12', 'variant7W14', 'variant7W16', 'variant8W10', 'variant8W12', 'variant8W14', 'variant8W16', 'variant9W10', 'variant9W12', 'variant9W14', 'variant9W16'), + 'eyesColor' => array('000000', 'ffffff'), + 'face' => array('variant1', 'variant2', 'variant3', 'variant5', 'variant4'), + 'faceOffsetX' => array('-15', '15'), + 'faceOffsetY' => array('-15', '15'), + 'faceRotation' => array('-20', '20'), + 'mouth' => array('variant2', 'variant1', 'variant3', 'variant4', 'variant5'), + 'mouthColor' => array('000000', 'ffffff'), + 'shape' => array('default'), + 'shapeColor' => array('0a5b83', '1c799f', '69d2e7', 'f1f4dc', 'f88c49'), + 'shapeOffsetX' => array('-5', '5'), + 'shapeOffsetY' => array('-5', '5'), + 'shapeRotation' => array('-20', '20'), +), +'groups' => array( + 'shape' => array( + 'default' => '@@k:face@@', + ), + 'face' => array( + 'variant1' => '@@k:eyes@@@@k:mouth@@', + 'variant2' => '@@k:eyes@@@@k:mouth@@', + 'variant3' => '@@k:eyes@@@@k:mouth@@', + 'variant5' => '@@k:eyes@@@@k:mouth@@', + 'variant4' => '@@k:eyes@@@@k:mouth@@', + ), + 'eyes' => array( + 'variant1W10' => '', + 'variant1W12' => '', + 'variant1W14' => '', + 'variant1W16' => '', + 'variant2W10' => '', + 'variant2W12' => '', + 'variant2W14' => '', + 'variant2W16' => '', + 'variant3W10' => '', + 'variant3W12' => '', + 'variant3W14' => '', + 'variant3W16' => '', + 'variant4W10' => '', + 'variant4W12' => '', + 'variant4W14' => '', + 'variant4W16' => '', + 'variant5W10' => '', + 'variant5W12' => '', + 'variant5W14' => '', + 'variant5W16' => '', + 'variant6W10' => '', + 'variant6W12' => '', + 'variant6W14' => '', + 'variant6W16' => '', + 'variant7W10' => '', + 'variant7W12' => '', + 'variant7W14' => '', + 'variant7W16' => '', + 'variant8W10' => '', + 'variant8W12' => '', + 'variant8W14' => '', + 'variant8W16' => '', + 'variant9W10' => '', + 'variant9W12' => '', + 'variant9W14' => '', + 'variant9W16' => '', + ), + 'mouth' => array( + 'variant2' => '', + 'variant1' => '', + 'variant3' => '', + 'variant4' => '', + 'variant5' => '', + ), +), +); \ No newline at end of file diff --git a/inc/theme-avatar.php b/inc/theme-avatar.php new file mode 100644 index 0000000..5b7f47d --- /dev/null +++ b/inc/theme-avatar.php @@ -0,0 +1,1906 @@ + + * @license MIT License + */ + +if (!defined('ABSPATH')) { + exit; +} + +/* ══════════════════════════════════════════════════════════════════ + * UTF-8 工具:**全部不依赖 mbstring** + * + * ── 为什么不能用 mb_* ──────────────────────────────────────────── + * mbstring 是 PHP 的**可选扩展**。线上这台机器装了,但主题是要发给 + * 别人装的 —— 谁也不能保证对方的虚拟主机开了 mbstring。一旦没开, + * `mb_substr()` 就是"调用未定义函数",PHP 7 里直接 Fatal error, + * **整个站点白屏**,而报错指向头像模块,跟"没装扩展"看起来毫无关系。 + * + * 所以要用的三件事都自己实现: + * · 按码点切开字符串(utf8_split) + * · 取前 N 个字符(utf8_substr) + * · 转大写(utf8_upper,只处理 Latin-1 + 常见希腊/西里尔) + * + * 转大写本来最麻烦(Unicode 有大写映射表),但这里只用在「首字母」 + * 上,输入是 `\p{L}` 单个字符。真实场景里 99% 是 ASCII 或中文: + * 中文没有大小写,原样返回即可;ASCII 走 strtoupper。其余语种按 + * 原样返回 —— 首字母不大写不影响正确性,只是观感略差, + * 远比"白屏"可接受。 + * ══════════════════════════════════════════════════════════════════ */ + +/** + * 单个 UTF-8 字符 → 码点(纯字节运算) + * + * @param string $ch 一个完整 UTF-8 字符 + * @return int|null 非法序列返回 null + */ +function yang_avatar_cp($ch) +{ + $n = strlen($ch); + if ($n === 0) { + return null; + } + $b0 = ord($ch[0]); + if ($n === 1) { + // 纯 ASCII + if ($b0 < 0x80) { + return $b0; + } + return null; + } + if ($n === 2) { + $b1 = ord($ch[1]); + if (($b0 & 0xE0) !== 0xC0 || ($b1 & 0xC0) !== 0x80) { + return null; + } + return (($b0 & 0x1F) << 6) | ($b1 & 0x3F); + } + if ($n === 3) { + $b1 = ord($ch[1]); + $b2 = ord($ch[2]); + if (($b0 & 0xF0) !== 0xE0 || ($b1 & 0xC0) !== 0x80 || ($b2 & 0xC0) !== 0x80) { + return null; + } + return (($b0 & 0x0F) << 12) | (($b1 & 0x3F) << 6) | ($b2 & 0x3F); + } + if ($n === 4) { + $b1 = ord($ch[1]); + $b2 = ord($ch[2]); + $b3 = ord($ch[3]); + if (($b0 & 0xF8) !== 0xF0 || ($b1 & 0xC0) !== 0x80 + || ($b2 & 0xC0) !== 0x80 || ($b3 & 0xC0) !== 0x80) { + return null; + } + return (($b0 & 0x07) << 18) | (($b1 & 0x3F) << 12) + | (($b2 & 0x3F) << 6) | ($b3 & 0x3F); + } + return null; +} + +/** + * UTF-8 字符串 → 字符数组(每个元素是一个完整字符) + * + * 遇到非法字节时**逐字节跳过并记一个 U+FFFD**,不整串放弃 —— + * 邮箱理论上都是 ASCII,但注释作者名/昵称可能是乱码, + * 那时应该画个替代符,而不是让整张头像渲染失败。 + * + * @param string $s + * @return string[] + */ +function yang_avatar_utf8_split($s) +{ + $out = array(); + $len = strlen($s); + $i = 0; + while ($i < $len) { + $b = ord($s[$i]); + if ($b < 0x80) { + $w = 1; + } elseif (($b & 0xE0) === 0xC0) { + $w = 2; + } elseif (($b & 0xF0) === 0xE0) { + $w = 3; + } elseif (($b & 0xF8) === 0xF0) { + $w = 4; + } else { + // 非法起始字节 + $out[] = "\xEF\xBF\xBD"; + $i++; + continue; + } + if ($i + $w > $len) { + // 结尾被截断 + $out[] = "\xEF\xBF\xBD"; + break; + } + $ch = substr($s, $i, $w); + if (yang_avatar_cp($ch) === null) { + $out[] = "\xEF\xBF\xBD"; + $i++; + continue; + } + $out[] = $ch; + $i += $w; + } + return $out; +} + +/** + * 取前 $n 个字符(替代 mb_substr($s, 0, $n, 'UTF-8')) + * + * @param string $s + * @param int $n + * @return string + */ +function yang_avatar_utf8_sub($s, $n) +{ + $n = (int) $n; + if ($n <= 0) { + return ''; + } + // ASCII 快路径:绝大多数种子(邮箱)都走这里 + if (!preg_match('/[\x80-\xFF]/', $s)) { + return strlen($s) <= $n ? $s : substr($s, 0, $n); + } + $chars = yang_avatar_utf8_split($s); + if (count($chars) <= $n) { + return $s; + } + return implode('', array_slice($chars, 0, $n)); +} + +/** + * 转大写(只保证 ASCII 与 Latin-1 补集,其余原样返回) + * + * @param string $s + * @return string + */ +function yang_avatar_utf8_upper($s) +{ + if ($s === '') { + return ''; + } + if (!preg_match('/[\x80-\xFF]/', $s)) { + return strtoupper($s); + } + // 带重音的拉丁字母:逐个码点处理,非 ASCII 的原样保留 + $chars = yang_avatar_utf8_split($s); + $out = ''; + foreach ($chars as $ch) { + $cp = yang_avatar_cp($ch); + if ($cp === null || $cp > 0x7F) { + $out .= $ch; + } else { + $out .= strtoupper($ch); + } + } + return $out; +} + +/** + * UTF-8 字符串 → UTF-16 码元数组(JS charCodeAt 的语义) + * + * @param string $s + * @return int[] + */ +function yang_avatar_utf16_units($s) +{ + $out = array(); + foreach (yang_avatar_utf8_split($s) as $ch) { + $cp = yang_avatar_cp($ch); + if ($cp === null) { + $out[] = 0xFFFD; + continue; + } + if ($cp <= 0xFFFF) { + // 单个"代理区"码元直接照搬(含 U+D800..U+DFFF 这种非法单值) + $out[] = $cp; + } else { + $cp -= 0x10000; + $out[] = 0xD800 + ($cp >> 10); + $out[] = 0xDC00 + ($cp & 0x3FF); + } + } + return $out; +} + +/** + * 与 DiceBear 9.x 完全一致的 PRNG + * + * 对照 @dicebear/core/lib/utils/prng.js: + * const MIN = -2147483648, MAX = 2147483647; + * function xorshift(value) { + * value ^= value << 13; value ^= value >> 17; value ^= value << 5; + * return value; + * } + * function hashSeed(seed) { + * let hash = 0; + * for (let i = 0; i < seed.length; i++) { + * hash = ((hash << 5) - hash + seed.charCodeAt(i)) | 0; + * hash = xorshift(hash); + * } + * return hash; + * } + * + * 三处必须小心的地方: + * ① `<<` `>>` `| 0` 都是 32 位有符号运算,PHP 里要逐步 i32() 归一; + * ② `>>` 是算术右移(保住符号位),PHP 的 >> 对负数行为一致, + * 但前提是先把值收紧到 int32; + * ③ charCodeAt 取的是 **UTF-16 码元**,不是 UTF-8 字节也不是码点。 + * emoji 这类增补平面字符在 JS 里占两个码元(代理对), + * 必须真的拆分,否则哈希完全不同。 + */ +class Yang_Avatar_Prng +{ + const MIN = -2147483648; + const MAX = 2147483647; + + /** @var int 当前状态 */ + private $value; + + /** + * 截断成 JS 的 int32(有符号) + * + * @param int $v + * @return int + */ + private static function i32($v) + { + $v = $v & 0xFFFFFFFF; + if ($v >= 0x80000000) { + $v -= 0x100000000; + } + return $v; + } + + /** + * JS 的 `value << n`(32 位有符号左移) + * + * @param int $v + * @param int $n + * @return int + */ + private static function shl($v, $n) + { + return self::i32(self::i32($v) << $n); + } + + /** + * JS 的 `value >> n`(32 位有符号算术右移) + * + * @param int $v + * @param int $n + * @return int + */ + private static function shr($v, $n) + { + return self::i32(self::i32($v) >> $n); + } + + /** + * 与 JS 逐位一致的 xorshift + * + * @param int $value + * @return int + */ + private static function xorshift($value) + { + $value = self::i32($value); + $value = self::i32($value ^ self::shl($value, 13)); + $value = self::i32($value ^ self::shr($value, 17)); + $value = self::i32($value ^ self::shl($value, 5)); + return $value; + } + + /** + * UTF-8 字符串 → UTF-16 码元数组 + * + * 不能直接用码点:JS 的 charCodeAt 按 UTF-16 码元取值, + * U+1F389 这种在 JS 里是 0xD83C 0xDF89 两个码元。 + * + * 直接走字节,不用 mb_* —— 见文件头的「为什么不用 mbstring」。 + * + * @param string $s + * @return int[] + */ + private static function utf16_code_units($s) + { + return yang_avatar_utf16_units($s); + } + + /** + * 单个 UTF-8 字符 → Unicode 码点 + * + * 不用 mb_ord():它是 PHP 7.2 才有的函数,而主题基线是 7.0。 + * 实现见 yang_avatar_cp(),那里带静态缓存。 + * + * @param string $ch + * @return int|null + */ + private static function code_point($ch) + { + static $cache = array(); + if (array_key_exists($ch, $cache)) { + return $cache[$ch]; + } + $v = yang_avatar_cp($ch); + $cache[$ch] = $v; + return $v; + } + + /** + * 与 JS 逐位一致的 hashSeed + * + * @param string $seed + * @return int + */ + private static function hash_seed($seed) + { + $hash = 0; + $units = self::utf16_code_units($seed); + $n = count($units); + for ($i = 0; $i < $n; $i++) { + $hash = self::i32(self::shl($hash, 5) - $hash + $units[$i]); + $hash = self::xorshift($hash); + } + return $hash; + } + + /** + * @param string $seed + */ + public function __construct($seed = '') + { + // JS: seed.toString().slice(0, 1024) —— 防止超长种子拖垮 CPU + $seed = yang_avatar_utf8_sub((string) $seed, 1024); + $h = self::hash_seed($seed); + // hashSeed 可能返回 0,此时退化成 1(否则 xorshift 永远停在 0) + $this->value = $h !== 0 ? $h : 1; + } + + /** + * @return int + */ + public function next() + { + $this->value = self::xorshift($this->value); + return $this->value; + } + + /** + * 闭区间 [min, max] 取整 + * + * @param int $min + * @param int $max + * @return int + */ + public function integer($min, $max) + { + $v = $this->next(); + $ratio = ($v - self::MIN) / (self::MAX - self::MIN); + return (int) floor($ratio * ($max + 1 - $min) + $min); + } + + /** + * @param int $likelihood 百分比 + * @return bool + */ + public function bool($likelihood = 50) + { + return $this->integer(1, 100) <= $likelihood; + } + + /** + * 从数组里取一个;空数组时**仍然消耗一次** next(),保证调用次数一致 + * + * @param array $arr + * @param mixed $fallback + * @return mixed + */ + public function pick($arr, $fallback = null) + { + if (!is_array($arr) || count($arr) === 0) { + $this->next(); + return $fallback; + } + return $arr[$this->integer(0, count($arr) - 1)]; + } + + /** + * 与 DiceBear 一致的 Fisher-Yates 洗牌 + * + * 关键:**只消耗本实例一次 next()**,其余随机数来自一个子 PRNG + * (种子 = 那次 next() 结果的十进制字符串)。若这里多消耗或少消耗, + * 后面 backgroundRotation 取到的值就歪了,整张图的背景色也会错。 + * + * @param array $arr + * @return array + */ + public function shuffle($arr) + { + $child = new self((string) $this->next()); + $n = count($arr); + $work = array_values($arr); + for ($i = $n - 1; $i > 0; $i--) { + $j = $child->integer(0, $i); + $tmp = $work[$i]; + $work[$i] = $work[$j]; + $work[$j] = $tmp; + } + return $work; + } +} + +/** + * 收录的风格清单 + * + * 只收 CC0 1.0 与 MIT。CC BY 4.0 那些(adventurer / avataaars / bottts / + * croodles / micah / toon-head 等)虽然更好看,但要求保留署名 —— + * 主题是给用户装的,用户不会去维护署名,等于把合规义务转嫁给用户。 + * + * 'label' 用中文,因为后台是给中文站长看的。 + * 'size' 是模板文件体积,用于后台提示「这套会占多少」。 + * + * @return array + */ +function yang_avatar_styles() +{ + return array( + 'lorelei' => array( + 'label' => __('柔和卡通', 'kratos'), + 'desc' => __('五官柔和,男女都有,头发变化多。最接近"真人头像"的一套。', 'kratos'), + ), + 'notionists' => array( + 'label' => __('简笔线稿', 'kratos'), + 'desc' => __('Notion 那种极简线稿。纯黑白,不挑主题配色。', 'kratos'), + ), + 'notionists-neutral' => array( + 'label' => __('简笔线稿(带底色)', 'kratos'), + 'desc' => __('跟上一套同一画风,但带一块背景色,更醒目一点。', 'kratos'), + ), + 'open-peeps' => array( + 'label' => __('手绘速写', 'kratos'), + 'desc' => __('手绘感强,表情夸张,很有个性。适合偏文艺的博客。', 'kratos'), + ), + 'lorelei-neutral' => array( + 'label' => __('柔和卡通(带底色)', 'kratos'), + 'desc' => __('同一套画风,带背景色块。', 'kratos'), + ), + 'shapes' => array( + 'label' => __('几何色块', 'kratos'), + 'desc' => __('随机几何图形叠加,不涉及人脸,永远不会"崩脸"。', 'kratos'), + ), + 'glass' => array( + 'label' => __('毛玻璃', 'kratos'), + 'desc' => __('模糊叠加的字母玻璃效果,设计感强。颜色比较鲜艳。', 'kratos'), + ), + 'rings' => array( + 'label' => __('同心圆环', 'kratos'), + 'desc' => __('极简同心圆,最不打扰视觉的一套。', 'kratos'), + ), + 'identicon' => array( + 'label' => __('对称方块', 'kratos'), + 'desc' => __('GitHub 那种 5x5 对称色块,程序员风格。', 'kratos'), + ), + 'thumbs' => array( + 'label' => __('拇指', 'kratos'), + 'desc' => __('只有"拇指"一个形象,靠表情与配色区分。', 'kratos'), + ), + 'pixel-art' => array( + 'label' => __('像素风', 'kratos'), + 'desc' => __('8-bit 像素人像。风格强烈,适合游戏/技术类博客。', 'kratos'), + ), + 'pixel-art-neutral' => array( + 'label' => __('像素风(带底色)', 'kratos'), + 'desc' => __('同一套像素画风,带背景色块。', 'kratos'), + ), + 'icons' => array( + 'label' => __('图标', 'kratos'), + 'desc' => __('抽象图标剪影,很干净。', 'kratos'), + ), + 'initials' => array( + 'label' => __('首字母', 'kratos'), + 'desc' => __('显示邮箱对应的首字母文字。若访客用中文邮箱会显示拼音首字母。', 'kratos'), + ), + ); +} + +/** + * 当前是否启用了自动头像 + * + * @return bool + */ +function yang_avatar_active() +{ + $on = kratos_option('a_auto_enable', '0'); + return $on === '1' || $on === 1 || $on === true; +} + +/** + * 当前选中的风格名(已校验,非法值回落到默认风格) + * + * @return string + */ +function yang_avatar_style() +{ + static $cached = null; + if ($cached !== null) { + return $cached; + } + $all = yang_avatar_styles(); + $want = kratos_option('a_auto_style', 'lorelei'); + if (is_string($want) && isset($all[$want])) { + $cached = $want; + } else { + $cached = 'lorelei'; + } + return $cached; +} + +/** + * 加载某套风格的编译后模板 + * + * 只 require 用到的那一份,其余不解析 —— 14 套全量 1.1MB, + * 每次请求都加载是不必要的开销。 + * + * @param string $style + * @return array|null + */ +function yang_avatar_load_style($style) +{ + static $loaded = array(); + if (isset($loaded[$style])) { + return $loaded[$style]; + } + $all = yang_avatar_styles(); + if (!isset($all[$style])) { + $loaded[$style] = null; + return null; + } + // 风格名 → 文件名:- 换成 _ + $file = str_replace('-', '_', $style); + $path = get_template_directory() . '/inc/avatar-styles/' . $file . '.php'; + if (!file_exists($path)) { + $loaded[$style] = null; + return null; + } + $data = require $path; + $loaded[$style] = is_array($data) ? $data : null; + return $loaded[$style]; +} + +/** + * 人类调色板 + * + * ── 为什么必须自带 ──────────────────────────────────────────────── + * DiceBear 的默认调色板几乎全是单值:hairColor / eyesColor / mouthColor + * 都只有 '000000',skinColor 只有 'ffffff'。实测不换调色板的话, + * 30 个不同邮箱只会产生 2 种配色 —— 这才是"默认头像丑"的真正原因, + * 光换风格解决不了。 + * + * 这里的键名对应各风格 schema 里的颜色字段;风格没有的键会被忽略。 + * + * @return array + */ +function yang_avatar_palette() +{ + return array( + // 背景色:低饱和,跟大多数主题都能搭 + 'backgroundColor' => array( + '8ecae6', '219ebc', 'ffb703', 'fb8500', '90be6d', '43aa8b', + 'f8961e', 'f9c74f', '4d908e', '577590', 'f94144', 'f3722c', + 'a8dadc', '457b9d', + ), + // 肤色:从浅到深,覆盖常见人种 + 'skinColor' => array( + 'f2d3b8', 'e8b98f', 'd9a066', 'c68642', '8d5524', 'ffdbac', 'f1c27d', + ), + // 发色 + 'hairColor' => array( + '2c1b18', '4a312c', '6a4e42', 'a55728', 'b89778', 'd6b370', + 'c93305', '1a1a1a', '5a3825', 'e6cea8', + ), + // 衣服色 + 'clothesColor' => array( + '2f4858', '33658a', '86bbd8', 'f6ae2d', 'f26419', '758e4f', + '5d2e46', '1b4079', '8f2d56', '3d315b', + ), + ); +} + +/** + * 把调色板映射到某套风格实际拥有的颜色字段 + * + * 不同风格用的是不同的键名(skinColor / baseColor、clothesColor / + * shirtColor / bodyColor …)。这里只给风格**真实存在**的字段赋值, + * 不存在的键不能塞进 options,否则会打乱 pick 的取值。 + * + * @param array $style 模板数据 + * @return array 要叠加到 options 上的键值 + */ +function yang_avatar_style_palette($style) +{ + $pal = yang_avatar_palette(); + $props = isset($style['defaults']) && is_array($style['defaults']) + ? $style['defaults'] : array(); + + // 字段名 → 调色板类别 + $map = array( + 'backgroundColor' => 'backgroundColor', + 'skinColor' => 'skinColor', + 'baseColor' => 'skinColor', + 'hairColor' => 'hairColor', + 'topColor' => 'hairColor', + 'beardColor' => 'hairColor', + 'facialHairColor' => 'hairColor', + 'eyebrowsColor' => 'hairColor', + 'clothesColor' => 'clothesColor', + 'shirtColor' => 'clothesColor', + 'bodyColor' => 'clothesColor', + 'clothingColor' => 'clothesColor', + ); + + $out = array(); + foreach ($map as $field => $kind) { + // 只处理风格真实声明过的字段 + if (array_key_exists($field, $props) && isset($pal[$kind])) { + $out[$field] = $pal[$kind]; + } + } + return $out; +} + +/** + * 渲染一张头像 + * + * @param string $seed 种子(用邮箱即可;空则回落到站点地址) + * @param string $style 风格名,空则用后台选中的 + * @param int $size 像素边长 + * @return string SVG 源码;失败时返回空串 + */ +function yang_avatar_svg($seed, $style = '', $size = 96) +{ + if ($style === '') { + $style = yang_avatar_style(); + } + $data = yang_avatar_load_style($style); + if ($data === null) { + return ''; + } + + $seed = trim((string) $seed); + if ($seed === '') { + // 没有邮箱时不能返回空 —— 那就所有匿名访客共用一张脸, + // 用站点地址兜底,至少是稳定的一张。 + $seed = home_url(); + } + + $size = (int) $size; + if ($size <= 0) { + $size = 96; + } + + $palette = yang_avatar_style_palette($data); + + return yang_avatar_render($data, $seed, $size, $palette); +} + +/** + * 供后台预览用的 data URI + * + * @param string $seed + * @param string $style + * @param int $size + * @return string + */ +function yang_avatar_data_uri($seed, $style = '', $size = 96) +{ + $svg = yang_avatar_svg($seed, $style, $size); + if ($svg === '') { + return ''; + } + return 'data:image/svg+xml;base64,' . base64_encode($svg); +} + +/** + * 接 get_avatar 过滤器 + * + * ── 判据:只动「原本指向 Gravatar 默认图」的标签 ────────────────── + * WP 的 get_avatar() 从不去查 Gravatar 到底有没有这张图 —— 它只是 + * 拼一个 `https:///avatar/?s=48&d=mm&r=g` 交给浏览器, + * 由 Gravatar 服务端在加载时决定返回真人头像还是默认图。 + * + * 也就是说:**服务端离线状态下,无法区分「这个邮箱有 Gravatar」和 + * 「没有、会退回默认图」**。任何声称能精确判断的写法(比如去看 + * 响应状态码)都要发一次外网请求,而 gravatar.com 在国内全部超时, + * 这条路根本走不通。 + * + * 所以这里退一步,用「src 指向谁」做判据: + * · src 是 data: → 说明别的插件/主题已经给过头像了,放行; + * · src 主机属于 Gravatar 家族 → 替换成生成图; + * · 其它(媒体库图片、外链头像)→ 放行。 + * + * 代价:如果站长开了「Gravatar 加速」镜像、并且访客确实有 Gravatar + * 头像,这里也会替换掉。这是有意的取舍 —— 相比"偶尔盖掉一个真实 + * 头像","多数访客是一张破图/灰头像"更伤。选项说明里写清了这一点。 + * + * ── 为什么不再重新拼整个 ──────────────────────────────────── + * 早先的实现直接用 $args 里的 class / extra_attr 重新拼一个标签。 + * 这有两个坑: + * ① get_avatar() 输出了 `srcset`(2x/3x)。只改 src、留着 srcset, + * 浏览器会**优先用 srcset**,我们生成的图根本不会被加载 —— + * 现象是"代码明明换了,页面上还是老样子"。 + * ② `decoding="async"`、`loading="lazy"`、`fetchpriority` 这类 + * 较新 WP 版本才输出的属性,$args 里并不完整。 + * 改成在原字符串上做定点替换:删掉 srcset/sizes,只换 src, + * 其余属性一律原样保留。 + * + * @param string $avatar 已经拼好的 标签 + * @param mixed $id_or_email + * @param int $size + * @param string $default + * @param string $alt + * @param array $args + * @return string + */ +function yang_avatar_filter($avatar, $id_or_email, $size, $default, $alt, $args = array()) +{ + if (!yang_avatar_active()) { + return $avatar; + } + + // 不是 就没有可换的地方(get_avatar 也可能是评论里的一段 HTML) + if (!is_string($avatar) || stripos($avatar, ' 256) { + $px = 256; + } + + $svg = yang_avatar_svg($email, '', $px); + if ($svg === '') { + return $avatar; + } + + $uri = 'data:image/svg+xml;base64,' . base64_encode($svg); + + // ① 删 srcset:不删的话浏览器优先用 srcset,替换等于没做 + $out = preg_replace('/\s+srcset\s*=\s*(["\']).*?\1/isu', '', $avatar, 1); + if (!is_string($out)) { + $out = $avatar; + } + // ② 删 sizes:srcset 去了之后 sizes 已无意义,留着是脏属性 + $out = preg_replace('/\s+sizes\s*=\s*(["\']).*?\1/isu', '', $out, 1); + if (!is_string($out)) { + $out = $avatar; + } + // ③ 只换 src + $out = preg_replace( + '/\ssrc\s*=\s*(["\'])[^"\']*\1/isu', + ' src="' . esc_attr($uri) . '"', + $out, + 1 + ); + if (!is_string($out) || $out === '') { + return $avatar; + } + + return $out; +} +add_filter('get_avatar', 'yang_avatar_filter', 20, 6); + +/** + * 判断一个头像 URL 是不是 Gravatar 家族的 + * + * 除了 gravatar.com 自身,还要认这些: + * · gravatar.loli.net —— 本主题「Gravatar 加速」用的镜像 + * · secure.gravatar.com / cn.gravatar.com / 0.gravatar.com 等子域 + * · i0.wp.com / i1.wp.com / i2.wp.com —— Jetpack 的头像 CDN 代理 + * + * 一律按主机名后缀判断,不做整串 contains: + * 否则 `https://evil.test/?x=gravatar.com` 这种也会被当成 Gravatar。 + * + * @param string $url + * @return bool + */ +function yang_avatar_is_gravatar_url($url) +{ + $host = parse_url($url, PHP_URL_HOST); + if (!is_string($host) || $host === '') { + return false; + } + $host = strtolower($host); + + $suffixes = array( + '.gravatar.com', + 'gravatar.com', + '.loli.net', + 'loli.net', + '.wp.com', + '.gravatar.com.cn', + '.geekzu.org', + '.233v4.com', + '.nihao.eu.org', + ); + foreach ($suffixes as $sfx) { + $len = strlen($sfx); + if (strlen($host) >= $len && substr($host, -$len) === $sfx) { + return true; + } + } + return false; +} + +/** + * 从 get_avatar 的各种入参里取出邮箱 + * + * $id_or_email 可能是:用户 ID、WP_User、WP_Comment、WP_Post、邮箱字符串。 + * + * @param mixed $id_or_email + * @return string + */ +function yang_avatar_email_of($id_or_email) +{ + if (is_string($id_or_email)) { + // 已经是邮箱 + if (strpos($id_or_email, '@') !== false) { + return trim($id_or_email); + } + return ''; + } + if (is_object($id_or_email)) { + // WP_Comment / WP_User / WP_Post + if (isset($id_or_email->comment_author_email)) { + return trim((string) $id_or_email->comment_author_email); + } + if (isset($id_or_email->user_email)) { + return trim((string) $id_or_email->user_email); + } + if (isset($id_or_email->post_author)) { + $u = get_userdata((int) $id_or_email->post_author); + if ($u && !empty($u->user_email)) { + return trim((string) $u->user_email); + } + } + if (isset($id_or_email->ID)) { + $u = get_userdata((int) $id_or_email->ID); + if ($u && !empty($u->user_email)) { + return trim((string) $u->user_email); + } + } + return ''; + } + if (is_int($id_or_email)) { + $u = get_userdata($id_or_email); + if ($u && !empty($u->user_email)) { + return trim((string) $u->user_email); + } + } + return ''; +} + +/* ══════════════════════════════════════════════════════════════════ + * 渲染器本体 + * ══════════════════════════════════════════════════════════════════ */ + +/** + * 渲染一张 SVG + * + * 必须严格复刻 @dicebear/core 里 createAvatar() 的**执行顺序**, + * 因为 PRNG 是有状态的: + * 1. style.create() 内部: + * onPreCreate() → getComponents() → getColors() → onPostCreate() + * 2. body = create() 返回的 body + * 3. 然后 core 才依次做: + * backgroundType = prng.pick(options.backgroundType, 'solid') + * primary/secondary = getBackgroundColors(prng, options.backgroundColor, bgType) + * backgroundRotation = prng.integer(min, max) + * background 层 → clip mask + * 顺序错一个,后面全歪。 + * + * @param array $style 模板数据 + * @param string $seed + * @param int $size + * @param array $palette 叠加到 options 上的颜色 + * @return string + */ +function yang_avatar_render($style, $seed, $size = 96, $palette = array()) +{ + $d = $style['defaults']; + foreach ($palette as $k => $v) { + $d[$k] = $v; + } + + $p = new Yang_Avatar_Prng($seed); + + /* ── 0. onPreCreate(会动态改 options,必须在一切之前) ── */ + if (!empty($style['has_pre_create'])) { + $d = yang_avatar_pre_create($style['name'], $p, $d); + } + + /* ── 1. getComponents ── + 两种语义并存: + a) 普通 prng.pick(values) —— 只消耗 1 次 + b) pickComponent(...) —— 额外取 rotation/offsetX/offsetY 三次整数 + 但**只有** glass / rings / shapes / thumbs 的 pickComponent 会取整数; + 其余风格虽然也调 pickComponent,却是简化版(只 pick)。 + 若对它们多取三次整数,PRNG 序列会多走 3N 步、整张图全变 + (实测 31 种里 27 种会挂)。编译期已用 pc_has_ints 标好。 */ + $pcMap = array(); + if (!empty($style['pc_meta']) && is_array($style['pc_meta'])) { + foreach ($style['pc_meta'] as $m) { + if ($m['group'] !== '') { + $pcMap[$m['group']] = $m; + } + } + } + $useInts = !empty($style['pc_has_ints']); + + $picked = array(); + $meta = array(); + foreach ($style['pick_order'] as $g) { + $vkey = (isset($pcMap[$g]) && $pcMap[$g]['values_opt'] !== '') + ? $pcMap[$g]['values_opt'] : $g; + $vals = (isset($d[$vkey]) && is_array($d[$vkey])) ? $d[$vkey] : array(); + $picked[$g] = $p->pick($vals); + + if ($useInts && isset($pcMap[$g])) { + $m = $pcMap[$g]; + $meta[$g] = array( + 'rotation' => yang_avatar_int_range($p, $m['rotation'], $d), + 'offsetX' => yang_avatar_int_range($p, $m['offsetX'], $d), + 'offsetY' => yang_avatar_int_range($p, $m['offsetY'], $d), + 'width' => $m['width'], + 'height' => $m['height'], + ); + } + } + + $on = array(); + foreach ($style['bool_order'] as $g) { + $pk = $g . 'Probability'; + $prob = isset($d[$pk]) ? $d[$pk] : 50; + $on[$g] = $p->bool($prob); + } + + /* ── 2. getColors ── */ + $colors = array(); + foreach ($style['color_order'] as $ck => $sk) { + $vals = (isset($d[$sk]) && is_array($d[$sk])) ? $d[$sk] : array(); + $colors[$ck] = yang_avatar_color($p->pick($vals, 'transparent')); + } + + /* ── 3. onPostCreate(会改 colors / options.backgroundColor) ── */ + if (!empty($style['has_post_create'])) { + $res = yang_avatar_post_create($style['name'], $d, $colors, $on); + $d = $res[0]; + $colors = $res[1]; + } + + /* ── 4. 展开 body ── */ + $subs = array(); + foreach ($style['groups'] as $g => $bag) { + if (in_array($g, $style['bool_order'], true) && empty($on[$g])) { + $subs[$g] = null; + } else { + $subs[$g] = isset($picked[$g]) ? $picked[$g] : null; + } + } + + if (!empty($style['is_initials'])) { + $body = yang_avatar_initials_body($d, $seed, $p); + } else { + $body = yang_avatar_expand($style, $subs, $colors, $meta); + } + + /* ── 5. core 的背景(顺序不能提前!) ── */ + $bgTypeArr = (isset($d['backgroundType']) && is_array($d['backgroundType'])) + ? $d['backgroundType'] : $style['bg_type']; + $bgType = $p->pick($bgTypeArr, 'solid'); + + $bgColors = (isset($d['backgroundColor']) && is_array($d['backgroundColor'])) + ? $d['backgroundColor'] : $style['bg_colors']; + $bg = yang_avatar_bg_colors($p, $bgColors, $bgType); + + $rotArr = (isset($d['backgroundRotation']) && is_array($d['backgroundRotation'])) + ? $d['backgroundRotation'] : $style['bg_rotation']; + $rotMin = count($rotArr) ? min($rotArr) : 0; + $rotMax = count($rotArr) ? max($rotArr) : 0; + $rotation = $p->integer($rotMin, $rotMax); + + /* ── 6. attributes(逐字保留,按编译期顺序) ── + initials 完全没有 fill / shape-rendering; + identicon / pixel-art / rings 是 crispEdges 而不是 auto。 */ + $attrs = array(); + foreach ($style['attr_pairs'] as $pair) { + $attrs[$pair[0]] = $pair[1]; + } + if ($size > 0) { + $attrs['width'] = (string) $size; + $attrs['height'] = (string) $size; + } + + /* ── 7. 背景层 ── */ + if ($bg['primary'] !== 'transparent' && $bg['secondary'] !== 'transparent') { + $body = yang_avatar_add_bg($body, $attrs, $bg['primary'], $bg['secondary'], $bgType, $rotation); + } + + /* ── 8. clip(core schema 默认 true)⇒ 恒定包 mask ── */ + if (!empty($style['clip_default'])) { + $vb = explode(' ', $attrs['viewBox']); + $body = '' + . '' + . '' + . '' . $body . ''; + } + + /* ── 9. 拼 SVG ── */ + $attrStr = 'xmlns="http://www.w3.org/2000/svg"'; + foreach ($attrs as $k => $v) { + $attrStr .= ' ' . yang_avatar_esc($k) . '="' . yang_avatar_esc($v) . '"'; + } + + return '' . yang_avatar_meta($style['meta']) . $body . ''; +} + +/** + * pickComponent 里的一次 integer 取值 + * + * 语义:options[$spec['opt']] 存在且非空 ⇒ 用它;否则用 $spec['fallback']。 + * + * @param Yang_Avatar_Prng $p + * @param array $spec + * @param array $d + * @return int + */ +function yang_avatar_int_range($p, $spec, $d) +{ + $arr = null; + if (!empty($spec['opt']) && isset($d[$spec['opt']]) && is_array($d[$spec['opt']]) + && count($d[$spec['opt']]) > 0) { + $arr = $d[$spec['opt']]; + } else { + $arr = $spec['fallback']; + } + if (!is_array($arr) || count($arr) === 0) { + $arr = array(0); + } + $nums = array(); + foreach ($arr as $v) { + $nums[] = (int) $v; + } + return $p->integer(min($nums), max($nums)); +} + +/** + * onPreCreate:按风格名精确复刻 + * + * 这些是**代码**(会动态改 options),不能当数据编译。 + * 收录的 14 套里只有 shapes 有,且它会消耗 3 次 pick。 + * + * @param string $name + * @param Yang_Avatar_Prng $p + * @param array $d + * @return array + */ +function yang_avatar_pre_create($name, $p, $d) +{ + if ($name === 'shapes') { + // 见 @dicebear/shapes/lib/hooks/onPreCreate.js: + // 三种形状各取一个"还没用过的颜色",并把用过的颜色从背景里剔掉。 + $used = array(); + foreach (array('shape1Color', 'shape2Color', 'shape3Color') as $key) { + $colors = isset($d[$key]) && is_array($d[$key]) ? $d[$key] : array(); + $avail = array(); + foreach ($colors as $c) { + if (!in_array($c, $used, true)) { + $avail[] = $c; + } + } + if (count($avail) === 0) { + $avail = $colors; + } + $c = $p->pick($avail, 'transparent'); + $used[] = $c; + $d[$key] = array($c); + } + $bg = isset($d['backgroundColor']) && is_array($d['backgroundColor']) + ? $d['backgroundColor'] : array(); + $avail = array(); + foreach ($bg as $c) { + if (!in_array($c, $used, true)) { + $avail[] = $c; + } + } + $d['backgroundColor'] = count($avail) > 0 ? $avail : $bg; + } + return $d; +} + +/** + * onPostCreate:按风格名精确复刻 + * + * @param string $name + * @param array $d + * @param array $colors + * @param array $on + * @return array array($d, $colors) + */ +function yang_avatar_post_create($name, $d, $colors, $on) +{ + if ($name === 'lorelei' || $name === 'lorelei-neutral') { + // 有胡子且胡子与嘴同色时,把嘴改成白色,否则看不出嘴 + if (!empty($on['beard']) && isset($colors['hair'], $colors['mouth']) + && $colors['hair'] === $colors['mouth']) { + $colors['mouth'] = '#ffffff'; + } + } elseif ($name === 'thumbs') { + // ① 背景色里去掉跟主体同色的那一项 + $shape = isset($colors['shape']) ? $colors['shape'] : ''; + $bg = isset($d['backgroundColor']) && is_array($d['backgroundColor']) + ? $d['backgroundColor'] : array(); + $filtered = array(); + foreach ($bg as $v) { + if ($v !== str_replace('#', '', $shape)) { + $filtered[] = $v; + } + } + if (count($filtered) > 0) { + $d['backgroundColor'] = $filtered; + } + // ② 眼睛/嘴按主体亮度取黑白对比色 + $contrast = null; + if (substr($shape, 0, 1) === '#') { + $r = hexdec(substr($shape, 1, 2)); + $g2 = hexdec(substr($shape, 3, 2)); + $b = hexdec(substr($shape, 5, 6)); + $yiq = ($r * 299 + $g2 * 587 + $b * 114) / 1000; + $contrast = $yiq >= 200 ? '#000000' : '#ffffff'; + } + if ($contrast !== null) { + foreach (array('eyes', 'mouth') as $ck) { + $opt = (isset($d[$ck . 'Color']) && is_array($d[$ck . 'Color'])) + ? $d[$ck . 'Color'] : array(); + if (count($opt) === 2 + && in_array('000000', $opt, true) + && in_array('ffffff', $opt, true)) { + $colors[$ck] = $contrast; + } + } + } + } + return array($d, $colors); +} + +/** + * initials 风格的 body 是代码拼出来的 ,单独复刻 + * + * @param array $d + * @param string $seed + * @param Yang_Avatar_Prng $p + * @return string + */ +function yang_avatar_initials_body($d, $seed, $p) +{ + // 顺序要紧:textColor 的 pick 在首字母计算**之前** + $tc = (isset($d['textColor']) && is_array($d['textColor'])) ? $d['textColor'] : array(); + $raw = $p->pick($tc); + if ($raw === null) { + $raw = 'ffffff'; + } + $textColor = yang_avatar_color($raw); + + $fontFamily = (isset($d['fontFamily']) && is_array($d['fontFamily'])) + ? implode(', ', $d['fontFamily']) : 'Arial, sans-serif'; + $fontSize = isset($d['fontSize']) ? $d['fontSize'] : 50; + $fontWeight = isset($d['fontWeight']) ? $d['fontWeight'] : 400; + $chars = isset($d['chars']) ? (int) $d['chars'] : 2; + + // JS 是 getInitials(prng.seed.trim()) —— 必须先 trim + $initials = yang_avatar_utf8_sub(yang_avatar_initials(trim($seed)), $chars); + + return '' + . yang_avatar_esc($initials) . ''; +} + +/** + * 复刻 @dicebear/initials 的 getInitials() + * + * 规则:先砍掉 @ 后面的部分,取首个与末个"词"的首字母。 + * 中文没有空格时整串算一个词,取前 2 个字符。 + * + * @param string $seed + * @param bool $discard_at + * @return string + */ +function yang_avatar_initials($seed, $discard_at = true) +{ + $input = $seed; + if ($discard_at) { + $input = preg_replace('/@.*/s', '', $input); + } + // 去掉各种撇号(与 DiceBear 的字符集一致) + $input = preg_replace("/[`´‘’'ʼ]/u", '', $input); + + if (!preg_match_all('/(\p{L}[\p{L}\p{M}]*)/u', $input, $mm)) { + // 一个字母都没匹配到 ⇒ 去掉 @ 限制再试一次 + return $discard_at ? yang_avatar_initials($seed, false) : ''; + } + $matches = $mm[0]; + if (count($matches) === 1) { + if (preg_match('/^(?:\p{L}\p{M}*){1,2}/u', $matches[0], $s)) { + return yang_avatar_utf8_upper($s[0]); + } + return ''; + } + $first = ''; + $last = ''; + if (preg_match('/^(?:\p{L}\p{M}*)/u', $matches[0], $s1)) { + $first = $s1[0]; + } + if (preg_match('/^(?:\p{L}\p{M}*)/u', $matches[count($matches) - 1], $s2)) { + $last = $s2[0]; + } + return yang_avatar_utf8_upper($first . $last); +} + +/** + * 复刻 core/utils/color.js 的 getBackgroundColors() + * + * 关键:**调用次数必须一致**。某个分支不 shuffle 时,也必须空 next() + * 一次,否则后面 backgroundRotation 取到的值就歪了。 + * + * @param Yang_Avatar_Prng $p + * @param array $list + * @param string $bgType + * @return array + */ +function yang_avatar_bg_colors($p, $list, $bgType) +{ + $shuffled = $p->shuffle($list); + if (count($shuffled) <= 1) { + $shuffled = $list; + $p->next(); + } elseif (count($list) === 2 && $bgType === 'gradientLinear') { + $shuffled = $list; + $p->next(); + } else { + $shuffled = $p->shuffle($list); + } + if (count($shuffled) === 0) { + $shuffled = array('transparent'); + } + $primary = $shuffled[0]; + $secondary = isset($shuffled[1]) ? $shuffled[1] : $shuffled[0]; + return array( + 'primary' => yang_avatar_color($primary), + 'secondary' => yang_avatar_color($secondary), + ); +} + +/** + * @param string $c + * @return string + */ +function yang_avatar_color($c) +{ + return ($c === 'transparent') ? $c : '#' . $c; +} + +/** + * 复刻 core/utils/svg.js 的 addBackground() + * + * @param string $body + * @param array $attrs + * @param string $primary + * @param string $secondary + * @param string $type + * @param int $rotation + * @return string + */ +function yang_avatar_add_bg($body, $attrs, $primary, $secondary, $type, $rotation) +{ + $vb = explode(' ', $attrs['viewBox']); + $x = $vb[0]; + $y = $vb[1]; + $w = $vb[2]; + $h = $vb[3]; + if ($type === 'gradientLinear') { + return '' + . '' + . '' + . '' + . '' + . '' + . '' + . $body; + } + return '' . $body; +} + +/** + * 把 body 模板里的 @@k:group@@ / @@c:key@@ 展开 + * + * 循环 8 轮是因为存在「聚合组件」:rings 的 ring.container 里会 + * 再调用 ringOne..ringFive,要展开两三层才干净。 + * + * @param array $style + * @param array $subs 组名 → 选中的变体名 + * @param array $colors + * @param array $meta pickComponent 的旋转/偏移 + * @return string + */ +function yang_avatar_expand($style, $subs, $colors, $meta) +{ + $svg = $style['body_template']; + for ($round = 0; $round < 8; $round++) { + if (strpos($svg, '@@k:') === false) { + break; + } + $changed = false; + foreach ($subs as $g => $variant) { + $token = '@@k:' . $g . '@@'; + if (strpos($svg, $token) === false) { + continue; + } + $rep = ''; + if ($variant !== null && isset($style['groups'][$g][$variant])) { + $rep = $style['groups'][$g][$variant]; + } + // pickComponent 的 value() 会包一层 transform(任一非零才包) + if ($rep !== '' && isset($meta[$g])) { + $m = $meta[$g]; + if ($m['rotation'] != 0 || $m['offsetX'] != 0 || $m['offsetY'] != 0) { + $rep = '' . $rep . ''; + } + } + $svg = str_replace($token, $rep, $svg); + $changed = true; + } + if (!$changed) { + break; + } + } + foreach ($colors as $k => $v) { + $svg = str_replace('@@c:' . $k . '@@', $v, $svg); + } + return $svg; +} + +/** + * JS 里 150/2 输出 75、100/2 输出 50 —— 都是整数,统一去掉 .0 + * + * @param float $v + * @return string + */ +function yang_avatar_num($v) +{ + if (abs($v - round($v)) < 1e-9) { + return (string) (int) round($v); + } + return (string) $v; +} + +/** + * XML 转义(顺序要紧:& 必须第一个换) + * + * @param string $s + * @return string + */ +function yang_avatar_esc($s) +{ + return str_replace( + array('&', "'", '"', '<', '>'), + array('&', ''', '"', '<', '>'), + $s + ); +} + +/** + * 版权元数据块(DiceBear 的 license.xml()) + * + * 这段必须保留:CC0 虽然不要求署名,但保留来源信息是良好实践, + * 也方便以后追溯。体积约 500 字节,压缩后几乎为零。 + * + * @param array $m + * @return string + */ +function yang_avatar_meta($m) +{ + $rights = yang_avatar_rights($m); + return '' + . '' + . '' . yang_avatar_esc($m['title']) . '' + . '' . yang_avatar_esc($m['creator']) . '' + . '' . yang_avatar_esc($m['source']) . '' + . '' . yang_avatar_esc($m['license_url']) . '' + . '' . yang_avatar_esc($rights) . '' + . ''; +} + +/** + * 拼接版权声明文字(与 DiceBear 的 license.xml() 逐字一致) + * + * @param array $m + * @return string + */ +function yang_avatar_rights($m) +{ + $title = (isset($m['title']) && $m['title'] !== '') ? '„' . $m['title'] . '”' : 'Design'; + $creator = '„' . ((isset($m['creator']) && $m['creator'] !== '') ? $m['creator'] : 'Unknown') . '”'; + if (!empty($m['source'])) { + $title .= ' (' . $m['source'] . ')'; + } + $r = ''; + if ($m['license_name'] !== 'MIT' && $m['creator'] !== 'DiceBear' && !empty($m['title'])) { + $r .= 'Remix of '; + } + $r .= $title . ' by ' . $creator; + if (!empty($m['license_name'])) { + $r .= ', licensed under „' . $m['license_name'] . '”'; + if (!empty($m['license_url'])) { + $r .= ' (' . $m['license_url'] . ')'; + } + } + return $r; +} + +/* ══════════════════════════════════════════════════════════════════ + * 后台:风格选择器控件 + * ══════════════════════════════════════════════════════════════════ */ + +/** + * 渲染「头像风格」这个自定义选项类型 + * + * 挂 optionsframework_avatar_style 过滤器 —— 选项框架在 + * class-options-interface.php 里对未知 type 会先查 + * has_filter('optionsframework_' . $type),命中就交给这里。 + * + * ── 为什么不用框架自带 images 类型 ──────────────────────────── + * images 类型要一组静态图片 URL,而这里的预览图是按邮箱实时生成的 + * data URI。用自定义类型才能同时做到「点一下换预览」和「16 个种子 + * 并排展示不同邮箱的效果」。 + * + * ── 为什么预览要 16 个种子 ──────────────────────────────────── + * 用户选风格时最容易踩的坑是「看一张觉得好看,装上发现都长一样」。 + * 单张预览看不出多样性,所以每个风格给 16 个不同邮箱的并排效果。 + * + * @param string $option_name 选项组名(这里是 'kratos') + * @param array $value 选项定义 + * @param mixed $val 当前值 + * @return string + */ +function yang_avatar_option_html($option_name, $value, $val) +{ + $styles = yang_avatar_styles(); + $current = is_string($val) && isset($styles[$val]) ? $val : 'lorelei'; + + // 预览用的 16 个邮箱:集中定义,AJAX 与首屏渲染共用同一份 + $seeds = yang_avatar_preview_seeds(); + + $name = $option_name . '[' . $value['id'] . ']'; + $id = $value['id']; + + $o = array(); + $o[] = '
'; + + foreach ($styles as $key => $meta) { + $checked = ($key === $current); + $rid = $id . '_' . $key; + + // 每个风格只生成 1 张 40px 预览(16 张全生成会让页面重 ~800KB)。 + // 单选后由 JS 把下方大预览换成该风格的 16 张。 + $thumb = yang_avatar_data_uri($seeds[0], $key, 80); + + $o[] = ''; + } + + $o[] = '
'; + + /* 大预览区:16 个邮箱的并排效果。 + 初始内容由 PHP 直接渲染,不依赖 JS —— 万一 JS 没跑起来, + 用户至少能看到当前选中风格的真实效果。 */ + $o[] = '
'; + $o[] = '

' + . esc_html__('效果预览(16 个不同邮箱)', 'kratos') . '

'; + $o[] = '
'; + foreach ($seeds as $seed) { + $uri = yang_avatar_data_uri($seed, $current, 96); + if ($uri === '') { + continue; + } + $o[] = '
' + . '' + . '
' . esc_html(preg_replace('/@.*/', '', $seed)) . '
' + . '
'; + } + $o[] = '
'; + $o[] = '

' + . esc_html__('同一个邮箱永远得到同一张脸;换邮箱就换一张。', 'kratos') + . '

'; + $o[] = '
'; + + return implode("\n", $o); +} +add_filter('optionsframework_avatar_style', 'yang_avatar_option_html', 10, 3); + +/** + * 保存校验:只接受收录清单里真实存在的风格名 + * + * 框架自带的 of_sanitize_enum 要读 $option['options'],而这里没设 + * 那个键(风格清单是运行时算的),所以单独写一个。 + * + * 非法值一律回落到默认风格,**不是**存空 —— 存空会导致下次 + * kratos_option() 拿到 ''、yang_avatar_style() 又要兜一次底, + * 多一层不必要的间接。 + * + * @param mixed $input + * @return string + */ +function yang_avatar_sanitize_style($input) +{ + $styles = yang_avatar_styles(); + if (is_string($input) && isset($styles[$input])) { + return $input; + } + return 'lorelei'; +} +add_filter('of_sanitize_avatar_style', 'yang_avatar_sanitize_style'); + +/** + * 后台:给 打上开关状态 class + * + * ── 为什么必须在服务端打 ────────────────────────────────────────── + * 风格列表的显隐靠 `body.yang-avatar-off` 控制。如果这个 class 只在 + * 底部 JS 里加,浏览器解析 HTML 时是「先显示全部内容」,等脚本跑到 + * 才藏起来 —— 每次进设置页、每次保存后重载,都会看到风格列表闪一下 + * 再消失。放在 PHP 里输出,首帧就是对的。 + * + * JS 里仍保留同一段 class 切换逻辑,负责「勾选/取消时立刻变化」。 + * + * @param string $classes + * @return string + */ +function yang_avatar_body_class($classes) +{ + if (yang_avatar_active()) { + return $classes; + } + return trim($classes . ' yang-avatar-off'); +} +add_filter('admin_body_class', 'yang_avatar_body_class'); + +/** + * 后台预览用的 16 个邮箱种子 + * + * 覆盖常见的中文博客访客形态:QQ / 163 / 126 / gmail / 企业邮, + * 以及几个极短、极长的边界值(`a@b.co`、`admin@example.org`)。 + * 短种子的哈希分布和长的不一样,混着放才能看出这套风格在极端 + * 输入下会不会崩。 + * + * @return array + */ +function yang_avatar_preview_seeds() +{ + return array( + 'yangyq@example.com', 'zhang.wei@qq.com', 'li.na@163.com', 'wang@126.com', + 'chen.jing@gmail.com', 'liu.yang@outlook.com', 'zhao.min@foxmail.com', 'sun.lei@sina.com', + 'admin@example.org', 'test@test.com', 'hello@world.net', 'a@b.co', + 'zhou.jie@139.com', 'xu@sohu.com', 'ma.chao@yeah.net', 'huang@vip.qq.com', + ); +} + +/** + * AJAX:换风格时重绘 16 格大预览 + * + * ── 为什么走 AJAX 而不是前端算 ──────────────────────────────────── + * 渲染器是 PHP 的(跟前台头像共用同一份代码)。前端要「即时换预览」 + * 只能要么重跑一遍 JS 版渲染器(等于维护两份实现,且极易不一致), + * 要么就请求后端。后者显然更稳。 + * + * 一次请求只生成一套风格的 16 张(约 20~40 ms、几十 KB), + * 只在用户点选时触发,不会拖慢设置页首屏。 + */ +function yang_avatar_ajax_preview() +{ + // 两道门:nonce + 权限。少一道都可能被低权限账号拿来刷 CPU + check_ajax_referer('yang_avatar_preview', 'nonce'); + if (!current_user_can('manage_options')) { + wp_send_json_error(array('message' => __('权限不足', 'kratos')), 403); + } + + $style = isset($_POST['style']) ? sanitize_text_field(wp_unslash($_POST['style'])) : ''; + $styles = yang_avatar_styles(); + // 白名单校验,别把用户传的值直接拼进文件路径 + if (!isset($styles[$style])) { + wp_send_json_error(array('message' => __('未知的风格', 'kratos')), 400); + } + + $cells = array(); + foreach (yang_avatar_preview_seeds() as $seed) { + $uri = yang_avatar_data_uri($seed, $style, 96); + if ($uri === '') { + continue; + } + $cells[] = array( + 'uri' => $uri, + 'name' => preg_replace('/@.*/', '', $seed), + ); + } + + if (empty($cells)) { + wp_send_json_error(array('message' => __('这套风格的模板文件缺失', 'kratos')), 500); + } + + wp_send_json_success(array('style' => $style, 'cells' => $cells)); +} +add_action('wp_ajax_yang_avatar_preview', 'yang_avatar_ajax_preview'); + +/** + * 后台样式与脚本 + * + * ── 为什么用 $_GET['page'] 判断而不是 $hook ────────────────────── + * 框架把真实的 hook 后缀存在 $this->options_screen 里(私有, + * 外面拿不到),对外的 add_menu_page 用的是 slug `kratos_options`。 + * 想靠 $hook 猜就得写 `strpos($hook, 'options')` 这种模糊匹配—— + * 一旦 WP 改了 hook 命名规则,匹配不上时 CSS/JS 会**静默不加载**, + * 表现就是「开关勾了没反应」,而且报错都没有。 + * + * 直接比 page 参数:菜单 slug 是我们自己注册的常量,永远对得上。 + * + * @param string $hook + */ +function yang_avatar_admin_assets($hook) +{ + if (!isset($_GET['page']) || $_GET['page'] !== 'kratos_options') { + return; + } + + $css = ' +.of-avatar-styles{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:10px;margin:0 0 16px;padding:0} +.of-avatar-style{display:flex;gap:11px;align-items:flex-start;padding:11px 12px;border:1px solid #dcdcde;border-radius:8px;background:#fff;cursor:pointer;transition:border-color .12s,box-shadow .12s} +.of-avatar-style:hover{border-color:#8c8f94} +.of-avatar-style.is-selected{border-color:#2271b1;box-shadow:0 0 0 1px #2271b1} +.of-avatar-style-radio{position:absolute;opacity:0;pointer-events:none} +.of-avatar-style-thumb{flex:0 0 56px;line-height:0} +.of-avatar-style-thumb img{border-radius:8px;display:block;background:#f6f7f7} +.of-avatar-style-text{display:flex;flex-direction:column;gap:3px;min-width:0} +.of-avatar-style-label{font-weight:600;font-size:13px;color:#1d2327} +.of-avatar-style-desc{font-size:12px;line-height:1.5;color:#646970} +.of-avatar-preview{margin-top:4px;padding:14px;border:1px solid #dcdcde;border-radius:8px;background:#f6f7f7} +.of-avatar-preview-title{margin:0 0 10px;font-size:13px;font-weight:600;color:#1d2327} +.of-avatar-preview-grid{display:flex;flex-wrap:wrap;gap:10px} +.of-avatar-preview-cell{margin:0;text-align:center;width:64px} +.of-avatar-preview-cell img{display:block;border-radius:8px;background:#fff} +.of-avatar-preview-cell figcaption{font-size:10px;color:#646970;margin-top:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.of-avatar-preview-note{margin:10px 0 0;font-size:12px;color:#646970} +/* 开关没打开时把风格那一行藏起来 —— 用 body class 而不是 JS 内联样式, + 这样保存后页面重载也不会闪一下 */ +body.yang-avatar-off #section-a_auto_style{display:none} +'; + wp_register_style('yang-avatar-admin', false, array(), null); + wp_enqueue_style('yang-avatar-admin'); + wp_add_inline_style('yang-avatar-admin', $css); + + $js = <<<'JS' +(function(){ + var box = document.getElementById('a_auto_enable'); + var section = document.getElementById('section-a_auto_style'); + var prev = document.querySelector('.of-avatar-preview'); + var grid = prev ? prev.querySelector('.of-avatar-preview-grid') : null; + + // ── 开关控制风格列表显隐 ──────────────────────────────────── + // 首帧的显隐由 PHP 打在 body class 上(不闪),这里只管"点了之后"。 + function syncToggle(){ + if (!box) return; + if (box.checked) { + document.body.classList.remove('yang-avatar-off'); + } else { + document.body.classList.add('yang-avatar-off'); + } + } + if (box) { box.addEventListener('change', syncToggle); syncToggle(); } + + // ── 点选风格:切换高亮 + 拉取该风格的 16 格预览 ────────────── + var CFG = window.YANG_AVATAR_CFG || {}; + var inflight = 0; // 请求序号,用来丢弃过期响应 + var cache = {}; // 已取过的风格,回点不再打后端 + var cur = prev ? prev.getAttribute('data-preview') : ''; + + // 首屏那份 PHP 渲染的格子直接进缓存。 + // 不这么做的话,用户点了一圈回到默认风格,会白白再请求一次 + // ——而屏幕上已经有一份一模一样的内容。 + function seedCache(){ + if (!prev || !grid || !cur || cache[cur]) return; + var cells = []; + Array.prototype.forEach.call( + grid.querySelectorAll('.of-avatar-preview-cell'), + function(fig){ + var im = fig.querySelector('img'); + var cp = fig.querySelector('figcaption'); + if (!im) return; + cells.push({uri: im.getAttribute('src'), name: cp ? cp.textContent : ''}); + } + ); + if (cells.length) { cache[cur] = cells; } + } + seedCache(); + + function paint(cells){ + if (!grid) return; + while (grid.firstChild) { grid.removeChild(grid.firstChild); } + cells.forEach(function(c){ + var fig = document.createElement('figure'); + fig.className = 'of-avatar-preview-cell'; + var img = document.createElement('img'); + img.src = c.uri; + img.alt = ''; + img.width = 64; + img.height = 64; + var cap = document.createElement('figcaption'); + cap.textContent = c.name; + fig.appendChild(img); + fig.appendChild(cap); + grid.appendChild(fig); + }); + } + + function load(style){ + if (!grid || !CFG.ajax || !style) return; + if (cache[style]) { paint(cache[style]); return; } + + // 连点时只认最后一次:先自增序号,回来时对不上就丢弃 + var seq = ++inflight; + if (prev) { prev.classList.add('is-loading'); } + + var body = new URLSearchParams(); + body.append('action', 'yang_avatar_preview'); + body.append('nonce', CFG.nonce); + body.append('style', style); + + fetch(CFG.ajax, { + method: 'POST', + credentials: 'same-origin', + headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'}, + body: body.toString() + }).then(function(r){ return r.json(); }).then(function(res){ + if (!res || !res.success || !res.data || !res.data.cells) return; + // 先入缓存:即使这次响应已过期(用户又点了别的), + // 下次点回来也能直接用,不必再打一次后端 + cache[style] = res.data.cells; + if (seq !== inflight) return; // 过期响应,不重绘 + if (prev) { prev.classList.remove('is-loading'); } + paint(res.data.cells); + }).catch(function(){ + if (seq !== inflight) return; + if (prev) { prev.classList.remove('is-loading'); } + // 失败就保持原样 —— 首屏已经有 PHP 渲染的一份,不必报错打扰用户 + }); + } + + function select(style){ + document.querySelectorAll('.of-avatar-style').forEach(function(x){ + x.classList.toggle('is-selected', x.getAttribute('data-style') === style); + }); + if (prev) { prev.setAttribute('data-preview', style); } + cur = style; + load(style); + } + + // ⚠️ 必须监听 radio 的 change,**不能**监听 label 的 click。 + // label 的默认行为是"把点击转发给关联的 input",而在 input 内部 + // 触发的那次 click 会继续冒泡回 label ⇒ label 的处理器被调用两次 + // ⇒ 同一风格发两次请求(实测 hits 2→4)。 + // change 只在选中状态真正变化时触发一次,天然去重; + // 点已选中的那张也不会触发(值没变),正好省掉一次无意义的重绘。 + document.querySelectorAll('.of-avatar-style-radio').forEach(function(r){ + r.addEventListener('change', function(){ + if (r.checked) { select(r.value); } + }); + }); + + // 键盘可达性:radio 被视觉隐藏,但焦点仍在它上面。 + // 方向键切换时浏览器自己会触发 change,上面已覆盖。 + window.yangAvatarSelect = select; +})(); +JS; + wp_register_script('yang-avatar-admin', '', array(), null, true); + wp_enqueue_script('yang-avatar-admin'); + wp_add_inline_script('yang-avatar-admin', $js); + + // 把 ajax 地址与 nonce 交给上面的脚本(不能内联进 heredoc,会破坏 <<<'JS') + wp_localize_script('yang-avatar-admin', 'YANG_AVATAR_CFG', array( + 'ajax' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('yang_avatar_preview'), + )); +} +add_action('admin_enqueue_scripts', 'yang_avatar_admin_assets'); \ No newline at end of file diff --git a/inc/theme-options.php b/inc/theme-options.php index 8ae932c..a302d95 100644 --- a/inc/theme-options.php +++ b/inc/theme-options.php @@ -229,12 +229,44 @@ function kratos_options() $options[] = array( 'name' => __('Gravatar 加速', 'kratos'), - 'desc' => __('把评论头像的请求转发到国内可访问的镜像,改善加载速度', 'kratos'), + 'desc' => __('把评论头像的请求转发到国内可访问的镜像,改善加载速度。若开启了下面的「自动头像」,未上传头像的访客不再需要这张图。', 'kratos'), 'std' => '0', 'id' => 'g_gravatar', 'type' => 'checkbox', ); + /* ── 自动头像 ──────────────────────────────────────────────── + * + * 总开关与风格选择拆成两个字段,但**风格列表只在开关打开后才显示**。 + * 这是用户明确要求的形式:先勾选,再出现列表。 + * + * 为什么不是一个字段直接存风格名(留空=关闭): + * 用户「关闭功能」和「还没选过风格」在库里会变成同一种状态(都空), + * 以后想恢复默认值时就分不清该填什么。两个字段各管一件事更清楚。 + * + * ⚠️ std 只在后台预填和「恢复默认」时起作用,运行时 kratos_option() + * 完全不看它。所以运行时默认值必须在这里同步给一份: + * 开关默认关('0'),风格默认 lorelei(见 yang_avatar_style())。 + */ + $options[] = array( + 'name' => __('自动头像', 'kratos'), + 'desc' => __('访客没注册、也没上传头像时,按他的邮箱生成一张头像。全程本地生成,不请求任何外部服务,同一个邮箱永远得到同一张。若访客已经上传过真实头像,不受影响。', 'kratos'), + 'std' => '0', + 'id' => 'a_auto_enable', + 'type' => 'checkbox', + ); + + $options[] = array( + 'name' => __('头像风格', 'kratos'), + 'desc' => __('勾选上面的「自动头像」后,这里才会出现风格列表。所有风格都是 CC0 或 MIT 许可,可自由使用与商用。点一下即时预览,保存后生效。', 'kratos'), + 'std' => 'lorelei', + 'id' => 'a_auto_style', + 'type' => 'avatar_style', + /* 只有开关打开时才渲染这个控件;关着的时候后台完全看不到它, + 避免用户在关闭状态下改了风格却不知道为什么没生效。 */ + 'requires' => 'a_auto_enable', + ); + $options[] = array( 'name' => __('禁止生成缩略图', 'kratos'), 'desc' => __('上传图片时不再额外生成多种尺寸的副本,节省磁盘空间', 'kratos'), diff --git a/style.css b/style.css index df25db4..e75ce94 100644 --- a/style.css +++ b/style.css @@ -1,7 +1,7 @@ /* Theme Name: Kratos_Yang Text Domain: kratos_Yang -Version: 3.2.35 +Version: 3.2.36 Requires PHP: 7.0 Description: 这是一个Kratos主题的分支版本,用来增加一些定制化功能。 Tags: 博客, 双栏, 响应式, 自定义背景, 自定义颜色, 自定义图标, 自定义菜单, 特色图像, 收录优化, 邮件中心, 主题选项, 小工具, 文章置顶, 无障碍友好