themer
Version:
Customizable theme creator for editors, terminals, wallpaper, and more.
215 lines (194 loc) • 6.31 kB
text/typescript
import { listOutputFiles, Template, weightedRandom } from './index.js';
import { colorSetToVariants } from '../color-set/index.js';
import { source } from 'common-tags';
const BORDER_SIZE = 75;
const GRID_CELL_SIZE = 30;
const END_CAP_SIZE = 5;
const FIND_PATH_TRY_COUNT = 4;
const JITTER = 0.5;
class Point {
static add(a: Point, b: Point) {
return new Point(a.x + b.x, a.y + b.y);
}
static subtract(a: Point, b: Point) {
return new Point(a.x - b.x, a.y - b.y);
}
clone() {
return new Point(this.x, this.y);
}
constructor(readonly x: number, readonly y: number) {}
}
const template: Template = {
name: 'Circuits wallpaper',
render: async function* (colorSet, options) {
const variants = colorSetToVariants(colorSet);
for (const variant of variants) {
for (const size of options.wallpaperSizes) {
// Calculate the drawing area
const columnCount = Math.floor(
(size.w - BORDER_SIZE * 2) / GRID_CELL_SIZE,
);
const rowCount = Math.floor(
(size.h - BORDER_SIZE * 2) / GRID_CELL_SIZE,
);
const gridWidth = columnCount * GRID_CELL_SIZE;
const gridHeight = rowCount * GRID_CELL_SIZE;
const gridOriginX = (size.w - gridWidth) / 2;
const gridOriginY = (size.h - gridHeight) / 2;
const gridCellState = new Map();
for (let i = 0; i < columnCount; i++) {
const column = new Map();
for (let j = 0; j < rowCount; j++) {
column.set(j, null);
}
gridCellState.set(i, column);
}
function getNext(start: Point, previous: Point | null) {
const options = [
new Point(0, -1),
new Point(1, 0),
new Point(0, 1),
new Point(-1, 0),
].filter((option) =>
previous
? !(option.x === previous.x && option.y === previous.y)
: true,
);
for (let tryCount = 0; tryCount < FIND_PATH_TRY_COUNT; tryCount++) {
const proposed = Point.add(
start,
options[Math.floor(Math.random() * options.length)]!,
);
if (
gridCellState.has(proposed.x) &&
gridCellState.get(proposed.x).has(proposed.y) &&
gridCellState.get(proposed.x).get(proposed.y) === null
) {
return proposed;
}
}
return null;
}
function jitter() {
return (Math.random() - 0.5) * END_CAP_SIZE * JITTER;
}
function getCenter(point: Point) {
return new Point(
gridOriginX +
point.x * GRID_CELL_SIZE +
GRID_CELL_SIZE / 2 +
jitter(),
gridOriginY +
point.y * GRID_CELL_SIZE +
GRID_CELL_SIZE / 2 +
jitter(),
);
}
const paths = [];
for (const [x, column] of gridCellState.entries()) {
for (const [y, cell] of column.entries()) {
if (cell === null) {
let previous = null;
let current = new Point(x, y);
const points = [current.clone()];
gridCellState.get(current.x).set(current.y, true);
let next;
while (
(next = getNext(
current,
previous && Point.subtract(previous, current),
)) !== null
) {
points.push(next.clone());
gridCellState.get(next.x).set(next.y, true);
previous = current;
current = next;
}
paths.push(points);
}
}
}
function getStrokeStyle() {
return weightedRandom(
new Map([
[variant.colors.shade2, 50],
[variant.colors.accent0, 1],
[variant.colors.accent1, 1],
[variant.colors.accent2, 1],
[variant.colors.accent3, 1],
[variant.colors.accent4, 1],
[variant.colors.accent5, 1],
[variant.colors.accent6, 1],
[variant.colors.accent7, 1],
]),
);
}
const elements: string[] = [];
paths.forEach((points) => {
const strokeColor =
points.length > 1 ? getStrokeStyle() : variant.colors.shade1;
elements.push(source`
<path
fill="none"
stroke="${strokeColor}"
stroke-width="2.5"
d="${points
.map((point, i) => {
const c = getCenter(point);
return i === 0 ? `M${c.x},${c.y}` : `L${c.x},${c.y}`;
})
.join(' ')}"
/>
`);
const startCenter = getCenter(points[0]!);
elements.push(source`
<circle
cx="${startCenter.x}"
cy="${startCenter.y}"
r="${END_CAP_SIZE}"
fill="${variant.colors.shade0}"
stroke="${strokeColor}"
stroke-width="2.5"
/>
`);
if (points.length > 1) {
const endCenter = getCenter(points[points.length - 1]!);
elements.push(source`
<circle
cx="${endCenter.x}"
cy="${endCenter.y}"
r="${END_CAP_SIZE}"
fill="${variant.colors.shade0}"
stroke="${strokeColor}"
stroke-width="2.5"
/>
`);
}
});
const svg = source`
<svg
xmlns="http://www.w3.org/2000/svg"
width="${size.w}"
height="${size.h}"
viewBox="0 0 ${size.w} ${size.h}"
>
<rect
x="0"
y="0"
width="${size.w}"
height="${size.h}"
fill="${variant.colors.shade0}"
/>
${elements}
</svg>
`;
yield {
path: `${variant.title.kebab}-${size.w}x${size.h}.svg`,
content: svg,
};
}
}
},
renderInstructions: listOutputFiles,
};
export default template;