@leftium/logo 0.0.2 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AppLogo.svelte +137 -0
- package/dist/AppLogo.svelte.d.ts +4 -0
- package/dist/LeftiumLogo.svelte +267 -186
- package/dist/LeftiumLogo.svelte.d.ts +1 -0
- package/dist/app-logo/color-transform.d.ts +36 -0
- package/dist/app-logo/color-transform.js +136 -0
- package/dist/app-logo/defaults.d.ts +16 -0
- package/dist/app-logo/defaults.js +38 -0
- package/dist/app-logo/generate-favicon-set.d.ts +44 -0
- package/dist/app-logo/generate-favicon-set.js +97 -0
- package/dist/app-logo/generate-ico.d.ts +18 -0
- package/dist/app-logo/generate-ico.js +63 -0
- package/dist/app-logo/generate-png.d.ts +16 -0
- package/dist/app-logo/generate-png.js +60 -0
- package/dist/app-logo/generate-svg.d.ts +9 -0
- package/dist/app-logo/generate-svg.js +158 -0
- package/dist/app-logo/iconify.d.ts +21 -0
- package/dist/app-logo/iconify.js +134 -0
- package/dist/app-logo/squircle.d.ts +43 -0
- package/dist/app-logo/squircle.js +213 -0
- package/dist/app-logo/types.d.ts +37 -0
- package/dist/app-logo/types.js +1 -0
- package/dist/assets/logo-parts/glow-squircle.svg +44 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.js +9 -3
- package/dist/webgl-ripples/webgl-ripples.d.ts +0 -4
- package/dist/webgl-ripples/webgl-ripples.js +1 -1
- package/package.json +33 -20
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { IconSourceType } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolved icon data ready for rendering/export.
|
|
4
|
+
*/
|
|
5
|
+
export interface ResolvedIcon {
|
|
6
|
+
/** The raw SVG markup (without the outer <svg> wrapper for Iconify/data-url/svg sources) */
|
|
7
|
+
svgContent: string;
|
|
8
|
+
/** The viewBox of the original icon SVG */
|
|
9
|
+
viewBox: string;
|
|
10
|
+
/** Whether the icon uses currentColor (monochrome) vs hardcoded colors (multicolor) */
|
|
11
|
+
isMonochrome: boolean;
|
|
12
|
+
/** The detected source type */
|
|
13
|
+
sourceType: IconSourceType;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Resolve an icon prop value to SVG data ready for rendering.
|
|
17
|
+
*
|
|
18
|
+
* Handles all 4 source types: Iconify ID, emoji, inline SVG, data URL.
|
|
19
|
+
* For emoji, returns a special SVG text element instead of path data.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveIcon(icon: string): Promise<ResolvedIcon>;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { detectIconSource } from './defaults.js';
|
|
2
|
+
/** Cached Iconify SVG strings keyed by icon ID */
|
|
3
|
+
const iconCache = new Map();
|
|
4
|
+
/**
|
|
5
|
+
* Fetch an Iconify icon SVG by its ID (e.g. "mdi:rocket-launch").
|
|
6
|
+
* Results are cached in memory for the session.
|
|
7
|
+
*/
|
|
8
|
+
async function fetchIconifySvg(iconId) {
|
|
9
|
+
const cached = iconCache.get(iconId);
|
|
10
|
+
if (cached)
|
|
11
|
+
return cached;
|
|
12
|
+
const [prefix, name] = iconId.split(':');
|
|
13
|
+
if (!prefix || !name) {
|
|
14
|
+
throw new Error(`Invalid Iconify icon ID: "${iconId}". Expected format: "prefix:name"`);
|
|
15
|
+
}
|
|
16
|
+
const url = `https://api.iconify.design/${prefix}/${name}.svg`;
|
|
17
|
+
const response = await fetch(url);
|
|
18
|
+
if (!response.ok) {
|
|
19
|
+
throw new Error(`Failed to fetch icon "${iconId}": ${response.status} ${response.statusText}`);
|
|
20
|
+
}
|
|
21
|
+
const svg = await response.text();
|
|
22
|
+
iconCache.set(iconId, svg);
|
|
23
|
+
return svg;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Decode a data URL to an SVG string.
|
|
27
|
+
* Supports both base64 and plain-text encoded data URLs.
|
|
28
|
+
*/
|
|
29
|
+
function decodeDataUrl(dataUrl) {
|
|
30
|
+
// data:image/svg+xml;base64,PHN2Zy...
|
|
31
|
+
// data:image/svg+xml,%3Csvg...
|
|
32
|
+
// data:image/svg+xml;utf8,<svg...
|
|
33
|
+
const commaIndex = dataUrl.indexOf(',');
|
|
34
|
+
if (commaIndex === -1) {
|
|
35
|
+
throw new Error('Invalid data URL: no comma separator found');
|
|
36
|
+
}
|
|
37
|
+
const meta = dataUrl.substring(0, commaIndex);
|
|
38
|
+
const data = dataUrl.substring(commaIndex + 1);
|
|
39
|
+
if (meta.includes(';base64')) {
|
|
40
|
+
return atob(data);
|
|
41
|
+
}
|
|
42
|
+
// URL-encoded or UTF-8
|
|
43
|
+
return decodeURIComponent(data);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Parse viewBox from an SVG string. Falls back to "0 0 24 24" (Iconify default).
|
|
47
|
+
*/
|
|
48
|
+
function parseViewBox(svg) {
|
|
49
|
+
const match = svg.match(/viewBox=["']([^"']+)["']/);
|
|
50
|
+
return match?.[1] ?? '0 0 24 24';
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Extract the inner content of an SVG element (everything between <svg> and </svg>).
|
|
54
|
+
*/
|
|
55
|
+
function extractSvgContent(svg) {
|
|
56
|
+
// Remove the outer <svg ...> and </svg> tags
|
|
57
|
+
const openTagEnd = svg.indexOf('>');
|
|
58
|
+
const closeTagStart = svg.lastIndexOf('</svg>');
|
|
59
|
+
if (openTagEnd === -1 || closeTagStart === -1) {
|
|
60
|
+
return svg; // Return as-is if we can't parse
|
|
61
|
+
}
|
|
62
|
+
return svg.substring(openTagEnd + 1, closeTagStart).trim();
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Detect whether an SVG is monochrome (uses currentColor) or multicolor (hardcoded fills).
|
|
66
|
+
*
|
|
67
|
+
* Heuristic: if the SVG contains "currentColor" it's monochrome.
|
|
68
|
+
* If it contains hardcoded color values (hex, rgb, named colors) in fill/stroke attributes,
|
|
69
|
+
* it's multicolor.
|
|
70
|
+
*/
|
|
71
|
+
function detectMonochrome(svg) {
|
|
72
|
+
// If it explicitly uses currentColor, it's monochrome
|
|
73
|
+
if (svg.includes('currentColor'))
|
|
74
|
+
return true;
|
|
75
|
+
// Check for hardcoded fill/stroke colors (hex, rgb, named colors)
|
|
76
|
+
// If we find fill or stroke with a value that's not "none" or "currentColor", it's multicolor
|
|
77
|
+
const colorAttrPattern = /(?:fill|stroke)=["'](?!none|currentColor)([^"']+)["']/gi;
|
|
78
|
+
const hasHardcodedColors = colorAttrPattern.test(svg);
|
|
79
|
+
// Also check for fill/stroke in style attributes
|
|
80
|
+
const styleColorPattern = /(?:fill|stroke)\s*:\s*(?!none|currentColor)([^;"']+)/gi;
|
|
81
|
+
const hasStyleColors = styleColorPattern.test(svg);
|
|
82
|
+
if (hasHardcodedColors || hasStyleColors)
|
|
83
|
+
return false;
|
|
84
|
+
// No colors found at all -- treat as monochrome (will use currentColor behavior)
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Resolve an icon prop value to SVG data ready for rendering.
|
|
89
|
+
*
|
|
90
|
+
* Handles all 4 source types: Iconify ID, emoji, inline SVG, data URL.
|
|
91
|
+
* For emoji, returns a special SVG text element instead of path data.
|
|
92
|
+
*/
|
|
93
|
+
export async function resolveIcon(icon) {
|
|
94
|
+
const sourceType = detectIconSource(icon);
|
|
95
|
+
switch (sourceType) {
|
|
96
|
+
case 'iconify': {
|
|
97
|
+
const svg = await fetchIconifySvg(icon);
|
|
98
|
+
return {
|
|
99
|
+
svgContent: extractSvgContent(svg),
|
|
100
|
+
viewBox: parseViewBox(svg),
|
|
101
|
+
isMonochrome: detectMonochrome(svg),
|
|
102
|
+
sourceType
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
case 'data-url': {
|
|
106
|
+
const svg = decodeDataUrl(icon);
|
|
107
|
+
return {
|
|
108
|
+
svgContent: extractSvgContent(svg),
|
|
109
|
+
viewBox: parseViewBox(svg),
|
|
110
|
+
isMonochrome: detectMonochrome(svg),
|
|
111
|
+
sourceType
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
case 'svg': {
|
|
115
|
+
const svg = icon.trim();
|
|
116
|
+
return {
|
|
117
|
+
svgContent: extractSvgContent(svg),
|
|
118
|
+
viewBox: parseViewBox(svg),
|
|
119
|
+
isMonochrome: detectMonochrome(svg),
|
|
120
|
+
sourceType
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
case 'emoji': {
|
|
124
|
+
// For emoji, we create a text-based SVG element
|
|
125
|
+
// The viewBox is set to a standard size; the emoji is centered
|
|
126
|
+
return {
|
|
127
|
+
svgContent: `<text x="50%" y="50%" dominant-baseline="central" text-anchor="middle" font-size="80" font-family="'Apple Color Emoji','Segoe UI Emoji','Noto Color Emoji',sans-serif">${icon}</text>`,
|
|
128
|
+
viewBox: '0 0 100 100',
|
|
129
|
+
isMonochrome: false,
|
|
130
|
+
sourceType
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Superellipse / corner-shape path generation.
|
|
3
|
+
*
|
|
4
|
+
* Follows the CSS Borders Level 4 specification for corner-shape rendering.
|
|
5
|
+
* Maps CornerShape keywords to superellipse parameters and generates SVG paths.
|
|
6
|
+
*
|
|
7
|
+
* The CSS spec defines the curve parametrically:
|
|
8
|
+
* K = 2^abs(curvature)
|
|
9
|
+
* For T in [0, 1]: point = mapPointToCorner(T^K, (1-T)^K)
|
|
10
|
+
*
|
|
11
|
+
* @see https://drafts.csswg.org/css-borders-4/#corner-shape-rendering
|
|
12
|
+
*/
|
|
13
|
+
import type { CornerShape } from './types.js';
|
|
14
|
+
/**
|
|
15
|
+
* Map a CornerShape keyword or superellipse(n) to its numeric curvature parameter.
|
|
16
|
+
*
|
|
17
|
+
* CSS spec keyword -> superellipse parameter:
|
|
18
|
+
* round -> 1 (standard elliptical arc)
|
|
19
|
+
* squircle -> 2 (iOS-style continuous curvature)
|
|
20
|
+
* square -> Infinity (sharp 90 deg corner)
|
|
21
|
+
* bevel -> 0 (straight diagonal cut)
|
|
22
|
+
* scoop -> -1 (inward concave curve)
|
|
23
|
+
* notch -> -Infinity (inward right-angle cut)
|
|
24
|
+
*/
|
|
25
|
+
export declare function cornerShapeToK(shape: CornerShape): number;
|
|
26
|
+
/**
|
|
27
|
+
* Generate an SVG path `d` attribute for a rounded rectangle with a
|
|
28
|
+
* superellipse corner shape.
|
|
29
|
+
*
|
|
30
|
+
* The curve for each corner is sampled parametrically per the CSS Borders L4 spec:
|
|
31
|
+
* K = 2^abs(curvature)
|
|
32
|
+
* point(T) = mapPointToCorner(T^K, (1-T)^K) for T in [0, 1]
|
|
33
|
+
*
|
|
34
|
+
* For convex shapes (curvature > 0), the curve bulges outward.
|
|
35
|
+
* For concave shapes (curvature < 0), the center reference flips to the
|
|
36
|
+
* outer corner, creating inward "scoop" curves.
|
|
37
|
+
*
|
|
38
|
+
* @param size - Square side length in pixels
|
|
39
|
+
* @param cornerRadius - Corner radius as percentage (0-50)
|
|
40
|
+
* @param cornerShape - CornerShape keyword or superellipse(n)
|
|
41
|
+
* @returns SVG path `d` string
|
|
42
|
+
*/
|
|
43
|
+
export declare function generateCornerPath(size: number, cornerRadius: number, cornerShape: CornerShape): string;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Superellipse / corner-shape path generation.
|
|
3
|
+
*
|
|
4
|
+
* Follows the CSS Borders Level 4 specification for corner-shape rendering.
|
|
5
|
+
* Maps CornerShape keywords to superellipse parameters and generates SVG paths.
|
|
6
|
+
*
|
|
7
|
+
* The CSS spec defines the curve parametrically:
|
|
8
|
+
* K = 2^abs(curvature)
|
|
9
|
+
* For T in [0, 1]: point = mapPointToCorner(T^K, (1-T)^K)
|
|
10
|
+
*
|
|
11
|
+
* @see https://drafts.csswg.org/css-borders-4/#corner-shape-rendering
|
|
12
|
+
*/
|
|
13
|
+
/** Number of line segments to sample per corner arc. */
|
|
14
|
+
const SEGMENTS = 64;
|
|
15
|
+
/**
|
|
16
|
+
* Map a CornerShape keyword or superellipse(n) to its numeric curvature parameter.
|
|
17
|
+
*
|
|
18
|
+
* CSS spec keyword -> superellipse parameter:
|
|
19
|
+
* round -> 1 (standard elliptical arc)
|
|
20
|
+
* squircle -> 2 (iOS-style continuous curvature)
|
|
21
|
+
* square -> Infinity (sharp 90 deg corner)
|
|
22
|
+
* bevel -> 0 (straight diagonal cut)
|
|
23
|
+
* scoop -> -1 (inward concave curve)
|
|
24
|
+
* notch -> -Infinity (inward right-angle cut)
|
|
25
|
+
*/
|
|
26
|
+
export function cornerShapeToK(shape) {
|
|
27
|
+
switch (shape) {
|
|
28
|
+
case 'round':
|
|
29
|
+
return 1;
|
|
30
|
+
case 'squircle':
|
|
31
|
+
return 2;
|
|
32
|
+
case 'square':
|
|
33
|
+
return Infinity;
|
|
34
|
+
case 'bevel':
|
|
35
|
+
return 0;
|
|
36
|
+
case 'scoop':
|
|
37
|
+
return -1;
|
|
38
|
+
case 'notch':
|
|
39
|
+
return -Infinity;
|
|
40
|
+
default: {
|
|
41
|
+
// superellipse(n) -- parse the numeric value
|
|
42
|
+
const match = shape.match(/^superellipse\((.+)\)$/);
|
|
43
|
+
if (match) {
|
|
44
|
+
const val = match[1].trim();
|
|
45
|
+
if (val === 'infinity')
|
|
46
|
+
return Infinity;
|
|
47
|
+
if (val === '-infinity')
|
|
48
|
+
return -Infinity;
|
|
49
|
+
const n = Number(val);
|
|
50
|
+
if (!isNaN(n))
|
|
51
|
+
return n;
|
|
52
|
+
}
|
|
53
|
+
return 1; // fallback to round
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Generate an SVG path `d` attribute for a rounded rectangle with a
|
|
59
|
+
* superellipse corner shape.
|
|
60
|
+
*
|
|
61
|
+
* The curve for each corner is sampled parametrically per the CSS Borders L4 spec:
|
|
62
|
+
* K = 2^abs(curvature)
|
|
63
|
+
* point(T) = mapPointToCorner(T^K, (1-T)^K) for T in [0, 1]
|
|
64
|
+
*
|
|
65
|
+
* For convex shapes (curvature > 0), the curve bulges outward.
|
|
66
|
+
* For concave shapes (curvature < 0), the center reference flips to the
|
|
67
|
+
* outer corner, creating inward "scoop" curves.
|
|
68
|
+
*
|
|
69
|
+
* @param size - Square side length in pixels
|
|
70
|
+
* @param cornerRadius - Corner radius as percentage (0-50)
|
|
71
|
+
* @param cornerShape - CornerShape keyword or superellipse(n)
|
|
72
|
+
* @returns SVG path `d` string
|
|
73
|
+
*/
|
|
74
|
+
export function generateCornerPath(size, cornerRadius, cornerShape) {
|
|
75
|
+
// No rounding needed
|
|
76
|
+
if (cornerRadius <= 0) {
|
|
77
|
+
return `M0,0 H${size} V${size} H0 Z`;
|
|
78
|
+
}
|
|
79
|
+
const curvature = cornerShapeToK(cornerShape);
|
|
80
|
+
// square (Infinity): sharp corners, radius is ignored
|
|
81
|
+
if (curvature === Infinity) {
|
|
82
|
+
return `M0,0 H${size} V${size} H0 Z`;
|
|
83
|
+
}
|
|
84
|
+
// Convert percentage to pixels, clamped to half the side
|
|
85
|
+
const r = Math.min((cornerRadius / 100) * size, size / 2);
|
|
86
|
+
// notch (-Infinity): inward right-angle cut at each corner.
|
|
87
|
+
// Goes from tangent point on one edge INWARD to the corner-region center,
|
|
88
|
+
// then back out to the tangent point on the other edge (concave square corner).
|
|
89
|
+
if (curvature === -Infinity) {
|
|
90
|
+
return [
|
|
91
|
+
`M${rd(r)},0`,
|
|
92
|
+
`H${rd(size - r)}`,
|
|
93
|
+
// top-right: inward to center of corner region, then out
|
|
94
|
+
`L${rd(size - r)},${rd(r)}`,
|
|
95
|
+
`L${rd(size)},${rd(r)}`,
|
|
96
|
+
`V${rd(size - r)}`,
|
|
97
|
+
// bottom-right: inward to center, then out
|
|
98
|
+
`L${rd(size - r)},${rd(size - r)}`,
|
|
99
|
+
`L${rd(size - r)},${rd(size)}`,
|
|
100
|
+
`H${rd(r)}`,
|
|
101
|
+
// bottom-left: inward to center, then out
|
|
102
|
+
`L${rd(r)},${rd(size - r)}`,
|
|
103
|
+
`L0,${rd(size - r)}`,
|
|
104
|
+
`V${rd(r)}`,
|
|
105
|
+
// top-left: inward to center, then out
|
|
106
|
+
`L${rd(r)},${rd(r)}`,
|
|
107
|
+
`L${rd(r)},0`,
|
|
108
|
+
`Z`
|
|
109
|
+
].join(' ');
|
|
110
|
+
}
|
|
111
|
+
// bevel (0): straight diagonal cut from tangent to tangent
|
|
112
|
+
if (curvature === 0) {
|
|
113
|
+
return [
|
|
114
|
+
`M${rd(r)},0`,
|
|
115
|
+
`H${rd(size - r)}`,
|
|
116
|
+
`L${rd(size)},${rd(r)}`,
|
|
117
|
+
`V${rd(size - r)}`,
|
|
118
|
+
`L${rd(size - r)},${rd(size)}`,
|
|
119
|
+
`H${rd(r)}`,
|
|
120
|
+
`L0,${rd(size - r)}`,
|
|
121
|
+
`V${rd(r)}`,
|
|
122
|
+
`Z`
|
|
123
|
+
].join(' ');
|
|
124
|
+
}
|
|
125
|
+
// round (1): use SVG arc commands for a perfect quarter-ellipse.
|
|
126
|
+
// This matches what browsers render for border-radius and is exact,
|
|
127
|
+
// unlike the parametric approximation.
|
|
128
|
+
if (curvature === 1) {
|
|
129
|
+
return [
|
|
130
|
+
`M${rd(r)},0`,
|
|
131
|
+
`H${rd(size - r)}`,
|
|
132
|
+
`A${rd(r)},${rd(r)} 0 0 1 ${rd(size)},${rd(r)}`,
|
|
133
|
+
`V${rd(size - r)}`,
|
|
134
|
+
`A${rd(r)},${rd(r)} 0 0 1 ${rd(size - r)},${rd(size)}`,
|
|
135
|
+
`H${rd(r)}`,
|
|
136
|
+
`A${rd(r)},${rd(r)} 0 0 1 0,${rd(size - r)}`,
|
|
137
|
+
`V${rd(r)}`,
|
|
138
|
+
`A${rd(r)},${rd(r)} 0 0 1 ${rd(r)},0`,
|
|
139
|
+
`Z`
|
|
140
|
+
].join(' ');
|
|
141
|
+
}
|
|
142
|
+
// Parametric superellipse curve.
|
|
143
|
+
// K = 2^abs(curvature) per the CSS spec: "Let K be 0.5^(-abs(curvature))"
|
|
144
|
+
const K = Math.pow(2, Math.abs(curvature));
|
|
145
|
+
// Invert: for the outline path (not clip-out), convex shapes use outer corner as ref
|
|
146
|
+
const useOuterAsRef = curvature > 0;
|
|
147
|
+
// Sample the corner arc and return SVG line-to commands.
|
|
148
|
+
//
|
|
149
|
+
// The CSS spec generates a "clip-out" path (what to REMOVE from the rect).
|
|
150
|
+
// Since we generate the OUTLINE path (what to KEEP), we invert the reference:
|
|
151
|
+
// - Convex (squircle, round): curveCenter = outerCorner (curve bows outward)
|
|
152
|
+
// - Concave (scoop): curveCenter = cornerCenter (curve bows inward)
|
|
153
|
+
//
|
|
154
|
+
// The parametric curve maps (T^K, (1-T)^K) through mapPointToCorner,
|
|
155
|
+
// which scales by vectors from curveCenter to the two tangent points.
|
|
156
|
+
const parts = [`M${rd(size - r)},0`];
|
|
157
|
+
// Top-right corner: start=(size-r, 0), end=(size, r), outer=(size, 0), center=(size-r, r)
|
|
158
|
+
parts.push(sampleCorner(size - r, 0, size, r, size, 0, size - r, r, K, useOuterAsRef));
|
|
159
|
+
// Right edge
|
|
160
|
+
parts.push(`L${rd(size)},${rd(size - r)}`);
|
|
161
|
+
// Bottom-right corner: start=(size, size-r), end=(size-r, size), outer=(size, size), center=(size-r, size-r)
|
|
162
|
+
parts.push(sampleCorner(size, size - r, size - r, size, size, size, size - r, size - r, K, useOuterAsRef));
|
|
163
|
+
// Bottom edge
|
|
164
|
+
parts.push(`L${rd(r)},${rd(size)}`);
|
|
165
|
+
// Bottom-left corner: start=(r, size), end=(0, size-r), outer=(0, size), center=(r, size-r)
|
|
166
|
+
parts.push(sampleCorner(r, size, 0, size - r, 0, size, r, size - r, K, useOuterAsRef));
|
|
167
|
+
// Left edge
|
|
168
|
+
parts.push(`L0,${rd(r)}`);
|
|
169
|
+
// Top-left corner: start=(0, r), end=(r, 0), outer=(0, 0), center=(r, r)
|
|
170
|
+
parts.push(sampleCorner(0, r, r, 0, 0, 0, r, r, K, useOuterAsRef));
|
|
171
|
+
parts.push('Z');
|
|
172
|
+
return parts.join(' ');
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Sample a superellipse corner arc as SVG line-to commands.
|
|
176
|
+
*
|
|
177
|
+
* @param sx, sy - Start point (tangent point on the "start" edge)
|
|
178
|
+
* @param ex, ey - End point (tangent point on the "end" edge)
|
|
179
|
+
* @param ox, oy - Outer corner point (the actual rectangle corner)
|
|
180
|
+
* @param cx, cy - Center point (diagonally opposite the outer corner within the corner region)
|
|
181
|
+
* @param K - Exponent: 2^abs(curvature)
|
|
182
|
+
* @param useOuterAsRef - Whether to use outer corner as the curve reference point
|
|
183
|
+
*/
|
|
184
|
+
function sampleCorner(sx, sy, ex, ey, ox, oy, cx, cy, K, useOuterAsRef) {
|
|
185
|
+
// For convex shapes (outline path), use outer corner so curve bows outward.
|
|
186
|
+
// For concave shapes (outline path), use inner center so curve bows inward.
|
|
187
|
+
const refX = useOuterAsRef ? ox : cx;
|
|
188
|
+
const refY = useOuterAsRef ? oy : cy;
|
|
189
|
+
// Vectors from curveCenter to the tangent points
|
|
190
|
+
const toEndX = ex - refX;
|
|
191
|
+
const toEndY = ey - refY;
|
|
192
|
+
const toStartX = sx - refX;
|
|
193
|
+
const toStartY = sy - refY;
|
|
194
|
+
const cmds = [];
|
|
195
|
+
// Sample T from 0 to 1 (exclusive of 0 since we're already at start)
|
|
196
|
+
for (let i = 1; i <= SEGMENTS; i++) {
|
|
197
|
+
const t = i / SEGMENTS;
|
|
198
|
+
const xParam = Math.pow(t, K);
|
|
199
|
+
const yParam = Math.pow(1 - t, K);
|
|
200
|
+
// mapPointToCorner(xParam, yParam):
|
|
201
|
+
// refPoint + toEnd * xParam + toStart * yParam
|
|
202
|
+
const px = refX + toEndX * xParam + toStartX * yParam;
|
|
203
|
+
const py = refY + toEndY * xParam + toStartY * yParam;
|
|
204
|
+
cmds.push(`L${rd(px)},${rd(py)}`);
|
|
205
|
+
}
|
|
206
|
+
return cmds.join(' ');
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Round a number to 2 decimal places for clean SVG output.
|
|
210
|
+
*/
|
|
211
|
+
function rd(n) {
|
|
212
|
+
return Math.round(n * 100) / 100;
|
|
213
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type IconColorMode = 'auto' | 'original' | 'monochrome' | 'grayscale' | 'grayscale-tint' | {
|
|
2
|
+
hue: number;
|
|
3
|
+
saturation?: number;
|
|
4
|
+
};
|
|
5
|
+
export type CornerShape = 'round' | 'squircle' | 'square' | 'bevel' | 'scoop' | 'notch' | `superellipse(${number})`;
|
|
6
|
+
export interface GradientConfig {
|
|
7
|
+
colors: string[];
|
|
8
|
+
stops?: number[];
|
|
9
|
+
angle?: number;
|
|
10
|
+
position?: number;
|
|
11
|
+
scale?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface AppLogoProps {
|
|
14
|
+
icon?: string;
|
|
15
|
+
iconColor?: string;
|
|
16
|
+
iconColorMode?: IconColorMode;
|
|
17
|
+
iconSize?: number;
|
|
18
|
+
iconOffsetX?: number;
|
|
19
|
+
iconOffsetY?: number;
|
|
20
|
+
iconRotation?: number;
|
|
21
|
+
cornerRadius?: number;
|
|
22
|
+
cornerShape?: CornerShape;
|
|
23
|
+
background?: string | GradientConfig;
|
|
24
|
+
size?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface AppLogoConfig {
|
|
27
|
+
icon: string;
|
|
28
|
+
iconColor?: string;
|
|
29
|
+
iconColorMode?: IconColorMode;
|
|
30
|
+
background?: AppLogoProps['background'];
|
|
31
|
+
cornerRadius?: number;
|
|
32
|
+
cornerShape?: CornerShape;
|
|
33
|
+
logo?: Partial<AppLogoProps>;
|
|
34
|
+
favicon?: Partial<AppLogoProps>;
|
|
35
|
+
}
|
|
36
|
+
/** Detected source type of the icon prop value */
|
|
37
|
+
export type IconSourceType = 'iconify' | 'emoji' | 'svg' | 'data-url';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
2
|
+
<!-- Squircle variant of glow (50% radius, K=2 superellipse) -->
|
|
3
|
+
|
|
4
|
+
<svg
|
|
5
|
+
width="1375.7469"
|
|
6
|
+
height="1375.7469"
|
|
7
|
+
viewBox="0 0 1375.7469 1375.7469"
|
|
8
|
+
version="1.1"
|
|
9
|
+
id="svg1"
|
|
10
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
11
|
+
xmlns:svg="http://www.w3.org/2000/svg">
|
|
12
|
+
<defs
|
|
13
|
+
id="defs1">
|
|
14
|
+
<filter
|
|
15
|
+
style="color-interpolation-filters:sRGB"
|
|
16
|
+
id="filter3"
|
|
17
|
+
x="-0.10799999"
|
|
18
|
+
y="-0.108"
|
|
19
|
+
width="1.216"
|
|
20
|
+
height="1.216">
|
|
21
|
+
<feGaussianBlur
|
|
22
|
+
stdDeviation="50.911691"
|
|
23
|
+
id="feGaussianBlur3" />
|
|
24
|
+
</filter>
|
|
25
|
+
</defs>
|
|
26
|
+
<g
|
|
27
|
+
id="layer1"
|
|
28
|
+
transform="translate(-122.54569,-162.64743)">
|
|
29
|
+
<g
|
|
30
|
+
id="g1"
|
|
31
|
+
transform="translate(253.40376,284.58137)"
|
|
32
|
+
style="display:inline">
|
|
33
|
+
<g
|
|
34
|
+
id="g8"
|
|
35
|
+
style="display:inline">
|
|
36
|
+
<path
|
|
37
|
+
style="opacity:0.7;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter3)"
|
|
38
|
+
id="path2"
|
|
39
|
+
transform="translate(-8.6700182, 0.25409326)"
|
|
40
|
+
d="M565.69,0 L600.22,0 L633.15,0 L664.52,0 L694.39,0.01 L722.8,0.02 L749.81,0.04 L775.45,0.08 L799.78,0.14 L822.83,0.22 L844.67,0.34 L865.32,0.49 L884.84,0.7 L903.27,0.96 L920.64,1.3 L937,1.71 L952.38,2.21 L966.84,2.82 L980.4,3.54 L993.11,4.39 L1004.99,5.39 L1016.1,6.56 L1026.45,7.9 L1036.09,9.44 L1045.05,11.19 L1053.37,13.17 L1061.07,15.41 L1068.18,17.92 L1074.74,20.72 L1080.77,23.85 L1086.31,27.31 L1091.38,31.14 L1096.02,35.36 L1100.23,39.99 L1104.06,45.06 L1107.52,50.6 L1110.65,56.63 L1113.45,63.19 L1115.96,70.31 L1118.2,78 L1120.18,86.32 L1121.94,95.28 L1123.47,104.92 L1124.81,115.27 L1125.98,126.38 L1126.98,138.26 L1127.83,150.97 L1128.55,164.53 L1129.16,178.99 L1129.66,194.37 L1130.08,210.73 L1130.41,228.11 L1130.67,246.53 L1130.88,266.05 L1131.03,286.7 L1131.15,308.54 L1131.23,331.59 L1131.29,355.92 L1131.33,381.56 L1131.35,408.57 L1131.36,436.98 L1131.37,466.85 L1131.37,498.22 L1131.37,531.15 L1131.37,565.69 L1131.37,565.69 L1131.37,600.22 L1131.37,633.15 L1131.37,664.52 L1131.36,694.39 L1131.35,722.8 L1131.33,749.81 L1131.29,775.45 L1131.23,799.78 L1131.15,822.83 L1131.03,844.67 L1130.88,865.32 L1130.67,884.84 L1130.41,903.27 L1130.08,920.64 L1129.66,937 L1129.16,952.38 L1128.55,966.84 L1127.83,980.4 L1126.98,993.11 L1125.98,1004.99 L1124.81,1016.1 L1123.47,1026.45 L1121.94,1036.09 L1120.18,1045.05 L1118.2,1053.37 L1115.96,1061.07 L1113.45,1068.18 L1110.65,1074.74 L1107.52,1080.77 L1104.06,1086.31 L1100.23,1091.38 L1096.02,1096.02 L1091.38,1100.23 L1086.31,1104.06 L1080.77,1107.52 L1074.74,1110.65 L1068.18,1113.45 L1061.07,1115.96 L1053.37,1118.2 L1045.05,1120.18 L1036.09,1121.94 L1026.45,1123.47 L1016.1,1124.81 L1004.99,1125.98 L993.11,1126.98 L980.4,1127.83 L966.84,1128.55 L952.38,1129.16 L937,1129.66 L920.64,1130.08 L903.27,1130.41 L884.84,1130.67 L865.32,1130.88 L844.67,1131.03 L822.83,1131.15 L799.78,1131.23 L775.45,1131.29 L749.81,1131.33 L722.8,1131.35 L694.39,1131.36 L664.52,1131.37 L633.15,1131.37 L600.22,1131.37 L565.69,1131.37 L565.69,1131.37 L531.15,1131.37 L498.22,1131.37 L466.85,1131.37 L436.98,1131.36 L408.57,1131.35 L381.56,1131.33 L355.92,1131.29 L331.59,1131.23 L308.54,1131.15 L286.7,1131.03 L266.05,1130.88 L246.53,1130.67 L228.11,1130.41 L210.73,1130.08 L194.37,1129.66 L178.99,1129.16 L164.53,1128.55 L150.97,1127.83 L138.26,1126.98 L126.38,1125.98 L115.27,1124.81 L104.92,1123.47 L95.28,1121.94 L86.32,1120.18 L78,1118.2 L70.31,1115.96 L63.19,1113.45 L56.63,1110.65 L50.6,1107.52 L45.06,1104.06 L39.99,1100.23 L35.36,1096.02 L31.14,1091.38 L27.31,1086.31 L23.85,1080.77 L20.72,1074.74 L17.92,1068.18 L15.41,1061.07 L13.17,1053.37 L11.19,1045.05 L9.44,1036.09 L7.9,1026.45 L6.56,1016.1 L5.39,1004.99 L4.39,993.11 L3.54,980.4 L2.82,966.84 L2.21,952.38 L1.71,937 L1.3,920.64 L0.96,903.27 L0.7,884.84 L0.49,865.32 L0.34,844.67 L0.22,822.83 L0.14,799.78 L0.08,775.45 L0.04,749.81 L0.02,722.8 L0.01,694.39 L0,664.52 L0,633.15 L0,600.22 L0,565.69 L0,565.69 L0,531.15 L0,498.22 L0,466.85 L0.01,436.98 L0.02,408.57 L0.04,381.56 L0.08,355.92 L0.14,331.59 L0.22,308.54 L0.34,286.7 L0.49,266.05 L0.7,246.53 L0.96,228.11 L1.3,210.73 L1.71,194.37 L2.21,178.99 L2.82,164.53 L3.54,150.97 L4.39,138.26 L5.39,126.38 L6.56,115.27 L7.9,104.92 L9.44,95.28 L11.19,86.32 L13.17,78 L15.41,70.31 L17.92,63.19 L20.72,56.63 L23.85,50.6 L27.31,45.06 L31.14,39.99 L35.36,35.36 L39.99,31.14 L45.06,27.31 L50.6,23.85 L56.63,20.72 L63.19,17.92 L70.31,15.41 L78,13.17 L86.32,11.19 L95.28,9.44 L104.92,7.9 L115.27,6.56 L126.38,5.39 L138.26,4.39 L150.97,3.54 L164.53,2.82 L178.99,2.21 L194.37,1.71 L210.73,1.3 L228.11,0.96 L246.53,0.7 L266.05,0.49 L286.7,0.34 L308.54,0.22 L331.59,0.14 L355.92,0.08 L381.56,0.04 L408.57,0.02 L436.98,0.01 L466.85,0 L498.22,0 L531.15,0 L565.69,0 Z" />
|
|
41
|
+
</g>
|
|
42
|
+
</g>
|
|
43
|
+
</g>
|
|
44
|
+
</svg>
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import LeftiumLogo from './LeftiumLogo.svelte';
|
|
2
|
-
import toggleAnimation from './LeftiumLogo.svelte';
|
|
3
|
-
import setAnimated from './LeftiumLogo.svelte';
|
|
1
|
+
import LeftiumLogo, { toggleAnimation, setAnimated } from './LeftiumLogo.svelte';
|
|
4
2
|
import favicon from './assets/favicon.svg';
|
|
5
3
|
export { LeftiumLogo, toggleAnimation, setAnimated, favicon };
|
|
4
|
+
export { default as AppLogo } from './AppLogo.svelte';
|
|
5
|
+
export type { AppLogoProps, AppLogoConfig, GradientConfig, IconColorMode, CornerShape } from './app-logo/types.js';
|
|
6
|
+
export { generateAppLogoSvg } from './app-logo/generate-svg.js';
|
|
7
|
+
export { generateAppLogoPng } from './app-logo/generate-png.js';
|
|
8
|
+
export { LEFTIUM_GRADIENT } from './app-logo/defaults.js';
|
|
9
|
+
export { generateCornerPath, cornerShapeToK } from './app-logo/squircle.js';
|
|
10
|
+
export { applyColorMode } from './app-logo/color-transform.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
// Reexport your entry components here
|
|
2
|
-
import LeftiumLogo from './LeftiumLogo.svelte';
|
|
3
|
-
import toggleAnimation from './LeftiumLogo.svelte';
|
|
4
|
-
import setAnimated from './LeftiumLogo.svelte';
|
|
2
|
+
import LeftiumLogo, { toggleAnimation, setAnimated } from './LeftiumLogo.svelte';
|
|
5
3
|
import favicon from './assets/favicon.svg';
|
|
6
4
|
export { LeftiumLogo, toggleAnimation, setAnimated, favicon };
|
|
5
|
+
// Phase 1: AppLogo component and generation utilities
|
|
6
|
+
export { default as AppLogo } from './AppLogo.svelte';
|
|
7
|
+
export { generateAppLogoSvg } from './app-logo/generate-svg.js';
|
|
8
|
+
export { generateAppLogoPng } from './app-logo/generate-png.js';
|
|
9
|
+
export { LEFTIUM_GRADIENT } from './app-logo/defaults.js';
|
|
10
|
+
// Phase 2: Advanced styling utilities
|
|
11
|
+
export { generateCornerPath, cornerShapeToK } from './app-logo/squircle.js';
|
|
12
|
+
export { applyColorMode } from './app-logo/color-transform.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const isBrowser="undefined"!=typeof window&&"undefined"!=typeof document;let gl=null,config=null,transparentPixels=null;function isPercentage(e){return"%"===e[e.length-1]}function loadConfig(){if(!isBrowser)return null;const e=document.createElement("canvas");if(gl=e.getContext("webgl")||e.getContext("experimental-webgl"),!gl)return null;const t={};if(["OES_texture_float","OES_texture_half_float","OES_texture_float_linear","OES_texture_half_float_linear"].forEach(e=>{const i=gl.getExtension(e);i&&(t[e]=i)}),!t.OES_texture_float)return null;const i=[];function r(e,i,r){const n="OES_texture_"+e,s=n+"_linear",o=s in t,a=[n];return o&&a.push(s),{type:i,arrayType:r,linearSupport:o,extensions:a}}i.push(r("float",gl.FLOAT,Float32Array)),t.OES_texture_half_float&&i.push(r("half_float",t.OES_texture_half_float.HALF_FLOAT_OES,null));const n=gl.createTexture(),s=gl.createFramebuffer();gl.bindFramebuffer(gl.FRAMEBUFFER,s),gl.bindTexture(gl.TEXTURE_2D,n),gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.NEAREST),gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.NEAREST),gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_S,gl.CLAMP_TO_EDGE),gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_T,gl.CLAMP_TO_EDGE);let o=null;for(let e=0;e<i.length;e++)if(gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,32,32,0,gl.RGBA,i[e].type,null),gl.framebufferTexture2D(gl.FRAMEBUFFER,gl.COLOR_ATTACHMENT0,gl.TEXTURE_2D,n,0),gl.checkFramebufferStatus(gl.FRAMEBUFFER)===gl.FRAMEBUFFER_COMPLETE){o=i[e];break}return o}function createImageData(e,t){if(!isBrowser)return null;try{return new ImageData(e,t)}catch{return document.createElement("canvas").getContext("2d").createImageData(e,t)}}function translateBackgroundPosition(e){const t=e.split(" ");if(1!==t.length)return t.map(t=>{switch(e){case"center":return"50%";case"top":case"left":return"0";case"right":case"bottom":return"100%";default:return t}});switch(e){case"center":return["50%","50%"];case"top":return["50%","0"];case"bottom":return["50%","100%"];case"left":return["0","50%"];case"right":return["100%","50%"];default:return[e,"50%"]}}function createProgram(e,t){function i(e,t){const i=gl.createShader(e);if(gl.shaderSource(i,t),gl.compileShader(i),!gl.getShaderParameter(i,gl.COMPILE_STATUS))throw new Error("compile error: "+gl.getShaderInfoLog(i));return i}const r={};if(r.id=gl.createProgram(),gl.attachShader(r.id,i(gl.VERTEX_SHADER,e)),gl.attachShader(r.id,i(gl.FRAGMENT_SHADER,t)),gl.linkProgram(r.id),!gl.getProgramParameter(r.id,gl.LINK_STATUS))throw new Error("link error: "+gl.getProgramInfoLog(r.id));r.uniforms={},r.locations={},gl.useProgram(r.id),gl.enableVertexAttribArray(0);let n,s,o=/uniform (\w+) (\w+)/g,a=e+t;for(;null!=(n=o.exec(a));)s=n[2],r.locations[s]=gl.getUniformLocation(r.id,s);return r}function bindTexture(e,t){gl.activeTexture(gl.TEXTURE0+(t||0)),gl.bindTexture(gl.TEXTURE_2D,e)}function extractUrl(e){const t=/url\(["']?([^"']*)["']?\)/.exec(e);return null==t?null:t[1]}function isDataUri(e){return e.match(/^data:/)}isBrowser&&(config=loadConfig(),transparentPixels=createImageData(32,32));const DEFAULTS={imageUrl:null,dropRadius:15,perturbance:.04,resolution:512,interactive:!0,crossOrigin:"",wavePropagation:2,dampening:.997};class Ripples{constructor(e,t){if(!isBrowser)return console.warn("Ripples: Cannot initialize in non-browser environment"),void(this.destroyed=!0);if(this.el="string"==typeof e?document.querySelector(e):e,!this.el)return void console.error("Ripples: Element not found.",e);this.options={...DEFAULTS,...t},this.interactive=this.options.interactive,this.resolution=this.options.resolution,this.textureDelta=new Float32Array([1/this.resolution,1/this.resolution]),this.perturbance=this.options.perturbance,this.dropRadius=this.options.dropRadius,this.crossOrigin=this.options.crossOrigin,this.imageUrl=this.options.imageUrl;const i=this.el.clientWidth,r=this.el.clientHeight;if(0===i||0===r)return console.warn("Ripples: Element has zero dimensions. Deferring initialization."),this.pendingInitialization=!0,this.destroyed=!1,this.visible=!1,this.running=!1,this.inited=!1,void this.observeVisibility();const n=document.createElement("canvas");if(n.width=i,n.height=r,this.canvas=n,this.canvas.style.position="absolute",this.canvas.style.inset=0,0===n.width||0===n.height)return console.warn("Ripples: Canvas has zero dimensions. Deferring initialization."),this.pendingInitialization=!0,this.destroyed=!1,this.visible=!1,this.running=!1,this.inited=!1,void this.observeVisibility();if(this.el.append(n),this.context=gl=n.getContext("webgl")||n.getContext("experimental-webgl"),!this.context)return console.error("Ripples: WebGL is not supported by your browser or is disabled."),void(this.destroyed=!0);config&&config.extensions&&config.extensions.forEach(e=>{gl.getExtension(e)}),this.abortController=new AbortController;const s=this.signal=this.abortController.signal;this.updateSize=this.updateSize.bind(this),window.addEventListener("resize",this.updateSize,{signal:s}),this.textures=[],this.framebuffers=[],this.bufferWriteIndex=0,this.bufferReadIndex=1;const o=config?config.arrayType:null,a=o?new o(this.resolution*this.resolution*4):null;for(let e=0;e<2;e++){const e=gl.createTexture(),t=gl.createFramebuffer();gl.bindFramebuffer(gl.FRAMEBUFFER,t),gl.bindTexture(gl.TEXTURE_2D,e),gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,config&&config.linearSupport?gl.LINEAR:gl.NEAREST),gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,config&&config.linearSupport?gl.LINEAR:gl.NEAREST),gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_S,gl.CLAMP_TO_EDGE),gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_T,gl.CLAMP_TO_EDGE),gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,this.resolution,this.resolution,0,gl.RGBA,config&&config.type||gl.UNSIGNED_BYTE,a),gl.framebufferTexture2D(gl.FRAMEBUFFER,gl.COLOR_ATTACHMENT0,gl.TEXTURE_2D,e,0),this.textures.push(e),this.framebuffers.push(t)}this.quad=gl.createBuffer(),gl.bindBuffer(gl.ARRAY_BUFFER,this.quad),gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,1,1,-1,1]),gl.STATIC_DRAW),this.initShaders(),this.initTexture(),this.setTransparentTexture(),this.originalCssBackgroundImage=getComputedStyle(this.el).backgroundImage,this.originalInlineCss=this.el.style.backgroundImage,this.loadImage(),gl.clearColor(0,0,0,0),gl.blendFunc(gl.SRC_ALPHA,gl.ONE_MINUS_SRC_ALPHA),this.visible=!0,this.running=!0,this.inited=!0,this.destroyed=!1,this.setupPointerEvents(),this.animationId=null;const l=()=>{this.destroyed||(this.step(),this.running||this.needsRender?this.animationId=requestAnimationFrame(l):this.animationId=null)};return this.animationId=requestAnimationFrame(l),this}observeVisibility(){isBrowser&&this.el&&("undefined"!=typeof ResizeObserver&&(this.resizeObserver=new ResizeObserver(()=>{if(this.pendingInitialization){const e=this.el.clientWidth,t=this.el.clientHeight;e>0&&t>0&&this.completeInitialization()}else this.inited&&this.canvas&&this.updateSize()}),this.resizeObserver.observe(this.el)),"undefined"!=typeof IntersectionObserver&&(this.intersectionObserver=new IntersectionObserver(e=>{e.forEach(e=>{if(e.isIntersecting&&this.pendingInitialization){const e=this.el.clientWidth,t=this.el.clientHeight;e>0&&t>0&&this.completeInitialization()}})}),this.intersectionObserver.observe(this.el)),this.resizeObserver||this.intersectionObserver||(this.visibilityCheckInterval=setInterval(()=>{if(this.pendingInitialization){const e=this.el.clientWidth,t=this.el.clientHeight;e>0&&t>0&&this.completeInitialization()}},500)))}completeInitialization(){if(!this.pendingInitialization||this.inited)return;console.log("Ripples: Element now visible, completing initialization."),this.pendingInitialization=!1,this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),this.intersectionObserver&&(this.intersectionObserver.disconnect(),this.intersectionObserver=null),this.visibilityCheckInterval&&(clearInterval(this.visibilityCheckInterval),this.visibilityCheckInterval=null);const e=this.options,t=this.el;Object.assign(this,new Ripples(t,e))}setupPointerEvents(){const e=()=>{const e=Math.max(this.el.clientWidth,this.el.clientHeight),t=Math.max(1,150/e);return Math.min(.04,.01*t)},t=t=>{if(this.visible&&this.running&&this.interactive){const i=t.changedTouches,r=e();for(let e=0;e<i.length;e++)this.dropAtPointer(i[e],1*this.dropRadius,r)}},i=this.signal;this.el.addEventListener("mousemove",t=>{this.visible&&this.running&&this.interactive&&this.dropAtPointer(t,1*this.dropRadius,e())},{signal:i}),this.el.addEventListener("touchmove",t,{signal:i,passive:!1}),this.el.addEventListener("touchstart",t,{signal:i,passive:!0}),this.el.addEventListener("mousedown",e=>{this.visible&&this.running&&this.interactive&&this.dropAtPointer(e,1.5*this.dropRadius,.14)},{signal:i})}loadImage(){gl=this.context;const e=this.imageUrl||extractUrl(this.originalCssBackgroundImage)||extractUrl(getComputedStyle(this.el).backgroundImage);if(e==this.imageSource)return;if(this.imageSource=e,!this.imageSource)return void this.setTransparentTexture();const t=new Image;t.onload=()=>{function e(e){return!(e&e-1)}gl=this.context;const i=e(t.width)&&e(t.height)?gl.REPEAT:gl.CLAMP_TO_EDGE;gl.bindTexture(gl.TEXTURE_2D,this.backgroundTexture),gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_S,i),gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_T,i),gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,gl.RGBA,gl.UNSIGNED_BYTE,t),this.backgroundWidth=t.width,this.backgroundHeight=t.height},t.onerror=()=>{gl=this.context,this.setTransparentTexture()},t.crossOrigin=isDataUri(this.imageSource)?null:this.crossOrigin,t.src=this.imageSource}step(){this.pendingInitialization||this.destroyed||(gl=this.context,gl&&this.canvas&&0!==this.canvas.width&&0!==this.canvas.height&&this.visible&&(this.computeTextureBoundaries(),this.running&&(this.update(),this.render())))}drawQuad(){gl.bindBuffer(gl.ARRAY_BUFFER,this.quad),gl.vertexAttribPointer(0,2,gl.FLOAT,!1,0,0),gl.drawArrays(gl.TRIANGLE_FAN,0,4)}render(){gl&&this.canvas&&0!==this.canvas.width&&0!==this.canvas.height&&(gl.bindFramebuffer(gl.FRAMEBUFFER,null),gl.viewport(0,0,this.canvas.width,this.canvas.height),gl.enable(gl.BLEND),gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT),gl.useProgram(this.renderProgram.id),bindTexture(this.backgroundTexture,0),bindTexture(this.textures[0],1),gl.uniform1f(this.renderProgram.locations.perturbance,this.perturbance),gl.uniform2fv(this.renderProgram.locations.topLeft,this.renderProgram.uniforms.topLeft),gl.uniform2fv(this.renderProgram.locations.bottomRight,this.renderProgram.uniforms.bottomRight),gl.uniform2fv(this.renderProgram.locations.containerRatio,this.renderProgram.uniforms.containerRatio),gl.uniform1i(this.renderProgram.locations.samplerBackground,0),gl.uniform1i(this.renderProgram.locations.samplerRipples,1),this.drawQuad(),gl.disable(gl.BLEND))}update(){gl&&0!==this.resolution&&(gl.viewport(0,0,this.resolution,this.resolution),gl.bindFramebuffer(gl.FRAMEBUFFER,this.framebuffers[this.bufferWriteIndex]),bindTexture(this.textures[this.bufferReadIndex]),gl.useProgram(this.updateProgram.id),gl.uniform1f(this.updateProgram.locations.wavePropagation,this.options.wavePropagation),gl.uniform1f(this.updateProgram.locations.dampening,this.options.dampening),this.drawQuad(),this.swapBufferIndices())}swapBufferIndices(){this.bufferWriteIndex=1-this.bufferWriteIndex,this.bufferReadIndex=1-this.bufferReadIndex}computeTextureBoundaries(){const e=getComputedStyle(this.el),t=e.backgroundSize,i=e.backgroundAttachment,r=translateBackgroundPosition(e.backgroundPosition);let n,s,o;if("fixed"==i)n={left:window.pageXOffset,top:window.pageYOffset},n.width=window.innerWidth,n.height=window.innerHeight;else{const e=this.el.getBoundingClientRect();n={left:e.left+window.pageXOffset,top:e.top+window.pageYOffset},n.width=this.el.clientWidth,n.height=this.el.clientHeight}if("cover"==t){const e=Math.max(n.width/this.backgroundWidth,n.height/this.backgroundHeight);s=this.backgroundWidth*e,o=this.backgroundHeight*e}else if("contain"==t){const e=Math.min(n.width/this.backgroundWidth,n.height/this.backgroundHeight);s=this.backgroundWidth*e,o=this.backgroundHeight*e}else{const e=t.split(" ");s=e[0]||"",o=e[1]||s,isPercentage(s)?s=n.width*parseFloat(s)/100:"auto"!=s&&(s=parseFloat(s)),isPercentage(o)?o=n.height*parseFloat(o)/100:"auto"!=o&&(o=parseFloat(o)),"auto"==s&&"auto"==o?(s=this.backgroundWidth,o=this.backgroundHeight):("auto"==s&&(s=this.backgroundWidth*(o/this.backgroundHeight)),"auto"==o&&(o=this.backgroundHeight*(s/this.backgroundWidth)))}let a=r[0],l=r[1];a=isPercentage(a)?n.left+(n.width-s)*parseFloat(a)/100:n.left+parseFloat(a),l=isPercentage(l)?n.top+(n.height-o)*parseFloat(l)/100:n.top+parseFloat(l);const g=this.el.getBoundingClientRect(),h=g.left+window.pageXOffset,d=g.top+window.pageYOffset;this.renderProgram.uniforms.topLeft=new Float32Array([(h-a)/s,(d-l)/o]),this.renderProgram.uniforms.bottomRight=new Float32Array([this.renderProgram.uniforms.topLeft[0]+this.el.clientWidth/s,this.renderProgram.uniforms.topLeft[1]+this.el.clientHeight/o]);const c=Math.max(this.canvas.width,this.canvas.height);this.renderProgram.uniforms.containerRatio=new Float32Array([this.canvas.width/c,this.canvas.height/c])}initShaders(){if(!gl)return;const e=["attribute vec2 vertex;","varying vec2 coord;","void main() {","coord = vertex * 0.5 + 0.5;","gl_Position = vec4(vertex, 0.0, 1.0);","}"].join("\n");this.dropProgram=createProgram(e,["precision highp float;","const float PI = 3.141592653589793;","uniform sampler2D texture;","uniform vec2 center;","uniform float radius;","uniform float strength;","varying vec2 coord;","void main() {","vec4 info = texture2D(texture, coord);","float drop = max(0.0, 1.0 - length(center * 0.5 + 0.5 - coord) / radius);","drop = 0.5 - cos(drop * PI) * 0.5;","info.r += drop * strength;","gl_FragColor = info;","}"].join("\n")),this.updateProgram=createProgram(e,["precision highp float;","uniform sampler2D texture;","uniform vec2 delta;","uniform float wavePropagation;","uniform float dampening;","varying vec2 coord;","void main() {","vec4 info = texture2D(texture, coord);","vec2 dx = vec2(delta.x, 0.0);","vec2 dy = vec2(0.0, delta.y);","// Detect edges and apply boundary conditions","float edgeFactor = 1.0;","if (coord.x <= delta.x || coord.x >= 1.0 - delta.x ||"," coord.y <= delta.y || coord.y >= 1.0 - delta.y) {"," edgeFactor = 0.95; // Dampen waves near edges more aggressively","}","float average = (","texture2D(texture, coord - dx).r +","texture2D(texture, coord - dy).r +","texture2D(texture, coord + dx).r +","texture2D(texture, coord + dy).r",") * 0.25;","// Wave propagation and dampening with configurable parameters","info.g += (average - info.r) * wavePropagation;","info.g *= dampening * edgeFactor;","info.r += info.g;","// Clamp values to prevent overflow artifacts","info.r = clamp(info.r, -1.0, 1.0);","info.g = clamp(info.g, -1.0, 1.0);","gl_FragColor = info;","}"].join("\n")),gl.uniform2fv(this.updateProgram.locations.delta,this.textureDelta),gl.uniform1f(this.updateProgram.locations.wavePropagation,this.options.wavePropagation),gl.uniform1f(this.updateProgram.locations.dampening,this.options.dampening),this.renderProgram=createProgram(["precision highp float;","attribute vec2 vertex;","uniform vec2 topLeft;","uniform vec2 bottomRight;","uniform vec2 containerRatio;","varying vec2 ripplesCoord;","varying vec2 backgroundCoord;","void main() {","backgroundCoord = mix(topLeft, bottomRight, vertex * 0.5 + 0.5);","backgroundCoord.y = 1.0 - backgroundCoord.y;","ripplesCoord = vec2(vertex.x, -vertex.y) * containerRatio * 0.5 + 0.5;","gl_Position = vec4(vertex.x, -vertex.y, 0.0, 1.0);","}"].join("\n"),["precision highp float;","uniform sampler2D samplerBackground;","uniform sampler2D samplerRipples;","uniform vec2 delta;","uniform float perturbance;","varying vec2 ripplesCoord;","varying vec2 backgroundCoord;","void main() {","float height = texture2D(samplerRipples, ripplesCoord).r;","// Sample neighboring pixels for calculating normals","vec2 texelSize = delta;","float heightX = texture2D(samplerRipples, clamp(ripplesCoord + vec2(texelSize.x, 0.0), 0.0, 1.0)).r;","float heightY = texture2D(samplerRipples, clamp(ripplesCoord + vec2(0.0, texelSize.y), 0.0, 1.0)).r;","vec3 dx = vec3(texelSize.x, heightX - height, 0.0);","vec3 dy = vec3(0.0, heightY - height, texelSize.y);","vec2 offset = -normalize(cross(dy, dx)).xz;","// Apply edge fading to reduce artifacts at boundaries","float edgeFade = 1.0;","float edgeDistance = 0.05;","edgeFade *= smoothstep(0.0, edgeDistance, ripplesCoord.x);","edgeFade *= smoothstep(0.0, edgeDistance, ripplesCoord.y);","edgeFade *= smoothstep(0.0, edgeDistance, 1.0 - ripplesCoord.x);","edgeFade *= smoothstep(0.0, edgeDistance, 1.0 - ripplesCoord.y);","float specular = pow(max(0.0, dot(offset, normalize(vec2(-0.6, 1.0)))), 4.0) * edgeFade;","gl_FragColor = texture2D(samplerBackground, backgroundCoord + offset * perturbance * edgeFade) + specular;","}"].join("\n")),gl.uniform2fv(this.renderProgram.locations.delta,this.textureDelta)}initTexture(){gl&&(this.backgroundTexture=gl.createTexture(),gl.bindTexture(gl.TEXTURE_2D,this.backgroundTexture),gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL,1),gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.LINEAR),gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.LINEAR))}setTransparentTexture(){gl&&(gl.bindTexture(gl.TEXTURE_2D,this.backgroundTexture),gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,gl.RGBA,gl.UNSIGNED_BYTE,transparentPixels))}dropAtPointer(e,t,i){if(!this.canvas||this.pendingInitialization)return;const r=this.canvas.getBoundingClientRect();let n,s;void 0!==e.clientX?(n=e.clientX-r.left,s=e.clientY-r.top):(n=e.pageX-(r.left+window.pageXOffset),s=e.pageY-(r.top+window.pageYOffset)),n<0||n>r.width||s<0||s>r.height||(this.canvas.width===r.width&&this.canvas.height===r.height||(n*=this.canvas.width/r.width,s*=this.canvas.height/r.height),this.drop(n,s,t,i))}drop(e,t,i,r){if(gl=this.context,!gl)return;const n=this.el.clientWidth,s=this.el.clientHeight,o=Math.max(n,s);i/=o;const a=new Float32Array([(2*e-n)/o,(s-2*t)/o]);gl.viewport(0,0,this.resolution,this.resolution),gl.bindFramebuffer(gl.FRAMEBUFFER,this.framebuffers[this.bufferWriteIndex]),bindTexture(this.textures[this.bufferReadIndex]),gl.useProgram(this.dropProgram.id),gl.uniform2fv(this.dropProgram.locations.center,a),gl.uniform1f(this.dropProgram.locations.radius,i),gl.uniform1f(this.dropProgram.locations.strength,r),this.drawQuad(),this.swapBufferIndices()}updateSize(){const e=this.el.clientWidth,t=this.el.clientHeight;e===this.canvas.width&&t===this.canvas.height||(this.canvas.width=e,this.canvas.height=t)}destroy(){this.destroyed=!0,this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),this.intersectionObserver&&(this.intersectionObserver.disconnect(),this.intersectionObserver=null),this.visibilityCheckInterval&&(clearInterval(this.visibilityCheckInterval),this.visibilityCheckInterval=null),this.abortController&&this.abortController.abort(),this.canvas&&this.canvas.remove();const e=this.context;e&&(this.quad&&e.deleteBuffer(this.quad),this.dropProgram&&e.deleteProgram(this.dropProgram.id),this.updateProgram&&e.deleteProgram(this.updateProgram.id),this.renderProgram&&e.deleteProgram(this.renderProgram.id),this.backgroundTexture&&e.deleteTexture(this.backgroundTexture),this.textures&&this.textures.forEach(t=>e.deleteTexture(t)),this.framebuffers&&this.framebuffers.forEach(t=>e.deleteFramebuffer(t)),gl===e&&(gl=null),this.context=null)}pause(){this.destroyed||(this.running=!1,this.needsRender=!0,setTimeout(()=>{this.needsRender=!1},50))}play(){if(!this.destroyed&&(this.running=!0,!this.animationId)){const e=()=>{this.destroyed||(this.step(),this.running||this.needsRender?this.animationId=requestAnimationFrame(e):this.animationId=null)};this.animationId=requestAnimationFrame(e)}}reset(){if(this.destroyed||!gl)return;gl=this.context;const e=config?config.arrayType:null,t=e?new e(this.resolution*this.resolution*4):null;for(let e=0;e<2;e++)gl.bindTexture(gl.TEXTURE_2D,this.textures[e]),gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,this.resolution,this.resolution,0,gl.RGBA,config&&config.type||gl.UNSIGNED_BYTE,t)}set(e,t){if(!this.destroyed)switch(e){case"dropRadius":case"perturbance":case"interactive":case"crossOrigin":this[e]=t,this.options[e]=t;break;case"imageUrl":this.imageUrl=t,this.options.imageUrl=t,this.loadImage();break;case"resolution":console.warn("Ripples: Changing 'resolution' dynamically is not supported. Please re-initialize the Ripples instance.");break;default:console.warn(`Ripples: Property "${e}" is not a settable option.`)}}}export{Ripples};export default Ripples;export const isWebGLSupported=()=>!!isBrowser&&null!==config;
|
|
1
|
+
const isBrowser="undefined"!=typeof window&&"undefined"!=typeof document;let gl=null,config=null,transparentPixels=null;function isPercentage(e){return"%"===e[e.length-1]}function loadConfig(){if(!isBrowser)return null;const e=document.createElement("canvas");if(gl=e.getContext("webgl")||e.getContext("experimental-webgl"),!gl)return null;const t={};if(["OES_texture_float","OES_texture_half_float","OES_texture_float_linear","OES_texture_half_float_linear"].forEach(e=>{const i=gl.getExtension(e);i&&(t[e]=i)}),!t.OES_texture_float)return null;const i=[];function r(e,i,r){const n="OES_texture_"+e,s=n+"_linear",o=s in t,a=[n];return o&&a.push(s),{type:i,arrayType:r,linearSupport:o,extensions:a}}i.push(r("float",gl.FLOAT,Float32Array)),t.OES_texture_half_float&&i.push(r("half_float",t.OES_texture_half_float.HALF_FLOAT_OES,null));const n=gl.createTexture(),s=gl.createFramebuffer();gl.bindFramebuffer(gl.FRAMEBUFFER,s),gl.bindTexture(gl.TEXTURE_2D,n),gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.NEAREST),gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.NEAREST),gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_S,gl.CLAMP_TO_EDGE),gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_T,gl.CLAMP_TO_EDGE);let o=null;for(let e=0;e<i.length;e++)if(gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,32,32,0,gl.RGBA,i[e].type,null),gl.framebufferTexture2D(gl.FRAMEBUFFER,gl.COLOR_ATTACHMENT0,gl.TEXTURE_2D,n,0),gl.checkFramebufferStatus(gl.FRAMEBUFFER)===gl.FRAMEBUFFER_COMPLETE){o=i[e];break}return o}function createImageData(e,t){if(!isBrowser)return null;try{return new ImageData(e,t)}catch{return document.createElement("canvas").getContext("2d").createImageData(e,t)}}function translateBackgroundPosition(e){const t=e.split(" ");if(1!==t.length)return t.map(t=>{switch(e){case"center":return"50%";case"top":case"left":return"0";case"right":case"bottom":return"100%";default:return t}});switch(e){case"center":return["50%","50%"];case"top":return["50%","0"];case"bottom":return["50%","100%"];case"left":return["0","50%"];case"right":return["100%","50%"];default:return[e,"50%"]}}function createProgram(e,t){function i(e,t){const i=gl.createShader(e);if(gl.shaderSource(i,t),gl.compileShader(i),!gl.getShaderParameter(i,gl.COMPILE_STATUS))throw new Error("compile error: "+gl.getShaderInfoLog(i));return i}const r={};if(r.id=gl.createProgram(),gl.attachShader(r.id,i(gl.VERTEX_SHADER,e)),gl.attachShader(r.id,i(gl.FRAGMENT_SHADER,t)),gl.linkProgram(r.id),!gl.getProgramParameter(r.id,gl.LINK_STATUS))throw new Error("link error: "+gl.getProgramInfoLog(r.id));r.uniforms={},r.locations={},gl.useProgram(r.id),gl.enableVertexAttribArray(0);let n,s,o=/uniform (\w+) (\w+)/g,a=e+t;for(;null!=(n=o.exec(a));)s=n[2],r.locations[s]=gl.getUniformLocation(r.id,s);return r}function bindTexture(e,t){gl.activeTexture(gl.TEXTURE0+(t||0)),gl.bindTexture(gl.TEXTURE_2D,e)}function extractUrl(e){const t=/url\(["']?([^"']*)["']?\)/.exec(e);return null==t?null:t[1]}function isDataUri(e){return e.match(/^data:/)}isBrowser&&(config=loadConfig(),transparentPixels=createImageData(32,32));const DEFAULTS={imageUrl:null,dropRadius:15,perturbance:.04,resolution:512,interactive:!0,crossOrigin:"",wavePropagation:2,dampening:.997};class Ripples{constructor(e,t){if(!isBrowser)return console.warn("Ripples: Cannot initialize in non-browser environment"),void(this.destroyed=!0);if(this.el="string"==typeof e?document.querySelector(e):e,!this.el)return void console.error("Ripples: Element not found.",e);this.options={...DEFAULTS,...t},this.interactive=this.options.interactive,this.resolution=this.options.resolution,this.textureDelta=new Float32Array([1/this.resolution,1/this.resolution]),this.perturbance=this.options.perturbance,this.dropRadius=this.options.dropRadius,this.crossOrigin=this.options.crossOrigin,this.imageUrl=this.options.imageUrl;const i=this.el.clientWidth,r=this.el.clientHeight;if(0===i||0===r)return console.warn("Ripples: Element has zero dimensions. Deferring initialization."),this.pendingInitialization=!0,this.destroyed=!1,this.visible=!1,this.running=!1,this.inited=!1,void this.observeVisibility();const n=document.createElement("canvas");if(n.width=i,n.height=r,this.canvas=n,this.canvas.style.position="absolute",this.canvas.style.inset=0,0===n.width||0===n.height)return console.warn("Ripples: Canvas has zero dimensions. Deferring initialization."),this.pendingInitialization=!0,this.destroyed=!1,this.visible=!1,this.running=!1,this.inited=!1,void this.observeVisibility();if(this.el.append(n),this.context=gl=n.getContext("webgl")||n.getContext("experimental-webgl"),!this.context)return console.error("Ripples: WebGL is not supported by your browser or is disabled."),void(this.destroyed=!0);config&&config.extensions&&config.extensions.forEach(e=>{gl.getExtension(e)}),this.abortController=new AbortController;const s=this.signal=this.abortController.signal;this.updateSize=this.updateSize.bind(this),window.addEventListener("resize",this.updateSize,{signal:s}),this.textures=[],this.framebuffers=[],this.bufferWriteIndex=0,this.bufferReadIndex=1;const o=config?config.arrayType:null,a=o?new o(this.resolution*this.resolution*4):null;for(let e=0;e<2;e++){const e=gl.createTexture(),t=gl.createFramebuffer();gl.bindFramebuffer(gl.FRAMEBUFFER,t),gl.bindTexture(gl.TEXTURE_2D,e),gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,config&&config.linearSupport?gl.LINEAR:gl.NEAREST),gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,config&&config.linearSupport?gl.LINEAR:gl.NEAREST),gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_S,gl.CLAMP_TO_EDGE),gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_T,gl.CLAMP_TO_EDGE),gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,this.resolution,this.resolution,0,gl.RGBA,config&&config.type||gl.UNSIGNED_BYTE,a),gl.framebufferTexture2D(gl.FRAMEBUFFER,gl.COLOR_ATTACHMENT0,gl.TEXTURE_2D,e,0),this.textures.push(e),this.framebuffers.push(t)}this.quad=gl.createBuffer(),gl.bindBuffer(gl.ARRAY_BUFFER,this.quad),gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,1,1,-1,1]),gl.STATIC_DRAW),this.initShaders(),this.initTexture(),this.setTransparentTexture(),this.originalCssBackgroundImage=getComputedStyle(this.el).backgroundImage,this.originalInlineCss=this.el.style.backgroundImage,this.loadImage(),gl.clearColor(0,0,0,0),gl.blendFunc(gl.SRC_ALPHA,gl.ONE_MINUS_SRC_ALPHA),this.visible=!0,this.running=!0,this.inited=!0,this.destroyed=!1,this.setupPointerEvents(),this.animationId=null;const l=()=>{this.destroyed||(this.step(),this.running||this.needsRender?this.animationId=requestAnimationFrame(l):this.animationId=null)};return this.animationId=requestAnimationFrame(l),this}observeVisibility(){isBrowser&&this.el&&("undefined"!=typeof ResizeObserver&&(this.resizeObserver=new ResizeObserver(()=>{if(this.pendingInitialization){const e=this.el.clientWidth,t=this.el.clientHeight;e>0&&t>0&&this.completeInitialization()}else this.inited&&this.canvas&&this.updateSize()}),this.resizeObserver.observe(this.el)),"undefined"!=typeof IntersectionObserver&&(this.intersectionObserver=new IntersectionObserver(e=>{e.forEach(e=>{if(e.isIntersecting&&this.pendingInitialization){const e=this.el.clientWidth,t=this.el.clientHeight;e>0&&t>0&&this.completeInitialization()}})}),this.intersectionObserver.observe(this.el)),this.resizeObserver||this.intersectionObserver||(this.visibilityCheckInterval=setInterval(()=>{if(this.pendingInitialization){const e=this.el.clientWidth,t=this.el.clientHeight;e>0&&t>0&&this.completeInitialization()}},500)))}completeInitialization(){if(!this.pendingInitialization||this.inited)return;console.log("Ripples: Element now visible, completing initialization."),this.pendingInitialization=!1,this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),this.intersectionObserver&&(this.intersectionObserver.disconnect(),this.intersectionObserver=null),this.visibilityCheckInterval&&(clearInterval(this.visibilityCheckInterval),this.visibilityCheckInterval=null);const e=this.options,t=this.el;Object.assign(this,new Ripples(t,e))}setupPointerEvents(){const e=()=>{const e=Math.max(this.el.clientWidth,this.el.clientHeight),t=Math.max(1,150/e);return Math.min(.04,.01*t)},t=t=>{if(this.visible&&this.running&&this.interactive){const i=t.changedTouches,r=e();for(let e=0;e<i.length;e++)this.dropAtPointer(i[e],1*this.dropRadius,r)}},i=this.signal;this.el.addEventListener("mousemove",t=>{this.visible&&this.running&&this.interactive&&this.dropAtPointer(t,1*this.dropRadius,e())},{signal:i}),this.el.addEventListener("touchmove",t,{signal:i,passive:!1}),this.el.addEventListener("touchstart",t,{signal:i,passive:!0}),this.el.addEventListener("mousedown",e=>{this.visible&&this.running&&this.interactive&&this.dropAtPointer(e,1.5*this.dropRadius,.14)},{signal:i})}loadImage(){gl=this.context;const e=this.imageUrl||extractUrl(this.originalCssBackgroundImage)||extractUrl(getComputedStyle(this.el).backgroundImage);if(e==this.imageSource)return;if(this.imageSource=e,!this.imageSource)return void this.setTransparentTexture();const t=new Image;t.onload=()=>{function e(e){return!(e&e-1)}gl=this.context;const i=e(t.width)&&e(t.height)?gl.REPEAT:gl.CLAMP_TO_EDGE;gl.bindTexture(gl.TEXTURE_2D,this.backgroundTexture),gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_S,i),gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_T,i),gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,gl.RGBA,gl.UNSIGNED_BYTE,t),this.backgroundWidth=t.width,this.backgroundHeight=t.height},t.onerror=()=>{gl=this.context,this.setTransparentTexture()},t.crossOrigin=isDataUri(this.imageSource)?null:this.crossOrigin,t.src=this.imageSource}step(){this.pendingInitialization||this.destroyed||(gl=this.context,gl&&this.canvas&&0!==this.canvas.width&&0!==this.canvas.height&&this.visible&&(this.computeTextureBoundaries(),this.running&&(this.update(),this.render())))}drawQuad(){gl.bindBuffer(gl.ARRAY_BUFFER,this.quad),gl.vertexAttribPointer(0,2,gl.FLOAT,!1,0,0),gl.drawArrays(gl.TRIANGLE_FAN,0,4)}render(){gl&&this.canvas&&0!==this.canvas.width&&0!==this.canvas.height&&(gl.bindFramebuffer(gl.FRAMEBUFFER,null),gl.viewport(0,0,this.canvas.width,this.canvas.height),gl.enable(gl.BLEND),gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT),gl.useProgram(this.renderProgram.id),bindTexture(this.backgroundTexture,0),bindTexture(this.textures[0],1),gl.uniform1f(this.renderProgram.locations.perturbance,this.perturbance),gl.uniform2fv(this.renderProgram.locations.topLeft,this.renderProgram.uniforms.topLeft),gl.uniform2fv(this.renderProgram.locations.bottomRight,this.renderProgram.uniforms.bottomRight),gl.uniform2fv(this.renderProgram.locations.containerRatio,this.renderProgram.uniforms.containerRatio),gl.uniform1i(this.renderProgram.locations.samplerBackground,0),gl.uniform1i(this.renderProgram.locations.samplerRipples,1),this.drawQuad(),gl.disable(gl.BLEND))}update(){gl&&0!==this.resolution&&(gl.viewport(0,0,this.resolution,this.resolution),gl.bindFramebuffer(gl.FRAMEBUFFER,this.framebuffers[this.bufferWriteIndex]),bindTexture(this.textures[this.bufferReadIndex]),gl.useProgram(this.updateProgram.id),gl.uniform1f(this.updateProgram.locations.wavePropagation,this.options.wavePropagation),gl.uniform1f(this.updateProgram.locations.dampening,this.options.dampening),this.drawQuad(),this.swapBufferIndices())}swapBufferIndices(){this.bufferWriteIndex=1-this.bufferWriteIndex,this.bufferReadIndex=1-this.bufferReadIndex}computeTextureBoundaries(){const e=getComputedStyle(this.el),t=e.backgroundSize,i=e.backgroundAttachment,r=translateBackgroundPosition(e.backgroundPosition);let n,s,o;if("fixed"==i)n={left:window.pageXOffset,top:window.pageYOffset},n.width=window.innerWidth,n.height=window.innerHeight;else{const e=this.el.getBoundingClientRect();n={left:e.left+window.pageXOffset,top:e.top+window.pageYOffset},n.width=this.el.clientWidth,n.height=this.el.clientHeight}if("cover"==t){const e=Math.max(n.width/this.backgroundWidth,n.height/this.backgroundHeight);s=this.backgroundWidth*e,o=this.backgroundHeight*e}else if("contain"==t){const e=Math.min(n.width/this.backgroundWidth,n.height/this.backgroundHeight);s=this.backgroundWidth*e,o=this.backgroundHeight*e}else{const e=t.split(" ");s=e[0]||"",o=e[1]||s,isPercentage(s)?s=n.width*parseFloat(s)/100:"auto"!=s&&(s=parseFloat(s)),isPercentage(o)?o=n.height*parseFloat(o)/100:"auto"!=o&&(o=parseFloat(o)),"auto"==s&&"auto"==o?(s=this.backgroundWidth,o=this.backgroundHeight):("auto"==s&&(s=this.backgroundWidth*(o/this.backgroundHeight)),"auto"==o&&(o=this.backgroundHeight*(s/this.backgroundWidth)))}let a=r[0],l=r[1];a=isPercentage(a)?n.left+(n.width-s)*parseFloat(a)/100:n.left+parseFloat(a),l=isPercentage(l)?n.top+(n.height-o)*parseFloat(l)/100:n.top+parseFloat(l);const g=this.el.getBoundingClientRect(),h=g.left+window.pageXOffset,d=g.top+window.pageYOffset;this.renderProgram.uniforms.topLeft=new Float32Array([(h-a)/s,(d-l)/o]),this.renderProgram.uniforms.bottomRight=new Float32Array([this.renderProgram.uniforms.topLeft[0]+this.el.clientWidth/s,this.renderProgram.uniforms.topLeft[1]+this.el.clientHeight/o]);const c=Math.max(this.canvas.width,this.canvas.height);this.renderProgram.uniforms.containerRatio=new Float32Array([this.canvas.width/c,this.canvas.height/c])}initShaders(){if(!gl)return;const e=["attribute vec2 vertex;","varying vec2 coord;","void main() {","coord = vertex * 0.5 + 0.5;","gl_Position = vec4(vertex, 0.0, 1.0);","}"].join("\n");this.dropProgram=createProgram(e,["precision highp float;","const float PI = 3.141592653589793;","uniform sampler2D texture;","uniform vec2 center;","uniform float radius;","uniform float strength;","varying vec2 coord;","void main() {","vec4 info = texture2D(texture, coord);","float drop = max(0.0, 1.0 - length(center * 0.5 + 0.5 - coord) / radius);","drop = 0.5 - cos(drop * PI) * 0.5;","info.r += drop * strength;","gl_FragColor = info;","}"].join("\n")),this.updateProgram=createProgram(e,["precision highp float;","uniform sampler2D texture;","uniform vec2 delta;","uniform float wavePropagation;","uniform float dampening;","varying vec2 coord;","void main() {","vec4 info = texture2D(texture, coord);","vec2 dx = vec2(delta.x, 0.0);","vec2 dy = vec2(0.0, delta.y);","// Detect edges and apply boundary conditions","float edgeFactor = 1.0;","if (coord.x <= delta.x || coord.x >= 1.0 - delta.x ||"," coord.y <= delta.y || coord.y >= 1.0 - delta.y) {"," edgeFactor = 0.95; // Dampen waves near edges more aggressively","}","float average = (","texture2D(texture, coord - dx).r +","texture2D(texture, coord - dy).r +","texture2D(texture, coord + dx).r +","texture2D(texture, coord + dy).r",") * 0.25;","// Wave propagation and dampening with configurable parameters","info.g += (average - info.r) * wavePropagation;","info.g *= dampening * edgeFactor;","info.r += info.g;","// Clamp values to prevent overflow artifacts","info.r = clamp(info.r, -1.0, 1.0);","info.g = clamp(info.g, -1.0, 1.0);","gl_FragColor = info;","}"].join("\n")),gl.uniform2fv(this.updateProgram.locations.delta,this.textureDelta),gl.uniform1f(this.updateProgram.locations.wavePropagation,this.options.wavePropagation),gl.uniform1f(this.updateProgram.locations.dampening,this.options.dampening),this.renderProgram=createProgram(["precision highp float;","attribute vec2 vertex;","uniform vec2 topLeft;","uniform vec2 bottomRight;","uniform vec2 containerRatio;","varying vec2 ripplesCoord;","varying vec2 backgroundCoord;","void main() {","backgroundCoord = mix(topLeft, bottomRight, vertex * 0.5 + 0.5);","backgroundCoord.y = 1.0 - backgroundCoord.y;","ripplesCoord = vec2(vertex.x, -vertex.y) * containerRatio * 0.5 + 0.5;","gl_Position = vec4(vertex.x, -vertex.y, 0.0, 1.0);","}"].join("\n"),["precision highp float;","uniform sampler2D samplerBackground;","uniform sampler2D samplerRipples;","uniform vec2 delta;","uniform float perturbance;","varying vec2 ripplesCoord;","varying vec2 backgroundCoord;","void main() {","float height = texture2D(samplerRipples, ripplesCoord).r;","// Sample neighboring pixels for calculating normals","vec2 texelSize = delta;","float heightX = texture2D(samplerRipples, clamp(ripplesCoord + vec2(texelSize.x, 0.0), 0.0, 1.0)).r;","float heightY = texture2D(samplerRipples, clamp(ripplesCoord + vec2(0.0, texelSize.y), 0.0, 1.0)).r;","vec3 dx = vec3(texelSize.x, heightX - height, 0.0);","vec3 dy = vec3(0.0, heightY - height, texelSize.y);","vec2 offset = -normalize(cross(dy, dx)).xz;","// Apply edge fading to reduce artifacts at boundaries","float edgeFade = 1.0;","float edgeDistance = 0.05;","edgeFade *= smoothstep(0.0, edgeDistance, ripplesCoord.x);","edgeFade *= smoothstep(0.0, edgeDistance, ripplesCoord.y);","edgeFade *= smoothstep(0.0, edgeDistance, 1.0 - ripplesCoord.x);","edgeFade *= smoothstep(0.0, edgeDistance, 1.0 - ripplesCoord.y);","float specular = pow(max(0.0, dot(offset, normalize(vec2(-0.6, 1.0)))), 4.0) * edgeFade;","gl_FragColor = texture2D(samplerBackground, backgroundCoord + offset * perturbance * edgeFade) + specular;","}"].join("\n")),gl.uniform2fv(this.renderProgram.locations.delta,this.textureDelta)}initTexture(){gl&&(this.backgroundTexture=gl.createTexture(),gl.bindTexture(gl.TEXTURE_2D,this.backgroundTexture),gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL,1),gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.LINEAR),gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.LINEAR))}setTransparentTexture(){gl&&(gl.bindTexture(gl.TEXTURE_2D,this.backgroundTexture),gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,gl.RGBA,gl.UNSIGNED_BYTE,transparentPixels))}dropAtPointer(e,t,i){if(!this.canvas||this.pendingInitialization)return;const r=this.canvas.getBoundingClientRect();let n,s;void 0!==e.clientX?(n=e.clientX-r.left,s=e.clientY-r.top):(n=e.pageX-(r.left+window.pageXOffset),s=e.pageY-(r.top+window.pageYOffset)),n<0||n>r.width||s<0||s>r.height||(this.canvas.width===r.width&&this.canvas.height===r.height||(n*=this.canvas.width/r.width,s*=this.canvas.height/r.height),this.drop(n,s,t,i))}drop(e,t,i,r){if(gl=this.context,!gl)return;const n=this.el.clientWidth,s=this.el.clientHeight,o=Math.max(n,s);i/=o;const a=new Float32Array([(2*e-n)/o,(s-2*t)/o]);gl.viewport(0,0,this.resolution,this.resolution),gl.bindFramebuffer(gl.FRAMEBUFFER,this.framebuffers[this.bufferWriteIndex]),bindTexture(this.textures[this.bufferReadIndex]),gl.useProgram(this.dropProgram.id),gl.uniform2fv(this.dropProgram.locations.center,a),gl.uniform1f(this.dropProgram.locations.radius,i),gl.uniform1f(this.dropProgram.locations.strength,r),this.drawQuad(),this.swapBufferIndices()}updateSize(){const e=this.el.clientWidth,t=this.el.clientHeight;e===this.canvas.width&&t===this.canvas.height||(this.canvas.width=e,this.canvas.height=t)}destroy(){this.destroyed=!0,this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),this.intersectionObserver&&(this.intersectionObserver.disconnect(),this.intersectionObserver=null),this.visibilityCheckInterval&&(clearInterval(this.visibilityCheckInterval),this.visibilityCheckInterval=null),this.abortController&&this.abortController.abort(),this.canvas&&this.canvas.remove();const e=this.context;e&&(this.quad&&e.deleteBuffer(this.quad),this.dropProgram&&e.deleteProgram(this.dropProgram.id),this.updateProgram&&e.deleteProgram(this.updateProgram.id),this.renderProgram&&e.deleteProgram(this.renderProgram.id),this.backgroundTexture&&e.deleteTexture(this.backgroundTexture),this.textures&&this.textures.forEach(t=>e.deleteTexture(t)),this.framebuffers&&this.framebuffers.forEach(t=>e.deleteFramebuffer(t)),gl===e&&(gl=null),this.context=null)}pause(){this.destroyed||(this.running=!1,this.needsRender=!0,setTimeout(()=>{this.needsRender=!1},50))}play(){if(!this.destroyed&&(this.running=!0,!this.animationId)){const e=()=>{this.destroyed||(this.step(),this.running||this.needsRender?this.animationId=requestAnimationFrame(e):this.animationId=null)};this.animationId=requestAnimationFrame(e)}}reset(){if(this.destroyed||!gl)return;gl=this.context;const e=config?config.arrayType:null,t=e?new e(this.resolution*this.resolution*4):null;for(let e=0;e<2;e++)gl.bindTexture(gl.TEXTURE_2D,this.textures[e]),gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,this.resolution,this.resolution,0,gl.RGBA,config&&config.type||gl.UNSIGNED_BYTE,t)}set(e,t){if(!this.destroyed)switch(e){case"dropRadius":case"perturbance":case"interactive":case"crossOrigin":this[e]=t,this.options[e]=t;break;case"imageUrl":this.imageUrl=t,this.options.imageUrl=t,this.loadImage();break;case"resolution":console.warn("Ripples: Changing 'resolution' dynamically is not supported. Please re-initialize the Ripples instance.");break;default:console.warn(`Ripples: Property "${e}" is not a settable option.`)}}}export{Ripples};
|