741 lines
15 KiB
JavaScript
741 lines
15 KiB
JavaScript
(function (global, factory) {
|
||
typeof exports === 'object' && typeof module !== 'undefined' ? factory() :
|
||
typeof define === 'function' && define.amd ? define(factory) :
|
||
(factory());
|
||
}(this, (function () { 'use strict';
|
||
|
||
const is = {
|
||
even: (n) => !!(n % 2),
|
||
odd: (n) => !(n % 2)
|
||
};
|
||
|
||
function nth(x, y, count) {
|
||
return n => n == count;
|
||
}
|
||
|
||
function at(x, y) {
|
||
return (x1, y1) => (x == x1 && y == y1);
|
||
}
|
||
|
||
function row(x) {
|
||
return n => /^(even|odd)$/.test(n) ? is[n](x) : (n == x)
|
||
}
|
||
|
||
function col(x, y) {
|
||
return n => /^(even|odd)$/.test(n) ? is[n](y) : (n == y);
|
||
}
|
||
|
||
function even(x, y, count) {
|
||
return _ => is.even(count);
|
||
}
|
||
|
||
function odd(x, y, count) {
|
||
return _ => is.odd(count);
|
||
}
|
||
|
||
|
||
var cond = Object.freeze({
|
||
nth: nth,
|
||
at: at,
|
||
row: row,
|
||
col: col,
|
||
even: even,
|
||
odd: odd
|
||
});
|
||
|
||
function index(x, y, count) {
|
||
return _ => count;
|
||
}
|
||
|
||
function row$1(x) {
|
||
return _ => x;
|
||
}
|
||
|
||
function col$1(x, y) {
|
||
return _ => y;
|
||
}
|
||
|
||
function any() {
|
||
return function(...args) {
|
||
return random.apply(null, args);
|
||
}
|
||
}
|
||
|
||
function rand() {
|
||
return function(...args) {
|
||
return random(memo(unitify(range)).apply(null, args));
|
||
}
|
||
}
|
||
|
||
function random(...items) {
|
||
let args = items.reduce((ret, n) => ret.concat(n), []);
|
||
return args[Math.floor(Math.random() * args.length)];
|
||
}
|
||
|
||
function memo(fn) {
|
||
return function(...args) {
|
||
const memo = {};
|
||
let key = args.join('-');
|
||
if (memo[key]) return memo[key];
|
||
return (memo[key] = fn.apply(null, args));
|
||
}
|
||
}
|
||
|
||
function range(start, stop, step) {
|
||
let count = 0;
|
||
let initial = n => (n > 0 && n < 1) ? .1 : 1;
|
||
let length = arguments.length;
|
||
if (length == 1) [start, stop] = [initial(start), start];
|
||
if (length < 3) step = initial(stop);
|
||
let range = [];
|
||
while ((step > 0 && start < stop)
|
||
|| (step < 0 && start > stop)) {
|
||
range.push(start);
|
||
start += step;
|
||
if (count++ >= 1000) break;
|
||
}
|
||
return range;
|
||
}
|
||
|
||
function unitify(fn) {
|
||
return function(...args) {
|
||
let unit = get_unit(args[0]);
|
||
if (unit) {
|
||
args = args.map(remove_unit);
|
||
return add_unit(fn, unit).apply(null, args);
|
||
}
|
||
return fn.apply(null, args);
|
||
}
|
||
}
|
||
|
||
function add_unit(fn, unit) {
|
||
return function(...args) {
|
||
args = args.map(remove_unit);
|
||
let result = fn.apply(null, args);
|
||
if (unit) {
|
||
result = result.map(n => n + unit);
|
||
}
|
||
return result;
|
||
}
|
||
}
|
||
|
||
function get_unit(str) {
|
||
if (!str) return '';
|
||
let unit = /(%|cm|em|ex|in|mm|pc|pt|px|vh|vw|vmin|deg|ms|s)$/;
|
||
let matched = ''.trim.call(str).match(unit);
|
||
return matched ? matched[0] : '';
|
||
}
|
||
|
||
function remove_unit(str) {
|
||
let unit = get_unit(str);
|
||
return unit ? +(str.replace(unit, '')) : str;
|
||
}
|
||
|
||
|
||
var func = Object.freeze({
|
||
index: index,
|
||
row: row$1,
|
||
col: col$1,
|
||
any: any,
|
||
rand: rand
|
||
});
|
||
|
||
function values(obj) {
|
||
return Array.isArray(obj)
|
||
? obj : Object.keys(obj).map(k => obj[k]);
|
||
}
|
||
|
||
function with_args(fn, ...args) {
|
||
return args.reduce((f, arg) => f.apply(null, values(arg)), fn);
|
||
}
|
||
|
||
function join_line(arr) {
|
||
return (arr || []).join('\n');
|
||
}
|
||
|
||
function rule_set() {
|
||
const set = {};
|
||
const props = {};
|
||
const styles = {
|
||
host: '', cells: ''
|
||
};
|
||
return {
|
||
prop(name, value) {
|
||
props[name] = value;
|
||
},
|
||
add(selector, rule) {
|
||
let rules = set[selector];
|
||
if (!rules) rules = set[selector] = [];
|
||
rules.push.apply(
|
||
rules,
|
||
Array.isArray(rule) ? rule : [rule]
|
||
);
|
||
},
|
||
output() {
|
||
Object.keys(set).forEach(selector => {
|
||
let is_host = selector.startsWith(':host');
|
||
styles[is_host ? 'host': 'cells'] += `
|
||
${ selector } {
|
||
${ join_line(set[selector]) }
|
||
}
|
||
`;
|
||
});
|
||
return { props, styles }
|
||
}
|
||
}
|
||
}
|
||
|
||
function compose_selector(count, psudo = '') {
|
||
return `.cell:nth-of-type(${ count }) .shape${ psudo }`;
|
||
}
|
||
|
||
function compose_value(value, coords) {
|
||
return value.map(val => {
|
||
switch (val.type) {
|
||
case 'text': {
|
||
return val.value;
|
||
}
|
||
case 'function': {
|
||
let fn = func[val.name.substr(1)];
|
||
if (fn) {
|
||
return with_args(fn, coords, val.arguments);
|
||
}
|
||
}
|
||
default: return '';
|
||
}
|
||
}).join('');
|
||
}
|
||
|
||
function compose_rule(token, set, coords) {
|
||
let property = token.property;
|
||
let value = compose_value(token.value, coords);
|
||
if (property == 'transition') {
|
||
set.prop('has_transition', true);
|
||
}
|
||
return `${ property }: ${ value };`;
|
||
}
|
||
|
||
function compose_tokens(tokens, set, coords) {
|
||
tokens.forEach((token, i) => {
|
||
if (token.skip) return false;
|
||
|
||
switch (token.type) {
|
||
case 'rule':
|
||
set.add(
|
||
compose_selector(coords.count),
|
||
compose_rule(token, set, coords)
|
||
);
|
||
break;
|
||
|
||
case 'psudo': {
|
||
if (token.selector.startsWith(':doodle')) {
|
||
token.selector =
|
||
token.selector.replace(/^\:+doodle/, ':host');
|
||
}
|
||
|
||
let is_host_selector =
|
||
token.selector.startsWith(':host');
|
||
|
||
if (is_host_selector) {
|
||
token.skip = true;
|
||
}
|
||
|
||
let psudo_rules = token.styles.map(s =>
|
||
compose_rule(s, set, coords)
|
||
);
|
||
|
||
let selector = is_host_selector
|
||
? token.selector
|
||
: compose_selector(coords.count, token.selector);
|
||
|
||
set.add(selector, psudo_rules);
|
||
break;
|
||
}
|
||
|
||
case 'cond':
|
||
let fn = cond[token.name.substr(1)];
|
||
if (fn) {
|
||
let result = with_args(fn, coords, token.arguments);
|
||
if (result) {
|
||
compose_tokens(token.styles, set, coords);
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
});
|
||
}
|
||
|
||
function generator(tokens, size) {
|
||
let set = rule_set();
|
||
let count = 0;
|
||
for (let x = 1; x <= size.x; ++x) {
|
||
for (let y = 1; y <= size.y; ++y) {
|
||
count++;
|
||
compose_tokens(tokens, set, { x, y, count});
|
||
}
|
||
}
|
||
return set.output();
|
||
}
|
||
|
||
const struct = {
|
||
func: () => ({ type: 'function', name: '', arguments: [] }),
|
||
text: () => ({ type: 'text', value: '' }),
|
||
psudo: () => ({ type: 'psudo', selector: '', styles: [] }),
|
||
cond: () => ({ type: 'cond', name: '', styles: [], arguments: [] }),
|
||
rule: () => ({ type: 'rule', property: '', value: [] }),
|
||
comment: () => ({ type: 'comment', value: ''})
|
||
};
|
||
|
||
const bracket_pair = {
|
||
'(': ')',
|
||
'[': ']',
|
||
'{': '}'
|
||
};
|
||
|
||
const is$1 = {
|
||
white_space(c) {
|
||
return /[\s\n\t]/.test(c);
|
||
},
|
||
open_bracket(c) {
|
||
return bracket_pair.hasOwnProperty(c);
|
||
},
|
||
close_bracket_of(c) {
|
||
let pair = bracket_pair[c];
|
||
return p => p == pair;
|
||
},
|
||
number(n) {
|
||
return !isNaN(n);
|
||
}
|
||
};
|
||
|
||
function iterator(input) {
|
||
let index = 0, col = 1, line = 1;
|
||
return {
|
||
curr: (n = 0) => input[index + n],
|
||
end: () => input.length <= index,
|
||
info: () => ({ index, col, line }),
|
||
next: () => {
|
||
let next = input[index++];
|
||
if (next == '\n') line++, col = 0;
|
||
else col++;
|
||
return next;
|
||
}
|
||
};
|
||
}
|
||
|
||
function throw_error(msg, { col, line }) {
|
||
throw new Error(
|
||
`(at line ${ line }, column ${ col }) ${ msg }`
|
||
);
|
||
}
|
||
|
||
function skip_block(it) {
|
||
let [skipped, c] = [it.curr(), it.curr()];
|
||
let is_close_bracket = is$1.close_bracket_of(c);
|
||
it.next();
|
||
while (!it.end()) {
|
||
if (is_close_bracket(c = it.curr())) {
|
||
skipped += c;
|
||
break;
|
||
}
|
||
else if (is$1.open_bracket(c)) {
|
||
skipped += skip_block(it);
|
||
} else {
|
||
skipped += c;
|
||
}
|
||
it.next();
|
||
}
|
||
return skipped;
|
||
}
|
||
|
||
function read_comments(it) {
|
||
let comment = struct.comment(), c;
|
||
it.next(); it.next();
|
||
while (!it.end()) {
|
||
if ((c = it.curr()) == '*' && it.curr(1) == '/') break;
|
||
else comment.value += c;
|
||
it.next();
|
||
}
|
||
it.next(); it.next();
|
||
return comment;
|
||
}
|
||
|
||
function read_property(it) {
|
||
let prop = '', c;
|
||
while (!it.end()) {
|
||
if ((c = it.curr()) == ':') break;
|
||
else if (!/[a-zA-Z\-]/.test(c)) {
|
||
throw_error('Syntax error: Bad property name.', it.info());
|
||
}
|
||
else if (!is$1.white_space(c)) prop += c;
|
||
it.next();
|
||
}
|
||
return prop;
|
||
}
|
||
|
||
function read_quote_block(it, quote) {
|
||
let block = '', c;
|
||
it.next();
|
||
while (!it.end()) {
|
||
if ((c = it.curr()) == quote) {
|
||
if (it.curr(-1) !== '\\') break;
|
||
else block += c;
|
||
}
|
||
else block += c;
|
||
it.next();
|
||
}
|
||
return block;
|
||
}
|
||
|
||
function read_arguments(it) {
|
||
let args = [], arg = '', c;
|
||
while (!it.end()) {
|
||
if (is$1.open_bracket(c = it.curr())) {
|
||
arg += skip_block(it);
|
||
}
|
||
else if (/['"]/.test(c)) {
|
||
arg += read_quote_block(it, c);
|
||
}
|
||
else if (/[,)]/.test(c)) {
|
||
if (arg.length) {
|
||
args.push(is$1.number(+arg) ? +arg : arg);
|
||
arg = '';
|
||
}
|
||
if (c == ')') break;
|
||
}
|
||
else if (!is$1.white_space(c)) {
|
||
arg += c;
|
||
}
|
||
it.next();
|
||
}
|
||
return args;
|
||
}
|
||
|
||
function read_func(it) {
|
||
let func = struct.func(), name = '', c;
|
||
while (!it.end()) {
|
||
if ((c = it.curr()) == ')') break;
|
||
if (c == '(') {
|
||
it.next();
|
||
func.name = name;
|
||
func.arguments = read_arguments(it);
|
||
break;
|
||
}
|
||
else name += c;
|
||
it.next();
|
||
}
|
||
return func;
|
||
}
|
||
|
||
function read_value(it) {
|
||
let text = struct.text(), c;
|
||
const value = [];
|
||
while (!it.end()) {
|
||
if ((c = it.curr()) == '\n') {
|
||
it.next();
|
||
continue;
|
||
}
|
||
else if (/[;}]/.test(c)) {
|
||
if (text.value.length) value.push(text);
|
||
text = struct.text();
|
||
break;
|
||
}
|
||
else if (c == '@') {
|
||
if (text.value.length) value.push(text);
|
||
text = struct.text();
|
||
value.push(read_func(it));
|
||
}
|
||
else if (!is$1.white_space(c) || !is$1.white_space(it.curr(-1))) {
|
||
if (c == ':') {
|
||
throw_error('Syntax error: Bad property name.', it.info());
|
||
}
|
||
text.value += c;
|
||
}
|
||
it.next();
|
||
}
|
||
if (text.value.length) value.push(text);
|
||
return value;
|
||
}
|
||
|
||
function read_selector(it) {
|
||
let selector = '', c;
|
||
while (!it.end()) {
|
||
if ((c = it.curr())== '{') break;
|
||
else if (!is$1.white_space(c)) selector += c;
|
||
it.next();
|
||
}
|
||
return selector;
|
||
}
|
||
|
||
function read_cond_selector(it) {
|
||
let selector = { name: '', arguments: [] }, c;
|
||
while (!it.end()) {
|
||
if ((c = it.curr()) == '(') {
|
||
it.next();
|
||
selector.arguments = read_arguments(it);
|
||
}
|
||
else if (/[){]/.test(c)) break;
|
||
else if (!is$1.white_space(c)) selector.name += c;
|
||
it.next();
|
||
}
|
||
return selector;
|
||
}
|
||
|
||
function read_psudo(it) {
|
||
let psudo = struct.psudo(), c;
|
||
while (!it.end()) {
|
||
if ((c = it.curr())== '}') break;
|
||
if (is$1.white_space(c)) {
|
||
it.next();
|
||
continue;
|
||
}
|
||
else if (!psudo.selector) psudo.selector = read_selector(it);
|
||
else psudo.styles.push(read_rule(it));
|
||
it.next();
|
||
}
|
||
return psudo;
|
||
}
|
||
|
||
function read_rule(it) {
|
||
let rule = struct.rule(), c;
|
||
while (!it.end()) {
|
||
if ((c = it.curr()) == ';') break;
|
||
else if (!rule.property.length) {
|
||
rule.property = read_property(it);
|
||
}
|
||
else {
|
||
rule.value = read_value(it);
|
||
break;
|
||
}
|
||
it.next();
|
||
}
|
||
return rule;
|
||
}
|
||
|
||
function read_cond(it) {
|
||
let cond = struct.cond(), c;
|
||
while (!it.end()) {
|
||
if ((c = it.curr()) == '}') break;
|
||
else if (!cond.name.length) {
|
||
Object.assign(cond, read_cond_selector(it));
|
||
}
|
||
else if (c == ':') {
|
||
let psudo = read_psudo(it);
|
||
if (psudo.selector) cond.styles.push(psudo);
|
||
}
|
||
else if (c == '@') {
|
||
cond.styles.push(read_cond(it));
|
||
}
|
||
else if (!is$1.white_space(c)) {
|
||
let rule = read_rule(it);
|
||
if (rule.property) cond.styles.push(rule);
|
||
}
|
||
it.next();
|
||
}
|
||
return cond;
|
||
}
|
||
|
||
function tokenizer(input) {
|
||
const it = iterator(input);
|
||
const tokens = [];
|
||
while (!it.end()) {
|
||
let c = it.curr();
|
||
if (is$1.white_space(c)) {
|
||
it.next();
|
||
continue;
|
||
}
|
||
else if (c == '/' && it.curr(1) == '*') {
|
||
tokens.push(read_comments(it));
|
||
}
|
||
else if (c == ':') {
|
||
let psudo = read_psudo(it);
|
||
if (psudo.selector) tokens.push(psudo);
|
||
}
|
||
else if (c == '@') {
|
||
let cond = read_cond(it);
|
||
if (cond.name.length) tokens.push(cond);
|
||
}
|
||
else if (!is$1.white_space(c)) {
|
||
let rule = read_rule(it);
|
||
if (rule.property) tokens.push(rule);
|
||
}
|
||
it.next();
|
||
}
|
||
return tokens;
|
||
}
|
||
|
||
function compile(input, size) {
|
||
return generator(tokenizer(input), size);
|
||
}
|
||
|
||
function parse(size) {
|
||
const split = (size + '')
|
||
.replace(/\s+/g, '')
|
||
.replace(/[,,xX]+/, 'x')
|
||
.split('x');
|
||
const ret = {
|
||
x: clamp(parseInt(split[0], 10), 1, 16),
|
||
y: clamp(parseInt(split[1] || split[0]), 1, 16)
|
||
};
|
||
return {
|
||
x: ret.x,
|
||
y: ret.y,
|
||
count: ret.x * ret.y
|
||
};
|
||
}
|
||
|
||
function clamp(num, min, max) {
|
||
return (num <= min) ? min : ((num >= max) ? max : num);
|
||
}
|
||
|
||
class Doodle extends HTMLElement {
|
||
constructor() {
|
||
super();
|
||
this.doodle = this.attachShadow({ mode: 'open' });
|
||
this.basic_styles = `
|
||
*, *:after, *:before {
|
||
box-sizing: border-box;
|
||
}
|
||
:host {
|
||
display: inline-block;
|
||
width: 1em;
|
||
height: 1em;
|
||
will-change: transform;
|
||
}
|
||
.container {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
.container:after {
|
||
content: '';
|
||
display: block;
|
||
clear: both;
|
||
visibility: hidden;
|
||
}
|
||
.cell {
|
||
position: relative;
|
||
float: left;
|
||
line-height: 0;
|
||
}
|
||
.shape {
|
||
line-height: 0;
|
||
position: absolute;
|
||
width: 100%;
|
||
height: 100%;
|
||
transform-origin: center center;
|
||
z-index: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
`;
|
||
}
|
||
|
||
connectedCallback() {
|
||
let compiled;
|
||
|
||
this.size = parse(
|
||
this.getAttribute('grid') || '1x1'
|
||
);
|
||
|
||
if (!this.innerHTML.trim()) return false;
|
||
|
||
// clear content before throwing error
|
||
try {
|
||
compiled = compile(this.innerHTML, this.size);
|
||
} catch (e) {
|
||
this.innerHTML = '';
|
||
throw new Error(e);
|
||
}
|
||
|
||
const { has_transition } = compiled.props;
|
||
this.doodle.innerHTML = `
|
||
<style>${ this.basic_styles }</style>
|
||
<style class="style-container">
|
||
${ this.style_size() }
|
||
${ compiled.styles.host }
|
||
</style>
|
||
<style class="style-cells">
|
||
${ has_transition ? '' : compiled.styles.cells }
|
||
</style>
|
||
<div class="container">
|
||
${ this.html_cells() }
|
||
</div>
|
||
`;
|
||
|
||
has_transition && setTimeout(() => {
|
||
this.set_style(
|
||
'.style-cells',
|
||
compiled.styles.cells
|
||
);
|
||
}, 50);
|
||
}
|
||
|
||
style_size() {
|
||
return `
|
||
.cell {
|
||
width: ${ 100 / this.size.x + '%' };
|
||
height: ${ 100 / this.size.y + '%' };
|
||
}
|
||
`;
|
||
}
|
||
|
||
html_cells() {
|
||
const cell = `
|
||
<div class="cell">
|
||
<div class="shape"></div>
|
||
</div>
|
||
`;
|
||
return cell.repeat(this.size.count);
|
||
}
|
||
|
||
set_style(selector, styles) {
|
||
const el = this.shadowRoot.querySelector(selector);
|
||
el && (el.styleSheet
|
||
? (el.styleSheet.cssText = styles )
|
||
: (el.innerHTML = styles));
|
||
}
|
||
|
||
update(styles) {
|
||
if (!styles) return false;
|
||
const compiled = compile(styles, this.size);
|
||
this.set_style('.style-container',
|
||
this.style_size() + compiled.styles.host
|
||
);
|
||
this.set_style('.style-cells',
|
||
compiled.styles.cells
|
||
);
|
||
this.innerHTML = styles;
|
||
}
|
||
|
||
refresh() {
|
||
this.update(this.innerHTML);
|
||
}
|
||
|
||
get grid() {
|
||
return Object.assign({}, this.size);
|
||
}
|
||
|
||
set grid(grid) {
|
||
this.setAttribute('grid', grid);
|
||
this.connectedCallback();
|
||
}
|
||
|
||
static get observedAttributes() {
|
||
return ['grid'];
|
||
}
|
||
|
||
attributeChangedCallback(name, old_val, new_val) {
|
||
if (name == 'grid' && old_val) {
|
||
if (old_val !== new_val) {
|
||
this.grid = new_val;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
customElements.define('css-doodle', Doodle);
|
||
|
||
})));
|