UNPKG

css-doodle

Version:

A web component for drawing patterns with CSS

360 lines (320 loc) 7.3 kB
import parse_value_group from '../parser/parse-value-group.js'; import parse_direction from '../parser/parse-direction.js'; import parse_compound_value from '../parser/parse-compound-value.js'; import parse_shape_commands from '../parser/parse-shape-commands.js'; import { clamp, is_empty } from '../utils/index.js'; import calc from '../calc.js'; const { cos, sin, abs, atan2, PI } = Math; const preset_shapes = { circle: ` split: 180; scale: .99 `, triangle: ` rotate: 30; scale: 1.1; move: 0 .2 `, pentagon: ` split: 5; rotate: 54 `, hexagon: ` split: 6; rotate: 30; scale: .98 `, octagon: ` split: 8; rotat: 22.5; scale: .99 `, star: ` split: 10; r: cos(5t); rotate: -18; scale: .99 `, infinity: ` split: 180; scale: .99; x: cos(t)*.99 / (sin(t)^2 + 1); y: x * sin(t) `, heart: ` split: 180; rotate: 180; a: cos(t)*13/18 - cos(2t)*5/18; b: cos(3t)/18 + cos(4t)/18; x: (.75 * sin(t)^3) * 1.2; y: (a - b + .2) * -1.1 `, bean: ` split: 180; r: sin(t)^3 + cos(t)^3; move: -.35 .35; `, bicorn: ` split: 180; x: cos(t); y: sin(t)^2 / (2 + sin(t)) - .5 `, drop: ` split: 180; rotate: 90; scale: .95; x: sin(t); y: (1 + sin(t)) * cos(t) / 1.6 `, fish: ` split: 240; x: cos(t) - sin(t)^2 / sqrt(2) - .04; y: sin(2t)/2 `, whale: ` split: 240; rotate: 180; R: 3.4 * (sin(t)^2 - .5) * cos(t); x: cos(t) * R + .75; y: sin(t) * R * 1.2 `, windmill: ` split: 18; R: seq(.618, 1, 0); T: seq(t-.55, t, t); x: R * cos(T); y: R * sin(T) `, vase: ` split: 240; scale: .3; x: sin(4t) + sin(t) * 1.4; y: cos(t) + cos(t) * 4.8 + .3 `, clover(k = 3) { k = clamp(k, 3, 5); if (k == 4) k = 2; return ` split: 240; r: cos(${k}t); scale: .98 `; }, hypocycloid(k = 3) { k = clamp(k, 3, 5); let scale = [.34, .25, .19][k - 3]; return ` split: 240; scale: ${scale}; k: ${k}; x: (k-1)*cos(t) + cos((k-1)*t); y: (k-1)*sin(t) - sin((k-1)*t) `; }, bud(k = 3) { k = clamp(k, 3, 10); return ` split: 240; scale: .8; r: 1 + .2 * cos(${k}t) `; }, }; class Point { constructor(x, y, angle) { this.x = x; this.y = y; this.extra = angle; } valueOf() { return this.x + ' ' + this.y; } toString() { return this.valueOf(); } } function create_polygon_points(option, fn) { if (!fn) { fn = t => [ cos(t), sin(t) ]; } let split = option.split || 180; let turn = option.turn || 1; let frame = option.frame; let fill = option['fill'] || option['fill-rule']; let direction = parse_direction(option['direction'] || option['dir'] || ''); let unit = option.unit; let rad = (PI * 2) * turn / split; let points = []; let first_point, first_point2; let factor = (option.scale === undefined) ? 1 : option.scale; let add = ([x1, y1, dx = 0, dy = 0]) => { if (x1 == 'evenodd' || x1 == 'nonzero') { return points.push(new Point(x1, '', '')); } let [x, y] = scale(x1, -y1, factor); let [dx1, dy2] = scale(dx, -dy, factor); let angle = calc_angle(x, y, dx1, dy2, direction); if (unit !== undefined && unit !== '%') { if (unit !== 'none') { x += unit; y += unit; } } else { x = (x + 1) * 50 + '%'; y = (y + 1) * 50 + '%'; } points.push(new Point(x, y, angle)); } if (fill == 'nonzero' || fill == 'evenodd') { add([fill, '', '']); } for (let i = 0; i < split; ++i) { let t = rad * i; let point = fn(t, i); if (!i) first_point = point; add(point); } if (frame !== undefined) { add(first_point); let w = frame / 100; if (turn > 1) w *= 2; if (w == 0) w = .002; for (let i = 0; i < split; ++i) { let t = -rad * i; let [x, y, dx = 0, dy = 0] = fn(t, i); let theta = atan2(y + dy, x - dx); let point = [ x - w * cos(theta), y - w * sin(theta) ]; if (!i) first_point2 = point; add(point); } add(first_point2); add(first_point); } return points; } function calc_angle(x, y, dx, dy, option) { let base = atan2(y + dy, x - dx) * 180 / PI; if (option.direction === 'reverse') { base -= 180; } if (!option.direction) { base = 90; } if (option.angle) { base += option.angle; } return base; } function rotate(x, y, deg) { let rad = -PI / 180 * deg; return [ x * cos(rad) - y * sin(rad), y * cos(rad) + x * sin(rad) ]; } function translate(x, y, offset) { let [dx, dy = dx] = parse_value_group(offset).map(Number); return [ x + (dx || 0), y - (dy || 0), dx, dy ]; } function scale(x, y, factor) { let [fx, fy = fx] = parse_value_group(factor).map(Number); return [ x * fx, y * fy ]; } function create_shape_points(props, {min, max}) { let split = clamp(parseInt(props.vertices || props.points || props.split) || 0, min, max); let px = is_empty(props.x) ? 'cos(t)' : props.x; let py = is_empty(props.y) ? 'sin(t)' : props.y; let pr = is_empty(props.r) ? '' : props.r; let pt = is_empty(props.t) ? '' : props.t; let { unit, value } = parse_compound_value(pr); if (unit && !props[unit] && unit !== 't') { if (is_empty(props.unit)) { props.unit = unit; } pr = props.r = value; } if (props.degree) { props.rotate = props.degree; } if (props.origin) { props.move = props.origin; } props.split = split; return create_polygon_points(props, (t, i) => { let context = Object.assign({}, props, { 't': pt || t, 'θ': pt || t, 'i': (i + 1), seq(...list) { if (!list.length) return ''; return list[i % list.length]; }, range(a, b = 0) { a = Number(a) || 0; b = Number(b) || 0; if (a > b) [a, b] = [b, a]; let step = abs(b - a) / (split - 1); return a + step * i; } }); let x = calc(px, context); let y = calc(py, context); let dx = 0; let dy = 0; if (pr) { let r = calc(pr, context); if (r == 0) { r = .00001; } if (pt) { t = calc(pt, context); } x = r * cos(t); y = r * sin(t); } if (props.rotate) { [x, y] = rotate(x, y, Number(props.rotate) || 0); } if (props.move) { [x, y, dx, dy] = translate(x, y, props.move); } return [x, y, dx, dy]; }); } export default function generate_shape(input, min=3, max=3600, modifier) { let commands = ''; let [name, ...args] = parse_value_group(input); let preset = false; switch (typeof preset_shapes[name]) { case 'function': commands = preset_shapes[name](...args); preset = true; break; case 'string': commands = preset_shapes[name]; preset = true; break; default: { commands = input; } } let rules = parse_shape_commands(commands); if (typeof modifier === 'function') { rules = modifier(rules); } let points = create_shape_points(rules, {min, max}); return { rules, points, preset } }