设计概述
本次分享将介绍如何使用CSS-Doodle库创建一个动态视觉效果的 "万花筒" 页面。这个项目通过CSS-Doodle特有的网格系统和动态样式生成能力,结合出色彩斑斓、不断变化的几何图案,营造出类似万花筒的视觉体验。
CSS-Doodle是一个基于Web Component组件的库,它允许开发者通过简洁的语法创建各种CSS图案和动画效果。本项目利用其网格系统、随机函数和动画能力,实现了一个具有深度感和动态变化的视觉作品。
效果概览
最终实现的效果全屏沉浸式的万花筒视觉效果:画面中心向外辐射的彩色心形元素持续进行动态色彩渐变,同时通过3D透视技术实现空间纵深的旋转动画;背景中随机分布的荧光粒子透明度和位移随时间推移产生平滑的相位变化,整体形成多层次的光影转换系统。
文件结构
项目包含三个核心文件:
万花筒.html——主页面文件
style.css——基础样式文件
css-doodle.min.js——CSS-Doodle库的压缩版本
文件结构如下(完整版代码见下文):
万花筒/ ├── css/ │ └── style.css ├── js/ │ └── css-doodle.min.js └── 万花筒.html重点解析
1.万花筒.html
主页面文件是效果实现的关键,主要包含四个部分:
1.1样式配置
css-doodle { --color: @p(#51eaea, #fffde1, #ff9d76, #FB3569); --rule: ( :doodle { @grid: 30x1 / 18vmin; --deg: @p(-180deg, 180deg); }要点:
--color变量:使用@p()函数从四种颜色中随机选择
--rule变量:包含CSS-Doodle的核心配置规则
1.2网格与容器设置
:doodle { @grid: 30x1 / 18vmin; --deg: @p(-180deg, 180deg); } :container { perspective: 30vmin; }要点:
@grid: 30x1创建30行1列的网格
perspective: 30vmin为容器添加3D透视效果,增强深度感
1.3元素与动画设置
:after, :before { content: ''; background: var(--color); @place-cell: @r(100%) @r(100%); @size: @r(6px); @shape: heart; } @place-cell: center; @size: 100%; @keyframes scale-up { 0%, 95.01%, 100% { transform: translateZ(0) rotate(0); opacity: 0; } 10% { opacity: 1; } 95% { transform: translateZ(35vmin) rotateZ(@var(--deg)); } }要点:
使用:before和:after伪元素创建心形图案
定义scale-up动画,实现元素从中心向外扩散的效果
1.4组件使用
<css-doodle use="var(--rule)"></css-doodle> <script src='js/css-doodle.min.js'></script>要点:
通过<css-doodle>标签使用定义好的规则
引入CSS-Doodle库实现功能
万花筒.html完整版代码:
<!DOCTYPE html> <html lang="en" > <head> <meta charset="UTF-8"> <title>万花筒</title> <link rel="stylesheet" href="css/style.css"> </head> <body> <style> css-doodle { --color: @p(#51eaea, #fffde1, #ff9d76, #FB3569); --rule: ( :doodle { @grid: 30x1 / 18vmin; --deg: @p(-180deg, 180deg); } :container { perspective: 30vmin; } :after, :before { content: ''; background: var(--color); @place-cell: @r(100%) @r(100%); @size: @r(6px); @shape: heart; } @place-cell: center; @size: 100%; box-shadow: @m(2, (0 0 50px var(--color))); background: @m(100, ( radial-gradient(var(--color) 50%, transparent 0) @r(-20%, 120%) @r(-20%, 100%) / 1px 1px no-repeat )); will-change: transform, opacity; animation: scale-up 12s linear infinite; animation-delay: calc(-12s / @size() * @i()); @keyframes scale-up { 0%, 95.01%, 100% { transform: translateZ(0) rotate(0); opacity: 0; } 10% { opacity: 1; } 95% { transform: translateZ(35vmin) rotateZ(@var(--deg)); } } ) } </style> <css-doodle use="var(--rule)"></css-doodle> <script src='js/css-doodle.min.js'></script> </body> </html>2.style.css
html, body { width: 100%; height: 100%; margin: 0; background: #270F34; overflow: hidden; display: flex; align-items: center; justify-content: center }3.css-doodle.min.js
该文件是CSS-Doodle库的核心,提供了以下关键能力:
自定义<css-doodle>标签,实现声明式图案生成
网格系统:通过@grid指令定义行列布局
随机函数:如@p()(随机选择)、@r()(随机范围)等
动画支持:集成关键帧动画系统
形状生成:内置多种基础形状(如本项目使用的 heart)
这个库的核心机制是将特殊的样式语法转换为标准CSS,并通过Web组件机制封装在自定义标签中,大大简化了复杂图案的创建过程
css-doodle.min.js完整版代码:
!function (e) { "function" == typeof define && define.amd ? define(e) : e() }(function () { "use strict"; function e(e) { let t = 0, n = 1, r = 1; return { curr: (n = 0) => e[t + n], end: () => e.length <= t, info: () => ({index: t, col: n, line: r}), index: e => void 0 === e ? t : t = e, next() { let s = e[t++]; return "\n" == s ? (r++, n = 0) : n++, s } } } function t(t) { t = t.trim(); let n = []; if (!/^var\(/.test(t)) return n; let r = e(t); try { n = function (e) { let t = "", n = [], r = [], s = {}; for (; !e.end();) { let i = e.curr(); if ("(" == i) n.push(i), t = ""; else if (")" == i || "," == i) { if (/^\-\-.+/.test(t) && (s.name ? (s.alternative || (s.alternative = []), s.alternative.push({name: t})) : s.name = t), ")" == i) { if ("(" != n[n.length - 1]) throw new Error("bad match"); n.pop() } "," == i && (n.length || (r.push(s), s = {})), t = "" } else /\s/.test(i) || (t += i); e.next() } return n.length ? [] : (s.name && r.push(s), r) }(r) } catch (e) { console.error(e && e.message || "Bad variables.") } return n } function n(e) { return Array.isArray(e) ? e : [e] } function r(e, t = "\n") { return (e || []).join(t) } function s(e) { return e[e.length - 1] } function i(e) { return e[0] } function l(e, t) { return Array.prototype.flatMap ? e.flatMap(t) : e.reduce((e, n) => e.concat(t(n)), []) } const u = { func: (e = "") => ({type: "func", name: e, arguments: []}), argument: () => ({type: "argument", value: []}), text: (e = "") => ({type: "text", value: e}), pseudo: (e = "") => ({type: "pseudo", selector: e, styles: []}), cond: (e = "") => ({type: "cond", name: e, styles: [], arguments: []}), rule: (e = "") => ({type: "rule", property: e, value: []}), keyframes: (e = "") => ({type: "keyframes", name: e, steps: []}), step: (e = "") => ({type: "step", name: e, styles: []}) }, o = { white_space: e => /[\s\n\t]/.test(e), line_break: e => /\n/.test(e), number: e => !isNaN(e), pair: e => ['"', "(", ")", "'"].includes(e), pair_of: (e, t) => ({'"': '"', "'": "'", "(": ")"})[e] == t }; function a(e, {col: t, line: n}) { console.error(`(at line ${n}, column ${t}) ${e}`) } function c(e) { return function (t, n) { let r = t.index(), s = ""; for (; !t.end();) { let n = t.next(); if (e(n)) break; s += n } return n && t.index(r), s } } function p(e, t) { return c(e => /[^\w@]/.test(e))(e, t) } function h(e) { return c(e => /[\s\{]/.test(e))(e) } function f(e, t) { return c(e => o.line_break(e) || "{" == e)(e, t) } function d(e, t) { let n, r = u.step(); for (; !e.end() && "}" != (n = e.curr());) if (o.white_space(n)) e.next(); else { if (r.name.length) { if (r.styles.push(j(e, t)), "}" == e.curr()) break } else r.name = k(e); e.next() } return r } function g(e, t) { const n = []; let r; for (; !e.end() && "}" != (r = e.curr());) o.white_space(r) ? e.next() : (n.push(d(e, t)), e.next()); return n } function m(e, t) { let n, r = u.keyframes(); for (; !e.end() && "}" != (n = e.curr());) if (r.name.length) { if ("{" == n) { e.next(), r.steps = g(e, t); break } e.next() } else if (p(e), r.name = h(e), !r.name.length) { a("missing keyframes name", e.info()); break } return r } function y(e, t = {}) { for (e.next(); !e.end();) { let n = e.curr(); if (t.inline) { if ("\n" == n) break } else if ("*" == (n = e.curr()) && "/" == e.curr(1)) break; e.next() } t.inline || (e.next(), e.next()) } function v(e) { let t, n = ""; for (; !e.end() && ":" != (t = e.curr());) o.white_space(t) || (n += t), e.next(); return n } function x(e) { let t, n = [], r = [], i = [], l = ""; for (; !e.end();) { if (t = e.curr(), /[\('"`]/.test(t) && "\\" !== e.curr(-1)) i.length && "(" != t && t === s(i) ? i.pop() : i.push(t), l += t; else if ("@" == t) r.length || (l = l.trimLeft()), l.length && (r.push(u.text(l)), l = ""), r.push(b(e)); else if (/[,)]/.test(t)) { if (i.length) ")" == t && i.pop(), l += t; else if (l.length && (r.length ? l.length && r.push(u.text(l)) : r.push(u.text((a = l).trim().length ? o.number(+a) ? +a : a.trim() : a))), n.push(_(r)), [r, l] = [[], ""], ")" == t) break } else l += t; e.next() } var a; return n } function _(e) { let t = e.map(e => { if ("text" == e.type && "string" == typeof e.value) { let t = String(e.value); t.includes("`") && (e.value = t = t.replace(/`/g, '"')), e.value = t.replace(/\n+|\s+/g, " ") } return e }), n = i(t) || {}, r = s(t) || {}; if ("text" == n.type && "text" == r.type) { let e = i(n.value), t = s(r.value); "string" == typeof n.value && "string" == typeof r.value && o.pair(e) && o.pair_of(e, t) && (n.value = n.value.slice(1), r.value = r.value.slice(0, r.value.length - 1)) } return t } function b(e) { let t, n = u.func(), r = ""; for (; !e.end() && ")" != (t = e.curr());) { if ("(" == t) { e.next(), n.name = r, n.arguments = x(e), n.position = e.info().index; break } r += t, e.next() } return n } function $(e) { let t, n = u.text(), r = 0, s = !0; const i = [], l = []; for (i[r] = []; !e.end();) if (t = e.curr(), s && o.white_space(t)) e.next(); else { if (s = !1, "\n" != t || o.white_space(e.curr(-1))) if ("," != t || l.length) { if (/[;}]/.test(t)) { n.value.length && (i[r].push(n), n = u.text()); break } "@" == t ? (n.value.length && (i[r].push(n), n = u.text()), i[r].push(b(e))) : o.white_space(t) && o.white_space(e.curr(-1)) || ("(" == t && l.push(t), ")" == t && l.pop(), n.value += t) } else n.value.length && (i[r].push(n), n = u.text()), i[++r] = [], s = !0; else n.value += " "; e.next() } return n.value.length && i[r].push(n), i } function k(e) { let t, n = ""; for (; !e.end() && "{" != (t = e.curr());) o.white_space(t) || (n += t), e.next(); return n } function w(e) { let t, n = {name: "", arguments: []}; for (; !e.end();) { if ("(" == (t = e.curr())) e.next(), n.arguments = x(e); else { if (/[){]/.test(t)) break; o.white_space(t) || (n.name += t) } e.next() } return n } function z(e, t) { let n, r = u.pseudo(); for (; !e.end() && "}" != (n = e.curr());) if (o.white_space(n)) e.next(); else { if (r.selector) { let n = j(e, t); if ("@use" == n.property ? r.styles = r.styles.concat(n.value) : r.styles.push(n), "}" == e.curr()) break } else r.selector = k(e); e.next() } return r } function j(e, t) { let n, r = u.rule(); for (; !e.end() && ";" != (n = e.curr());) { if (r.property.length) { r.value = $(e); break } if (r.property = v(e), "@use" == r.property) { r.value = S(e, t); break } e.next() } return r } function A(e, t) { let n, r = u.cond(); for (; !e.end() && "}" != (n = e.curr());) { if (r.name.length) if (":" == n) { let t = z(e); t.selector && r.styles.push(t) } else if ("@" != n || f(e, !0).includes(":")) { if (!o.white_space(n)) { let n = j(e, t); if (n.property && r.styles.push(n), "}" == e.curr()) break } } else r.styles.push(A(e)); else Object.assign(r, w(e)); e.next() } return r } function M(e, t) { let n = ""; return e && e.get_custom_property_value && (n = e.get_custom_property_value(t)), n } function S(e, n) { return e.next(), ($(e) || []).reduce((e, r) => { !function e(n, r) { n.forEach && n.forEach(n => { if ("text" == n.type && n.value) { let e = t(n.value); n.value = e.reduce((e, t) => { let n, s = "", i = ""; !(s = M(r, t.name)) && t.alternative && t.alternative.every(e => { if (i = M(r, e.name)) return s = i, !1 }); try { n = E(s, r) } catch (e) { } return n && e.push.apply(e, n), e }, []) } "func" == n.type && n.arguments && n.arguments.forEach(t => { e(t, r) }) }) }(r, n); let [s] = r; return s.value && s.value.length && e.push(...s.value), e }, []) } function E(t, n) { const r = e(t), s = []; for (; !r.end();) { let e = r.curr(); if (o.white_space(e)) r.next(); else { if ("/" == e && "*" == r.curr(1)) y(r); else if ("/" == e && "/" == r.curr(1)) y(r, {inline: !0}); else if (":" == e) { let e = z(r, n); e.selector && s.push(e) } else if ("@" == e && "@keyframes" === p(r, !0)) { let e = m(r, n); s.push(e) } else if ("@" != e || f(r, !0).includes(":")) { if (!o.white_space(e)) { let e = j(r, n); e.property && s.push(e) } } else { let e = A(r, n); e.name.length && s.push(e) } r.next() } } return s } function C(e, ...t) { return t.reduce((e, t) => e.apply(null, n(t)), e) } function O(e, t, n) { return Math.max(t, Math.min(n, e)) } function L(e, t, n) { let r = 0, s = e, i = e => e > 0 && e < 1 ? .1 : 1, l = arguments.length; 1 == l && ([e, t] = [i(e), e]), l < 3 && (n = i(e)); let u = []; for (; (n >= 0 && e <= t || n < 0 && e > t) && (u.push(e), e += n, !(r++ >= 1e3));) ; return u.length || u.push(s), u } function T(e) { return /^[a-zA-Z]$/.test(e) } function H(e) { let t = () => e; return t.lazy = !0, t } function N(e, t) { let n = []; for (let r = 0; r < e; ++r) n.push(t(r)); return n } const [W, R, I] = [1, 32, 1024]; function P(e) { let [t, n, r] = (e + "").replace(/\s+/g, "").replace(/[,,xX]+/g, "x").split("x").map(Number); const s = 1 == t || 1 == n ? I : R, i = {x: O(t || W, 1, s), y: O(n || t || W, 1, s), z: O(r || W, 1, s)}; return Object.assign({}, i, {count: i.x * i.y}) } function U(e, t) { if (t) { let n = new Blob([e], {type: "image/svg+xml"}); return `url(${URL.createObjectURL(n)}#${t})` } return `url("data:image/svg+xml;utf8,${encodeURIComponent(e)}")` } function q(e) { const t = 'xmlns="http://www.w3.org/2000/svg"'; return e.includes("<svg") || (e = `<svg ${t}>${e}</svg>`), e.includes("xmlns") || (e = e.replace(/<svg([\s>])/, `<svg ${t}$1`)), e } function B(e = 0, t = e) { return 1 == arguments.length && (e = e < 1 ? .1 : 1), function (e, t, n) { return e * (1 - n) + t * n }(e, t, Math.random()) } function D(e) { return (...t) => { let n = function (e) { let t = ""; return e.some(e => { let n = String(e).trim(); if (!n) return ""; let r = n.match(/\d(\D+)$/); return t = r ? r[1] : "" }), t }(t); return function (e, t) { return (...n) => { n = n.map(e => Number(String(e).replace(/\D+$/g, ""))); let r = e.apply(null, n); return t.length ? Array.isArray(r) ? r.map(e => e + t) : r + t : r } }(e, n).apply(null, t) } } function Z(e) { return (...t) => { let n = t.map(e => String(e).charCodeAt(0)), r = e.apply(null, n); return Array.isArray(r) ? r.map(e => String.fromCharCode(e)) : String.fromCharCode(r) } } function V(e) { const t = function (e) { let t = function (e) { let t = String(e), n = [], r = ""; for (let e = 0; e < t.length; ++e) { let i = t[e]; if (X[i]) if ("-" == i && "e" == t[e - 1]) r += i; else if (n.length || r.length || !/[+-]/.test(i)) { let {type: e, value: t} = s(n) || {}; "operator" == e && !r.length && /[^()]/.test(i) && /[^()]/.test(t) ? r += i : (r.length && (n.push({ type: "number", value: r }), r = ""), n.push({type: "operator", value: i})) } else r += i; else /\S/.test(i) && (r += i) } r.length && n.push({type: "number", value: r}); return n }(e); const n = [], r = []; for (let e = 0; e < t.length; ++e) { let {type: i, value: l} = t[e]; if ("number" == i) r.push(l); else if ("operator" == i) if ("(" == l) n.push(l); else if (")" == l) { for (; n.length && "(" != s(n);) r.push(n.pop()); n.pop() } else { for (; n.length && X[s(n)] >= X[l];) { let e = n.pop(); /[()]/.test(e) || r.push(e) } n.push(l) } } for (; n.length;) r.push(n.pop()); return r }(e), n = []; for (; t.length;) { let e = t.shift(); if (/\d+/.test(e)) n.push(e); else { let t = n.pop(), r = n.pop(); n.push(F(e, Number(r), Number(t))) } } return n[0] } const X = {"*": 3, "/": 3, "%": 3, "+": 2, "-": 2, "(": 1, ")": 1}; function F(e, t, n) { switch (e) { case"+": return t + n; case"-": return t - n; case"*": return t * n; case"/": return t / n; case"%": return t % n } } const G = {}; function J(e, t) { return (...n) => { let r = e + n.join("-"); return G[r] ? G[r] : G[r] = t.apply(null, n) } } function K(e) { return (...t) => e.apply(null, l(t, e => String(e).startsWith("[") ? Y(e) : e)) } function Q(e, t) { return {type: e, value: t} } const Y = J("build_range", e => { return l(function (e) { let t = String(e), n = [], r = []; if (!t.startsWith("[") || !t.endsWith("]")) return n; for (let e = 1; e < t.length - 1; ++e) { let i = t[e]; if ("-" != i || "-" != t[e - 1]) if ("-" != i) if ("-" != s(r)) r.length && n.push(Q("char", r.pop())), r.push(i); else { r.pop(); let e = r.pop(); n.push(e ? Q("range", [e, i]) : Q("char", i)) } else r.push(i) } return r.length && n.push(Q("char", r.pop())), n }(e), ({type: e, value: t}) => { if ("char" == e) return t; let [n, r] = t, s = !1; n > r && ([n, r] = [r, n], s = !0); let i = Z(L)(n, r); return s && i.reverse(), i }) }), {cos: ee, sin: te, sqrt: ne, pow: re, PI: se} = Math, ie = se / 180; function le(e, t) { "function" == typeof arguments[0] && (t = e, e = {}), t || (t = (e => [ee(e), te(e)])); let n = e.split || 120, r = e.scale || 1, s = ie * (e.start || 0), i = e.deg ? e.deg * ie : se / (n / 2), l = []; for (let e = 0; e < n; ++e) { let n = s + i * e, [u, o] = t(n); l.push(50 * u * r + 50 + "% " + (50 * o * r + 50) + "%") } return e.type ? `polygon(${e.type}, ${l.join(",")})` : `polygon(${l.join(",")})` } function ue(e, t, n) { let r = ie * n; return [e * ee(r) - t * te(r), t * ee(r) + e * te(r)] } const oe = { circle: () => "circle(49%)", triangle: () => le({split: 3, start: -90}, e => [1.1 * ee(e), 1.1 * te(e) + .2]), rhombus: () => le({split: 4}), pentagon: () => le({split: 5, start: 54}), hexgon: () => le({split: 6, start: 30}), hexagon: () => le({split: 6, start: 30}), heptagon: () => le({split: 7, start: -90}), octagon: () => le({split: 8, start: 22.5}), star: () => le({split: 5, start: 54, deg: 144}), diamond: () => "polygon(50% 5%, 80% 50%, 50% 95%, 20% 50%)", cross: () => "polygon(\n 5% 35%, 35% 35%, 35% 5%, 65% 5%,\n 65% 35%, 95% 35%, 95% 65%, 65% 65%,\n 65% 95%, 35% 95%, 35% 65%, 5% 65%\n )", clover: (e = 3) => (4 == (e = O(e, 3, 5)) && (e = 2), le({split: 240}, t => { let n = ee(e * t) * ee(t), r = ee(e * t) * te(t); return 3 == e && (n -= .2), 2 == e && (n /= 1.1, r /= 1.1), [n, r] })), hypocycloid(e = 3) { let t = 1 - (e = O(e, 3, 6)); return le({scale: 1 / e}, n => { let r = t * ee(n) + ee(t * (n - se)), s = t * te(n) + te(t * (n - se)); return 3 == e && (r = 1.1 * r - .6, s *= 1.1), [r, s] }) }, astroid: () => oe.hypocycloid(4), infinity: () => le(e => { let t = .7 * ne(2) * ee(e), n = re(te(e), 2) + 1; return [t / n, t * te(e) / n] }), heart: () => le(e => { return ue(1.2 * (.75 * re(te(e), 3)), 1.1 * (ee(1 * e) * (13 / 18) - ee(2 * e) * (5 / 18) - ee(3 * e) / 18 - ee(4 * e) / 18 + .2), 180) }), bean: () => le(e => { let [t, n] = [re(te(e), 3), re(ee(e), 3)]; return ue((t + n) * ee(e) * 1.3 - .45, (t + n) * te(e) * 1.3 - .45, -90) }), bicorn: () => le(e => ue(ee(e), re(te(e), 2) / (2 + te(e)) - .5, 180)), pear: () => le(e => [te(e), (1 + te(e)) * ee(e) / 1.4]), fish: () => le(e => [ee(e) - re(te(e), 2) / ne(2), te(2 * e) / 2]), whale: () => le({split: 240}, e => { let t = 3.4 * (re(te(e), 2) - .5) * ee(e); return ue(ee(e) * t + .75, te(e) * t * 1.2, 180) }), bud: (e = 3) => (e = O(e, 3, 10), le({split: 240}, t => [(1 + .2 * ee(e * t)) * ee(t) * .8, (1 + .2 * ee(e * t)) * te(t) * .8])), alien(...e) { let [t = 1, n = 1, r = 1, s = 1, i = 1] = e.map(e => O(e, 1, 9)); return le({ split: 480, type: "evenodd" }, e => [.31 * (ee(e * t) + ee(e * r) + ee(e * i)), .31 * (te(e * n) + te(e * s) + te(e))]) } }, ae = { index: ({count: e}) => t => e, row: ({x: e}) => t => e, col: ({y: e}) => t => e, size: ({grid: e}) => t => e.count, "size-row": ({grid: e}) => t => e.x, "size-col": ({grid: e}) => t => e.y, n: ({idx: e}) => t => e || 0, pick: ({context: e}) => K((...t) => e.last_pick = function (...e) { let t = e.reduce((e, t) => e.concat(t), []); return t[~~(Math.random() * t.length)] }(t)), "pick-n"({idx: e, context: t, position: n}) { let r = "pn-counter" + n; return K((...n) => { t[r] || (t[r] = 0), t[r] += 1; let s = n.length, i = ((null == e ? t[r] : e) - 1) % s; return t.last_pick = n[i] }) }, "pick-d"({idx: e, context: t, position: n}) { let r = "pd-counter" + n, s = "pd-values" + n; return K((...n) => { t[r] || (t[r] = 0), t[r] += 1, t[s] || (t[s] = function (e) { let t = Array.from ? Array.from(e) : e.slice(), n = e.length; for (; n;) { let e = ~~(Math.random() * n--), r = t[n]; t[n] = t[e], t[e] = r } return t }(n)); let i = n.length, l = ((null == e ? t[r] : e) - 1) % i; return t.last_pick = t[s][l] }) }, "last-pick": ({context: e}) => () => e.last_pick, multiple: H((e, t) => { if (!t || !e) return ""; return N(O(e(), 0, 65536), e => t(e + 1)).join(",") }), repeat: H((e, t) => { if (!t || !e) return ""; return N(O(e(), 0, 65536), e => t(e + 1)).join("") }), rand: ({context: e}) => (...t) => { let n = (t.every(T) ? Z : D)(B).apply(null, t); return e.last_rand = n }, "rand-int": ({context: e}) => (...t) => { let n = t.every(T) ? Z : D, r = parseInt(n(B).apply(null, t)); return e.last_rand = r }, "last-rand": ({context: e}) => () => e.last_rand, calc: () => e => V(e), hex: () => e => parseInt(e).toString(16), svg: H(e => { if (void 0 === e) return ""; return U(q(e().trim())) }), "svg-filter": H(e => { if (void 0 === e) return ""; let t = function (e = "") { return e + Math.random().toString(32).substr(2) }("filter-"); return U(q(e().trim()).replace(/<filter([\s>])/, `<filter id="${t}"$1`), t) }), var: () => e => `var(${e})`, shape: () => memo("shape-function", (e = "", ...t) => (e = e.trim(), "function" == typeof oe[e] ? oe[e](t) : "")) }; var ce, pe, he = (ce = ae, pe = { multi: "multiple", m: "multiple", pn: "pick-n", pd: "pick-d", r: "rand", ri: "rand-int", p: "pick", lp: "last-pick", lr: "last-rand", i: "index", "pick-by-turn": "pick-n", "max-row": "size-row", "max-col": "size-col" }, Object.keys(pe).forEach(e => { ce[e] = ce[pe[e]] }), ce); const fe = e => /[,,\s]/.test(e); function de(e) { for (; !e.end() && fe(e.curr(1));) e.next() } function ge(t) { const n = e(t), r = [], s = []; let i = ""; for (; !n.end();) { let e = n.curr(); "(" == e ? (i += e, s.push(e)) : ")" == e ? (i += e, s.length && s.pop()) : s.length ? i += e : fe(e) ? (r.push(i), i = "", de(n)) : i += e, n.next() } return i && r.push(i), r } let me = []; function ye(e) { if (!me.length) { let e = new Set; for (let t in document.head.style) t.startsWith("-") || e.add(t.replace(/[A-Z]/g, "-$&").toLowerCase()); e.has("grid-gap") || e.add("grid-gap"), me = Array.from(e) } return e && e.test ? me.filter(t => e.test(t)) : me } function ve(e) { let t = new RegExp(`\\-?${e}\\-?`); return ye(t).map(e => e.replace(t, "")).reduce((e, t) => (e[t] = t, e), {}) } const xe = ve("webkit"), _e = ve("moz"); function be(e, t) { return xe[e] ? `-webkit-${t}${t}` : _e[e] ? `-moz-${t}${t}` : t } var $e = { "@size"(e, {is_special_selector: t}) { let [n, r = n] = ge(e); return `\n width:${n};height:${r};${t ? "" : `\n --internal-cell-width:${n};--internal-cell-height:${r};`}` }, "@min-size"(e) { let [t, n = t] = ge(e); return `min-width:${t};min-height:${n};` }, "@max-size"(e) { let [t, n = t] = ge(e); return `max-width:${t};max-height:${n};` }, "@place-cell": (() => { let e = {center: "50%", 0: "0%", left: "0%", right: "100%", top: "50%", bottom: "50%"}, t = {center: "50%", 0: "0%", top: "0%", bottom: "100%", left: "50%", right: "50%"}; return n => { let [r, s = "50%"] = ge(n); const i = "var(--internal-cell-width, 25%)", l = "var(--internal-cell-height, 25%)"; return `\n position:absolute;left:${r = e[r] || r};top:${s = t[s] || s};width:${i};height:${l};margin-left:calc(${i}/ -2) !important;margin-top:calc(${l}/ -2) !important;grid-area:unset !important;` } })(), "@grid"(e, t) { let [n, r] = e.split("/").map(e => e.trim()); return {grid: P(n), size: r ? this["@size"](r, t) : ""} }, "@shape": J("shape-property", e => { let [t, ...n] = ge(e); return oe[t] ? be("clip-path", `clip-path:${oe[t].apply(null, n)};`) + "overflow:hidden;" : "" }), "@use"(e) { if (e.length > 2) return e } }; function ke(e, t, n) { let r = function (e) { return t => String(e).replace(/(\d+)(n)/g, "$1*" + t).replace(/n/g, t) }(e); for (let e = 0; e <= n; ++e) if (V(r(e)) == t) return !0 } const we = {even: e => !!(e % 2), odd: e => !(e % 2)}; function ze(e) { return /^(even|odd)$/.test(e) } var je = { at: ({x: e, y: t}) => (n, r) => e == n && t == r, nth: ({count: e, grid: t}) => (...n) => n.some(n => ze(n) ? we[n](e - 1) : ke(n, e, t.count)), row: ({x: e, grid: t}) => (...n) => n.some(n => ze(n) ? we[n](e - 1) : ke(n, e, t.x)), col: ({y: e, grid: t}) => (...n) => n.some(n => ze(n) ? we[n](e - 1) : ke(n, e, t.y)), even: ({count: e}) => t => we.even(e - 1), odd: ({count: e}) => t => we.odd(e - 1), random: () => (e = .5) => (e >= 1 && e <= 0 && (e = .5), Math.random() < e) }; var Ae = Object.getOwnPropertyNames(Math).reduce((e, t) => (e[t] = (() => (...e) => "number" == typeof Math[t] ? Math[t] : Math[t].apply(null, e.map(V))), e), {}); function Me(e) { return /^\:(host|doodle)/.test(e) } function Se(e) { return /^\:(container|parent)/.test(e) } function Ee(e) { return Me(e) || Se(e) } class Ce { constructor(e) { this.tokens = e, this.rules = {}, this.props = {}, this.keyframes = {}, this.grid = null, this.coords = [], this.reset() } reset() { this.styles = {host: "", container: "", cells: "", keyframes: ""}, this.coords = []; for (let e in this.rules) e.startsWith("[cell]") && delete this.rules[e] } add_rule(e, t) { let r = this.rules[e]; r || (r = this.rules[e] = []), r.push.apply(r, n(t)) } pick_func(e) { return he[e] || Ae[e] } compose_aname(...e) { return e.join("-") } compose_selector(e, t = "") { return `[cell]:nth-of-type(${e})${t}` } compose_argument(e, t, n) { let r = e.map(e => { if ("text" == e.type) return e.value; if ("func" == e.type) { let r = this.pick_func(e.name.substr(1)); if (r) { t.idx = n, t.position = e.position; let s = e.arguments.map(e => r.lazy ? n => this.compose_argument(e, t, n) : this.compose_argument(e, t, n)); return C(r, t, s) } } }); return r.length >= 2 ? r.join("") : r[0] } compose_value(e, t) { return e && e.reduce ? e.reduce((e, n) => { switch (n.type) { case"text": e += n.value; break; case"func": { let r = n.name.substr(1), s = this.pick_func(r); if (s) { t.position = n.position; let r = n.arguments.map(e => s.lazy ? n => this.compose_argument(e, t, n) : this.compose_argument(e, t)); e += C(s, t, r) } } } return e }, "") : "" } compose_rule(e, t, n) { let r = Object.assign({}, t), s = e.property, i = e.value.reduce((e, t) => { let n = this.compose_value(t, r); return n && e.push(n), e }, []), l = i.join(", "); if (/^animation(\-name)?$/.test(s) && (this.props.has_animation = !0, r.count > 1)) { let {count: e} = r; switch (s) { case"animation-name": l = i.map(t => this.compose_aname(t, e)).join(", "); break; case"animation": l = i.map(t => { let n = (t || "").split(/\s+/); return n[0] = this.compose_aname(n[0], e), n.join(" ") }).join(", ") } } "content" == s && (/["']|^(none|var|counter|counters|attr)\(/.test(l) || (l = `'${l}'`)), "transition" == s && (this.props.has_transition = !0); let u = `${s}:${l};`; if (u = be(s, u), "clip-path" == s && (u += ";overflow:hidden;"), "width" != s && "height" != s || Ee(n) || (u += `--internal-cell-${s}:${l};`), $e[s]) { let t = $e[s](l, {is_special_selector: Ee(n)}); switch (s) { case"@grid": Me(n) && (this.grid = t.grid, u = t.size || ""); break; case"@place-cell": Me(n) || (u = t); case"@use": e.value.length && this.compose(r, e.value), u = $e[s](e.value); default: u = t } } return u } compose(e, t) { this.coords.push(e), (t || this.tokens).forEach((t, n) => { if (t.skip) return !1; switch (t.type) { case"rule": this.add_rule(this.compose_selector(e.count), this.compose_rule(t, e)); break; case"pseudo": { t.selector.startsWith(":doodle") && (t.selector = t.selector.replace(/^\:+doodle/, ":host")); let n = Ee(t.selector); n && (t.skip = !0), t.selector.split(",").forEach(r => { let s = t.styles.map(t => this.compose_rule(t, e, r)), i = n ? r : this.compose_selector(e.count, r); this.add_rule(i, s) }); break } case"cond": { let n = je[t.name.substr(1)]; if (n) { let r = t.arguments.map(t => this.compose_argument(t, e)); C(n, e, r) && this.compose(e, t.styles) } break } case"keyframes": this.keyframes[t.name] || (this.keyframes[t.name] = (e => `\n ${r(t.steps.map(t => `\n ${t.name}{${r(t.styles.map(t => this.compose_rule(t, e)))}}`))}`)) } }) } output() { Object.keys(this.rules).forEach((e, t) => { if (Se(e)) this.styles.container += `\n .container{${r(this.rules[e])}}`; else { let t = Me(e) ? "host" : "cells"; this.styles[t] += `\n ${e}{${r(this.rules[e])}}` } }); let e = Object.keys(this.keyframes); return this.coords.forEach((t, n) => { e.forEach(e => { let r = this.compose_aname(e, t.count); this.styles.keyframes += `\n ${function (e, t) { return e ? "function" == typeof t ? t() : t : "" }(0 == n, `@keyframes ${e}{${this.keyframes[e](t)}}`)}@keyframes ${r}{${this.keyframes[e](t)}}` }) }), {props: this.props, styles: this.styles, grid: this.grid} } } function Oe(e, t) { let n = new Ce(e), r = {}; n.compose({x: 1, y: 1, count: 1, context: {}, grid: {x: 1, y: 1, count: 1}}); let {grid: s} = n.output(); s && (t = s), n.reset(); for (let e = 1, s = 0; e <= t.x; ++e) for (let i = 1; i <= t.y; ++i) n.compose({ x: e, y: i, count: ++s, grid: t, context: r }); return n.output() } customElements.define("css-doodle", class extends HTMLElement { constructor() { super(), this.doodle = this.attachShadow({mode: "open"}), this.extra = {get_custom_property_value: this.get_custom_property_value.bind(this)} } connectedCallback() { setTimeout(() => { let e, t = this.getAttribute("use") || ""; if (t && (t = `@use:${t};`), !this.innerHTML.trim() && !t) return !1; try { let n = E(t + this.innerHTML, this.extra); this.grid_size = P(this.getAttribute("grid")), (e = Oe(n, this.grid_size)).grid && (this.grid_size = e.grid), this.build_grid(e) } catch (e) { this.innerHTML = "", console.error(e && e.message || "Error in css-doodle.") } }) } get_custom_property_value(e) { return getComputedStyle(this).getPropertyValue(e).trim().replace(/^\(|\)$/g, "") } build_grid(e) { const {has_transition: t, has_animation: n} = e.props, { keyframes: r, host: s, container: i, cells: l } = e.styles; this.doodle.innerHTML = `\n<style>${this.style_basic()}</style><style class="style-keyframes">${r}</style><style class="style-container">${this.style_size()}${s}${i}</style><style class="style-cells">${t || n ? "" : l}</style><div class="container">${this.html_cells()}</div>`, (t || n) && setTimeout(() => { this.set_style(".style-cells", l) }, 50) } inherit_props(e) { return ye(/grid/).map(e => `${e}:inherit;`).join("") } style_basic() { return `\n:host{display:block;visibility:visible;width:1em;height:1em;}.container, [cell]:not(:empty){position:relative;width:100%;height:100%;display:grid;${this.inherit_props()}}[cell]:empty{position:relative;line-height:1;box-sizing:border-box;display:flex;justify-content:center;align-items:center;}` } style_size() { let {x: e, y: t} = this.grid_size; return `\n:host{grid-template-rows:repeat(${e}, 1fr);grid-template-columns:repeat(${t}, 1fr);}` } html_cells() { let e = "<div cell></div>", t = e.repeat(this.grid_size.count), n = this.grid_size.z; for (; n--;) e = e.replace(/<div\scell><\/div>/g, "<div cell>" + t + "</div>"); return e.replace(/^<div\scell>|<\/div>$/g, "") } set_style(e, t) { const n = this.shadowRoot.querySelector(e); n && (n.styleSheet ? n.styleSheet.cssText = t : n.innerHTML = t) } update(e) { let t = this.getAttribute("use") || ""; t && (t = `@use:${t};`), e || (e = this.innerHTML), this.innerHTML = e, this.grid_size || (this.grid_size = P(this.getAttribute("grid"))); const n = Oe(E(t + e, this.extra), this.grid_size); if (n.grid) { let {x: e, y: t, z: r} = n.grid, {x: s, y: i, z: l} = this.grid_size; if (s !== e || i !== t || l !== r) return Object.assign(this.grid_size, n.grid), this.build_grid(n); Object.assign(this.grid_size, n.grid) } else { let n = P(this.getAttribute("grid")), {x: r, y: s, z: i} = n, {x: l, y: u, z: o} = this.grid_size; if (l !== r || u !== s || o !== i) return Object.assign(this.grid_size, n), this.build_grid(Oe(E(t + e, this.extra), this.grid_size)) } this.set_style(".style-keyframes", n.styles.keyframes), this.set_style(".style-container", this.style_size() + n.styles.host + n.styles.container), this.set_style(".style-cells", n.styles.cells) } get grid() { return Object.assign({}, this.grid_size) } set grid(e) { this.setAttribute("grid", e), this.connectedCallback() } get use() { return this.getAttribute("use") } set use(e) { this.setAttribute("use", e), this.connectedCallback() } static get observedAttributes() { return ["grid", "use"] } attributeChangedCallback(e, t, n) { if (t == n) return !1; "grid" == e && t && (this.grid = n), "use" == e && t && (this.use = n) } }) });补充
可以尝试修改以下参数来获得不同效果:
扩展颜色palette,增加更多色彩变化
尝试不同的@shape值,如star、circle等,创造不同的视觉元素
调整动画参数,改变效果的节奏和强度