UNPKG

css-doodle

Version:

A web component for drawing patterns with CSS

902 lines (803 loc) 23.1 kB
import parse_value_group from './parser/parse-value-group.js'; import parse_svg from './parser/parse-svg.js'; import parse_svg_path from './parser/parse-svg-path.js'; import parse_compound_value from './parser/parse-compound-value.js'; import generate_svg from './generator/svg.js'; import generate_shape from './generator/shapes.js'; import Noise from './lib/noise.js'; import calc from './calc.js'; import { memo } from './cache.js'; import { utime, UTime, umousex, umousey, uwidth, uheight } from './uniforms.js'; import { create_svg_url, normalize_svg } from './utils/svg.js'; import { by_unit, by_charcode } from './utils/transform.js'; import expand from './utils/expand.js'; import Stack from './utils/stack.js'; import get_named_arguments from './utils/get-named-arguments.js'; import { cell_id, is_letter, is_nil, is_empty, add_alias, unique_id, lerp, lazy, clamp, sequence, get_value, last } from './utils/index.js'; function make_sequence(c) { return lazy((_, n, ...actions) => { if (!actions || !n) return ''; let count = get_value(n()); let evaluated = count; if (/\D/.test(count) && !/\d+[x-]\d+/.test(count)) { evaluated = calc(count); if (evaluated === 0) { evaluated = count; } } let signature = Math.random(); return sequence( evaluated, (...args) => { return actions.map(action => { return get_value(action(...args, signature)) }).join(','); } ).join(c); }); } function push_stack(context, name, value) { if (!context[name]) context[name] = new Stack(1024); context[name].push(value); return value; } function flip_value(num) { return -1 * num; } function map2d(value, min, max, amp = 1) { let dimension = 2; let v = Math.sqrt(dimension / 4) * amp; let [ma, mb] = [-v, v]; return lerp((value - ma) / (mb - ma), min * amp, max * amp); } function compute(op, a, b) { switch (op) { case '+': return a + b; case '-': return a - b; case '*': return a * b; case '/': return a / b; case '%': return a % b; default: return 0; } } function compute_var(input, unit) { return [`calc(${input})`, unit]; } function calc_value(base, v) { if (is_empty(v) || is_empty(base)) { return []; } if (/^[\+\*\-\/%][\-\.\d\s]/.test(v)) { let op = v[0]; let { unit = '', value } = parse_compound_value(v.substr(1).trim() || 0); if (/var\(/.test(base)) { return op === '%' ? compute_var(`mod(${base}, ${value})`, unit) : compute_var(`${base} ${op} ${value}`, unit); } return [compute(op, Number(base), Number(value)), unit]; } else if (/[\+\*\-\/%]$/.test(v)) { let op = v.substr(-1); let { unit = '', value } = parse_compound_value(v.substr(0, v.length - 1).trim() || 0); if (/var\(/.test(base)) { return op === '%' ? compute_var(`mod(${value}, ${base})`, unit) : compute_var(`${value} ${op} ${base}`, unit); } return [compute(op, Number(value), Number(base)), unit]; } else { let { unit = '', value } = parse_compound_value(v || 0); return [(Number(base) + Number(value)), unit]; } } function calc_with(base) { let unit = ''; return (...args) => { for (let v of args) { let [output, output_unit] = calc_value(base, v); base = output; if (!unit && output_unit) { unit = output_unit; } } if (/^calc\(/.test(base)) { return `calc(${base} * 1${unit})`; } return base + unit; } } const Expose = add_alias({ i({ count }) { return calc_with(count); }, y({ y }) { return calc_with(y); }, x({ x }) { return calc_with(x); }, z({ z }) { return calc_with(z); }, I({ grid }) { return calc_with(grid.count); }, Y({ grid }) { return calc_with(grid.y); }, X({ grid }) { return calc_with(grid.x); }, Z({ grid }) { return calc_with(grid.z); }, iI({ count, grid }) { return calc_with(count/grid.count); }, Ii({ count, grid }) { return calc_with((grid.count - count + 1) / grid.count); }, xX({ x, grid }) { return calc_with(x/grid.x); }, Xx({ x, grid }) { return calc_with((grid.x - x + 1) / grid.x); }, yY({ y, grid }) { return calc_with(y/grid.y); }, Yy({ y, grid }) { return calc_with((grid.y - y + 1) / grid.y); }, id({ x, y, z }) { return _ => cell_id(x, y, z); }, dx({ x, grid }) { return calc_with(x - .5 - grid.x / 2); }, dy({ y, grid }) { return calc_with(y - .5 - grid.y / 2); }, n({ extra }) { let lastExtra = last(extra); return lastExtra ? calc_with(lastExtra[0]) : '@n'; }, nx({ extra }) { let lastExtra = last(extra); return lastExtra ? calc_with(lastExtra[1]) : '@nx'; }, ny({ extra }) { let lastExtra = last(extra); return lastExtra ? calc_with(lastExtra[2]) : '@ny'; }, nd({ extra }) { let lastExtra = last(extra); if (lastExtra) { let n = lastExtra[0]; let N = lastExtra[3]; return d => { d = Number(d) || 0; return n - .5 - d - N / 2; } } return '@nd';; }, N({ extra }) { let lastExtra = last(extra); return lastExtra ? calc_with(lastExtra[3]) : '@N'; }, nN({ extra }) { let lastExtra = last(extra); return lastExtra ? calc_with(lastExtra[0]/lastExtra[3]) : '@nN'; }, Nn({ extra }) { let lastExtra = last(extra); if (lastExtra) { let n = lastExtra[0]; let N = lastExtra[3]; return calc_with((N - n + 1) / N); } return '@Nn'; }, m: make_sequence(','), M: make_sequence(' '), µ: make_sequence(''), p({ context, pick }) { return expand((...args) => { if (!args.length) { args = context.last_pick_args || []; } let picked = pick(args); context.last_pick_args = args; return push_stack(context, 'last_pick', picked); }); }, P({ context, pick, position }) { let counter = 'P-counter' + position; return expand((...args) => { let normal = true; if (!args.length) { args = context.last_pick_args || []; normal = false; } let stack = context.last_pick; let last = stack ? stack.last(1) : ''; if (normal) { if (!context[counter]) { context[counter] = {}; } last = context[counter].last_pick; } if (args.length > 1) { let i = args.findIndex(n => n === last); if (i !== -1) { args.splice(i, 1); } } let picked = pick(args); context.last_pick_args = args; if (normal) { context[counter].last_pick = picked; } return push_stack(context, 'last_pick', picked); }); }, pl({ context, extra, upextra, position }, upstream) { let lastExtra = upstream ? last(upextra.length ? upextra : extra) : last(extra); let sig = lastExtra ? last(lastExtra) : ''; let counter = (upstream ? 'PL-counter' : 'pl-counter') + position + sig; return expand((...args) => { if (!context[counter]) context[counter] = 0; context[counter] += 1; let max = args.length; let idx = lastExtra && lastExtra[6]; idx ??= context[counter]; let pos = (idx - 1) % max; let value = args[pos]; return push_stack(context, 'last_pick', value); }); }, PL(arg) { return Expose.pl(arg, true); }, pr({ context, extra, upextra, position }, upstream) { let lastExtra = upstream ? last(upextra.length ? upextra : extra) : last(extra); let sig = lastExtra ? last(lastExtra) : ''; let counter = (upstream ? 'PR-counter' : 'pr-counter') + position + sig; return expand((...args) => { if (!context[counter]) context[counter] = 0; context[counter] += 1; let max = args.length; let idx = lastExtra && lastExtra[6]; idx ??= context[counter]; let pos = (idx - 1) % max; let value = args[max - pos - 1]; return push_stack(context, 'last_pick', value); }); }, PR(arg) { return Expose.pr(arg, true); }, pd({ context, extra, upextra, position, shuffle }, upstream) { let lastExtra = upstream ? last(upextra.length ? upextra : extra) : last(extra); let sig = lastExtra ? last(lastExtra) : ''; let counter = (upstream ? 'PD-counter' : 'pd-counter') + position + sig; let values = (upstream ? 'PD-valeus' : 'pd-values') + position + sig;; return expand((...args) => { if (!context[counter]) context[counter] = 0; context[counter] += 1; if (!context[values]) { context[values] = shuffle(args || []); } let max = args.length; let idx = lastExtra && lastExtra[6]; idx ??= context[counter]; let pos = (idx - 1) % max; let value = context[values][pos]; return push_stack(context, 'last_pick', value); }); }, PD(arg) { return Expose.pd(arg, true); }, lp({ context }) { return (n = 1) => { let stack = context.last_pick; return stack ? stack.last(n) : ''; }; }, r({ context, rand }) { return (...args) => { let transform = (args.length && args.every(is_letter)) ? by_charcode : by_unit; let value = transform(rand)(...args); return push_stack(context, 'last_rand', value); }; }, rn({ x, y, context, position, grid, extra, random }) { let counter = 'noise-2d' + position; let counterX = counter + 'offset-x'; let counterY = counter + 'offset-y'; let [ni, nx, ny, nm, NX, NY] = last(extra) || []; let isSeqContext = (ni && nm); return (...args) => { let {from = 0, to = from, frequency = 1, scale = 1, octave = 1} = get_named_arguments(args, [ 'from', 'to', 'frequency', 'scale', 'octave' ]); frequency = clamp(frequency, 0, Infinity); scale = clamp(scale, 0, Infinity); octave = clamp(octave, 1, 100); if (args.length == 1) [from, to] = [0, from]; if (!context[counter]) context[counter] = new Noise(); if (!context[counterX]) context[counterX] = random(); if (!context[counterY]) context[counterY] = random(); let transform = (is_letter(from) && is_letter(to)) ? by_charcode : by_unit; let noise2d = context[counter]; let offsetX = context[counterX]; let offsetY = context[counterY]; let _x = (isSeqContext ? ((nx - 1) / NX) : ((x - 1) / grid.x)) + offsetX; let _y = (isSeqContext ? ((ny - 1) / NY) : ((y - 1) / grid.y)) + offsetY; // 1-dimentional if (NX <= 1 || grid.x <= 1) _x = 0; if (NY <= 1 || grid.y <= 1) _y = 0; // 1x1 if (_x == 0 && _y == 0) { _x = offsetX; _y = offsetY; } let t = noise2d.noise(_x * frequency, _y * frequency, 0) * scale; for (let i = 1; i < octave; ++i) { let i2 = i * 2; t += noise2d.noise(_x * frequency * i2, _y * frequency * i2, 0) * (scale / i2); } let fn = transform((from, to) => map2d(t, from, to, scale)); return push_stack(context, 'last_rand', fn(from, to)); }; }, lr({ context }) { return (n = 1) => { let stack = context.last_rand; return stack ? stack.last(n) : ''; }; }, stripe() { return (...input) => { let colors = input.map(get_value).flat(); let max = colors.length; let default_count = 0; let custom_sizes = []; let prev; if (!max) { return ''; } colors.forEach(step => { let [_, size] = parse_value_group(step); if (size !== undefined) custom_sizes.push(size); else default_count += 1; }); let default_size = custom_sizes.length ? `(100% - ${custom_sizes.join(' - ')}) / ${default_count}` : `100% / ${max}` return colors.map((step, i) => { if (custom_sizes.length) { let [color, size] = parse_value_group(step); let prefix = prev ? (prev + ' + ') : ''; prev = prefix + (size !== undefined ? size : default_size); return `${color} 0 calc(${ prev })` } return `${step} 0 ${100 / max * (i + 1)}%` }) .join(','); } }, calc() { return (value, context) => { return calc(get_value(value), context); } }, hex() { return value => parseInt(get_value(value)).toString(16); }, svg: lazy((_, ...args) => { let value = args.map(input => get_value(input())).join(','); if (!value.startsWith('<')) { let parsed = parse_svg(value); value = generate_svg(parsed); } let svg = normalize_svg(value); return create_svg_url(svg); }), 'svg-filter': lazy((upstream, ...args) => { let values = args.map(input => get_value(input())); let value = values.join(','); let id = unique_id('filter-'); // shorthand if (values.every(n => /^[\-\d.]/.test(n) || (/^(\w+)/.test(n) && !/[{}<>]/.test(n)))) { let { frequency, scale, octave, seed = upstream.seed, blur, erode, dilate } = get_named_arguments(values, [ 'frequency', 'scale', 'octave', 'seed', 'blur', 'erode', 'dilate' ]); value = ` x: -20%; y: -20%; width: 140%; height: 140%; `; if (!is_nil(dilate)) { value += ` feMorphology { operator: dilate; radius: ${dilate}; } ` } if (!is_nil(erode)) { value += ` feMorphology { operator: erode; radius: ${erode}; } ` } if (!is_nil(blur)) { value += ` feGaussianBlur { stdDeviation: ${blur}; } ` } if (!is_nil(frequency)) { let [bx, by = bx] = parse_value_group(frequency); octave = octave ? `numOctaves: ${octave};` : ''; value += ` feTurbulence { type: fractalNoise; baseFrequency: ${bx} ${by}; seed: ${seed}; ${octave} } `; if (scale) { value += ` feDisplacementMap { in: SourceGraphic; scale: ${scale}; } `; } } } // new svg syntax if (!value.startsWith('<')) { let parsed = parse_svg(value, { type: 'block', name: 'filter' }); value = generate_svg(parsed); } let svg = normalize_svg(value).replace( /<filter([\s>])/, `<filter id="${ id }"$1` ); return create_svg_url(svg, id); }), 'svg-pattern': lazy((_, ...args) => { let value = args.map(input => get_value(input())).join(','); let parsed = parse_svg(` viewBox: 0 0 1 1; preserveAspectRatio: xMidYMid slice; rect { width, height: 100%; fill: defs pattern { ${ value } } } `); let svg = generate_svg(parsed); return create_svg_url(svg); }), 'svg-polygon': lazy((_, ...args) => { let commands = args.map(input => get_value(input())).join(','); let { rules, points } = generate_shape(commands, 3, 65536, rules => { delete rules.frame; rules['unit'] = 'none'; rules['stroke-width'] ??= .01; rules['stroke'] ??= 'currentColor'; rules['fill'] ??= 'none'; return rules; }); let style = `points: ${points};`; let props = ''; let p = rules.padding ?? Number(rules['stroke-width']) / 2; for (let name of Object.keys(rules)) { if (/^(stroke|fill|clip|marker|mask|animate|draw)/.test(name)) { props += `${name}: ${rules[name]};` } }; let parsed = parse_svg(` viewBox: -1 -1 2 2 p ${p}; polygon { ${props} ${style} } `); return create_svg_url(generate_svg(parsed)); }), var() { return value => `var(${get_value(value)})`; }, ut() { return calc_with(`var(--${utime.name})`); }, ts() { return calc_with(`calc(var(--${utime.name}) / 1000)`); }, TS() { return calc_with(`calc(var(--${UTime.name}) / 1000)`); }, UT() { return calc_with(`var(--${UTime.name})`); }, uw() { return calc_with(`var(--${uwidth.name})`); }, uh() { return calc_with(`var(--${uheight.name})`); }, ux() { return calc_with(`var(--${umousex.name})`); }, uy() { return calc_with(`var(--${umousey.name})`); }, plot({ count, context, extra, position, grid }) { let lastExtra = last(extra); return (...args) => { let commands = args.join(','); let [idx = count, _, __, max = grid.count] = lastExtra || []; let { points, rules } = generate_shape(commands, 1, 65536, rules => { delete rules['fill']; delete rules['fill-rule']; delete rules['frame']; if (rules.split || rules.points) { rules.hasPoints = true; } else { rules.points = max; } return rules; }); return rules.hasPoints ? points : points[idx - 1]; }; }, Plot({ count, context, extra, position, grid }) { let lastExtra = last(extra); return (...args) => { let commands = args.join(','); let [idx = count, _, __, max = grid.count] = lastExtra || []; let { points, rules } = generate_shape(commands, 1, 65536, rules => { delete rules['fill']; delete rules['fill-rule']; delete rules['frame']; if (rules.split || rules.points) { rules.hasPoints = true; } else { rules.points = max; } rules.unit = rules.unit || 'none'; return rules; }); return rules.hasPoints ? points : points[idx - 1]; }; }, shape() { return memo('shape-function', (...args) => { let commands = args.join(','); let { points } = generate_shape(commands); return `polygon(${points.join(',')})`; }); }, doodle() { return value => value; }, shaders() { return value => value; }, pattern() { return value => value; }, invert() { return commands => { let parsed = parse_svg_path(commands); if (!parsed.valid) return commands; return parsed.commands.map(({ name, value }) => { switch (name) { case 'v': return 'h' + value.join(' '); case 'V': return 'H' + value.join(' '); case 'h': return 'v' + value.join(' '); case 'H': return 'V' + value.join(' '); default: return name + value.join(' '); } }).join(' '); }; }, flipH() { return commands => { let parsed = parse_svg_path(commands); if (!parsed.valid) return commands; return parsed.commands.map(({ name, value }) => { switch (name) { case 'h': case 'H': return name + value.map(flip_value).join(' '); default: return name + value.join(' '); } }).join(' '); }; }, flipV() { return commands => { let parsed = parse_svg_path(commands); if (!parsed.valid) return commands; return parsed.commands.map(({ name, value }) => { switch (name) { case 'v': case 'V': return name + value.map(flip_value).join(' '); default: return name + value.join(' '); } }).join(' '); }; }, flip(...args) { let flipH = Expose.flipH(...args); let flipV = Expose.flipV(...args); return commands => { return flipV(flipH(commands)); } }, reverse() { return (...args) => { let commands = args.map(get_value); let parsed = parse_svg_path(commands.join(',')); if (parsed.valid) { let result = []; for (let i = parsed.commands.length - 1; i >= 0; --i) { let { name, value } = parsed.commands[i]; result.push(name + value.join(' ')); } return result.join(' '); } return commands.reverse(); } }, cycle() { return (...args) => { args = args.map(n => '<' + n + '>'); let list = []; let separator; if (args.length == 1) { separator = ' ';; list = parse_value_group(args[0], { symbol: separator }); } else { separator = ','; list = parse_value_group(args.map(get_value).join(separator), { symbol: separator}); } list = list.map(n => n.replace(/^\<|>$/g,'')); let size = list.length - 1; let result = [list.join(separator)]; // Just ignore the performance for (let i = 0; i < size; ++i) { let item = list.shift(); list.push(item); result.push(list.join(separator)); } return result; } }, mirror() { return (...args) => { for (let i = args.length - 1; i >= 0; --i) { args.push(args[i]); } return args; } }, Mirror() { return (...args) => { for (let i = args.length - 2; i >= 0; --i) { args.push(args[i]); } return args; } }, code() { return (...args) => { return args.map(code => String.fromCharCode(code)); } }, once: lazy(({context, extra, position}, ...args) => { let counter = 'once-counter' + position; return context[counter] ??= args.map(input => get_value(input())).join(','); }), raw({ rules }) { return (raw = '') => { try { let cut = raw.substring(raw.indexOf(',') + 1, raw.lastIndexOf('")')); if (raw.startsWith('${doodle') && raw.endsWith('}')) { let key = raw.substring(2, raw.length - 1); let doodles = rules.doodles; if (doodles && doodles[key]) { return `<css-doodle>${doodles[key].doodle}</css-doodle>` } } if (raw.startsWith('url("data:image/svg+xml;utf8')) { return decodeURIComponent(cut); } /* future forms */ if (raw.startsWith('url("data:image/svg+xml;base64')) { return atob(cut); } if (raw.startsWith('url("data:image/png;base64')) { return `<img src="${raw}" alt="" />`; } } catch (e) { /* ignore */ } return raw; } } }, { 'index': 'i', 'col': 'x', 'row': 'y', 'depth': 'z', 'rand': 'r', 'pick': 'p', 'pn': 'pl', 'pnr': 'pr', 'PN': 'PL', 'PNR': 'PR', 'R': 'rn', 'T': 'UT', 't': 'ut', // error prone 'stripes': 'stripe', 'strip': 'stripe', 'patern': 'pattern', 'flipv': 'flipV', 'fliph': 'flipH', // legacy names, keep them before 1.0 'filter': 'svg-filter', 'last-rand': 'lr', 'last-pick': 'lp', 'multiple': 'm', 'multi': 'm', 'rep': 'µ', 'repeat': 'µ', 'ms': 'M', 's': 'I', 'size': 'I', 'sx': 'X', 'size-x': 'X', 'size-col': 'X', 'max-col': 'X', 'sy': 'Y', 'size-y': 'Y', 'size-row': 'Y', 'max-row': 'Y', 'sz': 'Z', 'size-z': 'Z', 'size-depth': 'Z', 'Svg': 'svg', 'pick-by-turn': 'pl', 'pick-n': 'pl', 'pick-d': 'pd', 'offset': 'plot', 'Offset': 'Plot', 'point': 'plot', 'Point': 'Plot', 'unicode': 'code' }); export default Expose;