Skip to content

Commit 0a9577b

Browse files
authored
feat(preset-wind4): add mask rule (#4754)
1 parent 1b06cf4 commit 0a9577b

File tree

7 files changed

+462
-2
lines changed

7 files changed

+462
-2
lines changed

packages-presets/preset-wind4/src/rules/default.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { gaps } from './gap'
1616
import { grids } from './grid'
1717
import { overflows } from './layout'
1818
import { lineClamps } from './line-clamp'
19+
import { masks } from './mask'
1920
import { placeholders } from './placeholder'
2021
import { alignments, boxSizing, flexGridJustifiesAlignments, floats, insets, justifies, orders, placements, positions, zIndexes } from './position'
2122
import { questionMark } from './question-mark'
@@ -114,6 +115,7 @@ export const rules: Rule<Theme>[] = [
114115
backgroundBlendModes,
115116
mixBlendModes,
116117
dynamicViewportHeight,
118+
masks,
117119

118120
columns,
119121
filters,

packages-presets/preset-wind4/src/rules/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export * from './gap'
1515
export * from './grid'
1616
export * from './layout'
1717
export * from './line-clamp'
18+
export * from './mask'
1819
export * from './placeholder'
1920
export * from './position'
2021
export * from './question-mark'
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import type { CSSObject, CSSValueInput, Rule, RuleContext } from '@unocss/core'
2+
import type { Theme } from '@unocss/preset-wind4'
3+
import {
4+
colorResolver,
5+
cornerMap,
6+
defineProperty,
7+
h,
8+
hasParseableColor,
9+
hyphenate,
10+
numberResolver,
11+
positionMap,
12+
themeTracking,
13+
} from '../utils'
14+
15+
const linearMap: Record<string, string[]> = {
16+
t: ['top'],
17+
b: ['bottom'],
18+
l: ['left'],
19+
r: ['right'],
20+
x: ['left', 'right'],
21+
y: ['top', 'bottom'],
22+
}
23+
24+
const maskInitialValue = 'linear-gradient(#fff, #fff)'
25+
26+
const baseMaskImage = {
27+
'mask-image': 'var(--un-mask-linear), var(--un-mask-radial), var(--un-mask-conic)',
28+
'mask-composite': 'intersect',
29+
}
30+
31+
function handlePosition([,v = '']: string[]) {
32+
if (v in cornerMap) {
33+
const positions = v.split('').flatMap(c => linearMap[c]).join(' ')
34+
return { 'mask-position': positions }
35+
}
36+
const _v = h.bracket.cssvar.global.position(v)
37+
if (_v !== null)
38+
return { 'mask-position': _v }
39+
}
40+
41+
function handleImage([_, gradient = '', direction, val]: string[], ctx: RuleContext<Theme>) {
42+
const css: CSSObject = { ...baseMaskImage }
43+
const props: (CSSValueInput | string)[] = []
44+
45+
props.push(...['linear', 'radial', 'conic'].map(g => defineProperty(`--un-mask-${g}`, { initialValue: maskInitialValue })))
46+
47+
if (gradient in linearMap) {
48+
css['--un-mask-linear'] = 'var(--un-mask-left), var(--un-mask-right), var(--un-mask-bottom), var(--un-mask-top)'
49+
50+
for (const dir of linearMap[gradient]) {
51+
css[`--un-mask-${dir}`] = `linear-gradient(to ${dir}, var(--un-mask-${dir}-from-color) var(--un-mask-${dir}-from-position), var(--un-mask-${dir}-to-color) var(--un-mask-${dir}-to-position))`
52+
53+
if (numberResolver(val) != null) {
54+
themeTracking('spacing')
55+
css[`--un-mask-${dir}-${direction}-position`] = `calc(var(--spacing) * ${h.bracket.cssvar.fraction.number(val)})`
56+
}
57+
else {
58+
css[`--un-mask-${dir}-${direction}-position`] = h.bracket.cssvar.fraction.rem(val)
59+
}
60+
61+
if (hasParseableColor(val, ctx.theme)) {
62+
const result = colorResolver(`--un-mask-${dir}-${direction}-color`, hyphenate('colors'))([_, val], ctx)
63+
if (result) {
64+
const [c, ...p] = result
65+
Object.assign(css, c)
66+
props.push(...p)
67+
}
68+
}
69+
70+
props.push(...['from', 'to'].flatMap(p => [
71+
defineProperty(`--un-mask-${dir}-${p}-position`, { syntax: '<length-percentage>', initialValue: p === 'from' ? '0%' : '100%' }),
72+
defineProperty(`--un-mask-${dir}-${p}-color`, { syntax: '<color>', initialValue: p === 'from' ? 'black' : 'transparent' }),
73+
]))
74+
}
75+
76+
props.push(...['top', 'right', 'bottom', 'left'].map(d => defineProperty(`--un-mask-${d}`, { initialValue: maskInitialValue })))
77+
}
78+
else {
79+
if (direction == null) {
80+
if (gradient === 'radial') {
81+
css['--un-mask-radial'] = 'radial-gradient(var(--un-mask-radial-stops, var(--un-mask-radial-size)))'
82+
css['--un-mask-radial-size'] = h.bracket.cssvar.rem(val)
83+
}
84+
else {
85+
css[`--un-mask-${gradient}`] = `${gradient}-gradient(var(--un-mask-${gradient}-stops, var(--un-mask-${gradient}-position)))`
86+
css[`--un-mask-${gradient}-position`] = numberResolver(val) ? `calc(1deg * ${h.bracket.cssvar.number(val)})` : h.bracket.cssvar.fraction(val)
87+
}
88+
}
89+
else {
90+
const gradientStopsPrefixMap: Record<string, string> = {
91+
linear: '',
92+
radial: 'var(--un-mask-radial-shape) var(--un-mask-radial-size) at ',
93+
conic: 'from ',
94+
}
95+
css[`--un-mask-${gradient}-stops`] = `${gradientStopsPrefixMap[gradient]}var(--un-mask-${gradient}-position), var(--un-mask-${gradient}-from-color) var(--un-mask-${gradient}-from-position), var(--un-mask-${gradient}-to-color) var(--un-mask-${gradient}-to-position)`
96+
css[`--un-mask-${gradient}`] = `${gradient}-gradient(var(--un-mask-${gradient}-stops))`
97+
98+
if (hasParseableColor(val, ctx.theme)) {
99+
const result = colorResolver(`--un-mask-${gradient}-${direction}-color`, hyphenate('colors'))([_, val], ctx)
100+
if (result) {
101+
const [c, ...p] = result
102+
Object.assign(css, c)
103+
props.push(...p)
104+
}
105+
}
106+
else {
107+
if (numberResolver(val) != null) {
108+
themeTracking('spacing')
109+
css[`--un-mask-${gradient}-${direction}-position`] = `calc(var(--spacing) * ${h.bracket.cssvar.fraction.number(val)})`
110+
}
111+
else {
112+
css[`--un-mask-${gradient}-${direction}-position`] = h.bracket.cssvar.fraction.rem(val)
113+
}
114+
}
115+
}
116+
117+
if (gradient === 'radial') {
118+
props.push(...[
119+
defineProperty('--un-mask-radial-shape', { initialValue: 'ellipse' }),
120+
defineProperty('--un-mask-radial-size', { initialValue: 'farthest-corner' }),
121+
])
122+
}
123+
124+
props.push(...['from', 'to'].flatMap(p => [
125+
defineProperty(`--un-mask-${gradient}-position`, { initialValue: gradient === 'radial' ? 'center' : '0deg' }),
126+
defineProperty(`--un-mask-${gradient}-${p}-position`, { syntax: '<length-percentage>', initialValue: p === 'from' ? '0%' : '100%' }),
127+
defineProperty(`--un-mask-${gradient}-${p}-color`, { syntax: '<color>', initialValue: p === 'from' ? 'black' : 'transparent' }),
128+
]))
129+
}
130+
131+
return [css, ...props]
132+
}
133+
134+
function handleSize([, v = '']: string[]) {
135+
const _v = h.bracket.cssvar.global.fraction.rem(v)
136+
if (_v !== null)
137+
return { 'mask-size': _v }
138+
}
139+
140+
export const masks: Rule<Theme>[] = [
141+
// mask-clip
142+
['mask-clip-border', { 'mask-clip': 'border-box' }],
143+
['mask-clip-padding', { 'mask-clip': 'padding-box' }],
144+
['mask-clip-content', { 'mask-clip': 'content-box' }],
145+
['mask-clip-fill', { 'mask-clip': 'fill-box' }],
146+
['mask-clip-stroke', { 'mask-clip': 'stroke-box' }],
147+
['mask-clip-view', { 'mask-clip': 'view-box' }],
148+
['mask-no-clip', { 'mask-clip': 'no-clip' }],
149+
150+
// mask-composite
151+
['mask-add', { 'mask-composite': 'add' }],
152+
['mask-subtract', { 'mask-composite': 'subtract' }],
153+
['mask-intersect', { 'mask-composite': 'intersect' }],
154+
['mask-exclude', { 'mask-composite': 'exclude' }],
155+
156+
// mask-image
157+
[/^mask-(.+)$/, ([,v]) => ({ 'mask-image': h.bracket.cssvar(v) })],
158+
[/^mask-(linear|radial|conic|[xytblr])-(from|to)()(?:-(.+))?$/, handleImage, {
159+
autocomplete: [
160+
'mask-(linear|radial|conic)-(from|to)-$colors',
161+
'mask-(linear|radial|conic)-(from|to)-<percentage>',
162+
'mask-(linear|radial|conic)-(from|to)',
163+
'mask-(linear|radial|conic)-<percent>',
164+
'mask-(x|y|t|b|l|r)-(from|to)-$colors',
165+
'mask-(x|y|t|b|l|r)-(from|to)-<percentage>',
166+
'mask-(x|y|t|b|l|r)-(from|to)',
167+
'mask-(x|y|t|b|l|r)-<percent>',
168+
],
169+
}],
170+
[/^mask-(linear|radial|conic)-(from|to)?-?(.*)$/, handleImage],
171+
[/^mask-([trblxy])-(from|to)-(.*)$/, handleImage],
172+
['mask-none', { 'mask-image': 'none' }],
173+
['mask-radial-circle', { '--un-mask-radial-shape': 'circle' }],
174+
['mask-radial-ellipse', { '--un-mask-radial-shape': 'ellipse' }],
175+
['mask-radial-closest-side', { '--un-mask-radial-size': 'closest-side' }],
176+
['mask-radial-closest-corner', { '--un-mask-radial-size': 'closest-corner' }],
177+
['mask-radial-farthest-side', { '--un-mask-radial-size': 'farthest-side' }],
178+
['mask-radial-farthest-corner', { '--un-mask-radial-size': 'farthest-corner' }],
179+
[/^mask-radial-at-([-\w]{3,})$/, ([, s]) => ({ '--un-mask-radial-position': positionMap[s] }), {
180+
autocomplete: [`mask-radial-at-(${Object.keys(positionMap).filter(p => p.length > 2).join('|')})`],
181+
}],
182+
183+
// mask-mode
184+
['mask-alpha', { 'mask-mode': 'alpha' }],
185+
['mask-luminance', { 'mask-mode': 'luminance' }],
186+
['mask-match', { 'mask-mode': 'match-source' }],
187+
188+
// mask-origin
189+
['mask-origin-border', { 'mask-origin': 'border-box' }],
190+
['mask-origin-padding', { 'mask-origin': 'padding-box' }],
191+
['mask-origin-content', { 'mask-origin': 'content-box' }],
192+
['mask-origin-fill', { 'mask-origin': 'fill-box' }],
193+
['mask-origin-stroke', { 'mask-origin': 'stroke-box' }],
194+
['mask-origin-view', { 'mask-origin': 'view-box' }],
195+
196+
// mask-position
197+
[/^mask-([rltb]{1,2})$/, handlePosition],
198+
[/^mask-([-\w]{3,})$/, ([, s]) => ({ 'mask-position': positionMap[s] })],
199+
[/^mask-(?:position-|pos-)(.+)$/, handlePosition],
200+
201+
// mask-repeat
202+
['mask-repeat', { 'mask-repeat': 'repeat' }],
203+
['mask-no-repeat', { 'mask-repeat': 'no-repeat' }],
204+
['mask-repeat-x', { 'mask-repeat': 'repeat-x' }],
205+
['mask-repeat-y', { 'mask-repeat': 'repeat-y' }],
206+
['mask-repeat-space', { 'mask-repeat': 'space' }],
207+
['mask-repeat-round', { 'mask-repeat': 'round' }],
208+
209+
// mask-size
210+
['mask-auto', { 'mask-size': 'auto' }],
211+
['mask-cover', { 'mask-size': 'cover' }],
212+
['mask-contain', { 'mask-size': 'contain' }],
213+
[/^mask-size-(.+)$/, handleSize],
214+
215+
// mask-type
216+
['mask-type-luminance', { 'mask-type': 'luminance' }],
217+
['mask-type-alpha', { 'mask-type': 'alpha' }],
218+
]

packages-presets/preset-wind4/src/variants/negative.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const anchoredNumberRE = /^-?[0-9.]+(?:[a-z]+|%)?$/
88
const numberRE = /-?[0-9.]+(?:[a-z]+|%)?/
99

1010
const ignoreProps = [
11-
/\b(opacity|color|flex|backdrop-filter|^filter|transform)\b/,
11+
/\b(opacity|color|flex|backdrop-filter|^filter|transform|mask-image)\b/,
1212
]
1313

1414
function negateMathFunction(value: string) {

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy