diff --git a/example/src/common/Pattern.svelte b/example/src/common/Pattern.svelte index f1e0e3b..43e462f 100644 --- a/example/src/common/Pattern.svelte +++ b/example/src/common/Pattern.svelte @@ -1,10 +1,14 @@ diff --git a/src/consts.ts b/src/consts.ts index 1b0bb70..58e9467 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -4,6 +4,7 @@ export const LINE_WIDTHS = [ 0.5, 1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12, 14, 15, 16, ]; export const GRID_CELL_SIZE: number[] = [...Array(128)].map((_, i) => i + 1); +export const SPACING: number[] = [...Array(128)].map((_, i) => i + 1); export const OFFSETS: number[] = [...Array(128 + 16)].map((_, i) => i + 1); export const DEFAULT_OPTS: IResolvedOpts = { diff --git a/src/index.ts b/src/index.ts index 285d3e5..1e6fa48 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ import plugin from "tailwindcss/plugin"; -import { GRID_CELL_SIZE, LINE_WIDTHS, OFFSETS } from "./consts"; +import { GRID_CELL_SIZE, LINE_WIDTHS, OFFSETS, SPACING } from "./consts"; +import { arrToTwConfig } from "./lib/arrToTwConfig"; import { generateCellSizes, matchCellSize } from "./lib/cellSizes"; import { generateLineColors, @@ -8,9 +9,10 @@ import { } from "./lib/line"; import { generateOffsets, matchOffsets } from "./lib/offsets"; import { resolveOptions } from "./lib/resolveOptions"; +import { generateSpacing, matchSpacing } from "./lib/spacing"; import { generateGridClass } from "./patterns/grid"; +import { generateHatchingClass, genreateHatchingDirection } from "./patterns/hatching"; import type { IOptions } from "./types"; -import { arrToTwConfig } from "./lib/arrToTwConfig"; export default plugin.withOptions( (options) => (api) => { @@ -18,15 +20,25 @@ export default plugin.withOptions( const opts = resolveOptions(options); addUtilities([ + // Patterns generateGridClass(e(`${opts.prefix}pattern-grid`)), + generateHatchingClass(e(`${opts.prefix}pattern-hatching`)), + generateHatchingClass(e(`${opts.prefix}pattern-cross-hatching`), { + isCrossHatch: true, + }), + + // Configs generateLineWidths(api, opts), generateCellSizes(api, opts), + generateSpacing(api, opts), + genreateHatchingDirection(api, opts), generateLineColors(api, opts), generateOffsets(api, opts), ]); matchUtilities({ ...matchCellSize(api, opts), + ...matchSpacing(api, opts), ...matchLineWidthsAndColors(api, opts), ...matchOffsets(api, opts), }); @@ -35,6 +47,7 @@ export default plugin.withOptions( theme: { bgPatternLineWidth: arrToTwConfig(LINE_WIDTHS), bgPatternCellSize: arrToTwConfig(GRID_CELL_SIZE), + bgPatternSpacing: arrToTwConfig(SPACING), bgPatternOffsets: arrToTwConfig(OFFSETS, "px"), }, }), diff --git a/src/lib/spacing.ts b/src/lib/spacing.ts new file mode 100644 index 0000000..dd64de1 --- /dev/null +++ b/src/lib/spacing.ts @@ -0,0 +1,27 @@ +import type { CSSRuleObject, PluginAPI } from "tailwindcss/types/config"; +import type { IOptions } from "../types"; + +export const generateSpacing = ( + api: PluginAPI, + opts: IOptions, +): CSSRuleObject => { + const spacing = api.theme("bgPatternSpacing"); + const styles: Record = {}; + if (spacing) { + for (const key in spacing) { + const value = spacing[key]; + styles[`.${api.e(`${opts.prefix}pattern-spacing-${key}`)}`] = { + "--tw-spacing": `${value} /* px */`, + } as unknown as CSSRuleObject; + } + } + return styles as CSSRuleObject; +}; + +export const matchSpacing = (api: PluginAPI, opts: IOptions) => ({ + [api.e(`${opts.prefix}pattern-spacing`)]: (value: string) => { + return { + "--tw-spacing": `${value} /* px */`, + } as unknown as CSSRuleObject; + } +}); diff --git a/src/patterns/hatching.ts b/src/patterns/hatching.ts new file mode 100644 index 0000000..22ff01a --- /dev/null +++ b/src/patterns/hatching.ts @@ -0,0 +1,102 @@ +import type { CSSRuleObject, PluginAPI } from "tailwindcss/types/config"; +import type { IOptions } from "../types"; + +interface IOpts { + lineSize: number; + spacing: number; + offsetX: number; + offsetY: number; + isCrossHatch: boolean; + isRightLeaning: boolean; + lineColor: string; +} + +export const generateHatchingClass = ( + className: string, + _opts?: Partial, +): CSSRuleObject => { + const opts: IOpts = { + lineSize: 1, + lineColor: "#ffffff", + spacing: 12, + offsetX: 0, + offsetY: 0, + isCrossHatch: false, + isRightLeaning: false, + ..._opts, + }; + + let bgImageCode = `linear-gradient( + var(--tw-hatching-angle), + transparent var(--tw-1st-start-stop), + var(--tw-line-color) var(--tw-1st-start-stop), + var(--tw-line-color) var(--tw-1st-end-stop), + transparent var(--tw-1st-end-stop), + transparent 100% + ), + linear-gradient( + var(--tw-hatching-angle), + transparent var(--tw-2ed-start-stop), + var(--tw-line-color) var(--tw-2ed-start-stop), + var(--tw-line-color) var(--tw-2ed-end-stop), + transparent var(--tw-2ed-end-stop), + transparent 100% + ) + `; + + if (opts.isCrossHatch) { + bgImageCode += `, linear-gradient( + calc(var(--tw-hatching-angle) * -1), + transparent var(--tw-1st-start-stop), + var(--tw-line-color) var(--tw-1st-start-stop), + var(--tw-line-color) var(--tw-1st-end-stop), + transparent var(--tw-1st-end-stop), + transparent 100% + ), + linear-gradient( + calc(var(--tw-hatching-angle) * -1), + transparent var(--tw-2ed-start-stop), + var(--tw-line-color) var(--tw-2ed-start-stop), + var(--tw-line-color) var(--tw-2ed-end-stop), + transparent var(--tw-2ed-end-stop), + transparent 100% + )`; + } + + return { + [`.${className}`]: { + "--tw-line-size": opts.lineSize.toString(), + "--tw-spacing": opts.spacing.toString(), + "--tw-offset-x": `${opts.offsetX * -1}px`, + "--tw-offset-y": `${opts.offsetY * -1}px`, + "--tw-hatching-angle": `${opts.isRightLeaning ? -45 : 45}deg`, + "--tw-line-color": opts.lineColor, + + "--tw-unit": "calc((var(--tw-line-size) + var(--tw-spacing)) * 2)", + "--tw-line-stop": + "calc((var(--tw-line-size) / var(--tw-unit) * 100%) / 2)", + "--tw-1st-start-stop": "calc(75% - var(--tw-line-stop))", + "--tw-1st-end-stop": "calc(75% + var(--tw-line-stop))", + "--tw-2ed-start-stop": "calc(25% - var(--tw-line-stop))", + "--tw-2ed-end-stop": "calc(25% + var(--tw-line-stop))", + backgroundImage: bgImageCode, + backgroundSize: "calc(var(--tw-unit) * 1px) calc(var(--tw-unit) * 1px)", + backgroundPosition: + "calc(var(--tw-spacing) * -0.5px + var(--tw-offset-x)) var(--tw-offset-y)", + }, + }; +}; + +export const genreateHatchingDirection = ( + api: PluginAPI, + opts: IOptions, +): CSSRuleObject => { + return { + [`.${api.e(`${opts.prefix}pattern-hatching-left-to-right`)}`]: { + "--tw-hatching-angle": "45deg", + }, + [`.${api.e(`${opts.prefix}pattern-hatching-right-to-left`)}`]: { + "--tw-hatching-angle": "-45deg", + }, + } as unknown as CSSRuleObject; +}; diff --git a/tests/index.test.ts b/tests/config.test.ts similarity index 75% rename from tests/index.test.ts rename to tests/config.test.ts index 2493266..357e9d3 100644 --- a/tests/index.test.ts +++ b/tests/config.test.ts @@ -1,5 +1,5 @@ import { describe, test } from "vitest"; -import { DEFAULT_OPTS, LINE_WIDTHS, OFFSETS } from "../src/consts"; +import { DEFAULT_OPTS, LINE_WIDTHS, OFFSETS, SPACING } from "../src/consts"; import { css, expectCssToBe, generateTwCss } from "./utils"; describe("line widths", () => { @@ -24,6 +24,31 @@ describe("line widths", () => { }); }); +describe("spacing", () => { + test(`spacing ${SPACING[0]}-${SPACING[SPACING.length - 1]}`, async () => { + const expected: string[] = []; + const classes: string[] = []; + for (const value of SPACING) { + classes.push(`${DEFAULT_OPTS.prefix}pattern-spacing-${value}`); + expected.push( + css`.${DEFAULT_OPTS.prefix}pattern-spacing-${value} { + --tw-spacing: ${value} /* px */ + }`, + ); + } + expectCssToBe(await generateTwCss(classes.join(" ")), expected.join("")); + }); + + test("custom spacing", async () => { + expectCssToBe( + await generateTwCss(`${DEFAULT_OPTS.prefix}pattern-spacing-[321]`), + css`.${DEFAULT_OPTS.prefix}pattern-spacing-[321] { + --tw-spacing: 321 /* px */ + }`, + ); + }); +}); + describe("offsets", () => { test(`offsets ${OFFSETS[0]}-${OFFSETS[OFFSETS.length - 1]}`, async () => { const expected: string[] = []; diff --git a/tests/consts.test.ts b/tests/consts.test.ts index 9b2e774..a622e29 100644 --- a/tests/consts.test.ts +++ b/tests/consts.test.ts @@ -4,6 +4,7 @@ import { GRID_CELL_SIZE, LINE_WIDTHS, OFFSETS, + SPACING, } from "../src/consts"; describe("default production values", () => { @@ -25,4 +26,8 @@ describe("default production values", () => { test("offsets", () => { expect(OFFSETS).toEqual([...Array(128 + 16)].map((_, i) => i + 1)); }); + + test("spacing", () => { + expect(SPACING).toEqual([...Array(128)].map((_, i) => i + 1)); + }); }); diff --git a/tests/hatching.test.ts b/tests/hatching.test.ts new file mode 100644 index 0000000..264fecd --- /dev/null +++ b/tests/hatching.test.ts @@ -0,0 +1,89 @@ +import { describe, test } from "vitest"; +import { DEFAULT_OPTS } from "../src/consts"; +import { css, expectCssToBe, generateTwCss } from "./utils"; + +const DEFAULT_HATCHING_CSS = css` +.${DEFAULT_OPTS.prefix}pattern-hatching { + --tw-line-size: 1; + --tw-spacing: 12; + --tw-offset-x: 0px; + --tw-offset-y: 0px; + --tw-hatching-angle: 45deg; + --tw-line-color: #ffffff; + --tw-unit: calc((var(--tw-line-size) + var(--tw-spacing))* 2); + --tw-line-stop: calc((var(--tw-line-size) / var(--tw-unit)* 100%) / 2); + --tw-1st-start-stop: calc(75% - var(--tw-line-stop)); + --tw-1st-end-stop: calc(75% + var(--tw-line-stop)); + --tw-2ed-start-stop: calc(25% - var(--tw-line-stop)); + --tw-2ed-end-stop: calc(25% + var(--tw-line-stop)); + background-image: linear-gradient(var(--tw-hatching-angle), transparent var(--tw-1st-start-stop), var(--tw-line-color) var(--tw-1st-start-stop), var(--tw-line-color) var(--tw-1st-end-stop), transparent var(--tw-1st-end-stop), transparent 100%), linear-gradient(var(--tw-hatching-angle), transparent var(--tw-2ed-start-stop), var(--tw-line-color) var(--tw-2ed-start-stop), var(--tw-line-color) var(--tw-2ed-end-stop), transparent var(--tw-2ed-end-stop), transparent 100%); + background-size: calc(var(--tw-unit)* 1px) calc(var(--tw-unit)* 1px); + background-position: calc(var(--tw-spacing)* -0.5px + var(--tw-offset-x)) var(--tw-offset-y) +}`; + +describe("hatching patterns", () => { + test("hatching left-to-right pattern (default)", async () => { + expectCssToBe( + await generateTwCss(`${DEFAULT_OPTS.prefix}pattern-hatching`), + DEFAULT_HATCHING_CSS, + ); + }); + + test("hatching right-to-left pattern", async () => { + expectCssToBe( + await generateTwCss( + `${DEFAULT_OPTS.prefix}pattern-hatching ${DEFAULT_OPTS.prefix}pattern-hatching-right-to-left`, + ), + css`${DEFAULT_HATCHING_CSS} + .${DEFAULT_OPTS.prefix}pattern-hatching-right-to-left { + --tw-hatching-angle: -45deg + }`, + ); + }); + + test("cross-hatching pattern", async () => { + expectCssToBe( + await generateTwCss(`${DEFAULT_OPTS.prefix}pattern-cross-hatching`), + css`.${DEFAULT_OPTS.prefix}pattern-cross-hatching { + --tw-line-size: 1; + --tw-spacing: 12; + --tw-offset-x: 0px; + --tw-offset-y: 0px; + --tw-hatching-angle: 45deg; + --tw-line-color: #ffffff; + --tw-unit: calc((var(--tw-line-size) + var(--tw-spacing))* 2); + --tw-line-stop: calc((var(--tw-line-size) / var(--tw-unit)* 100%) / 2); + --tw-1st-start-stop: calc(75% - var(--tw-line-stop)); + --tw-1st-end-stop: calc(75% + var(--tw-line-stop)); + --tw-2ed-start-stop: calc(25% - var(--tw-line-stop)); + --tw-2ed-end-stop: calc(25% + var(--tw-line-stop)); + background-image: linear-gradient(var(--tw-hatching-angle), transparent var(--tw-1st-start-stop), var(--tw-line-color) var(--tw-1st-start-stop), var(--tw-line-color) var(--tw-1st-end-stop), transparent var(--tw-1st-end-stop), transparent 100%), linear-gradient(var(--tw-hatching-angle), transparent var(--tw-2ed-start-stop), var(--tw-line-color) var(--tw-2ed-start-stop), var(--tw-line-color) var(--tw-2ed-end-stop), transparent var(--tw-2ed-end-stop), transparent 100%), linear-gradient(calc(var(--tw-hatching-angle)* -1), transparent var(--tw-1st-start-stop), var(--tw-line-color) var(--tw-1st-start-stop), var(--tw-line-color) var(--tw-1st-end-stop), transparent var(--tw-1st-end-stop), transparent 100%), linear-gradient(calc(var(--tw-hatching-angle)* -1), transparent var(--tw-2ed-start-stop), var(--tw-line-color) var(--tw-2ed-start-stop), var(--tw-line-color) var(--tw-2ed-end-stop), transparent var(--tw-2ed-end-stop), transparent 100%); + background-size: calc(var(--tw-unit)* 1px) calc(var(--tw-unit)* 1px); + background-position: calc(var(--tw-spacing)* -0.5px + var(--tw-offset-x)) var(--tw-offset-y) + }`, + ); + }); +}); + +describe("hatching direction", () => { + test("left-to-right", async () => { + expectCssToBe( + await generateTwCss( + `${DEFAULT_OPTS.prefix}pattern-hatching-left-to-right`, + ), + css`.${DEFAULT_OPTS.prefix}pattern-hatching-left-to-right { + --tw-hatching-angle: 45deg + }`, + ); + }); + test("right-to-left", async () => { + expectCssToBe( + await generateTwCss( + `${DEFAULT_OPTS.prefix}pattern-hatching-right-to-left`, + ), + css`.${DEFAULT_OPTS.prefix}pattern-hatching-right-to-left { + --tw-hatching-angle: -45deg + }`, + ); + }); +});