-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgenerate_marble.ts
107 lines (90 loc) · 3.23 KB
/
generate_marble.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
import {distinct} from 'https://deno.land/[email protected]/collections/distinct.ts';
const RADIUS = 500;
const DIAMETER = RADIUS * 2;
const DEFAULT_CANVAS_SIZE = 10000;
const DEFAULT_CURVENESS = .7;
enum BlendMode {
color = 'color',
colorBurn = 'color-burn',
colorDodge = 'color-dodge',
darken = 'darken',
difference = 'difference',
exclusion = 'exclusion',
hardLight = 'hard-light',
hue = 'hue',
lighten = 'lighten',
luminosity = 'luminosity',
multiply = 'multiply',
normal = 'normal',
overlay = 'overlay',
saturation = 'saturation',
screen = 'screen',
softLight = 'soft-light'
};
interface MarbleOptions {
circles?: number
/** Any CSS color. */
colors?: string[]
/** Between 0 and 1. Default 0.7 */
curveness?: number
}
interface Circle {
r: number
cx: number
cy: number
}
function generateMarble(opts?: MarbleOptions): string {
const curveness = (1 - (opts?.curveness ?? DEFAULT_CURVENESS) ** 2);
const virtualCanvasSize = Math.round(DEFAULT_CANVAS_SIZE * curveness);
const center = virtualCanvasSize / 2;
const blendOpts = (opts?.colors?.length ? {exclude: [BlendMode.colorBurn, BlendMode.difference]} : {});
let nCircles = (opts?.circles || 4);
const circles: Circle[] = [];
const blends: BlendMode[] = [randBlend(blendOpts)];
while (--nCircles) {
circles.push(randCircle(virtualCanvasSize));
blends.push(randBlend(blendOpts));
}
return `
<svg viewBox="${center - RADIUS} ${center - RADIUS} ${DIAMETER} ${DIAMETER}" width="${DIAMETER}" xmlns="http://www.w3.org/2000/svg">
<defs>
<mask id="mask">
<circle cx="${center}" cy="${center}" r="${RADIUS}" fill="#fff"/>
</mask>
${distinct(blends).map(mode => `
<filter id="blend_${mode}">
<feBlend mode="${mode}"/>
</filter>`).join('')
}
</defs>
<g mask="url(#mask)">
<circle r="${RADIUS + 5}" cx="${center}" cy="${center}" fill="${(opts?.colors?.length ? opts.colors[0] : randColor({opacity: 1}))}" filter="url(#blend_${blends[0]})"/>
${circles.map((circle, i) => `
<circle r="${circle.r}" cx="${circle.cx}" cy="${circle.cy}" fill="${((opts?.colors?.length && opts.colors.length > (i + 1)) ? opts.colors[i + 1] : randColor())}" filter="url(#blend_${blends[i + 1]})"/>`).join('')
}
</g>
</svg>
`.replaceAll(/[\n\t]/g, '');
}
function randCircle(virtualCanvasSize: number): Circle {
const cx = Math.round(Math.random() * virtualCanvasSize);
const cy = Math.round(Math.random() * virtualCanvasSize);
const canvasCenter = (virtualCanvasSize / 2);
const x = Math.abs(canvasCenter - cx);
const y = Math.abs(canvasCenter - cy);
const r = Math.max(0, Math.round(Math.sqrt(x ** 2 + y ** 2)) - RADIUS) + Math.round(Math.random() * DIAMETER);
return {r, cx, cy};
}
function randColor(opts?: {opacity: number}) {
return '#' + randColorComponent() + randColorComponent() + randColorComponent() + randColorComponent(opts);
}
function randColorComponent(opts?: {opacity: number}) {
return (Math.round(255 * (opts?.opacity ?? Math.random()))).toString(16).padStart(2, '0');
}
function randBlend(opts?: {exclude?: BlendMode[]}) {
let modes = Object.values(BlendMode);
if (opts?.exclude)
modes = modes.filter(mode => !opts.exclude?.includes(mode));
return modes[(Math.round(Math.random() * (modes.length - 1)))];
}
export default generateMarble;